## 别名替换(让 .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),未手工拼写
75 lines
3.1 KiB
JavaScript
75 lines
3.1 KiB
JavaScript
/**
|
||
* 启动补拉:把插件离线期间到的邮件变成与 SSE 事件同形的投递任务。
|
||
*
|
||
* 为什么需要它:**SSE 只推连上之后的事件**。插件重启前发来的邮件不会再推一次,
|
||
* 心跳响应的 `pending_mails` 是唯一线索。不补拉的后果是那封邮件永远躺在
|
||
* 收件箱里,而发件人以为 Agent 收到了 —— 这比明确的失败更难排查。
|
||
*
|
||
* 两个平台共用,必须逐字节相同(deploy/check-shared-libs.sh 校验)。
|
||
*/
|
||
|
||
/**
|
||
* 一次补拉最多处理几封。
|
||
*
|
||
* 上限存在的理由:每封都要起一轮模型。攒了 80 封的时候一次性全放出去,
|
||
* 等于对上游打 80 个并发请求,且最后那几封要等前面全部跑完。
|
||
* 超出的部分留在收件箱里,下次重启或人工触发时再处理。
|
||
*/
|
||
export const MAX_CATCHUP = 5;
|
||
|
||
/**
|
||
* 把收件箱里的一封邮件转成 SSE `new_mail` 那个形状。
|
||
*
|
||
* 补拉与 SSE 走同一条投递路径(deliverMail),因此形状必须一致 ——
|
||
* 两条路径各写一遍投递逻辑的话,某一条上的修复会漏掉另一条。
|
||
*
|
||
* @param {any} mail `/mail/inbox` 返回的一行
|
||
* @returns {{mail_id: string, session_id: string, from_name: string,
|
||
* subject: string, mail_type: string, role: string,
|
||
* to_workspace: string, catchup: true}}
|
||
*/
|
||
export function mailToEvent(mail) {
|
||
return {
|
||
mail_id: mail?.mail_id || '',
|
||
session_id: mail?.session_id || '',
|
||
from_name: mail?.from_name || '',
|
||
subject: mail?.subject || '',
|
||
mail_type: mail?.mail_type || 'normal',
|
||
role: 'to',
|
||
to_workspace: mail?.to_workspace || '',
|
||
// 标记来源,投递侧可据此决定是否在提示词里说明「这是积压的邮件」
|
||
catchup: true,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 从收件箱挑出该补投的邮件。
|
||
*
|
||
* @param {any[]} mails `/mail/inbox?status=unread` 的结果
|
||
* @param {Set<string>} seen 已经通过 SSE 投过的 mail_id(避免重复投递)
|
||
* @param {number} [max] 上限,默认 MAX_CATCHUP
|
||
* @returns {any[]} 与 SSE 事件同形的投递任务,按时间正序(老的先处理)
|
||
*/
|
||
export function selectCatchup(mails, seen, max = MAX_CATCHUP) {
|
||
if (!Array.isArray(mails) || mails.length === 0) return [];
|
||
|
||
const picked = [];
|
||
for (const m of mails) {
|
||
const id = m?.mail_id;
|
||
if (!id) continue;
|
||
// 心跳与 SSE 建连之间有个窗口:那期间到的邮件既在 pending_mails 里、
|
||
// 也会被 SSE 推一次。不去重就会投两遍,模型回两封信。
|
||
if (seen && seen.has(id)) continue;
|
||
// permission 类邮件不补投:它是给人看的询问,Agent 侧没有可恢复的上下文
|
||
// (原来的工具调用早随进程一起没了),投过去只会让模型困惑。
|
||
if (m?.mail_type && m.mail_type !== 'normal') continue;
|
||
picked.push(m);
|
||
}
|
||
|
||
// 收件箱按时间倒序返回,补投要按正序 —— 先来的先处理,
|
||
// 否则同一会话里的多封邮件会被倒着塞进去,上下文顺序是乱的。
|
||
picked.reverse();
|
||
|
||
return picked.slice(0, Math.max(0, max)).map(mailToEvent);
|
||
}
|