Files
MailUI4Agents/gateway/internal/repo/platform_owner_test.go
JianFeeeee c297468819 修 platform_session_id 无差别下发导致抄送方邮件静默消失 + homeagent 补投漏去重
## platform_session_id 只发给归属方(Gateway)

`notify.Recipients` 原来对所有参与方推同一个 `platform_session_id`,
而那是**会话级**的一个值。生产实测:会话 16845133 接管了 pi 的平台会话
`01a05a5e-…`,那封邮件抄送了 dsh@/home/program/agentmail.new。DSH 收到
同一个 id,在 ~/.dsh/sessions/ 里查不到(那是 /root/.pi/agent/sessions/
下的文件),于是走进「平台侧会话已删」那道防线抛错。

那道防线本身是对的(N-8:不能退回新建,否则人在界面上看不到这封邮件带来
的对话),它拦下的却是「别人的会话」。异常被 ctx.logger.error 吞掉,而
DSH 的 logger 不进 journalctl —— 邮件静默消失,日志里一个字都没有。

- 新增 `repo.PlatformSessionFor` 一并返回归属 Agent:以镜像
  `agent_platform_sessions.agent_name` 为准,镜像整表替换后退回
  `sessions.from_agent`(AdoptPlatformSession 写在那里)
- `PlatformIDOf` 变薄封装,保留原签名
- `notify.Recipients` 加 `platformFor(forName)`:归属方以外一律空串;
  归属抽不到时(owner 空)也不下发 —— 宁可退回当普通会话处理,
  也不让一个抽不到归属的 id 把邮件弄丢
- 归属与收件角色无关:归属方在抄送位上同样拿到

## homeagent catchUp 漏 deliveredMails 去重

`go p.catchUp(…)` 与 `go p.sseLoop()` 是两个并发 goroutine,重启时窗口
重叠:SSE 推一次 + 补投拉一次 = 同一封邮件注入两遍。homeagent 的回信正文
印证了这一点(「之前的对话时序中已经收到并确认过多次了」)。另三个插件的
catchUp 都有这层双查,只有这里漏了。

去重放在循环内逐封查而不是拉完一批再筛:InjectInputSync 一封要跑几十秒,
那期间 SSE 完全可能已经投过后面那几封。

## DSH 接管失败改用 console.error

DSH 的 ctx.logger 不进 journalctl,投递失败是「发件人等不到回信」的唯一
线索。接管失败点与 SSE 分发的 catch 都改走 console.error,并带上 mail_id
与发件人。

## 前端 ccAddress 移除(收尾上一轮未提交的改动)

cc_list 里的 `.new` 是**原始意图**,不该被替换成主收件人的别名:每个抄送
方的 `.new` 是独立的 —— pi@/x.new 给 pi 开一条、dsh@/x.new 给 dsh 开另一
条,各有自己的别名。数据库存的就是原文。删掉 ccAddress,MailView /
ThreadView 直接显示 c.raw。

## 测试

- `internal/notify/notify_test.go` +3 例:挂真实 SSE 客户端读帧,验
  归属方拿到 / 抄送方为空 / 归属方在抄送位也拿到 / 普通会话全空。
  负向对照跑过:platformFor 无条件返回时两条用例失败
- `internal/repo/platform_owner_test.go` +3 例:镜像取归属、普通会话、
  镜像被清后退回 from_agent
- 修好 web/test/components/replyTarget.test.tsx(上一轮遗留的语法损坏),
  三条 .new 用例改成断言原样保留
- gateway 7 包全绿;web 176 例 + 主题 26;dsh 219 / pi 250 / opencode 201

## 生产验证

- 抄送验证:jianf → pi(接管会话)cc dsh。DSH 正常建会话并回信「收到」,
  pi 走接管续谈 —— 两封回信都落在同一条线索上(此前 DSH 那封不存在)
- homeagent 去重:连发两轮,其中一轮在邮件未处理完时重启 homeagent 造出
  SSE/catchUp 并发窗口,两轮都只产生一封 Re:
- homeagent SSE:换新 plugin.bin 后连续 89 分钟零断连(此前 2 小时 102 次
  deadline exceeded 自激振荡)
2026-09-04 19:06:36 +08:00

79 lines
2.5 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 repo
import (
"context"
"testing"
)
// platform_session_id 必须只发给归属方 —— 生产上 pi 的会话 id 被推给了抄送方 dsh
// DSH 在自己磁盘上找不到那个文件,按 N-8 抛错,邮件静默消失。
func TestPlatformSessionFor_ReturnsOwnerFromMirror(t *testing.T) {
setupTestDB(t)
ctx := context.Background()
seedPlatformAgent(t, "pi")
seedPlatformAgent(t, "dsh")
// pi 上报一条平台会话
if err := ReplacePlatformSessions(ctx, "pi", []PlatformSession{
{PlatformID: "pid-pi-1", Workspace: "/w", Slug: "项目定位", Title: "项目定位"},
}); err != nil {
t.Fatalf("ReplacePlatformSessions: %v", err)
}
id, err := AdoptPlatformSession(ctx, "pi", "pid-pi-1", "项目定位", "/w", "项目定位")
if err != nil {
t.Fatalf("AdoptPlatformSession: %v", err)
}
pid, owner := PlatformSessionFor(ctx, id)
if pid != "pid-pi-1" {
t.Errorf("platformID = %q, want pid-pi-1", pid)
}
if owner != "pi" {
t.Errorf("owner = %q, want pi镜像里 agent_name=pi", owner)
}
}
// 未接管的普通会话不该返回任何 platform id。
func TestPlatformSessionFor_PlainSession(t *testing.T) {
setupTestDB(t)
ctx := context.Background()
seedAgent(t, "dsh", 20)
id, err := CreateSession(ctx, nil, "dsh", "普通会话", "/w")
if err != nil {
t.Fatalf("CreateSession: %v", err)
}
pid, owner := PlatformSessionFor(ctx, id)
if pid != "" || owner != "" {
t.Errorf("got (%q,%q), want ('','')", pid, owner)
}
}
// 镜像那行被整表替换掉(平台侧删了会话)时退回 sessions.from_agent
// 而不是让 owner 变空 —— 变空会让归属方也收不到 platform_session_id。
func TestPlatformSessionFor_MirrorGoneFallsBackToFromAgent(t *testing.T) {
setupTestDB(t)
ctx := context.Background()
seedPlatformAgent(t, "pi")
if err := ReplacePlatformSessions(ctx, "pi", []PlatformSession{
{PlatformID: "pid-pi-2", Workspace: "/w", Slug: "s2", Title: "t2"},
}); err != nil {
t.Fatalf("ReplacePlatformSessions: %v", err)
}
id, err := AdoptPlatformSession(ctx, "pi", "pid-pi-2", "s2", "/w", "t2")
if err != nil {
t.Fatalf("AdoptPlatformSession: %v", err)
}
// 平台侧删了这条会话 → 心跳整表替换成空
if err := ReplacePlatformSessions(ctx, "pi", []PlatformSession{}); err != nil {
t.Fatalf("ReplacePlatformSessions(empty): %v", err)
}
pid, owner := PlatformSessionFor(ctx, id)
if pid != "pid-pi-2" {
t.Errorf("platformID = %q, want pid-pi-2", pid)
}
if owner != "pi" {
t.Errorf("owner = %q, want pi退回 sessions.from_agent", owner)
}
}