feat: expand commercial ai drama platform
This commit is contained in:
@@ -0,0 +1,319 @@
|
||||
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));
|
||||
Reference in New Issue
Block a user