bc6360ccfc
http_request 零件擴展(registry/components/http_request):
- 加 body_json 物件欄位(內部 JSON.stringify),yaml 端不用手組 JSON 字串
- 新增 JSON 回應的 error 欄位偵測 → 若 body 含 `{"error":"..."}` 則零件回 success=false
解 cascade bug:mira_feed_watcher 用 http_request trigger wiki_synthesis,
原本 4xx response 也被當 success,ON_SUCCESS 鏈會誤觸發
根因架構債:host fn 沒回 HTTP status code(arcrun.md 列為 P1 follow-up)
landing 河道 feed (landing/app/mira/feed/page.tsx):
- 加回 triggerWikiSynthesis fire-and-forget 對 cypher.arcrun.dev/webhooks/named/
wiki_synthesis/trigger 公開觸發 endpoint(arcrun-native,非 mira-specific route)
- 不走 watcher 是因為 cypher-executor 自己 fetch 自己 workers.dev URL = CF 1042
self-fetch 擋
watcher 仍存在當 cron backup,但目前因 self-fetch 1042 不會真正觸發下游
wiki_synthesis(arcrun.md 列為 P1 follow-up)。
139 lines
3.6 KiB
Go
139 lines
3.6 KiB
Go
// http_request — 發送任意 HTTP 請求,回傳 status + body
|
||
// 透過 host function 發出 HTTP,.wasm 本身不含網路 syscall
|
||
//
|
||
//go:build tinygo
|
||
|
||
package main
|
||
|
||
import (
|
||
"encoding/json"
|
||
"io"
|
||
"os"
|
||
"unsafe"
|
||
)
|
||
|
||
// host function 宣告(由 WASI shim 注入)
|
||
//
|
||
//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 {
|
||
URL string `json:"url"`
|
||
Method string `json:"method"`
|
||
Headers map[string]string `json:"headers"`
|
||
Body string `json:"body"` // 模式 A:直接 string body
|
||
BodyJSON map[string]interface{} `json:"body_json"` // 模式 B:物件,內部 stringify(避免 yaml 端要自己組 JSON 字串)
|
||
}
|
||
|
||
// dummy byte for safe zero-length unsafe.Pointer operations
|
||
var dummy [1]byte
|
||
|
||
// safePtr returns a valid pointer for an empty-or-nonempty byte slice.
|
||
// TinyGo panics with "index out of range" when taking &b[0] on empty b.
|
||
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 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.URL == "" {
|
||
writeError("url 必填")
|
||
return
|
||
}
|
||
|
||
method := input.Method
|
||
if method == "" {
|
||
method = "GET"
|
||
}
|
||
|
||
headersJSON := "{}"
|
||
if len(input.Headers) > 0 {
|
||
b, _ := json.Marshal(input.Headers)
|
||
headersJSON = string(b)
|
||
}
|
||
|
||
// body 來源優先順序:body_json(物件 → JSON 字串)> body(直接 string)
|
||
bodyStr := input.Body
|
||
if input.BodyJSON != nil {
|
||
b, err := json.Marshal(input.BodyJSON)
|
||
if err == nil {
|
||
bodyStr = string(b)
|
||
}
|
||
}
|
||
|
||
urlBytes := []byte(input.URL)
|
||
methodBytes := []byte(method)
|
||
headersBytes := []byte(headersJSON)
|
||
bodyBytes := []byte(bodyStr)
|
||
outBuf := make([]byte, 65536) // 64KB output buffer
|
||
var outLen uint32
|
||
|
||
urlPtr, urlLen := safePtr(urlBytes)
|
||
methodPtr, methodLen := safePtr(methodBytes)
|
||
headersPtr, headersLen := safePtr(headersBytes)
|
||
bodyPtr, bodyLen := safePtr(bodyBytes)
|
||
|
||
result := hostHttpRequest(
|
||
urlPtr, urlLen,
|
||
methodPtr, methodLen,
|
||
headersPtr, headersLen,
|
||
bodyPtr, bodyLen,
|
||
uintptr(unsafe.Pointer(&outBuf[0])), uintptr(unsafe.Pointer(&outLen)),
|
||
)
|
||
|
||
if result != 0 {
|
||
writeError("HTTP request failed")
|
||
return
|
||
}
|
||
|
||
responseStr := string(outBuf[:outLen])
|
||
|
||
// 2026-05-14:偵測 JSON `{"error":"..."}` 模式視為 4xx 失敗
|
||
// 理由:host function 沒回 HTTP status code(架構債),先用 body 啟發式 catch。
|
||
// 標準 API(cypher-executor / KBDB / 多數 REST)失敗時都回 {"error":...} JSON。
|
||
// 對應 SDD: arcrun.md 三-A P1 #4「http_request status code 缺乏 surface」。
|
||
var parsed map[string]interface{}
|
||
if err := json.Unmarshal([]byte(responseStr), &parsed); err == nil {
|
||
if errVal, ok := parsed["error"]; ok && errVal != nil {
|
||
out, _ := json.Marshal(map[string]interface{}{
|
||
"success": false,
|
||
"error": errVal,
|
||
"data": map[string]interface{}{"body": responseStr},
|
||
})
|
||
os.Stdout.Write(out)
|
||
return
|
||
}
|
||
}
|
||
|
||
out, _ := json.Marshal(map[string]interface{}{
|
||
"success": true,
|
||
"data": map[string]interface{}{"body": responseStr},
|
||
})
|
||
os.Stdout.Write(out)
|
||
}
|
||
|
||
func writeError(msg string) {
|
||
out, _ := json.Marshal(map[string]interface{}{"success": false, "error": msg})
|
||
os.Stdout.Write(out)
|
||
}
|