memory-eternal 0.9.17 → 0.10.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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://anthropic.com/claude-code/plugin.schema.json",
3
3
  "name": "memory-eternal",
4
- "version": "0.9.17",
4
+ "version": "0.9.23",
5
5
  "description": "璁板繂鏍稿績锛圡emory Eternal锛夛細鐭ヨ瘑鍗¤嚜鍔ㄦ矇娣€/鍙洖锛岀粺涓€ ~/.dsh/memory-vault锛屼細璇濈粨鏉熻嚜鍔ㄥ叆搴撹繘寰呭鏍搞€?,
6
6
  "author": {
7
7
  "name": "EternalNight996",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memory-eternal",
3
- "version": "0.9.17",
3
+ "version": "0.9.23",
4
4
  "description": "璁板繂鏍稿績锛圡emory Eternal锛夛細鐭ヨ瘑鍗¤嚜鍔ㄦ矇娣€/鍙洖锛岀粺涓€ ~/.dsh/memory-vault锛屼細璇濈粨鏉熻嚜鍔ㄥ叆搴撹繘寰呭鏍搞€?,
5
5
  "author": {
6
6
  "name": "EternalNight996",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memory-eternal",
3
- "version": "0.9.17",
3
+ "version": "0.9.23",
4
4
  "description": "璁板繂鏍稿績锛圡emory Eternal锛夛細鐭ヨ瘑鍗¤嚜鍔ㄦ矇娣€/鍙洖锛岀粺涓€ ~/.dsh/memory-vault锛屼細璇濈粨鏉熻嚜鍔ㄥ叆搴撹繘寰呭鏍搞€?,
5
5
  "author": {
6
6
  "name": "EternalNight996",
package/README.en.md CHANGED
@@ -306,12 +306,23 @@ Diagnostics:
306
306
 
307
307
  ## 📋 Changelog
308
308
 
309
- > **Withdrawn versions**: **v0.9.14 is deprecated** (its client-rendering changes hurt the feel and were reverted; its server-side algorithm work shipped in **v0.9.15**, whose rendering is identical to v0.9.13). **v0.9.0 – v0.9.5 are all deprecated on npm, and the release tags for v0.9.1 / v0.9.2 / v0.9.3 / v0.9.4 have been removed from git** (v0.9.5 is merely superseded, so its tag stays) — v0.9.1/v0.9.2 ship the blank-memory-page defect, v0.9.0 false-alarms its capture warnings, v0.9.3 does not support DSH v0.1.7-rc.2 (the plugin stops mounting entirely: the settings service changed and the client-side `settingsScope` is gone), v0.9.4 crashes under the official desktop build (schemastery 3.18.4 volatile live references) with `cfg.vaultDir.trim is not a function`, and **v0.9.5's full-screen overlay covers the desktop shell's window controls (its top-right "×" sits on the window Close button — one click quits the whole shell)**. Use **v0.9.6+** (`npm i memory-eternal@latest`).
310
-
309
+ > **Withdrawn versions**: **v0.9.16 – v0.9.22 are all withdrawn** (problem-period releases: git tags deleted, npm entries deprecated — use **v0.9.23**). **v0.9.14 is deprecated** (its client-rendering changes hurt the feel and were reverted; its server-side algorithm work shipped in **v0.9.15**, whose rendering is identical to v0.9.13). **v0.9.0 – v0.9.5 are all deprecated on npm, and the release tags for v0.9.1 / v0.9.2 / v0.9.3 / v0.9.4 have been removed from git** (v0.9.5 is merely superseded, so its tag stays) — v0.9.1/v0.9.2 ship the blank-memory-page defect, v0.9.0 false-alarms its capture warnings, v0.9.3 does not support DSH v0.1.7-rc.2 (the plugin stops mounting entirely: the settings service changed and the client-side `settingsScope` is gone), v0.9.4 crashes under the official desktop build (schemastery 3.18.4 volatile live references) with `cfg.vaultDir.trim is not a function`, and **v0.9.5's full-screen overlay covers the desktop shell's window controls (its top-right "×" sits on the window Close button — one click quits the whole shell)**. Use **v0.9.6+** (`npm i memory-eternal@latest`).
311
310
  > **DSH compatibility**: `>=0.1.5-alpha.2 <0.2.0`. **v0.9.4 adds support for DSH v0.1.7-rc.2**, which replaced the settings service with a forms-only API and removed `@deepseek-ai/dsh-client-runtime` / `settingsScope`; **v0.9.5 adds support for schemastery ≥3.18.4 volatile live references** (the shape the official desktop profile uses, while the web profile still ships 3.18.1 — both work); **v0.9.6 keeps full-screen overlays clear of the desktop shell's window controls**. The 0.1.5 line still uses the legacy `settings.register` path; both paths are kept.
312
311
 
313
312
  | Version | Date | Highlights |
314
313
  |---|---|---|
314
+ | **v0.9.23** | 2026-09-30 | **No more save flicker.** After a successful save the client immediately re-fetches `/config`, but the host volatile echo lags, so inputs visibly reverted to the old values and then jumped back. The client now keeps a **local overlay** for fields that were saved but not yet echoed (`src/client/config-merge.js`, pure function + 4 checks), dropping each key once the host echoes it, and following the host value afterwards; resetting the form clears the overlay. `npm test` now runs **139 checks** |
315
+
316
+ <details>
317
+ <summary>📜 Previous releases (click to expand)</summary>
318
+
319
+ | Version | Date | Highlights |
320
+ |---|---|---|
321
+ | **v0.9.22** | 2026-09-30 | **Near-instant config sync (SSE push + file watching).** ① A new SSE hub (`lib/sse.js`): both hosts (DSH-embedded and standalone web) expose `/memory-eternal/api/events`, so a config change is pushed to every open page and the client reloads automatically — **without overwriting what you are currently typing** (dirty flag). ② Standalone save → DSH apply now triggers via `fs.watch` in **milliseconds** instead of a 5-second poll (the poll remains as a fallback). ③ The standalone page watches the shared config file, so a DSH-side save shows up there immediately. Adds `tests/sse.test.mjs` (5 checks); `npm test` now runs **135 checks** |
322
+ | **v0.9.21** | 2026-09-30 | **A successful save is no longer shown as a failure + config layout fix.** ① Success always uses the success style (the "written, host has not echoed yet" case used to pop up in the red failure style, which read as "save failed"), reworded to "saved to the config file; restart DSH to make it effective"; ② config field grid widened from 150/160px to **200/220px** with larger gaps and wrapping long labels, checkbox and text top-aligned — fields no longer crowd each other. |
323
+ | **v0.9.20** | 2026-09-30 | Standalone web page gains **range validation**: `/config` mirrors the Config schema min/max (`recycleRetentionDays` 1–3650, `webPort` 1–65535), returning **400** with "needs 1–3650, now 0" for out-of-range values. The most common save-failure path (cleared/out-of-range number) is now blocked with a field-level reason at all three layers: client, standalone page and host. |
324
+ | **v0.9.19** | 2026-09-30 | **"Save failed" now says why (root cause: clearing a number box yields 0, the schema rejects it, and the host answered with an empty 500).** Reproduced against the live host: out-of-range value / cleared number box / wrong type → **HTTP 500 with an empty body** → the client `r.json()` throws → only the generic "Save failed" is shown. Fixes: ① clearing a number input **no longer silently becomes 0** (an empty string coerced to a number is 0, while `recycleRetentionDays` requires `min(1)`); ② pre-save **per-field range validation** that names the field ("recycleRetentionDays must be a number between 1 and 3650, now 0"); ③ defensive response parsing: on a non-JSON or empty body the panel shows the HTTP status and the raw snippet instead of "Save failed"; ④ the host POST is wrapped so it **never returns an empty body**, and returns 400 plus the field names for empty numeric values; ⑤ the standalone `/config` validates types and ranges so invalid pending values cannot be written; ⑥ `drainPendingConfig` counts failures and gives up (deleting the file) after 5 tries instead of retrying forever. `npm test` now runs **130 checks** |
325
+ | **v0.9.18** | 2026-09-30 | **The standalone web page can finally save config — and a read-only page no longer feels dead.** Root cause (the other half of issue #12): the standalone web server has **no DSH settings service**, so its `/config` always returned `writable:false`; the client `save()` starts with `if (readonly) return` and the button was `disabled` — clicking Save did **nothing at all**, independent of the plugin version. Fixes: ① new shared-file protocol in `lib/config-sync.js`: the standalone page writes `memory-eternal-config.pending.json` atomically, and the **DSH host applies it on activation and every 5 seconds** via `settings.update(patch, undefined)` (skipping the optimistic-concurrency check), deleting the file on success and retrying on failure; if DSH is not running the change waits in the file for the next start. ② The standalone `/config` now reports `writable:true, viaPending:true` and the page shows a blue note explaining the sync. ③ **The button is no longer disabled when read-only** — clicking always explains why (`🔒 Read-only` + "desktop app → Settings → Memory"), so "no reaction" can no longer happen. Adds `tests/config-sync.test.mjs` (4 checks). `npm test` now runs **125 checks** |
315
326
  | **v0.9.17** | 2026-09-30 | **"I updated it but the UI is still old" — fixed for good.** ① The build now injects the plugin version into the client bundle and the settings page shows a "**Page script vX**" chip; ② when the page-script version differs from the on-disk version it says so loudly: "⚠ This page is still running an older script (page v{page} / disk v{disk}): **press Ctrl+F5**"; the running-vs-disk hint now also leads with "refresh first, restart the desktop app / DSH only if the host half is old". Root cause of issue #12 ("saving shows no feedback"): the page was still executing the JS loaded before the update — the host sends `Cache-Control: no-store`, but **an already-open page never reloads its scripts**, so without a refresh you keep seeing the old UI. The standalone web service on `:7999` (an old process started 9/29) was restarted and now reports `loaded=onDisk=latest` |
316
327
  | **v0.9.16** | 2026-09-30 | **Fixed config saves silently not persisting (issue #12) + full config coverage.** ① **Save path**: the host `dsh-settings.write()` requires the revision held by the panel to match `describe()` **exactly**; once stale it throws `SettingsConflictError` and the save fails — the plugin now re-reads the latest revision and **retries once** (marked ♻ in the message). ② **Late volatile echo**: after a successful write the patch is overlaid on the local view (reopening the panel shows the new value immediately) and the overlay is dropped once the host snapshot catches up; the host POST now **re-reads after writing** and returns `pending` with an honest note ("written; restart DSH to apply from the config file") when the echo has not landed. ③ **Immediate save feedback** (requested earlier): ⏳ Saving / ✅ Saved / ⚠ error now appears right next to the Save button, including whether a retry or an echo was involved, and the button label no longer wrongly says "Exporting…". ④ **Full config coverage**: seven fields that previously could only be edited by hand are now in the panel — the **master switch `enabled`**, single-vault `vaultDir`, distillation route `captureProvider` / `captureModel`, cooldown `captureCooldownMs`, semantic recall `recallEmbedding` and `sessionBudgetChars`; the host `/config` gained 6 keys and the standalone web `/config` gained 9. ⑤ New **permanent guard tests**: `tests/config-coverage.test.mjs` asserts every Config field is covered by the host API, the standalone web API and the settings panel (missing one turns the suite red), and `tests/settings-save.test.mjs` (5 checks) pins revision retry, overlay fallback and "never swallow errors". `npm test` now runs **121 checks** |
317
328
  | **v0.9.15** | 2026-09-29 | **Server-side graph speed-ups only — rendering is identical to v0.9.13.** ① **On-disk graph cache** keyed by fingerprint at `<vault>/memory-eternal-graph-cache.json` (atomic write, invalidated by fingerprint, skipped above 8MB, failures only cost a rebuild): measured cold start **422ms → 11ms** at 596 cards; ② **hashed bigrams + counting-sorted rare-gram index**: `bigramHashSet()` packs each bigram into a 32-bit integer (`Set<number>`; **capture de-duplication still uses the string version, so semantics are unchanged**), and the rare-gram index is built with a counting sort (202 → **54ms**); ③ **MinHash + LSH** candidate generation (48-dim signatures, 24 bands, unioned with the rare-gram index and then **verified exactly**): Jaccard calls 15,148 → 6,797 and the similarity stage 210 → 130ms. Full cold build **600 → 422ms**, edges unchanged from v0.9.13 (tag 2777 / similar 4). **Zero client changes** — no sprites, no static layer, no skeleton degradation (the v0.9.14 client work was reverted and deprecated because it hurt the feel). Adds `lib/minhash.js` + `tests/minhash.test.mjs` (4 checks); `npm test` runs **108 checks** |
@@ -337,6 +348,8 @@ Diagnostics:
337
348
  | v0.1.1 | 2026-09-03 | Recall body fix (`excerpt` field + body logic) |
338
349
  | v0.1.0 | 2026-08-31 | First release: auto capture + auto recall + graphical knowledge base + knowledge graph + audit center + recycle bin |
339
350
 
351
+ </details>
352
+
340
353
  ---
341
354
 
342
355
  ## 💬 Community & Support
package/README.md CHANGED
@@ -232,6 +232,14 @@ dsh-memory watchdog [--port 7799] # 看门狗保活 web(独立进程)
232
232
 
233
233
  > **v0.3.0 起:审核守卫下沉到数据库层。** 所有写入路径(自动沉淀 / MCP / UI 新建 / 导入)强制经过 `enforceAudit()`,调用方无法绕过。`setCardStatus()` 是唯一审核操作入口,每次变更写入 `audit_log` 审计日志(who/what/when/why),不可篡改。
234
234
 
235
+ > **v0.10.0 起:升级为「两套存储」——未审核内容物理隔离。**
236
+ > `cards`(**主库 / 正常区**)物理上**只存 approved**;`quarantine`(**隔离区 / 异常区**)存 `pending` / `rejected` / `deleted`。
237
+ > 意义:召回、检索、知识图谱、去重池、导出这些读路径**查主库即安全**——不再依赖「每处 SQL 都记得写 `WHERE status='approved'`」。
238
+ > (v0.9 审计实测漏过三处:去重池会把新知识追加进待审卡、`readCard` 可按 path 直读未审核正文、`/card` 接口无状态校验。)
239
+ > 审核流转 = **跨表搬家**:批准 → 搬进主库,驳回/软删 → 搬进隔离区;搬家在一个事务里完成,更新记录与审计日志跟着走。
240
+ > 升级说明:首次启动会自动把存量非 approved 卡搬进隔离区(**不删除任何内容**,都在同一个 `.db` 文件里)。
241
+ > 若回滚到旧版本,审核队列会显示为空(旧代码看不到隔离区表),升级回来即恢复。
242
+
235
243
  <p align="center">
236
244
  <img src="https://raw.githubusercontent.com/EternalNight996/memory-eternal/main/assets/screen/audit-center.png" width="880" alt="审核中心" />
237
245
  </p>
@@ -330,12 +338,24 @@ curl http://127.0.0.1:7999/memory-eternal/api/web-info # 独立 web 是否活
330
338
 
331
339
  ## 📋 更新日志
332
340
 
333
- > **版本撤回说明**:**v0.9.14 已 deprecated**(客户端渲染改动降低手感,已回退;其服务器侧算法优化并入 **v0.9.15**,渲染与 v0.9.13 完全一致)。**v0.9.0 – v0.9.5 均已在 npm 标记 deprecated;v0.9.1 / v0.9.2 / v0.9.3 / v0.9.4 的 release tag 已从 git 移除**(v0.9.5 只是被取代、tag 保留)—— v0.9.1/v0.9.2 带记忆页白屏缺陷,v0.9.0 沉淀告警误报刷屏,v0.9.3 不支持 DSH v0.1.7-rc.2(升级后插件整体挂不上:设置服务换血 + 客户端 `settingsScope` 消失),v0.9.4 在官方桌面版(schemastery 3.18.4 的 volatile 活引用)下抛 `cfg.vaultDir.trim is not a function`、插件整体挂不上,**v0.9.5 的全屏浮层会盖住桌面版的窗口控制面板(右上角「×」压在窗口「关闭」上,点一下会退出整个桌面壳)**。请一律使用 **v0.9.6+**(`npm i memory-eternal@latest`)。
334
-
341
+ > **版本撤回说明**:**v0.9.16 – v0.9.22 已全部撤回**(问题期间版本:git tag 已删除、npm 已标记 deprecated,请使用 **v0.9.23**)。**v0.9.14 已 deprecated**(客户端渲染改动降低手感,已回退;其服务器侧算法优化并入 **v0.9.15**,渲染与 v0.9.13 完全一致)。**v0.9.0 – v0.9.5 均已在 npm 标记 deprecated;v0.9.1 / v0.9.2 / v0.9.3 / v0.9.4 的 release tag 已从 git 移除**(v0.9.5 只是被取代、tag 保留)—— v0.9.1/v0.9.2 带记忆页白屏缺陷,v0.9.0 沉淀告警误报刷屏,v0.9.3 不支持 DSH v0.1.7-rc.2(升级后插件整体挂不上:设置服务换血 + 客户端 `settingsScope` 消失),v0.9.4 在官方桌面版(schemastery 3.18.4 的 volatile 活引用)下抛 `cfg.vaultDir.trim is not a function`、插件整体挂不上,**v0.9.5 的全屏浮层会盖住桌面版的窗口控制面板(右上角「×」压在窗口「关闭」上,点一下会退出整个桌面壳)**。请一律使用 **v0.9.6+**(`npm i memory-eternal@latest`)。
335
342
  > **DSH 兼容性**:`>=0.1.5-alpha.2 <0.2.0`。**v0.9.4 起适配 DSH v0.1.7-rc.2**(该版本把设置服务换成纯表单 API,并移除了 `@deepseek-ai/dsh-client-runtime` 与 `settingsScope`);**v0.9.5 起兼容 schemastery ≥3.18.4 的 volatile 活引用**(官方桌面版 profile 即此形态,web profile 仍是 3.18.1,两者都支持);**v0.9.6 起全屏浮层自动让开桌面壳的窗口标题栏**(不再压住最小化/最大化/关闭)。0.1.5 系列仍走旧的 `settings.register` 路径,两条路径都保留。
336
343
 
337
344
  | 版本 | 日期 | 关键改动 |
338
345
  |---|---|---|
346
+ | **v0.10.0** | 2026-09-30 | **两套存储:未审核内容物理隔离**。① `cards`(主库)只存 `approved`,新增 `quarantine`(隔离区,含 `quarantined_at` / `quarantine_reason`)存 `pending`/`rejected`/`deleted`;② 召回/检索/图谱/去重/导出等读路径**不再需要状态条件**(查主库即安全),根治 v0.9 审计出的三处绕过审核漏洞(去重池把新知识写进待审卡、`readCard` 直读未审核正文、`/card` 无状态校验);③ 审核流转改为**跨表搬家**(事务内完成,id 由新增的 `card_sequence` 单一发号器分配,`card_updates` 随卡改绑);④ 首次启动**自动迁移**存量非 approved 卡(不删内容,幂等);⑤ 新增 `checkMainStoreInvariant()` 不变量体检与 `tests/two-store.test.mjs`(15 条);⑥ 入料噪声闸门:剥离 DSH 运行时注入(环境快照 / team 广播 / teammate 原文 / 工具说明),挡住碎片卡与糊标题;⑦ 修 `settings-compat` 在 Windows 上的原生崩溃(`fs.watch` 父目录 → 改监听文件) |
347
+ | **v0.9.23** | 2026-09-30 | **保存不再闪烁**。保存成功后客户端会立刻重拉 `/config`,而宿主 volatile 回流是滞后的 → 输入框会「先退回原配置、再跳回修改后的值」。现在客户端把「已保存但宿主未回显」的字段做**本地叠加**(新增 `src/client/config-merge.js` 纯函数 + 4 项单测),宿主回显一致后自动摘除,之后仍跟随宿主权威值;重置表单会清空叠加层。`npm test` 共 **139 项** |
348
+
349
+ <details>
350
+ <summary>📜 历史版本(点击展开)</summary>
351
+
352
+ | 版本 | 日期 | 关键改动 |
353
+ |---|---|---|
354
+ | **v0.9.22** | 2026-09-30 | **配置准即时同步(SSE 推送 + 文件监听)**。① 新增 SSE 推送中心 `lib/sse.js`:DSH 内嵌宿主与独立 Web 宿主都暴露 `/memory-eternal/api/events`,配置一变更就推给所有已打开的页面,客户端订阅后自动重载 —— **正在编辑时不覆盖你的输入**(dirty 标记)。② 独立页保存 → DSH 应用:从 5 秒轮询升级为 `fs.watch` **毫秒级**触发(5 秒轮询保留为兜底)。③ 独立页监听共享配置文件:DSH 端一保存,独立页立刻同步。新增 `tests/sse.test.mjs`(5 项),`npm test` 共 **135 项** |
355
+ | **v0.9.21** | 2026-09-30 | **保存成功不再被误读为失败 + 配置页排版修复**。① 保存成功一律用**成功样式**提示(原来「已写入;宿主尚未回流」被弹成红色失败样式,用户以为保存失败),措辞改为「已写入配置文件;宿主仍是旧版,重启 DSH 后即以新值为准」;② 配置页字段网格列宽 150/160px → **200/220px**、间距加大、长中文标签允许换行,勾选框与说明文字顶端对齐,字段不再互相挤压。 |
356
+ | **v0.9.20** | 2026-09-30 | 独立 Web 页补**取值范围校验**:`/config` 镜像 Config schema 的 min/max(如 `recycleRetentionDays` 1–3650、`webPort` 1–65535),越界直接 **400** 并说明「需要 1–3650,当前 0」。至此「数字框清空 / 越界」这条最常见的保存失败链路在**客户端 + 独立页 + 宿主**三层都被拦住并给出字段级原因。 |
357
+ | **v0.9.19** | 2026-09-30 | **「保存失败」不再无信息量(根因:数字框清空=0 被 schema 拒绝,宿主回空 500)**。复现与实测:越界值 / 清空数字框 / 类型错误 → DSH 宿主返回 **HTTP 500 + 空响应体** → 客户端 `r.json()` 抛错 → 只能显示通用「保存失败」。修复:① **数字输入框清空不再静默变成 0**(JS 里空串转数字得 0,而 `recycleRetentionDays` 的 schema 是 `min(1)`,直接失败);② 保存前按 schema 范围**逐字段自检**,指名报错(例如「recycleRetentionDays」需要 1–3650 之间的数字,当前 0);③ 响应体兜底解析:非 JSON / 空响应体时显示 HTTP 状态码与原始片段,而不是「保存失败」;④ 宿主 POST 外层兜底,**绝不返回空响应体**,并对空字符串数值字段前置返回 400 + 字段名;⑤ 独立页 /config 增加类型与范围校验(拒绝空数值、类型不符),避免写入非法「待应用」值;⑥ `drainPendingConfig` 增加失败计数,连续 5 次失败后放弃并删除(不再每 5 秒无限重试)。`npm test` 共 **130 项** |
358
+ | **v0.9.18** | 2026-09-30 | **独立 Web 页终于能保存配置 + 只读状态不再「毫无反应」**。根因(issue #12 的另一半):独立 web server 进程**没有 DSH 的 settings 服务**,它的 `/config` 过去固定返回 `writable:false`,客户端 `save()` 第一行就 `if (readonly) return`,按钮还是 `disabled` —— 所以点保存**一点反应都没有**,与插件版本无关。修复:① 新增共享文件协议 `lib/config-sync.js`:独立页保存 → 原子写 `memory-eternal-config.pending.json` → **DSH 端激活时与每 5 秒轮询应用**(`settings.update(patch, undefined)` 跳过乐观并发校验),成功后删除,失败保留重试;DSH 没运行时改动留在文件里,下次启动生效。② 独立页 `/config` 返回 `writable:true, viaPending:true`,页面顶部显示蓝色说明「保存会写入待应用文件,由 DSH 自动同步」。③ **只读时按钮不再禁用**:点击必弹出明确原因(`🔒 只读` + 指引「桌面版 → 设置 → 记忆」),彻底消灭「按了没反应」。新增 `tests/config-sync.test.mjs`(4 项:读写往返、损坏内容容错、drain 成功即删/失败保留、clear 幂等)。`npm test` 共 **125 项** |
339
359
  | **v0.9.17** | 2026-09-30 | **「更新了却还是旧界面」根治**。① 构建期把插件版本注入客户端 bundle,配置页新增「**页面脚本 vX**」徽标;② 当**页面脚本版本 ≠ 磁盘版本**时醒目提示「⚠ 当前页面仍在运行旧版脚本(页面 v{page} / 磁盘 v{disk}):**按 Ctrl+F5 刷新页面即生效**」;当「运行中(宿主加载)」落后于磁盘时,提示语也改为「先刷新页面,宿主半未更新才需重启桌面版 / DSH」。起因:issue #12「保存配置没有任何反馈」的真实原因之一,就是页面里跑的还是更新前加载的旧 JS —— 宿主已设 `Cache-Control: no-store`,但**已打开的页面不会自动重新加载脚本**,不刷新永远是旧的。另:独立 web 服务(`:7999`,9/29 启动的旧进程)已重启到新版并验证 `loaded=onDisk=latest` |
340
360
  | **v0.9.16** | 2026-09-30 | **修复配置保存不生效(issue #12)+ 配置项全覆盖**。① **保存链路**:宿主 `dsh-settings.write()` 要求「面板持有的 revision 与 describe **完全一致**」,revision 一过期就抛 `SettingsConflictError` 导致保存失败 —— 现在遇到冲突会自动取最新 revision **重试一次**(提示里标 ♻)。② **宿主 volatile 回流滞后**:写成功后把改动叠加在本地视图(面板重开立刻是新值),等宿主快照追上再自动摘除;宿主 POST 新增**写入回读校验**,未回流时返回 `pending` 并提示「已写入,重启 DSH 后以配置文件为准」。③ **保存即时反馈**(你此前反馈过):保存按钮旁直接显示 ⏳ 保存中 / ✅ 已保存 / ⚠ 错误,并标明是否重试/是否回流;按钮文案修正(原来误显示「导出中…」)。④ **配置全覆盖**:补齐 7 个以前只能手改配置文件的字段 —— **总开关 `enabled`**、单库目录 `vaultDir`、蒸馏路由 `captureProvider` / `captureModel`、沉淀冷却 `captureCooldownMs`、语义召回 `recallEmbedding`、会话预算 `sessionBudgetChars`;宿主 `/config` 补 6 项、独立 web `/config` 补 9 项。⑤ 新增**永久守卫测试**:`tests/config-coverage.test.mjs` 保证每个 Config 字段都被「宿主 API / 独立 web API / 配置页」三处覆盖(漏一个就红),`tests/settings-save.test.mjs`(5 项)钉死 revision 重试、回流兜底与「错误不吞」。`npm test` 共 **121 项** |
341
361
  | **v0.9.15** | 2026-09-29 | **只带服务器侧图谱提速(渲染与 v0.9.13 完全一致)**。① **建图落盘缓存**:按指纹缓存 nodes+edges 到 `<vault>/memory-eternal-graph-cache.json`(原子写、指纹失效、超 8MB 不缓存、写失败只影响下次重建),596 卡实测冷启动 **422ms → 11ms**;② **bigram 哈希化 + 稀有 gram 计数排序**:`bigramHashSet()` 把 bigram 压成 32 位整数入 `Set<number>`(**写卡去重仍用字符串版,判定语义不变**),稀有倒排改按 df 计数排序(202 → **54ms**);③ **MinHash + LSH 候选生成**(48 维签名 / 24 band,与稀有倒排取并集后**精确校验**):jaccard 调用 15,148 → 6,797,相似度段 210 → 130ms。全量冷建图 **600 → 422ms**,边数与 v0.9.13 一致(tag 2777 / similar 4)。**客户端零改动**:不做精灵/静态层/骨架降级(v0.9.14 那三项因影响手感已回退并 deprecated)。新增 `lib/minhash.js` + `tests/minhash.test.mjs`(4 项),`npm test` 共 **108 项** |
@@ -361,6 +381,8 @@ curl http://127.0.0.1:7999/memory-eternal/api/web-info # 独立 web 是否活
361
381
  | v0.1.1 | 2026-09-03 | recall 含正文修复(excerpt 字段 + body 逻辑修正) |
362
382
  | v0.1.0 | 2026-08-31 | 首发:自动沉淀 + 自动召回 + 图形化知识库 + 知识图谱 + 审核中心 + 回收中心 |
363
383
 
384
+ </details>
385
+
364
386
  ---
365
387
 
366
388
  ## 💬 交流群 & 打赏
@@ -0,0 +1,59 @@
1
+ # 记忆核心(memory-eternal)· MCP 对外通道示例 overlay
2
+ #
3
+ # 用途:把 memory-eternal 的 MCP stdio 服务挂进 DSH —— 让 **DSH 自己**也能通过官方
4
+ # `@deepseek-ai/dsh-mcp-client` 使用记忆库(与 Claude Code / Codex / Cursor 走同一条协议路径)。
5
+ #
6
+ # 什么时候需要它:
7
+ # · 你没装 memory-eternal 插件本体,只想用它的记忆库 → 用这个 overlay;
8
+ # · 你已经装了插件(推荐):插件是「一等公民」,这条 MCP 通道**不需要**,因为插件
9
+ # 自带 memory_recall 工具与系统提示段,还能拿到会话事件做自动沉淀。
10
+ #
11
+ # 用法(示例文件不在交付包里自动生效,必须显式 --patch 或合并进用户 patch 层):
12
+ # dsh web --patch "<仓库>/examples/mcp-memory/memory-eternal.cordis.yml"
13
+ # 或把下面的 insert 合并进 $DSH_HOME/cordis.patch.yml(本机所有 profile 生效)
14
+ # 或 $DSH_HOME/profiles/<name>/cordis.patch.yml(只对该 profile 生效)
15
+ #
16
+ # 前置条件(二进制已随包提供,无需额外安装):
17
+ # · Node 22+(memory-eternal engines 要求)
18
+ # · 全局安装或 profile 内安装 memory-eternal(提供 bin/dsh-memory.mjs)
19
+ # 全局:npm i -g memory-eternal
20
+ # profile 内:cd ~/.dsh/profiles/<name> && pnpm add memory-eternal
21
+ # 下面的 args 用的是「profile 内安装」路径;全局安装请改成
22
+ # args: ['dsh-memory', 'mcp'](Windows 下为 dsh-memory.cmd)。
23
+
24
+ - insert:
25
+ - id: memory-eternal-mcp
26
+ name: '@deepseek-ai/dsh-mcp-client'
27
+ config:
28
+ serverName: eternal
29
+ transport: stdio
30
+ command: node
31
+ args:
32
+ # 如果是全局安装,改成 ['dsh-memory', 'mcp']
33
+ - 'C:/Users/<你>/.dsh/profiles/web/node_modules/memory-eternal/bin/dsh-memory.mjs'
34
+ - 'mcp'
35
+ # 注意:stdio 桥接会主动**剥离名字像凭据的环境变量以及所有 DSH_* 变量**,其余继承。
36
+ # 因此:
37
+ # · 想让 MCP 进程用哪个库,显式在这里写 MEMORY_VAULT_DIR(不要依赖继承);
38
+ # · 想让 MCP 的 memory_capture 具备「LLM 蒸馏」能力,需要在这里显式传 MEMORY_LLM_*
39
+ # (这类变量名字含 KEY/TOKEN,很可能被桥接当凭据剥掉);
40
+ # · 不传就是「原文卡」降级模式,仍然可用,只是不压缩。
41
+ env:
42
+ # 留空 = 用插件自己的解析顺序(共享配置 memory-eternal-config.json → 默认 ~/.dsh/memory-vault)
43
+ MEMORY_VAULT_DIR: ''
44
+ # 可选:MCP 侧蒸馏用的 OpenAI 兼容端点
45
+ # MEMORY_LLM_BASE_URL: 'https://api.deepseek.com'
46
+ # MEMORY_LLM_MODEL: 'deepseek-chat'
47
+ # MEMORY_LLM_API_KEY: '<在此填 key>'
48
+ cwd: !!js process.cwd()
49
+
50
+ # 挂上之后 DSH 侧会看到这些工具(前缀由 serverName 决定):
51
+ # mcp__eternal__memory_recall —— 检索记忆(CJK 整词 + 字符 bigram,只读主库/approved)
52
+ # mcp__eternal__memory_capture —— 手动沉淀一段内容(走审核闸门,默认落隔离区 pending)
53
+ # mcp__eternal__memory_stats —— 记忆库概览
54
+ #
55
+ # 验证(与官方文档同一套三步骤):
56
+ # 1) 会话 A:让模型调用 mcp__eternal__memory_capture 写入一个唯一值;
57
+ # 2) 新建会话 B(不必重启宿主):问「我的验证饮料是什么?查记忆」→ 应调用 memory_recall 并返回该值;
58
+ # 3) 继续在 B 里用该偏好做一次推断。
59
+ # 本仓库 tests/e2e-recall.test.mjs 是同一套步骤的自动化版本(走 vault API)。
package/index.js CHANGED
@@ -29,7 +29,8 @@ import { ensureVault, search } from './lib/vault.js'
29
29
  import { migrateFromMarkdown, setAuditConfig, backupDb } from './lib/db.js'
30
30
  import { evaluateStall, MAX_STAMPS } from './lib/stall.js'
31
31
  import { resolveVaultDir, currentWorkspace } from './lib/vault-resolve.js'
32
- import { summarizeTurn, summarizeTurnDetailed, routeCandidates, extractLastTurn, sliceNewEvents, sessionEvents, sessionEventApi, createCaptureHealth, resolveRoute, captureCard, captureUpdate, pickNeighbors } from './lib/capture.js'
32
+ import { createHub } from './lib/sse.js'
33
+ import { summarizeTurn, summarizeTurnDetailed, routeCandidates, extractLastTurn, sliceNewEvents, sessionEvents, sessionEventApi, createCaptureHealth, resolveRoute, captureCard, captureUpdate, pickNeighbors, hasUsableContent, deriveTitle, looksTruncated } from './lib/capture.js'
33
34
  import { createApi, json, encodeBody } from './lib/api.js'
34
35
  import { appendCaptureLog, readCaptureLog, rotateCaptureLog } from './lib/capture-log.js'
35
36
  import { nodeBinary, childEnv } from './lib/node-bin.js'
@@ -251,6 +252,9 @@ export function bindSettings(ctx, schema, config) {
251
252
 
252
253
  export function apply(ctx, config) {
253
254
  const settings = bindSettings(ctx, Config, config)
255
+ // SSE:配置变更即时推送到所有已打开的页面(DSH 内嵌页 + 独立 Web 页),不必手动刷新
256
+ const hub = createHub()
257
+ ctx.effect(() => () => hub.close(), 'memory-eternal: sse hub')
254
258
 
255
259
  // 首次激活:自动从 .md 文件迁移到 SQLite(幂等,已有数据则跳过)
256
260
  // vault 解析与所有独立进程共用同一套优先级(见 lib/vault-resolve.js):
@@ -277,6 +281,31 @@ export function apply(ctx, config) {
277
281
  }
278
282
  syncAudit()
279
283
  settings.watch(syncAudit)
284
+ // 独立 Web 页保存的配置:它写「待应用」文件,这边应用后删除(DSH 没运行时下次启动生效)
285
+ const drainPending = async () => {
286
+ try {
287
+ const { drainPendingConfig } = await import('./lib/config-sync.js')
288
+ const applied = await drainPendingConfig(process.env, (patch) => settings.update(patch, undefined))
289
+ if (applied) {
290
+ syncConfigFile()
291
+ syncAudit()
292
+ try { console.error('[memory-eternal] 已应用独立 Web 端的配置改动: ' + Object.keys(applied).join(',')) } catch { /* 日志失败无妨 */ }
293
+ }
294
+ } catch { /* 失败保留文件,下一轮重试 */ }
295
+ }
296
+ drainPending()
297
+ // 5 秒轮询只是兜底;真正让它「准即时」的是下面这个文件监听(毫秒级)
298
+ const pendingTimer = setInterval(drainPending, 5000)
299
+ ctx.effect(() => () => clearInterval(pendingTimer), 'memory-eternal: pending config sync')
300
+ ctx.effect(() => {
301
+ let stop = () => {}
302
+ import('./lib/config-sync.js').then(({ watchPendingConfig }) => {
303
+ stop = watchPendingConfig(process.env, () => {
304
+ drainPending().then(() => hub.broadcast('config', { at: Date.now(), source: 'pending' })).catch(() => {})
305
+ })
306
+ }).catch(() => {})
307
+ return () => { try { stop() } catch { /* 已停止 */ } }
308
+ }, 'memory-eternal: pending config watch')
280
309
 
281
310
  // 所有 profile 目录(当前激活 + 其余命名的),供跨库聚合。
282
311
  const vaultRoots = () => {
@@ -406,6 +435,12 @@ export function apply(ctx, config) {
406
435
  const raw = extractLastTurn(events)
407
436
  const text = raw.length > CAPTURE_TEXT_MAX ? raw.slice(-CAPTURE_TEXT_MAX) : raw
408
437
  logCapture(sessionId, 'listen', `会话事件 ${events.length} 条 → 有效对话 ${text.length} 字${raw.length > text.length ? '(超长已截尾)' : ''}`, { preview: preview(text) })
438
+ // 噪声闸门(P0):extractLastTurn 已剥掉运行时注入(环境快照 / team 广播 / 工具说明),
439
+ // 这里再判一次「剥完还有没有真实内容」——只有注入残留的轮次不进 LLM、不写卡。
440
+ if (!hasUsableContent(text)) {
441
+ logCapture(sessionId, 'skip', `有效内容不足:剥离运行时注入后只剩 ${text.trim().length} 字(多为环境快照 / 广播 / 工具说明)`)
442
+ return
443
+ }
409
444
  if (text.length < (cfg.captureMinChars ?? 200)) {
410
445
  logCapture(sessionId, 'skip', `内容太短:${text.length} < ${cfg.captureMinChars ?? 200} 字(调小「捕获最小长度」可放宽)`)
411
446
  return
@@ -426,9 +461,10 @@ export function apply(ctx, config) {
426
461
  // 成本控制:distillEnabled=false 时不调 LLM,直接存原文卡(零蒸馏成本)
427
462
  const source = DSH_AGENT
428
463
  if (cfg.distillEnabled === false || !llm) {
464
+ const rawTitle = deriveTitle(text)
429
465
  const out = await captureCard(vaultDir(), {
430
466
  kind: 'content',
431
- title: text.replace(/\s+/g, ' ').slice(0, 40) || '未命名记录',
467
+ title: rawTitle.length >= 6 && !looksTruncated(rawTitle) ? rawTitle : '对话原文记录',
432
468
  tags: ['raw'],
433
469
  body: text,
434
470
  source,
@@ -468,9 +504,10 @@ export function apply(ctx, config) {
468
504
  if (failure) { health.fail(`蒸馏失败(${failure.provider}):${failure.code} ${failure.message}`); touchPrompt() }
469
505
  if (!result) {
470
506
  // 蒸馏失败不能让内容白丢:退成原文卡(与「关闭蒸馏」同一条降级路径)。
507
+ const rawTitle = deriveTitle(text)
471
508
  const raw = await captureCard(vaultDir(), {
472
509
  kind: 'content',
473
- title: text.replace(/\s+/g, ' ').slice(0, 40) || '未命名记录',
510
+ title: rawTitle.length >= 6 && !looksTruncated(rawTitle) ? rawTitle : '对话原文记录',
474
511
  tags: ['raw'],
475
512
  body: text,
476
513
  source,
@@ -763,6 +800,11 @@ export function apply(ctx, config) {
763
800
  if (pathname === API_PREFIX + '/web-info') {
764
801
  return json(res, 200, { ok: true, ...webInfo })
765
802
  }
803
+ if (pathname === API_PREFIX + '/events') {
804
+ // SSE 长连接:不 end,保持推送(EventSource 自动重连)
805
+ hub.add(res)
806
+ return
807
+ }
766
808
  if (pathname === API_PREFIX + '/setup-run') {
767
809
  // 「补全 MCP」:真正执行 runSetup(写外部 agent 配置),返回每项结果,成功/失败可见
768
810
  if (req.method !== 'POST') return json(res, 405, { ok: false, error: '需 POST' })
@@ -833,6 +875,7 @@ export function apply(ctx, config) {
833
875
  return json(res, 200, { ok: true, config: safe, revision: me?.revision ?? 0, writable: true, readonly: false, schema: me?.schema ?? null, dsh: dshInfo, version: versionRef })
834
876
  }
835
877
  if (method === 'POST') {
878
+ try {
836
879
  const cfgChunks = []
837
880
  for await (const chunk of req) cfgChunks.push(chunk)
838
881
  const raw = Buffer.concat(cfgChunks).toString('utf8')
@@ -845,12 +888,16 @@ export function apply(ctx, config) {
845
888
  const clean = {}
846
889
  for (const k of Object.keys(patch)) { if (allowed.has(k)) clean[k] = patch[k] }
847
890
  if (Object.keys(clean).length === 0) return json(res, 400, { ok: false, error: '无可写入字段' })
891
+ // 数值字段前置校验:空串/NaN/非数字直接给「哪个字段不合法」,而不是让宿主 schema 抛错
892
+ const badNum = Object.entries(clean).filter(([k, v]) => typeof v === 'string' && v.trim() === '' && !/^(vaultDir|captureProvider|captureModel|recallEmbedding|activeVault)$/.test(k))
893
+ if (badNum.length) return json(res, 400, { ok: false, error: '字段不能为空:' + badNum.map(([k]) => k).join('、'), fields: badNum.map(([k]) => k) })
848
894
  if (typeof settings.update === 'function') {
849
895
  try {
850
896
  const writeResult = await settings.update(clean, expectedRevision)
851
897
  // 把完整配置写入共享文件,让独立 web / MCP hook 与 DSH 设置同步(不同步修复)
852
898
  syncConfigFile()
853
899
  // 回读校验:宿主 volatile 回流可能滞后(issue #12),此时明确告知而不是假装成功
900
+ hub.broadcast('config', { at: Date.now(), source: 'dsh', applied: Object.keys(clean) })
854
901
  const now = settings.get() ?? {}
855
902
  const pending = Object.keys(clean).filter((k) => JSON.stringify(now[k]) !== JSON.stringify(clean[k]))
856
903
  return json(res, 200, {
@@ -868,6 +915,10 @@ export function apply(ctx, config) {
868
915
  }
869
916
  }
870
917
  return json(res, 501, { ok: false, error: '当前环境不支持写配置' })
918
+ } catch (e) {
919
+ // 绝不返回空响应体:客户端拿到空 body 只能显示通用「保存失败」,无法定位(issue #12)
920
+ try { return json(res, 400, { ok: false, error: '配置写入失败:' + String(e?.message || e) }) } catch { return }
921
+ }
871
922
  }
872
923
  return json(res, 405, { ok: false, error: 'method not allowed' })
873
924
  }