Skip to content

Commit 07093b6

Browse files
committed
refactor: 创建 plugin_sdk 包,重构插件系统架构
- 创建 plugin_sdk 包,将插件开发 API 独立出来 - 移动 config_types、plugin_base、service_registry、server_bridge 到 plugin_sdk - 移动 shared_types/services 到 plugins/services - 重构配置系统:create_widget 返回 ConfigWidgetBase - 添加服务等待机制 wait_for_service - 重构 history 插件为包形式 - 更新插件管理器 UI:按钮移至工具栏 - 更新 build.bat 和 plugin-dev-tutorial.md
1 parent 5272480 commit 07093b6

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+1708
-1351
lines changed

build.bat

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ echo.
2525
echo [3/3] Copy resources to metaminsweeper\
2626
copy /y "%OUT%\plugin_manager\plugin_manager.exe" "%DEST%\"
2727
xcopy /e /y /i "%OUT%\plugin_manager\_internal" "%DEST%\_internal" >nul
28+
xcopy /e /y /i "src\plugin_sdk" "%DEST%\plugin_sdk" >nul
2829
xcopy /e /y /i "src\plugin_manager" "%DEST%\plugin_manager" >nul
2930
xcopy /e /y /i "src\plugins" "%DEST%\plugins" >nul
3031
xcopy /e /y /i "src\shared_types" "%DEST%\shared_types" >nul

plugin-dev-tutorial.md

Lines changed: 119 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -63,17 +63,19 @@ Meta-Minesweeper 采用 **ZMQ 多进程插件架构**:
6363
├── plugin_manager.exe # 插件管理器
6464
├── plugins/ # 👈 用户插件放这里!
6565
│ ├── my_hello.py # 你的插件(单文件)
66-
│ └── my_complex/ # 或包形式插件
67-
│ ├── __init__.py
68-
│ └── utils.py
69-
├── shared_types/ # 共享类型定义
70-
│ ├── events.py # 事件类型
71-
│ ├── commands.py # 指令类型
66+
│ ├── my_complex/ # 或包形式插件
67+
│ │ ├── __init__.py
68+
│ │ └── utils.py
7269
│ └── services/ # 👈 服务接口定义
7370
│ └── history.py # HistoryService 接口
74-
├── plugin_manager/ # 插件管理器模块
71+
├── plugin_sdk/ # 插件开发 SDK
7572
│ ├── plugin_base.py # 👈 BasePlugin 基类
76-
│ └── service_registry.py # 服务注册表
73+
│ ├── service_registry.py # 服务注册表
74+
│ └── config_types/ # 配置类型
75+
├── shared_types/ # 共享类型定义
76+
│ ├── events.py # 事件类型
77+
│ └── commands.py # 指令类型
78+
├── plugin_manager/ # 插件管理器内部模块
7779
├── user_plugins/ # 备用用户插件目录
7880
├── data/
7981
│ ├── logs/ # 日志输出(自动创建)
@@ -144,6 +146,7 @@ plugins/
144146
### 3.3 自动发现规则
145147

