feat(config): 运行时配置体系②——渠道配置落 channels.json + 面板落盘 + 热更新
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

- runtimeconfig/channels.go: channels.json 承载 map[platform][]ChannelAccount(token/baseURL/黑白名单/群聊限流), LoadChannels 优先文件/回退env, SaveChannels(0600+tmp原子写), Upsert/Delete/Masked脱敏
- channelbridge/reload.go: adapterManager 热替换——工厂重建新adapter先连接, 再原子断旧adapter(独立readCtx/进行中不中断); Bridge.Reload(platform) 逐账号增删
- 面板 PUT/DELETE /admin/channels/{platform}/config(owner+审计) 落盘channels.json+Reload热更新; GET /admin/channels 附账号掩码不泄裸token
- 前端 Channels.tsx 配置表单(id/token/baseURL/黑白名单/限流+config JSON)+保存热更新提示+掩码展示/删除
- .gitignore 加 channels.json; .env 退兜底(.env.example注明); token不打印/Config()脱敏
- channelbridge.go 拆 logic→reload.go(742→731); 全仓 go build/vet/test绿+前端build绿
- 未做真token端到端(需渠道凭据,不读.env)——如实标注待凭据
This commit is contained in:
Aska committed 2026-09-07 13:29:41 +08:00
1 parent 2d7151f979
commit aedae2190b
18 files changed
+1743 -126

No files matched your search

+36 -1
View File
@@ -100,6 +100,7 @@ type app struct {
history *history.Store
gw *gateway.Server
channelBridge *channelbridge.Bridge
channels gateway.ChannelConfigStore
}
// newApp assembles the full Cyrene runtime from cfg and log. It opens the
@@ -473,7 +474,13 @@ func (a *app) wireAuxiliary() error {
// config would either fail or, for weixin-ilink, trigger a blocking scan-code
// login.
func (a *app) wireChannels() error {
adapters := buildChannelAdapters(a.log)
// The channel config is runtime-editable: channels.json (when present and
// valid) is authoritative, otherwise the env channel block is the fallback
// (design/04 §3.1). The factory and the store read the same file so a panel
// save + hot-reload keeps boot and runtime consistent.
channelsPath := runtimeconfig.DefaultChannelsPath(resolveDataDir(a.cfg))
factory := &channelFactory{path: channelsPath, fallback: channelAccountsFromEnv(), log: a.log}
adapters := buildChannelAdapters(factory)
for _, ch := range adapters {
capp := registry.Capability{
Name: ch.Name(),
@@ -497,6 +504,9 @@ func (a *app) wireChannels() error {
// Optional review model prunes low-value queued messages near the per-adapter
// cap (design/12 §6 策略四). Nil adapter → the gate falls back to FIFO evict.
Review: reviewFor(a.modelAdapter, logger.WithSource(a.log, "services/channelbridge/review")),
// Factory lets the panel save hot-replace a platform's adapters without a
// restart (design/04 §3.1).
Factory: factory,
})
if berr != nil {
return fmt.Errorf("channel bridge: %w", berr)
@@ -506,10 +516,34 @@ func (a *app) wireChannels() error {
}
bridge.Start()
a.channelBridge = bridge
a.channels = &runtimeChannels{path: channelsPath, fallback: factory.fallback, bridge: bridge, log: a.log}
a.log.Info("channel adapters registered", "count", len(adapters))
return nil
}
// runtimeChannels is the gateway-facing view of the runtime-editable channel
// config. Load re-reads channels.json (file wins → env fallback) and Reload
// delegates to the bridge's hot-replace, so the panel's save/reload path drives
// the same source the bridge boots from.
type runtimeChannels struct {
path string
fallback map[string][]config.ChannelAccount
bridge *channelbridge.Bridge
log *slog.Logger
}
func (r *runtimeChannels) Path() string { return r.path }
func (r *runtimeChannels) Load() map[string][]config.ChannelAccount {
accounts, warn := runtimeconfig.LoadChannels(r.path, r.fallback)
if warn != nil {
r.log.Warn("channels.json load fell back to env", "error", warn)
}
return accounts
}
func (r *runtimeChannels) Reload(platform string) error { return r.bridge.Reload(platform) }
// wireFrontend builds the gateway with the embedded/external SPA filesystem and
// the JWT/RBAC authenticator. Session/Persona/Memories are wired to the concrete
// managers so the admin/user list endpoints return real data.
@@ -549,6 +583,7 @@ func (a *app) wireFrontend() error {
Backthink: a.backthink,
RAG: ragStore,
LLM: a.llm,
Channels: a.channels,
AuditStore: auditStore,
Streaming: true,
Frontend: resolveFrontendFS(a.log),