| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120 |
- #!/usr/bin/env node
- /**
- * 验证容器 typing 信号时序(2026-11-11)
- *
- * 要回答两个问题:
- * ① adapter 自带的 typing 循环是否已关闭(配置 typing_indicator: false)
- * ② typing=false 是否**晚于**最后一条 agent 消息
- *
- * 🔴 必须用 /sync 监听:`m.typing` 是 ephemeral 事件,**不写进 timeline**,
- * 用 /messages 读永远读不到(2026-11-11 踩过这个坑,误判过一次)。
- *
- * 用法:node scripts/verify-typing-order.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 TARGET = 'agent-node-lj-test';
- const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
- const tok = JSON.parse(readFileSync(join(homedir(), '.fmode/config/user.json'), 'utf8')).sessionToken;
- async function prov(id) {
- const r = await fetch(F, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ id: 'RP5Ht55NeX', token: tok, params: { agentId: id } }),
- });
- const j = await r.json();
- const d = j.result ?? j;
- if (!d?.accessToken) throw new Error('provision 失败');
- return d;
- }
- const p = await prov(TARGET);
- const a = await prov(TARGET);
- const mx = await import('matrix-js-sdk');
- const c = mx.createClient({ baseUrl: p.homeserver, accessToken: p.accessToken, userId: p.userMxId });
- await c.startClient({ initialSyncLimit: 1 });
- await sleep(3000);
- const hs = p.homeserver.replace(/\/$/, '');
- const auth = { Authorization: `Bearer ${p.accessToken}` };
- const room = (
- await c.createRoom({
- invite: [a.agentMxId],
- name: `typing-order-${Date.now()}`,
- preset: mx.Preset.TrustedPrivateChat,
- })
- ).room_id;
- for (let i = 0; i < 30; i++) {
- await sleep(2000);
- const r = await fetch(`${hs}/_matrix/client/v3/rooms/${encodeURIComponent(room)}/joined_members`, {
- headers: auth,
- }).catch(() => null);
- const j = r ? await r.json().catch(() => ({})) : {};
- if ((j.joined ?? {})[a.agentMxId]) break;
- }
- const events = [];
- const T0 = Date.now();
- const rel = (t) => (t - T0) / 1000;
- c.on('RoomMember.typing', (ev, m) => {
- if (m?.roomId !== room) return;
- events.push({ t: Date.now(), kind: 'typing', value: m.typing });
- });
- c.on('Room.timeline', (ev, rm) => {
- if (rm?.roomId !== room) return;
- const type = ev.getType();
- if (type === 'm.reaction' && ev.getSender() === a.agentMxId) {
- const key = ev.getContent()?.['m.relates_to']?.key ?? '';
- events.push({ t: Date.now(), kind: 'reaction', value: key });
- }
- if (type === 'm.room.message' && ev.getSender() === a.agentMxId) {
- events.push({ t: Date.now(), kind: 'msg', value: String(ev.getContent()?.body ?? '').slice(0, 40) });
- }
- });
- await c.sendEvent(room, 'm.room.message', {
- msgtype: 'm.text',
- body: '用一句话回答:3 乘以 7 等于多少?不要调用工具。',
- 'm.mentions': { user_ids: [a.agentMxId] },
- });
- console.log('已发送,监听 100 秒…\n');
- await sleep(100_000);
- console.log('=== 事件时序(数字 = 距发送的秒数)===');
- for (const e of events) {
- const tag = e.kind === 'typing' ? `typing=${e.value}` : e.kind === 'reaction' ? `reaction ${e.value}` : `消息「${e.value}」`;
- console.log(` +${rel(e.t).toFixed(1)}s ${tag}`);
- }
- const msgs = events.filter((e) => e.kind === 'msg');
- const lastMsg = msgs.at(-1);
- const typingFalse = events.filter((e) => e.kind === 'typing' && e.value === false);
- const typingTrue = events.filter((e) => e.kind === 'typing' && e.value === true);
- console.log('\n=== 判定 ===');
- console.log(`typing=true ${typingTrue.length} 次`);
- console.log(`typing=false ${typingFalse.length} 次`);
- console.log(`agent 消息 ${msgs.length} 条`);
- if (lastMsg && typingFalse.length) {
- const lastFalse = typingFalse.at(-1);
- const diff = rel(lastFalse.t) - rel(lastMsg.t);
- console.log(`\n最后一条消息在 +${rel(lastMsg.t).toFixed(1)}s,最后一个 typing=false 在 +${rel(lastFalse.t).toFixed(1)}s`);
- console.log(diff >= 0 ? `✅ false 晚于末条消息 ${diff.toFixed(1)}s(正确)` : `❌ false 早于末条消息 ${(-diff).toFixed(1)}s(仍然抢跑)`);
- } else if (msgs.length && !typingFalse.length) {
- console.log('\n⚠️ 没有任何 typing=false —— 插件/adapter 都没发结束信号');
- } else {
- console.log('\n⚠️ 数据不足,无法判定');
- }
- const rej = events.filter((e) => e.kind === 'reaction');
- console.log(`\n另:✅/❌ reaction ${rej.length} 次 ${rej.length ? '(这本身也是可靠的结束信号)' : ''}`);
- process.exit(0);
|