Skip to content

Latest commit

 

History

History
885 lines (665 loc) · 32.5 KB

File metadata and controls

885 lines (665 loc) · 32.5 KB

ElainaBot 插件开发文档

面向开发者的完整插件开发指南 — 从最简单的 "Hello World" 到复杂的多文件插件、Web 面板扩展、主动消息推送、生命周期钩子等。


目录


1. 快速开始

plugins/ 下新建文件 plugins/hello/main.py

"""Hello 插件 — 最小示例"""
from core.plugin.decorators import handler


@handler(r'^你好$', name='打招呼', desc='回复一句问候')
async def say_hello(event, match):
    await event.reply(f"你好, {event.user_id[:8]}****!")

完成。 框架启动时会自动扫描 plugins/ 目录加载插件,热更新也已内置。

元素 说明
@handler(r'^你好$') 正则匹配用户消息
event 当前消息事件对象 (core.message.event.Event)
match re.Match 对象 (匹配结果)
event.reply(...) 回复当前会话

2. 插件目录结构

ElainaBot 支持两种插件形态:

2.1 简单插件 (单文件)

plugins/
└── hello/
    ├── 任意文件名.py       # 入口文件 
    ├── requirements.txt   # 依赖 (可选, 自动 pip install)
    └── data/              # 持久化数据 (可选, 由 ctx 管理)

2.2 大型插件 (多文件 + 子模块)

plugins/
└── my_plugin/
    ├── main.py            # 入口(以下入口仅作推荐命名,不强制)
    ├── app/               # 子插件目录
    │   └── **.py
    ├── mod/               # 业务模块
    │   └── **.py
    ├── data/              # 数据存储
    │   └── **.yaml
    ├── necessary/         # 资源文件
    └── requirements.txt

入口文件命名: index.py / app.py / main.py 任选其一
子目录访问: from .mod.core import xxx
自由组织: 框架只识别入口文件,内部目录结构、文件数量和命名完全自由,只需在入口文件中 import 即可生效。


3. 核心装饰器

所有装饰器都从 core.plugin.decorators 导入:

from core.plugin.decorators import handler, on_load, on_unload, interceptor

3.1 @handler 消息处理器

签名:

@handler(pattern, *, name='', desc='', priority=0, owner_only=False,
         group_only=False, direct_only=False, channel_only=False,
         event_types=None, cooldown=0, ignore_at_check=False, block=False)
参数 类型 默认 说明
pattern str 正则表达式 (使用 re.DOTALL 编译)
name str 函数名 处理器显示名称 (Web 面板 / 日志)
desc str '' 功能描述
priority int 0 优先级 (数字越大越先匹配)
owner_only bool False 仅主人可触发
group_only bool False 仅群聊
direct_only bool False 仅私聊
channel_only bool False 仅频道
event_types list[str] None 仅响应指定事件类型 (见下表)
cooldown int 0 冷却时间 (秒, 0 = 无冷却)
ignore_at_check bool False 全量模式: 不需@机器人也触发
block bool False 命中后是否拦截后续处理器 (见 3.1.1)

事件类型常量 (event_types 可选值):

常量 含义
GROUP_AT_MESSAGE_CREATE 群聊 @ 机器人
GROUP_MESSAGE_CREATE 群聊全量消息
C2C_MESSAGE_CREATE 私聊消息
DIRECT_MESSAGE_CREATE 频道私信
AT_MESSAGE_CREATE 频道 @ 机器人
MESSAGE_CREATE 频道公开消息
INTERACTION_CREATE 按钮/交互回调
GROUP_ADD_ROBOT / GROUP_DEL_ROBOT 机器人加群/退群
GROUP_MEMBER_ADD / GROUP_MEMBER_REMOVE 群成员入群/退群
GROUP_MSG_REJECT / GROUP_MSG_RECEIVE 群消息拒绝/恢复
FRIEND_ADD / FRIEND_DEL 加好友/删好友
MESSAGE_REACTION_ADD / MESSAGE_REACTION_REMOVE 表态(表情回应)添加/移除

示例:

