feat(cypher-executor): trigger_workflow 內建零件 — 繞 CF self-fetch
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 <noreply@anthropic.com>
This commit is contained in:
@@ -94,6 +94,14 @@ const LOGIC_BINDING_MAP: Record<string, keyof Bindings> = {
|
|||||||
export function createComponentLoader(env: Bindings) {
|
export function createComponentLoader(env: Bindings) {
|
||||||
return async (componentId: string): Promise<ComponentRunner> => {
|
return async (componentId: string): Promise<ComponentRunner> => {
|
||||||
|
|
||||||
|
// 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,最優先)
|
// 1. 內建零件(純 JS,最優先)
|
||||||
const builtin = BUILTIN_COMPONENTS.get(componentId);
|
const builtin = BUILTIN_COMPONENTS.get(componentId);
|
||||||
if (builtin) return builtin;
|
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<string, unknown> : {};
|
||||||
|
const workflowName = String(c.workflow_name ?? '');
|
||||||
|
const apiKey = String(c.api_key ?? '');
|
||||||
|
const input = (c.input && typeof c.input === 'object')
|
||||||
|
? c.input as Record<string, unknown>
|
||||||
|
: {};
|
||||||
|
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<string, unknown> };
|
||||||
|
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 {
|
function makeHttpRunner(url: string): ComponentRunner {
|
||||||
return async (ctx: unknown) => {
|
return async (ctx: unknown) => {
|
||||||
const res = await fetch(url, {
|
const res = await fetch(url, {
|
||||||
|
|||||||
Reference in New Issue
Block a user