#!/usr/bin/env node import { readFile, writeFile } from 'node:fs/promises'; import path from 'node:path'; export const DEFAULT_ORPHAN_WORK_OWNER_USER_ID = 'wx-openid-placeholder'; export const WORK_OWNER_TABLES = [ 'custom_world_profile', 'custom_world_gallery_entry', 'custom_world_session', 'custom_world_agent_session', 'custom_world_draft_card', 'puzzle_agent_session', 'puzzle_work_profile', 'bark_battle_draft_config', 'bark_battle_published_config', 'match3d_agent_session', 'match3d_work_profile', 'jump_hop_agent_session', 'jump_hop_work_profile', 'wooden_fish_agent_session', 'wooden_fish_work_profile', 'square_hole_agent_session', 'square_hole_work_profile', 'visual_novel_agent_session', 'visual_novel_work_profile', 'big_fish_creation_session', ]; const ROW_KEY_FIELDS = ['profile_id', 'work_id', 'session_id', 'draft_id', 'gallery_entry_id', 'id']; if (isCliEntry()) { runCli(process.argv.slice(2)).catch((error) => { console.error( `[rebind-orphan-work-owners] ${error instanceof Error ? error.message : String(error)}`, ); process.exit(1); }); } export function rebindOrphanWorkOwnersInMigration( migration, { placeholderUserId = DEFAULT_ORPHAN_WORK_OWNER_USER_ID, validUserIds = [] } = {}, ) { if (!migration || !Array.isArray(migration.tables)) { throw new Error('迁移 JSON 必须包含 tables 数组。'); } const normalizedPlaceholderUserId = placeholderUserId.trim(); const validUserIdSet = new Set( (Array.isArray(validUserIds) ? validUserIds : []) .map((value) => String(value).trim()) .filter(Boolean), ); validUserIdSet.add(normalizedPlaceholderUserId); const reboundRows = []; for (const table of migration.tables) { if (!table || !WORK_OWNER_TABLES.includes(table.name) || !Array.isArray(table.rows)) { continue; } for (const row of table.rows) { if (!row || typeof row !== 'object') { continue; } const currentOwner = typeof row.owner_user_id === 'string' ? row.owner_user_id.trim() : ''; if (currentOwner === normalizedPlaceholderUserId || validUserIdSet.has(currentOwner)) { continue; } const originalOwner = typeof row.owner_user_id === 'string' ? row.owner_user_id : ''; row.owner_user_id = normalizedPlaceholderUserId; reboundRows.push({ table: table.name, rowKey: resolveRowKey(row), from: originalOwner, to: normalizedPlaceholderUserId, }); } } return { reboundRows, validUserCount: validUserIdSet.size }; } function resolveRowKey(row) { for (const field of ROW_KEY_FIELDS) { const value = row[field]; if (typeof value === 'string' && value.trim()) { return value; } } return ''; } async function runCli(argv) { const options = parseCliArgs(argv); const inputPath = path.resolve(options.in); const outputPath = path.resolve(options.out); const migration = JSON.parse(await readFile(inputPath, 'utf8')); const result = rebindOrphanWorkOwnersInMigration(migration, { placeholderUserId: options.placeholderUserId, validUserIds: collectValidUserIds(migration), }); if (!options.dryRun) { await writeFile(outputPath, `${JSON.stringify(migration, null, 2)}\n`, 'utf8'); } console.log( `[rebind-orphan-work-owners] ${options.dryRun ? 'dry-run' : `已写入 ${outputPath}`},回填 ${result.reboundRows.length} 行`, ); } function parseCliArgs(argv) { const options = { in: '', out: '', placeholderUserId: DEFAULT_ORPHAN_WORK_OWNER_USER_ID, dryRun: false, }; for (let index = 0; index < argv.length; index += 1) { const arg = argv[index]; const readValue = (name) => { const value = argv[index + 1]; if (!value || value.startsWith('--')) { throw new Error(`${name} 缺少参数值。`); } index += 1; return value; }; if (arg === '--in') { options.in = readValue(arg); } else if (arg === '--out') { options.out = readValue(arg); } else if (arg === '--placeholder-user-id') { options.placeholderUserId = readValue(arg); } else if (arg === '--dry-run') { options.dryRun = true; } else { throw new Error(`未知参数: ${arg}`); } } if (!options.in) { throw new Error('必须传入 --in。'); } if (!options.out && !options.dryRun) { throw new Error('非 dry-run 必须传入 --out。'); } return options; } function collectValidUserIds(migration) { const result = new Set(); for (const table of migration.tables ?? []) { if (!table || !Array.isArray(table.rows)) { continue; } if (table.name === 'user_account') { for (const row of table.rows) { if (typeof row?.user_id === 'string' && row.user_id.trim()) { result.add(row.user_id.trim()); } } } } return result; } function isCliEntry() { const entry = process.argv[1]; return entry ? import.meta.url === `file://${entry.replace(/\\/gu, '/')}` : false; }