fix(websearch): 面板「填 key + 同一次选它当主 provider」不再被自己的校验回滚
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

真机报障(2026-10-07 09:50):面板上给 zhipu 填了 API Key,保存却一直回
400 primary_not_configured;盘上配置文件 mtime 一动不动 —— 连刚填的 key
一起被回滚,界面表现为「怎么填都存不进去」。

根因(顺序错误):saveProvider 先 applySelectionPatch(内含「主 provider 必须
已配齐」的校验),后 UpsertProviderEntry。校验看的是**盘上的旧文件**,而这次
提交里的 key 还没落盘 ⇒ 校验判它没配齐,整笔拒绝。

修法:把校验从 applySelectionPatch 里摘出来,改名 mergeSelectionPatch(只合并
文件级字段 + 回报有没有给 primary),由 saveProvider 在 UpsertProviderEntry
**之后**对最终文件校验,再写盘。判据本身一字未改 —— 「真的没配齐的主家照样拒」。

验证:cmd/cyrene 整包测试绿;新增回归 TestWebSearchSaveProviderAndPrimaryInOneSubmit
(同一次提交填 key + 选主家 ⇒ 200 且盘上 key 与 primary 都在;反向:真没配齐仍 400
且文件字节不变);用 -overlay 把校验改回「拿旧文件校验」⇒ 该用例变红且报错文案
与主人看到的逐字相同。
This commit is contained in:
AskaEth committed 2026-10-07 09:56:37 +08:00
1 parent ef8fc66e54
commit aa0bf8b5a0
3 files changed
+73 -17

No files matched your search

+14 -13
View File
@@ -44,24 +44,30 @@ const (
// fail-fast),所以在写盘之前就拒掉,而不是先写进盘里等着下次起不来。
const actionCodePrimaryNotConfigured = "primary_not_configured"
// applySelectionPatch 把表单里的主 provider / 降级链合并进文件(三态:**缺位 = 不改动**)。
// mergeSelectionPatch 把表单里的主 provider / 降级链合并进文件(三态:**缺位 = 不改动**),
// 并回报「这次有没有给出 `primary`」—— **校验不在这里**。
//
// 与其余字段同一套哨兵语义(websearch_action_data.go 的 saveProvider 注释):
// `primary` 缺位 ⇒ 保留盘上的;给了值(含显式空串 / off / none)⇒ 采用 —— 空串与
// `off`/`none` 是本项目**唯一一套**关闭语义(config.IsWebSearchOffSentinel),面板不另造一套。
//
// 校验放在写盘之前(主 provider 必须已配齐),因为「先存下来、下次启动才发现起不来」
// 是本项目明确要避免的失败形态(AGENTS §10 的「诚实性」与 design/27 §3 的 fail-fast)。
func (e webSearchActionEnv) applySelectionPatch(
// 🔴 **顺序硬约束(缺陷修复 2026-10-07,真机报障)**:主 provider 的可用性校验必须由调用方
// 在「**本次这条 provider 条目已经并进文件之后**」做(`checkPrimaryUsable(final, …)`)。
// 本函数此前自己顺手校验,用的是**盘上的旧文件** ⇒ 面板上第一次给某家填 key、又在**同一次
// 提交**里把它选为主 provider 时,校验看不到那个还没落盘的 key,于是整笔(**连同刚填的 key**)
// 被 400 `primary_not_configured` 回滚 —— 界面上表现为「怎么填都存不进去」,而盘上文件
// mtime 一动不动(2026-10-07 09:50 日志连续两次 `plugin.saveProvider … code=primary_not_configured`)。
// 调用点见 websearch_action_data.go 的 saveProvider:先 `UpsertProviderEntry`,再校验,最后写盘。
func (e webSearchActionEnv) mergeSelectionPatch(
f runtimeconfig.WebSearchFile, values map[string]any,
) (runtimeconfig.WebSearchFile, error) {
) (runtimeconfig.WebSearchFile, bool, error) {
primary, setPrimary, err := selectionPrimary(values)
if err != nil {
return f, err
return f, false, err
}
fallback, setFallback, err := selectionFallback(values)
if err != nil {
return f, err
return f, false, err
}
if setPrimary {
f.Provider = primary
@@ -69,12 +75,7 @@ func (e webSearchActionEnv) applySelectionPatch(
if setFallback {
f.Fallback = fallback
}
if setPrimary {
if err := e.checkPrimaryUsable(f, f.Provider); err != nil {
return f, err
}
}
return f, nil
return f, setPrimary, nil
}
// checkPrimaryUsable 判定一个主 provider 名在**落盘后的文件**里是否可用。