@yottameta/yotta-memory 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -22,7 +22,7 @@ const crypto = require('crypto');
22
22
  const http = require('http');
23
23
  const child_process = require('child_process');
24
24
 
25
- const VERSION = '0.11.0';
25
+ const VERSION = '0.12.0';
26
26
  // MCP 协议(2026-07-28 无状态 + 2025-11-25 legacy 握手,dual-era)
27
27
  const MCP_PROTOCOL_MODERN = '2026-07-28';
28
28
  const MCP_PROTOCOL_LEGACY = '2025-11-25';
@@ -40,11 +40,19 @@ const SERVER_SUBDIR = '.server';
40
40
  const TOKENS_FILE = 'tokens.json';
41
41
  const AGENTS_FILE = 'agents.json';
42
42
  const PROFILE_FILE = 'profile.md';
43
+ const BACKUP_TASK_NAME = 'YottaMemoryBackup';
44
+ const BACKUP_SYSTEMD_SERVICE = 'yotta-memory-backup.service';
45
+ const BACKUP_SYSTEMD_TIMER = 'yotta-memory-backup.timer';
46
+ const BACKUP_CRON_MARKER = '#YTM_BACKUP:yotta-memory-backup';
47
+ const BACKUP_LAUNCHD_LABEL = 'cn.yottameta.yotta-memory.backup';
43
48
  // remember 类型启发式提示关键词(statement 含主观/关系词且 type=FACT 时提示改 PREF,仅提示不拦截)
44
49
  const HINT_KEYWORDS = ['用户', '偏好', '喜欢', '关系', '称呼', '本人', '希望', '讨厌', '欣赏', '习惯', '忌讳', '介意', '不要', '别用'];
45
50
 
46
51
  // ---- 全局配置(记忆库位置持久化,固定 ~/.yottamemory/config.json)----
47
52
  function configPath() {
53
+ if (process.env.YOTTA_MEMORY_CONFIG_DIR) {
54
+ return path.join(path.resolve(process.env.YOTTA_MEMORY_CONFIG_DIR), CONFIG_FILE);
55
+ }
48
56
  return path.join(os.homedir(), '.yottamemory', CONFIG_FILE);
49
57
  }
