Files
cellbot/internal/adapter/onebot11/adapter.go
lafay d16261e6bd feat: add rate limiting and improve event handling
- Introduced rate limiting configuration in config.toml with options for enabling, requests per second (RPS), and burst capacity.
- Enhanced event handling in the OneBot11 adapter to ignore messages sent by the bot itself.
- Updated the dispatcher to register rate limit middleware based on configuration settings.
- Refactored WebSocket message handling to support flexible JSON parsing and improved event type detection.
- Removed deprecated echo plugin and associated tests to streamline the codebase.
2026-01-05 01:00:38 +08:00

485 lines
13 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 onebot11
import (
"context"
"fmt"
"sync"
"time"
"cellbot/internal/engine"
"cellbot/internal/protocol"
"cellbot/pkg/net"
"github.com/bytedance/sonic"
"go.uber.org/zap"
)
// Adapter OneBot11协议适配器
type Adapter struct {
config *Config
logger *zap.Logger
wsManager *net.WebSocketManager
httpClient *HTTPClient
wsWaiter *WSResponseWaiter
eventBus *engine.EventBus
selfID string
connected bool
mu sync.RWMutex
wsConnection *net.WebSocketConnection
ctx context.Context
cancel context.CancelFunc
}
// Config OneBot11配置
type Config struct {
// 连接配置
ConnectionType string `json:"connection_type"` // ws, ws-reverse, http, http-post
Host string `json:"host"`
Port int `json:"port"`
AccessToken string `json:"access_token"`
// WebSocket配置
WSUrl string `json:"ws_url"` // 正向WS地址
WSReverseUrl string `json:"ws_reverse_url"` // 反向WS监听地址
Heartbeat int `json:"heartbeat"` // 心跳间隔(秒)
ReconnectInterval int `json:"reconnect_interval"` // 重连间隔(秒)
// HTTP配置
HTTPUrl string `json:"http_url"` // 正向HTTP地址
HTTPPostUrl string `json:"http_post_url"` // HTTP POST上报地址
Secret string `json:"secret"` // 签名密钥
Timeout int `json:"timeout"` // 超时时间(秒)
// 其他配置
SelfID string `json:"self_id"` // 机器人QQ号
Nickname string `json:"nickname"` // 机器人昵称
}
// NewAdapter 创建OneBot11适配器
func NewAdapter(config *Config, logger *zap.Logger, wsManager *net.WebSocketManager, eventBus *engine.EventBus) *Adapter {
ctx, cancel := context.WithCancel(context.Background())
timeout := time.Duration(config.Timeout) * time.Second
if timeout == 0 {
timeout = 30 * time.Second
}
adapter := &Adapter{
config: config,
logger: logger.Named("onebot11"),
wsManager: wsManager,
wsWaiter: NewWSResponseWaiter(timeout, logger),
eventBus: eventBus,
selfID: config.SelfID,
ctx: ctx,
cancel: cancel,
}
// 如果使用HTTP连接初始化HTTP客户端
if config.ConnectionType == "http" && config.HTTPUrl != "" {
adapter.httpClient = NewHTTPClient(config.HTTPUrl, config.AccessToken, timeout, logger)
}
return adapter
}
// Name 获取协议名称
func (a *Adapter) Name() string {
return "OneBot"
}
// Version 获取协议版本
func (a *Adapter) Version() string {
return "11"
}
// Connect 建立连接
func (a *Adapter) Connect(ctx context.Context) error {
a.mu.Lock()
defer a.mu.Unlock()
if a.connected {
return fmt.Errorf("already connected")
}
a.logger.Info("Starting OneBot11 connection",
zap.String("connection_type", a.config.ConnectionType),
zap.String("self_id", a.selfID))
switch a.config.ConnectionType {
case "ws":
return a.connectWebSocket(ctx)
case "ws-reverse":
return a.connectWebSocketReverse(ctx)
case "http":
return a.connectHTTP(ctx)
case "http-post":
return a.connectHTTPPost(ctx)
default:
return fmt.Errorf("unsupported connection type: %s", a.config.ConnectionType)
}
}
// Disconnect 断开连接
func (a *Adapter) Disconnect(ctx context.Context) error {
a.mu.Lock()
defer a.mu.Unlock()
if !a.connected {
a.logger.Debug("Already disconnected, skipping")
return nil
}
a.logger.Info("Disconnecting OneBot11 adapter",
zap.String("connection_type", a.config.ConnectionType))
// 取消上下文
if a.cancel != nil {
a.cancel()
a.logger.Debug("Context cancelled")
}
// 关闭WebSocket连接
if a.wsConnection != nil {
a.logger.Info("Closing WebSocket connection",
zap.String("connection_id", a.wsConnection.ID))
a.wsManager.RemoveConnection(a.wsConnection.ID)
a.wsConnection = nil
}
// 关闭HTTP客户端
if a.httpClient != nil {
if err := a.httpClient.Close(); err != nil {
a.logger.Error("Failed to close HTTP client", zap.Error(err))
} else {
a.logger.Debug("HTTP client closed")
}
}
a.connected = false
a.logger.Info("OneBot11 adapter disconnected successfully")
return nil
}
// IsConnected 检查连接状态
func (a *Adapter) IsConnected() bool {
a.mu.RLock()
defer a.mu.RUnlock()
return a.connected
}
// GetSelfID 获取机器人自身ID
func (a *Adapter) GetSelfID() string {
return a.selfID
}
// SendAction 发送动作
func (a *Adapter) SendAction(ctx context.Context, action protocol.Action) (map[string]interface{}, error) {
// 序列化为OneBot11格式
data, err := a.SerializeAction(action)
if err != nil {
return nil, err
}
switch a.config.ConnectionType {
case "ws", "ws-reverse":
return a.sendActionWebSocket(data)
case "http":
return a.sendActionHTTP(data)
default:
return nil, fmt.Errorf("unsupported connection type for sending action: %s", a.config.ConnectionType)
}
}
// HandleEvent 处理事件
func (a *Adapter) HandleEvent(ctx context.Context, event protocol.Event) error {
a.logger.Debug("Handling event",
zap.String("type", string(event.GetType())),
zap.String("detail_type", event.GetDetailType()))
return nil
}
// ParseMessage 解析原始消息为Event
func (a *Adapter) ParseMessage(raw []byte) (protocol.Event, error) {
var rawEvent RawEvent
if err := sonic.Unmarshal(raw, &rawEvent); err != nil {
return nil, fmt.Errorf("failed to unmarshal raw event: %w", err)
}
// 忽略机器人自己发送的消息
if rawEvent.PostType == "message_sent" {
return nil, fmt.Errorf("ignoring message_sent event")
}
return a.convertToEvent(&rawEvent)
}
// SerializeAction 序列化Action为协议格式
func (a *Adapter) SerializeAction(action protocol.Action) ([]byte, error) {
// 转换为OneBot11格式
ob11ActionName := ConvertAction(action)
// 检查是否有未转换的动作类型(如果转换后的名称与原始类型相同,说明没有匹配到)
originalType := string(action.GetType())
if ob11ActionName == originalType {
a.logger.Warn("Action type not converted, using original type",
zap.String("action_type", originalType),
zap.String("hint", "This action type may not be supported by OneBot11"))
}
ob11Action := &OB11Action{
Action: ob11ActionName,
Params: action.GetParams(),
}
return sonic.Marshal(ob11Action)
}
// connectWebSocket 正向WebSocket连接
func (a *Adapter) connectWebSocket(ctx context.Context) error {
if a.config.WSUrl == "" {
return fmt.Errorf("ws_url is required for ws connection")
}
a.logger.Info("Connecting to OneBot WebSocket server",
zap.String("url", a.config.WSUrl),
zap.Bool("has_token", a.config.AccessToken != ""))
// 添加访问令牌到URL
wsURL := a.config.WSUrl
if a.config.AccessToken != "" {
wsURL += "?access_token=" + a.config.AccessToken
a.logger.Debug("Added access token to WebSocket URL")
}
a.logger.Info("Dialing WebSocket...",
zap.String("full_url", wsURL))
wsConn, err := a.wsManager.Dial(wsURL, a.selfID)
if err != nil {
a.logger.Error("Failed to connect WebSocket",
zap.String("url", a.config.WSUrl),
zap.Error(err))
return fmt.Errorf("failed to connect websocket: %w", err)
}
a.wsConnection = wsConn
a.connected = true
a.logger.Info("WebSocket connected successfully",
zap.String("url", a.config.WSUrl),
zap.String("remote_addr", wsConn.RemoteAddr),
zap.String("connection_id", wsConn.ID))
// 启动消息接收处理
go a.handleWebSocketMessages()
a.logger.Info("WebSocket message handler started")
return nil
}
// connectWebSocketReverse 反向WebSocket连接
func (a *Adapter) connectWebSocketReverse(ctx context.Context) error {
// 反向WebSocket由客户端主动连接到服务器
// WebSocket服务器会在主Server中启动
// 这里只需要标记为已连接状态等待客户端通过HTTP服务器连接
a.connected = true
a.logger.Info("OneBot11 adapter ready for reverse WebSocket connections",
zap.String("bot_id", a.selfID),
zap.String("listen_addr", a.config.WSReverseUrl))
// 注意实际的WebSocket服务器由pkg/net/server.go提供
// OneBot客户端需要连接到 ws://host:port/ws?bot_id=<selfID>
return nil
}
// connectHTTP 正向HTTP连接
func (a *Adapter) connectHTTP(ctx context.Context) error {
if a.config.HTTPUrl == "" {
return fmt.Errorf("http_url is required for http connection")
}
// 创建HTTP客户端
// TODO: 实现HTTP轮询
a.connected = true
a.logger.Info("HTTP connected",
zap.String("url", a.config.HTTPUrl))
return nil
}
// connectHTTPPost HTTP POST上报
func (a *Adapter) connectHTTPPost(ctx context.Context) error {
if a.config.HTTPPostUrl == "" {
return fmt.Errorf("http_post_url is required for http-post connection")
}
// HTTP POST由客户端主动推送事件
a.connected = true
a.logger.Info("HTTP POST ready",
zap.String("url", a.config.HTTPPostUrl))
return nil
}
// sendActionWebSocket 通过WebSocket发送动作
func (a *Adapter) sendActionWebSocket(data []byte) (map[string]interface{}, error) {
if a.wsConnection == nil {
return nil, fmt.Errorf("websocket connection not established")
}
// 解析请求以获取或添加echo
var req OB11Action
if err := sonic.Unmarshal(data, &req); err != nil {
return nil, fmt.Errorf("failed to unmarshal action: %w", err)
}
// 如果没有echo生成一个
if req.Echo == "" {
req.Echo = GenerateEcho()
var err error
data, err = sonic.Marshal(req)
if err != nil {
return nil, fmt.Errorf("failed to marshal action with echo: %w", err)
}
}
// 发送消息
if err := a.wsConnection.SendMessage(data); err != nil {
return nil, fmt.Errorf("failed to send action: %w", err)
}
// 等待响应
resp, err := a.wsWaiter.Wait(req.Echo)
if err != nil {
return nil, err
}
// 检查响应状态
if resp.Status != "ok" && resp.Status != "async" {
return resp.Data, fmt.Errorf("action failed (retcode=%d)", resp.RetCode)
}
return resp.Data, nil
}
// sendActionHTTP 通过HTTP发送动作
func (a *Adapter) sendActionHTTP(data []byte) (map[string]interface{}, error) {
if a.httpClient == nil {
return nil, fmt.Errorf("http client not initialized")
}
// 解析请求
var req OB11Action
if err := sonic.Unmarshal(data, &req); err != nil {
return nil, fmt.Errorf("failed to unmarshal action: %w", err)
}
// 调用HTTP API
resp, err := a.httpClient.Call(a.ctx, req.Action, req.Params)
if err != nil {
return nil, err
}
// 检查响应状态
if resp.Status != "ok" && resp.Status != "async" {
return resp.Data, fmt.Errorf("action failed (retcode=%d)", resp.RetCode)
}
return resp.Data, nil
}
// handleWebSocketMessages 处理WebSocket消息
func (a *Adapter) handleWebSocketMessages() {
a.logger.Info("WebSocket message handler started, waiting for messages...")
for {
select {
case <-a.ctx.Done():
a.logger.Info("Context cancelled, stopping WebSocket message handler")
return
default:
}
if a.wsConnection == nil || a.wsConnection.Conn == nil {
a.logger.Warn("WebSocket connection is nil, stopping message handler")
return
}
// 读取消息
_, message, err := a.wsConnection.Conn.ReadMessage()
if err != nil {
a.logger.Error("Failed to read WebSocket message",
zap.Error(err),
zap.String("connection_id", a.wsConnection.ID))
return
}
a.logger.Debug("Received WebSocket message",
zap.Int("size", len(message)),
zap.String("preview", string(message[:min(len(message), 200)])))
// 尝试解析为响应(先检查是否有 echo 字段)
var tempMap map[string]interface{}
if err := sonic.Unmarshal(message, &tempMap); err == nil {
if echo, ok := tempMap["echo"].(string); ok && echo != "" {
// 有 echo 字段说明是API响应
var resp OB11Response
if err := sonic.Unmarshal(message, &resp); err == nil {
a.logger.Debug("Received API response",
zap.String("echo", resp.Echo),
zap.String("status", resp.Status),
zap.Int("retcode", resp.RetCode))
a.wsWaiter.Notify(&resp)
continue
} else {
a.logger.Warn("Failed to parse API response",
zap.Error(err),
zap.String("echo", echo))
}
}
}
// 否则当作事件处理
a.logger.Info("Received OneBot event",
zap.ByteString("raw_event", message))
// 解析事件
a.logger.Info("Parsing OneBot event...")
event, err := a.ParseMessage(message)
if err != nil {
// 如果是忽略的事件(如 message_sent只记录 debug 日志
if err.Error() == "ignoring message_sent event" {
a.logger.Debug("Ignoring message_sent event")
} else {
a.logger.Error("Failed to parse event",
zap.Error(err),
zap.ByteString("raw_message", message))
}
continue
}
// 发布事件到事件总线
a.logger.Info("Publishing event to event bus",
zap.String("event_type", string(event.GetType())),
zap.String("detail_type", event.GetDetailType()),
zap.String("self_id", event.GetSelfID()))
a.eventBus.Publish(event)
a.logger.Info("Event published successfully")
}
}
func min(a, b int) int {
if a < b {
return a
}
return b
}