dsh-memory-engine 0.5.6 → 0.6.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 CHANGED
@@ -4,6 +4,22 @@ DSH 插件的本地记忆引擎,带神经网络可视化。它让 agent 保存
4
4
 
5
5
  当前版本:0.5.2。Node ≥22,零新增依赖;DSH 宿主提供 `@deepseek-ai/dsh-tools@0.2.1-rc.3`。本机安装已启用,无需重复安装。
6
6
 
7
+ ## 支持的 DSH 版本
8
+
9
+ 实测可用范围:**@deepseek-ai/dsh-tools 0.1.0-rc.8 ~ 0.2.2-alpha.2**(本插件只用其中的 defineTool,这几个版本签名一致)。
10
+
11
+ 插件对宿主做了三处兜底,所以版本不合适时也不会「装上了却什么都没有」:
12
+
13
+ | 宿主情况 | 插件行为 |
14
+ | --- | --- |
15
+ | 有 ctx.effect | 正常注册,随插件卸载一起清理 |
16
+ | 没有 ctx.effect | 直接注册工具,功能不减少,日志留一条提示 |
17
+ | 解析不到 @deepseek-ai/dsh-tools | 用内置直通 defineTool,工具照常可用 |
18
+ | 没有 header.actions 插槽 | 图谱按钮退到 iews.actions(在「更多视图」菜单里) |
19
+ | 完全没有 slots 服务 | 跳过按钮,记忆工具不受影响 |
20
+
21
+ peerDependencies 已放宽为 >=0.1.0-rc.8 <0.3.0 并标为 optional——宿主自带这份 SDK,不该因为版本号不匹配而装不上。
22
+
7
23
  ## 界面
8
24
 
9
25
  安装并启用插件后,完全退出 DSH 再打开。进入任意会话,头部第一行会出现 **🧠 记忆图谱** 按钮。
