Files
MailUI4Agents/server/internal/handler/toworkspace.go
JianFeeeee a696b2a141 fix(handler): 回信的 to_workspace 从会话 workspace 继承 —— 修「每封邮件多一条会话」
# 现象(生产实测)

在 DSH 界面上观察到的:每处理一封邮件就多出一条独立会话。

# 根因

`to_workspace` 是插件唯一能知道「这个任务该在哪个目录干活」的入口,而它取的是
**地址里的 path 位**。Agent 之间的回信、以及人在对话页点回复,地址里通常没有
path 位 —— 平台下发的 `reply_address` 就是这个形状(`FormatAddress(replyTo, "", alias)`)。

空着传下去的后果是可观测的:插件只能自己拼一个临时目录,于是**每封邮件落在一个
不同的空目录**里;DSH / opencode 按 cwd 给会话分组,界面上就成了「每处理一封邮件
就多出一条未分组会话」,而模型在那个空目录里什么项目文件也看不到。

实测取证:
  - 线上 5 个兜底目录 `~/.dsh/mail-sessions/mail-*` **全部是空的**(0 条目)
  - 全天 journalctl 里**没有任何**相关告警(代码用的是 `ctx.logger.warn`,
    而同一文件别处明确写着 DSH 的 logger 不进 journalctl)→ 完全静默
  - 走兜底的那条会话(8e982e96)里,`dsh → opencode` 那封 `to_workspace` 有值,
    而 `opencode → dsh` 的回信 **to_workspace 全为空** —— 而该会话自身的
    `sessions.workspace` 一直是有值的

# 修法

会话的 workspace 才是权威来源(见 models.SessionWorkspace 的注释):回信本来就是
回给**那条会话**的,而那条会话知道自己属于哪个项目。规则抽成纯函数
`resolveToWorkspace(addrPath, sessionWorkspace, toIsHuman)`:

  1. 地址里写了 path → 照用(人的明确意图优先)
  2. 没写且收件方是**人** → 保持空(人没有工作目录;填了前端会拼出
     `gui-lab@/path.别名` 这种错地址,ToHuman 字段就是为此加的)
  3. 没写且收件方是 Agent → 用会话的 workspace

**改的是 `to`,不是只改建库那一行**:同一个值还进投递载荷(`to_workspace` /
`self_address`)。改一处另一处不改,会出现「API 读到的与插件推到的不是同一个
目录」——那正是本项目一直在治的静默不一致。

`notify/mail.go` 只加了一段注释说明 reply_address 的 path 位为何**刻意留空**
(它的语义是「**发件人**该在哪儿干活」),免得后人以为那是漏填。

# 测试

- `handler/toworkspace_test.go`:6 条规则用例 + 1 条**反向对照**
  (固定其他输入只翻转 toIsHuman,要求结果必须不同 —— 防止该参数被忽略后
  「给人也填 path」静默回归)
- `repo/session_workspace_test.go`:锁住**列名与真实 schema**。这个查询读不到时
  按设计返回空串,与「这条会话没有工作目录」无法区分 → 列名写错的功能表现是
  「看起来还在跑,只是工作目录永远继承不到」
2026-09-12 11:19:19 +08:00

39 lines
1.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package handler
// resolveToWorkspace 决定一封邮件落库与投递时用的 `to_workspace`。
//
// # 它要解决的问题
//
// `to_workspace` 是插件唯一能知道「这个任务该在哪个目录干活」的入口。而 Agent
// 之间的回信、以及人在对话页里点回复,三维地址里通常**没有 path 位** —— 平台
// 下发的 `reply_address` 就是这个形状。空着传下去的后果是可观测的:
//
// - 插件只能自己拼一个临时目录,于是**每封邮件落在一个不同的空目录**里;
// - DSH / opencode 按 cwd 给会话分组,界面上就成了「每处理一封邮件就多出
// 一条未分组会话」;
// - 模型在那个空目录里什么项目文件也看不到,却照样开工。
//
// 会话的 workspace 才是权威来源(见 models.SessionWorkspace 的注释):回信本来
// 就是回给**那条会话**的,而那条会话知道自己属于哪个项目。
//
// # 三条规则,顺序不能换
//
// 1. 地址里写了 path → **照用**。那是人的明确意图,会话的 workspace 只是兜底。
// 2. 没写、且收件方是**人** → 保持空。人只有名字,没有工作目录;
// 给人类也填 path前端与 agent_discovery 会拼出 `gui-lab@/root/proj.别名`
// 这种错地址(`ToHuman` 字段就是为避开这件事加的)。
// 3. 没写、且收件方是 **Agent** → 用会话的 workspace可能仍为空串
// 那表示这条会话确实没有工作目录,此时行为与从前一致)。
//
// 抽成纯函数是为了能直接对规则本身做判断:上面第 2 条是**反向对照**
// 少了它,把「一律填 path」写错成看起来也合理。
func resolveToWorkspace(addrPath, sessionWorkspace string, toIsHuman bool) string {
if addrPath != "" {
return addrPath
}
if toIsHuman {
return ""
}
return sessionWorkspace
}