详细描述
- 增加像nonebot2那样的命令过滤器,对于一些简单的命令,我觉得写正则太麻烦了。针对于该命令过滤器应支持配置多个命令前缀,以灵活匹配命令,以及增加命令分隔符,使得你可以更方便提取消息
- 增加一个等待用户输入的状态处理器
- 希望大改目前的插件装配方式,现在的插件编写,你需要继承
BotPlugin类,我希望后续能够像nonebot2那样基于注解式开发插件
- 尽管
shiro 并不提供状态管理,但是我觉得可以让插件类继承一个名为PluginMetaInfo的类/接口, 又或是通过一个额外的注解来添加元信息,来暴露你的插件的元信息,比如说作者,使用方式,甚至包括你的插件使用到的配置类
- 插件热重载,这可能需要修改现有的插件管理器类才可以实现
基本示例
1. 命令过滤器
你可以配置命令前缀prefixs和分隔符splits
shiro:
command:
prefixs: ["/", ""]
splits: [",","|"," "]
代码示例
@Component
@Shiro
@Slf4j
public class TestPlugin {
@AnyMessageHandler
@CommandHandlerFilter(types = {MsgTypeEnum.reply, MsgTypeEnum.text}, cmd = {"echo", "测试"}) //会匹配 "/echo","echo",同时提取时可以通过"/echo,测试消息"和"/echo 测试消息"
public void test(Bot bot, AnyMessageEvent messageEvent , CommandArgs args) {
log.info("{}", messageEvent);
String reply = args.get(0);
bot.sendMsg(messageEvent, reply ,false);
}
}
2. 状态处理器 Conversation
使用编程式开发
package com.mikuac.shiro.test;
import com.mikuac.shiro.annotation.AnyMessageHandler;
import com.mikuac.shiro.annotation.MessageHandlerFilter;
import com.mikuac.shiro.annotation.common.Shiro;
import com.mikuac.shiro.core.Bot;
import com.mikuac.shiro.dto.event.message.AnyMessageEvent;
import com.mikuac.shiro.core.Conversation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@Component
@Shiro
@Slf4j
public class WeatherPlugin {
/**
* 天气查询流程
*/
@AnyMessageHandler
@MessageHandlerFilter(cmd = "^/天气$")
public void queryWeather(Bot bot, AnyMessageEvent event, Matcher matcher, Conversation conv) {
// 1. 获取参数 "city"
// 逻辑:如果当前 event 已经包含参数(例如 /天气 北京),则直接获取
// 如果没有,Conversation 会自动发送 prompt 提示,并挂起当前线程,等待用户下一条消息
String city = conv.got("city", "请输入您想查询的城市:");
// 此时代码继续执行,说明已经拿到了 city
// 如果用户发了 "/天气",这里会暂停,bot 发送提示,用户回 "上海",这里 city 就是 "上海"
// 2. 甚至可以连续 got (多轮对话),你还可以设置最大超时时间,如果超时了,则会抛出异常给开发者自己处理
String date = conv.got("date", "请输入查询日期(今天/明天):", TimeUnit.SECONDS, 3);
// 3. 业务逻辑
log.info("正在查询 {} 的 {} 天气...", city, date);
String weatherResult = mockWeatherApi(city, date);
// 4. 发送最终结果
conv.finish(weatherResult); // finish 代表结束当前会话
}
// 模拟 API
private String mockWeatherApi(String city, String date) {
return String.format("【%s】%s的天气是:晴转多云,25℃", city, date);
}
}
使用注解式开发
为了更好使用注解式开发,我们需要让 shiro 框架注入 ConversationContext类来管理状态上下文,以及 @State 来跟踪这些上下文的流向 (注意,@State注解不可用跨类,只能在同个类下与其它状态输入合作)
package com.mikuac.shiro.test;
import com.mikuac.shiro.annotation.AnyMessageHandler;
import com.mikuac.shiro.annotation.MessageHandlerFilter;
import com.mikuac.shiro.annotation.State;
import com.mikuac.shiro.enums.StateEventType;
import com.mikuac.shiro.annotation.common.Shiro;
import com.mikuac.shiro.core.Bot;
import com.mikuac.shiro.core.ConversationContext;
import com.mikuac.shiro.dto.event.message.AnyMessageEvent;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@Component
@Shiro
@Slf4j
public class WeatherPlugin {
// 定义状态常量
private static final String STATE_WAIT_CITY = "WAIT_CITY";
private static final String STATE_WAIT_DATE = "WAIT_DATE";
/**
* 第一步:入口
* 触发命令:/天气
*/
@AnyMessageHandler
@MessageHandlerFilter(cmd = "^/天气$")
public void startWeatherFlow(Bot bot, AnyMessageEvent event, ConversationContext ctx) {
// 1. 发送提示
bot.sendMsg(event, "请输入您想查询的城市:", false);
// 2. 将用户置入 "WAIT_CITY" 状态
// 该方法结束后,框架会记录:UserId=123, State=WAIT_CITY, HandlerClass=WeatherPlugin
ctx.nextState(STATE_WAIT_CITY);
}
/**
* 第二步:处理城市输入
* 只有当用户处于 WAIT_CITY 状态,且消息发送到当前类时,才会触发此方法
*/
@State(value = STATE_WAIT_CITY)
public void handleCityInput(Bot bot, AnyMessageEvent event, ConversationContext ctx) {
String city = event.getMessage(); // 获取用户输入的城市
// 校验逻辑(可选)
if (city.length() > 10) {
bot.sendMsg(event, "城市名称太长啦,请重新输入:", false);
ctx.keepState(); // 保持当前状态,等待下一次输入
return;
}
// 保存数据到上下文,供后续步骤使用
ctx.put("city", city);
bot.sendMsg(event, "收到,请输入查询日期(例如:今天):", false);
// 进入下一个状态
ctx.nextState(STATE_WAIT_DATE);
}
/**
* 第三步:处理日期输入 & 结束
*/
@State(value = STATE_WAIT_DATE)
public void handleDateInput(Bot bot, AnyMessageEvent event, ConversationContext ctx) {
String date = event.getMessage();
// 从上下文中取出上一步存的 city
String city = ctx.get("city", String.class);
log.info("查询天气: city={}, date={}", city, date);
// 模拟业务逻辑
String result = String.format("【%s】%s的天气是:大雨转暴雨 🌧️", city, date);
bot.sendMsg(event, result, false);
// 结束流程,清除状态
ctx.finish();
}
// 如果你不想为整个状态或者单独的状态单独定义一个超时处理器,那么你可以这样
@State(value = STATE_WAIT_DATE, timeout = 30)
public void handleDateInput(Bot bot, AnyMessageEvent event, ConversationContext ctx, StateEventType type) {
// 1. 先判断事件类型
if (type == StateEventType.TIMEOUT) {
bot.sendMsg(event, "等太久啦,天气查询已取消。", false);
ctx.finish(); // 结束状态
return;
}
// 2. 正常处理消息逻辑
String date = event.getMessage();
String city = ctx.get("city", String.class);
bot.sendMsg(event, "查询结果: " + city + date + " 天气不错", false);
ctx.finish();
}
/**
* 可选:处理超时(比如用户 30秒 没理你)
* 这里的 value 可以是一个特殊标记或者通用配置
*/
@State(value = STATE_WAIT_CITY, type = StateEventType.TIMEOUT, timeunit = TimeUnit.SECONDS , timeout = 30)
public void onTimeout(Bot bot, AnyMessageEvent event) {
bot.sendMsg(event, "输入超时,天气查询已取消。", false);
}
}
3. 更好的外部插件开发方式,基于注解
@Component
@Shiro
@Slf4j
public class TestPlugin { // 你不再需要去继承`BotPlugin`类了
@AnyMessageHandler
@MessageHandlerFilter(types = {MsgTypeEnum.reply, MsgTypeEnum.text}, cmd = "^/echo$")
public void test(Bot bot, AnyMessageEvent messageEvent , Matcher matcher) {
log.info("{}", messageEvent);
log.info("rawmsg = {}", messageEvent.getRawMessage());
log.info("msg = {}", messageEvent.getMessage());
}
}
4. 插件元信息,对于一款规范的插件,应该具有插件元信息
/**
* 标注插件元信息
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Shiro
@Component
public @interface Plugin {
/**
* 插件名称
*/
String name();
// 插件描述信息
String description() default "暂无描述";
// 版本
String version() default "1.0.0";
// 作者
String author() default "";
// 用法
String usage() default "";
// 其它信息
Map<String,String> extra() default Map.of();
/**
* 指定该插件对应的配置类
* 必须继承 BasePluginConfig
* 默认为 Void.class 表示该插件没有配置
*/
Class<? extends BasePluginConfig> config() default BasePluginConfig.None.class;
}
/*
* 复读机插件
*/
@Plugin(
name = "复读机",
description = "提供复读跟随功能和群消息复读功能。",
version = "1.0.0",
author = "FlanChan",
usage = "使用\"/开启复读跟随\"开启复读,机器人会复读你的消息"
extra = Map.of("homepage", "https://xxxx"),
config = RepeaterConfig.class // 你可以暴露你的配置类,这使得其它插件或者shiro框架能够获取你的插件配置信息,如果可以的话,甚至可以让shiro框架帮你修改配置信息
)
public class RepeaterPlugin {}
如果该插件需要暴露自己使用的配置类,那么配置类必须去继承配置基类,该配置类还可以基于注解来描述自己的配置类信息
/**
* 插件配置的基类 (规范)
* 所有暴露的插件配置类必须继承此方类。
*/
@Data
public abstract class BasePluginConfig {
public static final class None extends BasePluginConfig {} // 防止没有指定配置类的时候出现空指针
}
/**
* 标记一个类为插件的配置类
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface PluginConfig {
String value() default "";
}
/**
* 标记字段为具体的配置项
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ConfigItem {
String key(); // 相对 Key,如 "enabled" (实际存储为 prefix.key)
String description(); // 描述,比如你可用于 WebUI 显示
String defaultValue(); // 默认值 (String 形式)
}
@Data
@PluginConfig("复读机配置")
public class RepeaterConfig extends BasePluginConfig {
@ConfigItem(key = "limit", defaultValue = "10", description = "连续复读次数限制")
private Integer limit;
}
5. 插件热重载
开启热重载
可以让shiro框架热重载插件
shiro:
plugins:
plugin-dir: "/plugins" # 插件路径
hotreload: true # 默认为false
需要注意的是,尽管有热重载,但是如果插件作者自己本身的代码在释放资源这方面不太行的话,那么就会造成内存泄漏的问题,shiro 框架只能帮你去取消注册spring容器,调用jvm进行gc清除那些内存,如果该插件使用了线程池,有一些正在运行中的线程,热重载开启后,如果插件被卸载了,那么线程不会被清理,这需要开发者自己定义销毁资源的方法
插件卸载时必须要合理释放资源
private ExecutorService threadPool;
private volatile boolean running = true;
@PostConstruct
public void init() {
log.info("插件组件初始化...");
threadPool = Executors.newVirtualThreadPerTaskExecutor();
threadPool.submit(() -> {
while (running) {
try {
Thread.sleep(1000);
log.info("插件后台任务运行中...");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
}
/**
* 当 jar 包被移除 -> context.close() 被调用 -> 此方法自动触发
*/
@PreDestroy
public void cleanup() {
log.info("检测到插件卸载,正在清理资源...");
// 1. 停止标志位
this.running = false;
// 2. 关闭线程池
if (threadPool != null) {
threadPool.shutdownNow();
}
log.info("资源清理完毕!");
}
其它
基于 shiro 框架的注解式开发应该更加易用,以及我有能力贡献这些功能/特性
此外,如果加入了这些新的功能/特性,那么想必也需要在文档中进行说明,可否允许我获得文档编辑的权限,这些功能的使用方式将由我来编写
详细描述
BotPlugin类,我希望后续能够像nonebot2那样基于注解式开发插件shiro并不提供状态管理,但是我觉得可以让插件类继承一个名为PluginMetaInfo的类/接口, 又或是通过一个额外的注解来添加元信息,来暴露你的插件的元信息,比如说作者,使用方式,甚至包括你的插件使用到的配置类基本示例
1. 命令过滤器
你可以配置命令前缀prefixs和分隔符splits
代码示例
2. 状态处理器
Conversation使用编程式开发
使用注解式开发
为了更好使用注解式开发,我们需要让 shiro 框架注入
ConversationContext类来管理状态上下文,以及@State来跟踪这些上下文的流向 (注意,@State注解不可用跨类,只能在同个类下与其它状态输入合作)3. 更好的外部插件开发方式,基于注解
4. 插件元信息,对于一款规范的插件,应该具有插件元信息
如果该插件需要暴露自己使用的配置类,那么配置类必须去继承配置基类,该配置类还可以基于注解来描述自己的配置类信息
5. 插件热重载
开启热重载
可以让shiro框架热重载插件
需要注意的是,尽管有热重载,但是如果插件作者自己本身的代码在释放资源这方面不太行的话,那么就会造成内存泄漏的问题,shiro 框架只能帮你去取消注册spring容器,调用jvm进行gc清除那些内存,如果该插件使用了线程池,有一些正在运行中的线程,热重载开启后,如果插件被卸载了,那么线程不会被清理,这需要开发者自己定义销毁资源的方法
插件卸载时必须要合理释放资源
其它
基于 shiro 框架的注解式开发应该更加易用,以及我有能力贡献这些功能/特性
此外,如果加入了这些新的功能/特性,那么想必也需要在文档中进行说明,可否允许我获得文档编辑的权限,这些功能的使用方式将由我来编写