## 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 自激振荡)
79 lines
2.5 KiB
Go
79 lines
2.5 KiB
Go
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)
|
||
}
|
||
}
|