#!/usr/bin/env node import { appendFile, mkdir, readFile, rename, writeFile } from "node:fs/promises"; import path from "node:path"; function parseArgs(argv) { const args = { base: "http://127.0.0.1:8899", deviceId: "xgfe65eimrrofyws", intervalSeconds: 60, timeoutSeconds: 45, outputDir: "", }; const mapping = { "--base": "base", "--device-id": "deviceId", "--interval-seconds": "intervalSeconds", "--timeout-seconds": "timeoutSeconds", "--output-dir": "outputDir", }; for (let index = 0; index < argv.length; index += 2) { const key = mapping[argv[index]]; if (!key || argv[index + 1] === undefined) { throw new Error(`invalid argument: ${argv[index] ?? ""}`); } args[key] = argv[index + 1]; } args.intervalSeconds = Number(args.intervalSeconds); args.timeoutSeconds = Number(args.timeoutSeconds); if (!args.outputDir) { throw new Error("--output-dir is required"); } return args; } async function requestJson(url, timeoutSeconds) { const response = await fetch(url, { headers: { Accept: "application/json", "User-Agent": "workphone-stability/2.0" }, signal: AbortSignal.timeout(timeoutSeconds * 1000), }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${url}`); } return response.json(); } async function writeJson(filePath, value) { const tempPath = `${filePath}.tmp`; await writeFile(tempPath, `${JSON.stringify(value, null, 2)}\n`, "utf8"); await rename(tempPath, filePath); } async function restoreSamples(samplesPath) { const state = { startedAt: new Date(), total: 0, ok: 0, failures: 0, offlineEvents: 0, reconnects: 0, latencyTotal: 0, maxLatency: 0, lastOk: null, lastError: "", }; let content = ""; try { content = await readFile(samplesPath, "utf8"); } catch (error) { if (error.code !== "ENOENT") throw error; return state; } for (const line of content.split("\n")) { if (!line.trim()) continue; let row; try { row = JSON.parse(line); } catch { continue; } if (!row || typeof row !== "object") continue; if (state.total === 0) { const firstTimestamp = new Date(row.timestamp); if (!Number.isNaN(firstTimestamp.getTime())) state.startedAt = firstTimestamp; } const rowOk = Boolean(row.ok); const elapsedMs = Number(row.request_elapsed_ms) || 0; state.total += 1; state.ok += Number(rowOk); state.failures += Number(!rowOk); state.latencyTotal += elapsedMs; state.maxLatency = Math.max(state.maxLatency, elapsedMs); if (rowOk) { if (state.lastOk === false) state.reconnects += 1; state.lastError = ""; } else { if (state.lastOk !== false) state.offlineEvents += 1; state.lastError = String(row.error || "sdk/ws/hook not ready"); } state.lastOk = rowOk; } return state; } const args = parseArgs(process.argv.slice(2)); const outputDir = path.resolve(args.outputDir); const samplesPath = path.join(outputDir, "stability_samples.jsonl"); const summaryPath = path.join(outputDir, "stability_latest_summary.json"); await mkdir(outputDir, { recursive: true }); const state = await restoreSamples(samplesPath); let stopping = false; process.on("SIGTERM", () => { stopping = true; }); process.on("SIGINT", () => { stopping = true; }); while (!stopping) { const sampleStarted = Date.now(); const now = new Date(); const row = { timestamp: now.toISOString(), device_id: args.deviceId, ok: false, }; try { const health = await requestJson(`${args.base}/health`, args.timeoutSeconds); const query = new URLSearchParams({ device_id: args.deviceId, samples: "1", interval_seconds: "0", }); const watch = await requestJson( `${args.base}/api/v3/stability/watch?${query.toString()}`, args.timeoutSeconds, ); const probe = Array.isArray(watch?.data?.samples) ? watch.data.samples[0] ?? {} : {}; Object.assign(row, { sdk_healthy: health.status === "healthy", devices_online: health.devices_online, ws_online: Boolean(probe.ws_online), adb_online: Boolean(probe.adb_online), hook_ok: Boolean(probe.hook_ok), wechat_version: probe.wechat_version || "", probe_latency_ms: probe.latency_ms || 0, error: probe.error || "", }); row.ok = Boolean(row.sdk_healthy && row.ws_online && row.hook_ok); } catch (error) { row.error = String(error?.message || error).slice(0, 500); } row.request_elapsed_ms = Date.now() - sampleStarted; state.total += 1; state.latencyTotal += row.request_elapsed_ms; state.maxLatency = Math.max(state.maxLatency, row.request_elapsed_ms); if (row.ok) { state.ok += 1; if (state.lastOk === false) state.reconnects += 1; state.lastError = ""; } else { state.failures += 1; state.lastError = String(row.error || "sdk/ws/hook not ready"); if (state.lastOk !== false) state.offlineEvents += 1; } state.lastOk = Boolean(row.ok); await appendFile(samplesPath, `${JSON.stringify(row)}\n`, "utf8"); const elapsedSeconds = Math.max((now.getTime() - state.startedAt.getTime()) / 1000, 0); await writeJson(summaryPath, { started_at: state.startedAt.toISOString(), updated_at: now.toISOString(), elapsed_seconds: Math.trunc(elapsedSeconds), elapsed_hours: Number((elapsedSeconds / 3600).toFixed(3)), device_id: args.deviceId, base: args.base, interval_seconds: args.intervalSeconds, total_samples: state.total, ok_samples: state.ok, failed_samples: state.failures, success_rate: state.total ? Number((state.ok / state.total).toFixed(6)) : 0, offline_events: state.offlineEvents, reconnects: state.reconnects, average_request_elapsed_ms: state.total ? Number((state.latencyTotal / state.total).toFixed(2)) : 0, max_request_elapsed_ms: state.maxLatency, last_ok: Boolean(row.ok), last_error: state.lastError, acceptance_24h_complete: elapsedSeconds >= 86400 && state.failures === 0, samples_file: samplesPath, runner: "node", }); if (!stopping) { await new Promise((resolve) => setTimeout(resolve, Math.max(args.intervalSeconds, 5) * 1000)); } }