memory-eternal 0.1.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 +34 -0
- package/.codex-plugin/plugin.json +29 -0
- package/.cursor-plugin/plugin.json +28 -0
- package/.mcp.json +9 -0
- package/LICENSE +21 -0
- package/README.en.md +224 -0
- package/README.md +236 -0
- package/bin/dsh-memory.mjs +183 -0
- package/cordis.patch.yml +15 -0
- package/hooks/capture.mjs +77 -0
- package/hooks/claude-session.js +56 -0
- package/hooks/hooks.claude.json +12 -0
- package/hooks/hooks.codex.json +15 -0
- package/hooks/hooks.cursor.json +11 -0
- package/index.js +565 -0
- package/lib/api.js +321 -0
- package/lib/capture-run.js +98 -0
- package/lib/capture.js +267 -0
- package/lib/client.js +186 -0
- package/lib/llm-openai.js +84 -0
- package/lib/mcp.js +166 -0
- package/lib/setup.js +470 -0
- package/lib/sweep.js +56 -0
- package/lib/vault.js +802 -0
- package/lib/watchdog.js +201 -0
- package/lib/web.js +149 -0
- package/package.json +101 -0
- package/skills/memory-recall/SKILL.md +14 -0
- package/web/app.js +230 -0
- package/web/index.html +46 -0
package/lib/api.js
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
// 记忆核心 · 知识库 JSON API(与宿主无关的纯 HTTP 层)。
|
|
2
|
+
//
|
|
3
|
+
// 从 index.js 抽出的 handleApi/json:逻辑原封不动,仅把对 DSH 闭包
|
|
4
|
+
// (vaultDir/vaultRoots/settings)的依赖改为通过 createApi(deps) 注入,
|
|
5
|
+
// 使 DSH 插件(经 webServer 前缀路由)与独立 Web server(lib/web.js)
|
|
6
|
+
// 共用同一份 API 实现。
|
|
7
|
+
//
|
|
8
|
+
// deps:
|
|
9
|
+
// - vaultDir() → 当前激活 vault 根目录(string)
|
|
10
|
+
// - vaultRoots() → [{ name, root }] 全部 profile 根(跨库聚合用)
|
|
11
|
+
// - getSettings() → 插件设置对象(/budget /compress 读取;可选,缺省 {})
|
|
12
|
+
|
|
13
|
+
import { listCards, readCard, search, searchAll, graph, graphAll, overview, exportCards, deleteCard, writeCard, parseCard, stats, optimizeCandidates, optimizeApply, addFeedback, dailyBrief, dailyCounts, mergeCards, ensureVault, auditQueue, recycleList, purgeExpired, setCardStatus, restoreCard } from './vault.js'
|
|
14
|
+
import { compressExcerpt } from './capture.js'
|
|
15
|
+
import { getSetupStatus, runSetup, mcpAgentAction } from './setup.js'
|
|
16
|
+
|
|
17
|
+
export const API_PREFIX = '/memory-eternal/api'
|
|
18
|
+
|
|
19
|
+
export function createApi(deps = {}) {
|
|
20
|
+
const vaultDir = deps.vaultDir ?? (() => '')
|
|
21
|
+
const vaultRoots = deps.vaultRoots ?? (() => [{ name: '', root: vaultDir() }])
|
|
22
|
+
const getSettings = deps.getSettings ?? (() => ({}))
|
|
23
|
+
// DSH 宿主信息(独立 web server 无 DSH 环境时为 null)。head DSH 状态行用。
|
|
24
|
+
const getDshInfo = deps.getDshInfo ?? (() => null)
|
|
25
|
+
|
|
26
|
+
return async function handleApi(req, res) {
|
|
27
|
+
const vaultRoot = vaultDir()
|
|
28
|
+
const url = new URL(req.url, 'http://localhost')
|
|
29
|
+
const route = url.pathname.slice(API_PREFIX.length).replace(/\/+$/, '') || '/overview'
|
|
30
|
+
const query = url.searchParams
|
|
31
|
+
|
|
32
|
+
switch (route) {
|
|
33
|
+
case '/overview': {
|
|
34
|
+
await ensureVault(vaultRoot)
|
|
35
|
+
json(res, 200, { ok: true, vaultDir: vaultRoot, ...(await overview(vaultRoot)) })
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
case '/cards': {
|
|
39
|
+
const kind = query.get('kind') || ''
|
|
40
|
+
const q = query.get('q') || ''
|
|
41
|
+
const status = (query.get('status') || 'approved').toLowerCase()
|
|
42
|
+
const limit = Math.min(Number(query.get('limit')) || 200, 500)
|
|
43
|
+
// all = 全部(含 pending/rejected,排除回收站);默认/approved = 已审核。
|
|
44
|
+
const want = status === 'all' ? ['approved', 'pending', 'rejected'] : status === 'pending' || status === 'rejected' ? [status] : ['approved']
|
|
45
|
+
let cards = await listCards(vaultRoot, { status: want })
|
|
46
|
+
if (kind) cards = cards.filter((c) => c.kind === kind)
|
|
47
|
+
if (q.trim()) {
|
|
48
|
+
const hits = await search(vaultRoot, q, { limit: 200 })
|
|
49
|
+
const hitPaths = new Set(hits.map((h) => h.path))
|
|
50
|
+
cards = cards.filter((c) => hitPaths.has(c.path))
|
|
51
|
+
}
|
|
52
|
+
json(res, 200, { ok: true, cards: cards.slice(0, limit) })
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
case '/card': {
|
|
56
|
+
const rel = query.get('path') || ''
|
|
57
|
+
if (!rel) return json(res, 400, { ok: false, error: '缺少 path' })
|
|
58
|
+
const text = await readCard(vaultRoot, rel)
|
|
59
|
+
json(res, 200, { ok: true, path: rel, text })
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
case '/search': {
|
|
63
|
+
const q = query.get('q') || ''
|
|
64
|
+
if (!q.trim()) return json(res, 200, { ok: true, hits: [] })
|
|
65
|
+
const all = query.get('all') === '1'
|
|
66
|
+
const semantic = query.get('semantic') === '1'
|
|
67
|
+
let hits
|
|
68
|
+
try { hits = all ? await searchAll(vaultRoots(), q, { limit: 30, semantic }) : await search(vaultRoot, q, { limit: 30, semantic }) }
|
|
69
|
+
catch (e) { hits = await search(vaultRoot, q, { limit: 30, semantic }) }
|
|
70
|
+
json(res, 200, { ok: true, hits })
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
case '/graph': {
|
|
74
|
+
await ensureVault(vaultRoot)
|
|
75
|
+
const all = query.get('all') === '1'
|
|
76
|
+
let g
|
|
77
|
+
try { g = all ? await graphAll(vaultRoots()) : await graph(vaultRoot) }
|
|
78
|
+
catch (e) { g = await graph(vaultRoot) }
|
|
79
|
+
json(res, 200, { ok: true, ...g })
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
case '/todayBrief': {
|
|
83
|
+
await ensureVault(vaultRoot)
|
|
84
|
+
const s = await stats(vaultRoot)
|
|
85
|
+
json(res, 200, { ok: true, today: s.today, brief: dailyBrief(s.todayCards) })
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
case '/export': {
|
|
89
|
+
await ensureVault(vaultRoot)
|
|
90
|
+
json(res, 200, { ok: true, cards: await exportCards(vaultRoot) })
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
case '/delete': {
|
|
94
|
+
const rel = query.get('path') || ''
|
|
95
|
+
if (!rel) return json(res, 400, { ok: false, error: '缺少 path' })
|
|
96
|
+
// 软删(进回收站);?permanent=1 直接永久删除
|
|
97
|
+
const permanent = query.get('permanent') === '1'
|
|
98
|
+
await deleteCard(vaultRoot, rel, { permanent })
|
|
99
|
+
json(res, 200, { ok: true, soft: !permanent })
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
case '/audit/list': {
|
|
103
|
+
const { pending, rejected } = await auditQueue(vaultRoot)
|
|
104
|
+
json(res, 200, { ok: true, pending, rejected })
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
case '/audit/approve': {
|
|
108
|
+
// POST {path} 或 GET ?path= → status=approved
|
|
109
|
+
const rel = query.get('path') || (await readBody(req))?.path || ''
|
|
110
|
+
if (!rel) return json(res, 400, { ok: false, error: '缺少 path' })
|
|
111
|
+
await setCardStatus(vaultRoot, rel, 'approved')
|
|
112
|
+
json(res, 200, { ok: true, path: rel, status: 'approved' })
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
case '/audit/reject': {
|
|
116
|
+
// POST {path} 或 GET ?path= → status=rejected(标记驳回,不删除)
|
|
117
|
+
const rel = query.get('path') || (await readBody(req))?.path || ''
|
|
118
|
+
if (!rel) return json(res, 400, { ok: false, error: '缺少 path' })
|
|
119
|
+
await setCardStatus(vaultRoot, rel, 'rejected')
|
|
120
|
+
json(res, 200, { ok: true, path: rel, status: 'rejected' })
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
case '/recycle/list': {
|
|
124
|
+
const items = await recycleList(vaultRoot)
|
|
125
|
+
json(res, 200, { ok: true, items })
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
case '/recycle/restore': {
|
|
129
|
+
const rel = query.get('path') || ''
|
|
130
|
+
if (!rel) return json(res, 400, { ok: false, error: '缺少 path' })
|
|
131
|
+
await restoreCard(vaultRoot, rel)
|
|
132
|
+
json(res, 200, { ok: true, path: rel })
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
case '/recycle/purge': {
|
|
136
|
+
const rel = query.get('path') || ''
|
|
137
|
+
if (!rel) return json(res, 400, { ok: false, error: '缺少 path' })
|
|
138
|
+
await deleteCard(vaultRoot, rel, { permanent: true })
|
|
139
|
+
json(res, 200, { ok: true, path: rel, purged: true })
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
case '/recycle/purge-expired': {
|
|
143
|
+
const days = Number(query.get('days')) || 30
|
|
144
|
+
const r = await purgeExpired(vaultRoot, days)
|
|
145
|
+
json(res, 200, r)
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
case '/write': {
|
|
149
|
+
let raw = ''
|
|
150
|
+
for await (const chunk of req) raw += chunk
|
|
151
|
+
let body
|
|
152
|
+
try { body = JSON.parse(raw || '{}') } catch { return json(res, 400, { ok: false, error: 'JSON 解析失败' }) }
|
|
153
|
+
if (!body.body) return json(res, 400, { ok: false, error: '缺少正文' })
|
|
154
|
+
await ensureVault(vaultRoot)
|
|
155
|
+
const r = await writeCard(vaultRoot, { kind: body.kind || 'knowledge', title: body.title || '无标题', tags: body.tags || [], body: body.body, source: body.source || 'manual', status: 'pending', submittedBy: body.source || 'manual', severity: body.severity || 'info', reason: body.reason || '手动创建' }, { dedup: false })
|
|
156
|
+
json(res, 200, r)
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
case '/import': {
|
|
160
|
+
let raw = ''
|
|
161
|
+
for await (const chunk of req) raw += chunk
|
|
162
|
+
if (raw.length > 20 * 1024 * 1024) return json(res, 413, { ok: false, error: '文件过大' })
|
|
163
|
+
let payload
|
|
164
|
+
try { payload = JSON.parse(raw || '{}') } catch { return json(res, 400, { ok: false, error: 'JSON 解析失败' }) }
|
|
165
|
+
const list = payload.cards || []
|
|
166
|
+
if (!Array.isArray(list)) return json(res, 400, { ok: false, error: '缺少 cards 数组' })
|
|
167
|
+
let imported = 0, skipped = 0
|
|
168
|
+
for (const c of list) {
|
|
169
|
+
const text = c.text || ''
|
|
170
|
+
let kind = c.kind || 'knowledge', title = c.title || '导入记忆', tags = [], body = text, source = ''
|
|
171
|
+
try { const p = parseCard(text); kind = p.meta.kind || kind; title = p.meta.title || title; tags = p.meta.tags || tags; body = p.body; source = p.meta.source } catch {}
|
|
172
|
+
const r = await writeCard(vaultRoot, { kind, title, tags, body, source, status: 'pending' })
|
|
173
|
+
if (r.ok) imported++; else skipped++
|
|
174
|
+
}
|
|
175
|
+
json(res, 200, { ok: true, imported, skipped })
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
case '/merge': {
|
|
179
|
+
const paths = (query.get('paths') || '').split(',').map((p) => p.trim()).filter(Boolean)
|
|
180
|
+
const r = await mergeCards(vaultRoot, paths)
|
|
181
|
+
if (!r.ok) return json(res, 400, r)
|
|
182
|
+
json(res, 200, r)
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
case '/stats': {
|
|
186
|
+
await ensureVault(vaultRoot)
|
|
187
|
+
const days = Math.min(Number(query.get('days')) || 30, 90)
|
|
188
|
+
json(res, 200, { ok: true, ...(await stats(vaultRoot)), trend: await dailyCounts(vaultRoot, days) })
|
|
189
|
+
return
|
|
190
|
+
}
|
|
191
|
+
case '/optimize': {
|
|
192
|
+
// 非破坏性:只返回「整理建议」(相似卡对 + 陈旧卡),不自动删改。
|
|
193
|
+
json(res, 200, { ok: true, ...(await optimizeCandidates(vaultRoot)) })
|
|
194
|
+
return
|
|
195
|
+
}
|
|
196
|
+
case '/optimize-execute': {
|
|
197
|
+
// 一键优化执行:合并相似卡 + 可选清理陈旧卡。POST body: {cleanupStale?, simThreshold?, staleDays?, dryRun?}
|
|
198
|
+
if (req.method !== 'POST') { json(res, 405, { ok: false, error: '需 POST' }); return }
|
|
199
|
+
let raw = ''
|
|
200
|
+
for await (const chunk of req) raw += chunk
|
|
201
|
+
let opts = {}
|
|
202
|
+
try { opts = JSON.parse(raw || '{}') } catch { opts = {} }
|
|
203
|
+
const result = await optimizeApply(vaultRoot, opts)
|
|
204
|
+
json(res, 200, result)
|
|
205
|
+
return
|
|
206
|
+
}
|
|
207
|
+
case '/setup-run': {
|
|
208
|
+
// 「补全 MCP」:真正执行 runSetup(写外部 agent 配置)。共享层(独立 web 也可用)
|
|
209
|
+
if (req.method !== 'POST') return json(res, 405, { ok: false, error: '需 POST' })
|
|
210
|
+
try {
|
|
211
|
+
const out = await runSetup({ log: () => {}, enabled: true })
|
|
212
|
+
json(res, 200, { ok: true, results: out.results })
|
|
213
|
+
} catch (e) { json(res, 500, { ok: false, error: String(e?.message || e) }) }
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
case '/mcp/action': {
|
|
217
|
+
// 单智能体安装/卸载 MCP:POST {agent, action}。共享层(独立 web 可用)
|
|
218
|
+
if (req.method !== 'POST') return json(res, 405, { ok: false, error: '需 POST' })
|
|
219
|
+
try {
|
|
220
|
+
let raw = ''
|
|
221
|
+
for await (const chunk of req) raw += chunk
|
|
222
|
+
const body = JSON.parse(raw || '{}')
|
|
223
|
+
const out = await mcpAgentAction(String(body.agent || ''), String(body.action || ''), { log: () => {} })
|
|
224
|
+
json(res, 200, out)
|
|
225
|
+
} catch (e) { json(res, 500, { ok: false, error: String(e?.message || e) }) }
|
|
226
|
+
return
|
|
227
|
+
}
|
|
228
|
+
case '/setup-status': {
|
|
229
|
+
// 只读查询各 agent MCP 配置状态,不写任何文件
|
|
230
|
+
const status = await getSetupStatus()
|
|
231
|
+
// prepend DSH 宿主行(独立 web server 无 DSH 环境时只有外部 agent)
|
|
232
|
+
const dsh = getDshInfo ? getDshInfo() : null
|
|
233
|
+
if (dsh) {
|
|
234
|
+
status.agents = [{ name: 'dsh', label: dsh.label || 'DSH (当前宿主)', installed: true, mcpConfigured: true, isDsh: true, recallTool: dsh.memoryRecallTool, autoCapture: dsh.autoCapture, autoRecall: dsh.autoRecall, vaultDir: dsh.vaultDir }, ...(status.agents || [])]
|
|
235
|
+
}
|
|
236
|
+
json(res, 200, status)
|
|
237
|
+
return
|
|
238
|
+
}
|
|
239
|
+
case '/feedback': {
|
|
240
|
+
let raw = ''
|
|
241
|
+
for await (const chunk of req) raw += chunk
|
|
242
|
+
let rec
|
|
243
|
+
try { rec = JSON.parse(raw || '{}') } catch { return json(res, 400, { ok: false, error: 'JSON 解析失败' }) }
|
|
244
|
+
if (!rec || !rec.path) return json(res, 400, { ok: false, error: '缺少 path' })
|
|
245
|
+
await addFeedback(vaultRoot, { query: String(rec.query || ''), path: String(rec.path || ''), useful: rec.useful === true })
|
|
246
|
+
json(res, 200, { ok: true })
|
|
247
|
+
return
|
|
248
|
+
}
|
|
249
|
+
case '/budget': {
|
|
250
|
+
try {
|
|
251
|
+
const cfg = getSettings() ?? {}
|
|
252
|
+
const env = typeof process !== 'undefined' ? process.env : {}
|
|
253
|
+
json(res, 200, {
|
|
254
|
+
ok: true,
|
|
255
|
+
budgetChars: cfg.sessionBudgetChars ?? 80000,
|
|
256
|
+
recallLimit: cfg.recallLimit ?? 5,
|
|
257
|
+
embedding: cfg.recallEmbedding || '',
|
|
258
|
+
// 服务自管理配置(DSH host 从 settings 读;独立 web server 从 env 兜底)
|
|
259
|
+
autoWeb: cfg.autoWeb ?? env.MEMORY_AUTO_WEB !== '0',
|
|
260
|
+
autoWebMode: cfg.autoWebMode || 'init',
|
|
261
|
+
webPort: Number(cfg.webPort) || Number(env.MEMORY_WEB_PORT) || 7999,
|
|
262
|
+
webCheckIntervalMs: Number(cfg.webCheckIntervalMs) || 5000,
|
|
263
|
+
webMaxRestart: Number(cfg.webMaxRestart) || 10,
|
|
264
|
+
watchdogAutoSpawn: cfg.watchdogAutoSpawn ?? env.MEMORY_WATCHDOG === '1',
|
|
265
|
+
autoMcpSetup: cfg.autoMcpSetup ?? false,
|
|
266
|
+
})
|
|
267
|
+
} catch (e) {
|
|
268
|
+
// 配置读取异常时降级返回默认值,避免面板整块红屏
|
|
269
|
+
json(res, 200, { ok: true, budgetChars: 80000, recallLimit: 5, embedding: '', autoWeb: true, autoWebMode: 'init', webPort: 7999, webCheckIntervalMs: 5000, webMaxRestart: 10, watchdogAutoSpawn: true, autoMcpSetup: false })
|
|
270
|
+
}
|
|
271
|
+
return
|
|
272
|
+
}
|
|
273
|
+
case '/config': {
|
|
274
|
+
// 独立 web server(无 DSH settings)的配置读取兜底:env + 默认值。DSH host 的 /config 在其 webServer handler 拦截实现(可写),此处只读。
|
|
275
|
+
const cfg = getSettings() ?? {}
|
|
276
|
+
const env = typeof process !== 'undefined' ? process.env : {}
|
|
277
|
+
const safe = {
|
|
278
|
+
autoCapture: cfg.autoCapture ?? true, autoRecall: cfg.autoRecall ?? true, recallLimit: cfg.recallLimit ?? 5, recallSummaryLen: cfg.recallSummaryLen ?? 130, recallIncludeBody: cfg.recallIncludeBody ?? false,
|
|
279
|
+
captureMinChars: cfg.captureMinChars ?? 200, captureCooldownMs: cfg.captureCooldownMs ?? 300000, dedupThreshold: cfg.dedupThreshold ?? 0.62, maxCardsPerDay: cfg.maxCardsPerDay ?? 60,
|
|
280
|
+
distillEnabled: cfg.distillEnabled ?? true, dedupByLLM: cfg.dedupByLLM ?? true, captureMaxTokens: cfg.captureMaxTokens ?? 900, recallMinScore: cfg.recallMinScore ?? 2,
|
|
281
|
+
autoWeb: cfg.autoWeb ?? env.MEMORY_AUTO_WEB !== '0', autoWebMode: cfg.autoWebMode || 'init', webPort: Number(cfg.webPort) || Number(env.MEMORY_WEB_PORT) || 7999, webCheckIntervalMs: Number(cfg.webCheckIntervalMs) || 5000, webMaxRestart: Number(cfg.webMaxRestart) || 10, watchdogAutoSpawn: cfg.watchdogAutoSpawn ?? env.MEMORY_WATCHDOG === '1', autoMcpSetup: cfg.autoMcpSetup ?? false,
|
|
282
|
+
auditMode: cfg.auditMode ?? 'all', auditExemptAgents: cfg.auditExemptAgents ?? [], auditExemptKinds: cfg.auditExemptKinds ?? [], recycleRetentionDays: cfg.recycleRetentionDays ?? 30,
|
|
283
|
+
}
|
|
284
|
+
json(res, 200, { ok: true, config: safe, revision: 0, writable: false, readonly: true, version: env.MEMORY_ETERNAL_VERSION || '' })
|
|
285
|
+
return
|
|
286
|
+
}
|
|
287
|
+
case '/compress': {
|
|
288
|
+
// 记忆侧「压缩产物」接口:供 harness 在会话内压缩旧轮次时调用,返回一段可注入的摘要。
|
|
289
|
+
const cfg = getSettings() ?? {}
|
|
290
|
+
const body = new URLSearchParams(query)
|
|
291
|
+
const text = body.get('text') || ''
|
|
292
|
+
const maxChars = Math.min(Number(body.get('max')) || 2400, 6000)
|
|
293
|
+
if (!text.trim()) return json(res, 400, { ok: false, error: '缺少 text' })
|
|
294
|
+
const compressed = await compressExcerpt(text, maxChars)
|
|
295
|
+
json(res, 200, { ok: true, compressed, budgetChars: cfg.sessionBudgetChars ?? 80000 })
|
|
296
|
+
return
|
|
297
|
+
}
|
|
298
|
+
default:
|
|
299
|
+
json(res, 404, { ok: false, error: '未知接口' })
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export function json(res, status, body) {
|
|
305
|
+
const payload = JSON.stringify(body)
|
|
306
|
+
res.writeHead(status, {
|
|
307
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
308
|
+
'Content-Length': Buffer.byteLength(payload),
|
|
309
|
+
'Cache-Control': 'no-store',
|
|
310
|
+
})
|
|
311
|
+
res.end(payload)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** 读请求体(POST body),返回解析后的 JSON 对象或空。 */
|
|
315
|
+
export async function readBody(req) {
|
|
316
|
+
try {
|
|
317
|
+
let raw = ''
|
|
318
|
+
for await (const chunk of req) raw += chunk
|
|
319
|
+
return raw ? JSON.parse(raw) : {}
|
|
320
|
+
} catch { return {} }
|
|
321
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// 记忆核心 · 独立进程沉淀管线(MCP / CLI / hooks 共用)。
|
|
2
|
+
//
|
|
3
|
+
// 复刻 index.js runCapture 的判定链(预筛→配额→LLM 蒸馏→新建/追加→词法兜底),
|
|
4
|
+
// 但 LLM 来源是 lib/llm-openai.js 的 OpenAI 兼容适配器而非 DSH llm 服务。
|
|
5
|
+
// 未配置 LLM 时降级:跳过蒸馏,直接把原文写成一张卡(kind/tool 或调用方指定),
|
|
6
|
+
// 保证「无 LLM 环境也能沉淀,只是不压缩」。
|
|
7
|
+
|
|
8
|
+
import path from 'node:path'
|
|
9
|
+
import os from 'node:os'
|
|
10
|
+
import fs from 'node:fs'
|
|
11
|
+
import { captureCard, captureUpdate, pickNeighbors, summarizeTurn } from './capture.js'
|
|
12
|
+
import { createLlmFromEnv } from './llm-openai.js'
|
|
13
|
+
|
|
14
|
+
export function defaultVaultDir(env = process.env) {
|
|
15
|
+
if (env.MEMORY_VAULT_DIR && env.MEMORY_VAULT_DIR.trim()) return path.resolve(env.MEMORY_VAULT_DIR.trim())
|
|
16
|
+
const home = env.DSH_HOME || path.join(os.homedir(), '.dsh')
|
|
17
|
+
return path.join(home, 'memory-vault')
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// 读取 DSH 设置的完整配置(写入的共享文件),使独立 web / MCP hook 捕获与 DSH 设置同步。
|
|
21
|
+
const CONFIG_FILE = 'memory-eternal-config.json'
|
|
22
|
+
export function configFilePath(env = process.env) {
|
|
23
|
+
return path.join(env.DSH_HOME || path.join(os.homedir(), '.dsh'), CONFIG_FILE)
|
|
24
|
+
}
|
|
25
|
+
function readConfig(env = process.env) {
|
|
26
|
+
try {
|
|
27
|
+
return JSON.parse(fs.readFileSync(configFilePath(env), 'utf8'))
|
|
28
|
+
} catch { return null }
|
|
29
|
+
}
|
|
30
|
+
function splitList(x) { return (Array.isArray(x) ? x : String(x || '').split(',')).map((s) => String(s).trim()).filter(Boolean) }
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 沉淀一段对话文本到 vault。
|
|
34
|
+
* @returns {Promise<{ok:boolean, action:'created'|'appended'|'skipped'|'failed', path?:string, reason?:string, degraded?:boolean}>}
|
|
35
|
+
*/
|
|
36
|
+
export async function runStandaloneCapture(vaultRoot, text, { source = '', llm, env = process.env, minChars = 120 } = {}) {
|
|
37
|
+
const clean = String(text || '').trim()
|
|
38
|
+
if (clean.length < minChars) return { ok: false, action: 'skipped', reason: 'text too short' }
|
|
39
|
+
|
|
40
|
+
// 自动审核:共享配置文件(DSH)优先,其次 env,默认 all(独立进程无 DSH settings)
|
|
41
|
+
const resolveAuditStatus = (kind, sub) => {
|
|
42
|
+
const shared = readConfig(env)
|
|
43
|
+
const mode = shared?.auditMode || env.MEMORY_AUDIT_MODE || 'all'
|
|
44
|
+
const agents = shared?.auditExemptAgents ? splitList(shared.auditExemptAgents) : splitList(env.MEMORY_AUDIT_EXEMPT_AGENTS || '')
|
|
45
|
+
const kinds = shared?.auditExemptKinds ? splitList(shared.auditExemptKinds) : splitList(env.MEMORY_AUDIT_EXEMPT_KINDS || '')
|
|
46
|
+
if (mode === 'none') return 'approved'
|
|
47
|
+
if (agents.includes('__all__') || agents.includes(sub)) return 'approved'
|
|
48
|
+
if (kinds.includes('__all__') || kinds.includes(kind)) return 'approved'
|
|
49
|
+
return 'pending'
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const llmClient = llm ?? createLlmFromEnv(env)
|
|
53
|
+
const route = llmClient ? { provider: llmClient.listProviders()[0].id, model: (await llmClient.listModels())[0].id } : null
|
|
54
|
+
|
|
55
|
+
// 有 LLM:蒸馏判定链(与 DSH 侧一致)
|
|
56
|
+
if (llmClient && route) {
|
|
57
|
+
try {
|
|
58
|
+
const neighbors = await pickNeighbors(vaultRoot, { title: '', body: clean.slice(0, 400) }, 8)
|
|
59
|
+
const result = await summarizeTurn(llmClient, route, clean, { existing: neighbors, signal: AbortSignal.timeout(45000) })
|
|
60
|
+
if (result && result.save === true) {
|
|
61
|
+
if (result.append_to) {
|
|
62
|
+
await captureUpdate(vaultRoot, result.append_to, result.update, { threshold: 0.62 })
|
|
63
|
+
return { ok: true, action: 'appended', path: result.append_to }
|
|
64
|
+
}
|
|
65
|
+
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 自动沉淀(蒸馏卡)' }
|
|
66
|
+
const out = await captureCard(vaultRoot, card, { threshold: 0.62 })
|
|
67
|
+
if (out.ok) return { ok: true, action: 'created', path: out.path ?? out.rel }
|
|
68
|
+
if (out.duplicate) {
|
|
69
|
+
await captureUpdate(vaultRoot, out.duplicate.path, `${result.title}:${result.body.slice(0, 400)}`, { threshold: 0.62 })
|
|
70
|
+
return { ok: true, action: 'appended', path: out.duplicate.path }
|
|
71
|
+
}
|
|
72
|
+
return { ok: false, action: 'failed', reason: String(out.reason || 'write failed') }
|
|
73
|
+
}
|
|
74
|
+
if (result && result.save === false) return { ok: true, action: 'skipped', reason: 'model judged not durable' }
|
|
75
|
+
} catch (error) {
|
|
76
|
+
// LLM 失败 → 落入降级路径,不能让一次网络抖动丢掉沉淀
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 降级:无 LLM(或 LLM 调用失败)→ 原文卡(kind 默认 content,dedup 开)
|
|
81
|
+
const out = await captureCard(vaultRoot, {
|
|
82
|
+
kind: 'content',
|
|
83
|
+
title: clean.replace(/\s+/g, ' ').slice(0, 40) || '未命名记录',
|
|
84
|
+
tags: ['raw'],
|
|
85
|
+
body: clean,
|
|
86
|
+
source: source || 'manual',
|
|
87
|
+
status: resolveAuditStatus('content', source || 'agent'),
|
|
88
|
+
submittedBy: source || 'agent',
|
|
89
|
+
severity: 'info',
|
|
90
|
+
reason: 'AI 自动沉淀(原文卡)',
|
|
91
|
+
}, { threshold: 0.62 })
|
|
92
|
+
if (out.ok) return { ok: true, action: 'created', path: out.path ?? out.rel, degraded: true }
|
|
93
|
+
if (out.duplicate) {
|
|
94
|
+
await captureUpdate(vaultRoot, out.duplicate.path, clean.slice(0, 400), { threshold: 0.62 })
|
|
95
|
+
return { ok: true, action: 'appended', path: out.duplicate.path, degraded: true }
|
|
96
|
+
}
|
|
97
|
+
return { ok: false, action: 'failed', reason: String(out.reason || 'write failed') }
|
|
98
|
+
}
|