- 只翻注释/文档字符串:剥掉注释后代码行逐字节不变(机械校验只剩允许的行尾注释形态) - 术语对齐既有译法(owner→主人、persona→角色、engage→参与度判定、quiet hours→安静时段、backoff→退避) - 复核:go build 绿 + channelbridge 包测试绿;gofmt 干净
179 lines
6.3 KiB
Go
179 lines
6.3 KiB
Go
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)
|
||
}
|