# XZCLOUD · 插件开发文档

> 版本 v1.0.1 · 本平台支持两类插件：**PHP 插件**（原生）与 **Python 插件**（AstrBot 原生写法，强兼容）
> 每次插件能力变更必须同步更新本文档

## 1. 插件布局

```
plugins/            # PHP 插件
└── <作者>/<插件名>/
    ├── main.php            # 入口（必须）
    ├── metadata.json       # 元数据（必须）
    └── _conf_schema.json   # 配置 schema（可选，后台自动渲染表单）

plugins_py/         # Python 插件（AstrBot 原生写法）
└── <作者>/<插件名>/
    ├── main.py             # 入口（必须）
    ├── metadata.json       # 元数据（必须）
    └── _conf_schema.json   # 配置 schema（可选）
```

创建后在后台「插件管理 → 重新扫描目录」即可看到并启用。也可以打成 zip 在「插件市场」在线安装。

### metadata.json

```json
{
  "name": "HelloWorld",
  "desc": "打招呼示例插件",
  "version": "1.0.0",
  "author": "XZQQ",
  "repo": "https://github.com/your/repo"
}
```

### _conf_schema.json（可选）

```json
{
  "reply_prefix": { "type": "string", "description": "回复前缀", "default": "【XZCLOUD】" },
  "max_len":      { "type": "int",    "description": "最大长度", "default": 100 },
  "enabled_extra":{ "type": "bool",   "description": "附加功能", "default": true }
}
```

支持 `type: string|int|float|bool`，后台「插件管理 → 配置」自动生成表单，保存后热加载。

## 2. PHP 插件

### 2.1 最小示例（`plugins/XZQQ/HelloWorld/main.php`）

```php
<?php
declare(strict_types=1);

use XZQQ\Event\MessageChain;
use XZQQ\Event\MessageEvent;
use XZQQ\Plugin\Filter;
use XZQQ\Plugin\PluginBase;

return new class extends PluginBase {

    #[Filter\Command('hello', alias: ['hi', '你好'],
        description: '打招呼',
        usage: '/hello [名字]')]
    public function onHello(MessageEvent $event): MessageChain
    {
        $name = $event->arg('name', '世界');       // 带参指令自动注入
        return MessageChain::plain($this->config('reply_prefix', '【XZCLOUD】') . " 你好，{$name}！");
    }

    #[Filter\Command('dice')]
    public function onDice(MessageEvent $event): MessageChain
    {
        return MessageChain::plain('🎲 你掷出了 ' . random_int(1, 6) . ' 点');
    }

    #[Filter\Command('admin', permission: 'admin')]
    #[Filter\PermissionType('admin')]
    public function onAdmin(MessageEvent $event): MessageChain
    {
        return MessageChain::plain('管理员专属指令');
    }

    #[Filter\Regex('/天气|下雨|气温/u')]
    public function onWeather(MessageEvent $event): MessageChain
    {
        return MessageChain::plain('今天晴，25℃');
    }

    #[Filter\EventMessageType('GROUP_MESSAGE')]
    public function onGroup(MessageEvent $event): MessageChain
    {
        if (!$event->isAtBot()) return MessageChain::empty();  // 未@不处理
        return MessageChain::at($event->senderId)->plain(' 在的');
    }
};
```

要点：
- `main.php` **return** 一个 `PluginBase` 子类实例（匿名类即可）
- 指令参数：带参方法（除 `MessageEvent` 外的形参）自动从消息文本按顺序注入
- 返回值：`MessageChain`（也可 `void` 不回复）
- `$this->config('key', $default)` 读取 `_conf_schema.json` 配置

### 2.2 过滤器（PHP 属性）

| 属性 | 参数（按序） | 说明 |
|---|---|---|
| `#[Filter\Command]` | `name, alias[], description, usage, needAt, permission` | 指令；前缀 `/ ! ！ # ＃ ． .`；`needAt` 群聊需@ |
| `#[Filter\CommandGroup]` | `name, alias[]` | 指令组（子指令 `group sub`） |
| `#[Filter\Regex]` | `pattern` | 正则匹配，捕获组注入形参 |
| `#[Filter\EventMessageType]` | `GROUP_MESSAGE/PRIVATE_MESSAGE/ALL` | 事件类型过滤 |
| `#[Filter\PlatformAdapterType]` | `napcat/qqofficial/telegram/discord` | 平台过滤 |
| `#[Filter\PermissionType]` | `admin/everyone/...` | 权限过滤 |
| `#[Filter\CustomFilter]` | 自定义过滤器类 | 高级扩展 |

### 2.3 MessageChain 常用构造

```php
MessageChain::plain('文本');
MessageChain::image($url, $name);
MessageChain::video($url, $name);
MessageChain::record($url);                 // 语音
MessageChain::file($url, $name);
MessageChain::face(123);                    // QQ 表情 ID
MessageChain::at($userId);
MessageChain::atAll();
MessageChain::reply($messageId);
MessageChain::empty();                      // 不回复
```

### 2.4 生命周期与调试

- `onLoad(array $meta)`：插件加载时（每次进程启动）
- `onInit()`：配置注入后
- 后台「插件管理」显示 `plugins.error`（加载异常会写入，便于定位）
- CLI：`php bin/xzqq plugin list` 查看已加载插件与指令

