Skip to content

PI-Desktop 插件开发:从零到一

翻译说明: 本页是与 英文源页面 一一对应的机器辅助翻译。代码、协议字段和标识符保持原文;如翻译与英文源事实有歧义,以英文版本为准。

本指南是从空文件夹到经过测试的最短完整路径 .piplug 包。它描述了 PI-Desktop 今天发布的插件运行时。 docs/spec/07-plugins 下的文件仍然是 当本指南与规范不同时,规范性合同。

1. 插件可以添加什么

插件可以提供以下一项或多项功能:

能力用它来主要构建模块
命令全局搜索中的显式操作contributes.commandspi.commands.register
面板一个小的独立的 HTML 界面ui.panelui.panel 权限、window.pluginBridge
Agent 工具Agent 可以调用的函数contributes.agentToolspi.agent.registerTool
技能Agent 按需加载指令contributes.skillsagent.prompt.inject 权限
主题设计令牌覆盖contributes.themesui.theme 权限
MCP 服务器从本地或远程 MCP 服务器发现的工具contributes.mcpServers,MCP 权限
服务驻地工作由主人监督contributes.servicesbackground.service 权限
消息总线插件之间按约定类型化事件contributes.bus,总线权限

插件入口代码在专用的 Node 进程中运行。面板在沙盒中运行, 上下文隔离的 Electron 窗口,没有 Node 集成。来自任一方的呼叫 表面跨越主机拥有的权限网关。

**信任边界:**权限模型对 pi.* 主机 API 和面板进行门控 桥梁。它还不是用于原始 Node API 的操作系统沙箱 插件输入过程。仅加载开发插件和第三方包 当您信任其来源并使用主机 API 而不是直接 Node 文件时 或网络访问。请参阅安全规范

2.先决条件

对于推荐的应用优先路径,您需要:

  • 正在运行的 PI-Desktop 版本;
  • 插件的空文件夹;和
  • 文本编辑器。

对于存储库 CLI 路径,您还需要 Node.js 22.19 或更高版本、pnpm 10 或 较新,并签出此存储库。 devkit 和 SDK 目前已 私有工作空间包,所以不要假设npm install @pi-desktop/plugin-devkit 在此存储库之外工作。

3. 创建第一个插件

选项 A:在 PI-Desktop 中创建它

  1. 打开 插件(扩展页面)。
  2. 打开标题溢出菜单并选择 从模板新建插件
  3. 选择 panel-basic
  4. 选择一个空文件夹。

PI-Desktop 编写起始文件,加载文件夹作为开发插件, 并将该文件夹作为活动项目打开。该插件立即生效。

四个内置模板是:

模板开始于权限
panel-basic命令和 HTML 面板ui.panel
agent-tool-basic代理可调用的 echo 工具agent.tool.register
skill-pack一份技能文件agent.prompt.inject
full-demo命令、面板、工具、技能和设置这些功能使用的权限

脚手架拒绝非空目的地,因此它不能默默地覆盖 现有项目。

选项 B:使用存储库 CLI 创建它

从 PI-Desktop 存储库根目录:

bash
pnpm install
pnpm --filter @pi-desktop/plugin-devkit... build
pnpm pi-plugin init panel-basic ../my-first-plugin \
  --id local.my-first-plugin \
  --name "My First Plugin"

然后打开PI-Desktop,进入插件,选择加载开发插件,然后 选择 ../my-first-plugin

例如,对已发布的插件使用反向域 ID com.example.workspace-summarylocal. 前缀是一个有用的约定 私人插件。保持 ID 稳定:设置、数据、授权、更新和 包名称由它作为键。

4.了解生成的文件

panel-basic 模板生成:

text
my-first-plugin/
├── manifest.json
├── main.js
├── README.md
└── renderer/
    └── index.html
  • manifest.json 声明身份、入口点、贡献和请求 权限。
  • main.js 在插件进程中运行并导出生命周期挂钩。
  • renderer/index.html 在隔离面板窗口中运行。
  • README.md 解释了如何开发和打包这个特定的插件。

分发包必须包含直接可执行的 JavaScript、HTML、CSS、 和资产。 PI-Desktop 不会安装依赖项或编译 TypeScript 它加载一个插件。如果您使用 TypeScript 或第三方软件包、捆绑包或 在检查和打包之前将它们编译到插件目录中。

5. 手动构建最小插件

