先想清楚:你需要的是插件还是 Skill
这两个东西经常被搞混,选错会白做很多工作。区别只有一句话:要写代码才能办到的事用插件,靠说清楚就能办到的事用 Skill。
| 场景 | 该用 |
|---|---|
| 调用某个外部系统的接口取数据 | 插件 |
| 解析一种特殊格式的文件 | 插件 |
| 让 AI 按你所在律所的格式写审查意见 | Skill |
| 规定 AI 处理某类案件时的步骤与产出结构 | Skill |
Skill 是纯文本(提示词 + 触发词),提交即用、无需审核,写起来快得多。 如果你的需求 Skill 能满足,直接去写一个 Skill,不用往下读了。
五分钟跑通第一个插件
插件是一个 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,而不是一个文件夹。
提交前先在自己机器上试
不用等审核。把 dist/ 整个目录复制到本机的插件目录,重启 AI Workdeck 就能看到:
# macOS / Linux
~/.aiworkdeck/plugins/my-plugin/在「插件广场 → 已安装」里应该出现你的插件。启用后,在对话中提一个 会用到你工具的问题,看 AI 是否调用了它。没调用的话, 八成是 @Tool 的描述没写清楚使用时机。
manifest.json 每个字段的意思
| 字段 | 说明 |
|---|---|
id | 全局唯一,小写字母数字连字符。一旦上架就不能改——它是升级时认定「同一个插件」的依据。 |
version | 语义化版本。每次提交都要比上一版高,否则会被拒。 |
name / description | 展示在插件广场卡片上。描述写清楚替用户做什么,别写技术实现。 |
author / homepage | 作者与项目主页,可选但建议填,用户会据此判断是否信任。 |
permissions | 这个插件会用到的能力,见下一节。 |
tools | 工具清单。name 必须等于 Java 方法名;description 用中文写清楚用途。 |
backendJars | JAR 文件名列表,相对包根目录。不能用 ../ 指到目录外。 |
permissions:如实声明,审核会交叉核对
四个可选值,按需声明:
file_read读取项目文件file_write创建、修改或删除文件network访问外部网络editor操作文档编辑器
这不是沙箱
插件与主程序在同一个进程里运行,技术上拦不住未声明的行为——声明了 空权限的工具,代码里照样能读文件。所以这不是运行时限制,而是审核依据: 我们会拿它跟 JAR 的静态扫描结果交叉核对。声明了没有 network 却引用网络 API,或者用到了却没声明,都会被驳回。
审核会看什么
每个版本都要人工过一遍,通常一到两个工作日。提交后自动扫描先跑一遍, 扫描报告和你的 permissions 声明会一起摆在审核台上。
这些情况会被直接驳回:
- 声明的 permissions 与实际调用的 API 对不上
- 自定义 TrustManager 或以其他方式绕过证书校验
- 硬编码的 IP 地址、明文 HTTP 外传数据
- 通过反射访问主程序内部对象(数据库连接、配置服务等)
- 代码混淆、加壳,或任何让人看不懂它在干什么的处理
- 版本号没有比上一版高
通过后平台会用私钥对整个包签名,客户端安装时验签。这意味着上架之后 任何人(包括我们)都无法在不重新签名的情况下改动包内容。
如果上架后发现问题,我们会撤销该版本。客户端拉到撤销名单后会自动 停用它并提示用户。
给用户的承诺,也是给你的约束
我们的用户是律师,他们的机器上有客户的机密材料。一个插件拿到的权限 跟主程序一样大——能读到的东西远超它自己需要的范围。
所以审核会偏严,被驳回时我们会说明原因。如果你的插件确实需要某个 看起来敏感的能力,在提交说明里讲清楚为什么,这会让审核快很多。
