@modusensus/dsh-mneme 0.7.17 → 0.7.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -55,7 +55,8 @@ window.__ModuleLoader__.load({
55
55
  copy: [["rect", { width: "14", height: "14", x: "8", y: "8", rx: "2", ry: "2" }], ["path", { d: "M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" }]],
56
56
  check: [["path", { d: "M20 6 9 17l-5-5" }]],
57
57
  inbox: [["polyline", { points: "22 12 16 12 14 15 10 15 8 12 2 12" }], ["path", { d: "M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" }]],
58
- activity: [["path", { d: "M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2" }]]
58
+ activity: [["path", { d: "M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2" }]],
59
+ flame: [["path", { d: "M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z" }]]
59
60
  };
60
61
  const Icon = ({ name, size = 16, className }) => {
61
62
  const parts = ICON_PATHS[name];
@@ -92,6 +93,18 @@ window.__ModuleLoader__.load({
92
93
  }))));
93
94
  };
94
95
 
96
+ // 热度徽章(阶段二):flame 图标 + 整数百分比,三档配色(热/温/冷)。
97
+ // /list 仅在 heatEnabled=true 时下发 heat 字段——缺省即不渲染,开关关闭
98
+ // 时徽章全站自动消失,前端无需感知开关状态。
99
+ const HeatBadge = ({ value, size = 12 }) => {
100
+ if (typeof value !== "number" || !Number.isFinite(value)) return null;
101
+ const pct = Math.round(Math.min(1, Math.max(0, value)) * 100);
102
+ const tier = pct >= 66 ? "hot" : pct >= 33 ? "warm" : "cold";
103
+ return h("span", { className: `mneme-heat mneme-heat--${tier}`, title: `${pct}%` },
104
+ h(Icon, { name: "flame", size, className: "mneme-heatico" }),
105
+ h("span", { className: "mneme-heatpct" }, `${pct}%`));
106
+ };
107
+
95
108
  // Unified API fetcher: attaches the optional apiToken (set in the settings
96
109
  // view, persisted in localStorage) as a Bearer header. When no token has
97
110
  // been configured the header is omitted and the API stays open (default).
