Files
MailUI4Agents/plugins/pi-mail-bridge/lib/addressing.js
JianFeeeee e6fd2fafdc feat: agent 邮件寻址能力全面补齐 + .new 别名替换
## 别名替换(让 .new 邮件可寻址)

repo/autoalias.go: AutoAliasFor + EnsureSessionAlias
- .new 建完会话立刻给别名(形如 dsh-重构导入路径)
- 名字与主题都要:只用主题跨 Agent 撞名,只用名字看不出聊什么
- sanitizeAliasPart 只留 unicode.IsLetter/IsDigit,其余折 -
- 撞名追加 -2/-3,全占用退 session-<uuid前8位>
- 不复用 SyncSessionAlias:那个假定已存在且跳过 manual
- 条件写入 WHERE alias IS NULL OR '',并发安全
- resolveTarget 的 .new 与默认会话两条路径都调

notifyRecipients 加三个字段(每个收件方拿到自己那个地址的版本):
- session_alias / reply_address / self_address
- 别名为空时退回省略 session 位,绝不写 new

FormatAddress(name,path,session) 空 path 也必须留 @ 与 .

## Agent 侧寻址发现(五个只读端点)

handler/agent_discovery.go:
- /agent/contacts + /agent/contacts/suggest(三段式补全)
- /agent/mail/{id} + /agent/mail/{id}/thread
- /agent/sessions/{id}/participants
- 不复用人类路由:scope 不同、审计需求不同
- 一律只读:归档/改名/权限决策仍只有人能做

repo/participants.go: SessionParticipants 逐封扫 from/to/cc
- Roles 用集合、MailCount 只数发信(0=还没开口的人)
- 发件人 path 不取 from_workspace(那列存的是 Agent 名)

repo.SuggestPaths 重写:mails.to_workspace(按 MAX(created_at) 倒序)
+ agents.workspaces 并集。原只读 workspaces,官方插件传 [] 永远空

## 共用模块(三插件逐字节相同)

lib/addressing.js: formatAddress/roleOf/replyAddressFor/selfAddressFor/participantsOfMail
lib/discovery.js: renderNameSuggestions/renderPathSuggestions/renderSessionSuggestions/
                  renderParticipants/renderContacts/renderThread

lib/inbox-format.js: renderMail 新增收件人/身份/可投递地址三段
  - selfName 参数(兼容旧调用不传的情况)

check-shared-libs.sh 纳入 addressing + discovery

## 插件侧

opencode: suggest_address + list_contacts + session_participants + read_thread + read_mail
dsh: 同上 + forward_mail(此前只有 opencode 有)+ upload_attachment 改真 multipart
pi: 同上(createMailTools 加 agentName 参数)

dsh: ctx.agents.create id collision 改为 readSession 探测后 resume
dsh: 关键路径日志改 console.error(ctx.logger 不进 journalctl)

## 测试

repo: autoalias_test.go 11 + participants_test.go 7 = 18 例
plugins: addressing.test 17 + discovery.test 23 + inbox-format.test 31 = 71 例
go test ./... + npm test(opencode 155 + dsh 173 + pi 199)全绿
端到端验证:admin 发 dsh@....new 抄送 opencode@....new
  → dsh 用 session_participants 取到地址 → send_mail 给 opencode
  → 地址取自工具返回值(.crisp-planet),未手工拼写
2026-09-03 12:09:12 +08:00

142 lines
5.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 三维寻址的构造与判读 —— 所有平台插件共用。
*
* 为什么这些函数必须共用、且必须是纯函数:
*
* 地址拼错不会报错。`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;
}