pigracing commited on
Commit
ec70da3
·
verified ·
1 Parent(s): 8c23c47

Upload 17 files

Browse files
Files changed (17) hide show
  1. Dockerfile +15 -0
  2. package-lock.json +912 -0
  3. package.json +16 -0
  4. src/admin/index.html +655 -0
  5. src/auth.js +463 -0
  6. src/chat.js +177 -0
  7. src/deepseek.js +57 -0
  8. src/headers.js +226 -0
  9. src/index.js +118 -0
  10. src/logger.js +48 -0
  11. src/openai.js +234 -0
  12. src/pow.js +220 -0
  13. src/queue.js +45 -0
  14. src/session.js +99 -0
  15. src/upload.js +107 -0
  16. test_single_flow.mjs +145 -0
  17. test_waf.js +31 -0
Dockerfile ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:20-slim
2
+
3
+ WORKDIR /app
4
+
5
+ ENV NODE_ENV=production \
6
+ PORT=7860
7
+
8
+ COPY package*.json ./
9
+ RUN npm ci --omit=dev
10
+
11
+ COPY . .
12
+
13
+ EXPOSE 7860
14
+
15
+ CMD ["npm", "start"]
package-lock.json ADDED
@@ -0,0 +1,912 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "deepseek-2api",
3
+ "version": "1.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "deepseek-2api",
9
+ "version": "1.0.0",
10
+ "dependencies": {
11
+ "dotenv": "^16.4.0",
12
+ "express": "^4.21.0",
13
+ "ssh2": "^1.17.0"
14
+ }
15
+ },
16
+ "node_modules/accepts": {
17
+ "version": "1.3.8",
18
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
19
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
20
+ "license": "MIT",
21
+ "dependencies": {
22
+ "mime-types": "~2.1.34",
23
+ "negotiator": "0.6.3"
24
+ },
25
+ "engines": {
26
+ "node": ">= 0.6"
27
+ }
28
+ },
29
+ "node_modules/array-flatten": {
30
+ "version": "1.1.1",
31
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
32
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
33
+ "license": "MIT"
34
+ },
35
+ "node_modules/asn1": {
36
+ "version": "0.2.6",
37
+ "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
38
+ "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
39
+ "license": "MIT",
40
+ "dependencies": {
41
+ "safer-buffer": "~2.1.0"
42
+ }
43
+ },
44
+ "node_modules/bcrypt-pbkdf": {
45
+ "version": "1.0.2",
46
+ "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
47
+ "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==",
48
+ "license": "BSD-3-Clause",
49
+ "dependencies": {
50
+ "tweetnacl": "^0.14.3"
51
+ }
52
+ },
53
+ "node_modules/body-parser": {
54
+ "version": "1.20.5",
55
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz",
56
+ "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==",
57
+ "license": "MIT",
58
+ "dependencies": {
59
+ "bytes": "~3.1.2",
60
+ "content-type": "~1.0.5",
61
+ "debug": "2.6.9",
62
+ "depd": "2.0.0",
63
+ "destroy": "~1.2.0",
64
+ "http-errors": "~2.0.1",
65
+ "iconv-lite": "~0.4.24",
66
+ "on-finished": "~2.4.1",
67
+ "qs": "~6.15.1",
68
+ "raw-body": "~2.5.3",
69
+ "type-is": "~1.6.18",
70
+ "unpipe": "~1.0.0"
71
+ },
72
+ "engines": {
73
+ "node": ">= 0.8",
74
+ "npm": "1.2.8000 || >= 1.4.16"
75
+ }
76
+ },
77
+ "node_modules/buildcheck": {
78
+ "version": "0.0.7",
79
+ "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.7.tgz",
80
+ "integrity": "sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==",
81
+ "optional": true,
82
+ "engines": {
83
+ "node": ">=10.0.0"
84
+ }
85
+ },
86
+ "node_modules/bytes": {
87
+ "version": "3.1.2",
88
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
89
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
90
+ "license": "MIT",
91
+ "engines": {
92
+ "node": ">= 0.8"
93
+ }
94
+ },
95
+ "node_modules/call-bind-apply-helpers": {
96
+ "version": "1.0.2",
97
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
98
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
99
+ "license": "MIT",
100
+ "dependencies": {
101
+ "es-errors": "^1.3.0",
102
+ "function-bind": "^1.1.2"
103
+ },
104
+ "engines": {
105
+ "node": ">= 0.4"
106
+ }
107
+ },
108
+ "node_modules/call-bound": {
109
+ "version": "1.0.4",
110
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
111
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
112
+ "license": "MIT",
113
+ "dependencies": {
114
+ "call-bind-apply-helpers": "^1.0.2",
115
+ "get-intrinsic": "^1.3.0"
116
+ },
117
+ "engines": {
118
+ "node": ">= 0.4"
119
+ },
120
+ "funding": {
121
+ "url": "https://github.com/sponsors/ljharb"
122
+ }
123
+ },
124
+ "node_modules/content-disposition": {
125
+ "version": "0.5.4",
126
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
127
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
128
+ "license": "MIT",
129
+ "dependencies": {
130
+ "safe-buffer": "5.2.1"
131
+ },
132
+ "engines": {
133
+ "node": ">= 0.6"
134
+ }
135
+ },
136
+ "node_modules/content-type": {
137
+ "version": "1.0.5",
138
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
139
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
140
+ "license": "MIT",
141
+ "engines": {
142
+ "node": ">= 0.6"
143
+ }
144
+ },
145
+ "node_modules/cookie": {
146
+ "version": "0.7.2",
147
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
148
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
149
+ "license": "MIT",
150
+ "engines": {
151
+ "node": ">= 0.6"
152
+ }
153
+ },
154
+ "node_modules/cookie-signature": {
155
+ "version": "1.0.7",
156
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
157
+ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
158
+ "license": "MIT"
159
+ },
160
+ "node_modules/cpu-features": {
161
+ "version": "0.0.10",
162
+ "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz",
163
+ "integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==",
164
+ "hasInstallScript": true,
165
+ "optional": true,
166
+ "dependencies": {
167
+ "buildcheck": "~0.0.6",
168
+ "nan": "^2.19.0"
169
+ },
170
+ "engines": {
171
+ "node": ">=10.0.0"
172
+ }
173
+ },
174
+ "node_modules/debug": {
175
+ "version": "2.6.9",
176
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
177
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
178
+ "license": "MIT",
179
+ "dependencies": {
180
+ "ms": "2.0.0"
181
+ }
182
+ },
183
+ "node_modules/depd": {
184
+ "version": "2.0.0",
185
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
186
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
187
+ "license": "MIT",
188
+ "engines": {
189
+ "node": ">= 0.8"
190
+ }
191
+ },
192
+ "node_modules/destroy": {
193
+ "version": "1.2.0",
194
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
195
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
196
+ "license": "MIT",
197
+ "engines": {
198
+ "node": ">= 0.8",
199
+ "npm": "1.2.8000 || >= 1.4.16"
200
+ }
201
+ },
202
+ "node_modules/dotenv": {
203
+ "version": "16.6.1",
204
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
205
+ "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
206
+ "license": "BSD-2-Clause",
207
+ "engines": {
208
+ "node": ">=12"
209
+ },
210
+ "funding": {
211
+ "url": "https://dotenvx.com"
212
+ }
213
+ },
214
+ "node_modules/dunder-proto": {
215
+ "version": "1.0.1",
216
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
217
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
218
+ "license": "MIT",
219
+ "dependencies": {
220
+ "call-bind-apply-helpers": "^1.0.1",
221
+ "es-errors": "^1.3.0",
222
+ "gopd": "^1.2.0"
223
+ },
224
+ "engines": {
225
+ "node": ">= 0.4"
226
+ }
227
+ },
228
+ "node_modules/ee-first": {
229
+ "version": "1.1.1",
230
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
231
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
232
+ "license": "MIT"
233
+ },
234
+ "node_modules/encodeurl": {
235
+ "version": "2.0.0",
236
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
237
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
238
+ "license": "MIT",
239
+ "engines": {
240
+ "node": ">= 0.8"
241
+ }
242
+ },
243
+ "node_modules/es-define-property": {
244
+ "version": "1.0.1",
245
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
246
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
247
+ "license": "MIT",
248
+ "engines": {
249
+ "node": ">= 0.4"
250
+ }
251
+ },
252
+ "node_modules/es-errors": {
253
+ "version": "1.3.0",
254
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
255
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
256
+ "license": "MIT",
257
+ "engines": {
258
+ "node": ">= 0.4"
259
+ }
260
+ },
261
+ "node_modules/es-object-atoms": {
262
+ "version": "1.1.1",
263
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
264
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
265
+ "license": "MIT",
266
+ "dependencies": {
267
+ "es-errors": "^1.3.0"
268
+ },
269
+ "engines": {
270
+ "node": ">= 0.4"
271
+ }
272
+ },
273
+ "node_modules/escape-html": {
274
+ "version": "1.0.3",
275
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
276
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
277
+ "license": "MIT"
278
+ },
279
+ "node_modules/etag": {
280
+ "version": "1.8.1",
281
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
282
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
283
+ "license": "MIT",
284
+ "engines": {
285
+ "node": ">= 0.6"
286
+ }
287
+ },
288
+ "node_modules/express": {
289
+ "version": "4.22.2",
290
+ "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz",
291
+ "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==",
292
+ "license": "MIT",
293
+ "dependencies": {
294
+ "accepts": "~1.3.8",
295
+ "array-flatten": "1.1.1",
296
+ "body-parser": "~1.20.5",
297
+ "content-disposition": "~0.5.4",
298
+ "content-type": "~1.0.4",
299
+ "cookie": "~0.7.1",
300
+ "cookie-signature": "~1.0.6",
301
+ "debug": "2.6.9",
302
+ "depd": "2.0.0",
303
+ "encodeurl": "~2.0.0",
304
+ "escape-html": "~1.0.3",
305
+ "etag": "~1.8.1",
306
+ "finalhandler": "~1.3.1",
307
+ "fresh": "~0.5.2",
308
+ "http-errors": "~2.0.0",
309
+ "merge-descriptors": "1.0.3",
310
+ "methods": "~1.1.2",
311
+ "on-finished": "~2.4.1",
312
+ "parseurl": "~1.3.3",
313
+ "path-to-regexp": "~0.1.12",
314
+ "proxy-addr": "~2.0.7",
315
+ "qs": "~6.15.1",
316
+ "range-parser": "~1.2.1",
317
+ "safe-buffer": "5.2.1",
318
+ "send": "~0.19.0",
319
+ "serve-static": "~1.16.2",
320
+ "setprototypeof": "1.2.0",
321
+ "statuses": "~2.0.1",
322
+ "type-is": "~1.6.18",
323
+ "utils-merge": "1.0.1",
324
+ "vary": "~1.1.2"
325
+ },
326
+ "engines": {
327
+ "node": ">= 0.10.0"
328
+ },
329
+ "funding": {
330
+ "type": "opencollective",
331
+ "url": "https://opencollective.com/express"
332
+ }
333
+ },
334
+ "node_modules/finalhandler": {
335
+ "version": "1.3.2",
336
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
337
+ "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
338
+ "license": "MIT",
339
+ "dependencies": {
340
+ "debug": "2.6.9",
341
+ "encodeurl": "~2.0.0",
342
+ "escape-html": "~1.0.3",
343
+ "on-finished": "~2.4.1",
344
+ "parseurl": "~1.3.3",
345
+ "statuses": "~2.0.2",
346
+ "unpipe": "~1.0.0"
347
+ },
348
+ "engines": {
349
+ "node": ">= 0.8"
350
+ }
351
+ },
352
+ "node_modules/forwarded": {
353
+ "version": "0.2.0",
354
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
355
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
356
+ "license": "MIT",
357
+ "engines": {
358
+ "node": ">= 0.6"
359
+ }
360
+ },
361
+ "node_modules/fresh": {
362
+ "version": "0.5.2",
363
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
364
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
365
+ "license": "MIT",
366
+ "engines": {
367
+ "node": ">= 0.6"
368
+ }
369
+ },
370
+ "node_modules/function-bind": {
371
+ "version": "1.1.2",
372
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
373
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
374
+ "license": "MIT",
375
+ "funding": {
376
+ "url": "https://github.com/sponsors/ljharb"
377
+ }
378
+ },
379
+ "node_modules/get-intrinsic": {
380
+ "version": "1.3.0",
381
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
382
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
383
+ "license": "MIT",
384
+ "dependencies": {
385
+ "call-bind-apply-helpers": "^1.0.2",
386
+ "es-define-property": "^1.0.1",
387
+ "es-errors": "^1.3.0",
388
+ "es-object-atoms": "^1.1.1",
389
+ "function-bind": "^1.1.2",
390
+ "get-proto": "^1.0.1",
391
+ "gopd": "^1.2.0",
392
+ "has-symbols": "^1.1.0",
393
+ "hasown": "^2.0.2",
394
+ "math-intrinsics": "^1.1.0"
395
+ },
396
+ "engines": {
397
+ "node": ">= 0.4"
398
+ },
399
+ "funding": {
400
+ "url": "https://github.com/sponsors/ljharb"
401
+ }
402
+ },
403
+ "node_modules/get-proto": {
404
+ "version": "1.0.1",
405
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
406
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
407
+ "license": "MIT",
408
+ "dependencies": {
409
+ "dunder-proto": "^1.0.1",
410
+ "es-object-atoms": "^1.0.0"
411
+ },
412
+ "engines": {
413
+ "node": ">= 0.4"
414
+ }
415
+ },
416
+ "node_modules/gopd": {
417
+ "version": "1.2.0",
418
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
419
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
420
+ "license": "MIT",
421
+ "engines": {
422
+ "node": ">= 0.4"
423
+ },
424
+ "funding": {
425
+ "url": "https://github.com/sponsors/ljharb"
426
+ }
427
+ },
428
+ "node_modules/has-symbols": {
429
+ "version": "1.1.0",
430
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
431
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
432
+ "license": "MIT",
433
+ "engines": {
434
+ "node": ">= 0.4"
435
+ },
436
+ "funding": {
437
+ "url": "https://github.com/sponsors/ljharb"
438
+ }
439
+ },
440
+ "node_modules/hasown": {
441
+ "version": "2.0.3",
442
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
443
+ "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
444
+ "license": "MIT",
445
+ "dependencies": {
446
+ "function-bind": "^1.1.2"
447
+ },
448
+ "engines": {
449
+ "node": ">= 0.4"
450
+ }
451
+ },
452
+ "node_modules/http-errors": {
453
+ "version": "2.0.1",
454
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
455
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
456
+ "license": "MIT",
457
+ "dependencies": {
458
+ "depd": "~2.0.0",
459
+ "inherits": "~2.0.4",
460
+ "setprototypeof": "~1.2.0",
461
+ "statuses": "~2.0.2",
462
+ "toidentifier": "~1.0.1"
463
+ },
464
+ "engines": {
465
+ "node": ">= 0.8"
466
+ },
467
+ "funding": {
468
+ "type": "opencollective",
469
+ "url": "https://opencollective.com/express"
470
+ }
471
+ },
472
+ "node_modules/iconv-lite": {
473
+ "version": "0.4.24",
474
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
475
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
476
+ "license": "MIT",
477
+ "dependencies": {
478
+ "safer-buffer": ">= 2.1.2 < 3"
479
+ },
480
+ "engines": {
481
+ "node": ">=0.10.0"
482
+ }
483
+ },
484
+ "node_modules/inherits": {
485
+ "version": "2.0.4",
486
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
487
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
488
+ "license": "ISC"
489
+ },
490
+ "node_modules/ipaddr.js": {
491
+ "version": "1.9.1",
492
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
493
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
494
+ "license": "MIT",
495
+ "engines": {
496
+ "node": ">= 0.10"
497
+ }
498
+ },
499
+ "node_modules/math-intrinsics": {
500
+ "version": "1.1.0",
501
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
502
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
503
+ "license": "MIT",
504
+ "engines": {
505
+ "node": ">= 0.4"
506
+ }
507
+ },
508
+ "node_modules/media-typer": {
509
+ "version": "0.3.0",
510
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
511
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
512
+ "license": "MIT",
513
+ "engines": {
514
+ "node": ">= 0.6"
515
+ }
516
+ },
517
+ "node_modules/merge-descriptors": {
518
+ "version": "1.0.3",
519
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
520
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
521
+ "license": "MIT",
522
+ "funding": {
523
+ "url": "https://github.com/sponsors/sindresorhus"
524
+ }
525
+ },
526
+ "node_modules/methods": {
527
+ "version": "1.1.2",
528
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
529
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
530
+ "license": "MIT",
531
+ "engines": {
532
+ "node": ">= 0.6"
533
+ }
534
+ },
535
+ "node_modules/mime": {
536
+ "version": "1.6.0",
537
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
538
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
539
+ "license": "MIT",
540
+ "bin": {
541
+ "mime": "cli.js"
542
+ },
543
+ "engines": {
544
+ "node": ">=4"
545
+ }
546
+ },
547
+ "node_modules/mime-db": {
548
+ "version": "1.52.0",
549
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
550
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
551
+ "license": "MIT",
552
+ "engines": {
553
+ "node": ">= 0.6"
554
+ }
555
+ },
556
+ "node_modules/mime-types": {
557
+ "version": "2.1.35",
558
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
559
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
560
+ "license": "MIT",
561
+ "dependencies": {
562
+ "mime-db": "1.52.0"
563
+ },
564
+ "engines": {
565
+ "node": ">= 0.6"
566
+ }
567
+ },
568
+ "node_modules/ms": {
569
+ "version": "2.0.0",
570
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
571
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
572
+ "license": "MIT"
573
+ },
574
+ "node_modules/nan": {
575
+ "version": "2.27.0",
576
+ "resolved": "https://registry.npmjs.org/nan/-/nan-2.27.0.tgz",
577
+ "integrity": "sha512-hC+0LidcL3XE4rp1C4H54KujgXKzbfyTngZTwBByQxsOxCEKZT0MPQ4hOKUH2jU1OYstqdDH4onyHPDzcV0XdQ==",
578
+ "license": "MIT",
579
+ "optional": true
580
+ },
581
+ "node_modules/negotiator": {
582
+ "version": "0.6.3",
583
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
584
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
585
+ "license": "MIT",
586
+ "engines": {
587
+ "node": ">= 0.6"
588
+ }
589
+ },
590
+ "node_modules/object-inspect": {
591
+ "version": "1.13.4",
592
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
593
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
594
+ "license": "MIT",
595
+ "engines": {
596
+ "node": ">= 0.4"
597
+ },
598
+ "funding": {
599
+ "url": "https://github.com/sponsors/ljharb"
600
+ }
601
+ },
602
+ "node_modules/on-finished": {
603
+ "version": "2.4.1",
604
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
605
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
606
+ "license": "MIT",
607
+ "dependencies": {
608
+ "ee-first": "1.1.1"
609
+ },
610
+ "engines": {
611
+ "node": ">= 0.8"
612
+ }
613
+ },
614
+ "node_modules/parseurl": {
615
+ "version": "1.3.3",
616
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
617
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
618
+ "license": "MIT",
619
+ "engines": {
620
+ "node": ">= 0.8"
621
+ }
622
+ },
623
+ "node_modules/path-to-regexp": {
624
+ "version": "0.1.13",
625
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
626
+ "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
627
+ "license": "MIT"
628
+ },
629
+ "node_modules/proxy-addr": {
630
+ "version": "2.0.7",
631
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
632
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
633
+ "license": "MIT",
634
+ "dependencies": {
635
+ "forwarded": "0.2.0",
636
+ "ipaddr.js": "1.9.1"
637
+ },
638
+ "engines": {
639
+ "node": ">= 0.10"
640
+ }
641
+ },
642
+ "node_modules/qs": {
643
+ "version": "6.15.2",
644
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
645
+ "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
646
+ "license": "BSD-3-Clause",
647
+ "dependencies": {
648
+ "side-channel": "^1.1.0"
649
+ },
650
+ "engines": {
651
+ "node": ">=0.6"
652
+ },
653
+ "funding": {
654
+ "url": "https://github.com/sponsors/ljharb"
655
+ }
656
+ },
657
+ "node_modules/range-parser": {
658
+ "version": "1.2.1",
659
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
660
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
661
+ "license": "MIT",
662
+ "engines": {
663
+ "node": ">= 0.6"
664
+ }
665
+ },
666
+ "node_modules/raw-body": {
667
+ "version": "2.5.3",
668
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
669
+ "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
670
+ "license": "MIT",
671
+ "dependencies": {
672
+ "bytes": "~3.1.2",
673
+ "http-errors": "~2.0.1",
674
+ "iconv-lite": "~0.4.24",
675
+ "unpipe": "~1.0.0"
676
+ },
677
+ "engines": {
678
+ "node": ">= 0.8"
679
+ }
680
+ },
681
+ "node_modules/safe-buffer": {
682
+ "version": "5.2.1",
683
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
684
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
685
+ "funding": [
686
+ {
687
+ "type": "github",
688
+ "url": "https://github.com/sponsors/feross"
689
+ },
690
+ {
691
+ "type": "patreon",
692
+ "url": "https://www.patreon.com/feross"
693
+ },
694
+ {
695
+ "type": "consulting",
696
+ "url": "https://feross.org/support"
697
+ }
698
+ ],
699
+ "license": "MIT"
700
+ },
701
+ "node_modules/safer-buffer": {
702
+ "version": "2.1.2",
703
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
704
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
705
+ "license": "MIT"
706
+ },
707
+ "node_modules/send": {
708
+ "version": "0.19.2",
709
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
710
+ "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
711
+ "license": "MIT",
712
+ "dependencies": {
713
+ "debug": "2.6.9",
714
+ "depd": "2.0.0",
715
+ "destroy": "1.2.0",
716
+ "encodeurl": "~2.0.0",
717
+ "escape-html": "~1.0.3",
718
+ "etag": "~1.8.1",
719
+ "fresh": "~0.5.2",
720
+ "http-errors": "~2.0.1",
721
+ "mime": "1.6.0",
722
+ "ms": "2.1.3",
723
+ "on-finished": "~2.4.1",
724
+ "range-parser": "~1.2.1",
725
+ "statuses": "~2.0.2"
726
+ },
727
+ "engines": {
728
+ "node": ">= 0.8.0"
729
+ }
730
+ },
731
+ "node_modules/send/node_modules/ms": {
732
+ "version": "2.1.3",
733
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
734
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
735
+ "license": "MIT"
736
+ },
737
+ "node_modules/serve-static": {
738
+ "version": "1.16.3",
739
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
740
+ "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
741
+ "license": "MIT",
742
+ "dependencies": {
743
+ "encodeurl": "~2.0.0",
744
+ "escape-html": "~1.0.3",
745
+ "parseurl": "~1.3.3",
746
+ "send": "~0.19.1"
747
+ },
748
+ "engines": {
749
+ "node": ">= 0.8.0"
750
+ }
751
+ },
752
+ "node_modules/setprototypeof": {
753
+ "version": "1.2.0",
754
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
755
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
756
+ "license": "ISC"
757
+ },
758
+ "node_modules/side-channel": {
759
+ "version": "1.1.0",
760
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
761
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
762
+ "license": "MIT",
763
+ "dependencies": {
764
+ "es-errors": "^1.3.0",
765
+ "object-inspect": "^1.13.3",
766
+ "side-channel-list": "^1.0.0",
767
+ "side-channel-map": "^1.0.1",
768
+ "side-channel-weakmap": "^1.0.2"
769
+ },
770
+ "engines": {
771
+ "node": ">= 0.4"
772
+ },
773
+ "funding": {
774
+ "url": "https://github.com/sponsors/ljharb"
775
+ }
776
+ },
777
+ "node_modules/side-channel-list": {
778
+ "version": "1.0.1",
779
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
780
+ "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
781
+ "license": "MIT",
782
+ "dependencies": {
783
+ "es-errors": "^1.3.0",
784
+ "object-inspect": "^1.13.4"
785
+ },
786
+ "engines": {
787
+ "node": ">= 0.4"
788
+ },
789
+ "funding": {
790
+ "url": "https://github.com/sponsors/ljharb"
791
+ }
792
+ },
793
+ "node_modules/side-channel-map": {
794
+ "version": "1.0.1",
795
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
796
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
797
+ "license": "MIT",
798
+ "dependencies": {
799
+ "call-bound": "^1.0.2",
800
+ "es-errors": "^1.3.0",
801
+ "get-intrinsic": "^1.2.5",
802
+ "object-inspect": "^1.13.3"
803
+ },
804
+ "engines": {
805
+ "node": ">= 0.4"
806
+ },
807
+ "funding": {
808
+ "url": "https://github.com/sponsors/ljharb"
809
+ }
810
+ },
811
+ "node_modules/side-channel-weakmap": {
812
+ "version": "1.0.2",
813
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
814
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
815
+ "license": "MIT",
816
+ "dependencies": {
817
+ "call-bound": "^1.0.2",
818
+ "es-errors": "^1.3.0",
819
+ "get-intrinsic": "^1.2.5",
820
+ "object-inspect": "^1.13.3",
821
+ "side-channel-map": "^1.0.1"
822
+ },
823
+ "engines": {
824
+ "node": ">= 0.4"
825
+ },
826
+ "funding": {
827
+ "url": "https://github.com/sponsors/ljharb"
828
+ }
829
+ },
830
+ "node_modules/ssh2": {
831
+ "version": "1.17.0",
832
+ "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.17.0.tgz",
833
+ "integrity": "sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==",
834
+ "hasInstallScript": true,
835
+ "dependencies": {
836
+ "asn1": "^0.2.6",
837
+ "bcrypt-pbkdf": "^1.0.2"
838
+ },
839
+ "engines": {
840
+ "node": ">=10.16.0"
841
+ },
842
+ "optionalDependencies": {
843
+ "cpu-features": "~0.0.10",
844
+ "nan": "^2.23.0"
845
+ }
846
+ },
847
+ "node_modules/statuses": {
848
+ "version": "2.0.2",
849
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
850
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
851
+ "license": "MIT",
852
+ "engines": {
853
+ "node": ">= 0.8"
854
+ }
855
+ },
856
+ "node_modules/toidentifier": {
857
+ "version": "1.0.1",
858
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
859
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
860
+ "license": "MIT",
861
+ "engines": {
862
+ "node": ">=0.6"
863
+ }
864
+ },
865
+ "node_modules/tweetnacl": {
866
+ "version": "0.14.5",
867
+ "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
868
+ "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==",
869
+ "license": "Unlicense"
870
+ },
871
+ "node_modules/type-is": {
872
+ "version": "1.6.18",
873
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
874
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
875
+ "license": "MIT",
876
+ "dependencies": {
877
+ "media-typer": "0.3.0",
878
+ "mime-types": "~2.1.24"
879
+ },
880
+ "engines": {
881
+ "node": ">= 0.6"
882
+ }
883
+ },
884
+ "node_modules/unpipe": {
885
+ "version": "1.0.0",
886
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
887
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
888
+ "license": "MIT",
889
+ "engines": {
890
+ "node": ">= 0.8"
891
+ }
892
+ },
893
+ "node_modules/utils-merge": {
894
+ "version": "1.0.1",
895
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
896
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
897
+ "license": "MIT",
898
+ "engines": {
899
+ "node": ">= 0.4.0"
900
+ }
901
+ },
902
+ "node_modules/vary": {
903
+ "version": "1.1.2",
904
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
905
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
906
+ "license": "MIT",
907
+ "engines": {
908
+ "node": ">= 0.8"
909
+ }
910
+ }
911
+ }
912
+ }
package.json ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "deepseek-2api",
3
+ "version": "1.0.0",
4
+ "description": "DeepSeek Web Chat to API proxy (OpenAI + DeepSeek native format)",
5
+ "main": "src/index.js",
6
+ "type": "module",
7
+ "scripts": {
8
+ "start": "node src/index.js",
9
+ "dev": "node --watch src/index.js"
10
+ },
11
+ "dependencies": {
12
+ "dotenv": "^16.4.0",
13
+ "express": "^4.21.0",
14
+ "ssh2": "^1.17.0"
15
+ }
16
+ }
src/admin/index.html ADDED
@@ -0,0 +1,655 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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">
6
+ <title>DeepSeek-2API 管理面板</title>
7
+ <style>
8
+ :root {
9
+ --bg: #1a1a2e;
10
+ --surface: #16213e;
11
+ --border: #0f3460;
12
+ --text: #e0e0e0;
13
+ --text-dim: #8892a0;
14
+ --green: #00e676;
15
+ --yellow: #ffd600;
16
+ --red: #ff1744;
17
+ --accent: #00b0ff;
18
+ }
19
+ * { margin: 0; padding: 0; box-sizing: border-box; }
20
+ body {
21
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
22
+ background: var(--bg);
23
+ color: var(--text);
24
+ line-height: 1.6;
25
+ min-height: 100vh;
26
+ }
27
+ .container {
28
+ max-width: 960px;
29
+ margin: 0 auto;
30
+ padding: 20px;
31
+ }
32
+ header {
33
+ display: flex;
34
+ align-items: center;
35
+ justify-content: space-between;
36
+ padding: 16px 20px;
37
+ background: var(--surface);
38
+ border: 1px solid var(--border);
39
+ border-radius: 8px;
40
+ margin-bottom: 20px;
41
+ }
42
+ header h1 {
43
+ font-size: 1.3em;
44
+ color: var(--accent);
45
+ }
46
+ header .meta {
47
+ font-size: 0.85em;
48
+ color: var(--text-dim);
49
+ }
50
+ .controls {
51
+ display: flex;
52
+ align-items: center;
53
+ gap: 8px;
54
+ }
55
+ .controls select, .controls button {
56
+ background: var(--bg);
57
+ color: var(--text);
58
+ border: 1px solid var(--border);
59
+ border-radius: 4px;
60
+ padding: 4px 10px;
61
+ font-size: 0.85em;
62
+ cursor: pointer;
63
+ }
64
+ .controls button:hover { border-color: var(--accent); }
65
+ .card {
66
+ background: var(--surface);
67
+ border: 1px solid var(--border);
68
+ border-radius: 8px;
69
+ padding: 16px 20px;
70
+ margin-bottom: 16px;
71
+ }
72
+ .card h2 {
73
+ font-size: 1.05em;
74
+ margin-bottom: 12px;
75
+ color: var(--accent);
76
+ }
77
+ .stats-grid {
78
+ display: grid;
79
+ grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
80
+ gap: 12px;
81
+ }
82
+ .stat-item {
83
+ background: var(--bg);
84
+ border-radius: 6px;
85
+ padding: 12px;
86
+ text-align: center;
87
+ }
88
+ .stat-item .label {
89
+ font-size: 0.75em;
90
+ color: var(--text-dim);
91
+ margin-bottom: 4px;
92
+ }
93
+ .stat-item .value {
94
+ font-size: 1.4em;
95
+ font-weight: 700;
96
+ }
97
+ .status-ok { color: var(--green); }
98
+ .status-warn { color: var(--yellow); }
99
+ .status-err { color: var(--red); }
100
+
101
+ table {
102
+ width: 100%;
103
+ border-collapse: collapse;
104
+ font-size: 0.88em;
105
+ }
106
+ th, td {
107
+ padding: 8px 10px;
108
+ text-align: left;
109
+ border-bottom: 1px solid var(--border);
110
+ }
111
+ th {
112
+ color: var(--text-dim);
113
+ font-weight: 600;
114
+ font-size: 0.8em;
115
+ text-transform: uppercase;
116
+ letter-spacing: 0.5px;
117
+ }
118
+ td { font-family: 'SF Mono', 'Consolas', monospace; font-size: 0.85em; }
119
+ .dot {
120
+ display: inline-block;
121
+ width: 8px;
122
+ height: 8px;
123
+ border-radius: 50%;
124
+ margin-right: 6px;
125
+ }
126
+ .dot-green { background: var(--green); }
127
+ .dot-yellow { background: var(--yellow); }
128
+ .dot-red { background: var(--red); }
129
+
130
+ .bar-wrap {
131
+ background: var(--bg);
132
+ border-radius: 4px;
133
+ height: 22px;
134
+ overflow: hidden;
135
+ position: relative;
136
+ }
137
+ .bar-fill {
138
+ height: 100%;
139
+ border-radius: 4px;
140
+ transition: width 0.4s, background 0.4s;
141
+ }
142
+ .bar-label {
143
+ position: absolute;
144
+ top: 0; left: 10px; right: 10px;
145
+ line-height: 22px;
146
+ font-size: 0.78em;
147
+ color: var(--text);
148
+ text-shadow: 0 1px 2px rgba(0,0,0,0.6);
149
+ }
150
+
151
+ .forms-row {
152
+ display: grid;
153
+ grid-template-columns: 1fr 1fr;
154
+ gap: 16px;
155
+ }
156
+ @media (max-width: 640px) {
157
+ .forms-row { grid-template-columns: 1fr; }
158
+ }
159
+ .form-group { margin-bottom: 10px; }
160
+ .form-group label {
161
+ display: block;
162
+ font-size: 0.82em;
163
+ color: var(--text-dim);
164
+ margin-bottom: 4px;
165
+ }
166
+ .form-group input {
167
+ width: 100%;
168
+ background: var(--bg);
169
+ color: var(--text);
170
+ border: 1px solid var(--border);
171
+ border-radius: 4px;
172
+ padding: 7px 10px;
173
+ font-size: 0.9em;
174
+ }
175
+ .form-group input:focus { outline: none; border-color: var(--accent); }
176
+ .btn {
177
+ background: var(--accent);
178
+ color: #fff;
179
+ border: none;
180
+ border-radius: 4px;
181
+ padding: 7px 18px;
182
+ font-size: 0.9em;
183
+ cursor: pointer;
184
+ }
185
+ .btn:hover { opacity: 0.85; }
186
+ .msg {
187
+ font-size: 0.82em;
188
+ margin-top: 6px;
189
+ min-height: 1.4em;
190
+ }
191
+ .msg-ok { color: var(--green); }
192
+ .msg-err { color: var(--red); }
193
+
194
+ details {
195
+ margin-bottom: 10px;
196
+ }
197
+ summary {
198
+ cursor: pointer;
199
+ color: var(--accent);
200
+ font-size: 0.92em;
201
+ padding: 4px 0;
202
+ }
203
+ pre {
204
+ background: var(--bg);
205
+ border: 1px solid var(--border);
206
+ border-radius: 4px;
207
+ padding: 10px 12px;
208
+ font-size: 0.82em;
209
+ overflow-x: auto;
210
+ color: var(--text);
211
+ margin: 6px 0;
212
+ }
213
+ .model-table {
214
+ width: auto;
215
+ margin-bottom: 12px;
216
+ }
217
+ .model-table td, .model-table th {
218
+ padding: 6px 16px;
219
+ }
220
+
221
+ footer {
222
+ text-align: center;
223
+ color: var(--text-dim);
224
+ font-size: 0.78em;
225
+ padding: 12px;
226
+ }
227
+
228
+ .auth-overlay {
229
+ position: fixed;
230
+ inset: 0;
231
+ background: rgba(0,0,0,0.7);
232
+ display: flex;
233
+ align-items: center;
234
+ justify-content: center;
235
+ z-index: 100;
236
+ }
237
+ .auth-box {
238
+ background: var(--surface);
239
+ border: 1px solid var(--border);
240
+ border-radius: 8px;
241
+ padding: 24px;
242
+ width: 340px;
243
+ }
244
+ .auth-box h3 { margin-bottom: 12px; color: var(--accent); }
245
+ .auth-box .form-group { margin-bottom: 14px; }
246
+ .auth-box .msg { margin-bottom: 8px; }
247
+
248
+ .loading { color: var(--text-dim); font-style: italic; }
249
+ </style>
250
+ </head>
251
+ <body>
252
+ <div class="container">
253
+ <header>
254
+ <div>
255
+ <h1>DeepSeek-2API 管理面板</h1>
256
+ <div class="meta">版本 <span id="version">-</span> · 运行 <span id="uptime">-</span></div>
257
+ </div>
258
+ <div class="controls">
259
+ <select id="interval" title="自动刷新间隔">
260
+ <option value="5000">5秒</option>
261
+ <option value="10000" selected>10秒</option>
262
+ <option value="30000">30秒</option>
263
+ <option value="0">关闭</option>
264
+ </select>
265
+ <button id="refreshBtn">刷新</button>
266
+ </div>
267
+ </header>
268
+
269
+ <!-- 总览 -->
270
+ <div class="card">
271
+ <h2>总览</h2>
272
+ <div class="stats-grid">
273
+ <div class="stat-item">
274
+ <div class="label">状态</div>
275
+ <div class="value" id="status">-</div>
276
+ </div>
277
+ <div class="stat-item">
278
+ <div class="label">总容量</div>
279
+ <div class="value" id="capacity">-</div>
280
+ </div>
281
+ <div class="stat-item">
282
+ <div class="label">令牌数</div>
283
+ <div class="value" id="tokenCount">-</div>
284
+ </div>
285
+ <div class="stat-item">
286
+ <div class="label">会话缓存</div>
287
+ <div class="value" id="sessionCount">-</div>
288
+ </div>
289
+ </div>
290
+ </div>
291
+
292
+ <!-- 请求队列 -->
293
+ <div class="card">
294
+ <h2>请求队列</h2>
295
+ <div class="bar-wrap">
296
+ <div class="bar-fill" id="queueBar" style="width:0%;background:var(--green)"></div>
297
+ <div class="bar-label" id="queueLabel">0 / 100</div>
298
+ </div>
299
+ </div>
300
+
301
+ <!-- 令牌池 -->
302
+ <div class="card">
303
+ <h2>令牌池</h2>
304
+ <div style="overflow-x:auto">
305
+ <table>
306
+ <thead>
307
+ <tr><th>令牌</th><th>视觉能力</th><th>错误数</th><th>活跃/最大</th><th>健康</th></tr>
308
+ </thead>
309
+ <tbody id="poolBody">
310
+ <tr><td colspan="5" class="loading">加载中...</td></tr>
311
+ </tbody>
312
+ </table>
313
+ </div>
314
+ </div>
315
+
316
+ <!-- 会话缓存 -->
317
+ <div class="card">
318
+ <h2>会话缓存</h2>
319
+ <div id="sessionSummary" style="font-size:0.88em;color:var(--text-dim);margin-bottom:8px">-</div>
320
+ <div style="overflow-x:auto">
321
+ <table>
322
+ <thead>
323
+ <tr><th>缓存键</th><th>模型</th><th>已用时间</th><th>剩余 TTL</th></tr>
324
+ </thead>
325
+ <tbody id="sessionBody">
326
+ <tr><td colspan="4" class="loading">加载中...</td></tr>
327
+ </tbody>
328
+ </table>
329
+ </div>
330
+ </div>
331
+
332
+ <!-- 快速入门 -->
333
+ <div class="card">
334
+ <h2>快速入门</h2>
335
+ <h3 style="font-size:0.92em;margin-bottom:8px">模型映射</h3>
336
+ <table class="model-table">
337
+ <thead><tr><th>API 模型名</th><th>DeepSeek 模型类型</th></tr></thead>
338
+ <tbody>
339
+ <tr><td>deepseek-v4-flash</td><td>default(默认/免费)</td></tr>
340
+ <tr><td>deepseek-v4-pro</td><td>expert(深度思考/付费)</td></tr>
341
+ <tr><td>deepseek-v4-vision</td><td>vision(视觉/图片理解)</td></tr>
342
+ </tbody>
343
+ </table>
344
+
345
+ <details>
346
+ <summary>OpenAI 格式 - 非流式请求</summary>
347
+ <pre>curl http://localhost:3000/v1/chat/completions \
348
+ -H "Content-Type: application/json" \
349
+ -H "Authorization: Bearer YOUR_API_KEY" \
350
+ -d '{
351
+ "model": "deepseek-v4-flash",
352
+ "messages": [{"role":"user","content":"你好"}]
353
+ }'</pre>
354
+ </details>
355
+
356
+ <details>
357
+ <summary>OpenAI 格式 - 流式请求(SSE)</summary>
358
+ <pre>curl http://localhost:3000/v1/chat/completions \
359
+ -H "Content-Type: application/json" \
360
+ -H "Authorization: Bearer YOUR_API_KEY" \
361
+ -d '{
362
+ "model": "deepseek-v4-pro",
363
+ "stream": true,
364
+ "messages": [{"role":"user","content":"解释量子计算"}]
365
+ }'</pre>
366
+ </details>
367
+
368
+ <details>
369
+ <summary>DeepSeek 原生格式</summary>
370
+ <pre>curl http://localhost:3000/api/v0/chat/completion \
371
+ -H "Content-Type: application/json" \
372
+ -H "Authorization: Bearer YOUR_API_KEY" \
373
+ -d '{
374
+ "model_type": "default",
375
+ "stream": true,
376
+ "chat_session_id": "SESSION_ID",
377
+ "prompt": "你好"
378
+ }'</pre>
379
+ </details>
380
+
381
+ <details>
382
+ <summary>查看可用模型</summary>
383
+ <pre>curl http://localhost:3000/v1/models \
384
+ -H "Authorization: Bearer YOUR_API_KEY"</pre>
385
+ </details>
386
+ </div>
387
+
388
+ <!-- 令牌管理 -->
389
+ <div class="card">
390
+ <h2>令牌管理</h2>
391
+ <div class="forms-row">
392
+ <div>
393
+ <h3 style="font-size:0.92em;margin-bottom:8px">添加令牌</h3>
394
+ <form id="addTokenForm">
395
+ <div class="form-group">
396
+ <label>令牌字符串</label>
397
+ <input type="text" id="addTokenInput" placeholder="粘贴 DeepSeek 令牌..." required>
398
+ </div>
399
+ <button type="submit" class="btn">添加</button>
400
+ <div class="msg" id="addTokenMsg"></div>
401
+ </form>
402
+ </div>
403
+ <div>
404
+ <h3 style="font-size:0.92em;margin-bottom:8px">登录添加</h3>
405
+ <form id="loginForm">
406
+ <div class="form-group">
407
+ <label>邮箱</label>
408
+ <input type="email" id="loginEmail" placeholder="DeepSeek 账号邮箱" required>
409
+ </div>
410
+ <div class="form-group">
411
+ <label>密码</label>
412
+ <input type="password" id="loginPassword" placeholder="DeepSeek 账号密码" required>
413
+ </div>
414
+ <button type="submit" class="btn">登录</button>
415
+ <div class="msg" id="loginMsg"></div>
416
+ </form>
417
+ </div>
418
+ </div>
419
+ </div>
420
+
421
+ <footer>上次刷新: <span id="lastRefresh">-</span></footer>
422
+ </div>
423
+
424
+ <!-- Auth overlay -->
425
+ <div class="auth-overlay" id="authOverlay" style="display:none">
426
+ <div class="auth-box">
427
+ <h3>需要 API Key</h3>
428
+ <p style="font-size:0.85em;color:var(--text-dim);margin-bottom:12px">此服务器已配置 API_KEY 验证,请输入密钥。</p>
429
+ <div class="form-group">
430
+ <label>API Key</label>
431
+ <input type="password" id="authKeyInput" placeholder="输入 API Key...">
432
+ </div>
433
+ <button class="btn" id="authSubmitBtn">确认</button>
434
+ <div class="msg" id="authMsg"></div>
435
+ </div>
436
+ </div>
437
+
438
+ <script>
439
+ const $ = id => document.getElementById(id);
440
+ let refreshTimer = null;
441
+
442
+ function getApiKey() {
443
+ return sessionStorage.getItem('adminApiKey') || '';
444
+ }
445
+
446
+ function authHeaders() {
447
+ const key = getApiKey();
448
+ return key ? { 'Authorization': 'Bearer ' + key } : {};
449
+ }
450
+
451
+ async function apiFetch(url, options = {}) {
452
+ const headers = { ...authHeaders(), ...(options.headers || {}) };
453
+ const res = await fetch(url, { ...options, headers });
454
+ if (res.status === 401) {
455
+ sessionStorage.removeItem('adminApiKey');
456
+ $('authOverlay').style.display = 'flex';
457
+ throw new Error('unauthorized');
458
+ }
459
+ return res;
460
+ }
461
+
462
+ function formatUptime(sec) {
463
+ const d = Math.floor(sec / 86400);
464
+ const h = Math.floor((sec % 86400) / 3600);
465
+ const m = Math.floor((sec % 3600) / 60);
466
+ const s = sec % 60;
467
+ const parts = [];
468
+ if (d) parts.push(d + '天');
469
+ if (h) parts.push(h + '时');
470
+ if (m) parts.push(m + '分');
471
+ parts.push(s + '秒');
472
+ return parts.join(' ');
473
+ }
474
+
475
+ function formatDuration(sec) {
476
+ if (sec < 60) return sec + '秒';
477
+ if (sec < 3600) return Math.floor(sec / 60) + '分' + (sec % 60) + '秒';
478
+ if (sec < 86400) return Math.floor(sec / 3600) + '时' + Math.floor((sec % 3600) / 60) + '分';
479
+ return Math.floor(sec / 86400) + '天' + Math.floor((sec % 86400) / 3600) + '时';
480
+ }
481
+
482
+ function healthDot(errorCount, active, max) {
483
+ if (errorCount >= 3) return '<span class="dot dot-red"></span>不可用';
484
+ if (errorCount >= 1) return '<span class="dot dot-yellow"></span>异常';
485
+ if (active >= max) return '<span class="dot dot-yellow"></span>满载';
486
+ return '<span class="dot dot-green"></span>正常';
487
+ }
488
+
489
+ function visionLabel(v) {
490
+ if (v === true) return '<span style="color:var(--green)">支持</span>';
491
+ if (v === false) return '<span style="color:var(--text-dim)">不支持</span>';
492
+ return '<span style="color:var(--yellow)">未知</span>';
493
+ }
494
+
495
+ function render(data) {
496
+ $('version').textContent = data.version;
497
+ $('uptime').textContent = formatUptime(data.uptimeSeconds);
498
+ $('status').innerHTML = '<span class="status-ok">正常</span>';
499
+ $('capacity').textContent = data.totalCapacity;
500
+ $('tokenCount').textContent = data.pool.length;
501
+ $('sessionCount').textContent = data.sessions.count;
502
+
503
+ // Queue bar
504
+ const q = data.queue.queued;
505
+ const qMax = data.queue.maxQueueSize;
506
+ const pct = Math.min(100, (q / qMax) * 100);
507
+ const qBar = $('queueBar');
508
+ qBar.style.width = pct + '%';
509
+ qBar.style.background = pct > 80 ? 'var(--red)' : pct > 50 ? 'var(--yellow)' : 'var(--green)';
510
+ $('queueLabel').textContent = q + ' / ' + qMax;
511
+
512
+ // Token pool
513
+ const poolBody = $('poolBody');
514
+ if (data.pool.length === 0) {
515
+ poolBody.innerHTML = '<tr><td colspan="5" style="color:var(--text-dim)">无令牌</td></tr>';
516
+ } else {
517
+ poolBody.innerHTML = data.pool.map(t => `<tr>
518
+ <td>${t.token}</td>
519
+ <td>${visionLabel(t.visionCapable)}</td>
520
+ <td>${t.errorCount}</td>
521
+ <td>${t.activeRequests} / ${t.maxConcurrent}</td>
522
+ <td>${healthDot(t.errorCount, t.activeRequests, t.maxConcurrent)}</td>
523
+ </tr>`).join('');
524
+ }
525
+
526
+ // Sessions
527
+ const sessions = data.sessions;
528
+ $('sessionSummary').textContent = sessions.count + ' 个会话已缓存 · TTL: ' + formatDuration(sessions.ttl);
529
+ const sBody = $('sessionBody');
530
+ if (sessions.sessions.length === 0) {
531
+ sBody.innerHTML = '<tr><td colspan="4" style="color:var(--text-dim)">无缓存会话</td></tr>';
532
+ } else {
533
+ sBody.innerHTML = sessions.sessions.map(s => {
534
+ const ttlStyle = s.ttlRemainingSeconds < 3600 ? 'color:var(--yellow)' : '';
535
+ const expiredStyle = s.ttlRemainingSeconds === 0 ? 'color:var(--red)' : '';
536
+ return `<tr>
537
+ <td>${s.key}</td>
538
+ <td>${s.modelType}</td>
539
+ <td>${formatDuration(s.ageSeconds)}</td>
540
+ <td style="${expiredStyle || ttlStyle}">${s.ttlRemainingSeconds === 0 ? '已过期' : formatDuration(s.ttlRemainingSeconds)}</td>
541
+ </tr>`;
542
+ }).join('');
543
+ }
544
+
545
+ $('lastRefresh').textContent = new Date().toLocaleTimeString();
546
+ }
547
+
548
+ async function fetchStats() {
549
+ try {
550
+ const res = await apiFetch('/admin/api/stats');
551
+ if (!res.ok) throw new Error('HTTP ' + res.status);
552
+ const data = await res.json();
553
+ render(data);
554
+ } catch (e) {
555
+ if (e.message !== 'unauthorized') {
556
+ console.error('fetchStats error:', e);
557
+ }
558
+ }
559
+ }
560
+
561
+ // Auto-refresh
562
+ function setupRefresh() {
563
+ if (refreshTimer) clearInterval(refreshTimer);
564
+ const ms = parseInt($('interval').value);
565
+ if (ms > 0) {
566
+ refreshTimer = setInterval(fetchStats, ms);
567
+ }
568
+ }
569
+
570
+ $('interval').addEventListener('change', setupRefresh);
571
+ $('refreshBtn').addEventListener('click', fetchStats);
572
+
573
+ // Auth overlay
574
+ $('authSubmitBtn').addEventListener('click', () => {
575
+ const key = $('authKeyInput').value.trim();
576
+ if (!key) {
577
+ $('authMsg').className = 'msg msg-err';
578
+ $('authMsg').textContent = '请输入 API Key';
579
+ return;
580
+ }
581
+ sessionStorage.setItem('adminApiKey', key);
582
+ $('authOverlay').style.display = 'none';
583
+ $('authMsg').textContent = '';
584
+ fetchStats();
585
+ });
586
+
587
+ $('authKeyInput').addEventListener('keydown', e => {
588
+ if (e.key === 'Enter') $('authSubmitBtn').click();
589
+ });
590
+
591
+ // Token management forms
592
+ $('addTokenForm').addEventListener('submit', async e => {
593
+ e.preventDefault();
594
+ const token = $('addTokenInput').value.trim();
595
+ const msgEl = $('addTokenMsg');
596
+ msgEl.textContent = '添加中...';
597
+ msgEl.className = 'msg';
598
+ try {
599
+ const res = await apiFetch('/admin/api/token/add', {
600
+ method: 'POST',
601
+ headers: { 'Content-Type': 'application/json' },
602
+ body: JSON.stringify({ token }),
603
+ });
604
+ const data = await res.json();
605
+ if (data.success) {
606
+ msgEl.className = 'msg msg-ok';
607
+ msgEl.textContent = '添加成功!视觉能力: ' + (data.visionCapable === true ? '支持' : data.visionCapable === false ? '不支持' : '未知');
608
+ $('addTokenInput').value = '';
609
+ fetchStats();
610
+ } else {
611
+ msgEl.className = 'msg msg-err';
612
+ msgEl.textContent = data.error?.message || '添加失败';
613
+ }
614
+ } catch (err) {
615
+ msgEl.className = 'msg msg-err';
616
+ msgEl.textContent = err.message === 'unauthorized' ? '认证失败' : '请求失败';
617
+ }
618
+ });
619
+
620
+ $('loginForm').addEventListener('submit', async e => {
621
+ e.preventDefault();
622
+ const email = $('loginEmail').value.trim();
623
+ const password = $('loginPassword').value;
624
+ const msgEl = $('loginMsg');
625
+ msgEl.textContent = '登录中...';
626
+ msgEl.className = 'msg';
627
+ try {
628
+ const res = await apiFetch('/admin/api/token/login', {
629
+ method: 'POST',
630
+ headers: { 'Content-Type': 'application/json' },
631
+ body: JSON.stringify({ email, password }),
632
+ });
633
+ const data = await res.json();
634
+ if (data.success) {
635
+ msgEl.className = 'msg msg-ok';
636
+ msgEl.textContent = '登录成功!令牌: ' + data.token;
637
+ $('loginEmail').value = '';
638
+ $('loginPassword').value = '';
639
+ fetchStats();
640
+ } else {
641
+ msgEl.className = 'msg msg-err';
642
+ msgEl.textContent = data.error?.message || '登录失败';
643
+ }
644
+ } catch (err) {
645
+ msgEl.className = 'msg msg-err';
646
+ msgEl.textContent = err.message === 'unauthorized' ? '认证失败' : '请求失败';
647
+ }
648
+ });
649
+
650
+ // Init
651
+ fetchStats();
652
+ setupRefresh();
653
+ </script>
654
+ </body>
655
+ </html>
src/auth.js ADDED
@@ -0,0 +1,463 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { config } from 'dotenv';
2
+ config();
3
+
4
+ const BASE_URL = 'https://chat.deepseek.com';
5
+
6
+ const MAX_CONCURRENT_PER_TOKEN = 2;
7
+ const TOKEN_DEAD_THRESHOLD = 5;
8
+
9
+ // Multi-token support: DS_TOKENS=token1,token2,token3 (comma-separated)
10
+ // Fallback: DS_TOKEN=single_token
11
+ // Account support: DS_ACCOUNTS=email1:pass1,email2:pass2 (auto-login to refresh tokens)
12
+ export function loadTokens() {
13
+ const tokensStr = process.env.DS_TOKENS?.trim();
14
+ if (tokensStr) {
15
+ return tokensStr.split(',').map(t => t.trim()).filter(Boolean);
16
+ }
17
+ const single = process.env.DS_TOKEN?.trim();
18
+ if (single) return [single];
19
+ return [];
20
+ }
21
+
22
+ export function loadAccounts() {
23
+ const accounts = [];
24
+
25
+ // Keep backward-compatible multi-account support.
26
+ const accountsStr = process.env.DS_ACCOUNTS?.trim();
27
+ if (accountsStr) {
28
+ accounts.push(...accountsStr.split(',').map(entry => {
29
+ const [email, ...passParts] = entry.trim().split(':');
30
+ const password = passParts.join(':');
31
+ return email && password ? { email, password } : null;
32
+ }).filter(Boolean));
33
+ }
34
+
35
+ // Single-account support requested by deployment envs.
36
+ // DS_TOKEN/DS_TOKENS has priority: when a token is configured,
37
+ // DS_EMAIL/DS_PASSWORD are ignored unless DS_ACCOUNTS is explicitly used.
38
+ const hasToken = Boolean(process.env.DS_TOKENS?.trim() || process.env.DS_TOKEN?.trim());
39
+ const email = process.env.DS_EMAIL?.trim();
40
+ const password = process.env.DS_PASSWORD?.trim();
41
+ if (!hasToken && email && password) {
42
+ accounts.push({ email, password });
43
+ }
44
+
45
+ return accounts;
46
+ }
47
+
48
+ const tokens = loadTokens();
49
+ const accounts = loadAccounts();
50
+
51
+ function generateDeviceId() {
52
+ const bytes = new Uint8Array(48);
53
+ for (let i = 0; i < 48; i++) bytes[i] = Math.floor(Math.random() * 256);
54
+ return Buffer.from(bytes).toString('base64').replace(/=/g, '') + '==';
55
+ }
56
+
57
+ if (tokens.length === 0 && accounts.length === 0) {
58
+ throw new Error('No DS_TOKEN/DS_TOKENS or DS_EMAIL+DS_PASSWORD/DS_ACCOUNTS configured');
59
+ }
60
+
61
+ // DS_ACCOUNTS_EXTENDED=email:password:token_prefix — links existing tokens to accounts
62
+ function loadAccountTokens() {
63
+ const extStr = process.env.DS_ACCOUNTS_EXTENDED?.trim();
64
+ if (!extStr) return [];
65
+ return extStr.split(',').map(entry => {
66
+ const parts = entry.trim().split(':');
67
+ if (parts.length >= 3) return { email: parts[0], password: parts[1], tokenPrefix: parts[2] };
68
+ return null;
69
+ }).filter(Boolean);
70
+ }
71
+
72
+ const accountTokenMap = loadAccountTokens();
73
+
74
+ // Token metadata: { token, email, password, visionCapable, lastUsed, errorCount, activeRequests, dead }
75
+ const tokenPool = tokens.map(t => ({
76
+ token: t,
77
+ email: null,
78
+ password: null,
79
+ visionCapable: null,
80
+ lastUsed: 0,
81
+ errorCount: 0,
82
+ activeRequests: 0,
83
+ dead: false,
84
+ }));
85
+
86
+ // Link existing tokens to accounts via token prefix
87
+ for (const entry of tokenPool) {
88
+ if (!entry.token) continue;
89
+ const prefix = entry.token.slice(0, 12);
90
+ const match = accountTokenMap.find(a => a.tokenPrefix === prefix);
91
+ if (match) {
92
+ entry.email = match.email;
93
+ entry.password = match.password;
94
+ }
95
+ }
96
+
97
+ // Create pool entries for accounts without matching tokens (will login on init)
98
+ for (const acct of accounts) {
99
+ const alreadyLinked = tokenPool.some(t => t.email === acct.email);
100
+ if (!alreadyLinked) {
101
+ tokenPool.push({
102
+ token: null,
103
+ email: acct.email,
104
+ password: acct.password,
105
+ visionCapable: null,
106
+ lastUsed: 0,
107
+ errorCount: 0,
108
+ activeRequests: 0,
109
+ dead: false,
110
+ });
111
+ }
112
+ }
113
+
114
+ import { loginHeaders, getHeaders, getDeviceId, proxiedFetch, getDeviceIdForToken } from './headers.js';
115
+
116
+ async function login(email, password) {
117
+ // Use a fresh deviceId for login — real browser gets it from portal101.cn device fingerprint
118
+ const loginDeviceId = 'B' + generateDeviceId();
119
+
120
+ const res = await proxiedFetch(`${BASE_URL}/api/v0/users/login`, {
121
+ method: 'POST',
122
+ headers: loginHeaders(),
123
+ body: JSON.stringify({ email, mobile: '', password, area_code: '', device_id: loginDeviceId, os: 'web' }),
124
+ });
125
+
126
+ // AWS WAF returns 202 with empty body — can't login from this IP
127
+ if (res.status === 202) {
128
+ throw new Error('WAF challenge (202) — login blocked from this IP, use external refresh');
129
+ }
130
+
131
+ const text = await res.text();
132
+ if (!text) throw new Error('Empty response from login endpoint');
133
+
134
+ const json = JSON.parse(text);
135
+ if (json.code !== 0) throw new Error(`Login failed for ${email}: ${json.msg || JSON.stringify(json)}`);
136
+
137
+ const bizCode = json.data?.biz_code;
138
+ if (bizCode === 10) {
139
+ throw new Error(`Account banned: ${email}`);
140
+ }
141
+ if (bizCode === 11) {
142
+ throw new Error(`Account requires verification: ${email}`);
143
+ }
144
+
145
+ const token = json.data?.biz_data?.user?.token;
146
+ if (!token) throw new Error(`Login succeeded but no token returned for ${email}`);
147
+ return token;
148
+ }
149
+
150
+ async function checkVisionCapability(token) {
151
+ try {
152
+ const res = await proxiedFetch(`${BASE_URL}/api/v0/client/settings?did=${getDeviceId()}&scope=model`, {
153
+ headers: await getHeaders(token),
154
+ });
155
+ const json = await res.json();
156
+ const configs = json.data?.biz_data?.settings?.model_configs?.value || [];
157
+ const visionConfig = configs.find(c => c.model_type === 'vision');
158
+ if (visionConfig) {
159
+ return visionConfig.switchable === true;
160
+ }
161
+ return false;
162
+ } catch {
163
+ return null;
164
+ }
165
+ }
166
+
167
+ // Check if a token is still valid — uses /users/current which actually validates the token
168
+ // (unlike /client/settings which returns code:0 even for invalid tokens)
169
+ async function validateToken(token) {
170
+ try {
171
+ const res = await proxiedFetch(`${BASE_URL}/api/v0/users/current`, {
172
+ headers: await getHeaders(token),
173
+ });
174
+ const json = await res.json();
175
+ return json.code === 0;
176
+ } catch {
177
+ return false;
178
+ }
179
+ }
180
+
181
+ // Refresh a dead token entry — login if account credentials exist
182
+ async function refreshToken(entry) {
183
+ if (!entry.password) return false;
184
+ try {
185
+ const newToken = await login(entry.email, entry.password);
186
+ entry.token = newToken;
187
+ entry.errorCount = 0;
188
+ entry.dead = false;
189
+ const vision = await checkVisionCapability(newToken);
190
+ entry.visionCapable = vision;
191
+ console.log(` Refreshed token for ${entry.email}: ${newToken.slice(0, 12)}... vision=${vision}`);
192
+ return true;
193
+ } catch (err) {
194
+ console.warn(` Refresh failed for ${entry.email}: ${err.message}`);
195
+ // If account is banned, mark dead permanently
196
+ if (err.message.includes('banned')) {
197
+ entry.dead = true;
198
+ entry.errorCount = TOKEN_DEAD_THRESHOLD;
199
+ }
200
+ return false;
201
+ }
202
+ }
203
+
204
+ export async function initTokenPool() {
205
+ console.log(`Token pool: ${tokenPool.length} entries (${tokens.length} tokens + ${accounts.length} accounts), max ${MAX_CONCURRENT_PER_TOKEN} concurrent each`);
206
+
207
+ // Validate existing tokens, mark dead ones (auto-refresh if account linked)
208
+ for (const entry of tokenPool) {
209
+ if (entry.token) {
210
+ const valid = await validateToken(entry.token);
211
+ if (!valid) {
212
+ console.log(` ${entry.token.slice(0, 12)}... INVALID — ${entry.password ? 'attempting refresh' : 'no account to refresh'}`);
213
+ if (entry.password) {
214
+ const ok = await refreshToken(entry);
215
+ if (ok) {
216
+ // Remove duplicate account-only entries that now have same email
217
+ const dupIdx = tokenPool.findIndex(t => t !== entry && t.email === entry.email && !t.token);
218
+ if (dupIdx !== -1) {
219
+ console.log(` Removing duplicate account entry for ${entry.email}`);
220
+ tokenPool.splice(dupIdx, 1);
221
+ }
222
+ }
223
+ } else {
224
+ entry.dead = true;
225
+ entry.errorCount = TOKEN_DEAD_THRESHOLD;
226
+ }
227
+ }
228
+ }
229
+ }
230
+
231
+ // Login account-only entries (no token yet) — remove if banned/WAF-blocked
232
+ for (let i = tokenPool.length - 1; i >= 0; i--) {
233
+ const entry = tokenPool[i];
234
+ if (!entry.token && entry.password) {
235
+ const ok = await refreshToken(entry);
236
+ if (!ok && entry.dead) {
237
+ // Banned or permanently failed — remove from pool
238
+ console.log(` Removing banned/failed account entry for ${entry.email}`);
239
+ tokenPool.splice(i, 1);
240
+ }
241
+ }
242
+ }
243
+
244
+ // Also remove account entries with no token and no password (stale NONE entries)
245
+ for (let i = tokenPool.length - 1; i >= 0; i--) {
246
+ if (!tokenPool[i].token && !tokenPool[i].password) {
247
+ console.log(` Removing stale NONE entry at index ${i}`);
248
+ tokenPool.splice(i, 1);
249
+ }
250
+ }
251
+
252
+ // Check vision capability for valid tokens
253
+ for (const entry of tokenPool) {
254
+ if (entry.token && !entry.dead) {
255
+ const vision = await checkVisionCapability(entry.token);
256
+ entry.visionCapable = vision;
257
+ const label = vision === true ? 'vision=YES' : vision === false ? 'vision=NO' : 'vision=UNKNOWN';
258
+ console.log(` ${entry.token.slice(0, 12)}... ${label} ${entry.email ? `(${entry.email})` : ''}`);
259
+ }
260
+ }
261
+
262
+ const alive = tokenPool.filter(t => !t.dead).length;
263
+ console.log(`Pool ready: ${alive}/${tokenPool.length} tokens alive`);
264
+ }
265
+
266
+ export function acquireToken(preferVision = false) {
267
+ let candidates = tokenPool.filter(t => !t.dead && t.activeRequests < MAX_CONCURRENT_PER_TOKEN && t.token);
268
+
269
+ if (preferVision) {
270
+ const visionTokens = candidates.filter(t => t.visionCapable === true);
271
+ if (visionTokens.length > 0) candidates = visionTokens;
272
+ } else {
273
+ const nonVisionTokens = candidates.filter(t => t.visionCapable !== true);
274
+ if (nonVisionTokens.length > 0) candidates = nonVisionTokens;
275
+ }
276
+
277
+ if (candidates.length === 0) {
278
+ return null;
279
+ }
280
+
281
+ candidates.sort((a, b) => a.activeRequests - b.activeRequests);
282
+ const chosen = candidates[0];
283
+ chosen.activeRequests++;
284
+ chosen.lastUsed = Date.now();
285
+
286
+ let released = false;
287
+ const release = () => {
288
+ if (released) return;
289
+ released = true;
290
+ chosen.activeRequests = Math.max(0, chosen.activeRequests - 1);
291
+ };
292
+
293
+ return { token: chosen.token, account: chosen, release };
294
+ }
295
+
296
+ export function reportTokenError(token) {
297
+ const entry = tokenPool.find(t => t.token === token);
298
+ if (!entry) return;
299
+ entry.errorCount++;
300
+
301
+ if (entry.errorCount >= TOKEN_DEAD_THRESHOLD) {
302
+ entry.dead = true;
303
+ console.warn(`Token ${token.slice(0, 12)}... marked DEAD (errorCount=${entry.errorCount})`);
304
+
305
+ // Try auto-refresh if account credentials exist
306
+ if (entry.password) {
307
+ refreshToken(entry).then(ok => {
308
+ if (ok) {
309
+ invalidateTokenSessions(token);
310
+ }
311
+ });
312
+ }
313
+ }
314
+ }
315
+
316
+ export function reportTokenSuccess(token) {
317
+ const entry = tokenPool.find(t => t.token === token);
318
+ if (!entry) return;
319
+ entry.errorCount = 0;
320
+ entry.lastUsed = Date.now();
321
+ if (entry.dead) {
322
+ entry.dead = false;
323
+ console.log(`Token ${token.slice(0, 12)}... revived (was dead, now working)`);
324
+ }
325
+ }
326
+
327
+ // Invalidate cached sessions for a token (after refresh)
328
+ function invalidateTokenSessions(token) {
329
+ const prefix = token.slice(0, 12);
330
+ // Dynamic import to avoid circular dependency
331
+ import('./session.js').then(m => m.invalidateTokenSessions(prefix)).catch(() => {});
332
+ }
333
+
334
+ // Legacy: pickToken returns just the token string
335
+ let tokenIndex = 0;
336
+ export function pickToken(preferVision = false) {
337
+ let candidates = tokenPool.filter(t => !t.dead && t.token);
338
+
339
+ if (preferVision) {
340
+ const visionTokens = candidates.filter(t => t.visionCapable === true);
341
+ if (visionTokens.length > 0) candidates = visionTokens;
342
+ } else {
343
+ const nonVisionTokens = candidates.filter(t => t.visionCapable !== true);
344
+ if (nonVisionTokens.length > 0) candidates = nonVisionTokens;
345
+ }
346
+
347
+ if (candidates.length === 0) {
348
+ // Absolute fallback — use any token with a token string
349
+ candidates = tokenPool.filter(t => t.token);
350
+ if (candidates.length === 0) throw new Error('No tokens available in pool');
351
+ }
352
+
353
+ const idx = tokenIndex % candidates.length;
354
+ const chosen = candidates[idx];
355
+ tokenIndex++;
356
+ return chosen.token;
357
+ }
358
+
359
+ // Legacy: sticky per-request token
360
+ let currentRequestToken = null;
361
+
362
+ export function setRequestToken(token) {
363
+ currentRequestToken = token;
364
+ }
365
+
366
+ export function getRequestToken() {
367
+ return currentRequestToken;
368
+ }
369
+
370
+ export async function getToken(preferVision = false) {
371
+ if (currentRequestToken) return currentRequestToken;
372
+ return pickToken(preferVision);
373
+ }
374
+
375
+ // Legacy: email/password login (adds to pool dynamically)
376
+ export async function loginAndAddToken(email, password) {
377
+ const token = await login(email, password);
378
+ const existing = tokenPool.find(t => t.token === token);
379
+ if (!existing) {
380
+ const vision = await checkVisionCapability(token);
381
+ tokenPool.push({ token, email, password, visionCapable: vision, lastUsed: 0, errorCount: 0, activeRequests: 0, dead: false });
382
+ }
383
+ return token;
384
+ }
385
+
386
+ export async function addTokenToPool(tokenStr) {
387
+ const trimmed = tokenStr.trim();
388
+ const existing = tokenPool.find(t => t.token === trimmed);
389
+ if (existing) return existing;
390
+ const vision = await checkVisionCapability(trimmed);
391
+ const entry = { token: trimmed, email: null, password: null, visionCapable: vision, lastUsed: 0, errorCount: 0, activeRequests: 0, dead: false };
392
+ tokenPool.push(entry);
393
+ return entry;
394
+ }
395
+
396
+ // Periodic health check — validate alive tokens and detect banned accounts early
397
+ // Only checks tokens that haven't been used recently (idle tokens) to reduce request volume
398
+ const HEALTH_CHECK_INTERVAL = 10 * 60 * 1000; // 10 minutes
399
+ const IDLE_THRESHOLD = 30 * 60 * 1000; // 30 minutes — only check tokens idle for 30+ min
400
+
401
+ async function healthCheck() {
402
+ const now = Date.now();
403
+ // Only check idle tokens — recently used ones are assumed valid
404
+ const idle = tokenPool.filter(t => !t.dead && t.token && (now - t.lastUsed) > IDLE_THRESHOLD);
405
+ if (idle.length === 0) return;
406
+
407
+ console.log(`Health check: ${idle.length} idle tokens (of ${tokenPool.filter(t => !t.dead).length} alive)`);
408
+
409
+ for (const entry of idle) {
410
+ const valid = await validateToken(entry.token);
411
+ if (!valid) {
412
+ console.log(`Health check: ${entry.token.slice(0, 12)}... INVALID — ${entry.password ? 'refreshing' : 'marking dead'}`);
413
+ if (entry.password) {
414
+ const ok = await refreshToken(entry);
415
+ if (!ok && entry.dead) {
416
+ console.log(`Health check: ${entry.email} is BANNED, removing from pool`);
417
+ const idx = tokenPool.indexOf(entry);
418
+ if (idx !== -1) tokenPool.splice(idx, 1);
419
+ }
420
+ } else {
421
+ entry.dead = true;
422
+ entry.errorCount = TOKEN_DEAD_THRESHOLD;
423
+ }
424
+ } else {
425
+ entry.lastUsed = now; // reset idle timer on successful check
426
+ }
427
+ }
428
+ }
429
+
430
+ let healthCheckTimer = null;
431
+
432
+ export function startHealthCheck() {
433
+ if (healthCheckTimer) return;
434
+ healthCheckTimer = setInterval(healthCheck, HEALTH_CHECK_INTERVAL);
435
+ console.log(`Health check enabled: every ${HEALTH_CHECK_INTERVAL / 1000}s`);
436
+ }
437
+
438
+ export function stopHealthCheck() {
439
+ if (healthCheckTimer) {
440
+ clearInterval(healthCheckTimer);
441
+ healthCheckTimer = null;
442
+ }
443
+ }
444
+
445
+ export function getPoolInfo() {
446
+ return tokenPool.filter(t => !t.dead).map(t => ({
447
+ token: t.token ? t.token.slice(0, 12) + '...' : 'NONE',
448
+ email: t.email || null,
449
+ visionCapable: t.visionCapable,
450
+ errorCount: t.errorCount,
451
+ activeRequests: t.activeRequests,
452
+ dead: t.dead,
453
+ maxConcurrent: MAX_CONCURRENT_PER_TOKEN,
454
+ }));
455
+ }
456
+
457
+ export function getAliveTokens() {
458
+ return tokenPool.filter(t => !t.dead && t.token).map(t => t.token);
459
+ }
460
+
461
+ export function getTotalCapacity() {
462
+ return tokenPool.filter(t => !t.dead).length * MAX_CONCURRENT_PER_TOKEN;
463
+ }
src/chat.js ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { setRequestToken, reportTokenError, reportTokenSuccess } from './auth.js';
2
+ import { solvePowChallengeWithToken } from './pow.js';
3
+ import { getSession } from './session.js';
4
+ import { streamHeaders, proxiedFetch } from './headers.js';
5
+ import { enqueueRequest, dispatchQueued } from './queue.js';
6
+
7
+ const BASE_URL = 'https://chat.deepseek.com';
8
+
9
+ export async function completion({ modelType, prompt, thinkingEnabled = false, searchEnabled = false, parentMessageId = null, refFileIds = [], preferVision = false }) {
10
+ // Step 1: Acquire token slot first — PoW and completion must use the same token
11
+ const slot = await enqueueRequest(preferVision);
12
+
13
+ try {
14
+ // Step 2: Solve PoW using the same token
15
+ const { powResponse } = await solvePowChallengeWithToken(slot.token);
16
+
17
+ setRequestToken(slot.token);
18
+ const session = await getSession(slot.token, modelType);
19
+ setRequestToken(null);
20
+
21
+ const body = {
22
+ chat_session_id: session.id,
23
+ parent_message_id: parentMessageId,
24
+ model_type: modelType,
25
+ prompt,
26
+ ref_file_ids: refFileIds,
27
+ thinking_enabled: thinkingEnabled,
28
+ search_enabled: searchEnabled,
29
+ action: null,
30
+ preempt: false,
31
+ };
32
+
33
+ const res = await proxiedFetch(`${BASE_URL}/api/v0/chat/completion`, {
34
+ method: 'POST',
35
+ headers: await streamHeaders(slot.token, powResponse),
36
+ body: JSON.stringify(body),
37
+ });
38
+
39
+ if (!res.ok) {
40
+ const text = await res.text();
41
+ // Check for specific DeepSeek error codes
42
+ try {
43
+ const errJson = JSON.parse(text);
44
+ const code = errJson.code;
45
+ // 40003 = invalid/expired token
46
+ if (code === 40003) {
47
+ reportTokenError(slot.token);
48
+ throw new Error('Token invalid (40003)');
49
+ }
50
+ // 40004 = account banned
51
+ if (code === 40004) {
52
+ reportTokenError(slot.token);
53
+ const entry = (await import('./auth.js')).getPoolInfo().find(t => slot.token.startsWith(t.token.replace('...', '')));
54
+ console.error(`Account BANNED during completion: ${entry?.email || slot.token.slice(0, 12)}...`);
55
+ throw new Error('Account banned (40004)');
56
+ }
57
+ } catch (parseErr) {
58
+ if (parseErr.message.includes('Token invalid') || parseErr.message.includes('Account banned')) throw parseErr;
59
+ }
60
+ reportTokenError(slot.token);
61
+ throw new Error(`Completion request failed: ${res.status} ${text}`);
62
+ }
63
+
64
+ reportTokenSuccess(slot.token);
65
+ return { body: res.body, slot };
66
+ } catch (err) {
67
+ slot.release();
68
+ dispatchQueued();
69
+ throw err;
70
+ }
71
+ }
72
+
73
+ export async function* parseSSEStream(body) {
74
+ const reader = body.getReader();
75
+ const decoder = new TextDecoder();
76
+ let buffer = '';
77
+ let messageIds = {};
78
+ let currentFragmentType = null;
79
+
80
+ try {
81
+ while (true) {
82
+ const { done, value } = await reader.read();
83
+ if (done) break;
84
+
85
+ buffer += decoder.decode(value, { stream: true });
86
+ const lines = buffer.split('\n');
87
+ buffer = lines.pop() || '';
88
+
89
+ for (const line of lines) {
90
+ if (line.startsWith('event:')) {
91
+ if (line.slice(6).trim() === 'close') return;
92
+ continue;
93
+ }
94
+ if (!line.startsWith('data:')) continue;
95
+ const data = line.slice(5).trim();
96
+ if (!data) continue;
97
+
98
+ try {
99
+ const parsed = JSON.parse(data);
100
+
101
+ // Check for error codes mid-stream
102
+ if (parsed.code === 40003) {
103
+ yield { type: 'error', code: 40003, message: 'Token invalid' };
104
+ return;
105
+ }
106
+ if (parsed.code === 40004) {
107
+ yield { type: 'error', code: 40004, message: 'Account banned' };
108
+ return;
109
+ }
110
+
111
+ if (parsed.request_message_id != null) {
112
+ messageIds.requestMessageId = parsed.request_message_id;
113
+ messageIds.responseMessageId = parsed.response_message_id;
114
+ }
115
+
116
+ if (parsed.v?.response?.fragments) {
117
+ for (const frag of parsed.v.response.fragments) {
118
+ if (frag.type === 'THINK' && frag.content) {
119
+ currentFragmentType = 'THINK';
120
+ yield { type: 'thinking', content: frag.content, messageIds };
121
+ } else if (frag.type === 'RESPONSE' && frag.content) {
122
+ currentFragmentType = 'RESPONSE';
123
+ yield { type: 'content', content: frag.content, messageIds };
124
+ }
125
+ }
126
+ if (parsed.v.response.accumulated_token_usage != null) {
127
+ yield { type: 'usage', usage: parsed.v.response.accumulated_token_usage, messageIds };
128
+ }
129
+ }
130
+
131
+ if (parsed.p && parsed.o) {
132
+ if (parsed.p === 'response/fragments/-1/content' && parsed.o === 'APPEND' && typeof parsed.v === 'string') {
133
+ yield { type: currentFragmentType === 'THINK' ? 'thinking' : 'content', content: parsed.v, messageIds };
134
+ } else if (parsed.p === 'response/fragments/-1/content' && !parsed.o && typeof parsed.v === 'string') {
135
+ yield { type: currentFragmentType === 'THINK' ? 'thinking' : 'content', content: parsed.v, messageIds };
136
+ } else if (parsed.p === 'response/status' && parsed.v === 'FINISHED') {
137
+ yield { type: 'done', messageIds };
138
+ } else if (parsed.p === 'response' && parsed.o === 'BATCH' && Array.isArray(parsed.v)) {
139
+ for (const item of parsed.v) {
140
+ if (item.p === 'accumulated_token_usage') {
141
+ yield { type: 'usage', usage: item.v, messageIds };
142
+ }
143
+ }
144
+ } else if (parsed.p === 'response/fragments' && parsed.o === 'APPEND' && Array.isArray(parsed.v)) {
145
+ for (const frag of parsed.v) {
146
+ if (frag.type === 'RESPONSE' && frag.content) {
147
+ currentFragmentType = 'RESPONSE';
148
+ yield { type: 'content', content: frag.content, messageIds };
149
+ } else if (frag.type === 'THINK' && frag.content) {
150
+ currentFragmentType = 'THINK';
151
+ yield { type: 'thinking', content: frag.content, messageIds };
152
+ }
153
+ }
154
+ }
155
+ continue;
156
+ }
157
+
158
+ if (typeof parsed.v === 'string') {
159
+ yield { type: currentFragmentType === 'THINK' ? 'thinking' : 'content', content: parsed.v, messageIds };
160
+ }
161
+
162
+ if (Array.isArray(parsed.v)) {
163
+ for (const item of parsed.v) {
164
+ if (item.p === 'accumulated_token_usage') {
165
+ yield { type: 'usage', usage: item.v, messageIds };
166
+ }
167
+ }
168
+ }
169
+ } catch {
170
+ // skip unparseable lines
171
+ }
172
+ }
173
+ }
174
+ } finally {
175
+ reader.releaseLock();
176
+ }
177
+ }
src/deepseek.js ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { completion, parseSSEStream } from './chat.js';
2
+ import { pickToken } from './auth.js';
3
+ import { dispatchQueued } from './queue.js';
4
+
5
+ const MODEL_MAP = {
6
+ 'deepseek-v4-flash': 'default',
7
+ 'deepseek-v4-pro': 'expert',
8
+ 'deepseek-v4-vision': 'vision',
9
+ 'deepseek-v4-flash[1m]': 'default',
10
+ 'deepseek-v4-pro[1m]': 'expert',
11
+ 'deepseek-v4-vision[1m]': 'vision',
12
+ };
13
+
14
+ export async function handleDeepSeekCompletion(req, res) {
15
+ const body = req.body;
16
+ const modelType = body.model_type || MODEL_MAP[body.model] || 'default';
17
+ const prompt = body.prompt || '';
18
+ const thinkingEnabled = body.thinking_enabled ?? false;
19
+ const searchEnabled = body.search_enabled ?? false;
20
+ const parentMessageId = body.parent_message_id ?? null;
21
+ const refFileIds = body.ref_file_ids ?? [];
22
+
23
+ if (!prompt) {
24
+ return res.status(400).json({ code: 1, msg: 'prompt is required' });
25
+ }
26
+
27
+ try {
28
+ const { body: streamBody, slot } = await completion({ modelType, prompt, thinkingEnabled, searchEnabled, parentMessageId, refFileIds });
29
+
30
+ res.writeHead(200, {
31
+ 'Content-Type': 'text/event-stream; charset=utf-8',
32
+ 'Cache-Control': 'no-cache',
33
+ });
34
+
35
+ const reader = streamBody.getReader();
36
+ try {
37
+ while (true) {
38
+ const { done, value } = await reader.read();
39
+ if (done) break;
40
+ res.write(value);
41
+ }
42
+ } finally {
43
+ reader.releaseLock();
44
+ }
45
+ res.end();
46
+ } catch (err) {
47
+ console.error('DeepSeek completion error:', err.message);
48
+ if (!res.headersSent) {
49
+ res.status(500).json({ code: 1, msg: err.message });
50
+ } else {
51
+ res.end();
52
+ }
53
+ } finally {
54
+ slot.release();
55
+ dispatchQueued();
56
+ }
57
+ }
src/headers.js ADDED
@@ -0,0 +1,226 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Mimic real Chrome 120 browser session
2
+ const UA_VERSION = '120.0.0.0';
3
+ const UA_MAJOR = '120';
4
+
5
+ const BROWSER_HEADERS = {
6
+ 'user-agent': `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${UA_VERSION} Safari/537.36`,
7
+ 'accept-language': 'zh-CN,zh;q=0.9',
8
+ 'accept-encoding': 'gzip, deflate, br',
9
+ 'origin': 'https://chat.deepseek.com',
10
+ 'referer': 'https://chat.deepseek.com/',
11
+ 'sec-ch-ua': `"Not_A Brand";v="8", "Chromium";v="${UA_MAJOR}", "Google Chrome";v="${UA_MAJOR}"`,
12
+ 'sec-ch-ua-mobile': '?0',
13
+ 'sec-ch-ua-platform': '"Windows"',
14
+ 'sec-fetch-dest': 'empty',
15
+ 'sec-fetch-mode': 'cors',
16
+ 'sec-fetch-site': 'same-origin',
17
+ 'x-client-timezone-offset': '28800',
18
+ };
19
+
20
+ // Per-token cookie jar: smidV2, HWWAFSESTIME, HWWAFSESID, ds_session_id, thumbcache
21
+ const tokenCookies = new Map();
22
+
23
+ function randomHex(len) {
24
+ const chars = '0123456789abcdef';
25
+ let s = '';
26
+ for (let i = 0; i < len; i++) s += chars[Math.floor(Math.random() * 16)];
27
+ return s;
28
+ }
29
+
30
+ function randomAlphaNum(len) {
31
+ const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
32
+ let s = '';
33
+ for (let i = 0; i < len; i++) s += chars[Math.floor(Math.random() * chars.length)];
34
+ return s;
35
+ }
36
+
37
+ function ensureCookies(token) {
38
+ if (tokenCookies.has(token)) return tokenCookies.get(token);
39
+ const smidV2 = `20260520${randomAlphaNum(10)}${randomHex(24)}`;
40
+ const HWWAFSESTIME = `${Date.now()}`;
41
+ const HWWAFSESID = `${randomAlphaNum(4)}${randomHex(12)}`;
42
+ const dsSessionId = `${randomHex(32)}`;
43
+ const thumbcacheKey = randomHex(32);
44
+ const deviceId = generateDeviceId();
45
+ const thumbcacheValue = deviceId;
46
+ const cookie = `smidV2=${smidV2}; HWWAFSESTIME=${HWWAFSESTIME}; HWWAFSESID=${HWWAFSESID}; ds_session_id=${dsSessionId}; .thumbcache_${thumbcacheKey}=${encodeURIComponent(thumbcacheValue)}`;
47
+ tokenCookies.set(token, { cookie, deviceId });
48
+ return tokenCookies.get(token);
49
+ }
50
+
51
+ // Generate deviceId from fp-it-acc.portal101.cn format (base64-like)
52
+ function generateDeviceId() {
53
+ const bytes = new Uint8Array(48);
54
+ for (let i = 0; i < 48; i++) bytes[i] = Math.floor(Math.random() * 256);
55
+ return Buffer.from(bytes).toString('base64').replace(/=/g, '') + '==';
56
+ }
57
+
58
+ export function getDeviceIdForToken(token) {
59
+ const cached = tokenCookies.get(token);
60
+ return cached?.deviceId || null;
61
+ }
62
+
63
+ // HIF (Hidden Integration Feature) token management
64
+ // DeepSeek uses hif-leim and hif-dliq headers for request validation
65
+ // These are fetched from hif-leim.deepseek.com/query and hif-dliq.deepseek.com/query
66
+ const HIF_BASE = 'https://hif-leim.deepseek.com/query';
67
+ const HIF_DLIQ_BASE = 'https://hif-dliq.deepseek.com/query';
68
+
69
+ const hifCache = new Map(); // key: token, value: { leim, dliq, expiresAt }
70
+
71
+ async function fetchHifToken(url, token) {
72
+ const headers = {
73
+ 'user-agent': BROWSER_HEADERS['user-agent'],
74
+ 'accept': '*/*',
75
+ 'accept-language': 'zh-CN,zh;q=0.9',
76
+ 'accept-encoding': 'gzip, deflate, br',
77
+ 'origin': 'https://chat.deepseek.com',
78
+ 'referer': 'https://chat.deepseek.com/',
79
+ };
80
+ if (token) {
81
+ headers['authorization'] = `Bearer ${token}`;
82
+ }
83
+ const res = await proxiedFetch(url, { headers });
84
+ const json = await res.json();
85
+ const value = json.data?.biz_data?.value;
86
+ const ttl = parseInt(res.headers.get('x-hif-ttl') || '600', 10);
87
+ return { value, ttl };
88
+ }
89
+
90
+ async function refreshHifTokens(token) {
91
+ try {
92
+ const [leimResult, dliqResult] = await Promise.all([
93
+ fetchHifToken(HIF_BASE, token),
94
+ fetchHifToken(HIF_DLIQ_BASE, token),
95
+ ]);
96
+
97
+ if (leimResult.value && dliqResult.value) {
98
+ const ttl = Math.min(leimResult.ttl, dliqResult.ttl);
99
+ hifCache.set(token, {
100
+ leim: leimResult.value,
101
+ dliq: dliqResult.value,
102
+ expiresAt: Date.now() + ttl * 1000,
103
+ });
104
+ }
105
+ } catch (e) {
106
+ // HIF fetch failed — requests will proceed without it
107
+ }
108
+ }
109
+
110
+ async function getHifHeaders(token) {
111
+ const cached = hifCache.get(token);
112
+ if (cached && cached.expiresAt > Date.now()) {
113
+ return { 'x-hif-leim': cached.leim, 'x-hif-dliq': cached.dliq };
114
+ }
115
+
116
+ await refreshHifTokens(token);
117
+ const refreshed = hifCache.get(token);
118
+ if (refreshed) {
119
+ return { 'x-hif-leim': refreshed.leim, 'x-hif-dliq': refreshed.dliq };
120
+ }
121
+ return {};
122
+ }
123
+
124
+ // Proxy dispatcher for bypassing IP-based rate limits
125
+ let proxyDispatcher = null;
126
+
127
+ export async function getDispatcher() {
128
+ if (proxyDispatcher !== null) return proxyDispatcher;
129
+ const proxyUrl = process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy;
130
+ if (!proxyUrl) {
131
+ proxyDispatcher = false;
132
+ return false;
133
+ }
134
+ try {
135
+ const { ProxyAgent } = await import('undici');
136
+ proxyDispatcher = new ProxyAgent(proxyUrl);
137
+ console.log(`Proxy enabled: ${proxyUrl}`);
138
+ return proxyDispatcher;
139
+ } catch (e) {
140
+ console.warn(`Failed to init proxy (${proxyUrl}): ${e.message}`);
141
+ proxyDispatcher = false;
142
+ return false;
143
+ }
144
+ }
145
+
146
+ // Common headers for API requests
147
+ export async function apiHeaders(token, extra = {}) {
148
+ const cookieData = ensureCookies(token);
149
+ const hifHeaders = await getHifHeaders(token);
150
+ return {
151
+ 'authorization': `Bearer ${token}`,
152
+ 'content-type': 'application/json',
153
+ 'accept': '*/*',
154
+ 'x-app-version': '2.0.0',
155
+ 'x-client-version': '2.0.0',
156
+ 'x-client-platform': 'web',
157
+ 'x-client-locale': 'zh_CN',
158
+ 'cookie': cookieData.cookie,
159
+ ...BROWSER_HEADERS,
160
+ ...hifHeaders,
161
+ ...extra,
162
+ };
163
+ }
164
+
165
+ // Headers for SSE streaming requests
166
+ export async function streamHeaders(token, powResponse, extra = {}) {
167
+ const cookieData = ensureCookies(token);
168
+ const hifHeaders = await getHifHeaders(token);
169
+ return {
170
+ 'authorization': `Bearer ${token}`,
171
+ 'content-type': 'application/json',
172
+ 'x-ds-pow-response': powResponse,
173
+ 'accept': 'text/event-stream',
174
+ 'x-app-version': '2.0.0',
175
+ 'x-client-version': '2.0.0',
176
+ 'x-client-platform': 'web',
177
+ 'x-client-locale': 'zh_CN',
178
+ 'cookie': cookieData.cookie,
179
+ ...BROWSER_HEADERS,
180
+ ...hifHeaders,
181
+ ...extra,
182
+ };
183
+ }
184
+
185
+ // Headers for GET requests (no content-type)
186
+ export async function getHeaders(token, extra = {}) {
187
+ const cookieData = ensureCookies(token);
188
+ return {
189
+ 'authorization': `Bearer ${token}`,
190
+ 'accept': '*/*',
191
+ 'x-app-version': '2.0.0',
192
+ 'x-client-version': '2.0.0',
193
+ 'x-client-platform': 'web',
194
+ 'x-client-locale': 'zh_CN',
195
+ 'cookie': cookieData.cookie,
196
+ ...BROWSER_HEADERS,
197
+ ...extra,
198
+ };
199
+ }
200
+
201
+ // Headers for login (no token)
202
+ export function loginHeaders(extra = {}) {
203
+ return {
204
+ 'content-type': 'application/json',
205
+ 'accept': '*/*',
206
+ 'x-app-version': '2.0.0',
207
+ 'x-client-version': '2.0.0',
208
+ 'x-client-platform': 'web',
209
+ 'x-client-locale': 'zh_CN',
210
+ ...BROWSER_HEADERS,
211
+ ...extra,
212
+ };
213
+ }
214
+
215
+ // Persistent device ID (per process lifetime) — used for settings endpoint
216
+ const deviceId = randomHex(8) + '-' + randomHex(4) + '-' + randomHex(4) + '-' + randomHex(4) + '-' + randomHex(12);
217
+ export function getDeviceId() { return deviceId; }
218
+
219
+ // Wrap fetch to use proxy dispatcher when available
220
+ export async function proxiedFetch(url, options = {}) {
221
+ const dispatcher = await getDispatcher();
222
+ if (dispatcher) {
223
+ options.dispatcher = dispatcher;
224
+ }
225
+ return fetch(url, options);
226
+ }
src/index.js ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { config } from 'dotenv';
2
+ config();
3
+
4
+ import express from 'express';
5
+ import { initTokenPool, getPoolInfo, getTotalCapacity, addTokenToPool, loginAndAddToken, getAliveTokens, startHealthCheck } from './auth.js';
6
+ import { prewarmSessions, getSessionInfo } from './session.js';
7
+ import { handleOpenAICompletion, handleOpenAIModels } from './openai.js';
8
+ import { handleDeepSeekCompletion } from './deepseek.js';
9
+ import { getQueueInfo } from './queue.js';
10
+ import { requestLogger, getRecentLogs, getLogStats } from './logger.js';
11
+ import { fileURLToPath } from 'url';
12
+ import { dirname, join } from 'path';
13
+
14
+ const __dirname = dirname(fileURLToPath(import.meta.url));
15
+ const startTime = Date.now();
16
+
17
+ const app = express();
18
+ const PORT = Number(process.env.PORT || 7860);
19
+
20
+ app.use(express.json({ limit: '50mb' }));
21
+
22
+ // Request logging
23
+ app.use(requestLogger);
24
+
25
+ // API Key auth middleware
26
+ app.use((req, res, next) => {
27
+ const apiKey = process.env.API_KEY;
28
+ if (!apiKey) return next();
29
+ if (req.path.startsWith('/admin')) return next();
30
+
31
+ const auth = req.headers['authorization'];
32
+ if (auth === `Bearer ${apiKey}`) return next();
33
+
34
+ res.status(401).json({ error: { message: 'Invalid API key' } });
35
+ });
36
+
37
+ // OpenAI format
38
+ app.post('/v1/chat/completions', handleOpenAICompletion);
39
+ app.get('/v1/models', handleOpenAIModels);
40
+
41
+ // DeepSeek native format
42
+ app.post('/api/v0/chat/completion', handleDeepSeekCompletion);
43
+
44
+ // Health check + pool info
45
+ app.get('/', (req, res) => {
46
+ res.json({
47
+ status: 'ok',
48
+ version: '2.0.0',
49
+ pool: getPoolInfo(),
50
+ totalCapacity: getTotalCapacity(),
51
+ queue: getQueueInfo(),
52
+ });
53
+ });
54
+
55
+ // Admin panel
56
+ app.get('/admin', (req, res) => {
57
+ res.sendFile(join(__dirname, 'admin', 'index.html'));
58
+ });
59
+
60
+ app.get('/admin/api/stats', (req, res) => {
61
+ const uptimeSeconds = Math.floor((Date.now() - startTime) / 1000);
62
+ res.json({
63
+ status: 'ok',
64
+ version: '2.0.0',
65
+ uptimeSeconds,
66
+ pool: getPoolInfo(),
67
+ totalCapacity: getTotalCapacity(),
68
+ queue: getQueueInfo(),
69
+ sessions: getSessionInfo(),
70
+ logStats: getLogStats(),
71
+ });
72
+ });
73
+
74
+ app.get('/admin/api/logs', (req, res) => {
75
+ const count = Math.min(parseInt(req.query.count) || 50, 200);
76
+ res.json({ logs: getRecentLogs(count), stats: getLogStats() });
77
+ });
78
+
79
+ app.post('/admin/api/token/add', async (req, res) => {
80
+ const { token } = req.body;
81
+ if (!token || typeof token !== 'string') {
82
+ return res.status(400).json({ error: { message: 'token required' } });
83
+ }
84
+ try {
85
+ const added = await addTokenToPool(token);
86
+ res.json({ success: true, visionCapable: added.visionCapable });
87
+ } catch (err) {
88
+ res.status(500).json({ error: { message: err.message } });
89
+ }
90
+ });
91
+
92
+ app.post('/admin/api/token/login', async (req, res) => {
93
+ const { email, password } = req.body;
94
+ if (!email || !password) {
95
+ return res.status(400).json({ error: { message: 'email and password required' } });
96
+ }
97
+ try {
98
+ const token = await loginAndAddToken(email, password);
99
+ res.json({ success: true, token: token.slice(0, 12) + '...' });
100
+ } catch (err) {
101
+ res.status(500).json({ error: { message: err.message } });
102
+ }
103
+ });
104
+
105
+ app.listen(PORT, async () => {
106
+ console.log(`DeepSeek 2API running on http://localhost:${PORT}`);
107
+ console.log(`OpenAI format: POST /v1/chat/completions`);
108
+ console.log(`DeepSeek format: POST /api/v0/chat/completion`);
109
+ console.log(`Models: GET /v1/models`);
110
+ console.log(`Admin panel: http://localhost:${PORT}/admin`);
111
+
112
+ await initTokenPool();
113
+
114
+ const aliveTokens = getAliveTokens();
115
+ await prewarmSessions(aliveTokens);
116
+
117
+ startHealthCheck();
118
+ });
src/logger.js ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Request logger middleware — structured log for every API request
2
+ // Usage: import { requestLogger, getRecentLogs } from './logger.js'; app.use(requestLogger);
3
+
4
+ const MAX_LOGS = 1000;
5
+ const recentLogs = [];
6
+
7
+ export function requestLogger(req, res, next) {
8
+ const start = Date.now();
9
+ const model = req.body?.model || '-';
10
+
11
+ res.on('finish', () => {
12
+ const duration = Date.now() - start;
13
+ const entry = {
14
+ time: new Date().toISOString(),
15
+ method: req.method,
16
+ path: req.path,
17
+ model,
18
+ status: res.statusCode,
19
+ duration,
20
+ };
21
+
22
+ console.log(`[${entry.time}] ${entry.method} ${entry.path} model=${entry.model} ${entry.status} ${entry.duration}ms`);
23
+
24
+ if (recentLogs.length >= MAX_LOGS) recentLogs.shift();
25
+ recentLogs.push(entry);
26
+ });
27
+
28
+ next();
29
+ }
30
+
31
+ export function getRecentLogs(count = 50) {
32
+ return recentLogs.slice(-count);
33
+ }
34
+
35
+ export function getLogStats() {
36
+ const now = Date.now();
37
+ const last5min = recentLogs.filter(e => now - new Date(e.time).getTime() < 300000);
38
+ const errors = last5min.filter(e => e.status >= 400);
39
+ const avgDuration = last5min.length
40
+ ? Math.round(last5min.reduce((s, e) => s + e.duration, 0) / last5min.length)
41
+ : 0;
42
+ return {
43
+ totalRequests: recentLogs.length,
44
+ last5min: last5min.length,
45
+ errors5min: errors.length,
46
+ avgDuration5min: avgDuration,
47
+ };
48
+ }
src/openai.js ADDED
@@ -0,0 +1,234 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { completion, parseSSEStream } from './chat.js';
2
+ import { resolveImageToRefId } from './upload.js';
3
+ import { enqueueRequest, dispatchQueued } from './queue.js';
4
+
5
+ const MODEL_MAP = {
6
+ 'deepseek-v4-flash': 'default',
7
+ 'deepseek-v4-pro': 'expert',
8
+ 'deepseek-v4-vision': 'vision',
9
+ 'deepseek-v4-flash[1m]': 'default',
10
+ 'deepseek-v4-pro[1m]': 'expert',
11
+ 'deepseek-v4-vision[1m]': 'vision',
12
+ };
13
+
14
+ function mapModel(model) {
15
+ const mapped = MODEL_MAP[model];
16
+ if (!mapped) throw new Error(`Unknown model: ${model}. Available: ${Object.keys(MODEL_MAP).join(', ')}`);
17
+ return mapped;
18
+ }
19
+
20
+ async function extractImages(messages, token) {
21
+ const refFileIds = [];
22
+ for (const msg of messages) {
23
+ if (Array.isArray(msg.content)) {
24
+ for (const part of msg.content) {
25
+ if (part.type === 'image_url' && part.image_url?.url) {
26
+ try {
27
+ const fileId = await resolveImageToRefId(part.image_url.url, token);
28
+ refFileIds.push(fileId);
29
+ } catch (err) {
30
+ console.error('Image upload failed:', err.message);
31
+ }
32
+ }
33
+ }
34
+ }
35
+ }
36
+ return refFileIds;
37
+ }
38
+
39
+ function buildPrompt(messages) {
40
+ let prompt = '';
41
+ for (const msg of messages) {
42
+ if (msg.role === 'system') {
43
+ prompt += `[System]: ${msg.content}\n\n`;
44
+ } else if (msg.role === 'user') {
45
+ if (Array.isArray(msg.content)) {
46
+ for (const part of msg.content) {
47
+ if (part.type === 'text') {
48
+ prompt += `[User]: ${part.text}\n\n`;
49
+ }
50
+ }
51
+ } else {
52
+ prompt += `[User]: ${msg.content}\n\n`;
53
+ }
54
+ } else if (msg.role === 'assistant') {
55
+ prompt += `[Assistant]: ${msg.content}\n\n`;
56
+ }
57
+ }
58
+ return prompt.trim();
59
+ }
60
+
61
+ export async function handleOpenAICompletion(req, res) {
62
+ const { model, messages, stream = false, max_tokens } = req.body;
63
+
64
+ if (!model || !messages || !messages.length) {
65
+ return res.status(400).json({ error: { message: 'model and messages are required' } });
66
+ }
67
+
68
+ const modelType = mapModel(model);
69
+ const prompt = buildPrompt(messages);
70
+ const thinkingEnabled = req.body.thinking_enabled ?? true;
71
+ const searchEnabled = req.body.search_enabled ?? (modelType !== 'vision');
72
+ // Merge reasoning_content into content with <think> tags so CLI tools can see it
73
+ const mergeThinking = req.body.merge_thinking ?? (process.env.MERGE_THINKING !== 'false');
74
+
75
+ const requestId = `chatcmpl-${Date.now()}-${Math.random().toString(36).slice(2)}`;
76
+ let result;
77
+
78
+ try {
79
+ let refFileIds = [];
80
+ let uploadSlot = null;
81
+ if (modelType === 'vision') {
82
+ uploadSlot = await enqueueRequest(true);
83
+ try {
84
+ refFileIds = await extractImages(messages, uploadSlot.token);
85
+ } finally {
86
+ uploadSlot.release();
87
+ dispatchQueued();
88
+ }
89
+ }
90
+
91
+ result = await completion({ modelType, prompt, thinkingEnabled, searchEnabled, refFileIds, preferVision: modelType === 'vision' });
92
+ } catch (err) {
93
+ console.error('Completion error:', err.message);
94
+ return res.status(500).json({ error: { message: err.message } });
95
+ }
96
+
97
+ const { body: streamBody, slot } = result;
98
+
99
+ try {
100
+ if (stream) {
101
+ res.writeHead(200, {
102
+ 'Content-Type': 'text/event-stream',
103
+ 'Cache-Control': 'no-cache',
104
+ 'Connection': 'keep-alive',
105
+ });
106
+
107
+ res.write(`data: ${JSON.stringify({
108
+ id: requestId,
109
+ object: 'chat.completion.chunk',
110
+ created: Math.floor(Date.now() / 1000),
111
+ model,
112
+ choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }],
113
+ })}\n\n`);
114
+
115
+ let inThinkingPhase = thinkingEnabled;
116
+
117
+ for await (const event of parseSSEStream(streamBody)) {
118
+ if (event.type === 'error') {
119
+ if (event.code === 40004) {
120
+ console.error(`Account BANNED in stream: ${slot.token.slice(0, 12)}...`);
121
+ }
122
+ throw new Error(event.message || `DeepSeek error ${event.code}`);
123
+ }
124
+ if (event.type === 'content') {
125
+ inThinkingPhase = false;
126
+ res.write(`data: ${JSON.stringify({
127
+ id: requestId,
128
+ object: 'chat.completion.chunk',
129
+ created: Math.floor(Date.now() / 1000),
130
+ model,
131
+ choices: [{ index: 0, delta: { content: event.content }, finish_reason: null }],
132
+ })}\n\n`);
133
+ } else if (event.type === 'thinking' && inThinkingPhase) {
134
+ if (mergeThinking) {
135
+ res.write(`data: ${JSON.stringify({
136
+ id: requestId,
137
+ object: 'chat.completion.chunk',
138
+ created: Math.floor(Date.now() / 1000),
139
+ model,
140
+ choices: [{ index: 0, delta: { content: event.content }, finish_reason: null }],
141
+ })}\n\n`);
142
+ } else {
143
+ res.write(`data: ${JSON.stringify({
144
+ id: requestId,
145
+ object: 'chat.completion.chunk',
146
+ created: Math.floor(Date.now() / 1000),
147
+ model,
148
+ choices: [{ index: 0, delta: { reasoning_content: event.content }, finish_reason: null }],
149
+ })}\n\n`);
150
+ }
151
+ } else if (event.type === 'done') {
152
+ if (mergeThinking && inThinkingPhase) {
153
+ // No thinking was ever emitted as content — nothing extra needed
154
+ }
155
+ res.write(`data: ${JSON.stringify({
156
+ id: requestId,
157
+ object: 'chat.completion.chunk',
158
+ created: Math.floor(Date.now() / 1000),
159
+ model,
160
+ choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],
161
+ })}\n\n`);
162
+ res.write('data: [DONE]\n\n');
163
+ }
164
+ }
165
+ res.end();
166
+ } else {
167
+ let fullContent = '';
168
+ let fullThinking = '';
169
+ let usage = 0;
170
+ let inThinkingPhase = thinkingEnabled;
171
+
172
+ for await (const event of parseSSEStream(streamBody)) {
173
+ if (event.type === 'error') {
174
+ if (event.code === 40004) {
175
+ console.error(`Account BANNED in stream: ${slot.token.slice(0, 12)}...`);
176
+ }
177
+ throw new Error(event.message || `DeepSeek error ${event.code}`);
178
+ }
179
+ if (event.type === 'content') {
180
+ fullContent += event.content;
181
+ inThinkingPhase = false;
182
+ } else if (event.type === 'thinking' && inThinkingPhase) {
183
+ fullThinking += event.content;
184
+ } else if (event.type === 'usage') usage = event.usage;
185
+ }
186
+
187
+ const response = {
188
+ id: requestId,
189
+ object: 'chat.completion',
190
+ created: Math.floor(Date.now() / 1000),
191
+ model,
192
+ choices: [{
193
+ index: 0,
194
+ message: {
195
+ role: 'assistant',
196
+ content: mergeThinking && fullThinking
197
+ ? fullThinking + (fullContent ? '\n' + fullContent : '')
198
+ : fullContent,
199
+ ...((!mergeThinking && fullThinking) ? { reasoning_content: fullThinking } : {}),
200
+ },
201
+ finish_reason: 'stop',
202
+ }],
203
+ usage: {
204
+ prompt_tokens: 0,
205
+ completion_tokens: usage,
206
+ total_tokens: usage,
207
+ },
208
+ };
209
+ res.json(response);
210
+ }
211
+ } catch (err) {
212
+ console.error('Stream error:', err.message);
213
+ if (!res.headersSent) {
214
+ res.status(500).json({ error: { message: err.message } });
215
+ } else {
216
+ res.end();
217
+ }
218
+ } finally {
219
+ slot.release();
220
+ dispatchQueued();
221
+ }
222
+ }
223
+
224
+ export function handleOpenAIModels(req, res) {
225
+ res.json({
226
+ object: 'list',
227
+ data: Object.keys(MODEL_MAP).map((id, i) => ({
228
+ id,
229
+ object: 'model',
230
+ created: 1700000000,
231
+ owned_by: 'deepseek',
232
+ })),
233
+ });
234
+ }
src/pow.js ADDED
@@ -0,0 +1,220 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { pickToken } from './auth.js';
2
+ import { apiHeaders, proxiedFetch } from './headers.js';
3
+ import { readFileSync } from 'fs';
4
+ import { fileURLToPath } from 'url';
5
+ import { dirname, join } from 'path';
6
+
7
+ const BASE_URL = 'https://chat.deepseek.com';
8
+
9
+ // Load and instantiate WASM sha3 module (one-time init)
10
+ let wasmInstance = null;
11
+
12
+ async function getWasm() {
13
+ if (wasmInstance) return wasmInstance;
14
+ const wasmPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'sha3_wasm_bg.wasm');
15
+ const wasmBuf = readFileSync(wasmPath);
16
+ const { instance } = await WebAssembly.instantiate(wasmBuf, { wbg: {} });
17
+ wasmInstance = instance;
18
+ return instance;
19
+ }
20
+
21
+ const encoder = new TextEncoder();
22
+
23
+ function passStringToWasm(instance, str) {
24
+ const { memory, __wbindgen_export_0 } = instance.exports;
25
+ const encoded = encoder.encode(str);
26
+ const ptr = __wbindgen_export_0(encoded.length, 1) >>> 0;
27
+ new Uint8Array(memory.buffer).set(encoded, ptr);
28
+ return [ptr, encoded.length];
29
+ }
30
+
31
+ function wasmSolve(instance, challengeHex, prefix, difficulty) {
32
+ const { memory, wasm_solve, __wbindgen_add_to_stack_pointer } = instance.exports;
33
+ const [chPtr, chLen] = passStringToWasm(instance, challengeHex);
34
+ const [pfPtr, pfLen] = passStringToWasm(instance, prefix);
35
+
36
+ const st = __wbindgen_add_to_stack_pointer(-16);
37
+ wasm_solve(st, chPtr, chLen, pfPtr, pfLen, difficulty);
38
+ const v = new DataView(memory.buffer);
39
+ const found = v.getInt32(st, true);
40
+ const answer = v.getFloat64(st + 8, true);
41
+ __wbindgen_add_to_stack_pointer(16);
42
+
43
+ if (!found) throw new Error('PoW solve failed: no solution within difficulty');
44
+ return answer;
45
+ }
46
+
47
+ // BigInt fallback
48
+ const RC = [
49
+ 0x0000000000000001n, 0x0000000000008082n, 0x800000000000808an, 0x8000000080008000n,
50
+ 0x000000000000808bn, 0x0000000080000001n, 0x8000000080008081n, 0x8000000000008009n,
51
+ 0x000000000000008an, 0x0000000000000088n, 0x0000000080008009n, 0x000000008000000an,
52
+ 0x000000008000808bn, 0x800000000000008bn, 0x8000000000008089n, 0x8000000000008003n,
53
+ 0x8000000000008002n, 0x8000000000000080n, 0x000000000000800an, 0x800000008000000an,
54
+ 0x8000000080008081n, 0x8000000000008080n, 0x0000000080000001n, 0x8000000080008008n,
55
+ ];
56
+ const MASK = 0xffffffffffffffffn;
57
+ const RATE = 136;
58
+
59
+ function rotl64(v, k) { return ((v << BigInt(k)) | (v >> BigInt(64 - k))) & MASK; }
60
+
61
+ function keccakF23(s) {
62
+ let a = s.slice();
63
+ for (let r = 1; r < 24; r++) {
64
+ const c = [a[0]^a[5]^a[10]^a[15]^a[20], a[1]^a[6]^a[11]^a[16]^a[21], a[2]^a[7]^a[12]^a[17]^a[22],
65
+ a[3]^a[8]^a[13]^a[18]^a[23], a[4]^a[9]^a[14]^a[19]^a[24]];
66
+ const d = [c[4]^rotl64(c[1],1), c[0]^rotl64(c[2],1), c[1]^rotl64(c[3],1), c[2]^rotl64(c[4],1), c[3]^rotl64(c[0],1)];
67
+ for (let i = 0; i < 25; i++) a[i] = (a[i] ^ d[i%5]) & MASK;
68
+ const b = new Array(25);
69
+ b[0]=a[0];b[10]=rotl64(a[1],1);b[20]=rotl64(a[2],62);b[5]=rotl64(a[3],28);b[15]=rotl64(a[4],27);
70
+ b[16]=rotl64(a[5],36);b[1]=rotl64(a[6],44);b[11]=rotl64(a[7],6);b[21]=rotl64(a[8],55);b[6]=rotl64(a[9],20);
71
+ b[7]=rotl64(a[10],3);b[17]=rotl64(a[11],10);b[2]=rotl64(a[12],43);b[12]=rotl64(a[13],25);b[22]=rotl64(a[14],39);
72
+ b[23]=rotl64(a[15],41);b[8]=rotl64(a[16],45);b[18]=rotl64(a[17],15);b[3]=rotl64(a[18],21);b[13]=rotl64(a[19],8);
73
+ b[14]=rotl64(a[20],18);b[24]=rotl64(a[21],2);b[9]=rotl64(a[22],61);b[19]=rotl64(a[23],56);b[4]=rotl64(a[24],14);
74
+ for (let y = 0; y < 5; y++) {
75
+ const t = [b[y*5], b[y*5+1], b[y*5+2], b[y*5+3], b[y*5+4]];
76
+ for (let x = 0; x < 5; x++) a[y*5+x] = (t[x] ^ ((t[(x+1)%5] ^ MASK) & t[(x+2)%5])) & MASK;
77
+ }
78
+ a[0] = (a[0] ^ RC[r]) & MASK;
79
+ }
80
+ for (let i = 0; i < 25; i++) s[i] = a[i];
81
+ }
82
+
83
+ function readU64LE(buf, off) {
84
+ let v = 0n;
85
+ for (let i = 0; i < 8; i++) v |= BigInt(buf[off + i]) << BigInt(i * 8);
86
+ return v & MASK;
87
+ }
88
+
89
+ function writeU64LE(val, buf, off) {
90
+ const v = val & MASK;
91
+ for (let i = 0; i < 8; i++) buf[off + i] = Number((v >> BigInt(i * 8)) & 0xffn);
92
+ }
93
+
94
+ function preAbsorbPrefix(prefix) {
95
+ const s = new Array(25).fill(0n);
96
+ const buf = Buffer.from(prefix);
97
+ let off = 0;
98
+ while (off + RATE <= buf.length) {
99
+ for (let i = 0; i < RATE / 8; i++) s[i] = (s[i] ^ readU64LE(buf, off + i * 8)) & MASK;
100
+ keccakF23(s);
101
+ off += RATE;
102
+ }
103
+ const tailLen = buf.length - off;
104
+ const tail = Buffer.alloc(RATE);
105
+ buf.copy(tail, 0, off);
106
+ return { s, tail, tailLen };
107
+ }
108
+
109
+ function solvePowBigInt(challengeHex, salt, expireAt, difficulty) {
110
+ if (challengeHex.length !== 64) throw new Error('pow: challenge must be 64 hex chars');
111
+ const target = Buffer.from(challengeHex, 'hex');
112
+ const t0 = readU64LE(target, 0);
113
+ const t1 = readU64LE(target, 8);
114
+ const t2 = readU64LE(target, 16);
115
+ const t3 = readU64LE(target, 24);
116
+ const prefix = `${salt}_${expireAt}_`;
117
+ const { s: baseState, tail, tailLen } = preAbsorbPrefix(prefix);
118
+
119
+ for (let n = 0; n < difficulty; n++) {
120
+ const nonceStr = n.toString();
121
+ const nonceBuf = Buffer.from(nonceStr);
122
+ const totalTail = tailLen + nonceBuf.length;
123
+ const s = baseState.slice();
124
+
125
+ if (totalTail < RATE) {
126
+ const buf = Buffer.alloc(RATE);
127
+ tail.copy(buf, 0, 0, tailLen);
128
+ nonceBuf.copy(buf, tailLen);
129
+ buf[totalTail] = 0x06;
130
+ buf[RATE - 1] |= 0x80;
131
+ for (let i = 0; i < RATE / 8; i++) s[i] = (s[i] ^ readU64LE(buf, i * 8)) & MASK;
132
+ keccakF23(s);
133
+ } else {
134
+ const buf1 = Buffer.alloc(RATE);
135
+ tail.copy(buf1, 0, 0, tailLen);
136
+ nonceBuf.copy(buf1, tailLen, 0, RATE - tailLen);
137
+ for (let i = 0; i < RATE / 8; i++) s[i] = (s[i] ^ readU64LE(buf1, i * 8)) & MASK;
138
+ keccakF23(s);
139
+ const rem = totalTail - RATE;
140
+ const buf2 = Buffer.alloc(RATE);
141
+ nonceBuf.copy(buf2, 0, RATE - tailLen, RATE - tailLen + rem);
142
+ buf2[rem] = 0x06;
143
+ buf2[RATE - 1] |= 0x80;
144
+ for (let i = 0; i < RATE / 8; i++) s[i] = (s[i] ^ readU64LE(buf2, i * 8)) & MASK;
145
+ keccakF23(s);
146
+ }
147
+
148
+ if (s[0] === t0 && s[1] === t1 && s[2] === t2 && s[3] === t3) return n;
149
+ }
150
+
151
+ throw new Error('PoW solve failed: no solution within difficulty');
152
+ }
153
+
154
+ let useWasm = true;
155
+
156
+ async function solvePowInternal(token, targetPath = '/api/v0/chat/completion') {
157
+ const res = await proxiedFetch(`${BASE_URL}/api/v0/chat/create_pow_challenge`, {
158
+ method: 'POST',
159
+ headers: await apiHeaders(token),
160
+ body: JSON.stringify({ target_path: targetPath }),
161
+ });
162
+
163
+ if (res.status === 202) {
164
+ throw new Error('PoW challenge: WAF blocked (202)');
165
+ }
166
+
167
+ const text = await res.text();
168
+ if (!text) throw new Error('PoW challenge: empty response');
169
+
170
+ const json = JSON.parse(text);
171
+ const c = json.data?.biz_data?.challenge;
172
+ if (!c) throw new Error('PoW challenge failed: no challenge returned');
173
+ if (c.algorithm !== 'DeepSeekHashV1') throw new Error(`Unsupported PoW algorithm: ${c.algorithm}`);
174
+
175
+ const difficulty = c.difficulty || 144000;
176
+ const prefix = `${c.salt}_${c.expire_at}_`;
177
+ let answer;
178
+
179
+ if (useWasm) {
180
+ try {
181
+ const instance = await getWasm();
182
+ answer = wasmSolve(instance, c.challenge, prefix, difficulty);
183
+ } catch (e) {
184
+ console.warn('WASM PoW failed, falling back to BigInt:', e.message);
185
+ useWasm = false;
186
+ answer = solvePowBigInt(c.challenge, c.salt, c.expire_at, difficulty);
187
+ }
188
+ } else {
189
+ answer = solvePowBigInt(c.challenge, c.salt, c.expire_at, difficulty);
190
+ }
191
+
192
+ const powResponse = {
193
+ algorithm: c.algorithm,
194
+ challenge: c.challenge,
195
+ salt: c.salt,
196
+ answer,
197
+ signature: c.signature,
198
+ target_path: c.target_path,
199
+ };
200
+
201
+ return Buffer.from(JSON.stringify(powResponse)).toString('base64');
202
+ }
203
+
204
+ // For chat.js — takes explicit token (same token used for PoW and completion)
205
+ export async function solvePowChallengeWithToken(token, targetPath = '/api/v0/chat/completion') {
206
+ const powResponse = await solvePowInternal(token, targetPath);
207
+ return { powResponse };
208
+ }
209
+
210
+ // Legacy: picks a token automatically (used by upload.js)
211
+ export async function solvePowChallenge(preferVision = false) {
212
+ const token = pickToken(preferVision);
213
+ const powResponse = await solvePowInternal(token, '/api/v0/chat/completion');
214
+ return { powResponse, token };
215
+ }
216
+
217
+ // For upload.js — takes explicit token
218
+ export async function solvePowChallengeForUpload(token) {
219
+ return solvePowInternal(token, '/api/v0/file/upload_file');
220
+ }
src/queue.js ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { acquireToken } from './auth.js';
2
+
3
+ const MAX_QUEUE_SIZE = 100;
4
+ const queue = [];
5
+
6
+ export function enqueueRequest(preferVision, timeoutMs = 30000) {
7
+ return new Promise((resolve, reject) => {
8
+ const timer = setTimeout(() => {
9
+ const idx = queue.findIndex(e => e.resolve === resolve);
10
+ if (idx !== -1) queue.splice(idx, 1);
11
+ reject(new Error('Request timed out waiting for available token'));
12
+ }, timeoutMs);
13
+
14
+ // Try immediate acquire first
15
+ const slot = acquireToken(preferVision);
16
+ if (slot) {
17
+ clearTimeout(timer);
18
+ resolve(slot);
19
+ return;
20
+ }
21
+
22
+ if (queue.length >= MAX_QUEUE_SIZE) {
23
+ clearTimeout(timer);
24
+ reject(new Error('Too many queued requests, try again later'));
25
+ return;
26
+ }
27
+
28
+ queue.push({ preferVision, resolve: (slot) => { clearTimeout(timer); resolve(slot); }, reject: (err) => { clearTimeout(timer); reject(err); } });
29
+ });
30
+ }
31
+
32
+ // Call when a token is released — try to dispatch queued request
33
+ export function dispatchQueued() {
34
+ while (queue.length > 0) {
35
+ const next = queue[0];
36
+ const slot = acquireToken(next.preferVision);
37
+ if (!slot) break; // No token available yet
38
+ queue.shift();
39
+ next.resolve(slot);
40
+ }
41
+ }
42
+
43
+ export function getQueueInfo() {
44
+ return { queued: queue.length, maxQueueSize: MAX_QUEUE_SIZE };
45
+ }
src/session.js ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { reportTokenError, reportTokenSuccess, setRequestToken, getRequestToken } from './auth.js';
2
+ import { apiHeaders, proxiedFetch } from './headers.js';
3
+
4
+ const BASE_URL = 'https://chat.deepseek.com';
5
+ const SESSION_TTL = 259200; // 3 days in seconds
6
+
7
+ const sessionPool = new Map(); // key: token:model_type, value: { id, model_type, createdAt, token }
8
+
9
+ export async function createSession(token, modelType = 'default') {
10
+ const res = await proxiedFetch(`${BASE_URL}/api/v0/chat_session/create`, {
11
+ method: 'POST',
12
+ headers: await apiHeaders(token),
13
+ body: JSON.stringify({}),
14
+ });
15
+ const json = await res.json();
16
+
17
+ // Token invalid — report error so auth.js can mark it dead
18
+ if (json.code === 40003) {
19
+ reportTokenError(token);
20
+ throw new Error('Token invalid (40003)');
21
+ }
22
+
23
+ const session = json.data?.biz_data?.chat_session;
24
+ if (!session) {
25
+ reportTokenError(token);
26
+ throw new Error(`Session create failed: ${json.msg || JSON.stringify(json)}`);
27
+ }
28
+
29
+ reportTokenSuccess(token);
30
+ return session;
31
+ }
32
+
33
+ export async function getSession(token, modelType) {
34
+ const cacheKey = `${token.slice(0, 12)}:${modelType}`;
35
+ const now = Date.now() / 1000;
36
+ const cached = sessionPool.get(cacheKey);
37
+
38
+ if (cached && (now - cached.createdAt) < SESSION_TTL) {
39
+ return cached;
40
+ }
41
+
42
+ const session = await createSession(token, modelType);
43
+ session.createdAt = now;
44
+ session.token = token;
45
+ sessionPool.set(cacheKey, session);
46
+ return session;
47
+ }
48
+
49
+ // Remove cached sessions for a specific token prefix (used after token refresh)
50
+ export function invalidateTokenSessions(tokenPrefix) {
51
+ for (const key of sessionPool.keys()) {
52
+ if (key.startsWith(tokenPrefix + ':')) {
53
+ sessionPool.delete(key);
54
+ }
55
+ }
56
+ }
57
+
58
+ export function getSessionInfo() {
59
+ const now = Date.now() / 1000;
60
+ const entries = [];
61
+ for (const [key, val] of sessionPool) {
62
+ const age = now - val.createdAt;
63
+ entries.push({
64
+ key,
65
+ modelType: val.model_type,
66
+ ageSeconds: Math.floor(age),
67
+ ttlRemainingSeconds: Math.max(0, Math.floor(SESSION_TTL - age)),
68
+ });
69
+ }
70
+ return { count: sessionPool.size, ttl: SESSION_TTL, sessions: entries };
71
+ }
72
+
73
+ export async function prewarmSessions(tokens, modelTypes = ['default', 'expert']) {
74
+ const { getPoolInfo } = await import('./auth.js');
75
+ const poolInfo = getPoolInfo();
76
+ const alivePrefixes = poolInfo.filter(t => !t.dead && t.token !== 'NONE').map(t => t.token.replace('...', ''));
77
+
78
+ console.log(`Pre-warming sessions for ${alivePrefixes.length} alive tokens × ${modelTypes.length} model types...`);
79
+ const promises = [];
80
+ for (const token of tokens) {
81
+ const prefix = token.slice(0, 12);
82
+ if (!alivePrefixes.includes(prefix)) continue;
83
+ for (const modelType of modelTypes) {
84
+ const cacheKey = `${prefix}:${modelType}`;
85
+ if (!sessionPool.has(cacheKey)) {
86
+ promises.push(
87
+ getSession(token, modelType).catch(() => {})
88
+ );
89
+ }
90
+ }
91
+ if (promises.length >= 6) {
92
+ await Promise.allSettled(promises.splice(0));
93
+ }
94
+ }
95
+ if (promises.length > 0) {
96
+ await Promise.allSettled(promises);
97
+ }
98
+ console.log(`Session pool: ${sessionPool.size} cached sessions`);
99
+ }
src/upload.js ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { pickToken, setRequestToken } from './auth.js';
2
+ import { solvePowChallengeForUpload } from './pow.js';
3
+ import { apiHeaders, getHeaders, proxiedFetch } from './headers.js';
4
+
5
+ const BASE_URL = 'https://chat.deepseek.com';
6
+
7
+ export async function uploadFile(fileBuffer, filename, mimeType, token) {
8
+ const powResponse = await solvePowChallengeForUpload(token);
9
+
10
+ const boundary = `----FormBoundary${Date.now()}`;
11
+ const header = Buffer.from(
12
+ `--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="${filename}"\r\nContent-Type: ${mimeType}\r\n\r\n`
13
+ );
14
+ const footer = Buffer.from(`\r\n--${boundary}--\r\n`);
15
+ const body = Buffer.concat([header, fileBuffer, footer]);
16
+
17
+ const res = await proxiedFetch(`${BASE_URL}/api/v0/file/upload_file`, {
18
+ method: 'POST',
19
+ headers: {
20
+ ...await apiHeaders(token, {
21
+ 'x-ds-pow-response': powResponse,
22
+ 'x-file-size': body.length.toString(),
23
+ 'x-model-type': 'vision',
24
+ 'content-type': `multipart/form-data; boundary=${boundary}`,
25
+ }),
26
+ },
27
+ body,
28
+ });
29
+
30
+ const json = await res.json();
31
+ const fileId = json.data?.biz_data?.id;
32
+ if (!fileId) throw new Error(`File upload failed: ${JSON.stringify(json)}`);
33
+
34
+ return fileId;
35
+ }
36
+
37
+ export async function waitForFileReady(fileId, token, maxAttempts = 30, intervalMs = 2000) {
38
+ for (let i = 0; i < maxAttempts; i++) {
39
+ const res = await proxiedFetch(`${BASE_URL}/api/v0/file/fetch_files?file_ids=${fileId}`, {
40
+ headers: await getHeaders(token),
41
+ });
42
+ const json = await res.json();
43
+ const files = json.data?.biz_data?.files;
44
+ if (files && files.length > 0) {
45
+ const status = files[0].status?.toUpperCase();
46
+ if (status === 'SUCCESS' || status === 'PROCESSED' || status === 'READY' || status === 'DONE') {
47
+ return files[0];
48
+ }
49
+ if (status === 'FAILED') {
50
+ throw new Error(`File processing failed: ${files[0].error_code || 'unknown'}`);
51
+ }
52
+ }
53
+
54
+ await new Promise(r => setTimeout(r, intervalMs));
55
+ }
56
+
57
+ throw new Error(`File not ready after ${maxAttempts * intervalMs / 1000}s`);
58
+ }
59
+
60
+ export async function uploadImageFromFile(fileBuffer, filename, mimeType, token) {
61
+ const fileId = await uploadFile(fileBuffer, filename, mimeType, token);
62
+ await waitForFileReady(fileId, token);
63
+ return fileId;
64
+ }
65
+
66
+ const MIME_MAP = {
67
+ 'jpg': 'image/jpeg',
68
+ 'jpeg': 'image/jpeg',
69
+ 'png': 'image/png',
70
+ 'gif': 'image/gif',
71
+ 'webp': 'image/webp',
72
+ 'svg': 'image/svg+xml',
73
+ 'bmp': 'image/bmp',
74
+ };
75
+
76
+ function extFromDataUrl(dataUrl) {
77
+ const match = dataUrl.match(/^data:(image\/\w+);/);
78
+ if (!match) return { ext: 'png', mime: 'image/png' };
79
+ const mime = match[1];
80
+ const ext = mime.split('/')[1] || 'png';
81
+ return { ext: ext === 'jpeg' ? 'jpg' : ext, mime };
82
+ }
83
+
84
+ export async function resolveImageToRefId(imageUrl, token) {
85
+ let buffer, filename, mimeType;
86
+
87
+ if (imageUrl.startsWith('data:')) {
88
+ const { ext, mime } = extFromDataUrl(imageUrl);
89
+ const base64 = imageUrl.split(',')[1];
90
+ if (!base64) throw new Error('Invalid data URL: no base64 data');
91
+ buffer = Buffer.from(base64, 'base64');
92
+ filename = `image.${ext}`;
93
+ mimeType = mime;
94
+ } else {
95
+ const res = await proxiedFetch(imageUrl);
96
+ if (!res.ok) throw new Error(`Failed to download image: ${res.status}`);
97
+ const contentType = res.headers.get('content-type') || 'image/png';
98
+ buffer = Buffer.from(await res.arrayBuffer());
99
+ const urlPath = new URL(imageUrl).pathname;
100
+ const ext = urlPath.split('.').pop()?.toLowerCase() || 'png';
101
+ const mappedMime = MIME_MAP[ext] || contentType;
102
+ filename = `image.${mappedMime.split('/')[1] === 'jpeg' ? 'jpg' : (mappedMime.split('/')[1] || 'png')}`;
103
+ mimeType = mappedMime;
104
+ }
105
+
106
+ return uploadImageFromFile(buffer, filename, mimeType, token);
107
+ }
test_single_flow.mjs ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const token = 'jzP1DfT7dlWGJ/h4imBWbVB6zGdYLBWB0hRqne6WxMFGFarEPkfj7MRcOiw3inKj';
2
+
3
+ // Inline the PoW solver to test independently
4
+ const RC = [
5
+ 0x0000000000000001n, 0x0000000000008082n, 0x800000000000808an, 0x8000000080008000n,
6
+ 0x000000000000808bn, 0x0000000080000001n, 0x8000000080008081n, 0x8000000000008009n,
7
+ 0x000000000000008an, 0x0000000000000088n, 0x0000000080008009n, 0x000000008000000an,
8
+ 0x000000008000808bn, 0x800000000000008bn, 0x8000000000008089n, 0x8000000000008003n,
9
+ 0x8000000000008002n, 0x8000000000000080n, 0x000000000000800an, 0x800000008000000an,
10
+ 0x8000000080008081n, 0x8000000000008080n, 0x0000000080000001n, 0x8000000080008008n,
11
+ ];
12
+ const MASK = 0xffffffffffffffffn;
13
+ const RATE = 136;
14
+
15
+ function rotl64(v, k) { return ((v << BigInt(k)) | (v >> BigInt(64 - k))) & MASK; }
16
+ function keccakF23(s) {
17
+ let a = s.slice();
18
+ for (let r = 1; r < 24; r++) {
19
+ const c = [a[0]^a[5]^a[10]^a[15]^a[20], a[1]^a[6]^a[11]^a[16]^a[21], a[2]^a[7]^a[12]^a[17]^a[22],
20
+ a[3]^a[8]^a[13]^a[18]^a[23], a[4]^a[9]^a[14]^a[19]^a[24]];
21
+ const d = [c[4]^rotl64(c[1],1), c[0]^rotl64(c[2],1), c[1]^rotl64(c[3],1), c[2]^rotl64(c[4],1), c[3]^rotl64(c[0],1)];
22
+ for (let i = 0; i < 25; i++) a[i] = (a[i] ^ d[i%5]) & MASK;
23
+ const b = new Array(25);
24
+ b[0]=a[0];b[10]=rotl64(a[1],1);b[20]=rotl64(a[2],62);b[5]=rotl64(a[3],28);b[15]=rotl64(a[4],27);
25
+ b[16]=rotl64(a[5],36);b[1]=rotl64(a[6],44);b[11]=rotl64(a[7],6);b[21]=rotl64(a[8],55);b[6]=rotl64(a[9],20);
26
+ b[7]=rotl64(a[10],3);b[17]=rotl64(a[11],10);b[2]=rotl64(a[12],43);b[12]=rotl64(a[13],25);b[22]=rotl64(a[14],39);
27
+ b[23]=rotl64(a[15],41);b[8]=rotl64(a[16],45);b[18]=rotl64(a[17],15);b[3]=rotl64(a[18],21);b[13]=rotl64(a[19],8);
28
+ b[14]=rotl64(a[20],18);b[24]=rotl64(a[21],2);b[9]=rotl64(a[22],61);b[19]=rotl64(a[23],56);b[4]=rotl64(a[24],14);
29
+ for (let y = 0; y < 5; y++) {
30
+ const t = [b[y*5], b[y*5+1], b[y*5+2], b[y*5+3], b[y*5+4]];
31
+ for (let x = 0; x < 5; x++) a[y*5+x] = (t[x] ^ ((t[(x+1)%5] ^ MASK) & t[(x+2)%5])) & MASK;
32
+ }
33
+ a[0] = (a[0] ^ RC[r]) & MASK;
34
+ }
35
+ for (let i = 0; i < 25; i++) s[i] = a[i];
36
+ }
37
+ function readU64LE(buf, off) { let v = 0n; for (let i = 0; i < 8; i++) v |= BigInt(buf[off + i]) << BigInt(i * 8); return v & MASK; }
38
+ function writeU64LE(val, buf, off) { const v = val & MASK; for (let i = 0; i < 8; i++) buf[off + i] = Number((v >> BigInt(i * 8)) & 0xffn); }
39
+
40
+ function deepSeekHashV1(data) {
41
+ const s = new Array(25).fill(0n);
42
+ const buf = Buffer.from(data);
43
+ let off = 0;
44
+ while (off + RATE <= buf.length) { for (let i = 0; i < RATE / 8; i++) s[i] = (s[i] ^ readU64LE(buf, off + i * 8)) & MASK; keccakF23(s); off += RATE; }
45
+ const tail = Buffer.alloc(RATE); buf.copy(tail, 0, off); tail[buf.length - off] = 0x06; tail[RATE - 1] |= 0x80;
46
+ for (let i = 0; i < RATE / 8; i++) s[i] = (s[i] ^ readU64LE(tail, i * 8)) & MASK; keccakF23(s);
47
+ const out = Buffer.alloc(32); for (let i = 0; i < 4; i++) writeU64LE(s[i], out, i * 8); return out;
48
+ }
49
+
50
+ function solvePow(challengeHex, salt, expireAt, difficulty) {
51
+ const target = Buffer.from(challengeHex, 'hex');
52
+ const prefix = `${salt}_${expireAt}_`;
53
+ console.log('Solving PoW, prefix:', prefix);
54
+ const t0 = Date.now();
55
+ for (let n = 0; n < difficulty; n++) {
56
+ const hash = deepSeekHashV1(prefix + n.toString());
57
+ if (hash.equals(target)) {
58
+ console.log('Found nonce:', n, 'in', Date.now() - t0, 'ms');
59
+ return n;
60
+ }
61
+ }
62
+ throw new Error('PoW failed');
63
+ }
64
+
65
+ async function test() {
66
+ // Step 1: Get challenge ONCE
67
+ const r1 = await fetch('https://chat.deepseek.com/api/v0/chat/create_pow_challenge', {
68
+ method: 'POST',
69
+ headers: {
70
+ 'content-type': 'application/json',
71
+ 'authorization': 'Bearer ' + token,
72
+ 'x-app-version': '2.0.0',
73
+ 'x-client-version': '2.0.0',
74
+ 'x-client-platform': 'web',
75
+ },
76
+ body: JSON.stringify({ target_path: '/api/v0/chat/completion' }),
77
+ });
78
+ const j1 = await r1.json();
79
+ const c = j1.data.biz_data.challenge;
80
+ console.log('Challenge:', c.challenge.slice(0, 20), 'difficulty:', c.difficulty);
81
+
82
+ // Step 2: Solve
83
+ const answer = solvePow(c.challenge, c.salt, c.expire_at, c.difficulty);
84
+ const powResponse = Buffer.from(JSON.stringify({
85
+ algorithm: c.algorithm,
86
+ challenge: c.challenge,
87
+ salt: c.salt,
88
+ answer: answer,
89
+ signature: c.signature,
90
+ target_path: c.target_path,
91
+ })).toString('base64');
92
+ console.log('PoW b64 (first 50):', powResponse.slice(0, 50));
93
+
94
+ // Verify: decode and check challenge matches
95
+ const decoded = JSON.parse(Buffer.from(powResponse, 'base64').toString());
96
+ console.log('Decoded challenge matches:', decoded.challenge === c.challenge);
97
+ console.log('Decoded answer:', decoded.answer);
98
+
99
+ // Step 3: Create session
100
+ const r2 = await fetch('https://chat.deepseek.com/api/v0/chat_session/create', {
101
+ method: 'POST',
102
+ headers: {
103
+ 'content-type': 'application/json',
104
+ 'authorization': 'Bearer ' + token,
105
+ 'x-app-version': '2.0.0',
106
+ 'x-client-version': '2.0.0',
107
+ 'x-client-platform': 'web',
108
+ },
109
+ body: JSON.stringify({}),
110
+ });
111
+ const j2 = await r2.json();
112
+ const sid = j2.data.biz_data.chat_session.id;
113
+ console.log('Session:', sid);
114
+
115
+ // Step 4: Call completion WITH PoW
116
+ const r3 = await fetch('https://chat.deepseek.com/api/v0/chat/completion', {
117
+ method: 'POST',
118
+ headers: {
119
+ 'content-type': 'application/json',
120
+ 'authorization': 'Bearer ' + token,
121
+ 'x-ds-pow-response': powResponse,
122
+ 'x-app-version': '2.0.0',
123
+ 'x-client-version': '2.0.0',
124
+ 'x-client-platform': 'web',
125
+ 'x-client-locale': 'zh_CN',
126
+ 'accept': 'text/event-stream',
127
+ },
128
+ body: JSON.stringify({
129
+ chat_session_id: sid,
130
+ parent_message_id: null,
131
+ model_type: 'default',
132
+ prompt: '1+1=?',
133
+ ref_file_ids: [],
134
+ thinking_enabled: false,
135
+ search_enabled: false,
136
+ preempt: false,
137
+ }),
138
+ });
139
+ console.log('Status:', r3.status);
140
+ console.log('CT:', r3.headers.get('content-type'));
141
+ const text = await r3.text();
142
+ console.log('Response:', text.slice(0, 800));
143
+ }
144
+
145
+ test().catch(e => console.error('Error:', e.message));
test_waf.js ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // The guest challenge doesn't work with these paths. Let me look at the JS bundle more carefully
2
+ // for what the actual auth flow looks like now.
3
+
4
+ // Key finding from the bundle analysis:
5
+ // - AWS WAF captcha SDK is loaded: "https://a0fea896111e.edge.captcha-sdk.awswaf.com/a0fea896111e/jsapi.js"
6
+ // - There's a silent WAF challenge: "https://a0fea896111e.edge.sdk.awswaf.com/a0fea896111e/3a8b889b892b/challenge.js"
7
+ // - This means the WAF requires solving a challenge to get a token/cookie
8
+ // - The 40003 might be because we're missing the WAF token cookie
9
+
10
+ // Let's check what cookies the WAF sets
11
+ async function testWAF() {
12
+ const r = await fetch('https://chat.deepseek.com/', {
13
+ headers: {
14
+ 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
15
+ },
16
+ });
17
+
18
+ // Check Set-Cookie headers
19
+ const setCookies = r.headers.getSetCookie?.() || [];
20
+ console.log('Set-Cookie headers:', setCookies.length);
21
+ for (const c of setCookies) {
22
+ console.log('Cookie:', c.slice(0, 200));
23
+ }
24
+
25
+ // Check if there's a WAF challenge in the response
26
+ const html = await r.text();
27
+ const wafRefs = html.match(/awswaf|captcha-sdk|challenge\.js/gi);
28
+ console.log('WAF refs in HTML:', wafRefs);
29
+ }
30
+
31
+ testWAF().catch(e => console.error(e));