8e2c32e466
Problem: canonical_id is readable but mutable; if a component is renamed,
all workflows referencing it by canonical_id break.
Solution: dual-id system
- component_hash_id: cmp_{sha256(canonical_id).slice(0,8)}, derived deterministically,
never changes, safe for workflow references
- canonical_id: human-readable name, used for search and display
- idx:{canonical_id} KV key: reverse-lookup index for resolving canonical_id → hash_id
Changes:
- types.ts: SandboxResult.component_id → component_hash_id + canonical_id,
added 'data' to category enum
- submitComponent.ts: deriveHashId(), writes idx: reverse-lookup on submit
- queryComponents.ts: full rewrite — removed KBDB dependency, uses SUBMISSIONS_KV;
supports both cmp_* and canonical_id as query id; Phase 0 keyword search
with note to upgrade to Vectorize in Phase 2
- sandboxAcceptance.ts: updated field names, fixed TextDecoder TS type
- ensureTemplate.ts: removed KBDB dependency, now a KV health check
- tests: updated component_id → canonical_id
- CONTRIBUTING.md: explain hash_id derivation and dual-id workflow reference syntax
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
436 lines
13 KiB
Markdown
436 lines
13 KiB
Markdown
# Contributing to arcrun
|
||
|
||
感謝你考慮貢獻 arcrun!本文件說明如何新增零件(WASM component)並提交至公眾零件庫。
|
||
|
||
arcrun 的零件**主要由 AI 撰寫**。你不需要是 TinyGo 或 AssemblyScript 專家,只需要把這份文件和你的 API 文件或需求貼給 AI,讓它生成源碼,你負責編譯、測試、提交。
|
||
|
||
---
|
||
|
||
## 選擇開發語言
|
||
|
||
零件只需要輸出符合 **WASI preview1** 的 `.wasm` 檔案,與使用哪個語言無關。
|
||
|
||
| 語言 | 輸出大小 | AI 撰寫品質 | 說明 |
|
||
|------|---------|------------|------|
|
||
| **TinyGo** | 極小(10–80KB) | 優秀 | 官方零件使用;語法簡單,AI 出錯率低 |
|
||
| **AssemblyScript** | 小(20–150KB) | 良好 | TypeScript 語法,前端開發者最快上手 |
|
||
| **Rust** | 小–中(30–300KB) | 良好 | 效能最強;適合複雜演算法,工具鏈稍複雜 |
|
||
|
||
**AI 開發建議:**
|
||
- 選 **TinyGo**:Go 語法與 TypeScript 差異夠大,AI 不易把 TS 邏輯直接搬過來造成錯誤,是最穩的選擇。
|
||
- 選 **AssemblyScript**:適合已熟悉 TypeScript 的開發者,但要注意 AS 不是 TS — 提示 AI 時明確說「AssemblyScript,不是 TypeScript」。
|
||
- 選 **Rust**:效能要求高時使用;需要更詳細的提示和更仔細的審查。
|
||
|
||
---
|
||
|
||
## 零件規格:共通規則
|
||
|
||
無論使用哪個語言,零件必須遵守:
|
||
|
||
- **I/O 模型**:從 `stdin` 讀取 JSON,往 `stdout` 輸出 JSON,不使用 return value
|
||
- **回傳格式**:成功 `{"success":true,"result":...}`,失敗 `{"success":false,"error":"..."}`
|
||
- **不 panic**:任何錯誤都應輸出 `success:false` JSON,不讓執行器收到空輸出
|
||
- **不使用網路 / 檔案系統**(功能類零件):`no_network_syscall: true`
|
||
- **允許網路**(整合類零件):`no_network_syscall: false`,必須宣告 `credentials_required`
|
||
|
||
---
|
||
|
||
## 目錄結構
|
||
|
||
```
|
||
registry/components/my_component/
|
||
├── component.contract.yaml # 零件規格宣告(必填)
|
||
├── main.go # TinyGo 源碼(TinyGo 零件)
|
||
├── assembly/index.ts # AssemblyScript 源碼(AS 零件)
|
||
├── src/lib.rs # Rust 源碼(Rust 零件)
|
||
└── my_component.wasm # 編譯產出(不提交至 git,CI 自動產生)
|
||
```
|
||
|
||
---
|
||
|
||
## component.contract.yaml
|
||
|
||
所有語言共用相同的合約格式:
|
||
|
||
```yaml
|
||
# component_hash_id 由 Registry 在提交時自動派發,格式為 cmp_{8碼hex}
|
||
# 提交者不需要填這個欄位,Registry 會根據 canonical_id 確定性生成
|
||
# Workflow 引用零件時,用 component_hash_id 才能保證永久不壞:
|
||
# component://cmp_a3f9b2c1 ← 推薦,即使 canonical_id 改名也不受影響
|
||
# component://string_reverse ← 方便,AI 寫 workflow 時用這個,Registry 自動解析
|
||
|
||
canonical_id: "string_reverse" # 見下方命名規範
|
||
display_name: "字串反轉" # 人類可讀,可中文,供 UI 顯示用
|
||
description: > # 語意搜尋用,見下方說明
|
||
將字串內容倒序排列,適合測試、資料清洗、回文判斷等場景。
|
||
不依賴外部服務,純本地運算。
|
||
category: "data" # api / logic / data / ai / style / anim / ui
|
||
version: "v1"
|
||
author: "@your-github-username"
|
||
wasi_target: "preview1"
|
||
stability: "floating" # floating / stable / pinned
|
||
runtime_compat:
|
||
- "cf-workers"
|
||
- "workerd"
|
||
- "wazero"
|
||
constraints:
|
||
max_size_kb: 2048
|
||
max_cold_start_ms: 50
|
||
no_network_syscall: true # 功能類 true,整合類 false
|
||
io_model: "stdin_stdout_json"
|
||
input_schema:
|
||
type: object
|
||
required: [text]
|
||
properties:
|
||
text:
|
||
type: string
|
||
description: 輸入文字
|
||
output_schema:
|
||
type: object
|
||
properties:
|
||
result:
|
||
type: string
|
||
gherkin_tests:
|
||
- scenario: "基本轉換"
|
||
given: '{"text":"hello"}'
|
||
then_contains: '"result"'
|
||
- scenario: "缺少必填欄位"
|
||
given: '{}'
|
||
then_contains: '"success":false'
|
||
config_example: |
|
||
transform:
|
||
text: "{{input.text}}"
|
||
description: "我的零件功能說明。"
|
||
```
|
||
|
||
整合類零件額外加入:
|
||
|
||
```yaml
|
||
credentials_required:
|
||
- key: my_api_token
|
||
type: api_key
|
||
description: "My Service API token"
|
||
inject_as: api_token
|
||
```
|
||
|
||
### canonical_id 命名規範
|
||
|
||
`canonical_id` 是零件的永久識別符,一旦上架不能更改(改名 = 新零件)。命名不統一會導致功能重複,請遵守以下規則:
|
||
|
||
| category | 格式 | 範例 |
|
||
|----------|------|------|
|
||
| `api`(整合類) | `{服務名}` 或 `{服務名}_{動作}` | `gmail`、`gmail_send`、`google_sheets`、`google_sheets_append`、`telegram` |
|
||
| `data`(資料處理) | `{資料型別}_ops` 或 `{動詞}_{名詞}` | `string_ops`、`array_ops`、`json_transform`、`csv_parse` |
|
||
| `logic`(控制流) | `{結構名}_control` 或單詞動詞 | `if_control`、`foreach_control`、`try_catch`、`switch`、`wait` |
|
||
| `ai`(AI 類) | `ai_{動作}` | `ai_transform_compile`、`ai_summarize`、`ai_classify` |
|
||
|
||
**規則:**
|
||
- 全部小寫、底線分隔、最多 4 個單詞
|
||
- 禁止:中文、空格、大寫、連字號(`-`)、版本號混入(用 `version` 欄位表達)
|
||
- `display_name` 才是人類可讀名稱,可以是「宇宙無敵 gsheets 新增一列」,`canonical_id` 不行
|
||
|
||
**提交前自問:** 如果有人想用 AI 搜尋「幫我找一個可以新增 Google Sheets 列的零件」,他搜到的名字應該是什麼?答案就是你的 `canonical_id`。
|
||
|
||
### description 寫法(語意搜尋)
|
||
|
||
`description` 是語意搜尋的索引來源,用自然語言描述「能做什麼、適合什麼情境」,而不是重複零件名稱。
|
||
|
||
**好的 description:**
|
||
```yaml
|
||
description: >
|
||
傳送 Gmail 電子郵件,適合 Workflow 完成後通知使用者、發送訂閱確認信、
|
||
錯誤警報通知等場景。支援自訂主旨、內文與收件人。需要 Gmail OAuth token。
|
||
```
|
||
|
||
**不好的 description(等於沒有):**
|
||
```yaml
|
||
description: "Gmail 發信零件" # 只是名稱的同義詞,搜不到任何額外資訊
|
||
```
|
||
|
||
原則:把這個 description 給一個不知道這個零件存在的人看,他能判斷「這就是我要的東西」嗎?
|
||
|
||
### aliases(搜尋同義詞)
|
||
|
||
arcrun 在 `registry/aliases.yaml` 維護一份 scope 級別的同義詞表。當你的零件 `canonical_id` 以已知 scope 為前綴,Registry 建立搜尋索引時會**自動**把對應的同義詞合併進去,不需要在 contract 裡手動填。
|
||
|
||
例如 `canonical_id: google_sheets_append`,Registry 會自動從 aliases.yaml 取得 `google_sheets` scope 的同義詞(`gsheets`、`試算表`、`spreadsheet`...),搜這些詞都能找到你的零件。
|
||
|
||
**如果你的零件有額外的情境同義詞**(超出 scope 範圍),可以在 contract 內手動補充:
|
||
|
||
```yaml
|
||
canonical_id: "google_sheets_append"
|
||
aliases:
|
||
- "新增資料列" # 情境同義詞,超出 scope 範圍
|
||
- "insert row"
|
||
# google_sheets scope 的同義詞(gsheets / 試算表 / spreadsheet...)
|
||
# 由 registry/aliases.yaml 自動合併,不需要重複填寫
|
||
```
|
||
|
||
**想新增新 scope 的同義詞**(例如你要加一個 `notion` 零件):在 `registry/aliases.yaml` 的對應 category 下加一個新 key,開 PR,merge 後所有以 `notion_` 開頭的零件都自動繼承。
|
||
|
||
> 這個機制目前是手工維護。未來接入 KBDB 後,`canonical_id` 將獲得系統派發的唯一 hash id,同義詞表將成為 KBDB synonym graph 的初始資料。
|
||
|
||
---
|
||
|
||
## TinyGo 零件開發
|
||
|
||
### 環境安裝
|
||
|
||
```bash
|
||
# TinyGo
|
||
brew install tinygo # macOS
|
||
# 其他平台:https://tinygo.org/getting-started/
|
||
|
||
# 本機測試執行器
|
||
brew install wasmtime # macOS
|
||
```
|
||
|
||
### 給 AI 的提示範本
|
||
|
||
```
|
||
請幫我用 TinyGo 寫一個 arcrun WASM 零件。
|
||
|
||
需求:[你的需求]
|
||
|
||
規則:
|
||
- 從 stdin 讀取 JSON,往 stdout 輸出 JSON
|
||
- 成功回傳 {"success":true,"result":...}
|
||
- 失敗回傳 {"success":false,"error":"..."},不 panic
|
||
- 不使用網路、不使用檔案系統(純功能類零件)
|
||
- import 只用標準庫(encoding/json, os, fmt, strings 等)
|
||
|
||
請生成 main.go 和 component.contract.yaml。
|
||
```
|
||
|
||
### main.go 範本
|
||
|
||
```go
|
||
package main
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"os"
|
||
)
|
||
|
||
type Input struct {
|
||
Text string `json:"text"`
|
||
}
|
||
|
||
type Output struct {
|
||
Success bool `json:"success"`
|
||
Result string `json:"result,omitempty"`
|
||
Error string `json:"error,omitempty"`
|
||
}
|
||
|
||
func main() {
|
||
var input Input
|
||
if err := json.NewDecoder(os.Stdin).Decode(&input); err != nil {
|
||
writeError("invalid input: " + err.Error())
|
||
return
|
||
}
|
||
|
||
if input.Text == "" {
|
||
writeError("text is required")
|
||
return
|
||
}
|
||
|
||
// 你的邏輯
|
||
result := "[transformed] " + input.Text
|
||
|
||
out, _ := json.Marshal(Output{Success: true, Result: result})
|
||
fmt.Println(string(out))
|
||
}
|
||
|
||
func writeError(msg string) {
|
||
out, _ := json.Marshal(Output{Success: false, Error: msg})
|
||
fmt.Println(string(out))
|
||
}
|
||
```
|
||
|
||
### 編譯
|
||
|
||
```bash
|
||
cd registry/components/my_component
|
||
tinygo build -o my_component.wasm -target wasi .
|
||
```
|
||
|
||
### 本機測試
|
||
|
||
```bash
|
||
echo '{"text":"hello world"}' | wasmtime run my_component.wasm
|
||
# 預期:{"success":true,"result":"[transformed] hello world"}
|
||
|
||
echo '{}' | wasmtime run my_component.wasm
|
||
# 預期:{"success":false,"error":"text is required"}
|
||
```
|
||
|
||
---
|
||
|
||
## AssemblyScript 零件開發
|
||
|
||
### 環境安裝
|
||
|
||
```bash
|
||
# Node.js >= 18
|
||
node --version
|
||
|
||
# 初始化 AS 專案
|
||
npm init -y
|
||
npm install --save-dev assemblyscript
|
||
npx asinit .
|
||
|
||
# 本機測試執行器
|
||
brew install wasmtime # macOS
|
||
```
|
||
|
||
### 給 AI 的提示範本
|
||
|
||
```
|
||
請幫我用 AssemblyScript(不是 TypeScript)寫一個 arcrun WASM 零件。
|
||
|
||
需求:[你的需求]
|
||
|
||
規則:
|
||
- AssemblyScript 是 TypeScript 的子集,編譯為 WASM,不能使用 DOM / Node.js API
|
||
- 從 stdin 讀取 JSON(使用 WASI fd_read),往 stdout 輸出 JSON(使用 Console.log)
|
||
- 成功回傳 {"success":true,"result":...}
|
||
- 失敗回傳 {"success":false,"error":"..."}
|
||
- 不使用網路、不使用檔案系統
|
||
|
||
請生成 assembly/index.ts 和 component.contract.yaml。
|
||
注意:AssemblyScript 沒有 JSON.parse,需要手動解析或使用 as-json 套件。
|
||
```
|
||
|
||
### assembly/index.ts 範本
|
||
|
||
```typescript
|
||
// AssemblyScript — 注意:這不是 Node.js / TypeScript!
|
||
// 沒有 DOM、沒有 fetch、沒有 require
|
||
|
||
import { Console } from "as-wasi/assembly";
|
||
import { JSON } from "assemblyscript-json/assembly";
|
||
|
||
export function _start(): void {
|
||
// 從 stdin 讀取輸入
|
||
const input = Console.readAll();
|
||
|
||
// 解析 JSON
|
||
const parsed = JSON.parse(input);
|
||
if (!parsed.isObj) {
|
||
Console.log('{"success":false,"error":"invalid input"}');
|
||
return;
|
||
}
|
||
|
||
const obj = parsed as JSON.Obj;
|
||
const textVal = obj.getString("text");
|
||
if (textVal == null) {
|
||
Console.log('{"success":false,"error":"text is required"}');
|
||
return;
|
||
}
|
||
|
||
const text = textVal.valueOf();
|
||
|
||
// 你的邏輯
|
||
const result = "[transformed] " + text;
|
||
|
||
Console.log('{"success":true,"result":"' + result + '"}');
|
||
}
|
||
```
|
||
|
||
**專案依賴(package.json):**
|
||
|
||
```json
|
||
{
|
||
"dependencies": {
|
||
"as-wasi": "^0.4.7",
|
||
"assemblyscript-json": "^1.1.0"
|
||
}
|
||
}
|
||
```
|
||
|
||
### 編譯
|
||
|
||
```bash
|
||
cd registry/components/my_component
|
||
npm install
|
||
npx asc assembly/index.ts \
|
||
--target release \
|
||
--outFile my_component.wasm \
|
||
--exportRuntime \
|
||
--use abort=~lib/wasi_abort
|
||
```
|
||
|
||
### 本機測試
|
||
|
||
```bash
|
||
echo '{"text":"hello world"}' | wasmtime run my_component.wasm
|
||
# 預期:{"success":true,"result":"[transformed] hello world"}
|
||
```
|
||
|
||
---
|
||
|
||
## Rust 零件開發
|
||
|
||
Rust 零件支援已就緒,但文件尚在完善中。如果你熟悉 Rust + WASM,歡迎參考 [wasm-wasi 官方文件](https://doc.rust-lang.org/stable/reference/linkage.html),核心要求與其他語言相同:WASI preview1,stdin/stdout JSON I/O。
|
||
|
||
基本設定:
|
||
|
||
```bash
|
||
rustup target add wasm32-wasip1
|
||
cargo build --target wasm32-wasip1 --release
|
||
```
|
||
|
||
---
|
||
|
||
## 提交至公眾 Registry
|
||
|
||
```bash
|
||
# 確保 .wasm 已編譯
|
||
ls my_component.wasm
|
||
|
||
# 提交(需要 arcrun.dev API Key)
|
||
acr parts publish ./registry/components/my_component/
|
||
```
|
||
|
||
提交後流程:
|
||
|
||
| 狀態 | 說明 |
|
||
|------|------|
|
||
| `sandbox_pending` | 沙盒驗收執行中 |
|
||
| `author_only` | 驗收通過,你自己可用 |
|
||
| `public` | 人工審核通過,所有人可用,開始累積統計 |
|
||
|
||
查詢審核進度:
|
||
|
||
```bash
|
||
acr parts publish --status <submission_id>
|
||
```
|
||
|
||
---
|
||
|
||
## 常見問題
|
||
|
||
### `no_network_syscall` 設定錯誤
|
||
|
||
- **功能類**(category: logic / data / ai):`no_network_syscall: true`。這類零件應完全沙盒化。
|
||
- **整合類**(category: api):`no_network_syscall: false`,因為要呼叫外部 API。
|
||
|
||
兩者都需要宣告在 `constraints` 下,設錯會在 syscall 掃描步驟被沙盒拒絕。
|
||
|
||
### `gherkin_tests` 必須包含 happy path 和 error path
|
||
|
||
至少兩個測試場景:一個輸入正確的 happy path、一個缺少必填欄位或輸入非法的 error path。
|
||
|
||
### 體積超過上限
|
||
|
||
- TinyGo:確認使用 `-target wasi`(而非 `-target wasm`),前者體積更小
|
||
- AssemblyScript:加上 `--optimize` 或 `--target release`
|
||
- Rust:使用 `--release` 並加入 `opt-level = "z"` 到 `Cargo.toml`
|
||
|
||
---
|
||
|
||
## 問題回報
|
||
|
||
開 Issue:[github.com/richblack/arcrun/issues](https://github.com/richblack/arcrun/issues)
|