dsh-memory-eternal 0.5.4 → 0.5.5

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/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # 🧠 最强记忆大脑 dsh-memory-eternal — 给 DeepSeek Harness 装上「第二大脑」
1
+ # 🧠 最强记忆大脑 dsh-memory-eternal — 给 DeepSeek Harness 装上「第二大脑」
2
2
 
3
3
  <p align="center">
4
4
  <img src="https://img.shields.io/badge/DeepSeek%20Harness-plugin-3B82F6" alt="DSH plugin" />
@@ -108,6 +108,19 @@ async function main() {
108
108
  startMcpServer()
109
109
  return
110
110
  }
111
+ case 'watchdog': {
112
+ const { startWatchdog } = await import('../lib/watchdog.js')
113
+ const { defaultVaultDir } = await import('../lib/capture-run.js')
114
+ const port = Number(argOf('--port')) || 7999
115
+ const interval = Number(argOf('--interval')) || 5000
116
+ const maxRestart = Number(argOf('--max-restart')) || 10
117
+ const vaultRoot = argOf('--vault') ? path.resolve(argOf('--vault')) : defaultVaultDir()
118
+ const autoStart = !has('--no-restart')
119
+ console.log(`dsh-memory watchdog 启动 → 目标端口 ${port},间隔 ${interval}ms,最大重拉 ${maxRestart} 次${autoStart ? '' : '(--no-restart)'}`)
120
+ console.log(`vault: ${vaultRoot}\n按 Ctrl+C 退出`)
121
+ startWatchdog({ port, interval, maxRestart, vaultRoot, autoStart })
122
+ return
123
+ }
111
124
  case 'setup': {
112
125
  const { runSetup } = await import('../lib/setup.js')
113
126
  const only = []
@@ -136,13 +149,14 @@ async function main() {
136
149
  console.log(`dsh-memory — 记忆核心 CLI
137
150
 
138
151
  用法:
139
- dsh-memory recall <query> [--limit N] 检索知识卡
140
- dsh-memory capture <text | -> [--source T] 手动沉淀(- 读 stdin)
141
- dsh-memory serve [--port N] Web UI(前台)
142
- dsh-memory open 确保 Web 存活并打开浏览器
143
- dsh-memory mcp MCP stdio server
144
- dsh-memory setup [--dry-run] ... 自动挂载 MCP 到已装 agent
145
- dsh-memory sweep <dir> 挖掘会话 JSONL
152
+ dsh-memory recall <query> [--limit N] 检索知识卡
153
+ dsh-memory capture <text | -> [--source T] 手动沉淀(- 读 stdin)
154
+ dsh-memory serve [--port N] Web UI(前台)
155
+ dsh-memory open 确保 Web 存活并打开浏览器
156
+ dsh-memory mcp MCP stdio server
157
+ dsh-memory setup [--dry-run] ... 自动挂载 MCP 到已装 agent
158
+ dsh-memory sweep <dir> 挖掘会话 JSONL
159
+ dsh-memory watchdog [--port N] [--interval MS] [--max-restart N] 看门狗保活 web server
146
160
 
147
161
  环境变量:MEMORY_VAULT_DIR / MEMORY_LLM_BASE_URL / MEMORY_LLM_KEY / MEMORY_LLM_MODEL`)
148
162
  }
@@ -0,0 +1,201 @@
1
+ // 记忆核心 · 看门狗(独立进程保活 lib/web.js)。
2
+ //
3
+ // 用法(经 CLI):dsh-memory watchdog [--port 7999] [--interval 5] [--max-restart 10]
4
+ //
5
+ // 行为:
6
+ // - 启动时探测端口:活则只监督不接管;死则 spawn 新 web server
7
+ // - 周期性探活:HTTP GET /memory-eternal/api/overview(不是我们服务视为死)
8
+ // - 探到死:清理残留进程 → spawn 新 web server → restart 计数 +1
9
+ // - restart 超过 max-restart(默认 10)→ fatal 退出,不再保活
10
+ // - SIGINT/SIGTERM:优雅退出(停掉 watchdog 自己拉起的 web,但不动用户手动起的)
11
+ // - 所有状态输出到 stderr(看门狗 daemon 的标准约定)
12
+ //
13
+ // 独立性:watchdog 完全独立于 DSH 宿主——即便旧版插件(无 autoWeb)跑的
14
+ // DSH 环境,watchdog 也能拉起并保活 web server,让浏览器始终可达。
15
+
16
+ import http from 'node:http'
17
+ import net from 'node:net'
18
+ import { spawn } from 'node:child_process'
19
+ import path from 'node:path'
20
+ import { fileURLToPath } from 'node:url'
21
+ import process from 'node:process'
22
+
23
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
24
+ const WEB_JS = path.join(__dirname, 'web.js')
25
+ const API_PREFIX = '/memory-eternal/api'
26
+
27
+ const ts = () => new Date().toISOString().slice(11, 19) // HH:MM:SS
28
+ const log = (...args) => process.stderr.write(`[${ts()}] [watchdog] ${args.join(' ')}\n`)
29
+
30
+ /** 探测某端口是否在响应 + 是我们的服务(响应含 vaultDir 标记)。 */
31
+ export async function probe(port, timeoutMs = 1000) {
32
+ return new Promise((resolve) => {
33
+ const req = http.request({ host: '127.0.0.1', port, path: `${API_PREFIX}/overview`, method: 'GET', timeout: timeoutMs }, (res) => {
34
+ if (res.statusCode !== 200) return resolve(null)
35
+ const chunks = []
36
+ res.on('data', (c) => chunks.push(c))
37
+ res.on('end', () => {
38
+ try {
39
+ const data = JSON.parse(Buffer.concat(chunks).toString('utf8'))
40
+ if (data && data.ok === true && typeof data.vaultDir === 'string') return resolve(data)
41
+ resolve(null)
42
+ } catch { resolve(null) }
43
+ })
44
+ res.on('error', () => resolve(null))
45
+ })
46
+ req.on('error', () => resolve(null))
47
+ req.on('timeout', () => { req.destroy(); resolve(null) })
48
+ req.end()
49
+ })
50
+ }
51
+
52
+ /** 探测端口是否有任何 TCP 监听(即便不是我们的服务也占用)。 */
53
+ export async function probeTcp(port, timeoutMs = 600) {
54
+ return new Promise((resolve) => {
55
+ const s = new net.Socket()
56
+ s.setTimeout(timeoutMs)
57
+ s.once('connect', () => { s.destroy(); resolve(true) })
58
+ s.once('timeout', () => { s.destroy(); resolve(false) })
59
+ s.once('error', () => resolve(false))
60
+ s.connect(port, '127.0.0.1')
61
+ })
62
+ }
63
+
64
+ /**
65
+ * 启动看门狗。返回对象含 .stop() 优雅退出。
66
+ * @param {object} opts
67
+ * @param {number} [opts.port=7999]
68
+ * @param {string} [opts.vaultRoot] // 默认 ~/.dsh/memory-vault
69
+ * @param {number} [opts.interval=5000]
70
+ * @param {number} [opts.maxRestart=10]
71
+ * @param {boolean} [opts.autoStart=true] // 首次探测到死时是否自动拉起
72
+ */
73
+ export function startWatchdog({ port = 7999, vaultRoot, interval = 5000, maxRestart = 10, autoStart = true } = {}) {
74
+ let child = null // 我们 spawn 的 web 进程
75
+ let ownedByUs = false
76
+ let restartCount = 0
77
+ let stopped = false
78
+ let timer = null
79
+
80
+ // 找空闲端口:port, port+1, ... 最多 +10
81
+ const findFreePort = async (start) => {
82
+ for (let p = start; p < start + 10; p++) {
83
+ if (await probeTcp(p) === false) return p
84
+ }
85
+ return start
86
+ }
87
+
88
+ const spawnWeb = async () => {
89
+ if (child && !child.killed) {
90
+ try { child.kill('SIGTERM') } catch {}
91
+ }
92
+ const targetPort = await findFreePort(port)
93
+ const args = [WEB_JS, '--port', String(targetPort)]
94
+ if (vaultRoot) args.push('--vault', vaultRoot)
95
+ log(`spawn web: node ${args.join(' ')}`)
96
+ const c = spawn(process.execPath, args, {
97
+ detached: true,
98
+ stdio: 'ignore',
99
+ env: { ...process.env, MEMORY_VAULT_DIR: vaultRoot || process.env.MEMORY_VAULT_DIR || '' },
100
+ windowsHide: true,
101
+ })
102
+ c.on('exit', (code, sig) => {
103
+ if (!stopped && code !== 0) log(`web exited code=${code} sig=${sig}`)
104
+ })
105
+ c.unref()
106
+ child = c
107
+ ownedByUs = true
108
+ return targetPort
109
+ }
110
+
111
+ const tick = async () => {
112
+ if (stopped) return
113
+ const alive = await probe(port)
114
+ if (alive) {
115
+ // 是我们的服务
116
+ if (!ownedByUs) {
117
+ log(`port ${port} 已存活(外部实例)→ 切换为保活模式(不接管)`)
118
+ ownedByUs = true // 至少逻辑上"知道"它活了
119
+ }
120
+ return
121
+ }
122
+ // 端口死
123
+ if (restartCount >= maxRestart) {
124
+ log(`restart 次数达上限 ${maxRestart} → 退出看门狗(请人工排查)`)
125
+ stop()
126
+ return
127
+ }
128
+ if (!autoStart) {
129
+ log(`port ${port} 已死,但 --no-restart 设置,跳过`)
130
+ return
131
+ }
132
+ restartCount += 1
133
+ log(`port ${port} 离线 → 第 ${restartCount} 次拉起 web server`)
134
+ try {
135
+ const newPort = await spawnWeb()
136
+ // 等待 ready
137
+ for (let i = 0; i < 30; i++) {
138
+ await new Promise((r) => setTimeout(r, 500))
139
+ const a = await probe(newPort)
140
+ if (a) {
141
+ log(`web 在 ${newPort} 就绪(vault: ${a.vaultDir})`)
142
+ if (newPort !== port) log(`提示:目标端口 ${port} 被占,watchdog 在 ${newPort} 上保活`)
143
+ return
144
+ }
145
+ }
146
+ log(`web 未能在 15s 内就绪`)
147
+ } catch (error) {
148
+ log(`spawn 失败:${error?.message || error}`)
149
+ }
150
+ }
151
+
152
+ const stop = () => {
153
+ if (stopped) return
154
+ stopped = true
155
+ if (timer) { clearInterval(timer); timer = null }
156
+ if (child && !child.killed) {
157
+ try { child.kill('SIGTERM') } catch {}
158
+ setTimeout(() => { try { child && child.kill('SIGKILL') } catch {} }, 1000).unref()
159
+ }
160
+ log('看门狗退出')
161
+ process.exit(0)
162
+ }
163
+
164
+ // 首次探测
165
+ ;(async () => {
166
+ const alive = await probe(port)
167
+ if (alive) {
168
+ log(`port ${port} 已活(外部实例),启动保活监督(间隔 ${interval}ms)`)
169
+ } else if (autoStart) {
170
+ log(`port ${port} 离线 → 首次拉起 web`)
171
+ restartCount += 1
172
+ try {
173
+ const np = await spawnWeb()
174
+ for (let i = 0; i < 30; i++) {
175
+ await new Promise((r) => setTimeout(r, 500))
176
+ if (await probe(np)) { log(`web 在 ${np} 就绪`); break }
177
+ }
178
+ } catch (e) { log(`首次拉起失败:${e?.message || e}`) }
179
+ }
180
+ timer = setInterval(tick, interval)
181
+ })()
182
+
183
+ process.on('SIGINT', stop)
184
+ process.on('SIGTERM', stop)
185
+ return { stop, get port() { return port }, get restartCount() { return restartCount } }
186
+ }
187
+
188
+ // 脚本入口:node lib/watchdog.js [--port N] [--interval MS] [--max-restart N] [--vault DIR]
189
+ if (process.argv[1] && /[\\/]watchdog\.js$/.test(process.argv[1])) {
190
+ const argv = process.argv.slice(2)
191
+ const argOf = (name) => {
192
+ const i = argv.indexOf(name)
193
+ return i >= 0 && i + 1 < argv.length ? argv[i + 1] : null
194
+ }
195
+ const port = Number(argOf('--port')) || 7999
196
+ const interval = Number(argOf('--interval')) || 5000
197
+ const maxRestart = Number(argOf('--max-restart')) || 10
198
+ const vaultRoot = argOf('--vault') || undefined
199
+ const autoStart = !argv.includes('--no-restart')
200
+ startWatchdog({ port, interval, maxRestart, vaultRoot, autoStart })
201
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-memory-eternal",
3
- "version": "0.5.4",
3
+ "version": "0.5.5",
4
4
  "description": "记忆核心(Memory Eternal):自研的 DeepSeek Harness 记忆插件,不移植任何既有记忆框架——对话结束后自动沉淀知识卡到本地 Markdown Vault(自研去重、自研 CJK 检索、可 git 管理),设置页提供图形化知识库(统计 / 搜索 / 知识图谱 + 侧边栏一键弹窗),Agent 通过 memory_recall 工具按需召回历史上下文。零人工干预。",
5
5
  "license": "MIT",
6
6
  "repository": {