- 插件管理器加生产动作处理器登记面(RegisterActions);未登记处理器/未声明动作仍 404 - web-search 四个动作真读写真源 + 真发 HTTP 探针:plugin.listProviders/saveProvider/ testProvider/reauthProvider(配置落 $DATA_DIR/config/plugins/web-search.json,0600) - 失败回结构化错误码(不含上游原文)+ 审计带 code 字段;501 文案改为「未登记处理器」 - 拆出 panelactions.go(动作索引推导/OptionsActionName/三闸门分发),panels.go 519→347 行 缺口(如实):面板保存后运行中的工具**不热重载**(下一片做);声明面缺 primary/fallback (按 DECISIONS-2026-10-06 #1/#2 补)。
118 lines
4.8 KiB
Go
118 lines
4.8 KiB
Go
package main
|
||
|
||
// 本文件是「搜索提供商」页**探针**的传输层绑定(切片 L1/PLG-2 的执行面接线的一部分):
|
||
// 探针要用的 HTTP 客户端、目标约束(同主机守卫)与失败状态映射。
|
||
// 动作本体的语义在 `websearch_action_data.go`,登记面在 `wire_plugin_actions.go`。
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"net/http"
|
||
"net/url"
|
||
"strings"
|
||
"time"
|
||
|
||
"git.yeij.top/AskaEth/Cyrene_v2/plugins/tools/io/search"
|
||
)
|
||
|
||
// 探针的边界值。名字化的理由与 io 包同一套:不许裸魔法数字(项目规范 §10)。
|
||
const (
|
||
// webSearchProbeTimeout 是探针的兜底超时(面板文件与 `.env` 都没声明时用它)——
|
||
// 取 io 工具集的默认 http 超时同值(15s),免得「面板测通了、工具超时」这种怪象。
|
||
webSearchProbeTimeout = 15 * time.Second
|
||
// webSearchProbeMaxBytes 是探针响应体的读取上限(与 io 包的 http 响应上限同口径 1 MiB)。
|
||
webSearchProbeMaxBytes = 1 << 20
|
||
// webSearchProbeMaxRedirects 是探针允许的跳转次数(与 io 的 maxRedirects 同值)。
|
||
webSearchProbeMaxRedirects = 3
|
||
)
|
||
|
||
// probeErrorStatus 按探针码给出 HTTP 状态:调用方/配置问题 ⇒ 400;上游或网络 ⇒ 502。
|
||
//
|
||
// 它**不**改变探针的语义(no_results 仍然是 no_results,不是「成功」也不是「该家不可用」),
|
||
// 只是给前端一个非 2xx 的信号,好让面板把结论显示出来(见 testProvider 的注释)。
|
||
func probeErrorStatus(code string) int {
|
||
switch code {
|
||
case search.ProbeUnknownProvider, search.ProbeNotConfigured:
|
||
return http.StatusBadRequest
|
||
default:
|
||
return http.StatusBadGateway
|
||
}
|
||
}
|
||
|
||
// probeTarget 是一次探针的目标约束:**只允许打到配置里那台主机**。
|
||
//
|
||
// 与 io 包 SSRF 守卫的关系(写清楚,免得被当成第二份安全策略):地址来自面板里存下的
|
||
// provider 基地址 —— 它是**运维输入**(owner 通过 owner/admin 写动作落下的),不是模型
|
||
// 输入。io 的守卫用「私网/回环黑名单 + 允许清单」,而那个守卫在 `plugins/tools/io`
|
||
// 内部未导出,本片**不改他人文件**(硬边界),因此这里用一条**更严**的规则替代:
|
||
// 探针只与配置主机通话,跨主机(含跨主机重定向)一律拒绝。
|
||
// 缺口如实登记:两处策略不逐字同源(见报告 §未做)。
|
||
type probeTarget struct {
|
||
// host 是配置主机名(小写、无端口)。
|
||
host string
|
||
}
|
||
|
||
// newProbeTarget 解析出配置主机;地址为空 / 非 http(s) / 没有主机名时返回错误。
|
||
func newProbeTarget(baseURL string) (probeTarget, error) {
|
||
raw := strings.TrimSpace(baseURL)
|
||
if raw == "" {
|
||
return probeTarget{}, fmt.Errorf("web-search probe: empty base url")
|
||
}
|
||
u, err := url.Parse(raw)
|
||
if err != nil {
|
||
return probeTarget{}, fmt.Errorf("web-search probe: parse base url: %w", err)
|
||
}
|
||
if !isHTTPURL(u) {
|
||
return probeTarget{}, fmt.Errorf("web-search probe: unsupported scheme %q", u.Scheme)
|
||
}
|
||
host := strings.ToLower(u.Hostname())
|
||
if host == "" {
|
||
return probeTarget{}, fmt.Errorf("web-search probe: base url has no host")
|
||
}
|
||
return probeTarget{host: host}, nil
|
||
}
|
||
|
||
// client 造这次探针要用的 HTTP 客户端:超时上限 + 同主机的跳转策略。
|
||
// 它是**每次调用新建**的(探针很少发生,不值得为它维护一份共享客户端与连接池);
|
||
// 也因此不牵动 http / web-fetch / web-search 共用的那份客户端与白名单。
|
||
func (t probeTarget) client(timeout time.Duration) *http.Client {
|
||
return &http.Client{Timeout: timeout, CheckRedirect: t.checkRedirect}
|
||
}
|
||
|
||
// guard 是注入给 search.Config 的地址判定(发请求前对最终地址做一次判定)。
|
||
func (t probeTarget) guard(_ context.Context, rawURL string) error { return t.check(rawURL) }
|
||
|
||
// checkRedirect 是跳转策略:次数上限 + 每一跳都要过同一条判定。
|
||
func (t probeTarget) checkRedirect(req *http.Request, via []*http.Request) error {
|
||
if len(via) >= webSearchProbeMaxRedirects {
|
||
return fmt.Errorf("web-search probe: too many redirects")
|
||
}
|
||
if req.URL == nil {
|
||
return fmt.Errorf("web-search probe: nil redirect target")
|
||
}
|
||
return t.check(req.URL.String())
|
||
}
|
||
|
||
// check 判定一个地址能否访问:必须是 http(s) 且主机等于配置主机。
|
||
func (t probeTarget) check(rawURL string) error {
|
||
u, err := url.Parse(rawURL)
|
||
if err != nil {
|
||
return fmt.Errorf("web-search probe: parse target: %w", err)
|
||
}
|
||
if !isHTTPURL(u) {
|
||
return fmt.Errorf("web-search probe: unsupported scheme %q", u.Scheme)
|
||
}
|
||
if !strings.EqualFold(u.Hostname(), t.host) {
|
||
return fmt.Errorf("web-search probe: refused cross-host target %q", u.Hostname())
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// isHTTPURL 报告地址是否是可访问的 http(s) 绝对地址。
|
||
func isHTTPURL(u *url.URL) bool {
|
||
if u == nil {
|
||
return false
|
||
}
|
||
return u.Scheme == "http" || u.Scheme == "https"
|
||
}
|