Files
uncle6me-web 922a57fe34 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>
2026-06-03 15:52:38 +08:00

281 lines
8.0 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// kbdb_upsert_block — 用 page_name 當 idempotency key 做 upsert
// 內部:GET /blocks?page_name=X → user_id filter → 找到 PATCH /blocks/:id 沒找到 POST /blocks
// 解 arcrun workflow 沒 IF/branch 能力的缺口(arcrun.md P1 #1
// 對應 SDDpolaris/mira/.agents/specs/mira-app/design.md §3.5.12.4.1
//
//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"` // 必填
PageName string `json:"page_name"` // 必填,當 idempotency key
Content string `json:"content"` // 必填
Type string `json:"type"` // optional(建立時用,PATCH 時忽略)
ParentID string `json:"parent_id"` // optional(建立時用,PATCH 時忽略)
UserID string `json:"user_id"` // optional(建立時用 + lookup filter
Source string `json:"source"` // optional
TagsJSON string `json:"tags_json"` // optional(完整覆寫)
CreateOnly bool `json:"create_only"` // 2026-05-17 加:若 true + 已存在 → 不 PATCH,回 action="exists"
// 用於 stub creation 場景(避免 stub 覆寫已存在的 full wiki
}
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)
}
func writeResult(action string, data map[string]interface{}) {
out, _ := json.Marshal(map[string]interface{}{
"success": true,
"action": action,
"data": data,
})
os.Stdout.Write(out)
}
// urlEncode:跟 kbdb_get 一致,避免引入 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 httpCall(method, url string, headers map[string]string, body []byte) ([]byte, uint32) {
headersBytes, _ := json.Marshal(headers)
urlBytes := []byte(url)
methodBytes := []byte(method)
outBuf := make([]byte, 1<<20) // 1MB
var outLen uint32
urlPtr, urlLen := safePtr(urlBytes)
methodPtr, methodLen := safePtr(methodBytes)
headersPtr, headersLenU := safePtr(headersBytes)
bodyPtr, bodyLenU := safePtr(body)
result := hostHttpRequest(
urlPtr, urlLen,
methodPtr, methodLen,
headersPtr, headersLenU,
bodyPtr, bodyLenU,
uintptr(unsafe.Pointer(&outBuf[0])), uintptr(unsafe.Pointer(&outLen)),
)
return outBuf[:outLen], result
}
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.PageName == "" {
writeError("page_name 必填(upsert 的 idempotency key")
return
}
if input.Content == "" {
writeError("content 必填")
return
}
kbdbURL := input.KBDBUrl
if kbdbURL == "" {
kbdbURL = "https://kbdb.finally.click"
}
headers := map[string]string{
"Authorization": "Bearer " + input.APIKey,
}
// ── Step 1lookup by page_name ────────────────────────────────────
lookupURL := kbdbURL + "/blocks?page_name=" + urlEncode(input.PageName) +
"&limit=" + strconv.Itoa(10)
lookupResp, callResult := httpCall("GET", lookupURL, headers, nil)
if callResult != 0 {
writeError("KBDB lookup failed (host_http_request returned non-zero)")
return
}
var lookupParsed struct {
Blocks []map[string]interface{} `json:"blocks"`
Count int `json:"count"`
Error interface{} `json:"error"`
}
if err := json.Unmarshal(lookupResp, &lookupParsed); err != nil {
writeError("KBDB lookup returned non-JSON: " + string(lookupResp))
return
}
if lookupParsed.Error != nil {
errBytes, _ := json.Marshal(map[string]interface{}{
"success": false,
"error": lookupParsed.Error,
"phase": "lookup",
})
os.Stdout.Write(errBytes)
return
}
// ── Step 2:找符合 user_id 的第一筆 ──────────────────────────────
var existing map[string]interface{}
for _, b := range lookupParsed.Blocks {
if input.UserID == "" {
existing = b
break
}
if uid, ok := b["user_id"].(string); ok && uid == input.UserID {
existing = b
break
}
}
// ── Step 3:分支寫入 ───────────────────────────────────────────────
postHeaders := map[string]string{
"Content-Type": "application/json",
"Authorization": "Bearer " + input.APIKey,
}
if existing != nil {
// CreateOnly 模式:已存在 → 不動,回 action="exists"(給 stub creation 用,
// 避免後續 raw 提到同 entity 時把完整 wiki 覆寫成 stub
if input.CreateOnly {
writeResult("exists", existing)
return
}
// PATCH 路徑
existingID, _ := existing["id"].(string)
if existingID == "" {
writeError("lookup 找到 block 但 id 為空")
return
}
patchBody := make(map[string]interface{})
patchBody["content"] = input.Content
if input.Source != "" {
patchBody["source"] = input.Source
}
if input.TagsJSON != "" {
// PATCH endpoint 用 tags array 不是 tags_json string
var tagsArr []string
if err := json.Unmarshal([]byte(input.TagsJSON), &tagsArr); err == nil {
patchBody["tags"] = tagsArr
}
}
patchBodyBytes, _ := json.Marshal(patchBody)
patchURL := kbdbURL + "/blocks/" + existingID
patchResp, callResult := httpCall("PATCH", patchURL, postHeaders, patchBodyBytes)
if callResult != 0 {
writeError("KBDB PATCH failed (host_http_request returned non-zero)")
return
}
var patchParsed map[string]interface{}
if err := json.Unmarshal(patchResp, &patchParsed); err != nil {
writeError("KBDB PATCH returned non-JSON: " + string(patchResp))
return
}
if _, hasErr := patchParsed["error"]; hasErr {
errBytes, _ := json.Marshal(map[string]interface{}{
"success": false,
"error": patchParsed["error"],
"phase": "patch",
})
os.Stdout.Write(errBytes)
return
}
writeResult("patched", patchParsed)
return
}
// CREATE 路徑
postBody := make(map[string]interface{})
postBody["content"] = input.Content
postBody["page_name"] = input.PageName
if input.Type != "" {
postBody["type"] = input.Type
}
if input.ParentID != "" {
postBody["parent_id"] = input.ParentID
}
if input.UserID != "" {
postBody["user_id"] = input.UserID
}
if input.Source != "" {
postBody["source"] = input.Source
}
if input.TagsJSON != "" {
postBody["tags_json"] = input.TagsJSON
}
postBodyBytes, _ := json.Marshal(postBody)
postURL := kbdbURL + "/blocks"
postResp, callResult := httpCall("POST", postURL, postHeaders, postBodyBytes)
if callResult != 0 {
writeError("KBDB POST failed (host_http_request returned non-zero)")
return
}
var postParsed map[string]interface{}
if err := json.Unmarshal(postResp, &postParsed); err != nil {
writeError("KBDB POST returned non-JSON: " + string(postResp))
return
}
if _, hasErr := postParsed["error"]; hasErr {
errBytes, _ := json.Marshal(map[string]interface{}{
"success": false,
"error": postParsed["error"],
"phase": "create",
})
os.Stdout.Write(errBytes)
return
}
writeResult("created", postParsed)
}