memory-eternal 0.10.26 → 0.10.28

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/lib/db.js CHANGED
@@ -4,7 +4,7 @@
4
4
  // 首次启动自动从 .md 文件迁移(幂等)
5
5
 
6
6
  import { DatabaseSync } from 'node:sqlite'
7
- import { promises as fs, mkdirSync } from 'node:fs'
7
+ import { promises as fs, mkdirSync, existsSync, renameSync, copyFileSync } from 'node:fs'
8
8
  import path from 'node:path'
9
9
  import { parseCard, KIND_ROOTS } from './vault.js'
10
10
 
@@ -441,6 +441,112 @@ export function closeAllDb() {
441
441
  * SQLite 备份:用 VACUUM INTO 创建紧凑副本。
442
442
  * 保留最近 maxKeep 个备份,自动清理旧的。
443
443
  */
444
+ /**
445
+ * 拿只读连接**当场**检查一个 .db 文件是否可用(quick_check),并尽可能数出卡片数。
446
+ * 为什么不复用 dbStatus():那是**缓存**的打开时结论,文件可能在那之后被写坏/替换 ——
447
+ * 恢复流程必须看**此刻**的真相。
448
+ * @returns {{ok:boolean, cards:number, error:string}}
449
+ */
450
+ export function probeDbFile(file) {
451
+ let probe = null
452
+ try {
453
+ probe = new DatabaseSync(file, { readOnly: true })
454
+ const row = probe.prepare('PRAGMA quick_check').get()
455
+ const verdict = String((row && (row.quick_check || Object.values(row)[0])) || '')
456
+ if (verdict.toLowerCase() !== 'ok') return { ok: false, cards: -1, error: 'quick_check 未通过:' + verdict }
457
+ let cards = -1
458
+ try { cards = Number(probe.prepare('SELECT count(*) AS n FROM cards').get().n) } catch { cards = -1 }
459
+ return { ok: true, cards, error: '' }
460
+ } catch (e) {
461
+ return { ok: false, cards: -1, error: String((e && e.message) || e) }
462
+ } finally {
463
+ try { probe && probe.close() } catch { /* 忽略 */ }
464
+ }
465
+ }
466
+
467
+ /**
468
+ * 从备份恢复数据库(issue #31 的现实出路:WAL 头一旦损坏,代码修不了**数据**,只能用备份重建)。
469
+ *
470
+ * 硬约束 —— **宁可拒绝,也不破坏现场**:
471
+ * 1. **不删任何东西**:当前库连同 -wal/-shm 一律改名成 `*.corrupt-<时间戳>` 留证;
472
+ * 2. 替换前先**校验备份真的可用**(能打开 + `PRAGMA quick_check` 通过 + 能数出卡片);
473
+ * 3. 拒绝「拿正在使用的库当备份」(自己替换自己);
474
+ * 4. 当前库**明明可写**时需要显式 `force`(防止手滑覆盖好数据);
475
+ * 5. 恢复后**复核**(重新打开 + 写探针 + 卡片数),结果如实返回。
476
+ *
477
+ * @param {string} root vault 目录
478
+ * @param {{from?:string, force?:boolean, dryRun?:boolean}} opts from:'latest' | 备份文件名 | 绝对路径
479
+ */
480
+ export async function restoreFromBackup(root, { from = '', force = false, dryRun = false } = {}) {
481
+ const dir = path.resolve(root)
482
+ const dbPath = path.join(dir, DB_FILE)
483
+ const backupsDir = path.join(dir, 'backups')
484
+ let src = String(from || '').trim()
485
+ if (!src) return { ok: false, error: '缺少备份来源(--restore latest 或 --restore <备份文件名>' + ')' }
486
+ if (src === 'latest') {
487
+ const files = (await fs.readdir(backupsDir).catch(() => []))
488
+ .filter((f) => f.startsWith('memory-eternal-') && f.endsWith('.db')).sort().reverse()
489
+ if (!files.length) return { ok: false, error: '没有可用备份:' + backupsDir }
490
+ src = path.join(backupsDir, files[0])
491
+ } else if (!src.includes('/') && !src.includes('\\')) {
492
+ src = path.join(backupsDir, src)
493
+ }
494
+ src = path.resolve(src)
495
+ if (src === path.resolve(dbPath)) return { ok: false, error: '备份文件不能是正在使用的库本身' }
496
+ let st
497
+ try { st = await fs.stat(src) } catch { return { ok: false, error: '备份文件不存在:' + src } }
498
+ if (!st.isFile()) return { ok: false, error: '不是文件:' + src }
499
+
500
+ // ② 校验备份可用(只读连接 + quick_check + 卡片数)
501
+ const backup = probeDbFile(src)
502
+ if (!backup.ok) return { ok: false, error: '备份不可用(' + src + '):' + backup.error }
503
+ const cards = backup.cards
504
+
505
+ // ④ 当前库**此刻**仍然健康时要求显式 force(手滑保护)—— 用当场探测,别信缓存
506
+ const curNow = existsSync(dbPath) ? probeDbFile(dbPath) : { ok: false }
507
+ if (curNow.ok && !force) {
508
+ return { ok: false, code: 'DB_HEALTHY', error: '当前库此刻是完好的(quick_check 通过),恢复会覆盖它 —— 确认要恢复请加 --force', cards }
509
+ }
510
+ if (dryRun) return { ok: true, dryRun: true, from: src, cards, note: '仅演练:未改任何文件' }
511
+
512
+ // ① 先关掉本进程里这个库的连接(Windows 上不关会改名失败)
513
+ try { closeDb(dir) } catch { /* 忽略 */ }
514
+ // ② 现场留证:库 + -wal + -shm 全部改名(**不删**)
515
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-')
516
+ const movedAside = []
517
+ for (const suffix of ['', '-wal', '-shm']) {
518
+ const f = dbPath + suffix
519
+ if (!existsSync(f)) continue
520
+ const to = f + '.corrupt-' + stamp
521
+ try { renameSync(f, to); movedAside.push(to) } catch (e) {
522
+ return { ok: false, error: '现场留证失败(' + f + '):' + String((e && e.message) || e) + '(未做任何替换)', movedAside }
523
+ }
524
+ }
525
+ // ③ 备份 → 库
526
+ try { copyFileSync(src, dbPath) } catch (e) {
527
+ return { ok: false, error: '写入新库失败:' + String((e && e.message) || e), movedAside }
528
+ }
529
+ // ⑤ 复核
530
+ try {
531
+ const db = getDb(dir)
532
+ const health = checkDbHealth(db)
533
+ const n = Number(db.prepare('SELECT count(*) AS n FROM cards').get().n)
534
+ return {
535
+ ok: Boolean(health.writable),
536
+ from: src,
537
+ cards: n,
538
+ backupCards: cards,
539
+ health,
540
+ movedAside,
541
+ note: health.writable
542
+ ? '已用备份重建;原现场已改名留证(可人工检查后再删)'
543
+ : '已替换文件,但写探针仍失败:请贴 doctor 输出',
544
+ }
545
+ } catch (e) {
546
+ return { ok: false, error: '复核失败:' + String((e && e.message) || e), movedAside, from: src }
547
+ }
548
+ }
549
+
444
550
  export async function backupDb(root, { maxKeep = 7 } = {}) {
445
551
  const resolved = path.resolve(root)
446
552
  const dbPath = path.join(resolved, DB_FILE)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memory-eternal",
3
- "version": "0.10.26",
3
+ "version": "0.10.28",
4
4
  "description": "记忆核心(Memory Eternal):自研的 DeepSeek Harness 记忆插件,不移植任何既有记忆框架——对话结束后自动沉淀知识卡到本地 Markdown Vault(自研去重、自研 CJK 检索、可 git 管理),设置页提供图形化知识库(统计 / 搜索 / 知识图谱 + 侧边栏一键弹窗),Agent 通过 memory_recall 工具按需召回历史上下文。零人工干预。",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -64,7 +64,7 @@
64
64
  "prepublishOnly": "node build.mjs",
65
65
  "release": "node scripts/release.mjs",
66
66
  "release:dry": "node scripts/release.mjs --dry-run",
67
- "test": "node build.mjs && node tests/vault.test.mjs && node tests/capture.test.mjs && node tests/capture-route.test.mjs && node tests/stall.test.mjs && node tests/vault-path.test.mjs && node tests/vault-resolve.test.mjs && node tests/minhash.test.mjs && node tests/settings-save.test.mjs && node tests/config-coverage.test.mjs && node tests/config-sync.test.mjs && node tests/config-merge.test.mjs && node tests/sse.test.mjs && node tests/api.test.mjs && node tests/import-roundtrip.test.mjs && node tests/graph-perf.test.mjs && node tests/settings-compat.test.mjs && node tests/node-bin.test.mjs && node tests/watchdog-lock.test.mjs && node tests/markdown.test.mjs && node tests/feedback.test.mjs && node tests/graph-lod.test.mjs && node tests/ssr.test.mjs && node tests/web-assets.test.mjs && node tests/manifests.test.mjs && node tests/two-store.test.mjs && node tests/noise-gate.test.mjs && node tests/e2e-recall.test.mjs && node tests/issue-fixes.test.mjs && node tests/issue-fixes-2.test.mjs && node tests/issue-fixes-3.test.mjs && node tests/watchdog-orphans.test.mjs && node tests/watchdog-restart.test.mjs && node tests/readme-i18n.test.mjs && node tests/issue-fixes-4.test.mjs && node tests/host-drain.test.mjs && node tests/e2e-host-drain.mjs && node tests/issue-fixes-5.test.mjs && node tests/issue-fixes-6.test.mjs && node tests/capture-json-repair.test.mjs && node tests/csrf-origin.test.mjs && node tests/watchdog-orphan-web.test.mjs && node tests/config-validate.test.mjs && node tests/issue-fixes-8.test.mjs && node tests/issue-fixes-9.test.mjs && node tests/release-verify.test.mjs && node tests/refine.test.mjs"
67
+ "test": "node build.mjs && node tests/vault.test.mjs && node tests/capture.test.mjs && node tests/capture-route.test.mjs && node tests/stall.test.mjs && node tests/vault-path.test.mjs && node tests/vault-resolve.test.mjs && node tests/minhash.test.mjs && node tests/settings-save.test.mjs && node tests/config-coverage.test.mjs && node tests/config-sync.test.mjs && node tests/config-merge.test.mjs && node tests/sse.test.mjs && node tests/api.test.mjs && node tests/import-roundtrip.test.mjs && node tests/graph-perf.test.mjs && node tests/settings-compat.test.mjs && node tests/node-bin.test.mjs && node tests/watchdog-lock.test.mjs && node tests/markdown.test.mjs && node tests/feedback.test.mjs && node tests/graph-lod.test.mjs && node tests/ssr.test.mjs && node tests/web-assets.test.mjs && node tests/manifests.test.mjs && node tests/two-store.test.mjs && node tests/noise-gate.test.mjs && node tests/e2e-recall.test.mjs && node tests/issue-fixes.test.mjs && node tests/issue-fixes-2.test.mjs && node tests/issue-fixes-3.test.mjs && node tests/watchdog-orphans.test.mjs && node tests/watchdog-restart.test.mjs && node tests/readme-i18n.test.mjs && node tests/issue-fixes-4.test.mjs && node tests/host-drain.test.mjs && node tests/e2e-host-drain.mjs && node tests/issue-fixes-5.test.mjs && node tests/issue-fixes-6.test.mjs && node tests/capture-json-repair.test.mjs && node tests/csrf-origin.test.mjs && node tests/watchdog-orphan-web.test.mjs && node tests/config-validate.test.mjs && node tests/issue-fixes-8.test.mjs && node tests/issue-fixes-9.test.mjs && node tests/release-verify.test.mjs && node tests/refine.test.mjs && node tests/db-restore.test.mjs && node tests/align.test.mjs"
68
68
  },
69
69
  "dependencies": {
70
70
  "@deepseek-ai/schemastery": "^3.18.1"
@@ -166,10 +166,22 @@ if (SKIP_NPM) {
166
166
  }
167
167
  // **不信 stdout,只信 registry**:publish 返回 0 不代表包真的上去了(0.10.21 实测:
168
168
  // 本地 packument/npm view 缓存会假装没有,边缘缓存又会假装有)—— 用 version 端点实证。
169
+ // 端点滞后(实测可达数分钟)不该中断后面的 tag/push/Release —— 但也**绝不能**当成已发布:
170
+ // 记进问题清单,继续跑完,最后汇总 + 非零退出(0.10.22 / 0.10.26 都因这里超时中断过一次发布)。
169
171
  if (!(await waitNpmVersion(pkg.version))) {
170
- throw new Error(`npm 上查不到 ${pkg.version}(version 端点超时):发布可能未生效,请手动 \`npm publish --access public\` 后复核 https://registry.npmjs.org/memory-eternal/${pkg.version}`)
172
+ // 端点滞后可能有几分钟(实测 0.10.26:publish 先"暂存"、稍后才落地)。再打一次 publish 探针:
173
+ // npm 若回「previously staged / previously published / 409」——那**恰好证明版本确实进去了**。
174
+ const probe = run('npm', ['publish', '--access', 'public'], { allowFail: true })
175
+ const txt = String(probe.out || '')
176
+ if (/previously staged version|previously published version|EPUBLISHCONFLICT|409/i.test(txt)) {
177
+ say('npm version 端点滞后,但 publish 探针回报「版本已存在 / 已暂存」→ 视为**已发布**')
178
+ } else {
179
+ say(`⚠ npm version 端点仍未可见:https://registry.npmjs.org/memory-eternal/${pkg.version}(可能只是 CDN 滞后)`)
180
+ problems.push(`npm 上暂时查不到 ${pkg.version} → 稍后复核(200 才算真的发布成功):https://registry.npmjs.org/memory-eternal/${pkg.version}`)
181
+ }
182
+ } else {
183
+ say(`npm 校验通过:https://registry.npmjs.org/memory-eternal/${pkg.version} → 200`)
171
184
  }
172
- say(`npm 校验通过:https://registry.npmjs.org/memory-eternal/${pkg.version} → 200`)
173
185
  }
174
186
  }
175
187