#!/usr/bin/env node 'use strict'; const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); const { WORKSPACE_ROOT, outputsRoot } = require('../mcp/src/core/output-paths'); const { readQiweiUid, readQiweiAccountMetadata, safeAccountKey } = require('../mcp/src/core/credentials'); const execute = process.argv.includes('--execute'); const force = process.argv.includes('--force'); const legacyKey = safeAccountKey(readQiweiAccountMetadata().legacyAccountKey); const outputs = outputsRoot(); const messages = process.env.QIWEI_MESSAGES_DIR ? path.resolve(process.env.QIWEI_MESSAGES_DIR) : path.join(WORKSPACE_ROOT, 'messages'); const manifestPath = path.join(outputs, 'dashboard', legacyKey, 'legacy-migration.json'); const uid = readQiweiUid(); const oldWorkbenchKey = uid ? crypto.createHash('sha256').update(uid).digest('hex').slice(0, 16) : ''; const actions = []; const DATE_DIR_RE = /^\d{4}-\d{2}-\d{2}$/; function addCopy(source, target) { if (fs.existsSync(source)) actions.push({ source, target }); } function addDirectoryContents(source, target, include = () => true) { if (!fs.existsSync(source)) return; for (const entry of fs.readdirSync(source, { withFileTypes: true })) { if (entry.name === legacyKey || !include(entry)) continue; if (entry.isDirectory() && /^[a-f0-9]{16}$/i.test(entry.name)) continue; addCopy(path.join(source, entry.name), path.join(target, entry.name)); } } const oldDb = oldWorkbenchKey ? path.join(outputs, 'messages', `agent-workbench-${oldWorkbenchKey}.db`) : path.join(outputs, 'messages', 'agent-workbench.db'); const newDb = path.join(outputs, 'messages', `agent-workbench-${legacyKey}.db`); addCopy(oldDb, newDb); addCopy(`${oldDb}-wal`, `${newDb}-wal`); addCopy(`${oldDb}-shm`, `${newDb}-shm`); if (oldWorkbenchKey) { addCopy( path.join(outputs, 'messages', `claude-code-sessions-${oldWorkbenchKey}.json`), path.join(outputs, 'messages', `claude-code-sessions-${legacyKey}.json`) ); } for (const category of ['customers', 'portraits', 'tags', 'groups']) { addDirectoryContents( path.join(outputs, category), path.join(outputs, category, legacyKey), entry => !DATE_DIR_RE.test(entry.name) && (category !== 'groups' || !/^agent-replies-.*\.json$/.test(entry.name)) ); } for (const category of ['chat', 'memory', 'group']) { addDirectoryContents(path.join(messages, category), path.join(messages, category, legacyKey)); } addCopy( path.join(outputs, 'dashboard', 'dashboard-state.json'), path.join(outputs, 'dashboard', legacyKey, 'dashboard-state.json') ); if (!execute) { console.log(JSON.stringify({ mode: 'dry-run', legacyKey, uid: uid || null, actions }, null, 2)); console.log('No files changed. Re-run with --execute after reviewing the plan.'); process.exit(0); } if (fs.existsSync(manifestPath) && !force) { console.error(`Migration already completed: ${manifestPath}. Use --force only after reviewing existing data.`); process.exit(2); } const results = actions.map(action => { if (fs.existsSync(action.target) && !force) return { ...action, result: 'skipped_target_exists' }; fs.mkdirSync(path.dirname(action.target), { recursive: true }); fs.cpSync(action.source, action.target, { recursive: true, force }); return { ...action, result: 'copied' }; }); fs.mkdirSync(path.dirname(manifestPath), { recursive: true }); fs.writeFileSync(manifestPath, JSON.stringify({ version: 1, migratedAt: new Date().toISOString(), legacyKey, uid: uid || null, results, }, null, 2), 'utf8'); console.log(JSON.stringify({ mode: 'execute', legacyKey, manifestPath, results }, null, 2));