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

116 lines
3.5 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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