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)。
This commit is contained in:
@@ -158,6 +158,29 @@ export default function MiraPage() {
|
|||||||
|
|
||||||
// ─── AI 回覆觸發器(fire-and-forget)──────────────────────
|
// ─── AI 回覆觸發器(fire-and-forget)──────────────────────
|
||||||
|
|
||||||
|
async function triggerWikiSynthesis(opts: { apiKey: string; rawBlockId: string }) {
|
||||||
|
// 觸發 arcrun wiki_synthesis workflow(arcrun-native public trigger endpoint)
|
||||||
|
// 不等結果(workflow 60-90s 含 2 次 claude_api pause/resume)
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_BASE}/webhooks/named/wiki_synthesis/trigger`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Arcrun-API-Key': opts.apiKey,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ api_key: opts.apiKey, raw_block_id: opts.rawBlockId }),
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
console.warn('[wiki_synthesis trigger] non-ok:', res.status);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = await res.json().catch(() => ({}));
|
||||||
|
console.log('[wiki_synthesis trigger] response:', data);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[wiki_synthesis trigger] error:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function triggerAiReply(opts: {
|
async function triggerAiReply(opts: {
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
postContent: string;
|
postContent: string;
|
||||||
@@ -272,8 +295,10 @@ function PostComposer({
|
|||||||
parentBlockId: postBlockId,
|
parentBlockId: postBlockId,
|
||||||
pageName,
|
pageName,
|
||||||
});
|
});
|
||||||
// 7B.3h:wiki_synthesis 由 arcrun cron-triggered workflow `mira_feed_watcher`
|
// 7B.3h:fire-and-forget 觸發 wiki_synthesis(browser → cypher.arcrun.dev,arcrun-native)
|
||||||
// 自動處理(每分鐘掃未處理 raw block),不需前端觸發。
|
// 不走 watcher 是因為 cypher-executor 自己 fetch 自己 workers.dev URL 被 CF 1042 擋
|
||||||
|
// watcher 仍作為 cron-driven backup(漏掉的 raws 5 分鐘後補跑),但需先解 self-fetch 問題
|
||||||
|
void triggerWikiSynthesis({ apiKey: me.api_key, rawBlockId: postBlockId });
|
||||||
onAiTriggered(pageName);
|
onAiTriggered(pageName);
|
||||||
|
|
||||||
// 給 D1 GROUP BY 查詢看到新資料的時間
|
// 給 D1 GROUP BY 查詢看到新資料的時間
|
||||||
|
|||||||
@@ -31,7 +31,11 @@ input_schema:
|
|||||||
additionalProperties:
|
additionalProperties:
|
||||||
type: string
|
type: string
|
||||||
body:
|
body:
|
||||||
description: 請求 body(任意 JSON)
|
type: string
|
||||||
|
description: 模式 A — body 字串(自行 stringify 後傳)
|
||||||
|
body_json:
|
||||||
|
type: object
|
||||||
|
description: 模式 B — body 物件,零件內部 JSON.stringify(yaml 端不用手組字串)
|
||||||
output_schema:
|
output_schema:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
|||||||
@@ -24,10 +24,11 @@ func hostHttpRequest(
|
|||||||
) uint32
|
) uint32
|
||||||
|
|
||||||
type Input struct {
|
type Input struct {
|
||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
Method string `json:"method"`
|
Method string `json:"method"`
|
||||||
Headers map[string]string `json:"headers"`
|
Headers map[string]string `json:"headers"`
|
||||||
Body string `json:"body"`
|
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
|
// dummy byte for safe zero-length unsafe.Pointer operations
|
||||||
@@ -71,10 +72,19 @@ func main() {
|
|||||||
headersJSON = string(b)
|
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)
|
urlBytes := []byte(input.URL)
|
||||||
methodBytes := []byte(method)
|
methodBytes := []byte(method)
|
||||||
headersBytes := []byte(headersJSON)
|
headersBytes := []byte(headersJSON)
|
||||||
bodyBytes := []byte(input.Body)
|
bodyBytes := []byte(bodyStr)
|
||||||
outBuf := make([]byte, 65536) // 64KB output buffer
|
outBuf := make([]byte, 65536) // 64KB output buffer
|
||||||
var outLen uint32
|
var outLen uint32
|
||||||
|
|
||||||
@@ -97,6 +107,24 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
responseStr := string(outBuf[:outLen])
|
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{}{
|
out, _ := json.Marshal(map[string]interface{}{
|
||||||
"success": true,
|
"success": true,
|
||||||
"data": map[string]interface{}{"body": responseStr},
|
"data": map[string]interface{}{"body": responseStr},
|
||||||
|
|||||||
Reference in New Issue
Block a user