feat(search): provider 热重载 + primary/fallback(W3)
- 保存被包成事务:快照 → 落盘 → 按新配置重建工具 → 原子替换注册表条目(registry.ReplaceTool 窄方法,保留运行期 Policy);下一轮即生效,不要求重启 - 若新配置会让后端下次起不来(主家未配齐 fail-fast)⇒ 文件按字节回滚 + 409 config_unstartable - 声明面补 primary + fallback[](可配可生效);否决「另开 form」方案(会破坏既有 reauth 回填) - 主 provider 解析口径核查结论:没被 PLG-4 改坏(顺手修了一处日志缺陷) - 4 个变异自检(丢 Policy/不换条目/回滚 no-op/忽略 fallback)
This commit is contained in:
1 parent
5ee5f9126f
commit
a30f4a49ff
12 files changed
+1696
-16
No files matched your search
@@ -23,6 +23,11 @@ package main
|
||||
// **启动期报错**,绝不静默回落到兜底。
|
||||
// 5. **超时不另造默认值**:`WEB_SEARCH_TIMEOUT` / 面板的 `timeout` 未声明时沿用 io 包既有
|
||||
// 超时;负值是笔误,按未配置处理并记一条带键名的 WARN(不静默)。
|
||||
//
|
||||
// 同一套解析也服务**热重载**(切片 W3,裁决 2026-10-06 #1):面板保存后由
|
||||
// `buildWebSearchTool` 按**当前落盘配置**重建这一把工具并替换注册表条目,因此
|
||||
// 「启动期的接线」与「保存后的接线」恒为同一条路径(不是两份语义)。两份调用的差别
|
||||
// 只有日志里的 cause 字段(startup / panel-save),便于排障时区分。
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
@@ -32,6 +37,7 @@ import (
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/config"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/config/runtimeconfig"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/logger"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/pkg/plugins/sdk"
|
||||
iotools "git.yeij.top/AskaEth/Cyrene_v2/plugins/tools/io"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/plugins/tools/io/search"
|
||||
)
|
||||
@@ -57,6 +63,21 @@ const (
|
||||
webSearchSourcePanel = "panel"
|
||||
)
|
||||
|
||||
// 解析一次 web-search 配置的**触发场景**(只进日志的 cause 字段)。
|
||||
const (
|
||||
// webSearchCauseStartup 是启动期装配(`ioToolOptions`)。
|
||||
webSearchCauseStartup = "startup"
|
||||
// webSearchCausePanelSave 是面板保存后的热重载(`buildWebSearchTool`,见 websearch_reload.go)。
|
||||
webSearchCausePanelSave = "panel-save"
|
||||
)
|
||||
|
||||
// webSearchToolName 是 web-search 工具在能力注册表里的名字。
|
||||
//
|
||||
// 它与 io 包 `func (s *webSearchTool) Name()` 里的字面量同源,而那个名字没有导出常量
|
||||
// (`plugins/tools/io` 不在本片文件面内,不去改它):因此这里用字面量,并由
|
||||
// websearch_reload_test.go 钉住 —— 名字一旦漂移,热重载会立刻找不到工具(测试红)。
|
||||
const webSearchToolName = "web-search"
|
||||
|
||||
// ioToolOptions 组装 io 工具集的全部选项。wire_runtime 把它原样交给 iotools.New,
|
||||
// 因此「注册面里有哪些 io 工具」只由这一处决定。
|
||||
func (a *app) ioToolOptions() []iotools.Option {
|
||||
@@ -64,12 +85,41 @@ func (a *app) ioToolOptions() []iotools.Option {
|
||||
iotools.WithKnowledgeRetriever(a.retriever),
|
||||
iotools.WithLogger(a.log),
|
||||
}
|
||||
return append(opts, a.webSearchOptions()...)
|
||||
return append(opts, a.webSearchOptions(webSearchCauseStartup)...)
|
||||
}
|
||||
|
||||
// webSearchOptions 把生效配置翻译成 io 选项。
|
||||
// buildWebSearchTool 按**当前生效配置**构造这一把 web-search 工具(面板保存后的热重载用它
|
||||
// 重建工具,见 websearch_reload.go)。
|
||||
//
|
||||
// 三种返回值,调用方必须分开处置:
|
||||
// - (tool, nil) ⇒ 正常,工具该在注册面里;
|
||||
// - (nil, nil) ⇒ 当前配置下它**不该在注册面里**(端点/主 provider 显式关闭,或选定
|
||||
// provider 没配齐被 WithoutSearch 摘掉)—— 调用方据此把旧条目摘掉;
|
||||
// - (nil, err) ⇒ 这份配置会让后端**下次启动直接失败**(主 provider 未登记/未配置,
|
||||
// design/27 §3 的 fail-fast)—— 调用方必须如实报错,绝不留下一个起不来的文件。
|
||||
//
|
||||
// 为什么要构造整份 io 工具集再挑出 web-search:io 包的公开构造入口只有
|
||||
// `iotools.New(cfg, opts...)`(整份集合),没有「只造 web-search」的入口,而
|
||||
// `plugins/tools/io` 不在本片文件面内。代价是一次 New(几个工具对象 + 一个 http.Client
|
||||
// 壳;Transport 是共享的 http.DefaultTransport,不新开连接池)—— 面板保存是低频动作,
|
||||
// 付得起,换来的是**重载后的工具与启动期构造的逐字段同源**(不是第二份接线逻辑)。
|
||||
func (a *app) buildWebSearchTool(cause string) (sdk.Tool, error) {
|
||||
set, err := iotools.New(a.cfg, a.webSearchOptions(cause)...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, t := range set {
|
||||
if t.Name() == webSearchToolName {
|
||||
return t, nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// webSearchOptions 把生效配置翻译成 io 选项。cause 说明**为什么**解析这一次(启动装配 /
|
||||
// 面板保存后的热重载),只进日志:同一个函数被两处调用,日志里必须能分清是哪一次。
|
||||
// 端点/后端「关闭 / 非法 / 可用」三种去处都在这里定死(ResolveProvider/ResolveEndpoint)。
|
||||
func (a *app) webSearchOptions() []iotools.Option {
|
||||
func (a *app) webSearchOptions(cause string) []iotools.Option {
|
||||
log := logger.WithSource(a.log, ioLogSource)
|
||||
ws, source := a.effectiveWebSearch(log)
|
||||
|
||||
@@ -77,7 +127,8 @@ func (a *app) webSearchOptions() []iotools.Option {
|
||||
if decision := ws.ResolveProvider(); decision.Disabled {
|
||||
// 配置值**绝不写进日志**(只记键名与原因)。
|
||||
log.Warn("web-search tool not registered",
|
||||
"env", webSearchProviderEnv, "reason", config.WebSearchReasonOff, "source", source,
|
||||
"selectorKey", webSearchProviderEnv, "reason", config.WebSearchReasonOff, "configSource", source,
|
||||
"cause", cause,
|
||||
"effect", "the model will not see a web-search tool at all")
|
||||
return []iotools.Option{iotools.WithoutSearch()}
|
||||
}
|
||||
@@ -89,14 +140,17 @@ func (a *app) webSearchOptions() []iotools.Option {
|
||||
// 错误,因此不 fatal,只留一条带键名的 WARN。
|
||||
if endpoint := ws.ResolveEndpoint(); !endpoint.Enabled && selectsSearXNG(ws) {
|
||||
log.Warn("web-search tool not registered",
|
||||
"env", webSearchEndpointEnv, "reason", endpoint.Reason, "source", source,
|
||||
"endpointKey", webSearchEndpointEnv, "reason", endpoint.Reason, "configSource", source,
|
||||
"cause", cause,
|
||||
"effect", "the model will not see a web-search tool at all")
|
||||
return []iotools.Option{iotools.WithoutSearch()}
|
||||
}
|
||||
|
||||
log.Info("web-search providers configured",
|
||||
"env", webSearchProviderEnv, "primary", strings.ToLower(strings.TrimSpace(ws.Provider)),
|
||||
"env_fallback", webSearchFallbackEnv, "fallback", ws.Fallback, "source", source,
|
||||
// primary 是**生效值**(可能来自面板文件、也可能来自 `.env` 播种)⇒ 它旁边的键名只是
|
||||
// 「这个键在哪份配置里」,而 source 才是「本次的值从哪来」。两者都打出来,不混为一谈。
|
||||
log.Info("web-search providers configured", "cause", cause,
|
||||
"selectorKey", webSearchProviderEnv, "primary", strings.ToLower(strings.TrimSpace(ws.Provider)),
|
||||
"fallbackKey", webSearchFallbackEnv, "fallback", ws.Fallback, "configSource", source,
|
||||
"available", availableProviderNames(specs))
|
||||
|
||||
opts := []iotools.Option{
|
||||
@@ -111,7 +165,7 @@ func (a *app) webSearchOptions() []iotools.Option {
|
||||
opts = append(opts, iotools.WithSearchTimeout(ws.Timeout))
|
||||
case ws.Timeout < 0:
|
||||
log.Warn("web-search timeout is negative, treated as unset",
|
||||
"env", webSearchTimeoutEnv, "source", source,
|
||||
"key", webSearchTimeoutEnv, "configSource", source, "cause", cause,
|
||||
"effect", "io package default timeout applies")
|
||||
}
|
||||
return opts
|
||||
|
||||
Reference in new issue
Block a user