dsh-lite-memory 0.16.2 → 0.17.0

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/client.js CHANGED
@@ -23,7 +23,7 @@ window.__ModuleLoader__.load({
23
23
  const KEEP_OFF = { ...KEEP_STYLE, opacity: 0.45, cursor: 'default' };
24
24
  const DROP = { flex: '0 0 auto', border: 'none', background: 'transparent', color: 'inherit', padding: '0 2px', fontSize: '12px', lineHeight: '18px', opacity: 0.55, cursor: 'pointer' };
25
25
  const DOCK = { display: 'flex', flexDirection: 'column', gap: '2px', minWidth: 0, fontSize: '12px', lineHeight: '1.6' };
26
- const TAIL = { ...DOCK, margin: '4px 0 2px', opacity: 0.9 };
26
+ const TAIL = { ...DOCK, margin: '4px 0 2px' };
27
27
  const CHIP = { alignSelf: 'flex-start', border: 'none', background: 'transparent', color: 'inherit', padding: 0, fontSize: '12px', lineHeight: '1.6', opacity: 0.6, cursor: 'pointer', maxWidth: '100%', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' };
28
28
  const ITEM = { alignSelf: 'flex-start', border: 'none', background: 'transparent', color: 'inherit', padding: 0, fontSize: '12px', lineHeight: '1.6', opacity: 0.75, cursor: 'pointer', textAlign: 'left', width: '100%', minWidth: 0, maxWidth: '100%', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' };
29
29
  const BODY = { margin: '2px 0 4px', padding: '4px 0 4px 8px', borderLeft: '1px solid currentColor', opacity: 0.7, whiteSpace: 'pre-wrap', wordBreak: 'break-word', maxHeight: '180px', overflowY: 'auto' };
@@ -98,6 +98,13 @@ window.__ModuleLoader__.load({
98
98
  origin: meta.origin === 'project_evolution' ? 'project_evolution' : 'user_explicit',
99
99
  kind,
100
100
  exposure: kind === 'memory-save' || kind === 'memory-forget' ? 'update' : 'reference',
101
+ reason: typeof meta.reason === 'string' ? meta.reason : '',
102
+ reasonCode: typeof meta.reasonCode === 'string' ? meta.reasonCode : '',
103
+ noTriggerReason: typeof meta.noTriggerReason === 'string' ? meta.noTriggerReason : '',
104
+ sourceExcerpt: meta.sourceExcerpt,
105
+ sourceRefs: meta.sourceRefs,
106
+ evidence: meta.evidence,
107
+ processingStatus: processingStatusOf(meta),
101
108
  };
102
109
  };
103
110
 
@@ -122,15 +129,22 @@ window.__ModuleLoader__.load({
122
129
  const citationsOfTurn = (nodes) => {
123
130
  if (!Array.isArray(nodes)) return EMPTY;
124
131
  const out = [];
132
+ let noTriggerReason = '';
125
133
  for (const node of nodes) {
126
134
  const root = node && node.root;
127
135
  if (root === null || typeof root !== 'object') continue;
128
136
  for (const block of [root, ...(Array.isArray(root.subCalls) ? root.subCalls : [])]) {
129
137
  const meta = metaOf(block);
130
138
  if (!meta) continue;
131
- out.push(...citationsOfMeta(meta));
139
+ const citations = citationsOfMeta(meta);
140
+ out.push(...citations);
141
+ if (citations.length === 0 && meta.kind === 'memory-search') {
142
+ const reason = [meta.noTriggerReason, meta.reason].find((value) => typeof value === 'string' && value);
143
+ if (reason) noTriggerReason = reason;
144
+ }
132
145
  }
133
146
  }
147
+ if (noTriggerReason) Object.defineProperty(out, 'noTriggerReason', { value: noTriggerReason, enumerable: false, configurable: true });
134
148
  return out;
135
149
  };
136
150
 
@@ -148,6 +162,9 @@ window.__ModuleLoader__.load({
148
162
  out[at] = item;
149
163
  }
150
164
  }
165
+ if (items && typeof items.noTriggerReason === 'string' && items.noTriggerReason) {
166
+ Object.defineProperty(out, 'noTriggerReason', { value: items.noTriggerReason, enumerable: false, configurable: true });
167
+ }
151
168
  return out;
152
169
  };
153
170
 
@@ -196,7 +213,9 @@ window.__ModuleLoader__.load({
196
213
  if (value !== lastNodes) {
197
214
  lastNodes = value;
198
215
  const next = dedupe(citationsOfTurn(value));
199
- if (!sameList(next, snapshot)) snapshot = next;
216
+ const nextReason = next.noTriggerReason || '';
217
+ const lastReason = snapshot.noTriggerReason || '';
218
+ if (!sameList(next, snapshot) || nextReason !== lastReason) snapshot = next;
200
219
  }
201
220
  return snapshot;
202
221
  };
@@ -308,23 +327,267 @@ window.__ModuleLoader__.load({
308
327
  setState('failed');
309
328
  });
310
329
  };
311
- const label = note || (firstLine(text) ?? RUNNING.memory_save);
330
+ const processing = processingStatusOf(meta);
331
+ const baseLabel = firstLine(text) ?? RUNNING.memory_save;
332
+ const label = note || `${baseLabel}${processing ? ` · ${PROCESSING_LABEL[processing]}` : ''}`;
312
333
  const button = changed && state !== 'dropped'
313
334
  ? h('button', { type: 'button', onClick: drop, disabled: !canDrop, 'aria-label': '撤销此次修改', style: state === 'idle' || state === 'failed' ? DROP : { ...DROP, opacity: 0.25, cursor: 'default' } }, '✕')
314
335
  : null;
315
336
  return h('div', { 'data-tool': props.toolName, style: ROW }, h('span', { style: DOT }, '·'), h('span', { style: LABEL }, label), button);
316
337
  };
317
338
 
339
+ const scopeTag = (scope) => (scope === 'global' ? '[全局]' : '[项目]');
340
+
341
+ /** 处理状态只认服务端枚举,绝不从中文 reason 反推。 */
342
+ const PROCESSING = ['applied', 'unchanged', 'superseded', 'failed'];
343
+ const PROCESSING_LABEL = {
344
+ applied: '已应用', unchanged: '无变化', superseded: '已被新版本取代', failed: '处理失败',
345
+ };
346
+ const processingStatusOf = (source) => {
347
+ if (!source || typeof source !== 'object') return '';
348
+ for (const field of ['processingStatus', 'processing', 'outcome', 'status']) {
349
+ const value = source[field];
350
+ if (typeof value === 'string' && PROCESSING.includes(value)) return value;
351
+ if (value && typeof value === 'object' && typeof value.status === 'string' && PROCESSING.includes(value.status)) return value.status;
352
+ }
353
+ return '';
354
+ };
355
+
356
+ const recordOfPayload = (payload) =>
357
+ (payload && payload.record && typeof payload.record === 'object' && !Array.isArray(payload.record) ? payload.record : null);
358
+
359
+ const bodyTextOf = (value) => {
360
+ if (typeof value === 'string') return value;
361
+ if (!value || typeof value !== 'object') return '';
362
+ for (const field of ['body', 'text', 'value']) {
363
+ if (typeof value[field] === 'string') return value[field];
364
+ }
365
+ return '';
366
+ };
367
+
368
+ /** 当前正文与历史版本分开,历史不拼进正文。 */
369
+ const currentBodyOf = (payload) => {
370
+ if (!payload || typeof payload !== 'object') return '';
371
+ if (typeof payload.currentBody === 'string') return payload.currentBody;
372
+ const record = recordOfPayload(payload);
373
+ if (record) {
374
+ const recordBody = bodyTextOf(record.current);
375
+ if (recordBody) return recordBody;
376
+ if (typeof record.body === 'string') return record.body;
377
+ }
378
+ if (payload.current !== undefined && typeof payload.current !== 'boolean') {
379
+ const current = bodyTextOf(payload.current);
380
+ if (current) return current;
381
+ }
382
+ return typeof payload.body === 'string' ? payload.body : '';
383
+ };
384
+
385
+ const historyOfPayload = (payload) => {
386
+ const record = recordOfPayload(payload);
387
+ const current = payload && payload.current && typeof payload.current === 'object' ? payload.current : null;
388
+ const lists = [
389
+ payload && payload.versions,
390
+ payload && payload.history,
391
+ record && record.versions,
392
+ record && record.history,
393
+ current && current.versions,
394
+ ];
395
+ for (const list of lists) {
396
+ if (Array.isArray(list)) return list.filter((row) => row !== null && row !== undefined);
397
+ }
398
+ return EMPTY;
399
+ };
400
+
401
+ /** 历史行统一成 old → new,缺前后值用相邻版本补。 */
402
+ const historyDiffsOf = (payload) => {
403
+ const rows = historyOfPayload(payload);
404
+ if (rows.length === 0) return EMPTY;
405
+ const currentBody = currentBodyOf(payload);
406
+ const normalized = rows.map((row, index) => {
407
+ const value = row && typeof row === 'object' && !Array.isArray(row) ? row : { body: row };
408
+ const explicitOld = [value.old, value.from, value.before, value.previousBody]
409
+ .find((item) => typeof item === 'string');
410
+ const explicitNew = [value.next, value.to, value.after, value.newBody]
411
+ .find((item) => typeof item === 'string');
412
+ return {
413
+ key: `${Number.isInteger(value.revision) ? value.revision : 'v'}-${index}`,
414
+ at: typeof value.at === 'string' ? value.at : (typeof value.when === 'string' ? value.when : ''),
415
+ revision: Number.isInteger(value.revision) ? value.revision : null,
416
+ status: typeof value.status === 'string' ? value.status : '',
417
+ body: bodyTextOf(value), // ui-copy-ignore
418
+ old: explicitOld || '', // ui-copy-ignore
419
+ next: explicitNew || '', // ui-copy-ignore
420
+ };
421
+ });
422
+ const byRevision = normalized.every((row) => row.revision !== null);
423
+ if (byRevision) normalized.sort((a, b) => a.revision - b.revision);
424
+ else if (normalized.every((row) => Boolean(row.at))) normalized.sort((a, b) => a.at.localeCompare(b.at));
425
+ return normalized.map((row, index) => ({
426
+ ...row,
427
+ old: row.old || row.body || '',
428
+ next: row.next || (index + 1 < normalized.length ? normalized[index + 1].body : currentBody),
429
+ }));
430
+ };
431
+
432
+ const payloadFieldOf = (payload, field) => {
433
+ const record = recordOfPayload(payload);
434
+ const current = payload && payload.current && typeof payload.current === 'object' ? payload.current : null;
435
+ for (const owner of [payload, record, current]) {
436
+ if (owner && owner[field] !== undefined) return owner[field];
437
+ }
438
+ return undefined;
439
+ };
440
+
441
+ const sourceExcerptOf = (payload) => bodyTextOf(payloadFieldOf(payload, 'sourceExcerpt'));
442
+ const refsOfPayload = (payload) => {
443
+ const value = payloadFieldOf(payload, 'sourceRefs');
444
+ if (typeof value === 'string' && value) return [value];
445
+ if (Array.isArray(value)) return value;
446
+ if (value && typeof value === 'object') return [value];
447
+ return EMPTY;
448
+ };
449
+ const evidenceOfPayload = (payload) => {
450
+ const value = payloadFieldOf(payload, 'evidence');
451
+ if (typeof value === 'string' && value) return [value];
452
+ if (Array.isArray(value)) return value;
453
+ if (value && typeof value === 'object') return [value];
454
+ return EMPTY;
455
+ };
456
+
457
+ const refLabelOf = (ref, index) => {
458
+ if (typeof ref === 'string') return ref;
459
+ if (!ref || typeof ref !== 'object') return '';
460
+ if (typeof ref.label === 'string' && ref.label) return ref.label;
461
+ if (typeof ref.title === 'string' && ref.title) return ref.title;
462
+ if (typeof ref.text === 'string' && ref.text) return ref.text;
463
+ if (ref.message !== undefined) return `消息 ${ref.message}`;
464
+ if (ref.messageId !== undefined) return `消息 ${ref.messageId}`;
465
+ if (ref.turn !== undefined) return `第 ${ref.turn} 轮`;
466
+ if (ref.turnIndex !== undefined) return `第 ${ref.turnIndex} 轮`;
467
+ return ref.id !== undefined ? String(ref.id) : `引用 ${index + 1}`;
468
+ };
469
+ const refHrefOf = (ref) => (ref && typeof ref === 'object'
470
+ ? [ref.href, ref.url, ref.link].find((value) => typeof value === 'string' && value)
471
+ : '');
472
+
473
+ const linkStyle = { color: 'inherit', textDecoration: 'underline', textUnderlineOffset: '2px', wordBreak: 'break-all' };
474
+
475
+ const HistoryVersions = (props) => {
476
+ const rows = Array.isArray(props.rows) ? props.rows : EMPTY;
477
+ const [open, setOpen] = React.useState(false);
478
+ if (rows.length === 0) return null;
479
+ return h('div', { style: { display: 'flex', flexDirection: 'column', gap: '2px' } },
480
+ h('button', {
481
+ type: 'button',
482
+ onClick: () => setOpen((value) => !value),
483
+ 'aria-expanded': open,
484
+ 'aria-label': `${open ? '收起' : '展开'}历史版本`,
485
+ style: CHIP,
486
+ }, `历史 ${rows.length}${open ? ' · 收起' : ''}`),
487
+ open ? h('div', { style: BODY }, rows.map((row) => h('div', {
488
+ key: row.key, style: { margin: '0 0 4px', minWidth: 0 },
489
+ },
490
+ h('div', { style: { opacity: 0.65, fontSize: '11px' } },
491
+ [row.at, row.revision === null ? '' : `v${row.revision}`, row.status].filter(Boolean).join(' · ')),
492
+ h('div', null,
493
+ h('span', { style: { opacity: 0.6 } }, SHORT(row.old, 120) || '—'),
494
+ ' → ',
495
+ h('span', null, SHORT(row.next, 120) || '—'))))) : null,
496
+ );
497
+ };
498
+
499
+ const DetailExtras = (props) => {
500
+ const payload = props.payload;
501
+ const excerpt = sourceExcerptOf(payload);
502
+ const refs = refsOfPayload(payload);
503
+ const evidence = evidenceOfPayload(payload);
504
+ const history = historyDiffsOf(payload);
505
+ if (!excerpt && refs.length === 0 && evidence.length === 0 && history.length === 0) return null;
506
+ const links = (items, label) => (items.length === 0 ? null : h('div', { style: { display: 'flex', gap: '6px', minWidth: 0 } },
507
+ h('span', { style: { opacity: 0.6, flex: '0 0 auto' } }, label),
508
+ h('span', { style: { minWidth: 0, wordBreak: 'break-word' } }, items.map((item, index) => {
509
+ const href = refHrefOf(item);
510
+ const text = refLabelOf(item, index);
511
+ const key = `${index}-${text}`;
512
+ if (!href) return h('span', { key }, index > 0 ? ' · ' : '', text);
513
+ return h('span', { key }, index > 0 ? ' · ' : '', h('a', {
514
+ href, target: '_blank', rel: 'noreferrer', style: linkStyle,
515
+ }, text));
516
+ }))));
517
+ return h('div', { style: { display: 'flex', flexDirection: 'column', gap: '4px' } },
518
+ excerpt ? h('div', { style: BODY }, h('span', { style: { opacity: 0.65 } }, '来源'), ' ', excerpt) : null,
519
+ links(refs, '消息'),
520
+ links(evidence, '依据'),
521
+ h(HistoryVersions, { rows: history }));
522
+ };
523
+
318
524
  const versionLine = (shown) => {
319
525
  const scope = shown.scopeLabel || (shown.scope === 'global' ? '全局' : '项目');
320
526
  const archived = shown.status === 'archived' ? ' · 已归档' : '';
321
- return `${scope}${archived}`;
527
+ const processing = processingStatusOf(shown.processingStatus ? { processingStatus: shown.processingStatus } : shown);
528
+ return `${scope}${archived}${processing ? ` · ${PROCESSING_LABEL[processing]}` : ''}`;
529
+ };
530
+
531
+ /**
532
+ * 引用三层的色调:中性 / 强调 / 成功。
533
+ * 三档色阶:中性 / 强调 / 成功;正文用主文本色。
534
+ */
535
+ const REF_TONE = {
536
+ found: 'var(--dsw-alias-label-secondary)',
537
+ provided: 'var(--dsw-alias-brand-primary)',
538
+ update: 'var(--dsw-alias-state-success-primary)',
539
+ };
540
+ 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
+ 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
+
543
+ /** 出处一句话:项目/全局、来源、状态。 */
544
+ const refMetaOf = (item) => {
545
+ const parts = [item.scope === 'global' ? '全局' : '项目', item.origin === 'project_evolution' ? '自进化' : '你记的'];
546
+ if (item.status === 'archived') parts.push('已归档');
547
+ const processing = PROCESSING_LABEL[item.processingStatus] || '';
548
+ if (processing) parts.push(processing);
549
+ return parts.join(' · ');
550
+ };
551
+
552
+ const RefChevron = (props) => h('svg', {
553
+ width: 8, height: 8, viewBox: '0 0 8 8', 'aria-hidden': 'true', focusable: 'false',
554
+ style: { flex: '0 0 auto', opacity: 0.65, transform: props.open ? 'rotate(90deg)' : 'none' },
555
+ }, h('path', { d: 'M2.5 1 L5.5 4 L2.5 7', fill: 'none', stroke: 'currentColor', 'stroke-width': '1.2', 'stroke-linecap': 'round', 'stroke-linejoin': 'round' }));
556
+
557
+ /** 一条引用:标题一行、原因与来源一行,窄栏也不抢宽度。 */
558
+ const RefRow = (props) => {
559
+ const [hover, setHover] = React.useState(false);
560
+ const item = props.item;
561
+ const reason = item.noTriggerReason || item.reason || '';
562
+ return h('button', {
563
+ type: 'button', // ui-copy-ignore
564
+ onMouseEnter: () => setHover(true),
565
+ onMouseLeave: () => setHover(false),
566
+ 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),
569
+ style: { // ui-copy-ignore
570
+ display: 'flex', alignItems: 'flex-start', gap: '7px', width: '100%', minWidth: 0, textAlign: 'left',
571
+ border: 'none', borderRadius: '8px', padding: '3px 7px', cursor: 'pointer', color: 'inherit',
572
+ background: props.active || hover ? 'var(--dsw-alias-bg-layer-2)' : 'transparent',
573
+ fontSize: '12px', lineHeight: '18px',
574
+ },
575
+ },
576
+ 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
+ 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),
579
+ h('span', { style: { display: 'flex', alignItems: 'center', gap: '6px', minWidth: 0, fontSize: '11px', lineHeight: '16px', color: 'var(--dsw-alias-label-secondary)' } },
580
+ reason ? h('span', { style: { minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, reason) : null,
581
+ h('span', { style: { marginLeft: 'auto', flex: '0 0 auto', fontVariantNumeric: 'tabular-nums' } }, refMetaOf(item)),
582
+ ),
583
+ ));
322
584
  };
323
585
 
324
586
  /** 一段引用:点开是名字,再点是当前正文。 */
325
587
  const RefSection = (props) => {
326
588
  const items = Array.isArray(props.items) ? props.items : EMPTY;
327
589
  const sessionId = typeof props.sessionId === 'string' ? props.sessionId : '';
590
+ const tone = REF_TONE[props.tone] || REF_TONE.found;
328
591
  const [open, setOpen] = React.useState(false);
329
592
  const [shown, setShown] = React.useState(null);
330
593
  const [note, setNote] = React.useState('');
@@ -340,7 +603,10 @@ window.__ModuleLoader__.load({
340
603
  setShown(null);
341
604
  setNote('');
342
605
  }, [sessionId, props.turnKey]);
343
- if (items.length === 0) return null;
606
+ if (items.length === 0) {
607
+ const reason = typeof props.noTriggerReason === 'string' ? props.noTriggerReason : '';
608
+ return reason ? h('div', { style: { ...REF_META, margin: 0 } }, reason) : null;
609
+ }
344
610
  const close = () => {
345
611
  generation.current += 1;
346
612
  setShown(null);
@@ -377,30 +643,35 @@ window.__ModuleLoader__.load({
377
643
  scopeLabel: typeof payload.scopeLabel === 'string' ? payload.scopeLabel : '',
378
644
  revision: Number.isInteger(payload.revision) ? payload.revision : item.revision,
379
645
  status: typeof payload.status === 'string' ? payload.status : 'ok',
380
- body: typeof payload.body === 'string' ? payload.body : '',
646
+ body: currentBodyOf(payload),
647
+ processingStatus: processingStatusOf(payload) || item.processingStatus || '',
648
+ payload,
381
649
  current: payload.revision === item.revision,
382
650
  });
383
651
  });
384
652
  };
385
- const rows = open ? items.map((item) => h('button', {
653
+ const rows = open ? items.map((item) => h(RefRow, {
386
654
  key: `${item.scope}|${item.id}|${item.revision}`, // ui-copy-ignore
387
- type: 'button', // ui-copy-ignore
388
- onClick: () => show(item), // ui-copy-ignore
389
- 'aria-label': `查看记忆 ${item.title || item.id}`,
390
- title: item.title || item.id,
391
- style: ITEM, // ui-copy-ignore
392
- }, `${item.title || item.id}${item.origin === 'project_evolution' ? ' · 实验' : ''}`)) : [];
655
+ item, tone, active: Boolean(shown && shown.id === item.id),
656
+ onShow: () => show(item),
657
+ })) : [];
393
658
  const detail = shown && shown.ref && items.some((item) => item.id === shown.id && item.revision === shown.ref.revision)
394
- ? h('div', { style: BODY },
395
- shown.loading ? '读取中' : null,
396
- shown.gone ? '这条记忆已删除' : null,
397
- shown.invalid ? '读取失败,点名字重试' : null,
659
+ ? h('div', { style: { ...REF_BODY, borderLeft: `2px solid ${tone}` } },
660
+ shown.loading ? h('span', { style: REF_META }, '读取中…') : null,
661
+ shown.gone ? h('span', { style: REF_META }, '这条记忆已删除') : null,
662
+ shown.invalid ? h('span', { style: REF_META }, '读不到正文,点名字重试') : null,
398
663
  !shown.loading && !shown.gone && !shown.invalid ? h(React.Fragment, null,
399
- shown.title && shown.title !== shown.ref.title ? h('div', { style: { opacity: 0.9 } }, shown.title) : null,
400
- h('div', null, versionLine(shown)),
401
- shown.when ? h('div', null, shown.when) : null,
402
- shown.body ? h('div', null, shown.body) : null,
664
+ shown.title && shown.title !== shown.ref.title
665
+ ? h('div', { style: { fontSize: '12px', lineHeight: '1.55', fontWeight: 600 } }, shown.title)
666
+ : null,
667
+ h('div', { style: REF_META }, versionLine(shown)),
668
+ shown.when ? h('div', { style: REF_META }, shown.when) : null,
669
+ shown.body
670
+ ? h('div', { style: { color: 'var(--dsw-alias-label-primary)', whiteSpace: 'pre-wrap', wordBreak: 'break-word' } }, shown.body)
671
+ : null,
672
+ h(DetailExtras, { payload: { ...shown.ref, ...shown.payload } }),
403
673
  ) : null,
674
+ note ? h('span', { style: REF_META }, note) : null,
404
675
  )
405
676
  : null;
406
677
  return h('div', { style: DOCK, 'data-memory-section': props.label },
@@ -408,12 +679,20 @@ window.__ModuleLoader__.load({
408
679
  type: 'button',
409
680
  onClick: () => { if (open) close(); else setOpen(true); },
410
681
  'aria-expanded': open,
411
- 'aria-label': `${open ? '收起' : '展开'}${props.label}`,
412
- style: CHIP,
413
- }, `${props.label} ${items.length} 条${open ? ' · 收起' : ''}`),
682
+ 'aria-label': `${open ? '收起' : '展开'}${props.label},${items.length} 条`,
683
+ style: {
684
+ alignSelf: 'flex-start', display: 'inline-flex', alignItems: 'center', gap: '6px',
685
+ border: 'none', background: 'transparent', padding: '1px 0', cursor: 'pointer',
686
+ color: 'var(--dsw-alias-label-secondary)', fontSize: '11px', lineHeight: '18px',
687
+ },
688
+ },
689
+ h('span', { 'aria-hidden': 'true', style: { width: '2px', height: '10px', borderRadius: '999px', background: tone, opacity: 0.9 } }),
690
+ h('span', null, props.label),
691
+ h('span', { style: { fontVariantNumeric: 'tabular-nums', opacity: 0.8 } }, String(items.length)),
692
+ h(RefChevron, { open }),
693
+ ),
414
694
  ...rows,
415
695
  detail,
416
- note ? h('div', { style: BODY }, note) : null,
417
696
  );
418
697
  };
