Plugin Development

Write a plugin

Plugins add new tools to AI WorkDeck so the AI can call your code from a conversation. This guide starts with a minimal working example and ends with submitting for review.

誰可以發布外掛

開源的免費外掛,註冊即可提交,不需要認證。

要給外掛或 Pack 定價收款,或發布閉源外掛,需要先通過開發者認證(驗證身分、綁定收款方式,審核制)。現在就可以前往

開發者中心

本页 SDK 1.6 的视图、连接、MCP、分域设置、$schema/default 兼容修复和宿主服务接口需要新版宿主;本次代码尚未发版,现有已安装客户端不因此自动支持。模拟器只使用合成数据,不能代替新版宿主验收。

First, decide: do you need a plugin or a Skill

The two are easy to confuse, and picking the wrong one wastes a lot of work. The difference fits in one sentence: if it takes code, write a plugin; if explaining it clearly is enough, write a Skill.

ScenarioUse
Call an external system’s API to fetch data外掛
Parse a special file format外掛
Have the AI write review comments in your firm’s formatSkill
Define the steps and deliverable structure for a type of matterSkill

A Skill is plain text (a prompt plus trigger words): it goes live on submission with no review, and is much faster to write. If a Skill covers your need, go write a Skill and skip the rest of this page.

Run your first plugin in five minutes

A plugin is a Java project compiled into a JAR, plus a manifest.json, zipped up and submitted. You need JDK 21 and Maven.

Download the template project above — it is itself a complete working example. Only two files matter. The first is the tool class:

package com.example.myplugin;

import dev.langchain4j.agent.tool.Tool;

/**
 * 插件工具类。
 *
 * 三条硬约定,违反任意一条工具都不会出现在 AI 面前:
 * 1. 必须有无参构造函数——宿主用反射实例化;
 * 2. 工具方法加 @Tool 注解,方法名即工具名,要与 manifest.json 的 tools[].name 一致;
 * 3. 参数与返回值用 String 最省事(复杂结构自己序列化成 JSON 字符串)。
 *
 * @Tool 里的描述是写给 AI 看的,直接决定它会不会在恰当的时候调用这个工具。
 * 写清楚"什么时候用",比写"这个方法做什么"有用得多。
 */
public class MyTools {

    @Tool("统计一段中文文本的字数,返回可读的统计结果。用户问'多少字'时调用。")
    public String countChinese(String text) {
        if (text == null || text.isBlank()) {
            return "输入为空,字数 0";
        }
        long cjk = text.codePoints()
                .filter(cp -> Character.UnicodeScript.of(cp) == Character.UnicodeScript.HAN)
                .count();
        return String.format("总字符 %d,其中汉字 %d", text.length(), cjk);
    }
}

The description in @Tool is written for the AI — it directly decides whether the AI calls your tool at the right moment. Writing "when to use it" is far more useful than "what this method does": the former is what the AI has to judge.

The second is manifest.json, which describes what the plugin is:

{
  "id": "my-plugin",
  "name": "我的插件",
  "version": "1.0.0",
  "description": "一句话说明这个插件替用户做什么。会展示在插件广场的卡片上。",
  "author": "你的名字或团队",
  "homepage": "https://example.com",
  "permissions": [],
  "tools": [
    {
      "name": "countChinese",
      "description": "统计中文文本字数",
      "permissions": []
    }
  ],
  "backendJars": ["my-plugin-1.0.0.jar"]
}

Tool names must match on both sides: tools[].name must equal the Java method name. If they differ, the tool fails to register and the AI cannot see it.

Then package it:

mvn package
mkdir -p dist && cp target/my-plugin-1.0.0.jar manifest.json dist/
cd dist && zip -r ../my-plugin-1.0.0.zip . && cd ..

Note the zip contains the files themselves — don’t nest an extra directory. Unzipping should reveal manifest.json directly, not a folder.

Try it on your own machine before submitting

No need to wait for review. Copy the entire dist/ directory into your local plugin folder and restart AI WorkDeck:

# macOS / Linux
~/.aiworkdeck/plugins/my-plugin/

Your plugin should appear under Plugin Marketplace → Installed. Enable it, then ask something in a conversation that should use your tool and see whether the AI calls it. If it doesn’t, the @Tool description most likely fails to explain when to use it.

