116 lines
3.5 KiB
JavaScript
116 lines
3.5 KiB
JavaScript
|
|
#!/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: 不抛出
|
|||
|
|
}
|
|||
|
|
};
|