主人报障:「为什么中文搜索接口……下拉菜单里全给的英文名?你甚至不愿意带个括号
把中文名写上去」。根因不是漏写文案,而是整条链路没有「显示名」的位置:字段
声明只有 optionsFrom,候选动作回的就是 KnownNames() 的纯 id 数组。
- 后端:候选改为对象数组 {value,label};label 表与接入位表同源(键用
NameSearXNG 等常量),未知 id 原样回。value 逐字等于 KnownNames()。
- 前端:下拉/多选「显示 label、写 value」;表格单元格按 value→label 索引显示,
查不到回落显示原值。多选从裸输入框改成「候选开关 + 落盘值输入框」。
解码器本就兼容裸字符串/{value,label}/{id} 三形状 ⇒ 其他插件零影响。
中文名逐字:SearXNG 本地兜底(searxng)/ 智谱 BigModel(zhipu)/
阿里云 IQS 信息查询(aliyun_iqs)/ 博查 Bocha(bocha)/
火山引擎·豆包搜索(volcengine)/ 百度千帆 AI 搜索(baidu_qianfan)。
验收(主会话复跑 + 亲自变异):go build ./... + vet 绿;search 包测试绿;
tsc --noEmit 0 错;新渲染测试 26 checks + 既有 plugin panel 55 / data 90 /
schema values 40 / channel config 70 全绿。我用 sed 把 values.ts 的
option.value 全换成 option.label ⇒ 渲染测试红(「点选把候选项的 id 追加到
降级链末尾」got="bocha, 智谱 BigModel(zhipu)")—— 这正是会把配置文件写坏的
错法;还原后 md5 一致且绿。
⚠️ 契约面变更(我在 Docs/api-client.md 登记):plugin.providers 候选响应形状
字符串数组 → 对象数组;label 仅供显示、value 才是落盘值。
⚠️ 已知边界:viewer 角色看不到 form 区块 ⇒ 表格对 viewer 仍显示裸 id(未做)。
⚠️ 真机浏览器点击未跑(项目无 jsdom)⇒ 建议刷新管理页点一次确认观感。
141 lines
6.3 KiB
Go
141 lines
6.3 KiB
Go
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)",
|
||
}
|
||
}
|
||
|
||
// 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
|
||
}
|