Files
Arcrun/cypher-executor/src/lib/cron-match.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

93 lines
3.2 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
/**
* 最小 cron expression matcher5 欄位(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 欄位 cronminute 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;
}