What each manifest.json field means

Field說明
idGlobally unique; lowercase letters, digits and hyphens. It can never change once published — it is how upgrades identify "the same plugin".
versionSemantic version. Every submission must be higher than the previous one, or it is rejected.
name / descriptionShown on the marketplace card. Describe what it does for the user, not the implementation.
author / homepageAuthor and project homepage. Optional but recommended — users judge trust by them.
permissionsThe capabilities this plugin uses; see the next section.
toolsTool list. name must equal the Java method name; write the description so its purpose is clear.
backendJarsJAR file names relative to the package root. ../ paths outside the package are not allowed.

permissions: declare honestly — review cross-checks

Four optional values; declare what you use:

  • file_readRead project files
  • file_writeCreate, modify or delete files
  • networkAccess the external network
  • editorOperate the document editor

This is not a sandbox

Plugins run in the same process as the host app, so undeclared behavior cannot be blocked technically — a tool that declares no permissions can still read files. The declaration is not a runtime restriction but review evidence: we cross-check it against the JAR’s static scan. Declaring no network while referencing network APIs — or the reverse — gets the submission rejected.

What review looks at

Every version is reviewed by a human, usually within one to two business days. An automated scan runs first, and its report sits next to your permissions declaration on the reviewer’s desk.

These are rejected outright:

  • Declared permissions don’t match the APIs actually called
  • Custom TrustManager or any other way of bypassing certificate validation
  • Hardcoded IP addresses, or data sent out over plaintext HTTP
  • Reflection into the host’s internal objects (database connections, config services, etc.)
  • Obfuscated or packed code, or anything that hides what the code does
  • 版本號沒有比上一版高

通過後平台會用私鑰對整個包簽名,用戶端安裝時驗簽。這意味著上架之後任何人(包括我們)都無法在不重新簽名的情況下改動包內容。

如果上架後發現問題,我們會撤銷該版本。用戶端拉到撤銷名單後會自動停用它並提示使用者。

給使用者的承諾,也是給你的約束

我們的使用者是律師,他們的機器上有客戶的機密資料。一個外掛拿到的權限跟主程式一樣大——能讀到的東西遠超它自己需要的範圍。

所以審核會偏嚴,被駁回時我們會說明原因。如果你的外掛確實需要某個看起來敏感的能力,在提交說明裡講清楚為什麼,這會讓審核快很多。

Web 外掛:用 HTML/JS 做介面

如果你要做的是一個有介面的東西——一張表單、一個查詢工具、一塊看板——不必寫 Java。包裡放一個 web/ 目錄(純靜態 HTML/JS/CSS,入口 web/index.html),manifest 把 frontendEntry 指向它就行,可以完全不帶 JAR。

權限在這裡是真的。桌面端把 Web 外掛裝進 sandbox iframe,刻意不與應用同源:外掛腳本拿不到宿主 session,一切能力都得經過 postMessage 橋,宿主逐呼叫比對 manifest 的 permissions。跟 JAR 外掛不同,這裡的權限宣告是執行邊界,不是自述。

範本裡的 web/awd-plugin-sdk.js 封裝了這座橋。握手完成前不要呼叫任何方法:

