From b8ecef0b4159522940190876bc1ae458ea7dfc08 Mon Sep 17 00:00:00 2001 From: richblack Date: Sat, 16 May 2026 13:54:26 +0800 Subject: [PATCH] =?UTF-8?q?feat(cypher-executor):=20trigger=5Fworkflow=20?= =?UTF-8?q?=E5=85=A7=E5=BB=BA=E9=9B=B6=E4=BB=B6=20=E2=80=94=20=E7=B9=9E=20?= =?UTF-8?q?CF=20self-fetch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mira_feed_watcher 之前用 http_request 自打 cypher.arcrun.dev / 自身 workers.dev URL 都被 CF self-fetch 防護擋(Worker→自身的 subrequest 攔截)。 新增 `trigger_workflow` 內建 orchestration 零件: - 在 createComponentLoader 最前面攔截 component_id === 'trigger_workflow' - 從 WEBHOOKS KV 撈 `{api_key}:wf:{name}` 拿 graph - 動態 import 避循環依賴 - in-process 呼叫 executeWebhookGraph,沒有任何外部 HTTP - 預設 wait=true(claude_api paused 仍視為 success 回傳) 不違反「業務邏輯走 WASM」鐵律:trigger_workflow 是 orchestrator 自己的 routing 能力(像既有的 CALLS_SUBFLOW),不是業務邏輯。 對應 mira_feed_watcher.yaml 同步改用此零件(在 polaris/mira/ repo)。 Co-Authored-By: Claude Opus 4.7 --- cypher-executor/src/lib/component-loader.ts | 70 +++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/cypher-executor/src/lib/component-loader.ts b/cypher-executor/src/lib/component-loader.ts index 7595c42..3ad8595 100644 --- a/cypher-executor/src/lib/component-loader.ts +++ b/cypher-executor/src/lib/component-loader.ts @@ -94,6 +94,14 @@ const LOGIC_BINDING_MAP: Record = { export function createComponentLoader(env: Bindings) { return async (componentId: string): Promise => { + // 0. 平台內建 orchestration 零件(需要 env / 跨 workflow 能力) + // 這類零件「是 orchestrator 的職責」(不是業務邏輯),故不違反「業務邏輯走 WASM」規則。 + // 目前只有 trigger_workflow:用 in-process call 觸發另一個 named workflow, + // 繞掉 CF 同 zone self-fetch 死鎖(避免 cypher-executor 自打 http_request → 1042)。 + if (componentId === 'trigger_workflow') { + return makeTriggerWorkflowRunner(env); + } + // 1. 內建零件(純 JS,最優先) const builtin = BUILTIN_COMPONENTS.get(componentId); if (builtin) return builtin; @@ -152,6 +160,68 @@ export function createComponentLoader(env: Bindings) { // ── 執行器工廠 ──────────────────────────────────────────────────────────────── +/** + * trigger_workflow 內建 orchestration 零件 + * + * 用途:在 workflow A 內 in-process 觸發 workflow B,繞 CF 同 zone self-fetch 死鎖。 + * + * 動機:mira_feed_watcher 之前用 http_request 自打 cypher.arcrun.dev → CF 1042。 + * 就算改打 arcrun-cypher-executor.{subdomain}.workers.dev,Worker → 自身 URL 仍 + * 被 CF 「self subrequest」防護擋(即使 hostname 不同)。 + * 改用 in-process call executeWebhookGraph 徹底繞掉外部 HTTP。 + * + * 不違反「業務邏輯走 WASM」鐵律:trigger_workflow 是 orchestrator 自己的 routing 能力 + * (像 CALLS_SUBFLOW),不是業務邏輯(不解密 / 不簽 JWT / 不打外部 API)。 + * + * Input ctx: + * - workflow_name: string (必填,目標 workflow 名稱) + * - api_key: string (必填,KV 查 key prefix) + * - input: object (可選,傳給子 workflow 當 triggerContext) + * - wait: boolean (預設 true,await 完成;false = fire-and-forget 用 waitUntil) + * + * 動態 import webhook-handlers 避循環依賴(webhook-handlers → component-loader → 自己)。 + */ +function makeTriggerWorkflowRunner(env: Bindings): ComponentRunner { + return async (ctx: unknown) => { + const c = (ctx && typeof ctx === 'object') ? ctx as Record : {}; + const workflowName = String(c.workflow_name ?? ''); + const apiKey = String(c.api_key ?? ''); + const input = (c.input && typeof c.input === 'object') + ? c.input as Record + : {}; + const wait = c.wait !== false; // 預設 true + + if (!workflowName) return { success: false, error: 'trigger_workflow 缺 workflow_name' }; + if (!apiKey) return { success: false, error: 'trigger_workflow 缺 api_key' }; + + // 從 WEBHOOKS KV 撈目標 workflow 的 graph + const wfKey = `${apiKey}:wf:${workflowName}`; + const wfRaw = await env.WEBHOOKS.get(wfKey, 'text'); + if (!wfRaw) return { success: false, error: `找不到 workflow "${workflowName}" (key=${wfKey})` }; + + let record: { graph?: Record }; + try { record = JSON.parse(wfRaw); } + catch { return { success: false, error: `workflow "${workflowName}" KV 內容非 JSON` }; } + if (!record.graph) return { success: false, error: `workflow "${workflowName}" 缺 graph 欄位` }; + + // 動態 import 避循環依賴 + const { executeWebhookGraph } = await import('../actions/webhook-handlers'); + + const triggerContext = { ...input, _triggered_by: 'trigger_workflow' }; + + if (wait) { + const r = await executeWebhookGraph(env, record.graph, triggerContext, workflowName, apiKey); + return { success: r.success, triggered_workflow: workflowName, sub_result: r }; + } else { + // fire-and-forget — 不 await,但因為沒拿到 ctx.waitUntil,這裡 promise 可能被 cancel + // 目前不啟用,留 wait=true 為預設。未來想要 fire-and-forget 需 plumb ExecutionContext + void executeWebhookGraph(env, record.graph, triggerContext, workflowName, apiKey) + .catch((e) => console.error('[trigger_workflow] fire-and-forget fail', workflowName, e)); + return { success: true, triggered_workflow: workflowName, mode: 'fire_and_forget' }; + } + }; +} + function makeHttpRunner(url: string): ComponentRunner { return async (ctx: unknown) => { const res = await fetch(url, {