memory-eternal 0.10.0 → 0.10.2
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 +2 -16
- package/.codex-plugin/plugin.json +2 -16
- package/.cursor-plugin/plugin.json +2 -16
- package/.mcp.json +9 -9
- package/LICENSE +21 -21
- package/README.en.md +16 -5
- package/README.md +20 -5
- package/bin/dsh-memory.mjs +318 -188
- package/examples/mcp-memory/memory-eternal.cordis.yml +59 -59
- package/hooks/capture.mjs +77 -77
- package/hooks/claude-session.js +56 -56
- package/hooks/hooks.claude.json +12 -12
- package/hooks/hooks.codex.json +15 -15
- package/hooks/hooks.cursor.json +11 -11
- package/index.js +177 -30
- package/lib/api.js +48 -14
- package/lib/capture-log.js +65 -59
- package/lib/capture.js +112 -7
- package/lib/client.js +13 -13
- package/lib/config-sync.js +161 -90
- package/lib/db.js +359 -359
- package/lib/feedback.js +214 -205
- package/lib/llm-openai.js +84 -84
- package/lib/mcp.js +197 -166
- package/lib/node-bin.js +99 -99
- package/lib/setup.js +470 -470
- package/lib/sse.js +51 -51
- package/lib/stall.js +30 -30
- package/lib/sweep.js +56 -56
- package/lib/vault-resolve.js +84 -84
- package/lib/vault.js +23 -6
- package/lib/watchdog.js +527 -392
- package/lib/web-assets.js +41 -0
- package/lib/web.js +316 -176
- package/package.json +12 -8
- package/scripts/release.mjs +204 -0
- package/skills/memory-recall/SKILL.md +14 -14
- package/web/app.js +12 -12
- package/web/index.html +46 -46
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// 记忆核心 · 插件自带前端资源(web/ 目录)的完整性判据。
|
|
2
|
+
//
|
|
3
|
+
// 为什么单独一个模块:**宿主启动自检**(index.js)与**独立 web 进程兜底**(lib/web.js)
|
|
4
|
+
// 必须用同一份判据和同一句措辞,否则两边会给出不一样的说法。
|
|
5
|
+
//
|
|
6
|
+
// 背景(issue #14):有人从插件市场装出来的副本里只有 package.json / lib/,`web/` 没落地
|
|
7
|
+
// (发布物本身是齐的:npm 0.10.0 tgz 与 tag v0.10.0 都含 web/index.html)。那时侧边栏
|
|
8
|
+
// 「记忆」的 iframe 只会拿到一页原始的 `{"ok":false,"error":"ENOENT ... web/index.html"}`,
|
|
9
|
+
// 用户和维护者都看不出「装坏了、该怎么修」。
|
|
10
|
+
|
|
11
|
+
import { existsSync } from 'node:fs'
|
|
12
|
+
import path from 'node:path'
|
|
13
|
+
|
|
14
|
+
/** 前端界面必需的静态资源(相对包的 web/ 目录)。 */
|
|
15
|
+
export const WEB_ASSET_FILES = ['index.html', 'app.js']
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 列出 web/ 下缺失的资源文件名;空数组 = 齐全。
|
|
19
|
+
* @param {string} packageRoot 插件包根目录(含 package.json 的那一层)
|
|
20
|
+
* @param {string[]} files 需要检查的文件名
|
|
21
|
+
*/
|
|
22
|
+
export function missingWebAssets(packageRoot, files = WEB_ASSET_FILES) {
|
|
23
|
+
return files.filter((f) => !existsSync(path.join(packageRoot, 'web', f)))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** 缺 app.js 是硬故障:界面彻底起不来(缺 index.html 还能用内置外壳兜底)。 */
|
|
27
|
+
export function isHardMissing(missing) {
|
|
28
|
+
return (missing || []).includes('app.js')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 写进「自动沉淀日志」的那句话(宿主与独立服务共用,保证两处措辞一致)。
|
|
33
|
+
* 顺带给出可照做的修法,避免用户再开一个 issue 问「怎么修」。
|
|
34
|
+
*/
|
|
35
|
+
export function missingAssetsReason(missing, packageRoot) {
|
|
36
|
+
const list = (missing || []).map((f) => `web/${f}`).join('、')
|
|
37
|
+
const where = path.join(packageRoot, 'web')
|
|
38
|
+
const hard = isHardMissing(missing)
|
|
39
|
+
return `插件安装不完整:缺少 ${list}(${where})——${hard ? '侧边栏「记忆」界面无法加载' : '已用内置外壳兜底,界面仍可用'}。`
|
|
40
|
+
+ `修法:完全退出 DSH → 删掉 ${path.join(path.dirname(packageRoot), 'memory-eternal')} → 重开 DSH 后「插件 → 添加插件」重新安装`
|
|
41
|
+
}
|
package/lib/web.js
CHANGED
|
@@ -1,176 +1,316 @@
|
|
|
1
|
-
// 记忆核心 · 独立 Web server(UI 唯一真源)。
|
|
2
|
-
//
|
|
3
|
-
// 职责:
|
|
4
|
-
// - 静态服务 web/index.html + web/app.js(自包含 bundle,react 打入)
|
|
5
|
-
// - 同源挂载 /memory-eternal/api/*(复用 lib/api.js,与 DSH 内完全同一实现)
|
|
6
|
-
// - iframe 友好:不设 X-Frame-Options(DSH 宿主内嵌加载本页)
|
|
7
|
-
//
|
|
8
|
-
// 常驻方式:ensureWebServer() 探活 → 未活则 detached spawn 独立进程
|
|
9
|
-
// (DSH 插件激活 / MCP server 启动 / CLI 调用都会走它,实现「默认开启」)。
|
|
10
|
-
//
|
|
11
|
-
// 直接运行:node lib/web.js --port 7999 [--vault <dir>]
|
|
12
|
-
|
|
13
|
-
import http from 'node:http'
|
|
14
|
-
import { promises as fs, readFileSync, watch } from 'node:fs'
|
|
15
|
-
import path from 'node:path'
|
|
16
|
-
import { fileURLToPath } from 'node:url'
|
|
17
|
-
import { createApi, API_PREFIX, encodeBody, broadcast } from './api.js'
|
|
18
|
-
import { defaultVaultDir, configFilePath } from './capture-run.js'
|
|
19
|
-
import { readCaptureLog, deriveHealth } from './capture-log.js'
|
|
20
|
-
import { nodeBinary, childEnv } from './node-bin.js'
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
1
|
+
// 记忆核心 · 独立 Web server(UI 唯一真源)。
|
|
2
|
+
//
|
|
3
|
+
// 职责:
|
|
4
|
+
// - 静态服务 web/index.html + web/app.js(自包含 bundle,react 打入)
|
|
5
|
+
// - 同源挂载 /memory-eternal/api/*(复用 lib/api.js,与 DSH 内完全同一实现)
|
|
6
|
+
// - iframe 友好:不设 X-Frame-Options(DSH 宿主内嵌加载本页)
|
|
7
|
+
//
|
|
8
|
+
// 常驻方式:ensureWebServer() 探活 → 未活则 detached spawn 独立进程
|
|
9
|
+
// (DSH 插件激活 / MCP server 启动 / CLI 调用都会走它,实现「默认开启」)。
|
|
10
|
+
//
|
|
11
|
+
// 直接运行:node lib/web.js --port 7999 [--vault <dir>]
|
|
12
|
+
|
|
13
|
+
import http from 'node:http'
|
|
14
|
+
import { promises as fs, readFileSync, watch } from 'node:fs'
|
|
15
|
+
import path from 'node:path'
|
|
16
|
+
import { fileURLToPath } from 'node:url'
|
|
17
|
+
import { createApi, API_PREFIX, encodeBody, broadcast } from './api.js'
|
|
18
|
+
import { defaultVaultDir, configFilePath } from './capture-run.js'
|
|
19
|
+
import { readCaptureLog, deriveHealth, appendCaptureLog } from './capture-log.js'
|
|
20
|
+
import { nodeBinary, childEnv } from './node-bin.js'
|
|
21
|
+
import { missingWebAssets, missingAssetsReason } from './web-assets.js'
|
|
22
|
+
|
|
23
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
24
|
+
const WEB_ROOT = path.join(__dirname, '..', 'web')
|
|
25
|
+
export const DEFAULT_WEB_PORT = Number(process.env.MEMORY_WEB_PORT) || 7999
|
|
26
|
+
const VERSION = (() => { try { return JSON.parse(readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8')).version } catch { return '' } })()
|
|
27
|
+
process.env.MEMORY_ETERNAL_VERSION = VERSION
|
|
28
|
+
|
|
29
|
+
const MIME = { '.html': 'text/html; charset=utf-8', '.js': 'application/javascript; charset=utf-8', '.css': 'text/css; charset=utf-8' }
|
|
30
|
+
|
|
31
|
+
// ---- 静态资源缺失兜底(issue #14)--------------------------------------------
|
|
32
|
+
// 装坏了的副本(package.json/lib 在、web/ 没落地)以前只会回一页原始
|
|
33
|
+
// `{"ok":false,"error":"ENOENT ..."}`。现在分两档:
|
|
34
|
+
// ① 缺 index.html 但 app.js 还在 → 用内置外壳把界面正常拉起来(自救);
|
|
35
|
+
// ② 缺 app.js(或两个都缺)→ 给一页人话诊断(缺哪个文件 / 绝对路径 / 版本 / 怎么重装)。
|
|
36
|
+
// 两种情况都写进自动沉淀日志:缺 app.js 记 fail(健康态亮红),只缺 index.html 记 warn。
|
|
37
|
+
const loggedMissingAssets = new Set()
|
|
38
|
+
|
|
39
|
+
const esc = (s) => String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]))
|
|
40
|
+
|
|
41
|
+
const BROKEN_CSS = `
|
|
42
|
+
:root { color-scheme: light dark; }
|
|
43
|
+
body { margin: 0; padding: 24px; font-family: system-ui, "Segoe UI", "Microsoft YaHei", sans-serif;
|
|
44
|
+
background: #f6f7f9; color: #1f2937; }
|
|
45
|
+
.me-broken { max-width: 720px; margin: 4vh auto; background: #fff; border: 1px solid #e5e7eb;
|
|
46
|
+
border-radius: 14px; padding: 22px 24px; line-height: 1.65; }
|
|
47
|
+
.me-broken h1 { margin: 0 0 10px; font-size: 18px; }
|
|
48
|
+
.me-broken h2 { margin: 18px 0 8px; font-size: 14px; }
|
|
49
|
+
.me-broken code { background: #f3f4f6; padding: 1px 5px; border-radius: 5px; font-size: 12.5px; }
|
|
50
|
+
.me-broken ul, .me-broken ol { margin: 6px 0; padding-left: 22px; }
|
|
51
|
+
.me-broken .me-dim { color: #6b7280; font-size: 12.5px; }
|
|
52
|
+
@media (prefers-color-scheme: dark) {
|
|
53
|
+
body { background: #111418; color: #e5e7eb; }
|
|
54
|
+
.me-broken { background: #1a1f26; border-color: #2c333c; }
|
|
55
|
+
.me-broken code { background: #22282f; }
|
|
56
|
+
.me-broken .me-dim { color: #9ca3af; }
|
|
57
|
+
}`
|
|
58
|
+
|
|
59
|
+
/** 诊断页正文(HTML 片段):内置外壳与 app.js 兜底共用,保证两处说法一致。 */
|
|
60
|
+
export function missingAssetsPanel({ missing = [], webRoot = '', version = '' } = {}) {
|
|
61
|
+
const items = missing.map((f) => `<li><code>web/${esc(f)}</code></li>`).join('')
|
|
62
|
+
return `<div class="me-broken">
|
|
63
|
+
<h1>记忆核心 · 插件安装不完整</h1>
|
|
64
|
+
<p>缺少下面这些界面资源,所以「记忆」打不开 —— 不是浏览器问题,也不是你的数据丢了:</p>
|
|
65
|
+
<ul>${items}</ul>
|
|
66
|
+
<p class="me-dim">插件目录:<code>${esc(webRoot)}</code>${version ? ` · 包版本 v${esc(version)}` : ''}</p>
|
|
67
|
+
<h2>怎么修</h2>
|
|
68
|
+
<ol>
|
|
69
|
+
<li>完全退出 DSH(桌面版请确认进程已结束);</li>
|
|
70
|
+
<li>删掉插件目录的上一级 <code>memory-eternal</code> 整个文件夹;</li>
|
|
71
|
+
<li>重开 DSH →「插件 → 添加插件」重新安装 <code>memory-eternal</code>(或填 <code>github:EternalNight996/memory-eternal</code>)。</li>
|
|
72
|
+
</ol>
|
|
73
|
+
<p class="me-dim">这条也会出现在「设置 → 记忆 → 用量/今日 → 自动沉淀日志」里。</p>
|
|
74
|
+
</div>`
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 缺 index.html 时回的内置外壳:app.js 还在就把界面正常拉起来(自救),
|
|
79
|
+
* 拉不起来(app.js 也缺)就把诊断页显示出来。
|
|
80
|
+
*/
|
|
81
|
+
export function missingIndexHtml({ missing = [], webRoot = '', version = '' } = {}) {
|
|
82
|
+
const panel = missingAssetsPanel({ missing, webRoot, version })
|
|
83
|
+
return `<!doctype html>
|
|
84
|
+
<html lang="zh-CN">
|
|
85
|
+
<head>
|
|
86
|
+
<meta charset="utf-8" />
|
|
87
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
88
|
+
<title>记忆核心 · Memory Eternal</title>
|
|
89
|
+
<style>${BROKEN_CSS}
|
|
90
|
+
html, body { height: 100%; }
|
|
91
|
+
#root { min-height: 100vh; }
|
|
92
|
+
</style>
|
|
93
|
+
</head>
|
|
94
|
+
<body>
|
|
95
|
+
<div id="root"></div>
|
|
96
|
+
<div id="me-broken-host" style="display:none">${panel}</div>
|
|
97
|
+
<script>
|
|
98
|
+
(function () {
|
|
99
|
+
var host = document.getElementById('me-broken-host')
|
|
100
|
+
var show = function () { host.style.display = 'block'; document.getElementById('root').appendChild(host) }
|
|
101
|
+
fetch('app.js', { method: 'HEAD' }).then(function (r) {
|
|
102
|
+
if (!r.ok) throw new Error('HTTP ' + r.status)
|
|
103
|
+
var s = document.createElement('script')
|
|
104
|
+
s.src = 'app.js'
|
|
105
|
+
s.onerror = show
|
|
106
|
+
document.body.appendChild(s)
|
|
107
|
+
}).catch(show)
|
|
108
|
+
})()
|
|
109
|
+
</script>
|
|
110
|
+
</body>
|
|
111
|
+
</html>`
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** 缺 app.js 时回的一段 JS:让已经加载好的页面把诊断信息画出来(而不是白屏或 raw JSON)。 */
|
|
115
|
+
export function missingAppJs({ missing = [], webRoot = '', version = '' } = {}) {
|
|
116
|
+
const panel = missingAssetsPanel({ missing, webRoot, version })
|
|
117
|
+
return `(function(){var r=document.getElementById('root')||document.body;r.innerHTML=${JSON.stringify(panel)};})()`
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** 记一条缺资源日志(每个文件每进程只记一次,避免每次请求刷屏)。 */
|
|
121
|
+
async function noteMissingAssets(missing, webRoot, packageRoot) {
|
|
122
|
+
for (const f of missing) {
|
|
123
|
+
const key = `${webRoot}|${f}`
|
|
124
|
+
if (loggedMissingAssets.has(key)) continue
|
|
125
|
+
loggedMissingAssets.add(key)
|
|
126
|
+
await appendCaptureLog({
|
|
127
|
+
time: Date.now(),
|
|
128
|
+
sessionId: 'system',
|
|
129
|
+
action: f === 'app.js' ? 'fail' : 'warn',
|
|
130
|
+
// 逐文件算措辞:只缺 index.html 时是「已用内置外壳兜底」,缺 app.js 才是「无法加载」
|
|
131
|
+
reason: missingAssetsReason([f], packageRoot),
|
|
132
|
+
}, process.env)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** 文件在不在(缺资源判定只看「读不到」这一种情况,不吞权限等其它错误)。 */
|
|
137
|
+
async function fileExists(file) {
|
|
138
|
+
try { await fs.access(file); return true } catch { return false }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** ENOENT / ENOTDIR 才算「资源缺失」,其它 IO 错误照旧走 500(不要把权限问题说成装坏了)。 */
|
|
142
|
+
function isMissingFile(error) {
|
|
143
|
+
return error?.code === 'ENOENT' || error?.code === 'ENOTDIR'
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function startWebServer({ port = DEFAULT_WEB_PORT, vaultRoot = defaultVaultDir(), host = '127.0.0.1', webRoot = WEB_ROOT, packageRoot = path.join(__dirname, '..') } = {}) {
|
|
147
|
+
const handleApi = createApi({
|
|
148
|
+
vaultDir: () => vaultRoot,
|
|
149
|
+
vaultRoots: () => [{ name: '', root: vaultRoot }],
|
|
150
|
+
// 读 DSH 写入的共享配置文件 → web 端与 DSH 设置同步(不同步修复)
|
|
151
|
+
getSettings: () => { try { return JSON.parse(readFileSync(configFilePath(process.env), 'utf8')) } catch { return {} } },
|
|
152
|
+
getDshInfo: () => ({ name: 'deepseek-harness', label: 'DeepSeek Harness(当前宿主)', installed: true, memoryRecallTool: true, autoCapture: true, autoRecall: true, vaultDir: vaultRoot, version: VERSION }),
|
|
153
|
+
// 独立 Web 页也能看「自动沉淀日志」:读宿主写的 JSONL(原来这里只能显示"不可用")
|
|
154
|
+
getCaptureLog: () => readCaptureLog(100, process.env),
|
|
155
|
+
getCaptureHealth: async () => deriveHealth(await readCaptureLog(50, process.env)),
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
// 共享配置文件变化 → 即时推给已打开的独立页(DSH 端一保存,这边不用刷新就能看到)
|
|
159
|
+
try {
|
|
160
|
+
// 监听**文件本身**而不是父目录(同 lib/config-sync.js 的说明):
|
|
161
|
+
// 父目录被删除时 Windows 上 libuv 会断言崩溃,监听文件只会得到 ENOENT。
|
|
162
|
+
const cfgFile = configFilePath(process.env)
|
|
163
|
+
const w = watch(cfgFile, () => {
|
|
164
|
+
broadcast('config', { at: Date.now(), source: 'file' })
|
|
165
|
+
})
|
|
166
|
+
if (typeof w.unref === 'function') w.unref()
|
|
167
|
+
} catch { /* 监听失败不影响服务;客户端仍会在操作时重新拉取 */ }
|
|
168
|
+
|
|
169
|
+
const server = http.createServer(async (req, res) => {
|
|
170
|
+
try {
|
|
171
|
+
const url = new URL(req.url, 'http://localhost')
|
|
172
|
+
const p = url.pathname
|
|
173
|
+
if (p.startsWith(API_PREFIX)) return await handleApi(req, res)
|
|
174
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
175
|
+
res.writeHead(405, { 'Content-Type': 'application/json' })
|
|
176
|
+
return res.end(JSON.stringify({ ok: false, error: 'method not allowed' }))
|
|
177
|
+
}
|
|
178
|
+
if (p === '/' || p === '/index.html') {
|
|
179
|
+
try {
|
|
180
|
+
const buf = await fs.readFile(path.join(webRoot, 'index.html'))
|
|
181
|
+
res.writeHead(200, { 'Content-Type': MIME['.html'], 'Cache-Control': 'no-store' })
|
|
182
|
+
return res.end(buf)
|
|
183
|
+
} catch (error) {
|
|
184
|
+
if (!isMissingFile(error)) throw error
|
|
185
|
+
// 缺 index.html:app.js 还在就自救(内置外壳把 UI 拉起来),否则连它一起报出来
|
|
186
|
+
const appOk = await fileExists(path.join(webRoot, 'app.js'))
|
|
187
|
+
const missing = appOk ? ['index.html'] : ['index.html', 'app.js']
|
|
188
|
+
await noteMissingAssets(missing, webRoot, packageRoot)
|
|
189
|
+
res.writeHead(200, { 'Content-Type': MIME['.html'], 'Cache-Control': 'no-store' })
|
|
190
|
+
return res.end(missingIndexHtml({ missing, webRoot, version: VERSION }))
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (p === '/app.js') {
|
|
194
|
+
let buf
|
|
195
|
+
try {
|
|
196
|
+
buf = await fs.readFile(path.join(webRoot, 'app.js'))
|
|
197
|
+
} catch (error) {
|
|
198
|
+
if (!isMissingFile(error)) throw error
|
|
199
|
+
// 回一段 JS 而不是 raw JSON:页面里能看到人话诊断
|
|
200
|
+
await noteMissingAssets(['app.js'], webRoot, packageRoot)
|
|
201
|
+
buf = Buffer.from(missingAppJs({ missing: ['app.js'], webRoot, version: VERSION }), 'utf8')
|
|
202
|
+
}
|
|
203
|
+
// 自包含 bundle 约 270KB,每次打开独立 Web 页都要重下 —— 协商 gzip 时压缩传输
|
|
204
|
+
const headers = { 'Content-Type': MIME['.js'], 'Cache-Control': 'no-store', 'Vary': 'Accept-Encoding' }
|
|
205
|
+
const { body: out, encoding } = encodeBody(res, buf)
|
|
206
|
+
if (encoding) headers['Content-Encoding'] = encoding
|
|
207
|
+
headers['Content-Length'] = out.length
|
|
208
|
+
res.writeHead(200, headers)
|
|
209
|
+
return res.end(out)
|
|
210
|
+
}
|
|
211
|
+
res.writeHead(404, { 'Content-Type': 'application/json' })
|
|
212
|
+
res.end(JSON.stringify({ ok: false, error: 'not found' }))
|
|
213
|
+
} catch (error) {
|
|
214
|
+
if (!res.headersSent) res.writeHead(500, { 'Content-Type': 'application/json' })
|
|
215
|
+
res.end(JSON.stringify({ ok: false, error: String(error?.message || error) }))
|
|
216
|
+
}
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
return new Promise((resolve, reject) => {
|
|
220
|
+
server.once('error', reject)
|
|
221
|
+
server.listen(port, host, () => {
|
|
222
|
+
const missing = missingWebAssets(packageRoot)
|
|
223
|
+
if (missing.length) {
|
|
224
|
+
// 启动即暴露(别等用户点开「记忆」才发现):消息与自动沉淀日志里那句一致
|
|
225
|
+
process.stderr.write(`[memory-eternal] ${missingAssetsReason(missing, packageRoot)}\n`)
|
|
226
|
+
void noteMissingAssets(missing, webRoot, packageRoot)
|
|
227
|
+
}
|
|
228
|
+
process.stderr.write(`[memory-eternal] web server on http://${host}:${port} (vault: ${vaultRoot}, web: ${webRoot})\n`)
|
|
229
|
+
resolve({ server, port, host, url: `http://${host}:${port}`, webRoot, missingAssets: missing })
|
|
230
|
+
})
|
|
231
|
+
})
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** 探测某端口是否已是本项目的 web server(响应 /api/overview 且带 vaultDir 标记)。 */
|
|
235
|
+
export async function probeWebServer(port = DEFAULT_WEB_PORT, timeoutMs = 800) {
|
|
236
|
+
try {
|
|
237
|
+
const res = await fetch(`http://127.0.0.1:${port}${API_PREFIX}/overview`, { signal: AbortSignal.timeout(timeoutMs) })
|
|
238
|
+
if (!res.ok) return null
|
|
239
|
+
const data = await res.json()
|
|
240
|
+
if (data && data.ok === true && typeof data.vaultDir === 'string') return data
|
|
241
|
+
return null
|
|
242
|
+
} catch {
|
|
243
|
+
return null
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* 确保 web server 存活(默认开启的实现核心)。
|
|
249
|
+
* 已活 → 直接返回 URL;未活 → detached spawn 独立进程并轮询探活。
|
|
250
|
+
* 端口被非本项目进程占用时向后漂移(+1..+10)。
|
|
251
|
+
* @returns {Promise<{url:string, port:number, spawned:boolean}>}
|
|
252
|
+
*/
|
|
253
|
+
export async function ensureWebServer({ port = DEFAULT_WEB_PORT, vaultRoot = defaultVaultDir(), spawnEnv = {}, totalTimeoutMs = 15000 } = {}) {
|
|
254
|
+
// 探测可用端口:先找「已是我们的服务」的端口,再找空闲端口
|
|
255
|
+
let target = port
|
|
256
|
+
let found = null
|
|
257
|
+
for (let p = port; p < port + 10; p++) {
|
|
258
|
+
const alive = await probeWebServer(p)
|
|
259
|
+
if (alive) { found = { url: `http://127.0.0.1:${p}`, port: p, spawned: false }; break }
|
|
260
|
+
// 该端口不是我们的服务:若连不上(空闲)就用它;连得上但不是我们的(被占用)则漂移
|
|
261
|
+
const isFree = await probeWebServer(p, 300) === null && await isPortConnectable(p) === false
|
|
262
|
+
if (isFree) { target = p; break }
|
|
263
|
+
target = p + 1
|
|
264
|
+
}
|
|
265
|
+
if (found) return found
|
|
266
|
+
|
|
267
|
+
// detached spawn:独立进程,父进程退出不影响(常驻);stdio ignore 防阻塞。
|
|
268
|
+
// 注意用 nodeBinary() 而不是 process.execPath:在 Electron 宿主里后者是
|
|
269
|
+
// Electron 主程序,spawn 出来不会执行 web.js(见 lib/node-bin.js)。
|
|
270
|
+
const { spawn } = await import('node:child_process')
|
|
271
|
+
const child = spawn(
|
|
272
|
+
nodeBinary(),
|
|
273
|
+
[path.join(__dirname, 'web.js'), '--port', String(target), '--vault', vaultRoot],
|
|
274
|
+
{ detached: true, stdio: 'ignore', env: childEnv({ ...spawnEnv, MEMORY_VAULT_DIR: vaultRoot }), windowsHide: true },
|
|
275
|
+
)
|
|
276
|
+
// spawn 失败(ENOENT/EACCES/...)默认是静默的,这里显式暴露出来
|
|
277
|
+
child.on('error', (error) => {
|
|
278
|
+
process.stderr.write(`[memory-eternal] web server spawn failed (${nodeBinary()}): ${error?.message || error}\n`)
|
|
279
|
+
})
|
|
280
|
+
child.unref()
|
|
281
|
+
|
|
282
|
+
const deadline = Date.now() + totalTimeoutMs
|
|
283
|
+
while (Date.now() < deadline) {
|
|
284
|
+
await new Promise((r) => setTimeout(r, 400))
|
|
285
|
+
const alive = await probeWebServer(target)
|
|
286
|
+
if (alive) return { url: `http://127.0.0.1:${target}`, port: target, spawned: true }
|
|
287
|
+
}
|
|
288
|
+
throw new Error(`[memory-eternal] web server 未能在 ${totalTimeoutMs}ms 内就绪 (port ${target})`)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async function isPortConnectable(port) {
|
|
292
|
+
const net = await import('node:net')
|
|
293
|
+
return new Promise((resolve) => {
|
|
294
|
+
const s = new net.Socket()
|
|
295
|
+
s.setTimeout(300)
|
|
296
|
+
s.once('connect', () => { s.destroy(); resolve(true) })
|
|
297
|
+
s.once('timeout', () => { s.destroy(); resolve(false) })
|
|
298
|
+
s.once('error', () => resolve(false))
|
|
299
|
+
s.connect(port, '127.0.0.1')
|
|
300
|
+
})
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// -- 脚本入口 -----------------------------------------------------------------
|
|
304
|
+
const argv = process.argv.slice(2)
|
|
305
|
+
const argOf = (name) => {
|
|
306
|
+
const i = argv.indexOf(name)
|
|
307
|
+
return i >= 0 && i + 1 < argv.length ? argv[i + 1] : null
|
|
308
|
+
}
|
|
309
|
+
if (process.argv[1] && /[\\/]web\.js$/.test(process.argv[1])) {
|
|
310
|
+
const port = Number(argOf('--port')) || DEFAULT_WEB_PORT
|
|
311
|
+
const vault = argOf('--vault') || defaultVaultDir()
|
|
312
|
+
startWebServer({ port, vaultRoot: path.resolve(vault) })
|
|
313
|
+
// 常驻:不主动退出;Ctrl+C / 进程被杀即停
|
|
314
|
+
process.on('SIGINT', () => process.exit(0))
|
|
315
|
+
process.on('SIGTERM', () => process.exit(0))
|
|
316
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memory-eternal",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.2",
|
|
4
4
|
"description": "记忆核心(Memory Eternal):自研的 DeepSeek Harness 记忆插件,不移植任何既有记忆框架——对话结束后自动沉淀知识卡到本地 Markdown Vault(自研去重、自研 CJK 检索、可 git 管理),设置页提供图形化知识库(统计 / 搜索 / 知识图谱 + 侧边栏一键弹窗),Agent 通过 memory_recall 工具按需召回历史上下文。零人工干预。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -27,6 +27,8 @@
|
|
|
27
27
|
"index.js",
|
|
28
28
|
"lib",
|
|
29
29
|
"bin",
|
|
30
|
+
"scripts",
|
|
31
|
+
"examples",
|
|
30
32
|
"web",
|
|
31
33
|
"hooks",
|
|
32
34
|
"skills",
|
|
@@ -35,14 +37,13 @@
|
|
|
35
37
|
".cursor-plugin",
|
|
36
38
|
".mcp.json",
|
|
37
39
|
"cordis.patch.yml",
|
|
38
|
-
"examples",
|
|
39
40
|
"README.md",
|
|
40
41
|
"README.en.md",
|
|
41
42
|
"LICENSE"
|
|
42
43
|
],
|
|
43
44
|
"engines": {
|
|
44
45
|
"node": ">=22",
|
|
45
|
-
"dsh": ">=0.1.5-alpha.2 <0.
|
|
46
|
+
"dsh": ">=0.1.5-alpha.2 <0.3.0-0"
|
|
46
47
|
},
|
|
47
48
|
"keywords": [
|
|
48
49
|
"deepseek-harness",
|
|
@@ -61,15 +62,17 @@
|
|
|
61
62
|
"build": "node build.mjs",
|
|
62
63
|
"build:client": "node build.mjs",
|
|
63
64
|
"prepublishOnly": "node build.mjs",
|
|
64
|
-
"
|
|
65
|
+
"release": "node scripts/release.mjs",
|
|
66
|
+
"release:dry": "node scripts/release.mjs --dry-run",
|
|
67
|
+
"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/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 && 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"
|
|
65
68
|
},
|
|
66
69
|
"dependencies": {
|
|
67
70
|
"@deepseek-ai/schemastery": "^3.18.1"
|
|
68
71
|
},
|
|
69
72
|
"peerDependencies": {
|
|
70
73
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
71
|
-
"@deepseek-ai/dsh-llm": ">=0.1.0-rc.2 <0.
|
|
72
|
-
"@deepseek-ai/dsh-tools": ">=0.1.0-rc.2 <0.
|
|
74
|
+
"@deepseek-ai/dsh-llm": ">=0.1.0-rc.2 <0.3.0-0",
|
|
75
|
+
"@deepseek-ai/dsh-tools": ">=0.1.0-rc.2 <0.3.0-0"
|
|
73
76
|
},
|
|
74
77
|
"devDependencies": {
|
|
75
78
|
"@types/react": "^18.3.1",
|
|
@@ -115,12 +118,13 @@
|
|
|
115
118
|
"notes": "Only `prepare` and `prepublishOnly` run `node build.mjs` (esbuild client bundling). No preinstall/install/postinstall hooks, no remote downloads, no native builds."
|
|
116
119
|
},
|
|
117
120
|
"compatibility": {
|
|
118
|
-
"dsh": ">=0.1.5-alpha.2 <0.
|
|
121
|
+
"dsh": ">=0.1.5-alpha.2 <0.3.0-0",
|
|
119
122
|
"dshReleases": {
|
|
120
123
|
"0.1.5-alpha.2": "compatible",
|
|
121
124
|
"0.1.5-rc.1": "compatible",
|
|
122
125
|
"0.1.5-rc.2": "compatible",
|
|
123
|
-
"0.1.7-rc.2": "compatible"
|
|
126
|
+
"0.1.7-rc.2": "compatible",
|
|
127
|
+
"0.2.0-rc.2": "compatible"
|
|
124
128
|
},
|
|
125
129
|
"node": ">=22",
|
|
126
130
|
"profiles": [
|