Java开发聊天机器人
# Java开发聊天机器人
Shiro是使用JAVA开发的基于 OneBo-V11 (opens new window) 协议的 QQ机器人 快速开发框架
文档:快速开始 | Shiro SDK Document (opens new window)
示例Demo:https://gitee.com/hidewnd/shiro-demo.git (opens new window)
- SpringBoot3.0.0+
- jdk17+
机器人框架基于springboot开发,可嵌入现有springboot框架项目中或作为独立子服务运行,目前框架支持go-cphttp协议的绝大部分oneBot事件或请求,支持自行扩展事件及请求
其他语言框架:
- Python:NoneBot (opens new window)
- TypeScript:Koishi (opens new window)
客户端配置反向连接:ws://127.0.0.1:5555/ws/shiro
非机器人端连接需配置header:x-self-id={机器人账号}
实现效果:
# 相关依赖
Maven依赖
maven依赖已包含springboot
<dependency>
<groupId>com.mikuac</groupId>
<artifactId>shiro</artifactId>
<version>latest</version>
</dependency>
1
2
3
4
5
2
3
4
5
Gradle Kotlin DSL
implementation("com.mikuac:shiro:latest")
1
Gradle Groovy DSL
implementation 'com.mikuac:shiro:latest'
1
# 配置文件
server:
port: 5555
shiro:
interceptor: com.hidewnd.shiro.plugins.InterceptorExample
# 注解方式编写的插件无需在插件列表(plugin-list)定义
# 插件列表为顺序执行,如果前一个插件返回了 MESSAGE_BLOCK 将不会执行后续插件
plugin-list:
- com.hidewnd.shiro.plugins.OverridePlugin
ws:
# 访问密钥,强烈推荐在公网的服务器设置
access-token: ""
# 超时回收,默认10秒
timeout: 10
# 最大文本消息缓冲区
max-text-message-buffer-size: 512000
# 二进制消息的最大长度
max-binary-message-buffer-size: 512000
# 反向 Websocket 连接地址
server:
enable: true
# 默认值 "/ws/shiro"
url: "/ws/shiro"
# 最大空闲时间,超过这个时间将关闭会话
max-session-idle-timeout: 900000
# 限速器(令牌桶算法)
limiter:
# 是否启用限速器
enable: false
# 补充速率(每秒补充的令牌数量)
rate: 1
# 令牌桶容量
capacity: 1
# 如果该值为 false 时,当令牌获取失败则会直接丢次本次请求
# 如果该值为 true 时,当令牌获取失败则会阻塞当前线程,后续任务将被添加到等待队列
awaitTask: true
# 等待超时
timeout: 10
# 线程池配置
task-pool:
# 核心线程数(默认线程数)
core-pool-size: 10
# 缓冲队列大小
queue-capacity: 200
# 允许线程空闲时间(单位:默认为秒)
keep-alive-time: 10
# 最大线程数
max-pool-size: 30
# 线程池名前缀
thread-name-prefix: "ShiroTaskPool-"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# 具体实现
在机器人项目框架中,通常以插件
的概念作为机器人的功能体现,一个机器人可拥有多个插件,进而获得多种功能;
插件概念类似于MVC架构中的Controller层,作为对外提供的接口,插件中声明的不同命令即代表可用的接口;
通常一个插件下的命令代表同一主题功能的不同命令,可通过项目构造香一个大型功能拆分为多个子插件开发,便于项目维护;
# 编写插件-重写父类方式
- 继承父类
BotPlugin
- 重写方法:
onGroupMessage
、onPrivateMessage
、onAnyMessage
...... - 配置文件
shiro.plugin-list
中声明重写类
需注意的点:
- 重写方法实现的插件根据配置文件上下声明顺序依次执行
- 重写方法,如果前一个插件返回了
MESSAGE_BLOCK
将不会执行后续插件
代码示例:
package com.hidewnd.shiro.plugins;
import ch.qos.logback.core.util.StringUtil;
import com.mikuac.shiro.annotation.common.Order;
import com.mikuac.shiro.common.utils.MsgUtils;
import com.mikuac.shiro.core.Bot;
import com.mikuac.shiro.core.BotPlugin;
import com.mikuac.shiro.dto.action.common.ActionData;
import com.mikuac.shiro.dto.action.common.MsgId;
import com.mikuac.shiro.dto.event.message.GroupMessageEvent;
import com.mikuac.shiro.dto.event.message.PrivateMessageEvent;
import org.springframework.stereotype.Component;
/**
* 目前发现晚于注解方式编写的插件
*/
@Component
public class OverridePlugin extends BotPlugin {
@Order(1)
@Override
public int onPrivateMessage(Bot bot, PrivateMessageEvent event) {
String message = StringUtil.nullStringToEmpty(event.getMessage());
bot.sendPrivateMsg(event.getUserId(), message, false);
// 返回 MESSAGE_IGNORE 执行 plugin-list 下一个插件,返回 MESSAGE_BLOCK 则不执行下一个插件
return MESSAGE_IGNORE;
}
@Order(2)
@Override
public int onGroupMessage(Bot bot, GroupMessageEvent event) {
String message = StringUtil.nullStringToEmpty(event.getMessage());
bot.sendGroupMsg(event.getGroupId(), message, false);
// 返回 MESSAGE_IGNORE 执行 plugin-list 下一个插件,返回 MESSAGE_BLOCK 则不执行下一个插件
return MESSAGE_IGNORE;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 编写插件-注解声明方式
- 一个类代表一个插件,在类上添加注解
@Shiro
、@Component
的方式由spring接管实例,并识别成一个插件类 - 插件类中的方法通过方法注解
@PrivateMessageHandler
、@GroupMessageHandler
、AnyMessageHandler
声明对不同的消息事件进行处理,常用入参包括:Bot
、PrivateMessageEvent
、GroupMessageEvent
、Matcher
......,对于不同类型的事件,可用的入参有所不同。 - 命令支持全匹配、前后匹配、正则匹配,正则匹配可通过
Matcher
获取匹配后的参数信息
代码示例:
package com.hidewnd.shiro.plugins;
import cn.hutool.core.util.StrUtil;
import com.mikuac.shiro.annotation.AnyMessageHandler;
import com.mikuac.shiro.annotation.GroupMessageHandler;
import com.mikuac.shiro.annotation.MessageHandlerFilter;
import com.mikuac.shiro.annotation.PrivateMessageHandler;
import com.mikuac.shiro.annotation.common.Shiro;
import com.mikuac.shiro.common.utils.MsgUtils;
import com.mikuac.shiro.core.Bot;
import com.mikuac.shiro.dto.event.message.AnyMessageEvent;
import com.mikuac.shiro.dto.event.message.GroupMessageEvent;
import com.mikuac.shiro.dto.event.message.PrivateMessageEvent;
import com.mikuac.shiro.enums.AtEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.regex.Matcher;
@Slf4j
@Shiro
@Component
public class ExamplePlugin {
// 当机器人收到的私聊消息消息符合 cmd 值 "hi" 时,这个方法会被调用。
@PrivateMessageHandler
@MessageHandlerFilter(cmd = "hi")
public void fun1(Bot bot, PrivateMessageEvent event, Matcher matcher) {
// 构建消息
String sendMsg = MsgUtils.builder().face(66).text("this is privateMessage callBack").build();
// 发送私聊消息
bot.sendPrivateMsg(event.getUserId(), sendMsg, false);
}
// 如果 at 参数设定为 AtEnum.NEED 则只有 at 了机器人的消息会被响应
@GroupMessageHandler
@MessageHandlerFilter(at = AtEnum.NEED)
public void fun2(GroupMessageEvent event) {
// 以注解方式调用可以根据自己的需要来为方法设定参数
// 例如群组消息可以传递 GroupMessageEvent, Bot, Matcher 多余的参数会被设定为 null
log.info("群组消息: {}", event.getMessage());
}
@GroupMessageHandler
@MessageHandlerFilter(startWith = "测试")
public void fun3(Bot bot, GroupMessageEvent event, Matcher matcher) {
String message = event.getMessage();
String group = matcher.group();
String text = StrUtil.format("收到群组消息: {},匹配到的内容: {}", message, group);
bot.sendGroupMsg(event.getGroupId(), text, false);
}
// 同时监听群组及私聊消息 并根据消息类型(私聊,群聊)回复
@AnyMessageHandler
@MessageHandlerFilter(cmd = "hi")
public void fun3(Bot bot, AnyMessageEvent event) {
bot.sendMsg(event, "this is GroupMessage callBack", false);
}
@GroupMessageHandler
@MessageHandlerFilter(cmd = "^匹配 (.*)?$")
public void fun4(Bot bot, GroupMessageEvent event, Matcher matcher) {
int index = 1;
String message = matcher.group(0);
String group = matcher.group(index);
while (StrUtil.isNotEmpty(group)) {
String text = StrUtil.format("收到群组消息: {},匹配到的内容: {}", message , group);
bot.sendGroupMsg(event.getGroupId(), text, false);
group = matcher.groupCount() < index ? matcher.group(++index) : null;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# 机器人上下线事件处理
继承父类CoreEvent
,并重写相关方法,可用于对ws上下线事件的处理及客户端黑名单处理
代码示例:
import ch.qos.logback.core.util.StringUtil;
import com.mikuac.shiro.core.Bot;
import com.mikuac.shiro.core.CoreEvent;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketSession;
@Slf4j
@Primary
@Component
public class BotRuntimeEvent extends CoreEvent {
@Override
public void online(Bot bot) {
// 客户端上线事件
// 例如上线后发送消息给指定的群或好友
// 如需获取上线的机器人账号可以调用 bot.getSelfId()
log.info("机器人({}) 上线了", bot.getSelfId());
}
@Override
public void offline(long account) {
// 客户端离线事件
log.info("机器人({}) 下线了", account);
}
@Override
public boolean session(WebSocketSession session) {
// 可以通过 session.getHandshakeHeaders().getFirst("x-self-id") 获取上线的机器人账号
// 例如当服务端为开放服务时,并且只有白名单内的账号才允许连接,此时可以检查账号是否存在于白名内
// 不存在的话返回 false 即可禁止连接
String botId = session.getHandshakeHeaders().getFirst("x-self-id");
if (StringUtil.isNullOrEmpty(botId)) {
log.warn("未知的连接请求");
}
log.info("机器人({})请求连接", botId);
return true;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# WS拦截器处理
在事件开始响应前后进行预处理操作,可用于对敏感、违规参数预处理、功能命令权限限制等操作
具体步骤:
- 实现接口
BotMessageEventInterceptor
,并实现方法:preHandle()
、afterCompletion()
- 配置文件声明类:
shiro.interceptor= com.hidewnd.shiro.plugins.InterceptorExample
代码示例:
import com.mikuac.shiro.common.utils.MsgUtils;
import com.mikuac.shiro.core.Bot;
import com.mikuac.shiro.core.BotMessageEventInterceptor;
import com.mikuac.shiro.dto.event.message.GroupMessageEvent;
import com.mikuac.shiro.dto.event.message.MessageEvent;
import com.mikuac.shiro.exception.ShiroException;
import org.springframework.stereotype.Component;
/**
* 拦截器
*/
@Component
public class InterceptorExample implements BotMessageEventInterceptor {
@Override
public boolean preHandle(Bot bot, MessageEvent event) throws ShiroException {
// 预处理方法,可以在触发事件前做一些处理,返回值为 false 时本次事件将会被拦截
// 使用场景可以是黑名单检查或检查权限等等,具体的使用场景可以发挥自己的想象力
// MessageEvent 可能为右边这三种类型 PrivateMessageEvent、GroupMessageEvent、GuildMessageEvent
String message = event.getMessage();
String messageType = event.getMessageType();
if (message.contains("敏感")) {
String sendMsg = MsgUtils.builder()
.face(37)
.text("检测敏感数据,中止执行")
.build();
if ("group".equals(messageType)) {
bot.sendGroupMsg(((GroupMessageEvent) event).getGroupId(), sendMsg, false);
}
return false;
}
return true;
}
@Override
public void afterCompletion(Bot bot, MessageEvent event) throws ShiroException {
// 后置处理方法
// 当 preHandle 返回值为 true 时则会执行 plugin-list,执行完毕后进入到 afterCompletion
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
帮助我改善此页面 (opens new window)
上次更新: 2025/01/22, 18:58:53