GoroBot

Go语言编写的跨平台聊天机器人框架

快速入门

支持平台

特性

  • 高性能(我瞎说的反正 Go 怎么也比 Node 快)
  • 高扩展性(插件化设计)
  • 高自订性(插件想干啥都行.jpg)

能做什么

  • 方便地编写自己的插件
  • 所见即所得的命令格式
  • 以通用的方法回复消息

注意事项

如果有能力,请审查所有使用的代码!!! 由于语言的特性,开发者拥有极高的权限,请在使用不可信来源的插件时多加注意!

TODO

欢迎向本项目提交 Issue

Roadmap

  • 完善指令系统

Official Plugins

  • ping: bot 还在线吗?ping 一下看看
  • message_logger: 在控制台输出消息日志
  • go_plugin: 支持 go 风格热插拔式插件

Bugs

  • Onebot 适配器未稳定(http连接未完善)

快速入门

安装框架

本框架仍然处于开发状态,代码迭代速度快,建议使用第二种安装方法。

从代码包导入

  • go get github.com/Jel1ySpot/GoroBot
  • main.go 中导入包

从仓库导入

Fork 并克隆仓库,在 main.go 文件与 plugins/ 文件夹中编写业务逻辑代码(已添加至 .gitignore)。 这样既能够适应框架的快速迭代,还可以更方便向本仓库提交 PR (对,我 鼓励任何人提交修复或功能代码,也欢迎在 github issue 中提出在使用过程中的任何问题与建议)

示范:

#/bin/bash
git clone https://github.com/PathtoYou/rRepo

cd rRpot
mkdir plugins
touch main.go

最小示例

这是一个最小示例:

package main

import (
	GoroBot "github.com/Jel1ySpot/GoroBot/pkg/core"
)

func main() {
	grb := GoroBot.Create()

	if err := grb.Run(); err != nil {
		panic(err)
	}
}

第一次运行上面的代码会报错,提示我们要填写配置文件(conf/config.json)。我们可以按照以下格式填写:

{
  "log_level": 1, // 日志等级。 0:Debug, 1:Info, 2:Warning, 3:Error
  "owner": { // 机器人所有者
    "qq": "你的QQ号" // 格式:"平台": "ID"
  }
}

填写完成后再次启动,可以发现报错消失了,但控制台没有输出。这是因为这段代码没有载入任何逻辑代码。在这段代码中,我们创建了一个 GoroBot 实例(为了方便,在文档中会用 grb 代表 GoroBot 实例),并通过 grb.Run() 运行了它。

引入适配器

GoroBot 框架使用适配器支持各种 IM 平台。适配器也是一种插件。让我们尝试引入一个适配器:

import LgrClient "github.com/Jel1ySpot/GoroBot/pkg/lagrange" // 引入模块

// 在 main 函数中:
grb.Use(LgrClient.Create())

插件使用 Create() 创建一个服务,我们可以使用 grb.Use(*Service) 来使用一个服务。运行这段代码,如无意外还需要填写配置文件,详细请见 pkg/lagrange。 配置好服务后再次运行,没有意外的话机器人服务就启动成功了。

目前支持的适配器:

适配器导入路径说明
Lagrangepkg/lagrangeQQ 平台
OneBotpkg/onebotOneBot 协议(WebSocket)
QBotpkg/qbotQQ 官方机器人
Telegrampkg/telegramTelegram Bot API

每个适配器首次运行后会在 conf/<adapter>/ 下生成配置文件,填写后重新启动即可。

使用插件

同样是一个例子:

import "github.com/Jel1ySpot/GoroBot/example_plugin/message_logger" // 引入模块

// 在 main 函数中
grb.Use(message_logger.Create())

再次运行代码,尝试向机器人账号发送消息,如果控制台中出现了刚才发送的消息,代表框架已经搭建成功了!开始编写属于你自己的 IM Chatbot 吧!

接下来应该做什么

事件系统

GoroBot 通过事件系统实现了消息监听等功能。这一设计允许插件开发者和用户编写简洁、通用的代码来扩展和实现各种功能。

