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:
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* scheduled() handler — 對應 wrangler.toml [triggers].crons 觸發。
|
||||
*
|
||||
* 流程:
|
||||
* 1. 列出 WEBHOOKS KV 所有 webhook:{api_key}:{name} key
|
||||
* 2. 對每個 workflow 解析 cron_expr(acr push 時若首節點是 cron 零件會存進 record.cron_expr)
|
||||
* 3. 用 cronMatch() 比對 event.scheduledTime(UTC 分鐘精度)
|
||||
* 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`);
|
||||
}
|
||||
Reference in New Issue
Block a user