Files
Cyrene_v2/plugins/tools/io/search/paneldata.go
T
AskaEth c46053e276
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(admin): 搜索提供商下拉/多选/表格显示中文名(值仍是 ASCII id)
主人报障:「为什么中文搜索接口……下拉菜单里全给的英文名?你甚至不愿意带个括号
把中文名写上去」。根因不是漏写文案,而是整条链路没有「显示名」的位置:字段
声明只有 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)⇒ 建议刷新管理页点一次确认观感。
2026-10-07 10:08:19 +08:00

141 lines
6.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}