插件系统开发
IDE 端插件系统的架构:Manifest、生命周期、桥接与开发约束。
PyriteIDE 使用内嵌 Python 运行时承载插件,通过 Dart Bridge 传输层在 IDE 与 SDK 之间交换协议消息,并由 Flutter 原生渲染器(native.*)及组件注册表渲染插件界面。
运行模型
- 插件运行在 IDE 进程内嵌的单例 Python 解释器中(
PythonRuntimeHost)。 - 每个插件会话拥有独立的中继 channel(
PythonBridge),标签pyrite.plugin.<pluginId>.<sessionId>。 - 每次启动会话都会产生新的
sessionId与递增的generation;重启运行时会重置整个解释器并再次递增generation。 - 插件类型为
ui、service和data。UI 插件提供视图,Service 插件持续提供后台服务,Data 插件完成on_contribute()和挂起响应后自动退出。
IDE (Dart) ⟷ PythonBridge ⟷ Python 单例解释器(Plugin A / B / C)插件目录结构
插件以 ZIP 包分发,解压后是一个插件目录,入口为 __main__.py:
src/
├── __main__.py
├── plugin.toml # Manifest v2
├── requirements.txt
└── assets/ # 静态资源(图片、视频等)
build/ # pyrsdk package 输出(<id>.zip + .zip.hash)运行时为每个已安装插件创建 data/、cache/ 与 sessions/<sessionId>/tmp 目录,并通过环境变量(PYRITE_IDE_PLUGIN_DATA_DIR 等)注入,插件经 self.path.data() / self.path.cache() / self.path.temp() 访问。
启动与安装流程
- 用户选择
.zip插件包。 - 校验插件 ID 安全(禁止绝对路径、
..越界),解压到插件目录。 - 解析并校验
plugin.toml(Manifest v2,含权限、激活事件、导航容器与视图贡献)。 - 激活事件命中后创建插件会话,完成四阶段握手,再用等待回复的方式发送
start生命周期钩子。 - 停止时发送
LifecycleHook.dispose,等待 Python 目标退出,随后清理临时目录。
Data 插件通过 runOnce 会话运行:run_once() 贡献完成后自动退出,不长期占用解释器。
会话与修订校验
PluginSession携带sessionId、generation、transport、各类目录与runOnce标记。- 宿主能力为
sdk.v1;所有消息使用协议 v1 并经 session/generation 校验。 - 运行时重启(
restartRuntime)会先stopAll(),resetRuntime()重置解释器并递增generation;重置前创建的对象引用全部失效(StaleReferenceError)。
激活协调
ActivationManagerNotifier 负责 onStartup、onView:<viewId>、onCommand:<commandId> 和 onLanguage:<languageId>。同一插件已有激活 Future 时,后续调用直接等待该 Future,因此并发打开视图或执行命令只会创建一个会话。停用同样按插件合并,且会先等待正在进行的激活结束,避免新会话越过停用操作继续存活。
启动失败由 PluginSessionMetrics 记录并进入指数退避,等待时间从 1 秒增加到最多 60 秒。PluginRunManagerNotifier 在创建会话前检查剩余时间,成功启动后清除失败次数。新增自动恢复入口时,应复用这套检查和计数,不能绕过它直接调用传输层。
协议与队列
协议 v1 使用四阶段握手:ide.initialize、sdk.initialize、ide.initialized、sdk.ready。每个方向维护独立的严格递增序列号,业务消息只能在 ready 后处理。所有信封包含插件 ID、会话 ID 和代数;请求响应关系由 requestId 与 replyTo 表达。
IDE 对插件入站消息分为控制队列和视图补丁队列。控制队列容量为 256,不能静默丢弃;补丁队列容量为 32,压力过大时丢弃最早补丁并依靠视图 resync 恢复。解析任务按插件串行衔接,保持线缆顺序,同时避免一个插件的 JSON 解析阻塞其他会话。
SDK 的 Bridge 出站队列默认容量为 50,每轮最多排空 32 条消息。需要回复的宿主请求作为独立 asyncio task 运行,支持 deadline 和 ide.request.cancel。实现新处理器时,要让取消异常正常传播,并保证每个请求至多回复一次。
停止清理
会话停止包含两端清理:
- SDK 执行
on_dispose(),释放事件订阅、命令、视图、请求任务和挂起回调。 PythonRuntimeHost等待 daemon 目标退出,关闭传输,并在确认退出后删除cache/sessions/<sessionId>/tmp。PluginRunManagerNotifier按会话清理PluginEventBus、ViewModelStore和ComponentMethodRegistry。- 运行时整体重启会停止全部会话,重置解释器并清除保留事件及运行时引用状态。
内嵌 daemon 目标没有安全的单目标强制终止能力。停止超时时,会话保持 failed 并继续被宿主追踪,待目标实际结束后再完成清理。不能把关闭 Bridge 传输等同于 Python 代码已经停止。
线程与单例约束
- Python 在 IDE 进程内的运行时线程执行。SDK 回调位于插件的 Bridge 事件循环,不在 Flutter UI isolate。
- 所有插件共享一个解释器,全局状态会在插件间共享;插件应使用
插件ID.前缀避免名称冲突。 - 一个插件若不正确退出会阻止该插件再次启停;此时“重启运行时”可整体恢复。
- 插件入口由 daemon 目标承载。Data 插件应使用
run_once(),在挂起响应归零后结束事件循环。
BridgeOutputRouter 为 stdout 和 stderr 安装进程级路由器,再按当前插件线程选择目标 Bridge。插件及其派生线程的输出会进入对应的 IDE 输出流。修改输出捕获时,必须保留原始流作为无会话上下文时的回退,避免插件输出互相串流或递归写回 Bridge。
主要实现位置
lib/core/sdk/python_runtime_host.dart:运行时启动锁、会话目录、generation、停止和整体重启。lib/core/sdk/activation_manager.dart:激活状态、触发条件和并发合并。lib/core/sdk/plugin_run_manager.dart:握手、信封校验、入站队列、请求回复和生命周期调用。lib/core/sdk/plugin_run_manager_provider.dart:会话与 Riverpod 状态绑定、退避检查和停止清理。lib/core/sdk/plugin_event_bus.dart:主题注册、权限、投递策略和会话隔离。- SDK
src/pyrite_sdk/core/bridge.py:插件事件循环、回调表、请求任务、输出路由和出站队列。
开发指南
- 插件开发使用 PyriteSDK,参见 SDK 文档。
- Manifest v2 说明见 Manifest v2。
- 事件主题与权限见 插件事件。
- 原生视图与组件目录见 原生视图与组件。
示例插件
SDK 仓库的 examples/ 提供可直接运行的示例:
debug_enhanced_plugin— Ui 插件,贡献一个native.outline(大纲)与一个native.variableInspector(设备变量)视图,展示大纲跳转与运行时变量分页查看。service_plugin— Service 插件,监听文件并做后台处理。theme_plugin— Data 插件,贡献 Nord 主题。i18n_plugin— Data 插件,贡献英文语言包。stubs_plugin— Data 插件,贡献 MicroPython 类型存根。
例如,一个贡献大纲与变量视图的 Ui 插件核心结构:
from pyrite_sdk.core.plugin import UiPlugin
class DebugPlugin(UiPlugin):
def __init__(self):
super().__init__()
self.outline = self.views.outline("debug-enhanced.outline", title="Outline")
self.variables = self.views.variable_inspector(
"debug-enhanced.device-variables",
title="Device Variables",
)
def on_start(self):
# 从语言服务获取大纲符号并推送
self.outline.open()
self.variables.open()
self.refresh_symbols()
def refresh_symbols(self):
self.documents.get_active(
lambda **cb: self._fill_outline(cb.get("document"))
)
def _fill_outline(self, document):
if document is None:
return
self.documents.symbols(
document.document_id,
callback=lambda **cb: print("symbols:", cb),
)设备变量视图需要 runtime.inspect 权限,并从运行时作用域分页读取变量(不要通过 serial.run_python 打断设备)。