497f92a268
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>
185 lines
4.6 KiB
Go
185 lines
4.6 KiB
Go
// kbdb_get — 從 KBDB 讀 block(GET /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
|
||
}
|
||
|
||
// 構造 URL:block_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) // 1MB(list 可能很大)
|
||
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 != "" {
|
||
// 單一 block:KBDB 直接回 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)
|
||
}
|