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>
This commit is contained in:
uncle6me-web
2026-06-03 15:52:38 +08:00
commit 922a57fe34
485 changed files with 89356 additions and 0 deletions
+79
View File
@@ -0,0 +1,79 @@
/**
* scheduled() handler — 對應 wrangler.toml [triggers].crons 觸發。
*
* 流程:
* 1. 列出 WEBHOOKS KV 所有 webhook:{api_key}:{name} key
* 2. 對每個 workflow 解析 cron_expracr push 時若首節點是 cron 零件會存進 record.cron_expr
* 3. 用 cronMatch() 比對 event.scheduledTimeUTC 分鐘精度)
* 4. 匹配 → executeWebhookGraph 跑(waitUntil 背景,不擋)
*
* SDD: arcrun.md 三-A P1 #3
*/
import type { ExecutionContext, ScheduledController } from '@cloudflare/workers-types';
import type { Bindings } from './types';
import { cronMatch } from './lib/cron-match';
import { executeWebhookGraph } from './actions/webhook-handlers';
type StoredWorkflowRecord = {
graph: Record<string, unknown>;
cron_expr?: string;
// 其他欄位(id, name, created_at 等)忽略
};
export async function handleScheduled(
controller: ScheduledController,
env: Bindings,
ctx: ExecutionContext,
): Promise<void> {
const now = new Date(controller.scheduledTime);
console.log('[scheduled] tick', now.toISOString(), 'controller.cron=', controller.cron);
// 只列 cron-idx: prefix,輕量 — acr push 時為 cron-tagged workflow 額外寫一筆 index
// 主 workflow record 仍在 {apiKey}:wf:{name},需要時再 get
const list = await env.WEBHOOKS.list({ prefix: 'cron-idx:' });
let triggered = 0;
for (const entry of list.keys) {
// key = cron-idx:{api_key}:{name}
const parts = entry.name.split(':');
if (parts.length < 3) continue;
const apiKey = parts[1];
const name = parts.slice(2).join(':'); // name 可能含 ':'(雖然 push handler 已用 /^[\w-]+$/ 擋)
// 從 cron-idx 拿 cron_expr(輕量)
const idxRaw = await env.WEBHOOKS.get(entry.name, 'text');
if (!idxRaw) continue;
let idx: { cron_expr?: string };
try { idx = JSON.parse(idxRaw); } catch { continue; }
if (!idx.cron_expr) continue;
if (!cronMatch(idx.cron_expr, now)) continue;
// 匹配才去讀完整 workflow record
const wfKey = `${apiKey}:wf:${name}`;
const wfRaw = await env.WEBHOOKS.get(wfKey, 'text');
if (!wfRaw) {
console.warn('[scheduled] cron-idx 對應 workflow 不存在', wfKey);
continue;
}
let record: StoredWorkflowRecord;
try { record = JSON.parse(wfRaw) as StoredWorkflowRecord; } catch { continue; }
triggered++;
console.log('[scheduled] trigger', name, 'apiKey=', apiKey.slice(0, 12) + '...', 'cron=', idx.cron_expr);
// 把 apiKey 也放進 triggerContext,讓 workflow 內節點能用 {{api_key}}(跟 webhook trigger 慣例一致)
const triggerContext = {
api_key: apiKey,
_triggered_by: 'cron' as const,
_scheduled_at: now.toISOString(),
};
ctx.waitUntil(
executeWebhookGraph(env, record.graph, triggerContext, name, apiKey)
.then(
(r) => console.log('[scheduled] done', name, r.success, r.duration_ms + 'ms'),
(e) => console.error('[scheduled] fail', name, e),
),
);
}
console.log(`[scheduled] scanned ${list.keys.length} cron-idx entries, ${triggered} triggered`);
}