memory-eternal 0.9.5 → 0.9.7

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.
@@ -0,0 +1,99 @@
1
+ // 记忆核心 · 子进程用的 node 可执行文件解析
2
+ //
3
+ // 背景(真实故障):
4
+ // 在 Electron 宿主里(例如 DSH Desktop),`process.execPath` 指向的是
5
+ // Electron 主程序,而不是 node。此前 `ensureWebServer()` / watchdog / 插件的
6
+ // 保活逻辑都用 `spawn(process.execPath, ['.../web.js', ...])` 拉起子进程,
7
+ // 于是在 Electron 宿主下 spawn 出来的是「第二个 Electron 实例」:
8
+ // 它不会执行 web.js,而是立刻退出(或被单实例锁挡掉),而调用方用的是
9
+ // `stdio: 'ignore'` + `.catch(() => {})`,失败被彻底吞掉——表现就是侧边栏
10
+ // 「记忆」弹窗里那个指向 127.0.0.1:7999 的 iframe 永远白屏。
11
+ //
12
+ // 本模块把「找出一个真正能跑 node 脚本的子进程入口」收敛到一处:
13
+ // 1. MEMORY_ETERNAL_NODE 显式指定(逃生口,便于测试与非常规宿主);
14
+ // 2. 非 Electron 宿主:process.execPath 本身就是 node,直接用;
15
+ // 3. Electron 宿主:优先从 PATH 找真正的 node;
16
+ // 4. 都找不到时退回 process.execPath,并由 childEnv() 打开
17
+ // ELECTRON_RUN_AS_NODE,让 Electron 二进制以纯 node 模式运行。
18
+
19
+ import fs from 'node:fs'
20
+ import path from 'node:path'
21
+
22
+ let cachedBinary = null
23
+ let cachedSource = null
24
+
25
+ /** 在 PATH 中查找真正的 node 可执行文件;找不到返回 null。 */
26
+ function findNodeOnPath() {
27
+ const names = process.platform === 'win32' ? ['node.exe', 'node.cmd', 'node'] : ['node', 'node.exe']
28
+ const dirs = String(process.env.PATH || '').split(path.delimiter)
29
+ for (const dir of dirs) {
30
+ if (!dir) continue
31
+ for (const name of names) {
32
+ const candidate = path.join(dir, name)
33
+ try {
34
+ if (fs.statSync(candidate).isFile()) return candidate
35
+ } catch {
36
+ // 目录不存在或无权访问:继续找下一个
37
+ }
38
+ }
39
+ }
40
+ return null
41
+ }
42
+
43
+ /**
44
+ * 返回用于 spawn 子进程(跑 .js 脚本)的可执行文件路径。
45
+ * @returns {string}
46
+ */
47
+ export function nodeBinary() {
48
+ if (cachedBinary) return cachedBinary
49
+
50
+ if (process.env.MEMORY_ETERNAL_NODE) {
51
+ cachedBinary = process.env.MEMORY_ETERNAL_NODE
52
+ cachedSource = 'env'
53
+ return cachedBinary
54
+ }
55
+
56
+ if (!process.versions.electron) {
57
+ cachedBinary = process.execPath
58
+ cachedSource = 'execPath'
59
+ return cachedBinary
60
+ }
61
+
62
+ const found = findNodeOnPath()
63
+ if (found) {
64
+ cachedBinary = found
65
+ cachedSource = 'path'
66
+ return cachedBinary
67
+ }
68
+
69
+ cachedBinary = process.execPath
70
+ cachedSource = 'electron-run-as-node'
71
+ return cachedBinary
72
+ }
73
+
74
+ /** 说明 nodeBinary() 最终选了哪条路径(诊断用,不参与逻辑)。 */
75
+ export function nodeBinarySource() {
76
+ nodeBinary()
77
+ return cachedSource
78
+ }
79
+
80
+ /**
81
+ * 子进程环境:始终打开 ELECTRON_RUN_AS_NODE。
82
+ * 真正的 node 会忽略这个变量,所以无条件设置是安全的;而当 nodeBinary()
83
+ * 只能退回 Electron 二进制时,它就是让子进程以 node 模式运行的关键。
84
+ * @param {Record<string, string>} [extra]
85
+ */
86
+ export function childEnv(extra) {
87
+ return { ...process.env, ...(extra || {}), ELECTRON_RUN_AS_NODE: '1' }
88
+ }
89
+
90
+ /** 是否只能依赖 Electron 二进制的 node 模式(PATH 上没有真 node)。 */
91
+ export function usesElectronBinary() {
92
+ return nodeBinarySource() === 'electron-run-as-node'
93
+ }
94
+
95
+ /** 仅测试用:清掉进程内缓存。 */
96
+ export function resetNodeBinaryCache() {
97
+ cachedBinary = null
98
+ cachedSource = null
99
+ }
package/lib/watchdog.js CHANGED
@@ -19,6 +19,7 @@ import { spawn } from 'node:child_process'
19
19
  import path from 'node:path'
20
20
  import { fileURLToPath } from 'node:url'
21
21
  import process from 'node:process'
