Mirai框架开发QQ机器人

QQ机器人

酷Q机器人已经从叱咤风云的QQ机器人框架中消失,仅存的几款开源框架,不是效果不理想,就是自定义度不高。而Mirai就是这样一个框架,其内部的功能并不是很强大,但它的封装并不严格,所以也就有机会能和底层进行交流。

先用几句话把几个名字的关系理清楚,不然刚开始翻文档很容易被绕晕。Mirai本身是一个开源的QQ协议库,负责跟腾讯的服务器通信,把收发消息这些操作包装成API;Mirai Console是跑在它上面的控制台框架,管理账号、加载插件、提供命令行;而MCL也就是Mirai Console Loader,是官方出的启动器,启动的时候会自动把Console本体和依赖下载拼好。我们平时说的“搭机器人”,其实就是装好MCL,再把自己的插件丢进它的插件目录。

这套框架最舒服的地方就是插件机制。本体功能不多,但功能都能通过插件往外扩,写坏了把jar一删就恢复原样,不会把整个机器人搞崩。插件可以用Kotlin写,也可以像下面这样用Java写,两者都编译成JVM字节码,互相调用没什么障碍,会哪个用哪个就行。常见的自动回复、群管理、消息转发,走的都是同一个套路:收到事件、处理、再回一条消息,理解了这个流程,剩下的就是查API了。

必要的知识

首先你要保证你已经掌握了Java或者Kotlin的语法规则,并且能够写出最基本的代码(Hello world)。要求不高,把类、接口、泛型和lambda看过一遍就行,因为下面注册事件监听用的就是lambda写法。Mirai本身是Kotlin写的,官方文档和社区里大部分例子也是Kotlin,碰到读不懂的地方可以对照Java写法去理解。语言选哪个都不影响功能,我后面统一用Java举例。

对了,JDK也要提前装好,版本不能低于11,这个到安装MCL的时候再细说。

安装MCL

MCL全称Mirai Console Loader,就是我们用来启动和管理Console的工具。装好它之后,Console本体和官方插件都是它启动时按需拉取的,我们不用一个个手动配环境。安装方式有两种,选一种顺手的就行。

自动安装

MCL Installer自动安装工具可以获取到所有系统的iTXTech MCL。进去下载对应系统的安装器,运行之后跟着提示走,它会自动生成一个能用的MCL目录,省得自己折腾,图省事就用这个。

手动安装

  1. 安装 Java 运行时(版本必须 >= 11)
  2. 从 Releases 下载最新版本的MCL
  3. 解压到某处
  4. 在命令行中执行.\mcl以启动MCL
  5. 启动过程中或许会需要安装openjdk,不推荐安装(使用你自己安装的jdk就可以)
  6. 如果启动过程中出现类文件版本高低等问题,都可以通过修改jdk版本解决

手动安装就是下载一个压缩包解压,目录里躺着mcl、mcl.cmd、mcl.jar这几个文件,Windows下在解压目录打开命令行敲.\mcl,Linux或者macOS下敲./mcl。第一次启动会去下载Console本体和一堆依赖,速度看网络,进度条走完之前别手快把它关了。启动成功后命令行会停在一个提示符上等输入,敲帮助指令就能看到所有可用命令,以后出问题也先来这里看日志。

Java版本这里多啰嗦两句:MCL和Console都跑在JVM上,JDK版本低了会直接报类文件版本相关的错误,其实就是它要求的Java版本比你正在用的高。遇到这种报错不用慌,把JAVA_HOME指到高版本的JDK再重新启动MCL一般就好了。

下载Mirai

开发插件不用把Console的源码拖下来看,官方给IDEA做了一个项目模板插件,建出来的工程自带Gradle配置和主类骨架,我们改自己的逻辑就行。

IDEA插件

打开IDEA,在文件->设置->插件中搜索Mirai Console

安装完成后重启IDEA,然后就可以新建一个Mirai项目了

选择jdk版本,新版本使用11及以上

你可以选择Kotlin或者Java,希望使用什么语言就选择什么语言,我这里用Java举例

一些简单的基本信息

文件路径

新建的时候有几个地方留意一下:JDK选11以上的,最好和运行MCL的JDK大版本一致,不然以后编译出来的插件容易加载失败;语言选Java还是Kotlin看习惯,功能一模一样;插件ID、名字、作者这些信息按自己喜好填,名字取得正经一点,之后在控制台的插件列表里都能看到。

开始编写