## 3. Python 插件（AstrBot 原生写法 · 强兼容）

### 3.1 兼容原理

平台内置 `bridge/shim/astrbot/`，提供与 AstrBot **同名同路径** 的 API 包（`from astrbot.api import star` 等）。桥接进程把它放在 `sys.path[0]`，因此 **AstrBot 插件源码几乎原样可运行**，无需改造、无需安装 AstrBot 依赖。

桥接进程通过 `ws://127.0.0.1:9527/ws?role=bridge&token=<bridge_token>` 与 PHP 守护进程互联：
- 插件加载/卸载/启用态由 PHP 侧 `plugins` 表驱动（3 秒 diff 推送）
- 插件回复经 `result` 帧回传，由 PHP 侧重建消息链并发送

### 3.2 最小示例（`plugins_py/XZQQ/HelloWorldPy/main.py`）

```python
from astrbot.api import star
from astrbot.api.event import filter, AstrMessageEvent

@star.register(name="helloworld_py", desc="打招呼示例", version="1.0.0", author="XZQQ")
class Main(star.Star):
    '''插件主类：类名建议叫 Main；可带 (context) 形参接收上下文'''

    def __init__(self, context):
        super().__init__(context)
        self.prefix = self.context.get_config().get("reply_prefix", "【XZCLOUD】")

    @filter.command("hello", alias={"你好", "hi"})
    async def hello(self, event: AstrMessageEvent, name: str = "世界"):
        '''带参指令：name 自动从指令参数注入；默认值支持'''
        yield event.plain_result(f"{self.prefix} 你好，{name}！")

    @filter.command("ping")
    async def ping(self, event: AstrMessageEvent):
        event.stop_event()            # 阻止后续插件处理
        yield event.plain_result("pong")

    @filter.regex(r"^(?:今天|明天)?天气.*")
    async def weather(self, event: AstrMessageEvent):
        yield event.plain_result("今天晴，25℃")

    @filter.permission_type(filter.PermissionType.ADMIN)
    @filter.command("pyinfo")
    async def pyinfo(self, event: AstrMessageEvent):
        yield event.plain_result("管理员信息")

    async def initialize(self):   # 生命周期：加载完成后调用
        pass

    async def terminate(self):    # 生命周期：卸载/停用时调用
        pass
```

### 3.3 兼容 API 速查

| AstrBot 原生写法 | 本平台状态 |
|---|---|
| `from astrbot.api import star` / `@star.register` | ✅ |
| `from astrbot.api.event import filter, AstrMessageEvent` | ✅ |
| `@filter.command(name, alias={...})` / `command_group` / `regex` | ✅ |
| `@filter.event_message_type(EventMessageType.X)` | ✅ |
| `@filter.platform_adapter_type(PlatformAdapterType.X)` | ✅（napcat→AIOCQHTTP 等已映射） |
| `@filter.permission_type(PermissionType.ADMIN)` | ✅（管理员名单在后台 `settings.admin_ids`） |
| `GreedyStr`（吃掉剩余全部参数） | ✅ |
| `event.send(chain)` / `event.plain_result()` / `event.chain_result()` / `event.make_result()` | ✅ |
| `event.image_result()/at_result()/reply_result()/face_result()` | ✅ |
| `event.stop_event()` / 返回 `EventResultType` | ✅ |
| `from astrbot.core.star...` 等 core 导入路径 | ✅（已做 re-export） |
| 消息组件 `Plain/Image/At/Reply/Face/Node/Json...` | ✅ |
| `initialize()/terminate()` 生命周期 | ✅ |
| `self.context.get_config()` | ✅（读取本插件配置 + 全局配置） |
| LLM / Provider / TTS 等 AI 能力 | ❌ 本平台未内置，需自行实现 HTTP 调用 |
| 自定义 filter 类 | ⚠️ 可运行，但指令无法静态上报到后台 |

### 3.4 指令前缀与参数

- 前缀：`/ ! ！ # ＃ ． .`（与 AstrBot 一致）
- 匹配逻辑与 PHP 侧 `PluginManager::matchCommand()` 完全一致
- 参数按方法签名注入：字符串按空格切分；缺失必填参数则不触发；`GreedyStr` 聚合剩余
- 指令元数据（名称/别名/说明）通过 **AST 静态扫描**在 hello 时上报，后台「指令管理」可见并可同步到 QQ 官方

### 3.5 调试

```bash
cd bridge
python3 astrbot_bridge.py --selftest      # 本地自检：强制加载插件并回放 3 条测试消息
python3 astrbot_bridge.py --verbose       # 前台运行并打印收发帧
```

桥接配置 `bridge/config.json`：`daemon_host/daemon_port/path/bridge_token/plugins_dir/handler_timeout`；令牌须与 PHP 侧 `config/app.php` 的 `plugin.bridge_token` 一致。

## 4. 推荐实践

1. 指令必须写 `description/usage`，后台指令面板与 `/help` 才能正常展示
2. 敏感配置放 `_conf_schema.json`，由后台表单管理，勿硬编码密钥
3. 网络请求加超时（插件 handler 有 30s 总超时，见 `handler_timeout`）
4. 群聊指令建议 `needAt: true`，避免打扰
5. 插件的重活放后台线程/队列，勿阻塞事件循环（Python 侧用 `await`，PHP 侧同步执行需自行控制耗时）

