Skip to content

Commit fc916e1

Browse files
committed
feat: 实现控制授权系统和跨线程命令调度
- 添加 ControlAuthorizationManager 管理控制命令授权 - 每个控制命令类型只能授权给一个插件 - 支持权限变更通知和持久化 - GameServerBridge 使用 Qt 信号槽自动调度到主线程 - 支持同步请求返回值和异步命令 - 拖出 tab 时自动进入拖拽模式 - 添加测试插件 test_control_a 和 test_control_b - 更新插件开发教程文档
1 parent 07093b6 commit fc916e1

File tree

11 files changed

+1084
-13
lines changed

11 files changed

+1084
-13
lines changed

plugin-dev-tutorial.md

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,96 @@ result = self.request(some_query_command, timeout=5.0)
385385
| `NewGameCommand` | 开始新游戏 | `rows`, `cols`, `mines` |
386386
| `MouseClickCommand` | 模拟鼠标点击 | `row`, `col`, `button`, `modifiers` |
387387

388-
### 5.4 线程安全的 GUI 更新(重要!)
388+
### 5.4 控制授权系统(重要!)
389+
390+
为了防止多个插件同时发送冲突的控制指令,系统实现了**控制授权机制**
391+
392+
- 每个**控制命令类型**只能授权给**一个插件**
393+
- 未获得授权的插件发送该命令会被拒绝
394+
- 授权变更时会通知相关插件
395+
396+
#### 声明需要的控制权限
397+
398+
`PluginInfo` 中通过 `required_controls` 字段声明:
399+
400+
```python
401+
from shared_types.commands import NewGameCommand, MouseClickCommand
402+
403+
class MyPlugin(BasePlugin):
404+
405+
@classmethod
406+
def plugin_info(cls) -> PluginInfo:
407+
return PluginInfo(
408+
name="my_plugin",
409+
description="需要控制权限的插件",
410+
required_controls=[NewGameCommand], # 👈 声明需要的控制权限
411+
)
412+
```
413+
414+
#### 检查和响应授权状态
415+
416+
```python
417+
class MyPlugin(BasePlugin):
418+
419+
def on_initialized(self) -> None:
420+
# 检查当前是否有权限
421+
has_auth = self.has_control_auth(NewGameCommand)
422+
self.logger.info(f"NewGameCommand 权限: {has_auth}")
423+
424+
# 更新 UI 状态
425+
self.run_on_gui(self._update_ui_auth, has_auth)
426+
427+
def on_control_auth_changed(
428+
self,
429+
command_type: type,
430+
granted: bool,
431+
) -> None:
432+
"""
433+
控制权限变更回调
434+
435+
Args:
436+
command_type: 命令类型
437+
granted: True 表示获得权限,False 表示失去权限
438+
"""
439+
if command_type == NewGameCommand:
440+
if granted:
441+
self.logger.info("获得了 NewGameCommand 控制权限")
442+
else:
443+
self.logger.warning("失去了 NewGameCommand 控制权限")
444+
# 停止正在进行的操作
445+
self._stop_auto_play()
446+
447+
# 更新 UI
448+
self.run_on_gui(self._update_ui_auth, granted)
449+
450+
def _on_button_click(self) -> None:
451+
# 发送前可以检查权限(不检查也行,无权限时 send_command 会自动拒绝)
452+
if self.has_control_auth(NewGameCommand):
453+
self.send_command(NewGameCommand(rows=16, cols=30, mines=99))
454+
else:
455+
self.logger.warning("没有 NewGameCommand 权限")
456+
```
457+
458+
#### 控制授权相关方法
459+
460+
| 方法 | 说明 |
461+
|------|------|
462+
| `has_control_auth(command_type)` | 检查是否有该控制类型的权限 |
463+
| `on_control_auth_changed(cmd_type, granted)` | 权限变更回调(覆写) |
464+
| `PluginInfo.required_controls` | 声明需要的控制权限 |
465+
466+
#### 用户授权操作
467+
468+
用户通过插件管理器工具栏的 **"🔐 控制授权"** 按钮管理授权:
469+
470+
1. 点击按钮打开授权对话框
471+
2. 选择要授权的控制类型
472+
3. 从下拉列表中选择插件(只显示声明了该控制权限的插件)
473+
4. 确认后生效
474+
475+
授权配置会持久化到 `data/control_authorization.json`
476+
477+
### 5.5 线程安全的 GUI 更新(重要!)
389478

