dsh-lite-memory 0.11.1 → 0.15.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/README.md +44 -18
- package/anchor.js +110 -13
- package/client.js +1078 -117
- package/docs/RELEASE-0.12.0.md +15 -0
- package/docs/RELEASE-0.12.1.md +17 -0
- package/docs/RELEASE-0.12.2.md +17 -0
- package/docs/RELEASE-0.13.0.md +7 -0
- package/docs/RELEASE-0.14.0.md +7 -0
- package/docs/RELEASE-0.15.0.md +8 -0
- package/index.js +579 -78
- package/learning.js +58 -32
- package/package.json +2 -2
- package/state.js +107 -8
- package/store.js +302 -29
- package/test/anchor.test.mjs +221 -0
- package/test/client.test.mjs +48 -6
- package/test/contract.test.mjs +214 -11
- package/test/control.test.mjs +278 -0
- package/test/evolution-read.test.mjs +63 -8
- package/test/learning.test.mjs +4 -2
- package/test/lexical.test.mjs +33 -0
- package/test/recall.test.mjs +11 -9
- package/test/state.test.mjs +21 -0
- package/test/store.test.mjs +25 -3
- package/view.js +16 -1
package/client.js
CHANGED
|
@@ -71,6 +71,9 @@ window.__ModuleLoader__.load({
|
|
|
71
71
|
/** 失败不装作成功:把服务端那一句原样放回行里。 */
|
|
72
72
|
const failure = (result, fallback) => (result && result.body && typeof result.body.reason === 'string' && result.body.reason ? result.body.reason : fallback);
|
|
73
73
|
|
|
74
|
+
/** 真成功要 HTTP 与响应体同时确认;2xx 但 ok:false 仍按失败显示。 */
|
|
75
|
+
const succeeded = (result) => Boolean(result && result.ok && result.body && result.body.ok === true);
|
|
76
|
+
|
|
74
77
|
/** 已落定、带元数据的调用块;旧行只读。 */
|
|
75
78
|
const metaOf = (block) => {
|
|
76
79
|
if (block === undefined || block === null || !('kind' in block) || block.isError === true) return null;
|
|
@@ -131,15 +134,19 @@ window.__ModuleLoader__.load({
|
|
|
131
134
|
return out;
|
|
132
135
|
};
|
|
133
136
|
|
|
134
|
-
/**
|
|
137
|
+
/** 同轮同 id 同用途去重:位置取首次,内容取最后。 */
|
|
135
138
|
const dedupe = (items) => {
|
|
136
|
-
const seen = new
|
|
139
|
+
const seen = new Map();
|
|
137
140
|
const out = [];
|
|
138
141
|
for (const item of items) {
|
|
139
|
-
const key = `${item.scope}:${item.id}:${item.
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
142
|
+
const key = `${item.scope}:${item.id}:${item.exposure}`;
|
|
143
|
+
const at = seen.get(key);
|
|
144
|
+
if (at === undefined) {
|
|
145
|
+
seen.set(key, out.length);
|
|
146
|
+
out.push(item);
|
|
147
|
+
} else {
|
|
148
|
+
out[at] = item;
|
|
149
|
+
}
|
|
143
150
|
}
|
|
144
151
|
return out;
|
|
145
152
|
};
|
|
@@ -248,7 +255,7 @@ window.__ModuleLoader__.load({
|
|
|
248
255
|
if (!operationId.current) operationId.current = newOperationId();
|
|
249
256
|
setState('saving');
|
|
250
257
|
post('/lite-memory/keep', { op: 'keep', candidateId, operationId: operationId.current, cwd, sessionId: sessionKeyOf(props) }).then((result) => {
|
|
251
|
-
if (result
|
|
258
|
+
if (succeeded(result)) {
|
|
252
259
|
setState('saved');
|
|
253
260
|
return;
|
|
254
261
|
}
|
|
@@ -287,7 +294,7 @@ window.__ModuleLoader__.load({
|
|
|
287
294
|
if (!canDrop) return;
|
|
288
295
|
setState('saving');
|
|
289
296
|
post('/lite-memory/undo', { op: 'undo', id: changed.id, revision: changed.revision, cwd }).then((result) => {
|
|
290
|
-
if (result
|
|
297
|
+
if (succeeded(result)) {
|
|
291
298
|
setState('dropped');
|
|
292
299
|
setNote(result.body && result.body.mode === 'restored' ? 'memory · restored' : 'memory · archived');
|
|
293
300
|
return;
|
|
@@ -310,9 +317,8 @@ window.__ModuleLoader__.load({
|
|
|
310
317
|
|
|
311
318
|
const versionLine = (shown) => {
|
|
312
319
|
const scope = shown.scopeLabel || (shown.scope === 'global' ? '全局' : '项目');
|
|
313
|
-
const version = shown.current === false ? `第 ${shown.revision} 版` : `当前第 ${shown.revision} 版`;
|
|
314
320
|
const archived = shown.status === 'archived' ? ' · 已归档' : '';
|
|
315
|
-
return `${
|
|
321
|
+
return `${scope}${archived}`;
|
|
316
322
|
};
|
|
317
323
|
|
|
318
324
|
/** 一段引用:点开是名字,再点是当前正文。 */
|
|
@@ -412,15 +418,6 @@ window.__ModuleLoader__.load({
|
|
|
412
418
|
};
|
|
413
419
|
|
|
414
420
|
const EVO_BUTTON = { flex: '0 0 auto', width: '28px', height: '28px', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', border: 'none', background: 'transparent', color: 'inherit', borderRadius: '999px', padding: 0, cursor: 'pointer' };
|
|
415
|
-
const EVO_WRAP = { position: 'relative', display: 'flex', alignItems: 'center' };
|
|
416
|
-
const EVO_PANEL = {
|
|
417
|
-
position: 'absolute', right: 0, top: 'calc(100% + 6px)', zIndex: 30, width: '280px', maxHeight: '420px', overflowY: 'auto',
|
|
418
|
-
padding: '8px', border: '1px solid currentColor', borderRadius: '8px',
|
|
419
|
-
background: 'Canvas', color: 'CanvasText', boxShadow: '0 4px 18px rgba(0, 0, 0, 0.22)',
|
|
420
|
-
display: 'flex', flexDirection: 'column', gap: '6px', fontSize: '12px', lineHeight: '1.6',
|
|
421
|
-
};
|
|
422
|
-
const EVO_MUTED = { opacity: 0.65 };
|
|
423
|
-
const EVO_ACTION = { ...KEEP_ON, alignSelf: 'flex-start' };
|
|
424
421
|
|
|
425
422
|
/** 大脑:两瓣轮廓加一条中线,状态只靠透明度和面板文字。 */
|
|
426
423
|
const BrainIcon = () => h('svg', {
|
|
@@ -432,133 +429,1092 @@ window.__ModuleLoader__.load({
|
|
|
432
429
|
h('path', { d: 'M12 5a3 3 0 1 1 5.997.125 4 4 0 0 1 2.526 5.77 4 4 0 0 1-.556 6.588A4 4 0 1 1 12 18Z' }),
|
|
433
430
|
h('path', { d: 'M15 13a4.5 4.5 0 0 1-3-4 4.5 4.5 0 0 1-3 4' }));
|
|
434
431
|
|
|
432
|
+
const CloseIcon = () => h('svg', {
|
|
433
|
+
width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none',
|
|
434
|
+
stroke: 'currentColor', strokeWidth: 1.6, strokeLinecap: 'round',
|
|
435
|
+
'aria-hidden': 'true', focusable: 'false',
|
|
436
|
+
}, h('path', { d: 'M6 6l12 12M18 6L6 18' }));
|
|
437
|
+
|
|
435
438
|
const EVO_STATE = { pending: '候选', trial: '试用', active: '生效', paused: '暂停' };
|
|
439
|
+
const KIND_LABEL = {
|
|
440
|
+
preference: '偏好', convention: '约定', procedure: '流程', pointer: '位置',
|
|
441
|
+
pitfall: '坑', credential_hint: '凭据位置', note: '笔记',
|
|
442
|
+
};
|
|
443
|
+
const CONDITION_LABEL = {
|
|
444
|
+
seriesId: '系列', taskKind: '任务', targetOs: '系统', shell: '终端', runtime: '运行时',
|
|
445
|
+
machine: '设备', projectRoot: '项目路径', pathPrefix: '路径前缀', artifactKey: '产物', phase: '阶段',
|
|
446
|
+
};
|
|
436
447
|
const EVO_DATE = (value) => (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}/.test(value) ? value.slice(0, 10) : '');
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
448
|
+
const SHORT = (value, size) => {
|
|
449
|
+
const text = typeof value === 'string' ? value : '';
|
|
450
|
+
return text.length > size ? `${text.slice(0, size)}…` : text;
|
|
451
|
+
};
|
|
452
|
+
/** 面板开关。 */
|
|
453
|
+
const panelUi = { open: false, sessionId: '' };
|
|
454
|
+
const panelListeners = new Set();
|
|
455
|
+
const updatePanel = (patch) => {
|
|
456
|
+
Object.assign(panelUi, patch);
|
|
457
|
+
for (const listener of panelListeners) listener();
|
|
458
|
+
};
|
|
459
|
+
const usePanelUi = () => {
|
|
460
|
+
const [, bump] = React.useState(0);
|
|
461
|
+
React.useEffect(() => {
|
|
462
|
+
const listener = () => bump((value) => value + 1);
|
|
463
|
+
panelListeners.add(listener);
|
|
464
|
+
return () => { panelListeners.delete(listener); };
|
|
465
|
+
}, []);
|
|
466
|
+
return panelUi;
|
|
467
|
+
};
|
|
441
468
|
|
|
442
|
-
/**
|
|
469
|
+
/** 会话头:只留一个图标按钮,开合由浮层里的面板负责。 */
|
|
443
470
|
const ProjectEvolution = (props) => {
|
|
444
471
|
const sessionId = sessionKeyOf(props);
|
|
445
|
-
const
|
|
446
|
-
|
|
447
|
-
|
|
472
|
+
const ui = usePanelUi();
|
|
473
|
+
|
|
474
|
+
React.useEffect(() => {
|
|
475
|
+
if (!sessionId) return undefined;
|
|
476
|
+
if (panelUi.sessionId && panelUi.sessionId !== sessionId) updatePanel({ open: false });
|
|
477
|
+
updatePanel({ sessionId });
|
|
478
|
+
return undefined;
|
|
479
|
+
}, [sessionId]);
|
|
480
|
+
|
|
481
|
+
if (!sessionId) return null;
|
|
482
|
+
return h('button', {
|
|
483
|
+
type: 'button',
|
|
484
|
+
onClick: () => updatePanel({ open: true, sessionId }),
|
|
485
|
+
'aria-haspopup': 'dialog',
|
|
486
|
+
'aria-expanded': ui.open === true && ui.sessionId === sessionId,
|
|
487
|
+
'aria-label': '记忆面板',
|
|
488
|
+
title: '记忆面板',
|
|
489
|
+
style: { ...EVO_BUTTON, opacity: 0.72 },
|
|
490
|
+
}, h(BrainIcon));
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
const PANEL_CARD = {
|
|
494
|
+
background: 'var(--dsw-alias-bg-layer-1)',
|
|
495
|
+
border: '1px solid var(--dsw-alias-border-l1)',
|
|
496
|
+
borderRadius: '12px',
|
|
497
|
+
padding: '12px',
|
|
498
|
+
minWidth: 0,
|
|
499
|
+
};
|
|
500
|
+
const PANEL_MUTED = { color: 'var(--dsw-alias-label-secondary)', fontSize: '11px', lineHeight: '1.5' };
|
|
501
|
+
const PANEL_LABEL = { color: 'var(--dsw-alias-label-secondary)', fontSize: '11px', lineHeight: '1.4', letterSpacing: '0.02em' };
|
|
502
|
+
const PANEL_LINK = {
|
|
503
|
+
border: 'none', background: 'transparent', color: 'inherit', padding: 0,
|
|
504
|
+
fontSize: '11px', lineHeight: '18px', opacity: 0.72, cursor: 'pointer',
|
|
505
|
+
};
|
|
506
|
+
const PANEL_INPUT = {
|
|
507
|
+
width: '100%', boxSizing: 'border-box', height: '30px', padding: '0 9px',
|
|
508
|
+
border: '1px solid var(--dsw-alias-border-l1)', borderRadius: '8px',
|
|
509
|
+
background: 'var(--dsw-alias-bg-base)', color: 'var(--dsw-alias-label-primary)',
|
|
510
|
+
fontSize: '12px', outline: 'none',
|
|
511
|
+
};
|
|
512
|
+
const PANEL_BACKDROP = {
|
|
513
|
+
position: 'fixed', inset: 0, zIndex: 1000, display: 'flex', alignItems: 'center',
|
|
514
|
+
justifyContent: 'center', padding: '24px', pointerEvents: 'auto',
|
|
515
|
+
background: 'color-mix(in srgb, var(--dsw-alias-bg-base) 58%, transparent)',
|
|
516
|
+
};
|
|
517
|
+
const PANEL_DIALOG = {
|
|
518
|
+
width: 'min(1240px, 100%)', height: 'min(780px, 100%)', display: 'flex',
|
|
519
|
+
flexDirection: 'column', background: 'var(--dsw-alias-bg-base)',
|
|
520
|
+
color: 'var(--dsw-alias-label-primary)', border: '1px solid var(--dsw-alias-border-l1)',
|
|
521
|
+
borderRadius: '16px', boxShadow: '0 24px 70px rgba(0, 0, 0, 0.28)', overflow: 'hidden',
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
const hashOf = (value) => {
|
|
525
|
+
let hash = 2166136261;
|
|
526
|
+
const text = String(value || '');
|
|
527
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
528
|
+
hash ^= text.charCodeAt(index);
|
|
529
|
+
hash = Math.imul(hash, 16777619);
|
|
530
|
+
}
|
|
531
|
+
return Math.abs(hash);
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* 图谱只画真实关系:显式关联,以及记录自己的标签 / 主题。
|
|
536
|
+
* 位置由 id 决定,同一份记忆每次打开都在同一处。
|
|
537
|
+
*/
|
|
538
|
+
/** 位置由 id 决定;同一份数据每次打开都在同一处。 */
|
|
539
|
+
const layoutGraph = (nodes, edges, primaryKind) => {
|
|
540
|
+
const degree = new Map();
|
|
541
|
+
for (const edge of edges) {
|
|
542
|
+
degree.set(edge.from, (degree.get(edge.from) || 0) + 1);
|
|
543
|
+
degree.set(edge.to, (degree.get(edge.to) || 0) + 1);
|
|
544
|
+
}
|
|
545
|
+
const placed = new Map();
|
|
546
|
+
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
|
+
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
|
+
}
|
|
568
|
+
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);
|
|
574
|
+
}
|
|
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) => {
|
|
581
|
+
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
|
+
});
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
return {
|
|
596
|
+
nodes: [...placed.values()],
|
|
597
|
+
edges: edges.filter((edge) => placed.has(edge.from) && placed.has(edge.to)),
|
|
598
|
+
people: count,
|
|
599
|
+
};
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
const graphOf = (records) => {
|
|
603
|
+
const rows = (Array.isArray(records) ? records : EMPTY).slice(0, 60);
|
|
604
|
+
const byId = new Map(rows.map((record) => [record.id, record]));
|
|
605
|
+
const tagCount = new Map();
|
|
606
|
+
const topicCount = new Map();
|
|
607
|
+
for (const record of rows) {
|
|
608
|
+
for (const tag of Array.isArray(record.tags) ? record.tags : EMPTY) tagCount.set(tag, (tagCount.get(tag) || 0) + 1);
|
|
609
|
+
if (record.topicKey) topicCount.set(record.topicKey, (topicCount.get(record.topicKey) || 0) + 1);
|
|
610
|
+
}
|
|
611
|
+
const tags = [...tagCount.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 8);
|
|
612
|
+
const topics = [...topicCount.entries()].filter(([, count]) => count > 1).sort((a, b) => b[1] - a[1]).slice(0, 4);
|
|
613
|
+
const nodes = [];
|
|
614
|
+
const edges = [];
|
|
615
|
+
const addEdge = (from, to, type) => {
|
|
616
|
+
if (!from || !to || from === to) return;
|
|
617
|
+
edges.push({ from, to, type });
|
|
618
|
+
};
|
|
619
|
+
for (const record of rows) {
|
|
620
|
+
nodes.push({ id: record.id, kind: 'memory', label: record.title || record.id, experimental: record.experimental === true });
|
|
621
|
+
}
|
|
622
|
+
for (const [tag, count] of tags) {
|
|
623
|
+
nodes.push({ id: `tag:${tag}`, kind: 'tag', label: `#${tag}`, count });
|
|
624
|
+
for (const record of rows) {
|
|
625
|
+
if (Array.isArray(record.tags) && record.tags.includes(tag)) addEdge(record.id, `tag:${tag}`, 'tag');
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
for (const [topic, count] of topics) {
|
|
629
|
+
nodes.push({ id: `topic:${topic}`, kind: 'topic', label: topic, count });
|
|
630
|
+
for (const record of rows) {
|
|
631
|
+
if (record.topicKey === topic) addEdge(record.id, `topic:${topic}`, 'topic');
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
for (const record of rows) {
|
|
635
|
+
for (const link of Array.isArray(record.links) ? record.links : EMPTY) {
|
|
636
|
+
if (byId.has(link.id)) addEdge(record.id, link.id, 'link');
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
return layoutGraph(nodes, edges, 'memory');
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
/** 经验图谱只连共享条件。 */
|
|
643
|
+
const ruleGraphOf = (rules) => {
|
|
644
|
+
const rows = (Array.isArray(rules) ? rules : EMPTY).slice(0, 60);
|
|
645
|
+
const conditionCount = new Map();
|
|
646
|
+
for (const rule of rows) {
|
|
647
|
+
const seen = new Set();
|
|
648
|
+
for (const condition of Array.isArray(rule.conditions) ? rule.conditions : EMPTY) {
|
|
649
|
+
if (!condition || !condition.key || !condition.value) continue;
|
|
650
|
+
const id = `condition:${condition.key}:${condition.value}`;
|
|
651
|
+
if (seen.has(id)) continue;
|
|
652
|
+
seen.add(id);
|
|
653
|
+
conditionCount.set(id, (conditionCount.get(id) || 0) + 1);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
const shared = [...conditionCount.entries()]
|
|
657
|
+
.filter(([, count]) => count > 1)
|
|
658
|
+
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
|
659
|
+
.slice(0, 10);
|
|
660
|
+
const nodes = [];
|
|
661
|
+
const edges = [];
|
|
662
|
+
for (const rule of rows) {
|
|
663
|
+
nodes.push({
|
|
664
|
+
id: rule.id, kind: 'rule', label: rule.title || rule.id,
|
|
665
|
+
state: rule.state, experimental: rule.state === 'pending' || rule.state === 'trial',
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
for (const [id, count] of shared) {
|
|
669
|
+
const [, key, ...rest] = id.split(':');
|
|
670
|
+
const value = rest.join(':');
|
|
671
|
+
nodes.push({
|
|
672
|
+
id, kind: 'condition', label: `#${value}`, count,
|
|
673
|
+
title: `${CONDITION_LABEL[key] || key} ${value}`,
|
|
674
|
+
});
|
|
675
|
+
for (const rule of rows) {
|
|
676
|
+
if ((Array.isArray(rule.conditions) ? rule.conditions : EMPTY).some((item) => item && item.key === key && item.value === value)) {
|
|
677
|
+
edges.push({ from: rule.id, to: id, type: 'condition' });
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
return layoutGraph(nodes, edges, 'rule');
|
|
682
|
+
};
|
|
683
|
+
|
|
684
|
+
/** 关系图只画真实关系;节点沿用原始 id,位置由 id 决定。 */
|
|
685
|
+
const MemoryGraph = (props) => {
|
|
686
|
+
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
|
+
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)));
|
|
692
|
+
const frame = React.useMemo(() => {
|
|
693
|
+
const list = graph.nodes;
|
|
694
|
+
if (list.length === 0) return { x: 0, y: 0, width: 640, height: 472 };
|
|
695
|
+
let minX = Infinity; let maxX = -Infinity; let minY = Infinity; let maxY = -Infinity;
|
|
696
|
+
for (const node of list) {
|
|
697
|
+
const half = node.kind === primaryKind ? 62 : 38;
|
|
698
|
+
minX = Math.min(minX, node.x - half);
|
|
699
|
+
maxX = Math.max(maxX, node.x + half);
|
|
700
|
+
minY = Math.min(minY, node.y - 16);
|
|
701
|
+
maxY = Math.max(maxY, node.y + (node.kind === primaryKind ? 28 : 24));
|
|
702
|
+
}
|
|
703
|
+
const width = Math.max(380, maxX - minX + 36);
|
|
704
|
+
const height = Math.max(250, maxY - minY + 36);
|
|
705
|
+
return { x: (minX + maxX) / 2 - width / 2, y: (minY + maxY) / 2 - height / 2, width, height };
|
|
706
|
+
}, [graph, primaryKind]);
|
|
707
|
+
if (!props.records || props.records.length === 0) {
|
|
708
|
+
return h('div', {
|
|
709
|
+
style: { flex: 1, minHeight: '220px', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--dsw-alias-label-secondary)', fontSize: '12px' },
|
|
710
|
+
}, props.emptyText || '还没有记忆');
|
|
711
|
+
}
|
|
712
|
+
const viewWidth = frame.width / zoom;
|
|
713
|
+
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
|
+
},
|
|
725
|
+
}, icon);
|
|
726
|
+
const magnifier = (sign) => h('svg', {
|
|
727
|
+
width: '15', height: '15', viewBox: '0 0 16 16', fill: 'none',
|
|
728
|
+
stroke: 'currentColor', strokeWidth: '1.4', strokeLinecap: 'round',
|
|
729
|
+
'aria-hidden': 'true',
|
|
730
|
+
},
|
|
731
|
+
h('circle', { cx: '6.8', cy: '6.8', r: '4.5' }),
|
|
732
|
+
h('line', { x1: '10.1', y1: '10.1', x2: '14', y2: '14' }),
|
|
733
|
+
h('line', { x1: '4.8', y1: '6.8', x2: '8.8', y2: '6.8' }),
|
|
734
|
+
sign > 0 ? h('line', { x1: '6.8', y1: '4.8', x2: '6.8', y2: '8.8' }) : null);
|
|
735
|
+
return h('div', { style: { position: 'relative', flex: 1, minHeight: 0, display: 'flex' } },
|
|
736
|
+
h('div', {
|
|
737
|
+
style: {
|
|
738
|
+
position: 'absolute', top: '6px', right: '6px', zIndex: 2,
|
|
739
|
+
display: 'flex', alignItems: 'center', gap: '1px', padding: '2px',
|
|
740
|
+
border: '1px solid var(--dsw-alias-border-l1)', borderRadius: '8px',
|
|
741
|
+
background: 'var(--dsw-alias-bg-layer-1)',
|
|
742
|
+
},
|
|
743
|
+
},
|
|
744
|
+
zoomButton('缩小图谱', -0.25, magnifier(-1)),
|
|
745
|
+
zoomButton('放大图谱', 0.25, magnifier(1))),
|
|
746
|
+
h('svg', {
|
|
747
|
+
viewBox: `${frame.x + frame.width / 2 - viewWidth / 2} ${frame.y + frame.height / 2 - viewHeight / 2} ${viewWidth} ${viewHeight}`,
|
|
748
|
+
preserveAspectRatio: 'xMidYMid meet',
|
|
749
|
+
style: { width: '100%', height: '100%', flex: 1, minHeight: 0, display: 'block' },
|
|
750
|
+
role: 'img', 'aria-label': props.ariaLabel || '关系图',
|
|
751
|
+
},
|
|
752
|
+
h('g', null, graph.edges.map((edge, index) => {
|
|
753
|
+
const from = graph.nodes.find((node) => node.id === edge.from);
|
|
754
|
+
const to = graph.nodes.find((node) => node.id === edge.to);
|
|
755
|
+
if (!from || !to) return null;
|
|
756
|
+
const linked = selectedId && (edge.from === selectedId || edge.to === selectedId);
|
|
757
|
+
return h('line', {
|
|
758
|
+
key: `e${index}`, x1: from.x, y1: from.y, x2: to.x, y2: to.y,
|
|
759
|
+
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',
|
|
763
|
+
});
|
|
764
|
+
})),
|
|
765
|
+
h('g', null, graph.nodes.map((node) => {
|
|
766
|
+
const isPrimary = node.kind === primaryKind;
|
|
767
|
+
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;
|
|
772
|
+
return h('g', {
|
|
773
|
+
key: node.id,
|
|
774
|
+
transform: `translate(${node.x} ${node.y})`,
|
|
775
|
+
onClick: isPrimary ? () => props.onPick(node) : undefined,
|
|
776
|
+
style: { cursor: isPrimary ? 'pointer' : 'default' },
|
|
777
|
+
},
|
|
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,
|
|
785
|
+
h('title', null, node.title || node.label),
|
|
786
|
+
);
|
|
787
|
+
}))),
|
|
788
|
+
);
|
|
789
|
+
};
|
|
790
|
+
|
|
791
|
+
const Toggle = (props) => h('button', {
|
|
792
|
+
type: 'button', role: 'switch', 'aria-checked': props.on === true, 'aria-label': props.ariaLabel || props.label,
|
|
793
|
+
onClick: props.onClick, disabled: props.busy,
|
|
794
|
+
style: {
|
|
795
|
+
width: '38px', height: '22px', flex: '0 0 auto', position: 'relative', padding: 0,
|
|
796
|
+
border: '1px solid var(--dsw-alias-border-l2)', borderRadius: '999px',
|
|
797
|
+
background: props.on ? 'var(--dsw-alias-brand-primary)' : 'var(--dsw-alias-bg-base)',
|
|
798
|
+
cursor: props.busy ? 'default' : 'pointer', opacity: props.busy ? 0.5 : 1,
|
|
799
|
+
},
|
|
800
|
+
}, h('span', {
|
|
801
|
+
style: {
|
|
802
|
+
position: 'absolute', top: '2px', left: props.on ? '18px' : '2px', width: '16px', height: '16px',
|
|
803
|
+
borderRadius: '999px', background: props.on ? 'var(--dsw-alias-bg-base)' : 'var(--dsw-alias-label-secondary)',
|
|
804
|
+
transition: 'left 120ms ease',
|
|
805
|
+
},
|
|
806
|
+
}));
|
|
807
|
+
|
|
808
|
+
const STAGE = { pending: '候选', trial: '试用', active: '生效', paused: '暂停' };
|
|
809
|
+
const STAGE_TONE = {
|
|
810
|
+
pending: 'var(--dsw-alias-state-warn-primary)',
|
|
811
|
+
trial: 'var(--dsw-alias-brand-primary)',
|
|
812
|
+
active: 'var(--dsw-alias-state-success-primary)',
|
|
813
|
+
paused: 'var(--dsw-alias-label-secondary)',
|
|
814
|
+
};
|
|
815
|
+
const StagePill = (props) => h('span', {
|
|
816
|
+
style: {
|
|
817
|
+
flex: '0 0 auto', padding: '0 6px', border: '1px solid var(--dsw-alias-border-l1)',
|
|
818
|
+
borderRadius: '999px', color: STAGE_TONE[props.state] || 'inherit',
|
|
819
|
+
fontSize: '11px', lineHeight: '18px',
|
|
820
|
+
},
|
|
821
|
+
}, STAGE[props.state] || '未定');
|
|
822
|
+
|
|
823
|
+
const Action = (props) => h('button', {
|
|
824
|
+
type: 'button', onClick: props.onClick, disabled: props.disabled,
|
|
825
|
+
style: {
|
|
826
|
+
...PANEL_LINK, opacity: props.disabled ? 0.35 : 0.82,
|
|
827
|
+
color: props.danger ? 'var(--dsw-alias-state-error-primary)' : 'inherit',
|
|
828
|
+
cursor: props.disabled ? 'default' : 'pointer',
|
|
829
|
+
},
|
|
830
|
+
}, props.children);
|
|
831
|
+
|
|
832
|
+
const Field = (props) => h('label', { style: { display: 'flex', flexDirection: 'column', gap: '4px', minWidth: 0 } },
|
|
833
|
+
h('span', { style: PANEL_LABEL }, props.label),
|
|
834
|
+
h('input', {
|
|
835
|
+
value: props.value, onChange: (event) => props.onChange(event.target.value),
|
|
836
|
+
'aria-label': props.label,
|
|
837
|
+
style: { ...PANEL_INPUT, height: '28px' },
|
|
838
|
+
}),
|
|
839
|
+
);
|
|
840
|
+
|
|
841
|
+
const countOf = (counts, key) => {
|
|
842
|
+
if (!counts || typeof counts !== 'object') return null;
|
|
843
|
+
const value = counts[key];
|
|
844
|
+
return Number.isInteger(value) && value >= 0 ? value : null;
|
|
845
|
+
};
|
|
846
|
+
|
|
847
|
+
/** 学习状态只是一段经历;能不能用由这里单独说。 */
|
|
848
|
+
const usabilityOf = (rule, evolution, memoryOn) => {
|
|
849
|
+
if (!memoryOn) return '记忆已关闭,暂不参与回答。';
|
|
850
|
+
if (!evolution || evolution.enabled !== true || evolution.effectiveEnabled === false) return '自进化已关闭,暂不参与回答。';
|
|
851
|
+
if (rule.state === 'paused') return '已暂停,不参与回答。';
|
|
852
|
+
const unresolved = rule.applicability === 'unresolved'
|
|
853
|
+
|| (rule.applicability === 'conditional' && (!Array.isArray(rule.conditions) || rule.conditions.length === 0));
|
|
854
|
+
if (unresolved) return '适用范围待确认,暂不参与回答。';
|
|
855
|
+
if (rule.state === 'pending') return '尚未参与回答,确认后才会试用。';
|
|
856
|
+
if (rule.state === 'trial') return '在条件匹配的任务中参与参考,到期后暂停。';
|
|
857
|
+
if (rule.state === 'active') return '在适用任务中作为默认参考,当前明确要求优先。';
|
|
858
|
+
return '';
|
|
859
|
+
};
|
|
860
|
+
|
|
861
|
+
const scopeLineOf = (rule) => {
|
|
862
|
+
const conditions = Array.isArray(rule.conditions) ? rule.conditions : EMPTY;
|
|
863
|
+
if (rule.applicability === 'unresolved') return '待确认';
|
|
864
|
+
if (rule.applicability === 'conditional' || conditions.length > 0) {
|
|
865
|
+
if (conditions.length === 0) return '待确认';
|
|
866
|
+
return conditions.map((item) => `${CONDITION_LABEL[item.key] || item.key} ${item.value}`).join('、');
|
|
867
|
+
}
|
|
868
|
+
return '所有项目';
|
|
869
|
+
};
|
|
870
|
+
|
|
871
|
+
const evidenceTextOf = (row) => {
|
|
872
|
+
if (typeof row === 'string') return row;
|
|
873
|
+
if (!row || typeof row !== 'object') return '';
|
|
874
|
+
return typeof row.text === 'string' ? row.text : (typeof row.note === 'string' ? row.note : '');
|
|
875
|
+
};
|
|
876
|
+
|
|
877
|
+
const MemoryRow = (props) => {
|
|
878
|
+
const record = props.record;
|
|
879
|
+
return h('button', {
|
|
880
|
+
type: 'button', onClick: props.onClick,
|
|
881
|
+
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,
|
|
886
|
+
},
|
|
887
|
+
},
|
|
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 } },
|
|
890
|
+
h('span', null, KIND_LABEL[record.kind] || record.kind),
|
|
891
|
+
record.experimental ? h('span', { style: { color: 'var(--dsw-alias-state-warn-primary)' } }, '实验') : null,
|
|
892
|
+
h('span', { style: { marginLeft: 'auto', flex: '0 0 auto', fontVariantNumeric: 'tabular-nums' } }, EVO_DATE(record.updated)),
|
|
893
|
+
),
|
|
894
|
+
);
|
|
895
|
+
};
|
|
896
|
+
|
|
897
|
+
const EvolutionDetail = (props) => {
|
|
898
|
+
const rule = props.rule;
|
|
899
|
+
const ruleId = rule ? rule.id : '';
|
|
900
|
+
const [confirming, setConfirming] = React.useState(false);
|
|
901
|
+
const [editing, setEditing] = React.useState(false);
|
|
902
|
+
const [open, setOpen] = React.useState('');
|
|
903
|
+
const [title, setTitle] = React.useState('');
|
|
904
|
+
const [body, setBody] = React.useState('');
|
|
905
|
+
React.useEffect(() => {
|
|
906
|
+
setConfirming(false);
|
|
907
|
+
setEditing(false);
|
|
908
|
+
setOpen('');
|
|
909
|
+
setTitle(rule && typeof rule.title === 'string' ? rule.title : '');
|
|
910
|
+
setBody(rule && typeof rule.body === 'string' ? rule.body : '');
|
|
911
|
+
}, [ruleId]);
|
|
912
|
+
if (!rule) return null;
|
|
913
|
+
const checkpoints = Array.isArray(rule.checkpoints) ? rule.checkpoints : EMPTY;
|
|
914
|
+
const samples = Array.isArray(rule.samples) ? rule.samples : EMPTY;
|
|
915
|
+
const evidence = Array.isArray(rule.learning?.evidence) ? rule.learning.evidence : EMPTY;
|
|
916
|
+
const versions = Array.isArray(rule.learning?.versions) ? rule.learning.versions : EMPTY;
|
|
917
|
+
const history = [
|
|
918
|
+
...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 : '修改' })),
|
|
920
|
+
].filter((row) => row.text).sort((a, b) => String(b.at).localeCompare(String(a.at))).slice(0, 6);
|
|
921
|
+
const canManage = props.canManage && !props.busy;
|
|
922
|
+
const act = (name, extra) => { if (canManage) props.onManage(name, extra || {}); };
|
|
923
|
+
const actions = props.canManage
|
|
924
|
+
? h('div', { style: { display: 'flex', alignItems: 'center', gap: '12px', flexWrap: 'wrap', paddingTop: '3px' } },
|
|
925
|
+
rule.state === 'pending' || rule.state === 'trial'
|
|
926
|
+
? h(Action, { disabled: !canManage, onClick: () => act('adopt') }, '采纳') : null,
|
|
927
|
+
rule.state === 'paused'
|
|
928
|
+
? h(Action, { disabled: !canManage, onClick: () => act('resume') }, '恢复')
|
|
929
|
+
: (rule.state === 'trial' || rule.state === 'active'
|
|
930
|
+
? h(Action, { disabled: !canManage, onClick: () => act('pause') }, '暂停')
|
|
931
|
+
: null),
|
|
932
|
+
rule.state !== 'paused'
|
|
933
|
+
? h(Action, { disabled: !canManage, onClick: () => setEditing((value) => !value) }, '修改') : null,
|
|
934
|
+
rule.state === 'paused'
|
|
935
|
+
? h(Action, { disabled: !canManage, onClick: () => setOpen((value) => value === 'history' ? '' : 'history') }, '查看记录') : null,
|
|
936
|
+
rule.state === 'pending'
|
|
937
|
+
? (confirming
|
|
938
|
+
? h(React.Fragment, null,
|
|
939
|
+
h(Action, { disabled: !canManage, danger: true, onClick: () => act('discard') }, '确认放弃'),
|
|
940
|
+
h(Action, { disabled: !canManage, onClick: () => setConfirming(false) }, '取消'))
|
|
941
|
+
: h(Action, { disabled: !canManage, onClick: () => setConfirming(true) }, '放弃'))
|
|
942
|
+
: null,
|
|
943
|
+
)
|
|
944
|
+
: h('div', { style: { ...PANEL_MUTED, paddingTop: '2px' } }, '只读');
|
|
945
|
+
|
|
946
|
+
const editor = editing
|
|
947
|
+
? h('div', { style: { display: 'flex', flexDirection: 'column', gap: '8px', paddingTop: '2px' } },
|
|
948
|
+
h(Field, { label: '标题', value: title, onChange: setTitle }),
|
|
949
|
+
h('label', { style: { display: 'flex', flexDirection: 'column', gap: '4px' } },
|
|
950
|
+
h('span', { style: PANEL_LABEL }, '经验'),
|
|
951
|
+
h('textarea', {
|
|
952
|
+
value: body, onChange: (event) => setBody(event.target.value), 'aria-label': '经验',
|
|
953
|
+
rows: 6,
|
|
954
|
+
style: { ...PANEL_INPUT, height: 'auto', minHeight: '110px', padding: '7px 9px', resize: 'vertical', lineHeight: '1.6' },
|
|
955
|
+
}),
|
|
956
|
+
),
|
|
957
|
+
h('div', { style: { display: 'flex', gap: '12px', flexWrap: 'wrap' } },
|
|
958
|
+
h(Action, { disabled: !canManage || !title.trim() || !body.trim(), onClick: () => { act('edit', { title: title.trim(), body: body.trim() }); setEditing(false); } }, '保存'),
|
|
959
|
+
rule.state === 'pending' || rule.state === 'trial'
|
|
960
|
+
? h(Action, { disabled: !canManage || !title.trim() || !body.trim(), onClick: () => { act('adopt', { title: title.trim(), body: body.trim() }); setEditing(false); } }, '保存并采纳')
|
|
961
|
+
: null,
|
|
962
|
+
h(Action, { disabled: !canManage, onClick: () => setEditing(false) }, '取消'),
|
|
963
|
+
),
|
|
964
|
+
)
|
|
965
|
+
: null;
|
|
966
|
+
|
|
967
|
+
const evidencePane = open === 'evidence'
|
|
968
|
+
? h('div', { style: { ...PANEL_MUTED, display: 'flex', flexDirection: 'column', gap: '4px', paddingTop: '2px' } },
|
|
969
|
+
evidence.length === 0 && samples.length === 0
|
|
970
|
+
? '暂无学习依据'
|
|
971
|
+
: [
|
|
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) || '样例')),
|
|
974
|
+
].filter(Boolean))
|
|
975
|
+
: null;
|
|
976
|
+
const historyPane = open === 'history'
|
|
977
|
+
? h('div', { style: { ...PANEL_MUTED, display: 'flex', flexDirection: 'column', gap: '4px', paddingTop: '2px' } },
|
|
978
|
+
history.length === 0
|
|
979
|
+
? '暂无变化记录'
|
|
980
|
+
: history.map((row, index) => h('div', { key: `h${index}`, style: { display: 'flex', gap: '8px' } },
|
|
981
|
+
h('span', { style: { flex: '0 0 auto', fontVariantNumeric: 'tabular-nums' } }, EVO_DATE(row.at) || '—'),
|
|
982
|
+
h('span', { style: { minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, row.text))))
|
|
983
|
+
: null;
|
|
984
|
+
|
|
985
|
+
return h('div', { style: { display: 'flex', flexDirection: 'column', gap: '10px' } },
|
|
986
|
+
h('div', { style: { display: 'flex', flexDirection: 'column', gap: '4px' } },
|
|
987
|
+
h('div', { style: PANEL_LABEL }, '当前经验'),
|
|
988
|
+
h('div', { style: { fontSize: '13px', lineHeight: '1.45', fontWeight: 600, wordBreak: 'break-word' } }, rule.title),
|
|
989
|
+
h('div', { style: { fontSize: '12px', lineHeight: '1.65', whiteSpace: 'pre-wrap', wordBreak: 'break-word', opacity: 0.88 } }, rule.body || '没有正文'),
|
|
990
|
+
),
|
|
991
|
+
h('div', { style: { display: 'flex', flexDirection: 'column', gap: '3px' } },
|
|
992
|
+
h('div', { style: PANEL_LABEL }, '适用范围'),
|
|
993
|
+
h('div', { style: { fontSize: '12px', lineHeight: '1.55', wordBreak: 'break-word' } }, scopeLineOf(rule)),
|
|
994
|
+
),
|
|
995
|
+
h('div', { style: { display: 'flex', flexDirection: 'column', gap: '4px' } },
|
|
996
|
+
h('div', { style: { display: 'flex', alignItems: 'center', gap: '6px' } },
|
|
997
|
+
h('span', { style: PANEL_LABEL }, '当前状态'),
|
|
998
|
+
h(StagePill, { state: rule.state }),
|
|
999
|
+
),
|
|
1000
|
+
h('div', { style: { fontSize: '12px', lineHeight: '1.55', opacity: 0.82 } }, usabilityOf(rule, props.evolution, props.memoryOn)),
|
|
1001
|
+
actions,
|
|
1002
|
+
),
|
|
1003
|
+
editor,
|
|
1004
|
+
h('div', { style: { display: 'flex', alignItems: 'center', gap: '12px', borderTop: '1px solid var(--dsw-alias-border-l1)', paddingTop: '6px' } },
|
|
1005
|
+
h(Action, { onClick: () => setOpen((value) => value === 'evidence' ? '' : 'evidence') }, `学习依据 ${evidence.length + samples.length}`),
|
|
1006
|
+
h(Action, { onClick: () => setOpen((value) => value === 'history' ? '' : 'history') }, `变化记录 ${history.length}`),
|
|
1007
|
+
),
|
|
1008
|
+
evidencePane,
|
|
1009
|
+
historyPane,
|
|
1010
|
+
);
|
|
1011
|
+
};
|
|
1012
|
+
|
|
1013
|
+
const MemoryPanel = (props) => {
|
|
1014
|
+
const sessionId = props.sessionId;
|
|
1015
|
+
const [data, setData] = React.useState(null);
|
|
1016
|
+
const [loading, setLoading] = React.useState(true);
|
|
1017
|
+
const [failed, setFailed] = React.useState(false);
|
|
448
1018
|
const [note, setNote] = React.useState('');
|
|
449
|
-
const [
|
|
1019
|
+
const [query, setQuery] = React.useState('');
|
|
1020
|
+
const [tab, setTab] = React.useState('memory');
|
|
1021
|
+
const [memoryView, setMemoryView] = React.useState('list');
|
|
1022
|
+
const [evolutionView, setEvolutionView] = React.useState('list');
|
|
1023
|
+
const [stage, setStage] = React.useState('all');
|
|
1024
|
+
const [selectedMemory, setSelectedMemory] = React.useState('');
|
|
1025
|
+
const [selectedRule, setSelectedRule] = React.useState('');
|
|
1026
|
+
const [detail, setDetail] = React.useState(null);
|
|
1027
|
+
const [detailNote, setDetailNote] = React.useState('');
|
|
1028
|
+
const [busy, setBusy] = React.useState(false);
|
|
1029
|
+
const [showReason, setShowReason] = React.useState(false);
|
|
450
1030
|
const alive = React.useRef(true);
|
|
451
|
-
|
|
452
|
-
const
|
|
453
|
-
|
|
454
|
-
|
|
1031
|
+
const generation = React.useRef(0);
|
|
1032
|
+
const scopeRef = React.useRef('');
|
|
1033
|
+
React.useEffect(() => {
|
|
1034
|
+
alive.current = true;
|
|
1035
|
+
return () => { alive.current = false; };
|
|
1036
|
+
}, []);
|
|
1037
|
+
|
|
1038
|
+
const load = (scopeKey = '', focusRule = '') => {
|
|
1039
|
+
const request = generation.current + 1;
|
|
1040
|
+
generation.current = request;
|
|
1041
|
+
setLoading(true);
|
|
1042
|
+
setFailed(false);
|
|
455
1043
|
setNote('');
|
|
456
|
-
post('/lite-memory/
|
|
457
|
-
if (!alive.current) return;
|
|
458
|
-
|
|
459
|
-
if (result.ok && result.body
|
|
460
|
-
|
|
1044
|
+
post('/lite-memory/overview', { sessionId, scopeKey }).then((result) => {
|
|
1045
|
+
if (!alive.current || generation.current !== request) return;
|
|
1046
|
+
setLoading(false);
|
|
1047
|
+
if (result.ok && result.body && result.body.ok) {
|
|
1048
|
+
const next = result.body;
|
|
1049
|
+
const nextKey = next.selected && next.selected.key ? next.selected.key : '';
|
|
1050
|
+
if (scopeRef.current !== nextKey) {
|
|
1051
|
+
scopeRef.current = nextKey;
|
|
1052
|
+
setQuery('');
|
|
1053
|
+
}
|
|
1054
|
+
setData(next);
|
|
1055
|
+
setSelectedMemory('');
|
|
1056
|
+
setSelectedRule(focusRule);
|
|
1057
|
+
setDetail(null);
|
|
1058
|
+
setDetailNote('');
|
|
1059
|
+
setShowReason(false);
|
|
1060
|
+
} else {
|
|
1061
|
+
setFailed(true);
|
|
1062
|
+
setNote(failure(result, '读不到记忆'));
|
|
1063
|
+
}
|
|
461
1064
|
});
|
|
462
1065
|
};
|
|
1066
|
+
|
|
463
1067
|
React.useEffect(() => { load(); }, [sessionId]);
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
1068
|
+
|
|
1069
|
+
React.useEffect(() => {
|
|
1070
|
+
if (typeof window === 'undefined') return undefined;
|
|
1071
|
+
const onKey = (event) => { if (event.key === 'Escape') { event.stopPropagation(); props.onClose(); } };
|
|
1072
|
+
window.addEventListener('keydown', onKey);
|
|
1073
|
+
const previous = typeof document !== 'undefined' ? document.body.style.overflow : '';
|
|
1074
|
+
if (typeof document !== 'undefined') document.body.style.overflow = 'hidden';
|
|
1075
|
+
return () => {
|
|
1076
|
+
window.removeEventListener('keydown', onKey);
|
|
1077
|
+
if (typeof document !== 'undefined') document.body.style.overflow = previous;
|
|
1078
|
+
};
|
|
1079
|
+
}, []);
|
|
1080
|
+
|
|
1081
|
+
React.useEffect(() => {
|
|
1082
|
+
if (!selectedMemory || !data) { setDetail(null); setDetailNote(''); return undefined; }
|
|
1083
|
+
let live = true;
|
|
1084
|
+
setDetail(null);
|
|
1085
|
+
setDetailNote('');
|
|
1086
|
+
post('/lite-memory/body', { sessionId, id: selectedMemory, scopeKey: data.selected.key }).then((result) => {
|
|
1087
|
+
if (!live) return;
|
|
1088
|
+
if (result.ok && result.body && result.body.ok) {
|
|
1089
|
+
if (result.body.status === 'gone') setDetailNote('这条记忆已删除');
|
|
1090
|
+
else setDetail(result.body);
|
|
1091
|
+
} else setDetailNote(failure(result, '读不到正文'));
|
|
1092
|
+
});
|
|
1093
|
+
return () => { live = false; };
|
|
1094
|
+
}, [selectedMemory, data && data.selected.key, sessionId]);
|
|
1095
|
+
|
|
1096
|
+
const current = data ? data.current : null;
|
|
1097
|
+
const selected = data ? data.selected : null;
|
|
1098
|
+
const records = selected && Array.isArray(selected.records) ? selected.records : EMPTY;
|
|
1099
|
+
const total = selected && selected.stats && Number.isInteger(selected.stats.total) ? selected.stats.total : records.length;
|
|
1100
|
+
const currentKey = current ? current.key : '';
|
|
1101
|
+
const selectedKey = selected ? selected.key : '';
|
|
1102
|
+
const control = selected && selected.control
|
|
1103
|
+
? selected.control
|
|
1104
|
+
: (current && selected && selected.key === current.key && current.control ? current.control : { enabled: true });
|
|
1105
|
+
const memoryOn = control.enabled !== false;
|
|
1106
|
+
const evolution = selected && selected.evolution
|
|
1107
|
+
? selected.evolution
|
|
1108
|
+
: (current && selected && selected.key === current.key ? current.evolution : null);
|
|
1109
|
+
const rules = evolution && Array.isArray(evolution.rules) ? evolution.rules : EMPTY;
|
|
1110
|
+
const counts = evolution && evolution.counts && typeof evolution.counts === 'object' ? evolution.counts : null;
|
|
1111
|
+
const effectiveEnabled = evolution
|
|
1112
|
+
? (typeof evolution.effectiveEnabled === 'boolean' ? evolution.effectiveEnabled : (memoryOn && evolution.enabled === true))
|
|
1113
|
+
: false;
|
|
1114
|
+
const canToggle = Boolean(selectedKey) && selectedKey === currentKey;
|
|
1115
|
+
const canManage = canToggle && memoryOn;
|
|
1116
|
+
const globalView = Boolean(selected && selected.kind === 'global');
|
|
1117
|
+
const activeTab = globalView ? 'memory' : tab;
|
|
1118
|
+
React.useEffect(() => { if (globalView) setTab('memory'); }, [globalView]);
|
|
1119
|
+
|
|
1120
|
+
const activity = evolution && evolution.activity ? evolution.activity : null;
|
|
1121
|
+
const latestReceipt = evolution && evolution.economy && Array.isArray(evolution.economy.recent) ? evolution.economy.recent[0] : null;
|
|
1122
|
+
const countsUnknown = Boolean(evolution) && (!counts || ['pending', 'trial', 'active', 'paused'].some((key) => countOf(counts, key) === null));
|
|
1123
|
+
const activityCode = activity && typeof activity === 'object'
|
|
1124
|
+
? String(activity.code || activity.state || '')
|
|
1125
|
+
: (typeof activity === 'string' ? activity : '');
|
|
1126
|
+
const changedCount = activity && typeof activity === 'object' && Number.isInteger(activity.changed)
|
|
1127
|
+
? activity.changed
|
|
1128
|
+
: (Number.isInteger(activity && activity.count) ? activity.count : null);
|
|
1129
|
+
let activityText = '';
|
|
1130
|
+
if (!evolution && memoryOn) {
|
|
1131
|
+
activityText = '读不到自进化状态';
|
|
1132
|
+
} else if (!memoryOn || !evolution || evolution.enabled !== true || (typeof evolution.effectiveEnabled === 'boolean' && !evolution.effectiveEnabled)) {
|
|
1133
|
+
activityText = '自进化已关闭,已有成果保留';
|
|
1134
|
+
} else if (activityCode === 'running' || activityCode === 'organizing') {
|
|
1135
|
+
activityText = '正在整理本次反馈';
|
|
1136
|
+
} else if (activityCode === 'updated' || activityCode === 'applied') {
|
|
1137
|
+
activityText = changedCount === null || changedCount < 1 ? '最近整理完成' : `最近整理完成,更新了 ${changedCount} 条经验`;
|
|
1138
|
+
} else if (activityCode === 'no_change' || activityCode === 'none') {
|
|
1139
|
+
activityText = '本次反馈未形成新的经验';
|
|
1140
|
+
} else if (activityCode === 'scope_unconfirmed' || activityCode === 'unknown_scope') {
|
|
1141
|
+
activityText = '暂未整理:适用范围待确认';
|
|
1142
|
+
} else if (activityCode === 'quota' || activityCode === 'exhausted') {
|
|
1143
|
+
activityText = '本轮整理额度已用完';
|
|
1144
|
+
} else if (activityCode === 'failed' || activityCode === 'error') {
|
|
1145
|
+
activityText = '最近整理失败,可查看原因';
|
|
1146
|
+
} else if (activityCode === 'waiting' || activityCode === 'idle' || activityCode === 'empty') {
|
|
1147
|
+
activityText = '等待可学习的反馈';
|
|
1148
|
+
}
|
|
1149
|
+
if (!activityText) {
|
|
1150
|
+
if (latestReceipt && latestReceipt.status === 'applied') {
|
|
1151
|
+
activityText = Number.isInteger(latestReceipt.changed) && latestReceipt.changed > 0 ? `最近整理完成,更新了 ${latestReceipt.changed} 条经验` : '最近整理完成';
|
|
1152
|
+
}
|
|
1153
|
+
else if (latestReceipt && latestReceipt.status === 'no_change') activityText = '本次反馈未形成新的经验';
|
|
1154
|
+
else if (latestReceipt && latestReceipt.status === 'failed') activityText = '最近整理失败,可查看原因';
|
|
1155
|
+
else if (rules.length === 0) activityText = countsUnknown ? '等待可学习的反馈' : '还没有整理出经验';
|
|
1156
|
+
else activityText = '暂无整理记录';
|
|
1157
|
+
}
|
|
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]);
|
|
1170
|
+
const filteredRules = React.useMemo(() => {
|
|
1171
|
+
if (stage === 'all') return rules;
|
|
1172
|
+
return rules.filter((rule) => rule.state === stage);
|
|
1173
|
+
}, [rules, stage]);
|
|
1174
|
+
const selectedRuleRow = selectedRule ? rules.find((rule) => rule.id === selectedRule) : null;
|
|
1175
|
+
const detailRecord = selectedMemory ? records.find((record) => record.id === selectedMemory) : null;
|
|
1176
|
+
|
|
1177
|
+
const onPickNode = (node) => {
|
|
1178
|
+
if (node.kind !== 'memory') return;
|
|
1179
|
+
setSelectedMemory(node.id);
|
|
1180
|
+
setSelectedRule('');
|
|
1181
|
+
};
|
|
1182
|
+
const onPickRuleNode = (node) => {
|
|
1183
|
+
if (node.kind !== 'rule') return;
|
|
1184
|
+
setSelectedRule(node.id);
|
|
1185
|
+
setSelectedMemory('');
|
|
1186
|
+
setShowReason(false);
|
|
1187
|
+
};
|
|
1188
|
+
|
|
472
1189
|
const manage = (learningAction, extra) => {
|
|
473
|
-
if (busy || !
|
|
1190
|
+
if (busy || !selectedRuleRow || !canManage) return;
|
|
474
1191
|
setBusy(true);
|
|
475
|
-
|
|
476
|
-
post('/lite-memory/evolution', {
|
|
1192
|
+
const payload = {
|
|
477
1193
|
sessionId,
|
|
478
1194
|
learningAction,
|
|
479
|
-
id:
|
|
480
|
-
expectedRevision:
|
|
1195
|
+
id: selectedRuleRow.id,
|
|
1196
|
+
expectedRevision: selectedRuleRow.revision,
|
|
481
1197
|
operationId: newOperationId(),
|
|
482
1198
|
...extra,
|
|
483
|
-
}
|
|
1199
|
+
};
|
|
1200
|
+
if (learningAction === 'pause') payload.ids = [selectedRuleRow.id];
|
|
1201
|
+
post('/lite-memory/evolution', payload).then((result) => {
|
|
484
1202
|
if (!alive.current) return;
|
|
485
1203
|
setBusy(false);
|
|
486
|
-
if (result.
|
|
487
|
-
|
|
488
|
-
load();
|
|
489
|
-
} else setNote(failure(result, '这次没有改动'));
|
|
1204
|
+
if (succeeded(result)) load(selectedKey, selectedRuleRow.id);
|
|
1205
|
+
else setNote(failure(result, '这次没有改动'));
|
|
490
1206
|
});
|
|
491
1207
|
};
|
|
492
|
-
|
|
493
|
-
|
|
1208
|
+
|
|
1209
|
+
const toggleControl = () => {
|
|
1210
|
+
if (busy || !canToggle) return;
|
|
494
1211
|
setBusy(true);
|
|
495
|
-
|
|
496
|
-
post('/lite-memory/evolution', { sessionId, enabled: !enabled, operationId: newOperationId() }).then((result) => {
|
|
1212
|
+
post('/lite-memory/control', { sessionId, enabled: !memoryOn, operationId: newOperationId() }).then((result) => {
|
|
497
1213
|
if (!alive.current) return;
|
|
498
1214
|
setBusy(false);
|
|
499
|
-
if (
|
|
1215
|
+
if (succeeded(result)) load(selectedKey);
|
|
500
1216
|
else setNote(failure(result, '开关没有改动'));
|
|
501
1217
|
});
|
|
502
1218
|
};
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
1219
|
+
|
|
1220
|
+
const toggleEvolution = () => {
|
|
1221
|
+
if (busy || !evolution || !canToggle || !memoryOn) return;
|
|
1222
|
+
setBusy(true);
|
|
1223
|
+
post('/lite-memory/evolution', { sessionId, enabled: evolution.enabled !== true, operationId: newOperationId() }).then((result) => {
|
|
1224
|
+
if (!alive.current) return;
|
|
1225
|
+
setBusy(false);
|
|
1226
|
+
if (succeeded(result)) load(selectedKey);
|
|
1227
|
+
else setNote(failure(result, '开关没有改动'));
|
|
1228
|
+
});
|
|
1229
|
+
};
|
|
1230
|
+
|
|
1231
|
+
const closeButton = h('button', {
|
|
1232
|
+
type: 'button', onClick: props.onClose, 'aria-label': '关闭',
|
|
1233
|
+
style: { width: '30px', height: '30px', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', border: 'none', borderRadius: '8px', background: 'transparent', color: 'inherit', opacity: 0.65, cursor: 'pointer' },
|
|
1234
|
+
}, h(CloseIcon));
|
|
1235
|
+
|
|
1236
|
+
const header = h('div', {
|
|
1237
|
+
style: { display: 'flex', alignItems: 'center', gap: '10px', padding: '14px 18px', borderBottom: '1px solid var(--dsw-alias-border-l1)', flex: '0 0 auto' },
|
|
1238
|
+
},
|
|
1239
|
+
h('span', { style: { width: '32px', height: '32px', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', borderRadius: '9px', background: 'var(--dsw-alias-bg-layer-2)', border: '1px solid var(--dsw-alias-border-l1)', color: 'var(--dsw-alias-brand-primary)' } }, h(BrainIcon)),
|
|
1240
|
+
h('span', { style: { display: 'flex', flexDirection: 'column', minWidth: 0 } },
|
|
1241
|
+
h('span', { style: { fontSize: '14px', lineHeight: '1.35', fontWeight: 600 } }, '记忆'),
|
|
1242
|
+
h('span', { style: { ...PANEL_MUTED, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, selected ? `${selected.label} · ${total} 条` : '读取中…'),
|
|
1243
|
+
),
|
|
1244
|
+
h('span', { style: { marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: '8px' } },
|
|
1245
|
+
activeTab === 'memory' ? h('select', {
|
|
1246
|
+
value: selectedKey,
|
|
1247
|
+
onChange: (event) => load(event.target.value),
|
|
1248
|
+
'aria-label': '记忆范围',
|
|
1249
|
+
disabled: loading || !data,
|
|
1250
|
+
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
|
+
},
|
|
1252
|
+
data && Array.isArray(data.scopes) ? data.scopes.map((scope) => h('option', { key: scope.key, value: scope.key }, scope.kind === 'global' ? '全局' : scope.label)) : null) : null,
|
|
1253
|
+
closeButton,
|
|
1254
|
+
),
|
|
1255
|
+
);
|
|
1256
|
+
|
|
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 }),
|
|
527
1261
|
),
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
'
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
1262
|
+
h('div', { style: PANEL_MUTED }, '读取项目和全局记忆'), // ui-copy-ignore 用户指定文案
|
|
1263
|
+
);
|
|
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 }),
|
|
1268
|
+
),
|
|
1269
|
+
h('div', { style: PANEL_MUTED }, '从反馈中整理经验,按需使用'), // ui-copy-ignore 用户指定文案
|
|
1270
|
+
h('div', { style: PANEL_MUTED }, '实验功能'), // 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,
|
|
1278
|
+
);
|
|
1279
|
+
|
|
1280
|
+
const switchTab = (key) => {
|
|
1281
|
+
if (key === 'evolution' && selectedKey !== currentKey) load(currentKey);
|
|
1282
|
+
setTab(key);
|
|
1283
|
+
};
|
|
1284
|
+
const tabButton = (key, text) => h('button', {
|
|
1285
|
+
type: 'button', role: 'tab', 'aria-selected': activeTab === key,
|
|
1286
|
+
onClick: () => switchTab(key),
|
|
1287
|
+
style: {
|
|
1288
|
+
...PANEL_LINK, flex: '0 0 auto', height: '34px', padding: '0 18px', borderRadius: 0,
|
|
1289
|
+
background: 'transparent',
|
|
1290
|
+
opacity: activeTab === key ? 0.95 : 0.55,
|
|
1291
|
+
border: 'none',
|
|
1292
|
+
borderBottom: activeTab === key ? '2px solid var(--dsw-alias-brand-primary)' : '2px solid transparent',
|
|
1293
|
+
},
|
|
1294
|
+
}, text);
|
|
1295
|
+
|
|
1296
|
+
const tabBar = h('div', { role: 'tablist', 'aria-label': '记忆内容', style: { display: 'flex', alignItems: 'center', gap: '2px', flex: '0 0 auto', borderBottom: '1px solid var(--dsw-alias-border-l1)' } },
|
|
1297
|
+
tabButton('memory', '记忆'),
|
|
1298
|
+
globalView ? null : tabButton('evolution', '自进化'),
|
|
1299
|
+
);
|
|
1300
|
+
|
|
1301
|
+
const memoryList = loading
|
|
1302
|
+
? h('div', { style: { ...PANEL_MUTED, padding: '12px 4px' } }, '读取中…')
|
|
1303
|
+
: filtered.length === 0
|
|
1304
|
+
? h('div', { style: { ...PANEL_MUTED, padding: '12px 4px' } }, query ? '没有匹配的记忆' : '还没有记忆')
|
|
1305
|
+
: filtered.map((record) => h(MemoryRow, {
|
|
1306
|
+
key: record.id, record, active: record.id === selectedMemory,
|
|
1307
|
+
onClick: () => { setSelectedMemory(record.id); setSelectedRule(''); },
|
|
1308
|
+
}));
|
|
1309
|
+
|
|
1310
|
+
const memoryDetail = selectedMemory
|
|
1311
|
+
? h('div', { style: { ...PANEL_CARD, minHeight: 0, overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: '6px' } },
|
|
1312
|
+
detail
|
|
1313
|
+
? h('div', { style: { display: 'flex', flexDirection: 'column', gap: '6px' } },
|
|
1314
|
+
h('div', { style: { display: 'flex', alignItems: 'flex-start', gap: '8px' } },
|
|
1315
|
+
h('div', { style: { flex: 1, minWidth: 0, fontSize: '13px', lineHeight: '1.45', fontWeight: 600, wordBreak: 'break-word' } }, detail.title),
|
|
1316
|
+
h(Action, { onClick: () => setSelectedMemory('') }, '关闭'),
|
|
1317
|
+
),
|
|
1318
|
+
h('div', { style: PANEL_MUTED }, `${KIND_LABEL[detailRecord && detailRecord.kind] || '记忆'} · ${detail.scopeLabel || ''}`),
|
|
1319
|
+
Array.isArray(detailRecord && detailRecord.tags) && detailRecord.tags.length
|
|
1320
|
+
? h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: '4px' } },
|
|
1321
|
+
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 || '没有正文'),
|
|
1323
|
+
)
|
|
1324
|
+
: h('div', { style: PANEL_MUTED }, detailNote || '读取中…'),
|
|
1325
|
+
)
|
|
1326
|
+
: null;
|
|
1327
|
+
|
|
1328
|
+
const viewToggleOf = (current, setValue, ariaLabel) => {
|
|
1329
|
+
const button = (key, label) => h('button', {
|
|
1330
|
+
key, type: 'button', onClick: () => setValue(key),
|
|
1331
|
+
'aria-pressed': current === key,
|
|
1332
|
+
style: {
|
|
1333
|
+
...PANEL_LINK, height: '24px', padding: '0 7px', borderRadius: '6px',
|
|
1334
|
+
background: current === key ? 'var(--dsw-alias-bg-layer-2)' : 'transparent',
|
|
1335
|
+
opacity: current === key ? 0.9 : 0.5,
|
|
1336
|
+
},
|
|
1337
|
+
}, label);
|
|
1338
|
+
return h('div', {
|
|
1339
|
+
role: 'group', 'aria-label': ariaLabel,
|
|
1340
|
+
style: {
|
|
1341
|
+
display: 'flex', alignItems: 'center', gap: '2px', flex: '0 0 auto',
|
|
1342
|
+
height: '30px', padding: '2px', boxSizing: 'border-box',
|
|
1343
|
+
border: '1px solid var(--dsw-alias-border-l1)', borderRadius: '8px',
|
|
1344
|
+
background: 'var(--dsw-alias-bg-base)',
|
|
1345
|
+
},
|
|
1346
|
+
}, button('list', '列表'), button('graph', '图谱'));
|
|
1347
|
+
};
|
|
1348
|
+
const memoryToggle = viewToggleOf(memoryView, setMemoryView, '记忆视图');
|
|
1349
|
+
|
|
1350
|
+
const memoryListPane = h('div', {
|
|
1351
|
+
style: {
|
|
1352
|
+
...PANEL_CARD, padding: '2px 4px', minHeight: 0, overflowY: 'auto',
|
|
1353
|
+
},
|
|
1354
|
+
}, memoryList);
|
|
1355
|
+
const memoryGraphPane = h('div', {
|
|
1356
|
+
style: { ...PANEL_CARD, padding: 0, minHeight: 0, overflow: 'hidden', display: 'flex' },
|
|
1357
|
+
},
|
|
1358
|
+
h(MemoryGraph, { records: filtered, selectedId: selectedMemory, onPick: onPickNode, emptyText: query ? '没有匹配的记忆' : '还没有记忆', ariaLabel: '记忆关系图' }));
|
|
1359
|
+
const memoryTab = h('div', { role: 'tabpanel', 'aria-label': '记忆', style: { flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', gap: '9px' } },
|
|
1360
|
+
h('div', { style: { display: 'flex', alignItems: 'center', gap: '8px', flex: '0 0 auto' } },
|
|
1361
|
+
h('input', {
|
|
1362
|
+
value: query, onChange: (event) => setQuery(event.target.value),
|
|
1363
|
+
placeholder: '搜索标题或标签', 'aria-label': '搜索记忆',
|
|
1364
|
+
style: { ...PANEL_INPUT, width: 'auto', flex: '1 1 0', minWidth: 0 },
|
|
1365
|
+
}),
|
|
1366
|
+
memoryToggle,
|
|
1367
|
+
),
|
|
1368
|
+
h('div', {
|
|
1369
|
+
style: {
|
|
1370
|
+
flex: 1, minHeight: 0, display: 'grid', gap: '10px',
|
|
1371
|
+
gridTemplateColumns: selectedMemory ? 'minmax(0, 1fr) minmax(280px, 340px)' : 'minmax(0, 1fr)',
|
|
1372
|
+
},
|
|
1373
|
+
},
|
|
1374
|
+
memoryView === 'graph' ? memoryGraphPane : memoryListPane,
|
|
1375
|
+
memoryDetail),
|
|
1376
|
+
);
|
|
1377
|
+
|
|
1378
|
+
const stageFilters = [
|
|
1379
|
+
['all', '全部'],
|
|
1380
|
+
['pending', '候选'],
|
|
1381
|
+
['trial', '试用'],
|
|
1382
|
+
['active', '生效'],
|
|
1383
|
+
['paused', '暂停'],
|
|
1384
|
+
];
|
|
1385
|
+
const countText = (key) => {
|
|
1386
|
+
if (!counts || countsUnknown) return '—';
|
|
1387
|
+
if (key === 'all') return String(['pending', 'trial', 'active', 'paused'].reduce((sum, name) => sum + (countOf(counts, name) || 0), 0));
|
|
1388
|
+
const value = countOf(counts, key);
|
|
1389
|
+
return value === null ? '—' : String(value);
|
|
1390
|
+
};
|
|
1391
|
+
const filterBar = h('div', { style: { display: 'flex', alignItems: 'center', gap: '12px', flexWrap: 'wrap', flex: '0 0 auto' } },
|
|
1392
|
+
stageFilters.map(([key, label]) => h('button', {
|
|
1393
|
+
key, type: 'button', onClick: () => setStage(key),
|
|
1394
|
+
style: {
|
|
1395
|
+
...PANEL_LINK, opacity: stage === key ? 0.92 : 0.5,
|
|
1396
|
+
borderBottom: stage === key ? '1px solid currentColor' : '1px solid transparent',
|
|
1397
|
+
fontVariantNumeric: 'tabular-nums',
|
|
1398
|
+
},
|
|
1399
|
+
}, `${label} ${countText(key)}`)),
|
|
1400
|
+
h('span', { style: { marginLeft: 'auto' } }, viewToggleOf(evolutionView, setEvolutionView, '自进化视图')),
|
|
1401
|
+
);
|
|
1402
|
+
|
|
1403
|
+
const stageLabel = (stageFilters.find(([key]) => key === stage) || ['all', '全部'])[1];
|
|
1404
|
+
const evolutionEmptyText = !memoryOn ? '记忆已关闭'
|
|
1405
|
+
: !evolution ? '读不到自进化状态'
|
|
1406
|
+
: evolution.enabled !== true ? '自进化已关闭'
|
|
1407
|
+
: rules.length === 0 ? (countsUnknown ? '数量未知' : '还没有整理出经验')
|
|
1408
|
+
: `没有${stageLabel}的经验`;
|
|
1409
|
+
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
|
+
)));
|
|
1425
|
+
|
|
1426
|
+
const evolutionReady = selectedKey === currentKey;
|
|
1427
|
+
const evolutionFailed = !evolutionReady || failed
|
|
1428
|
+
? h('div', { style: { ...PANEL_CARD, padding: '9px 11px', display: 'flex', alignItems: 'center', gap: '8px', flex: '0 0 auto' } },
|
|
1429
|
+
h('span', { style: { ...PANEL_MUTED, minWidth: 0 } }, !evolutionReady && loading ? '读取中…' : (note || '读不到自进化状态')),
|
|
1430
|
+
!evolutionReady && loading ? null : h(Action, { onClick: () => load(currentKey) }, '重试'),
|
|
1431
|
+
)
|
|
1432
|
+
: null;
|
|
1433
|
+
const evolutionDetailPane = selectedRuleRow
|
|
1434
|
+
? h('div', { style: { ...PANEL_CARD, padding: 0, minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' } },
|
|
1435
|
+
h('div', { style: { display: 'flex', justifyContent: 'flex-end', padding: '8px 10px 0', flex: '0 0 auto' } },
|
|
1436
|
+
h(Action, { onClick: () => setSelectedRule('') }, '关闭')),
|
|
1437
|
+
h('div', { style: { minHeight: 0, overflowY: 'auto', padding: '0 10px 10px' } },
|
|
1438
|
+
h(EvolutionDetail, {
|
|
1439
|
+
key: selectedRuleRow.id, rule: selectedRuleRow, evolution, memoryOn,
|
|
1440
|
+
canManage, busy, onManage: manage,
|
|
1441
|
+
})))
|
|
1442
|
+
: null;
|
|
1443
|
+
const evolutionListPane = h('div', {
|
|
1444
|
+
style: {
|
|
1445
|
+
...PANEL_CARD, padding: '2px 4px', minHeight: 0, overflowY: 'auto',
|
|
1446
|
+
},
|
|
1447
|
+
}, evolutionList);
|
|
1448
|
+
const evolutionGraphPane = h('div', {
|
|
1449
|
+
style: { ...PANEL_CARD, padding: 0, minHeight: 0, overflow: 'hidden', display: 'flex' },
|
|
1450
|
+
},
|
|
1451
|
+
h(MemoryGraph, {
|
|
1452
|
+
records: filteredRules, graphOf: ruleGraphOf, primaryKind: 'rule',
|
|
1453
|
+
selectedId: selectedRule, onPick: onPickRuleNode,
|
|
1454
|
+
emptyText: evolutionEmptyText, ariaLabel: '经验关系图',
|
|
1455
|
+
}));
|
|
1456
|
+
const evolutionTab = evolutionFailed ? evolutionFailed : h('div', { role: 'tabpanel', 'aria-label': '自进化', style: { flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', gap: '9px' } },
|
|
1457
|
+
h('div', { style: { ...PANEL_CARD, padding: '9px 11px', display: 'flex', alignItems: 'flex-start', gap: '8px', flex: '0 0 auto' } },
|
|
1458
|
+
h('span', { style: { flex: '0 0 auto', width: '6px', height: '6px', marginTop: '5px', borderRadius: '999px', background: effectiveEnabled ? 'var(--dsw-alias-brand-primary)' : 'var(--dsw-alias-label-secondary)', opacity: 0.8 } }),
|
|
1459
|
+
h('div', { style: { display: 'flex', flexDirection: 'column', gap: '2px', minWidth: 0 } },
|
|
1460
|
+
h('div', { style: { fontSize: '12px', lineHeight: '1.55' } }, activityText || (rules.length === 0 ? '还没有整理出经验' : '暂无整理记录')),
|
|
1461
|
+
countsUnknown ? h('div', { style: PANEL_MUTED }, '数量未知') : null,
|
|
1462
|
+
activityReason
|
|
1463
|
+
? (showReason
|
|
1464
|
+
? h('div', { style: { ...PANEL_MUTED, whiteSpace: 'pre-wrap', wordBreak: 'break-word' } }, activityReason)
|
|
1465
|
+
: h(Action, { onClick: () => setShowReason(true) }, '查看原因'))
|
|
1466
|
+
: null,
|
|
543
1467
|
),
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
note ? h('div', { style: EVO_MUTED }, note) : null,
|
|
560
|
-
) : null,
|
|
1468
|
+
),
|
|
1469
|
+
filterBar,
|
|
1470
|
+
h('div', {
|
|
1471
|
+
style: {
|
|
1472
|
+
flex: 1, minHeight: 0, display: 'grid', gap: '10px',
|
|
1473
|
+
gridTemplateColumns: selectedRuleRow ? 'minmax(0, 1fr) minmax(280px, 340px)' : 'minmax(0, 1fr)',
|
|
1474
|
+
},
|
|
1475
|
+
},
|
|
1476
|
+
evolutionView === 'graph' ? evolutionGraphPane : evolutionListPane,
|
|
1477
|
+
evolutionDetailPane),
|
|
1478
|
+
);
|
|
1479
|
+
|
|
1480
|
+
const content = h('section', { style: { flex: 1, display: 'flex', flexDirection: 'column', minWidth: 0, minHeight: 0, padding: '14px', gap: '10px' } },
|
|
1481
|
+
tabBar,
|
|
1482
|
+
activeTab === 'memory' ? memoryTab : evolutionTab,
|
|
561
1483
|
);
|
|
1484
|
+
|
|
1485
|
+
const shell = (children) => h('div', {
|
|
1486
|
+
style: PANEL_BACKDROP,
|
|
1487
|
+
onClick: (event) => { if (event.target === event.currentTarget) props.onClose(); },
|
|
1488
|
+
},
|
|
1489
|
+
h('div', {
|
|
1490
|
+
role: 'dialog', 'aria-modal': 'true', 'aria-label': '记忆面板',
|
|
1491
|
+
style: { ...PANEL_DIALOG, width: 'min(1120px, 100%)', height: 'min(760px, 100%)', boxSizing: 'border-box' },
|
|
1492
|
+
}, ...children));
|
|
1493
|
+
|
|
1494
|
+
if (!sessionId) return null;
|
|
1495
|
+
if (!data) {
|
|
1496
|
+
return shell([
|
|
1497
|
+
header,
|
|
1498
|
+
h('div', { style: { flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: '10px', color: 'var(--dsw-alias-label-secondary)', fontSize: '12px' } },
|
|
1499
|
+
h('span', null, loading ? '读取中…' : '读不到记忆'),
|
|
1500
|
+
loading || !note ? null : h(Action, { onClick: () => load() }, '重试'),
|
|
1501
|
+
note ? h('span', null, note) : null,
|
|
1502
|
+
),
|
|
1503
|
+
]);
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
return shell([header, switchBar, content]);
|
|
1507
|
+
};
|
|
1508
|
+
|
|
1509
|
+
/** 面板浮层。 */
|
|
1510
|
+
const MemoryOverlay = () => {
|
|
1511
|
+
const ui = usePanelUi();
|
|
1512
|
+
if (!ui.open || !ui.sessionId) return null;
|
|
1513
|
+
return h(MemoryPanel, {
|
|
1514
|
+
key: ui.sessionId,
|
|
1515
|
+
sessionId: ui.sessionId,
|
|
1516
|
+
onClose: () => updatePanel({ open: false }),
|
|
1517
|
+
});
|
|
562
1518
|
};
|
|
563
1519
|
|
|
564
1520
|
/** 输出末尾那几条:只算本轮的。 */
|
|
@@ -588,7 +1544,7 @@ window.__ModuleLoader__.load({
|
|
|
588
1544
|
|
|
589
1545
|
// 测试缝:Node 里验引用投影,浏览器里没有任何副作用。
|
|
590
1546
|
if (typeof globalThis.__LITE_MEMORY_TEST__ === 'function') {
|
|
591
|
-
globalThis.__LITE_MEMORY_TEST__({ metaOf, citationOf, citationsOfMeta, citationsOfTurn, dedupe, undoRefOf });
|
|
1547
|
+
globalThis.__LITE_MEMORY_TEST__({ metaOf, citationOf, citationsOfMeta, citationsOfTurn, dedupe, undoRefOf, graphOf, ruleGraphOf, MemoryPanel });
|
|
592
1548
|
}
|
|
593
1549
|
|
|
594
1550
|
return {
|
|
@@ -604,6 +1560,11 @@ window.__ModuleLoader__.load({
|
|
|
604
1560
|
id: 'lite-memory:project-evolution',
|
|
605
1561
|
order: 30, // ui-copy-ignore
|
|
606
1562
|
}, ProjectEvolution));
|
|
1563
|
+
ctx.slots.inject('shell.overlay', () => ctx.slots.register({
|
|
1564
|
+
name: 'shell.overlay',
|
|
1565
|
+
id: 'lite-memory:panel',
|
|
1566
|
+
order: 30, // ui-copy-ignore
|
|
1567
|
+
}, MemoryOverlay));
|
|
607
1568
|
ctx.slots.inject('conversation.chat.turnTail', () => {
|
|
608
1569
|
try {
|
|
609
1570
|
return ctx.slots.register({
|