倾慕慕官机平台 教程中心

插件开发

这里是 倾慕慕官机平台 的全站详细教程。教程按实际使用顺序编写,从账号登录、后台设置、机器人管理,到可视化工作流、节点、变量、按钮、排错全部覆盖。

🚀 插件开发指南

本指南教你如何从零开始开发一个 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插件被禁用时调用,可用于清理资源
register() 是必须实现的核心方法。所有事件监听器都在这里注册。

事件系统

框架提供两种事件类型,通过 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)文本字符串
MarkdownreplyMarkdown($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"
}
QQ Markdown 不是网页 Markdown。实际可用语法受 QQ 官方机器人接口和机器人权限控制;不要直接发送 HTML。连续回复同一事件时不要重复使用同一个 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
原生 keyboard 按钮不是 Markdown 文本的一部分。必须使用 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链接、回调数据或机器人指令
actionType0 跳转链接,1 回调,2 指令
style0 灰色线框,1 蓝色线框
permissionType0 指定用户,1 管理员,2 所有人,3 指定身份组
每行最多 5 个按钮,最多 5 行。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);
模板 37 的大图建议使用接近 975×540 的公网图片。#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"
}
ARK 的 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);
媒体地址必须能被服务器和 QQ 媒体接口访问。内网地址、本机路径、需要 Cookie 登录的地址通常无法上传。

RichMessage 所谓“Markdown + 图片”是什么

RichMessage 不会调用 QQ 媒体上传接口。它只是把公网图片 URL 转换成 QQ Markdown 图片语法 ![img #宽px #高px](URL),然后仍以 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 内容类似:

# 图片预览
![img #640px #360px](https://example.com/photo.png)
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、按钮与媒体回复

开发流程

  1. 编写 PHP 文件,类名格式 XxxPlugin.php,如 HelloPlugin.php
  2. 实现 PluginInterface 全部方法
  3. 在 register() 中注册事件监听器
  4. 测试:本地语法检查 php -l HelloPlugin.php
  5. 上传:用户中心 → 插件管理 → 上传插件
  6. 启用:在插件列表中找到插件,开启启用开关
  7. 测试:在 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 兜底,但不能放宽成所有人可审批。

前置条件:机器人需在 QQ 开放平台开通并订阅 GROUP_JOIN_REQUEST;审批接口还要求机器人本身是群管理员。申请头像不是必返字段,应优先读取官方字段,缺失时使用默认头像或平台代理。