文档
编写一个辅助工具插件
构建一个独立的读取工具,验证其清单和参数,有意启用它,并测试您实际收到的工作边界。
自动翻译暂时不可用;正文为英文,界面为您的语言。 查看英文原文
选择助手扩展格式
生产助手扩展加载器接受包含plugin.json 和捆绑的JavaScript 条目的目录。它将模块导入到 Bun 工作程序中,并将其 aiTools 记录中声明的函数公开给助手。当前支持的功能是 ai:tool。本指南构建了一个返回坐标的只读螺栓圆计算器;它不会将几何图形插入到绘图中。
/api/plugins 下还有一个浏览器插件 ZIP API,其中包含 manifest.json、main、浏览器权限和引导资产。该安装程序不会启用此处描述的工作插件。这 插件 HTTP 参考 分隔格式的文件; 插件 解释了两个表面。不要仅通过重命名清单来在它们之间进行转换。
辅助工具是在应用程序运行时边界内执行的代码。主机能力检查限制通过主机 RPC 的访问,但当前工作线程不是操作系统沙箱。使用其执行适合该环境的扩展代码,并使其声明的读/写行为与函数实际执行的行为一致。
创建目录和同意记录
使用您想要扩展的 sidecar 的确切应用程序数据目录。创建此结构,保持目录 ID 与清单的反向 DNS ID 相同:
<dataDir>/plugins/
enabled.json
com.example.bolt-circle/
plugin.json
index.js 生产加载程序将enabled.json 读取为安全标识符的JSON 数组。它会忽略重复/无效的条目,格式错误或丢失的 JSON 意味着未启用任何插件。不加载未列出的文件夹。加载程序还会拒绝文件夹/清单 ID 不匹配以及请求不受支持的功能的清单。
加载发生在助手插件服务初始化时。它不配置文件系统观察器进行热重载。首先准备并验证文件,然后启用/重新启动该实例。失败的插件会记录警告,而其他插件和应用程序继续启动;检查这些警告,而不是假设启用的文件条目保证工具正在运行。
申报精准工具合约
这个完整的清单命名了一个读取工具。贡献 ID 是面向模型的本地标识符;条目命名模块导出的函数。这些字符串可能有所不同,正如这里的bolt_circle 和boltCircle 所做的那样。
{
"id": "com.example.bolt-circle",
"name": "Bolt circle calculator",
"version": "1.0.0",
"apiVersion": "1",
"entry": "index.js",
"capabilities": ["ai:tool"],
"contributes": {
"aiTools": [{
"id": "bolt_circle",
"description": "Calculate equally spaced XY hole centers around the origin for a bolt circle. Returns coordinates without changing a drawing.",
"kind": "read",
"entry": "boltCircle",
"parameters": {
"type": "object",
"properties": {
"count": { "type": "integer", "minimum": 1, "maximum": 64 },
"radius": { "type": "number", "exclusiveMinimum": 0 }
},
"required": ["count", "radius"],
"additionalProperties": false
}
}]
}
} SDK 需要 3–128 字符的反向 DNS ID、带有可选预发布的语义major.minor.patch 版本、API 版本“1”、非空名称/条目以及可识别的功能。工具 ID 以小写字母开头,最多可使用小写字母、数字或下划线 40 个字符。描述包含 10–2,000 个字符。工具类型是读或写;不存在破坏性的扩展类型。
保持参数模式小且具有描述性。它的作用是帮助模型产生有效的参数,并让适配器在执行之前拒绝普通错误。它不会取代代码内部的验证。
实现独立模块
该条目使用清单中指定的条目函数导出 aiTools。它再次验证运行时参数并返回一个小的 JSON 兼容结果。有限坐标算术和有界计数可防止格式错误的输入产生无界列表或无意义的输出。
export const aiTools = {
boltCircle: async (args) => {
if (!args || typeof args !== "object") {
return { ok: false, error: "Expected count and radius" }
}
const { count, radius } = args
if (!Number.isInteger(count) || count < 1 || count > 64) {
return { ok: false, error: "count must be an integer from 1 to 64" }
}
if (typeof radius !== "number" || !Number.isFinite(radius) || radius <= 0) {
return { ok: false, error: "radius must be a positive finite number" }
}
const holes = Array.from({ length: count }, (_, index) => {
const angle = 2 * Math.PI * index / count
return {
x: Number((radius * Math.cos(angle)).toFixed(4)),
y: Number((radius * Math.sin(angle)).toFixed(4))
}
})
return { ok: true, units: "mm", holes }
}
} Count 4 and radius 10 return centers at approximately (10,0), (0,10), (−10,0), and (0,−10), in counter-clockwise order around the origin. The result deliberately contains no database mutation or drawing identifier. A client wanting to insert holes must use a separate supported application action with its own access and permission behavior.
将依赖项捆绑到条目中,而不是假设 Node 样式的相对导入已解析。主机读取条目文本并通过 blob 模块导入它。模块顶级代码在导入时运行,因此可以避免仅加载计算器带来的副作用。
验证清单和两个参数路径
在将插件添加到enabled.json 之前进行验证。此开发检查使用与应用程序相同的 SDK 清单解析器和 AI 架构适配器。将脚本放置在具有这些依赖项的工作区中,例如 apps/sidecar,并将绝对插件目录作为其参数传递。
import { join } from "node:path"
import { Effect } from "effect"
import { parseManifest } from "@cadcamfun/plugin-sdk"
import { pluginParameters } from "@cadcamfun/ai"
const directory = process.argv[2]
if (!directory) throw new Error("Pass the absolute plugin directory")
const manifest = await Effect.runPromise(parseManifest(
await Bun.file(join(directory, "plugin.json")).text()))
if (manifest.capabilities.some((capability) => capability !== "ai:tool")) {
throw new Error("This production loader accepts only ai:tool")
}
const contribution = manifest.contributes?.aiTools?.[0]
if (!contribution) throw new Error("No tool contribution")
const parameters = pluginParameters(contribution.parameters)
console.log(parameters.safeParse({ count: 4, radius: 10 }).success) // true
console.log(parameters.safeParse({ count: 0, radius: 10 }).success) // false
const module = await import(join(directory, manifest.entry))
console.log(await module.aiTools[contribution.entry]({ count: 4, radius: 10 }))
console.log(await module.aiTools[contribution.entry]({ count: 0, radius: 10 })) 最后两个调用独立于适配器拒绝来测试功能级行为。 SDK 验证清单结构,但生产加载程序添加自己的功能和文件夹 ID 规则。测试所有三个边界:声明、参数适配器和实际模块函数。
启用并发现实际的工具标识符
验证完毕后,在应用程序数据目录中启用该ID:
["com.example.bolt-circle"] 重新启动/重新初始化预期的 sidecar 服务并检查日志中是否有负载警告。然后列出 GET /api/ai/actions。读取插件工具以及嵌入式助手都在那里公开;政策可以缩小目录范围。工具标识符是根据插件 ID 和本地工具 ID 生成的,经过标准化并限制为主机的标识符大小。
对于此清单,当前派生 ID 是 plugin_com_example_bolt_circle_bolt_circle。发现它而不是在每个客户端中重新实现命名规则。长插件 ID 可能会被截断,以便为工具后缀留出空间。目录的描述包括插件名称,以便用户可以识别来源。
无论普通权限模式如何,写入工具都会在嵌入式助手中标记为人工批准。外部桥仅公开读取插件工具,并拒绝写入扩展调用,即使具有读/写令牌也是如此。保持种类准确;应用程序无法从 JSON 模式推断任意模块副作用。
通过范围外部访问调用读取工具
将应用程序发行的桥代币用于真实项目。其承载仅打开 GET /api/ai/actions 和 POST /api/ai/actions/invoke。它不允许调用者安装插件、编辑凭据或枚举不相关的项目。令牌的项目/模式由操作处理程序检查;对于独立的计算器客户端来说,应用程序范围的令牌是不必要的。
{
"projectId": "replace-with-your-project-id",
"tool": "plugin_com_example_bolt_circle_bolt_circle",
"input": { "count": 4, "radius": 10 }
} curl --fail-with-body \
-H "Authorization: Bearer $CADCAMFUN_BRIDGE_TOKEN" \
-H "Content-Type: application/json" \
--data-binary @bridge-call.json \
"$CADCAMFUN_URL/api/ai/actions/invoke" 插件输入是声明的参数对象;与内置分组工具不同,此读取插件不需要输入内部的动作鉴别器。桥用工具/操作元数据包装返回值;操作已运行。函数级别的 ok=false 仍然是一个可读的结果,而不是证明有用的坐标列表的 HTTP 传输成功。
桥接策略检查 tool.use 和bridge.use。丢失、过期、撤销或类型错误的令牌会导致授权失败。绑定到另一个项目的令牌并不是扩大此调用范围的方法。
了解架构转换和能力限制
当可表示时,适配器将贡献的 JSON 模式转换为 zod。如果转换失败,架构超过 20,000 序列化字符,或者它不描述对象,则会回退到任意对象架构。这就是示例函数检查计数和半径本身的原因。受支持模式的负固定装置在本地传递并不能证明每个模式功能都将保持可表示性。
SDK可识别drawing:read、drawing:write、toolpath:read、storage、ui:panel和ai:tool;绘图写入意味着绘图读取。生产助手加载程序当前仅接受 ai:tool。将绘图:写入此清单会导致其被跳过,而不是授予绘图编辑器上下文。
一般工作引导程序包含占位符绘图/工具路径/UI 处理程序。这些处理程序不是生产几何集成。同样,声明后处理器或操作贡献并不会使辅助加载程序将它们连接到制造注册表中。当功能需要更改真实 CAD/CAM 数据时,选择支持的应用程序操作。
围绕已实施的工作人员生命周期进行设计
SDK 描述了一个带有清单、激活、可选清理和停用挂钩的模块。当前的生产AI路径导入条目并调用导出的aiTools函数。其工作线程激活/停用 RPC 处理程序确认生命周期消息,但不调用导出的模块挂钩。因此,计算器可以使用此处显示的最小模块,而不依赖于激活回调。
主机为每个启用的插件启动一个工作线程,将回复与 RPC ID 相关联,并跟踪待处理的调用。函数异常使该调用失败;集中的真实工作人员测试表明,稍后的调用仍然可以成功。对预期的无效输入返回结构化拒绝,并为意外的计算错误保留抛出的失败。
调用超时会终止其工作线程并立即使插件出现故障。工作线程崩溃和发布失败也会删除正在运行的实例并将其标记为故障。崩溃历史记录有一个十分钟的窗口,但当前处理程序在第一次崩溃时出现故障;它不会等待三次成功的重新启动尝试。更正源并故意重新加载/重新启用,而不是假设自动重新启动。
按比例保持运行时间保证
生产加载程序为普通生命周期/命令 RPC 提供 10 秒的主机预算。 AI 工具调用有单独的 30 秒截止时间。主机将插件限制为每分钟 2、000 个调用和 64 个待处理调用。创建工作线程时,它会将 256 MB 资源限制设置传递给 Bun。这些是运行时控制,而不是单独的进程或操作系统遏制策略。
引导程序从空环境开始,并尝试删除选定的进程/网络全局变量。已尽最大努力进行删除,并且一些 Bun 全局变量仍然可以访问。 Capability checking is therefore meaningful for the APIs the host supplies, but cannot be presented as complete isolation of arbitrary JavaScript.
清单架构中存在可选的签名和开发字段。当前worker安装路径记录了签名文本,没有进行密码验证。不要标记已验证的发布者,因为存在该字段。独立的源代码审查和安装策略仍然与模式接受的决策分开。
十分钟内崩溃 3 次的插件会自动禁用,辅助工具调用会在 30 秒后停止。两个值都是常数 apps/sidecar/src/plugins/host.ts.
更新、删除和诊断工具
对于更新,请在重新启动目标服务之前编辑并验证捆绑的模块/清单。相同扩展名时保持其ID稳定,并增加版本以识别加载的内容。要在将来的初始化时停止加载它,请从enabled.json 中删除其ID 并重新启动/重新初始化。单独更改文件不会处置已经运行的工作程序。
如果该工具丢失,请检查日志中启用的数组、确切的文件夹/清单 ID 相等性、支持的功能列表以及清单解码错误。 NoSuchTool 通常意味着条目未命名导出的 aiTools 函数。参数拒绝指向模式或函数防护。超时表示工作受阻或未解决的承诺。浏览器 ZIP 启用/配置端点不管理此工作人员同意文件。
重点报道在 apps/sidecar/test/plugins-ai-tools.test.ts 对于实际工人和 ai-plugin-tools.test.ts 用于生产加载、参数验证、读/写暴露和桥接行为。实施来源是 packages/plugin-sdk/src/manifest.ts, packages/ai/src/tools/plugin-tool.ts, apps/sidecar/src/plugins/host.ts, 和 services/ai-plugins.ts。使用这些边界来确定成功的测试实际证明了什么。
合同规则一览
| 物品 | 约束 | 如果违反会发生什么 |
|---|---|---|
工具 id | ^[a-z][a-z0-9_]{0,39}$ | 清单解码失败;该插件未加载。 |
description | 10–2,000 个字符 | 清单解码失败。 |
kind | read 或 write;没有破坏性的 | 清单解码失败。 |
parameters | JSON Schema 对象,序列化时最多 20,000 个字符 | 过大或不可转换的模式会回退到接受任何对象。 |
| 能力 | 仅有的 ai:tool 在助理主持人中 | 该插件将被跳过并带有警告。 |
| 文件夹名称 | 与清单相同 id | 该插件将被跳过,并出现命名这两个值的警告。 |
| 普通通话预算 | 默认情况下每次调用 5 秒 | 工作线程像任何无响应的插件一样停止。 |
| 辅助工具通话预算 | 30 s | 调用返回失败结果。 |
| 记忆 | 256 MB 堆 | 工人被杀。 |
| 速度 | 2,000 calls per plugin per minute, 64 pending calls | 拒绝询问有关费率的电话;满队列失败 plugin RPC queue is full. |
| 崩溃和超时 | 已计算超过 10 分钟,限制为 3 | 每次崩溃或超时都会终止工作程序并将插件标记为故障。 |
写下模型的描述。说明该工具计算的内容、单位以及何时使用它;例如,“返回螺栓圆的孔中心(以毫米为单位)。当用户要求直径上均匀分布的孔时使用。”模糊的描述会导致工具在错误的时间被调用或根本不被调用。
预发布清单
- 将条目捆绑到一个独立的模块中。工作线程无法在运行时解析工作区导入。
- 跑步
parseManifest在决赛上plugin.json并修复每个报告的问题。 - 使用有效、边界和无效参数直接调用每个导出的工具函数,并确认无效输入会产生可读的错误结果,而不是转义的异常。
- 保持结果较小且与 JSON 兼容。助手阅读它们,大量的输出挤满了上下文。
- 选择
read除非该工具改变了一些东西。一个write工具会在每次调用之前提示用户。 - 将文件夹放在下面
<data dir>/plugins/<id>/,将 ID 添加到enabled.json,重新启动sidecar,并从助手的工具列表中确认派生工具ID。 - 撞
version对于您发布的每一个更改,并保留id稳定,因此派生工具 ID 不会更改。
安装前检查清单
import { decodeManifest, requestedCapabilities } from "@cadcamfun/plugin-sdk"
import { Effect } from "effect"
const raw = JSON.parse(await Bun.file(process.argv[2] ?? "plugin.json").text())
const manifest = await Effect.runPromise(decodeManifest(raw)) // throws a SchemaError naming the bad field
console.log(manifest.id, manifest.version, requestedCapabilities(manifest))
for (const tool of manifest.contributes?.aiTools ?? [])
console.log(`plugin_${manifest.id}_${tool.id}`, tool.kind) | 常见错误 | 使固定 |
|---|---|
"version": "1.0" | 使用三个数字: 1.0.0 |
"apiVersion": 1 | 它是一个字符串: "1" |
插件 ID gear 或 My.Plugin | 反向 DNS,小写: com.example.gear |
工具编号 Lookup-Thread | 小写、数字和下划线、字母在前、≤ 40 个字符 |
| 工具说明少于 10 个字符 | 描述它的作用以及何时使用它 |
声明两者 drawing:read 和 drawing:write | 允许,但是 drawing:write 单独已经意味着阅读 |
存储键开头为 / 或含有 .. | 使用相关键,例如 presets/m2 |