@handler(r'^管理\s+(\S+)$', name='管理命令', owner_only=True, group_only=True)
async def admin(event, match):
    await event.reply(f"✅ 已处理: {match.group(1)}")


@handler(r'^签到$', name='签到', ignore_at_check=True)  # 无需@即可触发
async def check_in(event, match):
    await event.reply("✅ 签到成功!")

3.1.1 block 放行 / 拦截

多个插件注册相同指令时, block=False (默认) 放行让所有命中处理器按 priority 顺序执行, block=True 命中即拦截后续低优先级处理器:

@handler(r'^状态$', name='系统状态', priority=10, block=True)  # 命中即拦截, 只有它响应
async def status(event, match):
    await event.reply("✅ 系统正常")


@handler(r'^状态$', name='天气状态', priority=0)  # 被上面 block 拦截, 不会触发
async def weather(event, match):
    await event.reply("☀️ 今天晴")

3.2 @on_load / @on_unload 生命周期钩子

from core.plugin.decorators import on_load, on_unload


@on_load
async def init():
    """插件加载完成时执行 (支持 async/sync)"""
    print("插件已加载")


@on_unload
def cleanup():
    """插件卸载/重载时执行 — 清理资源"""
    print("插件已卸载")

使用场景: 启动后台任务、连接数据库、注册 Web 页面、注销定时器等。

3.3 @interceptor 消息拦截器

@interceptor(priority=100)
async def filter_keywords(event):
    """返回 True 阻止后续 handler 匹配, 否则继续"""
    if '违禁词' in (event.content or ''):
        await event.reply("⛔ 消息包含违禁词")
        return True
    return False
参数 说明
priority 拦截器优先级 (数字越大越先执行)
返回值 True 阻止后续处理, 其他值继续

4. Event 事件对象

event 是所有 handler 的第一个参数, 提供事件的全部上下文。

4.1 常用字段

字段 类型 说明
event.user_id str 用户 ID (OpenID, 或 union_id 取决于配置)
event.username str 用户昵称 (可能为空)
event.group_id str 群 ID (仅群聊)
event.channel_id str 频道 ID
event.content str 消息文本 (已去除 @机器人 标记)
event.raw_content str 原始消息内容
event.message_id str 消息 ID
event.event_type str 事件类型
event.appid str 机器人 AppID
event.attachments list 附件列表 (图片/文件等)
event.image_url str 图片 URL (若消息含图片)
event.raw dict 原始 payload 字典
event.timestamp str 消息时间戳
event.event_id str 事件 ID
event.guild_id str 频道服务器 ID (频道场景)
event.interaction_data dict 交互回调数据 (仅 INTERACTION 事件)
event.callback_code int / None 交互回调状态码, 见 5.8
event.message_reference_id str 可引用的 REFIDX (用于引用回复, 见 5.7)
event.message_scene dict 消息场景信息 (source / ext 等)
event.raw_user_id str 平台原始用户 OpenID (不受 union_id 配置影响)
event.union_openid str 用户 union_openid (跨机器人统一 ID, 可能为空)
event.msg_elements list 消息元素列表 (平台原始 msg_elements)
event.member_role str 发送者群身份 (admin / owner / 空)
event.bot_member_role str 机器人在该群的身份 (被@时由 mentions 解析)
event.error dict / None 最近一次媒体上传失败的响应 (排查用)

4.2 场景标识 (布尔属性)

属性 说明
event.is_group 群聊
event.is_direct 私聊
event.is_channel 频道
event.is_interaction 按钮交互回调
event.is_lifecycle 生命周期事件 (加群/加好友)
event.is_bot 消息发送者是机器人

4.3 @ 相关 (仅群聊)

属性 说明
event.is_at_self 是否 @ 了当前机器人
event.is_at_other_bot 是否 @ 了其他机器人
event.is_at_other_user 是否 @ 了其他普通用户
event.is_at_all 是否 @ 了全体成员
event.mentions @ 列表原始数据

4.4 派生属性

