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.
@@ -16,7 +16,7 @@ const writable = new Map([
16
16
  ['autoMemory.dailyCap', value => Number.isSafeInteger(value) && value >= 0 && value <= 500],
17
17
  ['spinSpeed', value => Number.isFinite(value) && value >= 0.05 && value <= 1.5],
18
18
  ['graphTheme', value => ['classic', 'neon', 'paper', 'mono'].includes(value)],
19
- ['graphLayout', value => ['sphere', 'flat', 'spiral'].includes(value)],
19
+ ['graphLayout', value => ['sphere', 'galaxy', 'brain', 'flat', 'spiral'].includes(value)],
20
20
  ['showGuideOnEmpty', value => typeof value === 'boolean'],
21
21
  ['hideButton', value => typeof value === 'boolean'],
22
22
  ]);
@@ -61,6 +61,10 @@ export function updateConfig(home, changes, { now = Date.now, warn = () => {} }
61
61
  try { next.autoImport.sessionFilter = normalizeSessionFilter({ ...current.autoImport?.sessionFilter, ...changes.autoImport.sessionFilter }); }
62
62
  catch (error) { invalid(error.message); }
63
63
  }
64
+ if (Object.hasOwn(changes, 'graphLayout')) {
65
+ if (next.graphLayout === 'flat') next.graphLayout = 'galaxy';
66
+ if (next.graphLayout === 'spiral') next.graphLayout = 'brain';
67
+ }
64
68
  if (changes.autoMemory?.enabled === true) next.showAutoMemoryCostHint = false;
65
69
  fs.mkdirSync(directory, { recursive: true });
66
70
  const prefix = `config.json.bak-${now()}`;
