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.
@@ -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.39",
4
+ "version": "0.10.40",
5
5
  "description": "记忆核心(Memory Eternal):对话结束后自动沉淀知识卡到本地记忆库(~/.dsh/memory-vault),支持检索 / 召回 / 审核中心。",
6
6
  "author": {
7
7
  "name": "EternalNight996",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memory-eternal",
3
- "version": "0.10.39",
3
+ "version": "0.10.40",
4
4
  "description": "记忆核心(Memory Eternal):对话结束后自动沉淀知识卡到本地记忆库(~/.dsh/memory-vault),支持检索 / 召回 / 审核中心。",
5
5
  "author": {
6
6
  "name": "EternalNight996",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memory-eternal",
3
- "version": "0.10.39",
3
+ "version": "0.10.40",
4
4
  "description": "记忆核心(Memory Eternal):对话结束后自动沉淀知识卡到本地记忆库(~/.dsh/memory-vault),支持检索 / 召回 / 审核中心。",
5
5
  "author": {
6
6
  "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.37 and earlier · 64 releases, click to expand)</summary>
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.37 及以前 · 共 64 版,点开查看)</summary>
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 项** |
@@ -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
- const served = probePort ? await probeServedVersion(probePort) : ''
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
- `;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.39";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(`
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
- const out = { stopped: [], webStopped: [], portStopped: [], warnings: [], failed: [], portFree: false }
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} —— 端口没空出来,已中止接管(不退让到 port+1,避免又留一个孤儿 web)`
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.39",
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
- `;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.39";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(`
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