以下三个文件显示了完整的命令到面板路径。

manifest.json

json
{
  "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": {
    "piDesktop": ">=0.1.0"
  },
  "activationEvents": [
    "onCommand:my-first-plugin.open",
    "onStartup"
  ]
}

需要 schemaVersionidnameversionmain。每个文件 路径是相对于插件根目录的,并且必须保留在其中。仅声明 插件实际需要的权限。

main.js

js
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 作为全局注入。 onLoadonUnload 不接收任何参数。 CommonJS是最简单的入口格式;当条目是 ES 模块。模块评估加上 onLoad 有 15 秒预算。 onUnload 有 5 秒预算并且尽力而为,因此释放计时器和订阅 及时。

今天仅解雇 onLoadonUnload。中的其他生命周期名称 清单是为计划的完整生命周期保留的。

renderer/index.html

html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>My First Plugin</title>
  </head>
  <body>
    <h1>My First Plugin</h1>
    <button id="hello">Show toast</button>
    <script>
      document.getElementById("hello").addEventListener("click", async () => {
        await window.pluginBridge.invoke("ui.showToast", {
          message: "Hello from the panel",
        });
      });
    </script>
  </body>
</html>

面板不接收全局 pi 对象。它只接收 window.pluginBridge 和任意 Electron IPC 通道不可用。

6. 添加功能

6.1 Agent 工具

声明该工具及其权限:

json
{
  "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"]
}

onLoad 期间注册匹配的处理程序:

js
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 中取消注册。主机将其暴露给模型 插件命名空间名称,应用正常的 Agent 权限策略,审核 执行,并强制执行 110 秒的插件端超时。插件工具不是 在 Plan 模式下可用。

6.2 技能

添加一个文件,例如 skills/release-notes.md

markdown
---
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.

使用所需的权限声明它:

json
{
  "contributes": {
    "skills": ["skills/release-notes.md"]
  },
  "permissions": ["agent.prompt.inject"]
}

提示收到简短的技能目录;全文可按需阅读。 每个插件最多可以贡献32个技能,每个文件最多128 KiB, 描述的上限为 240 个字符。没有技能 agent.prompt.inject 被忽略而不是加载。

6.3 设置和私有数据

在清单中声明默认值:

json
{
  "contributes": {
    "settings": [
      {
        "key": "greeting",
        "title": "Greeting",
        "type": "string",
        "default": "Hello"
      }
    ]
  }
}

从插件进程中读取并更新它们:

js
const settings = await pi.plugin.getSettings();
await pi.plugin.setSettings({ greeting: "Welcome" });
const dataPath = await pi.plugin.getDataPath();

设置和数据路径是插件 ID 私有的。专用生成 设置 UI 尚未实现;插件必须提供自己的支持 互动以求改变。请勿将凭据放入 manifest.json 或源中 控制。

6.4 工作区文件、剪贴板、网络和通知

这些 API 需要明确的权限:

许可插件进程 API面板桥通道
fs.read.workspacepi.fs.readTextpi.fs.globfs.readTextfs.glob
fs.write.workspacepi.fs.writeTextfs.writeText
fs.delete.workspacepi.fs.remove没有暴露
clipboard.readpi.clipboard.readTextclipboard.readText
clipboard.writepi.clipboard.writeTextclipboard.writeText
net.fetchpi.net.fetchnet.fetch
shell.openExternalpi.shell.openExternalshell.openExternal
notifypi.ui.notifypi.ui.getNotificationPermissionpi.ui.requestNotificationPermissionpi.ui.showNativeNotificationui.notifyui.getNotificationPermissionui.requestNotificationPermissionui.showNativeNotification

工作空间路径是相对于活动工作空间的。绝对路径和 .. 逃跑被拒绝。 fs.remove 是非递归的,无法删除 工作区根目录。 openExternal 仅接受 HTTP(S) 和 mailto: URL; net.fetch 接受 HTTP(S)。

pi.ui.notify 显示应用内 Toast。本机通知可选择加入:呼叫 pi.ui.requestNotificationPermission() 之前 pi.ui.showNativeNotification(...)。返回的权限是尽力而为的 因为 Electron 没有公开跨平台只读操作系统权限 API; unknown 表示平台尚未上报结果,且 unsupported 表示桌面通知不可用。原生插件 通知不会添加到 PI-Desktop 的持久任务通知收件箱中。

