feat(arcrun): implement arcrun MVP — open-source AI workflow engine

Phase 1-5 complete per .agents/specs/u6u-core-mvp/:

**Phase 1 — Cherry-pick & cleanup**
- Create arcrun/ from cypher-executor, credentials, builtins, registry
- Remove 9 InkStone Service Bindings (KBDB, REGISTRY, CLINIC_*, AICEO, MINI_ME)
- Rewrite component-loader: 3-layer (builtin → WASM_BUCKET R2 → error)
- Remove autoPublishMissing.ts, proxy.ts (AICEO), execution-logger.ts (KBDB)
- Clean all KV namespace IDs and InkStone internal URLs from config files

**Phase 2 — contract.yaml completeness**
- Add credentials_required to gmail, google_sheets, telegram, line_notify
- Add config_example to all 21 components with annotated field descriptions

**Phase 3 — Credential injection**
- Add credential-injector.ts: AES-GCM decrypt from CREDENTIALS_KV
- Integrate into GraphExecutor before WASM execution
- Structured errors with repair instructions when credential missing

**Phase 4 — CLI (acr)**
- cli/package.json: arcrun package, bin: acr, deps: commander/js-yaml/chalk/ora
- 8 commands: init, creds push, push, run, validate, parts, list, logs
- Standard mode: writes directly to user's CF KV via CF REST API
- acr init: interactive setup with arcrun.dev API Key registration

**Phase 5 — Open source release prep**
- README.md: 5-minute quickstart, component table, workflow YAML syntax
- CONTRIBUTING.md: TinyGo dev env, component scaffolding, submission flow
- Security audit: no InkStone internal URLs/IDs in committed files
- .gitignore: exclude credentials.yaml, .wrangler, *.wasm

