Files
Cyrene_v2/plugins/tools/io/search/probe.go
T
AskaEth 120d926cf8 feat(search): 搜索 provider 可变源 + 插件声明面板(PLG-4)
- 新增 $DATA_DIR/config/plugins/web-search.json(0600 + temp+rename,文件名 = 插件 id)
- 组合根改「先读可变源、缺失回落 .env 播种」;**不新增任何 env 键**(.env 已冻结)
- 多实例:每家 provider 独立 key/baseURL/启用态 + 「测试连通」探针
- 用 PLG-1 的声明库给出该面板的 adminPanels 声明;动作名交给 PLG-2 的通用动作端点分发
  (本片自己不开路由)

⚠️ 本片自认越界:改了 cmd/cyrene/wire_web_search_test.go(既有测试,为适配 DataDir);
  另有 design/28 的相关注册留给主会话。
2026-10-06 09:04:04 +08:00

190 lines
7.9 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 (
"context"
"errors"
"fmt"
"strings"
"time"
)
// 本文件是「测试连通」动作(design/09 §5.2 示例的 plugin.testProvider)的数据面:
// 对**一条** provider 配置真发一次最小检索,把结论分成「通了」与「为什么没通」。
//
// 三条口径:
//
// 1. **真发一次请求**:连通性不靠 ping 主机、也不靠「配置看起来齐了」来假装 ——
// 那会给出「连通 OK 但一搜就报错」的假信号。这里走的就是工具运行时走的那条通路
// (同一个 provider 构造、同一个共享归一化)。
// 2. **空结果 ≠ 不通**(design/27 §1.4):上游正常应答但一条都没查到,如实报
// no_results —— 既不冒充成功,也不冤枉成「该家不可用」。
// 3. **密钥绝不进结果**:只回结构化错误码 + 固定文案,**绝不回上游原始报文**
// (错误体可能回显请求 URL 或凭据)。文案与错误码分离,前端可本地化(§5.4.6)。
//
// 本文件不读配置文件、不开路由、不落审计 —— 那些是插件管理器与宿主端点的事。
// 探针的查询词与条数:一次**最小但真实**的检索(只要 1 条,省上游额度)。
const (
probeQuery = "cyrene"
probeCount = 1
)
// 探针结论的错误码(稳定标识;design/09 §5.4.6「错误码与文案分离」)。
const (
// ProbeUnknownProvider 报告名字本期没登记(写错了 / 还没做)。
ProbeUnknownProvider = "unknown_provider"
// ProbeNotConfigured 报告这条没配齐(云家缺 API Key,或任何一家缺接口地址)。
ProbeNotConfigured = "not_configured"
// ProbeUnauthorized 报告上游拒了凭据(401/403)。
ProbeUnauthorized = "unauthorized"
// ProbeRateLimited 报告被上游限流(429)。
ProbeRateLimited = "rate_limited"
// ProbeQuotaExceeded 报告额度用尽 / 欠费 / 未开通。
ProbeQuotaExceeded = "quota_exceeded"
// ProbeNetwork 报告网络层失败(连不上 / DNS / 超时)。
ProbeNetwork = "network"
// ProbeUpstream 报告上游 5xx 或其它无法进一步分类的失败状态。
ProbeUpstream = "upstream"
// ProbeBadResponse 报告上游应答不是预期结构(或超过读取上限)。
ProbeBadResponse = "bad_response"
// ProbeNoResults 报告上游正常应答但没有任何结果(**不算失败**,只是没内容)。
ProbeNoResults = "no_results"
// ProbeUnknown 是兜底:未分类的失败。
ProbeUnknown = "unknown"
)
// ProbeResult 是「测试连通」的结论(面板行内动作的返回体)。
type ProbeResult struct {
// OK 报告这次探针真的拿到结果了(空结果不是 OK)。
OK bool `json:"ok"`
// Provider 是被探的那一家(小写归一后的名字)。
Provider string `json:"provider"`
// Count 是这次探针拿到的结果条数(OK 时 > 0)。
Count int `json:"count"`
// LatencyMS 是这次探针的耗时(毫秒),含建连与读体。
LatencyMS int64 `json:"latencyMs"`
// ErrorCode 是失败时的稳定错误码(OK 时为空)。
ErrorCode string `json:"errorCode,omitempty"`
// Error 是与错误码配对的固定文案(**不含上游原文、不含凭据**)。
Error string `json:"error,omitempty"`
}
// Probe 对 name 这一家做一次真实的最小检索(count ≤ 0 时用 probeCount)。
//
// cfg 由调用方填好(凭据 / 基地址 / 共享 HTTP 客户端 / SSRF 守卫),与工具运行时用的是
// 同一套接线;cfg.Client 为 nil 时如实报 not_configured(发不出请求就是没配齐)。
func Probe(ctx context.Context, name string, cfg Config, count int) (res ProbeResult) {
res.Provider = strings.ToLower(strings.TrimSpace(name))
if count <= 0 {
count = probeCount
}
start := time.Now()
defer func() { res.LatencyMS = time.Since(start).Milliseconds() }()
res.ErrorCode, res.Error = probePreflight(res.Provider, cfg)
if res.ErrorCode != "" {
return res
}
items, err := probeSearch(ctx, res.Provider, cfg, count)
if err != nil {
res.ErrorCode = ProbeErrorCode(err)
res.Error = probeMessage(res.ErrorCode)
return res
}
if len(items) == 0 {
res.ErrorCode = ProbeNoResults
res.Error = probeMessage(ProbeNoResults)
return res
}
res.OK = true
res.Count = len(items)
return res
}
// probePreflight 做发请求前的判定,返回 (错误码, 文案);都为空表示可以发。
//
// 为什么比运行时更严(运行时云家只看 API Key):运行时的基地址有代码默认值兜底
// (`WEB_SEARCH_<NAME>_BASE_URL` 的 tag 默认值),而面板里存下来的那条**可能真的没有地址**
// —— 那种情况下探针必须说「没配齐」,而不是发一个相对 URL 出去报一句看不懂的错。
func probePreflight(name string, cfg Config) (string, string) {
if !Known(name) {
return ProbeUnknownProvider, "未登记的搜索提供商(本期接入位:" +
strings.Join(KnownNames(), ", ") + ")"
}
if !Configured(name, cfg.APIKey, cfg.BaseURL) || strings.TrimSpace(cfg.BaseURL) == "" {
return ProbeNotConfigured, probeMessage(ProbeNotConfigured)
}
return "", ""
}
// probeSearch 建一家 provider 并真发一次检索。error 为 nil 时 items 可能为空
// (空结果由调用方按「空 ≠ 不通」处置)。
func probeSearch(ctx context.Context, name string, cfg Config, count int) ([]Result, error) {
reg := Build(
[]Wire{{Name: name, APIKey: cfg.APIKey, BaseURL: cfg.BaseURL, Configured: true}},
cfg.Client, cfg.MaxBytes, cfg.Guard)
p, ok := reg.Lookup(name)
if !ok {
// 只有「已登记却构造不出来」才走到这里(与 checkSearchSelection 的 ErrNotConfigured 同源)。
return nil, fmt.Errorf("%w: probe: provider %q cannot be built", ErrNotConfigured, name)
}
return p.Search(ctx, Query{Text: probeQuery, Count: count})
}
// ProbeErrorCode 把一次探针失败归类成稳定错误码(errors.Is 判定)。
func ProbeErrorCode(err error) string {
switch {
case err == nil:
return ""
case errors.Is(err, ErrUnknownProvider):
return ProbeUnknownProvider
case errors.Is(err, ErrNotConfigured):
return ProbeNotConfigured
case errors.Is(err, ErrUnauthorized):
return ProbeUnauthorized
case errors.Is(err, ErrRateLimited):
return ProbeRateLimited
case errors.Is(err, ErrQuotaExceeded):
return ProbeQuotaExceeded
case errors.Is(err, ErrNetwork):
return ProbeNetwork
case errors.Is(err, ErrResponseTooLarge), errors.Is(err, ErrBadResponse):
return ProbeBadResponse
case errors.Is(err, ErrNoResults):
return ProbeNoResults
case errors.Is(err, ErrUpstream):
return ProbeUpstream
default:
return ProbeUnknown
}
}
// probeMessage 给出与错误码配对的固定中文文案。
//
// 刻意**不回上游原文**:错误体与网络错误都可能回显请求 URL(某些上游把 key 放在查询串里)
// 或凭据,而这份文案会进面板响应。要看原文就走服务端日志(那里也不含凭据)。
func probeMessage(code string) string {
switch code {
case ProbeUnknownProvider:
return "未登记的搜索提供商:名字写错了,或本期还没做这一家"
case ProbeNotConfigured:
return "这条没配齐:云家需要 API Key,任何一家都需要接口地址"
case ProbeUnauthorized:
return "上游拒绝了凭据(401/403):请核对 API Key 是否有效"
case ProbeRateLimited:
return "被上游限流(429):稍后重试"
case ProbeQuotaExceeded:
return "上游额度用尽 / 欠费 / 未开通该服务"
case ProbeNetwork:
return "连不上上游:网络/域名/超时问题"
case ProbeUpstream:
return "上游返回了失败状态(5xx 或其它)"
case ProbeBadResponse:
return "上游应答不是预期结构(或超过读取上限)"
case ProbeNoResults:
return "连通正常,但这次探针没有查到结果(空结果不等于失败,换别的查询词再试)"
default:
return "探针失败,原因未分类(详见服务端日志)"
}
}