dsh-memory-engine 0.8.0 → 0.8.2

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 CHANGED
@@ -1,8 +1,8 @@
1
1
  # DSH 记忆引擎
2
2
 
3
- [![npm](https://img.shields.io/badge/version-0.8.0-blue)](https://www.npmjs.com/package/dsh-memory-engine)
3
+ [![npm](https://img.shields.io/badge/version-0.8.1-blue)](https://www.npmjs.com/package/dsh-memory-engine)
4
4
  [![license](https://img.shields.io/badge/license-MIT-green)](LICENSE)
5
- [![tests](https://img.shields.io/badge/tests-277%20passed-brightgreen)](test)
5
+ [![tests](https://img.shields.io/badge/tests-291%20passed-brightgreen)](test)
6
6
 
7
7
  DSH 插件的本地记忆引擎,带神经网络可视化。它让 agent 保存、召回和管理跨会话记忆,也能把你已有的记忆文件导进来。
8
8
 
@@ -11,18 +11,32 @@ DSH 插件的本地记忆引擎,带神经网络可视化。它让 agent 保存
11
11
 
12
12
  ![记忆图谱](docs/screenshot.png)
13
13
 
14
- 当前版本:0.8.0。Node ≥22,零新增依赖。新增四套配色与三种布局,默认保留原来的经典星空与球体。自动记忆默认关闭,由用户在图谱头部或设置页手动打开;开启后每轮对话会额外调用一次模型,可能产生费用。自动导入默认只收聊天会话的用户消息,支持来源白名单、工作指令过滤、流程话过滤、规则分类、错误原因、一次性重扫和账本清理。
14
+ 当前版本:0.8.1。Node ≥22,零新增依赖。四套配色与球体、星系、大脑神经网络三种布局自由组合,默认保留原来的经典星空与球体。自动记忆默认关闭,由用户在图谱头部或设置页手动打开;开启后每轮对话会额外调用一次模型,可能产生费用。自动导入默认只收聊天会话的用户消息,支持来源白名单、工作指令过滤、流程话过滤、规则分类、错误原因、一次性重扫和账本清理。
15
15
 
16
- ## 0.8.0 图谱外观
16
+ ## 安装(重要)
17
+
18
+ **请指定版本号安装。** pnpm 可能因为本地缓存的包元数据解析到旧版本(实测 `pnpm add dsh-memory-engine` 会装到 0.7.5):
19
+
20
+ ```bash
21
+ pnpm add dsh-memory-engine@0.8.1
22
+ ```
23
+
24
+ 装完重启 DSH,标题栏出现「🧠 记忆图谱」。
25
+
26
+ 0.7.5 及更早版本存在 `memory_map` 参数名撞 JSON Schema 保留字的问题,调用时整轮返回 400,请勿使用。
27
+
28
+ ## 0.8.1 图谱外观
17
29
 
18
30
  图谱头部点击「外观」,可组合选择四套配色与三种布局;「设置」页也有同一组选项。点选立即应用并保存,刷新后保留;保存失败会回滚并显示原因。「恢复默认」同时恢复经典星空和球体。
19
31
 
20
32
  | 维度 | 选项 |
21
33
  | --- | --- |
22
34
  | 配色 | 经典星空 `classic` / 赛博霓虹 `neon` / 铅绘素描 `paper` / 极简单色 `mono` |
23
- | 布局 | 球体 `sphere` / 平面圆 `flat` / 螺旋银河 `spiral` |
35
+ | 布局 | 球体 `sphere` / 星系 `galaxy` / 大脑神经网络 `brain` |
36
+
37
+ 铅绘素描采用素纸底色、石墨灰节点和极淡光晕。星系有密集核心、2~4 条带抖动的旋臂和 30% 星尘;大脑神经网络有不对称的左右半球、每半球 12~14 个簇心、5% 游离节点和表面起伏。固定种子与节点 id 哈希保证相同数据的位置完全一致。三种布局都保留中心区域角色卡、旋转、缩放和点击详情;大脑角色卡位于纵裂中间偏下。切换时保留已选节点、相机及动画状态,不重新请求记忆数据,不改变记忆的重要度、留存或节点尺寸算法。星系仅将选中关联线的透明度和宽度降到球体的 80%,避免密集连线发白。
24
38
 
25
- 铅绘素描采用素纸底色、石墨灰节点和极淡光晕。平面圆按连接数分为三个同心环;螺旋银河沿三条旋臂排列。三种布局都保留角色卡中心、旋转、缩放和点击详情。切换时保留已选节点、相机及动画状态,不重新请求记忆数据,不改变记忆的重要度、留存或节点尺寸算法。
39
+ 0.8.0 的旧配置自动兼容:`flat` 按 `galaxy` 读取,`spiral` 按 `brain` 读取,不需要手动改文件,也不在读取时写盘。旧客户端提交这两个旧值也会被接受,并通过原有备份和原子写保存为新值。界面只显示球体、星系、大脑神经网络三种选项。
26
40
 
27
41
  在现有配置中可合并 `"graphTheme": "classic"` 和 `"graphLayout": "sphere"`。这两个字段通过现有配置接口和备份、原子写流程保存,非法值返回 400。未配置时外观与升级前相同;没有角色卡的库仍保留原来的默认脑状布局。CLI 生成的离线图谱也读取同一配置。
28
42
 
package/lib/client.js CHANGED
@@ -54,7 +54,9 @@ window.__ModuleLoader__.load({
54
54
  if (!Object.hasOwn(THEMES, themeName)) throw new Error('未知图谱配色');
55
55
  let theme = THEMES[themeName];
56
56
  let layoutName = options.graphLayout ?? data.graphLayout ?? 'sphere';
57
- if (!['sphere', 'flat', 'spiral'].includes(layoutName)) throw new Error('未知图谱布局');
57
+ if (layoutName === 'flat') layoutName = 'galaxy';
58
+ if (layoutName === 'spiral') layoutName = 'brain';
59
+ if (!['sphere', 'galaxy', 'brain'].includes(layoutName)) throw new Error('未知图谱布局');
58
60
  let sphereNodes = [];
59
61
  const layouts = new Map();
60
62
  let nodes = [], links = [], hubs = [], linkTwinkle = [], visibleLinks = [];
@@ -102,7 +104,9 @@ function glowSprite(rgb) {
102
104
 
103
105
  function setLayout(nextLayout) {
104
106
  if (destroyed) return;
105
- if (!['sphere', 'flat', 'spiral'].includes(nextLayout)) throw new Error('未知图谱布局');
107
+ if (nextLayout === 'flat') nextLayout = 'galaxy';
108
+ if (nextLayout === 'spiral') nextLayout = 'brain';
109
+ if (!['sphere', 'galaxy', 'brain'].includes(nextLayout)) throw new Error('未知图谱布局');
106
110
  if (layoutName === nextLayout && layouts.has(nextLayout)) return;
107
111
  if (!layouts.has(nextLayout)) {
108
112
  if (typeof options.layoutNodes !== 'function') throw new Error('缺少图谱布局函数');
@@ -327,20 +331,22 @@ function frame(now) {
327
331
  }
328
332
  }
329
333
  if (linkPaths.length) {
334
+ if (layoutName === 'galaxy') ctx.globalAlpha=0.8;
330
335
  if (Array.isArray(theme.link)) {
331
336
  const gradient = ctx.createLinearGradient(0,0,width,height);
332
337
  gradient.addColorStop(0,theme.link[0]);
333
338
  gradient.addColorStop(1,theme.link[1]);
334
339
  ctx.strokeStyle=gradient;
335
340
  } else ctx.strokeStyle=theme.link;
336
- ctx.lineWidth=1;
341
+ ctx.lineWidth=layoutName === 'galaxy' ? 0.8 : 1;
337
342
  ctx.beginPath();
338
343
  for (const p of linkPaths) { ctx.moveTo(p[0],p[1]); ctx.quadraticCurveTo(p[2],p[3],p[4],p[5]); }
339
344
  ctx.stroke();
340
345
  // 更亮的一层,让关联线看得清
341
346
  ctx.strokeStyle=theme.linkHighlight;
342
- ctx.lineWidth=2;
347
+ ctx.lineWidth=layoutName === 'galaxy' ? 1.6 : 2;
343
348
  ctx.stroke();
349
+ if (layoutName === 'galaxy') ctx.globalAlpha=1;
344
350
  }
345
351
  for (let index=pulses.length-1;index>=0;index--) {
346
352
  const pulse=pulses[index],fraction=(now-pulse.born)/pulse.life;
@@ -473,7 +479,9 @@ function frame(now) {
473
479
 
474
480
 
475
481
  function layoutPositions(nodes, layout = 'sphere') {
476
- if (!['sphere', 'flat', 'spiral'].includes(layout)) throw new Error('未知图谱布局');
482
+ if (layout === 'flat') layout = 'galaxy';
483
+ if (layout === 'spiral') layout = 'brain';
484
+ if (!['sphere', 'galaxy', 'brain'].includes(layout)) throw new Error('未知图谱布局');
477
485
  if (layout === 'sphere') return nodes.map(node => ({ ...node, pos: { ...node.pos } }));
478
486
  const memories = nodes.filter(node => !node.isCore).sort((left, right) =>
479
487
  (right.degree || 0) - (left.degree || 0) || (String(left.id) < String(right.id) ? -1 : String(left.id) > String(right.id) ? 1 : 0));
@@ -484,34 +492,117 @@ function frame(now) {
484
492
  hash = Math.imul(hash ^ (hash >>> 16), 0x45d9f3b);
485
493
  return ((hash ^ (hash >>> 16)) >>> 0) / 4294967295 * 2 - 1;
486
494
  }
487
- if (layout === 'flat') {
488
- for (let ring = 0; ring < 3; ring++) {
489
- const members = memories.slice(Math.ceil(memories.length * ring / 3), Math.ceil(memories.length * (ring + 1) / 3));
490
- const radius = [30, 55, 80][ring];
491
- members.forEach((node, index) => {
492
- const angle = index / members.length * Math.PI * 2;
493
- positions.set(node.id, { x: Math.cos(angle) * radius + jitter(node.id, 'x') * 3,
494
- y: Math.sin(angle) * radius + jitter(node.id, 'y') * 3, z: 0 });
495
- });
496
- }
497
- } else {
495
+ let datasetSeed = 20261030;
496
+ for (const id of memories.map(node => String(node.id)).sort()) {
497
+ for (const character of id + '\u0000') datasetSeed = Math.imul(datasetSeed ^ character.codePointAt(0), 16777619);
498
+ }
499
+ const uniform = (id, axis) => Math.max(1e-9, Math.min(1 - 1e-9, (jitter(datasetSeed + ':' + id, axis) + 1) / 2));
500
+ const gaussian = (id, axis) => Math.sqrt(-2 * Math.log(uniform(id, axis + ':radius'))) * Math.cos(2 * Math.PI * uniform(id, axis + ':angle'));
501
+ if (layout === 'galaxy') {
502
+ const armCount = 2 + Math.floor(uniform('galaxy', 'arms') * 3);
503
+ const rotation = uniform('galaxy', 'rotation') * Math.PI * 2;
504
+ const coreCount = Math.ceil(memories.length * 0.15);
505
+ const haloCount = Math.floor(memories.length * 0.05);
506
+ const diskEnd = memories.length - haloCount;
507
+ const dustCount = Math.round(memories.length * 0.30);
508
+ const dust = new Set(memories.slice(coreCount, diskEnd)
509
+ .sort((left, right) => uniform(left.id, 'dust') - uniform(right.id, 'dust') || (String(left.id) < String(right.id) ? -1 : String(left.id) > String(right.id) ? 1 : 0))
510
+ .slice(0, Math.max(0, dustCount - haloCount)).map(node => node.id));
498
511
  memories.forEach((node, index) => {
499
- const fraction = index / Math.max(1, memories.length - 1);
500
- const angle = fraction * 3.2 * Math.PI + index % 3 * Math.PI * 2 / 3 + jitter(node.id, 'angle') * 0.35;
501
- const radius = 18 + fraction * 92;
502
- positions.set(node.id, { x: Math.cos(angle) * radius, y: Math.sin(angle) * radius, z: jitter(node.id, 'z') * 14 });
512
+ let radius, angle;
513
+ if (index < coreCount) {
514
+ radius = 6 + Math.sqrt(uniform(node.id, 'core-radius')) * 16;
515
+ angle = uniform(node.id, 'core-angle') * Math.PI * 2;
516
+ } else if (index >= diskEnd) {
517
+ radius = 95 + Math.sqrt(uniform(node.id, 'halo-radius')) * 35;
518
+ angle = uniform(node.id, 'halo-angle') * Math.PI * 2;
519
+ } else if (dust.has(node.id)) {
520
+ radius = 22 + Math.sqrt(uniform(node.id, 'dust-radius')) * 73;
521
+ angle = uniform(node.id, 'dust-angle') * Math.PI * 2;
522
+ } else {
523
+ const fraction = (index - coreCount + 0.5) / Math.max(1, diskEnd - coreCount);
524
+ const offset = fraction * 73 + gaussian(node.id, 'radius') * 9;
525
+ const folded = ((offset % 146) + 146) % 146;
526
+ radius = 22 + (folded <= 73 ? folded : 146 - folded);
527
+ const arm = Math.floor(uniform(node.id, 'arm') * armCount);
528
+ const base = rotation + arm * Math.PI * 2 / armCount + (uniform(arm, 'arm-offset') - 0.5) * 0.35;
529
+ const twist = (radius - 18) / 95 * 2.2 * Math.PI;
530
+ const ripple = Math.sin(radius * 0.055 + uniform(arm, 'phase') * Math.PI * 2) * 0.10;
531
+ angle = base + twist + ripple + gaussian(node.id, 'angle') * 0.30;
532
+ }
533
+ const thickness = 8 / (1 + radius / 40);
534
+ const height = Math.max(-1, Math.min(1, gaussian(node.id, 'height') * 0.5)) * thickness;
535
+ positions.set(node.id, { x: Math.cos(angle) * radius, y: Math.sin(angle) * radius, z: height });
536
+ });
537
+ } else {
538
+ function brainPoint(id, axis, extent = 1) {
539
+ const horizontal = gaussian(id, axis + ':x');
540
+ const vertical = gaussian(id, axis + ':y');
541
+ const depth = gaussian(id, axis + ':z');
542
+ const scale = Math.cbrt(uniform(id, axis + ':distance')) * extent / Math.hypot(horizontal, vertical, depth);
543
+ return { x: 42 + horizontal * scale * 40, y: vertical * scale * 106, z: depth * scale * 52 };
544
+ }
545
+ const centers = [0, 1].map(side => {
546
+ const count = 12 + Math.floor(uniform('brain:' + side, 'clusters') * 3);
547
+ const candidates = Array.from({ length: 96 }, (_, index) => {
548
+ const id = 'brain:' + side + ':' + index;
549
+ const point = brainPoint(id, 'center', 0.82 + uniform(id, 'surface') * 0.12);
550
+ return { ...point, spread: 6 + uniform(id, 'spread') * 8, phase: uniform(id, 'phase') * Math.PI * 2 };
551
+ });
552
+ const interior = candidates.reduce((best, point, index) => Math.hypot(point.x - 42, point.y, point.z)
553
+ < Math.hypot(candidates[best].x - 42, candidates[best].y, candidates[best].z) ? index : best, 0);
554
+ const selected = [candidates.splice(interior, 1)[0]];
555
+ while (selected.length < count) {
556
+ let best = 0, greatest = -Infinity;
557
+ candidates.forEach((point, index) => {
558
+ const distance = Math.min(...selected.map(center => Math.hypot(point.x - center.x, point.y - center.y, (point.z - center.z) * 0.7)));
559
+ if (distance > greatest) { greatest = distance; best = index; }
560
+ });
561
+ selected.push(candidates.splice(best, 1)[0]);
562
+ }
563
+ return selected;
564
+ });
565
+ const roamers = new Set([...memories].sort((left, right) => uniform(left.id, 'roaming') - uniform(right.id, 'roaming')
566
+ || (String(left.id) < String(right.id) ? -1 : String(left.id) > String(right.id) ? 1 : 0))
567
+ .slice(0, Math.round(memories.length * 0.05)).map(node => node.id));
568
+ memories.forEach(node => {
569
+ const side = uniform(node.id, 'hemisphere') < 0.5 ? 0 : 1;
570
+ const target = brainPoint(node.id, 'target');
571
+ let center, nearest = Infinity;
572
+ centers[side].forEach((candidate, index) => {
573
+ const distance = Math.hypot(target.x - candidate.x, target.y - candidate.y, target.z - candidate.z)
574
+ * (0.8 + uniform(node.id, 'weight:' + index) * 0.4);
575
+ if (distance < nearest) { nearest = distance; center = candidate; }
576
+ });
577
+ const point = roamers.has(node.id) ? target : {
578
+ x: center.x + gaussian(node.id, 'scatter:x') * center.spread,
579
+ y: center.y + gaussian(node.id, 'scatter:y') * center.spread,
580
+ z: center.z + gaussian(node.id, 'scatter:z') * center.spread,
581
+ };
582
+ point.x += Math.sin(point.y * 0.08 + center.phase) * 6;
583
+ point.z += Math.sin(point.y * 0.09 + center.phase) * 4;
584
+ point.y += Math.sin(point.z * 0.12 + center.phase) * 3;
585
+ const envelope = 0.96 + 0.06 * Math.sin(point.y * 0.08) + 0.035 * Math.cos(point.z * 0.11 + side);
586
+ const distance = Math.hypot((point.x - 42) / 40, point.y / 106, point.z / 52);
587
+ if (distance > envelope) {
588
+ const scale = envelope * (0.93 + uniform(node.id, 'edge') * 0.06) / distance;
589
+ point.x = 42 + (point.x - 42) * scale; point.y *= scale; point.z *= scale;
590
+ }
591
+ positions.set(node.id, { x: (side ? 1 : -1) * (4 + Math.abs(point.x)) + (side ? 2 : 0),
592
+ y: point.y - (side ? 1 : 0), z: point.z });
503
593
  });
504
594
  }
505
- return nodes.map(node => ({ ...node, pos: node.isCore ? { x: 0, y: 0, z: 0 } : positions.get(node.id) }));
595
+ return nodes.map(node => ({ ...node, pos: node.isCore ? { x: 0, y: layout === 'brain' ? 14 : 0, z: 0 } : positions.get(node.id) }));
506
596
  }
507
597
 
508
598
  var defaultSettings = { hideButton: false, showGuideOnEmpty: true, graphTheme: "classic", graphLayout: "sphere", autoMemory: { enabled: false, maxPerTurn: 2, dailyCap: 40 }, showAutoMemoryCostHint: true };
509
599
  var actionStyle = { padding: "7px 12px", borderRadius: "8px", border: "1px solid var(--memory-border,#6999b8)", background: "var(--memory-button,#102236)", color: "var(--memory-text,#e2f4ff)", font: "inherit", cursor: "pointer" };
510
- var fieldStyle = { boxSizing: "border-box", padding: "10px 12px", borderRadius: "8px", border: "1px solid #35536b", background: "#091422", color: "#e2f4ff", font: "inherit", minWidth: 0 };
600
+ var closeStyle = Object.assign({}, actionStyle, { padding: "11px 20px", minHeight: "42px", fontWeight: 700, letterSpacing: "0.02em" });
601
+ var fieldStyle = { boxSizing: "border-box", padding: "10px 12px", borderRadius: "8px", border: "1px solid #35536b", background: "#091422", color: "#e2f4ff", font: "inherit", minWidth: 0 };
511
602
  var kinds = ["知识", "印象", "对话", "事件", "日记"];
512
603
  var appearanceOptions = [
513
604
  { key: "graphTheme", label: "配色", choices: [["classic", "经典星空"], ["neon", "赛博霓虹"], ["paper", "铅绘素描"], ["mono", "极简单色"]] },
514
- { key: "graphLayout", label: "布局", choices: [["sphere", "球体"], ["flat", "平面圆"], ["spiral", "螺旋银河"]] },
605
+ { key: "graphLayout", label: "布局", choices: [["sphere", "球体"], ["galaxy", "星系"], ["brain", "大脑神经网络"]] },
515
606
  ];
516
607
  var appearanceStyles = {
517
608
  classic: {},
@@ -676,7 +767,7 @@ function frame(now) {
676
767
  style: { position: "absolute", top: "72px", right: "18px", zIndex: 1, width: "min(350px,calc(100% - 36px))", maxHeight: "calc(100% - 96px)", boxSizing: "border-box", display: "flex", flexDirection: "column", overflow: "hidden", padding: "18px", border: "1px solid #3976a0", borderRadius: "14px", background: "#07111fee", color: "#dbeaf8", font: "14px/1.6 system-ui,sans-serif", boxShadow: "0 0 40px #1763a033", backdropFilter: "blur(8px)", animation: "dsh-memory-detail-in 180ms ease-out" },
677
768
  }, React.createElement("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: "12px", flexShrink: 0 } },
678
769
  React.createElement("span", { "data-memory-engine-detail-kind": "true", style: { color: "#a4e6ff", background: "#15344b", borderRadius: "6px", padding: "2px 8px", overflowWrap: "anywhere" } }, node.kind || "未分类"),
679
- React.createElement("button", { type: "button", "aria-label": "关闭记忆详情", onClick: props.onClose, style: { border: 0, padding: "2px 6px", background: "transparent", color: "#a4bad0", fontSize: "22px", cursor: "pointer" } }, "×")),
770
+ React.createElement("button", { type: "button", "aria-label": "关闭记忆详情", onClick: props.onClose, style: Object.assign({ minWidth: "40px", minHeight: "40px", fontSize: "18px", lineHeight: 1, cursor: "pointer" }, { border: 0, padding: "2px 6px", background: "transparent", color: "#a4bad0", fontSize: "22px", cursor: "pointer" }) }, "×")),
680
771
  React.createElement("h2", { "data-memory-engine-detail-title": "true", style: { margin: "10px 0", fontSize: "18px", color: "#b7e9ff", overflowWrap: "anywhere", whiteSpace: "normal", maxHeight: "18vh", overflowY: "auto", flexShrink: 0 } }, title),
681
772
  node.isCore ? null : React.createElement("dl", { style: { display: "grid", gridTemplateColumns: "64px minmax(0,1fr)", gap: "5px 10px", margin: "0 0 12px", fontSize: "12px", color: "#9fb7ce", maxHeight: "25vh", overflowY: "auto", flexShrink: 0 } },
682
773
  rows.map(function (row) { return React.createElement(React.Fragment, { key: row[0] },
@@ -1127,7 +1218,7 @@ function frame(now) {
1127
1218
  return function () { if (configRequest.current) configRequest.current.abort(); };
1128
1219
  }, []);
1129
1220
  React.useEffect(function () {
1130
- if (!open) return undefined;
1221
+ if (!open || typeof window === "undefined" || typeof window.addEventListener !== "function") return undefined;
1131
1222
  var controller = new AbortController();
1132
1223
  setConfigBusy(true);
1133
1224
  readApi("/memory-engine/api/config", controller.signal).then(function (data) {
@@ -1219,7 +1310,7 @@ function frame(now) {
1219
1310
  if (gesture && gesture.moved < 6 && gesture.version === selectionVersion.current) setSelected(null);
1220
1311
  }
1221
1312
  React.useEffect(function () {
1222
- if (!open) return undefined;
1313
+ if (!open || typeof window === "undefined" || typeof window.addEventListener !== "function") return undefined;
1223
1314
  var controller = new AbortController();
1224
1315
  var active = true;
1225
1316
  setGuideData(null);
@@ -1237,7 +1328,7 @@ function frame(now) {
1237
1328
  return function () { active = false; controller.abort(); };
1238
1329
  }, [open, retry, settings.showGuideOnEmpty]);
1239
1330
  React.useEffect(function () {
1240
- if (!open) return undefined;
1331
+ if (!open || typeof window === "undefined" || typeof window.addEventListener !== "function") return undefined;
1241
1332
  function onEscape() {
1242
1333
  if (guideOpen) closeGuide();
1243
1334
  else if (appearanceOpen) { setAppearanceOpen(false); if (appearanceButton.current) appearanceButton.current.focus(); }
@@ -1296,12 +1387,13 @@ function frame(now) {
1296
1387
  type: "button",
1297
1388
  onClick: closeMap,
1298
1389
  "aria-label": "关闭记忆图谱",
1299
- style: actionStyle,
1390
+ title: "关闭(Esc)",
1391
+ style: closeStyle,
1300
1392
  }, "关闭 ×")), appearanceOpen ? React.createElement("section", { ref: appearancePanel, role: "dialog", "aria-label": "外观", "data-memory-engine-appearance-panel": "true",
1301
1393
  style: { position: "absolute", top: "62px", right: "18px", zIndex: 3, boxSizing: "border-box", width: "min(380px,calc(100vw - 36px))", maxHeight: "calc(100vh - 90px)", overflowY: "auto",
1302
1394
  padding: "18px", border: "1px solid var(--memory-border,#3976a0)", borderRadius: "12px", background: "var(--memory-panel,#07111f)", boxShadow: "0 12px 35px #0004" } },
1303
1395
  React.createElement("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "14px", color: "var(--memory-text,#dbeaf8)" } },
1304
- React.createElement("strong", null, "外观"), React.createElement("button", { type: "button", "aria-label": "关闭外观", style: actionStyle,
1396
+ React.createElement("strong", null, "外观"), React.createElement("button", { type: "button", "aria-label": "关闭外观", style: closeStyle,
1305
1397
  onClick: function () { setAppearanceOpen(false); if (appearanceButton.current) appearanceButton.current.focus(); } }, "×")),
1306
1398
  React.createElement(AppearanceControls, { settings: settings, busy: configBusy, onChange: saveAppearance, location: "header" })) : null,
1307
1399
  configError ? React.createElement("p", { role: "alert", "data-memory-engine-config-error": "true",
package/lib/client.src.js CHANGED
@@ -18,11 +18,12 @@ window.__ModuleLoader__.load({
18
18
 
19
19
  var defaultSettings = { hideButton: false, showGuideOnEmpty: true, graphTheme: "classic", graphLayout: "sphere", autoMemory: { enabled: false, maxPerTurn: 2, dailyCap: 40 }, showAutoMemoryCostHint: true };
20
20
  var actionStyle = { padding: "7px 12px", borderRadius: "8px", border: "1px solid var(--memory-border,#6999b8)", background: "var(--memory-button,#102236)", color: "var(--memory-text,#e2f4ff)", font: "inherit", cursor: "pointer" };
21
- var fieldStyle = { boxSizing: "border-box", padding: "10px 12px", borderRadius: "8px", border: "1px solid #35536b", background: "#091422", color: "#e2f4ff", font: "inherit", minWidth: 0 };
21
+ var closeStyle = Object.assign({}, actionStyle, { padding: "11px 20px", minHeight: "42px", fontWeight: 700, letterSpacing: "0.02em" });
22
+ var fieldStyle = { boxSizing: "border-box", padding: "10px 12px", borderRadius: "8px", border: "1px solid #35536b", background: "#091422", color: "#e2f4ff", font: "inherit", minWidth: 0 };
22
23
  var kinds = ["知识", "印象", "对话", "事件", "日记"];
23
24
  var appearanceOptions = [
24
25
  { key: "graphTheme", label: "配色", choices: [["classic", "经典星空"], ["neon", "赛博霓虹"], ["paper", "铅绘素描"], ["mono", "极简单色"]] },
25
- { key: "graphLayout", label: "布局", choices: [["sphere", "球体"], ["flat", "平面圆"], ["spiral", "螺旋银河"]] },
26
+ { key: "graphLayout", label: "布局", choices: [["sphere", "球体"], ["galaxy", "星系"], ["brain", "大脑神经网络"]] },
26
27
  ];
27
28
  var appearanceStyles = {
28
29
  classic: {},
@@ -187,7 +188,7 @@ window.__ModuleLoader__.load({
187
188
  style: { position: "absolute", top: "72px", right: "18px", zIndex: 1, width: "min(350px,calc(100% - 36px))", maxHeight: "calc(100% - 96px)", boxSizing: "border-box", display: "flex", flexDirection: "column", overflow: "hidden", padding: "18px", border: "1px solid #3976a0", borderRadius: "14px", background: "#07111fee", color: "#dbeaf8", font: "14px/1.6 system-ui,sans-serif", boxShadow: "0 0 40px #1763a033", backdropFilter: "blur(8px)", animation: "dsh-memory-detail-in 180ms ease-out" },
188
189
  }, React.createElement("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: "12px", flexShrink: 0 } },
189
190
  React.createElement("span", { "data-memory-engine-detail-kind": "true", style: { color: "#a4e6ff", background: "#15344b", borderRadius: "6px", padding: "2px 8px", overflowWrap: "anywhere" } }, node.kind || "未分类"),
190
- React.createElement("button", { type: "button", "aria-label": "关闭记忆详情", onClick: props.onClose, style: { border: 0, padding: "2px 6px", background: "transparent", color: "#a4bad0", fontSize: "22px", cursor: "pointer" } }, "×")),
191
+ React.createElement("button", { type: "button", "aria-label": "关闭记忆详情", onClick: props.onClose, style: Object.assign({ minWidth: "40px", minHeight: "40px", fontSize: "18px", lineHeight: 1, cursor: "pointer" }, { border: 0, padding: "2px 6px", background: "transparent", color: "#a4bad0", fontSize: "22px", cursor: "pointer" }) }, "×")),
191
192
  React.createElement("h2", { "data-memory-engine-detail-title": "true", style: { margin: "10px 0", fontSize: "18px", color: "#b7e9ff", overflowWrap: "anywhere", whiteSpace: "normal", maxHeight: "18vh", overflowY: "auto", flexShrink: 0 } }, title),
192
193
  node.isCore ? null : React.createElement("dl", { style: { display: "grid", gridTemplateColumns: "64px minmax(0,1fr)", gap: "5px 10px", margin: "0 0 12px", fontSize: "12px", color: "#9fb7ce", maxHeight: "25vh", overflowY: "auto", flexShrink: 0 } },
193
194
  rows.map(function (row) { return React.createElement(React.Fragment, { key: row[0] },
@@ -638,7 +639,7 @@ window.__ModuleLoader__.load({
638
639
  return function () { if (configRequest.current) configRequest.current.abort(); };
639
640
  }, []);
640
641
  React.useEffect(function () {
641
- if (!open) return undefined;
642
+ if (!open || typeof window === "undefined" || typeof window.addEventListener !== "function") return undefined;
642
643
  var controller = new AbortController();
643
644
  setConfigBusy(true);
644
645
  readApi("/memory-engine/api/config", controller.signal).then(function (data) {
@@ -730,7 +731,7 @@ window.__ModuleLoader__.load({
730
731
  if (gesture && gesture.moved < 6 && gesture.version === selectionVersion.current) setSelected(null);
731
732
  }
732
733
  React.useEffect(function () {
733
- if (!open) return undefined;
734
+ if (!open || typeof window === "undefined" || typeof window.addEventListener !== "function") return undefined;
734
735
  var controller = new AbortController();
735
736
  var active = true;
736
737
  setGuideData(null);
@@ -748,7 +749,7 @@ window.__ModuleLoader__.load({
748
749
  return function () { active = false; controller.abort(); };
749
750
  }, [open, retry, settings.showGuideOnEmpty]);
750
751
  React.useEffect(function () {
751
- if (!open) return undefined;
752
+ if (!open || typeof window === "undefined" || typeof window.addEventListener !== "function") return undefined;
752
753
  function onEscape() {
753
754
  if (guideOpen) closeGuide();
754
755
  else if (appearanceOpen) { setAppearanceOpen(false); if (appearanceButton.current) appearanceButton.current.focus(); }
@@ -807,12 +808,13 @@ window.__ModuleLoader__.load({
807
808
  type: "button",
808
809
  onClick: closeMap,
809
810
  "aria-label": "关闭记忆图谱",
810
- style: actionStyle,
811
+ title: "关闭(Esc)",
812
+ style: closeStyle,
811
813
  }, "关闭 ×")), appearanceOpen ? React.createElement("section", { ref: appearancePanel, role: "dialog", "aria-label": "外观", "data-memory-engine-appearance-panel": "true",
812
814
  style: { position: "absolute", top: "62px", right: "18px", zIndex: 3, boxSizing: "border-box", width: "min(380px,calc(100vw - 36px))", maxHeight: "calc(100vh - 90px)", overflowY: "auto",
813
815
  padding: "18px", border: "1px solid var(--memory-border,#3976a0)", borderRadius: "12px", background: "var(--memory-panel,#07111f)", boxShadow: "0 12px 35px #0004" } },
814
816
  React.createElement("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "14px", color: "var(--memory-text,#dbeaf8)" } },
815
- React.createElement("strong", null, "外观"), React.createElement("button", { type: "button", "aria-label": "关闭外观", style: actionStyle,
817
+ React.createElement("strong", null, "外观"), React.createElement("button", { type: "button", "aria-label": "关闭外观", style: closeStyle,
816
818
  onClick: function () { setAppearanceOpen(false); if (appearanceButton.current) appearanceButton.current.focus(); } }, "×")),
817
819
  React.createElement(AppearanceControls, { settings: settings, busy: configBusy, onChange: saveAppearance, location: "header" })) : null,
818
820
  configError ? React.createElement("p", { role: "alert", "data-memory-engine-config-error": "true",