465c505000
Haiku 自主壓測(test_arcrun/5)暴露的真 bug,逐一修復:
1. 假綠根因:http_request host function 丟掉 HTTP status code(main.go:112 架構債)
→ 非 2xx(如 Notion 401)被判 success → 引擎自己對失敗報成功。
修:host fn 非 2xx 回 {error,status,body} envelope,既有判定鏈正確識別。
http_request/claude_api/kbdb_upsert_block/km_writer 已修(4 worker deploy);
auth_service_account 自有 OAuth 判定不套。
2. acr run self-hosted:原一律走 /webhooks/<name>(需先 push)→ 沒 push 回 404 純文字
→ res.json() 爆假錯誤。修:本機有 YAML 走玩法一 /cypher/execute 直接執行(三模式一致)
+ res.ok 擋非 2xx + findWorkflowYaml 容忍 .yaml 副檔名。
3. D1-in-update:D1 只在 init 建一次,update 漏建 → token 補權限後無冪等補建路徑。
修:update 也 ensureD1Database(已驗證 D1 建起 count:1)。
4. CF token 教學漏 D1:llms.txt/.env.example 加「Account/D1/Edit」必勾 + init/preflight
訊息指明 token 缺 D1 權限的修法。
CLI 1.3.4 publish。Haiku 壓測結論:onboarding 治好(裝+init 沒跳過、建 recipe 不建零件),
但仍會假綠(curl 繞過/D1 沒建謊報)→ 印證執行真相要系統能驗、不信 AI 自報。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
145 lines
4.3 KiB
Go
145 lines
4.3 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])
|
||
|
||
// 偵測 JSON `{"error":"..."}` 模式視為失敗。
|
||
// 2026-06-09 修架構債:host function(.component-builds/http_request/src/index.ts)現在對非 2xx
|
||
// 回 envelope `{"error":"HTTP <status>","status":<code>,"body":<原文>}`——故此處 parsed["error"]
|
||
// 能正確 catch 所有 4xx/5xx(含 Notion 401 那種 body 用 {"object":"error"} 不帶 error key 的)。
|
||
// 之前 host fn 只回 body 原文丟掉 status → 401 被判 success(系統假綠根因,已修)。
|
||
// 註:claude_api/kbdb_upsert_block/km_writer 已同樣修(非 2xx 回 error envelope)。
|
||
// auth_service_account 不套此 envelope——它 main.go 自己解析 OAuth token 回應的
|
||
// {access_token,error,error_description},access_token 空即視為失敗,已有自己的判定,
|
||
// 套 envelope 反而會丟掉 error_description 破壞 token exchange 錯誤處理。
|
||
// 待辦:4 份 inline host fn 最好抽成共用 helper(dedup,目前複製貼上)。
|
||
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)
|
||
}
|