以消息事件举例

以下示例代码展示了如何使用事件系统注册和注销一个监听器:

import botc "github.com/Jel1ySpot/GoroBot/pkg/core/bot_context"

var del func()
del, _ = grb.On(GoroBot.MessageEvent(func(ctx botc.MessageContext) {
	if ctx.String() == "ping" {
		_, _ = ctx.ReplyText("🏓")
		del()
	}
}))

在这段代码中,通过调用 grb.On 方法,注册了一个监听消息事件的监听器。监听器的回调函数接收一个 ctx(消息内容上下文)参数。当接收到的消息内容为 "ping" 时,调用 ctx.ReplyText 方法回复 "🏓",并通过 del 函数注销该监听器,从而实现了单次响应特定消息的功能。

MessageEvent() 函数接受一个回调函数作为参数,并返回一个 Name 属性为 "message"EventHandler 实例。框架通过将用户提供的回调函数转换为 func(args ...any) error 的通用签名形式,简化了事件注册的流程。这种设计使开发者无需关心底层实现的细节,便可直观地使用框架或插件提供的事件注册器。

常用的消息上下文方法

  • ctx.String() — 消息文本预览
  • ctx.Message() — 获取完整消息结构(*botc.BaseMessage
  • ctx.Protocol() — 消息来源平台("lagrange""telegram" 等)
  • ctx.SenderID() — 发送者 ID
  • ctx.ReplyText(...) — 回复纯文本
  • ctx.Reply(elements) — 回复消息元素
  • ctx.BotContext() — 获取所在平台的适配器上下文

如果你需要的是对特定格式的消息做出回复(比如 /command arg1 arg2),那你应该看看命令系统

中间件

中间件使用 grb.Middleware(MiddlewareCallback) 注册。跟事件一样,这个方法返回一个函数用于注销该中间件。

MiddlewareCallback 的签名为:

func(msg botc.MessageContext, next func(...MiddlewareCallback) error) error

跟消息事件不同的是,中间件的回调函数参数中有一个额外的参数 next,只有调用了它才会进入接下来的流程。

使用中间件实现的 ping

grb.Middleware(func(msg botc.MessageContext, next func(...GoroBot.MiddlewareCallback) error) error {
	if msg.String() == "ping" {
		_, _ = msg.ReplyText("🏓")
	}
	return next()
})

prepare 模式

传入第二个参数 true 可以注册一个 prepare 中间件,它会在所有普通中间件之前执行:

grb.Middleware(func(msg botc.MessageContext, next func(...GoroBot.MiddlewareCallback) error) error {
	// 这段逻辑会在所有普通中间件之前执行
	return next()
}, true)

命令系统

命令系统是 GoroBot 中处理用户指令的核心。它提供了一个链式 API 来注册命令,支持参数、选项、子命令和别名。

注册一个命令

最简单的命令长这样:

import "github.com/Jel1ySpot/GoroBot/pkg/core/command"

delFn, _ := grb.Command("hello").
	Action(func(ctx *command.Context) error {
		_, _ = ctx.ReplyText("你好!")
		return nil
	}).
	Build()

用户发送 /hello 时,机器人会回复 "你好!"。Build() 返回一个注销函数,调用它可以移除这个命令。

命令描述

给命令加上描述,Telegram 适配器会自动同步到客户端的命令菜单:

grb.Command("hello").
	Description("打个招呼").
	Action(handler).
	Build()

参数

参数是命令后面跟着的值,比如 /echo 你好 中的 "你好":

grb.Command("echo").
	Argument("content", command.String, true, "要回复的内容").
	Action(func(ctx *command.Context) error {
		_, _ = ctx.ReplyText(ctx.KvArgs["content"])
		return nil
	}).
	Build()
  • 第一个参数是名称,在 ctx.KvArgs 中用它来取值
  • 第二个参数是类型:command.Stringcommand.Numbercommand.Boolean
  • 第三个参数表示是否必填
  • 第四个参数是帮助文本

取类型化的值可以用 command.GetInt()command.GetFloat()

选项

选项就是 --flag-f 这种写法:

grb.Command("search").
	Option("count", "n", command.Number, false, "10", "结果数量").
	Argument("keyword", command.String, true, "关键词").
	Action(func(ctx *command.Context) error {
		count := command.GetInt(ctx.KvArgs["count"])
		keyword := ctx.KvArgs["keyword"]
		// ...
		return nil
	}).
	Build()

用户可以这样用:/search --count 5 golang/search -n 5 golang

子命令

大的命令可以拆分成子命令:

cmd := grb.Command("plugin")

cmd.SubCommand("list").
	Action(func(ctx *command.Context) error {
		// /plugin list
		return nil
	}).Build()

cmd.SubCommand("load").
	Argument("name", command.String, true, "插件名").
	Action(func(ctx *command.Context) error {
		// /plugin load myplugin
		return nil
	}).Build()

cmd.Build()

注意最后要调用根命令的 Build() 来完成注册。

别名

别名让命令可以通过正则表达式匹配触发。比如骰子插件用 d6d20 这样的写法:

grb.Command("dice").
	Argument("upper_bound", command.Number, false, "骰子点数上限").
	Alias(`^d(\d+)$`, func(ctx *command.Context) *command.Context {
		_ = ctx.AppendArg(ctx.String()[1:])
		return ctx
	}).
	Action(func(ctx *command.Context) error {
		limit := command.GetInt(ctx.KvArgs["upper_bound"])
		// ...
		return nil
	}).
	Build()

别名的第一个参数是正则表达式,第二个参数是一个转换函数,可以在里面用 ctx.AppendArg() 把匹配到的内容追加为参数。转换函数可以传 nil,这样匹配到后会直接触发命令。

命令上下文

command.Context 嵌入了 botc.MessageContext,所以消息上下文的方法都能用。额外提供了以下内容:

  • ctx.KvArgs — 命名参数的键值对(map[string]string
  • ctx.Arguments — 位置参数列表([]string
  • ctx.Commands — 匹配到的命令/子命令路径
  • ctx.ReplyText(...) — 回复文本
  • ctx.Message() — 获取原始消息

在插件中使用

在插件的 Init 中注册命令,在 Release 中清理:

func (s *Service) Init(grb *GoroBot.Instant) error {
	s.bot = grb

	delFn, _ := grb.Command("mycommand").
		Description("我的命令").
		Action(func(ctx *command.Context) error {
			_, _ = ctx.ReplyText("收到!")
			return nil
		}).
		Build()

	s.releaseFunc = append(s.releaseFunc, delFn)
	return nil
}

更完整的示例可以参考 example_plugin/ 中的代码。

插件系统

GoroBot 的一切功能都是通过插件实现的,适配器也是插件。编写一个插件其实很简单,只需要实现三个方法就行。

插件骨架

每个插件都要实现 Service 接口:

package myplugin

import GoroBot "github.com/Jel1ySpot/GoroBot/pkg/core"

type Service struct {
	bot         *GoroBot.Instant
	releaseFunc []func()
}

func (s *Service) Name() string {
	return "MyPlugin"
}

func Create() *Service {
	return &Service{}
}

func (s *Service) Init(grb *GoroBot.Instant) error {
	s.bot = grb
	// 在这里注册命令、事件监听器、中间件等
	return nil
}

func (s *Service) Release(grb *GoroBot.Instant) error {
	for _, fn := range s.releaseFunc {
		fn()
	}
	return nil
}

就这么几行代码,一个空的插件就写好了。接下来往 Init 里面塞东西就行。

生命周期

  1. Create() — 创建插件实例。这时候框架还没启动,别在这里搞事情
  2. grb.Use(plugin) — 注册插件,告诉框架"我要用这个"
  3. Init() — 框架启动时调用。在这里注册命令、事件、中间件
  4. Release() — 框架关闭时调用。清理资源,注销注册过的东西

注册命令

func (s *Service) Init(grb *GoroBot.Instant) error {
	s.bot = grb

	delFn, _ := grb.Command("ping").
		Description("Ping 测试").
		Action(func(ctx *command.Context) error {
			_, _ = ctx.ReplyText("🏓")
			return nil
		}).
		Build()

	s.releaseFunc = append(s.releaseFunc, delFn)
	return nil
}

命令系统的详细用法参见 命令系统

监听事件

del, _ := grb.On(GoroBot.MessageEvent(func(ctx botc.MessageContext) {
	log.Info("收到消息: %s", ctx.String())
}))
s.releaseFunc = append(s.releaseFunc, del)

事件系统的详细用法参见 事件系统

使用中间件

del := grb.Middleware(func(msg botc.MessageContext, next func(...GoroBot.MiddlewareCallback) error) error {
	// 在消息到达事件和命令之前做点什么
	return next()
})
s.releaseFunc = append(s.releaseFunc, del)

中间件的详细用法参见 中间件系统

构建消息

如果你需要回复图片、引用、@某人之类的复杂消息:

_, _ = ctx.BotContext().NewMessageBuilder().
	Text("看看这张图").
	ImageFromFile("/path/to/image.png").
	ReplyTo(ctx)

MessageBuilder 支持的方法:

  • Text(text) — 文字
  • ImageFromFile(path) / ImageFromUrl(url) / ImageFromData(bytes) — 图片
  • Quote(baseMsg) — 引用消息
  • Mention(userID) — @某人
  • ReplyTo(msgCtx) — 作为回复发送
  • Send(chatID) — 发送到指定聊天

使用数据库

框架提供了一个可选的 SQLite 数据库:

db := grb.Database() // *sql.DB,如果没有打开数据库就是 nil
if db != nil {
	// 用标准 database/sql 操作
}

清理资源

很重要:在 Init 里注册的东西,一定要在 Release 里清理掉。所有 grb.On()grb.Command().Build()grb.Middleware() 都会返回一个注销函数,把它们收集起来,在 Release 里逐个调用就行了。

文件组织

把插件放在自己的包里就好。简单的插件一个 service.go 搞定,复杂一点的可以拆成多个文件:

example_plugin/myplugin/
├── service.go    # 生命周期(Init / Release)
├── commands.go   # 命令注册
└── ...

示例插件在 example_plugin/,是最好的参考。

API 文档

目前项目正处于开发初期,api 及其不稳定,建议参照 示例插件 中的使用方法。

资源文件管理

位于 pkg/core/resource.go,是 GoroBot 实例下的方法,为统一管理资源文件提供了接口。 文件存放于工作目录的 resources/ 下。

Resource

  • ID string 资源唯一标识符
  • Protocol string 来源协议(适配器 context ID)
  • RefLink string 协议特定的资源引用链接
  • FilePath string 资源文件本地路径
  • Downloaded time.Time 资源记录时间

保存资源引用链接,返回生成的资源 ID。此时并不会下载文件,而是等到 LoadResourceFromID 时再按需下载。

grb.LoadResourceFromID(id string) (string, error)

根据资源 ID 获取本地文件路径。如果文件还没下载,会通过对应适配器的 DownloadResourceFromRefLink 下载并缓存。

数据库操作

GoroBot 实例中存放着一个数据库接口。暴露了几个方法用于简易连接与操作。

grb.OpenDatabase(driverName string, dataSourceName string) error

打开数据库。同 sql.Open(driverName, dataSourceName)

grb.CloseDatabase() error

关闭数据库。

grb.Database() *sql.DB

获取数据库实例。

grb.DatabaseExist() bool

如果连接了数据库,返回 true,否则返回 false

消息类型

消息相关的类型位于 pkg/core/bot_context,包含消息上下文和消息结构。

MessageContext

消息的上下文,由适配器在触发消息事件时提供。

ctx.Protocol() string

消息的协议,值可以为机器人所处的平台或所使用的协议。

ctx.BotContext() BotContext

获取所在平台的适配器上下文。

ctx.String() string

返回消息的文本预览。

ctx.Message() *BaseMessage

返回消息结构。

ctx.SenderID() string

返回发送者 ID。

ctx.NewMessageBuilder() MessageBuilder

创建消息构建器。

ctx.Reply(elements []*MessageElement) (*BaseMessage, error)

在当前上下文中回复消息。具体实现由适配器决定。

ctx.ReplyText(a ...any) (*BaseMessage, error)

回复纯文本消息的便捷方法。

BaseMessage

消息的基本结构。含有以下属性:

  • MessageType MessageType 消息类型(DirectMessage / GroupMessage
  • ID string 消息 ID
  • Content string 显示文本
  • Elements []*MessageElement 消息元素
  • Sender *entity.Sender 消息发送者
  • Time time.Time 消息发送(接收)时间

*BaseMessage.Marshall() string

序列化消息实体,使其能被保存以及使用 UnmarshallMessage(string) 读取。

MessageElement

消息元素,消息的基本组成部分。

  • Type ElementType 元素类型
  • Content string 显示文本
  • Source string 多媒体索引

TextElement

  • Content: 显示文本
  • Source: ""

QuoteElement

  • Content: [回复]
  • Source: 被引用消息的序列化对象

MentionElement

  • Content: @Somebody
  • Source: protocol:被提及用户的ID

ImageElement

  • Content: [图片]
  • Source: 资源ID

VideoElement

  • Content: [视频]
  • Source: 资源ID

FileElement

  • Content: [文件]
  • Source: protocol:参数...

VoiceElement

  • Content: [语音]
  • Source: 资源ID

StickerElement

  • Content: [表情]
  • Source: protocol:参数...

LinkElement

  • Content: 百度一下,你就知道
  • Source: https://www.baidu.com

OtherElement

  • Content: 奇怪的东西
  • Source: protocol:参数

MessageBuilder

构建和发送消息的链式 API。通过 ctx.NewMessageBuilder()ctx.BotContext().NewMessageBuilder() 创建。

builder.Text(text string) MessageBuilder

添加文本内容。

builder.ImageFromFile(path string) MessageBuilder

从本地文件添加图片。

builder.ImageFromUrl(url string) MessageBuilder

从 URL 添加图片。

builder.ImageFromData(data []byte) MessageBuilder

从二进制数据添加图片。

builder.Quote(msg *BaseMessage) MessageBuilder

引用一条消息。

builder.Mention(id string) MessageBuilder

@某人。

builder.ReplyTo(msg MessageContext) (*BaseMessage, error)

作为回复发送到消息来源。

builder.Send(id string) (*BaseMessage, error)

发送到指定的聊天 ID。

BaseBuilder

构建消息元素链的工具,用于适配器内部解析消息。

NewBuilder() *BaseBuilder

创建构建器。

*BaseBuilder.Append(elementType ElementType, content string, source string) *BaseBuilder

在消息链中添加元素。

*BaseBuilder.Build() []*MessageElement

返回构造完成的元素链。

插件列表

欢迎在 issue 中提供你所写的插件信息。信息将会在维护者查看后加入列表。

注意事项

重申:如果有能力,请审查所有使用的代码!!! 由于语言的特性,开发者拥有极高的权限,请在使用不可信来源的插件时多加注意!

恶意插件可以做到的事情有:

  • 向恶意者发送消息
  • 发送恶意消息
  • 向恶意者传送账号信息以盗取账号
  • 盗取你的本机账号

插件

消息日志

import "github.com/Jel1ySpot/GoroBot/example_plugin/message_logger"

基础的消息事件监听。将消息预览打印到日志。

Ping

import "github.com/Jel1ySpot/GoroBot/example_plugin/ping"

检测机器人连接状态。发送 ping/ping 回复 🏓。

骰子

import "github.com/Jel1ySpot/GoroBot/example_plugin/dice/dice"

掷骰子。支持 /dice 20 或别名 d20 的写法。

插件载入

import "github.com/Jel1ySpot/GoroBot/example_plugin/go_plugin"

载入外部 .so 插件,支持加载/卸载/启用/禁用。限类 Unix 系统。

调试工具

import "github.com/Jel1ySpot/GoroBot/example_plugin/tests"

一些调试用的命令:/repr(输出消息 JSON)、/getOwner/getResourceFromID/sendImage