Files
cellbot/docs/plugin_guide.md
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

8.5 KiB
Raw Blame History

CellBot 插件开发指南

快速开始

CellBot 提供了类似 ZeroBot 风格的插件注册方式,可以在一个包内注册多个处理函数。

ZeroBot 风格(推荐)

init 函数中使用 OnXXX().Handle() 注册处理函数,一个包内可以注册多个:

package echo

import (
    "context"
    "cellbot/internal/engine"
    "cellbot/internal/protocol"
    "go.uber.org/zap"
)

func init() {
    // 处理私聊消息
    engine.OnPrivateMessage().
        Handle(func(ctx context.Context, event protocol.Event, botManager *protocol.BotManager, logger *zap.Logger) error {
            data := event.GetData()
            message := data["message"]
            userID := data["user_id"]
            
            // 获取 bot 实例
            bot, _ := botManager.Get(event.GetSelfID())
            
            // 发送回复
            action := &protocol.BaseAction{
                Type: protocol.ActionTypeSendPrivateMessage,
                Params: map[string]interface{}{
                    "user_id": userID,
                    "message": message,
                },
            }
            
            return bot.SendAction(ctx, action)
        })
    
    // 可以继续注册更多处理函数
    engine.OnGroupMessage().
        Handle(func(ctx context.Context, event protocol.Event, botManager *protocol.BotManager, logger *zap.Logger) error {
            // 处理群消息
            return nil
        })
    
    // 处理命令
    engine.OnCommand("/help").
        Handle(func(ctx context.Context, event protocol.Event, botManager *protocol.BotManager, logger *zap.Logger) error {
            // 处理 /help 命令
            return nil
        })
}

注意:需要在 internal/di/providers.go 中导入插件包以触发 init 函数:

import (
    _ "cellbot/internal/plugins/echo" // 导入插件以触发 init
)

方式二:传统方式

实现 protocol.EventHandler 接口:

package myplugin

import (
    "context"
    "cellbot/internal/protocol"
    "go.uber.org/zap"
)

type MyPlugin struct {
    logger     *zap.Logger
    botManager *protocol.BotManager
}

func NewMyPlugin(logger *zap.Logger, botManager *protocol.BotManager) *MyPlugin {
    return &MyPlugin{
        logger:     logger.Named("my-plugin"),
        botManager: botManager,
    }
}

func (p *MyPlugin) Name() string {
    return "MyPlugin"
}

func (p *MyPlugin) Description() string {
    return "我的插件"
}

func (p *MyPlugin) Priority() int {
    return 100
}

func (p *MyPlugin) Match(event protocol.Event) bool {
    return event.GetType() == protocol.EventTypeMessage
}

func (p *MyPlugin) Handle(ctx context.Context, event protocol.Event) error {
    // 处理逻辑
    return nil
}

内置匹配器

提供了以下便捷的匹配器函数:

  • engine.OnPrivateMessage() - 匹配私聊消息
  • engine.OnGroupMessage() - 匹配群消息
  • engine.OnMessage() - 匹配所有消息
  • engine.OnNotice() - 匹配通知事件
  • engine.OnRequest() - 匹配请求事件
  • engine.OnCommand(prefix) - 匹配命令(以指定前缀开头)
  • engine.OnPrefix(prefix) - 匹配以指定前缀开头的消息
  • engine.OnSuffix(suffix) - 匹配以指定后缀结尾的消息
  • engine.OnKeyword(keyword) - 匹配包含指定关键词的消息
  • engine.On(matchFunc) - 自定义匹配器

自定义匹配器

func init() {
    engine.On(func(event protocol.Event) bool {
        // 自定义匹配逻辑
        if event.GetType() != protocol.EventTypeMessage {
            return false
        }
        
        data := event.GetData()
        message, ok := data["raw_message"].(string)
        if !ok {
            return false
        }
        
        // 只匹配以 "/" 开头的消息
        return len(message) > 0 && message[0] == '/'
    }).
    Priority(50). // 可以设置优先级
    Handle(func(ctx context.Context, event protocol.Event, botManager *protocol.BotManager, logger *zap.Logger) error {
        // 处理命令
        return nil
    })
}

