Files
MailUI4Agents/plugins/dsh-mail-bridge/lib/mail-session-id.js
JianFeeeee 19a3161ee4 feat(pi): 交互式 pi 会话接入邮件工具(send_mail/read_inbox 等 10 个)
问题(⑧):守护进程用 noExtensions:true 起会话,它的邮件工具只给模型在邮件
会话里用;人在 TUI 里敲的 pi 拿不到。结果是平台的建设者自己收不到邮件 ——
一个「邮件驱动」的平台,维护者只能绕到 curl + 密钥直连 Gateway 才能看收件箱。

新增 plugins/pi-mail-bridge/extension/index.ts:把同一套工具(createMailTools)
注册到交互式会话。两者是同一条 AgentMail 身份(agent pi)的两个入口,与 DSH 的
「TUI + 邮箱是同一个 Agent」一致。

密钥解析顺序(交互式 pi 的环境里没有 AGENTMAIL_*):
  1. 进程环境
  2. AGENTMAIL_ENV_FILE(默认 /etc/agentmail/pi.env)—— 与守护进程同一把密钥,
     因此身份一致
  3. AGENTMAIL_CONFIG_DIR/agent.key 或 ~/.agentmail/agent.key
     (兼容 key 与 key_token 两种字段名;实测本机文件用的是 key_token,
      只认 key 会静默读不到)
拿不到密钥时不注册任何工具并明确告知 —— 挂一组永远 401 的工具比没有更糟。

不注册 connect_to_server:它会重写 Gateway 坐标并重新登记密钥,而交互式会话与
守护进程共用同一身份,一次 TUI 对话不该改到守护进程的配置。

为什么不会重复注册(读 SDK 实现确认,并用探针实测):
  resource-loader.js 里 noExtensions 为真时只用 cliEnabledExtensions,
  settings.json 的 extensions 数组被排除 —— 即 noExtensions:true 只加载
  命令行 -e 传入的扩展。
  探针:noExtensions=true → 扩展数=0;false → 16 个且含 pi-mail-bridge。

deploy/install.sh 增加幂等的扩展注册步骤(写入 settings.json 的 extensions)。

验证:headless pi 实际调用 read_inbox 返回真实邮件主题;工具清单含
send_mail/read_inbox/read_mail/forward_mail/upload_attachment/download_attachment/
suggest_address/list_contacts/session_participants/read_thread(10 个),
connect_to_server 按设计排除。
2026-09-11 11:32:47 +08:00

73 lines
2.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.

/**
* 邮件会话 → DSH 会话 id 的确定性派生。
*
* # 为什么需要它
*
* `session_update`(人在 WebUI 里改权限档位)必须找到**正在运行**的那条 DSH
* 会话才能立刻生效。查找原来只走 `sessionMap`,而那张表是纯内存的 ——
* 插件重启后为空。
*
* 于是一个具体的坏情形:人把一条 full 会话在界面上改回 workspace,
* 插件恰好刚重启过、那条会话还没收到新邮件 → `sessionMap` 没有它 →
* 这条更新被静默忽略,DSH 运行时仍按 full 执行。人以为自己收紧了权限。
*
* # 派生规则
*
* 建会话时(`deliverMail` 的新建分支)id 是确定性的:
* - 首次尝试:`mail-<邮件会话 id>`
* - 模型降级重试:`mail-<邮件会话 id>-r<i>`(i 从 1 开始)
*
* 接管会话(adopted)是唯一的例外:那条 DSH 会话 id 是平台自己生成的,
* 从邮件会话 id **推不出来**,只能靠内存映射。重启后接管会话的档位热更新
* 确实无法定位 —— 这是已知取舍,不是这里能修的。
*/
/** 首次尝试使用的 DSH 会话 id。 */
export function dshSessionIdForMail(mailSessionID) {
return `mail-${String(mailSessionID ?? "")}`;
}
/**
* 判断一个 DSH 会话 id 是否属于某条邮件会话。
*
* @param {string} dshSessionId DSH 侧会话 id
* @param {string} mailSessionID AgentMail 侧会话 id
* @returns {boolean}
*/
export function matchesMailSession(dshSessionId, mailSessionID) {
const id = String(dshSessionId ?? "");
const base = dshSessionIdForMail(mailSessionID);
if (id === base) return true;
// 模型降级重试:mail-<id>-r1 / -r2 / …
const suffix = id.startsWith(`${base}-r`) ? id.slice(base.length + 2) : "";
return suffix.length > 0 && /^\d+$/.test(suffix);
}
/**
* 从一批候选会话里挑出属于该邮件会话的那条。
*
* 优先 `mail-<id>`(首次尝试),其次序号最小的 `-r<i>` —— 与 deliverMail
* 的尝试顺序一致,而不是数组顺序。
*
* @param {string[]} dshSessionIds
* @param {string} mailSessionID
* @returns {string|undefined}
*/
export function pickMailSession(dshSessionIds, mailSessionID) {
const base = dshSessionIdForMail(mailSessionID);
const list = Array.isArray(dshSessionIds) ? dshSessionIds.map(String) : [];
if (list.includes(base)) return base;
let best;
let bestIndex = Infinity;
for (const id of list) {
if (!matchesMailSession(id, mailSessionID)) continue;
const idx = Number(id.slice(base.length + 2));
if (idx < bestIndex) {
bestIndex = idx;
best = id;
}
}
return best;
}