memory-eternal 0.10.39 → 0.10.40
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 +3 -2
- package/README.md +3 -2
- package/bin/dsh-memory.mjs +7 -1
- package/lib/client.js +32 -1
- package/lib/watchdog.js +98 -3
- package/package.json +2 -2
- package/web/app.js +32 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://anthropic.com/claude-code/plugin.schema.json",
|
|
3
3
|
"name": "memory-eternal",
|
|
4
|
-
"version": "0.10.
|
|
4
|
+
"version": "0.10.40",
|
|
5
5
|
"description": "记忆核心(Memory Eternal):对话结束后自动沉淀知识卡到本地记忆库(~/.dsh/memory-vault),支持检索 / 召回 / 审核中心。",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "EternalNight996",
|
package/README.en.md
CHANGED
|
@@ -361,14 +361,15 @@ curl http://127.0.0.1:7999/memory-eternal/api/web-info # is the standalone we
|
|
|
361
361
|
|
|
362
362
|
| Version | Date | Highlights |
|
|
363
363
|
|---|---|---|
|
|
364
|
+
| **v0.10.40** | 2026-10-19 | **Follow-up to #45 (release-blocking): an enumeration timeout is no longer treated as an empty port, plus the macOS traffic-light overlap (#46)**. ① The reporter measured Get-NetTCPConnection at **4.1s to 61.4s** on that machine (huge TCP table) while takeover allows only 2.5s, so it timed out, returned empty output, became null, was read as "port freed", and the new watchdog could not bind 7999 and drifted to 8001 — **the old web survived forever and the 0.10.38 fix could never run**. Fix: on Windows we now try **netstat -ano first** (1.8s there and it already reports the pid; 403ms on this machine, pid and command line included) with PowerShell only as a fallback; portFree became **tri-state** (free / occupied / **unknown**) where a timeout means unknown, which **aborts the takeover** with an explicit log instead of drifting; status separates "response timed out" from "no service"; probe paths no longer leak sockets. ② **#46**: the macOS window buttons are an Electron **native traffic light** (titleBarStyle:hiddenInset) that no z-index or portal can cover, so the plugin now **follows the host safe area** --dsh-frame-overlay-top (68px on macOS, 20px in native fullscreen), keeping the overlay top edge at 68px while the scrim still covers the screen, so the close button no longer sits on top of the plugin UI. npm test now runs **389 checks** |
|
|
364
365
|
| **v0.10.39** | 2026-10-17 | **Wiring self-healing: codex TOML parsed per section, single quotes and escaping supported, plus a read-only startup check (items ②/④ of #44)**. ① Fixed a real bug: setup / align parsed ~/.codex/config.toml **without scoping to [mcp_servers.memory]**, so they picked up the first command = of another server (on this machine it read MCP_DOCKER docker.exe), and only double quotes were understood while codex itself writes single quotes — so the entry was **neither readable nor fixable, yet setup still printed "updated"**. Reading and writing now share pure helpers (section scoping, both quote styles, unescaping), the writer verifies the text really changed, is idempotent (already-aligned entries are not counted as changes) and honestly reports section unparseable when it cannot edit. ② A new read-only startup check staleWiringCheck() logs a warn in the auto-capture log when MCP/hooks reference a **non-existent** node path or an entry that differs from the current version (dsh-memory setup fixes it) — nobody runs align daily, so silent failures must become visible. ③ Verified locally: all three agent wirings are now ✓ in align (codex previously kept reporting "node path differs"). npm test now runs **379 checks** |
|
|
365
|
-
| **v0.10.38** | 2026-10-17 | **The claim is released once a takeover succeeds (fixing a 0.10.35 side effect), plus real-machine A/B numbers for #45**. In 0.10.35 the claim was never released on the path where the helper becomes the resident watchdog (that process never exits), so a restart within 60 seconds of a completed takeover was blocked as in-flight by itself (reproduced locally). bootWatchdog() is now immediately followed by releaseReplaceClaim(), making the mutual-exclusion window exactly "a takeover is in progress". **#45 verified on a real machine (same host, same batch of 20 /version-check calls)**: 0.10.36 gave 9 successes / **11 refusals**, TIME_WAIT 24 to **1976** (+1952, about 100 connections per request, i.e. the recursion depth) and a 2389 ms first frame; 0.10.37 gave **20/20 successes**, TIME_WAIT 1969 to **1971** (+2) and 18-34 ms. npm test now runs **376 checks** |
|
|
366
366
|
|
|
367
367
|
<details>
|
|
368
|
-
<summary>Older release notes (v0.10.
|
|
368
|
+
<summary>Older release notes (v0.10.38 and earlier · 65 releases, click to expand)</summary>
|
|
369
369
|
|
|
370
370
|
| Version | Date | Highlights |
|
|
371
371
|
|---|---|---|
|
|
372
|
+
| **v0.10.38** | 2026-10-17 | **The claim is released once a takeover succeeds (fixing a 0.10.35 side effect), plus real-machine A/B numbers for #45**. In 0.10.35 the claim was never released on the path where the helper becomes the resident watchdog (that process never exits), so a restart within 60 seconds of a completed takeover was blocked as in-flight by itself (reproduced locally). bootWatchdog() is now immediately followed by releaseReplaceClaim(), making the mutual-exclusion window exactly "a takeover is in progress". **#45 verified on a real machine (same host, same batch of 20 /version-check calls)**: 0.10.36 gave 9 successes / **11 refusals**, TIME_WAIT 24 to **1976** (+1952, about 100 connections per request, i.e. the recursion depth) and a 2389 ms first frame; 0.10.37 gave **20/20 successes**, TIME_WAIT 1969 to **1971** (+2) and 18-34 ms. npm test now runs **376 checks** |
|
|
372
373
|
| **v0.10.37** | 2026-10-17 | **Fixes #45 (a severe regression I introduced in 0.10.36: the self-probing recursion in /version-check, amplified by retries), #44 (hardcoded node paths / zombie-watchdog misjudgement) and #43 (the official route fields for follow-session)**. ① **#45**: selfPort() is this process own port, yet /version-check called probeServedVersion to **request /version-check from it** — handler to itself to handler, with depth bounded only by timeouts and a new connection per level, which saturated the listen backlog and produced **intermittent ECONNREFUSED (17–37ms)**, 4402 extra TIME_WAIT entries in 40 idle seconds and an empty served value; the 0.10.36 retry multiplied it by three. Self-probes now **answer locally** (probedSelf:true, no HTTP), /restart-self has the same gate, and /version-check accepts ?port= for other ports. ② **#44**: nodeBinary() now **verifies existence** before using a cached path and falls back to PATH when execPath vanished (brew upgrade removing the old directory), so long-lived watchdogs heal themselves; inspectResident gains portServing and decideResidentAction treats "the lock says a watchdog is alive but the port answers nobody" as restart (watchdog-port-dead) instead of delegate; status prints the actual node binary and whether it exists. ③ **#43**: sessionRouteFrom() is async and prefers the official fields, session.requestHeader().config then agent.options, with the old sniffing as a fallback. npm test now runs **375 checks** |
|
|
373
374
|
| **v0.10.36** | 2026-10-16 | **Both new states now surface at every entry point (align / doctor), plus one test-hygiene fix**. ① align gains a finding: **the resident instance is host-managed** (the lock is unwritable, so the watchdog path is bypassed and the host stops/starts the instance itself and keeps it alive) — **warn level**, stating explicitly that "the instance is managed, it is not an orphan, and it returns to the watchdog once the lock becomes writable" (align.ok is unaffected; warn is not a failure); ② dsh-memory doctor prints a new line **resident instance management:** (watchdog / host-managed / cannot replace (environment limit)), so together with journal mode, write probe, lock writability and the backup list you can see at a glance which state a machine is in; ③ **test hygiene**: the /restart-self alreadyCurrent case depended on the **developer machine real lock file** (my local lock held only an endedAt record, so it stopped reporting alreadyCurrent and the check went red) — reading the lock is now injectable (deps.readWatchdogLock) and the case injects a live registration, making it independent of the dev machine. npm test now runs **371 checks** |
|
|
374
375
|
| **v0.10.35** | 2026-10-16 | **Single-flight for takeover helpers (issue #30, sixth retest round: the host tick and the CLI each spawn a --replace and step on each other)**. The reporter observed that during a failed restart **two takeover helpers were alive on the same port without knowing each other** (parent = the DSH host / parent = the CLI): neither wrote the lock and each reaped the other web, making the scene even harder to diagnose. Fix: a new **claim file** (memory-eternal-replace.claim, next to the watchdog lock) — ① **a helper claims it exclusively before starting**; if it cannot, it **exits immediately** (yielding to whoever is already running) and releases it on exit; ② **when restartResident sees a takeover in flight** (holder alive and less than 60s old) it **does not spawn a second helper**, returning stage in-flight with who is running it and how long ago it started; ③ **fail-open**: in restricted environments where the claim cannot be written the takeover still proceeds (permission problems must not block takeover for good), but the reason is surfaced in the result and the log. Tests cover: first-come-first-served / a live holder makes the second yield / stale and dead-pid claims can be taken over / only your own claim is released / the in-flight branch of restartResident. npm test now runs **370 checks** |
|
package/README.md
CHANGED
|
@@ -393,14 +393,15 @@ curl http://127.0.0.1:7999/memory-eternal/api/web-info # 独立 web 是否活
|
|
|
393
393
|
|
|
394
394
|
| 版本 | 日期 | 关键改动 |
|
|
395
395
|
|---|---|---|
|
|
396
|
+
| **v0.10.40** | 2026-10-19 | **#45 后续(阻塞级):枚举超时不再被当成「端口空闲」+ macOS 红绿灯遮挡(#46)**。① 报告者实测:Get-NetTCPConnection 在该机要 **4.1–61.4 秒**(TCP 表大),而 takeover 只给 2.5s → 超时 → 空输出 → null → 「端口已空出」→ 新看门狗绑不上 7999、漂到 8001,**旧 web 永远活着、0.10.38 的修复永远轮不到执行**。修法:Windows 改**先走 netstat -ano**(实测 1.8s 且直接给 pid,本机复测 **403ms** 拿到 pid+命令行),PowerShell 只作兜底;portFree 升级成**三态**(free / occupied / **unknown**),枚举超时=未知 → **中止接管**并写明确日志(不再漂移);status 把「响应超时」与「没有服务」分开报;探测路径不再漏 socket。② **#46**:macOS 桌面版的窗口按钮是 Electron **原生 traffic light**(titleBarStyle:hiddenInset),任何 z-index/portal 都盖不住 —— 插件改为**跟随宿主安全区** --dsh-frame-overlay-top(macOS 68px,原生全屏宿主降到 20px),浮层顶边恒 68px、遮罩仍铺满全屏,不再被「关闭」按钮压住。npm test 共 **389 项** |
|
|
396
397
|
| **v0.10.39** | 2026-10-17 | **接线自愈:codex 的 TOML 按段解析 + 单引号/转义兼容 + 启动期只读体检(#44 第②④条)**。① 修了一个真 bug:setup / align 解析 ~/.codex/config.toml 时**没有限定 [mcp_servers.memory] 段**,会抓到别的 server 的第一个 command =(本机实测读成了 MCP_DOCKER 的 docker.exe);而且只认双引号,而 codex 自己写出来是单引号 → **既读不到也改不掉,却照样打印「已更新」**。现在读写共用纯函数(按段 → 单/双引号 → 反转义),**改完校验真的变了**,幂等(已是目标形态不算改动),改不了就如实报 section unparseable。② 新增启动期**只读**体检 staleWiringCheck():MCP/hooks 里若写着**已不存在**的 node 路径、或条目与当前版本不一致,就在自动沉淀日志里留一条 warn(dsh-memory setup 可修)—— 没人会天天跑 align,静默失效要变成可见。③ 本机验证:align 三端接线全部 ✓(修前 codex 一直报「node 路径与当前不一致」)。npm test 共 **379 项** |
|
|
397
|
-
| **v0.10.38** | 2026-10-17 | **接管成功即释放 claim(修 0.10.35 的副作用)+ #45 真机 A/B 验证数据**。0.10.35 的 claim 在「助手变成常驻看门狗」这条路径上永远不会被释放(那个进程不退出),于是接管完成后 60 秒内的下一次 restart 会被自己挡成 in-flight(本机实测踩到)。现在 bootWatchdog() 之后立刻 releaseReplaceClaim() —— 互斥窗口严格等于「接管进行中」。**#45 真机验证(同一台机、同一组 20 次 /version-check)**:0.10.36 → 9 成功 / **11 拒连**,TIME_WAIT 24→**1976**(+1952 ≈ 每请求 100 条连接 = 递归深度),首帧 2389ms;0.10.37 → **20/20 成功**,TIME_WAIT 1969→**1971**(+2),18–34ms。npm test 共 **376 项** |
|
|
398
398
|
|
|
399
399
|
<details>
|
|
400
|
-
<summary>更早的更新日志(v0.10.
|
|
400
|
+
<summary>更早的更新日志(v0.10.38 及以前 · 共 65 版,点开查看)</summary>
|
|
401
401
|
|
|
402
402
|
| 版本 | 日期 | 关键改动 |
|
|
403
403
|
|---|---|---|
|
|
404
|
+
| **v0.10.38** | 2026-10-17 | **接管成功即释放 claim(修 0.10.35 的副作用)+ #45 真机 A/B 验证数据**。0.10.35 的 claim 在「助手变成常驻看门狗」这条路径上永远不会被释放(那个进程不退出),于是接管完成后 60 秒内的下一次 restart 会被自己挡成 in-flight(本机实测踩到)。现在 bootWatchdog() 之后立刻 releaseReplaceClaim() —— 互斥窗口严格等于「接管进行中」。**#45 真机验证(同一台机、同一组 20 次 /version-check)**:0.10.36 → 9 成功 / **11 拒连**,TIME_WAIT 24→**1976**(+1952 ≈ 每请求 100 条连接 = 递归深度),首帧 2389ms;0.10.37 → **20/20 成功**,TIME_WAIT 1969→**1971**(+2),18–34ms。npm test 共 **376 项** |
|
|
404
405
|
| **v0.10.37** | 2026-10-17 | **修 #45(我 0.10.36 引入的严重回归:/version-check 自我探测递归被重试放大)+ #44(node 路径硬编码 / 僵尸看门狗误判)+ #43(跟随会话的官方字段)**。① **#45**:selfPort() 就是本进程自己的端口,而 /version-check 会用 probeServedVersion **再向它发一条 /version-check** → 处理器 → 自己 → 处理器…… 深度只受超时约束、每层各开一条连接 → listen backlog 打满 → **间歇 ECONNREFUSED(17–37ms)**、TIME_WAIT 静置 40 秒涨 4402、served 反成空串;0.10.36 的「重试 3 次」把它**乘以 3**。现在自探一律**直接作答**(probedSelf:true,不发 HTTP),/restart-self 同一道闸门;/version-check 也支持 ?port= 问别的端口。② **#44**:nodeBinary() 缓存命中前**验存在性**,execPath 失效(brew 升级删掉旧目录)时回退 PATH —— 长命 watchdog 从此自愈;inspectResident 新增 portServing,decideResidentAction 遇「登记说 watchdog 活着、端口却没人应答」判 restart(watchdog-port-dead)而**不再 delegate**(那正是「沉淀静默停摆、日志只写 delegated」的现场);status 打印实际用的 node 与存在性。③ **#43**:sessionRouteFrom() 改为 async,按官方字段优先取 session.requestHeader().config → agent.options → 旧嗅探兜底。npm test 共 **375 项** |
|
|
405
406
|
| **v0.10.36** | 2026-10-16 | **把两种新状态接到所有入口(align / doctor)+ 修一处测试卫生**。① align 新增一条结论:**常驻实例由宿主托管**(锁不可写 → 看门狗路径被绕过,改由宿主停旧起新并保活)—— **warn 级**,明确写「实例是有人管的,不是孤儿;锁恢复可写后自动交还看门狗」(align.ok 不受影响,warn 不算失败);② dsh-memory doctor 增印一行 **常驻实例托管:**(看门狗 / 宿主托管 / 不可替换(环境限制)),配合已有的 journal 模式、写探针、锁可写性、备份清单,一次看清这台机器处于哪种状态;③ **测试卫生**:/restart-self 的 alreadyCurrent 用例依赖**开发者本机真实的锁文件**(我本机锁里只剩一条 endedAt 记录,于是它不再判 alreadyCurrent、用例翻红)—— 把「读锁」做成可注入(deps.readWatchdogLock),用例自己注入一条活登记,从此与开发机状态无关。npm test 共 **371 项** |
|
|
406
407
|
| **v0.10.35** | 2026-10-16 | **接管助手单飞互斥(#30 第六轮实测:宿主 tick 与 CLI 各拉一个 --replace 互相踩)**。报告者观察到失败的 restart 期间同端口上**同时存在两个互不相识的接管助手**(父 = DSH 宿主 / 父 = CLI),谁都不写锁、还互相收对方的 web —— 现场更难复盘。修法:新增 **claim 文件**(memory-eternal-replace.claim,与看门狗锁同目录)—— ① **助手开工前独占抢 claim**,抢不到就**直接退出**(把接管让给已经在跑的那个),并在退出时释放;② **restartResident 发现接管进行中**(持有者活着且 <60s)就**不再拉第二个助手**,返回 stage「in-flight」并说明是谁在跑、多久前启动;③ **失败开放**:受限环境写不了 claim 时**照常接管**(不能让权限问题把接管彻底堵死),但把原因写进返回值 / 日志。测试覆盖:先到先得 / 活持有者让位 / 过期与死 pid 可接管 / 只清自己的 claim / restartResident 的 in-flight 分支。npm test 共 **370 项** |
|
package/bin/dsh-memory.mjs
CHANGED
|
@@ -145,7 +145,11 @@ async function main() {
|
|
|
145
145
|
// issue #23:锁里的 pkgVersion 是「看门狗自述」,与端口上真正跑着的代码无关 ——
|
|
146
146
|
// 升级后旧 web 仍占端口时它会骗人地报新版本。这里直接问端口上服务的那个进程。
|
|
147
147
|
const probePort = Number(port) || (st.watchdogs[0] && Number(st.watchdogs[0].port)) || 0
|
|
148
|
-
|
|
148
|
+
// issue #45 后续(报告者第 3 条):`超时` 与 `没有服务` 必须分开报。
|
|
149
|
+
// 那台机器上旧进程回一次 /version-check 要 8–15s,而这里默认预算 1200ms ——
|
|
150
|
+
// 旧文案把「超时」写成了「没有服务在响应」,排查时极易误判成服务挂了。
|
|
151
|
+
const detail = probePort ? await (await import('../lib/watchdog.js')).probeServedVersionDetailed(probePort) : { version: '', timedOut: false, lastError: '' }
|
|
152
|
+
const served = detail.version
|
|
149
153
|
const occupant = probePort && !served ? await findPortListener(probePort) : null
|
|
150
154
|
// 旧版(< v0.10.4)的 /overview 里**没有 version 字段**,所以探不到版本不等于「不是我们的服务」。
|
|
151
155
|
// 这正是 v0.10.3 → v0.10.4 升级时的常见现场,必须说清楚,否则用户会以为端口空着。
|
|
@@ -160,6 +164,8 @@ async function main() {
|
|
|
160
164
|
if (probePort) {
|
|
161
165
|
if (served) console.log(`端口 ${probePort} 实际服务版本:${served}`)
|
|
162
166
|
else if (oursNoVersion) console.log(`端口 ${probePort} 上跑的是本插件的 web(pid ${occupant.pid}),但它不会自报版本 —— 说明是 v0.10.4 之前的旧版,升级尚未生效 → 执行 dsh-memory restart --port ${probePort}`)
|
|
167
|
+
else if (detail.timedOut) console.log(`端口 ${probePort} 有进程在听,但**响应超时**(>${1200}ms,最后错误 ${detail.lastError || 'TIMEOUT'})—— 这不等于「没有服务」;可能是实例正忙/被旧版递归拖慢,稍后重试或 dsh-memory restart --port ${probePort}`)
|
|
168
|
+
else if (occupant && occupant.unknown) console.log(`端口 ${probePort} 的占用者**无法判定**(进程枚举超时/不可用)—— 未知,不代表空闲`)
|
|
163
169
|
else console.log(`端口 ${probePort} 上没有本插件的服务在响应${occupant && occupant.pid ? `(被 pid ${occupant.pid} 占用:${occupant.command || '命令行未知'})` : ''}`)
|
|
164
170
|
}
|
|
165
171
|
console.log('')
|
package/lib/client.js
CHANGED
|
@@ -298,7 +298,38 @@ Option A, because:
|
|
|
298
298
|
height: calc(100vh - var(--dsh-windows-titlebar-height, 0px));
|
|
299
299
|
max-height: calc(100vh - var(--dsh-windows-titlebar-height, 0px));
|
|
300
300
|
}
|
|
301
|
-
|
|
301
|
+
/* ---- macOS \u684C\u9762\u7248\uFF1A\u8BA9\u5F00\u7A97\u53E3\u5DE6\u4E0A\u89D2\u7684\u7EA2\u7EFF\u706F ----------------------------------------
|
|
302
|
+
\u5BBF\u4E3B\u5C42\u7684\u7ED3\u8BBA\uFF08issue #46\uFF09\uFF1AmacOS \u684C\u9762\u7248\u662F Electron \u7684 titleBarStyle: hiddenInset +
|
|
303
|
+
trafficLightPosition {x:16, y:18}\uFF0C\u300C\u5173\u95ED / \u6700\u5C0F\u5316 / \u7F29\u653E\u300D\u662F**\u539F\u751F NSWindow \u63A7\u4EF6**\uFF0C
|
|
304
|
+
\u7531\u64CD\u4F5C\u7CFB\u7EDF\u753B\u5728\u7F51\u9875\u5185\u5BB9\u4E4B\u4E0A\u3002\u5B83\u4E0D\u662F DOM \u2014\u2014 \u4EFB\u4F55 z-index\u3001portal\u3001\u9876\u5C42\u5C42\u7EA7\u90FD\u6392\u4E0D\u5230\u5B83\u524D\u9762\uFF0C
|
|
305
|
+
\u63D2\u4EF6\u80FD\u505A\u7684\u53EA\u6709\u300C\u8BA9\u5F00\u90A3\u5757\u77E9\u5F62\u300D\uFF0C\u76D6\u4F4F\u5B83\u662F\u505A\u4E0D\u5230\u7684\u3002
|
|
306
|
+
\u5BBF\u4E3B\u81EA\u5DF1\u5DF2\u7ECF\u628A\u5B89\u5168\u533A\u53D1\u5E03\u4E86\uFF08ui-layout \u7684 AppFrame \u5199\u5728 <html> \u4E0A\uFF09\uFF1A
|
|
307
|
+
\xB7 html[data-platform="darwin"] \u53EA\u7531\u684C\u9762\u7248 preload \u5199\u5165\uFF08\u666E\u901A\u6D4F\u89C8\u5668\u6587\u6863\u6CA1\u6709\u8FD9\u4E2A\u5C5E\u6027\uFF0C
|
|
308
|
+
\u6240\u4EE5\u4E0B\u9762\u7684\u89C4\u5219\u5728\u6D4F\u89C8\u5668 / Windows / Linux \u4E0A\u5B8C\u5168\u4E0D\u751F\u6548\uFF0C\u884C\u4E3A\u4E0E\u4E4B\u524D\u4E00\u81F4\uFF09\uFF1B
|
|
309
|
+
\xB7 --dsh-frame-overlay-top \u662F\u5BBF\u4E3B\u7ED9\u5BF9\u8BDD\u6846/\u83DC\u5355\u5B9A\u7684\u9876\u8FB9\u8BA9\u4F4D\u91CF\uFF08macOS = \u9876\u5E26 48px + 20px\uFF0C
|
|
310
|
+
\u539F\u751F\u5168\u5C4F\u65F6\u5BBF\u4E3B\u81EA\u5DF1\u964D\u5230 20px\uFF0C\u56E0\u4E3A\u90A3\u65F6\u7EA2\u7EFF\u706F\u9690\u85CF\uFF09\u2014\u2014 \u76F4\u63A5\u8DDF\u968F\u5B83\uFF0C\u4E0D\u5FC5\u7279\u5224\u5168\u5C4F\u3002
|
|
311
|
+
\u4E3A\u4EC0\u4E48\u4E4B\u524D\u4F1A\u88AB\u906E\uFF1A\u6D6E\u5C42\u662F inset: 0 + \u5C45\u4E2D\uFF0C\u800C .me-modal \u9AD8 98vh\uFF08\u6BD4\u53EF\u89C6\u533A\u8FD8\u9AD8\uFF09\uFF0C
|
|
312
|
+
\u5C45\u4E2D\u6EA2\u51FA\u540E\u9876\u8FB9\u88AB\u9876\u5230 ~1vh\uFF0C\u6B63\u597D\u843D\u5728\u7EA2\u7EFF\u706F\u4E0B\u9762\u3002\u4E8E\u662F iframe \u91CC\u5DE6\u680F\u9876\u90E8\u90A3\u6392\u63A7\u4EF6
|
|
313
|
+
\uFF08\u914D\u7F6E / \u5BA1\u6838\u4E2D\u5FC3\u7B49\uFF09\u88AB\u7EA2\u7EFF\u706F\u538B\u4F4F\uFF0C\u70B9\u8FC7\u53BB\u5B9E\u9645\u547D\u4E2D\u7684\u662F\u539F\u751F\u300C\u5173\u95ED\u300D\u2192 \u6574\u4E2A\u684C\u9762\u58F3\u88AB\u5173\u6389\u3002
|
|
314
|
+
\u505A\u6CD5\uFF1A\u628A\u9876\u8FB9\u8BA9\u5230\u5B89\u5168\u533A\u4E0B\u6CBF\uFF0C\u5E76\u8BA9\u5BF9\u8BDD\u6846\u9AD8\u5EA6**\u6B63\u597D**\u7B49\u4E8E\u5269\u4F59\u7A7A\u95F4 \u2014\u2014 \u6B64\u65F6\u300C\u5C45\u4E2D\u300D\u7B49\u4E8E
|
|
315
|
+
\u300C\u9876\u5BF9\u9F50\u300D\uFF0C\u9876\u8FB9\u6052\u4E3A --dsh-frame-overlay-top\uFF1B\u906E\u7F69\u4ECD\u94FA\u6EE1\u5168\u5C4F\uFF08\u7EE7\u7EED\u6321\u4F4F\u80CC\u666F\u70B9\u51FB\uFF09\u3002 */
|
|
316
|
+
[data-platform=darwin] .me-overlay-top {
|
|
317
|
+
padding-top: var(--dsh-frame-overlay-top, 68px);
|
|
318
|
+
padding-bottom: 16px;
|
|
319
|
+
}
|
|
320
|
+
[data-platform=darwin] .me-overlay-top .me-modal:not(.me-modal-full) {
|
|
321
|
+
height: min(calc(100vh - var(--dsh-frame-overlay-top, 68px) - 16px), 1080px);
|
|
322
|
+
}
|
|
323
|
+
/* \u9605\u8BFB\u5668 / \u65B0\u5EFA\u5361 / \u5BFC\u51FA\u7B49\u5C0F\u5BF9\u8BDD\u6846\uFF1A\u540C\u6837\u8BA9\u5F00\u9876\u5E26\uFF08\u5BFC\u51FA\u5168\u5C4F\u6A21\u5F0F\u4F1A\u9876\u5230 24px\uFF09\u3002 */
|
|
324
|
+
[data-platform=darwin] .me-overlay {
|
|
325
|
+
padding-top: var(--dsh-frame-overlay-top, 68px);
|
|
326
|
+
}
|
|
327
|
+
[data-platform=darwin] .me-modal-full {
|
|
328
|
+
top: var(--dsh-frame-overlay-top, 68px);
|
|
329
|
+
height: calc(100vh - var(--dsh-frame-overlay-top, 68px));
|
|
330
|
+
max-height: calc(100vh - var(--dsh-frame-overlay-top, 68px));
|
|
331
|
+
}
|
|
332
|
+
`;function Ca(t){t.effect(()=>t.locale.register(st,{zh:Jt,en:Zt}),"memory-eternal: locale");let i=t.locale.bind(st);t.effect(()=>t.slots.inject("settings.section",()=>t.slots.register({name:"settings.section",id:st,order:25,label:()=>i("nav"),locale:st,inject:()=>({})},()=>o.default.createElement(Ma,{t:i}))),"memory-eternal: settings section"),t.effect(()=>t.slots.inject("sidebar.footer.action",()=>t.slots.register({name:"sidebar.footer.action",id:`${st}:footer`,order:100,label:()=>i("nav"),locale:st,inject:()=>({})},r=>o.default.createElement(Ra,{t:i,locale:t.locale,wide:!(r&&r.wide===!1)}))),"memory-eternal: sidebar footer action")}function Na(){let[t,i]=(0,o.useState)(O+"/ui/app");return(0,o.useEffect)(()=>{let r=!0,g=()=>fetch("/memory-eternal/api/web-info").then(c=>c.json()).then(c=>{r&&c&&c.ok&&c.alive!==!1&&c.url&&i(c.url.endsWith("/")?c.url:c.url+"/")}).catch(()=>{});return fetch(O+"/ui/app",{method:"HEAD"}).then(c=>{if(r&&!c.ok)return g()}).catch(()=>r?g():void 0),()=>{r=!1}},[]),t}function Ma({t}){return(0,e.jsxs)("div",{style:{display:"flex",flexDirection:"column",height:"100%",minHeight:0},children:[(0,e.jsx)("style",{children:Ee}),(0,e.jsx)(aa,{t,onReload:()=>{},version:0})]})}function Ra({t,locale:i,wide:r}){let[g,c]=(0,o.useState)(!1);return(0,e.jsxs)("div",{className:`me-footer${r?"":" rail"}`,children:[(0,e.jsx)("style",{children:Ee}),(0,e.jsxs)("button",{type:"button",className:"me-footer-btn",onClick:()=>c(!0),"aria-label":t("nav"),title:t("nav"),children:[(0,e.jsx)("span",{className:"me-footer-ico","aria-hidden":"true",children:(0,e.jsx)(ea,{})}),(0,e.jsx)("span",{className:"me-footer-label",children:t("nav")})]}),g&&(0,e.jsx)(Ta,{t,locale:i,onClose:()=>c(!1)})]})}function Ta({t,locale:i,onClose:r,tab:g}){let[c,l]=(0,o.useState)(!1),C=(0,o.useRef)(null),T=Na(),S=(0,o.useCallback)(R=>i&&typeof i.subscribe=="function"?i.subscribe(R):()=>{},[i]),H=(0,o.useCallback)(()=>i&&typeof i.getSnapshot=="function"?i.getSnapshot():null,[i]),k=(0,o.useSyncExternalStore)(S,H),x=k&&(k.active==="zh"||k.active==="en")?k.active:null,m=(0,o.useRef)(null);m.current===null&&x&&(m.current=x);let z=(0,o.useMemo)(()=>{let R=m.current?`lang=${m.current}`:"";if(g==="config")return`${O}/ui/config?tab=config${R?`&${R}`:""}`;let w=T.replace(/\/$/,""),j=w.endsWith("/ui/app")?"?":"/?";return g?`${w}${j}tab=${g}${R?`&${R}`:""}`:R?`${w}${j}${R}`:T},[T,g]);return(0,o.useEffect)(()=>{let R=C.current&&C.current.contentWindow;R&&x&&R.postMessage({source:"memory-eternal",type:"locale",lang:x},"*")},[x]),(0,o.useEffect)(()=>{let R=w=>{w.key==="Escape"&&r()};return window.addEventListener("keydown",R),()=>window.removeEventListener("keydown",R)},[r]),(0,e.jsxs)("div",{className:"me-overlay-top",onClick:r,children:[(0,e.jsx)("style",{children:Ee}),(0,e.jsx)("div",{className:"me-modal"+(c?" me-modal-full":""),onClick:R=>R.stopPropagation(),children:(0,e.jsxs)("div",{style:{display:"flex",flexDirection:"column",height:"100%",minHeight:0,position:"relative"},children:[(0,e.jsxs)("div",{style:{position:"absolute",top:8,right:8,zIndex:10,display:"flex",gap:4,padding:"4px 6px",background:"var(--dsw-alias-bg-overlay, rgba(17,24,39,0.45))",borderRadius:8,backdropFilter:"blur(4px)"},children:[(0,e.jsx)("button",{type:"button",onClick:()=>l(R=>!R),"aria-label":t(c?"exitFullscreen":"enterFullscreen"),title:t(c?"exitFullscreen":"enterFullscreen"),style:{width:26,height:26,borderRadius:6,border:"1px solid rgba(255,255,255,0.18)",background:"rgba(255,255,255,0.08)",color:"#fff",cursor:"pointer",fontSize:14,lineHeight:1,display:"flex",alignItems:"center",justifyContent:"center"},children:c?"\u2921":"\u2922"}),(0,e.jsx)("button",{type:"button",onClick:r,"aria-label":t("close"),title:t("close"),style:{width:26,height:26,borderRadius:6,border:"1px solid rgba(255,255,255,0.18)",background:"rgba(255,255,255,0.08)",color:"#fff",cursor:"pointer",fontSize:16,lineHeight:1,display:"flex",alignItems:"center",justifyContent:"center"},children:"\xD7"})]}),(0,e.jsx)("iframe",{ref:C,src:z,title:"memory-eternal",allow:"clipboard-write; clipboard-read",style:{width:"100%",flex:1,border:0,borderRadius:"inherit",background:"var(--dsw-alias-bg-base, #fff)"}})]})})]})}function ea(){return(0,e.jsxs)("svg",{viewBox:"0 0 24 24","aria-hidden":"true",children:[(0,e.jsx)("defs",{children:(0,e.jsxs)("linearGradient",{id:"mg-brain",x1:"0",y1:"0",x2:"1",y2:"1",children:[(0,e.jsx)("stop",{offset:"0%",stopColor:"#FFC46B"}),(0,e.jsx)("stop",{offset:"50%",stopColor:"#F97316"}),(0,e.jsx)("stop",{offset:"100%",stopColor:"#EA580C"})]})}),(0,e.jsx)("path",{fill:"url(#mg-brain)",d:"M12 4.5a3.2 3.2 0 0 0-6.4.1 4.1 4.1 0 0 0-2.7 5.9 4.1 4.1 0 0 0 .6 6.7A4.2 4.2 0 0 0 12 18.2Z"}),(0,e.jsx)("path",{fill:"url(#mg-brain)",d:"M12 4.5a3.2 3.2 0 0 1 6.4.1 4.1 4.1 0 0 1 2.7 5.9 4.1 4.1 0 0 1-.6 6.7A4.2 4.2 0 0 1 12 18.2Z"}),(0,e.jsx)("path",{d:"M12 9.2c-.8-1.4-1.7-2-3.1-2.2M12 9.2c.8-1.4 1.7-2 3.1-2.2M12 12.4c-1.2 1-3.2 1.6-5 1.4M12 12.4c1.2 1 3.2 1.6 5 1.4M12 13.2v4",fill:"none",stroke:"rgba(255,255,255,0.62)",strokeWidth:"1",strokeLinecap:"round"}),(0,e.jsx)("circle",{cx:"12",cy:"8.6",r:"1.1",fill:"rgba(255,255,255,0.85)"})]})}var yt="0.10.40";function Da({t,inModal:i,onClose:r,onFull:g,full:c}){let[l,C]=(0,o.useState)(null),[T,S]=(0,o.useState)([]),[H,k]=(0,o.useState)(!0),[x,m]=(0,o.useState)(null),[z,R]=(0,o.useState)(""),[w,j]=(0,o.useState)("all"),[L,K]=(0,o.useState)(""),[te,V]=(0,o.useState)("all"),[F,Ie]=(0,o.useState)("all"),[G,ge]=(0,o.useState)(()=>{try{let b=new URLSearchParams(typeof location<"u"?location.search:"").get("tab"),n={cards:"cards",graph:"graph",usage:"stats",stats:"stats",audit:"audit",recycle:"optimize",optimize:"optimize",config:"config"};if(b&&n[b])return n[b]}catch{}return"cards"}),[X,$]=(0,o.useState)(()=>new Set([G])),le=(0,o.useCallback)(b=>{$(n=>n.has(b)?n:new Set(n).add(b)),ge(b)},[]),[he,Ae]=(0,o.useState)("recent"),[ke,y]=(0,o.useState)(null),ue=(0,o.useRef)(null),Te=(0,o.useRef)(null),[h,U]=(0,o.useState)(!1),[re,ye]=(0,o.useState)(null),[ee,Z]=(0,o.useState)(!1),[Be,Ce]=(0,o.useState)(!1),[Re,Ne]=(0,o.useState)(!0),[s,N]=(0,o.useState)(null),[I,Q]=(0,o.useState)(0),we=(0,o.useCallback)(()=>Q(b=>b+1),[]),de=l?.quarantine?.pending??0,ae=l?.quarantine?.deleted??0,Y=(0,o.useRef)(null),[We,Le]=(0,o.useState)(0),[p,d]=(0,o.useState)(!1),_=(0,o.useRef)(null),se=(0,o.useRef)(null),ce=(0,o.useCallback)(async(b=w,n=L,v=F,D={})=>{let B=D.append===!0;try{B?d(!0):(k(!0),R(""));let E=new URLSearchParams;b&&b!=="all"&&E.set("kind",b),n.trim()&&E.set("q",n.trim()),E.set("status",v||"all"),E.set("limit",String(xa)),E.set("offset",String(D.offset||0)),he==="title"&&E.set("sort","title");let J=await(await fetch(`${O}/cards?${E.toString()}`)).json();if(J.ok){let ve=J.cards||[];S(De=>B?[...De,...ve.filter(Pe=>!De.some(vt=>vt.path===Pe.path))]:ve),Le(Number(J.total)||ve.length)}}catch(E){R(String(E&&E.message?E.message:E))}finally{B?d(!1):k(!1)}},[w,L,F,he]),fe=(0,o.useCallback)(async()=>{try{let[b,n]=await Promise.all([fetch(`${O}/overview`).then(v=>v.json()),fetch(`${O}/capture-log`).then(v=>v.json()).catch(()=>null)]);b.ok&&C(b),m(n&&n.ok&&n.available&&n.health||null)}catch(b){R(String(b&&b.message?b.message:b))}finally{k(!1)}},[]),a=async b=>{try{let v=await(await fetch(`${O}/delete?path=${encodeURIComponent(b)}`)).json();v.ok?(y({ok:!0,msg:t("deleted")}),await fe(),we()):y({ok:!1,msg:v.error||t("deleteFail")})}catch{y({ok:!1,msg:t("deleteFail")})}ue.current&&clearTimeout(ue.current),ue.current=setTimeout(()=>y(null),2e3)};(0,o.useEffect)(()=>(fe(),()=>{Y.current&&clearTimeout(Y.current)}),[fe]);let Fe=b=>{K(b),Y.current&&clearTimeout(Y.current),Y.current=setTimeout(()=>ce(w,b,F),280)},Je=b=>{j(b),ce(b,L,F)};(0,o.useEffect)(()=>{G==="cards"&&ce(w,L,F)},[F,G,I,he]),(0,o.useEffect)(()=>{if(G!=="cards"||H||p||T.length>=We)return;let b=_.current,n=se.current;if(!b)return;let v=new IntersectionObserver(D=>{D[0]&&D[0].isIntersecting&&ce(w,L,F,{append:!0,offset:T.length})},{root:n||null,rootMargin:"400px"});return v.observe(b),()=>v.disconnect()},[G,T,We,H,p,w,L,F,ce]);let et=async b=>{try{let n=b.status;if(!n&&b.path)try{let W=((await fetch(`${O}/cards?status=all&q=${encodeURIComponent(b.title||"")}&limit=50`).then(J=>J.json())).cards||[]).find(J=>J.path===b.path);W&&(n=W.status)}catch{}let v=n&&n!=="approved"?`&status=${encodeURIComponent(n)}`:"",D=await fetch(`${O}/card?path=${encodeURIComponent(b.path||b.id)}${v}`),B=await D.json();B.ok?ye({path:B.path,title:b.title,text:B.text}):R(B.error||`HTTP ${D.status}`)}catch(n){R(String(n&&n.message?n.message:n))}},at=async(b,n)=>{U(!0);try{let v=await fetch(`${O}/export`);if(!v.ok)throw new Error("HTTP "+v.status);let D=await v.json();if(!D.ok)throw new Error(D.error||"export failed");let B,E,W;if(b==="json"){E="memory-vault.json",W="application/json";let De=D.cards.map(Pe=>({path:Pe.path,title:Pe.title,kind:Pe.kind,status:Pe.status,store:Pe.store,text:Pe.text}));B=JSON.stringify({format:D.format||"memory-eternal-vault",formatVersion:D.formatVersion||1,exportedAt:D.exportedAt||new Date().toISOString(),count:De.length,cards:De},null,2)}else E="memory-vault.md",W="text/markdown;charset=utf-8",B=D.cards.map(De=>De.text.trim()).filter(Boolean).join(`
|
|
302
333
|
|
|
303
334
|
---
|
|
304
335
|
|
package/lib/watchdog.js
CHANGED
|
@@ -351,6 +351,8 @@ export async function stopWatchdogs({ env = process.env, port, timeoutMs = 5000,
|
|
|
351
351
|
for (const p of ports) {
|
|
352
352
|
const found = await probeListener(p)
|
|
353
353
|
if (found && found.pid) portOwners.push({ port: p, pid: Number(found.pid), command: String(found.command || '') })
|
|
354
|
+
// 「未知」要留痕(issue #45 后续):静默等于把人引回「端口空闲」的老误判
|
|
355
|
+
else if (found && found.unknown) warnings.push(`端口 ${p} 的占用者无法判定(进程枚举超时/不可用)—— 未知,不代表空闲`)
|
|
354
356
|
}
|
|
355
357
|
if (portOwners.length && forcePortOccupant) {
|
|
356
358
|
for (const occ of portOwners) {
|
|
@@ -576,6 +578,46 @@ export async function probe(port, timeoutMs = 1000) {
|
|
|
576
578
|
*
|
|
577
579
|
* @returns {Promise<string>} 取不到返回空串
|
|
578
580
|
*/
|
|
581
|
+
/**
|
|
582
|
+
* 内部:向某端口发一条 GET 并解析 JSON。**每次都销毁 socket**(issue #45 后续第 4 条)。
|
|
583
|
+
* @returns {Promise<{status:number, data:any, error:string}>}
|
|
584
|
+
*/
|
|
585
|
+
function requestJson(port, route, timeoutMs = 1200) {
|
|
586
|
+
return new Promise((resolve) => {
|
|
587
|
+
const req = http.request({ host: '127.0.0.1', port, path: route, method: 'GET', timeout: timeoutMs }, (res) => {
|
|
588
|
+
const chunks = []
|
|
589
|
+
res.on('data', (c) => chunks.push(c))
|
|
590
|
+
res.on('end', () => {
|
|
591
|
+
try { resolve({ status: res.statusCode || 0, data: JSON.parse(Buffer.concat(chunks).toString('utf8')), error: '' }) }
|
|
592
|
+
catch { resolve({ status: res.statusCode || 0, data: null, error: 'BAD_JSON' }) }
|
|
593
|
+
})
|
|
594
|
+
res.on('error', (e) => { try { res.destroy() } catch {} resolve({ status: 0, data: null, error: 'RES_ERROR:' + String((e && e.code) || e) }) })
|
|
595
|
+
})
|
|
596
|
+
req.on('error', (e) => resolve({ status: 0, data: null, error: String((e && e.code) || e) }))
|
|
597
|
+
req.on('timeout', () => { try { req.destroy() } catch {} resolve({ status: 0, data: null, error: 'TIMEOUT' }) })
|
|
598
|
+
req.end()
|
|
599
|
+
})
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* 「端口上的版本」+ **失败原因**(issue #45 后续第 3 条:`超时` 与 `没有服务` 必须能分开报)。
|
|
604
|
+
* 纯诊断用;返回的 version 为空时,timedOut 告诉你这是超时还是根本没人应答。
|
|
605
|
+
* @returns {Promise<{version:string, timedOut:boolean, lastError:string}>}
|
|
606
|
+
*/
|
|
607
|
+
export async function probeServedVersionDetailed(port, timeoutMs = 1200, { tries = 3 } = {}) {
|
|
608
|
+
let lastError = ''
|
|
609
|
+
for (let i = 0; i < Math.max(1, tries); i++) {
|
|
610
|
+
const a = await requestJson(port, `${API_PREFIX}/version-check`, timeoutMs)
|
|
611
|
+
if (a.data && a.data.ok === true && typeof a.data.loaded === 'string' && a.data.loaded) return { version: String(a.data.loaded), timedOut: false, lastError: '' }
|
|
612
|
+
if (a.error) lastError = a.error
|
|
613
|
+
const b = await requestJson(port, `${API_PREFIX}/overview`, timeoutMs)
|
|
614
|
+
if (b.data && typeof b.data.version === 'string' && b.data.version) return { version: String(b.data.version), timedOut: false, lastError: '' }
|
|
615
|
+
if (b.error) lastError = b.error
|
|
616
|
+
if (i < tries - 1) await new Promise((r) => setTimeout(r, 150 * (i + 1)))
|
|
617
|
+
}
|
|
618
|
+
return { version: '', timedOut: /TIMEOUT/i.test(lastError), lastError }
|
|
619
|
+
}
|
|
620
|
+
|
|
579
621
|
export async function probeServedVersion(port, timeoutMs = 1200) {
|
|
580
622
|
// issue #31/#33 第②层:`/overview` 会碰数据库,库只读/损坏时它 500 —— 于是「端口上明明活着、
|
|
581
623
|
// 版本却拿不到」:verify 报「(无响应)」、status 甚至误判成「v0.10.4 之前的旧版」,进而触发
|
|
@@ -603,7 +645,7 @@ export async function probeServedVersion(port, timeoutMs = 1200) {
|
|
|
603
645
|
async function probeViaVersionCheck(port, timeoutMs = 1200) {
|
|
604
646
|
return new Promise((resolve) => {
|
|
605
647
|
const req = http.request({ host: '127.0.0.1', port, path: `${API_PREFIX}/version-check`, method: 'GET', timeout: timeoutMs }, (res) => {
|
|
606
|
-
if (res.statusCode !== 200) return resolve('')
|
|
648
|
+
if (res.statusCode !== 200) { try { res.resume(); res.destroy() } catch { /* 已关闭 */ } return resolve('') }
|
|
607
649
|
const chunks = []
|
|
608
650
|
res.on('data', (c) => chunks.push(c))
|
|
609
651
|
res.on('end', () => {
|
|
@@ -917,7 +959,8 @@ export async function clearResidentForTakeover({ port = 7999, env = process.env,
|
|
|
917
959
|
const findListener = deps.findListener || findPortListener
|
|
918
960
|
// 枚举本身要能被时间预算约束(否则 8s 预算被一次 13.6s 的 PowerShell 查询吃光,决策基于过期数据)
|
|
919
961
|
const listenerOpts = { timeoutMs: Math.max(500, Math.min(2500, Number(timeoutMs) || 8000)) }
|
|
920
|
-
|
|
962
|
+
// portFree 只是兼容布尔;**判定看 portState**(issue #45 后续:超时 ≠ 空闲)。
|
|
963
|
+
const out = { stopped: [], webStopped: [], portStopped: [], warnings: [], failed: [], portFree: false, portState: 'unknown' }
|
|
921
964
|
const rawKill = deps.kill || ((pid) => { try { process.kill(pid, 'SIGTERM'); return true } catch { return false } })
|
|
922
965
|
// 记下「已经判定收不掉」的 pid:立刻再来一次只会同样失败,白折腾还可能误导用户以为是权限抖动
|
|
923
966
|
const failedKills = new Set()
|
|
@@ -949,7 +992,14 @@ export async function clearResidentForTakeover({ port = 7999, env = process.env,
|
|
|
949
992
|
}
|
|
950
993
|
if (left && left.pid) {
|
|
951
994
|
out.portFree = false
|
|
995
|
+
out.portState = 'occupied'
|
|
952
996
|
out.warnings.push(`端口 ${p} 仍被 pid ${left.pid} 占用(${left.command || '命令行未知'})`)
|
|
997
|
+
} else if (left && left.unknown) {
|
|
998
|
+
// 枚举器超时/不可用 —— 这是「未知」,不是「空闲」:中止接管(不杀、不漂移、不动锁)。
|
|
999
|
+
out.portFree = false
|
|
1000
|
+
out.portState = 'unknown'
|
|
1001
|
+
out.unknown = true
|
|
1002
|
+
out.warnings.push(`端口 ${p} 的占用者**无法判定**(进程枚举超时/不可用)→ 已中止接管:既不按「端口空闲」继续,也不把 web 挪到 port+1`)
|
|
953
1003
|
} else {
|
|
954
1004
|
// 枚举不到 ≠ 端口是空的(issue #30 现场):受限环境/被拒的进程查询会让 findListener 返回 null,
|
|
955
1005
|
// 旧实现据此认为"端口已空出" → 新 web 绑不上 7999 → 退让或失败 → 自检「(无响应)」+ 锁被清空。
|
|
@@ -959,9 +1009,11 @@ export async function clearResidentForTakeover({ port = 7999, env = process.env,
|
|
|
959
1009
|
try { listening = Boolean(await probe(p)) } catch { listening = false }
|
|
960
1010
|
if (listening) {
|
|
961
1011
|
out.portFree = false
|
|
1012
|
+
out.portState = 'occupied'
|
|
962
1013
|
out.warnings.push(`端口 ${p} 仍在监听,但占用者无法识别(进程枚举被拒/不可用)—— 已中止接管,且未改动任何锁登记`)
|
|
963
1014
|
} else {
|
|
964
1015
|
out.portFree = true
|
|
1016
|
+
out.portState = 'free'
|
|
965
1017
|
}
|
|
966
1018
|
}
|
|
967
1019
|
return out
|
|
@@ -1001,6 +1053,31 @@ export function parseLsofFields(text) {
|
|
|
1001
1053
|
* @param {{exec?: (cmd:string,args:string[])=>Promise<string>}} [opts]
|
|
1002
1054
|
* @returns {Promise<{pid:number, command:string}|null>}
|
|
1003
1055
|
*/
|
|
1056
|
+
/**
|
|
1057
|
+
* 纯函数:从 `netstat -ano` 输出里找出**监听指定端口**的 pid(issue #45 后续实测)。
|
|
1058
|
+
*
|
|
1059
|
+
* 为什么它是 Windows 上的首选:报告者实测 `Get-NetTCPConnection` 在本机要 **4.1–61.4 秒**
|
|
1060
|
+
* (TCP 表本身很大,它要先枚举整表再过滤),而 `netstat -ano` 全量只要 **1.8 秒**,且直接给 pid。
|
|
1061
|
+
* netstat 不连端口、不枚举两次 —— 对「端口上那个进程把 backlog 打满、连不上」的现场
|
|
1062
|
+
* (正是 0.10.36 自探递归留下的状态)也能给出正确结论。
|
|
1063
|
+
*
|
|
1064
|
+
* @returns {number} pid,没找到返回 0
|
|
1065
|
+
*/
|
|
1066
|
+
export function parseNetstatListener(text, port) {
|
|
1067
|
+
const want = String(Number(port) || 0)
|
|
1068
|
+
if (!want || want === '0') return 0
|
|
1069
|
+
for (const line of String(text || '').split(/\r?\n/)) {
|
|
1070
|
+
if (!/LISTENING/i.test(line)) continue
|
|
1071
|
+
// 形如: TCP 127.0.0.1:7999 0.0.0.0:0 LISTENING 10924
|
|
1072
|
+
// TCP [::1]:7999 [::]:0 LISTENING 10924
|
|
1073
|
+
const m = /^\s*TCP\s+\S*:(\d+)\s+\S*:\d+\s+LISTENING\s+(\d+)\s*$/i.exec(line.trim())
|
|
1074
|
+
if (!m) continue
|
|
1075
|
+
if (String(Number(m[1])) !== want) continue
|
|
1076
|
+
return Number(m[2]) || 0
|
|
1077
|
+
}
|
|
1078
|
+
return 0
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1004
1081
|
export async function findPortListener(port, { exec, platform = process.platform, timeoutMs = 4000 } = {}) {
|
|
1005
1082
|
const p = Number(port)
|
|
1006
1083
|
if (!Number.isFinite(p) || p <= 0) return null
|
|
@@ -1008,6 +1085,24 @@ export async function findPortListener(port, { exec, platform = process.platform
|
|
|
1008
1085
|
const execOpts = { timeoutMs: Math.max(200, Number(timeoutMs) || 4000) }
|
|
1009
1086
|
try {
|
|
1010
1087
|
if (platform === 'win32') {
|
|
1088
|
+
// ① netstat -ano 优先(issue #45 后续):不连端口,1.8s 量级给出监听 pid。
|
|
1089
|
+
// PowerShell 的 Get-NetTCPConnection 在 TCP 表大的机器上实测 4.1–61.4s ——
|
|
1090
|
+
// 一旦超时,旧实现返回 null,调用方把它当「端口空闲」,于是新看门狗去绑一个**还被旧 web 占着**的端口,
|
|
1091
|
+
// 只能漂到 port+1,旧 web 永远活着、修复永远轮不到执行(报告者的现场)。
|
|
1092
|
+
let netOut = null
|
|
1093
|
+
try { netOut = await runExec('netstat', ['-ano'], { timeoutMs: Math.max(3000, execOpts.timeoutMs) }) } catch { netOut = null }
|
|
1094
|
+
const netPid = netOut == null ? 0 : parseNetstatListener(netOut, p)
|
|
1095
|
+
if (netPid) {
|
|
1096
|
+
let cmd = ''
|
|
1097
|
+
try {
|
|
1098
|
+
cmd = String((await runExec('powershell', ['-NoProfile', '-Command', `(Get-CimInstance Win32_Process -Filter "ProcessId=${netPid}" | Select-Object -ExpandProperty CommandLine)`], execOpts)) || '').trim()
|
|
1099
|
+
} catch { cmd = '' }
|
|
1100
|
+
return { pid: netPid, command: cmd }
|
|
1101
|
+
}
|
|
1102
|
+
// netOut 为 null = 命令没跑起来/被杀超时 → **未知**,绝不能当成「端口空闲」
|
|
1103
|
+
// 空输出同样不可信:netstat 正常时至少会有一堆行;空 = 命令没跑起来/被超时杀掉(defaultExec 超时返回 '')
|
|
1104
|
+
if (netOut == null || String(netOut).trim() === '') return { pid: 0, command: '', unknown: true }
|
|
1105
|
+
// ② 兜底:老路径(netstat 明确没有该端口的 LISTEN 行时,用 PowerShell 再确认一次,兼容旧系统)
|
|
1011
1106
|
const out = await runExec('powershell', ['-NoProfile', '-Command', `Get-NetTCPConnection -LocalPort ${p} -State Listen -ErrorAction SilentlyContinue | Select-Object -ExpandProperty OwningProcess`], execOpts)
|
|
1012
1107
|
const pid = Number(String(out || '').split(/\r?\n/).map((s) => s.trim()).find((s) => /^\d+$/.test(s))) || 0
|
|
1013
1108
|
if (!pid) return null
|
|
@@ -1400,7 +1495,7 @@ if (process.argv[1] && /[\\/]watchdog\.js$/.test(process.argv[1])) {
|
|
|
1400
1495
|
const claim = acquireWatchdogLock({ port, vault: vaultRoot || '', pkgVersion: currentPkgVersion(), force: true })
|
|
1401
1496
|
if (claim.acquired) log('已登记本次替换尝试(端口仍被占用,看门狗未启动):pid ' + process.pid)
|
|
1402
1497
|
else log('⚠ 未能登记本次替换(锁不可写?):' + (claim.file || ''))
|
|
1403
|
-
const why = `${summary} ——
|
|
1498
|
+
const why = `${summary} —— ${cleared.portState === 'unknown' ? '占用者无法判定(枚举超时/不可用),按未知处理' : '端口没空出来'},已中止接管(不退让到 port+1,避免又留一个孤儿 web)`
|
|
1404
1499
|
await noteReplace('fail', why)
|
|
1405
1500
|
process.exit(1)
|
|
1406
1501
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memory-eternal",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.40",
|
|
4
4
|
"description": "记忆核心(Memory Eternal):自研的 DeepSeek Harness 记忆插件,不移植任何既有记忆框架——对话结束后自动沉淀知识卡到本地 Markdown Vault(自研去重、自研 CJK 检索、可 git 管理),设置页提供图形化知识库(统计 / 搜索 / 知识图谱 + 侧边栏一键弹窗),Agent 通过 memory_recall 工具按需召回历史上下文。零人工干预。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"prepublishOnly": "node build.mjs",
|
|
65
65
|
"release": "node scripts/release.mjs",
|
|
66
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 && node tests/issue-fixes-2.test.mjs && node tests/issue-fixes-3.test.mjs && node tests/watchdog-orphans.test.mjs && node tests/watchdog-restart.test.mjs && node tests/readme-i18n.test.mjs && node tests/issue-fixes-4.test.mjs && node tests/host-drain.test.mjs && node tests/e2e-host-drain.mjs && node tests/issue-fixes-5.test.mjs && node tests/issue-fixes-6.test.mjs && node tests/capture-json-repair.test.mjs && node tests/csrf-origin.test.mjs && node tests/watchdog-orphan-web.test.mjs && node tests/config-validate.test.mjs && node tests/issue-fixes-8.test.mjs && node tests/issue-fixes-9.test.mjs && node tests/release-verify.test.mjs && node tests/refine.test.mjs && node tests/db-restore.test.mjs && node tests/align.test.mjs && node tests/tags-normalize.test.mjs && node tests/resident-state.test.mjs && node tests/llm-route.test.mjs && node tests/host-managed.test.mjs && node tests/replace-claim.test.mjs && node tests/version-check-self.test.mjs && node tests/setup-codex.test.mjs"
|
|
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 && node tests/issue-fixes-2.test.mjs && node tests/issue-fixes-3.test.mjs && node tests/watchdog-orphans.test.mjs && node tests/watchdog-restart.test.mjs && node tests/readme-i18n.test.mjs && node tests/issue-fixes-4.test.mjs && node tests/host-drain.test.mjs && node tests/e2e-host-drain.mjs && node tests/issue-fixes-5.test.mjs && node tests/issue-fixes-6.test.mjs && node tests/capture-json-repair.test.mjs && node tests/csrf-origin.test.mjs && node tests/watchdog-orphan-web.test.mjs && node tests/config-validate.test.mjs && node tests/issue-fixes-8.test.mjs && node tests/issue-fixes-9.test.mjs && node tests/release-verify.test.mjs && node tests/refine.test.mjs && node tests/db-restore.test.mjs && node tests/align.test.mjs && node tests/tags-normalize.test.mjs && node tests/resident-state.test.mjs && node tests/llm-route.test.mjs && node tests/host-managed.test.mjs && node tests/replace-claim.test.mjs && node tests/version-check-self.test.mjs && node tests/setup-codex.test.mjs && node tests/listener-netstat.test.mjs && node tests/issue-46-overlay.test.mjs"
|
|
68
68
|
},
|
|
69
69
|
"dependencies": {
|
|
70
70
|
"@deepseek-ai/schemastery": "^3.18.1"
|
package/web/app.js
CHANGED
|
@@ -300,7 +300,38 @@ Option A, because:
|
|
|
300
300
|
height: calc(100vh - var(--dsh-windows-titlebar-height, 0px));
|
|
301
301
|
max-height: calc(100vh - var(--dsh-windows-titlebar-height, 0px));
|
|
302
302
|
}
|
|
303
|
-
|
|
303
|
+
/* ---- macOS \u684C\u9762\u7248\uFF1A\u8BA9\u5F00\u7A97\u53E3\u5DE6\u4E0A\u89D2\u7684\u7EA2\u7EFF\u706F ----------------------------------------
|
|
304
|
+
\u5BBF\u4E3B\u5C42\u7684\u7ED3\u8BBA\uFF08issue #46\uFF09\uFF1AmacOS \u684C\u9762\u7248\u662F Electron \u7684 titleBarStyle: hiddenInset +
|
|
305
|
+
trafficLightPosition {x:16, y:18}\uFF0C\u300C\u5173\u95ED / \u6700\u5C0F\u5316 / \u7F29\u653E\u300D\u662F**\u539F\u751F NSWindow \u63A7\u4EF6**\uFF0C
|
|
306
|
+
\u7531\u64CD\u4F5C\u7CFB\u7EDF\u753B\u5728\u7F51\u9875\u5185\u5BB9\u4E4B\u4E0A\u3002\u5B83\u4E0D\u662F DOM \u2014\u2014 \u4EFB\u4F55 z-index\u3001portal\u3001\u9876\u5C42\u5C42\u7EA7\u90FD\u6392\u4E0D\u5230\u5B83\u524D\u9762\uFF0C
|
|
307
|
+
\u63D2\u4EF6\u80FD\u505A\u7684\u53EA\u6709\u300C\u8BA9\u5F00\u90A3\u5757\u77E9\u5F62\u300D\uFF0C\u76D6\u4F4F\u5B83\u662F\u505A\u4E0D\u5230\u7684\u3002
|
|
308
|
+
\u5BBF\u4E3B\u81EA\u5DF1\u5DF2\u7ECF\u628A\u5B89\u5168\u533A\u53D1\u5E03\u4E86\uFF08ui-layout \u7684 AppFrame \u5199\u5728 <html> \u4E0A\uFF09\uFF1A
|
|
309
|
+
\xB7 html[data-platform="darwin"] \u53EA\u7531\u684C\u9762\u7248 preload \u5199\u5165\uFF08\u666E\u901A\u6D4F\u89C8\u5668\u6587\u6863\u6CA1\u6709\u8FD9\u4E2A\u5C5E\u6027\uFF0C
|
|
310
|
+
\u6240\u4EE5\u4E0B\u9762\u7684\u89C4\u5219\u5728\u6D4F\u89C8\u5668 / Windows / Linux \u4E0A\u5B8C\u5168\u4E0D\u751F\u6548\uFF0C\u884C\u4E3A\u4E0E\u4E4B\u524D\u4E00\u81F4\uFF09\uFF1B
|
|
311
|
+
\xB7 --dsh-frame-overlay-top \u662F\u5BBF\u4E3B\u7ED9\u5BF9\u8BDD\u6846/\u83DC\u5355\u5B9A\u7684\u9876\u8FB9\u8BA9\u4F4D\u91CF\uFF08macOS = \u9876\u5E26 48px + 20px\uFF0C
|
|
312
|
+
\u539F\u751F\u5168\u5C4F\u65F6\u5BBF\u4E3B\u81EA\u5DF1\u964D\u5230 20px\uFF0C\u56E0\u4E3A\u90A3\u65F6\u7EA2\u7EFF\u706F\u9690\u85CF\uFF09\u2014\u2014 \u76F4\u63A5\u8DDF\u968F\u5B83\uFF0C\u4E0D\u5FC5\u7279\u5224\u5168\u5C4F\u3002
|
|
313
|
+
\u4E3A\u4EC0\u4E48\u4E4B\u524D\u4F1A\u88AB\u906E\uFF1A\u6D6E\u5C42\u662F inset: 0 + \u5C45\u4E2D\uFF0C\u800C .me-modal \u9AD8 98vh\uFF08\u6BD4\u53EF\u89C6\u533A\u8FD8\u9AD8\uFF09\uFF0C
|
|
314
|
+
\u5C45\u4E2D\u6EA2\u51FA\u540E\u9876\u8FB9\u88AB\u9876\u5230 ~1vh\uFF0C\u6B63\u597D\u843D\u5728\u7EA2\u7EFF\u706F\u4E0B\u9762\u3002\u4E8E\u662F iframe \u91CC\u5DE6\u680F\u9876\u90E8\u90A3\u6392\u63A7\u4EF6
|
|
315
|
+
\uFF08\u914D\u7F6E / \u5BA1\u6838\u4E2D\u5FC3\u7B49\uFF09\u88AB\u7EA2\u7EFF\u706F\u538B\u4F4F\uFF0C\u70B9\u8FC7\u53BB\u5B9E\u9645\u547D\u4E2D\u7684\u662F\u539F\u751F\u300C\u5173\u95ED\u300D\u2192 \u6574\u4E2A\u684C\u9762\u58F3\u88AB\u5173\u6389\u3002
|
|
316
|
+
\u505A\u6CD5\uFF1A\u628A\u9876\u8FB9\u8BA9\u5230\u5B89\u5168\u533A\u4E0B\u6CBF\uFF0C\u5E76\u8BA9\u5BF9\u8BDD\u6846\u9AD8\u5EA6**\u6B63\u597D**\u7B49\u4E8E\u5269\u4F59\u7A7A\u95F4 \u2014\u2014 \u6B64\u65F6\u300C\u5C45\u4E2D\u300D\u7B49\u4E8E
|
|
317
|
+
\u300C\u9876\u5BF9\u9F50\u300D\uFF0C\u9876\u8FB9\u6052\u4E3A --dsh-frame-overlay-top\uFF1B\u906E\u7F69\u4ECD\u94FA\u6EE1\u5168\u5C4F\uFF08\u7EE7\u7EED\u6321\u4F4F\u80CC\u666F\u70B9\u51FB\uFF09\u3002 */
|
|
318
|
+
[data-platform=darwin] .me-overlay-top {
|
|
319
|
+
padding-top: var(--dsh-frame-overlay-top, 68px);
|
|
320
|
+
padding-bottom: 16px;
|
|
321
|
+
}
|
|
322
|
+
[data-platform=darwin] .me-overlay-top .me-modal:not(.me-modal-full) {
|
|
323
|
+
height: min(calc(100vh - var(--dsh-frame-overlay-top, 68px) - 16px), 1080px);
|
|
324
|
+
}
|
|
325
|
+
/* \u9605\u8BFB\u5668 / \u65B0\u5EFA\u5361 / \u5BFC\u51FA\u7B49\u5C0F\u5BF9\u8BDD\u6846\uFF1A\u540C\u6837\u8BA9\u5F00\u9876\u5E26\uFF08\u5BFC\u51FA\u5168\u5C4F\u6A21\u5F0F\u4F1A\u9876\u5230 24px\uFF09\u3002 */
|
|
326
|
+
[data-platform=darwin] .me-overlay {
|
|
327
|
+
padding-top: var(--dsh-frame-overlay-top, 68px);
|
|
328
|
+
}
|
|
329
|
+
[data-platform=darwin] .me-modal-full {
|
|
330
|
+
top: var(--dsh-frame-overlay-top, 68px);
|
|
331
|
+
height: calc(100vh - var(--dsh-frame-overlay-top, 68px));
|
|
332
|
+
max-height: calc(100vh - var(--dsh-frame-overlay-top, 68px));
|
|
333
|
+
}
|
|
334
|
+
`;function Kg(){return(0,i.jsxs)("svg",{viewBox:"0 0 24 24","aria-hidden":"true",children:[(0,i.jsx)("defs",{children:(0,i.jsxs)("linearGradient",{id:"mg-brain",x1:"0",y1:"0",x2:"1",y2:"1",children:[(0,i.jsx)("stop",{offset:"0%",stopColor:"#FFC46B"}),(0,i.jsx)("stop",{offset:"50%",stopColor:"#F97316"}),(0,i.jsx)("stop",{offset:"100%",stopColor:"#EA580C"})]})}),(0,i.jsx)("path",{fill:"url(#mg-brain)",d:"M12 4.5a3.2 3.2 0 0 0-6.4.1 4.1 4.1 0 0 0-2.7 5.9 4.1 4.1 0 0 0 .6 6.7A4.2 4.2 0 0 0 12 18.2Z"}),(0,i.jsx)("path",{fill:"url(#mg-brain)",d:"M12 4.5a3.2 3.2 0 0 1 6.4.1 4.1 4.1 0 0 1 2.7 5.9 4.1 4.1 0 0 1-.6 6.7A4.2 4.2 0 0 1 12 18.2Z"}),(0,i.jsx)("path",{d:"M12 9.2c-.8-1.4-1.7-2-3.1-2.2M12 9.2c.8-1.4 1.7-2 3.1-2.2M12 12.4c-1.2 1-3.2 1.6-5 1.4M12 12.4c1.2 1 3.2 1.6 5 1.4M12 13.2v4",fill:"none",stroke:"rgba(255,255,255,0.62)",strokeWidth:"1",strokeLinecap:"round"}),(0,i.jsx)("circle",{cx:"12",cy:"8.6",r:"1.1",fill:"rgba(255,255,255,0.85)"})]})}var Ko="0.10.40";function tf({t:e,inModal:t,onClose:n,onFull:r,full:o}){let[l,a]=(0,x.useState)(null),[s,u]=(0,x.useState)([]),[p,h]=(0,x.useState)(!0),[v,m]=(0,x.useState)(null),[C,M]=(0,x.useState)(""),[b,j]=(0,x.useState)("all"),[d,c]=(0,x.useState)(""),[y,S]=(0,x.useState)("all"),[E,H]=(0,x.useState)("all"),[D,A]=(0,x.useState)(()=>{try{let P=new URLSearchParams(typeof location<"u"?location.search:"").get("tab"),g={cards:"cards",graph:"graph",usage:"stats",stats:"stats",audit:"audit",recycle:"optimize",optimize:"optimize",config:"config"};if(P&&g[P])return g[P]}catch{}return"cards"}),[Q,F]=(0,x.useState)(()=>new Set([D])),ne=(0,x.useCallback)(P=>{F(g=>g.has(P)?g:new Set(g).add(P)),A(P)},[]),[ke,it]=(0,x.useState)("recent"),[Le,R]=(0,x.useState)(null),xe=(0,x.useRef)(null),Qe=(0,x.useRef)(null),[L,Z]=(0,x.useState)(!1),[pe,Ne]=(0,x.useState)(null),[ie,oe]=(0,x.useState)(!1),[at,Ae]=(0,x.useState)(!1),[$e,He]=(0,x.useState)(!0),[w,O]=(0,x.useState)(null),[U,le]=(0,x.useState)(0),Fe=(0,x.useCallback)(()=>le(P=>P+1),[]),ge=l?.quarantine?.pending??0,se=l?.quarantine?.deleted??0,te=(0,x.useRef)(null),[Tt,pt]=(0,x.useState)(0),[N,k]=(0,x.useState)(!1),J=(0,x.useRef)(null),fe=(0,x.useRef)(null),ye=(0,x.useCallback)(async(P=b,g=d,_=E,W={})=>{let K=W.append===!0;try{K?k(!0):(h(!0),M(""));let G=new URLSearchParams;P&&P!=="all"&&G.set("kind",P),g.trim()&&G.set("q",g.trim()),G.set("status",_||"all"),G.set("limit",String(Ug)),G.set("offset",String(W.offset||0)),ke==="title"&&G.set("sort","title");let re=await(await fetch(`${q}/cards?${G.toString()}`)).json();if(re.ok){let Te=re.cards||[];u(Xe=>K?[...Xe,...Te.filter(wt=>!Xe.some(Go=>Go.path===wt.path))]:Te),pt(Number(re.total)||Te.length)}}catch(G){M(String(G&&G.message?G.message:G))}finally{K?k(!1):h(!1)}},[b,d,E,ke]),be=(0,x.useCallback)(async()=>{try{let[P,g]=await Promise.all([fetch(`${q}/overview`).then(_=>_.json()),fetch(`${q}/capture-log`).then(_=>_.json()).catch(()=>null)]);P.ok&&a(P),m(g&&g.ok&&g.available&&g.health||null)}catch(P){M(String(P&&P.message?P.message:P))}finally{h(!1)}},[]),f=async P=>{try{let _=await(await fetch(`${q}/delete?path=${encodeURIComponent(P)}`)).json();_.ok?(R({ok:!0,msg:e("deleted")}),await be(),Fe()):R({ok:!1,msg:_.error||e("deleteFail")})}catch{R({ok:!1,msg:e("deleteFail")})}xe.current&&clearTimeout(xe.current),xe.current=setTimeout(()=>R(null),2e3)};(0,x.useEffect)(()=>(be(),()=>{te.current&&clearTimeout(te.current)}),[be]);let xt=P=>{c(P),te.current&&clearTimeout(te.current),te.current=setTimeout(()=>ye(b,P,E),280)},sn=P=>{j(P),ye(P,d,E)};(0,x.useEffect)(()=>{D==="cards"&&ye(b,d,E)},[E,D,U,ke]),(0,x.useEffect)(()=>{if(D!=="cards"||p||N||s.length>=Tt)return;let P=J.current,g=fe.current;if(!P)return;let _=new IntersectionObserver(W=>{W[0]&&W[0].isIntersecting&&ye(b,d,E,{append:!0,offset:s.length})},{root:g||null,rootMargin:"400px"});return _.observe(P),()=>_.disconnect()},[D,s,Tt,p,N,b,d,E,ye]);let bn=async P=>{try{let g=P.status;if(!g&&P.path)try{let X=((await fetch(`${q}/cards?status=all&q=${encodeURIComponent(P.title||"")}&limit=50`).then(re=>re.json())).cards||[]).find(re=>re.path===P.path);X&&(g=X.status)}catch{}let _=g&&g!=="approved"?`&status=${encodeURIComponent(g)}`:"",W=await fetch(`${q}/card?path=${encodeURIComponent(P.path||P.id)}${_}`),K=await W.json();K.ok?Ne({path:K.path,title:P.title,text:K.text}):M(K.error||`HTTP ${W.status}`)}catch(g){M(String(g&&g.message?g.message:g))}},Vn=async(P,g)=>{Z(!0);try{let _=await fetch(`${q}/export`);if(!_.ok)throw new Error("HTTP "+_.status);let W=await _.json();if(!W.ok)throw new Error(W.error||"export failed");let K,G,X;if(P==="json"){G="memory-vault.json",X="application/json";let Xe=W.cards.map(wt=>({path:wt.path,title:wt.title,kind:wt.kind,status:wt.status,store:wt.store,text:wt.text}));K=JSON.stringify({format:W.format||"memory-eternal-vault",formatVersion:W.formatVersion||1,exportedAt:W.exportedAt||new Date().toISOString(),count:Xe.length,cards:Xe},null,2)}else G="memory-vault.md",X="text/markdown;charset=utf-8",K=W.cards.map(Xe=>Xe.text.trim()).filter(Boolean).join(`
|
|
304
335
|
|
|
305
336
|
---
|
|
306
337
|
|