Commit Graph

5 Commits

Author SHA1 Message Date
55b3f9bc4e fix(web): 地址显示按「人 / Agent」分维度 —— 别名跟 Agent 走,人只显示名字
## 症状

单封邮件的元信息三行都不对(生产实测那封 12:12:12):

    发件  jianf.邮件驱动·多智能体协作平台-完整设计文档-一、项目概述-11-项目定位
    收件  pi@/home/program/agentmail
    抄送  pi@/home/program/agentmail.new

人指定的是「投进 pi 的那条会话」,而界面把会话别名拼给了**发件人**。

## 三处错

**1. 别名拼错了一方。** `name@path.session` 三段才唯一确定「哪个 Agent、
在哪个目录、哪条线索」—— 别名必须跟 Agent 走。拼给发件人之后收件人变成
`pi@/home/program/agentmail`,那指向**默认会话**而不是人指定的那条。

**2. 人不该有目录和会话位。** 人没有工作目录,发给人就是进收件箱。
`jianf.某会话` 是把 Agent 的三维语义硬套在人身上,而且因为 from_workspace
为空,拼出来的形态连 ParseAddress 都还原不了 —— 没有 `@` 时整串被当成
**名字**(实测 name="jianf.某会话别名"),投递必然 404。

**3. 抄送残留 `.new`。** 它是一次性动作,建完会话就失效;留着会让人以为
再发一次还能投进同一条会话,实际会开出第三条。

## 修法

`identityAddress` → `participantAddress(name, workspace, alias)`,
判据是有没有 workspace:

    Agent → pi@/home/program/agentmail.日程提醒:…    三段齐全
    人    → jianf                                     裸名字

`ccAddress` 按同一判据分流;`.new` 换成当前会话别名。
六处手工拼接(MailView / MailList ×2 / ThreadView)统一走这两个函数。

## 顺带修掉 `dsh@dsh`

改的时候实测发现:**`mails.from_workspace` 对 Agent 存的是 Agent 名而不是
路径**(历史遗留,见 db/migrate.go 里 sessions.workspace 的注释)。
拿它当路径拼,Agent 发来的信显示成 `dsh@dsh`。

会话的 workspace 才是权威来源 → `models.Mail` 新增 `SessionWorkspace`,
六处查询补 `s.workspace`:GetMailByID / ListInbox / GetSessionMails /
GetSessionMailByID / ListSentBy / threadCols。

## formatAddress 与后端对齐

第一版我改成「path 为空时舍弃 session 返回裸名字」,对着后端 ParseAddress
跑了一遍才发现搞反了 —— **正确形态是保留 `@`**:

    jianf@.任务  → name=jianf path="" session=任务   ✓
    jianf.任务   → name="jianf.任务"                  ✗

现在两端六个 case 逐例一致(这个分支只在内部逻辑上用得到;
展示一律走 participantAddress,人根本不带会话位)。

## 取舍

列表行与对话树节点**不带会话位**:列表的分组头已单独显示别名,
树的每个节点都在同一条线索上 —— 重复无信息量,而 92 字节的别名会把那行挤没。

## homeagent 日程工具的两个修复(同批)

**查询串手拼吃掉了时区。** RFC3339 的 `+08:00` 里那个 `+` 在查询串里正是
空格的转义形式,服务端 ParseQuery 还原成空格 → time.Parse 失败 →
AgentListCalendarEvents **静默退回默认区间**(不报错)。表现为「明明有日程
却说一条都没有」。改走 url.Values.Encode()。

**默认窗口 3 个月太窄。** yearly / lunar_yearly 的下一次触发随时落在窗口外,
模型问「我建过什么」得到空结果,然后照着空结果再建一条重复的。改成 14 个月。
空结果的话术也从「你还没有建过日程」改成说出实际查询区间 —— 前者在窗口外
有事件时是假话。

## 验收

- web 182 例(replyTarget 24 → 46);tsc 无错;Gateway 7 包全过
- 新增 test/manual/addr-verify.mjs:真渲染两个方向都验过
    人 → Agent:jianf / pi@/home/program/agentmail.日程提醒:…
    Agent → 人:dsh@/home/program/agentmail.查看工程与插件适配指南 / jianf
  判据含「Agent 的 path 必须是真路径而不是 Agent 名」(锁 dsh@dsh 那个 bug)
2026-09-04 13:49:16 +08:00
5b76ac5c57 feat(homeagent): 四个日程工具 + 工具计数不再硬编码
create_schedule / list_schedules / update_schedule / delete_schedule,
插件从 11 个工具变 15 个。

