插件开发文档
学习如何为平台开发功能强大的插件
QQ机器人平台插件开发文档
🎯 概述
本平台基于QQ官方API v2,支持插件化扩展,允许开发者创建自定义功能的机器人插件。
核心特性
- 事件驱动:基于QQ官方API v2的事件系统
- 多机器人管理:每个机器人可独立安装插件
- 插件市场:支持插件上传、下载和分享
- 用户等级系统:支持限制用户机器人数量和插件数量
- 富媒体支持:支持发送图片、视频、语音等富媒体消息
- 数据库支持:插件可以直接访问SQLite数据库
- 日志系统:完善的日志记录功能
支持的事件类型
| 事件类型 | 说明 | 处理器文件 | 回调函数名 |
|---|---|---|---|
GROUP_AT_MESSAGE_CREATE |
群@消息 | group_message.php |
group_message |
C2C_MESSAGE_CREATE |
私聊消息 | private_message.php |
private_message |
MESSAGE_CREATE |
频道消息 | channel_message.php |
channel_message |
GROUP_ADD_ROBOT |
机器人加群 | group_join.php |
group_join |
GROUP_DEL_ROBOT |
机器人退群 | group_leave.php |
group_leave |
INTERACTION_CREATE |
回调按钮 | interaction.php |
interaction |
🚀 快速开始
1. 创建插件目录
在 plugins/market/ 目录下创建你的插件文件夹,例如 MyPlugin:
mkdir -p plugins/market/MyPlugin
cd plugins/market/MyPlugin
2. 创建 plugin.json
{
"name": "MyPlugin",
"display_name": "我的插件",
"description": "插件描述",
"version": "1.0.0",
"author": "作者名称",
"category": "工具"
}
必需字段说明:
name: 插件唯一标识(英文,无空格,大驼峰命名)display_name: 显示名称description: 插件描述version: 版本号(语义化版本,如 1.0.0)author: 作者名称category: 分类(工具、娱乐、管理、其他等)
3. 创建群消息处理器
创建 group_message.php 文件:
<?php
namespace MyPlugin;
function group_message($rawData, $botData) {
try {
// 解析事件数据
$data = json_decode($rawData, true);
$eventData = $data['d'] ?? [];
// 提取消息信息
$messageId = $eventData['id'] ?? '';
$content = trim($eventData['content'] ?? '');
$groupId = $eventData['group_openid'] ?? '';
$userId = $eventData['author']['id'] ?? '';
\Utils::log('INFO', "MyPlugin received: {$content} from user {$userId} in group {$groupId}", 'MyPlugin');
// 处理命令
if ($content === '你好') {
sendMessage($groupId, '你好!我是 MyPlugin!', $messageId);
}
} catch (Exception $e) {
\Utils::log('ERROR', "MyPlugin error: " . $e->getMessage(), 'MyPlugin');
}
}
/**
* 发送群消息(辅助函数)
*/
function sendMessage($groupId, $content, $messageId = null) {
global $botData;
if (empty($botData)) {
\Utils::log('ERROR', "Bot data is not available", 'MyPlugin');
return;
}
$result = \BotAPI::sendGroup($botData['appid'], $botData['secret'], $groupId, $content, $messageId);
if ($result && isset($result['code']) && $result['code'] === 0) {
\Utils::log('INFO', "Message sent successfully", 'MyPlugin');
} else {
\Utils::log('ERROR', "Failed to send message: " . json_encode($result), 'MyPlugin');
}
}
4. 测试插件
- 将插件文件夹放在
plugins/market/MyPlugin/目录下 - 在机器人管理后台的插件市场中找到你的插件
- 点击"安装"按钮安装到机器人
- 在群聊中@机器人并发送"你好"测试
📁 插件结构
完整的插件目录结构如下:
MyPlugin/
├── plugin.json # 插件配置文件(必需)
├── README.md # 插件说明文档(推荐)
├── settings.php # 插件配置界面(可选,推荐)
├── group_message.php # 群消息处理器(可选)
├── private_message.php # 私聊消息处理器(可选)
├── channel_message.php # 频道消息处理器(可选)
├── group_join.php # 加群处理器(可选)
├── group_leave.php # 退群处理器(可选)
├── interaction.php # 交互处理器(可选)
└── data/ # 插件数据目录(可选)
└── config.json # 配置文件
命名空间规则
重要:所有插件函数必须使用命名空间,命名空间必须是插件的 name 字段(大驼峰命名)。
<?php
namespace MyPlugin; // 必须与 plugin.json 中的 name 字段一致
/**
* 群消息处理函数
* @param string $rawData 原始JSON数据
* @param array $botData 机器人数据
*/
function group_message($rawData, $botData) {
// 你的代码...
}
🎪 事件处理
事件数据结构
所有事件都遵循QQ官方API v2的数据结构:
{
"op": 0,
"id": "EVENT_ID",
"t": "GROUP_AT_MESSAGE_CREATE",
"d": {
"id": "MESSAGE_ID",
"content": "消息内容",
"group_openid": "GROUP_OPENID",
"author": {
"id": "USER_OPENID",
"member_openid": "MEMBER_OPENID",
"username": "用户名"
},
"timestamp": "2025-01-01T00:00:00+08:00"
}
}
提取事件数据
function group_message($rawData, $botData) {
// 解析原始JSON
$data = json_decode($rawData, true);
// 提取事件数据
$eventData = $data['d'] ?? [];
// 提取消息信息
$messageId = $eventData['id'] ?? '';
$content = trim($eventData['content'] ?? '');
$groupId = $eventData['group_openid'] ?? '';
// 提取用户信息
$author = $eventData['author'] ?? [];
$userId = $author['id'] ?? '';
$username = $author['username'] ?? 'Unknown';
// 提取时间戳
$timestamp = $eventData['timestamp'] ?? '';
// 处理逻辑...
}
$botData 数据
系统会自动传递 $botData 对象,包含:
$botData = [
'id' => 1, // 机器人数据库ID
'appid' => '102170711', // 机器人AppID
'secret' => 'xxx', // 机器人Secret
'name' => '机器人名称', // 机器人名称
'status' => 'active', // 机器人状态 (active/offline)
'owner' => [ // 机器人主人列表(OpenID数组)
'FFBACE6AA0484C10965BAC3A79FDA230',
'another_owner_id'
],
// ... 其他字段
];
💬 消息发送
BotAPI 类
平台提供了 BotAPI 类来简化QQ API调用。
发送群消息
function sendGroupMessage($groupId, $content, $messageId = null) {
global $botData;
if (empty($botData)) {
\Utils::log('ERROR', "Bot data is not available", 'MyPlugin');
return false;
}
$result = \BotAPI::sendGroup(
$botData['appid'], // 机器人AppID
$botData['secret'], // 机器人Secret
$groupId, // 群OpenID
$content, // 消息内容
$messageId // 回复的消息ID(可选)
);
// 检查结果
if ($result && isset($result['code']) && $result['code'] === 0) {
\Utils::log('INFO', "Message sent successfully to group {$groupId}", 'MyPlugin');
return true;
} else {
$errorMsg = $result['msg'] ?? 'Unknown error';
\Utils::log('ERROR', "Failed to send message: {$errorMsg}", 'MyPlugin');
return false;
}
}
发送私聊消息
function sendPrivateMessage($userId, $content) {
global $botData;
if (empty($botData)) {
return false;
}
$result = \BotAPI::sendPrivateMessage(
$botData['appid'],
$botData['secret'],
$userId,
$content
);
return $result && isset($result['code']) && $result['code'] === 0;
}
返回值
所有 BotAPI 方法都返回统一格式:
[
'code' => 0, // 0=成功, -1=失败
'msg' => 'Success', // 消息描述
'data' => [...] // 响应数据(如果成功)
]
🎨 富媒体消息
新增:平台现已提供快捷函数,大大简化富媒体消息发送流程!
图片消息
方式1:快捷函数(推荐)
// 群聊图片
\BotAPI::sendGroupImage(
$botData['appid'],
$botData['secret'],
$groupId,
'https://example.com/image.png',
$msgId // 可选
);
// 私聊图片
\BotAPI::sendPrivateImage(
$botData['appid'],
$botData['secret'],
$userId,
'https://example.com/image.png',
$msgId,
$msgSeq
);
方式2:手动上传+发送(高级用法)
function sendImage($groupId, $imageUrl, $messageId = null) {
global $botData;
if (empty($botData)) {
return false;
}
try {
// 1. 上传图片,获取 file_info
$result = \BotAPI::uploadRichMediaToGroup(
$botData['appid'],
$botData['secret'],
$groupId,
$imageUrl, // 图片URL
1 // 1=图片, 2=视频, 3=语音
);
\Utils::log('INFO', "Image upload response: " . json_encode($result), 'MyPlugin');
if (!isset($result['data']['file_info'])) {
$errorMsg = $result['msg'] ?? '未知错误';
\Utils::log('ERROR', "Image upload failed: " . $errorMsg, 'MyPlugin');
return false;
}
// 2. 发送富媒体消息
$fileInfo = $result['data']['file_info'];
$sendResult = \BotAPI::sendGroupRichMedia(
$botData['appid'],
$botData['secret'],
$groupId,
$fileInfo,
$messageId
);
if ($sendResult['code'] !== 0) {
\Utils::log('ERROR', "Failed to send image message: " . json_encode($sendResult), 'MyPlugin');
return false;
}
return true;
} catch (Exception $e) {
\Utils::log('ERROR', "Send image error: " . $e->getMessage(), 'MyPlugin');
return false;
}
}
视频消息
方式1:快捷函数(推荐)
// 群聊视频
\BotAPI::sendGroupVideo(
$botData['appid'],
$botData['secret'],
$groupId,
'https://example.com/video.mp4', // 必须是mp4格式
$msgId
);
// 私聊视频
\BotAPI::sendPrivateVideo(
$botData['appid'],
$botData['secret'],
$userId,
'https://example.com/video.mp4',
$msgId,
$msgSeq
);
方式2:手动上传+发送(高级用法)
function sendVideo($groupId, $videoUrl, $messageId = null) {
global $botData;
if (empty($botData)) {
return false;
}
try {
// 上传视频
$result = \BotAPI::uploadRichMediaToGroup(
$botData['appid'],
$botData['secret'],
$groupId,
$videoUrl,
2 // 2=视频
);
if (!isset($result['data']['file_info'])) {
return false;
}
// 发送视频
$sendResult = \BotAPI::sendGroupRichMedia(
$botData['appid'],
$botData['secret'],
$groupId,
$result['data']['file_info'],
$messageId
);
return $sendResult['code'] === 0;
} catch (Exception $e) {
\Utils::log('ERROR', "Send video error: " . $e->getMessage(), 'MyPlugin');
return false;
}
}
语音消息
重要更新:平台现已支持自动将 MP3 等音频格式转换为 SILK 格式,开发者无需手动转换!
方式1:快捷函数(推荐,支持自动转换)
// 群聊语音(自动转换MP3为SILK)
\BotAPI::sendGroupAudio(
$botData['appid'],
$botData['secret'],
$groupId,
'https://example.com/audio.mp3', // 支持MP3、WAV等格式,自动转换为SILK
$msgId
);
// 私聊语音(自动转换MP3为SILK)
\BotAPI::sendPrivateAudio(
$botData['appid'],
$botData['secret'],
$userId,
'https://example.com/audio.mp3', // 支持MP3、WAV等格式
$msgId,
$msgSeq
);
// 如果已经是SILK格式,可以直接使用(系统会自动检测,跳过转换)
\BotAPI::sendGroupAudio(
$botData['appid'],
$botData['secret'],
$groupId,
'https://example.com/audio.silk', // SILK格式,不会转换
$msgId
);
// 禁用自动转换(如果明确知道是SILK格式,可关闭转换以提高性能)
\BotAPI::sendGroupAudio(
$botData['appid'],
$botData['secret'],
$groupId,
'https://example.com/audio.silk',
$msgId,
false // 第三个参数设为false,禁用自动转换
);
自动转换特性:
- ✅ 支持 MP3、WAV 等常见音频格式自动转换为 SILK
- ✅ 智能检测:如果 URL 已包含
.silk,自动跳过转换 - ✅ 无缝集成:无需修改现有代码,直接传入音频链接即可
- ✅ 转换失败时返回详细错误信息
方式2:手动上传+发送(高级用法,需要先转换为SILK)
注意:此方式需要先手动将音频转换为SILK格式,然后上传并发送。如果音频不是SILK格式,请先使用 AudioConverter 转换,或直接使用方式1的快捷函数。
function sendAudio($groupId, $audioUrl, $messageId = null) {
global $botData;
if (empty($botData)) {
return false;
}
try {
// 注意:此方式要求音频必须是SILK格式
// 如果音频不是SILK格式,请先使用 AudioConverter::convertToSilk() 转换
// 或者直接使用方式1的快捷函数(自动转换)
// 上传语音(必须是SILK格式)
$result = \BotAPI::uploadRichMediaToGroup(
$botData['appid'],
$botData['secret'],
$groupId,
$audioUrl, // 必须是SILK格式的URL
3 // 3=语音
);
if (!isset($result['data']['file_info'])) {
$errorMsg = $result['msg'] ?? '未知错误';
\Utils::log('ERROR', "Upload failed: {$errorMsg}", 'MyPlugin');
return false;
}
// 发送语音
$sendResult = \BotAPI::sendGroupRichMedia(
$botData['appid'],
$botData['secret'],
$groupId,
$result['data']['file_info'],
$messageId
);
if ($sendResult['code'] !== 0) {
\Utils::log('ERROR', "Send failed: " . json_encode($sendResult), 'MyPlugin');
return false;
}
return true;
} catch (Exception $e) {
\Utils::log('ERROR', "Send audio error: " . $e->getMessage(), 'MyPlugin');
return false;
}
}
方式3:手动转换音频格式(高级用法)
如果需要手动控制转换过程,可以使用 AudioConverter 工具类。通常不需要手动转换,因为方式1的快捷函数已经自动处理了转换。
// 示例:手动转换MP3为SILK,然后发送
function sendAudioWithManualConversion($groupId, $mp3Url, $messageId = null) {
global $botData;
// 检查是否是SILK格式
if (!\AudioConverter::isSilkFormat($mp3Url)) {
// 转换为SILK格式
list($success, $silkUrl, $message) = \AudioConverter::convertToSilk($mp3Url);
if ($success) {
\Utils::log('INFO', "Audio converted: {$silkUrl}", 'MyPlugin');
$mp3Url = $silkUrl; // 使用转换后的URL
} else {
\Utils::log('ERROR', "Conversion failed: {$message}", 'MyPlugin');
return false;
}
}
// 使用转换后的SILK URL发送(禁用自动转换,因为已经是SILK格式)
$result = \BotAPI::sendGroupAudio(
$botData['appid'],
$botData['secret'],
$groupId,
$mp3Url, // 现在已经是SILK格式
$messageId,
false // 禁用自动转换
);
return $result['code'] === 0;
}
// 或者使用智能转换(自动判断是否需要转换)
list($success, $silkUrl, $message) = \AudioConverter::smartConvert($audioUrl);
if ($success) {
// 使用转换后的SILK URL发送语音
\BotAPI::sendGroupAudio($appid, $secret, $groupId, $silkUrl, $msgId, false);
} else {
\Utils::log('ERROR', "Conversion failed: {$message}", 'MyPlugin');
}
AudioConverter 方法说明
| 方法 | 说明 | 返回值 |
|---|---|---|
convertToSilk($audioUrl) |
将音频URL转换为SILK格式 | array [success, silkUrl, message] |
isSilkFormat($url) |
检查URL是否已经是SILK格式 | bool |
smartConvert($audioUrl) |
智能转换:已是SILK则跳过,否则转换 | array [success, silkUrl, message] |
富媒体支持格式
根据QQ官方API文档,支持的格式如下:
| 类型 | 支持格式 | file_type | 说明 |
|---|---|---|---|
| 图片 | png, jpg | 1 | 推荐尺寸不超过 10000x10000 |
| 视频 | mp4 | 2 | 大小不超过100MB,时长不超过60秒 |
| 语音 | silk | 3 | 必须是 silk 格式(QQ语音专用格式) ✨ 新功能:平台支持自动将 MP3/WAV 转换为 SILK |
重要提示:
- 语音格式:QQ官方API文档明确规定语音必须使用 silk 格式(官方文档链接)
- 自动转换:使用
sendGroupAudio()或sendPrivateAudio()时,可以直接传入 MP3 等格式,系统会自动转换为 SILK - 所有媒体URL必须是 HTTPS 协议
- URL必须可以公开访问
- 转换服务基于 oiapi.net API,如果转换失败请检查网络连接
实际应用示例
// 示例1:直接发送MP3链接(推荐,最简单)
\BotAPI::sendGroupAudio(
$botData['appid'],
$botData['secret'],
$groupId,
'https://example.com/song.mp3', // MP3格式,自动转换为SILK
$msgId
);
// 示例2:从TTS API获取音频并发送
function sendTTSMessage($groupId, $text, $messageId = null) {
global $botData;
// 调用TTS API获取MP3
$ttsUrl = "https://tts-api.example.com/speak?text=" . urlencode($text);
// 直接发送,系统自动转换
$result = \BotAPI::sendGroupAudio(
$botData['appid'],
$botData['secret'],
$groupId,
$ttsUrl, // TTS返回的MP3链接,自动转换为SILK
$messageId
);
return $result['code'] === 0;
}
// 示例3:批量发送语音消息
function sendMultipleAudios($groupId, $audioUrls, $messageId = null) {
global $botData;
foreach ($audioUrls as $audioUrl) {
$result = \BotAPI::sendGroupAudio(
$botData['appid'],
$botData['secret'],
$groupId,
$audioUrl, // 支持MP3、WAV、SILK等格式
$messageId
);
if ($result['code'] !== 0) {
\Utils::log('ERROR', "Failed to send audio: {$audioUrl}", 'MyPlugin');
}
// 避免发送过快
usleep(500000); // 0.5秒延迟
}
}
✨ 特殊消息类型
Markdown 消息
Markdown 消息支持丰富的文本格式,包括标题、加粗、链接等。
// 群聊 Markdown
$markdownContent = "# 标题\n这是**加粗**文本\n\n- 列表项1\n- 列表项2";
\BotAPI::sendGroupMarkdown(
$botData['appid'],
$botData['secret'],
$groupId,
$markdownContent,
null, // keyboard(可选)
$msgId
);
// 私聊 Markdown
\BotAPI::sendPrivateMarkdown(
$botData['appid'],
$botData['secret'],
$userId,
$markdownContent,
null, // keyboard(可选)
$msgId,
$msgSeq
);
Markdown + 按钮
可以在 Markdown 消息下方添加交互按钮:
$markdownContent = "# 请选择操作";
$keyboard = [
'content' => [
'rows' => [
[
'buttons' => [
[
'id' => '1',
'render_data' => [
'label' => '按钮1',
'visited_label' => '已点击',
'style' => 1 // 1=蓝色, 0=灰色
],
'action' => [
'type' => 2, // 2=回调指令
'permission' => [
'type' => 2 // 2=所有人可点击
],
'data' => '/命令1'
]
],
[
'id' => '2',
'render_data' => [
'label' => '按钮2',
'visited_label' => '已点击',
'style' => 0
],
'action' => [
'type' => 2,
'permission' => [
'type' => 2
],
'data' => '/命令2'
]
]
]
]
]
]
];
\BotAPI::sendGroupMarkdown(
$botData['appid'],
$botData['secret'],
$groupId,
$markdownContent,
$keyboard,
$msgId
);
ARK 消息
ARK 是一种结构化的卡片消息,适合展示复杂信息:
$ark = [
'template_id' => 23, // ARK模板ID
'kv' => [
['key' => '#DESC#', 'value' => '描述文本'],
['key' => '#PROMPT#', 'value' => '提示文本'],
['key' => '#LIST#', 'obj' => [
['obj_kv' => [
['key' => 'desc', 'value' => '列表项1']
]]
]]
]
];
\BotAPI::sendGroupArk(
$botData['appid'],
$botData['secret'],
$groupId,
$ark,
$msgId
);
Embed 消息
Embed 消息可以展示缩略图、字段等结构化内容:
$embed = [
'title' => '标题',
'prompt' => '提示文本',
'thumbnail' => [
'url' => 'https://example.com/thumbnail.png'
],
'fields' => [
['name' => '字段1', 'value' => '值1'],
['name' => '字段2', 'value' => '值2']
]
];
\BotAPI::sendGroupEmbed(
$botData['appid'],
$botData['secret'],
$groupId,
$embed,
$msgId
);
消息类型对比
| 消息类型 | 适用场景 | msg_type | 支持场景 |
|---|---|---|---|
| 文本消息 | 普通文本交流 | 0 | 群聊、私聊、频道 |
| Markdown | 格式化文本、按钮交互 | 2 | 群聊、私聊、频道 |
| ARK | 结构化卡片展示 | 3 | 群聊、私聊、频道 |
| Embed | 嵌入式内容展示 | 4 | 群聊、私聊、频道 |
| 富媒体 | 图片、视频、语音 | 7 | 群聊、私聊 |
🗄️ 数据库操作
访问数据库
插件可以通过全局的 $db 对象访问SQLite数据库。
global $db;
// 查询单条记录
$user = $db->fetch("SELECT * FROM users WHERE id = :id", ['id' => $userId]);
// 查询多条记录
$bots = $db->fetchAll("SELECT * FROM bots WHERE user_id = :user_id", ['user_id' => $userId]);
// 插入数据
$id = $db->insert('my_table', [
'column1' => 'value1',
'column2' => 'value2'
]);
// 更新数据
$success = $db->update('my_table',
['column1' => 'new_value'],
'id = :id',
['id' => $recordId]
);
// 删除数据
$success = $db->delete('my_table', 'id = :id', ['id' => $recordId]);
常用数据库类
BotDB
// 获取机器人信息
$bot = \BotDB::find($botId);
// 根据 AppID 查找机器人
$bot = \BotDB::findByAppid($appid);
// 获取机器人的所有插件
$plugins = \BotDB::getPlugins($botId);
// 获取机器人主人列表
$owners = \BotDB::getOwners($botId);
// 添加机器人主人
\BotDB::addOwner($botId, $ownerId);
// 删除机器人主人
\BotDB::removeOwner($botId, $ownerId);
// 检查用户是否是机器人主人
function isOwner($botData, $userId) {
$owners = $botData['owner'] ?? [];
return in_array($userId, $owners);
}
UserDB
// 获取用户信息
$user = \UserDB::find($userId);
// 根据邮箱查找用户
$user = \UserDB::findByEmail($email);
// 根据QQ号查找用户
$user = \UserDB::findByQQ($qqNumber);
// 创建用户
$userId = \UserDB::create([
'username' => 'username',
'email' => 'email@example.com',
'password' => 'hashed_password'
]);
// 更新用户
\UserDB::update($userId, ['username' => 'new_username']);
📝 日志记录
Utils::log() 方法
\Utils::log($level, $message, $context = 'plugin');
参数:
$level: 日志级别 (DEBUG, INFO, WARNING, ERROR)$message: 日志消息$context: 日志上下文(建议使用插件名)
示例:
// 记录普通信息
\Utils::log('INFO', 'Plugin loaded successfully', 'MyPlugin');
// 记录消息处理
\Utils::log('INFO', "Processing message: {$content}", 'MyPlugin');
// 记录警告
\Utils::log('WARNING', 'Rate limit approaching', 'MyPlugin');
// 记录错误
\Utils::log('ERROR', 'Failed to send message: ' . $errorMessage, 'MyPlugin');
// 记录调试信息
\Utils::log('DEBUG', 'Bot data: ' . json_encode($botData), 'MyPlugin');
日志文件位置
- 机器人日志:
Log/{appid}/{date}.log - 应用日志:
Log/app_{date}.log - Webhook日志:
logs/webhook_{date}.log - 插件日志:由插件自己指定context
⚙️ 插件配置
settings.php - 可视化配置界面
插件可以提供 settings.php 文件来创建可视化的配置界面,用户可以在后台直接设置插件参数。
基本结构
settings.php 文件包含两部分:
- PHP 处理逻辑:处理表单提交和配置保存
- HTML 界面:展示配置表单和说明
完整示例
重要提示:
- POST请求处理必须放在文件最前面,并在处理前设置JSON响应头
- JavaScript函数应该定义为
window.saveSettings确保全局作用域可用 - 使用
window.pluginSettingsAjaxUrl而不是window.location.href发送AJAX请求 - 文件写入时使用
LOCK_EX标志防止并发写入
<?php
/**
* MyPlugin/settings.php
* 注意:此文件会被系统通过iframe加载,POST请求需要返回JSON
*/
// POST请求处理(必须放在最前面,避免输出HTML)
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// 设置JSON响应头
header('Content-Type: application/json; charset=utf-8');
$action = $_POST['action'] ?? '';
if ($action === 'save_settings') {
$dataDir = __DIR__ . '/data';
// 确保目录存在
if (!file_exists($dataDir)) {
$dirCreated = mkdir($dataDir, 0755, true);
if (!$dirCreated) {
echo json_encode(['success' => false, 'message' => 'data目录创建失败(权限不足)']);
exit;
}
}
// 检查目录写入权限
if (!is_writable($dataDir)) {
echo json_encode(['success' => false, 'message' => 'data目录无写入权限(需设为0755/0775)']);
exit;
}
$configFile = $dataDir . '/config.json';
// 读取现有配置
$config = [];
if (file_exists($configFile)) {
$configContent = file_get_contents($configFile);
$config = json_decode($configContent, true);
if (json_last_error() !== JSON_ERROR_NONE) {
$config = [];
// 备份损坏的配置文件
if (file_exists($configFile)) {
rename($configFile, $configFile . '.bak');
}
}
}
// 获取表单数据并保存
$config['api_key'] = $_POST['api_key'] ?? '';
$config['enable_feature'] = isset($_POST['enable_feature']);
$config['max_items'] = intval($_POST['max_items'] ?? 10);
$jsonContent = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
if ($jsonContent === false) {
echo json_encode(['success' => false, 'message' => '配置含非法字符(无法编码)']);
exit;
}
// 使用LOCK_EX防止并发写入
$writeOk = file_put_contents($configFile, $jsonContent, LOCK_EX);
if ($writeOk === false) {
echo json_encode(['success' => false, 'message' => 'config.json写入失败(权限不足)']);
exit;
}
echo json_encode(['success' => true, 'message' => '设置已保存']);
exit;
}
// 未知的action
echo json_encode(['success' => false, 'message' => '未知的操作']);
exit;
}
// GET请求 - 显示配置界面
$dataDir = __DIR__ . '/data';
$configFile = $dataDir . '/config.json';
// 读取现有配置
$config = [];
if (file_exists($configFile)) {
$configContent = file_get_contents($configFile);
$config = json_decode($configContent, true);
if (json_last_error() !== JSON_ERROR_NONE) {
$config = [];
}
}
// 默认配置
$apiKey = $config['api_key'] ?? '';
$enableFeature = $config['enable_feature'] ?? true;
$maxItems = $config['max_items'] ?? 10;
?>
<!-- 配置界面 HTML -->
<div class="space-y-6">
<!-- API密钥设置 -->
<div class="bg-blue-50 border border-blue-200 rounded-lg p-6">
<h4 class="text-lg font-semibold text-gray-800 mb-4">
<i class="fas fa-key text-blue-600 mr-2"></i>API设置
</h4>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">API密钥</label>
<input type="password" id="apiKeyInput"
value="<?php echo htmlspecialchars($apiKey); ?>"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
placeholder="输入你的API密钥">
</div>
<div>
<label class="flex items-center">
<input type="checkbox" id="enableFeature"
<?php echo $enableFeature ? 'checked' : ''; ?>
class="w-4 h-4 text-blue-600">
<span class="ml-2 text-sm text-gray-700">启用特殊功能</span>
</label>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">最大项目数</label>
<input type="number" id="maxItems"
value="<?php echo $maxItems; ?>"
min="1" max="100"
class="w-full px-3 py-2 border border-gray-300 rounded-lg">
</div>
<button onclick="saveSettings()"
class="w-full bg-blue-600 text-white py-2 rounded-lg hover:bg-blue-700">
<i class="fas fa-save mr-2"></i>保存设置
</button>
</div>
</div>
<!-- 使用说明 -->
<div class="bg-gray-50 border border-gray-200 rounded-lg p-6">
<h4 class="text-lg font-semibold text-gray-800 mb-3">
<i class="fas fa-info-circle text-gray-600 mr-2"></i>使用说明
</h4>
<ul class="list-disc list-inside space-y-2 text-sm text-gray-700">
<li>在上方输入你的API密钥</li>
<li>配置保存后立即生效</li>
<li>如遇问题请查看日志</li>
</ul>
</div>
</div>
<!-- JavaScript 处理 -->
<script>
// 确保函数在全局作用域
window.saveSettings = function(event) {
event = event || window.event;
const apiKeyInput = document.getElementById('apiKeyInput');
if (!apiKeyInput) {
alert('❌ 找不到API密钥输入框');
return;
}
const apiKey = apiKeyInput.value.trim();
if (!apiKey) {
alert('⚠️ 请输入API密钥');
return;
}
const formData = new FormData();
formData.append('action', 'save_settings');
formData.append('api_key', apiKey);
formData.append('enable_feature', document.getElementById('enableFeature').checked ? '1' : '');
formData.append('max_items', document.getElementById('maxItems').value);
// 获取bot_id和plugin_name(可选,如果系统需要)
try {
const urlParams = new URLSearchParams(window.location.search);
const botId = urlParams.get('bot_id') || window.pluginBotId || '';
const pluginName = urlParams.get('plugin_name') || window.pluginName || '';
if (botId) {
formData.append('bot_id', botId);
}
if (pluginName) {
formData.append('plugin_name', pluginName);
}
} catch (e) {
console.warn('Could not get URL params:', e);
}
// 显示加载状态
const btn = event ? (event.target || event.srcElement) : document.querySelector('button[onclick*="saveSettings"]');
const originalText = btn ? btn.innerHTML : '';
if (btn) {
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>保存中...';
}
// 使用正确的AJAX URL(由bot-detail.php注入)
const ajaxUrl = window.pluginSettingsAjaxUrl || window.location.href;
fetch(ajaxUrl, {
method: 'POST',
body: formData
})
.then(response => {
const contentType = response.headers.get('content-type') || '';
if (contentType.includes('application/json')) {
return response.json();
} else {
return response.text().then(text => {
// 检查是否是HTML错误页面
if (text.trim().startsWith('<!DOCTYPE') || text.trim().startsWith('<html')) {
throw new Error('服务器返回了HTML页面,可能是路由错误。请检查插件路径是否正确。');
}
try {
return JSON.parse(text);
} catch (e) {
throw new Error('服务器返回格式错误: ' + text.substring(0, 200));
}
});
}
})
.then(data => {
if (data.success) {
alert('✅ ' + data.message);
console.log('设置保存成功');
} else {
alert('❌ ' + (data.message || '保存失败'));
}
})
.catch(error => {
console.error('Error:', error);
alert('❌ 保存失败: ' + error.message);
})
.finally(() => {
if (btn) {
btn.disabled = false;
btn.innerHTML = originalText;
}
});
};
// 兼容性:也定义在全局作用域
if (typeof saveSettings === 'undefined') {
window.saveSettings = window.saveSettings;
}
</script>
配置文件管理方式
有两种方式管理插件配置:
方式1:使用 JSON 文件(推荐)
// 读取配置
$configFile = __DIR__ . '/data/config.json';
$config = [];
if (file_exists($configFile)) {
$config = json_decode(file_get_contents($configFile), true) ?: [];
}
// 保存配置
file_put_contents($configFile, json_encode($config, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
优点:
- 简单直接,易于调试
- 可以手动编辑配置文件
- 适合单个机器人使用
方式2:使用数据库
global $db;
// 读取配置
$config = $db->fetch(
"SELECT config FROM bot_plugins WHERE bot_id = :bot_id AND plugin_name = :name",
['bot_id' => $botData['id'], 'name' => 'MyPlugin']
);
if ($config && $config['config']) {
$settings = json_decode($config['config'], true);
} else {
$settings = ['key' => 'value'];
}
// 保存配置
$db->update(
'bot_plugins',
['config' => json_encode($settings)],
'bot_id = :bot_id AND plugin_name = :name',
['bot_id' => $botData['id'], 'name' => 'MyPlugin']
);
优点:
- 支持多机器人独立配置
- 配置集中管理
- 适合复杂应用
UI 组件参考
平台使用 Tailwind CSS 和 Font Awesome 图标,以下是常用组件:
输入框
<input type="text"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
placeholder="请输入...">
按钮
<button class="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700">
<i class="fas fa-save mr-2"></i>保存
</button>
复选框
<label class="flex items-center">
<input type="checkbox" class="w-4 h-4 text-blue-600">
<span class="ml-2 text-sm">选项</span>
</label>
单选按钮
<label class="flex items-center p-3 border rounded-lg cursor-pointer hover:bg-gray-50">
<input type="radio" name="option" value="1" class="w-4 h-4">
<span class="ml-3">选项1</span>
</label>
信息卡片
<div class="bg-blue-50 border border-blue-200 rounded-lg p-6">
<h4 class="text-lg font-semibold text-gray-800 mb-4">
<i class="fas fa-info-circle text-blue-600 mr-2"></i>标题
</h4>
<p class="text-sm text-gray-700">内容</p>
</div>
在插件中使用配置
<?php
namespace MyPlugin;
function group_message($rawData, $botData) {
// 读取配置
$configFile = __DIR__ . '/data/config.json';
$config = [];
if (file_exists($configFile)) {
$config = json_decode(file_get_contents($configFile), true) ?: [];
}
// 使用配置
$apiKey = $config['api_key'] ?? '';
if (empty($apiKey)) {
\Utils::log('ERROR', 'API密钥未配置', 'MyPlugin');
return;
}
// 继续处理...
}
最佳实践
- POST请求优先:POST请求处理必须放在文件最前面,并设置正确的响应头
- 数据验证:始终验证用户输入的数据,检查目录和文件权限
- 错误处理:提供清晰的错误提示,包括权限问题、JSON解析错误等
- 文件安全:使用
LOCK_EX标志防止并发写入,备份损坏的配置文件 - 默认值:为所有配置项提供合理的默认值
- 安全性:敏感信息(如API密钥)使用
password类型输入框 - 全局作用域:JavaScript函数应定义为
window.saveSettings确保在iframe中可用 - AJAX URL:使用
window.pluginSettingsAjaxUrl而不是window.location.href - 用户体验:保存时显示加载状态,保存后给予明确反馈
- 文档说明:在配置界面添加使用说明
🔄 插件执行机制
插件执行顺序
当事件触发时,平台会按照以下顺序执行插件:
- 读取插件列表:从数据库获取该机器人的所有插件
- 排序:按
sort_order字段升序排列(数字越小越先执行) - 过滤:只执行已启用(
enabled=1)的插件 - 依次执行:按排序后的顺序依次调用插件处理器
- 检查停止标志:如果插件设置了"执行后停止",则不再执行后续插件
设置插件优先级
插件的执行顺序由 sort_order 字段决定,可以在后台管理界面设置:
- 数字越小,优先级越高,越先执行
- 默认值:0
- 建议范围:0-999
实际应用场景:
// 示例:插件执行顺序
1. PluginHelper (sort_order=0) - 最先执行,提供帮助信息
2. CustomAPI (sort_order=10) - 自定义API调用
3. DeepSeekAI (sort_order=20) - AI聊天
4. OtherPlugin (sort_order=100) - 其他功能
停止后续插件执行
插件可以设置"执行后停止"标志,防止后续插件重复处理相同消息。
工作原理
平台使用全局变量 $plugin_message_sent 来判断插件是否发送了消息:
// 在 webhook.php 中的处理逻辑
global $plugin_message_sent;
$plugin_message_sent = false; // 初始化
// 调用插件处理器
callHandler($handler, $rawData, $eventType, $bot);
// 检查是否停止
if (($botPlugin['stop_after'] ?? 0) == 1 && $plugin_message_sent) {
// 插件发送了消息且设置了停止,不再执行后续插件
break;
}
在插件中设置消息发送标志
平台的 BotAPI 类会自动设置此标志,无需手动处理:
// BotAPI 自动设置
if ($response && isset($response['id'])) {
global $plugin_message_sent;
$plugin_message_sent = true; // 自动标记消息已发送
return [
'code' => 0,
'msg' => 'Success',
'data' => $response
];
}
使用场景
- 菜单式插件:帮助插件处理"/帮助"命令后,不再让其他插件处理
- 权限控制:安全插件拦截非法请求后停止
- 优先响应:某些命令需要特定插件独占处理
注意事项:
- 只有在发送消息后才会停止,如果插件没有发送消息,会继续执行后续插件
- 设置此选项前请确保插件能正确处理所有情况
- 建议只在必要时使用,避免影响其他插件功能
权限验证机制
插件经常需要验证用户权限,平台提供了机器人主人(Owner)机制。
检查用户是否是机器人主人
/**
* 验证用户是否是机器人主人
*
* @param array $botData 机器人数据
* @param string $userId 用户OpenID
* @return bool
*/
function isOwner($botData, $userId) {
$owners = $botData['owner'] ?? [];
return in_array($userId, $owners);
}
// 使用示例
function group_message($rawData, $botData) {
$data = json_decode($rawData, true);
$eventData = $data['d'] ?? [];
$userId = $eventData['author']['id'] ?? '';
$content = trim($eventData['content'] ?? '');
$groupId = $eventData['group_openid'] ?? '';
$msgId = $eventData['id'] ?? '';
// 检查是否是管理员命令
if (strpos($content, '/admin') === 0) {
if (!isOwner($botData, $userId)) {
\BotAPI::sendGroup(
$botData['appid'],
$botData['secret'],
$groupId,
"❌ 只有机器人主人可以使用管理命令",
$msgId
);
return;
}
// 执行管理员操作...
}
}
管理机器人主人
可以通过数据库类管理机器人主人:
// 添加主人
\BotDB::addOwner($botId, $ownerOpenId);
// 删除主人
\BotDB::removeOwner($botId, $ownerOpenId);
// 获取主人列表
$owners = \BotDB::getOwners($botId);
全局变量说明
| 变量名 | 类型 | 说明 | 使用场景 |
|---|---|---|---|
$botData |
array | 当前机器人信息 | 系统自动传递给处理器函数 |
$GLOBALS['botData'] |
array | 全局机器人信息 | 在辅助函数中使用 |
$plugin_message_sent |
bool | 插件是否发送了消息 | 控制插件执行流程 |
$db |
Database | 数据库连接对象 | 执行数据库操作 |
在辅助函数中访问 botData
namespace MyPlugin;
// 主处理器函数(系统自动传递 $botData)
function group_message($rawData, $botData) {
$data = json_decode($rawData, true);
// 调用辅助函数
sendWelcome($data);
}
// 辅助函数(使用全局变量)
function sendWelcome($data) {
global $botData; // 获取全局 botData
if (empty($botData)) {
\Utils::log('ERROR', "Bot data is not available", 'MyPlugin');
return;
}
$groupId = $data['d']['group_openid'] ?? '';
\BotAPI::sendGroup(
$botData['appid'],
$botData['secret'],
$groupId,
"欢迎使用!"
);
}
🏆 最佳实践
1. 错误处理
function group_message($rawData, $botData) {
try {
$data = json_decode($rawData, true);
if (!$data) {
throw new Exception('Invalid JSON data');
}
// 处理逻辑
processMessage($data, $botData);
} catch (Exception $e) {
\Utils::log('ERROR', "MyPlugin error: " . $e->getMessage(), 'MyPlugin');
}
}
2. 性能优化
// 缓存机器人数据
private static $botCache = [];
function getBotData($botId) {
if (!isset(self::$botCache[$botId])) {
self::$botCache[$botId] = \BotDB::find($botId);
}
return self::$botCache[$botId];
}
// 批量处理消息
function processMessages($messages) {
foreach ($messages as $message) {
processMessage($message);
}
}
3. 安全性
// 验证权限
function checkPermission($botData, $userId, $requiredPermission) {
$owners = $botData['owner'] ?? [];
if (!in_array($userId, $owners)) {
\Utils::log('WARNING', "Unauthorized access from {$userId}", 'MyPlugin');
return false;
}
return true;
}
// 过滤危险内容
function sanitizeInput($input) {
$input = strip_tags($input);
$input = htmlspecialchars($input, ENT_QUOTES, 'UTF-8');
return trim($input);
}
4. 代码规范
<?php
/**
* MyPlugin - 插件描述
*
* @author 作者名称
* @version 1.0.0
* @since 2025-01-01
*/
namespace MyPlugin;
/**
* 群消息处理函数
*
* @param string $rawData 原始JSON数据
* @param array $botData 机器人数据
* @return void
*/
function group_message($rawData, $botData) {
// 实现逻辑
}
/**
* 辅助函数:发送消息
*
* @param string $groupId 群OpenID
* @param string $content 消息内容
* @param string|null $messageId 回复的消息ID
* @return bool
*/
function sendMessage($groupId, $content, $messageId = null) {
// 实现逻辑
return true;
}
📚 API参考
BotAPI 方法
sendGroup()
发送群消息
\BotAPI::sendGroup($appid, $secret, $groupId, $content, $messageId = null)
参数:
$appid: 机器人AppID$secret: 机器人Secret$groupId: 群OpenID$content: 消息内容$messageId: 回复的消息ID(可选)
返回值:
['code' => 0, 'msg' => 'Success', 'data' => [...]]
sendPrivateMessage()
发送私聊消息
\BotAPI::sendPrivateMessage($appid, $secret, $userId, $content)
sendGroupRichMedia()
发送富媒体消息
\BotAPI::sendGroupRichMedia($appid, $secret, $groupId, $fileInfo, $messageId = null)
uploadRichMediaToGroup()
上传富媒体文件,获取 file_info
\BotAPI::uploadRichMediaToGroup($appid, $secret, $groupId, $mediaUrl, $fileType = 1)
参数:
$mediaUrl: 媒体URL(必须是HTTPS)$fileType: 1=图片, 2=视频, 3=语音
返回值:
['code' => 0, 'data' => ['file_info' => '...', 'file_uuid' => '...', 'ttl' => 3600]]
getAccessToken()
获取访问令牌
\BotAPI::getAccessToken($appid, $secret)
便捷发送函数(推荐)
以下函数简化了富媒体和特殊消息的发送流程:
图片消息
// 群聊图片
\BotAPI::sendGroupImage($appid, $secret, $groupId, $imageUrl, $msgId = null)
// 私聊图片
\BotAPI::sendPrivateImage($appid, $secret, $userId, $imageUrl, $msgId = null, $msgSeq = 1)
视频消息
// 群聊视频
\BotAPI::sendGroupVideo($appid, $secret, $groupId, $videoUrl, $msgId = null)
// 私聊视频
\BotAPI::sendPrivateVideo($appid, $secret, $userId, $videoUrl, $msgId = null, $msgSeq = 1)
语音消息
// 群聊语音(支持自动转换MP3为SILK)
\BotAPI::sendGroupAudio($appid, $secret, $groupId, $audioUrl, $msgId = null, $autoConvert = true)
// 私聊语音(支持自动转换MP3为SILK)
\BotAPI::sendPrivateAudio($appid, $secret, $userId, $audioUrl, $msgId = null, $msgSeq = 1, $autoConvert = true)
// 参数说明:
// - $audioUrl: 音频URL(支持MP3、WAV、SILK等格式)
// - $autoConvert: 是否自动转换为SILK(默认true,设为false可禁用转换)
音频转换工具
// 将音频转换为SILK格式
list($success, $silkUrl, $message) = \AudioConverter::convertToSilk($audioUrl);
// 智能转换(已是SILK则跳过)
list($success, $silkUrl, $message) = \AudioConverter::smartConvert($audioUrl);
// 检查是否是SILK格式
$isSilk = \AudioConverter::isSilkFormat($audioUrl);
Markdown 消息
// 群聊 Markdown
\BotAPI::sendGroupMarkdown($appid, $secret, $groupId, $content, $keyboard = null, $msgId = null)
// 私聊 Markdown
\BotAPI::sendPrivateMarkdown($appid, $secret, $userId, $content, $keyboard = null, $msgId = null, $msgSeq = 1)
ARK 和 Embed 消息
// ARK 消息
\BotAPI::sendGroupArk($appid, $secret, $groupId, $ark, $msgId = null)
// Embed 消息
\BotAPI::sendGroupEmbed($appid, $secret, $groupId, $embed, $msgId = null)
Utils 方法
log()
记录日志
\Utils::log($level, $message, $context = 'plugin')
参数:
$level: DEBUG, INFO, WARNING, ERROR$message: 日志消息$context: 日志上下文
数据库类
BotDB
BotDB::find($botId)- 获取机器人信息BotDB::findByAppid($appid)- 根据AppID查找机器人BotDB::getOwners($botId)- 获取机器人主人列表BotDB::addOwner($botId, $ownerId)- 添加机器人主人BotDB::removeOwner($botId, $ownerId)- 删除机器人主人
UserDB
UserDB::find($userId)- 获取用户信息UserDB::findByEmail($email)- 根据邮箱查找用户UserDB::findByQQ($qqNumber)- 根据QQ号查找用户
❓ 常见问题
Q1: 插件无法加载?
检查清单:
- 确认
plugin.json格式正确 - 确认命名空间与
plugin.json中的name一致 - 确认处理器函数名为
group_message、private_message等 - 查看日志文件中的错误信息(
logs/目录)
Q2: 消息发送失败?
常见原因:
- 机器人权限不足
- 消息内容包含未配置的URL
msg_seq重复导致消息去重- 消息内容过长(超过10000字节)
解决方案:
$result = \BotAPI::sendGroup($botData['appid'], $botData['secret'], $groupId, $content, $messageId);
if ($result['code'] !== 0) {
\Utils::log('ERROR', "Send failed: " . $result['msg'], 'MyPlugin');
// 根据错误码处理
if (isset($result['data']['code'])) {
switch ($result['data']['code']) {
case 40034102:
// 权限不足
\Utils::log('ERROR', 'Permission denied', 'MyPlugin');
break;
case 40054005:
// 消息去重
\Utils::log('ERROR', 'Message duplicate', 'MyPlugin');
break;
default:
\Utils::log('ERROR', 'Unknown error: ' . $result['msg'], 'MyPlugin');
}
}
}
Q3: 如何调试插件?
// 使用日志记录调试信息
\Utils::log('DEBUG', "Received content: {$content}", 'MyPlugin');
\Utils::log('DEBUG', "Bot data: " . json_encode($botData), 'MyPlugin');
\Utils::log('DEBUG', "Event data: " . json_encode($eventData), 'MyPlugin');
// 在关键位置记录
\Utils::log('INFO', 'Step 1: Parse message', 'MyPlugin');
\Utils::log('INFO', 'Step 2: Check command', 'MyPlugin');
\Utils::log('INFO', 'Step 3: Send response', 'MyPlugin');
Q4: 富媒体上传失败?
检查点:
- 确认URL可以正常访问
- 确认文件格式正确(png/jpg for图片, mp4 for视频, silk for语音)
- 确认URL协议为
https:// - 确认文件大小不超过限制
// 检查URL是否可访问
$headers = @get_headers($mediaUrl);
if ($headers === false || strpos($headers[0], '200') === false) {
\Utils::log('ERROR', "Invalid media URL: {$mediaUrl}", 'MyPlugin');
return false;
}
Q5: 如何实现主人权限验证?
function isOwner($botData, $userId) {
$owners = $botData['owner'] ?? [];
return in_array($userId, $owners);
}
// 使用
if (!isOwner($botData, $userId)) {
sendMessage($groupId, "❌ 只有机器人主人可以使用此功能", $messageId);
return;
}
// 继续执行需要权限的操作...
Q6: 如何使用全局变量 $botData?
function myFunction($groupId, $content) {
global $botData;
if (empty($botData)) {
\Utils::log('ERROR', "Bot data is not available", 'MyPlugin');
return false;
}
// 使用 botData
$appid = $botData['appid'];
$secret = $botData['secret'];
// ...
}
Q7: 如何获取用户的 OpenID?
群聊消息(GROUP_AT_MESSAGE_CREATE):
$data = json_decode($rawData, true);
$eventData = $data['d'] ?? [];
// 用户 OpenID(跨群唯一)
$userId = $eventData['author']['id'] ?? '';
// 群成员 OpenID(群内唯一)
$memberOpenId = $eventData['author']['member_openid'] ?? '';
// 群 OpenID
$groupId = $eventData['group_openid'] ?? '';
私聊消息(C2C_MESSAGE_CREATE):
$data = json_decode($rawData, true);
$eventData = $data['d'] ?? [];
// 用户 OpenID
$userId = $eventData['author']['user_openid'] ?? '';
// 备用方式
$userId = $eventData['author']['id'] ?? '';
Q8: 插件如何实现多语言支持?
namespace MyPlugin;
// 语言配置文件
function getLang($key, $lang = 'zh-CN') {
$translations = [
'zh-CN' => [
'welcome' => '欢迎使用!',
'help' => '帮助信息',
],
'en-US' => [
'welcome' => 'Welcome!',
'help' => 'Help information',
]
];
return $translations[$lang][$key] ?? $key;
}
// 使用
function group_message($rawData, $botData) {
// 从配置读取用户语言设置
$userLang = 'zh-CN'; // 可以从数据库或配置文件读取
$message = getLang('welcome', $userLang);
// 发送消息...
}
Q9: 如何实现插件间通信?
方式1:使用数据库
// 插件A:写入数据
global $db;
$db->insert('plugin_shared_data', [
'key' => 'user_status',
'value' => json_encode(['online' => true]),
'plugin' => 'PluginA'
]);
// 插件B:读取数据
global $db;
$data = $db->fetch(
"SELECT value FROM plugin_shared_data WHERE key = :key",
['key' => 'user_status']
);
$status = json_decode($data['value'], true);
方式2:使用文件系统
// 插件A:写入共享文件
$sharedDir = __DIR__ . '/../shared/';
if (!file_exists($sharedDir)) {
mkdir($sharedDir, 0755, true);
}
file_put_contents($sharedDir . 'shared.json', json_encode(['data' => 'value']));
// 插件B:读取共享文件
$sharedFile = __DIR__ . '/../shared/shared.json';
if (file_exists($sharedFile)) {
$data = json_decode(file_get_contents($sharedFile), true);
}
Q10: 如何处理超长消息?
QQ 消息有长度限制(8000字符),需要分段发送:
function sendLongMessage($groupId, $content, $msgId = null) {
global $botData;
// 最大长度(留一些余量)
$maxLength = 7000;
if (mb_strlen($content) <= $maxLength) {
// 直接发送
\BotAPI::sendGroup($botData['appid'], $botData['secret'], $groupId, $content, $msgId);
return;
}
// 分段发送
$chunks = str_split($content, $maxLength);
$total = count($chunks);
foreach ($chunks as $index => $chunk) {
$message = "[{$index}/{$total}] " . $chunk;
\BotAPI::sendGroup($botData['appid'], $botData['secret'], $groupId, $message, $msgId);
// 避免发送过快
usleep(500000); // 0.5秒延迟
}
}
Q11: 如何实现定时任务?
插件本身不支持定时任务,但可以配合系统 cron 实现:
创建定时任务脚本:
<?php
// plugins/market/MyPlugin/cron.php
require_once __DIR__ . '/../../config/config.php';
// 获取需要通知的群列表
$groups = [
'group_openid_1',
'group_openid_2'
];
// 获取机器人信息
$bot = BotDB::findByAppid('YOUR_APPID');
foreach ($groups as $groupId) {
BotAPI::sendGroup(
$bot['appid'],
$bot['secret'],
$groupId,
'⏰ 定时提醒:该做任务了!'
);
}
echo "Cron job completed\n";
?>
添加到系统 crontab:
# 每天9点执行
0 9 * * * php /path/to/plugins/market/MyPlugin/cron.php
Q12: 插件如何缓存数据提高性能?
namespace MyPlugin;
// 简单的内存缓存
class Cache {
private static $cache = [];
private static $expiry = [];
public static function set($key, $value, $ttl = 3600) {
self::$cache[$key] = $value;
self::$expiry[$key] = time() + $ttl;
}
public static function get($key) {
if (!isset(self::$cache[$key])) {
return null;
}
if (isset(self::$expiry[$key]) && time() > self::$expiry[$key]) {
unset(self::$cache[$key], self::$expiry[$key]);
return null;
}
return self::$cache[$key];
}
public static function clear($key) {
unset(self::$cache[$key], self::$expiry[$key]);
}
}
// 使用示例
function group_message($rawData, $botData) {
$userId = 'user123';
// 尝试从缓存获取
$userData = Cache::get("user_data_{$userId}");
if ($userData === null) {
// 缓存未命中,从数据库读取
global $db;
$userData = $db->fetch("SELECT * FROM users WHERE id = :id", ['id' => $userId]);
// 存入缓存(1小时)
Cache::set("user_data_{$userId}", $userData, 3600);
}
// 使用数据...
}
🔗 相关资源
最后更新:2025年1月31日
文档版本:v2.2.0
新增:音频自动转换功能,支持将 MP3/WAV 等格式自动转换为 SILK 格式,大大简化语音消息发送流程