feat(search): 面板上终于能改「结果条数上限」(原来改了会被静默丢弃)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

真缺陷(上一片登记):`maxResults` 在面板上**根本改不了** —— ① 字段表里没有这个
字段(UI 不显示输入框);② `saveProvider` 只并 provider/apiKey/baseUrl/enabled;
③ `mergeSelectionPatch` 只处理 primary/fallback ⇒ **手工塞进 values 也会被静默丢弃**
(文件不变、也不报错)。「我改了它却没变」正是要消灭的那种假话。

- `panel.go`:表单加 `{Key: FieldMaxResults, Type: sdk.FieldNumber, Min: 1}`(**SDK 本就有
  数字类型**,未自造;前端本就渲染 number ⇒ **不需要改前端**)。刻意不设 `Default`
  (设了会让一次「只改 key」的保存把默认值固化进文件、还可能盖掉 `.env`)。
- `websearch_selection.go`:`mergeSelectionPatch` 里按**三态**落 `maxResults` ——
  缺位/null ⇒ **不改动**;正整数(含整数字符串)⇒ 采用;0/负数/小数/空串/非数/
  布尔/数组 ⇒ **400 `invalid_payload`** 且**盘上一字节不动**(绝不静默忽略)。
- `reauth` 回填该值(否则一保存就把上限清掉)。
- `.env.example`:该键的示例值与「默认 5」注释同步为 **10**(代码默认已改;⚠️ `.env` 未动)。

验收(主会话复跑 + 亲自变异):**整树 `go build ./...` 通过**;三包 vet/测试绿;
端到端「面板存 20 ⇒ 模型要 50 也只给 20、要 3 给 3」(走真 action hub → 落盘 →
热重载 → WithSearchMaxResults → 真 dispatcher,未重启后端);
我用 -overlay 把 `f.MaxResults = maxResults` 摘掉 ⇒ 两条用例变红
(「面板给的值必须真的落盘」/「回填的 maxResults = 5, want 20」)。

⚠️ 已知取舍(如实登记):**清空输入框 = 400**(后端按「非数 ⇒ 报错」拒),因此面板上
没有「去掉上限」的手势 —— 要回到 `.env` 兜底得改文件;要支持「清空 = 去掉覆盖」
需另定哨兵语义,未自行发挥。
⚠️ 契约面已变更(面板声明多一个 number 字段 + saveProvider 的 values 多一个可写键 +
响应多一个回显键)⇒ 主会话登记。
This commit is contained in:
AskaEth committed 2026-10-07 11:10:56 +08:00
1 parent 8325d1bb8c
commit c68387a69a
8 files changed
+387 -31

No files matched your search

+44 -1
View File
@@ -3,13 +3,15 @@ package config
import (
"os"
"path/filepath"
"strconv"
"strings"
"testing"
)
// 本文件钉住切片 WS1-B 的搜索配置键(design/27 §3):
// WEB_SEARCH_PROVIDER / _FALLBACK / _MAX_RESULTS 与各家独立一组键
// (WEB_SEARCH_<NAME>_API_KEY / _BASE_URL),以及 ResolveProvider 的关闭语义。
// (WEB_SEARCH_<NAME>_API_KEY / _BASE_URL),以及 ResolveProvider 的关闭语义;
// 末尾还钉住 `.env.example` 的取值与代码默认值同口径(模板写错就是一句假话)。
// wsTestLoad 写一份只含必填键加 extra 行的 .env,加载出配置。
func wsTestLoad(t *testing.T, extra ...string) (*Config, error) {
@@ -126,3 +128,44 @@ func TestWebSearchConfig_ResolveProvider(t *testing.T) {
})
}
}
// TestWebSearchMaxResults_EnvExampleMatchesCodeDefault 是「配置事实 ↔ 模板」守卫:
// `.env.example` 是给人照抄的模板,它给出的取值必须等于代码默认值 —— 模板写 5 而代码默认
// 已改成 10(2026-10-07 的口径调整漏改了模板),照抄模板的人拿到的就不是默认行为。
//
// ⚠️ 只读模板;`.env` 已冻结(基础键不加/不改/不删),本用例不碰它。
func TestWebSearchMaxResults_EnvExampleMatchesCodeDefault(t *testing.T) {
cfg, err := wsTestLoad(t)
if err != nil {
t.Fatalf("Load: %v", err)
}
raw, err := os.ReadFile(filepath.Join("..", "..", ".env.example"))
if err != nil {
t.Fatalf("read .env.example: %v", err)
}
const key = "WEB_SEARCH_MAX_RESULTS"
value, err := strconv.Atoi(wsEnvExampleValue(t, string(raw), key))
if err != nil {
t.Fatalf(".env.example 的 %s 取值不是整数: %v", key, err)
}
if value != cfg.WebSearch.MaxResults {
t.Fatalf(".env.example 的 %s = %d,代码默认 = %d(模板与代码必须同口径)",
key, value, cfg.WebSearch.MaxResults)
}
}
// wsEnvExampleValue 取 `.env.example` 里某个键的**生效**取值(跳过注释行)。
func wsEnvExampleValue(t *testing.T, raw, key string) string {
t.Helper()
for _, line := range strings.Split(raw, "\n") {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "#") {
continue
}
if rest, ok := strings.CutPrefix(line, key+"="); ok {
return strings.TrimSpace(rest)
}
}
t.Fatalf(".env.example 里没有生效的 %s=…(模板必须给出可照抄的取值)", key)
return ""
}