Files
Arcrun/registry/components/http_request/main.go
T
Leo bc6360ccfc feat(arcrun): http_request body_json + error heuristic; mira feed fire-and-forget
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)。
2026-05-14 16:06:46 +08:00

139 lines
3.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.
// 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。
// 標準 APIcypher-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)
}