390479
> **为什么需要跨线程机制?**
391480
>
@@ -1295,6 +1384,7 @@ def on_initialized(self):
12951384
from plugin_sdk import BasePlugin, PluginInfo, make_plugin_icon, WindowMode
12961385
from plugin_sdk import OtherInfoBase, BoolConfig, IntConfig # 配置类型(可选)
12971386
from shared_types.events import VideoSaveEvent # 按需导入
1387+
from shared_types.commands import NewGameCommand # 控制命令(可选)
12981388
from plugins.services.my_service import MyService # 服务接口(可选)
12991389

13001390
# ═══ 配置类定义(可选) ═══
@@ -1312,6 +1402,7 @@ class MyPlugin(BasePlugin):
13121402
window_mode=WindowMode.TAB, # TAB / DETACHED / CLOSED
13131403
icon=make_plugin_icon("#1976D2", "M"),
13141404
other_info=MyConfig, # 👈 绑定配置类(可选)
1405+
required_controls=[NewGameCommand], # 👈 声明控制权限(可选)
13151406
)
13161407

13171408
def _setup_subscriptions(self) -> None:
@@ -1336,11 +1427,19 @@ class MyPlugin(BasePlugin):
13361427
# 访问配置值(可选)
13371428
# if self.other_info:
13381429
# max_count = self.other_info.max_count
1430+
1431+
# 检查控制权限(可选)
1432+
# has_auth = self.has_control_auth(NewGameCommand)
13391433

13401434
def on_shutdown(self): # 可选:资源清理
13411435
pass
13421436

1437+
def on_control_auth_changed(self, cmd_type: type, granted: bool):
1438+
"""控制权限变更回调(可选覆写)"""
1439+
pass
1440+
13431441
def _handle_event(self, event):
13441442
self.logger.info(f"收到事件: {event}") # 用 logger 不用 print
13451443
# self.run_on_gui(gui_func, *args) # GUI 更新走这
1444+
# self.send_command(NewGameCommand(...)) # 发送控制命令
13461445
```

src/lib_zmq_plugins/server/zmq_server.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,8 @@ def _dispatch(self, client_id: bytes, cmd: BaseCommand) -> None:
167167
if isinstance(tag, type):
168168
tag = tag.__name__
169169
tag = str(tag)
170+
171+
self._log.info("[Server] 收到命令: tag=%s, request_id=%s", tag, cmd.request_id)
170172

171173
if tag == "__sync__":
172174
self._handle_sync(client_id, cmd)
@@ -179,6 +181,7 @@ def _dispatch(self, client_id: bytes, cmd: BaseCommand) -> None:
179181

180182
try:
181183
result = handler(cmd)
184+
self._log.info("[Server] handler 执行完成: tag=%s, result=%s", tag, result)
182185
except Exception as e:
183186
self._log.error("Handler error for %s: %s", tag, e, exc_info=True)
184187
if cmd.request_id:

src/main.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
# 插件系统(新)
1919
from plugin_sdk import GameServerBridge
2020
from plugin_manager.app_paths import get_env_for_subprocess
21+
from shared_types.commands import NewGameCommand
2122
import subprocess
2223

2324
os.environ["QT_FONT_DPI"] = "96"
@@ -198,6 +199,15 @@ def cli_check_file(file_path: str) -> int:
198199
ui._plugin_process = plugin_process # 保存引用,防止被 GC
199200

200201
GameServerBridge.instance().start()
202+
203+
# 注册控制命令处理器(自动在主线程执行)
204+
def handle_new_game(cmd: NewGameCommand):
205+
print(f"[NewGameCommand] rows={cmd.rows}, cols={cmd.cols}, mines={cmd.mines}")
206+
ui.setBoard_and_start(cmd.rows, cmd.cols, cmd.mines)
207+
from lib_zmq_plugins.shared.base import CommandResponse
208+
return CommandResponse(request_id=cmd.request_id, success=True)
209+
210+
GameServerBridge.instance().register_handler(NewGameCommand, handle_new_game)
201211

202212
# _translate = QtCore.QCoreApplication.translate
203213
hwnd = int(ui.mainWindow.winId())

0 commit comments

Comments
 (0)