import { copyFile, mkdir, readdir, rm } from "node:fs/promises"; import { existsSync } from "node:fs"; import { resolve } from "node:path"; import { dbAll, dbPath, dbRun, withTransaction } from "../server/db.mjs"; const projectRoot = resolve(import.meta.dirname, ".."); const storageRoot = resolve(projectRoot, "storage"); const backupsRoot = resolve(projectRoot, "data", "backups"); const dryRun = process.argv.includes("--dry-run"); const keep = { userIds: ["u-owner", "u-producer", "u-writer", "u-art", "u-voice", "u-review", "u-local-worker"], notificationIds: ["user-notification-seed-review", "user-notification-seed-policy"], usageIds: ["usage-seed-001"], auditIds: ["aud-001", "aud-002", "aud-003", "aud-004"], modelCatalogIds: [ "catalog-qwen-image-2d", "catalog-wan-i2v", "catalog-qwen-parser", "catalog-bge-m3", "catalog-indextts", "catalog-paraformer", "catalog-northstar-image" ], modelRouteIds: [ "route-script-ingest", "route-script-materialize", "route-image-keyframe", "route-video-clip", "route-voice-tts", "route-voice-asr" ], knowledgeDocumentIds: ["knowledge-rain-night"], knowledgeContextPackIds: ["knowledge-pack-rain-night-opening"], scriptIds: ["script-thunder-mouth-v1"], shotIds: ["shot-01", "shot-02", "shot-03"], assetIds: [ "asset-character-chen-yu", "asset-character-tang-xia", "asset-location-metro-canopy-rain", "asset-prop-blue-umbrella", "asset-prop-yellow-poncho", "asset-prop-warning-cones", "asset-prop-phone", "asset-voice-chen-yu", "asset-voice-tang-xia", "asset-style-cel-shading", "asset-lora-rain-local" ], jobIds: ["job-img-001", "job-i2v-002", "job-tts-003", "job-edit-004", "job-asr-005"], apiClientIds: ["client-local-runner"] }; const summary = { db: {}, files: [] }; class DryRunRollback extends Error { constructor() { super("dry-run rollback"); } } function stamp() { return new Date().toISOString().replace(/[-:]/g, "").replace(/\..+/, "").replace("T", "-"); } function placeholders(values) { return values.map(() => "?").join(", "); } function record(table, changes) { if (!changes) return; summary.db[table] = (summary.db[table] || 0) + changes; } function deleteWhere(table, where, params = []) { const result = dbRun(`DELETE FROM ${table} WHERE ${where}`, params); record(table, Number(result.changes || 0)); } function deleteExceptIds(table, ids, column = "id") { deleteWhere(table, `${column} NOT IN (${placeholders(ids)})`, ids); } function selectIds(sql, params = []) { return dbAll(sql, params).map((row) => row.id); } async function backupDatabase() { await mkdir(backupsRoot, { recursive: true }); dbRun("PRAGMA wal_checkpoint(FULL)"); const backupPath = resolve(backupsRoot, `platform-demo-noise-snapshot-${stamp()}.sqlite`); if (!dryRun) await copyFile(dbPath, backupPath); return backupPath; } async function removePath(path) { if (!path || !existsSync(path)) return; if (!dryRun) await rm(path, { recursive: true, force: true }); summary.files.push(path); } async function removeChildrenByPrefix(rootPath, prefix) { if (!existsSync(rootPath)) return; const entries = await readdir(rootPath, { withFileTypes: true }); for (const entry of entries) { if (!entry.name.startsWith(prefix)) continue; await removePath(resolve(rootPath, entry.name)); } } async function walkAndRemove(rootPath, matcher) { if (!existsSync(rootPath)) return; const entries = await readdir(rootPath, { withFileTypes: true }); for (const entry of entries) { const current = resolve(rootPath, entry.name); if (entry.isDirectory()) { await walkAndRemove(current, matcher); continue; } if (matcher(entry.name, current)) await removePath(current); } } function listRelativePathsForJobs(jobIds) { if (!jobIds.length) return []; return dbAll( `SELECT output_path FROM generation_jobs WHERE id IN (${placeholders(jobIds)})`, jobIds ) .map((row) => String(row.output_path || "").trim()) .filter(Boolean); } const smokeDeviceIds = selectIds("SELECT id FROM auth_devices WHERE label LIKE '%Smoke Browser%' OR label LIKE '%smoke%'"); const tempUserIds = selectIds( `SELECT id FROM users WHERE id NOT IN (${placeholders(keep.userIds)}) OR email LIKE 'mfa-%@local.test' OR email LIKE '%smoke%@local.test' OR display_name LIKE '%测试%'`, keep.userIds ); const tempReviewIds = selectIds( `SELECT id FROM reviews WHERE shot_id NOT IN (${placeholders(keep.shotIds)})`, keep.shotIds ); const tempJobIds = selectIds( `SELECT id FROM generation_jobs WHERE id NOT IN (${placeholders(keep.jobIds)}) OR kind IN ('单句 TTS 试听', '回归队列任务', '依赖测试关键帧', '依赖测试任务') OR output_path LIKE 'voices/auditions/%' OR output_path LIKE '%pending-output%'`, keep.jobIds ); const tempAssetIds = selectIds( `SELECT id FROM assets WHERE id NOT IN (${placeholders(keep.assetIds)}) OR name LIKE 'content-regression%' OR name LIKE '%smoke%'`, keep.assetIds ); const tempCompositionIds = selectIds( `SELECT id FROM media_compositions WHERE source_json LIKE '%missing-a.mp4%' OR source_json LIKE '%missing-b.mp4%' OR version LIKE 'local-%'` ); const tempDeliveryIds = selectIds( `SELECT id FROM deliveries WHERE version LIKE 'smoke-%' OR version LIKE 'v-regression%' OR manifest_path LIKE '%smoke%' OR manifest_path LIKE '%regression%'` ); const tempKnowledgeIds = selectIds( `SELECT id FROM knowledge_documents WHERE id NOT IN (${placeholders(keep.knowledgeDocumentIds)}) OR title LIKE '接口验收%'`, keep.knowledgeDocumentIds ); const tempKnowledgePackIds = selectIds( `SELECT id FROM knowledge_context_packs WHERE id NOT IN (${placeholders(keep.knowledgeContextPackIds)}) OR name LIKE '接口验收%' OR name LIKE '回归%'`, keep.knowledgeContextPackIds ); const tempScriptIds = selectIds( `SELECT id FROM script_documents WHERE id NOT IN (${placeholders(keep.scriptIds)}) OR title LIKE '接口验收%' OR title LIKE '自动化验收%' OR title LIKE '回归%'`, keep.scriptIds ); const tempJobOutputPaths = listRelativePathsForJobs(tempJobIds); const backupPath = await backupDatabase(); try { withTransaction(() => { deleteWhere("notification_deliveries", "1 = 1"); deleteExceptIds("user_notifications", keep.notificationIds); deleteExceptIds("usage_events", keep.usageIds); deleteExceptIds("audit_logs", keep.auditIds); deleteWhere("invitations", "1 = 1"); deleteExceptIds("api_clients", keep.apiClientIds); deleteWhere("oidc_login_states", "1 = 1"); deleteWhere("saml_login_states", "1 = 1"); deleteWhere("auth_sso_tickets", "1 = 1"); deleteWhere("auth_mfa_challenges", "1 = 1"); deleteWhere("auth_mfa_enrollment_challenges", "1 = 1"); if (smokeDeviceIds.length) { deleteWhere("auth_sessions", `device_id IN (${placeholders(smokeDeviceIds)})`, smokeDeviceIds); deleteWhere("auth_devices", `id IN (${placeholders(smokeDeviceIds)})`, smokeDeviceIds); } deleteWhere("auth_sessions", "revoked_at IS NOT NULL OR julianday(expires_at) < julianday('now')"); if (tempUserIds.length) { deleteWhere("auth_sessions", `user_id IN (${placeholders(tempUserIds)})`, tempUserIds); deleteWhere("user_mfa_methods", `user_id IN (${placeholders(tempUserIds)})`, tempUserIds); deleteWhere("auth_devices", `user_id IN (${placeholders(tempUserIds)})`, tempUserIds); deleteWhere("organization_members", `user_id IN (${placeholders(tempUserIds)})`, tempUserIds); deleteWhere("workspace_members", `user_id IN (${placeholders(tempUserIds)})`, tempUserIds); deleteWhere("project_members", `user_id IN (${placeholders(tempUserIds)})`, tempUserIds); deleteWhere("user_credentials", `user_id IN (${placeholders(tempUserIds)})`, tempUserIds); deleteWhere("users", `id IN (${placeholders(tempUserIds)})`, tempUserIds); } deleteWhere("auth_security_events", "1 = 1"); deleteWhere("auth_devices", "id NOT IN (SELECT DISTINCT device_id FROM auth_sessions WHERE device_id IS NOT NULL)"); deleteWhere("review_comments", "1 = 1"); if (tempReviewIds.length) deleteWhere("reviews", `id IN (${placeholders(tempReviewIds)})`, tempReviewIds); deleteWhere("delivery_access_feedback", "1 = 1"); deleteWhere("delivery_access_events", "1 = 1"); deleteWhere("delivery_access_links", "1 = 1"); deleteWhere("delivery_batch_items", "1 = 1"); deleteWhere("delivery_batches", "1 = 1"); deleteWhere("delivery_releases", "1 = 1"); if (tempDeliveryIds.length) deleteWhere("deliveries", `id IN (${placeholders(tempDeliveryIds)})`, tempDeliveryIds); else deleteWhere("deliveries", "version LIKE 'smoke-%' OR version LIKE 'v-regression%'"); deleteWhere("media_compositions", "1 = 1"); if (tempJobIds.length) { deleteWhere("job_attempts", `job_id IN (${placeholders(tempJobIds)})`, tempJobIds); deleteWhere( "job_dependencies", `job_id IN (${placeholders(tempJobIds)}) OR depends_on_job_id IN (${placeholders(tempJobIds)})`, [...tempJobIds, ...tempJobIds] ); deleteWhere("generation_jobs", `id IN (${placeholders(tempJobIds)})`, tempJobIds); } deleteWhere("voice_lines", `shot_id NOT IN (${placeholders(keep.shotIds)})`, keep.shotIds); deleteWhere("asset_bindings", `shot_id NOT IN (${placeholders(keep.shotIds)}) OR asset_id NOT IN (${placeholders(keep.assetIds)})`, [...keep.shotIds, ...keep.assetIds]); deleteWhere("shot_versions", `shot_id NOT IN (${placeholders(keep.shotIds)})`, keep.shotIds); deleteWhere("shots", `id NOT IN (${placeholders(keep.shotIds)})`, keep.shotIds); if (tempAssetIds.length) { deleteWhere("asset_versions", `asset_id IN (${placeholders(tempAssetIds)})`, tempAssetIds); deleteWhere("assets", `id IN (${placeholders(tempAssetIds)})`, tempAssetIds); } if (tempKnowledgePackIds.length) deleteWhere("knowledge_context_packs", `id IN (${placeholders(tempKnowledgePackIds)})`, tempKnowledgePackIds); if (tempKnowledgeIds.length) { deleteWhere("knowledge_governance_reviews", `document_id IN (${placeholders(tempKnowledgeIds)})`, tempKnowledgeIds); deleteWhere("knowledge_document_versions", `document_id IN (${placeholders(tempKnowledgeIds)})`, tempKnowledgeIds); deleteWhere("knowledge_chunks", `document_id IN (${placeholders(tempKnowledgeIds)})`, tempKnowledgeIds); deleteWhere("knowledge_documents", `id IN (${placeholders(tempKnowledgeIds)})`, tempKnowledgeIds); } if (tempScriptIds.length) deleteWhere("script_documents", `id IN (${placeholders(tempScriptIds)})`, tempScriptIds); deleteExceptIds("model_routing_policies", keep.modelRouteIds); deleteExceptIds("model_catalog_entries", keep.modelCatalogIds); if (dryRun) throw new DryRunRollback(); }); } catch (error) { if (!(error instanceof DryRunRollback)) throw error; } await removeChildrenByPrefix(resolve(storageRoot, "releases", "thunder-mouth"), "smoke-"); await removeChildrenByPrefix(resolve(storageRoot, "deliveries", "thunder-mouth"), "smoke-"); await removeChildrenByPrefix(resolve(storageRoot, "compositions"), ""); await walkAndRemove(resolve(storageRoot, "assets"), (name) => name.startsWith("content-regression") || name === "smoke-reference.txt"); for (const relativePath of tempJobOutputPaths) { await removePath(resolve(projectRoot, relativePath)); } const report = { dryRun, backupPath, removedTables: summary.db, removedFiles: summary.files.length, sampleFiles: summary.files.slice(0, 10) }; console.log(JSON.stringify(report, null, 2));