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