Skip to content

调试台插件开发指南

实验性功能

插件系统当前处于快速迭代阶段,API 可能会在后续版本中调整。本文档基于当前已实现的功能编写,如有疑问请参考仓库中的示例插件。

Playground 插件当前定位为内部受信扩展机制。插件代码直接在浏览器中运行,manifest.capabilities 用于声明插件会使用哪些宿主能力,便于宿主展示、人工审查和统一治理,不构成浏览器级安全沙箱。

最简插件结构

一个插件由 manifest.json 和入口文件组成:

my-plugin/
├── manifest.json
├── src/
│   └── index.ts
├── package.json
├── tsconfig.json
└── vite.config.ts

构建产物:

dist/
├── index.js          # 自包含的 ES 模块(所有依赖已内联)
└── manifest.json

manifest.json

json
{
  "name": "com.example.my-plugin",
  "version": "1.0.0",
  "displayName": "我的插件",
  "description": "一个示例插件",
  "author": "Your Name",
  "capabilities": ["network.fetch"],
  "sdkVersion": "^1.0.0"
}

完整字段说明

字段必填说明
name反域名格式 ID,全局唯一(如 com.example.my-plugin
version语义化版本号(如 1.0.0
displayName用户可见的名称
description功能描述。Agent 会看到这段文字来决定是否调用
author作者信息
capabilities声明插件会使用的宿主能力列表,见能力一览
sdkVersionSDK 兼容版本范围(如 ^1.0.0),安装时会检查兼容性
icon插件图标(URL 或 base64)
updateUrl插件更新检查地址
homepageUrl插件主页地址
changelog更新日志
configSchema配置界面声明,见插件配置 Schema
tools静态工具元数据。通常由 src/index.ts 中的顶层 tools 自动派生,见静态工具元数据

静态工具元数据

推荐把工具定义集中写在 src/index.ts 的顶层 tools 中,作为单一定义源。SDK 会自动把这些完整工具定义派生为 manifest.tools,用于安装阶段的探测。

只有在“纯 manifest 探测、没有顶层 tools”的特殊场景下,才需要手写 manifest.tools

json
{
  "tools": [
    {
      "name": "my_tool",
      "description": "这是一个示例工具",
      "parameters": {
        "type": "object",
        "properties": {
          "input": { "type": "string" }
        },
        "required": ["input"]
      }
    }
  ]
}

设置后,宿主无需执行插件代码即可获知工具名列表。更常见的情况是:你在顶层 tools 中声明完整工具,SDK 自动同步出 manifest.tools,这样探测、展示和真实执行始终保持一致。

插件配置 Schema

configSchema 允许你声明结构化的配置界面:

json
{
  "configSchema": {
    "description": "插件配置说明",
    "sections": [
      {
        "title": "API 设置",
        "description": "配置外部 API 连接",
        "fields": [
          {
            "key": "apiKey",
            "label": "API Key",
            "type": "password",
            "required": true,
            "description": "你的服务 API 密钥"
          },
          {
            "key": "baseUrl",
            "label": "服务地址",
            "type": "string",
            "defaultValue": "https://api.example.com",
            "placeholder": "输入 API 地址"
          },
          {
            "key": "timeout",
            "label": "超时时间(秒)",
            "type": "number",
            "defaultValue": 30,
            "min": 5,
            "max": 120
          },
          {
            "key": "format",
            "label": "输出格式",
            "type": "select",
            "options": [
              { "label": "JSON", "value": "json" },
              { "label": "CSV", "value": "csv" },
              { "label": "Markdown", "value": "markdown" }
            ],
            "defaultValue": "json"
          },
          {
            "key": "verbose",
            "label": "详细输出",
            "type": "boolean",
            "defaultValue": false
          }
        ]
      }
    ]
  }
}

字段类型string | number | boolean | select | password

验证规则

  • required — 是否必填
  • min / max — 数值范围(number 类型)
  • pattern — 正则验证(string 类型)
  • patternMessage — 正则验证失败的提示信息

插件代码中通过 ctx.getConfig() 读取当前配置值,通过 ctx.onConfigChange(handler) 监听配置变化。

src/index.ts

typescript
import { definePlugin, defineTool } from '@playground/plugin-sdk'

const myTool = defineTool({
  name: 'my_tool',
  description: '这是一个示例工具',
  parameters: {
    type: 'object',
    properties: {
      input: { type: 'string', description: '输入内容' },
    },
    required: ['input'],
  },
  execute: async (args, api) => {
    api.reportProgress({ phase: 'working', message: '正在处理...' })
    return { success: true, result: `你输入了: ${args.input}` }
  },
})

export default definePlugin({
  manifest: {
    name: 'com.example.my-plugin',
    version: '1.0.0',
    displayName: '我的插件',
    description: '一个示例插件',
    capabilities: ['network.fetch'],
  },
  tools: [myTool],
})

PluginContext API

install(ctx) 中的 ctx 对象提供以下方法。注意:它现在主要服务于“额外初始化逻辑”,不是推荐的工具主注册入口:

方法说明
ctx.registerTool(tool)兼容旧写法保留。新插件应优先使用顶层 tools 声明工具;这个方法主要留给旧插件或动态注册场景
ctx.registerWidgetKind(kind, component)注册一个自定义 Widget 类型和对应的 Vue 组件
ctx.getConfig()获取当前插件配置(用户在设置面板中填写的值)
ctx.onConfigChange(handler)监听配置变化,返回取消监听函数

推荐约定:

  • tools:声明所有工具,作为工具 schema 和执行实现的单一来源
  • install(ctx):只处理 Widget 注册、事件订阅或其他额外初始化逻辑

vite.config.ts

typescript
import { defineConfig } from 'vite'

export default defineConfig({
  build: {
    lib: {
      entry: 'src/index.ts',
      formats: ['es'],
      fileName: () => 'index.js',
    },
    rollupOptions: {
      output: {
        assetFileNames: 'assets/[name].[ext]',
      },
    },
    outDir: 'dist',
    emptyOutDir: true,
    minify: false,
  },
})

package.json

json
{
  "name": "@playground-plugin/my-plugin",
  "private": true,
  "type": "module",
  "scripts": {
    "build": "vite build && cp manifest.json dist/manifest.json",
    "pack": "npm run build && cd dist && zip -r ../my-plugin.zip ."
  },
  "dependencies": {
    "@playground/plugin-sdk": "file:../../playground-plugin-sdk"
  },
  "devDependencies": {
    "vite": "^6.0.0",
    "typescript": "^5.0.0"
  }
}

构建与安装

bash
# 安装依赖
npm install

# 构建
npm run build

# 打包为 ZIP
npm run pack

# 在调试台的插件管理面板中上传 my-plugin.zip

插件 API 参考

插件工具的 execute 函数接收 (args, api) 参数。args 是工具调用参数,apiPluginRuntimeAPI 实例。

api 方法完整列表

网络请求

API 方法说明需要能力声明
api.fetch(url, init?)发送 HTTP 请求,返回标准 Responsenetwork.fetch
typescript
const resp = await api.fetch(`https://api.example.com/data?q=${encodeURIComponent(args.query)}`)
if (!resp.ok) {
  return { success: false, error: `请求失败: HTTP ${resp.status}` }
}
const data = await resp.json()
return { success: true, data }

用户交互

API 方法说明需要能力声明
api.askUser(options)向用户提问并获取回答user_interact
typescript
const answer = await api.askUser({
  title: '确认操作',
  question: '请选择处理方式',
  type: 'single',       // 'single' | 'multi' | 'text'
  options: [
    { label: '方式一', value: 'option1' },
    { label: '方式二', value: 'option2' },
  ],
  allowOther: true,      // 允许用户输入自定义答案
  placeholder: '请选择...',
  required: true,
})
// answer = { values: ['option1'], text: '' }

Widget 展示

API 方法说明需要能力声明
api.showWidget(widget)返回 Widget 展示数据ui.widget
typescript
return api.showWidget({
  title: '处理结果',
  kind: 'my-custom-widget',
  summary: '简要描述',
  data: { /* Widget 数据 */ },
  controls: [ /* 可交互控件 */ ],
  actions: [ /* 操作按钮 */ ],
})

通知

API 方法说明需要能力声明
api.showNotification(options)显示通知ui.notification
typescript
api.showNotification({
  type: 'success',  // 'info' | 'success' | 'warning' | 'error'
  title: '处理完成',
  description: '文件已成功转换',
})

进度报告

API 方法说明需要能力声明
api.reportProgress({ phase, message, progress? })报告工具执行进度无需额外能力声明

文件操作

API 方法说明需要能力声明
api.workspace.readFile(path)读取文件为文本workspace.userworkspace.plugin
api.workspace.writeFile(path, content)写入文件(支持 stringUint8Arrayworkspace.userworkspace.plugin
api.workspace.deleteFile(path)删除文件workspace.userworkspace.plugin
api.workspace.listFiles(path?)列出目录内容workspace.userworkspace.plugin
api.workspace.readFileBlob(path)读取文件为 Blobworkspace.userworkspace.plugin

插件资源

API 方法说明需要能力声明
api.readPluginAsset(path)读取插件 src/ 目录下的静态资源(如 WASM、数据文件)无需额外能力声明
typescript
const wasmBlob = await api.readPluginAsset('assets/ffmpeg-core.wasm')
const wasmUrl = URL.createObjectURL(wasmBlob)

Python 运行时

API 方法说明需要能力声明
api.runPython(options)在浏览器端执行 Python 代码workspace.python
typescript
const result = await api.runPython({
  code: 'print("Hello from plugin")',
  files: ['data/input.csv'],  // 可选:关联工作区文件
  timeoutMs: 30000,           // 可选:超时时间
})
// result = { success: true, stdout: 'Hello from plugin\n', stderr: '', result: null }

WASM 运行时

API 方法说明需要能力声明
api.runWasm(options)加载并实例化 WASM 模块workspace.wasm
api.wasmCall(instanceId, fnName, args?)调用 WASM 实例的导出函数workspace.wasm
api.wasmWriteMemory(instanceId, offset, data)向 WASM 内存写入数据workspace.wasm
api.wasmReadMemory(instanceId, offset, length)从 WASM 内存读取数据workspace.wasm
api.wasmReadLog(instanceId)读取 WASM 实例的日志workspace.wasm
api.wasmDestroy(instanceId)销毁 WASM 实例workspace.wasm
typescript
// 加载 WASM 模块
const instance = await api.runWasm({
  module: 'assets/my-module.wasm',  // 也可以是 ArrayBuffer 或 URL
  imports: { /* WASM 导入配置 */ },
  timeoutMs: 30000,
})
// instance = { instanceId: '...', exports: ['add', 'multiply'], memoryPages: 1 }

// 调用函数
const result = await api.wasmCall(instance.instanceId, 'add', [1, 2])

// 读写内存
await api.wasmWriteMemory(instance.instanceId, 0, new Uint8Array([1, 2, 3]))
const data = await api.wasmReadMemory(instance.instanceId, 0, 3)

// 用完后销毁
await api.wasmDestroy(instance.instanceId)

runWasmmodule 参数支持三种形式:

  • string:相对于插件 assets/ 目录的路径(如 'assets/ffmpeg-core.wasm'
  • ArrayBuffer:直接的 WASM 二进制数据
  • URL:WASM 文件的 URL

imports 参数支持多种格式,用于配置 WASM 模块的导入对象,包括内存分配、表、错误处理等。

持久化存储

API 方法说明需要能力声明
api.storage.get(key)获取值storage
api.storage.set(key, value)设置值storage
api.storage.delete(key)删除键storage
api.storage.list()列出所有键storage

存储是插件作用域隔离的,不同插件的存储互不影响。

会话信息

API 方法说明需要能力声明
api.getSessionInfo()获取当前会话信息无需额外能力声明
api.getConfig()获取当前插件配置无需额外能力声明
typescript
const info = api.getSessionInfo()
// info = { id: 'session-xxx', folderName: 'agent-xxx', mode: 'agent' | 'chat' }

const config = api.getConfig()
// config = { apiKey: '...', baseUrl: '...' }  // 用户在设置面板中配置的值

返回结果约定

推荐使用 { success: boolean } 格式:

typescript
// 成功
return { success: true, city: '北京', temperature: 25 }

// 失败
return { success: false, error: '城市名称无法识别' }

能力一览

能力说明典型用途
network.fetchHTTP 请求调用外部 API
workspace.user读写用户工作区文件处理用户上传的文件
workspace.plugin读写插件沙箱目录插件内部文件管理
workspace.python执行 Python 代码数据处理
workspace.wasm运行 WASM 模块高性能计算
user_interact向用户提问获取用户输入
ui.widget显示自定义组件可视化展示
ui.notification显示通知状态提醒
storage插件 KV 存储持久化配置

进阶:构建含 WASM / Worker 的插件

当插件需要处理 WASM 二进制、Web Worker 等复杂依赖时,需要注意以下几点:

插件加载机制

  1. 安装:ZIP 解压到浏览器 OPFS 中的插件目录
  2. 加载:读取 JS 文件 → 创建 Blob URL → 重写相对 import 路径 → import(blobUrl) 动态加载
  3. 执行:运行顶层 tools,并调用 plugin.install(ctx) 做额外初始化(如注册 Widget)

关键限制:插件代码以 Blob URL 形式在浏览器中执行,import.meta.urlblob:https://...,无法解析常规的相对路径。因此:

  • 所有 npm 依赖必须在构建时由 Vite 打包进 index.js不要将包设为 external
  • 裸模块说明符(如 @ffmpeg/ffmpeg)无法在运行时被浏览器解析

WASM 作为静态资源

WASM 文件不适合内联到 JS 中(如 ffmpeg-core.wasm 有 31MB)。正确做法是:

  1. 将 WASM 包声明为依赖
  2. 构建后用脚本拷贝 WASM 到 dist/assets/
  3. 运行时通过 api.readPluginAsset()api.runWasm({ module: 'assets/...' }) 读取
typescript
// 方式一:使用 api.runWasm 直接加载
const instance = await api.runWasm({
  module: 'assets/ffmpeg-core.wasm',
})

// 方式二:手动读取并加载
const wasmBlob = await api.readPluginAsset('assets/ffmpeg-core.wasm')
const wasmBytes = await wasmBlob.arrayBuffer()
const instance = await api.runWasm({ module: wasmBytes })

Web Worker 处理

如果库内部创建 Web Worker,Worker 的 JS 源码如果有外部 import(ESM 格式),在 Blob URL 环境下无法解析。

解决方案:使用 Vite 打包后的自包含 Worker chunk(IIFE 格式)。Vite 会自动处理 new Worker(new URL(...)) 模式,生成自包含的 Worker chunk。构建脚本需要将其重命名为稳定文件名:

javascript
// 构建后脚本:重命名 Vite 生成的 worker chunk
import { readdirSync, renameSync } from 'node:fs'

const distAssets = 'dist/assets'
const workerFile = readdirSync(distAssets).find(f => /^worker-.*\.js$/.test(f))
if (workerFile) {
  renameSync(`${distAssets}/${workerFile}`, `${distAssets}/worker.js`)
}

运行时加载 Worker:

typescript
const workerBlob = await api.readPluginAsset('assets/worker.js')
const workerBlobUrl = URL.createObjectURL(
  new Blob([await workerBlob.text()], { type: 'text/javascript' })
)

workspace 能力选择

能力文件访问范围适用场景
workspace.pluginplugins/{name}/ 沙箱目录插件内部文件管理
workspace.user用户工作区根目录处理用户的文件(如音视频转换)

Agent 传入的路径(如 files/video.mp4)是相对于用户工作区根目录的。如果需要处理用户工作区中的文件,应声明 workspace.user 能力。

简单插件 vs 复杂插件

简单插件复杂插件(含 WASM)
依赖全部打包进 index.jsWASM 作为静态资源放在 assets/
能力network.fetchworkspace.user(读写用户文件)
Worker需用 Vite 打包的 IIFE 格式
构建脚本vite build额外拷贝 WASM + 重命名 Worker
ZIP 大小~5KB~10MB

更多内容