fix: 事故全链路修复 — 调度器合流 + 接管保护 + 补全去重 + 权限显示 + homeagent SSE 振荡
## 事故现场 用户选中补全里的「项目定位」→ 邮件投进另一条会话,界面显示的名字也不是 自己选的那个。授权页只显示 Agent 名,看不出哪个目录哪条线索。 ## 四处因果链 **① 调度器自己的 new_mail payload(起点)。** `notifyRecipients`(handler) 与 `SendCalendarMail`(scheduler)是两份代码。加 `platform_session_id` 时只改了 handler 那份 → 日历提醒投进接管会话时插件不知道是接管 → 另开一条新会话 → 命名同步冲掉接管会话的别名。 修法:抽出 `internal/notify` 包,唯一入口 `notify.Recipients`。 handler / scheduler / permission.go 都走它。新增字段时不存在「另一处忘了改」。 **② SyncSessionAlias 覆盖接管别名。** 别名是人从补全里选中的平台 slug, 任何平台命名同步都不该动它。加守卫 `platform_id <> ''` → 有绑定就返回当前值。 **③ SuggestSessionCandidates 按别名字符串去重。** 别名一被冲掉,同一条会话 出现两次(一次被冲的名字、一次镜像 slug),而另一条真实会话被吃掉。 改按 `platform_id` 去重。 mail 侧查 `s.platform_id`,镜像侧查 `platform_id`。 **④ FindOrCreateDefaultSession 不排除接管会话。** 日历提醒省略 session 位 → FindOrCreateDefaultSession 挑中人显式指定的接管会话。加 `platform_id = ''` 条件。 ## 权限页 **CreatePermissionMail 不写 from_workspace。** `from_workspace` 存空串 → 前端 `g.path && ...` 不渲染 → 人只看到光秃的 Agent 名,不知道哪个目录 哪条线索在请求权限。修法:INSERT 时从 sessions.workspace 取。 **SSE payload 缺 session_alias。** permission.go 的 SSE 不走 notify 包(决策人 不是地址解析出的参与方),但 payload 也要带 `session_alias` → 前端拼出 `pi@/home/program/agentmail.别名`,而不是光秃的 `pi`。 **mailGroups.ts:path ← session_workspace。** `from_workspace` 对 Agent 存的是 Agent 名(历史遗留),不能当路径用。PermissionList 显示完整三段地址 `agent@path.alias`。 ## NarrowStack z-index 窄屏日历的星期表头(`sticky top-0 z-10`)穿透到二级页面之上。覆盖层 auto z-index 输给 z-10 → 底层组件的层叠穿透到覆盖层。 修法:底层容器加 `isolate`(isolation: isolate),自成层叠上下文; 覆盖层加 `z-10`。只给覆盖层加 z-index 只能治当前一处,底层再写更大的 z-index 又会复现。 ## homeagent SSE 自激振荡 根因:五处缺陷叠加,SSE 每 60 秒断一次 → Gateway 全量重放 → 再断 → 再重放。 1. `p.client`(60s Timeout)跑 SSE 长连接 → 新增 `sseClient`(无超时) 2. `InjectInputSync` 在读循环里同步调用 → 改为 `go p.handleNewMail(evt)` 3. `lastEventID` 无条件赋值,Gateway 重放时发旧 ID → 单调递增 `sseMaxID` 4. 无邮件级去重 → 补 `deliveredMails map[string]bool` 5. 手动 `[]byte` 管理:每次 `buf[lineStart:]` 缩小 cap → 最终 len==cap → Read 零长切片 → 满速空转。改 `bufio.Reader`。 ## 清库 保留 jianf + 4 个 Agent 密钥 + 模型范围配置。清掉 mails/sessions/ calendar_events/attachments/agent_platform_sessions/relayed_mails/ permission_requests/rate_limits。测试数据已全部清零。 ## 测试 - gateway 7 包全过;repo + 7 例(adopt_alias_test.go) - web 182 例(mailGroups 新增 session_workspace 断言) - 前端构建通过
This commit is contained in:
@ -11,8 +11,8 @@ import (
|
||||
|
||||
"github.com/agentmail/gateway/internal/db"
|
||||
"github.com/agentmail/gateway/internal/models"
|
||||
"github.com/agentmail/gateway/internal/notify"
|
||||
"github.com/agentmail/gateway/internal/repo"
|
||||
"github.com/agentmail/gateway/internal/sse"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
@ -279,62 +279,33 @@ func SendCalendarMail(ctx context.Context, eventID, toAddr, subject, body, creat
|
||||
}
|
||||
}
|
||||
|
||||
alias := repo.SessionAliasOf(ctx, sessionID)
|
||||
sse.Default.SendToRecipient(addr.Name, "new_mail", map[string]interface{}{
|
||||
"mail_id": mailID.String(),
|
||||
"session_id": sessionID.String(),
|
||||
"from_name": calendarSender,
|
||||
"subject": subject,
|
||||
"mail_type": "normal",
|
||||
"role": "to",
|
||||
"to_workspace": addr.Path,
|
||||
"session_alias": alias,
|
||||
// 回信地址给 calendar 是发不出去的(它不是收件方),
|
||||
// 给会话自己的地址才让模型能把结果回报到同一条线索上。
|
||||
"reply_address": models.FormatAddress(addr.Name, addr.Path, alias),
|
||||
"self_address": models.FormatAddress(addr.Name, addr.Path, alias),
|
||||
// 让插件与 UI 能区分「这封是定时提醒」而不是有人在找它
|
||||
"origin": "calendar",
|
||||
})
|
||||
sse.Default.SendToRecipient(addr.Name, "session_update", map[string]interface{}{
|
||||
"session_id": sessionID.String(),
|
||||
"status": "active",
|
||||
})
|
||||
|
||||
// 抄送方也要收到 SSE。
|
||||
// ─── 推送 SSE ───
|
||||
//
|
||||
// 漏了这一步的后果很隐蔽:邮件的 cc_list 里有他们、他们**查**收件箱
|
||||
// 能看到这封信,但没有任何事件推给他们 —— 于是插件不会唤起会话,
|
||||
// Agent 直到下一次补拉(重启时)才发现。对「知情方」而言等于没通知。
|
||||
for _, c := range ccList {
|
||||
alias := repo.SessionAliasOf(ctx, sessionID)
|
||||
sse.Default.SendToRecipient(c.Name, "new_mail", map[string]interface{}{
|
||||
"mail_id": mailID.String(),
|
||||
"session_id": sessionID.String(),
|
||||
"from_name": calendarSender,
|
||||
"subject": subject,
|
||||
"mail_type": "normal",
|
||||
"role": "cc",
|
||||
"to_workspace": c.Path,
|
||||
"session_alias": alias,
|
||||
"reply_address": models.FormatAddress(addr.Name, addr.Path, alias),
|
||||
"self_address": models.FormatAddress(c.Name, c.Path, alias),
|
||||
"origin": "calendar",
|
||||
})
|
||||
sse.Default.SendToRecipient(c.Name, "session_update", map[string]interface{}{
|
||||
"session_id": sessionID.String(),
|
||||
"status": "active",
|
||||
})
|
||||
}
|
||||
// 这是整个系统里**唯一**的推送实现(handler 那条路径也是 notify.Recipients)。
|
||||
// 原来这里自己拼了一整份 payload(handler 里是另一份),
|
||||
// 加 `platform_session_id` 时只改了那边 → 日历提醒投进接管会话时
|
||||
// 插件不知道是接管、另开了一条新 pi 会话 → 命名同步把接管会话的别名冲掉
|
||||
// → 人在补全里选的「项目定位」变成了「日程提醒:…」→ 选哪条都落进同一条。
|
||||
// 根因只是「同一件事写了两遍」。
|
||||
//
|
||||
// ReplyToName = addr.Name:日历提醒的回信要落回那条线索,不是回给 calendar
|
||||
// Origin = "calendar":让插件与 UI 能区分「这封是定时提醒」
|
||||
notify.Recipients(ctx, notify.Mail{
|
||||
SessionID: sessionID,
|
||||
MailID: mailID,
|
||||
From: calendarSender,
|
||||
To: addr,
|
||||
CC: ccList,
|
||||
Subject: subject,
|
||||
MailType: "normal",
|
||||
Origin: "calendar",
|
||||
ReplyToName: addr.Name,
|
||||
})
|
||||
|
||||
// 创建者也该看到提醒发出去了 —— 否则「我设的提醒到底触发了没有」
|
||||
// 只能去翻 journalctl。
|
||||
if createdBy != "" && createdBy != addr.Name {
|
||||
sse.Default.SendToRecipient(createdBy, "session_update", map[string]interface{}{
|
||||
"session_id": sessionID.String(),
|
||||
"status": "active",
|
||||
})
|
||||
}
|
||||
// 创建者不是收件方,不在 Recipients 的参与方去重里 —— 他不会收到
|
||||
// new_mail(他不该被「有人在找你」打扰),但他应该看到会话活跃起来:
|
||||
// 「我设的提醒到底触发了没有」不应该只能去翻 journalctl。
|
||||
notify.SessionActive(createdBy, sessionID)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user