插件开发
这里是 倾慕慕官机平台 的全站详细教程。教程按实际使用顺序编写,从账号登录、后台设置、机器人管理,到可视化工作流、节点、变量、按钮、排错全部覆盖。
🚀 插件开发指南
本指南教你如何从零开始开发一个 QQBot 插件。插件是 PHP 类,需要实现 PluginInterface 接口,上传到平台后即可启用。
最简插件模板
以下是一个能回复"你好"的完整插件代码,保存为 HelloPlugin.php:
<?php
declare(strict_types=1);
namespace QQBot\Plugin;
use QQBot\Core\EventDispatcher;
use QQBot\Core\Logger;
use QQBot\Events\C2CMessageEvent;
use QQBot\Events\GroupAtMessageEvent;
use QQBot\Message\TextMessage;
class HelloPlugin implements PluginInterface
{
private Logger $logger;
public function getName(): string { return 'hello'; }
public function getDisplayName(): string { return '你好插件'; }
public function getDescription(): string { return '一个简单的打招呼插件'; }
public function getVersion(): string { return '1.0.0'; }
public function getAuthor(): string { return '你的名字'; }
public function getIcon(): ?string { return '👋'; }
public function getTags(): array { return ['打招呼','示例']; }
public function register(EventDispatcher \$dispatcher, Logger \$logger): void
{
\$this->logger = \$logger;
// 监听群聊 @机器人 消息
\$dispatcher->on(GroupAtMessageEvent::class, function (GroupAtMessageEvent \$event): void {
\$content = trim(\$event->getContent());
if (\$content === '你好') {
\$event->reply(new TextMessage('你好呀!👋'));
}
}, 50);
// 监听私聊消息
\$dispatcher->on(C2CMessageEvent::class, function (C2CMessageEvent \$event): void {
\$content = trim(\$event->getContent());
if (\$content === '你好') {
\$event->reply(new TextMessage('你好!有什么可以帮你的吗?'));
}
}, 50);
}
public function enable(): void {}
public function disable(): void {}
}
插件接口方法说明
| 方法 | 返回值 | 说明 |
|---|---|---|
| getName() | string | 插件唯一英文标识,如 hello、daily_sign |
| getDisplayName() | string | 插件中文显示名,如"你好插件" |
| getDescription() | string | 插件功能描述,会显示在插件列表 |
| getVersion() | string | 语义化版本号,如 1.0.0 |
| getAuthor() | string | 插件作者名 |
| getIcon() | string|null | 插件图标(emoji 或 URL),可返回 null |
| getTags() | array | 标签数组,如 ['签到','经验'],方便分类筛选 |
| register() | void | 核心方法:注册事件监听器,框架在加载插件时调用 |
| enable() | void | 插件被启用时调用,可用于初始化资源 |
| disable() | void | 插件被禁用时调用,可用于清理资源 |
事件系统
框架提供两种事件类型,通过 EventDispatcher 注册监听:
| 事件类 | 触发场景 | 获取消息 | 发送回复 |
|---|---|---|---|
| C2CMessageEvent | 用户私聊机器人 | getContent() | reply() |
| GroupAtMessageEvent | 群聊中 @机器人 | getContent() | reply() |
监听器注册
\$dispatcher->on(事件类::class, function (事件类 \$event): void {
// 处理逻辑
}, 优先级);
- 优先级:数字越小越先执行,范围 1-100,一般写 50
- 多个插件监听同一事件时,按优先级顺序执行
事件对象常用方法
| 方法 | 说明 |
|---|---|
| getContent() | 获取用户发送的纯文本内容(已去除 @ 和多余空格) |
| getGroupOpenid() | 获取群 openid(仅群聊事件) |
| getUserOpenid() / getMemberOpenid() | 获取用户 openid |
| getTimestamp() | 获取消息时间戳 |
| getAttachments() | 获取消息附件(图片、文件等) |
| getPayload() | 获取原始事件数据(高级用法) |
| reply(MessageInterface \$msg) | 回复消息 |
消息类型与回复
群聊 GroupAtMessageEvent 和单聊 C2CMessageEvent 提供相同的快捷回复方法。优先使用快捷方法;框架会自动补充当前消息的 msg_id/event_id 和递增的 msg_seq。
| 内容 | 推荐方法 | 参数 |
|---|---|---|
| 文本 | replyText($content) | 文本字符串 |
| Markdown | replyMarkdown($markdown) | QQ Markdown 字符串 |
| ARK 卡片 | replyArk($templateId, $kv) | 官方模板 ID 和 KV 列表 |
| 图片 | sendImage($url) | 公网可访问的图片 URL |
| 视频 | sendVideo($url) | 公网可访问的视频 URL |
| 语音 | sendAudio($url) | 公网可访问的音频 URL |
| 文件 | sendFile($url, $fileName) | 公网 URL;文件名可选 |
普通 Markdown:格式和发送
replyMarkdown() 接收一个完整的 QQ Markdown 字符串,不接收数组,也不需要开发者自己填写 msg_type。
| 效果 | Markdown 写法 |
|---|---|
| 一级标题 | # 标题 |
| 二级标题 | ## 标题 |
| 粗体 | **重要内容** |
| 斜体 | *提示内容* |
| 删除线 | ~~旧内容~~ |
| 无序列表 | - 项目 |
| 有序列表 | 1. 项目 |
| 引用 | > 引用内容 |
| 行内代码 | `code` |
| 链接 | [显示文字](https://example.com) |
| 换行 | PHP 字符串中的 \n |
$markdown = <<<'MD' # 查询结果 **状态:** 正常 - 用户:张三 - 积分:128 > 数据更新于 2026-07-22 [打开帮助](https://qq.qmkjcm.cn/doc/) MD; $event->replyMarkdown($markdown);
以上调用最终生成的 QQ API 核心请求结构如下。msg_id/event_id 与 msg_seq 由事件对象自动补充:
{
"msg_type": 2,
"markdown": {
"content": "# 查询结果
**状态:** 正常"
},
"msg_seq": 1,
"msg_id": "当前消息 ID"
}
msg_seq,省略第二个参数即可让框架自动递增。Markdown 蓝色文字指令链接
这种效果是 Markdown 正文中的可点击蓝色文字,不是消息下方的原生 keyboard 按钮。点击后由 QQ 客户端把指定指令填入输入框,或直接发送给机器人。
[农场查询](mqqapi://aio/inlinecmd?command=%E5%86%9C%E5%9C%BA%E6%9F%A5%E8%AF%A2&enter=false&reply=false)
| 参数 | 说明 |
|---|---|
command | 点击后填入或发送的机器人指令,必须进行 URL 编码 |
enter=false | 只把指令填入输入框,由用户确认后发送 |
enter=true | 点击后尝试直接发送;是否生效取决于 QQ 客户端和机器人权限 |
reply=false | 发送时不引用当前消息 |
reply=true | 发送时尝试引用当前消息 |
PHP 中不要手写中文的百分号编码,应使用 rawurlencode():
$command = '农场查询';
$url = 'mqqapi://aio/inlinecmd?command=' . rawurlencode($command)
. '&enter=false&reply=false';
$markdown = "# QQ农场
"
. "[农场查询]({$url})|"
. "[农场土地](mqqapi://aio/inlinecmd?command="
. rawurlencode('农场土地')
. "&enter=false&reply=false)";
$event->replyMarkdown($markdown);
推荐封装成一个辅助函数,避免每个指令重复拼接:
function commandLink(string $label, string $command, bool $enter = false, bool $reply = false): string
{
$query = http_build_query([
'command' => $command,
'enter' => $enter ? 'true' : 'false',
'reply' => $reply ? 'true' : 'false',
], '', '&', PHP_QUERY_RFC3986);
return '[' . $label . '](mqqapi://aio/inlinecmd?' . $query . ')';
}
$markdown = "# 功能菜单
"
. commandLink('农场信息', '农场信息') . '|'
. commandLink('农场土地', '农场土地') . '|'
. commandLink('农场背包', '农场背包');
$event->replyMarkdown($markdown);
command 必须 URL 编码。不要把用户输入直接拼入链接;需要动态参数时先校验长度和允许字符。mqqapi:// 是 QQ 客户端协议,只能在支持该能力的 QQ Markdown 消息中使用,普通浏览器不会打开。蓝色 QQ 原生 keyboard 按钮
原生按钮显示在 Markdown 消息下方。KeyboardBuilder::addButton() 的第 4 个参数是样式:1 为蓝色文字/蓝色线框按钮,0 为灰色按钮。
use QQBot\Message\KeyboardBuilder;
use QQBot\Message\KeyboardMarkdownMessage;
$keyboard = (new KeyboardBuilder())
// label, data, actionType, style
->addButton('农场信息', '农场信息', 2, 1)
->addButton('农场土地', '农场土地', 2, 1)
->newRow()
->addButton('打开文档', 'https://qq.qmkjcm.cn/doc/?s=plugin_dev', 0, 1)
->build();
$event->reply(new KeyboardMarkdownMessage(
"# QQ农场
请选择下面的功能",
$keyboard
));
上例中前两个按钮的 actionType=2,点击后触发机器人指令;“打开文档”的 actionType=0,点击后跳转链接。最后的 style=1 表示蓝色样式。
| 需要的效果 | 写法 |
|---|---|
| 正文中的蓝色可点击文字 | Markdown 链接 + mqqapi://aio/inlinecmd |
| 消息下方蓝色原生按钮 | KeyboardBuilder,并设置 style=1 |
| 点击填入指令 | 文字链接使用 enter=false;原生按钮使用 actionType=2 |
| 点击打开网页 | 普通 Markdown HTTPS 链接;原生按钮使用 actionType=0 |
KeyboardMarkdownMessage 发送;只调用 replyMarkdown() 不会附带 keyboard。带 QQ 原生按钮的 Markdown
replyMarkdown() 只发送 Markdown 正文。需要按钮时,使用 KeyboardBuilder 构建键盘,再发送 KeyboardMarkdownMessage。
use QQBot\Message\KeyboardBuilder;
use QQBot\Message\KeyboardMarkdownMessage;
$keyboard = (new KeyboardBuilder())
->addButton('查看菜单', '/菜单', 2, 1)
->addButton('打开文档', 'https://qq.qmkjcm.cn/doc/', 0, 1)
->newRow()
->addButton('签到', '/签到', 2, 0)
->build();
$event->reply(new KeyboardMarkdownMessage(
"# 功能菜单
请选择下面的操作",
$keyboard
));
| addButton 参数 | 说明 |
|---|---|
label | 按钮上显示的文字 |
data | 链接、回调数据或机器人指令 |
actionType | 0 跳转链接,1 回调,2 指令 |
style | 0 灰色线框,1 蓝色线框 |
permissionType | 0 指定用户,1 管理员,2 所有人,3 指定身份组 |
newRow() 结束当前行;最后一行不需要再调用。按钮回调能否使用取决于机器人端是否配置对应回调处理。QQ 官方 ARK 模板卡片
ARK 是 QQ 官方模板卡片,不是 Markdown。它使用 msg_type=3,核心数据由模板 ID 和 kv 列表组成。插件可直接调用群聊或单聊事件的 replyArk():
$event->replyArk(37, $kv);
| 模板 | 用途 | 主要字段 |
|---|---|---|
23 | 链接或文本列表 | #DESC#、#PROMPT#、#LIST# |
24 | 文本详情 + 缩略图 | #DESC#、#PROMPT#、#TITLE#、#METADESC#、#IMG#、#LINK#、#SUBTITLE# |
37 | 标题 + 副标题 + 大图 | #PROMPT#、#METATITLE#、#METASUBTITLE#、#METACOVER#、#METAURL# |
模板 37:大图卡片
$kv = [
['key' => '#PROMPT#', 'value' => '今日推荐'],
['key' => '#METATITLE#', 'value' => '插件开发文档'],
['key' => '#METASUBTITLE#', 'value' => '查看完整 SDK 和示例'],
['key' => '#METACOVER#', 'value' => 'https://example.com/cover.jpg'],
['key' => '#METAURL#', 'value' => 'https://qq.qmkjcm.cn/doc/?s=plugin_dev'],
];
$event->replyArk(37, $kv);
#METAURL# 可省略;填写时跳转域名必须在 QQ 开放平台报备。模板 24:缩略图详情卡片
$kv = [
['key' => '#DESC#', 'value' => '查询结果'],
['key' => '#PROMPT#', 'value' => '天气提醒'],
['key' => '#TITLE#', 'value' => '银川今日天气'],
['key' => '#METADESC#', 'value' => '晴,最高温度 28℃'],
['key' => '#IMG#', 'value' => 'https://example.com/weather.png'],
['key' => '#LINK#', 'value' => 'https://example.com/weather'],
['key' => '#SUBTITLE#', 'value' => '天气服务'],
];
$event->replyArk(24, $kv);
模板 23:文本与链接列表
模板 23 的 #LIST# 使用 obj,每一项包含一个 obj_kv 列表。每行的 link 可省略,省略后就是普通文本。
$kv = [
['key' => '#DESC#', 'value' => '功能菜单'],
['key' => '#PROMPT#', 'value' => '请选择功能'],
[
'key' => '#LIST#',
'obj' => [
['obj_kv' => [
['key' => 'desc', 'value' => '打开插件文档'],
['key' => 'link', 'value' => 'https://qq.qmkjcm.cn/doc/?s=plugin_dev'],
]],
['obj_kv' => [
['key' => 'desc', 'value' => '当前状态:运行正常'],
]],
],
],
];
$event->replyArk(23, $kv);
replyArk() 生成的 QQ API 核心请求结构如下,当前消息 ID 和序号仍由事件对象自动补充:
{
"msg_type": 3,
"ark": {
"template_id": 37,
"kv": [
{"key": "#PROMPT#", "value": "今日推荐"},
{"key": "#METATITLE#", "value": "插件开发文档"}
]
},
"msg_seq": 1,
"msg_id": "当前消息 ID"
}
kv 必须是从 0 开始的 PHP 列表数组,每项必须包含 key,并包含 value 或 obj。模板字段名必须保留前后的 #。图片需要公网直连;未报备的跳转域名可能出现接口成功但卡片不展示。机器人账号是否可用某个 ARK 模板以 QQ 官方当前权限为准。图片、视频、语音和文件
媒体 URL 不能直接传给 ImageMessage、VideoMessage、AudioMessage 或 FileMessage。这些消息类需要的是 QQ 上传接口返回的 file_info。最简单的方式是使用事件快捷方法,它会自动完成“上传 + 发送”。
$event->sendImage('https://example.com/photo.png');
$event->sendVideo('https://example.com/video.mp4');
$event->sendAudio('https://example.com/audio.mp3');
$event->sendFile('https://example.com/report.pdf', 'report.pdf');
需要复用 file_info 或自行控制发送时,可以拆成两步:
$fileInfo = $event->uploadImage('https://example.com/photo.png');
$event->replyImage($fileInfo);
RichMessage 所谓“Markdown + 图片”是什么
RichMessage 不会调用 QQ 媒体上传接口。它只是把公网图片 URL 转换成 QQ Markdown 图片语法 ,然后仍以 msg_type=2 的 Markdown 消息发送。
use QQBot\Message\RichMessage;
$msg = new RichMessage();
$image = $msg->addImage('https://example.com/photo.png', 640, 360);
$msg->setMarkdown("# 图片预览
" . $image);
$event->reply($msg);
上例最终 Markdown 内容类似:
# 图片预览 
RichMessage 是兼容性能力,不是普通图片消息,也不是“Markdown 请求体里附带 file_info”。图片 URL 必须公网直连,并且是否显示、尺寸提示是否生效取决于 QQ 当前 Markdown 能力。需要稳定发送图片时,请使用 sendImage();需要文字和图片时,建议先 replyMarkdown(),再 sendImage(),由框架自动递增 msg_seq。Logger 日志
插件中使用 \$this->logger 记录日志,方便排查问题:
\$this->logger->info('用户发送了消息', ['content'=>\$content]);
\$this->logger->warning('配置未设置,使用默认值');
\$this->logger->error('请求接口失败', ['error'=>\$e->getMessage()]);
日志可在后台管理 → 日志页面查看。
插件数据存储
插件如需持久化存储数据,推荐使用 data/ 目录:
// 数据目录 \$dataDir = __DIR__ . '/../../data/plugin_yourname/'; if (!is_dir(\$dataDir)) mkdir(\$dataDir, 0755, true); // 保存 JSON 数据 file_put_contents(\$dataDir . 'config.json', json_encode(\$config)); // 读取 JSON 数据 \$config = json_decode(file_get_contents(\$dataDir . 'config.json'), true) ?? [];
安全约束
上传插件时会进行安全扫描,以下操作被禁止:
- 代码执行:eval、assert、create_function、call_user_func
- 命令执行:system、exec、shell_exec、passthru、反引号
- 反射调用:ReflectionFunction、ReflectionMethod
- 文件写入:file_put_contents、fwrite、unlink、rename、copy、mkdir
- 目录遍历:scandir、glob、opendir
- 网络外联:curl_exec、fsockopen、stream_socket_client
- 配置操作:phpinfo、ini_set
- 读取配置文件:禁止包含 .env、config.php、database.php
- 动态 include:禁止使用变量进行 include/require
完整示例:打卡签到插件
查看 /plugins/DailySignPlugin.php 作为参考,它演示了:
- 多事件监听(群聊+私聊)
- 命令解析(签到、排行榜、抽奖等)
- JSON 文件数据存储
- Markdown、按钮与媒体回复
开发流程
- 编写 PHP 文件,类名格式
XxxPlugin.php,如HelloPlugin.php - 实现 PluginInterface 全部方法
- 在 register() 中注册事件监听器
- 测试:本地语法检查
php -l HelloPlugin.php - 上传:用户中心 → 插件管理 → 上传插件
- 启用:在插件列表中找到插件,开启启用开关
- 测试:在 QQ 群中发送触发词验证
常见问题
为什么上传后插件没生效?
检查插件是否已启用、文件名是否符合 XxxPlugin.php 格式、PHP 语法是否正确、是否有语法错误日志。
为什么机器人不回复?
确认机器人在目标群中、插件已启用、触发词匹配正确、插件 register() 中注册了对应的事件类型。
插件能调用外部 API 吗?
可以。使用 file_get_contents() 发起 HTTP 请求。注意:curl_exec 被安全策略禁止,请用 file_get_contents 或 stream_context_create。
// GET 请求
\$json = file_get_contents('https://api.example.com/data');
// POST 请求
\$ctx = stream_context_create([
'http' => ['method'=>'POST','header'=>'Content-Type: application/json','content'=>json_encode(\$data)]
]);
\$json = file_get_contents('https://api.example.com/action', false, \$ctx);
插件可以定时执行吗?
可以。SDK 提供 QQBot\Plugin\PluginScheduler,任务持久化保存,由服务器每分钟自动执行,机器人重启后仍然存在。支持单次、每 N 分钟、每天、每周、每月,消息支持文本、Markdown、媒体、TTS、指令等格式。
use QQBot\Plugin\PluginScheduler;
$botId = $event->getClient()->getCurrentBotId();
$groupId = $event->getGroupOpenid();
// 每天08:00向当前群发送消息
$taskId = PluginScheduler::daily(
$this->getName(), $botId, 'group', $groupId, '08:00',
'text', ['content' => '早上好,今天也要元气满满!']
);
$tasks = PluginScheduler::tasks($this->getName(), $botId);
PluginScheduler::cancel($this->getName(), $botId, $taskId);
daily() 都创建独立任务;多次开启同一群的同一时段,会多次发送。cancel() 只取消传入ID对应的一条任务,返回 false 时应检查状态和机器人归属。开启前检查、关闭时清理
插件应按机器人ID、群ID和业务时间识别任务;开启前先检查已有任务,存在则复用而不是无条件新增。下面的示例仅清理当前机器人、当前群的每天08:00和21:30两个时段,避免误删本插件在其他时段的任务:
use QQBot\Plugin\PluginScheduler;
$botId = $event->getClient()->getCurrentBotId();
$groupId = $event->getGroupOpenid();
foreach (PluginScheduler::tasks($this->getName(), $botId) as $task) {
if ($task['target_type'] !== 'group' || $task['target_id'] !== $groupId
|| $task['repeat_type'] !== 'daily'
|| !in_array(substr($task['run_at'], 11, 5), ['08:00', '21:30'], true)
|| !in_array($task['status'], ['pending', 'paused', 'failed'], true)) {
continue;
}
PluginScheduler::cancel($this->getName(), $botId, (int)$task['id']);
}
不要只取消配置文件里最后保存的ID;多次开启可能覆盖旧ID,但旧任务仍会继续运行。任务会持久化,机器人重启不会自动清理。tasks() 按ID倒序最多返回最近200条,不能保证覆盖更早的历史任务;若任务量较大或已有大量遗留任务,应联系平台管理员排查和清理,不能宣称上述示例已清理全量。任务正在 running 时,cancel() 不会取消已开始的这一次执行;遇到并发开启,也需要插件自行控制重复创建。插件本地配置应同时按机器人ID和群ID隔离。
单次任务使用 once(..., '2026-08-03 12:30:00', ...);每 N 分钟可使用 minutely(..., 5, ...),其中 5 表示每 5 分钟,范围 1 到 1440;每周和每月分别使用 weekly()、monthly()。目标类型为 group 或 c2c。插件必须属于该账号且已在目标机器人上启用,执行时平台会再次校验权限。
本节更新时间:2026-09-24 22:12(中国标准时间)
多个插件能同时工作吗?
可以。每个插件独立运行,互不干扰。同一事件可以被多个插件监听,按优先级依次执行。
插件主动执行官方群管理
插件可以通过 OfficialGroupManager 主动执行撤回、禁言、解除禁言以及审批入群申请。这不是通知事件。
use QQBot\Plugin\OfficialGroupManager;
$dispatcher->on(GroupAtMessageEvent::class, function (GroupAtMessageEvent $event): void {
$manager = OfficialGroupManager::fromEvent($event);
$group = $event->getGroupOpenid();
// 撤回当前触发消息
$manager->recallMessage($group, $event->getEventId());
// 禁言 10 分钟 / 解除禁言
$member = $event->getMemberOpenid();
$manager->muteMember($group, $member, 10);
$manager->unmuteMember($group, $member);
});审批入群
$manager = OfficialGroupManager::fromEvent($event);
$list = $manager->getJoinRequests($groupOpenid, '', 20);
foreach (($list['list'] ?? []) as $request) {
$member = (string)$request['member_openid'];
$requestId = (string)($request['join_request_id'] ?? '');
$manager->approveJoinRequest($groupOpenid, $member, $requestId);
// 或拒绝:
// $manager->rejectJoinRequest($groupOpenid, $member, $requestId, '验证答案错误', false);
}GROUP_JOIN_REQUEST 入群申请事件
平台现已支持 QQ 官方 GROUP_JOIN_REQUEST 回调,并分发为 OfficialGroupManageEvent。实测可用链路是:事件提醒 → 拉取申请列表 → 使用手动审批接口通过或拒绝。
use QQBot\Events\OfficialGroupManageEvent;
$dispatcher->on(OfficialGroupManageEvent::class, function ($event) {
if ($event->getEventType() !== 'GROUP_JOIN_REQUEST') return;
$payload = $event->getPayload();
});手动审批使用 GET /v2/groups/{group_openid}/join_request_list 和 POST /v2/groups/{group_openid}/approval_join_request/{member_openid}。/v2/groups/join_approval_strategy 是自动审批策略接口,不是点击按钮同意某个申请的接口。
Markdown 按钮可通过 KeyboardBuilder 将权限设为 permissionType=1(仅管理员)。后端仍必须复核点击者身份;如果按钮回调缺少 member_role,可用平台“主人系统”OpenID 兜底,但不能放宽成所有人可审批。