dsh-memory-engine 0.6.0 → 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,397 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-memory-engine",
3
+ factory: function (require) {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ var React = require("react");
7
+ var ReactDOM = require("react-dom");
8
+ // 2026-10-30:从 views.actions 挪到 header.actions。
9
+ // views.actions 是「更多视图」下拉菜单的行(Chat/Trajectory 旁边的 ⋯),
10
+ // 注册进去只是菜单里多一行,看不见;header.actions 才是头部正常按钮区。
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"];
15
+
16
+ /* __CANVAS_RENDERER__ */
17
+
18
+ var defaultSettings = { hideButton: false, showGuideOnEmpty: true };
19
+ var actionStyle = { padding: "7px 12px", borderRadius: "8px", border: "1px solid #6999b8", background: "#102236", color: "#e2f4ff", font: "inherit", cursor: "pointer" };
20
+
21
+ function readApi(url, signal) {
22
+ return fetch(url, { cache: "no-store", signal: signal }).then(function (response) {
23
+ if (!response.ok) throw new Error("HTTP " + response.status);
24
+ return response.json();
25
+ }).then(function (result) {
26
+ if (!result || result.ok !== true || !result.data || typeof result.data !== "object") throw new Error("接口返回格式无效");
27
+ return result.data;
28
+ });
29
+ }
30
+
31
+ function loadSettings() {
32
+ var controller = new AbortController();
33
+ var timer = setTimeout(function () { controller.abort(); }, 5000);
34
+ return Promise.resolve().then(function () {
35
+ return readApi("/memory-engine/api/config", controller.signal);
36
+ }).then(function (settings) {
37
+ if (typeof settings.hideButton !== "boolean" || typeof settings.showGuideOnEmpty !== "boolean") throw new Error("按钮配置格式无效");
38
+ return settings;
39
+ }).finally(function () { clearTimeout(timer); });
40
+ }
41
+
42
+ function copyText(text) {
43
+ function fallback() {
44
+ var area = document.createElement("textarea");
45
+ var active = document.activeElement;
46
+ area.value = text;
47
+ area.style.cssText = "position:fixed;left:-9999px;top:0";
48
+ document.body.appendChild(area);
49
+ area.select();
50
+ try {
51
+ if (!document.execCommand("copy")) throw new Error("复制失败");
52
+ } finally {
53
+ area.remove();
54
+ if (active && typeof active.focus === "function") active.focus();
55
+ }
56
+ }
57
+ return Promise.resolve().then(function () {
58
+ if (navigator.clipboard && navigator.clipboard.writeText) return navigator.clipboard.writeText(text);
59
+ return fallback();
60
+ }).catch(function () { return fallback(); });
61
+ }
62
+
63
+ function GuideCard(props) {
64
+ var copiedState = React.useState("复制这段提示词");
65
+ var copyLabel = copiedState[0];
66
+ var setCopyLabel = copiedState[1];
67
+ var card = React.useRef(null);
68
+ React.useEffect(function () { if (card.current) card.current.focus(); }, []);
69
+ function trapTab(event) {
70
+ if (event.key !== "Tab" || !card.current) return;
71
+ var buttons = Array.from(card.current.querySelectorAll("button"));
72
+ var position = buttons.indexOf(document.activeElement);
73
+ if (event.shiftKey && position <= 0) { event.preventDefault(); buttons[buttons.length - 1].focus(); }
74
+ else if (!event.shiftKey && (position === -1 || position === buttons.length - 1)) { event.preventDefault(); buttons[0].focus(); }
75
+ }
76
+ var data = props.data;
77
+ return React.createElement("div", {
78
+ "data-memory-engine-guide-layer": "true",
79
+ style: { position: "absolute", inset: 0, zIndex: 2, display: "grid", placeItems: "center", padding: "16px", background: "rgba(3,6,12,.7)" },
80
+ }, React.createElement("section", {
81
+ ref: card, role: "dialog", "aria-modal": "true", "aria-label": "使用指南", tabIndex: -1,
82
+ "data-memory-engine-guide": "true", onKeyDown: trapTab,
83
+ style: { boxSizing: "border-box", width: "min(620px,100%)", maxHeight: "calc(100vh - 32px)", overflow: "auto", padding: "24px", border: "1px solid #3976a0", borderRadius: "16px", background: "#07111f", color: "#dbeaf8", font: "14px/1.65 system-ui,sans-serif", boxShadow: "0 16px 80px #0008" },
84
+ }, React.createElement("h2", { style: { margin: "0 0 16px", fontSize: "22px", color: "#b7e9ff" } }, "怎么用"),
85
+ props.error ? React.createElement("div", { role: "alert" },
86
+ React.createElement("p", null, "引导加载失败,请重试。"),
87
+ React.createElement("button", { type: "button", onClick: props.onRetry, style: actionStyle }, "重试"))
88
+ : !data ? React.createElement("p", { role: "status" }, "正在加载使用指南……")
89
+ : React.createElement(React.Fragment, null,
90
+ data.empty ? React.createElement("p", { style: { color: "#9fb7ce" } }, "还没有记忆,从下面任一步开始。") : null,
91
+ data.steps.map(function (step, index) {
92
+ return React.createElement("div", { key: index, style: { borderLeft: "3px solid #28577e", paddingLeft: "14px", marginBottom: "18px" } },
93
+ React.createElement("h3", { style: { fontSize: "15px", color: "#9fd0f5", margin: "0 0 6px" } }, "第 " + (index + 1) + " 步 " + step.title),
94
+ React.createElement("p", { style: { margin: "0 0 8px" } }, step.body),
95
+ index === 0 ? React.createElement(React.Fragment, null,
96
+ React.createElement("pre", { style: { whiteSpace: "pre-wrap", overflowWrap: "anywhere", maxHeight: "180px", overflow: "auto", margin: "8px 0", padding: "12px", background: "#030a13", borderRadius: "8px", font: "12px/1.6 ui-monospace,Consolas,monospace" } }, data.prompt),
97
+ React.createElement("button", { type: "button", "data-memory-engine-copy": "true", style: actionStyle,
98
+ onClick: function () { copyText(data.prompt).then(function () { setCopyLabel("已复制 ✓"); }, function () { setCopyLabel("复制失败,请手动复制"); }); },
99
+ }, copyLabel)) : null,
100
+ index === 2 ? React.createElement("button", { type: "button", onClick: props.onRefresh, style: actionStyle }, "刷新图谱") : null);
101
+ }),
102
+ React.createElement("p", { style: { fontSize: "12px", color: "#9fb7ce", overflowWrap: "anywhere" } }, "数据目录:", data.dataDir)),
103
+ React.createElement("div", { style: { display: "flex", justifyContent: "flex-end", marginTop: "16px" } },
104
+ React.createElement("button", { type: "button", onClick: props.onClose, style: actionStyle }, "知道了"))));
105
+ }
106
+
107
+ function MemoryDetails(props) {
108
+ var node = props.node;
109
+ var text = typeof node.text === "string" ? node.text : "";
110
+ var title = String(node.title || "").trim() || Array.from(text).slice(0, 20).join("");
111
+ var date = new Date(node.ts);
112
+ var day = Number.isFinite(date.getTime()) ? date.getFullYear() + "-" + String(date.getMonth() + 1).padStart(2, "0") + "-" + String(date.getDate()).padStart(2, "0") : "未知";
113
+ var heat = { 1: "热", 2: "温", 3: "冷" }[node.heat];
114
+ var rows = [
115
+ ["重要度", heat ? heat + "(" + node.heat + ")" : "未知"],
116
+ ["留存", Number.isFinite(node.retention) ? node.retention.toFixed(2) : "—"],
117
+ ["连接数", node.degree == null ? 0 : node.degree],
118
+ ["时间", day],
119
+ ];
120
+ var emotion = Array.isArray(node.emotion) ? node.emotion.filter(function (item) { return item && item.tag; }).map(function (item) { return item.tag; }).join(" / ") : "";
121
+ if (emotion) rows.push(["情绪", emotion]);
122
+ var copiedState = React.useState("复制正文");
123
+ var copyLabel = copiedState[0];
124
+ var setCopyLabel = copiedState[1];
125
+ var timer = React.useRef(null);
126
+ var copyVersion = React.useRef(0);
127
+ var body = React.useRef(null);
128
+ React.useEffect(function () {
129
+ copyVersion.current++;
130
+ clearTimeout(timer.current);
131
+ timer.current = null;
132
+ setCopyLabel("复制正文");
133
+ if (body.current) body.current.scrollTop = 0;
134
+ return function () { copyVersion.current++; clearTimeout(timer.current); timer.current = null; };
135
+ }, [node.id, text]);
136
+ function copyBody() {
137
+ var version = ++copyVersion.current;
138
+ clearTimeout(timer.current);
139
+ copyText(text).then(function () {
140
+ if (version !== copyVersion.current) return;
141
+ setCopyLabel("已复制 ✓");
142
+ timer.current = setTimeout(function () {
143
+ if (version === copyVersion.current) setCopyLabel("复制正文");
144
+ timer.current = null;
145
+ }, 1800);
146
+ }, function () {
147
+ if (version === copyVersion.current) setCopyLabel("复制失败,请重试");
148
+ });
149
+ }
150
+ return React.createElement("section", {
151
+ role: "region", "aria-label": "记忆详情", "data-memory-engine-detail": "true", "data-memory-engine-node": node.id,
152
+ 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" },
153
+ }, React.createElement("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: "12px", flexShrink: 0 } },
154
+ React.createElement("span", { "data-memory-engine-detail-kind": "true", style: { color: "#a4e6ff", background: "#15344b", borderRadius: "6px", padding: "2px 8px", overflowWrap: "anywhere" } }, node.kind || "未分类"),
155
+ React.createElement("button", { type: "button", "aria-label": "关闭记忆详情", onClick: props.onClose, style: { border: 0, padding: "2px 6px", background: "transparent", color: "#a4bad0", fontSize: "22px", cursor: "pointer" } }, "×")),
156
+ 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),
157
+ 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 } },
158
+ rows.map(function (row) { return React.createElement(React.Fragment, { key: row[0] },
159
+ React.createElement("dt", { style: { margin: 0 } }, row[0]),
160
+ React.createElement("dd", { style: { margin: 0, overflowWrap: "anywhere" } }, row[1])); })),
161
+ React.createElement("div", { ref: body, "data-memory-engine-detail-text": "true", style: { minHeight: 0, flex: "1 1 auto", overflowY: "auto", whiteSpace: "pre-wrap", overflowWrap: "anywhere", borderTop: "1px solid #3976a055", borderBottom: "1px solid #3976a055", padding: "12px 0" } }, text),
162
+ React.createElement("div", { style: { display: "flex", flexWrap: "wrap", gap: "8px", marginTop: "12px", flexShrink: 0 } },
163
+ React.createElement("button", { type: "button", "data-memory-engine-highlight": "true", style: actionStyle, onClick: props.onHighlight }, "在图里高亮它"),
164
+ React.createElement("button", { type: "button", "data-memory-engine-copy-body": "true", style: actionStyle, onClick: copyBody }, copyLabel)));
165
+ }
166
+
167
+ function MemoryCanvas(props) {
168
+ var canvas = React.useRef(null);
169
+ var dataState = React.useState(null);
170
+ var data = dataState[0];
171
+ var setData = dataState[1];
172
+ var errorState = React.useState("");
173
+ var error = errorState[0];
174
+ var setError = errorState[1];
175
+ var retryState = React.useState(0);
176
+ var retry = retryState[0];
177
+ var setRetry = retryState[1];
178
+ React.useEffect(function () {
179
+ var controller = new AbortController();
180
+ var active = true;
181
+ var renderer;
182
+ setError("");
183
+ readApi("/memory-engine/api/map", controller.signal).then(function (nextData) {
184
+ if (!active) return;
185
+ renderer = createRenderer(canvas.current, nextData);
186
+ props.rendererRef.current = renderer;
187
+ renderer.onSelect(function (node) { props.selectionVersion.current++; props.onSelect(node); });
188
+ renderer.start();
189
+ setData(nextData);
190
+ }).catch(function (failure) {
191
+ if (!active) return;
192
+ if (renderer) renderer.destroy();
193
+ if (props.rendererRef.current === renderer) props.rendererRef.current = null;
194
+ console.error("[dsh-memory-engine] Canvas 图谱加载失败:", failure);
195
+ setError(failure.message);
196
+ });
197
+ return function () {
198
+ active = false;
199
+ controller.abort();
200
+ if (renderer) renderer.destroy();
201
+ if (props.rendererRef.current === renderer) props.rendererRef.current = null;
202
+ };
203
+ }, [retry, props.rendererRef, props.selectionVersion, props.onSelect]);
204
+ return React.createElement(React.Fragment, null,
205
+ React.createElement("canvas", {
206
+ ref: canvas, "data-memory-engine-canvas": "true", "aria-label": "记忆神经网络",
207
+ style: { display: "block", width: "100%", height: "100%", touchAction: "none", cursor: "grab" },
208
+ }),
209
+ React.createElement("div", {
210
+ "data-memory-engine-summary": "true",
211
+ style: { position: "absolute", top: "16px", left: "18px", pointerEvents: "none", color: "#d8e9f8", font: "13px/1.65 system-ui,sans-serif", textShadow: "0 0 12px #3388bb88" },
212
+ }, React.createElement("strong", { style: { color: "#a4e6ff", fontSize: "16px" } }, "记忆神经网络"),
213
+ React.createElement("div", null, data ? data.nodes.length + " 个神经元 · " + data.links.length + " 条突触" : "正在加载图谱……"),
214
+ React.createElement("div", { style: { color: "#7491a8", fontSize: "12px" } }, "大小=重要度和连接数 · 亮度=留存")),
215
+ data ? React.createElement("section", {
216
+ "aria-label": "复习清单",
217
+ style: { position: "absolute", bottom: "16px", left: "18px", width: "min(400px,calc(100% - 36px))", boxSizing: "border-box", maxHeight: "32%", overflow: "auto", padding: "15px", background: "#07111fee", border: "1px solid #3976a055", borderRadius: "14px", boxShadow: "0 0 40px #1763a033", backdropFilter: "blur(8px)", font: "12px/1.65 system-ui,sans-serif", color: "#b1c6d9" },
218
+ }, React.createElement("h2", { style: { fontSize: "14px", color: "#ffdb82", margin: "0 0 8px" } }, "复习清单 · 留存最低的记忆"),
219
+ data.review.map(function (item, index) {
220
+ return React.createElement("div", { key: item.id || index, style: { display: "flex", gap: "7px", alignItems: "center", margin: "5px 0" } },
221
+ React.createElement("span", { style: { flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, (index + 1) + ". [" + item.kind + "] " + item.title),
222
+ React.createElement("span", { style: { width: "52px", height: "5px", borderRadius: "5px", background: "#335066", overflow: "hidden" } },
223
+ React.createElement("i", { style: { display: "block", height: "100%", background: "#6bc9fa", width: Math.min(100, Math.max(0, item.retention * 100)) + "%" } })));
224
+ })) : null,
225
+ React.createElement("div", { style: { position: "absolute", right: "18px", bottom: "18px", pointerEvents: "none", color: "#7b99ad", font: "13px system-ui,sans-serif" } }, "拖拽旋转 · 滚轮缩放 · 点击选中神经元"),
226
+ error ? React.createElement("div", { role: "alert", style: { position: "absolute", inset: "40% 20% auto", textAlign: "center", color: "#d8e9f8" } },
227
+ React.createElement("p", null, "图谱加载失败:" + error),
228
+ React.createElement("button", { type: "button", style: actionStyle, onClick: function () { setRetry(function (value) { return value + 1; }); } }, "重试")) : null);
229
+ }
230
+
231
+ function MemoryButton(props) {
232
+ var settings = props.settings;
233
+ var state = React.useState(false);
234
+ var open = state[0];
235
+ var setOpen = state[1];
236
+ var guideState = React.useState(false);
237
+ var guideOpen = guideState[0];
238
+ var setGuideOpen = guideState[1];
239
+ var dataState = React.useState(null);
240
+ var guideData = dataState[0];
241
+ var setGuideData = dataState[1];
242
+ var errorState = React.useState("");
243
+ var guideError = errorState[0];
244
+ var setGuideError = errorState[1];
245
+ var retryState = React.useState(0);
246
+ var retry = retryState[0];
247
+ var setRetry = retryState[1];
248
+ var selectedState = React.useState(null);
249
+ var selected = selectedState[0];
250
+ var setSelected = selectedState[1];
251
+ var renderer = React.useRef(null);
252
+ var selectionVersion = React.useRef(0);
253
+ var pointer = React.useRef(null);
254
+ var help = React.useRef(null);
255
+ var dismissed = React.useRef(false);
256
+ function closeGuide() {
257
+ dismissed.current = true;
258
+ setGuideOpen(false);
259
+ if (help.current) help.current.focus();
260
+ }
261
+ function openMap() {
262
+ dismissed.current = false;
263
+ setGuideOpen(false);
264
+ setSelected(null);
265
+ setOpen(Date.now());
266
+ }
267
+ function closeMap() { setSelected(null); setOpen(false); }
268
+ function startPointer(event) {
269
+ if (event.target.closest("[data-memory-engine-detail], [data-memory-engine-guide-layer], [data-memory-engine-controls]")) { pointer.current = null; return; }
270
+ pointer.current = { x: event.clientX, y: event.clientY, moved: 0, version: selectionVersion.current };
271
+ }
272
+ function movePointer(event) {
273
+ if (!pointer.current) return;
274
+ pointer.current.moved += Math.abs(event.clientX - pointer.current.x) + Math.abs(event.clientY - pointer.current.y);
275
+ pointer.current.x = event.clientX; pointer.current.y = event.clientY;
276
+ }
277
+ function dismissOutside() {
278
+ var gesture = pointer.current;
279
+ pointer.current = null;
280
+ if (gesture && gesture.moved < 6 && gesture.version === selectionVersion.current) setSelected(null);
281
+ }
282
+ React.useEffect(function () {
283
+ if (!open) return undefined;
284
+ var controller = new AbortController();
285
+ var active = true;
286
+ setGuideData(null);
287
+ setGuideError("");
288
+ readApi("/memory-engine/api/onboard", controller.signal).then(function (data) {
289
+ if (typeof data.empty !== "boolean" || typeof data.prompt !== "string" || typeof data.dataDir !== "string"
290
+ || !Array.isArray(data.steps) || data.steps.length !== 3
291
+ || data.steps.some(function (step) { return !step || typeof step.title !== "string" || typeof step.body !== "string"; })) throw new Error("引导数据格式无效");
292
+ if (!active) return;
293
+ setGuideData(data);
294
+ if (data.empty && settings.showGuideOnEmpty && !dismissed.current) setGuideOpen(true);
295
+ }).catch(function (error) {
296
+ if (active) setGuideError(error.message);
297
+ });
298
+ return function () { active = false; controller.abort(); };
299
+ }, [open, retry, settings.showGuideOnEmpty]);
300
+ React.useEffect(function () {
301
+ if (!open) return undefined;
302
+ function onEscape() { if (guideOpen) closeGuide(); else if (selected) setSelected(null); else closeMap(); }
303
+ function onKey(event) {
304
+ if (event.key !== "Escape") return;
305
+ event.preventDefault();
306
+ event.stopPropagation();
307
+ if (!event.repeat) onEscape();
308
+ }
309
+ window.addEventListener("keydown", onKey, true);
310
+ return function () { window.removeEventListener("keydown", onKey, true); };
311
+ }, [open, guideOpen, selected]);
312
+ var button = React.createElement("button", {
313
+ type: "button",
314
+ onClick: openMap,
315
+ "data-memory-engine-map": "true",
316
+ "aria-label": "打开记忆图谱",
317
+ title: "记忆图谱",
318
+ style: { display: "inline-flex", alignItems: "center", gap: "5px", padding: "5px 9px", borderRadius: "8px", border: "1px solid rgba(130,160,190,.4)", background: "transparent", color: "inherit", font: "inherit", cursor: "pointer" },
319
+ }, React.createElement("span", { "aria-hidden": "true" }, "🧠"), "记忆图谱");
320
+ if (!open) return button;
321
+ var overlay = React.createElement("div", {
322
+ role: "dialog",
323
+ "aria-modal": "true",
324
+ "aria-label": "记忆图谱",
325
+ onPointerDownCapture: startPointer, onPointerMoveCapture: movePointer,
326
+ onPointerCancelCapture: function () { pointer.current = null; }, onClick: dismissOutside,
327
+ style: { position: "fixed", inset: 0, maxWidth: "none", maxHeight: "none", zIndex: 9999, background: "#03060c" },
328
+ }, React.createElement(MemoryCanvas, { key: open, rendererRef: renderer, selectionVersion: selectionVersion, onSelect: setSelected }),
329
+ React.createElement("style", null, "@keyframes dsh-memory-detail-in{from{opacity:0;transform:translateX(24px)}to{opacity:1;transform:translateX(0)}}@media(prefers-reduced-motion:reduce){[data-memory-engine-detail]{animation:none!important}}"),
330
+ React.createElement("div", {
331
+ "data-memory-engine-controls": "true",
332
+ style: { position: "absolute", top: "16px", right: "18px", zIndex: 1, display: "flex", gap: "8px" },
333
+ }, React.createElement("button", {
334
+ ref: help, type: "button", onClick: function () { setGuideOpen(true); },
335
+ "aria-label": "查看使用指南", title: "使用指南", style: actionStyle,
336
+ }, "?"), React.createElement("button", {
337
+ type: "button",
338
+ onClick: closeMap,
339
+ "aria-label": "关闭记忆图谱",
340
+ style: actionStyle,
341
+ }, "关闭 ×")), selected ? React.createElement(MemoryDetails, {
342
+ node: selected, onClose: function () { setSelected(null); },
343
+ onHighlight: function () { if (renderer.current) renderer.current.fireNeuron(selected.id); },
344
+ }) : null, guideOpen ? React.createElement(GuideCard, {
345
+ data: guideData, error: guideError, onClose: closeGuide, onRefresh: openMap,
346
+ onRetry: function () { setRetry(function (value) { return value + 1; }); },
347
+ }) : null);
348
+ return React.createElement(React.Fragment, null, button, ReactDOM.createPortal(overlay, document.body));
349
+ }
350
+
351
+ function registerOn(slots, name, settings) {
352
+ var disposable = null;
353
+ slots.inject(name, function () {
354
+ try {
355
+ disposable = slots.register({
356
+ name: name,
357
+ id: "memory-engine-map",
358
+ order: 50,
359
+ label: function () { return "记忆图谱"; },
360
+ }, function ConfiguredMemoryButton() { return React.createElement(MemoryButton, { settings: settings }); });
361
+ } catch (error) {
362
+ try { console.error("[dsh-memory-engine] 注册到 " + name + " 失败:", error); } catch (ignored) {}
363
+ }
364
+ return disposable;
365
+ });
366
+ return disposable;
367
+ }
368
+
369
+ function apply(ctx) {
370
+ try {
371
+ if (!ctx || !ctx.slots || typeof ctx.slots.inject !== "function") {
372
+ try { console.warn("[dsh-memory-engine] 宿主没有 slots 服务,跳过图谱按钮(记忆工具不受影响)"); } catch (ignored) {}
373
+ return;
374
+ }
375
+ // 只注册第一个能用的插槽:优先 header.actions,老宿主没有就退到 views.actions
376
+ var target = ctx.slots && typeof ctx.slots.has === "function" && !ctx.slots.has(SLOT_FALLBACKS[0])
377
+ ? SLOT_FALLBACKS[1]
378
+ : SLOT_NAME;
379
+ function mount(settings) {
380
+ if (!settings.hideButton) registerOn(ctx.slots, target, settings);
381
+ }
382
+ function fallback(error) {
383
+ try { console.warn("[dsh-memory-engine] 配置读取失败,按默认显示按钮:", error); } catch (ignored) {}
384
+ mount(defaultSettings);
385
+ }
386
+ try { loadSettings().then(mount, fallback); }
387
+ catch (error) { fallback(error); }
388
+ } catch (error) {
389
+ try { console.error("[dsh-memory-engine] 插槽注册失败:", error); } catch (ignored) {}
390
+ }
391
+ }
392
+
393
+ exports.apply = apply;
394
+ exports.inject = ["slots"];
395
+ return module.exports;
396
+ }
397
+ });
package/lib/config.js CHANGED
@@ -8,6 +8,11 @@ export function resolveDiaryDir({ home = defaultHome(), envDir = process.env.QQ_
8
8
  return envDir || readConfig(home).diaryDir || '';
9
9
  }
10
10
 
11
+ export function readUiConfig(home = defaultHome()) {
12
+ const config = readConfig(home);
13
+ return { hideButton: config.hideButton === true, showGuideOnEmpty: config.showGuideOnEmpty !== false };
14
+ }
15
+
11
16
  export function readConfig(home = defaultHome()) {
12
17
  const file = path.join(home, 'memory-engine', 'config.json');
13
18
  try {
package/lib/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import { MemoryStore } from './store.js';
2
2
  import { chars, titleOf } from './decay.js';
3
3
  import { generateMap, mapReport, openMap } from './map.js';
4
- import { startMapServer } from './serve.js';
4
+ import { registerMemoryRoutes } from './routes.js';
5
5
 
6
6
  export const name = 'memory-engine';
7
- export const inject = ['tools'];
7
+ export const inject = ['tools', 'webServer'];
8
8
 
9
9
  function report(ctx, error) {
10
10
  try {
@@ -184,10 +184,9 @@ export function apply(ctx) {
184
184
  const store = new MemoryStore();
185
185
  for (const tool of createTools(defineTool, store, { warning: message => warn(ctx, message) })) disposers.push(ctx.tools.register(tool));
186
186
  try {
187
- const server = await startMapServer({ store, warn: message => warn(ctx, message) });
188
- disposers.push(() => server.close());
187
+ disposers.push(registerMemoryRoutes(ctx.webServer, { store, warn: message => warn(ctx, message) }));
189
188
  } catch (error) {
190
- warn(ctx, `图谱服务启动失败,记忆工具仍可用:${error.message}`);
189
+ warn(ctx, `图谱路由注册失败,记忆工具仍可用:${error.message}`);
191
190
  }
192
191
  const timer = setTimeout(() => {
193
192
  try { if (store.read().length > 0) generateMap(store); }
package/lib/map.js CHANGED
@@ -6,6 +6,17 @@ import { pathToFileURL } from 'node:url';
6
6
  import { scoreMemories, layoutMemories, renderHtml } from './render/index.mjs';
7
7
  import { readConfig } from './config.js';
8
8
 
9
+ export function computeMapData(store) {
10
+ const items = store.read().map(({ id, text, title: nodeTitle, ts, kind, heat }) => ({ id, text,
11
+ title: nodeTitle || Array.from(text).slice(0, 20).join(''), ts, kind, heat }));
12
+ const now = store.now();
13
+ const scored = scoreMemories(items, now);
14
+ const nodes = layoutMemories(scored.nodes);
15
+ const kinds = Object.fromEntries([...new Set(nodes.map(node => node.kind))].map(kind => [kind, nodes.filter(node => node.kind === kind).length]));
16
+ return { generatedAt: now, nodes, links: scored.links, review: scored.review,
17
+ stats: { total: nodes.length, byKind: kinds, forgettingSoon: nodes.filter(node => node.retention < 0.3).length } };
18
+ }
19
+
9
20
  export function generateMap(store, { out, title = '我的记忆神经网络' } = {}) {
10
21
  const started = performance.now();
11
22
  if (typeof title !== 'string' || !title.trim()) throw new Error('title 必须是非空字符串');
@@ -20,15 +31,8 @@ export function generateMap(store, { out, title = '我的记忆神经网络' } =
20
31
  throw new Error('桥接目录只读,请把图谱输出到插件数据目录或其他目录');
21
32
  }
22
33
  }
23
- const items = store.read().map(({ id, text, title: nodeTitle, ts, kind, heat }) => ({ id, text,
24
- title: nodeTitle || Array.from(text).slice(0, 20).join(''), ts, kind, heat }));
25
- const now = store.now();
26
- const { nodes, links, review } = scoreMemories(items, now);
27
- const stars = layoutMemories(nodes);
28
- const kinds = Object.fromEntries([...new Set(stars.map(star => star.kind))].map(kind => [kind, stars.filter(star => star.kind === kind).length]));
29
- const payload = { generatedAt: now, stats: { total: stars.length, byKind: kinds,
30
- forgettingSoon: stars.filter(star => star.retention < 0.3).length }, stars, links, review };
31
- const html = renderHtml(payload, title);
34
+ const { nodes, ...data } = computeMapData(store);
35
+ const html = renderHtml({ ...data, stars: nodes }, title);
32
36
  fs.mkdirSync(path.dirname(file), { recursive: true });
33
37
  const temporary = `${file}.${process.pid}.${randomBytes(5).toString('hex')}.tmp`;
34
38
  try {
@@ -37,7 +41,8 @@ export function generateMap(store, { out, title = '我的记忆神经网络' } =
37
41
  } finally {
38
42
  try { fs.unlinkSync(temporary); } catch (error) { if (error.code !== 'ENOENT') throw error; }
39
43
  }
40
- return { file, total: stars.length, edges: links.length, kinds, bytes: Buffer.byteLength(html), elapsedMs: performance.now() - started };
44
+ return { file, total: nodes.length, edges: data.links.length, kinds: data.stats.byKind,
45
+ bytes: Buffer.byteLength(html), elapsedMs: performance.now() - started };
41
46
  }
42
47
 
43
48
  export function mapReport(result) {
package/lib/onboard.js ADDED
@@ -0,0 +1,18 @@
1
+ export const ONBOARD_PROMPT = [
2
+ '聊天时遇到这些,用 memory_save 记下来:',
3
+ ' · 对方的称呼、偏好、忌讳、习惯(kind="印象",who 填对方名字)',
4
+ ' · 答应过的事、约定、承诺(kind="对话")',
5
+ ' · 情绪特别重的瞬间(kind="事件",heat=1)',
6
+ ' · 新学到的知识、名词、梗的解释(kind="知识")',
7
+ ' · 每天结束时,用一两句写当天小结(kind="日记")',
8
+ '',
9
+ '不用每条都记。一天三五条就够,宁可少存也别灌水。',
10
+ '存之前先 memory_recall 查一下,避免重复。',
11
+ ].join('\n');
12
+
13
+ export const ONBOARD_STEPS = [
14
+ { title: '让机器人开始记', body: '把这段提示词加进机器人的角色卡或提示词里。' },
15
+ { title: '或者导入旧记忆', body: '在对话里说:「把 D:\\bot\\memories.json 导进记忆库」。支持 JSON、Markdown 和纯文本,先预览,确认后导入。' },
16
+ { title: '聊几句,回来点刷新', body: '有了新记忆后,点「刷新图谱」就能看到。' },
17
+ ];
18
+