@xiaoyuyu6420/dsh-backup 0.13.1 → 0.13.3

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xiaoyuyu6420/dsh-backup",
3
- "version": "0.13.1",
3
+ "version": "0.13.3",
4
4
  "description": "一键备份与恢复 DeepSeek Harness(dsh)的全部用户数据(~/.dsh):定时自动备份、升级快照、会话日志体检修复、迁移预检、宿主起不来也能用的救援通道、凭据脱敏、GitHub 同步跨机迁移,附可视化面板。 One command to back up & restore all of ~/.dsh: /backup, scheduled auto-backup, upgrade snapshots, session doctor & repair, migrate precheck, out-of-process rescue console, credential redaction, GitHub sync.",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@11.24.0",
@@ -53,12 +53,12 @@
53
53
  ],
54
54
  "peerDependencies": {
55
55
  "@deepseek-ai/cordis": "^4.0.1",
56
- "@deepseek-ai/dsh-commands": "^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.5-rc.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1",
57
- "@deepseek-ai/dsh-fs": "^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.5-rc.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1",
58
- "@deepseek-ai/dsh-settings": "^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.5-rc.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1",
59
- "@deepseek-ai/dsh-subprocess": "^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.5-rc.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1",
60
- "@deepseek-ai/dsh-tools": "^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.5-rc.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1",
61
- "@deepseek-ai/dsh-typert-protocol": "^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.5-rc.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1",
56
+ "@deepseek-ai/dsh-commands": "^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.5-rc.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1 || ^0.2.0-rc.1",
57
+ "@deepseek-ai/dsh-fs": "^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.5-rc.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1 || ^0.2.0-rc.1",
58
+ "@deepseek-ai/dsh-settings": "^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.5-rc.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1 || ^0.2.0-rc.1",
59
+ "@deepseek-ai/dsh-subprocess": "^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.5-rc.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1 || ^0.2.0-rc.1",
60
+ "@deepseek-ai/dsh-tools": "^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.5-rc.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1 || ^0.2.0-rc.1",
61
+ "@deepseek-ai/dsh-typert-protocol": "^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.5-rc.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1 || ^0.2.0-rc.1",
62
62
  "@deepseek-ai/schemastery": "^3.18.1"
63
63
  },
64
64
  "devDependencies": {
@@ -69,7 +69,7 @@
69
69
  },
70
70
  "engines": {
71
71
  "node": ">=20",
72
- "dsh": "^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.5-rc.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1"
72
+ "dsh": "^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.5-rc.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1 || ^0.2.0-rc.1"
73
73
  },
74
74
  "license": "MIT",