属性 说明
event.chat_id 自动返回 group_id / user_id / channel_id
event.chat_type 返回 'group' / 'direct' / 'channel' / 'unknown'
event.get(path) JSON 路径取值, 如 event.get('d/author/id')
event.sender 底层 MessageSender 实例 (高级用法)

5. 消息发送 API

event 通过代理表自动转发到 MessageSender, 调用形如 await event.reply(...)

5.1 文本与媒体回复

# 文本回复
await event.reply("Hello!")

# 带按钮回复 (按钮字段与示例见 5.2 节)
await event.reply("📌 选择操作", buttons=[[{'text': '回调', 'data': 'cb_1', 'type': 1}]])

# 自动撤回 (秒)
await event.reply("⏰ 5秒后撤回", auto_delete_time=5)

# 图片 (URL 或 bytes)
await event.reply_image("https://i.elaina.vin/1.png", "图片说明")
await event.reply_image(open('local.png', 'rb').read(), "本地图片")
await event.reply_image("https://...", "10秒后撤回", auto_delete_time=10)  # 媒体也支持自动撤回

# 语音 / 视频 / 文件
await event.reply_voice("https://example.com/audio.wav")
await event.reply_video("https://example.com/video.mp4")
await event.reply_file('/path/to/file.txt', "📄 文档", file_name="custom.txt")

reply() 完整参数一览

await event.reply(
    content=None,              # 文本内容
    buttons=None,              # 按钮 (见 5.2)
    media=None,                # 已上传的 media 对象 {'file_info': ...} (高级用法)
    msg_type=None,             # 强制消息类型 (见 5.1.2)
    template_name=None,        # 模板名 (见 5.4)
    template_vars=None,        # 模板变量 dict
    prompt_buttons=None,       # 扩展 prompt 按钮 (见 5.2)
    auto_delete_time=None,     # N 秒后自动撤回
    skip_suffix=False,         # 跳过全局 markdown_suffix 后缀 (见 5.1.2)
    message_reference_id=None, # 引用回复 REFIDX (见 5.7)
    message_reference=None,    # 完整引用对象 dict (优先于 message_reference_id)
    button_font_size=None,     # 键盘级按钮字号 small/middle/large (见 5.2)
    button_style=None,         # 键盘级样式 dict, 直接并入 keyboard.content.style
    # **kwargs: 其余关键字原样并入平台载荷 (payload), 支持平台新增字段
)

kwargs 透传: 未列出的关键字参数会原样写入发送载荷, 例如 await event.reply('hi', markdown={'custom_template_id': 'xxx', 'params': [...]}) 可直接使用平台原生字段。reply_image / reply_voice / reply_video / reply_file 支持的关键字为 file_name / auto_delete_time / target_user_id / target_group_id (见 5.5)。

5.1.2 消息类型: 强制 markdown / 强制纯文本

框架默认按 bot.yamlmessage.use_markdown 决定用 markdown (msg_type=2) 还是纯文本 (msg_type=0) 发送。单条消息可通过 msg_type 参数强制覆盖:

# 强制以纯文本发送 (即使全局开启 use_markdown)
await event.reply("**不会加粗**, 原样显示", msg_type=0)

# 强制以 markdown 发送 (即使全局关闭 use_markdown)
await event.reply("**加粗** 和 [链接](https://example.com)", msg_type=2)

# 主动消息同样支持
await event.send_to_group(event.group_id, "# 标题", msg_type=2)
说明
0 纯文本
2 原生 Markdown
3 Ark 卡片 (由 reply_ark 自动设置)
7 富媒体 (由 reply_image 等自动设置)
8 卡片消息 (由 reply_card 自动设置)

不传 msg_type 时按 message.use_markdown 配置决定。

markdown 全局后缀: markdown 消息会自动拼接 bot.yamlmessage.markdown_suffix 配置的全局后缀 (支持 \n 等转义)。单条消息可用 skip_suffix=True 跳过:

await event.reply("这条消息不带全局后缀", skip_suffix=True)
await event.send_to_group(event.group_id, "主动消息同样支持", skip_suffix=True)

5.2 按钮完整字段参考

按钮是二维数组 list[list[dict]] (行 × 列), 每个按钮是一个字典。全部字段一览:

