Files
Arcrun/CONTRIBUTING.md
Leo 8e2c32e466 feat(registry): component_hash_id — stable id system for workflow references
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>
2026-04-16 14:41:22 +08:00

436 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Contributing to arcrun
感謝你考慮貢獻 arcrun!本文件說明如何新增零件(WASM component)並提交至公眾零件庫。
arcrun 的零件**主要由 AI 撰寫**。你不需要是 TinyGo 或 AssemblyScript 專家,只需要把這份文件和你的 API 文件或需求貼給 AI,讓它生成源碼,你負責編譯、測試、提交。
---
## 選擇開發語言
零件只需要輸出符合 **WASI preview1**`.wasm` 檔案,與使用哪個語言無關。
| 語言 | 輸出大小 | AI 撰寫品質 | 說明 |
|------|---------|------------|------|
| **TinyGo** | 極小(10–80KB) | 優秀 | 官方零件使用;語法簡單,AI 出錯率低 |
| **AssemblyScript** | 小(20150KB | 良好 | 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,開 PRmerge 後所有以 `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 preview1stdin/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)