/** * Tournament cron job registration. * Follows the same file-based pattern as scheduler/cron-setup.ts. * @module openfinclaw/tournament/cron-setup */ import { randomUUID } from "node:crypto"; import fs from "node:fs"; import path from "node:path"; const DEFAULT_TOURNAMENT_CRON = "0 9 * * 1-5"; // 9 AM weekdays const DEFAULT_TOURNAMENT_TZ = "Asia/Shanghai"; interface StoredCronJob { id: string; name: string; enabled: boolean; schedule: { kind: "cron"; expr: string; tz?: string }; payload: { kind: "systemEvent"; text: string }; sessionTarget: string; wakeMode: string; delivery: { mode: string }; createdAtMs: number; updatedAtMs: number; state: Record; } interface CronStoreFile { version: 1; jobs: StoredCronJob[]; } function defaultStorePath(): string { const home = process.env.HOME ?? process.env.USERPROFILE ?? ""; return path.join(home, ".openclaw", "cron", "jobs.json"); } async function loadStore(storePath: string): Promise { try { const raw = await fs.promises.readFile(storePath, "utf-8"); const parsed = JSON.parse(raw) as Record; const jobs = Array.isArray(parsed.jobs) ? (parsed.jobs as StoredCronJob[]) : []; return { version: 1, jobs }; } catch (err) { if ((err as { code?: string }).code === "ENOENT") { return { version: 1, jobs: [] }; } throw err; } } async function saveStore(storePath: string, store: CronStoreFile): Promise { const dir = path.dirname(storePath); await fs.promises.mkdir(dir, { recursive: true }); const json = JSON.stringify(store, null, 2); const tmp = `${storePath}.${process.pid}.${Date.now()}.tmp`; await fs.promises.writeFile(tmp, json, "utf-8"); await fs.promises.rename(tmp, storePath); } const TOURNAMENT_JOB_NAME = "openfinclaw:tournament"; /** * Register the daily tournament cron job. * Idempotent: skips if job already exists by name. */ export async function setupTournamentCronJob(config?: { cronExpr?: string; timezone?: string; }): Promise<{ ok: boolean; created: boolean }> { const storePath = defaultStorePath(); const store = await loadStore(storePath); const existingNames = new Set(store.jobs.map((j) => j.name)); if (existingNames.has(TOURNAMENT_JOB_NAME)) { return { ok: true, created: false }; } const cronExpr = config?.cronExpr ?? process.env.OPENFINCLAW_TOURNAMENT_CRON ?? DEFAULT_TOURNAMENT_CRON; const timezone = config?.timezone ?? DEFAULT_TOURNAMENT_TZ; const now = Date.now(); store.jobs.push({ id: randomUUID(), name: TOURNAMENT_JOB_NAME, enabled: true, schedule: { kind: "cron", expr: cronExpr, tz: timezone }, payload: { kind: "systemEvent", text: "[openfinclaw:tournament] 每日策略锦标赛触发。请执行今日锦标赛流程。", }, sessionTarget: "main", wakeMode: "now", delivery: { mode: "none" }, createdAtMs: now, updatedAtMs: now, state: {}, }); await saveStore(storePath, store); return { ok: true, created: true }; }