// 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) // 對應 SDD:polaris/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 1:lookup 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) }