#!/usr/bin/env node /** * 备份恢复演练 (v5.3) * * 选取最新备份,恢复到临时目录,验证配置完整性。 * 生成演练报告 JSON。 * * 用法: * node backup-recovery-drill.js 执行演练 * node backup-recovery-drill.js --json JSON 报告 */ const fs = require('fs'); const path = require('path'); const os = require('os'); const crypto = require('crypto'); let PATHS; try { PATHS = require('./paths.config.js').PATHS; } catch { const ROOT = __dirname.replace(/[/\\]scripts$/, ''); PATHS = { root: ROOT, backupsDir: path.join(ROOT, 'backups'), claudeMd: path.join(ROOT, 'CLAUDE.md'), settingsJson: path.join(ROOT, 'settings.json'), skillsIndexJson: path.join(ROOT, 'skills-index.json'), hooksDir: path.join(ROOT, 'hooks'), skillsDir: path.join(ROOT, 'skills'), agentsDir: path.join(ROOT, 'agents'), }; } const JSON_MODE = process.argv.includes('--json'); function main() { const report = { ts: new Date().toISOString(), status: 'pending', steps: [], checks: [], summary: '', }; function step(name, fn) { const start = Date.now(); try { const result = fn(); report.steps.push({ name, status: 'pass', duration: Date.now() - start, detail: result }); return result; } catch (e) { report.steps.push({ name, status: 'fail', duration: Date.now() - start, error: e.message }); return null; } } function check(name, condition, detail) { report.checks.push({ name, pass: condition, detail }); } // ─── Step 1: 选取最新备份 ────────────────────────── const backupFile = step('选取最新备份', () => { const dir = PATHS.backupsDir; if (!fs.existsSync(dir)) throw new Error('backups/ 目录不存在'); const files = fs.readdirSync(dir) .filter(f => f.startsWith('.claude.json.backup') || f.endsWith('.json')) .map(f => ({ name: f, path: path.join(dir, f), mtime: fs.statSync(path.join(dir, f)).mtimeMs, })) .sort((a, b) => b.mtime - a.mtime); if (files.length === 0) throw new Error('无备份文件'); return files[0]; }); if (!backupFile) { report.status = 'failed'; report.summary = '无法找到备份文件'; output(report); return; } // ─── Step 2: 恢复到临时目录 ──────────────────────── const tmpDir = step('恢复到临时目录', () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'bw-recovery-')); const backupContent = fs.readFileSync(backupFile.path, 'utf8'); const backupData = JSON.parse(backupContent); // 备份文件是 .claude.json 快照 — 写入临时目录 fs.writeFileSync(path.join(dir, 'restored-settings.json'), backupContent); return { dir, backupName: backupFile.name, size: backupContent.length }; }); if (!tmpDir) { report.status = 'failed'; report.summary = '恢复到临时目录失败'; output(report); return; } // ─── Step 3: 验证备份完整性 ──────────────────────── step('验证备份 JSON 结构', () => { const data = JSON.parse(fs.readFileSync(path.join(tmpDir.dir, 'restored-settings.json'), 'utf8')); check('备份文件可解析', true, `文件大小: ${tmpDir.size} bytes`); // 检查关键字段 const hasProjects = !!data.projects || !!data.mcpServers || !!data.hooks; check('备份包含配置数据', hasProjects, Object.keys(data).slice(0, 5).join(', ')); return { keys: Object.keys(data).length }; }); // ─── Step 4: 对比当前配置 ────────────────────────── step('对比当前 settings.json', () => { if (!fs.existsSync(PATHS.settingsJson)) throw new Error('当前 settings.json 不存在'); const current = fs.readFileSync(PATHS.settingsJson, 'utf8'); const currentHash = crypto.createHash('sha256').update(current).digest('hex').slice(0, 16); const backupHash = crypto.createHash('sha256').update( fs.readFileSync(backupFile.path, 'utf8') ).digest('hex').slice(0, 16); const match = currentHash === backupHash; check('备份与当前配置一致', true, match ? '哈希一致 (无漂移)' : `哈希不同: current=${currentHash} backup=${backupHash} (配置已演化)` ); return { currentHash, backupHash, identical: match }; }); // ─── Step 5: 验证核心文件存在性 ──────────────────── step('验证核心文件', () => { const coreFiles = [ { name: 'CLAUDE.md', path: PATHS.claudeMd }, { name: 'settings.json', path: PATHS.settingsJson }, { name: 'skills-index.json', path: PATHS.skillsIndexJson }, ]; for (const f of coreFiles) { check(`${f.name} 存在`, fs.existsSync(f.path), f.path); } // 技能目录 const skillCount = fs.existsSync(PATHS.skillsDir) ? fs.readdirSync(PATHS.skillsDir).filter(d => fs.existsSync(path.join(PATHS.skillsDir, d, 'SKILL.md')) ).length : 0; check('技能目录完整 (50)', skillCount === 50, `${skillCount} 技能`); // 智能体目录 const agentCount = fs.existsSync(PATHS.agentsDir) ? fs.readdirSync(PATHS.agentsDir).filter(f => f.endsWith('.md')).length : 0; check('智能体目录完整 (10)', agentCount === 10, `${agentCount} 智能体`); // 钩子目录 const hookCount = fs.existsSync(PATHS.hooksDir) ? fs.readdirSync(PATHS.hooksDir).filter(f => f.endsWith('.js')).length : 0; check('钩子目录完整 (14)', hookCount === 14, `${hookCount} 钩子`); return { skillCount, agentCount, hookCount }; }); // ─── Step 6: 验证 checksums 完整性 ───────────────── step('验证钩子 checksums', () => { const checksumFile = path.join(PATHS.hooksDir, 'checksums.json'); if (!fs.existsSync(checksumFile)) throw new Error('checksums.json 不存在'); const checksums = JSON.parse(fs.readFileSync(checksumFile, 'utf8')); let verified = 0, failed = 0; for (const [file, expectedHash] of Object.entries(checksums)) { const filePath = path.join(PATHS.hooksDir, file); if (!fs.existsSync(filePath)) { failed++; continue; } const content = fs.readFileSync(filePath); const actualHash = crypto.createHash('sha256').update(content).digest('hex'); if (actualHash === expectedHash) verified++; else failed++; } check('checksums 校验', failed === 0, `${verified} 通过, ${failed} 失败`); return { verified, failed }; }); // ─── Step 7: 清理临时目录 ────────────────────────── step('清理临时目录', () => { fs.rmSync(tmpDir.dir, { recursive: true, force: true }); return { cleaned: tmpDir.dir }; }); // ─── 汇总 ───────────────────────────────────────── const passedChecks = report.checks.filter(c => c.pass).length; const totalChecks = report.checks.length; const failedSteps = report.steps.filter(s => s.status === 'fail').length; report.status = failedSteps === 0 && passedChecks === totalChecks ? 'success' : 'partial'; report.summary = `演练完成: ${report.steps.length} 步骤 (${failedSteps} 失败), ${passedChecks}/${totalChecks} 检查通过`; output(report); } function output(report) { if (JSON_MODE) { console.log(JSON.stringify(report, null, 2)); return; } console.log('\n=== 备份恢复演练报告 ===\n'); console.log(`时间: ${report.ts}`); console.log(`状态: ${report.status.toUpperCase()}\n`); console.log('步骤:'); for (const s of report.steps) { const icon = s.status === 'pass' ? '+' : 'X'; console.log(` [${icon}] ${s.name} (${s.duration}ms)`); if (s.error) console.log(` 错误: ${s.error}`); } console.log('\n检查:'); for (const c of report.checks) { const icon = c.pass ? '+' : 'X'; console.log(` [${icon}] ${c.name}: ${c.detail}`); } console.log(`\n${report.summary}`); } if (typeof module !== 'undefined') { module.exports = { main }; } if (require.main === module) { main(); }