@@ -105,6 +118,10 @@ window.__ModuleLoader__.load({
105
118
  return fetch(path, { ...opts, headers });
106
119
  }
107
120
 
121
+ // ⚠️ 'betterSidebar' 绝不能进模块级 inject(issue #88 实锤):loader 对
122
+ // inject 声明的服务是硬等待——未安装 better-sidebar 的环境里整个 entry
123
+ // 永远 pending("1 entry did not activate" → Failed to load plugins)。
124
+ // 软依赖靠内层动态子插件探测(见 apply 内 better-sidebar 挂载块)。
108
125
  const inject = ["slots", "locale"];
109
126
 
110
127
  const NS = "memory";
@@ -174,6 +191,15 @@ window.__ModuleLoader__.load({
174
191
  "memory.explorer.updated": "更新",
175
192
  "memory.explorer.tags": "标签",
176
193
  "memory.explorer.importance": "重要性",
194
+ "memory.explorer.heat": "热度",
195
+ "memory.explorer.sourceFilter": "来源",
196
+ "memory.explorer.sort": "排序",
197
+ "memory.explorer.heatSort": "热度优先",
198
+ "memory.explorer.heatSortHint": "对已加载条目按热度降序(页内排序,非全局序;时间树保持时间序)",
199
+ "memory.explorer.filterDeposited": "沉淀",
200
+ "memory.explorer.filterDepositedHint": "只看 autoDream 巩固/更新过的记忆",
201
+ "memory.explorer.filterArchived": "已归档",
202
+ "memory.explorer.filterArchivedHint": "只看已归档的记忆(默认列表不含它们)",
177
203
  "memory.explorer.topK": "返回数量",
178
204
  "memory.explorer.topKOption": "返回 {n} 条",
179
205
  "memory.card.open": "在记忆库中查看全文",
@@ -329,6 +355,8 @@ window.__ModuleLoader__.load({
329
355
  "memory.explorer.detail.save": "保存",
330
356
  "memory.explorer.detail.cancel": "取消",
331
357
  "memory.explorer.detail.archive": "归档",
358
+ "memory.explorer.detail.restore": "恢复",
359
+ "memory.explorer.detail.restored": "已恢复到主列表",
332
360
  "memory.explorer.detail.archived": "已归档",
333
361
  "memory.explorer.detail.archiveFailed": "操作失败",
334
362
  "memory.explorer.detail.saved": "已保存,镜像同步更新",
@@ -355,6 +383,11 @@ window.__ModuleLoader__.load({
355
383
  "memory.status.consolidated": "沉淀的记忆",
356
384
  "memory.status.consolidatedEmpty": "autoDream / autoSummarize 沉淀的记忆会出现在这里",
357
385
  "memory.status.archivedMemories": "已归档的记忆",
386
+ "memory.status.heatDistribution": "热度分布",
387
+ "memory.status.heatHint": "热门(≥66%){hot} · 温热(33–66%){warm} · 冷却(<33%){cold},样本为最近 {sample} 条",
388
+ "memory.status.viewAll": "查看全部",
389
+ "memory.status.depositedCount": "沉淀的记忆({n})",
390
+ "memory.status.archivedCount": "已归档的记忆({n})",
358
391
  "memory.status.archivedEmpty": "没有归档的记忆",
359
392
  "memory.status.restore": "恢复",
360
393
  "memory.settings.extapi.reveal": "显示",
@@ -438,6 +471,15 @@ window.__ModuleLoader__.load({
438
471
  "memory.explorer.updated": "Updated",
439
472
  "memory.explorer.tags": "Tags",
440
473
  "memory.explorer.importance": "Importance",
474
+ "memory.explorer.heat": "Heat",
475
+ "memory.explorer.sourceFilter": "Source",
476
+ "memory.explorer.sort": "Sort",
477
+ "memory.explorer.heatSort": "By heat",
478
+ "memory.explorer.heatSortHint": "Sort loaded items by heat, in-page (not global; the month tree keeps chronological order)",
479
+ "memory.explorer.filterDeposited": "Deposited",
480
+ "memory.explorer.filterDepositedHint": "Only memories autoDream consolidated or updated",
481
+ "memory.explorer.filterArchived": "Archived",
482
+ "memory.explorer.filterArchivedHint": "Only archived memories (excluded from the default list)",
441
483
  "memory.explorer.topK": "Results limit",
442
484
  "memory.explorer.topKOption": "Return {n}",
443
485
  "memory.card.open": "Open full text in the memory library",
@@ -593,6 +635,8 @@ window.__ModuleLoader__.load({
593
635
  "memory.explorer.detail.save": "Save",
594
636
  "memory.explorer.detail.cancel": "Cancel",
595
637
  "memory.explorer.detail.archive": "Archive",
638
+ "memory.explorer.detail.restore": "Restore",
639
+ "memory.explorer.detail.restored": "Restored to the main list",
596
640
  "memory.explorer.detail.archived": "Archived",
597
641
  "memory.explorer.detail.archiveFailed": "Action failed",
598
642
  "memory.explorer.detail.saved": "Saved; mirrors re-rendered",
@@ -619,6 +663,11 @@ window.__ModuleLoader__.load({
619
663
  "memory.status.consolidated": "Deposited memories",
620
664
  "memory.status.consolidatedEmpty": "Memories deposited by autoDream / autoSummarize appear here",
621
665
  "memory.status.archivedMemories": "Archived memories",
666
+ "memory.status.heatDistribution": "Heat distribution",
667
+ "memory.status.heatHint": "hot (≥66%) {hot} · warm (33–66%) {warm} · cooled (<33%) {cold}, sampled from the latest {sample}",
668
+ "memory.status.viewAll": "View all",
669
+ "memory.status.depositedCount": "Deposited memories ({n})",
670
+ "memory.status.archivedCount": "Archived memories ({n})",
622
671
  "memory.status.archivedEmpty": "Nothing archived",
623
672
  "memory.status.restore": "Restore",
624
673
  "memory.settings.extapi.reveal": "Reveal",
@@ -686,6 +735,8 @@ window.__ModuleLoader__.load({
686
735
  // text-turns-brand-blue active states — no boxed panels).
687
736
  const CSS_TAG = "@modusensus/dsh-mneme/drawer.css";
688
737
  const css = [
738
+ // --- explorer shell: container queries let the toolbar adapt to the
739
+ // better-sidebar workbench pane (~500px) as well as the 1240px sheet ---
689
740
  // --- sidebar foot trigger (wide row / collapsed rail icon) ---
690
741
  ".mneme-trigger{box-sizing:border-box;cursor:pointer;width:calc(100% + 4px);height:42px;color:var(--dsw-alias-label-primary);background:0 0;border:none;border-radius:12px;flex:none;align-items:center;gap:8px;margin:4px -2px;padding:0 10px 0 8px;font-family:inherit;font-size:14px;line-height:22px;display:flex;overflow:hidden}",
691
742
  ".mneme-trigger:hover{background:var(--dsw-alias-interactive-bg-hover)}",
@@ -705,7 +756,11 @@ window.__ModuleLoader__.load({
705
756
  ".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}",
706
757
  ".mneme-entitychip:hover{background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 10%,transparent)}",
707
758
  // --- main-area memory library page ---
708
- ".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)}",
759
+ ".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}",
760
+ // 窄容器(better-sidebar 工作台 ~500px)下中央 vtabs 会与右缘悬浮的
761
+ // 卡片/时间线切换器重叠:容器查询收窄时隐藏切换器,视图模式沿用
762
+ // localStorage 记忆,宽容器(sheet)不受影响。
763
+ "@container (max-width: 640px){.mneme-xtools .mneme-seg{display:none}}",
709
764
  ".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}",
710
765
  ".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)}",
711
766
  ".mneme-vtabs{display:flex;align-items:stretch;height:46px}",
@@ -845,6 +900,12 @@ window.__ModuleLoader__.load({
845
900
  // wrapper display:contents 隐身,按钮成为侧边栏弹性布局的直接子元素;
846
901
  // 盒模型/间距/收起态 rail 几何全部继承宿主,我们只覆盖配色为次级观感。
847
902
  ".mneme-stars{display:inline-flex;align-items:center;gap:2px}",
903
+ // 热度徽章三档:热(橙)/温(次级文字)/冷(弱化文字),冷档藏百分比只留图标。
904
+ ".mneme-heat{display:inline-flex;align-items:center;gap:2px;font-size:11px;line-height:14px;font-variant-numeric:tabular-nums}",
905
+ ".mneme-heat--hot{color:var(--dsw-alias-state-warning,#d97706)}",
906
+ ".mneme-heat--warm{color:var(--dsw-alias-label-secondary)}",
907
+ ".mneme-heat--cold{color:var(--dsw-alias-label-tertiary)}",
908
+ ".mneme-heat--cold .mneme-heatpct{display:none}",
848
909
  // width:100% 与「新会话」同宽(同一弹性父容器);文案对齐交给复制来的
849
910
  // 原生类,不再用 flex:1/text-align:left 覆盖成左对齐。
850
911
  ".mneme-topentry{display:contents}",
@@ -938,7 +999,7 @@ window.__ModuleLoader__.load({
938
999
  ".mneme-statusgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px;max-width:1000px;margin:0 auto}",
939
1000
  ".mneme-statuscard{min-width:0;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:14px}",
940
1001
  // --- 状态页工作台:让用户看见插件在干活(动态/沉淀/归档) ---
941
- ".mneme-wbhead{margin:28px auto 10px;font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);max-width:1000px}",
1002
+ ".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}",
942
1003
  ".mneme-wblist{max-width:1000px;display:flex;flex-direction:column;gap:6px}",
943
1004
  ".mneme-wbrow{display:flex;gap:10px;align-items:flex-start;padding:10px 14px;border:1px solid var(--dsw-alias-border-l1);border-radius:10px}",
944
1005
  ".mneme-wbdot{flex:none;width:8px;height:8px;border-radius:50%;margin-top:6px;background:var(--dsw-alias-state-success,#2a9d6a)}",
@@ -1023,7 +1084,16 @@ window.__ModuleLoader__.load({
1023
1084
 
1024
1085
  function nodeRadius(n) {
1025
1086
  // mention_count → area-ish growth, clamped so hubs stay legible.
1026
- return 7 + Math.min(20, Math.max(1, n.mention_count ?? 1)) * 0.55;
1087
+ const base = 7 + Math.min(20, Math.max(1, n.mention_count ?? 1)) * 0.55;
1088
+ // heat projection (0-1 or null): hot entities render slightly larger.
1089
+ if (n.heat == null) return base;
1090
+ return base + (Math.max(0, Math.min(1, n.heat)) - 0.5) * 4;
1091
+ }
1092
+
1093
+ function nodeOpacity(n) {
1094
+ // heat projection: hot entities are bright (opacity 1), cold fade to 0.4.
1095
+ if (n.heat == null) return 1;
1096
+ return 0.4 + 0.6 * Math.max(0, Math.min(1, n.heat));
1027
1097
  }
1028
1098
 
1029
1099
  // Deterministic golden-angle spiral: no two nodes start overlapping, and
@@ -1472,7 +1542,7 @@ window.__ModuleLoader__.load({
1472
1542
  onClick: () => onNodeClick(n),
1473
1543
  "data-node": n.name
1474
1544
  },
1475
- h("circle", { r: nodeRadius(n), fill: typeColor(n.type) }),
1545
+ h("circle", { r: nodeRadius(n), fill: typeColor(n.type), fillOpacity: nodeOpacity(n) }),
1476
1546
  h("text", { className: "mneme-glabel", y: nodeRadius(n) + 13 }, n.name)
1477
1547
  ))
1478
1548
  )
@@ -2379,11 +2449,14 @@ window.__ModuleLoader__.load({
2379
2449
  return [];
2380
2450
  }
2381
2451
 
2382
- function WorkbenchSection({ t }) {
2452
+ function WorkbenchSection({ t, onBrowse }) {
2383
2453
  const [feed, setFeed] = useState(null); // audit rows(已过滤巩固/总结)
2384
- const [deposited, setDeposited] = useState(null); // ids 解析出的记忆
2385
- const [archived, setArchived] = useState(null);
2454
+ const [deposited, setDeposited] = useState(null); // 沉淀记忆首页(服务端 deposited 视图)
2455
+ const [depositedTotal, setDepositedTotal] = useState(0);
2456
+ const [archived, setArchived] = useState(null); // 归档记忆首页
2457
+ const [archivedTotal, setArchivedTotal] = useState(0);
2386
2458
  const [restoring, setRestoring] = useState("");
2459
+ const [wbReload, setWbReload] = useState(0);
2387
2460
 
2388
2461
  const load = useCallback(() => {
2389
2462
  let cancelled = false;
@@ -2391,30 +2464,31 @@ window.__ModuleLoader__.load({
2391
2464
  .then((r) => (r.ok ? r.json() : { items: [] }))
2392
2465
  .then((d) => {
2393
2466
  if (cancelled) return;
2394
- const rows = (d.items || []).filter((r) =>
2395
- r.trigger_source === "autoDream" || r.trigger_source === "autoSummarize");
2396
- setFeed(rows);
2397
- const ids = [];
2398
- for (const row of rows) {
2399
- for (const id of parseRelatedIds(row.related_memory_ids)) {
2400
- if (!ids.includes(id)) ids.push(id);
2401
- }
2402
- }
2403
- return apiFetch("/api/dsh-mneme/list?limit=300&order=chrono")
2404
- .then((r2) => (r2.ok ? r2.json() : { items: [] }))
2405
- .then((d2) => {
2406
- if (cancelled) return;
2407
- const byId = new Map((d2.items || []).map((m) => [m.id, m]));
2408
- setDeposited(ids.map((id) => byId.get(id)).filter(Boolean).slice(0, 8));
2409
- });
2467
+ setFeed((d.items || []).filter((r) =>
2468
+ r.trigger_source === "autoDream" || r.trigger_source === "autoSummarize"));
2410
2469
  })
2411
2470
  .catch(() => { if (!cancelled) setFeed([]); });
2412
- apiFetch("/api/dsh-mneme/list?archived=only&limit=50&order=chrono")
2413
- .then((r) => (r.ok ? r.json() : { items: [] }))
2414
- .then((d) => { if (!cancelled) setArchived(d.items || []); })
2471
+ // 仪表盘数据源:沉淀/归档各取一小页 + 服务端 total(/list 的 total
2472
+ // 与行同过滤)。长列表不再进状态页——「查看全部」跳记忆库的筛选
2473
+ // 视图,查询收敛到有搜索/分页的地方。
2474
+ apiFetch("/api/dsh-mneme/list?deposited=only&limit=8&order=chrono")
2475
+ .then((r) => (r.ok ? r.json() : { items: [], total: 0 }))
2476
+ .then((d) => {
2477
+ if (cancelled) return;
2478
+ setDeposited(d.items || []);
2479
+ setDepositedTotal(d.total || 0);
2480
+ })
2481
+ .catch(() => { if (!cancelled) setDeposited([]); });
2482
+ apiFetch("/api/dsh-mneme/list?archived=only&limit=3&order=chrono")
2483
+ .then((r) => (r.ok ? r.json() : { items: [], total: 0 }))
2484
+ .then((d) => {
2485
+ if (cancelled) return;
2486
+ setArchived(d.items || []);
2487
+ setArchivedTotal(d.total || 0);
2488
+ })
2415
2489
  .catch(() => { if (!cancelled) setArchived([]); });
2416
2490
  return () => { cancelled = true; };
2417
- }, []);
2491
+ }, [wbReload]);
2418
2492
  useEffect(() => { load(); }, [load]);
2419
2493
 
2420
2494
  const restore = (id) => {
@@ -2425,7 +2499,10 @@ window.__ModuleLoader__.load({
2425
2499
  body: JSON.stringify({ id, archived: false })
2426
2500
  })
2427
2501
  .then((r) => { if (!r.ok) throw new Error("http"); })
2428
- .then(() => setArchived((cur) => (cur || []).filter((m) => m.id !== id)))
2502
+ .then(() => {
2503
+ setArchived((cur) => (cur || []).filter((m) => m.id !== id));
2504
+ setArchivedTotal((n) => Math.max(0, n - 1));
2505
+ })
2429
2506
  .catch(() => {})
2430
2507
  .finally(() => setRestoring(""));
2431
2508
  };
@@ -2457,8 +2534,14 @@ window.__ModuleLoader__.load({
2457
2534
  row.error_message ? ` · ${row.error_message}` : "")
2458
2535
  ));
2459
2536
  })),
2460
- deposited !== null && deposited.length > 0 && h(react.Fragment, null,
2461
- h("div", { className: "mneme-wbhead" }, t("memory.status.consolidated")),
2537
+ deposited !== null && depositedTotal > 0 && h(react.Fragment, null,
2538
+ h("div", { className: "mneme-wbhead" },
2539
+ h("span", null, t("memory.status.depositedCount").replace("{n}", String(depositedTotal))),
2540
+ onBrowse && depositedTotal > deposited.length && h("button", {
2541
+ type: "button",
2542
+ className: "mneme-footbtn",
2543
+ onClick: () => onBrowse({ deposited: true })
2544
+ }, t("memory.status.viewAll"))),
2462
2545
  h("div", { className: "mneme-wblist" },
2463
2546
  deposited.map((m) => h("div", { className: "mneme-wbmemo", key: m.id },
2464
2547
  h("span", { className: "mneme-xdot", style: { color: memoryTypeColor(m.type) }, "aria-hidden": "true" }),
@@ -2468,7 +2551,13 @@ window.__ModuleLoader__.load({
2468
2551
  `${typeLabel(t, m.type)} · ${formatRelativeTime(m.updated_at || m.created_at, t)}`))
2469
2552
  )))),
2470
2553
  archived !== null && h(react.Fragment, null,
2471
- h("div", { className: "mneme-wbhead" }, t("memory.status.archivedMemories")),
2554
+ h("div", { className: "mneme-wbhead" },
2555
+ h("span", null, t("memory.status.archivedCount").replace("{n}", String(archivedTotal))),
2556
+ onBrowse && archivedTotal > archived.length && h("button", {
2557
+ type: "button",
2558
+ className: "mneme-footbtn",
2559
+ onClick: () => onBrowse({ archived: true })
2560
+ }, t("memory.status.viewAll"))),
2472
2561
  archived.length === 0
2473
2562
  ? h("div", { className: "mneme-featsubhint" }, t("memory.status.archivedEmpty"))
2474
2563
  : h("div", { className: "mneme-wblist" },
@@ -2488,16 +2577,56 @@ window.__ModuleLoader__.load({
2488
2577
  );
2489
2578
  }
2490
2579
 
2491
- function StatusPanel({ t }) {
2580
+ // 热度分布卡(阶段二):采样最近 200 条的 heat 值做三档分布。自门控——
2581
+ // /list 不下发 heat(heatEnabled=false)时整卡不渲染,前端不感知开关。
2582
+ function HeatStatusCard({ t }) {
2583
+ const [state, setState] = useState({ loading: true, off: false, hot: 0, warm: 0, cold: 0, sample: 0 });
2584
+ useEffect(() => {
2585
+ let cancelled = false;
2586
+ apiFetch("/api/dsh-mneme/list?limit=200&order=chrono")
2587
+ .then((res) => { if (!res.ok) throw new Error("http"); return res.json(); })
2588
+ .then((d) => {
2589
+ if (cancelled) return;
2590
+ const items = d.items || [];
2591
+ if (!items.length || typeof items[0].heat !== "number") {
2592
+ setState({ loading: false, off: true, hot: 0, warm: 0, cold: 0, sample: 0 });
2593
+ return;
2594
+ }
2595
+ let hot = 0, warm = 0, cold = 0;
2596
+ for (const m of items) {
2597
+ if (m.heat >= 0.66) hot++; else if (m.heat >= 0.33) warm++; else cold++;
2598
+ }
2599
+ setState({ loading: false, off: false, hot, warm, cold, sample: items.length });
2600
+ })
2601
+ .catch(() => { if (!cancelled) setState({ loading: false, off: true, hot: 0, warm: 0, cold: 0, sample: 0 }); });
2602
+ return () => { cancelled = true; };
2603
+ }, []);
2604
+ if (state.off) return null;
2605
+ return h(StatusCard, {
2606
+ t,
2607
+ title: t("memory.status.heatDistribution"),
2608
+ loading: state.loading,
2609
+ error: false,
2610
+ num: `${state.hot}`,
2611
+ cap: t("memory.status.heatHint")
2612
+ .replace("{hot}", String(state.hot))
2613
+ .replace("{warm}", String(state.warm))
2614
+ .replace("{cold}", String(state.cold))
2615
+ .replace("{sample}", String(state.sample))
2616
+ });
2617
+ }
2618
+
2619
+ function StatusPanel({ t, onBrowse }) {
2492
2620
  return h("div", { className: "mneme-status" },
2493
2621
  h("div", { className: "mneme-statusgrid" },
2494
2622
  h(MemoriesStatusCard, { t }),
2495
2623
  h(EntitiesStatusCard, { t }),
2496
2624
  h(VectorStatusCard, { t }),
2497
2625
  h(LlmStatusCard, { t }),
2498
- h(DreamStatusCards, { t })
2626
+ h(DreamStatusCards, { t }),
2627
+ h(HeatStatusCard, { t })
2499
2628
  ),
2500
- h(WorkbenchSection, { t })
2629
+ h(WorkbenchSection, { t, onBrowse })
2501
2630
  );
2502
2631
  }
2503
2632
 
@@ -2584,6 +2713,9 @@ window.__ModuleLoader__.load({
2584
2713
  onChange: (e) => setImportance(Number(e.target.value))
2585
2714
  }, [1, 2, 3, 4, 5].map((n) => h("option", { key: n, value: n }, "★".repeat(n))))
2586
2715
  : h(ImportanceStars, { className: "mneme-dmetaval", value: memory.importance || 0 }),
2716
+ memory.heat != null && h(react.Fragment, null,
2717
+ h("span", { className: "mneme-dmetakey" }, t("memory.explorer.heat")),
2718
+ h("span", { className: "mneme-dmetaval" }, h(HeatBadge, { value: memory.heat, size: 13 }))),
2587
2719
  memory.source && h(react.Fragment, null,
2588
2720
  h("span", { className: "mneme-dmetakey" }, t("memory.explorer.source")),
2589
2721
  h("span", { className: "mneme-dmetaval", title: memory.source }, memory.source)),
@@ -2633,8 +2765,11 @@ window.__ModuleLoader__.load({
2633
2765
  )
2634
2766
  : h(react.Fragment, null,
2635
2767
  h("button", { type: "button", className: "mneme-btn", onClick: () => setEditing(true) }, t("memory.explorer.detail.edit")),
2636
- h("button", { type: "button", className: "mneme-btn", disabled: busy, onClick: () => postUpdate({ archived: true }, { archived: true }) },
2637
- t("memory.explorer.detail.archive")),
2768
+ memory.archived
2769
+ ? h("button", { type: "button", className: "mneme-btn", disabled: busy, onClick: () => postUpdate({ archived: false }, { restored: true }) },
2770
+ t("memory.explorer.detail.restore"))
2771
+ : h("button", { type: "button", className: "mneme-btn", disabled: busy, onClick: () => postUpdate({ archived: true }, { archived: true }) },
2772
+ t("memory.explorer.detail.archive")),
2638
2773
  h("button", {
2639
2774
  type: "button",
2640
2775
  className: "mneme-btn",
@@ -2747,6 +2882,10 @@ window.__ModuleLoader__.load({
2747
2882
  catch { return "timeline"; }
2748
2883
  });
2749
2884
  const [dateRange, setDateRange] = useState("all"); // all | 7d | 30d | 90d
2885
+ // 方案 A:沉淀/归档筛选 chip——状态页「查看全部」也会带着它们跳转过来。
2886
+ const [depositedOnly, setDepositedOnly] = useState(false); // 只看 autoDream 巩固
2887
+ const [archivedOnly, setArchivedOnly] = useState(false); // 只看已归档
2888
+ const [heatSort, setHeatSort] = useState(false); // 卡片页内热度降序(阶段二补口)
2750
2889
  const [pendingIds, setPendingIds] = useState(() => new Set());
2751
2890
  const [menuOpen, setMenuOpen] = useState(false);
2752
2891
  const [importOpen, setImportOpen] = useState(false);
@@ -2785,7 +2924,9 @@ window.__ModuleLoader__.load({
2785
2924
  ).toISOString();
2786
2925
  const filterQS = (type === "all" ? "" : `&type=${encodeURIComponent(type)}`)
2787
2926
  + (minImp ? `&minImportance=${minImp}` : "")
2788
- + (dateFromIso ? `&updatedFrom=${encodeURIComponent(dateFromIso)}` : "");
2927
+ + (dateFromIso ? `&updatedFrom=${encodeURIComponent(dateFromIso)}` : "")
2928
+ + (depositedOnly ? "&deposited=only" : "")
2929
+ + (archivedOnly ? "&archived=only" : "");
2789
2930
  const listUrl = (offset) => `/api/dsh-mneme/list?limit=${PAGE_SIZE}&offset=${offset}${filterQS}&order=chrono`;
2790
2931
 
2791
2932
  // Browse = paged, pure-chronological pages (order=chrono). The default
@@ -2914,6 +3055,13 @@ window.__ModuleLoader__.load({
2914
3055
  // the grouping follows the sort order instead of re-sorting buckets.
2915
3056
  const sorted = [...visible].sort((a, b) =>
2916
3057
  new Date(b.updated_at || b.created_at || 0) - new Date(a.updated_at || a.created_at || 0));
3058
+ // heatSort(阶段二补口,order=heat 的前端实现):热度是运行时投影、
3059
+ // 无存储序,SQL 排不了;页内对已加载条目降序(非全局序),时间树
3060
+ // 仍走 chrono 不受影响。开关仅在 heat 字段在场上时出现(自门控)。
3061
+ const heatAvailable = visible.some((m) => typeof m.heat === "number");
3062
+ const gridItems = heatSort
3063
+ ? [...sorted].sort((a, b) => (b.heat ?? 0) - (a.heat ?? 0))
3064
+ : sorted;
2917
3065
  const months = [];
2918
3066
  let curMonth = null, curDay = null;
2919
3067
  for (const m of sorted) {
@@ -2971,7 +3119,7 @@ window.__ModuleLoader__.load({
2971
3119
 
2972
3120
  // 编辑保存 / 归档的本地落账:替换或移除对应行;镜像由后端重渲染。
2973
3121
  // 归档从所有视图移除(列表默认只看未归档),并清空选中。
2974
- const applyMemoryUpdate = (updated, { archived = false } = {}) => {
3122
+ const applyMemoryUpdate = (updated, { archived = false, restored = false } = {}) => {
2975
3123
  if (archived) {
2976
3124
  setItems((cur) => cur.filter((m) => m.id !== updated.id));
2977
3125
  setRemoteItems((cur) => (cur ? cur.filter((m) => m.id !== updated.id) : cur));
@@ -2981,6 +3129,17 @@ window.__ModuleLoader__.load({
2981
3129
  setTimeout(() => setToastMsg(""), 2500);
2982
3130
  return;
2983
3131
  }
3132
+ if (restored) {
3133
+ // 恢复:行离开当前视图(抽屉只能从归档浏览列表打开它),回主列表
3134
+ // 由下一次筛选切换/刷新自然带出。
3135
+ setItems((cur) => cur.filter((m) => m.id !== updated.id));
3136
+ setRemoteItems((cur) => (cur ? cur.filter((m) => m.id !== updated.id) : cur));
3137
+ setTotal((n) => Math.max(0, n - 1));
3138
+ setSelectedId(null);
3139
+ setToastMsg(t("memory.explorer.detail.restored"));
3140
+ setTimeout(() => setToastMsg(""), 2500);
3141
+ return;
3142
+ }
2984
3143
  const replace = (m) => (m.id === updated.id ? updated : m);
2985
3144
  setItems((cur) => cur.map(replace));
2986
3145
  setRemoteItems((cur) => (cur ? cur.map(replace) : cur));
@@ -3026,6 +3185,16 @@ window.__ModuleLoader__.load({
3026
3185
  setView("entity");
3027
3186
  };
3028
3187
 
3188
+ // 状态页「查看全部」入口:跳回记忆库并预置对应筛选(清搜索态,避免
3189
+ // remoteItems 盖过浏览列表)。
3190
+ const browseWithFilter = (patch) => {
3191
+ setDepositedOnly(!!patch.deposited);
3192
+ setArchivedOnly(!!patch.archived);
3193
+ setQuery("");
3194
+ setRemoteItems(null);
3195
+ setView("memory");
3196
+ };
3197
+
3029
3198
  const subviews = [
3030
3199
  { key: "memory", label: t("memory.explorer.tabMemory") },
3031
3200
  { key: "entity", label: t("memory.explorer.tabEntities") },
@@ -3144,6 +3313,32 @@ window.__ModuleLoader__.load({
3144
3313
  onClick: () => setMinImp(v)
3145
3314
  }, v === 0 ? t("memory.tab.all") : `★${v}+`))
3146
3315
  ),
3316
+ h("div", { className: "mneme-xcolhead" }, t("memory.explorer.sourceFilter")),
3317
+ h("div", { className: "mneme-xrow" },
3318
+ h("button", {
3319
+ className: depositedOnly ? "mneme-chip mneme-active" : "mneme-chip",
3320
+ title: t("memory.explorer.filterDepositedHint"),
3321
+ onClick: () => setDepositedOnly(!depositedOnly)
3322
+ }, t("memory.explorer.filterDeposited")),
3323
+ h("button", {
3324
+ className: archivedOnly ? "mneme-chip mneme-active" : "mneme-chip",
3325
+ title: t("memory.explorer.filterArchivedHint"),
3326
+ onClick: () => setArchivedOnly(!archivedOnly)
3327
+ }, t("memory.explorer.filterArchived"))
3328
+ ),
3329
+ heatAvailable && h(react.Fragment, null,
3330
+ h("div", { className: "mneme-xcolhead" }, t("memory.explorer.sort")),
3331
+ h("div", { className: "mneme-xrow" },
3332
+ h("button", {
3333
+ className: heatSort ? "mneme-chip mneme-active" : "mneme-chip",
3334
+ title: t("memory.explorer.heatSortHint"),
3335
+ onClick: () => {
3336
+ setHeatSort(!heatSort);
3337
+ if (!heatSort) switchViewMode("cards"); // 排序只作用于卡片网格
3338
+ }
3339
+ }, t("memory.explorer.heatSort"))
3340
+ )
3341
+ ),
3147
3342
  h("div", { className: "mneme-xcolhead" }, t("memory.explorer.types")),
3148
3343
  h("button", {
3149
3344
  className: type === "all" ? "mneme-xtype mneme-active" : "mneme-xtype",
@@ -3168,7 +3363,7 @@ window.__ModuleLoader__.load({
3168
3363
  ? h("div", { className: "mneme-xempty", style: { gridColumn: "1 / -1" } },
3169
3364
  h(Icon, { name: "inbox", size: 20, className: "mneme-xemptyico" }),
3170
3365
  t("memory.explorer.empty"))
3171
- : sorted.map((m) => h("button", {
3366
+ : gridItems.map((m) => h("button", {
3172
3367
  key: m.id,
3173
3368
  type: "button",
3174
3369
  className: m.id === selectedId ? "mneme-card mneme-active" : "mneme-card",
@@ -3184,6 +3379,7 @@ window.__ModuleLoader__.load({
3184
3379
  h("div", { className: "mneme-cardexcerpt" }, m.content || ""),
3185
3380
  h("div", { className: "mneme-cardfoot" },
3186
3381
  h(ImportanceStars, { value: m.importance || 0, size: 12 }),
3382
+ h(HeatBadge, { value: m.heat }),
3187
3383
  m.source && h("span", { className: "mneme-cardsrc", title: m.source }, m.source)
3188
3384
  )
3189
3385
  )),
@@ -3276,7 +3472,7 @@ window.__ModuleLoader__.load({
3276
3472
  })
3277
3473
  ),
3278
3474
  view === "entity" && h(EntityPanel, { t, focusEntity: graphFocus, onJumpMemory: jumpToMemory }),
3279
- view === "status" && h(StatusPanel, { t }),
3475
+ view === "status" && h(StatusPanel, { t, onBrowse: browseWithFilter }),
3280
3476
  view === "settings" && h("div", { className: "mneme-set" },
3281
3477
  h("div", { className: "mneme-set-inner" }, h(SettingsContent, { t }))
3282
3478
  ),
@@ -3369,6 +3565,7 @@ window.__ModuleLoader__.load({
3369
3565
  }
3370
3566
 
3371
3567
  function apply(ctx) {
3568
+ const t = ctx.locale.bind(NS);
3372
3569
  ctx.effect(() => ctx.locale.register(NS, dictionaries), "dsh-mneme: dictionaries");
3373
3570
 
3374
3571
  // 记忆库唯一入口:sidebar.footer.action 注册作为锚点与回退;真实按钮
@@ -3377,7 +3574,6 @@ window.__ModuleLoader__.load({
3377
3574
  // v0.7.15 起不再注册 conversation.view tab——对话内嵌的面板被悬浮
3378
3575
  // 输入框遮挡、挤压会话布局,sheet 页是更舒服的承载方式。
3379
3576
  ctx.slots.inject("sidebar.footer.action", () => {
3380
- const t = ctx.locale.bind(NS);
3381
3577
  return ctx.slots.register({
3382
3578
  name: "sidebar.footer.action",
3383
3579
  id: "dsh-mneme",
@@ -3392,6 +3588,48 @@ window.__ModuleLoader__.load({
3392
3588
  h(MemoryOverlay, { t })
3393
3589
  ));
3394
3590
  });
3591
+
3592
+ // --- better-sidebar 生态 tab(可选软依赖)---
3593
+ // ⚠️ 平台事实(issue #88 实测 + dsh-server-deck 同款方案):模块级
3594
+ // inject 声明 betterSidebar 是硬等待——未安装 bs 的环境整个 entry
3595
+ // pending("1 entry did not activate" → Failed to load plugins),
3596
+ // v0.7.18 即因此对无 bs 用户启动失败。正确双模式(server-deck 验证
3597
+ // 过):外层入口零 inject 立即激活(独立模式保底);tab 注册挂在内层
3598
+ // 动态子插件 `ctx.plugin({ inject: ['betterSidebar'] })`,由 cordis
3599
+ // 原生等待服务——bs 未装时该内层 fiber 永远 INACTIVE,静默无害。
3600
+ // 兄弟上下文取未声明属性会直接抛错("cannot get property without
3601
+ // inject"),轮询探测方案不可用。
3602
+ ctx.effect(() => {
3603
+ let dead = false;
3604
+ try {
3605
+ ctx.plugin?.({
3606
+ name: "dsh-mneme:bs-tab",
3607
+ inject: ["betterSidebar"],
3608
+ apply: (bsCtx) => {
3609
+ bsCtx.effect(() => {
3610
+ if (dead) return;
3611
+ const reg = bsCtx.betterSidebar;
3612
+ if (!reg || typeof reg.registerTab !== "function") return;
3613
+ try {
3614
+ reg.registerTab({
3615
+ id: "dsh-mneme:memory",
3616
+ title: () => t("memory.view.label"),
3617
+ icon: (size) => h(IconArchiveOutline20, { size }),
3618
+ order: 60,
3619
+ component: () => h(MemoryExplorer, { t })
3620
+ });
3621
+ } catch (err) {
3622
+ // id 重复等注册失败不应拖垮其余功能,留一条线索即可
3623
+ console.error("[dsh-mneme] better-sidebar registerTab failed:", err);
3624
+ }
3625
+ }, "dsh-mneme: better-sidebar tab");
3626
+ }
3627
+ });
3628
+ } catch (err) {
3629
+ console.error("[dsh-mneme] better-sidebar mount failed:", err);
3630
+ }
3631
+ return () => { dead = true; };
3632
+ }, "dsh-mneme: better-sidebar mount");
3395
3633
  }
3396
3634
 
3397
3635
  exports.apply = apply;
package/lib/config.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import z from "@deepseek-ai/schemastery";
2
+ import { TYPE_DECAY_DEFAULTS } from "./heat.js";
2
3
 
3
4
  export const Config = z.object({
4
5
  memoryDir: z.string().default("~/.dsh/memory"),
@@ -301,6 +302,24 @@ export const Config = z.object({
301
302
  // reaches any service; a persisted panel_mode="light" (settings kv) counts
302
303
  // as lightMode=true too and wins over the bundle config.
303
304
  lightMode: z.boolean().default(false),
305
+
306
+ // --- heat: v0.7.0 self-evolution (heat + interest drift) ----------------
307
+ // 总开关,默认关(v0.7.12+ 用户已习惯无 heat 行为,默认开=全员行为变更)。
308
+ // 开启后:提供热度字段 / sleep 降级联合判定保护 / 前端热度投影,不改变
309
+ // 召回排序。关闭则跳过所有 heat 计算与热度触达,sleep 降级退回纯时间分层。
310
+ // 也走 feature_flags(FEATURE_FLAG_BOOLEANS 白名单),面板可启停=线上回滚开关。
311
+ heatEnabled: z.boolean().default(false),
312
+ // 幂律形状参数 α(heat = 1/(1+λΔt)^α),越大衰减越快。
313
+ heatGlobalAlpha: z.number().min(0.1).max(5).default(1.2),
314
+ // per-type 衰减因子 λ;λ=0 的类型免疫(热度恒 1.0,sleep 永不降级)。
315
+ // 未知类型走默认 0.002。dict 的键为 type 字符串、值为数字 λ。
316
+ heatTypeDecay: z.dict(z.number(), z.string()).default({ ...TYPE_DECAY_DEFAULTS }),
317
+ // sleep 降级联合判定的热度下限:heat < 该值 且 importance<5 才允许降级。
318
+ sleepHeatThreshold: z.number().min(0).max(1).default(0.05),
319
+ // recordRecall 默认值(recall_runs 记录默认开;显式传 false 的调用方不受影响)。
320
+ recallRecordDefault: z.boolean().default(true),
321
+ // recall_runs 滚动清理保留天数。
322
+ recallRetentionDays: z.natural().min(1).max(3650).default(90),
304
323
  });
305
324
 
306
325
  // Fields forced to false by the light-mode preset. Everything not listed here
@@ -315,7 +334,9 @@ const LIGHT_MODE_OFF = [
315
334
  "hybridInject",
316
335
  "searchSemanticDedup",
317
336
  "selectiveInjectEnabled",
318
- "bm25SearchEnabled"
337
+ "bm25SearchEnabled",
338
+ // 轻量模式不开热计算(heat 属于重型增强;关掉后 sleep 降级也退回纯时间分层)。
339
+ "heatEnabled"
319
340
  ];
320
341
 
321
342
  /**
@@ -18,6 +18,7 @@ import { randomUUID, createHash } from "node:crypto";
18
18
  import { validateDecisions, applyDecisions } from "./decisions.js";
19
19
  import { findPotentialConflicts } from "./clustering.js";
20
20
  import { buildReceipt, withEffortFallback } from "../dream.js";
21
+ import { computeHeat } from "../heat.js";
21
22
 
22
23
  const SUMMARY_MAX = 120;
23
24
  // Conflict similarity threshold per strictness level (v0.4.0):
@@ -242,10 +243,22 @@ function phaseDemotion(service, config, logger, runId, signal = null) {
242
243
  for (const m of service.all()) {
243
244
  if (signal?.aborted) break;
244
245
  if (m.archived || m.forgotten) continue;
245
- const ref = m.last_accessed_at ?? m.updated_at ?? m.created_at;
246
+ const ref = m.last_accessed_at ?? m.created_at;
246
247
  if (!ref) continue;
247
248
  const t = new Date(ref).getTime();
248
249
  if (Number.isNaN(t)) continue;
250
+ // v0.7.0 热联合判定(仅 heatEnabled 时启用;默认关则退回纯时间分层,
251
+ // 与 v0.7.12 行为一致):时间窗之外再加两道保护闸——热度低于
252
+ // sleepHeatThreshold 且 importance<5 才允许降级。λ=0 的免疫类型 heat 恒
253
+ // 1.0 天然豁免(preference/pattern/summary 永不因 sleep 降级);importance
254
+ // ≥5 的紧要记忆无论多冷都保留。`冷但重要` 与 `热但低值` 均不满足条件。
255
+ const heatOn = config.heatEnabled !== false;
256
+ if (heatOn) {
257
+ const heat = computeHeat(m, Date.now(), config);
258
+ const heatProtected = heat >= (config.sleepHeatThreshold ?? 0.05);
259
+ const important = (m.importance ?? 0) >= 5;
260
+ if (heatProtected || important) continue;
261
+ }
249
262
  if (t < compressCut) {
250
263
  service.setArchived(m.id, true);
251
264
  archived.push(m.id);