internal/config/sections.go — 注册回调internal/agent/client.go — 支持替换默认 Optionsinternal/container/container.go — 启动时注入钩子只展示代码,不修改文件。以下是方案 A(刷新钩子)的完整写法。
internal/config/sections.go — 注册回调// onSectionApplied 是配置段成功落盘并写入内存配置后的回调(可为 nil)。
// 由调用方(通常是 container 层)在启动时注入,用于把配置变更同步到
// 那些在启动时就已生成快照的运行时组件(例如 agent 的模型提供商表)。
//
// 放在 config 包内而不是直接 import agent 包,是为了避免 config ← agent 的循环依赖。
var onSectionApplied func(key string)
// SetSectionAppliedHook 注册「配置段已生效」回调;传 nil 表示取消。
// 必须在服务开始处理请求之前调用。
func SetSectionAppliedHook(fn func(key string)) {
onSectionApplied = fn
}
// notifySectionApplied 通知回调;回调内的 panic 不应影响保存结果。
func notifySectionApplied(key string) {
if onSectionApplied == nil {
return
}
defer func() {
if r := recover(); r != nil {
logger.Errorf(context.Background(), "[Config] section applied hook panic for %q: %v", key, r)
}
}()
onSectionApplied(key)
}
在 Section.Apply 调用处(internal/handler/config.go:126)触发:
if err := section.Apply(h.cfg, value); err != nil {
c.Error(errors.NewInternalServerError("failed to apply config section").WithDetails(err.Error()))
return
}
// 配置已落盘并写入内存,通知运行时组件刷新快照(未注册回调时为空操作)。
config.NotifySectionApplied(section.Key)
(用导出名 NotifySectionApplied,因为 handler 在别的包。也可以把触发点收进 config 包的一个 ApplySection(cfg, key, value) 包装函数里,保持 handler 无感知。)
internal/agent/client.go — 支持替换默认 Options// SetProviders 更新默认 Options 中的模型提供商配置表快照(热更新,无需重启)。
func (c *Client) SetProviders(providers map[string]ModelProviderConfig) {
c.mu.Lock()
c.defaultOptions.Providers = providers
c.mu.Unlock()
}
// SetOptions 整体替换默认 Options 中由配置派生的部分(目前即 Providers)。
// 供 container 层在配置变更后调用;其余进程级服务能力(Tools / FileReader 等)不受影响。
func (c *Client) SetProvidersFrom(fn func() map[string]ModelProviderConfig) {
c.SetProviders(fn())
}
internal/container/container.go — 启动时注入钩子// registerConfigHooks 把「配置段已生效」回调注入 config 包。
//
// config 包不认识 agent.Client,这里通过闭包把两者连起来:
// agent 段保存后,client 立刻重建 providers 快照,下一次请求即使用新配置。
func registerConfigHooks(cfg *config.Config, client *agent.Client) {
config.SetSectionAppliedHook(func(key string) {
switch key {
case "agent":
client.SetProviders(agentProviders(cfg))
}
})
}
在 DI 容器里、*agent.Client 构造完成之后调用(container.go 的 Provide 区块附近):
must(container.Invoke(func(cfg *config.Config, client *agent.Client) {
registerConfigHooks(cfg, client)
}))
| 方案 A(钩子推送) | 方案 B(当前已实现,拉取) | |
|---|---|---|
| 生效时机 | Apply 之后立即刷新快照 | 下一次请求时现取 |
| config 包依赖 | 需要新增导出函数 SetSectionAppliedHook | 无改动 |
| agent 包改动 | 需要 SetProviders,options() 不变 | Options.ProviderSource + options() 改动 |
| 遗漏风险 | 以后新增其它"启动快照型"组件,要记得在钩子里加分支 | 只要组件拿到的是 func() 就自动最新 |
| 并发 | 写 defaultOptions 需持写锁;正在执行的请求仍用旧快照 | 无写操作,只读当前 map |
你已经实现了 B,A 属于"推送式"替代方案,两者也可以共存(ProviderSource 优先时会覆盖 SetProviders 的结果,见 client.go:options() 的写法)。