Files
Arcrun/registry/components/kbdb_get/main.go
T
Leo 497f92a268 feat(arcrun): recipe system + resumable workflow + component registry canon
Three new platform capabilities + one component (kbdb_get) to enable
real AI workflow execution through cypher binding YAML.

## Recipe System (容器 + Recipe 模式)
SDD: .agents/specs/recipe-system/

- prompt_recipe schema (Zod): fragments + inputs + assembly + output
- recipe-expander.ts: expand recipe ref → real prompt by fetching KBDB blocks
  + pulling context fields with transforms (pluck_content / extract_field / etc)
- 7 transform whitelist: json_array / to_string / join / markdown_list /
  extract_field / first / pluck_content
- graph-executor hooks: detect node.data.recipe → expand → inject into ctx
- output JSON parser (with markdown fence stripping for Claude-wrapped JSON)
- Stored in RECIPES KV under prompt_recipe:{name}

## Resumable Workflow (webhook callback resume)
SDD: .agents/specs/resumable-workflow/

- WorkflowPaused class + paused-runs.ts (persist/load/consume in EXEC_CONTEXT KV, 24h TTL)
- graph-executor: detect {pending:true, task_id} → persist state → throw WorkflowPaused
- cypher-handlers: catch → return {success:true, paused:true, task_id, run_id}
- POST /workflows/resume route: consume KV state → resumeFromPaused()
- Auto-inject callback_url for claude_api nodes (PUBLIC_BASE_URL or default cypher.arcrun.dev)
- claude_api/main.go: forward callback_url to Mira daemon, default timeout 25s→120s
- Idempotent (consume = load+delete)

## Component Registry Canon
SDD: .agents/specs/component-registry-canon/

- Add POST /components/index-only endpoint (metadata-only, no wasm/sandbox)
- Backfill script (mjs): scan registry/components/*/contract.yaml → submit to KV
- register-component.sh: SSOT for local + CI hook (deploy.yml change in next commit)
- Drop R2 dead storage from submitComponent + types + wrangler
- Schema relaxed: category enum + auth/ai/platform; cold_start 50→500ms; size 2→8MB

## kbdb_get component
- registry/components/kbdb_get/: TinyGo WASM, two modes (block_id / page_name list)
- .component-builds/kbdb_get/: WASI shim worker (kbdb-get.arcrun.dev)

End-to-end validation: AI uses MCP execute_workflow with recipe ref →
cypher-executor expands prompt from KBDB schema/skill blocks + drafts →
claude_api calls Mira daemon → daemon callback fires resume route →
workflow continues. Verified with real 2KB+ Karpathy LLM Wiki draft.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 15:52:19 +08:00

185 lines
4.6 KiB
Go
Raw 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.
// kbdb_get — 從 KBDB 讀 blockGET /blocks?page_name=... 或 GET /blocks/:id
// thin wrapper:透過 host function http_request 呼叫 KBDB API
//
//go:build tinygo
package main
import (
"encoding/json"
"io"
"os"
"strconv"
"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 {
KBDBUrl string `json:"kbdb_url"` // optional
APIKey string `json:"api_key"` // 必填
BlockID string `json:"block_id"` // 與 page_name 二擇一
PageName string `json:"page_name"` // 與 block_id 二擇一
Limit int `json:"limit"` // optional, default 50
}
var dummy [1]byte
func safePtr(b []byte) (uintptr, uint32) {
if len(b) == 0 {
return uintptr(unsafe.Pointer(&dummy[0])), 0
}
return uintptr(unsafe.Pointer(&b[0])), uint32(len(b))
}
func writeError(msg string) {
out, _ := json.Marshal(map[string]interface{}{"success": false, "error": msg})
os.Stdout.Write(out)
}
// urlEncode:簡易 query string encoder(只處理 KBDB 會用到的字元)
// 避免引入 net/url(白名單外)
func urlEncode(s string) string {
var out []byte
for i := 0; i < len(s); i++ {
c := s[i]
if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') ||
c == '-' || c == '_' || c == '.' || c == '~' {
out = append(out, c)
} else {
const hex = "0123456789ABCDEF"
out = append(out, '%', hex[c>>4], hex[c&0x0f])
}
}
return string(out)
}
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.APIKey == "" {
writeError("api_key 必填")
return
}
if input.BlockID == "" && input.PageName == "" {
writeError("block_id 或 page_name 必填其一")
return
}
kbdbURL := input.KBDBUrl
if kbdbURL == "" {
kbdbURL = "https://kbdb.finally.click"
}
limit := input.Limit
if limit <= 0 {
limit = 50
}
// 構造 URLblock_id 模式走 /blocks/:id(單一),page_name 模式走 /blocks?page_name=...&limit=N
var url string
if input.BlockID != "" {
url = kbdbURL + "/blocks/" + urlEncode(input.BlockID)
} else {
url = kbdbURL + "/blocks?page_name=" + urlEncode(input.PageName) +
"&limit=" + strconv.Itoa(limit)
}
headers := map[string]string{
"Authorization": "Bearer " + input.APIKey,
}
headersBytes, _ := json.Marshal(headers)
method := "GET"
urlBytes := []byte(url)
methodBytes := []byte(method)
outBuf := make([]byte, 1<<20) // 1MBlist 可能很大)
var outLen uint32
urlPtr, urlLen := safePtr(urlBytes)
methodPtr, methodLen := safePtr(methodBytes)
headersPtr, headersLenU := safePtr(headersBytes)
bodyPtr, bodyLenU := safePtr(nil)
result := hostHttpRequest(
urlPtr, urlLen,
methodPtr, methodLen,
headersPtr, headersLenU,
bodyPtr, bodyLenU,
uintptr(unsafe.Pointer(&outBuf[0])), uintptr(unsafe.Pointer(&outLen)),
)
if result != 0 {
writeError("KBDB GET request failed (host_http_request returned non-zero)")
return
}
respStr := string(outBuf[:outLen])
// 解析回應
if input.BlockID != "" {
// 單一 blockKBDB 直接回 block 物件,包成 array 給下游 foreach
var block map[string]interface{}
if err := json.Unmarshal([]byte(respStr), &block); err != nil {
writeError("KBDB returned non-JSON: " + respStr)
return
}
if _, hasErr := block["error"]; hasErr {
out, _ := json.Marshal(map[string]interface{}{
"success": false, "error": block["error"],
})
os.Stdout.Write(out)
return
}
out, _ := json.Marshal(map[string]interface{}{
"success": true,
"blocks": []map[string]interface{}{block},
"count": 1,
})
os.Stdout.Write(out)
return
}
// page_name 列表模式:KBDB 回 {"blocks": [...], "count": N}
var listResp struct {
Blocks []map[string]interface{} `json:"blocks"`
Count int `json:"count"`
Error interface{} `json:"error"`
}
if err := json.Unmarshal([]byte(respStr), &listResp); err != nil {
writeError("KBDB returned non-JSON: " + respStr)
return
}
if listResp.Error != nil {
out, _ := json.Marshal(map[string]interface{}{
"success": false, "error": listResp.Error,
})
os.Stdout.Write(out)
return
}
out, _ := json.Marshal(map[string]interface{}{
"success": true,
"blocks": listResp.Blocks,
"count": listResp.Count,
})
os.Stdout.Write(out)
}