实战:从零写一个 Ui 插件
以「设备控制台」为例,一步步完成清单、视图、命令、配置与打包。
本教程带你从零实现一个名为 device-console(设备控制台)的 Ui 插件,把前几章的概念串成一条完整链路:清单声明 → 激活 → 视图渲染 → 命令/菜单 → 配置 → 事件 → 打包。每一步都会标注它对应的概念页面,方便回头查阅。
我们要做什么
最终插件包含两个视图、一条命令、一个配置项:
| 能力 | 说明 | 用到的东西 |
|---|---|---|
| 状态表单 | 显示工作区根目录 | native.form + FormField |
| 文件列表 | 列出工作区根目录下的文件 | native.tree + TreeItem |
| 打招呼命令 | 从菜单或命令面板触发 | contributes.commands + self.commands |
| 自动刷新开关 | 控制"文档变化时刷新状态" | contributes.configuration + self.configuration |
| 事件订阅 | 监听文档内容变化 | self.documents.on_changed |
第 1 步:创建项目
pyrsdk create ui device-console脚手架会生成 src/__main__.py 与 src/plugin.toml 模板。先看一下标准目录结构:
device-console/
├── src
│ ├── __main__.py # 插件入口
│ ├── plugin.toml # Manifest v2
│ ├── requirements.txt
│ └── assets/ # 可选:只读资源
└── build # pyrsdk package 输出第 2 步:编写 plugin.toml
用下面的内容替换 src/plugin.toml(逐字段注释):
manifest_version = 2 # 固定值
id = "device-console" # 全局唯一;贡献 ID 都要用 "device-console." 前缀
name = "设备控制台"
version = "1.0.0"
type = "ui" # ui | service | data
protocol_version = 1
python_version = "3.14"
author = "You"
description = "展示工作区信息与文件的示例插件。"
# 激活事件:只有这些事件命中时才创建插件会话
activation_events = [
"onView:device-console.status", # 打开状态视图时激活
"onView:device-console.files", # 打开文件视图时激活
"onCommand:device-console.hello", # 执行 hello 命令时激活
]
# 权限默认拒绝:这里声明的才是插件能访问的资源
permissions = ["ui.view", "file.read", "data.read"]
platforms = ["windows", "linux", "macos", "android"]
# 侧边导航容器:Ui 插件必须至少声明一个
[[contributes.navigation_containers]]
id = "device-console"
title = "设备控制台"
icon = { material = "monitor_heart_outlined" }
# 视图一:状态表单
[[contributes.views]]
id = "device-console.status"
container = "device-console"
title = "状态"
renderer = "native.form" # renderer 必填,限定 native.*
order = 0
# 视图二:文件树
[[contributes.views]]
id = "device-console.files"
container = "device-console"
title = "项目文件"
renderer = "native.tree"
order = 1
# 命令:hello
[[contributes.commands]]
id = "device-console.hello"
title = "打个招呼"
icon = { material = "waving_hand_outlined" }
# 菜单入口:在状态视图标题栏放一个按钮触发 hello 命令
[[contributes.menus]]
location = "view/title" # view/title | view/context | navigation/context | commandPalette
command = "device-console.hello"
view = "device-console.status"
group = "actions"
order = 0
# 配置项:自动刷新开关
[[contributes.configuration]]
id = "device-console.autoRefresh"
title = "自动刷新"
type = "boolean"
default = false
description = "文档内容变化时自动刷新状态视图。"字段的完整说明见 Manifest v2。注意激活事件的目标(device-console.status 等)必须指向本插件贡献的 ID,否则校验失败。
第 3 步:编写 main.py
用下面的内容替换 src/__main__.py:
from pyrite_sdk.api.native_views import FormField, TreeItem
from pyrite_sdk.core.plugin import UiPlugin
class DeviceConsolePlugin(UiPlugin):
def __init__(self):
super().__init__()
# 两个视图模型:view_id 必须与 plugin.toml 里声明的贡献 ID 一致
self.status = self.views.form("device-console.status", title="状态")
self.files = self.views.tree("device-console.files", title="项目文件")
self._auto_refresh = False
def on_start(self):
# 1) 注册命令处理器
self.commands.register("device-console.hello", self._hello)
# 2) 读取配置,并订阅配置变更
self.configuration.get(
"device-console.autoRefresh",
callback=lambda **cb: self._apply_auto_refresh(cb.get("value", False)),
)
self.configuration.on_changed(
self._on_config_changed,
configuration_id="device-console.autoRefresh",
)
# 3) 订阅文档内容变化(只在自动刷新开启时处理)
self.documents.on_changed(self._on_document_changed)
# 4) 打开视图并填充数据
self.status.open()
self.files.open()
self._refresh_status()
def on_dispose(self):
# 清理:注销命令;事件订阅随会话自动释放
self.commands.dispose_all()
self.status.close()
self.files.close()
# -- 命令 -------------------------------------------------------------
def _hello(self, args, context):
name = args.get("name", "世界")
self.message.info(f"你好,{name}!")
return {"ok": True}
# -- 配置 -------------------------------------------------------------
def _on_config_changed(self, event):
self._apply_auto_refresh(event.get("value", False))
def _apply_auto_refresh(self, enabled):
self._auto_refresh = bool(enabled)
# -- 事件 -------------------------------------------------------------
def _on_document_changed(self, event):
if self._auto_refresh:
self._refresh_status()
# -- 数据 -------------------------------------------------------------
def _refresh_status(self):
self.file.get_root_dir(
lambda **cb: self._on_root_dir(cb.get("path"))
)
def _on_root_dir(self, root):
if not root:
return
# 填充状态表单
self.status.set_items([
FormField(id="root", label="工作区", value=root),
])
# 列出根目录,填充文件树
self.file.get_file_list(
root,
lambda **cb: self._on_file_list(cb.get("entries", [])),
)
def _on_file_list(self, entries):
items = [TreeItem(id=p, label=p) for p in entries]
self.files.set_items(items)
plugin = DeviceConsolePlugin()
plugin.start()对照本教程开头的表格,注意以下几点:
- 回调都是异步的:
get_root_dir/get_file_list/configuration.get的结果以callback(**cb)送达,不要在on_start里"顺序等待"结果,而是用回调接力。回调运行在 Bridge 线程。 - 回调也要处理失败:上面的代码为简洁省略了
error=。生产代码建议写成callback=lambda **cb: ...并对cb.get("error")判空(见 API 概览 的回调模式)。 _refresh_status是"发出请求"而不是"拿到数据":真正的 UI 更新发生在后续回调里。这是整个 SDK 最重要的心智模型。
第 4 步:打包
cd device-console
pyrsdk package src --platform Windows打包完成后 build/ 会生成 device-console.zip 与 device-console.zip.hash。
第 5 步:安装与调试
- 在 PyriteIDE 的插件页安装
build/device-console.zip。 - 左侧导航会出现「设备控制台」容器,包含「状态」与「项目文件」两个视图——点开任意一个都会触发对应的
onView:激活事件并启动插件。 - 打开插件后,命令面板执行
device-console.hello,或在「状态」视图标题栏点击打招呼按钮。 - 观察 IDE 输出面板:
on_start里的print会被路由到插件的输出流。
常见调试技巧
| 现象 | 检查 |
|---|---|
| 插件没启动 | activation_events 是否包含当前触发的事件;manifest 校验是否通过(插件页会显示错误码) |
| 视图空白 | view_id 是否与贡献 ID 完全一致;permissions 是否声明了 ui.view |
| 命令没反应 | contributes.commands 是否声明了该 ID;self.commands.register 的 ID 是否与清单一致 |
请求返回 permission_denied | 在 permissions 里补上对应权限 |
完整的错误码与修复步骤见 排错指南。
对照清单
| 教程步骤 | 涉及概念 | 参考页 |
|---|---|---|
| plugin.toml | Manifest v2、贡献点、激活、权限 | Manifest v2、插件类型 |
| 激活流程 | 延迟启动、握手、钩子 | 生命周期 |
| 视图 | 两种渲染路径、facade、视图模型 | UI 插件开发、views |
| 命令/菜单 | 命令注册与分派 | commands |
| 配置 | 配置读写与变更订阅 | configuration |
| 事件 | 事件总线 | events |
| 文件 | 本地文件操作 | file |
| 打包 | ZIP 与校验文件 | 打包与发布 |