50
58
  function loadConfig() {
@@ -1102,10 +1110,25 @@ function verifyWrittenReadable(root, rel, subj, agent) {
1102
1110
  }
1103
1111
 
1104
1112
  // ---- 命令 core(CLI 与 MCP 共用;返回 { error, exitCode, text },不 process.exit)----
1113
+ function isExistingStore(root) {
1114
+ return ['facts', 'private', 'keys', 'agents.json', 'index.json'].some(function (name) {
1115
+ return fs.existsSync(path.join(root, name));
1116
+ });
1117
+ }
1105
1118
  function initCore(opts) {
1106
1119
  opts = opts || {};
1107
1120
  const root = opts.dir ? path.resolve(String(opts.dir)) : (opts.project ? projectRoot() : userRoot());
1108
- const fresh = !fs.existsSync(path.join(root, PUBLIC_DIR)) && !fs.existsSync(path.join(root, PRIVATE_DIR)) && !fs.existsSync(path.join(root, INDEX_FILE));
1121
+ const existing = isExistingStore(root);
1122
+ if (existing && opts.attach) {
1123
+ return { error: false, text: '已接入现有记忆库: ' + root };
1124
+ }
1125
+ if (existing && opts.force) {
1126
+ return { error: true, text: '拒绝: --force 重新初始化必须先做完整备份;当前版本尚未实现 backup,不能覆盖现有记忆库。请保留现场并先完成备份机制。' };
1127
+ }
1128
+ if (existing) {
1129
+ return { error: true, text: '拒绝: 目标已是现有记忆库: ' + root + '。如只是接入,请使用 init --attach;不要执行覆盖初始化。' };
1130
+ }
1131
+ const fresh = true;
1109
1132
  ensureInit(root);
1110
1133
  const wantEncrypt = opts.encrypt || (!opts.noEncrypt && fresh);
1111
1134
  if (wantEncrypt) {
@@ -1118,6 +1141,919 @@ function initCore(opts) {
1118
1141
  }
1119
1142
  return { error: false, text: '已初始化记忆库: ' + root + (isEncrypted(root) ? '(已启用加密)' : '(明文;建议 init --encrypt 或 migrate 启用加密)') };
1120
1143
  }
1144
+
1145
+ function backupDestination(opts) {
1146
+ opts = opts || {};
1147
+ const configured = opts.dir || process.env.YOTTA_MEMORY_BACKUP_DIR || loadConfig().backup_dir || '';
1148
+ return configured ? path.resolve(String(configured)) : null;
1149
+ }
1150
+ function sameVolume(left, right) {
1151
+ return path.parse(path.resolve(left)).root.toLowerCase() === path.parse(path.resolve(right)).root.toLowerCase();
1152
+ }
1153
+ function platformPathApi(platform) {
1154
+ return platform === 'win32' ? path.win32 : path.posix;
1155
+ }
1156
+ function normalizeVolumeCandidate(candidate, platform) {
1157
+ const p = platformPathApi(platform);
1158
+ const value = String(candidate || '').trim();
1159
+ if (!value) return '';
1160
+ if (platform === 'win32') {
1161
+ const drive = /^([A-Za-z]):[\\/]*$/.exec(value);
1162
+ if (drive) return drive[1].toUpperCase() + ':\\';
1163
+ }
1164
+ return p.resolve(value);
1165
+ }
1166
+ function sameVolumeOnPlatform(left, right, platform, statFn) {
1167
+ if (platform === 'win32') {
1168
+ return path.win32.parse(left).root.toLowerCase() === path.win32.parse(right).root.toLowerCase();
1169
+ }
1170
+ try {
1171
+ return statFn(left).dev === statFn(right).dev;
1172
+ } catch (_) {
1173
+ return left === right;
1174
+ }
1175
+ }
1176
+ function defaultVolumeCandidates(platform) {
1177
+ if (platform === 'win32') {
1178
+ const out = [];
1179
+ for (let i = 65; i <= 90; i++) {
1180
+ const root = String.fromCharCode(i) + ':\\';
1181
+ if (fs.existsSync(root)) out.push(root);
1182
+ }
1183
+ return out;
1184
+ }
1185
+ try {
1186
+ const output = child_process.execFileSync('df', ['-Pk'], {
1187
+ encoding: 'utf8',
1188
+ timeout: 3000,
1189
+ stdio: ['ignore', 'pipe', 'ignore'],
1190
+ });
1191
+ const excluded = /^\/(proc|sys|dev|run|snap|boot)(\/|$)/;
1192
+ return output.split(/\r?\n/).slice(1).map(function (line) {
1193
+ const parts = line.trim().split(/\s+/);
1194
+ return parts.length >= 6 ? parts[parts.length - 1] : '';
1195
+ }).filter(function (mount) {
1196
+ return mount && mount.charAt(0) === '/' && !excluded.test(mount);
1197
+ });
1198
+ } catch (_) {
1199
+ return [];
1200
+ }
1201
+ }
1202
+ function formatBytes(bytes) {
1203
+ const value = Number(bytes) || 0;
1204
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
1205
+ let n = value;
1206
+ let unit = 0;
1207
+ while (n >= 1024 && unit < units.length - 1) { n /= 1024; unit++; }
1208
+ return (unit === 0 ? Math.round(n) : n.toFixed(1)) + ' ' + units[unit];
1209
+ }
1210
+ function listBackupVolumesCore(opts) {
1211
+ opts = opts || {};
1212
+ const platform = opts.platform || process.platform;
1213
+ const p = platformPathApi(platform);
1214
+ const rootForStore = opts.root || userRoot();
1215
+ const storeRoot = normalizeVolumeCandidate(rootForStore, platform);
1216
+ const candidates = opts.candidates || defaultVolumeCandidates(platform);
1217
+ const existsFn = opts.existsFn || fs.existsSync;
1218
+ const statFn = opts.statFn || fs.statSync;
1219
+ const statfsFn = opts.statfsFn || fs.statfsSync;
1220
+ const accessFn = opts.accessFn || fs.accessSync;
1221
+ const volumes = [];
1222
+ const seen = new Set();
1223
+ for (const candidate of candidates) {
1224
+ const normalized = normalizeVolumeCandidate(candidate, platform);
1225
+ if (!normalized || seen.has(normalized)) continue;
1226
+ seen.add(normalized);
1227
+ try {
1228
+ if (!existsFn(normalized)) continue;
1229
+ const stat = statFn(normalized);
1230
+ if (!stat || !stat.isDirectory()) continue;
1231
+ accessFn(normalized, fs.constants.W_OK);
1232
+ if (sameVolumeOnPlatform(storeRoot, normalized, platform, statFn)) continue;
1233
+ const usage = statfsFn(normalized);
1234
+ const blockSize = Number(usage && usage.bsize) || 0;
1235
+ const availableBlocks = Number(usage && usage.bavail) || 0;
1236
+ const totalBlocks = Number(usage && usage.blocks) || 0;
1237
+ volumes.push({
1238
+ root: normalized,
1239
+ writable: true,
1240
+ sameVolume: false,
1241
+ free: blockSize * availableBlocks,
1242
+ total: blockSize * totalBlocks,
1243
+ });
1244
+ } catch (_) {
1245
+ // A mounted path that is not currently accessible is not a valid recommendation.
1246
+ }
1247
+ }
1248
+ volumes.sort(function (a, b) { return String(a.root).localeCompare(String(b.root)); });
1249
+ const lines = volumes.length
1250
+ ? ['可用的独立卷(排除记忆库所在卷 ' + storeRoot + '):'].concat(volumes.map(function (v) {
1251
+ return '- ' + v.root + ' 可用 ' + formatBytes(v.free);
1252
+ }))
1253
+ : ['未发现可写的独立卷(已排除记忆库所在卷 ' + storeRoot + ');请接入另一块磁盘或独立分区后重试。'];
1254
+ return { error: false, volumes: volumes, text: lines.join('\n') };
1255
+ }
1256
+ function validBackupTime(value) {
1257
+ return /^([01]\d|2[0-3]):([0-5]\d)$/.test(String(value || ''));
1258
+ }
1259
+ function backupTimeParts(value) {
1260
+ const text = String(value || '');
1261
+ if (!validBackupTime(text)) throw new Error('备份时间格式无效: ' + text + '(应为 HH:MM)');
1262
+ const parts = text.split(':');
1263
+ return { hour: parseInt(parts[0], 10), minute: parseInt(parts[1], 10), text: text };
1264
+ }
1265
+ function xmlEscape(value) {
1266
+ return String(value === undefined || value === null ? '' : value).replace(/[&<>"']/g, function (ch) {
1267
+ return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&apos;' }[ch];
1268
+ });
1269
+ }
1270
+ function backupWindowsTaskXml(opts) {
1271
+ opts = opts || {};
1272
+ const time = backupTimeParts(opts.time || '03:30');
1273
+ const nodePath = opts.nodePath || process.execPath;
1274
+ const scriptPath = opts.scriptPath || __filename;
1275
+ const userId = opts.userId || '';
1276
+ const principal = userId
1277
+ ? '<Principal id="Author"><UserId>' + xmlEscape(userId) + '</UserId><LogonType>InteractiveToken</LogonType><RunLevel>LeastPrivilege</RunLevel></Principal>'
1278
+ : '<Principal id="Author"><LogonType>InteractiveToken</LogonType><RunLevel>LeastPrivilege</RunLevel></Principal>';
1279
+ return [
1280
+ '<?xml version="1.0" encoding="UTF-16"?>',
1281
+ '<Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">',
1282
+ '<RegistrationInfo><Description>Yotta Memory daily backup</Description></RegistrationInfo>',
1283
+ '<Triggers><CalendarTrigger><StartBoundary>2026-01-01T' + time.text + ':00</StartBoundary><Enabled>true</Enabled><ScheduleByDay><DaysInterval>1</DaysInterval></ScheduleByDay></CalendarTrigger></Triggers>',
1284
+ '<Principals>' + principal + '</Principals>',
1285
+ '<Settings>',
1286
+ '<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>',
1287
+ '<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>',
1288
+ '<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>',
1289
+ '<AllowHardTerminate>true</AllowHardTerminate>',
1290
+ '<StartWhenAvailable>true</StartWhenAvailable>',
1291
+ '<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>',
1292
+ '<IdleSettings><StopOnIdleEnd>false</StopOnIdleEnd><RestartOnIdle>false</RestartOnIdle></IdleSettings>',
1293
+ '<Enabled>true</Enabled>',
1294
+ '<Hidden>false</Hidden>',
1295
+ '<RunOnlyIfIdle>false</RunOnlyIfIdle>',
1296
+ '<WakeToRun>false</WakeToRun>',
1297
+ '<ExecutionTimeLimit>PT2H</ExecutionTimeLimit>',
1298
+ '<Priority>7</Priority>',
1299
+ '</Settings>',
1300
+ '<Actions Context="Author"><Exec><Command>' + xmlEscape(nodePath) + '</Command><Arguments>"' + xmlEscape(scriptPath) + '" backup ensure-daily</Arguments></Exec></Actions>',
1301
+ '</Task>',
1302
+ ].join('\r\n');
1303
+ }
1304
+ function backupSystemdServiceContent(opts) {
1305
+ opts = opts || {};
1306
+ const nodePath = opts.nodePath || process.execPath;
1307
+ const scriptPath = opts.scriptPath || __filename;
1308
+ return [
1309
+ '[Unit]',
1310
+ 'Description=Yotta Memory daily backup',
1311
+ '',
1312
+ '[Service]',
1313
+ 'Type=oneshot',
1314
+ 'ExecStart=' + systemdEscapeArg(nodePath) + ' ' + systemdEscapeArg(scriptPath) + ' backup ensure-daily',
1315
+ 'Nice=10',
1316
+ '',
1317
+ ].join('\n');
1318
+ }
1319
+ function backupSystemdTimerContent(opts) {
1320
+ opts = opts || {};
1321
+ const time = backupTimeParts(opts.time || '03:30');
1322
+ return [
1323
+ '[Unit]',
1324
+ 'Description=Yotta Memory daily backup timer',
1325
+ '',
1326
+ '[Timer]',
1327
+ 'OnCalendar=*-*-* ' + time.text + ':00',
1328
+ 'Persistent=true',
1329
+ 'RandomizedDelaySec=300',
1330
+ '',
1331
+ '[Install]',
1332
+ 'WantedBy=timers.target',
1333
+ '',
1334
+ ].join('\n');
1335
+ }
1336
+ function backupCronLine(opts) {
1337
+ opts = opts || {};
1338
+ const time = backupTimeParts(opts.time || '03:30');
1339
+ const nodePath = opts.nodePath || process.execPath;
1340
+ const scriptPath = opts.scriptPath || __filename;
1341
+ const logPath = opts.logPath || '';
1342
+ const redirect = logPath ? ' >> ' + shQuote(logPath) + ' 2>&1' : '';
1343
+ return time.minute + ' ' + time.hour + ' * * * ' + shQuote(nodePath) + ' ' + shQuote(scriptPath) + ' backup ensure-daily' + redirect + ' ' + BACKUP_CRON_MARKER;
1344
+ }
1345
+ function backupLaunchdPlist(opts) {
1346
+ opts = opts || {};
1347
+ const time = backupTimeParts(opts.time || '03:30');
1348
+ const nodePath = opts.nodePath || process.execPath;
1349
+ const scriptPath = opts.scriptPath || __filename;
1350
+ const logPath = opts.logPath || '';
1351
+ const lines = [
1352
+ '<?xml version="1.0" encoding="UTF-8"?>',
1353
+ '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
1354
+ '<plist version="1.0"><dict>',
1355
+ '<key>Label</key><string>' + BACKUP_LAUNCHD_LABEL + '</string>',
1356
+ '<key>ProgramArguments</key><array>',
1357
+ '<string>' + xmlEscape(nodePath) + '</string>',
1358
+ '<string>' + xmlEscape(scriptPath) + '</string>',
1359
+ '<string>backup</string>',
1360
+ '<string>ensure-daily</string>',
1361
+ '</array>',
1362
+ '<key>StartCalendarInterval</key><dict>',
1363
+ '<key>Hour</key><integer>' + time.hour + '</integer>',
1364
+ '<key>Minute</key><integer>' + time.minute + '</integer>',
1365
+ '</dict>',
1366
+ ];
1367
+ if (logPath) {
1368
+ lines.push('<key>StandardOutPath</key><string>' + xmlEscape(logPath) + '</string>');
1369
+ lines.push('<key>StandardErrorPath</key><string>' + xmlEscape(logPath) + '</string>');
1370
+ }
1371
+ lines.push('</dict></plist>');
1372
+ return lines.join('\n');
1373
+ }
1374
+ function schedulerExec(opts, command, args, input) {
1375
+ if (opts && typeof opts.execFileFn === 'function') return opts.execFileFn(command, args, { input: input });
1376
+ return child_process.execFileSync(command, args, {
1377
+ encoding: 'utf8',
1378
+ timeout: 15000,
1379
+ input: input,
1380
+ stdio: [input === undefined ? 'ignore' : 'pipe', 'pipe', 'pipe'],
1381
+ });
1382
+ }
1383
+ function schedulerTry(opts, command, args, input) {
1384
+ try {
1385
+ return { ok: true, stdout: String(schedulerExec(opts, command, args, input) || '') };
1386
+ } catch (error) {
1387
+ return {
1388
+ ok: false,
1389
+ stdout: String((error && error.stdout) || ''),
1390
+ error: String((error && error.message) || error),
1391
+ };
1392
+ }
1393
+ }
1394
+ function backupScheduleEnableCore(opts) {
1395
+ opts = opts || {};
1396
+ const platform = opts.platform || process.platform;
1397
+ const time = opts.time || '03:30';
1398
+ try { backupTimeParts(time); } catch (error) { return { error: true, text: error.message }; }
1399
+ const nodePath = opts.nodePath || process.execPath;
1400
+ const scriptPath = opts.scriptPath || __filename;
1401
+ try {
1402
+ if (platform === 'win32') {
1403
+ const userId = opts.userId || (process.env.USERDOMAIN && process.env.USERNAME ? process.env.USERDOMAIN + '\\' + process.env.USERNAME : '');
1404
+ const taskName = opts.taskName || BACKUP_TASK_NAME;
1405
+ const xmlPath = opts.xmlPath || path.join(os.tmpdir(), taskName + '.xml');
1406
+ fs.writeFileSync(xmlPath, '\ufeff' + backupWindowsTaskXml({ time: time, nodePath: nodePath, scriptPath: scriptPath, userId: userId }), 'utf16le');
1407
+ schedulerExec(opts, 'schtasks', ['/create', '/tn', taskName, '/xml', xmlPath, '/f']);
1408
+ return { error: false, scheduler: 'windows-task', taskName: taskName, text: '已注册 Windows 每日备份任务: ' + taskName };
1409
+ }
1410
+ if (platform === 'linux') {
1411
+ const unitDir = opts.unitDir || path.join(os.homedir(), '.config', 'systemd', 'user');
1412
+ const servicePath = path.join(unitDir, BACKUP_SYSTEMD_SERVICE);
1413
+ const timerPath = path.join(unitDir, BACKUP_SYSTEMD_TIMER);
1414
+ fs.mkdirSync(unitDir, { recursive: true });
1415
+ fs.writeFileSync(servicePath, backupSystemdServiceContent({ nodePath: nodePath, scriptPath: scriptPath }), 'utf8');
1416
+ fs.writeFileSync(timerPath, backupSystemdTimerContent({ time: time }), 'utf8');
1417
+ const reload = schedulerTry(opts, 'systemctl', ['--user', 'daemon-reload']);
1418
+ const enable = reload.ok ? schedulerTry(opts, 'systemctl', ['--user', 'enable', '--now', BACKUP_SYSTEMD_TIMER]) : reload;
1419
+ if (enable.ok) {
1420
+ return { error: false, scheduler: 'linux-systemd', unit: BACKUP_SYSTEMD_TIMER, text: '已注册 systemd 用户定时器: ' + BACKUP_SYSTEMD_TIMER };
1421
+ }
1422
+ const logPath = opts.logPath || path.join(os.homedir(), '.yottamemory', 'backup.log');
1423
+ const line = backupCronLine({ time: time, nodePath: nodePath, scriptPath: scriptPath, logPath: logPath });
1424
+ const current = schedulerTry(opts, 'crontab', ['-l']);
1425
+ const kept = current.stdout.split(/\r?\n/).filter(function (item) {
1426
+ return item && item.indexOf(BACKUP_CRON_MARKER) === -1;
1427
+ });
1428
+ kept.push(line);
1429
+ schedulerExec(opts, 'crontab', ['-'], kept.join('\n') + '\n');
1430
+ return {
1431
+ error: false,
1432
+ scheduler: 'linux-cron',
1433
+ unit: 'crontab',
1434
+ warning: 'systemd 用户定时器不可用,已降级用户 crontab: ' + enable.error,
1435
+ text: '已注册用户 crontab 每日备份;systemd 不可用。',
1436
+ };
1437
+ }
1438
+ if (platform === 'darwin') {
1439
+ const dir = opts.launchAgentDir || path.join(os.homedir(), 'Library', 'LaunchAgents');
1440
+ const plistPath = path.join(dir, BACKUP_LAUNCHD_LABEL + '.plist');
1441
+ fs.mkdirSync(dir, { recursive: true });
1442
+ fs.writeFileSync(plistPath, backupLaunchdPlist({
1443
+ time: time,
1444
+ nodePath: nodePath,
1445
+ scriptPath: scriptPath,
1446
+ logPath: opts.logPath || path.join(os.homedir(), 'Library', 'Logs', 'yotta-memory-backup.log'),
1447
+ }), 'utf8');
1448
+ const uid = typeof process.getuid === 'function' ? process.getuid() : '';
1449
+ const bootstrap = schedulerTry(opts, 'launchctl', ['bootstrap', 'gui/' + uid, plistPath]);
1450
+ if (!bootstrap.ok) schedulerTry(opts, 'launchctl', ['load', '-w', plistPath]);
1451
+ return { error: false, scheduler: 'macos-launchd', plist: plistPath, text: '已注册 macOS LaunchAgent 每日备份。' };
1452
+ }
1453
+ } catch (error) {
1454
+ return { error: true, text: '注册自动备份调度失败: ' + error.message };
1455
+ }
1456
+ return { error: true, text: '当前平台暂不支持自动备份调度: ' + platform };
1457
+ }
1458
+ function backupScheduleDisableCore(opts) {
1459
+ opts = opts || {};
1460
+ const platform = opts.platform || process.platform;
1461
+ try {
1462
+ if (platform === 'win32') {
1463
+ const taskName = opts.taskName || BACKUP_TASK_NAME;
1464
+ const result = schedulerTry(opts, 'schtasks', ['/delete', '/tn', taskName, '/f']);
1465
+ if (!result.ok && !/cannot find|找不到|不存在/i.test(result.error)) return { error: true, text: result.error };
1466
+ return { error: false, scheduler: 'windows-task', text: result.ok ? '已移除 Windows 备份任务。' : 'Windows 备份任务未注册。' };
1467
+ }
1468
+ if (platform === 'linux') {
1469
+ schedulerTry(opts, 'systemctl', ['--user', 'disable', '--now', BACKUP_SYSTEMD_TIMER]);
1470
+ const unitDir = opts.unitDir || path.join(os.homedir(), '.config', 'systemd', 'user');
1471
+ for (const name of [BACKUP_SYSTEMD_SERVICE, BACKUP_SYSTEMD_TIMER]) {
1472
+ try { fs.unlinkSync(path.join(unitDir, name)); } catch (_) {}
1473
+ }
1474
+ const current = schedulerTry(opts, 'crontab', ['-l']);
1475
+ const kept = current.stdout.split(/\r?\n/).filter(function (item) {
1476
+ return item && item.indexOf(BACKUP_CRON_MARKER) === -1;
1477
+ });
1478
+ schedulerTry(opts, 'crontab', ['-'], kept.join('\n') + (kept.length ? '\n' : ''));
1479
+ return { error: false, scheduler: 'linux', text: '已移除 systemd 定时器与备份 cron 行。' };
1480
+ }
1481
+ if (platform === 'darwin') {
1482
+ const dir = opts.launchAgentDir || path.join(os.homedir(), 'Library', 'LaunchAgents');
1483
+ const plistPath = path.join(dir, BACKUP_LAUNCHD_LABEL + '.plist');
1484
+ const uid = typeof process.getuid === 'function' ? process.getuid() : '';
1485
+ schedulerTry(opts, 'launchctl', ['bootout', 'gui/' + uid, plistPath]);
1486
+ try { fs.unlinkSync(plistPath); } catch (_) {}
1487
+ return { error: false, scheduler: 'macos-launchd', text: '已移除 macOS LaunchAgent 备份。' };
1488
+ }
1489
+ } catch (error) {
1490
+ return { error: true, text: '移除自动备份调度失败: ' + error.message };
1491
+ }
1492
+ return { error: true, text: '当前平台暂不支持自动备份调度: ' + platform };
1493
+ }
1494
+ function backupScheduleStatusCore(opts) {
1495
+ opts = opts || {};
1496
+ const platform = opts.platform || process.platform;
1497
+ if (platform === 'win32') {
1498
+ const taskName = opts.taskName || BACKUP_TASK_NAME;
1499
+ const result = schedulerTry(opts, 'schtasks', ['/query', '/tn', taskName, '/fo', 'LIST']);
1500
+ return { error: false, registered: result.ok, scheduler: 'windows-task', text: result.ok ? 'Windows 备份任务已注册。' : 'Windows 备份任务未注册。' };
1501
+ }
1502
+ if (platform === 'linux') {
1503
+ const unitDir = opts.unitDir || path.join(os.homedir(), '.config', 'systemd', 'user');
1504
+ const timer = fs.existsSync(path.join(unitDir, BACKUP_SYSTEMD_TIMER));
1505
+ let cron = false;
1506
+ try {
1507
+ cron = schedulerTry(opts, 'crontab', ['-l']).stdout.indexOf(BACKUP_CRON_MARKER) !== -1;
1508
+ } catch (_) {}
1509
+ return { error: false, registered: timer || cron, scheduler: timer ? 'linux-systemd' : (cron ? 'linux-cron' : ''), text: timer || cron ? 'Linux 备份定时器已注册。' : 'Linux 备份定时器未注册。' };
1510
+ }
1511
+ if (platform === 'darwin') {
1512
+ const dir = opts.launchAgentDir || path.join(os.homedir(), 'Library', 'LaunchAgents');
1513
+ const registered = fs.existsSync(path.join(dir, BACKUP_LAUNCHD_LABEL + '.plist'));
1514
+ return { error: false, registered: registered, scheduler: 'macos-launchd', text: registered ? 'macOS 备份 LaunchAgent 已注册。' : 'macOS 备份 LaunchAgent 未注册。' };
1515
+ }
1516
+ return { error: false, registered: false, scheduler: '', text: '当前平台不支持自动备份调度: ' + platform };
1517
+ }
1518
+ function backupFallbackCommand() {
1519
+ return { command: process.execPath, args: [__filename, 'backup', 'ensure-daily'] };
1520
+ }
1521
+ function startBackupFallback() {
1522
+ const cfg = loadConfig();
1523
+ if (!cfg.backup_enabled) return;
1524
+ const launch = function () {
1525
+ try {
1526
+ const spec = backupFallbackCommand();
1527
+ const child = child_process.spawn(spec.command, spec.args, {
1528
+ detached: true,
1529
+ stdio: 'ignore',
1530
+ windowsHide: true,
1531
+ });
1532
+ child.unref();
1533
+ } catch (error) {
1534
+ console.error('备份补跑启动失败: ' + error.message);
1535
+ }
1536
+ };
1537
+ const initial = setTimeout(launch, 10000);
1538
+ if (initial.unref) initial.unref();
1539
+ const interval = setInterval(launch, 6 * 60 * 60 * 1000);
1540
+ if (interval.unref) interval.unref();
1541
+ }
1542
+ function backupSetupCore(opts) {
1543
+ opts = opts || {};
1544
+ const root = path.resolve(opts.root || userRoot());
1545
+ if (!isExistingStore(root)) return { error: true, text: '记忆库不存在或未初始化: ' + root };
1546
+ const cfg = loadConfig();
1547
+ const nowIso = new Date().toISOString();
1548
+ if (opts.manual) {
1549
+ cfg.backup_enabled = false;
1550
+ cfg.backup_schedule = 'manual';
1551
+ cfg.backup_setup_choice = 'manual';
1552
+ cfg.backup_setup_at = nowIso;
1553
+ delete cfg.backup_dir;
1554
+ saveConfig(cfg);
1555
+ return {
1556
+ error: false,
1557
+ configured: false,
1558
+ manual: true,
1559
+ text: '已记录:用户选择手动备份。未注册自动备份,也不会在会话开工时反复提示。',
1560
+ };
1561
+ }
1562
+ if (!opts.dir) return { error: true, text: '缺少备份目录 --dir <目录>。请先运行 backup volumes,只从实际枚举结果中选择。' };
1563
+ const dir = path.resolve(String(opts.dir));
1564
+ const same = opts.sameVolumeFn ? !!opts.sameVolumeFn(root, dir) : sameVolume(root, dir);
1565
+ if (same) return { error: true, text: '拒绝: 备份目录与记忆库在同一卷,请选择实际存在的独立卷。' };
1566
+ const time = String(opts.time || '03:30');
1567
+ if (!validBackupTime(time)) return { error: true, text: '备份时间格式无效: ' + time + '(应为 HH:MM,例如 03:30)' };
1568
+ try {
1569
+ fs.mkdirSync(dir, { recursive: true });
1570
+ fs.accessSync(dir, fs.constants.W_OK);
1571
+ } catch (error) {
1572
+ return { error: true, text: '备份目录不可写: ' + dir + '(' + error.message + ')' };
1573
+ }
1574
+ const created = backupCreateCore({
1575
+ root: root,
1576
+ dir: dir,
1577
+ allowSameVolume: !!opts.allowSameVolume,
1578
+ });
1579
+ if (created.error) return created;
1580
+ cfg.backup_dir = dir;
1581
+ cfg.backup_enabled = true;
1582
+ cfg.backup_schedule = 'daily';
1583
+ cfg.backup_time = time;
1584
+ cfg.backup_max_age_hours = opts.maxAgeHours !== undefined ? opts.maxAgeHours : 36;
1585
+ cfg.backup_setup_choice = 'daily';
1586
+ cfg.backup_setup_at = nowIso;
1587
+ cfg.backup_last_success = created.manifest.created;
1588
+ cfg.backup_last_backup_id = created.id;
1589
+ delete cfg.backup_last_error;
1590
+ saveConfig(cfg);
1591
+ let schedule = null;
1592
+ if (!opts.skipSchedule && typeof backupScheduleEnableCore === 'function') {
1593
+ schedule = backupScheduleEnableCore({ dir: dir, time: time, root: root });
1594
+ if (schedule && schedule.error) {
1595
+ cfg.backup_scheduler_error = schedule.text;
1596
+ delete cfg.backup_scheduler;
1597
+ } else if (schedule) {
1598
+ cfg.backup_scheduler = schedule.scheduler || '';
1599
+ delete cfg.backup_scheduler_error;
1600
+ }
1601
+ saveConfig(cfg);
1602
+ }
1603
+ return {
1604
+ error: false,
1605
+ configured: true,
1606
+ id: created.id,
1607
+ dir: dir,
1608
+ time: time,
1609
+ schedule: schedule,
1610
+ text: '已启用每日自动备份: ' + dir + '(每天 ' + time + ')\n首份备份: ' + created.id
1611
+ + (schedule && schedule.error ? '\n[警告] ' + schedule.text + ';serve 补跑仍会在启动后检查。' : ''),
1612
+ };
1613
+ }
1614
+ function backupStatusCore(opts) {
1615
+ opts = opts || {};
1616
+ const root = path.resolve(opts.root || userRoot());
1617
+ const cfg = loadConfig();
1618
+ const dir = cfg.backup_dir ? path.resolve(String(cfg.backup_dir)) : '';
1619
+ const maxAge = Number(cfg.backup_max_age_hours) || 36;
1620
+ if (!dir) {
1621
+ const manual = cfg.backup_setup_choice === 'manual';
1622
+ return {
1623
+ error: false,
1624
+ configured: false,
1625
+ enabled: false,
1626
+ healthy: false,
1627
+ manual: manual,
1628
+ text: manual
1629
+ ? '备份状态: 手动模式(未配置自动备份)'
1630
+ : '备份状态: 未配置\n请运行 backup volumes,只从实际枚举结果中选择独立卷,并由用户确认后再运行 backup setup。',
1631
+ };
1632
+ }
1633
+ const listed = backupListCore({ dir: dir });
1634
+ const latest = listed.backups[0] || null;
1635
+ const now = opts.now ? new Date(opts.now).getTime() : Date.now();
1636
+ const createdTs = latest && latest.created ? new Date(latest.created).getTime() : NaN;
1637
+ const ageHours = isNaN(createdTs) ? null : (now - createdTs) / 3600000;
1638
+ const fresh = ageHours !== null && ageHours <= maxAge;
1639
+ const same = opts.sameVolumeFn ? !!opts.sameVolumeFn(root, dir) : sameVolume(root, dir);
1640
+ const independent = !same;
1641
+ const healthy = !!latest && fresh && independent && !cfg.backup_last_error && !cfg.backup_scheduler_error;
1642
+ const lines = [
1643
+ '备份状态: ' + (cfg.backup_enabled ? '已启用' : '已关闭'),
1644
+ '- 备份目录: ' + dir,
1645
+ '- 独立卷: ' + (independent ? '是' : '否(不可作为可靠备份)'),
1646
+ '- 计划: ' + (cfg.backup_schedule || 'manual') + (cfg.backup_time ? ' ' + cfg.backup_time : ''),
1647
+ '- 调度器: ' + (cfg.backup_scheduler || '(未注册)'),
1648
+ '- 调度异常: ' + (cfg.backup_scheduler_error || '(无)'),
1649
+ '- 上次成功: ' + (cfg.backup_last_success || (latest && latest.created) || '(无)'),
1650
+ '- 备份年龄: ' + (ageHours === null ? '(无可用备份)' : ageHours.toFixed(1) + ' 小时'),
1651
+ '- 最近失败: ' + (cfg.backup_last_error || '(无)'),
1652
+ '- 健康: ' + (healthy ? '正常' : '异常'),
1653
+ ];
1654
+ return {
1655
+ error: false,
1656
+ configured: true,
1657
+ enabled: !!cfg.backup_enabled,
1658
+ healthy: healthy,
1659
+ latest: latest,
1660
+ ageHours: ageHours,
1661
+ text: lines.join('\n'),
1662
+ };
1663
+ }
1664
+ function localDateKey(value) {
1665
+ const d = value instanceof Date ? value : new Date(value);
1666
+ const p = (n) => String(n).padStart(2, '0');
1667
+ return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate());
1668
+ }
1669
+ function latestBackupByCreated(backups) {
1670
+ return (backups || []).slice().sort(function (a, b) {
1671
+ return String(b.created || '').localeCompare(String(a.created || ''));
1672
+ })[0] || null;
1673
+ }
1674
+ function acquireBackupLock(lockPath, now, staleHours) {
1675
+ const nowMs = now instanceof Date ? now.getTime() : new Date(now).getTime();
1676
+ const maxAgeMs = (staleHours || 6) * 3600000;
1677
+ try {
1678
+ fs.mkdirSync(path.dirname(lockPath), { recursive: true });
1679
+ fs.writeFileSync(lockPath, JSON.stringify({
1680
+ pid: process.pid,
1681
+ created: new Date(nowMs).toISOString(),
1682
+ }), { encoding: 'utf8', flag: 'wx' });
1683
+ return { acquired: true };
1684
+ } catch (error) {
1685
+ if (error.code !== 'EEXIST') return { acquired: false, error: error };
1686
+ try {
1687
+ const stat = fs.statSync(lockPath);
1688
+ if (nowMs - stat.mtimeMs > maxAgeMs) {
1689
+ fs.unlinkSync(lockPath);
1690
+ return acquireBackupLock(lockPath, now, staleHours);
1691
+ }
1692
+ } catch (_) {
1693
+ try { fs.unlinkSync(lockPath); } catch (__) {}
1694
+ return acquireBackupLock(lockPath, now, staleHours);
1695
+ }
1696
+ return { acquired: false, fresh: true };
1697
+ }
1698
+ }
1699
+ function releaseBackupLock(lockPath) {
1700
+ try { fs.unlinkSync(lockPath); } catch (_) {}
1701
+ }
1702
+ function backupEnsureDailyCore(opts) {
1703
+ opts = opts || {};
1704
+ const root = path.resolve(opts.root || userRoot());
1705
+ const cfg = loadConfig();
1706
+ const now = opts.now ? new Date(opts.now) : new Date();
1707
+ const nowIso = now.toISOString();
1708
+ if (!cfg.backup_enabled || cfg.backup_schedule === 'manual') {
1709
+ return { error: false, created: false, skipped: true, reason: 'disabled', text: '自动备份未启用;跳过 ensure-daily。' };
1710
+ }
1711
+ const dir = cfg.backup_dir ? path.resolve(String(cfg.backup_dir)) : '';
1712
+ if (!dir) {
1713
+ const text = '自动备份已启用但未配置 backup_dir;拒绝执行。';
1714
+ cfg.backup_last_attempt = nowIso;
1715
+ cfg.backup_last_error = text;
1716
+ saveConfig(cfg);
1717
+ return { error: true, created: false, skipped: false, text: text };
1718
+ }
1719
+ const same = opts.sameVolumeFn ? !!opts.sameVolumeFn(root, dir) : sameVolume(root, dir);
1720
+ if (same) {
1721
+ const text = '拒绝: 备份目录与记忆库在同一卷,不执行每日备份。';
1722
+ cfg.backup_last_attempt = nowIso;
1723
+ cfg.backup_last_error = text;
1724
+ saveConfig(cfg);
1725
+ return { error: true, created: false, skipped: false, text: text };
1726
+ }
1727
+ const lockPath = opts.lockPath || path.join(path.dirname(configPath()), 'backup.lock');
1728
+ const lock = acquireBackupLock(lockPath, now, opts.staleHours);
1729
+ if (!lock.acquired) {
1730
+ if (lock.error) {
1731
+ const text = '备份锁创建失败: ' + lock.error.message;
1732
+ cfg.backup_last_attempt = nowIso;
1733
+ cfg.backup_last_error = text;
1734
+ saveConfig(cfg);
1735
+ return { error: true, created: false, skipped: false, text: text };
1736
+ }
1737
+ return { error: false, created: false, skipped: true, reason: 'already-running', text: '已有备份任务在运行;跳过本次 ensure-daily。' };
1738
+ }
1739
+ try {
1740
+ const listed = backupListCore({ dir: dir });
1741
+ if (listed.error) throw new Error(listed.text);
1742
+ const todayKey = localDateKey(now);
1743
+ const latest = latestBackupByCreated(listed.backups);
1744
+ if (latest && latest.created && localDateKey(latest.created) === todayKey) {
1745
+ cfg.backup_last_check = nowIso;
1746
+ cfg.backup_last_success = latest.created;
1747
+ cfg.backup_last_backup_id = latest.id;
1748
+ delete cfg.backup_last_error;
1749
+ saveConfig(cfg);
1750
+ return { error: false, created: false, skipped: true, reason: 'already-backed-up', id: latest.id, text: '今天已有备份 ' + latest.id + ';跳过。' };
1751
+ }
1752
+ const created = backupCreateCore({
1753
+ root: root,
1754
+ dir: dir,
1755
+ now: nowIso,
1756
+ allowSameVolume: !!opts.allowSameVolume,
1757
+ });
1758
+ if (created.error) throw new Error(created.text);
1759
+ cfg.backup_last_attempt = nowIso;
1760
+ cfg.backup_last_check = nowIso;
1761
+ cfg.backup_last_success = created.manifest.created;
1762
+ cfg.backup_last_backup_id = created.id;
1763
+ delete cfg.backup_last_error;
1764
+ saveConfig(cfg);
1765
+ return { error: false, created: true, skipped: false, id: created.id, path: created.path, text: '每日备份完成: ' + created.id };
1766
+ } catch (error) {
1767
+ cfg.backup_last_attempt = nowIso;
1768
+ cfg.backup_last_check = nowIso;
1769
+ cfg.backup_last_error = error.message;
1770
+ saveConfig(cfg);
1771
+ return { error: true, created: false, skipped: false, text: '每日备份失败: ' + error.message };
1772
+ } finally {
1773
+ releaseBackupLock(lockPath);
1774
+ }
1775
+ }
1776
+ function backupHealthCore(opts) {
1777
+ opts = opts || {};
1778
+ const cfg = loadConfig();
1779
+ if (!cfg.backup_dir && cfg.backup_setup_choice !== 'manual') {
1780
+ return {
1781
+ level: 'warning',
1782
+ needsSetup: true,
1783
+ text: '尚未配置独立盘备份。AI 应先运行 yotta-memory backup volumes,只展示当前实际存在的可写异卷;由用户确认目录后,再运行 backup setup 启用每日自动备份。',
1784
+ };
1785
+ }
1786
+ if (cfg.backup_setup_choice === 'manual') return { level: 'none', needsSetup: false, text: '' };
1787
+ const status = backupStatusCore(opts);
1788
+ const maxAge = Number(cfg.backup_max_age_hours) || 36;
1789
+ if (cfg.backup_last_error) return { level: 'warning', needsSetup: false, text: '最近一次备份失败: ' + cfg.backup_last_error };
1790
+ if (cfg.backup_scheduler_error) return { level: 'warning', needsSetup: false, text: '每日备份调度注册异常: ' + cfg.backup_scheduler_error };
1791
+ if (!status.latest) return { level: 'warning', needsSetup: true, text: '自动备份已启用,但还没有成功备份。请检查 backup 目录与调度器。' };
1792
+ if (status.ageHours !== null && status.ageHours > maxAge) {
1793
+ return { level: 'warning', needsSetup: false, text: '最近备份已过期(' + status.ageHours.toFixed(1) + ' 小时,超过 ' + maxAge + ' 小时);请检查备份盘与每日调度。' };
1794
+ }
1795
+ return { level: 'none', needsSetup: false, text: '' };
1796
+ }
1797
+ function sha256File(file) {
1798
+ return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
1799
+ }
1800
+ function listBackupFiles(dir, baseDir) {
1801
+ const out = [];
1802
+ const walk = function (current) {
1803
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
1804
+ const full = path.join(current, entry.name);
1805
+ if (entry.isDirectory()) walk(full);
1806
+ else if (entry.isFile()) {
1807
+ out.push({
1808
+ path: path.relative(baseDir, full).replace(/\\/g, '/'),
1809
+ size: fs.statSync(full).size,
1810
+ sha256: sha256File(full),
1811
+ });
1812
+ }
1813
+ }
1814
+ };
1815
+ walk(dir);
1816
+ return out;
1817
+ }
1818
+ function isAllowedBackupPath(rel) {
1819
+ const normalized = String(rel || '').replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
1820
+ if (!normalized) return true;
1821
+ const top = normalized.split('/')[0];
1822
+ if (top === 'facts' || top === 'private' || top === '.archive') return true;
1823
+ if (top === 'agents.json' || top === 'index.json' || /^index-\d{4}\.json$/.test(top)) return true;
1824
+ if (top === 'keys') return normalized !== 'keys/cache' && normalized.indexOf('keys/cache/') !== 0;
1825
+ return false;
1826
+ }
1827
+ function backupCreateCore(opts) {
1828
+ opts = opts || {};
1829
+ const root = path.resolve(opts.root || userRoot());
1830
+ if (!isExistingStore(root)) return { error: true, text: '记忆库不存在或未初始化: ' + root };
1831
+ const destBase = backupDestination(opts);
1832
+ if (!destBase) return { error: true, text: '未配置备份目录。请用 --dir <目录>、YOTTA_MEMORY_BACKUP_DIR 或 config set backup_dir <目录>。' };
1833
+ if (!opts.allowSameVolume && sameVolume(root, destBase)) {
1834
+ return { error: true, text: '拒绝: 备份目录与记忆库在同一卷。请把备份放到独立盘或独立卷。' };
1835
+ }
1836
+ const createdAt = new Date(opts.now || Date.now()).toISOString();
1837
+ const id = os.hostname() + '-' + createdAt.replace(/[:.]/g, '-') + '-' + crypto.randomBytes(3).toString('hex');
1838
+ const target = path.join(destBase, id);
1839
+ try {
1840
+ fs.mkdirSync(destBase, { recursive: true });
1841
+ fs.cpSync(root, target, {
1842
+ recursive: true,
1843
+ force: false,
1844
+ errorOnExist: true,
1845
+ filter: function (src) {
1846
+ const rel = path.relative(root, src).replace(/\\/g, '/');
1847
+ return isAllowedBackupPath(rel);
1848
+ },
1849
+ });
1850
+ const files = listBackupFiles(target, target);
1851
+ const manifest = {
1852
+ id: id,
1853
+ version: VERSION,
1854
+ created: createdAt,
1855
+ source: root,
1856
+ files: files,
1857
+ };
1858
+ fs.writeFileSync(path.join(target, 'manifest.json'), JSON.stringify(manifest, null, 2), 'utf8');
1859
+ return { error: false, id: id, path: target, manifest: manifest, text: '已创建备份: ' + target + '(' + files.length + ' 文件)' };
1860
+ } catch (error) {
1861
+ return { error: true, text: '备份失败: ' + error.message };
1862
+ }
1863
+ }
1864
+ function backupListCore(opts) {
1865
+ opts = opts || {};
1866
+ const destBase = backupDestination(opts);
1867
+ if (!destBase) return { error: true, backups: [], text: '未配置备份目录。请用 --dir <目录>、YOTTA_MEMORY_BACKUP_DIR 或 config set backup_dir <目录>。' };
1868
+ if (!fs.existsSync(destBase)) return { error: false, backups: [], text: '(无备份)' };
1869
+ const backups = [];
1870
+ for (const entry of fs.readdirSync(destBase, { withFileTypes: true })) {
1871
+ if (!entry.isDirectory()) continue;
1872
+ const full = path.join(destBase, entry.name);
1873
+ let manifest = null;
1874
+ try { manifest = JSON.parse(fs.readFileSync(path.join(full, 'manifest.json'), 'utf8')); } catch (_) {}
1875
+ backups.push({
1876
+ id: entry.name,
1877
+ path: full,
1878
+ created: manifest && manifest.created ? manifest.created : '',
1879
+ files: manifest && Array.isArray(manifest.files) ? manifest.files.length : null,
1880
+ manifest: manifest,
1881
+ });
1882
+ }
1883
+ backups.sort(function (a, b) { return String(b.id).localeCompare(String(a.id)); });
1884
+ const lines = backups.length
1885
+ ? backups.map(function (b) { return '- ' + b.id + ' ' + (b.created || '?') + ' ' + (b.files === null ? 'manifest 缺失' : b.files + ' 文件'); })
1886
+ : ['(无备份)'];
1887
+ return { error: false, backups: backups, text: lines.join('\n') };
1888
+ }
1889
+ function backupDoctorCore(opts) {
1890
+ opts = opts || {};
1891
+ const listed = backupListCore(opts);
1892
+ if (listed.error) return { error: true, ok: false, text: listed.text };
1893
+ const id = opts.id || (listed.backups[0] && listed.backups[0].id);
1894
+ if (!id) return { error: false, ok: false, text: '没有可检查的备份。' };
1895
+ const backup = listed.backups.find(function (item) { return item.id === id; });
1896
+ if (!backup) return { error: true, ok: false, text: '未找到备份: ' + id };
1897
+ if (!backup.manifest || !Array.isArray(backup.manifest.files)) {
1898
+ return { error: false, ok: false, text: '备份 manifest 缺失或损坏: ' + id };
1899
+ }
1900
+ const bad = [];
1901
+ for (const file of backup.manifest.files) {
1902
+ if (!isSafeRelativePathForBackup(file.path)) { bad.push(file.path + '(路径不安全)'); continue; }
1903
+ const full = path.join(backup.path, file.path);
1904
+ if (!fs.existsSync(full)) { bad.push(file.path + '(缺失)'); continue; }
1905
+ const stat = fs.statSync(full);
1906
+ if (stat.size !== file.size) { bad.push(file.path + '(大小不符)'); continue; }
1907
+ if (sha256File(full) !== file.sha256) bad.push(file.path + '(哈希不符)');
1908
+ }
1909
+ return {
1910
+ error: false,
1911
+ ok: bad.length === 0,
1912
+ text: bad.length ? '备份体检失败: ' + id + '\n' + bad.join('\n') : '备份体检通过: ' + id + '(' + backup.manifest.files.length + ' 文件)',
1913
+ };
1914
+ }
1915
+ function isSafeRelativePathForBackup(value) {
1916
+ if (typeof value !== 'string' || value === '' || path.isAbsolute(value)) return false;
1917
+ return !value.split(/[\\/]+/).includes('..');
1918
+ }
1919
+ function backupRestoreCore(id, opts) {
1920
+ opts = opts || {};
1921
+ if (!id) return { error: true, text: '请指定备份 ID: yotta-memory backup restore <id> --to <目录>' };
1922
+ const destBase = backupDestination(opts);
1923
+ if (!destBase) return { error: true, text: '未配置备份目录。请用 --dir <目录> 或 config set backup_dir <目录>。' };
1924
+ const source = path.join(destBase, id);
1925
+ if (!fs.existsSync(path.join(source, 'manifest.json'))) return { error: true, text: '备份不存在或 manifest 缺失: ' + id };
1926
+ if (!opts.to) return { error: true, text: '拒绝恢复到未指定位置。请使用 --to <新目录>;默认不覆盖正在使用的记忆库。' };
1927
+ const target = path.resolve(String(opts.to));
1928
+ if (fs.existsSync(target) && fs.readdirSync(target).length > 0) {
1929
+ return { error: true, text: '恢复目标非空,拒绝覆盖: ' + target };
1930
+ }
1931
+ try {
1932
+ fs.mkdirSync(path.dirname(target), { recursive: true });
1933
+ fs.cpSync(source, target, {
1934
+ recursive: true,
1935
+ force: false,
1936
+ errorOnExist: true,
1937
+ filter: function (src) { return path.basename(src) !== 'manifest.json'; },
1938
+ });
1939
+ return { error: false, path: target, text: '已恢复到: ' + target };
1940
+ } catch (error) {
1941
+ return { error: true, text: '恢复失败: ' + error.message };
1942
+ }
1943
+ }
1944
+ function findFirstEncryptedPrivateFile(root) {
1945
+ const pdir = path.join(root, PRIVATE_DIR);
1946
+ if (!fs.existsSync(pdir)) return null;
1947
+ const owners = fs.readdirSync(pdir);
1948
+ for (const owner of owners) {
1949
+ const ownerDir = path.join(pdir, owner);
1950
+ if (!fs.statSync(ownerDir).isDirectory()) continue;
1951
+ for (const type of PRIVATE_LEAF) {
1952
+ const dir = path.join(ownerDir, type);
1953
+ if (!fs.existsSync(dir)) continue;
1954
+ for (const name of fs.readdirSync(dir)) {
1955
+ if (!name.endsWith('.md' + ENC_SUFFIX)) continue;
1956
+ const fp = path.join(dir, name);
1957
+ if (fs.statSync(fp).isFile()) return { fp: fp, owner: owner, rel: relOf(root, fp) };
1958
+ }
1959
+ }
1960
+ }
1961
+ return null;
1962
+ }
1963
+ function backupDrillCore(opts) {
1964
+ opts = opts || {};
1965
+ const destBase = backupDestination(opts);
1966
+ if (!destBase) return { error: true, ok: false, text: '未配置备份目录。请用 --dir <目录> 或 config set backup_dir <目录>。' };
1967
+ const listed = backupListCore({ dir: destBase });
1968
+ if (listed.error) return { error: true, ok: false, text: listed.text };
1969
+ const id = opts.id || (listed.backups[0] && listed.backups[0].id);
1970
+ if (!id) return { error: true, ok: false, text: '没有可演练的备份。' };
1971
+ let target = opts.to ? path.resolve(String(opts.to)) : '';
1972
+ let tempParent = '';
1973
+ if (!target) {
1974
+ tempParent = fs.mkdtempSync(path.join(os.tmpdir(), 'ytm-drill-'));
1975
+ target = path.join(tempParent, 'restored');
1976
+ }
1977
+ function fail(text, checks) {
1978
+ if (tempParent) {
1979
+ try { fs.rmSync(tempParent, { recursive: true, force: true }); } catch (_) {}
1980
+ }
1981
+ return { error: true, ok: false, text: text, checks: checks || {} };
1982
+ }
1983
+ const verified = backupDoctorCore({ dir: destBase, id: id });
1984
+ if (!verified.ok) return fail('恢复演练失败: 备份 manifest 校验未通过。\n' + verified.text, { manifest: false });
1985
+ const restored = backupRestoreCore(id, { dir: destBase, to: target });
1986
+ if (restored.error) return fail(restored.text, { manifest: true, index: false });
1987
+ let indexOk = false;
1988
+ let indexError = '';
1989
+ try {
1990
+ buildIndex(target);
1991
+ indexOk = true;
1992
+ } catch (error) {
1993
+ indexError = error.message;
1994
+ }
1995
+ if (!indexOk) return fail('恢复演练失败: 恢复副本无法重建索引: ' + indexError, { manifest: true, index: false });
1996
+ const privateFile = findFirstEncryptedPrivateFile(target);
1997
+ let privateCheck = { checked: false, decrypted: true, source: 'no-private-entry' };
1998
+ if (privateFile) {
1999
+ let ownerKey = null;
2000
+ let source = '';
2001
+ try {
2002
+ if (opts.recoveryKey) {
2003
+ ownerKey = unwrapOwnerKeyRecovery(target, privateFile.owner, Buffer.from(String(opts.recoveryKey), 'base64'));
2004
+ source = 'recovery-key';
2005
+ } else if (opts.password) {
2006
+ const salt = loadSalt(target);
2007
+ if (!salt) throw new Error('恢复副本缺少 keys/salt');
2008
+ ownerKey = unwrapOwnerKey(target, privateFile.owner, deriveUmk(opts.password, salt));
2009
+ source = 'password';
2010
+ } else {
2011
+ const ownerKeyFile = opts.ownerKeyFile || path.join(opts.sourceRoot || userRoot(), 'keys', 'cache', privateFile.owner + '.key');
2012
+ if (fs.existsSync(ownerKeyFile)) {
2013
+ ownerKey = fs.readFileSync(ownerKeyFile);
2014
+ source = 'owner-cache';
2015
+ }
2016
+ }
2017
+ } catch (error) {
2018
+ ownerKey = null;
2019
+ }
2020
+ if (!ownerKey) {
2021
+ return fail('恢复演练失败: 已恢复文件结构,但无法解密测试私密记忆。请提供 --recovery-key 或 --password,或确认本机 keys/cache 中有 ' + privateFile.owner + ' 的授权缓存。', {
2022
+ manifest: true,
2023
+ index: true,
2024
+ private: { checked: true, decrypted: false, source: '', file: privateFile.rel },
2025
+ });
2026
+ }
2027
+ try {
2028
+ decryptMemoryText(fs.readFileSync(privateFile.fp), ownerKey);
2029
+ privateCheck = { checked: true, decrypted: true, source: source, file: privateFile.rel, owner: privateFile.owner };
2030
+ } catch (error) {
2031
+ return fail('恢复演练失败: 测试私密记忆解密失败(' + error.message + ')。', {
2032
+ manifest: true,
2033
+ index: true,
2034
+ private: { checked: true, decrypted: false, source: source, file: privateFile.rel },
2035
+ });
2036
+ }
2037
+ }
2038
+ if (tempParent) {
2039
+ try { fs.rmSync(tempParent, { recursive: true, force: true }); } catch (_) {}
2040
+ }
2041
+ const checks = { manifest: true, index: true, private: privateCheck };
2042
+ const lines = [
2043
+ '恢复演练通过: ' + id,
2044
+ '- manifest / SHA-256: 通过',
2045
+ '- 恢复副本索引: 通过',
2046
+ '- 测试私密解密: ' + (privateCheck.checked ? ('通过(' + privateCheck.source + ',' + privateCheck.file + ')') : '跳过(备份中没有加密私密条目)'),
2047
+ ];
2048
+ return {
2049
+ error: false,
2050
+ ok: true,
2051
+ id: id,
2052
+ restoredTo: tempParent ? '(临时副本已清理)' : target,
2053
+ checks: checks,
2054
+ text: lines.join('\n'),
2055
+ };
2056
+ }
1121
2057
  function rememberCore(type, subject, statement, opts) {
1122
2058
  opts = opts || {};
1123
2059
  const root = userRoot();
@@ -1400,6 +2336,11 @@ function resolveMemoryFile(root, ref) {
1400
2336
  return null;
1401
2337
  }
1402
2338
  function relOf(root, fp) { return path.relative(root, fp).replace(/\\/g, '/'); }
2339
+ function appendTrashAudit(root, record) {
2340
+ const dir = path.join(root, '.trash');
2341
+ fs.mkdirSync(dir, { recursive: true });
2342
+ fs.appendFileSync(path.join(dir, 'audit-' + today() + '.jsonl'), JSON.stringify(record) + '\n', 'utf8');
2343
+ }
1403
2344
  function forgetCore(fileRef, opts) {
1404
2345
  opts = opts || {};
1405
2346
  const selfAgent = opts.selfAgent || currentAgent();
@@ -1418,9 +2359,30 @@ function forgetCore(fileRef, opts) {
1418
2359
  return { error: true, text: '拒绝: 不能删除其它智能体 ' + owner + ' 的私密记忆(当前身份 ' + (selfAgent || '未声明') + ')。请用 YOTTA_AGENT_ID 声明自己的身份,或加 --unsafe(用户显式授权)。' };
1419
2360
  }
1420
2361
  }
1421
- fs.unlinkSync(target);
2362
+ const trashFile = path.join(
2363
+ targetRoot,
2364
+ '.trash',
2365
+ new Date().toISOString().replace(/[:.]/g, '-'),
2366
+ targetRel,
2367
+ );
2368
+ try {
2369
+ fs.mkdirSync(path.dirname(trashFile), { recursive: true });
2370
+ fs.renameSync(target, trashFile);
2371
+ } catch (error) {
2372
+ return { error: true, text: '召回区写入失败,原记忆未删除: ' + error.message };
2373
+ }
1422
2374
  if (targetRoot) removeIndexEntry(targetRoot, targetRel);
1423
- return { error: false, text: '已删除: ' + target };
2375
+ if (targetRoot) {
2376
+ appendTrashAudit(targetRoot, {
2377
+ ts: new Date().toISOString(),
2378
+ action: 'forget',
2379
+ file: targetRel,
2380
+ trash: relOf(targetRoot, trashFile),
2381
+ owner: ownerFromPrivatePath(targetRoot, target),
2382
+ selfAgent,
2383
+ });
2384
+ }
2385
+ return { error: false, text: '已移入回收区: ' + trashFile };
1424
2386
  }
