跳转至

多平台适配器体系——平台无关的事件驱动抽象

ZeroBot 基因:这是 Remilia 与 ZeroBot 最大的结构性差异。ZeroBot 是 QQ/OneBot 单平台框架,Event 字段直接对应 OneBot JSON。多平台抽象是 Remilia 从"借鉴"走向"超越"的最关键一步。参阅 11-zerobot-inspiration.md

设计动机

机器人框架天然需要支持多平台:QQ、Discord、Telegram、微信等。每个平台的 API 风格、通信协议、消息格式各不相同。框架的目标是让业务逻辑完全与平台解耦——同一个 Handler 可以在任意平台上运行。

核心抽象:Adapter 接口

type Adapter interface {
    Platform() string                                          // 平台标识
    Start(ctx context.Context, handler func(Event)) error      // 启动事件循环
    Stop(ctx context.Context) error                            // 优雅停止
    Sender() Sender                                            // 消息发送器
    Capabilities() Capabilities                                // 平台能力声明
    IsRunning() bool                                           // 运行状态
}

每个平台(QQ、Discord、Telegram 等)实现此接口,框架核心通过此接口接收事件和发送消息——不依赖任何平台 SDK

事件抽象

type Event interface {
    Platform() string           // 来源平台
    Kind() EventKind            // 事件类别(平台无关)
    Raw() any                   // 原始平台事件
    // 平台无关字段
    GetMessage() Message        // 消息内容
    GetSender() UserInfo        // 发送者
    GetGroup() GroupInfo        // 群组信息(群消息时)
    GetChannel() ChannelInfo    // 频道信息
    // ...
}

平台无关的 EventKind 分类:

type EventKind string

const (
    EventKindUnknown          EventKind = ""
    EventKindPrivateMessage   EventKind = "private_message"
    EventKindGroupMessage     EventKind = "group_message"
    EventKindChannelMessage   EventKind = "channel_message"
    EventKindGuildMessage     EventKind = "guild_message"
    EventKindFriendAdd        EventKind = "friend_add"
    EventKindMemberJoin       EventKind = "member_join"
    EventKindMemberLeave      EventKind = "member_leave"
    EventKindReactionAdd      EventKind = "reaction_add"
    // ...
)

消息抽象

type Sender interface {
    SendMessage(ctx context.Context, target MessageTarget, msg Message) error
    EditMessage(ctx context.Context, target EditTarget, msg Message) error
    DeleteMessage(ctx context.Context, target DeleteTarget) error
}

能力声明

type Capabilities struct {
    Markdown         bool   // 支持 Markdown 格式
    Buttons          bool   // 支持按钮交互
    MultiAttachment  bool   // 支持多附件
    MessageEdit      bool   // 支持编辑消息
    MessageDelete    bool   // 支持删除消息
    Embeds           bool   // 支持 Embed 卡片
    MaxTextLength    int    // 最大文本长度
    MaxAttachmentMB  int    // 最大附件大小
    // ...
}

Handler 可以通过 ctx.GetPlatformCapabilities() 获取平台能力,实现渐进增强策略——例如同一份代码在 QQ 上使用按钮交互,在 Telegram 上退化为文字回复。

能力合并

当适配器通过 Bot 层被多个来源注入能力时,使用取 OR 并集 + 取最小限制的策略:

func mergePlatformCaps(caps []Capabilities) Capabilities {
    var m Capabilities
    for _, c := range caps {
        // 布尔能力取 OR(并集)
        m.Markdown = m.Markdown || c.Markdown
        m.Buttons = m.Buttons || c.Buttons
        // 量化限制取最小非零值(更保守)
        m.MaxTextLength = minNonZero(m.MaxTextLength, c.MaxTextLength)
        m.MaxAttachmentMB = minNonZero(m.MaxAttachmentMB, c.MaxAttachmentMB)
    }
    return m
}

已实现平台

QQ(platform/qq)

最完善的平台实现,支持两种接入模式:

Webhook 模式(推荐):

botInfo := &dto.BotInfo{
    AppID:     123456,
    Token:     "your-token",
    AppSecret: "your-secret",
}
adapter := qq.NewWebhookServerAdapter(":8080", botInfo)

自动管理: - Token 定期刷新(使用 golang.org/x/time/rate 限制刷新频率) - Webhook 签名验证(HMAC-SHA256) - 断连检测 + 自动重连

协议结构

platform/qq/
├── adapter.go         # Adapter 接口实现
├── webhook_server.go  # Webhook HTTP Server
├── webhook_conn.go    # WebSocket 连接管理
├── sender.go          # 消息发送封装
├── event.go           # QQ 事件 → platform.Event 转换
├── result.go          # API 响应处理
└── openapi/
    ├── openapi.go     # OpenAPI HTTP 客户端
    ├── dto/           # 数据模型
    ├── iface.go       # API 接口
    └── auth/token/    # Token 管理

Discord(platform/discord)

基于 bwmarrin/discordgo SDK 实现:

type Adapter struct {
    session *discordgo.Session  // DiscordGo SDK
    handler func(platform.Event)
}

