问题(⑧):守护进程用 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 按设计排除。
73 lines
2.6 KiB
JavaScript
73 lines
2.6 KiB
JavaScript
/**
|
||
* 邮件会话 → 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;
|
||
}
|