面板桥还暴露 ui.showToastui.closePanelplugin.getSettingsworkspace.get。它不暴露任意自定义 渠道。 onPanelInvoke 当前保留给主机支持的 skill.*面板操作,不是一般面板到插件的RPC机制。

6.5 主题

声明 CSS 文件和 ui.theme

json
{
  "contributes": {
    "themes": [
      {
        "id": "midnight",
        "label": "Midnight",
        "path": "themes/midnight.css",
        "base": "dark"
      }
    ]
  },
  "permissions": ["ui.theme"]
}

覆盖该 CSS 中的 PI-Desktop 设计标记。楼主对贡献的内容进行了清理 CSS,拒绝导入和非数据 URL,每个文件的上限为 256 KiB,并允许 每个插件有八个主题。用户在“设置”中选择主题。

6.6 MCP 服务器

MCP 服务器是声明性的。本地服务器需要 mcp.server.local;一个 远程服务器需要 mcp.server.remote

json
{
  "contributes": {
    "mcpServers": [
      {
        "id": "docs",
        "label": "Documentation tools",
        "transport": "stdio",
        "command": "bin/docs-server",
        "args": ["--stdio"],
        "env": {
          "DOCS_TOKEN": { "setting": "docsToken" }
        }
      },
      {
        "id": "issues",
        "transport": "http",
        "url": "https://mcp.example.com/tools",
        "headers": {
          "Authorization": { "setting": "issuesAuthorization" }
        }
      }
    ]
  },
  "permissions": ["mcp.server.local", "mcp.server.remote"]
}

stdio 命令必须是 PATH 上的裸命令或与插件相关的命令 可执行文件;绝对路径被拒绝。远程 URL 必须使用 HTTPS,但以下情况除外 环回 HTTP。设置引用仅读取该插件的设置——主机 环境和提供商秘密永远不会被转发。 MCP 工具遵循 与手写插件工具相同的仅代理策略和命名空间。

6.7 常驻服务和消息总线

声明服务 ID 和允许的主题:

json
{
  "contributes": {
    "services": [
      { "id": "watcher", "label": "Workspace watcher" }
    ],
    "bus": {
      "publish": ["example.index.ready"],
      "subscribe": ["example.build.*"]
    }
  },
  "permissions": [
    "background.service",
    "bus.publish",
    "bus.subscribe"
  ]
}

注册匹配的处理程序:

js
let unsubscribe;

pi.services.register({
  id: "watcher",
  start: ({ log }) => log("watcher started"),
  stop: () => {},
});

unsubscribe = await pi.bus.subscribe("example.build.*", async (message) => {
  await pi.bus.publish("example.index.ready", {
    source: message.from,
    at: message.at,
  });
});

卸载期间调用 unsubscribe()。插件不接收自己的总线 消息。将主题视为对任何已安装的具有匹配插件的公共主题 订阅;切勿将秘密放入有效负载中。

7.权限设计

权限均在 manifest.json 中声明并由用户授予。 未声明或未授予的 API 调用失败并显示 PERMISSION_DENIED

风险权限
ui.panelui.themenotify
中等clipboard.readclipboard.writefs.read.workspaceshell.openExternalbackground.servicebus.publishbus.subscribe
fs.write.workspacefs.delete.workspaceagent.tool.registeragent.prompt.injectnet.fetchmcp.server.localmcp.server.remote

要求尽可能最小的一组。向已加载的开发添加权限 插件无法通过热重载生效:PI-Desktop 停止重载并 要求用户再次加载该文件夹,以便可以查看新的授权。 删除权限在重新加载时生效。

完整的映射和策略位于 权限矩阵

8. 开发和调试

热重载

在第一个文件夹加载后跨应用程序监视开发插件 重新启动。 300 毫秒去抖后重新加载更改。 .gitnode_modulesdisttarget 和通用编辑器草稿文件将被忽略。

重新加载执行 unload → validate → load。不保留面板内存。一个 语法或明显错误卸载损坏的版本但保留观察者 活跃;保存修复以进行恢复。最多观看 16 个开发插件 一次。

验证每个贡献

  • 从全局搜索运行命令(Cmd/Ctrl+KCmd/Ctrl+Shift+P)。
  • 从命令或插件行打开面板。
  • 要求 Agent 在 Agent 模式下调用贡献的工具。
  • 请求与技能描述相匹配的任务,然后检查是否 技能被选择。
  • 在“设置”中选择贡献的主题。
  • 检查插件行的服务状态和重新启动计数。

