fix(channel): 热重载把新适配器写回能力注册表
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

面板保存渠道配置后 Bridge.Reload 只重建了桥接器自己的适配器表,能力注册表里的条目仍指向保存前的旧实例;而管理面的渠道列表、每账号连接状态、open/close 的目标解析全部读注册表 —— 于是卡片永远「未连接」,点「连接」驱动的是旧实例(按旧 URL 拨号)。

生产日志实证:保存后 bridge 侧新实例连上 ws://localhost:3001 且心跳正常,而点连接时被驱动的适配器拨的是旧地址 ws://localhost:10301。

改法:注册表新增 upsert 语义的 ReplaceChannel(覆盖 Value、保留运行期 Policy;Register 遇重名会 ErrDuplicate,热重载必然同名);桥接器经窄端口在适配器上线后同步,账号被移除时删除条目;组合根注入 a.caps 包装。同步失败只记日志、不中断 Reload;未接线时保持旧行为并只 WARN 一次。

验收:go build ./... + go vet 全绿;registry/channelbridge/gateway 三包 test 全绿;主会话亲自变异(摘掉 Policy 沿用 ⇒ TestReplaceChannelSwapsValueAndKeepsPolicy 红)。linux/darwin 交叉编译通过。
This commit is contained in:
AskaEth committed 2026-10-13 19:21:49 +08:00
1 parent 23b6e3fe1e
commit d81635672e
6 files changed
+560 -6

No files matched your search

+70 -6
View File
@@ -14,6 +14,7 @@ import (
"fmt"
"git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter"
"git.yeij.top/AskaEth/Cyrene_v2/pkg/plugins/sdk"
)
// PlatformFactory 依据平台当前生效的账号配置
@@ -27,6 +28,20 @@ type PlatformFactory interface {
Build(platform string) ([]adapter.PlatformAdapter, error)
}
// ChannelRegistry 是能力注册表的**窄端口**,只暴露热重载需要同步的两件事:
// 把某个名字的渠道条目换成新适配器、把已消失的账号条目删掉。桥接器用它让管理面板
// (渠道列表 / 在线状态 / open·close 解析)读到的永远是**当前**适配器实例,而不必
// 依赖 capability/registry 的具体类型 —— 注册表实现与桥接器因此保持解耦。
//
// 未接线(Options.Channels 为 nil)时桥接器照常热重载,但注册表条目会停在被保存前
// 的那个实例上:面板显示「未连接」、点「连接」驱动的是旧实例。此时只记一次 WARN。
type ChannelRegistry interface {
// ReplaceChannel 用同名的新适配器覆盖(或新增)渠道条目。
ReplaceChannel(ch sdk.Channel) error
// RemoveChannel 删除该名字的渠道条目。
RemoveChannel(name string) error
}
// ErrNilFactory 表示请求重载时未注入平台工厂
// (没有热重载配置来源)。
var ErrNilFactory = errors.New("channelbridge: nil platform factory")
@@ -112,16 +127,19 @@ func (b *Bridge) Reload(platform string) error {
}
for _, a := range newAdapters {
b.connectAndRun(a)
// 紧跟上线之后同步能力注册表:新实例先连上,面板才改为读它,
// 避免「注册表已指向新实例、它却还没连」的窗口。
b.replaceChannelEntry(a)
}
for _, o := range old {
if newNames[o.Name()] {
// 同名账号被重建后的适配器替换:停止旧的读取
// 循环,但保留注册表条目——它现在指向新适配器。
// 同名账号被重建后的适配器替换:只停旧实例的读取循环,
// 注册表条目已由 replaceChannelEntry 指向新实例。
b.cancelAndDisconnect(o, oldCancels[o.Name()])
continue
}
// 账号被彻底移除(或更名):停止它并删除其注册表
// 条目,后续 snapshot 便不再看到它。
// 条目,后续 snapshot 与面板便不再看到它。
b.removeAdapter(o, oldCancels[o.Name()])
}
b.log.Info("channel reloaded", "platform", platform, "adapters", len(newAdapters))
@@ -148,10 +166,55 @@ func (b *Bridge) adapterSnap(platform string) ([]adapter.PlatformAdapter, map[st
return list, cancels
}
// replaceChannelEntry 把刚上线的适配器写回能力注册表。注册表里的渠道条目只在启动
// 时写过一次,而热重载重建出来的是同名的新实例:不同步的话,管理面板(渠道列表、
// 账号在线状态、open/close 的目标解析)读到的仍是保存前的**旧实例** —— 卡片显示
// 「未连接」,点「连接」拨的是旧配置里的地址(design/04 §3.1)。
//
// 同步失败只记日志、绝不返回错误:面板保存不该被注册表同步拖垮。
func (b *Bridge) replaceChannelEntry(a adapter.PlatformAdapter) {
if a == nil {
return
}
name := a.Name()
if !b.IsConnected(name) {
// 没上线就不动注册表:换成连不上的实例只会让面板的在线状态更失真。
b.log.Warn("channel registry left unchanged: adapter not connected", "channel", name)
return
}
if b.channels == nil {
b.warnChannelRegistryUnwired()
return
}
if err := b.channels.ReplaceChannel(a); err != nil {
b.log.Warn("channel registry replace failed", "channel", name, "error", err)
}
}
// dropChannelEntry 在账号被移除(或更名)后删掉它的注册表条目,使管理面板不再列出
// 这条已经不存在的账号。同 replaceChannelEntry:失败只记日志。
func (b *Bridge) dropChannelEntry(name string) {
if b.channels == nil {
b.warnChannelRegistryUnwired()
return
}
if err := b.channels.RemoveChannel(name); err != nil {
b.log.Warn("channel registry remove failed", "channel", name, "error", err)
}
}
// warnChannelRegistryUnwired 只在首次同步时提醒一次:未注入 Channels 端口时热重载
// 照常进行(适配器确实换了),只是面板读到的是保存前的旧实例。
func (b *Bridge) warnChannelRegistryUnwired() {
b.channelsWarnOnce.Do(func() {
b.log.Warn("channel registry not wired: admin panel keeps the pre-reload adapter instance")
})
}
// cancelAndDisconnect 停止适配器的读取循环并拆除其会话。
// 它绝不改动注册表 map:替换同名适配器的调用方
// 希望(新)条目保留,而移除账号的调用方使用
// removeAdapter。
// 它绝不改动桥接器自己的适配器表与能力注册表:替换同名适配器
// 的调用方希望(新)条目保留,而移除账号的调用方使用
// removeAdapter(它会连注册表条目一起清掉)。
func (b *Bridge) cancelAndDisconnect(o adapter.PlatformAdapter, c context.CancelFunc) {
if c != nil {
c()
@@ -174,5 +237,6 @@ func (b *Bridge) removeAdapter(o adapter.PlatformAdapter, c context.CancelFunc)
delete(b.readCancels, name)
b.mu.Unlock()
b.forgetAdapterPersona(name)
b.dropChannelEntry(name)
b.log.Info("channel (reload) removed", "channel", name)
}