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:
AskaEth committed 2026-10-06 15:59:12 +08:00
1 parent 5ee5f9126f
commit a30f4a49ff
12 files changed
+1696 -16

No files matched your search

+38
View File
@@ -237,6 +237,44 @@ func (r *Registry) RegisterTool(t sdk.Tool) error {
})
}
// ReplaceTool 用**同名**的 t 就地换掉已登记的工具条目(热替换,design/09 §5.5:
// 面板保存后下一轮即用新接线,不必重启)。
//
// 与「Remove + Register」的两处实质差别,正是这个窄方法存在的理由:
// - **原子**:替换在一次写锁内完成,读者中间看不到「这个名字不存在」的空窗
// (Remove+Register 之间若有 Invoke 落到这个名字上,会得到 ErrUnknownTool);
// - **保留 Policy**:分组/陌生人的可达标记是**运行期可改的配置**(§11 §3),
// 不属于工具实现。初始注册时带的标记没有进 persisted 快照,Remove+Register
// 会把它悄悄重置成零值(= 退回 owner-only),用户会看到「面板保存后某个群
// 忽然不能用了」。这里原样沿用内存里的既有标记。
//
// 没有同名条目时返回 ErrNotFound(**不**顺手新增:热替换的语义是「换掉现在这把」,
// 名字打错时静默插入会掩盖调用方的错);既有条目不是 KindTool 时返回 ErrInvalidArgument。
func (r *Registry) ReplaceTool(t sdk.Tool) error {
if t == nil || t.Name() == "" {
return fmt.Errorf("registry: replace tool: %w", ErrInvalidArgument)
}
name := t.Name()
r.mu.Lock()
defer r.mu.Unlock()
cur, ok := r.caps[name]
if !ok {
return fmt.Errorf("registry: replace %q: %w", name, ErrNotFound)
}
if cur.Kind != KindTool {
return fmt.Errorf("registry: replace %q: 既有能力类型是 %q,不是工具: %w", name, cur.Kind, ErrInvalidArgument)
}
r.caps[name] = Capability{
Name: name,
Kind: KindTool,
Description: t.Description(),
// Policy 沿用旧条目的:它是可编辑的运行期配置,不属于工具的构造产出。
Policy: cur.Policy,
Value: t,
}
return nil
}
// RegisterSkill registers s as a KindSkill capability.
func (r *Registry) RegisterSkill(s sdk.Skill) error {
if s == nil {