feat: upload latest web source changes
This commit is contained in:
69
scripts/ADB_CONTROL_MATRIX_README.md
Normal file
69
scripts/ADB_CONTROL_MATRIX_README.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# Android Control Matrix Orchestrator
|
||||
|
||||
`scripts/adb_control_matrix_orchestrator.mjs` is the batch runner for:
|
||||
|
||||
1. Discovering ADB devices (USB + wireless)
|
||||
2. Deduplicating transports for one physical phone
|
||||
3. Installing APK
|
||||
4. Launching app
|
||||
5. Mapping ADB device to server `deviceId`
|
||||
6. Running control-panel smoke test
|
||||
7. Applying ROM-specific recovery actions and retrying
|
||||
|
||||
## Quick start
|
||||
|
||||
Run with defaults:
|
||||
|
||||
```bash
|
||||
npm run autotest:android-control
|
||||
```
|
||||
|
||||
Dry run:
|
||||
|
||||
```bash
|
||||
npm run autotest:android-control -- --dry-run --max-devices 2
|
||||
```
|
||||
|
||||
Single device:
|
||||
|
||||
```bash
|
||||
npm run autotest:android-control -- --only-serials 271d9738
|
||||
```
|
||||
|
||||
## Common options
|
||||
|
||||
- `--build-apk`: build debug APK before testing
|
||||
- `--apk <path>`: custom APK path
|
||||
- `--server-http <url>`: override server HTTP endpoint
|
||||
- `--socket-url <url>`: override socket endpoint for smoke script
|
||||
- `--max-devices <n>`: limit tested devices
|
||||
- `--attempts <n>`: override retry attempts
|
||||
- `--skip-install`: skip APK install stage
|
||||
- `--report <path>`: save matrix JSON to custom path
|
||||
|
||||
## ROM profile strategy
|
||||
|
||||
Built-in rule profiles:
|
||||
|
||||
- `oppo_family`
|
||||
- `vivo_iqoo`
|
||||
- `xiaomi_hyperos`
|
||||
- `huawei_honor`
|
||||
- `default`
|
||||
|
||||
Each profile controls:
|
||||
|
||||
- max attempts
|
||||
- warning tolerance
|
||||
- settle/wait timeout
|
||||
- recovery action sequence
|
||||
|
||||
## Output
|
||||
|
||||
The matrix report contains:
|
||||
|
||||
- selected serial and deduped transports
|
||||
- resolved `deviceId` mapping logic
|
||||
- per-attempt install/launch/online/smoke details
|
||||
- final pass/fail summary
|
||||
|
||||
1575
scripts/adb_control_matrix_orchestrator.mjs
Normal file
1575
scripts/adb_control_matrix_orchestrator.mjs
Normal file
File diff suppressed because it is too large
Load Diff
231
scripts/keepalive_app_list_probe.mjs
Normal file
231
scripts/keepalive_app_list_probe.mjs
Normal file
@@ -0,0 +1,231 @@
|
||||
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 <deviceId>')
|
||||
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)}...<trimmed>` : 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)
|
||||
})
|
||||
409
scripts/redmi_first_permission_flow.mjs
Normal file
409
scripts/redmi_first_permission_flow.mjs
Normal file
@@ -0,0 +1,409 @@
|
||||
#!/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)
|
||||
})
|
||||
Reference in New Issue
Block a user