memory-eternal 0.10.49 → 0.10.51
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 +5 -3
- package/README.md +5 -3
- package/bin/dsh-memory.mjs +15 -0
- package/index.js +16 -1
- package/lib/capture.js +8 -2
- package/lib/client.js +1 -1
- package/lib/model-strikes.js +72 -0
- package/lib/support.js +118 -0
- package/lib/zip.js +112 -0
- package/package.json +2 -2
- package/web/app.js +1 -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.51",
|
|
5
5
|
"description": "记忆核心(Memory Eternal):对话结束后自动沉淀知识卡到本地记忆库(~/.dsh/memory-vault),支持检索 / 召回 / 审核中心。",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "EternalNight996",
|
package/README.en.md
CHANGED
|
@@ -361,14 +361,16 @@ 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.
|
|
365
|
-
| **v0.10.
|
|
364
|
+
| **v0.10.51** | 2026-10-19 | **One-command diagnostic bundle: dsh-memory support** (reliability line; in #47 the user-pasted context was truncated by GitHub so I could not see the full picture — this exists for exactly that kind of exchange). A single command produces a **zip** with meta (version matrix / platform / node / vault), doctor, align, status, the watchdog lock, the host heartbeat, the shared config, the tail of the capture log (200 lines by default) and a README; everything is **redacted before writing** (home directory to <home>, sk-/ghp_/github_pat/Bearer and key-like config fields to ***). Implemented with **no dependencies**: a hand-written store/deflate ZIP writer (lib/zip.js) whose output opens in Windows Explorer (verified by extracting with Expand-Archive). In restricted environments --no-spawn skips the sub-processes, while --out and --tail N control destination and log length. Adds tests/support-bundle.test.mjs (redaction, zip structure, no plaintext secrets inside the bundle, CLI wiring). npm test now runs **433 checks** |
|
|
365
|
+
| **v0.10.50** | 2026-10-19 | **Models that ignore JSON no longer burn a call every turn (a follow-up to #47)**. ① "The model replied with prose and produced no JSON object at all" is now separated from the generic UNPARSEABLE_OUTPUT failure and gets its own code **MODEL_NOT_JSON**, with wording that says the model likely does not follow structured output. ② New `lib/model-strikes.js` records which provider/model recently ignored the format (24h window, at most 50 entries) and the host moves those to the end of the candidate list when routing (never removing them; if every candidate was struck the original order is kept, so we never run out), logging a "candidates reordered" line. ③ Effect: models such as stepfun/step-api, which answer distillation prompts conversationally, stop being tried first from the second turn on, saving a pointless call each time. Adds `tests/model-strikes.test.mjs` (prose detection, stable ranking, record window and trimming, wiring guards). npm test now runs **429 checks** |
|
|
366
366
|
|
|
367
367
|
<details>
|
|
368
|
-
<summary>Older release notes (v0.10.
|
|
368
|
+
<summary>Older release notes (v0.10.49 and earlier · 76 releases, click to expand)</summary>
|
|
369
369
|
|
|
370
370
|
| Version | Date | Highlights |
|
|
371
371
|
|---|---|---|
|
|
372
|
+
| **v0.10.49** | 2026-10-19 | **Fixes "cannot choose the distillation model" in the desktop app (#47): the standalone page now proxies /llm-routes and /llm-test to the host, plus one more state in the matrix**. Root cause: the desktop memory popup is a **standalone page inside an iframe** with no DSH llm service, so /llm-routes always failed and provider/model had to be typed by hand. The standalone page now reads the host heartbeat (the port in <config>.host.json, 127.0.0.1 only, valid for 120s) and forwards those two routes to the host, marking the reply as proxied; when the host is offline the panel shows the concrete reason instead of silently falling back to text inputs. ② The version-card matrix gains restartRepair: when versions already match but nothing keeps the instance alive it says "re-register and keep it alive", no longer the self-contradictory "switch to v0.10.48 (currently v0.10.48)". npm test now runs **425 checks** |
|
|
373
|
+
| **v0.10.48** | 2026-10-19 | **A visual gate plus exhaustive state coverage (the option we agreed on)**. ① **New `npm run panel-check`** (scripts/panel-check.mjs, zero dependencies): drives local headless Chrome over CDP, asserts **no horizontal overflow** on each screen and saves screenshots (list / reader / config / feedback modal). On its very first run it caught two real bugs: an unbreakable long run in a card title (`(begin/chunk/probe/complete/abort)`) overflowing by 45px, and the **feedback textarea overflowing by 22px** (width:100% plus padding under content-box) — exactly the layout/scrollbar anomaly reported. ② Fixes: word-breaking on `.mc-cardrow h4` and a shared border-box rule for form controls. ③ **The version-card decision is now a pure function** `src/client/version-state.js`: versionCardState returns {showButton, emphasis, warn, hint} and the component only maps keys to text; a table-driven test **enumerates 324 combinations** and enforces the invariant that the button is shown whenever version info exists — structurally preventing the 0.10.45 regression where a third state made it disappear. npm test now runs **420 checks** |
|
|
372
374
|
| **v0.10.47** | 2026-10-19 | **The panel version probe now refreshes itself, and the "resident not running" wording is fixed (user report: the panel kept saying "resident instance: not running" after a restart)**. ① The instance was in fact alive (status showed 7999 listening and serving v0.10.46), but the probe ran only once when the panel mounted, so a run that happened to land inside the restart window was cached as "not running" and stuck. It now **re-checks silently every 60 seconds** (without lighting up the Check-for-updates button) and **immediately when the tab becomes visible** (visibilitychange); a restart already triggers a re-check six seconds later. ② When the resident really is down, the hint used to interpolate an empty "currently v"; it now uses vcRestartStart: "Starts the resident instance on the current disk version v{disk} (no DSH restart needed)". npm test now runs **408 checks** |
|
|
373
375
|
| **v0.10.46** | 2026-10-19 | **The "restart resident instance" button is now always shown, and the "which one is stale" wording is fixed (second user report of the button disappearing)**. The previous build put the button inside the "resident needs restart" branch, while the real state was "the resident is already current, but the process serving this page (the DSH host) is stale", so it fell into the host-hint branch and the button vanished again; worse, the warning claimed clicking restart-resident would fix it even though restarting the resident never changes the host-served page. Now: ① the button depends only on vc existing; ② each of the three states gets an accurate explanation — resident stale (click and it upgrades without restarting DSH) / host stale (restarting the resident does not affect this page, restart DSH) / everything current (force restart if it looks stuck); ③ the warning text is split into vcHostProcessStale, making clear that the page is served by the host process. npm test now runs **406 checks** |
|
|
374
376
|
| **v0.10.45** | 2026-10-19 | **Fixes the disappearing "restart resident instance" button and the dialog layout / scrollbar anomalies (from user screenshots)**. ① **The button is persistent**: it used to live inside the warning block that only renders when something is wrong, so once versions matched the whole block vanished and the button could not be found (issue #28 complained about the same). The action row now renders independently: it is the repair entry point when something is off, and stays available when everything is fine, with a note that a restart is usually unnecessary but can be forced if the instance looks stuck. ② **Scrollbar anomalies**: the reader can have up to eight header buttons while 0.10.42 made the action group nowrap, so it overflowed horizontally inside a max-width:760px dialog; each button still never shrinks or wraps, but the **group itself may wrap** onto a second row without deforming. ③ **Layout**: .me-dialog and .me-dialog-body use border-box and hide horizontal scrolling in the body (the reader width:100% plus padding no longer breaks out of the container), diagnostic/feedback pre blocks use word-break for long paths, and large-title modals ellipsize their h2. Adds tests/ui-overflow.test.mjs (4 guards). npm test now runs **406 checks** |
|
package/README.md
CHANGED
|
@@ -393,14 +393,16 @@ curl http://127.0.0.1:7999/memory-eternal/api/web-info # 独立 web 是否活
|
|
|
393
393
|
|
|
394
394
|
| 版本 | 日期 | 关键改动 |
|
|
395
395
|
|---|---|---|
|
|
396
|
-
| **v0.10.
|
|
397
|
-
| **v0.10.
|
|
396
|
+
| **v0.10.51** | 2026-10-19 | **一键诊断包 dsh-memory support**(可靠性线;#47 里用户贴的现场被 GitHub 截断,我拿不到完整信息,这功能就是为这类沟通做的)。一条命令生成 **zip**:meta(版本矩阵/平台/node/vault)/ doctor / align / status / 看门狗锁 / 宿主心跳 / 共享配置 / 沉淀日志尾巴(默认 200 行)+ README;写入前**统一脱敏**(家目录→<home>,sk-/ghp_/github_pat/Bearer/配置里 key 类字段→***)。实现为**零依赖**:自写 store/deflate ZIP 写入器(lib/zip.js,Windows 资源管理器可直接打开,实测 Expand-Archive 解压正常)。受限环境可加 --no-spawn 跳过子进程;--out 指定输出、--tail N 控制日志条数。新增 tests/support-bundle.test.mjs(4 项:脱敏、zip 结构、包内不含明文密钥、CLI 接线)。npm test 共 **433 项** |
|
|
397
|
+
| **v0.10.50** | 2026-10-19 | **「模型不守 JSON」不再每轮白烧调用(#47 衍生改进)**。① 把「模型回了散文、输出里根本没有 JSON 对象」从笼统的 UNPARSEABLE_OUTPUT 里拆出来,给独立错误码 **MODEL_NOT_JSON**,文案直接说清「该模型可能不遵守结构化输出」。② 新增 `lib/model-strikes.js`:记下「哪个 provider/model 最近不守格式」(24h 窗口、最多 50 条),宿主选路时把它们**排到候选末尾**(不删除;全部命中过则按原顺序兜底,绝不没候选),并留一条「蒸馏候选已调整」日志。③ 效果:像 stepfun/step-api 这种会把蒸馏当聊天回的模型,第二轮起就不再优先尝试,省掉每次的无效调用。新增 `tests/model-strikes.test.mjs`(4 项:散文识别 / 排序稳定性 / 记录窗口与裁剪 / 接线守卫)。npm test 共 **429 项** |
|
|
398
398
|
|
|
399
399
|
<details>
|
|
400
|
-
<summary>更早的更新日志(v0.10.
|
|
400
|
+
<summary>更早的更新日志(v0.10.49 及以前 · 共 76 版,点开查看)</summary>
|
|
401
401
|
|
|
402
402
|
| 版本 | 日期 | 关键改动 |
|
|
403
403
|
|---|---|---|
|
|
404
|
+
| **v0.10.49** | 2026-10-19 | **桌面版里「无法选择蒸馏模型」修复(#47):独立页把 /llm-routes 与 /llm-test 代理给宿主 + 文案补一个状态**。根因:桌面版的「记忆」弹窗是 iframe 里的**独立页**,它没有 DSH 的 llm 服务 → /llm-routes 恒失败 → provider/model 只能手填。现在独立页读宿主心跳(<config>.host.json 里的端口,仅 127.0.0.1、心跳 120s 内有效),把这两个路由**转发给宿主**执行并标记 proxied;宿主不在线时给明确原因(面板直接显示,不再静默回退成手填)。② 版本卡状态矩阵补 restartRepair:**版本已对但没人保活**时提示「重新托管并保活」,不再出现「换成 v0.10.48(当前 v0.10.48)」这种自相矛盾的文案(截图实测)。npm test 共 **425 项** |
|
|
405
|
+
| **v0.10.48** | 2026-10-19 | **界面验收手段 + 状态穷举(方案一落地)**。① **新增 `npm run panel-check`**(scripts/panel-check.mjs,零依赖):用本机 Chrome headless + CDP 打开面板,逐屏断言**没有横向溢出**并截图(列表 / 阅读器 / 配置 / 反馈弹窗)。它第一次跑就抓到两个真 bug:卡片标题里的无断点长串(`(begin/chunk/probe/complete/abort)`)溢出 45px;**反馈弹窗 textarea 溢出 22px**(width:100% + padding 在 content-box 下)—— 正是用户报的「排版不对 / 滚动条异常」。② 修复:`.mc-cardrow h4` 断词、`.me-overlay input/textarea/select` 统一 border-box。③ **版本卡判定抽成纯函数** `src/client/version-state.js`:`versionCardState(...)` 返回 `{showButton, emphasis, warn, hint}`,组件只做 key→文案;新增表驱动测试**穷举 324 种组合**,并守住不变式「只要拿到版本信息,按钮永远显示」—— 0.10.45「第三种状态导致按钮消失」从结构上不可能再发生。npm test 共 **420 项** |
|
|
404
406
|
| **v0.10.47** | 2026-10-19 | **面板版本探针自动复查 + 「常驻未运行」文案修正(用户反馈:重启后面板一直显示「常驻实例 未运行」)**。① 现场其实**是活的**(status 显示 7999 在听、服务 v0.10.46),但版本探针只在面板挂载时跑一次 —— 恰好撞上重启窗口就会被缓存成「未运行」并一直显示。现在**每 60 秒静默复查**(不点亮「检查更新」按钮)+ **切回页面立刻复查**(visibilitychange),重启后 6 秒本来就会复查一次。② 文案:常驻实例真没跑时,原来那句会拼出「当前 v」(空值);现在改走 vcRestartStart:**「点击会用当前磁盘版本 v{disk} 把常驻实例拉起来(不用重启 DSH)」**。npm test 共 **408 项** |
|
|
405
407
|
| **v0.10.46** | 2026-10-19 | **「重启常驻实例」按钮改为**永远显示** + 修正「谁旧」的文案(用户第二次反馈按钮消失)**。上一版把按钮放进「常驻实例需要重启」的分支,而实际状态是**常驻实例已是新版、服务本页的进程(DSH 宿主)还是旧版** → 落进宿主提示分支、按钮又没了;更糟的是警告文案写着「点重启常驻实例即可」,而重启常驻根本不会改变宿主提供给你的本页版本。现在:① 按钮**只依赖 vc 存在**,任何状态都在;② 三种状态各给准确说明 —— 常驻旧(点了就换新版,不用重启 DSH)/ 宿主旧(重启常驻**不影响本页**,请重启 DSH)/ 全部对齐(卡住时可强制重启);③ 警告文案拆出 vcHostProcessStale,明确「服务本页的是宿主进程」。npm test 共 **406 项** |
|
|
406
408
|
| **v0.10.45** | 2026-10-19 | **修复「重启常驻实例」按钮消失 + 对话框排版/滚动条异常(用户截图反馈)**。① **按钮常驻**:它原来被包在「有异常才渲染」的警告块里 —— 版本一对齐整块消失,用户再也找不到(#28 也抱怨过)。现在操作行**独立渲染**:异常时是修复入口,一切正常时也保留按钮并说明「通常无需重启,卡住时可强制重启」。② **滚动条异常**:阅读器最多 8 个头部按钮,而 0.10.42 给按钮组设的是 nowrap → 在 max-width:760px 的对话框里**横向溢出**;现在按钮自身仍不缩不换行、但**按钮组整体允许换行**(掉到第二行也不变形)。③ **排版**:.me-dialog / .me-dialog-body 改 border-box 并隐藏正文横向滚动(阅读器的 width:100% + padding 不再撑破容器);诊断/反馈的 <pre> 长路径加 word-break(远程 agent 的路径没有断点,以前必横向滚);大标题 modal 的 h2 加省略号。新增 tests/ui-overflow.test.mjs(4 项守卫)。npm test 共 **406 项** |
|
package/bin/dsh-memory.mjs
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
// dsh-memory sweep <dir> 挖掘 Claude Code 会话 JSONL
|
|
13
13
|
// dsh-memory align [--json] [--fix] 版本对齐检查(宿主/常驻/npm 版本差 + MCP/hooks 旧接线),--fix 重写接线
|
|
14
14
|
// dsh-memory doctor [--vault DIR] [--json] 库健康自检(journal 模式 / 写探针 / 备份清单 / 恢复引导)
|
|
15
|
+
// dsh-memory support [--out FILE] [--tail N] [--no-spawn] 生成诊断包 zip(版本矩阵/doctor/align/status/日志尾巴,已脱敏)
|
|
15
16
|
// dsh-memory doctor --restore latest|备份名 [--yes] [--force] 从备份重建(不带 --yes 只演练;当前库可写时需 --force)
|
|
16
17
|
// dsh-memory refine [--vault DIR] [--json] 矛盾扫描候选(不调模型);--apply --a A --b B --action keepA|keepB|merge|keepBoth 落裁决
|
|
17
18
|
// dsh-memory refine --junk [--purge --path P] [--json] 注入垃圾卡(技能目录等注入文本沉淀成的卡):列清单 / 送回收站
|
|
@@ -275,6 +276,20 @@ async function main() {
|
|
|
275
276
|
if (!rep.ok) process.exitCode = 1
|
|
276
277
|
return
|
|
277
278
|
}
|
|
279
|
+
case 'support': {
|
|
280
|
+
// 一键诊断包:版本矩阵 / doctor / align / status / 锁 / 心跳 / 配置 / 日志尾巴 → 一个 zip(已脱敏)
|
|
281
|
+
const { buildSupportBundle } = await import('../lib/support.js')
|
|
282
|
+
const out = argOf('--out') || ''
|
|
283
|
+
const tailN = Number(argOf('--tail')) || 200
|
|
284
|
+
const portN = Number(argOf('--port')) || 0
|
|
285
|
+
// --no-spawn:跳过 doctor/align/status 子进程(受限环境排障用)
|
|
286
|
+
const r = buildSupportBundle({ out, tail: tailN, port: portN, spawn: !has('--no-spawn') })
|
|
287
|
+
if (!r.ok) { console.error('诊断包生成失败'); process.exitCode = 1; return }
|
|
288
|
+
console.log('诊断包已生成(已脱敏):' + r.path)
|
|
289
|
+
console.log('大小:' + Math.round(r.bytes / 1024) + ' KB · 条目:' + r.entries.join(', '))
|
|
290
|
+
console.log('提交 issue 时把这个 zip 拖进去即可;内含 README.txt 说明。')
|
|
291
|
+
return
|
|
292
|
+
}
|
|
278
293
|
case 'doctor': {
|
|
279
294
|
// ⑤ 损坏/不可写时的「引导」(issue #33):只体检、只列备份、只打印下一步 —— 绝不自动替换/删除数据。
|
|
280
295
|
// --restore:把「损坏时从备份重建」从**引导**升级成**可执行**(#31 里 WAL 头损坏的唯一现实出路)。
|
package/index.js
CHANGED
|
@@ -712,7 +712,18 @@ export function apply(ctx, config) {
|
|
|
712
712
|
}
|
|
713
713
|
} catch { /* 探测失败一律回退配置 */ }
|
|
714
714
|
}
|
|
715
|
-
|
|
715
|
+
let routes = await routeCandidates(llm, routeOpts)
|
|
716
|
+
// issue #47:近期「不守 JSON」的模型排到后面(不删除,全部命中过则原顺序兜底)
|
|
717
|
+
try {
|
|
718
|
+
const st = await import('./lib/model-strikes.js')
|
|
719
|
+
const strikes = st.recentModelStrikes(process.env)
|
|
720
|
+
if (Object.keys(strikes).length) {
|
|
721
|
+
const before = routes.map((r) => r.provider + '/' + r.model).join(' → ')
|
|
722
|
+
routes = st.rankRoutesByStrikes(routes, strikes)
|
|
723
|
+
const after = routes.map((r) => r.provider + '/' + r.model).join(' → ')
|
|
724
|
+
if (after !== before) logCapture(sessionId, 'boot', `蒸馏候选已调整(近期不守 JSON 的模型排后):${before} → ${after}`)
|
|
725
|
+
}
|
|
726
|
+
} catch { /* 记录读不到就按原顺序 */ }
|
|
716
727
|
if (!routes.length) { logCapture(sessionId, 'fail', '取不到模型路由(llm.listProviders 为空)'); return }
|
|
717
728
|
// 语义去重近邻:把已有卡片索引喂给模型,让模型决定新建 vs 追加。
|
|
718
729
|
// 成本控制:dedupByLLM=false 时跳过喂 LLM 的近邻采样(纯词法去重兜底)。
|
|
@@ -739,6 +750,10 @@ export function apply(ctx, config) {
|
|
|
739
750
|
if (detailed.card !== undefined) { result = detailed.card; failure = null; break }
|
|
740
751
|
if (detailed.failure) {
|
|
741
752
|
const f = { ...detailed.failure, provider: route.provider, model: route.model }
|
|
753
|
+
// 「模型回了散文」要记住:下次选路把它排到最后,别每轮都先白烧一次(issue #47)
|
|
754
|
+
if (f.code === 'MODEL_NOT_JSON') {
|
|
755
|
+
try { const st = await import('./lib/model-strikes.js'); st.recordModelStrike(process.env, route, f.code) } catch { /* 记不下来不影响主流程 */ }
|
|
756
|
+
}
|
|
742
757
|
failures.push(f)
|
|
743
758
|
// 只保留第一个:旧实现每轮覆盖,最终上报的是**最后一个兜底候选**的错误,
|
|
744
759
|
// 真正有信息量的主因被彻底丢弃(#18 根因 2)。
|
package/lib/capture.js
CHANGED
|
@@ -370,6 +370,10 @@ export async function summarizeTurnDetailed(llm, route, conversation, opts = {})
|
|
|
370
370
|
const out = activeAssembler.blocks().filter((b) => b.type === 'text').map((b) => b.text).join('').trim()
|
|
371
371
|
const parsed = parseCaptureJsonDetailed(out)
|
|
372
372
|
if (!parsed.card) {
|
|
373
|
+
// 模型回了散文(完全没有 JSON 对象):给独立错误码,调用方据此**记住该模型不守格式**
|
|
374
|
+
if (parsed.noJson && out) {
|
|
375
|
+
return { failure: { code: 'MODEL_NOT_JSON', message: '模型没有按 JSON 格式输出(纯文本回复,未发现任何 JSON 对象)—— 多半是模型不遵守结构化输出;建议换一个 provider/model,或开启「仅试首选」避免反复轮换:' + describeOutputExcerpt(out) } }
|
|
376
|
+
}
|
|
373
377
|
const why = out ? '模型输出无法解析为 JSON' : '模型返回空文本'
|
|
374
378
|
// 括号没闭合 = 截断的强特征:即便宿主没给 max-tokens finish,也把方向指对(#18 建议 1)
|
|
375
379
|
const hint = out && looksTruncatedJson(out) ? '(JSON 括号未闭合,疑似输出被截断)' : ''
|
|
@@ -604,7 +608,7 @@ function cardFromParsedJson(parsed) {
|
|
|
604
608
|
* parsed=true → JSON 解析成功;card=null 表示内容不合格 / 不值得保存
|
|
605
609
|
*/
|
|
606
610
|
export function parseCaptureJsonDetailed(text) {
|
|
607
|
-
if (!text) return { card: null, parsed: false, error: '空输出', repaired: false }
|
|
611
|
+
if (!text) return { card: null, parsed: false, error: '空输出', repaired: false, noJson: true }
|
|
608
612
|
// 去掉可能的 ```json 围栏
|
|
609
613
|
const cleaned = String(text).replace(/^```(?:json)?\s*/i, '').replace(/```\s*$/, '').trim()
|
|
610
614
|
const bases = [cleaned]
|
|
@@ -634,7 +638,9 @@ export function parseCaptureJsonDetailed(text) {
|
|
|
634
638
|
}
|
|
635
639
|
return { card: cardFromParsedJson(parsed), parsed: true, error: '', repaired: candidate.repaired }
|
|
636
640
|
}
|
|
637
|
-
|
|
641
|
+
// noJson=true = 输出里**根本没有 JSON 对象**(纯散文):这不是「格式写坏了」,是模型不守格式。
|
|
642
|
+
// 两者要分开报/分开处理 —— 前者换模型才有用,后者重试可能成功(issue #47 的日志就是前者)。
|
|
643
|
+
return { card: null, parsed: false, error: lastError || '输出不是合法 JSON', repaired: false, noJson: !cleaned.includes('{') }
|
|
638
644
|
}
|
|
639
645
|
|
|
640
646
|
/** 兼容旧签名:解析成卡片;解析失败 / 内容不合格返回 null。 */
|
package/lib/client.js
CHANGED
|
@@ -347,7 +347,7 @@ Option A, because:
|
|
|
347
347
|
height: calc(100vh - var(--dsh-frame-overlay-top, 68px));
|
|
348
348
|
max-height: calc(100vh - var(--dsh-frame-overlay-top, 68px));
|
|
349
349
|
}
|
|
350
|
-
`;function Ta(t){t.effect(()=>t.locale.register(pt,{zh:ta,en:aa}),"memory-eternal: locale");let i=t.locale.bind(pt);t.effect(()=>t.slots.inject("settings.section",()=>t.slots.register({name:"settings.section",id:pt,order:25,label:()=>i("nav"),locale:pt,inject:()=>({})},()=>o.default.createElement(za,{t:i}))),"memory-eternal: settings section"),t.effect(()=>t.slots.inject("sidebar.footer.action",()=>t.slots.register({name:"sidebar.footer.action",id:`${pt}:footer`,order:100,label:()=>i("nav"),locale:pt,inject:()=>({})},s=>o.default.createElement(Ia,{t:i,locale:t.locale,wide:!(s&&s.wide===!1)}))),"memory-eternal: sidebar footer action")}function Da(){let[t,i]=(0,o.useState)(K+"/ui/app");return(0,o.useEffect)(()=>{let s=!0,m=()=>fetch("/memory-eternal/api/web-info").then(p=>p.json()).then(p=>{s&&p&&p.ok&&p.alive!==!1&&p.url&&i(p.url.endsWith("/")?p.url:p.url+"/")}).catch(()=>{});return fetch(K+"/ui/app",{method:"HEAD"}).then(p=>{if(s&&!p.ok)return m()}).catch(()=>s?m():void 0),()=>{s=!1}},[]),t}function za({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)(ra,{t,onReload:()=>{},version:0})]})}function Ia({t,locale:i,wide:s}){let[m,p]=(0,o.useState)(!1);return(0,e.jsxs)("div",{className:`me-footer${s?"":" rail"}`,children:[(0,e.jsx)("style",{children:Ee}),(0,e.jsxs)("button",{type:"button",className:"me-footer-btn",onClick:()=>p(!0),"aria-label":t("nav"),title:t("nav"),children:[(0,e.jsx)("span",{className:"me-footer-ico","aria-hidden":"true",children:(0,e.jsx)(oa,{})}),(0,e.jsx)("span",{className:"me-footer-label",children:t("nav")})]}),m&&(0,e.jsx)(Ba,{t,locale:i,onClose:()=>p(!1)})]})}function Ba({t,locale:i,onClose:s,tab:m}){let[p,r]=(0,o.useState)(!1),v=(0,o.useRef)(null),R=Da(),N=(0,o.useCallback)(I=>i&&typeof i.subscribe=="function"?i.subscribe(I):()=>{},[i]),P=(0,o.useCallback)(()=>i&&typeof i.getSnapshot=="function"?i.getSnapshot():null,[i]),C=(0,o.useSyncExternalStore)(N,P),k=C&&(C.active==="zh"||C.active==="en")?C.active:null,g=(0,o.useRef)(null);g.current===null&&k&&(g.current=k);let B=(0,o.useMemo)(()=>{let I=g.current?`lang=${g.current}`:"";if(m==="config")return`${K}/ui/config?tab=config${I?`&${I}`:""}`;let S=R.replace(/\/$/,""),q=S.endsWith("/ui/app")?"?":"/?";return m?`${S}${q}tab=${m}${I?`&${I}`:""}`:I?`${S}${q}${I}`:R},[R,m]);return(0,o.useEffect)(()=>{let I=v.current&&v.current.contentWindow;I&&k&&I.postMessage({source:"memory-eternal",type:"locale",lang:k},"*")},[k]),(0,o.useEffect)(()=>{let I=S=>{S.key==="Escape"&&s()};return window.addEventListener("keydown",I),()=>window.removeEventListener("keydown",I)},[s]),(0,e.jsxs)("div",{className:"me-overlay-top",onClick:s,children:[(0,e.jsx)("style",{children:Ee}),(0,e.jsx)("div",{className:"me-modal"+(p?" me-modal-full":""),onClick:I=>I.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:()=>r(I=>!I),"aria-label":t(p?"exitFullscreen":"enterFullscreen"),title:t(p?"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:p?"\u2921":"\u2922"}),(0,e.jsx)("button",{type:"button",onClick:s,"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:v,src:B,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 oa(){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 vt="0.10.
|
|
350
|
+
`;function Ta(t){t.effect(()=>t.locale.register(pt,{zh:ta,en:aa}),"memory-eternal: locale");let i=t.locale.bind(pt);t.effect(()=>t.slots.inject("settings.section",()=>t.slots.register({name:"settings.section",id:pt,order:25,label:()=>i("nav"),locale:pt,inject:()=>({})},()=>o.default.createElement(za,{t:i}))),"memory-eternal: settings section"),t.effect(()=>t.slots.inject("sidebar.footer.action",()=>t.slots.register({name:"sidebar.footer.action",id:`${pt}:footer`,order:100,label:()=>i("nav"),locale:pt,inject:()=>({})},s=>o.default.createElement(Ia,{t:i,locale:t.locale,wide:!(s&&s.wide===!1)}))),"memory-eternal: sidebar footer action")}function Da(){let[t,i]=(0,o.useState)(K+"/ui/app");return(0,o.useEffect)(()=>{let s=!0,m=()=>fetch("/memory-eternal/api/web-info").then(p=>p.json()).then(p=>{s&&p&&p.ok&&p.alive!==!1&&p.url&&i(p.url.endsWith("/")?p.url:p.url+"/")}).catch(()=>{});return fetch(K+"/ui/app",{method:"HEAD"}).then(p=>{if(s&&!p.ok)return m()}).catch(()=>s?m():void 0),()=>{s=!1}},[]),t}function za({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)(ra,{t,onReload:()=>{},version:0})]})}function Ia({t,locale:i,wide:s}){let[m,p]=(0,o.useState)(!1);return(0,e.jsxs)("div",{className:`me-footer${s?"":" rail"}`,children:[(0,e.jsx)("style",{children:Ee}),(0,e.jsxs)("button",{type:"button",className:"me-footer-btn",onClick:()=>p(!0),"aria-label":t("nav"),title:t("nav"),children:[(0,e.jsx)("span",{className:"me-footer-ico","aria-hidden":"true",children:(0,e.jsx)(oa,{})}),(0,e.jsx)("span",{className:"me-footer-label",children:t("nav")})]}),m&&(0,e.jsx)(Ba,{t,locale:i,onClose:()=>p(!1)})]})}function Ba({t,locale:i,onClose:s,tab:m}){let[p,r]=(0,o.useState)(!1),v=(0,o.useRef)(null),R=Da(),N=(0,o.useCallback)(I=>i&&typeof i.subscribe=="function"?i.subscribe(I):()=>{},[i]),P=(0,o.useCallback)(()=>i&&typeof i.getSnapshot=="function"?i.getSnapshot():null,[i]),C=(0,o.useSyncExternalStore)(N,P),k=C&&(C.active==="zh"||C.active==="en")?C.active:null,g=(0,o.useRef)(null);g.current===null&&k&&(g.current=k);let B=(0,o.useMemo)(()=>{let I=g.current?`lang=${g.current}`:"";if(m==="config")return`${K}/ui/config?tab=config${I?`&${I}`:""}`;let S=R.replace(/\/$/,""),q=S.endsWith("/ui/app")?"?":"/?";return m?`${S}${q}tab=${m}${I?`&${I}`:""}`:I?`${S}${q}${I}`:R},[R,m]);return(0,o.useEffect)(()=>{let I=v.current&&v.current.contentWindow;I&&k&&I.postMessage({source:"memory-eternal",type:"locale",lang:k},"*")},[k]),(0,o.useEffect)(()=>{let I=S=>{S.key==="Escape"&&s()};return window.addEventListener("keydown",I),()=>window.removeEventListener("keydown",I)},[s]),(0,e.jsxs)("div",{className:"me-overlay-top",onClick:s,children:[(0,e.jsx)("style",{children:Ee}),(0,e.jsx)("div",{className:"me-modal"+(p?" me-modal-full":""),onClick:I=>I.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:()=>r(I=>!I),"aria-label":t(p?"exitFullscreen":"enterFullscreen"),title:t(p?"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:p?"\u2921":"\u2922"}),(0,e.jsx)("button",{type:"button",onClick:s,"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:v,src:B,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 oa(){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 vt="0.10.51";function La({t,inModal:i,onClose:s,onFull:m,full:p}){let[r,v]=(0,o.useState)(null),[R,N]=(0,o.useState)([]),[P,C]=(0,o.useState)(!0),[k,g]=(0,o.useState)(null),[B,I]=(0,o.useState)(""),[S,q]=(0,o.useState)("all"),[L,_]=(0,o.useState)(""),[J,H]=(0,o.useState)("all"),[D,Te]=(0,o.useState)("all"),[ue,se]=(0,o.useState)(""),[F,M]=(0,o.useState)(()=>{try{let h=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(h&&n[h])return n[h]}catch{}return"cards"}),[$,de]=(0,o.useState)(()=>new Set([F])),Ce=(0,o.useCallback)(h=>{de(n=>n.has(h)?n:new Set(n).add(h)),M(h)},[]),[fe,u]=(0,o.useState)("recent"),[Ie,re]=(0,o.useState)(null),y=(0,o.useRef)(null),Y=(0,o.useRef)(null),[te,ye]=(0,o.useState)(!1),[Z,X]=(0,o.useState)(null),[Fe,Me]=(0,o.useState)(!1),[Qe,De]=(0,o.useState)(!1),[l,T]=(0,o.useState)(!0),[j,Q]=(0,o.useState)(null),[Ne,ge]=(0,o.useState)(0),le=(0,o.useCallback)(()=>ge(h=>h+1),[]),ae=r?.quarantine?.pending??0,he=r?.quarantine?.deleted??0,ve=(0,o.useRef)(null),[c,d]=(0,o.useState)(0),[V,ce]=(0,o.useState)(!1),xe=(0,o.useRef)(null),Be=(0,o.useRef)(null),a=(0,o.useCallback)(async(h=S,n=L,x=D,w={})=>{let E=w.append===!0;try{E?ce(!0):(C(!0),I(""));let U=new URLSearchParams;h&&h!=="all"&&U.set("kind",h),n.trim()&&U.set("q",n.trim());let O=w.tag!==void 0?w.tag:ue;O&&U.set("tag",O),U.set("status",x||"all"),U.set("limit",String(Ca)),U.set("offset",String(w.offset||0)),fe==="title"&&U.set("sort","title");let we=await(await fetch(`${K}/cards?${U.toString()}`)).json();if(we.ok){let Ae=we.cards||[];N(Pe=>E?[...Pe,...Ae.filter(wt=>!Pe.some(St=>St.path===wt.path))]:Ae),d(Number(we.total)||Ae.length)}}catch(U){I(String(U&&U.message?U.message:U))}finally{E?ce(!1):C(!1)}},[S,L,D,fe,ue]),ze=(0,o.useCallback)(async()=>{try{let[h,n]=await Promise.all([fetch(`${K}/overview`).then(x=>x.json()),fetch(`${K}/capture-log`).then(x=>x.json()).catch(()=>null)]);h.ok&&v(h),g(n&&n.ok&&n.available&&n.health||null)}catch(h){I(String(h&&h.message?h.message:h))}finally{C(!1)}},[]),nt=async h=>{try{let x=await(await fetch(`${K}/delete?path=${encodeURIComponent(h)}`)).json();x.ok?(re({ok:!0,msg:t("deleted")}),await ze(),le()):re({ok:!1,msg:x.error||t("deleteFail")})}catch{re({ok:!1,msg:t("deleteFail")})}y.current&&clearTimeout(y.current),y.current=setTimeout(()=>re(null),2e3)};(0,o.useEffect)(()=>(ze(),()=>{ve.current&&clearTimeout(ve.current)}),[ze]);let lt=h=>{_(h),ve.current&&clearTimeout(ve.current),ve.current=setTimeout(()=>a(S,h,D),280)},We=h=>{q(h),a(h,L,D)},qe=h=>{let n=String(h||"");se(n),X(null),a(S,L,D,{tag:n})};(0,o.useEffect)(()=>{F==="cards"&&a(S,L,D)},[D,F,Ne,fe,ue]),(0,o.useEffect)(()=>{if(F!=="cards"||P||V||R.length>=c)return;let h=xe.current,n=Be.current;if(!h)return;let x=new IntersectionObserver(w=>{w[0]&&w[0].isIntersecting&&a(S,L,D,{append:!0,offset:R.length})},{root:n||null,rootMargin:"400px"});return x.observe(h),()=>x.disconnect()},[F,R,c,P,V,S,L,D,a]);let ot=async h=>{try{let n=h.status;if(!n&&h.path)try{let O=((await fetch(`${K}/cards?status=all&q=${encodeURIComponent(h.title||"")}&limit=50`).then(ee=>ee.json())).cards||[]).find(ee=>ee.path===h.path);O&&(n=O.status)}catch{}let x=n&&n!=="approved"?`&status=${encodeURIComponent(n)}`:"",w=await fetch(`${K}/card?path=${encodeURIComponent(h.path||h.id)}${x}`),E=await w.json();E.ok?X({path:E.path,title:h.title,text:E.text}):I(E.error||`HTTP ${w.status}`)}catch(n){I(String(n&&n.message?n.message:n))}},Ge=async(h,n)=>{ye(!0);try{let x=await fetch(`${K}/export`);if(!x.ok)throw new Error("HTTP "+x.status);let w=await x.json();if(!w.ok)throw new Error(w.error||"export failed");let E,U,O;if(h==="json"){U="memory-vault.json",O="application/json";let Ae=w.cards.map(Pe=>({path:Pe.path,title:Pe.title,kind:Pe.kind,status:Pe.status,store:Pe.store,text:Pe.text}));E=JSON.stringify({format:w.format||"memory-eternal-vault",formatVersion:w.formatVersion||1,exportedAt:w.exportedAt||new Date().toISOString(),count:Ae.length,cards:Ae},null,2)}else U="memory-vault.md",O="text/markdown;charset=utf-8",E=w.cards.map(Ae=>Ae.text.trim()).filter(Boolean).join(`
|
|
351
351
|
|
|
352
352
|
---
|
|
353
353
|
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// 「模型不守 JSON」的记忆(issue #47)。
|
|
2
|
+
//
|
|
3
|
+
// 现场:stepfun / step-api 这类模型会把蒸馏请求当聊天回(「已读取并理解以下技能约束…」),
|
|
4
|
+
// 解析失败 → 轮换下一个 provider。问题是不记录,于是**每轮对话都要先白烧一次调用**。
|
|
5
|
+
// 这里把「哪个 provider/model 最近连续不守格式」记下来,选路时把它们排到候选末尾
|
|
6
|
+
// (不删除:万一全都命中过,仍按原顺序兜底,绝不把自己饿死)。
|
|
7
|
+
import fs from 'node:fs'
|
|
8
|
+
import { configFilePath } from './capture-run.js'
|
|
9
|
+
|
|
10
|
+
/** 打击记录文件:与共享配置同目录(<config>.llm-strikes.json)。 */
|
|
11
|
+
export function strikesPath(env = process.env) {
|
|
12
|
+
return String(configFilePath(env)).replace(/\.json$/i, '.llm-strikes.json')
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** 路由键:provider|model。 */
|
|
16
|
+
export function routeKey(route) {
|
|
17
|
+
return String((route && route.provider) || '') + '|' + String((route && route.model) || '')
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function readAll(env) {
|
|
21
|
+
try {
|
|
22
|
+
const d = JSON.parse(fs.readFileSync(strikesPath(env), 'utf8'))
|
|
23
|
+
return d && typeof d === 'object' && d.strikes && typeof d.strikes === 'object' ? d.strikes : {}
|
|
24
|
+
} catch { return {} }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** 记一次「不守 JSON」。只保留最近 50 个路由,避免文件无限增长。 */
|
|
28
|
+
export function recordModelStrike(env, route, code = 'MODEL_NOT_JSON', now = Date.now()) {
|
|
29
|
+
const key = routeKey(route)
|
|
30
|
+
if (!key || key === '|') return null
|
|
31
|
+
const all = readAll(env)
|
|
32
|
+
const prev = all[key] && typeof all[key] === 'object' ? all[key] : { count: 0, at: 0 }
|
|
33
|
+
all[key] = { count: (Number(prev.count) || 0) + 1, at: Number(now) || Date.now(), code: String(code || '') }
|
|
34
|
+
const keys = Object.keys(all)
|
|
35
|
+
if (keys.length > 50) {
|
|
36
|
+
keys.sort((a, b) => (Number(all[a].at) || 0) - (Number(all[b].at) || 0))
|
|
37
|
+
for (const k of keys.slice(0, keys.length - 50)) delete all[k]
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
fs.mkdirSync(String(strikesPath(env)).replace(/[\\/][^\\/]+$/, ''), { recursive: true })
|
|
41
|
+
fs.writeFileSync(strikesPath(env), JSON.stringify({ version: 1, strikes: all }, null, 2), 'utf8')
|
|
42
|
+
return all[key]
|
|
43
|
+
} catch { return null }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** 最近 window 内的打击次数(默认 24h)。 */
|
|
47
|
+
export function recentModelStrikes(env, { hours = 24, now = Date.now() } = {}) {
|
|
48
|
+
const all = readAll(env)
|
|
49
|
+
const cut = Number(now) - Math.max(1, Number(hours) || 24) * 3600 * 1000
|
|
50
|
+
const out = {}
|
|
51
|
+
for (const [key, v] of Object.entries(all)) {
|
|
52
|
+
const at = Number(v && v.at) || 0
|
|
53
|
+
if (at < cut) continue
|
|
54
|
+
const count = Number(v && v.count) || 0
|
|
55
|
+
if (count > 0) out[key] = count
|
|
56
|
+
}
|
|
57
|
+
return out
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 纯函数:把「近期不守格式」的路由排到后面(稳定排序,阈值默认 2 次)。
|
|
62
|
+
* 全部命中过时保持原顺序(宁可慢一点,也不能没有候选)。
|
|
63
|
+
*/
|
|
64
|
+
export function rankRoutesByStrikes(routes = [], strikes = {}, { threshold = 2 } = {}) {
|
|
65
|
+
const list = Array.isArray(routes) ? routes : []
|
|
66
|
+
const th = Math.max(1, Number(threshold) || 2)
|
|
67
|
+
const isBad = (r) => (Number(strikes[routeKey(r)]) || 0) >= th
|
|
68
|
+
const clean = list.filter((r) => !isBad(r))
|
|
69
|
+
const bad = list.filter(isBad)
|
|
70
|
+
if (!clean.length) return list.slice() // 全都命中过 → 原顺序兜底
|
|
71
|
+
return [...clean, ...bad]
|
|
72
|
+
}
|
package/lib/support.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// 一键诊断包(dsh-memory support):把「用户报 issue 时我每次都要挨个要的东西」打成**一个 zip**。
|
|
2
|
+
//
|
|
3
|
+
// 为什么需要:issue #47 的用户贴了诊断,但被 GitHub 截断(内容过长),我拿不到完整现场,
|
|
4
|
+
// 只能来回问。这个命令把 版本矩阵 / doctor / align / status / 锁 / 心跳 / 配置 / 日志尾巴 一次收齐,
|
|
5
|
+
// 并在写入前**统一脱敏**(家目录、sk-*/ghp_*、Bearer、配置里 key 类字段)。
|
|
6
|
+
import fs from 'node:fs'
|
|
7
|
+
import os from 'node:os'
|
|
8
|
+
import path from 'node:path'
|
|
9
|
+
import { spawnSync } from 'node:child_process'
|
|
10
|
+
import { makeZip, listZipNames } from './zip.js'
|
|
11
|
+
import { captureLogPath } from './capture-log.js'
|
|
12
|
+
import { configFilePath, defaultVaultDir } from './capture-run.js'
|
|
13
|
+
import { PACKAGE_ROOT } from './setup.js'
|
|
14
|
+
|
|
15
|
+
/** 脱敏(纯函数,可单测):家目录 + 常见凭据形态 + 配置里 key 类字段。 */
|
|
16
|
+
export function redactText(text, { home = os.homedir() } = {}) {
|
|
17
|
+
let s = String(text ?? '')
|
|
18
|
+
const h = String(home || '')
|
|
19
|
+
if (h) {
|
|
20
|
+
// 同时处理 Windows 反斜杠与正斜杠两种写法
|
|
21
|
+
s = s.split(h).join('<home>')
|
|
22
|
+
s = s.split(h.replace(/\\/g, '/')).join('<home>')
|
|
23
|
+
}
|
|
24
|
+
s = s.replace(/\bsk-[A-Za-z0-9_-]{12,}/g, 'sk-***')
|
|
25
|
+
s = s.replace(/\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{20,}/g, 'gh*_***')
|
|
26
|
+
s = s.replace(/\bgithub_pat_[A-Za-z0-9_]{20,}/g, 'github_pat_***')
|
|
27
|
+
s = s.replace(/\bBearer\s+[A-Za-z0-9._~+\/-]{10,}=*/gi, 'Bearer ***')
|
|
28
|
+
// 配置/JSON 里的敏感字段:"secretHint": "..." / token=... 等
|
|
29
|
+
s = s.replace(/"((?:secret|token|api_?key|apikey|password|credential)[A-Za-z0-9_]*)"\s*:\s*"[^"]*"/gi, '"$1": "***"')
|
|
30
|
+
s = s.replace(/\b((?:secret|token|api_?key|apikey|password)[A-Za-z0-9_]*)\s*=\s*[^\s,;]+/gi, '$1=***')
|
|
31
|
+
return s
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** 跑自己的 CLI 子命令拿 JSON/文本;失败时返回错误文本而不是让整个包失败。 */
|
|
35
|
+
function runCli(args, { timeoutMs = 20000 } = {}) {
|
|
36
|
+
try {
|
|
37
|
+
const self = path.join(PACKAGE_ROOT, 'bin', 'dsh-memory.mjs')
|
|
38
|
+
const r = spawnSync(process.execPath, [self, ...args], { encoding: 'utf8', timeout: timeoutMs, windowsHide: true })
|
|
39
|
+
const out = String(r.stdout || '')
|
|
40
|
+
const err = String(r.stderr || '')
|
|
41
|
+
if (r.error) return { ok: false, text: '执行失败:' + String(r.error.message || r.error) + '\n' + out + err }
|
|
42
|
+
return { ok: r.status === 0, text: out || err || ('(无输出,退出码 ' + r.status + ')') }
|
|
43
|
+
} catch (e) {
|
|
44
|
+
return { ok: false, text: '执行异常:' + String((e && e.message) || e) }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function readIfExists(file, { tailLines = 0 } = {}) {
|
|
49
|
+
try {
|
|
50
|
+
const raw = fs.readFileSync(file, 'utf8')
|
|
51
|
+
if (!tailLines) return raw
|
|
52
|
+
const lines = raw.split(/\r?\n/).filter((l) => l.trim())
|
|
53
|
+
return lines.slice(-Math.max(1, tailLines)).join('\n')
|
|
54
|
+
} catch { return '' }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function versionOfPackage() {
|
|
58
|
+
try { return String(JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, 'package.json'), 'utf8')).version || '') } catch { return '' }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function defaultOutDir() {
|
|
62
|
+
const dl = path.join(os.homedir(), 'Downloads')
|
|
63
|
+
try { if (fs.statSync(dl).isDirectory()) return dl } catch { /* 没有 Downloads 就用当前目录 */ }
|
|
64
|
+
return process.cwd()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 生成诊断包。
|
|
69
|
+
* @returns {{ok:boolean, path:string, bytes:number, entries:string[]}}
|
|
70
|
+
*/
|
|
71
|
+
export function buildSupportBundle({
|
|
72
|
+
env = process.env,
|
|
73
|
+
vaultRoot = defaultVaultDir(env),
|
|
74
|
+
out = '',
|
|
75
|
+
tail = 200,
|
|
76
|
+
port = 0,
|
|
77
|
+
now = Date.now(),
|
|
78
|
+
spawn = true,
|
|
79
|
+
} = {}) {
|
|
80
|
+
const home = os.homedir()
|
|
81
|
+
const entries = []
|
|
82
|
+
const add = (name, text) => entries.push({ name, data: redactText(text, { home }) })
|
|
83
|
+
|
|
84
|
+
const meta = {
|
|
85
|
+
at: new Date(now).toISOString(),
|
|
86
|
+
pluginVersion: versionOfPackage(),
|
|
87
|
+
node: process.version,
|
|
88
|
+
platform: process.platform + ' ' + process.arch + ' ' + (os.release ? os.release() : ''),
|
|
89
|
+
vaultDir: vaultRoot,
|
|
90
|
+
dshHome: env.DSH_HOME || '',
|
|
91
|
+
hasLlmEnv: Boolean(env.MEMORY_LLM_BASE_URL || env.MEMORY_LLM_KEY),
|
|
92
|
+
note: '本包已脱敏:家目录→<home>,sk-/ghp_/github_pat/Bearer/配置里的 key 类字段→***',
|
|
93
|
+
}
|
|
94
|
+
add('meta.json', JSON.stringify(meta, null, 2))
|
|
95
|
+
if (spawn) {
|
|
96
|
+
add('doctor.json', runCli(['doctor', '--vault', vaultRoot, '--json']).text)
|
|
97
|
+
add('align.json', runCli(['align', '--json']).text)
|
|
98
|
+
add('status.txt', runCli(['status', ...(port ? ['--port', String(port)] : [])]).text)
|
|
99
|
+
}
|
|
100
|
+
add('lock.json', readIfExists(path.join(path.dirname(configFilePath(env)), 'memory-eternal-watchdog.json')))
|
|
101
|
+
add('host.json', readIfExists(String(configFilePath(env)).replace(/\.json$/i, '.host.json')))
|
|
102
|
+
add('config.json', readIfExists(configFilePath(env)))
|
|
103
|
+
add('capture-log.tail.jsonl', readIfExists(captureLogPath(env), { tailLines: Math.max(1, Number(tail) || 200) }))
|
|
104
|
+
add('README.txt', [
|
|
105
|
+
'记忆核心 · 诊断包',
|
|
106
|
+
'',
|
|
107
|
+
'内容:meta / doctor / align / status / lock / host / config / capture-log.tail / 本说明。',
|
|
108
|
+
'隐私:已脱敏(家目录→<home>;sk-/ghp_/github_pat/Bearer/配置里 key 类字段→***)。',
|
|
109
|
+
'提交 issue 前请自己再扫一眼:日志里可能有你的会话标题或路径片段。',
|
|
110
|
+
].join('\n'))
|
|
111
|
+
|
|
112
|
+
const zipBuf = makeZip(entries, new Date(now))
|
|
113
|
+
const stamp = new Date(now).toISOString().replace(/[:.]/g, '-').slice(0, 19)
|
|
114
|
+
const target = out ? path.resolve(out) : path.join(defaultOutDir(), 'memory-eternal-support-' + stamp + '.zip')
|
|
115
|
+
fs.mkdirSync(path.dirname(target), { recursive: true })
|
|
116
|
+
fs.writeFileSync(target, zipBuf)
|
|
117
|
+
return { ok: true, path: target, bytes: zipBuf.length, entries: listZipNames(zipBuf) }
|
|
118
|
+
}
|
package/lib/zip.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// 零依赖 ZIP 写入器(store 模式,不压缩)。
|
|
2
|
+
//
|
|
3
|
+
// 为什么不用第三方:插件发布体积要小、不能给用户装依赖;而 zip 是最通用的「用户能直接双击打开」格式。
|
|
4
|
+
// store(method 0)已经够用:诊断包里最大的是 capture 日志(几百 KB),压不压缩区别不大。
|
|
5
|
+
import zlib from 'node:zlib'
|
|
6
|
+
|
|
7
|
+
/** CRC32(ZIP 必需)。表在模块内只建一次。 */
|
|
8
|
+
const CRC_TABLE = (() => {
|
|
9
|
+
const t = new Uint32Array(256)
|
|
10
|
+
for (let i = 0; i < 256; i++) {
|
|
11
|
+
let c = i
|
|
12
|
+
for (let k = 0; k < 8; k++) c = c & 1 ? 0xEDB88320 ^ (c >>> 1) : c >>> 1
|
|
13
|
+
t[i] = c >>> 0
|
|
14
|
+
}
|
|
15
|
+
return t
|
|
16
|
+
})()
|
|
17
|
+
|
|
18
|
+
export function crc32(buf) {
|
|
19
|
+
let c = 0xFFFFFFFF
|
|
20
|
+
for (let i = 0; i < buf.length; i++) c = CRC_TABLE[(c ^ buf[i]) & 0xFF] ^ (c >>> 8)
|
|
21
|
+
return (c ^ 0xFFFFFFFF) >>> 0
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** DOS 时间戳(zip 头需要,取当前时间即可)。 */
|
|
25
|
+
function dosTime(d = new Date()) {
|
|
26
|
+
const time = ((d.getHours() & 0x1F) << 11) | ((d.getMinutes() & 0x3F) << 5) | ((Math.floor(d.getSeconds() / 2)) & 0x1F)
|
|
27
|
+
const date = (((d.getFullYear() - 1980) & 0x7F) << 9) | (((d.getMonth() + 1) & 0x0F) << 5) | (d.getDate() & 0x1F)
|
|
28
|
+
return { time, date }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 生成 zip buffer。
|
|
33
|
+
* @param {Array<{name: string, data: Buffer|string}>} entries
|
|
34
|
+
* @returns {Buffer}
|
|
35
|
+
*/
|
|
36
|
+
export function makeZip(entries, now = new Date()) {
|
|
37
|
+
const { time, date } = dosTime(now)
|
|
38
|
+
const locals = []
|
|
39
|
+
const centrals = []
|
|
40
|
+
let offset = 0
|
|
41
|
+
for (const e of entries) {
|
|
42
|
+
const name = Buffer.from(String(e.name || '').replace(/\\/g, '/'), 'utf8')
|
|
43
|
+
const raw = Buffer.isBuffer(e.data) ? e.data : Buffer.from(String(e.data ?? ''), 'utf8')
|
|
44
|
+
// 小文件顺手 deflate(日志压缩比很高);失败就退回 store
|
|
45
|
+
let body = raw
|
|
46
|
+
let method = 0
|
|
47
|
+
if (raw.length > 512) {
|
|
48
|
+
try { const z = zlib.deflateRawSync(raw); if (z.length < raw.length) { body = z; method = 8 } } catch { /* 保持 store */ }
|
|
49
|
+
}
|
|
50
|
+
const crc = crc32(raw)
|
|
51
|
+
const local = Buffer.alloc(30)
|
|
52
|
+
local.writeUInt32LE(0x04034b50, 0)
|
|
53
|
+
local.writeUInt16LE(20, 4) // version needed
|
|
54
|
+
local.writeUInt16LE(0x0800, 6) // flags: UTF-8 文件名
|
|
55
|
+
local.writeUInt16LE(method, 8)
|
|
56
|
+
local.writeUInt16LE(time, 10)
|
|
57
|
+
local.writeUInt16LE(date, 12)
|
|
58
|
+
local.writeUInt32LE(crc, 14)
|
|
59
|
+
local.writeUInt32LE(body.length, 18)
|
|
60
|
+
local.writeUInt32LE(raw.length, 22)
|
|
61
|
+
local.writeUInt16LE(name.length, 26)
|
|
62
|
+
local.writeUInt16LE(0, 28)
|
|
63
|
+
locals.push(local, name, body)
|
|
64
|
+
|
|
65
|
+
const central = Buffer.alloc(46)
|
|
66
|
+
central.writeUInt32LE(0x02014b50, 0)
|
|
67
|
+
central.writeUInt16LE(20, 4)
|
|
68
|
+
central.writeUInt16LE(20, 6)
|
|
69
|
+
central.writeUInt16LE(0x0800, 8)
|
|
70
|
+
central.writeUInt16LE(method, 10)
|
|
71
|
+
central.writeUInt16LE(time, 12)
|
|
72
|
+
central.writeUInt16LE(date, 14)
|
|
73
|
+
central.writeUInt32LE(crc, 16)
|
|
74
|
+
central.writeUInt32LE(body.length, 20)
|
|
75
|
+
central.writeUInt32LE(raw.length, 24)
|
|
76
|
+
central.writeUInt16LE(name.length, 28)
|
|
77
|
+
central.writeUInt16LE(0, 30)
|
|
78
|
+
central.writeUInt16LE(0, 32)
|
|
79
|
+
central.writeUInt16LE(0, 34)
|
|
80
|
+
central.writeUInt16LE(0, 36)
|
|
81
|
+
central.writeUInt32LE(0, 38)
|
|
82
|
+
central.writeUInt32LE(offset, 42)
|
|
83
|
+
centrals.push(central, name)
|
|
84
|
+
offset += local.length + name.length + body.length
|
|
85
|
+
}
|
|
86
|
+
const centralBuf = Buffer.concat(centrals)
|
|
87
|
+
const end = Buffer.alloc(22)
|
|
88
|
+
end.writeUInt32LE(0x06054b50, 0)
|
|
89
|
+
end.writeUInt16LE(0, 4)
|
|
90
|
+
end.writeUInt16LE(0, 6)
|
|
91
|
+
end.writeUInt16LE(entries.length, 8)
|
|
92
|
+
end.writeUInt16LE(entries.length, 10)
|
|
93
|
+
end.writeUInt32LE(centralBuf.length, 12)
|
|
94
|
+
end.writeUInt32LE(offset, 16)
|
|
95
|
+
end.writeUInt16LE(0, 20)
|
|
96
|
+
return Buffer.concat([...locals, centralBuf, end])
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** 读回收录名(自检/单测用:不需要真正解压)。 */
|
|
100
|
+
export function listZipNames(buf) {
|
|
101
|
+
const names = []
|
|
102
|
+
for (let i = 0; i + 46 <= buf.length; i++) {
|
|
103
|
+
if (buf.readUInt32LE(i) !== 0x02014b50) continue
|
|
104
|
+
const nameLen = buf.readUInt16LE(i + 28)
|
|
105
|
+
const extraLen = buf.readUInt16LE(i + 30)
|
|
106
|
+
const commentLen = buf.readUInt16LE(i + 32)
|
|
107
|
+
const start = i + 46
|
|
108
|
+
names.push(buf.slice(start, start + nameLen).toString('utf8'))
|
|
109
|
+
i = start + nameLen + extraLen + commentLen - 1
|
|
110
|
+
}
|
|
111
|
+
return names
|
|
112
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memory-eternal",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.51",
|
|
4
4
|
"description": "记忆核心(Memory Eternal):自研的 DeepSeek Harness 记忆插件,不移植任何既有记忆框架——对话结束后自动沉淀知识卡到本地 Markdown Vault(自研去重、自研 CJK 检索、可 git 管理),设置页提供图形化知识库(统计 / 搜索 / 知识图谱 + 侧边栏一键弹窗),Agent 通过 memory_recall 工具按需召回历史上下文。零人工干预。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"release": "node scripts/release.mjs",
|
|
66
66
|
"release:dry": "node scripts/release.mjs --dry-run",
|
|
67
67
|
"panel-check": "node scripts/panel-check.mjs",
|
|
68
|
-
"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 && node tests/graph-initial-zoom.test.mjs && node tests/card-head-actions.test.mjs && node tests/card-reader-ux.test.mjs && node tests/tag-filter.test.mjs && node tests/ui-overflow.test.mjs && node tests/version-card-state.test.mjs && node tests/llm-routes-proxy.test.mjs"
|
|
68
|
+
"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 && node tests/graph-initial-zoom.test.mjs && node tests/card-head-actions.test.mjs && node tests/card-reader-ux.test.mjs && node tests/tag-filter.test.mjs && node tests/ui-overflow.test.mjs && node tests/version-card-state.test.mjs && node tests/llm-routes-proxy.test.mjs && node tests/model-strikes.test.mjs && node tests/support-bundle.test.mjs"
|
|
69
69
|
},
|
|
70
70
|
"dependencies": {
|
|
71
71
|
"@deepseek-ai/schemastery": "^3.18.1"
|
package/web/app.js
CHANGED
|
@@ -349,7 +349,7 @@ Option A, because:
|
|
|
349
349
|
height: calc(100vh - var(--dsh-frame-overlay-top, 68px));
|
|
350
350
|
max-height: calc(100vh - var(--dsh-frame-overlay-top, 68px));
|
|
351
351
|
}
|
|
352
|
-
`;function Gg(){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 Xo="0.10.
|
|
352
|
+
`;function Gg(){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 Xo="0.10.51";function lf({t:e,inModal:t,onClose:n,onFull:r,full:o}){let[l,a]=(0,x.useState)(null),[s,c]=(0,x.useState)([]),[p,y]=(0,x.useState)(!0),[v,g]=(0,x.useState)(null),[C,M]=(0,x.useState)(""),[S,U]=(0,x.useState)("all"),[d,u]=(0,x.useState)(""),[h,k]=(0,x.useState)("all"),[E,W]=(0,x.useState)("all"),[O,B]=(0,x.useState)(""),[j,R]=(0,x.useState)(()=>{try{let L=new URLSearchParams(typeof location<"u"?location.search:"").get("tab"),m={cards:"cards",graph:"graph",usage:"stats",stats:"stats",audit:"audit",recycle:"optimize",optimize:"optimize",config:"config"};if(L&&m[L])return m[L]}catch{}return"cards"}),[K,ge]=(0,x.useState)(()=>new Set([j])),_e=(0,x.useCallback)(L=>{ge(m=>m.has(L)?m:new Set(m).add(L)),R(L)},[]),[Se,z]=(0,x.useState)("recent"),[Xe,pe]=(0,x.useState)(null),P=(0,x.useRef)(null),ee=(0,x.useRef)(null),[ae,Ce]=(0,x.useState)(!1),[re,ne]=(0,x.useState)(null),[ct,He]=(0,x.useState)(!1),[un,Ve]=(0,x.useState)(!1),[w,A]=(0,x.useState)(!0),[Q,oe]=(0,x.useState)(null),[Fe,we]=(0,x.useState)(0),me=(0,x.useCallback)(()=>we(L=>L+1),[]),se=l?.quarantine?.pending??0,Me=l?.quarantine?.deleted??0,Te=(0,x.useRef)(null),[N,b]=(0,x.useState)(0),[J,he]=(0,x.useState)(!1),Re=(0,x.useRef)(null),Ze=(0,x.useRef)(null),f=(0,x.useCallback)(async(L=S,m=d,_=E,F={})=>{let X=F.append===!0;try{X?he(!0):(y(!0),M(""));let q=new URLSearchParams;L&&L!=="all"&&q.set("kind",L),m.trim()&&q.set("q",m.trim());let Y=F.tag!==void 0?F.tag:O;Y&&q.set("tag",Y),q.set("status",_||"all"),q.set("limit",String(Qg)),q.set("offset",String(F.offset||0)),Se==="title"&&q.set("sort","title");let De=await(await fetch(`${Z}/cards?${q.toString()}`)).json();if(De.ok){let rt=De.cards||[];c(mt=>X?[...mt,...rt.filter(Zo=>!mt.some(Jo=>Jo.path===Zo.path))]:rt),b(Number(De.total)||rt.length)}}catch(q){M(String(q&&q.message?q.message:q))}finally{X?he(!1):y(!1)}},[S,d,E,Se,O]),Ke=(0,x.useCallback)(async()=>{try{let[L,m]=await Promise.all([fetch(`${Z}/overview`).then(_=>_.json()),fetch(`${Z}/capture-log`).then(_=>_.json()).catch(()=>null)]);L.ok&&a(L),g(m&&m.ok&&m.available&&m.health||null)}catch(L){M(String(L&&L.message?L.message:L))}finally{y(!1)}},[]),Kn=async L=>{try{let _=await(await fetch(`${Z}/delete?path=${encodeURIComponent(L)}`)).json();_.ok?(pe({ok:!0,msg:e("deleted")}),await Ke(),me()):pe({ok:!1,msg:_.error||e("deleteFail")})}catch{pe({ok:!1,msg:e("deleteFail")})}P.current&&clearTimeout(P.current),P.current=setTimeout(()=>pe(null),2e3)};(0,x.useEffect)(()=>(Ke(),()=>{Te.current&&clearTimeout(Te.current)}),[Ke]);let fr=L=>{u(L),Te.current&&clearTimeout(Te.current),Te.current=setTimeout(()=>f(S,L,E),280)},Tt=L=>{U(L),f(L,d,E)},Wt=L=>{let m=String(L||"");B(m),ne(null),f(S,d,E,{tag:m})};(0,x.useEffect)(()=>{j==="cards"&&f(S,d,E)},[E,j,Fe,Se,O]),(0,x.useEffect)(()=>{if(j!=="cards"||p||J||s.length>=N)return;let L=Re.current,m=Ze.current;if(!L)return;let _=new IntersectionObserver(F=>{F[0]&&F[0].isIntersecting&&f(S,d,E,{append:!0,offset:s.length})},{root:m||null,rootMargin:"400px"});return _.observe(L),()=>_.disconnect()},[j,s,N,p,J,S,d,E,f]);let Qn=async L=>{try{let m=L.status;if(!m&&L.path)try{let Y=((await fetch(`${Z}/cards?status=all&q=${encodeURIComponent(L.title||"")}&limit=50`).then(le=>le.json())).cards||[]).find(le=>le.path===L.path);Y&&(m=Y.status)}catch{}let _=m&&m!=="approved"?`&status=${encodeURIComponent(m)}`:"",F=await fetch(`${Z}/card?path=${encodeURIComponent(L.path||L.id)}${_}`),X=await F.json();X.ok?ne({path:X.path,title:L.title,text:X.text}):M(X.error||`HTTP ${F.status}`)}catch(m){M(String(m&&m.message?m.message:m))}},Zt=async(L,m)=>{Ce(!0);try{let _=await fetch(`${Z}/export`);if(!_.ok)throw new Error("HTTP "+_.status);let F=await _.json();if(!F.ok)throw new Error(F.error||"export failed");let X,q,Y;if(L==="json"){q="memory-vault.json",Y="application/json";let rt=F.cards.map(mt=>({path:mt.path,title:mt.title,kind:mt.kind,status:mt.status,store:mt.store,text:mt.text}));X=JSON.stringify({format:F.format||"memory-eternal-vault",formatVersion:F.formatVersion||1,exportedAt:F.exportedAt||new Date().toISOString(),count:rt.length,cards:rt},null,2)}else q="memory-vault.md",Y="text/markdown;charset=utf-8",X=F.cards.map(rt=>rt.text.trim()).filter(Boolean).join(`
|
|
353
353
|
|
|
354
354
|
---
|
|
355
355
|
|