Compare commits
6 Commits
bd6b6be7ea
...
a123c7cc40
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a123c7cc40 | ||
|
|
da337158b0 | ||
|
|
f62d7ff687 | ||
|
|
10028a0e2e | ||
|
|
af927dd9b4 | ||
|
|
5ed65090b9 |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"savedAt": "2026-02-14T07:46:12.968Z",
|
"savedAt": "2026-02-15T07:13:12.135Z",
|
||||||
"users": [
|
"users": [
|
||||||
{
|
{
|
||||||
"id": "admin_1762534368537",
|
"id": "admin_1762534368537",
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
"passwordHash": "$2b$10$3c/70RbBH4y7zhYwxk8ldOcls3Bj6kt3cSMidTeaMUVb1EJXH4GMy",
|
"passwordHash": "$2b$10$3c/70RbBH4y7zhYwxk8ldOcls3Bj6kt3cSMidTeaMUVb1EJXH4GMy",
|
||||||
"role": "superadmin",
|
"role": "superadmin",
|
||||||
"createdAt": "2025-11-07T16:53:46.677Z",
|
"createdAt": "2025-11-07T16:53:46.677Z",
|
||||||
"lastLoginAt": "2026-02-14T07:46:12.968Z"
|
"lastLoginAt": "2026-02-15T07:13:12.135Z"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
BIN
devices.db
BIN
devices.db
Binary file not shown.
232
src/index.ts
232
src/index.ts
@@ -1171,22 +1171,37 @@ class RemoteControlServer {
|
|||||||
|
|
||||||
private setupSocketHandlers(): void {
|
private setupSocketHandlers(): void {
|
||||||
this.io.on('connection', (socket: any) => {
|
this.io.on('connection', (socket: any) => {
|
||||||
this.logger.info(`新连接建立: ${socket.id} (传输: ${socket.conn.transport.name})`)
|
const remoteAddr = socket.handshake?.address || 'unknown'
|
||||||
|
const transport = socket.conn?.transport?.name || 'unknown'
|
||||||
|
const hasAuth = !!socket.handshake?.auth?.token
|
||||||
|
this.logger.info(`[Conn] New connection: ${socket.id} (transport: ${transport}, ip: ${remoteAddr}, hasAuth: ${hasAuth})`)
|
||||||
|
|
||||||
// 🔧 移除强制认证检查 - 让设备端可以正常连接,认证只在web客户端注册时进行
|
// 增强连接监控,帮助诊断误断开问题
|
||||||
// 🔧 增强连接监控,帮助诊断误断开问题
|
|
||||||
socket.conn.on('upgrade', () => {
|
socket.conn.on('upgrade', () => {
|
||||||
this.logger.info(`连接升级: ${socket.id} -> ${socket.conn.transport.name}`)
|
this.logger.info(`[Conn] Transport upgrade: ${socket.id} -> ${socket.conn.transport.name}`)
|
||||||
})
|
})
|
||||||
|
|
||||||
socket.conn.on('upgradeError', (error: any) => {
|
socket.conn.on('upgradeError', (error: any) => {
|
||||||
this.logger.warn(`连接升级失败: ${socket.id}`, error)
|
this.logger.warn(`[Conn] Transport upgrade failed: ${socket.id}`, error)
|
||||||
})
|
})
|
||||||
|
|
||||||
socket.on('disconnecting', (reason: string) => {
|
socket.on('disconnecting', (reason: string) => {
|
||||||
this.logger.warn(`⚠️ 连接即将断开: ${socket.id}, 原因: ${reason}`)
|
this.logger.warn(`[Conn] Disconnecting: ${socket.id}, reason: ${reason}`)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 主动检测未注册的连接,确保设备不会因注册丢失而显示离线
|
||||||
|
const REGISTRATION_CHECK_DELAY_MS = 8000
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!socket.clientType && socket.connected) {
|
||||||
|
this.logger.warn(`[Conn] Socket ${socket.id} connected ${REGISTRATION_CHECK_DELAY_MS}ms ago but not registered, sending ping_for_registration`)
|
||||||
|
socket.emit('ping_for_registration', {
|
||||||
|
requireReregistration: true,
|
||||||
|
reason: 'unregistered_connection_detected',
|
||||||
|
serverTime: new Date().toISOString()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, REGISTRATION_CHECK_DELAY_MS)
|
||||||
|
|
||||||
// 🔧 设备注册 - 使用队列处理
|
// 🔧 设备注册 - 使用队列处理
|
||||||
socket.on('device_register', (data: any) => {
|
socket.on('device_register', (data: any) => {
|
||||||
this.queueDeviceRegistration(socket, data)
|
this.queueDeviceRegistration(socket, data)
|
||||||
@@ -1661,7 +1676,9 @@ class RemoteControlServer {
|
|||||||
*/
|
*/
|
||||||
private handleWebClientRegister(socket: any, data: any): void {
|
private handleWebClientRegister(socket: any, data: any): void {
|
||||||
try {
|
try {
|
||||||
// 🔐 Web客户端认证验证:检查认证token
|
this.logger.info(`[WebClient] Register request from ${socket.id} (ip: ${socket.handshake?.address || 'unknown'})`)
|
||||||
|
|
||||||
|
// Web client auth: check token
|
||||||
const token = socket.handshake.auth?.token
|
const token = socket.handshake.auth?.token
|
||||||
if (!token) {
|
if (!token) {
|
||||||
this.logger.warn(`🔐 Web客户端注册缺少认证token: ${socket.id}`)
|
this.logger.warn(`🔐 Web客户端注册缺少认证token: ${socket.id}`)
|
||||||
@@ -1760,26 +1777,23 @@ class RemoteControlServer {
|
|||||||
|
|
||||||
if (socket.clientType === 'device' && socket.deviceId) {
|
if (socket.clientType === 'device' && socket.deviceId) {
|
||||||
const deviceId = socket.deviceId
|
const deviceId = socket.deviceId
|
||||||
this.logger.warn(`🔍 设备Socket断开: ${deviceId} (${socket.id})`)
|
this.logger.warn(`[Disconnect] Device socket disconnected: ${deviceId} (${socket.id})`)
|
||||||
|
|
||||||
// 🔧 优化:短延迟验证断开状态,平衡误判防护和真实断开检测速度
|
// Delay verification to tolerate polling transport jitter and reconnect race
|
||||||
// 因为Socket.IO的disconnect事件可能因为网络抖动等原因被误触发,但真正断开应该快速处理
|
// Polling transport may briefly drop the socket during upgrade or network hiccup
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.verifyDeviceDisconnection(deviceId, socket.id)
|
this.verifyDeviceDisconnection(deviceId, socket.id)
|
||||||
}, 1500) // 1.5秒后验证,更快响应真实断开
|
}, 5000) // 5s grace period for reconnect
|
||||||
|
|
||||||
} else if (socket.clientType === 'web' && socket.clientId) {
|
} else if (socket.clientType === 'web' && socket.clientId) {
|
||||||
// 🔧 优化Web客户端断开处理
|
|
||||||
const clientId = socket.clientId
|
const clientId = socket.clientId
|
||||||
const client = this.webClientManager.getClient(clientId)
|
const client = this.webClientManager.getClient(clientId)
|
||||||
|
|
||||||
if (client) {
|
if (client) {
|
||||||
// 如果客户端正在控制设备,释放控制权
|
|
||||||
if (client.controllingDeviceId) {
|
if (client.controllingDeviceId) {
|
||||||
this.logger.info(`🔓 Web客户端断开,释放设备控制权: ${client.controllingDeviceId}`)
|
this.logger.info(`[Disconnect] Web client disconnected, releasing device control: ${client.controllingDeviceId}`)
|
||||||
this.webClientManager.releaseDeviceControl(client.controllingDeviceId)
|
this.webClientManager.releaseDeviceControl(client.controllingDeviceId)
|
||||||
|
|
||||||
// 通知设备控制者已离开
|
|
||||||
const deviceSocketId = this.deviceManager.getDeviceSocketId(client.controllingDeviceId)
|
const deviceSocketId = this.deviceManager.getDeviceSocketId(client.controllingDeviceId)
|
||||||
if (deviceSocketId) {
|
if (deviceSocketId) {
|
||||||
const deviceSocket = this.io.sockets.sockets.get(deviceSocketId)
|
const deviceSocket = this.io.sockets.sockets.get(deviceSocketId)
|
||||||
@@ -1790,19 +1804,15 @@ class RemoteControlServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.webClientManager.removeClient(clientId)
|
this.webClientManager.removeClient(clientId)
|
||||||
this.logger.info(`Web客户端断开连接: ${clientId} (IP: ${client.ip})`)
|
this.logger.info(`[Disconnect] Web client removed: ${clientId} (IP: ${client.ip})`)
|
||||||
} else {
|
} else {
|
||||||
// 通过Socket ID移除客户端
|
|
||||||
this.webClientManager.removeClientBySocketId(socket.id)
|
this.webClientManager.removeClientBySocketId(socket.id)
|
||||||
this.logger.info(`Web客户端断开连接 (通过Socket ID): ${socket.id}`)
|
this.logger.info(`[Disconnect] Web client removed by socketId: ${socket.id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.info(`📊 当前Web客户端数量: ${this.webClientManager.getClientCount()}`)
|
this.logger.info(`[Disconnect] Web client count: ${this.webClientManager.getClientCount()}`)
|
||||||
} else {
|
} else {
|
||||||
// 🔧 处理未识别的连接类型
|
this.logger.warn(`[Disconnect] Unknown client type: ${socket.id} (type: ${socket.clientType})`)
|
||||||
this.logger.warn(`⚠️ 未识别的连接断开: ${socket.id} (类型: ${socket.clientType})`)
|
|
||||||
|
|
||||||
// 尝试清理可能存在的记录
|
|
||||||
this.webClientManager.removeClientBySocketId(socket.id)
|
this.webClientManager.removeClientBySocketId(socket.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1989,57 +1999,45 @@ class RemoteControlServer {
|
|||||||
private checkAndFixInconsistentStates(): void {
|
private checkAndFixInconsistentStates(): void {
|
||||||
try {
|
try {
|
||||||
const memoryDevices = this.deviceManager.getAllDevices()
|
const memoryDevices = this.deviceManager.getAllDevices()
|
||||||
let fixedCount = 0
|
|
||||||
const currentTime = Date.now()
|
const currentTime = Date.now()
|
||||||
|
|
||||||
this.logger.debug(`🔍 开始状态一致性检查,检查 ${memoryDevices.length} 个设备`)
|
this.logger.debug(`[Consistency] Checking ${memoryDevices.length} devices`)
|
||||||
|
|
||||||
for (const device of memoryDevices) {
|
for (const device of memoryDevices) {
|
||||||
const socket = this.io.sockets.sockets.get(device.socketId)
|
const socket = this.io.sockets.sockets.get(device.socketId)
|
||||||
|
|
||||||
// 🔧 修复:增加多重验证条件,避免误判
|
|
||||||
const socketExists = !!socket
|
const socketExists = !!socket
|
||||||
const socketConnected = socket?.connected || false
|
const socketConnected = socket?.connected || false
|
||||||
const timeSinceLastSeen = currentTime - device.lastSeen.getTime()
|
const timeSinceLastSeen = currentTime - device.lastSeen.getTime()
|
||||||
const isRecentlyActive = timeSinceLastSeen < 180000 // 3分钟内有活动
|
const timeSinceConnected = currentTime - device.connectedAt.getTime()
|
||||||
|
|
||||||
this.logger.debug(`📊 设备 ${device.id} 状态检查: socket存在=${socketExists}, 连接=${socketConnected}, 最后活跃=${Math.round(timeSinceLastSeen / 1000)}秒前`)
|
this.logger.debug(`[Consistency] Device ${device.id}: socket=${socketExists}, connected=${socketConnected}, lastSeen=${Math.round(timeSinceLastSeen / 1000)}s ago`)
|
||||||
|
|
||||||
|
// Only remove if:
|
||||||
|
// 1. Socket completely gone (not just disconnected)
|
||||||
|
// 2. No activity for 3 minutes
|
||||||
|
// 3. Connected for more than 2 minutes (avoid race with fresh connections)
|
||||||
|
const INACTIVE_THRESHOLD_MS = 180000 // 3 minutes
|
||||||
|
const MIN_CONNECTION_AGE_MS = 120000 // 2 minutes
|
||||||
|
|
||||||
// 🔧 平衡的断开判断逻辑:快速检测真实断开,避免心跳期间误判
|
|
||||||
// 1. Socket必须完全不存在(不检查connected状态,因为心跳期间可能瞬时为false)
|
|
||||||
// 2. 且设备超过2分钟无活动(适中的容错时间,足够检测真实断开)
|
|
||||||
// 3. 且不是刚连接的设备(避免恢复期间的竞态条件)
|
|
||||||
const shouldRemove = !socketExists &&
|
const shouldRemove = !socketExists &&
|
||||||
timeSinceLastSeen > 120000 && // 2分钟无活动才考虑断开
|
timeSinceLastSeen > INACTIVE_THRESHOLD_MS &&
|
||||||
(currentTime - device.connectedAt.getTime()) > 60000 // 连接超过1分钟才检查
|
timeSinceConnected > MIN_CONNECTION_AGE_MS
|
||||||
|
|
||||||
if (shouldRemove) {
|
if (shouldRemove) {
|
||||||
this.logger.warn(`⚠️ 确认设备真正断开: ${device.id} (${device.name})`)
|
this.logger.warn(`[Consistency] Device ${device.id} (${device.name}) confirmed disconnected`)
|
||||||
this.logger.warn(` - Socket存在: ${socketExists}, 连接: ${socketConnected}`)
|
this.logger.warn(` socketExists=${socketExists}, lastSeen=${Math.round(timeSinceLastSeen / 1000)}s, connAge=${Math.round(timeSinceConnected / 1000)}s`)
|
||||||
this.logger.warn(` - 最后活跃: ${Math.round(timeSinceLastSeen / 1000)}秒前`)
|
|
||||||
this.logger.warn(` - 连接时长: ${Math.round((currentTime - device.connectedAt.getTime()) / 1000)}秒`)
|
|
||||||
|
|
||||||
// 🔧 优化:适中的二次确认延迟,快速清理真正断开的设备
|
// Secondary check after 5s delay
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.performSecondaryDeviceCheck(device.id, device.socketId)
|
this.performSecondaryDeviceCheck(device.id, device.socketId)
|
||||||
}, 3000) // 3秒后二次确认
|
}, 5000)
|
||||||
|
} else if (!socketExists || !socketConnected) {
|
||||||
} else {
|
this.logger.debug(`[Consistency] Device ${device.id} socket abnormal but within tolerance (lastSeen: ${Math.round(timeSinceLastSeen / 1000)}s ago)`)
|
||||||
// 设备状态正常或在容错范围内
|
|
||||||
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) {
|
} catch (error) {
|
||||||
this.logger.error('状态一致性检查失败:', error)
|
this.logger.error('[Consistency] Check failed:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2056,13 +2054,13 @@ class RemoteControlServer {
|
|||||||
try {
|
try {
|
||||||
const device = this.deviceManager.getDevice(deviceId)
|
const device = this.deviceManager.getDevice(deviceId)
|
||||||
if (!device) {
|
if (!device) {
|
||||||
this.logger.debug(`📋 验证断开时设备 ${deviceId} 已不在内存中,可能已被其他逻辑清理`)
|
this.logger.debug(`[Verify] Device ${deviceId} already removed from memory, skip`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查设备是否已经重新连接(新的Socket ID)
|
// Device reconnected with a new socket - skip cleanup
|
||||||
if (device.socketId !== socketId) {
|
if (device.socketId !== socketId) {
|
||||||
this.logger.info(`✅ 设备 ${deviceId} 已重新连接,新Socket: ${device.socketId},跳过断开处理`)
|
this.logger.info(`[Verify] Device ${deviceId} reconnected with new socket: ${device.socketId}, skip`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2070,32 +2068,36 @@ class RemoteControlServer {
|
|||||||
const currentTime = Date.now()
|
const currentTime = Date.now()
|
||||||
const timeSinceLastSeen = currentTime - device.lastSeen.getTime()
|
const timeSinceLastSeen = currentTime - device.lastSeen.getTime()
|
||||||
|
|
||||||
// 🔧 优化:区分不同断开场景的检查条件
|
|
||||||
const socketExists = !!socket
|
const socketExists = !!socket
|
||||||
const socketConnected = socket?.connected || false
|
const socketConnected = socket?.connected || false
|
||||||
const hasRecentActivity = timeSinceLastSeen < 5000 // 5秒内有活动
|
const hasRecentActivity = timeSinceLastSeen < 10000 // 10s recent activity window
|
||||||
|
|
||||||
this.logger.info(`🔍 验证设备 ${deviceId} 断开状态:`)
|
this.logger.info(`[Verify] Device ${deviceId}: socketExists=${socketExists}, connected=${socketConnected}, lastSeen=${Math.round(timeSinceLastSeen / 1000)}s ago`)
|
||||||
this.logger.info(` - Socket存在: ${socketExists}, 连接: ${socketConnected}`)
|
|
||||||
this.logger.info(` - 最后活跃: ${Math.round(timeSinceLastSeen / 1000)}秒前`)
|
|
||||||
this.logger.info(` - 近期活跃: ${hasRecentActivity}`)
|
|
||||||
|
|
||||||
// 🔧 关键优化:如果Socket不存在,很可能是真正的断开
|
// Socket completely gone - confirm real disconnect
|
||||||
if (!socketExists) {
|
if (!socketExists) {
|
||||||
this.logger.warn(`❌ Socket完全不存在,确认设备真实断开: ${deviceId}`)
|
// Double check: if device had recent activity, give more time
|
||||||
|
if (hasRecentActivity) {
|
||||||
|
this.logger.info(`[Verify] Socket gone but device ${deviceId} had recent activity, defer cleanup by 10s`)
|
||||||
|
setTimeout(() => {
|
||||||
|
this.performSecondaryDeviceCheck(deviceId, socketId)
|
||||||
|
}, 10000)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.logger.warn(`[Verify] Socket gone and no recent activity, cleanup device: ${deviceId}`)
|
||||||
this.executeDeviceCleanup(deviceId, device)
|
this.executeDeviceCleanup(deviceId, device)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔧 如果Socket存在但未连接,且无近期活动,尝试主动测试连接
|
// Socket exists but not connected, and no recent activity - test connection
|
||||||
if (!socketConnected && !hasRecentActivity) {
|
if (!socketConnected && !hasRecentActivity) {
|
||||||
this.logger.warn(`🔍 Socket存在但未连接,主动测试设备连接: ${deviceId}`)
|
this.logger.warn(`[Verify] Socket exists but not connected, testing device: ${deviceId}`)
|
||||||
this.testDeviceConnection(deviceId, socketId, device)
|
this.testDeviceConnection(deviceId, socketId, device)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设备状态正常,确保Web端知道设备在线
|
// Device still alive - broadcast online status
|
||||||
this.logger.info(`✅ 验证结果:设备 ${deviceId} 仍然在线,disconnect事件是误报`)
|
this.logger.info(`[Verify] Device ${deviceId} still online, disconnect was false alarm`)
|
||||||
this.webClientManager.broadcastToAll('device_status_update', {
|
this.webClientManager.broadcastToAll('device_status_update', {
|
||||||
deviceId: device.id,
|
deviceId: device.id,
|
||||||
status: {
|
status: {
|
||||||
@@ -2107,7 +2109,7 @@ class RemoteControlServer {
|
|||||||
})
|
})
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`验证设备断开失败 (${deviceId}):`, error)
|
this.logger.error(`[Verify] Failed for device ${deviceId}:`, error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2117,39 +2119,35 @@ class RemoteControlServer {
|
|||||||
private testDeviceConnection(deviceId: string, socketId: string, device: any): void {
|
private testDeviceConnection(deviceId: string, socketId: string, device: any): void {
|
||||||
const socket = this.io.sockets.sockets.get(socketId)
|
const socket = this.io.sockets.sockets.get(socketId)
|
||||||
if (!socket) {
|
if (!socket) {
|
||||||
this.logger.warn(`❌ 测试连接时Socket已不存在: ${deviceId}`)
|
this.logger.warn(`[ConnTest] Socket gone for device ${deviceId}`)
|
||||||
this.executeDeviceCleanup(deviceId, device)
|
this.executeDeviceCleanup(deviceId, device)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.info(`📡 向设备 ${deviceId} 发送连接测试`)
|
this.logger.info(`[ConnTest] Testing device ${deviceId}`)
|
||||||
|
|
||||||
// 设置响应超时
|
|
||||||
let responded = false
|
let responded = false
|
||||||
|
const CONNECTION_TEST_TIMEOUT_MS = 15000 // 15s timeout for polling transport
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
if (!responded) {
|
if (!responded) {
|
||||||
this.logger.warn(`⏰ 设备 ${deviceId} 连接测试超时,确认断开`)
|
this.logger.warn(`[ConnTest] Device ${deviceId} timed out after ${CONNECTION_TEST_TIMEOUT_MS}ms`)
|
||||||
this.executeDeviceCleanup(deviceId, device)
|
this.executeDeviceCleanup(deviceId, device)
|
||||||
}
|
}
|
||||||
}, 5000) // 5秒超时
|
}, CONNECTION_TEST_TIMEOUT_MS)
|
||||||
|
|
||||||
// 发送测试ping
|
|
||||||
try {
|
try {
|
||||||
socket.emit('CONNECTION_TEST', {
|
socket.emit('CONNECTION_TEST', {
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
testId: `verify_${Date.now()}`
|
testId: `verify_${Date.now()}`
|
||||||
})
|
})
|
||||||
|
|
||||||
// 监听一次性响应
|
|
||||||
const responseHandler = (data: any) => {
|
const responseHandler = (data: any) => {
|
||||||
responded = true
|
responded = true
|
||||||
clearTimeout(timeout)
|
clearTimeout(timeout)
|
||||||
this.logger.info(`✅ 设备 ${deviceId} 连接测试成功,设备仍在线`)
|
this.logger.info(`[ConnTest] Device ${deviceId} responded, still online`)
|
||||||
|
|
||||||
// 更新设备活跃时间
|
|
||||||
device.lastSeen = new Date()
|
device.lastSeen = new Date()
|
||||||
|
|
||||||
// 确保Web端知道设备在线
|
|
||||||
this.webClientManager.broadcastToAll('device_status_update', {
|
this.webClientManager.broadcastToAll('device_status_update', {
|
||||||
deviceId: device.id,
|
deviceId: device.id,
|
||||||
status: {
|
status: {
|
||||||
@@ -2160,7 +2158,6 @@ class RemoteControlServer {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 清理监听器
|
|
||||||
socket.off('CONNECTION_TEST_RESPONSE', responseHandler)
|
socket.off('CONNECTION_TEST_RESPONSE', responseHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2169,7 +2166,7 @@ class RemoteControlServer {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
responded = true
|
responded = true
|
||||||
clearTimeout(timeout)
|
clearTimeout(timeout)
|
||||||
this.logger.error(`❌ 发送连接测试失败: ${deviceId}`, error)
|
this.logger.error(`[ConnTest] Failed to send test to device ${deviceId}:`, error)
|
||||||
this.executeDeviceCleanup(deviceId, device)
|
this.executeDeviceCleanup(deviceId, device)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2178,28 +2175,26 @@ class RemoteControlServer {
|
|||||||
* 🆕 执行设备清理逻辑
|
* 🆕 执行设备清理逻辑
|
||||||
*/
|
*/
|
||||||
private executeDeviceCleanup(deviceId: string, device: any): void {
|
private executeDeviceCleanup(deviceId: string, device: any): void {
|
||||||
this.logger.warn(`🧹 执行设备清理: ${deviceId} (${device.name})`)
|
this.logger.warn(`[Cleanup] Removing device: ${deviceId} (${device.name})`)
|
||||||
|
|
||||||
// 释放控制权
|
// Release control if held
|
||||||
const controllerId = this.webClientManager.getDeviceController(deviceId)
|
const controllerId = this.webClientManager.getDeviceController(deviceId)
|
||||||
if (controllerId) {
|
if (controllerId) {
|
||||||
this.logger.info(`🔓 设备断开,自动释放控制权: ${deviceId} (控制者: ${controllerId})`)
|
this.logger.info(`[Cleanup] Releasing control for device ${deviceId} (controller: ${controllerId})`)
|
||||||
this.webClientManager.releaseDeviceControl(deviceId)
|
this.webClientManager.releaseDeviceControl(deviceId)
|
||||||
|
|
||||||
// 通知控制的Web客户端设备已断开
|
|
||||||
this.webClientManager.sendToClient(controllerId, 'device_control_lost', {
|
this.webClientManager.sendToClient(controllerId, 'device_control_lost', {
|
||||||
deviceId: deviceId,
|
deviceId: deviceId,
|
||||||
reason: 'device_disconnected',
|
reason: 'device_disconnected',
|
||||||
message: '设备已断开连接'
|
message: 'Device disconnected'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清理设备
|
|
||||||
this.deviceManager.removeDevice(deviceId)
|
this.deviceManager.removeDevice(deviceId)
|
||||||
this.databaseService.setDeviceOffline(deviceId)
|
this.databaseService.setDeviceOffline(deviceId)
|
||||||
this.webClientManager.broadcastToAll('device_disconnected', deviceId)
|
this.webClientManager.broadcastToAll('device_disconnected', deviceId)
|
||||||
|
|
||||||
this.logger.info(`✅ 已清理断开的设备: ${device.name} (${deviceId})`)
|
this.logger.info(`[Cleanup] Device removed: ${device.name} (${deviceId})`)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -2209,30 +2204,32 @@ class RemoteControlServer {
|
|||||||
try {
|
try {
|
||||||
const device = this.deviceManager.getDevice(deviceId)
|
const device = this.deviceManager.getDevice(deviceId)
|
||||||
if (!device) {
|
if (!device) {
|
||||||
this.logger.debug(`📋 二次检查时设备 ${deviceId} 已不在内存中,跳过`)
|
this.logger.debug(`[SecondaryCheck] Device ${deviceId} already removed, skip`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const socket = this.io.sockets.sockets.get(socketId)
|
// Device reconnected with new socket
|
||||||
|
if (device.socketId !== socketId) {
|
||||||
|
this.logger.info(`[SecondaryCheck] Device ${deviceId} reconnected with new socket ${device.socketId}, skip`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const socket = this.io.sockets.sockets.get(device.socketId)
|
||||||
const currentTime = Date.now()
|
const currentTime = Date.now()
|
||||||
const timeSinceLastSeen = currentTime - device.lastSeen.getTime()
|
const timeSinceLastSeen = currentTime - device.lastSeen.getTime()
|
||||||
|
|
||||||
// 🔧 优化:二次检查条件更合理,60秒无活动就考虑断开
|
|
||||||
const socketExists = !!socket
|
const socketExists = !!socket
|
||||||
const socketConnected = socket?.connected || false
|
const socketConnected = socket?.connected || false
|
||||||
const isInactive = timeSinceLastSeen > 60000 // 1分钟无活动
|
const INACTIVE_THRESHOLD_MS = 90000 // 90s no activity
|
||||||
|
|
||||||
this.logger.info(`🔍 二次确认设备 ${deviceId} 状态:`)
|
this.logger.info(`[SecondaryCheck] Device ${deviceId}: socket=${socketExists}, connected=${socketConnected}, lastSeen=${Math.round(timeSinceLastSeen / 1000)}s ago`)
|
||||||
this.logger.info(` - Socket存在: ${socketExists}, 连接: ${socketConnected}`)
|
|
||||||
this.logger.info(` - 最后活跃: ${Math.round(timeSinceLastSeen / 1000)}秒前`)
|
|
||||||
|
|
||||||
if (!socketExists || (!socketConnected && isInactive)) {
|
if (!socketExists || (!socketConnected && timeSinceLastSeen > INACTIVE_THRESHOLD_MS)) {
|
||||||
this.logger.warn(`❌ 二次确认:设备 ${deviceId} 确实已断开,执行清理`)
|
this.logger.warn(`[SecondaryCheck] Device ${deviceId} confirmed disconnected, cleanup`)
|
||||||
this.executeDeviceCleanup(deviceId, device)
|
this.executeDeviceCleanup(deviceId, device)
|
||||||
} else {
|
} else {
|
||||||
this.logger.info(`✅ 二次确认:设备 ${deviceId} 状态正常,保持连接`)
|
this.logger.info(`[SecondaryCheck] Device ${deviceId} still alive`)
|
||||||
|
|
||||||
// 设备状态恢复正常,确保Web端知道设备在线
|
|
||||||
if (socketExists && socketConnected) {
|
if (socketExists && socketConnected) {
|
||||||
this.webClientManager.broadcastToAll('device_status_update', {
|
this.webClientManager.broadcastToAll('device_status_update', {
|
||||||
deviceId: device.id,
|
deviceId: device.id,
|
||||||
@@ -2247,7 +2244,7 @@ class RemoteControlServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`二次设备检查失败 (${deviceId}):`, error)
|
this.logger.error(`[SecondaryCheck] Failed for device ${deviceId}:`, error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2258,34 +2255,21 @@ class RemoteControlServer {
|
|||||||
try {
|
try {
|
||||||
const webClientCount = this.webClientManager.getClientCount()
|
const webClientCount = this.webClientManager.getClientCount()
|
||||||
if (webClientCount === 0) {
|
if (webClientCount === 0) {
|
||||||
return // 没有Web客户端,跳过刷新
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const onlineDevices = this.deviceManager.getAllDevices()
|
// 获取完整设备列表(含历史设备)并广播给Web端
|
||||||
|
const allDevices = this.getAllDevicesIncludingHistory()
|
||||||
for (const device of onlineDevices) {
|
if (allDevices.length > 0) {
|
||||||
// 验证设备Socket仍然连接
|
this.webClientManager.broadcastToAll('devices_list_refresh', {
|
||||||
const socket = this.io.sockets.sockets.get(device.socketId)
|
devices: allDevices,
|
||||||
if (socket && socket.connected) {
|
timestamp: Date.now()
|
||||||
// 广播设备在线状态
|
})
|
||||||
this.webClientManager.broadcastToAll('device_status_update', {
|
this.logger.debug(`[Refresh] Sent ${allDevices.length} devices to ${webClientCount} web clients`)
|
||||||
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) {
|
} catch (error) {
|
||||||
this.logger.error('刷新设备状态失败:', error)
|
this.logger.error('refreshDeviceStatusToWebClients failed:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -546,52 +546,21 @@ export class MessageRouter {
|
|||||||
if (dataSize > 0 && dataSize < MIN_VALID_FRAME_SIZE) {
|
if (dataSize > 0 && dataSize < MIN_VALID_FRAME_SIZE) {
|
||||||
this.droppedFrames++
|
this.droppedFrames++
|
||||||
|
|
||||||
// ✅ 追踪连续黑帧数
|
// 追踪连续黑帧数(仅记录日志,不触发模式切换)
|
||||||
const deviceId = screenData.deviceId
|
const deviceId = screenData.deviceId
|
||||||
const count = (this.consecutiveBlackFrames.get(deviceId) || 0) + 1
|
const count = (this.consecutiveBlackFrames.get(deviceId) || 0) + 1
|
||||||
this.consecutiveBlackFrames.set(deviceId, count)
|
this.consecutiveBlackFrames.set(deviceId, count)
|
||||||
|
|
||||||
if (this.routedFrames % 100 === 0) {
|
if (count % 100 === 1) {
|
||||||
this.logger.warn(`⚠️ 过滤黑屏帧: ${dataSize} 字符 < ${MIN_VALID_FRAME_SIZE}, 设备${deviceId}, 连续黑帧${count}, 已丢弃${this.droppedFrames}帧`)
|
this.logger.warn(`过滤黑屏帧: ${dataSize} 字符 < ${MIN_VALID_FRAME_SIZE}, 设备${deviceId}, 连续黑帧${count}, 已丢弃${this.droppedFrames}帧`)
|
||||||
}
|
|
||||||
|
|
||||||
// ✅ 连续50个黑帧后,通知Android端切换到无障碍截图模式
|
|
||||||
if (count >= 50 && !this.captureModeSwitchSent.has(deviceId)) {
|
|
||||||
this.captureModeSwitchSent.add(deviceId)
|
|
||||||
this.logger.warn(`🔄 设备${deviceId}连续${count}个黑帧,发送切换到无障碍截图模式指令`)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const deviceSocketId = this.deviceManager.getDeviceSocketId(deviceId)
|
|
||||||
if (deviceSocketId) {
|
|
||||||
const deviceSocket = this.webClientManager.io?.sockets.sockets.get(deviceSocketId)
|
|
||||||
if (deviceSocket) {
|
|
||||||
deviceSocket.emit('quality_adjust', {
|
|
||||||
captureMode: 'accessibility',
|
|
||||||
fps: 10,
|
|
||||||
quality: 50,
|
|
||||||
maxWidth: 480,
|
|
||||||
maxHeight: 854
|
|
||||||
})
|
|
||||||
this.logger.info(`📤 已向设备${deviceId}发送切换采集模式指令`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
this.logger.error(`❌ 发送切换采集模式指令失败:`, e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ 收到有效帧,重置黑帧计数
|
// 收到有效帧,重置黑帧计数
|
||||||
if (screenData.deviceId) {
|
if (screenData.deviceId) {
|
||||||
const prevCount = this.consecutiveBlackFrames.get(screenData.deviceId) || 0
|
|
||||||
if (prevCount > 0) {
|
|
||||||
this.logger.info(`✅ 设备${screenData.deviceId}收到有效帧(${dataSize}字符),重置黑帧计数(之前${prevCount})`)
|
|
||||||
}
|
|
||||||
this.consecutiveBlackFrames.set(screenData.deviceId, 0)
|
this.consecutiveBlackFrames.set(screenData.deviceId, 0)
|
||||||
// 收到有效帧后允许再次发送切换指令(如果后续又出现黑帧)
|
|
||||||
this.captureModeSwitchSent.delete(screenData.deviceId)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔧 检查设备是否有控制者,没有控制者直接丢弃(提前检查,减少处理开销)
|
// 🔧 检查设备是否有控制者,没有控制者直接丢弃(提前检查,减少处理开销)
|
||||||
@@ -603,14 +572,14 @@ export class MessageRouter {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔧 优化去重逻辑:调整时间间隔判断,避免误杀正常数据
|
// 优化去重逻辑:调整时间间隔判断,避免误杀正常数据
|
||||||
const existingBuffer = this.screenDataBuffer.get(screenData.deviceId)
|
const existingBuffer = this.screenDataBuffer.get(screenData.deviceId)
|
||||||
if (existingBuffer) {
|
if (existingBuffer) {
|
||||||
// 如果有旧数据且时间间隔过短,可能是重复数据,跳过
|
// 如果有旧数据且时间间隔过短,可能是重复数据,跳过
|
||||||
const timeDiff = Date.now() - existingBuffer.timestamp
|
const timeDiff = Date.now() - existingBuffer.timestamp
|
||||||
if (timeDiff < 50) { // 放宽到50ms内的重复数据才去重,避免误杀正常的250ms间隔数据
|
if (timeDiff < 30) { // 30ms内的重复数据才去重,配合50ms发送间隔
|
||||||
this.droppedFrames++
|
this.droppedFrames++
|
||||||
this.logger.debug(`⚠️ 跳过重复屏幕数据: 设备${screenData.deviceId}, 间隔${timeDiff}ms`)
|
this.logger.debug(`[dedup] skip screen data: device=${screenData.deviceId}, interval=${timeDiff}ms`)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1881,6 +1850,9 @@ export class MessageRouter {
|
|||||||
case 'REFRESH_MEDIA_PROJECTION_PERMISSION':
|
case 'REFRESH_MEDIA_PROJECTION_PERMISSION':
|
||||||
return this.handleRefreshMediaProjectionPermission(client.id, eventData.deviceId)
|
return this.handleRefreshMediaProjectionPermission(client.id, eventData.deviceId)
|
||||||
|
|
||||||
|
case 'REFRESH_SCREEN':
|
||||||
|
return this.handleRefreshScreen(client.id, eventData.deviceId)
|
||||||
|
|
||||||
case 'REFRESH_MEDIA_PROJECTION_MANUAL':
|
case 'REFRESH_MEDIA_PROJECTION_MANUAL':
|
||||||
return this.handleRefreshMediaProjectionManual(client.id, eventData.deviceId)
|
return this.handleRefreshMediaProjectionManual(client.id, eventData.deviceId)
|
||||||
|
|
||||||
@@ -4140,7 +4112,54 @@ export class MessageRouter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 🆕 处理手动授权投屏权限请求(不自动点击确认)
|
* Handle screen refresh request from Web client
|
||||||
|
* Sends REFRESH_SCREEN command to device to restart screen capture
|
||||||
|
*/
|
||||||
|
private handleRefreshScreen(clientId: string, deviceId: string): boolean {
|
||||||
|
try {
|
||||||
|
this.logger.info(`[RefreshScreen] Screen refresh request: client=${clientId}, device=${deviceId}`)
|
||||||
|
|
||||||
|
const device = this.deviceManager.getDevice(deviceId)
|
||||||
|
if (!device || !this.deviceManager.isDeviceOnline(deviceId)) {
|
||||||
|
this.logger.warn(`[RefreshScreen] Device offline: ${deviceId}`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.webClientManager.hasDeviceControl(clientId, deviceId)) {
|
||||||
|
this.logger.warn(`[RefreshScreen] Client ${clientId} has no control over ${deviceId}`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const deviceSocketId = this.deviceManager.getDeviceSocketId(deviceId)
|
||||||
|
if (!deviceSocketId) {
|
||||||
|
this.logger.error(`[RefreshScreen] Device socket not found: ${deviceId}`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const deviceSocket = this.webClientManager.io?.sockets.sockets.get(deviceSocketId)
|
||||||
|
if (!deviceSocket) {
|
||||||
|
this.logger.error(`[RefreshScreen] Device socket connection lost: ${deviceId}`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceSocket.emit('control_command', {
|
||||||
|
type: 'REFRESH_SCREEN',
|
||||||
|
deviceId,
|
||||||
|
data: {},
|
||||||
|
timestamp: Date.now()
|
||||||
|
})
|
||||||
|
|
||||||
|
this.logger.info(`[RefreshScreen] Refresh command sent to device: ${deviceId}`)
|
||||||
|
return true
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('[RefreshScreen] Failed to send refresh command:', error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle manual MediaProjection permission request (no auto-click)
|
||||||
*/
|
*/
|
||||||
private handleRefreshMediaProjectionManual(clientId: string, deviceId: string): boolean {
|
private handleRefreshMediaProjectionManual(clientId: string, deviceId: string): boolean {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user