Files
Cyrene_v2/plugins/tools/io/search/paneldata.go
T
AskaEth ce9af42945
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(search): 新增腾讯云·元宝搜索与秘塔搜索两家 provider
接入位表六家扩到八家,端点形状与错误码一字未改(契约 v0.87,仅两处名单)。

- tencent_wsa:POST {base}/SearchPro + Authorization Bearer,零签名。
  两条官方口径决定实现:HTTP 状态码恒 200(判失败只能读 Response.Error.Code);
  Pages 是「JSON 字符串数组」需二次解析。错误码按官方表分类
  (UnauthorizedOperation/AuthFailure.* = 凭据、ResourceUnavailable = 欠费、
  RequestLimitExceeded* = 限流),文案只带 code 不带上游 Message。
- metaso:POST {base}/api/v1/search,响应 schema 仅第三方来源且互相打架,
  实现走宽松解析(数组候选与字段名回退),逐处标「未实测」。
- 零新 env 键:两家只走 $DATA_DIR/config/plugins/web-search.json,
  .env / .env.example 未动;组合根 websearch_providers.go 零功能改动。
- 契约 v0.87:values.provider 合法闭集与 plugin.providers 候选各加两条,
  计数不变(14 + 25 + 40 = 79)。
2026-10-07 13:42:41 +08:00

143 lines
6.4 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)",
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
}