创建新项目的过程可能会有点慢,静静等待一会就好了,Gradle要在后台下依赖,第一次建项目卡个几分钟很正常。

新项目创建好的主类应该含有下面这些代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static final Plugin INSTANCE = new Plugin();
//这里是注册方法
private Plugin() {
super(new JvmPluginDescriptionBuilder("主类", "1.0")
.name("插件名字")
.author("作者名")
.build());
}
}
//插件被加载时的方法
@Override
public void onLoad(@NotNull PluginComponentStorage $this$onLoad) {
//一般在这里写有关配置文件的加载
}
//插件运行时的方法
@Override
public void onEnable() {
//主要功能的注册和事件的监听写在这里
}
//插件卸载时执行的方法
@Override
public void onDisable() {
//一般这里写保存配置文件或者关闭流
}

这段就是插件的骨架。INSTANCE是插件单例,整个运行期只有一个实例,别的地方想用它直接拿这个静态字段就行;构造函数里把插件信息交给JvmPluginDescriptionBuilder注册给Console,插件列表里显示的名字、版本、作者都来自这里;下面三个onXxx是生命周期方法,由Console在合适的时机自动调用,执行顺序是onLoad、onEnable、onDisable,其中onLoad只在加载时跑一次,适合读配置,onEnable每次启用都执行,事件监听和指令注册写在这,onDisable负责收尾,保存数据、关闭文件流这类活放这。

加载配置文件

1
2
3
4
@Override
public void onLoad(@NotNull PluginComponentStorage $this$onLoad) {
getConfigFolder();//获取配置文件目录,是以你插件的名字命名的
}

getConfigFolder()返回的是这个插件专属的配置目录,按插件名字命名,位于MCL根目录的config文件夹下,插件自己的配置就都往这里放。这里提一句为什么配置要在onLoad里读:插件刚被Console读进来的时候还没开始工作,这个时候把配置准备好,后面启用时就能直接用;如果暂时用不到配置文件,这个方法里什么都不写也完全没问题。

默认代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
public void onEnable() {
//注册监听事件
EventChannel<Event> eventChannel = GlobalEventChannel.INSTANCE.parentScope(this);

//可以选择监听群聊消息
eventChannel.subscribeAlways(GroupMessageEvent.class, g -> {
//监听群消息
System.out.println(g.getMessage().contentToString());
});

//也可以选择监听好友消息
eventChannel.subscribeAlways(FriendMessageEvent.class, f -> {
//好友消息
System.out.println(f.getMessage().contentToString());
});
//事件的种类很多就不一一列举了,不过写法都是类似的
}

这段是事件监听的基本写法。GlobalEventChannel.INSTANCE.parentScope(this)的意思是拿到全局事件频道,同时把作用域绑定到当前插件,好处是插件被停下或者卸载时,挂在它下面的监听会一起失效,不用一个个去取消。subscribeAlways用来订阅事件,第一个参数填事件种类,第二个参数就是收到事件后要执行的操作,例子里的操作只是把消息内容打印出来;getMessage().contentToString()是把消息链转成纯文本,文字原样保留,图片这类非文字元素会转成对应的表示形式。真要回消息的时候,把打印语句换成g.getGroup().sendMessage("...")这种写法就行。

常用事件

1
2
3
4
5
EventChannel<Event> eventChannel = GlobalEventChannel.INSTANCE.parentScope(this);
//定义操作
eventChannel.subscribeAlways(事件种类.class, g -> {
//操作
});
事件功能可供选择的操作
GroupEvent群聊事件.getBot()获取机器人Bot对象
FriendEvent好友事件.getBot()获取机器人Bot对象
StrangerEvent陌生人事件.getBot()获取机器人Bot对象
下面就是一些细化事件—-—-
FriendAddEvent添加好友事件.getFriend()获取添加者的Friend对象
StrangerAddEvent陌生人添加事件.getStranger()获取Stranger对象(一般Stranger都是临时会话的对象)
FriendAvatarChangedEvent好友头像改变事件.getBot()获取机器人Bot对象
MemberJoinEvent群聊新成员加入事件.getMember()获得成员对象

写法就这一个套路,换个事件种类就行。表里的事件大多是父类,具体事件都从它们继承,比如群消息、群成员加入属于群聊事件,好友消息、好友添加属于好友事件。订阅父类能接住这一类里的所有事件,订阅具体的事件类就只收那一种,拿不准用哪个就先写父类。

事件只是通知,真正干活还得看事件对象上的方法,下面按对象分类列一些常用的:

对象常用方法

GroupEvent:

