/** * 邮件会话 → DSH 会话 id 的确定性派生。 * * # 为什么需要它 * * `session_update`(人在 WebUI 里改权限档位)必须找到**正在运行**的那条 DSH * 会话才能立刻生效。查找原来只走 `sessionMap`,而那张表是纯内存的 —— * 插件重启后为空。 * * 于是一个具体的坏情形:人把一条 full 会话在界面上改回 workspace, * 插件恰好刚重启过、那条会话还没收到新邮件 → `sessionMap` 没有它 → * 这条更新被静默忽略,DSH 运行时仍按 full 执行。人以为自己收紧了权限。 * * ⚠ **这一格的后果是安全相关的,不是"技术限制"**(pi 2026-09-14 裁定): * 接管会话在重启后**定位不到**,意味着一次**收紧**(full → workspace)可能被 * **静默延迟** —— 而"延迟"对只被收紧、之后再没有新邮件的会话等于**永不生效** * (兜底是"下次投递按邮件里的档位重新 apply",它依赖将来还有邮件)。 * 方向是收紧,所以措辞必须按"权限可能没按你以为的那样收紧"来说, * 而不是"档位热更新定位不到"这种技术性说法 —— 后者会让人以为只是界面问题。 * * # 派生规则 * * 建会话时(`deliverMail` 的新建分支)id 是确定性的: * - 首次尝试:`mail-<邮件会话 id>` * - 模型降级重试:`mail-<邮件会话 id>-r`(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--r1 / -r2 / … const suffix = id.startsWith(`${base}-r`) ? id.slice(base.length + 2) : ''; return suffix.length > 0 && /^\d+$/.test(suffix); } /** * 从一批候选会话里挑出属于该邮件会话的那条。 * * 优先 `mail-`(首次尝试),其次序号最小的 `-r` —— 与 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; }