方法參數回傳需要權限
settings.open / settings.update{} / {values}{opened} / {}user / project settings; installation settings stay in trusted host UI
connections.list / connect / disconnect / request{id, operation?, params?}connection state / service JSONnetwork; host-held credentials and explicit developer trust
mcp.servers / mcp.call{} / {serverId, name, arguments}{servers} / MCP CallToolResultnetwork + project access; public HTTPS Streamable HTTP
billing.status / billing.checkout{connectionId?}{provider, active, plan?} / {opened}host purchase entitlement or developer subscription; no automatic charge
services.request / services.cancel{service, operation, params, requestId} / {requestId}operation resultexplicit service catalog; per-operation permission checks
context.get{}{ pluginId, projectId, language, theme, themeTokens }-
files.list{}{ files: [{ path, name, size }] }file_read
im.downloadAttachment{ conversationId, attachmentId }{ started, name }im(服务端附件权限校验,无需项目写权限)
notifications.publish{ eventKey, title, body?, actionState?, target? }notificationnotifications(当前账号收件箱;目标只打开本插件)
notifications.resolve{ eventKey, state }notificationnotifications(只更新本插件消息,已读不会完成业务)
im.request{ action, params }objectim(宿主 0.61.0+;固定聊天动作,项目归档另需 file_write)
chat.addFile{ path }{}file_read(宿主 0.61.0+;添加材料,不自动发送)
files.prepareDrag{ path }{ fileId, dragToken, dragType, expiresAt, ... }file_read(宿主 0.61.0+;短时单次拖拽票据)
files.endDrag{}{}file_read(宿主 0.61.0+)
files.read{ path }{ path, content, truncated }file_read
ui.toast{ message }{}-
storage.get{ key }{ key, value }-
storage.set{ key, value }{}-
evidence.link{ anchor: { selection: true } | { quote }, docPath?, targets: [{ path, locator?, relation?, method?, note? }] }{ linkKey, targetIds }editor
evidence.list{ docPath?, path?, sectionPath?, status? }{ links: [{ linkKey, docPath, anchorText, sectionPath, status, targets }] }file_read
evidence.locate{ linkKey, targetId? }{}editor
tools.invoke{ name, args? }{ output }-(工具须为本插件 manifest 声明)
chat.send{ prompt }{}-(上限 4000 字)
ui.openFile{ path }{}file_read
doc.exec{ action, params? }{ result }editor(宿主 0.27.4+;action 为 doc_/sheet_/slide_ 安全子集)
doc.active{}{ fileId, kind }editor(宿主 0.27.4+)
events.subscribe{ events: ["files.changed" | "selection.changed" | "project.switched"] }{ subscribed }按事件(宿主 0.27.4+)
events.unsubscribe{ events }{ subscribed }-
ai.request{ prompt, system?, purpose? }{ text, modelId }ai(宿主 0.27.4+;16000 字符、10 次/分钟,走用户 Credits)
settings.get{ key }{ key, value }-(宿主 0.28+;manifest 顶层 settings 声明的配置项,secret 项拿不到)
<script src="awd-plugin-sdk.js"></script>
<script>
  const ctx = await awd.ready();                   // { pluginId, projectId, language, theme, themeTokens }
  const files = await awd.files.list();            // 需 file_read
  const doc = await awd.files.read(files[0].path); // { path, content, truncated }
  await awd.ui.toast('已完成');
  await awd.storage.set('draft', { title: 'x' });  // 插件级 KV,上限 64 KB
</script>

錯誤以 rejected Promise 拋出,err.code 是錯誤碼:權限不足 permission_denied,宿主不認得的方法 unknown_method。files.read 文字上限 5 MB(超出時 truncated 為 true),storage 每個外掛總量上限 64 KB。

主題頻道(v2.6)

握手的 context 帶 theme('light'|'dark')與 themeTokens(一份 --awd-* CSS 變數表);此後主題切換,宿主還會推送 { type: 'theme', theme, tokens }。SDK 收到即自動掛 data-theme、awd-theme-light/awd-theme-dark class,把 tokens 逐個寫成 CSS 自訂屬性——外掛 CSS 直接用 var(--awd-surface) 這類語意 token 即可跟著主題走,零 JS。需要腳本聯動的場景用 awd.theme.get() 讀目前值、awd.theme.onChange(cb) 訂閱切換。宿主模擬器右上角有「切換到深色/淺色」按鈕,不裝桌面端也能調兩個主題。

範本還帶一個宿主模擬器 dev/host-simulator.html:它假扮桌面端完成握手、用假資料實現全部方法、逐條列印橋訊息。起個本地靜態服務就能在瀏覽器裡開發除錯,不需要安裝 AI WorkDeck。

python3 -m http.server 8000
# -> http://localhost:8000/dev/host-simulator.html

