@modusensus/dsh-mneme 0.8.6 → 0.8.7
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/README.md +18 -148
- package/bin/cli.mjs +689 -603
- package/lib/api-standalone.js +57 -1
- package/lib/api.js +34 -11
- package/lib/client.js +289 -59
- package/lib/config.js +54 -6
- package/lib/content-hash.js +54 -0
- package/lib/document-index.js +94 -0
- package/lib/document.js +93 -11
- package/lib/index.js +50 -11
- package/lib/inject.js +6 -2
- package/lib/lang.js +15 -4
- package/lib/maintenance.js +142 -0
- package/lib/mirror.js +147 -15
- package/lib/recall-stats.js +52 -2
- package/lib/scope.js +34 -6
- package/lib/service.js +86 -7
- package/lib/settings.js +2 -1
- package/lib/store.js +231 -21
- package/lib/tools.js +27 -9
- package/lib/write-admission.js +259 -0
- package/package.json +4 -4
- package/src/api-standalone.js +57 -1
- package/src/api.js +34 -11
- package/src/config.js +54 -6
- package/src/content-hash.js +54 -0
- package/src/document-index.js +94 -0
- package/src/document.js +93 -11
- package/src/index.js +50 -11
- package/src/inject.js +6 -2
- package/src/lang.js +15 -4
- package/src/maintenance.js +142 -0
- package/src/mirror.js +147 -15
- package/src/recall-stats.js +52 -2
- package/src/scope.js +34 -6
- package/src/service.js +86 -7
- package/src/settings.js +2 -1
- package/src/store.js +231 -21
- package/src/tools.js +27 -9
- package/src/write-admission.js +259 -0
- package/test/api.test.js +12 -0
- package/test/client.test.js +90 -5
- package/test/content-hash.test.js +204 -0
- package/test/document-dir.test.js +233 -0
- package/test/document.test.js +151 -3
- package/test/inject-parent-gate.test.js +106 -0
- package/test/inject-pools.test.js +8 -3
- package/test/llm-audit.test.js +42 -0
- package/test/maintenance.test.js +391 -0
- package/test/mirror-types.test.js +208 -0
- package/test/recall-evals.test.js +18 -0
- package/test/recall-stats.test.js +66 -0
- package/test/write-admission.test.js +345 -0
package/lib/client.js
CHANGED
|
@@ -26,7 +26,61 @@ window.__ModuleLoader__.load({
|
|
|
26
26
|
liveRegion.textContent = "";
|
|
27
27
|
window.requestAnimationFrame(() => { if (liveRegion) liveRegion.textContent = text; });
|
|
28
28
|
}
|
|
29
|
-
|
|
29
|
+
|
|
30
|
+
// #177 词级 diff:经典 LCS 对齐——删除片段标红、新增标绿、公共部分原样。
|
|
31
|
+
// 分词两段式:Han 段先切出再逐字打散(\p{L} 把整句中文当一个词,必须单列),
|
|
32
|
+
// 其余按 Unicode 词边界(拉丁按词、标点独立)。相邻同侧片段合并成 run。
|
|
33
|
+
// 输入先截 800 字符;O(n·m) DP 仅在小文本启用(任一侧 > 800 词或 edit run
|
|
34
|
+
// > 200 时返回 null),caller 退回原文展示,不为极端文本烧内存。
|
|
35
|
+
function wordDiff(aText, bText) {
|
|
36
|
+
const HAN = /^[\u{3400}-\u{4DBF}\u{4E00}-\u{9FFF}\u{F900}-\u{FAFF}]+$/u;
|
|
37
|
+
const tokenize = (s) => (s || "")
|
|
38
|
+
.split(/([\u{3400}-\u{4DBF}\u{4E00}-\u{9FFF}\u{F900}-\u{FAFF}]+)/u)
|
|
39
|
+
.filter(Boolean)
|
|
40
|
+
.flatMap((seg) => HAN.test(seg)
|
|
41
|
+
? [...seg]
|
|
42
|
+
: seg.split(/(?=[^\p{L}\p{N}])|(?<=[^\p{L}\p{N}])/u).filter(Boolean));
|
|
43
|
+
const a = tokenize((aText || "").slice(0, 800)), b = tokenize((bText || "").slice(0, 800));
|
|
44
|
+
const n = a.length, m = b.length;
|
|
45
|
+
if (n === 0 && m === 0) return null;
|
|
46
|
+
if (n > 800 || m > 800) return null; // ponytail: O(n·m) DP,800×800 封顶(~2.5MB Int32),超限退回原文
|
|
47
|
+
const dp = new Int32Array((n + 1) * (m + 1));
|
|
48
|
+
const at = (i, j) => i * (m + 1) + j;
|
|
49
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
50
|
+
for (let j = m - 1; j >= 0; j--) {
|
|
51
|
+
dp[at(i, j)] = a[i] === b[j] ? dp[at(i + 1, j + 1)] + 1 : Math.max(dp[at(i + 1, j)], dp[at(i, j + 1)]);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const ops = [];
|
|
55
|
+
const push = (kind, text) => {
|
|
56
|
+
const last = ops[ops.length - 1];
|
|
57
|
+
if (last && last.kind === kind) last.text += text;
|
|
58
|
+
else ops.push({ kind, text });
|
|
59
|
+
};
|
|
60
|
+
let i = 0, j = 0;
|
|
61
|
+
while (i < n && j < m) {
|
|
62
|
+
if (a[i] === b[j]) { push("same", a[i]); i++; j++; }
|
|
63
|
+
else if (dp[at(i + 1, j)] >= dp[at(i, j + 1)]) { push("del", a[i]); i++; }
|
|
64
|
+
else { push("ins", b[j]); j++; }
|
|
65
|
+
}
|
|
66
|
+
while (i < n) { push("del", a[i]); i++; }
|
|
67
|
+
while (j < m) { push("ins", b[j]); j++; }
|
|
68
|
+
if (ops.filter((o) => o.kind !== "same").length > 200) return null;
|
|
69
|
+
return ops;
|
|
70
|
+
}
|
|
71
|
+
// #287 归档图标跨代兼容:primitives 在 0.1.7 把图标命名从「像素后缀」改成
|
|
72
|
+
// 「字重后缀」(IconArchiveOutline20 → …OutlineRegular / …OutlineMedium),
|
|
73
|
+
// 旧名不留别名,而 peerDependencies 同时覆盖 0.1.6 与 0.1.7 两代宿主——
|
|
74
|
+
// 只认某一代的名字,另一代就会取到 undefined,h(undefined) 即 React #130
|
|
75
|
+
// (整个 slot entry 崩掉)。故按「旧名 → 新名」顺序取第一个存在的;
|
|
76
|
+
// 两代都缺时降级为不渲染图标,宁可无图标也不把 slot 打崩。
|
|
77
|
+
const IconArchive = primitives.IconArchiveOutline20
|
|
78
|
+
?? primitives.IconArchiveOutlineRegular
|
|
79
|
+
?? primitives.IconArchiveOutlineMedium
|
|
80
|
+
?? null;
|
|
81
|
+
|
|
82
|
+
/** 渲染归档图标;宿主未提供任一候选名时返回 null(无图标,不影响其余内容)。 */
|
|
83
|
+
const renderArchiveIcon = (props) => (IconArchive ? h(IconArchive, props) : null);
|
|
30
84
|
|
|
31
85
|
// Portal target for the hero fallback surface. The host whitelists
|
|
32
86
|
// react-dom for its own bundles (dsh-client-ui-trajectory requires it);
|
|
@@ -327,6 +381,7 @@ window.__ModuleLoader__.load({
|
|
|
327
381
|
"memory.features.autoInject.hint": "每轮对话自动携带相关记忆",
|
|
328
382
|
"memory.features.injectGuidanceEnabled": "能力说明",
|
|
329
383
|
"memory.features.injectGuidanceEnabled.hint": "在工具描述与一次性提示段里说明怎么用记忆(何时查、何时写、拿不准就不做)",
|
|
384
|
+
"memory.features.parentOff": "父开关关闭时不生效",
|
|
330
385
|
"memory.features.autoSummarize": "自动总结",
|
|
331
386
|
"memory.features.autoSummarize.hint": "对话结束自动提炼记忆条目",
|
|
332
387
|
"memory.features.hotMemoryEnabled": "热记忆",
|
|
@@ -491,6 +546,14 @@ window.__ModuleLoader__.load({
|
|
|
491
546
|
"memory.status.conflictQueue.resolved": "已裁决:保留{name}",
|
|
492
547
|
"memory.status.conflictQueue.refresh": "刷新队列",
|
|
493
548
|
"memory.status.conflictQueue.refreshed": "冲突队列已刷新",
|
|
549
|
+
"memory.status.conflictQueue.frozenBadge": "⏸ 冻结中",
|
|
550
|
+
"memory.status.conflictQueue.frozenTitle": "冻结中:不参与注入与巩固,等你裁决",
|
|
551
|
+
"memory.status.conflictQueue.similarity": "相似度",
|
|
552
|
+
"memory.status.conflictQueue.diffTitle": "差异已高亮:红=删除、绿=新增",
|
|
553
|
+
"memory.status.conflictQueue.diffFallback": "两段文本差异较大,请对照原文阅读",
|
|
554
|
+
"memory.status.conflictQueue.emptyTitle": "暂无待确认冲突",
|
|
555
|
+
"memory.status.conflictQueue.emptyBody": "当巩固发现两条互相矛盾或高度相似的记忆时,会先冻结在这里等你裁决,不会自动修改。裁决前双方都不参与注入。",
|
|
556
|
+
"memory.status.conflictQueue.applyHintTitle": "确认后:保留方正文追加已否决注记,另一方归档",
|
|
494
557
|
"memory.status.workbench": "工作动态",
|
|
495
558
|
"memory.status.dreamConsolidate": "记忆巩固",
|
|
496
559
|
"memory.status.summarize": "总结提炼",
|
|
@@ -508,6 +571,7 @@ window.__ModuleLoader__.load({
|
|
|
508
571
|
"memory.status.recallStats": "记忆复用",
|
|
509
572
|
"memory.status.recallHint": "复用率 {rate} · 僵尸 {zombie}/{active}(豁免 {exempt})· 30 天回执 {runs} 次 · Top:{top}",
|
|
510
573
|
"memory.status.recallInject": "注入 {runs} 轮 {count} 条,槽位填充 {fill}%",
|
|
574
|
+
"memory.status.recallArchive": "归档 {total} 行({add}/天),精确重复可压掉 {compress} 行",
|
|
511
575
|
"memory.status.viewAll": "查看全部",
|
|
512
576
|
"memory.status.depositedCount": "沉淀的记忆({n})",
|
|
513
577
|
"memory.status.archivedCount": "已归档的记忆({n})",
|
|
@@ -705,6 +769,7 @@ window.__ModuleLoader__.load({
|
|
|
705
769
|
"memory.features.autoInject.hint": "Carry relevant memories into every turn",
|
|
706
770
|
"memory.features.injectGuidanceEnabled": "Capability guide",
|
|
707
771
|
"memory.features.injectGuidanceEnabled.hint": "Explain how to use memory (when to search, when to save, when to do nothing) in tool descriptions plus a one-time prompt section",
|
|
772
|
+
"memory.features.parentOff": "Inactive while auto injection is off",
|
|
708
773
|
"memory.features.autoSummarize": "Auto summarization",
|
|
709
774
|
"memory.features.autoSummarize.hint": "Distill memory entries when a conversation ends",
|
|
710
775
|
"memory.features.hotMemoryEnabled": "Hot memory",
|
|
@@ -869,6 +934,14 @@ window.__ModuleLoader__.load({
|
|
|
869
934
|
"memory.status.conflictQueue.resolved": "Resolved: kept {name}",
|
|
870
935
|
"memory.status.conflictQueue.refresh": "Refresh queue",
|
|
871
936
|
"memory.status.conflictQueue.refreshed": "Conflict queue refreshed",
|
|
937
|
+
"memory.status.conflictQueue.frozenBadge": "⏸ Frozen",
|
|
938
|
+
"memory.status.conflictQueue.frozenTitle": "Frozen: excluded from injection and consolidation, awaiting your review",
|
|
939
|
+
"memory.status.conflictQueue.similarity": "similarity",
|
|
940
|
+
"memory.status.conflictQueue.diffTitle": "Differences highlighted: red = removed, green = added",
|
|
941
|
+
"memory.status.conflictQueue.diffFallback": "The two texts differ substantially — compare the originals",
|
|
942
|
+
"memory.status.conflictQueue.emptyTitle": "No pending conflicts",
|
|
943
|
+
"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.",
|
|
944
|
+
"memory.status.conflictQueue.applyHintTitle": "On confirm: a veto note is appended to the kept side and the other side is archived",
|
|
872
945
|
"memory.status.workbench": "Activity",
|
|
873
946
|
"memory.status.dreamConsolidate": "Consolidation",
|
|
874
947
|
"memory.status.summarize": "Summarization",
|
|
@@ -886,6 +959,7 @@ window.__ModuleLoader__.load({
|
|
|
886
959
|
"memory.status.recallStats": "Recall reuse",
|
|
887
960
|
"memory.status.recallHint": "reuse {rate} · zombie {zombie}/{active} (exempt {exempt}) · {runs} runs in 30d · top: {top}",
|
|
888
961
|
"memory.status.recallInject": "injected {runs} turns · {count} items ({fill}% slots)",
|
|
962
|
+
"memory.status.recallArchive": "archived {total} rows (+{add}/day) · {compress} exactly-duplicate rows compressible",
|
|
889
963
|
"memory.status.viewAll": "View all",
|
|
890
964
|
"memory.status.depositedCount": "Deposited memories ({n})",
|
|
891
965
|
"memory.status.archivedCount": "Archived memories ({n})",
|
|
@@ -1148,7 +1222,7 @@ window.__ModuleLoader__.load({
|
|
|
1148
1222
|
".mneme-heat--cold{color:var(--dsw-alias-label-tertiary)}",
|
|
1149
1223
|
".mneme-heat--cold .mneme-heatpct{display:none}",
|
|
1150
1224
|
// 纵向 flex 容器按原生按钮的宽度与外边距分配空间,保留展开和收起态的对齐。
|
|
1151
|
-
".mneme-topentry{display:flex;flex-direction:column}",
|
|
1225
|
+
".mneme-topentry{display:flex;flex-direction:column;position:relative}",
|
|
1152
1226
|
".mneme-topentry-native{background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary)}",
|
|
1153
1227
|
".mneme-topentry-native:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}",
|
|
1154
1228
|
".mneme-topentry-native .mneme-topentry-label{white-space:nowrap}",
|
|
@@ -1213,6 +1287,31 @@ window.__ModuleLoader__.load({
|
|
|
1213
1287
|
".mneme-conflict-missing{font-size:12px;color:var(--dsw-alias-label-tertiary)}",
|
|
1214
1288
|
".mneme-conflict-actions{display:flex;align-items:center;gap:6px;flex-wrap:wrap}",
|
|
1215
1289
|
".mneme-conflict-hint{font-size:11px;color:var(--dsw-alias-label-tertiary);margin-right:auto}",
|
|
1290
|
+
// #177 冲突队列视觉批次:A/B 侧色、相似度条、词级 diff、主色按钮、空态
|
|
1291
|
+
".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)}",
|
|
1292
|
+
".mneme-conflict-side--a .mneme-conflict-sidelabel{color:var(--dsw-alias-state-business-primary);font-weight:600}",
|
|
1293
|
+
".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)}",
|
|
1294
|
+
".mneme-conflict-side--b .mneme-conflict-sidelabel{color:var(--dsw-alias-state-warning,#e6a23c);font-weight:600}",
|
|
1295
|
+
".mneme-conflict-simrow{display:flex;align-items:center;gap:8px;font-size:11px;color:var(--dsw-alias-label-secondary)}",
|
|
1296
|
+
".mneme-conflict-simbar{position:relative;flex:1;height:5px;border-radius:3px;background:var(--dsw-alias-interactive-bg-hover);overflow:hidden}",
|
|
1297
|
+
".mneme-conflict-simfill{position:absolute;top:0;bottom:0;left:0;border-radius:3px;background:var(--dsw-alias-state-business-primary)}",
|
|
1298
|
+
".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}",
|
|
1299
|
+
".mneme-conflict-mark{text-decoration:none;border-radius:3px;padding:0 1px}",
|
|
1300
|
+
".mneme-conflict-mark--del{background:color-mix(in srgb,var(--dsw-alias-state-error,#c33) 16%,transparent);text-decoration:line-through}",
|
|
1301
|
+
".mneme-conflict-mark--ins{background:color-mix(in srgb,var(--dsw-alias-state-success,#3c9) 18%,transparent)}",
|
|
1302
|
+
".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}",
|
|
1303
|
+
".mneme-conflict-primary:disabled{opacity:.55;cursor:default}",
|
|
1304
|
+
".mneme-conflict-primary:hover:not(:disabled){background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 85%,black)}",
|
|
1305
|
+
".mneme-badge--frozen{color:var(--dsw-alias-state-warning,#e6a23c);background:color-mix(in srgb,var(--dsw-alias-state-warning,#e6a23c) 12%,transparent)}",
|
|
1306
|
+
".mneme-conflict-empty{border:1px dashed var(--dsw-alias-border-l2);border-radius:10px;padding:12px 14px;display:flex;flex-direction:column;gap:4px}",
|
|
1307
|
+
".mneme-conflict-empty-title{font-size:12.5px;font-weight:600;color:var(--dsw-alias-label-secondary)}",
|
|
1308
|
+
".mneme-conflict-empty-body{font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}",
|
|
1309
|
+
".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)}",
|
|
1310
|
+
".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)}",
|
|
1311
|
+
".mneme-triggerwrap{position:relative;pointer-events:none}",
|
|
1312
|
+
".mneme-triggerwrap .mneme-trigger{pointer-events:auto}",
|
|
1313
|
+
".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)}",
|
|
1314
|
+
".mneme-conflict-jump:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px;border-radius:12px}",
|
|
1216
1315
|
// 详情抽屉:sheet 内右侧滑出,覆盖在浏览区之上
|
|
1217
1316
|
".mneme-xmain{position:relative}",
|
|
1218
1317
|
"@keyframes mneme-slidein{from{opacity:0;transform:translateX(16px)}to{opacity:1;transform:translateX(0)}}",
|
|
@@ -1827,8 +1926,14 @@ window.__ModuleLoader__.load({
|
|
|
1827
1926
|
// 分组排版:核心/增强/巩固常驻,注入策略等收进「高级」折叠,普通用户
|
|
1828
1927
|
// 不被专业项淹没。429 调速器参数、distillMaxChars、codingBoostFactor
|
|
1829
1928
|
// 属调优噪音,按对齐结论留在配置文件,不上 UI。
|
|
1929
|
+
// #249 第二批:注入形态的父/子开关(父 = autoInject)。子项紧跟父行、缩进
|
|
1930
|
+
// 显示;父关时子项加一句「不生效」,但开关仍可点——父关是「不生效」而不是
|
|
1931
|
+
// 「重置用户配置」,子项自己勾着的值要留着,也应该能提前设好。同一份关系在
|
|
1932
|
+
// 后端 src/config.js 的 INJECT_CHILD_FLAGS(运行时闸门),两侧漂移由
|
|
1933
|
+
// test/inject-parent-gate.test.js 钉住。
|
|
1934
|
+
const FEATURE_CHILDREN = { autoInject: ["injectGuidanceEnabled"] };
|
|
1830
1935
|
const FEATURE_GROUPS = [
|
|
1831
|
-
{ key: "group.core", items: ["autoInject", "
|
|
1936
|
+
{ key: "group.core", items: ["autoInject", "autoSummarize", "hotMemoryEnabled", "memoryQualityFilter.enabled", "llmAudit.enabled"] },
|
|
1832
1937
|
{ key: "group.enhance", items: ["entityExtractionEnabled", "codingRetrospect", "rerankEnabled", "resilientModelDownload", "searchSemanticDedup", "bm25SearchEnabled", "heatEnabled", "documentMemoryEnabled"] },
|
|
1833
1938
|
{ key: "group.dream", items: ["autoDream", "sleepModeEnabled"] },
|
|
1834
1939
|
// v0.8.0 A4(issue #17):作用域隔离组——标注总开关 + 严格硬过滤。
|
|
@@ -1842,8 +1947,8 @@ window.__ModuleLoader__.load({
|
|
|
1842
1947
|
// 实体抽取思考强度(issue #109):与后端 FEATURE_FLAG_ENUMS 枚举对齐。
|
|
1843
1948
|
const ENTITY_REASONING = ["none", "low", "medium", "high"];
|
|
1844
1949
|
|
|
1845
|
-
function FeatureRow({ name, hint, on, disabled, onToggle }) {
|
|
1846
|
-
return h("div", { className: "mneme-featrow" },
|
|
1950
|
+
function FeatureRow({ name, hint, on, disabled, onToggle, sub }) {
|
|
1951
|
+
return h("div", { className: "mneme-featrow", style: sub ? { paddingLeft: 18, opacity: 0.86 } : undefined },
|
|
1847
1952
|
h("div", { className: "mneme-featmain" },
|
|
1848
1953
|
h("div", { className: "mneme-featname" }, name),
|
|
1849
1954
|
h("div", { className: "mneme-feathint" }, hint)
|
|
@@ -1934,15 +2039,24 @@ window.__ModuleLoader__.load({
|
|
|
1934
2039
|
put({ [key]: raw });
|
|
1935
2040
|
};
|
|
1936
2041
|
|
|
1937
|
-
const boolRow = (k) => h(FeatureRow, {
|
|
2042
|
+
const boolRow = (k, opts = {}) => h(FeatureRow, {
|
|
1938
2043
|
key: k,
|
|
1939
2044
|
name: t(`memory.features.${k}`),
|
|
1940
|
-
hint: t(`memory.features.${k}.hint`)
|
|
2045
|
+
hint: t(`memory.features.${k}.hint`)
|
|
2046
|
+
+ (opts.gate && !eff[opts.gate] ? ` · ${t("memory.features.parentOff")}` : ""),
|
|
1941
2047
|
on: !!eff[k],
|
|
1942
2048
|
disabled: busy,
|
|
2049
|
+
sub: !!opts.sub,
|
|
1943
2050
|
onToggle: () => put({ [k]: !eff[k] })
|
|
1944
2051
|
});
|
|
1945
2052
|
|
|
2053
|
+
// 带子项的父开关:父行 + 紧随其后的子行(子行缩进,父关时标注不生效)。
|
|
2054
|
+
const flagRow = (k) => {
|
|
2055
|
+
const kids = FEATURE_CHILDREN[k];
|
|
2056
|
+
if (!kids) return boolRow(k);
|
|
2057
|
+
return h(react.Fragment, { key: k }, boolRow(k), kids.map((c) => boolRow(c, { sub: true, gate: k })));
|
|
2058
|
+
};
|
|
2059
|
+
|
|
1946
2060
|
// 模型枚举项的统一形状:string 或 {id, name?}(/llm-providers 契约)。
|
|
1947
2061
|
const modelLabel = (m) => (typeof m === "string" ? m : String((m && (m.name || m.id)) ?? ""));
|
|
1948
2062
|
const modelValue = (m) => (typeof m === "string" ? m : String((m && m.id) ?? ""));
|
|
@@ -2133,7 +2247,7 @@ window.__ModuleLoader__.load({
|
|
|
2133
2247
|
: h(react.Fragment, null,
|
|
2134
2248
|
FEATURE_GROUPS.map((g) => h(react.Fragment, { key: g.key },
|
|
2135
2249
|
h("div", { className: "mneme-featgroup" }, t(`memory.features.${g.key}`)),
|
|
2136
|
-
g.items.map(
|
|
2250
|
+
g.items.map(flagRow),
|
|
2137
2251
|
g.key === "group.enhance" && h(react.Fragment, null, embedSub, entitySub),
|
|
2138
2252
|
g.key === "group.dream" && h(react.Fragment, null, dreamSub, sleepSub)
|
|
2139
2253
|
)),
|
|
@@ -2147,7 +2261,7 @@ window.__ModuleLoader__.load({
|
|
|
2147
2261
|
h(Icon, { name: showAdv ? "chevronDown" : "chevronRight", size: 12 }),
|
|
2148
2262
|
t("memory.features.advancedToggle"))
|
|
2149
2263
|
),
|
|
2150
|
-
showAdv && FEATURE_ADVANCED_BOOLS.map(boolRow)
|
|
2264
|
+
showAdv && FEATURE_ADVANCED_BOOLS.map((k) => boolRow(k))
|
|
2151
2265
|
)
|
|
2152
2266
|
);
|
|
2153
2267
|
}
|
|
@@ -2257,7 +2371,7 @@ window.__ModuleLoader__.load({
|
|
|
2257
2371
|
// 版本/平台方便归档,正文同款模板;纯链接零后端成本。
|
|
2258
2372
|
const feedbackEnv = `**插件版本**: ${pkgVersion}\n**平台**: ${platformLabel()}\n`;
|
|
2259
2373
|
const feedbackBody = feedbackEnv + "**问题描述**:\n- 期望行为:\n- 实际行为:\n- 复现步骤:\n";
|
|
2260
|
-
const issueHref = "https://github.com/slow-stack/
|
|
2374
|
+
const issueHref = "https://github.com/slow-stack/mneme/issues/new?title="
|
|
2261
2375
|
+ encodeURIComponent("[dsh-mneme] 问题反馈")
|
|
2262
2376
|
+ "&body=" + encodeURIComponent(feedbackBody);
|
|
2263
2377
|
const mailHref = "mailto:work@modusensus.space?subject="
|
|
@@ -2690,7 +2804,7 @@ window.__ModuleLoader__.load({
|
|
|
2690
2804
|
h("div", { style: { display: "flex", flexDirection: "column", gap: 2 } },
|
|
2691
2805
|
h("a", { className: "mneme-feed-link", href: issueHref, target: "_blank", rel: "noopener noreferrer" }, t("memory.settings.feedback.newIssue")),
|
|
2692
2806
|
h("a", { className: "mneme-feed-link", href: mailHref }, t("memory.settings.feedback.email")),
|
|
2693
|
-
h("a", { className: "mneme-feed-link", href: "https://github.com/slow-stack/
|
|
2807
|
+
h("a", { className: "mneme-feed-link", href: "https://github.com/slow-stack/mneme/issues", target: "_blank", rel: "noopener noreferrer" }, t("memory.settings.feedback.browse"))
|
|
2694
2808
|
),
|
|
2695
2809
|
h("div", { className: "mneme-featsubhint", style: { marginTop: 8 } }, t("memory.settings.feedback.hint"))
|
|
2696
2810
|
)
|
|
@@ -2772,7 +2886,7 @@ window.__ModuleLoader__.load({
|
|
|
2772
2886
|
h("div", { className: "mneme-overlay", role: "region", "aria-label": t("memory.view.label"), ref: panelRef },
|
|
2773
2887
|
h("div", { className: "mneme-overlaybar" },
|
|
2774
2888
|
h("span", { className: "mneme-overlaytitle" },
|
|
2775
|
-
|
|
2889
|
+
renderArchiveIcon({ size: 15 }),
|
|
2776
2890
|
t("memory.view.label")
|
|
2777
2891
|
),
|
|
2778
2892
|
h("button", {
|
|
@@ -3084,19 +3198,25 @@ window.__ModuleLoader__.load({
|
|
|
3084
3198
|
const [state, setState] = useState({ loading: true, error: false, lastRun: null, pending: 0 });
|
|
3085
3199
|
useEffect(() => {
|
|
3086
3200
|
let cancelled = false;
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3201
|
+
const loadOnce = () => {
|
|
3202
|
+
apiFetch("/api/dsh-mneme/dream-status")
|
|
3203
|
+
.then((res) => { if (!res.ok) throw new Error("http"); return res.json(); })
|
|
3204
|
+
.then((d) => {
|
|
3205
|
+
if (cancelled) return;
|
|
3206
|
+
setState({
|
|
3207
|
+
loading: false,
|
|
3208
|
+
error: false,
|
|
3209
|
+
lastRun: (d && d.lastRun) || null,
|
|
3210
|
+
pending: Number((d && d.pendingConflicts) ?? 0)
|
|
3211
|
+
});
|
|
3212
|
+
})
|
|
3213
|
+
.catch(() => { if (!cancelled) setState({ loading: false, error: true, lastRun: null, pending: 0 }); });
|
|
3214
|
+
};
|
|
3215
|
+
loadOnce();
|
|
3216
|
+
// #295 评审:裁决完成后队列会广播 mneme:conflicts-changed,这里重取
|
|
3217
|
+
// 计数,状态卡的「待确认冲突」不再停留旧值。
|
|
3218
|
+
window.addEventListener("mneme:conflicts-changed", loadOnce);
|
|
3219
|
+
return () => { cancelled = true; window.removeEventListener("mneme:conflicts-changed", loadOnce); };
|
|
3100
3220
|
}, []);
|
|
3101
3221
|
const run = state.lastRun;
|
|
3102
3222
|
const num = run ? formatRelativeTime(run.created_at, t) : t("memory.status.dreamNever");
|
|
@@ -3113,6 +3233,7 @@ window.__ModuleLoader__.load({
|
|
|
3113
3233
|
role: pending ? "button" : undefined,
|
|
3114
3234
|
tabIndex: pending ? 0 : undefined,
|
|
3115
3235
|
"aria-label": pending ? t("memory.status.conflictQueue.goto") : undefined,
|
|
3236
|
+
className: pending ? "mneme-conflict-jump" : undefined,
|
|
3116
3237
|
style: pending ? { cursor: "pointer" } : undefined,
|
|
3117
3238
|
onClick: pending ? gotoQueue : undefined,
|
|
3118
3239
|
onKeyDown: pending ? (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); gotoQueue(); } } : undefined
|
|
@@ -3205,15 +3326,19 @@ window.__ModuleLoader__.load({
|
|
|
3205
3326
|
// 队列为空时整块不渲染(不打扰无冲突实例)。
|
|
3206
3327
|
function ConflictsQueue({ t }) {
|
|
3207
3328
|
const [items, setItems] = useState(null);
|
|
3329
|
+
const [loadError, setLoadError] = useState(false);
|
|
3208
3330
|
const [busy, setBusy] = useState(false);
|
|
3209
|
-
const load = () => {
|
|
3331
|
+
const load = (silent) => {
|
|
3210
3332
|
apiFetch("/api/dsh-mneme/conflicts")
|
|
3211
3333
|
.then((res) => { if (!res.ok) throw new Error("http"); return res.json(); })
|
|
3212
|
-
.then((d) => { setItems(d.items || []); announce(t("memory.status.conflictQueue.refreshed")); })
|
|
3213
|
-
|
|
3334
|
+
.then((d) => { setItems(d.items || []); setLoadError(false); if (!silent) announce(t("memory.status.conflictQueue.refreshed")); })
|
|
3335
|
+
// #295 评审:加载失败 ≠ 没有冲突——错误态渲染错误卡而不是空态教育卡,
|
|
3336
|
+
// 不能让 500 把「暂时看不到」伪装成「没有冲突」。
|
|
3337
|
+
.catch(() => { setItems([]); setLoadError(true); });
|
|
3214
3338
|
};
|
|
3215
3339
|
useEffect(() => { load(); }, []);
|
|
3216
|
-
|
|
3340
|
+
// #177:空队列不再整块消失——空态教育卡要渲染(原 return null 已移除)。
|
|
3341
|
+
if (!items) return null;
|
|
3217
3342
|
const resolve = (id, winner) => {
|
|
3218
3343
|
if (busy) return;
|
|
3219
3344
|
setBusy(true);
|
|
@@ -3228,37 +3353,93 @@ window.__ModuleLoader__.load({
|
|
|
3228
3353
|
.then(() => {
|
|
3229
3354
|
announce(t("memory.status.conflictQueue.resolved").replace("{name}",
|
|
3230
3355
|
(winner === "a" ? (it.memory_a && it.memory_a.title) : winner === "b" ? (it.memory_b && it.memory_b.title) : "") || ""));
|
|
3231
|
-
|
|
3356
|
+
// #295 评审:裁决改变了 pending 总数——DreamStatusCards 只在挂载时
|
|
3357
|
+
// 读一次 dream-status,不广播的话状态卡停留在旧计数。同页两个组件
|
|
3358
|
+
// 用一个 window 事件对齐(比提升状态到 StatusPanel 少动三处)。
|
|
3359
|
+
try { window.dispatchEvent(new CustomEvent("mneme:conflicts-changed")); } catch { /* 非浏览器环境 */ }
|
|
3360
|
+
load(true);
|
|
3232
3361
|
})
|
|
3233
3362
|
.catch(() => {})
|
|
3234
3363
|
.finally(() => setBusy(false));
|
|
3235
3364
|
};
|
|
3236
|
-
const side = (s, label) => h("div", { className:
|
|
3365
|
+
const side = (s, label, sideClass, ops, diffSide) => h("div", { className: `mneme-conflict-side ${sideClass}` },
|
|
3237
3366
|
h("div", { className: "mneme-conflict-sidelabel" }, label),
|
|
3238
3367
|
s.missing
|
|
3239
3368
|
? h("div", { className: "mneme-conflict-missing" }, t("memory.status.conflictQueue.missing"))
|
|
3240
3369
|
: h(react.Fragment, null,
|
|
3241
3370
|
h("div", { className: "mneme-conflict-sidetitle", title: s.title }, s.title || "…"),
|
|
3242
|
-
|
|
3243
|
-
|
|
3371
|
+
// #177:词级 diff 可用(LCS 无损对齐成功)时用高亮视图替代纯文本。
|
|
3372
|
+
// 侧别过滤(#295 评审修正):A 侧渲染 same+del(它被删的部分高亮),
|
|
3373
|
+
// B 侧渲染 same+ins(它新增的部分高亮)——每列忠实于自己的原文。
|
|
3374
|
+
// title 提示颜色语义;diff 不可用时保留原 snippet。
|
|
3375
|
+
ops
|
|
3376
|
+
? h("div", { className: "mneme-conflict-diff", title: t("memory.status.conflictQueue.diffTitle") },
|
|
3377
|
+
ops.map((o, k) => {
|
|
3378
|
+
if (o.kind === "same") return o.text;
|
|
3379
|
+
if (o.kind === "del" && diffSide === "a") {
|
|
3380
|
+
return h("span", { key: k, className: "mneme-conflict-mark mneme-conflict-mark--del" }, o.text);
|
|
3381
|
+
}
|
|
3382
|
+
if (o.kind === "ins" && diffSide === "b") {
|
|
3383
|
+
return h("span", { key: k, className: "mneme-conflict-mark mneme-conflict-mark--ins" }, o.text);
|
|
3384
|
+
}
|
|
3385
|
+
return null; // 对侧的编辑片段不出现在本列
|
|
3386
|
+
}))
|
|
3387
|
+
: h("div", { className: "mneme-conflict-snippet" }, (s.content || "").slice(0, 140)),
|
|
3388
|
+
// #177:预裁决阶段两侧都还活着——「已归档」徽章换成「冻结中」,
|
|
3389
|
+
// 原注销记说明(applyHint)挪进 tooltip,不再整段占一行动态区。
|
|
3390
|
+
h("span", {
|
|
3391
|
+
className: "mneme-badge mneme-badge--frozen",
|
|
3392
|
+
title: `${t("memory.status.conflictQueue.frozenTitle")} ${t("memory.status.conflictQueue.applyHintTitle")}`
|
|
3393
|
+
}, t("memory.status.conflictQueue.frozenBadge")))
|
|
3244
3394
|
);
|
|
3395
|
+
// #177:相似度从 reason 文本中回收(sleep 路径写「相似度 0.87」,dream 路径
|
|
3396
|
+
// 是 LLM 自由文本)——回收得到就画进度条,否则不画,绝不显示编造的数字。
|
|
3397
|
+
// 防误报:只认「相似度 0.87 / similarity 0.87」这类锚定短语,不在全文里
|
|
3398
|
+
// 捞数字(否则 reason 里的年份、条数都会被当成相似度)。
|
|
3399
|
+
const similarityOf = (reason) => {
|
|
3400
|
+
const m = /(?:相似度|similarity)\s*([01](?:\.\d+)?)/i.exec(String(reason || ""));
|
|
3401
|
+
const v = m ? Number(m[1]) : NaN;
|
|
3402
|
+
return v >= 0 && v <= 1 ? v : null;
|
|
3403
|
+
};
|
|
3245
3404
|
return h("div", { className: "mneme-conflictq" },
|
|
3246
3405
|
h("div", { className: "mneme-conflictq-head" },
|
|
3247
3406
|
h("span", { className: "mneme-xcount" }, t("memory.status.conflicts")),
|
|
3248
3407
|
h("button", { className: "mneme-footbtn", onClick: () => load() },
|
|
3249
3408
|
h(Icon, { name: "refresh", size: 12 }), t("memory.status.conflictQueue.refresh"))),
|
|
3250
|
-
|
|
3409
|
+
// #177 空态教育卡:队列没有条目时也渲染(整块此前直接 return null),
|
|
3410
|
+
// 解释「什么情况会产生冲突、冻结是什么」——新用户第一次遇见冻结时
|
|
3411
|
+
// 状态页已有解释在等他。加载失败时显示错误卡而不是空态卡。
|
|
3412
|
+
items.length === 0 && (loadError
|
|
3413
|
+
? h("div", { className: "mneme-conflict-empty", role: "alert" },
|
|
3414
|
+
h("div", { className: "mneme-conflict-empty-title" }, t("memory.status.error")))
|
|
3415
|
+
: h("div", { className: "mneme-conflict-empty" },
|
|
3416
|
+
h("div", { className: "mneme-conflict-empty-title" }, t("memory.status.conflictQueue.emptyTitle")),
|
|
3417
|
+
h("div", { className: "mneme-conflict-empty-body" }, t("memory.status.conflictQueue.emptyBody")))),
|
|
3418
|
+
items.map((it) => {
|
|
3419
|
+
const sim = similarityOf(it.reason);
|
|
3420
|
+
const aText = (it.memory_a && it.memory_a.content) || "";
|
|
3421
|
+
const bText = (it.memory_b && it.memory_b.content) || "";
|
|
3422
|
+
// diff 与相似度条互补:reason 给不出数字时才跑 LCS(两者表达同一信息)。
|
|
3423
|
+
const diff = (sim === null && !it.memory_a.missing && !it.memory_b.missing)
|
|
3424
|
+
? wordDiff(aText, bText) : null;
|
|
3425
|
+
return h("div", { key: it.id, className: "mneme-conflict-item" },
|
|
3251
3426
|
it.reason && h("div", { className: "mneme-conflict-reason" },
|
|
3252
3427
|
`${t("memory.status.conflictQueue.reason")}: ${it.reason}`),
|
|
3428
|
+
sim !== null && h("div", { className: "mneme-conflict-simrow" },
|
|
3429
|
+
h("span", null, t("memory.status.conflictQueue.similarity")),
|
|
3430
|
+
h("span", { className: "mneme-conflict-simbar" },
|
|
3431
|
+
h("span", { className: "mneme-conflict-simfill", style: { width: `${Math.round(sim * 100)}%` } })),
|
|
3432
|
+
h("span", null, `${Math.round(sim * 100)}%`)),
|
|
3253
3433
|
h("div", { className: "mneme-conflict-pair" },
|
|
3254
|
-
side(it.memory_a, t("memory.status.conflictQueue.sideA")),
|
|
3255
|
-
side(it.memory_b, t("memory.status.conflictQueue.sideB"))),
|
|
3434
|
+
side(it.memory_a, t("memory.status.conflictQueue.sideA"), "mneme-conflict-side--a", diff, "a"),
|
|
3435
|
+
side(it.memory_b, t("memory.status.conflictQueue.sideB"), "mneme-conflict-side--b", diff, "b")),
|
|
3256
3436
|
h("div", { className: "mneme-conflict-actions" },
|
|
3257
|
-
h("span", { className: "mneme-conflict-hint"
|
|
3258
|
-
|
|
3259
|
-
h("button", { className: "mneme-
|
|
3260
|
-
h("button", { className: "mneme-
|
|
3261
|
-
|
|
3437
|
+
h("span", { className: "mneme-conflict-hint", title: t("memory.status.conflictQueue.applyHintTitle") },
|
|
3438
|
+
t("memory.status.conflictQueue.applyHint")),
|
|
3439
|
+
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")),
|
|
3440
|
+
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")),
|
|
3441
|
+
h("button", { className: "mneme-footbtn", disabled: busy, onClick: () => resolve(it.id, null) }, t("memory.status.conflictQueue.markReviewed"))));
|
|
3442
|
+
}));
|
|
3262
3443
|
}
|
|
3263
3444
|
|
|
3264
3445
|
// --- 状态页工作台:让用户切实看见插件在干活 ---
|
|
@@ -3446,7 +3627,7 @@ window.__ModuleLoader__.load({
|
|
|
3446
3627
|
// 口径不可信)或库为空时整卡不渲染,前端不感知;truncated(扫描超上限)
|
|
3447
3628
|
// 只影响 hint 里的回执计数,不挡渲染。
|
|
3448
3629
|
function RecallStatsCard({ t }) {
|
|
3449
|
-
const [state, setState] = useState({ loading: true, off: false, rate: null, zombie: 0, active: 0, exempt: 0, runs: 0, top: "", inject: "" });
|
|
3630
|
+
const [state, setState] = useState({ loading: true, off: false, rate: null, zombie: 0, active: 0, exempt: 0, runs: 0, top: "", inject: "", archive: "" });
|
|
3450
3631
|
useEffect(() => {
|
|
3451
3632
|
let cancelled = false;
|
|
3452
3633
|
apiFetch("/api/dsh-mneme/recall-stats?window=30")
|
|
@@ -3454,7 +3635,11 @@ window.__ModuleLoader__.load({
|
|
|
3454
3635
|
.then((d) => {
|
|
3455
3636
|
if (cancelled) return;
|
|
3456
3637
|
const z = d.zombie || {};
|
|
3457
|
-
|
|
3638
|
+
// 归档侧第五指标(#275)不能只靠 run/active 两路撑整张卡:整库归档是它的正常
|
|
3639
|
+
// 工作状态,那种库「窗口内没有召回回执、也没有活跃僵尸行」时若 hasData 为假,
|
|
3640
|
+
// 组件直接走 off 分支 return null,归档指标永远没机会显示。
|
|
3641
|
+
const hasData = (d.coverage?.runsScanned ?? 0) > 0 || (z.activeCount ?? 0) > 0
|
|
3642
|
+
|| (d.archive?.total ?? 0) > 0;
|
|
3458
3643
|
if (!hasData) {
|
|
3459
3644
|
setState({ loading: false, off: true });
|
|
3460
3645
|
return;
|
|
@@ -3471,10 +3656,19 @@ window.__ModuleLoader__.load({
|
|
|
3471
3656
|
.replace("{count}", String(inj.injectedCount ?? 0))
|
|
3472
3657
|
.replace("{fill}", inj.slotFillRate == null ? "—" : String(Math.round(inj.slotFillRate * 100)))
|
|
3473
3658
|
: "";
|
|
3659
|
+
// 第五指标(#275):归档净增速率 + 可压掉行数。与注入口径同款自门控
|
|
3660
|
+
// ——归档区为空时整段省略,不往卡里塞一个恒 0 的数字。
|
|
3661
|
+
const ar = d.archive || {};
|
|
3662
|
+
const archive = (ar.total ?? 0) > 0
|
|
3663
|
+
? t("memory.status.recallArchive")
|
|
3664
|
+
.replace("{total}", String(ar.total ?? 0))
|
|
3665
|
+
.replace("{add}", String(ar.perDay ?? 0))
|
|
3666
|
+
.replace("{compress}", String(ar.compressible?.rows ?? 0))
|
|
3667
|
+
: "";
|
|
3474
3668
|
setState({
|
|
3475
3669
|
loading: false, off: false, rate,
|
|
3476
3670
|
zombie: z.zombieCount ?? 0, active: z.activeCount ?? 0,
|
|
3477
|
-
exempt: z.exemptCount ?? 0, runs: d.coverage?.runsScanned ?? 0, top, inject
|
|
3671
|
+
exempt: z.exemptCount ?? 0, runs: d.coverage?.runsScanned ?? 0, top, inject, archive
|
|
3478
3672
|
});
|
|
3479
3673
|
})
|
|
3480
3674
|
.catch(() => { if (!cancelled) setState({ loading: false, off: true }); });
|
|
@@ -3493,7 +3687,7 @@ window.__ModuleLoader__.load({
|
|
|
3493
3687
|
.replace("{active}", String(state.active))
|
|
3494
3688
|
.replace("{exempt}", String(state.exempt))
|
|
3495
3689
|
.replace("{runs}", String(state.runs))
|
|
3496
|
-
.replace("{top}", state.top || "—") + (state.inject ? " · " + state.inject : "")
|
|
3690
|
+
.replace("{top}", state.top || "—") + (state.inject ? " · " + state.inject : "") + (state.archive ? " · " + state.archive : "")
|
|
3497
3691
|
});
|
|
3498
3692
|
}
|
|
3499
3693
|
|
|
@@ -4475,23 +4669,56 @@ window.__ModuleLoader__.load({
|
|
|
4475
4669
|
// 我们的修饰类覆盖配色(次级观感)。这样展开态与收起态(rail)都直接
|
|
4476
4670
|
// 继承宿主自己的盒模型与间距——宿主改版/缩进动画零位移,无需硬编码。
|
|
4477
4671
|
// 宿主结构异常时约 2 秒后放弃 portal,footer 回退按钮保持可用。
|
|
4672
|
+
// #177:未处理冲突数 badge——数据走 dream-status 的 pendingConflicts(状态卡
|
|
4673
|
+
// 同一端点),60s 轮询 + 打开面板即刷新。挂侧边栏入口按钮右上角(portal 与
|
|
4674
|
+
// 回退两处都挂),让用户不进面板也知道有冻结要裁决。
|
|
4675
|
+
function useConflictBadgeCount(t) {
|
|
4676
|
+
const [pending, setPending] = useState(0);
|
|
4677
|
+
useEffect(() => {
|
|
4678
|
+
let cancelled = false;
|
|
4679
|
+
const tick = () => {
|
|
4680
|
+
apiFetch("/api/dsh-mneme/dream-status")
|
|
4681
|
+
.then((res) => (res.ok ? res.json() : { pendingConflicts: 0 }))
|
|
4682
|
+
.then((d) => { if (!cancelled) setPending(Number((d && d.pendingConflicts) ?? 0) || 0); })
|
|
4683
|
+
.catch(() => { if (!cancelled) setPending(0); });
|
|
4684
|
+
};
|
|
4685
|
+
tick();
|
|
4686
|
+
const timer = setInterval(tick, 60_000);
|
|
4687
|
+
return () => { cancelled = true; clearInterval(timer); };
|
|
4688
|
+
}, []);
|
|
4689
|
+
return pending;
|
|
4690
|
+
}
|
|
4691
|
+
function ConflictBadge({ pending }) {
|
|
4692
|
+
if (!pending || pending <= 0) return null;
|
|
4693
|
+
return h("span", { className: "mneme-entrybadge", "aria-hidden": "true" },
|
|
4694
|
+
pending > 99 ? "99+" : String(pending));
|
|
4695
|
+
}
|
|
4478
4696
|
function SidebarFallbackTrigger({ wide, t }) {
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4697
|
+
const pending = useConflictBadgeCount(t);
|
|
4698
|
+
// #295 评审:.mneme-trigger 自身 overflow:hidden 且无定位上下文,badge
|
|
4699
|
+
// 直接放里面会被裁剪——包一层定位容器,badge 挂在容器上。
|
|
4700
|
+
return h("div", { className: "mneme-triggerwrap" },
|
|
4701
|
+
h("button", {
|
|
4702
|
+
type: "button",
|
|
4703
|
+
className: wide ? "mneme-trigger" : "mneme-trigger mneme-rail",
|
|
4704
|
+
"aria-label": pending > 0
|
|
4705
|
+
? `${t("memory.sidebar.aria")} · ${t("memory.status.conflicts")} ${pending}`
|
|
4706
|
+
: t("memory.sidebar.aria"),
|
|
4707
|
+
title: t("memory.panel.open"),
|
|
4708
|
+
onClick: openLibrary,
|
|
4709
|
+
"data-mneme-overlay-opener": "true"
|
|
4710
|
+
},
|
|
4711
|
+
renderArchiveIcon({ size: wide ? 16 : 18 }),
|
|
4712
|
+
wide && h("span", { className: "mneme-trigger-label" }, t("memory.panel.open"))
|
|
4713
|
+
),
|
|
4714
|
+
h(ConflictBadge, { pending })
|
|
4489
4715
|
);
|
|
4490
4716
|
}
|
|
4491
4717
|
|
|
4492
4718
|
function SidebarTopEntry({ wide, t, fallback }) {
|
|
4493
4719
|
const [host, setHost] = useState(null);
|
|
4494
4720
|
const [nativeCls, setNativeCls] = useState("");
|
|
4721
|
+
const pending = useConflictBadgeCount(t);
|
|
4495
4722
|
useEffect(() => {
|
|
4496
4723
|
if (!reactDom || typeof document === "undefined") return undefined;
|
|
4497
4724
|
let tries = 0, timer = null, created = null, mo = null;
|
|
@@ -4567,13 +4794,16 @@ window.__ModuleLoader__.load({
|
|
|
4567
4794
|
h("button", {
|
|
4568
4795
|
type: "button",
|
|
4569
4796
|
className: `${nativeCls} mneme-topentry-native`.trim(),
|
|
4570
|
-
"aria-label":
|
|
4797
|
+
"aria-label": pending > 0
|
|
4798
|
+
? `${t("memory.sidebar.aria")} · ${t("memory.status.conflicts")} ${pending}`
|
|
4799
|
+
: t("memory.sidebar.aria"),
|
|
4571
4800
|
title: t("memory.panel.open"),
|
|
4572
4801
|
onClick: openLibrary,
|
|
4573
4802
|
"data-mneme-overlay-opener": "true"
|
|
4574
4803
|
},
|
|
4575
|
-
|
|
4576
|
-
wide && h("span", { className: "mneme-topentry-label" }, t("memory.panel.open"))
|
|
4804
|
+
renderArchiveIcon({ size: wide ? 15 : 18 }),
|
|
4805
|
+
wide && h("span", { className: "mneme-topentry-label" }, t("memory.panel.open")),
|
|
4806
|
+
h(ConflictBadge, { pending })
|
|
4577
4807
|
)
|
|
4578
4808
|
), host);
|
|
4579
4809
|
}
|
|
@@ -4633,7 +4863,7 @@ window.__ModuleLoader__.load({
|
|
|
4633
4863
|
reg.registerTab({
|
|
4634
4864
|
id: TAB_ID,
|
|
4635
4865
|
title: () => t("memory.view.label"),
|
|
4636
|
-
icon: (size) =>
|
|
4866
|
+
icon: (size) => renderArchiveIcon({ size }),
|
|
4637
4867
|
order: 60,
|
|
4638
4868
|
component: () => h(MemoryExplorer, { t })
|
|
4639
4869
|
});
|