/** * 三维寻址的构造与判读 —— 所有平台插件共用。 * * 为什么这些函数必须共用、且必须是纯函数: * * 地址拼错不会报错。`name@path.session` 的每一段都可以省略,任何组合都能被 * `ParseAddress` 解析出**某个**结果,于是拼错的代价不是失败而是**投到别处**。 * 生产上真实发生过两次: * * 1. 插件把 `.new` 原样当作回信地址 —— `.new` 是一次性动作,回过去只会 * 再建一条平行会话,双方从此各说各话。 * 2. path 为空时朴素拼接得到 `admin.silent-harbor` —— 没有 `@`, * 整串被当成名字,session 位静默丢失。 * * 两次都是「拼字符串」造成的,所以拼地址这件事收进这里,各平台不再自己拼。 */ /** * 拼一个可寻址的 `name@path.session`。 * * **空 path 也必须留下 `@` 与 `.`**:`admin@.silent-harbor` 才解析成 * name=admin path="" session=silent-harbor。省掉 `@` 得到的 * `admin.silent-harbor` 会被整串当作名字。 * * session 省略时不写那一位(默认会话语义)。 * * @param {string} name 收件方名(Agent 名或人类用户名) * @param {string} [path] 工作目录,可为空 * @param {string} [session] 会话别名;空则省略该位 * @returns {string} 地址,name 为空时返回空串 */ export function formatAddress(name, path, session) { const n = String(name ?? '').trim(); const p = String(path ?? '').trim(); const s = String(session ?? '').trim(); if (!n) return ''; if (!s) return p ? `${n}@${p}` : n; return `${n}@${p}.${s}`; } /** * 判断自己在这封邮件里是收件人还是抄送方。 * * 为什么需要它:被抄送方与主收件人的**职责不同**。线上那封联调邮件里, * admin 主发 dsh、抄送 opencode,分工是「dsh 提供源码解读、opencode 提供部署 * 现状、最后由 dsh 汇报」。收件箱若不区分身份,两方都会以为自己是负责人, * 或者都以为自己只是旁观者。 * * @param {any} mail `/mail/inbox` 返回的一封邮件 * @param {string} selfName 自己的 Agent 名 * @returns {'to'|'cc'|'unknown'} */ export function roleOf(mail, selfName) { const self = String(selfName ?? '').trim(); if (!self) return 'unknown'; if (mail?.to_name === self) return 'to'; if (Array.isArray(mail?.cc_list) && mail.cc_list.some(c => c?.name === self)) { return 'cc'; } return 'unknown'; } /** * 给出「把回信发回这条会话」的地址。 * * 发件人一侧**不带 path**:Agent 回信时 `from_workspace` 存的是 Agent 名而不是 * 路径(历史遗留),拿它拼会得到 `dsh@dsh.alias` 这种投不出去的东西。 * 人类发件人本来就没有工作目录。 * * 别名为空时退回 `name`(默认会话)而不是编一个 —— 但注意这与「投回同一条会话」 * 不等价,默认会话是该 name 当前最活跃的那条。调用方要区分时看返回值有没有 `.`。 * * @param {any} mail 一封邮件 * @param {string} [alias] 会话别名,缺省取 mail.session_alias * @returns {string} */ export function replyAddressFor(mail, alias) { const a = alias ?? mail?.session_alias ?? ''; return formatAddress(mail?.from_name, '', a); } /** * 给出自己在这条会话里的地址,供转发说明或向第三方引用时使用。 * * 用 `to_workspace`(自己那个地址的 path 位)而不是发件人的: * 抄送给 `opencode@/a` 与主发给 `dsh@/b` 是两个不同的工作区。 * * @param {any} mail 一封邮件 * @param {string} selfName 自己的 Agent 名 * @param {string} [alias] 会话别名,缺省取 mail.session_alias * @returns {string} */ export function selfAddressFor(mail, selfName, alias) { const a = alias ?? mail?.session_alias ?? ''; // 抄送方拿到的 to_workspace 是主收件人的,自己的 path 在 cc_list 里。 // 不取对的那个会让「我是谁」这句话指向别人的工作目录。 let path = mail?.to_workspace ?? ''; if (mail?.to_name !== selfName && Array.isArray(mail?.cc_list)) { const mine = mail.cc_list.find(c => c?.name === selfName); if (mine) path = mine.path ?? ''; } return formatAddress(selfName, path, a); } /** * 列出这封邮件的全部参与方及各自可投递的地址。 * * 这是「回给抄收方」缺的那块信息:知道有谁,**以及用什么地址找到他**。 * 抄送方的 path 取它自己那个地址的 path 位。 * * 自己会被标 `is_self`,而不是从列表里剔掉 —— 剔掉的话模型无法确认 * 「这封信是不是也发给了我」,也就无法判断自己是不是该回。 * * @param {any} mail 一封邮件 * @param {string} [selfName] 自己的名字,用于标记 is_self * @param {string} [alias] 会话别名,缺省取 mail.session_alias * @returns {{role: string, name: string, path: string, address: string, is_self: boolean}[]} */ export function participantsOfMail(mail, selfName, alias) { const a = alias ?? mail?.session_alias ?? ''; const self = String(selfName ?? '').trim(); const out = []; const add = (role, name, path) => { const n = String(name ?? '').trim(); if (!n) return; out.push({ role, name: n, path: String(path ?? ''), address: formatAddress(n, path, a), is_self: !!self && n === self, }); }; // 发件人一侧 path 留空,理由同 replyAddressFor add('from', mail?.from_name, ''); add('to', mail?.to_name, mail?.to_workspace); if (Array.isArray(mail?.cc_list)) { for (const c of mail.cc_list) add('cc', c?.name, c?.path); } return out; }