@modusensus/dsh-mneme 0.8.6 → 0.8.8
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 +19 -148
- package/bin/cli.mjs +689 -603
- package/lib/api-standalone.js +57 -1
- package/lib/api.js +34 -11
- package/lib/client.js +349 -61
- package/lib/config.js +118 -7
- package/lib/content-hash.js +54 -0
- package/lib/continuity.js +220 -0
- package/lib/document-index.js +94 -0
- package/lib/document.js +93 -11
- package/lib/dream.js +138 -39
- package/lib/guide.js +18 -0
- package/lib/index.js +87 -12
- 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/peak-hours.js +117 -0
- package/lib/recall-stats.js +52 -2
- package/lib/scope.js +34 -6
- package/lib/service.js +86 -7
- package/lib/settings.js +16 -2
- package/lib/store.js +318 -21
- package/lib/summarize.js +52 -113
- package/lib/tools.js +37 -11
- 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 +118 -7
- package/src/content-hash.js +54 -0
- package/src/continuity.js +220 -0
- package/src/document-index.js +94 -0
- package/src/document.js +93 -11
- package/src/dream.js +138 -39
- package/src/guide.js +18 -0
- package/src/index.js +87 -12
- 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/peak-hours.js +117 -0
- package/src/recall-stats.js +52 -2
- package/src/scope.js +34 -6
- package/src/service.js +86 -7
- package/src/settings.js +16 -2
- package/src/store.js +318 -21
- package/src/summarize.js +52 -113
- package/src/tools.js +37 -11
- package/src/write-admission.js +259 -0
- package/test/api.test.js +21 -3
- package/test/client.test.js +103 -6
- package/test/content-hash.test.js +204 -0
- package/test/continuity.test.js +333 -0
- package/test/document-dir.test.js +233 -0
- package/test/document.test.js +151 -3
- package/test/dream-failure-backoff.test.js +173 -0
- package/test/dream-peak-hours.test.js +194 -0
- 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/reasoning-effort.test.js +6 -0
- package/test/recall-evals.test.js +18 -0
- package/test/recall-stats.test.js +66 -0
- package/test/summarize-reasoning-effort.test.js +189 -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,9 @@ window.__ModuleLoader__.load({
|
|
|
327
381
|
"memory.features.autoInject.hint": "每轮对话自动携带相关记忆",
|
|
328
382
|
"memory.features.injectGuidanceEnabled": "能力说明",
|
|
329
383
|
"memory.features.injectGuidanceEnabled.hint": "在工具描述与一次性提示段里说明怎么用记忆(何时查、何时写、拿不准就不做)",
|
|
384
|
+
"memory.features.continuityRescueEnabled": "压缩边缘抢救",
|
|
385
|
+
"memory.features.continuityRescueEnabled.hint": "上下文即将被精简前,落一条连续性快照(正在做什么 / 下一步 / 未决问题)并追加到对话末尾,让它活过这次压缩",
|
|
386
|
+
"memory.features.parentOff": "父开关关闭时不生效",
|
|
330
387
|
"memory.features.autoSummarize": "自动总结",
|
|
331
388
|
"memory.features.autoSummarize.hint": "对话结束自动提炼记忆条目",
|
|
332
389
|
"memory.features.hotMemoryEnabled": "热记忆",
|
|
@@ -340,6 +397,13 @@ window.__ModuleLoader__.load({
|
|
|
340
397
|
"memory.features.entityExtractionReasoning.low": "低",
|
|
341
398
|
"memory.features.entityExtractionReasoning.medium": "中",
|
|
342
399
|
"memory.features.entityExtractionReasoning.high": "高",
|
|
400
|
+
"memory.features.summarizeReasoningEffort": "蒸馏思考强度",
|
|
401
|
+
"memory.features.summarizeReasoningEffort.off": "关闭思考",
|
|
402
|
+
"memory.features.summarizeReasoningEffort.none": "跟随默认",
|
|
403
|
+
"memory.features.summarizeReasoningEffort.low": "低",
|
|
404
|
+
"memory.features.summarizeReasoningEffort.medium": "中",
|
|
405
|
+
"memory.features.summarizeReasoningEffort.high": "高",
|
|
406
|
+
"memory.features.summarizeReasoningEffort.hint": "思考型模型建议选低档或关闭思考,避免推理烧光输出预算导致总结失败;改后重启 DSH 生效",
|
|
343
407
|
"memory.features.entityExtractionModelHint": "Provider/模型留空 = 跟随主对话模型;思考强度 none = 服务商默认",
|
|
344
408
|
"memory.features.codingRetrospect": "编码记忆蒸馏",
|
|
345
409
|
"memory.features.codingRetrospect.hint": "用完整转录(含工具调用与报错)提炼踩坑、约束与被否决方案",
|
|
@@ -401,6 +465,10 @@ window.__ModuleLoader__.load({
|
|
|
401
465
|
"memory.features.dreamProvider": "巩固模型 Provider",
|
|
402
466
|
"memory.features.dreamModel": "巩固用模型名",
|
|
403
467
|
"memory.features.dreamModelHint": "留空 = 跟随主对话模型;只影响记忆巩固(autoDream)用的模型",
|
|
468
|
+
"memory.features.dreamPeakHours": "高峰时段(不做梦)",
|
|
469
|
+
"memory.features.dreamPeakHours.hint": "空 = 关闭。逗号分隔、可带星期前缀、支持跨零点,如 09:00-18:00 或 mon-fri 08:00-12:00,14:00-18:00。命中时段不调模型,顺延到最近的高峰结束时刻补跑(最多顺延 dreamPeakMaxDeferMinutes 分钟,避免长高峰把巩固饿死)",
|
|
470
|
+
"memory.features.summarizePeakHours": "高峰时段(不蒸馏)",
|
|
471
|
+
"memory.features.summarizePeakHours.hint": "空 = 关闭。与上方巩固侧同一份时段语法;命中时蒸馏顺延到非高峰、窗口累积后一次蒸",
|
|
404
472
|
"memory.features.sleepProvider": "睡眠 Provider",
|
|
405
473
|
"memory.features.sleepModel": "睡眠模型",
|
|
406
474
|
"memory.features.sleepModelHint": "留空 = 用巩固模型或当前模型;建议选非思考模型",
|
|
@@ -491,6 +559,14 @@ window.__ModuleLoader__.load({
|
|
|
491
559
|
"memory.status.conflictQueue.resolved": "已裁决:保留{name}",
|
|
492
560
|
"memory.status.conflictQueue.refresh": "刷新队列",
|
|
493
561
|
"memory.status.conflictQueue.refreshed": "冲突队列已刷新",
|
|
562
|
+
"memory.status.conflictQueue.frozenBadge": "⏸ 冻结中",
|
|
563
|
+
"memory.status.conflictQueue.frozenTitle": "冻结中:不参与注入与巩固,等你裁决",
|
|
564
|
+
"memory.status.conflictQueue.similarity": "相似度",
|
|
565
|
+
"memory.status.conflictQueue.diffTitle": "差异已高亮:红=删除、绿=新增",
|
|
566
|
+
"memory.status.conflictQueue.diffFallback": "两段文本差异较大,请对照原文阅读",
|
|
567
|
+
"memory.status.conflictQueue.emptyTitle": "暂无待确认冲突",
|
|
568
|
+
"memory.status.conflictQueue.emptyBody": "当巩固发现两条互相矛盾或高度相似的记忆时,会先冻结在这里等你裁决,不会自动修改。裁决前双方都不参与注入。",
|
|
569
|
+
"memory.status.conflictQueue.applyHintTitle": "确认后:保留方正文追加已否决注记,另一方归档",
|
|
494
570
|
"memory.status.workbench": "工作动态",
|
|
495
571
|
"memory.status.dreamConsolidate": "记忆巩固",
|
|
496
572
|
"memory.status.summarize": "总结提炼",
|
|
@@ -508,6 +584,7 @@ window.__ModuleLoader__.load({
|
|
|
508
584
|
"memory.status.recallStats": "记忆复用",
|
|
509
585
|
"memory.status.recallHint": "复用率 {rate} · 僵尸 {zombie}/{active}(豁免 {exempt})· 30 天回执 {runs} 次 · Top:{top}",
|
|
510
586
|
"memory.status.recallInject": "注入 {runs} 轮 {count} 条,槽位填充 {fill}%",
|
|
587
|
+
"memory.status.recallArchive": "归档 {total} 行({add}/天),精确重复可压掉 {compress} 行",
|
|
511
588
|
"memory.status.viewAll": "查看全部",
|
|
512
589
|
"memory.status.depositedCount": "沉淀的记忆({n})",
|
|
513
590
|
"memory.status.archivedCount": "已归档的记忆({n})",
|
|
@@ -705,6 +782,9 @@ window.__ModuleLoader__.load({
|
|
|
705
782
|
"memory.features.autoInject.hint": "Carry relevant memories into every turn",
|
|
706
783
|
"memory.features.injectGuidanceEnabled": "Capability guide",
|
|
707
784
|
"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",
|
|
785
|
+
"memory.features.continuityRescueEnabled": "Compaction-edge rescue",
|
|
786
|
+
"memory.features.continuityRescueEnabled.hint": "Before context compaction, record a continuity snapshot (current work / next step / open questions) and append it near the end of the conversation so it survives the compaction",
|
|
787
|
+
"memory.features.parentOff": "Inactive while auto injection is off",
|
|
708
788
|
"memory.features.autoSummarize": "Auto summarization",
|
|
709
789
|
"memory.features.autoSummarize.hint": "Distill memory entries when a conversation ends",
|
|
710
790
|
"memory.features.hotMemoryEnabled": "Hot memory",
|
|
@@ -718,6 +798,13 @@ window.__ModuleLoader__.load({
|
|
|
718
798
|
"memory.features.entityExtractionReasoning.low": "Low",
|
|
719
799
|
"memory.features.entityExtractionReasoning.medium": "Medium",
|
|
720
800
|
"memory.features.entityExtractionReasoning.high": "High",
|
|
801
|
+
"memory.features.summarizeReasoningEffort": "Distill reasoning effort",
|
|
802
|
+
"memory.features.summarizeReasoningEffort.off": "No reasoning",
|
|
803
|
+
"memory.features.summarizeReasoningEffort.none": "Follow default",
|
|
804
|
+
"memory.features.summarizeReasoningEffort.low": "Low",
|
|
805
|
+
"memory.features.summarizeReasoningEffort.medium": "Medium",
|
|
806
|
+
"memory.features.summarizeReasoningEffort.high": "High",
|
|
807
|
+
"memory.features.summarizeReasoningEffort.hint": "Prefer low or No reasoning for thinking models so reasoning cannot drain the output budget; takes effect after a DSH restart",
|
|
721
808
|
"memory.features.entityExtractionModelHint": "Provider / model empty = follow the main conversation model; reasoning none = provider default",
|
|
722
809
|
"memory.features.codingRetrospect": "Coding retrospection",
|
|
723
810
|
"memory.features.codingRetrospect.hint": "Distill pitfalls, constraints and rejected solutions from full transcripts (tools and errors included)",
|
|
@@ -779,6 +866,10 @@ window.__ModuleLoader__.load({
|
|
|
779
866
|
"memory.features.dreamProvider": "Consolidation provider",
|
|
780
867
|
"memory.features.dreamModel": "Consolidation model",
|
|
781
868
|
"memory.features.dreamModelHint": "Leave empty to follow the main conversation model; only affects autoDream consolidation",
|
|
869
|
+
"memory.features.dreamPeakHours": "Peak hours (no dreaming)",
|
|
870
|
+
"memory.features.dreamPeakHours.hint": "Empty = off. Comma-separated windows, optional weekday prefix, midnight-crossing allowed — e.g. 09:00-18:00 or mon-fri 08:00-12:00,14:00-18:00. Inside these windows no LLM call is made; the run is deferred to the nearest peak-end (capped by dreamPeakMaxDeferMinutes so an all-day peak cannot starve consolidation)",
|
|
871
|
+
"memory.features.summarizePeakHours": "Peak hours (no distillation)",
|
|
872
|
+
"memory.features.summarizePeakHours.hint": "Empty = off. Same window syntax as the consolidation side above; inside a peak, distillation is deferred and the window accumulates for one bigger run",
|
|
782
873
|
"memory.features.sleepProvider": "Sleep provider",
|
|
783
874
|
"memory.features.sleepModel": "Sleep model",
|
|
784
875
|
"memory.features.sleepModelHint": "Leave empty to reuse the consolidation model; a non-reasoning model is recommended",
|
|
@@ -869,6 +960,14 @@ window.__ModuleLoader__.load({
|
|
|
869
960
|
"memory.status.conflictQueue.resolved": "Resolved: kept {name}",
|
|
870
961
|
"memory.status.conflictQueue.refresh": "Refresh queue",
|
|
871
962
|
"memory.status.conflictQueue.refreshed": "Conflict queue refreshed",
|
|
963
|
+
"memory.status.conflictQueue.frozenBadge": "⏸ Frozen",
|
|
964
|
+
"memory.status.conflictQueue.frozenTitle": "Frozen: excluded from injection and consolidation, awaiting your review",
|
|
965
|
+
"memory.status.conflictQueue.similarity": "similarity",
|
|
966
|
+
"memory.status.conflictQueue.diffTitle": "Differences highlighted: red = removed, green = added",
|
|
967
|
+
"memory.status.conflictQueue.diffFallback": "The two texts differ substantially — compare the originals",
|
|
968
|
+
"memory.status.conflictQueue.emptyTitle": "No pending conflicts",
|
|
969
|
+
"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.",
|
|
970
|
+
"memory.status.conflictQueue.applyHintTitle": "On confirm: a veto note is appended to the kept side and the other side is archived",
|
|
872
971
|
"memory.status.workbench": "Activity",
|
|
873
972
|
"memory.status.dreamConsolidate": "Consolidation",
|
|
874
973
|
"memory.status.summarize": "Summarization",
|
|
@@ -886,6 +985,7 @@ window.__ModuleLoader__.load({
|
|
|
886
985
|
"memory.status.recallStats": "Recall reuse",
|
|
887
986
|
"memory.status.recallHint": "reuse {rate} · zombie {zombie}/{active} (exempt {exempt}) · {runs} runs in 30d · top: {top}",
|
|
888
987
|
"memory.status.recallInject": "injected {runs} turns · {count} items ({fill}% slots)",
|
|
988
|
+
"memory.status.recallArchive": "archived {total} rows (+{add}/day) · {compress} exactly-duplicate rows compressible",
|
|
889
989
|
"memory.status.viewAll": "View all",
|
|
890
990
|
"memory.status.depositedCount": "Deposited memories ({n})",
|
|
891
991
|
"memory.status.archivedCount": "Archived memories ({n})",
|
|
@@ -1148,7 +1248,7 @@ window.__ModuleLoader__.load({
|
|
|
1148
1248
|
".mneme-heat--cold{color:var(--dsw-alias-label-tertiary)}",
|
|
1149
1249
|
".mneme-heat--cold .mneme-heatpct{display:none}",
|
|
1150
1250
|
// 纵向 flex 容器按原生按钮的宽度与外边距分配空间,保留展开和收起态的对齐。
|
|
1151
|
-
".mneme-topentry{display:flex;flex-direction:column}",
|
|
1251
|
+
".mneme-topentry{display:flex;flex-direction:column;position:relative}",
|
|
1152
1252
|
".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
1253
|
".mneme-topentry-native:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}",
|
|
1154
1254
|
".mneme-topentry-native .mneme-topentry-label{white-space:nowrap}",
|
|
@@ -1213,6 +1313,31 @@ window.__ModuleLoader__.load({
|
|
|
1213
1313
|
".mneme-conflict-missing{font-size:12px;color:var(--dsw-alias-label-tertiary)}",
|
|
1214
1314
|
".mneme-conflict-actions{display:flex;align-items:center;gap:6px;flex-wrap:wrap}",
|
|
1215
1315
|
".mneme-conflict-hint{font-size:11px;color:var(--dsw-alias-label-tertiary);margin-right:auto}",
|
|
1316
|
+
// #177 冲突队列视觉批次:A/B 侧色、相似度条、词级 diff、主色按钮、空态
|
|
1317
|
+
".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)}",
|
|
1318
|
+
".mneme-conflict-side--a .mneme-conflict-sidelabel{color:var(--dsw-alias-state-business-primary);font-weight:600}",
|
|
1319
|
+
".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)}",
|
|
1320
|
+
".mneme-conflict-side--b .mneme-conflict-sidelabel{color:var(--dsw-alias-state-warning,#e6a23c);font-weight:600}",
|
|
1321
|
+
".mneme-conflict-simrow{display:flex;align-items:center;gap:8px;font-size:11px;color:var(--dsw-alias-label-secondary)}",
|
|
1322
|
+
".mneme-conflict-simbar{position:relative;flex:1;height:5px;border-radius:3px;background:var(--dsw-alias-interactive-bg-hover);overflow:hidden}",
|
|
1323
|
+
".mneme-conflict-simfill{position:absolute;top:0;bottom:0;left:0;border-radius:3px;background:var(--dsw-alias-state-business-primary)}",
|
|
1324
|
+
".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}",
|
|
1325
|
+
".mneme-conflict-mark{text-decoration:none;border-radius:3px;padding:0 1px}",
|
|
1326
|
+
".mneme-conflict-mark--del{background:color-mix(in srgb,var(--dsw-alias-state-error,#c33) 16%,transparent);text-decoration:line-through}",
|
|
1327
|
+
".mneme-conflict-mark--ins{background:color-mix(in srgb,var(--dsw-alias-state-success,#3c9) 18%,transparent)}",
|
|
1328
|
+
".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}",
|
|
1329
|
+
".mneme-conflict-primary:disabled{opacity:.55;cursor:default}",
|
|
1330
|
+
".mneme-conflict-primary:hover:not(:disabled){background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 85%,black)}",
|
|
1331
|
+
".mneme-badge--frozen{color:var(--dsw-alias-state-warning,#e6a23c);background:color-mix(in srgb,var(--dsw-alias-state-warning,#e6a23c) 12%,transparent)}",
|
|
1332
|
+
".mneme-conflict-empty{border:1px dashed var(--dsw-alias-border-l2);border-radius:10px;padding:12px 14px;display:flex;flex-direction:column;gap:4px}",
|
|
1333
|
+
".mneme-conflict-empty-title{font-size:12.5px;font-weight:600;color:var(--dsw-alias-label-secondary)}",
|
|
1334
|
+
".mneme-conflict-empty-body{font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}",
|
|
1335
|
+
".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)}",
|
|
1336
|
+
".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)}",
|
|
1337
|
+
".mneme-triggerwrap{position:relative;pointer-events:none}",
|
|
1338
|
+
".mneme-triggerwrap .mneme-trigger{pointer-events:auto}",
|
|
1339
|
+
".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)}",
|
|
1340
|
+
".mneme-conflict-jump:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px;border-radius:12px}",
|
|
1216
1341
|
// 详情抽屉:sheet 内右侧滑出,覆盖在浏览区之上
|
|
1217
1342
|
".mneme-xmain{position:relative}",
|
|
1218
1343
|
"@keyframes mneme-slidein{from{opacity:0;transform:translateX(16px)}to{opacity:1;transform:translateX(0)}}",
|
|
@@ -1827,8 +1952,14 @@ window.__ModuleLoader__.load({
|
|
|
1827
1952
|
// 分组排版:核心/增强/巩固常驻,注入策略等收进「高级」折叠,普通用户
|
|
1828
1953
|
// 不被专业项淹没。429 调速器参数、distillMaxChars、codingBoostFactor
|
|
1829
1954
|
// 属调优噪音,按对齐结论留在配置文件,不上 UI。
|
|
1955
|
+
// #249 第二批:注入形态的父/子开关(父 = autoInject)。子项紧跟父行、缩进
|
|
1956
|
+
// 显示;父关时子项加一句「不生效」,但开关仍可点——父关是「不生效」而不是
|
|
1957
|
+
// 「重置用户配置」,子项自己勾着的值要留着,也应该能提前设好。同一份关系在
|
|
1958
|
+
// 后端 src/config.js 的 INJECT_CHILD_FLAGS(运行时闸门),两侧漂移由
|
|
1959
|
+
// test/inject-parent-gate.test.js 钉住。
|
|
1960
|
+
const FEATURE_CHILDREN = { autoInject: ["injectGuidanceEnabled", "continuityRescueEnabled"] };
|
|
1830
1961
|
const FEATURE_GROUPS = [
|
|
1831
|
-
{ key: "group.core", items: ["autoInject", "
|
|
1962
|
+
{ key: "group.core", items: ["autoInject", "autoSummarize", "hotMemoryEnabled", "memoryQualityFilter.enabled", "llmAudit.enabled"] },
|
|
1832
1963
|
{ key: "group.enhance", items: ["entityExtractionEnabled", "codingRetrospect", "rerankEnabled", "resilientModelDownload", "searchSemanticDedup", "bm25SearchEnabled", "heatEnabled", "documentMemoryEnabled"] },
|
|
1833
1964
|
{ key: "group.dream", items: ["autoDream", "sleepModeEnabled"] },
|
|
1834
1965
|
// v0.8.0 A4(issue #17):作用域隔离组——标注总开关 + 严格硬过滤。
|
|
@@ -1837,13 +1968,19 @@ window.__ModuleLoader__.load({
|
|
|
1837
1968
|
const FEATURE_ADVANCED_BOOLS = ["hybridInject", "selectiveInjectEnabled", "adaptiveThresholdEnabled", "reflectionUpdateEnabled", "reflectionFailureTracking", "conflictFreezeEnabled", "trustEpistemicWeighting"];
|
|
1838
1969
|
// 字符串键(blur/Enter 提交,空串合法 = 跟随默认):巩固模型与语义
|
|
1839
1970
|
// 检索路线。embedProvider 是枚举,用下拉单独渲染。
|
|
1840
|
-
const FEATURE_STRINGS = ["dreamProvider", "dreamModel", "sleepProvider", "sleepModel", "entityExtractionProvider", "entityExtractionModel", "localEmbedModel", "ollamaBaseUrl", "ollamaModel"
|
|
1971
|
+
const FEATURE_STRINGS = ["dreamProvider", "dreamModel", "sleepProvider", "sleepModel", "entityExtractionProvider", "entityExtractionModel", "localEmbedModel", "ollamaBaseUrl", "ollamaModel",
|
|
1972
|
+
// Issue #239 第 4 项:错峰时段串(巩固侧与蒸馏侧)。此前只有后端白名单、
|
|
1973
|
+
// 面板调不到——两个错峰键一个能调一个不能比都不给更让人困惑。
|
|
1974
|
+
"dreamPeakHours", "summarizePeakHours"];
|
|
1841
1975
|
const EMBED_PROVIDERS = ["openai", "local", "ollama"];
|
|
1842
1976
|
// 实体抽取思考强度(issue #109):与后端 FEATURE_FLAG_ENUMS 枚举对齐。
|
|
1843
1977
|
const ENTITY_REASONING = ["none", "low", "medium", "high"];
|
|
1978
|
+
// 蒸馏思考强度(issue #315):比 entity 多一个 off(显式关思考,思考型模型
|
|
1979
|
+
// 蒸馏防推理烧预算)。后端枚举含 off,面板必须能选到,否则操作者用不上。
|
|
1980
|
+
const SUMMARIZE_REASONING = ["off", "none", "low", "medium", "high"];
|
|
1844
1981
|
|
|
1845
|
-
function FeatureRow({ name, hint, on, disabled, onToggle }) {
|
|
1846
|
-
return h("div", { className: "mneme-featrow" },
|
|
1982
|
+
function FeatureRow({ name, hint, on, disabled, onToggle, sub }) {
|
|
1983
|
+
return h("div", { className: "mneme-featrow", style: sub ? { paddingLeft: 18, opacity: 0.86 } : undefined },
|
|
1847
1984
|
h("div", { className: "mneme-featmain" },
|
|
1848
1985
|
h("div", { className: "mneme-featname" }, name),
|
|
1849
1986
|
h("div", { className: "mneme-feathint" }, hint)
|
|
@@ -1934,15 +2071,24 @@ window.__ModuleLoader__.load({
|
|
|
1934
2071
|
put({ [key]: raw });
|
|
1935
2072
|
};
|
|
1936
2073
|
|
|
1937
|
-
const boolRow = (k) => h(FeatureRow, {
|
|
2074
|
+
const boolRow = (k, opts = {}) => h(FeatureRow, {
|
|
1938
2075
|
key: k,
|
|
1939
2076
|
name: t(`memory.features.${k}`),
|
|
1940
|
-
hint: t(`memory.features.${k}.hint`)
|
|
2077
|
+
hint: t(`memory.features.${k}.hint`)
|
|
2078
|
+
+ (opts.gate && !eff[opts.gate] ? ` · ${t("memory.features.parentOff")}` : ""),
|
|
1941
2079
|
on: !!eff[k],
|
|
1942
2080
|
disabled: busy,
|
|
2081
|
+
sub: !!opts.sub,
|
|
1943
2082
|
onToggle: () => put({ [k]: !eff[k] })
|
|
1944
2083
|
});
|
|
1945
2084
|
|
|
2085
|
+
// 带子项的父开关:父行 + 紧随其后的子行(子行缩进,父关时标注不生效)。
|
|
2086
|
+
const flagRow = (k) => {
|
|
2087
|
+
const kids = FEATURE_CHILDREN[k];
|
|
2088
|
+
if (!kids) return boolRow(k);
|
|
2089
|
+
return h(react.Fragment, { key: k }, boolRow(k), kids.map((c) => boolRow(c, { sub: true, gate: k })));
|
|
2090
|
+
};
|
|
2091
|
+
|
|
1946
2092
|
// 模型枚举项的统一形状:string 或 {id, name?}(/llm-providers 契约)。
|
|
1947
2093
|
const modelLabel = (m) => (typeof m === "string" ? m : String((m && (m.name || m.id)) ?? ""));
|
|
1948
2094
|
const modelValue = (m) => (typeof m === "string" ? m : String((m && m.id) ?? ""));
|
|
@@ -2087,7 +2233,11 @@ window.__ModuleLoader__.load({
|
|
|
2087
2233
|
Array.isArray(routes)
|
|
2088
2234
|
? routeSelects("dreamProvider", "dreamModel", dreamTest, setDreamTest, "dreamReasoningEffort")
|
|
2089
2235
|
: h(react.Fragment, null, strRow("dreamProvider"), strRow("dreamModel")),
|
|
2090
|
-
h("div", { className: "mneme-featsubhint" }, t("memory.features.dreamModelHint"))
|
|
2236
|
+
h("div", { className: "mneme-featsubhint" }, t("memory.features.dreamModelHint")),
|
|
2237
|
+
// Issue #239 第 4 项镜像到巩固:高峰时段串(空 = 关闭)。放在 autoDream
|
|
2238
|
+
// 子块内——它是巩固的排程,开关关掉时不该还在界面上留着可编辑的输入框。
|
|
2239
|
+
strRow("dreamPeakHours"),
|
|
2240
|
+
h("div", { className: "mneme-featsubhint" }, t("memory.features.dreamPeakHours.hint"))
|
|
2091
2241
|
);
|
|
2092
2242
|
|
|
2093
2243
|
// 睡眠模型:sleepModeEnabled 开着才展开(sleepProvider/sleepModel 随本版
|
|
@@ -2117,6 +2267,27 @@ window.__ModuleLoader__.load({
|
|
|
2117
2267
|
h("div", { className: "mneme-featsubhint" }, t("memory.features.entityExtractionModelHint"))
|
|
2118
2268
|
);
|
|
2119
2269
|
|
|
2270
|
+
// 蒸馏思考强度(issue #315):autoSummarize 开着才展开,档位与实体抽取
|
|
2271
|
+
// 同款枚举下拉、即时提交。蒸馏没有独立 provider/model 路由键(跟随会话
|
|
2272
|
+
// 头或 config 文件的 summarizeProvider/summarizeModel),不上连通性测试。
|
|
2273
|
+
const summarizeSub = eff.autoSummarize && h("div", { className: "mneme-featsub" },
|
|
2274
|
+
h("div", { className: "mneme-featnum" },
|
|
2275
|
+
h("span", { className: "mneme-featnumlabel" }, t("memory.features.summarizeReasoningEffort")),
|
|
2276
|
+
h("select", {
|
|
2277
|
+
className: "mneme-select",
|
|
2278
|
+
value: eff.summarizeReasoningEffort || "none",
|
|
2279
|
+
disabled: busy,
|
|
2280
|
+
onChange: (e) => put({ summarizeReasoningEffort: e.target.value })
|
|
2281
|
+
},
|
|
2282
|
+
SUMMARIZE_REASONING.map((r) => h("option", { key: r, value: r }, t(`memory.features.summarizeReasoningEffort.${r}`))))
|
|
2283
|
+
),
|
|
2284
|
+
h("div", { className: "mneme-featsubhint" }, t("memory.features.summarizeReasoningEffort.hint")),
|
|
2285
|
+
// Issue #239 第 4 项:蒸馏侧错峰时段串(空 = 关闭)。与思考强度同处一个
|
|
2286
|
+
// autoSummarize 子块——两处错峰开关都能在面板上调(巩固侧见 dreamSub)。
|
|
2287
|
+
strRow("summarizePeakHours"),
|
|
2288
|
+
h("div", { className: "mneme-featsubhint" }, t("memory.features.summarizePeakHours.hint"))
|
|
2289
|
+
);
|
|
2290
|
+
|
|
2120
2291
|
if (error && !state) return h("section", { className: "mneme-set-card" },
|
|
2121
2292
|
h("div", { className: "mneme-set-title" }, t("memory.features.title")),
|
|
2122
2293
|
h("div", { className: "mneme-set-hint" }, error));
|
|
@@ -2133,7 +2304,8 @@ window.__ModuleLoader__.load({
|
|
|
2133
2304
|
: h(react.Fragment, null,
|
|
2134
2305
|
FEATURE_GROUPS.map((g) => h(react.Fragment, { key: g.key },
|
|
2135
2306
|
h("div", { className: "mneme-featgroup" }, t(`memory.features.${g.key}`)),
|
|
2136
|
-
g.items.map(
|
|
2307
|
+
g.items.map(flagRow),
|
|
2308
|
+
g.key === "group.core" && h(react.Fragment, null, summarizeSub),
|
|
2137
2309
|
g.key === "group.enhance" && h(react.Fragment, null, embedSub, entitySub),
|
|
2138
2310
|
g.key === "group.dream" && h(react.Fragment, null, dreamSub, sleepSub)
|
|
2139
2311
|
)),
|
|
@@ -2147,7 +2319,7 @@ window.__ModuleLoader__.load({
|
|
|
2147
2319
|
h(Icon, { name: showAdv ? "chevronDown" : "chevronRight", size: 12 }),
|
|
2148
2320
|
t("memory.features.advancedToggle"))
|
|
2149
2321
|
),
|
|
2150
|
-
showAdv && FEATURE_ADVANCED_BOOLS.map(boolRow)
|
|
2322
|
+
showAdv && FEATURE_ADVANCED_BOOLS.map((k) => boolRow(k))
|
|
2151
2323
|
)
|
|
2152
2324
|
);
|
|
2153
2325
|
}
|
|
@@ -2257,7 +2429,7 @@ window.__ModuleLoader__.load({
|
|
|
2257
2429
|
// 版本/平台方便归档,正文同款模板;纯链接零后端成本。
|
|
2258
2430
|
const feedbackEnv = `**插件版本**: ${pkgVersion}\n**平台**: ${platformLabel()}\n`;
|
|
2259
2431
|
const feedbackBody = feedbackEnv + "**问题描述**:\n- 期望行为:\n- 实际行为:\n- 复现步骤:\n";
|
|
2260
|
-
const issueHref = "https://github.com/slow-stack/
|
|
2432
|
+
const issueHref = "https://github.com/slow-stack/mneme/issues/new?title="
|
|
2261
2433
|
+ encodeURIComponent("[dsh-mneme] 问题反馈")
|
|
2262
2434
|
+ "&body=" + encodeURIComponent(feedbackBody);
|
|
2263
2435
|
const mailHref = "mailto:work@modusensus.space?subject="
|
|
@@ -2690,7 +2862,7 @@ window.__ModuleLoader__.load({
|
|
|
2690
2862
|
h("div", { style: { display: "flex", flexDirection: "column", gap: 2 } },
|
|
2691
2863
|
h("a", { className: "mneme-feed-link", href: issueHref, target: "_blank", rel: "noopener noreferrer" }, t("memory.settings.feedback.newIssue")),
|
|
2692
2864
|
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/
|
|
2865
|
+
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
2866
|
),
|
|
2695
2867
|
h("div", { className: "mneme-featsubhint", style: { marginTop: 8 } }, t("memory.settings.feedback.hint"))
|
|
2696
2868
|
)
|
|
@@ -2772,7 +2944,7 @@ window.__ModuleLoader__.load({
|
|
|
2772
2944
|
h("div", { className: "mneme-overlay", role: "region", "aria-label": t("memory.view.label"), ref: panelRef },
|
|
2773
2945
|
h("div", { className: "mneme-overlaybar" },
|
|
2774
2946
|
h("span", { className: "mneme-overlaytitle" },
|
|
2775
|
-
|
|
2947
|
+
renderArchiveIcon({ size: 15 }),
|
|
2776
2948
|
t("memory.view.label")
|
|
2777
2949
|
),
|
|
2778
2950
|
h("button", {
|
|
@@ -3084,19 +3256,25 @@ window.__ModuleLoader__.load({
|
|
|
3084
3256
|
const [state, setState] = useState({ loading: true, error: false, lastRun: null, pending: 0 });
|
|
3085
3257
|
useEffect(() => {
|
|
3086
3258
|
let cancelled = false;
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3259
|
+
const loadOnce = () => {
|
|
3260
|
+
apiFetch("/api/dsh-mneme/dream-status")
|
|
3261
|
+
.then((res) => { if (!res.ok) throw new Error("http"); return res.json(); })
|
|
3262
|
+
.then((d) => {
|
|
3263
|
+
if (cancelled) return;
|
|
3264
|
+
setState({
|
|
3265
|
+
loading: false,
|
|
3266
|
+
error: false,
|
|
3267
|
+
lastRun: (d && d.lastRun) || null,
|
|
3268
|
+
pending: Number((d && d.pendingConflicts) ?? 0)
|
|
3269
|
+
});
|
|
3270
|
+
})
|
|
3271
|
+
.catch(() => { if (!cancelled) setState({ loading: false, error: true, lastRun: null, pending: 0 }); });
|
|
3272
|
+
};
|
|
3273
|
+
loadOnce();
|
|
3274
|
+
// #295 评审:裁决完成后队列会广播 mneme:conflicts-changed,这里重取
|
|
3275
|
+
// 计数,状态卡的「待确认冲突」不再停留旧值。
|
|
3276
|
+
window.addEventListener("mneme:conflicts-changed", loadOnce);
|
|
3277
|
+
return () => { cancelled = true; window.removeEventListener("mneme:conflicts-changed", loadOnce); };
|
|
3100
3278
|
}, []);
|
|
3101
3279
|
const run = state.lastRun;
|
|
3102
3280
|
const num = run ? formatRelativeTime(run.created_at, t) : t("memory.status.dreamNever");
|
|
@@ -3113,6 +3291,7 @@ window.__ModuleLoader__.load({
|
|
|
3113
3291
|
role: pending ? "button" : undefined,
|
|
3114
3292
|
tabIndex: pending ? 0 : undefined,
|
|
3115
3293
|
"aria-label": pending ? t("memory.status.conflictQueue.goto") : undefined,
|
|
3294
|
+
className: pending ? "mneme-conflict-jump" : undefined,
|
|
3116
3295
|
style: pending ? { cursor: "pointer" } : undefined,
|
|
3117
3296
|
onClick: pending ? gotoQueue : undefined,
|
|
3118
3297
|
onKeyDown: pending ? (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); gotoQueue(); } } : undefined
|
|
@@ -3205,15 +3384,19 @@ window.__ModuleLoader__.load({
|
|
|
3205
3384
|
// 队列为空时整块不渲染(不打扰无冲突实例)。
|
|
3206
3385
|
function ConflictsQueue({ t }) {
|
|
3207
3386
|
const [items, setItems] = useState(null);
|
|
3387
|
+
const [loadError, setLoadError] = useState(false);
|
|
3208
3388
|
const [busy, setBusy] = useState(false);
|
|
3209
|
-
const load = () => {
|
|
3389
|
+
const load = (silent) => {
|
|
3210
3390
|
apiFetch("/api/dsh-mneme/conflicts")
|
|
3211
3391
|
.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
|
-
|
|
3392
|
+
.then((d) => { setItems(d.items || []); setLoadError(false); if (!silent) announce(t("memory.status.conflictQueue.refreshed")); })
|
|
3393
|
+
// #295 评审:加载失败 ≠ 没有冲突——错误态渲染错误卡而不是空态教育卡,
|
|
3394
|
+
// 不能让 500 把「暂时看不到」伪装成「没有冲突」。
|
|
3395
|
+
.catch(() => { setItems([]); setLoadError(true); });
|
|
3214
3396
|
};
|
|
3215
3397
|
useEffect(() => { load(); }, []);
|
|
3216
|
-
|
|
3398
|
+
// #177:空队列不再整块消失——空态教育卡要渲染(原 return null 已移除)。
|
|
3399
|
+
if (!items) return null;
|
|
3217
3400
|
const resolve = (id, winner) => {
|
|
3218
3401
|
if (busy) return;
|
|
3219
3402
|
setBusy(true);
|
|
@@ -3228,37 +3411,93 @@ window.__ModuleLoader__.load({
|
|
|
3228
3411
|
.then(() => {
|
|
3229
3412
|
announce(t("memory.status.conflictQueue.resolved").replace("{name}",
|
|
3230
3413
|
(winner === "a" ? (it.memory_a && it.memory_a.title) : winner === "b" ? (it.memory_b && it.memory_b.title) : "") || ""));
|
|
3231
|
-
|
|
3414
|
+
// #295 评审:裁决改变了 pending 总数——DreamStatusCards 只在挂载时
|
|
3415
|
+
// 读一次 dream-status,不广播的话状态卡停留在旧计数。同页两个组件
|
|
3416
|
+
// 用一个 window 事件对齐(比提升状态到 StatusPanel 少动三处)。
|
|
3417
|
+
try { window.dispatchEvent(new CustomEvent("mneme:conflicts-changed")); } catch { /* 非浏览器环境 */ }
|
|
3418
|
+
load(true);
|
|
3232
3419
|
})
|
|
3233
3420
|
.catch(() => {})
|
|
3234
3421
|
.finally(() => setBusy(false));
|
|
3235
3422
|
};
|
|
3236
|
-
const side = (s, label) => h("div", { className:
|
|
3423
|
+
const side = (s, label, sideClass, ops, diffSide) => h("div", { className: `mneme-conflict-side ${sideClass}` },
|
|
3237
3424
|
h("div", { className: "mneme-conflict-sidelabel" }, label),
|
|
3238
3425
|
s.missing
|
|
3239
3426
|
? h("div", { className: "mneme-conflict-missing" }, t("memory.status.conflictQueue.missing"))
|
|
3240
3427
|
: h(react.Fragment, null,
|
|
3241
3428
|
h("div", { className: "mneme-conflict-sidetitle", title: s.title }, s.title || "…"),
|
|
3242
|
-
|
|
3243
|
-
|
|
3429
|
+
// #177:词级 diff 可用(LCS 无损对齐成功)时用高亮视图替代纯文本。
|
|
3430
|
+
// 侧别过滤(#295 评审修正):A 侧渲染 same+del(它被删的部分高亮),
|
|
3431
|
+
// B 侧渲染 same+ins(它新增的部分高亮)——每列忠实于自己的原文。
|
|
3432
|
+
// title 提示颜色语义;diff 不可用时保留原 snippet。
|
|
3433
|
+
ops
|
|
3434
|
+
? h("div", { className: "mneme-conflict-diff", title: t("memory.status.conflictQueue.diffTitle") },
|
|
3435
|
+
ops.map((o, k) => {
|
|
3436
|
+
if (o.kind === "same") return o.text;
|
|
3437
|
+
if (o.kind === "del" && diffSide === "a") {
|
|
3438
|
+
return h("span", { key: k, className: "mneme-conflict-mark mneme-conflict-mark--del" }, o.text);
|
|
3439
|
+
}
|
|
3440
|
+
if (o.kind === "ins" && diffSide === "b") {
|
|
3441
|
+
return h("span", { key: k, className: "mneme-conflict-mark mneme-conflict-mark--ins" }, o.text);
|
|
3442
|
+
}
|
|
3443
|
+
return null; // 对侧的编辑片段不出现在本列
|
|
3444
|
+
}))
|
|
3445
|
+
: h("div", { className: "mneme-conflict-snippet" }, (s.content || "").slice(0, 140)),
|
|
3446
|
+
// #177:预裁决阶段两侧都还活着——「已归档」徽章换成「冻结中」,
|
|
3447
|
+
// 原注销记说明(applyHint)挪进 tooltip,不再整段占一行动态区。
|
|
3448
|
+
h("span", {
|
|
3449
|
+
className: "mneme-badge mneme-badge--frozen",
|
|
3450
|
+
title: `${t("memory.status.conflictQueue.frozenTitle")} ${t("memory.status.conflictQueue.applyHintTitle")}`
|
|
3451
|
+
}, t("memory.status.conflictQueue.frozenBadge")))
|
|
3244
3452
|
);
|
|
3453
|
+
// #177:相似度从 reason 文本中回收(sleep 路径写「相似度 0.87」,dream 路径
|
|
3454
|
+
// 是 LLM 自由文本)——回收得到就画进度条,否则不画,绝不显示编造的数字。
|
|
3455
|
+
// 防误报:只认「相似度 0.87 / similarity 0.87」这类锚定短语,不在全文里
|
|
3456
|
+
// 捞数字(否则 reason 里的年份、条数都会被当成相似度)。
|
|
3457
|
+
const similarityOf = (reason) => {
|
|
3458
|
+
const m = /(?:相似度|similarity)\s*([01](?:\.\d+)?)/i.exec(String(reason || ""));
|
|
3459
|
+
const v = m ? Number(m[1]) : NaN;
|
|
3460
|
+
return v >= 0 && v <= 1 ? v : null;
|
|
3461
|
+
};
|
|
3245
3462
|
return h("div", { className: "mneme-conflictq" },
|
|
3246
3463
|
h("div", { className: "mneme-conflictq-head" },
|
|
3247
3464
|
h("span", { className: "mneme-xcount" }, t("memory.status.conflicts")),
|
|
3248
3465
|
h("button", { className: "mneme-footbtn", onClick: () => load() },
|
|
3249
3466
|
h(Icon, { name: "refresh", size: 12 }), t("memory.status.conflictQueue.refresh"))),
|
|
3250
|
-
|
|
3467
|
+
// #177 空态教育卡:队列没有条目时也渲染(整块此前直接 return null),
|
|
3468
|
+
// 解释「什么情况会产生冲突、冻结是什么」——新用户第一次遇见冻结时
|
|
3469
|
+
// 状态页已有解释在等他。加载失败时显示错误卡而不是空态卡。
|
|
3470
|
+
items.length === 0 && (loadError
|
|
3471
|
+
? h("div", { className: "mneme-conflict-empty", role: "alert" },
|
|
3472
|
+
h("div", { className: "mneme-conflict-empty-title" }, t("memory.status.error")))
|
|
3473
|
+
: h("div", { className: "mneme-conflict-empty" },
|
|
3474
|
+
h("div", { className: "mneme-conflict-empty-title" }, t("memory.status.conflictQueue.emptyTitle")),
|
|
3475
|
+
h("div", { className: "mneme-conflict-empty-body" }, t("memory.status.conflictQueue.emptyBody")))),
|
|
3476
|
+
items.map((it) => {
|
|
3477
|
+
const sim = similarityOf(it.reason);
|
|
3478
|
+
const aText = (it.memory_a && it.memory_a.content) || "";
|
|
3479
|
+
const bText = (it.memory_b && it.memory_b.content) || "";
|
|
3480
|
+
// diff 与相似度条互补:reason 给不出数字时才跑 LCS(两者表达同一信息)。
|
|
3481
|
+
const diff = (sim === null && !it.memory_a.missing && !it.memory_b.missing)
|
|
3482
|
+
? wordDiff(aText, bText) : null;
|
|
3483
|
+
return h("div", { key: it.id, className: "mneme-conflict-item" },
|
|
3251
3484
|
it.reason && h("div", { className: "mneme-conflict-reason" },
|
|
3252
3485
|
`${t("memory.status.conflictQueue.reason")}: ${it.reason}`),
|
|
3486
|
+
sim !== null && h("div", { className: "mneme-conflict-simrow" },
|
|
3487
|
+
h("span", null, t("memory.status.conflictQueue.similarity")),
|
|
3488
|
+
h("span", { className: "mneme-conflict-simbar" },
|
|
3489
|
+
h("span", { className: "mneme-conflict-simfill", style: { width: `${Math.round(sim * 100)}%` } })),
|
|
3490
|
+
h("span", null, `${Math.round(sim * 100)}%`)),
|
|
3253
3491
|
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"))),
|
|
3492
|
+
side(it.memory_a, t("memory.status.conflictQueue.sideA"), "mneme-conflict-side--a", diff, "a"),
|
|
3493
|
+
side(it.memory_b, t("memory.status.conflictQueue.sideB"), "mneme-conflict-side--b", diff, "b")),
|
|
3256
3494
|
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
|
-
|
|
3495
|
+
h("span", { className: "mneme-conflict-hint", title: t("memory.status.conflictQueue.applyHintTitle") },
|
|
3496
|
+
t("memory.status.conflictQueue.applyHint")),
|
|
3497
|
+
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")),
|
|
3498
|
+
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")),
|
|
3499
|
+
h("button", { className: "mneme-footbtn", disabled: busy, onClick: () => resolve(it.id, null) }, t("memory.status.conflictQueue.markReviewed"))));
|
|
3500
|
+
}));
|
|
3262
3501
|
}
|
|
3263
3502
|
|
|
3264
3503
|
// --- 状态页工作台:让用户切实看见插件在干活 ---
|
|
@@ -3446,7 +3685,7 @@ window.__ModuleLoader__.load({
|
|
|
3446
3685
|
// 口径不可信)或库为空时整卡不渲染,前端不感知;truncated(扫描超上限)
|
|
3447
3686
|
// 只影响 hint 里的回执计数,不挡渲染。
|
|
3448
3687
|
function RecallStatsCard({ t }) {
|
|
3449
|
-
const [state, setState] = useState({ loading: true, off: false, rate: null, zombie: 0, active: 0, exempt: 0, runs: 0, top: "", inject: "" });
|
|
3688
|
+
const [state, setState] = useState({ loading: true, off: false, rate: null, zombie: 0, active: 0, exempt: 0, runs: 0, top: "", inject: "", archive: "" });
|
|
3450
3689
|
useEffect(() => {
|
|
3451
3690
|
let cancelled = false;
|
|
3452
3691
|
apiFetch("/api/dsh-mneme/recall-stats?window=30")
|
|
@@ -3454,7 +3693,11 @@ window.__ModuleLoader__.load({
|
|
|
3454
3693
|
.then((d) => {
|
|
3455
3694
|
if (cancelled) return;
|
|
3456
3695
|
const z = d.zombie || {};
|
|
3457
|
-
|
|
3696
|
+
// 归档侧第五指标(#275)不能只靠 run/active 两路撑整张卡:整库归档是它的正常
|
|
3697
|
+
// 工作状态,那种库「窗口内没有召回回执、也没有活跃僵尸行」时若 hasData 为假,
|
|
3698
|
+
// 组件直接走 off 分支 return null,归档指标永远没机会显示。
|
|
3699
|
+
const hasData = (d.coverage?.runsScanned ?? 0) > 0 || (z.activeCount ?? 0) > 0
|
|
3700
|
+
|| (d.archive?.total ?? 0) > 0;
|
|
3458
3701
|
if (!hasData) {
|
|
3459
3702
|
setState({ loading: false, off: true });
|
|
3460
3703
|
return;
|
|
@@ -3471,10 +3714,19 @@ window.__ModuleLoader__.load({
|
|
|
3471
3714
|
.replace("{count}", String(inj.injectedCount ?? 0))
|
|
3472
3715
|
.replace("{fill}", inj.slotFillRate == null ? "—" : String(Math.round(inj.slotFillRate * 100)))
|
|
3473
3716
|
: "";
|
|
3717
|
+
// 第五指标(#275):归档净增速率 + 可压掉行数。与注入口径同款自门控
|
|
3718
|
+
// ——归档区为空时整段省略,不往卡里塞一个恒 0 的数字。
|
|
3719
|
+
const ar = d.archive || {};
|
|
3720
|
+
const archive = (ar.total ?? 0) > 0
|
|
3721
|
+
? t("memory.status.recallArchive")
|
|
3722
|
+
.replace("{total}", String(ar.total ?? 0))
|
|
3723
|
+
.replace("{add}", String(ar.perDay ?? 0))
|
|
3724
|
+
.replace("{compress}", String(ar.compressible?.rows ?? 0))
|
|
3725
|
+
: "";
|
|
3474
3726
|
setState({
|
|
3475
3727
|
loading: false, off: false, rate,
|
|
3476
3728
|
zombie: z.zombieCount ?? 0, active: z.activeCount ?? 0,
|
|
3477
|
-
exempt: z.exemptCount ?? 0, runs: d.coverage?.runsScanned ?? 0, top, inject
|
|
3729
|
+
exempt: z.exemptCount ?? 0, runs: d.coverage?.runsScanned ?? 0, top, inject, archive
|
|
3478
3730
|
});
|
|
3479
3731
|
})
|
|
3480
3732
|
.catch(() => { if (!cancelled) setState({ loading: false, off: true }); });
|
|
@@ -3493,7 +3745,7 @@ window.__ModuleLoader__.load({
|
|
|
3493
3745
|
.replace("{active}", String(state.active))
|
|
3494
3746
|
.replace("{exempt}", String(state.exempt))
|
|
3495
3747
|
.replace("{runs}", String(state.runs))
|
|
3496
|
-
.replace("{top}", state.top || "—") + (state.inject ? " · " + state.inject : "")
|
|
3748
|
+
.replace("{top}", state.top || "—") + (state.inject ? " · " + state.inject : "") + (state.archive ? " · " + state.archive : "")
|
|
3497
3749
|
});
|
|
3498
3750
|
}
|
|
3499
3751
|
|
|
@@ -4475,23 +4727,56 @@ window.__ModuleLoader__.load({
|
|
|
4475
4727
|
// 我们的修饰类覆盖配色(次级观感)。这样展开态与收起态(rail)都直接
|
|
4476
4728
|
// 继承宿主自己的盒模型与间距——宿主改版/缩进动画零位移,无需硬编码。
|
|
4477
4729
|
// 宿主结构异常时约 2 秒后放弃 portal,footer 回退按钮保持可用。
|
|
4730
|
+
// #177:未处理冲突数 badge——数据走 dream-status 的 pendingConflicts(状态卡
|
|
4731
|
+
// 同一端点),60s 轮询 + 打开面板即刷新。挂侧边栏入口按钮右上角(portal 与
|
|
4732
|
+
// 回退两处都挂),让用户不进面板也知道有冻结要裁决。
|
|
4733
|
+
function useConflictBadgeCount(t) {
|
|
4734
|
+
const [pending, setPending] = useState(0);
|
|
4735
|
+
useEffect(() => {
|
|
4736
|
+
let cancelled = false;
|
|
4737
|
+
const tick = () => {
|
|
4738
|
+
apiFetch("/api/dsh-mneme/dream-status")
|
|
4739
|
+
.then((res) => (res.ok ? res.json() : { pendingConflicts: 0 }))
|
|
4740
|
+
.then((d) => { if (!cancelled) setPending(Number((d && d.pendingConflicts) ?? 0) || 0); })
|
|
4741
|
+
.catch(() => { if (!cancelled) setPending(0); });
|
|
4742
|
+
};
|
|
4743
|
+
tick();
|
|
4744
|
+
const timer = setInterval(tick, 60_000);
|
|
4745
|
+
return () => { cancelled = true; clearInterval(timer); };
|
|
4746
|
+
}, []);
|
|
4747
|
+
return pending;
|
|
4748
|
+
}
|
|
4749
|
+
function ConflictBadge({ pending }) {
|
|
4750
|
+
if (!pending || pending <= 0) return null;
|
|
4751
|
+
return h("span", { className: "mneme-entrybadge", "aria-hidden": "true" },
|
|
4752
|
+
pending > 99 ? "99+" : String(pending));
|
|
4753
|
+
}
|
|
4478
4754
|
function SidebarFallbackTrigger({ wide, t }) {
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4755
|
+
const pending = useConflictBadgeCount(t);
|
|
4756
|
+
// #295 评审:.mneme-trigger 自身 overflow:hidden 且无定位上下文,badge
|
|
4757
|
+
// 直接放里面会被裁剪——包一层定位容器,badge 挂在容器上。
|
|
4758
|
+
return h("div", { className: "mneme-triggerwrap" },
|
|
4759
|
+
h("button", {
|
|
4760
|
+
type: "button",
|
|
4761
|
+
className: wide ? "mneme-trigger" : "mneme-trigger mneme-rail",
|
|
4762
|
+
"aria-label": pending > 0
|
|
4763
|
+
? `${t("memory.sidebar.aria")} · ${t("memory.status.conflicts")} ${pending}`
|
|
4764
|
+
: t("memory.sidebar.aria"),
|
|
4765
|
+
title: t("memory.panel.open"),
|
|
4766
|
+
onClick: openLibrary,
|
|
4767
|
+
"data-mneme-overlay-opener": "true"
|
|
4768
|
+
},
|
|
4769
|
+
renderArchiveIcon({ size: wide ? 16 : 18 }),
|
|
4770
|
+
wide && h("span", { className: "mneme-trigger-label" }, t("memory.panel.open"))
|
|
4771
|
+
),
|
|
4772
|
+
h(ConflictBadge, { pending })
|
|
4489
4773
|
);
|
|
4490
4774
|
}
|
|
4491
4775
|
|
|
4492
4776
|
function SidebarTopEntry({ wide, t, fallback }) {
|
|
4493
4777
|
const [host, setHost] = useState(null);
|
|
4494
4778
|
const [nativeCls, setNativeCls] = useState("");
|
|
4779
|
+
const pending = useConflictBadgeCount(t);
|
|
4495
4780
|
useEffect(() => {
|
|
4496
4781
|
if (!reactDom || typeof document === "undefined") return undefined;
|
|
4497
4782
|
let tries = 0, timer = null, created = null, mo = null;
|
|
@@ -4567,13 +4852,16 @@ window.__ModuleLoader__.load({
|
|
|
4567
4852
|
h("button", {
|
|
4568
4853
|
type: "button",
|
|
4569
4854
|
className: `${nativeCls} mneme-topentry-native`.trim(),
|
|
4570
|
-
"aria-label":
|
|
4855
|
+
"aria-label": pending > 0
|
|
4856
|
+
? `${t("memory.sidebar.aria")} · ${t("memory.status.conflicts")} ${pending}`
|
|
4857
|
+
: t("memory.sidebar.aria"),
|
|
4571
4858
|
title: t("memory.panel.open"),
|
|
4572
4859
|
onClick: openLibrary,
|
|
4573
4860
|
"data-mneme-overlay-opener": "true"
|
|
4574
4861
|
},
|
|
4575
|
-
|
|
4576
|
-
wide && h("span", { className: "mneme-topentry-label" }, t("memory.panel.open"))
|
|
4862
|
+
renderArchiveIcon({ size: wide ? 15 : 18 }),
|
|
4863
|
+
wide && h("span", { className: "mneme-topentry-label" }, t("memory.panel.open")),
|
|
4864
|
+
h(ConflictBadge, { pending })
|
|
4577
4865
|
)
|
|
4578
4866
|
), host);
|
|
4579
4867
|
}
|
|
@@ -4633,7 +4921,7 @@ window.__ModuleLoader__.load({
|
|
|
4633
4921
|
reg.registerTab({
|
|
4634
4922
|
id: TAB_ID,
|
|
4635
4923
|
title: () => t("memory.view.label"),
|
|
4636
|
-
icon: (size) =>
|
|
4924
|
+
icon: (size) => renderArchiveIcon({ size }),
|
|
4637
4925
|
order: 60,
|
|
4638
4926
|
component: () => h(MemoryExplorer, { t })
|
|
4639
4927
|
});
|