实战:从零写一个 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__.pysrc/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.zipdevice-console.zip.hash

第 5 步:安装与调试

  1. 在 PyriteIDE 的插件页安装 build/device-console.zip
  2. 左侧导航会出现「设备控制台」容器,包含「状态」与「项目文件」两个视图——点开任意一个都会触发对应的 onView: 激活事件并启动插件。
  3. 打开插件后,命令面板执行 device-console.hello,或在「状态」视图标题栏点击打招呼按钮。
  4. 观察 IDE 输出面板:on_start 里的 print 会被路由到插件的输出流。

常见调试技巧

现象检查
插件没启动activation_events 是否包含当前触发的事件;manifest 校验是否通过(插件页会显示错误码)
视图空白view_id 是否与贡献 ID 完全一致;permissions 是否声明了 ui.view
命令没反应contributes.commands 是否声明了该 ID;self.commands.register 的 ID 是否与清单一致
请求返回 permission_deniedpermissions 里补上对应权限

完整的错误码与修复步骤见 排错指南

对照清单

教程步骤涉及概念参考页
plugin.tomlManifest v2、贡献点、激活、权限Manifest v2插件类型
激活流程延迟启动、握手、钩子生命周期
视图两种渲染路径、facade、视图模型UI 插件开发views
命令/菜单命令注册与分派commands
配置配置读写与变更订阅configuration
事件事件总线events
文件本地文件操作file
打包ZIP 与校验文件打包与发布

On this page