func (a *Adapter) Start(ctx context.Context, handler func(Event)) error {
    a.handler = handler
    a.session.AddHandler(func(s *discordgo.Session, m *discordgo.MessageCreate) {
        event := a.convertMessage(m)
        handler(event)
    })
    return a.session.Open()
}

支持 Slash Commands 和交互式组件。

Telegram(platform/telegram)

基于 long-polling 模式,使用 Telegram Bot API。

OneBot(platform/onebot)

支持 WebSocket 反向连接和 HTTP POST 两种模式,兼容 OneBot v11 协议:

// WebSocket 反向连接(OneBot 主动连接 Bot)
adapter := onebot.NewReverseWSAdapter("ws://127.0.0.1:6700")

Satori(platform/satori)

支持 Satori 协议标准,提供 WebSocket 和 Webhook 两种接入方式:

adapter := satori.NewWebSocketAdapter("ws://127.0.0.1:5500")
adapter := satori.NewWebhookAdapter(":8550")

微信(platform/wechat)

微信官方 API 适配,支持公众号和企业微信消息。

Milky(platform/milky)

自定义协议适配器,使用 WebSocket 长连接:

type Client struct {
    conn     *websocket.Conn
    api      *API
    handler  func(platform.Event)
    botID    string
}

Registry:多平台统一管理

type Registry struct {
    adapters map[string]Adapter  // platform → Adapter
    mu       sync.RWMutex
}

func (r *Registry) Register(adapter Adapter) {
    r.adapters[adapter.Platform()] = adapter
}

func (r *Registry) All() []Adapter {
    // 返回所有注册的适配器
}

Bot 通过 Registry 支持同时接入多个平台:

registry := platform.NewRegistry()
registry.Register(qqAdapter)
registry.Register(discordAdapter)
registry.Register(telegramAdapter)

bot, err := remilia.NewBotBuilder().
    WithPlatformRegistry(registry).
    WithEngine(eng).
    Build()

适配器快照(热路径优化)

// 在 Bot.Start() 时构建快照
snapshot := make(map[string]adapterCache)
for _, pa := range reg.All() {
    snapshot[pa.Platform()] = adapterCache{
        adapter: pa,
        caps:    pa.Capabilities(),
    }
}
b.adapterSnapshot.Store(snapshot)

// 热路径:atomic.Value.Load,零锁开销
snapshot := b.adapterSnapshot.Load()
if c, ok := snapshot[event.Platform()]; ok {
    sender = c.adapter.Sender()
    caps = c.caps
}

扩展新平台

添加一个新平台只需: 1. 实现 platform.Adapter 接口(4 个方法 + Start/Stop) 2. 将平台事件转换为 platform.Event 3. 实现 Sender 发送消息

无需修改框架核心代码——所有平台差异化逻辑封装在适配器内部。

迭代过程

V0:Bot 与 QQ API 深度绑定(Monolithic)

初始版本根本没有"平台"概念。Bot 直接依赖 QQ 的 openapi/dtoopenapi/auth/token

// V0 代码 — bot.go(初始 commit)
type Bot struct {
    wh     webhook.WebHook       // QQ Webhook 协议
    tm     *token.Manager        // QQ Token 管理
    engine *Engine               // 事件引擎
    api    openapi.OpenAPI       // QQ OpenAPI HTTP 客户端
    srv    *http.Server
}

// 事件处理直接操作 dto.Payload
func (b *Bot) handleEvent(payload *dto.Payload) {
    ctx := NewContext(payload, b.api)
    b.engine.ProcessEvent(ctx)
}
// V0 Context — 强依赖 dto.Payload
type Context struct {
    event *dto.Payload       // 只能处理 QQ 事件
    api   openapi.OpenAPI    // 只能通过 QQ API 回复
    state State
}
// V0 Engine — 死信队列也依赖 dto.Payload
type DeadLetterItem struct {
    Event   *dto.Payload  // QQ 专用
    Err     error
    Attempt int
}

问题: - 所有代码都耦合 dto.Payload(QQ 的消息结构体) - 添加 Discord 支持?需要修改 Bot、Context、Engine、DeadLetter...几乎所有类型 - 测试需要 mock openapi.OpenAPI,依赖整个 QQ SDK - 插件的 plugin.Load(engine) 持有整个 Engine,可以对任意平台 API 做操作

V1:Context 双路径(过渡方案)

在平台抽象层正式引入前,采取了过渡策略——Context 支持双路径:

// V1 Context — 双路径(旧路径 + 新路径)
type Context struct {
    // 旧路径(QQ 专用)
    event *dto.Payload
    api   openapi.OpenAPI

    // 新路径(平台无关)
    platformEvent  platform.Event
    platformSender platform.Sender
    isPlatformPath bool  // 路由选择器
}
// V4(当前):每事件新分配,不复用
func NewContextFromEvent(event platform.Event, sender platform.Sender) *Context {
    return &Context{
        platformEvent:  event,
        platformSender: sender,
    }
}
// Handler 中的兼容处理(V3 之前有双路径 isPlatformPath 判断,V4 完全抛弃旧路径)
func (ctx *Context) Reply(msg interface{}) error {
    }
    return ctx.api.SendMessage(...)                   // 旧路径
}

