Schreiben eines TypeScript/JavaScript-Partikels
Ein Partikel ist ein einzeiliges Programm, das ein oder mehrere Tools (benannte Operationen mit einer JSON-Schema-Eingabe) für eine Partikel-Laufzeitumgebung bereitstellt. Diese Fähigkeit enthält alles, was Sie benötigen, um ein Partikel zu erstellen, das gebaut und ausgeführt wird.
1. Dateiaufbau
Eine Quelldatei im Projektstammverzeichnis, die genau wie folgt benannt ist:
Particlefile.ts— bevorzugt (vollständige Typprüfung),Particlefile.js— funktioniert, aber ohne Typen.
Es ist nichts weiter erforderlich. npm-Abhängigkeiten werden inline mit standardmäßigen import-Anweisungen deklariert; die Build-Umgebung löst sie auf. Keine package.json, kein tsconfig.json, kein node_modules zu verwalten.
2. Der Standardexport — die gesamte API
Der Standardexport der Datei ist die Partikeldeklaration. Form:
export default {
name: "kebab-case-name", // erforderlich, kebab-case
description: "...", // erforderlich, eine Zeile
version: "0.1.0", // erforderlich, semver
capabilities: {
// Ausgehende HTTP-Whitelist. Hier nicht aufgeführte Hosts werden
// an der Netzwerkgrenze abgelehnt. Lassen Sie `http` vollständig weg, wenn das Partikel
// keine ausgehenden Anfragen stellt.
http: { allowedHosts: ["api.example.com"] },
// Host-Verzeichniszugriff, standardmäßig deaktiviert. Siehe "Dateisystem" weiter unten.
// filesystem: { mounts: { ... }, temp: { ... } },
},
// Optional. Wird pro Name deklariert; der Benutzer wählt bei der Einrichtung eine Methode aus.
credentials: {
example: {
hosts: ["api.example.com"], // Substitution an diese binden
required: true,
methods: {
// siehe "Anmeldeinformationsmethoden" weiter unten
},
},
},
// Erforderlich. Zuordnung von Tool-Name → { description, inputSchema, handler }.
tools: {
do_thing: {
description: "Was dieses Tool tut — eine Zeile, geschrieben für ein LLM.",
inputSchema: {
type: "object",
properties: {
name: { type: "string", description: "..." },
},
required: ["name"],
},
handler: async ({ name }: { name: string }) => {
return { result: `hello ${name}` };
},
},
},
// Optional. Gibt Lebendigkeit/Bereitschaft zurück. Wird von `particle ping` aufgerufen.
ping: async () => ({ status: "ok" as const, message: "alive" }),
};
Tools
description: geschrieben für einen LLM-Aufrufer — kurz, handlungsorientiert.inputSchema: JSON Schema Draft 2020-12, objektbasiert. Der Host validiert Argumente dagegen, bevor Ihr Handler aufgerufen wird, sodass Sie innerhalb des Handlers der Eingabeform vertrauen können. Verwenden Sierequired, um obligatorische Felder zu kennzeichnen; geben Siedefaultfür optionale an.handler: synchron oder asynchron. Geben Sie einen beliebigen JSON-serialisierbaren Wert zurück. Werfen Sie einen Fehler, um ihn dem Aufrufer anzuzeigen — die geworfene Nachricht wird angezeigt.
Ping
Optional, aber empfohlen. Rückgabeform:
{ status: "ok" | "degraded" | "unhealthy",
message?: string,
details?: string }
3. Laufzeit-APIs, die Handlern zur Verfügung stehen
Dies sind die einzigen Nicht-Stdlib-Dinge, die Sie importieren können. Jedes ist ein Host-Modul, das von der Laufzeitumgebung bereitgestellt wird — sie werden als reine Typ-npm-Pakete ausgeliefert, die Implementierung wird zur Ausführungszeit injiziert.
Ausgehendes HTTP
Verwenden Sie das globale fetch(). Es ist die standardmäßige Web-Fetch-API, die über den Host geleitet wird, sodass die Whitelist und die Anmeldeinformationssubstitution angewendet werden.
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();
Hosts, die nicht in capabilities.http.allowedHosts enthalten sind, werden abgelehnt. Ein einfaches fetch wendet keine Anmeldeinformationen an — verwenden Sie dafür credentials.fetcher.
Dateisystem
Standardmäßig deaktiviert — ein Handler sieht nur sein eigenes Bundle. Deklarieren Sie capabilities.filesystem, um Host-Verzeichniszugriff zu erhalten, und lesen und schreiben Sie dann mit den üblichen node:fs-APIs (die Laufzeitumgebung leitet sie durch wasi:filesystem). Kein Fähigkeitsmodul zum Importieren.
capabilities: {
filesystem: {
// Host-Verzeichnisse, die der *Benutzer* auf reale Pfade abbildet — dauerhaft
// mit `particle mount <particle> <name> <host-path>`, oder pro Ausführung
// mit `--mount <name>=<host-path>`.
mounts: {
data: {
description: "Wohin Berichte geschrieben werden", // im Installationsprompt angezeigt
path: "/mnt/data", // absoluter Pfad innerhalb der Sandbox
access: "readwrite", // "readonly" | "readwrite"
required: true, // Ausführung ablehnen, bis zugeordnet (Standard false)
},
},
// Scratch-Speicher, den der Host bei jeder Ausführung frisch bereitstellt und beim
// Beenden löscht. Immer lesend-schreibend; der Benutzer ordnet diese nie zu.
temp: {
work: { description: "Scratch-Speicher", 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));
- Der Handler öffnet den deklarierten
pathdirekt; der Mount-Name erscheint nie innerhalb der Sandbox. - Das Schreiben in einen
readonly-Mount oder das Lesen/Schreiben eines beliebigen Pfades außerhalb eines deklarierten Mounts führt zu einem Fehler. - Ein
required-Mount, den der Benutzer nie zuordnet, schlägt zur Laufzeit fehl, nicht beim Bau/Installation. maxSizeist eine Byteanzahl mit einem optionalenKB/MB/GB-Suffix ("10MB"); Schreibvorgänge, die darüber hinausgehen, schlagen fehl.
Anmeldeinformationen
import { credentials } from "@partite-ai/particle-credentials";
// fetcher(name) gibt eine fetch-artige Funktion zurück, die automatisch die
// Anmeldeinformationen an der host-konfigurierten Stelle (Header, Query-
// Parameter usw.) anwendet, bevor die Anfrage die Sandbox verlässt.
const fetcher = await credentials.fetcher("example");
const res = await fetcher("https://api.example.com/me");
// Ermitteln, welche Methode der Benutzer bei der Einrichtung konfiguriert hat ("oauth", "pat", ...).
const method = credentials.getConfiguredMethod("example"); // string | null
// Nur für Anmeldeinformationen, die mit type: "raw" deklariert wurden.
const value = await credentials.getRaw("some-raw-cred");
credentials.fetcher funktioniert für basic, oauth2- und apikey-Anmeldeinformationen. Für signing-key verwenden Sie signing.sign. Für raw verwenden Sie getRaw.
Schlüssel/Wert-Speicher
Pro Partikel begrenzt. Nur Zeichenfolgen — base64-kodieren Sie Binärdaten bei Bedarf. Deklarieren Sie es unter capabilities.kv — ohne die Deklaration schlägt jeder Aufruf mit einem not-declared-Fehler fehl (keine Benutzerfreigabe erforderlich, nur die Deklaration):
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-Aktualisierung
Nur, wenn Sie eine oauth2-Anmeldeinformation haben. Der Fetcher aktualisiert abgelaufene Token automatisch; rufen Sie dies nur auf, wenn ein vorgeschalteter Dienst ein Token ablehnt, von dem Sie dachten, es sei noch gültig.
import { oauth } from "@partite-ai/particle-oauth";
await oauth.refresh("example");
Signierung
Nur für signing-key-Anmeldeinformationen. Das Schlüsselmaterial gelangt nie in 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. Anmeldeinformationsmethoden
Jede Anmeldeinformation hat eine methods-Zuordnung. Der Benutzer wählt bei der Einrichtung eine aus. Ein Partikel bietet häufig mehrere Methoden für denselben Anbieter an.
oauth2
oauth: {
type: "oauth2",
description: "Über OAuth anmelden",
flows: ["authorization-code", "device-code"], // eine dieser drei auswählen
scopes: ["read", "write"],
authorizationUrl: "https://provider/oauth/authorize",
tokenUrl: "https://provider/oauth/token",
deviceAuthUrl: "https://provider/oauth/device/code", // erforderlich für "device-code"
},
Gültige Flow-Strings: "authorization-code", "authorization-code-pkce", "device-code".
apikey
pat: {
type: "apikey",
description: "Ein persönliches Zugriffstoken verwenden",
location: { kind: "auth-scheme", scheme: "Bearer" },
},
location.kind ist einer von:
"header"+name:<name>: <key>"auth-scheme"+scheme:Authorization: <scheme> <key>(z. B.Bearer)"query-param"+name: angehängt als?<name>=<key>
Lassen Sie location vollständig weg, um den Importeur aufzufordern, den Benutzer danach zu fragen.
basic
basic_auth: { type: "basic", description: "Benutzername + Passwort" },
Ersetzt als Authorization: Basic <base64>.
signing-key
webhook: { type: "signing-key", description: "...", algorithm: "hmac-sha256" },
Gültig: "hmac-sha256", "hmac-sha512". Verwendung über signing.sign.
raw
api_key_blob: { type: "raw", description: "..." },
Verwendung über credentials.getRaw(name). Wählen Sie dies nur, wenn Sie wirklich die Rohdatenbytes benötigen — die anderen Typen sind sicherer, da der Wert nie in Ihren Code gelangt.
5. npm-Abhängigkeiten
Importieren Sie jedes reine ESM-npm-Paket direkt:
import { z } from "zod";
import * as YAML from "yaml";
Die Build-Umgebung führt einen Import-Scan durch und löst sie automatisch auf — keine package.json erforderlich. Einschränkungen:
- Das Paket muss reines JS/TS sein (keine nativen Bindungen, keine Node-Only-APIs).
- Browser-kompatible Builds funktionieren am besten.
- Vermeiden Sie
fs,child_processusw. — sie existieren in der Laufzeitumgebung nicht.
6. Kritische Regeln
-
Keine Host-Aufrufe auf Modulebene. Das Aufrufen von
fetch,kv.get,credentials.fetcherusw. auf der obersten Ebene Ihrer Datei schlägt zur Build-Zeit fehl (die Introspect-Phase führt Ihr Modul unter Trap-Stores aus). Führen Sie alle Host-Aufrufe innerhalb eineshandleroderpingdurch.// ✗ Wird den Build fehlschlagen lassen const fetcher = await credentials.fetcher("github"); export default { tools: { ... } }; // ✓ Innerhalb des Handlers export default { tools: { go: { handler: async (args) => { const fetcher = await credentials.fetcher("github"); // ... }}, }, }; -
Deklarieren Sie jeden Host, den Sie abrufen. Eine Anfrage an einen Host, der nicht in
capabilities.http.allowedHostsenthalten ist, wird abgelehnt. Der Fehler wird als geworfene Ausnahme im Handler sichtbar. -
credentials.<name>.hostsmuss eine Teilmenge der HTTP-Whitelist sein. Der Build lehnt Anmeldeinformationen ab, die an Hosts gebunden sind, die das Partikel nicht erreichen kann. -
namemuss kebab-case sein (Kleinschreibung, Bindestriche). Der Registrierungsschlüssel ist(name, version). -
versionmuss gültiges semver sein.0.1.0,1.2.3-rc.1usw. -
Tool-Handler müssen JSON-serialisierbare Werte zurückgeben. Die Rückgabe von
undefinedist in Ordnung (wird zunull). Die Rückgabe einer Klasseninstanz, einesDate,BigIntoderMapschlägt bei der Serialisierung fehl.
7. Workflow
Nach dem Schreiben von Particlefile.ts:
particle build # Typprüfung, Bündelung, Introspection, Registrierung
particle ping <name> # Ping überprüfen (falls definiert)
particle run <name> # Tools auflisten
particle run <name> <tool> --help # Tool-Flags anzeigen
particle run <name> <tool> --foo=bar # Ausführen
particle build führt den Benutzer beim ersten Mal interaktiv durch die Einrichtung der Anmeldeinformationen. Wenn der Build fehlschlägt, gibt der Fehler die Phase (import-scan, typecheck, bundle, manifest-extract) und das spezifische Problem an.
Wenn das Partikel Dateisystem-Mounts deklariert, ordnen Sie sie vor der Ausführung zu (oder übergeben Sie --mount name=path an particle run für einen einmaligen Durchlauf):
particle mount <name> <mount-name> <host-path> # dauerhafte Zuordnung speichern
particle mount <name> # Mounts + aktuelle Zuordnungen auflisten
Um das Partikel als eigenständige ausführbare Datei bereitzustellen (praktisch für Claude Code-Fähigkeiten und Shell-Nutzung), schreibt particle link <name> ./<name> einen Launcher, der seine Argumente an particle run <name> weiterleitet.
8. Ausgearbeitetes Beispiel
Ein minimales Partikel, das ein GitHub-Repo-Lookup-Tool bereitstellt:
import { credentials } from "@partite-ai/particle-credentials";
export default {
name: "github-lookup",
description: "GitHub-Repository-Metadaten nachschlagen.",
version: "0.1.0",
capabilities: { http: { allowedHosts: ["api.github.com"] } },
credentials: {
github: {
hosts: ["api.github.com"],
required: true,
methods: {
pat: {
type: "apikey",
description: "GitHub persönliches Zugriffstoken",
location: { kind: "auth-scheme", scheme: "Bearer" },
},
},
},
},
tools: {
get_repo: {
description: "Metadaten für ein GitHub-Repository abrufen.",
inputSchema: {
type: "object",
properties: {
owner: { type: "string", description: "Besitzer-Login." },
repo: { type: "string", description: "Repository-Name." },
},
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. Kurzreferenz
| Bedarf | Import / Aufruf |
|---|---|
| HTTP-Anfrage | await fetch(url, init) |
| Authentifiziertes HTTP | const f = await credentials.fetcher(name); await f(url, init) |
| Lesen/Schreiben eines gemounteten Verzeichnisses | node:fs/promises auf dem deklarierten path (benötigt capabilities.filesystem) |
| Pro-Partikel-Zustand | kv.get / set / delete / list |
| OAuth-Aktualisierung erzwingen | oauth.refresh(name) |
| HMAC-Signierung/-Verifizierung | signing.sign(name, data) / signing.verify(name, data, sig) |
| Bei der Einrichtung ausgewählte Methode ermitteln | credentials.getConfiguredMethod(name) |
| Rohe Anmeldeinformationsbytes | credentials.getRaw(name) |
Imports kommen immer aus den @partite-ai/particle-<area>-Paketen — erfinden Sie keine anderen Namen.
Quelle und Lizenz
Adaptiert von ...
[truncated for translation]