memory-eternal 0.10.8 → 0.10.9
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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/README.en.md +11 -7
- package/README.md +10 -7
- package/index.js +40 -4
- package/lib/client.js +13 -13
- package/lib/config-sync.js +5 -1
- package/lib/watchdog.js +9 -2
- package/lib/web.js +40 -3
- package/package.json +2 -2
- package/web/app.js +1 -1
|
@@ -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.10.
|
|
4
|
+
"version": "0.10.9",
|
|
5
5
|
"description": "记忆核心(Memory Eternal):对话结束后自动沉淀知识卡到本地记忆库(~/.dsh/memory-vault),支持检索 / 召回 / 审核中心。",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "EternalNight996",
|
package/README.en.md
CHANGED
|
@@ -361,7 +361,14 @@ curl http://127.0.0.1:7999/memory-eternal/api/web-info # is the standalone we
|
|
|
361
361
|
|
|
362
362
|
| Version | Date | Highlights |
|
|
363
363
|
|---|---|---|
|
|
364
|
-
| **v0.10.
|
|
364
|
+
| **v0.10.9** | 2026-10-09 | **Root fix for the upgrade-window port drift that turned the memory page into "connection refused" (issue #29)**. The scene: a detached instance left by the old package sat on 7999 without reporting a version → the host `ensureWebServer()` could not recognise it, drifted to 8000 and wrote that into `web-info`; the plugin's self-heal then reaped the instance on 8000 as a stray → `web-info` dangled on a dead port (the page loaded once, then "connection refused 127.0.0.1" after a refresh). ① **The library page now uses a host same-origin shell**: the host gains `/memory-eternal/ui/app` (same index.html as the config page `/ui/config`, where `<script src="app.js">` resolves to `/ui/app.js`), so the iframe address is **decoupled from whichever port the resident instance listens on** (an older host still falls back to `web-info`). ② **`/web-info` converges live**: before answering, it probes "configured port → recorded port", uses whichever is serving, and reports `alive:false` when neither is (no more advertising a dead address); it is also rewritten right after a successful self-heal restart and whenever the interval keeper sees the instance alive. ③ **No more blind port drift**: new `looksLikeOurWeb()` (lenient detection of "ours but not reporting a version": any `ok:true` from /overview, /web-info or /budget) gives self-heal a 6s takeover window before drifting, and logs it when it does drift. ④ **Backoff retry in the health check**: `inspectResident()` no longer concludes "old instance that does not report its version" from a single missed probe (that verdict triggers a needless replacement on every start), it retries with backoff first. Adds `tests/issue-fixes-5.test.mjs` (4 checks). `npm test` now runs **287 checks** |
|
|
365
|
+
| **v0.10.8** | 2026-10-08 | **Saving on the standalone web page now takes effect on click: the pending apply moves from a plugin callback into a host HTTP request (design A, measurement-driven)**. The scene: a standalone-page save ended in a `dropped` patch after five retries, always with `HMR transactions cannot be nested` (0.10.7 only stopped it from masquerading as an auto-capture anomaly and filtered upgrade-window keys — the sync itself was not fixed). **Measured on the same host with the same `settings.update`**: called from a plugin **callback** (`setInterval` / `fs.watch`) → guard error and **nothing was committed at all** (a patch with only host-known keys and a genuinely different value failed the same way); called from a plugin **HTTP request handler** → **it really committed** (`recallSummaryLen` 130→132→130, `revision` 4→6). So the fix is not another API but another **calling context**: ① the host gains `POST /memory-eternal/api/drain-pending`, applying the pending patch inside a **request handler** (two gates: loopback origin plus a per-process token from the heartbeat file, so no other local page can write); ② the host publishes its web server's **listening port + token** in the heartbeat (`webServer.port` is a public getter), and a standalone save now **wakes the host** and reports the real result (new `savedApplied` toast); ③ a failed wake-up (old host without the route → 404, port mismatch, timeout) is **not** an error — it falls back to the 0.10.5 waiting path and the "change it under DSH Settings" hint. Adds `tests/host-drain.test.mjs` (6 checks: loopback, the three gates, the end-to-end wake-up, failure reporting, 404 fallback, source guards) plus `tests/e2e-host-drain.mjs` (2 checks: a **real spawned process** end-to-end "standalone save → wake the host → host applies → reports applied", and the fallback to "queued" when the heartbeat comes from an old host with no port/token). `npm test` now runs **283 checks** |
|
|
366
|
+
|
|
367
|
+
<details>
|
|
368
|
+
<summary>Older release notes (v0.10.7 and earlier · 34 releases, click to expand)</summary>
|
|
369
|
+
|
|
370
|
+
| Version | Date | Highlights |
|
|
371
|
+
|---|---|---|
|
|
365
372
|
| **v0.10.7** | 2026-10-08 | **"Still the old version after starting dsh" fixed at the root: version-drift self-heal + one-click resident restart (the positive answer to #19 / #23).** The scene: an npm upgrade only swaps files on disk, and the **resident web (port 7999 by default) outlives the DSH host** — the process on the port keeps running the code it loaded at startup, while activation policy was "there is already a live watchdog on this port → `delegated` (no spawn, no replacement)", so **no number of DSH restarts ever replaces it** (measured here: the host started at 10:19 on the new build and the same-origin settings panel showed 0.10.6, while the web on 7999 had started at 08:15 on 0.10.0 — `/config` and `/version-check` each reported a different version). ① **Startup self-heal (`autoRestartOnDrift`, default on)**: activation first runs `inspectResident()` — the version is always asked from **the port** (the lock's `pkgVersion` is only the watchdog's self-report, and an old instance wrote an empty string, so drift was invisible) — then `decideResidentAction()` decides: match → `delegate` (keeping #19's honest wording), drift → `restart`, own version unreadable → touch nothing, just replaced → debounce (a config change re-runs the effect). **"Updating the running program" is therefore defined as "restart that resident process on the new code", never as hot-swapping modules in memory**: ESM module cache plus closure state (settings / vault routing / SSE hub / `fs.watch` / the already-bound port) leaves no safe way to hot-swap, and a cache-busting re-import only produces a second module graph (double instances, double listeners, double writes). ② **One implementation for replacement**: new `restartResident()` spawns a detached `watchdog.js --replace` helper (reap the old watchdog / old web → **wait for the port to really free up** → take the lock → pull up the new web), then verifies "the version served on the port equals the local version" (#23 suggestion 2); if the port never frees up it **aborts the takeover** instead of falling back to `port+1` (fallback instances are exactly where orphan webs are born). `dsh-memory restart`, host startup self-heal and the panel button now share it. The helper is detached with `stdio:ignore` (nobody reads its stderr), so the takeover outcome **must** go into the capture log — otherwise "restarted but not in effect" becomes yet another untraceable silent failure. ③ **One-click fix in the panel**: new `POST /memory-eternal/api/restart-self` under three hard rules — this process may well **be** the stale web being replaced, so the response is sent **before** the replacement is scheduled; if the resident instance is already current it answers `alreadyCurrent` (the common case when clicking from the DSH panel — a healthy instance is never needlessly taken down); and only one replacement may run at a time. ④ **Panel semantics disambiguated**: "running" became "**running (process serving this page)**" (the resident web on the standalone page, the host inside DSH Settings — the very reason the two panels' numbers disagreed), plus a new "**resident instance vX**" chip so it is obvious which one to restart; the drift banner offers the button plus the equivalent command, and when the resident instance is already new but this page is not, it says plainly "restart the desktop app / DSH" instead of offering a button that would do nothing. ⑤ **`updateAvailable` now compares versions**: `latest !== onDisk` used to **advise downgrading** when running an unpublished local build (disk 0.10.7 / npm 0.10.6); it now uses `compareVersions` (with prerelease semantics, `rc.2 < rc.10`). ⑥ Also removed a duplicate `saving` key from the client dictionary (esbuild warned on every build). New `tests/watchdog-restart.test.mjs` (17 checks: decision matrix / inspection asking the port / replacement verification / non-lethal takeover cleanup / route responding before replacing + concurrency gate / version comparison). ⑦ **The README now ships as two separate files**: `README.md` is the **default** Simplified Chinese one (what npm and the DSH marketplace render) and `README.en.md` is the English mirror; both gained a language switch plus a "change one, change the other" note at the top, and the English self-diagnostics subsection that was missing; new `tests/readme-i18n.test.mjs` locks **1:1 structure** (heading-level sequence + code-block count + cross links + default-language markers) so future edits cannot land on one side only. ⑧ **`secretHint` credential hint + credential-read guard**: one line of your own convention gets injected into every session's systemPrompt so the agent looks up the **secret directory card** in memory first when it needs an API key / token (memory stores the directory only — names / locations / how to use — while **values stay in local credential stores**); `dsh-memory setup` now also registers the Claude Code `PreToolUse` guard (denies reads of `.credentials.yaml` / `.env` / `*-token` and points at the loader) and refreshes hook paths that still point at an older install. ⑨ **Three more live-site findings fixed before release**: ① the panel's **restart resident instance** button also showed when the *process serving this page* was the old one — clicking it must 404 (the old host has no such route) and the user only saw "unknown interface"; it now appears only when the resident instance on the port is behind the files on disk, and a 404/405 gets explicit "restart the desktop build / DSH, or run dsh-memory restart" guidance. ② When the standalone page (new) submits the whole form to an old host, the host drops the keys it does not know (e.g. `secretHint`) by whitelist → the read-back check can never match → five retries then `dropped`, with the user only seeing "HMR transactions cannot be nested"; the pending patch is now **filtered against the host schema** before it is applied (unknown keys take effect once the host upgrades), and "config sync failed" is kept strictly apart from "auto-capture anomaly" (`config-warn` / `config-fail`, no longer painting the capture health red). ③ `autoRestartOnDrift` now renders as **on** when the host snapshot has no such key (measured: 0.10.6 host + 0.10.7 panel → checkbox empty while the host actually treats it as on). Adds `tests/issue-fixes-4.test.mjs`. `npm test` now runs **274 checks** |
|
|
366
373
|
| **v0.10.6** | 2026-10-07 | **Orphan webs are now reclaimed + a failed shared-config sync is no longer silent.** ① **Orphan webs**: the web child is spawned detached, so when a watchdog is hard-killed (on Windows `process.kill(pid,'SIGTERM')` terminates unconditionally) its exit handler never runs; if that web had **fallen back to port+1…** because the target port was taken, the lock entry naming it disappears together with the watchdog — leaving an unowned web parked on 8001/8002 (two were found on this machine, one of them 20 hours old, and `status` only probes the configured port so it could never see them). Now `startWatchdog` on startup, and `stop`/`restart` (the `checkPort` path), sweep the **fallback window** `[port+1, port+9]` and reap them; three gates must all pass (the port is inside the window / the command line **really is this plugin's web** / the pid is not any watchdog's `pid` or `webPid` in the lock), so **someone else's `node web.js` is never killed**. In addition `spawnWeb` no longer spawns a duplicate when the target port already holds our own web (the main source of fallback instances) — liveness is left to the tick probe. ② **`syncConfigFile()` no longer swallows errors**: it writes the read source shared by the standalone page / hooks / MCP, so a failed write shows up as "config changed but they can't see it" — the old code was `catch { /* silent */ }` (observed once: the shared file's mtime stayed at pre-activation while the host's own value had changed). Failures now reach stderr and the capture log as a `warn` (same reason reported once, no spam); because `logCapture` is defined later inside `apply()`, the log line is deliberately deferred by one tick to avoid the `const` TDZ. New `tests/watchdog-orphans.test.mjs` (7 checks); `npm test` now runs **245 checks** |
|
|
367
374
|
| **v0.10.5** | 2026-10-07 | **Config saves from the standalone web page: no more empty promise, and no DSH install required.** ① **A host-independent write path (#21)**: if nothing consumes the pending file within 800 ms **and** no live DSH host is present (the heartbeat file `memory-eternal-config.host.json` is stale, or its pid is dead), the standalone side now **atomically merges** the change straight into the shared config, clears the pending file and answers `pendingOutcome: applied-direct` — so Codex / Claude Code / Cursor users, and anyone running only `dsh-memory serve`, get their save applied immediately (previously that path just wrote a "pending" file for a host that **does not exist**, while the UI promised "takes effect on next start" forever). When a host *is* alive the standalone side **never** writes directly: the shared file is the host's derived mirror, and a direct write would be overwritten by the next `syncConfigFile()` (a dedicated reverse test guards that boundary). ② **Failures now give directions, not just an error (#21)**: host-guard errors are recognised — `HMR transactions cannot be nested` (Cordis HMR hosts) and `root.events.emit is unavailable from a plugin activation` (dsh-tui and friends) are two phrasings of one defect family — and "this host does not let a plugin write config from its own callback context" plus where to change it instead (**DSH Settings → Memory**, or the profile patch layer) now lands in the give-up error, the diagnostics and the standalone page's hint; `POST /config` also returns `hostGuard` / `hint`. ③ **The UI no longer always says "Saved" (#21)**: the client consumes `pendingOutcome` — `queued` / `failed` use the non-success style and say "not confirmed applied yet", `applied-direct` states "no DSH host here, written straight into the shared config", and only a real apply shows "Saved". ④ Also fixed a cleanup race that could keep a host or test process **alive forever**: the heartbeat timer and the pending-file watcher are created after an async `import()`, so a dispose that happened before it resolved left them uncleaned (the heartbeat interval is additionally `unref()`ed) — this is exactly what hung `tests/settings-compat.test.mjs` when it ran `apply()` against a fake ctx. New `tests/issue-fixes-3.test.mjs` (9 checks); `npm test` now runs **238 checks** |
|
|
@@ -369,13 +376,8 @@ curl http://127.0.0.1:7999/memory-eternal/api/web-info # is the standalone we
|
|
|
369
376
|
| **v0.10.3** | 2026-10-06 | **Fixed the counting semantics of `audit list` (#20).** ① The total is now **decoupled from `--limit`**: the reported count is the **matching total** (via `countCards`) while `--limit` caps output only, and truncation is stated explicitly (`显示前 M 张 … 还有 X 张未显示`). ② **No more silent truncation by default** (omit `--limit`, or `--limit 0`, to list everything). ③ `--json` now exposes `total` / `returned` / `count` (a backward-compatible alias of total; in 0.10.2 it wrongly equalled returned) / `limit` (null = all) / `hasMore`. ④ The meaning of `--status all` is documented in the usage text and README: the audit queue (pending + rejected), **excluding** dequeued approved and recycle-bin deleted; `approved` remains queryable explicitly. ⑤ **Same defect fixed in the MCP `memory_audit_list` tool**: it also silently capped at 20 and reported the truncated length as the pending count; it now reports the matching total plus how many are hidden, cap 100 → 500. |
|
|
370
377
|
| **v0.10.2** | 2026-10-06 | **Five long-standing issues fixed at once (#15 / #16 / #17 / #18 / #19).** ① **Per-project vaults finally work in the host (#15-1)**: the host matched `vaultProfiles[].match.workspace` against the process cwd (the directory dsh was launched from) — it now uses the **session's own workspace** from `session.header.cwd` (both capture and `memory_recall`), while CLI/MCP/hooks keep their existing cwd behaviour. ② **Distillation stops burning doomed candidates (#15-2)**: providers declaring `supportsReasoningEffort:false` no longer receive a `reasoningEffort` field (unknown = supported), and they move behind supporting candidates instead of being dropped. ③ **Truncated output is no longer misreported as a parse failure (#18)**: a dedicated `MAX_TOKENS` code (with the actual cap in the message), one automatic retry with a doubled cap, failure reporting that **always keeps the first candidate's error as the primary cause** plus a per-code summary of the rest, default `captureMaxTokens` 900 → **2000**, and a `distill-failed` tag on fallback raw cards. ④ **Standalone-web config saves are no longer lost silently (#16)**: drain errors are no longer swallowed (stderr + capture log, throttled), and the shared `memory-eternal-config.json` is now written **atomically (tmp + rename)** — the old non-atomic write could be read mid-truncation by the standalone web / MCP, fail JSON.parse and silently fall back to defaults, the pending file is **kept** with `dropped:true` + `lastError` after retries are exhausted, and the state shows up in `/diagnostics` and `/config`. ⑤ **Audit CLI (#17)**: `dsh-memory audit list/approve/reject` (multiple paths, `--json` for jq) reuses the same `setCardStatus` guard and `audit_log`; MCP gains only a **read-only** `memory_audit_list`. ⑥ **Watchdog lifecycle (#19)**: new `dsh-memory status/stop/restart`, `pkgVersion` recorded in the lock (status warns when the resident instance is outdated), truthful `spawned` vs `delegated to existing pid` host logs, a clear notice when `watchdogAutoSpawn` is off but an instance is still running, `dsh-memory stop` also stops the **web server the watchdog spawned** (on Windows `process.kill(pid,'SIGTERM')` terminates unconditionally, so the watchdog's own exit handler never runs — the old behaviour left an orphan web holding the port; the web pid is recorded in the lock and confirmed against the process command line before killing, avoiding pid-reuse mistakes), plus README fixes for the contradictory default and the "restart turns it off" promise. |
|
|
371
378
|
| **v0.10.1** | 2026-10-02 | **Fixes "export works, import brings in 0 cards".** Root cause: "Export JSON" wrote a **bare array**, while `/import` only read `payload.cards` — a bare array was silently treated as an empty backup (`ok:true` / `imported:0` / `skipped:0`), so the UI only ever said "Import complete: 0". ① `/import` now accepts both a **bare array** (backups from v0.10.0 and earlier) and an **envelope object** `{format, formatVersion, exportedAt, count, cards}`; an unrecognized shape returns **400 with a readable reason**, an empty file returns a `warning`. ② Export now writes the envelope (older builds only read `payload.cards`, so the envelope stays compatible with them) and keeps each card's `status` / `store`. ③ The import dedup baseline is now a **pre-import snapshot** (new `dedupAgainst` option on `writeCard`) — otherwise near-identical cards inside one backup dedup against each other and swallow part of the batch (a backup is the source of truth). ④ The import response adds `total` / `skipped` / `quarantined` / `failed[]` (with a per-card reason), and the toast reads "Import complete: N / of M · pending review K · skipped D"; a run that imports nothing is shown as a failure. ⑤ New `tests/import-roundtrip.test.mjs` (6 checks). ⑥ **A broken install now speaks plainly (issue #14)**: a copy installed from the plugin marketplace can be missing its `web/` assets, and the sidebar then showed a raw `{"ok":false,"error":"ENOENT …"}` page. Now a missing `index.html` with `app.js` still present is covered by a **built-in shell** that boots the UI anyway (self-healing), while a missing `app.js` returns a JS snippet that paints the diagnosis into `#root` plus a page stating which file is gone, its absolute path, the package version and how to reinstall. Both cases land in the auto-capture log (`app.js` → `fail`, health turns red; `index.html` only → `warn`), and the host self-checks the same way at startup (shared predicates and wording in the new `lib/web-assets.js`, with 6 checks in `tests/web-assets.test.mjs`). ⑦ `appendCaptureLog` no longer fails silently when the `DSH_HOME` directory does not exist. ⑧ **DSH compatibility widened to `>=0.1.5-alpha.2 <0.3.0-0`**: the old `<0.2.0` upper bound excludes DSH `0.2.0-rc.x` under plain semver (DSH's guard uses `includePrerelease:true`, so installs still worked, but the declaration did not match reality) — the range now covers the whole 0.1/0.2 line while `<0.3.0-0` still blocks 0.3.0 and its prereleases; `dsh.compatibility` gains `"0.2.0-rc.2": "compatible"` (verified on the official desktop build 0.2.0-rc.2). ⑨ **Fixed three third-party plugin manifests that were invalid JSON**: the closing quote of `description` in `.claude-plugin` / `.codex-plugin` / `.cursor-plugin` had been swallowed by a double-encoding accident (last touched in v0.9.23); DSH never reads those files, so nothing surfaced, but installing through Claude Code / Codex / Cursor **always failed to parse**. They are now valid UTF-8 JSON with `version` bumped to 0.10.1, plus a new **shipped-files health guard** `tests/manifests.test.mjs` (3 checks: every shipped JSON parses / the three manifests' `name`+`version` match package.json / no encoding-accident mojibake in shipped text). Measured on a real **661-card bare-array backup (3.9 MB): 0 cards before the fix → 661 after** (654 into the main store + 7 into quarantine). `npm test` now runs **187 checks** |
|
|
379
|
+
| **v0.10.0** | 2026-09-30 | **Two stores: unreviewed content is physically quarantined.** ① `cards` (the main store) holds `approved` only, while a new `quarantine` store (with `quarantined_at` / `quarantine_reason`) holds `pending` / `rejected` / `deleted`; ② read paths (recall, search, graph, dedup, export) no longer need a status condition — querying the main store is safe by construction — which closes the three review-bypass holes audited in v0.9 (the dedup pool writing new knowledge into pending cards, `readCard` reading unreviewed bodies, `/card` skipping the status check); ③ review transitions became **cross-store moves** (inside a transaction, ids issued by the new `card_sequence` allocator, `card_updates` rebound with the card); ④ existing non-approved cards are **migrated automatically** on first start (nothing deleted, idempotent); ⑤ a new `checkMainStoreInvariant()` health check plus `tests/two-store.test.mjs`; ⑥ an intake noise gate that strips DSH runtime injections (environment snapshots, team broadcasts, teammate transcripts, tool manuals) so fragment cards and mushy titles never get in; ⑦ fixed the native crash of `settings-compat` on Windows (`fs.watch` on the parent directory → watch the file instead) |
|
|
372
380
|
| **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** |
|
|
373
|
-
|
|
374
|
-
<details>
|
|
375
|
-
<summary>📜 Previous releases (click to expand)</summary>
|
|
376
|
-
|
|
377
|
-
| Version | Date | Highlights |
|
|
378
|
-
|---|---|---|
|
|
379
381
|
| **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** |
|
|
380
382
|
| **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. |
|
|
381
383
|
| **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. |
|
|
@@ -401,6 +403,8 @@ curl http://127.0.0.1:7999/memory-eternal/api/web-info # is the standalone we
|
|
|
401
403
|
| **v0.8.0** | 2026-09-06 | **Full UI i18n**: follows the DSH system language live (zh/en) across cards, graph, audit center, config panel and new-card templates; SQLite EBUSY fix for the Windows test suite |
|
|
402
404
|
| **v0.3.1** | 2026-09-03 | Knowledge graph loads on demand (fetch on open, unregister on close) |
|
|
403
405
|
| **v0.3.0** | 2026-09-03 | **SQLite storage layer** (`node:sqlite`, zero deps) + DB-level audit guard `enforceAudit()` + `audit_log` table + automatic `.md` migration |
|
|
406
|
+
|
|
407
|
+
</details>
|
|
404
408
|
| v0.2.0 | 2026-09-03 | Recall with body text (`search()` returns the `excerpt` field) |
|
|
405
409
|
| v0.1.2 | 2026-09-03 | Audit guard hardening (`writeCard` defaults to `pending`, `mergeCards` goes through audit) |
|
|
406
410
|
| v0.1.1 | 2026-09-03 | Recall body fix (`excerpt` field + body logic) |
|
package/README.md
CHANGED
|
@@ -393,7 +393,14 @@ curl http://127.0.0.1:7999/memory-eternal/api/web-info # 独立 web 是否活
|
|
|
393
393
|
|
|
394
394
|
| 版本 | 日期 | 关键改动 |
|
|
395
395
|
|---|---|---|
|
|
396
|
-
| **v0.10.
|
|
396
|
+
| **v0.10.9** | 2026-10-09 | **升级期端口漂移把记忆页打成「拒绝连接」的根治(issue #29)**。现场:旧包遗留的 detached 实例占着 7999 且不自报版本 → 宿主 `ensureWebServer()` 认不出它、向后漂到 8000 并写进 `web-info`;而插件的自愈随后把 8000 上的实例当野实例收掉 → `web-info` 永久悬空,客户端 iframe 指向死端口(首次能进、刷新即「拒绝连接 127.0.0.1」)。① **记忆库页改走 host 同源壳**:宿主新增 `/memory-eternal/ui/app`(与配置页 `/ui/config` 同一份 index.html,其中 `<script src="app.js">` 相对路径正好解析到 `/ui/app.js`),iframe 地址从此与「常驻实例监听哪个端口」**彻底解耦**(旧宿主 404 时仍回落 `web-info`)。② **`/web-info` 实时收敛**:每次读之前先探「配置端口 → 记录端口」,谁在服务就用谁,都不通就如实 `alive:false`(不再把死地址当活的);自愈替换成功后与 interval 保活探到活时都就地回写。③ **不再盲目漂端口**:新增 `looksLikeOurWeb()`(宽松识别「自家但不自报版本」的占用者:/overview、/web-info、/budget 任一 ok:true 即认),疑似自家时先给自愈留 6s 接管窗口,等不到才漂并留日志。④ **体检退避重试**:`inspectResident()` 一次探针打空不再直接判「旧版不自报版本」(那个结论会触发误替换,每次启动都白起一次实例),退避重试后才下结论。新增 `tests/issue-fixes-5.test.mjs`(4 项:宽松识别真/假阳性、等自愈接管不漂端口、宿主与客户端源码守卫)。`npm test` 共 **287 项** |
|
|
397
|
+
| **v0.10.8** | 2026-10-08 | **独立 Web 端保存「点一下即生效」:把应用 pending 的上下文从插件回调搬到宿主 HTTP 请求(方案 A,实测驱动)**。现场:独立页保存 → 宿主连续重试 5 次后 `dropped`,报错恒为 `HMR transactions cannot be nested`(0.10.7 只修掉了它冒充「自动沉淀异常」与升级期未知键,同步本身没修)。**实测定位**(同一宿主、同一个 `settings.update`):从插件**回调**(`setInterval` / `fs.watch`)里调 → 抛守卫错且**一个字节都没提交**(只含宿主认识的键、值真的变了,同样失败);从插件 **HTTP 请求处理器**里调 → **真的提交**(`recallSummaryLen` 130→132→130,`revision` 4→6)。所以修法不是换 API 而是换**调用上下文**:① 宿主新增 `POST /memory-eternal/api/drain-pending`,在**请求处理器**里应用 pending(两道闸门:只接受本机来源 + 心跳里的一次性令牌,防本机任意页面乱写);② 宿主把自己 web server 的**监听端口 + 令牌**写进心跳(`webServer.port` 是服务公开的 getter),独立端保存后据此**主动叫醒宿主**,拿到真实结果再回给界面(新增 `savedApplied`「已保存并生效」);③ 叫醒失败(旧宿主没这条路由 404 / 端口对不上 / 超时)**不算错误** —— 回落到 0.10.5 的等待路径与「去 DSH 设置里改」的指路。新增 `tests/host-drain.test.mjs`(6 项:回环判定 / 守卫三件套 / 端到端叫醒 / 失败如实回报 / 404 回落 / 源码守卫)+ `tests/e2e-host-drain.mjs`(2 项:**真起进程**的端到端「独立页保存 → 叫醒宿主 → 宿主应用 → 回报已生效」,以及旧宿主心跳(无端口/令牌)时回落成「排队等待」)。`npm test` 共 **283 项** |
|
|
398
|
+
|
|
399
|
+
<details>
|
|
400
|
+
<summary>更早的更新日志(v0.10.7 及以前 · 共 34 版,点开查看)</summary>
|
|
401
|
+
|
|
402
|
+
| 版本 | 日期 | 关键改动 |
|
|
403
|
+
|---|---|---|
|
|
397
404
|
| **v0.10.7** | 2026-10-08 | **「启动 dsh 后运行中还是旧版本」根治:版本漂移自愈 + 一键重启常驻实例(#19 / #23 的正面解法)**。现场:升级 npm 包只换了磁盘文件,**常驻 web(默认 7999)比 DSH 宿主活得久** —— 端口上那个进程仍在跑「启动时加载进内存」的旧代码;而宿主激活时的策略是「同端口已有活着的 watchdog 就 `delegated`(不再 spawn、更不替换)」,于是**重启多少次 DSH 都不会换掉它**(实测本机:宿主 10:19 启动已是新版、设置页同源显示 0.10.6,而 7999 上的 web 是 08:15 启动的 0.10.0,`/config` 与 `/version-check` 各报一个版本)。① **启动自愈(`autoRestartOnDrift`,默认开)**:激活时先 `inspectResident()` 体检 —— 版本一律**问端口**(锁里的 `pkgVersion` 只是 watchdog 自述,旧实例写的还是空串,根本看不出版本漂移),再 `decideResidentAction()` 决策:一致 → `delegate`(保持 #19 的如实口径)、漂移 → `restart`、读不到本机版本 → 绝不乱动、刚替换过 → 防抖不重来(配置改动会让 effect 重跑)。**「更新运行中的程序」的正确形态由此定为「用新代码重启那个常驻进程」,而不是热替换内存里的模块**:ESM 模块缓存 + 闭包状态(settings / vault 路由 / SSE hub / `fs.watch` / 已监听端口)没有安全热换的余地,缓存失效重 import 只会得到第二份模块图(双实例、双监听、双写)。② **替换机制统一到一处**:新增 `restartResident()` = spawn 一个 detached 的 `watchdog.js --replace` 助手(收旧 watchdog / 旧 web → **等端口真的空出来** → 抢锁 → 拉起新 web),再自检「端口上真正服务的版本 = 本机版本」(#23 建议 2);端口没空出来就**中止接管**、不退让到 `port+1`(退让实例正是孤儿 web 的诞生地)。`dsh-memory restart`、宿主启动自愈、面板按钮现在共用这一份实现。助手是 detached + `stdio:ignore`(stderr 没人看),所以接管结论**必须**写进「自动沉淀日志」—— 否则「重启了却没生效」又会变成一条无迹可查的静默失败。③ **面板一键修复**:新增 `POST /memory-eternal/api/restart-self`,三条硬约束 —— 本进程很可能**就是**要被替换的旧 web,所以**先回响应再调度**替换;常驻实例已经是最新就回 `alreadyCurrent`(宿主面板点这个按钮时最常见,绝不为一个健康实例白折腾一次停机);同一时刻只允许一个替换在跑。④ **面板语义消歧**:「运行中」改为「**运行中(服务本页的进程)**」(独立 Web 页里它是常驻 web,DSH 设置页里是宿主 —— 这正是两个面板数字「打架」的根源),并新增「**常驻实例 vX**」徽标:两者不同时一眼看出该重启谁;漂移横幅给按钮 + 等价命令,若常驻实例已新而本页仍旧,则明说「需重启桌面版 / DSH」,不再给一个点了也没用的按钮。⑤ **`updateAvailable` 改按版本比较**:`latest !== onDisk` 在本地跑未发布版本时会**反过来劝你装回旧版**(磁盘 0.10.7 / npm 0.10.6),现在用 `compareVersions`(含预发布语义 `rc.2 < rc.10`)。⑥ 顺带清掉客户端词典里重复的 `saving` 键(esbuild 每次构建都在报警告)。新增 `tests/watchdog-restart.test.mjs`(17 项:决策矩阵 / 体检问端口 / 替换自检 / 接管前清场不误杀 / 路由先响应后替换 + 并发闸门 / 版本比较)。⑦ **README 改为中英两份独立文件**:`README.md` 是**默认**的简体中文(npm 与 DSH 插件市场展示这一份),`README.en.md` 是英文镜像;顶部加语言切换与「改一处要同步另一处」的说明,并补齐英文版缺失的「环境自检」小节;新增 `tests/readme-i18n.test.mjs` 锁住**结构 1:1**(标题层级序列 + 代码块数量 + 互链 + 默认语言标记),防止以后只改一份。⑧ **`secretHint` 凭证提示 + 凭据读取守卫**:填一句自己的约定就会被注入每个会话的 systemPrompt,让 agent 需要 API key / token 时**先查记忆里的「密钥目录」卡**(记忆库只存目录:名字/位置/取用方式,**值留在本机凭据库**);`dsh-memory setup` 同时注册 Claude Code 的 `PreToolUse` 守卫(拒读 `.credentials.yaml` / `.env` / `*-token` 并指路到加载器),并会刷新仍指向旧安装副本的 hook 路径。⑨ **发布前又抓到三个实测现场并修掉**:① 面板「重启常驻实例」按钮在**服务本页的进程**是旧版时也显示 —— 点了必然 404(旧宿主没有这条路由),用户只看到一句「未知接口」;现在按钮只在「端口上那个常驻实例落后于磁盘」时出现,404/405 还会给一条「重启桌面版 / DSH 或执行 dsh-memory restart」的指路。② 独立页(新版)提交整张表单、宿主还是旧版时:宿主按白名单丢掉它不认识的键(如 `secretHint`)→ 回读校验必然对不上 → 连试 5 次 `dropped`,用户只看到「HMR transactions cannot be nested」;现在应用前按宿主 schema **过滤未知键**(宿主升级后自然生效),并把「配置同步失败」与「自动沉淀异常」彻底分开(`config-warn` / `config-fail`,不再翻红沉淀健康状态)。③ `autoRestartOnDrift` 在宿主快照里没有这个键时按**默认开**渲染(实测现场:0.10.6 宿主 + 0.10.7 面板 → 复选框空着,而宿主实际按开处理,面板与真实行为不一致)。新增 `tests/issue-fixes-4.test.mjs`。`npm test` 共 **274 项** |
|
|
398
405
|
| **v0.10.6** | 2026-10-07 | **孤儿 web 有了回收机制 + 共享配置同步失败不再静默**。① **孤儿 web**:web 子进程是 detached 启动的,watchdog 被硬终止时(Windows 上 `process.kill(pid,'SIGTERM')` 即无条件终止)它的退出处理器不执行;若这个 web 当初因为目标端口被占而**退让到 port+1…**,登记它的锁条目又随 watchdog 一起消失 —— 于是 8001/8002 这类端口上会常驻没人认领的 web(实测本机就躺着两个,其中一个已跑了 20 小时,而 `status` 只探配置端口,永远发现不了)。现在 `startWatchdog` 启动时、以及 `stop`/`restart`(`checkPort` 路径)都会扫**退让窗口** `[port+1, port+9]` 并收掉它们;三道门缺一不可(端口在窗口内 / 命令行**确实是本插件的 web** / pid 不在锁里任何 watchdog 的 `pid` 或 `webPid` 上),**别人的 `node web.js` 一个都不杀**。另外 `spawnWeb` 发现目标端口上已有自家 web 时**不再重复拉起**(退让实例的主要来源),只交给 tick 探活。② **`syncConfigFile()` 不再静默吞错**:它写的是独立页 / hooks / MCP 共用的读源,写不进去就表现为「配置改了但他们看不到」——旧实现是 `catch { /* 静默 */ }`(实测到过一次:共享文件 mtime 停在激活前,而宿主里的值已经变了)。现在失败会进 stderr 与「自动沉淀日志」的 warn(同一原因只报一次,不刷屏);因为 `logCapture` 定义在 `apply()` 更靠后的位置,日志刻意**延迟一拍**再写,避开 `const` 的 TDZ。新增 `tests/watchdog-orphans.test.mjs`(7 项),`npm test` 共 **245 项** |
|
|
399
406
|
| **v0.10.5** | 2026-10-07 | **独立 Web 端的配置保存:不再空头承诺,也不再要求「必须装了 DSH」**。① **宿主无关的直写路径(#21)**:独立端保存后若 800ms 内没人消费 pending 文件、**且**本机没有活着的 DSH 宿主(心跳文件 `memory-eternal-config.host.json` 不新鲜、或里面的 pid 已死),就直接把改动**原子合并**写进共享配置、清掉 pending,响应回 `pendingOutcome: applied-direct` —— 于是 Codex / Claude Code / Cursor、以及只跑 `dsh-memory serve` 的用户,保存当场生效(此前那条路只会写「待应用」等一个**根本不存在**的宿主,界面上那句「下次启动生效」永远不会兑现)。宿主活着时**绝不**直写:共享配置是宿主的派生镜像,绕过它会被下一次 `syncConfigFile()` 盖回去(有专门的反向测试守着这条边界)。② **失败要指路,不只是重复报错(#21)**:识别「宿主守卫类」报错 —— `HMR transactions cannot be nested`(Cordis HMR 宿主)与 `root.events.emit is unavailable from a plugin activation`(dsh-tui 一类)是同一族缺陷的两种措辞 —— 并把「此宿主不允许插件从自己的回调上下文写配置」与「去哪改」(**DSH 设置 → 记忆**,或 profile 的配置补丁层)写进放弃时的报错、诊断信息与独立页提示;`POST /config` 同时回 `hostGuard` / `hint` 字段。③ **界面不再一律弹「已保存」(#21)**:客户端开始消费 `pendingOutcome` —— `queued` / `failed` 用**非成功样式**并说明「尚未确认生效」,`applied-direct` 明确写「本机没有 DSH 宿主,已直接写入共享配置」,只有真生效才显示「已保存」。④ 顺带修掉一个会让宿主/测试**无法退出**的清理竞态:心跳定时器与 pending 文件监听都是在异步 `import()` 之后才建立,若在它 resolve 之前就 dispose,回调就再没人清理(心跳定时器还额外 `unref()`)—— `tests/settings-compat.test.mjs` 用假 ctx 跑 `apply()` 时正是被它挂住的。新增 `tests/issue-fixes-3.test.mjs`(9 项),`npm test` 共 **238 项** |
|
|
@@ -403,12 +410,6 @@ curl http://127.0.0.1:7999/memory-eternal/api/web-info # 独立 web 是否活
|
|
|
403
410
|
| **v0.10.1** | 2026-10-02 | **修复「导出 OK、导入 0 张」**。根因:客户端「导出JSON」写出的是**裸数组**,而 `/import` 只读 `payload.cards` —— 裸数组被静默当成空备份(`ok:true` / `imported:0` / `skipped:0`),UI 只显示「导入完成:0」,看不出是格式不匹配。① `/import` 现在同时接受**裸数组**(v0.10.0 及以前的备份)与**信封对象** `{format, formatVersion, exportedAt, count, cards}`;形状不认识 → **400 + 人话原因**,空文件 → `warning`,坏 JSON → 原有的解析失败提示;② 导出改为**带信封**(老版本只认 `payload.cards`,信封对老版本也互通)并保留每张卡的 `status` / `store`;③ 导入去重基线改为**导入前快照**(`writeCard` 新增 `dedupAgainst`)—— 否则同一份备份里互相近似的卡会在写入过程中互相判重、被自己人吞掉一批(备份是真相源);④ 导入应答补 `total` / `skipped` / `quarantined` / `failed[]`(含每条未导入原因),提示语改为「导入完成:N 张 / 文件共 M 张 · 待审核 K · 跳过 D」,一张都没进来时按**失败样式**提示;⑤ 新增 `tests/import-roundtrip.test.mjs`(6 项:裸数组 / 信封 / 导出→导入全量往返 / 重复导入如实报重复 / 坏形状 / 审核守卫不可绕过)。⑥ **装坏了也要说人话(issue #14)**:从插件市场装出来的副本可能缺 `web/` 静态资源,那时侧边栏「记忆」只会弹一页 `{"ok":false,"error":"ENOENT …"}` —— 现在缺 `index.html` 但 `app.js` 还在就用**内置外壳**把界面拉起来(自救),缺 `app.js` 则回一段把诊断画进 `#root` 的 JS + 一页含「缺哪个文件 / 绝对路径 / 包版本 / 怎么重装」的说明;两种缺失都写进自动沉淀日志(`app.js` → `fail` 健康态亮红、只缺 `index.html` → `warn`),宿主启动时也自检一次(判据与措辞由新增的 `lib/web-assets.js` 统一,附 `tests/web-assets.test.mjs` 6 项)。⑦ 顺带修 `appendCaptureLog` 在 `DSH_HOME` 目录不存在时静默失败(诊断信息连一条都留不下)。⑧ **DSH 兼容范围放宽到 `>=0.1.5-alpha.2 <0.3.0-0`**:旧上界 `<0.2.0` 按 semver 会把 DSH `0.2.0-rc.x` 排除在外(DSH 守卫用 `includePrerelease:true`,实际装得上,但声明与事实不符)——现在覆盖整个 0.1/0.2 线,`<0.3.0-0` 仍挡住 0.3.0 及其预发布;`dsh.compatibility` 同步补 `"0.2.0-rc.2": "compatible"`(本机在官方桌面版 0.2.0-rc.2 上实测运行)。⑨ **修三个第三方插件清单长期非法 JSON**:`.claude-plugin` / `.codex-plugin` / `.cursor-plugin` 的 `description` 收尾引号被双重编码吃掉(最后改动停在 v0.9.23),DSH 不走这三个文件所以一直没暴露,但 Claude Code / Codex / Cursor 侧安装**必然解析失败**;现已重写为合法 UTF-8 JSON、`version` 跟到 0.10.1,并新增**随包体检守卫** `tests/manifests.test.mjs`(3 项:随包 JSON 全部可解析 / 三清单 `name`+`version` 与 package.json 一致 / 随包文本无编码事故乱码)。实测用户 **661 张真实备份(裸数组、3.9MB):修复前导入 0 张 → 修复后 661 张**(654 进主库 + 7 进隔离区)。`npm test` 共 **187 项** |
|
|
404
411
|
| **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` 父目录 → 改监听文件) |
|
|
405
412
|
| **v0.9.23** | 2026-09-30 | **保存不再闪烁**。保存成功后客户端会立刻重拉 `/config`,而宿主 volatile 回流是滞后的 → 输入框会「先退回原配置、再跳回修改后的值」。现在客户端把「已保存但宿主未回显」的字段做**本地叠加**(新增 `src/client/config-merge.js` 纯函数 + 4 项单测),宿主回显一致后自动摘除,之后仍跟随宿主权威值;重置表单会清空叠加层。`npm test` 共 **139 项** |
|
|
406
|
-
|
|
407
|
-
<details>
|
|
408
|
-
<summary>📜 历史版本(点击展开)</summary>
|
|
409
|
-
|
|
410
|
-
| 版本 | 日期 | 关键改动 |
|
|
411
|
-
|---|---|---|
|
|
412
413
|
| **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 项** |
|
|
413
414
|
| **v0.9.21** | 2026-09-30 | **保存成功不再被误读为失败 + 配置页排版修复**。① 保存成功一律用**成功样式**提示(原来「已写入;宿主尚未回流」被弹成红色失败样式,用户以为保存失败),措辞改为「已写入配置文件;宿主仍是旧版,重启 DSH 后即以新值为准」;② 配置页字段网格列宽 150/160px → **200/220px**、间距加大、长中文标签允许换行,勾选框与说明文字顶端对齐,字段不再互相挤压。 |
|
|
414
415
|
| **v0.9.20** | 2026-09-30 | 独立 Web 页补**取值范围校验**:`/config` 镜像 Config schema 的 min/max(如 `recycleRetentionDays` 1–3650、`webPort` 1–65535),越界直接 **400** 并说明「需要 1–3650,当前 0」。至此「数字框清空 / 越界」这条最常见的保存失败链路在**客户端 + 独立页 + 宿主**三层都被拦住并给出字段级原因。 |
|
|
@@ -434,6 +435,8 @@ curl http://127.0.0.1:7999/memory-eternal/api/web-info # 独立 web 是否活
|
|
|
434
435
|
| **v0.8.0** | 2026-09-06 | **全套 UI i18n**:跟随 DSH 系统语言实时切换(中/英),覆盖知识卡 / 知识图谱 / 审核中心 / 配置面板 / 新建卡模板;Windows 测试收尾 SQLite 连接 EBUSY 修复 |
|
|
435
436
|
| **v0.3.1** | 2026-09-03 | 知识图谱按需加载(点击时 fetch,关闭时注销) |
|
|
436
437
|
| **v0.3.0** | 2026-09-03 | **SQLite 化存储层**(`node:sqlite` 零依赖)+ 数据库层审核守卫 `enforceAudit()` + `audit_log` 审计日志 + 自动从 .md 迁移 |
|
|
438
|
+
|
|
439
|
+
</details>
|
|
437
440
|
| v0.2.0 | 2026-09-03 | recall 含正文修复(`search()` 返回 excerpt 字段) |
|
|
438
441
|
| v0.1.2 | 2026-09-03 | 审核守卫加固(`writeCard` 默认 `pending`,`mergeCards` 走审核) |
|
|
439
442
|
| v0.1.1 | 2026-09-03 | recall 含正文修复(excerpt 字段 + body 逻辑修正) |
|
package/index.js
CHANGED
|
@@ -1007,6 +1007,34 @@ export function apply(ctx, config) {
|
|
|
1007
1007
|
// /web-info 读取后用 iframe 渲染 web 端 UI——DSH 渲染也走 web,单一真源)。
|
|
1008
1008
|
let webInfo = { url: 'http://127.0.0.1:7999', port: 7999, alive: false }
|
|
1009
1009
|
const refreshWebInfo = (info) => { if (info && info.url) webInfo = { ...info, alive: true } }
|
|
1010
|
+
/**
|
|
1011
|
+
* 实时收敛 web-info(issue #29)。
|
|
1012
|
+
*
|
|
1013
|
+
* 为什么必须实时:webInfo 过去只在「启动期 ensureWeb 成功」时写一次。自愈(restartResident)
|
|
1014
|
+
* 会把实例挪回**配置端口**、或收掉漂移端口上的实例 —— 记录却还指着旧端口,客户端 iframe
|
|
1015
|
+
* 于是打开一个没人监听的地儿(「拒绝连接 127.0.0.1」)。现在每次读 web-info 前先问一遍:
|
|
1016
|
+
* 配置端口 → 记录端口,谁在服务就用谁;都不通就如实标 alive:false(别再给死地址)。
|
|
1017
|
+
*/
|
|
1018
|
+
let webInfoProbedAt = 0
|
|
1019
|
+
const reconcileWebInfo = async () => {
|
|
1020
|
+
const now = Date.now()
|
|
1021
|
+
if (now - webInfoProbedAt < 3000) return webInfo // 打开面板会连打几次,3s 内不重复探
|
|
1022
|
+
webInfoProbedAt = now
|
|
1023
|
+
try {
|
|
1024
|
+
const { probeWebServer } = await import('./lib/web.js')
|
|
1025
|
+
const cfgPort = Number((settings.get() ?? {}).webPort) || 7999
|
|
1026
|
+
const candidates = [cfgPort, Number(webInfo.port) || 0].filter((v, i, a) => v && a.indexOf(v) === i)
|
|
1027
|
+
for (const port of candidates) {
|
|
1028
|
+
const alive = await probeWebServer(port, 600).catch(() => null)
|
|
1029
|
+
if (alive) {
|
|
1030
|
+
if (webInfo.port !== port || !webInfo.alive) refreshWebInfo({ url: `http://127.0.0.1:${port}`, port, spawned: false })
|
|
1031
|
+
return webInfo
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
webInfo = { ...webInfo, alive: false }
|
|
1035
|
+
} catch { /* 探测失败保持原记录,不影响面板打开 */ }
|
|
1036
|
+
return webInfo
|
|
1037
|
+
}
|
|
1010
1038
|
|
|
1011
1039
|
// webServer 是「可能晚到」的服务:apply 执行时它常常尚未挂载,同步 ctx.get()
|
|
1012
1040
|
// 拿不到就整段跳过、且不留任何日志 —— 表现是 /memory-eternal/api/* 恒 404、
|
|
@@ -1041,7 +1069,8 @@ export function apply(ctx, config) {
|
|
|
1041
1069
|
try {
|
|
1042
1070
|
const pathname = new URL(req.url, 'http://localhost').pathname
|
|
1043
1071
|
if (pathname === API_PREFIX + '/web-info') {
|
|
1044
|
-
|
|
1072
|
+
// 别回报启动期的缓存:它可能指着已经被收掉的实例(issue #29)
|
|
1073
|
+
return json(res, 200, { ok: true, ...(await reconcileWebInfo()) })
|
|
1045
1074
|
}
|
|
1046
1075
|
if (pathname === API_PREFIX + '/events') {
|
|
1047
1076
|
// SSE 长连接:不 end,保持推送(EventSource 自动重连)
|
|
@@ -1182,9 +1211,12 @@ export function apply(ctx, config) {
|
|
|
1182
1211
|
}
|
|
1183
1212
|
return json(res, 405, { ok: false, error: 'method not allowed' })
|
|
1184
1213
|
}
|
|
1185
|
-
// DSH host
|
|
1186
|
-
//
|
|
1187
|
-
|
|
1214
|
+
// DSH host 同源 UI:/memory-eternal/ui/app(记忆库整页)+ /memory-eternal/ui/config(配置页,
|
|
1215
|
+
// 同一份壳)+ /memory-eternal/ui/app.js(同一份 bundle)。index.html 里 `<script src="app.js">`
|
|
1216
|
+
// 是相对路径,所以在这三个路由下都会解析到 /memory-eternal/ui/app.js —— 天然同源。
|
|
1217
|
+
// 为什么记忆库页也要同源(issue #29):iframe 过去指向「常驻实例监听的那个端口」,
|
|
1218
|
+
// 而自愈/漂移会让那个端口变成死地址 → 记忆页「拒绝连接」。同源后地址与端口彻底解耦。
|
|
1219
|
+
if (pathname === API_PREFIX + '/ui/app' || pathname === API_PREFIX + '/ui/config' || pathname === API_PREFIX + '/ui/app.js') {
|
|
1188
1220
|
const { readFile } = fs
|
|
1189
1221
|
const webRoot = path.join(PACKAGE_ROOT, 'web')
|
|
1190
1222
|
if (pathname.endsWith('app.js')) {
|
|
@@ -1271,6 +1303,8 @@ export function apply(ctx, config) {
|
|
|
1271
1303
|
} else {
|
|
1272
1304
|
// 探到活则重置计数
|
|
1273
1305
|
restartCount = 0
|
|
1306
|
+
// 活着也要回写记录:首轮 ensure 可能漂到过别的端口,此后没有任何校正点 → web-info 悬空(issue #29)
|
|
1307
|
+
refreshWebInfo({ url: `http://127.0.0.1:${port}`, port, spawned: false })
|
|
1274
1308
|
}
|
|
1275
1309
|
}
|
|
1276
1310
|
// 立即跑一次(首启 + 间隔循环)
|
|
@@ -1323,6 +1357,8 @@ export function apply(ctx, config) {
|
|
|
1323
1357
|
const done = out.ok
|
|
1324
1358
|
? `常驻实例已重启:端口 ${wdPort} 正在服务 v${out.served}(等待 ${Math.round(out.waitedMs / 1000)}s)`
|
|
1325
1359
|
: `常驻实例重启未确认生效(${out.stage}):${out.error || '未知原因'} —— 可再试「设置 → 记忆 → 插件信息 → 重启常驻实例」,或执行 dsh-memory restart --port ${wdPort}`
|
|
1360
|
+
// 自愈刚把实例挪到配置端口:立刻收敛 web-info,别再让面板指向被收掉的那个端口(issue #29)
|
|
1361
|
+
if (out.ok) refreshWebInfo({ url: `http://127.0.0.1:${wdPort}`, port: wdPort, spawned: false })
|
|
1326
1362
|
console.error(`[memory-eternal] ${done}`)
|
|
1327
1363
|
logCapture('system', out.ok ? 'boot' : 'fail', done)
|
|
1328
1364
|
return
|