好处:已有的 QQ Handler 代码不需要修改,新平台代码走新路径。 问题:双路径的 runtime 分支增加了心智负担;if isPlatformPath 散落在各处;错误路径的选择器 bug 难以调试。

V2:完整平台抽象(当前)

关键 commit 0709f98 完成迁移,一次性引入 platform.Eventplatform.Adapterplatform.Registry 三大抽象:

// V2 — 核心抽象
type Event interface {
    Platform() string       // "qq" / "discord" / "telegram"
    Kind() EventKind        // "private_message" / "group_message"
    Raw() any               // 原始事件(调试用)
    GetMessage() Message
    GetSender() UserInfo
    GetGroup() GroupInfo
    // ...
}

type Adapter interface {
    Platform() string
    Start(ctx context.Context, handler func(Event)) error
    Stop(ctx context.Context) error
    Sender() Sender
    Capabilities() Capabilities
    IsRunning() bool
}

type Registry struct {
    adapters map[string]Adapter
    mu       sync.RWMutex
}

Engine 的入口也随之改变

// V2 — 新增平台无关入口
func (e *Engine) ProcessPlatformEvent(event platform.Event, sender platform.Sender, caps ...platform.Capabilities) {
    ctx := context.NewContextFromEvent(event, sender)
    defer context.ReleaseContextFromEvent(ctx)
    if len(caps) > 0 {
        ctx.SetPlatformCapabilities(mergePlatformCaps(caps))
    }
    e.processEventContext(ctx)  // 共享核心逻辑
}

// 旧入口保留兼容,但内部也转换为新路径
func (e *Engine) ProcessEvent(ctx *context.Context) {
    // 现在只是 processEventContext 的包装
    e.processEventContext(ctx)
}

双路径清理isPlatformPath 字段在 V2 中被移除——Context 不再持有 dto.Payloadopenapi.OpenAPI,只保留 platform.Eventplatform.Sender。旧代码库中根包对 openapi/dto 的依赖被彻底移除(见 commit 93fd3a2)。

测试迁移——测试不再需要 QQ 类型的 mock:

// V2 测试 — 平台无关
// 之前(V0):
// mockAdapter.events <- &dto.Payload{Type: dto.C2CMessageCreate, ...}

// 之后(V2):
mockAdapter.events <- testutil.MakePlatformC2CEvent("hello")

V3 扩展:多平台适配器实现

在抽象稳定后,逐步添加平台适配器:

platform/
├── qq/         # V0 就有(从根包 openapi/ 迁移)
├── discord/    # 新增(基于 discordgo SDK)
├── telegram/   # 新增(Bot API long-polling)
├── onebot/     # 新增(OneBot v11 协议)
├── satori/     # 新增(Satori 协议)
├── wechat/     # 新增
└── milky/      # 新增(自定义 WS 协议)

V4 优化:适配器快照(零锁热路径)

多平台引入了一个性能隐患:每次事件处理都需要从 Registry 查找适配器。最初实现使用 sync.RWMutex

// V3 方式 — 每事件加锁
func (b *Bot) handlePlatformEvent(event platform.Event) {
    b.mu.RLock()
    pa := b.platformRegistry.Get(event.Platform())
    b.mu.RUnlock()
    // ...
}

虽然读锁允许多并发,但在 475K msg/s 下锁的开销仍然显著。V4 改为启动时构建只读快照

// V4(当前)— 启动时构建快照,热路径零锁
// Start() 时构建
snapshot := make(map[string]adapterCache)
for _, pa := range reg.All() {
    snapshot[pa.Platform()] = adapterCache{...}
}
b.adapterSnapshot.Store(snapshot)

// 热路径 — 仅 atomic.Load
snapshot := b.adapterSnapshot.Load()
if c, ok := snapshot[event.Platform()]; ok {
    sender = c.adapter.Sender()
    caps = c.caps
}

适配器断连感知:新增可选接口 RecoverableAdapter,适配器可以注册断连回调:

type RecoverableAdapter interface {
    OnDisconnect(fn func(err error)) (unregister func())
}

配合 AdapterObserver 和 Prometheus 指标,实现断连告警和自动恢复。

迭代历程

版本 核心变化 解决的问题
V0 Bot 紧耦合 QQ openapi/dto 快速实现,只支持 QQ
V1 Context 双路径(旧 dto + 新 platform) 过渡方案,兼容旧代码
V2(核心) platform.Event / Adapter / Registry 三大抽象 彻底解耦,多平台支持
V3 扩展 QQ/Discord/Telegram/OneBot/Satori 等适配器 丰富的平台生态
V4(当前) 适配器快照 + 断连感知 + BotIdentity 热路径零锁,运维友好

设计权衡

方面 选择 理由
协议 Adapter 接口抽象 平台无关,不依赖特定 SDK
事件分类 EventKind 枚举 + Raw 原始事件 业务用枚举,调试用原始事件
Sender 每个平台独立实现 适配各平台 API 差异
多平台 Registry + 快照 零锁热路径 + 统一管理
Capabilities 声明式能力模型 渐进增强策略