22
+ import { nodeBinary, childEnv } from './node-bin.js'
22
23
 
23
24
  const __dirname = path.dirname(fileURLToPath(import.meta.url))
24
25
  const WEB_JS = path.join(__dirname, 'web.js')
@@ -93,13 +94,17 @@ export function startWatchdog({ port = 7999, vaultRoot, interval = 5000, maxRest
93
94
  const targetPort = await findFreePort(port)
94
95
  const args = [WEB_JS, '--port', String(targetPort)]
95
96
  if (vaultRoot) args.push('--vault', vaultRoot)
96
- log(`spawn web: node ${args.join(' ')}`)
97
- const c = spawn(process.execPath, args, {
97
+ log(`spawn web: ${nodeBinary()} ${args.join(' ')}`)
98
+ const c = spawn(nodeBinary(), args, {
98
99
  detached: true,
99
100
  stdio: 'ignore',
100
- env: { ...process.env, MEMORY_VAULT_DIR: vaultRoot || process.env.MEMORY_VAULT_DIR || '' },
101
+ env: childEnv({ MEMORY_VAULT_DIR: vaultRoot || process.env.MEMORY_VAULT_DIR || '' }),
101
102
  windowsHide: true,
102
103
  })
104
+ // spawn 失败默认静默,显式记录(Electron 宿主下曾因此完全无迹可查)
105
+ c.on('error', (error) => {
106
+ log(`web spawn failed (${nodeBinary()}): ${error?.message || error}`)
107
+ })
103
108
  c.on('exit', (code, sig) => {
104
109
  spawned.delete(c)
105
110
  if (!stopped && code !== 0) log(`web exited code=${code} sig=${sig}`)
package/lib/web.js CHANGED
@@ -17,6 +17,7 @@ import { fileURLToPath } from 'node:url'
17
17
  import { createApi, API_PREFIX, encodeBody } from './api.js'
18
18
  import { defaultVaultDir, configFilePath } from './capture-run.js'
19
19
  import { readCaptureLog, deriveHealth } from './capture-log.js'
20
+ import { nodeBinary, childEnv } from './node-bin.js'
20
21
 
21
22
  const __dirname = path.dirname(fileURLToPath(import.meta.url))
22
23
  const WEB_ROOT = path.join(__dirname, '..', 'web')
@@ -112,13 +113,19 @@ export async function ensureWebServer({ port = DEFAULT_WEB_PORT, vaultRoot = def
112
113
  }
113
114
  if (found) return found
114
115
 
115
- // detached spawn:独立进程,父进程退出不影响(常驻);stdio ignore 防阻塞
116
+ // detached spawn:独立进程,父进程退出不影响(常驻);stdio ignore 防阻塞。
117
+ // 注意用 nodeBinary() 而不是 process.execPath:在 Electron 宿主里后者是
118
+ // Electron 主程序,spawn 出来不会执行 web.js(见 lib/node-bin.js)。
116
119
  const { spawn } = await import('node:child_process')
117
120
  const child = spawn(
118
- process.execPath,
121
+ nodeBinary(),
119
122
  [path.join(__dirname, 'web.js'), '--port', String(target), '--vault', vaultRoot],
120
- { detached: true, stdio: 'ignore', env: { ...process.env, ...spawnEnv, MEMORY_VAULT_DIR: vaultRoot }, windowsHide: true },
123
+ { detached: true, stdio: 'ignore', env: childEnv({ ...spawnEnv, MEMORY_VAULT_DIR: vaultRoot }), windowsHide: true },
121
124
  )
125
+ // spawn 失败(ENOENT/EACCES/...)默认是静默的,这里显式暴露出来
126
+ child.on('error', (error) => {
127
+ process.stderr.write(`[memory-eternal] web server spawn failed (${nodeBinary()}): ${error?.message || error}\n`)
128
+ })
122
129
  child.unref()
123
130
 
124
131
  const deadline = Date.now() + totalTimeoutMs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memory-eternal",
3
- "version": "0.9.5",
3
+ "version": "0.9.7",
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/api.test.mjs && node tests/settings-compat.test.mjs && node tests/ssr.test.mjs"
63
+ "test": "node build.mjs && node tests/vault.test.mjs && node tests/capture.test.mjs && node tests/api.test.mjs && node tests/settings-compat.test.mjs && node tests/node-bin.test.mjs && node tests/markdown.test.mjs && node tests/ssr.test.mjs"
64
64
  },
65
65
  "dependencies": {
66
66
  "@deepseek-ai/schemastery": "^3.18.1"
@@ -89,7 +89,8 @@
89
89
  },
90
90
  "marketplace": {
91
91
  "profiles": [
92
- "web"
92
+ "web",
93
+ "desktop"
93
94
  ],
94
95
  "requiresBuildApproval": false,
95
96
  "requiresRestart": true,
@@ -122,7 +123,8 @@
122
123
  },
123
124
  "node": ">=22",
124
125
  "profiles": [
125
- "web"
126
+ "web",
127
+ "desktop"
126
128
  ]
127
129
  }
128
130
  }