#!/usr/bin/env node // 线上环境一键核查(Node >=18, 零依赖): 登录 → 核心接口链 → 12 项 PASS/FAIL // 用法: // node check-online.mjs # 核查线上 https://recording.sh-lami.com // SMARTBADGE_API_BASE=http://localhost:3002 node check-online.mjs # 核查本地 // 密码: SMARTBADGE_PASSWORD 环境变量或第一个参数(亦可来自 .env,经 lib 回退链自动加载) import { baseUrl, login, httpJson, loadToken, fail, findEnvFile, loadEnv } from './lib.mjs'; const env = findEnvFile(); if (env.hit) loadEnv(env.hit); const PASSWORD = process.env.SMARTBADGE_PASSWORD || process.argv[2]; const base = baseUrl(); if (!PASSWORD) fail('用法: node check-online.mjs [密码](或先 SMARTBADGE_PASSWORD=<密码>)'); let pass = 0; let failCount = 0; const check = (name, cond, extra = '') => { const ok = !!cond; console.log(`[${ok ? 'PASS' : 'FAIL'}] ${name}${extra ? ` — ${extra}` : ''}`); ok ? pass++ : failCount++; }; console.log(`== 线上健康核查 (${base}) ==`); // 1. 登录 let token; try { token = await login({ base, password: PASSWORD }); } catch (err) { console.log(`\n== 结果: 0 PASS / FAIL(登录失败) ==`); process.exit(1); } const auth = { base, token }; // 2. health / 错误面 const h = await httpJson('GET', '/api/analysis/health', undefined, auth); const warnings = h.json?.data?.warnings ?? []; const isBenign = (w) => /无待转录文件|属正常/.test(String(w)); const realWarnings = warnings.filter((w) => !isBenign(w)); check('health ok(仅无害告警)', h.ok && (h.json?.data?.ok === true || realWarnings.length === 0), `warnings=${warnings.length}${realWarnings.length ? `(异常: ${realWarnings.join('; ')})` : ''}`); check('无害告警外无警告', realWarnings.length === 0, realWarnings.join('; ') || `无害=${warnings.length}条`); check('自动重试中=0', Number(h.json?.data?.retryingCount ?? -1) === 0, `retryingCount=${h.json?.data?.retryingCount}`); check('OSS 缺失=0', Number(h.json?.data?.ossMissingCount ?? -1) === 0, `ossMissingCount=${h.json?.data?.ossMissingCount}`); const cat = h.json?.data?.categoryCounts ?? {}; const catTotal = Object.values(cat).reduce((a, b) => a + b, 0); check('错误分类计数=0', catTotal === 0, JSON.stringify(cat)); // 3. 归档面 const missing = await httpJson('GET', '/api/archive/missing', undefined, auth); const missingList = missing.json?.data?.items ?? missing.json?.data; check('归档失败待重试=空', missing.ok && Array.isArray(missingList) && missingList.length === 0, `missing=${Array.isArray(missingList) ? missingList.length : 'err'}`); // 4. 列表面(ADR-005 本地主路径 + 实时分流) const rec = await httpJson('POST', '/api/smartbadge/recordings/list', { page: 1, pageSize: 5 }, auth); check('录音本地列表', rec.ok && (rec.json?.data?.total || 0) > 0, `total=${rec.json?.data?.total}`); check('录音列表无失败项', rec.ok && Array.isArray(rec.json?.data?.failed) && rec.json.data.failed.length === 0); const today = new Date().toISOString().slice(0, 10); const rt = await httpJson('POST', '/api/smartbadge/recordings/list', { endDate: today, page: 1, pageSize: 5 }, auth); check('实时窗分流', rt.ok && Array.isArray(rt.json?.data?.items), `total=${rt.json?.data?.total}`); const hb = await httpJson('POST', '/api/smartbadge/logs/heartbeat', { page: 1, pageSize: 5 }, auth); check('心跳接口', hb.ok && typeof hb.json?.data?.total === 'number', `total=${hb.json?.data?.total}(上游无数据属正常)`); const devs = await httpJson('GET', '/api/devices?page=1&pageSize=15', undefined, auth); check('设备列表', devs.ok && Array.isArray(devs.json?.data?.items || devs.json?.data), `count=${(devs.json?.data?.items || devs.json?.data || []).length}`); // 5. 任务/报告 const jobs = await httpJson('GET', '/api/analysis/jobs?pageSize=5', undefined, auth); const list = jobs.json?.data?.items ?? jobs.json?.data ?? []; const running = list.filter((j) => ['pending', 'downloading', 'filtering', 'transcribing', 'reporting'].includes(j.status)); check('无运行中任务(互斥空闲)', running.length === 0, `running=${running.length}`); console.log(`\n== 结果: ${pass} PASS / ${failCount} FAIL ==`); process.exit(failCount > 0 ? 1 : 0);