bookworm-smart-assistant/scripts/auto-backup.js

116 lines
3.5 KiB
JavaScript
Raw Permalink Normal View History

#!/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: 不抛出
}
};