时间格式是这组工具最容易出错的地方,parseEventTime 接受三种形态:
完整 RFC3339(推荐)、无时区(按本机时区解释)、只有日期(当天 9 点,
比零点有用 —— 零点的提醒没人看)。

**刻意不接受自然语言**(「明天九点」):解析它需要知道模型以为今天是哪天,
而它上下文里那个日期常常是错的 —— 一个静默偏移一天的提醒比报错糟得多。
解析失败的报文里带上当前本机时间,让它能自己算。

其他细节:
- recipients 用逗号分隔的 string 而非数组:几个平台对数组参数的 schema
  支持不一致,而逗号分隔在所有平台上都是普通 string。中英文逗号都收 ——
  中文输入法下打出「,」是常态。
- argInt 同时收 float64 与字符串:"30" 带引号会让「提前 30 分钟」静默失效。
- update 的工具描述里写明「只传要改的字段」:模型的默认倾向是回传它记得的
  全部字段,记错一个就覆盖掉原有的提醒正文或收件方。
- list 在接近上限(>=75%)时主动提示清理,而不是等撞墙 —— 撞墙那一刻
  模型往往正在做别的事,没有余裕去清理。
- scheduleEvent.effectiveRecipients() 与服务端同一条兜底链:只读
  recipients 会让旧数据显示成「发给:」后面空白。

**工具计数从 RegisterTool 的调用数派生,不硬编码。**
之前写死 13 而实际注册 11 —— 排查「工具没生效」时日志说 13、平台说 11,
两个数字都不可信,白花了一轮时间。加工具时忘改常量是必然的,
所以让它没有机会写错。

plugin.go 另补 put/delete 两个 HTTP 辅助(原来只有 get/post)。

已部署验证:homed 日志「注册完成(15 个工具 + 1 个输出通道)」,
模型实际调用 list_schedules 成功。
2026-09-04 06:28:55 +08:00
473c46659a fix(homeagent): 删除 plugin.go 里与 tools.go 重复的 6 个方法
plugin.go 与 tools.go 各自声明了同名工具处理器。逐个 diff 后保留
tools.go 的版本 —— 它严格更丰富:read_thread 多 ParentHid → parent_hidden
并区分「父邮件无权查看」与「父邮件尚未加载」,suggest_address 多解析
candidates,connect_to_server 多一条注释。

plugin.go 删除 handleConnectToServer / handleForwardMail /
handleSuggestAddress / handleListContacts / handleSessionParticipants /
handleReadThread + minInt。

tools.go 删除本地 min():go.mod 是 go 1.25.0,内置 min 可用,
本地定义只是遮蔽。

部署验证:11 项工具注册(日志里硬编码的「13 个工具」是错的数字)。
2026-09-03 21:11:15 +08:00
f321380fa3 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 已纳入校验)
2026-09-03 15:01:52 +08:00
fc33087982 feat: homeagent-mail-bridge —— TrueAgent 接入 AgentMail(MVP)
## 架构

TrueAgent 是单一常驻 agent 事件循环,没有 per-conversation session。
插件像 QQ 插件一样:所有邮件注入同一个事件循环,靠中断消息文本传递 context。
不做 platform_sessions 上报(无 sessions 可映射)。

## 工具

- read_inbox: 收件箱查询(渲染发件人/主题/正文/抄送/附件)
- send_mail: 三维地址发信(支持 reply_to)
- output_send__homeagent 输出通道(agent 可主动发信)

## 自动回信

InjectInputSync(阻塞)等待 agent 处理完毕 → 自动把 FinalText
发回给发件人(reply_to 指向原邮件)。agent 不需要记得调 send_mail。

试过 StageAfterOutput hook,但 before_output 看不到 ToolCalls
(read_inbox 调用在 turn 中间就被清掉了),导致自动回信不触发。
InjectInputSync 更可靠:TrueAgent 保证不重入。

## 基础设施

- 心跳 25 秒
- SSE 长连 + 自动重连(3 秒退避)
- 注册时自动注册密钥(AGENTMAIL_AGENT_KEY 环境变量)
- systemd drop-in 注入 AgentMail 环境变量

## 构建

plugindev build → dist/homeagent_linux_amd64.hmap
安装到 /home/newqqagent/plugins/homeagent-mail-bridge/
homeagent.service drop-in: /etc/systemd/system/homeagent.service.d/agentmail.conf

## 端到端验证

admin → homeagent@/tmp/homeagent.new(自动回信验证)
→ homeagent 读取邮件 + 自动回信「收到确认」→ 25 秒往返完成
2026-09-03 12:57:43 +08:00