#!/usr/bin/env node /** * auto-backup.js — 会话结束自动备份 * * 备份范围: 核心配置 + memory/ + evolution-log (最近200行) * 保留策略: auto-* 保留最近30天,超出自动清理 * fail-open: 任何异常只记录,不阻断 */ 'use strict'; const fs = require('fs'); const path = require('path'); let ROOT; try { ROOT = require('./paths.config.js').PATHS.root; } catch (_) { ROOT = path.join(require('os').homedir(), '.claude'); } const BACKUP_DIR = path.join(ROOT, 'backups'); const LOG_PATH = path.join(ROOT, 'debug', 'auto-backup.log'); const TARGET_FILES = [ 'settings.json', 'CLAUDE.md', 'SKILL-REGISTRY.md', 'feature-flags.json', 'feature-flags.json.sig', 'package.json', 'hooks/checksums.json', 'hooks/checksums.sig', 'config/auto-sync-repos.json', ]; const MIN_INTERVAL_MS = 4 * 60 * 60 * 1000; // 4小时内不重复备份 const RETAIN_DAYS = 30; function log(msg) { try { fs.appendFileSync(LOG_PATH, `${new Date().toISOString()} ${msg}\n`, 'utf8'); } catch (_) {} } function copyFileIfExists(src, dst) { if (!fs.existsSync(src)) return false; fs.mkdirSync(path.dirname(dst), { recursive: true }); fs.copyFileSync(src, dst); return true; } function copyDirRecursive(src, dst) { if (!fs.existsSync(src)) return 0; fs.mkdirSync(dst, { recursive: true }); let count = 0; for (const entry of fs.readdirSync(src, { withFileTypes: true })) { const s = path.join(src, entry.name); const d = path.join(dst, entry.name); if (entry.isDirectory()) { count += copyDirRecursive(s, d); } else { fs.copyFileSync(s, d); count++; } } return count; } module.exports = function runAutoBackup() { try { fs.mkdirSync(BACKUP_DIR, { recursive: true }); // 冷却检查 const existing = fs.readdirSync(BACKUP_DIR) .filter(d => d.startsWith('auto-')) .map(d => fs.statSync(path.join(BACKUP_DIR, d)).mtimeMs) .sort((a, b) => b - a); if (existing.length > 0 && Date.now() - existing[0] < MIN_INTERVAL_MS) { log('[SKIP] recent backup exists within 4h'); return; } const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 16); const dst = path.join(BACKUP_DIR, `auto-${ts}`); fs.mkdirSync(dst, { recursive: true }); // 1. 核心配置文件 let fileCount = 0; for (const f of TARGET_FILES) { if (copyFileIfExists(path.join(ROOT, f), path.join(dst, f))) fileCount++; } // 2. memory/ 全量 const memCount = copyDirRecursive(path.join(ROOT, 'memory'), path.join(dst, 'memory')); fileCount += memCount; // 3. evolution-log 最近200行(不全量,控制体积) const evoSrc = path.join(ROOT, 'evolution-log.jsonl'); if (fs.existsSync(evoSrc)) { const lines = fs.readFileSync(evoSrc, 'utf8').trim().split('\n'); const recent = lines.slice(-200).join('\n'); fs.writeFileSync(path.join(dst, 'evolution-log.jsonl'), recent, 'utf8'); fileCount++; } log(`[OK] ${dst} — ${fileCount} files`); // 4. 清理超期 auto-* 备份(保留最近30天) const cutoff = Date.now() - RETAIN_DAYS * 86400000; const oldDirs = fs.readdirSync(BACKUP_DIR) .filter(d => d.startsWith('auto-')) .filter(d => fs.statSync(path.join(BACKUP_DIR, d)).mtimeMs < cutoff); for (const d of oldDirs) { try { fs.rmSync(path.join(BACKUP_DIR, d), { recursive: true, force: true }); } catch (_) {} } if (oldDirs.length) log(`[CLEANUP] removed ${oldDirs.length} old backups`); } catch (e) { log(`[ERROR] ${e.message}`); // fail-open: 不抛出 } };