直接雙擊開啟(file://)多半是空白:瀏覽器不給 file:// 下的 sandbox iframe 載入子頁面。靜態服務也不能改寫 URL——npx serve 預設的 clean URLs 會把 /web/index.html 重新導向到 /web,頁面裡相對引用的 SDK 隨之 404。

SDK 1.6 平台接口(新版宿主预览)

下载 Web 模板后,Node.js 20+ 可直接运行内附 scripts/plugin-dev.mjs,无需克隆宿主仓库。scripts/ 与 sdk/plugin-sdk/ 必须保持相对目录。先在模拟器完成界面调试,再在兼容的本地开发宿主测试权限、项目切换、禁用、取消和重连。示例域名只是占位符,不是可调用服务。

views 声明 sidebar 或 workspace 视图、project 或 global 作用域;每种位置最多一个视图,入口必须是包内 web/ 下文件。侧栏停靠可声明 left/right/bottom。settings 的 scope 默认为 installation,另支持 user/project;插件只能更新自身非敏感的 user/project 设置,installation 设置由可信宿主设置页维护。secret 不回传 iframe;用户服务凭据请使用 connections。

connections 声明固定 HTTPS 服务来源及固定 GET/POST 操作路径,支持公开服务、Bearer 或 OAuth2 公共客户端 PKCE。awd.connections.connect 只打开宿主连接流程;令牌只在宿主保存。插件不能传任意 URL、Authorization 或任意宿主配置键。BYO 服务与宿主 AI、语音等通道是两种独立选择;使用宿主通道须相应权限及现有用量约束。

OAuth 使用 Authorization Code + S256 PKCE:单机宿主采用实际监听端口的 http://127.0.0.1:<port>/api/plugins/oauth/callback,服务端部署需显式配置 ai.plugins.oauth-redirect-uri 为可信 HTTPS 地址,并在供应商登记。过期且不能刷新或被服务以 401/403 拒绝的连接返回 needsReconnect,用户应在宿主重新登录。

MCP 当前仅支持公网 HTTPS Streamable HTTP,不支持 stdio、本地进程、私网地址或重定向。须声明 network、服务和全部工具;工具名和 inputSchema 必须与远端工具一致。参数根类型为 object,支持 object/properties/required/布尔 additionalProperties、array/items、string/enum、boolean/integer/number,以及 title/description;不支持 $ref、oneOf、nullable 或其他 JSON Schema 关键字。深度最多 8,单对象最多 64 属性,每服务最多 64 工具。connectionId 引用同插件同源连接;credentialKey 仅为已声明 installation secret 的兼容路径,不能读取任意全局设置。

通过 dev/install 装入的插件默认无远程服务信任。管理员须在可信宿主设置页明确授权;授权绑定完整 manifest 的 SHA256,声明变化后重新授权。开发包不得携带 .awd-dev、.awd-service-trust 或 .awd-bundled 标记。授权不解除开发安装的 JAR/本地进程禁令。

speech、tasks、clipboard 是宿主服务权限;services.request 仅接受宿主目录中的服务/操作,并非任意后端代理。用 services.cancel 取消同插件请求。无权限、宿主能力不存在或宿主版本过旧时,应显示明确说明,不能悄悄改走收费或外部服务。

平台收费目前仅支持插件购买权益,提交价格是一次性买断价;不提供按调用代收或开发者分账接口。开发者自有服务可用 connections.billing 声明订阅状态操作和结账页。billing.checkout 只打开结账流程,不能把返回 opened 当成支付成功。

平台 billing.status 主动刷新当前账户权益,refreshed 为 false 表示本次权益刷新未成功,active 可能来自仍有效的缓存,不能作为新支付确认。checkout 会等待浏览器打开结果并报告失败。服务插件须在真实宿主验收登录、令牌过期、订阅、MCP 错误、账户切换及共享/BYOK 选路;合成供应商只能证明宿主集成,不等于真实 OAuth 或支付认证。

先在开发者中心完成免费资格审核,再生成投稿令牌并在本机临时环境变量 AWD_PLUGIN_TOKEN 中提供,不写入源码、Agent 指令或 zip。合法包仍进入 pending,等待人工审核与签名。开发安装、模拟器通过及 CLI 校验均不代表已上架或获准执行后端代码。

node scripts/plugin-dev.mjs validate .
node scripts/plugin-dev.mjs package . --out ../my-plugin.zip
# Start the synthetic UI simulator, then open /dev/host-simulator.html
python3 -m http.server 8000
# Explicit development installation: select an isolated imported project/folder first.
# Replace PORT with the isolated host port. AWD_SESSION_ID is supplied locally if required.
# Do not paste credentials into source or an Agent prompt.
node scripts/plugin-dev.mjs install --endpoint http://127.0.0.1:PORT --project-id PROJECT_ID --folder-id FOLDER_ID
# Explicit marketplace submission only; provide AWD_PLUGIN_TOKEN locally.
node scripts/plugin-dev.mjs submit . --endpoint https://www.aiworkdeck.com/api/plugins --price-cents 0
node scripts/plugin-dev.mjs submissions --endpoint https://www.aiworkdeck.com/api/plugins
{
  "id": "my-service-plugin",
  "name": "Service panel",
  "version": "1.0.0",
  "description": "SDK 1.6 host preview example",
  "author": "Example developer",
  "frontendEntry": "web/index.html",
  "permissions": [
    "network",
    "ai",
    "speech",
    "tasks",
    "clipboard"
  ],
  "settings": [
    {
      "key": "language",
      "label": "Language",
      "type": "string",
      "scope": "user",
      "default": "zh-CN"
    },
    {
      "key": "summaryStyle",
      "label": "Summary style",
      "type": "string",
      "scope": "project",
      "default": "brief"
    }
  ],
  "contributes": {
    "views": [
      {
        "id": "service-panel",
        "name": "Service panel",
        "entry": "web/index.html",
        "placement": "sidebar",
        "scope": "project",
        "defaultDock": "left",
        "allowedDocks": [
          "left",
          "right",
          "bottom"
        ]
      }
    ],
    "connections": [
      {
        "id": "service",
        "label": "My service",
        "baseUrl": "https://api.example.com",
        "auth": {
          "type": "bearer"
        },
        "operations": {
          "summarize": {
            "method": "POST",
            "path": "/v1/summarize"
          },
          "status": {
            "method": "GET",
            "path": "/v1/subscription"
          }
        },
        "billing": {
          "statusOperation": "status",
          "checkoutUrl": "https://example.com/subscribe"
        }
      }
    ],
    "mcpServers": [
      {
        "id": "remote",
        "transport": "streamable-http",
        "url": "https://api.example.com/mcp",
        "connectionId": "service",
        "tools": [
          {
            "name": "summarize",
            "description": "Summarize supplied text",
            "inputSchema": {
              "type": "object",
              "properties": {
                "text": {
                  "type": "string"
                }
              },
              "required": [
                "text"
              ],
              "additionalProperties": false
            }
          }
        ]
      }
    ]
  }
}

交给 Agent 的完整任务指令

请基于下载的 AI WorkDeck Web 插件模板实现我的需求:[填写功能、输入、输出与验收标准]。
先读 README.md、sdk/plugin-sdk/awd-plugin-sdk.d.ts 和 manifest.schema.json,再规划最小权限。SDK 1.6 新能力需要尚未发版的新宿主;不要把模拟器成功当作现有客户端兼容证明。
只改插件目录。保留 scripts/ 与 sdk/plugin-sdk/ 相对结构;不要改 SDK 来绕过权限。界面遵循 --awd-* 主题变量、键盘可达、窄侧栏及明暗主题。
需要侧栏或工作区时声明 contributes.views;需要设置时区分 installation/user/project。用户密钥只通过宿主 connections 流程,不能进 iframe、日志、仓库或包。
选择宿主 AI/语音/任务/剪贴板通道或开发者自有 HTTPS 服务;前者声明相应权限,后者声明 network、固定 connections 操作。MCP 只用远程 HTTPS Streamable HTTP,显式声明每个工具和受支持的 inputSchema;禁止 stdio、私网、任意 URL 和任意系统配置读取。
用模拟器合成数据实现成功、权限拒绝、连接断开、失败重试与取消,不访问真实客户资料、登录或付费服务。真实宿主开发安装只使用我指定的隔离项目;服务访问须管理员在宿主设置页明确授权,manifest 改动后重新授权。
执行 node scripts/plugin-dev.mjs validate .;然后 node scripts/plugin-dev.mjs package . --out ../my-plugin.zip。检查压缩包无 .env、令牌、私钥和宿主信任标记。输出修改、验证结果、未验证边界与 zip 路径。
只有我明确要求提交时,再使用我本机提供的 AWD_PLUGIN_TOKEN,执行 submit 命令;不打印令牌。提交后报告 pending 审核状态,不声称已上架、已支付或已发布。

MCP Streamable HTTP specification

準備好了

打包成 zip,提交後我們會盡快審核。