@modusensus/dsh-mneme 0.8.15 → 0.8.16

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.
Files changed (56) hide show
  1. package/README.md +8 -3
  2. package/bin/cli.mjs +4 -2
  3. package/docs/CONFIGURATION.md +14 -2
  4. package/docs/DISTILL.md +162 -0
  5. package/docs/STORAGE.md +11 -2
  6. package/lib/api-standalone.js +22 -39
  7. package/lib/api.js +171 -3
  8. package/lib/client.js +1065 -156
  9. package/lib/config.js +68 -1
  10. package/lib/dream/decisions.js +20 -0
  11. package/lib/dream.js +2 -0
  12. package/lib/hot-memory.js +62 -0
  13. package/lib/index.js +24 -7
  14. package/lib/inject.js +27 -63
  15. package/lib/maintenance.js +25 -5
  16. package/lib/recall-stats.js +22 -7
  17. package/lib/reclaim-http.js +150 -0
  18. package/lib/safe-slice.js +18 -0
  19. package/lib/service.js +27 -2
  20. package/lib/settings.js +13 -2
  21. package/lib/store.js +196 -7
  22. package/lib/summarize.js +208 -60
  23. package/package.json +1 -1
  24. package/src/api-standalone.js +22 -39
  25. package/src/api.js +171 -3
  26. package/src/config.js +68 -1
  27. package/src/dream/decisions.js +20 -0
  28. package/src/dream.js +2 -0
  29. package/src/hot-memory.js +62 -0
  30. package/src/index.js +24 -7
  31. package/src/inject.js +27 -63
  32. package/src/maintenance.js +25 -5
  33. package/src/recall-stats.js +22 -7
  34. package/src/reclaim-http.js +150 -0
  35. package/src/safe-slice.js +18 -0
  36. package/src/service.js +27 -2
  37. package/src/settings.js +13 -2
  38. package/src/store.js +196 -7
  39. package/src/summarize.js +208 -60
  40. package/test/api-routes-gaps.test.js +681 -0
  41. package/test/api.test.js +25 -2
  42. package/test/client.test.js +354 -2
  43. package/test/contrast.test.js +314 -0
  44. package/test/dream-merge-guardrail.test.js +219 -0
  45. package/test/hot-memory.test.js +67 -1
  46. package/test/inject-recall.test.js +39 -0
  47. package/test/inject.test.js +23 -0
  48. package/test/llm-audit.test.js +98 -0
  49. package/test/maintenance.test.js +112 -2
  50. package/test/pre-inject-gate.test.js +10 -0
  51. package/test/recall-stats.test.js +89 -4
  52. package/test/reclaim-http.test.js +209 -0
  53. package/test/service.test.js +7 -2
  54. package/test/settings.test.js +8 -0
  55. package/test/store.test.js +108 -0
  56. package/test/summarize.test.js +174 -0
