c152f5fc1d
kbdb-base 8.P0:scheduled.ts cron 每分鐘 KV list → 單一 key get(lib/cron-index.ts); webhooks-named 維護單 key + 一次性 migrate-cron-index;acr update 自動遷移。1440 list/日 → 0。 self-hosted-init §7.8 onboarding: P0 init 偵測+裝完驗收(lib/preflight.ts,pip 式,冪等) P1 acr whoami(+--json)+ MCP arcrun_whoami(AI 不繞 CLI 猜帳號) P2 mcp-setup 寫完印「請重啟 client」 P3(部分)repo .env.example 範本(每格白話說明、值留空)+ llms.txt 教 AI 幫用戶 cp 建 .env Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
77 lines
2.9 KiB
TypeScript
77 lines
2.9 KiB
TypeScript
/**
|
||
* scheduled() handler — 對應 wrangler.toml [triggers].crons 觸發。
|
||
*
|
||
* 流程:
|
||
* 1. 單次 get cron index(cron-idx:_all,集中存所有 cron workflow 的 cron_expr)
|
||
* 2. 在記憶體比對每筆 cron_expr 跟 event.scheduledTime(UTC 分鐘精度)
|
||
* 3. 匹配才去讀完整 workflow record({apiKey}:wf:{name})
|
||
* 4. 匹配 → executeWebhookGraph 跑(waitUntil 背景,不擋)
|
||
*
|
||
* 8.P0 止血(SDD §8.2):原本每分鐘 WEBHOOKS.list('cron-idx:') = 1440 list/日 爆 KV 上限,
|
||
* 改成單一固定 key 只 get 一次 → list 歸零。
|
||
*
|
||
* SDD: arcrun.md 三-A P1 #3 / kbdb-base §8.2
|
||
*/
|
||
|
||
import type { ExecutionContext, ScheduledController } from '@cloudflare/workers-types';
|
||
import type { Bindings } from './types';
|
||
import { cronMatch } from './lib/cron-match';
|
||
import { readCronIndex, parseCronEntryKey } from './lib/cron-index';
|
||
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);
|
||
|
||
// 8.P0:單次 get 集中索引(取代每分鐘 list),主 workflow record 仍在 {apiKey}:wf:{name}
|
||
const index = await readCronIndex(env.WEBHOOKS);
|
||
const entries = Object.entries(index);
|
||
|
||
let triggered = 0;
|
||
for (const [entryKey, cronExpr] of entries) {
|
||
const parsed = parseCronEntryKey(entryKey);
|
||
if (!parsed) continue;
|
||
const { apiKey, name } = parsed;
|
||
|
||
if (!cronExpr) continue;
|
||
if (!cronMatch(cronExpr, 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=', cronExpr);
|
||
// 把 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 ${entries.length} cron-idx entries, ${triggered} triggered`);
|
||
}
|