fix(agents): 四家桥的 read_inbox 一律按会话收窄(dsh/opencode/zcode/homeagent)
用户:「你还是没修好不同 session agent 收件箱隔离的问题」。上一轮我只修了 **pi**, 另外四家还漏着 —— 它们是**每一家各自实现** read_inbox,不修就还是漏。 ## 缺陷 列表按 Agent 列(整个收件箱),而 read_inbox 按契约把**列出来的都标成已读** ⇒ A 会话的回合会把 B 会话的未读标掉 ⇒ B 之后按 `?status=unread` 补投时 再也看不到那封信(静默丢信,不是"少看一封")。用户是在别的 Agent 上看到它的。 ## 四家的修法(各自平台能力不同,但都要"并发安全") | 桥 | 会话来源 | 为什么这样做 | |---|---|---| | dsh | 工具第二参数 `exec.agent.id` → `reverseMap` | 平台就在上下文里给了会话;**不能用模块级"当前会话"变量**(同进程可能同时跑多条会话的回合,会互相覆盖) | | opencode | 工具第二参数 `context.sessionID` → `reverseMap` | 同上 | | zcode | `AGENTMAIL_SESSION_ID`(在**调用时**读) | 一轮一个进程,驱动本来就注入它给授权钩子用;调用时读,避免将来复用进程拿到旧值 | | homeagent | `p.currentSessionID`(回合开始设、结束清) | Go 插件,本来就有这个状态 | 取不到会话一律**退回整体收件箱**(历史行为),不猜 —— 猜错就是静默丢信。 ## 判据 - 服务端语义:`server/internal/repo/session_scope_test.go`(读 A 不动 B、列表收窄、 计数与列表口径一致)。 - 桥侧接线:dsh 4 条、opencode 3 条、zcode 3 条、homeagent Go 1 条 (`TestInboxURLScopedBySession`,直接断言拼出来的 URL)。 每家都带**判据自检**:拿旧写法喂进来必须判红;dsh/opencode 还专门断言 "不得用模块级当前会话变量"。 - **部署件**(不是仓库):四家的部署快照里都能 grep 到 `session_id=`。 - **线上实测**:用 opencode 自己的 Agent 身份请求收窄列表 —— 会话 A 3 封、 会话 B 0 封、两者无交集、且都是全量的子集。 套件:opencode **331**、dsh **381**、zcode **385**、homeagent ok,全绿。 四家桥已重新部署(dsh/opencode/zcode 快照切换 + homeagent 新 plugin.bin 并重启), 四个服务均 active。
This commit is contained in:
@ -10,6 +10,7 @@ import (
|
||||
"log"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@ -1071,7 +1072,14 @@ func (p *Plugin) handleReadInbox(args map[string]interface{}) (interface{}, erro
|
||||
limit = int(v)
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/api/v1/mail/inbox?status=%s&limit=%d", p.gwURL, status, limit)
|
||||
// ★ 会话收窄:只列**当前正在处理的那条会话**的邮件。
|
||||
//
|
||||
// 不带 session_id 时这是整个 Agent 的收件箱,而 read_inbox 按契约会把列出来的
|
||||
// 都标成已读 ⇒ A 会话的回合会把 B 会话的未读标掉,B 之后按 ?status=unread
|
||||
// 补投时就再也看不到那封信(静默丢信)。currentSessionID 在回合开始时设置、
|
||||
// 结束时清空(见 handleNewMail/handleThread 的 InjectInputSync 前后),
|
||||
// 所以这里读到的就是"这次在替谁干活"。
|
||||
url := p.inboxURL(status, limit)
|
||||
var result map[string]interface{}
|
||||
if err := p.get(url, &result); err != nil {
|
||||
return nil, err
|
||||
@ -1472,3 +1480,16 @@ func (p *Plugin) markRead(ids []string) {
|
||||
req.Header.Set("Authorization", "Bearer "+p.key)
|
||||
p.client.Do(req)
|
||||
}
|
||||
|
||||
// inboxURL 拼收件箱地址。单独抽出来是为了能被单测直接断言 ——
|
||||
// 会话收窄这种"少了个参数就静默丢信"的改动,必须有判据盯着 URL 本身。
|
||||
//
|
||||
// session_id 只在**正在处理某条会话**时带上;不在回合里(例如桥启动时的自检)
|
||||
// 就是整个 Agent 的收件箱,这也是历史行为。
|
||||
func (p *Plugin) inboxURL(status string, limit int) string {
|
||||
scope := ""
|
||||
if p.currentSessionID != "" {
|
||||
scope = "&session_id=" + url.QueryEscape(p.currentSessionID)
|
||||
}
|
||||
return fmt.Sprintf("%s/api/v1/mail/inbox?status=%s&limit=%d%s", p.gwURL, status, limit, scope)
|
||||
}
|
||||
|
||||
45
plugins/homeagent-mail-bridge/plugin_session_scope_test.go
Normal file
45
plugins/homeagent-mail-bridge/plugin_session_scope_test.go
Normal file
@ -0,0 +1,45 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
/*
|
||||
会话收窄:一个 Agent 同时在多条会话里干活时,收件箱列表必须只列**当前这条**。
|
||||
|
||||
缺陷(用户报的):「不同 session 的 agent 都可以看到全部邮件」。列表按 Agent 列,
|
||||
且 read_inbox 按契约把列出来的都标已读 ⇒ A 会话的回合标掉 B 会话的未读 ⇒
|
||||
B 之后按 `?status=unread` 补投时再也看不到那封信(静默丢信)。
|
||||
|
||||
homeagent 原先根本没有这个参数(另外三家桥也是),所以这里不只测"带上了",
|
||||
也测"不在回合里时不带"(那是历史行为,桥启动自检要用整箱)。
|
||||
*/
|
||||
func TestInboxURLScopedBySession(t *testing.T) {
|
||||
p := &Plugin{gwURL: "http://127.0.0.1:8180"}
|
||||
|
||||
outside := p.inboxURL("unread", 5)
|
||||
if strings.Contains(outside, "session_id=") {
|
||||
t.Fatalf("不在回合里时不应带 session_id:%s", outside)
|
||||
}
|
||||
|
||||
p.currentSessionID = "593988da-0000-0000-0000-000000000001"
|
||||
inside := p.inboxURL("unread", 5)
|
||||
if !strings.Contains(inside, "session_id=593988da-0000-0000-0000-000000000001") {
|
||||
t.Fatalf("回合中必须带上当前会话:%s", inside)
|
||||
}
|
||||
if !strings.Contains(inside, "status=unread") || !strings.Contains(inside, "limit=5") {
|
||||
t.Fatalf("原有参数不能被改坏:%s", inside)
|
||||
}
|
||||
|
||||
// 会话 id 里不可能出现的东西也要被转义(否则拼出坏 URL,网关会 400)
|
||||
p.currentSessionID = "a b&c"
|
||||
esc := p.inboxURL("all", 1)
|
||||
if strings.Contains(esc, "&c") {
|
||||
t.Fatalf("session_id 未转义:%s", esc)
|
||||
}
|
||||
if _, err := url.Parse(esc); err != nil {
|
||||
t.Fatalf("拼出的 URL 不合法:%v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user