https://claude.ai/code/session_01BnCdSLVH8tUed9VrrPavgT
This commit is contained in:
Claude
2026-04-16 04:06:25 +00:00
commit 2707fca32b
155 changed files with 17413 additions and 0 deletions
@@ -0,0 +1,51 @@
canonical_id: "ai_transform_compile"
display_name: "AI 轉換編譯"
category: "logic"
version: "v1"
wasi_target: "preview1"
stability: "floating"
runtime_compat:
- "cf-workers"
- "workerd"
- "wazero"
constraints:
max_size_kb: 2048
max_cold_start_ms: 50
no_network_syscall: true
no_filesystem_syscall: true
io_model: "stdin_stdout_json"
input_schema:
type: object
required: [description]
properties:
description:
type: string
description: 自然語言描述,如「把日期改成台灣格式」
example_input: {}
output_schema:
type: object
properties:
success:
type: boolean
data:
type: object
properties:
transform_id:
type: string
fn_preview:
type: string
description:
type: string
gherkin_tests:
- scenario: "正常編譯"
given: '{"description":"把日期改成台灣格式","example_input":{}}'
then_contains: '"transform_id"'
- scenario: "缺少 description"
given: '{}'
then_contains: '{"success":false'
tags: [ai, transform, compile, nlp, codegen]
description: "Phase 0 stub:接收自然語言描述,回傳 transform_id 和 fn_preview placeholder。Phase 2 接 AI host function。"
config_example: |
my_ai_compile: # 節點名稱(可自訂)
description: "把日期改成台灣格式" # 自然語言轉換描述(必填)
example_input: {"date": "2024-01-15"} # 範例輸入,用於輔助 AI 生成(選填)
@@ -0,0 +1,3 @@
module component
go 1.21
@@ -0,0 +1,53 @@
// ai_transform_compile — 接收自然語言描述,輸出 transform stub
// Phase 0: 輸出 placeholderPhase 2 再接 AI host function
//
//go:build tinygo
package main
import (
"encoding/json"
"io"
"os"
"strconv"
"time"
)
type Input struct {
Description string `json:"description"`
ExampleInput json.RawMessage `json:"example_input"`
}
func main() {
raw, err := io.ReadAll(os.Stdin)
if err != nil {
writeError("failed to read stdin: " + err.Error())
return
}
var input Input
if err := json.Unmarshal(raw, &input); err != nil {
writeError("invalid input JSON: " + err.Error())
return
}
if input.Description == "" {
writeError("description 必填")
return
}
transformID := "at-" + strconv.FormatInt(time.Now().UnixNano()/1e6, 10)
out, _ := json.Marshal(map[string]interface{}{
"success": true,
"data": map[string]interface{}{
"transform_id": transformID,
"fn_preview": "// AI generated\n// TODO: Phase 2 will implement AI-powered code generation\nreturn input;",
"description": input.Description,
},
})
os.Stdout.Write(out)
}
func writeError(msg string) {
out, _ := json.Marshal(map[string]interface{}{"success": false, "error": msg})
os.Stdout.Write(out)
}
@@ -0,0 +1,48 @@
canonical_id: "ai_transform_run"
display_name: "AI 轉換執行"
category: "logic"
version: "v1"
wasi_target: "preview1"
stability: "floating"
runtime_compat:
- "cf-workers"
- "workerd"
- "wazero"
constraints:
max_size_kb: 2048
max_cold_start_ms: 50
no_network_syscall: true
no_filesystem_syscall: true
io_model: "stdin_stdout_json"
input_schema:
type: object
required: [transform_id, input]
properties:
transform_id:
type: string
description: 由 ai_transform_compile 回傳的 ID
input: {}
output_schema:
type: object
properties:
success:
type: boolean
data:
type: object
properties:
result: {}
transform_id:
type: string
gherkin_tests:
- scenario: "正常執行"
given: '{"transform_id":"at-123","input":{"date":"2024-01-15"}}'
then_contains: '"transform_id":"at-123"'
- scenario: "缺少 transform_id"
given: '{"input":{}}'
then_contains: '{"success":false'
tags: [ai, transform, run, execute]
description: "Phase 0 stub:使用 transform_id 執行轉換,目前直接回傳 input。Phase 2 接 AI host function。"
config_example: |
my_ai_run: # 節點名稱(可自訂)
transform_id: "at-abc123" # 由 ai_transform_compile 回傳的轉換 ID(必填)
input: {"date": "2024-01-15"} # 要套用轉換的輸入資料(必填)
@@ -0,0 +1,3 @@
module component
go 1.21
@@ -0,0 +1,48 @@
// ai_transform_run — 使用已編譯的 transform_id 執行轉換
// Phase 0: stub 實作,直接回傳 input
//
//go:build tinygo
package main
import (
"encoding/json"
"io"
"os"
)
type Input struct {
TransformID string `json:"transform_id"`
Input json.RawMessage `json:"input"`
}
func main() {
raw, err := io.ReadAll(os.Stdin)
if err != nil {
writeError("failed to read stdin: " + err.Error())
return
}
var input Input
if err := json.Unmarshal(raw, &input); err != nil {
writeError("invalid input JSON: " + err.Error())
return
}
if input.TransformID == "" {
writeError("transform_id 必填")
return
}
out, _ := json.Marshal(map[string]interface{}{
"success": true,
"data": map[string]interface{}{
"result": input.Input,
"transform_id": input.TransformID,
},
})
os.Stdout.Write(out)
}
func writeError(msg string) {
out, _ := json.Marshal(map[string]interface{}{"success": false, "error": msg})
os.Stdout.Write(out)
}
@@ -0,0 +1,56 @@
canonical_id: "array_ops"
display_name: "陣列操作"
category: "logic"
version: "v1"
wasi_target: "preview1"
stability: "floating"
runtime_compat:
- "cf-workers"
- "workerd"
- "wazero"
constraints:
max_size_kb: 2048
max_cold_start_ms: 50
no_network_syscall: true
no_filesystem_syscall: true
io_model: "stdin_stdout_json"
input_schema:
type: object
required: [operation, input]
properties:
operation:
type: string
enum: [count, first, last, reverse, sum, average, min, max, sort, unique]
input:
type: array
description: 輸入陣列(元素為數字或字串)
args:
type: object
output_schema:
type: object
properties:
success:
type: boolean
data:
type: object
properties:
result: {}
operation:
type: string
gherkin_tests:
- scenario: "sort 數字陣列"
given: '{"operation":"sort","input":[3,1,2]}'
then_contains: '"result":[1,2,3]'
- scenario: "sum 操作"
given: '{"operation":"sum","input":[1,2,3]}'
then_contains: '"result":6'
- scenario: "空陣列 first"
given: '{"operation":"first","input":[]}'
then_contains: '{"success":false'
tags: [builtin, data, array, list, transform]
description: "陣列操作:count/first/last/reverse/sum/average/min/max/sort/unique。"
config_example: |
my_array_op: # 節點名稱(可自訂)
operation: "sort" # 運算類型(必填),可選值:count/first/last/reverse/sum/average/min/max/sort/unique
input: [3, 1, 4, 1, 5, 9, 2, 6] # 輸入陣列,元素為數字或字串(必填)
args: {} # 操作參數(選填,目前各 operation 不需額外參數)
+3
View File
@@ -0,0 +1,3 @@
module component
go 1.21
+205
View File
@@ -0,0 +1,205 @@
// array_ops — 陣列操作
// 支援: count, first, last, reverse, sum, average, min, max, sort, unique
// input 陣列元素支援 float64 或 string
//
//go:build tinygo
package main
import (
"encoding/json"
"io"
"math"
"os"
"sort"
"strconv"
)
type Input struct {
Operation string `json:"operation"`
Input []json.RawMessage `json:"input"`
Args map[string]string `json:"args"`
}
func main() {
raw, err := io.ReadAll(os.Stdin)
if err != nil {
writeError("failed to read stdin: " + err.Error())
return
}
var input Input
if err := json.Unmarshal(raw, &input); err != nil {
writeError("invalid input JSON: " + err.Error())
return
}
if input.Operation == "" {
writeError("operation 必填")
return
}
items := input.Input
op := input.Operation
switch op {
case "count":
writeResult(op, len(items))
case "first":
if len(items) == 0 {
writeError("陣列為空")
return
}
writeResultRaw(op, items[0])
case "last":
if len(items) == 0 {
writeError("陣列為空")
return
}
writeResultRaw(op, items[len(items)-1])
case "reverse":
reversed := make([]json.RawMessage, len(items))
for i, v := range items {
reversed[len(items)-1-i] = v
}
writeResultRaw(op, reversed)
case "sum":
nums, err := toFloats(items)
if err != nil {
writeError(err.Error())
return
}
sum := 0.0
for _, n := range nums {
sum += n
}
writeResult(op, sum)
case "average":
nums, err := toFloats(items)
if err != nil {
writeError(err.Error())
return
}
if len(nums) == 0 {
writeError("陣列為空")
return
}
sum := 0.0
for _, n := range nums {
sum += n
}
writeResult(op, sum/float64(len(nums)))
case "min":
nums, err := toFloats(items)
if err != nil {
writeError(err.Error())
return
}
if len(nums) == 0 {
writeError("陣列為空")
return
}
m := math.MaxFloat64
for _, n := range nums {
if n < m {
m = n
}
}
writeResult(op, m)
case "max":
nums, err := toFloats(items)
if err != nil {
writeError(err.Error())
return
}
if len(nums) == 0 {
writeError("陣列為空")
return
}
m := -math.MaxFloat64
for _, n := range nums {
if n > m {
m = n
}
}
writeResult(op, m)
case "sort":
// 嘗試數字排序,失敗則字串排序
nums, err := toFloats(items)
if err == nil {
sort.Float64s(nums)
writeResult(op, nums)
return
}
strs, err2 := toStrings(items)
if err2 != nil {
writeError("sort 只支援數字或字串陣列")
return
}
sort.Strings(strs)
writeResult(op, strs)
case "unique":
seen := map[string]bool{}
var result []json.RawMessage
for _, item := range items {
key := string(item)
if !seen[key] {
seen[key] = true
result = append(result, item)
}
}
if result == nil {
result = []json.RawMessage{}
}
writeResultRaw(op, result)
default:
writeError("不支援的 operation: " + op)
}
}
func toFloats(items []json.RawMessage) ([]float64, error) {
nums := make([]float64, len(items))
for i, item := range items {
var n float64
if err := json.Unmarshal(item, &n); err != nil {
return nil, &parseError{"元素 " + strconv.Itoa(i) + " 不是數字"}
}
nums[i] = n
}
return nums, nil
}
func toStrings(items []json.RawMessage) ([]string, error) {
strs := make([]string, len(items))
for i, item := range items {
var s string
if err := json.Unmarshal(item, &s); err != nil {
return nil, &parseError{"元素 " + strconv.Itoa(i) + " 不是字串"}
}
strs[i] = s
}
return strs, nil
}
type parseError struct{ msg string }
func (e *parseError) Error() string { return e.msg }
func writeResult(op string, result interface{}) {
out, _ := json.Marshal(map[string]interface{}{
"success": true,
"data": map[string]interface{}{"result": result, "operation": op},
})
os.Stdout.Write(out)
}
func writeResultRaw(op string, result interface{}) {
out, _ := json.Marshal(map[string]interface{}{
"success": true,
"data": map[string]interface{}{"result": result, "operation": op},
})
os.Stdout.Write(out)
}
func writeError(msg string) {
out, _ := json.Marshal(map[string]interface{}{"success": false, "error": msg})
os.Stdout.Write(out)
}
@@ -0,0 +1,57 @@
canonical_id: "cron"
display_name: "定時排程"
category: "logic"
version: "v1"
wasi_target: "preview1"
stability: "floating"
runtime_compat:
- "cf-workers"
- "workerd"
- "wazero"
constraints:
max_size_kb: 2048
max_cold_start_ms: 50
no_network_syscall: true
no_filesystem_syscall: true
io_model: "stdin_stdout_json"
input_schema:
type: object
required: [cron_expr]
properties:
cron_expr:
type: string
description: 標準 5 欄位 cron expression,如 0 9 * * *
description:
type: string
output_schema:
type: object
properties:
success:
type: boolean
data:
type: object
properties:
cron_id:
type: string
cron_expr:
type: string
enabled:
type: boolean
description:
type: string
gherkin_tests:
- scenario: "有效 cron expression"
given: '{"cron_expr":"0 9 * * *","description":"每天早上9點"}'
then_contains: '"enabled":true'
- scenario: "無效 cron expression(欄位數不對)"
given: '{"cron_expr":"0 9 * *"}'
then_contains: '{"success":false'
- scenario: "缺少 cron_expr"
given: '{}'
then_contains: '{"success":false'
tags: [builtin, cron, schedule, trigger, timer]
description: "驗證 cron expression 格式並回傳 cron_id。實際排程由 Cypher Executor 負責。"
config_example: |
my_cron: # 節點名稱(可自訂)
cron_expr: "0 9 * * *" # 標準 5 欄位 cron 表達式(必填),如:每天早上 9 點
description: "每天早上9點執行" # 排程說明文字(選填)
+3
View File
@@ -0,0 +1,3 @@
module component
go 1.21
+123
View File
@@ -0,0 +1,123 @@
// cron — 驗證 cron expression 格式,回傳 cron_id
// 實際排程由 Cypher Executor 負責
//
//go:build tinygo
package main
import (
"encoding/json"
"io"
"os"
"strconv"
"strings"
"time"
)
type Input struct {
CronExpr string `json:"cron_expr"`
Description string `json:"description"`
}
func main() {
raw, err := io.ReadAll(os.Stdin)
if err != nil {
writeError("failed to read stdin: " + err.Error())
return
}
var input Input
if err := json.Unmarshal(raw, &input); err != nil {
writeError("invalid input JSON: " + err.Error())
return
}
if input.CronExpr == "" {
writeError("cron_expr 必填")
return
}
if err := validateCronExpr(input.CronExpr); err != nil {
writeError("無效的 cron expression: " + err.Error())
return
}
cronID := "cron-" + strconv.FormatInt(time.Now().UnixNano()/1e6, 10)
out, _ := json.Marshal(map[string]interface{}{
"success": true,
"data": map[string]interface{}{
"cron_id": cronID,
"cron_expr": input.CronExpr,
"enabled": true,
"description": input.Description,
},
})
os.Stdout.Write(out)
}
// validateCronExpr — 驗證標準 5 欄位 cron expression
func validateCronExpr(expr string) error {
fields := strings.Fields(expr)
if len(fields) != 5 {
return &cronError{"需要 5 個欄位(分 時 日 月 週),實際: " + strconv.Itoa(len(fields))}
}
// 各欄位範圍: 分(0-59), 時(0-23), 日(1-31), 月(1-12), 週(0-7)
ranges := [][2]int{{0, 59}, {0, 23}, {1, 31}, {1, 12}, {0, 7}}
names := []string{"分鐘", "小時", "日", "月", "星期"}
for i, field := range fields {
if err := validateCronField(field, ranges[i][0], ranges[i][1], names[i]); err != nil {
return err
}
}
return nil
}
func validateCronField(field string, min, max int, name string) error {
if field == "*" {
return nil
}
// 支援 */n 格式
if strings.HasPrefix(field, "*/") {
n, err := strconv.Atoi(field[2:])
if err != nil || n <= 0 {
return &cronError{name + " 步進值無效: " + field}
}
return nil
}
// 支援 a-b 範圍
if strings.Contains(field, "-") {
parts := strings.SplitN(field, "-", 2)
a, err1 := strconv.Atoi(parts[0])
b, err2 := strconv.Atoi(parts[1])
if err1 != nil || err2 != nil || a < min || b > max || a > b {
return &cronError{name + " 範圍無效: " + field}
}
return nil
}
// 支援逗號分隔
if strings.Contains(field, ",") {
for _, part := range strings.Split(field, ",") {
n, err := strconv.Atoi(part)
if err != nil || n < min || n > max {
return &cronError{name + " 值無效: " + part}
}
}
return nil
}
// 單一數字
n, err := strconv.Atoi(field)
if err != nil || n < min || n > max {
return &cronError{name + " 值超出範圍 [" + strconv.Itoa(min) + "-" + strconv.Itoa(max) + "]: " + field}
}
return nil
}
type cronError struct{ msg string }
func (e *cronError) Error() string { return e.msg }
func writeError(msg string) {
out, _ := json.Marshal(map[string]interface{}{"success": false, "error": msg})
os.Stdout.Write(out)
}
@@ -0,0 +1,61 @@
canonical_id: "date_ops"
display_name: "日期操作"
category: "logic"
version: "v1"
wasi_target: "preview1"
stability: "floating"
runtime_compat:
- "cf-workers"
- "workerd"
- "wazero"
constraints:
max_size_kb: 2048
max_cold_start_ms: 50
no_network_syscall: true
no_filesystem_syscall: true
io_model: "stdin_stdout_json"
input_schema:
type: object
required: [operation]
properties:
operation:
type: string
enum: [now, format, parse]
input:
type: string
description: ISO 日期字串(now 操作可省略)
args:
type: object
properties:
layout:
type: string
description: Go time layout(如 2006-01-02
output_schema:
type: object
properties:
success:
type: boolean
data:
type: object
properties:
result: {}
operation:
type: string
gherkin_tests:
- scenario: "now 操作"
given: '{"operation":"now"}'
then_contains: '"success":true'
- scenario: "parse 操作"
given: '{"operation":"parse","input":"2024-01-15T10:30:00Z"}'
then_contains: '"year":2024'
- scenario: "無效日期"
given: '{"operation":"parse","input":"not-a-date"}'
then_contains: '{"success":false'
tags: [builtin, data, date, time, transform]
description: "日期操作:now(當前時間)、format(格式化)、parse(解析 ISO 字串)。"
config_example: |
my_date_op: # 節點名稱(可自訂)
operation: "format" # 運算類型(必填),可選值:now/format/parse
input: "2024-01-15T10:30:00Z" # ISO 日期字串(now 操作可省略,其餘必填)
args: # 操作參數(選填)
layout: "2006-01-02" # format 用:Go time layout 格式字串
+3
View File
@@ -0,0 +1,3 @@
module component
go 1.21
+103
View File
@@ -0,0 +1,103 @@
// date_ops — 日期操作
// 支援: now, format, parse
// TinyGo time 套件支援有限,只實作基本功能
//
//go:build tinygo
package main
import (
"encoding/json"
"io"
"os"
"time"
)
type Args struct {
Layout string `json:"layout"`
}
type Input struct {
Operation string `json:"operation"`
Input string `json:"input"`
Args Args `json:"args"`
}
func main() {
raw, err := io.ReadAll(os.Stdin)
if err != nil {
writeError("failed to read stdin: " + err.Error())
return
}
var input Input
if err := json.Unmarshal(raw, &input); err != nil {
writeError("invalid input JSON: " + err.Error())
return
}
if input.Operation == "" {
writeError("operation 必填")
return
}
switch input.Operation {
case "now":
result := time.Now().UTC().Format(time.RFC3339)
writeResult("now", result)
case "format":
if input.Input == "" {
writeError("format 需要 input 日期字串")
return
}
t, err := time.Parse(time.RFC3339, input.Input)
if err != nil {
// 嘗試其他格式
t, err = time.Parse("2006-01-02", input.Input)
if err != nil {
writeError("無法解析日期: " + err.Error())
return
}
}
layout := input.Args.Layout
if layout == "" {
layout = time.RFC3339
}
writeResult("format", t.Format(layout))
case "parse":
if input.Input == "" {
writeError("parse 需要 input 日期字串")
return
}
t, err := time.Parse(time.RFC3339, input.Input)
if err != nil {
t, err = time.Parse("2006-01-02", input.Input)
if err != nil {
writeError("無法解析日期: " + err.Error())
return
}
}
writeResult("parse", map[string]interface{}{
"iso": t.UTC().Format(time.RFC3339),
"year": t.Year(),
"month": int(t.Month()),
"day": t.Day(),
"hour": t.Hour(),
"min": t.Minute(),
"sec": t.Second(),
})
default:
writeError("不支援的 operation: " + input.Operation)
}
}
func writeResult(op string, result interface{}) {
out, _ := json.Marshal(map[string]interface{}{
"success": true,
"data": map[string]interface{}{"result": result, "operation": op},
})
os.Stdout.Write(out)
}
func writeError(msg string) {
out, _ := json.Marshal(map[string]interface{}{"success": false, "error": msg})
os.Stdout.Write(out)
}
@@ -0,0 +1,67 @@
canonical_id: "filter"
display_name: "過濾陣列"
category: "logic"
version: "v1"
wasi_target: "preview1"
stability: "floating"
runtime_compat:
- "cf-workers"
- "workerd"
- "wazero"
constraints:
max_size_kb: 2048
max_cold_start_ms: 50
no_network_syscall: true
no_filesystem_syscall: true
io_model: "stdin_stdout_json"
input_schema:
type: object
required: [items, condition]
properties:
items:
type: array
description: 要過濾的陣列
condition:
type: object
required: [key, op, value]
properties:
key:
type: string
description: 要比較的欄位名稱
op:
type: string
enum: [eq, ne, gt, lt, contains]
value:
type: string
description: 比較值
output_schema:
type: object
properties:
success:
type: boolean
data:
type: object
properties:
items:
type: array
count:
type: number
gherkin_tests:
- scenario: "過濾 status=active 的元素"
given: '{"items":[{"status":"active"},{"status":"inactive"}],"condition":{"key":"status","op":"eq","value":"active"}}'
then_contains: '{"success":true'
- scenario: "空陣列輸入"
given: '{"items":[],"condition":{"key":"status","op":"eq","value":"active"}}'
then_contains: '{"success":true'
- scenario: "缺少 condition.key"
given: '{"items":[],"condition":{"op":"eq","value":"x"}}'
then_contains: '{"success":false'
tags: [builtin, filter, array, condition]
description: "依條件過濾陣列,回傳符合條件的元素。支援 eq/ne/gt/lt/contains 運算子。"
config_example: |
my_filter: # 節點名稱(可自訂)
items: "{{upstream.results}}" # 要過濾的陣列(必填)
condition: # 過濾條件(必填)
key: status # 要比較的欄位名稱(必填)
op: eq # 運算子:eq / ne / gt / lt / contains(必填)
value: active # 比較值(必填)
+3
View File
@@ -0,0 +1,3 @@
module component
go 1.21
+122
View File
@@ -0,0 +1,122 @@
// filter — 依條件過濾陣列
// op 支援: eq, ne, gt, lt, contains
//
//go:build tinygo
package main
import (
"encoding/json"
"io"
"os"
"strconv"
"strings"
)
type Condition struct {
Key string `json:"key"`
Op string `json:"op"`
Value string `json:"value"`
}
type Input struct {
Items []json.RawMessage `json:"items"`
Condition Condition `json:"condition"`
}
func main() {
raw, err := io.ReadAll(os.Stdin)
if err != nil {
writeError("failed to read stdin: " + err.Error())
return
}
var input Input
if err := json.Unmarshal(raw, &input); err != nil {
writeError("invalid input JSON: " + err.Error())
return
}
if input.Condition.Key == "" {
writeError("condition.key 必填")
return
}
var filtered []json.RawMessage
for _, item := range input.Items {
var obj map[string]json.RawMessage
if err := json.Unmarshal(item, &obj); err != nil {
continue
}
fieldRaw, ok := obj[input.Condition.Key]
if !ok {
continue
}
if matchCondition(fieldRaw, input.Condition.Op, input.Condition.Value) {
filtered = append(filtered, item)
}
}
if filtered == nil {
filtered = []json.RawMessage{}
}
out, _ := json.Marshal(map[string]interface{}{
"success": true,
"data": map[string]interface{}{
"items": filtered,
"count": len(filtered),
},
})
os.Stdout.Write(out)
}
func matchCondition(fieldRaw json.RawMessage, op, expected string) bool {
// 取得欄位字串值
var strVal string
var numVal float64
isNum := false
// 嘗試解析為數字
if err := json.Unmarshal(fieldRaw, &numVal); err == nil {
isNum = true
strVal = strconv.FormatFloat(numVal, 'f', -1, 64)
} else {
// 嘗試解析為字串
if err := json.Unmarshal(fieldRaw, &strVal); err != nil {
strVal = string(fieldRaw)
}
}
switch strings.ToLower(op) {
case "eq":
return strVal == expected
case "ne":
return strVal != expected
case "gt":
if !isNum {
return false
}
threshold, err := strconv.ParseFloat(expected, 64)
if err != nil {
return false
}
return numVal > threshold
case "lt":
if !isNum {
return false
}
threshold, err := strconv.ParseFloat(expected, 64)
if err != nil {
return false
}
return numVal < threshold
case "contains":
return strings.Contains(strVal, expected)
default:
return false
}
}
func writeError(msg string) {
out, _ := json.Marshal(map[string]interface{}{"success": false, "error": msg})
os.Stdout.Write(out)
}
@@ -0,0 +1,56 @@
canonical_id: "foreach_control"
display_name: "迴圈控制"
category: "logic"
version: "v1"
wasi_target: "preview1"
stability: "floating"
runtime_compat:
- "cf-workers"
- "workerd"
- "wazero"
constraints:
max_size_kb: 2048
max_cold_start_ms: 50
no_network_syscall: true
no_filesystem_syscall: true
io_model: "stdin_stdout_json"
input_schema:
type: object
required: [items]
properties:
items:
type: array
description: 要迭代的陣列
item_key:
type: string
description: 每個元素注入的變數名,預設 item
output_schema:
type: object
properties:
success:
type: boolean
data:
type: object
properties:
items:
type: array
count:
type: number
current_index:
type: number
current_item: {}
item_key:
type: string
gherkin_tests:
- scenario: "正常迭代"
given: '{"items":[1,2,3],"item_key":"item"}'
then_contains: '"current_index":0'
- scenario: "空陣列"
given: '{"items":[]}'
then_contains: '{"success":false'
tags: [builtin, control, foreach, loop, iteration]
description: "輸出第一個元素供 Cypher Executor 迭代,current_index 從 0 開始。"
config_example: |
my_loop: # 節點名稱(可自訂)
items: "{{upstream.results}}" # 要迭代的陣列(必填)
item_key: item # 每個元素注入的變數名,預設 item(選填)
@@ -0,0 +1,3 @@
module component
go 1.21
@@ -0,0 +1,55 @@
// foreach_control — 輸出第一個元素,Cypher Executor 負責迭代
//
//go:build tinygo
package main
import (
"encoding/json"
"io"
"os"
)
type Input struct {
Items []json.RawMessage `json:"items"`
ItemKey string `json:"item_key"`
}
func main() {
raw, err := io.ReadAll(os.Stdin)
if err != nil {
writeError("failed to read stdin: " + err.Error())
return
}
var input Input
if err := json.Unmarshal(raw, &input); err != nil {
writeError("invalid input JSON: " + err.Error())
return
}
if len(input.Items) == 0 {
writeError("items 不能為空")
return
}
itemKey := input.ItemKey
if itemKey == "" {
itemKey = "item"
}
out, _ := json.Marshal(map[string]interface{}{
"success": true,
"data": map[string]interface{}{
"items": input.Items,
"count": len(input.Items),
"current_index": 0,
"current_item": input.Items[0],
"item_key": itemKey,
},
})
os.Stdout.Write(out)
}
func writeError(msg string) {
out, _ := json.Marshal(map[string]interface{}{"success": false, "error": msg})
os.Stdout.Write(out)
}
@@ -0,0 +1,60 @@
canonical_id: "gmail"
display_name: "Gmail 發信"
category: "api"
version: "v1"
wasi_target: "preview1"
stability: "floating"
runtime_compat:
- "cf-workers"
- "workerd"
- "wazero"
constraints:
max_size_kb: 2048
max_cold_start_ms: 50
no_network_syscall: true
no_filesystem_syscall: true
io_model: "stdin_stdout_json"
input_schema:
type: object
required: [to, subject, body, access_token]
properties:
to:
type: string
description: 收件人 Email
subject:
type: string
body:
type: string
access_token:
type: string
description: Google OAuth access token
output_schema:
type: object
properties:
success:
type: boolean
data:
type: object
properties:
message_id:
type: string
gherkin_tests:
- scenario: "缺少 access_token"
given: '{"to":"test@example.com","subject":"test","body":"hello"}'
then_contains: '{"success":false'
- scenario: "缺少 to"
given: '{"subject":"test","body":"hello","access_token":"token"}'
then_contains: '{"success":false'
tags: [integration, google, gmail, email, oauth]
description: "透過 Gmail API 發送 Email。透過 host function 呼叫,需要 Google OAuth access_token。"
credentials_required:
- key: gmail_token
type: google_oauth
description: "Google OAuth access tokengmail.send scope"
inject_as: access_token
config_example: |
send_email: # 節點名稱(可自訂)
to: "" # 收件人 Email(必填)
subject: "" # 主旨(必填)
body: "" # 內文(必填)
# access_token 由 credentials.yaml 的 gmail_token 自動注入
+3
View File
@@ -0,0 +1,3 @@
module component
go 1.21
+139
View File
@@ -0,0 +1,139 @@
// gmail — 透過 Gmail API 發送 Email
// 透過 host function 呼叫 Gmail API
//
//go:build tinygo
package main
import (
"encoding/json"
"io"
"os"
"strings"
"unsafe"
)
//go:wasmimport u6u http_request
func hostHttpRequest(
urlPtr uintptr, urlLen uint32,
methodPtr uintptr, methodLen uint32,
headersPtr uintptr, headersLen uint32,
bodyPtr uintptr, bodyLen uint32,
outPtr uintptr, outLenPtr uintptr,
) uint32
type Input struct {
To string `json:"to"`
Subject string `json:"subject"`
Body string `json:"body"`
AccessToken string `json:"access_token"`
}
func main() {
raw, err := io.ReadAll(os.Stdin)
if err != nil {
writeError("failed to read stdin: " + err.Error())
return
}
var input Input
if err := json.Unmarshal(raw, &input); err != nil {
writeError("invalid input JSON: " + err.Error())
return
}
if input.To == "" {
writeError("to 必填")
return
}
if input.Subject == "" {
writeError("subject 必填")
return
}
if input.AccessToken == "" {
writeError("access_token 必填")
return
}
// 建立 RFC 2822 格式的 emailbase64url 編碼
emailLines := []string{
"To: " + input.To,
"Subject: " + input.Subject,
"Content-Type: text/plain; charset=UTF-8",
"",
input.Body,
}
emailRaw := strings.Join(emailLines, "\r\n")
encoded := base64URLEncode([]byte(emailRaw))
bodyData, _ := json.Marshal(map[string]string{"raw": encoded})
apiURL := "https://gmail.googleapis.com/gmail/v1/users/me/messages/send"
method := "POST"
headers := map[string]string{
"Authorization": "Bearer " + input.AccessToken,
"Content-Type": "application/json",
}
headersJSON, _ := json.Marshal(headers)
urlBytes := []byte(apiURL)
methodBytes := []byte(method)
bodyBytes := bodyData
outBuf := make([]byte, 65536)
var outLen uint32
result := hostHttpRequest(
uintptr(unsafe.Pointer(&urlBytes[0])), uint32(len(urlBytes)),
uintptr(unsafe.Pointer(&methodBytes[0])), uint32(len(methodBytes)),
uintptr(unsafe.Pointer(&headersJSON[0])), uint32(len(headersJSON)),
uintptr(unsafe.Pointer(&bodyBytes[0])), uint32(len(bodyBytes)),
uintptr(unsafe.Pointer(&outBuf[0])), uintptr(unsafe.Pointer(&outLen)),
)
if result != 0 {
writeError("Gmail API 呼叫失敗")
return
}
responseStr := string(outBuf[:outLen])
var responseData map[string]interface{}
if err := json.Unmarshal([]byte(responseStr), &responseData); err != nil {
responseData = map[string]interface{}{"raw": responseStr}
}
messageID, _ := responseData["id"].(string)
out, _ := json.Marshal(map[string]interface{}{
"success": true,
"data": map[string]interface{}{"message_id": messageID},
})
os.Stdout.Write(out)
}
// base64URLEncode — 不依賴 encoding/base64TinyGo 相容)
func base64URLEncode(data []byte) string {
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
var sb strings.Builder
for i := 0; i < len(data); i += 3 {
b0 := data[i]
var b1, b2 byte
if i+1 < len(data) {
b1 = data[i+1]
}
if i+2 < len(data) {
b2 = data[i+2]
}
sb.WriteByte(chars[b0>>2])
sb.WriteByte(chars[((b0&0x3)<<4)|(b1>>4)])
if i+1 < len(data) {
sb.WriteByte(chars[((b1&0xf)<<2)|(b2>>6)])
}
if i+2 < len(data) {
sb.WriteByte(chars[b2&0x3f])
}
}
return sb.String()
}
func writeError(msg string) {
out, _ := json.Marshal(map[string]interface{}{"success": false, "error": msg})
os.Stdout.Write(out)
}
@@ -0,0 +1,65 @@
canonical_id: "google_sheets"
display_name: "Google 試算表"
category: "api"
version: "v1"
wasi_target: "preview1"
stability: "floating"
runtime_compat:
- "cf-workers"
- "workerd"
- "wazero"
constraints:
max_size_kb: 2048
max_cold_start_ms: 50
no_network_syscall: true
no_filesystem_syscall: true
io_model: "stdin_stdout_json"
input_schema:
type: object
required: [spreadsheet_id, range, access_token]
properties:
spreadsheet_id:
type: string
range:
type: string
description: 如 Sheet1!A1:B10
action:
type: string
enum: [read, write]
default: read
values:
type: array
description: write 時的資料(二維陣列)
access_token:
type: string
description: Google OAuth access token
output_schema:
type: object
properties:
success:
type: boolean
data:
type: object
properties:
values: {}
range:
type: string
gherkin_tests:
- scenario: "缺少 access_token"
given: '{"spreadsheet_id":"abc","range":"Sheet1!A1"}'
then_contains: '{"success":false'
- scenario: "缺少 spreadsheet_id"
given: '{"range":"Sheet1!A1","access_token":"token"}'
then_contains: '{"success":false'
tags: [integration, google, sheets, oauth]
description: "讀取或寫入 Google 試算表。透過 host function 呼叫 Google Sheets API,需要 OAuth access_token。"
credentials_required:
- key: google_oauth
type: google_oauth
description: "Google OAuth access tokenspreadsheets scope"
inject_as: access_token
config_example: |
read_sheet: # 節點名稱(可自訂)
spreadsheet_id: "" # 試算表 ID(必填)
range: "" # 範圍,如 Sheet1!A1:B10(必填)
# access_token 由 credentials.yaml 的 google_oauth 自動注入
+3
View File
@@ -0,0 +1,3 @@
module component
go 1.21
+135
View File
@@ -0,0 +1,135 @@
// google_sheets — 讀取或寫入 Google 試算表
// 透過 host function 呼叫 Google Sheets API
//
//go:build tinygo
package main
import (
"encoding/json"
"io"
"os"
"unsafe"
)
//go:wasmimport u6u http_request
func hostHttpRequest(
urlPtr uintptr, urlLen uint32,
methodPtr uintptr, methodLen uint32,
headersPtr uintptr, headersLen uint32,
bodyPtr uintptr, bodyLen uint32,
outPtr uintptr, outLenPtr uintptr,
) uint32
type Input struct {
SpreadsheetID string `json:"spreadsheet_id"`
Range string `json:"range"`
Action string `json:"action"`
Values [][]json.RawMessage `json:"values"`
AccessToken string `json:"access_token"`
}
func main() {
raw, err := io.ReadAll(os.Stdin)
if err != nil {
writeError("failed to read stdin: " + err.Error())
return
}
var input Input
if err := json.Unmarshal(raw, &input); err != nil {
writeError("invalid input JSON: " + err.Error())
return
}
if input.SpreadsheetID == "" {
writeError("spreadsheet_id 必填")
return
}
if input.Range == "" {
writeError("range 必填")
return
}
if input.AccessToken == "" {
writeError("access_token 必填")
return
}
action := input.Action
if action == "" {
action = "read"
}
headers := map[string]string{
"Authorization": "Bearer " + input.AccessToken,
"Content-Type": "application/json",
}
headersJSON, _ := json.Marshal(headers)
var apiURL, method, bodyStr string
switch action {
case "read":
apiURL = "https://sheets.googleapis.com/v4/spreadsheets/" + input.SpreadsheetID + "/values/" + input.Range
method = "GET"
bodyStr = ""
case "write":
apiURL = "https://sheets.googleapis.com/v4/spreadsheets/" + input.SpreadsheetID + "/values/" + input.Range + "?valueInputOption=RAW"
method = "PUT"
bodyData, _ := json.Marshal(map[string]interface{}{
"range": input.Range,
"majorDimension": "ROWS",
"values": input.Values,
})
bodyStr = string(bodyData)
default:
writeError("不支援的 action: " + action)
return
}
urlBytes := []byte(apiURL)
methodBytes := []byte(method)
bodyBytes := []byte(bodyStr)
if len(bodyBytes) == 0 {
bodyBytes = []byte{}
}
outBuf := make([]byte, 65536)
var outLen uint32
var bodyPtr uintptr
if len(bodyBytes) > 0 {
bodyPtr = uintptr(unsafe.Pointer(&bodyBytes[0]))
}
result := hostHttpRequest(
uintptr(unsafe.Pointer(&urlBytes[0])), uint32(len(urlBytes)),
uintptr(unsafe.Pointer(&methodBytes[0])), uint32(len(methodBytes)),
uintptr(unsafe.Pointer(&headersJSON[0])), uint32(len(headersJSON)),
bodyPtr, uint32(len(bodyBytes)),
uintptr(unsafe.Pointer(&outBuf[0])), uintptr(unsafe.Pointer(&outLen)),
)
if result != 0 {
writeError("Google Sheets API 呼叫失敗")
return
}
responseStr := string(outBuf[:outLen])
var responseData interface{}
if err := json.Unmarshal([]byte(responseStr), &responseData); err != nil {
responseData = responseStr
}
out, _ := json.Marshal(map[string]interface{}{
"success": true,
"data": map[string]interface{}{
"values": responseData,
"range": input.Range,
},
})
os.Stdout.Write(out)
}
func writeError(msg string) {
out, _ := json.Marshal(map[string]interface{}{"success": false, "error": msg})
os.Stdout.Write(out)
}
@@ -0,0 +1,62 @@
canonical_id: "http_request"
display_name: "HTTP 請求"
category: "api"
version: "v1"
wasi_target: "preview1"
stability: "floating"
runtime_compat:
- "cf-workers"
- "workerd"
- "wazero"
constraints:
max_size_kb: 2048
max_cold_start_ms: 50
no_network_syscall: true
no_filesystem_syscall: true
io_model: "stdin_stdout_json"
input_schema:
type: object
required: [url]
properties:
url:
type: string
description: 目標 URL(必填)
method:
type: string
description: HTTP 方法(GET / POST / PUT / DELETE 等),預設 GET
default: GET
headers:
type: object
description: 自訂 HTTP headerskey-value 物件)
additionalProperties:
type: string
body:
description: 請求 body(任意 JSON
output_schema:
type: object
properties:
success:
type: boolean
data:
type: object
properties:
body:
type: string
description: HTTP 回應 body(字串)
gherkin_tests:
- scenario: "缺少 url"
given: '{"method":"GET"}'
then_contains: '{"success":false'
- scenario: "基本 GET 請求"
given: '{"url":"https://example.com"}'
then_contains: '{"success":true'
tags: [integration, http, request, api]
description: "發送任意 HTTP 請求並回傳 status 與 body。透過 host function 呼叫,.wasm 本身不含網路 syscall。headers 由用戶手動填入。"
config_example: |
http_call: # 節點名稱(可自訂)
url: "" # 目標 URL(必填)
method: "GET" # HTTP 方法(選填,預設 GET)
headers: # 自訂 headers(選填,用戶手動填入)
Content-Type: "application/json"
Authorization: "Bearer <your_token>"
body: {} # 請求 body(選填)
+3
View File
@@ -0,0 +1,3 @@
module component
go 1.21
+100
View File
@@ -0,0 +1,100 @@
// http_request — 發送任意 HTTP 請求,回傳 status + body
// 透過 host function 發出 HTTP.wasm 本身不含網路 syscall
//
//go:build tinygo
package main
import (
"encoding/json"
"io"
"os"
"unsafe"
)
// host function 宣告(由 WASI shim 注入)
//
//go:wasmimport u6u http_request
func hostHttpRequest(
urlPtr uintptr, urlLen uint32,
methodPtr uintptr, methodLen uint32,
headersPtr uintptr, headersLen uint32,
bodyPtr uintptr, bodyLen uint32,
outPtr uintptr, outLenPtr uintptr,
) uint32
type Input struct {
URL string `json:"url"`
Method string `json:"method"`
Headers map[string]string `json:"headers"`
Body json.RawMessage `json:"body"`
}
func main() {
raw, err := io.ReadAll(os.Stdin)
if err != nil {
writeError("failed to read stdin: " + err.Error())
return
}
var input Input
if err := json.Unmarshal(raw, &input); err != nil {
writeError("invalid input JSON: " + err.Error())
return
}
if input.URL == "" {
writeError("url 必填")
return
}
method := input.Method
if method == "" {
method = "GET"
}
// 序列化 headers
headersJSON := "{}"
if len(input.Headers) > 0 {
b, _ := json.Marshal(input.Headers)
headersJSON = string(b)
}
// body
bodyStr := ""
if len(input.Body) > 0 {
bodyStr = string(input.Body)
}
// 呼叫 host function
urlBytes := []byte(input.URL)
methodBytes := []byte(method)
headersBytes := []byte(headersJSON)
bodyBytes := []byte(bodyStr)
outBuf := make([]byte, 65536) // 64KB output buffer
var outLen uint32
result := hostHttpRequest(
uintptr(unsafe.Pointer(&urlBytes[0])), uint32(len(urlBytes)),
uintptr(unsafe.Pointer(&methodBytes[0])), uint32(len(methodBytes)),
uintptr(unsafe.Pointer(&headersBytes[0])), uint32(len(headersBytes)),
uintptr(unsafe.Pointer(&bodyBytes[0])), uint32(len(bodyBytes)),
uintptr(unsafe.Pointer(&outBuf[0])), uintptr(unsafe.Pointer(&outLen)),
)
if result != 0 {
writeError("HTTP request failed")
return
}
responseStr := string(outBuf[:outLen])
out, _ := json.Marshal(map[string]interface{}{
"success": true,
"data": map[string]interface{}{"body": responseStr},
})
os.Stdout.Write(out)
}
func writeError(msg string) {
out, _ := json.Marshal(map[string]interface{}{"success": false, "error": msg})
os.Stdout.Write(out)
}
@@ -0,0 +1,56 @@
canonical_id: "if_control"
display_name: "條件判斷"
category: "logic"
version: "v1"
wasi_target: "preview1"
stability: "floating"
runtime_compat:
- "cf-workers"
- "workerd"
- "wazero"
constraints:
max_size_kb: 2048
max_cold_start_ms: 50
no_network_syscall: true
no_filesystem_syscall: true
io_model: "stdin_stdout_json"
input_schema:
type: object
required: [condition]
properties:
condition:
type: string
description: 條件運算式,支援 keytruthy)、key == value、key > number、key < number
input:
type: object
description: 條件運算式中參照的變數字典
output_schema:
type: object
properties:
success:
type: boolean
data:
type: object
properties:
result:
type: boolean
branch:
type: string
enum: ["true", "false"]
gherkin_tests:
- scenario: "條件成立走 true 分支"
given: '{"condition":"status == active","input":{"status":"active"}}'
then_contains: '"branch":"true"'
- scenario: "條件不成立走 false 分支"
given: '{"condition":"status == active","input":{"status":"inactive"}}'
then_contains: '"branch":"false"'
- scenario: "缺少 condition"
given: '{"input":{"status":"active"}}'
then_contains: '{"success":false'
tags: [builtin, control, if, condition, branch]
description: "評估條件運算式,依結果路由到 true 或 false 分支。"
config_example: |
my_if: # 節點名稱(可自訂)
condition: "status == active" # 條件運算式(必填)
input: # 條件運算式中參照的變數字典(選填)
status: "{{upstream.status}}"
+3
View File
@@ -0,0 +1,3 @@
module component
go 1.21
+138
View File
@@ -0,0 +1,138 @@
// if_control — 單一條件判斷,true/false 兩個出口
// condition 支援:keytruthy)、key == value、key > number、key < number
package main
import (
"encoding/json"
"io"
"os"
"strconv"
"strings"
)
type Input struct {
Condition string `json:"condition"`
Input map[string]interface{} `json:"input"`
}
func main() {
raw, err := io.ReadAll(os.Stdin)
if err != nil {
writeError("failed to read stdin: " + err.Error())
return
}
var input Input
if err := json.Unmarshal(raw, &input); err != nil {
writeError("invalid input JSON: " + err.Error())
return
}
if input.Condition == "" {
writeError("condition 必填")
return
}
result := evaluateCondition(input.Condition, input.Input)
branch := "false"
if result {
branch = "true"
}
out, _ := json.Marshal(map[string]interface{}{
"success": true,
"data": map[string]interface{}{"result": result, "branch": branch},
})
os.Stdout.Write(out)
}
func toString(v interface{}) string {
switch val := v.(type) {
case string:
return val
case float64:
return strconv.FormatFloat(val, 'f', -1, 64)
case bool:
if val {
return "true"
}
return "false"
case nil:
return ""
default:
b, _ := json.Marshal(val)
return string(b)
}
}
func evaluateCondition(condition string, ctx map[string]interface{}) bool {
if ctx == nil {
return false
}
expr := strings.TrimSpace(condition)
// key == value
if idx := strings.Index(expr, "=="); idx > 0 {
key := strings.TrimSpace(expr[:idx])
expected := strings.Trim(strings.TrimSpace(expr[idx+2:]), `"'`)
v, ok := ctx[key]
if !ok {
return false
}
return toString(v) == expected
}
// key > number
if idx := strings.Index(expr, ">"); idx > 0 {
key := strings.TrimSpace(expr[:idx])
threshold, err := strconv.ParseFloat(strings.TrimSpace(expr[idx+1:]), 64)
if err != nil {
return false
}
v, ok := ctx[key]
if !ok {
return false
}
n, err := strconv.ParseFloat(toString(v), 64)
if err != nil {
return false
}
return n > threshold
}
// key < number
if idx := strings.Index(expr, "<"); idx > 0 {
key := strings.TrimSpace(expr[:idx])
threshold, err := strconv.ParseFloat(strings.TrimSpace(expr[idx+1:]), 64)
if err != nil {
return false
}
v, ok := ctx[key]
if !ok {
return false
}
n, err := strconv.ParseFloat(toString(v), 64)
if err != nil {
return false
}
return n < threshold
}
// truthy check
v, ok := ctx[expr]
if !ok {
return false
}
switch val := v.(type) {
case bool:
return val
case string:
return val != ""
case float64:
return val != 0
case nil:
return false
default:
return true
}
}
func writeError(msg string) {
out, _ := json.Marshal(map[string]interface{}{"success": false, "error": msg})
os.Stdout.Write(out)
}
@@ -0,0 +1,54 @@
canonical_id: "line_notify"
display_name: "LINE Notify"
category: "api"
version: "v1"
wasi_target: "preview1"
stability: "floating"
runtime_compat:
- "cf-workers"
- "workerd"
- "wazero"
constraints:
max_size_kb: 2048
max_cold_start_ms: 50
no_network_syscall: true
no_filesystem_syscall: true
io_model: "stdin_stdout_json"
input_schema:
type: object
required: [message, token]
properties:
message:
type: string
description: 要發送的訊息
token:
type: string
description: LINE Notify Channel Access Token
output_schema:
type: object
properties:
success:
type: boolean
data:
type: object
properties:
status:
type: number
gherkin_tests:
- scenario: "缺少 token"
given: '{"message":"hello"}'
then_contains: '{"success":false'
- scenario: "缺少 message"
given: '{"token":"mytoken"}'
then_contains: '{"success":false'
tags: [integration, line, notify, message]
description: "發送 LINE Notify 訊息。透過 host function 呼叫 LINE Notify API,需要 Channel Access Token。"
credentials_required:
- key: line_token
type: line_token
description: "LINE Notify Channel Access Token"
inject_as: token
config_example: |
send_line: # 節點名稱(可自訂)
message: "" # 要發送的訊息(必填)
# token 由 credentials.yaml 的 line_token 自動注入
+3
View File
@@ -0,0 +1,3 @@
module component
go 1.21
+114
View File
@@ -0,0 +1,114 @@
// line_notify — 發送 LINE Notify 訊息
// 透過 host function 呼叫 LINE Notify API
//
//go:build tinygo
package main
import (
"encoding/json"
"io"
"os"
"strings"
"unsafe"
)
//go:wasmimport u6u http_request
func hostHttpRequest(
urlPtr uintptr, urlLen uint32,
methodPtr uintptr, methodLen uint32,
headersPtr uintptr, headersLen uint32,
bodyPtr uintptr, bodyLen uint32,
outPtr uintptr, outLenPtr uintptr,
) uint32
type Input struct {
Message string `json:"message"`
Token string `json:"token"`
}
func main() {
raw, err := io.ReadAll(os.Stdin)
if err != nil {
writeError("failed to read stdin: " + err.Error())
return
}
var input Input
if err := json.Unmarshal(raw, &input); err != nil {
writeError("invalid input JSON: " + err.Error())
return
}
if input.Message == "" {
writeError("message 必填")
return
}
if input.Token == "" {
writeError("token 必填")
return
}
apiURL := "https://notify-api.line.me/api/notify"
method := "POST"
headers := map[string]string{
"Authorization": "Bearer " + input.Token,
"Content-Type": "application/x-www-form-urlencoded",
}
headersJSON, _ := json.Marshal(headers)
// form-encoded body
body := "message=" + urlEncode(input.Message)
urlBytes := []byte(apiURL)
methodBytes := []byte(method)
bodyBytes := []byte(body)
outBuf := make([]byte, 4096)
var outLen uint32
result := hostHttpRequest(
uintptr(unsafe.Pointer(&urlBytes[0])), uint32(len(urlBytes)),
uintptr(unsafe.Pointer(&methodBytes[0])), uint32(len(methodBytes)),
uintptr(unsafe.Pointer(&headersJSON[0])), uint32(len(headersJSON)),
uintptr(unsafe.Pointer(&bodyBytes[0])), uint32(len(bodyBytes)),
uintptr(unsafe.Pointer(&outBuf[0])), uintptr(unsafe.Pointer(&outLen)),
)
if result != 0 {
writeError("LINE Notify API 呼叫失敗")
return
}
out, _ := json.Marshal(map[string]interface{}{
"success": true,
"data": map[string]interface{}{"status": 200},
})
os.Stdout.Write(out)
}
// urlEncode — 簡易 URL 編碼(只處理常見字元)
func urlEncode(s string) string {
var sb strings.Builder
for _, c := range s {
switch {
case c >= 'A' && c <= 'Z', c >= 'a' && c <= 'z', c >= '0' && c <= '9',
c == '-', c == '_', c == '.', c == '~':
sb.WriteRune(c)
case c == ' ':
sb.WriteByte('+')
default:
// UTF-8 encode
buf := []byte(string(c))
for _, b := range buf {
sb.WriteByte('%')
sb.WriteByte("0123456789ABCDEF"[b>>4])
sb.WriteByte("0123456789ABCDEF"[b&0xf])
}
}
}
return sb.String()
}
func writeError(msg string) {
out, _ := json.Marshal(map[string]interface{}{"success": false, "error": msg})
os.Stdout.Write(out)
}
@@ -0,0 +1,50 @@
canonical_id: "merge"
display_name: "合併物件"
category: "logic"
version: "v1"
wasi_target: "preview1"
stability: "floating"
runtime_compat:
- "cf-workers"
- "workerd"
- "wazero"
constraints:
max_size_kb: 2048
max_cold_start_ms: 50
no_network_syscall: true
no_filesystem_syscall: true
io_model: "stdin_stdout_json"
input_schema:
type: object
required: [inputs]
properties:
inputs:
type: array
description: 要合併的物件陣列,後者欄位覆蓋前者
items:
type: object
output_schema:
type: object
properties:
success:
type: boolean
data:
type: object
description: 所有輸入物件合併後的結果
gherkin_tests:
- scenario: "合併兩個物件"
given: '{"inputs":[{"a":1},{"b":2}]}'
then_contains: '"a":1'
- scenario: "後者欄位覆蓋前者"
given: '{"inputs":[{"a":1},{"a":2}]}'
then_contains: '"a":2'
- scenario: "inputs 為空陣列時失敗"
given: '{"inputs":[]}'
then_contains: '{"success":false'
tags: [builtin, merge, combine, object, context]
description: "將多個物件合併為一個,後者欄位覆蓋前者同名欄位。"
config_example: |
my_merge: # 節點名稱(可自訂)
inputs: # 要合併的物件陣列(必填)
- "{{node_a.data}}" # 第一個來源物件
- "{{node_b.data}}" # 第二個來源物件(後者覆蓋前者同名欄位)
+3
View File
@@ -0,0 +1,3 @@
module component
go 1.21
+44
View File
@@ -0,0 +1,44 @@
// merge — 合併多個輸入物件為一個
package main
import (
"encoding/json"
"io"
"os"
)
type Input struct {
Inputs []map[string]interface{} `json:"inputs"`
}
func main() {
raw, err := io.ReadAll(os.Stdin)
if err != nil {
writeError("failed to read stdin: " + err.Error())
return
}
var input Input
if err := json.Unmarshal(raw, &input); err != nil {
writeError("invalid input JSON: " + err.Error())
return
}
if len(input.Inputs) == 0 {
writeError("inputs 陣列不能為空")
return
}
result := make(map[string]interface{})
for _, obj := range input.Inputs {
for k, v := range obj {
result[k] = v
}
}
out, _ := json.Marshal(map[string]interface{}{"success": true, "data": result})
os.Stdout.Write(out)
}
func writeError(msg string) {
out, _ := json.Marshal(map[string]interface{}{"success": false, "error": msg})
os.Stdout.Write(out)
}
@@ -0,0 +1,62 @@
canonical_id: "number_ops"
display_name: "數字操作"
category: "logic"
version: "v1"
wasi_target: "preview1"
stability: "floating"
runtime_compat:
- "cf-workers"
- "workerd"
- "wazero"
constraints:
max_size_kb: 2048
max_cold_start_ms: 50
no_network_syscall: true
no_filesystem_syscall: true
io_model: "stdin_stdout_json"
input_schema:
type: object
required: [operation, input]
properties:
operation:
type: string
enum: [round, floor, ceil, abs, add, subtract, multiply, divide, mod, min, max, format]
input:
type: number
args:
type: object
properties:
value:
type: number
decimals:
type: number
output_schema:
type: object
properties:
success:
type: boolean
data:
type: object
properties:
result: {}
operation:
type: string
gherkin_tests:
- scenario: "round 操作"
given: '{"operation":"round","input":3.14}'
then_contains: '"result":3'
- scenario: "add 操作"
given: '{"operation":"add","input":10,"args":{"value":5}}'
then_contains: '"result":15'
- scenario: "除以零"
given: '{"operation":"divide","input":10,"args":{"value":0}}'
then_contains: '{"success":false'
tags: [builtin, data, number, math, transform]
description: "數字操作:round/floor/ceil/abs/add/subtract/multiply/divide/mod/min/max/format。"
config_example: |
my_number_op: # 節點名稱(可自訂)
operation: "add" # 運算類型(必填),可選值:round/floor/ceil/abs/add/subtract/multiply/divide/mod/min/max/format
input: 10 # 輸入數字(必填)
args: # 操作參數,依 operation 而定(選填)
value: 5 # add/subtract/multiply/divide/mod/min/max 用:第二個運算元
decimals: 2 # round/format 用:小數位數
+3
View File
@@ -0,0 +1,3 @@
module component
go 1.21
+100
View File
@@ -0,0 +1,100 @@
// number_ops — 數字操作
// 支援: round, floor, ceil, abs, add, subtract, multiply, divide, mod, min, max, format
//
//go:build tinygo
package main
import (
"encoding/json"
"io"
"math"
"os"
"strconv"
)
type Args struct {
Value float64 `json:"value"`
Decimals int `json:"decimals"`
}
type Input struct {
Operation string `json:"operation"`
Input float64 `json:"input"`
Args Args `json:"args"`
}
func main() {
raw, err := io.ReadAll(os.Stdin)
if err != nil {
writeError("failed to read stdin: " + err.Error())
return
}
var input Input
if err := json.Unmarshal(raw, &input); err != nil {
writeError("invalid input JSON: " + err.Error())
return
}
if input.Operation == "" {
writeError("operation 必填")
return
}
var result interface{}
switch input.Operation {
case "round":
result = math.Round(input.Input)
case "floor":
result = math.Floor(input.Input)
case "ceil":
result = math.Ceil(input.Input)
case "abs":
result = math.Abs(input.Input)
case "add":
result = input.Input + input.Args.Value
case "subtract":
result = input.Input - input.Args.Value
case "multiply":
result = input.Input * input.Args.Value
case "divide":
if input.Args.Value == 0 {
writeError("除數不能為 0")
return
}
result = input.Input / input.Args.Value
case "mod":
if input.Args.Value == 0 {
writeError("除數不能為 0")
return
}
result = math.Mod(input.Input, input.Args.Value)
case "min":
result = math.Min(input.Input, input.Args.Value)
case "max":
result = math.Max(input.Input, input.Args.Value)
case "format":
decimals := input.Args.Decimals
if decimals < 0 {
decimals = 0
}
result = strconv.FormatFloat(input.Input, 'f', decimals, 64)
default:
writeError("不支援的 operation: " + input.Operation)
return
}
out, _ := json.Marshal(map[string]interface{}{
"success": true,
"data": map[string]interface{}{
"result": result,
"operation": input.Operation,
},
})
os.Stdout.Write(out)
}
func writeError(msg string) {
out, _ := json.Marshal(map[string]interface{}{"success": false, "error": msg})
os.Stdout.Write(out)
}
@@ -0,0 +1,64 @@
canonical_id: "set"
display_name: "設定變數"
category: "logic"
version: "v1"
wasi_target: "preview1"
stability: "floating"
runtime_compat:
- "cf-workers"
- "workerd"
- "wazero"
constraints:
max_size_kb: 2048
max_cold_start_ms: 50
no_network_syscall: true
no_filesystem_syscall: true
io_model: "stdin_stdout_json"
input_schema:
type: object
properties:
assignments:
type: array
description: 賦值清單,每筆含 key 與 value(與 values 擇一必填)
items:
type: object
required: [key, value]
properties:
key:
type: string
value: {}
values:
type: object
description: 鍵值對物件,與 assignments 擇一必填
context:
type: object
description: 上游傳入的上下文,設定結果會合併覆寫
output_schema:
type: object
properties:
success:
type: boolean
data:
type: object
description: context 加上所有設定後的變數
gherkin_tests:
- scenario: "用 assignments 設定變數"
given: '{"assignments":[{"key":"name","value":"Alice"}]}'
then_contains: '"name":"Alice"'
- scenario: "用 values 設定變數"
given: '{"values":{"name":"Bob","age":30}}'
then_contains: '"name":"Bob"'
- scenario: "未提供 assignments 或 values 時失敗"
given: '{"context":{"x":1}}'
then_contains: '{"success":false'
tags: [builtin, set, assign, variable, context]
description: "設定或覆寫變數,支援 assignments 陣列或 values 物件兩種格式,結果合併自 context。"
config_example: |
my_set: # 節點名稱(可自訂)
assignments: # 賦值清單(與 values 擇一必填)
- key: status
value: active
- key: count
value: 0
context: # 上游上下文,設定結果會合併覆寫(選填)
payload: "{{upstream.data}}"
+3
View File
@@ -0,0 +1,3 @@
module component
go 1.21
+59
View File
@@ -0,0 +1,59 @@
// set — 設定/覆寫變數,傳遞到下一個節點
// 支援 assignments 陣列或 values 物件兩種格式
package main
import (
"encoding/json"
"io"
"os"
)
type Input struct {
Assignments []Assignment `json:"assignments"`
Values map[string]interface{} `json:"values"`
Context map[string]interface{} `json:"context"`
}
type Assignment struct {
Key string `json:"key"`
Value interface{} `json:"value"`
}
func main() {
raw, err := io.ReadAll(os.Stdin)
if err != nil {
writeError("failed to read stdin: " + err.Error())
return
}
var input Input
if err := json.Unmarshal(raw, &input); err != nil {
writeError("invalid input JSON: " + err.Error())
return
}
result := make(map[string]interface{})
for k, v := range input.Context {
result[k] = v
}
if len(input.Assignments) > 0 {
for _, a := range input.Assignments {
result[a.Key] = a.Value
}
} else if len(input.Values) > 0 {
for k, v := range input.Values {
result[k] = v
}
} else {
writeError("需提供 assignments 陣列或 values 物件")
return
}
out, _ := json.Marshal(map[string]interface{}{"success": true, "data": result})
os.Stdout.Write(out)
}
func writeError(msg string) {
out, _ := json.Marshal(map[string]interface{}{"success": false, "error": msg})
os.Stdout.Write(out)
}
@@ -0,0 +1,58 @@
canonical_id: "string_ops"
display_name: "字串操作"
category: "logic"
version: "v1"
wasi_target: "preview1"
stability: "floating"
runtime_compat:
- "cf-workers"
- "workerd"
- "wazero"
constraints:
max_size_kb: 2048
max_cold_start_ms: 50
no_network_syscall: true
no_filesystem_syscall: true
io_model: "stdin_stdout_json"
input_schema:
type: object
required: [operation, input]
properties:
operation:
type: string
enum: [upper, lower, trim, capitalize, replace, split, join, includes, starts_with, ends_with, length, substring]
input:
type: string
args:
type: object
description: 操作參數(依 operation 而定)
output_schema:
type: object
properties:
success:
type: boolean
data:
type: object
properties:
result: {}
operation:
type: string
gherkin_tests:
- scenario: "upper 操作"
given: '{"operation":"upper","input":"hello"}'
then_contains: '"result":"HELLO"'
- scenario: "replace 操作"
given: '{"operation":"replace","input":"hello world","args":{"from":"world","to":"u6u"}}'
then_contains: '"result":"hello u6u"'
- scenario: "不支援的 operation"
given: '{"operation":"unknown","input":"test"}'
then_contains: '{"success":false'
tags: [builtin, data, string, transform, text]
description: "字串操作:upper/lower/trim/capitalize/replace/split/join/includes/starts_with/ends_with/length/substring。"
config_example: |
my_string_op: # 節點名稱(可自訂)
operation: "replace" # 運算類型(必填),可選值:upper/lower/trim/capitalize/replace/split/join/includes/starts_with/ends_with/length/substring
input: "hello world" # 輸入字串(必填)
args: # 操作參數,依 operation 而定(選填)
from: "world" # replace 用:要被取代的子字串
to: "arcrun" # replace 用:取代後的字串
+3
View File
@@ -0,0 +1,3 @@
module component
go 1.21
+116
View File
@@ -0,0 +1,116 @@
// string_ops — 字串操作
// 支援: upper, lower, trim, capitalize, replace, split, join, includes,
// starts_with, ends_with, length, substring
//
//go:build tinygo
package main
import (
"encoding/json"
"io"
"os"
"strings"
)
type Args struct {
From string `json:"from"`
To string `json:"to"`
Sep string `json:"sep"`
Items []string `json:"items"`
Substr string `json:"substr"`
Prefix string `json:"prefix"`
Suffix string `json:"suffix"`
Start int `json:"start"`
End int `json:"end"`
}
type Input struct {
Operation string `json:"operation"`
Input string `json:"input"`
Args Args `json:"args"`
}
func main() {
raw, err := io.ReadAll(os.Stdin)
if err != nil {
writeError("failed to read stdin: " + err.Error())
return
}
var input Input
if err := json.Unmarshal(raw, &input); err != nil {
writeError("invalid input JSON: " + err.Error())
return
}
if input.Operation == "" {
writeError("operation 必填")
return
}
var result interface{}
switch input.Operation {
case "upper":
result = strings.ToUpper(input.Input)
case "lower":
result = strings.ToLower(input.Input)
case "trim":
result = strings.TrimSpace(input.Input)
case "capitalize":
if len(input.Input) == 0 {
result = ""
} else {
result = strings.ToUpper(input.Input[:1]) + strings.ToLower(input.Input[1:])
}
case "replace":
result = strings.ReplaceAll(input.Input, input.Args.From, input.Args.To)
case "split":
sep := input.Args.Sep
if sep == "" {
sep = ","
}
result = strings.Split(input.Input, sep)
case "join":
sep := input.Args.Sep
result = strings.Join(input.Args.Items, sep)
case "includes":
result = strings.Contains(input.Input, input.Args.Substr)
case "starts_with":
result = strings.HasPrefix(input.Input, input.Args.Prefix)
case "ends_with":
result = strings.HasSuffix(input.Input, input.Args.Suffix)
case "length":
result = len([]rune(input.Input))
case "substring":
runes := []rune(input.Input)
start := input.Args.Start
end := input.Args.End
if start < 0 {
start = 0
}
if end <= 0 || end > len(runes) {
end = len(runes)
}
if start > end {
start = end
}
result = string(runes[start:end])
default:
writeError("不支援的 operation: " + input.Operation)
return
}
out, _ := json.Marshal(map[string]interface{}{
"success": true,
"data": map[string]interface{}{
"result": result,
"operation": input.Operation,
},
})
os.Stdout.Write(out)
}
func writeError(msg string) {
out, _ := json.Marshal(map[string]interface{}{"success": false, "error": msg})
os.Stdout.Write(out)
}
@@ -0,0 +1,66 @@
canonical_id: "switch"
display_name: "條件路由"
category: "logic"
version: "v1"
wasi_target: "preview1"
stability: "floating"
runtime_compat:
- "cf-workers"
- "workerd"
- "wazero"
constraints:
max_size_kb: 2048
max_cold_start_ms: 50
no_network_syscall: true
no_filesystem_syscall: true
io_model: "stdin_stdout_json"
input_schema:
type: object
required: [value, cases]
properties:
value:
type: string
description: 要比對的值
cases:
type: array
items:
type: object
properties:
match:
type: string
branch:
type: string
default_branch:
type: string
description: 無匹配時的預設分支
output_schema:
type: object
properties:
success:
type: boolean
data:
type: object
properties:
branch:
type: string
gherkin_tests:
- scenario: "匹配到 case"
given: '{"value":"a","cases":[{"match":"a","branch":"branch_a"}],"default_branch":"default"}'
then_contains: '"branch":"branch_a"'
- scenario: "走 default 分支"
given: '{"value":"z","cases":[{"match":"a","branch":"branch_a"}],"default_branch":"fallback"}'
then_contains: '"branch":"fallback"'
- scenario: "無效 JSON"
given: 'not-json'
then_contains: '{"success":false'
tags: [builtin, switch, branch, route, condition]
description: "依值路由到對應分支,支援多個 case 和 default 分支。"
config_example: |
my_switch: # 節點名稱(可自訂)
value: "{{upstream.status}}" # 要比對的值(必填)
cases: # case 清單(必填)
- match: active # 比對值
branch: branch_active # 對應分支名稱
- match: inactive
branch: branch_inactive
default_branch: branch_default # 無匹配時的預設分支(選填)
+3
View File
@@ -0,0 +1,3 @@
module component
go 1.21
+61
View File
@@ -0,0 +1,61 @@
// switch — 依值路由到對應分支
//
//go:build tinygo
package main
import (
"encoding/json"
"io"
"os"
)
type Case struct {
Match string `json:"match"`
Branch string `json:"branch"`
}
type Input struct {
Value string `json:"value"`
Cases []Case `json:"cases"`
DefaultBranch string `json:"default_branch"`
}
func main() {
raw, err := io.ReadAll(os.Stdin)
if err != nil {
writeError("failed to read stdin: " + err.Error())
return
}
var input Input
if err := json.Unmarshal(raw, &input); err != nil {
writeError("invalid input JSON: " + err.Error())
return
}
for _, c := range input.Cases {
if c.Match == input.Value {
writeSuccess(c.Branch)
return
}
}
branch := input.DefaultBranch
if branch == "" {
branch = "default"
}
writeSuccess(branch)
}
func writeSuccess(branch string) {
out, _ := json.Marshal(map[string]interface{}{
"success": true,
"data": map[string]interface{}{"branch": branch},
})
os.Stdout.Write(out)
}
func writeError(msg string) {
out, _ := json.Marshal(map[string]interface{}{"success": false, "error": msg})
os.Stdout.Write(out)
}
@@ -0,0 +1,56 @@
canonical_id: "telegram"
display_name: "Telegram Bot"
category: "api"
version: "v1"
wasi_target: "preview1"
stability: "floating"
runtime_compat:
- "cf-workers"
- "workerd"
- "wazero"
constraints:
max_size_kb: 2048
max_cold_start_ms: 50
no_network_syscall: true
no_filesystem_syscall: true
io_model: "stdin_stdout_json"
input_schema:
type: object
required: [chat_id, text, bot_token]
properties:
chat_id:
type: string
text:
type: string
bot_token:
type: string
description: Telegram Bot Token
output_schema:
type: object
properties:
success:
type: boolean
data:
type: object
properties:
ok:
type: boolean
gherkin_tests:
- scenario: "缺少 bot_token"
given: '{"chat_id":"123","text":"hello"}'
then_contains: '{"success":false'
- scenario: "缺少 chat_id"
given: '{"text":"hello","bot_token":"token"}'
then_contains: '{"success":false'
tags: [integration, telegram, bot, message]
description: "透過 Telegram Bot 發送訊息。透過 host function 呼叫 Telegram Bot API,需要 bot_token。"
credentials_required:
- key: telegram_bot_token
type: telegram_bot_token
description: "Telegram Bot Token(由 @BotFather 取得)"
inject_as: bot_token
config_example: |
send_message: # 節點名稱(可自訂)
chat_id: "" # Telegram Chat ID(必填)
text: "" # 訊息內文(必填)
# bot_token 由 credentials.yaml 的 telegram_bot_token 自動注入
+3
View File
@@ -0,0 +1,3 @@
module component
go 1.21
+103
View File
@@ -0,0 +1,103 @@
// telegram — 透過 Telegram Bot 發送訊息
// 透過 host function 呼叫 Telegram Bot API
//
//go:build tinygo
package main
import (
"encoding/json"
"io"
"os"
"unsafe"
)
//go:wasmimport u6u http_request
func hostHttpRequest(
urlPtr uintptr, urlLen uint32,
methodPtr uintptr, methodLen uint32,
headersPtr uintptr, headersLen uint32,
bodyPtr uintptr, bodyLen uint32,
outPtr uintptr, outLenPtr uintptr,
) uint32
type Input struct {
ChatID string `json:"chat_id"`
Text string `json:"text"`
BotToken string `json:"bot_token"`
}
func main() {
raw, err := io.ReadAll(os.Stdin)
if err != nil {
writeError("failed to read stdin: " + err.Error())
return
}
var input Input
if err := json.Unmarshal(raw, &input); err != nil {
writeError("invalid input JSON: " + err.Error())
return
}
if input.ChatID == "" {
writeError("chat_id 必填")
return
}
if input.Text == "" {
writeError("text 必填")
return
}
if input.BotToken == "" {
writeError("bot_token 必填")
return
}
apiURL := "https://api.telegram.org/bot" + input.BotToken + "/sendMessage"
method := "POST"
headers := map[string]string{
"Content-Type": "application/json",
}
headersJSON, _ := json.Marshal(headers)
bodyData, _ := json.Marshal(map[string]string{
"chat_id": input.ChatID,
"text": input.Text,
})
urlBytes := []byte(apiURL)
methodBytes := []byte(method)
bodyBytes := bodyData
outBuf := make([]byte, 4096)
var outLen uint32
result := hostHttpRequest(
uintptr(unsafe.Pointer(&urlBytes[0])), uint32(len(urlBytes)),
uintptr(unsafe.Pointer(&methodBytes[0])), uint32(len(methodBytes)),
uintptr(unsafe.Pointer(&headersJSON[0])), uint32(len(headersJSON)),
uintptr(unsafe.Pointer(&bodyBytes[0])), uint32(len(bodyBytes)),
uintptr(unsafe.Pointer(&outBuf[0])), uintptr(unsafe.Pointer(&outLen)),
)
if result != 0 {
writeError("Telegram API 呼叫失敗")
return
}
responseStr := string(outBuf[:outLen])
var responseData map[string]interface{}
if err := json.Unmarshal([]byte(responseStr), &responseData); err != nil {
responseData = map[string]interface{}{"raw": responseStr}
}
ok, _ := responseData["ok"].(bool)
out, _ := json.Marshal(map[string]interface{}{
"success": true,
"data": map[string]interface{}{"ok": ok},
})
os.Stdout.Write(out)
}
func writeError(msg string) {
out, _ := json.Marshal(map[string]interface{}{"success": false, "error": msg})
os.Stdout.Write(out)
}
@@ -0,0 +1,53 @@
canonical_id: "try_catch"
display_name: "錯誤處理"
category: "logic"
version: "v1"
wasi_target: "preview1"
stability: "floating"
runtime_compat:
- "cf-workers"
- "workerd"
- "wazero"
constraints:
max_size_kb: 2048
max_cold_start_ms: 50
no_network_syscall: true
no_filesystem_syscall: true
io_model: "stdin_stdout_json"
input_schema:
type: object
properties:
result: {}
error:
type: string
description: 上游錯誤訊息,非空則走 catch 分支
output_schema:
type: object
properties:
success:
type: boolean
data:
type: object
properties:
branch:
type: string
enum: [try, catch]
result: {}
error:
type: string
gherkin_tests:
- scenario: "無錯誤走 try"
given: '{"result":{"value":42},"error":""}'
then_contains: '"branch":"try"'
- scenario: "有錯誤走 catch"
given: '{"result":null,"error":"something went wrong"}'
then_contains: '"branch":"catch"'
- scenario: "無效 JSON"
given: 'not-json'
then_contains: '{"success":false'
tags: [builtin, control, try, catch, error, handling]
description: "判斷上游結果是否有 error,決定走 try 或 catch 分支。"
config_example: |
my_try_catch: # 節點名稱(可自訂)
result: "{{upstream.data}}" # 上游回傳的結果,任意型別(選填)
error: "{{upstream.error}}" # 上游錯誤訊息,非空則走 catch 分支(選填)
+3
View File
@@ -0,0 +1,3 @@
module component
go 1.21
+55
View File
@@ -0,0 +1,55 @@
// try_catch — 判斷上游結果是否有 error,決定走 try 或 catch 分支
//
//go:build tinygo
package main
import (
"encoding/json"
"io"
"os"
)
type Input struct {
Result json.RawMessage `json:"result"`
Error string `json:"error"`
}
func main() {
raw, err := io.ReadAll(os.Stdin)
if err != nil {
writeError("failed to read stdin: " + err.Error())
return
}
var input Input
if err := json.Unmarshal(raw, &input); err != nil {
writeError("invalid input JSON: " + err.Error())
return
}
if input.Error != "" {
out, _ := json.Marshal(map[string]interface{}{
"success": true,
"data": map[string]interface{}{
"branch": "catch",
"error": input.Error,
},
})
os.Stdout.Write(out)
return
}
out, _ := json.Marshal(map[string]interface{}{
"success": true,
"data": map[string]interface{}{
"branch": "try",
"result": input.Result,
},
})
os.Stdout.Write(out)
}
func writeError(msg string) {
out, _ := json.Marshal(map[string]interface{}{"success": false, "error": msg})
os.Stdout.Write(out)
}
@@ -0,0 +1,42 @@
# validate_json
u6u 第一個 WASM 零件。驗證輸入字串是否為合法 JSON 格式。
## 編譯
需要安裝 [TinyGo](https://tinygo.org/getting-started/install/)
```bash
# macOS
brew install tinygo
# 編譯為 WASM
tinygo build -o validate_json.wasm -target=wasi .
```
## 本地測試
```bash
# 合法 JSON → {"valid":true}
echo '{"json_string":"{\"key\":\"value\"}"}' | wasmtime validate_json.wasm
# 非法 JSON → {"valid":false,"error":"..."}
echo '{"json_string":"not-json"}' | wasmtime validate_json.wasm
# 空字串 → {"valid":false,"error":"json_string is required"}
echo '{"json_string":""}' | wasmtime validate_json.wasm
```
## 提交至 Component Registry
```bash
# 驗證合約格式
curl -X POST https://component-registry.finally.click/components/validate-contract \
-H "Content-Type: application/json" \
-d @component.contract.yaml
# 提交零件(multipart
curl -X POST https://component-registry.finally.click/components \
-F "contract=@component.contract.yaml;type=application/yaml" \
-F "wasm=@validate_json.wasm;type=application/wasm"
```
@@ -0,0 +1,71 @@
canonical_id: "validate_json"
display_name: "JSON 格式驗證器"
category: "logic"
version: "v1"
wasi_target: "preview1"
stability: "floating"
runtime_compat:
- "cf-workers"
- "workerd"
- "wazero"
constraints:
max_size_kb: 2048
max_cold_start_ms: 50
no_network_syscall: true
no_filesystem_syscall: true
io_model: "stdin_stdout_json"
input_schema:
type: object
required:
- json_string
properties:
json_string:
type: string
description: "待驗證的 JSON 字串"
output_schema:
type: object
required:
- valid
properties:
valid:
type: boolean
description: "是否為合法 JSON"
error:
type: string
description: "驗證失敗時的錯誤訊息(valid=false 時存在)"
gherkin_tests:
- scenario: "合法 JSON 物件通過驗證"
given: '{"json_string":"{\"key\":\"value\"}"}'
then_contains: '{"valid":true}'
- scenario: "合法 JSON 陣列通過驗證"
given: '{"json_string":"[1,2,3]"}'
then_contains: '{"valid":true}'
- scenario: "非法 JSON 字串回傳錯誤"
given: '{"json_string":"not-json"}'
then_contains: '{"valid":false,"error":'
- scenario: "空字串回傳錯誤"
given: '{"json_string":""}'
then_contains: '{"valid":false,"error":"json_string is required"}'
- scenario: "缺少 json_string 欄位回傳錯誤"
given: '{}'
then_contains: '{"valid":false,"error":"json_string is required"}'
tags:
- "validation"
- "json"
- "utility"
- "logic"
description: "驗證輸入字串是否為合法 JSON 格式。輸入 json_string 欄位,回傳 valid(布林值)與 error(失敗時的錯誤訊息)。"
config_example: |
my_validate_json: # 節點名稱(可自訂)
json_string: '{"key":"value"}' # 待驗證的 JSON 字串(必填)
+3
View File
@@ -0,0 +1,3 @@
module validate_json
go 1.21
+71
View File
@@ -0,0 +1,71 @@
// validate_json — u6u 第一個 WASM 零件
// 驗證輸入字串是否為合法 JSON 格式
//
// 白名單 importTinyGo 規範):
// - "os" 只用 os.Stdin / os.Stdout
// - "io" io.ReadAll(os.Stdin)
// - "encoding/json" json.Unmarshal / json.Marshal
//
// 禁止:goroutine、channel、net/*、os.Open、syscall.*、第三方 module
//
// 編譯指令:
// tinygo build -o validate_json.wasm -target=wasi .
//
// 本地測試:
// echo '{"json_string":"{\"key\":\"value\"}"}' | wasmtime validate_json.wasm
// echo '{"json_string":"not-json"}' | wasmtime validate_json.wasm
package main
import (
"encoding/json"
"io"
"os"
)
// Input 對應 input_schema
type Input struct {
JSONString string `json:"json_string"`
}
// Output 對應 output_schema
type Output struct {
Valid bool `json:"valid"`
Error string `json:"error,omitempty"`
}
func main() {
// 1. 讀取 stdin
raw, err := io.ReadAll(os.Stdin)
if err != nil {
writeOutput(Output{Valid: false, Error: "failed to read stdin: " + err.Error()})
return
}
// 2. 解析 input JSON
var input Input
if err := json.Unmarshal(raw, &input); err != nil {
writeOutput(Output{Valid: false, Error: "invalid input JSON: " + err.Error()})
return
}
// 3. 驗證 json_string 欄位
if input.JSONString == "" {
writeOutput(Output{Valid: false, Error: "json_string is required"})
return
}
// 4. 嘗試解析 json_string
var target interface{}
if err := json.Unmarshal([]byte(input.JSONString), &target); err != nil {
writeOutput(Output{Valid: false, Error: err.Error()})
return
}
writeOutput(Output{Valid: true})
}
func writeOutput(out Output) {
data, _ := json.Marshal(out)
os.Stdout.Write(data)
}
@@ -0,0 +1,54 @@
canonical_id: "wait"
display_name: "等待延遲"
category: "logic"
version: "v1"
wasi_target: "preview1"
stability: "floating"
runtime_compat:
- "cf-workers"
- "workerd"
- "wazero"
constraints:
max_size_kb: 2048
max_cold_start_ms: 50
no_network_syscall: true
no_filesystem_syscall: true
io_model: "stdin_stdout_json"
input_schema:
type: object
required: [ms]
properties:
ms:
type: integer
description: 等待毫秒數,最大 30000(30 秒)
context:
type: object
description: 透傳到下一個節點的上下文資料
output_schema:
type: object
properties:
success:
type: boolean
data:
type: object
description: 透傳的 context 加上 waited_ms 欄位
properties:
waited_ms:
type: integer
gherkin_tests:
- scenario: "等待 100ms"
given: '{"ms":100}'
then_contains: '"waited_ms":100'
- scenario: "超過上限自動截斷為 30000ms"
given: '{"ms":99999}'
then_contains: '"waited_ms":30000'
- scenario: "ms 為 0 時失敗"
given: '{"ms":0}'
then_contains: '{"success":false'
tags: [builtin, wait, delay, sleep, timing]
description: "等待指定毫秒數後繼續,最長 30 秒,並透傳 context 資料。"
config_example: |
my_wait: # 節點名稱(可自訂)
ms: 1000 # 等待毫秒數,最大 30000(必填)
context: # 透傳到下一個節點的資料(選填)
payload: "{{upstream.data}}"
+3
View File
@@ -0,0 +1,3 @@
module component
go 1.21
+52
View File
@@ -0,0 +1,52 @@
// wait — 等待指定毫秒數後繼續(最多 30 秒)
// 注意:TinyGo/WASM 環境中 time.Sleep 可能不可用,改用 busy-wait 模擬
package main
import (
"encoding/json"
"io"
"os"
"time"
)
type Input struct {
Ms int `json:"ms"`
Context map[string]interface{} `json:"context"`
}
func main() {
raw, err := io.ReadAll(os.Stdin)
if err != nil {
writeError("failed to read stdin: " + err.Error())
return
}
var input Input
if err := json.Unmarshal(raw, &input); err != nil {
writeError("invalid input JSON: " + err.Error())
return
}
if input.Ms <= 0 {
writeError("ms 必須大於 0")
return
}
ms := input.Ms
if ms > 30000 {
ms = 30000
}
time.Sleep(time.Duration(ms) * time.Millisecond)
result := make(map[string]interface{})
for k, v := range input.Context {
result[k] = v
}
result["waited_ms"] = ms
out, _ := json.Marshal(map[string]interface{}{"success": true, "data": result})
os.Stdout.Write(out)
}
func writeError(msg string) {
out, _ := json.Marshal(map[string]interface{}{"success": false, "error": msg})
os.Stdout.Write(out)
}