Plugin Development

写一个插件

插件给 AI WorkDeck 增加新的工具,让 AI 在对话里能调用你写的代码。下面这份指南从一个能跑通的最小例子开始,到提交上架为止。

01

先想清楚:你需要的是插件还是 Skill

这两个东西经常被搞混,选错会白做很多工作。区别只有一句话:要写代码才能办到的事用插件,靠说清楚就能办到的事用 Skill。

场景该用
调用某个外部系统的接口取数据插件
解析一种特殊格式的文件插件
让 AI 按你所在律所的格式写审查意见Skill
规定 AI 处理某类案件时的步骤与产出结构Skill

Skill 是纯文本(提示词 + 触发词),提交即用、无需审核,写起来快得多。如果你的需求 Skill 能满足,直接去写一个 Skill,不用往下读了。

02

五分钟跑通第一个插件

插件是一个 Java 工程,编译出 JAR,配一份 manifest.json,打成 zip 提交。需要 JDK 21 和 Maven。

下载上面的模板工程,它本身就是一个能跑的完整例子。核心只有两个文件。第一个是工具类:

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);
    }
}

@Tool 里的描述是写给 AI 看的,它直接决定 AI 会不会在恰当的时候调用你的工具。写「什么时候该用它」比写「这个方法做了什么」有用得多——AI 需要判断的是前者。

第二个是 manifest.json,描述这个插件是什么:

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

工具名必须两边对上tools[].name 要等于 Java 里的方法名,不一致的话工具注册不上,AI 看不见它。

然后打包:

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

注意 zip 里是文件本身,不要多套一层目录——解压出来应该直接看到 manifest.json,而不是一个文件夹。

03

提交前先在自己机器上试

不用等审核。把 dist/ 整个目录复制到本机的插件目录,重启 AI WorkDeck 就能看到:

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

在「插件广场 → 已安装」里应该出现你的插件。启用后,在对话中提一个会用到你工具的问题,看 AI 是否调用了它。没调用的话,八成是 @Tool 的描述没写清楚使用时机。

04

manifest.json 每个字段的意思

字段说明
id全局唯一,小写字母数字连字符。一旦上架就不能改——它是升级时认定「同一个插件」的依据。
version语义化版本。每次提交都要比上一版高,否则会被拒。
name / description展示在插件广场卡片上。描述写清楚替用户做什么,别写技术实现。
author / homepage作者与项目主页,可选但建议填,用户会据此判断是否信任。
permissions这个插件会用到的能力,见下一节。
tools工具清单。name 必须等于 Java 方法名;description 用中文写清楚用途。
backendJarsJAR 文件名列表,相对包根目录。不能用 ../ 指到目录外。
05

permissions:如实声明,审核会交叉核对

四个可选值,按需声明:

  • file_read读取项目文件
  • file_write创建、修改或删除文件
  • network访问外部网络
  • editor操作文档编辑器

这不是沙箱

插件与主程序在同一个进程里运行,技术上拦不住未声明的行为——声明了空权限的工具,代码里照样能读文件。所以这不是运行时限制,而是审核依据:我们会拿它跟 JAR 的静态扫描结果交叉核对。声明了没有 network 却引用网络 API,或者用到了却没声明,都会被驳回。

06

审核会看什么

每个版本都要人工过一遍,通常一到两个工作日。提交后自动扫描先跑一遍,扫描报告和你的 permissions 声明会一起摆在审核台上。

这些情况会被直接驳回:

  • 声明的 permissions 与实际调用的 API 对不上
  • 自定义 TrustManager 或以其他方式绕过证书校验
  • 硬编码的 IP 地址、明文 HTTP 外传数据
  • 通过反射访问主程序内部对象(数据库连接、配置服务等)
  • 代码混淆、加壳,或任何让人看不懂它在干什么的处理
  • 版本号没有比上一版高

通过后平台会用私钥对整个包签名,客户端安装时验签。这意味着上架之后任何人(包括我们)都无法在不重新签名的情况下改动包内容。

如果上架后发现问题,我们会撤销该版本。客户端拉到撤销名单后会自动停用它并提示用户。

07

给用户的承诺,也是给你的约束

我们的用户是律师,他们的机器上有客户的机密材料。一个插件拿到的权限跟主程序一样大——能读到的东西远超它自己需要的范围。

所以审核会偏严,被驳回时我们会说明原因。如果你的插件确实需要某个看起来敏感的能力,在提交说明里讲清楚为什么,这会让审核快很多。

08

Web 插件:用 HTML/JS 做界面

如果你要做的是一个有界面的东西——一张表单、一个查询工具、一块看板——不必写 Java。包里放一个 web/ 目录(纯静态 HTML/JS/CSS,入口 web/index.html),manifest 把 frontendEntry 指向它就行,可以完全不带 JAR。

权限在这里是真的。桌面端把 Web 插件装进 sandbox iframe,刻意不与应用同源:插件脚本拿不到宿主会话,一切能力都得经过 postMessage 桥,宿主逐调用比对 manifest 的 permissions。跟 JAR 插件不同,这里的权限声明是执行边界,不是自述。

模板里的 web/awd-plugin-sdk.js 封装了这座桥。握手完成前不要调用任何方法:

方法参数返回需要权限
context.get{}{ pluginId, projectId, language, theme, themeTokens }-
files.list{}{ files: [{ path, name, size }] }file_read
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) 这类语义令牌即可跟着主题走,零 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。

准备好了

打包成 zip,提交后我们会尽快审核。