​ 1、getBot()获取机器人

​ 2、getGroup()获取群聊对象:

Group:

​ 1、contains(QQ号)判断是否在群聊内

​ 2、getAvatarUrl()获取群头像链接

​ 3、getBotAsMember()作为群成员对象获取机器人

​ 4、getBotMuteRemaining()查看机器人被禁言的剩余时间

​ 5、getBotPermission()获得机器人许可等级

​ 6、getMembers()获得群成员列表

​ 7、getName()获得机器人的群昵称

​ 8、getSettings()获得群聊的设置

​ 9、sendMessage()在群聊发消息

FriendEvent:

​ 1、getFriend()获得好友对象

Friend:

​ 1、sendMessage()发送消息

​ 2、delete()删除好友

Member:

​ 1、getNameCard()获得名片

​ 2、isMuted()判断是否禁言

​ 3、unmute()解除禁言

更多的方法可以在IDEA中尝试,命名还是很好懂的

消息元素

在Java中,消息不是简单的字符串,而是一条由各种元素拼起来的消息链,文字、@、表情、图片、语音都算。构建的时候一般先new一个构造器:

1
MessageChainBuilder builder = new MessageChainBuilder();

拿到builder之后就可以往里塞元素了,常用的有下面这几个:

1
2
3
AtAll.INSTANCE(@所有人)
new Face(Face.表情名)
new At(114514L) (@某个人,加QQ号)

builder可以一直组装,塞完所有元素最后用builder.build()拿到完整消息链,丢给sendMessage就发出去了,顺序就是塞进去的顺序。比如先builder.append("你好"),再builder.append(new At(114514L)),出来的就是一条“你好+@某人”的消息。AtAll.INSTANCE是@全体成员,new Face(...)是QQ表情,new At(...)是@某人,括号里的参数类型是long,所以数字后面要带个L,不然编译不过。


图片:

1
2
3
4
5
6
7
8
9
10
11
12
EventChannel<Event> eventChannel = GlobalEventChannel.INSTANCE.parentScope(this);
eventChannel.subscribeAlways(FriendEvent.class, event->{
//发送本地图片
//构建资源对象
ExternalResource res = ExternalResource.create(new File("本地图片"));
//构建图片对象
Image image = event.getFriend().uploadImage(res);
//关闭资源流
res.close();
//发送图片
event.getFriend().sendMessage(image);
});

发图片分两种情况:发本地文件,或者发服务器上已经有的图片。本地文件要先构建ExternalResource资源对象,再调用uploadImage上传,拿到返回的Image之后才能发送。注意res.close()这步别省,资源流用完要关掉,不然文件一直被占着,顺序就是建资源、上传、关流、发送。

也可以通过图片ID发送图片,不过图片ID一般是看不到的,也没有方便的办法主动拿,平时发图还是老实用上面上传的方式。

1
2
3
4
5
EventChannel<Event> eventChannel = GlobalEventChannel.INSTANCE.parentScope(this);
eventChannel.subscribeAlways(FriendEvent.class, event->{
//发送图片
event.getFriend().sendMessage(Image.fromId("图片ID"));
});

发出声音:

1
2
3
4
5
6
7
8
9
10
EventChannel<Event> eventChannel = GlobalEventChannel.INSTANCE.parentScope(this);
eventChannel.subscribeAlways(FriendEvent.class, event->{
//还是资源对象
ExternalResource res = ExternalResource.create(new File("声音文件.amr"));
Audio audio = event.getFriend().uploadAudio(res); // 适用于2.7+
// Voice audio = contact.uploadVoice(res); // 可用于2.0~2.7
//关闭资源流
res.close();
event.getFriend().sendMessage(audio);
});

发语音的流程和发图片几乎一样,只是把uploadImage换成uploadAudio,拿到的Audio同样是消息元素,直接sendMessage就完事。代码注释里写了2.7前后的API差异:老版本用uploadVoice,类型是Voice,新版本换成了uploadAudio,要是你的Console版本比较旧跑不起来,按注释换回老方法就行。还有语音格式最好用QQ支持的(比如代码里的amr),随便换个别的格式多半发不出去。

指令

指令就是聊天里发/开头的命令词触发的功能,需要单独写一个类来继承JSimpleCommand。新建一个java文件,命名随意,我这里使用了echo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class 指令类 extends JSimpleCommand {

public static final 指令类 INSTANCE = new 指令类();

//构造函数,用于初始化对象
private 指令类() {
super(主类名.INSTANCE, "指令", "功能");
this.setDescription("描述");
}

@Handler
public void handle(CommandSender sender, User target, String msg) {
//然后就可以实现你的算法了
}
}

