前言
这份文档对应的是一套轻量级 OlivOS 插件模板
因为有人提出现在的 OlivOS 的官方插件模板过于轻量,很多必要函数都需要自己实现也很麻烦;而之前我写的规则集插件模版又过于深度依赖 OlivaDiceCore 里面的必要函数,过于耦合化,故编写以下模板。本模板虽然没有和 OlivaDiceCore 完全解耦,但是其中的耦合化是必要的——实现群链在很多地方都方便不少。当然如果你的 OlivOS 并未装载 OlivaDiceCore,也完全没有影响。
注意,本说明和插件模板均为 vibe coding 所生成,但是所有函数和目录结构我均亲自确认和验证过,没有问题可以正常使用
下载链接
本模板已在 github 上进行开源:https://github.com/0Desom0/Desom-OlivaDice-Plugin/tree/main/%E7%A4%BA%E4%BE%8B/LightPluginTemplate
1. 模板定位
这套模板默认基于以下前提:
compatible_svn 为 190 或更高。
message_mode 为 olivos_string,即消息中的 at 码按 OP 码处理,即 [OP:at,id=xxx,name=xxx] 或 [OP:at,id=xxx]。
如果将来你把 message_mode 改成 old_string,那消息里的 OP 码会重新变回 CQ 码,模板里的 at 解析逻辑就不再完全对应当前默认实现。
2. 目录结构
当前模板目录如下:
LightPluginTemplate/
├── 模板说明文档.md
├── .flake8
└── YourPluginName/
├── __init__.py
├── app.json
├── config.py
├── function.py
├── gui.py
├── main.py
├── message.py
├── message_custom.py
└── utils.py
3. 运行时数据目录结构
模板运行后,数据会落在 plugin/data/YourPluginName/ 下。
其中分为两层:
3.1 全局配置
全局配置文件是:
plugin/data/YourPluginName/global_config.json
这份配置是 所有 Bot 共用 的。
目前只包含两个字段:
global_enable_switch
global_debug_mode_switch
3.2 Bot 级配置与回复目录
当前模板里,bot_config.json 不跟随 link;只有 storage、回复词与变量会按 linked_bot_hash 生效:
如果你还不知道这里的 link 指什么,可以先跳到 4.2 什么是 link,为什么要有 link 看解释。
plugin/data/YourPluginName/<raw_bot_hash>/bot_config.json
plugin/data/YourPluginName/<linked_bot_hash>/message_custom.json
plugin/data/YourPluginName/<linked_bot_hash>/message_variable.json
plugin/data/YourPluginName/<linked_bot_hash>/storage/
这意味着当前模板的数据会分成两部分:
bot_config.json 仍按原始 bot 保存
storage/目录,自定义回复,自定义变量按照 link_bot_hash 目录读取
这里还要再强调一点:
- 模板会在 init 阶段把 bot_config 与 linked 目录都初始化好。
- 初始化与后续保存时,都会直接使用最终解析出的
plugin/data/YourPluginName/<linked_bot_hash>/ 目录。
- 只有你是
从账号 的时候才会自动解析到主账号,其他情况就是你自己的 bot_hash 对应目录。
所以这里现在要明确区分两套语义:
- 原始配置目录:保存当前 bot 自己的
bot_config.json
- linked 运行目录:保存当前 bot 实际生效的 storage、回复词、变量
4. 术语说明
4.1 什么是 hag_id
hag_id 指的是:
- 带 host 的场景:
host_id|group_id
- 不带 host 的场景:
group_id
它的作用是标识“当前群”。
这个概念经常用于群配置、群开关、群权限判断。
4.2 什么是 link,为什么要有 link
这里说的 link,本质上不是一个单独的文件夹名,也不是一种新的 bot 身份,
而是一个“把当前账号映射到另一个账号去读取某些运行时数据”的逻辑关系。
在接了 OlivaDiceCore 的主从账号体系后,常见场景是:
- 一个主账号负责统一维护配置或回复词
- 若干从账号跟着主账号一起工作
- 从账号收到消息时,某些功能希望直接复用主账号那套内容,而不是每个号都各配一份
这时候就需要 link。
模板里所谓的 linked_bot_hash,可以理解成:
- 当前事件原本来自某个 bot
- 但在读取“会被主从账号共享”的那部分数据时
- 不一定继续用这个 bot 自己的原始 hash
- 而是先根据主从关系,解析出最终应该落到哪个 bot_hash 上
这个“最终用于共享数据读写的 bot_hash”,就是 linked_bot_hash。
为什么要有它,核心原因只有一个:
- 避免主从账号把同一套回复词、变量、运行期模板内容重复维护很多份
如果没有 link,那主账号和每个从账号都要各自维护自己的:
message_custom.json
message_variable.json
这样一来:
- 改一条回复词要改很多次
- 主从号的表现容易不一致
- 管理成本会明显变高
有了 link 以后,就可以把“共享的数据”统一收束到主账号对应的目录,
从账号在运行时直接复用主账号那份内容。
这里还需要特别提醒一个很容易误会的点:
- 本模板和
OlivaDiceCore 在“回复词解析是否跟随群链”这件事上,并不完全一样
OlivaDiceCore 自己解析回复时,默认是不跟随群链的,仍然读取各账号自己的回复词
- 但这个轻量模板里,回复词会跟随群链,其他地方都和
OlivaDiceCore 的群链逻辑是完全一致的。
也就是说:
- 如果你拿
OlivaDiceCore 的使用习惯来理解这个模板,就很容易误以为从账号会继续读自己的回复词
- 但本模板不是这样设计的;只要群链解析结果指向主账号,模板就会直接复用主账号那份回复词
这里还要特别强调:
link 是运行时读写策略,不是额外目录层级
- 所以模板不会创建
link/ 子目录
- 模板只是把回复词和变量直接写到最终解析出来的
linked_bot_hash 对应到准确的目录
本模板里 link 的作用范围是“除了 bot_config 之外的运行期 bot 数据”:
bot_config.json 不走 link
storage/ 走 link
message_custom.json 走 link
message_variable.json 走 link
所以你可以把它理解成一句话:
link 不是“把两个 bot 合并成一个 bot”
- 而是“让主从 bot 共用 storage、回复词、变量这类运行期数据,但 bot_config 仍保留各自独立”
5. GUI 设计说明
GUI 现在采用一个简单的双页签结构。
当前模板提供的页签有:
此外,这个模板已经在 app.json 中注册了一个插件菜单项:
- 标题:
打开插件配置
- 事件:
YourPluginName_Menu_001
OlivOS 触发该菜单事件后,会进入 main.py 的 Event.menu,再转交给 gui.py 中的 handle_menu_event 打开配置窗口。
窗口打开时会默认定位到当前触发菜单事件的那个 Bot,
但 Bot 配置页内部仍然提供一个下拉框,允许继续切换其他账号。
5.1 全局设置页
全局设置页保持成一个简单表单:
- 全局启用开关
- 全局调试模式开关
- 保存全局设置、打开总目录、刷新、关闭窗口按钮
5.2 Bot 配置页
Bot 配置页负责展示并修改当前菜单所属 Bot 的配置:
- 可切换当前 Bot 的下拉框
- 当前 Bot 信息
- 如果当前账号是从账号,则提示只有 storage、回复词、变量这些 linked 数据实际读取的主账号
- Bot 启用开关
- 保存 Bot 设置、打开目录、刷新等按钮
同时,为了不把主界面做得过重,以下功能改成从 Bot 配置页按钮打开子窗口:
5.3 回复词管理子窗口
回复词管理窗口采用树表加按钮条:
- 查看当前回复词条目、说明、内容
- 编辑回复词
- 恢复默认值或删除扩展条目
- 恢复全部默认回复
- 支持双击编辑
- 支持右键菜单执行“编辑”和“恢复/删除”
- “恢复默认回复”和“恢复/删除”都带二次确认
5.4 设主列表管理子窗口
设主列表管理窗口用于维护当前 linked 目录中的 configured_master_list:
这依然保持模板当前的 linked 目录语义:
- 骰主列表不跟随群链
- 也就是当前窗口操作的是当前原始 bot 自己那份数据
6. 文件职责总览
下面按文件逐一说明职责。
6.1 __init__.py
作用:暴露 OlivOS 插件入口模块。
6.2 app.json
作用:定义插件元信息。
主要字段说明:
name:插件显示名。
namespace:插件命名空间。
message_mode:当前模板默认是 olivos_string。
compatible_svn:当前模板默认是 190。
priority:插件优先级。
menu_config:插件菜单注册表,当前模板注册了一个“打开插件配置”菜单项。
特别提示与兼容性说明:
关于 compatible_svn 与 At 码格式:
当前模板的核心解析逻辑基于 compatible_svn >= 190 编写。该字段决定了底层框架下发 At 信息的结构:
- 当设为
190 以下时,框架传递的 At 码形如 [OP:at,id=xxx]。
- 当设为
190 及以上时,框架会在协议端支持的情况下附加昵称信息,格式扩展为 [OP:at,id=xxx,name=xxx](若协议端未提供 name 属性,则回退为基础格式)。
关于 message_mode 与 OP 码标准:
当前模板默认采用 olivos_string(即 OP 码模式),而非传统的 CQ 码模式(对应配置为 old_string)。
- OP 码特征:在底层语法和结构上与 CQ 码保持高度一致,核心区别仅在于标识符的映射转换:将所有的前缀
CQ 替换为 OP,并将参数键名 qq 替换为 id,其余属性与语法约束均未改变。
其他部分请参考官方文档:OlivOS 插件开发文档
当前模板的 menu_config 按官方最小模板要求提供 title 与 event 两个字段:
title:OlivOS 插件托盘里显示给用户看的菜单名。
event:点击菜单后传入 Event.menu 的事件名。
为了避免菜单事件名冲突,模板默认采用 命名空间_Menu_编号 的命名方式。
6.3 main.py
作用:纯事件入口。
这里只做这些事:
- 使用相对导入直接拿到
message、utils、gui 模块,避免在 OlivOS 运行时依赖包对象是否已经暴露这些子模块
init 时调用 utils.initialize_plugin 和 message.handle_init
init_after 时调用 message.handle_init_after
- 私聊消息转发给
message.handle_private_message
- 群聊消息转发给
message.handle_group_message
- 戳一戳事件转发给
message.handle_poke
- 好友申请事件转发给
message.handle_friend_add_request
- 群邀请事件转发给
message.handle_group_invite_request
- 群成员增加事件转发给
message.handle_group_member_increase
- 心跳事件转发给
message.handle_heartbeat
- 保存事件转发给
message.handle_save
- 菜单事件转发给
gui.handle_menu_event
这里不做:
原因很简单:入口文件越纯净,后续维护越稳。
这里额外需要注意一个运行时细节:
__init__.py 当前只导出 main,不会自动把 message、utils、gui 挂到包对象上。
- 所以
main.py 不能写成 YourPluginName.message.handle_xxx(...) 这种依赖包属性的形式。
- 在 OlivOS 把插件复制到
plugin/tmp/... 目录后,这类写法很容易出现 module 'YourPluginName' has no attribute 'message'。
- 模板现在改为在
main.py 内部直接相对导入对应模块,从根源上规避这个问题。
6.4 config.py
作用:存放静态常量配置。
这里适合放:
- 插件名称
- 数据目录名称
- 文件名常量
- 默认配置结构
- GUI 页签标题
- 允许的前缀列表
这里不适合放:
换句话说,config.py 负责“定义是什么”,utils.py 负责“怎么使用这些定义”。
6.5 utils.py
作用:提供公共基础能力。
这是整个模板里功能最集中、最基础的模块。
它目前负责:
- 字符串安全处理
- 默认配置合并
- 文件/目录读写
- 调试、信息、错误日志输出
- 插件初始化与 Bot 数据目录初始化
- 运行期
Proc 缓存
- 主动发消息封装
send_message_force
- linked bot 目录初始化
- bot 级目录统一跟随 linked_bot_hash
- hag_id 构建与拆分
- 前缀解析
- 命令解析
- at 解析
- 骰主 / 配置骰主 / 群主 / 群管判断
- OlivaDiceCore 群开关检查
- 回复词渲染
- 回复发送封装
- Bot 自定义回复词与变量的读写
6.6 message.py
作用:负责消息解析与回复流程。
它是模板中的“消息调度层”,职责包括:
- 接收来自
main.py 的事件
- 读取当前全局配置与 Bot 配置
- 解析回复头、前缀、命令、参数、at
- 处理 poke 事件示例
- 处理好友申请、群邀请、群成员增加、heartbeat、save 等非消息事件示例
- 判断是否该继续处理消息
- 把命令路由到具体的处理函数
- 按 linked_bot_hash 读取 bot_config、自定义回复与变量
- 用
utils.reply_message 发送结果
6.7 message_custom.py
作用:定义模板默认的回复词、变量、帮助文本、GUI 文案说明。
这里的内容是“默认值来源”,不是“运行时存储中心”。
也就是说:
- 模板运行时真正生效的回复词,是
message_custom.json 里的内容。
message_custom.py 提供的是初始默认文本和说明文案。
这样做的好处是:
- 模板作者可以很清楚地知道默认值在哪里。
- 用户改配置文件或在 GUI 里改回复词时,不会反向污染源码默认值。
6.8 gui.py
作用:实现通用 GUI。
它当前只负责:
- 渲染全局配置页
- 渲染 Bot 配置页
- 显示当前 Bot 的基本信息
- 编辑当前 Bot 的配置骰主与 Bot 开关
- 编辑当前 Bot 的自定义回复词
- 提供统一的布局、尺寸、样式辅助方法
它不负责:
6.9 function.py
作用:预留纯业务逻辑模块位置。
当前模板故意不在这个文件里放任何实际函数,因为这里装载插件功能函数。
也就是说,这里是放你的插件所需要实现的功能函数,也就是纯 Python 业务逻辑。
6.10 模板说明文档.md
作用:帮助使用模板的人快速理解整个结构。
也就是这篇文档
6.11 .flake8
作用:用于插件的 lint,进行格式规范化
7. 配置分层原则
模板当前遵循下面这条非常重要的规则:
7.1 全局配置
所有 Bot 共用:
global_enable_switch
global_debug_mode_switch
7.2 Bot 配置
每个 Bot 独立:
7.3 回复词与变量
运行期按 linked_bot_hash 生效:
storage/
message_custom.json
message_variable.json
这样拆分之后,用户就很容易理解:
- 全局开关是“整个插件都怎样”。
- Bot 配置是“这个 Bot 自己怎样”。
- linked 运行数据是“这个 Bot 实际共用哪一套 storage、变量和回复内容”。
8. 命名约定
模板统一采用下划线命名法。
例如:
global_enable_switch
configured_master_list
build_hag_id
get_linked_bot_hash
虽然 OlivaDiceCore 通常使用小驼峰命名法,但是我还是更喜欢用下划线命名法。
9. 回复词自定义流程
模板里的回复词自定义流程如下:
message_custom.py 提供默认回复词。
utils.initialize_bot_storage 首次启动时,会把 message_custom.json 和 message_variable.json 初始化到对应的 bot_hash 目录。
message.py 在回复时,通过 utils.load_bot_message_custom 读取当前 Bot 对应 linked_bot_hash 目录下的回复词。
- 如果后续通过 GUI 或保存接口写入回复词,文件会直接保存到对应的
plugin/data/YourPluginName/<bot_hash>/message_custom.json。
gui.py 在 Bot 配置页中提供回复词编辑能力。
所以后续你要新增一条可配置回复词,通常只需要做三件事:
- 在
message_custom.py 里补默认文本与说明。
- 在
message.py 里调用这条回复词。
- GUI 会自动把这条 key 列入可编辑列表。
10. 主动发消息与 poke 示例
模板现在额外提供了两类与官方最小模板同用途的能力:
10.1 主动发消息
对应封装:utils.send_message_force(bot_hash, send_type, target_id, message_text, Proc=None)
用途:
- 定时任务主动推送
- 菜单按钮触发后主动发消息
- 后台线程或其他没有现成
plugin_event 的场景
注意事项:
- 传入哪个
bot_hash,模板就会直接使用哪个 bot 发消息。
- 这里不遵循 link,因为 link 只用于数据读写,不代表 bot 自身会跟着切换。
send_type 一般传 group 或 private。
10.2 poke 事件
对应入口:main.Event.poke -> message.handle_poke
默认行为:
- 戳到当前 bot 时回复
reply_poke
- 自己戳自己时保留回复示例
- 仍然受全局开关与 Bot 开关控制
11. flake8 校验规则
模板当前按以下规则校验:
[flake8]
extend-ignore = E203
max-line-length = 120
per-file-ignores =
*/__init__.py: F401
当前校验命令:
flake8 --extend-ignore=E203 --max-line-length=120 --per-file-ignores="*/__init__.py:F401" "...\LightPluginTemplate\YourPluginName"
12. 继续扩展时的建议
如果后续你要基于这个模板继续写插件,建议按下面顺序扩展:
- 先在
message_custom.py 补回复词与说明(包括现在模板里面为了演示,都有很多回复没有自定义到 message_custom.py 里面)
- 再在
message.py 增加命令与回复逻辑。
- 当某块业务开始变复杂时,再把纯逻辑拆到
function.py。
- 如果新增的是运行时数据,再写到
utils.py 对应的存储封装里。
这样扩展下来,整个插件就不会被逐渐写乱。
13. 结语
借用此贴的一句话
抛砖引玉的根本目的是引玉!
我希望大家能够根据这份教程写出更好的插件!如果你在实践过程中仍有疑问,欢迎大家在此贴下进行提问,我会一一进行解答
最后,祝各位开发者 bug 越写越少,没有用户在你的酒吧里点炒饭。
让每一个插件都成为 OlivOS 生态的闪耀星辰!