refactor(search): provider 配置面去 env 化 —— 加新家不再需要动 .env
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

动因:`.env` 已冻结(基础键不加/不改/不删),而「加一家 provider」原本会逼出
新的 env 键(一家一个 struct 字段 + searchProviderVector 逐家读 + 逐家写死的
switch)⇒ 想接腾讯云元宝/秘塔就只能破红线。面板本来就该是插件配置的一等来源
($DATA_DIR/config/plugins/web-search.json),故让**文件成为权威**。

- plugins/tools/io/search/registry.go:接入位表加 defaultBaseURL(「没配 baseUrl
  回落代码默认值」从此只有一个出处);新增 Descriptor(s)/DefaultBaseURL。
- cmd/cyrene/websearch_providers.go(新):按接入位表逐家算出生效投影 ——
  加一家时这里**零改动**,这正是本片的核心;顺手把 wire_io_tools.go 从 430 行
  降到 342(它逼近 §10 的 500 行上限)。
- cmd/cyrene/wire_plugin_actions.go / websearch_action_data.go /
  websearch_selection.go:基地址判定统一走「文件值 → 接入位默认值」的进位链;
  checkPrimaryUsable 判据不放松(真没配齐仍 400 primary_not_configured)。

🔴 **一处对我任务书的纠正(子代理提出、我采纳)**:我要求「既有 6 家在文件里
没有该条目时仍回落 .env」。仓库现状**没有**这层回落,且有回归用例把它钉死
(TestWireIOTools_PanelConfigClearedProviderDoesNotReviveFromEnv:「文件里没有
的家不得从 .env 复活」)。本片按「不改变既有行为」实现 —— 我的要求才是错的:
文件一旦存在就是权威,env 只在文件缺失/为空时播种。

验收(主会话复跑 + 亲自变异):go build ./... + 两包 vet 绿;search 包与
cmd/cyrene 全绿;我用 -overlay 让文件里的 apiKey 被忽略 ⇒ 4 条用例变红
(含「无 env 字段的家只认文件也能进注册表」这条核心判据)。

⚠️ 新家仍未接:腾讯云元宝(tencent_wsa)/ 秘塔(metaso)两片随后串行落地。
This commit is contained in:
AskaEth committed 2026-10-07 10:18:58 +08:00
1 parent e5e5207c94
commit bc1dee3f56
8 files changed
+825 -175

No files matched your search

+63
View File
@@ -103,6 +103,69 @@ func TestRegistry_EmptyAndNilSafe(t *testing.T) {
}
}
// TestRegistry_DefaultBaseURLMatchesEnvDefaults 钉住接入位表里的**默认基地址**逐字等于
// `.env` 的标签默认值(`internal/config/websearch.go` 的 `*BaseURL` 那一组,`.env` 冻结的
// 基础键)。
//
// 为什么值得一条逐字表:接入位表是本片新增的「这家没配 baseUrl ⇒ 回落哪个地址」的
// **唯一出处**,而 `.env` 侧那几个键仍然存在、仍然参与播种。两处一旦漂移,同一家 provider
// 会因为「值从哪来」指向两个不同的上游 —— 只有逐字对得上的表拦得住这种不一致。
func TestRegistry_DefaultBaseURLMatchesEnvDefaults(t *testing.T) {
t.Parallel()
want := map[string]string{
NameZhipu: "https://open.bigmodel.cn",
NameAliyunIQS: "https://cloud-iqs.aliyuncs.com",
NameBocha: "https://api.bocha.cn",
NameVolcengine: "https://open.feedcoopapi.com",
NameBaiduQianfan: "https://qianfan.baidubce.com",
}
for name, base := range want {
if got := DefaultBaseURL(name); got != base {
t.Errorf("DefaultBaseURL(%q) = %q, want %q(必须与 .env 的标签默认值逐字一致)", name, got, base)
}
}
// searxng 没有默认基地址:它的地址为空本身就是「关掉这家」(组合根按 ResolveEndpoint
// 裁决整把工具的存亡),回落一个本机地址会让「清空地址」变成假话。
if got := DefaultBaseURL(NameSearXNG); got != "" {
t.Errorf("DefaultBaseURL(%q) = %q, want 空串", NameSearXNG, got)
}
// 未登记的接入位:不猜地址。
if got := DefaultBaseURL("tavily"); got != "" {
t.Errorf("未登记的接入位不该有默认地址, got %q", got)
}
}
// TestRegistry_DescriptorsMatchDefinitions 钉住「只读描述」与接入位表同源:
// 组合根按它逐家投影配置,两处一旦不同步,加一家 provider 就会「漏在注册面外」。
func TestRegistry_DescriptorsMatchDefinitions(t *testing.T) {
t.Parallel()
desc := Descriptors()
if len(desc) != len(KnownNames()) {
t.Fatalf("Descriptors() 有 %d 家,KnownNames() 有 %d 家(两者必须同一张表)",
len(desc), len(KnownNames()))
}
byName := make(map[string]Descriptor, len(desc))
for _, d := range desc {
if _, dup := byName[d.Name]; dup {
t.Fatalf("Descriptors() 里 %q 出现了两次", d.Name)
}
byName[d.Name] = d
}
for _, name := range KnownNames() {
d, ok := byName[name]
if !ok {
t.Fatalf("KnownNames() 里的 %q 不在 Descriptors() 里", name)
}
if d.NeedKey != (name != NameSearXNG) {
t.Errorf("Descriptor(%q).NeedKey = %v(只有 searxng 不需要 key)", name, d.NeedKey)
}
if d.DefaultBaseURL != DefaultBaseURL(name) {
t.Errorf("Descriptor(%q).DefaultBaseURL = %q, DefaultBaseURL(%q) = %q(必须同源)",
name, d.DefaultBaseURL, name, DefaultBaseURL(name))
}
}
}
// TestProviders_ErrorClassification 是验收 ⑤:鉴权 / 限流 / 额度 / 上游 各一条。
// 用同一家 provider + 不同应答,确认分类**如实区分**(错误分类决定降级链是否前进)。
func TestProviders_ErrorClassification(t *testing.T) {