建立一個 Python particle
一個 particle 是一個單一檔案程式,可向 Particle 執行時期公開一個或多個工具(具備 JSON-Schema 輸入的命名操作)。此技能提供你產出一個可建置並執行的 particle 所需的一切知識。
1. 檔案結構
在專案根目錄下放置一個原始碼檔案,並精確命名為 Particlefile.py。不需要其他檔案。PyPI 相依套件在檔案頂部的 PEP 723 內嵌中繼資料區塊中宣告;建置時會解析這些相依關係。不需要管理 pyproject.toml 或虛擬環境。
# /// script
# requires-python = ">=3.12"
# dependencies = [
# "idna>=3", # 選用——僅在使用第三方套件時需要
# ]
# ///
2. 模組層級的 particle——完整的 API
檔案必須將一個 Particle 實例賦值給一個模組層級的 particle 變數。執行時期在建置時會對此進行內省,並在每次工具呼叫時讀取其處理常式。
from particle.manifest import (
Particle, Tool, Http, Filesystem, Mount, TempMount,
Credential, OAuth2, ApiKey, ApiKeyLocation, SigningKey, Basic, Raw,
)
from particle import http, credentials, kv, oauth, signing
def _do_thing(args):
return {"result": f"hello {args['name']}"}
def _ping():
return {"status": "ok", "message": "alive"}
particle = Particle(
name="kebab-case-name", # 必填,kebab-case
description="...", # 必填,一行
version="0.1.0", # 必填,semver
# 對外 HTTP 允許清單。未在此列出的主機將在網路層級被拒絕。
# 若 particle 不發送任何對外請求,可完全省略 `http=`。
http=Http(allowed_hosts=["api.example.com"]),
# 主機目錄存取,預設為關閉。請參閱下方的「檔案系統」。
# filesystem=Filesystem(mounts={...}, temp={...}),
# 選用。依名稱宣告;使用者在設定時選擇方法。
credentials={
"example": Credential(
hosts=["api.example.com"],
required=True,
methods={
# 請參閱下方的「憑證方法」
},
),
},
# 必填。name → Tool(description, input_schema, handler)。
tools={
"do_thing": Tool(
description="此工具的功能——一行,專為 LLM 撰寫。",
input_schema={
"type": "object",
"properties": {
"name": {"type": "string", "description": "..."},
},
"required": ["name"],
},
handler=_do_thing,
),
},
# 選用。由 `particle ping` 呼叫。
ping=_ping,
)
工具
description:專為 LLM 呼叫者撰寫——簡短、以行動為導向。input_schema:JSON Schema Draft 2020-12,以物件為根。主機在呼叫你的處理常式之前會根據 schema 驗證引數,因此你可以信任處理常式內部的 dict 結構。handler:一個接受一個位置 dict 並傳回任何 JSON 可序列化值的純粹可呼叫物件。可以是def或async def;非同步處理常式在一次性輪詢迴圈中執行。透過拋出例外來表現錯誤。
Ping
選用。傳回一個類似以下的 dict:
{"status": "ok" | "degraded" | "unhealthy",
"message": "...", # 選用
"details": ...} # 選用,JSON 可序列化
3. 可供處理常式使用的執行時期 API
所有主機功能都位於 from particle import ... 之下。這些是唯一需要的非標準庫模組。
對外 HTTP
有四種等效的方式可發送 HTTP 呼叫。選擇與你程式碼其餘部分慣用語法相符的方式;它們都會透過主機允許清單路由。
particle.http.fetch——原生 API。當設定 credential_name= 時會自動套用憑證。
from particle import http
res = http.fetch(
"https://api.example.com/things",
method="POST",
headers={"Content-Type": "application/json"},
body=json.dumps({...}).encode(),
credential_name="example", # 選用;主機會替換秘密
)
if not res.ok:
raise RuntimeError(f"upstream {res.status_code}")
data = res.json()
http.fetch 傳回一個 Response,其中包含:
.status_code(int),.ok(bool),.headers((name, bytes)元組的 list ),.header(name)查詢,.body(bytes),.text(str),.json()(已解析)。
urllib.request.urlopen——直接使用即可。執行時期會對其進行猴子補丁,使其透過 wasi:http 路由。
import urllib.request
with urllib.request.urlopen("https://api.example.com/x") as r:
body = r.read()
urllib3 / requests——直接使用即可。執行時期會在 urllib3 匯入時注入一個自訂連線類別,因此 requests.get(...) 等可以直接運作。它們不會自動套用憑證——對於需要認證的請求,請使用 http.fetch(..., credential_name=...),或自行取得佔位符並設定標頭。
import requests
r = requests.get("https://api.example.com/x") # 運作
httpx——直接使用即可,同步和非同步皆可。匯入時會注入自訂 HTTPTransport / AsyncHTTPTransport。
import httpx
r = httpx.get("https://api.example.com/x")
原始 socket 不可用。 socket.socket() 會拋出 OSError。
檔案系統
預設為關閉——處理常式只能看到自己的套件。需宣告 Filesystem 功能以取得主機目錄存取權,然後使用一般的 open() / pathlib 進行讀寫(執行時期會透過 wasi:filesystem 路由)。
from particle.manifest import Filesystem, Mount, TempMount
filesystem=Filesystem(
# 使用者映射到真實路徑的主機目錄——可使用
# `particle mount <particle> <name> <host-path>` 永久映射,或每次執行時
# 使用 `--mount <name>=<host-path>` 映射。
mounts={
"data": Mount(
description="報告寫入的位置", # 顯示在安裝提示中
path="/mnt/data", # 沙箱內的絕對路徑
access="readwrite", # "readonly" | "readwrite"
required=True, # 拒絕執行直到映射完成(預設為 False)
),
},
# 主機每次執行時全新配置並在結束時清除的暫存空間。
# 始終為讀寫;使用者無需映射這些。
temp={
"work": TempMount(description="暫存空間", path="/tmp/work", max_size="10MB"),
},
),
with open("/mnt/data/in.json") as f:
data = f.read()
with open("/mnt/data/out.json", "w") as f:
f.write(result)
- 處理常式直接開啟已宣告的
path;掛載名稱永遠不會出現在沙箱內部。 - 寫入
readonly掛載,或讀寫任何已宣告掛載之外的路徑,會拋出OSError。 - 使用者從未映射的
required掛載會在執行時期(而非建置/安裝時)失敗。 max_size是一個位元組計數,可選擇性加上KB/MB/GB後綴(如"10MB");超過此大小的寫入會失敗。
憑證
from particle import credentials
# 使用者在設定時選擇的方法("oauth"、"pat" 等)或 None。
method = credentials.get_configured_method("example")
# "raw" 類型憑證的原始值。
value = credentials.get_raw("some-raw-cred")
# 低階:取得一個不透明佔位符 + 描述主機預期在哪裡替換它的 apply-spec。
# 幾乎總是使用 `http.fetch(..., credential_name=...)` 來代替,它會在內部執行此操作。
info = credentials.get_placeholder("example")
鍵值儲存
以每個 particle 為範圍。僅支援字串——如有需要,可對二進位資料進行 base64 編碼。需在 Particle 上使用 kv=KV(enabled=True) 宣告——若無宣告,每次呼叫都會拋出 not-declared 錯誤(無需使用者核准,只需宣告即可):
from particle.manifest import Particle, KV
# particle = Particle(..., kv=KV(enabled=True))
from particle import kv
kv.set("cursor", "abc123")
v = kv.get("cursor") # str | None
kv.delete("cursor")
keys = kv.list("prefix:") # list[str] — 注意:會遮蔽內建的 list
將模組匯入為 kv(而非 from particle.kv import list),這樣 list 就不會遮蔽內建的 list。
OAuth 重新整理
僅當你有 OAuth2 憑證時使用。使用 credential_name= 呼叫 http.fetch 會自動重新整理已過期的令牌;僅在上游拒絕了你認為仍然有效的令牌時才呼叫此方法。
from particle import oauth
oauth.refresh("example")
簽署
僅適用於 SigningKey 憑證。金鑰材料絕不會進入 Python。
from particle import signing
sig = signing.sign("webhook-key", payload_bytes)
ok = signing.verify("webhook-key", data_bytes, signature_bytes)
4. 憑證方法
每個 Credential 都有一個 methods={...} 映射。使用者在設定時選擇一個方法。資料類別類型來自 particle.manifest。
OAuth2
"oauth": OAuth2(
description="透過 OAuth 登入",
flows=["authorization-code", "device-code"],
scopes=["read", "write"],
authorization_url="https://provider/oauth/authorize",
token_url="https://provider/oauth/token",
device_auth_url="https://provider/oauth/device/code", # 適用於 "device-code"
),
有效的流程字串:"authorization-code"、"authorization-code-pkce"、"device-code"。
ApiKey
"pat": ApiKey(
description="使用個人存取令牌",
location=ApiKeyLocation(kind="auth-scheme", scheme="Bearer"),
),
ApiKeyLocation.kind 可以是:
"header"+name:<name>: <key>"auth-scheme"+scheme:Authorization: <scheme> <key>(例如Bearer)"query-param"+name:附加為?<name>=<key>
完全省略 location 以讓匯入者在設定時提示使用者。
Basic
"basic_auth": Basic(description="使用者名稱 + 密碼"),
替換為 Authorization: Basic <base64>。
SigningKey
"webhook": SigningKey(description="...", algorithm="hmac-sha256"),
有效的演算法:"hmac-sha256"、"hmac-sha512"。透過 signing.sign 使用。
Raw
"api_key_blob": Raw(description="..."),
透過 credentials.get_raw(name) 使用。僅在確實需要原始位元組時選擇此項——其他類型更安全,因為該值永遠不會進入 Python 記憶體。
5. PyPI 相依套件
在檔案頂部的 PEP 723 區塊中宣告:
# /// script
# requires-python = ">=3.12"
# dependencies = [
# "httpx>=0.27",
# "pyjwt>=2",
# ]
# ///
限制條件:
- 僅限純 Python wheel。 任何需要 C 擴充功能的套件(如 numpy、pydantic v1、lxml 等)都無法載入。
- 標準庫大部分可用——
json、re、hashlib、hmac、base64、secrets、datetime、urllib.parse、asyncio、email等都可運作。 ssl、原始socket、subprocess——不可用。open()/pathlib僅能操作已宣告檔案系統掛載內的路徑(請參閱上方「檔案系統」)以及 particle 自身的/particle/套件;任意主機路徑都會被拒絕。
6. 重要規則
-
不要在模組作用域呼叫主機功能。 在檔案頂層呼叫
http.fetch、kv.get、credentials.get_placeholder等會在建置時失敗(內省階段會在陷阱儲存下執行你的模組)。所有主機呼叫都必須在handler或ping函式內部進行。# ✗ 會導致建置失敗 _cached_data = http.fetch("https://api.example.com/init") particle = Particle(...) # ✓ 在處理常式內部 def _go(args): res = http.fetch("https://api.example.com/...") ... particle = Particle(tools={"go": Tool(..., handler=_go)}) -
宣告你將擷取的每一個主機。 對缺少於
http=Http(allowed_hosts=[...])清單中的主機發送請求會被拒絕。錯誤會以處理常式中的例外形式呈現。 -
Credential.hosts必須是 HTTP 允許清單的子集。 建置時會拒絕綁定到 particle 無法連線之主機的憑證。 -
name必須是 kebab-case(小寫、連字號)。註冊表的鍵是(name, version)。 -
version必須是有效的 semver。 例如0.1.0、1.2.3-rc.1等。 -
工具處理常式必須傳回 JSON 可序列化的值。 純
dict、list、str、int、float、bool、None。dataclass 或datetime將無法序列化——請在傳回前進行轉換。
7. 工作流程
在寫好 Particlefile.py 之後:
particle build # 解析、解析相依性、內省、註冊
particle ping <name> # 驗證 ping(如果有定義)
particle run <name> # 列出工具
particle run <name> <tool> --help # 顯示工具的旗標
particle run <name> <tool> --foo=bar # 執行它
particle build 會在首次執行時以互動方式引導使用者完成憑證設定。如果建置失敗,錯誤訊息會指出是哪個階段(import-scan、resolve-and-fetch、manifest-extract)以及具體問題。
如果 particle 宣告了檔案系統掛載,請在執行前對其進行映射(或在 particle run 中傳遞 --mount name=path 進行一次性執行):
particle mount <name> <mount-name> <host-path> # 儲存持久映射
particle mount <name> # 列出掛載 + 當前映射
若要將 particle 公開為獨立可執行檔(對於 Claude Code 技能和 shell 使用很方便),請執行 particle link <name> ./<name> 來建立一個啟動器,它會將其引數轉發給 particle run <name>。
8. 完整範例
一個公開 GitHub 倉庫查詢工具的最小 particle:
# /// script
# requires-python = ">=3.12"
# dependencies = []
# ///
import json
from particle import http
from particle.manifest import Particle, Tool, Http, Credential, ApiKey, ApiKeyLocation
def _get_repo(args):
res = http.fetch(
f"https://api.github.com/repos/{args['owner']}/{args['repo']}",
headers={"Accept": "application/vnd.github+json"},
credential_name="github",
)
if not res.ok:
raise RuntimeError(f"GitHub {res.status_code}: {res.text}")
r = res.json()
return {
"full_name": r.get("full_name"),
"stars": r.get("stargazers_count"),
"language": r.get("language"),
"description": r.get("description"),
}
particle = Particle(
name="github-repo-lookup",
description="查詢 GitHub 倉庫的詳細資訊",
version="0.1.0",
http=Http(allowed_hosts=["api.github.com"]),
credentials={
"github": Credential(
hosts=["api.github.com"],
required=True,
methods={
"pat": ApiKey(
description="GitHub 個人存取令牌",
location=ApiKeyLocation(kind="header", name="Authorization"),
),
},
),
},
tools={
"get_repo": Tool(
description="取得 GitHub 倉庫的資訊,包括星數、語言和描述。",
input_schema={
"type": "object",
"properties": {
"owner": {"type": "string", "description": "倉庫擁有者(使用者或組織)"},
"repo": {"type": "string", "description": "倉庫名稱"},
},
"required": ["owner", "repo"],
},
handler=_get_repo,
),
},
)