Files
Arcrun/cypher-executor/src/routes/executions.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

204 lines
6.6 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.
/**
* Executions routes — LI SDD M2.1
*
* 對應 .agents/specs/llm-interface/ Milestone 2.1。給 AI 看 workflow 執行狀態的端點。
*
* - GET /executions/paused — 列當前所有 paused 的 workflow(等 callback resume
* - GET /executions/:task_id — 看單一 paused state 細節(含 trace、graph、node id
* - GET /workflows/:name/executions — 列某 workflow 最近 N 次執行 verdict
*
* 設計:純讀,無 side effect。所有路由要 api_key auth(防偷看他人 workflow state)。
*/
import { Hono } from 'hono';
import type { Bindings } from '../types';
import { listPausedRunsByApiKey } from '../lib/paused-runs';
export const executionsRouter = new Hono<{ Bindings: Bindings }>();
/**
* GET /executions/paused — 列當前 api_key 下所有 paused workflow
*
* 走 per-user index `paused_idx:{api_key}`(單 KV get,強 consistent,無 KV list 延遲)
* 取代舊的 `paused_run:*` prefix scanCF KV list 30-60 秒 eventual consistent
*/
executionsRouter.get('/executions/paused', async (c) => {
const apiKey = c.req.header('X-Arcrun-API-Key');
if (!apiKey) {
return c.json({
ok: false,
error_code: 'auth_missing',
human_message: '缺 X-Arcrun-API-Key header',
next_actions: ['call /me 取得你的 ak_xxx,加進 header'],
}, 401);
}
const limitParam = c.req.query('limit');
const limit = Math.min(Math.max(parseInt(limitParam || '20', 10), 1), 100);
const paused = await listPausedRunsByApiKey(c.env.EXEC_CONTEXT, apiKey, limit);
return c.json({
ok: true,
data: { count: paused.length, paused },
hints: paused.length > 0
? [`${paused.length} 個 workflow 等 callback resume。call get_execution_trace(task_id) 看細節`]
: ['沒有任何 paused workflow'],
});
});
/**
* GET /executions/:task_id — 看單一 paused workflow 的 statetrace、graph、context
*
* task_id 來源:trigger workflow 時 response 含 paused 結果,task_id 在 error 字串裡,
* 或前端 list_paused_executions 回的 task_id。
*
* 隔離:只能讀自己 api_key 的 state。
*/
executionsRouter.get('/executions/:task_id', async (c) => {
const apiKey = c.req.header('X-Arcrun-API-Key');
if (!apiKey) {
return c.json({
ok: false,
error_code: 'auth_missing',
human_message: '缺 X-Arcrun-API-Key header',
next_actions: ['加 X-Arcrun-API-Key header'],
}, 401);
}
const taskId = c.req.param('task_id');
const raw = await c.env.EXEC_CONTEXT.get(`paused_run:${taskId}`);
if (!raw) {
return c.json({
ok: false,
error_code: 'not_found',
human_message: `task_id "${taskId}" 沒對應的 paused state(可能已 resume 完、過 24h TTL 被 GC、或從未存在)`,
next_actions: [
'call /executions/paused 看當前所有 paused,確認 task_id 正確',
'若該 workflow 不是 paused 型,看 /workflows/:name/executions 查歷史 verdict',
],
}, 404);
}
let state: {
run_id: string;
graph?: unknown;
paused_node_id: string;
paused_context?: Record<string, unknown>;
paused_pending_result?: Record<string, unknown>;
trace_so_far?: unknown;
api_key?: string;
expires_at?: number;
};
try {
state = JSON.parse(raw);
} catch {
return c.json({
ok: false,
error_code: 'internal_error',
human_message: 'paused state JSON 損毀',
next_actions: ['告訴 leo / 平台維護者'],
}, 500);
}
if (state.api_key !== apiKey) {
return c.json({
ok: false,
error_code: 'not_found', // 不洩漏存在性
human_message: `task_id "${taskId}" 找不到`,
next_actions: ['確認 task_id 屬於你 (用 /executions/paused 列出)'],
}, 404);
}
return c.json({
ok: true,
data: {
task_id: taskId,
run_id: state.run_id,
paused_node_id: state.paused_node_id,
paused_context: state.paused_context,
paused_pending_result: state.paused_pending_result,
trace_so_far: state.trace_so_far,
expires_at: state.expires_at,
},
hints: [
'paused 狀態 = workflow 等 daemon callback。等對應 service 回 POST /workflows/resume 即可繼續',
'若 daemon 掛了,看 expires_at — 過 24h KV TTL 會 GC 此 state',
],
});
});
/**
* GET /workflows/:name/executions — 看某 workflow 最近 N 次執行 verdict
*
* 走 ANALYTICS_KV `stats:{workflowId}:*` prefix scan。
*
* workflowId 等於 webhook nameexecution-logger 寫入時用 graph.id ?? name)。
*
* 限制:ANALYTICS_KV list 沒辦法依 timestamp 排序,只能拿 key 後段 timestamp 排。
*/
executionsRouter.get('/workflows/:name/executions', async (c) => {
const apiKey = c.req.header('X-Arcrun-API-Key');
if (!apiKey) {
return c.json({
ok: false,
error_code: 'auth_missing',
human_message: '缺 X-Arcrun-API-Key header',
next_actions: ['加 X-Arcrun-API-Key header'],
}, 401);
}
const name = c.req.param('name');
const limitParam = c.req.query('limit');
const limit = Math.min(Math.max(parseInt(limitParam || '10', 10), 1), 100);
// 確認 workflow 是該 api_key 的(防偷看他人)
const wfRaw = await c.env.WEBHOOKS.get(`${apiKey}:wf:${name}`, 'text');
if (!wfRaw) {
return c.json({
ok: false,
error_code: 'not_found',
human_message: `workflow "${name}" 不存在或不屬於你`,
next_actions: ['call /webhooks/named 看你有什麼 workflow'],
}, 404);
}
// 撈 stats:{name}:* 全 list(每個 key 含 timestamp 後綴)
const list = await c.env.ANALYTICS_KV.list({ prefix: `stats:${name}:`, limit: 1000 });
// 按 timestamp 降序(key suffix 是 unix ms
const sorted = [...list.keys].sort((a, b) => {
const ta = parseInt(a.name.split(':').pop() ?? '0', 10);
const tb = parseInt(b.name.split(':').pop() ?? '0', 10);
return tb - ta;
}).slice(0, limit);
const executions = [];
for (const key of sorted) {
const raw = await c.env.ANALYTICS_KV.get(key.name);
if (!raw) continue;
try {
const record = JSON.parse(raw);
executions.push({
timestamp: key.name.split(':').pop(),
...record,
});
} catch {
// skip
}
}
return c.json({
ok: true,
data: {
workflow_name: name,
count: executions.length,
executions,
},
hints: executions.length === 0
? ['尚未有任何執行紀錄(或都過了 90d TTL)。先 call /webhooks/named/:name/trigger 跑一次']
: [`最近 ${executions.length} 次。看到 verdict=failed 的,call /executions/:task_id 看 paused state 或繼續 debug`],
});
});