#!/usr/bin/env node /** * 多会话并发测试(2026-11-11 用户要求) * * 目的:同一容器同时开两个会话并发提问,观察: * ① 回复是否会串台(A 会话收到 B 的回答) * ② 回复是否丢失(只回了一句「我先去查」就没下文) * ③ typing 结束信号是按会话发还是按容器发 * ④ extra.currentSessions 能否区分两个会话的运行态 * * 用法:node scripts/test-multi-session.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 失败: ' + JSON.stringify(j).slice(0, 200)); return d; } /** 读云函数,看 currentSessions 有没有区分两个会话 */ async function readAgentStatus() { const r = await fetch(F, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: 'xlosi3m46q', token: tok, params: { agentId: TARGET } }), }); const j = await r.json(); const d = j.result ?? j; const ex = d?.extra; const parsed = typeof ex === 'string' ? safeJson(ex) : ex; return { status: d?.status, currentTask: d?.currentTask, currentSessions: parsed?.currentSessions, }; } function safeJson(s) { try { return JSON.parse(s); } catch { return null; } } 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}` }; async function makeRoom(tag) { const room = ( await c.createRoom({ invite: [a.agentMxId], name: `${tag}-${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]) return room; } throw new Error(`房间 ${tag} 容器未进房`); } /** 事件收集器:按房间记录 agent 消息 / typing / 自身消息 */ const log = { A: [], B: [], typing: [] }; const T0 = Date.now(); const rel = (t) => `+${((t - T0) / 1000).toFixed(1)}s`; let roomA, roomB; c.on('Room.timeline', (ev, rm) => { const rid = rm?.roomId; if (rid !== roomA && rid !== roomB) return; const tag = rid === roomA ? 'A' : 'B'; if (ev.getType() !== 'm.room.message') return; const mine = ev.getSender() === p.userMxId; if (mine) return; const body = String(ev.getContent()?.body ?? ''); log[tag].push({ at: Date.now(), body }); console.log(`[${rel(Date.now())}] 房间${tag} agent: ${JSON.stringify(body.slice(0, 70))}`); }); c.on('RoomMember.typing', (ev, m) => { if (m?.roomId !== roomA && m?.roomId !== roomB) return; const tag = m.roomId === roomA ? 'A' : 'B'; log.typing.push({ at: Date.now(), tag, typing: m.typing }); console.log(`[${rel(Date.now())}] 🔵 房间${tag} typing=${m.typing}`); }); console.log('\n=== 建两个会话 ==='); roomA = await makeRoom('multiA'); roomB = await makeRoom('multiB'); console.log('房间 A =', roomA); console.log('房间 B =', roomB); console.log('\n=== 并发发送(间隔 0.5 秒)==='); await c.sendEvent(roomA, 'm.room.message', { msgtype: 'm.text', body: '请详细介绍一下杭州西湖的历史,分 5 点说,每点写 2 句话。', 'm.mentions': { user_ids: [a.agentMxId] }, }); await sleep(500); await c.sendEvent(roomB, 'm.room.message', { msgtype: 'm.text', body: '请列出中国的 5 个一线城市,每个城市说一句特色。', 'm.mentions': { user_ids: [a.agentMxId] }, }); console.log('两条已发送'); /* 期间每 15 秒记录一次容器上报的 currentSessions */ console.log('\n=== 周期观察容器上报 ==='); for (let i = 0; i < 10; i++) { await sleep(15000); const st = await readAgentStatus().catch(() => null); if (st) { console.log( `[${rel(Date.now())}] 容器上报 status=${st.status} currentTask=${JSON.stringify((st.currentTask || '').slice(0, 40))} sessions=${JSON.stringify(st.currentSessions)}`, ); } // 两个房间都超过 60 秒没动静就提前结束 const lastA = log.A.at(-1)?.at ?? T0; const lastB = log.B.at(-1)?.at ?? T0; if (Date.now() - lastA > 60000 && Date.now() - lastB > 60000 && log.A.length && log.B.length) { console.log('两边都静默超过 60 秒,提前结束观察'); break; } } console.log('\n================ 结果 ================'); for (const tag of ['A', 'B']) { console.log(`\n【房间 ${tag}】agent 消息 ${log[tag].length} 条`); log[tag].forEach((m, i) => console.log(` ${i + 1}. ${JSON.stringify(m.body.slice(0, 80))}`)); } console.log(`\n【typing 事件】共 ${log.typing.length} 条`); const byTag = { A: log.typing.filter((t) => t.tag === 'A'), B: log.typing.filter((t) => t.tag === 'B') }; for (const tag of ['A', 'B']) { const arr = byTag[tag]; console.log(` 房间${tag}: ${arr.map((t) => `${rel(t.at)}=${t.typing}`).join(' | ') || '无'}`); } /* 串台检测:B 的问题里有「一线城市」,A 的回答不该出现它 */ const crossA = log.A.filter((m) => m.body.includes('一线城市')); const crossB = log.B.filter((m) => m.body.includes('西湖')); console.log('\n【串台检测】'); console.log(' A 里出现 B 的内容:', crossA.length ? `❌ ${crossA.length} 条` : '✅ 无'); console.log(' B 里出现 A 的内容:', crossB.length ? `❌ ${crossB.length} 条` : '✅ 无'); console.log('\n【回复完整性】'); for (const tag of ['A', 'B']) { const err = await probeRoom(tag === 'A' ? roomA : roomB); console.log(` 房间${tag} 原始事件数: ${err}`); } /** 直接读 timeline,确认有没有 App 没收到的事件 */ async function probeRoom(roomId) { const r = await fetch( `${hs}/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/messages?dir=b&limit=100`, { headers: auth }, ); const j = await r.json(); const bodies = (j.chunk ?? []) .filter((e) => e.type === 'm.room.message' && e.sender === a.agentMxId) .reverse(); console.log(` ─ 房间 ${roomId.slice(-8)} timeline 里 agent 消息 ${bodies.length} 条:`); bodies.forEach((e) => console.log(` ${JSON.stringify(String(e.content?.body ?? '').slice(0, 70))}`)); return bodies.length; } process.exit(0);