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

+19 -11
View File
@@ -80,9 +80,9 @@ func (e webSearchActionEnv) mergeSelectionPatch(
// checkPrimaryUsable 判定一个主 provider 名在**落盘后的文件**里是否可用。
//
// 判定口径与工具接线同源(searchProviderSpecs + effectiveBaseURL):云家看 API Key、
// searxng 看接口地址,基地址缺位时回落到代码默认值;条目被显式停用视同没配
// (effectiveSearchConfig 会跳过它)。
// 判定口径与工具接线同源(resolveSearchProviders + resolveBaseURL):云家看 API Key、
// searxng 看接口地址,基地址缺位时回落**该接入位的代码默认基地址**;条目被显式停用
// 视同没配(装配期会跳过它)。判据本身**不放松**:真的没配齐的主家仍必须被拒。
func (e webSearchActionEnv) checkPrimaryUsable(f runtimeconfig.WebSearchFile, name string) error {
// 显式关闭(空串 / off / none)永远合法:它表示「不要这把工具」,工具不进注册面,
// 后端照常启动(与 WEB_SEARCH_PROVIDER 的既有语义逐字一致)。
@@ -93,19 +93,27 @@ func (e webSearchActionEnv) checkPrimaryUsable(f runtimeconfig.WebSearchFile, na
return err
}
entry, found := findProviderEntry(f, name)
base := ""
if found {
base = e.effectiveBaseURL(name, entry)
if !found || !entry.IsEnabled() {
return primaryNotConfiguredError(name)
}
if !found || !entry.IsEnabled() || !search.Configured(name, entry.APIKey, base) {
return pluginmgr.NewActionError(actionCodePrimaryNotConfigured,
fmt.Sprintf("主提供商 %q 还没配齐(缺 API Key / 接口地址,或这条被停用了):"+
"先把它配好再选它 —— 否则这份配置会让后端下一次启动直接失败", name),
http.StatusBadRequest, nil)
// 基地址的进位链与装配期**同一个函数**:条目里的值 → 该接入位的默认基地址 → 空。
// 云家的可用性只看 key(地址取默认值也照样能打通),searxng 的默认值是空
// —— 因此「searxng 主家必须有地址」这条判据与既往逐字一致。
if !search.Configured(name, entry.APIKey, resolveBaseURL(name, entry.BaseURL)) {
return primaryNotConfiguredError(name)
}
return nil
}
// primaryNotConfiguredError 造「主家没配齐」这一条的 400:文案与码只有一份
// (两个判据分支共用,免得两处措辞漂移)。
func primaryNotConfiguredError(name string) error {
return pluginmgr.NewActionError(actionCodePrimaryNotConfigured,
fmt.Sprintf("主提供商 %q 还没配齐(缺 API Key / 接口地址,或这条被停用了):"+
"先把它配好再选它 —— 否则这份配置会让后端下一次启动直接失败", name),
http.StatusBadRequest, nil)
}
// selectionNames 返回降级链的线形状(**非 nil 空切片**:nil 会在 JSON 里变成 null,
// 而「一条降级都没有」与「这个字段没值」在前端不该长得不一样)。
func selectionNames(names []string) []string {