dsh-memory-engine 0.7.0-beta.4 → 0.7.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/LICENSE +1 -1
- package/README.md +83 -41
- package/lib/auto-import.js +227 -0
- package/lib/auto-log.js +30 -0
- package/lib/auto-memory.js +186 -0
- package/lib/auto-sources.js +106 -0
- package/lib/automation-config.js +66 -0
- package/lib/automation-state.js +43 -0
- package/lib/classify.js +11 -0
- package/lib/client.js +154 -50
- package/lib/client.src.js +73 -8
- package/lib/config-write.js +74 -0
- package/lib/config.js +10 -4
- package/lib/dedupe.js +2 -2
- package/lib/file-import.js +4 -3
- package/lib/import.js +8 -5
- package/lib/index.js +29 -7
- package/lib/noise.js +20 -0
- package/lib/recall.js +67 -28
- package/lib/render/canvas.js +81 -42
- package/lib/render/render.mjs +1 -1
- package/lib/routes.js +9 -3
- package/lib/session-text.js +44 -0
- package/lib/store.js +2 -2
- package/package.json +6 -3
package/lib/client.js
CHANGED
|
@@ -22,15 +22,18 @@ window.__ModuleLoader__.load({
|
|
|
22
22
|
if (!ctx) throw new Error('Canvas 2D 不可用');
|
|
23
23
|
const colors = { '人格':[255,215,106], '印象':[143,216,255], '知识':[126,231,178], '日记':[190,160,255], '对话':[255,160,140], '情绪':[255,90,90] };
|
|
24
24
|
const fallback = [145,205,255];
|
|
25
|
-
let nodes = [], links = [], hubs = [], linkTwinkle = [];
|
|
25
|
+
let nodes = [], links = [], hubs = [], linkTwinkle = [], visibleLinks = [];
|
|
26
26
|
const byId = new Map(), adjacency = new Map(), sprites = new Map();
|
|
27
27
|
const pulses = [], firing = new Map(), selections = new Set();
|
|
28
|
+
// 默认不画连线(数量太大会卡),只画选中光点周围的线
|
|
29
|
+
let selectedId=null, selectedNeighbours=new Set();
|
|
28
30
|
if (typeof options.onSelect === 'function') selections.add(options.onSelect);
|
|
29
31
|
let seed = 987654321;
|
|
30
32
|
const random = () => ((seed = seed * 16807 % 2147483647) - 1) / 2147483646;
|
|
31
33
|
let width=0,height=0,dpr=1,rotX=0.3,rotY=0.15,zoom=data.nodes.length<50?1.6:1,autoSpin=true;
|
|
32
34
|
let spinSpeed=data.spinSpeed??0.35,resumeAt=0;
|
|
33
|
-
|
|
35
|
+
const frameInterval=Number.isFinite(options.frameInterval)?Math.max(0,options.frameInterval):0;
|
|
36
|
+
let drag=null,nextFire=0,lastTime=0,lastDrawAt=0,rafId=null,running=false,destroyed=false,generation=0,observer=null;
|
|
34
37
|
|
|
35
38
|
function glowSprite(rgb) {
|
|
36
39
|
const off = document.createElement('canvas');
|
|
@@ -62,6 +65,8 @@ function glowSprite(rgb) {
|
|
|
62
65
|
nodes = nextData.nodes.map(node => ({ ...node, pos: { ...node.pos } }));
|
|
63
66
|
links = nextData.links.map(link => [...link]);
|
|
64
67
|
byId.clear(); adjacency.clear(); sprites.clear(); pulses.length = 0; firing.clear();
|
|
68
|
+
selectedId=typeof options.selectedId==='string'?options.selectedId:null;
|
|
69
|
+
selectedNeighbours=new Set(selectedId?(adjacency.get(selectedId)||[]):[]);
|
|
65
70
|
seed = 987654321; nextFire = 0;
|
|
66
71
|
for (const node of nextData.nodes) byId.set(node.id, node);
|
|
67
72
|
for (const node of nodes) adjacency.set(node.id, []);
|
|
@@ -87,6 +92,23 @@ function glowSprite(rgb) {
|
|
|
87
92
|
jitterEnd: ((Math.floor(h / 977) % 100) / 100 - 0.5) * 1.6,
|
|
88
93
|
};
|
|
89
94
|
});
|
|
95
|
+
// 连线数量会随记忆增长爆炸(332 条时 383 根,479 条时 1738 根)。
|
|
96
|
+
// 每帧全画会卡,所以按"两端留存"排序,最多画 LINK_DRAW_CAP 根最亮的,
|
|
97
|
+
// 其余的不进这一帧(数据没丢,只是不画,缩放/点击仍按全量算)。
|
|
98
|
+
const LINK_DRAW_CAP = 1200;
|
|
99
|
+
if (links.length <= LINK_DRAW_CAP) {
|
|
100
|
+
visibleLinks = links.map((_, index) => index);
|
|
101
|
+
} else {
|
|
102
|
+
const byIdLocal = new Map(nodes.map(node => [node.id, node]));
|
|
103
|
+
const strength = index => {
|
|
104
|
+
const [left, right] = links[index];
|
|
105
|
+
const a = byIdLocal.get(left), b = byIdLocal.get(right);
|
|
106
|
+
return Math.min(a?.retention ?? 0, b?.retention ?? 0) + Math.min(a?.degree ?? 0, b?.degree ?? 0) * 0.002;
|
|
107
|
+
};
|
|
108
|
+
visibleLinks = links.map((_, index) => index)
|
|
109
|
+
.sort((x, y) => strength(y) - strength(x))
|
|
110
|
+
.slice(0, LINK_DRAW_CAP);
|
|
111
|
+
}
|
|
90
112
|
for (const star of nodes) {
|
|
91
113
|
star._rgb = colors[star.kind] || fallback;
|
|
92
114
|
const count = Math.min(9, 4 + Math.round(Math.sqrt(star.degree) * 1.2));
|
|
@@ -123,17 +145,19 @@ function project(pos) {
|
|
|
123
145
|
function fire(id,now) {
|
|
124
146
|
firing.set(id,1);
|
|
125
147
|
const neighbours=adjacency.get(id)||[];
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
const
|
|
148
|
+
// 选中某个光点时,把它的关联线都点亮几条脉冲(最多 24 条,避免又糊成一片);
|
|
149
|
+
// 未选中时的随机心跳只挑 2~4 条,避免整片区域同时亮。
|
|
150
|
+
const selected = id===selectedId;
|
|
151
|
+
const limit = selected ? 24 : 2+Math.floor(Math.random()*3);
|
|
152
|
+
const count=Math.min(neighbours.length,limit);
|
|
129
153
|
const picked=new Set();
|
|
130
154
|
let guard=0;
|
|
131
|
-
while (picked.size<count && neighbours.length && guard++<
|
|
155
|
+
while (picked.size<count && neighbours.length && guard++<400) {
|
|
132
156
|
picked.add(neighbours[Math.floor(Math.random()*neighbours.length)]);
|
|
133
157
|
}
|
|
134
158
|
for (const other of picked) {
|
|
135
159
|
// 邻居亮度也压低:原来 0.65 会让整片区域同时亮起来
|
|
136
|
-
firing.set(other,Math.max(0.26,firing.get(other)||0));
|
|
160
|
+
firing.set(other,Math.max(selected?0.4:0.26,firing.get(other)||0));
|
|
137
161
|
pulses.push({from:id,to:other,born:now,life:700+Math.random()*2400});
|
|
138
162
|
}
|
|
139
163
|
}
|
|
@@ -149,6 +173,8 @@ function fire(id,now) {
|
|
|
149
173
|
if (score<bestScore) {bestScore=score;best=star;}
|
|
150
174
|
}
|
|
151
175
|
if (!best) return;
|
|
176
|
+
selectedId = best.id;
|
|
177
|
+
selectedNeighbours = new Set(adjacency.get(best.id) || []);
|
|
152
178
|
fire(best.id,performance.now());
|
|
153
179
|
for (const callback of selections) callback(byId.get(best.id));
|
|
154
180
|
}
|
|
@@ -187,10 +213,16 @@ function frame(now) {
|
|
|
187
213
|
if (!autoSpin && !drag && now>=resumeAt) autoSpin=true;
|
|
188
214
|
if (autoSpin) rotY+=0.0011*spinSpeed*3*delta/16.7;
|
|
189
215
|
if (now>nextFire) {
|
|
190
|
-
//
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
fire(
|
|
216
|
+
// 默认不画连线,所以也不撒"沿线的脉冲"——否则会出现飘来飘去的小点,找不到来源。
|
|
217
|
+
// 选中光点后,脉冲只在它的关联线上跑。
|
|
218
|
+
if (selectedId) {
|
|
219
|
+
fire(selectedId,now);
|
|
220
|
+
} else {
|
|
221
|
+
// 没选中时只让枢纽光点自己亮一下,不动线
|
|
222
|
+
if (hubs.length) {
|
|
223
|
+
const hub=hubs[Math.floor(Math.random()*hubs.length)];
|
|
224
|
+
firing.set(hub.id,1);
|
|
225
|
+
}
|
|
194
226
|
}
|
|
195
227
|
nextFire=now+1400+Math.random()*3200;
|
|
196
228
|
}
|
|
@@ -208,37 +240,37 @@ function frame(now) {
|
|
|
208
240
|
}
|
|
209
241
|
points.sort((left,right)=>left.star.isCore?1:right.star.isCore?-1:right.p.depth-left.p.depth);
|
|
210
242
|
const screen=new Map(points.map(point=>[point.star.id,point]));
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
ctx.
|
|
241
|
-
ctx.
|
|
243
|
+
// 连线只在"选中某个光点"时画,且只画和它相连的那些。
|
|
244
|
+
// 全量 1738 根每帧画会卡;默认干干净净,点谁看谁的关联。
|
|
245
|
+
const linkPaths=[];
|
|
246
|
+
if (selectedId) {
|
|
247
|
+
for (const [left,right] of links) {
|
|
248
|
+
if (left!==selectedId && right!==selectedId) continue;
|
|
249
|
+
const first=screen.get(left),second=screen.get(right);
|
|
250
|
+
if (!first||!second) continue;
|
|
251
|
+
const target = left===selectedId ? second : first;
|
|
252
|
+
const ret = Math.min(first.star.retention, second.star.retention);
|
|
253
|
+
const alpha = Math.min(.9, .22 + ret*.35);
|
|
254
|
+
const dx=second.p.x-first.p.x, dy=second.p.y-first.p.y;
|
|
255
|
+
const len=Math.hypot(dx,dy)||1;
|
|
256
|
+
const nx=-dy/len, ny=dx/len;
|
|
257
|
+
// 朝选中点那一侧弯,形成"从中心散开"的观感
|
|
258
|
+
const bow = (target===second ? 1 : -1) * .16;
|
|
259
|
+
const mx=(first.p.x+second.p.x)/2+nx*len*bow*.5;
|
|
260
|
+
const my=(first.p.y+second.p.y)/2+ny*len*bow*.5;
|
|
261
|
+
linkPaths.push([first.p.x,first.p.y,mx,my,second.p.x,second.p.y,alpha]);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (linkPaths.length) {
|
|
265
|
+
ctx.strokeStyle='rgba(110,190,255,0.30)';
|
|
266
|
+
ctx.lineWidth=1;
|
|
267
|
+
ctx.beginPath();
|
|
268
|
+
for (const p of linkPaths) { ctx.moveTo(p[0],p[1]); ctx.quadraticCurveTo(p[2],p[3],p[4],p[5]); }
|
|
269
|
+
ctx.stroke();
|
|
270
|
+
// 更亮的一层,让关联线看得清
|
|
271
|
+
ctx.strokeStyle='rgba(160,220,255,0.22)';
|
|
272
|
+
ctx.lineWidth=2;
|
|
273
|
+
ctx.stroke();
|
|
242
274
|
}
|
|
243
275
|
for (let index=pulses.length-1;index>=0;index--) {
|
|
244
276
|
const pulse=pulses[index],fraction=(now-pulse.born)/pulse.life;
|
|
@@ -323,6 +355,13 @@ function frame(now) {
|
|
|
323
355
|
function tick(now) {
|
|
324
356
|
if (!running || destroyed || currentGeneration !== generation) return;
|
|
325
357
|
rafId=null;
|
|
358
|
+
// 帧率上限靠 options.frameInterval 控制(默认 0 = 不限)。
|
|
359
|
+
// 界面里按 40fps 传;测试里不传,保持逐帧确定性。
|
|
360
|
+
if (frameInterval > 0 && now - lastDrawAt < frameInterval) {
|
|
361
|
+
rafId=window.requestAnimationFrame(tick);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
lastDrawAt = now;
|
|
326
365
|
frame(now);
|
|
327
366
|
if (running && !destroyed && currentGeneration === generation) rafId=window.requestAnimationFrame(tick);
|
|
328
367
|
}
|
|
@@ -364,7 +403,7 @@ function frame(now) {
|
|
|
364
403
|
|
|
365
404
|
|
|
366
405
|
|
|
367
|
-
var defaultSettings = { hideButton: false, showGuideOnEmpty: true };
|
|
406
|
+
var defaultSettings = { hideButton: false, showGuideOnEmpty: true, autoMemory: { enabled: false, maxPerTurn: 2, dailyCap: 40 }, showAutoMemoryCostHint: true };
|
|
368
407
|
var actionStyle = { padding: "7px 12px", borderRadius: "8px", border: "1px solid #6999b8", background: "#102236", color: "#e2f4ff", font: "inherit", cursor: "pointer" };
|
|
369
408
|
var fieldStyle = { boxSizing: "border-box", padding: "10px 12px", borderRadius: "8px", border: "1px solid #35536b", background: "#091422", color: "#e2f4ff", font: "inherit", minWidth: 0 };
|
|
370
409
|
var kinds = ["知识", "印象", "对话", "事件", "日记"];
|
|
@@ -537,7 +576,7 @@ function frame(now) {
|
|
|
537
576
|
setError("");
|
|
538
577
|
readApi("/memory-engine/api/map", controller.signal).then(function (nextData) {
|
|
539
578
|
if (!active) return;
|
|
540
|
-
renderer = createRenderer(canvas.current, nextData);
|
|
579
|
+
renderer = createRenderer(canvas.current, nextData, { frameInterval: 25 });
|
|
541
580
|
props.rendererRef.current = renderer;
|
|
542
581
|
renderer.onSelect(function (node) { props.selectionVersion.current++; props.onSelect(node); });
|
|
543
582
|
renderer.start();
|
|
@@ -577,7 +616,7 @@ function frame(now) {
|
|
|
577
616
|
React.createElement("span", { style: { width: "52px", height: "5px", borderRadius: "5px", background: "#335066", overflow: "hidden" } },
|
|
578
617
|
React.createElement("i", { style: { display: "block", height: "100%", background: "#6bc9fa", width: Math.min(100, Math.max(0, item.retention * 100)) + "%" } })));
|
|
579
618
|
})) : null,
|
|
580
|
-
React.createElement("div", { style: { position: "absolute", right: "18px", bottom: "18px", pointerEvents: "none", color: "#7b99ad", font: "13px system-ui,sans-serif" } }, "拖拽旋转 · 滚轮缩放 ·
|
|
619
|
+
React.createElement("div", { style: { position: "absolute", right: "18px", bottom: "18px", pointerEvents: "none", color: "#7b99ad", font: "13px system-ui,sans-serif" } }, "拖拽旋转 · 滚轮缩放 · 点光点看关联"),
|
|
581
620
|
error ? React.createElement("div", { role: "alert", style: { position: "absolute", inset: "40% 20% auto", textAlign: "center", color: "#d8e9f8" } },
|
|
582
621
|
React.createElement("p", null, "图谱加载失败:" + error),
|
|
583
622
|
React.createElement("button", { type: "button", style: actionStyle, onClick: function () { setRetry(function (value) { return value + 1; }); } }, "重试")) : null);
|
|
@@ -784,6 +823,17 @@ function frame(now) {
|
|
|
784
823
|
onClick: function () { setOffset(page.items.length); }, style: Object.assign({}, actionStyle, { marginLeft: "12px", opacity: loading ? 0.5 : 1 }) }, "加载更多") : null));
|
|
785
824
|
}
|
|
786
825
|
|
|
826
|
+
function AutoMemoryToggle(props) {
|
|
827
|
+
var enabled = props.settings.autoMemory && props.settings.autoMemory.enabled === true;
|
|
828
|
+
var maximum = props.settings.autoMemory && props.settings.autoMemory.maxPerTurn || 2;
|
|
829
|
+
return React.createElement("button", {
|
|
830
|
+
type: "button", role: "switch", "aria-checked": enabled, "aria-label": "自动记忆",
|
|
831
|
+
"data-memory-engine-auto-memory": props.location, disabled: props.busy, onClick: props.onToggle,
|
|
832
|
+
title: enabled ? "当前:开(每轮对话后会抽 0~" + maximum + " 条记忆)" : "当前:关(不会自动调用模型抽取记忆)",
|
|
833
|
+
style: Object.assign({}, actionStyle, { color: enabled ? "#e2f4ff" : "#8195aa", borderColor: enabled ? "#64bca0" : "#35536b", opacity: props.busy ? 0.6 : 1 }),
|
|
834
|
+
}, React.createElement("span", { "aria-hidden": "true", style: { color: enabled ? "#89f0bb" : "#8195aa", textShadow: enabled ? "0 0 8px #56d890" : "none" } }, enabled ? "●" : "○"), " 自动记忆");
|
|
835
|
+
}
|
|
836
|
+
|
|
787
837
|
function MemorySettings(props) {
|
|
788
838
|
var statsState = React.useState(null);
|
|
789
839
|
var stats = statsState[0], setStats = statsState[1];
|
|
@@ -849,6 +899,10 @@ function frame(now) {
|
|
|
849
899
|
return React.createElement("section", { "aria-label": "记忆设置", "data-memory-engine-settings": "true",
|
|
850
900
|
style: { position: "absolute", inset: "76px 0 0", overflowY: "auto", padding: "0 24px 32px", color: "#dbeaf8", font: "14px/1.7 system-ui,sans-serif" } },
|
|
851
901
|
React.createElement("div", { style: { maxWidth: "900px", margin: "0 auto", padding: "28px", border: "1px solid #263f54", borderRadius: "14px", background: "#0b1624" } },
|
|
902
|
+
React.createElement("section", { style: { marginBottom: "24px", paddingBottom: "20px", borderBottom: "1px solid #263f54" } },
|
|
903
|
+
React.createElement("h2", { style: { margin: "0 0 12px", fontSize: "20px", color: "#b7e9ff" } }, "自动记忆"),
|
|
904
|
+
React.createElement(AutoMemoryToggle, { settings: props.settings, busy: props.configBusy, onToggle: props.onToggle, location: "settings" }),
|
|
905
|
+
React.createElement("p", { style: { color: "#9fb7ce", marginBottom: 0 } }, "默认关闭。开启后每轮对话会额外调用一次模型,抽取值得记住的事实;通常最多 2 条,可随时关闭。大约每轮几分钱,实际费用以模型及计费方案为准。自动导入是独立设置,不受此开关控制。")),
|
|
852
906
|
React.createElement("h2", { style: { margin: "0 0 20px", fontSize: "20px", color: "#b7e9ff" } }, "数据管理"),
|
|
853
907
|
stats ? React.createElement(React.Fragment, null,
|
|
854
908
|
React.createElement("div", { style: { display: "flex", alignItems: "center", flexWrap: "wrap", gap: "12px" } },
|
|
@@ -875,10 +929,18 @@ function frame(now) {
|
|
|
875
929
|
}
|
|
876
930
|
|
|
877
931
|
function MemoryButton(props) {
|
|
878
|
-
var settings = props.settings;
|
|
879
932
|
var state = React.useState(false);
|
|
880
933
|
var open = state[0];
|
|
881
934
|
var setOpen = state[1];
|
|
935
|
+
var settingsState = React.useState(Object.assign({}, defaultSettings, props.settings));
|
|
936
|
+
var settings = settingsState[0], setSettings = settingsState[1];
|
|
937
|
+
var configBusyState = React.useState(false);
|
|
938
|
+
var configBusy = configBusyState[0], setConfigBusy = configBusyState[1];
|
|
939
|
+
var configErrorState = React.useState("");
|
|
940
|
+
var configError = configErrorState[0], setConfigError = configErrorState[1];
|
|
941
|
+
var costHintState = React.useState(false);
|
|
942
|
+
var costHint = costHintState[0], setCostHint = costHintState[1];
|
|
943
|
+
var configRequest = React.useRef(null);
|
|
882
944
|
var guideState = React.useState(false);
|
|
883
945
|
var guideOpen = guideState[0];
|
|
884
946
|
var setGuideOpen = guideState[1];
|
|
@@ -901,6 +963,43 @@ function frame(now) {
|
|
|
901
963
|
var pointer = React.useRef(null);
|
|
902
964
|
var help = React.useRef(null);
|
|
903
965
|
var dismissed = React.useRef(false);
|
|
966
|
+
React.useEffect(function () {
|
|
967
|
+
return function () { if (configRequest.current) configRequest.current.abort(); };
|
|
968
|
+
}, []);
|
|
969
|
+
React.useEffect(function () {
|
|
970
|
+
if (!open) return undefined;
|
|
971
|
+
var controller = new AbortController();
|
|
972
|
+
setConfigBusy(true);
|
|
973
|
+
readApi("/memory-engine/api/config", controller.signal).then(function (data) {
|
|
974
|
+
if (!controller.signal.aborted) { setSettings(Object.assign({}, defaultSettings, data)); setConfigError(""); }
|
|
975
|
+
}).catch(function (failure) {
|
|
976
|
+
if (!controller.signal.aborted) setConfigError("读取配置失败:" + failure.message);
|
|
977
|
+
}).finally(function () { if (!controller.signal.aborted) setConfigBusy(false); });
|
|
978
|
+
return function () { controller.abort(); };
|
|
979
|
+
}, [open]);
|
|
980
|
+
function saveAutoMemory(enabled) {
|
|
981
|
+
if (configRequest.current) return;
|
|
982
|
+
var previous = settings;
|
|
983
|
+
var controller = new AbortController();
|
|
984
|
+
var timer = setTimeout(function () { controller.abort(); }, 10000);
|
|
985
|
+
configRequest.current = controller;
|
|
986
|
+
setConfigBusy(true); setConfigError(""); setCostHint(false);
|
|
987
|
+
setSettings(Object.assign({}, settings, { autoMemory: Object.assign({}, settings.autoMemory, { enabled: enabled }) }));
|
|
988
|
+
requestApi("/memory-engine/api/config", { method: "PUT", headers: { "Content-Type": "application/json" },
|
|
989
|
+
body: JSON.stringify({ autoMemory: { enabled: enabled } }), signal: controller.signal }).then(function (data) {
|
|
990
|
+
setSettings(Object.assign({}, defaultSettings, data));
|
|
991
|
+
}).catch(function (failure) {
|
|
992
|
+
setSettings(previous);
|
|
993
|
+
setConfigError("保存失败:" + (controller.signal.aborted ? "请求超时或已中断,请刷新确认状态" : failure.message));
|
|
994
|
+
}).finally(function () { clearTimeout(timer); configRequest.current = null; setConfigBusy(false); });
|
|
995
|
+
}
|
|
996
|
+
function toggleAutoMemory() {
|
|
997
|
+
if (configBusy) return;
|
|
998
|
+
var enabled = settings.autoMemory && settings.autoMemory.enabled === true;
|
|
999
|
+
setConfigError("");
|
|
1000
|
+
if (!enabled && settings.showAutoMemoryCostHint !== false) setCostHint(true);
|
|
1001
|
+
else saveAutoMemory(!enabled);
|
|
1002
|
+
}
|
|
904
1003
|
function closeGuide() {
|
|
905
1004
|
dismissed.current = true;
|
|
906
1005
|
setGuideOpen(false);
|
|
@@ -913,7 +1012,7 @@ function frame(now) {
|
|
|
913
1012
|
setTab("map");
|
|
914
1013
|
setOpen(Date.now());
|
|
915
1014
|
}
|
|
916
|
-
function closeMap() { setSelected(null); setOpen(false); }
|
|
1015
|
+
function closeMap() { setSelected(null); setCostHint(false); setOpen(false); }
|
|
917
1016
|
function startPointer(event) {
|
|
918
1017
|
if (event.target.closest("[data-memory-engine-detail], [data-memory-engine-guide-layer], [data-memory-engine-controls]")) { pointer.current = null; return; }
|
|
919
1018
|
pointer.current = { x: event.clientX, y: event.clientY, moved: 0, version: selectionVersion.current };
|
|
@@ -977,7 +1076,8 @@ function frame(now) {
|
|
|
977
1076
|
style: { position: "fixed", inset: 0, maxWidth: "none", maxHeight: "none", zIndex: 9999, background: "#03060c" },
|
|
978
1077
|
}, tab === "map" ? React.createElement(MemoryCanvas, { key: open, rendererRef: renderer, selectionVersion: selectionVersion, onSelect: setSelected })
|
|
979
1078
|
: tab === "memories" ? React.createElement(MemoryList, { onChanged: function () { setRetry(function (value) { return value + 1; }); } })
|
|
980
|
-
: React.createElement(MemorySettings, {
|
|
1079
|
+
: React.createElement(MemorySettings, { settings: settings, configBusy: configBusy, onToggle: toggleAutoMemory,
|
|
1080
|
+
onCleared: function () { openMap(); setGuideOpen(true); } }),
|
|
981
1081
|
tab !== "map" ? React.createElement("strong", { style: { position: "absolute", top: "23px", left: "24px", font: "600 18px system-ui,sans-serif", color: "#b7e9ff" } }, "记忆管理") : null,
|
|
982
1082
|
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}}"),
|
|
983
1083
|
React.createElement("div", {
|
|
@@ -988,7 +1088,7 @@ function frame(now) {
|
|
|
988
1088
|
return React.createElement("button", { key: entry[0], type: "button", role: "tab", "aria-selected": tab === entry[0], "data-memory-engine-tab": entry[0],
|
|
989
1089
|
onClick: function () { setSelected(null); setTab(entry[0]); },
|
|
990
1090
|
style: Object.assign({}, actionStyle, { background: tab === entry[0] ? "#204762" : "#091422", borderColor: tab === entry[0] ? "#8acded" : "#35536b" }) }, entry[1]);
|
|
991
|
-
})), React.createElement("button", {
|
|
1091
|
+
})), React.createElement(AutoMemoryToggle, { settings: settings, busy: configBusy, onToggle: toggleAutoMemory, location: "header" }), React.createElement("button", {
|
|
992
1092
|
ref: help, type: "button", onClick: function () { setGuideOpen(true); },
|
|
993
1093
|
"aria-label": "查看使用指南", title: "使用指南", style: actionStyle,
|
|
994
1094
|
}, "?"), React.createElement("button", {
|
|
@@ -996,7 +1096,11 @@ function frame(now) {
|
|
|
996
1096
|
onClick: closeMap,
|
|
997
1097
|
"aria-label": "关闭记忆图谱",
|
|
998
1098
|
style: actionStyle,
|
|
999
|
-
}, "关闭 ×")),
|
|
1099
|
+
}, "关闭 ×")), configError ? React.createElement("p", { role: "alert", "data-memory-engine-config-error": "true",
|
|
1100
|
+
style: { position: "absolute", top: "50px", right: "18px", zIndex: 2, color: "#ff9c9c", background: "#271923", padding: "8px 12px", borderRadius: "8px", maxWidth: "min(600px,90vw)" } }, configError) : null,
|
|
1101
|
+
costHint ? React.createElement(ConfirmationDialog, { title: "开启自动记忆", message: "自动记忆会在每轮对话后调用一次模型,大约每轮几分钱。可以随时在这里关掉。实际费用以模型及计费方案为准。",
|
|
1102
|
+
submitLabel: "知道了,开启", busy: configBusy, onClose: function () { setCostHint(false); }, onConfirm: function () { saveAutoMemory(true); } }) : null,
|
|
1103
|
+
selected ? React.createElement(MemoryDetails, {
|
|
1000
1104
|
node: selected, onClose: function () { setSelected(null); },
|
|
1001
1105
|
onHighlight: function () { if (renderer.current) renderer.current.fireNeuron(selected.id); },
|
|
1002
1106
|
}) : null, guideOpen ? React.createElement(GuideCard, {
|
package/lib/client.src.js
CHANGED
|
@@ -15,7 +15,7 @@ window.__ModuleLoader__.load({
|
|
|
15
15
|
|
|
16
16
|
/* __CANVAS_RENDERER__ */
|
|
17
17
|
|
|
18
|
-
var defaultSettings = { hideButton: false, showGuideOnEmpty: true };
|
|
18
|
+
var defaultSettings = { hideButton: false, showGuideOnEmpty: true, autoMemory: { enabled: false, maxPerTurn: 2, dailyCap: 40 }, showAutoMemoryCostHint: true };
|
|
19
19
|
var actionStyle = { padding: "7px 12px", borderRadius: "8px", border: "1px solid #6999b8", background: "#102236", color: "#e2f4ff", font: "inherit", cursor: "pointer" };
|
|
20
20
|
var fieldStyle = { boxSizing: "border-box", padding: "10px 12px", borderRadius: "8px", border: "1px solid #35536b", background: "#091422", color: "#e2f4ff", font: "inherit", minWidth: 0 };
|
|
21
21
|
var kinds = ["知识", "印象", "对话", "事件", "日记"];
|
|
@@ -188,7 +188,7 @@ window.__ModuleLoader__.load({
|
|
|
188
188
|
setError("");
|
|
189
189
|
readApi("/memory-engine/api/map", controller.signal).then(function (nextData) {
|
|
190
190
|
if (!active) return;
|
|
191
|
-
renderer = createRenderer(canvas.current, nextData);
|
|
191
|
+
renderer = createRenderer(canvas.current, nextData, { frameInterval: 25 });
|
|
192
192
|
props.rendererRef.current = renderer;
|
|
193
193
|
renderer.onSelect(function (node) { props.selectionVersion.current++; props.onSelect(node); });
|
|
194
194
|
renderer.start();
|
|
@@ -228,7 +228,7 @@ window.__ModuleLoader__.load({
|
|
|
228
228
|
React.createElement("span", { style: { width: "52px", height: "5px", borderRadius: "5px", background: "#335066", overflow: "hidden" } },
|
|
229
229
|
React.createElement("i", { style: { display: "block", height: "100%", background: "#6bc9fa", width: Math.min(100, Math.max(0, item.retention * 100)) + "%" } })));
|
|
230
230
|
})) : null,
|
|
231
|
-
React.createElement("div", { style: { position: "absolute", right: "18px", bottom: "18px", pointerEvents: "none", color: "#7b99ad", font: "13px system-ui,sans-serif" } }, "拖拽旋转 · 滚轮缩放 ·
|
|
231
|
+
React.createElement("div", { style: { position: "absolute", right: "18px", bottom: "18px", pointerEvents: "none", color: "#7b99ad", font: "13px system-ui,sans-serif" } }, "拖拽旋转 · 滚轮缩放 · 点光点看关联"),
|
|
232
232
|
error ? React.createElement("div", { role: "alert", style: { position: "absolute", inset: "40% 20% auto", textAlign: "center", color: "#d8e9f8" } },
|
|
233
233
|
React.createElement("p", null, "图谱加载失败:" + error),
|
|
234
234
|
React.createElement("button", { type: "button", style: actionStyle, onClick: function () { setRetry(function (value) { return value + 1; }); } }, "重试")) : null);
|
|
@@ -435,6 +435,17 @@ window.__ModuleLoader__.load({
|
|
|
435
435
|
onClick: function () { setOffset(page.items.length); }, style: Object.assign({}, actionStyle, { marginLeft: "12px", opacity: loading ? 0.5 : 1 }) }, "加载更多") : null));
|
|
436
436
|
}
|
|
437
437
|
|
|
438
|
+
function AutoMemoryToggle(props) {
|
|
439
|
+
var enabled = props.settings.autoMemory && props.settings.autoMemory.enabled === true;
|
|
440
|
+
var maximum = props.settings.autoMemory && props.settings.autoMemory.maxPerTurn || 2;
|
|
441
|
+
return React.createElement("button", {
|
|
442
|
+
type: "button", role: "switch", "aria-checked": enabled, "aria-label": "自动记忆",
|
|
443
|
+
"data-memory-engine-auto-memory": props.location, disabled: props.busy, onClick: props.onToggle,
|
|
444
|
+
title: enabled ? "当前:开(每轮对话后会抽 0~" + maximum + " 条记忆)" : "当前:关(不会自动调用模型抽取记忆)",
|
|
445
|
+
style: Object.assign({}, actionStyle, { color: enabled ? "#e2f4ff" : "#8195aa", borderColor: enabled ? "#64bca0" : "#35536b", opacity: props.busy ? 0.6 : 1 }),
|
|
446
|
+
}, React.createElement("span", { "aria-hidden": "true", style: { color: enabled ? "#89f0bb" : "#8195aa", textShadow: enabled ? "0 0 8px #56d890" : "none" } }, enabled ? "●" : "○"), " 自动记忆");
|
|
447
|
+
}
|
|
448
|
+
|
|
438
449
|
function MemorySettings(props) {
|
|
439
450
|
var statsState = React.useState(null);
|
|
440
451
|
var stats = statsState[0], setStats = statsState[1];
|
|
@@ -500,6 +511,10 @@ window.__ModuleLoader__.load({
|
|
|
500
511
|
return React.createElement("section", { "aria-label": "记忆设置", "data-memory-engine-settings": "true",
|
|
501
512
|
style: { position: "absolute", inset: "76px 0 0", overflowY: "auto", padding: "0 24px 32px", color: "#dbeaf8", font: "14px/1.7 system-ui,sans-serif" } },
|
|
502
513
|
React.createElement("div", { style: { maxWidth: "900px", margin: "0 auto", padding: "28px", border: "1px solid #263f54", borderRadius: "14px", background: "#0b1624" } },
|
|
514
|
+
React.createElement("section", { style: { marginBottom: "24px", paddingBottom: "20px", borderBottom: "1px solid #263f54" } },
|
|
515
|
+
React.createElement("h2", { style: { margin: "0 0 12px", fontSize: "20px", color: "#b7e9ff" } }, "自动记忆"),
|
|
516
|
+
React.createElement(AutoMemoryToggle, { settings: props.settings, busy: props.configBusy, onToggle: props.onToggle, location: "settings" }),
|
|
517
|
+
React.createElement("p", { style: { color: "#9fb7ce", marginBottom: 0 } }, "默认关闭。开启后每轮对话会额外调用一次模型,抽取值得记住的事实;通常最多 2 条,可随时关闭。大约每轮几分钱,实际费用以模型及计费方案为准。自动导入是独立设置,不受此开关控制。")),
|
|
503
518
|
React.createElement("h2", { style: { margin: "0 0 20px", fontSize: "20px", color: "#b7e9ff" } }, "数据管理"),
|
|
504
519
|
stats ? React.createElement(React.Fragment, null,
|
|
505
520
|
React.createElement("div", { style: { display: "flex", alignItems: "center", flexWrap: "wrap", gap: "12px" } },
|
|
@@ -526,10 +541,18 @@ window.__ModuleLoader__.load({
|
|
|
526
541
|
}
|
|
527
542
|
|
|
528
543
|
function MemoryButton(props) {
|
|
529
|
-
var settings = props.settings;
|
|
530
544
|
var state = React.useState(false);
|
|
531
545
|
var open = state[0];
|
|
532
546
|
var setOpen = state[1];
|
|
547
|
+
var settingsState = React.useState(Object.assign({}, defaultSettings, props.settings));
|
|
548
|
+
var settings = settingsState[0], setSettings = settingsState[1];
|
|
549
|
+
var configBusyState = React.useState(false);
|
|
550
|
+
var configBusy = configBusyState[0], setConfigBusy = configBusyState[1];
|
|
551
|
+
var configErrorState = React.useState("");
|
|
552
|
+
var configError = configErrorState[0], setConfigError = configErrorState[1];
|
|
553
|
+
var costHintState = React.useState(false);
|
|
554
|
+
var costHint = costHintState[0], setCostHint = costHintState[1];
|
|
555
|
+
var configRequest = React.useRef(null);
|
|
533
556
|
var guideState = React.useState(false);
|
|
534
557
|
var guideOpen = guideState[0];
|
|
535
558
|
var setGuideOpen = guideState[1];
|
|
@@ -552,6 +575,43 @@ window.__ModuleLoader__.load({
|
|
|
552
575
|
var pointer = React.useRef(null);
|
|
553
576
|
var help = React.useRef(null);
|
|
554
577
|
var dismissed = React.useRef(false);
|
|
578
|
+
React.useEffect(function () {
|
|
579
|
+
return function () { if (configRequest.current) configRequest.current.abort(); };
|
|
580
|
+
}, []);
|
|
581
|
+
React.useEffect(function () {
|
|
582
|
+
if (!open) return undefined;
|
|
583
|
+
var controller = new AbortController();
|
|
584
|
+
setConfigBusy(true);
|
|
585
|
+
readApi("/memory-engine/api/config", controller.signal).then(function (data) {
|
|
586
|
+
if (!controller.signal.aborted) { setSettings(Object.assign({}, defaultSettings, data)); setConfigError(""); }
|
|
587
|
+
}).catch(function (failure) {
|
|
588
|
+
if (!controller.signal.aborted) setConfigError("读取配置失败:" + failure.message);
|
|
589
|
+
}).finally(function () { if (!controller.signal.aborted) setConfigBusy(false); });
|
|
590
|
+
return function () { controller.abort(); };
|
|
591
|
+
}, [open]);
|
|
592
|
+
function saveAutoMemory(enabled) {
|
|
593
|
+
if (configRequest.current) return;
|
|
594
|
+
var previous = settings;
|
|
595
|
+
var controller = new AbortController();
|
|
596
|
+
var timer = setTimeout(function () { controller.abort(); }, 10000);
|
|
597
|
+
configRequest.current = controller;
|
|
598
|
+
setConfigBusy(true); setConfigError(""); setCostHint(false);
|
|
599
|
+
setSettings(Object.assign({}, settings, { autoMemory: Object.assign({}, settings.autoMemory, { enabled: enabled }) }));
|
|
600
|
+
requestApi("/memory-engine/api/config", { method: "PUT", headers: { "Content-Type": "application/json" },
|
|
601
|
+
body: JSON.stringify({ autoMemory: { enabled: enabled } }), signal: controller.signal }).then(function (data) {
|
|
602
|
+
setSettings(Object.assign({}, defaultSettings, data));
|
|
603
|
+
}).catch(function (failure) {
|
|
604
|
+
setSettings(previous);
|
|
605
|
+
setConfigError("保存失败:" + (controller.signal.aborted ? "请求超时或已中断,请刷新确认状态" : failure.message));
|
|
606
|
+
}).finally(function () { clearTimeout(timer); configRequest.current = null; setConfigBusy(false); });
|
|
607
|
+
}
|
|
608
|
+
function toggleAutoMemory() {
|
|
609
|
+
if (configBusy) return;
|
|
610
|
+
var enabled = settings.autoMemory && settings.autoMemory.enabled === true;
|
|
611
|
+
setConfigError("");
|
|
612
|
+
if (!enabled && settings.showAutoMemoryCostHint !== false) setCostHint(true);
|
|
613
|
+
else saveAutoMemory(!enabled);
|
|
614
|
+
}
|
|
555
615
|
function closeGuide() {
|
|
556
616
|
dismissed.current = true;
|
|
557
617
|
setGuideOpen(false);
|
|
@@ -564,7 +624,7 @@ window.__ModuleLoader__.load({
|
|
|
564
624
|
setTab("map");
|
|
565
625
|
setOpen(Date.now());
|
|
566
626
|
}
|
|
567
|
-
function closeMap() { setSelected(null); setOpen(false); }
|
|
627
|
+
function closeMap() { setSelected(null); setCostHint(false); setOpen(false); }
|
|
568
628
|
function startPointer(event) {
|
|
569
629
|
if (event.target.closest("[data-memory-engine-detail], [data-memory-engine-guide-layer], [data-memory-engine-controls]")) { pointer.current = null; return; }
|
|
570
630
|
pointer.current = { x: event.clientX, y: event.clientY, moved: 0, version: selectionVersion.current };
|
|
@@ -628,7 +688,8 @@ window.__ModuleLoader__.load({
|
|
|
628
688
|
style: { position: "fixed", inset: 0, maxWidth: "none", maxHeight: "none", zIndex: 9999, background: "#03060c" },
|
|
629
689
|
}, tab === "map" ? React.createElement(MemoryCanvas, { key: open, rendererRef: renderer, selectionVersion: selectionVersion, onSelect: setSelected })
|
|
630
690
|
: tab === "memories" ? React.createElement(MemoryList, { onChanged: function () { setRetry(function (value) { return value + 1; }); } })
|
|
631
|
-
: React.createElement(MemorySettings, {
|
|
691
|
+
: React.createElement(MemorySettings, { settings: settings, configBusy: configBusy, onToggle: toggleAutoMemory,
|
|
692
|
+
onCleared: function () { openMap(); setGuideOpen(true); } }),
|
|
632
693
|
tab !== "map" ? React.createElement("strong", { style: { position: "absolute", top: "23px", left: "24px", font: "600 18px system-ui,sans-serif", color: "#b7e9ff" } }, "记忆管理") : null,
|
|
633
694
|
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}}"),
|
|
634
695
|
React.createElement("div", {
|
|
@@ -639,7 +700,7 @@ window.__ModuleLoader__.load({
|
|
|
639
700
|
return React.createElement("button", { key: entry[0], type: "button", role: "tab", "aria-selected": tab === entry[0], "data-memory-engine-tab": entry[0],
|
|
640
701
|
onClick: function () { setSelected(null); setTab(entry[0]); },
|
|
641
702
|
style: Object.assign({}, actionStyle, { background: tab === entry[0] ? "#204762" : "#091422", borderColor: tab === entry[0] ? "#8acded" : "#35536b" }) }, entry[1]);
|
|
642
|
-
})), React.createElement("button", {
|
|
703
|
+
})), React.createElement(AutoMemoryToggle, { settings: settings, busy: configBusy, onToggle: toggleAutoMemory, location: "header" }), React.createElement("button", {
|
|
643
704
|
ref: help, type: "button", onClick: function () { setGuideOpen(true); },
|
|
644
705
|
"aria-label": "查看使用指南", title: "使用指南", style: actionStyle,
|
|
645
706
|
}, "?"), React.createElement("button", {
|
|
@@ -647,7 +708,11 @@ window.__ModuleLoader__.load({
|
|
|
647
708
|
onClick: closeMap,
|
|
648
709
|
"aria-label": "关闭记忆图谱",
|
|
649
710
|
style: actionStyle,
|
|
650
|
-
}, "关闭 ×")),
|
|
711
|
+
}, "关闭 ×")), configError ? React.createElement("p", { role: "alert", "data-memory-engine-config-error": "true",
|
|
712
|
+
style: { position: "absolute", top: "50px", right: "18px", zIndex: 2, color: "#ff9c9c", background: "#271923", padding: "8px 12px", borderRadius: "8px", maxWidth: "min(600px,90vw)" } }, configError) : null,
|
|
713
|
+
costHint ? React.createElement(ConfirmationDialog, { title: "开启自动记忆", message: "自动记忆会在每轮对话后调用一次模型,大约每轮几分钱。可以随时在这里关掉。实际费用以模型及计费方案为准。",
|
|
714
|
+
submitLabel: "知道了,开启", busy: configBusy, onClose: function () { setCostHint(false); }, onConfirm: function () { saveAutoMemory(true); } }) : null,
|
|
715
|
+
selected ? React.createElement(MemoryDetails, {
|
|
651
716
|
node: selected, onClose: function () { setSelected(null); },
|
|
652
717
|
onHighlight: function () { if (renderer.current) renderer.current.fireNeuron(selected.id); },
|
|
653
718
|
}) : null, guideOpen ? React.createElement(GuideCard, {
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { readState, writeState } from './automation-state.js';
|
|
4
|
+
import { readUiConfig } from './config.js';
|
|
5
|
+
|
|
6
|
+
const listeners = new Map();
|
|
7
|
+
const writable = new Map([
|
|
8
|
+
['autoMemory.enabled', value => typeof value === 'boolean'],
|
|
9
|
+
['autoImport.enabled', value => typeof value === 'boolean'],
|
|
10
|
+
['autoMemory.maxPerTurn', value => Number.isSafeInteger(value) && value >= 1 && value <= 5],
|
|
11
|
+
['autoMemory.dailyCap', value => Number.isSafeInteger(value) && value >= 0 && value <= 500],
|
|
12
|
+
['spinSpeed', value => Number.isFinite(value) && value >= 0.05 && value <= 1.5],
|
|
13
|
+
['showGuideOnEmpty', value => typeof value === 'boolean'],
|
|
14
|
+
['hideButton', value => typeof value === 'boolean'],
|
|
15
|
+
]);
|
|
16
|
+
const keyFor = home => path.resolve(home).replaceAll('\\', '/').toLowerCase();
|
|
17
|
+
const object = value => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
18
|
+
|
|
19
|
+
export function onConfigUpdate(home, listener) {
|
|
20
|
+
const key = keyFor(home);
|
|
21
|
+
if (!listeners.has(key)) listeners.set(key, new Set());
|
|
22
|
+
const group = listeners.get(key);
|
|
23
|
+
group.add(listener);
|
|
24
|
+
return () => { group.delete(listener); if (!group.size) listeners.delete(key); };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function updateConfig(home, changes, { now = Date.now, warn = () => {} } = {}) {
|
|
28
|
+
const invalid = message => { throw Object.assign(new Error(message), { status: 400 }); };
|
|
29
|
+
if (!object(changes)) invalid('配置修改必须是 JSON 对象');
|
|
30
|
+
const fields = [];
|
|
31
|
+
for (const [key, value] of Object.entries(changes)) {
|
|
32
|
+
if (key === 'autoMemory' || key === 'autoImport') {
|
|
33
|
+
if (!object(value) || !Object.keys(value).length) invalid(`${key} 必须包含可修改字段`);
|
|
34
|
+
for (const [name, item] of Object.entries(value)) fields.push([`${key}.${name}`, item]);
|
|
35
|
+
} else fields.push([key, value]);
|
|
36
|
+
}
|
|
37
|
+
if (!fields.length) invalid('至少提供一个可修改字段');
|
|
38
|
+
for (const [key, value] of fields) {
|
|
39
|
+
if (!writable.has(key) || Object.hasOwn(changes, key) && key.includes('.')) invalid(`不支持修改 ${key}`);
|
|
40
|
+
if (!writable.get(key)(value)) invalid(`${key} 类型或范围错误`);
|
|
41
|
+
}
|
|
42
|
+
const directory = path.join(home, 'memory-engine');
|
|
43
|
+
const file = path.join(directory, 'config.json');
|
|
44
|
+
const current = readState(file, {});
|
|
45
|
+
if (!object(current)) throw new Error('现有配置必须是 JSON 对象');
|
|
46
|
+
const next = { ...current };
|
|
47
|
+
for (const key of Object.keys(changes)) {
|
|
48
|
+
if (key === 'autoMemory' || key === 'autoImport') next[key] = { ...current[key], ...changes[key] };
|
|
49
|
+
else next[key] = changes[key];
|
|
50
|
+
}
|
|
51
|
+
if (changes.autoMemory?.enabled === true) next.showAutoMemoryCostHint = false;
|
|
52
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
53
|
+
const prefix = `config.json.bak-${now()}`;
|
|
54
|
+
const suffixes = fs.readdirSync(directory).filter(name => name.startsWith(prefix + '-'))
|
|
55
|
+
.map(name => Number(name.slice(prefix.length + 1))).filter(Number.isSafeInteger);
|
|
56
|
+
let suffix = suffixes.length ? Math.max(...suffixes) + 1 : 0;
|
|
57
|
+
let name = suffix ? `${prefix}-${suffix}` : prefix;
|
|
58
|
+
while (fs.existsSync(path.join(directory, name))) name = `${prefix}-${++suffix}`;
|
|
59
|
+
const backup = path.join(directory, name);
|
|
60
|
+
if (fs.existsSync(file)) fs.copyFileSync(file, backup, fs.constants.COPYFILE_EXCL);
|
|
61
|
+
else fs.writeFileSync(backup, '{}\n', { flag: 'wx', mode: 0o600 });
|
|
62
|
+
writeState(file, next);
|
|
63
|
+
for (const listener of listeners.get(keyFor(home)) ?? []) {
|
|
64
|
+
try { listener(changes); } catch (error) { try { warn(`配置已保存,任务状态更新失败:${error.message}`); } catch {} }
|
|
65
|
+
}
|
|
66
|
+
const backups = fs.readdirSync(directory, { withFileTypes: true }).flatMap(entry => {
|
|
67
|
+
const match = /^config\.json\.bak-(\d+)(?:-(\d+))?$/u.exec(entry.name);
|
|
68
|
+
return entry.isFile() && match ? [{ name: entry.name, time: Number(match[1]), suffix: Number(match[2] ?? 0) }] : [];
|
|
69
|
+
}).sort((left, right) => right.time - left.time || right.suffix - left.suffix);
|
|
70
|
+
for (const old of backups.slice(5)) {
|
|
71
|
+
try { fs.unlinkSync(path.join(directory, old.name)); } catch (error) { try { warn(`配置已保存,清理旧备份失败:${error.message}`); } catch {} }
|
|
72
|
+
}
|
|
73
|
+
return { ...readUiConfig(home, { warn }), backup: name };
|
|
74
|
+
}
|
package/lib/config.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { automationConfig } from './automation-config.js';
|
|
5
|
+
import { readLastError } from './auto-log.js';
|
|
4
6
|
|
|
5
7
|
export const defaultHome = () => process.env.DSH_HOME || path.join(os.homedir(), '.dsh');
|
|
6
8
|
|
|
@@ -11,7 +13,11 @@ export function resolveDiaryDir({ home = defaultHome(), envDir = process.env.QQ_
|
|
|
11
13
|
export function readUiConfig(home = defaultHome(), options = {}) {
|
|
12
14
|
const config = readConfig(home);
|
|
13
15
|
return { hideButton: config.hideButton === true, showGuideOnEmpty: config.showGuideOnEmpty !== false,
|
|
14
|
-
hasPersona: readPersona(config, options) !== null, personaTitle: config.personaTitle, spinSpeed: config.spinSpeed
|
|
16
|
+
hasPersona: readPersona(config, options) !== null, personaTitle: config.personaTitle, spinSpeed: config.spinSpeed,
|
|
17
|
+
autoMemory: { enabled: config.autoMemory.enabled, maxPerTurn: config.autoMemory.maxPerTurn, dailyCap: config.autoMemory.dailyCap },
|
|
18
|
+
showAutoMemoryCostHint: config.showAutoMemoryCostHint !== false,
|
|
19
|
+
autoImport: { enabled: config.autoImport.enabled, sourceCount: config.autoImport.sources.length },
|
|
20
|
+
lastError: readLastError(path.join(home, 'memory-engine', 'auto-log.json'), options.warn) };
|
|
15
21
|
}
|
|
16
22
|
|
|
17
23
|
export function readPersona(config, { now = Date.now(), warn = message => console.warn(`[dsh-memory-engine] ${message}`) } = {}) {
|
|
@@ -34,7 +40,7 @@ export function readPersona(config, { now = Date.now(), warn = message => consol
|
|
|
34
40
|
}
|
|
35
41
|
}
|
|
36
42
|
|
|
37
|
-
export function readConfig(home = defaultHome()) {
|
|
43
|
+
export function readConfig(home = defaultHome(), { warn } = {}) {
|
|
38
44
|
const file = path.join(home, 'memory-engine', 'config.json');
|
|
39
45
|
try {
|
|
40
46
|
const value = JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/u, ''));
|
|
@@ -46,9 +52,9 @@ export function readConfig(home = defaultHome()) {
|
|
|
46
52
|
if (value.spinSpeed !== undefined && (!Number.isFinite(value.spinSpeed) || value.spinSpeed < 0.05 || value.spinSpeed > 1.5)) {
|
|
47
53
|
throw new Error('spinSpeed 必须是 0.05~1.5 的数字');
|
|
48
54
|
}
|
|
49
|
-
return { ...value, personaTitle: value.personaTitle?.trim() || '角色卡', spinSpeed: value.spinSpeed ?? 0.35 };
|
|
55
|
+
return { ...value, personaTitle: value.personaTitle?.trim() || '角色卡', spinSpeed: value.spinSpeed ?? 0.35, ...automationConfig(value, warn) };
|
|
50
56
|
} catch (error) {
|
|
51
|
-
if (error.code === 'ENOENT') return { personaTitle: '角色卡', spinSpeed: 0.35 };
|
|
57
|
+
if (error.code === 'ENOENT') return { personaTitle: '角色卡', spinSpeed: 0.35, ...automationConfig({}, warn) };
|
|
52
58
|
throw new Error(`无法读取 ${file}:${error.message}`);
|
|
53
59
|
}
|
|
54
60
|
}
|