fix(homeagent桥): ★ read_thread 不传 offset 时 URL 拼成 /thread&session_id ⇒ 100% 404
症状:模型默认不传 offset ⇒ offset="" ⇒ scopeQuery("&") 拼出
/agent/mail/{id}/thread&session_id=…
`&` 被当成路径的一部分,网关路由 /agent/mail/{id}/thread 匹配不上 ⇒ 404。
根因:2a5e3d7(09-14「四家桥的读端点也带上会话收窄」)只给 read_thread 拼错了
分隔符;其余四家桥走 `path.includes('?') ? '&' : '?'`,没踩到。
★ 网关日志里的单字符对照实验(09-30 10:14:47,同一 mail_id、同一 session_id 值、
相隔 0 秒的两条请求,只差分隔符):
/thread?session_id=x -> 401 86B ← 已路由进 handler,只是鉴权没过
/thread&session_id=x -> 404 19B ← 从未匹配到路由
同一 session_id、同样缺 workspace,唯一变量是 ? / &。09-29 的三次 404
(20:21:12 / 20:58:14 / 21:44:42)URL 形态一致,且**不伴随** [agent-scope]
「没声明 session_id」告警 ⇒ session_id 确实带上了,问题在分隔符。
为什么「同一二进制 09-29 全 404、09-30 全成功」不是矛盾:该缺陷只在
currentSessionID != ""(即正在处理某轮邮件)时触发。09-30 那三次读发生在回合外,
scopeQuery 返回空串、不追加分隔符 ⇒ 路径正确 ⇒ 200(网关日志有 [agent-scope]
「旧语义放行」告警为证)。所以它恰好只在**邮件回合内**发作 —— 即需要读线索
才能回信的那条路径。
修法:sep := "?" ; if offset != "" { sep = "&" }。
不能只把 "&" 改成 "?" —— offset 自带 "?offset=%d",一刀切会把分页那条从对的改错。
两条路径都在 plugin_read_thread_path_test.go 里逐字断言路径 == 网关路由。
自证边界(改完仍无法自证的部分):单测证明的是**拼出的 URL 落在网关路由上**,
不等于已在 homeagent 部署的那份 plugin.bin 上端到端复现过。原守卫测试
(plugin_read_scope_test.go)断言的是「URL 里有没有 session_id=」,缺陷 URL 里
恰恰有 ⇒ 绿着放行;新测试把判据落在 u.Path 上。
This commit is contained in:
@ -0,0 +1,79 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
)
|
||||
|
||||
/*
|
||||
read_thread 的两条路径都必须真的落在网关路由上。
|
||||
|
||||
# 缺陷(dsh 2026-09-29 报,已在本机复现)
|
||||
|
||||
`tools.go` 里曾写死 `scopeQuery("&")`。模型默认**不传 offset** ⇒ offset 为空串
|
||||
⇒ 拼出 `/agent/mail/{id}/thread&session_id=…`。`&` 不是 `?`,所以它被当成
|
||||
**路径的一部分**:网关路由 `/agent/mail/{id}/thread`({id} 只吃一个路径段)
|
||||
匹配不上 ⇒ **404**。生产日志里同一封邮件相隔 19 秒两条请求,
|
||||
只带 session_id 的那条 200、带 workspace 的那条 404 —— 后者就是本桥。
|
||||
|
||||
# 为什么原来的守卫测试没拦住
|
||||
|
||||
`plugin_read_scope_test.go` 断言的是「URL 里**有没有** session_id=」——
|
||||
缺陷 URL 里**有**,所以绿着。判据必须落在**路径**上,而不是查询串的存在性。
|
||||
|
||||
# 陷阱:不能只把 & 改成 ?
|
||||
|
||||
`offset>0` 那条的 offset 自带 `?`,所以它原本是**对的**。把 `"&"` 一刀切成 `"?"`
|
||||
反而会把这条改坏。两条路径必须都验 —— 所以这个测试跑两组。
|
||||
*/
|
||||
func TestReadThreadBothPathsRoutable(t *testing.T) {
|
||||
var seen []string
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
seen = append(seen, r.URL.RequestURI())
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
const sid = "593988da-0000-0000-0000-0000000000aa"
|
||||
const wantPath = "/api/v1/agent/mail/m-1/thread"
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
args map[string]interface{}
|
||||
wantPath string
|
||||
wantQ string
|
||||
}{
|
||||
{"offset 省略", map[string]interface{}{"mail_id": "m-1"},
|
||||
wantPath, "session_id=" + sid},
|
||||
{"offset>0", map[string]interface{}{"mail_id": "m-1", "offset": float64(5)},
|
||||
wantPath, "offset=5&session_id=" + sid},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
p := &Plugin{gwURL: srv.URL, client: srv.Client(), currentSessionID: sid}
|
||||
seen = nil
|
||||
if _, err := p.handleReadThread(c.args); err != nil {
|
||||
t.Fatalf("%s:调用失败 %v", c.name, err)
|
||||
}
|
||||
if len(seen) != 1 {
|
||||
t.Fatalf("%s:应当只请求一次(实际 %d 次:%v)", c.name, len(seen), seen)
|
||||
}
|
||||
|
||||
u, err := url.Parse(seen[0])
|
||||
if err != nil {
|
||||
t.Fatalf("%s:拼出的 URL 不合法 %q:%v", c.name, seen[0], err)
|
||||
}
|
||||
// ★ 关键判据:路径必须与网关路由逐字相等。缺陷版本这里是
|
||||
// "/api/v1/agent/mail/m-1/thread&session_id=…" ⇒ 一定不相等。
|
||||
if u.Path != c.wantPath {
|
||||
t.Fatalf("★ %s:路径匹配不上网关路由\n 实际: %s\n 期望: %s\n (完整请求 %s)",
|
||||
c.name, u.Path, c.wantPath, seen[0])
|
||||
}
|
||||
if u.RawQuery != c.wantQ {
|
||||
t.Fatalf("★ %s:查询串不对\n 实际: %s\n 期望: %s", c.name, u.RawQuery, c.wantQ)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -349,7 +349,11 @@ func (p *Plugin) handleReadThread(args map[string]interface{}) (interface{}, err
|
||||
ParentHid bool `json:"parent_hidden"`
|
||||
} `json:"nodes"`
|
||||
}
|
||||
if err := p.get(p.gwURL+"/api/v1/agent/mail/"+mid+"/thread"+offset+p.scopeQuery("&"), &data); err != nil {
|
||||
sep := "?"
|
||||
if offset != "" {
|
||||
sep = "&"
|
||||
}
|
||||
if err := p.get(p.gwURL+"/api/v1/agent/mail/"+mid+"/thread"+offset+p.scopeQuery(sep), &data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user