主人原话:「返回五条结果会不会太少了?」。裁定:**上限抬高到 10,且它是真上限**
(模型要 50 也只给 10);模型要更少(3)时**尊重模型**;缺省/非法(≤0/非数)⇒ 上限。
- plugins/tools/io/web_search.go:`n := resultLimit(s.maxResults, m)` 一处收口
(原来是 `if count>0 { n = count }` ⇒ 模型的 count 能顶掉配置上限,与
.env.example 写的「条数上限」不符 —— 那是一条真口径缺陷,本次一并修正)。
- internal/config/websearch.go:`WEB_SEARCH_MAX_RESULTS` 代码默认值 5 → 10
(三处口径现已一致:env 默认 10 / io 包 defaultMaxResults 10 / 各家发给上游的
请求区间是另一回事,未动)。
- 生产:`data/config/plugins/web-search.json` 的 `maxResults` 5 → 10 + 一次
saveProvider 触发**热重载**(applied+registered),盘上已生效。
验收(主会话复跑 + 亲自打两个变异):
- io 包 / internal/config / cmd/cyrene 相关用例全绿;
- 变异 A(拆掉上限、恒取 50)⇒ **4 条用例变红**(含「默认上限 10」「count 非数值
⇒ 上限」「count=0 ⇒ 上限」「既有 MaxResultsOption」);
- 变异 B(默认值退回 5)⇒ internal/config 的默认值断言变红。
🔴 顺带挖出一条真缺陷(**本片只报不修,等派**):`maxResults` **在面板 UI 上
根本改不了** —— ① `search/panel.go` 的字段表里没有这个字段(输入框不显示);
② `saveProvider` 只并 provider/apiKey/baseUrl/enabled;③ `mergeSelectionPatch`
只处理 primary/fallback ⇒ **手工塞进 values 也会被静默丢弃**(「我改了它却没变」
成立)。修它要给面板加一个数字字段 + 三态落 `f.MaxResults`,属**管理面板对外契约
变更**(须同步 Docs/api-client.md),留给主会话。
129 lines
4.7 KiB
Go
129 lines
4.7 KiB
Go
package config
|
||
|
||
import (
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
// 本文件钉住切片 WS1-B 的搜索配置键(design/27 §3):
|
||
// WEB_SEARCH_PROVIDER / _FALLBACK / _MAX_RESULTS 与各家独立一组键
|
||
// (WEB_SEARCH_<NAME>_API_KEY / _BASE_URL),以及 ResolveProvider 的关闭语义。
|
||
|
||
// wsTestLoad 写一份只含必填键加 extra 行的 .env,加载出配置。
|
||
func wsTestLoad(t *testing.T, extra ...string) (*Config, error) {
|
||
t.Helper()
|
||
lines := append([]string{"JWT_SECRET=" + strings.Repeat("s", 40)}, extra...)
|
||
path := filepath.Join(t.TempDir(), ".env")
|
||
if err := os.WriteFile(path, []byte(strings.Join(lines, "\n")+"\n"), 0o600); err != nil {
|
||
t.Fatalf("write .env: %v", err)
|
||
}
|
||
return Load(path)
|
||
}
|
||
|
||
// TestWebSearchProvider_Defaults 钉住三个新键的默认值:provider=searxng、
|
||
// maxResults=10、fallback 为空(= 只管手选那一家)。
|
||
func TestWebSearchProvider_Defaults(t *testing.T) {
|
||
cfg, err := wsTestLoad(t)
|
||
if err != nil {
|
||
t.Fatalf("Load: %v", err)
|
||
}
|
||
if cfg.WebSearch.Provider != "searxng" {
|
||
t.Fatalf("WEB_SEARCH_PROVIDER 默认值 = %q, want searxng", cfg.WebSearch.Provider)
|
||
}
|
||
if cfg.WebSearch.MaxResults != 10 {
|
||
t.Fatalf("WEB_SEARCH_MAX_RESULTS 默认值 = %d, want 10", cfg.WebSearch.MaxResults)
|
||
}
|
||
if len(cfg.WebSearch.Fallback) != 0 {
|
||
t.Fatalf("WEB_SEARCH_FALLBACK 未配置时应为空, got %v", cfg.WebSearch.Fallback)
|
||
}
|
||
// 云家的 base URL 有默认值、key 一律为空(未配置 ⇒ 该家不出现在可用清单里)。
|
||
if cfg.WebSearch.ZhipuBaseURL == "" || cfg.WebSearch.BochaBaseURL == "" ||
|
||
cfg.WebSearch.AliyunIQSBaseURL == "" || cfg.WebSearch.VolcengineBaseURL == "" ||
|
||
cfg.WebSearch.BaiduQianfanBaseURL == "" {
|
||
t.Fatalf("各家的 BASE_URL 必须有默认值: %+v", cfg.WebSearch)
|
||
}
|
||
if cfg.WebSearch.ZhipuAPIKey != "" || cfg.WebSearch.BochaAPIKey != "" ||
|
||
cfg.WebSearch.AliyunIQSAPIKey != "" || cfg.WebSearch.VolcengineAPIKey != "" ||
|
||
cfg.WebSearch.BaiduQianfanAPIKey != "" {
|
||
t.Fatalf("各家 API key 未配置时必须为空: %+v", cfg.WebSearch)
|
||
}
|
||
}
|
||
|
||
// TestWebSearchProvider_EnvOverrides 钉住新键都能被 .env 覆盖(含逗号分隔的降级链)。
|
||
func TestWebSearchProvider_EnvOverrides(t *testing.T) {
|
||
cfg, err := wsTestLoad(t,
|
||
"WEB_SEARCH_PROVIDER=Bocha",
|
||
"WEB_SEARCH_FALLBACK=zhipu,bocha,searxng",
|
||
"WEB_SEARCH_MAX_RESULTS=9",
|
||
"WEB_SEARCH_ZHIPU_API_KEY=zk",
|
||
"WEB_SEARCH_ZHIPU_BASE_URL=https://zhipu.example",
|
||
"WEB_SEARCH_ALIYUN_IQS_API_KEY=ak",
|
||
"WEB_SEARCH_ALIYUN_IQS_BASE_URL=https://iqs.example",
|
||
"WEB_SEARCH_BOCHA_API_KEY=bk",
|
||
"WEB_SEARCH_BOCHA_BASE_URL=https://bocha.example",
|
||
"WEB_SEARCH_VOLCENGINE_API_KEY=vk",
|
||
"WEB_SEARCH_VOLCENGINE_BASE_URL=https://volc.example",
|
||
"WEB_SEARCH_BAIDU_QIANFAN_API_KEY=qk",
|
||
"WEB_SEARCH_BAIDU_QIANFAN_BASE_URL=https://qianfan.example",
|
||
)
|
||
if err != nil {
|
||
t.Fatalf("Load: %v", err)
|
||
}
|
||
ws := cfg.WebSearch
|
||
if ws.Provider != "Bocha" {
|
||
t.Fatalf("Provider = %q(大小写应原样保留,比较时再归一化)", ws.Provider)
|
||
}
|
||
if got := ws.Fallback; len(got) != 3 || got[0] != "zhipu" || got[2] != "searxng" {
|
||
t.Fatalf("Fallback = %v, want [zhipu bocha searxng]", got)
|
||
}
|
||
if ws.MaxResults != 9 {
|
||
t.Fatalf("MaxResults = %d, want 9", ws.MaxResults)
|
||
}
|
||
for name, got := range map[string]string{
|
||
"Zhipu": ws.ZhipuAPIKey, "AliyunIQS": ws.AliyunIQSAPIKey, "Bocha": ws.BochaAPIKey,
|
||
"Volcengine": ws.VolcengineAPIKey, "BaiduQianfan": ws.BaiduQianfanAPIKey,
|
||
} {
|
||
if got == "" {
|
||
t.Errorf("%s 的 API key 没被读进来", name)
|
||
}
|
||
}
|
||
for name, got := range map[string]string{
|
||
"Zhipu": ws.ZhipuBaseURL, "AliyunIQS": ws.AliyunIQSBaseURL, "Bocha": ws.BochaBaseURL,
|
||
"Volcengine": ws.VolcengineBaseURL, "BaiduQianfan": ws.BaiduQianfanBaseURL,
|
||
} {
|
||
if !strings.HasSuffix(got, ".example") {
|
||
t.Errorf("%s 的 BASE_URL 没被覆盖: %q", name, got)
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestWebSearchConfig_ResolveProvider 是关闭语义的表驱动:空 / off / none ⇒ 关闭
|
||
// (组合根据此不注册工具);其余 ⇒ 选定名。
|
||
func TestWebSearchConfig_ResolveProvider(t *testing.T) {
|
||
cases := []struct {
|
||
name string
|
||
value string
|
||
wantDisable bool
|
||
wantSel string
|
||
}{
|
||
{"默认 searxng", "searxng", false, "searxng"},
|
||
{"大写 Zhipu", " Zhipu ", false, "Zhipu"},
|
||
{"空串 = 关闭", "", true, ""},
|
||
{"off", "off", true, ""},
|
||
{"none", "NONE", true, ""},
|
||
{"带空白的 None", " none ", true, ""},
|
||
}
|
||
for _, tc := range cases {
|
||
tc := tc
|
||
t.Run(tc.name, func(t *testing.T) {
|
||
got := WebSearchConfig{Provider: tc.value}.ResolveProvider()
|
||
if got.Disabled != tc.wantDisable || got.Selected != tc.wantSel {
|
||
t.Fatalf("ResolveProvider(%q) = %+v, want disabled=%v selected=%q",
|
||
tc.value, got, tc.wantDisable, tc.wantSel)
|
||
}
|
||
})
|
||
}
|
||
}
|