这个类也用了单例,INSTANCE是给外面注册用的;构造函数里super(主类名.INSTANCE, "指令", "功能")前两个参数是所属插件和主指令名,第三个开始是副指令名(别名),指令描述要用 setDescription 另外设置。真正干活的是@Handler标注的方法,第一个参数CommandSender是发指令的人,后面是跟着方法签名走的参数,写成什么样,用户调用时就得传什么样。想在处理函数里回复,用sender.sendMessage("...");想操作用户对象,直接用target就行。

然后在主类内进行注册指令

1
2
3
4
5
//在启用运行函数内注册方法
@Override
public void onEnable() {
CommandManager.INSTANCE.registerCommand(指令类.INSTANCE, false);
}

注册这步写在onEnable里,插件每次启用都会执行一遍。第二个参数是是否覆盖重名指令,这里传false就行。

插件的编译

插件写完要编译成jar包才能给MCL用,这一步在IDEA里完成。在IDEA右侧的Gradle工具窗口中展开build,找到jar任务,双击即可编译出jar文件,找不到这个窗口的话在View->Tool Windows->Gradle里能调出来。

导出的jar文件在你的项目路径下build/libs/文件名.jar,把它拖进MCL启动器的plugins文件夹就装好了。

plugins目录就是MCL约定放插件的地方,启动时它会把里面的jar一个个读进来,插件多了可以在里面建子文件夹分类,不影响加载。装完重启一次MCL就能生效,控制台日志里能看到插件的加载信息。

常见坑

折腾下来容易出问题的就那几处,先说在前面,能省点时间。

最先要盯的是JDK版本。IDEA里编译用的JDK和MCL运行用的JDK最好保持一致的大版本,最常见的情况就是插件用高版本JDK编译出来,MCL还在用低版本跑,一加载就报类文件版本的错。要么统一JDK,要么在Gradle里把编译目标版本调低。

然后是登录风控。首次登录或者换了设备之后,账号大概率会触发安全验证,终端会给链接或者提示,跟着走完就行;MCL生成的device.json千万别删,删一次就要重新验证一次。

插件不生效的话,先去logs文件夹翻插件加载日志,常见原因无非是jar没放进plugins、插件ID跟别人的冲突,或者Console版本跟插件要求对不上,日志里一般都能看出个大概。

最后,MCL的目录尽量别带中文和空格,省得踩到一些路径解析的小毛病,遇到莫名其妙的找不到文件,先排查这条。

配置MCL

在上述操作完成后,可以打开MCL启动器的目录,找到config/Console/AutoLogin.yml,向里面添加你的机器人QQ,方便之后进行自动登录,第一次登录可能会出现拦截,不过只需在一部手机上打开弹窗的链接,进行安全认证即可。

这个文件是按YAML格式写的账号列表,账号和密码都填进去,MCL启动时会自动读它去登录,省得每次守着命令行手输密码。填的时候注意缩进,YAML对缩进很敏感,错一格就会报解析错误;密码建议用引号包起来,免得特殊字符被当成语法。要是自动登录失败,或者遇到滑块之类的验证,在控制台里手动敲login QQ号 密码也能登录,跟着终端提示把验证做完就行。

权限相关的配置在同目录的Permission.yml里。控制台本身默认拥有全部权限,群成员想用受权限保护的指令,就得在这个文件里给账号或者群授权。平时不限制权限的机器人可以暂时不管它,等真的要管了再回来研究格式。


MCL里其他文件夹的功能就不一个个细说了,常用的无非是plugins放插件、config放配置、data放运行数据、logs放日志,看一眼名字就懂。当你完成上述操作后,回到MCL的根路径下,就可以开启机器人了

(放一张正常运行的图片)

总结

整篇走下来就是三件事:装MCL把运行环境搭起来,用IDEA插件写一个自己的机器人插件,事件监听、发消息、注册指令都过了一遍,最后配置自动登录把机器人跑起来。示例代码都是能直接用的骨架,感兴趣的可以照着往里面加功能。有不会的可以查看大佬的非官方文档,虽然是Kotlin的,不过大体是相似的:

https://mirai.mamoe.net/topic/802

觉得不错就分享出去吧~


Mirai框架开发QQ机器人
https://blog.minloha.cn/posts/2111352eabd5872021111139.html
作者
Minloha
发布于
2021年11月11日
更新于
2021年11月11日
许可协议