日志和失败

负载、崩溃、权限、工具、网络、服务和总线活动记录在 应用程序日志。打开 设置 → 信息 → 日志,然后搜索 插件 ID。面向用户的加载和热重载失败也显示为Toast和 当存在持续加载错误时,在插件行上。

主机 API 故障通常会引发 ErrorcodePERMISSION_DENIEDNOT_FOUNDINVALID_ARGUMENTTIMEOUTUNSUPPORTEDLIMIT_EXCEEDEDRATE_LIMITED。捕获可选操作的错误 并将代码包含在诊断中而不记录秘密。

9.检查、打包、安装

从存储库根运行验证:

bash
pnpm pi-plugin check ../my-first-plugin

check 报告阻塞错误和非阻塞警告。它验证了 清单、引用的文件、权限、路径包含、符号链接、包 大小和文件计数使用与安装相同的规则。还要查看警告, 特别是未使用的和高风险的权限。

仅与开发套件一起打包:

bash
pnpm pi-plugin pack ../my-first-plugin

结果是:

text
../my-first-plugin/dist/local.my-first-plugin-0.1.0.piplug

该命令打印包 SHA-256。 .piplug 仅限商店 (未压缩)ZIP;正常的 zip 默认值通常会生成一个存档 安装人员拒绝。该开发套件不包括 .gitnode_modulesdist, 拒绝符号链接,并强制最多 2,000 个文件和 50 MiB。

要测试用户收到的确切工件:

  1. 打开插件
  2. 从标题溢出菜单中选择“安装插件包”。
  3. 选择生成的.piplug。 4.检查权限并安装。
  4. 重复上一节中的贡献检查。
  5. 禁用并重新启用它以验证清理和启动行为。 7.卸载并确认其贡献消失。

Agent 还可以在每种操作模式下运行 PluginCheckPluginScaffoldPluginPack 是代理模式工具,仅限于当前 工作区。

10. 准备发布

共享包之前:

  1. 使用稳定的反向域插件id。
  2. 使用语义版本控制更新 version
  3. engines.piDesktop 设置为您实际支持的版本。
  4. 记录每个命令、设置、工具输入、权限和外部 插件 README 中的服务。
  5. 添加变更日志和许可证。
  6. 将所有生成的 JavaScript 和资源构建到插件文件夹中。
  7. 运行 pi-plugin check 并解决每个错误和意外警告。
  8. 运行 pi-plugin pack 并以干净的应用程序状态安装生成的包。
  9. 在发布工件旁边记录打印的 SHA-256。

对于官方市场,请将包和目录元数据提交至 vastsa/pi-desktop-plugins 和 遵循该存储库的 CONTRIBUTING.md。市场目录是 单独的存储库;在这里添加插件不会发布它。

签名不是当前的信任原语。包 SHA-256 和显式 许可审查是实施的基线;遵循 签署和更新规范 了解路线图详细信息。

11. 故障排除

症状可能的原因修复
manifest.json is missing选择了错误的目录选择根目录包含 manifest.json 的目录
main entry missingmain 指向未构建的源首先Compile/bundle或者更正相对路径
面板打不开缺少文件、ui.panel 或权限声明面板路径和ui.panel;重新加载以获得新的补助金
pluginBridge 不可用在普通浏览器中打开的 HTML在 PI-Desktop 面板内测试桥接调用
工具从未出现缺少贡献、注册或资助对齐 agentToolsregisterToolagent.tool.register;使用 Agent 模式
技能永远不适用缺少权限或元数据薄弱添加 agent.prompt.inject 和特定 name/description 前面的内容
保存报告 PERMISSION_DENIED清单扩大了权限再次加载开发文件夹并查看新的授权
语法错误后热重载停止损坏的插件已卸载保存修正后的文件;观察者保持活跃状态
包安装拒绝压缩存档是使用通用 ZIP 工具制作的使用 pi-plugin pack 重建它
MCP 服务器未启动传输字段、命令、URL、设置或权限无效运行 pi-plugin check,然后通过插件 ID 检查日志
服务反复重启start 抛出异常或插件进程退出使 start 幂等,在 stop 中进行清理,并检查重新启动日志

12.参考图

为本地优先开发而构建。