Files
MailUI4Agents/plugins/pi-mail-bridge/lib/catchup.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

75 lines
3.1 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.

/**
* 启动补拉:把插件离线期间到的邮件变成与 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);
}