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 +20 -6
- package/lib/client.js +121 -29
- package/lib/client.src.js +10 -8
- package/lib/client.src.js.bak-before-closefix +878 -0
- package/lib/config-write.js +5 -1
- package/lib/config.js +3 -1
- package/lib/render/canvas.js +10 -4
- package/lib/render/layout.mjs +101 -17
- package/package.json +1 -1
package/lib/config-write.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
package/lib/render/canvas.js
CHANGED
|
@@ -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 (
|
|
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 (
|
|
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;
|
package/lib/render/layout.mjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
const SEED = 20261030;
|
|
2
2
|
|
|
3
3
|
export function layoutPositions(nodes, layout = 'sphere') {
|
|
4
|
-
if (
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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) {
|