package/lib/config.js CHANGED
@@ -53,7 +53,9 @@ export function readConfig(home = defaultHome(), { warn } = {}) {
53
53
  if (value.spinSpeed !== undefined && (!Number.isFinite(value.spinSpeed) || value.spinSpeed < 0.05 || value.spinSpeed > 1.5)) {
54
54
  throw new Error('spinSpeed 必须是 0.05~1.5 的数字');
55
55
  }
56
- for (const [key, choices] of [['graphTheme', ['classic', 'neon', 'paper', 'mono']], ['graphLayout', ['sphere', 'flat', 'spiral']]]) {
56
+ if (value.graphLayout === 'flat') value.graphLayout = 'galaxy';
57
+ if (value.graphLayout === 'spiral') value.graphLayout = 'brain';
58
+ for (const [key, choices] of [['graphTheme', ['classic', 'neon', 'paper', 'mono']], ['graphLayout', ['sphere', 'galaxy', 'brain']]]) {
57
59
  if (value[key] !== undefined && !choices.includes(value[key])) throw new Error(`${key} 必须是 ${choices.join('、')}`);
58
60
  }
59
61
  return { ...value, personaTitle: value.personaTitle?.trim() || '角色卡', spinSpeed: value.spinSpeed ?? 0.35,
@@ -38,7 +38,9 @@ export function createRenderer(canvas, data, options = {}) {
38
38
  if (!Object.hasOwn(THEMES, themeName)) throw new Error('未知图谱配色');
39
39
  let theme = THEMES[themeName];
40
40
  let layoutName = options.graphLayout ?? data.graphLayout ?? 'sphere';
41
- if (!['sphere', 'flat', 'spiral'].includes(layoutName)) throw new Error('未知图谱布局');
41
+ if (layoutName === 'flat') layoutName = 'galaxy';
42
+ if (layoutName === 'spiral') layoutName = 'brain';
43
+ if (!['sphere', 'galaxy', 'brain'].includes(layoutName)) throw new Error('未知图谱布局');
42
44
  let sphereNodes = [];
43
45
  const layouts = new Map();
44
46
  let nodes = [], links = [], hubs = [], linkTwinkle = [], visibleLinks = [];
@@ -86,7 +88,9 @@ function glowSprite(rgb) {
86
88
 
87
89
  function setLayout(nextLayout) {
88
90
  if (destroyed) return;
89
- if (!['sphere', 'flat', 'spiral'].includes(nextLayout)) throw new Error('未知图谱布局');
91
+ if (nextLayout === 'flat') nextLayout = 'galaxy';
92
+ if (nextLayout === 'spiral') nextLayout = 'brain';
93
+ if (!['sphere', 'galaxy', 'brain'].includes(nextLayout)) throw new Error('未知图谱布局');
90
94
  if (layoutName === nextLayout && layouts.has(nextLayout)) return;
91
95
  if (!layouts.has(nextLayout)) {
92
96
  if (typeof options.layoutNodes !== 'function') throw new Error('缺少图谱布局函数');
@@ -311,20 +315,22 @@ function frame(now) {
311
315
  }
312
316
  }
313
317
  if (linkPaths.length) {
318
+ if (layoutName === 'galaxy') ctx.globalAlpha=0.8;
314
319
  if (Array.isArray(theme.link)) {
315
320
  const gradient = ctx.createLinearGradient(0,0,width,height);
316
321
  gradient.addColorStop(0,theme.link[0]);
317
322
  gradient.addColorStop(1,theme.link[1]);
318
323
  ctx.strokeStyle=gradient;
319
324
  } else ctx.strokeStyle=theme.link;
320
- ctx.lineWidth=1;
325
+ ctx.lineWidth=layoutName === 'galaxy' ? 0.8 : 1;
321
326
  ctx.beginPath();
322
327
  for (const p of linkPaths) { ctx.moveTo(p[0],p[1]); ctx.quadraticCurveTo(p[2],p[3],p[4],p[5]); }
323
328
  ctx.stroke();
324
329
  // 更亮的一层,让关联线看得清
325
330
  ctx.strokeStyle=theme.linkHighlight;
326
- ctx.lineWidth=2;
331
+ ctx.lineWidth=layoutName === 'galaxy' ? 1.6 : 2;
327
332
  ctx.stroke();
333
+ if (layoutName === 'galaxy') ctx.globalAlpha=1;
328
334
  }
329
335
  for (let index=pulses.length-1;index>=0;index--) {
330
336
  const pulse=pulses[index],fraction=(now-pulse.born)/pulse.life;
@@ -1,7 +1,9 @@
1
1
  const SEED = 20261030;
2
2
 
3
3
  export function layoutPositions(nodes, layout = 'sphere') {
4
- if (!['sphere', 'flat', 'spiral'].includes(layout)) throw new Error('未知图谱布局');
4
+ if (layout === 'flat') layout = 'galaxy';
5
+ if (layout === 'spiral') layout = 'brain';
6
+ if (!['sphere', 'galaxy', 'brain'].includes(layout)) throw new Error('未知图谱布局');
5
7
  if (layout === 'sphere') return nodes.map(node => ({ ...node, pos: { ...node.pos } }));
6
8
  const memories = nodes.filter(node => !node.isCore).sort((left, right) =>
7
9
  (right.degree || 0) - (left.degree || 0) || (String(left.id) < String(right.id) ? -1 : String(left.id) > String(right.id) ? 1 : 0));
@@ -12,25 +14,107 @@ export function layoutPositions(nodes, layout = 'sphere') {
12
14
  hash = Math.imul(hash ^ (hash >>> 16), 0x45d9f3b);
13
15
  return ((hash ^ (hash >>> 16)) >>> 0) / 4294967295 * 2 - 1;
14
16
  }
15
- if (layout === 'flat') {
16
- for (let ring = 0; ring < 3; ring++) {
17
- const members = memories.slice(Math.ceil(memories.length * ring / 3), Math.ceil(memories.length * (ring + 1) / 3));
18
- const radius = [30, 55, 80][ring];
19
- members.forEach((node, index) => {
20
- const angle = index / members.length * Math.PI * 2;
21
- positions.set(node.id, { x: Math.cos(angle) * radius + jitter(node.id, 'x') * 3,
22
- y: Math.sin(angle) * radius + jitter(node.id, 'y') * 3, z: 0 });
23
- });
24
- }
25
- } else {
17
+ let datasetSeed = 20261030;
18
+ for (const id of memories.map(node => String(node.id)).sort()) {
19
+ for (const character of id + '\u0000') datasetSeed = Math.imul(datasetSeed ^ character.codePointAt(0), 16777619);
20
+ }
21
+ const uniform = (id, axis) => Math.max(1e-9, Math.min(1 - 1e-9, (jitter(datasetSeed + ':' + id, axis) + 1) / 2));
22
+ const gaussian = (id, axis) => Math.sqrt(-2 * Math.log(uniform(id, axis + ':radius'))) * Math.cos(2 * Math.PI * uniform(id, axis + ':angle'));
23
+ if (layout === 'galaxy') {
24
+ const armCount = 2 + Math.floor(uniform('galaxy', 'arms') * 3);
25
+ const rotation = uniform('galaxy', 'rotation') * Math.PI * 2;
26
+ const coreCount = Math.ceil(memories.length * 0.15);
27
+ const haloCount = Math.floor(memories.length * 0.05);
28
+ const diskEnd = memories.length - haloCount;
29
+ const dustCount = Math.round(memories.length * 0.30);
30
+ const dust = new Set(memories.slice(coreCount, diskEnd)
31
+ .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))
32
+ .slice(0, Math.max(0, dustCount - haloCount)).map(node => node.id));
26
33
  memories.forEach((node, index) => {
27
- const fraction = index / Math.max(1, memories.length - 1);
28
- const angle = fraction * 3.2 * Math.PI + index % 3 * Math.PI * 2 / 3 + jitter(node.id, 'angle') * 0.35;
29
- const radius = 18 + fraction * 92;
30
- positions.set(node.id, { x: Math.cos(angle) * radius, y: Math.sin(angle) * radius, z: jitter(node.id, 'z') * 14 });
34
+ let radius, angle;
35
+ if (index < coreCount) {
36
+ radius = 6 + Math.sqrt(uniform(node.id, 'core-radius')) * 16;
37
+ angle = uniform(node.id, 'core-angle') * Math.PI * 2;
38
+ } else if (index >= diskEnd) {
39
+ radius = 95 + Math.sqrt(uniform(node.id, 'halo-radius')) * 35;
40
+ angle = uniform(node.id, 'halo-angle') * Math.PI * 2;
41
+ } else if (dust.has(node.id)) {
42
+ radius = 22 + Math.sqrt(uniform(node.id, 'dust-radius')) * 73;
43
+ angle = uniform(node.id, 'dust-angle') * Math.PI * 2;
44
+ } else {
45
+ const fraction = (index - coreCount + 0.5) / Math.max(1, diskEnd - coreCount);
46
+ const offset = fraction * 73 + gaussian(node.id, 'radius') * 9;
47
+ const folded = ((offset % 146) + 146) % 146;
48
+ radius = 22 + (folded <= 73 ? folded : 146 - folded);
49
+ const arm = Math.floor(uniform(node.id, 'arm') * armCount);
50
+ const base = rotation + arm * Math.PI * 2 / armCount + (uniform(arm, 'arm-offset') - 0.5) * 0.35;
51
+ const twist = (radius - 18) / 95 * 2.2 * Math.PI;
52
+ const ripple = Math.sin(radius * 0.055 + uniform(arm, 'phase') * Math.PI * 2) * 0.10;
53
+ angle = base + twist + ripple + gaussian(node.id, 'angle') * 0.30;
54
+ }
55
+ const thickness = 8 / (1 + radius / 40);
56
+ const height = Math.max(-1, Math.min(1, gaussian(node.id, 'height') * 0.5)) * thickness;
57
+ positions.set(node.id, { x: Math.cos(angle) * radius, y: Math.sin(angle) * radius, z: height });
58
+ });
59
+ } else {
60
+ function brainPoint(id, axis, extent = 1) {
61
+ const horizontal = gaussian(id, axis + ':x');
62
+ const vertical = gaussian(id, axis + ':y');
63
+ const depth = gaussian(id, axis + ':z');
64
+ const scale = Math.cbrt(uniform(id, axis + ':distance')) * extent / Math.hypot(horizontal, vertical, depth);
65
+ return { x: 42 + horizontal * scale * 40, y: vertical * scale * 106, z: depth * scale * 52 };
66
+ }
67
+ const centers = [0, 1].map(side => {
68
+ const count = 12 + Math.floor(uniform('brain:' + side, 'clusters') * 3);
69
+ const candidates = Array.from({ length: 96 }, (_, index) => {
70
+ const id = 'brain:' + side + ':' + index;
71
+ const point = brainPoint(id, 'center', 0.82 + uniform(id, 'surface') * 0.12);
72
+ return { ...point, spread: 6 + uniform(id, 'spread') * 8, phase: uniform(id, 'phase') * Math.PI * 2 };
73
+ });
74
+ const interior = candidates.reduce((best, point, index) => Math.hypot(point.x - 42, point.y, point.z)
75
+ < Math.hypot(candidates[best].x - 42, candidates[best].y, candidates[best].z) ? index : best, 0);
76
+ const selected = [candidates.splice(interior, 1)[0]];
77
+ while (selected.length < count) {
78
+ let best = 0, greatest = -Infinity;
79
+ candidates.forEach((point, index) => {
80
+ const distance = Math.min(...selected.map(center => Math.hypot(point.x - center.x, point.y - center.y, (point.z - center.z) * 0.7)));
81
+ if (distance > greatest) { greatest = distance; best = index; }
82
+ });
83
+ selected.push(candidates.splice(best, 1)[0]);
84
+ }
85
+ return selected;
86
+ });
87
+ const roamers = new Set([...memories].sort((left, right) => uniform(left.id, 'roaming') - uniform(right.id, 'roaming')
88
+ || (String(left.id) < String(right.id) ? -1 : String(left.id) > String(right.id) ? 1 : 0))
89
+ .slice(0, Math.round(memories.length * 0.05)).map(node => node.id));
90
+ memories.forEach(node => {
91
+ const side = uniform(node.id, 'hemisphere') < 0.5 ? 0 : 1;
92
+ const target = brainPoint(node.id, 'target');
93
+ let center, nearest = Infinity;
94
+ centers[side].forEach((candidate, index) => {
95
+ const distance = Math.hypot(target.x - candidate.x, target.y - candidate.y, target.z - candidate.z)
96
+ * (0.8 + uniform(node.id, 'weight:' + index) * 0.4);
97
+ if (distance < nearest) { nearest = distance; center = candidate; }
98
+ });
99
+ const point = roamers.has(node.id) ? target : {
100
+ x: center.x + gaussian(node.id, 'scatter:x') * center.spread,
101
+ y: center.y + gaussian(node.id, 'scatter:y') * center.spread,
102
+ z: center.z + gaussian(node.id, 'scatter:z') * center.spread,
103
+ };
104
+ point.x += Math.sin(point.y * 0.08 + center.phase) * 6;
105
+ point.z += Math.sin(point.y * 0.09 + center.phase) * 4;
106
+ point.y += Math.sin(point.z * 0.12 + center.phase) * 3;
107
+ const envelope = 0.96 + 0.06 * Math.sin(point.y * 0.08) + 0.035 * Math.cos(point.z * 0.11 + side);
108
+ const distance = Math.hypot((point.x - 42) / 40, point.y / 106, point.z / 52);
109
+ if (distance > envelope) {
110
+ const scale = envelope * (0.93 + uniform(node.id, 'edge') * 0.06) / distance;
111
+ point.x = 42 + (point.x - 42) * scale; point.y *= scale; point.z *= scale;
112
+ }
113
+ positions.set(node.id, { x: (side ? 1 : -1) * (4 + Math.abs(point.x)) + (side ? 2 : 0),
114
+ y: point.y - (side ? 1 : 0), z: point.z });
31
115
  });
32
116
  }
33
- return nodes.map(node => ({ ...node, pos: node.isCore ? { x: 0, y: 0, z: 0 } : positions.get(node.id) }));
117
+ return nodes.map(node => ({ ...node, pos: node.isCore ? { x: 0, y: layout === 'brain' ? 14 : 0, z: 0 } : positions.get(node.id) }));
34
118
  }
35
119
 
36
120
  export function layoutMemories(nodes) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-memory-engine",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "description": "DSH 本地记忆插件:自动记忆、遗忘曲线、关键词关联、可检索可编辑,数据留在本机。",
5
5
  "keywords": [
6
6
  "dsh",