Serpent 插件开发与发布指南
本文面向准备实际开发、联调和发布 Serpent 插件的人类开发者。文档以当前仓库的
plugin-manifest.ts、plugin-sdk.ts、Plugin Manager API 和测试 fixture 为准;当前平台仍处于开发态,
未在本文中承诺尚未实现的能力。
相关文档:
| 文档 | 用途 |
|---|---|
| 本文 | Manifest、生命周期、Contribution、Job、设置、发布前测试 |
| 插件 API 参考 | serpent.* 方法、权限和错误码 |
| 插件分发与更新 | 安装通道、平台 token、GitHub Release 文件名 |
| 插件开发最佳实践 | 成品包、ZIP 路径、Job 进度、内容分块、原生二进制 |
| 官方插件参考实现 | Renamer(非受限模式 UI 插件)、MediaConverter(媒体转码与通用包)、ImageUpscaler(非受限原生二进制与分流包) |
1. 先了解插件模型
插件是长期运行的扩展,不是一次性脚本。它可以声明命令、菜单、工具栏、Inspector/Viewer 操作、快捷键、
设置、沙箱 iframe 页面、Hook、Job 和 Provider,并在 setup 中注册运行时 handler。
领域读写仍通过 serpent API 进入 Automation Gateway;Renderer 不加载插件后端代码,插件也不能获得任意
SQL 或宿主 DOM。
安装范围和实例范围是两件事:
- 安装范围
user或library决定包存放在哪里;它不决定实例数量。 runtime.instanceScope为global时,一个已解析版本在应用会话中只有一个实例,可服务多个库。runtime.instanceScope为library时,每个打开的库各有一个隔离实例。- 每个实例有自己的
pluginInstanceId;Contribution 按实例撤销,不能只按插件 ID 处理。
当前入口只有一对生命周期函数:setup(context) 和 dispose(reason)。没有 openLibrary、closeLibrary 等第二套
生命周期。库打开/关闭通过领域事件表达。
2. 最小可安装包
发布包是不可变成品,至少包含:
my-plugin/
serpent-plugin.json # 必需,Manifest v1
entry/main.js # 必需,runtime.entry 指向它
README.md # 建议;社区详情英文/回退
README.zh-CN.md # 建议;社区详情中文优先
LICENSE # 建议,发布包应提供
entry/ui/index.html # 可选,沙箱 UI 页面
入口可以由 TypeScript 编译生成,但安装和运行时只读取包内已生成的 JavaScript;安装过程不替插件执行
npm install、构建、postinstall 或 Shell。ZIP 条目必须是相对 POSIX 路径(entry/main.js),不要写成
./entry/main.js 或 entry\\main.js。细则与 Windows 打包方式见 最佳实践 §8
和 分发规范。
一个最小受限插件:
{
"manifestVersion": 1,
"id": "com.example.hello",
"version": "1.0.0",
"name": "Hello",
"description": "A small Serpent plugin.",
"author": "Example",
"license": "MIT",
"engines": {
"serpent": ">=0.1.0 <1.0.0",
"pluginApi": 1
},
"runtime": {
"mode": "restricted",
"entry": "entry/main.js",
"instanceScope": "library"
},
"permissions": ["library.read", "storage.read", "storage.write"],
"contributes": {
"commands": [],
"menus": {},
"toolbar": [],
"inspector": [],
"viewerActions": [],
"shortcuts": [],
"views": [],
"settings": [],
"hooks": [],
"jobs": [],
"providers": [],
"themes": []
}
}
contributes 的数组/对象可以省略,schema 会使用空默认值;发布 包建议像上例一样完整写出,便于审查和维护。
Manifest 是 strict schema:未知字段、重复权限、重复 Contribution ID、无效包内路径都会被拒绝。
3. Manifest 完整字段
顶层字段如下:
| 字段 | 必需 | 当前约束 |
|---|---|---|
manifestVersion | 是 | 只能是 1 |
id | 是 | 3–64 字符,稳定的反向域名式 ID;发布后不要更改 |
version | 是 | SemVer |
name | 是 | 1–160 字符;英文或默认展示名 |
description | 是 | 1–2000 字符 |
locales | 否 | zh-CN / en 的 name、description。社区目录文案优先;未进目录时 Host 用清单 locales |
author | 是 | 1–160 字符 |
license | 是 | 1–160 字符 |
repository | 否 | HTTPS GitHub 仓库 URL,必须正好是 owner/repository |
engines | 是 | serpent 是显式比较符 SemVer range;pluginApi 当前只能为 1 |
runtime | 是 | 见下一节 |
ui | 否 | { "entry": "包内相对 HTML 路径" },作为插件 UI 包入口 |
permissions | 是 | 最多 64 个,不得重复;只声明当前确实需要的权限 |
contributes | 是 | 见第 5 节;各类 Contribution ID 在插件内必须唯一 |
mcp | 否 | { "expose": ["命令 local id"] },见第 10 节 |
所有路径必须是包内相对路径;不能是绝对路径、路径穿越或逃逸符号链接。runtime.entry 和 ui.entry 必须
指向包内文件。
4. restricted、unrestricted、安全与信任
restricted
restricted 运行在可终止的 QuickJS 隔离环境中:没有 Node built-ins、process、环境变量、任意 import、
原生文件系统、数据库或宿主 DOM。插件只能通过声明权限后可用的 serpent.* API 工作。适合一般菜单、领域操作、
Provider 和 Job。
unrestricted
unrestricted 运行在每个实例独立的 Node.js UtilityProcess 中,拥有 Node、文件系统、网络、子进程和依赖的完整能力。
它仍应优先通过 serpent.* 操作 Serpent 领域对象,但权限清单不能拦截插件绕过 Gateway 直接调用 Node 的行为;权限同时
是 Serpent API 的能力控制和对用户的风险披露,不是沙 箱承诺。需要原生模块时可声明:
"runtime": {
"mode": "unrestricted",
"entry": "entry/main.js",
"instanceScope": "global",
"nativeModules": [
{ "platform": "darwin", "arch": "arm64", "nodeAbi": 136 }
]
}
nativeModules 中的 platform 只能是 darwin/win32/linux,arch 只能是 arm64/x64/ia32,且 nodeAbi 为正整数。
不要把 unrestricted 描述成“受权限完全保护”;它等同于运行一段本地 Node 程序。
安装、信任、激活是三个独立阶段:
- 安装只把包放入用户或资源库存储,不执行入口代码。
- Host 校验 Manifest、文件列表、SHA-256、引擎版本、权限和 Resolution。
- 用户在当前设备明确授信后,Host 才能创建实例并调用
setup。
资源库包可随资源库复制,但信任决定、密钥和本机路径永远只保存在当前设备;不能因为包同步到另一台设备就自动运行。
同一 ID 同时有用户级和库级包时,Host 不设隐式优先级,由用户选择 use-global、use-library 或禁用。资源库中的非受限插件安装后
默认不启用,需在设置中手动信任并打开;全局受限插件可按解析结果自动启用。Safe Mode 只停用非受限插件,受限插件仍可运行。
当前安装通道:
- 插件社区(默认):设置 → 插件 → 打开插件社区。Host 只安装 Serpent-Plugin-Pool 钉死的 GitHub Release ZIP。要出现在目录里,按 分发规范 发布 Release,并在目录仓登记条目。
- 高级安装:本地文件夹、本地 ZIP,或粘贴 GitHub URL(仅此通道可 zipball 回退)。
发布时优先上传 GitHub Release 平台 ZIP。包升级应先 staging、校验、健康检查 再切换;权限、运行模式或来源改变时要重新信任。社区插件的新版本必须先改目录仓条目并合并,不会跟随 GitHub 最新 Release 自动更新。
5. 声明 Contribution
行为放在 contributes.commands,其他 Host 表面引用命令,不重复实现 handler。命令的 id 是插件内 local ID,运行时完整 ID
为 <pluginId>.<id>。
"contributes": {
"commands": [
{ "id": "inspect", "title": "Inspect asset", "mcp": { "export": true } }
],
"menus": {
"asset": [
{
"command": "inspect",
"group": "analysis",
"after": "asset.open",
"when": "selection.assetCount == selection.count",
"enablement": "library.open && library.writable"
}
]
},
"toolbar": [{ "id": "inspect-toolbar", "command": "inspect", "title": "Inspect" }],
"inspector": [{ "id": "inspect-section", "command": "inspect", "title": "Inspect" }],
"viewerActions": [{ "id": "inspect-viewer", "command": "inspect" }],
"shortcuts": [{ "id": "inspect-key", "command": "inspect", "accelerator": "F9" }]
}
当前菜单 key 为 asset、folder、collection、workspace;它们对应 Host 的菜单表面。菜单项必须二选一:
command 或 submenu。子菜单必须有 id 和 title,命令项不能有 id;before 与 after 不能同时使用,
first 与 last 不能同时为 true。子菜单最多三级。group、before、after 用稳定 ID,不要按文案或 DOM 定位。
缺失锚点只降级到目标 group 末尾;循环约束只拒绝相关边,不应让整张菜单消失。
when 为 false 时不渲染,enablement 为 false 时保留但置灰,checked 控制 toggle/radio 的选中状态。显示与置灰策略由插件
根据自己的能力实现,Host 不提供 accepts 过滤,也不替插件过滤混合选中。三者都只能读取 Context Key,不能执行 JavaScript、
I/O 或 API 调用。表达式支持 &&、||、!、==、!=、in、intersects、matches、括号和数组字面量;单个表达式最多
4096 字符。
快捷键 Contribution 只是默认 accelerator;菜单展示中央 Keybinding Registry 当前有效快捷键。插件设置的 boolean、select、
number 和 slider 均由 Host 使用统一的原生样式、ARIA、加载和错误状态渲染;插件不需要自己实现 toggle、dropdown 或 slider。
当前仍没有动态 Registrar;不要在插件中依赖运行时注册新的 Host surface。
slider 示例:
{ "id": "scale", "title": "Scale", "type": "slider", "default": 0.5,
"minimum": 0, "maximum": 1, "step": 0.1 }
5.1 Theme Contract v1
插件主题只能通过语义引用和插件自有颜色 token 接入 Host 主题,不得覆盖 Host 的任意 CSS 变量。主题贡献的 version 当前为 1:
"themes": [{
"id": "brand",
"version": 1,
"light": {
"references": {
"accent": "action.accent",
"panel": "surface.pane"
},
"tokens": {
"badge": "#c45a00"
}
},
"dark": {
"references": {
"accent": "action.accent",
"panel": "surface.pane"
},
"tokens": {
"badge": "#ff9a3c"
}
}
}]
references 的值必须是 Host 公共语义名:surface.canvas、surface.pane、surface.raised、surface.overlay、
content.primary、content.secondary、content.tertiary、border.divider、border.control、border.focus、
action.accent、state.info、state.success、state.warning、state.error。tokens 只能是颜色值(十六进制、rgb/rgba、
hsl/hsla、transparent 或 currentColor),每个模式最多 32 个。
在 iframe 中,Host 会保留只读的 --ui-* 语义变量,并把插件映射为隔离变量:references.accent 变成
--serpent-plugin-ref-accent,tokens.badge 变成 --serpent-plugin-token-badge。插件不得依赖 Host 内部旧变量(如
--accent、--canvas),也不能用主题贡献注入字体、布局、URL、calc() 或任意 CSS。
iframe 收到的主题消息类型是 plugin-ui.theme-changed,带有 theme、contrast、单调 revision 和 token map;插件应按 revision
更新自身样式,并忽略旧 revision。主题变化包括用户切换亮/暗/系统主题以及强调色变化。
Host 当前提供的外观预设包括 vscode-dark、serpent-dark、serpent-light 和 soft-light。插件不需要识别预设 ID,
只需使用 iframe 收到的 --ui-* 只读语义 token 和 plugin-ui.theme-changed 消息即可。用户在 Host 中设置的背景颜色或背景图片
不会暴露给插件,也不能通过插件主题贡献修改。
6. Contribution Context 与 Invocation Context
Contribution Context 是 Host 发布的有界 UI 快照,只用于菜单/命令的 when、enablement、checked。当前字段包括:
app.platform、app.locale、app.theme、app.busysurface.id、surface.kind、window.windowIdlibrary.id、library.open、library.writable、library.offlineselection.ref、count、primaryId、assetCount、folderCount、mixed、extensions、mediaKinds、MIME 类型、删除/不可用摘要browse.folderId、collectionId、tagId、search、filterviewer.active、assetId、extension、mimeType、mediaKind、fullscreen
Context 带 contextId 和单调递增 revision。它只含摘要;要读取完整资产,使用 Domain API。
插件通过 selection.mediaKinds、selection.extensions、selection.mixed 和 library.writable 自己表达菜单策略。例如,
只支持图片的命令可以用 when 隐藏,仍可出现在菜单但暂不可执行时用 enablement 置灰。图片与视频混合选中的处理规则属于
插件能力与产品策略,不属于 Host 的统一过滤规则。
Invocation Context 是命令触发时冻结的目标快照,包含 contextId、revision、目标 libraryId、selection refs/assetIds/folderIds/collectionIds、
浏览范围和 viewer 目标。异步等待后不能重新猜测当前焦点或选择;在 commands.register 的 handler 中从 context.invocation 读取这份快照,
顶层 targetLibraryId 与 ID 数组只是便捷字段。
复杂条件不能在打开菜单时 RPC 插件。开发态的 Predicate Resolver 会在 Context revision 变化后异步计算,缓存键包含
pluginInstanceId + contextId + revision + predicateId;新 revision 会取消旧计算,超时/错误使用 fallback。当前 Manifest 没有
公开的 Predicate Contribution 字段,不能把它写成可发布配置。