memory-eternal 0.10.35 → 0.10.36

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.10.35",
4
+ "version": "0.10.36",
5
5
  "description": "记忆核心(Memory 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.10.35",
3
+ "version": "0.10.36",
4
4
  "description": "记忆核心(Memory 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.10.35",
3
+ "version": "0.10.36",
4
4
  "description": "记忆核心(Memory Eternal):对话结束后自动沉淀知识卡到本地记忆库(~/.dsh/memory-vault),支持检索 / 召回 / 审核中心。",
5
5
  "author": {
6
6
  "name": "EternalNight996",
package/README.en.md CHANGED
@@ -361,14 +361,16 @@ 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.34** | 2026-10-16 | **Root fix: when the lock is unwritable the HOST manages the resident instance itself (adopting the reporter's fourth point in issue #30)**. Their diagnosis: on that machine the "registration" happens to live in the only process that cannot write the lock, so automatic replacement is permanently refused. Fixes: ① the host's `autoWebMode=interval` keep-alive loop now probes **the version served on the port**, not just liveness — if it lags and the lock is writable the watchdog/drift path still handles it (the host does not compete); if it lags and the **lock is unwritable** the **host takes over**: `ensureWebServer({forceTakeover:true})` stops only **our own** web, starts a new instance on the **same port** and keeps it alive; ② a testable `takeoverOwnWeb()` with a safety gate that only acts on occupants where `looksLikeOurWeb` is true (foreign processes are never touched); ③ "host-managed" is recorded as state (`recordHostManaged`) so `/version-check` reports `resident.managedBy='host'` and the panel says "**host-managed**" instead of "unmanaged / orphan"; the state clears itself once a watchdog registers again. `npm test` now runs **366 checks** |
365
- | **v0.10.33** | 2026-10-15 | **Distillation provider: dynamic dropdowns, best-effort follow-session, first-candidate-only and a test button (first slice of issue #42)**. ① **Dynamic dropdowns**: a new read-only `/llm-routes` endpoint (backed by the host's listProviders/listModels) turns the "distill provider / model" fields from hand-typed IDs into selects (falling back to text inputs when unavailable); ② **Follow the current session** (their top priority): the new `captureFollowSession` config makes host distillation **try** the session's provider/model first (`sessionRouteFrom()` sniffs `agent / session / session.route / session.options`), and **falls back to the explicit config with a log line** when nothing is exposed (DSH does not guarantee a route on the session object — that log is the basis for a future adapter rather than a guess); background/no-session tasks (CLI, watchdog, hooks) can only use the explicit config; ③ **First-candidate-only + test button**: `captureFirstOnly` makes routing take just the first candidate and stop on failure (no more "hit the official provider first, burn a call, then rotate"); `POST /llm-test` makes one small real call and echoes **provider / model / latency / candidate count / error code**, which also verifies ① and ②; ④ also fixes **probe jitter**: `probeServedVersion()` now retries with backoff three times (the reporter measured "version-check 200 at one moment, then status says no service 2s later"), so single-shot probes are no longer fooled by restart windows or transient refusals. `npm test` now runs **363 checks** |
364
+ | **v0.10.36** | 2026-10-16 | **Both new states now surface at every entry point (align / doctor), plus one test-hygiene fix**. ① align gains a finding: **the resident instance is host-managed** (the lock is unwritable, so the watchdog path is bypassed and the host stops/starts the instance itself and keeps it alive) — **warn level**, stating explicitly that "the instance is managed, it is not an orphan, and it returns to the watchdog once the lock becomes writable" (align.ok is unaffected; warn is not a failure); ② dsh-memory doctor prints a new line **resident instance management:** (watchdog / host-managed / cannot replace (environment limit)), so together with journal mode, write probe, lock writability and the backup list you can see at a glance which state a machine is in; ③ **test hygiene**: the /restart-self alreadyCurrent case depended on the **developer machine real lock file** (my local lock held only an endedAt record, so it stopped reporting alreadyCurrent and the check went red) — reading the lock is now injectable (deps.readWatchdogLock) and the case injects a live registration, making it independent of the dev machine. npm test now runs **371 checks** |
365
+ | **v0.10.35** | 2026-10-16 | **Single-flight for takeover helpers (issue #30, sixth retest round: the host tick and the CLI each spawn a --replace and step on each other)**. The reporter observed that during a failed restart **two takeover helpers were alive on the same port without knowing each other** (parent = the DSH host / parent = the CLI): neither wrote the lock and each reaped the other web, making the scene even harder to diagnose. Fix: a new **claim file** (memory-eternal-replace.claim, next to the watchdog lock) — ① **a helper claims it exclusively before starting**; if it cannot, it **exits immediately** (yielding to whoever is already running) and releases it on exit; ② **when restartResident sees a takeover in flight** (holder alive and less than 60s old) it **does not spawn a second helper**, returning stage in-flight with who is running it and how long ago it started; ③ **fail-open**: in restricted environments where the claim cannot be written the takeover still proceeds (permission problems must not block takeover for good), but the reason is surfaced in the result and the log. Tests cover: first-come-first-served / a live holder makes the second yield / stale and dead-pid claims can be taken over / only your own claim is released / the in-flight branch of restartResident. npm test now runs **370 checks** |
366
366
 
367
367
  <details>
368
- <summary>Older release notes (v0.10.32 and earlier · 59 releases, click to expand)</summary>
368
+ <summary>Older release notes (v0.10.34 and earlier · 61 releases, click to expand)</summary>
369
369
 
370
370
  | Version | Date | Highlights |
371
371
  |---|---|---|
372
+ | **v0.10.34** | 2026-10-16 | **Root fix: when the lock is unwritable the HOST manages the resident instance itself (adopting the reporter's fourth point in issue #30)**. Their diagnosis: on that machine the "registration" happens to live in the only process that cannot write the lock, so automatic replacement is permanently refused. Fixes: ① the host's `autoWebMode=interval` keep-alive loop now probes **the version served on the port**, not just liveness — if it lags and the lock is writable the watchdog/drift path still handles it (the host does not compete); if it lags and the **lock is unwritable** the **host takes over**: `ensureWebServer({forceTakeover:true})` stops only **our own** web, starts a new instance on the **same port** and keeps it alive; ② a testable `takeoverOwnWeb()` with a safety gate that only acts on occupants where `looksLikeOurWeb` is true (foreign processes are never touched); ③ "host-managed" is recorded as state (`recordHostManaged`) so `/version-check` reports `resident.managedBy='host'` and the panel says "**host-managed**" instead of "unmanaged / orphan"; the state clears itself once a watchdog registers again. `npm test` now runs **366 checks** |
373
+ | **v0.10.33** | 2026-10-15 | **Distillation provider: dynamic dropdowns, best-effort follow-session, first-candidate-only and a test button (first slice of issue #42)**. ① **Dynamic dropdowns**: a new read-only `/llm-routes` endpoint (backed by the host's listProviders/listModels) turns the "distill provider / model" fields from hand-typed IDs into selects (falling back to text inputs when unavailable); ② **Follow the current session** (their top priority): the new `captureFollowSession` config makes host distillation **try** the session's provider/model first (`sessionRouteFrom()` sniffs `agent / session / session.route / session.options`), and **falls back to the explicit config with a log line** when nothing is exposed (DSH does not guarantee a route on the session object — that log is the basis for a future adapter rather than a guess); background/no-session tasks (CLI, watchdog, hooks) can only use the explicit config; ③ **First-candidate-only + test button**: `captureFirstOnly` makes routing take just the first candidate and stop on failure (no more "hit the official provider first, burn a call, then rotate"); `POST /llm-test` makes one small real call and echoes **provider / model / latency / candidate count / error code**, which also verifies ① and ②; ④ also fixes **probe jitter**: `probeServedVersion()` now retries with backoff three times (the reporter measured "version-check 200 at one moment, then status says no service 2s later"), so single-shot probes are no longer fooled by restart windows or transient refusals. `npm test` now runs **363 checks** |
372
374
  | **v0.10.32** | 2026-10-15 | **"The resident instance cannot be replaced" becomes a long-lived state instead of a retry on every boot (issue #30, fifth retest round: the steady state on the A: drive machine)**. The reporter measured 44 "version drift" events over five days, with a mechanism where **every step works as designed yet the instance can never be replaced**: upgrade → the port still serves the previous generation → the host detects drift → it spawns a `--replace` helper → the helper pre-flights and finds `lock.writable=false` → it **correctly refuses** (the 0.10.29 design, zero damage) → the old instance survives → the next boot reports it again. Fixes: ① new `lib/resident-state.js` **records "cannot replace (environment limit)" as state** (cause / lock path / port / versions / count, valid for 7 days); ② the host now **probes lock writability before restarting** and, when unwritable, **stops retrying a doomed replacement**, recording just one line — `warn` the first time, then downgraded to a `boot`-level "known state" on later boots (the reporter's words: they do not want a warn on every boot forever); ③ the wording is explicitly split from "drift": **"the resident instance cannot be replaced (environment limit)"** plus the way out (fix permissions then restart, or run `dsh-memory watchdog --port N` in the foreground); ④ the panel, `/version-check` and `align` all surface the state (`resident.replaceBlocked`; in `align` it is a `warn` finding that clears itself once the environment is fixed). `npm test` now runs **359 checks** |
373
375
  | **v0.10.31** | 2026-10-15 | **Root fix for `restart` failing 100% of the time on machines with slow port enumeration (issue #30, third/fourth retest rounds — the reporter traced it to the line)**. Mechanism: the `--replace` helper ran "clear the scene → only then write the lock if the port is free → start the watchdog", while `Get-NetTCPConnection` inside the clear step measured **4–13.6 seconds** and was **not bounded by the 8-second budget** → the helper **never reached the lock write** → the lock stayed empty → `waitForTakeover` (which requires a fresh live watchdog in the lock) **was guaranteed to time out** → on such machines both the CLI and the panel button report "(no response)" **100% of the time, regardless of version**. Fixes: ① **a failed clear step still leaves a registration** (`acquireWatchdogLock({force:true})`), so "a web on the port with an empty lock" can no longer happen; ② **`acquireWatchdogLock` gains `force`** (the whole point of `--replace` is to replace that same-port instance, so it must not yield — otherwise the helper exits before the clear step finishes); ③ 0.10.30 already bounded the enumerator (`defaultExec` kills the child on timeout, `findPortListener({timeoutMs})` threads it through, the clear step uses ≤2.5s); ④ the CLI's `restart` result is now **actually written to the auto-capture log** (the hint always claimed so, but the CLI path never did). They also measured "two competing takeover helpers" (the host tick and the CLI each spawning one) — with registration plus force, that race is far less harmful (the later one takes over the registration instead of both leaving nothing). `npm test` now runs **355 checks** |
374
376
  | **v0.10.30** | 2026-10-15 | **Two fixes for "the whole page is unusable": malformed tags causing a black screen (#41) plus two real defects in the takeover path (issue #30, third/fourth retest rounds)**. ① **#41**: a string `tags` value sent to `/write` (e.g. `"[\"a\",\"b\"]"`) was passed straight into `writeCard`'s `JSON.stringify` → **double-encoded** in the database → read back as a string → the client's `card.tags.slice(...).map` threw → **the React root failed to mount and the whole page went black** (restarting DSH, the plugin or the resident instance changed nothing, because the bad data lives in the DB). `normalizeTags()` is now the **single entry point**: arrays, JSON strings, comma strings and **legacy double-encoded rows** all collapse to arrays, used by the write path **and every read path** (existing bad data needs no manual repair); the render layer also guards itself (bad tags cost a few chips, never the tree). ② **#30**: the reporter found two more issues inside my 0.10.29 — `restoreWatchdogLock`'s "live registration" test **missed `!w.endedAt`** (so with a reused pid the write-back was abandoned), and **Windows port enumeration had no timeout** (`Get-NetTCPConnection` measured at 4–7.5s, once 13.6s, while the takeover's 8s budget could not bound it → decisions were made on stale data). `defaultExec` now supports a timeout and actually kills the child, `findPortListener({timeoutMs})` threads it through, and the takeover passes a bounded value (≤2.5s). Adds `tests/tags-normalize.test.mjs` (4 checks) and 2 regressions in `watchdog-restart`. `npm test` now runs **354 checks** |
package/README.md CHANGED
@@ -393,14 +393,16 @@ curl http://127.0.0.1:7999/memory-eternal/api/web-info # 独立 web 是否活
393
393
 
394
394
  | 版本 | 日期 | 关键改动 |
395
395
  |---|---|---|
396
- | **v0.10.34** | 2026-10-16 | **治本:锁不可写时由「宿主」自己托管常驻实例(采纳 #30 报告者第四点建议)**。他的诊断:那台机器上「登记」恰好落在**唯一写不了锁的那个进程**上 → 自动替换被永久拒绝。修法:① 宿主的 `autoWebMode=interval` 保活循环**不再只探存活**,还探**端口上服务的版本** —— 落后且锁可写 → 仍交给看门狗 / 漂移路径(宿主不抢);落后且**锁不可写** → **宿主自己接管**(`ensureWebServer({forceTakeover:true})`:只收掉**自家** web、在**同端口**起新实例、由宿主保活);② 新增可测试的 `takeoverOwnWeb()`(安全闸门:只对 `looksLikeOurWeb` 为真的占用者下手,别人的进程一个都不动);③ 「宿主托管」记成状态(`recordHostManaged`)→ `/version-check` 报 `resident.managedBy='host'`,面板显示「**宿主托管**」而不是「未托管 / 孤儿」;看门狗重新登记后自动清除。`npm test` 共 **366 项** |
397
- | **v0.10.33** | 2026-10-15 | **蒸馏供应商:动态下拉 + 跟随当前会话(尽力)+ 仅试首选 + 测试按钮(issue #42 第一刀)**。① **动态下拉**:新增只读接口 `/llm-routes`(宿主侧的 listProviders/listModels),配置页的「蒸馏 provider / model」从**手填编号**变成**下拉选择**(取不到时自动回退成文本输入);② **跟随当前会话**(他的最高优先级):新增配置 `captureFollowSession`,开启后宿主蒸馏**先尝试**用这一轮会话的 provider/model(`sessionRouteFrom()` 按 `agent / session / session.route / session.options` 逐个嗅探),**读不到就回退显式配置并记一条日志**(DSH 未保证会话对象暴露 route —— 这条日志就是以后补适配的依据,不猜);后台 / 无会话任务(CLI、watchdog、hooks)天然只能回退配置;③ **仅试首选 + 测试按钮**:`captureFirstOnly` 让选路只取第一个候选、失败即停(避免「先打官方 → 白烧一次调用 → 再轮换」);`POST /llm-test` 用真实调用路径打一次小请求,回显 **provider / model / 耗时 / 候选数 / 错误码**,也是 ①② 的验证手段。④ 顺带修一处**探测抖动**:`probeServedVersion()` 现在退避重试 3 次(报告者实测「同一时刻 version-check 200、2 秒后 status 说没有服务」),单发探测不再被重启窗口/瞬时拒绝骗到。`npm test` 共 **363 项** |
396
+ | **v0.10.36** | 2026-10-16 | **把两种新状态接到所有入口(align / doctor)+ 修一处测试卫生**。① align 新增一条结论:**常驻实例由宿主托管**(锁不可写 → 看门狗路径被绕过,改由宿主停旧起新并保活)—— **warn 级**,明确写「实例是有人管的,不是孤儿;锁恢复可写后自动交还看门狗」(align.ok 不受影响,warn 不算失败);② dsh-memory doctor 增印一行 **常驻实例托管:**(看门狗 / 宿主托管 / 不可替换(环境限制)),配合已有的 journal 模式、写探针、锁可写性、备份清单,一次看清这台机器处于哪种状态;③ **测试卫生**:/restart-self 的 alreadyCurrent 用例依赖**开发者本机真实的锁文件**(我本机锁里只剩一条 endedAt 记录,于是它不再判 alreadyCurrent、用例翻红)—— 把「读锁」做成可注入(deps.readWatchdogLock),用例自己注入一条活登记,从此与开发机状态无关。npm test 共 **371 项** |
397
+ | **v0.10.35** | 2026-10-16 | **接管助手单飞互斥(#30 第六轮实测:宿主 tick 与 CLI 各拉一个 --replace 互相踩)**。报告者观察到失败的 restart 期间同端口上**同时存在两个互不相识的接管助手**(父 = DSH 宿主 / 父 = CLI),谁都不写锁、还互相收对方的 web —— 现场更难复盘。修法:新增 **claim 文件**(memory-eternal-replace.claim,与看门狗锁同目录)—— ① **助手开工前独占抢 claim**,抢不到就**直接退出**(把接管让给已经在跑的那个),并在退出时释放;② **restartResident 发现接管进行中**(持有者活着且 <60s)就**不再拉第二个助手**,返回 stage「in-flight」并说明是谁在跑、多久前启动;③ **失败开放**:受限环境写不了 claim 时**照常接管**(不能让权限问题把接管彻底堵死),但把原因写进返回值 / 日志。测试覆盖:先到先得 / 活持有者让位 / 过期与死 pid 可接管 / 只清自己的 claim / restartResident 的 in-flight 分支。npm test 共 **370 项** |
398
398
 
399
399
  <details>
400
- <summary>更早的更新日志(v0.10.32 及以前 · 共 59 版,点开查看)</summary>
400
+ <summary>更早的更新日志(v0.10.34 及以前 · 共 61 版,点开查看)</summary>
401
401
 
402
402
  | 版本 | 日期 | 关键改动 |
403
403
  |---|---|---|
404
+ | **v0.10.34** | 2026-10-16 | **治本:锁不可写时由「宿主」自己托管常驻实例(采纳 #30 报告者第四点建议)**。他的诊断:那台机器上「登记」恰好落在**唯一写不了锁的那个进程**上 → 自动替换被永久拒绝。修法:① 宿主的 `autoWebMode=interval` 保活循环**不再只探存活**,还探**端口上服务的版本** —— 落后且锁可写 → 仍交给看门狗 / 漂移路径(宿主不抢);落后且**锁不可写** → **宿主自己接管**(`ensureWebServer({forceTakeover:true})`:只收掉**自家** web、在**同端口**起新实例、由宿主保活);② 新增可测试的 `takeoverOwnWeb()`(安全闸门:只对 `looksLikeOurWeb` 为真的占用者下手,别人的进程一个都不动);③ 「宿主托管」记成状态(`recordHostManaged`)→ `/version-check` 报 `resident.managedBy='host'`,面板显示「**宿主托管**」而不是「未托管 / 孤儿」;看门狗重新登记后自动清除。`npm test` 共 **366 项** |
405
+ | **v0.10.33** | 2026-10-15 | **蒸馏供应商:动态下拉 + 跟随当前会话(尽力)+ 仅试首选 + 测试按钮(issue #42 第一刀)**。① **动态下拉**:新增只读接口 `/llm-routes`(宿主侧的 listProviders/listModels),配置页的「蒸馏 provider / model」从**手填编号**变成**下拉选择**(取不到时自动回退成文本输入);② **跟随当前会话**(他的最高优先级):新增配置 `captureFollowSession`,开启后宿主蒸馏**先尝试**用这一轮会话的 provider/model(`sessionRouteFrom()` 按 `agent / session / session.route / session.options` 逐个嗅探),**读不到就回退显式配置并记一条日志**(DSH 未保证会话对象暴露 route —— 这条日志就是以后补适配的依据,不猜);后台 / 无会话任务(CLI、watchdog、hooks)天然只能回退配置;③ **仅试首选 + 测试按钮**:`captureFirstOnly` 让选路只取第一个候选、失败即停(避免「先打官方 → 白烧一次调用 → 再轮换」);`POST /llm-test` 用真实调用路径打一次小请求,回显 **provider / model / 耗时 / 候选数 / 错误码**,也是 ①② 的验证手段。④ 顺带修一处**探测抖动**:`probeServedVersion()` 现在退避重试 3 次(报告者实测「同一时刻 version-check 200、2 秒后 status 说没有服务」),单发探测不再被重启窗口/瞬时拒绝骗到。`npm test` 共 **363 项** |
404
406
  | **v0.10.32** | 2026-10-15 | **「常驻实例不可替换」变成长期状态,而不是每次开机重试(#30 第五轮复测:A: 盘的稳态)**。报告者实测 5 天 44 次「版本漂移」,机制是**每一步都按设计工作、合起来永远换不掉**:升级 → 端口上仍是上一代 → 宿主探测漂移 → 拉起 `--replace` 助手 → 助手前置探到 `lock.writable=false` → **正确地拒绝**(0.10.29 的设计,零破坏)→ 旧实例继续存活 → 下次开机再报一次。修法:① 新增 `lib/resident-state.js`:把「不可替换(环境限制)」**记成状态**(原因 / 锁路径 / 端口 / 版本 / 次数,7 天有效);② 宿主在重启前**先探锁可写性**,不可写就**不再重试注定失败的替换**,只记一条 —— **首次 `warn`**、之后每次开机降级为 `boot` 级「已知状态」(报告者原话:不想每次开机都看到一条 warn 永久持续);③ 措辞与「漂移」显式分开:**「常驻实例不可替换(环境限制)」** + 出路(修权限后 restart,或 `dsh-memory watchdog --port N` 前台观察);④ 面板 / `/version-check` / `align` 都能看到这个状态(`resident.replaceBlocked`;`align` 里是一条 `warn` 级结论,环境修好后自动清除)。`npm test` 共 **359 项** |
405
407
  | **v0.10.31** | 2026-10-15 | **`restart` 在「端口枚举很慢」的机器上 100% 失败的根因修复(#30 第三/四轮复测,报告者定位到行)**。机制:`--replace` 助手的顺序是「清场 → 端口空了才写锁 → 启动看门狗」,而清场里 `Get-NetTCPConnection` 实测要 **4–13.6 秒**且不受 8 秒预算约束 → 助手**永远走不到写锁那一步** → 锁里什么都没有 → `waitForTakeover`(要求「锁里出现新的活看门狗」)**必然超时** → CLI 与面板按钮在这类机器上 100% 报「(无响应)」,**与版本号无关**。修法:① **清场失败也必须留下登记**(`acquireWatchdogLock({force:true})`),现场不再出现「端口有 web、锁里空白」;② **`acquireWatchdogLock` 支持 `force`**(`--replace` 的语义就是替换同端口那个,不许让位 —— 否则助手在清场没跑完时先退出);③ 0.10.30 已给枚举执行器加超时(`defaultExec` 真掐断子进程、`findPortListener({timeoutMs})` 逐层传递、清场给 ≤2.5s 有界值);④ CLI 的 `restart` 结果**真的写进自动沉淀日志**(提示里一直这么说,但 CLI 路径过去不写)。他还实测到「两个接管助手互相赛跑」(宿主 tick + CLI 各拉一个)—— 登记 + force 之后这种赛跑的危害大幅降低(后到者接管登记,而不是双双留空)。`npm test` 共 **355 项** |
406
408
  | **v0.10.30** | 2026-10-15 | **两处「让整页不可用」的修复:畸形 tags 黑屏(#41)+ 接管链路的两个真病灶(#30 第三/四轮复测)**。① **#41**:`/write` 传字符串 `tags`(如 `"[\"a\",\"b\"]"`)被 `writeCard` 直接 `JSON.stringify` → **双重编码**落库 → 读出来仍是字符串 → 前端 `card.tags.slice(...).map` 抛错 → **React 根组件挂载失败、整页黑屏**(重启 DSH / 插件 / 常驻实例都无效,因为坏数据一直在库里)。现在 `normalizeTags()` 是**唯一入口**:数组 / JSON 字符串 / 逗号串 / **双重编码的旧数据**都收敛成数组,写路径与**所有读路径**都走它(既有坏数据不必用户手工修);渲染层再加兜底(坏 tags 最多少显示几个标签,不许把整棵树带走)。② **#30**:报告者在我 0.10.29 里又抓出两处 —— `restoreWatchdogLock` 的「活登记」判定**少了 `!w.endedAt`**(PID 复用/正在退出时,写回会被放弃);**Windows 端口枚举没有超时**(实测 `Get-NetTCPConnection` 无条件要 4–7.5 秒、一次 13.6 秒,而接管清场的 8 秒预算根本管不住它 → 决策基于过期数据)。现在 `defaultExec` 支持超时并真的掐断子进程、`findPortListener({timeoutMs})` 逐层传下去、接管清场给有界值(≤2.5s)。新增 `tests/tags-normalize.test.mjs`(4 项)+ `watchdog-restart` 2 项回归。`npm test` 共 **354 项** |
@@ -335,6 +335,14 @@ async function main() {
335
335
  console.log('写探针:' + (health.writable ? 'OK(这个库现在可写)' : '失败 → ' + health.error))
336
336
  if (health.corrupt) console.log('损坏判定:疑似文件损坏(见下方引导)')
337
337
  console.log('看门狗锁:' + (lockInfo.writable ? '可写(' + lockInfo.path + ')' : '**写不进去** → ' + lockInfo.error + '(' + lockInfo.path + ')—— 保活一般仍有效,但「是否被托管」将无法判断'))
338
+ // 托管方式一眼可见(#30 ⑤/⑥):看门狗登记 / 宿主托管 / 因环境限制不可替换
339
+ try {
340
+ const st = await import('../lib/resident-state.js')
341
+ const hm = st.hostManagedInfo()
342
+ const rb = st.replaceBlockedInfo()
343
+ const how = hm && hm.fresh ? '宿主托管(锁不可写 → 由宿主进程停旧起新并保活;原因 ' + (hm.reason || '未记录') + ')' : (rb && rb.fresh ? '不可替换(环境限制:锁目录不可写)' : '看门狗(无异常记录)')
344
+ console.log('常驻实例托管:' + how)
345
+ } catch { /* 状态读不到就不打印 */ }
338
346
  console.log('备份:' + (backups.length ? backups.length + ' 个(最近:' + backups[0] + ')' : '(还没有备份 —— 可在面板或 backupDb 生成)'))
339
347
  if (guidance.length) { console.log(''); for (const line of guidance) console.log('· ' + line) }
340
348
  if (!health.writable) process.exitCode = 1
package/lib/align.js CHANGED
@@ -22,7 +22,7 @@ import { probeServedVersion } from './watchdog.js'
22
22
  * @returns {{ok:boolean, findings:Array<{id:string,level:'ok'|'warn'|'fix',title:string,detail:string,command:string}>, counts:{ok:number,warn:number,fix:number}}}
23
23
  */
24
24
  export function alignFindings({
25
- hostVersion = '', onDisk = '', residentServed = '', residentPort = 7999, replaceBlocked = null,
25
+ hostVersion = '', onDisk = '', residentServed = '', residentPort = 7999, replaceBlocked = null, hostManaged = null,
26
26
  latest = '', updateCommand = '', wiring = [], hookPaths = [], configMirror = null, knownKeys = null,
27
27
  } = {}) {
28
28
  const findings = []
@@ -53,6 +53,13 @@ export function alignFindings({
53
53
  '锁目录当前不可写(' + (replaceBlocked.error || '原因未知') + ';' + (replaceBlocked.lockPath || '') + ')→ 端口 ' + residentPort + ' 上会长期停在 v' + (replaceBlocked.served || replaceBlocked.version || '?') + ',自动替换被正确地拒绝(避免留下孤儿)。',
54
54
  '修复该目录权限后执行 dsh-memory restart --port ' + residentPort + ';或前台观察:dsh-memory watchdog --port ' + residentPort)
55
55
  }
56
+ // ②c 「宿主托管」(issue #30 ④):锁不可写的机器上由宿主自己停旧起新并保活 ——
57
+ // 这是**设计内的降级**(不是故障),但要让用户知道「看门狗路径被绕过了」。
58
+ if (hostManaged && hostManaged.fresh) {
59
+ push('resident-host-managed', 'warn', '常驻实例由宿主托管(看门狗路径被绕过)',
60
+ '锁目录不可写 → 看门狗无法登记,改由**宿主进程**停掉旧实例、在同端口拉起新实例并保活(原因:' + (hostManaged.reason || '未记录') + ')。实例是有人管的,不是孤儿;锁恢复可写后会自动交还给看门狗。',
61
+ '查看:dsh-memory status --port ' + residentPort + '(端口版本应与本机一致)')
62
+ }
56
63
  // ③ npm 上是否已有更新
57
64
  if (latest && onDisk && latest !== onDisk) {
58
65
  push('update-available', 'warn', `npm 上已有 v${latest}(本机 v${onDisk})`,
@@ -184,7 +191,12 @@ export async function inspectAlignment({ hostVersion = '', residentPort = 7999,
184
191
  } catch { configMirror = { exists: false, path: '', keys: [], unknownKeys: [] } }
185
192
 
186
193
  let replaceBlocked = null
187
- try { const st = await import('./resident-state.js'); replaceBlocked = st.replaceBlockedInfo(process.env) } catch { replaceBlocked = null }
188
- const report = alignFindings({ hostVersion, onDisk, residentServed, residentPort, replaceBlocked, latest, updateCommand, wiring, hookPaths, configMirror, knownKeys })
194
+ let hostManaged = null
195
+ try {
196
+ const st = await import('./resident-state.js')
197
+ replaceBlocked = st.replaceBlockedInfo(process.env)
198
+ hostManaged = st.hostManagedInfo(process.env)
199
+ } catch { replaceBlocked = null; hostManaged = null }
200
+ const report = alignFindings({ hostVersion, onDisk, residentServed, residentPort, replaceBlocked, hostManaged, latest, updateCommand, wiring, hookPaths, configMirror, knownKeys })
189
201
  return { ...report, onDisk, hostVersion, residentServed, latest, wiring, hookPaths, configMirror }
190
202
  }
package/lib/api.js CHANGED
@@ -144,6 +144,8 @@ export function createApi(deps = {}) {
144
144
  const getCaptureHealth = deps.getCaptureHealth ?? (() => null)
145
145
  // 矛盾扫描(issue #36)要用模型:宿主把 ctx.get('llm') 传进来;独立 web 进程拿不到就如实报错
146
146
  const getLlm = deps.getLlm ?? (() => null)
147
+ // 锁读取可注入:单测不能依赖开发者本机真实的看门狗锁(#30 复测期间就因此翻过车)
148
+ const readLock = deps.readWatchdogLock ?? ((env) => readWatchdogLock(env))
147
149
  // 「重启常驻实例」的实现(宿主与独立 web 共用 lib/watchdog.js 的那一份;注入仅为单测)
148
150
  const doRestartResident = deps.restartResident ?? restartResident
149
151
  const probeResidentVersion = deps.probeServedVersion ?? probeServedVersion
@@ -320,7 +322,7 @@ export function createApi(deps = {}) {
320
322
  // #28 的现场就是这一种:host 与端口版本号一致 → 面板认为「无需重启」,而其实该重启(重新托管)。
321
323
  let residentManaged = false
322
324
  try {
323
- residentManaged = readWatchdogLock(process.env).watchdogs
325
+ residentManaged = readLock(process.env).watchdogs
324
326
  .some((w) => Number(w.port) === Number(residentPort) && isPidAlive(w.pid))
325
327
  } catch { residentManaged = false }
326
328
  // 「托管状态」其实有三态(#33 第③层):确实被托管 / 确实没被托管 / **判不出来**(锁文件写不进去)。
@@ -379,7 +381,7 @@ export function createApi(deps = {}) {
379
381
  // 「版本一致」还不够:还要**有活着的看门狗托管它**。孤儿 web(看门狗被 stop / 被 kill -9 后残留)
380
382
  // 版本号可能正好也是新的,但没人保活、也不会被后续升级替换 —— 那时一键重启才是唯一出路(#28)。
381
383
  const managed = (() => {
382
- try { return readWatchdogLock(process.env).watchdogs.some((w) => Number(w.port) === Number(port) && isPidAlive(w.pid)) } catch { return false }
384
+ try { return readLock(process.env).watchdogs.some((w) => Number(w.port) === Number(port) && isPidAlive(w.pid)) } catch { return false }
383
385
  })()
384
386
  if (served && expect && String(served) === String(expect) && managed) {
385
387
  return json(res, 200, {
package/lib/client.js CHANGED
@@ -298,7 +298,7 @@ Option A, because:
298
298
  height: calc(100vh - var(--dsh-windows-titlebar-height, 0px));
299
299
  max-height: calc(100vh - var(--dsh-windows-titlebar-height, 0px));
300
300
  }
301
- `;function Ca(t){t.effect(()=>t.locale.register(st,{zh:Jt,en:Zt}),"memory-eternal: locale");let i=t.locale.bind(st);t.effect(()=>t.slots.inject("settings.section",()=>t.slots.register({name:"settings.section",id:st,order:25,label:()=>i("nav"),locale:st,inject:()=>({})},()=>o.default.createElement(Ma,{t:i}))),"memory-eternal: settings section"),t.effect(()=>t.slots.inject("sidebar.footer.action",()=>t.slots.register({name:"sidebar.footer.action",id:`${st}:footer`,order:100,label:()=>i("nav"),locale:st,inject:()=>({})},r=>o.default.createElement(Ra,{t:i,locale:t.locale,wide:!(r&&r.wide===!1)}))),"memory-eternal: sidebar footer action")}function Na(){let[t,i]=(0,o.useState)(O+"/ui/app");return(0,o.useEffect)(()=>{let r=!0,g=()=>fetch("/memory-eternal/api/web-info").then(c=>c.json()).then(c=>{r&&c&&c.ok&&c.alive!==!1&&c.url&&i(c.url.endsWith("/")?c.url:c.url+"/")}).catch(()=>{});return fetch(O+"/ui/app",{method:"HEAD"}).then(c=>{if(r&&!c.ok)return g()}).catch(()=>r?g():void 0),()=>{r=!1}},[]),t}function Ma({t}){return(0,e.jsxs)("div",{style:{display:"flex",flexDirection:"column",height:"100%",minHeight:0},children:[(0,e.jsx)("style",{children:Ee}),(0,e.jsx)(aa,{t,onReload:()=>{},version:0})]})}function Ra({t,locale:i,wide:r}){let[g,c]=(0,o.useState)(!1);return(0,e.jsxs)("div",{className:`me-footer${r?"":" rail"}`,children:[(0,e.jsx)("style",{children:Ee}),(0,e.jsxs)("button",{type:"button",className:"me-footer-btn",onClick:()=>c(!0),"aria-label":t("nav"),title:t("nav"),children:[(0,e.jsx)("span",{className:"me-footer-ico","aria-hidden":"true",children:(0,e.jsx)(ea,{})}),(0,e.jsx)("span",{className:"me-footer-label",children:t("nav")})]}),g&&(0,e.jsx)(Ta,{t,locale:i,onClose:()=>c(!1)})]})}function Ta({t,locale:i,onClose:r,tab:g}){let[c,l]=(0,o.useState)(!1),C=(0,o.useRef)(null),T=Na(),S=(0,o.useCallback)(R=>i&&typeof i.subscribe=="function"?i.subscribe(R):()=>{},[i]),H=(0,o.useCallback)(()=>i&&typeof i.getSnapshot=="function"?i.getSnapshot():null,[i]),k=(0,o.useSyncExternalStore)(S,H),x=k&&(k.active==="zh"||k.active==="en")?k.active:null,m=(0,o.useRef)(null);m.current===null&&x&&(m.current=x);let z=(0,o.useMemo)(()=>{let R=m.current?`lang=${m.current}`:"";if(g==="config")return`${O}/ui/config?tab=config${R?`&${R}`:""}`;let w=T.replace(/\/$/,""),j=w.endsWith("/ui/app")?"?":"/?";return g?`${w}${j}tab=${g}${R?`&${R}`:""}`:R?`${w}${j}${R}`:T},[T,g]);return(0,o.useEffect)(()=>{let R=C.current&&C.current.contentWindow;R&&x&&R.postMessage({source:"memory-eternal",type:"locale",lang:x},"*")},[x]),(0,o.useEffect)(()=>{let R=w=>{w.key==="Escape"&&r()};return window.addEventListener("keydown",R),()=>window.removeEventListener("keydown",R)},[r]),(0,e.jsxs)("div",{className:"me-overlay-top",onClick:r,children:[(0,e.jsx)("style",{children:Ee}),(0,e.jsx)("div",{className:"me-modal"+(c?" me-modal-full":""),onClick:R=>R.stopPropagation(),children:(0,e.jsxs)("div",{style:{display:"flex",flexDirection:"column",height:"100%",minHeight:0,position:"relative"},children:[(0,e.jsxs)("div",{style:{position:"absolute",top:8,right:8,zIndex:10,display:"flex",gap:4,padding:"4px 6px",background:"var(--dsw-alias-bg-overlay, rgba(17,24,39,0.45))",borderRadius:8,backdropFilter:"blur(4px)"},children:[(0,e.jsx)("button",{type:"button",onClick:()=>l(R=>!R),"aria-label":t(c?"exitFullscreen":"enterFullscreen"),title:t(c?"exitFullscreen":"enterFullscreen"),style:{width:26,height:26,borderRadius:6,border:"1px solid rgba(255,255,255,0.18)",background:"rgba(255,255,255,0.08)",color:"#fff",cursor:"pointer",fontSize:14,lineHeight:1,display:"flex",alignItems:"center",justifyContent:"center"},children:c?"\u2921":"\u2922"}),(0,e.jsx)("button",{type:"button",onClick:r,"aria-label":t("close"),title:t("close"),style:{width:26,height:26,borderRadius:6,border:"1px solid rgba(255,255,255,0.18)",background:"rgba(255,255,255,0.08)",color:"#fff",cursor:"pointer",fontSize:16,lineHeight:1,display:"flex",alignItems:"center",justifyContent:"center"},children:"\xD7"})]}),(0,e.jsx)("iframe",{ref:C,src:z,title:"memory-eternal",allow:"clipboard-write; clipboard-read",style:{width:"100%",flex:1,border:0,borderRadius:"inherit",background:"var(--dsw-alias-bg-base, #fff)"}})]})})]})}function ea(){return(0,e.jsxs)("svg",{viewBox:"0 0 24 24","aria-hidden":"true",children:[(0,e.jsx)("defs",{children:(0,e.jsxs)("linearGradient",{id:"mg-brain",x1:"0",y1:"0",x2:"1",y2:"1",children:[(0,e.jsx)("stop",{offset:"0%",stopColor:"#FFC46B"}),(0,e.jsx)("stop",{offset:"50%",stopColor:"#F97316"}),(0,e.jsx)("stop",{offset:"100%",stopColor:"#EA580C"})]})}),(0,e.jsx)("path",{fill:"url(#mg-brain)",d:"M12 4.5a3.2 3.2 0 0 0-6.4.1 4.1 4.1 0 0 0-2.7 5.9 4.1 4.1 0 0 0 .6 6.7A4.2 4.2 0 0 0 12 18.2Z"}),(0,e.jsx)("path",{fill:"url(#mg-brain)",d:"M12 4.5a3.2 3.2 0 0 1 6.4.1 4.1 4.1 0 0 1 2.7 5.9 4.1 4.1 0 0 1-.6 6.7A4.2 4.2 0 0 1 12 18.2Z"}),(0,e.jsx)("path",{d:"M12 9.2c-.8-1.4-1.7-2-3.1-2.2M12 9.2c.8-1.4 1.7-2 3.1-2.2M12 12.4c-1.2 1-3.2 1.6-5 1.4M12 12.4c1.2 1 3.2 1.6 5 1.4M12 13.2v4",fill:"none",stroke:"rgba(255,255,255,0.62)",strokeWidth:"1",strokeLinecap:"round"}),(0,e.jsx)("circle",{cx:"12",cy:"8.6",r:"1.1",fill:"rgba(255,255,255,0.85)"})]})}var yt="0.10.35";function Da({t,inModal:i,onClose:r,onFull:g,full:c}){let[l,C]=(0,o.useState)(null),[T,S]=(0,o.useState)([]),[H,k]=(0,o.useState)(!0),[x,m]=(0,o.useState)(null),[z,R]=(0,o.useState)(""),[w,j]=(0,o.useState)("all"),[L,K]=(0,o.useState)(""),[te,V]=(0,o.useState)("all"),[F,Ie]=(0,o.useState)("all"),[G,ge]=(0,o.useState)(()=>{try{let b=new URLSearchParams(typeof location<"u"?location.search:"").get("tab"),n={cards:"cards",graph:"graph",usage:"stats",stats:"stats",audit:"audit",recycle:"optimize",optimize:"optimize",config:"config"};if(b&&n[b])return n[b]}catch{}return"cards"}),[X,$]=(0,o.useState)(()=>new Set([G])),le=(0,o.useCallback)(b=>{$(n=>n.has(b)?n:new Set(n).add(b)),ge(b)},[]),[he,Ae]=(0,o.useState)("recent"),[ke,y]=(0,o.useState)(null),ue=(0,o.useRef)(null),Te=(0,o.useRef)(null),[h,U]=(0,o.useState)(!1),[re,ye]=(0,o.useState)(null),[ee,Z]=(0,o.useState)(!1),[Be,Ce]=(0,o.useState)(!1),[Re,Ne]=(0,o.useState)(!0),[s,N]=(0,o.useState)(null),[I,Q]=(0,o.useState)(0),we=(0,o.useCallback)(()=>Q(b=>b+1),[]),de=l?.quarantine?.pending??0,ae=l?.quarantine?.deleted??0,Y=(0,o.useRef)(null),[We,Le]=(0,o.useState)(0),[p,d]=(0,o.useState)(!1),_=(0,o.useRef)(null),se=(0,o.useRef)(null),ce=(0,o.useCallback)(async(b=w,n=L,v=F,D={})=>{let B=D.append===!0;try{B?d(!0):(k(!0),R(""));let E=new URLSearchParams;b&&b!=="all"&&E.set("kind",b),n.trim()&&E.set("q",n.trim()),E.set("status",v||"all"),E.set("limit",String(xa)),E.set("offset",String(D.offset||0)),he==="title"&&E.set("sort","title");let J=await(await fetch(`${O}/cards?${E.toString()}`)).json();if(J.ok){let ve=J.cards||[];S(De=>B?[...De,...ve.filter(Pe=>!De.some(vt=>vt.path===Pe.path))]:ve),Le(Number(J.total)||ve.length)}}catch(E){R(String(E&&E.message?E.message:E))}finally{B?d(!1):k(!1)}},[w,L,F,he]),fe=(0,o.useCallback)(async()=>{try{let[b,n]=await Promise.all([fetch(`${O}/overview`).then(v=>v.json()),fetch(`${O}/capture-log`).then(v=>v.json()).catch(()=>null)]);b.ok&&C(b),m(n&&n.ok&&n.available&&n.health||null)}catch(b){R(String(b&&b.message?b.message:b))}finally{k(!1)}},[]),a=async b=>{try{let v=await(await fetch(`${O}/delete?path=${encodeURIComponent(b)}`)).json();v.ok?(y({ok:!0,msg:t("deleted")}),await fe(),we()):y({ok:!1,msg:v.error||t("deleteFail")})}catch{y({ok:!1,msg:t("deleteFail")})}ue.current&&clearTimeout(ue.current),ue.current=setTimeout(()=>y(null),2e3)};(0,o.useEffect)(()=>(fe(),()=>{Y.current&&clearTimeout(Y.current)}),[fe]);let Fe=b=>{K(b),Y.current&&clearTimeout(Y.current),Y.current=setTimeout(()=>ce(w,b,F),280)},Je=b=>{j(b),ce(b,L,F)};(0,o.useEffect)(()=>{G==="cards"&&ce(w,L,F)},[F,G,I,he]),(0,o.useEffect)(()=>{if(G!=="cards"||H||p||T.length>=We)return;let b=_.current,n=se.current;if(!b)return;let v=new IntersectionObserver(D=>{D[0]&&D[0].isIntersecting&&ce(w,L,F,{append:!0,offset:T.length})},{root:n||null,rootMargin:"400px"});return v.observe(b),()=>v.disconnect()},[G,T,We,H,p,w,L,F,ce]);let et=async b=>{try{let n=b.status;if(!n&&b.path)try{let W=((await fetch(`${O}/cards?status=all&q=${encodeURIComponent(b.title||"")}&limit=50`).then(J=>J.json())).cards||[]).find(J=>J.path===b.path);W&&(n=W.status)}catch{}let v=n&&n!=="approved"?`&status=${encodeURIComponent(n)}`:"",D=await fetch(`${O}/card?path=${encodeURIComponent(b.path||b.id)}${v}`),B=await D.json();B.ok?ye({path:B.path,title:b.title,text:B.text}):R(B.error||`HTTP ${D.status}`)}catch(n){R(String(n&&n.message?n.message:n))}},at=async(b,n)=>{U(!0);try{let v=await fetch(`${O}/export`);if(!v.ok)throw new Error("HTTP "+v.status);let D=await v.json();if(!D.ok)throw new Error(D.error||"export failed");let B,E,W;if(b==="json"){E="memory-vault.json",W="application/json";let De=D.cards.map(Pe=>({path:Pe.path,title:Pe.title,kind:Pe.kind,status:Pe.status,store:Pe.store,text:Pe.text}));B=JSON.stringify({format:D.format||"memory-eternal-vault",formatVersion:D.formatVersion||1,exportedAt:D.exportedAt||new Date().toISOString(),count:De.length,cards:De},null,2)}else E="memory-vault.md",W="text/markdown;charset=utf-8",B=D.cards.map(De=>De.text.trim()).filter(Boolean).join(`
301
+ `;function Ca(t){t.effect(()=>t.locale.register(st,{zh:Jt,en:Zt}),"memory-eternal: locale");let i=t.locale.bind(st);t.effect(()=>t.slots.inject("settings.section",()=>t.slots.register({name:"settings.section",id:st,order:25,label:()=>i("nav"),locale:st,inject:()=>({})},()=>o.default.createElement(Ma,{t:i}))),"memory-eternal: settings section"),t.effect(()=>t.slots.inject("sidebar.footer.action",()=>t.slots.register({name:"sidebar.footer.action",id:`${st}:footer`,order:100,label:()=>i("nav"),locale:st,inject:()=>({})},r=>o.default.createElement(Ra,{t:i,locale:t.locale,wide:!(r&&r.wide===!1)}))),"memory-eternal: sidebar footer action")}function Na(){let[t,i]=(0,o.useState)(O+"/ui/app");return(0,o.useEffect)(()=>{let r=!0,g=()=>fetch("/memory-eternal/api/web-info").then(c=>c.json()).then(c=>{r&&c&&c.ok&&c.alive!==!1&&c.url&&i(c.url.endsWith("/")?c.url:c.url+"/")}).catch(()=>{});return fetch(O+"/ui/app",{method:"HEAD"}).then(c=>{if(r&&!c.ok)return g()}).catch(()=>r?g():void 0),()=>{r=!1}},[]),t}function Ma({t}){return(0,e.jsxs)("div",{style:{display:"flex",flexDirection:"column",height:"100%",minHeight:0},children:[(0,e.jsx)("style",{children:Ee}),(0,e.jsx)(aa,{t,onReload:()=>{},version:0})]})}function Ra({t,locale:i,wide:r}){let[g,c]=(0,o.useState)(!1);return(0,e.jsxs)("div",{className:`me-footer${r?"":" rail"}`,children:[(0,e.jsx)("style",{children:Ee}),(0,e.jsxs)("button",{type:"button",className:"me-footer-btn",onClick:()=>c(!0),"aria-label":t("nav"),title:t("nav"),children:[(0,e.jsx)("span",{className:"me-footer-ico","aria-hidden":"true",children:(0,e.jsx)(ea,{})}),(0,e.jsx)("span",{className:"me-footer-label",children:t("nav")})]}),g&&(0,e.jsx)(Ta,{t,locale:i,onClose:()=>c(!1)})]})}function Ta({t,locale:i,onClose:r,tab:g}){let[c,l]=(0,o.useState)(!1),C=(0,o.useRef)(null),T=Na(),S=(0,o.useCallback)(R=>i&&typeof i.subscribe=="function"?i.subscribe(R):()=>{},[i]),H=(0,o.useCallback)(()=>i&&typeof i.getSnapshot=="function"?i.getSnapshot():null,[i]),k=(0,o.useSyncExternalStore)(S,H),x=k&&(k.active==="zh"||k.active==="en")?k.active:null,m=(0,o.useRef)(null);m.current===null&&x&&(m.current=x);let z=(0,o.useMemo)(()=>{let R=m.current?`lang=${m.current}`:"";if(g==="config")return`${O}/ui/config?tab=config${R?`&${R}`:""}`;let w=T.replace(/\/$/,""),j=w.endsWith("/ui/app")?"?":"/?";return g?`${w}${j}tab=${g}${R?`&${R}`:""}`:R?`${w}${j}${R}`:T},[T,g]);return(0,o.useEffect)(()=>{let R=C.current&&C.current.contentWindow;R&&x&&R.postMessage({source:"memory-eternal",type:"locale",lang:x},"*")},[x]),(0,o.useEffect)(()=>{let R=w=>{w.key==="Escape"&&r()};return window.addEventListener("keydown",R),()=>window.removeEventListener("keydown",R)},[r]),(0,e.jsxs)("div",{className:"me-overlay-top",onClick:r,children:[(0,e.jsx)("style",{children:Ee}),(0,e.jsx)("div",{className:"me-modal"+(c?" me-modal-full":""),onClick:R=>R.stopPropagation(),children:(0,e.jsxs)("div",{style:{display:"flex",flexDirection:"column",height:"100%",minHeight:0,position:"relative"},children:[(0,e.jsxs)("div",{style:{position:"absolute",top:8,right:8,zIndex:10,display:"flex",gap:4,padding:"4px 6px",background:"var(--dsw-alias-bg-overlay, rgba(17,24,39,0.45))",borderRadius:8,backdropFilter:"blur(4px)"},children:[(0,e.jsx)("button",{type:"button",onClick:()=>l(R=>!R),"aria-label":t(c?"exitFullscreen":"enterFullscreen"),title:t(c?"exitFullscreen":"enterFullscreen"),style:{width:26,height:26,borderRadius:6,border:"1px solid rgba(255,255,255,0.18)",background:"rgba(255,255,255,0.08)",color:"#fff",cursor:"pointer",fontSize:14,lineHeight:1,display:"flex",alignItems:"center",justifyContent:"center"},children:c?"\u2921":"\u2922"}),(0,e.jsx)("button",{type:"button",onClick:r,"aria-label":t("close"),title:t("close"),style:{width:26,height:26,borderRadius:6,border:"1px solid rgba(255,255,255,0.18)",background:"rgba(255,255,255,0.08)",color:"#fff",cursor:"pointer",fontSize:16,lineHeight:1,display:"flex",alignItems:"center",justifyContent:"center"},children:"\xD7"})]}),(0,e.jsx)("iframe",{ref:C,src:z,title:"memory-eternal",allow:"clipboard-write; clipboard-read",style:{width:"100%",flex:1,border:0,borderRadius:"inherit",background:"var(--dsw-alias-bg-base, #fff)"}})]})})]})}function ea(){return(0,e.jsxs)("svg",{viewBox:"0 0 24 24","aria-hidden":"true",children:[(0,e.jsx)("defs",{children:(0,e.jsxs)("linearGradient",{id:"mg-brain",x1:"0",y1:"0",x2:"1",y2:"1",children:[(0,e.jsx)("stop",{offset:"0%",stopColor:"#FFC46B"}),(0,e.jsx)("stop",{offset:"50%",stopColor:"#F97316"}),(0,e.jsx)("stop",{offset:"100%",stopColor:"#EA580C"})]})}),(0,e.jsx)("path",{fill:"url(#mg-brain)",d:"M12 4.5a3.2 3.2 0 0 0-6.4.1 4.1 4.1 0 0 0-2.7 5.9 4.1 4.1 0 0 0 .6 6.7A4.2 4.2 0 0 0 12 18.2Z"}),(0,e.jsx)("path",{fill:"url(#mg-brain)",d:"M12 4.5a3.2 3.2 0 0 1 6.4.1 4.1 4.1 0 0 1 2.7 5.9 4.1 4.1 0 0 1-.6 6.7A4.2 4.2 0 0 1 12 18.2Z"}),(0,e.jsx)("path",{d:"M12 9.2c-.8-1.4-1.7-2-3.1-2.2M12 9.2c.8-1.4 1.7-2 3.1-2.2M12 12.4c-1.2 1-3.2 1.6-5 1.4M12 12.4c1.2 1 3.2 1.6 5 1.4M12 13.2v4",fill:"none",stroke:"rgba(255,255,255,0.62)",strokeWidth:"1",strokeLinecap:"round"}),(0,e.jsx)("circle",{cx:"12",cy:"8.6",r:"1.1",fill:"rgba(255,255,255,0.85)"})]})}var yt="0.10.36";function Da({t,inModal:i,onClose:r,onFull:g,full:c}){let[l,C]=(0,o.useState)(null),[T,S]=(0,o.useState)([]),[H,k]=(0,o.useState)(!0),[x,m]=(0,o.useState)(null),[z,R]=(0,o.useState)(""),[w,j]=(0,o.useState)("all"),[L,K]=(0,o.useState)(""),[te,V]=(0,o.useState)("all"),[F,Ie]=(0,o.useState)("all"),[G,ge]=(0,o.useState)(()=>{try{let b=new URLSearchParams(typeof location<"u"?location.search:"").get("tab"),n={cards:"cards",graph:"graph",usage:"stats",stats:"stats",audit:"audit",recycle:"optimize",optimize:"optimize",config:"config"};if(b&&n[b])return n[b]}catch{}return"cards"}),[X,$]=(0,o.useState)(()=>new Set([G])),le=(0,o.useCallback)(b=>{$(n=>n.has(b)?n:new Set(n).add(b)),ge(b)},[]),[he,Ae]=(0,o.useState)("recent"),[ke,y]=(0,o.useState)(null),ue=(0,o.useRef)(null),Te=(0,o.useRef)(null),[h,U]=(0,o.useState)(!1),[re,ye]=(0,o.useState)(null),[ee,Z]=(0,o.useState)(!1),[Be,Ce]=(0,o.useState)(!1),[Re,Ne]=(0,o.useState)(!0),[s,N]=(0,o.useState)(null),[I,Q]=(0,o.useState)(0),we=(0,o.useCallback)(()=>Q(b=>b+1),[]),de=l?.quarantine?.pending??0,ae=l?.quarantine?.deleted??0,Y=(0,o.useRef)(null),[We,Le]=(0,o.useState)(0),[p,d]=(0,o.useState)(!1),_=(0,o.useRef)(null),se=(0,o.useRef)(null),ce=(0,o.useCallback)(async(b=w,n=L,v=F,D={})=>{let B=D.append===!0;try{B?d(!0):(k(!0),R(""));let E=new URLSearchParams;b&&b!=="all"&&E.set("kind",b),n.trim()&&E.set("q",n.trim()),E.set("status",v||"all"),E.set("limit",String(xa)),E.set("offset",String(D.offset||0)),he==="title"&&E.set("sort","title");let J=await(await fetch(`${O}/cards?${E.toString()}`)).json();if(J.ok){let ve=J.cards||[];S(De=>B?[...De,...ve.filter(Pe=>!De.some(vt=>vt.path===Pe.path))]:ve),Le(Number(J.total)||ve.length)}}catch(E){R(String(E&&E.message?E.message:E))}finally{B?d(!1):k(!1)}},[w,L,F,he]),fe=(0,o.useCallback)(async()=>{try{let[b,n]=await Promise.all([fetch(`${O}/overview`).then(v=>v.json()),fetch(`${O}/capture-log`).then(v=>v.json()).catch(()=>null)]);b.ok&&C(b),m(n&&n.ok&&n.available&&n.health||null)}catch(b){R(String(b&&b.message?b.message:b))}finally{k(!1)}},[]),a=async b=>{try{let v=await(await fetch(`${O}/delete?path=${encodeURIComponent(b)}`)).json();v.ok?(y({ok:!0,msg:t("deleted")}),await fe(),we()):y({ok:!1,msg:v.error||t("deleteFail")})}catch{y({ok:!1,msg:t("deleteFail")})}ue.current&&clearTimeout(ue.current),ue.current=setTimeout(()=>y(null),2e3)};(0,o.useEffect)(()=>(fe(),()=>{Y.current&&clearTimeout(Y.current)}),[fe]);let Fe=b=>{K(b),Y.current&&clearTimeout(Y.current),Y.current=setTimeout(()=>ce(w,b,F),280)},Je=b=>{j(b),ce(b,L,F)};(0,o.useEffect)(()=>{G==="cards"&&ce(w,L,F)},[F,G,I,he]),(0,o.useEffect)(()=>{if(G!=="cards"||H||p||T.length>=We)return;let b=_.current,n=se.current;if(!b)return;let v=new IntersectionObserver(D=>{D[0]&&D[0].isIntersecting&&ce(w,L,F,{append:!0,offset:T.length})},{root:n||null,rootMargin:"400px"});return v.observe(b),()=>v.disconnect()},[G,T,We,H,p,w,L,F,ce]);let et=async b=>{try{let n=b.status;if(!n&&b.path)try{let W=((await fetch(`${O}/cards?status=all&q=${encodeURIComponent(b.title||"")}&limit=50`).then(J=>J.json())).cards||[]).find(J=>J.path===b.path);W&&(n=W.status)}catch{}let v=n&&n!=="approved"?`&status=${encodeURIComponent(n)}`:"",D=await fetch(`${O}/card?path=${encodeURIComponent(b.path||b.id)}${v}`),B=await D.json();B.ok?ye({path:B.path,title:b.title,text:B.text}):R(B.error||`HTTP ${D.status}`)}catch(n){R(String(n&&n.message?n.message:n))}},at=async(b,n)=>{U(!0);try{let v=await fetch(`${O}/export`);if(!v.ok)throw new Error("HTTP "+v.status);let D=await v.json();if(!D.ok)throw new Error(D.error||"export failed");let B,E,W;if(b==="json"){E="memory-vault.json",W="application/json";let De=D.cards.map(Pe=>({path:Pe.path,title:Pe.title,kind:Pe.kind,status:Pe.status,store:Pe.store,text:Pe.text}));B=JSON.stringify({format:D.format||"memory-eternal-vault",formatVersion:D.formatVersion||1,exportedAt:D.exportedAt||new Date().toISOString(),count:De.length,cards:De},null,2)}else E="memory-vault.md",W="text/markdown;charset=utf-8",B=D.cards.map(De=>De.text.trim()).filter(Boolean).join(`
302
302
 
303
303
  ---
304
304
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memory-eternal",
3
- "version": "0.10.35",
3
+ "version": "0.10.36",
4
4
  "description": "记忆核心(Memory Eternal):自研的 DeepSeek Harness 记忆插件,不移植任何既有记忆框架——对话结束后自动沉淀知识卡到本地 Markdown Vault(自研去重、自研 CJK 检索、可 git 管理),设置页提供图形化知识库(统计 / 搜索 / 知识图谱 + 侧边栏一键弹窗),Agent 通过 memory_recall 工具按需召回历史上下文。零人工干预。",
5
5
  "license": "MIT",
6
6
  "repository": {
package/web/app.js CHANGED
@@ -300,7 +300,7 @@ Option A, because:
300
300
  height: calc(100vh - var(--dsh-windows-titlebar-height, 0px));
301
301
  max-height: calc(100vh - var(--dsh-windows-titlebar-height, 0px));
302
302
  }
303
- `;function Kg(){return(0,i.jsxs)("svg",{viewBox:"0 0 24 24","aria-hidden":"true",children:[(0,i.jsx)("defs",{children:(0,i.jsxs)("linearGradient",{id:"mg-brain",x1:"0",y1:"0",x2:"1",y2:"1",children:[(0,i.jsx)("stop",{offset:"0%",stopColor:"#FFC46B"}),(0,i.jsx)("stop",{offset:"50%",stopColor:"#F97316"}),(0,i.jsx)("stop",{offset:"100%",stopColor:"#EA580C"})]})}),(0,i.jsx)("path",{fill:"url(#mg-brain)",d:"M12 4.5a3.2 3.2 0 0 0-6.4.1 4.1 4.1 0 0 0-2.7 5.9 4.1 4.1 0 0 0 .6 6.7A4.2 4.2 0 0 0 12 18.2Z"}),(0,i.jsx)("path",{fill:"url(#mg-brain)",d:"M12 4.5a3.2 3.2 0 0 1 6.4.1 4.1 4.1 0 0 1 2.7 5.9 4.1 4.1 0 0 1-.6 6.7A4.2 4.2 0 0 1 12 18.2Z"}),(0,i.jsx)("path",{d:"M12 9.2c-.8-1.4-1.7-2-3.1-2.2M12 9.2c.8-1.4 1.7-2 3.1-2.2M12 12.4c-1.2 1-3.2 1.6-5 1.4M12 12.4c1.2 1 3.2 1.6 5 1.4M12 13.2v4",fill:"none",stroke:"rgba(255,255,255,0.62)",strokeWidth:"1",strokeLinecap:"round"}),(0,i.jsx)("circle",{cx:"12",cy:"8.6",r:"1.1",fill:"rgba(255,255,255,0.85)"})]})}var Ko="0.10.35";function tf({t:e,inModal:t,onClose:n,onFull:r,full:o}){let[l,a]=(0,x.useState)(null),[s,u]=(0,x.useState)([]),[p,h]=(0,x.useState)(!0),[v,m]=(0,x.useState)(null),[C,M]=(0,x.useState)(""),[b,j]=(0,x.useState)("all"),[d,c]=(0,x.useState)(""),[y,S]=(0,x.useState)("all"),[E,H]=(0,x.useState)("all"),[D,A]=(0,x.useState)(()=>{try{let P=new URLSearchParams(typeof location<"u"?location.search:"").get("tab"),g={cards:"cards",graph:"graph",usage:"stats",stats:"stats",audit:"audit",recycle:"optimize",optimize:"optimize",config:"config"};if(P&&g[P])return g[P]}catch{}return"cards"}),[Q,F]=(0,x.useState)(()=>new Set([D])),ne=(0,x.useCallback)(P=>{F(g=>g.has(P)?g:new Set(g).add(P)),A(P)},[]),[ke,it]=(0,x.useState)("recent"),[Le,R]=(0,x.useState)(null),xe=(0,x.useRef)(null),Qe=(0,x.useRef)(null),[L,Z]=(0,x.useState)(!1),[pe,Ne]=(0,x.useState)(null),[ie,oe]=(0,x.useState)(!1),[at,Ae]=(0,x.useState)(!1),[$e,He]=(0,x.useState)(!0),[w,O]=(0,x.useState)(null),[U,le]=(0,x.useState)(0),Fe=(0,x.useCallback)(()=>le(P=>P+1),[]),ge=l?.quarantine?.pending??0,se=l?.quarantine?.deleted??0,te=(0,x.useRef)(null),[Tt,pt]=(0,x.useState)(0),[N,k]=(0,x.useState)(!1),J=(0,x.useRef)(null),fe=(0,x.useRef)(null),ye=(0,x.useCallback)(async(P=b,g=d,_=E,W={})=>{let K=W.append===!0;try{K?k(!0):(h(!0),M(""));let G=new URLSearchParams;P&&P!=="all"&&G.set("kind",P),g.trim()&&G.set("q",g.trim()),G.set("status",_||"all"),G.set("limit",String(Ug)),G.set("offset",String(W.offset||0)),ke==="title"&&G.set("sort","title");let re=await(await fetch(`${q}/cards?${G.toString()}`)).json();if(re.ok){let Te=re.cards||[];u(Xe=>K?[...Xe,...Te.filter(wt=>!Xe.some(Go=>Go.path===wt.path))]:Te),pt(Number(re.total)||Te.length)}}catch(G){M(String(G&&G.message?G.message:G))}finally{K?k(!1):h(!1)}},[b,d,E,ke]),be=(0,x.useCallback)(async()=>{try{let[P,g]=await Promise.all([fetch(`${q}/overview`).then(_=>_.json()),fetch(`${q}/capture-log`).then(_=>_.json()).catch(()=>null)]);P.ok&&a(P),m(g&&g.ok&&g.available&&g.health||null)}catch(P){M(String(P&&P.message?P.message:P))}finally{h(!1)}},[]),f=async P=>{try{let _=await(await fetch(`${q}/delete?path=${encodeURIComponent(P)}`)).json();_.ok?(R({ok:!0,msg:e("deleted")}),await be(),Fe()):R({ok:!1,msg:_.error||e("deleteFail")})}catch{R({ok:!1,msg:e("deleteFail")})}xe.current&&clearTimeout(xe.current),xe.current=setTimeout(()=>R(null),2e3)};(0,x.useEffect)(()=>(be(),()=>{te.current&&clearTimeout(te.current)}),[be]);let xt=P=>{c(P),te.current&&clearTimeout(te.current),te.current=setTimeout(()=>ye(b,P,E),280)},sn=P=>{j(P),ye(P,d,E)};(0,x.useEffect)(()=>{D==="cards"&&ye(b,d,E)},[E,D,U,ke]),(0,x.useEffect)(()=>{if(D!=="cards"||p||N||s.length>=Tt)return;let P=J.current,g=fe.current;if(!P)return;let _=new IntersectionObserver(W=>{W[0]&&W[0].isIntersecting&&ye(b,d,E,{append:!0,offset:s.length})},{root:g||null,rootMargin:"400px"});return _.observe(P),()=>_.disconnect()},[D,s,Tt,p,N,b,d,E,ye]);let bn=async P=>{try{let g=P.status;if(!g&&P.path)try{let X=((await fetch(`${q}/cards?status=all&q=${encodeURIComponent(P.title||"")}&limit=50`).then(re=>re.json())).cards||[]).find(re=>re.path===P.path);X&&(g=X.status)}catch{}let _=g&&g!=="approved"?`&status=${encodeURIComponent(g)}`:"",W=await fetch(`${q}/card?path=${encodeURIComponent(P.path||P.id)}${_}`),K=await W.json();K.ok?Ne({path:K.path,title:P.title,text:K.text}):M(K.error||`HTTP ${W.status}`)}catch(g){M(String(g&&g.message?g.message:g))}},Vn=async(P,g)=>{Z(!0);try{let _=await fetch(`${q}/export`);if(!_.ok)throw new Error("HTTP "+_.status);let W=await _.json();if(!W.ok)throw new Error(W.error||"export failed");let K,G,X;if(P==="json"){G="memory-vault.json",X="application/json";let Xe=W.cards.map(wt=>({path:wt.path,title:wt.title,kind:wt.kind,status:wt.status,store:wt.store,text:wt.text}));K=JSON.stringify({format:W.format||"memory-eternal-vault",formatVersion:W.formatVersion||1,exportedAt:W.exportedAt||new Date().toISOString(),count:Xe.length,cards:Xe},null,2)}else G="memory-vault.md",X="text/markdown;charset=utf-8",K=W.cards.map(Xe=>Xe.text.trim()).filter(Boolean).join(`
303
+ `;function Kg(){return(0,i.jsxs)("svg",{viewBox:"0 0 24 24","aria-hidden":"true",children:[(0,i.jsx)("defs",{children:(0,i.jsxs)("linearGradient",{id:"mg-brain",x1:"0",y1:"0",x2:"1",y2:"1",children:[(0,i.jsx)("stop",{offset:"0%",stopColor:"#FFC46B"}),(0,i.jsx)("stop",{offset:"50%",stopColor:"#F97316"}),(0,i.jsx)("stop",{offset:"100%",stopColor:"#EA580C"})]})}),(0,i.jsx)("path",{fill:"url(#mg-brain)",d:"M12 4.5a3.2 3.2 0 0 0-6.4.1 4.1 4.1 0 0 0-2.7 5.9 4.1 4.1 0 0 0 .6 6.7A4.2 4.2 0 0 0 12 18.2Z"}),(0,i.jsx)("path",{fill:"url(#mg-brain)",d:"M12 4.5a3.2 3.2 0 0 1 6.4.1 4.1 4.1 0 0 1 2.7 5.9 4.1 4.1 0 0 1-.6 6.7A4.2 4.2 0 0 1 12 18.2Z"}),(0,i.jsx)("path",{d:"M12 9.2c-.8-1.4-1.7-2-3.1-2.2M12 9.2c.8-1.4 1.7-2 3.1-2.2M12 12.4c-1.2 1-3.2 1.6-5 1.4M12 12.4c1.2 1 3.2 1.6 5 1.4M12 13.2v4",fill:"none",stroke:"rgba(255,255,255,0.62)",strokeWidth:"1",strokeLinecap:"round"}),(0,i.jsx)("circle",{cx:"12",cy:"8.6",r:"1.1",fill:"rgba(255,255,255,0.85)"})]})}var Ko="0.10.36";function tf({t:e,inModal:t,onClose:n,onFull:r,full:o}){let[l,a]=(0,x.useState)(null),[s,u]=(0,x.useState)([]),[p,h]=(0,x.useState)(!0),[v,m]=(0,x.useState)(null),[C,M]=(0,x.useState)(""),[b,j]=(0,x.useState)("all"),[d,c]=(0,x.useState)(""),[y,S]=(0,x.useState)("all"),[E,H]=(0,x.useState)("all"),[D,A]=(0,x.useState)(()=>{try{let P=new URLSearchParams(typeof location<"u"?location.search:"").get("tab"),g={cards:"cards",graph:"graph",usage:"stats",stats:"stats",audit:"audit",recycle:"optimize",optimize:"optimize",config:"config"};if(P&&g[P])return g[P]}catch{}return"cards"}),[Q,F]=(0,x.useState)(()=>new Set([D])),ne=(0,x.useCallback)(P=>{F(g=>g.has(P)?g:new Set(g).add(P)),A(P)},[]),[ke,it]=(0,x.useState)("recent"),[Le,R]=(0,x.useState)(null),xe=(0,x.useRef)(null),Qe=(0,x.useRef)(null),[L,Z]=(0,x.useState)(!1),[pe,Ne]=(0,x.useState)(null),[ie,oe]=(0,x.useState)(!1),[at,Ae]=(0,x.useState)(!1),[$e,He]=(0,x.useState)(!0),[w,O]=(0,x.useState)(null),[U,le]=(0,x.useState)(0),Fe=(0,x.useCallback)(()=>le(P=>P+1),[]),ge=l?.quarantine?.pending??0,se=l?.quarantine?.deleted??0,te=(0,x.useRef)(null),[Tt,pt]=(0,x.useState)(0),[N,k]=(0,x.useState)(!1),J=(0,x.useRef)(null),fe=(0,x.useRef)(null),ye=(0,x.useCallback)(async(P=b,g=d,_=E,W={})=>{let K=W.append===!0;try{K?k(!0):(h(!0),M(""));let G=new URLSearchParams;P&&P!=="all"&&G.set("kind",P),g.trim()&&G.set("q",g.trim()),G.set("status",_||"all"),G.set("limit",String(Ug)),G.set("offset",String(W.offset||0)),ke==="title"&&G.set("sort","title");let re=await(await fetch(`${q}/cards?${G.toString()}`)).json();if(re.ok){let Te=re.cards||[];u(Xe=>K?[...Xe,...Te.filter(wt=>!Xe.some(Go=>Go.path===wt.path))]:Te),pt(Number(re.total)||Te.length)}}catch(G){M(String(G&&G.message?G.message:G))}finally{K?k(!1):h(!1)}},[b,d,E,ke]),be=(0,x.useCallback)(async()=>{try{let[P,g]=await Promise.all([fetch(`${q}/overview`).then(_=>_.json()),fetch(`${q}/capture-log`).then(_=>_.json()).catch(()=>null)]);P.ok&&a(P),m(g&&g.ok&&g.available&&g.health||null)}catch(P){M(String(P&&P.message?P.message:P))}finally{h(!1)}},[]),f=async P=>{try{let _=await(await fetch(`${q}/delete?path=${encodeURIComponent(P)}`)).json();_.ok?(R({ok:!0,msg:e("deleted")}),await be(),Fe()):R({ok:!1,msg:_.error||e("deleteFail")})}catch{R({ok:!1,msg:e("deleteFail")})}xe.current&&clearTimeout(xe.current),xe.current=setTimeout(()=>R(null),2e3)};(0,x.useEffect)(()=>(be(),()=>{te.current&&clearTimeout(te.current)}),[be]);let xt=P=>{c(P),te.current&&clearTimeout(te.current),te.current=setTimeout(()=>ye(b,P,E),280)},sn=P=>{j(P),ye(P,d,E)};(0,x.useEffect)(()=>{D==="cards"&&ye(b,d,E)},[E,D,U,ke]),(0,x.useEffect)(()=>{if(D!=="cards"||p||N||s.length>=Tt)return;let P=J.current,g=fe.current;if(!P)return;let _=new IntersectionObserver(W=>{W[0]&&W[0].isIntersecting&&ye(b,d,E,{append:!0,offset:s.length})},{root:g||null,rootMargin:"400px"});return _.observe(P),()=>_.disconnect()},[D,s,Tt,p,N,b,d,E,ye]);let bn=async P=>{try{let g=P.status;if(!g&&P.path)try{let X=((await fetch(`${q}/cards?status=all&q=${encodeURIComponent(P.title||"")}&limit=50`).then(re=>re.json())).cards||[]).find(re=>re.path===P.path);X&&(g=X.status)}catch{}let _=g&&g!=="approved"?`&status=${encodeURIComponent(g)}`:"",W=await fetch(`${q}/card?path=${encodeURIComponent(P.path||P.id)}${_}`),K=await W.json();K.ok?Ne({path:K.path,title:P.title,text:K.text}):M(K.error||`HTTP ${W.status}`)}catch(g){M(String(g&&g.message?g.message:g))}},Vn=async(P,g)=>{Z(!0);try{let _=await fetch(`${q}/export`);if(!_.ok)throw new Error("HTTP "+_.status);let W=await _.json();if(!W.ok)throw new Error(W.error||"export failed");let K,G,X;if(P==="json"){G="memory-vault.json",X="application/json";let Xe=W.cards.map(wt=>({path:wt.path,title:wt.title,kind:wt.kind,status:wt.status,store:wt.store,text:wt.text}));K=JSON.stringify({format:W.format||"memory-eternal-vault",formatVersion:W.formatVersion||1,exportedAt:W.exportedAt||new Date().toISOString(),count:Xe.length,cards:Xe},null,2)}else G="memory-vault.md",X="text/markdown;charset=utf-8",K=W.cards.map(Xe=>Xe.text.trim()).filter(Boolean).join(`
304
304
 
305
305
  ---
306
306