1425
2387
  function archiveCore(opts) {
1426
2388
  opts = opts || {};
@@ -2199,7 +3161,13 @@ const VIEW_HTML = "<!doctype html><html lang=\"zh\"><head><meta charset=\"utf-8\
2199
3161
  // ---- 命令包装(CLI 入口)----
2200
3162
  async function cmdInit(opts) {
2201
3163
  const root = opts.dir ? path.resolve(String(opts.dir)) : (opts.project ? projectRoot() : userRoot());
2202
- const fresh = !fs.existsSync(path.join(root, PUBLIC_DIR)) && !fs.existsSync(path.join(root, PRIVATE_DIR)) && !fs.existsSync(path.join(root, INDEX_FILE));
3164
+ if (isExistingStore(root) && !opts.attach) {
3165
+ const blocked = initCore(Object.assign({}, opts));
3166
+ console.log(blocked.text);
3167
+ if (blocked.error) process.exit(2);
3168
+ return;
3169
+ }
3170
+ const fresh = !isExistingStore(root);
2203
3171
  const wantEncrypt = opts.encrypt || (!opts.noEncrypt && fresh);
2204
3172
  let o = Object.assign({}, opts);
2205
3173
  if (wantEncrypt && !o.password && !process.env.YOTTA_MEMORY_PASS) {
@@ -2264,6 +3232,65 @@ function cmdArchive(opts) {
2264
3232
  const r = archiveCore(opts);
2265
3233
  console.log(r.text);
2266
3234
  }
3235
+ function cmdBackupCreate(opts) {
3236
+ const r = backupCreateCore(opts);
3237
+ console.log(r.text);
3238
+ if (r.error) process.exit(2);
3239
+ }
3240
+ function cmdBackupVolumes(opts) {
3241
+ const r = listBackupVolumesCore(opts);
3242
+ if (opts.json) console.log(JSON.stringify({ root: userRoot(), volumes: r.volumes }, null, 2));
3243
+ else console.log(r.text);
3244
+ if (r.error) process.exit(2);
3245
+ }
3246
+ function cmdBackupSetup(opts) {
3247
+ const r = backupSetupCore(opts);
3248
+ console.log(r.text);
3249
+ if (r.error) process.exit(2);
3250
+ }
3251
+ function cmdBackupStatus(opts) {
3252
+ const r = backupStatusCore(opts);
3253
+ if (opts.json) console.log(JSON.stringify(r, null, 2));
3254
+ else console.log(r.text);
3255
+ if (r.error) process.exit(2);
3256
+ }
3257
+ function cmdBackupEnsureDaily(opts) {
3258
+ const r = backupEnsureDailyCore(opts);
3259
+ console.log(r.text);
3260
+ if (r.error) process.exit(2);
3261
+ }
3262
+ function cmdBackupSchedule(action, opts) {
3263
+ let r;
3264
+ if (action === 'enable') r = backupScheduleEnableCore(opts);
3265
+ else if (action === 'disable') r = backupScheduleDisableCore(opts);
3266
+ else if (action === 'status') r = backupScheduleStatusCore(opts);
3267
+ else {
3268
+ console.error('backup schedule 子命令: enable [--time HH:MM] / disable / status');
3269
+ process.exit(2);
3270
+ }
3271
+ console.log(r.text);
3272
+ if (r.error) process.exit(2);
3273
+ }
3274
+ function cmdBackupList(opts) {
3275
+ const r = backupListCore(opts);
3276
+ console.log(r.text);
3277
+ if (r.error) process.exit(2);
3278
+ }
3279
+ function cmdBackupDoctor(opts) {
3280
+ const r = backupDoctorCore(opts);
3281
+ console.log(r.text);
3282
+ if (r.error || !r.ok) process.exit(2);
3283
+ }
3284
+ function cmdBackupRestore(id, opts) {
3285
+ const r = backupRestoreCore(id, opts);
3286
+ console.log(r.text);
3287
+ if (r.error) process.exit(2);
3288
+ }
3289
+ function cmdBackupDrill(id, opts) {
3290
+ const r = backupDrillCore(Object.assign({}, opts, { id: id || opts.id }));
3291
+ console.log(r.text);
3292
+ if (r.error) process.exit(2);
3293
+ }
2267
3294
  function cmdReindex() {
2268
3295
  const roots = memoryRoots();
2269
3296
  if (!roots.length) { console.log('记忆库不存在。'); return; }
@@ -2785,6 +3812,13 @@ function contextCore(opts) {
2785
3812
  if (!trace.length) lines.push('(无选择记录)');
2786
3813
  for (const t of trace) lines.push(t);
2787
3814
  }
3815
+ const backupHealth = backupHealthCore({ root: root });
3816
+ if (backupHealth.level === 'warning') {
3817
+ lines.push('');
3818
+ lines.push('## 可靠性提醒');
3819
+ lines.push('');
3820
+ lines.push('- ' + backupHealth.text);
3821
+ }
2788
3822
  return { error: false, exitCode: 0, text: lines.join('\n') };
2789
3823
  }
2790
3824
  function cmdContext(opts) {
@@ -2795,13 +3829,16 @@ function cmdContext(opts) {
2795
3829
 
2796
3830
  // ---- config 命令 ----
2797
3831
  // v0.10.0:maintain_* / consolidate_* 数值键纳入 config set/get(此前只支持 3 个键但文档已写可调)
2798
- function isNumericConfigKey(key) { return /^(maintain_|consolidate_)/.test(key); }
3832
+ function isNumericConfigKey(key) { return /^(maintain_|consolidate_|backup_max_age_hours$)/.test(key); }
2799
3833
  function cmdConfigSet(key, value) {
2800
- const known = ['memory_home', 'embedding_cmd', 'embedding_timeout', 'maintain_archived_utility', 'maintain_archived_age', 'maintain_forget_utility', 'maintain_forget_age', 'maintain_decay_halflife_FACT', 'maintain_decay_halflife_PREF', 'maintain_decay_halflife_COMMIT', 'consolidate_min_age', 'consolidate_min_idle', 'consolidate_max_utility', 'consolidate_min_group', 'consolidate_period'];
2801
- if (known.indexOf(key) === -1) { console.error('未知配置项: ' + key + '(可用: memory_home / embedding_cmd / embedding_timeout / maintain_* / consolidate_*)'); process.exit(2); }
3834
+ const known = ['memory_home', 'embedding_cmd', 'embedding_timeout', 'backup_dir', 'backup_enabled', 'backup_schedule', 'backup_time', 'backup_max_age_hours', 'backup_setup_choice', 'maintain_archived_utility', 'maintain_archived_age', 'maintain_forget_utility', 'maintain_forget_age', 'maintain_decay_halflife_FACT', 'maintain_decay_halflife_PREF', 'maintain_decay_halflife_COMMIT', 'consolidate_min_age', 'consolidate_min_idle', 'consolidate_max_utility', 'consolidate_min_group', 'consolidate_period'];
3835
+ if (known.indexOf(key) === -1) { console.error('未知配置项: ' + key + '(可用: memory_home / backup_dir / embedding_cmd / embedding_timeout / maintain_* / consolidate_*)'); process.exit(2); }
2802
3836
  if (value === undefined || value === null || value === '') { console.error('缺少值: config set ' + key + ' <值>'); process.exit(2); }
2803
3837
  const cfg = loadConfig();
2804
3838
  if (key === 'memory_home') cfg.memory_home = value;
3839
+ else if (key === 'backup_dir') cfg.backup_dir = value;
3840
+ else if (key === 'backup_enabled') cfg.backup_enabled = value !== 'false' && value !== '0';
3841
+ else if (key === 'backup_schedule' || key === 'backup_time' || key === 'backup_setup_choice') cfg[key] = value;
2805
3842
  else if (key === 'embedding_cmd') cfg.embedding_cmd = value;
2806
3843
  else if (key === 'embedding_timeout') cfg.embedding_timeout = parseInt(value, 10) || 3000;
2807
3844
  else if (isNumericConfigKey(key)) {
@@ -2810,16 +3847,20 @@ function cmdConfigSet(key, value) {
2810
3847
  cfg[key] = n;
2811
3848
  }
2812
3849
  saveConfig(cfg);
2813
- console.log('已写入配置: ' + key + ' = ' + (key === 'memory_home' || key === 'embedding_cmd' ? value : cfg[key]));
3850
+ console.log('已写入配置: ' + key + ' = ' + (key === 'memory_home' || key === 'backup_dir' || key === 'embedding_cmd' ? value : cfg[key]));
2814
3851
  }
2815
3852
  function cmdConfigGet() {
2816
3853
  const cfg = loadConfig();
2817
3854
  console.log('memory_home: ' + (cfg.memory_home || '(未设置,默认 ~/.yottamemory)'));
3855
+ console.log('backup_dir: ' + (cfg.backup_dir || '(未设置)'));
3856
+ console.log('backup_enabled: ' + (cfg.backup_enabled === undefined ? '(未设置)' : cfg.backup_enabled));
3857
+ console.log('backup_schedule: ' + (cfg.backup_schedule || '(未设置)'));
3858
+ console.log('backup_time: ' + (cfg.backup_time || '(未设置)'));
2818
3859
  console.log('embedding_cmd: ' + (cfg.embedding_cmd || '(未设置)'));
2819
3860
  console.log('embedding_timeout: ' + (cfg.embedding_timeout || 3000));
2820
- const keys = ['memory_home', 'embedding_cmd', 'embedding_timeout', 'maintain_archived_utility', 'maintain_archived_age', 'maintain_forget_utility', 'maintain_forget_age', 'maintain_decay_halflife_FACT', 'maintain_decay_halflife_PREF', 'maintain_decay_halflife_COMMIT', 'consolidate_min_age', 'consolidate_min_idle', 'consolidate_max_utility', 'consolidate_min_group', 'consolidate_period'];
3861
+ const keys = ['memory_home', 'backup_dir', 'backup_enabled', 'backup_schedule', 'backup_time', 'backup_max_age_hours', 'backup_setup_choice', 'embedding_cmd', 'embedding_timeout', 'maintain_archived_utility', 'maintain_archived_age', 'maintain_forget_utility', 'maintain_forget_age', 'maintain_decay_halflife_FACT', 'maintain_decay_halflife_PREF', 'maintain_decay_halflife_COMMIT', 'consolidate_min_age', 'consolidate_min_idle', 'consolidate_max_utility', 'consolidate_min_group', 'consolidate_period'];
2821
3862
  for (const k of keys) {
2822
- if (k === 'memory_home' || k === 'embedding_cmd' || k === 'embedding_timeout') continue;
3863
+ if (k === 'memory_home' || k === 'backup_dir' || k === 'backup_enabled' || k === 'backup_schedule' || k === 'backup_time' || k === 'embedding_cmd' || k === 'embedding_timeout') continue;
2823
3864
  if (cfg[k] !== undefined) console.log(k + ': ' + cfg[k]);
2824
3865
  }
2825
3866
  console.log('当前生效用户级位置: ' + userRoot());
@@ -3096,6 +4137,7 @@ function cmdServe(opts) {
3096
4137
  if (noAuth) console.log('鉴权: 已关闭(--no-auth,仅限可信内网)');
3097
4138
  else console.log('鉴权: Bearer token + X-Agent-Id(yotta-memory token new --agent <id> 生成)');
3098
4139
  console.log('按 Ctrl+C 停止');
4140
+ startBackupFallback();
3099
4141
  });
3100
4142
  }
3101
4143
  // ---- stdio 本地零进程模式(客户端按需拉起 CLI)----
@@ -4046,11 +5088,12 @@ function usage() {
4046
5088
  const banner = 'yotta-memory v' + VERSION + ' — 元忆:有权限边界的文件式智能体记忆';
4047
5089
  const sections = [
4048
5090
  ['核心记忆', [
4049
- ['init', '初始化记忆库(新建默认加密:需主口令 + 恢复钥匙;--no-encrypt 降级明文)'],
5091
+ ['init', '初始化记忆库(新建默认加密:需主口令 + 恢复钥匙;--attach 接入已有库;--no-encrypt 降级明文)'],
4050
5092
  ['remember', '写入记忆(--source 来源;--weight 权重;--verify 写后回读校验;--no-hint 关启发)'],
4051
5093
  ['recall', '检索记忆(--type/--limit/--agent/--owner/--all/--unsafe)'],
4052
5094
  ['forget', '删除一条记忆'],
4053
5095
  ['archive', '归档(--days/--threshold 盖棺分+年龄)'],
5096
+ ['backup', '备份记忆库(volumes / setup / status / ensure-daily / schedule / create / list / doctor / restore <id> --to <目录> / drill)'],
4054
5097
  ['maintain', '记忆自组织(归档/遗忘候选/去重/合并;默认 dry-run;--dedup 查重+置信度,--dedup --apply 自动合并高置信组)'],
4055
5098
  ['consolidate', '周期摘要压缩(默认 dry-run;--apply 执行;--undo <batch> 回滚;--batches 查批次;候选=超龄+闲置+低效用,immutable/BOUND 豁免)'],
4056
5099
  ['distill', '心理日志蒸馏(统计摘要/主题画像/知识地图;--model 可选外部模型)'],
@@ -4072,7 +5115,7 @@ function usage() {
4072
5115
  ['view', '启动用户查看平台(--port/--host;口令解锁浏览/授权/吊销 AI)'],
4073
5116
  ['reset-password', '重设主口令(忘口令用恢复钥匙)'],
4074
5117
  ['key', '管理 AI 私密读取授权缓存(list / authorize <id> / revoke <id>)'],
4075
- ['config', '查看/设置配置(get;set memory_home <目录> / embedding_cmd <命令> / embedding_timeout <毫秒> / maintain_* 阈值与半衰 / consolidate_* 参数)']
5118
+ ['config', '查看/设置配置(get;set memory_home <目录> / backup_dir <目录> / embedding_cmd <命令> / embedding_timeout <毫秒> / maintain_* 阈值与半衰 / consolidate_* 参数)']
4076
5119
  ]],
4077
5120
  ['平台与服务', [
4078
5121
  ['serve', '启动 MCP 记忆引擎(streamable HTTP;--stdio 本地零进程)'],
@@ -4100,7 +5143,7 @@ async function main() {
4100
5143
  if (!args.length) { usage(); return; }
4101
5144
  const opts = {};
4102
5145
  const positional = [];
4103
- const valueOpts = new Set(['--type', '--limit', '--days', '--out', '--owner', '--agent', '--threshold', '--scope', '--host', '--port', '--dir', '--name', '--user', '--relationship', '--source', '--weight', '--budget', '--password', '--new-password', '--recovery-key', '--reason', '--merge', '--model', '--subject', '--embedding', '--focus', '--embedding-timeout', '--min-age', '--min-idle', '--max-utility', '--min-group', '--period']);
5146
+ const valueOpts = new Set(['--type', '--limit', '--days', '--out', '--owner', '--agent', '--threshold', '--scope', '--host', '--port', '--dir', '--name', '--user', '--relationship', '--source', '--weight', '--budget', '--password', '--new-password', '--recovery-key', '--reason', '--merge', '--model', '--subject', '--embedding', '--focus', '--embedding-timeout', '--min-age', '--min-idle', '--max-utility', '--min-group', '--period', '--to', '--id', '--time']);
4104
5147
  for (let i = 0; i < args.length; i++) {
4105
5148
  const a = args[i];
4106
5149
  if (a === '--version' || a === '-v') { console.log(VERSION); return; }
@@ -4112,10 +5155,15 @@ async function main() {
4112
5155
  else if (a === '--stdio') opts.stdio = true;
4113
5156
  else if (a === '--onstart') opts.onstart = true;
4114
5157
  else if (a === '--force') opts.force = true;
5158
+ else if (a === '--attach') opts.attach = true;
5159
+ else if (a === '--allow-same-volume') opts.allowSameVolume = true;
4115
5160
  else if (a === '--verify') opts.verify = true;
4116
5161
  else if (a === '--no-hint') opts.noHint = true;
4117
5162
  else if (a === '--encrypt') opts.encrypt = true;
4118
5163
  else if (a === '--no-encrypt') opts.noEncrypt = true;
5164
+ else if (a === '--json') opts.json = true;
5165
+ else if (a === '--manual') opts.manual = true;
5166
+ else if (a === '--skip-schedule') opts.skipSchedule = true;
4119
5167
  else if (a === '--useful') opts.useful = true;
4120
5168
  else if (a === '--useless') opts.useless = true;
4121
5169
  else if (a === '--undo') opts.undo = true;
@@ -4160,6 +5208,9 @@ async function main() {
4160
5208
  else if (a === '--max-utility') opts.maxUtility = parseFloat(v);
4161
5209
  else if (a === '--min-group') opts.minGroup = parseInt(v, 10) || 0;
4162
5210
  else if (a === '--period') opts.period = parseInt(v, 10) || 0;
5211
+ else if (a === '--to') opts.to = v;
5212
+ else if (a === '--id') opts.id = v;
5213
+ else if (a === '--time') opts.time = v;
4163
5214
  } else if (a.startsWith('--')) {
4164
5215
  console.error('未知选项: ' + a);
4165
5216
  process.exit(2);
@@ -4190,6 +5241,21 @@ async function main() {
4190
5241
  case 'consolidate': if (opts.undo === true && rest.length) opts.undo = rest[0]; cmdConsolidate(opts); break;
4191
5242
  case 'forget': cmdForget(rest[0]); break;
4192
5243
  case 'archive': cmdArchive(opts); break;
5244
+ case 'backup': {
5245
+ const sub = rest[0];
5246
+ if (sub === 'volumes') cmdBackupVolumes(opts);
5247
+ else if (sub === 'setup') cmdBackupSetup(opts);
5248
+ else if (sub === 'status') cmdBackupStatus(opts);
5249
+ else if (sub === 'ensure-daily') cmdBackupEnsureDaily(opts);
5250
+ else if (sub === 'schedule') cmdBackupSchedule(rest[1], opts);
5251
+ else if (sub === 'create') cmdBackupCreate(opts);
5252
+ else if (sub === 'list') cmdBackupList(opts);
5253
+ else if (sub === 'doctor') cmdBackupDoctor(opts);
5254
+ else if (sub === 'restore') cmdBackupRestore(rest[1] || opts.id, opts);
5255
+ else if (sub === 'drill') cmdBackupDrill(rest[1] || opts.id, opts);
5256
+ else { console.error('backup 子命令: volumes / setup / status / ensure-daily / schedule / create / list / doctor / restore <id> --to <目录> / drill'); process.exit(2); }
5257
+ break;
5258
+ }
4193
5259
  case 'reindex': cmdReindex(); break;
4194
5260
  case 'profile': cmdProfile(opts); break;
4195
5261
  case 'context': cmdContext(opts); break;
@@ -4359,6 +5425,25 @@ module.exports = {
4359
5425
  keyRevokeCore: keyRevokeCore,
4360
5426
  promptPassword: promptPassword,
4361
5427
  initCore: initCore,
5428
+ backupCreateCore: backupCreateCore,
5429
+ listBackupVolumesCore: listBackupVolumesCore,
5430
+ backupSetupCore: backupSetupCore,
5431
+ backupStatusCore: backupStatusCore,
5432
+ backupEnsureDailyCore: backupEnsureDailyCore,
5433
+ backupHealthCore: backupHealthCore,
5434
+ backupWindowsTaskXml: backupWindowsTaskXml,
5435
+ backupSystemdServiceContent: backupSystemdServiceContent,
5436
+ backupSystemdTimerContent: backupSystemdTimerContent,
5437
+ backupCronLine: backupCronLine,
5438
+ backupLaunchdPlist: backupLaunchdPlist,
5439
+ backupScheduleEnableCore: backupScheduleEnableCore,
5440
+ backupScheduleDisableCore: backupScheduleDisableCore,
5441
+ backupScheduleStatusCore: backupScheduleStatusCore,
5442
+ backupFallbackCommand: backupFallbackCommand,
5443
+ backupListCore: backupListCore,
5444
+ backupDoctorCore: backupDoctorCore,
5445
+ backupRestoreCore: backupRestoreCore,
5446
+ backupDrillCore: backupDrillCore,
4362
5447
  viewServerCore: viewServerCore,
4363
5448
 
4364
5449
  };