memory-eternal 0.16.0 → 0.17.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 +138 -656
- package/README.zh-CN.md +138 -0
- package/bin/dsh-memory.mjs +33 -3
- 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/index.js +26 -3
- package/lib/api.js +9 -0
- package/lib/capture-run.js +35 -8
- package/lib/client.js +17 -17
- package/lib/db.js +19 -0
- package/lib/ledger.js +107 -0
- package/lib/mcp.js +8 -2
- package/orca-plugin/orca-plugin.json +1 -1
- package/package.json +145 -145
- package/scripts/release.mjs +5 -4
- package/web/app.js +15 -15
- package/README.en.md +0 -623
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# 🧠 memory-eternal —— 给 AI 装一个可信的第二大脑
|
|
2
|
+
|
|
3
|
+
[](https://github.com/EternalNight996/memory-eternal/stargazers)
|
|
4
|
+
[](https://www.npmjs.com/package/memory-eternal)
|
|
5
|
+
[](https://github.com/EternalNight996/memory-eternal/releases)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
[English (default)](README.md) · **简体中文(镜像)**
|
|
9
|
+
|
|
10
|
+
<p align="center">
|
|
11
|
+
<a href="https://eternalnight996.github.io/memory-eternal/demo/"><img src="https://raw.githubusercontent.com/EternalNight996/memory-eternal/main/assets/screen/demo.gif" width="900" alt="memory-eternal 20 秒演示:审核队列、记忆账本、知识星图" /></a>
|
|
12
|
+
</p>
|
|
13
|
+
<p align="center"><sub>▲ 20 秒演示 —— 审核队列 · 记忆账本 · 知识星图 · <a href="https://eternalnight996.github.io/memory-eternal/demo/">在线 Demo</a></sub></p>
|
|
14
|
+
|
|
15
|
+
> **你的 AI 每次会话都失忆。给它一个可信的记忆库。**
|
|
16
|
+
>
|
|
17
|
+
> 本地优先、先审后入、可度量 —— 一个 SQLite 库,DSH / Claude Code / Codex / Cursor 与任意 MCP 客户端共享。
|
|
18
|
+
|
|
19
|
+
- 🔍 **先审核,再入库** —— 新卡先进待审队列,Web 或终端里批准/驳回;每次状态变更写入不可篡改的审计日志。
|
|
20
|
+
- 🧾 **记忆账本** —— 召回注入了多少、蒸馏调了几次、召回到底有没有用,全部可查。
|
|
21
|
+
- 🏠 **本地优先,零云端** —— SQLite + Markdown 存在你自己的磁盘上。没有第三方记忆框架、不需要账号、不外发请求。
|
|
22
|
+
|
|
23
|
+
<p align="center"><strong>⭐ 如果它帮你少解释了一遍上下文,点个 Star 就是最大的支持</strong><br/><sub>DSH 一条命令:<code>dsh plugin --profile desktop add memory-eternal</code></sub></p>
|
|
24
|
+
|
|
25
|
+
## ⚡ 快速开始
|
|
26
|
+
|
|
27
|
+
**DeepSeek Harness(桌面版 / Web)**
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
dsh plugin --profile desktop add memory-eternal
|
|
31
|
+
# 重开 DSH → 左栏「记忆」
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**Claude Code、Codex 或 Cursor —— 通过 MCP 共享同一个库**
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm i -g memory-eternal
|
|
38
|
+
dsh-memory connect claude # 或:codex / cursor
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**任意 MCP 客户端**
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npx -y -p memory-eternal dsh-memory mcp
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**浏览器或终端**
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
dsh-memory open # http://127.0.0.1:7999
|
|
51
|
+
npx -y -p memory-eternal dsh-memory tui # 终端里的实时待审队列
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## 🧾 真的省 token 吗?
|
|
55
|
+
|
|
56
|
+
大多数记忆产品都说自己省 token。我们认为你应该自己验证。插件内置**记忆账本**(运行状态 → 记忆账本),记录:
|
|
57
|
+
|
|
58
|
+
| 记录项 | 含义 |
|
|
59
|
+
|---|---|
|
|
60
|
+
| 召回:命中数 + 注入字符 | 到底有多少记忆进入了上下文 |
|
|
61
|
+
| 沉淀:LLM 调用次数 + 输入/输出字符 | 蒸馏花了你多少成本 |
|
|
62
|
+
| 有用率:有用 / 没用 反馈 | 召回的记忆是否真的帮上了忙 |
|
|
63
|
+
|
|
64
|
+
诚实的算式:
|
|
65
|
+
|
|
66
|
+
```text
|
|
67
|
+
净收益 ≈ 被避免的上下文开销 −(注入字符 + 蒸馏调用 × 单价)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
| 场景 | 你要付 | 你能省 |
|
|
71
|
+
|---|---|---|
|
|
72
|
+
| 有决策与踩坑记录的多会话项目 | 每次召回约 0.6–0.9k token | 重新解释上下文(约 1.5–3k)或重读文件(3–6k) |
|
|
73
|
+
| 一次性问题、低命中率 | 召回注入 | 通常什么也省不到 |
|
|
74
|
+
|
|
75
|
+
> 以上是示意数字,不是 benchmark —— 用账本测你自己的。如果召回有用率长期低于约 60%,就调小(更少条数、更短摘要)或直接关掉记忆。
|
|
76
|
+
|
|
77
|
+
## 🛡️ 先审核,再入库
|
|
78
|
+
|
|
79
|
+
一个「什么都自动吞进去」的记忆系统,迟早会把错误事实当成真相召回。memory-eternal 默认**先审后入**:
|
|
80
|
+
|
|
81
|
+
- 新卡先进入**待审队列**,人工批准后才进主库;
|
|
82
|
+
- 在 Web、终端 TUI 或 CLI 里批准 / 驳回 / 批量处理;
|
|
83
|
+
- 每次状态变更追加到**不可篡改的审计日志**(谁 / 什么 / 何时 / 为什么)—— 由 SQLite 层强制,而不是靠约定;
|
|
84
|
+
- Agent 可以读取和提议,但**不能批准自己写的卡**;
|
|
85
|
+
- 删除的卡进回收站(默认保留 30 天)。
|
|
86
|
+
|
|
87
|
+
## 🧩 一个库,所有 Agent
|
|
88
|
+
|
|
89
|
+
| 入口 | 你能得到什么 |
|
|
90
|
+
|---|---|
|
|
91
|
+
| **DSH 桌面版 / Web** | 内嵌记忆界面:审核、图谱、配置、账本 |
|
|
92
|
+
| **Claude Code / Codex / Cursor** | MCP server,同一个本地库,<code>dsh-memory connect</code> |
|
|
93
|
+
| **任意 MCP 客户端** | <code>npx -y -p memory-eternal dsh-memory mcp</code> |
|
|
94
|
+
| **终端** | <code>dsh-memory tui</code> —— 实时队列、全选、批量批/驳、长文读卡 |
|
|
95
|
+
| **浏览器** | <code>dsh-memory open</code> → http://127.0.0.1:7999 |
|
|
96
|
+
|
|
97
|
+
## 📸 截图
|
|
98
|
+
|
|
99
|
+
<p align="center">
|
|
100
|
+
<img src="https://raw.githubusercontent.com/EternalNight996/memory-eternal/main/assets/screen/graph.png" width="32%" alt="知识星图:按类型分簇、标签连边" />
|
|
101
|
+
<img src="https://raw.githubusercontent.com/EternalNight996/memory-eternal/main/assets/screen/audit.png" width="32%" alt="审核中心:类型条、队列与判词栏" />
|
|
102
|
+
<img src="https://raw.githubusercontent.com/EternalNight996/memory-eternal/main/assets/screen/status.png" width="32%" alt="运行状态:库、备份、自动沉淀与版本" />
|
|
103
|
+
</p>
|
|
104
|
+
<p align="center"><sub>▲ 知识星图 · 审核中心 · 运行状态</sub></p>
|
|
105
|
+
|
|
106
|
+
<p align="center">
|
|
107
|
+
<img src="https://raw.githubusercontent.com/EternalNight996/memory-eternal/main/assets/screen/tui-audit.png" width="49%" alt="终端原生审核:全选、批量批准/驳回" />
|
|
108
|
+
<img src="https://raw.githubusercontent.com/EternalNight996/memory-eternal/main/assets/screen/tui-reader.png" width="49%" alt="终端原生长文读卡器" />
|
|
109
|
+
</p>
|
|
110
|
+
<p align="center"><sub>▲ 终端原生审核与长文读卡 —— <code>dsh-memory tui</code></sub></p>
|
|
111
|
+
|
|
112
|
+
## 📚 文档与链接
|
|
113
|
+
|
|
114
|
+
- [英文完整手册](docs/GUIDE.md) · [完整手册(中文)](docs/GUIDE.zh-CN.md)
|
|
115
|
+
- [私有化部署(Docker / systemd)](docs/DEPLOY.md)
|
|
116
|
+
- [更新日志](docs/CHANGELOG.md) · [架构](docs/ARCHITECTURE.md)
|
|
117
|
+
- [终端插件(dsh-tui)](dsh-tui-plugin/README.md) · [Orca 启动器](orca-plugin/README.md) · [Cursor / VS Code 扩展](cursor-extension/README.md)
|
|
118
|
+
- [反馈问题](https://github.com/EternalNight996/memory-eternal/issues)
|
|
119
|
+
|
|
120
|
+
## 💬 交流群
|
|
121
|
+
|
|
122
|
+
**遇到问题、想提需求、或者想聊聊 DSH 插件开发 —— 欢迎进群:**
|
|
123
|
+
|
|
124
|
+
| QQ 交流群 | 微信交流群 |
|
|
125
|
+
|:---:|:---:|
|
|
126
|
+
| <img src="https://raw.githubusercontent.com/EternalNight996/memory-eternal/main/assets/support/group-qr.jpg" width="240" alt="QQ 群二维码" /> | <img src="https://raw.githubusercontent.com/EternalNight996/memory-eternal/main/assets/support/group-wechat.png" width="240" alt="微信群二维码" /> |
|
|
127
|
+
|
|
128
|
+
**如果这个插件帮你省了时间,可以请作者喝杯咖啡(两个码任选其一):**
|
|
129
|
+
|
|
130
|
+
| 微信收款码 | 微信赞赏码 |
|
|
131
|
+
|:---:|:---:|
|
|
132
|
+
| <img src="https://raw.githubusercontent.com/EternalNight996/memory-eternal/main/assets/support/wechat-pay.jpg" width="200" alt="微信收款码" /> | <img src="https://raw.githubusercontent.com/EternalNight996/memory-eternal/main/assets/support/wechat-reward.jpg" width="200" alt="微信赞赏码" /> |
|
|
133
|
+
|
|
134
|
+
> 打赏完全自愿,不影响任何功能;插件本体始终开源免费(MIT)。详见 [SPONSOR.md](SPONSOR.md)。
|
|
135
|
+
|
|
136
|
+
## 📄 License
|
|
137
|
+
|
|
138
|
+
MIT —— 随便用、随便自托管、随便二次开发。
|
package/bin/dsh-memory.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// 用法:
|
|
5
5
|
// dsh-memory recall <query> [--limit N] [--vault DIR] 检索知识卡
|
|
6
6
|
// dsh-memory capture <text | - > [--source TAG] 手动沉淀(- 读 stdin)
|
|
7
|
-
// dsh-memory serve [--port N] [--vault DIR]
|
|
7
|
+
// dsh-memory serve [--port N] [--host H] [--vault DIR] 启动 Web UI(前台常驻;--host 0.0.0.0 用于容器/内网,需自行加认证)
|
|
8
8
|
// dsh-memory open [--port N] 确保 Web 存活并用浏览器打开
|
|
9
9
|
// dsh-memory tui [--live] [--write] 打开完整终端客户端(无需绝对路径)
|
|
10
10
|
// dsh-memory mcp MCP stdio server(各 agent 挂载)
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
// dsh-memory refine [--vault DIR] [--json] 矛盾扫描候选(不调模型);--apply --a A --b B --action keepA|keepB|merge|keepBoth 落裁决
|
|
19
19
|
// dsh-memory refine --junk [--purge --path P] [--json] 注入垃圾卡(技能目录等注入文本沉淀成的卡):列清单 / 送回收站
|
|
20
20
|
// dsh-memory refine --goals [--keep --path A --path B] 同目标不同做法:聚类需模型(面板里跑);--keep 落"都保留"
|
|
21
|
+
// dsh-memory ledger [--days N] [--json] 记忆账本:召回注入 / 蒸馏调用 / 召回有用率
|
|
21
22
|
//
|
|
22
23
|
// 环境变量:
|
|
23
24
|
// MEMORY_VAULT_DIR vault 目录(默认 ~/.dsh/memory-vault)
|
|
@@ -105,8 +106,10 @@ async function main() {
|
|
|
105
106
|
const { startWebServer, DEFAULT_WEB_PORT } = await import('../lib/web.js')
|
|
106
107
|
const { defaultVaultDir } = await import('../lib/capture-run.js')
|
|
107
108
|
const port = Number(argOf('--port')) || DEFAULT_WEB_PORT
|
|
109
|
+
// 默认只监听本机:私有化/容器部署时显式 --host 0.0.0.0,并在前面加 TLS + 认证的反向代理
|
|
110
|
+
const host = argOf('--host') || process.env.MEMORY_WEB_HOST || '127.0.0.1'
|
|
108
111
|
const vault = path.resolve(argOf('--vault') || defaultVaultDir())
|
|
109
|
-
await startWebServer({ port, vaultRoot: vault })
|
|
112
|
+
await startWebServer({ port, vaultRoot: vault, host })
|
|
110
113
|
process.on('SIGINT', () => process.exit(0))
|
|
111
114
|
process.on('SIGTERM', () => process.exit(0))
|
|
112
115
|
return
|
|
@@ -148,6 +151,32 @@ async function main() {
|
|
|
148
151
|
startMcpServer()
|
|
149
152
|
return
|
|
150
153
|
}
|
|
154
|
+
case 'ledger': {
|
|
155
|
+
const { ledgerSummary } = await import('../lib/ledger.js')
|
|
156
|
+
const { defaultVaultDir } = await import('../lib/capture-run.js')
|
|
157
|
+
const root = path.resolve(argOf('--vault') || defaultVaultDir())
|
|
158
|
+
const days = Math.min(Math.max(Math.floor(Number(argOf('--days')) || 7), 1), 90)
|
|
159
|
+
let s
|
|
160
|
+
try { s = ledgerSummary(root, { days }) } catch (e) {
|
|
161
|
+
console.error('读取记忆账本失败:' + (e?.message || e))
|
|
162
|
+
process.exitCode = 1
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
if (has('--json')) { console.log(JSON.stringify(s, null, 2)); return }
|
|
166
|
+
const fmt = (n) => Number(n || 0).toLocaleString('en-US')
|
|
167
|
+
console.log('记忆账本(最近 ' + s.days + ' 天)\n')
|
|
168
|
+
console.log('召回 ' + fmt(s.totals.recalls) + ' 次 · 注入 ' + fmt(s.totals.injectedChars) + ' 字符')
|
|
169
|
+
console.log('沉淀 ' + fmt(s.totals.captures) + ' 次 · LLM ' + fmt(s.totals.llmCalls) + ' 次 · 输入 ' + fmt(s.totals.captureInChars) + ' / 输出 ' + fmt(s.totals.captureOutChars) + ' 字符')
|
|
170
|
+
console.log('估算 ≈ ' + fmt(s.totals.estTokens) + ' tokens(中英混排按 2 字符 ≈ 1 token,仅看量级)')
|
|
171
|
+
console.log('')
|
|
172
|
+
console.log(s.feedback.rate === null
|
|
173
|
+
? '召回有用率 暂无反馈(Agent 回传 useful 后才有数据)'
|
|
174
|
+
: '召回有用率 ' + Math.round(s.feedback.rate * 100) + '%(有用 ' + fmt(s.feedback.useful) + ' / 无用 ' + fmt(s.feedback.useless) + ')')
|
|
175
|
+
console.log('\n最近 ' + s.days + ' 天:')
|
|
176
|
+
for (const d of s.byDay) console.log(' ' + d.date + ' 召回 ' + fmt(d.recalls) + ' · 注入 ' + fmt(d.injectedChars) + ' 字符 · 沉淀 ' + fmt(d.captures))
|
|
177
|
+
console.log('\n提示:--json 输出原始数据;Web 面板「运行状态 → 记忆账本」可看趋势。')
|
|
178
|
+
return
|
|
179
|
+
}
|
|
151
180
|
case 'status': {
|
|
152
181
|
const { watchdogStatus, probeServedVersion, findPortListener, looksLikeOurWeb } = await import('../lib/watchdog.js')
|
|
153
182
|
const port = argOf('--port')
|
|
@@ -590,7 +619,7 @@ MCP 侧只提供只读的 memory_audit_list —— 批准/驳回必须由人显
|
|
|
590
619
|
用法:
|
|
591
620
|
dsh-memory recall <query> [--limit N] 检索知识卡
|
|
592
621
|
dsh-memory capture <text | -> [--source T] 手动沉淀(- 读 stdin)
|
|
593
|
-
dsh-memory serve [--port N]
|
|
622
|
+
dsh-memory serve [--port N] [--host H] Web UI(前台;私有化部署见 docs/DEPLOY.md)
|
|
594
623
|
dsh-memory open 确保 Web 存活并打开浏览器
|
|
595
624
|
dsh-memory mcp MCP stdio server
|
|
596
625
|
dsh-memory setup [--dry-run] ... 自动挂载 MCP 到已装 agent
|
|
@@ -601,6 +630,7 @@ MCP 侧只提供只读的 memory_audit_list —— 批准/驳回必须由人显
|
|
|
601
630
|
dsh-memory status [--port N] [--json] 查看常驻看门狗(pid/端口/版本/存活)
|
|
602
631
|
dsh-memory stop [--port N] [--json] 停止常驻看门狗(显式命令,配置变更不会自动停)
|
|
603
632
|
dsh-memory restart [--port N] [--vault DIR] 停止并重新拉起看门狗(应用新版本)
|
|
633
|
+
dsh-memory ledger [--days N] [--json] 记忆账本:召回注入 / 蒸馏调用 / 有用率
|
|
604
634
|
dsh-memory audit list [--status pending] [--kind K] [--json] 列出待审/驳回卡片
|
|
605
635
|
dsh-memory audit approve <path...> | reject <path...> [--reason "原因"] 人工批量审批
|
|
606
636
|
|
|
@@ -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.17.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.17.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/index.js
CHANGED
|
@@ -34,6 +34,7 @@ import { createHub } from './lib/sse.js'
|
|
|
34
34
|
import { summarizeTurn, summarizeTurnDetailed, routeCandidates, sessionRouteFrom, extractLastTurn, sliceNewEvents, sessionEvents, sessionEventApi, createCaptureHealth, resolveRoute, captureCard, captureUpdate, pickNeighbors, hasUsableContent, deriveTitle, looksTruncated, maxTokenLadder, DEFAULT_CAPTURE_MAX_TOKENS, MAX_CAPTURE_MAX_TOKENS } from './lib/capture.js'
|
|
35
35
|
import { createApi, json, encodeBody, crossOriginError, emptyNumericFields } from './lib/api.js'
|
|
36
36
|
import { appendCaptureLog, readCaptureLog, rotateCaptureLog } from './lib/capture-log.js'
|
|
37
|
+
import { recordLedger } from './lib/ledger.js'
|
|
37
38
|
import { missingWebAssets, missingAssetsReason } from './lib/web-assets.js'
|
|
38
39
|
import { writeHostMarker, clearHostMarker } from './lib/host-heartbeat.js'
|
|
39
40
|
import { nodeBinary, childEnv } from './lib/node-bin.js'
|
|
@@ -737,6 +738,8 @@ export function apply(ctx, config) {
|
|
|
737
738
|
let failure = null // 主失败 = **第一个**候选的失败(通常是显式配置的那个,信息量最大)
|
|
738
739
|
const failures = [] // 全部候选的失败,用于汇总上报(#18 根因 2)
|
|
739
740
|
const maxTokens = Number(cfg.captureMaxTokens) || DEFAULT_CAPTURE_MAX_TOKENS
|
|
741
|
+
const captureT0 = Date.now()
|
|
742
|
+
let llmCalls = 0
|
|
740
743
|
for (let i = 0; i < routes.length; i++) {
|
|
741
744
|
route = routes[i]
|
|
742
745
|
// issue #48:超时不再硬编码 45s(本地 30B 冷启动 40–60s 必超时),且超时后**同候选**用更长超时重试一次。
|
|
@@ -744,7 +747,7 @@ export function apply(ctx, config) {
|
|
|
744
747
|
const isTimeoutFailure = (x) => /ABORTED|TIMEOUT|ETIMEDOUT|aborted/i.test(String((x && (x.code + ' ' + x.message)) || ''))
|
|
745
748
|
// issue #51:把真实会话 id 透传给 LLM(opencode-go 网关靠它注入 x-opencode-session,
|
|
746
749
|
// 缺了首跳固定 400 MissingSessionID)。同一会话的蒸馏请求也因此能被网关按会话归并。
|
|
747
|
-
const attempt = (budget, ms) => summarizeTurnDetailed(llm, route, text, { signal: AbortSignal.timeout(ms), existing: neighbors, maxTokens: budget, sessionId })
|
|
750
|
+
const attempt = (budget, ms) => { llmCalls++; return summarizeTurnDetailed(llm, route, text, { signal: AbortSignal.timeout(ms), existing: neighbors, maxTokens: budget, sessionId }) }
|
|
748
751
|
let detailed = await attempt(maxTokens, baseTimeout)
|
|
749
752
|
if (detailed.failure && isTimeoutFailure(detailed.failure)) {
|
|
750
753
|
const longer = Math.min(Math.max(Math.round(baseTimeout * 2.5), baseTimeout + 30000), 180000)
|
|
@@ -781,6 +784,21 @@ export function apply(ctx, config) {
|
|
|
781
784
|
}
|
|
782
785
|
break // skip(太短 / 不值得保存)或没有更多候选
|
|
783
786
|
}
|
|
787
|
+
// 账本:记下这次沉淀的真实开销(输入字符 / LLM 调用次数 / 输出字符),失败也记(便于看失败率与白烧次数)
|
|
788
|
+
try {
|
|
789
|
+
recordLedger(root, {
|
|
790
|
+
kind: 'capture',
|
|
791
|
+
session: sessionId,
|
|
792
|
+
charsIn: String(text || '').length,
|
|
793
|
+
chars: result ? JSON.stringify(result).length : 0,
|
|
794
|
+
llmCalls,
|
|
795
|
+
provider: route.provider,
|
|
796
|
+
model: route.model,
|
|
797
|
+
ms: Date.now() - captureT0,
|
|
798
|
+
ok: !!result,
|
|
799
|
+
note: failure ? failure.code : '',
|
|
800
|
+
})
|
|
801
|
+
} catch { /* 账本失败不影响沉淀主流程 */ }
|
|
784
802
|
// 真失败要亮红并进提示段:此前只有笼统的「蒸馏无输出」,真实原因(如缺凭证)完全不可见。
|
|
785
803
|
if (failure) { health.fail(`蒸馏失败(${failure.provider}):${failure.code} ${failure.message}`); touchPrompt() }
|
|
786
804
|
// 主因 + 兜底候选失败汇总:卡片 reason 与 created 日志都用这个,避免把排查者引向错误方向。
|
|
@@ -1010,7 +1028,10 @@ export function apply(ctx, config) {
|
|
|
1010
1028
|
} catch { /* 单个库失败不影响其它库 */ }
|
|
1011
1029
|
}
|
|
1012
1030
|
if (multi) hits = hits.slice(0, limit)
|
|
1013
|
-
if (hits.length === 0)
|
|
1031
|
+
if (hits.length === 0) {
|
|
1032
|
+
recordLedger((targets[0] && targets[0].root) || vaultDir(workspace), { kind: 'recall', results: 0, chars: 0, note: query.slice(0, 120) })
|
|
1033
|
+
return `记忆库中没有与「${query}」相关的内容。`
|
|
1034
|
+
}
|
|
1014
1035
|
const lines = hits.map((h, i) => {
|
|
1015
1036
|
const tags = h.tags.length ? ` [${h.tags.join(', ')}]` : ''
|
|
1016
1037
|
const snippet = String(h.summary || '').replace(/\s+/g, ' ').trim().slice(0, defLen)
|
|
@@ -1019,7 +1040,9 @@ export function apply(ctx, config) {
|
|
|
1019
1040
|
return `### ${i + 1}. ${h.title}${tags}${from}\n路径:${h.path}\n${snippet}${body}`
|
|
1020
1041
|
})
|
|
1021
1042
|
const where = multi ? `(作用域 ${scope || 'all'},共 ${targets.length} 个库)` : ''
|
|
1022
|
-
|
|
1043
|
+
const out = `从记忆核心检索到 ${hits.length} 条相关卡片${where}:\n\n${lines.join('\n\n')}`
|
|
1044
|
+
recordLedger((targets[0] && targets[0].root) || vaultDir(workspace), { kind: 'recall', results: hits.length, chars: out.length, note: query.slice(0, 120) })
|
|
1045
|
+
return out
|
|
1023
1046
|
},
|
|
1024
1047
|
}))
|
|
1025
1048
|
}
|
package/lib/api.js
CHANGED
|
@@ -20,6 +20,7 @@ import { listCards, countCards, readCard, search, searchAll, graph, graphAll, ov
|
|
|
20
20
|
import { compressExcerpt, DEFAULT_CAPTURE_MAX_TOKENS } from './capture.js'
|
|
21
21
|
import { buildDiagnostics } from './feedback.js'
|
|
22
22
|
import { getSetupStatus, runSetup, mcpAgentAction } from './setup.js'
|
|
23
|
+
import { ledgerSummary } from './ledger.js'
|
|
23
24
|
import { backupDb, dbStatus, rebuildGuidance, withDbRetry } from './db.js'
|
|
24
25
|
import { createHub } from './sse.js'
|
|
25
26
|
import { probeServedVersion, restartResident, currentPkgVersion, readWatchdogLock, isPidAlive, probeLockWritable } from './watchdog.js'
|
|
@@ -952,6 +953,14 @@ export function createApi(deps = {}) {
|
|
|
952
953
|
json(res, 200, { ok: true })
|
|
953
954
|
return
|
|
954
955
|
}
|
|
956
|
+
case '/ledger': {
|
|
957
|
+
// 记忆账本:近 N 天召回注入 / 蒸馏调用开销 + 召回有用率
|
|
958
|
+
try {
|
|
959
|
+
const days = Number(query.get('days')) || 7
|
|
960
|
+
json(res, 200, { ok: true, ...ledgerSummary(vaultRoot, { days }) })
|
|
961
|
+
} catch (e) { json(res, 200, { ok: false, error: String(e?.message || e) }) }
|
|
962
|
+
return
|
|
963
|
+
}
|
|
955
964
|
case '/budget': {
|
|
956
965
|
try {
|
|
957
966
|
const cfg = getSettings() ?? {}
|
package/lib/capture-run.js
CHANGED
|
@@ -11,6 +11,7 @@ import fs from 'node:fs'
|
|
|
11
11
|
import { captureCard, captureUpdate, pickNeighbors, summarizeTurnDetailed, routeCandidates, stripRuntimeNoise, hasUsableContent, deriveTitle, isUsableTitle } from './capture.js'
|
|
12
12
|
import { resolveVaultDir, defaultVaultRoot } from './vault-resolve.js'
|
|
13
13
|
import { createLlmFromEnv } from './llm-openai.js'
|
|
14
|
+
import { recordLedger } from './ledger.js'
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* 解析当前 vault 目录。
|
|
@@ -63,6 +64,28 @@ export async function runStandaloneCapture(vaultRoot, text, { source = '', llm,
|
|
|
63
64
|
const title = deriveTitle(clean)
|
|
64
65
|
if (!isUsableTitle(title)) return { ok: false, action: 'skipped', reason: 'cannot derive a usable title' }
|
|
65
66
|
|
|
67
|
+
// 记忆账本:独立进程(MCP / CLI / hooks)的沉淀也要记账 —— 否则「哪个入口花了 token」永远说不清。
|
|
68
|
+
const t0 = Date.now()
|
|
69
|
+
let llmCalls = 0
|
|
70
|
+
let routeUsed = null
|
|
71
|
+
let outChars = 0
|
|
72
|
+
const finish = (res) => {
|
|
73
|
+
try {
|
|
74
|
+
recordLedger(vaultRoot, {
|
|
75
|
+
kind: 'capture',
|
|
76
|
+
charsIn: clean.length,
|
|
77
|
+
chars: outChars,
|
|
78
|
+
llmCalls,
|
|
79
|
+
provider: routeUsed ? routeUsed.provider : '',
|
|
80
|
+
model: routeUsed ? routeUsed.model : '',
|
|
81
|
+
ms: Date.now() - t0,
|
|
82
|
+
ok: !!(res && res.ok === true),
|
|
83
|
+
note: res && res.action ? res.action : '',
|
|
84
|
+
})
|
|
85
|
+
} catch { /* 账本失败不影响沉淀 */ }
|
|
86
|
+
return res
|
|
87
|
+
}
|
|
88
|
+
|
|
66
89
|
// 自动审核:共享配置文件(DSH)优先,其次 env,默认 all(独立进程无 DSH settings)
|
|
67
90
|
// 注:`shared` 原先在回调里局部读取,第 80 行的路由兜底却引用了外层同名变量 → ReferenceError
|
|
68
91
|
// 被下面的 try/catch 静默吞掉,导致 captureProvider/captureModel 在本进程从不生效。这里提到函数级。
|
|
@@ -85,9 +108,12 @@ export async function runStandaloneCapture(vaultRoot, text, { source = '', llm,
|
|
|
85
108
|
// 有 LLM:蒸馏判定链(与 DSH 侧一致)
|
|
86
109
|
if (llmClient && route) {
|
|
87
110
|
try {
|
|
111
|
+
routeUsed = route
|
|
88
112
|
const neighbors = await pickNeighbors(vaultRoot, { title: '', body: clean.slice(0, 400) }, 8)
|
|
113
|
+
llmCalls++
|
|
89
114
|
const detailed = await summarizeTurnDetailed(llmClient, route, clean, { existing: neighbors, signal: AbortSignal.timeout(45000) })
|
|
90
115
|
const result = detailed.card
|
|
116
|
+
if (result) outChars = JSON.stringify(result).length
|
|
91
117
|
// 独立进程也必须把真实失败暴露出来(走 stderr),别再静默降级成「无输出」
|
|
92
118
|
if (detailed.failure) {
|
|
93
119
|
process.stderr.write(`[memory-eternal] 蒸馏失败(${route.provider}/${route.model}):${detailed.failure.code} ${detailed.failure.message}\n`)
|
|
@@ -95,18 +121,18 @@ export async function runStandaloneCapture(vaultRoot, text, { source = '', llm,
|
|
|
95
121
|
if (result && result.save === true) {
|
|
96
122
|
if (result.append_to) {
|
|
97
123
|
await captureUpdate(vaultRoot, result.append_to, result.update, { threshold: 0.62 })
|
|
98
|
-
return { ok: true, action: 'appended', path: result.append_to }
|
|
124
|
+
return finish({ ok: true, action: 'appended', path: result.append_to })
|
|
99
125
|
}
|
|
100
126
|
const card = { kind: result.kind, title: result.title, tags: result.tags, body: result.body, source, status: resolveAuditStatus(result.kind, source || 'agent'), submittedBy: source || 'agent', severity: 'info', reason: 'AI 自动沉淀(蒸馏卡)' }
|
|
101
127
|
const out = await captureCard(vaultRoot, card, { threshold: 0.62 })
|
|
102
|
-
if (out.ok) return { ok: true, action: 'created', path: out.path ?? out.rel }
|
|
128
|
+
if (out.ok) return finish({ ok: true, action: 'created', path: out.path ?? out.rel })
|
|
103
129
|
if (out.duplicate) {
|
|
104
130
|
await captureUpdate(vaultRoot, out.duplicate.path, `${result.title}:${result.body.slice(0, 400)}`, { threshold: 0.62 })
|
|
105
|
-
return { ok: true, action: 'appended', path: out.duplicate.path }
|
|
131
|
+
return finish({ ok: true, action: 'appended', path: out.duplicate.path })
|
|
106
132
|
}
|
|
107
|
-
return { ok: false, action: 'failed', reason: String(out.reason || 'write failed') }
|
|
133
|
+
return finish({ ok: false, action: 'failed', reason: String(out.reason || 'write failed') })
|
|
108
134
|
}
|
|
109
|
-
if (result && result.save === false) return { ok: true, action: 'skipped', reason: 'model judged not durable' }
|
|
135
|
+
if (result && result.save === false) return finish({ ok: true, action: 'skipped', reason: 'model judged not durable' })
|
|
110
136
|
} catch (error) {
|
|
111
137
|
// LLM 失败 → 落入降级路径,不能让一次网络抖动丢掉沉淀
|
|
112
138
|
}
|
|
@@ -114,6 +140,7 @@ export async function runStandaloneCapture(vaultRoot, text, { source = '', llm,
|
|
|
114
140
|
|
|
115
141
|
// 降级:无 LLM(或 LLM 调用失败)→ 原文卡(kind 默认 content,dedup 开)
|
|
116
142
|
// 标题用 deriveTitle 派生(不再 slice 硬切);正文用剥离噪声后的 clean。
|
|
143
|
+
outChars = clean.length
|
|
117
144
|
const out = await captureCard(vaultRoot, {
|
|
118
145
|
kind: 'content',
|
|
119
146
|
title,
|
|
@@ -125,10 +152,10 @@ export async function runStandaloneCapture(vaultRoot, text, { source = '', llm,
|
|
|
125
152
|
severity: 'info',
|
|
126
153
|
reason: 'AI 自动沉淀(原文卡)',
|
|
127
154
|
}, { threshold: 0.62 })
|
|
128
|
-
if (out.ok) return { ok: true, action: 'created', path: out.path ?? out.rel, degraded: true }
|
|
155
|
+
if (out.ok) return finish({ ok: true, action: 'created', path: out.path ?? out.rel, degraded: true })
|
|
129
156
|
if (out.duplicate) {
|
|
130
157
|
await captureUpdate(vaultRoot, out.duplicate.path, clean.slice(0, 400), { threshold: 0.62 })
|
|
131
|
-
return { ok: true, action: 'appended', path: out.duplicate.path, degraded: true }
|
|
158
|
+
return finish({ ok: true, action: 'appended', path: out.duplicate.path, degraded: true })
|
|
132
159
|
}
|
|
133
|
-
return { ok: false, action: 'failed', reason: String(out.reason || 'write failed') }
|
|
160
|
+
return finish({ ok: false, action: 'failed', reason: String(out.reason || 'write failed') })
|
|
134
161
|
}
|