import { io } from 'socket.io-client' import { writeFile } from 'node:fs/promises' const DEVICE_ID = process.env.RC_DEVICE_ID || process.argv[2] const SERVER_HTTP = process.env.RC_SERVER_HTTP || 'http://127.0.0.1:3001' const SOCKET_URL = process.env.RC_SOCKET_URL || SERVER_HTTP const USERNAME = process.env.RC_USER || 'superadmin' const PASSWORD = process.env.RC_PASS || 'superadmin123456789' const RUNS = Number(process.env.RC_APP_LIST_RUNS || 4) const OPEN_BATTERY_SETTINGS = String(process.env.RC_OPEN_BATTERY_SETTINGS || '0') === '1' const REPORT_PATH = process.env.RC_REPORT_PATH || './_tmp_keepalive_app_list_probe.json' if (!DEVICE_ID) { console.error('usage: node scripts/keepalive_app_list_probe.mjs ') process.exit(1) } const nowIso = () => new Date().toISOString() const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) const report = { startedAt: nowIso(), serverHttp: SERVER_HTTP, socketUrl: SOCKET_URL, deviceId: DEVICE_ID, runs: [], keepalive: {}, openBatterySettings: null, events: [], summary: { appListOk: 0, appListFail: 0 } } function trimPreview(value, maxLen = 360) { try { const text = JSON.stringify(value) if (!text) return '' return text.length > maxLen ? `${text.slice(0, maxLen)}...` : text } catch { return String(value) } } function pushEvent(event, payload) { report.events.push({ at: nowIso(), event, payload: trimPreview(payload) }) if (report.events.length > 600) report.events.shift() } function waitEvent(socket, event, predicate, timeoutMs) { return new Promise((resolve, reject) => { const timer = setTimeout(() => { cleanup() reject(new Error(`timeout_waiting_${event}_${timeoutMs}ms`)) }, timeoutMs) const handler = (...args) => { try { if (!predicate(...args)) return cleanup() resolve(args) } catch (error) { cleanup() reject(error) } } const cleanup = () => { clearTimeout(timer) socket.off(event, handler) } socket.on(event, handler) }) } async function loginToken() { const resp = await fetch(`${SERVER_HTTP}/api/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: USERNAME, password: PASSWORD }) }) const data = await resp.json().catch(() => ({})) if (!resp.ok || !data?.success || !data?.token) { throw new Error(`login_failed_${resp.status}_${trimPreview(data, 800)}`) } return data.token } async function run() { const token = await loginToken() const socket = io(SOCKET_URL, { transports: ['websocket', 'polling'], timeout: 12000, reconnection: false, auth: { token } }) socket.onAny((event, ...args) => { if (['app_list_data', 'permission_response', 'device_control_response', 'control_error'].includes(event)) { pushEvent(event, args[0]) } }) const emitClientEvent = (type, data = {}) => { socket.emit('client_event', { type, data: { ...data, deviceId: DEVICE_ID }, timestamp: Date.now() }) } const emitControl = (type, data = {}) => { socket.emit('control_message', { type, deviceId: DEVICE_ID, data, timestamp: Date.now() }) } const emitCamera = (action, data = {}) => { socket.emit('camera_control', { action, deviceId: DEVICE_ID, data }) } let hasControl = false try { await waitEvent(socket, 'connect', () => true, 12000) socket.emit('web_client_register', { userAgent: 'codex-keepalive-app-list-probe' }) await waitEvent(socket, 'client_registered', () => true, 12000) emitClientEvent('REQUEST_DEVICE_CONTROL', { deviceId: DEVICE_ID }) const [controlResp] = await waitEvent( socket, 'device_control_response', (payload) => payload?.deviceId === DEVICE_ID, 12000 ) if (!controlResp?.success) { throw new Error(controlResp?.message || 'request_control_failed') } hasControl = true for (let i = 1; i <= RUNS; i += 1) { const includeIcons = i % 2 === 0 const started = Date.now() try { emitControl('APP_LIST', { includeIcons }) const [payload] = await waitEvent( socket, 'app_list_data', (resp) => resp?.deviceId === DEVICE_ID && Array.isArray(resp?.appList), includeIcons ? 35000 : 18000 ) const count = Array.isArray(payload?.appList) ? payload.appList.length : 0 report.runs.push({ idx: i, includeIcons, ok: true, count, durationMs: Date.now() - started }) report.summary.appListOk += 1 } catch (error) { report.runs.push({ idx: i, includeIcons, ok: false, error: error instanceof Error ? error.message : String(error), durationMs: Date.now() - started }) report.summary.appListFail += 1 } await sleep(includeIcons ? 900 : 450) } const waitBattery = waitEvent( socket, 'permission_response', (payload) => payload?.deviceId === DEVICE_ID && String(payload?.permissionType || '').toLowerCase() === 'battery_optimization', 15000 ) const waitBackground = waitEvent( socket, 'permission_response', (payload) => payload?.deviceId === DEVICE_ID && String(payload?.permissionType || '').toLowerCase() === 'background_start', 15000 ) emitCamera('KEEPALIVE_STATUS_CHECK', {}) const [[batteryResp], [backgroundResp]] = await Promise.all([waitBattery, waitBackground]) report.keepalive = { batteryOptimization: { success: !!batteryResp?.success, message: batteryResp?.message || '' }, backgroundStart: { success: !!backgroundResp?.success, message: backgroundResp?.message || '' } } if (OPEN_BATTERY_SETTINGS) { const waitBatteryAfterOpen = waitEvent( socket, 'permission_response', (payload) => payload?.deviceId === DEVICE_ID && String(payload?.permissionType || '').toLowerCase() === 'battery_optimization', 18000 ) emitCamera('OPEN_BATTERY_OPTIMIZATION_SETTINGS', {}) const [afterOpen] = await waitBatteryAfterOpen report.openBatterySettings = { success: !!afterOpen?.success, message: afterOpen?.message || '' } } } finally { if (hasControl) { emitClientEvent('RELEASE_DEVICE_CONTROL', { deviceId: DEVICE_ID }) await sleep(300) } socket.close() } report.finishedAt = nowIso() await writeFile(REPORT_PATH, JSON.stringify(report, null, 2), 'utf8') console.log(`report=${REPORT_PATH}`) console.log(`app_list_ok=${report.summary.appListOk} app_list_fail=${report.summary.appListFail}`) console.log(`keepalive_battery=${report.keepalive?.batteryOptimization?.success} keepalive_bg=${report.keepalive?.backgroundStart?.success}`) } run().catch(async (error) => { report.finishedAt = nowIso() report.fatal = error instanceof Error ? error.message : String(error) await writeFile(REPORT_PATH, JSON.stringify(report, null, 2), 'utf8') console.error(report.fatal) process.exit(1) })