// 数据保留清理(docs/runbook/storage.md「保留规则」):按 settings.storage.retentionDays 统计 / 执行各保留任务。 // 规则与定时任务 storage.retention.* 是同一份代码(apps/server/src/modules/system/retention.ts)。 // 默认只读(dry-run),输出每张表将删的行数;--apply 才删除(分批、每批单独事务,删得多的表随后 ANALYZE)。 // // 用法(DATABASE_URL 必须显式给出,脚本不带默认库): // DATABASE_URL=postgres://umt:***@:5432/umt deno run -A scripts/db-retention.ts [--dry-run] # 看每张表将删多少行 // DATABASE_URL=… deno run -A scripts/db-retention.ts --apply # 执行 // DATABASE_URL=… deno run -A scripts/db-retention.ts --only task_run,pipeline [--apply] # 只处理部分目标 // ENGINE_SCHEMA=openworkflow(缺省) // // 不包含:进程心跳(system.heartbeat_cleanup)、图片缓存与 kv_cache(cache 模块)、ANALYZE 任务(--apply 结束时对删过的表各自 ANALYZE)。 // 对真实库先 dry-run,逐表看过再 --apply(docs/plan/lessons.md 第 22 条)。 import { withStorageDefaults } from "@umt/contracts"; import { createDb, sql } from "@umt/db"; import { previewJob, RETENTION_JOBS } from "../apps/server/src/modules/system/retention.ts"; const argv = [...Deno.args]; const APPLY = argv.includes("--apply"); if (APPLY && argv.includes("--dry-run")) { console.error("--apply 与 --dry-run 不能同时给出"); Deno.exit(2); } const onlyArg = argv.indexOf("--only"); const only = onlyArg >= 0 ? new Set((argv[onlyArg + 1] ?? "").split(",").filter(Boolean)) : null; const url = Deno.env.get("DATABASE_URL"); if (!url) { console.error("需要 DATABASE_URL(脚本不带默认库)"); Deno.exit(2); } const engineSchema = Deno.env.get("ENGINE_SCHEMA") ?? "openworkflow"; const db = createDb({ connectionString: url, max: 2 }); try { const stored = await sql<{ value: unknown }>`select value from settings where key = 'storage'`.execute(db); const settings = withStorageDefaults(stored.rows[0]?.value); const now = new Date(); const jobs = RETENTION_JOBS.filter((j) => j.target !== "analyze" && (!only || only.has(j.target))); console.log(`# db-retention (${APPLY ? "APPLY" : "DRY-RUN"}) ${now.toISOString()}`); console.log(`# retentionDays ${JSON.stringify(settings.retentionDays)}\n`); let total = 0; for (const job of jobs) { const counts = await previewJob(job, { db, engineSchema, settings, now }); console.log(`## ${job.target}(${job.jobId},保留 ${job.retention(settings) ?? "—"})`); for (const c of counts) { console.log(` ${c.table.padEnd(34)} ${String(c.rows).padStart(8)} ${c.label}`); if (!c.cascade && job.rules) total += c.rows; } if (APPLY) { const t0 = Date.now(); const summary = await job.run({ db, engineSchema, settings, now, batch: { budgetMs: 60 * 60_000 } }); console.log(` → ${summary}(${Math.round((Date.now() - t0) / 100) / 10} 秒)`); } console.log(""); } console.log(`# 合计将删 ${total} 行${APPLY ? "(已执行)" : "(dry-run,未改库;确认后加 --apply)"}`); } finally { await db.destroy(); }