419
698
 
@@ -422,7 +701,7 @@ window.__ModuleLoader__.load({
422
701
  /** 大脑:两瓣轮廓加一条中线,状态只靠透明度和面板文字。 */
423
702
  const BrainIcon = () => h('svg', {
424
703
  width: 18, height: 18, viewBox: '0 0 24 24', fill: 'none',
425
- stroke: 'currentColor', strokeWidth: 1.5, strokeLinecap: 'round', strokeLinejoin: 'round',
704
+ stroke: 'currentColor', 'stroke-width': 1.5, 'stroke-linecap': 'round', 'stroke-linejoin': 'round',
426
705
  'aria-hidden': 'true', focusable: 'false',
427
706
  },
428
707
  h('path', { d: 'M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z' }),
@@ -431,7 +710,7 @@ window.__ModuleLoader__.load({
431
710
 
432
711
  const CloseIcon = () => h('svg', {
433
712
  width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none',
434
- stroke: 'currentColor', strokeWidth: 1.6, strokeLinecap: 'round',
713
+ stroke: 'currentColor', 'stroke-width': 1.6, 'stroke-linecap': 'round',
435
714
  'aria-hidden': 'true', focusable: 'false',
436
715
  }, h('path', { d: 'M6 6l12 12M18 6L6 18' }));
437
716
 
@@ -449,6 +728,151 @@ window.__ModuleLoader__.load({
449
728
  const text = typeof value === 'string' ? value : '';
450
729
  return text.length > size ? `${text.slice(0, size)}…` : text;
451
730
  };
731
+
732
+ /** 显示设置只决定画不画,不改学习状态;按项目 key 存在本机。 */
733
+ const DISPLAY_DEFAULTS = Object.freeze({ graph: false, palace: false });
734
+ const displayKeyOf = (projectKey) => `lite-memory:display:${projectKey || 'default'}`;
735
+ const displayStoreOf = () => {
736
+ try {
737
+ const store = globalThis.localStorage;
738
+ return store && typeof store.getItem === 'function' ? store : null;
739
+ } catch {
740
+ return null;
741
+ }
742
+ };
743
+ const readDisplaySettings = (projectKey) => {
744
+ const store = displayStoreOf();
745
+ const next = { ...DISPLAY_DEFAULTS };
746
+ if (!store) return next;
747
+ try {
748
+ const raw = store.getItem(displayKeyOf(projectKey));
749
+ if (!raw) return next;
750
+ const saved = JSON.parse(raw);
751
+ if (saved && typeof saved === 'object') {
752
+ next.graph = saved.graph === true;
753
+ next.palace = saved.palace === true;
754
+ }
755
+ } catch {
756
+ return next;
757
+ }
758
+ return next;
759
+ };
760
+ const writeDisplaySettings = (projectKey, next) => {
761
+ const store = displayStoreOf();
762
+ if (!store) return false;
763
+ try {
764
+ store.setItem(displayKeyOf(projectKey), JSON.stringify({
765
+ graph: next && next.graph === true,
766
+ palace: next && next.palace === true,
767
+ }));
768
+ return true;
769
+ } catch {
770
+ return false;
771
+ }
772
+ };
773
+ /** 功能关闭时视图统一回列表,不保留一个画不出来的页签。 */
774
+ const graphViewOf = (enabled, view) => (enabled === true && view === 'graph' ? 'graph' : 'list');
775
+ const viewModeOf = (display, view) => {
776
+ if (view === 'graph' && display && display.graph === true) return 'graph';
777
+ if (view === 'palace' && display && display.palace === true) return 'palace';
778
+ return 'list';
779
+ };
780
+ const reduceMotionOf = () => {
781
+ try {
782
+ const query = globalThis.matchMedia && globalThis.matchMedia('(prefers-reduced-motion: reduce)');
783
+ return Boolean(query && query.matches === true);
784
+ } catch {
785
+ return false;
786
+ }
787
+ };
788
+ /** 跟随系统偏好切换,打开面板期间改设置也能立即生效。 */
789
+ const useReducedMotion = () => {
790
+ const [reduced, setReduced] = React.useState(reduceMotionOf);
791
+ React.useEffect(() => {
792
+ let query = null;
793
+ try {
794
+ query = globalThis.matchMedia && globalThis.matchMedia('(prefers-reduced-motion: reduce)');
795
+ } catch {
796
+ query = null;
797
+ }
798
+ if (!query || typeof query.addEventListener !== 'function') return undefined;
799
+ const update = () => setReduced(query.matches === true);
800
+ query.addEventListener('change', update);
801
+ return () => query.removeEventListener('change', update);
802
+ }, []);
803
+ return reduced;
804
+ };
805
+
806
+ /** 标签/话题按精确值筛选;只有自由搜索才用包含匹配。 */
807
+ const filterRecords = (records, query, hub) => {
808
+ const rows = Array.isArray(records) ? records : EMPTY;
809
+ const kind = hub && typeof hub.kind === 'string' ? hub.kind : '';
810
+ const value = hub && typeof hub.value === 'string' ? hub.value : '';
811
+ if (kind === 'tag' && value) {
812
+ return rows.filter((record) => Array.isArray(record.tags) && record.tags.includes(value));
813
+ }
814
+ if (kind === 'topic' && value) {
815
+ return rows.filter((record) => record.topicKey === value);
816
+ }
817
+ const needle = typeof query === 'string' ? query.trim().toLowerCase() : '';
818
+ if (!needle) return rows;
819
+ return rows.filter((record) => {
820
+ const tags = Array.isArray(record.tags) ? record.tags.join(' ') : '';
821
+ return `${record.title || ''} ${record.summary || ''} ${tags} ${record.topicKey || ''} ${KIND_LABEL[record.kind] || ''}`.toLowerCase().includes(needle);
822
+ });
823
+ };
824
+
825
+ /** 记忆宫殿:房间固定,槽位固定,换项目只换陈列内容。 */
826
+ const PALACE_PAGE_SIZE = 24;
827
+ const PALACE_PREVIEW = 4;
828
+ const PALACE_ROOMS = Object.freeze([
829
+ Object.freeze({ id: 'habits', name: '习惯与规范', kinds: Object.freeze(['preference', 'convention']) }),
830
+ Object.freeze({ id: 'methods', name: '方法与经验', kinds: Object.freeze(['procedure', 'pitfall']) }),
831
+ Object.freeze({ id: 'entries', name: '资料与入口', kinds: Object.freeze(['pointer', 'credential_hint']) }),
832
+ Object.freeze({ id: 'notes', name: '其他记录', kinds: Object.freeze(['note']) }),
833
+ ]);
834
+ const PALACE_ROOM_OF_KIND = Object.freeze({
835
+ preference: 'habits', convention: 'habits',
836
+ procedure: 'methods', pitfall: 'methods',
837
+ pointer: 'entries', credential_hint: 'entries',
838
+ note: 'notes',
839
+ });
840
+ const palaceRoomIdOf = (kind) => PALACE_ROOM_OF_KIND[kind] || 'notes';
841
+ const palaceRoomsOf = (records) => {
842
+ const rows = Array.isArray(records) ? records : EMPTY;
843
+ return PALACE_ROOMS.map((room) => {
844
+ const items = rows.filter((record) => palaceRoomIdOf(record && record.kind) === room.id);
845
+ return { id: room.id, name: room.name, items, total: items.length, pages: Math.max(1, Math.ceil(items.length / PALACE_PAGE_SIZE)) };
846
+ });
847
+ };
848
+ const palaceRoomPageOf = (records, roomId, page) => {
849
+ const room = palaceRoomsOf(records).find((row) => row.id === roomId);
850
+ if (!room) return { items: EMPTY, total: 0, pages: 0, page: 1 };
851
+ const current = Math.min(room.pages, Math.max(1, Number.isFinite(page) ? Math.floor(page) : 1));
852
+ return {
853
+ items: room.items.slice((current - 1) * PALACE_PAGE_SIZE, current * PALACE_PAGE_SIZE),
854
+ total: room.total, pages: room.pages, page: current,
855
+ };
856
+ };
857
+ /** 页码最多铺 7 个,当前页始终在窗口里。 */
858
+ const palacePageWindow = (page, pages) => {
859
+ const total = Math.max(1, Math.floor(Number.isFinite(pages) ? pages : 1));
860
+ const current = Math.min(total, Math.max(1, Math.floor(Number.isFinite(page) ? page : 1)));
861
+ if (total <= 7) return Array.from({ length: total }, (_, index) => index + 1);
862
+ const start = Math.max(1, Math.min(current - 3, total - 6));
863
+ return Array.from({ length: 7 }, (_, index) => start + index);
864
+ };
865
+ const palaceToggleOf = (expanded, roomId) => (expanded === roomId ? '' : roomId);
866
+ const palaceMotionOf = (reduced) => ({ duration: reduced === true ? 0 : 180, easing: 'cubic-bezier(0.2, 0.7, 0.3, 1)' });
867
+
868
+ /** 失败回执分「当前」和「过去」:当前失败要修,过去的只说明发生过。 */
869
+ const failureStateOf = (evolution) => {
870
+ const rows = evolution && evolution.economy && Array.isArray(evolution.economy.recent) ? evolution.economy.recent : EMPTY;
871
+ if (rows.length === 0) return '';
872
+ if (rows[0] && rows[0].status === 'failed') return 'current';
873
+ return rows.slice(1).some((row) => row && row.status === 'failed') ? 'past' : '';
874
+ };
875
+
452
876
  /** 面板开关。 */
453
877
  const panelUi = { open: false, sessionId: '' };
454
878
  const panelListeners = new Set();
@@ -533,74 +957,124 @@ window.__ModuleLoader__.load({
533
957
 
534
958
  /**
535
959
  * 图谱只画真实关系:显式关联,以及记录自己的标签 / 主题。
536
- * 位置由 id 决定,同一份记忆每次打开都在同一处。
960
+ * 标签 / 主题是组:枢纽画大点,成员围在它周围画小点。
961
+ * 位置由 id 与成员关系决定,同一份数据每次打开都在同一处。
537
962
  */
538
- /** 位置由 id 决定;同一份数据每次打开都在同一处。 */
539
963
  const layoutGraph = (nodes, edges, primaryKind) => {
964
+ const kindOf = new Map(nodes.map((node) => [node.id, node.kind]));
540
965
  const degree = new Map();
966
+ const linkedOf = new Map();
967
+ const membersOf = new Map();
968
+ const addLink = (primaryId, hubId) => {
969
+ if (!linkedOf.has(primaryId)) linkedOf.set(primaryId, new Set());
970
+ linkedOf.get(primaryId).add(hubId);
971
+ if (!membersOf.has(hubId)) membersOf.set(hubId, new Set());
972
+ membersOf.get(hubId).add(primaryId);
973
+ };
541
974
  for (const edge of edges) {
542
975
  degree.set(edge.from, (degree.get(edge.from) || 0) + 1);
543
976
  degree.set(edge.to, (degree.get(edge.to) || 0) + 1);
977
+ if (kindOf.get(edge.from) === primaryKind && kindOf.get(edge.to) !== primaryKind) addLink(edge.from, edge.to);
978
+ if (kindOf.get(edge.to) === primaryKind && kindOf.get(edge.from) !== primaryKind) addLink(edge.to, edge.from);
544
979
  }
545
- const placed = new Map();
546
980
  const primaries = nodes.filter((node) => node.kind === primaryKind);
547
- const count = primaries.length;
548
- const spread = 0.55 + Math.min(0.45, count * 0.06);
549
- primaries.forEach((node, index) => {
550
- const seed = hashOf(node.id);
551
- const ring = count <= 1 ? 0 : count <= 8 ? 128 : count <= 18 ? 168 : 206;
552
- const wobble = count <= 8 ? 1 : 0.64 + ((seed % 100) / 100) * 0.36;
553
- const angle = count <= 1 ? 0 : (index / count) * Math.PI * 2 - Math.PI / 2 + ((seed % 17) / 17) * 0.16;
554
- placed.set(node.id, {
555
- ...node,
556
- x: 320 + Math.cos(angle) * ring * 1.22 * wobble * spread,
557
- y: 236 + Math.sin(angle) * ring * 0.8 * wobble * spread,
558
- radius: 5.5 + Math.min(4, (degree.get(node.id) || 0) * 0.7),
559
- });
560
- });
561
981
  const hubs = nodes.filter((node) => node.kind !== primaryKind);
562
- const kindOf = new Map(nodes.map((node) => [node.id, node.kind]));
563
- const linkedOf = new Map(hubs.map((node) => [node.id, new Set()]));
564
- for (const edge of edges) {
565
- if (linkedOf.has(edge.from) && kindOf.get(edge.to) === primaryKind) linkedOf.get(edge.from).add(edge.to);
566
- if (linkedOf.has(edge.to) && kindOf.get(edge.from) === primaryKind) linkedOf.get(edge.to).add(edge.from);
567
- }
982
+ const weightOf = new Map(hubs.map((hub) => [hub.id, (membersOf.get(hub.id) || EMPTY).size]));
568
983
  const groups = new Map();
569
- for (const node of hubs) {
570
- const linked = [...linkedOf.get(node.id)].sort();
571
- const key = linked.length > 0 ? linked.join('|') : `solo:${node.id}`;
572
- if (!groups.has(key)) groups.set(key, { linked, hubs: [] });
573
- groups.get(key).hubs.push(node);
984
+ const assignedHub = new Map();
985
+ const loose = [];
986
+ for (const node of primaries) {
987
+ const options = [...(linkedOf.get(node.id) || EMPTY)]
988
+ .filter((id) => (weightOf.get(id) || 0) > 1)
989
+ .sort((a, b) => ((weightOf.get(b) || 0) - (weightOf.get(a) || 0)) || a.localeCompare(b));
990
+ const hubId = options[0];
991
+ if (!hubId) { loose.push(node); continue; }
992
+ if (!groups.has(hubId)) groups.set(hubId, []);
993
+ groups.get(hubId).push(node);
994
+ assignedHub.set(node.id, hubId);
995
+ }
996
+ const placed = new Map();
997
+ if (primaryKind === 'memory') {
998
+ for (const [hubId, members] of [...groups]) {
999
+ if (members.length > 1) continue;
1000
+ for (const member of members) {
1001
+ loose.push(member);
1002
+ assignedHub.delete(member.id);
1003
+ }
1004
+ groups.delete(hubId);
1005
+ }
574
1006
  }
575
- for (const group of groups.values()) {
576
- const points = group.linked.map((id) => placed.get(id)).filter(Boolean);
577
- const cx = points.length > 0 ? points.reduce((sum, point) => sum + point.x, 0) / points.length : 320;
578
- const cy = points.length > 0 ? points.reduce((sum, point) => sum + point.y, 0) / points.length : 236;
579
- const outward = Math.hypot(cx - 320, cy - 236);
580
- group.hubs.forEach((node, index) => {
1007
+ const clusterList = [...groups.entries()]
1008
+ .sort((a, b) => (b[1].length - a[1].length) || a[0].localeCompare(b[0]));
1009
+ const groupCount = clusterList.length;
1010
+ const ring = groupCount <= 1 ? 0 : groupCount <= 4 ? 150 : groupCount <= 8 ? 182 : 212;
1011
+ const hubRadius = (size) => 8 + Math.min(10, Math.sqrt(size) * 3);
1012
+ const placePrimary = (node, x, y) => {
1013
+ placed.set(node.id, {
1014
+ ...node, x, y,
1015
+ radius: 3.2 + Math.min(2.2, (degree.get(node.id) || 0) * 0.35),
1016
+ });
1017
+ };
1018
+ clusterList.forEach(([hubId, members], index) => {
1019
+ const seed = hashOf(hubId);
1020
+ const angle = groupCount <= 1
1021
+ ? 0
1022
+ : (index / groupCount) * Math.PI * 2 - Math.PI / 2 + ((seed % 13) / 13) * 0.12;
1023
+ const cx = 320 + Math.cos(angle) * ring * 1.22;
1024
+ const cy = 236 + Math.sin(angle) * ring * 0.8;
1025
+ const hub = hubs.find((node) => node.id === hubId);
1026
+ const radius = hubRadius(members.length);
1027
+ placed.set(hubId, { ...hub, x: cx, y: cy, radius });
1028
+ const orbit = radius + 20 + Math.sqrt(members.length) * 13;
1029
+ const start = ((seed % 360) / 180) * Math.PI;
1030
+ members.forEach((node, memberIndex) => {
1031
+ const step = (memberIndex / members.length) * Math.PI * 2;
1032
+ const wobble = 0.88 + ((hashOf(node.id) % 25) / 100);
1033
+ placePrimary(node, cx + Math.cos(start + step) * orbit * wobble, cy + Math.sin(start + step) * orbit * 0.82 * wobble);
1034
+ });
1035
+ });
1036
+ if (loose.length > 0) {
1037
+ const orbit = groupCount === 0
1038
+ ? (loose.length <= 1 ? 0 : loose.length <= 8 ? 128 : loose.length <= 18 ? 168 : 206)
1039
+ : ring + 96;
1040
+ loose.forEach((node, index) => {
581
1041
  const seed = hashOf(node.id);
582
- const base = outward > 12
583
- ? Math.atan2(cy - 236, cx - 320)
584
- : ((seed % 360) / 180) * Math.PI;
585
- const angle = base + (index - (group.hubs.length - 1) / 2) * 0.6;
586
- const distance = group.linked.length >= 3 && group.hubs.length === 1 ? 0 : 48 + group.hubs.length * 4;
587
- placed.set(node.id, {
588
- ...node,
589
- x: cx + Math.cos(angle) * distance,
590
- y: cy + Math.sin(angle) * distance * 0.82,
591
- radius: node.kind === 'topic' ? 5 : 4,
592
- });
1042
+ const angle = loose.length <= 1
1043
+ ? 0
1044
+ : (index / loose.length) * Math.PI * 2 - Math.PI / 2 + ((seed % 17) / 17) * 0.14;
1045
+ const wobble = loose.length <= 8 ? 1 : 0.64 + ((seed % 100) / 100) * 0.36;
1046
+ placePrimary(node, 320 + Math.cos(angle) * orbit * 1.22 * wobble, 236 + Math.sin(angle) * orbit * 0.8 * wobble);
593
1047
  });
594
1048
  }
595
1049
  return {
596
1050
  nodes: [...placed.values()],
597
- edges: edges.filter((edge) => placed.has(edge.from) && placed.has(edge.to)),
598
- people: count,
1051
+ edges: edges.filter((edge) => {
1052
+ if (!placed.has(edge.from) || !placed.has(edge.to)) return false;
1053
+ const fromPrimary = kindOf.get(edge.from) === primaryKind;
1054
+ const toPrimary = kindOf.get(edge.to) === primaryKind;
1055
+ if (fromPrimary && toPrimary) return true;
1056
+ if (primaryKind !== 'memory') return true;
1057
+ if (fromPrimary) return assignedHub.get(edge.from) === edge.to;
1058
+ if (toPrimary) return assignedHub.get(edge.to) === edge.from;
1059
+ return false;
1060
+ }),
1061
+ people: primaries.length,
599
1062
  };
600
1063
  };
601
1064
 
602
- const graphOf = (records) => {
603
- const rows = (Array.isArray(records) ? records : EMPTY).slice(0, 60);
1065
+ /** 60 条投影之外也要保留当前选中项,点开的节点不能从图里消失。 */
1066
+ const projectionOf = (all, selectedId) => {
1067
+ const rows = all.slice(0, 60);
1068
+ if (selectedId && !rows.some((row) => row.id === selectedId)) {
1069
+ const selected = all.find((row) => row.id === selectedId);
1070
+ if (selected) rows[rows.length - 1] = selected;
1071
+ }
1072
+ return rows;
1073
+ };
1074
+
1075
+ const graphOf = (records, selectedId) => {
1076
+ const all = Array.isArray(records) ? records : EMPTY;
1077
+ const rows = projectionOf(all, selectedId);
604
1078
  const byId = new Map(rows.map((record) => [record.id, record]));
605
1079
  const tagCount = new Map();
606
1080
  const topicCount = new Map();
@@ -608,7 +1082,7 @@ window.__ModuleLoader__.load({
608
1082
  for (const tag of Array.isArray(record.tags) ? record.tags : EMPTY) tagCount.set(tag, (tagCount.get(tag) || 0) + 1);
609
1083
  if (record.topicKey) topicCount.set(record.topicKey, (topicCount.get(record.topicKey) || 0) + 1);
610
1084
  }
611
- const tags = [...tagCount.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 8);
1085
+ const tags = [...tagCount.entries()].filter(([, count]) => count > 1).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 8);
612
1086
  const topics = [...topicCount.entries()].filter(([, count]) => count > 1).sort((a, b) => b[1] - a[1]).slice(0, 4);
613
1087
  const nodes = [];
614
1088
  const edges = [];
@@ -636,12 +1110,17 @@ window.__ModuleLoader__.load({
636
1110
  if (byId.has(link.id)) addEdge(record.id, link.id, 'link');
637
1111
  }
638
1112
  }
639
- return layoutGraph(nodes, edges, 'memory');
1113
+ return {
1114
+ ...layoutGraph(nodes, edges, 'memory'),
1115
+ shown: rows.length,
1116
+ total: all.length,
1117
+ };
640
1118
  };
641
1119
 
642
1120
  /** 经验图谱只连共享条件。 */
643
- const ruleGraphOf = (rules) => {
644
- const rows = (Array.isArray(rules) ? rules : EMPTY).slice(0, 60);
1121
+ const ruleGraphOf = (rules, selectedId) => {
1122
+ const all = Array.isArray(rules) ? rules : EMPTY;
1123
+ const rows = projectionOf(all, selectedId);
645
1124
  const conditionCount = new Map();
646
1125
  for (const rule of rows) {
647
1126
  const seen = new Set();
@@ -678,32 +1157,134 @@ window.__ModuleLoader__.load({
678
1157
  }
679
1158
  }
680
1159
  }
681
- return layoutGraph(nodes, edges, 'rule');
1160
+ return {
1161
+ ...layoutGraph(nodes, edges, 'rule'),
1162
+ shown: rows.length,
1163
+ total: all.length,
1164
+ };
682
1165
  };
683
1166
 
684
1167
  /** 关系图只画真实关系;节点沿用原始 id,位置由 id 决定。 */
1168
+ /** 图谱缩放:28px 命中区,悬停/聚焦才给底色。 */
1169
+ const ZoomButton = (props) => {
1170
+ const [hover, setHover] = React.useState(false);
1171
+ return h('button', {
1172
+ type: 'button', onClick: props.onClick, disabled: props.disabled,
1173
+ 'aria-label': props.label, title: props.label,
1174
+ onMouseEnter: () => setHover(true),
1175
+ onMouseLeave: () => setHover(false),
1176
+ onFocus: () => setHover(true),
1177
+ onBlur: () => setHover(false),
1178
+ style: {
1179
+ width: '28px', height: '28px', padding: 0, display: 'inline-flex',
1180
+ alignItems: 'center', justifyContent: 'center', border: 'none',
1181
+ borderRadius: '8px', background: hover && !props.disabled ? 'var(--dsw-alias-bg-layer-2)' : 'transparent',
1182
+ color: 'inherit', opacity: props.disabled ? 0.35 : hover ? 0.95 : 0.72,
1183
+ cursor: props.disabled ? 'default' : 'pointer',
1184
+ },
1185
+ }, props.children);
1186
+ };
1187
+
685
1188
  const MemoryGraph = (props) => {
686
1189
  const primaryKind = props.primaryKind || 'memory';
687
- const graph = React.useMemo(() => (props.graphOf || graphOf)(props.records), [props.records, props.graphOf]);
688
- const [zoom, setZoom] = React.useState(1);
689
1190
  const selectedId = props.selectedId;
690
- const labelSet = graph.people <= 10;
691
- const stepZoom = (delta) => setZoom((value) => Math.min(2.5, Math.max(0.75, Math.round((value + delta) * 100) / 100)));
1191
+ const graph = React.useMemo(() => (props.graphOf || graphOf)(props.records, selectedId), [props.records, props.graphOf, selectedId]);
1192
+ const [zoom, setZoom] = React.useState(1);
1193
+ const [pan, setPan] = React.useState({ x: 0, y: 0 });
1194
+ const [dragging, setDragging] = React.useState(false);
1195
+ const [hoveredId, setHoveredId] = React.useState('');
1196
+ const svgRef = React.useRef(null);
1197
+ const dragRef = React.useRef(null);
1198
+ const labelSet = graph.people <= 40;
1199
+ const hubCount = graph.nodes.length - graph.people;
1200
+ const shownCount = Number.isInteger(graph.shown) ? graph.shown : graph.people;
1201
+ const totalCount = Number.isInteger(props.total) && props.total >= shownCount
1202
+ ? props.total
1203
+ : (Number.isInteger(graph.total) ? graph.total : shownCount);
1204
+ const truncated = totalCount > shownCount;
1205
+ const nearIds = React.useMemo(() => {
1206
+ const set = new Set();
1207
+ if (!selectedId) return set;
1208
+ set.add(selectedId);
1209
+ for (const edge of graph.edges) {
1210
+ if (edge.from === selectedId) set.add(edge.to);
1211
+ if (edge.to === selectedId) set.add(edge.from);
1212
+ }
1213
+ return set;
1214
+ }, [graph.edges, selectedId]);
1215
+ const orderedEdges = selectedId
1216
+ ? [...graph.edges].sort((a, b) => Number(a.from === selectedId || a.to === selectedId) - Number(b.from === selectedId || b.to === selectedId))
1217
+ : graph.edges;
1218
+ const orderedNodes = selectedId
1219
+ ? [...graph.nodes].sort((a, b) => (Number(nearIds.has(a.id)) - Number(nearIds.has(b.id))) || (Number(a.id === selectedId) - Number(b.id === selectedId)))
1220
+ : graph.nodes;
1221
+ const pickHub = props.onPickHub;
1222
+ const MIN_ZOOM = 0.2;
1223
+ const MAX_ZOOM = 2.5;
1224
+ const stepZoom = (delta) => setZoom((value) => {
1225
+ const next = delta > 0 ? value * 1.25 : value / 1.25;
1226
+ return Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, Math.round(next * 100) / 100));
1227
+ });
1228
+ const pointAt = (event, matrix) => {
1229
+ const svg = svgRef.current;
1230
+ if (!svg || !matrix) return null;
1231
+ const point = svg.createSVGPoint();
1232
+ point.x = event.clientX;
1233
+ point.y = event.clientY;
1234
+ return point.matrixTransform(matrix);
1235
+ };
1236
+ const startPan = (event) => {
1237
+ if (event.button !== 0) return;
1238
+ if (event.target && event.target.closest && event.target.closest('[data-node]')) return;
1239
+ const svg = svgRef.current;
1240
+ if (!svg) return;
1241
+ const ctm = svg.getScreenCTM();
1242
+ if (!ctm) return;
1243
+ const inverse = ctm.inverse();
1244
+ const point = pointAt(event, inverse);
1245
+ if (!point) return;
1246
+ dragRef.current = [event.pointerId, inverse, point.x, point.y, pan.x, pan.y];
1247
+ setDragging(true);
1248
+ if (svg.setPointerCapture) svg.setPointerCapture(event.pointerId);
1249
+ event.preventDefault();
1250
+ };
1251
+ const movePan = (event) => {
1252
+ const drag = dragRef.current;
1253
+ if (!drag || drag[0] !== event.pointerId) return;
1254
+ const point = pointAt(event, drag[1]);
1255
+ if (!point) return;
1256
+ setPan({ x: drag[4] - (point.x - drag[2]), y: drag[5] - (point.y - drag[3]) });
1257
+ event.preventDefault();
1258
+ };
1259
+ const endPan = (event) => {
1260
+ const drag = dragRef.current;
1261
+ if (!drag || (event && drag[0] !== event.pointerId)) return;
1262
+ dragRef.current = null;
1263
+ setDragging(false);
1264
+ const svg = svgRef.current;
1265
+ if (event && svg && svg.releasePointerCapture && svg.hasPointerCapture && svg.hasPointerCapture(event.pointerId)) {
1266
+ svg.releasePointerCapture(event.pointerId);
1267
+ }
1268
+ };
1269
+ React.useEffect(() => {
1270
+ setPan({ x: 0, y: 0 });
1271
+ }, [props.records, primaryKind]);
692
1272
  const frame = React.useMemo(() => {
693
1273
  const list = graph.nodes;
694
1274
  if (list.length === 0) return { x: 0, y: 0, width: 640, height: 472 };
695
1275
  let minX = Infinity; let maxX = -Infinity; let minY = Infinity; let maxY = -Infinity;
696
1276
  for (const node of list) {
697
- const half = node.kind === primaryKind ? 62 : 38;
1277
+ const wide = node.kind === primaryKind && (graph.people <= 12 || hubCount === 0 || node.id === selectedId);
1278
+ const half = node.kind === primaryKind ? (wide ? 62 : 54) : 38;
698
1279
  minX = Math.min(minX, node.x - half);
699
1280
  maxX = Math.max(maxX, node.x + half);
700
1281
  minY = Math.min(minY, node.y - 16);
701
- maxY = Math.max(maxY, node.y + (node.kind === primaryKind ? 28 : 24));
1282
+ maxY = Math.max(maxY, node.y + (node.kind === primaryKind ? (wide ? 30 : 28) : 26));
702
1283
  }
703
1284
  const width = Math.max(380, maxX - minX + 36);
704
1285
  const height = Math.max(250, maxY - minY + 36);
705
1286
  return { x: (minX + maxX) / 2 - width / 2, y: (minY + maxY) / 2 - height / 2, width, height };
706
- }, [graph, primaryKind]);
1287
+ }, [graph, primaryKind, selectedId]);
707
1288
  if (!props.records || props.records.length === 0) {
708
1289
  return h('div', {
709
1290
  style: { flex: 1, minHeight: '220px', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--dsw-alias-label-secondary)', fontSize: '12px' },
@@ -711,80 +1292,184 @@ window.__ModuleLoader__.load({
711
1292
  }
712
1293
  const viewWidth = frame.width / zoom;
713
1294
  const viewHeight = frame.height / zoom;
714
- const zoomButton = (label, delta, icon) => h('button', {
715
- type: 'button', onClick: () => stepZoom(delta),
716
- disabled: delta > 0 ? zoom >= 2.5 : zoom <= 0.75,
717
- 'aria-label': label, title: label,
718
- style: {
719
- width: '26px', height: '26px', padding: 0, display: 'inline-flex',
720
- alignItems: 'center', justifyContent: 'center', border: 'none',
721
- borderRadius: '6px', background: 'transparent', color: 'inherit',
722
- opacity: delta > 0 ? (zoom >= 2.5 ? 0.28 : 0.68) : (zoom <= 0.75 ? 0.28 : 0.68),
723
- cursor: 'pointer',
724
- },
1295
+ const zoomButton = (label, delta, icon) => h(ZoomButton, {
1296
+ key: label, label, disabled: delta > 0 ? zoom >= MAX_ZOOM : zoom <= MIN_ZOOM,
1297
+ onClick: () => stepZoom(delta),
725
1298
  }, icon);
726
1299
  const magnifier = (sign) => h('svg', {
727
1300
  width: '15', height: '15', viewBox: '0 0 16 16', fill: 'none',
728
- stroke: 'currentColor', strokeWidth: '1.4', strokeLinecap: 'round',
1301
+ stroke: 'currentColor', 'stroke-width': '1.4', 'stroke-linecap': 'round',
729
1302
  'aria-hidden': 'true',
730
1303
  },
731
1304
  h('circle', { cx: '6.8', cy: '6.8', r: '4.5' }),
732
1305
  h('line', { x1: '10.1', y1: '10.1', x2: '14', y2: '14' }),
733
1306
  h('line', { x1: '4.8', y1: '6.8', x2: '8.8', y2: '6.8' }),
734
1307
  sign > 0 ? h('line', { x1: '6.8', y1: '4.8', x2: '6.8', y2: '8.8' }) : null);
1308
+ /** 标签避让:hub 常显、选中/悬停优先,记忆标签按度数贪心排布,重叠就省掉(悬停仍出标题)。 */
1309
+ const labelLayout = React.useMemo(() => {
1310
+ const nodes = graph.nodes;
1311
+ const fsOf = (primary) => (primary ? 10.5 : 10);
1312
+ const widthOf = (text, fs) => {
1313
+ let width = 0;
1314
+ for (const ch of text) width += ch.charCodeAt(0) > 255 ? fs : fs * 0.56;
1315
+ return width;
1316
+ };
1317
+ const rank = (node) => (node.id === selectedId ? 0 : node.id === hoveredId ? 1
1318
+ : nearIds.has(node.id) ? 2 : node.kind === primaryKind ? 4 : 3);
1319
+ const boxes = [];
1320
+ const placed = new Map();
1321
+ const hits = (box) => boxes.some((other) => !(box.x2 < other.x1 || box.x1 > other.x2 || box.y2 < other.y1 || box.y1 > other.y2));
1322
+ const list = nodes.slice().sort((a, b) => rank(a) - rank(b) || (b.degree || 0) - (a.degree || 0));
1323
+ /** 条目多时不全标,用度数最高的十条记忆给方向感。 */
1324
+ const topIds = new Set(list.filter((node) => node.kind === primaryKind).slice(0, 10).map((node) => node.id));
1325
+ for (const node of list) {
1326
+ const isPrimary = node.kind === primaryKind;
1327
+ if (isPrimary && !labelSet && rank(node) > 2 && !topIds.has(node.id)) continue;
1328
+ const chars = rank(node) <= 1 ? 16 : isPrimary ? (labelSet ? 10 : 8) : 13;
1329
+ const text = SHORT(node.label, chars);
1330
+ const fs = fsOf(isPrimary);
1331
+ const width = widthOf(text, fs);
1332
+ const gap = isPrimary ? 12 : 13;
1333
+ let spot = null;
1334
+ for (const below of [true, false]) {
1335
+ const baseline = below ? node.y + node.radius + gap : node.y - node.radius - gap + fs;
1336
+ const box = { x1: node.x - width / 2 - 3, x2: node.x + width / 2 + 3, y1: baseline - fs - 2, y2: baseline + 3 };
1337
+ if (box.y1 < 3 || box.y2 > graph.height - 3) continue;
1338
+ if (hits(box)) continue;
1339
+ spot = { box, baseline, below };
1340
+ break;
1341
+ }
1342
+ if (!spot) continue;
1343
+ boxes.push(spot.box);
1344
+ placed.set(node.id, { text, isPrimary, baseline: spot.baseline, fs, width });
1345
+ }
1346
+ return placed;
1347
+ }, [graph, primaryKind, selectedId, hoveredId, labelSet, nearIds]);
735
1348
  return h('div', { style: { position: 'relative', flex: 1, minHeight: 0, display: 'flex' } },
736
1349
  h('div', {
737
1350
  style: {
738
- position: 'absolute', top: '6px', right: '6px', zIndex: 2,
739
- display: 'flex', alignItems: 'center', gap: '1px', padding: '2px',
1351
+ position: 'absolute', top: '8px', right: '8px', zIndex: 2,
1352
+ display: 'flex', alignItems: 'center', gap: '2px', padding: '2px',
740
1353
  border: '1px solid var(--dsw-alias-border-l1)', borderRadius: '8px',
741
1354
  background: 'var(--dsw-alias-bg-layer-1)',
742
1355
  },
743
1356
  },
744
1357
  zoomButton('缩小图谱', -0.25, magnifier(-1)),
745
- zoomButton('放大图谱', 0.25, magnifier(1))),
1358
+ zoomButton('放大图谱', 0.25, magnifier(1)),
1359
+ h('button', {
1360
+ type: 'button', onClick: () => setZoom(1),
1361
+ 'aria-label': '重置缩放', title: '重置缩放',
1362
+ style: {
1363
+ minWidth: '44px', height: '28px', padding: '0 6px', border: 'none', borderRadius: '8px',
1364
+ background: 'transparent', color: 'var(--dsw-alias-label-secondary)',
1365
+ fontSize: '11px', lineHeight: '28px', fontVariantNumeric: 'tabular-nums', cursor: 'pointer',
1366
+ },
1367
+ }, `${Math.round(zoom * 100)}%`)),
746
1368
  h('svg', {
747
- viewBox: `${frame.x + frame.width / 2 - viewWidth / 2} ${frame.y + frame.height / 2 - viewHeight / 2} ${viewWidth} ${viewHeight}`,
1369
+ ref: svgRef,
1370
+ viewBox: `${frame.x + frame.width / 2 - viewWidth / 2 + pan.x} ${frame.y + frame.height / 2 - viewHeight / 2 + pan.y} ${viewWidth} ${viewHeight}`,
748
1371
  preserveAspectRatio: 'xMidYMid meet',
749
- style: { width: '100%', height: '100%', flex: 1, minHeight: 0, display: 'block' },
1372
+ style: {
1373
+ width: '100%', height: '100%', flex: 1, minHeight: 0, display: 'block',
1374
+ cursor: dragging ? 'grabbing' : 'grab', touchAction: 'none',
1375
+ userSelect: 'none', WebkitUserSelect: 'none',
1376
+ },
1377
+ onPointerDown: startPan,
1378
+ onPointerMove: movePan,
1379
+ onPointerUp: endPan,
1380
+ onPointerCancel: endPan,
1381
+ onLostPointerCapture: endPan,
1382
+ onDragStart: (event) => event.preventDefault(),
750
1383
  role: 'img', 'aria-label': props.ariaLabel || '关系图',
751
1384
  },
752
- h('g', null, graph.edges.map((edge, index) => {
1385
+ h('g', null, orderedEdges.map((edge, index) => {
753
1386
  const from = graph.nodes.find((node) => node.id === edge.from);
754
1387
  const to = graph.nodes.find((node) => node.id === edge.to);
755
1388
  if (!from || !to) return null;
756
1389
  const linked = selectedId && (edge.from === selectedId || edge.to === selectedId);
1390
+ const dimmed = selectedId && !nearIds.has(edge.from) && !nearIds.has(edge.to);
757
1391
  return h('line', {
758
1392
  key: `e${index}`, x1: from.x, y1: from.y, x2: to.x, y2: to.y,
759
1393
  stroke: linked ? 'var(--dsw-alias-brand-primary)' : 'var(--dsw-alias-label-secondary)',
760
- strokeWidth: linked ? 1.4 : 1,
761
- opacity: linked ? 0.65 : edge.type === 'link' ? 0.38 : 0.16,
762
- strokeDasharray: edge.type === 'link' ? undefined : '2 4',
1394
+ 'stroke-width': linked ? 1.4 : 1,
1395
+ opacity: linked ? 0.72 : dimmed ? 0.08 : edge.type === 'link' ? 0.38 : 0.16,
1396
+ 'stroke-dasharray': edge.type === 'link' ? undefined : '2 4',
763
1397
  });
764
1398
  })),
765
- h('g', null, graph.nodes.map((node) => {
1399
+ h('g', null, orderedNodes.map((node) => {
766
1400
  const isPrimary = node.kind === primaryKind;
767
1401
  const active = node.id === selectedId;
768
- const fill = !isPrimary ? 'var(--dsw-alias-label-secondary)'
769
- : node.state ? (STAGE_TONE[node.state] || 'var(--dsw-alias-brand-primary)')
770
- : node.experimental ? 'var(--dsw-alias-state-warn-primary)' : 'var(--dsw-alias-brand-primary)';
771
- const showLabel = active || (isPrimary && labelSet) || !isPrimary;
1402
+ const hovered = node.id === hoveredId;
1403
+ const near = nearIds.has(node.id);
1404
+ const dimmed = Boolean(selectedId) && !near;
1405
+ const pick = isPrimary ? props.onPick : (pickHub || undefined);
1406
+ /** 记忆是主体:实心、可读;主题/标签 hub 是背景:空心、只留轮廓。 */
1407
+ const fill = active ? 'var(--dsw-alias-brand-primary)'
1408
+ : isPrimary ? 'var(--dsw-alias-label-secondary)'
1409
+ : 'none';
1410
+ const stroke = active ? 'none'
1411
+ : hovered || near ? 'var(--dsw-alias-brand-primary)'
1412
+ : node.experimental ? 'var(--dsw-alias-state-warn-primary)'
1413
+ : isPrimary ? 'none' : 'var(--dsw-alias-label-secondary)';
1414
+ const strokeWidth = !active && (hovered || near) ? 1.3 : node.experimental ? 1 : isPrimary ? 0 : 1;
1415
+ const label = labelLayout.get(node.id) || null;
772
1416
  return h('g', {
773
1417
  key: node.id,
1418
+ 'data-node': 'true',
774
1419
  transform: `translate(${node.x} ${node.y})`,
775
- onClick: isPrimary ? () => props.onPick(node) : undefined,
776
- style: { cursor: isPrimary ? 'pointer' : 'default' },
1420
+ onClick: pick ? () => pick(node) : undefined,
1421
+ onKeyDown: pick ? (event) => {
1422
+ if (event.key === 'Enter' || event.key === ' ' || event.key === 'Spacebar') {
1423
+ event.preventDefault();
1424
+ pick(node);
1425
+ }
1426
+ } : undefined,
1427
+ onFocus: () => setHoveredId(node.id),
1428
+ onBlur: () => setHoveredId((value) => (value === node.id ? '' : value)),
1429
+ onMouseEnter: () => setHoveredId(node.id),
1430
+ onMouseLeave: () => setHoveredId((value) => (value === node.id ? '' : value)),
1431
+ tabIndex: pick ? 0 : undefined,
1432
+ role: pick ? 'button' : undefined,
1433
+ focusable: pick ? 'true' : 'false',
1434
+ 'aria-label': pick ? (node.title || node.label) : undefined,
1435
+ style: { cursor: pick ? 'pointer' : 'default', outline: 'none' },
777
1436
  },
778
- active ? h('circle', { r: node.radius + 5, fill: 'none', stroke: 'var(--dsw-alias-brand-primary)', strokeWidth: 1.4, opacity: 0.5 }) : null,
779
- h('circle', { r: node.radius, fill, opacity: isPrimary ? 0.9 : 0.5 }),
780
- showLabel ? h('text', {
781
- y: node.radius + 12, textAnchor: 'middle',
782
- fill: active ? 'var(--dsw-alias-label-primary)' : 'var(--dsw-alias-label-secondary)',
783
- fontSize: isPrimary ? 10 : 8.5, opacity: active ? 0.95 : 0.68,
784
- }, SHORT(node.label, isPrimary ? 9 : 10)) : null,
1437
+ h('circle', { r: Math.max(isPrimary ? 14 : 18, node.radius + 6), fill: 'transparent' }),
1438
+ active ? h('circle', { r: node.radius + 5, fill: 'none', stroke: 'var(--dsw-alias-brand-primary)', 'stroke-width': 1.4, opacity: 0.45 }) : null,
1439
+ h('circle', {
1440
+ r: node.radius, fill,
1441
+ opacity: active ? 1 : dimmed ? (isPrimary ? 0.32 : 0.14) : isPrimary ? 0.92 : 0.34,
1442
+ stroke, 'stroke-width': strokeWidth,
1443
+ }),
1444
+ label ? h('text', {
1445
+ y: label.baseline, 'text-anchor': 'middle',
1446
+ fill: label.isPrimary ? 'var(--dsw-alias-label-primary)' : 'var(--dsw-alias-label-secondary)',
1447
+ 'font-size': label.fs,
1448
+ 'font-weight': 500,
1449
+ opacity: active ? 1 : dimmed ? 0.2 : label.isPrimary ? 0.9 : 0.72,
1450
+ stroke: 'var(--dsw-alias-bg-layer-1)', 'stroke-width': 2.6, 'stroke-linejoin': 'round', 'paint-order': 'stroke',
1451
+ }, label.text) : null,
785
1452
  h('title', null, node.title || node.label),
786
1453
  );
787
1454
  }))),
1455
+ h('div', {
1456
+ style: {
1457
+ position: 'absolute', left: '10px', bottom: '8px', zIndex: 1,
1458
+ maxWidth: truncated ? 'calc(100% - 178px)' : 'calc(100% - 60px)', padding: '2px 6px', borderRadius: '6px',
1459
+ background: 'var(--dsw-alias-bg-layer-1)',
1460
+ color: 'var(--dsw-alias-label-secondary)', fontSize: '11px', lineHeight: '1.5',
1461
+ pointerEvents: 'none',
1462
+ },
1463
+ }, props.legend || '实心 记忆 · 空心 标签/主题 · 虚线 归类'),
1464
+ truncated ? h('div', {
1465
+ style: {
1466
+ position: 'absolute', right: '10px', bottom: '8px', zIndex: 1,
1467
+ padding: '2px 6px', borderRadius: '6px',
1468
+ background: 'var(--dsw-alias-bg-layer-1)',
1469
+ color: 'var(--dsw-alias-label-secondary)', fontSize: '11px', lineHeight: '1.5',
1470
+ pointerEvents: 'none', whiteSpace: 'nowrap',
1471
+ },
1472
+ }, `显示 ${shownCount} / 共 ${totalCount} 条`) : null,
788
1473
  );
789
1474
  };
790
1475
 
@@ -801,7 +1486,7 @@ window.__ModuleLoader__.load({
801
1486
  style: {
802
1487
  position: 'absolute', top: '2px', left: props.on ? '18px' : '2px', width: '16px', height: '16px',
803
1488
  borderRadius: '999px', background: props.on ? 'var(--dsw-alias-bg-base)' : 'var(--dsw-alias-label-secondary)',
804
- transition: 'left 120ms ease',
1489
+ transition: reduceMotionOf() ? 'none' : 'left 120ms ease',
805
1490
  },
806
1491
  }));
807
1492
 
@@ -871,22 +1556,41 @@ window.__ModuleLoader__.load({
871
1556
  const evidenceTextOf = (row) => {
872
1557
  if (typeof row === 'string') return row;
873
1558
  if (!row || typeof row !== 'object') return '';
874
- return typeof row.text === 'string' ? row.text : (typeof row.note === 'string' ? row.note : '');
1559
+ for (const key of ['sourceExcerpt', 'excerpt', 'text', 'note']) {
1560
+ if (typeof row[key] === 'string' && row[key].trim()) return row[key];
1561
+ }
1562
+ return '';
1563
+ };
1564
+ /** 样例和证据都带版本/核对信息,不拿「样例」二字占位。 */
1565
+ const evidenceMetaOf = (row) => {
1566
+ if (!row || typeof row !== 'object') return '';
1567
+ const bits = [];
1568
+ if (Number.isInteger(row.artifactVersion)) bits.push(`第 ${row.artifactVersion} 版`);
1569
+ else if (typeof row.artifactVersion === 'string' && row.artifactVersion) bits.push(`第 ${row.artifactVersion} 版`);
1570
+ if (typeof row.dimension === 'string' && row.dimension) bits.push(row.dimension);
1571
+ if (row.verified === true) bits.push('已核对');
1572
+ else if (row.verified === false) bits.push('未核对');
1573
+ if (row.at) bits.push(EVO_DATE(row.at));
1574
+ return bits.join(' · ');
875
1575
  };
876
1576
 
877
1577
  const MemoryRow = (props) => {
878
1578
  const record = props.record;
1579
+ const [hover, setHover] = React.useState(false);
879
1580
  return h('button', {
880
1581
  type: 'button', onClick: props.onClick,
1582
+ onMouseEnter: () => setHover(true),
1583
+ onMouseLeave: () => setHover(false),
881
1584
  style: {
882
- display: 'flex', flexDirection: 'column', gap: '2px', width: '100%', textAlign: 'left',
883
- border: 'none', borderBottom: '1px solid var(--dsw-alias-border-l1)',
884
- background: props.active ? 'var(--dsw-alias-bg-layer-2)' : 'transparent',
885
- color: 'inherit', padding: '8px 7px', cursor: 'pointer', minWidth: 0,
1585
+ display: 'flex', flexDirection: 'column', gap: 0, width: '100%', textAlign: 'left',
1586
+ border: 'none', borderBottom: '1px solid var(--dsw-alias-border-l1)', borderRadius: '6px',
1587
+ borderLeft: `2px solid ${props.active ? 'var(--dsw-alias-brand-primary)' : 'transparent'}`,
1588
+ background: props.active || hover ? 'var(--dsw-alias-bg-layer-2)' : 'transparent',
1589
+ color: 'inherit', padding: '4px 8px', cursor: 'pointer', minWidth: 0,
886
1590
  },
887
1591
  },
888
- h('span', { style: { fontSize: '12px', lineHeight: '1.45', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', opacity: 0.92 } }, record.title || record.id),
889
- h('span', { style: { ...PANEL_MUTED, display: 'flex', alignItems: 'center', gap: '5px', minWidth: 0 } },
1592
+ h('span', { style: { fontSize: '12px', lineHeight: '16px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, record.title || record.id),
1593
+ h('span', { style: { ...PANEL_MUTED, display: 'flex', alignItems: 'center', gap: '5px', minWidth: 0, lineHeight: '14px' } },
890
1594
  h('span', null, KIND_LABEL[record.kind] || record.kind),
891
1595
  record.experimental ? h('span', { style: { color: 'var(--dsw-alias-state-warn-primary)' } }, '实验') : null,
892
1596
  h('span', { style: { marginLeft: 'auto', flex: '0 0 auto', fontVariantNumeric: 'tabular-nums' } }, EVO_DATE(record.updated)),
@@ -894,6 +1598,29 @@ window.__ModuleLoader__.load({
894
1598
  );
895
1599
  };
896
1600
 
1601
+ /** 一条经验:状态用小标签,标题独占一行。 */
1602
+ const EvolutionRow = (props) => {
1603
+ const rule = props.rule;
1604
+ const [hover, setHover] = React.useState(false);
1605
+ return h('button', {
1606
+ type: 'button', onClick: props.onClick,
1607
+ onMouseEnter: () => setHover(true),
1608
+ onMouseLeave: () => setHover(false),
1609
+ style: {
1610
+ display: 'flex', flexDirection: 'column', gap: '1px', width: '100%', textAlign: 'left',
1611
+ border: 'none', borderRadius: '8px', minWidth: 0,
1612
+ borderLeft: `2px solid ${props.active ? 'var(--dsw-alias-brand-primary)' : 'transparent'}`,
1613
+ background: props.active || hover ? 'var(--dsw-alias-bg-layer-2)' : 'transparent',
1614
+ color: 'inherit', padding: '6px 9px', cursor: 'pointer',
1615
+ },
1616
+ },
1617
+ h('span', { style: { fontSize: '12px', lineHeight: '18px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, rule.title),
1618
+ h('span', { style: { ...PANEL_MUTED, display: 'flex', alignItems: 'center', gap: '6px', minWidth: 0, lineHeight: '16px' } },
1619
+ h(StagePill, { state: rule.state }),
1620
+ h('span', { style: { marginLeft: 'auto', flex: '0 0 auto', fontVariantNumeric: 'tabular-nums' } }, EVO_DATE(rule.updated)),
1621
+ ));
1622
+ };
1623
+
897
1624
  const EvolutionDetail = (props) => {
898
1625
  const rule = props.rule;
899
1626
  const ruleId = rule ? rule.id : '';
@@ -916,8 +1643,12 @@ window.__ModuleLoader__.load({
916
1643
  const versions = Array.isArray(rule.learning?.versions) ? rule.learning.versions : EMPTY;
917
1644
  const history = [
918
1645
  ...checkpoints.map((row) => ({ at: row.at || row.updated || '', text: typeof row.text === 'string' ? row.text : '' })),
919
- ...versions.map((row) => ({ at: row.at || row.updated || '', text: typeof row.title === 'string' ? row.title : '修改' })),
1646
+ ...versions.map((row, index) => ({
1647
+ at: row.at || row.updated || '',
1648
+ text: `第 ${versions.length - index} 版${row.body ? ` ${SHORT(row.body, 42)}` : row.digest ? ` ${SHORT(String(row.digest), 16)}` : ''}`,
1649
+ })),
920
1650
  ].filter((row) => row.text).sort((a, b) => String(b.at).localeCompare(String(a.at))).slice(0, 6);
1651
+ const failureState = failureStateOf(props.evolution);
921
1652
  const canManage = props.canManage && !props.busy;
922
1653
  const act = (name, extra) => { if (canManage) props.onManage(name, extra || {}); };
923
1654
  const actions = props.canManage
@@ -969,8 +1700,24 @@ window.__ModuleLoader__.load({
969
1700
  evidence.length === 0 && samples.length === 0
970
1701
  ? '暂无学习依据'
971
1702
  : [
972
- ...evidence.slice(0, 3).map((row, index) => h('div', { key: `e${index}` }, evidenceTextOf(row))),
973
- ...samples.slice(0, 2).map((row, index) => h('div', { key: `s${index}` }, evidenceTextOf(row) || '样例')),
1703
+ ...evidence.slice(0, 3).map((row, index) => {
1704
+ const text = evidenceTextOf(row);
1705
+ const meta = evidenceMetaOf(row);
1706
+ return text
1707
+ ? h('div', { key: `e${index}`, style: { display: 'flex', flexDirection: 'column', gap: '1px' } },
1708
+ h('span', { style: { color: 'var(--dsw-alias-label-primary)', opacity: 0.8, whiteSpace: 'pre-wrap', wordBreak: 'break-word' } }, text),
1709
+ meta ? h('span', null, meta) : null)
1710
+ : null;
1711
+ }),
1712
+ ...samples.slice(0, 2).map((row, index) => {
1713
+ const text = evidenceTextOf(row);
1714
+ const meta = evidenceMetaOf(row);
1715
+ return text || meta
1716
+ ? h('div', { key: `s${index}`, style: { display: 'flex', flexDirection: 'column', gap: '1px' } },
1717
+ text ? h('span', { style: { whiteSpace: 'pre-wrap', wordBreak: 'break-word' } }, text) : null,
1718
+ meta ? h('span', null, meta) : null)
1719
+ : null;
1720
+ }),
974
1721
  ].filter(Boolean))
975
1722
  : null;
976
1723
  const historyPane = open === 'history'
@@ -998,6 +1745,10 @@ window.__ModuleLoader__.load({
998
1745
  h(StagePill, { state: rule.state }),
999
1746
  ),
1000
1747
  h('div', { style: { fontSize: '12px', lineHeight: '1.55', opacity: 0.82 } }, usabilityOf(rule, props.evolution, props.memoryOn)),
1748
+ failureState
1749
+ ? h('div', { style: { ...PANEL_MUTED, color: failureState === 'current' ? 'var(--dsw-alias-state-warn-primary)' : undefined } },
1750
+ failureState === 'current' ? '最近一次整理失败,现有经验未更新' : '以前有整理失败,最近一次已完成')
1751
+ : null,
1001
1752
  actions,
1002
1753
  ),
1003
1754
  editor,
@@ -1010,6 +1761,178 @@ window.__ModuleLoader__.load({
1010
1761
  );
1011
1762
  };
1012
1763
 
1764
+
1765
+ const PalaceSlot = (props) => {
1766
+ const record = props.record;
1767
+ if (!record) {
1768
+ return h('div', {
1769
+ 'aria-hidden': 'true',
1770
+ style: { height: '46px', border: '1px dashed var(--dsw-alias-border-l1)', borderRadius: '8px', opacity: 0.28 },
1771
+ });
1772
+ }
1773
+ const title = String(record.title || record.id || '记忆');
1774
+ return h('button', {
1775
+ type: 'button', title, 'aria-label': title,
1776
+ onClick: () => props.onPick(record.id),
1777
+ style: {
1778
+ height: '46px', minWidth: 0, display: 'flex', flexDirection: 'column', justifyContent: 'center',
1779
+ padding: '4px 7px', textAlign: 'left', cursor: 'pointer',
1780
+ border: `1px solid ${props.active ? 'var(--dsw-alias-brand-primary)' : 'var(--dsw-alias-border-l1)'}`,
1781
+ borderRadius: '8px', color: 'inherit',
1782
+ background: props.active ? 'var(--dsw-alias-bg-layer-2)' : 'var(--dsw-alias-bg-base)',
1783
+ },
1784
+ },
1785
+ h('span', {
1786
+ style: {
1787
+ fontSize: '11px', lineHeight: '15px', height: '30px', overflow: 'hidden',
1788
+ display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', wordBreak: 'break-word',
1789
+ },
1790
+ }, title),
1791
+ );
1792
+ };
1793
+
1794
+ const PalaceRoomCard = (props) => {
1795
+ const room = props.room;
1796
+ const empty = room.total === 0;
1797
+ const preview = room.items.slice(0, PALACE_PREVIEW);
1798
+ const slots = Array.from({ length: PALACE_PREVIEW }, (_, index) => preview[index] || null);
1799
+ return h('div', {
1800
+ role: 'listitem',
1801
+ style: { ...PANEL_CARD, padding: 0, position: 'relative', overflow: 'hidden', display: 'flex', width: '100%', flexDirection: 'column' },
1802
+ },
1803
+ h('div', { style: { position: 'relative', display: 'flex', flexDirection: 'column', gap: '8px', padding: '9px 10px 10px', boxSizing: 'border-box' } },
1804
+ h('button', {
1805
+ type: 'button', 'aria-expanded': empty ? undefined : props.expanded, 'aria-controls': empty ? undefined : `lite-palace-room-${room.id}`,
1806
+ disabled: empty,
1807
+ onClick: () => { if (!empty) props.onToggle(room.id); },
1808
+ style: { ...PANEL_LINK, width: '100%', display: 'flex', alignItems: 'center', gap: '7px', textAlign: 'left', opacity: 1 },
1809
+ },
1810
+ h('span', { style: { fontSize: '12px', lineHeight: '18px', fontWeight: 600, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, room.name),
1811
+ h('span', { style: { ...PANEL_MUTED, flex: '0 0 auto', fontVariantNumeric: 'tabular-nums' } }, `${room.total} 条`),
1812
+ empty ? null : h('span', { style: { ...PANEL_MUTED, flex: '0 0 auto', marginLeft: 'auto', opacity: 0.6 } }, props.expanded ? '收起' : '展开'),
1813
+ ),
1814
+ empty
1815
+ ? h('span', { style: { ...PANEL_MUTED, padding: '2px 0 4px' } }, '还没有记录')
1816
+ : h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(96px, 1fr))', gap: '6px' } },
1817
+ slots.map((record, index) => h(PalaceSlot, {
1818
+ key: record ? record.id : `slot-${room.id}-${index}`,
1819
+ record, active: Boolean(record && record.id === props.selectedId), onPick: props.onPick,
1820
+ }))),
1821
+ ),
1822
+ );
1823
+ };
1824
+
1825
+ const MemoryPalace = (props) => {
1826
+ const rows = Array.isArray(props.records) ? props.records : EMPTY;
1827
+ const [expanded, setExpanded] = React.useState('');
1828
+ const [page, setPage] = React.useState(1);
1829
+ const panelRef = React.useRef(null);
1830
+ const reduced = useReducedMotion();
1831
+ const motion = palaceMotionOf(reduced);
1832
+ const rooms = React.useMemo(() => palaceRoomsOf(rows), [rows]);
1833
+ const activeRoom = rooms.find((room) => room.id === expanded) || null;
1834
+ const pageInfo = activeRoom ? palaceRoomPageOf(rows, activeRoom.id, page) : null;
1835
+
1836
+ React.useEffect(() => {
1837
+ if (expanded && !rooms.some((room) => room.id === expanded)) setExpanded('');
1838
+ }, [rooms, expanded]);
1839
+ React.useEffect(() => {
1840
+ const node = panelRef.current;
1841
+ if (!expanded || !node || motion.duration === 0 || typeof node.animate !== 'function') return;
1842
+ node.animate([
1843
+ { opacity: 0, transform: 'translateY(-6px)' },
1844
+ { opacity: 1, transform: 'translateY(0)' },
1845
+ ], { duration: motion.duration, easing: motion.easing });
1846
+ }, [expanded, page, motion.duration, motion.easing]);
1847
+
1848
+ if (rows.length === 0) {
1849
+ return h('div', { style: { ...PANEL_CARD, padding: '14px', minHeight: 0 } },
1850
+ h('div', { style: PANEL_MUTED }, props.emptyText || '这个项目还没有记忆'));
1851
+ }
1852
+ const toggle = (roomId) => {
1853
+ setExpanded((current) => palaceToggleOf(current, roomId));
1854
+ setPage(1);
1855
+ };
1856
+ const goPage = (target) => {
1857
+ if (!pageInfo) return;
1858
+ setPage(Math.min(pageInfo.pages, Math.max(1, target)));
1859
+ };
1860
+ const pageButton = (label, target, disabled, current) => h('button', {
1861
+ key: `page-${target}-${label}`, type: 'button', disabled,
1862
+ 'aria-current': current ? 'page' : undefined, 'aria-label': `第 ${target} 页`,
1863
+ onClick: () => goPage(target),
1864
+ style: {
1865
+ ...PANEL_LINK, minWidth: '22px', height: '22px', padding: '0 5px', borderRadius: '6px',
1866
+ border: 'none', opacity: disabled ? 0.28 : current ? 0.85 : 0.5,
1867
+ background: current ? 'var(--dsw-alias-bg-layer-2)' : 'transparent',
1868
+ fontVariantNumeric: 'tabular-nums', cursor: disabled ? 'default' : 'pointer',
1869
+ },
1870
+ }, label);
1871
+ const pageSlots = pageInfo
1872
+ ? Array.from({ length: pageInfo.items.length ? Math.ceil(pageInfo.items.length / 6) * 6 : 0 }, (_, index) => pageInfo.items[index] || null)
1873
+ : EMPTY;
1874
+ return h('div', {
1875
+ role: 'group', 'aria-label': '记忆宫殿',
1876
+ onKeyDown: (event) => {
1877
+ if (!pageInfo || pageInfo.pages <= 1) return;
1878
+ if (event.key === 'ArrowRight' && pageInfo.page < pageInfo.pages) {
1879
+ event.preventDefault();
1880
+ goPage(pageInfo.page + 1);
1881
+ }
1882
+ if (event.key === 'ArrowLeft' && pageInfo.page > 1) {
1883
+ event.preventDefault();
1884
+ goPage(pageInfo.page - 1);
1885
+ }
1886
+ if (event.key === 'Home' && pageInfo.page > 1) {
1887
+ event.preventDefault();
1888
+ goPage(1);
1889
+ }
1890
+ if (event.key === 'End' && pageInfo.page < pageInfo.pages) {
1891
+ event.preventDefault();
1892
+ goPage(pageInfo.pages);
1893
+ }
1894
+ },
1895
+ style: { ...PANEL_CARD, padding: '10px', minHeight: 0, flex: '0 0 auto', overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: '10px' },
1896
+ },
1897
+ h('div', {
1898
+ role: 'list',
1899
+ 'aria-label': '记忆房间',
1900
+ style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(232px, 1fr))', gap: '9px' },
1901
+ },
1902
+ rooms.map((room) => h(PalaceRoomCard, {
1903
+ key: room.id, room, expanded: room.id === expanded, selectedId: props.selectedId,
1904
+ onPick: props.onPick, onToggle: toggle,
1905
+ }))),
1906
+ h('div', { 'aria-live': 'polite', style: { ...PANEL_MUTED, marginTop: '-2px' } }, `${rooms.length} 个房间 · ${rows.length} 条`),
1907
+ activeRoom && activeRoom.total > 0 && pageInfo
1908
+ ? h('section', {
1909
+ id: `lite-palace-room-${activeRoom.id}`, 'aria-label': activeRoom.name, ref: panelRef,
1910
+ style: { borderTop: '1px solid var(--dsw-alias-border-l1)', paddingTop: '9px', display: 'flex', flexDirection: 'column', gap: '8px' },
1911
+ },
1912
+ h('div', { style: { display: 'flex', alignItems: 'center', gap: '8px' } },
1913
+ h('span', { style: { fontSize: '12px', lineHeight: '1.4', fontWeight: 600 } }, activeRoom.name),
1914
+ h('span', { style: PANEL_MUTED, 'aria-live': 'polite', 'aria-atomic': 'true' }, `${pageInfo.total} 条${pageInfo.pages > 1 ? ` · 第 ${pageInfo.page} / ${pageInfo.pages} 页` : ''}`),
1915
+ h('button', {
1916
+ type: 'button', onClick: () => toggle(activeRoom.id),
1917
+ style: { ...PANEL_LINK, marginLeft: 'auto', height: '24px', padding: '0 7px', borderRadius: '6px', border: '1px solid var(--dsw-alias-border-l1)' },
1918
+ }, '收起'),
1919
+ ),
1920
+ h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(96px, 1fr))', gap: '6px' } },
1921
+ pageSlots.map((record, index) => h(PalaceSlot, {
1922
+ key: record ? record.id : `page-${pageInfo.page}-slot-${index}`,
1923
+ record, active: Boolean(record && record.id === props.selectedId), onPick: props.onPick,
1924
+ }))),
1925
+ pageInfo.pages > 1
1926
+ ? h('div', { role: 'navigation', 'aria-label': '分页', style: { display: 'flex', alignItems: 'center', gap: '4px', flexWrap: 'wrap' } },
1927
+ pageButton('上一页', pageInfo.page - 1, pageInfo.page <= 1, false),
1928
+ palacePageWindow(pageInfo.page, pageInfo.pages).map((number) => pageButton(String(number), number, false, number === pageInfo.page)),
1929
+ pageButton('下一页', pageInfo.page + 1, pageInfo.page >= pageInfo.pages, false))
1930
+ : null,
1931
+ )
1932
+ : null,
1933
+ );
1934
+ };
1935
+
1013
1936
  const MemoryPanel = (props) => {
1014
1937
  const sessionId = props.sessionId;
1015
1938
  const [data, setData] = React.useState(null);
@@ -1027,9 +1950,14 @@ window.__ModuleLoader__.load({
1027
1950
  const [detailNote, setDetailNote] = React.useState('');
1028
1951
  const [busy, setBusy] = React.useState(false);
1029
1952
  const [showReason, setShowReason] = React.useState(false);
1953
+ const [showSettings, setShowSettings] = React.useState(false);
1954
+ const [display, setDisplay] = React.useState({ ...DISPLAY_DEFAULTS });
1955
+ const [hubFilter, setHubFilter] = React.useState(null);
1030
1956
  const alive = React.useRef(true);
1031
1957
  const generation = React.useRef(0);
1032
1958
  const scopeRef = React.useRef('');
1959
+ /** 先算出当前范围键,显示设置的读取/落盘都按它来,不能等后面的派生变量。 */
1960
+ const selectedKey = data && data.selected && data.selected.key ? data.selected.key : '';
1033
1961
  React.useEffect(() => {
1034
1962
  alive.current = true;
1035
1963
  return () => { alive.current = false; };
@@ -1066,6 +1994,22 @@ window.__ModuleLoader__.load({
1066
1994
 
1067
1995
  React.useEffect(() => { load(); }, [sessionId]);
1068
1996
 
1997
+ /** 显示设置跟着当前项目走;换项目或换范围时重新读。 */
1998
+ React.useEffect(() => {
1999
+ setDisplay(readDisplaySettings(selectedKey));
2000
+ setShowSettings(false);
2001
+ setHubFilter(null);
2002
+ }, [selectedKey]);
2003
+ React.useEffect(() => {
2004
+ setMemoryView((value) => viewModeOf(display, value));
2005
+ setEvolutionView((value) => viewModeOf(display, value));
2006
+ }, [display.graph, display.palace]);
2007
+ const setDisplayFlag = (name, value) => {
2008
+ const next = { ...display, [name]: value === true };
2009
+ setDisplay(next);
2010
+ writeDisplaySettings(selectedKey, next);
2011
+ };
2012
+
1069
2013
  React.useEffect(() => {
1070
2014
  if (typeof window === 'undefined') return undefined;
1071
2015
  const onKey = (event) => { if (event.key === 'Escape') { event.stopPropagation(); props.onClose(); } };
@@ -1098,7 +2042,6 @@ window.__ModuleLoader__.load({
1098
2042
  const records = selected && Array.isArray(selected.records) ? selected.records : EMPTY;
1099
2043
  const total = selected && selected.stats && Number.isInteger(selected.stats.total) ? selected.stats.total : records.length;
1100
2044
  const currentKey = current ? current.key : '';
1101
- const selectedKey = selected ? selected.key : '';
1102
2045
  const control = selected && selected.control
1103
2046
  ? selected.control
1104
2047
  : (current && selected && selected.key === current.key && current.control ? current.control : { enabled: true });
@@ -1126,6 +2069,11 @@ window.__ModuleLoader__.load({
1126
2069
  const changedCount = activity && typeof activity === 'object' && Number.isInteger(activity.changed)
1127
2070
  ? activity.changed
1128
2071
  : (Number.isInteger(activity && activity.count) ? activity.count : null);
2072
+ const activityReason = activity && typeof activity === 'object' && typeof activity.reason === 'string'
2073
+ ? activity.reason
2074
+ : (latestReceipt && typeof latestReceipt.reason === 'string' ? latestReceipt.reason : '');
2075
+ const staleLearning = /没采用|作废|已更新|换到别的作品/.test(activityReason);
2076
+ const activityReasonText = staleLearning ? '整理过程中反馈或作品变了,这轮结果没采用。' : activityReason;
1129
2077
  let activityText = '';
1130
2078
  if (!evolution && memoryOn) {
1131
2079
  activityText = '读不到自进化状态';
@@ -1142,7 +2090,7 @@ window.__ModuleLoader__.load({
1142
2090
  } else if (activityCode === 'quota' || activityCode === 'exhausted') {
1143
2091
  activityText = '本轮整理额度已用完';
1144
2092
  } else if (activityCode === 'failed' || activityCode === 'error') {
1145
- activityText = '最近整理失败,可查看原因';
2093
+ activityText = staleLearning ? '这轮整理没采用' : '最近整理失败,可查看原因';
1146
2094
  } else if (activityCode === 'waiting' || activityCode === 'idle' || activityCode === 'empty') {
1147
2095
  activityText = '等待可学习的反馈';
1148
2096
  }
@@ -1155,18 +2103,7 @@ window.__ModuleLoader__.load({
1155
2103
  else if (rules.length === 0) activityText = countsUnknown ? '等待可学习的反馈' : '还没有整理出经验';
1156
2104
  else activityText = '暂无整理记录';
1157
2105
  }
1158
- const activityReason = activity && typeof activity === 'object' && typeof activity.reason === 'string'
1159
- ? activity.reason
1160
- : (latestReceipt && typeof latestReceipt.reason === 'string' ? latestReceipt.reason : '');
1161
-
1162
- const filtered = React.useMemo(() => {
1163
- const needle = query.trim().toLowerCase();
1164
- if (!needle) return records;
1165
- return records.filter((record) => {
1166
- const tags = Array.isArray(record.tags) ? record.tags.join(' ') : '';
1167
- return `${record.title} ${record.summary} ${tags} ${KIND_LABEL[record.kind] || ''}`.toLowerCase().includes(needle);
1168
- });
1169
- }, [records, query]);
2106
+ const filtered = React.useMemo(() => filterRecords(records, query, hubFilter), [records, query, hubFilter]);
1170
2107
  const filteredRules = React.useMemo(() => {
1171
2108
  if (stage === 'all') return rules;
1172
2109
  return rules.filter((rule) => rule.state === stage);
@@ -1179,6 +2116,19 @@ window.__ModuleLoader__.load({
1179
2116
  setSelectedMemory(node.id);
1180
2117
  setSelectedRule('');
1181
2118
  };
2119
+ /** 点 hub 用精确值筛选,标签不发散成子串匹配。 */
2120
+ const onPickHub = (node) => {
2121
+ if (!node || typeof node.id !== 'string') return;
2122
+ const cut = node.id.indexOf(':');
2123
+ const kind = cut > 0 ? node.id.slice(0, cut) : '';
2124
+ const value = cut > 0 ? node.id.slice(cut + 1) : '';
2125
+ if ((kind !== 'tag' && kind !== 'topic') || !value) return;
2126
+ setHubFilter({ kind, value, display: kind === 'tag' ? `#${value}` : value });
2127
+ setQuery('');
2128
+ setSelectedMemory('');
2129
+ setSelectedRule('');
2130
+ setMemoryView('list');
2131
+ };
1182
2132
  const onPickRuleNode = (node) => {
1183
2133
  if (node.kind !== 'rule') return;
1184
2134
  setSelectedRule(node.id);
@@ -1250,31 +2200,47 @@ window.__ModuleLoader__.load({
1250
2200
  style: { height: '30px', maxWidth: '190px', padding: '0 8px', border: '1px solid var(--dsw-alias-border-l1)', borderRadius: '8px', background: 'var(--dsw-alias-bg-layer-2)', color: 'inherit', fontSize: '12px' },
1251
2201
  },
1252
2202
  data && Array.isArray(data.scopes) ? data.scopes.map((scope) => h('option', { key: scope.key, value: scope.key }, scope.kind === 'global' ? '全局' : scope.label)) : null) : null,
2203
+ h('button', {
2204
+ type: 'button', 'aria-expanded': showSettings, 'aria-label': '显示设置',
2205
+ onClick: () => setShowSettings((value) => !value),
2206
+ style: {
2207
+ ...PANEL_LINK, height: '30px', padding: '0 9px', borderRadius: '8px',
2208
+ border: '1px solid var(--dsw-alias-border-l1)',
2209
+ background: showSettings ? 'var(--dsw-alias-bg-layer-2)' : 'transparent',
2210
+ opacity: showSettings ? 0.92 : 0.68,
2211
+ },
2212
+ }, '显示设置'),
1253
2213
  closeButton,
1254
2214
  ),
1255
2215
  );
1256
2216
 
1257
- const memorySwitch = h('div', { style: { ...PANEL_CARD, flex: '1 1 300px', display: 'flex', flexDirection: 'column', gap: '4px', padding: '9px 11px' } },
1258
- h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '8px' } },
1259
- h('span', { style: { fontSize: '12px', lineHeight: '1.4', fontWeight: 600 } }, '启用记忆'),
1260
- h(Toggle, { ariaLabel: '启用记忆', on: memoryOn, busy: busy || !canToggle, onClick: canToggle ? toggleControl : undefined }),
1261
- ),
1262
- h('div', { style: PANEL_MUTED }, '读取项目和全局记忆'), // ui-copy-ignore 用户指定文案
2217
+ const displayToggle = (label, key) => switchItem(
2218
+ label, '', display[key] === true, !selectedKey,
2219
+ selectedKey ? () => setDisplayFlag(key, display[key] !== true) : undefined,
1263
2220
  );
1264
- const evolutionSwitch = h('div', { style: { ...PANEL_CARD, flex: '1 1 300px', display: 'flex', flexDirection: 'column', gap: '4px', padding: '9px 11px', opacity: memoryOn ? 1 : 0.55 } },
1265
- h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '8px' } },
1266
- h('span', { style: { fontSize: '12px', lineHeight: '1.4', fontWeight: 600 } }, '自进化'),
1267
- h(Toggle, { ariaLabel: '自进化', on: Boolean(memoryOn && evolution && evolution.enabled === true), busy: busy || !evolution || !canToggle || !memoryOn, onClick: canToggle && memoryOn ? toggleEvolution : undefined }),
2221
+
2222
+ const switchItem = (label, hint, on, disabled, onClick) => h('div', {
2223
+ style: { display: 'flex', flexDirection: 'column', gap: '1px', minWidth: 0, opacity: disabled && !on ? 0.55 : 1 },
2224
+ },
2225
+ h('span', { style: { display: 'flex', alignItems: 'center', gap: '8px', minWidth: 0 } },
2226
+ h('span', { style: { fontSize: '12px', lineHeight: '20px', fontWeight: 600, whiteSpace: 'nowrap' } }, label),
2227
+ h(Toggle, { ariaLabel: label, on, busy: busy || disabled, onClick: disabled ? undefined : onClick }),
2228
+ ),
2229
+ hint ? h('span', { style: { ...PANEL_MUTED, lineHeight: '16px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, hint) : null);
2230
+ const evolutionOn = Boolean(memoryOn && evolution && evolution.enabled === true);
2231
+ const switchBar = h('div', {
2232
+ style: { display: 'flex', flexDirection: 'column', gap: '3px', padding: '7px 18px 8px', borderBottom: '1px solid var(--dsw-alias-border-l1)', flex: '0 0 auto' },
2233
+ },
2234
+ globalView
2235
+ ? h('div', { style: { ...PANEL_MUTED, lineHeight: '20px' } }, '全局记忆只读')
2236
+ : h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(190px, 1fr))', gap: '4px 18px' } },
2237
+ switchItem('启用记忆', memoryOn ? '读取项目和全局记忆' : '记忆已关闭,内容只读', memoryOn, busy || !canToggle, toggleControl),
2238
+ switchItem('自进化', evolutionOn ? '实验功能,会增加 token 消耗' : '从反馈里整理经验', evolutionOn, busy || !evolution || !canToggle || !memoryOn, toggleEvolution), // ui-copy-ignore 用户指定文案
2239
+ showSettings ? displayToggle('启用图谱', 'graph') : null,
2240
+ showSettings ? displayToggle('启用记忆宫殿', 'palace') : null,
1268
2241
  ),
1269
- h('div', { style: PANEL_MUTED }, '从反馈中整理经验,按需使用'), // ui-copy-ignore 用户指定文案
1270
- h('div', { style: PANEL_MUTED }, '实验功能,会增加 token 消耗'), // ui-copy-ignore 用户指定文案
1271
- );
1272
- const switchBar = h('div', { style: { display: 'flex', alignItems: 'stretch', gap: '10px', flexWrap: 'wrap', padding: '12px 18px', borderBottom: '1px solid var(--dsw-alias-border-l1)', flex: '0 0 auto' } },
1273
- globalView ? h('div', { style: { ...PANEL_MUTED, padding: '2px 0' } }, '全局记忆只读') : memorySwitch,
1274
- globalView ? null : evolutionSwitch,
1275
- !memoryOn && !globalView ? h('div', { style: { ...PANEL_MUTED, flexBasis: '100%' } }, '记忆已关闭,内容只读') : null,
1276
- !globalView && !canToggle ? h('div', { style: { ...PANEL_MUTED, flexBasis: '100%' } }, '只读,切换回当前项目后可修改') : null,
1277
- note ? h('div', { style: { ...PANEL_MUTED, flexBasis: '100%', color: 'var(--dsw-alias-state-error-primary)' } }, note) : null,
2242
+ !globalView && !canToggle ? h('div', { style: { ...PANEL_MUTED, lineHeight: '18px' } }, '只读,切换回当前项目后可修改') : null,
2243
+ note ? h('div', { style: { ...PANEL_MUTED, lineHeight: '18px', color: 'var(--dsw-alias-state-error-primary)' } }, note) : null,
1278
2244
  );
1279
2245
 
1280
2246
  const switchTab = (key) => {
@@ -1298,15 +2264,20 @@ window.__ModuleLoader__.load({
1298
2264
  globalView ? null : tabButton('evolution', '自进化'),
1299
2265
  );
1300
2266
 
2267
+ const filteredNow = Boolean(query || hubFilter);
1301
2268
  const memoryList = loading
1302
- ? h('div', { style: { ...PANEL_MUTED, padding: '12px 4px' } }, '读取中…')
2269
+ ? h('div', { style: { ...PANEL_MUTED, padding: '14px 9px' } }, '读取中…')
1303
2270
  : filtered.length === 0
1304
- ? h('div', { style: { ...PANEL_MUTED, padding: '12px 4px' } }, query ? '没有匹配的记忆' : '还没有记忆')
2271
+ ? h('div', { style: { display: 'flex', alignItems: 'center', gap: '8px', padding: '14px 9px', minWidth: 0 } },
2272
+ h('span', { style: { ...PANEL_MUTED, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, filteredNow ? '没有匹配的记忆' : '还没有记忆'),
2273
+ filteredNow ? h(Action, { onClick: () => { setHubFilter(null); setQuery(''); } }, '清除搜索') : null,
2274
+ )
1305
2275
  : filtered.map((record) => h(MemoryRow, {
1306
2276
  key: record.id, record, active: record.id === selectedMemory,
1307
2277
  onClick: () => { setSelectedMemory(record.id); setSelectedRule(''); },
1308
2278
  }));
1309
2279
 
2280
+ const detailProcessing = detail ? processingStatusOf(detail) : '';
1310
2281
  const memoryDetail = selectedMemory
1311
2282
  ? h('div', { style: { ...PANEL_CARD, minHeight: 0, overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: '6px' } },
1312
2283
  detail
@@ -1315,17 +2286,18 @@ window.__ModuleLoader__.load({
1315
2286
  h('div', { style: { flex: 1, minWidth: 0, fontSize: '13px', lineHeight: '1.45', fontWeight: 600, wordBreak: 'break-word' } }, detail.title),
1316
2287
  h(Action, { onClick: () => setSelectedMemory('') }, '关闭'),
1317
2288
  ),
1318
- h('div', { style: PANEL_MUTED }, `${KIND_LABEL[detailRecord && detailRecord.kind] || '记忆'} · ${detail.scopeLabel || ''}`),
2289
+ h('div', { style: PANEL_MUTED }, `${KIND_LABEL[detailRecord && detailRecord.kind] || '记忆'} · ${detail.scopeLabel || ''}${detailProcessing ? ` · ${PROCESSING_LABEL[detailProcessing]}` : ''}`),
1319
2290
  Array.isArray(detailRecord && detailRecord.tags) && detailRecord.tags.length
1320
2291
  ? h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: '4px' } },
1321
2292
  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,
1322
- h('div', { style: { fontSize: '12px', lineHeight: '1.65', whiteSpace: 'pre-wrap', wordBreak: 'break-word' } }, detail.body || '没有正文'),
2293
+ h('div', { style: { fontSize: '12px', lineHeight: '1.65', whiteSpace: 'pre-wrap', wordBreak: 'break-word' } }, currentBodyOf(detail) || '没有正文'),
2294
+ h(DetailExtras, { payload: detail }),
1323
2295
  )
1324
2296
  : h('div', { style: PANEL_MUTED }, detailNote || '读取中…'),
1325
2297
  )
1326
2298
  : null;
1327
2299
 
1328
- const viewToggleOf = (current, setValue, ariaLabel) => {
2300
+ const viewToggleOf = (current, setValue, ariaLabel, modes) => {
1329
2301
  const button = (key, label) => h('button', {
1330
2302
  key, type: 'button', onClick: () => setValue(key),
1331
2303
  'aria-pressed': current === key,
@@ -1335,6 +2307,7 @@ window.__ModuleLoader__.load({
1335
2307
  opacity: current === key ? 0.9 : 0.5,
1336
2308
  },
1337
2309
  }, label);
2310
+ const options = Array.isArray(modes) ? modes : [['list', '列表'], ['graph', '图谱']];
1338
2311
  return h('div', {
1339
2312
  role: 'group', 'aria-label': ariaLabel,
1340
2313
  style: {
@@ -1343,23 +2316,41 @@ window.__ModuleLoader__.load({
1343
2316
  border: '1px solid var(--dsw-alias-border-l1)', borderRadius: '8px',
1344
2317
  background: 'var(--dsw-alias-bg-base)',
1345
2318
  },
1346
- }, button('list', '列表'), button('graph', '图谱'));
2319
+ }, options.map(([key, label]) => button(key, label)));
1347
2320
  };
1348
- const memoryToggle = viewToggleOf(memoryView, setMemoryView, '记忆视图');
2321
+ const memoryModes = [['list', '列表']];
2322
+ if (display.graph) memoryModes.push(['graph', '图谱']);
2323
+ if (display.palace) memoryModes.push(['palace', '宫殿']);
2324
+ const memoryToggle = memoryModes.length > 1 ? viewToggleOf(memoryView, setMemoryView, '记忆视图', memoryModes) : null;
2325
+ const memoryViewActive = viewModeOf(display, memoryView);
2326
+ const evolutionViewActive = graphViewOf(display.graph, evolutionView);
1349
2327
 
1350
2328
  const memoryListPane = h('div', {
1351
2329
  style: {
1352
2330
  ...PANEL_CARD, padding: '2px 4px', minHeight: 0, overflowY: 'auto',
2331
+ display: 'flex', flexDirection: 'column', gap: 0,
1353
2332
  },
1354
2333
  }, memoryList);
1355
2334
  const memoryGraphPane = h('div', {
1356
2335
  style: { ...PANEL_CARD, padding: 0, minHeight: 0, overflow: 'hidden', display: 'flex' },
1357
2336
  },
1358
- h(MemoryGraph, { records: filtered, selectedId: selectedMemory, onPick: onPickNode, emptyText: query ? '没有匹配的记忆' : '还没有记忆', ariaLabel: '记忆关系图' }));
2337
+ h(MemoryGraph, {
2338
+ records: filtered, total: hubFilter ? filtered.length : total,
2339
+ selectedId: selectedMemory, onPick: onPickNode,
2340
+ onPickHub,
2341
+ emptyText: query || hubFilter ? '没有匹配的记忆' : '还没有记忆', ariaLabel: '记忆关系图',
2342
+ legend: '实心 记忆 · 空心 标签/主题 · 虚线 归类 · 点标签筛选,点记忆看正文',
2343
+ }));
2344
+ const memoryPalacePane = h(MemoryPalace, {
2345
+ key: selectedKey, records: filtered, selectedId: selectedMemory,
2346
+ onPick: (id) => { setSelectedMemory(id); setSelectedRule(''); },
2347
+ emptyText: query || hubFilter ? '没有匹配的记忆' : '这个项目还没有记忆',
2348
+ });
1359
2349
  const memoryTab = h('div', { role: 'tabpanel', 'aria-label': '记忆', style: { flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', gap: '9px' } },
1360
2350
  h('div', { style: { display: 'flex', alignItems: 'center', gap: '8px', flex: '0 0 auto' } },
1361
2351
  h('input', {
1362
- value: query, onChange: (event) => setQuery(event.target.value),
2352
+ value: hubFilter ? hubFilter.display : query,
2353
+ onChange: (event) => { setHubFilter(null); setQuery(event.target.value); },
1363
2354
  placeholder: '搜索标题或标签', 'aria-label': '搜索记忆',
1364
2355
  style: { ...PANEL_INPUT, width: 'auto', flex: '1 1 0', minWidth: 0 },
1365
2356
  }),
@@ -1371,7 +2362,7 @@ window.__ModuleLoader__.load({
1371
2362
  gridTemplateColumns: selectedMemory ? 'minmax(0, 1fr) minmax(280px, 340px)' : 'minmax(0, 1fr)',
1372
2363
  },
1373
2364
  },
1374
- memoryView === 'graph' ? memoryGraphPane : memoryListPane,
2365
+ memoryViewActive === 'graph' ? memoryGraphPane : memoryViewActive === 'palace' ? memoryPalacePane : memoryListPane,
1375
2366
  memoryDetail),
1376
2367
  );
1377
2368
 
@@ -1397,7 +2388,7 @@ window.__ModuleLoader__.load({
1397
2388
  fontVariantNumeric: 'tabular-nums',
1398
2389
  },
1399
2390
  }, `${label} ${countText(key)}`)),
1400
- h('span', { style: { marginLeft: 'auto' } }, viewToggleOf(evolutionView, setEvolutionView, '自进化视图')),
2391
+ h('span', { style: { marginLeft: 'auto' } }, display.graph ? viewToggleOf(evolutionView, setEvolutionView, '自进化视图') : null),
1401
2392
  );
1402
2393
 
1403
2394
  const stageLabel = (stageFilters.find(([key]) => key === stage) || ['all', '全部'])[1];
@@ -1407,21 +2398,14 @@ window.__ModuleLoader__.load({
1407
2398
  : rules.length === 0 ? (countsUnknown ? '数量未知' : '还没有整理出经验')
1408
2399
  : `没有${stageLabel}的经验`;
1409
2400
  const evolutionList = filteredRules.length === 0
1410
- ? h('div', { style: { ...PANEL_MUTED, padding: '12px 4px' } }, evolutionEmptyText)
1411
- : filteredRules.map((rule) => h('button', {
1412
- key: rule.id, type: 'button', onClick: () => { setSelectedRule(rule.id); setSelectedMemory(''); setShowReason(false); },
1413
- style: {
1414
- display: 'flex', flexDirection: 'column', gap: '3px', width: '100%', textAlign: 'left',
1415
- border: 'none', borderBottom: '1px solid var(--dsw-alias-border-l1)',
1416
- background: rule.id === selectedRule ? 'var(--dsw-alias-bg-layer-2)' : 'transparent',
1417
- color: 'inherit', padding: '8px 7px', cursor: 'pointer', minWidth: 0,
1418
- },
1419
- },
1420
- h('span', { style: { fontSize: '12px', lineHeight: '1.45', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', opacity: 0.92 } }, rule.title),
1421
- h('span', { style: { ...PANEL_MUTED, display: 'flex', alignItems: 'center', gap: '6px', minWidth: 0 } },
1422
- h(StagePill, { state: rule.state }),
1423
- h('span', { style: { marginLeft: 'auto', flex: '0 0 auto', fontVariantNumeric: 'tabular-nums' } }, EVO_DATE(rule.updated)),
1424
- )));
2401
+ ? h('div', { style: { display: 'flex', alignItems: 'center', gap: '8px', padding: '14px 9px', minWidth: 0 } },
2402
+ h('span', { style: { ...PANEL_MUTED, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, evolutionEmptyText),
2403
+ stage !== 'all' && (memoryOn && evolution && evolution.enabled === true) ? h(Action, { onClick: () => setStage('all') }, '看全部') : null,
2404
+ )
2405
+ : filteredRules.map((rule) => h(EvolutionRow, {
2406
+ key: rule.id, rule, active: rule.id === selectedRule,
2407
+ onClick: () => { setSelectedRule(rule.id); setSelectedMemory(''); setShowReason(false); },
2408
+ }));
1425
2409
 
1426
2410
  const evolutionReady = selectedKey === currentKey;
1427
2411
  const evolutionFailed = !evolutionReady || failed
@@ -1442,16 +2426,18 @@ window.__ModuleLoader__.load({
1442
2426
  : null;
1443
2427
  const evolutionListPane = h('div', {
1444
2428
  style: {
1445
- ...PANEL_CARD, padding: '2px 4px', minHeight: 0, overflowY: 'auto',
2429
+ ...PANEL_CARD, padding: '4px', minHeight: 0, overflowY: 'auto',
2430
+ display: 'flex', flexDirection: 'column', gap: '2px',
1446
2431
  },
1447
2432
  }, evolutionList);
1448
2433
  const evolutionGraphPane = h('div', {
1449
2434
  style: { ...PANEL_CARD, padding: 0, minHeight: 0, overflow: 'hidden', display: 'flex' },
1450
2435
  },
1451
2436
  h(MemoryGraph, {
1452
- records: filteredRules, graphOf: ruleGraphOf, primaryKind: 'rule',
2437
+ records: filteredRules, total: filteredRules.length, graphOf: ruleGraphOf, primaryKind: 'rule',
1453
2438
  selectedId: selectedRule, onPick: onPickRuleNode,
1454
2439
  emptyText: evolutionEmptyText, ariaLabel: '经验关系图',
2440
+ legend: '实心 经验 · 空心 条件 · 虚线 共用条件 · 点经验查看',
1455
2441
  }));
1456
2442
  const evolutionTab = evolutionFailed ? evolutionFailed : h('div', { role: 'tabpanel', 'aria-label': '自进化', style: { flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', gap: '9px' } },
1457
2443
  h('div', { style: { ...PANEL_CARD, padding: '9px 11px', display: 'flex', alignItems: 'flex-start', gap: '8px', flex: '0 0 auto' } },
@@ -1459,9 +2445,9 @@ window.__ModuleLoader__.load({
1459
2445
  h('div', { style: { display: 'flex', flexDirection: 'column', gap: '2px', minWidth: 0 } },
1460
2446
  h('div', { style: { fontSize: '12px', lineHeight: '1.55' } }, activityText || (rules.length === 0 ? '还没有整理出经验' : '暂无整理记录')),
1461
2447
  countsUnknown ? h('div', { style: PANEL_MUTED }, '数量未知') : null,
1462
- activityReason
2448
+ activityReasonText
1463
2449
  ? (showReason
1464
- ? h('div', { style: { ...PANEL_MUTED, whiteSpace: 'pre-wrap', wordBreak: 'break-word' } }, activityReason)
2450
+ ? h('div', { style: { ...PANEL_MUTED, whiteSpace: 'pre-wrap', wordBreak: 'break-word' } }, activityReasonText)
1465
2451
  : h(Action, { onClick: () => setShowReason(true) }, '查看原因'))
1466
2452
  : null,
1467
2453
  ),
@@ -1473,11 +2459,17 @@ window.__ModuleLoader__.load({
1473
2459
  gridTemplateColumns: selectedRuleRow ? 'minmax(0, 1fr) minmax(280px, 340px)' : 'minmax(0, 1fr)',
1474
2460
  },
1475
2461
  },
1476
- evolutionView === 'graph' ? evolutionGraphPane : evolutionListPane,
2462
+ evolutionViewActive === 'graph' ? evolutionGraphPane : evolutionListPane,
1477
2463
  evolutionDetailPane),
1478
2464
  );
1479
2465
 
1480
- const content = h('section', { style: { flex: 1, display: 'flex', flexDirection: 'column', minWidth: 0, minHeight: 0, padding: '14px', gap: '10px' } },
2466
+ const palaceLoose = activeTab === 'memory' && viewModeOf(display, memoryView) === 'palace' && !selectedMemory;
2467
+ const content = h('section', {
2468
+ style: {
2469
+ flex: palaceLoose ? '0 1 auto' : 1, display: 'flex', flexDirection: 'column', minWidth: 0, minHeight: 0,
2470
+ padding: '12px 18px 14px', gap: '10px', overflowY: palaceLoose ? 'auto' : 'visible',
2471
+ },
2472
+ },
1481
2473
  tabBar,
1482
2474
  activeTab === 'memory' ? memoryTab : evolutionTab,
1483
2475
  );
@@ -1488,7 +2480,10 @@ window.__ModuleLoader__.load({
1488
2480
  },
1489
2481
  h('div', {
1490
2482
  role: 'dialog', 'aria-modal': 'true', 'aria-label': '记忆面板',
1491
- style: { ...PANEL_DIALOG, width: 'min(1120px, 100%)', height: 'min(760px, 100%)', boxSizing: 'border-box' },
2483
+ style: {
2484
+ ...PANEL_DIALOG, width: 'min(1120px, 100%)', boxSizing: 'border-box',
2485
+ height: palaceLoose ? 'auto' : 'min(760px, 100%)', maxHeight: 'min(760px, 100%)',
2486
+ },
1492
2487
  }, ...children));
1493
2488
 
1494
2489
  if (!sessionId) return null;
@@ -1533,12 +2528,22 @@ window.__ModuleLoader__.load({
1533
2528
  update: list.filter((item) => item.exposure === 'update'),
1534
2529
  };
1535
2530
  }, [refs]);
2531
+ const noTriggerReason = typeof props.noTriggerReason === 'string' && props.noTriggerReason
2532
+ ? props.noTriggerReason
2533
+ : (typeof refs.noTriggerReason === 'string' ? refs.noTriggerReason : '');
1536
2534
  if (!sessionId && refs.length === 0) return null;
1537
- if (groups.found.length === 0 && groups.provided.length === 0 && groups.update.length === 0) return null;
2535
+ if (groups.found.length === 0 && groups.provided.length === 0 && groups.update.length === 0) {
2536
+ return noTriggerReason
2537
+ ? h('div', { style: TAIL }, h(RefSection, { label: '搜索命中', tone: 'found', items: EMPTY, sessionId, turnKey: turn, noTriggerReason }))
2538
+ : null;
2539
+ }
2540
+ const section = (label, items, tone) => (items.length === 0
2541
+ ? null
2542
+ : h(RefSection, { key: label, label, tone, items, sessionId, turnKey: turn }));
1538
2543
  return h('div', { style: TAIL },
1539
- h(RefSection, { label: '检索发现', items: groups.found, sessionId, turnKey: turn }),
1540
- h(RefSection, { label: '已提供正文', items: groups.provided, sessionId, turnKey: turn }),
1541
- h(RefSection, { label: '本轮更新', items: groups.update, sessionId, turnKey: turn }),
2544
+ section('搜索命中', groups.found, 'found'),
2545
+ section('读过正文', groups.provided, 'provided'),
2546
+ section('本轮更新', groups.update, 'update'),
1542
2547
  );
1543
2548
  };
1544
2549
 
@@ -1546,7 +2551,7 @@ window.__ModuleLoader__.load({
1546
2551
 
1547
2552
  // 测试缝:Node 里验引用投影,浏览器里没有任何副作用。
1548
2553
  if (typeof globalThis.__LITE_MEMORY_TEST__ === 'function') {
1549
- globalThis.__LITE_MEMORY_TEST__({ metaOf, citationOf, citationsOfMeta, citationsOfTurn, dedupe, undoRefOf, graphOf, ruleGraphOf, MemoryPanel });
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 });
1550
2555
  }
1551
2556
 
1552
2557
  return {