Files
Cyrene_v2/internal/services/channelbridge/reload.go
T
AskaEth 093f38d986
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
docs(comments): 注释中文化 · internal/services/channelbridge(纯替换)
- 只翻注释/文档字符串:剥掉注释后代码行逐字节不变(机械校验只剩允许的行尾注释形态)
- 术语对齐既有译法(owner→主人、persona→角色、engage→参与度判定、quiet hours→安静时段、backoff→退避)
- 复核:go build 绿 + channelbridge 包测试绿;gofmt 干净
2026-10-04 13:33:06 +08:00

179 lines
6.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package channelbridge
// 本文件承载渠道桥接器的热重载机制(design/04
// §3.1)。面板把某平台的账号配置保存到 channels.json 并调用
// Bridge.Reload(platform):工厂重建该平台的适配器,它们
// 先连接(新消息使用新配置),随后旧适配器
// 被优雅停止。已在途的消息仍在其旧适配器上完成,
// 因为消息处理是以桥接器生命周期 context 派生的,而非
// 重载所取消的按适配器读取 context。
import (
"context"
"errors"
"fmt"
"git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter"
)
// PlatformFactory 依据平台当前生效的账号配置
// (channels.json,回退到环境变量)重建其适配器。它以注入方式提供,
// 使桥接器能在面板保存新配置时热替换该平台的适配器
// 而无需重启。具体实现位于组合根
// (cmd/cyrene),平台适配器的构造函数由那里持有。
type PlatformFactory interface {
// Build 依据生效的账号配置返回该平台的适配器。
// 平台没有已配置账号时,返回的切片可能为空。
Build(platform string) ([]adapter.PlatformAdapter, error)
}
// ErrNilFactory 表示请求重载时未注入平台工厂
// (没有热重载配置来源)。
var ErrNilFactory = errors.New("channelbridge: nil platform factory")
// ErrBridgeClosed 表示在已停止的桥接器上请求重载。
var ErrBridgeClosed = errors.New("channelbridge: bridge is not running")
// connectAndRun 拨号单个适配器(受 connectTimeout 约束),成功时
// 装配其入站处理器,并在按适配器 context 下启动其读取
// 循环。热重载正是取消这个按适配器 context 来停止旧
// 读取循环;消息处理则以桥接器生命周期 context 派生,
// 因此 Reload 之前已在途的消息会在其旧适配器上完成。
func (b *Bridge) connectAndRun(a adapter.PlatformAdapter) {
if a == nil {
return
}
if !b.configured(a) {
b.log.Warn("channel skipped: no credentials", "channel", a.Name())
return
}
connCtx, cancel := context.WithTimeout(context.Background(), connectTimeout)
err := a.Connect(connCtx)
cancel()
if err != nil {
b.log.Warn("channel connect failed, leaving disconnected",
"channel", a.Name(), "error", err)
return
}
// 闭包捕获该适配器,使入站回调知道是哪个账号收到了
// 消息——多账号回复路由,以及读取该账号的
// 允许/拒绝名单都依赖它(design/16 §4.6 §5)。
a.SetMessageHandler(func(_ context.Context, msg adapter.UnifiedMessage) {
b.onMessage(b.ctx, msg, a)
})
b.mu.Lock()
if b.closed {
b.mu.Unlock()
ctx, c := context.WithTimeout(context.Background(), connectTimeout)
_ = a.Disconnect(ctx)
c()
return
}
readCtx, rcancel := context.WithCancel(b.ctx)
b.readCancels[a.Name()] = rcancel
b.connected[a.Name()] = true
b.mu.Unlock()
// 适配器已上线,此时缓存该账号的渠道级角色:
// 入站路径读取缓存而不是配置,而重载(由面板保存
// 触发)会重跑这里以拾取新的绑定。
b.rememberAdapterPersona(a.Name())
b.wg.Add(1)
go b.readLoop(a, readCtx)
b.log.Info("channel connected", "channel", a.Name())
}
// Reload 依据平台当前生效的配置(经由注入的工厂)重建其适配器,
// 并在不重启的前提下热替换正在运行的适配器
// (design/04 §3.1)。重建后的适配器先连接;到达旧适配器的
// 消息在旧适配器上完成,新消息则由新配置
// 接管。被移除的账号会停掉其旧适配器;名称未变的
// 账号在新适配器上线后停掉旧适配器。
func (b *Bridge) Reload(platform string) error {
if b.factory == nil {
return ErrNilFactory
}
b.mu.Lock()
started := b.started
closed := b.closed
b.mu.Unlock()
if !started || closed {
return ErrBridgeClosed
}
newAdapters, err := b.factory.Build(platform)
if err != nil {
return fmt.Errorf("channelbridge: build %s: %w", platform, err)
}
old, oldCancels := b.adapterSnap(platform)
newNames := make(map[string]bool, len(newAdapters))
for _, a := range newAdapters {
newNames[a.Name()] = true
}
for _, a := range newAdapters {
b.connectAndRun(a)
}
for _, o := range old {
if newNames[o.Name()] {
// 同名账号被重建后的适配器替换:停止旧的读取
// 循环,但保留注册表条目——它现在指向新适配器。
b.cancelAndDisconnect(o, oldCancels[o.Name()])
continue
}
// 账号被彻底移除(或更名):停止它并删除其注册表
// 条目,后续 snapshot 便不再看到它。
b.removeAdapter(o, oldCancels[o.Name()])
}
b.log.Info("channel reloaded", "platform", platform, "adapters", len(newAdapters))
return nil
}
// adapterSnap 返回某平台当前已注册的适配器,以及
// 控制各自读取循环的 context 取消函数。Reload 用它
// 在重建的适配器上线后停止旧适配器。
func (b *Bridge) adapterSnap(platform string) ([]adapter.PlatformAdapter, map[string]context.CancelFunc) {
b.mu.RLock()
defer b.mu.RUnlock()
var list []adapter.PlatformAdapter
cancels := make(map[string]context.CancelFunc)
for name, a := range b.adapters {
if basePlatform(name) != platform {
continue
}
list = append(list, a)
if c, ok := b.readCancels[name]; ok {
cancels[name] = c
}
}
return list, cancels
}
// cancelAndDisconnect 停止适配器的读取循环并拆除其会话。
// 它绝不改动注册表 map:替换同名适配器的调用方
// 希望(新)条目保留,而移除账号的调用方使用
// removeAdapter。
func (b *Bridge) cancelAndDisconnect(o adapter.PlatformAdapter, c context.CancelFunc) {
if c != nil {
c()
}
ctx, cancel := context.WithTimeout(context.Background(), connectTimeout)
_ = o.Disconnect(ctx)
cancel()
b.log.Info("channel (reload) stopped", "channel", o.Name())
}
// removeAdapter 停止账号的适配器并删除其注册表条目。它
// 只对未被 Reload 重建的名称调用,因此删除绝不会
// 误删如今占用该名称的替换适配器。
func (b *Bridge) removeAdapter(o adapter.PlatformAdapter, c context.CancelFunc) {
b.cancelAndDisconnect(o, c)
name := o.Name()
b.mu.Lock()
delete(b.adapters, name)
delete(b.connected, name)
delete(b.readCancels, name)
b.mu.Unlock()
b.forgetAdapterPersona(name)
b.log.Info("channel (reload) removed", "channel", name)
}