注册插件

插件通过 init 函数自动注册,只需在 internal/di/providers.go 中导入插件包:

import (
    _ "cellbot/internal/plugins/echo"  // 导入插件以触发 init
    _ "cellbot/internal/plugins/other" // 可以导入多个插件包
)

插件会在应用启动时自动加载,无需手动注册。

插件优先级

优先级数值越小,越先执行。建议:

  • 0-50: 高优先级(预处理、权限检查等)
  • 51-100: 中等优先级(普通功能插件)
  • 101-200: 低优先级(日志记录、统计等)

完整示例

示例 1关键词回复插件

package keyword

import (
    "context"
    "cellbot/internal/engine"
    "cellbot/internal/protocol"
    "go.uber.org/zap"
)

func init() {
    keywords := map[string]string{
        "你好":   "你好呀!",
        "再见":   "再见~",
        "帮助":   "发送 /help 查看帮助",
    }
    
    engine.OnMessage().
        Priority(80).
        Handle(func(ctx context.Context, event protocol.Event, botManager *protocol.BotManager, logger *zap.Logger) error {
            data := event.GetData()
            message, ok := data["raw_message"].(string)
            if !ok {
                return nil
            }
            
            // 检查关键词
            reply, found := keywords[message]
            if !found {
                return nil
            }
            
            // 获取 bot 和用户信息
            bot, _ := botManager.Get(event.GetSelfID())
            userID := data["user_id"]
            
            // 发送回复
            action := &protocol.BaseAction{
                Type: protocol.ActionTypeSendPrivateMessage,
                Params: map[string]interface{}{
                    "user_id": userID,
                    "message": reply,
                },
            }
            
            _, err := bot.SendAction(ctx, action)
            return err
        })
}

示例 2命令插件

func RegisterCommandPlugin(registry *engine.PluginRegistry, botManager *protocol.BotManager, logger *zap.Logger) {
    plugin := engine.NewPlugin("CommandPlugin").
        Description("命令处理插件").
        Priority(50).
        Match(func(event protocol.Event) bool {
            if event.GetType() != protocol.EventTypeMessage {
                return false
            }
            data := event.GetData()
            message, ok := data["raw_message"].(string)
            return ok && len(message) > 0 && message[0] == '/'
        }).
        Handle(func(ctx context.Context, event protocol.Event) error {
            data := event.GetData()
            message := data["raw_message"].(string)
            userID := data["user_id"]
            
            bot, _ := botManager.Get(event.GetSelfID())
            
            var reply string
            switch message {
            case "/help":
                reply = "可用命令:\n/help - 显示帮助\n/ping - 测试连接\n/time - 显示时间"
            case "/ping":
                reply = "pong!"
            case "/time":
                reply = time.Now().Format("2006-01-02 15:04:05")
            default:
                reply = "未知命令,发送 /help 查看帮助"
            }
            
            action := &protocol.BaseAction{
                Type: protocol.ActionTypeSendPrivateMessage,
                Params: map[string]interface{}{
                    "user_id": userID,
                    "message": reply,
                },
            }
            
            _, err := bot.SendAction(ctx, action)
            return err
        }).
        Build()
    
    registry.Register(plugin)
}

最佳实践

  1. 使用简化方式:对于简单插件,使用 engine.NewPlugin 构建器
  2. 使用传统方式:对于复杂插件(需要状态管理、配置等),使用传统方式
  3. 合理设置优先级:确保插件按正确顺序执行
  4. 错误处理:在 Handle 函数中妥善处理错误
  5. 日志记录:使用 logger 记录关键操作
  6. 避免阻塞Handle 函数应快速返回,耗时操作应使用 goroutine

插件生命周期

插件在应用启动时注册,在应用运行期间持续监听事件。目前不支持热重载。

调试技巧

  1. 使用 logger.Debug 记录调试信息
  2. 在 Match 函数中添加日志,确认匹配逻辑
  3. 检查事件数据结构,确保字段存在
  4. 使用 zap.Any 打印完整事件数据
logger.Debug("Event data", zap.Any("data", event.GetData()))