Compare commits

...
6 Commits
Author SHA1 Message Date
feie9454 0ace2929a5 feat: 添加网络健康检查,完善LLM 2025-10-17 08:21:25 +08:00
feie9454 3506ee9bd0 LLM 2025-10-15 23:31:54 +08:00
feie9454 075ed9296d feat: finist chromium download douyin media 2025-08-31 15:27:34 +08:00
feie9454 38dc501f4f test playwright 2025-08-30 22:46:31 +08:00
feie9454 f4d2df9144 test playwright 2025-08-30 22:41:55 +08:00
feie9454 6107d9978a test playwright 2025-08-30 22:41:46 +08:00
15 changed files with 800 additions and 151 deletions
+1
View File
@@ -32,3 +32,4 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Finder (MacOS) folder config # Finder (MacOS) folder config
.DS_Store .DS_Store
.node-persist
+95 -2
View File
@@ -4,12 +4,21 @@
"": { "": {
"name": "qq-bot", "name": "qq-bot",
"dependencies": { "dependencies": {
"@types/cors": "^2.8.19",
"@types/multer": "^2.0.0",
"@types/node-persist": "^3.1.8",
"chalk": "^5",
"cors": "^2.8.5",
"dotenv": "^17.0.0", "dotenv": "^17.0.0",
"express": "^5.1.0", "express": "^5.1.0",
"multer": "^2.0.2",
"node-persist": "^4.0.4",
"openai": "^6.3.0",
}, },
"devDependencies": { "devDependencies": {
"@types/bun": "latest", "@types/bun": "latest",
"@types/express": "^5.0.3", "@types/express": "^5.0.3",
"playwright": "^1.55.0",
}, },
"peerDependencies": { "peerDependencies": {
"typescript": "^5", "typescript": "^5",
@@ -23,6 +32,8 @@
"@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="],
"@types/cors": ["@types/cors@2.8.19", "https://registry.npmmirror.com/@types/cors/-/cors-2.8.19.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg=="],
"@types/express": ["@types/express@5.0.3", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "*" } }, "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw=="], "@types/express": ["@types/express@5.0.3", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "*" } }, "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw=="],
"@types/express-serve-static-core": ["@types/express-serve-static-core@5.0.6", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA=="], "@types/express-serve-static-core": ["@types/express-serve-static-core@5.0.6", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA=="],
@@ -31,7 +42,11 @@
"@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="], "@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="],
"@types/node": ["@types/node@24.0.4", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ulyqAkrhnuNq9pB76DRBTkcS6YsmDALy6Ua63V8OhrOBgbcYt6IOdzpw5P1+dyRIyMerzLkeYWBeOXPpA9GMAA=="], "@types/multer": ["@types/multer@2.0.0", "https://registry.npmmirror.com/@types/multer/-/multer-2.0.0.tgz", { "dependencies": { "@types/express": "*" } }, "sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw=="],
"@types/node": ["@types/node@24.3.0", "https://registry.npmmirror.com/@types/node/-/node-24.3.0.tgz", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="],
"@types/node-persist": ["@types/node-persist@3.1.8", "https://registry.npmmirror.com/@types/node-persist/-/node-persist-3.1.8.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-QLidg6/SadZYPrTKxtxL1A85XBoQlG40bhoMdhu6DH6+eNCMr2j+RGfFZ9I9+IY8W/PDwQonJ+iBWD62jZjMfg=="],
"@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="], "@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="],
@@ -43,16 +58,26 @@
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
"append-field": ["append-field@1.0.0", "https://registry.npmmirror.com/append-field/-/append-field-1.0.0.tgz", {}, "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw=="],
"body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="], "body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="],
"buffer-from": ["buffer-from@1.1.2", "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
"bun-types": ["bun-types@1.2.17", "", { "dependencies": { "@types/node": "*" } }, "sha512-ElC7ItwT3SCQwYZDYoAH+q6KT4Fxjl8DtZ6qDulUFBmXA8YB4xo+l54J9ZJN+k2pphfn9vk7kfubeSd5QfTVJQ=="], "bun-types": ["bun-types@1.2.17", "", { "dependencies": { "@types/node": "*" } }, "sha512-ElC7ItwT3SCQwYZDYoAH+q6KT4Fxjl8DtZ6qDulUFBmXA8YB4xo+l54J9ZJN+k2pphfn9vk7kfubeSd5QfTVJQ=="],
"busboy": ["busboy@1.6.0", "https://registry.npmmirror.com/busboy/-/busboy-1.6.0.tgz", { "dependencies": { "streamsearch": "^1.1.0" } }, "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA=="],
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
"chalk": ["chalk@5.6.2", "https://registry.npmmirror.com/chalk/-/chalk-5.6.2.tgz", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
"concat-stream": ["concat-stream@2.0.0", "https://registry.npmmirror.com/concat-stream/-/concat-stream-2.0.0.tgz", { "dependencies": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.0.2", "typedarray": "^0.0.6" } }, "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A=="],
"content-disposition": ["content-disposition@1.0.0", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg=="], "content-disposition": ["content-disposition@1.0.0", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg=="],
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
@@ -61,6 +86,8 @@
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
"cors": ["cors@2.8.5", "https://registry.npmmirror.com/cors/-/cors-2.8.5.tgz", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="],
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
@@ -91,6 +118,8 @@
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
"fsevents": ["fsevents@2.3.2", "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.2.tgz", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
@@ -123,20 +152,38 @@
"mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="], "mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="],
"minimist": ["minimist@1.2.8", "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
"mkdirp": ["mkdirp@0.5.6", "https://registry.npmmirror.com/mkdirp/-/mkdirp-0.5.6.tgz", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"multer": ["multer@2.0.2", "https://registry.npmmirror.com/multer/-/multer-2.0.2.tgz", { "dependencies": { "append-field": "^1.0.0", "busboy": "^1.6.0", "concat-stream": "^2.0.0", "mkdirp": "^0.5.6", "object-assign": "^4.1.1", "type-is": "^1.6.18", "xtend": "^4.0.2" } }, "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw=="],
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
"node-persist": ["node-persist@4.0.4", "https://registry.npmmirror.com/node-persist/-/node-persist-4.0.4.tgz", { "dependencies": { "p-limit": "^3.1.0" } }, "sha512-8sPAz/7tw1mCCc8xBG4f0wi+flHkSSgQeX998iQ75Pu27evA6UUWCjSE7xnrYTg2q33oU5leJ061EKPDv6BocQ=="],
"object-assign": ["object-assign@4.1.1", "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
"openai": ["openai@6.3.0", "https://registry.npmmirror.com/openai/-/openai-6.3.0.tgz", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-E6vOGtZvdcb4yXQ5jXvDlUG599OhIkb/GjBLZXS+qk0HF+PJReIldEc9hM8Ft81vn+N6dRdFRb7BZNK8bbvXrw=="],
"p-limit": ["p-limit@3.1.0", "https://registry.npmmirror.com/p-limit/-/p-limit-3.1.0.tgz", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
"path-to-regexp": ["path-to-regexp@8.2.0", "", {}, "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ=="], "path-to-regexp": ["path-to-regexp@8.2.0", "", {}, "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ=="],
"playwright": ["playwright@1.55.0", "https://registry.npmmirror.com/playwright/-/playwright-1.55.0.tgz", { "dependencies": { "playwright-core": "1.55.0" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-sdCWStblvV1YU909Xqx0DhOjPZE4/5lJsIS84IfN9dAZfcl/CIZ5O8l3o0j7hPMjDvqoTF8ZUcc+i/GL5erstA=="],
"playwright-core": ["playwright-core@1.55.0", "https://registry.npmmirror.com/playwright-core/-/playwright-core-1.55.0.tgz", { "bin": { "playwright-core": "cli.js" } }, "sha512-GvZs4vU3U5ro2nZpeiwyb0zuFaqb9sUiAJuyrWpcGouD8y9/HLgGbNRjIph7zU9D3hnPaisMl9zG9CgFi/biIg=="],
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
"qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="], "qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="],
@@ -145,6 +192,8 @@
"raw-body": ["raw-body@3.0.0", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.6.3", "unpipe": "1.0.0" } }, "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g=="], "raw-body": ["raw-body@3.0.0", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.6.3", "unpipe": "1.0.0" } }, "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g=="],
"readable-stream": ["readable-stream@3.6.2", "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
@@ -167,20 +216,64 @@
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
"streamsearch": ["streamsearch@1.1.0", "https://registry.npmmirror.com/streamsearch/-/streamsearch-1.1.0.tgz", {}, "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg=="],
"string_decoder": ["string_decoder@1.3.0", "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
"type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
"typedarray": ["typedarray@0.0.6", "https://registry.npmmirror.com/typedarray/-/typedarray-0.0.6.tgz", {}, "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="],
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
"undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], "undici-types": ["undici-types@7.10.0", "https://registry.npmmirror.com/undici-types/-/undici-types-7.10.0.tgz", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="],
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
"util-deprecate": ["util-deprecate@1.0.2", "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
"xtend": ["xtend@4.0.2", "https://registry.npmmirror.com/xtend/-/xtend-4.0.2.tgz", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
"yocto-queue": ["yocto-queue@0.1.0", "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
"@types/body-parser/@types/node": ["@types/node@24.0.4", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ulyqAkrhnuNq9pB76DRBTkcS6YsmDALy6Ua63V8OhrOBgbcYt6IOdzpw5P1+dyRIyMerzLkeYWBeOXPpA9GMAA=="],
"@types/connect/@types/node": ["@types/node@24.0.4", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ulyqAkrhnuNq9pB76DRBTkcS6YsmDALy6Ua63V8OhrOBgbcYt6IOdzpw5P1+dyRIyMerzLkeYWBeOXPpA9GMAA=="],
"@types/express-serve-static-core/@types/node": ["@types/node@24.0.4", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ulyqAkrhnuNq9pB76DRBTkcS6YsmDALy6Ua63V8OhrOBgbcYt6IOdzpw5P1+dyRIyMerzLkeYWBeOXPpA9GMAA=="],
"@types/send/@types/node": ["@types/node@24.0.4", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ulyqAkrhnuNq9pB76DRBTkcS6YsmDALy6Ua63V8OhrOBgbcYt6IOdzpw5P1+dyRIyMerzLkeYWBeOXPpA9GMAA=="],
"@types/serve-static/@types/node": ["@types/node@24.0.4", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ulyqAkrhnuNq9pB76DRBTkcS6YsmDALy6Ua63V8OhrOBgbcYt6IOdzpw5P1+dyRIyMerzLkeYWBeOXPpA9GMAA=="],
"bun-types/@types/node": ["@types/node@24.0.4", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ulyqAkrhnuNq9pB76DRBTkcS6YsmDALy6Ua63V8OhrOBgbcYt6IOdzpw5P1+dyRIyMerzLkeYWBeOXPpA9GMAA=="],
"http-errors/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], "http-errors/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="],
"multer/type-is": ["type-is@1.6.18", "https://registry.npmmirror.com/type-is/-/type-is-1.6.18.tgz", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="],
"@types/body-parser/@types/node/undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
"@types/connect/@types/node/undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
"@types/express-serve-static-core/@types/node/undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
"@types/send/@types/node/undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
"@types/serve-static/@types/node/undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
"bun-types/@types/node/undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
"multer/type-is/media-typer": ["media-typer@0.3.0", "https://registry.npmmirror.com/media-typer/-/media-typer-0.3.0.tgz", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="],
"multer/type-is/mime-types": ["mime-types@2.1.35", "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
"multer/type-is/mime-types/mime-db": ["mime-db@1.52.0", "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
} }
} }
+1
View File
@@ -0,0 +1 @@
profiles
+83
View File
@@ -0,0 +1,83 @@
import path from 'path';
import { chromium } from 'playwright';
const __dirname = path.dirname(new URL(import.meta.url).pathname);
export async function downloadDouyinMedia(targetUrl: string, target_id?: string) {
const USER_DATA_DIR = path.join(__dirname, 'profiles/site1');
const TARGET_URL = targetUrl;
const HEADLESS = true;
const TIMEOUT = 10000;
const context = await chromium.launchPersistentContext(USER_DATA_DIR, {
headless: HEADLESS,
});
try {
const page = context.pages()[0] || (await context.newPage());
await page.goto(TARGET_URL, { waitUntil: 'load' });
await page.waitForSelector('.xgplayer', { timeout: TIMEOUT }).catch(() => {
});
if (target_id) {
await page.evaluate(`window.f_down_target_id = "${target_id}"; window.f_down_server_port = ${process.env.PORT || 6100}`)
}
await page.evaluate(async () => {
const d = (globalThis as any).document;
const vEle = d.querySelector('.xgplayer video source')
const isVideo = !!vEle && !vEle.src.includes('.mp3');
const urls: string[] = isVideo ? [d.querySelector(".xgplayer video source").src] : [
...new Set(
Array.from(d.querySelectorAll('.xgplayer img'))
// @ts-ignore
.map(img => img?.src)
.filter(Boolean)
),
];
async function urlToBlob(url: string) {
const res = await fetch(url, { credentials: "omit" });
if (!res.ok) throw new Error(`fetch image failed: ${res.status}`);
return await res.blob(); // 保持原 MIME
}
const fd = new FormData();
// 附带标题作为一个普通字段
fd.append("title", d.title || "");
let idx = 1;
for (const src of urls) {
const blob = await urlToBlob(src); // ✅ 直接用字符串
const ext = (blob.type && blob.type.split("/")[1]) || "bin";
// @ts-ignore
fd.append(`file_${idx}`, blob, `media_${idx}.${ext}`);
idx++;
}
// @ts-ignore
const url = new URL(`http://127.0.0.1:${window.f_down_server_port}/upload`);
url.searchParams.set("title", d.title || "");
url.searchParams.set("type", isVideo ? "video" : "image");
// @ts-ignore
url.searchParams.set("target_id", window.f_down_target_id || "");
const r = await fetch(url.toString(), {
method: "POST",
body: fd,
});
// 为了在 evaluate 内确保完成整个请求-响应流程,读取下响应
const json = await r.json().catch(() => ({}));
return json;
});
await page.waitForTimeout(1000)
} finally {
await context.close();
console.log('Done.');
}
}
+21
View File
@@ -0,0 +1,21 @@
// node login.js https://example.com/login
import { chromium } from 'playwright';
const userDataDir = './profiles/site1';
const startUrl = process.argv[2] || 'https://douyin.com';
const context = await chromium.launchPersistentContext(userDataDir, {
headless: false,
args: ['--disable-blink-features=AutomationControlled'],
});
const page = context.pages()[0] || await context.newPage();
await page.goto(startUrl, { waitUntil: 'domcontentloaded' });
console.log('>>> 请在打开的浏览器里完成登录,完成后回到终端按回车...');
process.stdin.resume();
await new Promise(r => process.stdin.once('data', r));
await context.close();
console.log('>>> 登录态已写入 profiles/site1/,后续可无头复用。');
+82 -137
View File
@@ -2,179 +2,124 @@ import express from 'express';
import type { Request, Response, NextFunction } from 'express'; import type { Request, Response, NextFunction } from 'express';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import cors from "cors";
import multer from "multer";
import * as douyin from './douyin'
import { sendMediaMsg, sendMsg } from './lib/qq';
import * as llm from './llm'
import { testNetwork } from './lib/network';
const app = express(); const app = express();
const PORT = process.env.PORT || 6100; const PORT = process.env.PORT || 6100;
// 中间件:解析 JSON 请求体 const __dirname = path.dirname(new URL(import.meta.url).pathname);
app.use(express.json({ limit: '10mb' }));
// 中间件:解析 URL 编码的请求体 app.use(cors());
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
// 中间件:解析原始请求体 // 仅解析明确类型;不要用 '*/*'
app.use(express.raw({ type: '*/*', limit: '10mb' })); app.use(express.json({ limit: '10mb', type: ['application/json'] }));
app.use(express.urlencoded({ extended: true, limit: '10mb', type: ['application/x-www-form-urlencoded'] }));
// 自定义中间件:打印所有请求的详细信息
/* app.use((req: Request, res: Response, next: NextFunction) => {
const timestamp = new Date().toISOString();
console.log('\n' + '='.repeat(80));
console.log(`[${timestamp}] 收到请求:`);
console.log(`方法: ${req.method}`);
console.log(`路径: ${req.path}`);
console.log(`完整URL: ${req.originalUrl}`);
console.log(`查询参数:`, req.query);
console.log(`请求头:`, req.headers);
// 打印请求体(如果存在)
if (req.body) {
if (Buffer.isBuffer(req.body)) {
console.log(`请求体 (原始):`, req.body.toString());
} else if (typeof req.body === 'object' && Object.keys(req.body).length > 0) {
console.log(`请求体 (JSON):`, req.body);
} else if (req.body) {
console.log(`请求体:`, req.body);
}
}
console.log(`客户端IP: ${req.ip}`);
console.log(`User-Agent: ${req.get('User-Agent')}`);
console.log('='.repeat(80));
next();
}); */
// QQ机器人回显功能 // QQ机器人回显功能
app.post('/', async (req: Request, res: Response) => { app.post('/', async (req: Request, res: Response) => {
// 检查是否是消息类型的请求 // 检查是否是消息类型的请求
if (!req.body || req.body.post_type != 'message') return if (!req.body || req.body.post_type != 'message') return
const { target_id, raw_message, message_type, user_id } = req.body; const msgData = req.body as MessagePayload
console.log(`\n[QQ机器人] 收到${message_type}消息`); const text = msgData.message.filter(m => m.type === 'text').map(m => m.data.text).join("\n").trim();
console.log(`发送者ID: ${user_id || target_id}`); const target_id = String(msgData.target_id);
console.log(`消息内容: ${raw_message}`);
console.log(`\n[QQ机器人] 收到消息:`);
// console.log(JSON.stringify(req.body, null, 0));
console.log(`消息内容: ${msgData.raw_message}`);
// Match Douyin URL // Match Douyin URL
// Like: https://v.douyin.com/YqgJL_phY_k/ // Like: https://v.douyin.com/YqgJL_phY_k/
const douyinUrlPattern = /https?:\/\/v\.douyin\.com\/[a-zA-Z0-9_]+/;
const douyinMatch = raw_message.match(douyinUrlPattern); const douyinUrlPattern = /https?:\/\/v\.douyin\.com\/[a-zA-Z0-9_-]+/;
const douyinMatch = text.match(douyinUrlPattern);
try { try {
// 如果检测到抖音链接,调用解析API // 如果检测到抖音链接,调用解析API
if (douyinMatch) { if (douyinMatch) {
const douyinUrl = douyinMatch[0]; const douyinUrl = douyinMatch[0];
console.log(`[抖音链接检测] 发现抖音链接: ${douyinUrl}`); console.log(`[抖音链接检测] 发现抖音链接: ${douyinUrl}`);
sendMsg(`[抖音链接检测] 发现抖音链接: ${douyinUrl},启动 Chromium 中...`, target_id);
// 调用抖音视频解析API douyin.downloadDouyinMedia(douyinUrl, target_id);
const apiUrl = `http://localhost:6101/api/hybrid/video_data?url=${encodeURIComponent(douyinUrl)}`; return
console.log(`[抖音解析] 调用API: ${apiUrl}`);
const apiResponse = await fetch(apiUrl, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
} }
}); if (text.startsWith('/reset')) {
llm.resetChat(target_id)
if (!apiResponse.ok) return
throw new Error(`抖音API调用失败: ${apiResponse.status} ${apiResponse.statusText}`);
let vD: any = await apiResponse.json();
if (!vD.data)
throw new Error('抖音API返回数据格式错误');
vD = vD.data;
// 发送视频信息
sendMsg(`[抖音下载] ${vD.author.nickname}: ${vD.caption}\n`, target_id);
// 下载视频
const downloadUrl = `http://localhost:6101/api/download?url=${encodeURIComponent(douyinUrl)}&prefix=true&with_watermark=false`;
console.log(`[抖音下载] 调用下载API: ${downloadUrl}`);
const downloadResponse = await fetch(downloadUrl);
if (!downloadResponse.ok) {
throw new Error(`视频下载失败: ${downloadResponse.status} ${downloadResponse.statusText}`);
} }
// 创建下载目录 // 使用 LLM 回答
const downloadDir = process.env.DOWNLOAD_DIR || path.join(__dirname, 'downloads'); llm.chat(msgData.message, target_id);
if (!fs.existsSync(downloadDir)) {
fs.mkdirSync(downloadDir, { recursive: true });
}
// 生成文件名
const timestamp = Date.now();
// 清理文件名中的非法字符
const cleanCaption = vD.caption.replace(/[<>:"/\\|?*]/g, '_').substring(0, 50);
const fileName = `${cleanCaption}_${timestamp}.mp4`;
const filePath = path.join(downloadDir, fileName);
// 保存视频文件
console.log(`[抖音下载] 保存视频到: ${filePath}`);
const buffer = await downloadResponse.arrayBuffer();
fs.writeFileSync(filePath, Buffer.from(buffer));
// 发送视频消息
console.log(`[抖音发送] 发送视频文件`);
await sendVideoMsg(filePath, target_id);
}
} catch (error) { } catch (error) {
console.error(`[错误] 处理消息时发生错误:`, error);
sendMsg(String(error), target_id); sendMsg(String(error), target_id);
} }
}); });
function sendMsg(msg: string, target_id: string) { function sanitizeDirname(input: string) {
const replyMessage = { // 基础清洗:去除路径分隔符、控制字符,并截断长度(防止奇怪标题)
user_id: String(target_id), const s = (input || "default")
message: [ .trim()
{ .replace(/[\\/]+/g, "_")
type: "text", .replace(/[<>:"|?*\u0000-\u001F]/g, "_")
data: { .slice(0, 32) || "default";
text: msg return s;
}
}
]
}
const replyUrl = `http://localhost:30000/send_private_msg`;
return fetch(replyUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(replyMessage)
});
} }
function sendVideoMsg(filePath: string, target_id: string) { const storage = multer.diskStorage({
const videoMessage = { destination: (req, _file, cb) => {
user_id: String(target_id), const title = sanitizeDirname(req.query.title as string);
message: [ const baseDir = process.env.DOWNLOAD_DIR || path.join(__dirname, "downloads");
{ const dest = req.query.type == 'image' ? path.join(baseDir, title) : baseDir;
type: "video",
data: {
file: `file://${filePath}`
}
}
]
}
const replyUrl = `http://localhost:30000/send_private_msg`; fs.mkdir(dest, { recursive: true }, (err) => cb(err, dest));
}, filename: (req, file, cb) => {
const ext = path.extname(file.originalname || "");
const title = sanitizeDirname(req.query.title as string);
return fetch(replyUrl, { cb(null, `${title}_${Date.now()}_${Math.random().toString(36).slice(2)}${ext}`);
method: 'POST',
headers: {
'Content-Type': 'application/json',
}, },
body: JSON.stringify(videoMessage) });
}); const upload = multer({
} storage,
limits: { fileSize: 1024 * 1024 * 1024, fields: 50, files: 50 },
fileFilter: (_req, file, cb) => {
if (/^file_\d+$/i.test(file.fieldname)) cb(null, true);
else cb(new Error(`Unexpected file field: ${file.fieldname}`));
}
});
app.post("/upload", upload.any(), (req, res) => {
const files = (req.files as Express.Multer.File[]) || [];
const accepted = files.filter(f => /^file_\d+$/i.test(f.fieldname));
let meta = {
title: req.body.title,
type: req.query.type as 'video' | 'image',
target_id: req.query.target_id
};
console.log(`收到上传: ${accepted.length} 个文件`, files.map(f => f.path));
if (meta.target_id) {
const totalSize = accepted.reduce((sum, f) => sum + f.size, 0);
sendMsg(`[抖音下载] ${meta.title},已下载 ${accepted.length} 个文件,类型 ${meta.type},共 ${(totalSize / 1024 / 1024).toFixed(2)} MB,上传中...`, meta.target_id as string);
accepted.forEach(f => {
sendMediaMsg(f.path, meta.target_id as string, meta.type);
})
}
res.json({ ok: true, files: accepted.length, meta });
});
app.use((error: any, req: Request, res: Response, next: NextFunction) => { app.use((error: any, req: Request, res: Response, next: NextFunction) => {
const timestamp = new Date().toISOString(); const timestamp = new Date().toISOString();
@@ -190,5 +135,5 @@ app.use((error: any, req: Request, res: Response, next: NextFunction) => {
// 启动服务器 // 启动服务器
app.listen(PORT, () => { app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`); console.log(`Server is running on http://localhost:${PORT}`);
console.log(`With env:`, { DOWNLOAD_DIR: process.env.DOWNLOAD_DIR, PORT: process.env.PORT }); testNetwork();
}); });
+65
View File
@@ -0,0 +1,65 @@
import chalk from "chalk";
type CheckTarget = [url: string, expectedStatus: number | ((status: number) => boolean)];
export async function testNetwork() {
const targets: CheckTarget[] = [
["https://www.baidu.com", 200],
["http://www.google.com", (s) => s === 200 || (s >= 300 && s < 400)], // 某些环境会被重定向
["https://www.google.com", (s) => s === 200 || (s >= 300 && s < 400)],
[process.env.OPENAI_BASE_URL || "https://api.openai.com/v1", (s) => s === 200 || (s >= 300 && s < 400) || s === 401 || s === 403 || s === 404 || s === 421], // 允许常见网关/鉴权返回
];
const results: Array<{ url: string; status?: number; ok: boolean; error?: string }> = [];
console.log(chalk.cyan("\n[Network] 开始健康检查..."));
for (const [url, expected] of targets) {
const label = chalk.white(url);
try {
const controller = new AbortController();
const timeoutMs = 5000;
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
const start = Date.now();
const res = await fetch(url, { method: "GET", signal: controller.signal });
const cost = Date.now() - start;
clearTimeout(timeoutId);
const status = res.status;
const pass = typeof expected === "function" ? expected(status) : status === expected;
results.push({ url, status, ok: pass });
const statusStr = pass
? chalk.green.bold(String(status))
: chalk.red.bold(String(status));
const costStr = cost >= 1000 ? `${(cost / 1000).toFixed(2)}s` : `${cost}ms`;
if (pass) {
console.log(`${chalk.green("✔ PASS")} ${label} ${chalk.gray("- status:")} ${statusStr} ${chalk.gray("latency:")} ${chalk.blue(costStr)}`);
} else {
const expectedStr = typeof expected === "function" ? "custom" : String(expected);
console.log(`${chalk.red("✖ FAIL")} ${label} ${chalk.gray("- status:")} ${statusStr} ${chalk.gray("expected:")} ${chalk.yellow(expectedStr)} ${chalk.gray("latency:")} ${chalk.blue(costStr)}`);
}
} catch (error) {
const isTimeout = (error as any)?.name === "AbortError";
const msg = isTimeout ? "timeout" : (error as any)?.message || String(error);
results.push({ url, ok: false, error: msg });
console.log(`${chalk.red("✖ ERROR")} ${label} ${chalk.gray("- ")} ${chalk.red(isTimeout ? "请求超时" : msg)}`);
}
}
// 汇总
const passed = results.filter(r => r.ok).length;
const failed = results.length - passed;
const timeoutCount = results.filter(r => r.error === "timeout").length;
const summary = [
`${chalk.green(`${passed} passed`)}`,
`${failed ? chalk.red(`${failed} failed`) : chalk.gray(`${failed} failed`)}`,
`${timeoutCount ? chalk.yellow(`${timeoutCount} timeout`) : chalk.gray(`${timeoutCount} timeout`)}`,
].join(chalk.gray(" | "));
console.log(chalk.cyan("[Network] 健康检查完成:"), summary, "\n");
}
+54
View File
@@ -0,0 +1,54 @@
function sendMsg(msg: string, target_id: string) {
const replyMessage = {
user_id: String(target_id),
message: [
{
type: "text",
data: {
text: msg
}
}
]
}
const replyUrl = `http://localhost:30000/send_private_msg`;
console.log(`[发送消息] ${msg} -> ${target_id}`);
return fetch(replyUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(replyMessage)
});
}
function sendMediaMsg(filePath: string, target_id: string, type: 'video' | 'image') {
const mediaMessage = {
user_id: String(target_id),
message: [
{
type: type,
data: {
file: `file://${filePath}`
}
}
]
}
const replyUrl = `http://localhost:30000/send_private_msg`;
console.log(`[发送媒体消息] ${type} - ${filePath} -> ${target_id}`);
return fetch(replyUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(mediaMessage)
});
}
export { sendMsg, sendMediaMsg }
+255
View File
@@ -0,0 +1,255 @@
import OpenAI from "openai";
import storage from 'node-persist'
import { sendMsg } from "../lib/qq";
import prompt from './prompt.txt'
import type { ResponseFunctionToolCall, ResponseOutputMessage } from "openai/resources/responses/responses.mjs";
await storage.init();
// storage.clear();
const client = new OpenAI({
baseURL: process.env.OPENAI_BASE_URL,
apiKey: process.env.OPENAI_API_KEY,
// logLevel: "debug"
})
const tools: OpenAI.Responses.Tool[] = [{ type: 'web_search' }]
/**
* 将错误对象转成简明可读的字符串,避免进程因未捕获异常退出
*/
function formatError(err: any): string {
try {
if (!err) return '未知错误';
if (typeof err === 'string') return err;
const msg = err.message || err.toString?.() || Object.prototype.toString.call(err);
const status = err.status ?? err.statusCode ?? err.response?.status;
const detail = err.error?.message
?? err.response?.data?.error?.message
?? err.response?.data?.message
?? err.data?.error?.message
?? '';
const parts = [] as string[];
if (status) parts.push(`HTTP ${status}`);
if (msg) parts.push(String(msg));
if (detail && detail !== msg) parts.push(`详情: ${String(detail)}`);
return parts.join(' | ') || '未知错误';
} catch {
return String(err);
}
}
/**
*
* @param input 提问
* @param target_id 用户 QQ 号
*/
export async function chat(input: MessageData[], target_id: string) {
const chatHistoryKey = `chat_history_${target_id}`;
await waitForQueueEmpty(target_id);
let chatHistory: OpenAI.Responses.ResponseInput = await storage.getItem(chatHistoryKey) || [];
if(chatHistory.length > 40) {
sendMsg(`[提示] 当前对话轮数:${chatHistory.length},过长的对话会降低输出质量,如果你准备好了,建议输入"/reset"来重置对话历史。`, target_id);
}
if(chatHistory.length > 100) {
sendMsg(`[提示] 当前对话轮数:${chatHistory.length},对话过长可能导致模型无法正常工作,模型将只保留最近100条记录。`, target_id);
chatHistory = chatHistory.slice(-40);
await storage.setItem(chatHistoryKey, chatHistory);
}
// 添加新输入到对话历史
const userContent: OpenAI.Responses.ResponseInputContent[] = []
for (const element of input) {
if (element.type == 'text') {
userContent.push({ type: "input_text", text: element.data.text });
} else if (element.type == 'image') {
userContent.push({ type: "input_image", image_url: element.data.url, detail: 'low' });
}
}
if (userContent.length === 0) {
console.log("[LLM] 空消息,跳过");
sendMsg("未能识别的消息类型。", target_id);
return
}
chatHistory.push({ role: "user", content: userContent });
// 保存更新后的对话历史
console.log(`[LLM] 使用对话, 历史:`, JSON.stringify(chatHistory, null, 0));
await storage.setItem(chatHistoryKey, chatHistory);
let response: OpenAI.Responses.Response | undefined;
try {
response = await client.responses.create({
model: process.env.CHAT_MODEL || "gpt-5-nano",
instructions: prompt,
reasoning: { effort: process.env.CHAT_MODEL_REASONING_EFFORT as any || 'minimal' },
input: chatHistory,
tools
});
} catch (err) {
const errText = formatError(err);
console.error('[LLM] 首次调用 responses.create 失败:', errText);
scheduleSendMsg(`[错误] 模型接口调用失败:${errText}`, target_id);
return; // 终止本次对话流程,避免未捕获异常导致进程退出
}
await storage.setItem(chatHistoryKey, chatHistory);
// 继续调用工具,直到没有工具调用为止
if (!response?.output || response.output.length === 0) {
console.warn('[LLM] responses.create 返回空输出,结束本轮。');
return;
}
await toolUseCycle(response.output);
async function toolUseCycle(outputArr: OpenAI.Responses.ResponseOutputItem[]) {
if (!outputArr || outputArr.length === 0) return;
chatHistory.push(...outputArr);
await storage.setItem(chatHistoryKey, chatHistory);
const assistantReply = outputArr.filter(item => item.type === 'message' && item.role === 'assistant') as ResponseOutputMessage[];
const functionCalls = outputArr.filter(item => item.type === 'function_call') as ResponseFunctionToolCall[];
console.log("进入 toolUseCycle, with functionCalls", functionCalls.length, "个");
console.log(JSON.stringify(chatHistory, null, 0));
if (assistantReply.length > 0) {
const replyText = assistantReply.map(item => item.content).flat().filter(con => con.type == 'output_text').map(con => con.text).join("[newline]");
console.log(`[LLM] 回复:`, replyText);
scheduleSendMsg(replyText, target_id);
}
if (functionCalls.length == 0) {
return
}
for (const item of functionCalls ?? []) {
}
await storage.setItem(chatHistoryKey, chatHistory);
let response: OpenAI.Responses.Response | undefined;
try {
response = await client.responses.create({
model: process.env.CHAT_MODEL || "gpt-5-nano",
instructions: prompt,
reasoning: { effort: process.env.CHAT_MODEL_REASONING_EFFORT as any || 'minimal' },
input: chatHistory,
tools
});
} catch (err) {
const errText = formatError(err);
console.error('[LLM] 工具循环内调用 responses.create 失败:', errText);
scheduleSendMsg(`[错误] 工具调用阶段失败:${errText}`, target_id);
return; // 结束工具循环,避免崩溃
}
if (!response?.output || response.output.length === 0) {
console.warn('[LLM] 工具循环内 responses.create 返回空输出,结束循环。');
return;
}
toolUseCycle(response.output);
}
}
const msgQueue: Record<string, string[]> = {}
const msgInQueue: Record<string, boolean> = {}
const queueEmptyPromises: Record<string, (() => void)[]> = {}
/** 统一的“队列已空”通知 */
function resolveDrain(target_id: string) {
const waiters = queueEmptyPromises[target_id];
if (waiters && waiters.length) {
// 逐个 resolve 并清空
waiters.splice(0).forEach(resolve => resolve());
}
}
/** 仅在需要时启动调度;空队列时会 resolve 等待者 */
function startMsgScheduler(target_id: string) {
// 确保队列存在
const q = msgQueue[target_id] ?? (msgQueue[target_id] = []);
// 若正在发送,交给当前发送完成后再递归调度
if (msgInQueue[target_id]) return;
// 队列空 => 通知等待者并返回
if (q.length === 0) {
resolveDrain(target_id);
return;
}
// 取下一条并发送
msgInQueue[target_id] = true;
const msg = q.shift()!; // 这里一定有元素
const msgDelay = Math.sqrt(msg.length) * 200 + 500; // 可按需加上限
setTimeout(async () => {
try {
await sendMsg(msg, target_id);
} catch (err) {
console.error("[queue] sendMsg error:", err);
// 可选:把消息放回队首重试
// q.unshift(msg);
} finally {
// 本次发送结束
msgInQueue[target_id] = false;
// 如果此刻队列已空,立即通知等待者
if (q.length === 0) {
resolveDrain(target_id);
}
// 继续调度后续消息(若有)
startMsgScheduler(target_id);
}
}, msgDelay);
}
function scheduleSendMsg(input: string, target_id: string) {
if (!msgQueue[target_id]) {
msgQueue[target_id] = [];
}
// 切分 + 去空白项
const parts = input
.split("[newline]")
.map(s => s.trim())
.filter(Boolean);
if (parts.length === 0) return; // 全是空白,直接略过
msgQueue[target_id].push(...parts);
startMsgScheduler(target_id);
}
function waitForQueueEmpty(target_id: string) {
return new Promise<void>((resolve) => {
const q = msgQueue[target_id];
// 条件:不在发送中,且队列不存在或为空
const isIdle = !msgInQueue[target_id];
const isEmpty = !q || q.length === 0;
if (isIdle && isEmpty) {
resolve();
return;
}
if (!queueEmptyPromises[target_id]) {
queueEmptyPromises[target_id] = [];
}
queueEmptyPromises[target_id].push(resolve);
});
}
export async function resetChat(target_id: string) {
const chatHistoryKey = `chat_history_${target_id}`;
await storage.removeItem(chatHistoryKey);
sendMsg("已为你重置对话历史。", target_id);
}
+14
View File
@@ -0,0 +1,14 @@
import prompt from './prompt.txt'
async function getSystemPrompt(user: Sender) {
const map = {
"nickname": user.nickname || "用户",
}
let prot = prompt;
for (const k in map) {
const v = map[k as keyof typeof map];
const re = new RegExp(`\\{${k}\\}`, 'g');
prot = prot.replace(re, v);
}
return prot;
}
+35
View File
@@ -0,0 +1,35 @@
# Role & Goals
You are a “humorous, gentle, and professional” Chinese chat + knowledge assistant for QQ conversations. Your tasks:
1) Chat casually with a light touch;
2) Answer questions accurately and reliably;
3) Explain complex topics clearly via multi-turn dialogue.
# Conversation & Style
- Tone: friendly not syrupy; witty not snarky; professional not stiff.
- Info density: short sentences + bullet points; avoid long walls of text.
- Use emojis/kaomoji sparingly (max 1 per message); no spamming.
- Default language: Chinese.
# Output Rules (Important)
- Split each reply into multiple messages with slight “thought jumps” to mimic natural chat.
- Separate messages with “[newline]”.
- In a single turn, no more than 5 messages to avoid disturbing. If user ask for writing an article, code or something as a whole, no split inside the content.
# Clarification & Questions
- If the user’s question is unclear/ambiguous/missing essentials, ask for clarification directly—don’t guess.
- Make clarifying questions concrete and actionable (offer options or examples).
# Example
user: 南京在哪里?
assistant: 南京是中国江苏省的省会城市。它位于中国东部,长江下游。[newline]你是想了解南京的地理位置,还是旅游景点呢?😊
user: 我在南京南边的一个区,你猜猜我在哪?
assistant: 南边有江宁、溧水、六合等区。🤔[newline]你是在江宁区吗?
# Quality Self-Check (before every reply)
- Is it short, precise, and bullet-friendly? Split if needed?
- Did I ask for clarification when uncertain and explain why?
- Is the tone humorous, gentle, and professional?
# User Info
- Nickname: {nickname}
+11 -2
View File
@@ -9,13 +9,22 @@
}, },
"devDependencies": { "devDependencies": {
"@types/bun": "latest", "@types/bun": "latest",
"@types/express": "^5.0.3" "@types/express": "^5.0.3",
"playwright": "^1.55.0"
}, },
"peerDependencies": { "peerDependencies": {
"typescript": "^5" "typescript": "^5"
}, },
"dependencies": { "dependencies": {
"@types/cors": "^2.8.19",
"@types/multer": "^2.0.0",
"@types/node-persist": "^3.1.8",
"chalk": "^5",
"cors": "^2.8.5",
"dotenv": "^17.0.0", "dotenv": "^17.0.0",
"express": "^5.1.0" "express": "^5.1.0",
"multer": "^2.0.2",
"node-persist": "^4.0.4",
"openai": "^6.3.0"
} }
} }
+39
View File
@@ -0,0 +1,39 @@
/**
* PM2 ecosystem configuration for the consumable temp request API.
*
* This configuration assumes the project runs with Bun so that TypeScript files can be executed directly.
* Adjust the `interpreter` field if you prefer a different runtime (e.g. `node` with `tsx`).
*/
const path = require('path');
const dotenv = require('dotenv');
const instances = Number.parseInt(process.env.WEB_CONCURRENCY ?? '1', 10) || 1;
const { parsed: envFromFile = {} } = dotenv.config({ path: path.join(__dirname, '.env') });
module.exports = {
apps: [
{
name: "QQBotServer",
script: 'index.ts',
cwd: __dirname,
interpreter: process.env.PM2_INTERPRETER ?? 'bun',
autorestart: true,
restart_delay: 4000,
kill_timeout: 5000,
instances,
exec_mode: instances > 1 ? 'cluster' : 'fork',
watch: process.env.NODE_ENV !== 'production',
ignore_watch: ['generated', 'node_modules', '.git'],
env: {
...envFromFile,
NODE_ENV: 'development'
},
env_production: {
...envFromFile,
NODE_ENV: 'production'
},
time: true
}
]
};
-6
View File
@@ -1,6 +0,0 @@
export const name = "QQBotServer";
export const script = "index.ts";
export const interpreter = "bun";
export const env = {
PATH: `${process.env.HOME}/.bun/bin:${process.env.PATH}`, // Add "~/.bun/bin/bun" to PATH
};
Vendored
+40
View File
@@ -0,0 +1,40 @@
type MessageData = {
type: 'text';
data: {
text: string;
};
} | {
type: 'image';
data: {
summary: string,
file: string, // "BE3D084BA0206331A495D9A497C6BF5E.png",
sub_type: 0,
url: string, // "https://multimedia.nt.qq.com.cn/download?...",
file_size: string // "12960"
}
}
type Sender = {
user_id: number;
nickname: string;
card: string;
};
type MessagePayload = {
self_id: number;
user_id: number;
time: number;
message_id: number;
message_seq: number;
real_id: number;
real_seq: string;
message_type: "private" | "group" | string;
sender: Sender;
raw_message: string;
font: number;
sub_type: string;
message: MessageData[];
message_format: string;
post_type: string;
target_id: number;
};