import { Agent } from './api/models'; import { formatRelayQuestion, formatRelayReply, parseRelayIntent } from './relay-intent'; /** * 容器中转 —— 识别「让 A 去问 B」的指令。 * * 实测依据(2026-10-29): * · 容器有能力主动发 Matrix 消息(lj 实发成功并回读验证) * · 但 lj-test 发送失败(PUT 返回空体),且两端互相看不见对方 * · 所以让容器互发不可靠 —— 由 App 搬运更稳 */ const agent = (id: string, name: string): Agent => ({ id, name, status: 'idle' as Agent['status'], matrixReady: true, unread: 0, }); const A = agent('a', 'lj'); const B = agent('b', 'lj-test'); const ALL = [A, B]; describe('parseRelayIntent · 动词式', () => { it('「问 X:问题」', () => { const r = parseRelayIntent('问 lj-test:数据库用的什么版本', ALL)!; expect(r.target.id).toBe(B.id); expect(r.question).toBe('数据库用的什么版本'); }); it('「让 X 做某事」', () => { const r = parseRelayIntent('让 lj-test 检查一下接口', ALL)!; expect(r.target.id).toBe(B.id); expect(r.question).toBe('检查一下接口'); }); it('「问问 X」/「问一下 X」', () => { expect(parseRelayIntent('问问 lj-test:进度如何', ALL)!.question).toBe('进度如何'); expect(parseRelayIntent('问一下 lj-test,进度如何', ALL)!.question).toBe('进度如何'); }); it('全角冒号与逗号也能分隔', () => { expect(parseRelayIntent('问 lj-test:进度如何', ALL)!.question).toBe('进度如何'); expect(parseRelayIntent('问 lj-test,进度如何', ALL)!.question).toBe('进度如何'); }); }); describe('parseRelayIntent · @ 式', () => { it('「@X 问题」', () => { const r = parseRelayIntent('@lj-test 这个问题你怎么看', ALL)!; expect(r.target.id).toBe(B.id); expect(r.question).toBe('这个问题你怎么看'); }); }); describe('parseRelayIntent · 🔴 前缀陷阱', () => { /* * `lj` 是 `lj-test` 的前缀。若按短名优先匹配, * 「问 lj-test:xxx」会被误判成问 lj —— 实测容器间也有同样问题。 */ it('「问 lj-test」不会被误判成问 lj', () => { const r = parseRelayIntent('问 lj-test:接口怎么样了', ALL)!; expect(r.target.id).toBe(B.id); expect(r.target.name).toBe('lj-test'); }); it('「问 lj」正确命中短名那个', () => { const r = parseRelayIntent('问 lj:今天状态如何', ALL)!; expect(r.target.id).toBe(A.id); }); it('短名出现在问题正文里不会误触发', () => { // 「问 lj-test」命中 lj-test;问题里的 lj 只是正文 const r = parseRelayIntent('问 lj-test:lj 那边的接口好了吗', ALL)!; expect(r.target.id).toBe(B.id); expect(r.question).toBe('lj 那边的接口好了吗'); }); }); describe('parseRelayIntent · 不该命中的情况', () => { it('普通对话不触发', () => { expect(parseRelayIntent('帮我做个登录页', ALL)).toBeNull(); expect(parseRelayIntent('这个接口怎么写', ALL)).toBeNull(); }); // 只有名字没有问题时,转过去也没意义 it('只有名字没问题时不触发', () => { expect(parseRelayIntent('问 lj-test', ALL)).toBeNull(); expect(parseRelayIntent('问 lj-test:', ALL)).toBeNull(); expect(parseRelayIntent('@lj-test', ALL)).toBeNull(); }); it('空文本 / 无候选容器时不触发', () => { expect(parseRelayIntent('', ALL)).toBeNull(); expect(parseRelayIntent('问 lj-test:xxx', [])).toBeNull(); }); it('名字以正则元字符结尾也能正确匹配', () => { const dot = agent('c', 'bot.v2'); const r = parseRelayIntent('问 bot.v2:你好', [dot])!; expect(r.target.id).toBe('c'); expect(r.question).toBe('你好'); }); it('多行问题完整保留', () => { const r = parseRelayIntent('问 lj-test:第一行\n第二行', ALL)!; expect(r.question).toBe('第一行\n第二行'); }); }); describe('formatRelayQuestion', () => { it('标明是代问,并说明答复会转回', () => { const s = formatRelayQuestion(A, '用户9836', '数据库什么版本'); expect(s).toContain('lj'); expect(s).toContain('用户9836'); expect(s).toContain('数据库什么版本'); expect(s).toContain('转回'); }); }); describe('formatRelayReply', () => { it('标明来源与原问题', () => { const s = formatRelayReply(B, '数据库什么版本', 'MySQL 8.0'); expect(s).toContain('lj-test'); expect(s).toContain('数据库什么版本'); expect(s).toContain('MySQL 8.0'); }); it('超长问题截断', () => { const s = formatRelayReply(B, '一'.repeat(200), '答'); expect(s).toContain('…'); }); it('对方没给内容时如实说明', () => { expect(formatRelayReply(B, 'q', ' ')).toContain('没有给出内容'); }); });