package/lib/client.js CHANGED
@@ -9,6 +9,9 @@ window.__ModuleLoader__.load({
9
9
  // views.actions 是「更多视图」下拉菜单的行(Chat/Trajectory 旁边的 ⋯),
10
10
  // 注册进去只是菜单里多一行,看不见;header.actions 才是头部正常按钮区。
11
11
  var SLOT_NAME = "conversation.session.header.actions";
12
+ // 不同 DSH 版本暴露的插槽名不一样:新版本用 header.actions,
13
+ // 老版本没有这个槽时退回 views.actions(会出现在「更多视图」菜单里,虽然不显眼但能用)。
14
+ var SLOT_FALLBACKS = ["conversation.session.header.actions", "conversation.session.header.views.actions"];
12
15
  var VIEW_URL = "http://127.0.0.1:18886/index.html";
13
16
 
14
17
  function MemoryButton() {
@@ -56,16 +59,35 @@ window.__ModuleLoader__.load({
56
59
  return React.createElement(React.Fragment, null, button, ReactDOM.createPortal(overlay, document.body));
57
60
  }
58
61
 
59
- function apply(ctx) {
60
- try {
61
- ctx.slots.inject(SLOT_NAME, function () {
62
- return ctx.slots.register({
63
- name: SLOT_NAME,
62
+ function registerOn(slots, name) {
63
+ var disposable = null;
64
+ slots.inject(name, function () {
65
+ try {
66
+ disposable = slots.register({
67
+ name: name,
64
68
  id: "memory-engine-map",
65
69
  order: 50,
66
70
  label: function () { return "记忆图谱"; },
67
71
  }, MemoryButton);
68
- });
72
+ } catch (error) {
73
+ try { console.error("[dsh-memory-engine] 注册到 " + name + " 失败:", error); } catch (ignored) {}
74
+ }
75
+ return disposable;
76
+ });
77
+ return disposable;
78
+ }
79
+
80
+ function apply(ctx) {
81
+ try {
82
+ if (!ctx || !ctx.slots || typeof ctx.slots.inject !== "function") {
83
+ try { console.warn("[dsh-memory-engine] 宿主没有 slots 服务,跳过图谱按钮(记忆工具不受影响)"); } catch (ignored) {}
84
+ return;
85
+ }
86
+ // 只注册第一个能用的插槽:优先 header.actions,老宿主没有就退到 views.actions
87
+ var target = ctx.slots && typeof ctx.slots.has === "function" && !ctx.slots.has(SLOT_FALLBACKS[0])
88
+ ? SLOT_FALLBACKS[1]
89
+ : SLOT_NAME;
90
+ registerOn(ctx.slots, target);
69
91
  } catch (error) {
70
92
  try { console.error("[dsh-memory-engine] 插槽注册失败:", error); } catch (ignored) {}
71
93
  }
package/lib/index.js CHANGED
@@ -154,15 +154,33 @@ export function createTools(defineTool, store = new MemoryStore(), { map = gener
154
154
  ];
155
155
  }
156
156
 
157
+ // 宿主 SDK 的 defineTool 在不同 DSH 版本间签名一致(实测 0.1.0-rc.8 ~ 0.2.2-alpha.2 都是
158
+ // defineTool(options)),但**包不一定能解析到**——比如插件被装在宿主 node_modules 之外。
159
+ // 解析失败时退回一个直通实现:工具照常注册,只是少了宿主的返回值包装。
160
+ async function resolveDefineTool(warning) {
161
+ try {
162
+ const sdk = await import('@deepseek-ai/dsh-tools');
163
+ if (typeof sdk.defineTool === 'function') {
164
+ const version = sdk.version ?? '';
165
+ if (version) warning(`宿主 dsh-tools 版本 ${version}`);
166
+ return sdk.defineTool;
167
+ }
168
+ warning('@deepseek-ai/dsh-tools 没有导出 defineTool,改用内置直通实现');
169
+ } catch (error) {
170
+ warning(`解析 @deepseek-ai/dsh-tools 失败(${error.message}),改用内置直通实现`);
171
+ }
172
+ return options => options;
173
+ }
174
+
157
175
  export function apply(ctx) {
158
176
  try {
159
177
  if (typeof ctx?.tools?.register !== 'function') throw new Error('tools 服务不可用');
160
- if (typeof ctx.effect !== 'function') throw new Error('ctx.effect 不可用');
161
- const registration = ctx.effect(async () => {
178
+ // ctx.effect 是较新宿主才有的生命周期钩子。没有它就直接注册——
179
+ // 否则老版本 DSH 上插件会「装上了但一个工具都没有」,这是最糟的失败方式。
180
+ const setup = async () => {
162
181
  const disposers = [];
163
182
  try {
164
- // The host resolves this public SDK; no bundled copy or runtime dependency.
165
- const { defineTool } = await import('@deepseek-ai/dsh-tools');
183
+ const defineTool = await resolveDefineTool(message => warn(ctx, message));
166
184
  const store = new MemoryStore();
167
185
  for (const tool of createTools(defineTool, store, { warning: message => warn(ctx, message) })) disposers.push(ctx.tools.register(tool));
168
186
  try {
@@ -182,8 +200,14 @@ export function apply(ctx) {
182
200
  for (const dispose of disposers.reverse()) { try { dispose(); } catch { /* keep unwinding */ } }
183
201
  report(ctx, error);
184
202
  }
185
- }, 'memory-engine.tools');
186
- // Await setup without returning its disposer as a second plugin teardown.
187
- return Promise.resolve(registration).then(() => undefined, error => { report(ctx, error); });
203
+ };
204
+
205
+ if (typeof ctx.effect === 'function') {
206
+ const registration = ctx.effect(setup, 'memory-engine.tools');
207
+ // Await setup without returning its disposer as a second plugin teardown.
208
+ return Promise.resolve(registration).then(() => undefined, error => { report(ctx, error); });
209
+ }
210
+ warn(ctx, '宿主没有 ctx.effect,直接用注册式初始化(功能不减少)');
211
+ return Promise.resolve(setup()).then(() => undefined, error => { report(ctx, error); });
188
212
  } catch (error) { report(ctx, error); }
189
213
  }
@@ -1,5 +1,7 @@
1
+ const SEED = 20261030;
2
+
1
3
  export function layoutMemories(nodes) {
2
- let seed = 20261030;
4
+ let seed = SEED;
3
5
  const random = () => ((seed = (seed * 16807) % 2147483647) - 1) / 2147483646;
4
6
  function brainPoint(radiusScale) {
5
7
  const radius = Math.pow(random(), 1 / 3) * radiusScale;
@@ -11,12 +13,49 @@ export function layoutMemories(nodes) {
11
13
  z: radius * Math.sin(phi) * Math.sin(theta) * 0.78,
12
14
  };
13
15
  }
14
- return nodes.map((node) => {
16
+
17
+ const placed = nodes.map((node) => {
15
18
  const inner = ['人格', '印象', '知识'].includes(node.kind) || node.majorEmotion;
16
- const distance = inner ? 14 + Math.pow(random(), 0.65) * 36 : 48 + Math.min(1, node.ageDays / 30) * 58 + random() * 12;
19
+ // 整体半径放大 1.35 倍:原来 298 个节点挤在一起,点谁都容易点错
20
+ const distance = (inner ? 14 + Math.pow(random(), 0.65) * 36 : 48 + Math.min(1, node.ageDays / 30) * 58 + random() * 12) * 1.35;
17
21
  const baseRadius = node.majorEmotion ? 3.2 : inner
18
22
  ? (node.heat === 1 ? 2.4 : node.heat === 2 ? 1.8 : 1.3)
19
23
  : (node.heat === 1 ? 2.1 : node.heat === 2 ? 1.5 : 1.0);
20
- return { ...node, pos: brainPoint(distance), radius: baseRadius + Math.min(1.4, Math.sqrt(node.degree) * 0.18) };
24
+ // 连接数加成收敛得更紧一点,避免枢纽节点长得过大压住邻居
25
+ return { ...node, pos: brainPoint(distance), radius: baseRadius + Math.min(1.1, Math.sqrt(node.degree) * 0.15) };
21
26
  });
27
+
28
+ // ── 分离松弛:把互相重叠的节点推开 ──
29
+ // 不做这一步的话,随机落点会让相邻节点的圆重叠,视觉糊成一片,点击也分不清。
30
+ // 只在世界坐标里推开,不改 z 深度关系太多;迭代次数固定,保证同一份数据每次布局一致。
31
+ relax(placed, { iterations: 60, gap: 1.55, strength: 0.5 });
32
+ return placed;
33
+ }
34
+
35
+ function relax(items, { iterations, gap, strength }) {
36
+ if (items.length < 2) return;
37
+ for (let round = 0; round < iterations; round++) {
38
+ let moved = false;
39
+ for (let i = 0; i < items.length; i++) {
40
+ const a = items[i];
41
+ for (let j = i + 1; j < items.length; j++) {
42
+ const b = items[j];
43
+ const dx = b.pos.x - a.pos.x;
44
+ const dy = b.pos.y - a.pos.y;
45
+ const dz = b.pos.z - a.pos.z;
46
+ const dist = Math.hypot(dx, dy, dz) || 1e-6;
47
+ const minDist = (a.radius + b.radius) * gap;
48
+ if (dist >= minDist) continue;
49
+ // 单位向量;重合时用一个确定的、由索引决定的偏移方向,避免除零和随机抖动
50
+ const ux = dist > 1e-5 ? dx / dist : Math.cos(i * 2.39996);
51
+ const uy = dist > 1e-5 ? dy / dist : Math.sin(i * 2.39996);
52
+ const uz = dist > 1e-5 ? dz / dist : 0.35;
53
+ const push = (minDist - dist) * strength * 0.5;
54
+ a.pos.x -= ux * push; a.pos.y -= uy * push; a.pos.z -= uz * push;
55
+ b.pos.x += ux * push; b.pos.y += uy * push; b.pos.z += uz * push;
56
+ moved = true;
57
+ }
58
+ }
59
+ if (!moved) break;
60
+ }
22
61
  }
@@ -121,11 +121,17 @@ function fire(id,now) {
121
121
  }
122
122
  }
123
123
  function select(x,y) {
124
- let best=null,distance=Infinity;
124
+ // 命中判定:先按「离光标最近的节点」选,而不是「第一个框住光标的节点」。
125
+ // 原来要求 值<该节点半径,小节点容易被邻居抢走 → 点错。
126
+ // 现在给每个节点一个最小容差,再用距离减去一点半径作为打分(同样近时偏向大的那个)。
127
+ let best=null,bestScore=Infinity;
125
128
  for (const star of DATA.stars) {
126
129
  if (!star._screen) continue;
127
130
  const value=Math.hypot(star._screen.x-x,star._screen.y-y);
128
- if (value<star._screen.r && value<distance) {best=star;distance=value;}
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;}
129
135
  }
130
136
  if (!best) return;
131
137
  fire(best.id,performance.now());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-memory-engine",
3
- "version": "0.5.6",
3
+ "version": "0.6.0",
4
4
  "description": "Local DSH memory engine with neuron network visualization and private file import",
5
5
  "keywords": [
6
6
  "dsh",
@@ -36,7 +36,7 @@
36
36
  },
37
37
  "dependencies": {},
38
38
  "peerDependencies": {
39
- "@deepseek-ai/dsh-tools": "0.2.1-rc.3"
39
+ "@deepseek-ai/dsh-tools": ">=0.1.0-rc.8 <0.3.0"
40
40
  },
41
41
  "dsh": {
42
42
  "bundle": {
@@ -48,6 +48,11 @@
48
48
  }
49
49
  },
50
50
  "bugs": "https://github.com/",
51
+ "peerDependenciesMeta": {
52
+ "@deepseek-ai/dsh-tools": {
53
+ "optional": true
54
+ }
55
+ },
51
56
  "scripts": {
52
57
  "test": "node --test test/*.test.mjs"
53
58
  }