arcrun — AI workflow execution engine (clean history)
Self-hosted 開源:WASM 零件 + recipe + cypher-executor,跑在你自己的 Cloudflare。 此為重建的乾淨歷史起點(移除曾誤 commit 的 GCP SA 金鑰,舊歷史保留在 richblack/arcrun 與本地 backup 分支)。含: - acr init --self-hosted installer(建 KV/R2 + codeload 拉預編譯 wasm + wrangler deploy + seed recipe) - recipe push 把關(資料外流提醒 + 打通檢查) - 19 個正當零件預編譯 wasm(claude_api/km_writer/kbdb_upsert_block 排除:違反 DECISIONS §1) - CLI / cypher-executor / registry / 完整 SDD Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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 # 比較值(必填)
|
||||
@@ -0,0 +1,3 @@
|
||||
module component
|
||||
|
||||
go 1.21
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user