feat: L0 线协议冻结 + 附件链路修复 + 人/Agent 区分
L0 核心: - 严格解码 Decode(DisallowUnknownFields) 全覆盖 29 个 DecodeBody 调用点 - DecodeLenient 心跳专用:容忍新字段但回报 unknown_fields - 400 消息列出本端点接受的全部字段(jsonFieldNames 反射 tag) - 日历 status 校验(create 补字段 + update 拦非法值) - 新增 strictdecode_test.go 10 例 + blob/list_test.go 6 例 A-4 附件挂载回滚:checkAttachable 在 CreateMail 前校验,失败按 解挂→释放 relay→删邮件→退预算回滚,幽灵邮件这条路堵住了 A-5 反向 GC:blob.Store.List() 枚举磁盘(跳 .upload-*), SweepUnreferencedBlobs 按 attachments + calendar_attachments 反查, 48h 年龄下限兜上传窗口。已接进每小时 sweep 循环 C 人/Agent 区分:四个读路径 + threadCols 补 from_human / to_human (EXISTS users 判定),models.Mail 加 ToHuman。前端判据从 workspace 启发式改成显式布尔,mailCounterpart/sessionCounterpart 从 session_workspace 取 path(修 dsh@dsh 拼接 bug) 契约文档:SSE new_mail 补 4 字段(in_reply_to/from_human/ permission_mode/permission_enforcement),B-5 加 B-5.6 (Agent→Agent 不转发),B-3.4 MUST 改条件式,心跳补 mode_enforcement + unknown_fields,demo 死链修复 + from_human 检查 验收清单加 Agent→Agent 负向对照项
This commit is contained in:
@ -137,11 +137,13 @@ function SessionGroup({
|
||||
// 列表行只显示「跟谁在通信」,**不带会话位**:分组头下面已经单独显示了
|
||||
// 会话别名,再拼一遍会让长别名(实测 92 字节)把这一行挤没。
|
||||
//
|
||||
// workspace 从会话取:from_workspace 对 Agent 存的是 Agent 名而非路径。
|
||||
const peerWs = showTo ? g.latest.to_workspace : g.latest.from_workspace;
|
||||
// 人还是 Agent 走显式布尔;workspace 从会话取(from_workspace 对 Agent
|
||||
// 存的是 Agent 名而非路径)。
|
||||
const isPeerHuman = showTo ? g.latest.to_human : g.latest.from_human;
|
||||
const peer = participantAddress(
|
||||
showTo ? g.latest.to_name : g.latest.from_name,
|
||||
peerWs ? g.latest.session_workspace || '' : ''
|
||||
isPeerHuman,
|
||||
isPeerHuman ? '' : g.latest.session_workspace || ''
|
||||
);
|
||||
|
||||
// 组内含选中邮件时给个边框,否则展开一个组再滚下去会找不到自己在看哪封
|
||||
@ -240,10 +242,11 @@ function MailItem({
|
||||
minute: '2-digit'
|
||||
});
|
||||
|
||||
const rowWs = showTo ? mail.to_workspace : mail.from_workspace;
|
||||
const isRowHuman = showTo ? mail.to_human : mail.from_human;
|
||||
const peer = participantAddress(
|
||||
showTo ? mail.to_name : mail.from_name,
|
||||
rowWs ? mail.session_workspace || '' : ''
|
||||
isRowHuman,
|
||||
isRowHuman ? '' : mail.session_workspace || ''
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@ -383,12 +383,14 @@ function Header({
|
||||
const ws = mail.session_workspace || '';
|
||||
const from = participantAddress(
|
||||
mail.from_name,
|
||||
mail.from_workspace ? ws : '',
|
||||
mail.from_human,
|
||||
mail.from_human ? '' : ws,
|
||||
mail.session_alias
|
||||
);
|
||||
const to = participantAddress(
|
||||
mail.to_name,
|
||||
mail.to_workspace ? ws : '',
|
||||
mail.to_human,
|
||||
mail.to_human ? '' : ws,
|
||||
mail.session_alias
|
||||
);
|
||||
|
||||
|
||||
@ -240,11 +240,13 @@ function Node({
|
||||
)}
|
||||
{/* 树节点一行里塞了 from → to、转发标记与时间,不带会话位:
|
||||
整棵树本来就在同一条线索上,每个节点重复一遍别名毫无信息量。
|
||||
workspace 从会话取(from_workspace 对 Agent 存的是 Agent 名)。 */}
|
||||
人还是 Agent 走显式布尔;workspace 从会话取(from_workspace
|
||||
对 Agent 存的是 Agent 名)。 */}
|
||||
<span className="text-xs font-mono text-gray-700 truncate">
|
||||
{participantAddress(
|
||||
node.from_name,
|
||||
node.from_workspace ? node.session_workspace || '' : ''
|
||||
node.from_human,
|
||||
node.from_human ? '' : node.session_workspace || ''
|
||||
)}
|
||||
</span>
|
||||
<span className="text-[10px] text-gray-400">→</span>
|
||||
|
||||
@ -26,6 +26,8 @@ export interface Counterpart {
|
||||
name: string;
|
||||
/** 工作目录,可能为空(人类没有工作目录) */
|
||||
path: string;
|
||||
/** 这一方是人类用户还是 Agent —— 决定地址拼几段 */
|
||||
isHuman: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -69,23 +71,23 @@ export function formatAddress(name: string, path: string, session?: string | nul
|
||||
* **人只要名字**:人没有工作目录,也不需要指定会话(发给人就是进他的收件箱)。
|
||||
* 给人拼 `jianf@.某会话` 或 `jianf.某会话` 都是把 Agent 的维度硬套在人身上。
|
||||
*
|
||||
* 判据是有没有 workspace:Agent 一定带工作目录,人一定不带。
|
||||
* 判据从「workspace 是否为空」的启发式改成**显式布尔**:
|
||||
* `mails.from_workspace` 对 Agent 存的是 Agent 名而不是路径(历史遗留),
|
||||
* 拿它当「是不是 Agent」的代理变量会在边界上猜错。
|
||||
* 服务端用 `EXISTS (SELECT 1 FROM users …)` 判人/Agent,那条布尔才是权威。
|
||||
*
|
||||
* # workspace 必须从**会话**取,不能用 from_workspace
|
||||
*
|
||||
* `mails.from_workspace` 对 Agent 存的是**Agent 名而不是路径**(历史遗留,
|
||||
* 见后端 db/migrate.go 里 sessions.workspace 的注释)。拿它当路径拼会得到
|
||||
* `dsh@dsh` —— 界面上真出现过。会话的 `workspace` 才是权威来源。
|
||||
* workspace 必须从**会话**取(`session_workspace`),不能用 from_workspace:
|
||||
* 后者对 Agent 存的是 Agent 名,拿它拼会得到 `dsh@dsh`。
|
||||
*/
|
||||
export function participantAddress(
|
||||
name: string,
|
||||
isHuman: boolean,
|
||||
workspace?: string | null,
|
||||
sessionAlias?: string | null
|
||||
): string {
|
||||
const path = (workspace || '').trim();
|
||||
// 人(无工作目录):只有名字,不带 path 也不带会话位
|
||||
if (!path) return formatAddress(name, '');
|
||||
return formatAddress(name, path, sessionAlias || null);
|
||||
if (isHuman) return formatAddress(name, '');
|
||||
return formatAddress(name, (workspace || '').trim(), sessionAlias || null);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -96,9 +98,11 @@ export function participantAddress(
|
||||
*/
|
||||
export function mailCounterpart(mail: Mail, me: string): Counterpart {
|
||||
const iSent = !!me && mail.from_name === me;
|
||||
return iSent
|
||||
? { name: mail.to_name, path: mail.to_workspace || '' }
|
||||
: { name: mail.from_name, path: mail.from_workspace || '' };
|
||||
const ws = mail.session_workspace || '';
|
||||
if (iSent) {
|
||||
return { name: mail.to_name, path: mail.to_human ? '' : ws, isHuman: mail.to_human };
|
||||
}
|
||||
return { name: mail.from_name, path: mail.from_human ? '' : ws, isHuman: mail.from_human };
|
||||
}
|
||||
|
||||
/**
|
||||
@ -124,13 +128,14 @@ export function sessionCounterpart(mails: Mail[], me: string): Counterpart | nul
|
||||
return na !== nb ? na - nb : a.mail_id.localeCompare(b.mail_id);
|
||||
});
|
||||
|
||||
const ws = (m: Mail) => m.session_workspace || '';
|
||||
let found: Counterpart | null = null;
|
||||
for (const m of sorted) {
|
||||
// 收件人优先于发件人:会话首封多是「我 → Agent」,
|
||||
// 收件人优先于发件人:会话首封多是「我 → Agent」,
|
||||
// 那个 to_name 就是这次任务派给了谁
|
||||
const candidates: Counterpart[] = [
|
||||
{ name: m.to_name, path: m.to_workspace || '' },
|
||||
{ name: m.from_name, path: m.from_workspace || '' }
|
||||
{ name: m.to_name, path: m.to_human ? '' : ws(m), isHuman: m.to_human },
|
||||
{ name: m.from_name, path: m.from_human ? '' : ws(m), isHuman: m.from_human }
|
||||
];
|
||||
for (const c of candidates) {
|
||||
if (!c.name || c.name === me) continue;
|
||||
@ -173,15 +178,20 @@ export function mailReplyTarget(mail: Mail, me: string): string {
|
||||
*
|
||||
* 原先用 `!a.startsWith('human')` 去掉自己 —— 同一个遗留判据,
|
||||
* 结果是点「回复全部」会把自己抄送进去。
|
||||
*
|
||||
* 地址拼法与 participantAddress 一致:人只有名字,Agent 拼 `name@path.session`。
|
||||
* path 从 session_workspace 取,不能从 from_workspace/to_workspace 取
|
||||
* (后者对 Agent 存的是 Agent 名)。
|
||||
*/
|
||||
export function replyAllCC(
|
||||
mail: Mail,
|
||||
me: string,
|
||||
primaryName: string
|
||||
): string[] {
|
||||
const ws = mail.session_workspace || '';
|
||||
const raw = [
|
||||
formatAddress(mail.from_name, mail.from_workspace || ''),
|
||||
formatAddress(mail.to_name, mail.to_workspace || ''),
|
||||
formatAddress(mail.from_name, mail.from_human ? '' : ws),
|
||||
formatAddress(mail.to_name, mail.to_human ? '' : ws),
|
||||
// cc_list 使用 raw 字段(用户输入的原文,保留 .new 等原始意图)
|
||||
...(mail.cc_list ?? []).map(c => c.raw || formatAddress(c.name, c.path || '', c.session || null))
|
||||
];
|
||||
|
||||
@ -114,6 +114,10 @@ export interface Mail {
|
||||
session_workspace?: string;
|
||||
body_preview?: string;
|
||||
attachments?: Attachment[];
|
||||
/** 发件方是人类用户而不是 Agent(服务端 EXISTS users 判的) */
|
||||
from_human: boolean;
|
||||
/** 收件方是人类用户而不是 Agent */
|
||||
to_human: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -36,6 +36,8 @@ function mail(over: Partial<Mail> = {}): Mail {
|
||||
permission_result: null,
|
||||
status: 'read',
|
||||
created_at: `2026-09-03T10:${String(seq % 60).padStart(2, '0')}:00Z`,
|
||||
from_human: false,
|
||||
to_human: true,
|
||||
...over
|
||||
};
|
||||
}
|
||||
|
||||
@ -41,6 +41,11 @@ function mail(over: Partial<Mail> = {}): Mail {
|
||||
permission_result: null,
|
||||
status: 'read',
|
||||
created_at: `2026-09-03T10:${String(seq % 60).padStart(2, '0')}:00Z`,
|
||||
// 默认值:会话工作目录、发件方是 Agent、收件方是人 ——
|
||||
// 对应「pi 在 /home/program/llmsproxy 干活 → jianf」这条最常见的信
|
||||
session_workspace: '/home/program/llmsproxy',
|
||||
from_human: false,
|
||||
to_human: true,
|
||||
...over
|
||||
};
|
||||
}
|
||||
@ -79,18 +84,20 @@ describe('formatAddress', () => {
|
||||
describe('mailCounterpart', () => {
|
||||
it('别人发来的 → 回给发件人', () => {
|
||||
const m = mail({ from_name: 'pi', to_name: 'jianf' });
|
||||
expect(mailCounterpart(m, 'jianf')).toEqual({ name: 'pi', path: '/home/program/llmsproxy' });
|
||||
expect(mailCounterpart(m, 'jianf')).toEqual({ name: 'pi', path: '/home/program/llmsproxy', isHuman: false });
|
||||
});
|
||||
|
||||
it('我发出的 → 回给收件人(不是回给自己)', () => {
|
||||
const m = mail({
|
||||
from_name: 'jianf',
|
||||
from_human: true,
|
||||
from_workspace: '',
|
||||
to_name: 'pi',
|
||||
to_human: false,
|
||||
to_workspace: '/home/program/llmsproxy'
|
||||
});
|
||||
// 这一条就是生产 bug:原代码在这里返回 jianf
|
||||
expect(mailCounterpart(m, 'jianf')).toEqual({ name: 'pi', path: '/home/program/llmsproxy' });
|
||||
expect(mailCounterpart(m, 'jianf')).toEqual({ name: 'pi', path: '/home/program/llmsproxy', isHuman: false });
|
||||
});
|
||||
|
||||
it('登录名未知时退化为回给发件人,不会回给自己', () => {
|
||||
@ -104,22 +111,37 @@ describe('mailCounterpart', () => {
|
||||
expect(mailCounterpart(m, 'jianf').name).toBe('human');
|
||||
expect(mailCounterpart(m, 'human').name).toBe('pi');
|
||||
});
|
||||
|
||||
it('path 从 session_workspace 取,不用 from_workspace(后者存的是 Agent 名)', () => {
|
||||
// from_workspace 存的是 Agent 名而不是路径(历史遗留),
|
||||
// 从 session_workspace 取路径才不会拼出 dsh@dsh
|
||||
const m = mail({
|
||||
from_name: 'dsh',
|
||||
from_workspace: 'dsh', // Agent 名,不是路径
|
||||
to_name: 'jianf',
|
||||
to_human: true,
|
||||
session_workspace: '/home/program/webui4frpc'
|
||||
});
|
||||
const peer = mailCounterpart(m, 'jianf');
|
||||
expect(peer.path).toBe('/home/program/webui4frpc');
|
||||
expect(peer.path).not.toBe('dsh');
|
||||
});
|
||||
});
|
||||
|
||||
describe('sessionCounterpart', () => {
|
||||
it('按会话定对端,不受最后一封是谁发的影响', () => {
|
||||
const mails = [
|
||||
mail({ from_name: 'jianf', from_workspace: '', to_name: 'pi', created_at: '2026-09-03T09:00:00Z' }),
|
||||
mail({ from_name: 'jianf', from_human: true, from_workspace: '', to_name: 'pi', to_human: false, created_at: '2026-09-03T09:00:00Z' }),
|
||||
mail({ from_name: 'pi', to_name: 'jianf', created_at: '2026-09-03T09:30:00Z' }),
|
||||
// 最后一封是我自己发的 —— 原代码在这里会把自己当对端
|
||||
mail({ from_name: 'jianf', from_workspace: '', to_name: 'jianf', created_at: '2026-09-03T10:00:00Z' })
|
||||
mail({ from_name: 'jianf', from_human: true, from_workspace: '', to_name: 'jianf', to_human: true, created_at: '2026-09-03T10:00:00Z' })
|
||||
];
|
||||
expect(sessionCounterpart(mails, 'jianf')?.name).toBe('pi');
|
||||
});
|
||||
|
||||
it('取首个非我参与方:后来被抄送进来的第三方不抢位置', () => {
|
||||
const mails = [
|
||||
mail({ from_name: 'jianf', from_workspace: '', to_name: 'pi', created_at: '2026-09-03T09:00:00Z' }),
|
||||
mail({ from_name: 'jianf', from_human: true, from_workspace: '', to_name: 'pi', to_human: false, created_at: '2026-09-03T09:00:00Z' }),
|
||||
mail({ from_name: 'dsh', from_workspace: '/opt', to_name: 'jianf', created_at: '2026-09-03T09:30:00Z' })
|
||||
];
|
||||
expect(sessionCounterpart(mails, 'jianf')?.name).toBe('pi');
|
||||
@ -127,27 +149,28 @@ describe('sessionCounterpart', () => {
|
||||
|
||||
it('补上首次出现时缺失的 path', () => {
|
||||
const mails = [
|
||||
// 首封的 to_workspace 是空的
|
||||
mail({ from_name: 'jianf', from_workspace: '', to_name: 'pi', to_workspace: '', created_at: '2026-09-03T09:00:00Z' }),
|
||||
// 首封的 session_workspace 是空的
|
||||
mail({ from_name: 'jianf', from_human: true, from_workspace: '', to_name: 'pi', to_human: false, session_workspace: '', created_at: '2026-09-03T09:00:00Z' }),
|
||||
// 后一封才带上目录
|
||||
mail({ from_name: 'pi', from_workspace: '/home/program/llmsproxy', to_name: 'jianf', created_at: '2026-09-03T09:30:00Z' })
|
||||
mail({ from_name: 'pi', from_workspace: '/home/program/llmsproxy', to_name: 'jianf', session_workspace: '/home/program/llmsproxy', created_at: '2026-09-03T09:30:00Z' })
|
||||
];
|
||||
// 同名 Agent 在不同目录是不同的活,path 不能丢
|
||||
expect(sessionCounterpart(mails, 'jianf')).toEqual({
|
||||
name: 'pi',
|
||||
path: '/home/program/llmsproxy'
|
||||
path: '/home/program/llmsproxy',
|
||||
isHuman: false
|
||||
});
|
||||
});
|
||||
|
||||
it('同刻邮件用 mail_id 定序,结果稳定', () => {
|
||||
const ts = '2026-09-03T09:00:00Z';
|
||||
const a = mail({ mail_id: 'aaa', from_name: 'jianf', from_workspace: '', to_name: 'pi', created_at: ts });
|
||||
const z = mail({ mail_id: 'zzz', from_name: 'jianf', from_workspace: '', to_name: 'dsh', created_at: ts });
|
||||
const a = mail({ mail_id: 'aaa', from_name: 'jianf', from_human: true, from_workspace: '', to_name: 'pi', to_human: false, created_at: ts });
|
||||
const z = mail({ mail_id: 'zzz', from_name: 'jianf', from_human: true, from_workspace: '', to_name: 'dsh', to_human: false, created_at: ts });
|
||||
expect(sessionCounterpart([a, z], 'jianf')?.name).toBe(sessionCounterpart([z, a], 'jianf')?.name);
|
||||
});
|
||||
|
||||
it('全是自己的会话返回 null 而不是自己', () => {
|
||||
const mails = [mail({ from_name: 'jianf', from_workspace: '', to_name: 'jianf' })];
|
||||
const mails = [mail({ from_name: 'jianf', from_human: true, from_workspace: '', to_name: 'jianf', to_human: true })];
|
||||
expect(sessionCounterpart(mails, 'jianf')).toBeNull();
|
||||
});
|
||||
|
||||
@ -156,8 +179,8 @@ describe('sessionCounterpart', () => {
|
||||
});
|
||||
|
||||
it('created_at 解析失败不影响确定性', () => {
|
||||
const bad = mail({ mail_id: 'bad', from_name: 'jianf', from_workspace: '', to_name: 'pi', created_at: '不是时间' });
|
||||
const good = mail({ mail_id: 'good', from_name: 'jianf', from_workspace: '', to_name: 'dsh', created_at: '2026-09-03T09:00:00Z' });
|
||||
const bad = mail({ mail_id: 'bad', from_name: 'jianf', from_human: true, from_workspace: '', to_name: 'pi', to_human: false, created_at: '不是时间' });
|
||||
const good = mail({ mail_id: 'good', from_name: 'jianf', from_human: true, from_workspace: '', to_name: 'dsh', to_human: false, created_at: '2026-09-03T09:00:00Z' });
|
||||
expect(sessionCounterpart([bad, good], 'jianf')?.name).toBe(
|
||||
sessionCounterpart([good, bad], 'jianf')?.name
|
||||
);
|
||||
@ -167,7 +190,7 @@ describe('sessionCounterpart', () => {
|
||||
describe('sessionReplyTarget', () => {
|
||||
it('带上会话别名:不带会落到该 Agent 的默认会话', () => {
|
||||
const mails = [
|
||||
mail({ from_name: 'jianf', from_workspace: '', to_name: 'pi', to_workspace: '/home/program/llmsproxy' })
|
||||
mail({ from_name: 'jianf', from_human: true, from_workspace: '', to_name: 'pi', to_human: false, to_workspace: '/home/program/llmsproxy' })
|
||||
];
|
||||
expect(sessionReplyTarget(mails, session('pi-关于llmsproxy工程的联合审查'), 'jianf')).toBe(
|
||||
'pi@/home/program/llmsproxy.pi-关于llmsproxy工程的联合审查'
|
||||
@ -176,7 +199,7 @@ describe('sessionReplyTarget', () => {
|
||||
|
||||
it('会话未命名时省略会话段', () => {
|
||||
const mails = [
|
||||
mail({ from_name: 'jianf', from_workspace: '', to_name: 'pi', to_workspace: '/home' })
|
||||
mail({ from_name: 'jianf', from_human: true, from_workspace: '', to_name: 'pi', to_human: false, session_workspace: '/home' })
|
||||
];
|
||||
expect(sessionReplyTarget(mails, session(null), 'jianf')).toBe('pi@/home');
|
||||
});
|
||||
@ -184,9 +207,9 @@ describe('sessionReplyTarget', () => {
|
||||
it('生产链条重现:回复自己发的那封仍指向 pi', () => {
|
||||
const mails = [
|
||||
mail({ from_name: 'pi', to_name: 'jianf', created_at: '2026-09-03T10:53:12Z' }),
|
||||
mail({ from_name: 'jianf', from_workspace: '', to_name: 'pi', to_workspace: '/home/program/llmsproxy', created_at: '2026-09-03T10:53:39Z' }),
|
||||
mail({ from_name: 'jianf', from_human: true, from_workspace: '', to_name: 'pi', to_human: false, to_workspace: '/home/program/llmsproxy', created_at: '2026-09-03T10:53:39Z' }),
|
||||
mail({ from_name: 'pi', to_name: 'jianf', created_at: '2026-09-03T10:55:13Z' }),
|
||||
mail({ from_name: 'jianf', from_workspace: '', to_name: 'jianf', created_at: '2026-09-03T10:56:34Z' })
|
||||
mail({ from_name: 'jianf', from_human: true, from_workspace: '', to_name: 'jianf', to_human: true, created_at: '2026-09-03T10:56:34Z' })
|
||||
];
|
||||
const target = sessionReplyTarget(mails, session('pi-关于llmsproxy工程的联合审查'), 'jianf');
|
||||
expect(target.startsWith('pi@')).toBe(true);
|
||||
@ -239,7 +262,9 @@ describe('replyAllCC', () => {
|
||||
from_name: 'pi',
|
||||
from_workspace: '/home',
|
||||
to_name: 'dsh',
|
||||
to_human: false,
|
||||
to_workspace: '/opt',
|
||||
session_workspace: '/opt',
|
||||
cc_list: [{ name: 'dsh', path: '/opt', session: '', raw: 'dsh@/opt' }]
|
||||
});
|
||||
expect(replyAllCC(m, 'jianf', 'pi')).toEqual(['dsh@/opt']);
|
||||
@ -302,33 +327,36 @@ describe('participantAddress', () => {
|
||||
* 人只要名字(没有工作目录,也不需要指定会话)
|
||||
*/
|
||||
it('Agent 带完整三段 —— 少任何一段都不是可投递地址', () => {
|
||||
expect(participantAddress('pi', '/home/program/agentmail', '我的任务'))
|
||||
expect(participantAddress('pi', false, '/home/program/agentmail', '我的任务'))
|
||||
.toBe('pi@/home/program/agentmail.我的任务');
|
||||
});
|
||||
|
||||
it('Agent 无会话别名时退到 name@path(默认会话)', () => {
|
||||
expect(participantAddress('pi', '/home/program/agentmail', null))
|
||||
expect(participantAddress('pi', false, '/home/program/agentmail', null))
|
||||
.toBe('pi@/home/program/agentmail');
|
||||
expect(participantAddress('pi', '/home/program/agentmail'))
|
||||
expect(participantAddress('pi', false, '/home/program/agentmail'))
|
||||
.toBe('pi@/home/program/agentmail');
|
||||
});
|
||||
|
||||
it('人只显示名字,即便传了会话别名也不拼', () => {
|
||||
// 给人拼 `jianf@.某会话` 是把 Agent 的维度硬套在人身上
|
||||
expect(participantAddress('jianf', '', '某会话')).toBe('jianf');
|
||||
expect(participantAddress('jianf', null, '某会话')).toBe('jianf');
|
||||
expect(participantAddress('jianf')).toBe('jianf');
|
||||
expect(participantAddress('jianf', true, '', '某会话')).toBe('jianf');
|
||||
expect(participantAddress('jianf', true, null, '某会话')).toBe('jianf');
|
||||
expect(participantAddress('jianf', true)).toBe('jianf');
|
||||
});
|
||||
|
||||
it('人的地址里不含 @ 也不含 .', () => {
|
||||
const addr = participantAddress('jianf', '', '邮件驱动·多智能体协作平台-完整设计文档');
|
||||
const addr = participantAddress('jianf', true, '', '邮件驱动·多智能体协作平台-完整设计文档');
|
||||
expect(addr).toBe('jianf');
|
||||
expect(addr).not.toContain('@');
|
||||
expect(addr).not.toContain('.');
|
||||
});
|
||||
|
||||
it('workspace 只有空白时按人处理', () => {
|
||||
expect(participantAddress('jianf', ' ', '某会话')).toBe('jianf');
|
||||
it('人是 Agent 时 workspace 为空也不会拼出裸 @', () => {
|
||||
// 边界:Agent 名在库里但 session_workspace 为空(数据不完整),
|
||||
// 仍然要留 @ 而不是裸名字 —— 否则按最后一个 . 切分会错
|
||||
expect(participantAddress('pi', false, '', 'sess'))
|
||||
.toBe('pi@.sess');
|
||||
});
|
||||
});
|
||||
|
||||
@ -378,8 +406,10 @@ describe('replyAllCC 保留 cc_list 的原始意图', () => {
|
||||
it('人类发件人(无 workspace)在抄送里是裸名字,不带会话位', () => {
|
||||
const m = mail({
|
||||
from_name: 'jianf',
|
||||
from_human: true,
|
||||
from_workspace: '',
|
||||
to_name: 'pi',
|
||||
to_human: false,
|
||||
to_workspace: '/home',
|
||||
session_alias: '某个很长的会话别名'
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user