Files
Arcrun/cypher-executor/src/lib/cron-index.ts
T
uncle6me-web c152f5fc1d feat(onboarding+kbdb): 8.P0 cron 止血 + §7.8 onboarding + .env.example 範本
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>
2026-06-09 19:15:51 +08:00

71 lines
2.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
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 index — 單一固定 key 模型(kbdb-base 8.P0 止血)。
*
* 背景:原本每個 cron workflow 寫一筆 `cron-idx:{apiKey}:{name}`scheduled() 每分鐘
* `WEBHOOKS.list({prefix:'cron-idx:'})` 一次 = 1440 list/日,單獨就爆 CF KV 免費 list 上限(1000/日)。
*
* 解法(SDD §8.2):所有 cron workflow 的 cron_expr 集中存進**單一固定 key** `cron-idx:_all`。
* scheduled() 每分鐘只 `get` 一次(KV get 免費額度 100K/日,遠夠)→ list 次數歸零。
* acr pushwebhooks-named POST/ delete 時對這個 key 做 read-modify-write 維護。
*
* 結構:{ [ "{apiKey}:{name}" ]: cron_expr }
* key 用 `{apiKey}:{name}` 維持多租戶隔離(scheduled 觸發時拆回 apiKey/name 去讀完整 record)。
*/
// KVNamespace 用全域 ambient 型別(與 types.ts 一致,不從 @cloudflare/workers-types import
// 以免產生第二個不相容的 KVNamespace 型別)。
/** 單一固定索引 key — 全租戶共用一筆,scheduled() 只 get 這個 */
export const CRON_INDEX_KEY = 'cron-idx:_all';
/** 索引內容:entryKey"{apiKey}:{name}")→ cron_expr */
export type CronIndex = Record<string, string>;
/** 組出索引 entry 的 keyapiKey + name),含 ':' 也安全:split 時 name 用 slice 還原 */
export function cronEntryKey(apiKey: string, name: string): string {
return `${apiKey}:${name}`;
}
/** 從 entryKey 拆回 { apiKey, name }name 可能含 ':',取第一個 ':' 後全部為 name */
export function parseCronEntryKey(entryKey: string): { apiKey: string; name: string } | null {
const idx = entryKey.indexOf(':');
if (idx <= 0) return null;
return { apiKey: entryKey.slice(0, idx), name: entryKey.slice(idx + 1) };
}
/** 讀整個 cron index(單次 get,不 list */
export async function readCronIndex(kv: KVNamespace): Promise<CronIndex> {
const raw = await kv.get(CRON_INDEX_KEY, 'text');
if (!raw) return {};
try {
const parsed = JSON.parse(raw);
return parsed && typeof parsed === 'object' ? (parsed as CronIndex) : {};
} catch {
return {};
}
}
/**
* upsert / 移除單筆 cron entryread-modify-write 單一 key)。
* @param cronExpr - 有值=upsertnull/undefined=移除(push 改掉 cron 後清乾淨)
*/
export async function updateCronIndexEntry(
kv: KVNamespace,
apiKey: string,
name: string,
cronExpr: string | null | undefined,
): Promise<void> {
const index = await readCronIndex(kv);
const entryKey = cronEntryKey(apiKey, name);
if (cronExpr) {
if (index[entryKey] === cronExpr) return; // 無變化,不浪費一次 put
index[entryKey] = cronExpr;
} else {
if (!(entryKey in index)) return; // 本來就沒有,不浪費一次 put
delete index[entryKey];
}
await kv.put(CRON_INDEX_KEY, JSON.stringify(index));
}