package search import "strings" // 本文件是「搜索提供商」页的**数据面**:表格行与下拉选项的来源。 // // 它只做**翻译**(事实 → 面板要的形状),不读文件、不开路由、不鉴权 —— 那些是插件管理器 // 与宿主端点的事(design/09 §5.2.1 B)。放这里的理由只有一个:`state` 与选项值的语义 // 属于 provider 领域(要问 `Configured` / `KnownNames`),散到宿主里就会与面板声明各写一套。 // 表格 `state` 列的取值(design/09 §5.2 示例:「ok | 未配置 | 凭据失效 | 未知」)。 // // 「凭据失效」不在这里:它只能由一次真实调用得知(那是 plugin.testProvider 的结论, // 见 probe.go),列表接口不该为了它去逐家发请求。 const ( // StateOK 表示这家可以用了(名字已登记且已配齐且启用)。 StateOK = "ok" // StateNotConfigured 表示这个名字已登记,但没配齐(云家缺 key、searxng 缺地址)。 StateNotConfigured = "not_configured" // StateDisabled 表示这条已被显式停用(enabled=false)。 StateDisabled = "disabled" // StateUnknown 表示这个名字本期没登记(写错了 / 还没做,如 tavily)。 StateUnknown = "unknown" ) // ProviderInput 是构造一行所需的最小事实(一条 provider 配置)。 // // 刻意用平铺字段而不是引用 runtimeconfig 的类型:search 包是插件实现,不该认识宿主的 // 配置文件形状(那份形状属于宿主,见 internal/config/runtimeconfig/websearch.go)。 type ProviderInput struct { // Provider 是 provider 名。 Provider string // APIKey 是上游凭据(判定是否配齐用,**绝不进行数据**)。 APIKey string // BaseURL 是上游基地址(同上,判定用)。 BaseURL string // Enabled 是启用态。 Enabled bool } // ProviderRow 是「已配置的提供商」表格的一行。字段名即表格列的 key(panel.go 的 columns)。 // // 凭据与基地址**都不进行数据**:列表只需要状态,凭据读侧无明文(design/09 §5.4.2), // 而基地址可能带用户名口令。要看地址就点「重配凭据」,由表单一对一带回。 type ProviderRow struct { Provider string `json:"provider"` State string `json:"state"` Enabled bool `json:"enabled"` } // BuildRows 把各条配置翻译成表格行。 // // 判定顺序是把「根因」排在前面:名字没登记(写错了)> 显式停用 > 没配齐 > 可用。 // 一个停用的陌生名字报 `unknown` —— 名字错了才是要修的那处。 func BuildRows(inputs []ProviderInput) []ProviderRow { out := make([]ProviderRow, 0, len(inputs)) for _, in := range inputs { out = append(out, ProviderRow{ Provider: strings.ToLower(strings.TrimSpace(in.Provider)), State: providerState(in), Enabled: in.Enabled, }) } return out } // providerState 判定一条配置的表格状态。 func providerState(in ProviderInput) string { name := strings.ToLower(strings.TrimSpace(in.Provider)) if !Known(name) { return StateUnknown } if !in.Enabled { return StateDisabled } if !Configured(name, in.APIKey, in.BaseURL) { return StateNotConfigured } return StateOK } // ProviderOption 是下拉候选的一项。**值 / 显示名分离**是本类型的全部意义: // // - `Value` 是**唯一真源** —— 它会被写进 `$DATA_DIR/config/plugins/web-search.json`, // 并被注册表按名字取用(`Registry.Lookup`),大小写与下划线不许动; // - `Label` 只是**给人看**的显示名(中文名 + 括注 id),任何写盘路径都不得使用它。 // // 为什么需要这个类型(此前只回 `["searxng", …]`):面板的候选只回 ASCII id 时,管理页的 // 下拉与表格里全是英文名(主人 2026-10-06 报障:「全给的英文名,你甚至不愿意带个括号把 // 中文名写上去」)。但候选是**声明面**,不能因此把中文写进配置 —— 所以值仍回 id, // 中文名另走一路(`Label`),由前端只负责显示。 type ProviderOption struct { // Value 是 provider 名,逐字等于 `KnownNames()` 的输出。 Value string `json:"value"` // Label 是显示名,形如「智谱 BigModel(zhipu)」:中文名 + 括注 id,人工核对时能对上。 Label string `json:"label"` } // providerLabels 是 id → 显示名。键用 provider 名常量(与 registry.definitions() 同源), // 免得「新加一家但标签表用字面量写」时两边各写一份、悄悄分叉。 // // 这里**不**给未知 id 兜一个假名字:`ProviderLabel` 一律原样回(见下)。 func providerLabels() map[string]string { return map[string]string{ NameSearXNG: "SearXNG 本地兜底(searxng)", NameZhipu: "智谱 BigModel(zhipu)", NameAliyunIQS: "阿里云 IQS 信息查询(aliyun_iqs)", NameBocha: "博查 Bocha(bocha)", NameVolcengine: "火山引擎·豆包搜索(volcengine)", NameBaiduQianfan: "百度千帆 AI 搜索(baidu_qianfan)", NameTencentWSA: "腾讯云·元宝搜索(tencent_wsa)", NameMetaso: "秘塔搜索 Metaso(metaso)", } } // ProviderLabel 给出一个 provider 名的显示名:标签表里有就用它(大小写与首尾空白不敏感, // 与 `Known` 同口径),**取不到一律原样回** —— 新增接入位而忘了加标签时,面板显示成 id, // 不崩、不丢,也不编一个假中文名出来。空白名照原样回空白。 func ProviderLabel(name string) string { label := strings.TrimSpace(providerLabels()[strings.ToLower(strings.TrimSpace(name))]) if label == "" { return name } return label } // ProviderOptions 返回 `optionsFrom: plugin:providers` 的候选项 —— 本期**全部已登记**的 // provider(含尚未配置的家),稳定排序,每项带值 + 显示名。 // // 为什么给全部而不是「已配置的家」:表单的用途就是**新增**一家,只列已配置的会让 // 「第一次配置」无从选起。可用性不靠下拉限制,靠列表的 `state` 列如实呈现。 // // 宿主把返回值原样放进动作响应(`{"options":[{"value":…,"label":…}]}`),前端只拿 `label` // 显示、只把 `value` 写回。 func ProviderOptions() []ProviderOption { names := KnownNames() out := make([]ProviderOption, 0, len(names)) for _, name := range names { out = append(out, ProviderOption{Value: name, Label: ProviderLabel(name)}) } return out }