dsh-memory-engine 0.7.6 → 0.7.7
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 +19 -6
- package/lib/client.js +232 -42
- package/lib/client.src.js +111 -22
- package/lib/config-write.js +2 -0
- package/lib/config.js +7 -2
- package/lib/index.js +8 -4
- package/lib/index.js.bak-before-titlefix +235 -0
- package/lib/map.js +1 -0
- package/lib/render/canvas.js +90 -20
- package/lib/render/layout.mjs +33 -0
- package/lib/render/render.mjs +15 -2
- package/package.json +1 -1
- package/scripts/build-client.mjs +4 -1
package/lib/client.src.js
CHANGED
|
@@ -14,11 +14,40 @@ window.__ModuleLoader__.load({
|
|
|
14
14
|
var SLOT_FALLBACKS = ["conversation.session.header.actions", "conversation.session.header.views.actions"];
|
|
15
15
|
|
|
16
16
|
/* __CANVAS_RENDERER__ */
|
|
17
|
+
/* __GRAPH_LAYOUT__ */
|
|
17
18
|
|
|
18
|
-
var defaultSettings = { hideButton: false, showGuideOnEmpty: true, autoMemory: { enabled: false, maxPerTurn: 2, dailyCap: 40 }, showAutoMemoryCostHint: true };
|
|
19
|
-
var actionStyle = { padding: "7px 12px", borderRadius: "8px", border: "1px solid
|
|
19
|
+
var defaultSettings = { hideButton: false, showGuideOnEmpty: true, graphTheme: "classic", graphLayout: "sphere", autoMemory: { enabled: false, maxPerTurn: 2, dailyCap: 40 }, showAutoMemoryCostHint: true };
|
|
20
|
+
var actionStyle = { padding: "7px 12px", borderRadius: "8px", border: "1px solid var(--memory-border,#6999b8)", background: "var(--memory-button,#102236)", color: "var(--memory-text,#e2f4ff)", font: "inherit", cursor: "pointer" };
|
|
20
21
|
var fieldStyle = { boxSizing: "border-box", padding: "10px 12px", borderRadius: "8px", border: "1px solid #35536b", background: "#091422", color: "#e2f4ff", font: "inherit", minWidth: 0 };
|
|
21
22
|
var kinds = ["知识", "印象", "对话", "事件", "日记"];
|
|
23
|
+
var appearanceOptions = [
|
|
24
|
+
{ key: "graphTheme", label: "配色", choices: [["classic", "经典星空"], ["neon", "赛博霓虹"], ["paper", "铅绘素描"], ["mono", "极简单色"]] },
|
|
25
|
+
{ key: "graphLayout", label: "布局", choices: [["sphere", "球体"], ["flat", "平面圆"], ["spiral", "螺旋银河"]] },
|
|
26
|
+
];
|
|
27
|
+
var appearanceStyles = {
|
|
28
|
+
classic: {},
|
|
29
|
+
neon: { "--memory-text": "#f4dfff", "--memory-muted": "#ba9aca", "--memory-accent": "#ff9de1", "--memory-border": "#704580", "--memory-panel": "#170d21ee", "--memory-button": "#271333", "--memory-shadow": "none" },
|
|
30
|
+
paper: { "--memory-text": "#303030", "--memory-muted": "#72716d", "--memory-accent": "#404040", "--memory-border": "#b5b1a9", "--memory-panel": "#f7f5f0f2", "--memory-button": "#eeece6", "--memory-shadow": "none" },
|
|
31
|
+
mono: { "--memory-text": "#eeeeee", "--memory-muted": "#aaaaaa", "--memory-accent": "#eeeeee", "--memory-border": "#666666", "--memory-panel": "#171717ee", "--memory-button": "#252525", "--memory-shadow": "none" },
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function AppearanceControls(props) {
|
|
35
|
+
return React.createElement("section", { "aria-label": "图谱外观", "data-memory-engine-appearance-options": props.location,
|
|
36
|
+
style: { color: "var(--memory-text,#dbeaf8)", font: "14px/1.7 system-ui,sans-serif" } },
|
|
37
|
+
appearanceOptions.map(function (group) {
|
|
38
|
+
return React.createElement("fieldset", { key: group.key, disabled: props.busy, style: { border: 0, margin: "0 0 14px", padding: 0 } },
|
|
39
|
+
React.createElement("legend", { style: { marginBottom: "8px", color: "var(--memory-muted,#9fb7ce)" } }, group.label),
|
|
40
|
+
React.createElement("div", { style: { display: "flex", flexWrap: "wrap", gap: "8px 14px" } }, group.choices.map(function (choice) {
|
|
41
|
+
return React.createElement("label", { key: choice[0], style: { display: "inline-flex", alignItems: "center", gap: "6px", cursor: props.busy ? "wait" : "pointer" } },
|
|
42
|
+
React.createElement("input", { type: "radio", name: "memory-" + props.location + "-" + group.key, value: choice[0],
|
|
43
|
+
"data-memory-engine-appearance-choice": group.key + ":" + choice[0],
|
|
44
|
+
checked: props.settings[group.key] === choice[0], style: { margin: 0, accentColor: "var(--memory-accent,#a4e6ff)" },
|
|
45
|
+
onChange: function () { var changes = {}; changes[group.key] = choice[0]; props.onChange(changes); } }), choice[1]);
|
|
46
|
+
})));
|
|
47
|
+
}), React.createElement("button", { type: "button", disabled: props.busy, style: actionStyle,
|
|
48
|
+
"data-memory-engine-appearance-reset": props.location,
|
|
49
|
+
onClick: function () { props.onChange({ graphTheme: "classic", graphLayout: "sphere" }); } }, "恢复默认"));
|
|
50
|
+
}
|
|
22
51
|
|
|
23
52
|
function requestApi(url, options) {
|
|
24
53
|
return fetch(url, Object.assign({ cache: "no-store" }, options)).then(function (response) {
|
|
@@ -172,6 +201,8 @@ window.__ModuleLoader__.load({
|
|
|
172
201
|
|
|
173
202
|
function MemoryCanvas(props) {
|
|
174
203
|
var canvas = React.useRef(null);
|
|
204
|
+
var appearance = React.useRef(null);
|
|
205
|
+
appearance.current = { graphTheme: props.graphTheme || "classic", graphLayout: props.graphLayout || "sphere" };
|
|
175
206
|
var dataState = React.useState(null);
|
|
176
207
|
var data = dataState[0];
|
|
177
208
|
var setData = dataState[1];
|
|
@@ -188,7 +219,7 @@ window.__ModuleLoader__.load({
|
|
|
188
219
|
setError("");
|
|
189
220
|
readApi("/memory-engine/api/map", controller.signal).then(function (nextData) {
|
|
190
221
|
if (!active) return;
|
|
191
|
-
renderer = createRenderer(canvas.current, nextData, { frameInterval: 25 });
|
|
222
|
+
renderer = createRenderer(canvas.current, nextData, Object.assign({ frameInterval: 25, layoutNodes: layoutPositions }, appearance.current));
|
|
192
223
|
props.rendererRef.current = renderer;
|
|
193
224
|
renderer.onSelect(function (node) { props.selectionVersion.current++; props.onSelect(node); });
|
|
194
225
|
renderer.start();
|
|
@@ -207,6 +238,11 @@ window.__ModuleLoader__.load({
|
|
|
207
238
|
if (props.rendererRef.current === renderer) props.rendererRef.current = null;
|
|
208
239
|
};
|
|
209
240
|
}, [retry, props.rendererRef, props.selectionVersion, props.onSelect]);
|
|
241
|
+
React.useEffect(function () {
|
|
242
|
+
if (!props.rendererRef.current) return;
|
|
243
|
+
props.rendererRef.current.setTheme(appearance.current.graphTheme);
|
|
244
|
+
props.rendererRef.current.setLayout(appearance.current.graphLayout);
|
|
245
|
+
}, [props.graphTheme, props.graphLayout]);
|
|
210
246
|
return React.createElement(React.Fragment, null,
|
|
211
247
|
React.createElement("canvas", {
|
|
212
248
|
ref: canvas, "data-memory-engine-canvas": "true", "aria-label": "记忆神经网络",
|
|
@@ -214,21 +250,21 @@ window.__ModuleLoader__.load({
|
|
|
214
250
|
}),
|
|
215
251
|
React.createElement("div", {
|
|
216
252
|
"data-memory-engine-summary": "true",
|
|
217
|
-
style: { position: "absolute", top: "16px", left: "18px", pointerEvents: "none", color: "
|
|
218
|
-
}, React.createElement("strong", { style: { color: "
|
|
253
|
+
style: { position: "absolute", top: "16px", left: "18px", pointerEvents: "none", color: "var(--memory-text,#d8e9f8)", font: "13px/1.65 system-ui,sans-serif", textShadow: "var(--memory-shadow,0 0 12px #3388bb88)" },
|
|
254
|
+
}, React.createElement("strong", { style: { color: "var(--memory-accent,#a4e6ff)", fontSize: "16px" } }, "记忆神经网络"),
|
|
219
255
|
React.createElement("div", null, data ? data.nodes.length + " 个神经元 · " + data.links.length + " 条突触" : "正在加载图谱……"),
|
|
220
|
-
React.createElement("div", { style: { color: "
|
|
256
|
+
React.createElement("div", { style: { color: "var(--memory-muted,#7491a8)", fontSize: "12px" } }, "大小=重要度和连接数 · 亮度=留存")),
|
|
221
257
|
data ? React.createElement("section", {
|
|
222
258
|
"aria-label": "复习清单",
|
|
223
|
-
style: { position: "absolute", bottom: "16px", left: "18px", width: "min(400px,calc(100% - 36px))", boxSizing: "border-box", maxHeight: "32%", overflow: "auto", padding: "15px", background: "
|
|
224
|
-
}, React.createElement("h2", { style: { fontSize: "14px", color: "
|
|
259
|
+
style: { position: "absolute", bottom: "16px", left: "18px", width: "min(400px,calc(100% - 36px))", boxSizing: "border-box", maxHeight: "32%", overflow: "auto", padding: "15px", background: "var(--memory-panel,#07111fee)", border: "1px solid var(--memory-border,#3976a055)", borderRadius: "14px", boxShadow: "var(--memory-shadow,0 0 40px #1763a033)", backdropFilter: "blur(8px)", font: "12px/1.65 system-ui,sans-serif", color: "var(--memory-text,#b1c6d9)" },
|
|
260
|
+
}, React.createElement("h2", { style: { fontSize: "14px", color: "var(--memory-accent,#ffdb82)", margin: "0 0 8px" } }, "复习清单 · 留存最低的记忆"),
|
|
225
261
|
data.review.map(function (item, index) {
|
|
226
262
|
return React.createElement("div", { key: item.id || index, style: { display: "flex", gap: "7px", alignItems: "center", margin: "5px 0" } },
|
|
227
263
|
React.createElement("span", { style: { flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, (index + 1) + ". [" + item.kind + "] " + item.title),
|
|
228
|
-
React.createElement("span", { style: { width: "52px", height: "5px", borderRadius: "5px", background: "
|
|
229
|
-
React.createElement("i", { style: { display: "block", height: "100%", background: "
|
|
264
|
+
React.createElement("span", { style: { width: "52px", height: "5px", borderRadius: "5px", background: "var(--memory-border,#335066)", overflow: "hidden" } },
|
|
265
|
+
React.createElement("i", { style: { display: "block", height: "100%", background: "var(--memory-accent,#6bc9fa)", width: Math.min(100, Math.max(0, item.retention * 100)) + "%" } })));
|
|
230
266
|
})) : null,
|
|
231
|
-
React.createElement("div", { style: { position: "absolute", right: "18px", bottom: "18px", pointerEvents: "none", color: "
|
|
267
|
+
React.createElement("div", { style: { position: "absolute", right: "18px", bottom: "18px", pointerEvents: "none", color: "var(--memory-muted,#7b99ad)", font: "13px system-ui,sans-serif" } }, "拖拽旋转 · 滚轮缩放 · 点光点看关联"),
|
|
232
268
|
error ? React.createElement("div", { role: "alert", style: { position: "absolute", inset: "40% 20% auto", textAlign: "center", color: "#d8e9f8" } },
|
|
233
269
|
React.createElement("p", null, "图谱加载失败:" + error),
|
|
234
270
|
React.createElement("button", { type: "button", style: actionStyle, onClick: function () { setRetry(function (value) { return value + 1; }); } }, "重试")) : null);
|
|
@@ -516,6 +552,9 @@ window.__ModuleLoader__.load({
|
|
|
516
552
|
React.createElement("span", null, props.settings.autoImport.message), " ",
|
|
517
553
|
React.createElement("button", { type: "button", "data-memory-engine-use-auto-import": "true", disabled: props.configBusy,
|
|
518
554
|
onClick: props.onUseAutoImport, style: actionStyle }, "改用宽松模式")) : null,
|
|
555
|
+
React.createElement("section", { style: { marginBottom: "24px", padding: "20px", border: "1px solid var(--memory-border,#263f54)", borderRadius: "10px", background: "var(--memory-panel,#0b1624)" } },
|
|
556
|
+
React.createElement("h2", { style: { margin: "0 0 12px", fontSize: "20px", color: "var(--memory-accent,#b7e9ff)" } }, "图谱外观"),
|
|
557
|
+
React.createElement(AppearanceControls, { settings: props.settings, busy: props.configBusy, onChange: props.onAppearanceChange, location: "settings" })),
|
|
519
558
|
React.createElement("section", { style: { marginBottom: "24px", paddingBottom: "20px", borderBottom: "1px solid #263f54" } },
|
|
520
559
|
React.createElement("h2", { style: { margin: "0 0 12px", fontSize: "20px", color: "#b7e9ff" } }, "自动记忆"),
|
|
521
560
|
React.createElement(AutoMemoryToggle, { settings: props.settings, busy: props.configBusy, onToggle: props.onToggle, location: "settings" }),
|
|
@@ -580,6 +619,21 @@ window.__ModuleLoader__.load({
|
|
|
580
619
|
var pointer = React.useRef(null);
|
|
581
620
|
var help = React.useRef(null);
|
|
582
621
|
var dismissed = React.useRef(false);
|
|
622
|
+
var appearanceState = React.useState(false);
|
|
623
|
+
var appearanceOpen = appearanceState[0], setAppearanceOpen = appearanceState[1];
|
|
624
|
+
var appearancePanel = React.useRef(null);
|
|
625
|
+
var appearanceButton = React.useRef(null);
|
|
626
|
+
React.useEffect(function () {
|
|
627
|
+
if (!open || !appearanceOpen) return undefined;
|
|
628
|
+
var first = appearancePanel.current && appearancePanel.current.querySelector("input:checked");
|
|
629
|
+
if (first) first.focus();
|
|
630
|
+
function outside(event) {
|
|
631
|
+
if (appearancePanel.current && !appearancePanel.current.contains(event.target)
|
|
632
|
+
&& appearanceButton.current && !appearanceButton.current.contains(event.target)) setAppearanceOpen(false);
|
|
633
|
+
}
|
|
634
|
+
window.addEventListener("pointerdown", outside);
|
|
635
|
+
return function () { window.removeEventListener("pointerdown", outside); };
|
|
636
|
+
}, [open, appearanceOpen]);
|
|
583
637
|
React.useEffect(function () {
|
|
584
638
|
return function () { if (configRequest.current) configRequest.current.abort(); };
|
|
585
639
|
}, []);
|
|
@@ -594,6 +648,23 @@ window.__ModuleLoader__.load({
|
|
|
594
648
|
}).finally(function () { if (!controller.signal.aborted) setConfigBusy(false); });
|
|
595
649
|
return function () { controller.abort(); };
|
|
596
650
|
}, [open]);
|
|
651
|
+
function saveAppearance(changes) {
|
|
652
|
+
if (configBusy || configRequest.current) return;
|
|
653
|
+
if (Object.keys(changes).every(function (key) { return settings[key] === changes[key]; })) return;
|
|
654
|
+
var previous = settings;
|
|
655
|
+
var controller = new AbortController();
|
|
656
|
+
var timer = setTimeout(function () { controller.abort(); }, 10000);
|
|
657
|
+
configRequest.current = controller;
|
|
658
|
+
setConfigBusy(true); setConfigError("");
|
|
659
|
+
setSettings(Object.assign({}, settings, changes));
|
|
660
|
+
requestApi("/memory-engine/api/config", { method: "PUT", headers: { "Content-Type": "application/json" },
|
|
661
|
+
body: JSON.stringify(changes), signal: controller.signal }).then(function (data) {
|
|
662
|
+
if (!controller.signal.aborted) setSettings(Object.assign({}, defaultSettings, data));
|
|
663
|
+
}).catch(function (failure) {
|
|
664
|
+
setSettings(previous);
|
|
665
|
+
setConfigError("外观保存失败,已恢复原设置:" + (controller.signal.aborted ? "请求超时或已中断,请刷新确认状态" : failure.message));
|
|
666
|
+
}).finally(function () { clearTimeout(timer); configRequest.current = null; setConfigBusy(false); });
|
|
667
|
+
}
|
|
597
668
|
function saveAutoMemory(enabled) {
|
|
598
669
|
if (configRequest.current) return;
|
|
599
670
|
var previous = settings;
|
|
@@ -639,12 +710,13 @@ window.__ModuleLoader__.load({
|
|
|
639
710
|
dismissed.current = false;
|
|
640
711
|
setGuideOpen(false);
|
|
641
712
|
setSelected(null);
|
|
713
|
+
setAppearanceOpen(false);
|
|
642
714
|
setTab("map");
|
|
643
715
|
setOpen(Date.now());
|
|
644
716
|
}
|
|
645
|
-
function closeMap() { setSelected(null); setCostHint(false); setOpen(false); }
|
|
717
|
+
function closeMap() { setSelected(null); setCostHint(false); setAppearanceOpen(false); setOpen(false); }
|
|
646
718
|
function startPointer(event) {
|
|
647
|
-
if (event.target.closest("[data-memory-engine-detail], [data-memory-engine-guide-layer], [data-memory-engine-controls]")) { pointer.current = null; return; }
|
|
719
|
+
if (event.target.closest("[data-memory-engine-detail], [data-memory-engine-guide-layer], [data-memory-engine-controls], [data-memory-engine-appearance-panel]")) { pointer.current = null; return; }
|
|
648
720
|
pointer.current = { x: event.clientX, y: event.clientY, moved: 0, version: selectionVersion.current };
|
|
649
721
|
}
|
|
650
722
|
function movePointer(event) {
|
|
@@ -677,7 +749,12 @@ window.__ModuleLoader__.load({
|
|
|
677
749
|
}, [open, retry, settings.showGuideOnEmpty]);
|
|
678
750
|
React.useEffect(function () {
|
|
679
751
|
if (!open) return undefined;
|
|
680
|
-
function onEscape() {
|
|
752
|
+
function onEscape() {
|
|
753
|
+
if (guideOpen) closeGuide();
|
|
754
|
+
else if (appearanceOpen) { setAppearanceOpen(false); if (appearanceButton.current) appearanceButton.current.focus(); }
|
|
755
|
+
else if (selected) setSelected(null);
|
|
756
|
+
else closeMap();
|
|
757
|
+
}
|
|
681
758
|
function onKey(event) {
|
|
682
759
|
if (event.key !== "Escape") return;
|
|
683
760
|
if (event.target && event.target.closest && event.target.closest("[data-memory-engine-confirm]")) return;
|
|
@@ -687,7 +764,7 @@ window.__ModuleLoader__.load({
|
|
|
687
764
|
}
|
|
688
765
|
window.addEventListener("keydown", onKey, true);
|
|
689
766
|
return function () { window.removeEventListener("keydown", onKey, true); };
|
|
690
|
-
}, [open, guideOpen, selected]);
|
|
767
|
+
}, [open, guideOpen, selected, appearanceOpen]);
|
|
691
768
|
var button = React.createElement("button", {
|
|
692
769
|
type: "button",
|
|
693
770
|
onClick: openMap,
|
|
@@ -701,12 +778,15 @@ window.__ModuleLoader__.load({
|
|
|
701
778
|
role: "dialog",
|
|
702
779
|
"aria-modal": "true",
|
|
703
780
|
"aria-label": "记忆图谱",
|
|
781
|
+
"data-memory-engine-theme": settings.graphTheme, "data-memory-engine-layout": settings.graphLayout,
|
|
704
782
|
onPointerDownCapture: startPointer, onPointerMoveCapture: movePointer,
|
|
705
783
|
onPointerCancelCapture: function () { pointer.current = null; }, onClick: dismissOutside,
|
|
706
|
-
style: { position: "fixed", inset: 0, maxWidth: "none", maxHeight: "none", zIndex: 9999, background: "#03060c" },
|
|
707
|
-
}, tab === "map" ? React.createElement(MemoryCanvas, { key: open, rendererRef: renderer, selectionVersion: selectionVersion, onSelect: setSelected
|
|
784
|
+
style: Object.assign({ position: "fixed", inset: 0, maxWidth: "none", maxHeight: "none", zIndex: 9999, background: "#03060c", colorScheme: settings.graphTheme === "paper" ? "light" : "dark" }, appearanceStyles[settings.graphTheme]),
|
|
785
|
+
}, tab === "map" ? React.createElement(MemoryCanvas, { key: open, rendererRef: renderer, selectionVersion: selectionVersion, onSelect: setSelected,
|
|
786
|
+
graphTheme: settings.graphTheme, graphLayout: settings.graphLayout })
|
|
708
787
|
: tab === "memories" ? React.createElement(MemoryList, { onChanged: function () { setRetry(function (value) { return value + 1; }); } })
|
|
709
788
|
: React.createElement(MemorySettings, { settings: settings, configBusy: configBusy, onToggle: toggleAutoMemory, onUseAutoImport: useAutoImport,
|
|
789
|
+
onAppearanceChange: saveAppearance,
|
|
710
790
|
onCleared: function () { openMap(); setGuideOpen(true); } }),
|
|
711
791
|
tab !== "map" ? React.createElement("strong", { style: { position: "absolute", top: "23px", left: "24px", font: "600 18px system-ui,sans-serif", color: "#b7e9ff" } }, "记忆管理") : null,
|
|
712
792
|
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}}"),
|
|
@@ -716,9 +796,11 @@ window.__ModuleLoader__.load({
|
|
|
716
796
|
}, React.createElement("div", { role: "tablist", "aria-label": "记忆面板", style: { display: "flex", gap: "4px", marginRight: "8px" } },
|
|
717
797
|
[["map", "图谱"], ["memories", "记忆"], ["settings", "设置"]].map(function (entry) {
|
|
718
798
|
return React.createElement("button", { key: entry[0], type: "button", role: "tab", "aria-selected": tab === entry[0], "data-memory-engine-tab": entry[0],
|
|
719
|
-
onClick: function () { setSelected(null); setTab(entry[0]); },
|
|
720
|
-
style: Object.assign({}, actionStyle, { background: tab === entry[0] ? "
|
|
721
|
-
})), React.createElement(
|
|
799
|
+
onClick: function () { setSelected(null); setAppearanceOpen(false); setTab(entry[0]); },
|
|
800
|
+
style: Object.assign({}, actionStyle, { background: tab === entry[0] ? "var(--memory-button,#204762)" : "var(--memory-panel,#091422)", borderColor: tab === entry[0] ? "var(--memory-accent,#8acded)" : "var(--memory-border,#35536b)" }) }, entry[1]);
|
|
801
|
+
})), React.createElement("button", { ref: appearanceButton, type: "button", "data-memory-engine-appearance": "true", "aria-expanded": appearanceOpen,
|
|
802
|
+
"aria-haspopup": "dialog", onClick: function () { setAppearanceOpen(!appearanceOpen); }, style: actionStyle }, "外观"),
|
|
803
|
+
React.createElement(AutoMemoryToggle, { settings: settings, busy: configBusy, onToggle: toggleAutoMemory, location: "header" }), React.createElement("button", {
|
|
722
804
|
ref: help, type: "button", onClick: function () { setGuideOpen(true); },
|
|
723
805
|
"aria-label": "查看使用指南", title: "使用指南", style: actionStyle,
|
|
724
806
|
}, "?"), React.createElement("button", {
|
|
@@ -726,8 +808,15 @@ window.__ModuleLoader__.load({
|
|
|
726
808
|
onClick: closeMap,
|
|
727
809
|
"aria-label": "关闭记忆图谱",
|
|
728
810
|
style: actionStyle,
|
|
729
|
-
}, "关闭 ×")),
|
|
730
|
-
style: { position: "absolute", top: "
|
|
811
|
+
}, "关闭 ×")), appearanceOpen ? React.createElement("section", { ref: appearancePanel, role: "dialog", "aria-label": "外观", "data-memory-engine-appearance-panel": "true",
|
|
812
|
+
style: { position: "absolute", top: "62px", right: "18px", zIndex: 3, boxSizing: "border-box", width: "min(380px,calc(100vw - 36px))", maxHeight: "calc(100vh - 90px)", overflowY: "auto",
|
|
813
|
+
padding: "18px", border: "1px solid var(--memory-border,#3976a0)", borderRadius: "12px", background: "var(--memory-panel,#07111f)", boxShadow: "0 12px 35px #0004" } },
|
|
814
|
+
React.createElement("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "14px", color: "var(--memory-text,#dbeaf8)" } },
|
|
815
|
+
React.createElement("strong", null, "外观"), React.createElement("button", { type: "button", "aria-label": "关闭外观", style: actionStyle,
|
|
816
|
+
onClick: function () { setAppearanceOpen(false); if (appearanceButton.current) appearanceButton.current.focus(); } }, "×")),
|
|
817
|
+
React.createElement(AppearanceControls, { settings: settings, busy: configBusy, onChange: saveAppearance, location: "header" })) : null,
|
|
818
|
+
configError ? React.createElement("p", { role: "alert", "data-memory-engine-config-error": "true",
|
|
819
|
+
style: { position: "absolute", top: "50px", left: "18px", zIndex: 4, color: "#ff9c9c", background: "#271923", padding: "8px 12px", borderRadius: "8px", maxWidth: "min(600px,90vw)" } }, configError) : null,
|
|
731
820
|
costHint ? React.createElement(ConfirmationDialog, { title: "开启自动记忆", message: "自动记忆会在每轮对话后调用一次模型,大约每轮几分钱。可以随时在这里关掉。实际费用以模型及计费方案为准。",
|
|
732
821
|
submitLabel: "知道了,开启", busy: configBusy, onClose: function () { setCostHint(false); }, onConfirm: function () { saveAutoMemory(true); } }) : null,
|
|
733
822
|
selected ? React.createElement(MemoryDetails, {
|
package/lib/config-write.js
CHANGED
|
@@ -15,6 +15,8 @@ const writable = new Map([
|
|
|
15
15
|
['autoMemory.maxPerTurn', value => Number.isSafeInteger(value) && value >= 1 && value <= 5],
|
|
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
|
+
['graphTheme', value => ['classic', 'neon', 'paper', 'mono'].includes(value)],
|
|
19
|
+
['graphLayout', value => ['sphere', 'flat', 'spiral'].includes(value)],
|
|
18
20
|
['showGuideOnEmpty', value => typeof value === 'boolean'],
|
|
19
21
|
['hideButton', value => typeof value === 'boolean'],
|
|
20
22
|
]);
|
package/lib/config.js
CHANGED
|
@@ -14,6 +14,7 @@ export function readUiConfig(home = defaultHome(), options = {}) {
|
|
|
14
14
|
const config = readConfig(home);
|
|
15
15
|
return { hideButton: config.hideButton === true, showGuideOnEmpty: config.showGuideOnEmpty !== false,
|
|
16
16
|
hasPersona: readPersona(config, options) !== null, personaTitle: config.personaTitle, spinSpeed: config.spinSpeed,
|
|
17
|
+
graphTheme: config.graphTheme, graphLayout: config.graphLayout,
|
|
17
18
|
autoMemory: { enabled: config.autoMemory.enabled, maxPerTurn: config.autoMemory.maxPerTurn, dailyCap: config.autoMemory.dailyCap },
|
|
18
19
|
showAutoMemoryCostHint: config.showAutoMemoryCostHint !== false,
|
|
19
20
|
autoImport: { enabled: config.autoImport.enabled, sourceCount: config.autoImport.sources.length },
|
|
@@ -52,9 +53,13 @@ export function readConfig(home = defaultHome(), { warn } = {}) {
|
|
|
52
53
|
if (value.spinSpeed !== undefined && (!Number.isFinite(value.spinSpeed) || value.spinSpeed < 0.05 || value.spinSpeed > 1.5)) {
|
|
53
54
|
throw new Error('spinSpeed 必须是 0.05~1.5 的数字');
|
|
54
55
|
}
|
|
55
|
-
|
|
56
|
+
for (const [key, choices] of [['graphTheme', ['classic', 'neon', 'paper', 'mono']], ['graphLayout', ['sphere', 'flat', 'spiral']]]) {
|
|
57
|
+
if (value[key] !== undefined && !choices.includes(value[key])) throw new Error(`${key} 必须是 ${choices.join('、')}`);
|
|
58
|
+
}
|
|
59
|
+
return { ...value, personaTitle: value.personaTitle?.trim() || '角色卡', spinSpeed: value.spinSpeed ?? 0.35,
|
|
60
|
+
graphTheme: value.graphTheme ?? 'classic', graphLayout: value.graphLayout ?? 'sphere', ...automationConfig(value, warn) };
|
|
56
61
|
} catch (error) {
|
|
57
|
-
if (error.code === 'ENOENT') return { personaTitle: '角色卡', spinSpeed: 0.35, ...automationConfig({}, warn) };
|
|
62
|
+
if (error.code === 'ENOENT') return { personaTitle: '角色卡', spinSpeed: 0.35, graphTheme: 'classic', graphLayout: 'sphere', ...automationConfig({}, warn) };
|
|
58
63
|
throw new Error(`无法读取 ${file}:${error.message}`);
|
|
59
64
|
}
|
|
60
65
|
}
|
package/lib/index.js
CHANGED
|
@@ -83,13 +83,15 @@ export function createTools(defineTool, store = new MemoryStore(), { map = gener
|
|
|
83
83
|
name: 'memory_forget',
|
|
84
84
|
description: '删除本地记忆。id/q/older_than_days 三选一。q 批量删除先报告数量,用户确认后带 confirm=true 再调用。',
|
|
85
85
|
parameters: {
|
|
86
|
-
|
|
86
|
+
memoryId: { type: 'string', description: '精确删除的记忆 id' },
|
|
87
87
|
q: { type: 'string', description: '按关键词批量删除,必须二次确认' },
|
|
88
88
|
older_than_days: { type: 'number', description: '仅删除超过此天数且留存低于 0.15 的条目' },
|
|
89
89
|
confirm: { type: 'boolean', description: '收到用户对本次关键词批删的确认后才设为 true' },
|
|
90
90
|
}, output,
|
|
91
91
|
async execute(args) {
|
|
92
|
-
|
|
92
|
+
// id 是 JSON Schema 保留字(draft-04),宿主不转换参数表时会被拒;对外叫 memoryId,内部接受旧的 id。
|
|
93
|
+
if (args.memoryId === undefined && args.id !== undefined) args.memoryId = args.id;
|
|
94
|
+
const result = store.forget({ ...args, id: args.memoryId ?? args.id });
|
|
93
95
|
return result.needsConfirmation ? `匹配到 ${result.count} 条,确认请带 confirm=true 再调一次。`
|
|
94
96
|
: `已删除 ${result.count} 条记忆。`;
|
|
95
97
|
},
|
|
@@ -138,11 +140,13 @@ export function createTools(defineTool, store = new MemoryStore(), { map = gener
|
|
|
138
140
|
name: 'memory_map', description: '把当前本地记忆生成可离线打开的神经网络 HTML 图谱,返回文件路径、节点和关联边数量。',
|
|
139
141
|
parameters: {
|
|
140
142
|
out: { type: 'string', description: '输出 HTML 路径,默认 DSH_HOME/memory-engine/starmap.html' },
|
|
141
|
-
|
|
143
|
+
pageTitle: { type: 'string', description: '页面标题,默认“我的记忆神经网络”' },
|
|
142
144
|
open: { type: 'boolean', description: '默认 false;true 时用系统默认浏览器打开' },
|
|
143
145
|
}, output,
|
|
144
146
|
async execute(args) {
|
|
145
|
-
|
|
147
|
+
// title 是 JSON Schema 的保留关键字:宿主若不转换参数表,直接发 title 会被 DeepSeek 拒绝(400)。
|
|
148
|
+
// 所以对外叫 pageTitle,内部仍接受旧的 title 作为别名。
|
|
149
|
+
const result = map(store, { ...args, title: args.pageTitle ?? args.title });
|
|
146
150
|
if (args.open === true) await openMap(result.file);
|
|
147
151
|
return mapReport(result);
|
|
148
152
|
},
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { MemoryStore } from './store.js';
|
|
2
|
+
import { chars, titleOf } from './decay.js';
|
|
3
|
+
import { generateMap, mapReport, openMap } from './map.js';
|
|
4
|
+
import { registerMemoryRoutes } from './routes.js';
|
|
5
|
+
import { createAutoMemory } from './auto-memory.js';
|
|
6
|
+
import { createAutoImport } from './auto-import.js';
|
|
7
|
+
|
|
8
|
+
export const name = 'memory-engine';
|
|
9
|
+
export const inject = ['tools', 'webServer'];
|
|
10
|
+
|
|
11
|
+
function report(ctx, error) {
|
|
12
|
+
try {
|
|
13
|
+
const logger = typeof ctx?.logger === 'function' ? ctx.logger(name) : ctx?.logger;
|
|
14
|
+
logger?.error?.(`memory-engine: ${error?.message ?? String(error)}`);
|
|
15
|
+
} catch { /* Error reporting must not take down the host. */ }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function warn(ctx, message) {
|
|
19
|
+
try {
|
|
20
|
+
const logger = typeof ctx?.logger === 'function' ? ctx.logger(name) : ctx?.logger;
|
|
21
|
+
if (logger?.warn) logger.warn(message);
|
|
22
|
+
else console.warn(`[dsh-memory-engine] ${message}`);
|
|
23
|
+
} catch { /* Preview failures must not affect the memory tools. */ }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function createTools(defineTool, store = new MemoryStore(), { map = generateMap, warning = message => warn(null, message) } = {}) {
|
|
27
|
+
const output = { schema: { type: 'string' }, render: (_args, value) => [{ type: 'text', text: value }] };
|
|
28
|
+
return [
|
|
29
|
+
defineTool({
|
|
30
|
+
name: 'memory_save',
|
|
31
|
+
description: '把值得记住的事保存到跨会话的本地记忆。与 QQ 桥接记忆独立。重复正文自动合并。'
|
|
32
|
+
+ '该存:对方的称呼/偏好/忌讳/习惯、答应过的事、情绪特别重的瞬间、新学到的知识、当天小结。'
|
|
33
|
+
+ '不该存:寒暄、重复内容、纯事务性对话。一天三五条足够,存前可先用 memory_recall 查重。',
|
|
34
|
+
parameters: {
|
|
35
|
+
text: { type: 'string', required: true, description: '记忆正文,最多 2000 字' },
|
|
36
|
+
kind: { type: 'string', description: '分类,默认对话;可用知识/印象/日记/对话/事件或自定义' },
|
|
37
|
+
heat: { type: 'number', description: '1=热,2=温,3=冷;省略按情绪推断' },
|
|
38
|
+
who: { type: 'string', description: '跟谁有关' },
|
|
39
|
+
ts: { type: 'number', description: '事情发生的毫秒时间戳,默认现在' },
|
|
40
|
+
}, output,
|
|
41
|
+
async execute(args) {
|
|
42
|
+
const memory = store.save(args);
|
|
43
|
+
if (memory.merged) return `已合并到既有记忆(第 ${memory.merged.index} 条,相似度 ${memory.merged.similarity.toFixed(2)}):${chars(memory.text, 40)}`;
|
|
44
|
+
return `已记住:${chars(memory.text, 40)}(重要度 ${['', '热', '温', '冷'][memory.heat]})`;
|
|
45
|
+
},
|
|
46
|
+
}),
|
|
47
|
+
defineTool({
|
|
48
|
+
name: 'memory_recall',
|
|
49
|
+
description: '按话题查跨会话的本地记忆,并联想相关条目。q 使用具体关键词,如「手冲」「阅读 书签」。与 QQ 桥接记忆独立。',
|
|
50
|
+
parameters: {
|
|
51
|
+
q: { type: 'string', required: true, description: '要查的话题或关键词' },
|
|
52
|
+
limit: { type: 'number', description: '默认 8,最大 20' },
|
|
53
|
+
hops: { type: 'number', description: '联想跳数,默认 1;0 只查直接命中,最大 2' },
|
|
54
|
+
kind: { type: 'string', description: '只看某一分类' },
|
|
55
|
+
}, output,
|
|
56
|
+
async execute(args) {
|
|
57
|
+
const hits = store.recall(args);
|
|
58
|
+
if (!hits.length) return `【没有找到】「${args.q}」在记忆库里没有相关内容。`;
|
|
59
|
+
const quote = words => words.map(word => `「${word}」`).join('');
|
|
60
|
+
let index = 0;
|
|
61
|
+
const describe = hit => `${++index}. [${hit.kind}] ${titleOf(hit)}(留存 ${hit.retention.toFixed(2)};id=${hit.id})`;
|
|
62
|
+
const sections = [];
|
|
63
|
+
for (const via of ['direct', 'partial']) {
|
|
64
|
+
const group = hits.filter(hit => hit.via === via);
|
|
65
|
+
if (!group.length) continue;
|
|
66
|
+
const terms = group[0].terms;
|
|
67
|
+
const matched = terms.filter(term => group.some(hit => hit.matched.includes(term)));
|
|
68
|
+
const missed = terms.filter(term => !matched.includes(term));
|
|
69
|
+
const label = via === 'direct' ? '直接命中' : '部分命中';
|
|
70
|
+
const summary = `【${label} ${group.length} 条】` + (via === 'partial' ? '(覆盖不足 50%,不是直接命中)' : '')
|
|
71
|
+
+ `命中${quote(matched)}` + (missed.length ? `;未命中${quote(missed)}` : ';全部查询词均命中');
|
|
72
|
+
sections.push([summary, ...group.map(hit => `${describe(hit)}\n`
|
|
73
|
+
+ ` 覆盖 ${hit.matched.length}/${hit.terms.length};命中${quote(hit.matched)}`
|
|
74
|
+
+ (hit.missed.length ? `;未命中${quote(hit.missed)}` : '') + `\n ${hit.snippet}`)].join('\n\n'));
|
|
75
|
+
}
|
|
76
|
+
const spread = hits.filter(hit => hit.via === 'spread');
|
|
77
|
+
if (spread.length) sections.push(['【联想到的(不是直接命中)】', ...spread.map(hit => `${describe(hit)}\n`
|
|
78
|
+
+ ` 从「${hit.from.title}」(id=${hit.from.id})联想:完整标题「${hit.from.term}」出现在关联正文中。\n ${hit.snippet}`)].join('\n\n'));
|
|
79
|
+
return sections.join('\n\n');
|
|
80
|
+
},
|
|
81
|
+
}),
|
|
82
|
+
defineTool({
|
|
83
|
+
name: 'memory_forget',
|
|
84
|
+
description: '删除本地记忆。id/q/older_than_days 三选一。q 批量删除先报告数量,用户确认后带 confirm=true 再调用。',
|
|
85
|
+
parameters: {
|
|
86
|
+
id: { type: 'string', description: '精确删除的记忆 id' },
|
|
87
|
+
q: { type: 'string', description: '按关键词批量删除,必须二次确认' },
|
|
88
|
+
older_than_days: { type: 'number', description: '仅删除超过此天数且留存低于 0.15 的条目' },
|
|
89
|
+
confirm: { type: 'boolean', description: '收到用户对本次关键词批删的确认后才设为 true' },
|
|
90
|
+
}, output,
|
|
91
|
+
async execute(args) {
|
|
92
|
+
const result = store.forget(args);
|
|
93
|
+
return result.needsConfirmation ? `匹配到 ${result.count} 条,确认请带 confirm=true 再调一次。`
|
|
94
|
+
: `已删除 ${result.count} 条记忆。`;
|
|
95
|
+
},
|
|
96
|
+
}),
|
|
97
|
+
defineTool({
|
|
98
|
+
name: 'memory_dedupe', description: '检测全库相似重复记忆。默认只预览;用户确认后显式 dry_run=false 才备份并合并。',
|
|
99
|
+
parameters: {
|
|
100
|
+
threshold: { type: 'number', description: 'LCS 相似度阈值,默认 0.7,范围 0.5–0.95' },
|
|
101
|
+
dry_run: { type: 'boolean', description: '默认 true,只报告;false 备份后执行合并' },
|
|
102
|
+
}, output,
|
|
103
|
+
async execute(args) {
|
|
104
|
+
const result = store.dedupe(args);
|
|
105
|
+
if (!result.dryRun) return `已合并 ${result.groups.length} 组,删除 ${result.removed} 条。备份:${result.backup}(耗时 ${result.elapsedMs.toFixed(0)}ms)`;
|
|
106
|
+
const involved = result.groups.reduce((sum, group) => sum + group.texts.length, 0);
|
|
107
|
+
return [`发现 ${result.groups.length} 组重复(共 ${involved} 条 → 可合并为 ${result.groups.length} 条)`,
|
|
108
|
+
...result.groups.map((group, index) => `组 ${index + 1}:${group.texts.length} 条 → 保留「${chars(group.memory.text, 80)}」(boosts 合计 ${group.memory.boosts})\n`
|
|
109
|
+
+ group.texts.map(text => ` - ${chars(text, 80)}`).join('\n')),
|
|
110
|
+
'确认执行请带 dry_run=false', `耗时 ${result.elapsedMs.toFixed(0)}ms`].join('\n');
|
|
111
|
+
},
|
|
112
|
+
}),
|
|
113
|
+
defineTool({
|
|
114
|
+
name: 'memory_import', description: '从 JSON、Markdown 目录、逐行纯文本或 qq-bridge 导入本地记忆。默认只预览,显式 dry_run=false 才备份并导入;不会修改来源。新增后自动生成图谱。',
|
|
115
|
+
parameters: {
|
|
116
|
+
source: { type: 'string', required: true, description: 'qq-bridge / json / markdown / text' },
|
|
117
|
+
path: { type: 'string', description: 'json/text 的本地文件绝对路径,或 markdown 的目录绝对路径;qq-bridge 无需此参数' },
|
|
118
|
+
dry_run: { type: 'boolean', description: '默认 true,只报告;false 先备份本插件记忆再导入' },
|
|
119
|
+
}, output,
|
|
120
|
+
async execute(args) {
|
|
121
|
+
const result = args.source === 'qq-bridge' ? store.importBridge(args) : await store.importFile(args);
|
|
122
|
+
if (result.missing) return '找不到桥接目录,可用 QQ_BRIDGE_DIR 环境变量指定';
|
|
123
|
+
let mapLine;
|
|
124
|
+
if (!result.dryRun && result.imported > 0) {
|
|
125
|
+
try { mapLine = `已同步生成神经网络图:${map(store).file}`; }
|
|
126
|
+
catch (error) { warning(`图谱生成失败,记忆已成功导入:${error.message}`); }
|
|
127
|
+
}
|
|
128
|
+
return [result.dryRun ? `将从 ${args.source} 导入:` : `已从 ${args.source} 导入:`,
|
|
129
|
+
...Object.entries(result.counts).map(([kind, count]) => `- ${kind} ${count} 条(${result.dryRun ? '可新增' : '新增'} ${result.added[kind]} 条)`),
|
|
130
|
+
`合计 ${result.candidates} 条;其中 ${result.skipped} 条与现有或本批重复会被跳过。`,
|
|
131
|
+
`${result.dryRun ? '预计导入后' : '导入后'}总数:${result.total} 条(新增 ${result.imported} 条)`,
|
|
132
|
+
...(result.dryRun ? ['确认执行请带 dry_run=false'] : [`备份:${result.backup}`]),
|
|
133
|
+
...(result.warnings.length ? ['来源提示:', ...result.warnings] : []),
|
|
134
|
+
`耗时 ${(result.elapsedMs / 1000).toFixed(2)} 秒`, ...(mapLine ? [mapLine] : [])].join('\n');
|
|
135
|
+
},
|
|
136
|
+
}),
|
|
137
|
+
defineTool({
|
|
138
|
+
name: 'memory_map', description: '把当前本地记忆生成可离线打开的神经网络 HTML 图谱,返回文件路径、节点和关联边数量。',
|
|
139
|
+
parameters: {
|
|
140
|
+
out: { type: 'string', description: '输出 HTML 路径,默认 DSH_HOME/memory-engine/starmap.html' },
|
|
141
|
+
title: { type: 'string', description: '页面标题,默认“我的记忆神经网络”' },
|
|
142
|
+
open: { type: 'boolean', description: '默认 false;true 时用系统默认浏览器打开' },
|
|
143
|
+
}, output,
|
|
144
|
+
async execute(args) {
|
|
145
|
+
const result = map(store, args);
|
|
146
|
+
if (args.open === true) await openMap(result.file);
|
|
147
|
+
return mapReport(result);
|
|
148
|
+
},
|
|
149
|
+
}),
|
|
150
|
+
defineTool({
|
|
151
|
+
name: 'memory_stats', description: '查看本地记忆总数、分类、衰减状态、最老日期和存储路径及体积。',
|
|
152
|
+
parameters: {}, output,
|
|
153
|
+
async execute() {
|
|
154
|
+
const stats = store.stats();
|
|
155
|
+
const kinds = Object.entries(stats.kinds).map(([kind, count]) => `${kind} ${count}`).join(' / ');
|
|
156
|
+
const body = `记忆引擎状态\n- 总条数:${stats.total}${kinds ? `(${kinds})` : ''}\n`
|
|
157
|
+
+ `- 快忘了的(留存<0.3):${stats.fading} 条\n`
|
|
158
|
+
+ `- 最老:${stats.oldest === null ? '暂无' : `${new Date(stats.oldest).toISOString().slice(0, 10)}(${stats.ageDays} 天前)`}\n`
|
|
159
|
+
+ `- 存储:${stats.file}(${(stats.bytes / 1024).toFixed(1)} KB)`;
|
|
160
|
+
// 库是空的时,替 agent 把「接下来该做什么」说出来(见 README「装完是空库」)。
|
|
161
|
+
if (stats.total === 0) {
|
|
162
|
+
return body + '\n\n记忆库还是空的。插件不会自己读聊天记录,记忆要靠两件事之一进来:\n'
|
|
163
|
+
+ '① 在提示词里告诉 agent 该记什么,聊天时就调 memory_save(推荐)\n'
|
|
164
|
+
+ '② 用 memory_import 把旧记忆导进来(支持 json / markdown / text)\n'
|
|
165
|
+
+ '建议主动把这两条简短告诉主人,并提醒他点开图谱可以看到上手引导页。';
|
|
166
|
+
}
|
|
167
|
+
return body;
|
|
168
|
+
},
|
|
169
|
+
}),
|
|
170
|
+
];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 宿主 SDK 的 defineTool 在不同 DSH 版本间签名一致(实测 0.1.0-rc.8 ~ 0.2.2-alpha.2 都是
|
|
174
|
+
// defineTool(options)),但**包不一定能解析到**——比如插件被装在宿主 node_modules 之外。
|
|
175
|
+
// 解析失败时退回一个直通实现:工具照常注册,只是少了宿主的返回值包装。
|
|
176
|
+
async function resolveDefineTool(warning) {
|
|
177
|
+
try {
|
|
178
|
+
const sdk = await import('@deepseek-ai/dsh-tools');
|
|
179
|
+
if (typeof sdk.defineTool === 'function') {
|
|
180
|
+
const version = sdk.version ?? '';
|
|
181
|
+
if (version) warning(`宿主 dsh-tools 版本 ${version}`);
|
|
182
|
+
return sdk.defineTool;
|
|
183
|
+
}
|
|
184
|
+
warning('@deepseek-ai/dsh-tools 没有导出 defineTool,改用内置直通实现');
|
|
185
|
+
} catch (error) {
|
|
186
|
+
warning(`解析 @deepseek-ai/dsh-tools 失败(${error.message}),改用内置直通实现`);
|
|
187
|
+
}
|
|
188
|
+
return options => options;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function apply(ctx) {
|
|
192
|
+
try {
|
|
193
|
+
if (typeof ctx?.tools?.register !== 'function') throw new Error('tools 服务不可用');
|
|
194
|
+
// ctx.effect 是较新宿主才有的生命周期钩子。没有它就直接注册——
|
|
195
|
+
// 否则老版本 DSH 上插件会「装上了但一个工具都没有」,这是最糟的失败方式。
|
|
196
|
+
const setup = async () => {
|
|
197
|
+
const disposers = [];
|
|
198
|
+
try {
|
|
199
|
+
const defineTool = await resolveDefineTool(message => warn(ctx, message));
|
|
200
|
+
const store = new MemoryStore();
|
|
201
|
+
for (const tool of createTools(defineTool, store, { warning: message => warn(ctx, message) })) disposers.push(ctx.tools.register(tool));
|
|
202
|
+
try {
|
|
203
|
+
disposers.push(registerMemoryRoutes(ctx.webServer, { store, warn: message => warn(ctx, message),
|
|
204
|
+
listSessions: () => ctx.get('sessionQuery')?.listSessions() }));
|
|
205
|
+
} catch (error) {
|
|
206
|
+
warn(ctx, `图谱路由注册失败,记忆工具仍可用:${error.message}`);
|
|
207
|
+
}
|
|
208
|
+
for (const create of [createAutoMemory, createAutoImport]) {
|
|
209
|
+
try {
|
|
210
|
+
const automation = create({ ctx, store, warn: message => warn(ctx, message) });
|
|
211
|
+
disposers.push(() => automation.dispose());
|
|
212
|
+
} catch (error) { warn(ctx, `自动任务初始化失败,记忆工具仍可用:${error.message}`); }
|
|
213
|
+
}
|
|
214
|
+
const timer = setTimeout(() => {
|
|
215
|
+
try { if (store.read().length > 0) generateMap(store); }
|
|
216
|
+
catch (error) { warn(ctx, `启动时图谱生成失败:${error.message}`); }
|
|
217
|
+
}, 3000);
|
|
218
|
+
timer.unref();
|
|
219
|
+
disposers.push(() => clearTimeout(timer));
|
|
220
|
+
return () => { for (const dispose of disposers.reverse()) dispose(); };
|
|
221
|
+
} catch (error) {
|
|
222
|
+
for (const dispose of disposers.reverse()) { try { dispose(); } catch { /* keep unwinding */ } }
|
|
223
|
+
report(ctx, error);
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
if (typeof ctx.effect === 'function') {
|
|
228
|
+
const registration = ctx.effect(setup, 'memory-engine.tools');
|
|
229
|
+
// Await setup without returning its disposer as a second plugin teardown.
|
|
230
|
+
return Promise.resolve(registration).then(() => undefined, error => { report(ctx, error); });
|
|
231
|
+
}
|
|
232
|
+
warn(ctx, '宿主没有 ctx.effect,直接用注册式初始化(功能不减少)');
|
|
233
|
+
return Promise.resolve(setup()).then(() => undefined, error => { report(ctx, error); });
|
|
234
|
+
} catch (error) { report(ctx, error); }
|
|
235
|
+
}
|
package/lib/map.js
CHANGED
|
@@ -21,6 +21,7 @@ export function computeMapData(store, { warn } = {}) {
|
|
|
21
21
|
return { generatedAt: now, nodes, links: scored.links, review: scored.review,
|
|
22
22
|
core: core ? { id: core.id, title: core.title, preview: Array.from(core.text).slice(0, 120).join('') + (Array.from(core.text).length > 120 ? '…' : '') } : null,
|
|
23
23
|
spinSpeed: config.spinSpeed,
|
|
24
|
+
graphTheme: config.graphTheme, graphLayout: config.graphLayout,
|
|
24
25
|
stats: { total: nodes.length, byKind: kinds, forgettingSoon: nodes.filter(node => node.retention < 0.3).length } };
|
|
25
26
|
}
|
|
26
27
|
|