├── resouce.md ├── .gitignore ├── service.md ├── trigger-demo ├── mulmatch.py ├── fullmatch.py ├── priority_0.py ├── priority_1.py ├── suffix.py ├── prefix.py └── keyword.py ├── scheduler.md ├── README.md └── trigger.md /resouce.md: -------------------------------------------------------------------------------- 1 | # 资源文件 2 | 3 | ## 图片资源 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | .vscode/ 3 | -------------------------------------------------------------------------------- /service.md: -------------------------------------------------------------------------------- 1 | # 插件即服务 2 | 3 | ## 配置文件 4 | 5 | ## 分群管理 6 | 7 | ## 权限管理 8 | -------------------------------------------------------------------------------- /trigger-demo/mulmatch.py: -------------------------------------------------------------------------------- 1 | from hoshino import Service 2 | from hoshino.typing import CQEvent 3 | 4 | sv = Service('mulmatch-demo') 5 | 6 | @sv.on_prefix('之后') 7 | @sv.on_suffix('之前') 8 | async def demo_fun(bot, ev:CQEvent): 9 | msg = ev.message.extract_plain_text() 10 | await bot.send(ev, f'提取到了的文本内容是:\n{msg}') 11 | -------------------------------------------------------------------------------- /trigger-demo/fullmatch.py: -------------------------------------------------------------------------------- 1 | from hoshino import Service 2 | from hoshino.typing import CQEvent 3 | 4 | sv = Service('fullmatch-demo') 5 | 6 | @sv.on_fullmatch('你好') 7 | async def demo_fun_0(bot, ev:CQEvent): 8 | await bot.send(ev, '你好!') 9 | 10 | @sv.on_fullmatch(('老婆', '老公'), only_to_me=True) 11 | async def demo_fun_1(bot, ev): 12 | await bot.send(ev, '???') 13 | -------------------------------------------------------------------------------- /trigger-demo/priority_0.py: -------------------------------------------------------------------------------- 1 | from hoshino import Service 2 | from hoshino.typing import CQEvent 3 | 4 | sv = Service('priority-demo-0') 5 | 6 | @sv.on_prefix('优先级0') 7 | async def demo_fun_0(bot, ev:CQEvent): 8 | await bot.send(ev, '触发了on_prefixh呦~~~') 9 | 10 | @sv.on_fullmatch('优先级0测试') 11 | async def demo_fun_1(bot, ev:CQEvent): 12 | await bot.send(ev, '触发了on_fullmatch呦~~~') 13 | -------------------------------------------------------------------------------- /trigger-demo/priority_1.py: -------------------------------------------------------------------------------- 1 | from hoshino import Service 2 | from hoshino.typing import CQEvent 3 | 4 | sv = Service('priority-demo-1') 5 | 6 | @sv.on_keyword('优先级1') 7 | async def demo_fun_0(bot, ev:CQEvent): 8 | await bot.send(ev, '触发了on_keyword呦~~~') 9 | 10 | 11 | @sv.on_fullmatch('优先级1') 12 | async def demo_fun_1(bot, ev:CQEvent): 13 | await bot.send(ev, '触发了on_fullmatch呦~~~') 14 | -------------------------------------------------------------------------------- /trigger-demo/suffix.py: -------------------------------------------------------------------------------- 1 | from hoshino import Service 2 | from hoshino.typing import CQEvent 3 | 4 | sv = Service('suffix-demo') 5 | 6 | @sv.on_suffix('前面') 7 | async def demo_fun_0(bot, ev:CQEvent): 8 | msg = ev.message.extract_plain_text() 9 | await bot.send(ev, f'suffix提取到了的文本内容是:\n{msg}') 10 | 11 | @sv.on_suffix(('之前', '以前'), only_to_me=True) 12 | async def demo_fun_1(bot, ev:CQEvent): 13 | msg = ev.message.extract_plain_text() 14 | await bot.send(ev, f'suffix提取到了的文本内容是:\n{msg}') 15 | -------------------------------------------------------------------------------- /trigger-demo/prefix.py: -------------------------------------------------------------------------------- 1 | from hoshino import Service 2 | from hoshino.typing import CQEvent 3 | 4 | sv = Service('prefix-demo') 5 | 6 | @sv.on_prefix('内容提取') 7 | async def demo_fun_0(bot, ev:CQEvent): 8 | msg = ev.message.extract_plain_text() 9 | await bot.send(ev, f'prefix提取到了的文本内容是:\n{msg}') 10 | 11 | @sv.on_prefix(['提取', '试试提取'], only_to_me=True) 12 | async def demo_fun_1(bot, ev:CQEvent): 13 | msg = ev.message.extract_plain_text() 14 | await bot.send(ev, f'prefix提取到了的文本内容是:\n{msg}') 15 | -------------------------------------------------------------------------------- /trigger-demo/keyword.py: -------------------------------------------------------------------------------- 1 | from hoshino import Service 2 | from hoshino.typing import CQEvent 3 | 4 | sv = Service('keyword-demo') 5 | 6 | @sv.on_keyword('色图') 7 | async def demo_fun_0(bot, ev:CQEvent): 8 | await bot.send(ev, '发点色图。--鲁迅') 9 | 10 | @sv.on_keyword(('憨批', '憨憨'), only_to_me=True) 11 | async def demo_fun_1(bot, ev:CQEvent): 12 | await bot.send(ev, '别骂辣别骂辣') 13 | 14 | @sv.on_keyword('鏡華', normalize=False) 15 | async def demo_fun_2(bot, ev:CQEvent): 16 | # 仅当繁体或日文汉字时触发 17 | await bot.send(ev, '叫我?') 18 | 19 | @sv.on_keyword('小倉唯') 20 | async def demo_fun_3(bot, ev:CQEvent): 21 | # 日本汉字、简体字、繁体字均触发 22 | await bot.send(ev, '叫我干嘛~') 23 | -------------------------------------------------------------------------------- /scheduler.md: -------------------------------------------------------------------------------- 1 | # 定时任务 2 | 3 | 这是一个简易的演示,其使用可以有多种扩展。 4 | 当然如果你已经熟练掌握了APScheduler那么你并不需要这个文档 5 | 6 | ## 定时发送 7 | ``` 8 | sv = Service('hello') 9 | 10 | @sv.scheduled_job('cron', minute='*/1') 11 | async def hello(): 12 | await sv.broadcast('hello,world','hello') 13 | broadcast需要的参数是 消息'hello,world' 可选参数是 标签'hello'、发送间隔 不小于等于0的任意数,单位是秒 14 | ``` 15 | 这样一个简单的定时发送任务便完成了,在启动你的机器人之后将会每分钟都发送一次hello,world。 16 | 更加详细的定时任务可以了解APScheduler。 17 | - [中文](https://www.jianshu.com/p/4f5305e220f0) 翻译文档 18 | - [官方](https://apscheduler.readthedocs.io/en/latest/) 文档 19 | 20 | ## 进阶 21 | ``` 22 | def hello(): 23 | print('hello,world') 24 | 25 | @sv.scheduled_job('cron', hour='12') 26 | async def test(): 27 | hello() 28 | ``` 29 | 30 | 这样可以用于执行hoshino外部的任务也可以用于执行内部任务,任凭发挥。 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HoshinoBot(v2) 插件开发指南(社区版) 2 | 3 | [HoshinoBot](https://github.com/Ice-Cirno/HoshinoBot) 是一款 QQ 群应答机器人框架,在 [nonebot](https://github.com/richardchien/nonebot) 框架的基础上进一步提供了更优秀的封装,大大方便了开发者和使用者。 4 | 5 | 本文档为社区文档,是对官方文档的做出一些更方便初学者阅读的说明。 6 | 7 | ## 部署 HoshinoBot 8 | 9 | 请参考[HoshinoBot 部署方法(社区版)](https://github.com/pcrbot/HoshinoBot-gacha/blob/master/README.md) 10 | 11 | ## 快速上手 12 | 13 | HoshinoBot 部署完成后,我们可以在 `hoshino/modules` 目录下创建一个新的文件夹,作为功能模块 14 | 15 | ```shell 16 | cd hoshino/modules 17 | mkdir my_module 18 | ``` 19 | 20 | 在这个模块下,创建一个新的插件文件 21 | 22 | ```shell 23 | cd my_module 24 | vi hello.py 25 | ``` 26 | 27 | 编写一点内容 28 | 29 | ```python 30 | from hoshino import Service 31 | 32 | sv = Service('hello') 33 | 34 | @sv.on_fullmatch('你好') 35 | async def hello(bot, ev): 36 | await bot.send(ev, '你好,世界!') 37 | ``` 38 | 39 | 然后编辑 `HoshinoBot/config/__bot__.py` 来启用这个新的模块 40 | 41 | ```python 42 | # 前略 43 | MODULES_ON = { 44 | # 前略 45 | 'my_module', # 添加新的功能模块名称到这里 46 | } 47 | ``` 48 | 49 | 现在,重新启动 hoshino,对机器人说一声 `你好` 吧! 50 | 51 | ## 插件结构介绍 52 | 53 | HoshinoBot 内文件分布如下: 54 | 55 | `/hoshino/modules` 模块目录,每个文件夹是一组插件构成的模块 56 | `/hoshino/config` 配置目录,每个`.py`文件是同名模块的配置项 57 | `/hoshino/modules/` 模块内容,每个文件或目录是一个 python 模块,`*.py`文件和目录下的`__init__.py`文件会被加载为模块插件 58 | 59 | ## 高级功能 60 | 61 | HoshinoBot 封装了很多高级用法,使得编写插件非常简单 62 | 63 | [服务层](./service.md):分群、分权限的管理插件 64 | 65 | [触发器](./trigger.md):多种消息触发方式 66 | 67 | [计划任务](./scheduler.md):定时执行任务,在运行时动态调整任务,持久化储存任务 68 | 69 | [资源管理](./resource.md):轻松管理图片、语言资源,简单地合成图片并发送 70 | 71 | ## 学习资料 72 | 73 | ### Python 74 | 75 | 为了编写 Hoshino 插件,请学习 python 至**协程**部分 76 | 77 | - [Python 官方教程](https://docs.python.org/zh-cn/3/tutorial/index.html) 78 | - [廖雪峰的 Python 教程](https://www.liaoxuefeng.com/wiki/1016959663602400) 79 | 80 | ### Git 81 | 82 | 为了向 [HoshinoBot 项目](https://github.com/Ice-Cirno/HoshinoBot)提交代码,请学习使用 Git 管理项目源码 83 | 84 | - [Git 互动教程](https://oschina.gitee.io/learn-git-branching/) 85 | - [廖雪峰的 Git 教程](https://www.liaoxuefeng.com/wiki/896043488029600) 86 | -------------------------------------------------------------------------------- /trigger.md: -------------------------------------------------------------------------------- 1 | # 消息触发器 2 | 3 | 本文介绍的是封装在`hoshino.Service`中的触发器类型,如果您要查看nonebot原生的触发器,可以前往nonebot的[官方文档](https://docs.nonebot.dev/)进行查阅,本文中可能会给出一些简单的介绍,但不会作出详细说明。HoshinoBot 的触发器是通用的程序入口,通过内部维护的前缀树实现高效率的消息预处理。 4 | 5 | 本教程附带示范代码,如果想要尝试,请将本项目中的`trigger-demo`目录拷贝至到`hoshino/modules/`目录下,并在`hoshino/config/__bot__.py`的`MODULES_ON`中仿照格式添加`'trigger-demo'`。 6 | 7 | ## 触发流程 8 | 9 | HoshinoBot对消息的触发方法在文件`msghandler.py`中使用nonebot自带的装饰器`@message_preprocessor`来预处理,当接收到消息后,会按照顺序进行如下的步骤: 10 | 11 | 1. 检查是否是群聊消息,如果否则过滤 12 | 2. 检查是否是指令,如果否则过滤,如果是则输出日志显示指令已被触发 13 | 3. 检查消息接受条件是否符合,即是否是仅对自己发送 14 | 4. 检查权限是否允许 15 | 5. 调用对应方法 16 | 17 | 因此,当您在控制台看到日志显示指令被触发,而实际并未响应时,应当依照此顺序检查错误的环节。 18 | 19 | ## 优先级 20 | 21 | **注意本条目内容由测试而来,优先级排序实际上只是优先级数值只用于相互对比(越大优先级越高),并无可量化的含义。** 22 | 23 | **本条目仅用来排查错误,请勿利用本条目特性来开发**。 24 | 25 | 使用已`on_*`开头的装饰器来添加命令,如果有一条消息有多个匹配指令,会按照优先级来触发。同一优先级会先按照命令匹配程度来触发,如果命令匹配程度一致,则按照命令注册的先后顺序(即在代码中的先后顺序)触发。 26 | 27 | 相同优先级的示例代码——`priority_0.py` 28 | 29 | ```python 30 | from hoshino import Service 31 | from hoshino.typing import CQEvent 32 | 33 | sv = Service('priority-demo-0') 34 | 35 | @sv.on_prefix('优先级0') 36 | async def demo_fun_0(bot, ev:CQEvent): 37 | await bot.send(ev, '触发了on_prefix呦~~~') 38 | 39 | @sv.on_fullmatch('优先级0测试') 40 | async def demo_fun_1(bot, ev:CQEvent): 41 | await bot.send(ev, '触发了on_fullmatch呦~~~') 42 | 43 | ``` 44 | 45 | 当发送指令【优先级测试】时,由于触发器`on_fullmatch`和`on_prefix`优先级相同,但是`on_fullmatch`匹配度更高,因此会优先触发`on_fullmatch`,即便调换两个函数的顺序,也会触发`on_fullmatch`。 46 | 47 | 如果将全字匹配的条件改为“优先级”三个字,则会按照导入的先后顺序触发。 48 | 49 | 50 | 51 | 不同优先级的示例代码——`priority_1.py` 52 | 53 | ```python 54 | from hoshino import Service 55 | from hoshino.typing import CQEvent 56 | 57 | sv = Service('priority-demo-1') 58 | 59 | @sv.on_keyword('优先级1') 60 | async def demo_fun_0(bot, ev:CQEvent): 61 | await bot.send(ev, '触发了on_keyword呦~~~') 62 | 63 | 64 | @sv.on_fullmatch('优先级1') 65 | async def demo_fun_1(bot, ev:CQEvent): 66 | await bot.send(ev, '触发了on_fullmatch呦~~~') 67 | 68 | ``` 69 | 70 | 由于`on_fullmatch`的优先级高于`on_keyword`,因此虽然关键词触发的方法更靠前,但是此时只会触发`on_fullmatch`。 71 | 72 | 推荐您在选择触发器时,先选择低优先级的触发器来完成(如果可以满足需求),以免与其他插件的指令产生冲突,当您确定要使用`on_prefix`触发器时,您的触发关键词应当注意规避现有插件的指令、群聊经常出现的词、其他插件可能会用到的词。 73 | 74 | ## 触发器与函数方法 75 | 76 | *消息触发器*封装于[服务层](./service.md)中,使用服务提供的`on_*`装饰器作为入口,后跟方法,一个触发器只能对应一个函数方法,但是一个函数方法可以有多个触发器作为入口。 77 | 78 | 多匹配的实例代码——`mulmatch.py` 79 | 80 | ```python 81 | from hoshino import Service 82 | from hoshino.typing import CQEvent 83 | 84 | sv = Service('mulmatch-demo') 85 | 86 | @sv.on_prefix('之后') 87 | @sv.on_suffix('之前') 88 | async def demo_fun_0(bot, ev:CQEvent): 89 | msg = ev.message.extract_plain_text() 90 | await bot.send(ev, f'提取到了的文本内容是:\n{msg}') 91 | 92 | ``` 93 | 在群聊中发送”XXX之前“或”之后XXX“,均会触发相关函数。此种情况仍受到优先级约束。 94 | 95 | 96 | ## HoshinoBot触发器种类 97 | 98 | 以下触发器均为`Service`类中封装的方法。由于原生HoshinoBot 过滤了群聊以外的消息,因此本页所示触发器均为群聊消息有效,如果需私聊指令,可以考虑: 99 | 100 | 1. 使用nonebot原生触发器`on_command` 101 | 2. 修改`msghandler.py`,使得其不过滤非群聊消息 102 | 103 | ### 全字匹配 104 | 105 | 装饰器:`on_fullmatch` 106 | 107 | 优先级:999 108 | 109 | 触发条件:当接收到的消息与触发词完全相同时触发。 110 | 111 | 原型: 112 | 113 | ```python 114 | def on_fullmatch(self, word, only_to_me=False) -> Callable: 115 | #... 116 | ``` 117 | 118 | 参数与缺省值: 119 | 120 | 1. `word`,全字匹配的条件,可以使用元组或列表配置多个(使用列表时最终也会转换为元组)。 121 | 2. `only_to_me`,指令是否需要@机器人,缺省值为False,即不@也可触发。 122 | 123 | 用法示例——`fullmatch.py` 124 | 125 | ```python 126 | from hoshino import Service 127 | from hoshino.typing import CQEvent 128 | 129 | sv = Service('fullmatch-demo') 130 | 131 | @sv.on_fullmatch('你好') 132 | async def demo_fun_0(bot, ev:CQEvent): 133 | await bot.send(ev, '你好!') 134 | 135 | @sv.on_fullmatch(('老婆', '老公'), only_to_me=True) 136 | async def demo_fun_1(bot, ev): 137 | await bot.send(ev, '???') 138 | 139 | ``` 140 | 141 | 142 | ### 前缀触发器 143 | 144 | 装饰器:`on_prefix` 145 | 146 | 优先级:999 147 | 148 | 触发条件:当接收到的消息以触发词开头时触发。当使用`CQEvent.message.extract_plain_text()`提取消息内容时将不会包含前缀关键词。 149 | 150 | 原型: 151 | 152 | ```python 153 | def on_prefix(self, prefix, only_to_me=False) -> Callable: 154 | # ... 155 | ``` 156 | 157 | 参数与缺省值: 158 | 159 | 1. `prefix`,前缀触发的关键词,可以使用元组或列表配置多个(使用列表时最终也会转换为元组)。 160 | 2. `only_to_me`,指令是否需要@机器人,缺省值为False,即不@也可触发。 161 | 162 | 用法示例——`prefix.py` 163 | 164 | ```python 165 | from hoshino import Service 166 | from hoshino.typing import CQEvent 167 | 168 | sv = Service('prefix-demo') 169 | 170 | @sv.on_prefix('内容提取') 171 | async def demo_fun_0(bot, ev:CQEvent): 172 | msg = ev.message.extract_plain_text() 173 | await bot.send(ev, f'prefix提取到了的文本内容是:\n{msg}') 174 | 175 | @sv.on_prefix(['提取', '试试提取'], only_to_me=True) 176 | async def demo_fun_1(bot, ev:CQEvent): 177 | msg = ev.message.extract_plain_text() 178 | await bot.send(ev, f'prefix提取到了的文本内容是:\n{msg}') 179 | 180 | ``` 181 | 182 | ### 后缀触发器 183 | 184 | 装饰器:`on_suffix` 185 | 186 | 优先级:22 187 | 188 | 触发条件:于`on_prefix`类似,当接收到的消息以触发词结尾时触发。使用`CQEvent.message.extract_plain_text()`提取消息内容时将不会包含后缀关键词。 189 | 190 | 原型: 191 | 192 | ```python 193 | def on_suffix(self, suffix, only_to_me=False) -> Callable: 194 | # ... 195 | ``` 196 | 197 | 参数与缺省值: 198 | 199 | 1. `suffix`,后缀触发的关键词,可以使用元组或列表配置多个(使用列表时最终也会转换为元组)。 200 | 2. `only_to_me`,指令是否需要@机器人,缺省值为False,即不@也可触发。 201 | 202 | 示例代码——`suffix.py` 203 | 204 | ```python 205 | from hoshino import Service 206 | from hoshino.typing import CQEvent 207 | 208 | sv = Service('suffix-demo') 209 | 210 | @sv.on_suffix('前面') 211 | async def demo_fun_0(bot, ev:CQEvent): 212 | msg = ev.message.extract_plain_text() 213 | await bot.send(ev, f'suffix提取到了的文本内容是:\n{msg}') 214 | 215 | @sv.on_suffix(('之前', '以前'), only_to_me=True) 216 | async def demo_fun_1(bot, ev:CQEvent): 217 | msg = ev.message.extract_plain_text() 218 | await bot.send(ev, f'suffix提取到了的文本内容是:\n{msg}') 219 | 220 | ``` 221 | 222 | ### 关键词触发器 223 | 224 | 装饰器:`on_keyword` 225 | 226 | 优先级:21 227 | 228 | 触发条件:当接受到的消息包含该命令时,即触发。 229 | 230 | 原型: 231 | 232 | ```python 233 | def on_keyword(self, keywords, only_to_me=False, normalize=True) -> Callable: 234 | # ... 235 | ``` 236 | 237 | 参数与缺省值: 238 | 239 | 1. `keywords`,触发的关键词,可以元组配置多个 240 | 241 | 2. `only_to_me`,指令是否需要@机器人,缺省值为False,即不@也可触发。 242 | 243 | 3. `normalize`,是否进行归一化处理,缺省值为True。如果处理,则会规范化Unicode字符、字母转换为小写、繁体转换为简体,详细方法可查看`trigger.py`中的`normalize_str()`函数。**仅此触发器包含normalize,HoshinoBot于2020年8月10日之后已支持自动[繁简转换](https://github.com/Ice-Cirno/HoshinoBot/commit/d91756408762f971b7f0491ffa06f78b7450cbc4),其他触发器添加的简体指令可以直接用繁体触发** 244 | 245 | 用法示例——`keyword.py` 246 | 247 | ```python 248 | from hoshino import Service 249 | from hoshino.typing import CQEvent 250 | 251 | sv = Service('keyword-demo') 252 | 253 | @sv.on_keyword('色图') 254 | async def demo_fun_0(bot, ev:CQEvent): 255 | await bot.send(ev, '发点色图。--鲁迅') 256 | 257 | @sv.on_keyword(('憨批', '憨憨'), only_to_me=True) 258 | async def demo_fun_1(bot, ev:CQEvent): 259 | await bot.send(ev, '别骂辣别骂辣') 260 | 261 | @sv.on_keyword('鏡華', normalize=False) 262 | async def demo_fun_2(bot, ev:CQEvent): 263 | # 仅当繁体或日文汉字时触发 264 | await bot.send(ev, '叫我?') 265 | 266 | @sv.on_keyword('小倉唯') 267 | async def demo_fun_3(bot, ev:CQEvent): 268 | # 日本汉字、简体字、繁体字均触发 269 | await bot.send(ev, '叫我干嘛~') 270 | 271 | ``` 272 | 273 | ### 正则表达式触发器 274 | 275 | `on_rex` 276 | 277 | 效率极低不推荐使用,当接收到的消息符合正则表达式时触发。 278 | 279 | ### 其他 nonebot 原生触发器 280 | 281 | `on_message` 捕获所有消息,不推荐使用; 282 | `on_command` 以第一个单词作为触发词,不符合中文习惯,不推荐使用; 283 | `on_natural_language` 效率极低不推荐使用。 284 | 285 | 具体使用方法与 nonebot 相同,此处不再赘述。 286 | 287 | ## 触发器参数 288 | 289 | `only_to_me` 参数类型 `bool`,只有当 bot 被使用 `@` 提及时才会触发,默认值 `False` 290 | 291 | ## 消息处理 292 | 293 | 具体使用方法与 nonebot 相同。 294 | 295 | --------------------------------------------------------------------------------