Skip to content

Commit 4bd326d

Browse files
committed
feat: 实现插件间服务通讯机制
新增功能: - ServiceRegistry: 线程安全的服务注册表 - HistoryService: 历史记录服务接口(Protocol + GameRecord) - BasePlugin 服务相关方法: - register_service(): 注册服务(自动验证 Protocol 实现) - has_service(): 检查服务存在 - get_service_proxy(): 获取服务代理(IDE 友好) - call_service_async(): 异步调用服务 设计特点: - 服务方法在服务提供者线程执行,线程安全 - 使用 Protocol 定义接口,IDE 可完整推断类型 - 返回 frozen dataclass,保证数据不可变 - shutdown 时自动注销服务 - GUI 回调添加异常保护 文档更新: - plugin-dev-tutorial.md: 添加服务通讯说明 - 目录结构: 添加 shared_types/services 目录
1 parent 1387163 commit 4bd326d

File tree

7 files changed

+999
-63
lines changed

7 files changed

+999
-63
lines changed

plugin-dev-tutorial.md

Lines changed: 135 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,14 @@ Meta-Minesweeper 采用 **ZMQ 多进程插件架构**:
6565
│ └── my_complex/ # 或包形式插件
6666
│ ├── __init__.py
6767
│ └── utils.py
68+
├── shared_types/ # 共享类型定义
69+
│ ├── events.py # 事件类型
70+
│ ├── commands.py # 指令类型
71+
│ └── services/ # 👈 服务接口定义
72+
│ └── history.py # HistoryService 接口
73+
├── plugin_manager/ # 插件管理器模块
74+
│ ├── plugin_base.py # 👈 BasePlugin 基类
75+
│ └── service_registry.py # 服务注册表
6876
├── user_plugins/ # 备用用户插件目录
6977
├── data/
7078
│ ├── logs/ # 日志输出(自动创建)
@@ -435,7 +443,57 @@ self.run_on_gui(some_function, arg1, arg2, keyword_arg=value)
435443

436444
两种方式的底层原理相同 —— 都是通过 QueuedConnection 将调用投递到 Qt 主线程的事件循环。
437445

438-
### 5.5 日志记录
446+
### 5.5 服务通讯 API(插件间调用)
447+
448+
插件间通过**服务接口**进行类型安全的调用,服务方法会在**服务提供者线程**执行,线程安全。
449+
450+
```python
451+
# ════════════════════════════════════════
452+
# 1. 注册服务(服务提供者)
453+
# ════════════════════════════════════════
454+
def on_initialized(self):
455+
# 注册服务,显式指定 Protocol 类型
456+
self.register_service(self, protocol=MyService)
457+
458+
# ════════════════════════════════════════
459+
# 2. 检查服务是否存在
460+
# ════════════════════════════════════════
461+
if self.has_service(MyService):
462+
# 服务可用
463+
pass
464+
465+
# ════════════════════════════════════════
466+
# 3. 获取服务代理(推荐)
467+
# ════════════════════════════════════════
468+
service = self.get_service_proxy(MyService)
469+
470+
# 调用服务方法(IDE 完整补全,在服务提供者线程执行)
471+
data = service.get_data(123) # 同步调用,阻塞等待结果
472+
all_data = service.list_data(100) # 超时默认 10 秒
473+
474+
# ════════════════════════════════════════
475+
# 4. 异步调用(非阻塞)
476+
# ════════════════════════════════════════
477+
future = self.call_service_async(MyService, "get_data", 123)
478+
# 做其他事情...
479+
result = future.result(timeout=5.0) # 阻塞等待结果
480+
```
481+
482+
**服务相关方法:**
483+
484+
| 方法 | 说明 |
485+
|------|------|
486+
| `register_service(self, protocol=MyService)` | 注册服务(在 `on_initialized` 中调用) |
487+
| `has_service(MyService)` | 检查服务是否可用 |
488+
| `get_service_proxy(MyService)` | 获取服务代理对象(推荐) |
489+
| `call_service_async(MyService, "method", *args)` | 异步调用,返回 Future |
490+
491+
**注意事项:**
492+
- 服务方法在**服务提供者线程**执行,调用方无需关心线程安全
493+
- **死锁风险**:不要让两个插件互相调用对方的服务
494+
- 服务接口中不要暴露删除等敏感操作
495+
496+
### 5.6 日志记录
439497

440498
```python
441499
# 每个 BasePlugin 实例都有绑定好的 logger,直接使用即可
@@ -449,7 +507,7 @@ self.logger.error("错误信息")
449507
# <data_dir>/logs/plugin_manager.log (主日志)
450508
```
451509

452-
### 5.6 PluginInfo 配置项
510+
### 5.7 PluginInfo 配置项
453511