package/lib/client.js CHANGED
@@ -577,6 +577,20 @@ window.__ModuleLoader__.load({
577
577
  "memory.explorer.imported": "已合并 {n} 条编辑",
578
578
  "memory.explorer.importFailed": "导入失败",
579
579
  "memory.explorer.importNoFile": "请先选择文件",
580
+ "memory.explorer.newMemory": "新增记忆…",
581
+ "memory.explorer.newTitle": "新增记忆",
582
+ "memory.explorer.newHint": "手动写一条记忆。同类型同标题会并入既有条目,而不是长出第二行。",
583
+ "memory.explorer.newType": "类型",
584
+ "memory.explorer.newTitlePlaceholder": "短标题,也是去重键",
585
+ "memory.explorer.newContentPlaceholder": "正文",
586
+ "memory.explorer.newTagsPlaceholder": "标签,逗号分隔(可留空)",
587
+ "memory.explorer.newConfirm": "新建",
588
+ "memory.explorer.newSaving": "保存中…",
589
+ "memory.explorer.newCreated": "已新建",
590
+ "memory.explorer.newMerged": "已并入同标题的既有条目",
591
+ "memory.explorer.newFailed": "新建失败",
592
+ "memory.explorer.newNoTitle": "请先填写标题",
593
+ "memory.explorer.newNoContent": "请先填写内容",
580
594
  "memory.explorer.exportFailed": "导出失败",
581
595
  "memory.explorer.conflictBadge": "冲突",
582
596
  "memory.explorer.archivedBadge": "已归档",
@@ -643,6 +657,14 @@ window.__ModuleLoader__.load({
643
657
  "memory.status.conflictQueue.similarity": "相似度",
644
658
  "memory.status.conflictQueue.diffTitle": "差异已高亮:红=删除、绿=新增",
645
659
  "memory.status.conflictQueue.diffFallback": "两段文本差异较大,请对照原文阅读",
660
+ "memory.status.conflictQueue.expand": "展开全文",
661
+ "memory.status.conflictQueue.collapse": "收起全文",
662
+ "memory.status.conflictQueue.copySide": "复制本侧",
663
+ "memory.status.conflictQueue.copiedSide": "本侧全文已复制",
664
+ "memory.status.conflictQueue.importance": "重要度 {n}",
665
+ "memory.status.conflictQueue.chars": "{n} 字符",
666
+ "memory.status.conflictQueue.updated": "更新于 {time}",
667
+ "memory.status.conflictQueue.created": "创建于 {time}",
646
668
  "memory.status.conflictQueue.emptyTitle": "暂无待确认冲突",
647
669
  "memory.status.conflictQueue.emptyBody": "当巩固发现两条互相矛盾或高度相似的记忆时,会先冻结在这里等你裁决,不会自动修改。裁决前双方都不参与注入。",
648
670
  "memory.status.conflictQueue.applyHintTitle": "确认后:保留方正文追加已否决注记,另一方归档",
@@ -724,8 +746,62 @@ window.__ModuleLoader__.load({
724
746
  "memory.status.mirror.foot": "记忆条目在磁盘上的 Markdown 副本同步状态",
725
747
  "memory.status.sec.overview": "库内一览",
726
748
  "memory.status.sec.engine": "后台运转",
727
- "memory.status.llm": "LLM 消耗",
728
- "memory.status.llmCalls": "近 7 天 · {n} 次调用",
749
+ "memory.status.sec.maintenance": "维护",
750
+ "memory.status.maint": "存储回收",
751
+ "memory.status.maint.freelist": "空闲页可立刻回收",
752
+ "memory.status.maint.plannedInputs": "待清理输入快照",
753
+ "memory.status.maint.plannedEmbeddings": "待清理归档向量",
754
+ "memory.status.maint.window": "保留窗口",
755
+ "memory.status.maint.windowUnit": "天",
756
+ "memory.status.maint.windowLabel": "保留窗口(天)",
757
+ "memory.status.maint.reestimate": "重新估算",
758
+ "memory.status.maint.reestimating": "估算中…",
759
+ "memory.status.maint.stale": "窗口已改动:先点「重新估算」再执行,否则执行的是上一次估算的数字。",
760
+ "memory.status.maint.vacuum": "同时 VACUUM",
761
+ "memory.status.maint.vacuumHint": "VACUUM 会短暂独占数据库,期间其它读写要等待;不做也不影响清理结果。",
762
+ "memory.status.maint.execute": "执行回收",
763
+ "memory.status.maint.confirm": "确认执行(不可逆)",
764
+ "memory.status.maint.running": "执行中…",
765
+ "memory.status.maint.cancel": "取消",
766
+ "memory.status.maint.confirmHint": "将清理 {inputs} 行输入快照与 {embeddings} 行归档向量,且不可逆。",
767
+ "memory.status.maint.badWindow": "保留窗口要填不小于 0 的整数",
768
+ "memory.status.maint.failed": "回收失败",
769
+ "memory.status.maint.done": "回收已完成",
770
+ "memory.status.maint.size": "体积(前 → 后)",
771
+ "memory.status.maint.reclaimed": "实际释放",
772
+ "memory.status.maint.clearedInputs": "已清理输入快照",
773
+ "memory.status.maint.clearedEmbeddings": "已清理归档向量",
774
+ "memory.status.maint.receipt": "审计回执",
775
+ "memory.status.maint.receiptNone": "未记录(审计未启用或回执写入失败)",
776
+ "memory.status.maint.duration": "耗时",
777
+ "memory.status.maint.vacuumRan": "VACUUM 已执行",
778
+ "memory.status.maint.vacuumOff": "未 VACUUM",
779
+ "memory.status.maint.vacuumFailed": "VACUUM 失败",
780
+ "memory.status.maint.vacuumFailedHint": "清理已完成且不可逆,但文件暂时不会变小;稍后重跑即可(清理是幂等的)。",
781
+ "memory.status.sec.token": "Token 去向 · 近 7 天",
782
+ "memory.status.token.in": "输入",
783
+ "memory.status.token.out": "输出",
784
+ "memory.status.token.total": "7 天 token 总量",
785
+ "memory.status.token.calls": "调用次数",
786
+ "memory.status.token.callsCap": "近 7 天 · 全部后台组件",
787
+ "memory.status.token.share": "巩固占比",
788
+ "memory.status.token.shareFoot": "dream_consolidate · {n} token",
789
+ "memory.status.token.conflicts": "待确认冲突",
790
+ "memory.status.token.conflictsFoot": "来自 /dream-status · 实时",
791
+ "memory.status.token.donut": "按组件 · 谁花了这些 token",
792
+ "memory.status.token.days": "按天 · 7 天趋势",
793
+ "memory.status.token.heat": "8 周热力 · 每天花了多少",
794
+ "memory.status.token.heatLess": "少",
795
+ "memory.status.token.heatMore": "多",
796
+ "memory.status.token.last7": "近 7 天",
797
+ "memory.status.token.meterOnly": "(只计量)",
798
+ "memory.status.token.detail": "详细状态卡(原「库内一览 / 后台运转」9 张,点击展开)",
799
+ "memory.status.token.op.dream_consolidate": "记忆巩固",
800
+ "memory.status.token.op.summarize_compress": "会话蒸馏",
801
+ "memory.status.token.op.entity_extract": "实体抽取",
802
+ "memory.status.token.op.dream_summarize": "总览摘要",
803
+ "memory.status.token.op.sleep_pattern": "睡眠模式",
804
+ "memory.status.token.op.write_admission": "写入准入",
729
805
  "memory.status.error": "加载失败"
730
806
  },
731
807
  en: {
@@ -1044,6 +1120,20 @@ window.__ModuleLoader__.load({
1044
1120
  "memory.explorer.imported": "Merged {n} edits",
1045
1121
  "memory.explorer.importFailed": "Import failed",
1046
1122
  "memory.explorer.importNoFile": "Choose a file first",
1123
+ "memory.explorer.newMemory": "New memory…",
1124
+ "memory.explorer.newTitle": "New memory",
1125
+ "memory.explorer.newHint": "Write one memory by hand. Same type + same title merges into the existing entry instead of minting a second row.",
1126
+ "memory.explorer.newType": "Type",
1127
+ "memory.explorer.newTitlePlaceholder": "Short title; also the dedupe key",
1128
+ "memory.explorer.newContentPlaceholder": "Body",
1129
+ "memory.explorer.newTagsPlaceholder": "Tags, comma separated (optional)",
1130
+ "memory.explorer.newConfirm": "Create",
1131
+ "memory.explorer.newSaving": "Saving…",
1132
+ "memory.explorer.newCreated": "Created",
1133
+ "memory.explorer.newMerged": "Merged into the existing entry with that title",
1134
+ "memory.explorer.newFailed": "Create failed",
1135
+ "memory.explorer.newNoTitle": "Title is required",
1136
+ "memory.explorer.newNoContent": "Body is required",
1047
1137
  "memory.explorer.exportFailed": "Export failed",
1048
1138
  "memory.explorer.conflictBadge": "Conflict",
1049
1139
  "memory.explorer.archivedBadge": "Archived",
@@ -1110,6 +1200,14 @@ window.__ModuleLoader__.load({
1110
1200
  "memory.status.conflictQueue.similarity": "similarity",
1111
1201
  "memory.status.conflictQueue.diffTitle": "Differences highlighted: red = removed, green = added",
1112
1202
  "memory.status.conflictQueue.diffFallback": "The two texts differ substantially — compare the originals",
1203
+ "memory.status.conflictQueue.expand": "Expand full text",
1204
+ "memory.status.conflictQueue.collapse": "Collapse",
1205
+ "memory.status.conflictQueue.copySide": "Copy this side",
1206
+ "memory.status.conflictQueue.copiedSide": "Full text copied",
1207
+ "memory.status.conflictQueue.importance": "importance {n}",
1208
+ "memory.status.conflictQueue.chars": "{n} chars",
1209
+ "memory.status.conflictQueue.updated": "updated {time}",
1210
+ "memory.status.conflictQueue.created": "created {time}",
1113
1211
  "memory.status.conflictQueue.emptyTitle": "No pending conflicts",
1114
1212
  "memory.status.conflictQueue.emptyBody": "When consolidation finds two contradictory or highly similar memories, it freezes them here for your review instead of changing anything automatically. Neither side participates in injection until resolved.",
1115
1213
  "memory.status.conflictQueue.applyHintTitle": "On confirm: a veto note is appended to the kept side and the other side is archived",
@@ -1191,8 +1289,62 @@ window.__ModuleLoader__.load({
1191
1289
  "memory.status.mirror.foot": "Sync state of the on-disk Markdown mirror of memory entries",
1192
1290
  "memory.status.sec.overview": "Library",
1193
1291
  "memory.status.sec.engine": "Background activity",
1194
- "memory.status.llm": "LLM Usage",
1195
- "memory.status.llmCalls": "Last 7 days · {n} calls",
1292
+ "memory.status.sec.maintenance": "Maintenance",
1293
+ "memory.status.maint": "Storage reclaim",
1294
+ "memory.status.maint.freelist": "Free pages reclaimable now",
1295
+ "memory.status.maint.plannedInputs": "Input snapshots to clear",
1296
+ "memory.status.maint.plannedEmbeddings": "Archived embeddings to clear",
1297
+ "memory.status.maint.window": "Retention window",
1298
+ "memory.status.maint.windowUnit": "days",
1299
+ "memory.status.maint.windowLabel": "Retention window (days)",
1300
+ "memory.status.maint.reestimate": "Re-estimate",
1301
+ "memory.status.maint.reestimating": "Estimating…",
1302
+ "memory.status.maint.stale": "Window changed - re-estimate before running, or the run would use the previous estimate's numbers.",
1303
+ "memory.status.maint.vacuum": "VACUUM as well",
1304
+ "memory.status.maint.vacuumHint": "VACUUM briefly takes an exclusive lock on the database; other reads and writes wait. Skipping it does not change what gets cleared.",
1305
+ "memory.status.maint.execute": "Run reclaim",
1306
+ "memory.status.maint.confirm": "Confirm (irreversible)",
1307
+ "memory.status.maint.running": "Running…",
1308
+ "memory.status.maint.cancel": "Cancel",
1309
+ "memory.status.maint.confirmHint": "This clears {inputs} input-snapshot rows and {embeddings} archived-embedding rows. It cannot be undone.",
1310
+ "memory.status.maint.badWindow": "The retention window must be an integer of at least 0",
1311
+ "memory.status.maint.failed": "Reclaim failed",
1312
+ "memory.status.maint.done": "Reclaim finished",
1313
+ "memory.status.maint.size": "Size (before → after)",
1314
+ "memory.status.maint.reclaimed": "Actually freed",
1315
+ "memory.status.maint.clearedInputs": "Input snapshots cleared",
1316
+ "memory.status.maint.clearedEmbeddings": "Archived embeddings cleared",
1317
+ "memory.status.maint.receipt": "Audit receipt",
1318
+ "memory.status.maint.receiptNone": "Not recorded (audit disabled or receipt write failed)",
1319
+ "memory.status.maint.duration": "Duration",
1320
+ "memory.status.maint.vacuumRan": "VACUUM ran",
1321
+ "memory.status.maint.vacuumOff": "Not VACUUMed",
1322
+ "memory.status.maint.vacuumFailed": "VACUUM failed",
1323
+ "memory.status.maint.vacuumFailedHint": "The reclaim already completed and cannot be undone, but the file will not shrink yet; run it again later (reclaim is idempotent).",
1324
+ "memory.status.sec.token": "Token flow · last 7 days",
1325
+ "memory.status.token.in": "input",
1326
+ "memory.status.token.out": "output",
1327
+ "memory.status.token.total": "7-day token total",
1328
+ "memory.status.token.calls": "Calls",
1329
+ "memory.status.token.callsCap": "Last 7 days · all background components",
1330
+ "memory.status.token.share": "Consolidation share",
1331
+ "memory.status.token.shareFoot": "dream_consolidate · {n} token",
1332
+ "memory.status.token.conflicts": "Pending conflicts",
1333
+ "memory.status.token.conflictsFoot": "From /dream-status · live",
1334
+ "memory.status.token.donut": "By component · who spent these tokens",
1335
+ "memory.status.token.days": "By day · 7-day trend",
1336
+ "memory.status.token.heat": "8-week heat · daily spend",
1337
+ "memory.status.token.heatLess": "less",
1338
+ "memory.status.token.heatMore": "more",
1339
+ "memory.status.token.last7": "last 7 days",
1340
+ "memory.status.token.meterOnly": "(metered only)",
1341
+ "memory.status.token.detail": "Detailed status cards (the original 9 under Library / Background activity, click to expand)",
1342
+ "memory.status.token.op.dream_consolidate": "Memory consolidation",
1343
+ "memory.status.token.op.summarize_compress": "Session distillation",
1344
+ "memory.status.token.op.entity_extract": "Entity extraction",
1345
+ "memory.status.token.op.dream_summarize": "Overview summary",
1346
+ "memory.status.token.op.sleep_pattern": "Sleep patterns",
1347
+ "memory.status.token.op.write_admission": "Write admission",
1196
1348
  "memory.status.error": "Failed to load"
1197
1349
  }
1198
1350
  };
@@ -1253,18 +1405,50 @@ window.__ModuleLoader__.load({
1253
1405
  // --- shared controls ---
1254
1406
  ".mneme-search{box-sizing:border-box;height:30px;padding:0 10px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base,transparent);color:var(--dsw-alias-label-primary);font-family:inherit;font-size:13px;outline:none;transition:border-color .12s,box-shadow .12s}",
1255
1407
  ".mneme-search:focus{border-color:var(--dsw-alias-state-business-primary);box-shadow:0 0 0 3px color-mix(in srgb,var(--dsw-alias-state-business-primary) 15%,transparent)}",
1256
- ".mneme-search::placeholder{color:var(--dsw-alias-label-tertiary)}",
1408
+ ".mneme-search::placeholder{color:var(--mneme-fg-dim)}",
1257
1409
  ".mneme-chip{height:26px;padding:0 10px;border-radius:8px;border:1px solid transparent;background:none;color:var(--dsw-alias-label-secondary);cursor:pointer;font-family:inherit;font-size:12px;line-height:16px;display:inline-flex;align-items:center}",
1258
1410
  ".mneme-chip:hover{background:var(--dsw-alias-interactive-bg-hover)}",
1259
- ".mneme-chip.mneme-active{color:var(--dsw-alias-state-business-primary);background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 10%,transparent)}",
1411
+ ".mneme-chip.mneme-active{color:var(--mneme-accent);background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 10%,transparent)}",
1260
1412
  ".mneme-select{box-sizing:border-box;height:30px;padding:0 8px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base,transparent);color:var(--dsw-alias-label-primary);font-family:inherit;font-size:13px;outline:none}",
1261
1413
  ".mneme-routeselect{width:240px;max-width:60%}",
1262
1414
  ".mneme-footbtn{border:none;background:none;color:var(--dsw-alias-label-secondary);cursor:pointer;font-family:inherit;font-size:12px;line-height:16px;padding:3px 8px;border-radius:6px}",
1263
1415
  ".mneme-footbtn:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}",
1264
- ".mneme-hint{color:var(--dsw-alias-label-tertiary);padding:24px 0;text-align:center;font-size:13px}",
1265
- ".mneme-entitychip{flex:none;height:26px;padding:0 10px;border-radius:8px;border:none;background:none;color:var(--dsw-alias-state-business-primary);cursor:pointer;font-family:inherit;font-size:12px;line-height:16px;display:inline-flex;align-items:center}",
1416
+ ".mneme-hint{color:var(--mneme-fg-dim);padding:24px 0;text-align:center;font-size:13px}",
1417
+ ".mneme-entitychip{flex:none;height:26px;padding:0 10px;border-radius:8px;border:none;background:none;color:var(--mneme-accent);cursor:pointer;font-family:inherit;font-size:12px;line-height:16px;display:inline-flex;align-items:center}",
1266
1418
  ".mneme-entitychip:hover{background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 10%,transparent)}",
1267
1419
  // --- main-area memory library page ---
1420
+ // 面板语义色层(#178):宿主 token 在浅色下给正文/次要文字只有 1.26–4.49
1421
+ // 的对比度,且面板还一直在引用 5 个根本不存在的宿主名(--dsw-alias-state-error
1422
+ // / -warning / -success / -warn / --dsw-alias-border),
1423
+ // 于是长年吃的是写在 fallback 里的硬编码色。这里按宿主真实 token 现算一组
1424
+ // 面板自有语义色:混色基准全是宿主 token,宿主换肤时跟着走,不引入新字面量。
1425
+ // 必须挂四个根——面板不只长在 .mneme-x 下:记忆库 sheet portal 到 body
1426
+ // (.mneme-overlay)、侧边栏入口在宿主插槽(.mneme-triggerwrap)、顶部入口
1427
+ // portal 到宿主侧边栏(.mneme-topentry)。
1428
+ ".mneme-x,.mneme-overlay,.mneme-triggerwrap,.mneme-topentry{" +
1429
+ "--mneme-fg-dim:color-mix(in srgb,var(--dsw-alias-label-primary) 60%,transparent);" +
1430
+ "--mneme-graphic-dim:color-mix(in srgb,var(--dsw-alias-label-primary) 50%,transparent);" +
1431
+ "--mneme-accent:color-mix(in srgb,var(--dsw-alias-state-business-primary) 85%,var(--dsw-alias-label-primary));" +
1432
+ "--mneme-danger:color-mix(in srgb,var(--dsw-alias-state-error-primary) 80%,var(--dsw-alias-label-primary));" +
1433
+ "--mneme-warn:color-mix(in srgb,var(--dsw-static-amber-600,#dd8629) 65%,var(--dsw-alias-label-primary));" +
1434
+ "--mneme-ok:color-mix(in srgb,var(--dsw-static-green-500,#22c55e) 65%,var(--dsw-alias-label-primary));" +
1435
+ // Token 去向三屏(2026-10 可观测性改造):图表六色全部单层 color-mix、
1436
+ // 基准只用宿主 token,test/contrast.test.js GRAPHICS 逐 selector 锁浅/深
1437
+ // ≥3:1。配比按 2026-10 审计数据校准(巩固 37% 是最大弧,占最深主色)。
1438
+ "--mneme-chart-1:color-mix(in srgb,var(--dsw-alias-state-business-primary) 70%,var(--dsw-alias-label-primary));" +
1439
+ "--mneme-chart-2:color-mix(in srgb,var(--dsw-static-green-500,#22c55e) 55%,var(--dsw-alias-label-primary));" +
1440
+ "--mneme-chart-3:color-mix(in srgb,var(--dsw-static-amber-600,#dd8629) 60%,var(--dsw-alias-label-primary));" +
1441
+ "--mneme-chart-4:color-mix(in srgb,var(--dsw-alias-state-error-primary) 65%,var(--dsw-alias-label-primary));" +
1442
+ "--mneme-chart-5:color-mix(in srgb,var(--dsw-static-green-500,#22c55e) 45%,var(--dsw-alias-state-business-primary));" +
1443
+ // chart-6 与 graphic-dim 同款但内联写:对比度解析器不递归 --mneme-* 变量。
1444
+ "--mneme-chart-6:color-mix(in srgb,var(--dsw-alias-label-primary) 50%,transparent);" +
1445
+ // 热力档位用 label 补余而非 transparent:transparent 混色在浅色主题下
1446
+ // 低档位对白底不足 3:1;label 补余两套主题各档都过线且单调。
1447
+ "--mneme-heat-1:color-mix(in srgb,var(--dsw-alias-state-business-primary) 85%,var(--dsw-alias-label-primary));" +
1448
+ "--mneme-heat-2:color-mix(in srgb,var(--dsw-alias-state-business-primary) 70%,var(--dsw-alias-label-primary));" +
1449
+ "--mneme-heat-3:color-mix(in srgb,var(--dsw-alias-state-business-primary) 55%,var(--dsw-alias-label-primary));" +
1450
+ "--mneme-heat-4:color-mix(in srgb,var(--dsw-alias-state-business-primary) 40%,var(--dsw-alias-label-primary))" +
1451
+ "}",
1268
1452
  ".mneme-x{flex:1;min-height:0;height:100%;width:100%;box-sizing:border-box;display:flex;flex-direction:column;background:var(--dsw-alias-bg-layer-1);--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2);container-type:inline-size}",
1269
1453
  // 窄容器(better-sidebar 工作台 ~500px)下中央 vtabs 会与右缘悬浮的
1270
1454
  // 卡片/时间线切换器重叠:容器查询收窄时隐藏切换器,视图模式沿用
@@ -1273,14 +1457,14 @@ window.__ModuleLoader__.load({
1273
1457
  ".mneme-xbar{position:relative;flex:none;display:flex;align-items:center;justify-content:center;gap:6px;border-bottom:1px solid var(--dsw-alias-border-l2);padding:0 16px;min-height:52px}",
1274
1458
  ".mneme-filterbar{flex:none;display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:8px 16px;border-bottom:1px solid var(--dsw-alias-border-l2)}",
1275
1459
  ".mneme-vtabs{display:flex;align-items:stretch;height:46px}",
1276
- ".mneme-vtab{position:relative;border:0;background:none;cursor:pointer;padding:0 16px;color:var(--dsw-alias-label-tertiary);font-family:inherit;font-size:14px;font-weight:500;line-height:20px;display:inline-flex;align-items:center;gap:7px}",
1460
+ ".mneme-vtab{position:relative;border:0;background:none;cursor:pointer;padding:0 16px;color:var(--mneme-fg-dim);font-family:inherit;font-size:14px;font-weight:500;line-height:20px;display:inline-flex;align-items:center;gap:7px}",
1277
1461
  ".mneme-vtab:hover{color:var(--dsw-alias-label-primary)}",
1278
- ".mneme-vtab.mneme-active{color:var(--dsw-alias-state-business-primary)}",
1462
+ ".mneme-vtab.mneme-active{color:var(--mneme-accent)}",
1279
1463
  ".mneme-vtab.mneme-active::after{content:\"\";position:absolute;left:10px;right:10px;bottom:-1px;height:2.5px;border-radius:2px;background:var(--dsw-alias-state-business-primary)}",
1280
1464
  // transform 会自建层叠上下文:层级必须给在容器上——3 高于吸顶月份头(2)、
1281
1465
  // 低于详情抽屉(6),下拉菜单/导入对话框随容器整体上浮。
1282
1466
  ".mneme-xtools{position:absolute;right:14px;top:50%;transform:translateY(-50%);display:flex;align-items:center;gap:8px;padding:0;z-index:3}",
1283
- ".mneme-xcount{flex:none;font-size:12px;line-height:16px;color:var(--dsw-alias-label-tertiary);white-space:nowrap}",
1467
+ ".mneme-xcount{flex:none;font-size:12px;line-height:16px;color:var(--mneme-fg-dim);white-space:nowrap}",
1284
1468
  // --- three-column browse layout: hairline separators, no outer box ---
1285
1469
  ".mneme-xmain{flex:1;min-height:0;display:flex;flex-direction:row;overflow:hidden}",
1286
1470
  ".mneme-xside{flex:none;width:236px;min-width:0;min-height:0;overflow-y:auto;padding:16px 14px;border-right:1px solid var(--dsw-alias-border-l2);box-sizing:border-box;display:flex;flex-direction:column;gap:8px}",
@@ -1289,23 +1473,23 @@ window.__ModuleLoader__.load({
1289
1473
  ".mneme-xrow{display:flex;align-items:center;gap:8px;flex-wrap:wrap}",
1290
1474
  ".mneme-xsearch{width:100%}",
1291
1475
  ".mneme-xselect{flex:1;min-width:0}",
1292
- ".mneme-xcolhead{flex:none;font-size:12px;font-weight:500;margin:0;color:var(--dsw-alias-label-tertiary);padding:2px 8px 8px}",
1476
+ ".mneme-xcolhead{flex:none;font-size:12px;font-weight:500;margin:0;color:var(--mneme-fg-dim);padding:2px 8px 8px}",
1293
1477
  ".mneme-xtype{display:flex;justify-content:flex-start;align-items:center;gap:8px;width:100%;padding:5px 8px;border:none;border-radius:8px;background:none;color:var(--dsw-alias-label-secondary);cursor:pointer;font-family:inherit;font-size:13px;line-height:18px;text-align:left}",
1294
1478
  ".mneme-xtype:hover{background:var(--dsw-alias-interactive-bg-hover)}",
1295
1479
  ".mneme-xtype.mneme-active{background:var(--dsw-alias-interactive-bg-active);color:var(--dsw-alias-label-primary);font-weight:500}",
1296
- ".mneme-xcount2{flex:none;margin-left:auto;font-size:12px;color:var(--dsw-alias-label-tertiary)}",
1480
+ ".mneme-xcount2{flex:none;margin-left:auto;font-size:12px;color:var(--mneme-fg-dim)}",
1297
1481
  ".mneme-xmonth{display:flex;align-items:center;gap:4px;width:100%;padding:6px 8px 4px;border:none;border-radius:8px;background:none;color:var(--dsw-alias-label-primary);cursor:pointer;font-family:inherit;font-size:13px;font-weight:500;line-height:18px;text-align:left}",
1298
1482
  ".mneme-xmonth:first-child{margin-top:0}",
1299
1483
  ".mneme-xmonth:hover{background:var(--dsw-alias-interactive-bg-hover)}",
1300
- ".mneme-xcaret{flex:none;font-size:10px;color:var(--dsw-alias-label-tertiary);width:10px}",
1301
- ".mneme-xday{display:flex;align-items:center;gap:4px;margin:6px 0 2px 22px;padding:2px 6px;border:none;border-radius:6px;background:none;font-family:inherit;font-size:12px;line-height:16px;color:var(--dsw-alias-label-tertiary);cursor:pointer;text-align:left}",
1484
+ ".mneme-xcaret{flex:none;font-size:10px;color:var(--mneme-fg-dim);width:10px}",
1485
+ ".mneme-xday{display:flex;align-items:center;gap:4px;margin:6px 0 2px 22px;padding:2px 6px;border:none;border-radius:6px;background:none;font-family:inherit;font-size:12px;line-height:16px;color:var(--mneme-fg-dim);cursor:pointer;text-align:left}",
1302
1486
  ".mneme-xday:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}",
1303
1487
  ".mneme-xitem{display:flex;gap:8px;align-items:baseline;width:calc(100% - 22px);margin-left:22px;padding:4px 8px;border:none;border-radius:8px;background:none;color:var(--dsw-alias-label-secondary);cursor:pointer;font-family:inherit;font-size:13px;line-height:18px;text-align:left}",
1304
1488
  ".mneme-xitem:hover{background:var(--dsw-alias-interactive-bg-hover)}",
1305
1489
  ".mneme-xitem.mneme-active{background:var(--dsw-alias-interactive-bg-active);color:var(--dsw-alias-label-primary)}",
1306
- ".mneme-xtime{flex:none;font-size:12px;line-height:16px;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums}",
1490
+ ".mneme-xtime{flex:none;font-size:12px;line-height:16px;color:var(--mneme-fg-dim);font-variant-numeric:tabular-nums}",
1307
1491
  ".mneme-xname{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
1308
- ".mneme-xempty{padding:18px 16px;text-align:center;color:var(--dsw-alias-label-tertiary);font-size:13px}",
1492
+ ".mneme-xempty{padding:18px 16px;text-align:center;color:var(--mneme-fg-dim);font-size:13px}",
1309
1493
  // --- memory-type color dots (mirrored across filter / timeline / detail) ---
1310
1494
  ".mneme-xdot{flex:none;display:inline-block;width:8px;height:8px;border-radius:50%;background:currentColor}",
1311
1495
  ".mneme-xdot--all{background:transparent;border:1.5px solid currentColor;opacity:.55;box-sizing:border-box}",
@@ -1317,7 +1501,7 @@ window.__ModuleLoader__.load({
1317
1501
  ".mneme-xtree{flex:1;min-height:0;overflow-y:auto;padding:8px 10px 28px}",
1318
1502
  ".mneme-xdinner{max-width:720px}",
1319
1503
  ".mneme-xdtitle{font-size:16px;font-weight:600;line-height:24px;color:var(--dsw-alias-label-primary);margin-bottom:10px;word-break:break-word}",
1320
- ".mneme-xdmeta{display:flex;flex-wrap:wrap;gap:4px 14px;margin-bottom:6px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}",
1504
+ ".mneme-xdmeta{display:flex;flex-wrap:wrap;gap:4px 14px;margin-bottom:6px;font-size:12px;line-height:18px;color:var(--mneme-fg-dim)}",
1321
1505
  ".mneme-xdsrc{display:inline-block;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:bottom}",
1322
1506
  ".mneme-xdcontent{margin-top:14px;font-size:14px;line-height:1.75;color:var(--dsw-alias-label-primary);white-space:pre-wrap;word-break:break-word}",
1323
1507
  ".mneme-xdactions{display:flex;gap:8px;margin-top:18px}",
@@ -1330,42 +1514,44 @@ window.__ModuleLoader__.load({
1330
1514
  ".mneme-gnode:hover circle{stroke-width:4}",
1331
1515
  ".mneme-gnode.mneme-groot circle{stroke:var(--dsw-alias-state-business-primary);stroke-width:3}",
1332
1516
  ".mneme-glabel{fill:var(--dsw-alias-label-secondary);font-size:11px;text-anchor:middle;pointer-events:none;user-select:none}",
1333
- ".mneme-gedge{stroke:var(--dsw-alias-label-dimmed);stroke-width:1.5;cursor:pointer}",
1517
+ ".mneme-gedge{stroke:var(--mneme-graphic-dim);stroke-width:1.5;cursor:pointer}",
1334
1518
  ".mneme-gedge:hover{stroke:var(--dsw-alias-state-business-primary)}",
1335
- ".mneme-gedge-dashed{stroke-dasharray:5 4;opacity:.7}",
1336
- ".mneme-graphhint{flex:none;color:var(--dsw-alias-label-tertiary);font-size:13px;text-align:center;padding:6px 0 2px}",
1519
+ // 虚线态靠 dash 图案区分,不再叠 opacity:50% 的描边再乘 .7 后浅色画布上
1520
+ // 只剩 2.28,低于图形要求的 3:1(图案已经承担了「没有 memory_id」这个语义)。
1521
+ ".mneme-gedge-dashed{stroke-dasharray:5 4}",
1522
+ ".mneme-graphhint{flex:none;color:var(--mneme-fg-dim);font-size:13px;text-align:center;padding:6px 0 2px}",
1337
1523
  ".mneme-graphside{flex:none;max-height:220px;overflow-y:auto;border-top:1px solid var(--dsw-alias-border-l1);margin-top:10px;padding-top:10px}",
1338
1524
  ".mneme-gs-title{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary);margin-bottom:4px;display:flex;justify-content:space-between;align-items:baseline;gap:8px}",
1339
- ".mneme-gs-meta{font-size:13px;color:var(--dsw-alias-label-tertiary);margin-bottom:6px}",
1525
+ ".mneme-gs-meta{font-size:13px;color:var(--mneme-fg-dim);margin-bottom:6px}",
1340
1526
  ".mneme-gs-attr{display:flex;gap:6px;font-size:13px;padding:2px 0}",
1341
- ".mneme-gs-attrkey{flex:none;color:var(--dsw-alias-label-tertiary);font-size:13px}",
1527
+ ".mneme-gs-attrkey{flex:none;color:var(--mneme-fg-dim);font-size:13px}",
1342
1528
  ".mneme-gs-attrval{color:var(--dsw-alias-label-secondary);word-break:break-word;font-size:13px}",
1343
1529
  ".mneme-gs-link{display:block;width:100%;text-align:left;border:none;background:none;color:var(--dsw-alias-label-secondary);cursor:pointer;font-family:inherit;font-size:13px;padding:3px 6px;border-radius:6px;word-break:break-word}",
1344
1530
  ".mneme-gs-link:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}",
1345
1531
  // --- entity explorer: rail + layered detail (entities → attrs → relations → graph) ---
1346
1532
  ".mneme-ent{flex:1;min-height:0;display:flex;overflow:hidden}",
1347
1533
  ".mneme-entrail{flex:none;width:224px;min-height:0;overflow-y:auto;padding:12px;border-right:1px solid var(--dsw-alias-border-l2);box-sizing:border-box;display:flex;flex-direction:column;gap:4px}",
1348
- ".mneme-enttype{display:flex;align-items:center;gap:6px;width:100%;padding:8px 8px 2px;border:none;background:none;color:var(--dsw-alias-label-tertiary);cursor:pointer;font-family:inherit;font-size:11px;font-weight:600;line-height:16px;text-align:left;letter-spacing:.02em}",
1534
+ ".mneme-enttype{display:flex;align-items:center;gap:6px;width:100%;padding:8px 8px 2px;border:none;background:none;color:var(--mneme-fg-dim);cursor:pointer;font-family:inherit;font-size:11px;font-weight:600;line-height:16px;text-align:left;letter-spacing:.02em}",
1349
1535
  ".mneme-entitem{display:flex;align-items:center;gap:8px;width:100%;padding:5px 8px;border:none;border-radius:8px;background:none;color:var(--dsw-alias-label-secondary);cursor:pointer;font-family:inherit;font-size:13px;line-height:18px;text-align:left}",
1350
1536
  ".mneme-entitem:hover{background:var(--dsw-alias-interactive-bg-hover)}",
1351
1537
  ".mneme-entitem.mneme-active{background:var(--dsw-alias-interactive-bg-active);color:var(--dsw-alias-label-primary)}",
1352
1538
  ".mneme-entname{min-width:0;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
1353
- ".mneme-entmentions{flex:none;font-size:11px;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums}",
1539
+ ".mneme-entmentions{flex:none;font-size:11px;color:var(--mneme-fg-dim);font-variant-numeric:tabular-nums}",
1354
1540
  ".mneme-entdetail{flex:1;min-width:0;min-height:0;overflow-y:auto;padding:20px 24px 32px;box-sizing:border-box}",
1355
1541
  ".mneme-entinner{max-width:720px}",
1356
1542
  ".mneme-enttitle{display:flex;align-items:center;gap:10px;font-size:17px;font-weight:600;line-height:24px;color:var(--dsw-alias-label-primary);word-break:break-word}",
1357
1543
  ".mneme-enttitle .mneme-xdot{width:10px;height:10px}",
1358
- ".mneme-entmeta{display:flex;flex-wrap:wrap;gap:4px 14px;margin-top:6px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}",
1544
+ ".mneme-entmeta{display:flex;flex-wrap:wrap;gap:4px 14px;margin-top:6px;font-size:12px;line-height:18px;color:var(--mneme-fg-dim)}",
1359
1545
  ".mneme-layer{margin-top:24px}",
1360
1546
  ".mneme-layerhead{display:flex;align-items:baseline;gap:8px;font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);margin-bottom:10px}",
1361
- ".mneme-layercount{font-size:12px;font-weight:400;color:var(--dsw-alias-label-tertiary)}",
1547
+ ".mneme-layercount{font-size:12px;font-weight:400;color:var(--mneme-fg-dim)}",
1362
1548
  ".mneme-attrgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:8px}",
1363
1549
  ".mneme-attrcard{min-width:0;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:8px 12px}",
1364
- ".mneme-attrkey{font-size:11px;color:var(--dsw-alias-label-tertiary);margin-bottom:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
1550
+ ".mneme-attrkey{font-size:11px;color:var(--mneme-fg-dim);margin-bottom:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
1365
1551
  ".mneme-attrval{font-size:13px;line-height:19px;color:var(--dsw-alias-label-primary);word-break:break-word}",
1366
1552
  ".mneme-relrow{display:flex;align-items:center;gap:10px;width:100%;padding:7px 10px;border:none;border-radius:10px;background:none;color:var(--dsw-alias-label-secondary);cursor:pointer;font-family:inherit;font-size:13px;line-height:20px;text-align:left}",
1367
1553
  ".mneme-relrow:hover{background:var(--dsw-alias-interactive-bg-hover)}",
1368
- ".mneme-reltype{flex:none;font-size:11px;line-height:16px;color:var(--dsw-alias-state-business-primary);border:1px solid color-mix(in srgb,var(--dsw-alias-state-business-primary) 35%,transparent);border-radius:6px;padding:0 6px}",
1554
+ ".mneme-reltype{flex:none;font-size:11px;line-height:16px;color:var(--mneme-accent);border:1px solid color-mix(in srgb,var(--dsw-alias-state-business-primary) 35%,transparent);border-radius:6px;padding:0 6px}",
1369
1555
  ".mneme-relname{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
1370
1556
  // --- settings: quiet stacked sections, one concern per section ---
1371
1557
  ".mneme-set{flex:1;min-height:0;overflow-y:auto;padding:8px 24px 48px;box-sizing:border-box}",
@@ -1373,25 +1559,25 @@ window.__ModuleLoader__.load({
1373
1559
  // 设置页各节一律用 boxed 卡(.mneme-set-card)+ 分组标题(.mneme-set-grouphead):
1374
1560
  // 旧的「无框 set-sec + 有框 set-card」混排是版面杂乱的一半来源
1375
1561
  ".mneme-set-title{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary);margin-bottom:4px}",
1376
- ".mneme-set-desc{font-size:13px;line-height:19px;color:var(--dsw-alias-label-tertiary);margin-bottom:14px}",
1562
+ ".mneme-set-desc{font-size:13px;line-height:19px;color:var(--mneme-fg-dim);margin-bottom:14px}",
1377
1563
  ".mneme-set-input{box-sizing:border-box;width:100%;height:34px;padding:0 12px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base,transparent);color:var(--dsw-alias-label-primary);font-family:inherit;font-size:13px;outline:none;margin-bottom:10px;transition:border-color .12s,box-shadow .12s}",
1378
1564
  ".mneme-set-input:focus{border-color:var(--dsw-alias-state-business-primary);box-shadow:0 0 0 3px color-mix(in srgb,var(--dsw-alias-state-business-primary) 15%,transparent)}",
1379
- ".mneme-set-input::placeholder{color:var(--dsw-alias-label-tertiary)}",
1565
+ ".mneme-set-input::placeholder{color:var(--mneme-fg-dim)}",
1380
1566
  ".mneme-set-input--area{height:auto;min-height:76px;padding:8px 12px;resize:vertical;line-height:1.6}",
1381
1567
  ".mneme-btn{flex:none;height:30px;padding:0 12px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:none;color:var(--dsw-alias-label-primary);cursor:pointer;font-family:inherit;font-size:12px;line-height:16px;display:inline-flex;align-items:center;gap:6px}",
1382
1568
  ".mneme-btn:hover{background:var(--dsw-alias-interactive-bg-hover)}",
1383
1569
  ".mneme-btn:disabled{opacity:.5;cursor:default}",
1384
- ".mneme-saved{font-size:12px;color:var(--dsw-alias-state-success,#2a7)}",
1570
+ ".mneme-saved{font-size:12px;color:var(--mneme-ok)}",
1385
1571
  // rule rows: numbered chips + hover-revealed delete
1386
1572
  ".mneme-set-row{display:flex;align-items:flex-start;gap:10px;padding:10px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;margin-bottom:8px;background:var(--dsw-alias-bg-base,transparent)}",
1387
- ".mneme-set-idx{flex:none;width:20px;height:20px;border-radius:50%;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:18px;display:inline-flex;align-items:center;justify-content:center;font-variant-numeric:tabular-nums}",
1573
+ ".mneme-set-idx{flex:none;width:20px;height:20px;border-radius:50%;border:1px solid var(--dsw-alias-border-l2);color:var(--mneme-fg-dim);font-size:11px;line-height:18px;display:inline-flex;align-items:center;justify-content:center;font-variant-numeric:tabular-nums}",
1388
1574
  ".mneme-set-ruletext{flex:1;min-width:0;font-size:13px;line-height:20px;color:var(--dsw-alias-label-primary);word-break:break-word}",
1389
- ".mneme-set-del{flex:none;opacity:0;transition:opacity .12s;border:none;background:none;color:var(--dsw-alias-label-tertiary);cursor:pointer;font-size:14px;line-height:20px;padding:0 4px;border-radius:6px}",
1575
+ ".mneme-set-del{flex:none;opacity:0;transition:opacity .12s;border:none;background:none;color:var(--mneme-fg-dim);cursor:pointer;font-size:14px;line-height:20px;padding:0 4px;border-radius:6px}",
1390
1576
  ".mneme-set-row:hover .mneme-set-del,.mneme-set-row:focus-within .mneme-set-del{opacity:1}",
1391
- ".mneme-set-del:hover{color:var(--dsw-alias-state-error,#c33);background:var(--dsw-alias-interactive-bg-hover)}",
1577
+ ".mneme-set-del:hover{color:var(--mneme-danger);background:var(--dsw-alias-interactive-bg-hover)}",
1392
1578
  ".mneme-set-cmd{flex:1;min-width:0}",
1393
- ".mneme-set-cmdname{font-size:13px;font-weight:600;color:var(--dsw-alias-state-business-primary)}",
1394
- ".mneme-set-cmddesc{font-size:12px;line-height:17px;color:var(--dsw-alias-label-tertiary);margin-top:1px;word-break:break-word}",
1579
+ ".mneme-set-cmdname{font-size:13px;font-weight:600;color:var(--mneme-accent)}",
1580
+ ".mneme-set-cmddesc{font-size:12px;line-height:17px;color:var(--mneme-fg-dim);margin-top:1px;word-break:break-word}",
1395
1581
  // --- settings sub-view ---
1396
1582
  ".mneme-set{flex:1;min-height:0;overflow-y:auto;padding:8px 24px 48px;box-sizing:border-box}",
1397
1583
  // --- 记忆库页面:居中 sheet,非全屏 ---
@@ -1411,9 +1597,9 @@ window.__ModuleLoader__.load({
1411
1597
  ".mneme-stars{display:inline-flex;align-items:center;gap:2px}",
1412
1598
  // 热度徽章三档:热(橙)/温(次级文字)/冷(弱化文字),冷档藏百分比只留图标。
1413
1599
  ".mneme-heat{display:inline-flex;align-items:center;gap:2px;font-size:11px;line-height:14px;font-variant-numeric:tabular-nums}",
1414
- ".mneme-heat--hot{color:var(--dsw-alias-state-warning,#d97706)}",
1600
+ ".mneme-heat--hot{color:var(--mneme-warn)}",
1415
1601
  ".mneme-heat--warm{color:var(--dsw-alias-label-secondary)}",
1416
- ".mneme-heat--cold{color:var(--dsw-alias-label-tertiary)}",
1602
+ ".mneme-heat--cold{color:var(--mneme-fg-dim)}",
1417
1603
  ".mneme-heat--cold .mneme-heatpct{display:none}",
1418
1604
  // 侧边栏入口两种形态的样式(挂载与搬移逻辑见 SidebarTopEntry 的头注释)。
1419
1605
  // 展开态兜底:逐条镜像宿主 panelRow(36px 行高 / 透明底 / 2px 外边距 /
@@ -1436,12 +1622,12 @@ window.__ModuleLoader__.load({
1436
1622
  // 菜单让位只在「收起 + 入口确实挂在标题栏里」时生效;展开态完全不碰宿主变量。
1437
1623
  "html[data-windows-titlebar]:has([data-sidebar-collapsed=true]):has([data-plugin-entry=\"@modusensus/dsh-mneme\"]){--dsh-windows-menu-start:120px}",
1438
1624
  // --- 功能开关:一行一开关,Claude 式安静排版 ---
1439
- ".mneme-featgroup{flex:none;font-size:12px;font-weight:600;letter-spacing:.02em;color:var(--dsw-alias-label-tertiary);margin:16px 0 2px}",
1625
+ ".mneme-featgroup{flex:none;font-size:12px;font-weight:600;letter-spacing:.02em;color:var(--mneme-fg-dim);margin:16px 0 2px}",
1440
1626
  ".mneme-featrow{display:flex;align-items:center;gap:14px;padding:11px 2px;border-bottom:1px solid var(--dsw-alias-border-l1)}",
1441
1627
  ".mneme-featrow:last-child{border-bottom:none}",
1442
1628
  ".mneme-featmain{flex:1;min-width:0}",
1443
1629
  ".mneme-featname{font-size:13.5px;font-weight:500;line-height:20px;color:var(--dsw-alias-label-primary)}",
1444
- ".mneme-feathint{font-size:12.5px;line-height:18px;color:var(--dsw-alias-label-tertiary);margin-top:2px}",
1630
+ ".mneme-feathint{font-size:12.5px;line-height:18px;color:var(--mneme-fg-dim);margin-top:2px}",
1445
1631
  ".mneme-switch{position:relative;flex:none;width:36px;height:22px;border-radius:11px;border:none;cursor:pointer;background:var(--dsw-alias-interactive-bg-active);transition:background .15s}",
1446
1632
  ".mneme-switch::after{content:\"\";position:absolute;top:2px;left:2px;width:18px;height:18px;border-radius:50%;background:var(--dsw-alias-bg-layer-1,#fff);box-shadow:0 1px 3px color-mix(in srgb,var(--dsw-alias-label-primary) 25%,transparent);transition:transform .15s}",
1447
1633
  ".mneme-switch.mneme-on{background:var(--dsw-alias-state-business-primary)}",
@@ -1457,14 +1643,14 @@ window.__ModuleLoader__.load({
1457
1643
  ".mneme-strinput:focus{border-color:var(--dsw-alias-state-business-primary);box-shadow:0 0 0 3px color-mix(in srgb,var(--dsw-alias-state-business-primary) 15%,transparent)}",
1458
1644
  ".mneme-featsub{margin:2px 0 8px;padding:10px 12px;border:1px solid var(--dsw-alias-border-l1);border-radius:10px;display:flex;flex-direction:column;gap:6px}",
1459
1645
  ".mneme-featsub .mneme-featnum{padding:6px 0;border-bottom:none}",
1460
- ".mneme-featsubhint{font-size:12px;line-height:17px;color:var(--dsw-alias-label-tertiary)}",
1646
+ ".mneme-featsubhint{font-size:12px;line-height:17px;color:var(--mneme-fg-dim)}",
1461
1647
  // 运行时那一块的说明文字:比常规 hint 再小一档,它是背景信息而不是要读的正文。
1462
- ".mneme-runtimehint{font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary)}",
1648
+ ".mneme-runtimehint{font-size:11px;line-height:16px;color:var(--mneme-fg-dim)}",
1463
1649
  // --- 交互式记忆库:工具栏 / 卡片网格 / 详情抽屉 / 更多菜单 ---
1464
1650
  // sheet 边距放宽:与窗口边缘保持呼吸距离,居中不顶满。
1465
1651
  // 视图切换(卡片/时间线)分段控件挂在子页栏右侧。
1466
1652
  ".mneme-seg{display:inline-flex;align-items:center;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;overflow:hidden}",
1467
- ".mneme-seg button{border:none;background:none;color:var(--dsw-alias-label-tertiary);cursor:pointer;font-family:inherit;font-size:12px;line-height:16px;padding:5px 10px;display:inline-flex;align-items:center;gap:5px}",
1653
+ ".mneme-seg button{border:none;background:none;color:var(--mneme-fg-dim);cursor:pointer;font-family:inherit;font-size:12px;line-height:16px;padding:5px 10px;display:inline-flex;align-items:center;gap:5px}",
1468
1654
  ".mneme-seg button:hover{color:var(--dsw-alias-label-primary)}",
1469
1655
  ".mneme-seg button.mneme-active{background:var(--dsw-alias-interactive-bg-active);color:var(--dsw-alias-label-primary);font-weight:500}",
1470
1656
  // 卡片网格:响应式 auto-fill,卡片悬停轻浮起
@@ -1472,53 +1658,63 @@ window.__ModuleLoader__.load({
1472
1658
  ".mneme-card{display:flex;flex-direction:column;gap:7px;min-width:0;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;padding:12px 13px;background:var(--dsw-alias-bg-layer-1);cursor:pointer;font-family:inherit;text-align:left;transition:border-color .12s,box-shadow .12s,transform .12s}",
1473
1659
  ".mneme-card:hover{border-color:color-mix(in srgb,var(--dsw-alias-state-business-primary) 45%,var(--dsw-alias-border-l2));box-shadow:0 6px 20px color-mix(in srgb,var(--dsw-alias-label-primary) 10%,transparent);transform:translateY(-1px)}",
1474
1660
  ".mneme-card.mneme-active{border-color:var(--dsw-alias-state-business-primary)}",
1475
- ".mneme-cardhead{display:flex;align-items:center;gap:6px;font-size:11.5px;line-height:16px;color:var(--dsw-alias-label-tertiary)}",
1661
+ ".mneme-cardhead{display:flex;align-items:center;gap:6px;font-size:11.5px;line-height:16px;color:var(--mneme-fg-dim)}",
1476
1662
  ".mneme-cardtitle{font-size:13.5px;font-weight:600;line-height:19px;color:var(--dsw-alias-label-primary);word-break:break-word;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}",
1477
1663
  ".mneme-cardexcerpt{font-size:12.5px;line-height:18px;color:var(--dsw-alias-label-secondary);word-break:break-word;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}",
1478
- ".mneme-cardfoot{display:flex;align-items:center;gap:8px;margin-top:auto;font-size:11.5px;line-height:16px;color:var(--dsw-alias-label-tertiary)}",
1664
+ ".mneme-cardfoot{display:flex;align-items:center;gap:8px;margin-top:auto;font-size:11.5px;line-height:16px;color:var(--mneme-fg-dim)}",
1479
1665
  ".mneme-cardsrc{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
1480
1666
  // 徽章:冲突待确认 / 已归档
1481
1667
  ".mneme-badge{flex:none;display:inline-flex;align-items:center;gap:4px;height:18px;padding:0 7px;border-radius:6px;font-size:11px;line-height:14px}",
1482
- ".mneme-badge--conflict{color:var(--dsw-alias-state-error,#c33);background:color-mix(in srgb,var(--dsw-alias-state-error,#c33) 10%,transparent);border:1px solid color-mix(in srgb,var(--dsw-alias-state-error,#c33) 30%,transparent)}",
1483
- ".mneme-badge--archived{color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-interactive-bg-hover)}",
1484
- ".mneme-badge--scope{color:var(--dsw-alias-state-business-primary);background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 10%,transparent);border:1px solid color-mix(in srgb,var(--dsw-alias-state-business-primary) 30%,transparent)}",
1668
+ ".mneme-badge--conflict{color:var(--mneme-danger);background:color-mix(in srgb,var(--mneme-danger) 10%,transparent);border:1px solid color-mix(in srgb,var(--mneme-danger) 30%,transparent)}",
1669
+ ".mneme-badge--archived{color:var(--mneme-fg-dim);background:var(--dsw-alias-interactive-bg-hover)}",
1670
+ ".mneme-badge--scope{color:var(--mneme-accent);background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 10%,transparent);border:1px solid color-mix(in srgb,var(--dsw-alias-state-business-primary) 30%,transparent)}",
1485
1671
  // 冲突集中处理队列(v0.8.0):状态页的待确认冲突列表
1486
1672
  ".mneme-conflictq{display:flex;flex-direction:column;gap:10px}",
1487
1673
  ".mneme-conflictq-head{display:flex;align-items:center;gap:8px}",
1488
- ".mneme-conflict-item{border:1px solid var(--dsw-alias-border,var(--dsw-alias-interactive-bg-hover));border-radius:10px;padding:10px;display:flex;flex-direction:column;gap:8px}",
1674
+ ".mneme-conflict-item{border:1px solid var(--dsw-alias-border-l1,var(--dsw-alias-interactive-bg-hover));border-radius:10px;padding:10px;display:flex;flex-direction:column;gap:8px}",
1489
1675
  ".mneme-conflict-reason{font-size:12px;line-height:17px;color:var(--dsw-alias-label-secondary)}",
1490
1676
  ".mneme-conflict-pair{display:grid;grid-template-columns:1fr 1fr;gap:8px}",
1491
- ".mneme-conflict-side{border:1px solid var(--dsw-alias-border,var(--dsw-alias-interactive-bg-hover));border-radius:8px;padding:8px;display:flex;flex-direction:column;gap:4px;min-width:0}",
1492
- ".mneme-conflict-sidelabel{font-size:11px;color:var(--dsw-alias-label-tertiary)}",
1677
+ ".mneme-conflict-side{border:1px solid var(--dsw-alias-border-l1,var(--dsw-alias-interactive-bg-hover));border-radius:8px;padding:8px;display:flex;flex-direction:column;gap:4px;min-width:0}",
1678
+ ".mneme-conflict-sidelabel{font-size:11px;color:var(--mneme-fg-dim)}",
1493
1679
  ".mneme-conflict-sidetitle{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
1494
1680
  ".mneme-conflict-snippet{font-size:12px;line-height:17px;color:var(--dsw-alias-label-secondary);display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}",
1495
- ".mneme-conflict-missing{font-size:12px;color:var(--dsw-alias-label-tertiary)}",
1681
+ ".mneme-conflict-missing{font-size:12px;color:var(--mneme-fg-dim)}",
1496
1682
  ".mneme-conflict-actions{display:flex;align-items:center;gap:6px;flex-wrap:wrap}",
1497
- ".mneme-conflict-hint{font-size:11px;color:var(--dsw-alias-label-tertiary);margin-right:auto}",
1683
+ ".mneme-conflict-hint{font-size:11px;color:var(--mneme-fg-dim);margin-right:auto}",
1498
1684
  // #177 冲突队列视觉批次:A/B 侧色、相似度条、词级 diff、主色按钮、空态
1499
1685
  ".mneme-conflict-side--a{border-color:color-mix(in srgb,var(--dsw-alias-state-business-primary) 35%,transparent);background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 4%,transparent)}",
1500
- ".mneme-conflict-side--a .mneme-conflict-sidelabel{color:var(--dsw-alias-state-business-primary);font-weight:600}",
1501
- ".mneme-conflict-side--b{border-color:color-mix(in srgb,var(--dsw-alias-state-warning,#e6a23c) 45%,transparent);background:color-mix(in srgb,var(--dsw-alias-state-warning,#e6a23c) 5%,transparent)}",
1502
- ".mneme-conflict-side--b .mneme-conflict-sidelabel{color:var(--dsw-alias-state-warning,#e6a23c);font-weight:600}",
1686
+ ".mneme-conflict-side--a .mneme-conflict-sidelabel{color:var(--mneme-accent);font-weight:600}",
1687
+ ".mneme-conflict-side--b{border-color:color-mix(in srgb,var(--mneme-warn) 45%,transparent);background:color-mix(in srgb,var(--mneme-warn) 5%,transparent)}",
1688
+ ".mneme-conflict-side--b .mneme-conflict-sidelabel{color:var(--mneme-warn);font-weight:600}",
1503
1689
  ".mneme-conflict-simrow{display:flex;align-items:center;gap:8px;font-size:11px;color:var(--dsw-alias-label-secondary)}",
1504
1690
  ".mneme-conflict-simbar{position:relative;flex:1;height:5px;border-radius:3px;background:var(--dsw-alias-interactive-bg-hover);overflow:hidden}",
1505
1691
  ".mneme-conflict-simfill{position:absolute;top:0;bottom:0;left:0;border-radius:3px;background:var(--dsw-alias-state-business-primary)}",
1506
1692
  ".mneme-conflict-diff{font-size:12px;line-height:18px;color:var(--dsw-alias-label-primary);display:-webkit-box;-webkit-line-clamp:4;-webkit-box-orient:vertical;overflow:hidden}",
1693
+ // 展开全文:取消行数钳制并给一个内部滚动区——报障点是「窗口太小又不能
1694
+ // 放大,完全对比不了内容」,所以展开态必须能容纳长文本且不撑破面板。
1695
+ // 上限用 em 而非 vh:宿主可能把面板放在零高视口里(隐藏 webview / 后台
1696
+ // 页),那时 46vh 会算成 0px,展开态直接看不见字——实测踩过。
1697
+ ".mneme-conflict-diff--full{display:block;-webkit-line-clamp:unset;max-height:28em;overflow-y:auto}",
1698
+ ".mneme-conflict-text{font-size:12px;line-height:18px;color:var(--dsw-alias-label-primary);white-space:pre-wrap;overflow-wrap:anywhere;max-height:28em;overflow-y:auto}",
1699
+ ".mneme-conflict-meta{display:flex;align-items:center;gap:6px;flex-wrap:wrap;font-size:11px;line-height:16px;color:var(--mneme-fg-dim)}",
1700
+ ".mneme-conflict-metaval{color:var(--dsw-alias-label-secondary)}",
1701
+ // 窄面板下并排会把每侧挤到无法阅读,退成单列(容器查询按面板实际宽度判)
1702
+ "@container (max-width:560px){.mneme-conflict-pair{grid-template-columns:1fr}}",
1507
1703
  ".mneme-conflict-mark{text-decoration:none;border-radius:3px;padding:0 1px}",
1508
- ".mneme-conflict-mark--del{background:color-mix(in srgb,var(--dsw-alias-state-error,#c33) 16%,transparent);text-decoration:line-through}",
1509
- ".mneme-conflict-mark--ins{background:color-mix(in srgb,var(--dsw-alias-state-success,#3c9) 18%,transparent)}",
1510
- ".mneme-conflict-primary{display:inline-flex;align-items:center;gap:4px;height:24px;padding:0 10px;border-radius:8px;border:none;background:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-bg-layer-1);cursor:pointer;font-family:inherit;font-size:12px;line-height:16px}",
1704
+ ".mneme-conflict-mark--del{background:color-mix(in srgb,var(--mneme-danger) 16%,transparent);text-decoration:line-through}",
1705
+ ".mneme-conflict-mark--ins{background:color-mix(in srgb,var(--mneme-ok) 18%,transparent)}",
1706
+ ".mneme-conflict-primary{display:inline-flex;align-items:center;gap:4px;height:24px;padding:0 10px;border-radius:8px;border:none;background:var(--mneme-accent);color:var(--dsw-alias-bg-layer-1);cursor:pointer;font-family:inherit;font-size:12px;line-height:16px}",
1511
1707
  ".mneme-conflict-primary:disabled{opacity:.55;cursor:default}",
1512
- ".mneme-conflict-primary:hover:not(:disabled){background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 85%,black)}",
1513
- ".mneme-badge--frozen{color:var(--dsw-alias-state-warning,#e6a23c);background:color-mix(in srgb,var(--dsw-alias-state-warning,#e6a23c) 12%,transparent)}",
1708
+ ".mneme-conflict-primary:hover:not(:disabled){background:color-mix(in srgb,var(--mneme-accent) 85%,var(--dsw-alias-label-primary))}",
1709
+ ".mneme-badge--frozen{color:var(--mneme-warn);background:color-mix(in srgb,var(--mneme-warn) 12%,transparent)}",
1514
1710
  ".mneme-conflict-empty{border:1px dashed var(--dsw-alias-border-l2);border-radius:10px;padding:12px 14px;display:flex;flex-direction:column;gap:4px}",
1515
1711
  ".mneme-conflict-empty-title{font-size:12.5px;font-weight:600;color:var(--dsw-alias-label-secondary)}",
1516
- ".mneme-conflict-empty-body{font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}",
1517
- ".mneme-statuscard--actionable{border-color:color-mix(in srgb,var(--dsw-alias-state-warning,#e6a23c) 55%,transparent);box-shadow:0 0 0 1px color-mix(in srgb,var(--dsw-alias-state-warning,#e6a23c) 35%,transparent)}",
1518
- ".mneme-entrybadge{position:absolute;top:-3px;right:-3px;min-width:16px;height:16px;padding:0 4px;border-radius:8px;background:var(--dsw-alias-state-error,#c33);color:var(--dsw-alias-bg-layer-1,#fff);font-size:10.5px;line-height:16px;font-weight:600;text-align:center;box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-1)}",
1712
+ ".mneme-conflict-empty-body{font-size:12px;line-height:18px;color:var(--mneme-fg-dim)}",
1713
+ ".mneme-statuscard--actionable{border-color:color-mix(in srgb,var(--mneme-warn) 55%,transparent);box-shadow:0 0 0 1px color-mix(in srgb,var(--mneme-warn) 35%,transparent)}",
1714
+ ".mneme-entrybadge{position:absolute;top:-3px;right:-3px;min-width:16px;height:16px;padding:0 4px;border-radius:8px;background:var(--mneme-danger);color:var(--dsw-alias-bg-layer-1,#fff);font-size:10.5px;line-height:16px;font-weight:600;text-align:center;box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-1)}",
1519
1715
  ".mneme-triggerwrap{position:relative;pointer-events:none}",
1520
1716
  ".mneme-triggerwrap .mneme-trigger{pointer-events:auto}",
1521
- ".mneme-conflict-jump .mneme-statuscard{border-color:color-mix(in srgb,var(--dsw-alias-state-warning,#e6a23c) 55%,transparent);box-shadow:0 0 0 1px color-mix(in srgb,var(--dsw-alias-state-warning,#e6a23c) 35%,transparent)}",
1717
+ ".mneme-conflict-jump .mneme-statuscard{border-color:color-mix(in srgb,var(--mneme-warn) 55%,transparent);box-shadow:0 0 0 1px color-mix(in srgb,var(--mneme-warn) 35%,transparent)}",
1522
1718
  ".mneme-conflict-jump:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px;border-radius:12px}",
1523
1719
  // 详情抽屉:sheet 内右侧滑出,覆盖在浏览区之上
1524
1720
  ".mneme-xmain{position:relative}",
@@ -1530,7 +1726,7 @@ window.__ModuleLoader__.load({
1530
1726
  ".mneme-drawertitle{width:100%;font-size:16px;font-weight:600;line-height:23px;color:var(--dsw-alias-label-primary);word-break:break-word}",
1531
1727
  ".mneme-drawertitle-input{width:100%;box-sizing:border-box;font-size:15px;font-weight:600;line-height:22px;padding:7px 10px;border-radius:8px;border:1px solid var(--dsw-alias-state-business-primary);background:var(--dsw-alias-bg-base,transparent);color:var(--dsw-alias-label-primary);font-family:inherit;outline:none;box-shadow:0 0 0 3px color-mix(in srgb,var(--dsw-alias-state-business-primary) 15%,transparent)}",
1532
1728
  ".mneme-dmeta{display:grid;grid-template-columns:auto 1fr;gap:4px 14px;margin-top:12px;font-size:12.5px;line-height:19px}",
1533
- ".mneme-dmetakey{color:var(--dsw-alias-label-tertiary)}",
1729
+ ".mneme-dmetakey{color:var(--mneme-fg-dim)}",
1534
1730
  ".mneme-dmetaval{color:var(--dsw-alias-label-secondary);min-width:0;word-break:break-word}",
1535
1731
  ".mneme-dcontent{margin-top:14px;font-size:13.5px;line-height:1.75;color:var(--dsw-alias-label-primary);white-space:pre-wrap;word-break:break-word}",
1536
1732
  ".mneme-dcontent-input{width:100%;box-sizing:border-box;min-height:180px;font-size:13.5px;line-height:1.7;padding:9px 11px;border-radius:8px;border:1px solid var(--dsw-alias-state-business-primary);background:var(--dsw-alias-bg-base,transparent);color:var(--dsw-alias-label-primary);font-family:inherit;outline:none;resize:vertical;box-shadow:0 0 0 3px color-mix(in srgb,var(--dsw-alias-state-business-primary) 15%,transparent)}",
@@ -1550,12 +1746,12 @@ window.__ModuleLoader__.load({
1550
1746
  // --- explorer refresh: paged month tree, sticky headers, inline icons ---
1551
1747
  ".mneme-vtabico{flex:none;opacity:.85}",
1552
1748
  ".mneme-xsearchwrap{position:relative;flex:none}",
1553
- ".mneme-xsearchico{position:absolute;left:9px;top:50%;transform:translateY(-50%);color:var(--dsw-alias-label-tertiary);pointer-events:none}",
1749
+ ".mneme-xsearchico{position:absolute;left:9px;top:50%;transform:translateY(-50%);color:var(--mneme-fg-dim);pointer-events:none}",
1554
1750
  ".mneme-xsearchwrap .mneme-xsearch{padding-left:27px}",
1555
1751
  ".mneme-footbtn{display:inline-flex;align-items:center;gap:4px}",
1556
1752
  ".mneme-xmonth{position:sticky;top:0;z-index:2;background:var(--dsw-alias-bg-layer-1);display:flex;align-items:center;gap:6px}",
1557
- ".mneme-xmonthcount{margin-left:auto;color:var(--dsw-alias-label-tertiary);font-weight:400;font-variant-numeric:tabular-nums}",
1558
- ".mneme-xcaret{display:inline-flex;align-items:center;justify-content:center;color:var(--dsw-alias-label-tertiary)}",
1753
+ ".mneme-xmonthcount{margin-left:auto;color:var(--mneme-fg-dim);font-weight:400;font-variant-numeric:tabular-nums}",
1754
+ ".mneme-xcaret{display:inline-flex;align-items:center;justify-content:center;color:var(--mneme-fg-dim)}",
1559
1755
  ".mneme-xmore{display:flex;justify-content:center;align-items:center;padding:10px 0 24px;min-height:20px}",
1560
1756
  ".mneme-xemptyico{display:block;margin:0 auto 6px;opacity:.7}",
1561
1757
  // --- status sub-view: responsive stat-card grid (auto-fill, ~220px min) ---
@@ -1569,45 +1765,103 @@ window.__ModuleLoader__.load({
1569
1765
  "@container (min-width:452px) and (max-width:683px){.mneme-statuscard--vectorwide{grid-column:span 2}}",
1570
1766
  "@container (min-width:452px){.mneme-statuscard--wide{grid-column:span 2}}",
1571
1767
  // 状态页分组标题(库内一览 / 后台运转):只做扫读锚点,压得比卡片标题低
1572
- ".mneme-statushead{max-width:1000px;margin:0 auto 10px;font-size:12px;font-weight:600;letter-spacing:.02em;color:var(--dsw-alias-label-tertiary)}",
1768
+ ".mneme-statushead{max-width:1000px;margin:0 auto 10px;font-size:12px;font-weight:600;letter-spacing:.02em;color:var(--mneme-fg-dim)}",
1573
1769
  ".mneme-statusgrid+.mneme-statushead{margin-top:26px}",
1574
1770
  ".mneme-statuscard{min-width:0;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;padding:16px 16px 14px}",
1575
1771
  // 指标行:一行一指标(label 左 / 数值右),取代「·」串联的说明长句——
1576
1772
  // 拥挤感的根源是把多指标压进一段会换行的散文
1577
1773
  ".mneme-statrows{margin-top:10px;display:flex;flex-direction:column;gap:4px}",
1578
- ".mneme-statrow{display:flex;align-items:baseline;justify-content:space-between;gap:12px;font-size:12px;line-height:17px;color:var(--dsw-alias-label-tertiary)}",
1774
+ ".mneme-statrow{display:flex;align-items:baseline;justify-content:space-between;gap:12px;font-size:12px;line-height:17px;color:var(--mneme-fg-dim)}",
1579
1775
  ".mneme-statrow>span:first-child{flex:none;white-space:nowrap}",
1580
1776
  ".mneme-statrow .mneme-heatdot{align-self:center}",
1581
1777
  ".mneme-statrowval{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;text-align:right}",
1582
- ".mneme-statfoot{margin-top:8px;font-size:11px;line-height:16px;color:var(--dsw-alias-label-dimmed)}",
1778
+ ".mneme-statfoot{margin-top:8px;font-size:11px;line-height:16px;color:var(--mneme-fg-dim)}",
1583
1779
  // 热度分布条:三档占比一图看清,行标只补数字(类名带双横线,与 heatdot 同款)
1584
1780
  ".mneme-heatbar{display:flex;height:6px;border-radius:3px;overflow:hidden;margin-top:12px;background:var(--dsw-alias-interactive-bg-hover)}",
1585
1781
  ".mneme-heatbar-seg{min-width:2px}",
1586
- ".mneme-heatbar--hot{background:var(--dsw-alias-state-warning,#d97706)}",
1587
- ".mneme-heatbar--warm{background:var(--dsw-alias-label-tertiary)}",
1588
- ".mneme-heatbar--cold{background:var(--dsw-alias-label-dimmed)}",
1782
+ ".mneme-heatbar--hot{background:var(--mneme-warn)}",
1783
+ ".mneme-heatbar--warm{background:var(--mneme-fg-dim)}",
1784
+ ".mneme-heatbar--cold{background:var(--mneme-graphic-dim)}",
1589
1785
  ".mneme-heatdot{flex:none;width:8px;height:8px;border-radius:50%}",
1590
- ".mneme-heatdot--hot{background:var(--dsw-alias-state-warning,#d97706)}",
1591
- ".mneme-heatdot--warm{background:var(--dsw-alias-label-tertiary)}",
1592
- ".mneme-heatdot--cold{background:var(--dsw-alias-label-dimmed)}",
1786
+ ".mneme-heatdot--hot{background:var(--mneme-warn)}",
1787
+ ".mneme-heatdot--warm{background:var(--mneme-fg-dim)}",
1788
+ ".mneme-heatdot--cold{background:var(--mneme-graphic-dim)}",
1789
+ // --- Token 去向三屏(2026-10 可观测性改造):KPI 带 / donut / 7 天柱状 / 8 周热力 ---
1790
+ // KPI 带固定 4 列、容器收窄降 2 列:用 @container 不用 @media——面板同时长在
1791
+ // 宿主侧栏(~500px)与 sheet(1240px)里,决定布局的是容器宽度而非视口。
1792
+ ".mneme-kpiband{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;max-width:1000px;margin:0 auto 12px}",
1793
+ "@container (max-width:560px){.mneme-kpiband{grid-template-columns:repeat(2,minmax(0,1fr))}}",
1794
+ ".mneme-tokenflow{max-width:1000px;margin:0 auto 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;padding:16px 16px 14px}",
1795
+ ".mneme-donutwrap{display:flex;gap:20px;align-items:center;margin-top:12px}",
1796
+ "@container (max-width:480px){.mneme-donutwrap{flex-direction:column;align-items:stretch}.mneme-donutbox{align-self:center}}",
1797
+ ".mneme-donutbox{position:relative;flex:none;width:148px;height:148px}",
1798
+ ".mneme-donutbox svg{width:100%;height:100%;display:block}",
1799
+ ".mneme-donut-center{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;pointer-events:none}",
1800
+ ".mneme-donut-center b{font-size:19px;font-weight:650;font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-primary)}",
1801
+ ".mneme-donut-center span{font-size:11px;color:var(--mneme-fg-dim);margin-top:1px}",
1802
+ // r=15.9155 让周长≈100,dasharray 直接填百分比;段缝靠 JS 侧留,这里只管观感。
1803
+ ".mneme-donut circle.mneme-seg{fill:none;stroke-width:5.6;transition:opacity .15s}",
1804
+ ".mneme-donut.has-focus circle.mneme-seg:not(.focused){opacity:.35}",
1805
+ ".mneme-seg--1{stroke:var(--mneme-chart-1)}",
1806
+ ".mneme-seg--2{stroke:var(--mneme-chart-2)}",
1807
+ ".mneme-seg--3{stroke:var(--mneme-chart-3)}",
1808
+ ".mneme-seg--4{stroke:var(--mneme-chart-4)}",
1809
+ ".mneme-seg--5{stroke:var(--mneme-chart-5)}",
1810
+ ".mneme-seg--6{stroke:color-mix(in srgb,var(--dsw-alias-label-primary) 50%,transparent)}",
1811
+ ".mneme-legend{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}",
1812
+ ".mneme-legendrow{display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:8px;cursor:default;font-size:12.5px;line-height:18px;color:var(--dsw-alias-label-primary)}",
1813
+ ".mneme-legendrow:hover,.mneme-legendrow.focused{background:var(--dsw-alias-interactive-bg-hover)}",
1814
+ ".mneme-legendrow.dim{opacity:.45}",
1815
+ ".mneme-legenddot{flex:none;width:10px;height:10px;border-radius:3px}",
1816
+ ".mneme-dot--1{background:var(--mneme-chart-1)}",
1817
+ ".mneme-dot--2{background:var(--mneme-chart-2)}",
1818
+ ".mneme-dot--3{background:var(--mneme-chart-3)}",
1819
+ ".mneme-dot--4{background:var(--mneme-chart-4)}",
1820
+ ".mneme-dot--5{background:var(--mneme-chart-5)}",
1821
+ ".mneme-dot--6{background:color-mix(in srgb,var(--dsw-alias-label-primary) 50%,transparent)}",
1822
+ ".mneme-legendname{color:var(--dsw-alias-label-primary);white-space:nowrap}",
1823
+ ".mneme-legendsub{color:var(--mneme-fg-dim);font-size:11px}",
1824
+ ".mneme-legendval{margin-left:auto;font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-secondary);white-space:nowrap}",
1825
+ ".mneme-legendpct{flex:none;width:46px;text-align:right;font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-primary);font-weight:600}",
1826
+ ".mneme-bars{position:relative;height:150px;margin-top:14px}",
1827
+ ".mneme-gridline{position:absolute;left:0;right:0;border-top:1px dashed var(--dsw-alias-border-l2)}",
1828
+ ".mneme-gridlabel{position:absolute;right:0;transform:translateY(-58%);font-size:10px;color:var(--mneme-fg-dim)}",
1829
+ ".mneme-barcols{position:absolute;inset:0;display:flex;align-items:flex-end;gap:8px;padding:0 2px}",
1830
+ ".mneme-barcol{flex:1;min-width:0;display:flex;flex-direction:column;justify-content:flex-end;height:100%}",
1831
+ ".mneme-bar{border-radius:5px 5px 2px 2px;background:var(--mneme-accent);min-height:2px;transition:opacity .15s}",
1832
+ ".mneme-barcol:hover .mneme-bar{opacity:.8}",
1833
+ ".mneme-barlabels{display:flex;gap:8px;padding:6px 2px 0}",
1834
+ ".mneme-barlabels span{flex:1;min-width:0;text-align:center;font-size:10.5px;color:var(--mneme-fg-dim);font-variant-numeric:tabular-nums}",
1835
+ ".mneme-heatgrid{display:grid;grid-auto-flow:column;grid-template-rows:repeat(7,12px);gap:3px;margin-top:12px}",
1836
+ ".mneme-heatcell{width:12px;height:12px;border-radius:3px;background:var(--dsw-alias-interactive-bg-hover)}",
1837
+ // 类名编号=视觉档位(l1 最浅…l4 最深),变量编号=business 占比降序,故交叉引用。
1838
+ ".mneme-heatcell.l1{background:var(--mneme-heat-4)}",
1839
+ ".mneme-heatcell.l2{background:var(--mneme-heat-3)}",
1840
+ ".mneme-heatcell.l3{background:var(--mneme-heat-2)}",
1841
+ ".mneme-heatcell.l4{background:var(--mneme-heat-1)}",
1842
+ ".mneme-heatlegend{display:flex;align-items:center;gap:3px;margin-top:8px;font-size:10.5px;color:var(--mneme-fg-dim)}",
1843
+ ".mneme-heatlegend .mneme-heatcell{width:10px;height:10px}",
1844
+ // 原 9 卡降级折叠区:默认收起,展开才挂载子卡(见 TokenFlowSection 注释)。
1845
+ ".mneme-legacy{max-width:1000px;margin:0 auto 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;padding:12px 16px}",
1846
+ ".mneme-legacy summary{cursor:pointer;font-size:13px;font-weight:600;color:var(--dsw-alias-label-secondary)}",
1593
1847
  // --- 状态页工作台:让用户看见插件在干活(动态/沉淀/归档) ---
1594
1848
  ".mneme-wbhead{margin:28px auto 10px;font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);max-width:1000px;display:flex;align-items:baseline;justify-content:space-between;gap:8px}",
1595
1849
  ".mneme-wblist{max-width:1000px;display:flex;flex-direction:column;gap:6px}",
1596
1850
  ".mneme-wbrow{display:flex;gap:10px;align-items:flex-start;padding:10px 14px;border:1px solid var(--dsw-alias-border-l1);border-radius:10px}",
1597
- ".mneme-wbdot{flex:none;width:8px;height:8px;border-radius:50%;margin-top:6px;background:var(--dsw-alias-state-success,#2a9d6a)}",
1598
- ".mneme-wbdot--err{background:var(--dsw-alias-state-error,#c33)}",
1851
+ ".mneme-wbdot{flex:none;width:8px;height:8px;border-radius:50%;margin-top:6px;background:var(--mneme-ok)}",
1852
+ ".mneme-wbdot--err{background:var(--mneme-danger)}",
1599
1853
  ".mneme-wbmain{min-width:0;flex:1}",
1600
1854
  ".mneme-wbtitle{font-size:13px;line-height:19px;color:var(--dsw-alias-label-primary)}",
1601
- ".mneme-wbsub{font-size:12px;line-height:17px;color:var(--dsw-alias-label-tertiary);margin-top:1px}",
1855
+ ".mneme-wbsub{font-size:12px;line-height:17px;color:var(--mneme-fg-dim);margin-top:1px}",
1602
1856
  ".mneme-wbmemo{display:flex;gap:10px;align-items:center;padding:10px 14px;border:1px solid var(--dsw-alias-border-l1);border-radius:10px}",
1603
1857
  ".mneme-wbmemo .mneme-xdot{flex:none}",
1604
1858
  ".mneme-wbmemo-main{min-width:0;flex:1}",
1605
1859
  ".mneme-statusnum{font-size:24px;font-weight:600;line-height:32px;color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums}",
1606
- ".mneme-statuscap{margin-top:4px;font-size:13px;line-height:19px;color:var(--dsw-alias-label-tertiary);word-break:break-word}",
1860
+ ".mneme-statuscap{margin-top:4px;font-size:13px;line-height:19px;color:var(--mneme-fg-dim);word-break:break-word}",
1607
1861
  // --- destructive actions: red outline = delete, solid red = confirm ---
1608
- ".mneme-btndanger{color:var(--dsw-alias-state-error,#c33);border-color:var(--dsw-alias-state-error,#c33)}",
1609
- ".mneme-btndanger:hover{background:color-mix(in srgb,var(--dsw-alias-state-error,#c33) 12%,transparent)}",
1610
- ".mneme-btndangerconfirm{background:var(--dsw-alias-state-error,#c33);border-color:var(--dsw-alias-state-error,#c33);color:#fff}",
1862
+ ".mneme-btndanger{color:var(--mneme-danger);border-color:var(--mneme-danger)}",
1863
+ ".mneme-btndanger:hover{background:color-mix(in srgb,var(--mneme-danger) 12%,transparent)}",
1864
+ ".mneme-btndangerconfirm{background:var(--mneme-danger);border-color:var(--mneme-danger);color:var(--dsw-alias-bg-layer-1)}",
1611
1865
  ".mneme-btndangerconfirm:hover{filter:brightness(.9)}",
1612
1866
  // --- settings cards: boxed cards for the runtime-mode and external-API
1613
1867
  // sections — each card owns its fetch/PUT state, so it renders as a
@@ -1615,14 +1869,14 @@ window.__ModuleLoader__.load({
1615
1869
  ".mneme-set-card{border:1px solid var(--dsw-alias-border-l2);border-radius:12px;padding:16px;margin-bottom:10px}",
1616
1870
  // 设置页分组标题(个性化 / 记忆引擎 / 搜索 / 连接与安全 / 帮助):
1617
1871
  // 先给「这一段在管什么」,卡片才是具体选项
1618
- ".mneme-set-grouphead{font-size:12px;font-weight:600;letter-spacing:.02em;color:var(--dsw-alias-label-tertiary);margin:24px 0 10px}",
1872
+ ".mneme-set-grouphead{font-size:12px;font-weight:600;letter-spacing:.02em;color:var(--mneme-fg-dim);margin:24px 0 10px}",
1619
1873
  ".mneme-set-grouphead:first-child{margin-top:8px}",
1620
1874
  ".mneme-set-token{font-family:monospace;font-size:12px;padding:7px 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-base,transparent);color:var(--dsw-alias-label-primary);word-break:break-all;user-select:all}",
1621
- ".mneme-set-hint{font-size:12px;color:var(--dsw-alias-label-tertiary)}",
1875
+ ".mneme-set-hint{font-size:12px;color:var(--mneme-fg-dim)}",
1622
1876
  // --- 重要性编辑行:与查看态同一份 Lucide 星形(此前是文本星号下拉,角更尖锐且与其他星不统一) ---
1623
1877
  ".mneme-staredit{display:inline-flex;align-items:center;gap:1px;justify-self:start}",
1624
1878
  ".mneme-starbtn{border:none;background:none;cursor:pointer;padding:2px 1px;display:inline-flex;color:var(--dsw-alias-label-secondary);transition:transform .12s}",
1625
- ".mneme-starbtn--on{color:var(--dsw-alias-state-warning,#d97706)}",
1879
+ ".mneme-starbtn--on{color:var(--mneme-warn)}",
1626
1880
  ".mneme-starbtn:hover{transform:scale(1.12)}",
1627
1881
  ".mneme-starbtn:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px;border-radius:4px}",
1628
1882
  // --- 意见与反馈:图标行 + 版本 footer,收在设置页末尾当「关于」用 ---
@@ -1633,8 +1887,18 @@ window.__ModuleLoader__.load({
1633
1887
  ".mneme-feedrow:hover .mneme-feedico{background:var(--dsw-alias-interactive-bg-active)}",
1634
1888
  ".mneme-feedtext{display:flex;flex-direction:column;gap:1px;min-width:0}",
1635
1889
  ".mneme-feedname{font-size:13px;line-height:18px;font-weight:500;color:var(--dsw-alias-label-primary)}",
1636
- ".mneme-feedsub{font-size:12px;line-height:17px;color:var(--dsw-alias-label-tertiary)}",
1637
- ".mneme-set-foot{display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin-top:12px;padding-top:10px;border-top:1px solid var(--dsw-alias-border-l1);font-size:12px;color:var(--dsw-alias-label-tertiary)}"
1890
+ ".mneme-feedsub{font-size:12px;line-height:17px;color:var(--mneme-fg-dim)}",
1891
+ ".mneme-set-foot{display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin-top:12px;padding-top:10px;border-top:1px solid var(--dsw-alias-border-l1);font-size:12px;color:var(--mneme-fg-dim)}",
1892
+ // --- 存储回收卡(#386 第 4 条):控制行 + 两步确认的警告块 ---
1893
+ ".mneme-maintrow{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:10px}",
1894
+ ".mneme-maintlbl{flex:none;font-size:12px;line-height:17px;color:var(--mneme-fg-dim)}",
1895
+ ".mneme-maintrow .mneme-set-input{flex:none;width:96px;margin-bottom:0}",
1896
+ ".mneme-maintcheck{display:flex;align-items:center;gap:8px;margin-top:12px;font-size:13px;line-height:19px;color:var(--dsw-alias-label-primary)}",
1897
+ ".mneme-maintwarn{margin-top:12px;padding:10px 12px;border-radius:10px;border:1px solid color-mix(in srgb,var(--mneme-danger) 45%,transparent);background:color-mix(in srgb,var(--mneme-danger) 8%,transparent)}",
1898
+ ".mneme-maintwarnhead{font-size:13px;line-height:19px;font-weight:600;color:var(--mneme-danger)}",
1899
+ ".mneme-maintresult{margin-top:12px;padding-top:10px;border-top:1px solid var(--dsw-alias-border-l1)}",
1900
+ ".mneme-maintresulthead{font-size:13px;line-height:19px;font-weight:600;color:var(--mneme-ok)}",
1901
+ ".mneme-mainterr{margin-top:10px;font-size:12px;line-height:17px;color:var(--mneme-danger)}"
1638
1902
  ].join("\n");
1639
1903
  if (typeof document !== "undefined" && document.querySelector(`style[data-plugin-css="${CSS_TAG}"]`) === null) {
1640
1904
  const tag = document.createElement("style");
@@ -2982,7 +3246,7 @@ window.__ModuleLoader__.load({
2982
3246
  h("textarea", { className: "mneme-set-input mneme-set-input--area", style: { minHeight: 56 }, value: newCmd.instruction, placeholder: t("memory.settings.cmdInstruction"), onChange: (e) => setNewCmd({ ...newCmd, instruction: e.target.value }) }),
2983
3247
  h("div", { style: { display: "flex", alignItems: "center", gap: 8 } },
2984
3248
  h("button", { className: "mneme-btn", onClick: addCommand }, t("memory.settings.cmdAdd")),
2985
- cmdError && h("span", { style: { fontSize: 12, color: "var(--dsw-alias-state-error, #c33)" } }, cmdError)
3249
+ cmdError && h("span", { style: { fontSize: 12, color: "var(--mneme-danger)" } }, cmdError)
2986
3250
  )
2987
3251
  )
2988
3252
  ),
@@ -2993,7 +3257,7 @@ window.__ModuleLoader__.load({
2993
3257
  h("section", { className: "mneme-set-card" },
2994
3258
  h("div", { className: "mneme-set-title" }, t("memory.settings.mode.title")),
2995
3259
  h("div", { className: "mneme-set-desc" }, t("memory.settings.mode.desc")),
2996
- modeError && h("div", { style: { fontSize: 12, color: "var(--dsw-alias-state-error,#c33)", marginBottom: 8 } }, modeError),
3260
+ modeError && h("div", { style: { fontSize: 12, color: "var(--mneme-danger)", marginBottom: 8 } }, modeError),
2997
3261
  mode === null && !modeError
2998
3262
  ? h("div", { className: "mneme-set-hint" }, "…")
2999
3263
  : h(react.Fragment, null,
@@ -3044,7 +3308,7 @@ window.__ModuleLoader__.load({
3044
3308
  h("section", { className: "mneme-set-card" },
3045
3309
  h("div", { className: "mneme-set-title" }, t("memory.settings.extapi.title")),
3046
3310
  h("div", { className: "mneme-set-desc" }, t("memory.settings.extapi.desc")),
3047
- extapiError && h("div", { style: { fontSize: 12, color: "var(--dsw-alias-state-error,#c33)", marginBottom: 8 } }, extapiError),
3311
+ extapiError && h("div", { style: { fontSize: 12, color: "var(--mneme-danger)", marginBottom: 8 } }, extapiError),
3048
3312
  extapi === null && !extapiError
3049
3313
  ? h("div", { className: "mneme-set-hint" }, "…")
3050
3314
  : h(react.Fragment, null,
@@ -3288,7 +3552,7 @@ window.__ModuleLoader__.load({
3288
3552
  loading
3289
3553
  ? h("div", { className: "mneme-statusnum" }, "…")
3290
3554
  : error
3291
- ? h("div", { className: "mneme-statuscap", style: { color: "var(--dsw-alias-state-error,#c33)" } }, t("memory.status.error"))
3555
+ ? h("div", { className: "mneme-statuscap", style: { color: "var(--mneme-danger)" } }, t("memory.status.error"))
3292
3556
  : h(react.Fragment, null,
3293
3557
  h("div", { className: "mneme-statusnum" }, num),
3294
3558
  rows && rows.length
@@ -3546,6 +3810,250 @@ window.__ModuleLoader__.load({
3546
3810
  );
3547
3811
  }
3548
3812
 
3813
+ // 人读体积:回收报告里全是字节数(六到七位),不折成 KiB/MiB 就看不出「到底有没有
3814
+ // 变小」——而那正是这张卡要回答的问题。
3815
+ function formatBytes(n) {
3816
+ const v = Number(n);
3817
+ if (!Number.isFinite(v) || v < 0) return "—";
3818
+ if (v < 1024) return `${v} B`;
3819
+ const units = ["KiB", "MiB", "GiB", "TiB"];
3820
+ let x = v / 1024;
3821
+ let i = 0;
3822
+ while (x >= 1024 && i < units.length - 1) { x /= 1024; i++; }
3823
+ return `${x.toFixed(x >= 100 ? 0 : x >= 10 ? 1 : 2)} ${units[i]}`;
3824
+ }
3825
+
3826
+ // 存储回收卡(issue #386 第 4 条,状态页「维护」区)。`dsh-mneme reclaim` 此前只有
3827
+ // CLI 与 standalone 两张脸,面板侧一个字都没有——「不可逆的内容丢弃」这种能力要么
3828
+ // 能被发现,要么等于不存在。
3829
+ //
3830
+ // 三条不变量,别在「顺手简化」时弄丢:
3831
+ // ① 执行是两步。第一次点只进确认态,把这次要动的数字摆到眼前;第二次点才真的发
3832
+ // POST。不用 window.confirm——宿主 webview 里它不一定可用,而且它挡不住「到底
3833
+ // 要清多少行」这个问题。
3834
+ // ② VACUUM 失败 ≠ 成功。清理的两个 UPDATE 各自提交且不可逆,VACUUM 只负责把文件
3835
+ // 缩回去,失败最常见的原因是锁被别的进程占着。所以失败时必须同时说清四件事:
3836
+ // 清理已完成、不可逆、文件暂时不会变小、下一步重跑即可(清理是幂等的)。绝不
3837
+ // 能渲染成一次干净的「成功」。
3838
+ // ③ 保留窗口只在点「重新估算」时发一次请求。键入即发 = 想输 30 却先敲出 3 的那一刻
3839
+ // 就换了估算,数字会跳。而且估算一旦与输入框不一致,确认态里摆出来的就是另一个
3840
+ // 动作的数字——所以那种状态下执行钮直接禁用(见下面的 stale)。
3841
+ //
3842
+ // 报告字段由 src/maintenance.js 的 reclaim() 决定,卡片只读不改。
3843
+ function MaintenanceCard({ t }) {
3844
+ // 与 src/maintenance.js 的 DEFAULT_INPUT_RETENTION_DAYS 同值:这里只用来初始化
3845
+ // 输入框与首屏那次 GET,真正的默认值仍由服务端兜底。
3846
+ const DEFAULT_WINDOW_DAYS = 7;
3847
+ // 与后端 MAX_RECLAIM_WINDOW_DAYS 同值。超界窗口服务端会回 400,但本地先拦一道:
3848
+ // 否则用户点「重新估算」只会看到一句服务端错误,还以为是网络问题。
3849
+ const MAX_WINDOW_DAYS = 3650;
3850
+ // 面板走 in-process 路由,与 CLI/standalone 底下是同一条 reclaim 实现。
3851
+ const RECLAIM_PATH = "/api/dsh-mneme/maintenance/reclaim";
3852
+ const [days, setDays] = useState(String(DEFAULT_WINDOW_DAYS));
3853
+ const [vacuum, setVacuum] = useState(false);
3854
+ const [report, setReport] = useState(null);
3855
+ const [loading, setLoading] = useState(true);
3856
+ const [estimating, setEstimating] = useState(false);
3857
+ const [error, setError] = useState("");
3858
+ // 确认态:true = 第一次点过了,此刻按钮换成实心红确认钮。
3859
+ const [armed, setArmed] = useState(false);
3860
+ const [running, setRunning] = useState(false);
3861
+ // 成功与失败分开存:合成一个对象容易在「先失败后成功」时把上一次的残留显示出来。
3862
+ const [result, setResult] = useState(null);
3863
+ const [resultError, setResultError] = useState("");
3864
+
3865
+ // 挂载即预估:打开状态页就该看见「现在能收多少」,而不是先点一下才有数字。
3866
+ useEffect(() => {
3867
+ let cancelled = false;
3868
+ apiFetch(`${RECLAIM_PATH}?olderThanDays=${DEFAULT_WINDOW_DAYS}`)
3869
+ .then((res) => { if (!res.ok) throw new Error("http"); return res.json(); })
3870
+ .then((j) => { if (!cancelled) { setReport(j); setLoading(false); } })
3871
+ .catch(() => { if (!cancelled) { setError(t("memory.status.maint.failed")); setLoading(false); } });
3872
+ return () => { cancelled = true; };
3873
+ }, []);
3874
+
3875
+ // 只由「重新估算」按钮调用,不在 onChange 里调用(见上面 ③)。
3876
+ const estimate = () => {
3877
+ if (!/^\d+$/.test(days)) { setError(t("memory.status.maint.badWindow")); setArmed(false); return; }
3878
+ // 上界也在这里拦:`1e20` 与 400 位数字都能过 /^\d+$/(后者化成 Infinity),
3879
+ // 后端会回 400,但那样用户看到的是一句「回收失败」,不如直接说窗口不合法。
3880
+ if (Number(days) > MAX_WINDOW_DAYS) { setError(t("memory.status.maint.badWindow")); setArmed(false); return; }
3881
+ setEstimating(true);
3882
+ setError("");
3883
+ // 窗口一变,上一轮的确认态连同它显示的数字一起作废;上一次执行的回执也别再跟
3884
+ // 新的估算混在一起。
3885
+ setArmed(false);
3886
+ setResult(null);
3887
+ setResultError("");
3888
+ apiFetch(`${RECLAIM_PATH}?olderThanDays=${days}`)
3889
+ .then((res) => { if (!res.ok) throw new Error("http"); return res.json(); })
3890
+ .then((j) => { setReport(j); setEstimating(false); })
3891
+ .catch(() => { setError(t("memory.status.maint.failed")); setEstimating(false); });
3892
+ };
3893
+
3894
+ // 第二次点击(确认态里)才走这里。dryRun:false 与 confirm:true 是后端那条不可逆
3895
+ // 动作的确认门:缺 confirm 会回 400 confirm-required 并附一份 dry-run 报告。两个
3896
+ // 字面量逐字保留——改名或省掉一个都等于把门拆了。
3897
+ const execute = () => {
3898
+ setRunning(true);
3899
+ setResultError("");
3900
+ apiFetch(RECLAIM_PATH, {
3901
+ method: "POST",
3902
+ headers: { "Content-Type": "application/json" },
3903
+ body: JSON.stringify({ dryRun: false, confirm: true, olderThanDays: Number(days), vacuum })
3904
+ })
3905
+ .then((res) => res.json().catch(() => null).then((body) => ({ ok: res.ok, status: res.status, body })))
3906
+ .then((r) => {
3907
+ // 非 2xx 一律落到这条分支并带着服务端的 error 串返回:成功态只在守卫另一侧。
3908
+ if (!r.ok) {
3909
+ setResultError(`${t("memory.status.maint.failed")} · ${(r.body && r.body.error) || `HTTP ${r.status}`}`);
3910
+ setRunning(false);
3911
+ return;
3912
+ }
3913
+ setResult(r.body);
3914
+ setArmed(false);
3915
+ setRunning(false);
3916
+ })
3917
+ .catch(() => { setResultError(t("memory.status.maint.failed")); setRunning(false); });
3918
+ };
3919
+
3920
+ const size = (report && report.size) || {};
3921
+ const planned = (report && report.planned) || {};
3922
+ const rv = (result && result.vacuum) || {};
3923
+ const rsz = (result && result.size) || {};
3924
+ const rcl = (result && result.cleared) || {};
3925
+ // 估算数字与输入框不一致(刚改过窗口还没重新估算)时禁止执行:否则确认态里摆出来
3926
+ // 的是另一个窗口的数字,而对不可逆操作来说这是最坏的一种错位。
3927
+ const stale = !report || String(report.olderThanDays) !== days;
3928
+ // vacuum.error 是 string|null;非 null 就是「清理成功但文件没缩」这种半成功态。
3929
+ const vacuumError = rv.error == null ? null : String(rv.error);
3930
+ const freed = Number(rsz.before_bytes ?? 0) - Number(rsz.after_bytes ?? 0);
3931
+
3932
+ return h("div", { className: "mneme-statuscard mneme-statuscard--wide" },
3933
+ h("h3", { className: "mneme-xcolhead" }, t("memory.status.maint")),
3934
+ loading
3935
+ ? h("div", { className: "mneme-statusnum" }, "…")
3936
+ : h("div", { className: "mneme-statusnum" }, formatBytes(size.before_bytes)),
3937
+ report ? h("div", { className: "mneme-statrows" },
3938
+ h("div", { className: "mneme-statrow" },
3939
+ h("span", null, t("memory.status.maint.freelist")),
3940
+ h("span", { className: "mneme-statrowval" }, `${formatBytes(size.freelist_bytes)} · ${size.freelist_pages ?? 0}`)),
3941
+ h("div", { className: "mneme-statrow" },
3942
+ h("span", null, t("memory.status.maint.plannedInputs")),
3943
+ h("span", { className: "mneme-statrowval" }, String(planned.dream_run_inputs ?? 0))),
3944
+ h("div", { className: "mneme-statrow" },
3945
+ h("span", null, t("memory.status.maint.plannedEmbeddings")),
3946
+ h("span", { className: "mneme-statrowval" }, String(planned.archived_embeddings ?? 0))),
3947
+ h("div", { className: "mneme-statrow" },
3948
+ h("span", null, t("memory.status.maint.window")),
3949
+ h("span", { className: "mneme-statrowval" }, `${report.olderThanDays} ${t("memory.status.maint.windowUnit")}`))
3950
+ ) : null,
3951
+ // 控制区:窗口只在点按钮时重新估算;VACUUM 默认关,并明说它会短暂独占数据库。
3952
+ h("div", { className: "mneme-maintrow" },
3953
+ h("span", { className: "mneme-maintlbl" }, t("memory.status.maint.windowLabel")),
3954
+ h("input", {
3955
+ className: "mneme-set-input",
3956
+ inputMode: "numeric",
3957
+ value: days,
3958
+ disabled: armed || running,
3959
+ onChange: (e) => setDays(e.target.value)
3960
+ }),
3961
+ h("button", {
3962
+ type: "button",
3963
+ className: "mneme-btn",
3964
+ disabled: armed || running || estimating,
3965
+ onClick: estimate
3966
+ }, estimating ? t("memory.status.maint.reestimating") : t("memory.status.maint.reestimate"))
3967
+ ),
3968
+ h("label", { className: "mneme-maintcheck" },
3969
+ h("input", {
3970
+ type: "checkbox",
3971
+ checked: vacuum,
3972
+ disabled: armed || running,
3973
+ onChange: (e) => setVacuum(e.target.checked)
3974
+ }),
3975
+ h("span", null, t("memory.status.maint.vacuum"))
3976
+ ),
3977
+ h("div", { className: "mneme-statuscap" }, t("memory.status.maint.vacuumHint")),
3978
+ stale && !loading && !estimating
3979
+ ? h("div", { className: "mneme-statuscap" }, t("memory.status.maint.stale"))
3980
+ : null,
3981
+ // 两步确认。未武装时只有「执行回收」(点了只进确认态,不发请求);武装后换成实心
3982
+ // 红确认钮,并把窗口/VACUUM 在确认态里锁住——保证屏幕上的数字就是即将发出的动作。
3983
+ armed
3984
+ ? h(react.Fragment, null,
3985
+ h("div", { className: "mneme-maintwarn" },
3986
+ h("div", { className: "mneme-maintwarnhead" },
3987
+ t("memory.status.maint.confirmHint")
3988
+ .replace("{inputs}", String(planned.dream_run_inputs ?? 0))
3989
+ .replace("{embeddings}", String(planned.archived_embeddings ?? 0))),
3990
+ h("div", { className: "mneme-statuscap" },
3991
+ `${t("memory.status.maint.window")} ${report.olderThanDays} ${t("memory.status.maint.windowUnit")} · `
3992
+ + (vacuum ? t("memory.status.maint.vacuum") : t("memory.status.maint.vacuumOff")))
3993
+ ),
3994
+ h("div", { className: "mneme-maintrow" },
3995
+ h("button", {
3996
+ type: "button",
3997
+ className: "mneme-btn mneme-btndangerconfirm",
3998
+ disabled: running,
3999
+ onClick: execute
4000
+ }, running ? t("memory.status.maint.running") : t("memory.status.maint.confirm")),
4001
+ h("button", {
4002
+ type: "button",
4003
+ className: "mneme-btn",
4004
+ disabled: running,
4005
+ onClick: () => setArmed(false)
4006
+ }, t("memory.status.maint.cancel"))
4007
+ )
4008
+ )
4009
+ : h("div", { className: "mneme-maintrow" },
4010
+ h("button", {
4011
+ type: "button",
4012
+ className: "mneme-btn mneme-btndanger",
4013
+ disabled: running || loading || estimating || stale,
4014
+ onClick: () => setArmed(true)
4015
+ }, t("memory.status.maint.execute"))
4016
+ ),
4017
+ result ? h("div", { className: "mneme-maintresult" },
4018
+ h("div", { className: "mneme-maintresulthead" },
4019
+ vacuumError ? t("memory.status.maint.vacuumFailed") : t("memory.status.maint.done")),
4020
+ h("div", { className: "mneme-statrows" },
4021
+ h("div", { className: "mneme-statrow" },
4022
+ h("span", null, t("memory.status.maint.clearedInputs")),
4023
+ h("span", { className: "mneme-statrowval" }, String(rcl.dream_run_inputs ?? 0))),
4024
+ h("div", { className: "mneme-statrow" },
4025
+ h("span", null, t("memory.status.maint.clearedEmbeddings")),
4026
+ h("span", { className: "mneme-statrowval" }, String(rcl.archived_embeddings ?? 0))),
4027
+ h("div", { className: "mneme-statrow" },
4028
+ h("span", null, t("memory.status.maint.size")),
4029
+ h("span", { className: "mneme-statrowval" }, `${formatBytes(rsz.before_bytes)} → ${formatBytes(rsz.after_bytes)}`)),
4030
+ h("div", { className: "mneme-statrow" },
4031
+ h("span", null, t("memory.status.maint.reclaimed")),
4032
+ h("span", { className: "mneme-statrowval" }, formatBytes(freed))),
4033
+ h("div", { className: "mneme-statrow" },
4034
+ h("span", null, t("memory.status.maint.receipt")),
4035
+ h("span", { className: "mneme-statrowval" },
4036
+ result.receipt == null ? t("memory.status.maint.receiptNone") : String(result.receipt))),
4037
+ h("div", { className: "mneme-statrow" },
4038
+ h("span", null, t("memory.status.maint.duration")),
4039
+ h("span", { className: "mneme-statrowval" }, `${Number(result.duration_ms ?? 0)} ms`))
4040
+ ),
4041
+ // VACUUM 失败 ≠ 成功:清理已经落库且不可逆,失败的只是「把文件缩回去」这一步。
4042
+ // 这里必须把「已完成 + 不可逆 + 暂时不会变小」和下一步(重跑即可,清理是幂等
4043
+ // 的)一起说清,否则用户只看到「没变小」会以为白清了。
4044
+ vacuumError
4045
+ ? h("div", { className: "mneme-maintwarn" },
4046
+ h("div", { className: "mneme-maintwarnhead" },
4047
+ `${t("memory.status.maint.vacuumFailed")} · ${vacuumError}`),
4048
+ h("div", { className: "mneme-statuscap" }, t("memory.status.maint.vacuumFailedHint")))
4049
+ : h("div", { className: "mneme-statuscap" },
4050
+ rv.ran ? t("memory.status.maint.vacuumRan") : t("memory.status.maint.vacuumOff"))
4051
+ ) : null,
4052
+ resultError ? h("div", { className: "mneme-mainterr" }, resultError) : null,
4053
+ error ? h("div", { className: "mneme-mainterr" }, error) : null
4054
+ );
4055
+ }
4056
+
3549
4057
  // Markdown 镜像健康卡(issue #386 第 3 条)。后端 /health 对镜像读失败是
3550
4058
  // fail-closed(dirty=null → unknown);面板侧同样 fail-closed:fetch 失败
3551
4059
  // 或任何非 ok/degraded 的 status 一律渲染成「未知」,绝不显示「正常」——
@@ -3585,34 +4093,256 @@ window.__ModuleLoader__.load({
3585
4093
  });
3586
4094
  }
3587
4095
 
3588
- // LLM 消耗 — calls + tokens over the trailing 7 days.
3589
- function LlmStatusCard({ t }) {
3590
- const [state, setState] = useState({ loading: true, error: false, calls: 0, tokens: 0 });
4096
+ // --- Token 去向三屏(2026-10 可观测性改造)---
4097
+ // 原 LlmStatusCard 只有一个「LLM 消耗」总数,且 by_source 把两个最大成本项
4098
+ // dream_consolidate / dream_summarize 合并成 autoDream(2026-10 审计里巩固
4099
+ // 单项就占 37%),「token 花在哪」不可见。这里换成三屏:KPI 带(总量/调用/
4100
+ // 巩固占比/待确认冲突)、按组件 donut、按天柱状 + 8 周热力;原 9 张状态卡
4101
+ // 降级到折叠区。数据走 getLlmAuditStats 新增的 by_operation / by_day 两个
4102
+ // 聚合(后端已排好序,前端按 TOKEN_OPS 恒定顺序取用,不依赖 API 排序)。
4103
+ const TOKEN_OPS = [
4104
+ "dream_consolidate",
4105
+ "summarize_compress",
4106
+ "entity_extract",
4107
+ "dream_summarize",
4108
+ "sleep_pattern",
4109
+ "write_admission"
4110
+ ];
4111
+ const tokenOpLabel = (t, op) => {
4112
+ const key = `memory.status.token.op.${op}`;
4113
+ const label = t(key);
4114
+ return label !== key ? label : String(op);
4115
+ };
4116
+ const tokenCompact = (n) => {
4117
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
4118
+ if (n >= 1e3) return `${Math.round(n / 1e3)}K`;
4119
+ return String(n);
4120
+ };
4121
+ const heatLevel = (n) => (n > 2500000 ? 4 : n > 1500000 ? 3 : n > 750000 ? 2 : n > 0 ? 1 : 0);
4122
+
4123
+ function TokenFlowSection({ t }) {
4124
+ const [stats, setStats] = useState(null);
4125
+ const [heatStats, setHeatStats] = useState(null);
4126
+ const [statsError, setStatsError] = useState(false);
4127
+ const [conf, setConf] = useState({ loading: true, error: false, pending: 0 });
4128
+ const [focusOp, setFocusOp] = useState(null);
3591
4129
  useEffect(() => {
3592
4130
  let cancelled = false;
3593
4131
  apiFetch("/api/dsh-mneme/semantic/llm-audit/stats?days=7")
3594
4132
  .then((res) => { if (!res.ok) throw new Error("http"); return res.json(); })
3595
4133
  .then((j) => {
3596
4134
  if (cancelled) return;
3597
- const s = j && typeof j === "object" ? (j.stats || j) : {};
3598
- setState({ loading: false, error: false, calls: Number(s.total_calls ?? 0), tokens: Number(s.total_tokens ?? 0) });
4135
+ setStats(j && typeof j === "object" ? (j.stats || j) : {});
4136
+ setStatsError(false);
3599
4137
  })
3600
- .catch(() => { if (!cancelled) setState({ loading: false, error: true, calls: 0, tokens: 0 }); });
3601
- return () => { cancelled = true; };
4138
+ .catch(() => { if (!cancelled) setStatsError(true); });
4139
+ // 8 周热力要 56 天窗口,而 days=7 的 by_day 最多只有 8 个 UTC 日期——
4140
+ // 复用同一份响应会让 56 格里 48 格恒为 0(自动评审 #412 指出)。两个
4141
+ // 窗口各取所需:7 天喂 KPI 与柱状,56 天只喂热力。
4142
+ apiFetch("/api/dsh-mneme/semantic/llm-audit/stats?days=56")
4143
+ .then((res) => { if (!res.ok) throw new Error("http"); return res.json(); })
4144
+ .then((j) => {
4145
+ if (cancelled) return;
4146
+ setHeatStats(j && typeof j === "object" ? (j.stats || j) : {});
4147
+ })
4148
+ .catch(() => { if (!cancelled) setHeatStats({}); });
4149
+ // KPI 第 4 格的待确认冲突与 DreamStatusCards 同源(/dream-status),
4150
+ // 同样监听 mneme:conflicts-changed 重取(#295),两处计数永不漂移。
4151
+ const loadConflicts = () => {
4152
+ apiFetch("/api/dsh-mneme/dream-status")
4153
+ .then((res) => { if (!res.ok) throw new Error("http"); return res.json(); })
4154
+ .then((d) => {
4155
+ if (cancelled) return;
4156
+ setConf({ loading: false, error: false, pending: Number((d && d.pendingConflicts) ?? 0) });
4157
+ })
4158
+ .catch(() => { if (!cancelled) setConf({ loading: false, error: true, pending: 0 }); });
4159
+ };
4160
+ loadConflicts();
4161
+ window.addEventListener("mneme:conflicts-changed", loadConflicts);
4162
+ return () => { cancelled = true; window.removeEventListener("mneme:conflicts-changed", loadConflicts); };
3602
4163
  }, []);
3603
- return h(StatusCard, {
3604
- t,
3605
- title: t("memory.status.llm"),
3606
- loading: state.loading,
3607
- error: state.error,
3608
- num: state.tokens.toLocaleString(),
3609
- cap: t("memory.status.llmCalls").replace("{n}", state.calls.toLocaleString())
4164
+
4165
+ const loading = !stats && !statsError;
4166
+ const byOp = stats && Array.isArray(stats.by_operation) ? stats.by_operation : [];
4167
+ // 按恒定顺序取行:API 排序变化不影响图例次序;未知操作留到渲染时兜底。
4168
+ const ops = TOKEN_OPS.map((op) => {
4169
+ const row = byOp.find((r) => r.operation === op);
4170
+ return { op, total: Number((row && row.total_tokens) ?? 0) };
4171
+ });
4172
+ const unknownOps = byOp.filter((r) => !TOKEN_OPS.includes(r.operation));
4173
+ const allOps = ops.concat(unknownOps.map((r) => ({ op: r.operation, total: Number(r.total_tokens ?? 0) })));
4174
+ const grand = allOps.reduce((a, r) => a + r.total, 0);
4175
+ const consolidate = ops[0].total;
4176
+ // donut:r=15.9155 让周长≈100,dasharray 直接填百分比;段与段留缝(取
4177
+ // 半个百分点与四分之一弧长的较小者),小弧不被缝吃掉。write_admission
4178
+ // 的 token 恒 0 是设计(只计量不拦截),自然不画弧,图例仍保留一行。
4179
+ let cursor = 0;
4180
+ const segs = allOps
4181
+ .map((r, i) => {
4182
+ const len = grand > 0 ? (r.total / grand) * 100 : 0;
4183
+ const gap = Math.min(0.25, len / 4);
4184
+ const seg = Math.max(len - gap * 2, 0.4);
4185
+ const off = -(cursor + gap);
4186
+ cursor += len;
4187
+ return { ...r, i, len, seg, off };
4188
+ })
4189
+ .filter((s) => s.len > 0);
4190
+ const opIdx = (op) => {
4191
+ const i = TOKEN_OPS.indexOf(op);
4192
+ return i >= 0 ? i : Math.max(0, Math.min(allOps.findIndex((r) => r.op === op), 5));
4193
+ };
4194
+ // 7 天柱状:上限向上取整到整 M,四条网格线按 niceMax/4 等距。
4195
+ // by_day 只返回有审计行的日期(SQL 是 GROUP BY,缺日无行),而 since=now-7d
4196
+ // 的窗口横跨 8 个 UTC 日期——直接映射会让柱子缺列、横轴不是连续 7 天(自动
4197
+ // 评审 #412 指出)。这里以「今天 UTC」为右端补成连续 7 天,缺日填 0;响应还
4198
+ // 没到时保持空数组,否则加载态会先画 7 根零高柱子(stats 为 null 与 {} 分属
4199
+ // 「未知」与「已知为空」,只有后者该补零)。
4200
+ const rawDays = stats && Array.isArray(stats.by_day) ? stats.by_day : null;
4201
+ const rawDayMap = new Map((rawDays ?? []).map((d) => [String((d && d.day) ?? ""), Number((d && d.total_tokens) ?? 0)]));
4202
+ const endDay = new Date(`${new Date().toISOString().slice(0, 10)}T00:00:00Z`);
4203
+ const days = rawDays
4204
+ ? Array.from({ length: 7 }, (_, i) => {
4205
+ const key = new Date(endDay.getTime() - (6 - i) * 86400000).toISOString().slice(0, 10);
4206
+ return { day: key, total: rawDayMap.get(key) ?? 0 };
4207
+ })
4208
+ : [];
4209
+ const dayMax = Math.max(1, ...days.map((d) => d.total));
4210
+ const niceMax = Math.max(1e6, Math.ceil(dayMax / 1e6) * 1e6);
4211
+ const gridVals = [niceMax, niceMax * 0.75, niceMax * 0.5, niceMax * 0.25];
4212
+ // 8 周热力:数据必须来自 56 天窗口(heatStats),右缘对齐该窗口最后一天
4213
+ // (UTC),左侧补齐到周一;数据窗口之外的格子隐藏留形。日期一律走 UTC——
4214
+ // by_day 的 day 就是 UTC 日期。用 heatStats 而非 stats:否则 56 格里
4215
+ // 48 格恒 0。
4216
+ const heatDays = (heatStats && Array.isArray(heatStats.by_day) ? heatStats.by_day : []).map((d) => ({
4217
+ day: String((d && d.day) ?? ""),
4218
+ total: Number((d && d.total_tokens) ?? 0)
4219
+ }));
4220
+ const heatCells = [];
4221
+ if (heatDays.length) {
4222
+ const byDayMap = new Map(heatDays.map((d) => [d.day, d.total]));
4223
+ const end = new Date(`${heatDays[heatDays.length - 1].day}T00:00:00Z`);
4224
+ const start = new Date(end.getTime() - 55 * 86400000);
4225
+ for (let i = 0; i < 56; i++) {
4226
+ const d = new Date(start.getTime() + i * 86400000);
4227
+ const key = d.toISOString().slice(0, 10);
4228
+ heatCells.push({ key, total: byDayMap.get(key) ?? 0 });
4229
+ }
4230
+ }
4231
+ const kpi = (title, num, cap) =>
4232
+ h(StatusCard, { t, title, loading, error: statsError, num, cap });
4233
+ const segClass = (s) =>
4234
+ `mneme-seg mneme-seg--${opIdx(s.op) + 1}${focusOp === s.op ? " focused" : ""}`;
4235
+ const rowProps = (op) => ({
4236
+ className: focusOp && focusOp !== op ? "mneme-legendrow dim" : focusOp === op ? "mneme-legendrow focused" : "mneme-legendrow",
4237
+ onMouseEnter: () => setFocusOp(op),
4238
+ onMouseLeave: () => setFocusOp(null)
3610
4239
  });
4240
+
4241
+ return h(react.Fragment, null,
4242
+ h("div", { className: "mneme-statushead" }, t("memory.status.sec.token")),
4243
+ h("div", { className: "mneme-kpiband" },
4244
+ kpi(
4245
+ t("memory.status.token.total"),
4246
+ Number((stats && stats.total_tokens) ?? 0).toLocaleString(),
4247
+ `${t("memory.status.token.in")} ${Number((stats && stats.input_tokens) ?? 0).toLocaleString()} · ${t("memory.status.token.out")} ${Number((stats && stats.output_tokens) ?? 0).toLocaleString()}`
4248
+ ),
4249
+ kpi(t("memory.status.token.calls"), Number((stats && stats.total_calls) ?? 0).toLocaleString(), t("memory.status.token.callsCap")),
4250
+ kpi(
4251
+ t("memory.status.token.share"),
4252
+ grand > 0 ? `${((consolidate / grand) * 100).toFixed(1)}%` : "0.0%",
4253
+ t("memory.status.token.shareFoot").replace("{n}", consolidate.toLocaleString())
4254
+ ),
4255
+ h(StatusCard, {
4256
+ t,
4257
+ title: t("memory.status.token.conflicts"),
4258
+ loading: conf.loading,
4259
+ error: conf.error,
4260
+ num: conf.pending.toLocaleString(),
4261
+ cap: t("memory.status.token.conflictsFoot")
4262
+ })
4263
+ ),
4264
+ h("div", { className: "mneme-tokenflow" },
4265
+ h("h3", { className: "mneme-xcolhead" }, t("memory.status.token.donut")),
4266
+ statsError
4267
+ ? h("div", { className: "mneme-statuscap", style: { color: "var(--mneme-danger)" } }, t("memory.status.error"))
4268
+ : h("div", { className: "mneme-donutwrap" },
4269
+ h("div", { className: "mneme-donutbox" },
4270
+ h("svg", {
4271
+ className: focusOp ? "mneme-donut has-focus" : "mneme-donut",
4272
+ viewBox: "0 0 42 42",
4273
+ "aria-hidden": "true"
4274
+ },
4275
+ h("g", { transform: "rotate(-90 21 21)" },
4276
+ segs.map((s) => h("circle", {
4277
+ key: s.op,
4278
+ className: segClass(s),
4279
+ cx: 21,
4280
+ cy: 21,
4281
+ r: 15.9155,
4282
+ "stroke-dasharray": `${s.seg.toFixed(2)} ${(100 - s.seg).toFixed(2)}`,
4283
+ "stroke-dashoffset": s.off.toFixed(2)
4284
+ }))
4285
+ )
4286
+ ),
4287
+ h("div", { className: "mneme-donut-center" },
4288
+ h("b", null, tokenCompact(grand)),
4289
+ h("span", null, t("memory.status.token.last7"))
4290
+ )
4291
+ ),
4292
+ h("div", { className: "mneme-legend" },
4293
+ allOps.map((r) => h("div", Object.assign({ key: r.op }, rowProps(r.op)),
4294
+ h("span", { className: `mneme-legenddot mneme-dot--${opIdx(r.op) + 1}` }),
4295
+ h("span", { className: "mneme-legendname" }, tokenOpLabel(t, r.op)),
4296
+ r.op === "write_admission"
4297
+ ? h("span", { className: "mneme-legendsub" }, t("memory.status.token.meterOnly"))
4298
+ : null,
4299
+ h("span", { className: "mneme-legendval" }, r.total.toLocaleString()),
4300
+ h("span", { className: "mneme-legendpct" },
4301
+ grand > 0 ? `${((r.total / grand) * 100).toFixed(1)}%` : "0.0%")
4302
+ ))
4303
+ )
4304
+ ),
4305
+ h("h3", { className: "mneme-xcolhead" }, t("memory.status.token.days")),
4306
+ h("div", { className: "mneme-bars" },
4307
+ gridVals.map((v, i) => h("div", { key: v, className: "mneme-gridline", style: { top: `${i * 25}%` } },
4308
+ h("span", { className: "mneme-gridlabel" }, `${(v / 1e6).toFixed(0)}M`)
4309
+ )),
4310
+ h("div", { className: "mneme-barcols" },
4311
+ days.map((d) => h("div", { className: "mneme-barcol", key: d.day },
4312
+ h("div", {
4313
+ className: "mneme-bar",
4314
+ style: { height: `${Math.max((d.total / niceMax) * 100, 0.4)}%` },
4315
+ title: `${d.day} · ${d.total.toLocaleString()}`
4316
+ })
4317
+ ))
4318
+ )
4319
+ ),
4320
+ h("div", { className: "mneme-barlabels" },
4321
+ days.map((d) => h("span", { key: d.day }, d.day.slice(5)))
4322
+ ),
4323
+ h("h3", { className: "mneme-xcolhead" }, t("memory.status.token.heat")),
4324
+ h("div", { className: "mneme-heatgrid" },
4325
+ heatCells.map((c) => h("div", {
4326
+ key: c.key,
4327
+ className: heatLevel(c.total) ? `mneme-heatcell l${heatLevel(c.total)}` : "mneme-heatcell",
4328
+ title: `${c.key} · ${c.total.toLocaleString()}`
4329
+ }))
4330
+ ),
4331
+ h("div", { className: "mneme-heatlegend" },
4332
+ h("span", null, t("memory.status.token.heatLess")),
4333
+ h("span", { className: "mneme-heatcell" }),
4334
+ [1, 2, 3, 4].map((lv) => h("span", { key: lv, className: `mneme-heatcell l${lv}` })),
4335
+ h("span", null, t("memory.status.token.heatMore"))
4336
+ )
4337
+ )
4338
+ );
3611
4339
  }
3612
4340
 
3613
4341
  // 巩固状态卡(×2):最近一次 autoDream 运行 + 待确认冲突计数。数据来自
3614
4342
  // /dream-status,一次取回两张卡共用;失败只影响这两张卡自身。
3615
- function DreamStatusCards({ t, onGotoQueue }) {
4343
+ // showConflicts=false 用于 2026-10 改造后的降级折叠区:待确认冲突已升入
4344
+ // KPI 带,这里不再重复渲染冲突卡,只留最近运行卡。
4345
+ function DreamStatusCards({ t, onGotoQueue, showConflicts = true }) {
3616
4346
  const [state, setState] = useState({ loading: true, error: false, lastRun: null, pending: 0 });
3617
4347
  useEffect(() => {
3618
4348
  let cancelled = false;
@@ -3647,24 +4377,26 @@ window.__ModuleLoader__.load({
3647
4377
  const gotoQueue = () => { if (pending && onGotoQueue) onGotoQueue(); };
3648
4378
  return h(react.Fragment, null,
3649
4379
  h(StatusCard, { t, title: t("memory.status.dream"), loading: state.loading, error: state.error, num, cap }),
3650
- h("div", {
3651
- role: pending ? "button" : undefined,
3652
- tabIndex: pending ? 0 : undefined,
3653
- "aria-label": pending ? t("memory.status.conflictQueue.goto") : undefined,
3654
- className: pending ? "mneme-conflict-jump" : undefined,
3655
- style: pending ? { cursor: "pointer" } : undefined,
3656
- onClick: pending ? gotoQueue : undefined,
3657
- onKeyDown: pending ? (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); gotoQueue(); } } : undefined
3658
- },
3659
- h(StatusCard, {
3660
- t,
3661
- title: t("memory.status.conflicts"),
3662
- loading: state.loading,
3663
- error: state.error,
3664
- num: state.pending.toLocaleString(),
3665
- cap: t("memory.status.conflictsHint")
3666
- })
3667
- )
4380
+ showConflicts
4381
+ ? h("div", {
4382
+ role: pending ? "button" : undefined,
4383
+ tabIndex: pending ? 0 : undefined,
4384
+ "aria-label": pending ? t("memory.status.conflictQueue.goto") : undefined,
4385
+ className: pending ? "mneme-conflict-jump" : undefined,
4386
+ style: pending ? { cursor: "pointer" } : undefined,
4387
+ onClick: pending ? gotoQueue : undefined,
4388
+ onKeyDown: pending ? (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); gotoQueue(); } } : undefined
4389
+ },
4390
+ h(StatusCard, {
4391
+ t,
4392
+ title: t("memory.status.conflicts"),
4393
+ loading: state.loading,
4394
+ error: state.error,
4395
+ num: state.pending.toLocaleString(),
4396
+ cap: t("memory.status.conflictsHint")
4397
+ })
4398
+ )
4399
+ : null
3668
4400
  );
3669
4401
  }
3670
4402
 
@@ -3682,7 +4414,7 @@ window.__ModuleLoader__.load({
3682
4414
  return () => { cancelled = true; };
3683
4415
  }, []);
3684
4416
  if (state.loading || !state.suppressed) return null;
3685
- return h("div", { className: "mneme-statuscard", style: { borderColor: "var(--dsw-alias-state-warning,#e6a23c)" } },
4417
+ return h("div", { className: "mneme-statuscard", style: { borderColor: "var(--mneme-warn)" } },
3686
4418
  h("div", { className: "mneme-statusnum", style: { fontSize: "16px", lineHeight: "24px" } }, t("memory.status.injectSuppressed")),
3687
4419
  h("div", { className: "mneme-statuscap" }, t("memory.status.injectSuppressedHint"))
3688
4420
  );
@@ -3757,6 +4489,17 @@ window.__ModuleLoader__.load({
3757
4489
  const [items, setItems] = useState(null);
3758
4490
  const [loadError, setLoadError] = useState(false);
3759
4491
  const [busy, setBusy] = useState(false);
4492
+ // 展开态按冲突条目记(一对两侧同时展开,便于逐行对照);复制态按
4493
+ // 「条目:侧别」记,避免一次复制把别的按钮也点亮。
4494
+ const [openItems, setOpenItems] = useState({});
4495
+ const [copiedKey, setCopiedKey] = useState(null);
4496
+ const toggleItem = (id) => setOpenItems((prev) => ({ ...prev, [id]: !prev[id] }));
4497
+ const copySide = (key, content) => {
4498
+ navigator.clipboard?.writeText(String(content || "")).then(
4499
+ () => { setCopiedKey(key); announce(t("memory.status.conflictQueue.copiedSide")); setTimeout(() => setCopiedKey((k) => (k === key ? null : k)), 1500); },
4500
+ () => {}
4501
+ );
4502
+ };
3760
4503
  const load = (silent) => {
3761
4504
  apiFetch("/api/dsh-mneme/conflicts")
3762
4505
  .then((res) => { if (!res.ok) throw new Error("http"); return res.json(); })
@@ -3791,18 +4534,39 @@ window.__ModuleLoader__.load({
3791
4534
  .catch(() => {})
3792
4535
  .finally(() => setBusy(false));
3793
4536
  };
3794
- const side = (s, label, sideClass, ops, diffSide) => h("div", { className: `mneme-conflict-side ${sideClass}` },
4537
+ const side = (s, label, sideClass, ops, diffSide, open, itemKey) => h("div", { className: `mneme-conflict-side ${sideClass}` },
3795
4538
  h("div", { className: "mneme-conflict-sidelabel" }, label),
3796
4539
  s.missing
3797
4540
  ? h("div", { className: "mneme-conflict-missing" }, t("memory.status.conflictQueue.missing"))
3798
4541
  : h(react.Fragment, null,
3799
4542
  h("div", { className: "mneme-conflict-sidetitle", title: s.title }, s.title || "…"),
4543
+ // 判读依据行:类型 / 重要度 / 字数 / 新旧 + 单侧全文复制。报障者
4544
+ // 「只好两个都删或胡乱选」的直接原因是这一行了无信息——只有
4545
+ // A 方/B 方两个标签,看不出哪条更新、哪条更长、哪条更重要。
4546
+ h("div", { className: "mneme-conflict-meta" },
4547
+ h("span", { className: "mneme-conflict-metaval" }, typeLabel(t, s.type)),
4548
+ s.importance !== null && s.importance !== undefined
4549
+ ? h("span", null, t("memory.status.conflictQueue.importance").replace("{n}", String(s.importance)))
4550
+ : null,
4551
+ h("span", null, t("memory.status.conflictQueue.chars").replace("{n}", String((s.content || "").length))),
4552
+ h("span", null, (s.updated_at ? t("memory.status.conflictQueue.updated") : t("memory.status.conflictQueue.created"))
4553
+ .replace("{time}", formatRelativeTime(s.updated_at || s.created_at, t))),
4554
+ h("button", {
4555
+ className: "mneme-footbtn",
4556
+ onClick: () => copySide(`${itemKey}:${diffSide}`, s.content)
4557
+ }, copiedKey === `${itemKey}:${diffSide}`
4558
+ ? t("memory.status.conflictQueue.copiedSide")
4559
+ : t("memory.status.conflictQueue.copySide"))),
3800
4560
  // #177:词级 diff 可用(LCS 无损对齐成功)时用高亮视图替代纯文本。
3801
4561
  // 侧别过滤(#295 评审修正):A 侧渲染 same+del(它被删的部分高亮),
3802
4562
  // B 侧渲染 same+ins(它新增的部分高亮)——每列忠实于自己的原文。
3803
4563
  // title 提示颜色语义;diff 不可用时保留原 snippet。
4564
+ // 展开态:diff 去掉 4 行钳制、纯文本不再截 140 字(改走 pre-wrap 滚动区)。
3804
4565
  ops
3805
- ? h("div", { className: "mneme-conflict-diff", title: t("memory.status.conflictQueue.diffTitle") },
4566
+ ? h("div", {
4567
+ className: open ? "mneme-conflict-diff mneme-conflict-diff--full" : "mneme-conflict-diff",
4568
+ title: t("memory.status.conflictQueue.diffTitle")
4569
+ },
3806
4570
  ops.map((o, k) => {
3807
4571
  if (o.kind === "same") return o.text;
3808
4572
  if (o.kind === "del" && diffSide === "a") {
@@ -3813,7 +4577,8 @@ window.__ModuleLoader__.load({
3813
4577
  }
3814
4578
  return null; // 对侧的编辑片段不出现在本列
3815
4579
  }))
3816
- : h("div", { className: "mneme-conflict-snippet" }, (s.content || "").slice(0, 140)),
4580
+ : h("div", { className: open ? "mneme-conflict-text" : "mneme-conflict-snippet" },
4581
+ open ? (s.content || "") : (s.content || "").slice(0, 140)),
3817
4582
  // #177:预裁决阶段两侧都还活着——「已归档」徽章换成「冻结中」,
3818
4583
  // 原注销记说明(applyHint)挪进 tooltip,不再整段占一行动态区。
3819
4584
  h("span", {
@@ -3846,6 +4611,7 @@ window.__ModuleLoader__.load({
3846
4611
  h("div", { className: "mneme-conflict-empty-body" }, t("memory.status.conflictQueue.emptyBody")))),
3847
4612
  items.map((it) => {
3848
4613
  const sim = similarityOf(it.reason);
4614
+ const open = openItems[it.id] === true;
3849
4615
  const aText = (it.memory_a && it.memory_a.content) || "";
3850
4616
  const bText = (it.memory_b && it.memory_b.content) || "";
3851
4617
  // diff 与相似度条互补:reason 给不出数字时才跑 LCS(两者表达同一信息)。
@@ -3860,11 +4626,16 @@ window.__ModuleLoader__.load({
3860
4626
  h("span", { className: "mneme-conflict-simfill", style: { width: `${Math.round(sim * 100)}%` } })),
3861
4627
  h("span", null, `${Math.round(sim * 100)}%`)),
3862
4628
  h("div", { className: "mneme-conflict-pair" },
3863
- side(it.memory_a, t("memory.status.conflictQueue.sideA"), "mneme-conflict-side--a", diff, "a"),
3864
- side(it.memory_b, t("memory.status.conflictQueue.sideB"), "mneme-conflict-side--b", diff, "b")),
4629
+ side(it.memory_a, t("memory.status.conflictQueue.sideA"), "mneme-conflict-side--a", diff, "a", open, it.id),
4630
+ side(it.memory_b, t("memory.status.conflictQueue.sideB"), "mneme-conflict-side--b", diff, "b", open, it.id)),
3865
4631
  h("div", { className: "mneme-conflict-actions" },
3866
4632
  h("span", { className: "mneme-conflict-hint", title: t("memory.status.conflictQueue.applyHintTitle") },
3867
4633
  t("memory.status.conflictQueue.applyHint")),
4634
+ h("button", {
4635
+ className: "mneme-footbtn",
4636
+ "aria-expanded": open,
4637
+ onClick: () => toggleItem(it.id)
4638
+ }, t(open ? "memory.status.conflictQueue.collapse" : "memory.status.conflictQueue.expand")),
3868
4639
  h("button", { className: "mneme-conflict-primary", disabled: busy, "aria-label": `${t("memory.status.conflictQueue.keepA")}: ${(it.memory_a && it.memory_a.title) || ""}`, onClick: () => resolve(it.id, "a") }, t("memory.status.conflictQueue.keepA")),
3869
4640
  h("button", { className: "mneme-conflict-primary", disabled: busy, "aria-label": `${t("memory.status.conflictQueue.keepB")}: ${(it.memory_b && it.memory_b.title) || ""}`, onClick: () => resolve(it.id, "b") }, t("memory.status.conflictQueue.keepB")),
3870
4641
  h("button", { className: "mneme-footbtn", disabled: busy, onClick: () => resolve(it.id, null) }, t("memory.status.conflictQueue.markReviewed"))));
@@ -4152,24 +4923,41 @@ window.__ModuleLoader__.load({
4152
4923
  queueRef.current.focus({ preventScroll: true });
4153
4924
  if (queueRef.current.scrollIntoView) queueRef.current.scrollIntoView({ block: "start", behavior: "smooth" });
4154
4925
  };
4926
+ // 折叠区受控开合:收起时不挂载子卡,10 张卡各自的请求一次都不发——
4927
+ // 展开才是「我要看后台细节」的明确信号,首屏只承担 Token 三屏的开销。
4928
+ const [legacyOpen, setLegacyOpen] = useState(false);
4155
4929
  return h("div", { className: "mneme-status" },
4156
- // 两个分组标题把九张卡拆成「库内一览 / 后台运转」两段扫读单元
4157
- h("div", { className: "mneme-statushead" }, t("memory.status.sec.overview")),
4158
- h("div", { className: "mneme-statusgrid mneme-statusgrid--overview" },
4159
- h(MemoriesStatusCard, { t }),
4160
- h(EntitiesStatusCard, { t }),
4161
- h(VectorStatusCard, { t }),
4162
- h(MirrorHealthCard, { t })
4163
- ),
4164
- h("div", { className: "mneme-statushead" }, t("memory.status.sec.engine")),
4165
- h("div", { className: "mneme-statusgrid" },
4166
- h(DreamStatusCards, { t, onGotoQueue: gotoQueue }),
4167
- h(LlmStatusCard, { t }),
4168
- h(HeatStatusCard, { t }),
4169
- h(RecallStatsCard, { t }),
4170
- h(InjectStatusCard, { t }),
4171
- h(InjectPreviewCard, { t })
4930
+ h(TokenFlowSection, { t }),
4931
+ h("details", {
4932
+ className: "mneme-legacy",
4933
+ open: legacyOpen
4934
+ },
4935
+ // 开合挂在 summary 上而非 details:details 上的 onClick 会连折叠区里
4936
+ // 子卡片的点击一起吃掉(preventDefault 拦掉冒泡上来的激活事件),
4937
+ // 而 summary 是浏览器原生的开合把手,点它以外的地方不该切换(自动
4938
+ // 评审 #412 指出)。
4939
+ h("summary", { onClick: (e) => { e.preventDefault(); setLegacyOpen(!legacyOpen); } }, t("memory.status.token.detail")),
4940
+ legacyOpen ? h(react.Fragment, null,
4941
+ // 分组标题把九张卡拆成「库内一览 / 后台运转」两段扫读单元
4942
+ h("div", { className: "mneme-statushead" }, t("memory.status.sec.overview")),
4943
+ h("div", { className: "mneme-statusgrid mneme-statusgrid--overview" },
4944
+ h(MemoriesStatusCard, { t }),
4945
+ h(EntitiesStatusCard, { t }),
4946
+ h(VectorStatusCard, { t }),
4947
+ h(MirrorHealthCard, { t })
4948
+ ),
4949
+ h("div", { className: "mneme-statushead" }, t("memory.status.sec.engine")),
4950
+ h("div", { className: "mneme-statusgrid" },
4951
+ h(DreamStatusCards, { t, showConflicts: false }),
4952
+ h(HeatStatusCard, { t }),
4953
+ h(RecallStatsCard, { t }),
4954
+ h(InjectStatusCard, { t }),
4955
+ h(InjectPreviewCard, { t })
4956
+ )
4957
+ ) : null
4172
4958
  ),
4959
+ h("div", { className: "mneme-statushead" }, t("memory.status.sec.maintenance")),
4960
+ h("div", { className: "mneme-statusgrid" }, h(MaintenanceCard, { t })),
4173
4961
  h("div", { ref: queueRef }, h(ConflictsQueue, { t })),
4174
4962
  h(WorkbenchSection, { t, onBrowse })
4175
4963
  );
@@ -4377,7 +5165,7 @@ window.__ModuleLoader__.load({
4377
5165
  ? h(react.Fragment, null,
4378
5166
  confirmWiden
4379
5167
  ? h(react.Fragment, null,
4380
- h("span", { style: { fontSize: 12, color: "var(--dsw-alias-state-warn,#b8860b)" } },
5168
+ h("span", { style: { fontSize: 12, color: "var(--mneme-warn)" } },
4381
5169
  t("memory.explorer.detail.scopeWidenHint")),
4382
5170
  h("button", { type: "button", className: "mneme-btn mneme-btndangerconfirm", disabled: busy, onClick: save },
4383
5171
  t("memory.explorer.detail.scopeWidenConfirm")),
@@ -4400,7 +5188,7 @@ window.__ModuleLoader__.load({
4400
5188
  }
4401
5189
  }, t("memory.explorer.detail.cancel")),
4402
5190
  savedTick && h("span", { className: "mneme-saved" }, t("memory.explorer.detail.saved")),
4403
- saveError && h("span", { style: { fontSize: 12, color: "var(--dsw-alias-state-error,#c33)" } }, t("memory.explorer.detail.archiveFailed"))
5191
+ saveError && h("span", { style: { fontSize: 12, color: "var(--mneme-danger)" } }, t("memory.explorer.detail.archiveFailed"))
4404
5192
  )
4405
5193
  : h(react.Fragment, null,
4406
5194
  h("button", { type: "button", className: "mneme-btn", onClick: () => setEditing(true) }, t("memory.explorer.detail.edit")),
@@ -4428,7 +5216,7 @@ window.__ModuleLoader__.load({
4428
5216
  )
4429
5217
  : h("button", { type: "button", className: "mneme-btn mneme-btndanger", onClick: () => setConfirmDelete(true) },
4430
5218
  t("memory.explorer.delete")),
4431
- deleteError && h("span", { style: { fontSize: 12, color: "var(--dsw-alias-state-error,#c33)" } },
5219
+ deleteError && h("span", { style: { fontSize: 12, color: "var(--mneme-danger)" } },
4432
5220
  t("memory.explorer.deleteFailed"))
4433
5221
  )
4434
5222
  )
@@ -4483,7 +5271,7 @@ window.__ModuleLoader__.load({
4483
5271
  onChange: (e) => { setFile((e.target.files && e.target.files[0]) || null); setErr(""); }
4484
5272
  })
4485
5273
  ),
4486
- err && h("div", { className: "mneme-featsubhint", style: { color: "var(--dsw-alias-state-error,#c33)", marginTop: 8 } }, err),
5274
+ err && h("div", { className: "mneme-featsubhint", style: { color: "var(--mneme-danger)", marginTop: 8 } }, err),
4487
5275
  msg && h("div", { className: "mneme-saved", style: { marginTop: 8 } }, msg),
4488
5276
  h("div", { className: "mneme-improw", style: { justifyContent: "flex-end" } },
4489
5277
  h("button", { type: "button", className: "mneme-btn", disabled: busy, onClick: onClose }, t("memory.explorer.importCancel")),
@@ -4493,6 +5281,117 @@ window.__ModuleLoader__.load({
4493
5281
  );
4494
5282
  }
4495
5283
 
5284
+ // #386 第 5 条:面板手动新增一条记忆。字段与 memory_save 工具对齐
5285
+ //(type/title/content/importance/tags),但**不放 scope/sensitivity**:归属的
5286
+ // 显式声明走详情抽屉那条路,它带「放宽可见性」二次确认;在新建表单里顺手盖
5287
+ // 章会绕开那道确认。后端的去重键仍是 (type, title, scope, sensitivity),所以
5288
+ // 同类型同标题会并入既有行——服务端回的 action 决定提示语说「已新建」还是
5289
+ // 「已并入」,两者对用户是不同的事实,不能都报「已保存」。
5290
+ function CreateDialog({ t, onClose, onCreated }) {
5291
+ const [type, setType] = useState("preference");
5292
+ const [title, setTitle] = useState("");
5293
+ const [content, setContent] = useState("");
5294
+ const [importance, setImportance] = useState(3);
5295
+ const [tags, setTags] = useState("");
5296
+ const [busy, setBusy] = useState(false);
5297
+ const [msg, setMsg] = useState("");
5298
+ const [err, setErr] = useState("");
5299
+ const submit = () => {
5300
+ // 前端先拦一次空标题/空正文:后端也会 400,但往返一趟只为报「标题不能
5301
+ // 为空」不值当。服务端校验仍是权威——它同时管着类型白名单与去重。
5302
+ if (!title.trim()) { setErr(t("memory.explorer.newNoTitle")); return; }
5303
+ if (!content.trim()) { setErr(t("memory.explorer.newNoContent")); return; }
5304
+ setBusy(true);
5305
+ setErr("");
5306
+ apiFetch("/api/dsh-mneme/create", {
5307
+ method: "POST",
5308
+ headers: { "Content-Type": "application/json" },
5309
+ body: JSON.stringify({
5310
+ type,
5311
+ title: title.trim(),
5312
+ content: content.trim(),
5313
+ importance,
5314
+ tags: tags.split(",").map((s) => s.trim()).filter(Boolean)
5315
+ })
5316
+ })
5317
+ .then((res) => { if (!res.ok) throw new Error("http"); return res.json(); })
5318
+ .then((d) => {
5319
+ setMsg(d && d.action === "merged" ? t("memory.explorer.newMerged") : t("memory.explorer.newCreated"));
5320
+ // 成功后**不**重置 busy。这 900ms 是留给提示的阅读时间,而「新建」按钮此刻
5321
+ // 若重新可点,第二次同 type+title 会落进同一行,service 走 appendContent 把
5322
+ // 正文再追加一遍,提示还会从「已新建」翻成「已并入」——一次误触就写坏一条
5323
+ // 记忆。保持 busy 直到关窗,按钮全程 disabled(成功路径组件随即卸载,没有
5324
+ // 需要复原的状态)。
5325
+ // 停 900ms 让提示可读,再关窗并把新建的行选中(见 onCreated)。
5326
+ setTimeout(() => { onCreated(d); onClose(); }, 900);
5327
+ })
5328
+ .catch(() => { setBusy(false); setErr(t("memory.explorer.newFailed")); });
5329
+ };
5330
+ return h("div", { className: "mneme-impdialog", role: "region", "aria-label": t("memory.explorer.newTitle") },
5331
+ h("div", { className: "mneme-set-title" }, t("memory.explorer.newTitle")),
5332
+ h("div", { className: "mneme-featsubhint", style: { marginTop: 4 } }, t("memory.explorer.newHint")),
5333
+ h("div", { className: "mneme-improw" },
5334
+ h("span", { className: "mneme-featnumlabel" }, t("memory.explorer.newType")),
5335
+ h("select", { className: "mneme-select", value: type, onChange: (e) => setType(e.target.value) },
5336
+ // document 刻意不在列:它是 agent 注册长文档的指针行,铸造口唯一
5337
+ //(registerDocument,#230),后端路由也会 400。列出来只会让人点了
5338
+ // 才被拒。
5339
+ ["preference", "project", "decision", "history", "rejected_solution", "pitfall", "constraint"].map((k) =>
5340
+ h("option", { key: k, value: k }, typeLabel(t, k))))
5341
+ ),
5342
+ h("div", { className: "mneme-improw" },
5343
+ h("input", {
5344
+ type: "text",
5345
+ className: "mneme-set-input",
5346
+ value: title,
5347
+ placeholder: t("memory.explorer.newTitlePlaceholder"),
5348
+ "aria-label": t("memory.explorer.newTitlePlaceholder"),
5349
+ onChange: (e) => { setTitle(e.target.value); setErr(""); }
5350
+ })
5351
+ ),
5352
+ h("div", { className: "mneme-improw" },
5353
+ h("textarea", {
5354
+ className: "mneme-set-input mneme-set-input--area",
5355
+ value: content,
5356
+ placeholder: t("memory.explorer.newContentPlaceholder"),
5357
+ "aria-label": t("memory.explorer.newContentPlaceholder"),
5358
+ rows: 4,
5359
+ onChange: (e) => { setContent(e.target.value); setErr(""); }
5360
+ })
5361
+ ),
5362
+ h("div", { className: "mneme-improw" },
5363
+ h("input", {
5364
+ type: "text",
5365
+ className: "mneme-set-input",
5366
+ value: tags,
5367
+ placeholder: t("memory.explorer.newTagsPlaceholder"),
5368
+ "aria-label": t("memory.explorer.newTagsPlaceholder"),
5369
+ onChange: (e) => setTags(e.target.value)
5370
+ })
5371
+ ),
5372
+ h("div", { className: "mneme-improw" },
5373
+ h("span", { className: "mneme-featnumlabel" }, t("memory.explorer.importance")),
5374
+ h("div", { className: "mneme-staredit", role: "radiogroup", "aria-label": t("memory.explorer.detail.editImportance") },
5375
+ [1, 2, 3, 4, 5].map((n) => h("button", {
5376
+ key: n,
5377
+ type: "button",
5378
+ role: "radio",
5379
+ "aria-checked": String(importance === n),
5380
+ "aria-label": t("memory.explorer.detail.starLabel").replace("{n}", String(n)),
5381
+ className: importance >= n ? "mneme-starbtn mneme-starbtn--on" : "mneme-starbtn",
5382
+ onClick: () => setImportance(n)
5383
+ }, h(StarGlyph, { size: 15, filled: importance >= n }))))
5384
+ ),
5385
+ err && h("div", { className: "mneme-featsubhint", style: { color: "var(--mneme-danger)", marginTop: 8 } }, err),
5386
+ msg && h("div", { className: "mneme-saved", style: { marginTop: 8 } }, msg),
5387
+ h("div", { className: "mneme-improw", style: { justifyContent: "flex-end" } },
5388
+ h("button", { type: "button", className: "mneme-btn", disabled: busy, onClick: onClose }, t("memory.explorer.importCancel")),
5389
+ h("button", { type: "button", className: "mneme-btn", disabled: busy, onClick: submit },
5390
+ busy && !msg ? t("memory.explorer.newSaving") : t("memory.explorer.newConfirm"))
5391
+ )
5392
+ );
5393
+ }
5394
+
4496
5395
  function MemoryExplorer({ t }) {
4497
5396
  const [view, setView] = useState("memory"); // memory | entity | status | settings
4498
5397
  const [items, setItems] = useState([]); // loaded browse pages, chrono desc
@@ -4528,6 +5427,7 @@ window.__ModuleLoader__.load({
4528
5427
  const [pendingIds, setPendingIds] = useState(() => new Set());
4529
5428
  const [menuOpen, setMenuOpen] = useState(false);
4530
5429
  const [importOpen, setImportOpen] = useState(false);
5430
+ const [createOpen, setCreateOpen] = useState(false);
4531
5431
  const itemRefs = useRef(new Map());
4532
5432
  const moreRef = useRef(null);
4533
5433
 
@@ -4877,9 +5777,11 @@ window.__ModuleLoader__.load({
4877
5777
  "aria-label": t("memory.explorer.more"),
4878
5778
  "aria-expanded": String(menuOpen),
4879
5779
  title: t("memory.explorer.more"),
4880
- onClick: () => { setMenuOpen(!menuOpen); setImportOpen(false); }
5780
+ onClick: () => { setMenuOpen(!menuOpen); setImportOpen(false); setCreateOpen(false); }
4881
5781
  }, "⋯"),
4882
5782
  menuOpen && h("div", { className: "mneme-menu", role: "menu" },
5783
+ h("button", { type: "button", role: "menuitem", onClick: () => { setCreateOpen(true); setMenuOpen(false); } },
5784
+ t("memory.explorer.newMemory")),
4883
5785
  h("button", { type: "button", role: "menuitem", onClick: () => { setMenuOpen(false); exportData("json"); } },
4884
5786
  t("memory.explorer.exportJson")),
4885
5787
  h("button", { type: "button", role: "menuitem", onClick: () => { setMenuOpen(false); exportData("markdown"); } },
@@ -4891,6 +5793,13 @@ window.__ModuleLoader__.load({
4891
5793
  t,
4892
5794
  onClose: () => setImportOpen(false),
4893
5795
  onImported: () => setReloadKey((k) => k + 1)
5796
+ }),
5797
+ createOpen && h(CreateDialog, {
5798
+ t,
5799
+ onClose: () => setCreateOpen(false),
5800
+ // 新建后既刷新列表也选中新行:并入既有条目时 action 是 merged,
5801
+ // 返回的 id 就是被并入那一行——两种情况都该把它滚进视野。
5802
+ onCreated: (d) => { setReloadKey((k) => k + 1); if (d && d.id) setSelectedId(d.id); }
4894
5803
  })
4895
5804
  )
4896
5805
  ),
@@ -5442,4 +6351,4 @@ window.__ModuleLoader__.load({
5442
6351
  exports.inject = inject;
5443
6352
  return module.exports;
5444
6353
  }
5445
- });
6354
+ });