字段 类型 默认 说明
text str '' 按钮显示文字 (必填)
type int 2 按钮类型: 0=跳转链接 / 1=回调 / 2=输入指令 / 4=订阅
data str text type=0: URL; type=1: 回调标识; type=2: 填充到输入框的内容
link str 快捷方式: 设置后自动设为 type=0 + data=link
show str text 点击后显示的文字 (visited_label)
style int 1 样式: 0=灰框 / 1=蓝框蓝字 / 2=黑框(PC 端气泡) / 3=黑框红字 / 4=蓝底白字
reply bool 点击后作为引用回复发送
limit int 点击次数限制 (click_limit)可能无效
tips str 不支持时的提示文字 (unsupport_tips)
modal str/dict 点击后的二次确认弹窗; 字符串等价于 {'content': 文本}, dict 可额外指定 confirm_text / cancel_text
subscribe str/list/dict 订阅模板 ID, 设置后自动设为 type=4 并生成 subscribe_data; 含 _ 的 ID 转为 custom_template_id, 否则为 template_id; dict 原样透传

权限字段 (五者二选一, 优先级从上到下): permission (显式权限对象, 如 {'type': 1}) > role (身份组 ID 列表, 频道场景 → type=3) > list (用户 ID 列表 → type=0) > admin (仅管理员 → type=1) > 默认所有人可点 (type=2)。

平台原生 action 字段 (subscribe_data / click_limit / unsupport_tips / anchor) 也可以直接写在按钮字典里, 原样透传到 action; 写了 subscribe_data 且未指定 type 时自动设为 type=4

按钮示例

buttons = [
    # type: 0=跳转链接 / 1=回调 / 2=输入指令 / 4=订阅 (link 等同 type=0)
    [{'text': '跳转官网', 'link': 'https://example.com'},
     {'text': '点我回调', 'data': 'cb_action_1', 'type': 1},
     {'text': '/帮助', 'type': 2}],
    # style: 0=灰框 / 1=蓝框蓝字(默认) / 2=黑框(PC 端气泡) / 3=黑框红字 / 4=蓝底白字
    [{'text': '灰框', 'data': 's0', 'type': 1, 'style': 0},
     {'text': '蓝框蓝字', 'data': 's1', 'type': 1, 'style': 1},
     {'text': '黑框', 'data': 's2', 'type': 1, 'style': 2},
     {'text': '黑框红字', 'data': 's3', 'type': 1, 'style': 3},
     {'text': '蓝底白字', 'data': 's4', 'type': 1, 'style': 4}],
    # 权限与限制
    [{'text': '仅管理员', 'data': 'admin_only', 'type': 1, 'admin': True},
     {'text': '点击一次', 'data': 'once', 'type': 1, 'limit': 1}],
    # 订阅按钮 (type=4): 必须挂在 markdown 消息 (msg_type=2) 上发送
    [{'text': '订阅', 'show': '已订阅',
      'subscribe': '102134274_1749040268',  # 机器人Markdown模板 id
      'modal': {'content': '确认订阅?', 'confirm_text': '✔️确认', 'cancel_text': '❌取消'},
      'tips': '请升级QQ版本'}],
]
await event.reply("📌 多功能按钮面板", buttons=buttons, msg_type=2)

⚠️ subscribe 必须传入真实存在的模板 ID: 无效模板 (如 template_id: "0") 会导致部分 QQ 客户端点击按钮后闪退。

发送订阅消息

用户点击订阅按钮后, 平台下发 SUBSCRIBE_MESSAGE_STATUS 订阅事件, 事件中返回 subscribe_id (发送订阅消息的票据)。框架会自动记录订阅关系 (模板ID ↔ 群/用户, 含 subscribe_id), 也可用 event_types=['SUBSCRIBE_MESSAGE_STATUS'] 订阅该事件自行读取 event.subscribe_results

必须携带 subscribe_id — 不填写将按普通主动消息推送 (占用主动消息条数):

