// 自动转发去重的纯逻辑。 // // 单独一个文件而不是放在 index.js 里导出:**opencode 会把插件入口模块的 // 每一个导出都当成插件工厂**(`Object.values(mod)` 逐个检查是不是函数), // 多导出一个 Map 就会让整个插件加载失败: // ERROR message="failed to load plugin" error="Plugin export is not a function" // 实测踩过 —— 插件静默不加载,邮件全都投不进去。 // 因此入口文件只能 `export default`,其余东西一律搁在这里。 /** 取三维地址的名字段:admin@root.alias -> admin */ export function addrName(addr) { return String(addr || "").split("@")[0].trim(); } /** * 本轮内模型**自己调 send_mail** 发出去的信(按 opencode 会话)。 * * session.idle 的自动转发要据此让位:模型已经亲手回过这条线索了, * 再把它最后那段话转一遍,收件箱里就是两封内容几乎一样的邮件。 * 生产实测过这个后果 —— 同一轮里 311 字节和 342 字节各一封, * 说的是同一件事,其中带附件的那封才是模型真正想发的。 * * 为什么不靠 relay_key 幂等:那个键是 assistant message id, * 保证的是「同一条消息不被转两次」,管不了「模型已经自己发过了」。 * * 窗口是「一轮」:deliverMail 投递新邮件时清空(新一轮开始), * relaySummary 用完即清。 */ export const explicitSends = new Map(); // opencode session id -> { names:Set, replyTos:Set } /** 记下模型这一轮主动发了信,给谁、回的哪封。 */ export function noteExplicitSend(sessionID, to, replyTo) { if (!sessionID) return; let rec = explicitSends.get(sessionID); if (!rec) { rec = { names: new Set(), replyTos: new Set() }; explicitSends.set(sessionID, rec); } const name = addrName(to); if (name) rec.names.add(name); if (replyTo) rec.replyTos.add(String(replyTo)); } /** * 本轮是否该跳过自动转发。 * * @param sent 该会话本轮的主动发信记录 { names:Set, replyTos:Set },可为空 * @param replyTo 自动转发本来要发给谁(三维地址或纯名字) * @param mailID 自动转发本来要 reply_to 的邮件 id */ export function shouldSkipAutoRelay(sent, replyTo, mailID) { if (!sent) return false; // 收件人同名:模型已经跟这个人说过了 if (sent.names.has(addrName(replyTo))) return true; // 同一封信已被回过:即使收件人写法不同(别名/路径不同)也算回过 if (mailID && sent.replyTos.has(String(mailID))) return true; return false; }