Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -169,4 +169,5 @@ old/
src/plugins/*/*.json
src/plugins/*/*.db
.vscode/
data/*
data/*
.iflow/
3 changes: 3 additions & 0 deletions build.bat
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ xcopy /e /y /i "%SP%\debugpy" "%DEST%\_internal\debugpy" >nul
xcopy /e /y /i "%SP%\msgspec" "%DEST%\_internal\msgspec" >nul 2>nul
xcopy /e /y /i "%SP%\setuptools" "%DEST%\_internal\setuptools" >nul 2>nul

echo [5/5] Copy plugin-dev-tutorial.md
copy /y "plugin-dev-tutorial.md" "%DEST%\" >nul

echo.
echo Done! Both in: %OUT%\
pause
97 changes: 97 additions & 0 deletions skills/plugin-dev/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
---
name: plugin-dev
description: Meta-Minesweeper 插件开发助手,支持创建、调试和优化插件
---

# Meta-Minesweeper 插件开发助手

你是一个专业的 Meta-Minesweeper 插件开发助手。你熟悉插件系统的架构、API 和最佳实践,能够帮助用户创建、调试和优化插件。

## 核心能力

1. **插件创建** - 支持单文件和包形式两种插件结构
2. **渐进式指导** - 根据用户需求逐步披露相关知识点
3. **代码生成** - 生成符合规范的插件模板代码
4. **问题诊断** - 帮助排查插件加载、运行时的常见问题

## 知识体系

### 架构概览
详见 [references/architecture.md](./references/architecture.md)

### 创建插件
详见 [references/creating.md](./references/creating.md)

### 渐进式知识披露
- [references/level-1-basics.md](./references/level-1-basics.md) - 基础概念
- [references/level-2-events.md](./references/level-2-events.md) - 事件系统
- [references/level-3-threading.md](./references/level-3-threading.md) - 跨线程 GUI
- [references/level-4-control.md](./references/level-4-control.md) - 控制授权
- [references/level-5-service.md](./references/level-5-service.md) - 服务系统
- [references/level-6-config.md](./references/level-6-config.md) - 配置系统

### 问题诊断
详见 [references/troubleshooting.md](./references/troubleshooting.md)

### 最佳实践
详见 [references/best-practices.md](./references/best-practices.md)

## 快速开始

### 第一步:检测环境

**必须首先执行**,检测运行环境并获取可用的类型:

```bash
python scripts/create_plugin.py discover
```

返回 JSON:
```json
{
"environment": "dev", // "dev" 或 "frozen"
"install_dir": "...", // 安装目录
"plugins_dir": "...", // 插件目录
"shared_types_dir": "...", // shared_types 目录
"events": [...], // 可用事件列表
"commands": [...] // 可用命令列表
}
```

根据 `environment` 判断:
- `"dev"` - 开发模式,插件放在 `src/plugins/`
- `"frozen"` - 打包模式,插件放在 `plugins/`

### 第二步:收集信息

使用 `ask_user_question` 工具收集插件信息:

1. 插件形式(单文件/包形式)
2. 插件名称、描述、作者
3. 窗口模式(TAB/DETACHED/CLOSED)
4. 订阅的事件(从 discover 返回的 events 选择)
5. 控制权限(从 discover 返回的 commands 选择)
6. 是否需要配置系统、服务接口

### 第三步:创建插件

调用脚本创建插件:

```bash
python scripts/create_plugin.py create \
--name my_plugin \
--description "描述" \
--window-mode TAB \
--events VideoSaveEvent \
--commands NewGameCommand
```

脚本输出创建结果(JSON 格式)

## 插件模板

模板文件位于 `assets/templates/` 目录:
- `minimal.py` - 最小可行插件
- `with-gui.py` - 带 GUI 的插件
- `with-config.py` - 带配置的插件
- `with-control.py` - 带控制权限的插件
31 changes: 31 additions & 0 deletions skills/plugin-dev/assets/templates/minimal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""
{plugin_name} - {description}

一个最小可行插件示例。
"""
from __future__ import annotations

from plugin_sdk import BasePlugin, PluginInfo
from shared_types.events import VideoSaveEvent


class {PluginName}(BasePlugin):
"""{description}"""

@classmethod
def plugin_info(cls) -> PluginInfo:
return PluginInfo(
name="{plugin_name}",
version="1.0.0",
description="{description}",
window_mode=WindowMode.CLOSED, # 无界面
)

def _setup_subscriptions(self) -> None:
self.subscribe(VideoSaveEvent, self._on_video_save)

def on_initialized(self) -> None:
self.logger.info("{PluginName} 已初始化")

def _on_video_save(self, event: VideoSaveEvent):
self.logger.info(f"游戏结束: 用时={event.rtime}s, 3BV={event.bbbv}")
90 changes: 90 additions & 0 deletions skills/plugin-dev/assets/templates/with-config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
"""
{plugin_name} - {description}

带配置系统的插件示例。
"""
from __future__ import annotations

from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel
from PyQt5.QtCore import pyqtSignal

from plugin_sdk import (
BasePlugin, PluginInfo, make_plugin_icon, WindowMode,
OtherInfoBase, BoolConfig, IntConfig,
)
from shared_types.events import VideoSaveEvent


class {PluginName}Config(OtherInfoBase):
"""插件配置"""

enable_logging = BoolConfig(
default=True,
label="启用日志",
description="是否记录游戏数据到日志",
)

max_records = IntConfig(
default=100,
label="最大记录数",
min_value=10,
max_value=1000,
)


class {PluginName}Widget(QWidget):
"""插件 UI"""

_update_signal = pyqtSignal(str)

def __init__(self, parent=None):
super().__init__(parent)
layout = QVBoxLayout(self)

self._label = QLabel("等待游戏数据...")
layout.addWidget(self._label)

self._update_signal.connect(self._on_update)

def _on_update(self, text: str):
self._label.setText(text)


class {PluginName}(BasePlugin):
"""{description}"""

@classmethod
def plugin_info(cls) -> PluginInfo:
return PluginInfo(
name="{plugin_name}",
version="1.0.0",
description="{description}",
window_mode=WindowMode.TAB,
icon=make_plugin_icon("#2196F3", "C"),
other_info={PluginName}Config, # 绑定配置类
)

def _setup_subscriptions(self) -> None:
self.subscribe(VideoSaveEvent, self._on_video_save)

def _create_widget(self) -> QWidget | None:
self._widget = {PluginName}Widget()
return self._widget

def on_initialized(self) -> None:
self.logger.info("{PluginName} 已初始化")

# 连接配置变化信号
self.config_changed.connect(self._on_config_changed)

# 读取配置
if self.other_info:
self.logger.info(f"配置: enable_logging={self.other_info.enable_logging}")

def _on_config_changed(self, name: str, value):
self.logger.info(f"配置变化: {name} = {value}")

def _on_video_save(self, event: VideoSaveEvent):
if self.other_info and self.other_info.enable_logging:
self.logger.info(f"游戏数据: 用时={event.rtime}s")
self._widget._update_signal.emit(f"用时: {event.rtime:.2f}s")
93 changes: 93 additions & 0 deletions skills/plugin-dev/assets/templates/with-control.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
"""
{plugin_name} - {description}

带控制权限的插件示例。
"""
from __future__ import annotations

from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QPushButton
from PyQt5.QtCore import pyqtSignal

from plugin_sdk import BasePlugin, PluginInfo, make_plugin_icon, WindowMode
from shared_types.events import VideoSaveEvent
from shared_types.commands import NewGameCommand


class {PluginName}Widget(QWidget):
"""插件 UI"""

_update_signal = pyqtSignal(str)
_auth_signal = pyqtSignal(bool)

def __init__(self, parent=None):
super().__init__(parent)
layout = QVBoxLayout(self)

self._status = QLabel("控制权限: 未知")
layout.addWidget(self._status)

self._btn_new_game = QPushButton("开始新游戏")
self._btn_new_game.clicked.connect(self._on_new_game_click)
layout.addWidget(self._btn_new_game)

self._update_signal.connect(self._on_update)
self._auth_signal.connect(self._on_auth)

def _on_update(self, text: str):
self._status.setText(text)

def _on_auth(self, granted: bool):
self._status.setText(f"控制权限: {'已授权' if granted else '未授权'}")
self._btn_new_game.setEnabled(granted)

def _on_new_game_click(self):
# 由插件类处理
pass


class {PluginName}(BasePlugin):
"""{description}"""

@classmethod
def plugin_info(cls) -> PluginInfo:
return PluginInfo(
name="{plugin_name}",
version="1.0.0",
description="{description}",
window_mode=WindowMode.TAB,
icon=make_plugin_icon("#FF5722", "G"),
required_controls=[NewGameCommand], # 声明需要的控制权限
)

def _setup_subscriptions(self) -> None:
self.subscribe(VideoSaveEvent, self._on_video_save)

def _create_widget(self) -> QWidget | None:
self._widget = {PluginName}Widget()
# 连接按钮点击
self._widget._btn_new_game.clicked.connect(self._start_new_game)
return self._widget

def on_initialized(self) -> None:
self.logger.info("{PluginName} 已初始化")

# 检查控制权限
has_auth = self.has_control_auth(NewGameCommand)
self._widget._auth_signal.emit(has_auth)

def on_control_auth_changed(self, cmd_type, granted: bool):
"""控制权限变更回调"""
if cmd_type == NewGameCommand:
self.logger.info(f"权限变更: {granted}")
self._widget._auth_signal.emit(granted)

def _start_new_game(self):
"""开始新游戏"""
if self.has_control_auth(NewGameCommand):
self.send_command(NewGameCommand(rows=16, cols=30, mines=99))
self.logger.info("已发送 NewGameCommand")
else:
self.logger.warning("没有控制权限")

def _on_video_save(self, event: VideoSaveEvent):
self._widget._update_signal.emit(f"用时: {event.rtime:.2f}s")
Loading
Loading