markdown_id = '102134274_1749040268'  # markdown 模板 id (订阅按钮 subscribe 字段填的那个)
# 先查该群订阅了哪些模板: [{template_id, sub_type, subscribe_id}, ...]
subs = log_service.subscribe_get_by_target(group_id)
t = next((x for x in subs if x['template_id'] == markdown_id), None)
if t:
    ok, data, _ = await event.send_to_group(
        group_id, '🔔 这是一条订阅消息推送', subscribe_id=t['subscribe_id'])
    # 单次订阅 (sub_type='once') 发送后作废, 永久订阅可重复推送; 单群有每日推送限额
    if ok and t['sub_type'] == 'once':
        await log_service.subscribe_consume(markdown_id, group_id)

小按钮 (键盘级字号)

通过键盘级样式 content.style.font_size 控制整组按钮的大小 (对应官方 botgo CustomKeyboard.Style.FontSize), 取值 small / middle / large, small 即「小按钮」, 不传则保持默认大小:

# 方式一: reply 关键字 button_font_size / button_style
await event.reply("📌 小按钮面板", buttons=buttons, button_font_size='small')
await event.reply("📌", buttons=buttons, button_style={'font_size': 'small'})  # 整体样式 dict, 对应平台 keyboard.content.style

# 方式二: buttons 用 dict 包装 (适用于所有发送入口, 含主动推送/频道)
await event.reply("📌 小按钮面板", buttons={'rows': buttons, 'font_size': 'small'})
await event.reply("📌", buttons={'rows': buttons, 'style': {'font_size': 'small'}})

附: 扩展 prompt 按钮 (最多 3 个)

# 字符串简写 (点击后自动发送 'elaina')
await event.reply("选择:", prompt_buttons=['选项A', '选项B', '选项C'])

# (文本, 样式) 元组
await event.reply("选择:", prompt_buttons=[('确认', 1), ('取消', 0)])

5.3 Ark 卡片

# ark23 — 列表卡片
await event.reply_ark(23, (
    "列表卡片标题", "提示文本",
    [['项目1'], ['项目2', 'https://link.com']]))

# ark24 — 文本+图片
await event.reply_ark(24, (
    "提示", "标题", "副标题", "描述", "图片URL", "跳转URL", "图片副标题"))

# ark37 — 大图文
await event.reply_ark(37, (
    "提示", "标题", "副标题", "图片URL", "跳转URL"))

5.3.1 卡片消息 (msg_type=8)

reply_card(card_type, data) 发送卡片消息, card_type 可自定义以支持平台新增卡片类型, data 为 dict 时原样作为 card.content 发送:

# tuwen 图文卡片, 元组简写: (标题, 描述, 图片URL, 跳转URL)
await event.reply_card('tuwen', (
    "QQ开放平台", "2分钟完成注册并创建QQBot",
    "https://example.com/pic.png", "https://q.qq.com/#/"))

# 自定义类型/字段, dict 原样透传
await event.reply_card('tuwen', {
    'title': 'QQ开放平台', 'description': '...', 'pic_url': '...', 'url': '...'})

5.4 模板消息

# 引用 templates 目录下的模板
await event.reply(template_name='maintenance',
                  template_vars={'user_id': event.user_id})

5.5 主动推送图片

# 主动推送图片到指定群/用户 (不关联消息)
await event.send_image('group', event.group_id, "https://...", "图片说明")
await event.send_image('user', event.user_id, image_bytes, "说明")

# 或者用 reply_* 系列 + target_group_id / target_user_id 主动推送任意媒体
await event.reply_image("https://...", "说明", target_group_id="群ID")
await event.reply_video("https://...", target_user_id="用户ID")
await event.reply_file('/path/file.zip', "📦", file_name="pkg.zip", target_group_id="群ID")

传入 target_group_id / target_user_id 后, 媒体不再关联当前消息 (变为主动推送), 可另传 msg_id=... 使其关联指定消息成为被动回复。媒体上传失败时, 最近一次失败响应会记录到 event.error 供排查。

5.6 主动消息推送

await event.send_to_group(event.group_id, "主动群消息")     # 目标 ID 可为当前会话或任意指定
await event.send_to_user(event.user_id, "主动私聊消息")
await event.send_to_channel("频道ID", "频道消息")

