编写 TypeScript/JavaScript 粒子
粒子是一个单文件程序,向粒子运行时暴露一个或多个工具(具有 JSON Schema 输入的命名操作)。此技能告诉您生成一个可构建和运行的程序所需的一切。
1. 文件布局
项目根目录下的一个源文件,名称必须为:
Particlefile.ts— 首选(完整类型检查),Particlefile.js— 也可工作,但没有类型。
不需要其他任何内容。npm 依赖项通过标准的 import 语句内联声明;构建会解析它们。无需管理 package.json、tsconfig.json 或 node_modules。
2. 默认导出 — 整个 API
文件的默认导出是粒子声明。形状如下:
export default {
name: "kebab-case-name", // 必需,kebab 命名法
description: "...", // 必需,一行
version: "0.1.0", // 必需,语义化版本
capabilities: {
// 出站 HTTP 允许列表。未在此列出的主机在网络边界被拒绝。
// 如果粒子没有出站请求,则完全省略 `http`。
http: { allowedHosts: ["api.example.com"] },
// 主机目录访问,默认关闭。请参见下面的“文件系统”。
// filesystem: { mounts: { ... }, temp: { ... } },
},
// 可选。按名称声明;用户在设置时选择方法。
credentials: {
example: {
hosts: ["api.example.com"], // 将替换绑定到这些主机
required: true,
methods: {
// 请参见下面的“凭据方法”
},
},
},
// 必需。工具名称 → { description, inputSchema, handler } 的映射。
tools: {
do_thing: {
description: "此工具的作用 — 一行,为 LLM 编写。",
inputSchema: {
type: "object",
properties: {
name: { type: "string", description: "..." },
},
required: ["name"],
},
handler: async ({ name }: { name: string }) => {
return { result: `hello ${name}` };
},
},
},
// 可选。返回存活/就绪状态。由 `particle ping` 调用。
ping: async () => ({ status: "ok" as const, message: "alive" }),
};
工具
description:为 LLM 调用者编写 — 简短、面向操作。inputSchema:JSON Schema Draft 2020-12,对象根。主机在调用你的处理器之前会验证参数,因此你可以在处理器内部信任输入形状。使用required标记必填字段;为可选字段提供default。handler:同步或异步。返回任何 JSON 可序列化的值。抛出错误以向调用者展示错误 — 他们看到的是抛出的消息。
Ping
可选但推荐。返回形状:
{ status: "ok" | "degraded" | "unhealthy",
message?: string,
details?: string }
3. 处理器的运行时 API
这些是你可以导入的唯一非标准库内容。每个都是运行时提供的主机模块 — 它们作为仅类型 npm 包提供,实现会在执行时注入。
出站 HTTP
使用全局的 fetch()。它是标准的 Web Fetch API,通过主机路由,以便应用允许列表和凭据替换。
const res = await fetch("https://api.example.com/things", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ... }),
});
if (!res.ok) throw new Error(`upstream ${res.status}`);
const data = await res.json();
不在 capabilities.http.allowedHosts 中的主机会被拒绝。普通的 fetch 不会应用凭据 — 请使用 credentials.fetcher 来实现。
文件系统
默认关闭 — 处理器只能看到自己的包。声明 capabilities.filesystem 以获取主机目录访问权限,然后使用普通的 node:fs API 进行读写(运行时通过 wasi:filesystem 路由它们)。无需导入能力模块。
capabilities: {
filesystem: {
// *用户*映射到真实路径的主机目录 — 使用 `particle mount <particle> <name> <host-path>` 持久化,
// 或每次运行时使用 `--mount <name>=<host-path>`。
mounts: {
data: {
description: "报告写入位置", // 在安装提示中显示
path: "/mnt/data", // 沙箱内的绝对路径
access: "readwrite", // "readonly" | "readwrite"
required: true, // 拒绝运行直至映射(默认 false)
},
},
// 主机每次运行时重新提供并在退出时清理的临时空间。始终可读写;用户从不映射这些。
temp: {
work: { description: "临时空间", path: "/tmp/work", maxSize: "10MB" },
},
},
},
import { readFile, writeFile } from "node:fs/promises";
const input = await readFile("/mnt/data/in.json", "utf8");
await writeFile("/mnt/data/out.json", JSON.stringify(result));
- 处理器直接打开声明的
path;挂载的名称永远不会出现在沙箱内。 - 写入
readonly挂载,或读取/写入任何已声明挂载之外的路径,都会抛出错误。 - 用户从未映射的
required挂载会在运行时失败,而不是构建/安装时。 maxSize是一个字节计数,带有可选的KB/MB/GB后缀("10MB");超过它的写入会失败。
凭据
import { credentials } from "@partite-ai/particle-credentials";
// fetcher(name) 返回一个 fetch 形状的函数,该函数在请求离开沙箱之前自动应用
// 主机配置位置的凭据(标头、查询参数等)。
const fetcher = await credentials.fetcher("example");
const res = await fetcher("https://api.example.com/me");
// 发现用户在设置时配置的方法("oauth", "pat", ...)。
const method = credentials.getConfiguredMethod("example"); // string | null
// 仅适用于声明了 type: "raw" 的凭据。
const value = await credentials.getRaw("some-raw-cred");
credentials.fetcher 适用于 basic、oauth2 和 apikey 凭据。对于 signing-key,使用 signing.sign。对于 raw,使用 getRaw。
键/值存储
按粒子作用域。仅字符串 — 如果需要二进制数据,请进行 base64 编码。在 capabilities.kv 下声明 — 如果没有声明,每次调用都会抛出 not-declared 错误(无需用户批准,只需声明):
capabilities: { kv: { enabled: true } },
import { kv } from "@partite-ai/particle-kv";
await kv.set("cursor", "abc123");
const v = await kv.get("cursor"); // string | null
await kv.delete("cursor");
const keys = await kv.list("prefix:"); // string[]
OAuth 刷新
仅当你有 oauth2 凭据时。fetcher 会自动刷新过期的令牌;仅当上游拒绝了你认为仍然有效的令牌时,才调用此函数。
import { oauth } from "@partite-ai/particle-oauth";
await oauth.refresh("example");
签名
仅用于 signing-key 凭据。密钥材料永远不会进入 JS。
import { signing } from "@partite-ai/particle-signing";
const sig = await signing.sign("webhook-key", new TextEncoder().encode(payload));
const ok = await signing.verify("webhook-key", data, sig);
4. 凭据方法
每个凭据都有一个 methods 映射。用户在设置时选择一个方法。一个粒子通常为同一个提供者提供多种方法。
oauth2
oauth: {
type: "oauth2",
description: "通过 OAuth 登录",
flows: ["authorization-code", "device-code"], // 选择这三个中的任意一个
scopes: ["read", "write"],
authorizationUrl: "https://provider/oauth/authorize",
tokenUrl: "https://provider/oauth/token",
deviceAuthUrl: "https://provider/oauth/device/code", // "device-code" 流必需
},
有效的流程字符串:"authorization-code"、"authorization-code-pkce"、"device-code"。
apikey
pat: {
type: "apikey",
description: "使用个人访问令牌",
location: { kind: "auth-scheme", scheme: "Bearer" },
},
location.kind 是以下之一:
"header"+name:<name>: <key>"auth-scheme"+scheme:Authorization: <scheme> <key>(例如Bearer)"query-param"+name:附加为?<name>=<key>
完全省略 location 以让导入者提示用户。
basic
basic_auth: { type: "basic", description: "用户名 + 密码" },
替换为 Authorization: Basic <base64>。
signing-key
webhook: { type: "signing-key", description: "...", algorithm: "hmac-sha256" },
有效值:"hmac-sha256"、"hmac-sha512"。通过 signing.sign 使用。
raw
api_key_blob: { type: "raw", description: "..." },
通过 credentials.getRaw(name) 使用。仅当你确实需要原始字节时才选择此选项 — 其他类型更安全,因为值永远不会进入你的代码。
5. npm 依赖项
直接导入任何纯 ESM npm 包:
import { z } from "zod";
import * as YAML from "yaml";
构建会执行导入扫描并自动解析它们 — 无需 package.json。限制:
- 包必须是纯 JS/TS(没有原生绑定,没有仅 Node 的 API)。
- 兼容浏览器的构建效果最好。
- 避免使用
fs、child_process等 — 它们在运行时中不存在。
6. 关键规则
-
禁止模块级的主机调用。 在文件顶层调用
fetch、kv.get、credentials.fetcher等会在构建时失败(内省阶段会在陷阱存储下运行你的模块)。所有主机调用都应在handler或ping内部进行。// ✗ 会导致构建失败 const fetcher = await credentials.fetcher("github"); export default { tools: { ... } }; // ✓ 在处理器内部 export default { tools: { go: { handler: async (args) => { const fetcher = await credentials.fetcher("github"); // ... }}, }, }; -
声明你请求的每个主机。 请求一个不在
capabilities.http.allowedHosts中的主机将被拒绝。错误会以处理器中抛出的异常形式呈现。 -
credentials.<name>.hosts必须是 HTTP 允许列表的子集。 构建会拒绝绑定到粒子无法访问的主机的凭据。 -
name必须使用 kebab 命名法(小写字母、连字符)。注册表键是(name, version)。 -
version必须是有效的语义化版本。 例如0.1.0、1.2.3-rc.1。 -
工具处理器必须返回 JSON 可序列化的值。 返回
undefined是可以的(会变成null)。返回类实例、Date、BigInt或Map会导致序列化失败。
7. 工作流程
编写 Particlefile.ts 之后:
particle build # 类型检查、打包、内省、注册
particle ping <name> # 验证 ping(如果定义了)
particle run <name> # 列出工具
particle run <name> <tool> --help # 显示工具的选项
particle run <name> <tool> --foo=bar # 调用它
particle build 在首次运行时以交互方式引导用户完成凭据设置。如果构建失败,错误会指明阶段(import-scan、typecheck、bundle、manifest-extract)和具体问题。
如果粒子声明了文件系统挂载,请在运行前映射它们(或者在 particle run 上传递 --mount name=path 以进行一次性运行):
particle mount <name> <mount-name> <host-path> # 保存持久化映射
particle mount <name> # 列出挂载和当前映射
要将粒子暴露为独立的可执行文件(方便用于 Claude Code 技能和 shell 使用),particle link <name> ./<name> 会写入一个启动器,将其参数转发给 particle run <name>。
8. 工作示例
一个暴露 GitHub 仓库查找工具的最小粒子:
import { credentials } from "@partite-ai/particle-credentials";
export default {
name: "github-lookup",
description: "查找 GitHub 仓库元数据。",
version: "0.1.0",
capabilities: { http: { allowedHosts: ["api.github.com"] } },
credentials: {
github: {
hosts: ["api.github.com"],
required: true,
methods: {
pat: {
type: "apikey",
description: "GitHub 个人访问令牌",
location: { kind: "auth-scheme", scheme: "Bearer" },
},
},
},
},
tools: {
get_repo: {
description: "获取 GitHub 仓库的元数据。",
inputSchema: {
type: "object",
properties: {
owner: { type: "string", description: "所有者登录名。" },
repo: { type: "string", description: "仓库名称。" },
},
required: ["owner", "repo"],
},
handler: async ({ owner, repo }: { owner: string; repo: string }) => {
const fetcher = await credentials.fetcher("github");
const res = await fetcher(`https://api.github.com/repos/${owner}/${repo}`, {
headers: { Accept: "application/vnd.github+json" },
});
if (!res.ok) throw new Error(`GitHub ${res.status}: ${await res.text()}`);
const r = (await res.json()) as Record<string, unknown>;
return {
full_name: r.full_name,
stars: r.stargazers_count,
url: r.html_url,
};
},
},
},
};
9. 快速参考
| 需求 | 导入 / 调用 |
|---|---|
| HTTP 请求 | await fetch(url, init) |
| 身份验证的 HTTP | const f = await credentials.fetcher(name); await f(url, init) |
| 读取/写入挂载目录 | node:fs/promises 作用于声明的 path(需要 capabilities.filesystem) |
| 按粒子状态 | kv.get / set / delete / list |
| 强制 OAuth 刷新 | oauth.refresh(name) |
| HMAC 签名/验证 | signing.sign(name, data) / signing.verify(name, data, sig) |
| 发现设置时选择的方法 | credentials.getConfiguredMethod(name) |
| 原始凭据字节 | credentials.getRaw(name) |
导入始终来自 @partite-ai/particle-<area> 包 — 不要发明其他名称。
来源和许可
改编自…
[由于长度限制而截断]