| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143 |
- #!/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);
|