dsh-lite-memory 0.17.0 → 0.18.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +15 -15
  2. package/TRIGGERS.md +1 -1
  3. package/client.js +343 -44
  4. package/docs/BENCH.md +10 -8
  5. package/docs/RELEASE-0.18.0.md +35 -0
  6. package/docs/RELEASE-0.18.1.md +18 -0
  7. package/docs/RELEASE-0.18.10.md +25 -0
  8. package/docs/RELEASE-0.18.11.md +48 -0
  9. package/docs/RELEASE-0.18.12.md +27 -0
  10. package/docs/RELEASE-0.18.13.md +13 -0
  11. package/docs/RELEASE-0.18.14.md +13 -0
  12. package/docs/RELEASE-0.18.15.md +5 -0
  13. package/docs/RELEASE-0.18.2.md +21 -0
  14. package/docs/RELEASE-0.18.3.md +16 -0
  15. package/docs/RELEASE-0.18.4.md +15 -0
  16. package/docs/RELEASE-0.18.5.md +21 -0
  17. package/docs/RELEASE-0.18.6.md +18 -0
  18. package/docs/RELEASE-0.18.7.md +23 -0
  19. package/docs/RELEASE-0.18.8.md +18 -0
  20. package/docs/RELEASE-0.18.9.md +24 -0
  21. package/index.js +403 -131
  22. package/learning.js +65 -6
  23. package/package.json +7 -4
  24. package/state.js +56 -8
  25. package/store.js +361 -33
  26. package/test/client.test.mjs +79 -17
  27. package/test/contract.test.mjs +231 -93
  28. package/test/control.test.mjs +2 -2
  29. package/test/crossing.test.mjs +22 -1
  30. package/test/eval-seed.test.mjs +113 -0
  31. package/test/evolution-read.test.mjs +33 -10
  32. package/test/ha.test.mjs +186 -0
  33. package/test/layer-read.test.mjs +293 -0
  34. package/test/learning.test.mjs +80 -0
  35. package/test/palace.test.mjs +2 -0
  36. package/test/recall.test.mjs +80 -9
  37. package/test/record.test.mjs +130 -0
  38. package/test/safety.test.mjs +119 -0
  39. package/test/state.test.mjs +57 -1
  40. package/test/store.test.mjs +49 -5
  41. package/test/view.test.mjs +3 -0
  42. package/view.js +97 -13
