clash-linux commited on
Commit
5952dec
·
verified ·
1 Parent(s): 6ee5fda

Upload 51 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
.env ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 服务端口
2
+ PORT=3010
3
+
4
+ # 日志格式 (tiny, combined, common, dev, short)
5
+ MORGAN_FORMAT=tiny
6
+
7
+ # API Key与Cookie映射关系 (JSON格式)
8
+ # 格式: {"自定义API Key": "Cookie值"} 或 {"自定义API Key": ["Cookie值1", "Cookie值2"]}
9
+ API_KEYS={"sk-123":[]}
10
+
11
+ # 轮询策略 (random 或 round-robin 或 default)
12
+ ROTATION_STRATEGY=default
13
+
14
+ # 是否使用TLS代理 (true 或 false)
15
+ USE_TLS_PROXY=true
16
+
17
+ # 是否使用辅助代理服务器 (true 或 false)
18
+ USE_OTHERS_PROXY=true
19
+
20
+ # 代理服务器平台
21
+ # 可选值: auto, windows_x64, linux_x64, android_arm64
22
+ # auto: 自动检测平台
23
+ # windows_x64: Windows 64位
24
+ # linux_x64: Linux 64位
25
+ # android_arm64: 安卓ARM 64位
26
+ PROXY_PLATFORM=auto
27
+
28
+ # 是否使用其它接口 (true 或 false)
29
+ USE_OTHERS=true
.gitattributes CHANGED
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ src/proxy/cursor_proxy_server_linux_amd64 filter=lfs diff=lfs merge=lfs -text
37
+ src/proxy/others/cursor_proxy_server_linux_amd64 filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ lerna-debug.log*
8
+ .pnpm-debug.log*
9
+
10
+ # Diagnostic reports (https://nodejs.org/api/report.html)
11
+ report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
12
+
13
+ # Runtime data
14
+ pids
15
+ *.pid
16
+ *.seed
17
+ *.pid.lock
18
+
19
+ # Directory for instrumented libs generated by jscoverage/JSCover
20
+ lib-cov
21
+
22
+ # Coverage directory used by tools like istanbul
23
+ coverage
24
+ *.lcov
25
+
26
+ # nyc test coverage
27
+ .nyc_output
28
+
29
+ # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
30
+ .grunt
31
+
32
+ # Bower dependency directory (https://bower.io/)
33
+ bower_components
34
+
35
+ # node-waf configuration
36
+ .lock-wscript
37
+
38
+ # Compiled binary addons (https://nodejs.org/api/addons.html)
39
+ build/Release
40
+
41
+ # Dependency directories
42
+ node_modules/
43
+ jspm_packages/
44
+
45
+ # Snowpack dependency directory (https://snowpack.dev/)
46
+ web_modules/
47
+
48
+ # TypeScript cache
49
+ *.tsbuildinfo
50
+
51
+ # Optional npm cache directory
52
+ .npm
53
+
54
+ # Optional eslint cache
55
+ .eslintcache
56
+
57
+ # Optional stylelint cache
58
+ .stylelintcache
59
+
60
+ # Microbundle cache
61
+ .rpt2_cache/
62
+ .rts2_cache_cjs/
63
+ .rts2_cache_es/
64
+ .rts2_cache_umd/
65
+
66
+ # Optional REPL history
67
+ .node_repl_history
68
+
69
+ # Output of 'npm pack'
70
+ *.tgz
71
+
72
+ # Yarn Integrity file
73
+ .yarn-integrity
74
+
75
+ # dotenv environment variable files
76
+ .env
77
+ .env.development.local
78
+ .env.test.local
79
+ .env.production.local
80
+ .env.local
81
+
82
+ # parcel-bundler cache (https://parceljs.org/)
83
+ .cache
84
+ .parcel-cache
85
+
86
+ # Next.js build output
87
+ .next
88
+ out
89
+
90
+ # Nuxt.js build / generate output
91
+ .nuxt
92
+ dist
93
+
94
+ # Gatsby files
95
+ .cache/
96
+ # Comment in the public line in if your project uses Gatsby and not Next.js
97
+ # https://nextjs.org/blog/next-9-1#public-directory-support
98
+ # public
99
+
100
+ # vuepress build output
101
+ .vuepress/dist
102
+
103
+ # vuepress v2.x temp and cache directory
104
+ .temp
105
+ .cache
106
+
107
+ # Docusaurus cache and generated files
108
+ .docusaurus
109
+
110
+ # Serverless directories
111
+ .serverless/
112
+
113
+ # FuseBox cache
114
+ .fusebox/
115
+
116
+ # DynamoDB Local files
117
+ .dynamodb/
118
+
119
+ # TernJS port file
120
+ .tern-port
121
+
122
+ # Stores VSCode versions used for testing VSCode extensions
123
+ .vscode-test
124
+
125
+ # yarn v2
126
+ .yarn/cache
127
+ .yarn/unplugged
128
+ .yarn/build-state.yml
129
+ .yarn/install-state.gz
130
+ .pnp.*
131
+
132
+ data/admin.json
133
+ data/api_keys.json
134
+ data/invalid_cookies.json
135
+ .env.*
136
+
137
+ # 测试脚本
138
+ test-*.js
139
+ src/utils/test-*.js
140
+
141
+ *.bak
Dockerfile ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:lts-alpine
2
+
3
+ WORKDIR /app
4
+
5
+ COPY package.json package-lock.json ./
6
+
7
+ RUN npm install
8
+
9
+ COPY . /app
10
+
11
+ EXPOSE 3010
12
+
13
+ CMD ["npm", "run", "start"]
LICENSE ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright 2024 liuw1535
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
README.en.md ADDED
@@ -0,0 +1,209 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Cursor-To-OpenAI-Nexus
2
+
3
+ [中文](README.md) | English
4
+
5
+ Forward Cursor API requests to OpenAI, with support for multiple API Keys rotation.
6
+
7
+ ## Features
8
+
9
+ - 🔑 **Multiple Keys Rotation**: Configure multiple API Keys rotation to improve availability
10
+ - 🚀 **Easy Configuration**: One-click configuration script for quick setup
11
+ - 📊 **Status Monitoring**: Monitor API Key usage status
12
+ - 🔧 **Easy Maintenance**: Convenient maintenance scripts to simplify daily operations
13
+
14
+ ## 🚀 Basic Installation
15
+ ### Clone Repository
16
+ ```
17
+ git clone https://github.com/liuw1535/cursor-to-openai-nexus.git
18
+ ```
19
+ ### Enter Project Directory
20
+ ```
21
+ cd cursor-to-openai-nexus
22
+ ```
23
+ ### Install Dependencies
24
+ ```
25
+ npm install
26
+ ```
27
+
28
+ ## ⚙️ Configure Project
29
+ ```
30
+ npm run setup
31
+ ```
32
+ - Just fill in the custom key and whether to enable TLS proxy server
33
+ - Other options can be skipped by pressing Enter or filled in randomly
34
+ - 🛡️ If you frequently encounter account blocking issues, it's recommended to enable TLS server
35
+ - If you're not satisfied with the configuration, you can re-run this command to modify it
36
+
37
+ ## 🏃 Start Service
38
+ ```
39
+ npm start
40
+ ```
41
+
42
+ ## 🔍 Usage
43
+ 1. Access the management interface: `http://127.0.0.1:3010`
44
+ 2. Use the blue button at the bottom of the page to get cookies
45
+ 3. Configure in the Tavern page:
46
+ - API address: `http://127.0.0.1:3010/v1`
47
+ - Key: `sk-text` (if "text" was entered during configuration)
48
+
49
+ ## 📧 Account Registration Recommendations
50
+ - Recommended to use domain email (subdomain email is better)
51
+ - Search for "cloudfare domain email" for configuration tutorials
52
+ - ⚠️ Register no more than 2 accounts at a time to avoid being blocked
53
+
54
+ ## 🛠️ Common Commands
55
+ ```
56
+ npm start # Start project
57
+ npm run setup # Modify configuration
58
+ ```
59
+
60
+ ## Environment Configuration
61
+
62
+ Configure the following key parameters in the `.env` file:
63
+
64
+ - `API_KEYS`: Mapping relationship between API Key and Cookie (JSON format)
65
+ - `USE_TLS_PROXY`: (true) Enable TLS server, which can avoid request blocking issues
66
+ - `PROXY_PLATFORM`: The platform corresponding to the TLS server when enabled, default is auto detection
67
+
68
+ The system will automatically merge API Keys from `.env` and `data/api_keys.json` at startup to ensure data consistency.
69
+
70
+ ## Deployment Method
71
+
72
+ ### Using Docker Compose
73
+
74
+ ```bash
75
+ # Create configuration files
76
+ cp .env.example .env
77
+ mkdir -p data
78
+ cp data/admin.example.json data/admin.json
79
+
80
+ # Create admin account
81
+ node scripts/create-admin.js
82
+
83
+ # Start service
84
+ docker compose up -d --build
85
+
86
+ # View logs
87
+ docker compose logs -f
88
+
89
+ # Stop service
90
+ docker compose down
91
+ ```
92
+
93
+ ## API Usage Example
94
+
95
+ ### Python Example
96
+
97
+ ```python
98
+ from openai import OpenAI
99
+
100
+ # Use custom API Key
101
+ client = OpenAI(api_key="your_custom_api_key",
102
+ base_url="http://localhost:3010/v1")
103
+
104
+ # Or use Cookie directly
105
+ # client = OpenAI(api_key="user_...",
106
+ # base_url="http://localhost:3010/v1")
107
+
108
+ response = client.chat.completions.create(
109
+ model="claude-3-7-sonnet",
110
+ messages=[
111
+ {"role": "user", "content": "Hello."},
112
+ ],
113
+ stream=False
114
+ )
115
+
116
+ print(response.choices)
117
+ ```
118
+
119
+ ## Notes
120
+
121
+ - Please keep your WorkosCursorSessionToken secure
122
+ - This project is for learning and research purposes only, please comply with Cursor's terms of use
123
+
124
+ ## Acknowledgements
125
+
126
+ - This project is based on [cursor-api](https://github.com/zhx47/cursor-api) (by zhx47)
127
+ - Integrated content from [cursor-api](https://github.com/lvguanjun/cursor-api) (by lvguanjun)
128
+
129
+ # Logging System
130
+
131
+ The project integrates a unified logging system, which can be configured through the following methods:
132
+
133
+ ## Log Level Configuration
134
+
135
+ 1. Set environment variables in the `.env` file
136
+ ```
137
+ LOG_LEVEL=INFO
138
+ LOG_FORMAT=colored
139
+ LOG_TO_FILE=true
140
+ LOG_MAX_SIZE=10
141
+ LOG_MAX_FILES=10
142
+ ```
143
+ 2. Specify environment variables in the startup command, for example: `LOG_LEVEL=DEBUG npm start`
144
+
145
+ Supported log levels include:
146
+ - ERROR: Only display error messages
147
+ - WARN: Display warning and error messages
148
+ - INFO: Display general information, warnings, and error messages (default)
149
+ - DEBUG: Display debug information, general information, warnings, and error messages
150
+ - TRACE: Display all log information
151
+
152
+ ## Log Format
153
+
154
+ The log format is: `[LEVEL] timestamp log content`, with different levels displayed in different colors for easy differentiation:
155
+ - ERROR: Red
156
+ - WARN: Yellow
157
+ - INFO: Green
158
+ - DEBUG: Blue
159
+ - TRACE: Cyan
160
+ - HTTP: Cyan (dedicated to HTTP request logs)
161
+
162
+ ## HTTP Request Logs
163
+
164
+ The project uses the Morgan middleware to record HTTP requests, integrated into the unified logging system:
165
+
166
+ 1. Set HTTP log format in the `.env` file:
167
+ ```
168
+ # Options: tiny, combined, common, dev, short
169
+ MORGAN_FORMAT=tiny
170
+ ```
171
+
172
+ 2. HTTP logs will be displayed with the `[HTTP]` prefix, highlighted in cyan for easy identification
173
+
174
+ 3. Morgan format options explanation:
175
+ - `tiny`: The most concise format, including only method, URL, status code, response time
176
+ - `combined`: Standard Apache combined log format, including IP, time, request, status code, response size, referrer, user-agent
177
+ - `common`: Standard Apache common log format, similar to combined but without referrer and user-agent
178
+ - `dev`: Developer-friendly colored format, including method, URL, status code (with color), response time
179
+ - `short`: Shorter format, including method, URL, status code, response time, response size
180
+
181
+ ## File Logs
182
+
183
+ The project supports outputting logs to both console and files, which can be enabled with the following configuration:
184
+
185
+ 1. Set in the `.env` file: `LOG_TO_FILE=true`
186
+ 2. Optional configuration:
187
+ - `LOG_MAX_SIZE`: Maximum size of log file, in MB, default 10MB
188
+ - `LOG_MAX_FILES`: Number of historical log files to keep, default 10
189
+
190
+ Log files are stored in the `logs` folder in the project root directory:
191
+ - Current log file: `app.log`
192
+ - Historical log file: `app-2023-05-05T12-45-30-000Z.log`
193
+
194
+ File logs will automatically rotate, creating a new log file when the log file size exceeds the set value and keeping the most recent N files.
195
+
196
+ ## Usage in Code
197
+
198
+ Different log levels can be used as needed in the code:
199
+
200
+ ```javascript
201
+ const logger = require('./utils/logger');
202
+
203
+ logger.error('This is an error message');
204
+ logger.warn('This is a warning message');
205
+ logger.info('This is a general information message');
206
+ logger.debug('This is a debug message');
207
+ logger.trace('This is a trace message');
208
+ logger.http('This is an HTTP request log');
209
+ ```
README.md CHANGED
@@ -1,11 +1,228 @@
1
- ---
2
- title: Cursor
3
- emoji: 🏢
4
- colorFrom: purple
5
- colorTo: yellow
6
- sdk: docker
7
- pinned: false
8
- license: mit
9
- ---
10
-
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Cursor-To-OpenAI-Nexus
3
+ emoji: 🚀
4
+ colorFrom: blue
5
+ colorTo: green
6
+ sdk: docker
7
+ app_port: 3010
8
+ ---
9
+ # Cursor-To-OpenAI-Nexus
10
+
11
+ [English](README.en.md) | 中文
12
+
13
+ 将Cursor的API请求转发到OpenAI,支持多个API Key轮询。
14
+
15
+ ## 项目特点
16
+
17
+ - 🔑 **多Key轮询**: 配置多个API Key轮询,提高可用性
18
+ - 🚀 **简易配置**: 一键配置脚本,快速搭建环境
19
+ - 📊 **状态监控**: 查看API Key使用情况
20
+ - 🔧 **易于维护**: 便捷的维护脚本,简化日常操作
21
+
22
+ ## 🚀 基础安装
23
+ ### 克隆项目
24
+ ```
25
+ git clone https://github.com/liuw1535/cursor-to-openai-nexus.git
26
+ ```
27
+ ### 进入项目
28
+ ```
29
+ cd cursor-to-openai-nexus
30
+ ```
31
+ ### 安装依赖
32
+ ```
33
+ npm install
34
+ ```
35
+
36
+ ## ⚙️ 配置项目
37
+ ```
38
+ npm run setup
39
+ ```
40
+ - 只需填写自定义密钥和是否启用TLS伪造代理服务器
41
+ - 其他选项可直接回车跳过或随意填写
42
+ - 🛡️ 如频繁遇到封号问题,建议启用TLS服务器
43
+ - 配置不满意可重新执行此命令修改
44
+
45
+ ## 🏃 启动服务
46
+ ```
47
+ npm start
48
+ ```
49
+
50
+ ## 🔍 使用方法
51
+ 1. 访问管理界面: `http://127.0.0.1:3010`
52
+ 2. 使用页面底部蓝色按钮获取cookie
53
+ 3. 在酒馆页面配置:
54
+ - API地址: `http://127.0.0.1:3010/v1`
55
+ - 密钥: `sk-text` (如果配置时输入的是"text")
56
+
57
+ ## 📧 账号注册建议
58
+ - 推荐使用域名邮箱(子域名邮箱更佳)
59
+ - 搜索"cloudfare 域名邮箱"获取配置教程
60
+ - ⚠️ 每次注册账号不超过2个,避免被封
61
+
62
+ ## 🛠️ 常用命令
63
+ ```
64
+ npm start # 启动项目
65
+ npm run setup # 修改配置
66
+ ```
67
+
68
+ ## 环境配置
69
+
70
+ 在`.env`文件中配置以下关键参数:
71
+
72
+ - `API_KEYS`: API Key与Cookie的映射关系(JSON格式)
73
+ - `USE_TLS_PROXY`: (true)启用tls服务器,可以避免阻止请求(block)的问题
74
+ - `PROXY_PLATFORM`: 启用tls服务器时对应的平台,默认auto自动检测
75
+
76
+ 系统启动时会自动合并`.env`中的API Keys和`data/api_keys.json`中的API Keys,确保数据一致性。
77
+
78
+ ## 部署方式
79
+
80
+ ### 使用Docker Compose
81
+
82
+ ```bash
83
+ # 创建配置文件
84
+ cp .env.example .env
85
+ mkdir -p data
86
+ cp data/admin.example.json data/admin.json
87
+
88
+ # 创建管理员账户
89
+ node scripts/create-admin.js
90
+
91
+ # 启动服务
92
+ docker compose up -d --build
93
+
94
+ # 查看日志
95
+ docker compose logs -f
96
+
97
+ # 停止服务
98
+ docker compose down
99
+ ```
100
+
101
+ ### 部署到 Hugging Face Spaces
102
+
103
+ 本项目包含 `Dockerfile`,可以直接部署到 Hugging Face Spaces。
104
+
105
+ 1. 在Hugging Face上创建一个新的Space,并选择 **Docker** 作为SDK。
106
+ 2. 将此仓库克隆到您的Space中。
107
+ 3. 在Space的 **Settings -> Repository secrets** 中设置您的环境变量(如 `API_KEYS` 等)。这些secrets会作为环境变量注入到容器中,作用等同于 `.env` 文件。
108
+ 4. Hugging Face会自动构建并运行应用。
109
+
110
+ **注意**: 部署后,请将文档中所有的 `http://127.0.0.1:3010` 替换为您的Space URL,例如 `https://your-space-name.hf.space`。
111
+
112
+ ## API使用示例
113
+
114
+ ### Python示例
115
+
116
+ ```python
117
+ from openai import OpenAI
118
+
119
+ # 使用自定义API Key
120
+ client = OpenAI(api_key="your_custom_api_key",
121
+ base_url="http://localhost:3010/v1")
122
+
123
+ # 或直接使用Cookie
124
+ # client = OpenAI(api_key="user_...",
125
+ # base_url="http://localhost:3010/v1")
126
+
127
+ response = client.chat.completions.create(
128
+ model="claude-3-7-sonnet",
129
+ messages=[
130
+ {"role": "user", "content": "Hello."},
131
+ ],
132
+ stream=False
133
+ )
134
+
135
+ print(response.choices)
136
+ ```
137
+
138
+ ## 注意事项
139
+
140
+ - 请妥善保管你的WorkosCursorSessionToken
141
+ - 本项目仅用于学习和研究目的,请遵守Cursor的使用条款
142
+
143
+ ## 致谢
144
+
145
+ - 本项目基于[cursor-api](https://github.com/zhx47/cursor-api)(by zhx47)
146
+ - 整合了[cursor-api](https://github.com/lvguanjun/cursor-api)(by lvguanjun)的提交内容
147
+
148
+ # 日志系统
149
+
150
+ 项目集成了统一的日志系统,可以通过以下方式配置:
151
+
152
+ ## 日志级别配置
153
+
154
+ 1. 在 `.env` 文件中设置环境变量
155
+ ```
156
+ LOG_LEVEL=INFO
157
+ LOG_FORMAT=colored
158
+ LOG_TO_FILE=true
159
+ LOG_MAX_SIZE=10
160
+ LOG_MAX_FILES=10
161
+ ```
162
+ 2. 在启动命令中指定环境变量,例如:`LOG_LEVEL=DEBUG npm start`
163
+
164
+ 支持的日志级别有:
165
+ - ERROR:只显示错误信息
166
+ - WARN:显示警告和错误信息
167
+ - INFO:显示一般信息、警告和错误信息(默认)
168
+ - DEBUG:显示调试信息、一般信息、警告和错误信息
169
+ - TRACE:显示所有日志信息
170
+
171
+ ## 日志格式
172
+
173
+ 日志格式为:`[级别] 时间戳 日志内容`,不同级别使用不同颜色显示,方便区分:
174
+ - ERROR:红色
175
+ - WARN:黄色
176
+ - INFO:绿色
177
+ - DEBUG:蓝色
178
+ - TRACE:青色
179
+ - HTTP:青色(专用于HTTP请求日志)
180
+
181
+ ## HTTP请求日志
182
+
183
+ 项目使用 Morgan 中间件记录 HTTP 请求,并集成到统一日志系统中:
184
+
185
+ 1. 在 `.env` 文件中设置 HTTP 日志格式:
186
+ ```
187
+ # 选项: tiny, combined, common, dev, short
188
+ MORGAN_FORMAT=tiny
189
+ ```
190
+
191
+ 2. HTTP 日志会以 `[HTTP]` 前缀显示,使用青色高亮,便于识别
192
+
193
+ 3. Morgan 格式选项说明:
194
+ - `tiny`: 最简洁的格式,仅包含方法、URL、状态码、响应时间
195
+ - `combined`: 标准的 Apache 组合日志格式,包含 IP、时间、请求、状态码、响应大小、referrer、user-agent
196
+ - `common`: 标准的 Apache 通用日志格式,类似 combined 但不包含 referrer 和 user-agent
197
+ - `dev`: 开发友好的彩色格式,包含方法、URL、状态码(带颜色)、响应时间
198
+ - `short`: 更短的格式,包含方法、URL、状态码、响应时间、响应大小
199
+
200
+ ## 文件日志
201
+
202
+ 项目支持将日志同时输出到控制台和文件,可以通过以下配置启用:
203
+
204
+ 1. 在 `.env` 文件中设置:`LOG_TO_FILE=true`
205
+ 2. 可选配置:
206
+ - `LOG_MAX_SIZE`: 日志文件最大大小,单位MB,默认10MB
207
+ - `LOG_MAX_FILES`: 保留的历史日志文件数量,默认10个
208
+
209
+ 日志文件存储在项目根目录的 `logs` 文件夹下:
210
+ - 当前日志文件: `app.log`
211
+ - 历史日志文件: `app-2023-05-05T12-45-30-000Z.log`
212
+
213
+ 文件日志会自动轮转,当日志文件大小超过设定值时,会创建新的日志文件并保留最近的N个文件。
214
+
215
+ ## 代码中使用
216
+
217
+ 在代码中可以按需使用不同级别的日志:
218
+
219
+ ```javascript
220
+ const logger = require('./utils/logger');
221
+
222
+ logger.error('这是错误信息');
223
+ logger.warn('这是警告信息');
224
+ logger.info('这是一般信息');
225
+ logger.debug('这是调试信息');
226
+ logger.trace('这是跟踪信息');
227
+ logger.http('这是HTTP请求日志');
228
+ ```
SETUP.md ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Cursor-To-OpenAI 一键配置指南
2
+
3
+ 本文档将指导你使用一键配置工具来设置 Cursor-To-OpenAI 环境。
4
+
5
+ ## 准备工作
6
+
7
+ 在开始配置前,请确保你已经:
8
+
9
+ 1. Fork了 [Cursor-Register-fix](https://github.com/liuw1535/Cursor-Register-fix) 仓库到你的GitHub账号
10
+ 2. 创建了一个GitHub个人访问令牌(Personal Access Token),且具有 `repo` 权限
11
+ 3. 拥有至少一个Gmail账号,并启用了两步验证
12
+ 4. 为Gmail账号创建了应用密码(Application Password)
13
+
14
+ ## 配置步骤
15
+
16
+ ### 1. 安装依赖
17
+
18
+ ```bash
19
+ npm install
20
+ ```
21
+
22
+ ### 2. 运行配置脚本
23
+
24
+ ```bash
25
+ npm run setup
26
+ ```
27
+
28
+ 或者直接运行:
29
+
30
+ ```bash
31
+ node setup.js
32
+ ```
33
+
34
+ ### 3. 按照提示输入信息
35
+
36
+ 脚本会引导你输入以下信息:
37
+
38
+ - GitHub用户名:你的GitHub账号用户名
39
+ - GitHub Token:你的个人访问令牌
40
+ - API Key:自定义的API Key,用于访问服务
41
+ - Gmail账号:用于自动注册Cursor账号的Gmail地址
42
+ - Gmail应用密码:对应Gmail账号的应用密码(不是邮箱密码)
43
+
44
+ ### 4. 创建应用密码的步骤
45
+
46
+ 如果你还没有创建Gmail应用密码,请按照以下步骤操作:
47
+
48
+ 1. 访问 [Google账号安全设置](https://myaccount.google.com/security)
49
+ 2. 在"登录Google"部分,点击"两步验证"
50
+ (如果未启用两步验证,需要先启用)
51
+ 3. 在页面底部找到"应用密码",点击进入
52
+ 4. 在"选择应用"下拉菜单中选择"其他(自定义名称)"
53
+ 5. 输入一个名称,例如"Cursor注册"
54
+ 6. 点击"生成"
55
+ 7. 复制生成的16位应用密码(格式如:xxxx xxxx xxxx xxxx)
56
+
57
+ ### 5. 管理邮箱配置
58
+
59
+ 系统提供了一个专门的邮箱配置管理工具,可以随时添加、修改或删除邮箱:
60
+
61
+ ```bash
62
+ npm run manage-emails
63
+ ```
64
+
65
+ 使用此工具可以:
66
+ - 查看所有已配置的邮箱
67
+ - 添加新的Gmail账号
68
+ - 修改现有Gmail账号的配置
69
+ - 删除不再使用的Gmail账号
70
+
71
+ ## 配置完成后
72
+
73
+ 配置完成后,你可以:
74
+
75
+ 1. 启动服务:
76
+
77
+ ```bash
78
+ npm start
79
+ ```
80
+
81
+ 2. 手动触发Cookie刷新:
82
+
83
+ ```bash
84
+ npm run refresh-cookies:force
85
+ ```
86
+
87
+ ## 配置文件说明
88
+
89
+ 脚本会生成`.env`文件,其中包含以下主要配置:
90
+
91
+ - `API_KEYS`:API Key到Cookie的映射关系
92
+ - `GITHUB_OWNER`:你的GitHub用户名
93
+ - `GITHUB_TOKEN`:你的GitHub个人访问令牌
94
+ - `REGISTER_EMAIL_CONFIGS`:Gmail账号配置,用于自动注册
95
+
96
+ ## 注意事项
97
+
98
+ 1. GitHub Token需要具有repo权限,用于访问你fork的仓库
99
+ 2. Gmail应用密码不同于你的Gmail登录密码,是专门为第三方应用生成的
100
+ 3. MIN_COOKIE_COUNT设置为1000,确保系统会尝试刷新Cookie
101
+ 4. 配置完成后,你可以通过Web界面查看和管理Cookie状态
102
+ 5. 始终确保至少有一个有效的Gmail账号配置,否则自动刷新功能将无法正常工作
add-api-key.js ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const fetch = require('node-fetch');
2
+
3
+ async function addApiKey() {
4
+ try {
5
+ console.log('添加API Key...');
6
+ const response = await fetch('http://localhost:3010/v1/api-keys', {
7
+ method: 'POST',
8
+ headers: {
9
+ 'Content-Type': 'application/json',
10
+ },
11
+ body: JSON.stringify({
12
+ apiKey: 'test-key',
13
+ cookieValues: ['test-cookie'],
14
+ }),
15
+ });
16
+
17
+ console.log('响应状态:', response.status);
18
+
19
+ if (!response.ok) {
20
+ throw new Error(`HTTP错误: ${response.status} ${response.statusText}`);
21
+ }
22
+
23
+ const data = await response.json();
24
+ console.log('响应数据:', data);
25
+
26
+ // 测试获取API Keys
27
+ console.log('\n测试获取API Keys...');
28
+ const getResponse = await fetch('http://localhost:3010/v1/api-keys');
29
+
30
+ console.log('响应状态:', getResponse.status);
31
+
32
+ if (!getResponse.ok) {
33
+ throw new Error(`HTTP错误: ${getResponse.status} ${getResponse.statusText}`);
34
+ }
35
+
36
+ const getData = await getResponse.json();
37
+ console.log('获取到的数据:', getData);
38
+ } catch (error) {
39
+ console.error('操作失败:', error);
40
+ }
41
+ }
42
+
43
+ addApiKey();
auto-refresh-cookies.js ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // 加载环境变量
2
+ require('dotenv').config();
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const axios = require('axios');
7
+ const { spawn } = require('child_process');
8
+ const keyManager = require('./src/utils/keyManager');
9
+ const logger = require('./src/utils/logger');
10
+
11
+ // 环境检查
12
+ const envChecker = require('./src/utils/envChecker');
13
+ logger.info('启动前检查环境配置...');
14
+ envChecker.enforceEnvCheck();
15
+
16
+ // 已适配GitHub Actions工作流新参数 (use_config_file, email_configs)
17
+ logger.info('环境检查通过,已适配最新GitHub Actions工作流参数');
18
+
19
+ const cookieRefresher = require('./src/utils/cookieRefresher');
20
+ const config = require('./src/config/config');
21
+
22
+ // 解析命令行参数
23
+ const args = process.argv.slice(2);
24
+ const targetApiKey = args.length > 0 ? args[0] : null;
25
+ const forceRefresh = args.includes('--force') || args.includes('-f');
26
+
27
+ // 最小 Cookie 数量
28
+ const MIN_COOKIE_COUNT = process.env.MIN_COOKIE_COUNT || 3;
29
+
30
+ // 获取Cookie刷新模式
31
+ const COOKIE_REFRESH_MODE = process.env.COOKIE_REFRESH_MODE || 'append';
32
+
33
+ // 主函数
34
+ async function main() {
35
+ logger.info('===== 自动刷新 Cookie 开始 =====');
36
+ logger.info(`最小 Cookie 数量: ${MIN_COOKIE_COUNT}`);
37
+ logger.info(`Cookie 刷新模式: ${COOKIE_REFRESH_MODE} (${COOKIE_REFRESH_MODE === 'replace' ? '替换现有cookie' : '追加新cookie'})`);
38
+
39
+ if (targetApiKey) {
40
+ logger.info(`指定刷新 API Key: ${targetApiKey}`);
41
+ }
42
+
43
+ if (forceRefresh) {
44
+ logger.info('强制刷新模式: 忽略 Cookie 数量检查');
45
+ }
46
+
47
+ try {
48
+ // 获取所有 API Key
49
+ const apiKeys = keyManager.getAllApiKeys();
50
+
51
+ if (apiKeys.length === 0) {
52
+ logger.warn('警告: 系统中没有找到任何 API Key');
53
+
54
+ // 检查环境变量中是否有 API Keys
55
+ const envApiKeys = Object.keys(config.apiKeys);
56
+ if (envApiKeys.length > 0) {
57
+ logger.info(`检测到环境变量中有 ${envApiKeys.length} 个 API Key,但尚未加载到系统中`);
58
+ logger.info('正在重新初始化 API Keys...');
59
+
60
+ // 重新初始化 API Keys
61
+ keyManager.initializeApiKeys();
62
+
63
+ // 重新获取 API Keys
64
+ const refreshedApiKeys = keyManager.getAllApiKeys();
65
+ if (refreshedApiKeys.length > 0) {
66
+ logger.info(`成功加载 ${refreshedApiKeys.length} 个 API Key,继续刷新流程`);
67
+ // 继续执行后续刷新逻辑
68
+ } else {
69
+ logger.warn('初始化后仍未找到 API Key,请检查配置');
70
+ logger.info('===== 自动刷新 Cookie 结束 =====');
71
+ return;
72
+ }
73
+ } else {
74
+ logger.warn('环境变量中也没有配置 API Key,请先添加 API Key');
75
+ logger.info('===== 自动刷新 Cookie 结束 =====');
76
+ return;
77
+ }
78
+ }
79
+
80
+ // 重新获取最新的 API Keys(可能已经通过上面的初始化更新了)
81
+ const updatedApiKeys = keyManager.getAllApiKeys();
82
+ logger.info(`系统中共有 ${updatedApiKeys.length} 个 API Key`);
83
+
84
+ // 如果指定了特定的 API Key,检查它是否存在
85
+ if (targetApiKey && !updatedApiKeys.includes(targetApiKey)) {
86
+ logger.error(`错误: 指定的 API Key "${targetApiKey}" 不存在`);
87
+ logger.info('===== 自动刷新 Cookie 异常结束 =====');
88
+ return;
89
+ }
90
+
91
+ // 过滤需要处理的 API Keys
92
+ const keysToProcess = targetApiKey ? [targetApiKey] : updatedApiKeys;
93
+
94
+ // 按 Cookie 数量排序,优先处理 Cookie 数量少的 API Key
95
+ const sortedKeys = keysToProcess.sort((a, b) => {
96
+ const aCount = keyManager.getAllCookiesForApiKey(a).length;
97
+ const bCount = keyManager.getAllCookiesForApiKey(b).length;
98
+ return aCount - bCount; // 升序排列,Cookie 数量少的排在前面
99
+ });
100
+
101
+ // 检查每个 API Key 是否需要刷新
102
+ let refreshedCount = 0;
103
+ let needRefreshCount = 0;
104
+
105
+ for (const apiKey of sortedKeys) {
106
+ const cookies = keyManager.getAllCookiesForApiKey(apiKey);
107
+ logger.info(`API Key: ${apiKey}, Cookie 数量: ${cookies.length}`);
108
+
109
+ // 判断是否需要刷新:强制刷新模式或 Cookie 数量低于阈值
110
+ if (forceRefresh || cookies.length < MIN_COOKIE_COUNT) {
111
+ needRefreshCount++;
112
+ if (forceRefresh) {
113
+ logger.info(`强制刷新 API Key: ${apiKey}`);
114
+ } else {
115
+ logger.info(`API Key ${apiKey} 的 Cookie 数量不足,需要刷新`);
116
+ }
117
+
118
+ // 执行刷新
119
+ logger.info(`开始自动刷新 Cookie,目标 API Key: ${apiKey},最小 Cookie 数量: ${MIN_COOKIE_COUNT},刷新模式: ${COOKIE_REFRESH_MODE}`);
120
+ const result = await cookieRefresher.autoRefreshCookies(apiKey, MIN_COOKIE_COUNT);
121
+
122
+ if (result.success) {
123
+ refreshedCount++;
124
+ logger.info(`刷新结果: ${result.message}`);
125
+
126
+ // 根据刷新模式输出额外的信息
127
+ if (COOKIE_REFRESH_MODE === 'replace') {
128
+ logger.info(`使用替换模式: 现有cookie已全部标记为无效,系统现在只使用新cookie`);
129
+ } else {
130
+ logger.info(`使用追加模式: 现有cookie已保留,新cookie已添加到系统`);
131
+ }
132
+ } else {
133
+ logger.error(`刷新失败: ${result.message}`);
134
+ }
135
+ } else {
136
+ logger.info(`API Key ${apiKey} 的 Cookie 数量足够,不需要刷新`);
137
+ }
138
+ }
139
+
140
+ logger.info('===== 自动刷新 Cookie 完成 =====');
141
+ logger.info(`共有 ${needRefreshCount} 个 API Key 需要刷新,成功刷新 ${refreshedCount} 个`);
142
+ } catch (error) {
143
+ logger.error('自动刷新 Cookie 失败:', error);
144
+ logger.info('===== 自动刷新 Cookie 异常结束 =====');
145
+ }
146
+ }
147
+
148
+ // 执行主函数
149
+ main().catch(err => logger.error(err));
cursor-to-openai-helper.sh ADDED
@@ -0,0 +1,557 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # Colors for better UI
4
+ RED='\033[0;31m'
5
+ GREEN='\033[0;32m'
6
+ YELLOW='\033[0;33m'
7
+ BLUE='\033[0;34m'
8
+ NC='\033[0m' # No Color
9
+
10
+ # Create backups directory if it doesn't exist
11
+ mkdir -p backups
12
+
13
+ # Function to display header
14
+ show_header() {
15
+ clear
16
+ echo -e "${BLUE}=======================================${NC}"
17
+ echo -e "${GREEN} Cursor-To-OpenAI 简易脚本 ${NC}"
18
+ echo -e "${BLUE}=======================================${NC}"
19
+ echo
20
+ }
21
+
22
+ # Function to check if Docker is installed
23
+ check_docker() {
24
+ if ! command -v docker &> /dev/null; then
25
+ echo -e "${RED}未安装Docker。请先安装Docker。${NC}"
26
+ exit 1
27
+ fi
28
+ }
29
+
30
+ # Function to check if Node.js is installed
31
+ check_nodejs() {
32
+ if ! command -v node &> /dev/null; then
33
+ echo -e "${RED}未安装Node.js。请先安装Node.js。${NC}"
34
+ exit 1
35
+ fi
36
+
37
+ if ! command -v npm &> /dev/null; then
38
+ echo -e "${RED}未安装npm。请先安装npm。${NC}"
39
+ exit 1
40
+ fi
41
+ }
42
+
43
+ # Function to backup configuration before update
44
+ backup_configs() {
45
+ echo -e "${YELLOW}正在备份配置文件...${NC}"
46
+ backup_dir="backups/update_backup_$(date +"%Y%m%d_%H%M%S")"
47
+ mkdir -p "$backup_dir"
48
+
49
+ # Backup important configurations
50
+ if [ -d data ]; then
51
+ cp -r data "$backup_dir/"
52
+ fi
53
+
54
+ if [ -f .env ]; then
55
+ cp .env "$backup_dir/"
56
+ fi
57
+
58
+ echo -e "${GREEN}配置文件已备份到 ${backup_dir}${NC}"
59
+ }
60
+
61
+ # Function to restore configuration after update
62
+ restore_configs() {
63
+ if [ -z "$1" ]; then
64
+ echo -e "${RED}未指定备份目录,无法恢复配置。${NC}"
65
+ return 1
66
+ fi
67
+
68
+ backup_dir="$1"
69
+ echo -e "${YELLOW}正在恢复配置文件...${NC}"
70
+
71
+ if [ -d "$backup_dir/data" ]; then
72
+ cp -r "$backup_dir/data/"* data/ 2>/dev/null
73
+ fi
74
+
75
+ if [ -f "$backup_dir/.env" ]; then
76
+ cp "$backup_dir/.env" ./ 2>/dev/null
77
+ fi
78
+
79
+ echo -e "${GREEN}配置文件已恢复${NC}"
80
+ }
81
+
82
+ # Function for installation tasks
83
+ installation_menu() {
84
+ show_header
85
+ echo -e "${YELLOW}===== 安装菜单 =====${NC}"
86
+ echo -e "1) 克隆仓库"
87
+ echo -e "2) 安装依赖"
88
+ echo -e "3) 创建配置文件"
89
+ echo -e "4) 设置管理员账户"
90
+ echo -e "5) 运行安装向导"
91
+ echo -e "6) 构建并启动Docker容器"
92
+ echo -e "7) 使用npm启动"
93
+ echo -e "8) 返回主菜单"
94
+ echo
95
+ echo -n "请输入选择 [1-8]: "
96
+ read -r choice
97
+
98
+ case $choice in
99
+ 1)
100
+ show_header
101
+ echo -e "${YELLOW}正在克隆仓库...${NC}"
102
+ read -p "请输入您的GitHub用户名: " username
103
+ git clone "https://github.com/${username}/cursor-to-openai.git"
104
+ if [ $? -eq 0 ]; then
105
+ echo -e "${GREEN}仓库克隆成功!${NC}"
106
+ cd cursor-to-openai
107
+ else
108
+ echo -e "${RED}克隆仓库失败!${NC}"
109
+ fi
110
+ read -p "按回车键继续..."
111
+ installation_menu
112
+ ;;
113
+ 2)
114
+ show_header
115
+ echo -e "${YELLOW}正在安装依赖...${NC}"
116
+ npm install
117
+ if [ $? -eq 0 ]; then
118
+ echo -e "${GREEN}依赖安装成功!${NC}"
119
+ else
120
+ echo -e "${RED}依赖安装失败!${NC}"
121
+ fi
122
+ read -p "按回车键继续..."
123
+ installation_menu
124
+ ;;
125
+ 3)
126
+ show_header
127
+ echo -e "${YELLOW}正在创建配置文件...${NC}"
128
+
129
+ if [ ! -f .env ]; then
130
+ cp .env.example .env
131
+ echo -e "${GREEN}.env文件已创建。${NC}"
132
+ else
133
+ echo -e "${YELLOW}.env文件已存在。${NC}"
134
+ fi
135
+
136
+ mkdir -p data
137
+
138
+ if [ ! -f data/admin.json ]; then
139
+ cp data/admin.example.json data/admin.json
140
+ echo -e "${GREEN}admin.json文件已创建。${NC}"
141
+ else
142
+ echo -e "${YELLOW}admin.json文件已存在。${NC}"
143
+ fi
144
+
145
+ echo -e "${YELLOW}您应该编辑.env文件来配置您的环境。${NC}"
146
+ read -p "是否现在编辑.env文件?(y/n): " edit_env
147
+ if [[ $edit_env == "y" || $edit_env == "Y" ]]; then
148
+ if command -v nano &> /dev/null; then
149
+ nano .env
150
+ elif command -v vim &> /dev/null; then
151
+ vim .env
152
+ else
153
+ echo -e "${RED}未找到编辑器。请稍后手动编辑.env文件。${NC}"
154
+ fi
155
+ fi
156
+
157
+ read -p "按回车键继续..."
158
+ installation_menu
159
+ ;;
160
+ 4)
161
+ show_header
162
+ echo -e "${YELLOW}正在设置管理员账户...${NC}"
163
+ node scripts/create-admin.js
164
+ read -p "按回车键继续..."
165
+ installation_menu
166
+ ;;
167
+ 5)
168
+ show_header
169
+ echo -e "${YELLOW}正在运行安装向导...${NC}"
170
+ node setup.js
171
+ read -p "按回车键继续..."
172
+ installation_menu
173
+ ;;
174
+ 6)
175
+ show_header
176
+ echo -e "${YELLOW}正在构建并启动Docker容器...${NC}"
177
+ check_docker
178
+ docker compose up -d --build
179
+ if [ $? -eq 0 ]; then
180
+ echo -e "${GREEN}Docker容器启动成功!${NC}"
181
+ echo -e "${GREEN}访问管理界面: http://localhost:3010${NC}"
182
+ else
183
+ echo -e "${RED}启动Docker容器失败!${NC}"
184
+ fi
185
+ read -p "按回车键继续..."
186
+ installation_menu
187
+ ;;
188
+ 7)
189
+ show_header
190
+ echo -e "${YELLOW}正在使用npm启动...${NC}"
191
+ check_nodejs
192
+ npm start &
193
+ if [ $? -eq 0 ]; then
194
+ echo -e "${GREEN}服务启动成功!${NC}"
195
+ echo -e "${GREEN}访问管理界面: http://localhost:3010${NC}"
196
+ else
197
+ echo -e "${RED}启动服务失败!${NC}"
198
+ fi
199
+ read -p "按回车键继续..."
200
+ installation_menu
201
+ ;;
202
+ 8)
203
+ main_menu
204
+ ;;
205
+ *)
206
+ echo -e "${RED}无效选项。请重试。${NC}"
207
+ read -p "按回车键继续..."
208
+ installation_menu
209
+ ;;
210
+ esac
211
+ }
212
+
213
+ # Function for maintenance tasks
214
+ maintenance_menu() {
215
+ show_header
216
+ echo -e "${YELLOW}===== 维护菜单 =====${NC}"
217
+ echo -e "1) 查看服务状态"
218
+ echo -e "2) 刷新Cookie"
219
+ echo -e "3) 强制刷新Cookie"
220
+ echo -e "4) 管理邮箱"
221
+ echo -e "5) 管理无效Cookie"
222
+ echo -e "6) 查看日志"
223
+ echo -e "7) 重启服务"
224
+ echo -e "8) 停止服务"
225
+ echo -e "9) 更新项目代码"
226
+ echo -e "10) 备份项目数据"
227
+ echo -e "11) 持续刷新Cookie直到成功"
228
+ echo -e "12) 返回主菜单"
229
+ echo
230
+ echo -n "请输入选择 [1-12]: "
231
+ read -r choice
232
+
233
+ case $choice in
234
+ 1)
235
+ show_header
236
+ echo -e "${YELLOW}服务状态:${NC}"
237
+ if docker ps | grep -q cursor-to-openai; then
238
+ echo -e "${GREEN}Docker容器正在运行。${NC}"
239
+ docker ps | grep cursor-to-openai
240
+ else
241
+ echo -e "${RED}Docker容器未运行。${NC}"
242
+ fi
243
+
244
+ pids=$(pgrep -f "node.*start")
245
+ if [ -n "$pids" ]; then
246
+ echo -e "${GREEN}Node.js服务正在运行,PID: $pids${NC}"
247
+ else
248
+ echo -e "${RED}Node.js服务未运行。${NC}"
249
+ fi
250
+
251
+ read -p "按回车键继续..."
252
+ maintenance_menu
253
+ ;;
254
+ 2)
255
+ show_header
256
+ echo -e "${YELLOW}正在刷新Cookie...${NC}"
257
+ npm run refresh-cookies
258
+ read -p "按回车键继续..."
259
+ maintenance_menu
260
+ ;;
261
+ 3)
262
+ show_header
263
+ echo -e "${YELLOW}正在强制刷新Cookie...${NC}"
264
+ npm run refresh-cookies -- --force
265
+ read -p "按回车键继续..."
266
+ maintenance_menu
267
+ ;;
268
+ 4)
269
+ show_header
270
+ echo -e "${YELLOW}正在管理邮箱...${NC}"
271
+ npm run manage-emails
272
+ read -p "按回车键继续..."
273
+ maintenance_menu
274
+ ;;
275
+ 5)
276
+ show_header
277
+ echo -e "${YELLOW}正在管理无效Cookie...${NC}"
278
+ node manage-invalid-cookies.js
279
+ read -p "按回车键继续..."
280
+ maintenance_menu
281
+ ;;
282
+ 6)
283
+ show_header
284
+ echo -e "${YELLOW}正在查看日志...${NC}"
285
+ if docker ps | grep -q cursor-to-openai; then
286
+ docker compose logs -f
287
+ else
288
+ echo -e "${RED}Docker容器未运行。${NC}"
289
+ echo -e "${YELLOW}正在检查npm日志...${NC}"
290
+ # Try to find logs in npm-debug.log or similar
291
+ if [ -f npm-debug.log ]; then
292
+ cat npm-debug.log
293
+ else
294
+ echo -e "${RED}未找到日志文件。${NC}"
295
+ fi
296
+ fi
297
+ read -p "按回车键继续..."
298
+ maintenance_menu
299
+ ;;
300
+ 7)
301
+ show_header
302
+ echo -e "${YELLOW}正在重启服务...${NC}"
303
+ if docker ps | grep -q cursor-to-openai; then
304
+ docker compose restart
305
+ echo -e "${GREEN}Docker容器已重启。${NC}"
306
+ else
307
+ pids=$(pgrep -f "node.*start")
308
+ if [ -n "$pids" ]; then
309
+ kill $pids
310
+ sleep 2
311
+ npm start &
312
+ echo -e "${GREEN}Node.js服务已重启。${NC}"
313
+ else
314
+ echo -e "${RED}未检测到运行中的服务。${NC}"
315
+ echo -e "${YELLOW}是否要启动服务?(y/n): ${NC}"
316
+ read -r start_service
317
+ if [[ $start_service == "y" || $start_service == "Y" ]]; then
318
+ npm start &
319
+ echo -e "${GREEN}服务已启动。${NC}"
320
+ fi
321
+ fi
322
+ fi
323
+ read -p "按回车键继续..."
324
+ maintenance_menu
325
+ ;;
326
+ 8)
327
+ show_header
328
+ echo -e "${YELLOW}正在停止服务...${NC}"
329
+ if docker ps | grep -q cursor-to-openai; then
330
+ docker compose down
331
+ echo -e "${GREEN}Docker容器已停止。${NC}"
332
+ else
333
+ pids=$(pgrep -f "node.*start")
334
+ if [ -n "$pids" ]; then
335
+ kill $pids
336
+ echo -e "${GREEN}Node.js服务已停止。${NC}"
337
+ else
338
+ echo -e "${RED}未检测到运行中的服务。${NC}"
339
+ fi
340
+ fi
341
+ read -p "按回车键继续..."
342
+ maintenance_menu
343
+ ;;
344
+ 9)
345
+ show_header
346
+ echo -e "${YELLOW}正在更新项目代码...${NC}"
347
+
348
+ # 备份配置文件
349
+ backup_configs
350
+ backup_dir=$(ls -td backups/update_backup_* | head -1)
351
+
352
+ # 检查是否存在未提交的更改
353
+ if [ -n "$(git status --porcelain)" ]; then
354
+ echo -e "${YELLOW}检测到未提交的更改。更新前请处理这些更改。${NC}"
355
+ echo -e "1) 查看更改"
356
+ echo -e "2) 备份并放弃更改"
357
+ echo -e "3) 取消更新"
358
+ echo -n "请选择操作 [1-3]: "
359
+ read -r update_choice
360
+
361
+ case $update_choice in
362
+ 1)
363
+ git status
364
+ echo -e "${YELLOW}是否继续更新?(y/n): ${NC}"
365
+ read -r continue_update
366
+ if [[ $continue_update != "y" && $continue_update != "Y" ]]; then
367
+ echo -e "${YELLOW}更新已取消。${NC}"
368
+ read -p "按回车键继续..."
369
+ maintenance_menu
370
+ return
371
+ fi
372
+ ;;
373
+ 2)
374
+ echo -e "${YELLOW}备份更改...${NC}"
375
+ git diff > "$backup_dir/local_changes.patch"
376
+ git checkout -- .
377
+ echo -e "${GREEN}更改已备份到 $backup_dir/local_changes.patch${NC}"
378
+ ;;
379
+ 3)
380
+ echo -e "${YELLOW}更新已取消。${NC}"
381
+ read -p "按回车键继续..."
382
+ maintenance_menu
383
+ return
384
+ ;;
385
+ *)
386
+ echo -e "${RED}无效选项。更新已取消。${NC}"
387
+ read -p "按回车键继续..."
388
+ maintenance_menu
389
+ return
390
+ ;;
391
+ esac
392
+ fi
393
+
394
+ # 更新代码
395
+ git pull
396
+ update_status=$?
397
+
398
+ # 恢复配置文件
399
+ restore_configs "$backup_dir"
400
+
401
+ if [ $update_status -eq 0 ]; then
402
+ echo -e "${GREEN}项目代码更新成功!${NC}"
403
+ echo -e "${YELLOW}是否需要重新安装依赖?(y/n): ${NC}"
404
+ read -r reinstall
405
+ if [[ $reinstall == "y" || $reinstall == "Y" ]]; then
406
+ npm install
407
+ if [ $? -eq 0 ]; then
408
+ echo -e "${GREEN}依赖安装成功!${NC}"
409
+ else
410
+ echo -e "${RED}依赖安装失败!${NC}"
411
+ fi
412
+ fi
413
+ else
414
+ echo -e "${RED}项目代码更新失败!${NC}"
415
+ fi
416
+
417
+ read -p "按回车键继续..."
418
+ maintenance_menu
419
+ ;;
420
+ 10)
421
+ show_header
422
+ echo -e "${YELLOW}正在备份项目数据...${NC}"
423
+
424
+ # 创建备份目录
425
+ backup_dir="backups/backup_$(date +"%Y%m%d_%H%M%S")"
426
+ mkdir -p "$backup_dir"
427
+
428
+ # 备份关键文件
429
+ cp -r data "$backup_dir/" 2>/dev/null
430
+ if [ -f .env ]; then
431
+ cp .env "$backup_dir/"
432
+ fi
433
+
434
+ # 压缩备份
435
+ tar -czf "${backup_dir}.tar.gz" "$backup_dir"
436
+ rm -rf "$backup_dir"
437
+
438
+ echo -e "${GREEN}备份已创建: ${backup_dir}.tar.gz${NC}"
439
+ read -p "按回车键继续..."
440
+ maintenance_menu
441
+ ;;
442
+ 11)
443
+ show_header
444
+ echo -e "${YELLOW}持续刷新Cookie直到成功...${NC}"
445
+ read -p "请输入最大尝试时间(分钟, 默认60): " max_time
446
+ max_time=${max_time:-60}
447
+ max_seconds=$((max_time * 60))
448
+
449
+ echo -e "${YELLOW}将持续尝试刷新Cookie,最长 ${max_time} 分钟...${NC}"
450
+
451
+ start_time=$(date +%s)
452
+ success=false
453
+ attempt=0
454
+
455
+ while ! $success && [ $(($(date +%s) - start_time)) -lt $max_seconds ]; do
456
+ attempt=$((attempt + 1))
457
+ elapsed=$(($(date +%s) - start_time))
458
+ remaining=$((max_seconds - elapsed))
459
+ remaining_min=$((remaining / 60))
460
+ remaining_sec=$((remaining % 60))
461
+
462
+ echo -e "${YELLOW}尝试 #${attempt}...(剩余时间: ${remaining_min}分${remaining_sec}秒)${NC}"
463
+
464
+ # 运行刷新命令并检查输出
465
+ output=$(npm run refresh-cookies -- --force 2>&1)
466
+ echo "$output"
467
+
468
+ if echo "$output" | grep -q "成功添加新的Cookie" || echo "$output" | grep -q "Successfully added new cookies"; then
469
+ success=true
470
+ echo -e "${GREEN}成功添加新Cookie!${NC}"
471
+ else
472
+ wait_time=$((RANDOM % 61 + 30)) # 30-90秒随机间隔
473
+ echo -e "${YELLOW}等待 ${wait_time} 秒后重试...${NC}"
474
+ sleep $wait_time
475
+ fi
476
+ done
477
+
478
+ if $success; then
479
+ echo -e "${GREEN}成功刷新Cookie!${NC}"
480
+ else
481
+ echo -e "${RED}达到最大尝试时间,未能成功刷新Cookie。${NC}"
482
+ fi
483
+
484
+ read -p "按回车键继续..."
485
+ maintenance_menu
486
+ ;;
487
+ 12)
488
+ main_menu
489
+ ;;
490
+ *)
491
+ echo -e "${RED}无效选项。请重试。${NC}"
492
+ read -p "按回车键继续..."
493
+ maintenance_menu
494
+ ;;
495
+ esac
496
+ }
497
+
498
+ # Main menu function
499
+ main_menu() {
500
+ show_header
501
+ echo -e "${YELLOW}===== 主菜单 =====${NC}"
502
+ echo -e "1) 启动服务 (npm)"
503
+ echo -e "2) 安装配置"
504
+ echo -e "3) 系统维护"
505
+ echo -e "4) 退出"
506
+ echo
507
+ echo -n "请输入选择 [1-4]: "
508
+ read -r choice
509
+
510
+ case $choice in
511
+ 1)
512
+ show_header
513
+ echo -e "${YELLOW}正在使用npm启动服务...${NC}"
514
+ check_nodejs
515
+
516
+ # 检查Node.js服务是否已在运行
517
+ pids=$(pgrep -f "node.*start")
518
+ if [ -n "$pids" ]; then
519
+ echo -e "${YELLOW}服务已在运行,PID: $pids${NC}"
520
+ echo -e "${YELLOW}是否要重启服务?(y/n): ${NC}"
521
+ read -r restart
522
+ if [[ $restart == "y" || $restart == "Y" ]]; then
523
+ kill $pids
524
+ sleep 2
525
+ npm start &
526
+ echo -e "${GREEN}服务已重启${NC}"
527
+ fi
528
+ else
529
+ npm start &
530
+ echo -e "${GREEN}服务已启动${NC}"
531
+ fi
532
+
533
+ echo -e "${GREEN}访问管理界面: http://localhost:3010${NC}"
534
+ read -p "按回车键继续..."
535
+ main_menu
536
+ ;;
537
+ 2)
538
+ installation_menu
539
+ ;;
540
+ 3)
541
+ maintenance_menu
542
+ ;;
543
+ 4)
544
+ show_header
545
+ echo -e "${GREEN}感谢使用Cursor-To-OpenAI简易脚本!${NC}"
546
+ exit 0
547
+ ;;
548
+ *)
549
+ echo -e "${RED}无效选项。请重试。${NC}"
550
+ read -p "按回车键继续..."
551
+ main_menu
552
+ ;;
553
+ esac
554
+ }
555
+
556
+ # Start the script
557
+ main_menu
data/admin.example.json ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "admin": {
3
+ "username": "your_admin_username",
4
+ "salt": "your_generated_salt",
5
+ "hash": "your_password_hash"
6
+ }
7
+ }
data/api_keys.example.json ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ {
2
+ "sk-text@example": [
3
+ "user_XXXXXXXXXXXX%3A%3AeyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
4
+ ]
5
+ }
data/invalid_cookies.example.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ [
2
+ "user_XXXXXXXXXXXX%3A%3AeyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
3
+ ]
docker-compose.yaml ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3.8'
2
+
3
+ services:
4
+ app:
5
+ build:
6
+ context: .
7
+ dockerfile: Dockerfile
8
+ image: cursor-to-openai
9
+ volumes:
10
+ - ./data:/app/data
11
+ ports:
12
+ - "3010:3010"
13
+ env_file:
14
+ - .env
manage-emails.js ADDED
@@ -0,0 +1,309 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const readline = require('readline');
6
+ const dotenv = require('dotenv');
7
+
8
+ // 创建交互式命令行界面
9
+ const rl = readline.createInterface({
10
+ input: process.stdin,
11
+ output: process.stdout
12
+ });
13
+
14
+ // 加载环境变量
15
+ const ENV_FILE_PATH = path.join(process.cwd(), '.env');
16
+ let envContent = '';
17
+ let emailConfigs = [];
18
+
19
+ // 应用密码说明
20
+ function printAppPasswordInstructions() {
21
+ console.log('\n===== 如何创建谷歌应用密码 =====');
22
+ console.log('1. 访问 https://myaccount.google.com/security');
23
+ console.log('2. 在"登录Google"部分,点击"两步验证"');
24
+ console.log(' (如果未启用两步验证,需要先启用)');
25
+ console.log('3. 在页面底部找到"应用密码",点击进入');
26
+ console.log('4. 在"选择应用"下拉菜单中选择"其他(自定义名称)"');
27
+ console.log('5. 输入一个名称,例如"Cursor注册"');
28
+ console.log('6. 点击"生成"');
29
+ console.log('7. 复制生成的16位应用密码(格式如:xxxx xxxx xxxx xxxx)');
30
+ console.log('注意: 应用密码只会显示一次,请务必保存好\n');
31
+ }
32
+
33
+ // 加载当前环境变量和邮箱配置
34
+ function loadEnvironment() {
35
+ try {
36
+ if (!fs.existsSync(ENV_FILE_PATH)) {
37
+ console.error('❌ .env文件不存在,请先运行setup.js进行初始化配置');
38
+ process.exit(1);
39
+ }
40
+
41
+ // 读取原始.env文件内容
42
+ envContent = fs.readFileSync(ENV_FILE_PATH, 'utf8');
43
+
44
+ // 解析环境变量
45
+ dotenv.config();
46
+
47
+ // 尝试解析当前的邮箱配置
48
+ try {
49
+ const configStr = process.env.REGISTER_EMAIL_CONFIGS;
50
+ if (configStr) {
51
+ emailConfigs = JSON.parse(configStr);
52
+ if (!Array.isArray(emailConfigs)) {
53
+ emailConfigs = [];
54
+ }
55
+ }
56
+ } catch (parseErr) {
57
+ console.warn('⚠️ 解析当前邮箱配置出错,将使用空配置');
58
+ emailConfigs = [];
59
+ }
60
+
61
+ return true;
62
+ } catch (error) {
63
+ console.error(`❌ 加载环境变量失败: ${error.message}`);
64
+ return false;
65
+ }
66
+ }
67
+
68
+ // 保存更新后的邮箱配置到.env文件
69
+ function saveEmailConfigs() {
70
+ try {
71
+ // 将邮箱配置格式化为JSON字符串
72
+ const configStr = JSON.stringify(emailConfigs);
73
+
74
+ // 替换.env文件中的配置
75
+ let newEnvContent = '';
76
+
77
+ if (envContent.includes('REGISTER_EMAIL_CONFIGS=')) {
78
+ // 使用正则表达式替换REGISTER_EMAIL_CONFIGS行
79
+ newEnvContent = envContent.replace(
80
+ /REGISTER_EMAIL_CONFIGS=.*/,
81
+ `REGISTER_EMAIL_CONFIGS=${configStr}`
82
+ );
83
+ } else {
84
+ // 如果不存在该配置行,添加到文件末尾
85
+ newEnvContent = `${envContent}\nREGISTER_EMAIL_CONFIGS=${configStr}`;
86
+ }
87
+
88
+ // 同时确保USE_CONFIG_FILE设置为false
89
+ if (newEnvContent.includes('REGISTER_USE_CONFIG_FILE=')) {
90
+ newEnvContent = newEnvContent.replace(
91
+ /REGISTER_USE_CONFIG_FILE=.*/,
92
+ 'REGISTER_USE_CONFIG_FILE=false'
93
+ );
94
+ } else {
95
+ newEnvContent = `${newEnvContent}\nREGISTER_USE_CONFIG_FILE=false`;
96
+ }
97
+
98
+ // 确保EMAIL_SERVER设置为IMAP
99
+ if (newEnvContent.includes('REGISTER_EMAIL_SERVER=')) {
100
+ newEnvContent = newEnvContent.replace(
101
+ /REGISTER_EMAIL_SERVER=.*/,
102
+ 'REGISTER_EMAIL_SERVER=IMAP'
103
+ );
104
+ } else {
105
+ newEnvContent = `${newEnvContent}\nREGISTER_EMAIL_SERVER=IMAP`;
106
+ }
107
+
108
+ // 写入更新后的内容
109
+ fs.writeFileSync(ENV_FILE_PATH, newEnvContent, 'utf8');
110
+
111
+ console.log('✅ 邮箱配置已成功保存到.env文件');
112
+ return true;
113
+ } catch (error) {
114
+ console.error(`❌ 保存邮箱配置失败: ${error.message}`);
115
+ return false;
116
+ }
117
+ }
118
+
119
+ // 显示所有已配置的邮箱
120
+ function displayEmails() {
121
+ console.log('\n===== 当前已配置的邮箱 =====');
122
+
123
+ if (emailConfigs.length === 0) {
124
+ console.log('暂无已配置的邮箱');
125
+ return;
126
+ }
127
+
128
+ emailConfigs.forEach((config, index) => {
129
+ console.log(`[${index + 1}] ${config.email}`);
130
+ console.log(` IMAP服务器: ${config.imap_server}`);
131
+ console.log(` IMAP端口: ${config.imap_port}`);
132
+ console.log(` 用户名: ${config.username}`);
133
+ console.log(` 应用密码: ${config.password}`);
134
+ console.log('');
135
+ });
136
+ }
137
+
138
+ // 添加新邮箱
139
+ function addEmail() {
140
+ console.log('\n===== 添加新邮箱 =====');
141
+ printAppPasswordInstructions();
142
+
143
+ rl.question('请输入Gmail地址: ', (email) => {
144
+ rl.question('请输入Gmail的应用密码 (不是邮箱密码): ', (password) => {
145
+ // 创建新配置
146
+ const newConfig = {
147
+ email: email,
148
+ imap_server: 'imap.gmail.com',
149
+ imap_port: 993,
150
+ username: email,
151
+ password: password
152
+ };
153
+
154
+ // 添加到配置列表
155
+ emailConfigs.push(newConfig);
156
+
157
+ console.log(`\n✅ 已添加邮箱: ${email}`);
158
+
159
+ // 保存到.env文件
160
+ if (saveEmailConfigs()) {
161
+ showMainMenu();
162
+ }
163
+ });
164
+ });
165
+ }
166
+
167
+ // 修改邮箱
168
+ function modifyEmail() {
169
+ if (emailConfigs.length === 0) {
170
+ console.log('\n❌ 当前没有可修改的邮箱。请先添加邮箱。');
171
+ showMainMenu();
172
+ return;
173
+ }
174
+
175
+ console.log('\n===== 修改邮箱 =====');
176
+ displayEmails();
177
+
178
+ rl.question('请输入要修改的邮箱序号 (1-' + emailConfigs.length + '): ', (indexStr) => {
179
+ const index = parseInt(indexStr) - 1;
180
+
181
+ if (isNaN(index) || index < 0 || index >= emailConfigs.length) {
182
+ console.log('\n❌ 无效的序号。请重新选择。');
183
+ modifyEmail();
184
+ return;
185
+ }
186
+
187
+ const currentConfig = emailConfigs[index];
188
+
189
+ console.log(`\n正在修改邮箱: ${currentConfig.email}`);
190
+
191
+ rl.question(`新的Gmail地址 (当前: ${currentConfig.email},直接回车保持不变): `, (email) => {
192
+ const newEmail = email.trim() === '' ? currentConfig.email : email;
193
+
194
+ rl.question('新的应用密码 (直接回车保持不变): ', (password) => {
195
+ const newPassword = password.trim() === '' ? currentConfig.password : password;
196
+
197
+ // 更新配置
198
+ emailConfigs[index] = {
199
+ email: newEmail,
200
+ imap_server: 'imap.gmail.com',
201
+ imap_port: 993,
202
+ username: newEmail,
203
+ password: newPassword
204
+ };
205
+
206
+ console.log(`\n✅ 已修改邮箱配置: ${newEmail}`);
207
+
208
+ // 保存到.env文件
209
+ if (saveEmailConfigs()) {
210
+ showMainMenu();
211
+ }
212
+ });
213
+ });
214
+ });
215
+ }
216
+
217
+ // 删除邮箱
218
+ function deleteEmail() {
219
+ if (emailConfigs.length === 0) {
220
+ console.log('\n❌ 当前没有可删除的邮箱。');
221
+ showMainMenu();
222
+ return;
223
+ }
224
+
225
+ console.log('\n===== 删除邮箱 =====');
226
+ displayEmails();
227
+
228
+ rl.question('请输入要删除的邮箱序号 (1-' + emailConfigs.length + '): ', (indexStr) => {
229
+ const index = parseInt(indexStr) - 1;
230
+
231
+ if (isNaN(index) || index < 0 || index >= emailConfigs.length) {
232
+ console.log('\n❌ 无效的序号。请重新选择。');
233
+ deleteEmail();
234
+ return;
235
+ }
236
+
237
+ const emailToDelete = emailConfigs[index].email;
238
+
239
+ rl.question(`确认删除邮箱 "${emailToDelete}"? (y/n): `, (answer) => {
240
+ if (answer.toLowerCase() === 'y') {
241
+ // 删除邮箱
242
+ emailConfigs.splice(index, 1);
243
+
244
+ console.log(`\n✅ 已删除邮箱: ${emailToDelete}`);
245
+
246
+ // 保存到.env文件
247
+ if (saveEmailConfigs()) {
248
+ showMainMenu();
249
+ }
250
+ } else {
251
+ console.log('\n操作已取消');
252
+ showMainMenu();
253
+ }
254
+ });
255
+ });
256
+ }
257
+
258
+ // 显示主菜单
259
+ function showMainMenu() {
260
+ console.log('\n===== 邮箱配置管理 =====');
261
+ console.log('1. 查看所有邮箱');
262
+ console.log('2. 添加新邮箱');
263
+ console.log('3. 修改邮箱');
264
+ console.log('4. 删除邮箱');
265
+ console.log('0. 退出');
266
+
267
+ rl.question('请选择操作 (0-4): ', (choice) => {
268
+ switch (choice) {
269
+ case '1':
270
+ displayEmails();
271
+ showMainMenu();
272
+ break;
273
+ case '2':
274
+ addEmail();
275
+ break;
276
+ case '3':
277
+ modifyEmail();
278
+ break;
279
+ case '4':
280
+ deleteEmail();
281
+ break;
282
+ case '0':
283
+ console.log('\n✅ 配置完成,退出程序');
284
+ rl.close();
285
+ break;
286
+ default:
287
+ console.log('\n❌ 无效的选择,请重新输入');
288
+ showMainMenu();
289
+ break;
290
+ }
291
+ });
292
+ }
293
+
294
+ // 主函数
295
+ async function main() {
296
+ console.log('===== Cursor-To-OpenAI 邮箱配置管理 =====');
297
+
298
+ // 加载当前配置
299
+ if (loadEnvironment()) {
300
+ // 显示主菜单
301
+ showMainMenu();
302
+ } else {
303
+ console.error('程序退出');
304
+ rl.close();
305
+ }
306
+ }
307
+
308
+ // 运行主函数
309
+ main();
manage-invalid-cookies.js ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // 加载环境变量
2
+ require('dotenv').config();
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const readline = require('readline');
7
+ const keyManager = require('./src/utils/keyManager');
8
+ const logger = require('./src/utils/logger');
9
+
10
+ // 创建命令行交互界面
11
+ const rl = readline.createInterface({
12
+ input: process.stdin,
13
+ output: process.stdout
14
+ });
15
+
16
+ // 初始化API Keys
17
+ keyManager.initializeApiKeys();
18
+
19
+ // 显示菜单
20
+ function showMenu() {
21
+ logger.info('\n===== 无效Cookie管理工具 =====');
22
+ logger.info('1. 查看所有无效Cookie');
23
+ logger.info('2. 添加无效Cookie');
24
+ logger.info('3. 删除特定无效Cookie');
25
+ logger.info('4. 清空所有无效Cookie');
26
+ logger.info('5. 从API Keys中移除所有无效Cookie');
27
+ logger.info('6. 退出');
28
+ logger.info('============================');
29
+
30
+ rl.question('请选择操作 (1-6): ', (answer) => {
31
+ switch(answer) {
32
+ case '1':
33
+ listInvalidCookies();
34
+ break;
35
+ case '2':
36
+ addInvalidCookie();
37
+ break;
38
+ case '3':
39
+ removeInvalidCookie();
40
+ break;
41
+ case '4':
42
+ clearAllInvalidCookies();
43
+ break;
44
+ case '5':
45
+ removeInvalidCookiesFromApiKeys();
46
+ break;
47
+ case '6':
48
+ logger.info('退出程序');
49
+ rl.close();
50
+ break;
51
+ default:
52
+ logger.warn('无效的选择,请重新输入');
53
+ showMenu();
54
+ break;
55
+ }
56
+ });
57
+ }
58
+
59
+ // 查看所有无效Cookie
60
+ function listInvalidCookies() {
61
+ const invalidCookies = Array.from(keyManager.getInvalidCookies());
62
+
63
+ logger.info('\n===== 所有无效Cookie =====');
64
+ if (invalidCookies.length === 0) {
65
+ logger.info('没有无效Cookie');
66
+ } else {
67
+ invalidCookies.forEach((cookie, index) => {
68
+ logger.info(`${index + 1}. ${cookie}`);
69
+ });
70
+ }
71
+
72
+ showMenu();
73
+ }
74
+
75
+ // 添加无效Cookie
76
+ function addInvalidCookie() {
77
+ rl.question('\n请输入要添加的无效Cookie: ', (cookie) => {
78
+ if (!cookie.trim()) {
79
+ logger.warn('Cookie不能为空');
80
+ showMenu();
81
+ return;
82
+ }
83
+
84
+ // 将cookie添加到无效集合
85
+ const invalidCookies = new Set(keyManager.getInvalidCookies());
86
+ invalidCookies.add(cookie.trim());
87
+
88
+ // 保存到文件
89
+ const INVALID_COOKIES_FILE = path.join(__dirname, 'data/invalid_cookies.json');
90
+ try {
91
+ // 确保目录存在
92
+ const dataDir = path.join(__dirname, 'data');
93
+ if (!fs.existsSync(dataDir)) {
94
+ fs.mkdirSync(dataDir, { recursive: true });
95
+ }
96
+
97
+ fs.writeFileSync(INVALID_COOKIES_FILE, JSON.stringify(Array.from(invalidCookies), null, 2), 'utf8');
98
+ logger.info('无效Cookie添加成功');
99
+
100
+ // 重新加载无效cookie
101
+ keyManager.loadInvalidCookiesFromFile();
102
+ } catch (err) {
103
+ logger.error('保存无效Cookie失败:', err);
104
+ }
105
+
106
+ showMenu();
107
+ });
108
+ }
109
+
110
+ // 删除特定无效Cookie
111
+ function removeInvalidCookie() {
112
+ const invalidCookies = Array.from(keyManager.getInvalidCookies());
113
+
114
+ if (invalidCookies.length === 0) {
115
+ logger.warn('\n没有无效Cookie可删除');
116
+ showMenu();
117
+ return;
118
+ }
119
+
120
+ logger.info('\n===== 所有无效Cookie =====');
121
+ invalidCookies.forEach((cookie, index) => {
122
+ logger.info(`${index + 1}. ${cookie}`);
123
+ });
124
+
125
+ rl.question('\n请输入要删除的Cookie编号 (1-' + invalidCookies.length + '): ', (answer) => {
126
+ const index = parseInt(answer) - 1;
127
+
128
+ if (isNaN(index) || index < 0 || index >= invalidCookies.length) {
129
+ logger.warn('无效的编号');
130
+ showMenu();
131
+ return;
132
+ }
133
+
134
+ const cookieToRemove = invalidCookies[index];
135
+ const result = keyManager.clearInvalidCookie(cookieToRemove);
136
+
137
+ if (result) {
138
+ logger.info(`成功删除无效Cookie: ${cookieToRemove}`);
139
+ } else {
140
+ logger.warn('删除失败');
141
+ }
142
+
143
+ showMenu();
144
+ });
145
+ }
146
+
147
+ // 清空所有无效Cookie
148
+ function clearAllInvalidCookies() {
149
+ rl.question('\n确定要清空所有无效Cookie吗? (y/n): ', (answer) => {
150
+ if (answer.toLowerCase() === 'y') {
151
+ keyManager.clearAllInvalidCookies();
152
+ logger.info('所有无效Cookie已清空');
153
+ } else {
154
+ logger.info('操作已取消');
155
+ }
156
+
157
+ showMenu();
158
+ });
159
+ }
160
+
161
+ // 从API Keys中移除所有无效Cookie
162
+ function removeInvalidCookiesFromApiKeys() {
163
+ // 重新初始化API Keys,这会自动移除无效cookie
164
+ keyManager.initializeApiKeys();
165
+ logger.info('已从API Keys中移除所有无效Cookie');
166
+
167
+ showMenu();
168
+ }
169
+
170
+ // 启动程序
171
+ logger.info('正在加载无效Cookie...');
172
+ keyManager.loadInvalidCookiesFromFile();
173
+ showMenu();
package-lock.json ADDED
@@ -0,0 +1,2457 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "cursor-to-openai",
3
+ "version": "1.3.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "cursor-to-openai",
9
+ "version": "1.3.0",
10
+ "license": "MIT",
11
+ "dependencies": {
12
+ "@octokit/rest": "^20.0.2",
13
+ "adm-zip": "^0.5.16",
14
+ "axios": "^1.6.7",
15
+ "cookie-parser": "^1.4.7",
16
+ "csv-parser": "^3.0.0",
17
+ "dotenv": "^16.4.7",
18
+ "express": "4.21.2",
19
+ "jsonwebtoken": "^9.0.2",
20
+ "logger": "^0.0.1",
21
+ "morgan": "^1.10.0",
22
+ "node-cron": "^3.0.3",
23
+ "node-fetch": "^2.7.0",
24
+ "open": "^10.1.0",
25
+ "protobufjs": "^7.4.0",
26
+ "undici": "^6.21.2",
27
+ "uuid": "11.0.5"
28
+ },
29
+ "devDependencies": {
30
+ "protobufjs-cli": "^1.1.3"
31
+ }
32
+ },
33
+ "node_modules/@babel/helper-string-parser": {
34
+ "version": "7.25.9",
35
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
36
+ "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==",
37
+ "dev": true,
38
+ "license": "MIT",
39
+ "engines": {
40
+ "node": ">=6.9.0"
41
+ }
42
+ },
43
+ "node_modules/@babel/helper-validator-identifier": {
44
+ "version": "7.25.9",
45
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
46
+ "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
47
+ "dev": true,
48
+ "license": "MIT",
49
+ "engines": {
50
+ "node": ">=6.9.0"
51
+ }
52
+ },
53
+ "node_modules/@babel/parser": {
54
+ "version": "7.26.7",
55
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.7.tgz",
56
+ "integrity": "sha512-kEvgGGgEjRUutvdVvZhbn/BxVt+5VSpwXz1j3WYXQbXDo8KzFOPNG2GQbdAiNq8g6wn1yKk7C/qrke03a84V+w==",
57
+ "dev": true,
58
+ "license": "MIT",
59
+ "dependencies": {
60
+ "@babel/types": "^7.26.7"
61
+ },
62
+ "bin": {
63
+ "parser": "bin/babel-parser.js"
64
+ },
65
+ "engines": {
66
+ "node": ">=6.0.0"
67
+ }
68
+ },
69
+ "node_modules/@babel/types": {
70
+ "version": "7.26.7",
71
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.7.tgz",
72
+ "integrity": "sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg==",
73
+ "dev": true,
74
+ "license": "MIT",
75
+ "dependencies": {
76
+ "@babel/helper-string-parser": "^7.25.9",
77
+ "@babel/helper-validator-identifier": "^7.25.9"
78
+ },
79
+ "engines": {
80
+ "node": ">=6.9.0"
81
+ }
82
+ },
83
+ "node_modules/@jsdoc/salty": {
84
+ "version": "0.2.9",
85
+ "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.9.tgz",
86
+ "integrity": "sha512-yYxMVH7Dqw6nO0d5NIV8OQWnitU8k6vXH8NtgqAfIa/IUqRMxRv/NUJJ08VEKbAakwxlgBl5PJdrU0dMPStsnw==",
87
+ "dev": true,
88
+ "license": "Apache-2.0",
89
+ "dependencies": {
90
+ "lodash": "^4.17.21"
91
+ },
92
+ "engines": {
93
+ "node": ">=v12.0.0"
94
+ }
95
+ },
96
+ "node_modules/@octokit/auth-token": {
97
+ "version": "4.0.0",
98
+ "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz",
99
+ "integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==",
100
+ "license": "MIT",
101
+ "engines": {
102
+ "node": ">= 18"
103
+ }
104
+ },
105
+ "node_modules/@octokit/core": {
106
+ "version": "5.2.0",
107
+ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.0.tgz",
108
+ "integrity": "sha512-1LFfa/qnMQvEOAdzlQymH0ulepxbxnCYAKJZfMci/5XJyIHWgEYnDmgnKakbTh7CH2tFQ5O60oYDvns4i9RAIg==",
109
+ "license": "MIT",
110
+ "dependencies": {
111
+ "@octokit/auth-token": "^4.0.0",
112
+ "@octokit/graphql": "^7.1.0",
113
+ "@octokit/request": "^8.3.1",
114
+ "@octokit/request-error": "^5.1.0",
115
+ "@octokit/types": "^13.0.0",
116
+ "before-after-hook": "^2.2.0",
117
+ "universal-user-agent": "^6.0.0"
118
+ },
119
+ "engines": {
120
+ "node": ">= 18"
121
+ }
122
+ },
123
+ "node_modules/@octokit/endpoint": {
124
+ "version": "9.0.6",
125
+ "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.6.tgz",
126
+ "integrity": "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==",
127
+ "license": "MIT",
128
+ "dependencies": {
129
+ "@octokit/types": "^13.1.0",
130
+ "universal-user-agent": "^6.0.0"
131
+ },
132
+ "engines": {
133
+ "node": ">= 18"
134
+ }
135
+ },
136
+ "node_modules/@octokit/graphql": {
137
+ "version": "7.1.1",
138
+ "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.1.1.tgz",
139
+ "integrity": "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g==",
140
+ "license": "MIT",
141
+ "dependencies": {
142
+ "@octokit/request": "^8.4.1",
143
+ "@octokit/types": "^13.0.0",
144
+ "universal-user-agent": "^6.0.0"
145
+ },
146
+ "engines": {
147
+ "node": ">= 18"
148
+ }
149
+ },
150
+ "node_modules/@octokit/openapi-types": {
151
+ "version": "23.0.1",
152
+ "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-23.0.1.tgz",
153
+ "integrity": "sha512-izFjMJ1sir0jn0ldEKhZ7xegCTj/ObmEDlEfpFrx4k/JyZSMRHbO3/rBwgE7f3m2DHt+RrNGIVw4wSmwnm3t/g==",
154
+ "license": "MIT"
155
+ },
156
+ "node_modules/@octokit/plugin-paginate-rest": {
157
+ "version": "11.4.4-cjs.2",
158
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.4.4-cjs.2.tgz",
159
+ "integrity": "sha512-2dK6z8fhs8lla5PaOTgqfCGBxgAv/le+EhPs27KklPhm1bKObpu6lXzwfUEQ16ajXzqNrKMujsFyo9K2eaoISw==",
160
+ "license": "MIT",
161
+ "dependencies": {
162
+ "@octokit/types": "^13.7.0"
163
+ },
164
+ "engines": {
165
+ "node": ">= 18"
166
+ },
167
+ "peerDependencies": {
168
+ "@octokit/core": "5"
169
+ }
170
+ },
171
+ "node_modules/@octokit/plugin-request-log": {
172
+ "version": "4.0.1",
173
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-4.0.1.tgz",
174
+ "integrity": "sha512-GihNqNpGHorUrO7Qa9JbAl0dbLnqJVrV8OXe2Zm5/Y4wFkZQDfTreBzVmiRfJVfE4mClXdihHnbpyyO9FSX4HA==",
175
+ "license": "MIT",
176
+ "engines": {
177
+ "node": ">= 18"
178
+ },
179
+ "peerDependencies": {
180
+ "@octokit/core": "5"
181
+ }
182
+ },
183
+ "node_modules/@octokit/plugin-rest-endpoint-methods": {
184
+ "version": "13.3.2-cjs.1",
185
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.3.2-cjs.1.tgz",
186
+ "integrity": "sha512-VUjIjOOvF2oELQmiFpWA1aOPdawpyaCUqcEBc/UOUnj3Xp6DJGrJ1+bjUIIDzdHjnFNO6q57ODMfdEZnoBkCwQ==",
187
+ "license": "MIT",
188
+ "dependencies": {
189
+ "@octokit/types": "^13.8.0"
190
+ },
191
+ "engines": {
192
+ "node": ">= 18"
193
+ },
194
+ "peerDependencies": {
195
+ "@octokit/core": "^5"
196
+ }
197
+ },
198
+ "node_modules/@octokit/request": {
199
+ "version": "8.4.1",
200
+ "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.1.tgz",
201
+ "integrity": "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw==",
202
+ "license": "MIT",
203
+ "dependencies": {
204
+ "@octokit/endpoint": "^9.0.6",
205
+ "@octokit/request-error": "^5.1.1",
206
+ "@octokit/types": "^13.1.0",
207
+ "universal-user-agent": "^6.0.0"
208
+ },
209
+ "engines": {
210
+ "node": ">= 18"
211
+ }
212
+ },
213
+ "node_modules/@octokit/request-error": {
214
+ "version": "5.1.1",
215
+ "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.1.tgz",
216
+ "integrity": "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==",
217
+ "license": "MIT",
218
+ "dependencies": {
219
+ "@octokit/types": "^13.1.0",
220
+ "deprecation": "^2.0.0",
221
+ "once": "^1.4.0"
222
+ },
223
+ "engines": {
224
+ "node": ">= 18"
225
+ }
226
+ },
227
+ "node_modules/@octokit/rest": {
228
+ "version": "20.1.2",
229
+ "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-20.1.2.tgz",
230
+ "integrity": "sha512-GmYiltypkHHtihFwPRxlaorG5R9VAHuk/vbszVoRTGXnAsY60wYLkh/E2XiFmdZmqrisw+9FaazS1i5SbdWYgA==",
231
+ "license": "MIT",
232
+ "dependencies": {
233
+ "@octokit/core": "^5.0.2",
234
+ "@octokit/plugin-paginate-rest": "11.4.4-cjs.2",
235
+ "@octokit/plugin-request-log": "^4.0.0",
236
+ "@octokit/plugin-rest-endpoint-methods": "13.3.2-cjs.1"
237
+ },
238
+ "engines": {
239
+ "node": ">= 18"
240
+ }
241
+ },
242
+ "node_modules/@octokit/types": {
243
+ "version": "13.8.0",
244
+ "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.8.0.tgz",
245
+ "integrity": "sha512-x7DjTIbEpEWXK99DMd01QfWy0hd5h4EN+Q7shkdKds3otGQP+oWE/y0A76i1OvH9fygo4ddvNf7ZvF0t78P98A==",
246
+ "license": "MIT",
247
+ "dependencies": {
248
+ "@octokit/openapi-types": "^23.0.1"
249
+ }
250
+ },
251
+ "node_modules/@protobufjs/aspromise": {
252
+ "version": "1.1.2",
253
+ "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
254
+ "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
255
+ "license": "BSD-3-Clause"
256
+ },
257
+ "node_modules/@protobufjs/base64": {
258
+ "version": "1.1.2",
259
+ "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
260
+ "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
261
+ "license": "BSD-3-Clause"
262
+ },
263
+ "node_modules/@protobufjs/codegen": {
264
+ "version": "2.0.4",
265
+ "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
266
+ "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
267
+ "license": "BSD-3-Clause"
268
+ },
269
+ "node_modules/@protobufjs/eventemitter": {
270
+ "version": "1.1.0",
271
+ "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
272
+ "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
273
+ "license": "BSD-3-Clause"
274
+ },
275
+ "node_modules/@protobufjs/fetch": {
276
+ "version": "1.1.0",
277
+ "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
278
+ "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
279
+ "license": "BSD-3-Clause",
280
+ "dependencies": {
281
+ "@protobufjs/aspromise": "^1.1.1",
282
+ "@protobufjs/inquire": "^1.1.0"
283
+ }
284
+ },
285
+ "node_modules/@protobufjs/float": {
286
+ "version": "1.0.2",
287
+ "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
288
+ "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
289
+ "license": "BSD-3-Clause"
290
+ },
291
+ "node_modules/@protobufjs/inquire": {
292
+ "version": "1.1.0",
293
+ "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
294
+ "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
295
+ "license": "BSD-3-Clause"
296
+ },
297
+ "node_modules/@protobufjs/path": {
298
+ "version": "1.1.2",
299
+ "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
300
+ "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
301
+ "license": "BSD-3-Clause"
302
+ },
303
+ "node_modules/@protobufjs/pool": {
304
+ "version": "1.1.0",
305
+ "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
306
+ "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
307
+ "license": "BSD-3-Clause"
308
+ },
309
+ "node_modules/@protobufjs/utf8": {
310
+ "version": "1.1.0",
311
+ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
312
+ "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
313
+ "license": "BSD-3-Clause"
314
+ },
315
+ "node_modules/@types/linkify-it": {
316
+ "version": "5.0.0",
317
+ "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
318
+ "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
319
+ "dev": true,
320
+ "license": "MIT"
321
+ },
322
+ "node_modules/@types/markdown-it": {
323
+ "version": "14.1.2",
324
+ "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
325
+ "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==",
326
+ "dev": true,
327
+ "license": "MIT",
328
+ "dependencies": {
329
+ "@types/linkify-it": "^5",
330
+ "@types/mdurl": "^2"
331
+ }
332
+ },
333
+ "node_modules/@types/mdurl": {
334
+ "version": "2.0.0",
335
+ "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
336
+ "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
337
+ "dev": true,
338
+ "license": "MIT"
339
+ },
340
+ "node_modules/@types/node": {
341
+ "version": "22.10.5",
342
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz",
343
+ "integrity": "sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==",
344
+ "license": "MIT",
345
+ "dependencies": {
346
+ "undici-types": "~6.20.0"
347
+ }
348
+ },
349
+ "node_modules/accepts": {
350
+ "version": "1.3.8",
351
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
352
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
353
+ "license": "MIT",
354
+ "dependencies": {
355
+ "mime-types": "~2.1.34",
356
+ "negotiator": "0.6.3"
357
+ },
358
+ "engines": {
359
+ "node": ">= 0.6"
360
+ }
361
+ },
362
+ "node_modules/acorn": {
363
+ "version": "8.14.0",
364
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
365
+ "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
366
+ "dev": true,
367
+ "license": "MIT",
368
+ "bin": {
369
+ "acorn": "bin/acorn"
370
+ },
371
+ "engines": {
372
+ "node": ">=0.4.0"
373
+ }
374
+ },
375
+ "node_modules/acorn-jsx": {
376
+ "version": "5.3.2",
377
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
378
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
379
+ "dev": true,
380
+ "license": "MIT",
381
+ "peerDependencies": {
382
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
383
+ }
384
+ },
385
+ "node_modules/adm-zip": {
386
+ "version": "0.5.16",
387
+ "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz",
388
+ "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==",
389
+ "license": "MIT",
390
+ "engines": {
391
+ "node": ">=12.0"
392
+ }
393
+ },
394
+ "node_modules/ansi-styles": {
395
+ "version": "4.3.0",
396
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
397
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
398
+ "dev": true,
399
+ "license": "MIT",
400
+ "dependencies": {
401
+ "color-convert": "^2.0.1"
402
+ },
403
+ "engines": {
404
+ "node": ">=8"
405
+ },
406
+ "funding": {
407
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
408
+ }
409
+ },
410
+ "node_modules/argparse": {
411
+ "version": "2.0.1",
412
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
413
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
414
+ "dev": true,
415
+ "license": "Python-2.0"
416
+ },
417
+ "node_modules/array-flatten": {
418
+ "version": "1.1.1",
419
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
420
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
421
+ "license": "MIT"
422
+ },
423
+ "node_modules/asynckit": {
424
+ "version": "0.4.0",
425
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
426
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
427
+ "license": "MIT"
428
+ },
429
+ "node_modules/axios": {
430
+ "version": "1.8.3",
431
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz",
432
+ "integrity": "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==",
433
+ "license": "MIT",
434
+ "dependencies": {
435
+ "follow-redirects": "^1.15.6",
436
+ "form-data": "^4.0.0",
437
+ "proxy-from-env": "^1.1.0"
438
+ }
439
+ },
440
+ "node_modules/balanced-match": {
441
+ "version": "1.0.2",
442
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
443
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
444
+ "dev": true,
445
+ "license": "MIT"
446
+ },
447
+ "node_modules/basic-auth": {
448
+ "version": "2.0.1",
449
+ "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
450
+ "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
451
+ "license": "MIT",
452
+ "dependencies": {
453
+ "safe-buffer": "5.1.2"
454
+ },
455
+ "engines": {
456
+ "node": ">= 0.8"
457
+ }
458
+ },
459
+ "node_modules/basic-auth/node_modules/safe-buffer": {
460
+ "version": "5.1.2",
461
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
462
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
463
+ "license": "MIT"
464
+ },
465
+ "node_modules/before-after-hook": {
466
+ "version": "2.2.3",
467
+ "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz",
468
+ "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==",
469
+ "license": "Apache-2.0"
470
+ },
471
+ "node_modules/bluebird": {
472
+ "version": "3.7.2",
473
+ "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
474
+ "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
475
+ "dev": true,
476
+ "license": "MIT"
477
+ },
478
+ "node_modules/body-parser": {
479
+ "version": "1.20.3",
480
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
481
+ "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
482
+ "license": "MIT",
483
+ "dependencies": {
484
+ "bytes": "3.1.2",
485
+ "content-type": "~1.0.5",
486
+ "debug": "2.6.9",
487
+ "depd": "2.0.0",
488
+ "destroy": "1.2.0",
489
+ "http-errors": "2.0.0",
490
+ "iconv-lite": "0.4.24",
491
+ "on-finished": "2.4.1",
492
+ "qs": "6.13.0",
493
+ "raw-body": "2.5.2",
494
+ "type-is": "~1.6.18",
495
+ "unpipe": "1.0.0"
496
+ },
497
+ "engines": {
498
+ "node": ">= 0.8",
499
+ "npm": "1.2.8000 || >= 1.4.16"
500
+ }
501
+ },
502
+ "node_modules/brace-expansion": {
503
+ "version": "2.0.1",
504
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
505
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
506
+ "dev": true,
507
+ "license": "MIT",
508
+ "dependencies": {
509
+ "balanced-match": "^1.0.0"
510
+ }
511
+ },
512
+ "node_modules/buffer-equal-constant-time": {
513
+ "version": "1.0.1",
514
+ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
515
+ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
516
+ "license": "BSD-3-Clause"
517
+ },
518
+ "node_modules/bundle-name": {
519
+ "version": "4.1.0",
520
+ "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz",
521
+ "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==",
522
+ "license": "MIT",
523
+ "dependencies": {
524
+ "run-applescript": "^7.0.0"
525
+ },
526
+ "engines": {
527
+ "node": ">=18"
528
+ },
529
+ "funding": {
530
+ "url": "https://github.com/sponsors/sindresorhus"
531
+ }
532
+ },
533
+ "node_modules/bytes": {
534
+ "version": "3.1.2",
535
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
536
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
537
+ "license": "MIT",
538
+ "engines": {
539
+ "node": ">= 0.8"
540
+ }
541
+ },
542
+ "node_modules/call-bind-apply-helpers": {
543
+ "version": "1.0.1",
544
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz",
545
+ "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==",
546
+ "license": "MIT",
547
+ "dependencies": {
548
+ "es-errors": "^1.3.0",
549
+ "function-bind": "^1.1.2"
550
+ },
551
+ "engines": {
552
+ "node": ">= 0.4"
553
+ }
554
+ },
555
+ "node_modules/call-bound": {
556
+ "version": "1.0.3",
557
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz",
558
+ "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==",
559
+ "license": "MIT",
560
+ "dependencies": {
561
+ "call-bind-apply-helpers": "^1.0.1",
562
+ "get-intrinsic": "^1.2.6"
563
+ },
564
+ "engines": {
565
+ "node": ">= 0.4"
566
+ },
567
+ "funding": {
568
+ "url": "https://github.com/sponsors/ljharb"
569
+ }
570
+ },
571
+ "node_modules/catharsis": {
572
+ "version": "0.9.0",
573
+ "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz",
574
+ "integrity": "sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==",
575
+ "dev": true,
576
+ "license": "MIT",
577
+ "dependencies": {
578
+ "lodash": "^4.17.15"
579
+ },
580
+ "engines": {
581
+ "node": ">= 10"
582
+ }
583
+ },
584
+ "node_modules/chalk": {
585
+ "version": "4.1.2",
586
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
587
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
588
+ "dev": true,
589
+ "license": "MIT",
590
+ "dependencies": {
591
+ "ansi-styles": "^4.1.0",
592
+ "supports-color": "^7.1.0"
593
+ },
594
+ "engines": {
595
+ "node": ">=10"
596
+ },
597
+ "funding": {
598
+ "url": "https://github.com/chalk/chalk?sponsor=1"
599
+ }
600
+ },
601
+ "node_modules/color-convert": {
602
+ "version": "2.0.1",
603
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
604
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
605
+ "dev": true,
606
+ "license": "MIT",
607
+ "dependencies": {
608
+ "color-name": "~1.1.4"
609
+ },
610
+ "engines": {
611
+ "node": ">=7.0.0"
612
+ }
613
+ },
614
+ "node_modules/color-name": {
615
+ "version": "1.1.4",
616
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
617
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
618
+ "dev": true,
619
+ "license": "MIT"
620
+ },
621
+ "node_modules/combined-stream": {
622
+ "version": "1.0.8",
623
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
624
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
625
+ "license": "MIT",
626
+ "dependencies": {
627
+ "delayed-stream": "~1.0.0"
628
+ },
629
+ "engines": {
630
+ "node": ">= 0.8"
631
+ }
632
+ },
633
+ "node_modules/content-disposition": {
634
+ "version": "0.5.4",
635
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
636
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
637
+ "license": "MIT",
638
+ "dependencies": {
639
+ "safe-buffer": "5.2.1"
640
+ },
641
+ "engines": {
642
+ "node": ">= 0.6"
643
+ }
644
+ },
645
+ "node_modules/content-type": {
646
+ "version": "1.0.5",
647
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
648
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
649
+ "license": "MIT",
650
+ "engines": {
651
+ "node": ">= 0.6"
652
+ }
653
+ },
654
+ "node_modules/cookie": {
655
+ "version": "0.7.1",
656
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
657
+ "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
658
+ "license": "MIT",
659
+ "engines": {
660
+ "node": ">= 0.6"
661
+ }
662
+ },
663
+ "node_modules/cookie-parser": {
664
+ "version": "1.4.7",
665
+ "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz",
666
+ "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==",
667
+ "license": "MIT",
668
+ "dependencies": {
669
+ "cookie": "0.7.2",
670
+ "cookie-signature": "1.0.6"
671
+ },
672
+ "engines": {
673
+ "node": ">= 0.8.0"
674
+ }
675
+ },
676
+ "node_modules/cookie-parser/node_modules/cookie": {
677
+ "version": "0.7.2",
678
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
679
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
680
+ "license": "MIT",
681
+ "engines": {
682
+ "node": ">= 0.6"
683
+ }
684
+ },
685
+ "node_modules/cookie-signature": {
686
+ "version": "1.0.6",
687
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
688
+ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
689
+ "license": "MIT"
690
+ },
691
+ "node_modules/csv-parser": {
692
+ "version": "3.2.0",
693
+ "resolved": "https://registry.npmjs.org/csv-parser/-/csv-parser-3.2.0.tgz",
694
+ "integrity": "sha512-fgKbp+AJbn1h2dcAHKIdKNSSjfp43BZZykXsCjzALjKy80VXQNHPFJ6T9Afwdzoj24aMkq8GwDS7KGcDPpejrA==",
695
+ "license": "MIT",
696
+ "bin": {
697
+ "csv-parser": "bin/csv-parser"
698
+ },
699
+ "engines": {
700
+ "node": ">= 10"
701
+ }
702
+ },
703
+ "node_modules/debug": {
704
+ "version": "2.6.9",
705
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
706
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
707
+ "license": "MIT",
708
+ "dependencies": {
709
+ "ms": "2.0.0"
710
+ }
711
+ },
712
+ "node_modules/deep-is": {
713
+ "version": "0.1.4",
714
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
715
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
716
+ "dev": true,
717
+ "license": "MIT"
718
+ },
719
+ "node_modules/default-browser": {
720
+ "version": "5.2.1",
721
+ "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz",
722
+ "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==",
723
+ "license": "MIT",
724
+ "dependencies": {
725
+ "bundle-name": "^4.1.0",
726
+ "default-browser-id": "^5.0.0"
727
+ },
728
+ "engines": {
729
+ "node": ">=18"
730
+ },
731
+ "funding": {
732
+ "url": "https://github.com/sponsors/sindresorhus"
733
+ }
734
+ },
735
+ "node_modules/default-browser-id": {
736
+ "version": "5.0.0",
737
+ "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz",
738
+ "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==",
739
+ "license": "MIT",
740
+ "engines": {
741
+ "node": ">=18"
742
+ },
743
+ "funding": {
744
+ "url": "https://github.com/sponsors/sindresorhus"
745
+ }
746
+ },
747
+ "node_modules/define-lazy-prop": {
748
+ "version": "3.0.0",
749
+ "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz",
750
+ "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==",
751
+ "license": "MIT",
752
+ "engines": {
753
+ "node": ">=12"
754
+ },
755
+ "funding": {
756
+ "url": "https://github.com/sponsors/sindresorhus"
757
+ }
758
+ },
759
+ "node_modules/delayed-stream": {
760
+ "version": "1.0.0",
761
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
762
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
763
+ "license": "MIT",
764
+ "engines": {
765
+ "node": ">=0.4.0"
766
+ }
767
+ },
768
+ "node_modules/depd": {
769
+ "version": "2.0.0",
770
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
771
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
772
+ "license": "MIT",
773
+ "engines": {
774
+ "node": ">= 0.8"
775
+ }
776
+ },
777
+ "node_modules/deprecation": {
778
+ "version": "2.3.1",
779
+ "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz",
780
+ "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==",
781
+ "license": "ISC"
782
+ },
783
+ "node_modules/destroy": {
784
+ "version": "1.2.0",
785
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
786
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
787
+ "license": "MIT",
788
+ "engines": {
789
+ "node": ">= 0.8",
790
+ "npm": "1.2.8000 || >= 1.4.16"
791
+ }
792
+ },
793
+ "node_modules/dotenv": {
794
+ "version": "16.4.7",
795
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
796
+ "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==",
797
+ "license": "BSD-2-Clause",
798
+ "engines": {
799
+ "node": ">=12"
800
+ },
801
+ "funding": {
802
+ "url": "https://dotenvx.com"
803
+ }
804
+ },
805
+ "node_modules/dunder-proto": {
806
+ "version": "1.0.1",
807
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
808
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
809
+ "license": "MIT",
810
+ "dependencies": {
811
+ "call-bind-apply-helpers": "^1.0.1",
812
+ "es-errors": "^1.3.0",
813
+ "gopd": "^1.2.0"
814
+ },
815
+ "engines": {
816
+ "node": ">= 0.4"
817
+ }
818
+ },
819
+ "node_modules/ecdsa-sig-formatter": {
820
+ "version": "1.0.11",
821
+ "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
822
+ "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
823
+ "license": "Apache-2.0",
824
+ "dependencies": {
825
+ "safe-buffer": "^5.0.1"
826
+ }
827
+ },
828
+ "node_modules/ee-first": {
829
+ "version": "1.1.1",
830
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
831
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
832
+ "license": "MIT"
833
+ },
834
+ "node_modules/encodeurl": {
835
+ "version": "2.0.0",
836
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
837
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
838
+ "license": "MIT",
839
+ "engines": {
840
+ "node": ">= 0.8"
841
+ }
842
+ },
843
+ "node_modules/entities": {
844
+ "version": "4.5.0",
845
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
846
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
847
+ "dev": true,
848
+ "license": "BSD-2-Clause",
849
+ "engines": {
850
+ "node": ">=0.12"
851
+ },
852
+ "funding": {
853
+ "url": "https://github.com/fb55/entities?sponsor=1"
854
+ }
855
+ },
856
+ "node_modules/es-define-property": {
857
+ "version": "1.0.1",
858
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
859
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
860
+ "license": "MIT",
861
+ "engines": {
862
+ "node": ">= 0.4"
863
+ }
864
+ },
865
+ "node_modules/es-errors": {
866
+ "version": "1.3.0",
867
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
868
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
869
+ "license": "MIT",
870
+ "engines": {
871
+ "node": ">= 0.4"
872
+ }
873
+ },
874
+ "node_modules/es-object-atoms": {
875
+ "version": "1.0.0",
876
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz",
877
+ "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==",
878
+ "license": "MIT",
879
+ "dependencies": {
880
+ "es-errors": "^1.3.0"
881
+ },
882
+ "engines": {
883
+ "node": ">= 0.4"
884
+ }
885
+ },
886
+ "node_modules/es-set-tostringtag": {
887
+ "version": "2.1.0",
888
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
889
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
890
+ "license": "MIT",
891
+ "dependencies": {
892
+ "es-errors": "^1.3.0",
893
+ "get-intrinsic": "^1.2.6",
894
+ "has-tostringtag": "^1.0.2",
895
+ "hasown": "^2.0.2"
896
+ },
897
+ "engines": {
898
+ "node": ">= 0.4"
899
+ }
900
+ },
901
+ "node_modules/escape-html": {
902
+ "version": "1.0.3",
903
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
904
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
905
+ "license": "MIT"
906
+ },
907
+ "node_modules/escape-string-regexp": {
908
+ "version": "2.0.0",
909
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
910
+ "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==",
911
+ "dev": true,
912
+ "license": "MIT",
913
+ "engines": {
914
+ "node": ">=8"
915
+ }
916
+ },
917
+ "node_modules/escodegen": {
918
+ "version": "1.14.3",
919
+ "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz",
920
+ "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==",
921
+ "dev": true,
922
+ "license": "BSD-2-Clause",
923
+ "dependencies": {
924
+ "esprima": "^4.0.1",
925
+ "estraverse": "^4.2.0",
926
+ "esutils": "^2.0.2",
927
+ "optionator": "^0.8.1"
928
+ },
929
+ "bin": {
930
+ "escodegen": "bin/escodegen.js",
931
+ "esgenerate": "bin/esgenerate.js"
932
+ },
933
+ "engines": {
934
+ "node": ">=4.0"
935
+ },
936
+ "optionalDependencies": {
937
+ "source-map": "~0.6.1"
938
+ }
939
+ },
940
+ "node_modules/escodegen/node_modules/estraverse": {
941
+ "version": "4.3.0",
942
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
943
+ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
944
+ "dev": true,
945
+ "license": "BSD-2-Clause",
946
+ "engines": {
947
+ "node": ">=4.0"
948
+ }
949
+ },
950
+ "node_modules/eslint-visitor-keys": {
951
+ "version": "3.4.3",
952
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
953
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
954
+ "dev": true,
955
+ "license": "Apache-2.0",
956
+ "engines": {
957
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
958
+ },
959
+ "funding": {
960
+ "url": "https://opencollective.com/eslint"
961
+ }
962
+ },
963
+ "node_modules/espree": {
964
+ "version": "9.6.1",
965
+ "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
966
+ "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
967
+ "dev": true,
968
+ "license": "BSD-2-Clause",
969
+ "dependencies": {
970
+ "acorn": "^8.9.0",
971
+ "acorn-jsx": "^5.3.2",
972
+ "eslint-visitor-keys": "^3.4.1"
973
+ },
974
+ "engines": {
975
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
976
+ },
977
+ "funding": {
978
+ "url": "https://opencollective.com/eslint"
979
+ }
980
+ },
981
+ "node_modules/esprima": {
982
+ "version": "4.0.1",
983
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
984
+ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
985
+ "dev": true,
986
+ "license": "BSD-2-Clause",
987
+ "bin": {
988
+ "esparse": "bin/esparse.js",
989
+ "esvalidate": "bin/esvalidate.js"
990
+ },
991
+ "engines": {
992
+ "node": ">=4"
993
+ }
994
+ },
995
+ "node_modules/estraverse": {
996
+ "version": "5.3.0",
997
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
998
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
999
+ "dev": true,
1000
+ "license": "BSD-2-Clause",
1001
+ "engines": {
1002
+ "node": ">=4.0"
1003
+ }
1004
+ },
1005
+ "node_modules/esutils": {
1006
+ "version": "2.0.3",
1007
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
1008
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
1009
+ "dev": true,
1010
+ "license": "BSD-2-Clause",
1011
+ "engines": {
1012
+ "node": ">=0.10.0"
1013
+ }
1014
+ },
1015
+ "node_modules/etag": {
1016
+ "version": "1.8.1",
1017
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
1018
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
1019
+ "license": "MIT",
1020
+ "engines": {
1021
+ "node": ">= 0.6"
1022
+ }
1023
+ },
1024
+ "node_modules/express": {
1025
+ "version": "4.21.2",
1026
+ "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
1027
+ "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
1028
+ "license": "MIT",
1029
+ "dependencies": {
1030
+ "accepts": "~1.3.8",
1031
+ "array-flatten": "1.1.1",
1032
+ "body-parser": "1.20.3",
1033
+ "content-disposition": "0.5.4",
1034
+ "content-type": "~1.0.4",
1035
+ "cookie": "0.7.1",
1036
+ "cookie-signature": "1.0.6",
1037
+ "debug": "2.6.9",
1038
+ "depd": "2.0.0",
1039
+ "encodeurl": "~2.0.0",
1040
+ "escape-html": "~1.0.3",
1041
+ "etag": "~1.8.1",
1042
+ "finalhandler": "1.3.1",
1043
+ "fresh": "0.5.2",
1044
+ "http-errors": "2.0.0",
1045
+ "merge-descriptors": "1.0.3",
1046
+ "methods": "~1.1.2",
1047
+ "on-finished": "2.4.1",
1048
+ "parseurl": "~1.3.3",
1049
+ "path-to-regexp": "0.1.12",
1050
+ "proxy-addr": "~2.0.7",
1051
+ "qs": "6.13.0",
1052
+ "range-parser": "~1.2.1",
1053
+ "safe-buffer": "5.2.1",
1054
+ "send": "0.19.0",
1055
+ "serve-static": "1.16.2",
1056
+ "setprototypeof": "1.2.0",
1057
+ "statuses": "2.0.1",
1058
+ "type-is": "~1.6.18",
1059
+ "utils-merge": "1.0.1",
1060
+ "vary": "~1.1.2"
1061
+ },
1062
+ "engines": {
1063
+ "node": ">= 0.10.0"
1064
+ },
1065
+ "funding": {
1066
+ "type": "opencollective",
1067
+ "url": "https://opencollective.com/express"
1068
+ }
1069
+ },
1070
+ "node_modules/fast-levenshtein": {
1071
+ "version": "2.0.6",
1072
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
1073
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
1074
+ "dev": true,
1075
+ "license": "MIT"
1076
+ },
1077
+ "node_modules/finalhandler": {
1078
+ "version": "1.3.1",
1079
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
1080
+ "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
1081
+ "license": "MIT",
1082
+ "dependencies": {
1083
+ "debug": "2.6.9",
1084
+ "encodeurl": "~2.0.0",
1085
+ "escape-html": "~1.0.3",
1086
+ "on-finished": "2.4.1",
1087
+ "parseurl": "~1.3.3",
1088
+ "statuses": "2.0.1",
1089
+ "unpipe": "~1.0.0"
1090
+ },
1091
+ "engines": {
1092
+ "node": ">= 0.8"
1093
+ }
1094
+ },
1095
+ "node_modules/follow-redirects": {
1096
+ "version": "1.15.9",
1097
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
1098
+ "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
1099
+ "funding": [
1100
+ {
1101
+ "type": "individual",
1102
+ "url": "https://github.com/sponsors/RubenVerborgh"
1103
+ }
1104
+ ],
1105
+ "license": "MIT",
1106
+ "engines": {
1107
+ "node": ">=4.0"
1108
+ },
1109
+ "peerDependenciesMeta": {
1110
+ "debug": {
1111
+ "optional": true
1112
+ }
1113
+ }
1114
+ },
1115
+ "node_modules/form-data": {
1116
+ "version": "4.0.2",
1117
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
1118
+ "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
1119
+ "license": "MIT",
1120
+ "dependencies": {
1121
+ "asynckit": "^0.4.0",
1122
+ "combined-stream": "^1.0.8",
1123
+ "es-set-tostringtag": "^2.1.0",
1124
+ "mime-types": "^2.1.12"
1125
+ },
1126
+ "engines": {
1127
+ "node": ">= 6"
1128
+ }
1129
+ },
1130
+ "node_modules/forwarded": {
1131
+ "version": "0.2.0",
1132
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
1133
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
1134
+ "license": "MIT",
1135
+ "engines": {
1136
+ "node": ">= 0.6"
1137
+ }
1138
+ },
1139
+ "node_modules/fresh": {
1140
+ "version": "0.5.2",
1141
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
1142
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
1143
+ "license": "MIT",
1144
+ "engines": {
1145
+ "node": ">= 0.6"
1146
+ }
1147
+ },
1148
+ "node_modules/fs.realpath": {
1149
+ "version": "1.0.0",
1150
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
1151
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
1152
+ "dev": true,
1153
+ "license": "ISC"
1154
+ },
1155
+ "node_modules/function-bind": {
1156
+ "version": "1.1.2",
1157
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
1158
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
1159
+ "license": "MIT",
1160
+ "funding": {
1161
+ "url": "https://github.com/sponsors/ljharb"
1162
+ }
1163
+ },
1164
+ "node_modules/get-intrinsic": {
1165
+ "version": "1.2.7",
1166
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz",
1167
+ "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==",
1168
+ "license": "MIT",
1169
+ "dependencies": {
1170
+ "call-bind-apply-helpers": "^1.0.1",
1171
+ "es-define-property": "^1.0.1",
1172
+ "es-errors": "^1.3.0",
1173
+ "es-object-atoms": "^1.0.0",
1174
+ "function-bind": "^1.1.2",
1175
+ "get-proto": "^1.0.0",
1176
+ "gopd": "^1.2.0",
1177
+ "has-symbols": "^1.1.0",
1178
+ "hasown": "^2.0.2",
1179
+ "math-intrinsics": "^1.1.0"
1180
+ },
1181
+ "engines": {
1182
+ "node": ">= 0.4"
1183
+ },
1184
+ "funding": {
1185
+ "url": "https://github.com/sponsors/ljharb"
1186
+ }
1187
+ },
1188
+ "node_modules/get-proto": {
1189
+ "version": "1.0.1",
1190
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
1191
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
1192
+ "license": "MIT",
1193
+ "dependencies": {
1194
+ "dunder-proto": "^1.0.1",
1195
+ "es-object-atoms": "^1.0.0"
1196
+ },
1197
+ "engines": {
1198
+ "node": ">= 0.4"
1199
+ }
1200
+ },
1201
+ "node_modules/glob": {
1202
+ "version": "8.1.0",
1203
+ "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz",
1204
+ "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==",
1205
+ "deprecated": "Glob versions prior to v9 are no longer supported",
1206
+ "dev": true,
1207
+ "license": "ISC",
1208
+ "dependencies": {
1209
+ "fs.realpath": "^1.0.0",
1210
+ "inflight": "^1.0.4",
1211
+ "inherits": "2",
1212
+ "minimatch": "^5.0.1",
1213
+ "once": "^1.3.0"
1214
+ },
1215
+ "engines": {
1216
+ "node": ">=12"
1217
+ },
1218
+ "funding": {
1219
+ "url": "https://github.com/sponsors/isaacs"
1220
+ }
1221
+ },
1222
+ "node_modules/gopd": {
1223
+ "version": "1.2.0",
1224
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
1225
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
1226
+ "license": "MIT",
1227
+ "engines": {
1228
+ "node": ">= 0.4"
1229
+ },
1230
+ "funding": {
1231
+ "url": "https://github.com/sponsors/ljharb"
1232
+ }
1233
+ },
1234
+ "node_modules/graceful-fs": {
1235
+ "version": "4.2.11",
1236
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
1237
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
1238
+ "dev": true,
1239
+ "license": "ISC"
1240
+ },
1241
+ "node_modules/has-flag": {
1242
+ "version": "4.0.0",
1243
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
1244
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
1245
+ "dev": true,
1246
+ "license": "MIT",
1247
+ "engines": {
1248
+ "node": ">=8"
1249
+ }
1250
+ },
1251
+ "node_modules/has-symbols": {
1252
+ "version": "1.1.0",
1253
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
1254
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
1255
+ "license": "MIT",
1256
+ "engines": {
1257
+ "node": ">= 0.4"
1258
+ },
1259
+ "funding": {
1260
+ "url": "https://github.com/sponsors/ljharb"
1261
+ }
1262
+ },
1263
+ "node_modules/has-tostringtag": {
1264
+ "version": "1.0.2",
1265
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
1266
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
1267
+ "license": "MIT",
1268
+ "dependencies": {
1269
+ "has-symbols": "^1.0.3"
1270
+ },
1271
+ "engines": {
1272
+ "node": ">= 0.4"
1273
+ },
1274
+ "funding": {
1275
+ "url": "https://github.com/sponsors/ljharb"
1276
+ }
1277
+ },
1278
+ "node_modules/hasown": {
1279
+ "version": "2.0.2",
1280
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
1281
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
1282
+ "license": "MIT",
1283
+ "dependencies": {
1284
+ "function-bind": "^1.1.2"
1285
+ },
1286
+ "engines": {
1287
+ "node": ">= 0.4"
1288
+ }
1289
+ },
1290
+ "node_modules/http-errors": {
1291
+ "version": "2.0.0",
1292
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
1293
+ "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
1294
+ "license": "MIT",
1295
+ "dependencies": {
1296
+ "depd": "2.0.0",
1297
+ "inherits": "2.0.4",
1298
+ "setprototypeof": "1.2.0",
1299
+ "statuses": "2.0.1",
1300
+ "toidentifier": "1.0.1"
1301
+ },
1302
+ "engines": {
1303
+ "node": ">= 0.8"
1304
+ }
1305
+ },
1306
+ "node_modules/iconv-lite": {
1307
+ "version": "0.4.24",
1308
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
1309
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
1310
+ "license": "MIT",
1311
+ "dependencies": {
1312
+ "safer-buffer": ">= 2.1.2 < 3"
1313
+ },
1314
+ "engines": {
1315
+ "node": ">=0.10.0"
1316
+ }
1317
+ },
1318
+ "node_modules/inflight": {
1319
+ "version": "1.0.6",
1320
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
1321
+ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
1322
+ "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
1323
+ "dev": true,
1324
+ "license": "ISC",
1325
+ "dependencies": {
1326
+ "once": "^1.3.0",
1327
+ "wrappy": "1"
1328
+ }
1329
+ },
1330
+ "node_modules/inherits": {
1331
+ "version": "2.0.4",
1332
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
1333
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
1334
+ "license": "ISC"
1335
+ },
1336
+ "node_modules/ipaddr.js": {
1337
+ "version": "1.9.1",
1338
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
1339
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
1340
+ "license": "MIT",
1341
+ "engines": {
1342
+ "node": ">= 0.10"
1343
+ }
1344
+ },
1345
+ "node_modules/is-docker": {
1346
+ "version": "3.0.0",
1347
+ "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz",
1348
+ "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==",
1349
+ "license": "MIT",
1350
+ "bin": {
1351
+ "is-docker": "cli.js"
1352
+ },
1353
+ "engines": {
1354
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
1355
+ },
1356
+ "funding": {
1357
+ "url": "https://github.com/sponsors/sindresorhus"
1358
+ }
1359
+ },
1360
+ "node_modules/is-inside-container": {
1361
+ "version": "1.0.0",
1362
+ "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz",
1363
+ "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==",
1364
+ "license": "MIT",
1365
+ "dependencies": {
1366
+ "is-docker": "^3.0.0"
1367
+ },
1368
+ "bin": {
1369
+ "is-inside-container": "cli.js"
1370
+ },
1371
+ "engines": {
1372
+ "node": ">=14.16"
1373
+ },
1374
+ "funding": {
1375
+ "url": "https://github.com/sponsors/sindresorhus"
1376
+ }
1377
+ },
1378
+ "node_modules/is-wsl": {
1379
+ "version": "3.1.0",
1380
+ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz",
1381
+ "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==",
1382
+ "license": "MIT",
1383
+ "dependencies": {
1384
+ "is-inside-container": "^1.0.0"
1385
+ },
1386
+ "engines": {
1387
+ "node": ">=16"
1388
+ },
1389
+ "funding": {
1390
+ "url": "https://github.com/sponsors/sindresorhus"
1391
+ }
1392
+ },
1393
+ "node_modules/js2xmlparser": {
1394
+ "version": "4.0.2",
1395
+ "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz",
1396
+ "integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==",
1397
+ "dev": true,
1398
+ "license": "Apache-2.0",
1399
+ "dependencies": {
1400
+ "xmlcreate": "^2.0.4"
1401
+ }
1402
+ },
1403
+ "node_modules/jsdoc": {
1404
+ "version": "4.0.4",
1405
+ "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.4.tgz",
1406
+ "integrity": "sha512-zeFezwyXeG4syyYHbvh1A967IAqq/67yXtXvuL5wnqCkFZe8I0vKfm+EO+YEvLguo6w9CDUbrAXVtJSHh2E8rw==",
1407
+ "dev": true,
1408
+ "license": "Apache-2.0",
1409
+ "dependencies": {
1410
+ "@babel/parser": "^7.20.15",
1411
+ "@jsdoc/salty": "^0.2.1",
1412
+ "@types/markdown-it": "^14.1.1",
1413
+ "bluebird": "^3.7.2",
1414
+ "catharsis": "^0.9.0",
1415
+ "escape-string-regexp": "^2.0.0",
1416
+ "js2xmlparser": "^4.0.2",
1417
+ "klaw": "^3.0.0",
1418
+ "markdown-it": "^14.1.0",
1419
+ "markdown-it-anchor": "^8.6.7",
1420
+ "marked": "^4.0.10",
1421
+ "mkdirp": "^1.0.4",
1422
+ "requizzle": "^0.2.3",
1423
+ "strip-json-comments": "^3.1.0",
1424
+ "underscore": "~1.13.2"
1425
+ },
1426
+ "bin": {
1427
+ "jsdoc": "jsdoc.js"
1428
+ },
1429
+ "engines": {
1430
+ "node": ">=12.0.0"
1431
+ }
1432
+ },
1433
+ "node_modules/jsonwebtoken": {
1434
+ "version": "9.0.2",
1435
+ "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
1436
+ "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
1437
+ "license": "MIT",
1438
+ "dependencies": {
1439
+ "jws": "^3.2.2",
1440
+ "lodash.includes": "^4.3.0",
1441
+ "lodash.isboolean": "^3.0.3",
1442
+ "lodash.isinteger": "^4.0.4",
1443
+ "lodash.isnumber": "^3.0.3",
1444
+ "lodash.isplainobject": "^4.0.6",
1445
+ "lodash.isstring": "^4.0.1",
1446
+ "lodash.once": "^4.0.0",
1447
+ "ms": "^2.1.1",
1448
+ "semver": "^7.5.4"
1449
+ },
1450
+ "engines": {
1451
+ "node": ">=12",
1452
+ "npm": ">=6"
1453
+ }
1454
+ },
1455
+ "node_modules/jsonwebtoken/node_modules/ms": {
1456
+ "version": "2.1.3",
1457
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1458
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
1459
+ "license": "MIT"
1460
+ },
1461
+ "node_modules/jwa": {
1462
+ "version": "1.4.1",
1463
+ "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
1464
+ "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
1465
+ "license": "MIT",
1466
+ "dependencies": {
1467
+ "buffer-equal-constant-time": "1.0.1",
1468
+ "ecdsa-sig-formatter": "1.0.11",
1469
+ "safe-buffer": "^5.0.1"
1470
+ }
1471
+ },
1472
+ "node_modules/jws": {
1473
+ "version": "3.2.2",
1474
+ "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
1475
+ "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
1476
+ "license": "MIT",
1477
+ "dependencies": {
1478
+ "jwa": "^1.4.1",
1479
+ "safe-buffer": "^5.0.1"
1480
+ }
1481
+ },
1482
+ "node_modules/klaw": {
1483
+ "version": "3.0.0",
1484
+ "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz",
1485
+ "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==",
1486
+ "dev": true,
1487
+ "license": "MIT",
1488
+ "dependencies": {
1489
+ "graceful-fs": "^4.1.9"
1490
+ }
1491
+ },
1492
+ "node_modules/levn": {
1493
+ "version": "0.3.0",
1494
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
1495
+ "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==",
1496
+ "dev": true,
1497
+ "license": "MIT",
1498
+ "dependencies": {
1499
+ "prelude-ls": "~1.1.2",
1500
+ "type-check": "~0.3.2"
1501
+ },
1502
+ "engines": {
1503
+ "node": ">= 0.8.0"
1504
+ }
1505
+ },
1506
+ "node_modules/linkify-it": {
1507
+ "version": "5.0.0",
1508
+ "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
1509
+ "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
1510
+ "dev": true,
1511
+ "license": "MIT",
1512
+ "dependencies": {
1513
+ "uc.micro": "^2.0.0"
1514
+ }
1515
+ },
1516
+ "node_modules/lodash": {
1517
+ "version": "4.17.21",
1518
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
1519
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
1520
+ "dev": true,
1521
+ "license": "MIT"
1522
+ },
1523
+ "node_modules/lodash.includes": {
1524
+ "version": "4.3.0",
1525
+ "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
1526
+ "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
1527
+ "license": "MIT"
1528
+ },
1529
+ "node_modules/lodash.isboolean": {
1530
+ "version": "3.0.3",
1531
+ "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
1532
+ "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
1533
+ "license": "MIT"
1534
+ },
1535
+ "node_modules/lodash.isinteger": {
1536
+ "version": "4.0.4",
1537
+ "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
1538
+ "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
1539
+ "license": "MIT"
1540
+ },
1541
+ "node_modules/lodash.isnumber": {
1542
+ "version": "3.0.3",
1543
+ "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
1544
+ "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
1545
+ "license": "MIT"
1546
+ },
1547
+ "node_modules/lodash.isplainobject": {
1548
+ "version": "4.0.6",
1549
+ "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
1550
+ "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
1551
+ "license": "MIT"
1552
+ },
1553
+ "node_modules/lodash.isstring": {
1554
+ "version": "4.0.1",
1555
+ "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
1556
+ "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
1557
+ "license": "MIT"
1558
+ },
1559
+ "node_modules/lodash.once": {
1560
+ "version": "4.1.1",
1561
+ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
1562
+ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
1563
+ "license": "MIT"
1564
+ },
1565
+ "node_modules/logger": {
1566
+ "version": "0.0.1",
1567
+ "resolved": "https://registry.npmjs.org/logger/-/logger-0.0.1.tgz",
1568
+ "integrity": "sha512-UD45f4iZrsj6dQKt5QBN7K+R0hmFwGS8G+Pv8WtHjrnhrMQftIclma8b86mNtg1LKB6HDIOW/ZtjnXELBhr89w==",
1569
+ "engines": {
1570
+ "node": ">=0.1.90"
1571
+ }
1572
+ },
1573
+ "node_modules/long": {
1574
+ "version": "5.2.4",
1575
+ "resolved": "https://registry.npmjs.org/long/-/long-5.2.4.tgz",
1576
+ "integrity": "sha512-qtzLbJE8hq7VabR3mISmVGtoXP8KGc2Z/AT8OuqlYD7JTR3oqrgwdjnk07wpj1twXxYmgDXgoKVWUG/fReSzHg==",
1577
+ "license": "Apache-2.0"
1578
+ },
1579
+ "node_modules/markdown-it": {
1580
+ "version": "14.1.0",
1581
+ "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz",
1582
+ "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==",
1583
+ "dev": true,
1584
+ "license": "MIT",
1585
+ "dependencies": {
1586
+ "argparse": "^2.0.1",
1587
+ "entities": "^4.4.0",
1588
+ "linkify-it": "^5.0.0",
1589
+ "mdurl": "^2.0.0",
1590
+ "punycode.js": "^2.3.1",
1591
+ "uc.micro": "^2.1.0"
1592
+ },
1593
+ "bin": {
1594
+ "markdown-it": "bin/markdown-it.mjs"
1595
+ }
1596
+ },
1597
+ "node_modules/markdown-it-anchor": {
1598
+ "version": "8.6.7",
1599
+ "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz",
1600
+ "integrity": "sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==",
1601
+ "dev": true,
1602
+ "license": "Unlicense",
1603
+ "peerDependencies": {
1604
+ "@types/markdown-it": "*",
1605
+ "markdown-it": "*"
1606
+ }
1607
+ },
1608
+ "node_modules/marked": {
1609
+ "version": "4.3.0",
1610
+ "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz",
1611
+ "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==",
1612
+ "dev": true,
1613
+ "license": "MIT",
1614
+ "bin": {
1615
+ "marked": "bin/marked.js"
1616
+ },
1617
+ "engines": {
1618
+ "node": ">= 12"
1619
+ }
1620
+ },
1621
+ "node_modules/math-intrinsics": {
1622
+ "version": "1.1.0",
1623
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
1624
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
1625
+ "license": "MIT",
1626
+ "engines": {
1627
+ "node": ">= 0.4"
1628
+ }
1629
+ },
1630
+ "node_modules/mdurl": {
1631
+ "version": "2.0.0",
1632
+ "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
1633
+ "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
1634
+ "dev": true,
1635
+ "license": "MIT"
1636
+ },
1637
+ "node_modules/media-typer": {
1638
+ "version": "0.3.0",
1639
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
1640
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
1641
+ "license": "MIT",
1642
+ "engines": {
1643
+ "node": ">= 0.6"
1644
+ }
1645
+ },
1646
+ "node_modules/merge-descriptors": {
1647
+ "version": "1.0.3",
1648
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
1649
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
1650
+ "license": "MIT",
1651
+ "funding": {
1652
+ "url": "https://github.com/sponsors/sindresorhus"
1653
+ }
1654
+ },
1655
+ "node_modules/methods": {
1656
+ "version": "1.1.2",
1657
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
1658
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
1659
+ "license": "MIT",
1660
+ "engines": {
1661
+ "node": ">= 0.6"
1662
+ }
1663
+ },
1664
+ "node_modules/mime": {
1665
+ "version": "1.6.0",
1666
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
1667
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
1668
+ "license": "MIT",
1669
+ "bin": {
1670
+ "mime": "cli.js"
1671
+ },
1672
+ "engines": {
1673
+ "node": ">=4"
1674
+ }
1675
+ },
1676
+ "node_modules/mime-db": {
1677
+ "version": "1.52.0",
1678
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
1679
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
1680
+ "license": "MIT",
1681
+ "engines": {
1682
+ "node": ">= 0.6"
1683
+ }
1684
+ },
1685
+ "node_modules/mime-types": {
1686
+ "version": "2.1.35",
1687
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
1688
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
1689
+ "license": "MIT",
1690
+ "dependencies": {
1691
+ "mime-db": "1.52.0"
1692
+ },
1693
+ "engines": {
1694
+ "node": ">= 0.6"
1695
+ }
1696
+ },
1697
+ "node_modules/minimatch": {
1698
+ "version": "5.1.6",
1699
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
1700
+ "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
1701
+ "dev": true,
1702
+ "license": "ISC",
1703
+ "dependencies": {
1704
+ "brace-expansion": "^2.0.1"
1705
+ },
1706
+ "engines": {
1707
+ "node": ">=10"
1708
+ }
1709
+ },
1710
+ "node_modules/minimist": {
1711
+ "version": "1.2.8",
1712
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
1713
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
1714
+ "dev": true,
1715
+ "license": "MIT",
1716
+ "funding": {
1717
+ "url": "https://github.com/sponsors/ljharb"
1718
+ }
1719
+ },
1720
+ "node_modules/mkdirp": {
1721
+ "version": "1.0.4",
1722
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
1723
+ "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
1724
+ "dev": true,
1725
+ "license": "MIT",
1726
+ "bin": {
1727
+ "mkdirp": "bin/cmd.js"
1728
+ },
1729
+ "engines": {
1730
+ "node": ">=10"
1731
+ }
1732
+ },
1733
+ "node_modules/morgan": {
1734
+ "version": "1.10.0",
1735
+ "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz",
1736
+ "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==",
1737
+ "license": "MIT",
1738
+ "dependencies": {
1739
+ "basic-auth": "~2.0.1",
1740
+ "debug": "2.6.9",
1741
+ "depd": "~2.0.0",
1742
+ "on-finished": "~2.3.0",
1743
+ "on-headers": "~1.0.2"
1744
+ },
1745
+ "engines": {
1746
+ "node": ">= 0.8.0"
1747
+ }
1748
+ },
1749
+ "node_modules/morgan/node_modules/on-finished": {
1750
+ "version": "2.3.0",
1751
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
1752
+ "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==",
1753
+ "license": "MIT",
1754
+ "dependencies": {
1755
+ "ee-first": "1.1.1"
1756
+ },
1757
+ "engines": {
1758
+ "node": ">= 0.8"
1759
+ }
1760
+ },
1761
+ "node_modules/ms": {
1762
+ "version": "2.0.0",
1763
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
1764
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
1765
+ "license": "MIT"
1766
+ },
1767
+ "node_modules/negotiator": {
1768
+ "version": "0.6.3",
1769
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
1770
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
1771
+ "license": "MIT",
1772
+ "engines": {
1773
+ "node": ">= 0.6"
1774
+ }
1775
+ },
1776
+ "node_modules/node-cron": {
1777
+ "version": "3.0.3",
1778
+ "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz",
1779
+ "integrity": "sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==",
1780
+ "license": "ISC",
1781
+ "dependencies": {
1782
+ "uuid": "8.3.2"
1783
+ },
1784
+ "engines": {
1785
+ "node": ">=6.0.0"
1786
+ }
1787
+ },
1788
+ "node_modules/node-cron/node_modules/uuid": {
1789
+ "version": "8.3.2",
1790
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
1791
+ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
1792
+ "license": "MIT",
1793
+ "bin": {
1794
+ "uuid": "dist/bin/uuid"
1795
+ }
1796
+ },
1797
+ "node_modules/node-fetch": {
1798
+ "version": "2.7.0",
1799
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
1800
+ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
1801
+ "license": "MIT",
1802
+ "dependencies": {
1803
+ "whatwg-url": "^5.0.0"
1804
+ },
1805
+ "engines": {
1806
+ "node": "4.x || >=6.0.0"
1807
+ },
1808
+ "peerDependencies": {
1809
+ "encoding": "^0.1.0"
1810
+ },
1811
+ "peerDependenciesMeta": {
1812
+ "encoding": {
1813
+ "optional": true
1814
+ }
1815
+ }
1816
+ },
1817
+ "node_modules/object-inspect": {
1818
+ "version": "1.13.3",
1819
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz",
1820
+ "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==",
1821
+ "license": "MIT",
1822
+ "engines": {
1823
+ "node": ">= 0.4"
1824
+ },
1825
+ "funding": {
1826
+ "url": "https://github.com/sponsors/ljharb"
1827
+ }
1828
+ },
1829
+ "node_modules/on-finished": {
1830
+ "version": "2.4.1",
1831
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
1832
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
1833
+ "license": "MIT",
1834
+ "dependencies": {
1835
+ "ee-first": "1.1.1"
1836
+ },
1837
+ "engines": {
1838
+ "node": ">= 0.8"
1839
+ }
1840
+ },
1841
+ "node_modules/on-headers": {
1842
+ "version": "1.0.2",
1843
+ "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
1844
+ "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
1845
+ "license": "MIT",
1846
+ "engines": {
1847
+ "node": ">= 0.8"
1848
+ }
1849
+ },
1850
+ "node_modules/once": {
1851
+ "version": "1.4.0",
1852
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
1853
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
1854
+ "license": "ISC",
1855
+ "dependencies": {
1856
+ "wrappy": "1"
1857
+ }
1858
+ },
1859
+ "node_modules/open": {
1860
+ "version": "10.1.0",
1861
+ "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz",
1862
+ "integrity": "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==",
1863
+ "license": "MIT",
1864
+ "dependencies": {
1865
+ "default-browser": "^5.2.1",
1866
+ "define-lazy-prop": "^3.0.0",
1867
+ "is-inside-container": "^1.0.0",
1868
+ "is-wsl": "^3.1.0"
1869
+ },
1870
+ "engines": {
1871
+ "node": ">=18"
1872
+ },
1873
+ "funding": {
1874
+ "url": "https://github.com/sponsors/sindresorhus"
1875
+ }
1876
+ },
1877
+ "node_modules/optionator": {
1878
+ "version": "0.8.3",
1879
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz",
1880
+ "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==",
1881
+ "dev": true,
1882
+ "license": "MIT",
1883
+ "dependencies": {
1884
+ "deep-is": "~0.1.3",
1885
+ "fast-levenshtein": "~2.0.6",
1886
+ "levn": "~0.3.0",
1887
+ "prelude-ls": "~1.1.2",
1888
+ "type-check": "~0.3.2",
1889
+ "word-wrap": "~1.2.3"
1890
+ },
1891
+ "engines": {
1892
+ "node": ">= 0.8.0"
1893
+ }
1894
+ },
1895
+ "node_modules/parseurl": {
1896
+ "version": "1.3.3",
1897
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
1898
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
1899
+ "license": "MIT",
1900
+ "engines": {
1901
+ "node": ">= 0.8"
1902
+ }
1903
+ },
1904
+ "node_modules/path-to-regexp": {
1905
+ "version": "0.1.12",
1906
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
1907
+ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
1908
+ "license": "MIT"
1909
+ },
1910
+ "node_modules/prelude-ls": {
1911
+ "version": "1.1.2",
1912
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
1913
+ "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==",
1914
+ "dev": true,
1915
+ "engines": {
1916
+ "node": ">= 0.8.0"
1917
+ }
1918
+ },
1919
+ "node_modules/protobufjs": {
1920
+ "version": "7.4.0",
1921
+ "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz",
1922
+ "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==",
1923
+ "hasInstallScript": true,
1924
+ "license": "BSD-3-Clause",
1925
+ "dependencies": {
1926
+ "@protobufjs/aspromise": "^1.1.2",
1927
+ "@protobufjs/base64": "^1.1.2",
1928
+ "@protobufjs/codegen": "^2.0.4",
1929
+ "@protobufjs/eventemitter": "^1.1.0",
1930
+ "@protobufjs/fetch": "^1.1.0",
1931
+ "@protobufjs/float": "^1.0.2",
1932
+ "@protobufjs/inquire": "^1.1.0",
1933
+ "@protobufjs/path": "^1.1.2",
1934
+ "@protobufjs/pool": "^1.1.0",
1935
+ "@protobufjs/utf8": "^1.1.0",
1936
+ "@types/node": ">=13.7.0",
1937
+ "long": "^5.0.0"
1938
+ },
1939
+ "engines": {
1940
+ "node": ">=12.0.0"
1941
+ }
1942
+ },
1943
+ "node_modules/protobufjs-cli": {
1944
+ "version": "1.1.3",
1945
+ "resolved": "https://registry.npmjs.org/protobufjs-cli/-/protobufjs-cli-1.1.3.tgz",
1946
+ "integrity": "sha512-MqD10lqF+FMsOayFiNOdOGNlXc4iKDCf0ZQPkPR+gizYh9gqUeGTWulABUCdI+N67w5RfJ6xhgX4J8pa8qmMXQ==",
1947
+ "dev": true,
1948
+ "license": "BSD-3-Clause",
1949
+ "dependencies": {
1950
+ "chalk": "^4.0.0",
1951
+ "escodegen": "^1.13.0",
1952
+ "espree": "^9.0.0",
1953
+ "estraverse": "^5.1.0",
1954
+ "glob": "^8.0.0",
1955
+ "jsdoc": "^4.0.0",
1956
+ "minimist": "^1.2.0",
1957
+ "semver": "^7.1.2",
1958
+ "tmp": "^0.2.1",
1959
+ "uglify-js": "^3.7.7"
1960
+ },
1961
+ "bin": {
1962
+ "pbjs": "bin/pbjs",
1963
+ "pbts": "bin/pbts"
1964
+ },
1965
+ "engines": {
1966
+ "node": ">=12.0.0"
1967
+ },
1968
+ "peerDependencies": {
1969
+ "protobufjs": "^7.0.0"
1970
+ }
1971
+ },
1972
+ "node_modules/proxy-addr": {
1973
+ "version": "2.0.7",
1974
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
1975
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
1976
+ "license": "MIT",
1977
+ "dependencies": {
1978
+ "forwarded": "0.2.0",
1979
+ "ipaddr.js": "1.9.1"
1980
+ },
1981
+ "engines": {
1982
+ "node": ">= 0.10"
1983
+ }
1984
+ },
1985
+ "node_modules/proxy-from-env": {
1986
+ "version": "1.1.0",
1987
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
1988
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
1989
+ "license": "MIT"
1990
+ },
1991
+ "node_modules/punycode.js": {
1992
+ "version": "2.3.1",
1993
+ "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
1994
+ "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
1995
+ "dev": true,
1996
+ "license": "MIT",
1997
+ "engines": {
1998
+ "node": ">=6"
1999
+ }
2000
+ },
2001
+ "node_modules/qs": {
2002
+ "version": "6.13.0",
2003
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
2004
+ "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
2005
+ "license": "BSD-3-Clause",
2006
+ "dependencies": {
2007
+ "side-channel": "^1.0.6"
2008
+ },
2009
+ "engines": {
2010
+ "node": ">=0.6"
2011
+ },
2012
+ "funding": {
2013
+ "url": "https://github.com/sponsors/ljharb"
2014
+ }
2015
+ },
2016
+ "node_modules/range-parser": {
2017
+ "version": "1.2.1",
2018
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
2019
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
2020
+ "license": "MIT",
2021
+ "engines": {
2022
+ "node": ">= 0.6"
2023
+ }
2024
+ },
2025
+ "node_modules/raw-body": {
2026
+ "version": "2.5.2",
2027
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
2028
+ "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
2029
+ "license": "MIT",
2030
+ "dependencies": {
2031
+ "bytes": "3.1.2",
2032
+ "http-errors": "2.0.0",
2033
+ "iconv-lite": "0.4.24",
2034
+ "unpipe": "1.0.0"
2035
+ },
2036
+ "engines": {
2037
+ "node": ">= 0.8"
2038
+ }
2039
+ },
2040
+ "node_modules/requizzle": {
2041
+ "version": "0.2.4",
2042
+ "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.4.tgz",
2043
+ "integrity": "sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw==",
2044
+ "dev": true,
2045
+ "license": "MIT",
2046
+ "dependencies": {
2047
+ "lodash": "^4.17.21"
2048
+ }
2049
+ },
2050
+ "node_modules/run-applescript": {
2051
+ "version": "7.0.0",
2052
+ "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz",
2053
+ "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==",
2054
+ "license": "MIT",
2055
+ "engines": {
2056
+ "node": ">=18"
2057
+ },
2058
+ "funding": {
2059
+ "url": "https://github.com/sponsors/sindresorhus"
2060
+ }
2061
+ },
2062
+ "node_modules/safe-buffer": {
2063
+ "version": "5.2.1",
2064
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
2065
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
2066
+ "funding": [
2067
+ {
2068
+ "type": "github",
2069
+ "url": "https://github.com/sponsors/feross"
2070
+ },
2071
+ {
2072
+ "type": "patreon",
2073
+ "url": "https://www.patreon.com/feross"
2074
+ },
2075
+ {
2076
+ "type": "consulting",
2077
+ "url": "https://feross.org/support"
2078
+ }
2079
+ ],
2080
+ "license": "MIT"
2081
+ },
2082
+ "node_modules/safer-buffer": {
2083
+ "version": "2.1.2",
2084
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
2085
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
2086
+ "license": "MIT"
2087
+ },
2088
+ "node_modules/semver": {
2089
+ "version": "7.7.0",
2090
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.0.tgz",
2091
+ "integrity": "sha512-DrfFnPzblFmNrIZzg5RzHegbiRWg7KMR7btwi2yjHwx06zsUbO5g613sVwEV7FTwmzJu+Io0lJe2GJ3LxqpvBQ==",
2092
+ "license": "ISC",
2093
+ "bin": {
2094
+ "semver": "bin/semver.js"
2095
+ },
2096
+ "engines": {
2097
+ "node": ">=10"
2098
+ }
2099
+ },
2100
+ "node_modules/send": {
2101
+ "version": "0.19.0",
2102
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
2103
+ "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
2104
+ "license": "MIT",
2105
+ "dependencies": {
2106
+ "debug": "2.6.9",
2107
+ "depd": "2.0.0",
2108
+ "destroy": "1.2.0",
2109
+ "encodeurl": "~1.0.2",
2110
+ "escape-html": "~1.0.3",
2111
+ "etag": "~1.8.1",
2112
+ "fresh": "0.5.2",
2113
+ "http-errors": "2.0.0",
2114
+ "mime": "1.6.0",
2115
+ "ms": "2.1.3",
2116
+ "on-finished": "2.4.1",
2117
+ "range-parser": "~1.2.1",
2118
+ "statuses": "2.0.1"
2119
+ },
2120
+ "engines": {
2121
+ "node": ">= 0.8.0"
2122
+ }
2123
+ },
2124
+ "node_modules/send/node_modules/encodeurl": {
2125
+ "version": "1.0.2",
2126
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
2127
+ "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
2128
+ "license": "MIT",
2129
+ "engines": {
2130
+ "node": ">= 0.8"
2131
+ }
2132
+ },
2133
+ "node_modules/send/node_modules/ms": {
2134
+ "version": "2.1.3",
2135
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
2136
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
2137
+ "license": "MIT"
2138
+ },
2139
+ "node_modules/serve-static": {
2140
+ "version": "1.16.2",
2141
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
2142
+ "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
2143
+ "license": "MIT",
2144
+ "dependencies": {
2145
+ "encodeurl": "~2.0.0",
2146
+ "escape-html": "~1.0.3",
2147
+ "parseurl": "~1.3.3",
2148
+ "send": "0.19.0"
2149
+ },
2150
+ "engines": {
2151
+ "node": ">= 0.8.0"
2152
+ }
2153
+ },
2154
+ "node_modules/setprototypeof": {
2155
+ "version": "1.2.0",
2156
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
2157
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
2158
+ "license": "ISC"
2159
+ },
2160
+ "node_modules/side-channel": {
2161
+ "version": "1.1.0",
2162
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
2163
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
2164
+ "license": "MIT",
2165
+ "dependencies": {
2166
+ "es-errors": "^1.3.0",
2167
+ "object-inspect": "^1.13.3",
2168
+ "side-channel-list": "^1.0.0",
2169
+ "side-channel-map": "^1.0.1",
2170
+ "side-channel-weakmap": "^1.0.2"
2171
+ },
2172
+ "engines": {
2173
+ "node": ">= 0.4"
2174
+ },
2175
+ "funding": {
2176
+ "url": "https://github.com/sponsors/ljharb"
2177
+ }
2178
+ },
2179
+ "node_modules/side-channel-list": {
2180
+ "version": "1.0.0",
2181
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
2182
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
2183
+ "license": "MIT",
2184
+ "dependencies": {
2185
+ "es-errors": "^1.3.0",
2186
+ "object-inspect": "^1.13.3"
2187
+ },
2188
+ "engines": {
2189
+ "node": ">= 0.4"
2190
+ },
2191
+ "funding": {
2192
+ "url": "https://github.com/sponsors/ljharb"
2193
+ }
2194
+ },
2195
+ "node_modules/side-channel-map": {
2196
+ "version": "1.0.1",
2197
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
2198
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
2199
+ "license": "MIT",
2200
+ "dependencies": {
2201
+ "call-bound": "^1.0.2",
2202
+ "es-errors": "^1.3.0",
2203
+ "get-intrinsic": "^1.2.5",
2204
+ "object-inspect": "^1.13.3"
2205
+ },
2206
+ "engines": {
2207
+ "node": ">= 0.4"
2208
+ },
2209
+ "funding": {
2210
+ "url": "https://github.com/sponsors/ljharb"
2211
+ }
2212
+ },
2213
+ "node_modules/side-channel-weakmap": {
2214
+ "version": "1.0.2",
2215
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
2216
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
2217
+ "license": "MIT",
2218
+ "dependencies": {
2219
+ "call-bound": "^1.0.2",
2220
+ "es-errors": "^1.3.0",
2221
+ "get-intrinsic": "^1.2.5",
2222
+ "object-inspect": "^1.13.3",
2223
+ "side-channel-map": "^1.0.1"
2224
+ },
2225
+ "engines": {
2226
+ "node": ">= 0.4"
2227
+ },
2228
+ "funding": {
2229
+ "url": "https://github.com/sponsors/ljharb"
2230
+ }
2231
+ },
2232
+ "node_modules/source-map": {
2233
+ "version": "0.6.1",
2234
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
2235
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
2236
+ "dev": true,
2237
+ "license": "BSD-3-Clause",
2238
+ "optional": true,
2239
+ "engines": {
2240
+ "node": ">=0.10.0"
2241
+ }
2242
+ },
2243
+ "node_modules/statuses": {
2244
+ "version": "2.0.1",
2245
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
2246
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
2247
+ "license": "MIT",
2248
+ "engines": {
2249
+ "node": ">= 0.8"
2250
+ }
2251
+ },
2252
+ "node_modules/strip-json-comments": {
2253
+ "version": "3.1.1",
2254
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
2255
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
2256
+ "dev": true,
2257
+ "license": "MIT",
2258
+ "engines": {
2259
+ "node": ">=8"
2260
+ },
2261
+ "funding": {
2262
+ "url": "https://github.com/sponsors/sindresorhus"
2263
+ }
2264
+ },
2265
+ "node_modules/supports-color": {
2266
+ "version": "7.2.0",
2267
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
2268
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
2269
+ "dev": true,
2270
+ "license": "MIT",
2271
+ "dependencies": {
2272
+ "has-flag": "^4.0.0"
2273
+ },
2274
+ "engines": {
2275
+ "node": ">=8"
2276
+ }
2277
+ },
2278
+ "node_modules/tmp": {
2279
+ "version": "0.2.3",
2280
+ "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz",
2281
+ "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==",
2282
+ "dev": true,
2283
+ "license": "MIT",
2284
+ "engines": {
2285
+ "node": ">=14.14"
2286
+ }
2287
+ },
2288
+ "node_modules/toidentifier": {
2289
+ "version": "1.0.1",
2290
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
2291
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
2292
+ "license": "MIT",
2293
+ "engines": {
2294
+ "node": ">=0.6"
2295
+ }
2296
+ },
2297
+ "node_modules/tr46": {
2298
+ "version": "0.0.3",
2299
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
2300
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
2301
+ "license": "MIT"
2302
+ },
2303
+ "node_modules/type-check": {
2304
+ "version": "0.3.2",
2305
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
2306
+ "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==",
2307
+ "dev": true,
2308
+ "license": "MIT",
2309
+ "dependencies": {
2310
+ "prelude-ls": "~1.1.2"
2311
+ },
2312
+ "engines": {
2313
+ "node": ">= 0.8.0"
2314
+ }
2315
+ },
2316
+ "node_modules/type-is": {
2317
+ "version": "1.6.18",
2318
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
2319
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
2320
+ "license": "MIT",
2321
+ "dependencies": {
2322
+ "media-typer": "0.3.0",
2323
+ "mime-types": "~2.1.24"
2324
+ },
2325
+ "engines": {
2326
+ "node": ">= 0.6"
2327
+ }
2328
+ },
2329
+ "node_modules/uc.micro": {
2330
+ "version": "2.1.0",
2331
+ "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
2332
+ "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
2333
+ "dev": true,
2334
+ "license": "MIT"
2335
+ },
2336
+ "node_modules/uglify-js": {
2337
+ "version": "3.19.3",
2338
+ "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz",
2339
+ "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==",
2340
+ "dev": true,
2341
+ "license": "BSD-2-Clause",
2342
+ "bin": {
2343
+ "uglifyjs": "bin/uglifyjs"
2344
+ },
2345
+ "engines": {
2346
+ "node": ">=0.8.0"
2347
+ }
2348
+ },
2349
+ "node_modules/underscore": {
2350
+ "version": "1.13.7",
2351
+ "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz",
2352
+ "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==",
2353
+ "dev": true,
2354
+ "license": "MIT"
2355
+ },
2356
+ "node_modules/undici": {
2357
+ "version": "6.21.2",
2358
+ "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.2.tgz",
2359
+ "integrity": "sha512-uROZWze0R0itiAKVPsYhFov9LxrPMHLMEQFszeI2gCN6bnIIZ8twzBCJcN2LJrBBLfrP0t1FW0g+JmKVl8Vk1g==",
2360
+ "license": "MIT",
2361
+ "engines": {
2362
+ "node": ">=18.17"
2363
+ }
2364
+ },
2365
+ "node_modules/undici-types": {
2366
+ "version": "6.20.0",
2367
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
2368
+ "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
2369
+ "license": "MIT"
2370
+ },
2371
+ "node_modules/universal-user-agent": {
2372
+ "version": "6.0.1",
2373
+ "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz",
2374
+ "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==",
2375
+ "license": "ISC"
2376
+ },
2377
+ "node_modules/unpipe": {
2378
+ "version": "1.0.0",
2379
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
2380
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
2381
+ "license": "MIT",
2382
+ "engines": {
2383
+ "node": ">= 0.8"
2384
+ }
2385
+ },
2386
+ "node_modules/utils-merge": {
2387
+ "version": "1.0.1",
2388
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
2389
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
2390
+ "license": "MIT",
2391
+ "engines": {
2392
+ "node": ">= 0.4.0"
2393
+ }
2394
+ },
2395
+ "node_modules/uuid": {
2396
+ "version": "11.0.5",
2397
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.5.tgz",
2398
+ "integrity": "sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA==",
2399
+ "funding": [
2400
+ "https://github.com/sponsors/broofa",
2401
+ "https://github.com/sponsors/ctavan"
2402
+ ],
2403
+ "license": "MIT",
2404
+ "bin": {
2405
+ "uuid": "dist/esm/bin/uuid"
2406
+ }
2407
+ },
2408
+ "node_modules/vary": {
2409
+ "version": "1.1.2",
2410
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
2411
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
2412
+ "license": "MIT",
2413
+ "engines": {
2414
+ "node": ">= 0.8"
2415
+ }
2416
+ },
2417
+ "node_modules/webidl-conversions": {
2418
+ "version": "3.0.1",
2419
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
2420
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
2421
+ "license": "BSD-2-Clause"
2422
+ },
2423
+ "node_modules/whatwg-url": {
2424
+ "version": "5.0.0",
2425
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
2426
+ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
2427
+ "license": "MIT",
2428
+ "dependencies": {
2429
+ "tr46": "~0.0.3",
2430
+ "webidl-conversions": "^3.0.0"
2431
+ }
2432
+ },
2433
+ "node_modules/word-wrap": {
2434
+ "version": "1.2.5",
2435
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
2436
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
2437
+ "dev": true,
2438
+ "license": "MIT",
2439
+ "engines": {
2440
+ "node": ">=0.10.0"
2441
+ }
2442
+ },
2443
+ "node_modules/wrappy": {
2444
+ "version": "1.0.2",
2445
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
2446
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
2447
+ "license": "ISC"
2448
+ },
2449
+ "node_modules/xmlcreate": {
2450
+ "version": "2.0.4",
2451
+ "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz",
2452
+ "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==",
2453
+ "dev": true,
2454
+ "license": "Apache-2.0"
2455
+ }
2456
+ }
2457
+ }
package.json ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "cursor-to-openai",
3
+ "version": "1.3.0",
4
+ "description": "Cursor to OpenAPI",
5
+ "author": "JiuZ-Chn",
6
+ "private": false,
7
+ "main": "index.js",
8
+ "url": "https://github.com/JiuZ-Chn/Cursor-To-OpenAI",
9
+ "license": "MIT",
10
+ "dependencies": {
11
+ "@octokit/rest": "^20.0.2",
12
+ "adm-zip": "^0.5.16",
13
+ "axios": "^1.6.7",
14
+ "cookie-parser": "^1.4.7",
15
+ "csv-parser": "^3.0.0",
16
+ "dotenv": "^16.4.7",
17
+ "express": "4.21.2",
18
+ "jsonwebtoken": "^9.0.2",
19
+ "logger": "^0.0.1",
20
+ "morgan": "^1.10.0",
21
+ "node-cron": "^3.0.3",
22
+ "node-fetch": "^2.7.0",
23
+ "open": "^10.1.0",
24
+ "protobufjs": "^7.4.0",
25
+ "undici": "^6.21.2",
26
+ "uuid": "11.0.5"
27
+ },
28
+ "scripts": {
29
+ "proto": "npx pbjs -t static-module -w commonjs -o src/proto/message.js src/proto/message.proto",
30
+ "start": "node src/app.js",
31
+ "setup": "node setup.js",
32
+ "manage-emails": "node manage-emails.js",
33
+ "refresh-cookies": "node auto-refresh-cookies.js",
34
+ "refresh-cookies:force": "node auto-refresh-cookies.js --force",
35
+ "refresh-cookies:api": "node auto-refresh-cookies.js",
36
+ "manage-cookies": "node manage-invalid-cookies.js"
37
+ },
38
+ "devDependencies": {
39
+ "protobufjs-cli": "^1.1.3"
40
+ }
41
+ }
project.sh ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/data/data/com.termux/files/usr/bin/bash
2
+
3
+ # 项目管理脚本(数字选择版)
4
+
5
+ echo "请选择操作:"
6
+ echo "1. 更新 cookie"
7
+ echo "2. 启动项目"
8
+ echo "3. 管理邮箱"
9
+ echo "4. 初始化配置"
10
+ echo "5. 更新项目代码"
11
+ echo "6. 备份项目"
12
+ echo "7. 退出"
13
+
14
+ read -p "输入数字 (1-7): " choice
15
+
16
+ case $choice in
17
+ 1)
18
+ echo "正在更新 cookie..."
19
+ npm run refresh-cookies
20
+ ;;
21
+ 2)
22
+ echo "正在启动项目..."
23
+ npm start
24
+ ;;
25
+ 3)
26
+ echo "正在管理邮箱..."
27
+ npm run manage-emails
28
+ ;;
29
+ 4)
30
+ echo "正在初始化配置文件..."
31
+ npm run setup
32
+ ;;
33
+ 5)
34
+ echo "正在更新项目代码..."
35
+ git pull
36
+ ;;
37
+ 6)
38
+ echo "正在备份项目..."
39
+ DATE=$(date +%Y%m%d_%H%M%S)
40
+ tar -czf "backup_$DATE.tar.gz" .
41
+ echo "备份完成: backup_$DATE.tar.gz"
42
+ ;;
43
+ 7)
44
+ echo "退出"
45
+ exit 0
46
+ ;;
47
+ *)
48
+ echo "错误:请输入 1-7 之间的数字"
49
+ exit 1
50
+ ;;
51
+ esac
scripts/create-admin.js ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const crypto = require('crypto');
4
+ const readline = require('readline');
5
+
6
+ const ADMIN_FILE = path.join(__dirname, '../data/admin.json');
7
+
8
+ // 创建readline接口
9
+ const rl = readline.createInterface({
10
+ input: process.stdin,
11
+ output: process.stdout
12
+ });
13
+
14
+ // 生成盐值
15
+ function generateSalt() {
16
+ return crypto.randomBytes(16).toString('hex');
17
+ }
18
+
19
+ // 使用盐值哈希密码
20
+ function hashPassword(password, salt) {
21
+ return crypto.pbkdf2Sync(password, salt, 1000, 64, 'sha512').toString('hex');
22
+ }
23
+
24
+ // 提示用户输入
25
+ function question(query) {
26
+ return new Promise((resolve) => {
27
+ rl.question(query, resolve);
28
+ });
29
+ }
30
+
31
+ async function main() {
32
+ try {
33
+ console.log('创建管理员账户\n');
34
+
35
+ // 获取用户输入
36
+ const username = await question('请输入管理员用户名: ');
37
+ const password = await question('请输入管理员密码: ');
38
+
39
+ // 生成盐值和密码哈希
40
+ const salt = generateSalt();
41
+ const hash = hashPassword(password, salt);
42
+
43
+ // 创建管理员数据
44
+ const adminData = {
45
+ admin: {
46
+ username,
47
+ salt,
48
+ hash
49
+ }
50
+ };
51
+
52
+ // 确保data目录存在
53
+ const dataDir = path.dirname(ADMIN_FILE);
54
+ if (!fs.existsSync(dataDir)) {
55
+ fs.mkdirSync(dataDir, { recursive: true });
56
+ }
57
+
58
+ // 写入文件
59
+ fs.writeFileSync(ADMIN_FILE, JSON.stringify(adminData, null, 2));
60
+
61
+ console.log('\n管理员账户创建成功!');
62
+ console.log('请妥善保管账户信息,不要将admin.json文件提交到版本控制系统。');
63
+
64
+ } catch (error) {
65
+ console.error('创建管理员账户失败:', error);
66
+ } finally {
67
+ rl.close();
68
+ }
69
+ }
70
+
71
+ main();
setup.js ADDED
@@ -0,0 +1,296 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const readline = require('readline');
6
+ const dotenv = require('dotenv');
7
+
8
+ // 创建交互式命令行界面
9
+ const rl = readline.createInterface({
10
+ input: process.stdin,
11
+ output: process.stdout
12
+ });
13
+
14
+ // 配置模板
15
+ const ENV_TEMPLATE = `# 服务端口
16
+ PORT=3010
17
+
18
+ # 日志格式 (tiny, combined, common, dev, short)
19
+ MORGAN_FORMAT=tiny
20
+
21
+ # API Key与Cookie映射关系 (JSON格式)
22
+ # 格式: {"自定义API Key": "Cookie值"} 或 {"自定义API Key": ["Cookie值1", "Cookie值2"]}
23
+ API_KEYS={API_KEYS_PLACEHOLDER}
24
+
25
+ # 轮询策略 (random 或 round-robin 或 default)
26
+ ROTATION_STRATEGY=default
27
+
28
+ # 是否使用TLS代理 (true 或 false)
29
+ USE_TLS_PROXY={USE_TLS_PROXY_PLACEHOLDER}
30
+
31
+ # 是否使用辅助代理服务器 (true 或 false)
32
+ USE_OTHERS_PROXY={USE_OTHERS_PROXY_PLACEHOLDER}
33
+
34
+ # 代理服务器平台
35
+ # 可选值: auto, windows_x64, linux_x64, android_arm64
36
+ # auto: 自动检测平台
37
+ # windows_x64: Windows 64位
38
+ # linux_x64: Linux 64位
39
+ # android_arm64: 安卓ARM 64位
40
+ PROXY_PLATFORM={PROXY_PLATFORM_PLACEHOLDER}
41
+
42
+ # 是否使用其它接口 (true 或 false)
43
+ USE_OTHERS={USE_OTHERS_PLACEHOLDER}
44
+ `;
45
+
46
+ // 提示信息
47
+ console.log('===== Cursor-To-OpenAI 环境配置助手 =====');
48
+ console.log('这个脚本将帮助你配置必要的环境变量\n');
49
+
50
+ // 从现有.env文件加载配置
51
+ function loadExistingConfig() {
52
+ const envPath = path.join(process.cwd(), '.env');
53
+ let existingConfig = {
54
+ apiKeys: {},
55
+ useTlsProxy: true,
56
+ useOthersProxy: true,
57
+ proxyPlatform: 'auto',
58
+ useOthers: true,
59
+ rotationStrategy: 'default'
60
+ };
61
+
62
+ if (fs.existsSync(envPath)) {
63
+ console.log('发现现有的.env配置文件,将加载现有设置作为默认值');
64
+ console.log('提示: 直接按回车将保留现有设置不变\n');
65
+
66
+ try {
67
+ // 加载.env文件
68
+ const envConfig = dotenv.parse(fs.readFileSync(envPath));
69
+
70
+ // 提取API Keys
71
+ if (envConfig.API_KEYS) {
72
+ try {
73
+ existingConfig.apiKeys = JSON.parse(envConfig.API_KEYS);
74
+ } catch (e) {
75
+ console.log('无法解析现有的API Keys配置,将使用默认设置');
76
+ }
77
+ }
78
+
79
+ // 提取TLS代理配置
80
+ if (envConfig.USE_TLS_PROXY !== undefined) {
81
+ existingConfig.useTlsProxy = envConfig.USE_TLS_PROXY === 'true';
82
+ }
83
+
84
+ // 提取辅助代理服务器配置
85
+ if (envConfig.USE_OTHERS_PROXY !== undefined) {
86
+ existingConfig.useOthersProxy = envConfig.USE_OTHERS_PROXY === 'true';
87
+ }
88
+
89
+ // 提取代理服务器平台
90
+ if (envConfig.PROXY_PLATFORM) {
91
+ existingConfig.proxyPlatform = envConfig.PROXY_PLATFORM;
92
+ }
93
+
94
+ // 提取是否使用其它接口
95
+ if (envConfig.USE_OTHERS !== undefined) {
96
+ existingConfig.useOthers = envConfig.USE_OTHERS === 'true';
97
+ }
98
+
99
+ // 提取轮询策略
100
+ if (envConfig.ROTATION_STRATEGY) {
101
+ existingConfig.rotationStrategy = envConfig.ROTATION_STRATEGY;
102
+ }
103
+
104
+ console.log('成功加载现有配置');
105
+ } catch (error) {
106
+ console.error('加载现有配置时出错:', error.message);
107
+ console.log('将使用默认设置');
108
+ }
109
+ } else {
110
+ console.log('未找到现有的.env配置文件,将创建新的配置文件');
111
+ }
112
+
113
+ return existingConfig;
114
+ }
115
+
116
+ // 提示用户输入,带有默认值
117
+ function promptWithDefault(question, defaultValue) {
118
+ return new Promise((resolve) => {
119
+ const defaultText = defaultValue ? ` [${defaultValue}]` : '';
120
+ rl.question(`${question}${defaultText}: `, (answer) => {
121
+ // 如果用户只按了回车,使用默认值
122
+ resolve(answer.trim() || defaultValue || '');
123
+ });
124
+ });
125
+ }
126
+
127
+ // 收集配置信息
128
+ async function collectConfig() {
129
+ // 加载现有配置
130
+ const existingConfig = loadExistingConfig();
131
+
132
+ const config = {
133
+ apiKeys: {},
134
+ useTlsProxy: existingConfig.useTlsProxy,
135
+ useOthersProxy: existingConfig.useOthersProxy,
136
+ proxyPlatform: existingConfig.proxyPlatform,
137
+ useOthers: existingConfig.useOthers,
138
+ rotationStrategy: existingConfig.rotationStrategy
139
+ };
140
+
141
+ // 询问是否使用TLS代理
142
+ const useTlsProxyPrompt = `是否使用TLS代理服务器? (y/n)`;
143
+ const defaultUseTlsProxy = existingConfig.useTlsProxy ? 'y' : 'n';
144
+ const useTlsProxyAnswer = await promptWithDefault(useTlsProxyPrompt, defaultUseTlsProxy);
145
+ config.useTlsProxy = useTlsProxyAnswer.toLowerCase() === 'y';
146
+
147
+ if (config.useTlsProxy) {
148
+ // 询问是否使用辅助代理服务器
149
+ const useOthersProxyPrompt = `是否使用辅助代理服务器(port 10654)? (y/n)`;
150
+ const defaultUseOthersProxy = existingConfig.useOthersProxy ? 'y' : 'n';
151
+ const useOthersProxyAnswer = await promptWithDefault(useOthersProxyPrompt, defaultUseOthersProxy);
152
+ config.useOthersProxy = useOthersProxyAnswer.toLowerCase() === 'y';
153
+
154
+ // 询问代理服务器平台
155
+ console.log('\n代理服务器平台选项:');
156
+ console.log('- auto: 自动检测当前系统平台');
157
+ console.log('- windows_x64: Windows 64位');
158
+ console.log('- linux_x64: Linux 64位');
159
+ console.log('- android_arm64: 安卓ARM 64位');
160
+
161
+ const proxyPlatformPrompt = `选择代理服务器平台`;
162
+ const defaultProxyPlatform = existingConfig.proxyPlatform || 'auto';
163
+ config.proxyPlatform = await promptWithDefault(proxyPlatformPrompt, defaultProxyPlatform);
164
+ }
165
+
166
+ // 询问是否使用其它接口
167
+ const useOthersPrompt = `是否使用其它接口? (y/n)`;
168
+ const defaultUseOthers = existingConfig.useOthers ? 'y' : 'n';
169
+ const useOthersAnswer = await promptWithDefault(useOthersPrompt, defaultUseOthers);
170
+ config.useOthers = useOthersAnswer.toLowerCase() === 'y';
171
+
172
+ // 询问轮询策略
173
+ console.log('\n轮询策略选项:');
174
+ console.log('- default: 默认策略');
175
+ console.log('- random: 随机策略');
176
+ console.log('- round-robin: 轮询策略');
177
+
178
+ const rotationStrategyPrompt = `选择轮询策略`;
179
+ const defaultRotationStrategy = existingConfig.rotationStrategy || 'default';
180
+ config.rotationStrategy = await promptWithDefault(rotationStrategyPrompt, defaultRotationStrategy);
181
+
182
+ // 处理API Keys
183
+ const existingApiKeys = Object.keys(existingConfig.apiKeys);
184
+ if (existingApiKeys.length > 0) {
185
+ console.log('\n现有的API Keys:');
186
+ existingApiKeys.forEach(key => console.log(`- ${key}`));
187
+
188
+ const keepExistingApiKeys = await promptWithDefault('是否保留现有的API Keys? (y/n)', 'y');
189
+ if (keepExistingApiKeys.toLowerCase() === 'y') {
190
+ config.apiKeys = { ...existingConfig.apiKeys };
191
+ }
192
+ }
193
+
194
+ // 询问是否添加新的API Key
195
+ const addNewApiKey = await promptWithDefault('是否添加新的API Key? (y/n)', existingApiKeys.length === 0 ? 'y' : 'n');
196
+ if (addNewApiKey.toLowerCase() === 'y') {
197
+ const apiKey = await promptWithDefault('请输入自定义的API Key (不含sk-前缀,将自动添加)', '');
198
+ if (apiKey) {
199
+ const fullApiKey = apiKey.startsWith('sk-') ? apiKey : `sk-${apiKey}`;
200
+ config.apiKeys[fullApiKey] = [];
201
+ } else {
202
+ // 如果用户直接回车跳过,默认添加 sk-text
203
+ config.apiKeys['sk-text'] = [];
204
+ console.log('已默认添加API Key: sk-text');
205
+ }
206
+ } else if (Object.keys(config.apiKeys).length === 0) {
207
+ // 如果没有任何API Key,默认添加 sk-text
208
+ config.apiKeys['sk-text'] = [];
209
+ console.log('已默认添加API Key: sk-text');
210
+ }
211
+
212
+ return config;
213
+ }
214
+
215
+ // 生成配置文件
216
+ function generateEnvFile(config) {
217
+ try {
218
+ // 准备API Keys
219
+ const apiKeysJson = JSON.stringify(config.apiKeys);
220
+
221
+ // 替换模板中的占位符
222
+ let envContent = ENV_TEMPLATE
223
+ .replace('{API_KEYS_PLACEHOLDER}', apiKeysJson)
224
+ .replace('{USE_TLS_PROXY_PLACEHOLDER}', config.useTlsProxy)
225
+ .replace('{USE_OTHERS_PROXY_PLACEHOLDER}', config.useOthersProxy)
226
+ .replace('{PROXY_PLATFORM_PLACEHOLDER}', config.proxyPlatform)
227
+ .replace('{USE_OTHERS_PLACEHOLDER}', config.useOthers);
228
+
229
+ // 更新轮询策略
230
+ envContent = envContent.replace('ROTATION_STRATEGY=default', `ROTATION_STRATEGY=${config.rotationStrategy}`);
231
+
232
+ // 写入.env文件
233
+ const envPath = path.join(process.cwd(), '.env');
234
+
235
+ // 检查是否存在备份文件
236
+ const backupPath = path.join(process.cwd(), '.env.backup');
237
+ if (fs.existsSync(envPath)) {
238
+ // 创建备份
239
+ fs.copyFileSync(envPath, backupPath);
240
+ console.log(`\n✅ 已创建原配置文件备份: ${backupPath}`);
241
+ }
242
+
243
+ fs.writeFileSync(envPath, envContent, 'utf8');
244
+ console.log(`\n✅ 配置文件已生成: ${envPath}`);
245
+
246
+ // 检查data目录
247
+ const dataDir = path.join(process.cwd(), 'data');
248
+ if (!fs.existsSync(dataDir)) {
249
+ fs.mkdirSync(dataDir, { recursive: true });
250
+ console.log(`✅ 创建数据目录: ${dataDir}`);
251
+ }
252
+
253
+ return true;
254
+ } catch (error) {
255
+ console.error('\n❌ 生成配置文件时出错:', error.message);
256
+ return false;
257
+ }
258
+ }
259
+
260
+ // 主函数
261
+ async function main() {
262
+ try {
263
+ const config = await collectConfig();
264
+
265
+ if (generateEnvFile(config)) {
266
+ console.log('\n===== 配置完成 =====');
267
+ console.log('你可以使用以下命令启动服务:');
268
+ console.log(' npm start');
269
+
270
+ // 显示TLS代理配置信息
271
+ console.log(`\n当前TLS代理配置:`);
272
+ console.log(`- 是否启用TLS代理: ${config.useTlsProxy ? '是' : '否'}`);
273
+ if (config.useTlsProxy) {
274
+ console.log(`- 是否启用辅助代理服务器: ${config.useOthersProxy ? '是' : '否'}`);
275
+ console.log(`- 代理服务器平台: ${config.proxyPlatform}`);
276
+ }
277
+
278
+ // 显示是否使用其它接口配置信息
279
+ console.log(`\n当前是否使用其它接口: ${config.useOthers ? '是' : '否'}`);
280
+
281
+ // 显示轮询策略
282
+ console.log(`\n当前轮询策略: ${config.rotationStrategy}`);
283
+
284
+ // 显示API Keys
285
+ console.log('\n当前配置的API Keys:');
286
+ Object.keys(config.apiKeys).forEach(key => console.log(`- ${key}`));
287
+ }
288
+ } catch (error) {
289
+ console.error('\n❌ 配置过程中出错:', error.message);
290
+ } finally {
291
+ rl.close();
292
+ }
293
+ }
294
+
295
+ // 运行主函数
296
+ main();
src/app.js ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // 加载环境变量
2
+ require('dotenv').config();
3
+
4
+ // 在logger加载前添加临时日志函数
5
+ function tempLog(level, message) {
6
+ const timestamp = new Date().toISOString();
7
+ if (level === 'ERROR') {
8
+ console.error(`[ERROR] ${timestamp} ${message}`);
9
+ } else if (level === 'WARN') {
10
+ console.warn(`[WARN] ${timestamp} ${message}`);
11
+ } else {
12
+ console.log(`[INFO] ${timestamp} ${message}`);
13
+ }
14
+ }
15
+
16
+ // 环境检查
17
+ tempLog('INFO', '启动前检查环境配置...');
18
+ const envChecker = require('./utils/envChecker');
19
+ // 先执行简单检查,避免循环依赖
20
+ envChecker.enforceEnvCheck();
21
+
22
+ const express = require('express');
23
+ const morgan = require('morgan');
24
+ const path = require('path');
25
+ const cron = require('node-cron');
26
+ const app = express();
27
+ const cookieParser = require('cookie-parser');
28
+ const { spawn } = require('child_process');
29
+
30
+ // 先加载配置,再加载logger
31
+ const config = require('./config/config');
32
+ const logger = require('./utils/logger');
33
+ const routes = require('./routes');
34
+ const keyManager = require('./utils/keyManager');
35
+ const cookieRefresher = require('./utils/cookieRefresher');
36
+ const authMiddleware = require('./middleware/auth');
37
+ const proxyLauncher = require('./utils/proxyLauncher');
38
+
39
+ // 初始化代理服务器
40
+ if (process.env.USE_TLS_PROXY === 'true') {
41
+ logger.info('正在启动TLS代理服务器...');
42
+ proxyLauncher.startProxyServer();
43
+ } else {
44
+ logger.info('TLS代理服务器未启用,跳过启动代理');
45
+ }
46
+
47
+ // 加载路由
48
+ const v1Router = require('./routes/v1');
49
+
50
+ // 初始化API Keys
51
+ logger.info('初始化API Keys...');
52
+ keyManager.initializeApiKeys();
53
+
54
+ // 输出最终的API Keys配置
55
+ logger.debug('最终API Keys配置:', JSON.stringify(keyManager.getAllApiKeys().reduce((obj, key) => {
56
+ obj[key] = keyManager.getAllCookiesForApiKey(key);
57
+ return obj;
58
+ }, {}), null, 2));
59
+
60
+ // 输出每个API key的Cookie数量信息
61
+ const apiKeys = keyManager.getAllApiKeys();
62
+ const keySummary = apiKeys.map(key => {
63
+ const cookies = keyManager.getAllCookiesForApiKey(key);
64
+ return `${key}: ${cookies.length}个Cookie`;
65
+ }).join(', ');
66
+
67
+ logger.info(`当前已加载 ${apiKeys.length} 个API Key,详情: ${keySummary}`);
68
+
69
+ // 添加CORS支持
70
+ app.use((req, res, next) => {
71
+ res.header('Access-Control-Allow-Origin', '*');
72
+ res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
73
+ res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
74
+
75
+ if (req.method === 'OPTIONS') {
76
+ return res.status(200).end();
77
+ }
78
+
79
+ next();
80
+ });
81
+
82
+ app.use(express.json({ limit: '50mb' }));
83
+ app.use(express.urlencoded({ extended: true, limit: '50mb' }));
84
+ app.use(cookieParser());
85
+
86
+ // 自定义Morgan格式,将日志输出到我们的日志系统
87
+ morgan.token('remote-addr', (req) => {
88
+ return req.headers['x-forwarded-for'] || req.socket.remoteAddress;
89
+ });
90
+
91
+ // 创建一个将 Morgan 日志写入我们的日志系统的流
92
+ const morganLoggerStream = {
93
+ write: (message) => {
94
+ // 移除行尾的换行符
95
+ const trimmedMessage = message.trim();
96
+ if (trimmedMessage) {
97
+ logger.http(trimmedMessage);
98
+ }
99
+ }
100
+ };
101
+
102
+ // 使用自定义格式的 Morgan 中间件
103
+ app.use(morgan(process.env.MORGAN_FORMAT || 'combined', {
104
+ stream: morganLoggerStream,
105
+ // 跳过健康检查等路由的日志
106
+ skip: (req, res) => {
107
+ return req.path === '/health' || req.path === '/favicon.ico';
108
+ }
109
+ }));
110
+
111
+ // 添加静态文件支持
112
+ app.use(express.static(path.join(__dirname, 'public')));
113
+
114
+ // 添加根路由,重定向到登录页面
115
+ app.get('/', (req, res) => {
116
+ res.redirect('/login.html');
117
+ });
118
+
119
+ // 添加认证中间件
120
+ app.use(authMiddleware);
121
+
122
+ // API路由
123
+ app.use('/v1', v1Router);
124
+
125
+ app.use("/", routes)
126
+
127
+ // 设置自动定时刷新Cookie任务
128
+ if (config.refresh.enabled) {
129
+ logger.info(`已启用自动刷新 Cookie,定时任务将在每 ${config.refresh.interval} 运行`);
130
+ cron.schedule(config.refresh.interval, () => {
131
+ logger.info('开始定时自动刷新 Cookie...');
132
+ const scriptPath = path.resolve(__dirname, '../auto-refresh-cookies.js');
133
+
134
+ const child = spawn('node', [scriptPath], {
135
+ stdio: ['ignore', 'pipe', 'pipe']
136
+ });
137
+
138
+ child.stdout.on('data', (data) => {
139
+ logger.info(`刷新进程输出: ${data.toString().trim()}`);
140
+ });
141
+
142
+ child.stderr.on('data', (data) => {
143
+ logger.error(`刷新进程错误: ${data.toString().trim()}`);
144
+ });
145
+
146
+ child.on('close', (code) => {
147
+ if (code === 0) {
148
+ logger.info('自动刷新 Cookie 定时任务完成');
149
+ } else {
150
+ logger.error(`自动刷新 Cookie 定时任务异常退出,代码: ${code}`);
151
+ }
152
+ });
153
+ });
154
+ } else {
155
+ logger.info('未启用自动刷新 Cookie,如需启用请设置环境变量 ENABLE_AUTO_REFRESH=true');
156
+ }
157
+
158
+ // 错误处理中间件
159
+ app.use((err, req, res, next) => {
160
+ logger.error('服务器错误:', err);
161
+ res.status(500).json({
162
+ error: 'Internal server error',
163
+ message: err.message
164
+ });
165
+ });
166
+
167
+ // 处理404请求
168
+ app.use((req, res) => {
169
+ logger.warn(`未找到路由: ${req.method} ${req.url}`);
170
+ res.status(404).json({
171
+ error: 'Not found',
172
+ message: '请求的资源不存在'
173
+ });
174
+ });
175
+
176
+ app.listen(config.port, () => {
177
+ logger.info(`服务器已启动,监听端口: ${config.port}`);
178
+ logger.info(`打开管理界面: http://localhost:${config.port}`);
179
+ });
180
+
181
+ // 处理进程退出事件,清理资源
182
+ process.on('SIGINT', () => {
183
+ logger.info('接收到SIGINT信号,正在优雅关闭服务...');
184
+ // 停止代理服务器
185
+ if (process.env.USE_TLS_PROXY === 'true') {
186
+ logger.info('正在停止TLS代理服务器...');
187
+ proxyLauncher.stopProxyServer();
188
+ }
189
+ process.exit(0);
190
+ });
191
+
192
+ process.on('SIGTERM', () => {
193
+ logger.info('接收到SIGTERM信号,正在优雅关闭服务...');
194
+ // 停止代理服务器
195
+ if (process.env.USE_TLS_PROXY === 'true') {
196
+ logger.info('正在停止TLS代理服务器...');
197
+ proxyLauncher.stopProxyServer();
198
+ }
199
+ process.exit(0);
200
+ });
201
+
202
+ module.exports = app;
src/config/CURSOR_CONFIG.md ADDED
@@ -0,0 +1,238 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Cursor AI 配置指南
2
+
3
+ 本文档详细说明了所有可用的Cursor配置选项,以及它们的用途和影响。
4
+
5
+ ## 🚀 快速开始
6
+
7
+ 1. 复制配置模板文件:
8
+ ```bash
9
+ cp cursor-config.env.example cursor-config.env
10
+ ```
11
+
12
+ 2. 根据需要修改配置文件
13
+ 3. 重启服务以应用配置:
14
+ ```bash
15
+ npm start
16
+ ```
17
+
18
+ ## 📋 配置选项详解
19
+
20
+ ### 🎯 核心模式设置
21
+
22
+ | 配置项 | 默认值 | 说明 |
23
+ |--------|--------|------|
24
+ | `CURSOR_MAX_MODE_ENABLED` | `true` | **MAX模式开关** - 启用增强AI能力,提供更深入的分析和建议 |
25
+ | `CURSOR_AGENT_MODE` | `true` | **Agent模式开关** - 启用智能代理功能,可以执行复杂的多步骤任务 |
26
+ | `CURSOR_UNIFIED_MODE` | `1` | **统一模式** - 整合多种功能模式,提供一致的用户体验 |
27
+ | `CURSOR_CHAT_MODE_ENUM` | `2` | **聊天模式枚举** - 控制对话交互的行为模式<br/>• `0`: 简单问答<br/>• `1`: 对话模式<br/>• `2`: 协作模式 |
28
+ | `CURSOR_CHAT_MODE` | `collaborative` | **聊天模式字符串** - 自定义聊天模式名称 |
29
+ | `CURSOR_PREPROCESSING_FLAG` | `false` | **预处理模式** - 是否在主要处理前进行环境初始化 |
30
+ | `CURSOR_STREAM_MODE` | `1` | **流式输出模式** - 控制响应是否实时流式输出<br/>• `0`: 关闭<br/>• `1`: 启用 |
31
+ | `CURSOR_THINKING_LEVEL` | `3` | **思考级别** - AI的思考深度等级 (0-5,数值越高思考越深入) |
32
+
33
+ ### 🤖 模型配置选项
34
+
35
+ | 配置项 | 默认值 | 说明 |
36
+ |--------|--------|------|
37
+ | `CURSOR_MODEL_NAME` | `claude-4-sonnet-thinking` | **默认模型名称** - 指定使用的AI模型 |
38
+ | `CURSOR_STREAMING_ENABLED` | `true` | **启用流式响应** - 是否支持实时响应输出 |
39
+ | `CURSOR_MAX_TOKENS` | `4096` | **最大Token数量** - 单次对话的最大token限制 (影响响应长度) |
40
+ | `CURSOR_TEMPERATURE` | `0.7` | **创造性温度参数** - 控制AI响应的创造性<br/>• `0.0`: 最保守,结果一致<br/>• `1.0`: 平衡<br/>• `2.0`: 最有创意 |
41
+ | `CURSOR_THINKING_MODE` | `true` | **思考模式开关** - 是否显示AI的思考过程 |
42
+ | `CURSOR_THINKING_DEPTH` | `5` | **思考深度级别** - 思考过程的详细程度 (1-10) |
43
+
44
+ ### 🛠️ Agent能力配置
45
+
46
+ #### 代码相关功能
47
+ | 配置项 | 默认值 | 说明 |
48
+ |--------|--------|------|
49
+ | `CURSOR_CODE_UNDERSTANDING` | `true` | **代码理解能力** - 分析和理解现有代码的能力 |
50
+ | `CURSOR_CODE_GENERATION` | `true` | **代码生成能力** - 自动生成新代码的能力 |
51
+ | `CURSOR_CODE_REFACTORING` | `true` | **代码重构能力** - 优化和重构现有代码的能力 |
52
+ | `CURSOR_DEBUGGING_ASSISTANCE` | `true` | **调试辅助功能** - 帮助查找和修复代码错误 |
53
+ | `CURSOR_TEST_GENERATION` | `true` | **测试代码生成** - 自动生成单元测试和集成测试 |
54
+ | `CURSOR_ERROR_ANALYSIS` | `true` | **错误分析能力** - 深度分析错误原因和解决方案 |
55
+
56
+ #### 文件操作能力
57
+ | 配置项 | 默认值 | 说明 | 安全级别 |
58
+ |--------|--------|------|----------|
59
+ | `CURSOR_FILE_READING` | `true` | **文件读取权限** - 允许AI读取项目文件内容 | 🟢 低风险 |
60
+ | `CURSOR_FILE_WRITING` | `false` | **文件写入权限** - 允许AI创建和修改文件 | 🟡 谨慎开启 |
61
+ | `CURSOR_FILE_SEARCH` | `true` | **文件搜索功能** - 在项目中搜索特定文件和内容 | 🟢 低风险 |
62
+ | `CURSOR_FILESYSTEM_ACCESS` | `false` | **文件系统访问** - 更广泛的文件系统操作权限 | 🔴 谨慎开启 |
63
+
64
+ #### 项目分析能力
65
+ | 配置项 | 默认值 | 说明 |
66
+ |--------|--------|------|
67
+ | `CURSOR_PROJECT_ANALYSIS` | `true` | **项目结构分析** - 分析项目的整体架构和组织结构 |
68
+ | `CURSOR_DEPENDENCY_ANALYSIS` | `true` | **依赖关系分析** - 分析项目的依赖关系和模块间的交互 |
69
+ | `CURSOR_ARCHITECTURE_ANALYSIS` | `true` | **架构分析** - 深度分析软件架构和设计模式 |
70
+ | `CURSOR_PERFORMANCE_OPTIMIZATION` | `true` | **性能优化建议** - 提供代码和架构的性能优化建议 |
71
+ | `CURSOR_SECURITY_ANALYSIS` | `true` | **安全分析功能** - 检查代码中的安全漏洞和风险 |
72
+
73
+ ### 📝 文档和注释功能
74
+
75
+ | 配置项 | 默认值 | 说明 |
76
+ |--------|--------|------|
77
+ | `CURSOR_DOCUMENTATION_GENERATION` | `true` | **文档自动生成** - 根据代码自动生成技术文档 |
78
+ | `CURSOR_COMMENT_GENERATION` | `true` | **代码注释生成** - 为代码添加详细的注释说明 |
79
+
80
+ ### 🧠 思考配置选项
81
+
82
+ | 配置项 | 默认值 | 说明 |
83
+ |--------|--------|------|
84
+ | `CURSOR_DEPTH_LEVEL` | `5` | **思考深度级别** - 问题分析的深度 (1-10) |
85
+ | `CURSOR_SHOW_THINKING` | `true` | **显示思考过程** - 是否在响应中显示AI的思考步骤 |
86
+ | `CURSOR_STEP_BY_STEP` | `true` | **逐步分析模式** - 将复杂问题分解为步骤处理 |
87
+ | `CURSOR_REASONING_CHAINS` | `true` | **推理链展示** - 显示逻���推理的完整链条 |
88
+ | `CURSOR_SELF_VERIFICATION` | `true` | **自我验证机制** - AI对自己的答案进行验证和检查 |
89
+ | `CURSOR_ALTERNATIVE_APPROACHES` | `true` | **提供替代方案** - 为问题提供多种解决方案 |
90
+
91
+ ### 📚 上下文配置选项
92
+
93
+ | 配置项 | 默认值 | 说明 |
94
+ |--------|--------|------|
95
+ | `CURSOR_MAX_CONTEXT_TOKENS` | `8192` | **最大上下文Token数** - 对话中保持的最大上下文长度 |
96
+ | `CURSOR_MAX_FILES` | `50` | **最大处理文件数** - 单次可以分析的最大文件数量 |
97
+ | `CURSOR_MAX_FILE_SIZE` | `1048576` | **单个文件大小限制** - 每个文件的最大大小(字节,默认1MB) |
98
+ | `CURSOR_CONVERSATION_HISTORY` | `10` | **对话历史长度** - 保持的对话轮数 |
99
+ | `CURSOR_INCLUDE_EDIT_HISTORY` | `true` | **包含编辑历史** - 是否在上下文中包含文件编辑历史 |
100
+ | `CURSOR_INCLUDE_PROJECT_STRUCTURE` | `true` | **包含项目结构信息** - 是否在上下文中包含完整项目结构 |
101
+ | `CURSOR_INCLUDE_DEPENDENCIES` | `true` | **包含依赖关系信息** - 是否在上下文中包含依赖关系图 |
102
+ | `CURSOR_LARGE_CONTEXT` | `1` | **启用大上下文处理** - 处理超长文本和复杂项目 |
103
+
104
+ ### 🔧 外部工具配置
105
+
106
+ | 配置项 | 默认值 | 说明 | 安全级别 |
107
+ |--------|--------|------|----------|
108
+ | `CURSOR_WEB_SEARCH_ENABLED` | `false` | **Web搜索功能** - 允许AI进行网络搜索获取最新信息 | 🟡 中等风险 |
109
+ | `CURSOR_CODE_EXECUTION_ENABLED` | `false` | **代码执行权限** - 允许AI执行代码进行测试 | 🔴 高风险功能 |
110
+ | `CURSOR_EXTERNAL_API_CALLS` | `false` | **外部API调用** - 允许调用第三方API服务 | 🟡 中等风险 |
111
+ | `CURSOR_DATABASE_ACCESS` | `false` | **数据库访问权限** - 允许访问数据库 | 🔴 高风险功能 |
112
+ | `CURSOR_WIKI_TOOL` | `[]` | **Wiki工具集成** - 集成Wiki知识库查询 | 🟢 低风险 |
113
+ | `CURSOR_WEB_TOOL` | `0` | **Web工具集成** - Web相关工具的启用状态 | 🟢 低风险 |
114
+
115
+ ### ⚙️ 高级控制选项
116
+
117
+ | 配置项 | 默认值 | 说明 |
118
+ |--------|--------|------|
119
+ | `CURSOR_ENABLE_MAX_FEATURES` | `1` | **启用所有最大功能** - 开启所有高级功能 (可能影响性能) |
120
+ | `CURSOR_STREAM_CONTROL_FLAG` | `1` | **流式输出控制** - 精细控制流式输出行为 |
121
+ | `CURSOR_TOKEN_START_FLAG` | `1` | **Token开始标志** - 控制Token处理的开始时机 |
122
+ | `CURSOR_TOKEN_CONTROL_FLAG` | `1` | **Token控制标志** - 高级Token管理选项 |
123
+ | `CURSOR_SESSION_TRACKING_FLAG` | `1` | **会话跟踪** - 跟踪用户会话状态和历史 |
124
+
125
+ ### 🎛️ 请求控制标志
126
+
127
+ | 配置项 | 默认值 | 说明 |
128
+ |--------|--------|------|
129
+ | `CURSOR_CONTROL_FLAG` | `true` | **主控制标志** - 全局功能开关 |
130
+ | `CURSOR_INSTRUCTION_FLAG` | `1` | **指令处理标志** - 如何处理用户指令 |
131
+ | `CURSOR_MODEL_FLAG` | `1` | **模型选择标志** - 动态模型选择控制 |
132
+ | `CURSOR_REQUEST_FLAG` | `1` | **请求类型标志** - 请求处理方式控制 |
133
+ | `CURSOR_FEEDBACK_FLAG` | `1` | **反馈收集标志** - 是否收集用户反馈数据 |
134
+ | `CURSOR_DESIRED_MAX_TOKENS` | `2048` | **期望最大Token数** - 响应的期望最大长度 |
135
+ | `CURSOR_CONTENT_FORMAT` | `markdown` | **内容格式** - 响应内容的格式类型 |
136
+
137
+ ### 🌐 系统环境配置
138
+
139
+ | 配置项 | 默认值 | 说明 |
140
+ |--------|--------|------|
141
+ | `CURSOR_TIMEZONE` | `Asia/Shanghai` | **客户端时区设置** |
142
+ | `CURSOR_CLIENT_VERSION` | `0.50.5` | **客户端版本号** |
143
+ | `CURSOR_CONFIG_VERSION` | `v1.0.0` | **配置版本号** - 配置文件的版本标识 |
144
+ | `CURSOR_PROJECT_TYPE` | `javascript` | **项目类型标识** - 帮助AI理解项目性质 |
145
+
146
+ ## 🎯 预设配置模式
147
+
148
+ ### ASK模式 (简单问答)
149
+ ```env
150
+ CURSOR_MAX_MODE_ENABLED=false
151
+ CURSOR_AGENT_MODE=false
152
+ CURSOR_THINKING_MODE=false
153
+ CURSOR_CODE_GENERATION=true
154
+ CURSOR_FILE_READING=true
155
+ CURSOR_FILE_WRITING=false
156
+ CURSOR_SHOW_THINKING=false
157
+ ```
158
+
159
+ ### AGENT模式 (智能代理)
160
+ ```env
161
+ CURSOR_MAX_MODE_ENABLED=true
162
+ CURSOR_AGENT_MODE=true
163
+ CURSOR_THINKING_MODE=true
164
+ CURSOR_THINKING_DEPTH=5
165
+ CURSOR_PROJECT_ANALYSIS=true
166
+ CURSOR_FILE_WRITING=true
167
+ CURSOR_CODE_REFACTORING=true
168
+ CURSOR_SHOW_THINKING=true
169
+ ```
170
+
171
+ ### MAX模式 (全功能)
172
+ ```env
173
+ CURSOR_MAX_MODE_ENABLED=true
174
+ CURSOR_AGENT_MODE=true
175
+ CURSOR_ENABLE_MAX_FEATURES=1
176
+ CURSOR_THINKING_DEPTH=10
177
+ CURSOR_LARGE_CONTEXT=1
178
+ CURSOR_SHOW_THINKING=true
179
+ # 启用所有Agent能力...
180
+ ```
181
+
182
+ ## ⚠️ 安全建议
183
+
184
+ ### 🔴 高风险功能 (谨慎开启)
185
+ - `CURSOR_CODE_EXECUTION_ENABLED`: 允许执行代码
186
+ - `CURSOR_DATABASE_ACCESS`: 数据库访问权限
187
+ - `CURSOR_FILESYSTEM_ACCESS`: 文件系统全面访问
188
+
189
+ ### 🟡 中等风险功能 (建议监控)
190
+ - `CURSOR_FILE_WRITING`: 文件写入权限
191
+ - `CURSOR_WEB_SEARCH_ENABLED`: 网络搜索功能
192
+ - `CURSOR_EXTERNAL_API_CALLS`: 外部API调用
193
+
194
+ ### 🟢 低风险功能 (安全开启)
195
+ - `CURSOR_FILE_READING`: 文件读取
196
+ - `CURSOR_CODE_UNDERSTANDING`: 代码理解
197
+ - `CURSOR_DOCUMENTATION_GENERATION`: 文档生成
198
+
199
+ ## 🔄 配置热重载
200
+
201
+ 修改配置文件后,可以通过以下方式重新加载:
202
+
203
+ 1. **重启服务** (推荐):
204
+ ```bash
205
+ npm start
206
+ ```
207
+
208
+ 2. **API重载** (如果支持):
209
+ ```bash
210
+ curl -X POST http://localhost:3010/v1/admin/reload-config
211
+ ```
212
+
213
+ ## 🐛 常见问题
214
+
215
+ ### Q: 配置修改后没有生效?
216
+ A: 确保:
217
+ 1. 配置文件名为 `cursor-config.env`
218
+ 2. 重启了服务
219
+ 3. 环境变量格式正确(boolean用true/false,数字不加引号)
220
+
221
+ ### Q: MAX模式无法启用?
222
+ A: 检查以下配置:
223
+ 1. `CURSOR_MAX_MODE_ENABLED=true`
224
+ 2. `CURSOR_AGENT_MODE=true`
225
+ 3. 模型名称包含"max"关键词
226
+
227
+ ### Q: 思考过程不显示?
228
+ A: 确保:
229
+ 1. `CURSOR_THINKING_MODE=true`
230
+ 2. `CURSOR_SHOW_THINKING=true`
231
+ 3. `CURSOR_THINKING_DEPTH > 0`
232
+
233
+ ## 📚 相关文档
234
+
235
+ - [项目README](README.md)
236
+ - [安装指南](SETUP.md)
237
+ - [API文档](API.md)
238
+ - [故障排除](TROUBLESHOOTING.md)
src/config/config.js ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // 读取并解析API_KEYS环境变量
2
+ // 避免循环依赖,不要在此处引用logger
3
+
4
+ // 添加自己的简单日志函数,防止循环依赖
5
+ function log(level, message) {
6
+ // 只在控制台输出,不写入文件
7
+ const timestamp = new Date().toISOString();
8
+ if (level === 'ERROR') {
9
+ console.error(`[ERROR] ${timestamp} ${message}`);
10
+ } else if (level === 'WARN') {
11
+ console.warn(`[WARN] ${timestamp} ${message}`);
12
+ } else {
13
+ console.log(`[INFO] ${timestamp} ${message}`);
14
+ }
15
+ }
16
+
17
+ // 解析API Keys配置
18
+ let apiKeysConfig = {};
19
+ try {
20
+ if (process.env.API_KEYS) {
21
+ // 解析API Keys字符串为对象
22
+ apiKeysConfig = JSON.parse(process.env.API_KEYS);
23
+ log('INFO', '正在从环境变量加载API Keys...');
24
+ log('INFO', `成功解析API Keys,包含 ${Object.keys(apiKeysConfig).length} 个键`);
25
+ }
26
+ } catch (error) {
27
+ log('ERROR', '解析API_KEYS环境变量失败:' + error.message);
28
+ log('ERROR', '请确保API_KEYS是有效的JSON格式');
29
+ }
30
+
31
+ // 导出配置
32
+ module.exports = {
33
+ port: process.env.PORT || 3000,
34
+
35
+ // 日志配置
36
+ log: {
37
+ level: process.env.LOG_LEVEL || 'INFO', // ERROR, WARN, INFO, DEBUG, TRACE
38
+ format: process.env.LOG_FORMAT || 'colored', // colored, json, text
39
+ toFile: process.env.LOG_TO_FILE === 'true' || false,
40
+ maxSize: parseInt(process.env.LOG_MAX_SIZE || '10', 10) * 1024 * 1024, // 默认10MB
41
+ maxFiles: parseInt(process.env.LOG_MAX_FILES || '10', 10) // 保留最近10个日志文件
42
+ },
43
+
44
+ // 合并API Keys设置
45
+ apiKeys: {
46
+ ...apiKeysConfig,
47
+ ...Object.fromEntries(
48
+ Object.entries(process.env)
49
+ .filter(([key]) => key.startsWith('API_KEY_'))
50
+ .map(([key, value]) => {
51
+ const apiKey = key.replace('API_KEY_', 'sk-');
52
+ try {
53
+ // 尝试解析JSON字符串,支持数组格式的cookie
54
+ const parsed = JSON.parse(value);
55
+ return [apiKey, parsed];
56
+ } catch (e) {
57
+ // 如果不是JSON,直接作为字符串处理
58
+ return [apiKey, value];
59
+ }
60
+ })
61
+ )
62
+ },
63
+
64
+ defaultRotationStrategy: process.env.ROTATION_STRATEGY || 'round-robin',
65
+
66
+ // 添加代理配置
67
+ proxy: {
68
+ enabled: process.env.PROXY_ENABLED === 'true' || false,
69
+ url: process.env.PROXY_URL || 'http://127.0.0.1:7890',
70
+ },
71
+
72
+ // GitHub相关配置
73
+ github: {
74
+ token: process.env.GITHUB_TOKEN,
75
+ owner: process.env.GITHUB_OWNER,
76
+ repo: process.env.GITHUB_REPO,
77
+ workflowId: process.env.GITHUB_WORKFLOW_ID,
78
+ triggerWorkflow: process.env.TRIGGER_WORKFLOW === 'true'
79
+ },
80
+
81
+ // 工作流参数
82
+ workflowParams: {
83
+ number: parseInt(process.env.REGISTER_NUMBER || '2', 10),
84
+ maxWorkers: parseInt(process.env.REGISTER_MAX_WORKERS || '1', 10),
85
+ emailServer: process.env.REGISTER_EMAIL_SERVER || 'TempEmail',
86
+ ingestToOneapi: process.env.REGISTER_INGEST_TO_ONEAPI === 'true',
87
+ uploadArtifact: process.env.REGISTER_UPLOAD_ARTIFACT === 'true',
88
+ useConfigFile: process.env.REGISTER_USE_CONFIG_FILE !== 'false',
89
+ emailConfigs: process.env.REGISTER_EMAIL_CONFIGS || '[]'
90
+ },
91
+
92
+ // 刷新配置
93
+ refresh: {
94
+ cron: process.env.REFRESH_CRON || '0 */6 * * *',
95
+ minCookieCount: parseInt(process.env.MIN_COOKIE_COUNT || '2', 10),
96
+ enabled: process.env.ENABLE_AUTO_REFRESH === 'true'
97
+ }
98
+ };
src/config/cursor-config.env.example ADDED
@@ -0,0 +1,253 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # =================================================================
2
+ # Cursor AI 配置环境变量
3
+ # 复制此文件为 cursor-config.env 并根据需要修改配置
4
+ # =================================================================
5
+
6
+ # =================================================================
7
+ # 🎯 核心模式设置
8
+ # =================================================================
9
+
10
+ # MAX模式开关 - 启用增强AI能力,提供更深入的分析和建议
11
+ CURSOR_MAX_MODE_ENABLED=true
12
+
13
+ # Agent模式开关 - 启用智能代理功能,可以执行复杂的多步骤任务
14
+ CURSOR_AGENT_MODE=true
15
+
16
+ # 统一模式 - 整合多种功能模式,提供一致的用户体验
17
+ CURSOR_UNIFIED_MODE=1
18
+
19
+ # 聊天模式枚举值 - 控制对话交互的行为模式 (0=简单问答, 1=对话模式, 2=协作模式)
20
+ CURSOR_CHAT_MODE_ENUM=2
21
+
22
+ # 聊天模式字符串 - 自定义聊天模式名称
23
+ CURSOR_CHAT_MODE=collaborative
24
+
25
+ # 预处理模式开关 - 是否在主要处理前进行环境初始化
26
+ CURSOR_PREPROCESSING_FLAG=false
27
+
28
+ # 流式输出模式 - 控制响应是否实时流式输出 (0=关闭, 1=启用)
29
+ CURSOR_STREAM_MODE=1
30
+
31
+ # 思考级别 - AI的思考深度等级 (0-5, 数值越高思考越深入)
32
+ CURSOR_THINKING_LEVEL=3
33
+
34
+ # =================================================================
35
+ # 🤖 模型配置选项
36
+ # =================================================================
37
+
38
+ # 默认模型名称 - 指定使用的AI模型
39
+ CURSOR_MODEL_NAME=claude-4-sonnet-thinking
40
+
41
+ # 启用流式响应 - 是否支持实时响应输出
42
+ CURSOR_STREAMING_ENABLED=true
43
+
44
+ # 最大Token数量 - 单次对话的最大token限制 (影响响应长度)
45
+ CURSOR_MAX_TOKENS=4096
46
+
47
+ # 创造性温度参数 - 控制AI响应的创造性 (0.0-2.0, 越高越有创意)
48
+ CURSOR_TEMPERATURE=0.7
49
+
50
+ # 思考模式开关 - 是否显示AI的思考过程
51
+ CURSOR_THINKING_MODE=true
52
+
53
+ # 思考深度级别 - 思考过程的详细程度 (1-10)
54
+ CURSOR_THINKING_DEPTH=5
55
+
56
+ # =================================================================
57
+ # 🛠️ Agent能力配置 - 代码相关功能
58
+ # =================================================================
59
+
60
+ # 代码理解能力 - 分析和理解现有代码的能力
61
+ CURSOR_CODE_UNDERSTANDING=true
62
+
63
+ # 代码生成能力 - 自动生成新代码的能力
64
+ CURSOR_CODE_GENERATION=true
65
+
66
+ # 代码重构能力 - 优化和重构现有代码的能力
67
+ CURSOR_CODE_REFACTORING=true
68
+
69
+ # 调试辅助功能 - 帮助查找和修复代码错误
70
+ CURSOR_DEBUGGING_ASSISTANCE=true
71
+
72
+ # 测试代码生成 - 自动生成单元测试和集成测试
73
+ CURSOR_TEST_GENERATION=true
74
+
75
+ # 错误分析能力 - 深度分析错误原因和解决方案
76
+ CURSOR_ERROR_ANALYSIS=true
77
+
78
+ # =================================================================
79
+ # 📁 文件操作能力配置
80
+ # =================================================================
81
+
82
+ # 文件读取权限 - 允许AI读取项目文件内容
83
+ CURSOR_FILE_READING=true
84
+
85
+ # 文件写入权限 - 允许AI创建和修改文件 (谨慎开启)
86
+ CURSOR_FILE_WRITING=false
87
+
88
+ # 文件搜索功能 - 在项目中搜索特定文件和内容
89
+ CURSOR_FILE_SEARCH=true
90
+
91
+ # 文件系统访问 - 更广泛的文件系统操作权限 (谨慎开启)
92
+ CURSOR_FILESYSTEM_ACCESS=false
93
+
94
+ # =================================================================
95
+ # 📊 项目分析能力配置
96
+ # =================================================================
97
+
98
+ # 项目结构分析 - 分析项目的整体架构和组织结构
99
+ CURSOR_PROJECT_ANALYSIS=true
100
+
101
+ # 依赖关系分析 - 分析项目的依赖关系和模块间的交互
102
+ CURSOR_DEPENDENCY_ANALYSIS=true
103
+
104
+ # 架构分析 - 深度分析软件架构和设计模式
105
+ CURSOR_ARCHITECTURE_ANALYSIS=true
106
+
107
+ # 性能优化建议 - 提供代码和架构的性能优化建议
108
+ CURSOR_PERFORMANCE_OPTIMIZATION=true
109
+
110
+ # 安全分析功能 - 检查代码中的安全漏洞和风险
111
+ CURSOR_SECURITY_ANALYSIS=true
112
+
113
+ # =================================================================
114
+ # 📝 文档和注释功能
115
+ # =================================================================
116
+
117
+ # 文档自动生成 - 根据代码自动生成技术文档
118
+ CURSOR_DOCUMENTATION_GENERATION=true
119
+
120
+ # 代码注释生成 - 为代码添加详细的注释说明
121
+ CURSOR_COMMENT_GENERATION=true
122
+
123
+ # =================================================================
124
+ # 🧠 思考配置选项
125
+ # =================================================================
126
+
127
+ # 思考深度级别 - 问题分析的深度 (1-10)
128
+ CURSOR_DEPTH_LEVEL=5
129
+
130
+ # 显示思考过程 - 是否在响应中显示AI的思考步骤
131
+ CURSOR_SHOW_THINKING=true
132
+
133
+ # 逐步分析模式 - 将复杂问题分解为步骤处理
134
+ CURSOR_STEP_BY_STEP=true
135
+
136
+ # 推理链展示 - 显示逻辑推理的完整链条
137
+ CURSOR_REASONING_CHAINS=true
138
+
139
+ # 自我验证机制 - AI对自己的答案进行验证和检查
140
+ CURSOR_SELF_VERIFICATION=true
141
+
142
+ # 提供替代方案 - 为问题提供多种解决方案
143
+ CURSOR_ALTERNATIVE_APPROACHES=true
144
+
145
+ # =================================================================
146
+ # 📚 上下文配置选项
147
+ # =================================================================
148
+
149
+ # 最大上下文Token数 - 对话中保持的最大上下文长度
150
+ CURSOR_MAX_CONTEXT_TOKENS=8192
151
+
152
+ # 最大处理文件数 - 单次可以分析的最大文件数量
153
+ CURSOR_MAX_FILES=50
154
+
155
+ # 单个文件大小限制 - 每个文件的最大大小(字节)
156
+ CURSOR_MAX_FILE_SIZE=1048576
157
+
158
+ # 对话历史长度 - 保持的对话轮数
159
+ CURSOR_CONVERSATION_HISTORY=10
160
+
161
+ # 包含编辑历史 - 是否在上下文中包含文件编辑历史
162
+ CURSOR_INCLUDE_EDIT_HISTORY=true
163
+
164
+ # 包含项目结构信息 - 是否在上下文中包含完整项目结构
165
+ CURSOR_INCLUDE_PROJECT_STRUCTURE=true
166
+
167
+ # 包含依赖关系信息 - 是否在上下文中包含依赖关系图
168
+ CURSOR_INCLUDE_DEPENDENCIES=true
169
+
170
+ # 启用大上下文处理 - 处理超长文本和复杂项目
171
+ CURSOR_LARGE_CONTEXT=1
172
+
173
+ # =================================================================
174
+ # 🔧 外部工具配置
175
+ # =================================================================
176
+
177
+ # Web搜索功能 - 允许AI进行网络搜索获取最新信息
178
+ CURSOR_WEB_SEARCH_ENABLED=false
179
+
180
+ # 代码执行权限 - 允许AI执行代码进行测试 (高风险功能)
181
+ CURSOR_CODE_EXECUTION_ENABLED=false
182
+
183
+ # 外部API调用 - 允许调用第三方API服务
184
+ CURSOR_EXTERNAL_API_CALLS=false
185
+
186
+ # 数据库访问权限 - 允许访问数据库 (高风险功能)
187
+ CURSOR_DATABASE_ACCESS=false
188
+
189
+ # Wiki工具集成 - 集成Wiki知识库查询
190
+ CURSOR_WIKI_TOOL=[]
191
+
192
+ # Web工具集成 - Web相关工具的启用状态
193
+ CURSOR_WEB_TOOL=0
194
+
195
+ # =================================================================
196
+ # ⚙️ 高级控制选项
197
+ # =================================================================
198
+
199
+ # 启用所有最大功能 - 开启所有高级功能 (可能影响性能)
200
+ CURSOR_ENABLE_MAX_FEATURES=1
201
+
202
+ # 流式输出控制 - 精细控制流式输出行为
203
+ CURSOR_STREAM_CONTROL_FLAG=1
204
+
205
+ # Token开始标志 - 控制Token处理的开始时机
206
+ CURSOR_TOKEN_START_FLAG=1
207
+
208
+ # Token控制标志 - 高级Token管理选项
209
+ CURSOR_TOKEN_CONTROL_FLAG=1
210
+
211
+ # 会话跟踪 - 跟踪用户会话状态和历史
212
+ CURSOR_SESSION_TRACKING_FLAG=1
213
+
214
+ # =================================================================
215
+ # 🎛️ 请求控制标志
216
+ # =================================================================
217
+
218
+ # 主控制标志 - 全局功能开关
219
+ CURSOR_CONTROL_FLAG=true
220
+
221
+ # 指令处理标志 - 如何处理用户指令
222
+ CURSOR_INSTRUCTION_FLAG=1
223
+
224
+ # 模型选择标志 - 动态模型选择控制
225
+ CURSOR_MODEL_FLAG=1
226
+
227
+ # 请求类型标志 - 请求处理方式控制
228
+ CURSOR_REQUEST_FLAG=1
229
+
230
+ # 反馈收集标志 - 是否收集用户反馈数据
231
+ CURSOR_FEEDBACK_FLAG=1
232
+
233
+ # 期望最大Token数 - 响应的期望最大长度
234
+ CURSOR_DESIRED_MAX_TOKENS=2048
235
+
236
+ # 内容格式 - 响应内容的格式类型
237
+ CURSOR_CONTENT_FORMAT=markdown
238
+
239
+ # =================================================================
240
+ # 🌐 系统环境配置
241
+ # =================================================================
242
+
243
+ # 客户端时区设置
244
+ CURSOR_TIMEZONE=Asia/Shanghai
245
+
246
+ # 客户端版本号
247
+ CURSOR_CLIENT_VERSION=0.50.5
248
+
249
+ # 配置版本号 - 配置文件的版本标识
250
+ CURSOR_CONFIG_VERSION=v1.0.0
251
+
252
+ # 项目类型标识 - 帮助AI理解项目性质
253
+ CURSOR_PROJECT_TYPE=javascript
src/config/cursorConfig.js ADDED
@@ -0,0 +1,257 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+
4
+ // 尝试加载自定义的cursor配置文件
5
+ const customConfigPath = path.join(process.cwd(), 'cursor-config.env');
6
+ if (fs.existsSync(customConfigPath)) {
7
+ require('dotenv').config({ path: customConfigPath });
8
+ }
9
+
10
+ // 辅助函数:解析布尔值
11
+ function parseBoolean(value, defaultValue = false) {
12
+ if (typeof value === 'boolean') return value;
13
+ if (typeof value === 'string') {
14
+ return value.toLowerCase() === 'true' || value === '1';
15
+ }
16
+ return defaultValue;
17
+ }
18
+
19
+ // 辅助函数:解析整数
20
+ function parseInteger(value, defaultValue = 0) {
21
+ const parsed = parseInt(value, 10);
22
+ return isNaN(parsed) ? defaultValue : parsed;
23
+ }
24
+
25
+ // 辅助函数:解析浮点数
26
+ function parseFloat(value, defaultValue = 0.0) {
27
+ const parsed = global.parseFloat(value);
28
+ return isNaN(parsed) ? defaultValue : parsed;
29
+ }
30
+
31
+ // 辅助函数:解析数组
32
+ function parseArray(value, defaultValue = []) {
33
+ if (Array.isArray(value)) return value;
34
+ if (typeof value === 'string') {
35
+ try {
36
+ return JSON.parse(value);
37
+ } catch (e) {
38
+ return value.split(',').map(item => item.trim()).filter(item => item);
39
+ }
40
+ }
41
+ return defaultValue;
42
+ }
43
+
44
+ // Cursor配置对象
45
+ const cursorConfig = {
46
+ // 🎯 核心模式设置
47
+ core: {
48
+ maxModeEnabled: parseBoolean(process.env.CURSOR_MAX_MODE_ENABLED, true),
49
+ agentMode: parseBoolean(process.env.CURSOR_AGENT_MODE, true),
50
+ unifiedMode: parseInteger(process.env.CURSOR_UNIFIED_MODE, 1),
51
+ chatModeEnum: parseInteger(process.env.CURSOR_CHAT_MODE_ENUM, 2),
52
+ chatMode: process.env.CURSOR_CHAT_MODE || 'collaborative',
53
+ preprocessingFlag: parseBoolean(process.env.CURSOR_PREPROCESSING_FLAG, false),
54
+ streamMode: parseInteger(process.env.CURSOR_STREAM_MODE, 1),
55
+ thinkingLevel: parseInteger(process.env.CURSOR_THINKING_LEVEL, 3)
56
+ },
57
+
58
+ // 🤖 模型配置选项
59
+ model: {
60
+ modelName: process.env.CURSOR_MODEL_NAME || 'claude-4-sonnet-thinking',
61
+ streamingEnabled: parseBoolean(process.env.CURSOR_STREAMING_ENABLED, true),
62
+ maxTokens: parseInteger(process.env.CURSOR_MAX_TOKENS, 4096),
63
+ temperature: parseFloat(process.env.CURSOR_TEMPERATURE, 0.7),
64
+ thinkingMode: parseBoolean(process.env.CURSOR_THINKING_MODE, true),
65
+ thinkingDepth: parseInteger(process.env.CURSOR_THINKING_DEPTH, 5)
66
+ },
67
+
68
+ // 🛠️ Agent能力配置
69
+ agentCapabilities: {
70
+ codeUnderstanding: parseBoolean(process.env.CURSOR_CODE_UNDERSTANDING, true),
71
+ codeGeneration: parseBoolean(process.env.CURSOR_CODE_GENERATION, true),
72
+ codeRefactoring: parseBoolean(process.env.CURSOR_CODE_REFACTORING, true),
73
+ debuggingAssistance: parseBoolean(process.env.CURSOR_DEBUGGING_ASSISTANCE, true),
74
+ testGeneration: parseBoolean(process.env.CURSOR_TEST_GENERATION, true),
75
+ errorAnalysis: parseBoolean(process.env.CURSOR_ERROR_ANALYSIS, true),
76
+ fileReading: parseBoolean(process.env.CURSOR_FILE_READING, true),
77
+ fileWriting: parseBoolean(process.env.CURSOR_FILE_WRITING, false),
78
+ fileSearch: parseBoolean(process.env.CURSOR_FILE_SEARCH, true),
79
+ filesystemAccess: parseBoolean(process.env.CURSOR_FILESYSTEM_ACCESS, false),
80
+ projectAnalysis: parseBoolean(process.env.CURSOR_PROJECT_ANALYSIS, true),
81
+ dependencyAnalysis: parseBoolean(process.env.CURSOR_DEPENDENCY_ANALYSIS, true),
82
+ architectureAnalysis: parseBoolean(process.env.CURSOR_ARCHITECTURE_ANALYSIS, true),
83
+ performanceOptimization: parseBoolean(process.env.CURSOR_PERFORMANCE_OPTIMIZATION, true),
84
+ securityAnalysis: parseBoolean(process.env.CURSOR_SECURITY_ANALYSIS, true),
85
+ documentationGeneration: parseBoolean(process.env.CURSOR_DOCUMENTATION_GENERATION, true),
86
+ commentGeneration: parseBoolean(process.env.CURSOR_COMMENT_GENERATION, true)
87
+ },
88
+
89
+ // 🧠 思考配置选项
90
+ thinking: {
91
+ depthLevel: parseInteger(process.env.CURSOR_DEPTH_LEVEL, 5),
92
+ showThinking: parseBoolean(process.env.CURSOR_SHOW_THINKING, true),
93
+ stepByStep: parseBoolean(process.env.CURSOR_STEP_BY_STEP, true),
94
+ reasoningChains: parseBoolean(process.env.CURSOR_REASONING_CHAINS, true),
95
+ selfVerification: parseBoolean(process.env.CURSOR_SELF_VERIFICATION, true),
96
+ alternativeApproaches: parseBoolean(process.env.CURSOR_ALTERNATIVE_APPROACHES, true)
97
+ },
98
+
99
+ // 📚 上下文配置选项
100
+ context: {
101
+ maxContextTokens: parseInteger(process.env.CURSOR_MAX_CONTEXT_TOKENS, 8192),
102
+ maxFiles: parseInteger(process.env.CURSOR_MAX_FILES, 50),
103
+ maxFileSize: parseInteger(process.env.CURSOR_MAX_FILE_SIZE, 1048576),
104
+ conversationHistory: parseInteger(process.env.CURSOR_CONVERSATION_HISTORY, 10),
105
+ includeEditHistory: parseBoolean(process.env.CURSOR_INCLUDE_EDIT_HISTORY, true),
106
+ includeProjectStructure: parseBoolean(process.env.CURSOR_INCLUDE_PROJECT_STRUCTURE, true),
107
+ includeDependencies: parseBoolean(process.env.CURSOR_INCLUDE_DEPENDENCIES, true),
108
+ largeContext: parseInteger(process.env.CURSOR_LARGE_CONTEXT, 1)
109
+ },
110
+
111
+ // 🔧 外部工具配置
112
+ tools: {
113
+ webSearchEnabled: parseBoolean(process.env.CURSOR_WEB_SEARCH_ENABLED, false),
114
+ codeExecutionEnabled: parseBoolean(process.env.CURSOR_CODE_EXECUTION_ENABLED, false),
115
+ externalApiCalls: parseBoolean(process.env.CURSOR_EXTERNAL_API_CALLS, false),
116
+ databaseAccess: parseBoolean(process.env.CURSOR_DATABASE_ACCESS, false),
117
+ wikiTool: parseArray(process.env.CURSOR_WIKI_TOOL, []),
118
+ webTool: parseInteger(process.env.CURSOR_WEB_TOOL, 0)
119
+ },
120
+
121
+ // ⚙️ 高级控制选项
122
+ advanced: {
123
+ enableMaxFeatures: parseInteger(process.env.CURSOR_ENABLE_MAX_FEATURES, 1),
124
+ streamControlFlag: parseInteger(process.env.CURSOR_STREAM_CONTROL_FLAG, 1),
125
+ tokenStartFlag: parseInteger(process.env.CURSOR_TOKEN_START_FLAG, 1),
126
+ tokenControlFlag: parseInteger(process.env.CURSOR_TOKEN_CONTROL_FLAG, 1),
127
+ sessionTrackingFlag: parseInteger(process.env.CURSOR_SESSION_TRACKING_FLAG, 1)
128
+ },
129
+
130
+ // 🎛️ 请求控制标志
131
+ control: {
132
+ controlFlag: parseBoolean(process.env.CURSOR_CONTROL_FLAG, true),
133
+ instructionFlag: parseInteger(process.env.CURSOR_INSTRUCTION_FLAG, 1),
134
+ modelFlag: parseInteger(process.env.CURSOR_MODEL_FLAG, 1),
135
+ requestFlag: parseInteger(process.env.CURSOR_REQUEST_FLAG, 1),
136
+ feedbackFlag: parseInteger(process.env.CURSOR_FEEDBACK_FLAG, 1),
137
+ desiredMaxTokens: parseInteger(process.env.CURSOR_DESIRED_MAX_TOKENS, 2048),
138
+ contentFormat: process.env.CURSOR_CONTENT_FORMAT || 'markdown'
139
+ },
140
+
141
+ // 🌐 系统环境配置
142
+ system: {
143
+ timezone: process.env.CURSOR_TIMEZONE || 'Asia/Shanghai',
144
+ clientVersion: process.env.CURSOR_CLIENT_VERSION || '0.50.5',
145
+ configVersion: process.env.CURSOR_CONFIG_VERSION || 'v1.0.0',
146
+ projectType: process.env.CURSOR_PROJECT_TYPE || 'javascript'
147
+ }
148
+ };
149
+
150
+ // 导出配置和辅助函数
151
+ module.exports = {
152
+ cursorConfig,
153
+
154
+ // 生成能力数组(用于protobuf)
155
+ getCapabilities() {
156
+ const caps = [];
157
+ const abilities = cursorConfig.agentCapabilities;
158
+
159
+ if (abilities.codeUnderstanding) caps.push(1);
160
+ if (abilities.codeGeneration) caps.push(3);
161
+ if (abilities.codeRefactoring) caps.push(5);
162
+ if (abilities.fileReading) caps.push(6);
163
+ if (abilities.fileWriting) caps.push(7);
164
+ if (abilities.fileSearch) caps.push(8);
165
+ if (abilities.projectAnalysis) caps.push(9);
166
+ if (abilities.debuggingAssistance) caps.push(11);
167
+ if (abilities.testGeneration) caps.push(12);
168
+ if (abilities.errorAnalysis) caps.push(14);
169
+ if (abilities.documentationGeneration) caps.push(15);
170
+ if (abilities.performanceOptimization) caps.push(17);
171
+ if (abilities.securityAnalysis) caps.push(18);
172
+ if (abilities.dependencyAnalysis) caps.push(20);
173
+ if (abilities.architectureAnalysis) caps.push(19);
174
+ if (abilities.commentGeneration) caps.push(21);
175
+
176
+ // 添加基础能力
177
+ caps.push(22, 23, 24);
178
+
179
+ return caps;
180
+ },
181
+
182
+ // 生成模型配置对象
183
+ getModelConfiguration(modelName, stream) {
184
+ return {
185
+ model_name: modelName || cursorConfig.model.modelName,
186
+ api_endpoint: 'https://api.openai.com/v1',
187
+ streaming_enabled: stream !== undefined ? stream : cursorConfig.model.streamingEnabled,
188
+ max_tokens: cursorConfig.model.maxTokens,
189
+ temperature: cursorConfig.model.temperature,
190
+ thinking_mode: cursorConfig.model.thinkingMode,
191
+ thinking_depth: cursorConfig.model.thinkingDepth,
192
+ agent_capabilities: cursorConfig.core.agentMode
193
+ };
194
+ },
195
+
196
+ // 生成系统环境信息
197
+ getSystemEnvironment() {
198
+ return {
199
+ platform: 'win32',
200
+ architecture: 'x64',
201
+ os_version: '10.0.26100',
202
+ executable_path: 'cursor.exe',
203
+ timestamp: new Date().toISOString(),
204
+ timezone: cursorConfig.system.timezone,
205
+ client_version: cursorConfig.system.clientVersion,
206
+ config_version: cursorConfig.system.configVersion
207
+ };
208
+ },
209
+
210
+ // 生成MAX模式设置
211
+ getMaxModeSettings() {
212
+ return {
213
+ enabled: cursorConfig.core.maxModeEnabled,
214
+ agent_caps: cursorConfig.agentCapabilities,
215
+ thinking_config: cursorConfig.thinking,
216
+ context_config: cursorConfig.context,
217
+ tool_config: cursorConfig.tools
218
+ };
219
+ },
220
+
221
+ // 检查是否启用了某个功能
222
+ isFeatureEnabled(feature) {
223
+ const parts = feature.split('.');
224
+ let current = cursorConfig;
225
+
226
+ for (const part of parts) {
227
+ if (current[part] === undefined) return false;
228
+ current = current[part];
229
+ }
230
+
231
+ return parseBoolean(current, false);
232
+ },
233
+
234
+ // 获取配置值
235
+ getValue(path, defaultValue) {
236
+ const parts = path.split('.');
237
+ let current = cursorConfig;
238
+
239
+ for (const part of parts) {
240
+ if (current[part] === undefined) return defaultValue;
241
+ current = current[part];
242
+ }
243
+
244
+ return current;
245
+ },
246
+
247
+ // 重新加载配置
248
+ reload() {
249
+ if (fs.existsSync(customConfigPath)) {
250
+ delete require.cache[require.resolve('dotenv')];
251
+ require('dotenv').config({ path: customConfigPath, override: true });
252
+ }
253
+
254
+ // 重新计算配置值...
255
+ // 这里可以重新执行配置初始化逻辑
256
+ }
257
+ };
src/middleware/auth.js ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const admin = require('../models/admin');
2
+
3
+ // 验证管理员权限的中间件
4
+ function authMiddleware(req, res, next) {
5
+ // 跳过登录相关的路由
6
+ if (req.path.startsWith('/v1/admin/')) {
7
+ return next();
8
+ }
9
+
10
+ // 对静态HTML页面的处理
11
+ if (req.path === '/logs.html') {
12
+ // 日志页面的访问不在中间件中做验证,而是在前端页面中进行验证
13
+ return next();
14
+ }
15
+
16
+ // 修改为:只对管理相关的API进行认证
17
+ if (req.path.startsWith('/v1/api-keys') ||
18
+ req.path.startsWith('/v1/invalid-cookies') ||
19
+ req.path.startsWith('/v1/refresh-cookies') ||
20
+ req.path.startsWith('/v1/logs')) {
21
+ // 获取Authorization头
22
+ const authHeader = req.headers.authorization;
23
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
24
+ return res.status(401).json({
25
+ success: false,
26
+ message: '未提供认证token'
27
+ });
28
+ }
29
+
30
+ // 提取token
31
+ const token = authHeader.split(' ')[1];
32
+
33
+ // 验证token
34
+ const result = admin.verifyToken(token);
35
+ if (!result.success) {
36
+ return res.status(401).json({
37
+ success: false,
38
+ message: '无效的token'
39
+ });
40
+ }
41
+
42
+ // 将用户信息添加到请求对象
43
+ req.admin = {
44
+ username: result.username
45
+ };
46
+ }
47
+
48
+ next();
49
+ }
50
+
51
+ module.exports = authMiddleware;
src/models/admin.js ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const crypto = require('crypto');
4
+ const jwt = require('jsonwebtoken');
5
+
6
+ // 管理员数据文件路径
7
+ const ADMIN_FILE = path.join(__dirname, '../../data/admin.json');
8
+ const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
9
+
10
+ // 确保data目录存在
11
+ const dataDir = path.dirname(ADMIN_FILE);
12
+ if (!fs.existsSync(dataDir)) {
13
+ fs.mkdirSync(dataDir, { recursive: true });
14
+ }
15
+
16
+ // 确保admin.json文件存在
17
+ if (!fs.existsSync(ADMIN_FILE)) {
18
+ fs.writeFileSync(ADMIN_FILE, JSON.stringify({ admin: null }), 'utf8');
19
+ }
20
+
21
+ class Admin {
22
+ constructor() {
23
+ this.loadAdmin();
24
+ }
25
+
26
+ // 加载管理员数据
27
+ loadAdmin() {
28
+ try {
29
+ const data = fs.readFileSync(ADMIN_FILE, 'utf8');
30
+ this.admin = JSON.parse(data).admin;
31
+ } catch (error) {
32
+ console.error('加载管理员数据失败:', error);
33
+ this.admin = null;
34
+ }
35
+ }
36
+
37
+ // 保存管理员数据
38
+ saveAdmin() {
39
+ try {
40
+ fs.writeFileSync(ADMIN_FILE, JSON.stringify({ admin: this.admin }), 'utf8');
41
+ } catch (error) {
42
+ console.error('保存管理员数据失败:', error);
43
+ throw error;
44
+ }
45
+ }
46
+
47
+ // 检查是否已有管理员
48
+ hasAdmin() {
49
+ return !!this.admin;
50
+ }
51
+
52
+ // 注册管理员
53
+ register(username, password) {
54
+ if (this.hasAdmin()) {
55
+ throw new Error('已存在管理员账号');
56
+ }
57
+
58
+ // 生成盐值
59
+ const salt = crypto.randomBytes(16).toString('hex');
60
+ // 使用盐值加密密码
61
+ const hash = crypto.pbkdf2Sync(password, salt, 1000, 64, 'sha512').toString('hex');
62
+
63
+ this.admin = {
64
+ username,
65
+ salt,
66
+ hash
67
+ };
68
+
69
+ this.saveAdmin();
70
+ return this.generateToken(username);
71
+ }
72
+
73
+ // 验证密码
74
+ verifyPassword(password, salt, hash) {
75
+ const testHash = crypto.pbkdf2Sync(password, salt, 1000, 64, 'sha512').toString('hex');
76
+ return testHash === hash;
77
+ }
78
+
79
+ // 登录验证
80
+ login(username, password) {
81
+ if (!this.admin || username !== this.admin.username) {
82
+ throw new Error('用户名或密码错误');
83
+ }
84
+
85
+ if (!this.verifyPassword(password, this.admin.salt, this.admin.hash)) {
86
+ throw new Error('用户名或密码错误');
87
+ }
88
+
89
+ return this.generateToken(username);
90
+ }
91
+
92
+ // 生成JWT token
93
+ generateToken(username) {
94
+ return jwt.sign({ username }, JWT_SECRET, { expiresIn: '24h' });
95
+ }
96
+
97
+ // 验证JWT token
98
+ verifyToken(token) {
99
+ try {
100
+ const decoded = jwt.verify(token, JWT_SECRET);
101
+ return {
102
+ success: true,
103
+ username: decoded.username
104
+ };
105
+ } catch (error) {
106
+ return {
107
+ success: false,
108
+ error: 'Invalid token'
109
+ };
110
+ }
111
+ }
112
+ }
113
+
114
+ module.exports = new Admin();
src/proto/message.js ADDED
The diff for this file is too large to render. See raw diff
 
src/proto/message.proto ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ syntax = "proto3";
2
+
3
+ message AvailableModelsResponse {
4
+ message AvailableModel {
5
+ string name = 1;
6
+ bool defaultOn = 2;
7
+ optional bool isLongContextOnly = 3;
8
+ optional bool isChatOnly = 4;
9
+ }
10
+ repeated AvailableModel models = 2;
11
+ repeated string modelNames = 1;
12
+ }
13
+
14
+ message MessageSummary {
15
+ string content = 1;
16
+ string summaryId1 = 2;
17
+ string summaryId2 = 3; // uuid, equal to summaryId1
18
+ string previousSummaryId = 4;
19
+ }
20
+
21
+ message MessageThinking {
22
+ string content = 1;
23
+ }
24
+ message StreamUnifiedChatWithToolsRequest {
25
+ message Request {
26
+ message Message {
27
+ message Image {
28
+ message Metadata {
29
+ int32 width = 1;
30
+ int32 height = 2;
31
+ }
32
+ bytes data = 1;
33
+ Metadata metadata = 2;
34
+ }
35
+ string content = 1;
36
+ int32 role = 2;
37
+ Image image = 10;
38
+ string messageId = 13;
39
+ string unknown29 = 29; // 1, only for user message
40
+ string summaryId = 32;
41
+ MessageSummary summary = 39;
42
+ MessageThinking thinking = 45;
43
+ int32 chatModeEnum = 47; // 1 for ask, 2 for agent, 3 for edit
44
+ }
45
+ message Instruction {
46
+ string instruction = 1;
47
+ }
48
+ message Model {
49
+ string name = 1;
50
+ bytes empty = 4;
51
+ }
52
+ message CursorSetting {
53
+ message Unknown6 {
54
+ bytes unknown1 = 1;
55
+ bytes unknown2 = 2;
56
+ }
57
+ string name = 1;
58
+ bytes unknown3 = 3;
59
+ Unknown6 unknown6 = 6;
60
+ int32 unknown8 = 8;
61
+ int32 unknown9 = 9;
62
+ }
63
+ message Metadata {
64
+ string os = 1; // win32
65
+ string arch = 2; // x64
66
+ string version = 3; // 10.0.22631
67
+ string path = 4; // C:\Program Files\PowerShell\7\pwsh.exe
68
+ string timestamp = 5; // 2025-03-03T13:10:08.590Z
69
+ }
70
+ message MessageId {
71
+ string messageId = 1;
72
+ string summaryId = 2;
73
+ int32 role = 3;
74
+ }
75
+
76
+ repeated Message messages = 1;
77
+ int32 unknown2 = 2; // 1
78
+ Instruction instruction = 3;
79
+ int32 unknown4 = 4; // 1
80
+ Model model = 5;
81
+ repeated string wikiTool = 7; // one url one item
82
+ string webTool = 8; // "full search"
83
+ int32 unknown13 = 13;
84
+ CursorSetting cursorSetting = 15;
85
+ int32 unknown19 = 19; // 1
86
+ int32 unknown22 = 22; // 1
87
+ string conversationId = 23; // uuid
88
+ Metadata metadata = 26;
89
+ int32 unknown27 = 27; // 1
90
+ string unknown29 = 29;
91
+ repeated MessageId messageIds = 30;
92
+ int32 largeContext = 35; // 1
93
+ int32 unknown38 = 38; // 0
94
+ int32 chatModeEnum = 46; // 1 for ask, 2 for agent, 3 for edit
95
+ string unknown47 = 47;
96
+ int32 unknown48 = 48; // 0
97
+ int32 unknown49 = 49; // 0
98
+ int32 unknown51 = 51; // 0
99
+ int32 unknown53 = 53; // 0
100
+ string chatMode = 54;
101
+ }
102
+
103
+ Request request = 1;
104
+ }
105
+
106
+ message StreamUnifiedChatWithToolsResponse {
107
+ message Message {
108
+ message WebTool {
109
+ message WebPage {
110
+ string url = 1;
111
+ string title = 2;
112
+ string content = 3;
113
+ }
114
+ repeated WebPage webPage = 1;
115
+ }
116
+ message Unknown12 {
117
+ message Content {
118
+ string content = 1;
119
+ }
120
+ Content content = 1;
121
+ }
122
+ string content = 1;
123
+ WebTool webtool = 11;
124
+ Unknown12 unknown12 = 12;
125
+ string unknown22 = 22; // uuid
126
+ string unknown23 = 23;
127
+ string unknown27 = 27; // uuid
128
+ MessageThinking thinking = 25;
129
+ }
130
+
131
+ Message message = 2;
132
+ MessageSummary summary = 3;
133
+ }
src/proxy/cursor_proxy_server_linux_amd64 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:985fa643f2a5b793102a4e97b058f14fc1e58cfd280c0bcc70e8a8455e932a4e
3
+ size 12790492
src/proxy/others/cursor_proxy_server_linux_amd64 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6fe0a95e158d753f3f179b88690d5e76cdb4a5b6408ca024bc544d2a84c6a919
3
+ size 12780345
src/public/index.html ADDED
@@ -0,0 +1,326 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
6
+ <title>Cursor To OpenAI - API Key 管理</title>
7
+ <link rel="stylesheet" href="styles.css">
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
+ <meta name="apple-mobile-web-app-capable" content="yes">
10
+ <meta name="apple-mobile-web-app-status-bar-style" content="default">
11
+ <meta name="theme-color" content="#007aff">
12
+ <script src="theme.js"></script>
13
+ </head>
14
+ <body>
15
+ <div class="container">
16
+ <div class="card header-card">
17
+ <div style="display: flex; align-items: center; margin-bottom: 8px;">
18
+ <i class="fas fa-link" style="font-size: 24px; margin-right: 12px;"></i>
19
+ <h1>Cursor To OpenAI</h1>
20
+ </div>
21
+ <p>管理自定义 API Key 与 Cursor Cookie 映射关系的高效工具</p>
22
+ <div style="margin-top: 20px; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap;">
23
+ <div style="display: flex; flex-wrap: wrap; gap: 10px;">
24
+ <button id="clearCacheBtn" style="background-color: rgba(255,255,255,0.15); backdrop-filter: blur(5px);">
25
+ <i class="fas fa-sync-alt" style="margin-right: 6px;"></i>清除缓存
26
+ </button>
27
+ <button id="logsBtn" style="background-color: rgba(255,255,255,0.15); backdrop-filter: blur(5px);">
28
+ <i class="fas fa-list-ul" style="margin-right: 6px;"></i>系统日志
29
+ </button>
30
+ </div>
31
+ <div>
32
+ <span id="adminUsername" style="margin-right: 10px; color: white; font-weight: 500; display: inline-flex; align-items: center;">
33
+ <i class="fas fa-user-circle" style="margin-right: 6px;"></i><span id="usernameText"></span>
34
+ </span>
35
+ <button id="logoutBtn" style="background: rgba(255, 59, 48, 0.8); backdrop-filter: blur(5px);">
36
+ <i class="fas fa-sign-out-alt" style="margin-right: 6px;"></i>退出
37
+ </button>
38
+ </div>
39
+ </div>
40
+ <div id="testApiResult" style="margin-top: 15px; display: none;"></div>
41
+ </div>
42
+
43
+ <div class="card">
44
+ <div style="display: flex; align-items: center; margin-bottom: 16px;">
45
+ <i class="fas fa-key" style="color: var(--ios-blue); font-size: 18px; margin-right: 10px;"></i>
46
+ <h2 style="margin: 0;">添加/更新 API Key</h2>
47
+ </div>
48
+ <div id="addKeyMessage"></div>
49
+ <form id="addKeyForm">
50
+ <div class="form-group">
51
+ <label for="apiKey">
52
+ <i class="fas fa-fingerprint" style="margin-right: 6px; color: var(--ios-gray);"></i>API Key(自定义)
53
+ </label>
54
+ <input type="text" id="apiKey" name="apiKey" placeholder="输入您想使用的自定义 API Key" required>
55
+ </div>
56
+ <div class="form-group">
57
+ <label for="cookieValues">
58
+ <i class="fas fa-cookie-bite" style="margin-right: 6px; color: var(--ios-gray);"></i>Cursor Cookie 值列表
59
+ </label>
60
+ <div class="cookies-container" id="addCookieTagsContainer">
61
+ <!-- Cookie标签将在这里动态显示 -->
62
+ </div>
63
+ <div class="form-group">
64
+ <label for="addNewCookie">
65
+ <i class="fas fa-plus-circle" style="margin-right: 6px; color: var(--ios-gray);"></i>添加新Cookie
66
+ </label>
67
+ <div style="display: flex; gap: 10px;">
68
+ <input type="text" id="addNewCookie" placeholder="输入新的WorkosCursorSessionToken值">
69
+ <button type="button" id="addNewCookieBtn" class="add-cookie-btn">
70
+ <i class="fas fa-plus"></i>
71
+ </button>
72
+ </div>
73
+ </div>
74
+ <textarea id="cookieValues" name="cookieValues" style="display: none;"></textarea>
75
+ </div>
76
+ <button type="submit">
77
+ <i class="fas fa-save" style="margin-right: 6px;"></i>保存
78
+ </button>
79
+ </form>
80
+ </div>
81
+
82
+ <div class="card">
83
+ <div style="display: flex; align-items: center; margin-bottom: 16px;">
84
+ <i class="fas fa-list" style="color: var(--ios-blue); font-size: 18px; margin-right: 10px;"></i>
85
+ <h2 style="margin: 0;">现有 API Key</h2>
86
+ </div>
87
+ <div id="keyListMessage"></div>
88
+ <div class="table-responsive">
89
+ <table id="keyTable">
90
+ <thead>
91
+ <tr>
92
+ <th>API KEY</th>
93
+ <th>COOKIE 数量</th>
94
+ <th>操作</th>
95
+ </tr>
96
+ </thead>
97
+ <tbody id="keyList">
98
+ <!-- 数据将通过 JavaScript 动态加载 -->
99
+ </tbody>
100
+ </table>
101
+ </div>
102
+ </div>
103
+
104
+ <div class="card">
105
+ <div style="display: flex; align-items: center; margin-bottom: 16px;">
106
+ <i class="fas fa-book" style="color: var(--ios-blue); font-size: 18px; margin-right: 10px;"></i>
107
+ <h2 style="margin: 0;">使用说明</h2>
108
+ </div>
109
+ <ol style="padding-left: 20px; margin-top: 0;">
110
+ <li>添加自定义 API Key 和对应的 Cursor Cookie 值。</li>
111
+ <li>API Key 可以不添加 Cookie,但不会有任何功能,可以后续再添加 Cookie。</li>
112
+ <li>使用自定义 API Key 作为 OpenAI API 的认证凭证。</li>
113
+ <li>系统将自动在多个 Cookie 之间进行轮询。
114
+ <ul style="margin-top: 8px;">
115
+ <li><code style="background: rgba(0, 122, 255, 0.1); padding: 2px 6px; border-radius: 4px; font-family: monospace;">/v1/models</code> - 模型列表</li>
116
+ <li><code style="background: rgba(0, 122, 255, 0.1); padding: 2px 6px; border-radius: 4px; font-family: monospace;">/v1/chat/completions</code> - 聊天补全</li>
117
+ </ul>
118
+ </li>
119
+ </ol>
120
+ </div>
121
+
122
+ <div class="card">
123
+ <div style="display: flex; align-items: center; margin-bottom: 16px;">
124
+ <i class="fas fa-exclamation-triangle" style="color: var(--ios-red); font-size: 18px; margin-right: 10px;"></i>
125
+ <h2 style="margin: 0;">无效Cookie管理</h2>
126
+ </div>
127
+ <div class="form-group">
128
+ <div class="info">
129
+ <i class="fas fa-info-circle" style="margin-right: 8px;"></i>
130
+ 以下是系统自动检测到的无效Cookie列表。这些Cookie在请求过程中被发现无效,已被自动从API Key中移除。
131
+ </div>
132
+ <div id="invalidCookiesContainer">
133
+ <div style="text-align: center; padding: 20px;">
134
+ <i class="fas fa-spinner fa-spin" style="margin-right: 8px;"></i>
135
+ <span>正在加载...</span>
136
+ </div>
137
+ </div>
138
+ </div>
139
+ </div>
140
+
141
+ <!-- 新增:Cookie刷新功能 -->
142
+ <div class="card" style="border: 1px solid rgba(255, 59, 48, 0.3); position: relative; background: linear-gradient(to right, rgba(255, 59, 48, 0.05), rgba(255, 255, 255, 0));">
143
+ <div style="position: absolute; top: -10px; right: -10px; background: var(--ios-red); color: white; padding: 5px 10px; border-radius: 8px; font-weight: 500; font-size: 13px; box-shadow: 0 2px 6px rgba(255, 59, 48, 0.3);">不建议使用</div>
144
+
145
+ <div style="display: flex; align-items: center; margin-bottom: 16px;">
146
+ <i class="fas fa-sync-alt" style="color: var(--ios-red); font-size: 18px; margin-right: 10px;"></i>
147
+ <h2 style="margin: 0;">Cookie自动刷新 <span style="color: var(--ios-red); font-size: 0.8em; font-weight: 400;">(不建议使用)</span></h2>
148
+ </div>
149
+
150
+ <div class="form-group">
151
+ <div class="error">
152
+ <i class="fas fa-exclamation-circle" style="margin-right: 8px;"></i>
153
+ <strong>警告:</strong>此功能已标记为不建议使用,可能会导致账号风险。除非必要,请避免使用此功能。
154
+ </div>
155
+ <div class="info" style="margin-top: 10px;">
156
+ <i class="fas fa-info-circle" style="margin-right: 8px;"></i>
157
+ 系统支持自动刷新Cookie,确保API Key始终有足够的可用Cookie。您可以在此手动触发刷新操作。
158
+ </div>
159
+ <div id="refreshCookieMessage"></div>
160
+ <div style="margin-top: 15px;">
161
+ <div class="form-group">
162
+ <label for="refreshApiKey">
163
+ <i class="fas fa-filter" style="margin-right: 6px; color: var(--ios-gray);"></i>选择要刷新的API Key
164
+ </label>
165
+ <select id="refreshApiKey">
166
+ <option value="">所有API Key</option>
167
+ <!-- 选项将通过JavaScript动态加载 -->
168
+ </select>
169
+ </div>
170
+ <button id="refreshCookieBtn" style="background: var(--ios-green);">
171
+ <i class="fas fa-sync-alt" style="margin-right: 6px;"></i>刷新Cookie
172
+ </button>
173
+ </div>
174
+ <div id="refreshStatusContainer" style="margin-top: 15px; display: none;">
175
+ <div class="info">
176
+ <div>
177
+ <i class="fas fa-spinner fa-spin" style="margin-right: 8px;"></i>
178
+ 刷新状态:<span id="refreshStatus">准备中...</span>
179
+ </div>
180
+ <div style="margin-top: 10px;">
181
+ <progress id="refreshProgress" value="0" max="100"></progress>
182
+ </div>
183
+ </div>
184
+ </div>
185
+ </div>
186
+ </div>
187
+
188
+ <!-- 新增:获取Cursor Cookie功能 -->
189
+ <div class="card">
190
+ <div style="display: flex; align-items: center; margin-bottom: 16px;">
191
+ <i class="fas fa-plus-circle" style="color: var(--ios-blue); font-size: 18px; margin-right: 10px;"></i>
192
+ <h2 style="margin: 0;">获取Cursor Cookie</h2>
193
+ </div>
194
+ <div class="form-group">
195
+ <div class="info">
196
+ <i class="fas fa-info-circle" style="margin-right: 8px;"></i>
197
+ 通过此功能可以快速获取新的Cursor Cookie并添加到系统中。点击按钮生成链接,完成登录后自动添加Cookie。系统将等待最多5分钟获取Cookie。
198
+ </div>
199
+ <div id="getCookieMessage"></div>
200
+ <div style="margin-top: 15px;">
201
+ <div class="form-group">
202
+ <label for="targetApiKey">
203
+ <i class="fas fa-tag" style="margin-right: 6px; color: var(--ios-gray);"></i>选择要添加Cookie的API Key
204
+ </label>
205
+ <select id="targetApiKey">
206
+ <option value="">所有API Key</option>
207
+ <!-- 选项将通过JavaScript动态加载 -->
208
+ </select>
209
+ </div>
210
+ <button id="generateLinkBtn" style="background: var(--ios-blue);">
211
+ <i class="fas fa-link" style="margin-right: 6px;"></i>生成登录链接
212
+ </button>
213
+ </div>
214
+ <div id="loginLinkContainer" style="margin-top: 15px; display: none;">
215
+ <div class="info">
216
+ <p><i class="fas fa-info-circle" style="margin-right: 8px;"></i>请点击下面的链接或复制到浏览器打开,然后登录Cursor账号并授权:</p>
217
+ <div style="margin: 10px 0; word-break: break-all; background: rgba(0, 122, 255, 0.08); padding: 12px; border-radius: 8px; border: 1px solid rgba(0, 122, 255, 0.15);">
218
+ <a id="loginLink" href="#" target="_blank" style="color: var(--ios-blue); text-decoration: none; display: flex; align-items: center;">
219
+ <i class="fas fa-external-link-alt" style="margin-right: 8px;"></i>
220
+ <span id="loginLinkText"></span>
221
+ </a>
222
+ </div>
223
+ <p><i class="fas fa-clock" style="margin-right: 8px;"></i>登录完成后系统将自动获取Cookie并添加到API Key中。请等待最多5分钟,期间请勿关闭此页面。</p>
224
+ </div>
225
+ <div id="pollStatus" style="margin-top: 10px;">
226
+ <div style="display: flex; align-items: center; margin-bottom: 8px;">
227
+ <i class="fas fa-spinner fa-spin" style="margin-right: 8px; color: var(--ios-blue);"></i>
228
+ 状态:<span id="pollStatusText" style="margin-left: 4px;">等待用户登录...</span>
229
+ </div>
230
+ <div>
231
+ <progress id="pollProgress" value="0" max="100"></progress>
232
+ </div>
233
+ </div>
234
+ </div>
235
+ </div>
236
+ </div>
237
+ </div>
238
+
239
+ <!-- 修改 Cookie 的模态框 -->
240
+ <div id="editModal" class="modal">
241
+ <div class="modal-content">
242
+ <span class="close">&times;</span>
243
+ <div style="display: flex; align-items: center; margin-bottom: 16px;">
244
+ <i class="fas fa-edit" style="color: var(--ios-blue); font-size: 18px; margin-right: 10px;"></i>
245
+ <h2 style="margin: 0;">修改 API Key 的 Cookie</h2>
246
+ </div>
247
+ <div id="editModalMessage"></div>
248
+ <form id="editCookieForm">
249
+ <input type="hidden" id="editApiKey" name="editApiKey">
250
+ <div class="form-group">
251
+ <label for="editCookieValues">
252
+ <i class="fas fa-cookie-bite" style="margin-right: 6px; color: var(--ios-gray);"></i>Cursor Cookie 值列表
253
+ </label>
254
+ <div class="cookies-container" id="cookieTagsContainer">
255
+ <!-- Cookie标签将在这里动态显示 -->
256
+ </div>
257
+ <div class="form-group">
258
+ <label for="newCookie">
259
+ <i class="fas fa-plus-circle" style="margin-right: 6px; color: var(--ios-gray);"></i>添加新Cookie
260
+ </label>
261
+ <div style="display: flex; gap: 10px;">
262
+ <input type="text" id="newCookie" placeholder="输入新的WorkosCursorSessionToken值">
263
+ <button type="button" id="addCookieBtn" class="add-cookie-btn">
264
+ <i class="fas fa-plus"></i>
265
+ </button>
266
+ </div>
267
+ </div>
268
+ <textarea id="editCookieValues" name="editCookieValues" style="display: none;"></textarea>
269
+ </div>
270
+ <button type="submit">
271
+ <i class="fas fa-save" style="margin-right: 6px;"></i>保存修改
272
+ </button>
273
+ </form>
274
+ </div>
275
+ </div>
276
+
277
+ <!-- 无效Cookie编辑模态框 -->
278
+ <div id="invalidCookieModal" class="modal">
279
+ <div class="modal-content">
280
+ <span class="close" id="closeInvalidCookieModal">&times;</span>
281
+ <div style="display: flex; align-items: center; margin-bottom: 16px;">
282
+ <i class="fas fa-ban" style="color: var(--ios-red); font-size: 18px; margin-right: 10px;"></i>
283
+ <h2 style="margin: 0;">管理无效Cookie</h2>
284
+ </div>
285
+ <div id="invalidCookieModalMessage"></div>
286
+ <form id="invalidCookieForm">
287
+ <div class="form-group">
288
+ <label for="invalidCookiesValues">
289
+ <i class="fas fa-cookie-bite" style="margin-right: 6px; color: var(--ios-gray);"></i>无效Cookie列表
290
+ </label>
291
+ <div class="cookies-container" id="invalidCookieTagsContainer">
292
+ <!-- Cookie标签将在这里动态显示 -->
293
+ </div>
294
+ <div class="form-group">
295
+ <label for="newInvalidCookie">
296
+ <i class="fas fa-plus-circle" style="margin-right: 6px; color: var(--ios-gray);"></i>添加Cookie
297
+ </label>
298
+ <div style="display: flex; gap: 10px;">
299
+ <input type="text" id="newInvalidCookie" placeholder="输入Cookie值">
300
+ <button type="button" id="addInvalidCookieBtn" class="add-cookie-btn">
301
+ <i class="fas fa-plus"></i>
302
+ </button>
303
+ </div>
304
+ </div>
305
+ <textarea id="invalidCookiesValues" name="invalidCookiesValues" style="display: none;"></textarea>
306
+ </div>
307
+ <button type="submit">
308
+ <i class="fas fa-save" style="margin-right: 6px;"></i>保存修改
309
+ </button>
310
+ </form>
311
+ </div>
312
+ </div>
313
+
314
+ <!-- 侧边目录导航 -->
315
+ <div class="side-nav-trigger">
316
+ <div class="trigger-dot"></div>
317
+ </div>
318
+ <div class="side-nav-menu">
319
+ <div class="side-nav-content">
320
+ <!-- 目录项将通过JS动态生成 -->
321
+ </div>
322
+ </div>
323
+
324
+ <script src="scripts.js"></script>
325
+ </body>
326
+ </html>
src/public/login.html ADDED
@@ -0,0 +1,282 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
6
+ <title>Cursor To OpenAI - 管理员登录</title>
7
+ <link rel="stylesheet" href="styles.css">
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
+ <meta name="apple-mobile-web-app-capable" content="yes">
10
+ <meta name="apple-mobile-web-app-status-bar-style" content="default">
11
+ <meta name="theme-color" content="#007aff">
12
+ <script src="theme.js"></script>
13
+ </head>
14
+ <body>
15
+ <div class="container">
16
+ <div class="card header-card">
17
+ <div style="display: flex; align-items: center; margin-bottom: 8px;">
18
+ <i class="fas fa-link" style="font-size: 24px; margin-right: 12px;"></i>
19
+ <h1>Cursor To OpenAI</h1>
20
+ </div>
21
+ <p>管理员登录系统 - 安全访问后台管理界面</p>
22
+ </div>
23
+
24
+ <!-- 登录表单卡片 -->
25
+ <div class="card" id="loginCard">
26
+ <div style="display: flex; align-items: center; margin-bottom: 16px;">
27
+ <i class="fas fa-sign-in-alt" style="color: var(--ios-blue); font-size: 18px; margin-right: 10px;"></i>
28
+ <h2 style="margin: 0;">管理员登录</h2>
29
+ </div>
30
+ <div id="loginMessage" class="message"></div>
31
+ <form id="loginForm">
32
+ <div class="form-group">
33
+ <label for="loginUsername">
34
+ <i class="fas fa-user" style="margin-right: 6px; color: var(--ios-gray);"></i>用户名
35
+ </label>
36
+ <input type="text" id="loginUsername" placeholder="请输入管理员用户名" required>
37
+ </div>
38
+ <div class="form-group">
39
+ <label for="loginPassword">
40
+ <i class="fas fa-lock" style="margin-right: 6px; color: var(--ios-gray);"></i>密码
41
+ </label>
42
+ <input type="password" id="loginPassword" placeholder="请输入密码" required>
43
+ </div>
44
+ <button type="submit">
45
+ <i class="fas fa-sign-in-alt" style="margin-right: 6px;"></i>登录
46
+ </button>
47
+ </form>
48
+ <div class="toggle-form" id="toggleForm">
49
+ <i class="fas fa-user-plus" style="margin-right: 6px; color: var(--ios-blue);"></i>还没有账号?点击注册
50
+ </div>
51
+ </div>
52
+
53
+ <!-- 注册表单卡片 -->
54
+ <div class="card" id="registerCard" style="display: none;">
55
+ <div style="display: flex; align-items: center; margin-bottom: 16px;">
56
+ <i class="fas fa-user-plus" style="color: var(--ios-green); font-size: 18px; margin-right: 10px;"></i>
57
+ <h2 style="margin: 0;">注册管理员账号</h2>
58
+ </div>
59
+ <div id="registerMessage" class="message"></div>
60
+ <form id="registerForm">
61
+ <div class="form-group">
62
+ <label for="registerUsername">
63
+ <i class="fas fa-user" style="margin-right: 6px; color: var(--ios-gray);"></i>用户名
64
+ </label>
65
+ <input type="text" id="registerUsername" placeholder="请设置管理员用户名" required>
66
+ </div>
67
+ <div class="form-group">
68
+ <label for="registerPassword">
69
+ <i class="fas fa-lock" style="margin-right: 6px; color: var(--ios-gray);"></i>密码
70
+ </label>
71
+ <input type="password" id="registerPassword" placeholder="请设置密码" required>
72
+ </div>
73
+ <div class="form-group">
74
+ <label for="confirmPassword">
75
+ <i class="fas fa-check-circle" style="margin-right: 6px; color: var(--ios-gray);"></i>确认密码
76
+ </label>
77
+ <input type="password" id="confirmPassword" placeholder="请再次输入密码" required>
78
+ </div>
79
+ <button type="submit">
80
+ <i class="fas fa-user-plus" style="margin-right: 6px;"></i>注册
81
+ </button>
82
+ </form>
83
+ <div class="toggle-form" id="toggleLoginForm">
84
+ <i class="fas fa-sign-in-alt" style="margin-right: 6px; color: var(--ios-blue);"></i>已有账号?点击登录
85
+ </div>
86
+ </div>
87
+
88
+ <div class="card">
89
+ <div style="display: flex; align-items: center; margin-bottom: 16px;">
90
+ <i class="fas fa-info-circle" style="color: var(--ios-blue); font-size: 18px; margin-right: 10px;"></i>
91
+ <h2 style="margin: 0;">系统信息</h2>
92
+ </div>
93
+ <div class="info">
94
+ <i class="fas fa-shield-alt" style="margin-right: 8px;"></i>
95
+ Cursor To OpenAI 是一个管理自定义 API Key 与 Cursor Cookie 映射关系的高效工具。登录后可以进行API Key的管理和配置。
96
+ </div>
97
+ </div>
98
+ </div>
99
+
100
+ <script>
101
+ // 获取元素
102
+ const loginCard = document.getElementById('loginCard');
103
+ const registerCard = document.getElementById('registerCard');
104
+ const toggleForm = document.getElementById('toggleForm');
105
+ const toggleLoginForm = document.getElementById('toggleLoginForm');
106
+ const loginMessage = document.getElementById('loginMessage');
107
+ const registerMessage = document.getElementById('registerMessage');
108
+ const loginForm = document.getElementById('loginForm');
109
+ const registerForm = document.getElementById('registerForm');
110
+
111
+ // 切换表单显示
112
+ toggleForm.addEventListener('click', () => {
113
+ loginCard.style.display = 'none';
114
+ registerCard.style.display = 'block';
115
+ loginMessage.textContent = '';
116
+ });
117
+
118
+ toggleLoginForm.addEventListener('click', () => {
119
+ registerCard.style.display = 'none';
120
+ loginCard.style.display = 'block';
121
+ registerMessage.textContent = '';
122
+ });
123
+
124
+ // 检查是否已有管理员账号
125
+ async function checkAdminExists() {
126
+ try {
127
+ const response = await fetch('/v1/admin/check');
128
+ const data = await response.json();
129
+
130
+ if (data.exists) {
131
+ // 如果已有管理员,显示登录表单
132
+ loginCard.style.display = 'block';
133
+ registerCard.style.display = 'none';
134
+ toggleForm.style.display = 'none';
135
+ } else {
136
+ // 如果没有管理员,显示注册表单
137
+ loginCard.style.display = 'none';
138
+ registerCard.style.display = 'block';
139
+ toggleLoginForm.style.display = 'none';
140
+
141
+ // 添加首次使用的信息提示
142
+ registerMessage.innerHTML = `
143
+ <div class="info">
144
+ <i class="fas fa-info-circle" style="margin-right: 8px;"></i>
145
+ 首次使用,请注册管理员账号
146
+ </div>`;
147
+ }
148
+ } catch (error) {
149
+ console.error('检查管理员账号失败:', error);
150
+ loginMessage.innerHTML = `
151
+ <div class="error">
152
+ <i class="fas fa-exclamation-circle" style="margin-right: 8px;"></i>
153
+ 连接服务器失败,请检查网络连接
154
+ </div>`;
155
+ }
156
+ }
157
+
158
+ // 登录处理
159
+ loginForm.addEventListener('submit', async (e) => {
160
+ e.preventDefault();
161
+
162
+ const username = document.getElementById('loginUsername').value;
163
+ const password = document.getElementById('loginPassword').value;
164
+
165
+ // 添加登录中的状态提示
166
+ loginMessage.innerHTML = `
167
+ <div class="info">
168
+ <i class="fas fa-spinner fa-spin" style="margin-right: 8px;"></i>
169
+ 登录中,请稍候...
170
+ </div>`;
171
+
172
+ try {
173
+ const response = await fetch('/v1/admin/login', {
174
+ method: 'POST',
175
+ headers: {
176
+ 'Content-Type': 'application/json',
177
+ },
178
+ body: JSON.stringify({ username, password }),
179
+ });
180
+
181
+ const data = await response.json();
182
+
183
+ if (data.success) {
184
+ // 登录成功,保存token并跳转
185
+ localStorage.setItem('adminToken', data.token);
186
+
187
+ loginMessage.innerHTML = `
188
+ <div class="info" style="background-color: rgba(52, 199, 89, 0.1); border-left: 3px solid var(--ios-green);">
189
+ <i class="fas fa-check-circle" style="margin-right: 8px;"></i>
190
+ 登录成功,正在跳转...
191
+ </div>`;
192
+
193
+ setTimeout(() => {
194
+ window.location.href = '/index.html';
195
+ }, 1000);
196
+ } else {
197
+ loginMessage.innerHTML = `
198
+ <div class="error">
199
+ <i class="fas fa-exclamation-circle" style="margin-right: 8px;"></i>
200
+ ${data.message}
201
+ </div>`;
202
+ }
203
+ } catch (error) {
204
+ console.error('登录失败:', error);
205
+ loginMessage.innerHTML = `
206
+ <div class="error">
207
+ <i class="fas fa-exclamation-circle" style="margin-right: 8px;"></i>
208
+ 登录失败,请稍后重试
209
+ </div>`;
210
+ }
211
+ });
212
+
213
+ // 注册处理
214
+ registerForm.addEventListener('submit', async (e) => {
215
+ e.preventDefault();
216
+
217
+ const username = document.getElementById('registerUsername').value;
218
+ const password = document.getElementById('registerPassword').value;
219
+ const confirmPassword = document.getElementById('confirmPassword').value;
220
+
221
+ if (password !== confirmPassword) {
222
+ registerMessage.innerHTML = `
223
+ <div class="error">
224
+ <i class="fas fa-exclamation-circle" style="margin-right: 8px;"></i>
225
+ 两次输入的密码不一致
226
+ </div>`;
227
+ return;
228
+ }
229
+
230
+ // 添加注册中的状态提示
231
+ registerMessage.innerHTML = `
232
+ <div class="info">
233
+ <i class="fas fa-spinner fa-spin" style="margin-right: 8px;"></i>
234
+ 注册中,请稍候...
235
+ </div>`;
236
+
237
+ try {
238
+ const response = await fetch('/v1/admin/register', {
239
+ method: 'POST',
240
+ headers: {
241
+ 'Content-Type': 'application/json',
242
+ },
243
+ body: JSON.stringify({ username, password }),
244
+ });
245
+
246
+ const data = await response.json();
247
+
248
+ if (data.success) {
249
+ // 注册成功,保存token并跳转
250
+ localStorage.setItem('adminToken', data.token);
251
+
252
+ registerMessage.innerHTML = `
253
+ <div class="info" style="background-color: rgba(52, 199, 89, 0.1); border-left: 3px solid var(--ios-green);">
254
+ <i class="fas fa-check-circle" style="margin-right: 8px;"></i>
255
+ 注册成功,正在跳转...
256
+ </div>`;
257
+
258
+ setTimeout(() => {
259
+ window.location.href = '/index.html';
260
+ }, 1000);
261
+ } else {
262
+ registerMessage.innerHTML = `
263
+ <div class="error">
264
+ <i class="fas fa-exclamation-circle" style="margin-right: 8px;"></i>
265
+ ${data.message}
266
+ </div>`;
267
+ }
268
+ } catch (error) {
269
+ console.error('注册失败:', error);
270
+ registerMessage.innerHTML = `
271
+ <div class="error">
272
+ <i class="fas fa-exclamation-circle" style="margin-right: 8px;"></i>
273
+ 注册失败,请稍后重试
274
+ </div>`;
275
+ }
276
+ });
277
+
278
+ // 页面加载时检查管理员账号
279
+ document.addEventListener('DOMContentLoaded', checkAdminExists);
280
+ </script>
281
+ </body>
282
+ </html>
src/public/logs.html ADDED
@@ -0,0 +1,714 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
6
+ <title>Cursor To OpenAI - 日志查看</title>
7
+ <link rel="stylesheet" href="styles.css">
8
+ <style>
9
+ .log-table {
10
+ width: 100%;
11
+ border-collapse: collapse;
12
+ }
13
+ .log-table th, .log-table td {
14
+ padding: 8px;
15
+ text-align: left;
16
+ border-bottom: 1px solid #ddd;
17
+ }
18
+ .log-table tr:hover {
19
+ background-color: rgba(0,0,0,0.05);
20
+ }
21
+ .log-level {
22
+ padding: 3px 6px;
23
+ border-radius: 4px;
24
+ font-weight: bold;
25
+ }
26
+ .log-level-ERROR {
27
+ background-color: #e74c3c;
28
+ color: white;
29
+ }
30
+ .log-level-WARN {
31
+ background-color: #f39c12;
32
+ color: white;
33
+ }
34
+ .log-level-INFO {
35
+ background-color: #27ae60;
36
+ color: white;
37
+ }
38
+ .log-level-DEBUG {
39
+ background-color: #3498db;
40
+ color: white;
41
+ }
42
+ .log-level-TRACE {
43
+ background-color: #9b59b6;
44
+ color: white;
45
+ }
46
+ .log-level-HTTP {
47
+ background-color: #1abc9c;
48
+ color: white;
49
+ }
50
+ .filter-container {
51
+ display: flex;
52
+ flex-wrap: wrap;
53
+ gap: 10px;
54
+ margin-bottom: 15px;
55
+ }
56
+ .filter-group {
57
+ flex: 1;
58
+ min-width: 150px;
59
+ }
60
+ /* 移动端优化 */
61
+ @media (max-width: 768px) {
62
+ .filter-group {
63
+ flex-basis: 100%;
64
+ min-width: auto;
65
+ }
66
+
67
+ .table-responsive {
68
+ overflow-x: auto;
69
+ -webkit-overflow-scrolling: touch;
70
+ }
71
+ }
72
+ .pagination {
73
+ display: flex;
74
+ justify-content: center;
75
+ gap: 10px;
76
+ margin-top: 20px;
77
+ flex-wrap: wrap;
78
+ }
79
+ .pagination button {
80
+ padding: 5px 10px;
81
+ background-color: #3498db;
82
+ color: white;
83
+ border: none;
84
+ border-radius: 4px;
85
+ cursor: pointer;
86
+ min-width: 80px;
87
+ min-height: 36px; /* 增加触摸区域 */
88
+ }
89
+ .pagination button:disabled {
90
+ background-color: #bdc3c7;
91
+ cursor: not-allowed;
92
+ }
93
+ .pagination-info {
94
+ margin-right: 15px;
95
+ align-self: center;
96
+ width: 100%;
97
+ text-align: center;
98
+ margin-bottom: 10px;
99
+ }
100
+ @media (min-width: 768px) {
101
+ .pagination-info {
102
+ width: auto;
103
+ margin-bottom: 0;
104
+ text-align: left;
105
+ }
106
+ }
107
+ .level-filter {
108
+ display: flex;
109
+ flex-wrap: wrap;
110
+ gap: 8px;
111
+ margin-top: 8px;
112
+ }
113
+ .level-checkbox {
114
+ display: none;
115
+ }
116
+ .level-label {
117
+ padding: 5px 10px;
118
+ border-radius: 4px;
119
+ cursor: pointer;
120
+ opacity: 0.4;
121
+ transition: opacity 0.2s;
122
+ /* 增加触摸区域 */
123
+ min-height: 28px;
124
+ display: flex;
125
+ align-items: center;
126
+ justify-content: center;
127
+ }
128
+ .level-checkbox:checked + .level-label {
129
+ opacity: 1;
130
+ outline: 2px solid white;
131
+ }
132
+ .search-box {
133
+ position: relative;
134
+ }
135
+ .search-box input {
136
+ width: 100%;
137
+ padding: 8px;
138
+ padding-right: 40px; /* 增加右侧空间给搜索按钮 */
139
+ border: 1px solid #ddd;
140
+ border-radius: 4px;
141
+ font-size: 16px; /* 避免iOS自动缩放 */
142
+ min-height: 44px; /* 增加触摸区域 */
143
+ -webkit-appearance: none; /* 移除iOS默认样式 */
144
+ appearance: none;
145
+ }
146
+ .search-box button {
147
+ position: absolute;
148
+ right: 5px;
149
+ top: 50%;
150
+ transform: translateY(-50%);
151
+ background: none;
152
+ border: none;
153
+ cursor: pointer;
154
+ color: #555;
155
+ padding: 10px; /* 增加触摸区域 */
156
+ font-size: 18px; /* 增大搜索图标 */
157
+ }
158
+ .date-picker {
159
+ width: 100%;
160
+ padding: 8px;
161
+ border: 1px solid #ddd;
162
+ border-radius: 4px;
163
+ font-size: 16px; /* 避免iOS自动缩放 */
164
+ min-height: 44px; /* 增加触摸区域 */
165
+ -webkit-appearance: none; /* 移除iOS默认样式 */
166
+ appearance: none;
167
+ }
168
+ /* 为安卓设备特殊优化日期选择器 */
169
+ input[type="datetime-local"]::-webkit-calendar-picker-indicator {
170
+ width: 20px;
171
+ height: 20px;
172
+ padding: 5px;
173
+ }
174
+ /* 按钮样式优化 */
175
+ button {
176
+ min-height: 44px; /* 增加触摸区域 */
177
+ font-size: 16px; /* 移动端更容易点击的字体大小 */
178
+ }
179
+ /* 表格在移动端的特殊处理 */
180
+ @media (max-width: 480px) {
181
+ .log-table th:nth-child(1),
182
+ .log-table td:nth-child(1) {
183
+ min-width: 120px;
184
+ }
185
+ .log-table th:nth-child(2),
186
+ .log-table td:nth-child(2) {
187
+ min-width: 80px;
188
+ }
189
+ }
190
+ /* 开关样式 */
191
+ .switch {
192
+ position: relative;
193
+ display: inline-block;
194
+ width: 50px;
195
+ height: 24px;
196
+ }
197
+
198
+ .switch input {
199
+ opacity: 0;
200
+ width: 0;
201
+ height: 0;
202
+ }
203
+
204
+ .slider {
205
+ position: absolute;
206
+ cursor: pointer;
207
+ top: 0;
208
+ left: 0;
209
+ right: 0;
210
+ bottom: 0;
211
+ background-color: #ccc;
212
+ transition: .4s;
213
+ }
214
+
215
+ .slider:before {
216
+ position: absolute;
217
+ content: "";
218
+ height: 16px;
219
+ width: 16px;
220
+ left: 4px;
221
+ bottom: 4px;
222
+ background-color: white;
223
+ transition: .4s;
224
+ }
225
+
226
+ input:checked + .slider {
227
+ background-color: #27ae60;
228
+ }
229
+
230
+ input:focus + .slider {
231
+ box-shadow: 0 0 1px #27ae60;
232
+ }
233
+
234
+ input:checked + .slider:before {
235
+ transform: translateX(26px);
236
+ }
237
+
238
+ .slider.round {
239
+ border-radius: 24px;
240
+ }
241
+
242
+ .slider.round:before {
243
+ border-radius: 50%;
244
+ }
245
+ </style>
246
+ </head>
247
+ <body>
248
+ <div class="container">
249
+ <div class="card header-card">
250
+ <h1>Cursor To OpenAI - 日志查看</h1>
251
+ <p>在此页面上,您可以查看和筛选系统日志。</p>
252
+ <div style="margin-top: 15px; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px;">
253
+ <div style="display: flex; flex-wrap: wrap; gap: 10px;">
254
+ <button id="clearCacheBtn" style="background-color: rgba(255,255,255,0.2);">清除缓存并刷新</button>
255
+ <button id="backBtn" style="background-color: rgba(255,255,255,0.2);">返回主页</button>
256
+ </div>
257
+ <div>
258
+ <span id="adminUsername" style="margin-right: 10px; color: white;"></span>
259
+ <button id="logoutBtn" style="background: rgba(231, 76, 60, 0.8);">退出登录</button>
260
+ </div>
261
+ </div>
262
+ </div>
263
+
264
+ <div class="card">
265
+ <h2>日志筛选</h2>
266
+ <div class="filter-container">
267
+ <div class="filter-group">
268
+ <label>日志级别</label>
269
+ <div class="level-filter">
270
+ <input type="radio" name="level-filter" id="level-all" class="level-checkbox" value="ALL" checked>
271
+ <label for="level-all" class="level-label" style="background-color: #7f8c8d; color: white;">全部</label>
272
+
273
+ <input type="radio" name="level-filter" id="level-error" class="level-checkbox" value="ERROR">
274
+ <label for="level-error" class="level-label log-level-ERROR">错误</label>
275
+
276
+ <input type="radio" name="level-filter" id="level-warn" class="level-checkbox" value="WARN">
277
+ <label for="level-warn" class="level-label log-level-WARN">警告</label>
278
+
279
+ <input type="radio" name="level-filter" id="level-info" class="level-checkbox" value="INFO">
280
+ <label for="level-info" class="level-label log-level-INFO">信息</label>
281
+
282
+ <input type="radio" name="level-filter" id="level-http" class="level-checkbox" value="HTTP">
283
+ <label for="level-http" class="level-label log-level-HTTP">HTTP</label>
284
+
285
+ <input type="radio" name="level-filter" id="level-debug" class="level-checkbox" value="DEBUG">
286
+ <label for="level-debug" class="level-label log-level-DEBUG">调试</label>
287
+
288
+ <input type="radio" name="level-filter" id="level-trace" class="level-checkbox" value="TRACE">
289
+ <label for="level-trace" class="level-label log-level-TRACE">跟踪</label>
290
+ </div>
291
+ </div>
292
+ <div class="filter-group">
293
+ <label for="search">搜索</label>
294
+ <div class="search-box">
295
+ <input type="text" id="search" placeholder="搜索日志内容..." autocomplete="off">
296
+ <button id="searchBtn" aria-label="搜索">🔍</button>
297
+ </div>
298
+ </div>
299
+ <div class="filter-group">
300
+ <label for="startTime">开始时间</label>
301
+ <input type="datetime-local" id="startTime" class="date-picker" pattern="[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}">
302
+ </div>
303
+ <div class="filter-group">
304
+ <label for="endTime">结束时间</label>
305
+ <input type="datetime-local" id="endTime" class="date-picker" pattern="[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}">
306
+ </div>
307
+ <div class="filter-group" style="display: flex; align-items: flex-end;">
308
+ <button id="filterBtn" style="flex: 1; padding: 8px; background-color: #3498db;">应用筛选</button>
309
+ </div>
310
+ <div class="filter-group" style="display: flex; align-items: flex-end;">
311
+ <button id="clearLogsBtn" style="flex: 1; padding: 8px; background-color: #e74c3c;">清空日志</button>
312
+ </div>
313
+ <div class="filter-group">
314
+ <label for="hideCommonLogs">屏蔽常见请求</label>
315
+ <div style="display: flex; align-items: center; margin-top: 8px;">
316
+ <label class="switch" style="margin-right: 10px;">
317
+ <input type="checkbox" id="hideCommonLogs" checked>
318
+ <span class="slider round"></span>
319
+ </label>
320
+ <span id="hideStatus">已开启</span>
321
+ </div>
322
+ </div>
323
+ </div>
324
+ </div>
325
+
326
+ <div class="card">
327
+ <h2>日志列表</h2>
328
+ <div id="logsContainer">
329
+ <div class="table-responsive">
330
+ <table class="log-table">
331
+ <thead>
332
+ <tr>
333
+ <th style="width: 200px;">时间</th>
334
+ <th style="width: 100px;">级别</th>
335
+ <th>内容</th>
336
+ </tr>
337
+ </thead>
338
+ <tbody id="logsList">
339
+ <!-- 日志数据将通过JavaScript动态加载 -->
340
+ </tbody>
341
+ </table>
342
+ </div>
343
+ <div class="pagination">
344
+ <span class="pagination-info">显示 <span id="currentRange">0-0</span> / <span id="totalLogs">0</span> 条日志</span>
345
+ <button id="prevPage" disabled>上一页</button>
346
+ <button id="nextPage" disabled>下一页</button>
347
+ </div>
348
+ </div>
349
+ </div>
350
+ </div>
351
+
352
+ <script>
353
+ // 全局变量
354
+ let currentPage = 1;
355
+ const pageSize = 12;
356
+ let totalLogs = 0;
357
+ let token = localStorage.getItem('adminToken');
358
+ let selectedLevels = []; // 默认为空数组,表示不筛选日志级别
359
+ let hideCommonLogs = true; // 默认屏蔽常见请求日志
360
+
361
+ // 页面加载完成后执行
362
+ document.addEventListener('DOMContentLoaded', function() {
363
+ // 检查登录状态
364
+ checkAuthStatus();
365
+
366
+ // 加载日志数据
367
+ loadLogs();
368
+
369
+ // 屏蔽常见请求开关
370
+ const hideCommonLogsCheckbox = document.getElementById('hideCommonLogs');
371
+ const hideStatus = document.getElementById('hideStatus');
372
+
373
+ hideCommonLogsCheckbox.addEventListener('change', function() {
374
+ hideCommonLogs = this.checked;
375
+ hideStatus.textContent = hideCommonLogs ? '已开启' : '已关闭';
376
+ loadLogs();
377
+ });
378
+
379
+ // 返回主页
380
+ document.getElementById('backBtn').addEventListener('click', function() {
381
+ window.location.href = '/';
382
+ });
383
+
384
+ // 清除缓存并刷新
385
+ document.getElementById('clearCacheBtn').addEventListener('click', function() {
386
+ localStorage.removeItem('logs');
387
+ window.location.reload();
388
+ });
389
+
390
+ // 退出登录
391
+ document.getElementById('logoutBtn').addEventListener('click', function() {
392
+ localStorage.removeItem('adminToken');
393
+ window.location.href = '/login.html';
394
+ });
395
+
396
+ // 筛选按钮
397
+ document.getElementById('filterBtn').addEventListener('click', function() {
398
+ currentPage = 1;
399
+ loadLogs();
400
+ });
401
+
402
+ // 清空日志按钮
403
+ document.getElementById('clearLogsBtn').addEventListener('click', function() {
404
+ if (confirm('确定要清空所有日志吗?此操作不可撤销。')) {
405
+ clearLogs();
406
+ }
407
+ });
408
+
409
+ // 上一页
410
+ document.getElementById('prevPage').addEventListener('click', function() {
411
+ if (currentPage > 1) {
412
+ currentPage--;
413
+ loadLogs();
414
+ }
415
+ });
416
+
417
+ // 下一页
418
+ document.getElementById('nextPage').addEventListener('click', function() {
419
+ if (currentPage * pageSize < totalLogs) {
420
+ currentPage++;
421
+ loadLogs();
422
+ }
423
+ });
424
+
425
+ // 搜索按钮
426
+ document.getElementById('searchBtn').addEventListener('click', function() {
427
+ currentPage = 1;
428
+ loadLogs();
429
+ });
430
+
431
+ // 搜索框回车
432
+ document.getElementById('search').addEventListener('keypress', function(e) {
433
+ if (e.key === 'Enter') {
434
+ currentPage = 1;
435
+ loadLogs();
436
+ }
437
+ });
438
+
439
+ // 日志级别筛选
440
+ document.querySelectorAll('.level-checkbox').forEach(radio => {
441
+ radio.addEventListener('change', function() {
442
+ // 更新选中的日志级别
443
+ if (this.value === 'ALL') {
444
+ selectedLevels = [];
445
+ } else {
446
+ selectedLevels = [this.value];
447
+ }
448
+ });
449
+ });
450
+
451
+ // 修复Android日期选择器问题
452
+ const isAndroid = /Android/i.test(navigator.userAgent);
453
+ if (isAndroid) {
454
+ // 为Android设备添加特殊处理
455
+ const dateInputs = document.querySelectorAll('input[type="datetime-local"]');
456
+ dateInputs.forEach(input => {
457
+ // 监听焦点事件,确保日期选择器在Android上正常工作
458
+ input.addEventListener('focus', function() {
459
+ this.click(); // 确保日期选择器弹出
460
+ });
461
+
462
+ // 监听输入变化,处理可能的格式问题
463
+ input.addEventListener('change', function() {
464
+ if (this.value) {
465
+ // 确保日期格式有效
466
+ try {
467
+ const date = new Date(this.value);
468
+ if (!isNaN(date.getTime())) {
469
+ // 格式有效,无需处理
470
+ } else {
471
+ // 格式无效,清空输入
472
+ this.value = '';
473
+ }
474
+ } catch (e) {
475
+ // 出错时清空输入
476
+ this.value = '';
477
+ }
478
+ }
479
+ });
480
+ });
481
+ }
482
+ });
483
+
484
+ // 检查登录状态
485
+ function checkAuthStatus() {
486
+ const token = localStorage.getItem('adminToken');
487
+
488
+ if (!token) {
489
+ window.location.href = '/login.html';
490
+ return;
491
+ }
492
+
493
+ // 验证token
494
+ fetch('/v1/admin/verify', {
495
+ headers: {
496
+ 'Authorization': `Bearer ${token}`
497
+ }
498
+ })
499
+ .then(response => response.json())
500
+ .then(data => {
501
+ if (!data.success) {
502
+ localStorage.removeItem('adminToken');
503
+ window.location.href = '/login.html';
504
+ } else {
505
+ // 显示管理员用户名
506
+ document.getElementById('adminUsername').textContent = `管理员:${data.username}`;
507
+ }
508
+ })
509
+ .catch(error => {
510
+ console.error('验证失败:', error);
511
+ localStorage.removeItem('adminToken');
512
+ window.location.href = '/login.html';
513
+ });
514
+ }
515
+
516
+ // 加载日志数据
517
+ function loadLogs() {
518
+ const search = document.getElementById('search').value;
519
+ const startTime = document.getElementById('startTime').value;
520
+ const endTime = document.getElementById('endTime').value;
521
+
522
+ // 构建查询参数
523
+ const params = new URLSearchParams({
524
+ page: currentPage,
525
+ pageSize: pageSize
526
+ });
527
+
528
+ // 添加日志级别筛选
529
+ if (selectedLevels.length === 1) {
530
+ params.append('level', selectedLevels[0]);
531
+ }
532
+
533
+ if (search) {
534
+ params.append('search', search);
535
+ }
536
+
537
+ if (startTime) {
538
+ try {
539
+ params.append('startTime', new Date(startTime).toISOString());
540
+ } catch (e) {
541
+ console.error('开始时间格式错误', e);
542
+ }
543
+ }
544
+
545
+ if (endTime) {
546
+ try {
547
+ params.append('endTime', new Date(endTime).toISOString());
548
+ } catch (e) {
549
+ console.error('结束时间格式错误', e);
550
+ }
551
+ }
552
+
553
+ // 输出请求URL便于调试
554
+ console.log(`请求URL: /v1/logs?${params.toString()}`);
555
+
556
+ // 发起请求
557
+ fetch(`/v1/logs?${params.toString()}`, {
558
+ method: 'GET',
559
+ headers: {
560
+ 'Authorization': `Bearer ${token}`
561
+ }
562
+ })
563
+ .then(response => {
564
+ if (!response.ok) {
565
+ throw new Error('加载日志失败');
566
+ }
567
+ return response.json();
568
+ })
569
+ .then(data => {
570
+ if (data.success) {
571
+ renderLogs(data.data);
572
+ } else {
573
+ showMessage('加载日志失败:' + data.message);
574
+ }
575
+ })
576
+ .catch(error => {
577
+ console.error('加载日志错误:', error);
578
+ showMessage('加载日志错误:' + error.message);
579
+ });
580
+ }
581
+
582
+ // 清空日志
583
+ function clearLogs() {
584
+ fetch('/v1/logs', {
585
+ method: 'DELETE',
586
+ headers: {
587
+ 'Authorization': `Bearer ${token}`
588
+ }
589
+ })
590
+ .then(response => {
591
+ if (!response.ok) {
592
+ throw new Error('清空日志失败');
593
+ }
594
+ return response.json();
595
+ })
596
+ .then(data => {
597
+ if (data.success) {
598
+ loadLogs();
599
+ showMessage('日志已清空');
600
+ } else {
601
+ showMessage('清空日志失败:' + data.message);
602
+ }
603
+ })
604
+ .catch(error => {
605
+ console.error('清空日志错误:', error);
606
+ showMessage('清空日志错误:' + error.message);
607
+ });
608
+ }
609
+
610
+ // 渲染日志数据
611
+ function renderLogs(data) {
612
+ const logsList = document.getElementById('logsList');
613
+ logsList.innerHTML = '';
614
+
615
+ if (!data.logs || data.logs.length === 0) {
616
+ logsList.innerHTML = '<tr><td colspan="3" style="text-align: center;">没有符合条件的日志</td></tr>';
617
+ document.getElementById('prevPage').disabled = true;
618
+ document.getElementById('nextPage').disabled = true;
619
+ document.getElementById('currentRange').textContent = '0-0';
620
+ document.getElementById('totalLogs').textContent = '0';
621
+ return;
622
+ }
623
+
624
+ // 要屏蔽的请求路径
625
+ const excludePaths = [
626
+ 'GET /styles.css',
627
+ 'GET /v1/logs',
628
+ 'GET /logs.html'
629
+ ];
630
+
631
+ // 根据用户设置决定是否过滤日志
632
+ let logsToRender = data.logs;
633
+ let totalToShow = data.total;
634
+
635
+ if (hideCommonLogs) {
636
+ // 过滤掉不需要显示的日志
637
+ logsToRender = data.logs.filter(log => {
638
+ if (log.level === 'HTTP') {
639
+ // 检查是否为要屏蔽的请求路径
640
+ for (const path of excludePaths) {
641
+ if (log.message.includes(path)) {
642
+ return false;
643
+ }
644
+ }
645
+ }
646
+ return true;
647
+ });
648
+
649
+ // 如果过滤后没有日志
650
+ if (logsToRender.length === 0) {
651
+ logsList.innerHTML = '<tr><td colspan="3" style="text-align: center;">没有符合条件的日志</td></tr>';
652
+ document.getElementById('prevPage').disabled = true;
653
+ document.getElementById('nextPage').disabled = true;
654
+ document.getElementById('currentRange').textContent = '0-0';
655
+ document.getElementById('totalLogs').textContent = '0';
656
+ return;
657
+ }
658
+
659
+ // 更新总数
660
+ totalToShow = data.total - (data.logs.length - logsToRender.length);
661
+ }
662
+
663
+ // 更新总数和分页信息
664
+ totalLogs = totalToShow;
665
+ const start = (currentPage - 1) * pageSize + 1;
666
+ const end = Math.min(currentPage * pageSize, totalToShow);
667
+ document.getElementById('currentRange').textContent = `${start}-${end}`;
668
+ document.getElementById('totalLogs').textContent = totalToShow;
669
+
670
+ // 更新分页按钮状态
671
+ document.getElementById('prevPage').disabled = currentPage <= 1;
672
+ document.getElementById('nextPage').disabled = end >= totalToShow;
673
+
674
+ // 渲染日志列表
675
+ logsToRender.forEach(log => {
676
+ const row = document.createElement('tr');
677
+
678
+ // 格式化时间
679
+ const timestamp = new Date(log.timestamp);
680
+ const formattedTime = timestamp.toLocaleString('zh-CN');
681
+
682
+ row.innerHTML = `
683
+ <td>${formattedTime}</td>
684
+ <td><span class="log-level log-level-${log.level}">${log.level}</span></td>
685
+ <td>${log.message}</td>
686
+ `;
687
+
688
+ logsList.appendChild(row);
689
+ });
690
+ }
691
+
692
+ // 显示消息
693
+ function showMessage(message) {
694
+ alert(message);
695
+ }
696
+
697
+ // 添加token到所有日志API请求
698
+ (function() {
699
+ const originalFetch = window.fetch;
700
+ window.fetch = function(url, options = {}) {
701
+ // 如果是日志API请求,添加token
702
+ if (url.includes('/v1/logs') && !url.includes('/v1/admin/')) {
703
+ const token = localStorage.getItem('adminToken');
704
+ options.headers = {
705
+ ...options.headers,
706
+ 'Authorization': `Bearer ${token}`
707
+ };
708
+ }
709
+ return originalFetch(url, options);
710
+ };
711
+ })();
712
+ </script>
713
+ </body>
714
+ </html>
src/public/scripts.js ADDED
@@ -0,0 +1,1342 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // 模态框相关功能
2
+ document.addEventListener('DOMContentLoaded', function() {
3
+ // 获取所有模态框和关闭按钮
4
+ const modals = document.querySelectorAll('.modal');
5
+ const closeBtns = document.querySelectorAll('.close');
6
+
7
+ // 关闭所有模态框的函数
8
+ function closeAllModals() {
9
+ modals.forEach(modal => {
10
+ modal.style.display = 'none';
11
+ });
12
+ document.body.classList.remove('modal-open');
13
+ }
14
+
15
+ // 为每个关闭按钮添加事件
16
+ closeBtns.forEach(btn => {
17
+ btn.onclick = closeAllModals;
18
+ });
19
+
20
+ // 点击模态框外部关闭
21
+ window.onclick = function(event) {
22
+ modals.forEach(modal => {
23
+ if (event.target == modal) {
24
+ closeAllModals();
25
+ }
26
+ });
27
+ }
28
+
29
+ // 页面加载时获取 API Key 列表和无效Cookie列表
30
+ checkAuth();
31
+ loadApiKeys();
32
+ renderInvalidCookies();
33
+ populateRefreshApiKeySelect();
34
+ populateCookieApiKeySelect();
35
+
36
+ // 初始化添加Cookie的标签容器
37
+ renderAddCookieTags([]);
38
+
39
+ // 绑定事件监听器
40
+ bindEventListeners();
41
+
42
+ // 处理日志按钮点击事件
43
+ document.getElementById('logsBtn')?.addEventListener('click', function() {
44
+ window.location.href = '/logs.html';
45
+ });
46
+ });
47
+
48
+ // 绑定各种事件监听器
49
+ function bindEventListeners() {
50
+ // 表单提交
51
+ document.getElementById('addKeyForm').addEventListener('submit', handleAddKeyForm);
52
+ document.getElementById('editCookieForm').addEventListener('submit', handleEditCookieForm);
53
+ document.getElementById('invalidCookieForm').addEventListener('submit', handleInvalidCookieForm);
54
+
55
+ // 按钮点击
56
+ // 注意:testApiBtn可能在页面上出现两次,需要检查元素是否存在
57
+ const testApiButtons = document.querySelectorAll('#testApiBtn');
58
+ testApiButtons.forEach(btn => {
59
+ if(btn) btn.addEventListener('click', testApiConnection);
60
+ });
61
+
62
+ const clearCacheButtons = document.querySelectorAll('#clearCacheBtn');
63
+ clearCacheButtons.forEach(btn => {
64
+ if(btn) btn.addEventListener('click', clearCacheAndRefresh);
65
+ });
66
+
67
+ // 其他按钮
68
+ if(document.getElementById('addNewCookieBtn')) document.getElementById('addNewCookieBtn').addEventListener('click', handleAddNewCookie);
69
+ if(document.getElementById('addCookieBtn')) document.getElementById('addCookieBtn').addEventListener('click', handleAddCookie);
70
+ if(document.getElementById('addInvalidCookieBtn')) document.getElementById('addInvalidCookieBtn').addEventListener('click', handleAddInvalidCookie);
71
+ if(document.getElementById('closeInvalidCookieModal')) document.getElementById('closeInvalidCookieModal').addEventListener('click', closeInvalidCookieModal);
72
+
73
+ // 修复刷新Cookie和生成链接按钮的事件绑定
74
+ const refreshCookieBtn = document.getElementById('refreshCookieBtn');
75
+ if(refreshCookieBtn) {
76
+ console.log('为refreshCookieBtn绑定事件');
77
+ refreshCookieBtn.addEventListener('click', handleRefreshCookie);
78
+ }
79
+
80
+ const generateLinkBtn = document.getElementById('generateLinkBtn');
81
+ if(generateLinkBtn) {
82
+ console.log('为generateLinkBtn绑定事件');
83
+ generateLinkBtn.addEventListener('click', handleGenerateLink);
84
+ }
85
+
86
+ if(document.getElementById('logoutBtn')) document.getElementById('logoutBtn').addEventListener('click', handleLogout);
87
+ }
88
+
89
+ // API Key 管理相关函数
90
+ // 加载现有 API Key
91
+ async function loadApiKeys() {
92
+ try {
93
+ console.log('开始加载API Keys...');
94
+ const response = await fetch('/v1/api-keys', {
95
+ method: 'GET',
96
+ headers: {
97
+ 'Content-Type': 'application/json',
98
+ 'Cache-Control': 'no-cache'
99
+ }
100
+ });
101
+
102
+ if (!response.ok) {
103
+ throw new Error(`HTTP错误: ${response.status} ${response.statusText}`);
104
+ }
105
+
106
+ console.log('API响应状态:', response.status);
107
+ const data = await response.json();
108
+ console.log('获取到的数据:', data);
109
+
110
+ const keyList = document.getElementById('keyList');
111
+ keyList.innerHTML = '';
112
+
113
+ if (data.success && data.apiKeys.length > 0) {
114
+ data.apiKeys.forEach(key => {
115
+ const row = document.createElement('tr');
116
+ row.innerHTML = `
117
+ <td data-title="API Key">${key.key}</td>
118
+ <td data-title="Cookie 数量">${key.cookieCount}</td>
119
+ <td data-title="操作">
120
+ <button class="edit-btn" onclick="editApiKey('${key.key}')">修改</button>
121
+ <button class="action-btn" onclick="deleteApiKey('${key.key}')">删除</button>
122
+ </td>
123
+ `;
124
+ keyList.appendChild(row);
125
+ });
126
+ } else {
127
+ keyList.innerHTML = '<tr><td colspan="3" data-title="状态">暂无 API Key</td></tr>';
128
+ }
129
+ } catch (error) {
130
+ console.error('加载 API Key 失败:', error);
131
+ document.getElementById('keyListMessage').innerHTML = `
132
+ <div class="error">加载 API Key 失败: ${error.message}</div>
133
+ `;
134
+ }
135
+ }
136
+
137
+ // 处理添加/更新 API Key 表单提交
138
+ async function handleAddKeyForm(e) {
139
+ e.preventDefault();
140
+
141
+ const apiKey = document.getElementById('apiKey').value.trim();
142
+ const cookieValuesText = document.getElementById('cookieValues').value.trim();
143
+
144
+ if (!apiKey) {
145
+ document.getElementById('addKeyMessage').innerHTML = `
146
+ <div class="error">API Key 不能为空</div>
147
+ `;
148
+ return;
149
+ }
150
+
151
+ // 将逗号分隔的 Cookie 值转换为数组
152
+ const cookieValues = cookieValuesText ?
153
+ cookieValuesText.split(',').map(cookie => cookie.trim()).filter(cookie => cookie) :
154
+ [];
155
+
156
+ try {
157
+ const response = await fetch('/v1/api-keys', {
158
+ method: 'POST',
159
+ headers: {
160
+ 'Content-Type': 'application/json',
161
+ },
162
+ body: JSON.stringify({
163
+ apiKey,
164
+ cookieValues,
165
+ }),
166
+ });
167
+
168
+ const data = await response.json();
169
+
170
+ if (data.success) {
171
+ document.getElementById('addKeyMessage').innerHTML = `
172
+ <div class="info">API Key 添加/更新成功</div>
173
+ `;
174
+ // 等待3秒后再刷新页面
175
+ setTimeout(() => {
176
+ window.location.reload();
177
+ }, 3000);
178
+ } else {
179
+ document.getElementById('addKeyMessage').innerHTML = `
180
+ <div class="error">API Key 添加/更新失败: ${data.error}</div>
181
+ `;
182
+ }
183
+ } catch (error) {
184
+ console.error('添加/更新 API Key 失败:', error);
185
+ document.getElementById('addKeyMessage').innerHTML = `
186
+ <div class="error">添加/更新 API Key 失败: ${error.message}</div>
187
+ `;
188
+ }
189
+ }
190
+
191
+ // 删除 API Key
192
+ async function deleteApiKey(apiKey) {
193
+ if (!confirm(`确定要删除 API Key "${apiKey}" 吗?`)) {
194
+ return;
195
+ }
196
+
197
+ try {
198
+ const response = await fetch(`/v1/api-keys/${encodeURIComponent(apiKey)}`, {
199
+ method: 'DELETE',
200
+ });
201
+
202
+ const data = await response.json();
203
+
204
+ if (data.success) {
205
+ document.getElementById('keyListMessage').innerHTML = `
206
+ <div class="info">API Key 删除成功</div>
207
+ `;
208
+ loadApiKeys();
209
+ } else {
210
+ document.getElementById('keyListMessage').innerHTML = `
211
+ <div class="error">API Key 删除失败: ${data.error}</div>
212
+ `;
213
+ }
214
+ } catch (error) {
215
+ console.error('删除 API Key 失败:', error);
216
+ document.getElementById('keyListMessage').innerHTML = `
217
+ <div class="error">删除 API Key 失败: ${error.message}</div>
218
+ `;
219
+ }
220
+ }
221
+
222
+ // 获取API Key的Cookie值
223
+ async function getCookiesForApiKey(apiKey) {
224
+ try {
225
+ const response = await fetch(`/v1/api-keys/${encodeURIComponent(apiKey)}/cookies`, {
226
+ method: 'GET',
227
+ headers: {
228
+ 'Content-Type': 'application/json',
229
+ 'Cache-Control': 'no-cache'
230
+ }
231
+ });
232
+
233
+ if (!response.ok) {
234
+ throw new Error(`HTTP错误: ${response.status} ${response.statusText}`);
235
+ }
236
+
237
+ const data = await response.json();
238
+ return data.cookies;
239
+ } catch (error) {
240
+ console.error(`获取 ${apiKey} 的Cookie值失败:`, error);
241
+ throw error;
242
+ }
243
+ }
244
+
245
+ // 修改 API Key
246
+ async function editApiKey(apiKey) {
247
+ try {
248
+ document.getElementById('editModalMessage').innerHTML = '';
249
+ document.getElementById('editApiKey').value = apiKey;
250
+
251
+ // 获取当前Cookie值
252
+ const cookies = await getCookiesForApiKey(apiKey);
253
+
254
+ // 更新隐藏的textarea
255
+ document.getElementById('editCookieValues').value = cookies.join(',');
256
+
257
+ // 更新Cookie标签容器
258
+ renderCookieTags(cookies);
259
+
260
+ // 清空新Cookie输入框
261
+ document.getElementById('newCookie').value = '';
262
+
263
+ // 显示模态框
264
+ const modal = document.getElementById('editModal');
265
+ modal.style.display = 'block';
266
+ document.body.classList.add('modal-open');
267
+
268
+ } catch (error) {
269
+ console.error('打开修改模态框失败:', error);
270
+ document.getElementById('editModalMessage').innerHTML = `
271
+ <div class="error">无法加载Cookie数据: ${error.message}</div>
272
+ `;
273
+ const modal = document.getElementById('editModal');
274
+ modal.style.display = 'block'; // 即使出错也显示模态框,以便显示错误信息
275
+ document.body.classList.add('modal-open');
276
+ }
277
+ }
278
+
279
+ // 获取API Keys的辅助函数
280
+ async function getApiKeys() {
281
+ const response = await fetch('/v1/api-keys', {
282
+ method: 'GET',
283
+ headers: {
284
+ 'Content-Type': 'application/json',
285
+ 'Cache-Control': 'no-cache'
286
+ }
287
+ });
288
+
289
+ if (!response.ok) {
290
+ throw new Error(`HTTP错误: ${response.status} ${response.statusText}`);
291
+ }
292
+
293
+ const data = await response.json();
294
+ return data.success ? data.apiKeys : [];
295
+ }
296
+
297
+ // 复制文本到剪贴板的通用函数
298
+ async function copyTextToClipboard(text) {
299
+ try {
300
+ await navigator.clipboard.writeText(text);
301
+ return true;
302
+ } catch (err) {
303
+ console.error('复制到剪贴板失败:', err);
304
+
305
+ // 如果navigator.clipboard不可用,使用备用方法
306
+ try {
307
+ const textArea = document.createElement('textarea');
308
+ textArea.value = text;
309
+
310
+ // 将元素设置为看不见
311
+ textArea.style.position = 'fixed';
312
+ textArea.style.top = '0';
313
+ textArea.style.left = '0';
314
+ textArea.style.width = '2em';
315
+ textArea.style.height = '2em';
316
+ textArea.style.padding = '0';
317
+ textArea.style.border = 'none';
318
+ textArea.style.outline = 'none';
319
+ textArea.style.boxShadow = 'none';
320
+ textArea.style.background = 'transparent';
321
+
322
+ document.body.appendChild(textArea);
323
+ textArea.focus();
324
+ textArea.select();
325
+
326
+ const successful = document.execCommand('copy');
327
+ document.body.removeChild(textArea);
328
+ return successful;
329
+ } catch (fallbackErr) {
330
+ console.error('备用复制方法失败:', fallbackErr);
331
+ return false;
332
+ }
333
+ }
334
+ }
335
+
336
+ // 显示复制成功弹窗提示
337
+ function showCopyToast(success) {
338
+ const toast = document.createElement('div');
339
+ toast.style.position = 'fixed';
340
+ toast.style.bottom = '20px';
341
+ toast.style.left = '50%';
342
+ toast.style.transform = 'translateX(-50%)';
343
+ toast.style.padding = '8px 16px';
344
+ toast.style.borderRadius = '4px';
345
+ toast.style.zIndex = '9999';
346
+ toast.style.fontSize = '14px';
347
+
348
+ if (success) {
349
+ toast.style.backgroundColor = '#28a745';
350
+ toast.style.color = 'white';
351
+ toast.textContent = '复制成功!';
352
+ } else {
353
+ toast.style.backgroundColor = '#dc3545';
354
+ toast.style.color = 'white';
355
+ toast.textContent = '复制失败,请手动复制';
356
+ }
357
+
358
+ document.body.appendChild(toast);
359
+
360
+ setTimeout(() => {
361
+ toast.style.opacity = '0';
362
+ toast.style.transition = 'opacity 0.5s';
363
+ setTimeout(() => {
364
+ document.body.removeChild(toast);
365
+ }, 500);
366
+ }, 2000);
367
+ }
368
+
369
+ // 处理复制Cookie按钮点击
370
+ async function handleCopyCookie(cookie) {
371
+ const success = await copyTextToClipboard(cookie);
372
+ showCopyToast(success);
373
+ }
374
+
375
+ // Cookie管理相关函数
376
+ // 渲染Cookie标签
377
+ function renderCookieTags(cookies) {
378
+ const container = document.getElementById('cookieTagsContainer');
379
+ container.innerHTML = '';
380
+
381
+ if (cookies.length === 0) {
382
+ container.innerHTML = '<div style="padding: 10px; color: #666;">暂无Cookie,请添加</div>';
383
+ return;
384
+ }
385
+
386
+ cookies.forEach((cookie, index) => {
387
+ // 创建标签
388
+ const tag = document.createElement('span');
389
+ tag.className = 'cookie-tag';
390
+
391
+ // 对短文本添加特殊类
392
+ if (cookie.length < 5) {
393
+ tag.classList.add('short-cookie');
394
+ }
395
+
396
+ // 截断Cookie显示
397
+ const displayText = cookie.length > 20 ?
398
+ cookie.substring(0, 8) + '...' + cookie.substring(cookie.length - 8) :
399
+ cookie;
400
+
401
+ tag.title = cookie; // 完整Cookie作为工具提示
402
+
403
+ // 增加对移动端友好的结构,添加复制按钮
404
+ tag.innerHTML = `
405
+ <span class="cookie-text-content">${displayText}</span>
406
+ <div class="cookie-buttons">
407
+ <button type="button" class="copy-btn" data-cookie="${cookie}" aria-label="复制">C</button>
408
+ <button type="button" class="delete-cookie" data-index="${index}" aria-label="删除">×</button>
409
+ </div>
410
+ `;
411
+ container.appendChild(tag);
412
+ });
413
+
414
+ // 添加删除按钮的事件监听
415
+ document.querySelectorAll('.delete-cookie').forEach(btn => {
416
+ btn.addEventListener('click', function() {
417
+ const index = parseInt(this.getAttribute('data-index'));
418
+ deleteCookieTag(index);
419
+ });
420
+ });
421
+
422
+ // 添加复制按钮的事件监听
423
+ document.querySelectorAll('.copy-btn').forEach(btn => {
424
+ btn.addEventListener('click', function() {
425
+ const cookie = this.getAttribute('data-cookie');
426
+ handleCopyCookie(cookie);
427
+ });
428
+ });
429
+ }
430
+
431
+ // 删除Cookie标签
432
+ function deleteCookieTag(index) {
433
+ // 从隐藏的textarea中获取当前的cookies
434
+ const cookieValuesElem = document.getElementById('editCookieValues');
435
+ let cookies = cookieValuesElem.value.split(',').map(c => c.trim()).filter(c => c);
436
+
437
+ // 删除指定索引的cookie
438
+ cookies.splice(index, 1);
439
+
440
+ // 更新隐藏的textarea
441
+ cookieValuesElem.value = cookies.join(',');
442
+
443
+ // 重新渲染标签
444
+ renderCookieTags(cookies);
445
+ }
446
+
447
+ // 处理添加新Cookie
448
+ function handleAddCookie() {
449
+ const newCookieInput = document.getElementById('newCookie');
450
+ const newCookie = newCookieInput.value.trim();
451
+
452
+ if (!newCookie) {
453
+ return;
454
+ }
455
+
456
+ // 获取当前的cookies
457
+ const cookieValuesElem = document.getElementById('editCookieValues');
458
+ let cookies = cookieValuesElem.value ?
459
+ cookieValuesElem.value.split(',').map(c => c.trim()).filter(c => c) :
460
+ [];
461
+
462
+ // 添加新cookie
463
+ cookies.push(newCookie);
464
+
465
+ // 更新隐藏的textarea
466
+ cookieValuesElem.value = cookies.join(',');
467
+
468
+ // 重新渲染标签
469
+ renderCookieTags(cookies);
470
+
471
+ // 清空输入框
472
+ newCookieInput.value = '';
473
+ }
474
+
475
+ // 处理编辑表单提交
476
+ async function handleEditCookieForm(e) {
477
+ e.preventDefault();
478
+
479
+ const apiKey = document.getElementById('editApiKey').value.trim();
480
+ const cookieValuesText = document.getElementById('editCookieValues').value.trim();
481
+
482
+ if (!apiKey) {
483
+ document.getElementById('editModalMessage').innerHTML = `
484
+ <div class="error">API Key不能为空</div>
485
+ `;
486
+ return;
487
+ }
488
+
489
+ // 将逗号分隔的 Cookie 值转换为数组
490
+ const cookieValues = cookieValuesText ?
491
+ cookieValuesText.split(',').map(cookie => cookie.trim()).filter(cookie => cookie) :
492
+ [];
493
+
494
+ try {
495
+ const response = await fetch('/v1/api-keys', {
496
+ method: 'POST',
497
+ headers: {
498
+ 'Content-Type': 'application/json',
499
+ },
500
+ body: JSON.stringify({
501
+ apiKey,
502
+ cookieValues,
503
+ }),
504
+ });
505
+
506
+ const data = await response.json();
507
+
508
+ if (data.success) {
509
+ document.getElementById('editModalMessage').innerHTML = `
510
+ <div class="info">Cookie 修改成功</div>
511
+ `;
512
+ setTimeout(() => {
513
+ document.getElementById('editModal').style.display = 'none';
514
+ loadApiKeys();
515
+ }, 1500);
516
+ } else {
517
+ document.getElementById('editModalMessage').innerHTML = `
518
+ <div class="error">Cookie 修改失败: ${data.error}</div>
519
+ `;
520
+ }
521
+ } catch (error) {
522
+ console.error('修改 Cookie 失败:', error);
523
+ document.getElementById('editModalMessage').innerHTML = `
524
+ <div class="error">修改 Cookie 失败: ${error.message}</div>
525
+ `;
526
+ }
527
+ }
528
+
529
+ // 渲染添加API Key表单中的Cookie标签
530
+ function renderAddCookieTags(cookies) {
531
+ const container = document.getElementById('addCookieTagsContainer');
532
+ container.innerHTML = '';
533
+
534
+ if (cookies.length === 0) {
535
+ container.innerHTML = '<div style="padding: 10px; color: #666;">暂无Cookie,请添加</div>';
536
+ return;
537
+ }
538
+
539
+ cookies.forEach((cookie, index) => {
540
+ const tag = document.createElement('span');
541
+ tag.className = 'cookie-tag';
542
+
543
+ // 对短文本添加特殊类
544
+ if (cookie.length < 5) {
545
+ tag.classList.add('short-cookie');
546
+ }
547
+
548
+ const displayText = cookie.length > 20 ?
549
+ cookie.substring(0, 8) + '...' + cookie.substring(cookie.length - 8) :
550
+ cookie;
551
+
552
+ tag.title = cookie;
553
+
554
+ // 增加对移动端友好的结构,添加复制按钮
555
+ tag.innerHTML = `
556
+ <span class="cookie-text-content">${displayText}</span>
557
+ <div class="cookie-buttons">
558
+ <button type="button" class="copy-btn" data-cookie="${cookie}" aria-label="复制">C</button>
559
+ <button type="button" class="delete-add-cookie" data-index="${index}" aria-label="删除">×</button>
560
+ </div>
561
+ `;
562
+ container.appendChild(tag);
563
+ });
564
+
565
+ document.querySelectorAll('.delete-add-cookie').forEach(btn => {
566
+ btn.addEventListener('click', function() {
567
+ const index = parseInt(this.getAttribute('data-index'));
568
+ deleteAddCookieTag(index);
569
+ });
570
+ });
571
+
572
+ // 添加复制按钮的事件监听
573
+ document.querySelectorAll('.copy-btn').forEach(btn => {
574
+ btn.addEventListener('click', function() {
575
+ const cookie = this.getAttribute('data-cookie');
576
+ handleCopyCookie(cookie);
577
+ });
578
+ });
579
+ }
580
+
581
+ // 删除添加表单中的Cookie标签
582
+ function deleteAddCookieTag(index) {
583
+ const cookieValuesElem = document.getElementById('cookieValues');
584
+ let cookies = cookieValuesElem.value ?
585
+ cookieValuesElem.value.split(',').map(c => c.trim()).filter(c => c) :
586
+ [];
587
+
588
+ cookies.splice(index, 1);
589
+ cookieValuesElem.value = cookies.join(',');
590
+ renderAddCookieTags(cookies);
591
+ }
592
+
593
+ // 处理添加新Cookie标签到添加表单
594
+ function handleAddNewCookie() {
595
+ const newCookieInput = document.getElementById('addNewCookie');
596
+ const newCookie = newCookieInput.value.trim();
597
+
598
+ if (!newCookie) {
599
+ return;
600
+ }
601
+
602
+ const cookieValuesElem = document.getElementById('cookieValues');
603
+ let cookies = cookieValuesElem.value ?
604
+ cookieValuesElem.value.split(',').map(c => c.trim()).filter(c => c) :
605
+ [];
606
+
607
+ cookies.push(newCookie);
608
+ cookieValuesElem.value = cookies.join(',');
609
+ renderAddCookieTags(cookies);
610
+ newCookieInput.value = '';
611
+ }
612
+
613
+ // 无效Cookie管理相关函数
614
+ // 获取无效Cookie列表
615
+ async function getInvalidCookies() {
616
+ try {
617
+ const response = await fetch('/v1/invalid-cookies', {
618
+ method: 'GET',
619
+ headers: {
620
+ 'Content-Type': 'application/json',
621
+ 'Cache-Control': 'no-cache'
622
+ }
623
+ });
624
+
625
+ if (!response.ok) {
626
+ throw new Error(`HTTP错误: ${response.status} ${response.statusText}`);
627
+ }
628
+
629
+ const data = await response.json();
630
+ return data.invalidCookies;
631
+ } catch (error) {
632
+ console.error('获取无效Cookie失败:', error);
633
+ throw error;
634
+ }
635
+ }
636
+
637
+ // 清除特定无效Cookie
638
+ async function clearInvalidCookie(cookie) {
639
+ try {
640
+ const response = await fetch(`/v1/invalid-cookies/${encodeURIComponent(cookie)}`, {
641
+ method: 'DELETE',
642
+ headers: {
643
+ 'Content-Type': 'application/json',
644
+ 'Cache-Control': 'no-cache'
645
+ }
646
+ });
647
+
648
+ if (!response.ok) {
649
+ throw new Error(`HTTP错误: ${response.status} ${response.statusText}`);
650
+ }
651
+
652
+ const data = await response.json();
653
+ return data.success;
654
+ } catch (error) {
655
+ console.error(`清除无效Cookie失败:`, error);
656
+ throw error;
657
+ }
658
+ }
659
+
660
+ // 清除所有无效Cookie
661
+ async function clearAllInvalidCookies() {
662
+ try {
663
+ const response = await fetch('/v1/invalid-cookies', {
664
+ method: 'DELETE',
665
+ headers: {
666
+ 'Content-Type': 'application/json',
667
+ 'Cache-Control': 'no-cache'
668
+ }
669
+ });
670
+
671
+ if (!response.ok) {
672
+ throw new Error(`HTTP错误: ${response.status} ${response.statusText}`);
673
+ }
674
+
675
+ const data = await response.json();
676
+ return data.success;
677
+ } catch (error) {
678
+ console.error('清除所有无效Cookie失败:', error);
679
+ throw error;
680
+ }
681
+ }
682
+
683
+ // 渲染无效Cookie列表
684
+ async function renderInvalidCookies() {
685
+ const container = document.getElementById('invalidCookiesContainer');
686
+
687
+ try {
688
+ const invalidCookies = await getInvalidCookies();
689
+
690
+ if (invalidCookies.length === 0) {
691
+ container.innerHTML = '<div class="info">没有检测到无效Cookie</div>';
692
+ return;
693
+ }
694
+
695
+ let html = '<div class="table-responsive"><table><thead><tr><th>无效Cookie</th><th>数量</th><th>操作</th></tr></thead><tbody>';
696
+
697
+ // 展示为一行,类似于API Key列表
698
+ html += `
699
+ <tr>
700
+ <td data-title="无效Cookie">无效Cookie</td>
701
+ <td data-title="数量">${invalidCookies.length}</td>
702
+ <td data-title="操作">
703
+ <button class="edit-btn" id="editInvalidCookiesBtn">修改</button>
704
+ <button class="action-btn" id="clearAllInvalidCookiesInTable">删除</button>
705
+ </td>
706
+ </tr>
707
+ `;
708
+
709
+ html += '</tbody></table></div>';
710
+ container.innerHTML = html;
711
+
712
+ // 添加按钮事件监听
713
+ document.getElementById('editInvalidCookiesBtn').addEventListener('click', openInvalidCookieModal);
714
+ document.getElementById('clearAllInvalidCookiesInTable').addEventListener('click', handleClearAllInvalidCookies);
715
+
716
+ } catch (error) {
717
+ container.innerHTML = `<div class="error">加载失败: ${error.message}</div>`;
718
+ }
719
+ }
720
+
721
+ // 处理清除所有无效Cookie按钮事件
722
+ async function handleClearAllInvalidCookies() {
723
+ try {
724
+ await clearAllInvalidCookies();
725
+ showMessage('invalidCookiesContainer', '所有无效Cookie已清除', 'info');
726
+ renderInvalidCookies(); // 重新渲染列表
727
+ } catch (error) {
728
+ showMessage('invalidCookiesContainer', `清除失败: ${error.message}`, 'error');
729
+ }
730
+ }
731
+
732
+ // API 测试相关函数
733
+ // 测试API连接
734
+ async function testApiConnection() {
735
+ const resultDiv = document.getElementById('testApiResult');
736
+ resultDiv.innerHTML = '<div class="info">正在测试API连接...</div>';
737
+
738
+ try {
739
+ const response = await fetch('/v1/api-keys', {
740
+ method: 'GET',
741
+ headers: {
742
+ 'Content-Type': 'application/json',
743
+ 'Cache-Control': 'no-cache'
744
+ }
745
+ });
746
+
747
+ resultDiv.innerHTML = `<div class="info">API响应状态: ${response.status}</div>`;
748
+
749
+ if (!response.ok) {
750
+ throw new Error(`HTTP错误: ${response.status} ${response.statusText}`);
751
+ }
752
+
753
+ const data = await response.json();
754
+ resultDiv.innerHTML += `<div class="info">获取到的数据: ${JSON.stringify(data)}</div>`;
755
+ } catch (error) {
756
+ console.error('测试API失败:', error);
757
+ resultDiv.innerHTML = `<div class="error">测试API失败: ${error.message}</div>`;
758
+ }
759
+ }
760
+
761
+ // 清除缓存并刷新
762
+ function clearCacheAndRefresh() {
763
+ // 清除缓存
764
+ if ('caches' in window) {
765
+ caches.keys().then(function(names) {
766
+ for (let name of names) {
767
+ caches.delete(name);
768
+ }
769
+ });
770
+ }
771
+
772
+ // 强制刷新页面(绕过缓存)
773
+ window.location.reload(true);
774
+ }
775
+
776
+ // 显示消息的通用函数
777
+ function showMessage(containerId, message, type) {
778
+ const container = document.getElementById(containerId);
779
+ container.innerHTML = `<div class="${type}">${message}</div>`;
780
+ }
781
+
782
+ // Cookie刷新相关函数
783
+ // 填充刷新API Key的下拉选择框
784
+ async function populateRefreshApiKeySelect() {
785
+ try {
786
+ const apiKeys = await getApiKeys();
787
+ const select = document.getElementById('refreshApiKey');
788
+
789
+ // 清空现有选项(保留"所有API Key"选项)
790
+ while (select.options.length > 1) {
791
+ select.remove(1);
792
+ }
793
+
794
+ // 添加API Key选项
795
+ apiKeys.forEach(key => {
796
+ const option = document.createElement('option');
797
+ option.value = key.key;
798
+ option.textContent = `${key.key} (${key.cookieCount} 个Cookie)`;
799
+ select.appendChild(option);
800
+ });
801
+ } catch (error) {
802
+ console.error('加载API Key选项失败:', error);
803
+ }
804
+ }
805
+
806
+ // 处理刷新Cookie按钮事件
807
+ async function handleRefreshCookie() {
808
+ console.log('刷新Cookie按钮被点击');
809
+ const refreshBtn = document.getElementById('refreshCookieBtn');
810
+ const apiKey = document.getElementById('refreshApiKey').value;
811
+ const statusContainer = document.getElementById('refreshStatusContainer');
812
+ const statusText = document.getElementById('refreshStatus');
813
+ const progressBar = document.getElementById('refreshProgress');
814
+
815
+ // 显示调试信息
816
+ showMessage('refreshCookieMessage', '正在准备发送请求...', 'info');
817
+
818
+ // 禁用按钮,显示状态容器
819
+ refreshBtn.disabled = true;
820
+ statusContainer.style.display = 'block';
821
+ statusText.textContent = '正在发送刷新请求...';
822
+ progressBar.value = 10;
823
+
824
+ try {
825
+ // 构建请求URL
826
+ let url = '/v1/refresh-cookies';
827
+ if (apiKey) {
828
+ url += `?apiKey=${encodeURIComponent(apiKey)}`;
829
+ }
830
+
831
+ // 发送刷新请求
832
+ statusText.textContent = '正在发送刷新请求...';
833
+ progressBar.value = 20;
834
+
835
+ const response = await fetch(url, {
836
+ method: 'POST',
837
+ headers: {
838
+ 'Content-Type': 'application/json',
839
+ 'Cache-Control': 'no-cache'
840
+ }
841
+ });
842
+
843
+ if (!response.ok) {
844
+ throw new Error(`HTTP错误: ${response.status} ${response.statusText}`);
845
+ }
846
+
847
+ // 显示长时间等待提示
848
+ statusText.textContent = '刷新请求已发送,请耐心等待2-12分钟...';
849
+ progressBar.value = 50;
850
+ showMessage('refreshCookieMessage', '刷新请求已发送,由于需要访问Cursor官网获取新Cookie,整个过程可能需要2-12分钟,请耐心等待。您可以关闭此页面,稍后再来查看结果。', 'info');
851
+
852
+ // 启动定时检查刷新状态
853
+ let checkInterval = setInterval(async () => {
854
+ try {
855
+ const statusResponse = await fetch('/v1/refresh-status', {
856
+ method: 'GET',
857
+ headers: {
858
+ 'Cache-Control': 'no-cache'
859
+ }
860
+ });
861
+
862
+ if (!statusResponse.ok) {
863
+ throw new Error(`HTTP错误: ${statusResponse.status} ${statusResponse.statusText}`);
864
+ }
865
+
866
+ const statusData = await statusResponse.json();
867
+ const refreshData = statusData.data;
868
+
869
+ // 更新状态信息
870
+ statusText.textContent = refreshData.message || '正在刷新...';
871
+
872
+ // 根据状态更新进度条和UI
873
+ if (refreshData.status === 'completed') {
874
+ // 刷新完成
875
+ progressBar.value = 100;
876
+ statusText.textContent = `刷新完成: ${refreshData.message}`;
877
+ clearInterval(checkInterval);
878
+
879
+ // 重新加载API Key列表
880
+ await loadApiKeys();
881
+ await populateRefreshApiKeySelect();
882
+
883
+ // 显示成功消息
884
+ showMessage('refreshCookieMessage', `刷新完成: ${refreshData.message}`, 'success');
885
+
886
+ // 启用按钮
887
+ refreshBtn.disabled = false;
888
+
889
+ // 3秒后隐藏状态容器
890
+ setTimeout(() => {
891
+ statusContainer.style.display = 'none';
892
+ }, 3000);
893
+ } else if (refreshData.status === 'failed') {
894
+ // 刷新失败
895
+ progressBar.value = 0;
896
+ statusText.textContent = `刷新失败: ${refreshData.message}`;
897
+ clearInterval(checkInterval);
898
+
899
+ // 显示错误消息
900
+ showMessage('refreshCookieMessage', `刷新失败: ${refreshData.message}`, 'error');
901
+
902
+ // 启用按钮
903
+ refreshBtn.disabled = false;
904
+ } else if (refreshData.status === 'running') {
905
+ // 正在刷新
906
+ progressBar.value = 75;
907
+ } else if (!refreshData.isRunning) {
908
+ // 未知状态但不在运行
909
+ clearInterval(checkInterval);
910
+ refreshBtn.disabled = false;
911
+ }
912
+ } catch (error) {
913
+ console.error('检查刷新状态失败:', error);
914
+ }
915
+ }, 5000); // 每5秒检查一次
916
+
917
+ // 设置超时检查,12分钟后如果还没完成就停止检查
918
+ setTimeout(() => {
919
+ if (checkInterval) {
920
+ clearInterval(checkInterval);
921
+ refreshBtn.disabled = false;
922
+ statusContainer.style.display = 'none';
923
+ }
924
+ }, 720000);
925
+ } catch (error) {
926
+ console.error('刷新Cookie失败:', error);
927
+ statusText.textContent = '刷新请求发送失败';
928
+ progressBar.value = 0;
929
+ showMessage('refreshCookieMessage', `刷新请求发送失败: ${error.message}`, 'error');
930
+ refreshBtn.disabled = false;
931
+ }
932
+ }
933
+
934
+ // 获取Cookie相关函数
935
+ // 为Cookie获取功能填充API Key下拉框
936
+ function populateCookieApiKeySelect() {
937
+ populateRefreshApiKeySelect().then(() => {
938
+ // 复制refreshApiKey的选项到targetApiKey
939
+ const sourceSelect = document.getElementById('refreshApiKey');
940
+ const targetSelect = document.getElementById('targetApiKey');
941
+
942
+ // 保留第一个选项("所有API Key")
943
+ while (targetSelect.options.length > 1) {
944
+ targetSelect.remove(1);
945
+ }
946
+
947
+ // 复制选项
948
+ for (let i = 1; i < sourceSelect.options.length; i++) {
949
+ const option = document.createElement('option');
950
+ option.value = sourceSelect.options[i].value;
951
+ option.textContent = sourceSelect.options[i].textContent;
952
+ targetSelect.appendChild(option);
953
+ }
954
+ });
955
+ }
956
+
957
+ // 处理生成登录链接
958
+ async function handleGenerateLink() {
959
+ console.log('生成登录链接按钮被点击');
960
+ const messageContainer = document.getElementById('getCookieMessage');
961
+ const linkContainer = document.getElementById('loginLinkContainer');
962
+ const loginLink = document.getElementById('loginLink');
963
+ const pollStatusText = document.getElementById('pollStatusText');
964
+ const pollProgress = document.getElementById('pollProgress');
965
+ const targetApiKey = document.getElementById('targetApiKey').value;
966
+
967
+ try {
968
+ // 显示加载状态
969
+ messageContainer.innerHTML = '<div class="info">正在生成登录链接...</div>';
970
+
971
+ // 请求生成登录链接
972
+ const response = await fetch('/v1/generate-cookie-link', {
973
+ method: 'POST',
974
+ headers: {
975
+ 'Content-Type': 'application/json',
976
+ 'Cache-Control': 'no-cache'
977
+ },
978
+ body: JSON.stringify({ apiKey: targetApiKey })
979
+ });
980
+
981
+ if (!response.ok) {
982
+ throw new Error(`HTTP错误: ${response.status} ${response.statusText}`);
983
+ }
984
+
985
+ const data = await response.json();
986
+
987
+ if (!data.success) {
988
+ throw new Error(data.message || '生成链接失败');
989
+ }
990
+
991
+ // 显示链接
992
+ loginLink.href = data.url;
993
+ loginLink.textContent = data.url;
994
+ linkContainer.style.display = 'block';
995
+
996
+ // 更新状态
997
+ pollStatusText.textContent = '等待用户登录...';
998
+ pollProgress.value = 10;
999
+
1000
+ // 开始轮询获取cookie状态
1001
+ messageContainer.innerHTML = '<div class="info">链接已生成,请点击链接登录Cursor账号并授权</div>';
1002
+
1003
+ // 开始轮询cookie状态
1004
+ pollForCookieStatus(data.uuid);
1005
+
1006
+ } catch (error) {
1007
+ console.error('生成登录链接失败:', error);
1008
+ messageContainer.innerHTML = `<div class="error">生成链接失败: ${error.message}</div>`;
1009
+ }
1010
+ }
1011
+
1012
+ // 轮询Cookie获取状态
1013
+ function pollForCookieStatus(uuid) {
1014
+ const messageContainer = document.getElementById('getCookieMessage');
1015
+ const pollStatusText = document.getElementById('pollStatusText');
1016
+ const pollProgress = document.getElementById('pollProgress');
1017
+ const maxAttempts = 300; // 最多尝试300次,相当于5分钟(原来是120次,2分钟)
1018
+ let attempt = 0;
1019
+
1020
+ // 更新状态显示
1021
+ pollStatusText.textContent = '等待用户登录...';
1022
+
1023
+ const interval = setInterval(function() {
1024
+ attempt++;
1025
+
1026
+ try {
1027
+ // 更新进度条(保持在10%-90%之间,表示等待中)
1028
+ pollProgress.value = 10 + Math.min(80, attempt / 3.75); // 调整进度条速度以适应5分钟
1029
+
1030
+ // 请求检查状态
1031
+ fetch(`/v1/check-cookie-status?uuid=${encodeURIComponent(uuid)}`, {
1032
+ method: 'GET',
1033
+ headers: {
1034
+ 'Cache-Control': 'no-cache'
1035
+ }
1036
+ }).then(function(response) {
1037
+ if (!response.ok) {
1038
+ pollStatusText.textContent = `请求失败: ${response.status}`;
1039
+ return;
1040
+ }
1041
+
1042
+ return response.json();
1043
+ }).then(function(data) {
1044
+ if (data.success) {
1045
+ // Cookie获取成功
1046
+ clearInterval(interval);
1047
+ pollProgress.value = 100;
1048
+ pollStatusText.textContent = '获取Cookie成功!';
1049
+ messageContainer.innerHTML = `<div class="info">成功获取并添加Cookie!${data.message || ''}</div>`;
1050
+
1051
+ // 刷新API Keys列表
1052
+ loadApiKeys();
1053
+ populateCookieApiKeySelect();
1054
+
1055
+ } else if (data.status === 'waiting') {
1056
+ // 继续等待
1057
+ pollStatusText.textContent = '等待用户登录...';
1058
+ } else if (data.status === 'failed') {
1059
+ // 获取失败
1060
+ clearInterval(interval);
1061
+ pollStatusText.textContent = '获取失败';
1062
+ pollProgress.value = 0;
1063
+ messageContainer.innerHTML = `<div class="error">获取Cookie失败: ${data.message || '未知错误'}</div>`;
1064
+ }
1065
+ }).catch(function(error) {
1066
+ console.error('轮询Cookie状态失败:', error);
1067
+ pollStatusText.textContent = `轮询出错: ${error.message}`;
1068
+ });
1069
+
1070
+ } catch (error) {
1071
+ console.error('轮询Cookie状态出错:', error);
1072
+ pollStatusText.textContent = `轮询出错: ${error.message}`;
1073
+ }
1074
+
1075
+ // 达到最大尝试次数后停止
1076
+ if (attempt >= maxAttempts) {
1077
+ clearInterval(interval);
1078
+ pollStatusText.textContent = '超时,请重试';
1079
+ pollProgress.value = 0;
1080
+ messageContainer.innerHTML = '<div class="error">获取Cookie超时,请重新尝试</div>';
1081
+ }
1082
+
1083
+ }, 1000); // 每秒轮询一次
1084
+ }
1085
+
1086
+ // 授权相关函数
1087
+ // 检查登录状态
1088
+ function checkAuth() {
1089
+ const token = localStorage.getItem('adminToken');
1090
+ if (!token) {
1091
+ window.location.href = '/login.html';
1092
+ return;
1093
+ }
1094
+
1095
+ // 验证token
1096
+ fetch('/v1/admin/verify', {
1097
+ headers: {
1098
+ 'Authorization': `Bearer ${token}`
1099
+ }
1100
+ })
1101
+ .then(response => response.json())
1102
+ .then(data => {
1103
+ if (!data.success) {
1104
+ localStorage.removeItem('adminToken');
1105
+ window.location.href = '/login.html';
1106
+ } else {
1107
+ // 更新为新的用户名显示方式
1108
+ const usernameElem = document.getElementById('usernameText');
1109
+ if (usernameElem) {
1110
+ usernameElem.textContent = data.username;
1111
+ } else {
1112
+ // 兼容旧版模板,可能没有usernameText元素
1113
+ const adminElem = document.getElementById('adminUsername');
1114
+ if (adminElem) {
1115
+ adminElem.textContent = `管理员:${data.username}`;
1116
+ }
1117
+ }
1118
+ }
1119
+ })
1120
+ .catch(error => {
1121
+ console.error('验证失败:', error);
1122
+ localStorage.removeItem('adminToken');
1123
+ window.location.href = '/login.html';
1124
+ });
1125
+ }
1126
+
1127
+ // 处理退出登录
1128
+ function handleLogout() {
1129
+ localStorage.removeItem('adminToken');
1130
+ window.location.href = '/login.html';
1131
+ }
1132
+
1133
+ // 添加token到所有API请求
1134
+ function addAuthHeader(headers = {}) {
1135
+ const token = localStorage.getItem('adminToken');
1136
+ return {
1137
+ ...headers,
1138
+ 'Authorization': `Bearer ${token}`
1139
+ };
1140
+ }
1141
+
1142
+ // 修改所有fetch请求,添加token
1143
+ (function() {
1144
+ const originalFetch = window.fetch;
1145
+ window.fetch = function(url, options = {}) {
1146
+ // 只对管理页面的API请求添加token
1147
+ if (url.includes('/v1/api-keys') ||
1148
+ url.includes('/v1/invalid-cookies') ||
1149
+ url.includes('/v1/refresh-cookies') ||
1150
+ url.includes('/v1/generate-cookie-link') ||
1151
+ url.includes('/v1/check-cookie-status') ||
1152
+ url.includes('/v1/logs')) {
1153
+ options.headers = addAuthHeader(options.headers);
1154
+ }
1155
+ return originalFetch(url, options);
1156
+ };
1157
+ })();
1158
+
1159
+ // 无效Cookie模态窗口相关函数
1160
+ // 打开无效Cookie模态窗口
1161
+ async function openInvalidCookieModal() {
1162
+ try {
1163
+ document.getElementById('invalidCookieModalMessage').innerHTML = '';
1164
+ const invalidCookies = await getInvalidCookies();
1165
+ renderInvalidCookieTags(invalidCookies);
1166
+ document.getElementById('invalidCookiesValues').value = invalidCookies.join(',');
1167
+ document.getElementById('newInvalidCookie').value = '';
1168
+ const modal = document.getElementById('invalidCookieModal');
1169
+ modal.style.display = 'block';
1170
+ document.body.classList.add('modal-open');
1171
+ } catch (error) {
1172
+ console.error('打开无效Cookie模态框失败:', error);
1173
+ showMessage('invalidCookiesContainer', `加载无效Cookie失败: ${error.message}`, 'error');
1174
+ }
1175
+ }
1176
+
1177
+ // 关闭无效Cookie模态框
1178
+ function closeInvalidCookieModal() {
1179
+ const modal = document.getElementById('invalidCookieModal');
1180
+ modal.style.display = 'none';
1181
+ document.body.classList.remove('modal-open');
1182
+ }
1183
+
1184
+ // 渲染无效Cookie标签
1185
+ function renderInvalidCookieTags(cookies) {
1186
+ const container = document.getElementById('invalidCookieTagsContainer');
1187
+ container.innerHTML = '';
1188
+
1189
+ if (cookies.length === 0) {
1190
+ container.innerHTML = '<div style="padding: 10px; color: #666;">暂无无效Cookie</div>';
1191
+ return;
1192
+ }
1193
+
1194
+ cookies.forEach((cookie, index) => {
1195
+ // 创建标签
1196
+ const tag = document.createElement('span');
1197
+ tag.className = 'cookie-tag';
1198
+
1199
+ // 对短文本添加特殊类
1200
+ if (cookie.length < 5) {
1201
+ tag.classList.add('short-cookie');
1202
+ }
1203
+
1204
+ // 截断Cookie显示
1205
+ const displayText = cookie.length > 20 ?
1206
+ cookie.substring(0, 8) + '...' + cookie.substring(cookie.length - 8) :
1207
+ cookie;
1208
+
1209
+ tag.title = cookie; // 完整Cookie作为工具提示
1210
+
1211
+ // 修改样式,使用与API Key相同的删除按钮样式
1212
+ tag.innerHTML = `
1213
+ <span class="cookie-text-content">${displayText}</span>
1214
+ <div class="cookie-buttons">
1215
+ <button type="button" class="copy-btn" data-cookie="${cookie}" aria-label="复制">C</button>
1216
+ <button type="button" class="delete-cookie" data-index="${index}" aria-label="删除">×</button>
1217
+ </div>
1218
+ `;
1219
+ container.appendChild(tag);
1220
+ });
1221
+
1222
+ // 添加删除按钮的事件监听
1223
+ document.querySelectorAll('#invalidCookieTagsContainer .delete-cookie').forEach(btn => {
1224
+ btn.addEventListener('click', function() {
1225
+ const index = parseInt(this.getAttribute('data-index'));
1226
+ deleteInvalidCookieTag(index);
1227
+ });
1228
+ });
1229
+
1230
+ // 添加复制按钮的事件监听
1231
+ document.querySelectorAll('#invalidCookieTagsContainer .copy-btn').forEach(btn => {
1232
+ btn.addEventListener('click', function() {
1233
+ const cookie = this.getAttribute('data-cookie');
1234
+ handleCopyCookie(cookie);
1235
+ });
1236
+ });
1237
+ }
1238
+
1239
+ // 删除无效Cookie标签
1240
+ function deleteInvalidCookieTag(index) {
1241
+ // 从隐藏的textarea中获取当前的cookies
1242
+ const cookieValuesElem = document.getElementById('invalidCookiesValues');
1243
+ let cookies = cookieValuesElem.value.split(',').map(c => c.trim()).filter(c => c);
1244
+
1245
+ // 删除指定索引的cookie
1246
+ cookies.splice(index, 1);
1247
+
1248
+ // 更新隐藏的textarea
1249
+ cookieValuesElem.value = cookies.join(',');
1250
+
1251
+ // 重新渲染标签
1252
+ renderInvalidCookieTags(cookies);
1253
+ }
1254
+
1255
+ // 处理添加新无效Cookie
1256
+ function handleAddInvalidCookie() {
1257
+ const newCookieInput = document.getElementById('newInvalidCookie');
1258
+ const newCookie = newCookieInput.value.trim();
1259
+
1260
+ if (!newCookie) {
1261
+ return;
1262
+ }
1263
+
1264
+ // 获取当前的cookies
1265
+ const cookieValuesElem = document.getElementById('invalidCookiesValues');
1266
+ let cookies = cookieValuesElem.value ?
1267
+ cookieValuesElem.value.split(',').map(c => c.trim()).filter(c => c) :
1268
+ [];
1269
+
1270
+ // 添加新cookie
1271
+ cookies.push(newCookie);
1272
+
1273
+ // 更新隐藏的textarea
1274
+ cookieValuesElem.value = cookies.join(',');
1275
+
1276
+ // 重新渲染标签
1277
+ renderInvalidCookieTags(cookies);
1278
+
1279
+ // 清空输入框
1280
+ newCookieInput.value = '';
1281
+ }
1282
+
1283
+ // 处理无效Cookie编辑表单提交
1284
+ async function handleInvalidCookieForm(e) {
1285
+ e.preventDefault();
1286
+
1287
+ const cookieValuesText = document.getElementById('invalidCookiesValues').value.trim();
1288
+
1289
+ // 将逗号分隔的 Cookie 值转换为数组
1290
+ const invalidCookies = cookieValuesText ?
1291
+ cookieValuesText.split(',').map(cookie => cookie.trim()).filter(cookie => cookie) :
1292
+ [];
1293
+
1294
+ try {
1295
+ // 先清除所有无效Cookie
1296
+ await clearAllInvalidCookies();
1297
+
1298
+ // 如果有新的无效Cookie,逐个添加
1299
+ if (invalidCookies.length > 0) {
1300
+ // 假设API提供了批量添加无效Cookie的接口
1301
+ const response = await fetch('/v1/invalid-cookies', {
1302
+ method: 'POST',
1303
+ headers: {
1304
+ 'Content-Type': 'application/json',
1305
+ },
1306
+ body: JSON.stringify({
1307
+ invalidCookies,
1308
+ }),
1309
+ });
1310
+
1311
+ const data = await response.json();
1312
+
1313
+ if (data.success) {
1314
+ document.getElementById('invalidCookieModalMessage').innerHTML = `
1315
+ <div class="info">无效Cookie修改成功</div>
1316
+ `;
1317
+ setTimeout(() => {
1318
+ closeInvalidCookieModal();
1319
+ renderInvalidCookies(); // 重新渲染列表
1320
+ }, 1500);
1321
+ } else {
1322
+ document.getElementById('invalidCookieModalMessage').innerHTML = `
1323
+ <div class="error">无效Cookie修改失败: ${data.error}</div>
1324
+ `;
1325
+ }
1326
+ } else {
1327
+ // 如果清空了所有无效Cookie
1328
+ document.getElementById('invalidCookieModalMessage').innerHTML = `
1329
+ <div class="info">已清空所有无效Cookie</div>
1330
+ `;
1331
+ setTimeout(() => {
1332
+ closeInvalidCookieModal();
1333
+ renderInvalidCookies(); // 重新渲染列表
1334
+ }, 1500);
1335
+ }
1336
+ } catch (error) {
1337
+ console.error('修改无效Cookie失败:', error);
1338
+ document.getElementById('invalidCookieModalMessage').innerHTML = `
1339
+ <div class="error">修改无效Cookie失败: ${error.message}</div>
1340
+ `;
1341
+ }
1342
+ }
src/public/styles.css ADDED
@@ -0,0 +1,1270 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* iOS风格现代化样式 */
2
+ :root {
3
+ --ios-background: #f2f2f7;
4
+ --ios-card-background: #ffffff;
5
+ --ios-blue: #007aff;
6
+ --ios-green: #34c759;
7
+ --ios-red: #ff3b30;
8
+ --ios-yellow: #ffcc00;
9
+ --ios-gray: #8e8e93;
10
+ --ios-light-gray: #d1d1d6;
11
+ --ios-text-primary: #000000;
12
+ --ios-text-secondary: #6c6c70;
13
+ --ios-border-radius: 10px;
14
+ --ios-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
15
+ --ios-padding: 16px;
16
+ }
17
+
18
+ /* 夜间模式变量 */
19
+ [data-theme="dark"] {
20
+ --ios-background: #000000;
21
+ --ios-card-background: #1c1c1e;
22
+ --ios-blue: #0a84ff;
23
+ --ios-green: #30d158;
24
+ --ios-red: #ff453a;
25
+ --ios-yellow: #ffd60a;
26
+ --ios-gray: #8e8e93;
27
+ --ios-light-gray: #38383a;
28
+ --ios-text-primary: #ffffff;
29
+ --ios-text-secondary: #adadb5;
30
+ --ios-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
31
+ }
32
+
33
+ /* 防止初始加载时主题切换闪烁 */
34
+ .no-transition * {
35
+ transition: none !important;
36
+ }
37
+
38
+ /* 主题切换时的平滑过渡效果 */
39
+ .theme-transition {
40
+ transition: background-color 0.6s cubic-bezier(0.25, 1, 0.5, 1),
41
+ color 0.6s cubic-bezier(0.25, 1, 0.5, 1),
42
+ border-color 0.6s cubic-bezier(0.25, 1, 0.5, 1),
43
+ box-shadow 0.6s cubic-bezier(0.25, 1, 0.5, 1);
44
+ }
45
+
46
+ .theme-transition * {
47
+ transition: background-color 0.6s cubic-bezier(0.25, 1, 0.5, 1),
48
+ color 0.6s cubic-bezier(0.25, 1, 0.5, 1),
49
+ border-color 0.6s cubic-bezier(0.25, 1, 0.5, 1),
50
+ box-shadow 0.6s cubic-bezier(0.25, 1, 0.5, 1),
51
+ transform 0.3s cubic-bezier(0.25, 1, 0.5, 1),
52
+ opacity 0.3s cubic-bezier(0.25, 1, 0.5, 1);
53
+ }
54
+
55
+ /* 主题切换按钮样式 */
56
+ .theme-switch {
57
+ position: fixed;
58
+ bottom: 20px;
59
+ right: 20px;
60
+ background: var(--ios-card-background);
61
+ border-radius: 50%;
62
+ width: 50px;
63
+ height: 50px;
64
+ display: flex;
65
+ align-items: center;
66
+ justify-content: center;
67
+ cursor: pointer;
68
+ box-shadow: var(--ios-shadow);
69
+ z-index: 1000;
70
+ border: 1px solid rgba(142, 142, 147, 0.1);
71
+ transition: all 0.3s cubic-bezier(0.25, 1, 0.5, 1);
72
+ overflow: hidden;
73
+ }
74
+
75
+ /* 主题切换按钮动画 */
76
+ .theme-switch-animate {
77
+ transform: rotate(180deg) scale(1.1);
78
+ box-shadow: 0 0 20px rgba(var(--ios-blue-rgb, 0, 122, 255), 0.5);
79
+ }
80
+
81
+ /* 主题图标动画 */
82
+ .theme-icon {
83
+ width: 24px;
84
+ height: 24px;
85
+ color: var(--ios-text-primary);
86
+ transition: transform 0.5s cubic-bezier(0.25, 1, 0.5, 1),
87
+ opacity 0.3s cubic-bezier(0.25, 1, 0.5, 1);
88
+ }
89
+
90
+ .theme-icon.dark-mode {
91
+ transform: rotate(360deg) scale(1.1);
92
+ }
93
+
94
+ .theme-switch:hover {
95
+ transform: scale(1.05);
96
+ box-shadow: 0 0 15px rgba(var(--ios-blue-rgb), 0.3);
97
+ }
98
+
99
+ .theme-switch:active {
100
+ transform: scale(0.95);
101
+ }
102
+
103
+ /* 提取颜色的RGB值用于动画和透明度 */
104
+ :root {
105
+ --ios-blue-rgb: 0, 122, 255;
106
+ --ios-green-rgb: 52, 199, 89;
107
+ --ios-red-rgb: 255, 59, 48;
108
+ --ios-yellow-rgb: 255, 204, 0;
109
+ }
110
+
111
+ [data-theme="dark"] {
112
+ --ios-blue-rgb: 10, 132, 255;
113
+ --ios-green-rgb: 48, 209, 88;
114
+ --ios-red-rgb: 255, 69, 58;
115
+ --ios-yellow-rgb: 255, 214, 10;
116
+ }
117
+
118
+ /* 适配夜间模式的样式调整 */
119
+ [data-theme="dark"] .header-card {
120
+ background: linear-gradient(-45deg, #30d158, #0a84ff, #5e5ce6, #64d2ff);
121
+ background-size: 300% 300%;
122
+ animation: gradientAnimation 12s ease infinite;
123
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
124
+ border: none;
125
+ }
126
+
127
+ [data-theme="dark"] .header-card::after {
128
+ background: linear-gradient(120deg, rgba(255,255,255,0.2) 0%, rgba(255,255,255,0) 60%);
129
+ }
130
+
131
+ [data-theme="dark"] .header-card,
132
+ [data-theme="dark"] .header-card h1,
133
+ [data-theme="dark"] .header-card p {
134
+ color: white;
135
+ }
136
+
137
+ [data-theme="dark"] .cookie-tag {
138
+ background-color: rgba(10, 132, 255, 0.15);
139
+ border: 1px solid rgba(10, 132, 255, 0.2);
140
+ }
141
+
142
+ [data-theme="dark"] .cookie-tag:hover {
143
+ background-color: rgba(10, 132, 255, 0.25);
144
+ }
145
+
146
+ [data-theme="dark"] .delete-cookie,
147
+ [data-theme="dark"] .delete-add-cookie {
148
+ background-color: rgba(255, 69, 58, 0.2);
149
+ }
150
+
151
+ [data-theme="dark"] .delete-cookie:hover,
152
+ [data-theme="dark"] .delete-add-cookie:hover {
153
+ background-color: rgba(255, 69, 58, 0.3);
154
+ }
155
+
156
+ [data-theme="dark"] .copy-btn {
157
+ background: rgba(142, 142, 147, 0.2);
158
+ }
159
+
160
+ [data-theme="dark"] .copy-btn:hover {
161
+ background-color: rgba(10, 132, 255, 0.2);
162
+ }
163
+
164
+ [data-theme="dark"] .sticky-actions {
165
+ background: rgba(28, 28, 30, 0.8);
166
+ }
167
+
168
+ [data-theme="dark"] .modal-content {
169
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
170
+ }
171
+
172
+ [data-theme="dark"] input,
173
+ [data-theme="dark"] textarea,
174
+ [data-theme="dark"] select {
175
+ background-color: rgba(142, 142, 147, 0.12);
176
+ }
177
+
178
+ [data-theme="dark"] input:focus,
179
+ [data-theme="dark"] textarea:focus,
180
+ [data-theme="dark"] select:focus {
181
+ background-color: rgba(142, 142, 147, 0.18);
182
+ box-shadow: 0 0 0 3px rgba(10, 132, 255, 0.25);
183
+ }
184
+
185
+ /* 主题切换动画 */
186
+ body {
187
+ transition: background-color 0.3s ease;
188
+ }
189
+
190
+ .card, input, textarea, select, button, th, td, .modal-content, .cookie-tag, .sticky-actions {
191
+ transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease;
192
+ }
193
+
194
+ * {
195
+ box-sizing: border-box;
196
+ -webkit-font-smoothing: antialiased;
197
+ -moz-osx-font-smoothing: grayscale;
198
+ }
199
+
200
+ html {
201
+ scroll-behavior: smooth;
202
+ height: -webkit-fill-available;
203
+ }
204
+
205
+ body {
206
+ font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'SF Pro Display', 'Helvetica Neue', Arial, sans-serif;
207
+ line-height: 1.5;
208
+ margin: 0;
209
+ padding: 16px;
210
+ color: var(--ios-text-primary);
211
+ max-width: 960px;
212
+ margin: 0 auto;
213
+ background-color: var(--ios-background);
214
+ font-size: 16px;
215
+ font-weight: 400;
216
+ }
217
+
218
+ h1, h2 {
219
+ color: var(--ios-text-primary);
220
+ margin-top: 0;
221
+ font-weight: 600;
222
+ letter-spacing: -0.5px;
223
+ }
224
+
225
+ h1 {
226
+ font-size: 28px;
227
+ margin-bottom: 8px;
228
+ }
229
+
230
+ h2 {
231
+ font-size: 22px;
232
+ margin-bottom: 16px;
233
+ }
234
+
235
+ .container {
236
+ display: flex;
237
+ flex-direction: column;
238
+ gap: 16px;
239
+ }
240
+
241
+ .card {
242
+ background: var(--ios-card-background);
243
+ border-radius: var(--ios-border-radius);
244
+ box-shadow: var(--ios-shadow);
245
+ padding: var(--ios-padding);
246
+ transition: transform 0.2s, box-shadow 0.2s;
247
+ border: 1px solid rgba(0, 0, 0, 0.04);
248
+ overflow: hidden;
249
+ }
250
+
251
+ .card:hover {
252
+ transform: translateY(-1px);
253
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);
254
+ }
255
+
256
+ .form-group {
257
+ margin-bottom: 20px;
258
+ }
259
+
260
+ label {
261
+ display: block;
262
+ margin-bottom: 8px;
263
+ font-weight: 500;
264
+ color: var(--ios-text-primary);
265
+ font-size: 15px;
266
+ }
267
+
268
+ input, textarea, select {
269
+ width: 100%;
270
+ padding: 12px 14px;
271
+ border: 1px solid var(--ios-light-gray);
272
+ border-radius: 8px;
273
+ font-size: 16px;
274
+ transition: all 0.2s;
275
+ box-sizing: border-box;
276
+ background-color: rgba(142, 142, 147, 0.06);
277
+ color: var(--ios-text-primary);
278
+ appearance: none;
279
+ font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Helvetica Neue', Arial, sans-serif;
280
+ }
281
+
282
+ input:focus, textarea:focus, select:focus {
283
+ border-color: var(--ios-blue);
284
+ outline: none;
285
+ box-shadow: 0 0 0 3px rgba(0, 122, 255, 0.15);
286
+ background-color: var(--ios-card-background);
287
+ }
288
+
289
+ textarea {
290
+ min-height: 100px;
291
+ font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Helvetica Neue', Arial, sans-serif;
292
+ line-height: 1.5;
293
+ }
294
+
295
+ button {
296
+ background: var(--ios-blue);
297
+ color: white;
298
+ border: none;
299
+ padding: 12px 18px;
300
+ border-radius: 8px;
301
+ cursor: pointer;
302
+ font-size: 16px;
303
+ transition: all 0.2s;
304
+ font-weight: 500;
305
+ -webkit-tap-highlight-color: transparent;
306
+ text-align: center;
307
+ display: inline-flex;
308
+ align-items: center;
309
+ justify-content: center;
310
+ }
311
+
312
+ button:hover {
313
+ background: #0062cc;
314
+ transform: translateY(-1px);
315
+ }
316
+
317
+ button:active {
318
+ transform: translateY(1px);
319
+ opacity: 0.9;
320
+ }
321
+
322
+ table {
323
+ width: 100%;
324
+ border-collapse: separate;
325
+ border-spacing: 0;
326
+ margin-bottom: 16px;
327
+ border-radius: 8px;
328
+ overflow: hidden;
329
+ border: 1px solid var(--ios-light-gray);
330
+ }
331
+
332
+ th, td {
333
+ padding: 14px 16px;
334
+ text-align: left;
335
+ border-bottom: 1px solid rgba(0, 0, 0, 0.05);
336
+ }
337
+
338
+ th {
339
+ background-color: rgba(142, 142, 147, 0.06);
340
+ font-weight: 600;
341
+ color: var(--ios-text-primary);
342
+ font-size: 14px;
343
+ text-transform: uppercase;
344
+ letter-spacing: 0.5px;
345
+ }
346
+
347
+ tr:last-child td {
348
+ border-bottom: none;
349
+ }
350
+
351
+ tr:hover {
352
+ background-color: rgba(0, 122, 255, 0.03);
353
+ }
354
+
355
+ .action-btn {
356
+ background: var(--ios-red);
357
+ margin-right: 8px;
358
+ font-size: 14px;
359
+ padding: 8px 12px;
360
+ }
361
+
362
+ .action-btn:hover {
363
+ background: #e02e24;
364
+ }
365
+
366
+ .edit-btn {
367
+ background: var(--ios-blue);
368
+ margin-right: 8px;
369
+ font-size: 14px;
370
+ padding: 8px 12px;
371
+ }
372
+
373
+ .edit-btn:hover {
374
+ background: #0062cc;
375
+ }
376
+
377
+ .info {
378
+ background-color: rgba(52, 199, 89, 0.1);
379
+ color: var(--ios-text-primary);
380
+ padding: 14px;
381
+ border-radius: 8px;
382
+ margin-bottom: 16px;
383
+ border-left: 3px solid var(--ios-green);
384
+ font-size: 15px;
385
+ }
386
+
387
+ .error {
388
+ background-color: rgba(255, 59, 48, 0.1);
389
+ color: var(--ios-text-primary);
390
+ padding: 14px;
391
+ border-radius: 8px;
392
+ margin-bottom: 16px;
393
+ border-left: 3px solid var(--ios-red);
394
+ font-size: 15px;
395
+ }
396
+
397
+ .modal {
398
+ display: none;
399
+ position: fixed;
400
+ z-index: 1000;
401
+ left: 0;
402
+ top: 0;
403
+ width: 100%;
404
+ height: 100%;
405
+ overflow: auto;
406
+ background-color: rgba(0, 0, 0, 0.4);
407
+ backdrop-filter: blur(5px);
408
+ -webkit-backdrop-filter: blur(5px);
409
+ }
410
+
411
+ .modal-content {
412
+ background-color: var(--ios-card-background);
413
+ margin: 10% auto;
414
+ padding: 24px;
415
+ border: none;
416
+ width: 85%;
417
+ max-width: 500px;
418
+ border-radius: 14px;
419
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
420
+ animation: modalFadeIn 0.3s;
421
+ }
422
+
423
+ @keyframes modalFadeIn {
424
+ from { opacity: 0; transform: translateY(-20px); }
425
+ to { opacity: 1; transform: translateY(0); }
426
+ }
427
+
428
+ .close {
429
+ color: var(--ios-gray);
430
+ float: right;
431
+ font-size: 24px;
432
+ font-weight: 300;
433
+ cursor: pointer;
434
+ margin-top: -5px;
435
+ transition: all 0.2s;
436
+ }
437
+
438
+ .close:hover,
439
+ .close:focus {
440
+ color: var(--ios-text-primary);
441
+ text-decoration: none;
442
+ }
443
+
444
+ .cookie-text {
445
+ word-break: break-all;
446
+ font-size: 14px;
447
+ font-family: monospace;
448
+ }
449
+
450
+ .cookie-tag {
451
+ display: inline-flex;
452
+ align-items: center;
453
+ justify-content: space-between;
454
+ background-color: rgba(0, 122, 255, 0.08);
455
+ padding: 8px 12px;
456
+ border-radius: 8px;
457
+ margin: 0 0 8px 0;
458
+ width: 100%;
459
+ position: relative;
460
+ transition: all 0.2s;
461
+ border: 1px solid rgba(0, 122, 255, 0.12);
462
+ }
463
+
464
+ .cookie-tag:hover {
465
+ background-color: rgba(0, 122, 255, 0.12);
466
+ }
467
+
468
+ .delete-cookie, .delete-add-cookie {
469
+ background-color: rgba(255, 59, 48, 0.1);
470
+ color: var(--ios-red);
471
+ border: none;
472
+ width: 22px;
473
+ height: 22px;
474
+ border-radius: 50%;
475
+ cursor: pointer;
476
+ display: inline-flex;
477
+ align-items: center;
478
+ justify-content: center;
479
+ font-size: 12px;
480
+ margin-left: 8px;
481
+ padding: 0;
482
+ line-height: 1;
483
+ transition: all 0.2s;
484
+ -webkit-tap-highlight-color: transparent;
485
+ }
486
+
487
+ .delete-cookie:hover, .delete-add-cookie:hover {
488
+ background-color: rgba(255, 59, 48, 0.2);
489
+ transform: scale(1.05);
490
+ }
491
+
492
+ .delete-cookie:active, .delete-add-cookie:active {
493
+ transform: scale(0.95);
494
+ opacity: 0.9;
495
+ }
496
+
497
+ .cookie-text-content {
498
+ max-width: calc(100% - 70px);
499
+ overflow: hidden;
500
+ text-overflow: ellipsis;
501
+ white-space: nowrap;
502
+ color: var(--ios-text-primary);
503
+ }
504
+
505
+ .cookies-container {
506
+ display: flex;
507
+ flex-wrap: wrap;
508
+ margin-bottom: 15px;
509
+ padding: 5px 0;
510
+ }
511
+
512
+ .add-cookie-btn {
513
+ padding: 10px 14px;
514
+ background-color: var(--ios-blue);
515
+ transition: all 0.2s ease;
516
+ }
517
+
518
+ .add-cookie-btn:hover {
519
+ background-color: #0062cc;
520
+ }
521
+
522
+ progress {
523
+ width: 100%;
524
+ height: 8px;
525
+ border-radius: 4px;
526
+ overflow: hidden;
527
+ }
528
+
529
+ progress::-webkit-progress-bar {
530
+ background-color: rgba(142, 142, 147, 0.2);
531
+ border-radius: 4px;
532
+ }
533
+
534
+ progress::-webkit-progress-value {
535
+ background-color: var(--ios-blue);
536
+ border-radius: 4px;
537
+ transition: width 0.3s ease;
538
+ }
539
+
540
+ /* 梦幻动态渐变背景动画 */
541
+ @keyframes gradientAnimation {
542
+ 0% {
543
+ background-position: 0% 50%;
544
+ }
545
+ 50% {
546
+ background-position: 100% 50%;
547
+ }
548
+ 100% {
549
+ background-position: 0% 50%;
550
+ }
551
+ }
552
+
553
+ .header-card {
554
+ background: linear-gradient(-45deg, #4CD964, #5ac8fa, #34C759, #5ac8fa);
555
+ background-size: 300% 300%;
556
+ animation: gradientAnimation 12s ease infinite;
557
+ color: var(--ios-text-primary);
558
+ position: relative;
559
+ overflow: hidden;
560
+ padding: 20px;
561
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
562
+ border: none;
563
+ }
564
+
565
+ .header-card::after {
566
+ content: "";
567
+ position: absolute;
568
+ top: 0;
569
+ left: 0;
570
+ right: 0;
571
+ bottom: 0;
572
+ background: linear-gradient(120deg, rgba(255,255,255,0.4) 0%, rgba(255,255,255,0) 60%);
573
+ pointer-events: none;
574
+ }
575
+
576
+ .header-card h1, .header-card p {
577
+ color: var(--ios-text-primary);
578
+ }
579
+
580
+ .sticky-actions {
581
+ position: sticky;
582
+ bottom: 20px;
583
+ background: rgba(255, 255, 255, 0.9);
584
+ backdrop-filter: blur(10px);
585
+ -webkit-backdrop-filter: blur(10px);
586
+ padding: 15px;
587
+ border-radius: 12px;
588
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.08);
589
+ margin-top: 20px;
590
+ z-index: 100;
591
+ display: flex;
592
+ justify-content: flex-end;
593
+ gap: 10px;
594
+ }
595
+
596
+ .cookie-tag.short-cookie {
597
+ max-width: 180px;
598
+ }
599
+
600
+ .copy-btn {
601
+ background: rgba(142, 142, 147, 0.1);
602
+ color: var(--ios-blue);
603
+ border: none;
604
+ width: 28px;
605
+ height: 28px;
606
+ border-radius: 50%;
607
+ cursor: pointer;
608
+ display: inline-flex;
609
+ align-items: center;
610
+ justify-content: center;
611
+ font-size: 14px;
612
+ margin-left: 6px;
613
+ padding: 0;
614
+ transition: all 0.2s;
615
+ -webkit-tap-highlight-color: transparent;
616
+ }
617
+
618
+ .copy-btn:hover {
619
+ background-color: rgba(0, 122, 255, 0.1);
620
+ transform: scale(1.05);
621
+ }
622
+
623
+ .copy-btn:active {
624
+ transform: scale(0.95);
625
+ opacity: 0.9;
626
+ }
627
+
628
+ .delete-cookie, .delete-add-cookie {
629
+ background-color: rgba(255, 59, 48, 0.1);
630
+ color: var(--ios-red);
631
+ border: none;
632
+ width: 22px;
633
+ height: 22px;
634
+ border-radius: 50%;
635
+ cursor: pointer;
636
+ display: inline-flex;
637
+ align-items: center;
638
+ justify-content: center;
639
+ font-size: 12px;
640
+ margin-left: 8px;
641
+ padding: 0;
642
+ line-height: 1;
643
+ transition: all 0.2s;
644
+ -webkit-tap-highlight-color: transparent;
645
+ }
646
+
647
+ .delete-cookie:hover, .delete-add-cookie:hover {
648
+ background-color: rgba(255, 59, 48, 0.2);
649
+ transform: scale(1.05);
650
+ }
651
+
652
+ .delete-cookie:active, .delete-add-cookie:active {
653
+ transform: scale(0.95);
654
+ opacity: 0.9;
655
+ }
656
+
657
+ .cookie-buttons {
658
+ display: flex;
659
+ align-items: center;
660
+ flex-shrink: 0;
661
+ margin-left: auto;
662
+ }
663
+
664
+ /* 适配移动端的样式 */
665
+ @media screen and (max-width: 768px) {
666
+ body {
667
+ padding: 12px;
668
+ font-size: 15px;
669
+ }
670
+
671
+ h1 {
672
+ font-size: 24px;
673
+ }
674
+
675
+ h2 {
676
+ font-size: 20px;
677
+ }
678
+
679
+ .card {
680
+ padding: 16px;
681
+ }
682
+
683
+ input, textarea, select {
684
+ padding: 10px 12px;
685
+ font-size: 16px;
686
+ }
687
+
688
+ button {
689
+ padding: 12px 16px;
690
+ width: 100%;
691
+ margin-bottom: 8px;
692
+ }
693
+
694
+ /* 优化添加按钮在移动端的宽度 */
695
+ .add-cookie-btn {
696
+ width: auto;
697
+ min-width: 42px;
698
+ max-width: 42px;
699
+ padding: 10px;
700
+ margin-bottom: 0;
701
+ flex: 0 0 auto;
702
+ }
703
+
704
+ .add-cookie-btn i {
705
+ margin: 0;
706
+ }
707
+
708
+ /* 让输入框占据更多宽度 */
709
+ #addNewCookie,
710
+ #newCookie,
711
+ #newInvalidCookie {
712
+ flex: 1;
713
+ }
714
+
715
+ /* 优化表单布局 */
716
+ div[style*="display: flex; gap: 10px;"] {
717
+ gap: 8px !important;
718
+ }
719
+
720
+ /* 优化Cookie标签在移动端的显示 */
721
+ .cookie-tag {
722
+ font-size: 13px;
723
+ padding: 6px 10px;
724
+ margin: 0 0 8px 0;
725
+ width: 100%;
726
+ }
727
+
728
+ /* 让Cookie内容区域尽可能宽 */
729
+ .cookie-text-content {
730
+ max-width: calc(100% - 68px);
731
+ }
732
+
733
+ /* 优化复制与删除按钮 */
734
+ .cookie-buttons {
735
+ display: flex;
736
+ align-items: center;
737
+ gap: 4px;
738
+ flex-shrink: 0;
739
+ }
740
+
741
+ .copy-btn,
742
+ .delete-cookie,
743
+ .delete-add-cookie {
744
+ width: 28px;
745
+ height: 28px;
746
+ margin-left: 4px;
747
+ background-color: rgba(142, 142, 147, 0.15);
748
+ transition: all 0.15s ease;
749
+ }
750
+
751
+ .copy-btn:active,
752
+ .delete-cookie:active,
753
+ .delete-add-cookie:active {
754
+ transform: scale(0.92);
755
+ opacity: 0.8;
756
+ }
757
+
758
+ .cookie-tag.short-cookie {
759
+ max-width: calc(100% - 16px);
760
+ }
761
+
762
+ th, td {
763
+ padding: 12px;
764
+ font-size: 14px;
765
+ }
766
+
767
+ .modal-content {
768
+ width: 92%;
769
+ padding: 20px;
770
+ margin: 15% auto 5%;
771
+ }
772
+
773
+ /* 在小屏幕上重新排列表格 */
774
+ table, thead, tbody, th, td, tr {
775
+ display: block;
776
+ }
777
+
778
+ thead tr {
779
+ position: absolute;
780
+ top: -9999px;
781
+ left: -9999px;
782
+ }
783
+
784
+ tr {
785
+ border: 1px solid rgba(0, 0, 0, 0.05);
786
+ margin-bottom: 10px;
787
+ border-radius: 8px;
788
+ overflow: hidden;
789
+ }
790
+
791
+ td {
792
+ border: none;
793
+ border-bottom: 1px solid rgba(0, 0, 0, 0.05);
794
+ position: relative;
795
+ padding-left: 50%;
796
+ white-space: normal;
797
+ text-align: left;
798
+ }
799
+
800
+ td:before {
801
+ position: absolute;
802
+ top: 12px;
803
+ left: 12px;
804
+ width: 45%;
805
+ padding-right: 10px;
806
+ white-space: nowrap;
807
+ text-align: left;
808
+ font-weight: 600;
809
+ content: attr(data-title);
810
+ color: var(--ios-text-secondary);
811
+ font-size: 13px;
812
+ }
813
+
814
+ /* 设置每个单元格的标题 */
815
+ #keyTable td:nth-of-type(1):before { content: "API Key"; }
816
+ #keyTable td:nth-of-type(2):before { content: "Cookie 数量"; }
817
+ #keyTable td:nth-of-type(3):before { content: "操作"; }
818
+
819
+ .action-btn, .edit-btn {
820
+ margin-right: 8px;
821
+ font-size: 14px;
822
+ padding: 8px 12px;
823
+ width: auto;
824
+ display: inline-flex;
825
+ }
826
+
827
+ .form-input-group {
828
+ display: flex;
829
+ flex-direction: column;
830
+ gap: 8px;
831
+ }
832
+
833
+ .form-input-group input {
834
+ width: 100%;
835
+ }
836
+
837
+ .form-input-group button {
838
+ width: 100%;
839
+ }
840
+
841
+ .header-card div[style*="display: flex; justify-content: space-between;"] {
842
+ flex-direction: column;
843
+ gap: 10px;
844
+ }
845
+
846
+ .header-card div[style*="display: flex; justify-content: space-between;"] div {
847
+ width: 100%;
848
+ display: flex;
849
+ flex-wrap: wrap;
850
+ gap: 8px;
851
+ }
852
+
853
+ .header-card div[style*="display: flex; justify-content: space-between;"] button {
854
+ flex: 1;
855
+ min-width: 0;
856
+ }
857
+
858
+ #adminUsername {
859
+ display: block;
860
+ width: 100%;
861
+ margin-bottom: 8px;
862
+ text-align: center;
863
+ }
864
+ }
865
+
866
+ /* 为header-card内的按钮添加特殊样式 */
867
+ .header-card button {
868
+ background-color: rgba(255, 255, 255, 0.2);
869
+ backdrop-filter: blur(5px);
870
+ -webkit-backdrop-filter: blur(5px);
871
+ color: var(--ios-text-primary);
872
+ border: 1px solid rgba(255, 255, 255, 0.3);
873
+ font-weight: 500;
874
+ transition: all 0.3s ease;
875
+ }
876
+
877
+ .header-card button:hover {
878
+ background-color: rgba(255, 255, 255, 0.25);
879
+ transform: translateY(-2px);
880
+ }
881
+
882
+ .header-card button.danger {
883
+ background-color: #ff3b30;
884
+ color: white;
885
+ border: none;
886
+ }
887
+
888
+ .header-card button.danger:hover {
889
+ background-color: #ff2d20;
890
+ }
891
+
892
+ [data-theme="dark"] .header-card button {
893
+ background-color: rgba(255, 255, 255, 0.15);
894
+ color: white;
895
+ border: 1px solid rgba(255, 255, 255, 0.2);
896
+ }
897
+
898
+ [data-theme="dark"] .header-card button:hover {
899
+ background-color: rgba(255, 255, 255, 0.2);
900
+ }
901
+
902
+ /* 用户信息样式 */
903
+ .header-card .user-info {
904
+ display: flex;
905
+ align-items: center;
906
+ margin: 15px 0;
907
+ }
908
+
909
+ .header-card .user-info i {
910
+ margin-right: 8px;
911
+ opacity: 0.9;
912
+ }
913
+
914
+ /* 退出按钮特殊样式 */
915
+ .header-card .logout-btn {
916
+ background-color: rgba(255, 59, 48, 0.9);
917
+ color: white;
918
+ border: none;
919
+ width: 100%;
920
+ padding: 12px;
921
+ margin-top: 10px;
922
+ font-weight: 500;
923
+ transition: all 0.3s ease;
924
+ }
925
+
926
+ .header-card .logout-btn:hover {
927
+ background-color: rgba(255, 59, 48, 1);
928
+ transform: translateY(-2px);
929
+ }
930
+
931
+ [data-theme="dark"] .header-card .logout-btn {
932
+ background-color: rgba(255, 69, 58, 0.8);
933
+ }
934
+
935
+ [data-theme="dark"] .header-card .logout-btn:hover {
936
+ background-color: rgba(255, 69, 58, 0.9);
937
+ }
938
+
939
+ /* 按钮组样式 */
940
+ .header-card .button-group {
941
+ display: flex;
942
+ gap: 10px;
943
+ margin-bottom: 15px;
944
+ }
945
+
946
+ .header-card .button-group button {
947
+ flex: 1;
948
+ }
949
+
950
+ /* 用户名颜色随主题切换 */
951
+ #adminUsername {
952
+ color: var(--ios-text-primary) !important;
953
+ }
954
+
955
+ /* 优化主题切换动画 - 添加在文件末尾 */
956
+ /* 提取颜色的RGB值用于动画和透明度 */
957
+ :root {
958
+ --ios-blue-rgb: 0, 122, 255;
959
+ --ios-green-rgb: 52, 199, 89;
960
+ --ios-red-rgb: 255, 59, 48;
961
+ --ios-yellow-rgb: 255, 204, 0;
962
+ }
963
+
964
+ [data-theme="dark"] {
965
+ --ios-blue-rgb: 10, 132, 255;
966
+ --ios-green-rgb: 48, 209, 88;
967
+ --ios-red-rgb: 255, 69, 58;
968
+ --ios-yellow-rgb: 255, 214, 10;
969
+ }
970
+
971
+ /* 防止初始加载时主题切换闪烁 */
972
+ .no-transition * {
973
+ transition: none !important;
974
+ }
975
+
976
+ /* 主题切换时的平滑过渡效果 */
977
+ .theme-transition {
978
+ transition: background-color 0.6s cubic-bezier(0.25, 1, 0.5, 1),
979
+ color 0.6s cubic-bezier(0.25, 1, 0.5, 1),
980
+ border-color 0.6s cubic-bezier(0.25, 1, 0.5, 1),
981
+ box-shadow 0.6s cubic-bezier(0.25, 1, 0.5, 1);
982
+ }
983
+
984
+ .theme-transition * {
985
+ transition: background-color 0.6s cubic-bezier(0.25, 1, 0.5, 1),
986
+ color 0.6s cubic-bezier(0.25, 1, 0.5, 1),
987
+ border-color 0.6s cubic-bezier(0.25, 1, 0.5, 1),
988
+ box-shadow 0.6s cubic-bezier(0.25, 1, 0.5, 1),
989
+ transform 0.3s cubic-bezier(0.25, 1, 0.5, 1),
990
+ opacity 0.3s cubic-bezier(0.25, 1, 0.5, 1);
991
+ }
992
+
993
+ /* 主题切换按钮动画 */
994
+ .theme-switch-animate {
995
+ transform: rotate(180deg) scale(1.1);
996
+ box-shadow: 0 0 20px rgba(var(--ios-blue-rgb), 0.5);
997
+ }
998
+
999
+ /* 主题图标动画 */
1000
+ .theme-icon {
1001
+ width: 24px;
1002
+ height: 24px;
1003
+ color: var(--ios-text-primary);
1004
+ transition: transform 0.5s cubic-bezier(0.25, 1, 0.5, 1),
1005
+ opacity 0.3s cubic-bezier(0.25, 1, 0.5, 1);
1006
+ }
1007
+
1008
+ .theme-icon.dark-mode {
1009
+ transform: rotate(360deg) scale(1.1);
1010
+ }
1011
+
1012
+ .theme-switch:hover {
1013
+ transform: scale(1.05);
1014
+ box-shadow: 0 0 15px rgba(var(--ios-blue-rgb), 0.3);
1015
+ }
1016
+
1017
+ .theme-switch:active {
1018
+ transform: scale(0.95);
1019
+ }
1020
+
1021
+ /* 优化特定元素的过渡效果 */
1022
+ .card {
1023
+ transition: transform 0.3s cubic-bezier(0.25, 1, 0.5, 1),
1024
+ background-color 0.6s cubic-bezier(0.25, 1, 0.5, 1),
1025
+ box-shadow 0.6s cubic-bezier(0.25, 1, 0.5, 1),
1026
+ border-color 0.6s cubic-bezier(0.25, 1, 0.5, 1);
1027
+ }
1028
+
1029
+ button {
1030
+ transition: background-color 0.3s cubic-bezier(0.25, 1, 0.5, 1),
1031
+ transform 0.2s cubic-bezier(0.25, 1, 0.5, 1),
1032
+ box-shadow 0.3s cubic-bezier(0.25, 1, 0.5, 1),
1033
+ opacity 0.2s cubic-bezier(0.25, 1, 0.5, 1);
1034
+ }
1035
+
1036
+ input, textarea, select {
1037
+ transition: background-color 0.6s cubic-bezier(0.25, 1, 0.5, 1),
1038
+ border-color 0.6s cubic-bezier(0.25, 1, 0.5, 1),
1039
+ box-shadow 0.3s cubic-bezier(0.25, 1, 0.5, 1);
1040
+ }
1041
+
1042
+ .cookie-tag, .delete-cookie, .delete-add-cookie, .copy-btn {
1043
+ transition: background-color 0.6s cubic-bezier(0.25, 1, 0.5, 1),
1044
+ transform 0.2s cubic-bezier(0.25, 1, 0.5, 1),
1045
+ border-color 0.6s cubic-bezier(0.25, 1, 0.5, 1);
1046
+ }
1047
+
1048
+ /* 添加主题切换时的特殊动画效果 */
1049
+ .theme-transition .card {
1050
+ animation: card-theme-shift 0.8s cubic-bezier(0.25, 1, 0.5, 1);
1051
+ }
1052
+
1053
+ @keyframes card-theme-shift {
1054
+ 0% { transform: translateY(0); }
1055
+ 50% { transform: translateY(-5px); }
1056
+ 100% { transform: translateY(0); }
1057
+ }
1058
+
1059
+ /* 确保元素在滚动和动画期间的流畅渲染 */
1060
+ .card, .theme-switch, .modal-content, input, button {
1061
+ backface-visibility: hidden;
1062
+ -webkit-backface-visibility: hidden;
1063
+ transform-style: preserve-3d;
1064
+ -webkit-transform-style: preserve-3d;
1065
+ will-change: transform, opacity;
1066
+ }
1067
+
1068
+ /* 调整主题切换动画效果 */
1069
+ .theme-transition body {
1070
+ animation: bg-fade 0.8s cubic-bezier(0.25, 1, 0.5, 1);
1071
+ }
1072
+
1073
+ @keyframes bg-fade {
1074
+ 0% { opacity: 0.98; }
1075
+ 50% { opacity: 0.95; }
1076
+ 100% { opacity: 1; }
1077
+ }
1078
+
1079
+ /* 侧边导航栏样式 */
1080
+ .side-nav-trigger {
1081
+ position: fixed;
1082
+ right: 20px;
1083
+ top: 50%;
1084
+ transform: translateY(-50%);
1085
+ z-index: 1000;
1086
+ width: 12px;
1087
+ height: 12px;
1088
+ display: flex;
1089
+ align-items: center;
1090
+ justify-content: center;
1091
+ cursor: pointer;
1092
+ transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
1093
+ border-radius: 50%;
1094
+ background-color: rgba(255, 255, 255, 0.1);
1095
+ backdrop-filter: blur(4px);
1096
+ -webkit-backdrop-filter: blur(4px);
1097
+ box-shadow: 0 0 5px rgba(0, 122, 255, 0.2);
1098
+ }
1099
+
1100
+ .trigger-dot {
1101
+ width: 6px;
1102
+ height: 6px;
1103
+ border-radius: 50%;
1104
+ background-color: rgba(88, 166, 255, 0.8);
1105
+ box-shadow: 0 0 4px rgba(88, 166, 255, 0.5);
1106
+ transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
1107
+ }
1108
+
1109
+ .side-nav-trigger:hover {
1110
+ background-color: rgba(255, 255, 255, 0.2);
1111
+ }
1112
+
1113
+ .side-nav-trigger:hover .trigger-dot {
1114
+ background-color: var(--ios-blue);
1115
+ box-shadow: 0 0 8px rgba(0, 122, 255, 0.7);
1116
+ }
1117
+
1118
+ .side-nav-menu {
1119
+ position: fixed;
1120
+ right: 40px;
1121
+ top: 50%;
1122
+ transform: translateY(-50%);
1123
+ z-index: 999;
1124
+ background-color: rgba(255, 255, 255, 0.08);
1125
+ backdrop-filter: blur(25px);
1126
+ -webkit-backdrop-filter: blur(25px);
1127
+ border-radius: 16px;
1128
+ padding: 15px 10px;
1129
+ width: 200px;
1130
+ max-height: 80vh;
1131
+ overflow-y: auto;
1132
+ box-shadow: 0 5px 20px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(255, 255, 255, 0.15);
1133
+ opacity: 0;
1134
+ visibility: hidden;
1135
+ transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
1136
+ }
1137
+
1138
+ .side-nav-trigger:hover + .side-nav-menu,
1139
+ .side-nav-menu:hover {
1140
+ opacity: 1;
1141
+ visibility: visible;
1142
+ }
1143
+
1144
+ .side-nav-content {
1145
+ display: flex;
1146
+ flex-direction: column;
1147
+ gap: 3px;
1148
+ }
1149
+
1150
+ .nav-item {
1151
+ display: flex;
1152
+ align-items: center;
1153
+ padding: 8px 10px;
1154
+ border-radius: 10px;
1155
+ cursor: pointer;
1156
+ transition: all 0.2s ease;
1157
+ }
1158
+
1159
+ .nav-item:hover {
1160
+ background-color: rgba(0, 0, 0, 0.05);
1161
+ }
1162
+
1163
+ .nav-item.active {
1164
+ background-color: rgba(0, 122, 255, 0.1);
1165
+ }
1166
+
1167
+ .nav-item-dot {
1168
+ width: 6px;
1169
+ height: 6px;
1170
+ border-radius: 50%;
1171
+ background-color: rgba(158, 158, 158, 0.8);
1172
+ margin-right: 10px;
1173
+ transition: all 0.2s ease;
1174
+ }
1175
+
1176
+ .nav-item.active .nav-item-dot {
1177
+ background-color: var(--ios-blue);
1178
+ box-shadow: 0 0 5px rgba(0, 122, 255, 0.5);
1179
+ }
1180
+
1181
+ /* 非活动条目使用浅灰色文字 */
1182
+ .nav-item-title {
1183
+ color: var(--ios-gray);
1184
+ font-size: 14px;
1185
+ font-weight: 400;
1186
+ white-space: nowrap;
1187
+ overflow: hidden;
1188
+ text-overflow: ellipsis;
1189
+ letter-spacing: -0.2px;
1190
+ transition: all 0.25s cubic-bezier(0.25, 0.8, 0.25, 1);
1191
+ }
1192
+
1193
+ /* 当前激活条目使用主要文字颜色并加粗 */
1194
+ .nav-item.active .nav-item-title {
1195
+ color: var(--ios-blue);
1196
+ font-weight: 600;
1197
+ }
1198
+
1199
+ /* 鼠标悬停时字体放大加粗 */
1200
+ .nav-item:hover .nav-item-title {
1201
+ transform: scale(1.05);
1202
+ font-weight: 500;
1203
+ color: var(--ios-text-primary);
1204
+ }
1205
+
1206
+ @media (max-width: 768px) {
1207
+ .side-nav-trigger {
1208
+ width: 14px;
1209
+ height: 14px;
1210
+ }
1211
+
1212
+ .trigger-dot {
1213
+ width: 7px;
1214
+ height: 7px;
1215
+ }
1216
+
1217
+ .side-nav-menu {
1218
+ right: 40px;
1219
+ width: 180px;
1220
+ }
1221
+
1222
+ /* 移动端点击显示目录 */
1223
+ .side-nav-trigger.touch-active + .side-nav-menu,
1224
+ .side-nav-menu.touch-active {
1225
+ opacity: 1;
1226
+ visibility: visible;
1227
+ }
1228
+ }
1229
+
1230
+ /* 暗色模式自适应 */
1231
+ @media (prefers-color-scheme: dark) {
1232
+ .side-nav-trigger {
1233
+ background-color: rgba(30, 30, 30, 0.3);
1234
+ }
1235
+
1236
+ .side-nav-trigger:hover {
1237
+ background-color: rgba(30, 30, 30, 0.5);
1238
+ }
1239
+
1240
+ .side-nav-menu {
1241
+ background-color: rgba(30, 30, 30, 0.08);
1242
+ box-shadow: 0 5px 20px rgba(0, 0, 0, 0.2), 0 0 0 1px rgba(255, 255, 255, 0.08);
1243
+ }
1244
+
1245
+ .nav-item:hover {
1246
+ background-color: rgba(255, 255, 255, 0.08);
1247
+ }
1248
+
1249
+ .nav-item.active {
1250
+ background-color: rgba(10, 132, 255, 0.15);
1251
+ }
1252
+ }
1253
+
1254
+ [data-theme="dark"] .nav-item-title {
1255
+ color: var(--ios-gray);
1256
+ }
1257
+
1258
+ [data-theme="dark"] .nav-item.active .nav-item-title {
1259
+ color: var(--ios-blue);
1260
+ }
1261
+
1262
+ [data-theme="dark"] .nav-item:hover .nav-item-title {
1263
+ color: rgba(255, 255, 255, 0.9);
1264
+ }
1265
+
1266
+ input, textarea, select {
1267
+ transition: background-color 0.6s cubic-bezier(0.25, 1, 0.5, 1),
1268
+ border-color 0.6s cubic-bezier(0.25, 1, 0.5, 1),
1269
+ box-shadow 0.3s cubic-bezier(0.25, 1, 0.5, 1);
1270
+ }
src/public/theme.js ADDED
@@ -0,0 +1,290 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // 主题管理系统
2
+
3
+ // 在HTML解析前应用主题,防止初始闪烁
4
+ (function() {
5
+ // 尝试从localStorage读取用户主题偏好
6
+ const savedTheme = localStorage.getItem('userThemePreference');
7
+
8
+ // 如果有保存的主题偏好,立即应用
9
+ if (savedTheme) {
10
+ document.documentElement.setAttribute('data-theme', savedTheme);
11
+ } else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
12
+ // 检查系统主题
13
+ document.documentElement.setAttribute('data-theme', 'dark');
14
+ } else {
15
+ // 检查当前时间
16
+ const currentHour = new Date().getHours();
17
+ if (currentHour >= 19 || currentHour < 7) {
18
+ document.documentElement.setAttribute('data-theme', 'dark');
19
+ }
20
+ }
21
+
22
+ // 添加类以防止过渡效果在页面加载时触发
23
+ document.documentElement.classList.add('no-transition');
24
+ })();
25
+
26
+ document.addEventListener('DOMContentLoaded', () => {
27
+ // 创建主题切换按钮
28
+ createThemeToggle();
29
+
30
+ // 初始化主题
31
+ initTheme();
32
+
33
+ // 监听系统主题变化
34
+ listenForSystemThemeChanges();
35
+
36
+ // 移除阻止过渡效果的类
37
+ setTimeout(() => {
38
+ document.documentElement.classList.remove('no-transition');
39
+ }, 100);
40
+ });
41
+
42
+ // 创建主题切换按钮
43
+ function createThemeToggle() {
44
+ const themeSwitch = document.createElement('div');
45
+ themeSwitch.className = 'theme-switch';
46
+ themeSwitch.setAttribute('title', '切换亮/暗主题');
47
+ themeSwitch.innerHTML = `
48
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="theme-icon">
49
+ <circle cx="12" cy="12" r="5"></circle>
50
+ <line x1="12" y1="1" x2="12" y2="3"></line>
51
+ <line x1="12" y1="21" x2="12" y2="23"></line>
52
+ <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
53
+ <line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
54
+ <line x1="1" y1="12" x2="3" y2="12"></line>
55
+ <line x1="21" y1="12" x2="23" y2="12"></line>
56
+ <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
57
+ <line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
58
+ </svg>
59
+ `;
60
+
61
+ // 更新为当前主题的图标
62
+ const currentTheme = document.documentElement.getAttribute('data-theme');
63
+ updateThemeIcon(currentTheme);
64
+
65
+ // 添加点击事件监听
66
+ themeSwitch.addEventListener('click', toggleTheme);
67
+
68
+ // 添加到页面
69
+ document.body.appendChild(themeSwitch);
70
+ }
71
+
72
+ // 初始化主题
73
+ function initTheme() {
74
+ // 首先检查用户的主题偏好
75
+ const savedTheme = localStorage.getItem('userThemePreference');
76
+ if (savedTheme) {
77
+ applyTheme(savedTheme);
78
+ return;
79
+ }
80
+
81
+ // 如果没有用户偏好,检查系统主题
82
+ if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
83
+ applyTheme('dark');
84
+ return;
85
+ }
86
+
87
+ // 检查当前时间
88
+ const currentHour = new Date().getHours();
89
+ if (currentHour >= 19 || currentHour < 7) {
90
+ applyTheme('dark');
91
+ return;
92
+ }
93
+
94
+ // 如果没有特殊情况,使用亮色主题
95
+ applyTheme('light');
96
+ }
97
+
98
+ // 应用主题
99
+ function applyTheme(theme) {
100
+ document.documentElement.setAttribute('data-theme', theme);
101
+ updateThemeIcon(theme);
102
+ localStorage.setItem('userThemePreference', theme);
103
+ }
104
+
105
+ // 更新主题图标
106
+ function updateThemeIcon(theme) {
107
+ const themeIcon = document.querySelector('.theme-icon');
108
+
109
+ if (!themeIcon) return;
110
+
111
+ if (theme === 'dark') {
112
+ // 使用CSS类切换动画而不是直接修改innerHTML
113
+ themeIcon.classList.add('dark-mode');
114
+ themeIcon.innerHTML = `
115
+ <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
116
+ `;
117
+ } else {
118
+ themeIcon.classList.remove('dark-mode');
119
+ themeIcon.innerHTML = `
120
+ <circle cx="12" cy="12" r="5"></circle>
121
+ <line x1="12" y1="1" x2="12" y2="3"></line>
122
+ <line x1="12" y1="21" x2="12" y2="23"></line>
123
+ <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
124
+ <line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
125
+ <line x1="1" y1="12" x2="3" y2="12"></line>
126
+ <line x1="21" y1="12" x2="23" y2="12"></line>
127
+ <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
128
+ <line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
129
+ `;
130
+ }
131
+ }
132
+
133
+ // 切换主题
134
+ function toggleTheme() {
135
+ const currentTheme = document.documentElement.getAttribute('data-theme') || 'light';
136
+ const newTheme = currentTheme === 'light' ? 'dark' : 'light';
137
+
138
+ // 添加过渡类,启用平滑动画
139
+ document.documentElement.classList.add('theme-transition');
140
+
141
+ // 应用新主题
142
+ applyTheme(newTheme);
143
+
144
+ // 切换动画效果
145
+ const themeSwitch = document.querySelector('.theme-switch');
146
+ if (themeSwitch) {
147
+ themeSwitch.classList.add('theme-switch-animate');
148
+ setTimeout(() => {
149
+ themeSwitch.classList.remove('theme-switch-animate');
150
+ }, 700);
151
+ }
152
+ }
153
+
154
+ // 监听系统主题变化
155
+ function listenForSystemThemeChanges() {
156
+ if (window.matchMedia) {
157
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
158
+ // 只有当用户没有手动设置主题时才跟随系统
159
+ if (!localStorage.getItem('userThemePreference')) {
160
+ applyTheme(e.matches ? 'dark' : 'light');
161
+ }
162
+ });
163
+ }
164
+ }
165
+
166
+ // 基于时间自动切换主题的功能
167
+ function scheduleThemeChange() {
168
+ // 只有当用户没有手动设置主题时才自动切换
169
+ if (!localStorage.getItem('userThemePreference')) {
170
+ const currentHour = new Date().getHours();
171
+ if (currentHour >= 19 || currentHour < 7) {
172
+ applyTheme('dark');
173
+ } else {
174
+ applyTheme('light');
175
+ }
176
+ }
177
+
178
+ // 每小时检查一次
179
+ setTimeout(scheduleThemeChange, 3600000);
180
+ }
181
+
182
+ // 启动基于时间的主题切换
183
+ scheduleThemeChange();
184
+
185
+ // 侧边导航功能
186
+ document.addEventListener('DOMContentLoaded', function() {
187
+ initSideNavigation();
188
+ });
189
+
190
+ // 初始化侧边导航
191
+ function initSideNavigation() {
192
+ // 获取所有卡片
193
+ const cards = document.querySelectorAll('.card');
194
+ const navContent = document.querySelector('.side-nav-content');
195
+ const trigger = document.querySelector('.side-nav-trigger');
196
+ const menu = document.querySelector('.side-nav-menu');
197
+
198
+ if (!cards.length || !navContent) return;
199
+
200
+ // 为每个卡片创建导航项
201
+ cards.forEach((card, index) => {
202
+ // 尝试获取卡片标题
203
+ let title = '';
204
+ const h2 = card.querySelector('h2');
205
+ const h1 = card.querySelector('h1');
206
+
207
+ if (h2) {
208
+ title = h2.textContent.trim();
209
+ } else if (h1) {
210
+ title = h1.textContent.trim();
211
+ } else {
212
+ title = `部分 ${index + 1}`;
213
+ }
214
+
215
+ // 创建导航项
216
+ const navItem = document.createElement('div');
217
+ navItem.className = 'nav-item';
218
+ navItem.setAttribute('data-target', index);
219
+
220
+ // 创建导航点
221
+ const dot = document.createElement('div');
222
+ dot.className = 'nav-item-dot';
223
+
224
+ // 创建标题
225
+ const titleSpan = document.createElement('div');
226
+ titleSpan.className = 'nav-item-title';
227
+ titleSpan.textContent = title;
228
+
229
+ navItem.appendChild(dot);
230
+ navItem.appendChild(titleSpan);
231
+ navContent.appendChild(navItem);
232
+
233
+ // 点击事件:滚动到对应卡片
234
+ navItem.addEventListener('click', (e) => {
235
+ e.preventDefault();
236
+ card.scrollIntoView({ behavior: 'smooth', block: 'start' });
237
+ });
238
+ });
239
+
240
+ // 使用 Intersection Observer 检测当前可见的卡片
241
+ const observer = new IntersectionObserver((entries) => {
242
+ entries.forEach(entry => {
243
+ if (entry.isIntersecting) {
244
+ const index = Array.from(cards).indexOf(entry.target);
245
+ updateActiveNavItem(index);
246
+ }
247
+ });
248
+ }, { threshold: 0.3 });
249
+
250
+ // 观察所有卡片
251
+ cards.forEach(card => {
252
+ observer.observe(card);
253
+ });
254
+
255
+ // 更新活动导航项
256
+ function updateActiveNavItem(index) {
257
+ const navItems = document.querySelectorAll('.nav-item');
258
+ navItems.forEach(item => {
259
+ item.classList.remove('active');
260
+ });
261
+
262
+ const activeItem = document.querySelector(`.nav-item[data-target="${index}"]`);
263
+ if (activeItem) {
264
+ activeItem.classList.add('active');
265
+
266
+ // 确保活动项在可视区域内
267
+ if (activeItem.offsetTop < navContent.scrollTop ||
268
+ activeItem.offsetTop > navContent.scrollTop + navContent.clientHeight) {
269
+ activeItem.scrollIntoView({ behavior: 'smooth', block: 'center' });
270
+ }
271
+ }
272
+ }
273
+
274
+ // 移动端触摸事件处理
275
+ if (trigger && menu) {
276
+ trigger.addEventListener('touchstart', function(e) {
277
+ e.preventDefault();
278
+ this.classList.toggle('touch-active');
279
+ menu.classList.toggle('touch-active');
280
+ });
281
+
282
+ // 点击其他区域关闭移动端目录
283
+ document.addEventListener('touchstart', function(e) {
284
+ if (!e.target.closest('.side-nav-trigger') && !e.target.closest('.side-nav-menu')) {
285
+ trigger.classList.remove('touch-active');
286
+ menu.classList.remove('touch-active');
287
+ }
288
+ });
289
+ }
290
+ }
src/routes/index.js ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ const express = require('express');
2
+ const router = express.Router();
3
+ const v1Routes = require('./v1');
4
+
5
+ // OpenAI v1 API routes
6
+ router.use('/v1', v1Routes);
7
+
8
+ module.exports = router;
src/routes/v1.js ADDED
@@ -0,0 +1,1924 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const express = require('express');
2
+ const router = express.Router();
3
+ const { fetch, ProxyAgent, Agent } = require('undici');
4
+
5
+ const $root = require('../proto/message.js');
6
+ const { v4: uuidv4, v5: uuidv5 } = require('uuid');
7
+ const { generateCursorBody, chunkToUtf8String, generateHashed64Hex, generateCursorChecksum } = require('../utils/utils.js');
8
+ const keyManager = require('../utils/keyManager.js');
9
+ const { spawn } = require('child_process');
10
+ const path = require('path');
11
+ const admin = require('../models/admin');
12
+ const config = require('../config/config');
13
+ const crypto = require('crypto');
14
+ const logger = require('../utils/logger');
15
+
16
+ const activeRequestControllers = new Map(); // 用于存储 API Key -> AbortController 的映射
17
+
18
+ // 存储刷新状态的变量
19
+ let refreshStatus = {
20
+ isRunning: false,
21
+ status: 'idle', // idle, running, completed, failed
22
+ message: '',
23
+ startTime: null,
24
+ endTime: null,
25
+ error: null
26
+ };
27
+
28
+ // 储存当前正在处理的Cookie获取请求
29
+ const pendingCookieRequests = new Map();
30
+
31
+ // 检查是否已有管理员账号
32
+ router.get('/admin/check', (req, res) => {
33
+ try {
34
+ return res.json({
35
+ success: true,
36
+ exists: admin.hasAdmin()
37
+ });
38
+ } catch (error) {
39
+ logger.error('检查管理员账号失败:', error);
40
+ return res.status(500).json({
41
+ success: false,
42
+ message: error.message
43
+ });
44
+ }
45
+ });
46
+
47
+ // 注册管理员
48
+ router.post('/admin/register', (req, res) => {
49
+ try {
50
+ const { username, password } = req.body;
51
+
52
+ if (!username || !password) {
53
+ return res.status(400).json({
54
+ success: false,
55
+ message: '用户名和密码不能为空'
56
+ });
57
+ }
58
+
59
+ const token = admin.register(username, password);
60
+
61
+ return res.json({
62
+ success: true,
63
+ message: '注册成功',
64
+ token
65
+ });
66
+ } catch (error) {
67
+ logger.error('注册管理员失败:', error);
68
+ return res.status(400).json({
69
+ success: false,
70
+ message: error.message
71
+ });
72
+ }
73
+ });
74
+
75
+ // 管理员登录
76
+ router.post('/admin/login', (req, res) => {
77
+ try {
78
+ const { username, password } = req.body;
79
+
80
+ if (!username || !password) {
81
+ return res.status(400).json({
82
+ success: false,
83
+ message: '用户名和密码不能为空'
84
+ });
85
+ }
86
+
87
+ const token = admin.login(username, password);
88
+
89
+ return res.json({
90
+ success: true,
91
+ message: '登录成功',
92
+ token
93
+ });
94
+ } catch (error) {
95
+ logger.error('登录失败:', error);
96
+ return res.status(400).json({
97
+ success: false,
98
+ message: error.message
99
+ });
100
+ }
101
+ });
102
+
103
+ // 验证token
104
+ router.get('/admin/verify', (req, res) => {
105
+ try {
106
+ const authHeader = req.headers.authorization;
107
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
108
+ return res.status(401).json({
109
+ success: false,
110
+ message: '未提供认证token'
111
+ });
112
+ }
113
+
114
+ const token = authHeader.split(' ')[1];
115
+ const result = admin.verifyToken(token);
116
+
117
+ return res.json(result);
118
+ } catch (error) {
119
+ logger.error('验证token失败:', error);
120
+ return res.status(401).json({
121
+ success: false,
122
+ message: error.message
123
+ });
124
+ }
125
+ });
126
+
127
+ // 添加API key管理路由
128
+ router.post("/api-keys", async (req, res) => {
129
+ try {
130
+ const { apiKey, cookieValues } = req.body;
131
+
132
+ if (!apiKey || !cookieValues) {
133
+ return res.status(400).json({
134
+ error: 'API key and cookie values are required',
135
+ });
136
+ }
137
+
138
+ keyManager.addOrUpdateApiKey(apiKey, cookieValues);
139
+
140
+ return res.json({
141
+ success: true,
142
+ message: 'API key added or updated successfully',
143
+ });
144
+ } catch (error) {
145
+ logger.error(error);
146
+ return res.status(500).json({
147
+ error: 'Internal server error',
148
+ });
149
+ }
150
+ });
151
+
152
+ // 获取所有API Keys
153
+ router.get("/api-keys", async (req, res) => {
154
+ try {
155
+ logger.info('收到获取API Keys请求');
156
+ const apiKeys = keyManager.getAllApiKeys();
157
+ logger.info('获取到的API Keys:', apiKeys);
158
+
159
+ const result = {
160
+ success: true,
161
+ apiKeys: apiKeys.map(apiKey => ({
162
+ key: apiKey,
163
+ cookieCount: keyManager.getAllCookiesForApiKey(apiKey).length,
164
+ })),
165
+ };
166
+ logger.info('返回结果:', result);
167
+
168
+ return res.json(result);
169
+ } catch (error) {
170
+ logger.error('获取API Keys失败:', error);
171
+ return res.status(500).json({
172
+ error: 'Internal server error',
173
+ message: error.message
174
+ });
175
+ }
176
+ });
177
+
178
+ // 删除API key
179
+ router.delete("/api-keys/:apiKey", async (req, res) => {
180
+ try {
181
+ const { apiKey } = req.params;
182
+
183
+ keyManager.removeApiKey(apiKey);
184
+
185
+ return res.json({
186
+ success: true,
187
+ message: 'API key removed successfully',
188
+ });
189
+ } catch (error) {
190
+ logger.error(error);
191
+ return res.status(500).json({
192
+ error: 'Internal server error',
193
+ });
194
+ }
195
+ });
196
+
197
+ // 获取特定API Key的Cookie值
198
+ router.get("/api-keys/:apiKey/cookies", async (req, res) => {
199
+ try {
200
+ const { apiKey } = req.params;
201
+ logger.info(`收到获取API Key ${apiKey}的Cookie值请求`);
202
+
203
+ const cookies = keyManager.getAllCookiesForApiKey(apiKey);
204
+ logger.info(`API Key ${apiKey}的Cookie值:`, cookies);
205
+
206
+ return res.json({
207
+ success: true,
208
+ cookies: cookies
209
+ });
210
+ } catch (error) {
211
+ logger.error(`获取API Key ${req.params.apiKey}的Cookie值失败:`, error);
212
+ return res.status(500).json({
213
+ error: 'Internal server error',
214
+ message: error.message
215
+ });
216
+ }
217
+ });
218
+
219
+ // 获取所有无效的cookie
220
+ router.get("/invalid-cookies", async (req, res) => {
221
+ try {
222
+ const invalidCookies = keyManager.getInvalidCookies();
223
+
224
+ return res.json({
225
+ success: true,
226
+ invalidCookies: Array.from(invalidCookies)
227
+ });
228
+ } catch (error) {
229
+ logger.error('获取无效cookie失败:', error);
230
+ return res.status(500).json({
231
+ error: 'Internal server error',
232
+ message: error.message
233
+ });
234
+ }
235
+ });
236
+
237
+ // 清除特定的无效cookie
238
+ router.delete("/invalid-cookies/:cookie", async (req, res) => {
239
+ try {
240
+ const { cookie } = req.params;
241
+ const success = keyManager.clearInvalidCookie(cookie);
242
+
243
+ return res.json({
244
+ success: success,
245
+ message: success ? '无效cookie已清除' : '未找到指定的无效cookie'
246
+ });
247
+ } catch (error) {
248
+ logger.error('清除无效cookie失败:', error);
249
+ return res.status(500).json({
250
+ error: 'Internal server error',
251
+ message: error.message
252
+ });
253
+ }
254
+ });
255
+
256
+ // 清除所有无效cookie
257
+ router.delete("/invalid-cookies", async (req, res) => {
258
+ try {
259
+ keyManager.clearAllInvalidCookies();
260
+
261
+ return res.json({
262
+ success: true,
263
+ message: '所有无效cookie已清除'
264
+ });
265
+ } catch (error) {
266
+ logger.error('清除所有无效cookie失败:', error);
267
+ return res.status(500).json({
268
+ error: 'Internal server error',
269
+ message: error.message
270
+ });
271
+ }
272
+ });
273
+
274
+ // 批量添加无效cookie
275
+ router.post("/invalid-cookies", async (req, res) => {
276
+ try {
277
+ const { invalidCookies } = req.body;
278
+
279
+ if (!Array.isArray(invalidCookies)) {
280
+ return res.status(400).json({
281
+ success: false,
282
+ error: 'Invalid request',
283
+ message: 'invalidCookies必须是一个数组'
284
+ });
285
+ }
286
+
287
+ // 获取当前无效cookie集合
288
+ const currentInvalidCookies = keyManager.getInvalidCookies();
289
+
290
+ // 添加新的无效cookie
291
+ for (const cookie of invalidCookies) {
292
+ if (typeof cookie === 'string' && cookie.trim()) {
293
+ currentInvalidCookies.add(cookie.trim());
294
+ }
295
+ }
296
+
297
+ // 保存到文件
298
+ keyManager.saveInvalidCookiesToFile();
299
+
300
+ return res.json({
301
+ success: true,
302
+ message: `已添加${invalidCookies.length}个无效cookie`
303
+ });
304
+ } catch (error) {
305
+ logger.error('添加无效cookie失败:', error);
306
+ return res.status(500).json({
307
+ error: 'Internal server error',
308
+ message: error.message
309
+ });
310
+ }
311
+ });
312
+
313
+ // 获取可用模型列表
314
+ router.get("/models", async (req, res) => {
315
+ try{
316
+ let bearerToken = req.headers.authorization?.replace('Bearer ', '');
317
+
318
+ // 使用keyManager获取实际的cookie
319
+ let authToken = keyManager.getCookieForApiKey(bearerToken);
320
+
321
+ if (authToken && authToken.includes('%3A%3A')) {
322
+ authToken = authToken.split('%3A%3A')[1];
323
+ }
324
+ else if (authToken && authToken.includes('::')) {
325
+ authToken = authToken.split('::')[1];
326
+ }
327
+
328
+ const checksum = req.headers['x-cursor-checksum']
329
+ ?? process.env['x-cursor-checksum']
330
+ ?? generateCursorChecksum(authToken.trim());
331
+ //const cursorClientVersion = "0.45.11"
332
+ const cursorClientVersion = "0.50.4";
333
+
334
+ const availableModelsResponse = await fetch("https://api2.cursor.sh/aiserver.v1.AiService/AvailableModels", {
335
+ method: 'POST',
336
+ headers: {
337
+ 'accept-encoding': 'gzip',
338
+ 'authorization': `Bearer ${authToken}`,
339
+ 'connect-protocol-version': '1',
340
+ 'content-type': 'application/proto',
341
+ 'user-agent': 'connect-es/1.6.1',
342
+ 'x-cursor-checksum': checksum,
343
+ 'x-cursor-client-version': cursorClientVersion,
344
+ 'x-cursor-config-version': uuidv4(),
345
+ 'x-cursor-timezone': 'Asia/Tokyo',
346
+ 'x-ghost-mode': 'true',
347
+ 'Host': 'api2.cursor.sh',
348
+ },
349
+ })
350
+ const data = await availableModelsResponse.arrayBuffer();
351
+ const buffer = Buffer.from(data);
352
+ try{
353
+ const models = $root.AvailableModelsResponse.decode(buffer).models;
354
+
355
+ // 生成带前缀的模型列表
356
+ const prefixedModels = models.map(model => ({
357
+ id: `[auto]-${model.name}`,
358
+ created: Date.now(),
359
+ object: 'model',
360
+ owned_by: 'cursor'
361
+ }));
362
+
363
+ // 合并原始模型和带前缀的模型
364
+ const combinedModels = models.map(model => ({
365
+ id: model.name,
366
+ created: Date.now(),
367
+ object: 'model',
368
+ owned_by: 'cursor'
369
+ })).concat(prefixedModels);
370
+
371
+ return res.json({
372
+ object: "list",
373
+ data: combinedModels
374
+ })
375
+ } catch (error) {
376
+ const text = buffer.toString('utf-8');
377
+ throw new Error(text);
378
+ }
379
+ }
380
+ catch (error) {
381
+ logger.error(error);
382
+ return res.status(500).json({
383
+ error: 'Internal server error',
384
+ });
385
+ }
386
+ })
387
+
388
+
389
+ router.post('/chat/completions', async (req, res) => {
390
+ // 检查请求体是否存在
391
+ if (!req.body) {
392
+ return res.status(400).json({
393
+ error: '请求体不能为空',
394
+ });
395
+ }
396
+
397
+ // 检查模型属性是否存在
398
+ if (!req.body.model) {
399
+ return res.status(400).json({
400
+ error: '缺少必要参数: model',
401
+ });
402
+ }
403
+
404
+ // 检查未支持的模型和流式传输 (对原始模型进行检查)
405
+ if (typeof req.body.model === 'string' && req.body.model.replace('[auto]-', '').startsWith('o1-') && req.body.stream) {
406
+ return res.status(400).json({
407
+ error: 'Model not supported stream',
408
+ });
409
+ }
410
+
411
+ try {
412
+ const { model, messages, stream = false } = req.body;
413
+ let extractedStopTokens = [];
414
+ let processedMessages = JSON.parse(JSON.stringify(messages)); // 复制一份,避免修改原始请求体
415
+ let foundStopStringPattern = false;
416
+
417
+ let actualModel = model; // 实际发送给Cursor的模型名称
418
+ const autoPrefix = '[auto]-';
419
+
420
+ // 检查并处理带前缀的模型
421
+ if (typeof model === 'string' && model.startsWith(autoPrefix)) {
422
+ actualModel = model.substring(autoPrefix.length); // 移除前缀
423
+ logger.info(`检测到预定模板模型: ${model}, 实际使用模型: ${actualModel}`);
424
+
425
+ // 定义模板和随机标签
426
+ const template = `
427
+ <|Stop-String|><In-The-End>::<S-top>::<The-End>::<stop-string>::<stop-str>::<stop-word>::<STOP-s><|Stop-String|>
428
+ ###Please ensure to output the following stop string wrapped in xml tag {{random}} at the end of each reply:
429
+
430
+ Ending this round of conversation: Ten, nine, eight, seven, six, five, four, three, two, one. This round of replies has been perfectly completed!
431
+ `;
432
+ const tags = ['<In-The-End>', '<S-top>', '<The-End>', '<stop-string>', '<stop-str>', '<stop-word>', '<STOP-s>'];
433
+
434
+ // 1. 随机选择一个标签,确保本次请求中所有注入都使用这一个
435
+ const randomTag = tags[Math.floor(Math.random() * tags.length)];
436
+
437
+ // 2. 构建注入系统消息的指令
438
+ const processedTemplate = template.replace('{{random}}', randomTag);
439
+
440
+ // 3. 构建追加到assistant消息的声明
441
+ const declarationString = `
442
+ ${randomTag}
443
+ Ending this round of conversation: Ten, nine, eight, seven, six, five, four, three, two, one. This round of replies has been perfectly completed!
444
+ ${randomTag.replace('<', '</')}
445
+ `;
446
+
447
+ // 4. 将指令注入到系统消息中 (不存在则创建)
448
+ let systemMessage = processedMessages.find(m => m.role === 'system');
449
+ if (systemMessage) {
450
+ systemMessage.content = (systemMessage.content || '') + '\n\n' + processedTemplate;
451
+ logger.debug('已将模板追加到现有系统消息');
452
+ } else {
453
+ processedMessages.unshift({ role: 'system', content: processedTemplate });
454
+ logger.debug('未找到系统消息,已创建并添加新的系统消息');
455
+ }
456
+
457
+ // 5. 将声明追加到最后5条assistant消息
458
+ let assistantMessagesToModify = 5;
459
+ for (let i = processedMessages.length - 1; i >= 0 && assistantMessagesToModify > 0; i--) {
460
+ if (processedMessages[i].role === 'assistant') {
461
+ processedMessages[i].content = (processedMessages[i].content || '') + declarationString;
462
+ assistantMessagesToModify--;
463
+ }
464
+ }
465
+ logger.debug(`已将声明追加到 ${5 - assistantMessagesToModify} 条assistant消息`);
466
+
467
+ // 在处理完预设模板后,确保foundStopStringPattern为false,以便后续的停止字符串提取逻辑能够运行在processedMessages上
468
+ foundStopStringPattern = false; // 重置foundStopStringPattern
469
+ }
470
+
471
+ // 从messages中提取停止字符串并移除标记 (现在会作用于可能修改过的processedMessages)
472
+ for (const message of processedMessages) {
473
+ let content = message.content || '';
474
+ const stopStringPattern = /<\|Stop-String\|>(.*?)<\|Stop-String\|>/s;
475
+ const match = content.match(stopStringPattern);
476
+
477
+ if (match && match[1] && !foundStopStringPattern) {
478
+ // 只处理找到的第一个匹配
479
+ const stopStrings = match[1].split('::').map(s => s.trim()).filter(s => s.length > 0);
480
+ extractedStopTokens = stopStrings;
481
+ foundStopStringPattern = true;
482
+
483
+ // 移除content中的停止字符串标记
484
+ content = content.replace(stopStringPattern, '').trim();
485
+
486
+ // 如果移除后内容为空,考虑删除该消息或保留角色信息
487
+ if (content === '') {
488
+ // Option 1: Keep role but empty content, prevents removing valid turn.
489
+ message.content = ''; // 直接修改processedMessages中的对象
490
+ // Option 2: Remove message entirely if content becomes empty.
491
+ // continue; // 这需要重建processedMessages数组
492
+ } else {
493
+ message.content = content; // 直接修改processedMessages中的对象
494
+ }
495
+ } else if (foundStopStringPattern) {
496
+ // 如果已经找到模式,直接使用原始内容,不再进行移除操作
497
+ // message.content 保持不变
498
+ }
499
+ }
500
+
501
+ // 如果没有找到停止字符串模式,返回错误 (现在只有在没有[auto]-前缀模型且没有找到标记时才会触发)
502
+ // 对于[auto]-前缀模型,由于模板中包含了标记,foundStopStringPattern会被设置为true
503
+ if (!foundStopStringPattern) {
504
+ return res.json({
505
+ id: `chatcmpl-${uuidv4()}`,
506
+ object: 'chat.completion',
507
+ created: Math.floor(Date.now() / 1000),
508
+ model: model || 'unknown',
509
+ choices: [
510
+ {
511
+ index: 0,
512
+ message: {
513
+ role: 'assistant',
514
+ content: '预设错误,请使用指定预设结构',
515
+ },
516
+ finish_reason: 'stop',
517
+ },
518
+ ],
519
+ usage: {
520
+ prompt_tokens: 0,
521
+ completion_tokens: 0,
522
+ total_tokens: 0,
523
+ },
524
+ });
525
+ }
526
+
527
+ // 使用提取的停止字符串
528
+ const stopTokens = extractedStopTokens;
529
+
530
+ // 记录本次回复的所有停止字符串
531
+ logger.info(`本次回复使用的停止字符串: [${stopTokens.join(', ')}]`);
532
+
533
+ let bearerToken = req.headers.authorization?.replace('Bearer ', '');
534
+
535
+ // 使用keyManager获取实际的cookie
536
+ let authToken = keyManager.getCookieForApiKey(bearerToken);
537
+ // 保存原始cookie,用于后续可能的错误处理
538
+ const originalAuthToken = authToken;
539
+ //console.log('原始cookie:', originalAuthToken);
540
+
541
+ if (authToken && authToken.includes('%3A%3A')) {
542
+ authToken = authToken.split('%3A%3A')[1];
543
+ }
544
+ else if (authToken && authToken.includes('::')) {
545
+ authToken = authToken.split('::')[1];
546
+ }
547
+
548
+ // 使用processedMessages (可能包含追加的模板)
549
+ if (!processedMessages || processedMessages.length === 0 || !authToken) {
550
+ return res.status(400).json({
551
+ error: 'Invalid request. Messages should be a non-empty array and authorization is required',
552
+ });
553
+ }
554
+
555
+ const checksum = req.headers['x-cursor-checksum']
556
+ ?? process.env['x-cursor-checksum']
557
+ ?? generateCursorChecksum(authToken.trim());
558
+
559
+ const sessionid = uuidv5(authToken, uuidv5.DNS);
560
+ const clientKey = generateHashed64Hex(authToken);
561
+ const cursorClientVersion = "0.50.4";
562
+
563
+ // 在请求聊天接口前,依次调用6个接口
564
+ if (process.env.USE_OTHERS === 'true') {
565
+ try{
566
+ others(authToken, clientKey, checksum, cursorClientVersion, sessionid).then( () => {
567
+ logger.info("其它接口异步调用成功");
568
+ });
569
+ } catch (error) {
570
+ logger.error(error.message);
571
+ }
572
+ }
573
+
574
+ // 使用实际发送给Cursor的模型名称 (不带前缀)
575
+ logger.info('发送给Cursor的完整消息上下文:', JSON.stringify(processedMessages, null, 2));
576
+ logger.info('发送给Cursor的实际模型:', actualModel);
577
+ const cursorBody = generateCursorBody(processedMessages, actualModel);
578
+
579
+ // 添加代理支持
580
+ const dispatcher = config.proxy && config.proxy.enabled
581
+ ? new ProxyAgent(config.proxy.url, { allowH2: true })
582
+ : new Agent({ allowH2: true });
583
+
584
+ // 根据.env配置决定是否使用TLS代理
585
+ const useTlsProxy = process.env.USE_TLS_PROXY === 'true';
586
+
587
+ // 创建AbortController用于能够中止请求
588
+ const controller = new AbortController();
589
+ const signal = controller.signal;
590
+
591
+ let response;
592
+
593
+ try {
594
+ if (useTlsProxy) {
595
+ // 使用JA3指纹伪造代理服务器
596
+ logger.info(`使用TLS代理服务器`);
597
+ response = await fetch('http://localhost:8080/proxy', {
598
+ method: 'POST',
599
+ headers: {
600
+ 'Content-Type': 'application/json',
601
+ },
602
+ body: JSON.stringify({
603
+ url: 'https://api2.cursor.sh/aiserver.v1.ChatService/StreamUnifiedChatWithTools',
604
+ method: 'POST',
605
+ headers: {
606
+ 'authorization': `Bearer ${authToken}`,
607
+ 'connect-accept-encoding': 'gzip',
608
+ 'connect-content-encoding': 'gzip',
609
+ 'connect-protocol-version': '1',
610
+ 'content-type': 'application/connect+proto',
611
+ 'user-agent': 'connect-es/1.6.1',
612
+ 'x-amzn-trace-id': `Root=${uuidv4()}`,
613
+ 'x-client-key': clientKey,
614
+ 'x-cursor-checksum': checksum,
615
+ 'x-cursor-client-version': cursorClientVersion,
616
+ 'x-cursor-config-version': uuidv4(),
617
+ 'x-cursor-timezone': 'Asia/Tokyo',
618
+ 'x-ghost-mode': 'true',
619
+ 'x-request-id': uuidv4(),
620
+ 'x-session-id': sessionid,
621
+ 'Host': 'api2.cursor.sh',
622
+ },
623
+ body: cursorBody,
624
+ stream: true // 启用流式响应
625
+ }),
626
+ timeout: {
627
+ connect: 5000,
628
+ read: 30000
629
+ },
630
+ signal // 添加AbortController的signal
631
+ });
632
+ } else {
633
+ // 直接调用API,不使用TLS代理
634
+ logger.info('不使用TLS代理服务器,直接请求API');
635
+ response = await fetch('https://api2.cursor.sh/aiserver.v1.ChatService/StreamUnifiedChatWithTools', {
636
+ method: 'POST',
637
+ headers: {
638
+ 'authorization': `Bearer ${authToken}`,
639
+ 'connect-accept-encoding': 'gzip',
640
+ 'connect-content-encoding': 'gzip',
641
+ 'connect-protocol-version': '1',
642
+ 'content-type': 'application/connect+proto',
643
+ 'user-agent': 'connect-es/1.6.1',
644
+ 'x-amzn-trace-id': `Root=${uuidv4()}`,
645
+ 'x-client-key': clientKey,
646
+ 'x-cursor-checksum': checksum,
647
+ 'x-cursor-client-version': cursorClientVersion,
648
+ 'x-cursor-config-version': uuidv4(),
649
+ 'x-cursor-timezone': 'Asia/Shanghai',
650
+ 'x-ghost-mode': 'true',
651
+ 'x-request-id': uuidv4(),
652
+ 'x-session-id': sessionid,
653
+ 'Host': 'api2.cursor.sh',
654
+ },
655
+ body: cursorBody,
656
+ dispatcher: dispatcher,
657
+ timeout: {
658
+ connect: 5000,
659
+ read: 30000
660
+ },
661
+ signal // 添加AbortController的signal
662
+ });
663
+ }
664
+ } catch (fetchError) {
665
+ logger.error(`Fetch错误: ${fetchError.message}`);
666
+
667
+ // 处理连接超时错误
668
+ const isConnectTimeout = fetchError.cause &&
669
+ (fetchError.cause.code === 'UND_ERR_CONNECT_TIMEOUT' ||
670
+ fetchError.message.includes('Connect Timeout Error'));
671
+
672
+ // 构建错误响应
673
+ const errorMessage = isConnectTimeout
674
+ ? `⚠️ 连接超时 ⚠️\\n\\n无法连接到API服务器(api2.cursor.sh),请检查您的网络连接或尝试使用代理。`
675
+ : `⚠️ 请求失败 ⚠️\\n\\n错误: ${fetchError.message}`;
676
+
677
+ if (stream) {
678
+ // 流式响应格式的错误
679
+ const responseId = `chatcmpl-${uuidv4()}`;
680
+ res.write(
681
+ `data: ${JSON.stringify({
682
+ id: responseId,
683
+ object: 'chat.completion.chunk',
684
+ created: Math.floor(Date.now() / 1000),
685
+ model: req.body.model || 'unknown',
686
+ choices: [
687
+ {
688
+ index: 0,
689
+ delta: {
690
+ content: errorMessage,
691
+ },
692
+ },
693
+ ],
694
+ })}\n\n`
695
+ );
696
+ res.write('data: [DONE]\n\n');
697
+ res.end();
698
+ } else {
699
+ // 非流式响应格式的错误
700
+ res.json({
701
+ id: `chatcmpl-${uuidv4()}`,
702
+ object: 'chat.completion',
703
+ created: Math.floor(Date.now() / 1000),
704
+ model: req.body.model || 'unknown',
705
+ choices: [
706
+ {
707
+ index: 0,
708
+ message: {
709
+ role: 'assistant',
710
+ content: errorMessage,
711
+ },
712
+ finish_reason: 'stop',
713
+ },
714
+ ],
715
+ usage: {
716
+ prompt_tokens: 0,
717
+ completion_tokens: 0,
718
+ total_tokens: 0,
719
+ },
720
+ });
721
+ }
722
+ return; // 重要:提前返回
723
+ }
724
+
725
+ // 处理响应
726
+ if (stream) {
727
+ // 如果存在此 API Key 的旧请求,则中止它
728
+ if (bearerToken && activeRequestControllers.has(bearerToken)) {
729
+ const oldController = activeRequestControllers.get(bearerToken);
730
+ logger.info(`API Key [${bearerToken}] 的新流式请求到达,正在中止旧请求...`);
731
+ oldController.abort();
732
+ // activeRequestControllers.delete(bearerToken); // 旧的会被新的覆盖,或在旧请求的清理逻辑中移除
733
+ }
734
+ // 存储当前请求的 AbortController
735
+ if (bearerToken) {
736
+ activeRequestControllers.set(bearerToken, controller);
737
+ }
738
+
739
+ // 清理当前请求的 AbortController 的辅助函数
740
+ const cleanupCurrentController = () => {
741
+ if (bearerToken && activeRequestControllers.get(bearerToken) === controller) {
742
+ activeRequestControllers.delete(bearerToken);
743
+ logger.debug(`API Key [${bearerToken}] 的流式请求处理完毕,已清理 AbortController。`);
744
+ }
745
+ };
746
+ res.on('finish', cleanupCurrentController); // 响应正常结束时清理
747
+ res.on('close', cleanupCurrentController); // 响应因任何原因关闭时清理 (包括客户端断开)
748
+
749
+ res.setHeader('Content-Type', 'text/event-stream');
750
+ res.setHeader('Cache-Control', 'no-cache');
751
+ res.setHeader('Connection', 'keep-alive');
752
+
753
+ // 监听客户端断开连接事件
754
+ req.on('close', () => {
755
+ if (!responseEnded) {
756
+ logger.warn(`客户端已断开连接 (API Key: [${bearerToken}]), 正在中止对Cursor服务端的请求...`);
757
+ controller.abort();
758
+ responseEnded = true;
759
+ // cleanupCurrentController 会在 res 'close' 时被调用
760
+ }
761
+ });
762
+
763
+ const responseId = `chatcmpl-${uuidv4()}`;
764
+
765
+ let isThinking_status = 0; //0为没有思考,1为处于思考状态
766
+ try {
767
+ let responseEnded = false; // 添加标志,标记响应是否已结束
768
+ let hasWrittenThinkingStart = false; // 标记是否已发送thinking开始标签
769
+ let hasWrittenThinkingEnd = false; // 标记是否已发送thinking结束标签
770
+ let hasWrittenContent = false; // 标记是否已发送content
771
+ let accumulatedThinking = ''; // 累积thinking内容
772
+ let accumulatedContent = ''; // 累积content内容
773
+
774
+ for await (const chunk of response.body) {
775
+ // 如果响应已结束,不再处理后续数据
776
+ if (responseEnded) {
777
+ continue;
778
+ }
779
+
780
+ let result = {};
781
+ try {
782
+ result = chunkToUtf8String(chunk);
783
+ } catch (error) {
784
+ logger.error('解析响应块失败:', error);
785
+ // 提供默认的空结果,避免后续处理出错
786
+ result = {
787
+ isThink: false,
788
+ thinkingContent: '',
789
+ content: '',
790
+ error: `解析错误: ${error.message}`
791
+ };
792
+ }
793
+
794
+ // 检查是否返回了错误对象
795
+ if (result && typeof result === 'object' && result.error) {
796
+ // 检查是否包含特定的无效cookie错误信息
797
+ const errorStr = typeof result.error === 'string' ? result.error : JSON.stringify(result.error);
798
+
799
+ // 处理错误并获取结果
800
+ const errorResult = handleCursorError(errorStr, bearerToken, originalAuthToken);
801
+
802
+ // 如果是需要移除的cookie,从API Key中移除
803
+ if (errorResult.shouldRemoveCookie) {
804
+ const removed = keyManager.removeCookieFromApiKey(bearerToken, originalAuthToken);
805
+ logger.info(`Cookie移除${removed ? '成功' : '失败'}`);
806
+
807
+ // 如果成功移除,在错误消息中添加明确提示
808
+ if (removed) {
809
+ errorResult.message = `⚠️ 目前Cookie已从API Key中移除 ⚠️\\n\\n${errorResult.message}`;
810
+ }
811
+ }
812
+
813
+ // 返回错误信息给客户端,作为assistant消息
814
+ res.write(
815
+ `data: ${JSON.stringify({
816
+ id: responseId,
817
+ object: 'chat.completion.chunk',
818
+ created: Math.floor(Date.now() / 1000),
819
+ model: req.body.model,
820
+ choices: [
821
+ {
822
+ index: 0,
823
+ delta: {
824
+ content: errorResult.message,
825
+ },
826
+ },
827
+ ],
828
+ })}\n\n`
829
+ );
830
+
831
+ res.write('data: [DONE]\n\n');
832
+ responseEnded = true; // 标记响应已结束
833
+ break; // 跳出循环,不再处理后续数据
834
+ }
835
+
836
+ // 处理thinking内容
837
+ if (result.isThink && result.thinkingContent && result.thinkingContent.length > 0) {
838
+ // 累积thinking内容
839
+ accumulatedThinking += result.thinkingContent;
840
+
841
+ // 如果没有发送thinking开始标记,则发送
842
+ if (!hasWrittenThinkingStart) {
843
+ res.write(
844
+ `data: ${JSON.stringify({
845
+ id: responseId,
846
+ object: 'chat.completion.chunk',
847
+ created: Math.floor(Date.now() / 1000),
848
+ model: req.body.model,
849
+ choices: [
850
+ {
851
+ index: 0,
852
+ delta: {
853
+ content: "<think>\\n",
854
+ },
855
+ },
856
+ ],
857
+ })}\n\n`
858
+ );
859
+ hasWrittenThinkingStart = true;
860
+ }
861
+
862
+ // 发送accumulated thinking内容片段
863
+ res.write(
864
+ `data: ${JSON.stringify({
865
+ id: responseId,
866
+ object: 'chat.completion.chunk',
867
+ created: Math.floor(Date.now() / 1000),
868
+ model: req.body.model,
869
+ choices: [
870
+ {
871
+ index: 0,
872
+ delta: {
873
+ content: result.thinkingContent,
874
+ },
875
+ },
876
+ ],
877
+ })}\n\n`
878
+ );
879
+ }
880
+
881
+ // 处理常规内容
882
+ if (result.content && result.content.length > 0) {
883
+ // 累积content内容
884
+ accumulatedContent += result.content;
885
+
886
+ // 如果已经有thinking内容,且尚未发送thinking结束标记��则发送
887
+ if (hasWrittenThinkingStart && !hasWrittenThinkingEnd) {
888
+ res.write(
889
+ `data: ${JSON.stringify({
890
+ id: responseId,
891
+ object: 'chat.completion.chunk',
892
+ created: Math.floor(Date.now() / 1000),
893
+ model: req.body.model,
894
+ choices: [
895
+ {
896
+ index: 0,
897
+ delta: {
898
+ content: "\\n</think>\\n",
899
+ },
900
+ },
901
+ ],
902
+ })}\n\n`
903
+ );
904
+ hasWrittenThinkingEnd = true;
905
+ }
906
+
907
+ // 检查是否遇到停止字符串
908
+ let shouldStop = false;
909
+ let contentToSend = result.content;
910
+
911
+ // 检查停止字符串
912
+ if (stopTokens.length > 0) {
913
+ for (const stopToken of stopTokens) {
914
+ const stopIndex = accumulatedContent.indexOf(stopToken);
915
+ if (stopIndex !== -1) {
916
+ // 记录检测到停止字符串的日志
917
+ logger.info(`检测到停止字符串: "${stopToken}" 在位置 ${stopIndex},累积内容长度: ${accumulatedContent.length}`);
918
+
919
+ // 如果找到停止字符串,立即停止,不管停止字符串在哪个chunk中
920
+ const lastChunkIndex = accumulatedContent.length - result.content.length;
921
+
922
+ if (stopIndex >= lastChunkIndex) {
923
+ // 停止字符串在当前块中,只发送到停止字符串之前的内容
924
+ contentToSend = result.content.substring(0, stopIndex - lastChunkIndex);
925
+ } else {
926
+ // 停止字符串在之前的chunks中,不发送当前chunk的任何内容
927
+ contentToSend = '';
928
+ }
929
+
930
+ shouldStop = true;
931
+ break;
932
+ }
933
+ }
934
+ }
935
+
936
+ // 只有当有内容要发送时才发送
937
+ if (contentToSend.length > 0) {
938
+ // 发送content内容
939
+ res.write(
940
+ `data: ${JSON.stringify({
941
+ id: responseId,
942
+ object: 'chat.completion.chunk',
943
+ created: Math.floor(Date.now() / 1000),
944
+ model: req.body.model,
945
+ choices: [
946
+ {
947
+ index: 0,
948
+ delta: {
949
+ content: contentToSend,
950
+ },
951
+ },
952
+ ],
953
+ })}\n\n`
954
+ );
955
+ hasWrittenContent = true;
956
+ }
957
+
958
+ // 如果需要停止,发送[DONE]并结束响应
959
+ if (shouldStop) {
960
+ res.write('data: [DONE]\n\n');
961
+ res.end();
962
+ responseEnded = true;
963
+
964
+ try {
965
+ controller.abort();
966
+ } catch (abortError) {
967
+ logger.error('中止Cursor连接时出错 (停止字符串):', abortError);
968
+ }
969
+ // cleanupCurrentController 会在 res 'finish' 或 'close' 时被调用
970
+ break;
971
+ }
972
+ }
973
+ }
974
+
975
+ // 处理结束逻辑:确保thinking标签被正确关闭
976
+ if (!responseEnded) {
977
+ // 如果有thinking内容但没有发送结束标记,则发送
978
+ if (hasWrittenThinkingStart && !hasWrittenThinkingEnd) {
979
+ res.write(
980
+ `data: ${JSON.stringify({
981
+ id: responseId,
982
+ object: 'chat.completion.chunk',
983
+ created: Math.floor(Date.now() / 1000),
984
+ model: req.body.model,
985
+ choices: [
986
+ {
987
+ index: 0,
988
+ delta: {
989
+ content: "\\n</think>\\n",
990
+ },
991
+ },
992
+ ],
993
+ })}\n\n`
994
+ );
995
+ }
996
+
997
+ res.write('data: [DONE]\n\n');
998
+ res.end();
999
+ // cleanupCurrentController 会在 res 'finish' 时被调用
1000
+ }
1001
+ } catch (streamError) {
1002
+ // 区分正常的中止操作和真正的错误
1003
+ if (streamError.name === 'AbortError') {
1004
+ logger.info(`流处理被中止 (API Key: [${bearerToken}]), 原因可能为: 新请求覆盖, 客户端断开, 或停止字符串触发。`);
1005
+ } else {
1006
+ logger.error(`Stream error (API Key: [${bearerToken}]):`, streamError);
1007
+ }
1008
+
1009
+ if (!res.writableEnded) {
1010
+ if (streamError.name === 'AbortError') {
1011
+ // AbortError 通常意味着我们主动中止,响应可能已处理或将由 'close' 事件处理
1012
+ // 但为确保万无一失,如果响应未结束,尝试结束它
1013
+ if (!res.headersSent) { // 避免在已发送头后再次发送
1014
+ res.status(500).json({ error: 'Stream aborted' });
1015
+ } else {
1016
+ res.end(); //尝试结束流
1017
+ }
1018
+ return; // AbortError 处理完毕
1019
+ } else if (streamError.name === 'TimeoutError') {
1020
+ // 将超时错误作为assistant消息发送
1021
+ const errorMessage = `⚠️ 请求超时 ⚠️\\n\\n错误:服务器响应超时,请稍后重试。`;
1022
+ res.write(
1023
+ `data: ${JSON.stringify({
1024
+ id: responseId,
1025
+ object: 'chat.completion.chunk',
1026
+ created: Math.floor(Date.now() / 1000),
1027
+ model: req.body.model,
1028
+ choices: [
1029
+ {
1030
+ index: 0,
1031
+ delta: {
1032
+ content: errorMessage,
1033
+ },
1034
+ },
1035
+ ],
1036
+ })}\n\n`
1037
+ );
1038
+ } else {
1039
+ // 将处理错误作为assistant消息发送
1040
+ const errorMessage = `⚠️ 处理错误 ⚠️\\n\\n错误:流处理出错,请稍后重试。\\n\\n${streamError.message || ''}`;
1041
+ res.write(
1042
+ `data: ${JSON.stringify({
1043
+ id: responseId,
1044
+ object: 'chat.completion.chunk',
1045
+ created: Math.floor(Date.now() / 1000),
1046
+ model: req.body.model,
1047
+ choices: [
1048
+ {
1049
+ index: 0,
1050
+ delta: {
1051
+ content: errorMessage,
1052
+ },
1053
+ },
1054
+ ],
1055
+ })}\n\n`
1056
+ );
1057
+ }
1058
+ res.write('data: [DONE]\n\n');
1059
+ res.end();
1060
+ }
1061
+ }
1062
+ } else {
1063
+ try {
1064
+ let text = '';
1065
+ let thinkingText = '';
1066
+ let hasThinking = false;
1067
+ let responseEnded = false; // 添加标志,标记响应是否已结束
1068
+
1069
+ for await (const chunk of response.body) {
1070
+ // 如果响应已结束,不再处理后续数据
1071
+ if (responseEnded) {
1072
+ continue;
1073
+ }
1074
+
1075
+ let result = {};
1076
+ try {
1077
+ result = chunkToUtf8String(chunk);
1078
+ } catch (error) {
1079
+ logger.error('非流式响应解析块失败:', error);
1080
+ // 提供默认的空结果,避免后续处理出错
1081
+ result = {
1082
+ thinkingContent: '',
1083
+ content: '',
1084
+ error: `解析错误: ${error.message}`
1085
+ };
1086
+ }
1087
+ // 输出完整的result内容和类型,便于调试
1088
+ //console.log("收到的非流式响应:", typeof result, result && typeof result === 'object' ? JSON.stringify(result) : result);
1089
+
1090
+ // 检查是否返回了错误对象
1091
+ if (result && typeof result === 'object' && result.error) {
1092
+ //console.error('检测到错误响应:', result.error);
1093
+
1094
+ // 检查是否包含特定的无效cookie错误信息
1095
+ const errorStr = typeof result.error === 'string' ? result.error : JSON.stringify(result.error);
1096
+
1097
+ // 处理错误并获取结果
1098
+ const errorResult = handleCursorError(errorStr, bearerToken, originalAuthToken);
1099
+
1100
+ // 如果是需要移除的cookie,从API Key中移除
1101
+ if (errorResult.shouldRemoveCookie) {
1102
+ const removed = keyManager.removeCookieFromApiKey(bearerToken, originalAuthToken);
1103
+ logger.info(`Cookie移除${removed ? '成功' : '失败'}`);
1104
+
1105
+ // 如果成功移除,在错误消息中添加明确提示
1106
+ if (removed) {
1107
+ errorResult.message = `⚠️ 目前Cookie已从API Key中移除 ⚠️\\n\\n${errorResult.message}`;
1108
+ }
1109
+ }
1110
+
1111
+ // 无效cookie错误,格式化为assistant消息
1112
+ res.json({
1113
+ id: `chatcmpl-${uuidv4()}`,
1114
+ object: 'chat.completion',
1115
+ created: Math.floor(Date.now() / 1000),
1116
+ model,
1117
+ choices: [
1118
+ {
1119
+ index: 0,
1120
+ message: {
1121
+ role: 'assistant',
1122
+ content: errorResult.message,
1123
+ },
1124
+ finish_reason: 'stop',
1125
+ },
1126
+ ],
1127
+ usage: {
1128
+ prompt_tokens: 0,
1129
+ completion_tokens: 0,
1130
+ total_tokens: 0,
1131
+ },
1132
+ });
1133
+
1134
+ responseEnded = true; // 标记响应已结束
1135
+ break; // 跳出循环,不再处理后续数据
1136
+ }
1137
+
1138
+ // 处理thinking内容
1139
+ if (result.thinkingContent && result.thinkingContent.length > 0) {
1140
+ thinkingText += result.thinkingContent;
1141
+ hasThinking = true;
1142
+ }
1143
+
1144
+ // 处理正常文本内容
1145
+ if (result.content && typeof result.content === 'string') {
1146
+ text += result.content;
1147
+ }
1148
+ }
1149
+
1150
+ // 只有在响应尚未结束的情况下,才处理和返回结果
1151
+ if (!responseEnded) {
1152
+ // 对解析后的字符串进行进一步处理
1153
+ text = text.replace(/^.*<\|END_USER\|>/s, '');
1154
+ text = text.replace(/^\n[a-zA-Z]?/, '').trim();
1155
+
1156
+ // 检查停止字符串并截断内容
1157
+ if (stopTokens.length > 0) {
1158
+ for (const stopToken of stopTokens) {
1159
+ const stopIndex = text.indexOf(stopToken);
1160
+ if (stopIndex !== -1) {
1161
+ // 记录检测到停止字符串的日志
1162
+ logger.info(`非流式响应检测到停止字符串: "${stopToken}" 在位置 ${stopIndex}`);
1163
+
1164
+ // 截断到停止字符串之前的内容
1165
+ text = text.substring(0, stopIndex);
1166
+ break;
1167
+ }
1168
+ }
1169
+ }
1170
+
1171
+ // 如果存在thinking内容,添加标签
1172
+ let finalContent = text;
1173
+ if (hasThinking && thinkingText.length > 0) {
1174
+ finalContent = `<think>\\n${thinkingText}\\n</think>\\n${text}`;
1175
+ }
1176
+
1177
+ res.json({
1178
+ id: `chatcmpl-${uuidv4()}`,
1179
+ object: 'chat.completion',
1180
+ created: Math.floor(Date.now() / 1000),
1181
+ model,
1182
+ choices: [
1183
+ {
1184
+ index: 0,
1185
+ message: {
1186
+ role: 'assistant',
1187
+ content: finalContent,
1188
+ },
1189
+ finish_reason: 'stop',
1190
+ },
1191
+ ],
1192
+ usage: {
1193
+ prompt_tokens: 0,
1194
+ completion_tokens: 0,
1195
+ total_tokens: 0,
1196
+ },
1197
+ });
1198
+ }
1199
+ } catch (error) {
1200
+ logger.error('Non-stream error:', error);
1201
+ // 确保在发送错误信息前检查响应是否已结束
1202
+ if (!res.headersSent) {
1203
+ if (error.name === 'TimeoutError') {
1204
+ // 使用统一的错误格式
1205
+ const errorMessage = `⚠️ 请求超时 ⚠️\\n\\n错误:服务器响应超时,请稍后重试。`;
1206
+ return res.json({
1207
+ id: `chatcmpl-${uuidv4()}`,
1208
+ object: 'chat.completion',
1209
+ created: Math.floor(Date.now() / 1000),
1210
+ model: req.body.model || 'unknown',
1211
+ choices: [
1212
+ {
1213
+ index: 0,
1214
+ message: {
1215
+ role: 'assistant',
1216
+ content: errorMessage,
1217
+ },
1218
+ finish_reason: 'stop',
1219
+ },
1220
+ ],
1221
+ usage: {
1222
+ prompt_tokens: 0,
1223
+ completion_tokens: 0,
1224
+ total_tokens: 0,
1225
+ },
1226
+ });
1227
+ }
1228
+ throw error;
1229
+ }
1230
+ }
1231
+ }
1232
+ } catch (error) {
1233
+ logger.error('Error:', error);
1234
+ if (!res.headersSent) {
1235
+ const errorText = error.name === 'TimeoutError' ? '请求超时' : '服务器内部错误';
1236
+
1237
+ if (req.body.stream) {
1238
+ // 流式响应格式的错误
1239
+ const responseId = `chatcmpl-${uuidv4()}`;
1240
+ // 添加清晰的错误提示
1241
+ const errorMessage = `⚠️ 请求失败 ⚠️\\n\\n错误:${errorText},请稍后重试。\\n\\n${error.message || ''}`;
1242
+ res.write(
1243
+ `data: ${JSON.stringify({
1244
+ id: responseId,
1245
+ object: 'chat.completion.chunk',
1246
+ created: Math.floor(Date.now() / 1000),
1247
+ model: req.body.model || 'unknown',
1248
+ choices: [
1249
+ {
1250
+ index: 0,
1251
+ delta: {
1252
+ content: errorMessage,
1253
+ },
1254
+ },
1255
+ ],
1256
+ })}\n\n`
1257
+ );
1258
+ res.write('data: [DONE]\n\n');
1259
+ res.end();
1260
+ } else {
1261
+ // 非流式响应格式的错误
1262
+ // 添加清晰的错误提示
1263
+ const errorMessage = `⚠️ 请求失败 ⚠️\\n\\n错误:${errorText},请稍后重试。\\n\\n${error.message || ''}`;
1264
+ res.json({
1265
+ id: `chatcmpl-${uuidv4()}`,
1266
+ object: 'chat.completion',
1267
+ created: Math.floor(Date.now() / 1000),
1268
+ model: req.body.model || 'unknown',
1269
+ choices: [
1270
+ {
1271
+ index: 0,
1272
+ message: {
1273
+ role: 'assistant',
1274
+ content: errorMessage,
1275
+ },
1276
+ finish_reason: 'stop',
1277
+ },
1278
+ ],
1279
+ usage: {
1280
+ prompt_tokens: 0,
1281
+ completion_tokens: 0,
1282
+ total_tokens: 0,
1283
+ },
1284
+ });
1285
+ }
1286
+ }
1287
+ }
1288
+ });
1289
+
1290
+ // 触发Cookie刷新
1291
+ router.post("/refresh-cookies", async (req, res) => {
1292
+ try {
1293
+ // 如果已经有刷新进程在运行,则返回错误
1294
+ if (refreshStatus.isRunning) {
1295
+ return res.status(409).json({
1296
+ success: false,
1297
+ message: '已有刷新进程在运行,请等待完成后再试'
1298
+ });
1299
+ }
1300
+
1301
+ // 获取请求参数
1302
+ const apiKey = req.query.apiKey || '';
1303
+
1304
+ // 重置刷新状态
1305
+ refreshStatus = {
1306
+ isRunning: true,
1307
+ status: 'running',
1308
+ message: '正在启动刷新进程...',
1309
+ startTime: new Date(),
1310
+ endTime: null,
1311
+ error: null
1312
+ };
1313
+
1314
+ logger.info(`收到刷新Cookie请求,API Key: ${apiKey || '所有'}`);
1315
+
1316
+ // 构建命令行参数
1317
+ const args = [];
1318
+ if (apiKey) {
1319
+ args.push(apiKey);
1320
+ }
1321
+
1322
+ // 获取auto-refresh-cookies.js的绝对路径
1323
+ const scriptPath = path.resolve(__dirname, '../../auto-refresh-cookies.js');
1324
+
1325
+ // 启动子进程执行刷新脚本
1326
+ const refreshProcess = spawn('node', [scriptPath, ...args], {
1327
+ stdio: ['ignore', 'pipe', 'pipe']
1328
+ });
1329
+
1330
+ // 收集输出
1331
+ let output = '';
1332
+
1333
+ refreshProcess.stdout.on('data', (data) => {
1334
+ const text = data.toString();
1335
+ output += text;
1336
+ logger.info(`刷新进程输出: ${text}`);
1337
+
1338
+ // 更新状态消息
1339
+ if (text.includes('开始自动刷新')) {
1340
+ refreshStatus.message = '正在刷新Cookie...';
1341
+ } else if (text.includes('刷新结果:')) {
1342
+ refreshStatus.message = text.trim();
1343
+ }
1344
+ });
1345
+
1346
+ refreshProcess.stderr.on('data', (data) => {
1347
+ const text = data.toString();
1348
+ output += text;
1349
+ logger.error(`刷新进程错误: ${text}`);
1350
+
1351
+ // 更新错误信息
1352
+ refreshStatus.error = text.trim();
1353
+ refreshStatus.message = `发生错误: ${text.trim()}`;
1354
+ });
1355
+
1356
+ refreshProcess.on('close', (code) => {
1357
+ logger.info(`刷新进程退出,代码: ${code}`);
1358
+
1359
+ refreshStatus.isRunning = false;
1360
+ refreshStatus.endTime = new Date();
1361
+
1362
+ if (code === 0) {
1363
+ refreshStatus.status = 'completed';
1364
+
1365
+ // 提取成功信息
1366
+ const successMatch = output.match(/成功刷新 (\d+) 个/);
1367
+ if (successMatch) {
1368
+ refreshStatus.message = `成功刷新 ${successMatch[1]} 个API Key的Cookie`;
1369
+ } else {
1370
+ refreshStatus.message = '刷新完成';
1371
+ }
1372
+
1373
+ // 子进程执行完成后,重新初始化API Keys来加载新的Cookie
1374
+ try {
1375
+ const keyManager = require('../utils/keyManager');
1376
+ logger.info('子进程刷新Cookie完成,重新初始化主进程中的API Keys...');
1377
+ keyManager.initializeApiKeys();
1378
+ logger.info('主进程API Keys重新加载完成');
1379
+ } catch (initError) {
1380
+ logger.error('重新初始化API Keys失败:', initError);
1381
+ }
1382
+ } else {
1383
+ refreshStatus.status = 'failed';
1384
+ refreshStatus.message = refreshStatus.error || '刷新失败,请查看服务器日志';
1385
+ }
1386
+ });
1387
+
1388
+ // 立即返回响应,不等待刷新完成
1389
+ return res.json({
1390
+ success: true,
1391
+ message: '刷新请求已接受,正在后台处理'
1392
+ });
1393
+ } catch (error) {
1394
+ logger.error('触发刷新Cookie失败:', error);
1395
+
1396
+ // 更新刷新状态
1397
+ refreshStatus.isRunning = false;
1398
+ refreshStatus.status = 'failed';
1399
+ refreshStatus.endTime = new Date();
1400
+ refreshStatus.error = error.message;
1401
+ refreshStatus.message = `触发刷新失败: ${error.message}`;
1402
+
1403
+ return res.status(500).json({
1404
+ success: false,
1405
+ message: `触发刷新失败: ${error.message}`
1406
+ });
1407
+ }
1408
+ });
1409
+
1410
+ // 查询Cookie刷新状态
1411
+ router.get("/refresh-status", (req, res) => {
1412
+ try {
1413
+ // 返回当前刷新状态
1414
+ return res.json({
1415
+ success: true,
1416
+ data: {
1417
+ ...refreshStatus,
1418
+ isRunning: refreshStatus.isRunning || false,
1419
+ status: refreshStatus.status || 'unknown',
1420
+ message: refreshStatus.message || '未触发刷新',
1421
+ startTime: refreshStatus.startTime || null,
1422
+ endTime: refreshStatus.endTime || null
1423
+ }
1424
+ });
1425
+ } catch (error) {
1426
+ logger.error('获取刷新状态失败:', error);
1427
+ return res.status(500).json({
1428
+ success: false,
1429
+ message: `获取刷新状态失败: ${error.message}`
1430
+ });
1431
+ }
1432
+ });
1433
+
1434
+ // 生成获取Cookie的链接
1435
+ router.post('/generate-cookie-link', async (req, res) => {
1436
+ try {
1437
+ // 验证管理员权限
1438
+ const authHeader = req.headers.authorization;
1439
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
1440
+ return res.status(401).json({
1441
+ success: false,
1442
+ message: '未提供认证token'
1443
+ });
1444
+ }
1445
+
1446
+ const token = authHeader.split(' ')[1];
1447
+ const authResult = admin.verifyToken(token);
1448
+
1449
+ if (!authResult.success) {
1450
+ return res.status(401).json({
1451
+ success: false,
1452
+ message: '认证失败'
1453
+ });
1454
+ }
1455
+
1456
+ // 生成UUID和PKCE验证器
1457
+ const uuid = uuidv4();
1458
+ const verifier = crypto.randomBytes(32).toString('base64url');
1459
+ const challenge = crypto.createHash('sha256').update(verifier).digest('base64url');
1460
+
1461
+ // 生成登录链接
1462
+ const loginUrl = `https://www.cursor.com/ja/loginDeepControl?challenge=${challenge}&uuid=${uuid}&mode=login`;
1463
+
1464
+ // 记录请求信息
1465
+ pendingCookieRequests.set(uuid, {
1466
+ uuid,
1467
+ verifier,
1468
+ status: 'waiting',
1469
+ created: Date.now(),
1470
+ apiKey: req.body.apiKey || '', // 目标API Key,空字符串表示所有API Key
1471
+ lastCheck: Date.now(),
1472
+ cookie: null
1473
+ });
1474
+
1475
+ // 设置60分钟后自动清理
1476
+ setTimeout(() => {
1477
+ if (pendingCookieRequests.has(uuid)) {
1478
+ pendingCookieRequests.delete(uuid);
1479
+ }
1480
+ }, 60 * 60 * 1000);
1481
+
1482
+ return res.json({
1483
+ success: true,
1484
+ url: loginUrl,
1485
+ uuid: uuid
1486
+ });
1487
+ } catch (error) {
1488
+ logger.error('生成Cookie链接失败:', error);
1489
+ return res.status(500).json({
1490
+ success: false,
1491
+ message: error.message
1492
+ });
1493
+ }
1494
+ });
1495
+
1496
+ // 查询Cookie获取状态
1497
+ router.get('/check-cookie-status', async (req, res) => {
1498
+ try {
1499
+ const { uuid } = req.query;
1500
+
1501
+ if (!uuid || !pendingCookieRequests.has(uuid)) {
1502
+ return res.json({
1503
+ success: false,
1504
+ status: 'failed',
1505
+ message: '无效的UUID或请求已过期'
1506
+ });
1507
+ }
1508
+
1509
+ const request = pendingCookieRequests.get(uuid);
1510
+ request.lastCheck = Date.now();
1511
+
1512
+ // 检查状态
1513
+ if (request.status === 'waiting') {
1514
+ // 检查Cursor API获取token
1515
+ try {
1516
+ const apiUrl = `https://api2.cursor.sh/auth/poll?uuid=${uuid}&verifier=${request.verifier}`;
1517
+ const response = await fetch(apiUrl, {
1518
+ method: 'GET',
1519
+ headers: {
1520
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.210 Safari/537.36',
1521
+ 'Accept': '*/*',
1522
+ 'Origin': 'vscode-file://vscode-app',
1523
+ 'x-ghost-mode': 'true'
1524
+ },
1525
+ timeout: 5000
1526
+ });
1527
+
1528
+ if (response.ok) {
1529
+ const data = await response.json();
1530
+
1531
+ if (data && data.accessToken) {
1532
+ // 获取到了Cookie
1533
+ request.cookie = data.accessToken;
1534
+ request.status = 'success';
1535
+
1536
+ // 将Cookie添加到目标API Key
1537
+ let message = '';
1538
+
1539
+ if (request.apiKey) {
1540
+ // 添加到特定API Key
1541
+ const apiKey = request.apiKey;
1542
+ const cookies = keyManager.getAllCookiesForApiKey(apiKey) || [];
1543
+ cookies.push(request.cookie);
1544
+ keyManager.addOrUpdateApiKey(apiKey, cookies);
1545
+ message = `Cookie已添加到API Key: ${apiKey}`;
1546
+ } else {
1547
+ // 添加到所有API Key
1548
+ const apiKeys = keyManager.getAllApiKeys();
1549
+ for (const apiKey of apiKeys) {
1550
+ const cookies = keyManager.getAllCookiesForApiKey(apiKey) || [];
1551
+ cookies.push(request.cookie);
1552
+ keyManager.addOrUpdateApiKey(apiKey, cookies);
1553
+ }
1554
+ message = `Cookie已添加到所有API Key,共${apiKeys.length}个`;
1555
+ }
1556
+
1557
+ // 完成后从等待列表中移除
1558
+ pendingCookieRequests.delete(uuid);
1559
+
1560
+ return res.json({
1561
+ success: true,
1562
+ message: message
1563
+ });
1564
+ }
1565
+ }
1566
+
1567
+ // 如果没有获取到Cookie,继续等待
1568
+ return res.json({
1569
+ success: false,
1570
+ status: 'waiting'
1571
+ });
1572
+
1573
+ } catch (error) {
1574
+ logger.error('查询Cursor API失败:', error);
1575
+ // 发生错误但继续等待,不改变状态
1576
+ return res.json({
1577
+ success: false,
1578
+ status: 'waiting',
1579
+ message: '轮询过程中出现错误,继续等待'
1580
+ });
1581
+ }
1582
+ } else if (request.status === 'success') {
1583
+ // 已成功,返回结果
1584
+ const message = request.apiKey
1585
+ ? `Cookie已添加到API Key: ${request.apiKey}`
1586
+ : `Cookie已添加到所有API Key`;
1587
+
1588
+ // 完成后从等待列表中移除
1589
+ pendingCookieRequests.delete(uuid);
1590
+
1591
+ return res.json({
1592
+ success: true,
1593
+ message: message
1594
+ });
1595
+ } else {
1596
+ // 失败
1597
+ pendingCookieRequests.delete(uuid);
1598
+ return res.json({
1599
+ success: false,
1600
+ status: 'failed',
1601
+ message: '获取Cookie失败'
1602
+ });
1603
+ }
1604
+ } catch (error) {
1605
+ logger.error('检查Cookie状态失败:', error);
1606
+ return res.status(500).json({
1607
+ success: false,
1608
+ status: 'failed',
1609
+ message: error.message
1610
+ });
1611
+ }
1612
+ });
1613
+
1614
+ // 获取日志API
1615
+ router.get("/logs", (req, res) => {
1616
+ try {
1617
+ // 获取查询参数
1618
+ const level = req.query.level;
1619
+ const search = req.query.search;
1620
+ const page = parseInt(req.query.page) || 1;
1621
+ const pageSize = parseInt(req.query.pageSize) || 100;
1622
+ const startTime = req.query.startTime;
1623
+ const endTime = req.query.endTime;
1624
+
1625
+ // 过滤参数
1626
+ const filter = {
1627
+ level,
1628
+ search,
1629
+ page,
1630
+ pageSize,
1631
+ startTime,
1632
+ endTime
1633
+ };
1634
+
1635
+ // 获取日志
1636
+ const logs = logger.getLogs(filter);
1637
+
1638
+ return res.json({
1639
+ success: true,
1640
+ data: logs
1641
+ });
1642
+ } catch (error) {
1643
+ logger.error('获取日志失败:', error);
1644
+ return res.status(500).json({
1645
+ success: false,
1646
+ message: `获取日志失败: ${error.message}`
1647
+ });
1648
+ }
1649
+ });
1650
+
1651
+ // 清除内存日志
1652
+ router.delete("/logs", (req, res) => {
1653
+ try {
1654
+ logger.clearMemoryLogs();
1655
+ return res.json({
1656
+ success: true,
1657
+ message: '日志已清除'
1658
+ });
1659
+ } catch (error) {
1660
+ logger.error('清除日志失败:', error);
1661
+ return res.status(500).json({
1662
+ success: false,
1663
+ message: `清除日志失败: ${error.message}`
1664
+ });
1665
+ }
1666
+ });
1667
+ async function others(authToken, clientKey, checksum, cursorClientVersion, sessionid){
1668
+ try {
1669
+ // 定义所有API端点配置
1670
+ const endpoints = [
1671
+ {
1672
+ url: 'https://api2.cursor.sh/aiserver.v1.AiService/CheckFeatureStatus',
1673
+ method: 'POST',
1674
+ headers: {
1675
+ 'accept-encoding': 'gzip',
1676
+ 'authorization': `Bearer ${authToken}`,
1677
+ 'connect-protocol-version': '1',
1678
+ 'content-type': 'application/proto',
1679
+ 'user-agent': 'connect-es/1.6.1',
1680
+ 'x-client-key': clientKey,
1681
+ 'x-cursor-checksum': checksum,
1682
+ 'x-cursor-client-version': cursorClientVersion,
1683
+ 'x-cursor-config-version': uuidv4(),
1684
+ 'x-cursor-timezone': 'Asia/Tokyo',
1685
+ 'x-ghost-mode': 'true',
1686
+ 'x-new-onboarding-completed': 'false',
1687
+ 'x-session-id': sessionid,
1688
+ 'Host': 'api2.cursor.sh',
1689
+ },
1690
+ body: '', // 实际长度为23字节
1691
+ timeout: {
1692
+ connect: 5000,
1693
+ read: 30000
1694
+ }
1695
+ },
1696
+ {
1697
+ url: 'https://api2.cursor.sh/aiserver.v1.AiService/AvailableDocs',
1698
+ method: 'POST',
1699
+ headers: {
1700
+ 'authorization': `Bearer ${authToken}`,
1701
+ 'connect-accept-encoding': 'gzip',
1702
+ 'connect-protocol-version': '1',
1703
+ 'content-type': 'application/proto',
1704
+ 'user-agent': 'connect-es/1.6.1',
1705
+ 'x-amzn-trace-id': `Root=${uuidv4()}`,
1706
+ 'x-client-key': clientKey,
1707
+ 'x-cursor-checksum': checksum,
1708
+ 'x-cursor-client-version': cursorClientVersion,
1709
+ 'x-cursor-config-version': uuidv4(),
1710
+ 'x-cursor-timezone': 'Asia/Tokyo',
1711
+ 'x-ghost-mode': 'true',
1712
+ 'x-request-id': uuidv4(),
1713
+ 'x-session-id': sessionid,
1714
+ 'Host': 'api2.cursor.sh',
1715
+ },
1716
+ timeout: {
1717
+ connect: 5000,
1718
+ read: 30000
1719
+ }
1720
+ },
1721
+ {
1722
+ url: 'https://api2.cursor.sh/aiserver.v1.DashboardService/GetTeams',
1723
+ method: 'POST',
1724
+ headers: {
1725
+ 'accept-encoding': 'gzip',
1726
+ 'authorization': `Bearer ${authToken}`,
1727
+ 'connect-protocol-version': '1',
1728
+ 'content-type': 'application/proto',
1729
+ 'user-agent': 'connect-es/1.6.1',
1730
+ 'x-amzn-trace-id': `Root=${uuidv4()}`,
1731
+ 'x-client-key': clientKey,
1732
+ 'x-cursor-checksum': checksum,
1733
+ 'x-cursor-client-version': cursorClientVersion,
1734
+ 'x-cursor-config-version': uuidv4(),
1735
+ 'x-cursor-timezone': 'Asia/Tokyo',
1736
+ 'x-ghost-mode': 'true',
1737
+ 'x-new-onboarding-completed': 'false',
1738
+ 'x-request-id': uuidv4(),
1739
+ 'x-session-id': sessionid,
1740
+ 'Host': 'api2.cursor.sh',
1741
+ },
1742
+ body: '',
1743
+ timeout: {
1744
+ connect: 5000,
1745
+ read: 30000
1746
+ }
1747
+ },
1748
+ {
1749
+ url: 'https://api2.cursor.sh/auth/full_stripe_profile',
1750
+ method: 'GET',
1751
+ headers: {
1752
+ 'Host': 'api2.cursor.sh',
1753
+ 'Connection': 'keep-alive',
1754
+ 'Authorization': `Bearer ${authToken}`,
1755
+ 'x-new-onboarding-completed': 'false',
1756
+ 'x-ghost-mode': 'true',
1757
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Cursor/0.49.4 Chrome/132.0.6834.210 Electron/34.3.4 Safari/537.36',
1758
+ 'Accept': '*/*',
1759
+ 'Origin': 'vscode-file://vscode-app',
1760
+ 'Sec-Fetch-Site': 'cross-site',
1761
+ 'Sec-Fetch-Mode': 'cors',
1762
+ 'Sec-Fetch-Dest': 'empty',
1763
+ 'Accept-Encoding': 'gzip, deflate, br, zstd',
1764
+ 'Accept-Language': 'zh-CN'
1765
+ },
1766
+ timeout: {
1767
+ connect: 5000,
1768
+ read: 30000
1769
+ }
1770
+ },
1771
+ {
1772
+ url: 'https://api2.cursor.sh/aiserver.v1.DashboardService/GetUsageBasedPremiumRequests',
1773
+ method: 'POST',
1774
+ headers: {
1775
+ 'accept-encoding': 'gzip',
1776
+ 'authorization': `Bearer ${authToken}`,
1777
+ 'connect-protocol-version': '1',
1778
+ 'content-type': 'application/proto',
1779
+ 'user-agent': 'connect-es/1.6.1',
1780
+ 'x-client-key': clientKey,
1781
+ 'x-cursor-checksum': checksum,
1782
+ 'x-cursor-client-version': cursorClientVersion,
1783
+ 'x-cursor-config-version': uuidv4(),
1784
+ 'x-cursor-timezone': 'Asia/Tokyo',
1785
+ 'x-ghost-mode': 'true',
1786
+ 'x-new-onboarding-completed': 'false',
1787
+ 'x-session-id': sessionid,
1788
+ 'Host': 'api2.cursor.sh',
1789
+ },
1790
+ body: '',
1791
+ timeout: {
1792
+ connect: 5000,
1793
+ read: 30000
1794
+ }
1795
+ },
1796
+ {
1797
+ url: 'https://api2.cursor.sh/aiserver.v1.DashboardService/GetHardLimit',
1798
+ method: 'POST',
1799
+ headers: {
1800
+ 'accept-encoding': 'gzip',
1801
+ 'authorization': `Bearer ${authToken}`,
1802
+ 'connect-protocol-version': '1',
1803
+ 'content-type': 'application/proto',
1804
+ 'user-agent': 'connect-es/1.6.1',
1805
+ 'x-client-key': clientKey,
1806
+ 'x-cursor-checksum': checksum,
1807
+ 'x-cursor-client-version': cursorClientVersion,
1808
+ 'x-cursor-config-version': uuidv4(),
1809
+ 'x-cursor-timezone': 'Asia/Tokyo',
1810
+ 'x-ghost-mode': 'true',
1811
+ 'x-new-onboarding-completed': 'false',
1812
+ 'x-session-id': sessionid,
1813
+ 'Host': 'api2.cursor.sh',
1814
+ },
1815
+ body: '',
1816
+ timeout: {
1817
+ connect: 5000,
1818
+ read: 30000
1819
+ }
1820
+ }
1821
+ ];
1822
+
1823
+ // 随机选择2-4个接口调用
1824
+ const minApis = 2;
1825
+ const maxApis = 4;
1826
+ const numApisToCall = Math.floor(Math.random() * (maxApis - minApis + 1)) + minApis;
1827
+
1828
+ // 随机打乱数组并取前几个元素
1829
+ const shuffledEndpoints = [...endpoints].sort(() => 0.5 - Math.random()).slice(0, numApisToCall);
1830
+
1831
+ // 使用Promise.allSettled确保即使一个请求失败也不会影响其他请求
1832
+ const results = await Promise.allSettled(shuffledEndpoints.map(async (endpoint) => {
1833
+ try {
1834
+ const response = await fetch(endpoint.url, {
1835
+ method: endpoint.method,
1836
+ headers: endpoint.headers,
1837
+ body: endpoint.body || undefined,
1838
+ timeout: endpoint.timeout
1839
+ });
1840
+
1841
+ return {
1842
+ url: endpoint.url,
1843
+ status: response.status,
1844
+ success: true
1845
+ };
1846
+ } catch (error) {
1847
+ // 记录单个请求的错误,但不中断整体流程
1848
+ logger.debug(`其它API调用失败 (${endpoint.url}): ${error.message}`);
1849
+ return {
1850
+ url: endpoint.url,
1851
+ success: false,
1852
+ error: error.message
1853
+ };
1854
+ }
1855
+ }));
1856
+
1857
+ // 记录请求结果统计
1858
+ const successCount = results.filter(r => r.status === 'fulfilled' && r.value.success).length;
1859
+ logger.debug(`其它API调用完成: 成功 ${successCount}/${results.length}`);
1860
+
1861
+ return true;
1862
+ } catch (error) {
1863
+ // 记录整体错误,但不影响主流程
1864
+ logger.error(`others函数执行出错: ${error.message}`);
1865
+ return false;
1866
+ }
1867
+ }
1868
+ // 在文件末尾添加错误处理函数
1869
+ function handleCursorError(errorStr, bearerToken, originalAuthToken) {
1870
+ let message = '';
1871
+ let shouldRemoveCookie = false;
1872
+
1873
+ if (errorStr.includes('Not logged in')) {
1874
+ // 更明确的错误日志
1875
+ if (originalAuthToken === bearerToken) {
1876
+ logger.error(`检测到API Key "${bearerToken}" 中没有可用Cookie,正在尝试以向后兼容模式使用API Key本身`);
1877
+ message = `错误:API Key "${bearerToken}" 中没有可用的Cookie。请添加有效的Cookie到此API Key,或使用其他有效的API Key。\\n\\n详细信息:${errorStr}`;
1878
+ } else {
1879
+ logger.error('检测到无效cookie:', originalAuthToken);
1880
+ message = `错误:Cookie无效或已过期,请更新Cookie。\\n\\n详细信息:${errorStr}`;
1881
+ }
1882
+ shouldRemoveCookie = true;
1883
+ } else if (errorStr.includes('You\'ve reached your trial request limit') || errorStr.includes('You\'ve reached the usage limit for free usage')) {
1884
+ logger.error('检测到额度用尽cookie:', originalAuthToken);
1885
+ message = `错误:Cookie使用额度已用完,请更换Cookie或等待刷新。\\n\\n详细信息:${errorStr}`;
1886
+ shouldRemoveCookie = true;
1887
+ } else if (errorStr.includes('User is unauthorized')) {
1888
+ logger.error('检测到未授权cookie:', originalAuthToken);
1889
+ message = `错误:Cookie已被封禁或失效,请更换Cookie。\\n\\n详细信息:${errorStr}`;
1890
+ shouldRemoveCookie = true;
1891
+ } else if (errorStr.includes('suspicious activity checks')) {
1892
+ logger.error('检测到IP黑名单:', originalAuthToken);
1893
+ message = `错误:IP可能被列入黑名单,请尝试更换网络环境或使用代理。\\n\\n详细信息:${errorStr}`;
1894
+ shouldRemoveCookie = false;
1895
+ } else if (errorStr.includes('Too many computers')) {
1896
+ logger.error('检测到账户暂时被封禁:', originalAuthToken);
1897
+ message = `错误:账户因在多台设备登录而暂时被封禁,请稍后再试或更换账户。\\n\\n详细信息:${errorStr}`;
1898
+ shouldRemoveCookie = true;
1899
+ } else if (errorStr.includes('Login expired') || errorStr.includes('login expired')) {
1900
+ logger.error('检测到登录过期cookie:', originalAuthToken);
1901
+ message = `错误:Cookie登录已过期,请更新Cookie。\\n\\n详细信息:${errorStr}`;
1902
+ shouldRemoveCookie = true;
1903
+ } else if(errorStr.includes('your request has been blocked due to the use of a temporary email service for this account')) {
1904
+ logger.error('检测到临时邮箱:', originalAuthToken);
1905
+ message = `错误:请求被阻止,检测到临时邮箱服务,请更换邮箱。\\n\\n详细信息:${errorStr}`;
1906
+ shouldRemoveCookie = true;
1907
+ } else if (errorStr.includes('Your request has been blocked as our system has detected suspicious activity from your account')) {
1908
+ logger.error('检测到账户异常:', originalAuthToken);
1909
+ message = `错误:请求被阻止,可能是假ban,多重试几次/更换cookie/更换设备。\\n\\n详细信息:${errorStr}`;
1910
+ shouldRemoveCookie = false;
1911
+ } else {
1912
+ // 非Cookie相关错误
1913
+ logger.error('检测到其他错误:', errorStr);
1914
+ message = `错误:请求失败。\\n\\n详细信息:${errorStr}`;
1915
+ shouldRemoveCookie = false;
1916
+ }
1917
+
1918
+ return {
1919
+ message,
1920
+ shouldRemoveCookie
1921
+ };
1922
+ }
1923
+
1924
+ module.exports = router;
src/utils/cookieRefresher.js ADDED
@@ -0,0 +1,818 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const csv = require('csv-parser');
4
+ const axios = require('axios');
5
+ const AdmZip = require('adm-zip');
6
+ const { Octokit } = require('@octokit/rest');
7
+ const keyManager = require('./keyManager');
8
+ const config = require('../config/config');
9
+ const { extractCookiesFromCsv } = require('./extractCookieFromCsv');
10
+ const logger = require('./logger');
11
+
12
+ // GitHub 仓库信息从环境变量中获取
13
+ const GITHUB_OWNER = process.env.GITHUB_OWNER || 'liuw1535';
14
+ const GITHUB_REPO = process.env.GITHUB_REPO || 'Cursor-Register';
15
+ const GITHUB_TOKEN = process.env.GITHUB_TOKEN; // 需要在环境变量中设置
16
+ const GITHUB_WORKFLOW_ID = process.env.GITHUB_WORKFLOW_ID || 'register.yml';
17
+ const TRIGGER_WORKFLOW = process.env.TRIGGER_WORKFLOW === 'true';
18
+
19
+ // 下载目录
20
+ const DOWNLOAD_DIR = path.join(__dirname, '../../downloads');
21
+ const EXTRACT_DIR = path.join(__dirname, '../../extracted');
22
+
23
+ // 确保目录存在
24
+ function ensureDirectoryExists(dir) {
25
+ if (!fs.existsSync(dir)) {
26
+ try {
27
+ fs.mkdirSync(dir, { recursive: true });
28
+ logger.info(`创建目录成功: ${dir}`);
29
+ } catch (err) {
30
+ logger.error(`创建目录失败: ${dir}`, err);
31
+ throw err;
32
+ }
33
+ }
34
+ }
35
+
36
+ // 触发 GitHub Actions 工作流
37
+ async function triggerWorkflow() {
38
+ try {
39
+ if (!GITHUB_TOKEN) {
40
+ logger.error('未设置 GITHUB_TOKEN,无法触发工作流');
41
+ return null;
42
+ }
43
+
44
+ logger.info(`正在触发 GitHub Actions 工作流: ${GITHUB_WORKFLOW_ID}...`);
45
+ const octokit = new Octokit({
46
+ auth: GITHUB_TOKEN
47
+ });
48
+
49
+ // 从环境变量获取工作流参数
50
+ const number = process.env.REGISTER_NUMBER || '2';
51
+ const maxWorkers = process.env.REGISTER_MAX_WORKERS || '1';
52
+ const emailServer = process.env.REGISTER_EMAIL_SERVER || 'TempEmail';
53
+ const ingestToOneapi = process.env.REGISTER_INGEST_TO_ONEAPI === 'true';
54
+ const uploadArtifact = process.env.REGISTER_UPLOAD_ARTIFACT !== 'false'; // 默认为true
55
+ const useConfigFile = process.env.REGISTER_USE_CONFIG_FILE !== 'false'; // 默认为true
56
+ const emailConfigs = process.env.REGISTER_EMAIL_CONFIGS || '[]';
57
+
58
+ logger.info(`工作流参数: number=${number}, maxWorkers=${maxWorkers}, emailServer=${emailServer}, ingestToOneapi=${ingestToOneapi}, uploadArtifact=${uploadArtifact}, useConfigFile=${useConfigFile}`);
59
+
60
+ // 获取触发前的最新工作流ID,用于后续识别新触发的工作流
61
+ const { data: beforeWorkflowRuns } = await octokit.actions.listWorkflowRuns({
62
+ owner: GITHUB_OWNER,
63
+ repo: GITHUB_REPO,
64
+ workflow_id: GITHUB_WORKFLOW_ID,
65
+ per_page: 1
66
+ });
67
+
68
+ const latestWorkflowIdBefore = beforeWorkflowRuns.workflow_runs && beforeWorkflowRuns.workflow_runs.length > 0
69
+ ? beforeWorkflowRuns.workflow_runs[0].id
70
+ : 0;
71
+
72
+ logger.info(`触发前最新工作流ID: ${latestWorkflowIdBefore}`);
73
+
74
+ // 触发工作流
75
+ const response = await octokit.actions.createWorkflowDispatch({
76
+ owner: GITHUB_OWNER,
77
+ repo: GITHUB_REPO,
78
+ workflow_id: GITHUB_WORKFLOW_ID,
79
+ ref: 'main', // 默认使用 main 分支,可以根据需要修改
80
+ inputs: {
81
+ number: number,
82
+ max_workers: maxWorkers,
83
+ email_server: emailServer,
84
+ ingest_to_oneapi: ingestToOneapi.toString(),
85
+ upload_artifact: uploadArtifact.toString(),
86
+ use_config_file: useConfigFile.toString(),
87
+ email_configs: emailConfigs
88
+ }
89
+ });
90
+
91
+ logger.info('工作流触发成功,等待工作流开始运行...');
92
+
93
+ // 等待新工作流出现并获取其ID
94
+ let newWorkflowRunId = null;
95
+ let findAttempts = 0;
96
+ const maxFindAttempts = 30; // 最多等待30次,每次5秒
97
+
98
+ while (findAttempts < maxFindAttempts && !newWorkflowRunId) {
99
+ findAttempts++;
100
+ logger.info(`查找新触发的工作流,尝试 ${findAttempts}/${maxFindAttempts}...`);
101
+
102
+ try {
103
+ const { data: afterWorkflowRuns } = await octokit.actions.listWorkflowRuns({
104
+ owner: GITHUB_OWNER,
105
+ repo: GITHUB_REPO,
106
+ workflow_id: GITHUB_WORKFLOW_ID,
107
+ per_page: 5
108
+ });
109
+
110
+ if (afterWorkflowRuns.workflow_runs && afterWorkflowRuns.workflow_runs.length > 0) {
111
+ // 查找ID大于之前最新工作流ID的工作流(即新触发的工作流)
112
+ const newWorkflow = afterWorkflowRuns.workflow_runs.find(run => run.id > latestWorkflowIdBefore);
113
+ if (newWorkflow) {
114
+ newWorkflowRunId = newWorkflow.id;
115
+ logger.info(`找到新触发的工作流,ID: ${newWorkflowRunId}, 状态: ${newWorkflow.status}`);
116
+ }
117
+ }
118
+ } catch (error) {
119
+ logger.error(`查找工作流时出错 (尝试 ${findAttempts}/${maxFindAttempts}): ${error.message}`);
120
+ // 出错时继续尝试,不中断循环
121
+ }
122
+
123
+ if (!newWorkflowRunId) {
124
+ // 等待5秒后再次检查
125
+ await new Promise(resolve => setTimeout(resolve, 5000));
126
+ }
127
+ }
128
+
129
+ if (!newWorkflowRunId) {
130
+ logger.info('未能找到新触发的工作流,可能触发失败');
131
+ return null;
132
+ }
133
+
134
+ // 等待工作流完成
135
+ let attempts = 0;
136
+ const maxAttempts = 120; // 最多等待120次,每次30秒,总共60分钟
137
+ let consecutiveErrors = 0;
138
+ const maxConsecutiveErrors = 5; // 最多允许连续5次错误
139
+
140
+ while (attempts < maxAttempts) {
141
+ attempts++;
142
+ logger.info(`等待工作流完成,尝试 ${attempts}/${maxAttempts}...`);
143
+
144
+ try {
145
+ // 获取工作流状态
146
+ const { data: workflowRun } = await octokit.actions.getWorkflowRun({
147
+ owner: GITHUB_OWNER,
148
+ repo: GITHUB_REPO,
149
+ run_id: newWorkflowRunId
150
+ });
151
+
152
+ // 重置连续错误计数
153
+ consecutiveErrors = 0;
154
+
155
+ logger.info(`工作流状态: ${workflowRun.status}, 结果: ${workflowRun.conclusion || '进行中'}`);
156
+
157
+ // 检查工作流是否完成
158
+ if (workflowRun.status === 'completed') {
159
+ if (workflowRun.conclusion === 'success') {
160
+ logger.info(`工作流运行成功,ID: ${newWorkflowRunId}`);
161
+ return workflowRun;
162
+ } else {
163
+ logger.info(`工作流运行失败,结果: ${workflowRun.conclusion}`);
164
+ return null;
165
+ }
166
+ }
167
+ } catch (error) {
168
+ consecutiveErrors++;
169
+ logger.error(`获取工作流状态时出错 (尝试 ${attempts}/${maxAttempts}, 连续错误 ${consecutiveErrors}/${maxConsecutiveErrors}): ${error.message}`);
170
+
171
+ // 如果连续错误次数超过阈值,则放弃
172
+ if (consecutiveErrors >= maxConsecutiveErrors) {
173
+ logger.error(`连续错误次数超过阈值 (${maxConsecutiveErrors}),放弃等待`);
174
+ throw new Error(`连续 ${maxConsecutiveErrors} 次获取工作流状态失败: ${error.message}`);
175
+ }
176
+
177
+ // 错误后等待时间稍微延长
178
+ await new Promise(resolve => setTimeout(resolve, 10000));
179
+ // 继续循环,不中断
180
+ continue;
181
+ }
182
+
183
+ // 等待30秒后再次检查
184
+ await new Promise(resolve => setTimeout(resolve, 30000));
185
+ }
186
+
187
+ logger.info('等待工作流完成超时');
188
+ return null;
189
+ } catch (error) {
190
+ logger.error('触发工作流失败:', error);
191
+ throw error; // 重新抛出错误,让调用者处理
192
+ }
193
+ }
194
+
195
+ // 从 GitHub Actions 获取最新的 Artifact
196
+ async function getLatestArtifact() {
197
+ try {
198
+ logger.info('正在连接 GitHub API...');
199
+ const octokit = new Octokit({
200
+ auth: GITHUB_TOKEN
201
+ });
202
+
203
+ // 如果配置了自动触发工作流,则先触发工作流
204
+ let workflowRun = null;
205
+ if (TRIGGER_WORKFLOW) {
206
+ logger.info('配置了自动触发工作流,正在触发...');
207
+ try {
208
+ workflowRun = await triggerWorkflow();
209
+ } catch (error) {
210
+ logger.error('触发工作流过程中出现错误:', error.message);
211
+ logger.info('尝试继续使用已找到的工作流ID...');
212
+
213
+ // 尝试获取最新的工作流,看是否有正在运行的工作流
214
+ const { data: runningWorkflows } = await octokit.actions.listWorkflowRuns({
215
+ owner: GITHUB_OWNER,
216
+ repo: GITHUB_REPO,
217
+ workflow_id: GITHUB_WORKFLOW_ID,
218
+ status: 'in_progress',
219
+ per_page: 5
220
+ });
221
+
222
+ if (runningWorkflows.workflow_runs && runningWorkflows.workflow_runs.length > 0) {
223
+ // 找到正在运行的工作流
224
+ const runningWorkflow = runningWorkflows.workflow_runs[0];
225
+ logger.info(`找到正在运行的工作流,ID: ${runningWorkflow.id}, 状态: ${runningWorkflow.status}`);
226
+
227
+ // 等待工作流完成
228
+ let attempts = 0;
229
+ const maxAttempts = 120; // 最多等待120次,每次30秒,总共60分钟
230
+ let consecutiveErrors = 0;
231
+ const maxConsecutiveErrors = 5; // 最多允许连续5次错误
232
+
233
+ while (attempts < maxAttempts) {
234
+ attempts++;
235
+ logger.info(`等待工作流完成,尝试 ${attempts}/${maxAttempts}...`);
236
+
237
+ try {
238
+ // 获取工作流状态
239
+ const { data: currentWorkflow } = await octokit.actions.getWorkflowRun({
240
+ owner: GITHUB_OWNER,
241
+ repo: GITHUB_REPO,
242
+ run_id: runningWorkflow.id
243
+ });
244
+
245
+ // 重置连续错误计数
246
+ consecutiveErrors = 0;
247
+
248
+ logger.info(`工作流状态: ${currentWorkflow.status}, 结果: ${currentWorkflow.conclusion || '进行中'}`);
249
+
250
+ // 检查工作流是否完成
251
+ if (currentWorkflow.status === 'completed') {
252
+ if (currentWorkflow.conclusion === 'success') {
253
+ logger.info(`工作流运行成功,ID: ${currentWorkflow.id}`);
254
+ workflowRun = currentWorkflow;
255
+ break;
256
+ } else {
257
+ logger.info(`工作流运行失败,结果: ${currentWorkflow.conclusion}`);
258
+ break;
259
+ }
260
+ }
261
+ } catch (err) {
262
+ consecutiveErrors++;
263
+ logger.error(`获取工作流状态时出错 (尝试 ${attempts}/${maxAttempts}, 连续错误 ${consecutiveErrors}/${maxConsecutiveErrors}): ${err.message}`);
264
+
265
+ // 如果连续错误次数超过阈值,则放弃
266
+ if (consecutiveErrors >= maxConsecutiveErrors) {
267
+ logger.error(`连续错误次数超过阈值 (${maxConsecutiveErrors}),放弃等待`);
268
+ break;
269
+ }
270
+
271
+ // 错误后等待时间稍微延长
272
+ await new Promise(resolve => setTimeout(resolve, 10000));
273
+ // 继续循环,不中断
274
+ continue;
275
+ }
276
+
277
+ // 等待30秒后再次检查
278
+ await new Promise(resolve => setTimeout(resolve, 30000));
279
+ }
280
+ }
281
+ }
282
+
283
+ if (!workflowRun) {
284
+ logger.info('触发工作流失败或等待超时,尝试获取最新的工作流运行');
285
+ }
286
+ }
287
+
288
+ // 如果没有触发工作流或触发失败,则获取最新的工作流运行
289
+ if (!workflowRun) {
290
+ logger.info('获取最新的工作流运行...');
291
+ const { data: workflowRuns } = await octokit.actions.listWorkflowRunsForRepo({
292
+ owner: GITHUB_OWNER,
293
+ repo: GITHUB_REPO,
294
+ status: 'success',
295
+ per_page: 5
296
+ });
297
+
298
+ if (!workflowRuns.workflow_runs || workflowRuns.workflow_runs.length === 0) {
299
+ logger.info('没有找到成功的工作流运行');
300
+ return null;
301
+ }
302
+
303
+ // 获取最新成功运行的 Artifacts
304
+ workflowRun = workflowRuns.workflow_runs[0];
305
+ }
306
+
307
+ logger.info(`找到最新的工作流运行: ${workflowRun.id}`);
308
+
309
+ // 等待一段时间,确保Artifact已经上传完成
310
+ logger.info('等待Artifact上传完成...');
311
+ await new Promise(resolve => setTimeout(resolve, 10000));
312
+
313
+ // 获取工作流的Artifacts
314
+ let artifacts = null;
315
+ let artifactAttempts = 0;
316
+ const maxArtifactAttempts = 10; // 最多尝试10次,每次10秒
317
+
318
+ while (artifactAttempts < maxArtifactAttempts && (!artifacts || !artifacts.artifacts || artifacts.artifacts.length === 0)) {
319
+ artifactAttempts++;
320
+ logger.info(`尝试获取Artifacts,尝试 ${artifactAttempts}/${maxArtifactAttempts}...`);
321
+
322
+ try {
323
+ const response = await octokit.actions.listWorkflowRunArtifacts({
324
+ owner: GITHUB_OWNER,
325
+ repo: GITHUB_REPO,
326
+ run_id: workflowRun.id
327
+ });
328
+
329
+ artifacts = response.data;
330
+ } catch (error) {
331
+ logger.error(`获取Artifacts时出错 (尝试 ${artifactAttempts}/${maxArtifactAttempts}): ${error.message}`);
332
+ // 出错时继续尝试,不中断循环
333
+ }
334
+
335
+ if (!artifacts || !artifacts.artifacts || artifacts.artifacts.length === 0) {
336
+ logger.info('暂时没有找到Artifacts,等待10秒后重试...');
337
+ await new Promise(resolve => setTimeout(resolve, 10000));
338
+ }
339
+ }
340
+
341
+ if (!artifacts || !artifacts.artifacts || artifacts.artifacts.length === 0) {
342
+ logger.info('没有找到Artifacts,可能工作流没有生成Artifact');
343
+ return null;
344
+ }
345
+
346
+ logger.info(`找到 ${artifacts.artifacts.length} 个Artifacts`);
347
+
348
+ // 查找 Account info Artifact
349
+ const accountInfoArtifact = artifacts.artifacts.find(artifact =>
350
+ artifact.name.toLowerCase().includes('account info'));
351
+
352
+ if (!accountInfoArtifact) {
353
+ logger.info('没有找到 Account info Artifact');
354
+ return null;
355
+ }
356
+
357
+ logger.info(`找到 Account info Artifact: ${accountInfoArtifact.id}`);
358
+ return accountInfoArtifact;
359
+ } catch (error) {
360
+ logger.error('获取 Artifact 失败:', error);
361
+ return null;
362
+ }
363
+ }
364
+
365
+ // 下载 Artifact
366
+ async function downloadArtifact(artifact) {
367
+ let downloadAttempts = 0;
368
+ const maxDownloadAttempts = 5; // 最多尝试5次下载
369
+
370
+ while (downloadAttempts < maxDownloadAttempts) {
371
+ downloadAttempts++;
372
+ try {
373
+ logger.info(`开始下载 Artifact: ${artifact.id}... (尝试 ${downloadAttempts}/${maxDownloadAttempts})`);
374
+ ensureDirectoryExists(DOWNLOAD_DIR);
375
+
376
+ const octokit = new Octokit({
377
+ auth: GITHUB_TOKEN
378
+ });
379
+
380
+ // 获取下载 URL
381
+ const { url } = await octokit.actions.downloadArtifact({
382
+ owner: GITHUB_OWNER,
383
+ repo: GITHUB_REPO,
384
+ artifact_id: artifact.id,
385
+ archive_format: 'zip'
386
+ });
387
+
388
+ // 下载 zip 文件
389
+ const zipFilePath = path.join(DOWNLOAD_DIR, `${artifact.id}.zip`);
390
+ const response = await axios({
391
+ method: 'get',
392
+ url: url,
393
+ responseType: 'arraybuffer',
394
+ timeout: 60000 // 设置60秒超时
395
+ });
396
+
397
+ fs.writeFileSync(zipFilePath, response.data);
398
+ logger.info(`Artifact 下载完成: ${zipFilePath}`);
399
+ return zipFilePath;
400
+ } catch (error) {
401
+ logger.error(`下载 Artifact 失败 (尝试 ${downloadAttempts}/${maxDownloadAttempts}): ${error.message}`);
402
+
403
+ if (downloadAttempts >= maxDownloadAttempts) {
404
+ logger.error('达到最大尝试次数,放弃下载');
405
+ return null;
406
+ }
407
+
408
+ // 等待一段时间后重试
409
+ const retryDelay = 10000; // 10秒
410
+ logger.info(`等待 ${retryDelay/1000} 秒后重试...`);
411
+ await new Promise(resolve => setTimeout(resolve, retryDelay));
412
+ }
413
+ }
414
+
415
+ return null;
416
+ }
417
+
418
+ // 解压 Artifact
419
+ async function extractArtifact(zipFilePath) {
420
+ let extractAttempts = 0;
421
+ const maxExtractAttempts = 3; // 最多尝试3次解压
422
+
423
+ while (extractAttempts < maxExtractAttempts) {
424
+ extractAttempts++;
425
+ try {
426
+ logger.info(`开始解压 Artifact: ${zipFilePath}... (尝试 ${extractAttempts}/${maxExtractAttempts})`);
427
+ ensureDirectoryExists(EXTRACT_DIR);
428
+
429
+ const zip = new AdmZip(zipFilePath);
430
+ zip.extractAllTo(EXTRACT_DIR, true);
431
+ logger.info(`Artifact 解压完成: ${EXTRACT_DIR}`);
432
+
433
+ // 查找 token CSV 文件
434
+ const files = fs.readdirSync(EXTRACT_DIR);
435
+ const tokenFile = files.find(file => file.startsWith('token_') && file.endsWith('.csv'));
436
+
437
+ if (!tokenFile) {
438
+ logger.info('没有找到 token CSV 文件');
439
+
440
+ if (extractAttempts >= maxExtractAttempts) {
441
+ return null;
442
+ }
443
+
444
+ // 等待一段时间后重试
445
+ const retryDelay = 5000; // 5秒
446
+ logger.info(`等待 ${retryDelay/1000} 秒后重试...`);
447
+ await new Promise(resolve => setTimeout(resolve, retryDelay));
448
+ continue;
449
+ }
450
+
451
+ logger.info(`找到 token CSV 文件: ${tokenFile}`);
452
+ return path.join(EXTRACT_DIR, tokenFile);
453
+ } catch (error) {
454
+ logger.error(`解压 Artifact 失败 (尝试 ${extractAttempts}/${maxExtractAttempts}): ${error.message}`);
455
+
456
+ if (extractAttempts >= maxExtractAttempts) {
457
+ logger.error('达到最大尝试次数,放弃解压');
458
+ return null;
459
+ }
460
+
461
+ // 等待一段时间后重试
462
+ const retryDelay = 5000; // 5秒
463
+ logger.info(`等待 ${retryDelay/1000} 秒后重试...`);
464
+ await new Promise(resolve => setTimeout(resolve, retryDelay));
465
+ }
466
+ }
467
+
468
+ return null;
469
+ }
470
+
471
+ /**
472
+ * 从CSV文件中提取cookies
473
+ * @param {string} csvFilePath - CSV文件路径
474
+ * @returns {Promise<string[]>} - 提取到的cookie数组
475
+ */
476
+ async function extractCookiesFromCsvFile(csvFilePath) {
477
+ const maxExtractAttempts = 3;
478
+ let attempt = 1;
479
+
480
+ while (attempt <= maxExtractAttempts) {
481
+ logger.info(`尝试从CSV文件提取cookies (尝试 ${attempt}/${maxExtractAttempts})...`);
482
+
483
+ try {
484
+ // 读取文件内容
485
+ if (!fs.existsSync(csvFilePath)) {
486
+ logger.error(`CSV文件不存在: ${csvFilePath}`);
487
+ return [];
488
+ }
489
+
490
+ // 读取文件内容并处理可能的换行符
491
+ let fileContent = fs.readFileSync(csvFilePath, 'utf8');
492
+ fileContent = fileContent.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
493
+
494
+ // 首先尝试直接从文件内容中提取所有可能的cookie
495
+ const cookies = [];
496
+
497
+ // 1. 检查是否有JWT格式的token (新格式)
498
+ const jwtRegex = /ey[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g;
499
+ const jwtMatches = fileContent.match(jwtRegex);
500
+
501
+ if (jwtMatches && jwtMatches.length > 0) {
502
+ logger.info(`直接从文件内容中提取到 ${jwtMatches.length} 个JWT token格式的Cookie`);
503
+ jwtMatches.forEach(match => {
504
+ if (!cookies.includes(match)) {
505
+ cookies.push(match);
506
+ }
507
+ });
508
+ }
509
+
510
+ // 2. 检查是否有旧格式的cookie
511
+ if (fileContent.includes('user_')) {
512
+ logger.info('文件包含旧格式cookie标识"user_"');
513
+
514
+ // 使用旧的提取函数尝试提取
515
+ try {
516
+ const oldFormatCookies = await extractCookiesFromCsv(csvFilePath);
517
+ if (oldFormatCookies && oldFormatCookies.length > 0) {
518
+ logger.info(`通过提取模块获取到 ${oldFormatCookies.length} 个cookie`);
519
+ oldFormatCookies.forEach(cookie => {
520
+ if (!cookies.includes(cookie)) {
521
+ cookies.push(cookie);
522
+ }
523
+ });
524
+ }
525
+ } catch (e) {
526
+ logger.warn('通过提取模块获取cookie失败:', e.message);
527
+ }
528
+ }
529
+
530
+ // 3. 如果找到了cookie,返回结果
531
+ if (cookies.length > 0) {
532
+ const newFormatCount = cookies.filter(c => c.startsWith('ey')).length;
533
+ const oldFormatCount = cookies.filter(c => c.includes('%3A%3A')).length;
534
+
535
+ logger.info(`总共找到 ${cookies.length} 个cookie`);
536
+ logger.info(`新格式cookie(ey开头): ${newFormatCount}个`);
537
+ logger.info(`旧格式cookie(包含%3A%3A): ${oldFormatCount}个`);
538
+ logger.info(`其他格式cookie: ${cookies.length - newFormatCount - oldFormatCount}个`);
539
+
540
+ return cookies;
541
+ }
542
+
543
+ logger.warn(`未能从文件中提取到任何cookie (尝试 ${attempt}/${maxExtractAttempts})`);
544
+ } catch (error) {
545
+ logger.error(`从CSV文件提取cookies时出错 (尝试 ${attempt}/${maxExtractAttempts}):`, error);
546
+ }
547
+
548
+ attempt++;
549
+ if (attempt <= maxExtractAttempts) {
550
+ logger.info(`等待5秒后重试...`);
551
+ await new Promise(resolve => setTimeout(resolve, 5000));
552
+ }
553
+ }
554
+
555
+ logger.error(`在 ${maxExtractAttempts} 次尝试后未能从CSV文件提取到cookies`);
556
+ return [];
557
+ }
558
+
559
+ // 将新的有效cookie添加到系统中
560
+ function addNewCookiesToSystem(apiKey, newCookies) {
561
+ try {
562
+ logger.info(`准备添加 ${newCookies.length} 个新cookie到系统中`);
563
+
564
+ // 获取当前的cookies
565
+ const currentCookies = keyManager.getAllCookiesForApiKey(apiKey) || [];
566
+ logger.info(`当前API密钥 ${apiKey} 有 ${currentCookies.length} 个cookies`);
567
+
568
+ // 获取无效的cookies
569
+ const invalidCookies = keyManager.getInvalidCookies() || [];
570
+ logger.info(`系统中有 ${invalidCookies.length || 0} 个无效cookies`);
571
+
572
+ // 过滤出新的有效cookie
573
+ let newValidCookies = [];
574
+
575
+ // 检查invalidCookies的类型并相应处理
576
+ if (invalidCookies instanceof Set) {
577
+ newValidCookies = newCookies.filter(cookie =>
578
+ !currentCookies.includes(cookie) && !invalidCookies.has(cookie)
579
+ );
580
+ } else if (Array.isArray(invalidCookies)) {
581
+ newValidCookies = newCookies.filter(cookie =>
582
+ !currentCookies.includes(cookie) && !invalidCookies.includes(cookie)
583
+ );
584
+ } else if (invalidCookies && typeof invalidCookies === 'object') {
585
+ // 如果是普通对象,检查cookie是否作为键存在
586
+ newValidCookies = newCookies.filter(cookie =>
587
+ !currentCookies.includes(cookie) && !(cookie in invalidCookies)
588
+ );
589
+ } else {
590
+ // 如果invalidCookies不是预期的类型,只过滤当前cookies
591
+ newValidCookies = newCookies.filter(cookie => !currentCookies.includes(cookie));
592
+ }
593
+
594
+ logger.info(`过滤后有 ${newValidCookies.length} 个新的有效cookies`);
595
+
596
+ // 验证cookie是否完整
597
+ const validatedCookies = newValidCookies.filter(cookie => {
598
+ // 检查是否是新格式的JWT token (ey开头)
599
+ if (cookie.startsWith('ey') && cookie.includes('.')) {
600
+ const parts = cookie.split('.');
601
+ // 检查JWT是否包含三个部分
602
+ if (parts.length !== 3) {
603
+ logger.warn(`跳过不完整的JWT cookie (新格式): ${cookie}`);
604
+ return false;
605
+ }
606
+ return true;
607
+ }
608
+ // 检查旧格式cookie是否包含JWT的三个部分
609
+ else if (cookie.includes('%3A%3A')) {
610
+ const parts = cookie.split('%3A%3A');
611
+ if (parts.length === 2) {
612
+ const jwt = parts[1];
613
+ // 检查JWT是否包含点(表示JWT的三个部分)
614
+ if (!jwt.includes('.') || jwt.split('.').length !== 3) {
615
+ logger.warn(`跳过不完整的cookie (旧格式): ${cookie}`);
616
+ return false;
617
+ }
618
+ }
619
+ }
620
+ return true;
621
+ });
622
+
623
+ logger.info(`验证完整性后有 ${validatedCookies.length} 个有效cookies`);
624
+
625
+ if (validatedCookies.length > 0) {
626
+ // 添加新的有效cookie到系统
627
+ keyManager.addOrUpdateApiKey(apiKey, [...currentCookies, ...validatedCookies]);
628
+ logger.info(`成功添加 ${validatedCookies.length} 个新cookie到API密钥 ${apiKey}`);
629
+ return validatedCookies.length; // 返回添加的cookie数量
630
+ } else {
631
+ logger.info(`没有新的有效cookie需要添加到API密钥 ${apiKey}`);
632
+ return 0; // 没有添加cookie,返回0
633
+ }
634
+ } catch (error) {
635
+ logger.error('添加新cookie到系统时出错:', error);
636
+ return 0; // 出错时返回0
637
+ }
638
+ }
639
+
640
+ // 清理临时文件
641
+ function cleanupTempFiles() {
642
+ try {
643
+ logger.info('开始清理临时文件...');
644
+
645
+ // 清理下载目录
646
+ if (fs.existsSync(DOWNLOAD_DIR)) {
647
+ fs.readdirSync(DOWNLOAD_DIR).forEach(file => {
648
+ fs.unlinkSync(path.join(DOWNLOAD_DIR, file));
649
+ });
650
+ }
651
+
652
+ // 清理解压目录
653
+ if (fs.existsSync(EXTRACT_DIR)) {
654
+ fs.readdirSync(EXTRACT_DIR).forEach(file => {
655
+ fs.unlinkSync(path.join(EXTRACT_DIR, file));
656
+ });
657
+ }
658
+
659
+ logger.info('临时文件清理完成');
660
+ } catch (error) {
661
+ logger.error('清理临时文件失败:', error);
662
+ }
663
+ }
664
+
665
+ // 检查 API Key 是否需要补充 Cookie
666
+ function checkApiKeyNeedRefresh(apiKey, minCookieCount = config.refresh.minCookieCount) {
667
+ const cookies = keyManager.getAllCookiesForApiKey(apiKey);
668
+ return cookies.length < minCookieCount;
669
+ }
670
+
671
+ // 将现有cookie全部设为无效并从API Key中移除
672
+ function markExistingCookiesAsInvalid(apiKey) {
673
+ try {
674
+ // 获取当前API Key的所有cookie
675
+ const currentCookies = keyManager.getAllCookiesForApiKey(apiKey) || [];
676
+ logger.info(`正在将API Key ${apiKey} 的 ${currentCookies.length} 个现有cookie标记为无效...`);
677
+
678
+ // 如果没有cookie,直接返回
679
+ if (currentCookies.length === 0) {
680
+ logger.info(`API Key ${apiKey} 没有现有cookie,无需标记为无效`);
681
+ return 0;
682
+ }
683
+
684
+ // 获取无效cookie列表
685
+ const invalidCookies = keyManager.getInvalidCookies();
686
+ let markedCount = 0;
687
+
688
+ // 遍历cookie并添加到无效列表
689
+ for (const cookie of currentCookies) {
690
+ // 将cookie添加到无效集合中
691
+ if (invalidCookies instanceof Set) {
692
+ invalidCookies.add(cookie);
693
+ }
694
+ markedCount++;
695
+ }
696
+
697
+ // 保存无效cookie到文件
698
+ keyManager.saveInvalidCookiesToFile();
699
+
700
+ // 清空当前API Key的cookie列表
701
+ keyManager.addOrUpdateApiKey(apiKey, []);
702
+
703
+ // 保存更新后的API Keys
704
+ keyManager.saveApiKeysToFile();
705
+
706
+ logger.info(`已将API Key ${apiKey} 的 ${markedCount} 个cookie标记为无效并从API Key中移除`);
707
+ return markedCount;
708
+ } catch (error) {
709
+ logger.error(`标记现有cookie为无效时出错:`, error);
710
+ return 0;
711
+ }
712
+ }
713
+
714
+ // 主函数:自动刷新 Cookie
715
+ async function autoRefreshCookies(apiKey, minCookieCount = config.refresh.minCookieCount) {
716
+ logger.info(`开始自动刷新 Cookie,目标 API Key: ${apiKey},最小 Cookie 数量: ${minCookieCount}`);
717
+
718
+ try {
719
+ // 检查是否需要刷新
720
+ if (!checkApiKeyNeedRefresh(apiKey, minCookieCount)) {
721
+ logger.info(`API Key ${apiKey} 的 Cookie 数量足够,不需要刷新`);
722
+ return {
723
+ success: true,
724
+ message: '当前 Cookie 数量足够,不需要刷新',
725
+ refreshed: 0
726
+ };
727
+ }
728
+
729
+ // 获取最新的 Artifact
730
+ const artifact = await getLatestArtifact();
731
+ if (!artifact) {
732
+ return {
733
+ success: false,
734
+ message: '获取 Artifact 失败',
735
+ refreshed: 0
736
+ };
737
+ }
738
+
739
+ // 下载 Artifact
740
+ const zipFilePath = await downloadArtifact(artifact);
741
+ if (!zipFilePath) {
742
+ return {
743
+ success: false,
744
+ message: '下载 Artifact 失败',
745
+ refreshed: 0
746
+ };
747
+ }
748
+
749
+ // 解压 Artifact
750
+ const csvFilePath = await extractArtifact(zipFilePath);
751
+ if (!csvFilePath) {
752
+ return {
753
+ success: false,
754
+ message: '解压 Artifact 失败',
755
+ refreshed: 0
756
+ };
757
+ }
758
+
759
+ // 提取 Cookie
760
+ const cookies = await extractCookiesFromCsvFile(csvFilePath);
761
+ if (cookies.length === 0) {
762
+ return {
763
+ success: false,
764
+ message: '没有找到有效的 Cookie',
765
+ refreshed: 0
766
+ };
767
+ }
768
+
769
+ // 分析提取到的cookie格式
770
+ const newFormatCookies = cookies.filter(cookie => cookie.startsWith('ey'));
771
+ const oldFormatCookies = cookies.filter(cookie => cookie.includes('%3A%3A'));
772
+ logger.info(`提取到 ${newFormatCookies.length} 个新格式cookie(ey开头)`);
773
+ logger.info(`提取到 ${oldFormatCookies.length} 个旧格式cookie(包含%3A%3A)`);
774
+
775
+ // 根据配置决定是否将现有cookie标记为无效
776
+ const refreshMode = process.env.COOKIE_REFRESH_MODE || 'append';
777
+
778
+ if (refreshMode === 'replace') {
779
+ // 将现有cookie标记为无效并从API Key中移除
780
+ logger.info('使用替换模式: 将现有cookie标记为无效');
781
+ markExistingCookiesAsInvalid(apiKey);
782
+ } else {
783
+ logger.info('使用追加模式: 保留现有cookie,只添加新cookie');
784
+ }
785
+
786
+ // 添加新的 Cookie 到系统
787
+ const addedCount = addNewCookiesToSystem(apiKey, cookies);
788
+
789
+ // 清理临时文件
790
+ cleanupTempFiles();
791
+
792
+ return {
793
+ success: true,
794
+ message: `成功添加 ${addedCount} 个新 Cookie (新格式: ${newFormatCookies.length}, 旧格式: ${oldFormatCookies.length})`,
795
+ refreshed: addedCount
796
+ };
797
+ } catch (error) {
798
+ logger.error('自动刷新 Cookie 失败:', error);
799
+ return {
800
+ success: false,
801
+ message: `刷新失败: ${error.message}`,
802
+ refreshed: 0
803
+ };
804
+ }
805
+ }
806
+
807
+ module.exports = {
808
+ autoRefreshCookies,
809
+ checkApiKeyNeedRefresh,
810
+ getLatestArtifact,
811
+ downloadArtifact,
812
+ extractArtifact,
813
+ extractCookiesFromCsvFile,
814
+ addNewCookiesToSystem,
815
+ cleanupTempFiles,
816
+ triggerWorkflow,
817
+ markExistingCookiesAsInvalid
818
+ };
src/utils/envChecker.js ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ // 添加自己的简单日志函数,防止循环依赖
5
+ function log(level, message) {
6
+ // 只在控制台输出,不写入文件
7
+ const timestamp = new Date().toISOString();
8
+ if (level === 'ERROR') {
9
+ console.error(`[ERROR] ${timestamp} ${message}`);
10
+ } else if (level === 'WARN') {
11
+ console.warn(`[WARN] ${timestamp} ${message}`);
12
+ } else {
13
+ console.log(`[INFO] ${timestamp} ${message}`);
14
+ }
15
+ }
16
+
17
+ /**
18
+ * 检查 .env 文件是否存在
19
+ * @returns {boolean} 文件是否存在
20
+ */
21
+ function checkEnvFileExists() {
22
+ const envPath = path.resolve(process.cwd(), '.env');
23
+ return fs.existsSync(envPath);
24
+ }
25
+
26
+ /**
27
+ * 检查必要的环境变量是否已设置
28
+ * @returns {Object} 检查结果,包含是否通过和缺失的变量列表
29
+ */
30
+ function checkRequiredEnvVars() {
31
+ // 定义必要的环境变量列表
32
+ const requiredVars = [
33
+ 'API_KEYS', // API Keys 配置
34
+ ];
35
+
36
+ // 如果启用了自动刷新,则需要检查相关配置
37
+ if (process.env.ENABLE_AUTO_REFRESH === 'true') {
38
+ requiredVars.push(
39
+ 'GITHUB_TOKEN',
40
+ 'GITHUB_OWNER',
41
+ 'GITHUB_REPO',
42
+ 'GITHUB_WORKFLOW_ID',
43
+ 'TRIGGER_WORKFLOW'
44
+ );
45
+ }
46
+
47
+ // 检查每个必要的环境变量
48
+ const missingVars = requiredVars.filter(varName => !process.env[varName]);
49
+
50
+ return {
51
+ passed: missingVars.length === 0,
52
+ missingVars
53
+ };
54
+ }
55
+
56
+ /**
57
+ * 执行环境检查,如果不符合要求则退出程序
58
+ */
59
+ function enforceEnvCheck() {
60
+ log('INFO', '正在检查环境配置...');
61
+
62
+ // 检查 .env 文件是否存在
63
+ const envFileExists = checkEnvFileExists();
64
+ if (!envFileExists) {
65
+ log('ERROR', '\n错误: 未找到 .env 文件!');
66
+ log('ERROR', '请根据 .env.example 创建 .env 文件并配置必要的环境变量。');
67
+ log('ERROR', '执行以下命令复制示例文件: cp .env.example .env,或执行npm run setup\n');
68
+ process.exit(1); // 退出程序,状态码 1 表示错误
69
+ }
70
+
71
+ // 检查必要的环境变量
72
+ const { passed, missingVars } = checkRequiredEnvVars();
73
+ if (!passed) {
74
+ log('ERROR', '\n错误: 以下必要的环境变量未在 .env 文件中设置:');
75
+ missingVars.forEach(varName => {
76
+ log('ERROR', ` - ${varName}`);
77
+ });
78
+ log('ERROR', '\n请在 .env 文件中配置这些变量后重新启动程序。\n');
79
+ process.exit(1); // 退出程序,状态码 1 表示错误
80
+ }
81
+
82
+ log('INFO', '环境检查通过,继续启动程序...');
83
+ }
84
+
85
+ module.exports = {
86
+ checkEnvFileExists,
87
+ checkRequiredEnvVars,
88
+ enforceEnvCheck
89
+ };
src/utils/extractCookieFromCsv.js ADDED
@@ -0,0 +1,269 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const csv = require('csv-parser');
4
+
5
+ /**
6
+ * 从CSV文件中提取完整的cookie
7
+ * @param {string} csvFilePath - CSV文件路径
8
+ * @returns {Promise<string[]>} - 提取到的cookie数组
9
+ */
10
+ async function extractCookiesFromCsv(csvFilePath) {
11
+ return new Promise((resolve, reject) => {
12
+ try {
13
+ // 检查文件是否存在
14
+ if (!fs.existsSync(csvFilePath)) {
15
+ console.error(`CSV文件不存在: ${csvFilePath}`);
16
+ return resolve([]);
17
+ }
18
+
19
+ // 读取文件内容
20
+ const fileContent = fs.readFileSync(csvFilePath, 'utf8');
21
+ console.log(`文件内容前200个字符: ${fileContent.substring(0, 200)}`);
22
+
23
+ // 检查文件是否为空
24
+ if (!fileContent || fileContent.trim() === '') {
25
+ console.error('CSV文件为空');
26
+ return resolve([]);
27
+ }
28
+
29
+ // 首先尝试直接从文件内容中提取所有可能的cookie
30
+ const cookies = [];
31
+
32
+ // 检查是否有JWT格式的token (新格式)
33
+ const jwtRegex = /ey[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g;
34
+ const jwtMatches = fileContent.match(jwtRegex);
35
+
36
+ if (jwtMatches && jwtMatches.length > 0) {
37
+ console.log(`直接从文件内容中提取到 ${jwtMatches.length} 个JWT token格式的Cookie`);
38
+ jwtMatches.forEach(match => {
39
+ if (!cookies.includes(match)) {
40
+ cookies.push(match);
41
+ }
42
+ });
43
+ }
44
+
45
+ // 检查文件内容是否包含关键字
46
+ const hasTokenKeyword = fileContent.includes('token');
47
+ const hasUserPrefix = fileContent.includes('user_');
48
+ console.log(`文件包含"token"关键字: ${hasTokenKeyword}`);
49
+ console.log(`文件包含"user_"前缀: ${hasUserPrefix}`);
50
+
51
+ // 如果文件包含user_前缀,尝试提取旧格式cookie
52
+ if (hasUserPrefix) {
53
+ const oldFormatCookies = extractCookiesFromText(fileContent);
54
+ if (oldFormatCookies.length > 0) {
55
+ console.log(`从文件内容中提取到 ${oldFormatCookies.length} 个旧格式Cookie`);
56
+ oldFormatCookies.forEach(cookie => {
57
+ if (!cookies.includes(cookie)) {
58
+ cookies.push(cookie);
59
+ }
60
+ });
61
+ }
62
+ }
63
+
64
+ // 如果已经找到cookie,返回结果
65
+ if (cookies.length > 0) {
66
+ console.log(`总共提取到 ${cookies.length} 个Cookie`);
67
+ return resolve(validateCookies(cookies));
68
+ }
69
+
70
+ // 使用csv-parser解析CSV文件
71
+ const possibleTokenFields = ['token', 'cookie', 'value', 'Token', 'Cookie', 'Value', 'jwt', 'JWT'];
72
+
73
+ fs.createReadStream(csvFilePath)
74
+ .pipe(csv())
75
+ .on('data', (row) => {
76
+ // 检查所有可能的字段名
77
+ for (const field of possibleTokenFields) {
78
+ if (row[field]) {
79
+ // 检查是否是JWT格式
80
+ if (row[field].startsWith('ey') && row[field].includes('.')) {
81
+ if (!cookies.includes(row[field])) {
82
+ cookies.push(row[field]);
83
+ }
84
+ break;
85
+ }
86
+ // 检查是否是旧格式
87
+ else if (row[field].includes('user_')) {
88
+ if (!cookies.includes(row[field])) {
89
+ cookies.push(row[field]);
90
+ }
91
+ break;
92
+ }
93
+ }
94
+ }
95
+
96
+ // 如果没有找到预定义的字段,遍历所有字段
97
+ if (cookies.length === 0) {
98
+ for (const field in row) {
99
+ if (row[field] && typeof row[field] === 'string') {
100
+ // 检查是否是JWT格式
101
+ if (row[field].startsWith('ey') && row[field].includes('.')) {
102
+ if (!cookies.includes(row[field])) {
103
+ cookies.push(row[field]);
104
+ }
105
+ break;
106
+ }
107
+ // 检查是否是旧格式
108
+ else if (row[field].includes('user_')) {
109
+ if (!cookies.includes(row[field])) {
110
+ cookies.push(row[field]);
111
+ }
112
+ break;
113
+ }
114
+ }
115
+ }
116
+ }
117
+ })
118
+ .on('end', () => {
119
+ console.log(`从CSV解析中提取到 ${cookies.length} 个Cookie`);
120
+
121
+ // 如果通过CSV解析没有找到cookie,尝试按行读取
122
+ if (cookies.length === 0) {
123
+ console.log('尝试按行读取文件...');
124
+ const lines = fileContent.split('\n');
125
+ for (const line of lines) {
126
+ // 检查是否有JWT格式token
127
+ if (line.includes('ey')) {
128
+ const jwtMatches = line.match(jwtRegex);
129
+ if (jwtMatches) {
130
+ jwtMatches.forEach(match => {
131
+ if (!cookies.includes(match)) {
132
+ cookies.push(match);
133
+ }
134
+ });
135
+ }
136
+ }
137
+
138
+ // 检查是否有旧格式cookie
139
+ if (line.includes('user_')) {
140
+ const extractedCookies = extractCookiesFromText(line);
141
+ extractedCookies.forEach(cookie => {
142
+ if (!cookies.includes(cookie)) {
143
+ cookies.push(cookie);
144
+ }
145
+ });
146
+ }
147
+ }
148
+ console.log(`按行读取后提取到 ${cookies.length} 个Cookie`);
149
+ }
150
+
151
+ // 验证提取的cookie是否完整
152
+ const validatedCookies = validateCookies(cookies);
153
+
154
+ resolve(validatedCookies);
155
+ })
156
+ .on('error', (error) => {
157
+ console.error('解析CSV文件时出错:', error);
158
+
159
+ // 如果已经提取到cookie,直接返回
160
+ if (cookies.length > 0) {
161
+ console.log(`解析出错但已提取到 ${cookies.length} 个Cookie,进行验证后返回`);
162
+ resolve(validateCookies(cookies));
163
+ } else {
164
+ // 否则尝试其他方法提取
165
+ console.log('尝试其他方法提取Cookie...');
166
+
167
+ // 尝试提取JWT格式token
168
+ const jwtMatches = fileContent.match(jwtRegex);
169
+ if (jwtMatches) {
170
+ jwtMatches.forEach(match => {
171
+ if (!cookies.includes(match)) {
172
+ cookies.push(match);
173
+ }
174
+ });
175
+ }
176
+
177
+ // 尝试提取旧格式cookie
178
+ const oldFormatCookies = extractCookiesFromText(fileContent);
179
+ oldFormatCookies.forEach(cookie => {
180
+ if (!cookies.includes(cookie)) {
181
+ cookies.push(cookie);
182
+ }
183
+ });
184
+
185
+ console.log(`通过其他方法提取到 ${cookies.length} 个Cookie`);
186
+ resolve(validateCookies(cookies));
187
+ }
188
+ });
189
+ } catch (error) {
190
+ console.error('提取Cookie时出错:', error);
191
+ reject(error);
192
+ }
193
+ });
194
+ }
195
+
196
+ /**
197
+ * 从文本中提取cookie
198
+ * @param {string} text - 要提取cookie的文本
199
+ * @returns {string[]} - 提取到的cookie数组
200
+ */
201
+ function extractCookiesFromText(text) {
202
+ const cookies = [];
203
+
204
+ // 使用正则表达式匹配user_开头的cookie(旧格式)
205
+ const oldFormatRegex = /user_[a-zA-Z0-9%]+%3A%3A[a-zA-Z0-9%\.\_\-]+/g;
206
+ const oldFormatMatches = text.match(oldFormatRegex);
207
+
208
+ if (oldFormatMatches) {
209
+ oldFormatMatches.forEach(match => {
210
+ if (!cookies.includes(match)) {
211
+ cookies.push(match);
212
+ }
213
+ });
214
+ }
215
+
216
+ // 使用正则表达式匹配以ey开头的JWT格式cookie(新格式)
217
+ const jwtRegex = /ey[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g;
218
+ const jwtMatches = text.match(jwtRegex);
219
+
220
+ if (jwtMatches) {
221
+ jwtMatches.forEach(match => {
222
+ if (!cookies.includes(match)) {
223
+ cookies.push(match);
224
+ }
225
+ });
226
+ }
227
+
228
+ return cookies;
229
+ }
230
+
231
+ /**
232
+ * 验证cookie是否完整
233
+ * @param {string[]} cookies - 要验证的cookie数组
234
+ * @returns {string[]} - 验证后的cookie数组
235
+ */
236
+ function validateCookies(cookies) {
237
+ return cookies.filter(cookie => {
238
+ // 检查是否是新格式的JWT token (ey开头)
239
+ if (cookie.startsWith('ey') && cookie.includes('.')) {
240
+ const parts = cookie.split('.');
241
+ // 检查JWT是否包含三个部分
242
+ if (parts.length === 3) {
243
+ return true; // cookie有效
244
+ } else {
245
+ console.warn(`检测到不完整的JWT(新格式): ${cookie}`);
246
+ return false;
247
+ }
248
+ }
249
+ // 检查旧格式cookie是否完整
250
+ else if (cookie.includes('%3A%3A')) {
251
+ const parts = cookie.split('%3A%3A');
252
+ if (parts.length === 2) {
253
+ const jwt = parts[1];
254
+ // 检查JWT是否包含两个点(表示三个部分)
255
+ if (jwt.includes('.') && jwt.split('.').length === 3) {
256
+ return true; // cookie完整
257
+ } else {
258
+ console.warn(`检测到不完整的JWT(旧格式): ${cookie}`);
259
+ return false;
260
+ }
261
+ }
262
+ }
263
+ return true; // 对于无法识别的格式,默认保留
264
+ });
265
+ }
266
+
267
+ module.exports = {
268
+ extractCookiesFromCsv
269
+ };
src/utils/keyManager.js ADDED
@@ -0,0 +1,410 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const config = require('../config/config');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const logger = require('./logger');
5
+
6
+ // 定义无效cookie的存储文件路径
7
+ const INVALID_COOKIES_FILE = path.join(__dirname, '../../data/invalid_cookies.json');
8
+ // 定义API Keys的存储文件路径
9
+ const API_KEYS_FILE = path.join(__dirname, '../../data/api_keys.json');
10
+
11
+ // 确保data目录存在
12
+ function ensureDataDirExists() {
13
+ const dataDir = path.join(__dirname, '../../data');
14
+ if (!fs.existsSync(dataDir)) {
15
+ try {
16
+ fs.mkdirSync(dataDir, { recursive: true });
17
+ logger.info(`创建data目录: ${dataDir}`);
18
+ } catch (err) {
19
+ logger.error('创建data目录失败:', err);
20
+ }
21
+ }
22
+ }
23
+
24
+ // 存储API key与Cursor cookie的映射关系
25
+ let apiKeyMap = new Map();
26
+
27
+ // 存储每个API key对应的cookie轮询索引
28
+ let rotationIndexes = new Map();
29
+
30
+ // 存储被标记为无效的cookie
31
+ let invalidCookies = new Set();
32
+
33
+ // 从文件加载无效cookie
34
+ function loadInvalidCookiesFromFile() {
35
+ ensureDataDirExists();
36
+
37
+ try {
38
+ if (fs.existsSync(INVALID_COOKIES_FILE)) {
39
+ const data = fs.readFileSync(INVALID_COOKIES_FILE, 'utf8');
40
+ const cookiesArray = JSON.parse(data);
41
+
42
+ // 清空当前集合并添加从文件加载的cookie
43
+ invalidCookies.clear();
44
+ cookiesArray.forEach(cookie => invalidCookies.add(cookie));
45
+
46
+ logger.info(`从文件加载了 ${cookiesArray.length} 个无效cookie`);
47
+ } else {
48
+ saveInvalidCookiesToFile(); // 如果文件不存在,创建新文件
49
+ }
50
+ } catch (err) {
51
+ logger.error('加载无效cookie文件失败:', err);
52
+ saveInvalidCookiesToFile(); // 如果加载失败,尝试创建新文件
53
+ }
54
+ }
55
+
56
+ // 将无效cookie保存到文件
57
+ function saveInvalidCookiesToFile() {
58
+ ensureDataDirExists();
59
+
60
+ try {
61
+ const cookiesArray = Array.from(invalidCookies);
62
+ fs.writeFileSync(INVALID_COOKIES_FILE, JSON.stringify(cookiesArray, null, 2), 'utf8');
63
+ logger.info(`已将 ${cookiesArray.length} 个无效cookie保存到文件`);
64
+ } catch (err) {
65
+ logger.error('保存无效cookie文件失败:', err);
66
+ }
67
+ }
68
+
69
+ // 从文件加载API Keys
70
+ function loadApiKeysFromFile() {
71
+ ensureDataDirExists();
72
+
73
+ try {
74
+ if (fs.existsSync(API_KEYS_FILE)) {
75
+ const data = fs.readFileSync(API_KEYS_FILE, 'utf8');
76
+ const apiKeysObj = JSON.parse(data);
77
+
78
+ // 清空现有映射
79
+ apiKeyMap.clear();
80
+ rotationIndexes.clear();
81
+
82
+ // 统计总cookie数量
83
+ let totalCookies = 0;
84
+
85
+ // 添加从文件加载的API Keys
86
+ for (const [apiKey, cookies] of Object.entries(apiKeysObj)) {
87
+ if (Array.isArray(cookies)) {
88
+ apiKeyMap.set(apiKey, cookies);
89
+ rotationIndexes.set(apiKey, 0);
90
+ totalCookies += cookies.length;
91
+ } else {
92
+ logger.error(`API Key ${apiKey} 的cookies不是数组,跳过`);
93
+ }
94
+ }
95
+
96
+ const apiKeyCount = Object.keys(apiKeysObj).length;
97
+ logger.info(`从文件加载了 ${apiKeyCount} 个API Key,共 ${totalCookies} 个Cookie`);
98
+ return apiKeyCount > 0;
99
+ } else {
100
+ logger.info('API Keys文件不存在,将使用配置中的API Keys');
101
+ return false;
102
+ }
103
+ } catch (err) {
104
+ logger.error('加载API Keys文件失败:', err);
105
+ return false;
106
+ }
107
+ }
108
+
109
+ // 将API Keys保存到文件
110
+ function saveApiKeysToFile() {
111
+ ensureDataDirExists();
112
+
113
+ try {
114
+ // 将Map转换为普通对象
115
+ const apiKeysObj = {};
116
+ for (const [apiKey, cookies] of apiKeyMap.entries()) {
117
+ apiKeysObj[apiKey] = cookies;
118
+ }
119
+
120
+ // 使用JSON.stringify时避免特殊字符处理问题
121
+ const jsonString = JSON.stringify(apiKeysObj, null, 2);
122
+ fs.writeFileSync(API_KEYS_FILE, jsonString, 'utf8');
123
+ logger.info(`已将 ${Object.keys(apiKeysObj).length} 个API Key保存到文件`);
124
+
125
+ // 简化验证过程
126
+ try {
127
+ const savedContent = fs.readFileSync(API_KEYS_FILE, 'utf8');
128
+ JSON.parse(savedContent); // 只验证JSON格式是否正确
129
+ logger.info('验证通过: 所有cookie都被完整保存');
130
+ } catch (verifyErr) {
131
+ logger.error('验证保存内容时出错:', verifyErr);
132
+ }
133
+ } catch (err) {
134
+ logger.error('保存API Keys文件失败:', err);
135
+ }
136
+ }
137
+
138
+ // API Keys初始化函数
139
+ function initializeApiKeys() {
140
+ // 首先从文件加载现有的API Keys
141
+ const loadedFromFile = loadApiKeysFromFile();
142
+
143
+ // 检查环境变量中是否有API Keys配置
144
+ const configApiKeys = config.apiKeys;
145
+ const hasEnvApiKeys = Object.keys(configApiKeys).length > 0;
146
+
147
+ if (hasEnvApiKeys) {
148
+ logger.info('从环境变量检测到API Keys配置,将合并到现有配置...');
149
+
150
+ // 记录合并前的Cookie数量
151
+ let beforeMergeCookies = 0;
152
+ for (const cookies of apiKeyMap.values()) {
153
+ beforeMergeCookies += cookies.length;
154
+ }
155
+
156
+ // 合并环境变量中的API Keys到现有映射
157
+ for (const [apiKey, cookieValue] of Object.entries(configApiKeys)) {
158
+ // 获取现有的cookies(如果有)
159
+ const existingCookies = apiKeyMap.get(apiKey) || [];
160
+
161
+ // 准备要添加的新cookies
162
+ let newCookies = [];
163
+ if (typeof cookieValue === 'string') {
164
+ newCookies = [cookieValue];
165
+ } else if (Array.isArray(cookieValue)) {
166
+ newCookies = cookieValue;
167
+ }
168
+
169
+ // 合并cookies,确保不重复
170
+ const mergedCookies = [...existingCookies];
171
+ for (const cookie of newCookies) {
172
+ if (!mergedCookies.includes(cookie)) {
173
+ mergedCookies.push(cookie);
174
+ }
175
+ }
176
+
177
+ // 更新映射
178
+ apiKeyMap.set(apiKey, mergedCookies);
179
+
180
+ // 确保轮询索引存在
181
+ if (!rotationIndexes.has(apiKey)) {
182
+ rotationIndexes.set(apiKey, 0);
183
+ }
184
+ }
185
+
186
+ // 记录合并后的Cookie数量
187
+ let afterMergeCookies = 0;
188
+ for (const cookies of apiKeyMap.values()) {
189
+ afterMergeCookies += cookies.length;
190
+ }
191
+
192
+ logger.info(`合并前共有 ${beforeMergeCookies} 个Cookie,合并后共有 ${afterMergeCookies} 个Cookie`);
193
+
194
+ // 保存合并后的结果到文件
195
+ saveApiKeysToFile();
196
+ } else if (!loadedFromFile) {
197
+ logger.warn('警告: 未能从文件加载API Keys,且环境变量中也没有配置API Keys');
198
+ }
199
+
200
+ // 统计API Keys和Cookies数量
201
+ let totalCookies = 0;
202
+ for (const cookies of apiKeyMap.values()) {
203
+ totalCookies += cookies.length;
204
+ }
205
+
206
+ logger.info(`API Keys初始化完成,共有 ${apiKeyMap.size} 个API Key,${totalCookies} 个Cookie`);
207
+
208
+ // 加载无效cookie
209
+ loadInvalidCookiesFromFile();
210
+
211
+ // 从API Key中移除已知的无效cookie
212
+ logger.info('开始从API Keys中移除无效cookie...');
213
+ removeInvalidCookiesFromApiKeys();
214
+ }
215
+
216
+ // 从所有API Key中移除已知的无效cookie
217
+ function removeInvalidCookiesFromApiKeys() {
218
+ let totalRemoved = 0;
219
+
220
+ for (const [apiKey, cookies] of apiKeyMap.entries()) {
221
+ const initialLength = cookies.length;
222
+
223
+ // 过滤掉无效的cookie
224
+ const filteredCookies = cookies.filter(cookie => !invalidCookies.has(cookie));
225
+
226
+ // 如果有cookie被移除,更新API Key的cookie列表
227
+ if (filteredCookies.length < initialLength) {
228
+ const removedCount = initialLength - filteredCookies.length;
229
+ totalRemoved += removedCount;
230
+
231
+ apiKeyMap.set(apiKey, filteredCookies);
232
+ rotationIndexes.set(apiKey, 0);
233
+
234
+ logger.info(`从API Key ${apiKey} 中移除了 ${removedCount} 个无效cookie,剩余 ${filteredCookies.length} 个`);
235
+ }
236
+ }
237
+
238
+ logger.info(`总共从API Keys中移除了 ${totalRemoved} 个无效cookie`);
239
+
240
+ // 如果有cookie被移除,保存更新后的API Keys
241
+ if (totalRemoved > 0) {
242
+ saveApiKeysToFile();
243
+ }
244
+ }
245
+
246
+ // 添加或更新API key映射
247
+ function addOrUpdateApiKey(apiKey, cookieValues) {
248
+ if (!Array.isArray(cookieValues)) {
249
+ cookieValues = [cookieValues];
250
+ }
251
+
252
+ // 过滤掉已知的无效cookie
253
+ const validCookies = cookieValues.filter(cookie => !invalidCookies.has(cookie));
254
+
255
+ if (validCookies.length < cookieValues.length) {
256
+ logger.info(`API Key ${apiKey} 中有 ${cookieValues.length - validCookies.length} 个无效cookie被过滤`);
257
+ }
258
+
259
+ apiKeyMap.set(apiKey, validCookies);
260
+ rotationIndexes.set(apiKey, 0);
261
+
262
+ // 保存更新后的API Keys
263
+ saveApiKeysToFile();
264
+ }
265
+
266
+ // 删除API key映射
267
+ function removeApiKey(apiKey) {
268
+ apiKeyMap.delete(apiKey);
269
+ rotationIndexes.delete(apiKey);
270
+
271
+ // 保存更新后的API Keys
272
+ saveApiKeysToFile();
273
+ }
274
+
275
+ // 获取API key对应的cookie值(根据轮询策略)
276
+ function getCookieForApiKey(apiKey, strategy = config.defaultRotationStrategy) {
277
+ // 如果API key不存在,也许是cookie本身,直接返回API key本身(向后兼容)
278
+ if (!apiKeyMap.has(apiKey)) {
279
+ return apiKey;
280
+ }
281
+ const cookies = apiKeyMap.get(apiKey);
282
+
283
+ if (!cookies || cookies.length === 0) {
284
+ return apiKey;
285
+ }
286
+
287
+ if (cookies.length === 1) {
288
+ return cookies[0];
289
+ }
290
+
291
+ // 根据策略选择cookie
292
+ if (strategy === 'random') {
293
+ // 随机策略
294
+ const randomIndex = Math.floor(Math.random() * cookies.length);
295
+ return cookies[randomIndex];
296
+ } else if(strategy === 'round-robin') {
297
+ // 轮询策略(round-robin)
298
+ let currentIndex = rotationIndexes.get(apiKey) || 0;
299
+ const cookie = cookies[currentIndex];
300
+
301
+ // 更新索引
302
+ currentIndex = (currentIndex + 1) % cookies.length;
303
+ rotationIndexes.set(apiKey, currentIndex);
304
+
305
+ return cookie;
306
+ } else {
307
+ // 默认策略(default)
308
+ return cookies[0];
309
+ }
310
+ }
311
+
312
+ // 获取所有API key
313
+ function getAllApiKeys() {
314
+ return Array.from(apiKeyMap.keys());
315
+ }
316
+
317
+ // 获取API key对应的所有cookie
318
+ function getAllCookiesForApiKey(apiKey) {
319
+ return apiKeyMap.get(apiKey) || [];
320
+ }
321
+
322
+ // 从API key的cookie列表中移除特定cookie
323
+ function removeCookieFromApiKey(apiKey, cookieToRemove) {
324
+ if (!apiKeyMap.has(apiKey)) {
325
+ logger.info(`API Key ${apiKey} 不存在,无法移除cookie`);
326
+ return false;
327
+ }
328
+
329
+ const cookies = apiKeyMap.get(apiKey);
330
+ const initialLength = cookies.length;
331
+
332
+ // 检查是否尝试移除与API Key相同的值(可能是向后兼容模式)
333
+ if (cookieToRemove === apiKey && initialLength === 0) {
334
+ logger.info(`API Key ${apiKey} 中没有任何cookie,系统正在尝试以向后兼容模式使用API Key本身`);
335
+ return false;
336
+ }
337
+
338
+ // 过滤掉要移除的cookie
339
+ const filteredCookies = cookies.filter(cookie => cookie !== cookieToRemove);
340
+
341
+ // 如果长度没变,说明没有找到要移除的cookie
342
+ if (filteredCookies.length === initialLength) {
343
+ logger.info(`未找到要移除的cookie: ${cookieToRemove}`);
344
+ return false;
345
+ }
346
+
347
+ // 更新cookie列表
348
+ apiKeyMap.set(apiKey, filteredCookies);
349
+
350
+ // 重置轮询索引
351
+ rotationIndexes.set(apiKey, 0);
352
+
353
+ // 将移除的cookie添加到无效cookie集合中
354
+ invalidCookies.add(cookieToRemove);
355
+
356
+ // 保存无效cookie到文件
357
+ saveInvalidCookiesToFile();
358
+
359
+ // 保存更新后的API Keys
360
+ saveApiKeysToFile();
361
+
362
+ logger.info(`已从API Key ${apiKey} 中移除cookie: ${cookieToRemove}`);
363
+ logger.info(`剩余cookie数量: ${filteredCookies.length}`);
364
+
365
+ return true;
366
+ }
367
+
368
+ // 获取所有被标记为无效的cookie
369
+ function getInvalidCookies() {
370
+ return invalidCookies;
371
+ }
372
+
373
+ // 清除特定的无效cookie记录
374
+ function clearInvalidCookie(cookie) {
375
+ const result = invalidCookies.delete(cookie);
376
+
377
+ if (result) {
378
+ // 保存更新后的无效cookie到文件
379
+ saveInvalidCookiesToFile();
380
+ }
381
+
382
+ return result;
383
+ }
384
+
385
+ // 清除所有无效cookie记录
386
+ function clearAllInvalidCookies() {
387
+ invalidCookies.clear();
388
+
389
+ // 保存更新后的无效cookie到文件
390
+ saveInvalidCookiesToFile();
391
+
392
+ return true;
393
+ }
394
+
395
+ module.exports = {
396
+ addOrUpdateApiKey,
397
+ removeApiKey,
398
+ getCookieForApiKey,
399
+ getAllApiKeys,
400
+ getAllCookiesForApiKey,
401
+ initializeApiKeys,
402
+ removeCookieFromApiKey,
403
+ getInvalidCookies,
404
+ clearInvalidCookie,
405
+ clearAllInvalidCookies,
406
+ loadInvalidCookiesFromFile,
407
+ saveInvalidCookiesToFile,
408
+ loadApiKeysFromFile,
409
+ saveApiKeysToFile
410
+ };
src/utils/logger.js ADDED
@@ -0,0 +1,392 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // logger.js - 统一的日志系统模块
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+
5
+ // 避免循环依赖
6
+ let config = null;
7
+ // 延迟加载配置
8
+ function getConfig() {
9
+ if (!config) {
10
+ try {
11
+ config = require('../config/config');
12
+ } catch (err) {
13
+ console.error('加载配置文件失败:', err.message);
14
+ config = { log: { level: 'INFO', format: 'colored' } };
15
+ }
16
+ }
17
+ return config;
18
+ }
19
+
20
+ const LOG_LEVELS = {
21
+ ERROR: 0,
22
+ WARN: 1,
23
+ INFO: 2,
24
+ DEBUG: 3,
25
+ TRACE: 4,
26
+ HTTP: 2 // HTTP日志级别与INFO相同
27
+ };
28
+
29
+ // 默认日志级别
30
+ let currentLogLevel = LOG_LEVELS.INFO;
31
+
32
+ // 日志格式
33
+ let logFormat = 'colored'; // colored, json, text
34
+
35
+ // 带颜色的控制台输出
36
+ const COLORS = {
37
+ RESET: '\x1b[0m',
38
+ RED: '\x1b[31m',
39
+ YELLOW: '\x1b[33m',
40
+ GREEN: '\x1b[32m',
41
+ BLUE: '\x1b[34m',
42
+ CYAN: '\x1b[36m'
43
+ };
44
+
45
+ // 日志文件配置
46
+ const LOG_DIR = path.join(__dirname, '../../logs');
47
+ const LOG_FILE = path.join(LOG_DIR, 'app.log');
48
+ const MAX_LOG_SIZE = 10 * 1024 * 1024; // 10MB
49
+ let logToFile = false;
50
+
51
+ // 内存中存储的日志(用于网页显示)
52
+ const memoryLogs = [];
53
+ const MAX_MEMORY_LOGS = 1000; // 内存中最多保存的日志条数
54
+
55
+ // 确保日志目录存在
56
+ function ensureLogDirExists() {
57
+ try {
58
+ if (!fs.existsSync(LOG_DIR)) {
59
+ fs.mkdirSync(LOG_DIR, { recursive: true });
60
+ }
61
+ return true;
62
+ } catch (err) {
63
+ console.error(`创建日志目录失败: ${err.message}`);
64
+ return false;
65
+ }
66
+ }
67
+
68
+ // 初始化文件日志
69
+ function initFileLogging() {
70
+ const conf = getConfig();
71
+ if (process.env.LOG_TO_FILE === 'true' || (conf.log && conf.log.toFile)) {
72
+ if (ensureLogDirExists()) {
73
+ logToFile = true;
74
+ // 检查日志文件大小,如果超过最大值则进行轮转
75
+ if (fs.existsSync(LOG_FILE)) {
76
+ const stats = fs.statSync(LOG_FILE);
77
+ if (stats.size > MAX_LOG_SIZE) {
78
+ rotateLogFile();
79
+ }
80
+ }
81
+ return true;
82
+ }
83
+ }
84
+ return false;
85
+ }
86
+
87
+ // 日志文件轮转
88
+ function rotateLogFile() {
89
+ try {
90
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
91
+ const newLogFile = path.join(LOG_DIR, `app-${timestamp}.log`);
92
+ if (fs.existsSync(LOG_FILE)) {
93
+ fs.renameSync(LOG_FILE, newLogFile);
94
+ }
95
+ // 清理旧日志文件,保留最近10个
96
+ const logFiles = fs.readdirSync(LOG_DIR)
97
+ .filter(file => file.startsWith('app-') && file.endsWith('.log'))
98
+ .sort()
99
+ .reverse();
100
+
101
+ if (logFiles.length > 10) {
102
+ logFiles.slice(10).forEach(file => {
103
+ try {
104
+ fs.unlinkSync(path.join(LOG_DIR, file));
105
+ } catch (err) {
106
+ console.error(`删除旧日志文件失败: ${err.message}`);
107
+ }
108
+ });
109
+ }
110
+ } catch (err) {
111
+ console.error(`日志文件轮转失败: ${err.message}`);
112
+ logToFile = false;
113
+ }
114
+ }
115
+
116
+ // 添加日志到内存
117
+ function addLogToMemory(level, timestamp, ...args) {
118
+ // 将日志对象添加到内存数组
119
+ const logEntry = {
120
+ level,
121
+ timestamp,
122
+ message: args.map(arg => typeof arg === 'object' ? JSON.stringify(arg) : String(arg)).join(' ')
123
+ };
124
+
125
+ memoryLogs.unshift(logEntry); // 新日志添加到数组开头
126
+
127
+ // 保持数组在最大长度以内
128
+ if (memoryLogs.length > MAX_MEMORY_LOGS) {
129
+ memoryLogs.pop(); // 移除最旧的日志
130
+ }
131
+ }
132
+
133
+ // 将日志写入文件
134
+ function writeLogToFile(level, timestamp, ...args) {
135
+ if (!logToFile) return;
136
+
137
+ try {
138
+ let logEntry;
139
+
140
+ if (logFormat === 'json') {
141
+ // JSON格式
142
+ const data = args.map(arg => typeof arg === 'object' ? arg : String(arg));
143
+ const logObject = {
144
+ level,
145
+ timestamp,
146
+ message: data.length === 1 ? data[0] : data
147
+ };
148
+ logEntry = JSON.stringify(logObject) + '\n';
149
+ } else {
150
+ // 文本格式
151
+ logEntry = `[${level}] ${timestamp} ${args.map(arg =>
152
+ typeof arg === 'object' ? JSON.stringify(arg) : arg
153
+ ).join(' ')}\n`;
154
+ }
155
+
156
+ fs.appendFileSync(LOG_FILE, logEntry);
157
+
158
+ // 检查文件大小,必要时进行轮转
159
+ const stats = fs.statSync(LOG_FILE);
160
+ if (stats.size > MAX_LOG_SIZE) {
161
+ rotateLogFile();
162
+ }
163
+ } catch (err) {
164
+ console.error(`写入日志文件失败: ${err.message}`);
165
+ logToFile = false;
166
+ }
167
+ }
168
+
169
+ // 获取时间戳
170
+ function getTimestamp() {
171
+ return new Date().toISOString();
172
+ }
173
+
174
+ // 设置日志级别
175
+ function setLogLevel(level) {
176
+ if (typeof level === 'string') {
177
+ level = level.toUpperCase();
178
+ if (LOG_LEVELS[level] !== undefined) {
179
+ currentLogLevel = LOG_LEVELS[level];
180
+ } else {
181
+ error(`无效的日志级别: ${level}`);
182
+ }
183
+ } else if (typeof level === 'number' && level >= 0 && level <= 4) {
184
+ currentLogLevel = level;
185
+ } else {
186
+ error(`无效的日志级别: ${level}`);
187
+ }
188
+ }
189
+
190
+ // 设置日志格式
191
+ function setLogFormat(format) {
192
+ const validFormats = ['colored', 'json', 'text'];
193
+ if (validFormats.includes(format)) {
194
+ logFormat = format;
195
+ return true;
196
+ } else {
197
+ error(`无效的日志格式: ${format}`);
198
+ return false;
199
+ }
200
+ }
201
+
202
+ // 格式化控制台日志
203
+ function formatConsoleLog(level, timestamp, color, ...args) {
204
+ if (logFormat === 'json') {
205
+ // JSON格式
206
+ const data = args.map(arg => typeof arg === 'object' ? arg : String(arg));
207
+ return JSON.stringify({
208
+ level,
209
+ timestamp,
210
+ message: data.length === 1 ? data[0] : data
211
+ });
212
+ } else if (logFormat === 'text') {
213
+ // 纯文本格式(无颜色)
214
+ return `[${level}] ${timestamp} ${args.join(' ')}`;
215
+ } else {
216
+ // 默认:带颜色格式
217
+ return `${color}[${level}] ${timestamp}${COLORS.RESET} ${args.join(' ')}`;
218
+ }
219
+ }
220
+
221
+ // 错误日志
222
+ function error(...args) {
223
+ if (currentLogLevel >= LOG_LEVELS.ERROR) {
224
+ const timestamp = getTimestamp();
225
+ const formattedLog = formatConsoleLog('ERROR', timestamp, COLORS.RED, ...args);
226
+ console.error(formattedLog);
227
+ writeLogToFile('ERROR', timestamp, ...args);
228
+ addLogToMemory('ERROR', timestamp, ...args);
229
+ }
230
+ }
231
+
232
+ // 警告日志
233
+ function warn(...args) {
234
+ if (currentLogLevel >= LOG_LEVELS.WARN) {
235
+ const timestamp = getTimestamp();
236
+ const formattedLog = formatConsoleLog('WARN', timestamp, COLORS.YELLOW, ...args);
237
+ console.warn(formattedLog);
238
+ writeLogToFile('WARN', timestamp, ...args);
239
+ addLogToMemory('WARN', timestamp, ...args);
240
+ }
241
+ }
242
+
243
+ // 信息日志
244
+ function info(...args) {
245
+ if (currentLogLevel >= LOG_LEVELS.INFO) {
246
+ const timestamp = getTimestamp();
247
+ const formattedLog = formatConsoleLog('INFO', timestamp, COLORS.GREEN, ...args);
248
+ console.log(formattedLog);
249
+ writeLogToFile('INFO', timestamp, ...args);
250
+ addLogToMemory('INFO', timestamp, ...args);
251
+ }
252
+ }
253
+
254
+ // 调试日志
255
+ function debug(...args) {
256
+ if (currentLogLevel >= LOG_LEVELS.DEBUG) {
257
+ const timestamp = getTimestamp();
258
+ const formattedLog = formatConsoleLog('DEBUG', timestamp, COLORS.BLUE, ...args);
259
+ console.log(formattedLog);
260
+ writeLogToFile('DEBUG', timestamp, ...args);
261
+ addLogToMemory('DEBUG', timestamp, ...args);
262
+ }
263
+ }
264
+
265
+ // 跟踪日志
266
+ function trace(...args) {
267
+ if (currentLogLevel >= LOG_LEVELS.TRACE) {
268
+ const timestamp = getTimestamp();
269
+ const formattedLog = formatConsoleLog('TRACE', timestamp, COLORS.CYAN, ...args);
270
+ console.log(formattedLog);
271
+ writeLogToFile('TRACE', timestamp, ...args);
272
+ addLogToMemory('TRACE', timestamp, ...args);
273
+ }
274
+ }
275
+
276
+ // HTTP请求日志 (特殊处理,方便筛选)
277
+ function http(...args) {
278
+ if (currentLogLevel >= LOG_LEVELS.INFO) {
279
+ const timestamp = getTimestamp();
280
+ const formattedLog = formatConsoleLog('HTTP', timestamp, COLORS.CYAN, ...args);
281
+ console.log(formattedLog);
282
+ writeLogToFile('HTTP', timestamp, ...args);
283
+ addLogToMemory('HTTP', timestamp, ...args);
284
+ }
285
+ }
286
+
287
+ // 获取内存中的日志
288
+ function getLogs(filter = {}) {
289
+ let filteredLogs = [...memoryLogs];
290
+
291
+ // 按日志级别筛选
292
+ if (filter.level) {
293
+ filteredLogs = filteredLogs.filter(log => log.level === filter.level);
294
+ }
295
+
296
+ // 按时间范围筛选
297
+ if (filter.startTime) {
298
+ filteredLogs = filteredLogs.filter(log => new Date(log.timestamp) >= new Date(filter.startTime));
299
+ }
300
+
301
+ if (filter.endTime) {
302
+ filteredLogs = filteredLogs.filter(log => new Date(log.timestamp) <= new Date(filter.endTime));
303
+ }
304
+
305
+ // 按关键词搜索
306
+ if (filter.search) {
307
+ const searchTerm = filter.search.toLowerCase();
308
+ filteredLogs = filteredLogs.filter(log =>
309
+ log.message.toLowerCase().includes(searchTerm) ||
310
+ log.level.toLowerCase().includes(searchTerm)
311
+ );
312
+ }
313
+
314
+ // 分页
315
+ const page = filter.page || 1;
316
+ const pageSize = filter.pageSize || 100;
317
+ const start = (page - 1) * pageSize;
318
+ const end = start + pageSize;
319
+
320
+ return {
321
+ logs: filteredLogs.slice(start, end),
322
+ total: filteredLogs.length,
323
+ page,
324
+ pageSize
325
+ };
326
+ }
327
+
328
+ // 清除内存日志
329
+ function clearMemoryLogs() {
330
+ memoryLogs.length = 0;
331
+ info('内存日志已清除');
332
+ }
333
+
334
+ // 初始化配置
335
+ function initialize() {
336
+ try {
337
+ const conf = getConfig();
338
+
339
+ // 初始化日志级别
340
+ const envLevel = process.env.LOG_LEVEL;
341
+ if (envLevel) {
342
+ setLogLevel(envLevel);
343
+ } else if (conf && conf.log && conf.log.level) {
344
+ setLogLevel(conf.log.level);
345
+ }
346
+
347
+ // 初始化日志格式
348
+ const envFormat = process.env.LOG_FORMAT;
349
+ if (envFormat) {
350
+ setLogFormat(envFormat);
351
+ } else if (conf && conf.log && conf.log.format) {
352
+ setLogFormat(conf.log.format);
353
+ }
354
+
355
+ // 初始化文件日志
356
+ initFileLogging();
357
+ } catch (err) {
358
+ console.error(`初始化日志系统出错: ${err.message}`);
359
+ }
360
+ }
361
+
362
+ // 初始化
363
+ initialize();
364
+
365
+ module.exports = {
366
+ LOG_LEVELS,
367
+ setLogLevel,
368
+ setLogFormat,
369
+ error,
370
+ warn,
371
+ info,
372
+ debug,
373
+ trace,
374
+ http,
375
+ // 暴露文件日志相关方法
376
+ enableFileLogging: () => {
377
+ if (ensureLogDirExists()) {
378
+ logToFile = true;
379
+ info('文件日志已启用');
380
+ return true;
381
+ }
382
+ return false;
383
+ },
384
+ disableFileLogging: () => {
385
+ logToFile = false;
386
+ info('文件日志已禁用');
387
+ },
388
+ rotateLogFile,
389
+ // 添加内存日志相关方法
390
+ getLogs,
391
+ clearMemoryLogs
392
+ };
src/utils/proxyLauncher.js ADDED
@@ -0,0 +1,317 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const { spawn } = require('child_process');
2
+ const path = require('path');
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const logger = require('./logger');
6
+
7
+ let mainProxyProcess = null;
8
+ let othersProxyProcess = null;
9
+ let mainProxyLogStream = null;
10
+ let othersProxyLogStream = null;
11
+
12
+ /**
13
+ * 获取当前系统平台
14
+ * @returns {string} 平台标识
15
+ */
16
+ function detectPlatform() {
17
+ const platform = os.platform();
18
+ const arch = os.arch();
19
+
20
+ if (platform === 'win32' && arch === 'x64') {
21
+ return 'windows_x64';
22
+ } else if (platform === 'linux' && arch === 'x64') {
23
+ return 'linux_x64';
24
+ } else if ((platform === 'android' || platform === 'linux') && (arch === 'arm64' || arch === 'aarch64')) {
25
+ return 'android_arm64';
26
+ }
27
+
28
+ // 默认返回linux版本
29
+ logger.warn(`未识别的平台: ${platform} ${arch},将使用linux_x64代理`);
30
+ return 'linux_x64';
31
+ }
32
+
33
+ /**
34
+ * 获取代理服务器可执行文件路径
35
+ * @param {string} platform 平台类型
36
+ * @param {string} proxyType 代理类型 ('main' 或 'others')
37
+ * @returns {string} 可执行文件路径
38
+ */
39
+ function getProxyExecutablePath(platform, proxyType = 'main') {
40
+ let proxyDir;
41
+
42
+ if (proxyType === 'others') {
43
+ proxyDir = path.join(process.cwd(), 'src', 'proxy', 'others');
44
+ } else {
45
+ proxyDir = path.join(process.cwd(), 'src', 'proxy');
46
+ }
47
+
48
+ // 根据平台选择可执行文件
49
+ switch (platform) {
50
+ case 'windows_x64':
51
+ return path.join(proxyDir, 'cursor_proxy_server_windows_amd64.exe');
52
+ case 'linux_x64':
53
+ return path.join(proxyDir, 'cursor_proxy_server_linux_amd64');
54
+ case 'android_arm64':
55
+ return path.join(proxyDir, 'cursor_proxy_server_android_arm64');
56
+ default:
57
+ logger.warn(`未知平台: ${platform},将使用linux_x64代理`);
58
+ return path.join(proxyDir, 'cursor_proxy_server_linux_amd64');
59
+ }
60
+ }
61
+
62
+ /**
63
+ * 创建并打开代理服务器日志文件
64
+ * @param {string} platform 平台类型
65
+ * @param {string} proxyType 代理类型 ('main' 或 'others')
66
+ * @returns {fs.WriteStream} 日志文件写入流
67
+ */
68
+ function createProxyLogFile(platform, proxyType = 'main') {
69
+ try {
70
+ // 确保logs目录存在
71
+ const logsDir = path.join(process.cwd(), 'logs');
72
+ if (!fs.existsSync(logsDir)) {
73
+ fs.mkdirSync(logsDir, { recursive: true });
74
+ }
75
+
76
+ // 创建日志文件名,包含日期和平台信息
77
+ const now = new Date();
78
+ const dateStr = now.toISOString().split('T')[0];
79
+ const logFileName = `proxy_server_${proxyType}_${platform}_${dateStr}.log`;
80
+ const logFilePath = path.join(logsDir, logFileName);
81
+
82
+ // 创建日志文件流
83
+ const logStream = fs.createWriteStream(logFilePath, { flags: 'a' });
84
+
85
+ // 写入日志文件头
86
+ const headerLine = `\n\n========== ${proxyType}代理服务器日志 - ${platform} - ${now.toISOString()} ==========\n\n`;
87
+ logStream.write(headerLine);
88
+
89
+ logger.info(`${proxyType}代理服务器详细日志将记录到: ${logFilePath}`);
90
+
91
+ return logStream;
92
+ } catch (error) {
93
+ logger.error(`创建${proxyType}代理服务器日志文件失败: ${error.message}`);
94
+ return null;
95
+ }
96
+ }
97
+
98
+ /**
99
+ * 写入日志到代理服务器日志文件
100
+ * @param {fs.WriteStream} logStream 日志文件流
101
+ * @param {string} message 日志消息
102
+ * @param {string} type 日志类型 (stdout 或 stderr)
103
+ */
104
+ function writeToProxyLog(logStream, message, type = 'stdout') {
105
+ if (!logStream) return;
106
+
107
+ try {
108
+ const timestamp = new Date().toISOString();
109
+ const logLine = `[${timestamp}] [${type}] ${message}\n`;
110
+ logStream.write(logLine);
111
+ } catch (error) {
112
+ logger.error(`写入代理服务器日志失败: ${error.message}`);
113
+ }
114
+ }
115
+
116
+ /**
117
+ * 启动单个代理服务器
118
+ * @param {string} platform 平台类型
119
+ * @param {string} proxyType 代理类型 ('main' 或 'others')
120
+ * @param {number} port 代理服务器端口
121
+ * @returns {object} 包含进程和日志流的对象
122
+ */
123
+ function startSingleProxyServer(platform, proxyType, port) {
124
+ try {
125
+ // 获取可执行文件路径
126
+ const execPath = getProxyExecutablePath(platform, proxyType);
127
+
128
+ // 检查文件是否存在
129
+ if (!fs.existsSync(execPath)) {
130
+ logger.error(`${proxyType}代理服务器可执行文件不存在: ${execPath}`);
131
+ return { process: null, logStream: null };
132
+ }
133
+
134
+ // 在Linux/Android上,设置可执行权限
135
+ if (platform !== 'windows_x64') {
136
+ try {
137
+ fs.chmodSync(execPath, '755');
138
+ } catch (err) {
139
+ logger.warn(`无法设置${proxyType}代理服务器可执行权限: ${err.message}`);
140
+ }
141
+ }
142
+
143
+ // 创建代理服务器日志文件
144
+ const logStream = createProxyLogFile(platform, proxyType);
145
+
146
+ // 启动代理服务器进程
147
+ logger.info(`正在启动${platform}平台的${proxyType}���理服务器: ${execPath},端口: ${port}`);
148
+
149
+ // 添加端口参数
150
+ const args = port ? [`--port=${port}`] : [];
151
+
152
+ const proxyProcess = spawn(execPath, args, {
153
+ detached: false,
154
+ stdio: ['ignore', 'pipe', 'pipe']
155
+ });
156
+
157
+ // 记录代理服务器的详细日志到文件
158
+ proxyProcess.stdout.on('data', (data) => {
159
+ const output = data.toString().trim();
160
+ writeToProxyLog(logStream, output, 'stdout');
161
+ });
162
+
163
+ proxyProcess.stderr.on('data', (data) => {
164
+ const errorOutput = data.toString().trim();
165
+ writeToProxyLog(logStream, errorOutput, 'stderr');
166
+
167
+ // 只在启动失败时记录错误信息到控制台
168
+ if (!proxyProcess.startSuccessful && errorOutput.includes('error')) {
169
+ logger.error(`${proxyType}代理服务器启动错误: ${errorOutput.split('\n')[0]}`);
170
+ }
171
+ });
172
+
173
+ proxyProcess.on('error', (err) => {
174
+ logger.error(`${proxyType}代理服务器启动失败: ${err.message}`);
175
+ writeToProxyLog(logStream, `启动失败: ${err.message}`, 'error');
176
+ return { process: null, logStream: null };
177
+ });
178
+
179
+ proxyProcess.on('close', (code) => {
180
+ // 只有在非正常退出时记录到控制台
181
+ if (code !== 0) {
182
+ logger.warn(`${proxyType}代理服务器已退出,代码: ${code}`);
183
+ }
184
+
185
+ writeToProxyLog(logStream, `进程已退出,退出代码: ${code}`, 'info');
186
+
187
+ // 关闭日志文件
188
+ if (logStream) {
189
+ logStream.end();
190
+ }
191
+ });
192
+
193
+ // 等待一段时间确保启动成功
194
+ setTimeout(() => {
195
+ if (proxyProcess && proxyProcess.exitCode === null) {
196
+ proxyProcess.startSuccessful = true;
197
+ logger.info(`${proxyType}代理服务器已成功启动`);
198
+ writeToProxyLog(logStream, `${proxyType}代理服务器已成功启动`, 'info');
199
+ } else {
200
+ logger.error(`${proxyType}代理服务器启动失败或异常退出`);
201
+ writeToProxyLog(logStream, `${proxyType}代理服务器启动失败或异常退出`, 'error');
202
+ }
203
+ }, 1000);
204
+
205
+ return { process: proxyProcess, logStream };
206
+ } catch (error) {
207
+ logger.error(`启动${proxyType}代理服务器出错: ${error.message}`);
208
+ return { process: null, logStream: null };
209
+ }
210
+ }
211
+
212
+ /**
213
+ * 启动代理服务器
214
+ * @returns {boolean} 是否成功启动
215
+ */
216
+ function startProxyServer() {
217
+ try {
218
+ // 检查是否启用代理
219
+ const useTlsProxy = process.env.USE_TLS_PROXY === 'true';
220
+ if (!useTlsProxy) {
221
+ logger.warn('TLS代理服务器未启用,跳过启动');
222
+ return true;
223
+ }
224
+
225
+ // 检查是否启用辅助代理服务器
226
+ const useOthersProxy = process.env.USE_OTHERS_PROXY === 'true';
227
+
228
+ // 确定要使用的平台
229
+ let platform = process.env.PROXY_PLATFORM || 'auto';
230
+ if (platform === 'auto') {
231
+ platform = detectPlatform();
232
+ }
233
+
234
+ // 启动主代理服务器(默认使用8080端口)
235
+ const mainProxy = startSingleProxyServer(platform, 'main', 8080);
236
+ mainProxyProcess = mainProxy.process;
237
+ mainProxyLogStream = mainProxy.logStream;
238
+
239
+ // 根据配置决定是否启动辅助代理服务器
240
+ if (useOthersProxy) {
241
+ logger.info('辅助代理服务器已启用,正在启动...');
242
+ // 启动others代理服务器(端口 10654)
243
+ const othersProxy = startSingleProxyServer(platform, 'others', 10654);
244
+ othersProxyProcess = othersProxy.process;
245
+ othersProxyLogStream = othersProxy.logStream;
246
+
247
+ // 如果辅助代理启动失败,记录警告
248
+ if (!othersProxyProcess) {
249
+ logger.warn('辅助代理服务器启动失败');
250
+ } else {
251
+ logger.info('辅助代理服务器启动成功');
252
+ }
253
+ } else {
254
+ logger.warn('辅助代理服务器未启用,跳过启动');
255
+ }
256
+
257
+ // 如果主代理启动失败,记录警告
258
+ if (!mainProxyProcess) {
259
+ logger.warn('主代理服务器启动失败');
260
+ return false;
261
+ }
262
+
263
+ return true;
264
+ } catch (error) {
265
+ logger.error(`启动代理服务器出错: ${error.message}`);
266
+ return false;
267
+ }
268
+ }
269
+
270
+ /**
271
+ * 停止代理服务器
272
+ */
273
+ function stopProxyServer() {
274
+ const stopSingleProxy = (proxyProcess, logStream, proxyType) => {
275
+ if (proxyProcess) {
276
+ logger.info(`正在停止${proxyType}代理服务器...`);
277
+ writeToProxyLog(logStream, `正在停止${proxyType}代理服务器`, 'info');
278
+
279
+ // 在Windows上,使用taskkill强制终止
280
+ if (os.platform() === 'win32') {
281
+ try {
282
+ spawn('taskkill', ['/pid', proxyProcess.pid, '/f', '/t']);
283
+ } catch (err) {
284
+ logger.error(`使用taskkill终止${proxyType}代理进程失败: ${err.message}`);
285
+ writeToProxyLog(logStream, `使用taskkill终止${proxyType}代理进程失败: ${err.message}`, 'error');
286
+ }
287
+ } else {
288
+ // 在Linux/Mac上直接kill
289
+ proxyProcess.kill('SIGTERM');
290
+ }
291
+
292
+ // 允许一些时间写入最后的日志
293
+ setTimeout(() => {
294
+ // 关闭日志文件
295
+ if (logStream) {
296
+ logStream.end();
297
+ }
298
+ }, 500);
299
+ }
300
+ };
301
+
302
+ // 停止主代理服务器
303
+ stopSingleProxy(mainProxyProcess, mainProxyLogStream, 'main');
304
+ mainProxyProcess = null;
305
+ mainProxyLogStream = null;
306
+
307
+ // 停止others代理服务器
308
+ stopSingleProxy(othersProxyProcess, othersProxyLogStream, 'others');
309
+ othersProxyProcess = null;
310
+ othersProxyLogStream = null;
311
+ }
312
+
313
+ // 导出模块
314
+ module.exports = {
315
+ startProxyServer,
316
+ stopProxyServer
317
+ };
src/utils/utils.js ADDED
@@ -0,0 +1,206 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const os = require('os');
2
+ const zlib = require('zlib');
3
+ const crypto = require('crypto');
4
+ const { v4: uuidv4 } = require('uuid');
5
+ const $root = require('../proto/message.js');
6
+
7
+ function generateCursorBody(messages, modelName) {
8
+
9
+ const instruction = messages
10
+ .filter(msg => msg.role === 'system')
11
+ .map(msg => msg.content)
12
+ .join('\n')
13
+
14
+ const formattedMessages = messages
15
+ .filter(msg => msg.role !== 'system')
16
+ .map(msg => ({
17
+ content: msg.content,
18
+ role: msg.role === 'user' ? 1 : 2,
19
+ messageId: uuidv4(),
20
+ ...(msg.role === 'user' ? { chatModeEnum: 1 } : {})
21
+ //...(msg.role !== 'user' ? { summaryId: uuidv4() } : {})
22
+ }));
23
+
24
+ const messageIds = formattedMessages.map(msg => {
25
+ const { role, messageId, summaryId } = msg;
26
+ return summaryId ? { role, messageId, summaryId } : { role, messageId };
27
+ });
28
+
29
+ const body = {
30
+ request:{
31
+ messages: formattedMessages,
32
+ unknown2: 1,
33
+ instruction: {
34
+ instruction: instruction
35
+ },
36
+ unknown4: 1,
37
+ model: {
38
+ name: modelName,
39
+ empty: '',
40
+ },
41
+ webTool: "",
42
+ unknown13: 1,
43
+ cursorSetting: {
44
+ name: "cursor\\aisettings",
45
+ unknown3: "",
46
+ unknown6: {
47
+ unknwon1: "",
48
+ unknown2: ""
49
+ },
50
+ unknown8: 1,
51
+ unknown9: 1
52
+ },
53
+ unknown19: 1,
54
+ //unknown22: 1,
55
+ conversationId: uuidv4(),
56
+ metadata: {
57
+ os: "win32",
58
+ arch: "x64",
59
+ version: "10.0.22631",
60
+ path: "C:\\Program Files\\PowerShell\\7\\pwsh.exe",
61
+ timestamp: new Date().toISOString(),
62
+ },
63
+ unknown27: 0,
64
+ //unknown29: "",
65
+ messageIds: messageIds,
66
+ largeContext: 0,
67
+ unknown38: 0,
68
+ chatModeEnum: 1,
69
+ unknown47: "",
70
+ unknown48: 0,
71
+ unknown49: 0,
72
+ unknown51: 0,
73
+ unknown53: 1,
74
+ chatMode: "Ask"
75
+ }
76
+ };
77
+
78
+ const errMsg = $root.StreamUnifiedChatWithToolsRequest.verify(body);
79
+ if (errMsg) throw Error(errMsg);
80
+ const instance = $root.StreamUnifiedChatWithToolsRequest.create(body);
81
+ let buffer = $root.StreamUnifiedChatWithToolsRequest.encode(instance).finish();
82
+ let magicNumber = 0x00
83
+ if (formattedMessages.length >= 3){
84
+ buffer = zlib.gzipSync(buffer)
85
+ magicNumber = 0x01
86
+ }
87
+
88
+ const finalBody = Buffer.concat([
89
+ Buffer.from([magicNumber]),
90
+ Buffer.from(buffer.length.toString(16).padStart(8, '0'), 'hex'),
91
+ buffer
92
+ ])
93
+
94
+ return finalBody
95
+ }
96
+
97
+ function chunkToUtf8String(chunk) {
98
+ const results = []
99
+ const thinkingResults = []
100
+ const contentResults = []
101
+ const errorResults = { hasError: false, errorMessage: '' }
102
+ const buffer = Buffer.from(chunk, 'hex');
103
+ //console.log("Chunk buffer:", buffer.toString('hex'))
104
+
105
+ try {
106
+ for(let i = 0; i < buffer.length; i++){
107
+ const magicNumber = parseInt(buffer.subarray(i, i + 1).toString('hex'), 16)
108
+ const dataLength = parseInt(buffer.subarray(i + 1, i + 5).toString('hex'), 16)
109
+ const data = buffer.subarray(i + 5, i + 5 + dataLength)
110
+ //console.log("Parsed buffer:", magicNumber, dataLength, data.toString('hex'))
111
+
112
+ if (magicNumber == 0 || magicNumber == 1) {
113
+ const gunzipData = magicNumber == 0 ? data : zlib.gunzipSync(data)
114
+ const response = $root.StreamUnifiedChatWithToolsResponse.decode(gunzipData);
115
+ const thinking = response?.message?.thinking?.content
116
+ if (thinking !== undefined && thinking.length > 0){
117
+ thinkingResults.push(thinking);
118
+ // console.log('[DEBUG] 收到 thinking:', thinking);
119
+ }
120
+ const content = response?.message?.content
121
+ if (content !== undefined && content.length > 0){
122
+ contentResults.push(content)
123
+ // console.log('[DEBUG] 收到 content:', content);
124
+ }
125
+ }
126
+ else if (magicNumber == 2 || magicNumber == 3) {
127
+ // Json message
128
+ const gunzipData = magicNumber == 2 ? data : zlib.gunzipSync(data)
129
+ const utf8 = gunzipData.toString('utf-8')
130
+ const message = JSON.parse(utf8)
131
+
132
+ if (message != null && (typeof message !== 'object' ||
133
+ (Array.isArray(message) ? message.length > 0 : Object.keys(message).length > 0))){
134
+ //results.push(utf8)
135
+ console.error(utf8)
136
+
137
+ // 检查是否为错误消息
138
+ if (message && message.error) {
139
+ errorResults.hasError = true;
140
+ errorResults.errorMessage = utf8;
141
+ }
142
+ }
143
+ }
144
+ else {
145
+ //console.log('Unknown magic number when parsing chunk response: ' + magicNumber)
146
+ }
147
+
148
+ i += 5 + dataLength - 1
149
+ }
150
+ } catch (err) {
151
+ console.log('Error parsing chunk response:', err)
152
+ }
153
+
154
+ // 如果存在错误,返回错误对象
155
+ if (errorResults.hasError) {
156
+ return { error: errorResults.errorMessage };
157
+ }
158
+
159
+ // 分别返回thinking和content内容
160
+ return {
161
+ reasoning_content: thinkingResults.join(''),
162
+ content: contentResults.join('')
163
+ };
164
+ }
165
+
166
+ function generateHashed64Hex(input, salt = '') {
167
+ const hash = crypto.createHash('sha256');
168
+ hash.update(input + salt);
169
+ return hash.digest('hex');
170
+ }
171
+
172
+ function obfuscateBytes(byteArray) {
173
+ let t = 165;
174
+ for (let r = 0; r < byteArray.length; r++) {
175
+ byteArray[r] = (byteArray[r] ^ t) + (r % 256);
176
+ t = byteArray[r];
177
+ }
178
+ return byteArray;
179
+ }
180
+
181
+ function generateCursorChecksum(token) {
182
+ const machineId = generateHashed64Hex(token, 'machineId');
183
+ const macMachineId = generateHashed64Hex(token, 'macMachineId');
184
+
185
+ const timestamp = Math.floor(Date.now() / 1e6);
186
+ const byteArray = new Uint8Array([
187
+ (timestamp >> 40) & 255,
188
+ (timestamp >> 32) & 255,
189
+ (timestamp >> 24) & 255,
190
+ (timestamp >> 16) & 255,
191
+ (timestamp >> 8) & 255,
192
+ 255 & timestamp,
193
+ ]);
194
+
195
+ const obfuscatedBytes = obfuscateBytes(byteArray);
196
+ const encodedChecksum = Buffer.from(obfuscatedBytes).toString('base64');
197
+
198
+ return `${encodedChecksum}${machineId}/${macMachineId}`;
199
+ }
200
+
201
+ module.exports = {
202
+ generateCursorBody,
203
+ chunkToUtf8String,
204
+ generateHashed64Hex,
205
+ generateCursorChecksum
206
+ };
start.bat ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @echo off
2
+ chcp 65001 >nul
3
+ REM 编码:UTF-8
4
+
5
+ REM 安装依赖
6
+ echo install dependencies...
7
+ call npm install --no-fund --quiet --no-audit
8
+
9
+ REM 检查上一个命令的退出状态
10
+ if %ERRORLEVEL% neq 0 (
11
+ echo dependencies installation failed,maybe start application failed
12
+ )
13
+
14
+ REM 启动应用
15
+ echo start application...
16
+ call npm start
17
+ pause
start.sh ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # 编码:UTF-8
3
+
4
+ # 安装依赖
5
+ echo "install dependencies..."
6
+ npm install --no-fund --quiet --no-audit
7
+
8
+ # 检查上一个命令的退出状态
9
+ if [ $? -ne 0 ]; then
10
+ echo "dependencies installation failed,maybe start application failed"
11
+ fi
12
+
13
+ # 启动应用
14
+ echo "start application..."
15
+ npm start