memory-eternal 0.23.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/README.md +106 -1
- package/README.zh-CN.md +106 -1
- package/cursor-extension/package.json +1 -1
- package/dsh-tui-plugin/dsh-plugin.json +1 -1
- package/dsh-tui-plugin/package.json +1 -1
- package/lib/client.js +1 -1
- package/lib/search-index.js +212 -0
- package/lib/vault.js +44 -12
- package/orca-plugin/orca-plugin.json +1 -1
- package/package.json +2 -2
- package/scripts/bench-search.mjs +64 -0
- package/web/app.js +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://anthropic.com/claude-code/plugin.schema.json",
|
|
3
3
|
"name": "memory-eternal",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.24.0",
|
|
5
5
|
"description": "记忆核心(Memory Eternal):对话结束后自动沉淀知识卡到本地记忆库(~/.dsh/memory-vault),支持检索 / 召回 / 审核中心。",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "EternalNight996",
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/EternalNight996/memory-eternal/stargazers)
|
|
4
4
|
[](https://www.npmjs.com/package/memory-eternal)
|
|
5
|
-
[](https://github.com/EternalNight996/memory-eternal/releases)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
|
|
8
8
|
**English (default)** · [简体中文](README.zh-CN.md)
|
|
@@ -54,6 +54,75 @@ dsh-memory open # http://127.0.0.1:7999
|
|
|
54
54
|
npx -y -p memory-eternal dsh-memory tui # live queue in the terminal
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
+
## 🧬 How memory actually flows
|
|
58
|
+
|
|
59
|
+
One SQLite vault, one write gate, two read paths. Nothing enters recall without passing the gate.
|
|
60
|
+
|
|
61
|
+
```mermaid
|
|
62
|
+
flowchart TD
|
|
63
|
+
subgraph S1["① Input"]
|
|
64
|
+
A1["Conversation + tool events<br/>captured when a turn ends"]
|
|
65
|
+
A2["Installed skills<br/>scanned from the host"]
|
|
66
|
+
A3["News follows<br/>RSS · GitHub · HN · arXiv · DSH plugins"]
|
|
67
|
+
A4["Manual cards / imports"]
|
|
68
|
+
end
|
|
69
|
+
subgraph S2["② Distill — the only LLM step"]
|
|
70
|
+
B1["LLM distills the session<br/>on failure: raw card, nothing lost"]
|
|
71
|
+
B2["Dedup: lexical Jaccard<br/>optional LLM merge decision"]
|
|
72
|
+
B3["Kind classification"]
|
|
73
|
+
end
|
|
74
|
+
subgraph S3["③ Review gate — human only"]
|
|
75
|
+
C1[("Quarantine<br/>pending / rejected")]
|
|
76
|
+
C2{"approve / reject"}
|
|
77
|
+
C3[("Main store<br/>approved only")]
|
|
78
|
+
end
|
|
79
|
+
subgraph S4["④ Storage"]
|
|
80
|
+
D1[("SQLite: cards + FTS5 index")]
|
|
81
|
+
D2["Markdown mirror<br/>git-able"]
|
|
82
|
+
D3["audit_log + token ledger"]
|
|
83
|
+
end
|
|
84
|
+
subgraph S5["⑤ Recall — every session"]
|
|
85
|
+
E1["Core memory → system prompt"]
|
|
86
|
+
E2["memory_recall → FTS5 + BM25"]
|
|
87
|
+
E3["memory_revise → pending revision"]
|
|
88
|
+
end
|
|
89
|
+
A1 --> B1 --> B2 --> B3 --> C1
|
|
90
|
+
A2 --> B2
|
|
91
|
+
A3 --> C1
|
|
92
|
+
A4 --> B2
|
|
93
|
+
C1 --> C2
|
|
94
|
+
C2 -->|approve| C3
|
|
95
|
+
C2 -->|reject| C1
|
|
96
|
+
C3 --> D1
|
|
97
|
+
D1 --> D2
|
|
98
|
+
C3 --> D3
|
|
99
|
+
C3 --> E1
|
|
100
|
+
C3 --> E2
|
|
101
|
+
E2 -->|hits + chars| D3
|
|
102
|
+
E3 --> C1
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The diagram is only true because three invariants are enforced in code, not by convention:
|
|
106
|
+
|
|
107
|
+
| Invariant | Enforced by |
|
|
108
|
+
|---|---|
|
|
109
|
+
| The main store **only** ever contains approved cards | Two physical tables; every read path queries the main one |
|
|
110
|
+
| An agent can never approve its own memory | Agent writes land in quarantine; approve/reject is a human action in the UI / TUI / CLI |
|
|
111
|
+
| Recall costs **zero model calls** | FTS5 + BM25; the LLM runs only while distilling |
|
|
112
|
+
|
|
113
|
+
## 🔌 How agents talk to the memory
|
|
114
|
+
|
|
115
|
+
| Channel | Direction | When | Gate | Cost |
|
|
116
|
+
|---|---|---|---|---|
|
|
117
|
+
| `memory_recall` | agent → memory | on demand | approved only | **0 model calls** (p50 2.3 ms) |
|
|
118
|
+
| Core memory | memory → agent | every session, automatic | approved only | ~0.2k chars |
|
|
119
|
+
| Session capture / `memory_write` | conversation → memory | turn end | **queued for review** | 1 LLM call |
|
|
120
|
+
| `memory_revise` | agent → memory | on demand | **queued as a revision** | 0 |
|
|
121
|
+
| Review (Web UI · TUI · CLI) | human → memory | manual | — | 0 |
|
|
122
|
+
| Hooks (Claude Code / Codex / Cursor) | host → memory | session end | **queued** | 1 LLM call |
|
|
123
|
+
| MCP server | any MCP client | on demand | same gate | same |
|
|
124
|
+
| News follows | web → memory | interval / 24h | isolated from cards | network only |
|
|
125
|
+
|
|
57
126
|
## 🧾 Does it actually save tokens?
|
|
58
127
|
|
|
59
128
|
Most memory products <em>say</em> they save tokens. We think you should check. The plugin ships a built-in **memory ledger** (Runtime status → Memory ledger) that records:
|
|
@@ -77,6 +146,42 @@ net benefit ≈ avoided context cost − (injected characters + distillation cal
|
|
|
77
146
|
|
|
78
147
|
> Illustrative numbers, not a benchmark — measure your own with the ledger. If recall usefulness stays below ~60%, tune it down (fewer results, shorter summaries) or turn memory off.
|
|
79
148
|
|
|
149
|
+
## 📊 How it compares — measured on a real vault
|
|
150
|
+
|
|
151
|
+
Snapshot **2026-11-07**. Our figures are measured on a real **2,321-card vault** on Windows (not a toy corpus); competitor figures come from their public README / npm / GitHub on the same date. `—` means *not advertised or not verifiable*, not "absent".
|
|
152
|
+
|
|
153
|
+
| Capability | **memory-eternal v0.24.0** | 灵枢 dsh-memory v0.9.23 | SAG-Wiki v0.2.7 | CLAUDE.md / AGENTS.md |
|
|
154
|
+
|---|---|---|---|---|
|
|
155
|
+
| Storage | SQLite + Markdown mirror | Markdown cognitive graph | SQLite FTS5 over your files | flat file(s) |
|
|
156
|
+
| Write gate | **human review queue** (3 stores) | deterministic rules | none — index is truth | none |
|
|
157
|
+
| Immutable audit log | ✅ enforced at the SQLite layer | ✅ claimed | — | — |
|
|
158
|
+
| Retrieval | FTS5 + **BM25/IDF**, column weights | rule-based graph walk | BM25 → JEV (LLM judge) | whole file injected |
|
|
159
|
+
| Recall latency | **p50 2.33 ms · p95 6.18 ms** | — | — | grows with file size |
|
|
160
|
+
| Recall model calls | **0** | 0 | 1–2 per query | 0 |
|
|
161
|
+
| Token ledger (measured usefulness) | ✅ | — | — | — |
|
|
162
|
+
| Multi-agent | DSH + MCP + hooks + TUI + browser | DSH + MCP | CLI + skill | per tool |
|
|
163
|
+
| npm downloads / month | 14,370 | **31,421** | 119 | n/a |
|
|
164
|
+
| GitHub stars | 147 | **612** | 124 | n/a |
|
|
165
|
+
| License | MIT | MIT | MIT | n/a |
|
|
166
|
+
|
|
167
|
+
**We are not the biggest here.** 灵枢 has more stars and downloads; SAG-Wiki retrieves over your own documents better than we do. What we do differently is the *write path*: a human gate, a three-store separation, and an audit trail you can point at — plus a token ledger so you can check whether recall is worth its cost.
|
|
168
|
+
|
|
169
|
+
Reproduce our numbers on your own vault:
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
node scripts/bench-search.mjs --rebuild # rebuild time + p50/p95/max latency
|
|
173
|
+
dsh-memory ledger --days 30 --json # recall injection / capture / usefulness
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
| Our measured baseline | Value |
|
|
177
|
+
|---|---|
|
|
178
|
+
| Cards indexed | 2,321 |
|
|
179
|
+
| Index rebuild (cold) | 869 ms |
|
|
180
|
+
| Recall p50 / p95 / max | 2.33 / 6.18 / 7.60 ms |
|
|
181
|
+
| Recall throughput | ~431 queries/s |
|
|
182
|
+
| Recall model calls | 0 |
|
|
183
|
+
| Injection per recall (60-day ledger) | ~1,394 chars |
|
|
184
|
+
|
|
80
185
|
## 🛡️ Review first, prove later
|
|
81
186
|
|
|
82
187
|
A memory system that silently ingests everything will eventually recall a wrong fact as truth. memory-eternal defaults to **review-before-commit**:
|
package/README.zh-CN.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/EternalNight996/memory-eternal/stargazers)
|
|
4
4
|
[](https://www.npmjs.com/package/memory-eternal)
|
|
5
|
-
[](https://github.com/EternalNight996/memory-eternal/releases)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
|
|
8
8
|
[English (default)](README.md) · **简体中文(镜像)**
|
|
@@ -54,6 +54,75 @@ dsh-memory open # http://127.0.0.1:7999
|
|
|
54
54
|
npx -y -p memory-eternal dsh-memory tui # 终端里的实时待审队列
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
+
## 🧬 底层记忆是怎么流动的
|
|
58
|
+
|
|
59
|
+
一个 SQLite 库、一道写入闸门、两条读取路径。**没过闸门的东西,永远不会被召回。**
|
|
60
|
+
|
|
61
|
+
```mermaid
|
|
62
|
+
flowchart TD
|
|
63
|
+
subgraph S1["① 输入"]
|
|
64
|
+
A1["对话 + 工具调用事件<br/>一轮结束后采集"]
|
|
65
|
+
A2["本机已装技能<br/>从宿主技能注册表扫描"]
|
|
66
|
+
A3["新闻关注<br/>RSS · GitHub · HN · arXiv · DSH 插件"]
|
|
67
|
+
A4["手动建卡 / 导入"]
|
|
68
|
+
end
|
|
69
|
+
subgraph S2["② 沉淀 —— 唯一消耗 LLM 的一步"]
|
|
70
|
+
B1["LLM 蒸馏成知识卡<br/>失败则降级原文卡,不丢内容"]
|
|
71
|
+
B2["去重:词法 Jaccard<br/>可选 LLM 判「新建 vs 追加」"]
|
|
72
|
+
B3["归类:knowledge / project / skill …"]
|
|
73
|
+
end
|
|
74
|
+
subgraph S3["③ 审核闸门 —— 只能由人操作"]
|
|
75
|
+
C1[("隔离区<br/>pending / rejected")]
|
|
76
|
+
C2{"批准 / 驳回"}
|
|
77
|
+
C3[("主库<br/>只存 approved")]
|
|
78
|
+
end
|
|
79
|
+
subgraph S4["④ 存储"]
|
|
80
|
+
D1[("SQLite:cards + FTS5 索引")]
|
|
81
|
+
D2["Markdown 镜像<br/>可 git 管理"]
|
|
82
|
+
D3["审计日志 + 记忆账本"]
|
|
83
|
+
end
|
|
84
|
+
subgraph S5["⑤ 召回 —— 每轮会话"]
|
|
85
|
+
E1["核心记忆 → systemPrompt"]
|
|
86
|
+
E2["memory_recall → FTS5 + BM25"]
|
|
87
|
+
E3["memory_revise → 待审修改副本"]
|
|
88
|
+
end
|
|
89
|
+
A1 --> B1 --> B2 --> B3 --> C1
|
|
90
|
+
A2 --> B2
|
|
91
|
+
A3 --> C1
|
|
92
|
+
A4 --> B2
|
|
93
|
+
C1 --> C2
|
|
94
|
+
C2 -->|批准| C3
|
|
95
|
+
C2 -->|驳回| C1
|
|
96
|
+
C3 --> D1
|
|
97
|
+
D1 --> D2
|
|
98
|
+
C3 --> D3
|
|
99
|
+
C3 --> E1
|
|
100
|
+
C3 --> E2
|
|
101
|
+
E2 -->|命中 + 注入字符| D3
|
|
102
|
+
E3 --> C1
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
这张图之所以成立,是因为三条不变量**写在代码里**,不是写在文档里:
|
|
106
|
+
|
|
107
|
+
| 不变量 | 由什么保证 |
|
|
108
|
+
|---|---|
|
|
109
|
+
| 主库**物理上只存 approved** | 两张物理表;所有读取路径只查主库 |
|
|
110
|
+
| Agent **永远不能批准自己的记忆** | Agent 只能写进隔离区;批准/驳回是 UI / TUI / CLI 里的人工动作 |
|
|
111
|
+
| 召回**零模型调用** | FTS5 + BM25;LLM 只在沉淀时运行 |
|
|
112
|
+
|
|
113
|
+
## 🔌 Agent 与记忆的交互通道
|
|
114
|
+
|
|
115
|
+
| 通道 | 方向 | 何时 | 闸门 | 成本 |
|
|
116
|
+
|---|---|---|---|---|
|
|
117
|
+
| `memory_recall` | Agent → 记忆 | 按需 | 只读已批准 | **0 次模型调用**(p50 2.3ms) |
|
|
118
|
+
| 核心记忆 | 记忆 → Agent | 每轮会话自动 | 只读已批准 | 约 0.2k 字符 |
|
|
119
|
+
| 会话沉淀 / `memory_write` | 对话 → 记忆 | 一轮结束 | **进待审队列** | 1 次 LLM |
|
|
120
|
+
| `memory_revise` | Agent → 记忆 | 按需 | **进待审「修改副本」** | 0 |
|
|
121
|
+
| 审核(Web / TUI / CLI) | 人 → 记忆 | 手动 | — | 0 |
|
|
122
|
+
| Hook(Claude Code / Codex / Cursor) | 宿主 → 记忆 | 会话结束 | **进待审** | 1 次 LLM |
|
|
123
|
+
| MCP 服务 | 任意 MCP 客户端 | 按需 | 同上 | 同上 |
|
|
124
|
+
| 新闻关注 | 网络 → 记忆 | 按间隔 / 24h | 与知识卡隔离 | 仅网络 |
|
|
125
|
+
|
|
57
126
|
## 🧾 真的省 token 吗?
|
|
58
127
|
|
|
59
128
|
大多数记忆产品都说自己省 token。我们认为你应该自己验证。插件内置**记忆账本**(运行状态 → 记忆账本),记录:
|
|
@@ -77,6 +146,42 @@ npx -y -p memory-eternal dsh-memory tui # 终端里的实时待审队列
|
|
|
77
146
|
|
|
78
147
|
> 以上是示意数字,不是 benchmark —— 用账本测你自己的。如果召回有用率长期低于约 60%,就调小(更少条数、更短摘要)或直接关掉记忆。
|
|
79
148
|
|
|
149
|
+
## 📊 与同类产品的实测对比
|
|
150
|
+
|
|
151
|
+
数据快照:**2026-11-07**。我们的数字在**真实 2,321 张卡的库**上实测(不是玩具语料);竞品数据取自同一日期的公开 README / npm / GitHub。`—` 表示**未公开或无法核实**,不代表"没有"。
|
|
152
|
+
|
|
153
|
+
| 能力 | **memory-eternal v0.24.0** | 灵枢 dsh-memory v0.9.23 | SAG-Wiki v0.2.7 | CLAUDE.md / AGENTS.md |
|
|
154
|
+
|---|---|---|---|---|
|
|
155
|
+
| 存储 | SQLite + Markdown 镜像 | Markdown 认知图 | 对本地文件的 SQLite FTS5 索引 | 单个文件 |
|
|
156
|
+
| 写入闸门 | **人工审核队列**(三库分离) | 确定性规则裁决 | 无(索引即真相) | 无 |
|
|
157
|
+
| 不可变审计日志 | ✅ SQLite 层强制 | ✅ 官方声称 | — | — |
|
|
158
|
+
| 检索 | FTS5 + **BM25/IDF**,多列加权 | 规则化图谱遍历 | BM25 → JEV(LLM 判定) | 整文件注入 |
|
|
159
|
+
| 召回延迟 | **p50 2.33ms · p95 6.18ms** | — | — | 随文件体积增长 |
|
|
160
|
+
| 召回模型调用 | **0** | 0 | 每次查询 1–2 次 | 0 |
|
|
161
|
+
| 记忆账本(可量化有用率) | ✅ | — | — | — |
|
|
162
|
+
| 多 Agent | DSH + MCP + Hook + TUI + 浏览器 | DSH + MCP | CLI + Skill | 各工具各自为政 |
|
|
163
|
+
| npm 月下载 | 14,370 | **31,421** | 119 | 不适用 |
|
|
164
|
+
| GitHub Star | 147 | **612** | 124 | 不适用 |
|
|
165
|
+
| 许可证 | MIT | MIT | MIT | 不适用 |
|
|
166
|
+
|
|
167
|
+
**我们不是这里最大的。** 灵枢的 star 和下载都比我们多;SAG-Wiki 检索你自己文档的能力也比我们强。我们做的不一样的是**写入路径**:人审闸门、三库分离、可指认的审计链条,外加一本让你能自己核对"召回到底值不值"的记忆账本。
|
|
168
|
+
|
|
169
|
+
在我们的库上自己复跑:
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
node scripts/bench-search.mjs --rebuild # 重建耗时 + p50/p95/max 延迟
|
|
173
|
+
dsh-memory ledger --days 30 --json # 召回注入 / 沉淀调用 / 有用率
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
| 我们实测的基线 | 数值 |
|
|
177
|
+
|---|---|
|
|
178
|
+
| 索引卡片数 | 2,321 |
|
|
179
|
+
| 冷启动全量重建 | 869 ms |
|
|
180
|
+
| 召回 p50 / p95 / max | 2.33 / 6.18 / 7.60 ms |
|
|
181
|
+
| 召回吞吐 | 约 431 次/秒 |
|
|
182
|
+
| 召回模型调用 | 0 |
|
|
183
|
+
| 每次召回注入(60 天账本) | 约 1,394 字符 |
|
|
184
|
+
|
|
80
185
|
## 🛡️ 先审核,再入库
|
|
81
186
|
|
|
82
187
|
一个「什么都自动吞进去」的记忆系统,迟早会把错误事实当成真相召回。memory-eternal 默认**先审后入**:
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "memory-eternal",
|
|
3
3
|
"displayName": "Memory Atlas",
|
|
4
4
|
"description": "Memory-eternal: open the local Memory Atlas library, review queue and knowledge graph in a Cursor/VS Code webview.",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.24.0",
|
|
6
6
|
"publisher": "EternalNight996",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"repository": {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "urn:dsh-std:community-draft:dsh-plugin:0.15",
|
|
3
3
|
"id": "memory-eternal-tui",
|
|
4
4
|
"name": "memory-eternal-tui",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.24.0",
|
|
6
6
|
"manifestVersion": "0.15",
|
|
7
7
|
"description": "Open Memory Atlas from dsh-TUI: native /audit scene plus browser launcher.",
|
|
8
8
|
"facets": {
|
package/lib/client.js
CHANGED
|
@@ -1015,7 +1015,7 @@ Option A, because:
|
|
|
1015
1015
|
.mc-audit-batchcard > .mc-btn { width: 100%; }
|
|
1016
1016
|
.mc-audit-batchcard > div[style*="flex: 1"] { display: none; }
|
|
1017
1017
|
}
|
|
1018
|
-
`;function Do(t){t.effect(()=>t.locale.register(Yt,{zh:Hn,en:On}),"memory-eternal: locale");let a=t.locale.bind(Yt);t.effect(()=>t.slots.inject("settings.section",()=>t.slots.register({name:"settings.section",id:Yt,order:25,label:()=>a("nav"),locale:Yt,inject:()=>({})},()=>o.default.createElement(Ao,{t:a}))),"memory-eternal: settings section"),t.effect(()=>t.slots.inject("sidebar.footer.action",()=>t.slots.register({name:"sidebar.footer.action",id:`${Yt}:footer`,order:100,label:()=>a("nav"),locale:Yt,inject:()=>({})},n=>o.default.createElement(Lo,{t:a,locale:t.locale,wide:!(n&&n.wide===!1)}))),"memory-eternal: sidebar footer action")}function zo(){let[t,a]=(0,o.useState)(V+"/ui/app");return(0,o.useEffect)(()=>{let n=!0,r=()=>fetch("/memory-eternal/api/web-info").then(s=>s.json()).then(s=>{n&&s&&s.ok&&s.alive!==!1&&s.url&&a(s.url.endsWith("/")?s.url:s.url+"/")}).catch(()=>{});return fetch(V+"/ui/app",{method:"HEAD"}).then(s=>{if(n&&!s.ok)return r()}).catch(()=>n?r():void 0),()=>{n=!1}},[]),t}function Ao({t}){return(0,e.jsxs)("div",{style:{display:"flex",flexDirection:"column",height:"100%",minHeight:0},children:[(0,e.jsx)("style",{children:ft}),(0,e.jsx)(Un,{t,onReload:()=>{},version:0})]})}function Lo({t,locale:a,wide:n}){let[r,s]=(0,o.useState)(!1);return(0,e.jsxs)("div",{className:`me-footer${n?"":" rail"}`,children:[(0,e.jsx)("style",{children:ft}),(0,e.jsxs)("button",{type:"button",className:"me-footer-btn",onClick:()=>s(!0),"aria-label":t("nav"),title:t("nav"),children:[(0,e.jsx)("span",{className:"me-footer-ico","aria-hidden":"true",children:(0,e.jsx)(Bo,{})}),(0,e.jsx)("span",{className:"me-footer-label",children:t("nav")})]}),r&&(0,e.jsx)(Eo,{t,locale:a,onClose:()=>s(!1)})]})}function Fo(){return(0,e.jsx)("svg",{viewBox:"0 0 16 16",fill:"none",stroke:"currentColor",strokeWidth:"1.4","aria-hidden":"true",children:(0,e.jsx)("rect",{x:"2.9",y:"2.9",width:"10.2",height:"10.2",rx:"1.7"})})}function Io(){return(0,e.jsxs)("svg",{viewBox:"0 0 16 16",fill:"none",stroke:"currentColor",strokeWidth:"1.4","aria-hidden":"true",children:[(0,e.jsx)("rect",{x:"2.6",y:"5.4",width:"8",height:"8",rx:"1.5"}),(0,e.jsx)("path",{d:"M5.6 5.4V4.1c0-.9.7-1.6 1.6-1.6h4.7c.9 0 1.6.7 1.6 1.6v4.7c0 .9-.7 1.6-1.6 1.6h-1.3"})]})}function Po(){return(0,e.jsx)("svg",{viewBox:"0 0 16 16",fill:"none",stroke:"currentColor",strokeWidth:"1.4",strokeLinecap:"round","aria-hidden":"true",children:(0,e.jsx)("path",{d:"M3.9 3.9l8.2 8.2M12.1 3.9l-8.2 8.2"})})}function Eo({t,locale:a,onClose:n,tab:r}){let[s,m]=(0,o.useState)(!1),[b,N]=(0,o.useState)(!1),g=(0,o.useRef)(null),C=zo(),p=(0,o.useCallback)(U=>a&&typeof a.subscribe=="function"?a.subscribe(U):()=>{},[a]),S=(0,o.useCallback)(()=>a&&typeof a.getSnapshot=="function"?a.getSnapshot():null,[a]),k=(0,o.useSyncExternalStore)(p,S),R=k&&(k.active==="zh"||k.active==="en")?k.active:null,$=(0,o.useRef)(null);$.current===null&&R&&($.current=R);let q=(0,o.useMemo)(()=>{let U=$.current?`lang=${$.current}`:"";if(r==="config")return`${V}/ui/config?tab=config${U?`&${U}`:""}`;let G=C.replace(/\/$/,""),K=G.endsWith("/ui/app")?"?":"/?";return r?`${G}${K}tab=${r}${U?`&${U}`:""}`:U?`${G}${K}${U}`:C},[C,r]);(0,o.useEffect)(()=>{let U=g.current&&g.current.contentWindow;U&&R&&U.postMessage({source:"memory-eternal",type:"locale",lang:R},"*")},[R]),(0,o.useEffect)(()=>{let U=G=>{G.key==="Escape"&&n()};return window.addEventListener("keydown",U),()=>window.removeEventListener("keydown",U)},[n]);let D=(0,e.jsxs)("div",{className:"me-overlay-top",onClick:n,children:[(0,e.jsx)("style",{children:ft}),(0,e.jsx)("div",{className:"me-modal"+(s?" me-modal-full":""),onClick:U=>U.stopPropagation(),children:(0,e.jsxs)("div",{style:{display:"flex",flexDirection:"column",height:"100%",minHeight:0,position:"relative"},children:[(0,e.jsxs)("div",{className:"me-wctl",children:[(0,e.jsx)("button",{type:"button",className:"me-wctl-btn","data-me-wctl":s?"restore":"full","aria-pressed":s,onClick:()=>m(U=>!U),"aria-label":t(s?"exitFullscreen":"enterFullscreen"),title:t(s?"exitFullscreen":"enterFullscreen"),children:s?(0,e.jsx)(Io,{}):(0,e.jsx)(Fo,{})}),(0,e.jsx)("button",{type:"button",className:"me-wctl-btn me-wctl-close","data-me-wctl":"close",onClick:n,"aria-label":t("close"),title:t("close"),children:(0,e.jsx)(Po,{})})]}),(0,e.jsx)("iframe",{ref:g,src:q,title:"memory-eternal",allow:"clipboard-write; clipboard-read",onLoad:()=>N(!0),style:{width:"100%",flex:1,border:0,borderRadius:"inherit",background:"transparent"}}),!b&&(0,e.jsxs)("div",{className:"me-frame-boot",role:"status","aria-live":"polite",children:[(0,e.jsx)(ra,{}),(0,e.jsx)("span",{className:"me-loading-text",children:t("loading")})]})]})})]});return typeof document>"u"?D:(0,jn.createPortal)(D,document.body)}function Bo(){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 Vt="0.
|
|
1018
|
+
`;function Do(t){t.effect(()=>t.locale.register(Yt,{zh:Hn,en:On}),"memory-eternal: locale");let a=t.locale.bind(Yt);t.effect(()=>t.slots.inject("settings.section",()=>t.slots.register({name:"settings.section",id:Yt,order:25,label:()=>a("nav"),locale:Yt,inject:()=>({})},()=>o.default.createElement(Ao,{t:a}))),"memory-eternal: settings section"),t.effect(()=>t.slots.inject("sidebar.footer.action",()=>t.slots.register({name:"sidebar.footer.action",id:`${Yt}:footer`,order:100,label:()=>a("nav"),locale:Yt,inject:()=>({})},n=>o.default.createElement(Lo,{t:a,locale:t.locale,wide:!(n&&n.wide===!1)}))),"memory-eternal: sidebar footer action")}function zo(){let[t,a]=(0,o.useState)(V+"/ui/app");return(0,o.useEffect)(()=>{let n=!0,r=()=>fetch("/memory-eternal/api/web-info").then(s=>s.json()).then(s=>{n&&s&&s.ok&&s.alive!==!1&&s.url&&a(s.url.endsWith("/")?s.url:s.url+"/")}).catch(()=>{});return fetch(V+"/ui/app",{method:"HEAD"}).then(s=>{if(n&&!s.ok)return r()}).catch(()=>n?r():void 0),()=>{n=!1}},[]),t}function Ao({t}){return(0,e.jsxs)("div",{style:{display:"flex",flexDirection:"column",height:"100%",minHeight:0},children:[(0,e.jsx)("style",{children:ft}),(0,e.jsx)(Un,{t,onReload:()=>{},version:0})]})}function Lo({t,locale:a,wide:n}){let[r,s]=(0,o.useState)(!1);return(0,e.jsxs)("div",{className:`me-footer${n?"":" rail"}`,children:[(0,e.jsx)("style",{children:ft}),(0,e.jsxs)("button",{type:"button",className:"me-footer-btn",onClick:()=>s(!0),"aria-label":t("nav"),title:t("nav"),children:[(0,e.jsx)("span",{className:"me-footer-ico","aria-hidden":"true",children:(0,e.jsx)(Bo,{})}),(0,e.jsx)("span",{className:"me-footer-label",children:t("nav")})]}),r&&(0,e.jsx)(Eo,{t,locale:a,onClose:()=>s(!1)})]})}function Fo(){return(0,e.jsx)("svg",{viewBox:"0 0 16 16",fill:"none",stroke:"currentColor",strokeWidth:"1.4","aria-hidden":"true",children:(0,e.jsx)("rect",{x:"2.9",y:"2.9",width:"10.2",height:"10.2",rx:"1.7"})})}function Io(){return(0,e.jsxs)("svg",{viewBox:"0 0 16 16",fill:"none",stroke:"currentColor",strokeWidth:"1.4","aria-hidden":"true",children:[(0,e.jsx)("rect",{x:"2.6",y:"5.4",width:"8",height:"8",rx:"1.5"}),(0,e.jsx)("path",{d:"M5.6 5.4V4.1c0-.9.7-1.6 1.6-1.6h4.7c.9 0 1.6.7 1.6 1.6v4.7c0 .9-.7 1.6-1.6 1.6h-1.3"})]})}function Po(){return(0,e.jsx)("svg",{viewBox:"0 0 16 16",fill:"none",stroke:"currentColor",strokeWidth:"1.4",strokeLinecap:"round","aria-hidden":"true",children:(0,e.jsx)("path",{d:"M3.9 3.9l8.2 8.2M12.1 3.9l-8.2 8.2"})})}function Eo({t,locale:a,onClose:n,tab:r}){let[s,m]=(0,o.useState)(!1),[b,N]=(0,o.useState)(!1),g=(0,o.useRef)(null),C=zo(),p=(0,o.useCallback)(U=>a&&typeof a.subscribe=="function"?a.subscribe(U):()=>{},[a]),S=(0,o.useCallback)(()=>a&&typeof a.getSnapshot=="function"?a.getSnapshot():null,[a]),k=(0,o.useSyncExternalStore)(p,S),R=k&&(k.active==="zh"||k.active==="en")?k.active:null,$=(0,o.useRef)(null);$.current===null&&R&&($.current=R);let q=(0,o.useMemo)(()=>{let U=$.current?`lang=${$.current}`:"";if(r==="config")return`${V}/ui/config?tab=config${U?`&${U}`:""}`;let G=C.replace(/\/$/,""),K=G.endsWith("/ui/app")?"?":"/?";return r?`${G}${K}tab=${r}${U?`&${U}`:""}`:U?`${G}${K}${U}`:C},[C,r]);(0,o.useEffect)(()=>{let U=g.current&&g.current.contentWindow;U&&R&&U.postMessage({source:"memory-eternal",type:"locale",lang:R},"*")},[R]),(0,o.useEffect)(()=>{let U=G=>{G.key==="Escape"&&n()};return window.addEventListener("keydown",U),()=>window.removeEventListener("keydown",U)},[n]);let D=(0,e.jsxs)("div",{className:"me-overlay-top",onClick:n,children:[(0,e.jsx)("style",{children:ft}),(0,e.jsx)("div",{className:"me-modal"+(s?" me-modal-full":""),onClick:U=>U.stopPropagation(),children:(0,e.jsxs)("div",{style:{display:"flex",flexDirection:"column",height:"100%",minHeight:0,position:"relative"},children:[(0,e.jsxs)("div",{className:"me-wctl",children:[(0,e.jsx)("button",{type:"button",className:"me-wctl-btn","data-me-wctl":s?"restore":"full","aria-pressed":s,onClick:()=>m(U=>!U),"aria-label":t(s?"exitFullscreen":"enterFullscreen"),title:t(s?"exitFullscreen":"enterFullscreen"),children:s?(0,e.jsx)(Io,{}):(0,e.jsx)(Fo,{})}),(0,e.jsx)("button",{type:"button",className:"me-wctl-btn me-wctl-close","data-me-wctl":"close",onClick:n,"aria-label":t("close"),title:t("close"),children:(0,e.jsx)(Po,{})})]}),(0,e.jsx)("iframe",{ref:g,src:q,title:"memory-eternal",allow:"clipboard-write; clipboard-read",onLoad:()=>N(!0),style:{width:"100%",flex:1,border:0,borderRadius:"inherit",background:"transparent"}}),!b&&(0,e.jsxs)("div",{className:"me-frame-boot",role:"status","aria-live":"polite",children:[(0,e.jsx)(ra,{}),(0,e.jsx)("span",{className:"me-loading-text",children:t("loading")})]})]})})]});return typeof document>"u"?D:(0,jn.createPortal)(D,document.body)}function Bo(){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 Vt="0.24.0";function jo({t}){let[a,n]=(0,o.useState)(""),r="dsh-memory tui",s="dsh-memory tui --write",m=async(C,p)=>{try{await navigator.clipboard.writeText(C),n(p),setTimeout(()=>n(""),1600)}catch{}},b={border:"1px solid #3a404a",borderRadius:14,background:"#22262d",padding:"16px 18px"},N={margin:"10px 0 0",padding:"12px 14px",borderRadius:10,background:"#171a1f",border:"1px solid #333943",color:"#f3f5f8",fontSize:12.5,lineHeight:1.7,overflowX:"auto",fontFamily:"ui-monospace, Consolas, monospace"},g={marginTop:10,border:"1px solid #4a515c",background:"transparent",color:"#ffb08a",borderRadius:9,padding:"7px 12px",fontSize:12,cursor:"pointer"};return(0,e.jsxs)("div",{style:{padding:"22px 26px 34px",maxWidth:1040,color:"#f3f5f8"},children:[(0,e.jsxs)("div",{style:{display:"flex",alignItems:"baseline",gap:12,flexWrap:"wrap"},children:[(0,e.jsx)("h2",{style:{margin:0,fontSize:24,letterSpacing:"-.02em"},children:t("terminalTitle")}),(0,e.jsx)("span",{style:{color:"#99a1ac",fontSize:12},children:"TUI \xB7 LIVE \xB7 dsh-tui"})]}),(0,e.jsx)("p",{style:{margin:"9px 0 20px",color:"#aeb5bf",fontSize:13.5,lineHeight:1.8},children:t("terminalIntro")}),(0,e.jsxs)("div",{style:{display:"grid",gridTemplateColumns:"minmax(0, 1.1fr) minmax(0, .9fr)",gap:14},children:[(0,e.jsxs)("div",{style:b,children:[(0,e.jsxs)("div",{style:{fontSize:13,fontWeight:700},children:["\u25B8 ",t("terminalRun")]}),(0,e.jsx)("pre",{style:N,children:r}),(0,e.jsx)("button",{type:"button",style:g,onClick:()=>m(r,"run"),children:a==="run"?"\u2713 "+t("terminalCopied"):"\u29C9 "+t("terminalCopy")}),(0,e.jsxs)("div",{style:{marginTop:12,color:"#99a1ac",fontSize:12,lineHeight:1.7},children:["\u9ED8\u8BA4\u8BFB\u53D6\u771F\u5B9E\u8BB0\u5FC6\u5E93\u4E0E\u5F85\u5BA1\u961F\u5217\uFF1B\u53EA\u8BFB\uFF0C\u786E\u8BA4\u540E\u624D\u7528 `--write`\u3002\u793A\u4F8B\u6570\u636E\u7528 `--demo`\u3002",(0,e.jsx)("br",{}),"\u6CA1\u6709 `dsh-memory` \u547D\u4EE4\u65F6\uFF1A`npx -p memory-eternal dsh-memory tui`"]})]}),(0,e.jsxs)("div",{style:b,children:[(0,e.jsxs)("div",{style:{fontSize:13,fontWeight:700},children:["\u25B8 ",t("terminalTui")]}),(0,e.jsx)("pre",{style:N,children:s}),(0,e.jsx)("button",{type:"button",style:g,onClick:()=>m(s,"tui"),children:a==="tui"?"\u2713 "+t("terminalCopied"):"\u29C9 "+t("terminalCopy")}),(0,e.jsx)("div",{style:{marginTop:12,color:"#99a1ac",fontSize:12,lineHeight:1.7},children:"\u771F\u5B9E\u6279\u51C6/\u9A73\u56DE\uFF1B\u4E0D\u786E\u5B9A\u65F6\u5148\u7528\u53EA\u8BFB\u6A21\u5F0F\u3002"})]})]}),(0,e.jsxs)("div",{style:{...b,marginTop:14},children:[(0,e.jsxs)("div",{style:{fontSize:13,fontWeight:700},children:["\u25B8 ",t("terminalPreview")]}),(0,e.jsx)("pre",{style:{...N,marginTop:10,color:"#d7dce3",fontSize:12,lineHeight:1.5},children:`\u256D\u2500 MEMORY ATLAS / \u5F85\u5BA1 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E
|
|
1019
1019
|
\u2502 \u7C7B\u578B \u25CF \u5168\u90E8 7 \u25CB \u9879\u76EE 3 \u25CB \u77E5\u8BC6 1 \u25CB \u6559\u8BAD 1 \u2502
|
|
1020
1020
|
\u2502 \u2502
|
|
1021
1021
|
\u2502 \u25A3 \u5168\u9009 7 \u5DF2\u9009 3 s \u5168\u9009 \u2502
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
// 记忆核心 · 检索索引(SQLite FTS5 + BM25/IDF)
|
|
2
|
+
//
|
|
3
|
+
// 为什么要有这一层:旧的 search() 是**全表扫描 + 子串计数**——
|
|
4
|
+
// score = 整串命中 3 分 + 每个词命中 1 分
|
|
5
|
+
// 没有 IDF、没有词频、没有长度归一,于是「用户」「插件」这种高频词和稀有词同权,
|
|
6
|
+
// 含一个「用户」的原始对话快照就能挤进前五(实测:搜「审核红线」返回全是噪声)。
|
|
7
|
+
//
|
|
8
|
+
// 这里换成 FTS5:
|
|
9
|
+
// · BM25 自带 IDF 与长度归一,天然压制高频噪声词;
|
|
10
|
+
// · 多列加权(标题 8 / 正文 1 / 标签 2 / 路径 1.5),标题命中优先;
|
|
11
|
+
// · 中文用**字符 bigram 预切词**(不用 jieba、零依赖):FTS5 的 unicode61 与 trigram
|
|
12
|
+
// 对「检索」「插件」这类 2 字中文词都会失效,bigram 是唯一零依赖的正解。
|
|
13
|
+
//
|
|
14
|
+
// 维护策略(两次踩坑后的结论):
|
|
15
|
+
// 1. **写时增量**:卡片增删改后立刻调用 indexCardById / unindexCard,
|
|
16
|
+
// 检索路径不承担重建成本(否则每次搜都要 diff 全表,比旧扫描还慢)。
|
|
17
|
+
// 2. **签名只由 SQL 计算**:JS 的 String#length 是 UTF-16 码元、SQLite length() 是字符,
|
|
18
|
+
// 带 emoji 的卡两边永远对不上 —— 曾导致 279 张卡每次检索都被重灌(300ms/次)。
|
|
19
|
+
// 现在两边都用同一段 SQL 表达式,彻底消除口径差。
|
|
20
|
+
// 3. **兜底对账**:进程内每 5 分钟最多做一次全表 diff,防外部改库导致索引漂移。
|
|
21
|
+
const FTS_TABLE = 'cards_fts'
|
|
22
|
+
const ROWS_TABLE = '_fts_rows'
|
|
23
|
+
const INDEX_VERSION = '2'
|
|
24
|
+
const RECONCILE_MS = 5 * 60 * 1000
|
|
25
|
+
const lastReconcile = new WeakMap()
|
|
26
|
+
|
|
27
|
+
const CJK_RE = /[\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff]/
|
|
28
|
+
|
|
29
|
+
/** 签名表达式(SQL 侧唯一口径,禁止在 JS 里另算一套)。 */
|
|
30
|
+
const SIG_SQL = "(COALESCE(updated_at,'') || '|' || length(body) || '|' || length(title) || '|' || length(tags) || '|' || length(path))"
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 把文本切成 FTS token 序列。
|
|
34
|
+
* 中文:连续汉字段 → 全部 bigram(长度 1 则保留单字)
|
|
35
|
+
* 其他:按非字母数字切分,小写,保留长度 ≥2 的词
|
|
36
|
+
*/
|
|
37
|
+
export function tokenize(text) {
|
|
38
|
+
const s = String(text || '').toLowerCase()
|
|
39
|
+
const out = []
|
|
40
|
+
let i = 0
|
|
41
|
+
while (i < s.length) {
|
|
42
|
+
const ch = s[i]
|
|
43
|
+
if (CJK_RE.test(ch)) {
|
|
44
|
+
let j = i
|
|
45
|
+
while (j < s.length && CJK_RE.test(s[j])) j++
|
|
46
|
+
const run = s.slice(i, j)
|
|
47
|
+
if (run.length === 1) out.push(run)
|
|
48
|
+
else for (let k = 0; k < run.length - 1; k++) out.push(run.slice(k, k + 2))
|
|
49
|
+
i = j
|
|
50
|
+
} else if (/[0-9a-z\u00c0-\u024f]/i.test(ch)) {
|
|
51
|
+
let j = i
|
|
52
|
+
while (j < s.length && /[0-9a-z\u00c0-\u024f]/i.test(s[j])) j++
|
|
53
|
+
const w = s.slice(i, j)
|
|
54
|
+
if (w.length >= 2) out.push(w)
|
|
55
|
+
i = j
|
|
56
|
+
} else {
|
|
57
|
+
i++
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return [...new Set(out)]
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 索引存储形态:空格分隔的 token 串。 */
|
|
64
|
+
export function indexText(text) { return tokenize(text).join(' ') }
|
|
65
|
+
|
|
66
|
+
/** 构造 FTS5 MATCH 表达式。mode='and' 要求全部命中,'or' 任一命中。 */
|
|
67
|
+
export function buildMatch(terms, mode = 'and') {
|
|
68
|
+
const valid = (Array.isArray(terms) ? terms : [])
|
|
69
|
+
.map((t) => String(t || '').trim())
|
|
70
|
+
.filter(Boolean)
|
|
71
|
+
.map((t) => '"' + t.replace(/"/g, '""') + '"')
|
|
72
|
+
if (!valid.length) return ''
|
|
73
|
+
return valid.join(mode === 'or' ? ' OR ' : ' AND ')
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function ensureTables(db) {
|
|
77
|
+
db.exec(`CREATE TABLE IF NOT EXISTS ${ROWS_TABLE} (id INTEGER PRIMARY KEY, sig TEXT NOT NULL)`)
|
|
78
|
+
db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS ${FTS_TABLE} USING fts5(title, body, tags, path, content='', contentless_delete=1)`)
|
|
79
|
+
db.exec(`CREATE TABLE IF NOT EXISTS _fts_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL)`)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function writeSig(db, id) {
|
|
83
|
+
db.prepare(`INSERT OR REPLACE INTO ${ROWS_TABLE}(id, sig) SELECT id, ${SIG_SQL} FROM cards WHERE id = ?`).run(id)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function insertRow(db, row) {
|
|
87
|
+
db.prepare(`DELETE FROM ${FTS_TABLE} WHERE rowid = ?`).run(row.id)
|
|
88
|
+
db.prepare(`INSERT INTO ${FTS_TABLE}(rowid, title, body, tags, path) VALUES (?, ?, ?, ?, ?)`)
|
|
89
|
+
.run(row.id, indexText(row.title), indexText(row.body), indexText(row.tags), indexText(row.path))
|
|
90
|
+
writeSig(db, row.id)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** 单卡增量索引(写路径调用;任何错误都吞掉,索引绝不能影响业务写入)。 */
|
|
94
|
+
export function indexCardById(db, id) {
|
|
95
|
+
try {
|
|
96
|
+
ensureTables(db)
|
|
97
|
+
const row = db.prepare('SELECT id, path, title, tags, body FROM cards WHERE id = ?').get(id)
|
|
98
|
+
if (!row) return unindexCard(db, id)
|
|
99
|
+
insertRow(db, row)
|
|
100
|
+
return true
|
|
101
|
+
} catch { return false }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** 单卡移出索引(删除 / 迁出主库)。 */
|
|
105
|
+
export function unindexCard(db, id) {
|
|
106
|
+
try {
|
|
107
|
+
ensureTables(db)
|
|
108
|
+
db.prepare(`DELETE FROM ${FTS_TABLE} WHERE rowid = ?`).run(id)
|
|
109
|
+
db.prepare(`DELETE FROM ${ROWS_TABLE} WHERE id = ?`).run(id)
|
|
110
|
+
return true
|
|
111
|
+
} catch { return false }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** 全量重建(导出给排障 / 「重建索引」用)。 */
|
|
115
|
+
export function reindexAll(db, { force = true } = {}) {
|
|
116
|
+
ensureTables(db)
|
|
117
|
+
db.exec(`DELETE FROM ${FTS_TABLE}`)
|
|
118
|
+
db.exec(`DELETE FROM ${ROWS_TABLE}`)
|
|
119
|
+
const rows = db.prepare('SELECT id, path, title, tags, body FROM cards').all()
|
|
120
|
+
const del = db.prepare(`DELETE FROM ${FTS_TABLE} WHERE rowid = ?`)
|
|
121
|
+
const ins = db.prepare(`INSERT INTO ${FTS_TABLE}(rowid, title, body, tags, path) VALUES (?, ?, ?, ?, ?)`)
|
|
122
|
+
db.exec('BEGIN')
|
|
123
|
+
try {
|
|
124
|
+
for (const r of rows) {
|
|
125
|
+
del.run(r.id)
|
|
126
|
+
ins.run(r.id, indexText(r.title), indexText(r.body), indexText(r.tags), indexText(r.path))
|
|
127
|
+
}
|
|
128
|
+
db.prepare(`INSERT OR REPLACE INTO ${ROWS_TABLE}(id, sig) SELECT id, ${SIG_SQL} FROM cards`).run()
|
|
129
|
+
db.prepare("INSERT OR REPLACE INTO _fts_meta(key, value) VALUES('version', ?)").run(INDEX_VERSION)
|
|
130
|
+
db.exec('COMMIT')
|
|
131
|
+
} catch (e) { try { db.exec('ROLLBACK') } catch {} throw e }
|
|
132
|
+
lastReconcile.set(db, Date.now())
|
|
133
|
+
return { ok: true, indexed: rows.length }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 对账(不是每次检索都跑):进程内每 5 分钟最多一次全表 diff。
|
|
138
|
+
* 写路径已经做了增量,这里只兜住「外部改库 / 漏挂的写路径」。
|
|
139
|
+
*/
|
|
140
|
+
export function ensureIndex(db, { force = false } = {}) {
|
|
141
|
+
ensureTables(db)
|
|
142
|
+
const meta = db.prepare("SELECT value FROM _fts_meta WHERE key = 'version'").get()
|
|
143
|
+
if (!meta || meta.value !== INDEX_VERSION) return reindexAll(db)
|
|
144
|
+
|
|
145
|
+
const indexedCount = db.prepare(`SELECT COUNT(*) n FROM ${ROWS_TABLE}`).get().n
|
|
146
|
+
const cardCount = db.prepare('SELECT COUNT(*) n FROM cards').get().n
|
|
147
|
+
if (!force && indexedCount === cardCount && Date.now() - (lastReconcile.get(db) || 0) < RECONCILE_MS) {
|
|
148
|
+
return { rebuilt: 'skipped' }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const current = db.prepare(`SELECT id, ${SIG_SQL} AS sig FROM cards`).all()
|
|
152
|
+
const indexed = new Map(db.prepare(`SELECT id, sig FROM ${ROWS_TABLE}`).all().map((r) => [r.id, r.sig]))
|
|
153
|
+
const changed = []
|
|
154
|
+
const seen = new Set()
|
|
155
|
+
for (const row of current) {
|
|
156
|
+
seen.add(row.id)
|
|
157
|
+
if (indexed.get(row.id) !== row.sig) changed.push(row.id)
|
|
158
|
+
}
|
|
159
|
+
const removed = []
|
|
160
|
+
for (const id of indexed.keys()) if (!seen.has(id)) removed.push(id)
|
|
161
|
+
lastReconcile.set(db, Date.now())
|
|
162
|
+
|
|
163
|
+
if (!changed.length && !removed.length) return { rebuilt: 'none' }
|
|
164
|
+
const rows = db.prepare('SELECT id, path, title, tags, body FROM cards WHERE id IN (' + [...changed].map(() => '?').join(',') + ')').all(...changed)
|
|
165
|
+
const rowMap = new Map(rows.map((r) => [r.id, r]))
|
|
166
|
+
db.exec('BEGIN')
|
|
167
|
+
try {
|
|
168
|
+
for (const id of removed) unindexCard(db, id)
|
|
169
|
+
for (const id of changed) {
|
|
170
|
+
const r = rowMap.get(id)
|
|
171
|
+
if (r) insertRow(db, r)
|
|
172
|
+
else unindexCard(db, id)
|
|
173
|
+
}
|
|
174
|
+
db.exec('COMMIT')
|
|
175
|
+
} catch (e) { try { db.exec('ROLLBACK') } catch {} throw e }
|
|
176
|
+
return { rebuilt: 'partial', updated: changed.length, removed: removed.length }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* FTS5 检索。返回 [{ id, score }],score 越大越相关(bm25 是负值,已翻正)。
|
|
181
|
+
* 先按「全部词命中」查;零结果才退化成「任一命中」,避免多打一个词就全灭。
|
|
182
|
+
*/
|
|
183
|
+
export function searchIndexed(db, query, { limit = 60 } = {}) {
|
|
184
|
+
const terms = tokenize(query)
|
|
185
|
+
if (!terms.length) return []
|
|
186
|
+
ensureTables(db)
|
|
187
|
+
const stmt = db.prepare(
|
|
188
|
+
`SELECT rowid AS id, bm25(${FTS_TABLE}, 8.0, 1.0, 2.0, 1.5) AS s FROM ${FTS_TABLE} WHERE ${FTS_TABLE} MATCH ? ORDER BY s LIMIT ?`
|
|
189
|
+
)
|
|
190
|
+
let rows = []
|
|
191
|
+
const andExpr = buildMatch(terms, 'and')
|
|
192
|
+
if (andExpr) { try { rows = stmt.all(andExpr, limit) } catch { rows = [] } }
|
|
193
|
+
if (!rows.length) {
|
|
194
|
+
const orExpr = buildMatch(terms, 'or')
|
|
195
|
+
if (orExpr) { try { rows = stmt.all(orExpr, limit) } catch { rows = [] } }
|
|
196
|
+
}
|
|
197
|
+
// bm25 是「越小越相关」的负值,量级随语料规模变化(小库 1e-6、大库 1e-1)。
|
|
198
|
+
// 直接乘常数会让小库的分数 <1,与旧的 1~10 分口径不兼容(minScore=2 会误杀)。
|
|
199
|
+
// 用 log1p 归一到 ~1–20 的稳定区间:单调保序,且任何命中都 ≥1。
|
|
200
|
+
return rows.map((r) => ({ id: r.id, score: 1 + Math.log1p(-Number(r.s) * 1e6) }))
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** 索引统计(排障/面板用)。 */
|
|
204
|
+
export function indexStats(db) {
|
|
205
|
+
try {
|
|
206
|
+
ensureTables(db)
|
|
207
|
+
return {
|
|
208
|
+
indexed: db.prepare(`SELECT COUNT(*) n FROM ${FTS_TABLE}`).get().n,
|
|
209
|
+
cards: db.prepare('SELECT COUNT(*) n FROM cards').get().n,
|
|
210
|
+
}
|
|
211
|
+
} catch { return { indexed: 0, cards: 0 } }
|
|
212
|
+
}
|
package/lib/vault.js
CHANGED
|
@@ -7,6 +7,7 @@ import path from 'node:path'
|
|
|
7
7
|
import fs from 'node:fs'
|
|
8
8
|
import { getDb, enforceAudit, nextCardId, MAIN_STATUS, QUARANTINE_STATUSES, tableForStatus } from './db.js'
|
|
9
9
|
import { minhashSignature, lshCandidates } from './minhash.js'
|
|
10
|
+
import { indexCardById, unindexCard } from './search-index.js'
|
|
10
11
|
|
|
11
12
|
/** 主题目录根。 */
|
|
12
13
|
export const KIND_ROOTS = {
|
|
@@ -416,6 +417,9 @@ function moveCardBetweenStores(db, from, to, rel, { status, deletedAt = null, qu
|
|
|
416
417
|
// 更新记录跟着卡走(id 变了,按新 id 改绑)
|
|
417
418
|
db.prepare('UPDATE card_updates SET card_id = ? WHERE card_id = ?').run(newId, src.id)
|
|
418
419
|
db.prepare(`DELETE FROM ${from} WHERE path = ?`).run(rel)
|
|
420
|
+
// 索引跟随搬家:进主库→加;离开主库→删(隔离区不进召回面)
|
|
421
|
+
if (to === 'cards') indexCardById(db, newId)
|
|
422
|
+
if (from === 'cards') unindexCard(db, src.id)
|
|
419
423
|
return { ok: true, status, table: to, oldId: src.id, newId }
|
|
420
424
|
})
|
|
421
425
|
}
|
|
@@ -539,6 +543,7 @@ export async function deleteCard(root, rel, { permanent = false } = {}) {
|
|
|
539
543
|
db.prepare('DELETE FROM cards WHERE path = ?').run(rel)
|
|
540
544
|
db.prepare('DELETE FROM quarantine WHERE path = ?').run(rel)
|
|
541
545
|
})
|
|
546
|
+
for (const id of ids) unindexCard(db, id)
|
|
542
547
|
return { ok: true, permanent: true }
|
|
543
548
|
}
|
|
544
549
|
const existing = findCardRow(db, rel)
|
|
@@ -708,6 +713,7 @@ export async function writeCard(root, { kind, title, tags = [], body, source = '
|
|
|
708
713
|
id, rel, kind, title, JSON.stringify(tags), body.trim(), summary,
|
|
709
714
|
enforcedStatus, source, submittedBy, severity, reason, now, now, deletedAt || null,
|
|
710
715
|
)
|
|
716
|
+
indexCardById(db, id)
|
|
711
717
|
} else {
|
|
712
718
|
db.prepare(`
|
|
713
719
|
INSERT INTO quarantine (id, path, kind, title, tags, body, summary, status, source, submitted_by, severity, reason, created_at, updated_at, deleted_at, quarantined_at, quarantine_reason)
|
|
@@ -734,6 +740,7 @@ export async function appendUpdate(root, rel, updateText) {
|
|
|
734
740
|
const now = new Date().toISOString()
|
|
735
741
|
db.prepare('INSERT INTO card_updates (card_id, content, created_at) VALUES (?, ?, ?)').run(row.id, updateText.trim(), now)
|
|
736
742
|
db.prepare(`UPDATE ${found.table} SET updated_at = ? WHERE id = ?`).run(now, row.id)
|
|
743
|
+
if (found.table === 'cards') indexCardById(db, row.id)
|
|
737
744
|
return { ok: true, path: rel, updated: now, store: found.table }
|
|
738
745
|
}
|
|
739
746
|
|
|
@@ -758,6 +765,7 @@ export async function updateCardContent(root, rel, { title, tags, body, reason }
|
|
|
758
765
|
db.prepare(
|
|
759
766
|
`UPDATE ${found.table} SET title = ?, tags = ?, body = ?, summary = ?, reason = ?, updated_at = ? WHERE path = ?`,
|
|
760
767
|
).run(nextTitle, JSON.stringify(nextTags), nextBody, nextBody.slice(0, 200), nextReason, now, rel)
|
|
768
|
+
if (found.table === 'cards') indexCardById(db, row.id)
|
|
761
769
|
return { ok: true, path: rel, store: found.table, updated: now }
|
|
762
770
|
}
|
|
763
771
|
|
|
@@ -844,6 +852,7 @@ function applyRevision(db, revisionPath, rev, { changedBy = 'user', reason = ''
|
|
|
844
852
|
const tags = normalizeTags(revRow.tags)
|
|
845
853
|
db.prepare('UPDATE cards SET title = ?, tags = ?, body = ?, summary = ?, updated_at = ? WHERE path = ?')
|
|
846
854
|
.run(revRow.title, JSON.stringify(tags), revRow.body, String(revRow.body).trim().slice(0, 200), now, rev.target_path)
|
|
855
|
+
indexCardById(db, target.id)
|
|
847
856
|
db.prepare('INSERT INTO card_updates (card_id, content, created_at) VALUES (?, ?, ?)')
|
|
848
857
|
.run(target.id, '经审核修改:' + (reason || ('由 ' + (revRow.submitted_by || 'agent') + ' 提交,' + changedBy + ' 批准')), now)
|
|
849
858
|
db.prepare('DELETE FROM quarantine WHERE path = ?').run(revisionPath)
|
|
@@ -889,20 +898,43 @@ export async function search(root, query, { limit = 30, minScore = 0, semantic =
|
|
|
889
898
|
if (!q) return []
|
|
890
899
|
const db = getDb(root)
|
|
891
900
|
// 两套存储后这里**不再需要状态条件**:主库物理上只含 approved。
|
|
892
|
-
const rows = db.prepare('SELECT * FROM cards ORDER BY updated_at DESC').all()
|
|
893
901
|
const wanted = queryTerms(q)
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
+
let out = []
|
|
903
|
+
|
|
904
|
+
// ① 主路径:FTS5 + BM25(多列加权,自带 IDF 与长度归一)
|
|
905
|
+
// 旧实现是「全表扫描 + 子串计数」,没有 IDF —— 高频词和稀有词同权,
|
|
906
|
+
// 含一个「用户」的原始快照就能挤进前五(实测 precision@5 ≈ 12%)。
|
|
907
|
+
try {
|
|
908
|
+
const { searchIndexed } = await import('./search-index.js')
|
|
909
|
+
const hits = searchIndexed(db, q, { limit: Math.max(limit * 4, 80) })
|
|
910
|
+
if (hits.length) {
|
|
911
|
+
const scoreOf = new Map(hits.map((h) => [h.id, h.score]))
|
|
912
|
+
const ids = hits.map((h) => h.id)
|
|
913
|
+
const rows = db.prepare('SELECT * FROM cards WHERE id IN (' + ids.map(() => '?').join(',') + ')').all(...ids)
|
|
914
|
+
out = rows.map((row) => {
|
|
915
|
+
const card = rowToCard(row)
|
|
916
|
+
let score = scoreOf.get(row.id) || 0
|
|
917
|
+
if (semantic) score += Math.round(textSimilarity(q, `${card.title} ${card.summary}`) * 6)
|
|
918
|
+
return { ...card, score, excerpt: row.body.trim().slice(0, 300) }
|
|
919
|
+
}).filter((h) => h.score > 0 && h.score >= minScore)
|
|
902
920
|
}
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
921
|
+
} catch { out = [] } // 索引异常 → 静默降级到旧路径,绝不能因为索引坏了就搜不到
|
|
922
|
+
|
|
923
|
+
// ② 兜底路径:FTS 不可用 / 索引为空时保持旧行为
|
|
924
|
+
if (!out.length) {
|
|
925
|
+
const rows = db.prepare('SELECT * FROM cards ORDER BY updated_at DESC').all()
|
|
926
|
+
for (const row of rows) {
|
|
927
|
+
const card = rowToCard(row)
|
|
928
|
+
const haystack = `${card.path}\n${card.title}\n${row.body}`.toLowerCase()
|
|
929
|
+
let score = 0
|
|
930
|
+
if (haystack.includes(q.toLowerCase())) score = 3
|
|
931
|
+
for (const term of wanted) {
|
|
932
|
+
if (haystack.includes(term.toLowerCase())) score += 1
|
|
933
|
+
}
|
|
934
|
+
if (semantic) score += Math.round(textSimilarity(q, `${card.title} ${card.summary}`) * 6)
|
|
935
|
+
if (score > 0 && score >= minScore) {
|
|
936
|
+
out.push({ ...card, score, excerpt: row.body.trim().slice(0, 300) })
|
|
937
|
+
}
|
|
906
938
|
}
|
|
907
939
|
}
|
|
908
940
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"id": "memory-eternal",
|
|
4
4
|
"publisher": "eternalnight996",
|
|
5
5
|
"name": "memory-eternal",
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.24.0",
|
|
7
7
|
"description": "Memory Atlas for Orca: open the local memory vault, pending review and knowledge graph from a sandboxed Orca panel.",
|
|
8
8
|
"author": {
|
|
9
9
|
"name": "EternalNight996",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memory-eternal",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"description": "记忆核心(Memory Eternal):自研的 DeepSeek Harness 记忆插件,不移植任何既有记忆框架——对话结束后自动沉淀知识卡到本地 Markdown Vault(自研去重、自研 CJK 检索、可 git 管理),设置页提供图形化知识库(统计 / 搜索 / 知识图谱 + 侧边栏一键弹窗),Agent 通过 memory_recall 工具按需召回历史上下文。零人工干预。",
|
|
5
5
|
"icon": "assets/icon.png",
|
|
6
6
|
"license": "MIT",
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
"release": "node scripts/release.mjs",
|
|
71
71
|
"release:dry": "node scripts/release.mjs --dry-run",
|
|
72
72
|
"panel-check": "node scripts/panel-check.mjs",
|
|
73
|
-
"test": "node build.mjs && node tests/vault.test.mjs && node tests/capture.test.mjs && node tests/capture-route.test.mjs && node tests/stall.test.mjs && node tests/vault-path.test.mjs && node tests/vault-resolve.test.mjs && node tests/minhash.test.mjs && node tests/settings-save.test.mjs && node tests/config-coverage.test.mjs && node tests/config-sync.test.mjs && node tests/config-merge.test.mjs && node tests/sse.test.mjs && node tests/api.test.mjs && node tests/import-roundtrip.test.mjs && node tests/graph-perf.test.mjs && node tests/graph-force.test.mjs && node tests/settings-compat.test.mjs && node tests/node-bin.test.mjs && node tests/watchdog-lock.test.mjs && node tests/markdown.test.mjs && node tests/feedback.test.mjs && node tests/graph-lod.test.mjs && node tests/graph-warmup.test.mjs && node tests/loading-hints.test.mjs && node tests/web-entry.test.mjs && node tests/tui-scroll.test.mjs && node tests/tui-audit-scene.test.mjs && node tests/tui-plugin-boot.test.mjs && node tests/capture-session-id.test.mjs && node tests/ssr.test.mjs && node tests/window-controls.test.mjs && node tests/web-assets.test.mjs && node tests/manifests.test.mjs && node tests/two-store.test.mjs && node tests/noise-gate.test.mjs && node tests/e2e-recall.test.mjs && node tests/issue-fixes.test.mjs && node tests/issue-fixes-2.test.mjs && node tests/issue-fixes-3.test.mjs && node tests/watchdog-orphans.test.mjs && node tests/watchdog-restart.test.mjs && node tests/readme-i18n.test.mjs && node tests/issue-fixes-4.test.mjs && node tests/host-drain.test.mjs && node tests/e2e-host-drain.mjs && node tests/issue-fixes-5.test.mjs && node tests/issue-fixes-6.test.mjs && node tests/capture-json-repair.test.mjs && node tests/csrf-origin.test.mjs && node tests/watchdog-orphan-web.test.mjs && node tests/config-validate.test.mjs && node tests/issue-fixes-8.test.mjs && node tests/issue-fixes-9.test.mjs && node tests/release-verify.test.mjs && node tests/refine.test.mjs && node tests/db-restore.test.mjs && node tests/align.test.mjs && node tests/tags-normalize.test.mjs && node tests/resident-state.test.mjs && node tests/llm-route.test.mjs && node tests/host-managed.test.mjs && node tests/replace-claim.test.mjs && node tests/version-check-self.test.mjs && node tests/setup-codex.test.mjs && node tests/listener-netstat.test.mjs && node tests/issue-46-overlay.test.mjs && node tests/graph-initial-zoom.test.mjs && node tests/card-head-actions.test.mjs && node tests/card-reader-ux.test.mjs && node tests/tag-filter.test.mjs && node tests/ui-overflow.test.mjs && node tests/version-card-state.test.mjs && node tests/llm-routes-proxy.test.mjs && node tests/model-strikes.test.mjs && node tests/support-bundle.test.mjs && node tests/health-panel.test.mjs && node tests/issue-48-timeout.test.mjs && node tests/setup-node-path.test.mjs && node tests/capture-kind.test.mjs && node tests/core-memory.test.mjs && node tests/skill-cards.test.mjs && node tests/audit-search.test.mjs && node tests/news.test.mjs && node tests/news-feed.test.mjs && node tests/ledger.test.mjs && node tests/issue-55-timezone.test.mjs && node tests/recycle-center.test.mjs && node tests/news-follows.test.mjs && node tests/news-digest.test.mjs"
|
|
73
|
+
"test": "node build.mjs && node tests/vault.test.mjs && node tests/capture.test.mjs && node tests/capture-route.test.mjs && node tests/stall.test.mjs && node tests/vault-path.test.mjs && node tests/vault-resolve.test.mjs && node tests/minhash.test.mjs && node tests/settings-save.test.mjs && node tests/config-coverage.test.mjs && node tests/config-sync.test.mjs && node tests/config-merge.test.mjs && node tests/sse.test.mjs && node tests/api.test.mjs && node tests/import-roundtrip.test.mjs && node tests/graph-perf.test.mjs && node tests/graph-force.test.mjs && node tests/settings-compat.test.mjs && node tests/node-bin.test.mjs && node tests/watchdog-lock.test.mjs && node tests/markdown.test.mjs && node tests/feedback.test.mjs && node tests/graph-lod.test.mjs && node tests/graph-warmup.test.mjs && node tests/loading-hints.test.mjs && node tests/web-entry.test.mjs && node tests/tui-scroll.test.mjs && node tests/tui-audit-scene.test.mjs && node tests/tui-plugin-boot.test.mjs && node tests/capture-session-id.test.mjs && node tests/ssr.test.mjs && node tests/window-controls.test.mjs && node tests/web-assets.test.mjs && node tests/manifests.test.mjs && node tests/two-store.test.mjs && node tests/noise-gate.test.mjs && node tests/e2e-recall.test.mjs && node tests/issue-fixes.test.mjs && node tests/issue-fixes-2.test.mjs && node tests/issue-fixes-3.test.mjs && node tests/watchdog-orphans.test.mjs && node tests/watchdog-restart.test.mjs && node tests/readme-i18n.test.mjs && node tests/issue-fixes-4.test.mjs && node tests/host-drain.test.mjs && node tests/e2e-host-drain.mjs && node tests/issue-fixes-5.test.mjs && node tests/issue-fixes-6.test.mjs && node tests/capture-json-repair.test.mjs && node tests/csrf-origin.test.mjs && node tests/watchdog-orphan-web.test.mjs && node tests/config-validate.test.mjs && node tests/issue-fixes-8.test.mjs && node tests/issue-fixes-9.test.mjs && node tests/release-verify.test.mjs && node tests/refine.test.mjs && node tests/db-restore.test.mjs && node tests/align.test.mjs && node tests/tags-normalize.test.mjs && node tests/resident-state.test.mjs && node tests/llm-route.test.mjs && node tests/host-managed.test.mjs && node tests/replace-claim.test.mjs && node tests/version-check-self.test.mjs && node tests/setup-codex.test.mjs && node tests/listener-netstat.test.mjs && node tests/issue-46-overlay.test.mjs && node tests/graph-initial-zoom.test.mjs && node tests/card-head-actions.test.mjs && node tests/card-reader-ux.test.mjs && node tests/tag-filter.test.mjs && node tests/ui-overflow.test.mjs && node tests/version-card-state.test.mjs && node tests/llm-routes-proxy.test.mjs && node tests/model-strikes.test.mjs && node tests/support-bundle.test.mjs && node tests/health-panel.test.mjs && node tests/issue-48-timeout.test.mjs && node tests/setup-node-path.test.mjs && node tests/capture-kind.test.mjs && node tests/core-memory.test.mjs && node tests/skill-cards.test.mjs && node tests/audit-search.test.mjs && node tests/news.test.mjs && node tests/news-feed.test.mjs && node tests/ledger.test.mjs && node tests/issue-55-timezone.test.mjs && node tests/recycle-center.test.mjs && node tests/news-follows.test.mjs && node tests/news-digest.test.mjs && node tests/search-index.test.mjs"
|
|
74
74
|
},
|
|
75
75
|
"dependencies": {
|
|
76
76
|
"@deepseek-ai/schemastery": "^3.18.1"
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// 记忆核心 · 检索延迟基准(用户可自行复跑,不接受"我们很快"这种空话)
|
|
3
|
+
//
|
|
4
|
+
// 用法:
|
|
5
|
+
// node scripts/bench-search.mjs # 默认 ~/.dsh/memory-vault
|
|
6
|
+
// node scripts/bench-search.mjs --vault <DIR> # 指定库
|
|
7
|
+
// node scripts/bench-search.mjs --rounds 5 # 每个查询跑几轮(默认 5)
|
|
8
|
+
// node scripts/bench-search.mjs --rebuild # 先全量重建索引,报出重建耗时
|
|
9
|
+
//
|
|
10
|
+
// 输出:卡片数、索引条数、p50 / p95 / max 延迟、QPS 估算。
|
|
11
|
+
// 为什么要有它:README 里的性能数字必须能被人现场复核,否则就是营销。
|
|
12
|
+
import os from 'node:os'
|
|
13
|
+
import path from 'node:path'
|
|
14
|
+
import fs from 'node:fs'
|
|
15
|
+
|
|
16
|
+
const args = process.argv.slice(2)
|
|
17
|
+
const opt = (name, def = '') => { const i = args.indexOf('--' + name); return i >= 0 ? (args[i + 1] ?? def) : def }
|
|
18
|
+
const flag = (name) => args.includes('--' + name)
|
|
19
|
+
|
|
20
|
+
const vault = opt('vault') || process.env.MEMORY_VAULT_DIR
|
|
21
|
+
|| path.join(process.env.DSH_HOME || path.join(os.homedir(), '.dsh'), 'memory-vault')
|
|
22
|
+
const rounds = Math.max(1, Number(opt('rounds', '5')) || 5)
|
|
23
|
+
|
|
24
|
+
if (!fs.existsSync(vault)) {
|
|
25
|
+
console.error('找不到记忆库:' + vault + '(用 --vault 指定)')
|
|
26
|
+
process.exit(1)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const { search } = await import('../lib/vault.js')
|
|
30
|
+
const { reindexAll, indexStats } = await import('../lib/search-index.js')
|
|
31
|
+
const { getDb } = await import('../lib/db.js')
|
|
32
|
+
|
|
33
|
+
const db = getDb(vault)
|
|
34
|
+
if (flag('rebuild')) {
|
|
35
|
+
const t0 = Date.now()
|
|
36
|
+
reindexAll(db)
|
|
37
|
+
console.log('全量重建索引:' + (Date.now() - t0) + 'ms')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const st = indexStats(db)
|
|
41
|
+
// 固定查询集:混合中英、长词、短词、专有名词,避免只挑"快的那几个"
|
|
42
|
+
const queries = [
|
|
43
|
+
'检索 索引', '审核 红线', '回收站 清空', '作者语录', '新闻 关注', '发布 版本',
|
|
44
|
+
'数据库 选型', '记忆 架构', '插件 安装', '异常 排查', 'token 成本', '知识图谱 性能',
|
|
45
|
+
'审核 中心', '技能卡', 'FTS5 索引', '缓存 一致性', '配置 同步', '新闻 源', '摘要 推送', 'GitHub 热门',
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
const samples = []
|
|
49
|
+
for (const q of queries) {
|
|
50
|
+
for (let i = 0; i < rounds; i++) {
|
|
51
|
+
const t = process.hrtime.bigint()
|
|
52
|
+
await search(vault, q, { limit: 5 })
|
|
53
|
+
samples.push(Number(process.hrtime.bigint() - t) / 1e6)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
samples.sort((a, b) => a - b)
|
|
57
|
+
const at = (p) => samples[Math.min(samples.length - 1, Math.floor(samples.length * p))]
|
|
58
|
+
const total = samples.reduce((a, b) => a + b, 0)
|
|
59
|
+
|
|
60
|
+
console.log('库路径 : ' + vault)
|
|
61
|
+
console.log('卡片 / 索引 : ' + st.cards + ' / ' + st.indexed)
|
|
62
|
+
console.log('查询 × 轮次 : ' + queries.length + ' × ' + rounds + ' = ' + samples.length + ' 次')
|
|
63
|
+
console.log('p50 / p95 / max: ' + at(0.5).toFixed(2) + 'ms / ' + at(0.95).toFixed(2) + 'ms / ' + samples[samples.length - 1].toFixed(2) + 'ms')
|
|
64
|
+
console.log('平均 / QPS : ' + (total / samples.length).toFixed(2) + 'ms / ' + Math.round(1000 / (total / samples.length)) + ' 次/秒')
|
package/web/app.js
CHANGED
|
@@ -1017,7 +1017,7 @@ Option A, because:
|
|
|
1017
1017
|
.mc-audit-batchcard > .mc-btn { width: 100%; }
|
|
1018
1018
|
.mc-audit-batchcard > div[style*="flex: 1"] { display: none; }
|
|
1019
1019
|
}
|
|
1020
|
-
`;var za="0.
|
|
1020
|
+
`;var za="0.24.0";function Xy({t:e}){let[t,n]=(0,m.useState)(""),a="dsh-memory tui",o="dsh-memory tui --write",i=async(p,u)=>{try{await navigator.clipboard.writeText(p),n(u),setTimeout(()=>n(""),1600)}catch{}},l={border:"1px solid #3a404a",borderRadius:14,background:"#22262d",padding:"16px 18px"},s={margin:"10px 0 0",padding:"12px 14px",borderRadius:10,background:"#171a1f",border:"1px solid #333943",color:"#f3f5f8",fontSize:12.5,lineHeight:1.7,overflowX:"auto",fontFamily:"ui-monospace, Consolas, monospace"},c={marginTop:10,border:"1px solid #4a515c",background:"transparent",color:"#ffb08a",borderRadius:9,padding:"7px 12px",fontSize:12,cursor:"pointer"};return(0,r.jsxs)("div",{style:{padding:"22px 26px 34px",maxWidth:1040,color:"#f3f5f8"},children:[(0,r.jsxs)("div",{style:{display:"flex",alignItems:"baseline",gap:12,flexWrap:"wrap"},children:[(0,r.jsx)("h2",{style:{margin:0,fontSize:24,letterSpacing:"-.02em"},children:e("terminalTitle")}),(0,r.jsx)("span",{style:{color:"#99a1ac",fontSize:12},children:"TUI \xB7 LIVE \xB7 dsh-tui"})]}),(0,r.jsx)("p",{style:{margin:"9px 0 20px",color:"#aeb5bf",fontSize:13.5,lineHeight:1.8},children:e("terminalIntro")}),(0,r.jsxs)("div",{style:{display:"grid",gridTemplateColumns:"minmax(0, 1.1fr) minmax(0, .9fr)",gap:14},children:[(0,r.jsxs)("div",{style:l,children:[(0,r.jsxs)("div",{style:{fontSize:13,fontWeight:700},children:["\u25B8 ",e("terminalRun")]}),(0,r.jsx)("pre",{style:s,children:a}),(0,r.jsx)("button",{type:"button",style:c,onClick:()=>i(a,"run"),children:t==="run"?"\u2713 "+e("terminalCopied"):"\u29C9 "+e("terminalCopy")}),(0,r.jsxs)("div",{style:{marginTop:12,color:"#99a1ac",fontSize:12,lineHeight:1.7},children:["\u9ED8\u8BA4\u8BFB\u53D6\u771F\u5B9E\u8BB0\u5FC6\u5E93\u4E0E\u5F85\u5BA1\u961F\u5217\uFF1B\u53EA\u8BFB\uFF0C\u786E\u8BA4\u540E\u624D\u7528 `--write`\u3002\u793A\u4F8B\u6570\u636E\u7528 `--demo`\u3002",(0,r.jsx)("br",{}),"\u6CA1\u6709 `dsh-memory` \u547D\u4EE4\u65F6\uFF1A`npx -p memory-eternal dsh-memory tui`"]})]}),(0,r.jsxs)("div",{style:l,children:[(0,r.jsxs)("div",{style:{fontSize:13,fontWeight:700},children:["\u25B8 ",e("terminalTui")]}),(0,r.jsx)("pre",{style:s,children:o}),(0,r.jsx)("button",{type:"button",style:c,onClick:()=>i(o,"tui"),children:t==="tui"?"\u2713 "+e("terminalCopied"):"\u29C9 "+e("terminalCopy")}),(0,r.jsx)("div",{style:{marginTop:12,color:"#99a1ac",fontSize:12,lineHeight:1.7},children:"\u771F\u5B9E\u6279\u51C6/\u9A73\u56DE\uFF1B\u4E0D\u786E\u5B9A\u65F6\u5148\u7528\u53EA\u8BFB\u6A21\u5F0F\u3002"})]})]}),(0,r.jsxs)("div",{style:{...l,marginTop:14},children:[(0,r.jsxs)("div",{style:{fontSize:13,fontWeight:700},children:["\u25B8 ",e("terminalPreview")]}),(0,r.jsx)("pre",{style:{...s,marginTop:10,color:"#d7dce3",fontSize:12,lineHeight:1.5},children:`\u256D\u2500 MEMORY ATLAS / \u5F85\u5BA1 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E
|
|
1021
1021
|
\u2502 \u7C7B\u578B \u25CF \u5168\u90E8 7 \u25CB \u9879\u76EE 3 \u25CB \u77E5\u8BC6 1 \u25CB \u6559\u8BAD 1 \u2502
|
|
1022
1022
|
\u2502 \u2502
|
|
1023
1023
|
\u2502 \u25A3 \u5168\u9009 7 \u5DF2\u9009 3 s \u5168\u9009 \u2502
|