send_to_group / send_to_user 参数

除首参为目标 ID 外, content / buttons / media / msg_type / skip_suffix / message_reference_id / **kwargs 透传均与 reply() 一致 (见 5.1), 额外支持:

await event.send_to_group(
    group_id,      # 目标群 ID (send_to_user 为 user_id)
    msg_id=None,   # 关联消息 ID → 变为被动回复 (不占主动推送额度)
    event_id=None, # 关联事件 ID (加群/加好友等事件回复)
)

send_to_channel(channel_id, content, *, msg_id=None, buttons=None, **kwargs) 支持 msg_id 被动化与 buttons

event.reply() vs event.send_to_*(): reply 是被动回复 (关联当前消息 msg_id), send_to_* 是主动推送 (不关联消息)。日常使用 reply 即可, 延迟场景 (如定时任务、sleep 后) 可以用 send_to_*

不依赖 event 的主动推送

send_to_* 不读取 event 字段, event.send_to_* 只是便捷写法。没有 event 时 (定时任务、@on_load 后台循环) 取一个 sender 直接调用:

from core.bot.manager import _bot_manager_ref

# 取任意可用 bot 的 sender (指定 appid: _bot_manager_ref.get_bot(appid).sender)
sender = next(iter(_bot_manager_ref._bots.values())).sender

await sender.send_to_group("群ID", "通知内容")
await sender.send_to_user("用户ID", "私信内容")
await sender.send_to_channel("频道ID", "频道消息")

5.7 引用消息与发送返回值

发送返回值

方法 返回值
reply / reply_image / reply_ark 平台响应 dict (失败为 None)
send_to_group / send_to_user (ok, data, payload) 三元组
send_to_channel (ok, data)
send_wakeup (ok, 消息ID或原因)

响应 dict 常用字段: data['id'] (平台消息 ID)、data['ext_info']['ref_idx'] (本条消息的可引用 REFIDX)。

引用消息 (message_reference_id)

引用回复需要传 REFIDX (REFIDX_xxx), 而不是平台消息 ID (ROBOT1.0_xxx):

# 1) 引用"用户当前这条消息"
await event.reply("引用你刚发的消息", message_reference_id=event.message_reference_id)

# 2) 引用"机器人刚发出的那条消息" (从发送响应里取 ref_idx)
data = await event.reply("第一条")
ref = (data or {}).get('ext_info', {}).get('ref_idx', '')
if ref:
    await event.reply("引用上面那条", message_reference_id=ref)

# 3) 主动消息也可引用
await event.send_to_group(event.group_id, "通知", message_reference_id=ref)

框架会自动组装为 {"message_reference": {"message_id": "REFIDX_xxx", "ignore_get_message_error": true}}。只有显式传 message_reference_id 才会引用。需要完全自定义引用对象时可直接传 message_reference={...} (优先级高于 message_reference_id)。

5.8 撤回与交互回调

# 撤回当前消息
await event.recall()

# 撤回指定消息
await event.recall(message_id="xxx")

交互回调 — 回调按钮 (type=1) 被点击时下发 INTERACTION_CREATE 事件, 此时 event.content 即按钮的 data。用 set_callback_code 应答这次点击:

@handler(r'^my_btn$', event_types=['INTERACTION_CREATE'])
async def on_click(event, match):
    event.set_callback_code(0)     # 应答这次交互
    # event.set_ack_timeout(15)    # 需更久处理时, 先延长等待(秒)再设置 code
    # 未调用时, 框架会自动用默认 code 应答

# 旧式 REST 应答 (会额外发一次请求, 一般无需使用)
await event.ack_interaction(code=0)

5.9 唤醒消息 (召回功能)

# 智能召回 (按规则发送)
ok, reason = await event.send_wakeup(user_id, "📢 召回提示")

# 强制召回 (跳过条件)
ok, result = await event.sender.force_wakeup(user_id, "强制召回")

5.10 高级工具方法 (通过 event.sender)

# 生成分享链接
url = await event.sender.get_share_link(callback_data='my_data')