146148
- 文件/目录名以 `_` 开头的会被跳过(如 `_template.py`
149+
- `services` 目录会被跳过(它是服务接口定义,不是插件)
147150
- 单个 `.py` 文件中可以定义多个继承 `BasePlugin` 的类,都会被加载
148151
- 包形式插件中,只有 `__init__.py` 中导出的 `BasePlugin` 子类会被发现
149152

@@ -167,7 +170,7 @@ from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QTextEdit
167170
from PyQt5.QtCore import Qt, pyqtSignal
168171

169172
# 导入插件基类和辅助类型
170-
from plugin_manager import BasePlugin, PluginInfo, make_plugin_icon, WindowMode
173+
from plugin_sdk import BasePlugin, PluginInfo, make_plugin_icon, WindowMode
171174

172175
# 导入可用的事件类型
173176
from shared_types.events import VideoSaveEvent
@@ -466,7 +469,19 @@ if self.has_service(MyService):
466469
pass
467470

468471
# ════════════════════════════════════════
469-
# 3. 获取服务代理(推荐)
472+
# 3. 等待服务就绪(推荐)
473+
# ════════════════════════════════════════
474+
# 如果服务提供者可能在消费者之后初始化,使用 wait_for_service
475+
service = self.wait_for_service(MyService, timeout=10.0)
476+
if service:
477+
# 服务可用
478+
data = service.get_data(123)
479+
else:
480+
# 服务未就绪
481+
self.logger.warning("MyService 未就绪")
482+
483+
# ════════════════════════════════════════
484+
# 4. 获取服务代理(已知服务存在时)
470485
# ════════════════════════════════════════
471486
service = self.get_service_proxy(MyService)
472487

@@ -475,7 +490,7 @@ data = service.get_data(123) # 同步调用,阻塞等待结果
475490
all_data = service.list_data(100) # 超时默认 10 秒
476491

477492
# ════════════════════════════════════════
478-
# 4. 异步调用(非阻塞)
493+
# 5. 异步调用(非阻塞)
479494
# ════════════════════════════════════════
480495
future = self.call_service_async(MyService, "get_data", 123)
481496
# 做其他事情...
@@ -488,7 +503,8 @@ result = future.result(timeout=5.0) # 阻塞等待结果
488503
|------|------|
489504
| `register_service(self, protocol=MyService)` | 注册服务(在 `on_initialized` 中调用) |
490505
| `has_service(MyService)` | 检查服务是否可用 |
491-
| `get_service_proxy(MyService)` | 获取服务代理对象(推荐) |
506+
| `wait_for_service(MyService, timeout=10.0)` | 等待服务就绪并获取代理(推荐) |
507+
| `get_service_proxy(MyService)` | 获取服务代理对象(已知存在时) |
492508
| `call_service_async(MyService, "method", *args)` | 异步调用,返回 Future |
493509

494510
**注意事项:**
@@ -566,7 +582,7 @@ class PluginInfo:
566582
继承 `OtherInfoBase` 并声明配置字段:
567583

568584
```python
569-
from plugin_manager.config_types import (
585+
from plugin_sdk import (
570586
OtherInfoBase, BoolConfig, IntConfig, FloatConfig,
571587
ChoiceConfig, TextConfig, ColorConfig, FileConfig,
572588
PathConfig, LongTextConfig, RangeConfig,
@@ -742,9 +758,9 @@ data/plugin_data/<plugin_name>/config.json
742758
如果预定义的配置类型不满足需求,可以继承 `BaseConfig` 创建自定义类型:
743759

744760
```python
745-
from plugin_manager.config_types.base_config import BaseConfig
746-
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QDial
747-
from PyQt5.QtCore import Qt
761+
from plugin_sdk.config_types import BaseConfig, ConfigWidgetBase, ConfigWidgetWrapper
762+
from PyQt5.QtWidgets import QDial
763+
from typing import Any
748764

749765
class DialConfig(BaseConfig[int]):
750766
"""旋钮配置 → QDial 控件"""
@@ -762,8 +778,8 @@ class DialConfig(BaseConfig[int]):
762778
self.min_value = min_value
763779
self.max_value = max_value
764780

765-
def create_widget(self):
766-
"""创建自定义 UI 控件,返回 (控件, getter, setter, 信号)"""
781+
def create_widget(self) -> ConfigWidgetBase:
782+
"""创建自定义 UI 控件,返回 ConfigWidgetBase 实例"""
767783
widget = QDial()
768784
widget.setRange(self.min_value, self.max_value)
769785
widget.setValue(int(self.default))
@@ -772,14 +788,23 @@ class DialConfig(BaseConfig[int]):
772788
if self.description:
773789
widget.setToolTip(self.description)
774790

775-
# 返回控件、getter、setter、以及 valueChanged 信号
776-
return widget, widget.value, widget.setValue, widget.valueChanged
791+
# 使用 ConfigWidgetWrapper 包装控件
792+
# 参数:控件, getter, setter, 信号
793+
return ConfigWidgetWrapper(
794+
widget,
795+
widget.value, # getter
796+
widget.setValue, # setter
797+
widget.valueChanged # 信号
798+
)
777799

778800
def to_storage(self, value: int) -> int:
779801
return int(value)
780802

781-
def from_storage(self, data) -> int:
782-
return int(data)
803+
def from_storage(self, data: Any) -> int:
804+
try:
805+
return int(data)
806+
except (ValueError, TypeError):
807+
return int(self.default)
783808

784809
# 使用自定义配置类型
785810
class MyConfig(OtherInfoBase):
@@ -792,29 +817,53 @@ class MyConfig(OtherInfoBase):
792817
| 方法/属性 | 说明 |
793818
|-----------|------|
794819
| `widget_type` | 控件类型标识 |
795-
| `create_widget()` | 返回 `(控件, getter, setter, 信号)` 四元组 |
820+
| `create_widget()` | 返回 `ConfigWidgetBase` 实例 |
796821
| `to_storage(value)` | 将值转换为 JSON 可序列化格式 |
797822
| `from_storage(data)` | 从 JSON 数据恢复值 |
798823

799-
**信号对象说明:**
824+
**ConfigWidgetBase 接口:**
825+
826+
`create_widget()` 必须返回一个实现了以下接口的对象:
827+
828+
| 方法/信号 | 说明 |
829+
|-----------|------|
830+
| `get_value() -> Any` | 获取当前值 |
831+
| `set_value(value: Any)` | 设置当前值 |
832+
| `value_change` | `pyqtSignal(object)` 值变化信号 |
800833

801-
信号对象可以是:
802-
- Qt 控件的内置信号(如 `widget.valueChanged``widget.textChanged`
803-
- 自定义 `pyqtSignal`(需要通过 QObject 子类定义)
834+
**使用 ConfigWidgetWrapper:**
835+
836+
对于简单的包装需求,可以使用 `ConfigWidgetWrapper`
804837

805838
```python
806-
# 方式一:使用控件的内置信号
807-
return widget, widget.value, widget.setValue, widget.valueChanged
839+
return ConfigWidgetWrapper(widget, getter, setter, signal)
840+
```
841+
842+
**创建自定义 ConfigWidgetBase 子类:**
808843

809-
# 方式二:自定义信号(复杂控件)
810-
from PyQt5.QtCore import QObject, pyqtSignal
844+
对于复杂控件,可以创建 `ConfigWidgetBase` 的子类:
811845

812-
class MySignal(QObject):
813-
changed = pyqtSignal()
846+
```python
847+
from plugin_sdk.config_types import ConfigWidgetBase
848+
from PyQt5.QtCore import pyqtSignal
814849

815-
signal_emitter = MySignal(parent=container) # parent 防止垃圾回收
816-
# ... 控件变化时调用 signal_emitter.changed.emit()
817-
return container, get_value, set_value, signal_emitter.changed
850+
class MyCustomWidget(ConfigWidgetBase):
851+
"""自定义配置控件"""
852+
853+
# 子类会继承 value_change = pyqtSignal(object) 信号
854+
855+
def __init__(self, parent=None):
856+
super().__init__(parent)
857+
# 创建内部控件...
858+
859+
def get_value(self) -> Any:
860+
"""获取当前值"""
861+
return self._internal_widget.value()
862+
863+
def set_value(self, value: Any) -> None:
864+
"""设置当前值"""
865+
self._internal_widget.setValue(value)
866+
self.value_change.emit(value) # 发射信号
818867
```
819868

820869
---
@@ -842,7 +891,7 @@ from PyQt5.QtWidgets import (
842891
from PyQt5.QtCore import Qt, pyqtSignal, QTimer
843892
from PyQt5.QtGui import QFont
844893

845-
from plugin_manager import BasePlugin, PluginInfo, make_plugin_icon, WindowMode
894+
from plugin_sdk import BasePlugin, PluginInfo, make_plugin_icon, WindowMode
846895
from shared_types.events import VideoSaveEvent, BoardUpdateEvent
847896

848897

@@ -1103,10 +1152,10 @@ def on_initialized(self):
11031152

11041153
#### 1. 定义服务接口
11051154

1106-
`shared_types/services/` 目录下创建接口定义文件:
1155+
`plugins/services/` 目录下创建接口定义文件:
11071156

11081157
```python
1109-
# shared_types/services/my_service.py
1158+
# plugins/services/my_service.py
11101159
from typing import Protocol, runtime_checkable
11111160
from dataclasses import dataclass
11121161

@@ -1126,6 +1175,8 @@ class MyService(Protocol):
11261175
#### 2. 服务提供者
11271176

11281177
```python
1178+
from plugins.services.my_service import MyService, MyData
1179+
11291180
class ProviderPlugin(BasePlugin):
11301181
def on_initialized(self):
11311182
# 注册服务(显式指定 protocol)
@@ -1142,16 +1193,36 @@ class ProviderPlugin(BasePlugin):
11421193
#### 3. 服务使用者
11431194

11441195
```python
1196+
from plugins.services.my_service import MyService, MyData
1197+
11451198
class ConsumerPlugin(BasePlugin):
11461199
def on_initialized(self):
1147-
if self.has_service(MyService):
1148-
# 获取服务代理(推荐)
1149-
self._service = self.get_service_proxy(MyService)
1200+
# 方式一:等待服务就绪(推荐)
1201+
self._service = self.wait_for_service(MyService, timeout=10.0)
1202+
if self._service is None:
1203+
self.logger.warning("MyService 未就绪")
11501204

11511205
def _do_something(self):
1152-
# 调用服务方法(IDE 完整补全,在服务提供者线程执行)
1153-
data = self._service.get_data(123)
1154-
all_data = self._service.list_data(100)
1206+
if self._service:
1207+
# 调用服务方法(IDE 完整补全,在服务提供者线程执行)
1208+
data = self._service.get_data(123)
1209+
all_data = self._service.list_data(100)
1210+
```
1211+
1212+
#### 4. 等待服务就绪
1213+
1214+
如果服务提供者可能在消费者之后初始化,可以使用 `wait_for_service`
1215+
1216+
```python
1217+
def on_initialized(self):
1218+
# 等待服务就绪,最多 10 秒
1219+
service = self.wait_for_service(MyService, timeout=10.0)
1220+
if service:
1221+
# 服务可用
1222+
data = service.get_data(123)
1223+
else:
1224+
# 服务未就绪,可以稍后重试或降级处理
1225+
self.logger.warning("MyService 未就绪")
11551226
```
11561227

11571228
#### 服务调用方式
@@ -1221,10 +1292,10 @@ class ConsumerPlugin(BasePlugin):
12211292
```python
12221293
# ═══ 最小可行插件模板 ═══
12231294

1224-
from plugin_manager import BasePlugin, PluginInfo, make_plugin_icon, WindowMode
1225-
from plugin_manager.config_types import OtherInfoBase, BoolConfig, IntConfig # 可选
1295+
from plugin_sdk import BasePlugin, PluginInfo, make_plugin_icon, WindowMode
1296+
from plugin_sdk import OtherInfoBase, BoolConfig, IntConfig # 配置类型(可选)
12261297
from shared_types.events import VideoSaveEvent # 按需导入
1227-
from shared_types.services.my_service import MyService # 服务接口(可选)
1298+
from plugins.services.my_service import MyService # 服务接口(可选)
12281299

12291300
# ═══ 配置类定义(可选) ═══
12301301
class MyConfig(OtherInfoBase):

src/main.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from utils import get_paths, patch_env
1717

1818
# 插件系统(新)
19-
from plugin_manager import GameServerBridge
19+
from plugin_sdk import GameServerBridge
2020
from plugin_manager.app_paths import get_env_for_subprocess
2121
import subprocess
2222

@@ -56,7 +56,8 @@ def cli_check_file(file_path: str) -> int:
5656
for root, _, files in os.walk(file_path):
5757
for file in files:
5858
if file.endswith((".evf", ".evfs")):
59-
evf_evfs_files.append(os.path.abspath(os.path.join(root, file)))
59+
evf_evfs_files.append(
60+
os.path.abspath(os.path.join(root, file)))
6061

6162
if not evf_evfs_files:
6263
result["error"] = "must be evf or evfs files or directory"
@@ -80,7 +81,8 @@ def cli_check_file(file_path: str) -> int:
8081
checksum = ui.checksum_guard.get_checksum(
8182
video.raw_data[: -(len(video.checksum) + 2)]
8283
)
83-
evf_evfs_files[ide] = (e, 0 if list(video.checksum) == list(checksum) else 1)
84+
evf_evfs_files[ide] = (e, 0 if list(
85+
video.checksum) == list(checksum) else 1)
8486
elif e.endswith(".evfs"):
8587
videos = ms.Evfs(e)
8688
try:
@@ -92,14 +94,16 @@ def cli_check_file(file_path: str) -> int:
9294
evf_evfs_files[ide] = (e, 2)
9395
continue
9496

95-
checksum = ui.checksum_guard.get_checksum(videos[0].evf_video.raw_data)
97+
checksum = ui.checksum_guard.get_checksum(
98+
videos[0].evf_video.raw_data)
9699
if list(videos[0].checksum) != list(checksum):
97100
evf_evfs_files[ide] = (e, 1)
98101
continue
99102

100103
for idcell, cell in enumerate(videos[1:]):
101104
checksum = ui.checksum_guard.get_checksum(
102-
cell.evf_video.raw_data + videos[idcell - 1].checksum
105+
cell.evf_video.raw_data +
106+
videos[idcell - 1].checksum
103107
)
104108
if list(cell.evf_video.checksum) != list(checksum):
105109
evf_evfs_files[ide] = (e, 1)
@@ -114,7 +118,8 @@ def cli_check_file(file_path: str) -> int:
114118
if isinstance(item, tuple) and len(item) == 2
115119
]
116120

117-
output_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), "out.json")
121+
output_file = os.path.join(os.path.dirname(
122+
os.path.abspath(__file__)), "out.json")
118123
with open(output_file, "w", encoding="utf-8") as f:
119124
json.dump(result, f, ensure_ascii=False, indent=2)
120125

@@ -155,8 +160,6 @@ def cli_check_file(file_path: str) -> int:
155160
ui.mainWindow.show()
156161

157162
# ── 启动 ZMQ Server + 插件管理器 ──
158-
game_server = GameServerBridge(ui)
159-
ui.gameServerBridge = game_server
160163

161164
# 打包后直接调用 plugin_manager.exe,开发模式用 python -m
162165
if getattr(sys, 'frozen', False):
@@ -194,10 +197,7 @@ def cli_check_file(file_path: str) -> int:
194197

195198
ui._plugin_process = plugin_process # 保存引用,防止被 GC
196199

197-
# 连接信号:插件发来的新游戏指令 → 主线程处理
198-
game_server.signals.new_game_requested.connect(lambda r, c, m: None) # TODO: 接入游戏逻辑
199-
200-
game_server.start()
200+
GameServerBridge.instance().start()
201201

202202
# _translate = QtCore.QCoreApplication.translate
203203
hwnd = int(ui.mainWindow.winId())
@@ -210,7 +210,7 @@ def cli_check_file(file_path: str) -> int:
210210
)
211211

212212
def _cleanup():
213-
game_server.stop()
213+
GameServerBridge.instance().stop()
214214
if plugin_process is not None and plugin_process.poll() is None:
215215
plugin_process.terminate()
216216
plugin_process.wait(timeout=5)

0 commit comments

Comments
 (0)