@@ -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
792913from plugin_manager import BasePlugin, PluginInfo, make_plugin_icon, WindowMode
793914from shared_types.events import VideoSaveEvent # 按需导入
915+ from shared_types.services.my_service import MyService # 服务接口(可选)
794916
795917class 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
0 commit comments