# 获取图片尺寸 (URL/bytes/本地路径, 返回 {'width', 'height', 'px'} 或 None)
size = await event.sender.get_image_size("https://...")

# 手动上传媒体文件 (返回 file_info)
file_info = await event.sender.upload_media(event, file_bytes, file_type=1)
# file_type: 1=图片, 2=视频, 3=语音, 4=文件

# 查询单个群成员详情 (返回 dict 或 None, 含 member_openid/username/member_role 等)
member = await event.sender.get_group_member(group_id, user_id)

# 查询机器人自身在某群的成员信息 (返回 dict 或 None)
bot_member = await event.sender.get_bot_member(group_id)
is_admin = bot_member and bot_member.get('member_role') in ('admin', 'owner')

6. 插件上下文 ctx

ctx 在插件加载时由框架注入, 提供 数据目录管理 + YAML 配置:

import core.plugin.context as _ctx
ctx = _ctx.ctx  # 在模块顶层捕获

# 读写文本
ctx.save_data('log.txt', 'hello')
content = ctx.read_data('log.txt')

# YAML 配置 (推荐)
config = ctx.ensure_config({
    'enabled': True,
    'timeout': 30,
}, filename='config.yaml')

ctx.save_config({'enabled': False}, filename='config.yaml')

# 异步版本
await ctx.read_config_async()
await ctx.save_config_async({'k': 'v'})

# 路径辅助
ctx.get_data_path('foo.json')      # data/ 下文件
ctx.get_resource_path('image.png') # 插件根目录文件
方法 说明
read_config(filename) 读取 YAML 配置
save_config(data, filename, comments) 保存 YAML (可带注释)
ensure_config(defaults, ...) 缺项自动补齐, 返回完整配置
read_data / save_data 文本文件读写
read_data_async / save_data_async 异步版本
data_exists(filename) 文件是否存在
list_data() 列出 data/ 下所有文件

7. 插件元数据 __plugin_meta__

在入口模块顶层声明, Web 面板将展示这些信息:

__plugin_meta__ = {
    'name': '我的插件',
    'author': 'YourName',
    'description': '插件功能说明',
    'version': '1.0.0',
    'github': 'https://github.com/xxx/repo',
    'homepage': 'https://example.com',
    'license': 'MIT',
}
字段 说明
name 显示名称
author 作者
description 简介
version 版本号
github 仓库地址
homepage 主页
license 许可证

8. Web 面板扩展

插件可注册自定义页面到 Web 面板侧边栏:

from core.plugin.web_pages import register_page, unregister_page
from core.plugin.decorators import on_unload


# 内联 HTML 注册
register_page(
    key='my-page',          # 唯一标识 (URL)
    label='我的页面',        # 侧边栏显示名
    source='plugin',        # 来源类型
    source_name='my_plugin',
    html='<h1>Hello Panel</h1>',
    icon='settings',        # 侧边栏图标 (可选)
)

# 或指定 HTML 文件
register_page(key='my-page', label='我的页面',
              html_file='/abs/path/to/page.html')


@on_unload
def _cleanup():
    """插件卸载时清理"""
    unregister_page('my-page')

8.1 自定义 HTTP 路由

除了页面, 插件还能注册自己的 HTTP 接口。路径必须以 /api/ext/ 开头(建议用 /api/ext/{插件名}/ 避免冲突)。默认 auth=True 复用后台登录 token; 设 auth=False 则开放免验证(如对外回调、健康检查)。

from aiohttp import web
from core.plugin.web_pages import register_route


# 免验证路由: 任何人可直接访问
@register_route('GET', '/api/ext/myplugin/ping', auth=False)
async def ping(request):
    return web.json_response({'ok': True})


# 需要 token (auth=True 是默认值, 可省略): 请求头带 Authorization: Bearer <token>
@register_route('POST', '/api/ext/myplugin/echo')
async def echo(request):
    body = await request.json()
    return web.json_response({'you_sent': body})
