#!/usr/bin/env node /** * 统一状态裁决 · 自动化验证(2026-11-11) * * 目的:在**真实容器数据**上验证 App 的状态判定,而不是只看单测。 * * 覆盖 5 个核心场景: * ① 本会话在跑 → 显示「正在回复」 * ② 本会话空闲、别的会话在跑 → 显示「在线」(不串台) * ③ 收到 ✅ reaction → 立刻收尾 * ④ 容器没上报会话级状态 → 只显示在线/离线,不猜 * ⑤ 多人共用:别人在跑不影响我 * * 用法:node scripts/verify-unified-state.mjs */ import { readFileSync } from 'node:fs'; import { homedir } from 'node:os'; import { join } from 'node:path'; const F = 'https://server.fmode.cn/api/functions'; const AGENT = 'agent-node-lj-test'; const tok = JSON.parse(readFileSync(join(homedir(), '.fmode/config/user.json'), 'utf8')).sessionToken; /** * 复刻 App 侧 `resolveSessionPhase()` 的优先级。 * 🔴 这段必须与 `src/app/core/agent-liveness.ts` 保持一致, * 脚本只是把同一套规则搬到 Node 里跑真实数据。 */ function resolveSessionPhase({ turnEnded, sessionRunning, typing, turnLive }) { if (turnEnded) return { busy: false, reason: 'reaction' }; if (sessionRunning === true) return { busy: true, phase: 'replying', reason: 'session' }; if (sessionRunning === false) return { busy: false, reason: 'session' }; if (typing) return { busy: true, phase: 'thinking', reason: 'typing' }; if (turnLive) return { busy: true, phase: 'replying', reason: 'turn' }; return { busy: false, reason: 'idle' }; } /** 复刻 `sessionStateOf()`:userId 收窄 → thread 根 → roomId */ function sessionRunning(sessions, appSessionId, roomId, myUserId) { if (!sessions?.length || !appSessionId) return undefined; const core = appSessionId.split(':')[0]; const reportedUsers = sessions.some((s) => !!s.userId); const pool = myUserId && reportedUsers ? sessions.filter((s) => !s.userId || s.userId === myUserId) : sessions; /* 过滤后为空 = 容器没报我的状态 = 不知道,不回落到全集 */ if (!pool.length) return undefined; const byThread = pool.find((s) => s.sessionKey.includes(core) || s.sessionKey.includes(appSessionId)); const hit = byThread ?? (roomId ? pool.find((s) => s.sessionKey.includes(roomId)) : undefined); return hit ? hit.status === 'running' : undefined; } async function readAgent() { const r = await fetch(F, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: 'xlosi3m46q', token: tok, params: { agentId: AGENT } }), }); const j = await r.json(); const a = j.agent ?? j.result ?? j; const ex = typeof a.extra === 'string' ? JSON.parse(a.extra) : a.extra; return { status: a.status, sessions: ex?.currentSessions ?? [] }; } let pass = 0; let fail = 0; function check(name, actual, expected) { const ok = JSON.stringify(actual) === JSON.stringify(expected); console.log(` ${ok ? '✅' : '❌'} ${name}`); if (!ok) console.log(` 期望 ${JSON.stringify(expected)} / 实际 ${JSON.stringify(actual)}`); ok ? pass++ : fail++; } console.log('=== 读取容器真实上报 ===\n'); const d = await readAgent(); console.log('全局 status =', d.status); console.log('currentSessions =', d.sessions.length, '条'); for (const s of d.sessions) { console.log(` ${String(s.status).padEnd(8)} ${s.sessionKey} userId=${s.userId ?? '(无)'}`); } const running = d.sessions.find((s) => s.status === 'running'); const idle = d.sessions.find((s) => s.status === 'idle'); const myMx = d.sessions[0]?.userId ?? '@xhvrjtmwqh:fmode.cn'; const MY_SID = '$approot_selftest_abc:fmode.cn'; console.log('\n=== 场景验证 ===\n'); console.log('① 本会话在跑 → 正在回复'); { const room = running?.sessionKey; if (room) { const sr = sessionRunning([running], MY_SID, room, myMx); const r = resolveSessionPhase({ turnEnded: false, sessionRunning: sr, typing: false, turnLive: false }); check('本会话 running → 忙碌', r.busy, true); check('归类为 session 信号', r.reason, 'session'); } else { console.log(' ⏭ 当前没有 running 会话,跳过(可发一条长任务后重跑)'); } } console.log('\n② 本会话空闲、别的会话在跑 → 在线(不串台)'); { if (idle && running) { const sr = sessionRunning([idle, running], MY_SID, idle.sessionKey, myMx); const r = resolveSessionPhase({ turnEnded: false, sessionRunning: sr, typing: true, turnLive: true }); check('别的在跑不算我在跑', r.busy, false); check('typing 残留也不能翻盘', r.reason, 'session'); } else { console.log(' ⏭ 需要同时存在 running 和 idle 两条,跳过'); } } console.log('\n③ 收到 ✅ reaction → 立刻收尾'); { const r = resolveSessionPhase({ turnEnded: true, sessionRunning: true, typing: true, turnLive: true }); check('reaction 一票否决', r.busy, false); check('归类为 reaction', r.reason, 'reaction'); } console.log('\n④ 容器没上报会话级状态 → 不猜'); { const r = resolveSessionPhase({ turnEnded: false, sessionRunning: undefined, typing: false, turnLive: false }); check('无信号 → 空闲', r.busy, false); check('不拿全局 status 冒充', r.reason, 'idle'); } console.log('\n⑤ 多人共用:别人的会话不影响我'); { const other = d.sessions.find((s) => s.userId && s.userId !== myMx); if (other) { const sr = sessionRunning([other], MY_SID, other.sessionKey, myMx); check('别人的 running 不算我的', sr, undefined); } else { console.log(' ⏭ 当前库里只有本人会话,用构造数据验证'); const fake = [{ sessionKey: '!shared', userId: '@other:fmode.cn', status: 'running' }]; check('别人的 running 不算我的', sessionRunning(fake, MY_SID, '!shared', myMx), undefined); } } console.log(`\n=== 结果:${pass} 通过 / ${fail} 失败 ===`); process.exit(fail ? 1 : 0);