memory-eternal 0.9.16 → 0.9.23
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.en.md +5 -0
- package/README.md +5 -0
- package/index.js +43 -0
- package/lib/api.js +54 -1
- package/lib/client.js +17 -17
- package/lib/config-sync.js +89 -0
- package/lib/sse.js +51 -0
- package/lib/web.js +13 -2
- package/package.json +2 -2
- package/web/app.js +19 -19
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// 配置同步(共享文件协议):让「独立 Web 页」也能保存配置。
|
|
2
|
+
//
|
|
3
|
+
// 背景:独立 web server 进程里没有 DSH 的 settings 服务,`ctx.settings.update` 不存在,
|
|
4
|
+
// 所以它的 /config 过去只能只读 —— 用户点保存永远没反应(issue #12 的另一半)。
|
|
5
|
+
// 方案:独立端把改动写进 `memory-eternal-config.pending.json`,DSH 宿主激活时与运行期
|
|
6
|
+
// 轮询该文件,用 settings.update 应用后删除。DSH 没运行时改动留在文件里,下次启动生效。
|
|
7
|
+
import fs from 'node:fs'
|
|
8
|
+
import path from 'node:path'
|
|
9
|
+
import { configFilePath } from './capture-run.js'
|
|
10
|
+
|
|
11
|
+
/** 待应用配置文件的路径(与共享配置文件同目录)。 */
|
|
12
|
+
export function pendingConfigPath(env = process.env) {
|
|
13
|
+
return String(configFilePath(env)).replace(/\.json$/i, '.pending.json')
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** 读待应用改动;文件不存在/损坏都返回 null(不影响主流程)。 */
|
|
17
|
+
export function readPendingConfig(env = process.env) {
|
|
18
|
+
try {
|
|
19
|
+
const data = JSON.parse(fs.readFileSync(pendingConfigPath(env), 'utf8'))
|
|
20
|
+
if (!data || typeof data.patch !== 'object' || data.patch === null || Array.isArray(data.patch)) return null
|
|
21
|
+
return { at: Number(data.at) || 0, tries: Number(data.tries) || 0, patch: data.patch }
|
|
22
|
+
} catch { return null }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** 原子写入待应用改动(tmp + rename),返回写入内容。 */
|
|
26
|
+
export function writePendingConfig(env, patch, now = Date.now(), tries = 0) {
|
|
27
|
+
const file = pendingConfigPath(env)
|
|
28
|
+
const data = tries > 0 ? { at: now, tries, patch } : { at: now, patch }
|
|
29
|
+
const tmp = file + '.' + process.pid + '.tmp'
|
|
30
|
+
fs.writeFileSync(tmp, JSON.stringify(data, null, 2), 'utf8')
|
|
31
|
+
fs.renameSync(tmp, file)
|
|
32
|
+
return data
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** 删除待应用文件;不存在也算成功。 */
|
|
36
|
+
export function clearPendingConfig(env = process.env) {
|
|
37
|
+
try { fs.unlinkSync(pendingConfigPath(env)); return true } catch { return false }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 监听待应用文件的变化(毫秒级触发),用于把「独立页保存 → DSH 应用」从 5 秒轮询
|
|
42
|
+
* 提升到准即时。监听失败(平台/权限)时退化为不监听 —— 仍有 5 秒轮询兜底。
|
|
43
|
+
* @returns {() => void} 停止监听
|
|
44
|
+
*/
|
|
45
|
+
export function watchPendingConfig(env, onChange, { debounceMs = 60 } = {}) {
|
|
46
|
+
const file = pendingConfigPath(env)
|
|
47
|
+
let timer = null
|
|
48
|
+
let watcher = null
|
|
49
|
+
const fire = () => {
|
|
50
|
+
if (timer) clearTimeout(timer)
|
|
51
|
+
timer = setTimeout(() => { try { onChange() } catch { /* 回调异常不影响监听 */ } }, debounceMs)
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
watcher = fs.watch(path.dirname(file), (_event, name) => {
|
|
55
|
+
if (name && !String(name).startsWith('memory-eternal-config.pending')) return
|
|
56
|
+
fire()
|
|
57
|
+
})
|
|
58
|
+
if (typeof watcher.unref === 'function') watcher.unref()
|
|
59
|
+
} catch { return () => {} }
|
|
60
|
+
return () => {
|
|
61
|
+
if (timer) clearTimeout(timer)
|
|
62
|
+
try { watcher.close() } catch { /* 已关闭 */ }
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 取出并应用一次待应用改动:apply(patch) 成功后删除文件;失败保留(下轮重试)。
|
|
68
|
+
* @returns {Promise<object|null>} 应用成功的 patch,没有待应用项时返回 null
|
|
69
|
+
*/
|
|
70
|
+
export async function drainPendingConfig(env, apply, maxTries = 5) {
|
|
71
|
+
const pending = readPendingConfig(env)
|
|
72
|
+
if (!pending) return null
|
|
73
|
+
try {
|
|
74
|
+
await apply(pending.patch)
|
|
75
|
+
} catch (error) {
|
|
76
|
+
const tries = (pending.tries || 0) + 1
|
|
77
|
+
if (tries >= maxTries) {
|
|
78
|
+
// 连续失败(多半是值不合法):放弃并删掉,否则会每 5 秒无限重试
|
|
79
|
+
clearPendingConfig(env)
|
|
80
|
+
const dropped = new Error('待应用配置连续失败 ' + tries + ' 次已放弃:' + String((error && error.message) || error))
|
|
81
|
+
dropped.dropped = true
|
|
82
|
+
throw dropped
|
|
83
|
+
}
|
|
84
|
+
writePendingConfig(env, pending.patch, pending.at, tries)
|
|
85
|
+
throw error
|
|
86
|
+
}
|
|
87
|
+
clearPendingConfig(env)
|
|
88
|
+
return pending.patch
|
|
89
|
+
}
|
package/lib/sse.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// SSE 推送中心:把「配置/卡片变更」即时推给所有已打开的页面(不必手动刷新)。
|
|
2
|
+
// 两个宿主(DSH 内嵌 + 独立 web)共用同一套实现;纯逻辑,便于单测(注入假 res)。
|
|
3
|
+
export function createHub({ heartbeatMs = 25000 } = {}) {
|
|
4
|
+
const clients = new Set()
|
|
5
|
+
let timer = null
|
|
6
|
+
const send = (res, chunk) => {
|
|
7
|
+
try { res.write(chunk); return true } catch { return false }
|
|
8
|
+
}
|
|
9
|
+
const drop = (res) => {
|
|
10
|
+
clients.delete(res)
|
|
11
|
+
try { res.end() } catch { /* 已断开 */ }
|
|
12
|
+
}
|
|
13
|
+
const ensureTimer = () => {
|
|
14
|
+
if (timer || !(heartbeatMs > 0)) return
|
|
15
|
+
timer = setInterval(() => {
|
|
16
|
+
for (const res of [...clients]) if (!send(res, ': ping\n\n')) drop(res)
|
|
17
|
+
}, heartbeatMs)
|
|
18
|
+
if (typeof timer.unref === 'function') timer.unref()
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
/** 接入一个连接(保持长连接,不要 end)。 */
|
|
22
|
+
add(res) {
|
|
23
|
+
try {
|
|
24
|
+
res.writeHead(200, {
|
|
25
|
+
'Content-Type': 'text/event-stream; charset=utf-8',
|
|
26
|
+
'Cache-Control': 'no-store',
|
|
27
|
+
Connection: 'keep-alive',
|
|
28
|
+
'X-Accel-Buffering': 'no',
|
|
29
|
+
})
|
|
30
|
+
} catch { return false }
|
|
31
|
+
if (!send(res, 'retry: 3000\n\n')) return false
|
|
32
|
+
clients.add(res)
|
|
33
|
+
ensureTimer()
|
|
34
|
+
const onClose = () => drop(res)
|
|
35
|
+
try { res.on('close', onClose); res.on('error', onClose) } catch { /* 某些响应对象没有 on */ }
|
|
36
|
+
return true
|
|
37
|
+
},
|
|
38
|
+
/** 广播事件(event: <name>,data: JSON)。返回当前连接数。 */
|
|
39
|
+
broadcast(event, data) {
|
|
40
|
+
const chunk = 'event: ' + event + '\ndata: ' + JSON.stringify(data === undefined ? {} : data) + '\n\n'
|
|
41
|
+
for (const res of [...clients]) if (!send(res, chunk)) drop(res)
|
|
42
|
+
return clients.size
|
|
43
|
+
},
|
|
44
|
+
get size() { return clients.size },
|
|
45
|
+
close() {
|
|
46
|
+
if (timer) clearInterval(timer)
|
|
47
|
+
timer = null
|
|
48
|
+
for (const res of [...clients]) drop(res)
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
}
|
package/lib/web.js
CHANGED
|
@@ -11,10 +11,10 @@
|
|
|
11
11
|
// 直接运行:node lib/web.js --port 7999 [--vault <dir>]
|
|
12
12
|
|
|
13
13
|
import http from 'node:http'
|
|
14
|
-
import { promises as fs, readFileSync } from 'node:fs'
|
|
14
|
+
import { promises as fs, readFileSync, watch } from 'node:fs'
|
|
15
15
|
import path from 'node:path'
|
|
16
16
|
import { fileURLToPath } from 'node:url'
|
|
17
|
-
import { createApi, API_PREFIX, encodeBody } from './api.js'
|
|
17
|
+
import { createApi, API_PREFIX, encodeBody, broadcast } from './api.js'
|
|
18
18
|
import { defaultVaultDir, configFilePath } from './capture-run.js'
|
|
19
19
|
import { readCaptureLog, deriveHealth } from './capture-log.js'
|
|
20
20
|
import { nodeBinary, childEnv } from './node-bin.js'
|
|
@@ -39,6 +39,17 @@ export function startWebServer({ port = DEFAULT_WEB_PORT, vaultRoot = defaultVau
|
|
|
39
39
|
getCaptureHealth: async () => deriveHealth(await readCaptureLog(50, process.env)),
|
|
40
40
|
})
|
|
41
41
|
|
|
42
|
+
// 共享配置文件变化 → 即时推给已打开的独立页(DSH 端一保存,这边不用刷新就能看到)
|
|
43
|
+
try {
|
|
44
|
+
const cfgFile = configFilePath(process.env)
|
|
45
|
+
const cfgName = path.basename(cfgFile)
|
|
46
|
+
const w = watch(path.dirname(cfgFile), (_event, name) => {
|
|
47
|
+
if (name && !String(name).startsWith(cfgName)) return
|
|
48
|
+
broadcast('config', { at: Date.now(), source: 'file' })
|
|
49
|
+
})
|
|
50
|
+
if (typeof w.unref === 'function') w.unref()
|
|
51
|
+
} catch { /* 监听失败不影响服务;客户端仍会在操作时重新拉取 */ }
|
|
52
|
+
|
|
42
53
|
const server = http.createServer(async (req, res) => {
|
|
43
54
|
try {
|
|
44
55
|
const url = new URL(req.url, 'http://localhost')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memory-eternal",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.23",
|
|
4
4
|
"description": "记忆核心(Memory Eternal):自研的 DeepSeek Harness 记忆插件,不移植任何既有记忆框架——对话结束后自动沉淀知识卡到本地 Markdown Vault(自研去重、自研 CJK 检索、可 git 管理),设置页提供图形化知识库(统计 / 搜索 / 知识图谱 + 侧边栏一键弹窗),Agent 通过 memory_recall 工具按需召回历史上下文。零人工干预。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"build": "node build.mjs",
|
|
61
61
|
"build:client": "node build.mjs",
|
|
62
62
|
"prepublishOnly": "node build.mjs",
|
|
63
|
-
"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/api.test.mjs && node tests/graph-perf.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/ssr.test.mjs"
|
|
63
|
+
"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/graph-perf.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/ssr.test.mjs"
|
|
64
64
|
},
|
|
65
65
|
"dependencies": {
|
|
66
66
|
"@deepseek-ai/schemastery": "^3.18.1"
|