Files
Arcrun/cypher-executor/src/routes/resume.ts
T
uncle6me-web 922a57fe34 arcrun — AI workflow execution engine (clean history)
Self-hosted 開源:WASM 零件 + recipe + cypher-executor,跑在你自己的 Cloudflare。

此為重建的乾淨歷史起點(移除曾誤 commit 的 GCP SA 金鑰,舊歷史保留在
richblack/arcrun 與本地 backup 分支)。含:
- acr init --self-hosted installer(建 KV/R2 + codeload 拉預編譯 wasm + wrangler deploy + seed recipe)
- recipe push 把關(資料外流提醒 + 打通檢查)
- 19 個正當零件預編譯 wasm(claude_api/km_writer/kbdb_upsert_block 排除:違反 DECISIONS §1)
- CLI / cypher-executor / registry / 完整 SDD

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 15:52:38 +08:00

88 lines
2.8 KiB
TypeScript
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.
/**
* POST /workflows/resume
* Webhook callback 進來時,從 paused state 撿起來繼續跑下游節點
* SDD: matrix/arcrun/.agents/specs/resumable-workflow/design.md Phase 3
*
* 安全:因為這是 daemon 主動 callback,沒有 partner keydaemon 不知道用戶 key
* 靠 task_id 為 nonce + 24h TTL + idempotent consume 保護
*/
import { Hono } from 'hono';
import type { Bindings } from '../types';
import { WorkflowPaused } from '../types';
import { GraphExecutor } from '../graph-executor';
import { createComponentLoader } from '../lib/component-loader';
import { consumePausedRun } from '../lib/paused-runs';
export const resumeRouter = new Hono<{ Bindings: Bindings }>();
resumeRouter.post('/workflows/resume', async (c) => {
let body: Record<string, unknown>;
try {
body = await c.req.json();
} catch {
return c.json({ error: 'request body 必須為 JSON' }, 400);
}
const taskId = typeof body.task_id === 'string' ? body.task_id : undefined;
if (!taskId) {
return c.json({ error: 'task_id 必填' }, 400);
}
// consume = load + deleteidempotent:重複 callback 第二次找不到 state,回 200
const state = await consumePausedRun(c.env.EXEC_CONTEXT, taskId);
if (!state) {
return c.json({
success: true,
noop: true,
reason: `paused state 不存在或已過期 (task_id=${taskId})`,
});
}
const callbackResult = {
success: body.success ?? true,
data: body.data,
error: body.error,
};
const loader = createComponentLoader(c.env);
const executor = new GraphExecutor(loader, undefined, c.env, state.api_key);
const start = Date.now();
try {
const result = await executor.resumeFromPaused({
graph: state.graph,
paused_node_id: state.paused_node_id,
paused_context: state.paused_context,
callback_result: callbackResult,
prior_trace: state.trace_so_far,
kvNamespace: c.env.EXEC_CONTEXT,
recipe_output_format: state.recipe_output_format,
recipe_output_required_fields: state.recipe_output_required_fields,
});
const duration_ms = Date.now() - start;
return c.json({
success: true,
resumed: true,
task_id: taskId,
run_id: state.run_id,
data: result.data,
trace: result.trace,
duration_ms,
});
} catch (err) {
if (err instanceof WorkflowPaused) {
// resume 後又遇到 pendingv2 nested 情境)— v1 仍持久化但回 paused-again
return c.json({
success: true,
paused_again: true,
task_id: err.task_id,
run_id: err.run_id,
paused_node_id: err.paused_node_id,
});
}
const errMsg = err instanceof Error ? err.message : String(err);
return c.json({ success: false, error: errMsg, task_id: taskId, run_id: state.run_id }, 500);
}
});