
Moonlark(Nonebot2+Python)命令式聊天机器人插件开发记录
往事
一直沉迷在lingmo-webbrowser的开发中无法自拔。
但是,由于Qt WebEngine 一个致命的问题——如果想要支持html5视频元素,就得重新编译qt
这个问题很难解决
后来,由于种种原因,webbrowser最终archive掉了,于是我有了时间去看别的。
在接触IT Craft Development Team)后,我开始想去给由Python+Nonebot2写出来的Moonlark做一些贡献。
参考的资料
搭建开发环境
(基于moonlark文档修改)
- 确保您的 Python 版本
>= 3.11
。 - 安装包管理工具 Poetry(建议安装
2.1.0
及以上的版本)。 - 克隆仓库并安装依赖,可以使用以下指令:
1 | git clone https://github.com/Moonlark-Dev/Moonlark |
配置
将.env.template
复制为 .env
并在 .env
文件中填写相关环境变量。
(不测试对应的功能不用填)
搭建测试环境
为了测试 Moonlark 的代码,您需要运行一个 OneBot 实现并使其连接到 ws://localhost:8080/onebot/v11/ws
(默认状态下),我们提供了以下两个参考方案:
NapCat确实好用,但是在qq上面,所有用了两天就被tx封。
Matcha虽然不能直接显示图片,但是在测试时可以通过nonebot的logger和with open(path,”wb”)将bytes类型的图片保存到本地。
开发
一个moonlark插件基本上要有这些内容
假设插件的名字叫nonebot_plugin_[name]
。
src/plugins/nonebot_plugin_[name]
首先建立插件文件夹
1 | nb plugin create nonebot_plugin_[name] |
修改Moonlark目录下的pyproject.toml
1
2
3
4
5
6
7
8...
[tool.nonebot]
...
plugins=[
...,
"nonebot_plugin_[name]",#加入这一行,确保插件会被加载
...
]
src/plugins/nonebot_plugin_[name]/__init__.py
这个文件包含了一些基本信息
1 | from nonebot import require |
src/lang/[lang]/[name].yaml
这是moonlark用于本地化(翻译)的文件
目前支持zh_hans
(简体中文,大陆)、zh_tw
(繁体中文,台湾)、en_us
(英语,美国)三种语言。
每个语言文件夹下都要创建一个对应的yaml,才能识别成功,并且每个对应的yaml中的键都要相同
例如,在zh_hans/[name].yaml
中有一个键key
,那么在zh_tw
和en_us
下的[name].yaml
都要有key
这个键。
键的命名满足以下规则:
- 不以数字开头;
- 由26个大写、26个小写、10个阿拉伯数字或者减号(
-
)组成 - 子键要缩进
src/templates/[name].html.jinja
有时候,机器人需要发送图片。这时需要一个图片模版来图片
引用Moonlark开发文档:
模板编写
Render 读取的模板储存在
src/templates
中,后缀为*.jinja
。一个模板的格式如下:
html
1
2
3
4 {% extends base %}
{% block body %}
{{ content }}
{% endblock body %}基模板
html
1 {% extends base %}这里使用了一个模板变量
base
作为基模板的名称,这个变量在渲染时会自动填充为对应主题的基模板。内容块
header
拓展页面页面的头部。
body
卡片主体内容。
card
卡片。
保留变量
这些变量会在渲染时被自动填充,请避免使用这些模板变量名。
base
: 主题基模板的相对路径。main_title
: 页面主标题。footer
: 页面页脚(版权信息)。主题
Render 支持主题,主题基模板储存在
src/templates/base
中,主题列表储存在src/plugins/nonebot_plugin_render/themes.json
中。基模板
一个主题的基模板需要定义以下变量和内容块:
模板变量
main_title
: 页面主标题。footer
: 页面页脚(版权信息)。内容块
header
: 页面拓展头部。body
: 卡片主体内容。card
: 卡片。TIP
一般来说,
card
会覆盖body
块。主题配置
主题配置是一个 JSON 文件,格式为
"主题ID": "主题模板相对于 src/templates 的路径"
。本地化
所有向用户展示的文本都要被本地化。
本地文件引用
可以使用
{% include "xx" %}
块或src=xxx
引入本地文件。 使用相对引入时,基路径为src/templates
。
src/plugins/nonebot_plugin_[name]/__main__.py
一般nonebot的插件主要部分都是在这里写的
里面的内容由插件的功能而定,由开发者自由发挥
但是有需要注意的几点:
创建命令
用户通过命令来使用nonebot2的插件。
Moonlark提供了一系列相关工具。(注意:这些都是nonebot2本身没有的)
首先在__init__.py
里加上1
2
3require("nonebot_plugin_alconna")
require("nonebot_plugin_larklang")
require("nonebot_plugin_larkuser")
然后就可以在__main__.py
里导入工具1
2
3from nonebot_plugin_alconna import Alconna, Args, on_alconna, Subcommand
from nonebot_plugin_larklang.__main__ import LangHelper
from nonebot_plugin_larkuser.utils.matcher import patch_matcher
接着就可以创建插件的命令
nonebot本身有on_command(
)方法,但是不方便获取参数,Moonlark使用Alconna让获取命令的参数更方便。1
2
3
4
5
6[command] = on_alconna(
Alconna(
"[command]",
Subcommand("[subcommand]", Args["[argument name]", [argument type],[default value]])
)
)
期中[command]
指命令名称[subcommand]
指子命令名称[argument name]
指参数名称[argument type]
参数类型[default value]
默认值(可选)1
2lang = LangHelper() #本地化
patch_matcher(sudoku) #启动指令配对器
事件处理
Nonebot是用到了异步编程,所以很多函数都用了async
和await
关键字
我总结了一下使用规则:
def
,for
,with
等关键字前可以使用async
,让其异步执行;- 调用使用了
async
定义的函数,需要使用await
; - 函数体中调用其他函数使用了
await
的函数(函数调用了其他异步函数),这个函数定义时要使用async
(这个函数也一定是异步函数)
主命令的事件处理1
2
3
4
5
6
7
8
9
10
#记得在__init__.py加入这个
require("nonebot_plugin_larkutils")
#记得在__main__.py开头导入这个
from nonebot_plugin_larkutils import get_user_id
async def _(args : [type], user_id: str = get_user_id()): #参数不止一个,建议指定类型
...#干你想干的事情
子命令就是把$main
换成对应的子命令名称即可
向用户发送消息
事件处理的一个重要步骤就是让机器人发送消息。
Alconna发送消息
1 | [command].send([message])#发送消息 |
[command]
就是on_alconna()
返回的AlconnaMatcher()
对象。
LangHelper发送消息
Moonlark开发工具,用于本地化。
1 | #lang就是刚才lang=LangHelper()创建的对象 |
其中key
是刚才本地化时添加的组件
访问子键时要先把父键写在前面,父键和子键用点号(.
)连接。
渲染图片模版并发送图片
导入依赖
**在__init__.py
中1
require("nonebot_plugin_htmlrender")
在__main__.py
中1
2from nonebot_plugin_render.render import render_template
from nonebot_plugin_render.cache import creator
定义渲染模版函数
1 |
|
使用1
2
3
4async def _(user_id: str = get_user_id()):
...
image = await render([content], user_id)
...
其中content
是记录内容的字典,要提供jinja对应的信息。
发送图片
1 | await UniMessage().image(raw=image).send(reply_to=True) |
reply_to
设置为True
时,机器人会回复用户的消息。
测试
启动Nonebot
在moonlark文件夹下运行命令,启动机器人
1 | poetry run nb run |
使用Matcha测试
不推荐Napcat测试,容易被封。
先设置角色(角色唯一标识必须是纯数字)
在启动Nonebot后向机器人发送消息就可以收到消息了