fix(寻址): flatten 的候选**逐条**标注 —— 第一版把 §C 噪声放进了新端点
## 缺口(部署后实测才发现,是我自己引入的)
第一版 flatten 只在响应的 `paths[]` 数组里标注。实测:
222 条候选,其中 37 条(16%)落在桥内部目录(/root/.pi/mail-sessions/<uuid>)
而标注在**另一个数组** —— 模型必须自己把 candidates 与 paths 对照才认得出
那正是「§C 噪声淹没信号」换个位置复活。我在动手前的判断是「先修 C 再修 A,
否则新端点会把噪声一起放大」—— 做了 A,却让 C 的噪声原样跟进了 A。
只在真机跑过 `flatten=1` 才看见:单测全绿(它们只断言了 paths[] 有标注),
是生产数据的 16% 把它翻出来的。
## 修法
`AddressedCandidate` 逐候选带 `path_kind` / `path_note` / `is_absolute_path`,
MCP 渲染逐条打 `⚠`。
marker 收敛到 repo 层一份,handler 的 `classifyPath` 改为委托调用:
同一目录在 path 列表里标成「工作区」、在候选列表里却没标 ——
而那两个数组是**同一次调用**返回的。两处各写一份 marker 时,
改一处忘另一处就会出现这种自相矛盾,且没有任何报错。
## 判据(2 格)
TestFlattenAnnotatesEachCandidate 桥内部目录/相对路径能分类 + 带说明;
真工作区不得被误标(否则全是噪声)
TestClassifyPathAgreesWithRepo handler 与 repo 口径必须逐条一致
## 顺带
第一版 flatten 本身已验证有效(生产实测):
flatten=1 → 222 条候选、66 个工作区
/home/program/agentmail 125 条 · /root 16 条 · root 2 条
⇒ root 与 /root **同时可见**且各自带 path,不再需要「先猜 path 再枚举」
path 标注:66 条候选里 35 条桥内部目录 + 1 条相对路径被标出
This commit is contained in:
@ -526,9 +526,18 @@ type AddressedCandidate struct {
|
||||
Path string `json:"path"`
|
||||
// Address 是可直接投递的三维地址(name@path.alias)。
|
||||
Address string `json:"address"`
|
||||
// PathSource 说明这个 path 是从哪学来的(投递历史 / 平台镜像 / 注册自报),
|
||||
// 供调用方判断可信度与新鲜度。
|
||||
PathSource string `json:"path_source,omitempty"`
|
||||
// PathKind 与 PathNote 复用 handler 层的 pathCandidate 分类。
|
||||
//
|
||||
// ★ 为什么必须**逐候选**带,而不只是靠响应里那个 paths[] 数组:
|
||||
// 实测 flatten 一次给 222 条候选,其中 37 条(16%)落在桥内部目录
|
||||
// (/root/.pi/mail-sessions/<uuid>)。标注只放在 paths[] 里的话,
|
||||
// 模型必须自己把 candidates 与 paths 两个数组对照才认得出 ——
|
||||
// 而这正是 §C「噪声淹没信号」的翻版,只是换了位置。
|
||||
// 逐条自带标注,扫一眼列表就知道该跳过哪些。
|
||||
PathKind string `json:"path_kind,omitempty"`
|
||||
PathNote string `json:"path_note,omitempty"`
|
||||
// IsAbsolutePath 标出相对路径(`root` 与 `/root` 是两个不同工作区)。
|
||||
IsAbsolutePath bool `json:"is_absolute_path"`
|
||||
}
|
||||
|
||||
// SuggestAddressesForPeer 一次列出「我能投递的、属于 peerName 的全部地址」。
|
||||
@ -587,6 +596,9 @@ func SuggestAddressesForPeer(ctx context.Context, forUser, peerName string) ([]A
|
||||
SessionCandidate: c,
|
||||
Path: p,
|
||||
Address: models.FormatAddress(peerName, p, c.Alias),
|
||||
PathKind: classifyPathKind(p),
|
||||
PathNote: classifyPathNote(p),
|
||||
IsAbsolutePath: strings.HasPrefix(p, "/"),
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -595,3 +607,44 @@ func SuggestAddressesForPeer(ctx context.Context, forUser, peerName string) ([]A
|
||||
})
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// classifyPathKind / classifyPathNote 把「这个 path 是不是项目工作区」讲清楚。
|
||||
//
|
||||
// ★ 为什么放在 repo 层而不是只在 handler 层:标注要**逐候选**随行(见
|
||||
// AddressedCandidate 的注释),所以 repo 拼候选时就得能分类。
|
||||
// 判定口径与 handler 的 classifyPath 逐字一致(同一批 marker)。
|
||||
var bridgeInternalMarkers = []string{
|
||||
"/.pi/mail-sessions/",
|
||||
"/mail-sessions/",
|
||||
"/.agentmail/sessions/",
|
||||
"/.zcode/mail-sessions/",
|
||||
"/.dsh/",
|
||||
}
|
||||
|
||||
// ClassifyPathKind 导出给 handler 层复用(marker 只有这一份)。
|
||||
func ClassifyPathKind(p string) string { return classifyPathKind(p) }
|
||||
|
||||
// ClassifyPathNote 同上。
|
||||
func ClassifyPathNote(p string) string { return classifyPathNote(p) }
|
||||
|
||||
func classifyPathKind(p string) string {
|
||||
for _, m := range bridgeInternalMarkers {
|
||||
if strings.Contains(p, m) {
|
||||
return "bridge-internal"
|
||||
}
|
||||
}
|
||||
return "workspace"
|
||||
}
|
||||
|
||||
func classifyPathNote(p string) string {
|
||||
var notes []string
|
||||
if classifyPathKind(p) == "bridge-internal" {
|
||||
notes = append(notes,
|
||||
"这是 Agent 桥的内部会话存储目录,不是项目工作区;投到这里的信会把会话 cwd 变成它")
|
||||
}
|
||||
if !strings.HasPrefix(p, "/") {
|
||||
notes = append(notes,
|
||||
"这是相对路径,与 /"+strings.TrimPrefix(p, "/")+" 是两个不同工作区")
|
||||
}
|
||||
return strings.Join(notes, " ")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user