构建扩展
服务
暴露带类型和 schema 验证的方法,供其他扩展和 AI 代理调用。
服务 是你的扩展暴露的一组可调用方法。其他扩展通过服务代理调用它们,内置的 AI 代理可以将其作为工具调用。服务将你的扩展转化为可重用的基础设施——OCR 引擎、FFmpeg 封装器、YouTube 下载器。
契约优先模式
真实的服务扩展(ffmpeg、youtube-tools)在提供者和消费者之间共享一个契约模块,这样双方在编译时就方法名称、输入和输出达成一致。
1. 定义契约(共享)
// src/contract.ts
import * as v from "valibot";
import { defineServiceContract, method, type InferService } from "@kunkunsh/sdk/contract";
export const mathContract = defineServiceContract({
id: "com.you.math",
serviceName: "math",
description: "基础数学运算",
methods: {
add: method({
description: "两个数字相加",
input: v.object({ a: v.number(), b: v.number() }),
output: v.object({ result: v.number() }),
}),
},
});
export type MathService = InferService<typeof mathContract>;从专用子路径导出,供消费者导入:
{ "exports": { "./contract": { "types": "./dist/contract.d.ts", "default": "./dist/contract.js" } } }2. 实现(提供者)
implementService 将你的处理程序连接到契约,并自动验证输入/输出:
// src/MathService.ts
import { implementService } from "@kunkunsh/sdk/runtime";
import { mathContract } from "./contract";
export default implementService(mathContract, {
async add({ a, b }) {
return { result: a + b };
},
async onInit() { /* 打开资源 */ },
async onDestroy() { /* 清理 */ },
});使用 service 模式构建:
await Bun.build({
entrypoints: ["./src/MathService.ts"],
outdir: "./dist/node",
target: "node", format: "esm", minify: true,
plugins: [kunkunCommandPlugin({ mode: "service" }) as BunPlugin],
});3. 在清单中声明
services[] 数组是商店和代理可见的内容(methods 镜像你的契约;JSON schema 驱动验证和 AI 工具描述):
{
"kunkun": {
"services": [
{
"name": "math",
"description": "基础数学运算",
"main": "dist/node/MathService.js",
"serviceMode": "node-headless",
"methods": [
{
"name": "add",
"description": "两个数字相加",
"inputSchema": { "type": "object", "properties": { "a": { "type": "number" }, "b": { "type": "number" } }, "required": ["a", "b"] },
"outputSchema": { "type": "object", "properties": { "result": { "type": "number" } } }
}
]
}
]
}
}消费服务
三个层级,从最松散到最安全:
第一层——无类型(无需导入):
import { getHostAPI } from "@kunkunsh/sdk/runtime";
const { result } = await getHostAPI().services.call("com.you.math", "math", "add", { a: 5, b: 3 });第二层——编译时类型(零运行时开销):
import { createServiceClient } from "@kunkunsh/sdk/runtime";
import type { MathService } from "com.you.math/contract";
const math = createServiceClient<{ math: MathService }>("com.you.math");
const { result } = await math.math.add({ a: 5, b: 3 }); // 完全类型化第三层——类型 + 运行时验证(发送前验证输入,接收后验证输出):
import { createValidatedServiceClient } from "@kunkunsh/sdk/runtime";
import { mathSchemas } from "com.you.math/contract";
const math = createValidatedServiceClient("com.you.math", mathSchemas);
const { result } = await math.math.add({ a: 5, b: 3 });声明依赖
使用 serviceDependencies 预批准提供者(这样调用时不会弹出提示):
{
"kunkun": {
"serviceDependencies": [
{ "extensionIdentifier": "com.you.math", "service": "math" }
]
}
}未声明的调用会通过代理提示用户批准。
权限组合
服务调用组合权限,这样调用者不会继承提供者的私有细节:
- 提供者 提供自己的实现权限(例如 FFmpeg 对
ffmpeg二进制的限定shell访问)。 - 调用者 提供用户选择的输入的资源权限(例如要转换文件的
fs-read/fs-write)。
因此 video-processing 可以在用户选择的文件上调用 ffmpeg.convertImage,而无需声明对 ffmpeg 的 shell 访问权限。参见权限 → 交集。
AI 代理桥接
每个服务方法自动注册为一个 AI 工具,名称为:
plugin__<extensionIdentifier>__<serviceName>__<methodName>安装一个服务扩展后,代理就可以调用它——一个描述良好的 OCR 服务意味着"提取此图片中的文字"直接生效。
选项与生命周期
await math.math.add({ a: 1, b: 2 }); // 使用默认 30s 超时
// 每次调用:createServiceClient(id, { timeout: 5000 })- 默认超时 30s,最长 10 分钟。
onInit/onDestroy钩子在服务 worker 启动/停止时运行(onDestroy有约 5s 硬超时)。
实际示例
ffmpeg(提供者)↔video-processing(消费者)——图片/视频转换youtube-tools(提供者)↔youtube-podcast-generator(消费者)——元数据、转录、下载