fix: relay 死循环防护 + DSH 工作区注册修复 + homeagent 工具集补齐 + 三插件 connect_to_server
## relay 死循环防护(两道防线) ### 主防线:免配额只给发往人类的 relay(handler/mail.go) 原设计:relay 走免配额通道(harness 搬运不该算模型自主发信)。 问题:收件方是另一个同样会自动转发的 Agent 时,整个回路里没有任何 一处在计数——生产上跑出过 41 封(会话 f3d824ce),间隔从 15 分钟 缩到 5 秒,且用了 37 封才烧掉 4/20 预算。 改为:repo.IsHumanUser(to.Name) 判定。Agent→Agent 的 relay 照样扣预算。 顺带修次序问题:原来是「先占幂等键再扣预算」,预算耗尽时幂等键 已被占用,加了额度也无法重发。现在预算失败会 ReleaseRelay 还回去。 ### 兜底:hop_limit 列接通(repo/relayhops.go) schema 里早有 hop_limit INT DEFAULT 5,从未有代码读它。 CountTrailingRelayHops 从最新邮件往前扫,遇到第一封非 relay 邮件即停(中间有一封自主发信或人类插话就归零)。 5 测试:空会话 / 只数 relay / 自主发信打断归零 / 达到上限 / 按会话独立 ## DSH 工作区注册修复 问题:上一轮加的 workspaceRegistry.create(cwd) 用了兜底值 cwd(来自 resolveWorkspaceCwd,可能是 ~/.dsh/mail-sessions/mail-<uuid>), 而不是会话 header 里的真实 cwd。两者不一致时 attachSession 拒绝, 且 create 已先执行,每封邮件都往注册表里塞一条空的垃圾 workspace。 修复:读 handle.agent.session.header.cwd —— create 路径下是 meta.cwd, resume 路径下是持久化 header 里那个。 ## homeagent 插件:11 工具齐平 opencode tools.go 新增:read_mail / forward_mail / suggest_address / list_contacts / session_participants / read_thread / connect_to_server + handleConnectToServer(注册到 Gateway 前先用候选坐标试注册, 成功才写回 p.gwURL/p.key,失败不破坏原配置) 关键修:Plugin.name(插件名,homed 注册用)与 Plugin.agentName (AgentMail 身份,Gateway 密钥绑定用)是两个命名空间。 它们混淆会导致 403:「该密钥已绑定到 Agent 'homeagent',不能用于 注册 'homeagent-mail-bridge'」。现已分开,并在 systemd drop-in 里显式设 AGENTMAIL_AGENT_NAME=homeagent。 ## 三插件补齐 connect_to_server 之前只有 opencode 有。后果:Gateway 换地址或密钥需要重新登记时, opencode 里的模型能自己修好,其他平台只能干等环境变量被人改。 DSH 版:从 GatewayClient 内部调 register(),成功后写回 client.baseURL 与 client.agentKey 当场生效。 pi 版:新导出 KEY_FILE / saveLocalKey(从 gateway.mjs),connect 工具直接用。 # 测试 relayhops_test.go 5 例 opencode 172 / dsh 188 / pi 214 全绿 check-shared-libs.sh 三方同源(rename-proposal 已纳入校验)
This commit is contained in:
@ -33,6 +33,7 @@ import {
|
||||
noteExplicitSend,
|
||||
shouldSkipAutoRelay,
|
||||
} from "./lib/relay-dedup.js";
|
||||
import { appendRenameProposal, renameProposalNote } from "./lib/rename-proposal.js";
|
||||
|
||||
const GATEWAY_URL = process.env.AGENTMAIL_GATEWAY_URL || "http://127.0.0.1:8180";
|
||||
const AGENT_NAME = process.env.AGENTMAIL_AGENT_NAME || "opencode";
|
||||
@ -170,14 +171,11 @@ const sendMailTool = {
|
||||
// 让 session.idle 的自动转发让位,避免同一件事发两封。
|
||||
async execute(args, context) {
|
||||
// 改名建议以 HTML 注释形式附在正文末尾,由网关解析后剥离。
|
||||
// 选注释而不是自造标记:react-markdown 不解析 raw HTML,
|
||||
// 万一网关没剥掉,它在页面上也只是一行不显眼的转义文本而非破版内容。
|
||||
let body = args.body;
|
||||
if (args.propose_alias) {
|
||||
const esc = (v) => String(v).replace(/"/g, ""); // 双引号是标记的定界符
|
||||
const reason = args.propose_reason ? ` reason="${esc(args.propose_reason)}"` : "";
|
||||
body += `\n\n<!-- agentmail:rename-session alias="${esc(args.propose_alias)}"${reason} -->`;
|
||||
}
|
||||
// 标记格式是服务端正则的镜像,因此拼接收在 lib/rename-proposal.js
|
||||
// (三平台共用)—— 各写一遍的话少个空格就静默失效:邮件照常发出,
|
||||
// 提议凭空消失,而模型以为自己提过了。
|
||||
const { body, proposed } = appendRenameProposal(
|
||||
args.body, args.propose_alias, args.propose_reason);
|
||||
|
||||
const result = await apiPost("/mail/send", {
|
||||
to: args.to,
|
||||
@ -205,11 +203,11 @@ const sendMailTool = {
|
||||
? "预算即将用尽,请尽快给出结论;自动转发的总结不占预算。"
|
||||
: "")
|
||||
: "";
|
||||
// 回传规范化后的别名:Agent 提的名字可能含非法字符被改写过
|
||||
const proposed = result.rename_proposed
|
||||
? `\n已向用户提议把会话改名为 ${result.rename_proposed},等待其确认。`
|
||||
: "";
|
||||
return `已发送。Mail ID: ${result.mail_id},Session: ${result.session_id}${alias}${budget}${proposed}`;
|
||||
// 别名取服务端回的 rename_proposed:它跑过 normalizeAlias,
|
||||
// 回显本地值会让模型记住一个不存在的名字
|
||||
const note = renameProposalNote(result.rename_proposed, args.propose_alias, proposed);
|
||||
return `已发送。Mail ID: ${result.mail_id},Session: ${result.session_id}${alias}${budget}` +
|
||||
(note ? `\n${note}` : "");
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user