插件开发文档

学习如何为平台开发功能强大的插件

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. 测试插件

  1. 将插件文件夹放在 plugins/market/MyPlugin/ 目录下
  2. 在机器人管理后台的插件市场中找到你的插件
  3. 点击"安装"按钮安装到机器人
  4. 在群聊中@机器人并发送"你好"测试

📁 插件结构

完整的插件目录结构如下:

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 文件包含两部分:

  1. PHP 处理逻辑:处理表单提交和配置保存
  2. 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
  • 用户体验:保存时显示加载状态,保存后给予明确反馈
  • 文档说明:在配置界面添加使用说明

🔄 插件执行机制

插件执行顺序

当事件触发时,平台会按照以下顺序执行插件:

  1. 读取插件列表:从数据库获取该机器人的所有插件
  2. 排序:按 sort_order 字段升序排列(数字越小越先执行)
  3. 过滤:只执行已启用(enabled=1)的插件
  4. 依次执行:按排序后的顺序依次调用插件处理器
  5. 检查停止标志:如果插件设置了"执行后停止",则不再执行后续插件

设置插件优先级

插件的执行顺序由 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: 插件无法加载?

检查清单:

  1. 确认 plugin.json 格式正确
  2. 确认命名空间与 plugin.json 中的 name 一致
  3. 确认处理器函数名为 group_messageprivate_message
  4. 查看日志文件中的错误信息(logs/ 目录)

Q2: 消息发送失败?

常见原因:

  1. 机器人权限不足
  2. 消息内容包含未配置的URL
  3. msg_seq 重复导致消息去重
  4. 消息内容过长(超过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: 富媒体上传失败?

检查点:

  1. 确认URL可以正常访问
  2. 确认文件格式正确(png/jpg for图片, mp4 for视频, silk for语音)
  3. 确认URL协议为 https://
  4. 确认文件大小不超过限制
// 检查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 格式,大大简化语音消息发送流程