Files
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

174 lines
4.6 KiB
Go
Raw Permalink 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
import (
"context"
"net/http"
"git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter"
)
// 本文件承载渠道桥接器的适配器注册表与连接生命周期
// (design/08 §0):适配器经 Add 注册、由 Start 拨号
// (各自受 connectTimeout 约束)、由 Stop 拆除,并在不带
// 可用配置时被凭据闸门跳过。
// Add 按适配器的 Name 注册它。在 Start 之前调用是安全的;
// Start 之后再添加不影响已在运行的适配器。
func (b *Bridge) Add(a adapter.PlatformAdapter) {
if a == nil {
return
}
b.mu.Lock()
defer b.mu.Unlock()
b.adapters[a.Name()] = a
}
// WebhookHandlers 返回每个暴露 HTTP 回调的已注册适配器
// (如 *webhook.Adapter)的入站 HTTP 处理器,键为适配器配置中的
// 端点路径。gateway 挂载它们,把外部 webhook 回调
// 接入运行时。当没有任何适配器提供入站处理器时
// 返回 nil map。
func (b *Bridge) WebhookHandlers() map[string]http.Handler {
b.mu.RLock()
defer b.mu.RUnlock()
out := make(map[string]http.Handler)
for _, a := range b.adapters {
wh, ok := a.(webhookAdapter)
if !ok {
continue
}
path := wh.WebhookPath()
if path == "" {
// 适配器当前未暴露 webhook(例如 websocket 模式的
// KOOK);跳过它可避免把处理器挂在过期或冲突的路由上。
continue
}
h := wh.InboundHandler()
if h == nil {
continue
}
out[path] = h
}
if len(out) == 0 {
return nil
}
return out
}
// Start 连接每个已配置的适配器、装配入站处理器并
// 启动读取循环。没有凭据(或 Connect 失败)的适配器
// 只记日志并保持未连接;它们绝不会导致启动失败。Start 只
// 阻塞到连接完成(每个适配器受 connectTimeout 约束)便返回。它
// 是幂等的:第二次调用无副作用。
func (b *Bridge) Start() {
b.mu.Lock()
if b.closed || b.started {
b.mu.Unlock()
return
}
b.ctx, b.cancel = context.WithCancel(context.Background())
b.started = true
b.mu.Unlock()
for _, a := range b.snapshot() {
b.connectAndRun(a)
}
b.log.Info("channel bridge started", "connected", b.ConnectedCount(),
"registered", len(b.snapshot()))
}
// Stop 取消读取循环、断开所有适配器,并等待
// 处理中的消息完成。它是幂等的。
func (b *Bridge) Stop(ctx context.Context) {
b.mu.Lock()
if b.closed {
b.mu.Unlock()
return
}
b.closed = true
cancel := b.cancel
b.mu.Unlock()
cancel()
for name, a := range b.snapshot() {
if err := a.Disconnect(ctx); err != nil {
b.log.Warn("channel disconnect", "channel", name, "error", err)
}
}
b.wg.Wait()
b.log.Info("channel bridge stopped")
}
// IsConnected 报告指定名称的适配器当前是否已连接。
func (b *Bridge) IsConnected(name string) bool {
b.mu.RLock()
defer b.mu.RUnlock()
return b.connected[name]
}
// ConnectedCount 报告当前已连接的适配器数量。
func (b *Bridge) ConnectedCount() int {
b.mu.RLock()
defer b.mu.RUnlock()
n := 0
for _, ok := range b.connected {
if ok {
n++
}
}
return n
}
// snapshot 返回已注册适配器的副本,以便无锁遍历。
func (b *Bridge) snapshot() []adapter.PlatformAdapter {
b.mu.RLock()
defer b.mu.RUnlock()
out := make([]adapter.PlatformAdapter, 0, len(b.adapters))
for _, a := range b.adapters {
out = append(out, a)
}
return out
}
// configured 报告适配器是否带有足够配置可供尝试
// Connect。它检查适配器自身的 Config() 快照来看各平台所需的
// 凭据,因此空壳(无配置)适配器会被跳过而不是拨号。
// 正是这一点避免了 weixin-ilink 在缺少 bot token 时
// 落入会阻塞的扫码登录流程。
func (b *Bridge) configured(a adapter.PlatformAdapter) bool {
cfg := a.Config()
switch basePlatform(a.Name()) {
case "telegram", "weixin-ilink", "kook":
return cfgStr(cfg, "bot_token") != ""
case "qq-obv11":
if cfgStr(cfg, "mode") == "client" {
return cfgStr(cfg, "remoteURL") != ""
}
return cfgStr(cfg, "listenAddr") != ""
case "webhook":
// 仅入站:维持会话不需要外部凭据。
return true
default:
return true
}
}
// cfgStr 从适配器配置 map 中读取字符串字段,缺失时
// 返回 ""。
func cfgStr(m map[string]any, key string) string {
if m == nil {
return ""
}
if s, ok := m[key].(string); ok {
return s
}
return ""
}
// adapter 返回平台名对应的已注册适配器,没有则返回 nil。
func (b *Bridge) adapter(platform string) adapter.PlatformAdapter {
b.mu.RLock()
defer b.mu.RUnlock()
return b.adapters[platform]
}