dsh-memory-engine 0.6.1 → 0.7.0-beta.2b

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.
@@ -0,0 +1,259 @@
1
+ export function createRenderer(canvas, data, options = {}) {
2
+ const document = canvas.ownerDocument;
3
+ const window = document.defaultView;
4
+ const performance = window.performance;
5
+ const ctx = canvas.getContext('2d', { alpha: false });
6
+ if (!ctx) throw new Error('Canvas 2D 不可用');
7
+ const colors = { '人格':[255,215,106], '印象':[143,216,255], '知识':[126,231,178], '日记':[190,160,255], '对话':[255,160,140], '情绪':[255,90,90] };
8
+ const fallback = [145,205,255];
9
+ let nodes = [], links = [], hubs = [];
10
+ const byId = new Map(), adjacency = new Map(), sprites = new Map();
11
+ const pulses = [], firing = new Map(), selections = new Set();
12
+ if (typeof options.onSelect === 'function') selections.add(options.onSelect);
13
+ let seed = 987654321;
14
+ const random = () => ((seed = seed * 16807 % 2147483647) - 1) / 2147483646;
15
+ let width=0,height=0,dpr=1,rotX=0.3,rotY=0.15,zoom=data.nodes.length<50?1.6:1,autoSpin=true;
16
+ let drag=null,nextFire=0,lastTime=0,rafId=null,running=false,destroyed=false,generation=0,observer=null;
17
+
18
+ function glowSprite(rgb) {
19
+ const off = document.createElement('canvas');
20
+ off.width = off.height = 96;
21
+ const brush = off.getContext('2d');
22
+ const gradient = brush.createRadialGradient(48,48,1,48,48,48);
23
+ gradient.addColorStop(0,'rgba('+rgb.join(',')+',0.7)');
24
+ gradient.addColorStop(0.4,'rgba('+rgb.join(',')+',0.19)');
25
+ gradient.addColorStop(1,'rgba('+rgb.join(',')+',0)');
26
+ brush.fillStyle = gradient; brush.fillRect(0,0,96,96);
27
+ return off;
28
+ }
29
+
30
+ function setData(nextData) {
31
+ if (destroyed) return;
32
+ nodes = nextData.nodes.map(node => ({ ...node, pos: { ...node.pos } }));
33
+ links = nextData.links.map(link => [...link]);
34
+ byId.clear(); adjacency.clear(); sprites.clear(); pulses.length = 0; firing.clear();
35
+ seed = 987654321; nextFire = 0;
36
+ for (const node of nextData.nodes) byId.set(node.id, node);
37
+ for (const node of nodes) adjacency.set(node.id, []);
38
+ for (const [left,right] of links) {
39
+ if (adjacency.has(left) && adjacency.has(right)) { adjacency.get(left).push(right); adjacency.get(right).push(left); }
40
+ }
41
+ hubs = nodes.filter(star => star.degree > 0).sort((left,right) => right.degree-left.degree).slice(0,6);
42
+ for (const star of nodes) {
43
+ star._rgb = colors[star.kind] || fallback;
44
+ const count = Math.min(9, 4 + Math.round(Math.sqrt(star.degree) * 1.2));
45
+ star._dendrites = Array.from({length:count},(_,index) => ({
46
+ angle:index/count*Math.PI*2+random()*0.7,
47
+ length:0.9+random()*1.5, bend:(random()-0.5)*0.9, width:0.5+random()*0.7,
48
+ fork:random()<0.55 ? (random()-0.5)*1.5 : null
49
+ }));
50
+ }
51
+ for (const star of nodes) {
52
+ const key = star._rgb.join(',');
53
+ if (!sprites.has(key)) sprites.set(key,glowSprite(star._rgb));
54
+ star._glow = sprites.get(key);
55
+ }
56
+
57
+ }
58
+
59
+ function resize() {
60
+ if (destroyed) return;
61
+ const bounds = canvas.getBoundingClientRect();
62
+ width=bounds.width; height=bounds.height; dpr=Math.min(1.5,window.devicePixelRatio||1);
63
+ canvas.width=Math.round(width*dpr); canvas.height=Math.round(height*dpr);
64
+ ctx.setTransform(dpr,0,0,dpr,0,0);
65
+ }
66
+
67
+ function project(pos) {
68
+ const cy=Math.cos(rotY),sy=Math.sin(rotY),cx=Math.cos(rotX),sx=Math.sin(rotX);
69
+ const x=pos.x*cy-pos.z*sy,z=pos.x*sy+pos.z*cy;
70
+ const y=pos.y*cx-z*sx,depth=pos.y*sx+z*cx+250;
71
+ if (depth<=12) return null;
72
+ const scale=760/depth*zoom;
73
+ return {x:width/2+x*scale,y:height/2+y*scale,scale,depth};
74
+ }
75
+ function fire(id,now) {
76
+ firing.set(id,1);
77
+ for (const other of adjacency.get(id)||[]) {
78
+ firing.set(other,Math.max(0.65,firing.get(other)||0));
79
+ pulses.push({from:id,to:other,born:now,life:900+Math.random()*700});
80
+ }
81
+ }
82
+
83
+ function select(x,y) {
84
+ let best=null,bestScore=Infinity;
85
+ for (const star of nodes) {
86
+ if (!star._screen) continue;
87
+ const value=Math.hypot(star._screen.x-x,star._screen.y-y);
88
+ const tolerance=Math.max(14,star._screen.r*1.15);
89
+ if (value>tolerance) continue;
90
+ const score=value-star._screen.r*0.22;
91
+ if (score<bestScore) {bestScore=score;best=star;}
92
+ }
93
+ if (!best) return;
94
+ fire(best.id,performance.now());
95
+ for (const callback of selections) callback(byId.get(best.id));
96
+ }
97
+
98
+ function onPointerDown(event) {
99
+ autoSpin=false; drag={x:event.clientX,y:event.clientY,moved:0,pointerId:event.pointerId};
100
+ canvas.setPointerCapture(event.pointerId);
101
+ }
102
+ function onPointerMove(event) {
103
+ if (!drag) return;
104
+ const dx=event.clientX-drag.x,dy=event.clientY-drag.y;
105
+ drag.x=event.clientX; drag.y=event.clientY; drag.moved+=Math.abs(dx)+Math.abs(dy);
106
+ rotY+=dx*0.006; rotX=Math.max(-1.5,Math.min(1.5,rotX+dy*0.006));
107
+ }
108
+ function releaseDrag() {
109
+ if (drag && canvas.hasPointerCapture(drag.pointerId)) canvas.releasePointerCapture(drag.pointerId);
110
+ drag=null;
111
+ }
112
+ function onPointerUp(event) {
113
+ const clicked = drag && drag.moved<6;
114
+ releaseDrag();
115
+ if (clicked) {
116
+ const bounds = canvas.getBoundingClientRect();
117
+ select(event.clientX-bounds.left,event.clientY-bounds.top);
118
+ }
119
+ }
120
+ function onWheel(event) {
121
+ event.preventDefault();
122
+ zoom=Math.max(0.3,Math.min(4.5,zoom*(event.deltaY>0?0.92:1.08)));
123
+ }
124
+
125
+ function frame(now) {
126
+ const delta=Math.min(50,now-lastTime||16.7); lastTime=now;
127
+ if (autoSpin) rotY+=0.0011*delta/16.7;
128
+ if (now>nextFire) { nextFire=now+1000+Math.random()*1500; if (hubs.length) fire(hubs[Math.floor(Math.random()*hubs.length)].id,now); }
129
+ ctx.globalCompositeOperation='source-over'; ctx.fillStyle='#03060c'; ctx.fillRect(0,0,width,height);
130
+ ctx.strokeStyle='rgba(40,90,140,0.055)';ctx.lineWidth=1;
131
+ for (let index=0;index<20;index++) {
132
+ const y=index/20*height;
133
+ ctx.beginPath();ctx.moveTo(0,y);ctx.bezierCurveTo(width*.3,y+Math.sin(now*.0003+index)*30,width*.7,y-Math.cos(now*.00025+index)*30,width,y);ctx.stroke();
134
+ }
135
+ const points=[];
136
+ for (const star of nodes) {
137
+ const p=project(star.pos);
138
+ if (!p || p.x< -100 || p.x>width+100 || p.y< -100 || p.y>height+100) {star._screen=null;continue;}
139
+ points.push({star,p});
140
+ }
141
+ points.sort((left,right)=>right.p.depth-left.p.depth);
142
+ const screen=new Map(points.map(point=>[point.star.id,point]));
143
+ ctx.globalCompositeOperation='lighter';
144
+ for (const [left,right] of links) {
145
+ const first=screen.get(left),second=screen.get(right);
146
+ if (!first||!second) continue;
147
+ const act=Math.max(firing.get(left)||0,firing.get(right)||0);
148
+ const alpha=Math.min(.85,.1*Math.min(first.star.retention,second.star.retention)+.35*act);
149
+ if (alpha<.01) continue;
150
+ const mx=(first.p.x+second.p.x)/2+(first.p.y-second.p.y)*.18;
151
+ const my=(first.p.y+second.p.y)/2+(second.p.x-first.p.x)*.18;
152
+ ctx.strokeStyle='rgba(110,190,255,'+alpha+')';ctx.lineWidth=.7+act*1.6;
153
+ ctx.beginPath();ctx.moveTo(first.p.x,first.p.y);ctx.quadraticCurveTo(mx,my,second.p.x,second.p.y);ctx.stroke();
154
+ }
155
+ for (let index=pulses.length-1;index>=0;index--) {
156
+ const pulse=pulses[index],fraction=(now-pulse.born)/pulse.life;
157
+ if (fraction>=1) {pulses.splice(index,1);continue;}
158
+ const first=screen.get(pulse.from),second=screen.get(pulse.to);
159
+ if (!first||!second) continue;
160
+ const mx=(first.p.x+second.p.x)/2+(first.p.y-second.p.y)*.18,my=(first.p.y+second.p.y)/2+(second.p.x-first.p.x)*.18;
161
+ const inverse=1-fraction;
162
+ const x=inverse*inverse*first.p.x+2*inverse*fraction*mx+fraction*fraction*second.p.x;
163
+ const y=inverse*inverse*first.p.y+2*inverse*fraction*my+fraction*fraction*second.p.y;
164
+ ctx.fillStyle='rgba(205,245,255,'+(.9*inverse)+')';ctx.beginPath();ctx.arc(x,y,2.5+2*inverse,0,Math.PI*2);ctx.fill();
165
+ }
166
+ for (const {star,p} of points) {
167
+ const act=firing.get(star.id)||0,ret=Math.max(.05,Math.min(1.2,star.retention));
168
+ const intensity=Math.min(1,.22+ret*.72)*(1+act*1.1);
169
+ const radius=Math.max(2,star.radius*p.scale*.55)*(1+Math.sin(now*.0016+(star.pos.x+star.pos.z)*.3)*.06)*(1+act*.5);
170
+ const rgb=star._rgb.join(',');
171
+ ctx.strokeStyle='rgba('+rgb+','+Math.min(1,.3*intensity)+')';
172
+ if (radius>2.5) for (const branch of star._dendrites) {
173
+ const length=radius*2.2*branch.length,dx=Math.cos(branch.angle),dy=Math.sin(branch.angle);
174
+ const ex=p.x+dx*length,ey=p.y+dy*length;
175
+ const cx=p.x+dx*length*.55-dy*length*branch.bend*.5;
176
+ const cy=p.y+dy*length*.55+dx*length*branch.bend*.5;
177
+ ctx.lineWidth=branch.width;ctx.beginPath();ctx.moveTo(p.x,p.y);ctx.quadraticCurveTo(cx,cy,ex,ey);ctx.stroke();
178
+ if (branch.fork!==null) {
179
+ const angle=branch.angle+branch.fork;
180
+ ctx.lineWidth=branch.width*.65;ctx.beginPath();ctx.moveTo(cx,cy);ctx.quadraticCurveTo(cx+Math.cos(angle)*length*.2,cy+Math.sin(angle)*length*.2,cx+Math.cos(angle)*length*.34,cy+Math.sin(angle)*length*.34);ctx.stroke();
181
+ }
182
+ }
183
+ const glowRadius=radius*3.6;
184
+ ctx.globalAlpha=Math.min(1,.4+intensity*.7);ctx.drawImage(star._glow,p.x-glowRadius,p.y-glowRadius,glowRadius*2,glowRadius*2);ctx.globalAlpha=1;
185
+ ctx.fillStyle='rgba('+rgb+','+Math.min(1,.42*intensity)+')';ctx.beginPath();ctx.arc(p.x,p.y,radius,0,Math.PI*2);ctx.fill();
186
+ ctx.strokeStyle='rgba('+rgb+','+Math.min(1,.75*intensity)+')';ctx.lineWidth=1;ctx.stroke();
187
+ ctx.fillStyle='rgba(255,255,255,'+Math.min(1,.6*intensity)+')';ctx.beginPath();ctx.arc(p.x,p.y,radius*.42,0,Math.PI*2);ctx.fill();
188
+ if (star.retention<.25) {
189
+ ctx.strokeStyle='rgba(255,110,110,'+Math.max(0,.45-star.retention)+')';ctx.setLineDash([3,4]);
190
+ ctx.beginPath();ctx.arc(p.x,p.y,radius*3.2,0,Math.PI*2);ctx.stroke();ctx.setLineDash([]);
191
+ }
192
+ star._screen={x:p.x,y:p.y,r:Math.max(8,radius*2.6)};
193
+ }
194
+ for (const [id,value] of firing) {
195
+ const next=value*Math.pow(.965,delta/16.7);
196
+ if (next<.02) firing.delete(id); else firing.set(id,next);
197
+ }
198
+ ctx.globalCompositeOperation='source-over';
199
+ const vignette=ctx.createRadialGradient(width/2,height/2,Math.min(width,height)*.34,width/2,height/2,Math.max(width,height)*.8);
200
+ vignette.addColorStop(0,'rgba(0,0,0,0)');vignette.addColorStop(1,'rgba(0,0,0,.78)');
201
+ ctx.fillStyle=vignette;ctx.fillRect(0,0,width,height);
202
+ }
203
+ function start() {
204
+ if (running || destroyed) return;
205
+ running=true; lastTime=0; nextFire=0;
206
+ const currentGeneration = ++generation;
207
+ canvas.addEventListener('pointerdown',onPointerDown);
208
+ canvas.addEventListener('pointermove',onPointerMove);
209
+ canvas.addEventListener('pointerup',onPointerUp);
210
+ canvas.addEventListener('pointercancel',releaseDrag);
211
+ canvas.addEventListener('wheel',onWheel,{passive:false});
212
+ window.addEventListener('resize',resize);
213
+ if (window.ResizeObserver) {
214
+ observer = new window.ResizeObserver(resize);
215
+ observer.observe(canvas);
216
+ }
217
+ resize();
218
+ function tick(now) {
219
+ if (!running || destroyed || currentGeneration !== generation) return;
220
+ rafId=null;
221
+ frame(now);
222
+ if (running && !destroyed && currentGeneration === generation) rafId=window.requestAnimationFrame(tick);
223
+ }
224
+ rafId=window.requestAnimationFrame(tick);
225
+ }
226
+
227
+ function stop() {
228
+ running=false; generation++;
229
+ if (rafId !== null) window.cancelAnimationFrame(rafId);
230
+ rafId=null;
231
+ observer?.disconnect(); observer=null;
232
+ canvas.removeEventListener('pointerdown',onPointerDown);
233
+ canvas.removeEventListener('pointermove',onPointerMove);
234
+ canvas.removeEventListener('pointerup',onPointerUp);
235
+ canvas.removeEventListener('pointercancel',releaseDrag);
236
+ canvas.removeEventListener('wheel',onWheel);
237
+ window.removeEventListener('resize',resize);
238
+ releaseDrag();
239
+ pulses.length=0; firing.clear(); lastTime=0; nextFire=0;
240
+ }
241
+
242
+ function destroy() {
243
+ if (destroyed) return;
244
+ stop(); destroyed=true;
245
+ selections.clear(); byId.clear(); adjacency.clear(); sprites.clear();
246
+ nodes=[]; links=[]; hubs=[];
247
+ }
248
+
249
+ setData(data);
250
+ return {
251
+ start, stop, resize, setData, destroy,
252
+ fireNeuron(id) { if (!destroyed && byId.has(id)) fire(id,performance.now()); },
253
+ onSelect(callback) {
254
+ if (!destroyed) selections.add(callback);
255
+ return () => selections.delete(callback);
256
+ },
257
+ };
258
+ }
259
+
@@ -1,3 +1,8 @@
1
+ import fs from 'node:fs';
2
+ import { stripRendererExport } from '../../scripts/lib/strip-export.mjs';
3
+
4
+ const rendererSource = stripRendererExport(fs.readFileSync(new URL('./canvas.js', import.meta.url), 'utf8'));
5
+
1
6
  function escapeHtml(value) {
2
7
  return String(value).replace(/[&<>"']/g, (character) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[character]);
3
8
  }
@@ -14,7 +19,7 @@ export function renderHtml(data, title = '记忆神经网络') {
14
19
  <style>
15
20
  :root{color-scheme:dark}*{box-sizing:border-box}html,body{margin:0;width:100%;height:100%}
16
21
  body{overflow:hidden;background:#03060c;color:#d8e9f8;font:13px/1.65 system-ui,"Microsoft YaHei",sans-serif}
17
- canvas{display:block;touch-action:none;cursor:grab}canvas:active{cursor:grabbing}
22
+ canvas{display:block;width:100%;height:100%;touch-action:none;cursor:grab}canvas:active{cursor:grabbing}
18
23
  .hud{position:fixed;top:16px;left:18px;pointer-events:none;text-shadow:0 0 12px #3388bb88}
19
24
  .hud strong{color:#a4e6ff;font-size:16px}.sub{color:#7491a8;font-size:12px}
20
25
  .panel,.review{position:fixed;background:#07111fee;border:1px solid #3976a055;border-radius:14px;box-shadow:0 0 40px #1763a033;padding:15px;backdrop-filter:blur(8px)}
@@ -37,64 +42,8 @@ export function renderHtml(data, title = '记忆神经网络') {
37
42
  <script>
38
43
  const DATA = ${embedded};
39
44
  const canvas = document.getElementById('map');
40
- const ctx = canvas.getContext('2d', { alpha: false });
41
- const colors = { '人格':[255,215,106], '印象':[143,216,255], '知识':[126,231,178], '日记':[190,160,255], '对话':[255,160,140], '情绪':[255,90,90] };
42
- const fallback = [145,205,255];
43
- const byId = new Map(DATA.stars.map(star => [star.id, star]));
44
- const adjacency = new Map(DATA.stars.map(star => [star.id, []]));
45
- for (const [left,right] of DATA.links) {
46
- if (adjacency.has(left) && adjacency.has(right)) { adjacency.get(left).push(right); adjacency.get(right).push(left); }
47
- }
48
- const hubs = DATA.stars.filter(star => star.degree > 0).sort((left,right) => right.degree-left.degree).slice(0,6);
49
- const pulses = [];
50
- const firing = new Map();
51
- let seed = 987654321;
52
- const random = () => ((seed = seed * 16807 % 2147483647) - 1) / 2147483646;
53
- for (const star of DATA.stars) {
54
- star._rgb = colors[star.kind] || fallback;
55
- const count = Math.min(9, 4 + Math.round(Math.sqrt(star.degree) * 1.2));
56
- star._dendrites = Array.from({length:count},(_,index) => ({
57
- angle:index/count*Math.PI*2+random()*0.7,
58
- length:0.9+random()*1.5, bend:(random()-0.5)*0.9, width:0.5+random()*0.7,
59
- fork:random()<0.55 ? (random()-0.5)*1.5 : null
60
- }));
61
- }
62
- function glowSprite(rgb) {
63
- const off = document.createElement('canvas');
64
- off.width = off.height = 96;
65
- const brush = off.getContext('2d');
66
- const gradient = brush.createRadialGradient(48,48,1,48,48,48);
67
- gradient.addColorStop(0,'rgba('+rgb.join(',')+',0.7)');
68
- gradient.addColorStop(0.4,'rgba('+rgb.join(',')+',0.19)');
69
- gradient.addColorStop(1,'rgba('+rgb.join(',')+',0)');
70
- brush.fillStyle = gradient; brush.fillRect(0,0,96,96);
71
- return off;
72
- }
73
- const sprites = new Map();
74
- for (const star of DATA.stars) {
75
- const key = star._rgb.join(',');
76
- if (!sprites.has(key)) sprites.set(key,glowSprite(star._rgb));
77
- star._glow = sprites.get(key);
78
- }
79
- let width=0,height=0,dpr=1,rotX=0.3,rotY=0.15,zoom=DATA.stars.length<50?1.6:1,autoSpin=true;
80
- function resize() {
81
- width=innerWidth; height=innerHeight; dpr=Math.min(1.5,devicePixelRatio||1);
82
- canvas.width=Math.round(width*dpr); canvas.height=Math.round(height*dpr);
83
- canvas.style.width=width+'px'; canvas.style.height=height+'px';
84
- ctx.setTransform(dpr,0,0,dpr,0,0);
85
- }
86
- addEventListener('resize',resize); resize();
87
- let drag=null;
88
- canvas.addEventListener('pointerdown',event => { autoSpin=false; drag={x:event.clientX,y:event.clientY,moved:0}; canvas.setPointerCapture(event.pointerId); });
89
- canvas.addEventListener('pointermove',event => {
90
- if (!drag) return;
91
- const dx=event.clientX-drag.x,dy=event.clientY-drag.y;
92
- drag.x=event.clientX; drag.y=event.clientY; drag.moved+=Math.abs(dx)+Math.abs(dy);
93
- rotY+=dx*0.006; rotX=Math.max(-1.5,Math.min(1.5,rotX+dy*0.006));
94
- });
95
- canvas.addEventListener('pointerup',event => { if (drag && drag.moved<6) select(event.clientX,event.clientY); drag=null; });
96
- canvas.addEventListener('pointercancel',() => { drag=null; });
97
- canvas.addEventListener('wheel',event => { event.preventDefault(); zoom=Math.max(0.3,Math.min(4.5,zoom*(event.deltaY>0?0.92:1.08))); },{passive:false});
45
+ ${rendererSource}
46
+ const renderer = createRenderer(canvas, { ...DATA, nodes: DATA.stars });
98
47
  document.getElementById('close').onclick=() => { document.getElementById('panel').style.display='none'; };
99
48
  document.getElementById('total').textContent=DATA.stats.total;
100
49
  document.getElementById('linkCount').textContent=DATA.links.length;
@@ -105,123 +54,14 @@ for (const [index,item] of DATA.review.entries()) {
105
54
  const fill=document.createElement('i'); fill.style.width=Math.min(100,Math.max(0,item.retention*100))+'%'; bar.append(fill);
106
55
  row.append(name,bar); document.getElementById('reviewList').append(row);
107
56
  }
108
- function project(pos) {
109
- const cy=Math.cos(rotY),sy=Math.sin(rotY),cx=Math.cos(rotX),sx=Math.sin(rotX);
110
- const x=pos.x*cy-pos.z*sy,z=pos.x*sy+pos.z*cy;
111
- const y=pos.y*cx-z*sx,depth=pos.y*sx+z*cx+250;
112
- if (depth<=12) return null;
113
- const scale=760/depth*zoom;
114
- return {x:width/2+x*scale,y:height/2+y*scale,scale,depth};
115
- }
116
- function fire(id,now) {
117
- firing.set(id,1);
118
- for (const other of adjacency.get(id)||[]) {
119
- firing.set(other,Math.max(0.65,firing.get(other)||0));
120
- pulses.push({from:id,to:other,born:now,life:900+Math.random()*700});
121
- }
122
- }
123
- function select(x,y) {
124
- // 命中判定:先按「离光标最近的节点」选,而不是「第一个框住光标的节点」。
125
- // 原来要求 值<该节点半径,小节点容易被邻居抢走 → 点错。
126
- // 现在给每个节点一个最小容差,再用距离减去一点半径作为打分(同样近时偏向大的那个)。
127
- let best=null,bestScore=Infinity;
128
- for (const star of DATA.stars) {
129
- if (!star._screen) continue;
130
- const value=Math.hypot(star._screen.x-x,star._screen.y-y);
131
- const tolerance=Math.max(14,star._screen.r*1.15);
132
- if (value>tolerance) continue;
133
- const score=value-star._screen.r*0.22;
134
- if (score<bestScore) {bestScore=score;best=star;}
135
- }
136
- if (!best) return;
137
- fire(best.id,performance.now());
57
+ renderer.onSelect(node => {
138
58
  document.getElementById('panel').style.display='block';
139
- document.getElementById('kind').textContent=best.kind;
140
- document.getElementById('title').textContent=best.title;
141
- document.getElementById('meta').textContent='重要度 '+best.heat+' · 留存 '+best.retention.toFixed(2)+' · 连接数 '+best.degree+(best.emotion.length?' · '+best.emotion.map(item=>item.tag).join('/'):'');
142
- document.getElementById('body').textContent=best.text;
143
- }
144
- let nextFire=0,lastTime=0;
145
- function frame(now) {
146
- const delta=Math.min(50,now-lastTime||16.7); lastTime=now;
147
- if (autoSpin) rotY+=0.0011*delta/16.7;
148
- if (now>nextFire) { nextFire=now+1000+Math.random()*1500; if (hubs.length) fire(hubs[Math.floor(Math.random()*hubs.length)].id,now); }
149
- ctx.globalCompositeOperation='source-over'; ctx.fillStyle='#03060c'; ctx.fillRect(0,0,width,height);
150
- ctx.strokeStyle='rgba(40,90,140,0.055)';ctx.lineWidth=1;
151
- for (let index=0;index<20;index++) {
152
- const y=index/20*height;
153
- ctx.beginPath();ctx.moveTo(0,y);ctx.bezierCurveTo(width*.3,y+Math.sin(now*.0003+index)*30,width*.7,y-Math.cos(now*.00025+index)*30,width,y);ctx.stroke();
154
- }
155
- const points=[];
156
- for (const star of DATA.stars) {
157
- const p=project(star.pos);
158
- if (!p || p.x< -100 || p.x>width+100 || p.y< -100 || p.y>height+100) {star._screen=null;continue;}
159
- points.push({star,p});
160
- }
161
- points.sort((left,right)=>right.p.depth-left.p.depth);
162
- const screen=new Map(points.map(point=>[point.star.id,point]));
163
- ctx.globalCompositeOperation='lighter';
164
- for (const [left,right] of DATA.links) {
165
- const first=screen.get(left),second=screen.get(right);
166
- if (!first||!second) continue;
167
- const act=Math.max(firing.get(left)||0,firing.get(right)||0);
168
- const alpha=Math.min(.85,.1*Math.min(first.star.retention,second.star.retention)+.35*act);
169
- if (alpha<.01) continue;
170
- const mx=(first.p.x+second.p.x)/2+(first.p.y-second.p.y)*.18;
171
- const my=(first.p.y+second.p.y)/2+(second.p.x-first.p.x)*.18;
172
- ctx.strokeStyle='rgba(110,190,255,'+alpha+')';ctx.lineWidth=.7+act*1.6;
173
- ctx.beginPath();ctx.moveTo(first.p.x,first.p.y);ctx.quadraticCurveTo(mx,my,second.p.x,second.p.y);ctx.stroke();
174
- }
175
- for (let index=pulses.length-1;index>=0;index--) {
176
- const pulse=pulses[index],fraction=(now-pulse.born)/pulse.life;
177
- if (fraction>=1) {pulses.splice(index,1);continue;}
178
- const first=screen.get(pulse.from),second=screen.get(pulse.to);
179
- if (!first||!second) continue;
180
- const mx=(first.p.x+second.p.x)/2+(first.p.y-second.p.y)*.18,my=(first.p.y+second.p.y)/2+(second.p.x-first.p.x)*.18;
181
- const inverse=1-fraction;
182
- const x=inverse*inverse*first.p.x+2*inverse*fraction*mx+fraction*fraction*second.p.x;
183
- const y=inverse*inverse*first.p.y+2*inverse*fraction*my+fraction*fraction*second.p.y;
184
- ctx.fillStyle='rgba(205,245,255,'+(.9*inverse)+')';ctx.beginPath();ctx.arc(x,y,2.5+2*inverse,0,Math.PI*2);ctx.fill();
185
- }
186
- for (const {star,p} of points) {
187
- const act=firing.get(star.id)||0,ret=Math.max(.05,Math.min(1.2,star.retention));
188
- const intensity=Math.min(1,.22+ret*.72)*(1+act*1.1);
189
- const radius=Math.max(2,star.radius*p.scale*.55)*(1+Math.sin(now*.0016+(star.pos.x+star.pos.z)*.3)*.06)*(1+act*.5);
190
- const rgb=star._rgb.join(',');
191
- ctx.strokeStyle='rgba('+rgb+','+Math.min(1,.3*intensity)+')';
192
- if (radius>2.5) for (const branch of star._dendrites) {
193
- const length=radius*2.2*branch.length,dx=Math.cos(branch.angle),dy=Math.sin(branch.angle);
194
- const ex=p.x+dx*length,ey=p.y+dy*length;
195
- const cx=p.x+dx*length*.55-dy*length*branch.bend*.5;
196
- const cy=p.y+dy*length*.55+dx*length*branch.bend*.5;
197
- ctx.lineWidth=branch.width;ctx.beginPath();ctx.moveTo(p.x,p.y);ctx.quadraticCurveTo(cx,cy,ex,ey);ctx.stroke();
198
- if (branch.fork!==null) {
199
- const angle=branch.angle+branch.fork;
200
- ctx.lineWidth=branch.width*.65;ctx.beginPath();ctx.moveTo(cx,cy);ctx.quadraticCurveTo(cx+Math.cos(angle)*length*.2,cy+Math.sin(angle)*length*.2,cx+Math.cos(angle)*length*.34,cy+Math.sin(angle)*length*.34);ctx.stroke();
201
- }
202
- }
203
- const glowRadius=radius*3.6;
204
- ctx.globalAlpha=Math.min(1,.4+intensity*.7);ctx.drawImage(star._glow,p.x-glowRadius,p.y-glowRadius,glowRadius*2,glowRadius*2);ctx.globalAlpha=1;
205
- ctx.fillStyle='rgba('+rgb+','+Math.min(1,.42*intensity)+')';ctx.beginPath();ctx.arc(p.x,p.y,radius,0,Math.PI*2);ctx.fill();
206
- ctx.strokeStyle='rgba('+rgb+','+Math.min(1,.75*intensity)+')';ctx.lineWidth=1;ctx.stroke();
207
- ctx.fillStyle='rgba(255,255,255,'+Math.min(1,.6*intensity)+')';ctx.beginPath();ctx.arc(p.x,p.y,radius*.42,0,Math.PI*2);ctx.fill();
208
- if (star.retention<.25) {
209
- ctx.strokeStyle='rgba(255,110,110,'+Math.max(0,.45-star.retention)+')';ctx.setLineDash([3,4]);
210
- ctx.beginPath();ctx.arc(p.x,p.y,radius*3.2,0,Math.PI*2);ctx.stroke();ctx.setLineDash([]);
211
- }
212
- star._screen={x:p.x,y:p.y,r:Math.max(8,radius*2.6)};
213
- }
214
- for (const [id,value] of firing) {
215
- const next=value*Math.pow(.965,delta/16.7);
216
- if (next<.02) firing.delete(id); else firing.set(id,next);
217
- }
218
- ctx.globalCompositeOperation='source-over';
219
- const vignette=ctx.createRadialGradient(width/2,height/2,Math.min(width,height)*.34,width/2,height/2,Math.max(width,height)*.8);
220
- vignette.addColorStop(0,'rgba(0,0,0,0)');vignette.addColorStop(1,'rgba(0,0,0,.78)');
221
- ctx.fillStyle=vignette;ctx.fillRect(0,0,width,height);
222
- requestAnimationFrame(frame);
223
- }
224
- requestAnimationFrame(frame);
59
+ document.getElementById('kind').textContent=node.kind;
60
+ document.getElementById('title').textContent=node.title;
61
+ document.getElementById('meta').textContent='重要度 '+node.heat+' · 留存 '+node.retention.toFixed(2)+' · 连接数 '+node.degree+(node.emotion.length?' · '+node.emotion.map(item=>item.tag).join('/'):'');
62
+ document.getElementById('body').textContent=node.text;
63
+ });
64
+ renderer.start();
225
65
  </script>
226
66
  </body>
227
67
  </html>`;
package/lib/routes.js ADDED
@@ -0,0 +1,56 @@
1
+ import path from 'node:path';
2
+ import { computeMapData } from './map.js';
3
+ import { renderHtml } from './render/render.mjs';
4
+ import { ONBOARD_PROMPT, ONBOARD_STEPS } from './onboard.js';
5
+ import { readUiConfig } from './config.js';
6
+
7
+ const base = '/memory-engine';
8
+ const paths = new Set([base, `${base}/`, `${base}/api/map`, `${base}/api/stats`, `${base}/api/onboard`, `${base}/api/config`]);
9
+ const allowedMethods = new Set(['GET', 'POST', 'PUT', 'DELETE']);
10
+ const escapeBridge = '<script>window.addEventListener("keydown",function(event){if(event.key==="Escape")parent.postMessage({type:"dsh-memory-engine-close"},"*")});</script>';
11
+
12
+ export function createMemoryHandler({ store, warn = () => {} }) {
13
+ return async (request, response) => {
14
+ const reply = (status, payload, html = false) => {
15
+ response.writeHead(status, {
16
+ 'Content-Type': html ? 'text/html; charset=utf-8' : 'application/json; charset=utf-8',
17
+ 'Cache-Control': 'no-store',
18
+ 'X-Content-Type-Options': 'nosniff',
19
+ 'Referrer-Policy': 'no-referrer',
20
+ ...(status === 405 ? { Allow: 'GET' } : {}),
21
+ ...(html ? { 'Content-Security-Policy': "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data:; base-uri 'none'; form-action 'none'; frame-ancestors 'self'" } : {}),
22
+ });
23
+ response.end(html ? payload : JSON.stringify(payload));
24
+ };
25
+ const fail = (status, error) => reply(status, { ok: false, error });
26
+ try {
27
+ const host = String(request.headers.host ?? '');
28
+ if (!/^(?:127\.0\.0\.1|localhost)(?::\d+)?$/.test(host)) return fail(403, '只允许本机访问记忆图谱');
29
+ const rawPath = (request.url ?? '/').split('?')[0];
30
+ let pathname;
31
+ try { pathname = decodeURIComponent(rawPath); }
32
+ catch { return fail(404, '找不到这个记忆接口'); }
33
+ if (pathname.includes('..') || pathname.includes('\\') || !paths.has(pathname)) return fail(404, '找不到这个记忆接口');
34
+ if (!allowedMethods.has(request.method) || request.method !== 'GET') return fail(405, '此接口仅支持 GET');
35
+ if (Number(request.headers['content-length']) > 1024 * 1024) return fail(413, '请求体不能超过 1MB');
36
+ if (request.headers['transfer-encoding'] || Number(request.headers['content-length']) > 0) return fail(400, '此只读接口不接受请求体');
37
+ if (pathname === `${base}/api/map`) return reply(200, { ok: true, data: computeMapData(store) });
38
+ if (pathname === `${base}/api/stats`) return reply(200, { ok: true, data: store.stats() });
39
+ if (pathname === `${base}/api/config`) return reply(200, { ok: true, data: readUiConfig(path.dirname(path.dirname(store.file))) });
40
+ if (pathname === `${base}/api/onboard`) return reply(200, { ok: true, data: {
41
+ empty: store.read().length === 0, prompt: ONBOARD_PROMPT, steps: ONBOARD_STEPS, dataDir: path.dirname(store.file),
42
+ } });
43
+ const { nodes, ...data } = computeMapData(store);
44
+ const html = renderHtml({ ...data, stars: nodes }, '我的记忆神经网络');
45
+ return reply(200, html.replace('</body>', escapeBridge + '</body>'), true);
46
+ } catch (error) {
47
+ warn(`图谱请求失败:${error.message}`);
48
+ return fail(500, '读取记忆失败,请检查本地插件日志');
49
+ }
50
+ };
51
+ }
52
+
53
+ export function registerMemoryRoutes(webServer, options) {
54
+ if (typeof webServer?.register !== 'function') throw new Error('宿主 webServer 服务不可用');
55
+ return webServer.register({ kind: 'prefix', path: base, handler: createMemoryHandler(options) });
56
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-memory-engine",
3
- "version": "0.6.1",
3
+ "version": "0.7.0-beta.2b",
4
4
  "description": "Local DSH memory engine with neuron network visualization and private file import",
5
5
  "keywords": [
6
6
  "dsh",
@@ -55,6 +55,7 @@
55
55
  }
56
56
  },
57
57
  "scripts": {
58
+ "build": "node scripts/build-client.mjs",
58
59
  "test": "node --test test/*.test.mjs"
59
60
  }
60
61
  }
@@ -0,0 +1,22 @@
1
+ import fs from 'node:fs';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { stripRendererExport } from './lib/strip-export.mjs';
4
+
5
+ const template = fs.readFileSync(new URL('../lib/client.src.js', import.meta.url), 'utf8');
6
+ const renderer = fs.readFileSync(new URL('../lib/render/canvas.js', import.meta.url), 'utf8');
7
+ const marker = '/* __CANVAS_RENDERER__ */';
8
+ if (template.split(marker).length !== 2) throw new Error('Expected exactly one renderer marker');
9
+ const bundled = '// 由 scripts/build-client.mjs 生成,别直接改这个文件\n'
10
+ + template.replace(marker, () => stripRendererExport(renderer));
11
+ const target = new URL('../lib/client.js', import.meta.url);
12
+ if (process.argv.includes('--check')) {
13
+ if (fs.readFileSync(target, 'utf8') !== bundled) throw new Error('lib/client.js 已过期,请跑 pnpm build');
14
+ } else {
15
+ const temporary = new URL(`../lib/.client-${randomUUID()}.tmp`, import.meta.url);
16
+ try {
17
+ fs.writeFileSync(temporary, bundled, { encoding: 'utf8', flag: 'wx' });
18
+ fs.renameSync(temporary, target);
19
+ } finally {
20
+ fs.rmSync(temporary, { force: true });
21
+ }
22
+ }
@@ -0,0 +1,7 @@
1
+ export function stripRendererExport(source) {
2
+ const declaration = /^export (?=function createRenderer\()/m;
3
+ if (!declaration.test(source)) throw new Error('Missing createRenderer export');
4
+ const stripped = source.replace(declaration, '');
5
+ if (/^\s*(?:import|export)\b/m.test(stripped)) throw new Error('Renderer must be a standalone browser function');
6
+ return stripped;
7
+ }