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

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/lib/client.src.js CHANGED
@@ -17,17 +17,21 @@ window.__ModuleLoader__.load({
17
17
 
18
18
  var defaultSettings = { hideButton: false, showGuideOnEmpty: true };
19
19
  var actionStyle = { padding: "7px 12px", borderRadius: "8px", border: "1px solid #6999b8", background: "#102236", color: "#e2f4ff", font: "inherit", cursor: "pointer" };
20
+ var fieldStyle = { boxSizing: "border-box", padding: "10px 12px", borderRadius: "8px", border: "1px solid #35536b", background: "#091422", color: "#e2f4ff", font: "inherit", minWidth: 0 };
21
+ var kinds = ["知识", "印象", "对话", "事件", "日记"];
20
22
 
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;
23
+ function requestApi(url, options) {
24
+ return fetch(url, Object.assign({ cache: "no-store" }, options)).then(function (response) {
25
+ return response.json().then(function (result) {
26
+ if (!response.ok || !result || result.ok !== true) throw new Error(result && result.error || "HTTP " + response.status);
27
+ if (!result.data || typeof result.data !== "object") throw new Error("接口返回格式无效");
28
+ return result.data;
29
+ });
28
30
  });
29
31
  }
30
32
 
33
+ function readApi(url, signal) { return requestApi(url, { signal: signal }); }
34
+
31
35
  function loadSettings() {
32
36
  var controller = new AbortController();
33
37
  var timer = setTimeout(function () { controller.abort(); }, 5000);
@@ -107,6 +111,8 @@ window.__ModuleLoader__.load({
107
111
  function MemoryDetails(props) {
108
112
  var node = props.node;
109
113
  var text = typeof node.text === "string" ? node.text : "";
114
+ if (node.isCore) text = Array.from(text).slice(0, 4000).join("");
115
+ var defaultCopyLabel = node.isCore ? "复制全文" : "复制正文";
110
116
  var title = String(node.title || "").trim() || Array.from(text).slice(0, 20).join("");
111
117
  var date = new Date(node.ts);
112
118
  var day = Number.isFinite(date.getTime()) ? date.getFullYear() + "-" + String(date.getMonth() + 1).padStart(2, "0") + "-" + String(date.getDate()).padStart(2, "0") : "未知";
@@ -119,7 +125,7 @@ window.__ModuleLoader__.load({
119
125
  ];
120
126
  var emotion = Array.isArray(node.emotion) ? node.emotion.filter(function (item) { return item && item.tag; }).map(function (item) { return item.tag; }).join(" / ") : "";
121
127
  if (emotion) rows.push(["情绪", emotion]);
122
- var copiedState = React.useState("复制正文");
128
+ var copiedState = React.useState(defaultCopyLabel);
123
129
  var copyLabel = copiedState[0];
124
130
  var setCopyLabel = copiedState[1];
125
131
  var timer = React.useRef(null);
@@ -129,10 +135,10 @@ window.__ModuleLoader__.load({
129
135
  copyVersion.current++;
130
136
  clearTimeout(timer.current);
131
137
  timer.current = null;
132
- setCopyLabel("复制正文");
138
+ setCopyLabel(defaultCopyLabel);
133
139
  if (body.current) body.current.scrollTop = 0;
134
140
  return function () { copyVersion.current++; clearTimeout(timer.current); timer.current = null; };
135
- }, [node.id, text]);
141
+ }, [node.id, text, defaultCopyLabel]);
136
142
  function copyBody() {
137
143
  var version = ++copyVersion.current;
138
144
  clearTimeout(timer.current);
@@ -140,7 +146,7 @@ window.__ModuleLoader__.load({
140
146
  if (version !== copyVersion.current) return;
141
147
  setCopyLabel("已复制 ✓");
142
148
  timer.current = setTimeout(function () {
143
- if (version === copyVersion.current) setCopyLabel("复制正文");
149
+ if (version === copyVersion.current) setCopyLabel(defaultCopyLabel);
144
150
  timer.current = null;
145
151
  }, 1800);
146
152
  }, function () {
@@ -154,13 +160,13 @@ window.__ModuleLoader__.load({
154
160
  React.createElement("span", { "data-memory-engine-detail-kind": "true", style: { color: "#a4e6ff", background: "#15344b", borderRadius: "6px", padding: "2px 8px", overflowWrap: "anywhere" } }, node.kind || "未分类"),
155
161
  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
162
  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 } },
163
+ node.isCore ? null : 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
164
  rows.map(function (row) { return React.createElement(React.Fragment, { key: row[0] },
159
165
  React.createElement("dt", { style: { margin: 0 } }, row[0]),
160
166
  React.createElement("dd", { style: { margin: 0, overflowWrap: "anywhere" } }, row[1])); })),
161
167
  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
168
  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 }, "在图里高亮它"),
169
+ node.isCore ? null : React.createElement("button", { type: "button", "data-memory-engine-highlight": "true", style: actionStyle, onClick: props.onHighlight }, "在图里高亮它"),
164
170
  React.createElement("button", { type: "button", "data-memory-engine-copy-body": "true", style: actionStyle, onClick: copyBody }, copyLabel)));
165
171
  }
166
172
 
@@ -228,6 +234,297 @@ window.__ModuleLoader__.load({
228
234
  React.createElement("button", { type: "button", style: actionStyle, onClick: function () { setRetry(function (value) { return value + 1; }); } }, "重试")) : null);
229
235
  }
230
236
 
237
+ function ConfirmationDialog(props) {
238
+ var valueState = React.useState("");
239
+ var value = valueState[0], setValue = valueState[1];
240
+ var errorState = React.useState("");
241
+ var error = errorState[0], setError = errorState[1];
242
+ var form = React.useRef(null);
243
+ React.useEffect(function () {
244
+ var active = document.activeElement;
245
+ var target = form.current && form.current.querySelector("input,button");
246
+ if (target) target.focus();
247
+ return function () { if (active && active.isConnected) active.focus(); };
248
+ }, []);
249
+ function onKey(event) {
250
+ if (event.key === "Escape") {
251
+ event.preventDefault(); event.stopPropagation();
252
+ if (!props.busy) props.onClose();
253
+ }
254
+ if (event.key === "Tab" && form.current) {
255
+ var controls = Array.from(form.current.querySelectorAll("input,button")).filter(function (control) { return !control.disabled; });
256
+ if (!controls.length) { event.preventDefault(); return; }
257
+ var index = controls.indexOf(document.activeElement);
258
+ if (event.shiftKey && index <= 0) { event.preventDefault(); controls[controls.length - 1].focus(); }
259
+ else if (!event.shiftKey && (index === -1 || index === controls.length - 1)) { event.preventDefault(); controls[0].focus(); }
260
+ }
261
+ }
262
+ return React.createElement("div", { "data-memory-engine-confirm": "true", onKeyDown: onKey,
263
+ style: { position: "fixed", inset: 0, zIndex: 3, display: "grid", placeItems: "center", padding: "24px", background: "#03060ccc" } },
264
+ React.createElement("form", { ref: form, role: "dialog", "aria-modal": "true", "aria-label": props.title,
265
+ onSubmit: function (event) {
266
+ event.preventDefault();
267
+ if (props.busy) return;
268
+ if (props.requireDelete && value !== "DELETE") { setError("请输入大写 DELETE,其他内容不会执行清空。"); return; }
269
+ setError(""); props.onConfirm();
270
+ },
271
+ style: { width: "min(460px,100%)", padding: "24px", border: "1px solid #8b505c", borderRadius: "16px", background: "#101a29", color: "#e2f4ff", boxShadow: "0 16px 80px #0008" } },
272
+ React.createElement("h2", { style: { margin: "0 0 12px", fontSize: "20px" } }, props.title),
273
+ React.createElement("p", { style: { color: "#c2cedc" } }, props.message),
274
+ props.requireDelete ? React.createElement("label", null, "输入 DELETE 确认",
275
+ React.createElement("input", { "aria-label": "输入 DELETE 确认", autoComplete: "off", value: value, disabled: props.busy,
276
+ onChange: function (event) { setValue(event.target.value); setError(""); }, style: Object.assign({}, fieldStyle, { display: "block", width: "100%", marginTop: "8px" }) })) : null,
277
+ error || props.error ? React.createElement("p", { role: "alert", style: { color: "#ffb8b8" } }, error || props.error) : null,
278
+ React.createElement("div", { style: { display: "flex", justifyContent: "flex-end", gap: "10px", marginTop: "24px" } },
279
+ React.createElement("button", { type: "button", disabled: props.busy, onClick: props.onClose, style: actionStyle }, "取消"),
280
+ React.createElement("button", { type: "submit", disabled: props.busy, style: Object.assign({}, actionStyle, { borderColor: "#c86b7a", background: "#572a38" }) }, props.busy ? "正在处理…" : props.submitLabel))));
281
+ }
282
+
283
+ function MemoryRow(props) {
284
+ var item = props.item;
285
+ var expandedState = React.useState(false);
286
+ var expanded = expandedState[0], setExpanded = expandedState[1];
287
+ var detailState = React.useState(item);
288
+ var detail = detailState[0], setDetail = detailState[1];
289
+ var editState = React.useState(null);
290
+ var edit = editState[0], setEdit = editState[1];
291
+ var confirmState = React.useState(false);
292
+ var confirming = confirmState[0], setConfirming = confirmState[1];
293
+ var errorState = React.useState("");
294
+ var error = errorState[0], setError = errorState[1];
295
+ var loadingState = React.useState(false);
296
+ var loading = loadingState[0], setLoading = loadingState[1];
297
+ var busyState = React.useState(false);
298
+ var busy = busyState[0], setBusy = busyState[1];
299
+ var mutation = React.useRef(null);
300
+ React.useEffect(function () { return function () { if (mutation.current) mutation.current.abort(); }; }, []);
301
+ React.useEffect(function () {
302
+ if (!expanded) return;
303
+ var controller = new AbortController();
304
+ setLoading(true); setError("");
305
+ readApi("/memory-engine/api/item/" + encodeURIComponent(item.id), controller.signal).then(function (result) {
306
+ if (!controller.signal.aborted) setDetail(result);
307
+ }).catch(function (failure) {
308
+ if (!controller.signal.aborted) setError("读取详情失败:" + failure.message);
309
+ }).finally(function () { if (!controller.signal.aborted) setLoading(false); });
310
+ return function () { controller.abort(); };
311
+ }, [expanded, item.id]);
312
+ function mutate(method) {
313
+ if (mutation.current) return;
314
+ var controller = new AbortController();
315
+ mutation.current = controller;
316
+ setBusy(true); setError("");
317
+ var options = { method: method, signal: controller.signal };
318
+ if (method === "PUT") { options.headers = { "Content-Type": "application/json" }; options.body = JSON.stringify(edit); }
319
+ requestApi("/memory-engine/api/item/" + encodeURIComponent(item.id), options).then(function () {
320
+ if (controller.signal.aborted) return;
321
+ setEdit(null); setExpanded(false); setConfirming(false); props.onChanged();
322
+ }).catch(function (failure) {
323
+ if (!controller.signal.aborted) setError((method === "PUT" ? "保存失败:" : "删除失败:") + failure.message);
324
+ }).finally(function () {
325
+ if (!controller.signal.aborted) { mutation.current = null; setBusy(false); }
326
+ });
327
+ }
328
+ function change(field, value) { setEdit(function (previous) { var next = Object.assign({}, previous); next[field] = value; return next; }); }
329
+ var choices = kinds.includes(detail.kind) ? kinds : [detail.kind].concat(kinds);
330
+ return React.createElement("article", { "data-memory-engine-row": item.id,
331
+ style: { marginBottom: "12px", border: "1px solid " + (expanded ? "#4c85a4" : "#263f54"), borderRadius: "12px", background: "#0b1624", overflowWrap: "anywhere" } },
332
+ React.createElement("button", { type: "button", "data-memory-engine-expand": item.id, "aria-expanded": expanded, disabled: busy,
333
+ onClick: function () { setExpanded(!expanded); setEdit(null); setError(""); },
334
+ style: { width: "100%", display: "block", padding: "18px 20px", border: 0, borderRadius: "12px", background: "transparent", color: "inherit", textAlign: "left", font: "inherit", cursor: "pointer" } },
335
+ React.createElement("span", { style: { display: "flex", alignItems: "center", gap: "12px", flexWrap: "wrap" } },
336
+ React.createElement("span", { style: { color: "#a4e6ff", background: "#15344b", padding: "2px 8px", borderRadius: "6px", flexShrink: 0 } }, item.kind),
337
+ React.createElement("strong", { style: { flex: "1 1 180px", overflowWrap: "anywhere" } }, item.title),
338
+ React.createElement("span", { style: { color: "#8db0c7", fontSize: "12px", whiteSpace: "nowrap" } }, "留存 " + item.retention.toFixed(2) + " · " + item.degree + " 个连接")),
339
+ !expanded ? React.createElement("span", { style: { display: "block", color: "#9fb7ce", marginTop: "8px", overflowWrap: "anywhere" } }, item.preview) : null),
340
+ expanded ? React.createElement("div", { style: { padding: "0 20px 18px" } },
341
+ loading ? React.createElement("p", { role: "status" }, "正在读取全文…") : null,
342
+ edit ? React.createElement("form", { "data-memory-engine-editor": item.id, onSubmit: function (event) { event.preventDefault(); mutate("PUT"); } },
343
+ React.createElement("textarea", { "aria-label": "记忆正文", value: edit.text, disabled: busy,
344
+ onChange: function (event) { change("text", event.target.value); },
345
+ style: Object.assign({}, fieldStyle, { width: "100%", minHeight: "150px", maxHeight: "40vh", resize: "vertical", lineHeight: 1.7 }) }),
346
+ React.createElement("div", { style: { display: "flex", gap: "12px", flexWrap: "wrap", margin: "12px 0" } },
347
+ React.createElement("label", null, "分类 ", React.createElement("select", { "aria-label": "编辑分类", value: edit.kind, disabled: busy, style: fieldStyle,
348
+ onChange: function (event) { change("kind", event.target.value); } }, choices.map(function (value) { return React.createElement("option", { key: value, value: value }, value); }))),
349
+ React.createElement("label", null, "重要度 ", React.createElement("select", { "aria-label": "编辑重要度", value: edit.heat, disabled: busy, style: fieldStyle,
350
+ onChange: function (event) { change("heat", Number(event.target.value)); } },
351
+ [[1, "热(1)"], [2, "温(2)"], [3, "冷(3)"]].map(function (entry) { return React.createElement("option", { key: entry[0], value: entry[0] }, entry[1]); }))),
352
+ React.createElement("label", null, "相关人 ", React.createElement("input", { "aria-label": "编辑相关人", value: edit.who, disabled: busy,
353
+ onChange: function (event) { change("who", event.target.value); }, style: Object.assign({}, fieldStyle, { width: "140px" }) }))),
354
+ React.createElement("div", { style: { display: "flex", gap: "8px" } },
355
+ React.createElement("button", { type: "submit", disabled: busy, "data-memory-engine-save": "true", style: actionStyle }, busy ? "正在保存…" : "保存"),
356
+ React.createElement("button", { type: "button", disabled: busy, "data-memory-engine-cancel-edit": "true", style: actionStyle,
357
+ onClick: function () { setEdit(null); setExpanded(false); setError(""); } }, "取消")))
358
+ : React.createElement(React.Fragment, null,
359
+ React.createElement("div", { "data-memory-engine-full-text": "true", style: { whiteSpace: "pre-wrap", maxHeight: "40vh", overflowY: "auto", color: "#c2d3e3", marginBottom: "14px" } }, detail.text),
360
+ React.createElement("div", { style: { display: "flex", gap: "8px" } },
361
+ React.createElement("button", { type: "button", "data-memory-engine-edit": item.id, disabled: loading || busy, style: actionStyle,
362
+ onClick: function () { setError(""); setEdit({ text: detail.text, kind: detail.kind, heat: detail.heat, who: detail.who || "" }); } }, "编辑"),
363
+ React.createElement("button", { type: "button", "data-memory-engine-delete": item.id, disabled: loading || busy,
364
+ onClick: function () { setError(""); setConfirming(true); }, style: Object.assign({}, actionStyle, { color: "#ffb8b8", borderColor: "#8b505c" }) }, "删除"))),
365
+ error ? React.createElement("p", { role: "alert", style: { color: "#ffb8b8" } }, error) : null) : null,
366
+ confirming ? React.createElement(ConfirmationDialog, { title: "删除这条记忆", message: "确定删除这条记忆?不可撤销。操作前会保留备份。", submitLabel: "确定删除",
367
+ busy: busy, error: error, onClose: function () { setConfirming(false); }, onConfirm: function () { mutate("DELETE"); } }) : null);
368
+ }
369
+
370
+ function MemoryList(props) {
371
+ var searchState = React.useState("");
372
+ var search = searchState[0], setSearch = searchState[1];
373
+ var queryState = React.useState("");
374
+ var query = queryState[0], setQuery = queryState[1];
375
+ var kindState = React.useState("");
376
+ var kind = kindState[0], setKind = kindState[1];
377
+ var sortState = React.useState("recent");
378
+ var sort = sortState[0], setSort = sortState[1];
379
+ var offsetState = React.useState(0);
380
+ var offset = offsetState[0], setOffset = offsetState[1];
381
+ var pageState = React.useState({ items: [], total: 0, allTotal: 0 });
382
+ var page = pageState[0], setPage = pageState[1];
383
+ var loadingState = React.useState(true);
384
+ var loading = loadingState[0], setLoading = loadingState[1];
385
+ var errorState = React.useState("");
386
+ var error = errorState[0], setError = errorState[1];
387
+ var retryState = React.useState(0);
388
+ var retry = retryState[0], setRetry = retryState[1];
389
+ function refresh() {
390
+ setOffset(0); setRetry(function (value) { return value + 1; });
391
+ if (props.onChanged) props.onChanged();
392
+ }
393
+ React.useEffect(function () {
394
+ if (search === query) return;
395
+ var timer = setTimeout(function () { setOffset(0); setQuery(search); }, 250);
396
+ return function () { clearTimeout(timer); };
397
+ }, [search]);
398
+ React.useEffect(function () {
399
+ var controller = new AbortController();
400
+ var active = true;
401
+ setLoading(true); setError("");
402
+ var params = new URLSearchParams({ q: query, kind: kind, sort: sort, limit: "50", offset: String(offset) });
403
+ readApi("/memory-engine/api/list?" + params, controller.signal).then(function (result) {
404
+ if (!active) return;
405
+ setPage(function (previous) { return Object.assign({}, result, { items: offset === 0 ? result.items : previous.items.concat(result.items) }); });
406
+ }).catch(function (failure) {
407
+ if (active) setError("加载记忆失败:" + failure.message);
408
+ }).finally(function () { if (active) setLoading(false); });
409
+ return function () { active = false; controller.abort(); };
410
+ }, [query, kind, sort, offset, retry]);
411
+ return React.createElement("section", {
412
+ "aria-label": "记忆列表", "data-memory-engine-list": "true",
413
+ style: { position: "absolute", inset: "76px 0 0", display: "flex", flexDirection: "column", color: "#dbeaf8", font: "14px/1.6 system-ui,sans-serif" },
414
+ }, React.createElement("div", { style: { width: "min(980px,100%)", margin: "0 auto", padding: "0 24px 16px", display: "flex", gap: "12px", flexWrap: "wrap" } },
415
+ React.createElement("input", { type: "search", "aria-label": "搜索记忆", placeholder: "搜索正文或标题…", value: search,
416
+ onChange: function (event) { setSearch(event.target.value); }, style: Object.assign({}, fieldStyle, { flex: "1 1 220px" }) }),
417
+ React.createElement("select", { "aria-label": "分类筛选", value: kind, style: fieldStyle,
418
+ onChange: function (event) { setOffset(0); setKind(event.target.value); } },
419
+ [""].concat(kinds).map(function (value) { return React.createElement("option", { key: value, value: value }, value || "全部分类"); })),
420
+ React.createElement("select", { "aria-label": "记忆排序", value: sort, style: fieldStyle,
421
+ onChange: function (event) { setOffset(0); setSort(event.target.value); } },
422
+ [["recent", "最近"], ["oldest", "最老"], ["fading", "快忘了"], ["degree", "连接最多"]].map(function (entry) { return React.createElement("option", { key: entry[0], value: entry[0] }, entry[1]); }))),
423
+ React.createElement("div", { "data-memory-engine-list-scroll": "true", style: { flex: 1, minHeight: 0, overflowY: "auto", padding: "0 24px 24px" } },
424
+ React.createElement("div", { style: { maxWidth: "932px", margin: "0 auto" }, "aria-busy": loading },
425
+ error ? React.createElement("div", { role: "alert", style: { color: "#ffb8b8", padding: "16px" } }, error,
426
+ React.createElement("button", { type: "button", style: actionStyle, onClick: function () { setRetry(function (value) { return value + 1; }); } }, "重试")) : null,
427
+ page.items.map(function (item) {
428
+ return React.createElement(MemoryRow, { key: item.id, item: item, onChanged: refresh });
429
+ }),
430
+ !loading && !error && !page.items.length ? React.createElement("p", { role: "status", style: { padding: "24px", textAlign: "center", color: "#9fb7ce" } }, page.allTotal ? "没有匹配的记忆,试试其他关键词或分类。" : "还没有记忆,可以点右上角 ? 查看导入方法。") : null)),
431
+ React.createElement("div", { "data-memory-engine-list-total": "true", style: { padding: "14px", textAlign: "center", color: "#9fb7ce", flexShrink: 0, borderTop: "1px solid #263f54" } },
432
+ "共 " + page.total + " 条 · 已显示 " + page.items.length + " 条",
433
+ loading ? React.createElement("span", { role: "status" }, " · 正在加载…") : null,
434
+ page.items.length < page.total ? React.createElement("button", { type: "button", "data-memory-engine-more": "true", disabled: loading || search !== query || !!error,
435
+ onClick: function () { setOffset(page.items.length); }, style: Object.assign({}, actionStyle, { marginLeft: "12px", opacity: loading ? 0.5 : 1 }) }, "加载更多") : null));
436
+ }
437
+
438
+ function MemorySettings(props) {
439
+ var statsState = React.useState(null);
440
+ var stats = statsState[0], setStats = statsState[1];
441
+ var errorState = React.useState("");
442
+ var error = errorState[0], setError = errorState[1];
443
+ var noticeState = React.useState("");
444
+ var notice = noticeState[0], setNotice = noticeState[1];
445
+ var retryState = React.useState(0);
446
+ var retry = retryState[0], setRetry = retryState[1];
447
+ var busyState = React.useState(false);
448
+ var busy = busyState[0], setBusy = busyState[1];
449
+ var confirmState = React.useState(false);
450
+ var confirming = confirmState[0], setConfirming = confirmState[1];
451
+ var mutation = React.useRef(null);
452
+ var download = React.useRef(null);
453
+ var timer = React.useRef(null);
454
+ var active = React.useRef(true);
455
+ React.useEffect(function () {
456
+ active.current = true;
457
+ return function () {
458
+ active.current = false;
459
+ if (mutation.current) mutation.current.abort();
460
+ clearTimeout(timer.current);
461
+ if (download.current) URL.revokeObjectURL(download.current);
462
+ };
463
+ }, []);
464
+ React.useEffect(function () {
465
+ var controller = new AbortController();
466
+ setError("");
467
+ readApi("/memory-engine/api/stats", controller.signal).then(function (result) {
468
+ if (!controller.signal.aborted) setStats(result);
469
+ }).catch(function (failure) {
470
+ if (!controller.signal.aborted) setError("读取设置失败:" + failure.message);
471
+ });
472
+ return function () { controller.abort(); };
473
+ }, [retry]);
474
+ function manage(clearing) {
475
+ if (mutation.current) return;
476
+ var controller = new AbortController();
477
+ mutation.current = controller; setBusy(true); setError(""); setNotice("");
478
+ var options = { signal: controller.signal };
479
+ if (clearing) { options.method = "POST"; options.headers = { "Content-Type": "application/json" }; options.body = JSON.stringify({ confirm: "DELETE" }); }
480
+ requestApi("/memory-engine/api/" + (clearing ? "clear" : "export"), options).then(function (result) {
481
+ if (controller.signal.aborted) return;
482
+ if (clearing) { props.onCleared(); return; }
483
+ if (download.current) URL.revokeObjectURL(download.current);
484
+ clearTimeout(timer.current);
485
+ download.current = URL.createObjectURL(new Blob([JSON.stringify({ ok: true, data: result }, null, 2) + "\n"], { type: "application/json;charset=utf-8" }));
486
+ var anchor = document.createElement("a");
487
+ var date = new Date();
488
+ anchor.href = download.current;
489
+ anchor.download = "memories-" + date.getFullYear() + "-" + String(date.getMonth() + 1).padStart(2, "0") + "-" + String(date.getDate()).padStart(2, "0") + ".json";
490
+ document.body.appendChild(anchor); anchor.click(); anchor.remove();
491
+ timer.current = setTimeout(function () { URL.revokeObjectURL(download.current); download.current = null; timer.current = null; }, 1000);
492
+ setNotice("已开始下载,共 " + result.total + " 条记忆。");
493
+ }).catch(function (failure) {
494
+ if (!controller.signal.aborted) setError((clearing ? "清空失败:" : "导出失败:") + failure.message);
495
+ }).finally(function () {
496
+ if (!controller.signal.aborted) { mutation.current = null; setBusy(false); }
497
+ });
498
+ }
499
+ var directory = stats ? stats.file.replace(/[\\/][^\\/]*$/, "") : "";
500
+ return React.createElement("section", { "aria-label": "记忆设置", "data-memory-engine-settings": "true",
501
+ style: { position: "absolute", inset: "76px 0 0", overflowY: "auto", padding: "0 24px 32px", color: "#dbeaf8", font: "14px/1.7 system-ui,sans-serif" } },
502
+ React.createElement("div", { style: { maxWidth: "900px", margin: "0 auto", padding: "28px", border: "1px solid #263f54", borderRadius: "14px", background: "#0b1624" } },
503
+ React.createElement("h2", { style: { margin: "0 0 20px", fontSize: "20px", color: "#b7e9ff" } }, "数据管理"),
504
+ stats ? React.createElement(React.Fragment, null,
505
+ React.createElement("div", { style: { display: "flex", alignItems: "center", flexWrap: "wrap", gap: "12px" } },
506
+ React.createElement("span", { style: { flex: "1 1 360px", overflowWrap: "anywhere" } }, "数据目录:", React.createElement("code", { "data-memory-engine-directory": "true" }, directory)),
507
+ React.createElement("button", { type: "button", "data-memory-engine-copy-path": "true", style: actionStyle,
508
+ onClick: function () {
509
+ setNotice(""); setError("");
510
+ copyText(directory).then(function () { if (active.current) setNotice("数据目录已复制 ✓"); }, function () { if (active.current) setError("复制路径失败,请手动复制上方目录。"); });
511
+ } }, "复制路径")),
512
+ React.createElement("p", { "data-memory-engine-storage-stats": "true", style: { color: "#9fb7ce", margin: "16px 0 24px" } }, "记忆条数:" + stats.total + "  文件大小:" + (stats.bytes / 1024).toFixed(1) + " KB"))
513
+ : React.createElement("p", { role: "status" }, error ? "暂时无法读取数据目录。" : "正在读取数据目录…"),
514
+ error ? React.createElement("div", { role: "alert", style: { color: "#ffb8b8", marginBottom: "16px" } }, error,
515
+ !stats ? React.createElement("button", { type: "button", style: actionStyle, onClick: function () { setRetry(function (value) { return value + 1; }); } }, "重试") : null) : null,
516
+ notice ? React.createElement("p", { role: "status", style: { color: "#9fe5c4" } }, notice) : null,
517
+ React.createElement("div", { style: { padding: "24px 0", borderTop: "1px solid #263f54", borderBottom: "1px solid #263f54" } },
518
+ React.createElement("button", { type: "button", "data-memory-engine-export": "true", disabled: busy, onClick: function () { manage(false); }, style: actionStyle }, "导出全部记忆(JSON)"),
519
+ React.createElement("p", { style: { margin: "10px 0 0", fontSize: "12px", color: "#9fb7ce" } }, "下载包含全部原始条目的 JSON 文件。导入已有记忆请使用 memory_import 工具。")),
520
+ React.createElement("h3", { style: { color: "#ffb8b8", marginTop: "24px" } }, "危险操作"),
521
+ React.createElement("p", { style: { color: "#9fb7ce" } }, "清空前会自动备份,最近五份操作备份保留在数据目录。"),
522
+ React.createElement("button", { type: "button", "data-memory-engine-clear": "true", disabled: busy,
523
+ onClick: function () { setError(""); setNotice(""); setConfirming(true); }, style: Object.assign({}, actionStyle, { color: "#ffb8b8", borderColor: "#8b505c", background: "#341c28" }) }, "清空全部记忆")),
524
+ confirming ? React.createElement(ConfirmationDialog, { title: "清空全部记忆", message: "这会删除当前记忆库的全部条目。请确认已导出需要保留的内容。", requireDelete: true,
525
+ submitLabel: "确认清空", busy: busy, error: error, onClose: function () { setConfirming(false); }, onConfirm: function () { manage(true); } }) : null);
526
+ }
527
+
231
528
  function MemoryButton(props) {
232
529
  var settings = props.settings;
233
530
  var state = React.useState(false);
@@ -248,6 +545,8 @@ window.__ModuleLoader__.load({
248
545
  var selectedState = React.useState(null);
249
546
  var selected = selectedState[0];
250
547
  var setSelected = selectedState[1];
548
+ var tabState = React.useState("map");
549
+ var tab = tabState[0], setTab = tabState[1];
251
550
  var renderer = React.useRef(null);
252
551
  var selectionVersion = React.useRef(0);
253
552
  var pointer = React.useRef(null);
@@ -262,6 +561,7 @@ window.__ModuleLoader__.load({
262
561
  dismissed.current = false;
263
562
  setGuideOpen(false);
264
563
  setSelected(null);
564
+ setTab("map");
265
565
  setOpen(Date.now());
266
566
  }
267
567
  function closeMap() { setSelected(null); setOpen(false); }
@@ -302,6 +602,7 @@ window.__ModuleLoader__.load({
302
602
  function onEscape() { if (guideOpen) closeGuide(); else if (selected) setSelected(null); else closeMap(); }
303
603
  function onKey(event) {
304
604
  if (event.key !== "Escape") return;
605
+ if (event.target && event.target.closest && event.target.closest("[data-memory-engine-confirm]")) return;
305
606
  event.preventDefault();
306
607
  event.stopPropagation();
307
608
  if (!event.repeat) onEscape();
@@ -325,12 +626,20 @@ window.__ModuleLoader__.load({
325
626
  onPointerDownCapture: startPointer, onPointerMoveCapture: movePointer,
326
627
  onPointerCancelCapture: function () { pointer.current = null; }, onClick: dismissOutside,
327
628
  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 }),
629
+ }, tab === "map" ? React.createElement(MemoryCanvas, { key: open, rendererRef: renderer, selectionVersion: selectionVersion, onSelect: setSelected })
630
+ : tab === "memories" ? React.createElement(MemoryList, { onChanged: function () { setRetry(function (value) { return value + 1; }); } })
631
+ : React.createElement(MemorySettings, { onCleared: function () { openMap(); setGuideOpen(true); } }),
632
+ tab !== "map" ? React.createElement("strong", { style: { position: "absolute", top: "23px", left: "24px", font: "600 18px system-ui,sans-serif", color: "#b7e9ff" } }, "记忆管理") : null,
329
633
  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
634
  React.createElement("div", {
331
635
  "data-memory-engine-controls": "true",
332
636
  style: { position: "absolute", top: "16px", right: "18px", zIndex: 1, display: "flex", gap: "8px" },
333
- }, React.createElement("button", {
637
+ }, React.createElement("div", { role: "tablist", "aria-label": "记忆面板", style: { display: "flex", gap: "4px", marginRight: "8px" } },
638
+ [["map", "图谱"], ["memories", "记忆"], ["settings", "设置"]].map(function (entry) {
639
+ return React.createElement("button", { key: entry[0], type: "button", role: "tab", "aria-selected": tab === entry[0], "data-memory-engine-tab": entry[0],
640
+ onClick: function () { setSelected(null); setTab(entry[0]); },
641
+ style: Object.assign({}, actionStyle, { background: tab === entry[0] ? "#204762" : "#091422", borderColor: tab === entry[0] ? "#8acded" : "#35536b" }) }, entry[1]);
642
+ })), React.createElement("button", {
334
643
  ref: help, type: "button", onClick: function () { setGuideOpen(true); },
335
644
  "aria-label": "查看使用指南", title: "使用指南", style: actionStyle,
336
645
  }, "?"), React.createElement("button", {
package/lib/config.js CHANGED
@@ -8,9 +8,30 @@ 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()) {
11
+ export function readUiConfig(home = defaultHome(), options = {}) {
12
12
  const config = readConfig(home);
13
- return { hideButton: config.hideButton === true, showGuideOnEmpty: config.showGuideOnEmpty !== false };
13
+ return { hideButton: config.hideButton === true, showGuideOnEmpty: config.showGuideOnEmpty !== false,
14
+ hasPersona: readPersona(config, options) !== null, personaTitle: config.personaTitle, spinSpeed: config.spinSpeed };
15
+ }
16
+
17
+ export function readPersona(config, { now = Date.now(), warn = message => console.warn(`[dsh-memory-engine] ${message}`) } = {}) {
18
+ if (config.personaText?.trim()) return { text: config.personaText, ts: now };
19
+ if (!config.personaFile?.trim()) return null;
20
+ let descriptor;
21
+ try {
22
+ descriptor = fs.openSync(config.personaFile, 'r');
23
+ const metadata = fs.fstatSync(descriptor);
24
+ if (!metadata.isFile()) throw new Error('角色卡路径不是普通文件');
25
+ const buffer = Buffer.alloc(16003);
26
+ const length = fs.readSync(descriptor, buffer, 0, buffer.length, 0);
27
+ const text = Array.from(buffer.subarray(0, length).toString('utf8').replace(/^\uFEFF/u, '')).slice(0, 4000).join('');
28
+ return text.trim() ? { text, ts: metadata.mtimeMs } : null;
29
+ } catch (error) {
30
+ try { warn(`读取角色卡失败,使用原布局:${error.message}`); } catch {}
31
+ return null;
32
+ } finally {
33
+ if (descriptor !== undefined) fs.closeSync(descriptor);
34
+ }
14
35
  }
15
36
 
16
37
  export function readConfig(home = defaultHome()) {
@@ -18,9 +39,16 @@ export function readConfig(home = defaultHome()) {
18
39
  try {
19
40
  const value = JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/u, ''));
20
41
  if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('配置必须是 JSON 对象');
21
- return value;
42
+ for (const key of ['personaFile', 'personaText', 'personaTitle']) {
43
+ if (value[key] !== undefined && typeof value[key] !== 'string') throw new Error(`${key} 必须是字符串`);
44
+ }
45
+ if (value.personaText !== undefined && Array.from(value.personaText).length > 4000) throw new Error('personaText 最多 4000 字符');
46
+ if (value.spinSpeed !== undefined && (!Number.isFinite(value.spinSpeed) || value.spinSpeed < 0.05 || value.spinSpeed > 1.5)) {
47
+ throw new Error('spinSpeed 必须是 0.05~1.5 的数字');
48
+ }
49
+ return { ...value, personaTitle: value.personaTitle?.trim() || '角色卡', spinSpeed: value.spinSpeed ?? 0.35 };
22
50
  } catch (error) {
23
- if (error.code === 'ENOENT') return {};
51
+ if (error.code === 'ENOENT') return { personaTitle: '角色卡', spinSpeed: 0.35 };
24
52
  throw new Error(`无法读取 ${file}:${error.message}`);
25
53
  }
26
54
  }
package/lib/map.js CHANGED
@@ -4,16 +4,23 @@ import { randomBytes } from 'node:crypto';
4
4
  import { spawn } from 'node:child_process';
5
5
  import { pathToFileURL } from 'node:url';
6
6
  import { scoreMemories, layoutMemories, renderHtml } from './render/index.mjs';
7
- import { readConfig } from './config.js';
7
+ import { readConfig, readPersona } from './config.js';
8
8
 
9
- export function computeMapData(store) {
9
+ export function computeMapData(store, { warn } = {}) {
10
10
  const items = store.read().map(({ id, text, title: nodeTitle, ts, kind, heat }) => ({ id, text,
11
11
  title: nodeTitle || Array.from(text).slice(0, 20).join(''), ts, kind, heat }));
12
12
  const now = store.now();
13
+ const config = readConfig(path.dirname(path.dirname(store.file)));
14
+ const persona = readPersona(config, { now, warn });
13
15
  const scored = scoreMemories(items, now);
14
- const nodes = layoutMemories(scored.nodes);
16
+ const core = persona ? { id: 'persona', text: persona.text, title: config.personaTitle, kind: '人格', heat: 1,
17
+ ts: persona.ts, isCore: true, retention: 1, degree: 0, emotion: [], ageDays: 0, boosts: 0,
18
+ pos: { x: 0, y: 0, z: 0 }, radius: 6.5 } : null;
19
+ const nodes = layoutMemories(core ? [core, ...scored.nodes] : scored.nodes);
15
20
  const kinds = Object.fromEntries([...new Set(nodes.map(node => node.kind))].map(kind => [kind, nodes.filter(node => node.kind === kind).length]));
16
21
  return { generatedAt: now, nodes, links: scored.links, review: scored.review,
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
+ spinSpeed: config.spinSpeed,
17
24
  stats: { total: nodes.length, byKind: kinds, forgettingSoon: nodes.filter(node => node.retention < 0.3).length } };
18
25
  }
19
26