diff --git a/README.md b/README.md index 902faf3..4ada785 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,255 @@ -# chat_rebot_plugen_support +以下是针对您提供的SDK工具的开发者文档,包含使用说明、工作流程和最佳实践: -聊天机器人插件开发支持 \ No newline at end of file +--- + +# 插件SDK工具文档 + +## 1. 工具概览 + +### 文件结构 + +``` +sdk/ +├── dist/ # 打包输出目录 +├── src/ # 插件源码目录 +│ ├── packages/ # 依赖库目录(自动生成) +│ ├── process.py # 插件主逻辑文件 +│ └── config.toml # 插件配置文件 +├── packup.bat # Windows打包脚本 +├── packup.sh # Linux/macOS打包脚本 +├── dependence.py # 依赖安装工具 +├── package.py # 打包工具 +└── requirements.txt # 依赖声明文件 +``` + +## 2. 快速开始 + +### 2.1 开发流程 + +1. 在 `src/process.py`中开发插件逻辑 +2. 添加依赖到 `requirements.txt` +3. 运行打包脚本: + ```bash + # Windows + ./packup.bat + + # Linux/macOS + chmod +x packup.sh + ./packup.sh + ``` + +### 2.2 示例插件结构 + +`process.py` 最小示例: + +```python +from src.modules.plugin_modules import BasePlugin + +class MyPlugin(BasePlugin): + def after_save(self): + self.ctx.user.send_message("Hello World") + return "ok" +``` + +## 3. SDK工具详解 + +### 3.1 dependence.py + +**功能**:安装依赖到 `src/packages/` + +| 参数 | 说明 | +| ---------------- | -------------------------------- | +| `--no-deps` | 仅安装直接依赖(不含依赖的依赖) | +| `--no-upgrade` | 禁用自动升级 | + +**示例**: + +```bash +python dependence.py --no-deps +``` + +### 3.2 package.py + +**打包规则**: + +1. 自动检测继承 `BasePlugin`的类 +2. 打包生成 `dist/{插件类名小写}.zip` +3. 包含文件: + - `process.py` + - `config.toml` + - `packages/`(依赖目录) + +**错误处理**: + +| 错误码 | 说明 | 解决方案 | +| ------ | -------------------- | --------------- | +| 1 | process.py未找到 | 检查src目录结构 | +| 2 | 未找到BasePlugin子类 | 检查类继承关系 | + +### 3.3 打包脚本 + +**packup.bat/packup.sh** 执行顺序: + +1. 安装依赖(dependence.py) +2. 打包插件(package.py) +3. 输出到dist目录 + +## 4. 测试与调试 + +### 4.1 测试流程 + +使用 `test.py`进行本地测试: + +```python +from sdk.src.process import MyPlugin +from src.modules.plugin_modules import MessageContext + +# 模拟上下文 +ctx = MessageContext(uid="test", gid=None, raw_message="hello", id="bot") + +# 测试插件 +plugin = MyPlugin(ctx) +print(plugin.after_save()) # 应该输出"ok" +``` + +### 4.2 调试技巧 + +```python +# 在process.py中添加调试代码 +import pdb; pdb.set_trace() # 添加断点 + +# 查看可用属性 +print(dir(self.ctx.user)) +``` + +## 5. 最佳实践 + +### 5.1 依赖管理 + +✅ **推荐做法**: + +```text +# requirements.txt 示例 +requests==2.31.0 # 固定版本 +numpy>=1.21.0 # 最低版本限制 +``` + +❌ **应避免**: + +```text +pytorch # 无版本声明 +``` + +### 5.2 配置建议 + +`config.toml`标准结构: + +```toml +[plugin] +name = "my_plugin" +version = "1.0.0" + +[settings] +timeout = 30 +``` + +## 6. 常见问题 + +### Q1: 打包后插件不生效 + +- ✅ 检查类名是否继承 `BasePlugin` +- ✅ 确认ZIP内文件在根目录(不在src子目录) + +### Q2: + +## **7. `self.ctx` 核心对象方法/属性总表** + +#### **1. 基础信息** + +| 属性/方法 | 类型 | 说明 | 示例 | +| --------------- | ----------------- | -------------------------------------- | ------------------------------ | +| `raw_message` | `str` | 用户原始消息文本 | `ctx.raw_message` → "hello" | +| `response` | `Optional[str]` | 可设置的响应内容(设置后拦截后续处理) | `ctx.response = "ok"` | +| `rebot_id` | `str` | 当前机器人ID | `print(ctx.rebot_id)` | +| `_processed` | `bool` | 标记消息是否已被处理(自动管理) | `if ctx._processed: ...` | + +#### **2. 用户相关 (`ctx.user`)** + +| 属性/方法 | 类型 | 说明 | 示例 | +| --------------------------- | ----------------- | --------------------------------------------- | --------------------------------------- | +| `user.user_id` | `str` | 用户唯一ID | `uid = ctx.user.user_id` | +| `user.nickname` | `Optional[str]` | 用户昵称(自动从API获取) | `greet = f"Hi {ctx.user.nickname}"` | +| `user.messages` | `List[dict]` | 用户历史消息记录(需 `after_load`后才有值) | `last_msg = ctx.user.messages[-1]` | +| `user.send_message()` | `method` | **发送私聊消息** | `ctx.user.send_message("Hello")` | +| `user.set_input_status()` | `method` | 设置用户输入状态(如"typing") | `ctx.user.set_input_status("typing")` | + +#### **3. 群组相关 (`ctx.group`)** + +> *仅当消息来自群聊时可用* +> +> | 属性/方法 | 类型 | 说明 | 示例 | +> | ------------------------ | ----------------- | ------------------------------------- | --------------------------------------- | +> | `group.group_id` | `str` | 群组唯一ID | `gid = ctx.group.group_id` | +> | `group.nickname` | `Optional[str]` | 群名称(自动从API获取) | `print(ctx.group.nickname)` | +> | `group.users` | `List[dict]` | 群成员列表 | `members = ctx.group.users` | +> | `group.current_user` | `User` | 当前发言用户(即 `ctx.user`的引用) | `sender = ctx.group.current_user` | +> | `group.send_message()` | `method` | **发送群消息** | `ctx.group.send_message("@all 通知")` | +> | `group.messages` | `List[dict]` | 群聊历史消息 | `last_msg = ctx.group.messages[-1]` | + +注:在群聊消息中current_user中的message存储了用户在群里的近十条消息。 + +#### **4. 数据存储 (`ctx.chat_manager`)** + +| 方法 | 参数 | 说明 | 示例 | +| --------------------------- | --------------------------------------------------------- | -------------------- | ---------------------------------------------------------------------------------- | +| `save_private_message()` | `user: User, role: str, content: str` | 保存私聊消息到数据库 | `ctx.chat_manager.save_private_message(ctx.user, "user", "hi")` | +| `save_group_message()` | `group: Group, role: str, content: str, sender_id: str` | 保存群消息 | `ctx.chat_manager.save_group_message(ctx.group, "user", "hi", ctx.user.user_id)` | +| `load_private_messages()` | `user: User` → `List[dict]` | 加载用户私聊历史 | `ctx.user.messages = ctx.chat_manager.load_private_messages(ctx.user)` | +| `load_group_messages()` | `group: Group` → `List[dict]` | 加载群聊历史 | `ctx.group.messages = ctx.chat_manager.load_group_messages(ctx.group)` | + +#### **5. 插件配置 (`self.config`)** + +| 方法/属性 | 说明 | 示例 | +| ------------------------------- | ---------------------------------------------------- | ------------------------------------------------- | +| `self.config` | **自动加载**的配置字典(来自 `config.toml`) | `timeout = self.config.get("timeout", 30)` | +| `self.save_config()` | 保存修改后的配置 | `self.save_config({"key": "value"})` | +| `self._get_plugin_resource()` | 从插件ZIP包内读取文件 | `data = self._get_plugin_resource("data.json")` | + +--- + +#### **6. 调试工具** + +| 属性 | 类型 | 说明 | +| ------------------- | -------- | ------------------------------------------------------------- | +| `ctx.phase` | `str` | 当前处理阶段(`before_load`/`after_load`/`after_save`) | +| `ctx.intercepted` | `bool` | 是否已被其他插件拦截 | + +### **典型使用场景** + +#### 场景1:消息预处理 + +```python +def before_load(self): + if "admin" in ctx.raw_message: + if not self.check_admin(ctx.user.user_id): + ctx.response = "权限不足" # 拦截请求 + return +``` + +#### 场景2:响应生成 + +```python +def after_save(self): + if "天气" in ctx.raw_message: + city = extract_city(ctx.raw_message) # 自定义解析逻辑 + ctx.response = get_weather(city) # 返回天气信息 +``` + +### **注意事项** + +1. **`ctx.group` 可能为 `None`**:私聊消息时需判空 + ```python + if ctx.group: # 群聊专属逻辑 + ``` +2. **配置热更新**:修改 `self.config` 后需手动调用 `save_config()` +3. **大文件处理**:通过 `_get_plugin_resource()` 读取ZIP内资源,避免解压 diff --git a/dependence.py b/dependence.py new file mode 100644 index 0000000..1f02c32 --- /dev/null +++ b/dependence.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +import os +import sys +import shutil +import subprocess +from pathlib import Path + +def check_requirements_file(requirements_file="requirements.txt"): + """检查 requirements.txt 是否存在""" + if not os.path.exists(requirements_file): + print(f"❌ 错误:未找到 {requirements_file}!请确保它在当前目录。") + print(f"👉 生成 requirements.txt 的方法:`pip freeze > requirements.txt`") + sys.exit(1) + +def install_deps_to_folder( + requirements_file="requirements.txt", + target_dir="package", + upgrade=True, + no_deps=False +): + """ + 安装依赖到目标文件夹 + :param requirements_file: 依赖文件路径 + :param target_dir: 目标目录(默认 package) + :param upgrade: 是否更新已安装的包 + :param no_deps: 是否跳过依赖包(仅安装直接依赖) + """ + target_path = Path(target_dir).resolve() + + # 1. 如果目标文件夹已存在,提醒用户确认覆盖 + if target_path.exists(): + print(f"⚠️ 警告:目标文件夹 {target_path} 已存在!") + choice = input("是否删除并重建?(y/N) ").strip().lower() + if choice != "y": + print("⏹ 用户取消操作") + return False + shutil.rmtree(target_path) + + target_path.mkdir(parents=True) + + # 2. 构造 pip 安装命令 + pip_cmd = [ + sys.executable, "-m", "pip", "install", + "-r", requirements_file, + "--target", str(target_path), + ] + + if upgrade: + pip_cmd.append("--upgrade") + if no_deps: + pip_cmd.append("--no-deps") + + # 3. 执行安装 + print(f"\n📦 正在安装依赖到 {target_path} ...") + result = subprocess.run(pip_cmd, capture_output=True, text=True) + + if result.returncode != 0: + print("\n❌ 安装失败!错误信息:") + print(result.stderr) + return False + + # 4. 成功时打印提示 + print("\n✅ 安装成功!依赖已保存到:") + print(f" → {target_path}\n") + # 5. 打印后续使用说明 + return True + +def main(): + check_requirements_file() + dir = "src" + dir = os.path.join(dir,"packages") + install_success = install_deps_to_folder( + requirements_file="requirements.txt", + target_dir=dir, + upgrade=True, + no_deps=False, # 设为 True 仅安装直接依赖 + ) + + if install_success: + # 检查是否安装了 pip 依赖 + package_size = sum(f.stat().size for f in Path("package").glob("**/*") if f.is_file()) + print(f"📂 安装大小: {package_size / 1024 / 1024:.2f} MB") + else: + print("\n❌ 安装失败!请检查报错信息。") + +if __name__ == "__main__": + main() diff --git a/package.py b/package.py new file mode 100644 index 0000000..d1a32e2 --- /dev/null +++ b/package.py @@ -0,0 +1,62 @@ +import os +import re +import shutil +import ast +from pathlib import Path + +def find_plugin_class(process_file: str) -> str: + """从process.py中找到继承BasePlugin的类名""" + with open(process_file, "r", encoding="utf-8") as f: + content = f.read() + + # 使用AST解析Python代码,比正则更可靠 + tree = ast.parse(content) + + for node in ast.walk(tree): + if isinstance(node, ast.ClassDef): + # 检查是否继承BasePlugin + for base in node.bases: + if isinstance(base, ast.Name) and base.id == "BasePlugin": + return node.name + + raise ValueError("❌ 未找到继承自BasePlugin的类!请检查process.py") + +def create_zip_package(class_name: str): + """打包成ZIP文件,类名全小写""" + src_path = Path("src") + zip_name = class_name.lower() # 类名转全小写作为ZIP名 + temp_dir = Path(f"temp_{zip_name}") # 临时打包目录 + + # 1. 创建临时目录 + temp_dir.mkdir(exist_ok=True) + + # 2. 复制需要的文件 + shutil.copytree(src_path / "packages", temp_dir / "packages") + shutil.copy(src_path / "process.py", temp_dir) + shutil.copy(src_path / "config.toml", temp_dir) + + # 3. 生成ZIP + shutil.make_archive(f"dist/{zip_name}", "zip", temp_dir) + + # 4. 清理临时目录 + shutil.rmtree(temp_dir) + print(f"✅ 打包完成:dist/{zip_name}.zip") + +def main(): + process_file = "src/process.py" + if not os.path.exists(process_file): + print(f"❌ 错误:{process_file} 文件不存在!") + return + + try: + plugin_class = find_plugin_class(process_file) + print(f"找到插件类: {plugin_class} → 包名: {plugin_class.lower()}.zip") + + Path("dist").mkdir(exist_ok=True) # 创建dist目录 + create_zip_package(plugin_class) + + except Exception as e: + print(f"❌ 打包失败:{e}") + +if __name__ == "__main__": + main() diff --git a/packup.bat b/packup.bat new file mode 100644 index 0000000..d0a04b4 --- /dev/null +++ b/packup.bat @@ -0,0 +1,21 @@ +@echo off +REM 依次运行dependence.py和package.py的批处理脚本 + +echo 正在运行依赖安装脚本... +python dependence.py +if %ERRORLEVEL% neq 0 ( + echo 运行dependence.py失败! 错误码: %ERRORLEVEL% + pause + exit /b %ERRORLEVEL% +) + +echo 正在运行打包脚本... +python package.py +if %ERRORLEVEL% neq 0 ( + echo 运行package.py失败! 错误码: %ERRORLEVEL% + pause + exit /b %ERRORLEVEL% +) + +echo 所有脚本执行完毕! +pause diff --git a/packup.sh b/packup.sh new file mode 100644 index 0000000..7d5ed14 --- /dev/null +++ b/packup.sh @@ -0,0 +1,18 @@ +#!/bin/bash +# 依次运行dependence.py和package.py的shell脚本 + +echo "正在运行依赖安装脚本..." +python3 dependence.py +if [ $? -ne 0 ]; then + echo "运行dependence.py失败! 错误码: $?" + exit $? +fi + +echo "正在运行打包脚本..." +python3 package.py +if [ $? -ne 0 ]; then + echo "运行package.py失败! 错误码: $?" + exit $? +fi + +echo "所有脚本执行完毕!" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e69de29 diff --git a/scripts/__init__.py b/scripts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/scripts/file_store_api.py b/scripts/file_store_api.py new file mode 100644 index 0000000..7ad7284 --- /dev/null +++ b/scripts/file_store_api.py @@ -0,0 +1,65 @@ +import json +import os +import logging +import sqlite3 +import os +import time +import toml +from pathlib import Path +from http import HTTPStatus +from datetime import datetime + + +class ConfigManager: + """配置管理类,处理应用配置""" + def __init__(self, config_path="src"): + self.config = {} + self.config_path = config_path + self.build_config_dict() + + + def build_config_dict(self) -> dict[str, str]: + config_dict = {} + for config_file in Path(self.config_path).rglob("*.toml"): + if not config_file.is_file(): + continue + + # 获取相对路径的父目录名 + rel_path = config_file.relative_to(self.config_path) + parent_name = rel_path.parent.name if rel_path.parent.name else None + + if parent_name: + key = parent_name + else: + key = config_file.stem # 去掉扩展名 + + config_dict[key] = str(config_file.absolute()) + self.config = config_dict + + def load_config(self,name): + """加载配置文件""" + if not os.path.exists(self.config[name]): + return {} + with open(self.config[name], 'r', encoding='utf-8') as f: + try: + return toml.load(f) + except toml.TomlDecodeError: + return {} + + def save_config(self, key=None, value=None): + """保存配置项""" + if key is not None and value is not None: + # 如果提供了 key 和 value,则更新单个值 + self.config[key] = value + with open(self.config_path, 'w', encoding='utf-8') as f: + toml.dump(self.config, f) + + def update_config(self, config_dict): + """更新配置字典""" + self.config.update(config_dict) + self.save_config() + + +class ChatManager: + def __init__(self): + return None \ No newline at end of file diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/config.toml b/src/config.toml new file mode 100644 index 0000000..61eb304 --- /dev/null +++ b/src/config.toml @@ -0,0 +1,2 @@ +[test] +message = "helloworld" \ No newline at end of file diff --git a/src/modules/__init__.py b/src/modules/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/plugin_modules.py b/src/modules/plugin_modules.py new file mode 100644 index 0000000..c205635 --- /dev/null +++ b/src/modules/plugin_modules.py @@ -0,0 +1,55 @@ +from abc import ABC, abstractmethod +from typing import Optional +from dataclasses import dataclass +from src.modules import user_module as usermod +import scripts.file_store_api as file_M + +class MessageContext: + """封装消息处理的上下文数据""" + def __init__(self, uid: str, gid: Optional[str], raw_message: str,id:str): + self.raw_message = raw_message + self._processed = False + self.response: Optional[str] = None + # 核心服务实例化 + self.chat_manager = file_M.ChatManager() + self.user = usermod.User(user_id=uid) + self.rebot_id = id + + # 动态加载数据 + if gid: + self.group = usermod.Group(group_id=gid) + self.group.current_user = self.user + else: + self.group = None + + +@dataclass +class PluginPermission: + access_private: bool = False # 允许处理私聊消息 + access_group: bool = True # 允许处理群消息 + read_history: bool = False # 允许读取历史记录 + +from pathlib import Path +import os + +class BasePlugin(ABC): + def __init__(self, ctx: MessageContext): + self.ctx = ctx + self._config_manager = file_M.ConfigManager(self._get_plugin_config_path()) + + @property + def config(self) -> dict: + """直接访问插件配置的快捷方式""" + return self._config_manager.load_config(self.name) + + def _get_plugin_config_path(self) -> str: + """获取插件配置目录路径(config/插件名)""" + plugin_name = self.__class__.__name__.lower() + + return str(Path("config") / plugin_name) + + def save_config(self, config_dict: dict = None) -> bool: + """快捷保存配置""" + if config_dict: + self._config_manager.update_config({"config": config_dict}) + return self._config_manager.save_config() \ No newline at end of file diff --git a/src/modules/user_module.py b/src/modules/user_module.py new file mode 100644 index 0000000..1dc71aa --- /dev/null +++ b/src/modules/user_module.py @@ -0,0 +1,42 @@ +import json +import time + + + +class User: + def __init__(self, user_id): + self.user_id = user_id + self.nickname = "test" + self.messages = [] + self.signal = True + + def set_input_status(self, status): + payload = json.dumps({ + "user_id": self.user_id, + "event_type": status + }) + headers = { + 'Content-Type': 'application/json' + } + while self.signal: + print(f"刷新 {self.nickname} 的输入状态为: {status}") + time.sleep(0.5) + + def send_message(self, message): + print(f"send message{0}".format(message)) + +class Group: + def __init__(self, group_id,user=None,users=None): + self.group_id = group_id + self.current_user = user + self.nickname = "test" + self.users = users + self.get_group_users() + self.messages =[] + + + def get_group_users(self): + return(["user1","user2"]) + + def send_message(self,message): + print(f"send message{0}".format(message)) diff --git a/src/process.py b/src/process.py new file mode 100644 index 0000000..491d179 --- /dev/null +++ b/src/process.py @@ -0,0 +1,13 @@ +import threading +from src.modules.plugin_modules import BasePlugin, MessageContext +import time + + +class Hello_world(BasePlugin): + def before_load(self):#加载消息前 + self.ctx.user.send_message(self.ctx.config.get("test").get("message")) + def after_load(self):#加载消息后 + self.ctx.user.send_message(self.ctx.user.messages) + def after_save(self):#保存用户消息后 + self.ctx.user.send_message("hello_world") + return "ok" \ No newline at end of file diff --git a/test.py b/test.py new file mode 100644 index 0000000..1d920e3 --- /dev/null +++ b/test.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import sys +import os +import importlib +from pathlib import Path +from typing import Type, List +from unittest.mock import Mock + +# 添加src目录到系统路径 + +# 导入基础类 +from src.modules.plugin_modules import MessageContext, BasePlugin + +class PluginTester: + """ + 插件测试助手类 + 创建模拟上下文和测试配置环境 + """ + def __init__(self, plugin_class: Type[BasePlugin], config_dir: str = "test_configs"): + self.plugin_class = plugin_class + self.config_dir = config_dir + os.makedirs(self.config_dir, exist_ok=True) + + # 创建模拟上下文 + self.mock_ctx = MessageContext( + uid="test_user", + gid="test_group", + raw_message="测试消息", + id="test_bot" + ) + + # 创建插件实例 + self.plugin = self.plugin_class(self.mock_ctx) + + def test_before_load(self): + """测试before_load方法(如果存在)""" + if hasattr(self.plugin, 'before_load'): + print(f"\n正在测试 {self.plugin_class.__name__} 的 before_load") + try: + result = self.plugin.before_load() + print(f"before_load 执行成功,返回值: {result}") + return True + except Exception as e: + print(f"before_load 执行出错: {str(e)}") + return False + else: + print(f"\n{self.plugin_class.__name__} 中没有找到 before_load 方法") + return None + + def test_after_load(self): + """测试after_load方法(如果存在)""" + if hasattr(self.plugin, 'after_load'): + print(f"\n正在测试 {self.plugin_class.__name__} 的 after_load") + try: + result = self.plugin.after_load() + print(f"after_load 执行成功,返回值: {result}") + return True + except Exception as e: + print(f"after_load 执行出错: {str(e)}") + return False + else: + print(f"\n{self.plugin_class.__name__} 中没有找到 after_load 方法") + return None + + def test_after_save(self): + """测试after_save方法(如果存在)""" + if hasattr(self.plugin, 'after_save'): + print(f"\n正在测试 {self.plugin_class.__name__} 的 after_save") + try: + result = self.plugin.after_save() + print(f"after_save 执行成功,返回值: {result}") + return True + except Exception as e: + print(f"after_save 执行出错: {str(e)}") + return False + else: + print(f"\n{self.plugin_class.__name__} 中没有找到 after_save 方法") + return None + +def find_plugin_classes(module_path: str) -> List[Type[BasePlugin]]: + """ + 在process.py中查找所有继承自BasePlugin的类 + 返回类类型列表 + """ + plugin_classes = [] + + # 动态导入模块 + module_name = os.path.splitext(os.path.basename(module_path))[0] + spec = importlib.util.spec_from_file_location(module_name, module_path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + # 查找所有继承自BasePlugin的类 + for name, obj in vars(module).items(): + try: + if isinstance(obj, type) and issubclass(obj, BasePlugin) and obj != BasePlugin: + plugin_classes.append(obj) + except TypeError: + continue + + return plugin_classes + +def main(): + # process.py文件路径 + process_path = Path("src/process.py") + + if not process_path.exists(): + print(f"错误: 在 {process_path} 没有找到 process.py") + return + + # 查找process.py中的所有插件类 + plugin_classes = find_plugin_classes(str(process_path)) + + if not plugin_classes: + print("在 process.py 中没有找到插件类") + return + + print(f"发现了 {len(plugin_classes)} 个需要测试的插件类:") + for i, plugin_class in enumerate(plugin_classes, 1): + print(f"{i}. {plugin_class.__name__}") + + # 测试每个插件类 + for plugin_class in plugin_classes: + print(f"\n{'='*50}") + print(f"正在测试插件: {plugin_class.__name__}") + + tester = PluginTester(plugin_class) + + # 按照自然顺序测试生命周期方法 + tester.test_before_load() + tester.test_after_load() + + # 如果需要测试配置保存 + if hasattr(plugin_class, 'after_save'): + tester.test_after_save() + + print(f"{'='*50}\n") + +if __name__ == "__main__": + main()