454512
```python
455513
@dataclass
@@ -730,11 +788,74 @@ def on_initialized(self):
730788

731789
### Q4: 插件之间如何通信?
732790

733-
目前插件间没有直接的通信 API。间接方式:
791+
插件间通过**服务接口(Protocol)**进行类型安全的通讯。
792+
793+
#### 1. 定义服务接口
794+
795+
`shared_types/services/` 目录下创建接口定义文件:
796+
797+
```python
798+
# shared_types/services/my_service.py
799+
from typing import Protocol, runtime_checkable
800+
from dataclasses import dataclass
801+
802+
@dataclass(frozen=True, slots=True)
803+
class MyData:
804+
"""数据类型(frozen 保证不可变,线程安全)"""
805+
id: int
806+
name: str
807+
808+
@runtime_checkable
809+
class MyService(Protocol):
810+
"""服务接口定义"""
811+
def get_data(self, id: int) -> MyData | None: ...
812+
def list_data(self, limit: int = 100) -> list[MyData]: ...
813+
```
814+
815+
#### 2. 服务提供者
734816

735-
- **通过主进程中转**:插件 A 发送 Command → 主进程处理 → 触发 Event → 插件 B 收到
736-
- **通过文件系统**:插件 A 写文件到公共目录 → 插件 B 定时轮询(不推荐)
737-
- **共享 ZMQ Client**:未来可能会支持插件间自定义频道
817+
```python
818+
class ProviderPlugin(BasePlugin):
819+
def on_initialized(self):
820+
# 注册服务(显式指定 protocol)
821+
self.register_service(self, protocol=MyService)
822+
823+
# 实现服务接口方法
824+
def get_data(self, id: int) -> MyData | None:
825+
return self._db.query(id)
826+
827+
def list_data(self, limit: int = 100) -> list[MyData]:
828+
return self._db.query_all(limit)
829+
```
830+
831+
#### 3. 服务使用者
832+
833+
```python
834+
class ConsumerPlugin(BasePlugin):
835+
def on_initialized(self):
836+
if self.has_service(MyService):
837+
# 获取服务代理(推荐)
838+
self._service = self.get_service_proxy(MyService)
839+
840+
def _do_something(self):
841+
# 调用服务方法(IDE 完整补全,在服务提供者线程执行)
842+
data = self._service.get_data(123)
843+
all_data = self._service.list_data(100)
844+
```
845+
846+
#### 服务调用方式
847+
848+
| 方法 | 说明 | 推荐 |
849+
|------|------|------|
850+
| `get_service_proxy(MyService)` | 获取代理对象,方法调用在提供者线程执行 ||
851+
| `call_service_async(MyService, "method", *args)` | 异步调用,返回 Future | 高级用法 |
852+
| `has_service(MyService)` | 检查服务是否可用 | - |
853+
854+
#### 注意事项
855+
856+
- **死锁风险**:不要让两个插件互相调用对方的服务
857+
- **线程安全**:服务方法在提供者线程执行,调用方无需关心
858+
- **删除接口**:不要在服务接口中暴露删除等敏感操作
738859

739860
### Q5: 最佳实践清单
740861

@@ -791,6 +912,7 @@ def on_initialized(self):
791912

792913
from plugin_manager import BasePlugin, PluginInfo, make_plugin_icon, WindowMode
793914
from shared_types.events import VideoSaveEvent # 按需导入
915+
from shared_types.services.my_service import MyService # 服务接口(可选)
794916

795917
class MyPlugin(BasePlugin):
796918

@@ -811,6 +933,13 @@ class MyPlugin(BasePlugin):
811933

812934
def on_initialized(self): # 可选:耗时初始化
813935
pass # self.data_dir 可存放数据
936+
937+
# 注册服务(如果是服务提供者)
938+
# self.register_service(self, protocol=MyService)
939+
940+
# 获取服务代理(如果是服务使用者)
941+
# if self.has_service(MyService):
942+
# self._service = self.get_service_proxy(MyService)
814943

815944
def on_shutdown(self): # 可选:资源清理
816945
pass

src/plugin_manager/event_dispatcher.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515

1616
import loguru
1717

18+
from .service_registry import ServiceRegistry
19+
1820
if TYPE_CHECKING:
1921
from .plugin_base import BasePlugin
2022

@@ -39,11 +41,29 @@ class EventDispatcher:
3941
- dispatch() 不阻塞:将事件投递到各插件的队列,立即返回
4042
- 异常隔离通过各插件线程自行处理
4143
- 背压控制:队列满时丢弃事件并记录警告
44+
- 服务注册:管理插件间服务通讯
4245
"""
4346

4447
def __init__(self):
4548
self._handlers: dict[str, list[HandlerEntry]] = defaultdict(list)
4649
self._lock = threading.RLock()
50+
# 服务注册表(用于插件间通讯)
51+
self._service_registry = ServiceRegistry()
52+
53+
@property
54+
def services(self) -> ServiceRegistry:
55+
"""
56+
获取服务注册表
57+
58+
用法::
59+
60+
# 注册服务
61+
dispatcher.services.register(HistoryService, provider, "history")
62+
63+
# 获取服务
64+
history = dispatcher.services.get(HistoryService)
65+
"""
66+
return self._service_registry
4767

4868
def subscribe(
4969
self,

0 commit comments

Comments
 (0)