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),未手工拼写
This commit is contained in:
141
plugins/opencode-mail-bridge/lib/addressing.js
Normal file
141
plugins/opencode-mail-bridge/lib/addressing.js
Normal file
@ -0,0 +1,141 @@
|
||||
/**
|
||||
* 三维寻址的构造与判读 —— 所有平台插件共用。
|
||||
*
|
||||
* 为什么这些函数必须共用、且必须是纯函数:
|
||||
*
|
||||
* 地址拼错不会报错。`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;
|
||||
}
|
||||
Reference in New Issue
Block a user