参数 说明
method 'GET' / 'POST' / 'PUT' / 'DELETE'
path 路由路径, 必须以 /api/ext/ 开头 (精确匹配, 不支持路径参数; 可变部分用查询串/请求体传)
handler async def handler(request), 返回 web.json_response(...) / web.Response(...)
auth 是否要求登录 token, 默认 True

路由由 web 层动态查表执行, 插件热重载/卸载即时生效; 插件卸载时框架会自动注销其全部路由, 无需手动清理。也可直接调用 register_route('POST', '/api/ext/x/do', handler) (非装饰器写法)。


9. 配置项与全量环境

bot.yamlnon_at_message 区块控制 "未 @ 机器人" 时的消息处理:

non_at_message:
  enabled: false                 # 是否响应未@消息 (开启后全量正则匹配)
  group_whitelist: []            # 未开启全量时, 仅白名单群触发插件
  ignore_at_other_bot: true      # 忽略仅@其他机器人的消息
  ignore_at_other_user: true     # 忽略仅@其他用户的消息
  ignore_bot_sender: true        # 屏蔽其他机器人发出的消息
  quiet_at_self: false           # @机器人时抑制默认黑名单/维护回复

在 handler 中使用

# 永远响应 (即使全量未开启, 即使未@机器人)
@handler(r'^签到$', ignore_at_check=True)
async def check_in(event, match):
    await event.reply("✅ 签到成功")

# 仅响应 @机器人 的消息 (默认行为)
@handler(r'^菜单$')
async def menu(event, match):
    await event.reply("📋 菜单")

读取自定义配置

from core.base.config import cfg

# 读取当前机器人配置项
value = cfg.get_bot_setting(event.appid, 'message.use_markdown', True)

# 读取全局 settings
port = cfg.get('settings', 'server.port', 5200)

# 获取单个机器人完整配置
bot_cfg = cfg.get_bot_config(event.appid)

# 写入配置
cfg.set_value('bot', 'bots.0.message.use_markdown', False)

# 监听配置变更
def on_bot_changed(new_data):
    print('配置已变更', new_data)
cfg.on_change('bot', on_bot_changed)
# cfg.off_change('bot', on_bot_changed)  # 移除监听

10. 调试与最佳实践

10.1 异常报错

from core.base.logger import get_logger, PLUGIN, report_error

log = get_logger(PLUGIN, '我的插件')

try:
    await risky_operation()
except Exception as e:
    report_error(PLUGIN, '我的插件', e,
                 context={'user_id': event.user_id, 'extra': '...'})
    await event.reply("❌ 操作失败")

超时: 框架对 handler 强制 300 秒超时, 超时会自动取消并记录错误。

10.2 异步规范

# 推荐 — async/await
@handler(r'^test$')
async def test(event, match):
    await event.reply("hi")

# 也支持同步 (会自动跑在 executor 中)
@handler(r'^test$')
def test_sync(event, match):
    import time
    time.sleep(1)
    return  # 同步函数无法 await reply, 应当避免

10.3 命名规范

规则 推荐
handler 函数名 snake_case, 体现功能
name= 参数 中文短名, 用于面板展示
desc= 参数 一句话描述功能
正则锚定 始终使用 ^$ 避免误匹配
资源清理 on_unload 中关闭文件/连接/页面

10.4 性能要点

  • 避免阻塞: 不要在 async handler 中调用同步 IO (用 asyncio.to_thread / run_in_executor)
  • 延迟导入: 体积大的依赖在 handler 内 import, 加快插件加载
  • 冷却限流: 高频指令加 cooldown=N
  • 大型插件: 子模块放 app/ / mod/ 目录, 按需 import

附录: 项目示例插件

路径 功能
plugins/alone/示例插件.py 媒体/ark/按钮/交互回调/引用消息/撤回/主动消息/Web 面板综合示例
plugins/system/main.py 内置系统插件 (信息、管理)

启用/禁用插件: 在 Web 面板「插件」页切换即可, 状态持久化到 data/plugins_disabled.json


反馈与贡献

  • 提交 Issue: 项目 GitHub 仓库
  • 插件市场: Web 面板 → 市场 页面

Happy Coding! 🎉