package/client.js CHANGED
@@ -82,22 +82,26 @@ window.__ModuleLoader__.load({
82
82
  return meta;
83
83
  };
84
84
 
85
- const isMemoryId = (value) => typeof value === 'string' && /^mem_[a-z0-9]{6,}$/.test(value);
86
85
  const isCandidateId = (value) => typeof value === 'string' && /^sug_[a-z0-9]{6,}$/.test(value);
87
86
 
88
- /** 一条引用的规范形态:只有定位与版次。 */
87
+ /** 一条引用的规范形态:只有标题、范围与版次;id 不进界面。 */
89
88
  const citationOf = (meta, kind) => {
90
- if (!meta || !isMemoryId(meta.id)) return null;
89
+ if (!meta || typeof meta.title !== 'string' || !meta.title.trim()) return null;
91
90
  if (kind === 'memory-save' && meta.changed !== true) return null;
92
91
  return {
93
- id: meta.id,
94
- title: typeof meta.title === 'string' ? meta.title : '',
92
+ title: meta.title,
93
+ key: `${meta.scope === 'global' ? 'global' : 'project'}\u0000${meta.title}`, // ui-copy-ignore
95
94
  scope: meta.scope === 'global' ? 'global' : 'project',
96
95
  revision: Number.isInteger(meta.revision) ? meta.revision : 1,
97
96
  status: typeof meta.status === 'string' ? meta.status : 'ok',
98
97
  origin: meta.origin === 'project_evolution' ? 'project_evolution' : 'user_explicit',
99
98
  kind,
100
99
  exposure: kind === 'memory-save' || kind === 'memory-forget' ? 'update' : 'reference',
100
+ /** 条目类型:读正文看 recordKind,检索看 ref 自己的 kind;工具名不是类型。 */
101
+ entryKind: typeof meta.kind === 'string' && Object.prototype.hasOwnProperty.call(KIND_LABEL, meta.kind)
102
+ ? meta.kind
103
+ : (typeof meta.recordKind === 'string' ? meta.recordKind : ''),
104
+ updated: typeof meta.updated === 'string' ? meta.updated : '',
101
105
  reason: typeof meta.reason === 'string' ? meta.reason : '',
102
106
  reasonCode: typeof meta.reasonCode === 'string' ? meta.reasonCode : '',
103
107
  noTriggerReason: typeof meta.noTriggerReason === 'string' ? meta.noTriggerReason : '',
@@ -153,7 +157,7 @@ window.__ModuleLoader__.load({
153
157
  const seen = new Map();
154
158
  const out = [];
155
159
  for (const item of items) {
156
- const key = `${item.scope}:${item.id}:${item.exposure}`;
160
+ const key = `${item.scope}:${item.key}:${item.exposure}`;
157
161
  const at = seen.get(key);
158
162
  if (at === undefined) {
159
163
  seen.set(key, out.length);
@@ -253,9 +257,51 @@ window.__ModuleLoader__.load({
253
257
  };
254
258
  };
255
259
 
260
+ /** 一行展示的身份:曝光类型 + 文案 + 正文;拦截结果没有 id,只能按内容认。 */
261
+ const rowKeyOf = (toolName, block) => {
262
+ const text = settledText(block);
263
+ return text ? `${toolName}|${text}` : '';
264
+ };
265
+
266
+ /** 同一组行每次出现的去留:同键只留第一次。 */
267
+ const rowKeepFlags = (keys) => {
268
+ const seen = new Set();
269
+ return keys.map((key) => {
270
+ if (!key) return true;
271
+ if (seen.has(key)) return false;
272
+ seen.add(key);
273
+ return true;
274
+ });
275
+ };
276
+
256
277
  const Row = (props) => {
257
278
  const line = props.phase === 'preparing' ? null : firstLine(settledText(props.block));
258
- return h('div', { 'data-tool': props.toolName, style: ROW }, h('span', { style: DOT }, '·'), h('span', { style: LABEL }, line ?? RUNNING[props.toolName] ?? 'memory'));
279
+ const key = props.phase === 'preparing' ? '' : rowKeyOf(props.toolName, props.block);
280
+ const nodeRef = React.useRef(null);
281
+ const [dup, setDup] = React.useState(false);
282
+ const useLayout = React.useLayoutEffect || React.useEffect;
283
+ useLayout(() => {
284
+ const node = nodeRef.current;
285
+ if (!key || !node || typeof node.closest !== 'function') return;
286
+ const turnBox = node.closest('[data-chat-turn]');
287
+ const turn = turnBox ? turnBox.getAttribute('data-chat-turn') : null;
288
+ const rootScope = node.ownerDocument || node.parentElement;
289
+ if (!rootScope || typeof rootScope.querySelectorAll !== 'function') return;
290
+ const peers = Array.prototype.filter.call(rootScope.querySelectorAll('[data-memory-row]'), (peer) => {
291
+ if (!turn) return peer.parentElement === node.parentElement;
292
+ const box = peer.closest('[data-chat-turn]');
293
+ return box !== null && box.getAttribute('data-chat-turn') === turn;
294
+ });
295
+ const at = peers.indexOf(node);
296
+ if (at < 0) return;
297
+ setDup(!rowKeepFlags(peers.map((peer) => peer.getAttribute('data-memory-row') || ''))[at]);
298
+ }, [key]);
299
+ return h('div', {
300
+ ref: nodeRef,
301
+ 'data-tool': props.toolName,
302
+ 'data-memory-row': key || undefined,
303
+ style: dup ? { ...ROW, display: 'none' } : ROW,
304
+ }, h('span', { style: DOT }, '·'), h('span', { style: LABEL }, line ?? RUNNING[props.toolName] ?? 'memory'));
259
305
  };
260
306
 
261
307
  /** 建议行:候选 id 来自持久元数据,不读文案。 */
@@ -293,7 +339,7 @@ window.__ModuleLoader__.load({
293
339
  const undoRefOf = (meta, cwd) => (meta
294
340
  && meta.kind === 'memory-save'
295
341
  && meta.changed === true
296
- && isMemoryId(meta.id)
342
+ && typeof meta.title === 'string' && meta.title.trim().length > 0
297
343
  && Number.isInteger(meta.revision)
298
344
  && typeof cwd === 'string'
299
345
  && cwd.length > 0
@@ -312,7 +358,7 @@ window.__ModuleLoader__.load({
312
358
  const drop = () => {
313
359
  if (!canDrop) return;
314
360
  setState('saving');
315
- post('/lite-memory/undo', { op: 'undo', id: changed.id, revision: changed.revision, cwd }).then((result) => {
361
+ post('/lite-memory/undo', { op: 'undo', title: changed.title, revision: changed.revision, cwd }).then((result) => {
316
362
  if (succeeded(result)) {
317
363
  setState('dropped');
318
364
  setNote(result.body && result.body.mode === 'restored' ? 'memory · restored' : 'memory · archived');
@@ -464,7 +510,7 @@ window.__ModuleLoader__.load({
464
510
  if (ref.messageId !== undefined) return `消息 ${ref.messageId}`;
465
511
  if (ref.turn !== undefined) return `第 ${ref.turn} 轮`;
466
512
  if (ref.turnIndex !== undefined) return `第 ${ref.turnIndex} 轮`;
467
- return ref.id !== undefined ? String(ref.id) : `引用 ${index + 1}`;
513
+ return `引用 ${index + 1}`;
468
514
  };
469
515
  const refHrefOf = (ref) => (ref && typeof ref === 'object'
470
516
  ? [ref.href, ref.url, ref.link].find((value) => typeof value === 'string' && value)
@@ -540,13 +586,27 @@ window.__ModuleLoader__.load({
540
586
  const REF_META = { display: 'flex', alignItems: 'center', gap: '6px', margin: '0 0 2px 2px', fontSize: '11px', lineHeight: '18px', color: 'var(--dsw-alias-label-secondary)' };
541
587
  const REF_BODY = { margin: '2px 0 3px', padding: '3px 0 3px 9px', display: 'flex', flexDirection: 'column', gap: '3px', fontSize: '12px', lineHeight: '1.65', maxHeight: '180px', overflowY: 'auto' };
542
588
 
543
- /** 出处一句话:项目/全局、来源、状态。 */
589
+ /** 出处一句话:类型、项目/全局、来源、状态。 */
590
+ /** 只管最近一个月:更早或没时间就不占位。 */
591
+ const agoLabel = (value) => {
592
+ const at = Date.parse(value);
593
+ if (!Number.isFinite(at)) return '';
594
+ const days = Math.floor((Date.now() - at) / 86_400_000);
595
+ if (days <= 0) return '今天更新';
596
+ if (days === 1) return '昨天更新';
597
+ if (days < 30) return `${days} 天前更新`;
598
+ return '';
599
+ };
544
600
  const refMetaOf = (item) => {
545
- const parts = [item.scope === 'global' ? '全局' : '项目', item.origin === 'project_evolution' ? '自进化' : '你记的'];
601
+ /** 会话里只标层与来源:类型留给面板和工具,不占引用行的宽度。 */
602
+ const layer = `${item.scope === 'global' ? '[全局]' : '[项目]'}${item.origin === 'project_evolution' ? '[自进化]' : ''}`;
603
+ const parts = [];
604
+ const ago = agoLabel(item.updated);
605
+ if (ago) parts.push(ago);
546
606
  if (item.status === 'archived') parts.push('已归档');
547
607
  const processing = PROCESSING_LABEL[item.processingStatus] || '';
548
608
  if (processing) parts.push(processing);
549
- return parts.join(' · ');
609
+ return [layer, ...parts].join(' · ');
550
610
  };
551
611
 
552
612
  const RefChevron = (props) => h('svg', {
@@ -564,8 +624,8 @@ window.__ModuleLoader__.load({
564
624
  onMouseEnter: () => setHover(true),
565
625
  onMouseLeave: () => setHover(false),
566
626
  onClick: props.onShow, // ui-copy-ignore
567
- 'aria-label': `查看记忆 ${item.title || item.id}${reason ? `,${reason}` : ''}`,
568
- title: reason ? `${item.title || item.id},${reason}` : (item.title || item.id),
627
+ 'aria-label': `查看记忆 ${item.title || '记忆'}${reason ? `,${reason}` : ''}`,
628
+ title: reason ? `${item.title || '记忆'},${reason}` : (item.title || '记忆'),
569
629
  style: { // ui-copy-ignore
570
630
  display: 'flex', alignItems: 'flex-start', gap: '7px', width: '100%', minWidth: 0, textAlign: 'left',
571
631
  border: 'none', borderRadius: '8px', padding: '3px 7px', cursor: 'pointer', color: 'inherit',
@@ -575,7 +635,7 @@ window.__ModuleLoader__.load({
575
635
  },
576
636
  h('span', { 'aria-hidden': 'true', style: { flex: '0 0 auto', alignSelf: 'stretch', width: '2px', minHeight: '10px', borderRadius: '999px', background: props.tone, opacity: 0.85 } }),
577
637
  h('span', { style: { display: 'flex', flexDirection: 'column', gap: '1px', flex: '1 1 auto', minWidth: 0 } },
578
- h('span', { style: { minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, item.title || item.id),
638
+ h('span', { style: { minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, item.title || '记忆'),
579
639
  h('span', { style: { display: 'flex', alignItems: 'center', gap: '6px', minWidth: 0, fontSize: '11px', lineHeight: '16px', color: 'var(--dsw-alias-label-secondary)' } },
580
640
  reason ? h('span', { style: { minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, reason) : null,
581
641
  h('span', { style: { marginLeft: 'auto', flex: '0 0 auto', fontVariantNumeric: 'tabular-nums' } }, refMetaOf(item)),
@@ -614,28 +674,28 @@ window.__ModuleLoader__.load({
614
674
  setOpen(false);
615
675
  };
616
676
  const show = (item) => {
617
- if (shown && shown.id === item.id) {
677
+ if (shown && shown.key === item.key) {
618
678
  close();
619
679
  return;
620
680
  }
621
681
  const request = generation.current + 1;
622
682
  generation.current = request;
623
683
  setOpen(true);
624
- setShown({ id: item.id, ref: item, loading: true });
684
+ setShown({ key: item.key, ref: item, loading: true });
625
685
  setNote('');
626
- post('/lite-memory/body', { id: item.id, revision: item.revision, sessionId }).then((result) => {
686
+ post('/lite-memory/body', { title: item.title, scope: item.scope, revision: item.revision, sessionId }).then((result) => {
627
687
  if (!alive.current || generation.current !== request) return;
628
688
  const payload = result.ok && result.body ? result.body : null;
629
689
  if (!payload || payload.ok !== true) {
630
- setShown({ id: item.id, ref: item, invalid: true });
690
+ setShown({ key: item.key, ref: item, invalid: true });
631
691
  return;
632
692
  }
633
693
  if (payload.status === 'gone') {
634
- setShown({ id: item.id, ref: item, gone: true });
694
+ setShown({ key: item.key, ref: item, gone: true });
635
695
  return;
636
696
  }
637
697
  setShown({
638
- id: item.id,
698
+ key: item.key,
639
699
  ref: item,
640
700
  title: typeof payload.title === 'string' ? payload.title : '',
641
701
  when: typeof payload.when === 'string' ? payload.when : '',
@@ -651,11 +711,11 @@ window.__ModuleLoader__.load({
651
711
  });
652
712
  };
653
713
  const rows = open ? items.map((item) => h(RefRow, {
654
- key: `${item.scope}|${item.id}|${item.revision}`, // ui-copy-ignore
655
- item, tone, active: Boolean(shown && shown.id === item.id),
714
+ key: `${item.key}|${item.revision}`, // ui-copy-ignore
715
+ item, tone, active: Boolean(shown && shown.key === item.key),
656
716
  onShow: () => show(item),
657
717
  })) : [];
658
- const detail = shown && shown.ref && items.some((item) => item.id === shown.id && item.revision === shown.ref.revision)
718
+ const detail = shown && shown.ref && items.some((item) => item.key === shown.key && item.revision === shown.ref.revision)
659
719
  ? h('div', { style: { ...REF_BODY, borderLeft: `2px solid ${tone}` } },
660
720
  shown.loading ? h('span', { style: REF_META }, '读取中…') : null,
661
721
  shown.gone ? h('span', { style: REF_META }, '这条记忆已删除') : null,
@@ -770,6 +830,64 @@ window.__ModuleLoader__.load({
770
830
  return false;
771
831
  }
772
832
  };
833
+ /**
834
+ * 解释模式只在本机、按会话记;默认关,零命中原因不打扰。
835
+ * 面板开关与对话末尾共用同一条广播。
836
+ */
837
+ const explainBus = (() => {
838
+ const key = '__liteMemoryExplainBus'; // ui-copy-ignore
839
+ if (!(globalThis[key] instanceof Set)) globalThis[key] = new Set();
840
+ return globalThis[key];
841
+ })();
842
+ const explainKeyOf = (sessionId) => `lite-memory:explain:${sessionId || 'default'}`;
843
+ const explainModeOf = (sessionId) => {
844
+ const store = displayStoreOf();
845
+ if (!store) return false;
846
+ try {
847
+ return store.getItem(explainKeyOf(sessionId)) === '1';
848
+ } catch {
849
+ return false;
850
+ }
851
+ };
852
+ const setExplainMode = (sessionId, on) => {
853
+ const store = displayStoreOf();
854
+ if (!store) return false;
855
+ try {
856
+ store.setItem(explainKeyOf(sessionId), on === true ? '1' : '0');
857
+ } catch {
858
+ return false;
859
+ }
860
+ for (const listener of [...explainBus]) {
861
+ try {
862
+ listener();
863
+ } catch {
864
+ // 单个订阅坏掉不影响其它面板。
865
+ }
866
+ }
867
+ return true;
868
+ };
869
+ const watchExplain = (listener) => {
870
+ explainBus.add(listener);
871
+ return () => explainBus.delete(listener);
872
+ };
873
+ /**
874
+ * 面板只说判断结果,不说内部原因码;条数来自上一次自动召回的回执。
875
+ */
876
+ const recallLineOf = (receipt) => {
877
+ if (!receipt || typeof receipt !== 'object') return '';
878
+ const cards = Number.isInteger(receipt.cards) ? receipt.cards : 0;
879
+ const matched = Number.isInteger(receipt.matched) ? receipt.matched : 0;
880
+ const related = matched > 0 ? `${matched} 条相关记忆,` : '';
881
+ switch (receipt.reason) {
882
+ case 'hit': return cards > 0 ? `上次自动提醒 ${cards} 条` : '';
883
+ case 'budget': return `上次没提醒,${related}本轮自动提醒已用满`;
884
+ case 'needs_context': return `上次没提醒,${related}适用条件未确认`;
885
+ case 'no_overlap': return '上次没提醒,没有匹配的记忆';
886
+ case 'memory_off': return '上次没提醒,记忆已关闭';
887
+ case 'tools_only': return '上次没提醒,只在你问时检索';
888
+ default: return '';
889
+ }
890
+ };
773
891
  /** 功能关闭时视图统一回列表,不保留一个画不出来的页签。 */
774
892
  const graphViewOf = (enabled, view) => (enabled === true && view === 'graph' ? 'graph' : 'list');
775
893
  const viewModeOf = (display, view) => {
@@ -803,6 +921,15 @@ window.__ModuleLoader__.load({
803
921
  return reduced;
804
922
  };
805
923
 
924
+ /** 同标题(去空白后)出现几次;体检与重复标题筛选共用。 */
925
+ const titleCountsOf = (records) => {
926
+ const counts = new Map();
927
+ (Array.isArray(records) ? records : EMPTY).forEach((record) => {
928
+ const title = typeof record.title === 'string' ? record.title.trim() : '';
929
+ if (title) counts.set(title, (counts.get(title) || 0) + 1);
930
+ });
931
+ return counts;
932
+ };
806
933
  /** 标签/话题按精确值筛选;只有自由搜索才用包含匹配。 */
807
934
  const filterRecords = (records, query, hub) => {
808
935
  const rows = Array.isArray(records) ? records : EMPTY;
@@ -814,6 +941,15 @@ window.__ModuleLoader__.load({
814
941
  if (kind === 'topic' && value) {
815
942
  return rows.filter((record) => record.topicKey === value);
816
943
  }
944
+ if (kind === 'duplicate' || kind === 'unread' || kind === 'noWhen') {
945
+ const counts = titleCountsOf(rows);
946
+ return rows.filter((record) => {
947
+ if (kind === 'unread') return !record.lastUsed;
948
+ if (kind === 'noWhen') return !record.when;
949
+ const title = typeof record.title === 'string' ? record.title.trim() : '';
950
+ return Boolean(title) && counts.get(title) > 1;
951
+ });
952
+ }
817
953
  const needle = typeof query === 'string' ? query.trim().toLowerCase() : '';
818
954
  if (!needle) return rows;
819
955
  return rows.filter((record) => {
@@ -822,6 +958,21 @@ window.__ModuleLoader__.load({
822
958
  });
823
959
  };
824
960
 
961
+ /** 结构体检:只看现行记录,缺的数据给 null,不猜。 */
962
+ const checkupOf = (records, scopes, globalSelected) => {
963
+ const rows = Array.isArray(records) ? records : EMPTY;
964
+ const counts = titleCountsOf(rows);
965
+ let duplicate = 0;
966
+ counts.forEach((count) => { if (count > 1) duplicate += 1; });
967
+ const globalRow = (Array.isArray(scopes) ? scopes : EMPTY).find((scope) => scope && scope.kind === 'global');
968
+ return {
969
+ duplicate,
970
+ unread: rows.filter((record) => !record.lastUsed).length,
971
+ globalEntries: globalRow && Number.isInteger(globalRow.entries) ? globalRow.entries : null,
972
+ globalMissingWhen: globalSelected ? rows.filter((record) => !record.when).length : null,
973
+ };
974
+ };
975
+
825
976
  /** 记忆宫殿:房间固定,槽位固定,换项目只换陈列内容。 */
826
977
  const PALACE_PAGE_SIZE = 24;
827
978
  const PALACE_PREVIEW = 4;
@@ -1091,7 +1242,7 @@ window.__ModuleLoader__.load({
1091
1242
  edges.push({ from, to, type });
1092
1243
  };
1093
1244
  for (const record of rows) {
1094
- nodes.push({ id: record.id, kind: 'memory', label: record.title || record.id, experimental: record.experimental === true });
1245
+ nodes.push({ id: record.id, kind: 'memory', label: record.title || '记忆', experimental: record.experimental === true });
1095
1246
  }
1096
1247
  for (const [tag, count] of tags) {
1097
1248
  nodes.push({ id: `tag:${tag}`, kind: 'tag', label: `#${tag}`, count });
@@ -1490,13 +1641,15 @@ window.__ModuleLoader__.load({
1490
1641
  },
1491
1642
  }));
1492
1643
 
1493
- const STAGE = { pending: '候选', trial: '试用', active: '生效', paused: '暂停' };
1644
+ const STAGE = { pending: '候选', trial: '试用', active: '生效', paused: '暂停', legacy: '待复核' };
1494
1645
  const STAGE_TONE = {
1495
1646
  pending: 'var(--dsw-alias-state-warn-primary)',
1496
1647
  trial: 'var(--dsw-alias-brand-primary)',
1497
1648
  active: 'var(--dsw-alias-state-success-primary)',
1498
1649
  paused: 'var(--dsw-alias-label-secondary)',
1650
+ legacy: 'var(--dsw-alias-state-warn-primary)',
1499
1651
  };
1652
+ const stageOf = (rule) => (rule.reviewState === 'legacy_unreviewed' ? 'legacy' : rule.state);
1500
1653
  const StagePill = (props) => h('span', {
1501
1654
  style: {
1502
1655
  flex: '0 0 auto', padding: '0 6px', border: '1px solid var(--dsw-alias-border-l1)',
@@ -1533,6 +1686,7 @@ window.__ModuleLoader__.load({
1533
1686
  const usabilityOf = (rule, evolution, memoryOn) => {
1534
1687
  if (!memoryOn) return '记忆已关闭,暂不参与回答。';
1535
1688
  if (!evolution || evolution.enabled !== true || evolution.effectiveEnabled === false) return '自进化已关闭,暂不参与回答。';
1689
+ if (rule.reviewState === 'legacy_unreviewed') return '所属项目还没确认,暂不参与回答。';
1536
1690
  if (rule.state === 'paused') return '已暂停,不参与回答。';
1537
1691
  const unresolved = rule.applicability === 'unresolved'
1538
1692
  || (rule.applicability === 'conditional' && (!Array.isArray(rule.conditions) || rule.conditions.length === 0));
@@ -1589,7 +1743,7 @@ window.__ModuleLoader__.load({
1589
1743
  color: 'inherit', padding: '4px 8px', cursor: 'pointer', minWidth: 0,
1590
1744
  },
1591
1745
  },
1592
- h('span', { style: { fontSize: '12px', lineHeight: '16px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, record.title || record.id),
1746
+ h('span', { style: { fontSize: '12px', lineHeight: '16px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, record.title || '记忆'),
1593
1747
  h('span', { style: { ...PANEL_MUTED, display: 'flex', alignItems: 'center', gap: '5px', minWidth: 0, lineHeight: '14px' } },
1594
1748
  h('span', null, KIND_LABEL[record.kind] || record.kind),
1595
1749
  record.experimental ? h('span', { style: { color: 'var(--dsw-alias-state-warn-primary)' } }, '实验') : null,
@@ -1616,7 +1770,7 @@ window.__ModuleLoader__.load({
1616
1770
  },
1617
1771
  h('span', { style: { fontSize: '12px', lineHeight: '18px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, rule.title),
1618
1772
  h('span', { style: { ...PANEL_MUTED, display: 'flex', alignItems: 'center', gap: '6px', minWidth: 0, lineHeight: '16px' } },
1619
- h(StagePill, { state: rule.state }),
1773
+ h(StagePill, { state: stageOf(rule) }),
1620
1774
  h('span', { style: { marginLeft: 'auto', flex: '0 0 auto', fontVariantNumeric: 'tabular-nums' } }, EVO_DATE(rule.updated)),
1621
1775
  ));
1622
1776
  };
@@ -1655,7 +1809,7 @@ window.__ModuleLoader__.load({
1655
1809
  ? h('div', { style: { display: 'flex', alignItems: 'center', gap: '12px', flexWrap: 'wrap', paddingTop: '3px' } },
1656
1810
  rule.state === 'pending' || rule.state === 'trial'
1657
1811
  ? h(Action, { disabled: !canManage, onClick: () => act('adopt') }, '采纳') : null,
1658
- rule.state === 'paused'
1812
+ rule.state === 'paused' && rule.reviewState !== 'legacy_unreviewed'
1659
1813
  ? h(Action, { disabled: !canManage, onClick: () => act('resume') }, '恢复')
1660
1814
  : (rule.state === 'trial' || rule.state === 'active'
1661
1815
  ? h(Action, { disabled: !canManage, onClick: () => act('pause') }, '暂停')
@@ -1742,7 +1896,7 @@ window.__ModuleLoader__.load({
1742
1896
  h('div', { style: { display: 'flex', flexDirection: 'column', gap: '4px' } },
1743
1897
  h('div', { style: { display: 'flex', alignItems: 'center', gap: '6px' } },
1744
1898
  h('span', { style: PANEL_LABEL }, '当前状态'),
1745
- h(StagePill, { state: rule.state }),
1899
+ h(StagePill, { state: stageOf(rule) }),
1746
1900
  ),
1747
1901
  h('div', { style: { fontSize: '12px', lineHeight: '1.55', opacity: 0.82 } }, usabilityOf(rule, props.evolution, props.memoryOn)),
1748
1902
  failureState
@@ -1770,7 +1924,7 @@ window.__ModuleLoader__.load({
1770
1924
  style: { height: '46px', border: '1px dashed var(--dsw-alias-border-l1)', borderRadius: '8px', opacity: 0.28 },
1771
1925
  });
1772
1926
  }
1773
- const title = String(record.title || record.id || '记忆');
1927
+ const title = String(record.title || '记忆');
1774
1928
  return h('button', {
1775
1929
  type: 'button', title, 'aria-label': title,
1776
1930
  onClick: () => props.onPick(record.id),
@@ -1948,11 +2102,17 @@ window.__ModuleLoader__.load({
1948
2102
  const [selectedRule, setSelectedRule] = React.useState('');
1949
2103
  const [detail, setDetail] = React.useState(null);
1950
2104
  const [detailNote, setDetailNote] = React.useState('');
2105
+ const [editingMemory, setEditingMemory] = React.useState(false);
2106
+ const [editTitle, setEditTitle] = React.useState('');
2107
+ const [editBody, setEditBody] = React.useState('');
2108
+ const [editNote, setEditNote] = React.useState('');
1951
2109
  const [busy, setBusy] = React.useState(false);
1952
2110
  const [showReason, setShowReason] = React.useState(false);
1953
2111
  const [showSettings, setShowSettings] = React.useState(false);
2112
+ const [explainOn, setExplainOn] = React.useState(explainModeOf(sessionId));
1954
2113
  const [display, setDisplay] = React.useState({ ...DISPLAY_DEFAULTS });
1955
2114
  const [hubFilter, setHubFilter] = React.useState(null);
2115
+ const [checkup, setCheckup] = React.useState(false);
1956
2116
  const alive = React.useRef(true);
1957
2117
  const generation = React.useRef(0);
1958
2118
  const scopeRef = React.useRef('');
@@ -1966,6 +2126,8 @@ window.__ModuleLoader__.load({
1966
2126
  const load = (scopeKey = '', focusRule = '') => {
1967
2127
  const request = generation.current + 1;
1968
2128
  generation.current = request;
2129
+ /** 用户选的项目立刻算数:晚到的保存/开关回执不能再把界面拉回旧范围。 */
2130
+ if (scopeKey) scopeRef.current = scopeKey;
1969
2131
  setLoading(true);
1970
2132
  setFailed(false);
1971
2133
  setNote('');
@@ -2023,6 +2185,8 @@ window.__ModuleLoader__.load({
2023
2185
  }, []);
2024
2186
 
2025
2187
  React.useEffect(() => {
2188
+ setEditingMemory(false);
2189
+ setEditNote('');
2026
2190
  if (!selectedMemory || !data) { setDetail(null); setDetailNote(''); return undefined; }
2027
2191
  let live = true;
2028
2192
  setDetail(null);
@@ -2088,7 +2252,7 @@ window.__ModuleLoader__.load({
2088
2252
  } else if (activityCode === 'scope_unconfirmed' || activityCode === 'unknown_scope') {
2089
2253
  activityText = '暂未整理:适用范围待确认';
2090
2254
  } else if (activityCode === 'quota' || activityCode === 'exhausted') {
2091
- activityText = '本轮整理额度已用完';
2255
+ activityText = '这轮先整理这些,下一条消息接着整理';
2092
2256
  } else if (activityCode === 'failed' || activityCode === 'error') {
2093
2257
  activityText = staleLearning ? '这轮整理没采用' : '最近整理失败,可查看原因';
2094
2258
  } else if (activityCode === 'waiting' || activityCode === 'idle' || activityCode === 'empty') {
@@ -2109,6 +2273,25 @@ window.__ModuleLoader__.load({
2109
2273
  return rules.filter((rule) => rule.state === stage);
2110
2274
  }, [rules, stage]);
2111
2275
  const selectedRuleRow = selectedRule ? rules.find((rule) => rule.id === selectedRule) : null;
2276
+ const checkupData = checkupOf(records, data && data.scopes, globalView);
2277
+ const globalScopeRow = (data && Array.isArray(data.scopes) ? data.scopes : EMPTY).find((scope) => scope && scope.kind === 'global');
2278
+ const checkupJump = (hub) => { setCheckup(false); setTab('memory'); setQuery(''); setHubFilter(hub); };
2279
+ const checkupJumpGlobal = (hub) => {
2280
+ setCheckup(false); setQuery(''); setHubFilter(hub || null);
2281
+ if (globalScopeRow) load(globalScopeRow.key);
2282
+ else setTab('memory');
2283
+ };
2284
+ const checkupRowOf = (key, label, value, unit, onClick) => h('button', {
2285
+ key, type: 'button', onClick,
2286
+ style: {
2287
+ display: 'flex', alignItems: 'center', gap: '8px', width: '100%', minWidth: 0,
2288
+ padding: '7px 9px', border: 'none', background: 'transparent', color: 'inherit',
2289
+ font: 'inherit', textAlign: 'left', cursor: 'pointer',
2290
+ borderBottom: '1px solid var(--dsw-alias-border-l1)',
2291
+ },
2292
+ },
2293
+ h('span', { style: { minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, label),
2294
+ h('span', { style: { marginLeft: 'auto', flex: '0 0 auto', fontVariantNumeric: 'tabular-nums', opacity: 0.72 } }, value === null || value === undefined ? '—' : `${value} ${unit}`));
2112
2295
  const detailRecord = selectedMemory ? records.find((record) => record.id === selectedMemory) : null;
2113
2296
 
2114
2297
  const onPickNode = (node) => {
@@ -2149,18 +2332,64 @@ window.__ModuleLoader__.load({
2149
2332
  };
2150
2333
  if (learningAction === 'pause') payload.ids = [selectedRuleRow.id];
2151
2334
  post('/lite-memory/evolution', payload).then((result) => {
2152
- if (!alive.current) return;
2335
+ if (!alive.current || (selectedKey && scopeRef.current !== selectedKey)) return;
2153
2336
  setBusy(false);
2154
2337
  if (succeeded(result)) load(selectedKey, selectedRuleRow.id);
2155
2338
  else setNote(failure(result, '这次没有改动'));
2156
2339
  });
2157
2340
  };
2158
2341
 
2342
+ /** 普通记忆的正文由人直接改:带读到的版本写,冲突就提示刷新,不覆盖别处的修改。 */
2343
+ const saveMemoryEdit = (record) => {
2344
+ if (busy || !canManage || !record || !record.id) return;
2345
+ const title = editTitle.trim();
2346
+ const text = editBody.trim();
2347
+ if (!title || !text) return;
2348
+ setBusy(true);
2349
+ setEditNote('');
2350
+ const payload = { sessionId, scopeKey: selectedKey, id: record.id, title, body: text, expectedRevision: record.revision };
2351
+ post('/lite-memory/entry', payload).then((result) => {
2352
+ if (!alive.current || (selectedKey && scopeRef.current !== selectedKey)) return;
2353
+ setBusy(false);
2354
+ if (succeeded(result)) {
2355
+ const nextTitle = result.body && typeof result.body.title === 'string' ? result.body.title : title;
2356
+ const nextRevision = result.body && Number.isInteger(result.body.revision) ? result.body.revision : record.revision;
2357
+ setEditingMemory(false);
2358
+ setDetail((prev) => (prev ? { ...prev, title: nextTitle, body: text, currentBody: text, revision: nextRevision } : prev));
2359
+ setData((prev) => {
2360
+ if (!prev || !prev.selected || !Array.isArray(prev.selected.records)) return prev;
2361
+ const records = prev.selected.records.map((row) => (row.id === record.id ? { ...row, title: nextTitle, revision: nextRevision } : row));
2362
+ return { ...prev, selected: { ...prev.selected, records } };
2363
+ });
2364
+ setEditNote('');
2365
+ } else {
2366
+ setEditNote(failure(result, '这次没有改动'));
2367
+ /**
2368
+ * 版本过期不是失败终点:把最新正文和版本取回来,草稿留着,
2369
+ * 人确认后再按一次保存就写在最新版本上。
2370
+ */
2371
+ if (result.status === 409 && result.body && result.body.retry) {
2372
+ post('/lite-memory/body', { sessionId, id: record.id, scopeKey: selectedKey }).then((fresh) => {
2373
+ if (!alive.current || (selectedKey && scopeRef.current !== selectedKey)
2374
+ || !fresh.ok || !fresh.body || !fresh.body.ok || fresh.body.status === 'gone') return;
2375
+ const revision = Number.isInteger(fresh.body.revision) ? fresh.body.revision : 0;
2376
+ setDetail(fresh.body);
2377
+ setData((prev) => {
2378
+ if (!prev || !prev.selected || !Array.isArray(prev.selected.records)) return prev;
2379
+ const records = prev.selected.records.map((row) => (row.id === record.id ? { ...row, revision: revision || row.revision } : row));
2380
+ return { ...prev, selected: { ...prev.selected, records } };
2381
+ });
2382
+ });
2383
+ }
2384
+ }
2385
+ });
2386
+ };
2387
+
2159
2388
  const toggleControl = () => {
2160
2389
  if (busy || !canToggle) return;
2161
2390
  setBusy(true);
2162
2391
  post('/lite-memory/control', { sessionId, enabled: !memoryOn, operationId: newOperationId() }).then((result) => {
2163
- if (!alive.current) return;
2392
+ if (!alive.current || (selectedKey && scopeRef.current !== selectedKey)) return;
2164
2393
  setBusy(false);
2165
2394
  if (succeeded(result)) load(selectedKey);
2166
2395
  else setNote(failure(result, '开关没有改动'));
@@ -2171,7 +2400,7 @@ window.__ModuleLoader__.load({
2171
2400
  if (busy || !evolution || !canToggle || !memoryOn) return;
2172
2401
  setBusy(true);
2173
2402
  post('/lite-memory/evolution', { sessionId, enabled: evolution.enabled !== true, operationId: newOperationId() }).then((result) => {
2174
- if (!alive.current) return;
2403
+ if (!alive.current || (selectedKey && scopeRef.current !== selectedKey)) return;
2175
2404
  setBusy(false);
2176
2405
  if (succeeded(result)) load(selectedKey);
2177
2406
  else setNote(failure(result, '开关没有改动'));
@@ -2219,6 +2448,18 @@ window.__ModuleLoader__.load({
2219
2448
  selectedKey ? () => setDisplayFlag(key, display[key] !== true) : undefined,
2220
2449
  );
2221
2450
 
2451
+ const explainToggle = () => switchItem(
2452
+ '解释模式',
2453
+ explainOn ? '零命中时说明原因' : '零命中时不出提示',
2454
+ explainOn,
2455
+ !sessionId,
2456
+ sessionId ? () => {
2457
+ const next = explainOn !== true;
2458
+ setExplainOn(next);
2459
+ setExplainMode(sessionId, next);
2460
+ } : undefined,
2461
+ );
2462
+
2222
2463
  const switchItem = (label, hint, on, disabled, onClick) => h('div', {
2223
2464
  style: { display: 'flex', flexDirection: 'column', gap: '1px', minWidth: 0, opacity: disabled && !on ? 0.55 : 1 },
2224
2465
  },
@@ -2228,6 +2469,7 @@ window.__ModuleLoader__.load({
2228
2469
  ),
2229
2470
  hint ? h('span', { style: { ...PANEL_MUTED, lineHeight: '16px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, hint) : null);
2230
2471
  const evolutionOn = Boolean(memoryOn && evolution && evolution.enabled === true);
2472
+ const recallLine = recallLineOf(current && current.recall);
2231
2473
  const switchBar = h('div', {
2232
2474
  style: { display: 'flex', flexDirection: 'column', gap: '3px', padding: '7px 18px 8px', borderBottom: '1px solid var(--dsw-alias-border-l1)', flex: '0 0 auto' },
2233
2475
  },
@@ -2238,9 +2480,11 @@ window.__ModuleLoader__.load({
2238
2480
  switchItem('自进化', evolutionOn ? '实验功能,会增加 token 消耗' : '从反馈里整理经验', evolutionOn, busy || !evolution || !canToggle || !memoryOn, toggleEvolution), // ui-copy-ignore 用户指定文案
2239
2481
  showSettings ? displayToggle('启用图谱', 'graph') : null,
2240
2482
  showSettings ? displayToggle('启用记忆宫殿', 'palace') : null,
2483
+ showSettings ? explainToggle() : null,
2241
2484
  ),
2242
2485
  !globalView && !canToggle ? h('div', { style: { ...PANEL_MUTED, lineHeight: '18px' } }, '只读,切换回当前项目后可修改') : null,
2243
2486
  note ? h('div', { style: { ...PANEL_MUTED, lineHeight: '18px', color: 'var(--dsw-alias-state-error-primary)' } }, note) : null,
2487
+ !globalView && recallLine ? h('div', { style: { ...PANEL_MUTED, lineHeight: '18px' } }, recallLine) : null,
2244
2488
  );
2245
2489
 
2246
2490
  const switchTab = (key) => {
@@ -2278,6 +2522,25 @@ window.__ModuleLoader__.load({
2278
2522
  }));
2279
2523
 
2280
2524
  const detailProcessing = detail ? processingStatusOf(detail) : '';
2525
+ const memoryEditor = editingMemory && detailRecord
2526
+ ? h('div', { style: { display: 'flex', flexDirection: 'column', gap: '8px' } },
2527
+ h(Field, { label: '标题', value: editTitle, onChange: setEditTitle }),
2528
+ h('label', { style: { display: 'flex', flexDirection: 'column', gap: '4px' } },
2529
+ h('span', { style: PANEL_LABEL }, '经验'),
2530
+ h('textarea', {
2531
+ value: editBody, onChange: (event) => setEditBody(event.target.value), 'aria-label': '经验',
2532
+ rows: 8,
2533
+ style: { ...PANEL_INPUT, height: 'auto', minHeight: '150px', padding: '7px 9px', resize: 'vertical', lineHeight: '1.6' },
2534
+ }),
2535
+ ),
2536
+ editNote ? h('div', { style: { ...PANEL_MUTED, color: 'var(--dsw-alias-label-error)' } }, editNote) : null,
2537
+ h('div', { style: { display: 'flex', alignItems: 'center', gap: '12px', flexWrap: 'wrap' } },
2538
+ h(Action, { disabled: busy || !editTitle.trim() || !editBody.trim(), onClick: () => saveMemoryEdit(detailRecord) }, '保存'),
2539
+ h(Action, { disabled: busy, onClick: () => { setEditingMemory(false); setEditNote(''); } }, '取消'),
2540
+ ),
2541
+ )
2542
+ : null;
2543
+
2281
2544
  const memoryDetail = selectedMemory
2282
2545
  ? h('div', { style: { ...PANEL_CARD, minHeight: 0, overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: '6px' } },
2283
2546
  detail
@@ -2290,8 +2553,22 @@ window.__ModuleLoader__.load({
2290
2553
  Array.isArray(detailRecord && detailRecord.tags) && detailRecord.tags.length
2291
2554
  ? h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: '4px' } },
2292
2555
  detailRecord.tags.slice(0, 6).map((tag) => h('span', { key: tag, style: { ...PANEL_MUTED, padding: '0 6px', border: '1px solid var(--dsw-alias-border-l1)', borderRadius: '999px' } }, `#${tag}`))) : null,
2293
- h('div', { style: { fontSize: '12px', lineHeight: '1.65', whiteSpace: 'pre-wrap', wordBreak: 'break-word' } }, currentBodyOf(detail) || '没有正文'),
2294
- h(DetailExtras, { payload: detail }),
2556
+ memoryEditor,
2557
+ editingMemory ? null : h('div', { style: { fontSize: '12px', lineHeight: '1.65', whiteSpace: 'pre-wrap', wordBreak: 'break-word' } }, currentBodyOf(detail) || '没有正文'),
2558
+ editingMemory ? null : h(DetailExtras, { payload: detail }),
2559
+ !editingMemory && detailRecord && canManage
2560
+ ? h('div', { style: { display: 'flex', alignItems: 'center', gap: '12px', flexWrap: 'wrap', paddingTop: '2px' } },
2561
+ h(Action, {
2562
+ disabled: busy,
2563
+ onClick: () => {
2564
+ setEditTitle(detailRecord.title || detail.title || '');
2565
+ setEditBody(currentBodyOf(detail) || '');
2566
+ setEditNote('');
2567
+ setEditingMemory(true);
2568
+ },
2569
+ }, '修改'),
2570
+ )
2571
+ : null,
2295
2572
  )
2296
2573
  : h('div', { style: PANEL_MUTED }, detailNote || '读取中…'),
2297
2574
  )
@@ -2388,7 +2665,15 @@ window.__ModuleLoader__.load({
2388
2665
  fontVariantNumeric: 'tabular-nums',
2389
2666
  },
2390
2667
  }, `${label} ${countText(key)}`)),
2391
- h('span', { style: { marginLeft: 'auto' } }, display.graph ? viewToggleOf(evolutionView, setEvolutionView, '自进化视图') : null),
2668
+ h('span', { style: { marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: '8px' } },
2669
+ h('button', {
2670
+ type: 'button', 'aria-pressed': checkup, onClick: () => setCheckup((value) => !value),
2671
+ style: {
2672
+ ...PANEL_LINK, height: '30px', opacity: checkup ? 0.92 : 0.68,
2673
+ borderBottom: checkup ? '1px solid currentColor' : '1px solid transparent',
2674
+ },
2675
+ }, '结构体检'),
2676
+ display.graph ? viewToggleOf(evolutionView, setEvolutionView, '自进化视图') : null),
2392
2677
  );
2393
2678
 
2394
2679
  const stageLabel = (stageFilters.find(([key]) => key === stage) || ['all', '全部'])[1];
@@ -2439,6 +2724,18 @@ window.__ModuleLoader__.load({
2439
2724
  emptyText: evolutionEmptyText, ariaLabel: '经验关系图',
2440
2725
  legend: '实心 经验 · 空心 条件 · 虚线 共用条件 · 点经验查看',
2441
2726
  }));
2727
+ const evolutionCheckupPane = h('div', {
2728
+ style: {
2729
+ ...PANEL_CARD, padding: '4px', flex: '0 0 auto', minHeight: 0, overflowY: 'auto',
2730
+ display: 'flex', flexDirection: 'column', gap: 0,
2731
+ },
2732
+ },
2733
+ checkupRowOf('duplicate', '重复标题', checkupData.duplicate, '组', () => checkupJump({ kind: 'duplicate', display: '重复标题' })),
2734
+ checkupRowOf('global', '全局条目超 200', checkupData.globalEntries, '条', () => checkupJumpGlobal()),
2735
+ checkupRowOf('globalWhen', '全局缺适用条件', checkupData.globalMissingWhen, '条',
2736
+ () => checkupJumpGlobal({ kind: 'noWhen', display: '全局缺适用条件' })),
2737
+ checkupRowOf('unread', '从未读过', checkupData.unread, '条', () => checkupJump({ kind: 'unread', display: '从未读过' })));
2738
+
2442
2739
  const evolutionTab = evolutionFailed ? evolutionFailed : h('div', { role: 'tabpanel', 'aria-label': '自进化', style: { flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', gap: '9px' } },
2443
2740
  h('div', { style: { ...PANEL_CARD, padding: '9px 11px', display: 'flex', alignItems: 'flex-start', gap: '8px', flex: '0 0 auto' } },
2444
2741
  h('span', { style: { flex: '0 0 auto', width: '6px', height: '6px', marginTop: '5px', borderRadius: '999px', background: effectiveEnabled ? 'var(--dsw-alias-brand-primary)' : 'var(--dsw-alias-label-secondary)', opacity: 0.8 } }),
@@ -2456,11 +2753,11 @@ window.__ModuleLoader__.load({
2456
2753
  h('div', {
2457
2754
  style: {
2458
2755
  flex: 1, minHeight: 0, display: 'grid', gap: '10px',
2459
- gridTemplateColumns: selectedRuleRow ? 'minmax(0, 1fr) minmax(280px, 340px)' : 'minmax(0, 1fr)',
2756
+ gridTemplateColumns: !checkup && selectedRuleRow ? 'minmax(0, 1fr) minmax(280px, 340px)' : 'minmax(0, 1fr)',
2460
2757
  },
2461
2758
  },
2462
- evolutionViewActive === 'graph' ? evolutionGraphPane : evolutionListPane,
2463
- evolutionDetailPane),
2759
+ checkup ? evolutionCheckupPane : (evolutionViewActive === 'graph' ? evolutionGraphPane : evolutionListPane),
2760
+ checkup ? null : evolutionDetailPane),
2464
2761
  );
2465
2762
 
2466
2763
  const palaceLoose = activeTab === 'memory' && viewModeOf(display, memoryView) === 'palace' && !selectedMemory;
@@ -2528,12 +2825,14 @@ window.__ModuleLoader__.load({
2528
2825
  update: list.filter((item) => item.exposure === 'update'),
2529
2826
  };
2530
2827
  }, [refs]);
2828
+ const [explainOn, setExplainOn] = React.useState(explainModeOf(sessionId));
2829
+ React.useEffect(() => watchExplain(() => setExplainOn(explainModeOf(sessionId))), [sessionId]);
2531
2830
  const noTriggerReason = typeof props.noTriggerReason === 'string' && props.noTriggerReason
2532
2831
  ? props.noTriggerReason
2533
2832
  : (typeof refs.noTriggerReason === 'string' ? refs.noTriggerReason : '');
2534
2833
  if (!sessionId && refs.length === 0) return null;
2535
2834
  if (groups.found.length === 0 && groups.provided.length === 0 && groups.update.length === 0) {
2536
- return noTriggerReason
2835
+ return explainOn && noTriggerReason
2537
2836
  ? h('div', { style: TAIL }, h(RefSection, { label: '搜索命中', tone: 'found', items: EMPTY, sessionId, turnKey: turn, noTriggerReason }))
2538
2837
  : null;
2539
2838
  }
@@ -2551,7 +2850,7 @@ window.__ModuleLoader__.load({
2551
2850
 
2552
2851
  // 测试缝:Node 里验引用投影,浏览器里没有任何副作用。
2553
2852
  if (typeof globalThis.__LITE_MEMORY_TEST__ === 'function') {
2554
- globalThis.__LITE_MEMORY_TEST__({ metaOf, citationOf, citationsOfMeta, citationsOfTurn, dedupe, undoRefOf, graphOf, ruleGraphOf, processingStatusOf, currentBodyOf, historyDiffsOf, MemoryPanel, MemoryGraph, MemoryPalace, TurnMemory, RefSection, displayKeyOf, readDisplaySettings, writeDisplaySettings, graphViewOf, viewModeOf, filterRecords, projectionOf, failureStateOf, evidenceTextOf, evidenceMetaOf, palaceRoomsOf, palaceRoomPageOf, palacePageWindow, palaceToggleOf, palaceMotionOf, palaceRoomIdOf });
2853
+ globalThis.__LITE_MEMORY_TEST__({ metaOf, citationOf, citationsOfMeta, citationsOfTurn, dedupe, undoRefOf, graphOf, ruleGraphOf, processingStatusOf, currentBodyOf, historyDiffsOf, MemoryPanel, MemoryGraph, MemoryPalace, TurnMemory, RefSection, displayKeyOf, readDisplaySettings, writeDisplaySettings, graphViewOf, viewModeOf, filterRecords, projectionOf, failureStateOf, evidenceTextOf, evidenceMetaOf, palaceRoomsOf, palaceRoomPageOf, palacePageWindow, palaceToggleOf, palaceMotionOf, palaceRoomIdOf, refMetaOf, explainKeyOf, explainModeOf, setExplainMode, recallLineOf, agoLabel, checkupOf, titleCountsOf, rowKeyOf, rowKeepFlags, Row });
2555
2854
  }
2556
2855
 
2557
2856
  return {