75
75
  "repository": {
package/rescue/rescue.mjs CHANGED
@@ -37,7 +37,11 @@ const HERE = path.dirname(fileURLToPath(import.meta.url));
37
37
  const SENSITIVE_DEFAULTS = ['.credentials.yaml', '.env', 'qq-bridge/config.json'];
38
38
  const VAULT_DIR = 'vault';
39
39
  const ZSTD_MAGIC = 0xfd2fb528;
40
- const SESSION_LOG_NAMES = new Set(['session.jsonl.zstd', 'session.jsonl']);
40
+ // 与 lib/index.js 同源:会话日志名形如 session[.v<N>].jsonl[.zstd](宿主
41
+ // sessionFormatLogFilename 的形态)。写死具体名字会在格式换代当天整体失明
42
+ // (#117)——救援台是宿主起不来时的唯一通道,更不能看不见会话。
43
+ const SESSION_LOG_RE = /^session(?:\.v([1-9][0-9]*))?\.jsonl(?:\.zstd)?$/;
44
+ const isSessionLogName = (name) => SESSION_LOG_RE.test(name);
41
45
  const SCAN_SKIP_DIRS = new Set(['node_modules', '.system', '.git', 'vault']);
42
46
  const SCAN_MAX_FILES = 4000;
43
47
  const SCAN_MAX_DEPTH = 8;
@@ -108,7 +112,7 @@ async function listBackups(root) {
108
112
  }
109
113
  const backups = [];
110
114
  for (const d of dirents) {
111
- if (!d.name.startsWith('dsh-') || d.name.startsWith('dsh-pre-restore-') || d.name.startsWith('dsh-t-') || !d.name.endsWith('.tar.gz')) continue;
115
+ if (!d.name.startsWith('dsh-') || d.name.startsWith('dsh-pre-restore-') || d.name.startsWith('dsh-pre-upgrade-') || d.name.startsWith('dsh-t-') || !d.name.endsWith('.tar.gz')) continue;
112
116
  let size;
113
117
  try {
114
118
  size = (await fs.stat(`${root}/${d.name}`)).size;
@@ -173,24 +177,48 @@ function parseTarEntry(line) {
173
177
  }
174
178
  }
175
179
  if (nameIdx < 0 || nameIdx >= f.length) return null;
176
- return { type: f[0][0], name: f.slice(nameIdx).join(' ').replace(/\/$/, '') };
180
+ const rest = f.slice(nameIdx).join(' ');
181
+ // 链接目标单独摘出(硬链接 `<名> link to <目标>`、符号链接 `<名> -> <目标>`):
182
+ // 与插件侧 parseTarEntry 同语义,别把目标混进名字(#112)。
183
+ let name = rest;
184
+ let link = null;
185
+ const hardAt = rest.indexOf(' link to ');
186
+ if (hardAt >= 0) {
187
+ name = rest.slice(0, hardAt);
188
+ link = rest.slice(hardAt + ' link to '.length);
189
+ } else {
190
+ const symAt = rest.lastIndexOf(' -> ');
191
+ if (symAt >= 0) {
192
+ name = rest.slice(0, symAt);
193
+ link = rest.slice(symAt + 4);
194
+ }
195
+ }
196
+ return {
197
+ type: f[0][0],
198
+ name: name.replace(/\/$/, ''),
199
+ link: link === null ? null : link.replace(/\/$/, ''),
200
+ };
177
201
  }
178
202
 
179
203
  function safeEntries(listedText, base) {
180
204
  const entries = [];
181
205
  const bad = [];
206
+ const insideBase = (p) => Boolean(p)
207
+ && !p.startsWith('/') && !/^[A-Za-z]:/.test(p)
208
+ && !p.split('/').includes('..')
209
+ && (p === base || p.startsWith(`${base}/`));
182
210
  for (const line of listedText.split('\n')) {
183
211
  const s = line.trim();
184
212
  if (!s) continue;
185
213
  const parsed = parseTarEntry(s);
186
214
  const name = parsed ? parsed.name.replace(/\\/g, '/').replace(/\/$/, '') : null;
187
- if (
188
- !parsed || !name
189
- || (parsed.type !== '-' && parsed.type !== 'd')
190
- || name.startsWith('/') || /^[A-Za-z]:/.test(name)
191
- || name.split('/').includes('..')
192
- || (name !== base && !name.startsWith(`${base}/`))
193
- ) {
215
+ const link = parsed && parsed.link ? parsed.link.replace(/\\/g, '/').replace(/\/$/, '') : null;
216
+ // 与插件侧同语义:放行目标也在备份根内的硬链接 'h'(DSH 附件库的正常形态,
217
+ // 见 issue #112),符号链接/设备等仍拒绝。救援台是宿主起不来时的唯一通路,
218
+ // 这里漏判会让"插件写出的归档自己都恢复不了"。
219
+ const plainOk = Boolean(parsed) && Boolean(name) && (parsed.type === '-' || parsed.type === 'd') && insideBase(name);
220
+ const hardOk = Boolean(parsed) && parsed.type === 'h' && insideBase(name) && insideBase(link);
221
+ if (!plainOk && !hardOk) {
194
222
  bad.push(parsed ? name : s);
195
223
  } else {
196
224
  entries.push(name);
@@ -338,7 +366,7 @@ async function collectSessionLogs(dir, relPrefix, depth, out) {
338
366
  if (d.isDirectory()) {
339
367
  if (SCAN_SKIP_DIRS.has(d.name)) continue;
340
368
  await collectSessionLogs(`${dir}/${d.name}`, rel, depth + 1, out);
341
- } else if (SESSION_LOG_NAMES.has(d.name)) {
369
+ } else if (isSessionLogName(d.name)) {
342
370
  out.push({ abs: `${dir}/${d.name}`, rel });
343
371
  }
344
372
  }
@@ -610,23 +638,27 @@ const show = (r) => {
610
638
  const log = (t) => { document.getElementById('log').textContent = typeof t === 'string' ? t : show(t); };
611
639
  const dlg = document.getElementById('confirm');
612
640
  const fmtSize = (n) => typeof n !== 'number' ? '?' : n >= 1048576 ? (n/1048576).toFixed(1) + 'MB' : Math.max(1, Math.round(n/1024)) + 'KB';
641
+ const esc = (s) => String(s).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
613
642
  async function api(op, body) {
614
643
  const res = await fetch('/api/' + op, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Dsh-Rescue': '1' }, body: JSON.stringify(body || {}) });
615
644
  return res.json();
616
645
  }
646
+ let listNames = [];
617
647
  async function renderList() {
618
648
  const r = await api('list');
619
649
  if (!r.ok) { log(r); return; }
620
- const rows = r.backups.map(b => '<tr><td>' + b.name + '</td><td>' + fmtSize(b.size) +
621
- '</td><td><button onclick="verify(\\'' + b.name + '\\')">校验</button> ' +
622
- '<button class="danger" onclick="restore(\\'' + b.name + '\\')">恢复…</button></td></tr>').join('');
650
+ listNames = r.backups.map(b => b.name);
651
+ const rows = r.backups.map((b, i) => '<tr><td>' + esc(b.name) + '</td><td>' + fmtSize(b.size) +
652
+ '</td><td><button onclick="verify(' + i + ')">校验</button> ' +
653
+ '<button class="danger" onclick="restore(' + i + ')">恢复…</button></td></tr>').join('');
623
654
  document.getElementById('list').innerHTML = r.backups.length
624
655
  ? '<table><tr><th>备份</th><th>大小</th><th></th></tr>' + rows + '</table>'
625
656
  : '<p>备份目录里没有找到归档。</p>';
626
657
  log(r.summary || '');
627
658
  }
628
- async function verify(name) { log(await api('verify', { selector: name })); }
629
- async function restore(name) {
659
+ async function verify(i) { log(await api('verify', { selector: listNames[i] })); }
660
+ async function restore(i) {
661
+ const name = listNames[i];
630
662
  const pre = await api('restore', { selector: name, dryRun: true });
631
663
  log(pre);
632
664
  if (!pre.ok) return;