922a57fe34
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>
88 lines
2.8 KiB
TypeScript
88 lines
2.8 KiB
TypeScript
/**
|
||
* POST /workflows/resume
|
||
* Webhook callback 進來時,從 paused state 撿起來繼續跑下游節點
|
||
* SDD: matrix/arcrun/.agents/specs/resumable-workflow/design.md Phase 3
|
||
*
|
||
* 安全:因為這是 daemon 主動 callback,沒有 partner key(daemon 不知道用戶 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 + delete(idempotent:重複 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 後又遇到 pending(v2 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);
|
||
}
|
||
});
|