memory-eternal 0.10.2 → 0.10.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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.2",
4
+ "version": "0.10.3",
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.2",
3
+ "version": "0.10.3",
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.2",
3
+ "version": "0.10.3",
4
4
  "description": "记忆核心(Memory Eternal):对话结束后自动沉淀知识卡到本地记忆库(~/.dsh/memory-vault),支持检索 / 召回 / 审核中心。",
5
5
  "author": {
6
6
  "name": "EternalNight996",
package/README.en.md CHANGED
@@ -113,7 +113,9 @@ dsh-memory watchdog [--port 7799] # watchdog keep-alive (standalone proce
113
113
  dsh-memory status [--json] # resident watchdog: pid / port / version / alive
114
114
  dsh-memory stop [--port 7799] # stop the resident watchdog (+ the web it spawned)
115
115
  dsh-memory restart [--port 7799] # stop + respawn (apply a new version)
116
- dsh-memory audit list [--status pending] [--json] # list pending/rejected cards
116
+ dsh-memory audit list [--status pending|rejected|approved|deleted|all] [--limit N] [--json]
117
+ # list cards; the total is never rewritten by --limit (which caps output only)
118
+ # all = audit queue (pending + rejected), excluding approved / deleted
117
119
  dsh-memory audit approve <card path...> # human bulk approve
118
120
  dsh-memory audit reject <card path...> --reason "..." # human bulk reject
119
121
  ```
@@ -320,6 +322,7 @@ Diagnostics:
320
322
 
321
323
  | Version | Date | Highlights |
322
324
  |---|---|---|
325
+ | **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. |
323
326
  | **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. |
324
327
  | **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** |
325
328
  | **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** |
package/README.md CHANGED
@@ -124,7 +124,9 @@ dsh-memory watchdog [--port 7799] # 看门狗保活 web(独立进程)
124
124
  dsh-memory status [--json] # 看常驻 watchdog:pid / 端口 / 版本 / 是否存活
125
125
  dsh-memory stop [--port 7799] # 停止常驻 watchdog(配置改动不会自动停;连它拉起的 web 一起停)
126
126
  dsh-memory restart [--port 7799] # 停止并重起 watchdog(升级后换新版)
127
- dsh-memory audit list [--status pending] [--json] # 列出待审/驳回卡片(可配 jq 筛)
127
+ dsh-memory audit list [--status pending|rejected|approved|deleted|all] [--limit N] [--json]
128
+ # 列出卡片;总数不受 --limit 影响,--limit 只限制显示条数(默认全部)
129
+ # all = 审核队列(pending + rejected),不含 approved / deleted
128
130
  dsh-memory audit approve <卡片路径...> # 人工批量批准
129
131
  dsh-memory audit reject <卡片路径...> --reason "..." # 人工批量驳回
130
132
  ```
@@ -356,6 +358,7 @@ curl http://127.0.0.1:7999/memory-eternal/api/web-info # 独立 web 是否活
356
358
 
357
359
  | 版本 | 日期 | 关键改动 |
358
360
  |---|---|---|
361
+ | **v0.10.3** | 2026-10-06 | **修复 `audit list` 的计数语义(#20)**。① 总数与 `--limit` **解耦**:`共 N 张` 取**匹配总数**(`countCards`),`--limit` 只限制显示条数,截断时明确写出 `显示前 M 张 … 还有 X 张未显示`。② **默认不再静默截断**(不给 `--limit`、或 `--limit 0` = 全部显示)。③ `--json` 拆分为 `total`(匹配总数)/ `returned`(本次返回)/ `count`(= total 的兼容别名;0.10.2 里它误等于 returned)/ `limit`(null = 全部)/ `hasMore`。④ `--status all` 的语义写进用法与 README:= 审核队列(pending + rejected),**不含**已出队的 approved 与回收中心的 deleted;`approved` 可显式查询。⑤ **同源修复 MCP 的 `memory_audit_list`**:它同样存在「默认 20 静默截断 + 把截断条数报成待处理数」,现改为报匹配总数 + 未显示提示,上限 100 → 500。 |
359
362
  | **v0.10.2** | 2026-10-06 | **五个存量 issue 集中修复(#15 / #16 / #17 / #18 / #19)**。① **按项目选库在宿主侧终于生效(#15-1)**:宿主过去拿进程 cwd 去匹配 `vaultProfiles[].match.workspace`,而那是「启动 dsh 的目录」——现在从 `session.header.cwd` 取**会话自己的工作区**(capture 与 memory_recall 两条路径都传),CLI/MCP/hooks 行为不变。② **蒸馏不再选错 provider(#15-2)**:声明 `supportsReasoningEffort:false` 的候选不再被传 `reasoningEffort`(能力未知一律按支持处理),并把它们排到支持者之后但**不删除**,避免兜底全无。③ **输出截断不再伪装成解析失败(#18)**:新增独立错误码 `MAX_TOKENS`(含实际上限值),撞上限自动以双倍上限重试同一候选;兜底上报改为**始终以第一候选为主因** + 其余错误按码汇总(不再被最后一个候选覆盖);默认 `captureMaxTokens` 900 → **2000**;失败兜底的原文卡加 `distill-failed` 标签便于过滤。④ **独立 Web 端保存配置不再静默丢失(#16)**:drain 报错不再被 `catch {}` 吞掉(进 stderr + 自动沉淀日志,5 分钟去重节流),共享配置 `memory-eternal-config.json` 改为 **tmp + rename 原子写**(原先非原子写会在截断窗口内被独立 web / MCP 读到半截内容 → JSON.parse 失败 → 设置静默回落成默认值),重试耗尽后**保留 pending 文件**并标注 `dropped:true` + `lastError`(不再删除用户改动),失败状态进 `/diagnostics` 与 `/config` 响应。⑤ **审核中心有了 CLI(#17)**:`dsh-memory audit list/approve/reject`(多路径批量、`--json` 便于 jq 筛选、复用同一套 `setCardStatus` 守卫与 `audit_log`);MCP 侧只新增**只读**的 `memory_audit_list`,不暴露 approve/reject。⑥ **看门狗有了生命周期(#19)**:新增 `dsh-memory status/stop/restart`,锁文件写入 `pkgVersion`(status 会提示「常驻实例是旧版」),宿主日志按抢锁结果如实打印 `spawned` / `delegated to existing pid`(不再每次都谎报 spawned),关闭 `watchdogAutoSpawn` 时明确提示既有实例仍在跑;并且 `dsh-memory stop` 会**连同该 watchdog 拉起的 web 一起停**(Windows 上 `process.kill(pid,'SIGTERM')` 是无条件终止,watchdog 的退出处理器根本不会执行 —— 旧写法会在 stop 后留下一个占着端口的孤儿 web;web 子进程 pid 现在记在锁里,杀之前还会用进程命令行确认确实是 `web.js`,防 pid 复用误杀);README 修正「默认值自相矛盾」与「重启即可关闭」的错误承诺。 |
360
363
  | **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 项** |
361
364
  | **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` 父目录 → 改监听文件) |
@@ -173,27 +173,48 @@ async function main() {
173
173
  }
174
174
  case 'audit': {
175
175
  const sub = argv[1] || ''
176
- const { listCards, setCardStatus } = await import('../lib/vault.js')
176
+ const { listCards, setCardStatus, countCards } = await import('../lib/vault.js')
177
177
  const { defaultVaultDir } = await import('../lib/capture-run.js')
178
178
  const root = path.resolve(argOf('--vault') || defaultVaultDir())
179
179
  const asJson = has('--json')
180
180
  if (sub === 'list') {
181
- const STATUS = { pending: ['pending'], rejected: ['rejected'], deleted: ['deleted'], all: ['pending', 'rejected'], queue: ['pending', 'rejected'] }
181
+ // 状态别名:all = 「审核队列」的全部(pending + rejected)。
182
+ // approved 是已出队状态、deleted 归回收中心,都要显式指定(issue #20 的问号)。
183
+ const STATUS = { pending: ['pending'], rejected: ['rejected'], deleted: ['deleted'], approved: ['approved'], all: ['pending', 'rejected'], queue: ['pending', 'rejected'] }
182
184
  const wanted = String(argOf('--status') || 'pending').toLowerCase()
183
185
  const status = STATUS[wanted] || [wanted]
184
186
  const kind = argOf('--kind') || undefined
185
- const limit = Math.min(Math.max(Number(argOf('--limit')) || 50, 1), 500)
186
- const cards = await listCards(root, { status, kind, limit, sort: 'recent' })
187
+ // issue #20:总数必须与 --limit 解耦。
188
+ // 旧实现把 listCards(limit) 的**截断后条数**当成「共 N 张」,且默认 limit=50 静默截断 ——
189
+ // 队列规模被系统性低估(报告者因此把 32 张待审看成 5 张)。
190
+ // 现在:不给 --limit(或 --limit 0)= 全部;给了只限制**显示条数**,总数永远是匹配总数。
191
+ const rawLimit = argOf('--limit')
192
+ const wantedLimit = Number(rawLimit)
193
+ const limit = rawLimit === null || !Number.isFinite(wantedLimit) || wantedLimit <= 0
194
+ ? null
195
+ : Math.min(Math.floor(wantedLimit), 5000)
196
+ const total = await countCards(root, { status, kind })
197
+ const cards = await listCards(root, { status, kind, limit: limit ?? undefined, sort: 'recent' })
198
+ const returned = cards.length
199
+ const hidden = Math.max(0, total - returned)
187
200
  if (asJson) {
188
201
  console.log(JSON.stringify({
189
- ok: true, vaultDir: root, status: wanted, count: cards.length,
202
+ ok: true, vaultDir: root, status: wanted,
203
+ total, // 匹配总数(与 --limit 无关)
204
+ returned, // 本次实际返回条数
205
+ count: total, // 兼容别名:0.10.2 起曾误等于 returned,属 bug(issue #20),现与 total 一致
206
+ limit: limit ?? null, // 生效的显示上限;null = 全部
207
+ hasMore: hidden > 0,
190
208
  cards: cards.map((c) => ({ path: c.path, title: c.title, kind: c.kind, status: c.status, tags: c.tags, reason: c.reason, createdAt: c.createdAt, submittedBy: c.submittedBy })),
191
209
  }, null, 2))
192
210
  return
193
211
  }
194
- if (!cards.length) { console.log(`没有 ${wanted} 状态的卡片(库:${root})`); return }
195
- console.log(`${wanted} 共 ${cards.length} 张(库:${root}):`)
212
+ if (!total) { console.log(`没有 ${wanted} 状态的卡片(库:${root})`); return }
213
+ console.log(hidden > 0
214
+ ? `${wanted} 共 ${total} 张,显示前 ${returned} 张(库:${root}):`
215
+ : `${wanted} 共 ${total} 张(库:${root}):`)
196
216
  for (const c of cards) console.log(`- [${c.status}] ${c.kind} | ${c.title}\n ${c.path}`)
217
+ if (hidden > 0) console.log(`… 还有 ${hidden} 张未显示(用 --limit N 调整,--limit 0 = 全部)`)
197
218
  return
198
219
  }
199
220
  if (sub === 'approve' || sub === 'reject') {
@@ -223,11 +244,15 @@ async function main() {
223
244
  return
224
245
  }
225
246
  console.error(`用法:
226
- dsh-memory audit list [--status pending|rejected|deleted|all] [--kind KIND] [--limit N] [--json] [--vault DIR]
247
+ dsh-memory audit list [--status pending|rejected|approved|deleted|all] [--kind KIND] [--limit N] [--json] [--vault DIR]
227
248
  dsh-memory audit approve <卡片路径...> [--reason "原因"] [--vault DIR]
228
249
  dsh-memory audit reject <卡片路径...> [--reason "原因"] [--vault DIR]
229
250
 
230
251
  说明:审核写入仍走 setCardStatus(DB 层守卫 + 不可变 audit_log 都不绕过)。
252
+ --status all = 审核队列(pending + rejected),不含已出队的 approved / 回收中心的 deleted。
253
+ --limit 只限制「显示条数」(默认全部显示,--limit 0 同义);总数始终是匹配总数,不会被 --limit 改写。
254
+ --json 字段:total(匹配总数)/ returned(本次返回条数)/ count(= total 的兼容别名)
255
+ / limit(生效上限,null = 全部)/ hasMore / cards
231
256
  MCP 侧只提供只读的 memory_audit_list —— 批准/驳回必须由人显式发起。`)
232
257
  process.exitCode = 1
233
258
  return
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(rt,{zh:Gt,en:Jt}),"memory-eternal: locale");let o=t.locale.bind(rt);t.effect(()=>t.slots.inject("settings.section",()=>t.slots.register({name:"settings.section",id:rt,order:25,label:()=>o("nav"),locale:rt,inject:()=>({})},()=>n.default.createElement(Na,{t:o}))),"memory-eternal: settings section"),t.effect(()=>t.slots.inject("sidebar.footer.action",()=>t.slots.register({name:"sidebar.footer.action",id:`${rt}:footer`,order:100,label:()=>o("nav"),locale:rt,inject:()=>({})},l=>n.default.createElement(Ma,{t:o,locale:t.locale,wide:!(l&&l.wide===!1)}))),"memory-eternal: sidebar footer action")}function Sa(){let[t,o]=(0,n.useState)("http://127.0.0.1:7999/");return(0,n.useEffect)(()=>{fetch("/memory-eternal/api/web-info").then(l=>l.json()).then(l=>{l&&l.ok&&l.url&&o(l.url.endsWith("/")?l.url:l.url+"/")}).catch(()=>{})},[]),t}function Na({t}){return(0,e.jsxs)("div",{style:{display:"flex",flexDirection:"column",height:"100%",minHeight:0},children:[(0,e.jsx)("style",{children:Ae}),(0,e.jsx)(ta,{t,onReload:()=>{},version:0})]})}function Ma({t,locale:o,wide:l}){let[g,c]=(0,n.useState)(!1);return(0,e.jsxs)("div",{className:`me-footer${l?"":" rail"}`,children:[(0,e.jsx)("style",{children:Ae}),(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)(Qt,{})}),(0,e.jsx)("span",{className:"me-footer-label",children:t("nav")})]}),g&&(0,e.jsx)(za,{t,locale:o,onClose:()=>c(!1)})]})}function za({t,locale:o,onClose:l,tab:g}){let[c,i]=(0,n.useState)(!1),x=(0,n.useRef)(null),S=Sa(),v=(0,n.useCallback)(T=>o&&typeof o.subscribe=="function"?o.subscribe(T):()=>{},[o]),P=(0,n.useCallback)(()=>o&&typeof o.getSnapshot=="function"?o.getSnapshot():null,[o]),w=(0,n.useSyncExternalStore)(v,P),h=w&&(w.active==="zh"||w.active==="en")?w.active:null,u=(0,n.useRef)(null);u.current===null&&h&&(u.current=h);let D=(0,n.useMemo)(()=>{let T=u.current?`lang=${u.current}`:"";return g==="config"?`${U}/ui/config?tab=config${T?`&${T}`:""}`:g?`${S.replace(/\/$/,"")}/?tab=${g}${T?`&${T}`:""}`:T?`${S}${S.endsWith("/")?"":"/"}?${T}`:S},[S,g]);return(0,n.useEffect)(()=>{let T=x.current&&x.current.contentWindow;T&&h&&T.postMessage({source:"memory-eternal",type:"locale",lang:h},"*")},[h]),(0,n.useEffect)(()=>{let T=k=>{k.key==="Escape"&&l()};return window.addEventListener("keydown",T),()=>window.removeEventListener("keydown",T)},[l]),(0,e.jsxs)("div",{className:"me-overlay-top",onClick:l,children:[(0,e.jsx)("style",{children:Ae}),(0,e.jsx)("div",{className:"me-modal"+(c?" me-modal-full":""),onClick:T=>T.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:()=>i(T=>!T),"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:l,"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:x,src:D,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 Qt(){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 gt="0.10.2";function Da({t,inModal:o,onClose:l,onFull:g,full:c}){let[i,x]=(0,n.useState)(null),[S,v]=(0,n.useState)([]),[P,w]=(0,n.useState)(!0),[h,u]=(0,n.useState)(null),[D,T]=(0,n.useState)(""),[k,A]=(0,n.useState)("all"),[z,F]=(0,n.useState)(""),[Z,pe]=(0,n.useState)("all"),[_,ze]=(0,n.useState)("all"),[H,re]=(0,n.useState)(()=>{try{let y=new URLSearchParams(typeof location<"u"?location.search:"").get("tab"),E={cards:"cards",graph:"graph",usage:"stats",stats:"stats",audit:"audit",recycle:"optimize",optimize:"optimize",config:"config"};if(y&&E[y])return E[y]}catch{}return"cards"}),[ie,O]=(0,n.useState)(()=>new Set([H])),b=(0,n.useCallback)(y=>{O(E=>E.has(y)?E:new Set(E).add(y)),re(y)},[]),[f,L]=(0,n.useState)("recent"),[K,$]=(0,n.useState)(null),W=(0,n.useRef)(null),ae=(0,n.useRef)(null),[Q,ge]=(0,n.useState)(!1),[Ue,fe]=(0,n.useState)(null),[Ce,he]=(0,n.useState)(!1),[be,De]=(0,n.useState)(!1),[ve,Ie]=(0,n.useState)(!0),[Ze,Ke]=(0,n.useState)(null),[Se,Xe]=(0,n.useState)(0),Qe=(0,n.useCallback)(()=>Xe(y=>y+1),[]),Fe=i?.quarantine?.pending??0,Ne=i?.quarantine?.deleted??0,ee=(0,n.useRef)(null),[je,we]=(0,n.useState)(0),[d,r]=(0,n.useState)(!1),B=(0,n.useRef)(null),s=(0,n.useRef)(null),N=(0,n.useCallback)(async(y=k,E=z,J=_,ue={})=>{let V=ue.append===!0;try{V?r(!0):(w(!0),T(""));let ce=new URLSearchParams;y&&y!=="all"&&ce.set("kind",y),E.trim()&&ce.set("q",E.trim()),ce.set("status",J||"all"),ce.set("limit",String(va)),ce.set("offset",String(ue.offset||0)),f==="title"&&ce.set("sort","title");let ke=await(await fetch(`${U}/cards?${ce.toString()}`)).json();if(ke.ok){let He=ke.cards||[];v(Be=>V?[...Be,...He.filter(We=>!Be.some(yt=>yt.path===We.path))]:He),we(Number(ke.total)||He.length)}}catch(ce){T(String(ce&&ce.message?ce.message:ce))}finally{V?r(!1):w(!1)}},[k,z,_,f]),M=(0,n.useCallback)(async()=>{try{let[y,E]=await Promise.all([fetch(`${U}/overview`).then(J=>J.json()),fetch(`${U}/capture-log`).then(J=>J.json()).catch(()=>null)]);y.ok&&x(y),u(E&&E.ok&&E.available&&E.health||null)}catch(y){T(String(y&&y.message?y.message:y))}finally{w(!1)}},[]),a=async y=>{try{let J=await(await fetch(`${U}/delete?path=${encodeURIComponent(y)}`)).json();J.ok?($({ok:!0,msg:t("deleted")}),await M(),Qe()):$({ok:!1,msg:J.error||t("deleteFail")})}catch{$({ok:!1,msg:t("deleteFail")})}W.current&&clearTimeout(W.current),W.current=setTimeout(()=>$(null),2e3)};(0,n.useEffect)(()=>(M(),()=>{ee.current&&clearTimeout(ee.current)}),[M]);let de=y=>{F(y),ee.current&&clearTimeout(ee.current),ee.current=setTimeout(()=>N(k,y,_),280)},G=y=>{A(y),N(y,z,_)};(0,n.useEffect)(()=>{H==="cards"&&N(k,z,_)},[_,H,Se,f]),(0,n.useEffect)(()=>{if(H!=="cards"||P||d||S.length>=je)return;let y=B.current,E=s.current;if(!y)return;let J=new IntersectionObserver(ue=>{ue[0]&&ue[0].isIntersecting&&N(k,z,_,{append:!0,offset:S.length})},{root:E||null,rootMargin:"400px"});return J.observe(y),()=>J.disconnect()},[H,S,je,P,d,k,z,_,N]);let te=async y=>{try{let E=y.status;if(!E&&y.path)try{let xe=((await fetch(`${U}/cards?status=all&q=${encodeURIComponent(y.title||"")}&limit=50`).then(ke=>ke.json())).cards||[]).find(ke=>ke.path===y.path);xe&&(E=xe.status)}catch{}let J=E&&E!=="approved"?`&status=${encodeURIComponent(E)}`:"",ue=await fetch(`${U}/card?path=${encodeURIComponent(y.path||y.id)}${J}`),V=await ue.json();V.ok?fe({path:V.path,title:y.title,text:V.text}):T(V.error||`HTTP ${ue.status}`)}catch(E){T(String(E&&E.message?E.message:E))}},Oe=async(y,E)=>{ge(!0);try{let J=await fetch(`${U}/export`);if(!J.ok)throw new Error("HTTP "+J.status);let ue=await J.json();if(!ue.ok)throw new Error(ue.error||"export failed");let V,ce,xe;if(y==="json"){ce="memory-vault.json",xe="application/json";let Be=ue.cards.map(We=>({path:We.path,title:We.title,kind:We.kind,status:We.status,store:We.store,text:We.text}));V=JSON.stringify({format:ue.format||"memory-eternal-vault",formatVersion:ue.formatVersion||1,exportedAt:ue.exportedAt||new Date().toISOString(),count:Be.length,cards:Be},null,2)}else ce="memory-vault.md",xe="text/markdown;charset=utf-8",V=ue.cards.map(Be=>Be.text.trim()).filter(Boolean).join(`
301
+ `;function Ca(t){t.effect(()=>t.locale.register(rt,{zh:Gt,en:Jt}),"memory-eternal: locale");let o=t.locale.bind(rt);t.effect(()=>t.slots.inject("settings.section",()=>t.slots.register({name:"settings.section",id:rt,order:25,label:()=>o("nav"),locale:rt,inject:()=>({})},()=>n.default.createElement(Na,{t:o}))),"memory-eternal: settings section"),t.effect(()=>t.slots.inject("sidebar.footer.action",()=>t.slots.register({name:"sidebar.footer.action",id:`${rt}:footer`,order:100,label:()=>o("nav"),locale:rt,inject:()=>({})},l=>n.default.createElement(Ma,{t:o,locale:t.locale,wide:!(l&&l.wide===!1)}))),"memory-eternal: sidebar footer action")}function Sa(){let[t,o]=(0,n.useState)("http://127.0.0.1:7999/");return(0,n.useEffect)(()=>{fetch("/memory-eternal/api/web-info").then(l=>l.json()).then(l=>{l&&l.ok&&l.url&&o(l.url.endsWith("/")?l.url:l.url+"/")}).catch(()=>{})},[]),t}function Na({t}){return(0,e.jsxs)("div",{style:{display:"flex",flexDirection:"column",height:"100%",minHeight:0},children:[(0,e.jsx)("style",{children:Ae}),(0,e.jsx)(ta,{t,onReload:()=>{},version:0})]})}function Ma({t,locale:o,wide:l}){let[g,c]=(0,n.useState)(!1);return(0,e.jsxs)("div",{className:`me-footer${l?"":" rail"}`,children:[(0,e.jsx)("style",{children:Ae}),(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)(Qt,{})}),(0,e.jsx)("span",{className:"me-footer-label",children:t("nav")})]}),g&&(0,e.jsx)(za,{t,locale:o,onClose:()=>c(!1)})]})}function za({t,locale:o,onClose:l,tab:g}){let[c,i]=(0,n.useState)(!1),x=(0,n.useRef)(null),S=Sa(),v=(0,n.useCallback)(T=>o&&typeof o.subscribe=="function"?o.subscribe(T):()=>{},[o]),P=(0,n.useCallback)(()=>o&&typeof o.getSnapshot=="function"?o.getSnapshot():null,[o]),w=(0,n.useSyncExternalStore)(v,P),h=w&&(w.active==="zh"||w.active==="en")?w.active:null,u=(0,n.useRef)(null);u.current===null&&h&&(u.current=h);let D=(0,n.useMemo)(()=>{let T=u.current?`lang=${u.current}`:"";return g==="config"?`${U}/ui/config?tab=config${T?`&${T}`:""}`:g?`${S.replace(/\/$/,"")}/?tab=${g}${T?`&${T}`:""}`:T?`${S}${S.endsWith("/")?"":"/"}?${T}`:S},[S,g]);return(0,n.useEffect)(()=>{let T=x.current&&x.current.contentWindow;T&&h&&T.postMessage({source:"memory-eternal",type:"locale",lang:h},"*")},[h]),(0,n.useEffect)(()=>{let T=k=>{k.key==="Escape"&&l()};return window.addEventListener("keydown",T),()=>window.removeEventListener("keydown",T)},[l]),(0,e.jsxs)("div",{className:"me-overlay-top",onClick:l,children:[(0,e.jsx)("style",{children:Ae}),(0,e.jsx)("div",{className:"me-modal"+(c?" me-modal-full":""),onClick:T=>T.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:()=>i(T=>!T),"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:l,"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:x,src:D,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 Qt(){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 gt="0.10.3";function Da({t,inModal:o,onClose:l,onFull:g,full:c}){let[i,x]=(0,n.useState)(null),[S,v]=(0,n.useState)([]),[P,w]=(0,n.useState)(!0),[h,u]=(0,n.useState)(null),[D,T]=(0,n.useState)(""),[k,A]=(0,n.useState)("all"),[z,F]=(0,n.useState)(""),[Z,pe]=(0,n.useState)("all"),[_,ze]=(0,n.useState)("all"),[H,re]=(0,n.useState)(()=>{try{let y=new URLSearchParams(typeof location<"u"?location.search:"").get("tab"),E={cards:"cards",graph:"graph",usage:"stats",stats:"stats",audit:"audit",recycle:"optimize",optimize:"optimize",config:"config"};if(y&&E[y])return E[y]}catch{}return"cards"}),[ie,O]=(0,n.useState)(()=>new Set([H])),b=(0,n.useCallback)(y=>{O(E=>E.has(y)?E:new Set(E).add(y)),re(y)},[]),[f,L]=(0,n.useState)("recent"),[K,$]=(0,n.useState)(null),W=(0,n.useRef)(null),ae=(0,n.useRef)(null),[Q,ge]=(0,n.useState)(!1),[Ue,fe]=(0,n.useState)(null),[Ce,he]=(0,n.useState)(!1),[be,De]=(0,n.useState)(!1),[ve,Ie]=(0,n.useState)(!0),[Ze,Ke]=(0,n.useState)(null),[Se,Xe]=(0,n.useState)(0),Qe=(0,n.useCallback)(()=>Xe(y=>y+1),[]),Fe=i?.quarantine?.pending??0,Ne=i?.quarantine?.deleted??0,ee=(0,n.useRef)(null),[je,we]=(0,n.useState)(0),[d,r]=(0,n.useState)(!1),B=(0,n.useRef)(null),s=(0,n.useRef)(null),N=(0,n.useCallback)(async(y=k,E=z,J=_,ue={})=>{let V=ue.append===!0;try{V?r(!0):(w(!0),T(""));let ce=new URLSearchParams;y&&y!=="all"&&ce.set("kind",y),E.trim()&&ce.set("q",E.trim()),ce.set("status",J||"all"),ce.set("limit",String(va)),ce.set("offset",String(ue.offset||0)),f==="title"&&ce.set("sort","title");let ke=await(await fetch(`${U}/cards?${ce.toString()}`)).json();if(ke.ok){let He=ke.cards||[];v(Be=>V?[...Be,...He.filter(We=>!Be.some(yt=>yt.path===We.path))]:He),we(Number(ke.total)||He.length)}}catch(ce){T(String(ce&&ce.message?ce.message:ce))}finally{V?r(!1):w(!1)}},[k,z,_,f]),M=(0,n.useCallback)(async()=>{try{let[y,E]=await Promise.all([fetch(`${U}/overview`).then(J=>J.json()),fetch(`${U}/capture-log`).then(J=>J.json()).catch(()=>null)]);y.ok&&x(y),u(E&&E.ok&&E.available&&E.health||null)}catch(y){T(String(y&&y.message?y.message:y))}finally{w(!1)}},[]),a=async y=>{try{let J=await(await fetch(`${U}/delete?path=${encodeURIComponent(y)}`)).json();J.ok?($({ok:!0,msg:t("deleted")}),await M(),Qe()):$({ok:!1,msg:J.error||t("deleteFail")})}catch{$({ok:!1,msg:t("deleteFail")})}W.current&&clearTimeout(W.current),W.current=setTimeout(()=>$(null),2e3)};(0,n.useEffect)(()=>(M(),()=>{ee.current&&clearTimeout(ee.current)}),[M]);let de=y=>{F(y),ee.current&&clearTimeout(ee.current),ee.current=setTimeout(()=>N(k,y,_),280)},G=y=>{A(y),N(y,z,_)};(0,n.useEffect)(()=>{H==="cards"&&N(k,z,_)},[_,H,Se,f]),(0,n.useEffect)(()=>{if(H!=="cards"||P||d||S.length>=je)return;let y=B.current,E=s.current;if(!y)return;let J=new IntersectionObserver(ue=>{ue[0]&&ue[0].isIntersecting&&N(k,z,_,{append:!0,offset:S.length})},{root:E||null,rootMargin:"400px"});return J.observe(y),()=>J.disconnect()},[H,S,je,P,d,k,z,_,N]);let te=async y=>{try{let E=y.status;if(!E&&y.path)try{let xe=((await fetch(`${U}/cards?status=all&q=${encodeURIComponent(y.title||"")}&limit=50`).then(ke=>ke.json())).cards||[]).find(ke=>ke.path===y.path);xe&&(E=xe.status)}catch{}let J=E&&E!=="approved"?`&status=${encodeURIComponent(E)}`:"",ue=await fetch(`${U}/card?path=${encodeURIComponent(y.path||y.id)}${J}`),V=await ue.json();V.ok?fe({path:V.path,title:y.title,text:V.text}):T(V.error||`HTTP ${ue.status}`)}catch(E){T(String(E&&E.message?E.message:E))}},Oe=async(y,E)=>{ge(!0);try{let J=await fetch(`${U}/export`);if(!J.ok)throw new Error("HTTP "+J.status);let ue=await J.json();if(!ue.ok)throw new Error(ue.error||"export failed");let V,ce,xe;if(y==="json"){ce="memory-vault.json",xe="application/json";let Be=ue.cards.map(We=>({path:We.path,title:We.title,kind:We.kind,status:We.status,store:We.store,text:We.text}));V=JSON.stringify({format:ue.format||"memory-eternal-vault",formatVersion:ue.formatVersion||1,exportedAt:ue.exportedAt||new Date().toISOString(),count:Be.length,cards:Be},null,2)}else ce="memory-vault.md",xe="text/markdown;charset=utf-8",V=ue.cards.map(Be=>Be.text.trim()).filter(Boolean).join(`
302
302
 
303
303
  ---
304
304
 
package/lib/mcp.js CHANGED
@@ -14,7 +14,7 @@
14
14
  // 启动:node bin/dsh-memory.mjs mcp (或 node lib/mcp.js)
15
15
 
16
16
  import readline from 'node:readline'
17
- import { search, overview, stats, ensureVault, listCards } from './vault.js'
17
+ import { search, overview, stats, ensureVault, listCards, countCards } from './vault.js'
18
18
  import { runStandaloneCapture, defaultVaultDir } from './capture-run.js'
19
19
 
20
20
  const SUPPORTED_PROTOCOL_VERSIONS = ['2025-06-18', '2025-03-26', '2024-11-05']
@@ -58,12 +58,13 @@ export const MCP_TOOLS = [
58
58
  name: 'memory_audit_list',
59
59
  description:
60
60
  '只读:列出记忆库里待审核 / 已驳回的卡片(不会批准或驳回任何卡)。' +
61
+ '返回文本里的「待处理 N 张」永远是匹配总数;limit 只限制列出条数,被截断时会明确写出还有多少张未显示。' +
61
62
  '用于主动提醒用户「有 N 张待审卡」;批准/驳回必须由人在审核中心面板或 CLI(dsh-memory audit approve/reject)显式发起。',
62
63
  inputSchema: {
63
64
  type: 'object',
64
65
  properties: {
65
- status: { type: 'string', description: 'pending(默认)| rejected | all' },
66
- limit: { type: 'number', description: '返回条数上限,默认 20(最大 100)' },
66
+ status: { type: 'string', description: 'pending(默认)| rejected | all(= pending + rejected)' },
67
+ limit: { type: 'number', description: '列出条数上限,默认 20(最大 500);总数不受它影响' },
67
68
  },
68
69
  },
69
70
  },
@@ -73,7 +74,7 @@ function toolResult(text, isError = false) {
73
74
  return { content: [{ type: 'text', text: String(text) }], ...(isError ? { isError: true } : {}) }
74
75
  }
75
76
 
76
- async function callTool(name, args, vaultRoot, meta = {}) {
77
+ export async function callTool(name, args, vaultRoot, meta = {}) {
77
78
  switch (name) {
78
79
  case 'memory_recall': {
79
80
  const query = String(args?.query || '').trim()
@@ -99,14 +100,23 @@ async function callTool(name, args, vaultRoot, meta = {}) {
99
100
  case 'memory_audit_list': {
100
101
  await ensureVault(vaultRoot)
101
102
  const wanted = String(args?.status || 'pending').toLowerCase()
102
- const STATUS = { pending: ['pending'], rejected: ['rejected'], all: ['pending', 'rejected'], queue: ['pending', 'rejected'] }
103
+ const STATUS = { pending: ['pending'], rejected: ['rejected'], approved: ['approved'], deleted: ['deleted'], all: ['pending', 'rejected'], queue: ['pending', 'rejected'] }
103
104
  const status = STATUS[wanted] || ['pending']
104
- const limit = Math.min(Math.max(Number(args?.limit) || 20, 1), 100)
105
+ // issue #20:默认上限只为控制上下文(不给 agent 灌 500 张卡),但**不能把截断后的条数
106
+ // 当总数报出去** —— 总数取匹配总数,截断时明确写出「还有 N 张未显示」。
107
+ const rawLimit = Number(args?.limit)
108
+ const limit = Number.isFinite(rawLimit) && rawLimit > 0 ? Math.min(Math.floor(rawLimit), 500) : 20
109
+ const total = await countCards(vaultRoot, { status })
110
+ if (!total) return toolResult(`记忆库里没有 ${wanted} 状态的卡片(vault: ${vaultRoot})。`)
105
111
  const cards = await listCards(vaultRoot, { status, limit })
106
- if (!cards.length) return toolResult(`记忆库里没有 ${wanted} 状态的卡片(vault: ${vaultRoot})。`)
112
+ const hidden = Math.max(0, total - cards.length)
107
113
  const lines = cards.map((c, i) => `${i + 1}. [${c.status}] ${c.kind} · ${c.title}\n 路径:${c.path}`)
114
+ const head = hidden > 0
115
+ ? `待处理 ${total} 张,显示前 ${cards.length} 张(status=${wanted},vault: ${vaultRoot}):`
116
+ : `待处理 ${total} 张(status=${wanted},vault: ${vaultRoot}):`
117
+ const tail = hidden > 0 ? `\n\n… 还有 ${hidden} 张未显示(可加大 limit,最大 500)。` : ''
108
118
  return toolResult(
109
- `待处理 ${cards.length} 张(status=${wanted},vault: ${vaultRoot}):\n\n${lines.join('\n')}\n\n` +
119
+ `${head}\n\n${lines.join('\n')}${tail}\n\n` +
110
120
  '(只读列表。批准/驳回请由用户在「审核中心」面板或运行 dsh-memory audit approve/reject 显式完成。)',
111
121
  )
112
122
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memory-eternal",
3
- "version": "0.10.2",
3
+ "version": "0.10.3",
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 Vg(){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 $l="0.10.2";function ef({t:e,inModal:t,onClose:n,onFull:r,full:l}){let[o,a]=(0,v.useState)(null),[s,u]=(0,v.useState)([]),[f,y]=(0,v.useState)(!0),[h,m]=(0,v.useState)(null),[b,z]=(0,v.useState)(""),[k,I]=(0,v.useState)("all"),[p,c]=(0,v.useState)(""),[g,N]=(0,v.useState)("all"),[L,B]=(0,v.useState)("all"),[P,O]=(0,v.useState)(()=>{try{let _=new URLSearchParams(typeof location<"u"?location.search:"").get("tab"),K={cards:"cards",graph:"graph",usage:"stats",stats:"stats",audit:"audit",recycle:"optimize",optimize:"optimize",config:"config"};if(_&&K[_])return K[_]}catch{}return"cards"}),[Y,R]=(0,v.useState)(()=>new Set([P])),M=(0,v.useCallback)(_=>{R(K=>K.has(_)?K:new Set(K).add(_)),O(_)},[]),[T,$]=(0,v.useState)("recent"),[Z,G]=(0,v.useState)(null),X=(0,v.useRef)(null),ce=(0,v.useRef)(null),[ie,_e]=(0,v.useState)(!1),[Qt,Ie]=(0,v.useState)(null),[nt,We]=(0,v.useState)(!1),[Fe,dt]=(0,v.useState)(!1),[$e,pt]=(0,v.useState)(!0),[wn,Yt]=(0,v.useState)(null),[rt,on]=(0,v.useState)(0),kn=(0,v.useCallback)(()=>on(_=>_+1),[]),Dt=o?.quarantine?.pending??0,lt=o?.quarantine?.deleted??0,se=(0,v.useRef)(null),[_t,Ge]=(0,v.useState)(0),[S,x]=(0,v.useState)(!1),U=(0,v.useRef)(null),w=(0,v.useRef)(null),A=(0,v.useCallback)(async(_=k,K=p,oe=L,Me={})=>{let J=Me.append===!0;try{J?x(!0):(y(!0),z(""));let be=new URLSearchParams;_&&_!=="all"&&be.set("kind",_),K.trim()&&be.set("q",K.trim()),be.set("status",oe||"all"),be.set("limit",String(Wg)),be.set("offset",String(Me.offset||0)),T==="title"&&be.set("sort","title");let qe=await(await fetch(`${q}/cards?${be.toString()}`)).json();if(qe.ok){let Rt=qe.cards||[];u(kt=>J?[...kt,...Rt.filter(Ot=>!kt.some(Kl=>Kl.path===Ot.path))]:Rt),Ge(Number(qe.total)||Rt.length)}}catch(be){z(String(be&&be.message?be.message:be))}finally{J?x(!1):y(!1)}},[k,p,L,T]),j=(0,v.useCallback)(async()=>{try{let[_,K]=await Promise.all([fetch(`${q}/overview`).then(oe=>oe.json()),fetch(`${q}/capture-log`).then(oe=>oe.json()).catch(()=>null)]);_.ok&&a(_),m(K&&K.ok&&K.available&&K.health||null)}catch(_){z(String(_&&_.message?_.message:_))}finally{y(!1)}},[]),d=async _=>{try{let oe=await(await fetch(`${q}/delete?path=${encodeURIComponent(_)}`)).json();oe.ok?(G({ok:!0,msg:e("deleted")}),await j(),kn()):G({ok:!1,msg:oe.error||e("deleteFail")})}catch{G({ok:!1,msg:e("deleteFail")})}X.current&&clearTimeout(X.current),X.current=setTimeout(()=>G(null),2e3)};(0,v.useEffect)(()=>(j(),()=>{se.current&&clearTimeout(se.current)}),[j]);let ke=_=>{c(_),se.current&&clearTimeout(se.current),se.current=setTimeout(()=>A(k,_,L),280)},le=_=>{I(_),A(_,p,L)};(0,v.useEffect)(()=>{P==="cards"&&A(k,p,L)},[L,P,rt,T]),(0,v.useEffect)(()=>{if(P!=="cards"||f||S||s.length>=_t)return;let _=U.current,K=w.current;if(!_)return;let oe=new IntersectionObserver(Me=>{Me[0]&&Me[0].isIntersecting&&A(k,p,L,{append:!0,offset:s.length})},{root:K||null,rootMargin:"400px"});return oe.observe(_),()=>oe.disconnect()},[P,s,_t,f,S,k,p,L,A]);let ue=async _=>{try{let K=_.status;if(!K&&_.path)try{let Ue=((await fetch(`${q}/cards?status=all&q=${encodeURIComponent(_.title||"")}&limit=50`).then(qe=>qe.json())).cards||[]).find(qe=>qe.path===_.path);Ue&&(K=Ue.status)}catch{}let oe=K&&K!=="approved"?`&status=${encodeURIComponent(K)}`:"",Me=await fetch(`${q}/card?path=${encodeURIComponent(_.path||_.id)}${oe}`),J=await Me.json();J.ok?Ie({path:J.path,title:_.title,text:J.text}):z(J.error||`HTTP ${Me.status}`)}catch(K){z(String(K&&K.message?K.message:K))}},It=async(_,K)=>{_e(!0);try{let oe=await fetch(`${q}/export`);if(!oe.ok)throw new Error("HTTP "+oe.status);let Me=await oe.json();if(!Me.ok)throw new Error(Me.error||"export failed");let J,be,Ue;if(_==="json"){be="memory-vault.json",Ue="application/json";let kt=Me.cards.map(Ot=>({path:Ot.path,title:Ot.title,kind:Ot.kind,status:Ot.status,store:Ot.store,text:Ot.text}));J=JSON.stringify({format:Me.format||"memory-eternal-vault",formatVersion:Me.formatVersion||1,exportedAt:Me.exportedAt||new Date().toISOString(),count:kt.length,cards:kt},null,2)}else be="memory-vault.md",Ue="text/markdown;charset=utf-8",J=Me.cards.map(kt=>kt.text.trim()).filter(Boolean).join(`
303
+ `;function Vg(){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 $l="0.10.3";function ef({t:e,inModal:t,onClose:n,onFull:r,full:l}){let[o,a]=(0,v.useState)(null),[s,u]=(0,v.useState)([]),[f,y]=(0,v.useState)(!0),[h,m]=(0,v.useState)(null),[b,z]=(0,v.useState)(""),[k,I]=(0,v.useState)("all"),[p,c]=(0,v.useState)(""),[g,N]=(0,v.useState)("all"),[L,B]=(0,v.useState)("all"),[P,O]=(0,v.useState)(()=>{try{let _=new URLSearchParams(typeof location<"u"?location.search:"").get("tab"),K={cards:"cards",graph:"graph",usage:"stats",stats:"stats",audit:"audit",recycle:"optimize",optimize:"optimize",config:"config"};if(_&&K[_])return K[_]}catch{}return"cards"}),[Y,R]=(0,v.useState)(()=>new Set([P])),M=(0,v.useCallback)(_=>{R(K=>K.has(_)?K:new Set(K).add(_)),O(_)},[]),[T,$]=(0,v.useState)("recent"),[Z,G]=(0,v.useState)(null),X=(0,v.useRef)(null),ce=(0,v.useRef)(null),[ie,_e]=(0,v.useState)(!1),[Qt,Ie]=(0,v.useState)(null),[nt,We]=(0,v.useState)(!1),[Fe,dt]=(0,v.useState)(!1),[$e,pt]=(0,v.useState)(!0),[wn,Yt]=(0,v.useState)(null),[rt,on]=(0,v.useState)(0),kn=(0,v.useCallback)(()=>on(_=>_+1),[]),Dt=o?.quarantine?.pending??0,lt=o?.quarantine?.deleted??0,se=(0,v.useRef)(null),[_t,Ge]=(0,v.useState)(0),[S,x]=(0,v.useState)(!1),U=(0,v.useRef)(null),w=(0,v.useRef)(null),A=(0,v.useCallback)(async(_=k,K=p,oe=L,Me={})=>{let J=Me.append===!0;try{J?x(!0):(y(!0),z(""));let be=new URLSearchParams;_&&_!=="all"&&be.set("kind",_),K.trim()&&be.set("q",K.trim()),be.set("status",oe||"all"),be.set("limit",String(Wg)),be.set("offset",String(Me.offset||0)),T==="title"&&be.set("sort","title");let qe=await(await fetch(`${q}/cards?${be.toString()}`)).json();if(qe.ok){let Rt=qe.cards||[];u(kt=>J?[...kt,...Rt.filter(Ot=>!kt.some(Kl=>Kl.path===Ot.path))]:Rt),Ge(Number(qe.total)||Rt.length)}}catch(be){z(String(be&&be.message?be.message:be))}finally{J?x(!1):y(!1)}},[k,p,L,T]),j=(0,v.useCallback)(async()=>{try{let[_,K]=await Promise.all([fetch(`${q}/overview`).then(oe=>oe.json()),fetch(`${q}/capture-log`).then(oe=>oe.json()).catch(()=>null)]);_.ok&&a(_),m(K&&K.ok&&K.available&&K.health||null)}catch(_){z(String(_&&_.message?_.message:_))}finally{y(!1)}},[]),d=async _=>{try{let oe=await(await fetch(`${q}/delete?path=${encodeURIComponent(_)}`)).json();oe.ok?(G({ok:!0,msg:e("deleted")}),await j(),kn()):G({ok:!1,msg:oe.error||e("deleteFail")})}catch{G({ok:!1,msg:e("deleteFail")})}X.current&&clearTimeout(X.current),X.current=setTimeout(()=>G(null),2e3)};(0,v.useEffect)(()=>(j(),()=>{se.current&&clearTimeout(se.current)}),[j]);let ke=_=>{c(_),se.current&&clearTimeout(se.current),se.current=setTimeout(()=>A(k,_,L),280)},le=_=>{I(_),A(_,p,L)};(0,v.useEffect)(()=>{P==="cards"&&A(k,p,L)},[L,P,rt,T]),(0,v.useEffect)(()=>{if(P!=="cards"||f||S||s.length>=_t)return;let _=U.current,K=w.current;if(!_)return;let oe=new IntersectionObserver(Me=>{Me[0]&&Me[0].isIntersecting&&A(k,p,L,{append:!0,offset:s.length})},{root:K||null,rootMargin:"400px"});return oe.observe(_),()=>oe.disconnect()},[P,s,_t,f,S,k,p,L,A]);let ue=async _=>{try{let K=_.status;if(!K&&_.path)try{let Ue=((await fetch(`${q}/cards?status=all&q=${encodeURIComponent(_.title||"")}&limit=50`).then(qe=>qe.json())).cards||[]).find(qe=>qe.path===_.path);Ue&&(K=Ue.status)}catch{}let oe=K&&K!=="approved"?`&status=${encodeURIComponent(K)}`:"",Me=await fetch(`${q}/card?path=${encodeURIComponent(_.path||_.id)}${oe}`),J=await Me.json();J.ok?Ie({path:J.path,title:_.title,text:J.text}):z(J.error||`HTTP ${Me.status}`)}catch(K){z(String(K&&K.message?K.message:K))}},It=async(_,K)=>{_e(!0);try{let oe=await fetch(`${q}/export`);if(!oe.ok)throw new Error("HTTP "+oe.status);let Me=await oe.json();if(!Me.ok)throw new Error(Me.error||"export failed");let J,be,Ue;if(_==="json"){be="memory-vault.json",Ue="application/json";let kt=Me.cards.map(Ot=>({path:Ot.path,title:Ot.title,kind:Ot.kind,status:Ot.status,store:Ot.store,text:Ot.text}));J=JSON.stringify({format:Me.format||"memory-eternal-vault",formatVersion:Me.formatVersion||1,exportedAt:Me.exportedAt||new Date().toISOString(),count:kt.length,cards:kt},null,2)}else be="memory-vault.md",Ue="text/markdown;charset=utf-8",J=Me.cards.map(kt=>kt.text.trim()).filter(Boolean).join(`
304
304
 
305
305
  ---
306
306