插件能力
一个插件可以贡献一项或多项能力。宿主负责加载、权限校验、生命周期和审计; 插件只声明自己需要的贡献与权限。
| 能力 | 用途 | 主要构建块 |
|---|---|---|
| 命令 | 全局搜索中的显式操作 | contributes.commands、pi.commands |
| 面板 | 隔离的 HTML 界面 | ui.panel、window.pluginBridge |
| 悬浮小组件 | 透明、无边框的伴随窗口 | "ui": { "shape": "widget" } |
| 工作面板视图 | 停靠在应用右侧工作面板 | contributes.views、ui.view |
| 智能体工具 | 智能体可以调用的函数 | contributes.agentTools、pi.agent.registerTool |
| OAuth 提供方 | 由宿主管理登录和加密凭据 | contributes.providers、provider.oauth |
| 一次性补全 | 使用用户配置的模型完成短任务 | pi.agent.complete |
| 技能 | 智能体按需加载的指令文档 | contributes.skills、agent.prompt.inject |
| 主题 | 设计令牌覆盖 | contributes.themes、ui.theme |
| MCP 服务器 | 接入本地或远程 MCP 工具 | contributes.mcpServers |
| 服务 | 由宿主监管的常驻工作 | contributes.services、background.service |
| 消息总线 | 插件之间按约定类型通信 | contributes.bus |
| 渲染插槽 | 绘制在应用窗口内的小型 UI | renderer、renderer.extension |
信任边界:权限模型管控 pi.* 宿主 API 和面板桥,
但插件入口进程不是操作系统级沙箱。只加载你信任的来源,
并优先使用宿主 API,而不是直接访问 Node 文件与网络。
前置条件
- 一个可运行的 voki-work 构建。
- 一个用于插件的空文件夹,以及一个文本编辑器。
-
如果导入带 npm 依赖的 pi 扩展目录,系统
PATH中需要有npm。发布构建不附带独立 Node/npm。 - 使用仓库 CLI 时,还需要 Node.js 22.19+、pnpm 10+ 和仓库检出。 DevKit 与 SDK 是项目私有包,不能假设在仓库外可以安装。
创建插件
方案 A:在应用中创建
- 打开 扩展(Plugins) 页面。
- 打开头部溢出菜单,选择 New plugin from template。
- 选择模板并指定一个空文件夹。
| 模板 | 起点 | 权限 |
|---|---|---|
panel-basic | 命令与 HTML 面板 | ui.panel |
agent-tool-basic | 智能体可调用的 echo 工具 | agent.tool.register |
skill-pack | 一个技能文档 | agent.prompt.inject |
full-demo | 命令、面板、工具、技能与设置 | 上述功能所需的权限 |
脚手架会拒绝非空目录,不会静默覆盖已有项目。
方案 B:使用仓库 CLI
pnpm install
pnpm --filter @voki-work/plugin-devkit... build
pnpm pi-plugin init panel-basic ../my-first-plugin \
--id local.my-first-plugin \
--name "My First Plugin"
然后在应用的 扩展 页面选择 Load development plugin,
选中 ../my-first-plugin。
保持 id 稳定:设置、数据、授权、更新和包名都以插件 id 为键。
发布插件应使用反向域名 id(例如 com.example.workspace-summary);
local. 前缀适合私有插件。
文件结构
panel-basic 模板生成以下文件:
my-first-plugin/
├── manifest.json
├── main.js
├── README.md
└── renderer/
└── index.html
manifest.json声明身份、入口点、贡献与权限。main.js运行在插件进程中,导出生命周期钩子。renderer/index.html运行在隔离的面板窗口中。README.md说明如何开发和打包这个插件。
分发包必须包含可直接执行的 JavaScript、HTML、CSS 与资源。 应用不会在加载时安装依赖或编译 TypeScript;请提前打包或编译到插件目录。
最小 manifest
{
"schemaVersion": 1,
"id": "local.my-first-plugin",
"name": "My First Plugin",
"version": "0.1.0",
"description": "Opens a panel and shows a greeting.",
"main": "main.js",
"ui": {
"panel": "renderer/index.html",
"title": "My First Plugin",
"width": 480,
"height": 360
},
"contributes": {
"commands": [
{
"id": "my-first-plugin.open",
"title": "My First Plugin: Open Panel",
"keywords": ["hello", "panel"]
}
]
},
"permissions": ["ui.panel"],
"engines": { "vokiWork": ">=0.1.0" },
"activationEvents": ["onCommand:my-first-plugin.open", "onStartup"]
}
schemaVersion、id、name、
version 和 main 是必需字段。所有文件路径
都相对于插件根目录,且不能越界。只声明实际需要的权限。
最小入口
async function onLoad() {
await pi.commands.register({
id: "my-first-plugin.open",
title: "My First Plugin: Open Panel",
keywords: ["hello", "panel"],
run: async () => {
await pi.ui.openPanel({ title: "My First Plugin" });
await pi.ui.showToast("Hello from My First Plugin");
},
});
}
async function onUnload() {
await pi.commands.unregister("my-first-plugin.open");
}
module.exports = { onLoad, onUnload };
宿主把 pi 注入为全局对象。onLoad 与
onUnload 不接收参数;模块求值加 onLoad
有 15 秒预算,onUnload 有 5 秒预算且是尽力而为的。
请及时释放定时器和订阅。
面板不会收到全局 pi,只收到
window.pluginBridge。所有面板调用都要经过权限网关。
添加能力
智能体工具
{
"contributes": {
"agentTools": [
{
"name": "summarize_text",
"description": "Summarize text supplied by the agent.",
"risk": "low",
"schema": {
"type": "object",
"properties": { "text": { "type": "string" } },
"required": ["text"]
}
}
]
},
"permissions": ["agent.tool.register"]
}
await pi.agent.registerTool({
name: "summarize_text",
description: "Summarize text supplied by the agent.",
risk: "low",
schema: {
type: "object",
properties: { text: { type: "string" } },
required: ["text"],
},
execute: async (args, context) => {
context.log("summarize_text called");
const text = String(args?.text ?? "");
return { summary: text.slice(0, 120) };
},
});
在 onUnload 中注销工具。宿主会以插件命名空间暴露工具,
应用正常的智能体权限策略,审计执行,并强制执行插件侧超时。
插件工具在 Plan 模式下不可用。
技能
---
name: Release notes
description: Use when the user asks for release notes or a changelog entry.
---
# Release notes
Write one line per user-visible change. Use imperative mood and put the newest
change first.
{
"contributes": { "skills": ["skills/release-notes.md"] },
"permissions": ["agent.prompt.inject"]
}
提示词会收到简短的技能目录,正文按需读取。每个插件最多贡献 32 个技能,
每个文件最大 128 KiB,描述上限 240 字符。缺少
agent.prompt.inject 的技能会被忽略。
设置与私有数据
{
"contributes": {
"settings": [
{
"key": "greeting",
"title": "Greeting",
"type": "string",
"default": "Hello"
}
]
}
}
const settings = await pi.plugin.getSettings();
await pi.plugin.setSettings({ greeting: "Welcome" });
const dataPath = await pi.plugin.getDataPath();
设置与数据路径按插件 id 隔离。已安装插件页面会为
string、number、boolean、
select、json 和 shortcut
字段生成控件。不要把凭据写进 manifest.json 或版本控制。
权限边界
文件、剪贴板、网络、外部链接和通知都需要显式权限。文件权限只是一半,
manifest.fs 还决定每种模式可以触碰哪些路径。
| 权限 | 插件进程 API | 面板桥通道 |
|---|---|---|
fs.read | pi.fs.readText、glob、list | fs.readText、fs.glob、fs.list |
fs.write | pi.fs.writeText | fs.writeText |
fs.delete | pi.fs.remove | 不暴露 |
clipboard.read | pi.clipboard.readText | clipboard.readText |
clipboard.write | pi.clipboard.writeText | clipboard.writeText |
net.fetch | pi.net.fetch | net.fetch |
shell.openExternal | pi.shell.openExternal | shell.openExternal |
notify | pi.ui.notify、showNativeNotification | ui.notify |
文件范围
{
"permissions": ["fs.read", "fs.write", "fs.delete"],
"fs": {
"read": { "scope": ["**/*"] },
"write": { "scope": ["docs/**", "*.md"] },
"delete": { "own": true, "scope": ["dist/**"] }
}
}
scope是相对于根的 glob,*匹配一段,**跨分隔符。- 读可以声明整棵树;写入和删除不能使用整树模式。
- 超出范围的访问会询问用户;拒绝会以
PERMISSION_DENIED返回。 -
.env*、SSH 与云凭据、*.pem、.git/**以及应用自身数据目录始终不可访问。 fs.remove非递归,移入系统废纸篓,且不能删除根本身。
网络访问
{
"permissions": ["net.fetch"],
"net": { "domains": ["api.example.com", "*.githubusercontent.com"] }
}
条目是裸主机名,不含协议、端口或路径;前导 *. 覆盖该域及其子域,
裸 * 会在安装时被拒绝。该列表同时约束插件进程、面板中的
fetch、图片、脚本、样式表以及远程 MCP 服务器。
省略或留空 net.domains 意味着没有出站访问。
重定向由宿主手动跟随并重新检查。
不要依赖回退:需要重定向策略时先检查宿主能力, 不要在旧宿主上退回裸网络请求。
校验与打包
校验
pnpm pi-plugin check ../my-first-plugin
check 使用与安装相同的规则,检查 manifest、被引用文件、权限、
路径包含、符号链接、包大小和文件数。阻塞错误必须修复;警告也应逐项审查,
尤其是未使用和高风险权限。
打包
pnpm pi-plugin pack ../my-first-plugin
../my-first-plugin/dist/local.my-first-plugin-0.1.0.piplug
.piplug 是仅存储(未压缩)的 ZIP;普通 zip
默认参数通常会产生被安装器拒绝的归档。DevKit 会排除
.git、node_modules 和 dist,
拒绝符号链接,并限制为最多 2,000 个文件、50 MiB。
打包命令会打印包的 SHA-256。
安装并验证
- 打开 扩展 页面。
- 从头部溢出菜单选择 Install plugin package。
- 选择生成的
.piplug,审查权限并安装。 - 验证命令、面板、工具、设置和清理行为。
- 禁用再启用,确认卸载后所有贡献消失。
智能体也可以运行 PluginCheck;
PluginScaffold 与 PluginPack
仅限智能体模式,并被限制在当前项目。
发布前检查
- 使用稳定、反向域名的插件 id。
- 按语义化版本更新
version。 - 将
engines.vokiWork设为实际支持的版本范围。 - 在 README 中记录每个命令、设置、工具输入、权限和外部服务。
- 添加变更日志与许可证。
- 把所有生成的 JavaScript 和资源构建到插件目录。
- 运行
pi-plugin check并解决错误与意外警告。 - 运行
pi-plugin pack,在干净的应用状态中安装并验证。 - 把打印的 SHA-256 记录在发布工件旁。
分发到市场时,插件中心会重新打包、审计源并记录 SHA-256, 再把目录和安装包镜像到市场目录。插件源码不会托管在分发仓库中。
常见问题
插件加载后没有出现命令或面板
检查 manifest.json 的 activationEvents、
贡献项 id 与入口路径是否一致,并确认文件都在插件根目录内。
开发插件可以在扩展页面重新加载后查看宿主日志。
调用文件或网络 API 返回 PERMISSION_DENIED
先确认 permissions 已声明对应权限,再检查
manifest.fs 的路径范围或 manifest.net.domains
是否包含目标主机。超出范围时宿主会询问用户,拒绝即返回该错误。
包安装时被拒绝
很可能使用了通用 ZIP 工具。请用 pi-plugin pack
重新打包;它会生成安装器接受的仅存储 ZIP,并排除符号链接与开发目录。
TypeScript 或 npm 依赖在安装后不可用
应用不会在安装时安装依赖或编译源码。请先把 TypeScript 和第三方包
打包成可直接执行的 JavaScript,再运行 pi-plugin pack。
插件工具在 Plan 模式下不可用
这是预期行为。插件提供的智能体工具不会在 Plan 模式中暴露; 需要先用 Agent 或 Goal 模式执行。