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>
93 lines
3.2 KiB
TypeScript
93 lines
3.2 KiB
TypeScript
/**
|
||
* 最小 cron expression matcher:5 欄位(minute hour dayOfMonth month dayOfWeek)。
|
||
*
|
||
* 用於 cypher-executor scheduled() handler — 把 workflow 註冊的 cron_expr 跟
|
||
* 每分鐘 tick 的 event.scheduledTime 比對,匹配就觸發該 workflow。
|
||
*
|
||
* 支援語法(夠用即可,未來再擴):
|
||
* `*` — 任何值
|
||
* `5` — 等於 5
|
||
* `*/N` — 每 N 個(N>0)
|
||
* `5,10,15` — 任一
|
||
* `1-5` — range(含兩端)
|
||
*
|
||
* 不支援(暫):
|
||
* `?` / `L` / `W` / `#` 等延伸語法
|
||
* month / weekday 用名稱(jan/mon 等)
|
||
*
|
||
* 對應 SDD: arcrun.md 三-A P1 #3。
|
||
*/
|
||
|
||
/** 一個欄位(如 'minute')的值是否匹配 expr 段 */
|
||
function matchField(expr: string, value: number, min: number, max: number): boolean {
|
||
if (expr === '*') return true;
|
||
for (const part of expr.split(',')) {
|
||
if (matchPart(part.trim(), value, min, max)) return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
function matchPart(part: string, value: number, min: number, max: number): boolean {
|
||
// `*/N`
|
||
if (part.startsWith('*/')) {
|
||
const step = parseInt(part.slice(2), 10);
|
||
if (!Number.isFinite(step) || step <= 0) return false;
|
||
return (value - min) % step === 0;
|
||
}
|
||
// `X-Y` 或 `X-Y/N`
|
||
if (part.includes('-')) {
|
||
const [rangePart, stepStr] = part.split('/');
|
||
const [aStr, bStr] = rangePart.split('-');
|
||
const a = parseInt(aStr, 10);
|
||
const b = parseInt(bStr, 10);
|
||
if (!Number.isFinite(a) || !Number.isFinite(b)) return false;
|
||
if (value < a || value > b) return false;
|
||
if (stepStr === undefined) return true;
|
||
const step = parseInt(stepStr, 10);
|
||
if (!Number.isFinite(step) || step <= 0) return false;
|
||
return (value - a) % step === 0;
|
||
}
|
||
// `N`
|
||
const n = parseInt(part, 10);
|
||
if (!Number.isFinite(n)) return false;
|
||
if (n < min || n > max) return false;
|
||
return value === n;
|
||
}
|
||
|
||
/**
|
||
* 比對 cron expr 跟某個時間點。
|
||
* @param expr - 5 欄位 cron(minute hour dom month dow)
|
||
* @param date - 要比對的時間(UTC)
|
||
*/
|
||
export function cronMatch(expr: string, date: Date): boolean {
|
||
const fields = expr.trim().split(/\s+/);
|
||
if (fields.length !== 5) return false;
|
||
const [m, h, dom, mon, dow] = fields;
|
||
// dow: 0=Sun ... 6=Sat (跟 JavaScript 一致;ISO Mon=1 暫不轉)
|
||
return (
|
||
matchField(m, date.getUTCMinutes(), 0, 59) &&
|
||
matchField(h, date.getUTCHours(), 0, 23) &&
|
||
matchField(dom, date.getUTCDate(), 1, 31) &&
|
||
matchField(mon, date.getUTCMonth() + 1, 1, 12) &&
|
||
matchField(dow, date.getUTCDay(), 0, 6)
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 從 workflow YAML 的 config 找出 cron 零件節點的 cron_expr。
|
||
* 找不到回 null(代表此 workflow 不是 cron-triggered)。
|
||
*
|
||
* @param graph - acr push 解析後的 ExecutionGraph
|
||
*/
|
||
export function extractCronExpr(graph: unknown): string | null {
|
||
if (!graph || typeof graph !== 'object') return null;
|
||
const nodes = (graph as { nodes?: Array<{ id: string; componentId?: string; data?: Record<string, unknown> }> }).nodes;
|
||
if (!Array.isArray(nodes)) return null;
|
||
for (const node of nodes) {
|
||
if (node.componentId !== 'cron') continue;
|
||
const expr = node.data?.cron_expr;
|
||
if (typeof expr === 'string' && expr.trim()) return expr.trim();
|
||
}
|
||
return null;
|
||
}
|