Files
Cyrene_v2/cmd/cyrene/websearch_probe.go
T
AskaEth 2e1c3ed3fd feat(plugins): 面板动作执行器接线 —— 动作真可用,不再 501 兜底(L1)
- 插件管理器加生产动作处理器登记面(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 补)。
2026-10-06 14:12:23 +08:00

118 lines
4.8 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 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"
}