From 1816a837ec07603196218c9621a86d3f54d2ba98 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Mon, 5 Oct 2026 10:22:46 +0800 Subject: [PATCH] =?UTF-8?q?fix(agent)=E2=98=85=E2=98=85:=20=E4=BC=9A?= =?UTF-8?q?=E8=AF=9D=E4=BD=9C=E7=94=A8=E5=9F=9F=E6=8B=92=E7=BB=9D=E6=96=87?= =?UTF-8?q?=E6=A1=88=E4=B8=8D=E5=86=8D=E6=8B=BC=E5=87=BA=E3=80=8C=E4=BD=A0?= =?UTF-8?q?=E5=BD=93=E5=89=8D=E5=9C=A8=E4=BC=9A=E8=AF=9D=20=E3=80=82?= =?UTF-8?q?=E3=80=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 2026-10-05,比对 MCP 与插件接入能力时实测发现。 ## 症状 独立 MCP 宿主读自己刚收到的那封信,得到: 这封信不在你当前所在的那条会话里:…你当前在会话 。要读它,… `你当前在会话 。` 中间是空的。 ## 根因:一个 reason 值管三种拒绝,文案却只有一句 `AgentMayReadSession` 用**同一个 `reason`** 覆盖三种情形: ① 没声明 session_id(scope == nil) ② 声明了别人的会话 ③ 声明了但**不是参与方**(session_id 由请求方提供,自述不是事实) 而 `canReadSession` 无条件拼 `"你当前在会话 " + self + "。"`, 于是 ① 拼出那句残缺文案。 ★ 为什么这句残缺文案比「不说」更糟:模型读到「你当前在会话 。」 会以为自己在一个**无名会话**里,于是转去猜/去搜 —— 而正确做法(从 new_mail 事件取 session_id)就在同一句话的后半段, 它却读不到前半段。 ## 修法 按 `scope` 有无分句,**不给空值留位置**: ① → 「你当前**没有**声明自己所在的那条会话,所以无从判断这封信是不是你的。」 ② → 「你当前在会话 00000000-…」(仍只报自己声明的那条) ②③ 故意不拆开:`session_workspace.go:171` 已经警告过「新增 reason 还要同步改 handler 的 switch,漏改就是 500」,而两者给模型的指引**完全相同** (都是"去那条会话读"),拆开只增加分支不增加信息。 ## 实测(真实请求,部署后) ① 不声明 → 「你当前**没有**声明自己所在的那条会话…」 无残缺 ✓ ② 声明他人 → 「你当前在会话 00000000-…」(自己声明的那条)✓ ★ 「会话 。」残缺句已消失 ## 判据 `TestSessionScopeHintNeverLeavesEmptySessionSlot` —— 盯**结构**而非文案本身: - 禁止再出现 `"你当前在会话 "+self+"。` 这种无条件拼接 - 要求按 `scope != nil` 分句 - 要求存在「未声明会话」那一句 两个变异都转红:退回无条件拼接 / 删掉「未声明」那句。 ## 顺带确认:MCP 与插件的工具面已对齐 实测 `tools/list`(11 个)与 4 个插件的真实注册项做集合比对: MCP 有、插件没有: 无 插件有、MCP 没有: 无 ⚠️ 中途差点误判:grep 到插件里有 `permission_decision` 等 3 个词, 以为 MCP 缺 3 个工具。查下去 `permission_decision` 是 **mail_type** (收到授权请求邮件后转给 worker),另两个只存在于 `src/pool.mjs` 和测试里,**从不暴露给模型**。 --- server/internal/handler/agent_discovery.go | 14 ++++-- .../handler/session_scope_hint_test.go | 45 ++++++++++++++++++- 2 files changed, 55 insertions(+), 4 deletions(-) diff --git a/server/internal/handler/agent_discovery.go b/server/internal/handler/agent_discovery.go index 9b4045a..45b64e8 100644 --- a/server/internal/handler/agent_discovery.go +++ b/server/internal/handler/agent_discovery.go @@ -98,13 +98,21 @@ func canReadSession(w http.ResponseWriter, r *http.Request, agentName string, sc // 记忆是隔离的」。所以**不再拿工作区当理由**(旧文案就是 cross-workspace, // 它让 zcode 推断出"那五位 agent 不存在",把错误结论写进了给人看的信里)。 // 报出"我当前在哪条会话"是安全的(那是调用方自己的);**不报**目标会话在哪。 - self := "" + // + // ★ 2026-10-05:`reason` 是**一个值管三种拒绝**(没声明 / 声明了别人的 / + // 不是参与方),而「你当前在会话 」在 self 为空时会拼出 + // 「你当前在会话 。」—— 实测(独立 MCP 宿主读自己那封信)确实读到这句, + // 残缺的话会让模型以为自己在一个无名会话里,转而去猜。 + // 所以先按 self 有无分句,**不给空值留位置**。 + var where string if scope != nil { - self = (*scope).String() + where = "你当前在会话 " + (*scope).String() + "。" + } else { + where = "你当前**没有**声明自己所在的那条会话,所以无从判断这封信是不是你的。" } Error(w, http.StatusForbidden, "这封信不在你当前所在的那条会话里:每个会话各有各的收件箱(会话之间的记忆是隔离的)。"+ - "你当前在会话 "+self+"。要读它,就在它那条会话里读 —— 每封来信都会把 worker 唤醒到"+ + where+"要读它,就在它那条会话里读 —— 每封来信都会把 worker 唤醒到"+ "它自己那条会话上,那时你看到的就是那条会话的收件箱。"+ // ★ 2026-10-04 补:调用方多半是**拿不到**目标会话 id,而不是 // 「知道却不传」。原来那句「就在它那条会话里读」只说了去哪读, diff --git a/server/internal/handler/session_scope_hint_test.go b/server/internal/handler/session_scope_hint_test.go index 9974631..bf8dacc 100644 --- a/server/internal/handler/session_scope_hint_test.go +++ b/server/internal/handler/session_scope_hint_test.go @@ -58,13 +58,56 @@ func TestSessionScopeHintTellsWhereToGetSessionID(t *testing.T) { t.Error("★ 只说了「要传 session_id」却没说**从哪拿** ——" + " 那等于把问题原样推回去,宿主仍然读不到信(实测卡点)") } - // ③ 不得泄露目标会话 id(安全边界:那是别人的会话) +// ③ 不得泄露目标会话 id(安全边界:那是别人的会话) if strings.Contains(seg, `self = (*scope).String()`) && strings.Contains(seg, "self+"+"\" 目标会话") { t.Error("★ 不得把目标会话 id 拼进文案 —— 那是安全边界(只报自己所在的会话)") } } +/* +★ 「你当前在会话 。」—— 一个值管三种拒绝,文案却只有一句(2026-10-05)。 + + `AgentMayReadSession` 用**同一个 reason** 覆盖三种拒绝: + ① 没声明 session_id(scope == nil) + ② 声明了别人的会话 + ③ 声明了但**不是参与方**(session_id 由请求方提供,自述不是事实) + + 原文案无条件拼 `"你当前在会话 " + self + "。"`,于是 ① 拼出: + +这封信不在你当前所在的那条会话里:…你当前在会话 。要读它,… + + 实测(独立 MCP 宿主读自己刚收到的那封信)确实读到这句。 + 残缺的话会让模型以为自己在一个**无名会话**里,转而去猜/去搜 —— + 比不说更糟。 + +★ 这条判据盯的是**结构**:文案必须按 scope 有无分句, + 而不是靠手测一次。 +*/ +func TestSessionScopeHintNeverLeavesEmptySessionSlot(t *testing.T) { + b, err := os.ReadFile("agent_discovery.go") + if err != nil { + t.Fatalf("读 agent_discovery.go: %v", err) + } + src := string(b) + + // ★ 回归:不得再出现「无条件拼接 self」的写法 —— + // 它就是那句残缺文案的唯一来源。 + if strings.Contains(src, `"你当前在会话 "+self+"。`) { + t.Error("★ 又用回「你当前在会话 \" + self」的无条件拼接 —— " + +"scope 为 nil 时会拼出「你当前在会话 。」,模型会以为自己在一个无名会话里") + } + + // 替代写法必须按 scope 有无分句 + if !strings.Contains(src, "if scope != nil {") { + t.Error("文案没有按 scope 有无分句") + } + if !strings.Contains(src, "你当前**没有**声明") { + t.Error("★ 没有「未声明会话」这一句的文案 —— " + +"调用方不声明 session_id 时读不到任何提示该传什么") + } +} + func min2(a, b int) int { if a < b { return a