Upload 17 files
Browse files- Dockerfile +15 -0
- package-lock.json +912 -0
- package.json +16 -0
- src/admin/index.html +655 -0
- src/auth.js +463 -0
- src/chat.js +177 -0
- src/deepseek.js +57 -0
- src/headers.js +226 -0
- src/index.js +118 -0
- src/logger.js +48 -0
- src/openai.js +234 -0
- src/pow.js +220 -0
- src/queue.js +45 -0
- src/session.js +99 -0
- src/upload.js +107 -0
- test_single_flow.mjs +145 -0
- 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));
|