Files
MailUI4Agents/server/internal/handler/session_scope_hint_test.go
JianFeeeee 1816a837ec fix(agent)★★: 会话作用域拒绝文案不再拼出「你当前在会话 。」
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`
和测试里,**从不暴露给模型**。
2026-10-05 10:22:46 +08:00

117 lines
4.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package handler
/*
会话作用域拒绝的文案必须**指出 id 从哪来**(2026-10-04 实测驱动)。
★ 为什么要这一格:
实测「独立 MCP 宿主 E2E」时,第一版客户端卡在这里 ——
它收到了 new_mail 事件、知道该读那封信,却**不知道该传什么 session_id**,
于是一封信都读不到,报的是:
这封信不在你当前所在的那条会话里:…你当前在会话 。
原文案说了「就在它那条会话里读」(去哪读),却没说 **id 从哪来**
(怎么去读)。缺的那半句正是宿主唯一缺的信息。
★ 判据盯两件事,缺一不可:
① 提到 session_id(名字本身)
② 指出**来源**(new_mail 事件的 payload / session/default 端点)
—— 只加「请传 session_id」等于把问题原样推回去,宿主仍不知道去哪拿。
*/
import (
"os"
"strings"
"testing"
)
func TestSessionScopeHintTellsWhereToGetSessionID(t *testing.T) {
b, err := os.ReadFile("agent_discovery.go")
if err != nil {
t.Fatalf("读 agent_discovery.go: %v", err)
}
src := string(b)
i := strings.Index(src, "这封信不在你当前所在的那条会话里")
if i < 0 {
t.Fatal("找不到会话作用域的拒绝文案")
}
// ★ 只取这一条 Error(...) 调用,不能按固定窗口往后截。
// 初版用「起点 + 2200 字符」,而**同一文件别处的文档注释**里也写了
// `/agent/session/default`(那是 `agentScope` 上方的说明)⇒
// 删掉提示后窗口里仍有它 ⇒ 判据假绿。
// 这与「变异必须确认真的应用」同族,但更隐蔽:变异应用了、判据却因为
// **看错了范围**而没反应。
seg := src[i:]
if end := strings.Index(seg, `Error(w, http.StatusForbidden, "无权访问该会话"`); end > 0 {
seg = seg[:end]
}
// ① 提到 session_id
if !strings.Contains(seg, "session_id") {
t.Error("★ 拒绝文案里没有 session_id —— 调用方不知道该传什么")
}
// ② 指出来源:事件载荷 或 默认会话语义端点
hasEvent := strings.Contains(seg, "new_mail")
hasEndpoint := strings.Contains(seg, "/agent/session/default")
if !hasEvent && !hasEndpoint {
t.Error("★ 只说了「要传 session_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
}
return b
}