dsh-memory-engine 0.7.0-beta.6b → 0.7.1
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 +39 -20
- package/lib/auto-memory.js +36 -6
- package/lib/automation-config.js +1 -1
- package/lib/client.js +71 -6
- package/lib/client.src.js +71 -6
- package/lib/config-write.js +74 -0
- package/lib/config.js +2 -1
- package/lib/index.js +1 -1
- package/lib/onboard.js +19 -4
- package/lib/routes.js +9 -3
- package/package.json +6 -3
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,20 +1,33 @@
|
|
|
1
|
-
# DSH 记忆引擎
|
|
1
|
+
# DSH 记忆引擎
|
|
2
2
|
|
|
3
3
|
DSH 插件的本地记忆引擎,带神经网络可视化。它让 agent 保存、召回和管理跨会话记忆,也能把你已有的记忆文件导进来。
|
|
4
4
|
|
|
5
|
-
当前版本:0.7.
|
|
5
|
+
当前版本:0.7.1。Node ≥22,零新增依赖。自动记忆默认关闭,由用户在图谱头部或设置页手动打开;开启后每轮对话会额外调用一次模型,可能产生费用。自动导入默认只收用户消息,支持流程话过滤、规则分类、错误原因和一次性重扫。
|
|
6
6
|
|
|
7
|
-
## 0.7.0
|
|
7
|
+
## 从 0.6.1 升级到 0.7.0 的变化
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
- 不再占用 18886 端口,改用 DSH 宿主的同源路由。
|
|
10
|
+
- 图谱从 iframe 改为直接绘制的 Canvas,保留旋转、缩放和节点详情。
|
|
11
|
+
- 新增中心角色卡、球体布局、记忆管理面板、自动记忆和自动导入。
|
|
12
|
+
- 记忆数据格式不变,可以直接升级,无需迁移命令。已有配置明确设置的启用状态会保留;未配置自动记忆时默认关闭。
|
|
13
|
+
|
|
14
|
+
## 已知限制
|
|
15
|
+
|
|
16
|
+
- 检索按字面匹配,不理解同义词,例如查“自行车”不能自动匹配只写“单车”的记忆。
|
|
17
|
+
- 单机使用,没有跨设备同步。
|
|
18
|
+
- 数据保存为明文 JSON,没有加密。
|
|
19
|
+
- 只支持 DSH 宿主。
|
|
20
|
+
- 自动记忆开启后会产生额外模型调用费用,实际金额取决于模型和计费方案。
|
|
21
|
+
|
|
22
|
+
## 0.7.0 功能
|
|
23
|
+
|
|
24
|
+
| 功能 | 内容 | 使用方式 |
|
|
10
25
|
| --- | --- | --- |
|
|
11
|
-
|
|
|
12
|
-
|
|
|
13
|
-
|
|
|
14
|
-
|
|
|
15
|
-
|
|
|
16
|
-
| 中心角色卡 · 本版完成 | 固定角色核心、分层球壳、慢速自转 | 角色卡全文与复制;拖拽暂停,松手 2 秒恢复;缺省退回原布局 |
|
|
17
|
-
| 引导小修 · 已完成 | 空库和手动回看共用三步卡片 | 编号步骤、复制提示词、刷新和关闭卡片可用 |
|
|
26
|
+
| 原生图谱 | 同源路由、React Canvas,界面与离线 HTML 共用绘制源码 | 旋转、缩放、点击详情;离开页面停止动画 |
|
|
27
|
+
| 记忆管理 | 列表、搜索、编辑、删除、导出和清空 | 分页、筛选、确认与错误提示;写操作先备份 |
|
|
28
|
+
| 中心角色卡 | 固定角色核心、分层球壳、慢速自转 | 点击查看全文;拖拽暂停,松手 2 秒恢复 |
|
|
29
|
+
| 自动记忆开关 | 默认关闭,头部与设置页同步 | 首次开启提示成本;关闭即取消排队抽取并阻止迟到结果写库 |
|
|
30
|
+
| 使用引导 | 空库和手动回看共用三步卡片 | 复制提示词、刷新、知道了/Esc 关闭 |
|
|
18
31
|
|
|
19
32
|
- [x] 引导回看:右上角「?」、中央卡片、复制提示词、知道了/Esc 关闭。
|
|
20
33
|
- [x] 按钮开关:hideButton、showGuideOnEmpty;配置请求失败时保留按钮并警告。
|
|
@@ -32,7 +45,7 @@ DSH 插件的本地记忆引擎,带神经网络可视化。它让 agent 保存
|
|
|
32
45
|
| 有 ctx.effect | 正常注册,随插件卸载一起清理 |
|
|
33
46
|
| 没有 ctx.effect | 直接注册工具,功能不减少,日志留一条提示 |
|
|
34
47
|
| 解析不到 @deepseek-ai/dsh-tools | 用内置直通 defineTool,工具照常可用 |
|
|
35
|
-
| 没有 header.actions 插槽 | 图谱按钮退到
|
|
48
|
+
| 没有 header.actions 插槽 | 图谱按钮退到 `views.actions`(在「更多视图」菜单里) |
|
|
36
49
|
| 完全没有 slots 服务 | 跳过按钮,记忆工具不受影响 |
|
|
37
50
|
|
|
38
51
|
peerDependencies 已放宽为 >=0.1.0-rc.8 <0.3.0 并标为 optional——宿主自带这份 SDK,不该因为版本号不匹配而装不上。
|
|
@@ -45,7 +58,7 @@ peerDependencies 已放宽为 >=0.1.0-rc.8 <0.3.0 并标为 optional——宿主
|
|
|
45
58
|
|
|
46
59
|
安装并启用插件后,完全退出 DSH 再打开。进入任意会话,头部第一行会出现 **🧠 记忆图谱** 按钮。
|
|
47
60
|
|
|
48
|
-
点击后全屏显示图谱,顶部有 **图谱 / 记忆 / 设置** 三个 tab
|
|
61
|
+
点击后全屏显示图谱,顶部有 **图谱 / 记忆 / 设置** 三个 tab,**?** 左侧是 **○ 自动记忆 / ● 自动记忆** 开关,右上角 **关闭 ×** 退出。鼠标悬停可看当前状态和抽取上限。设置页也有同一个开关及成本说明,两处同步、刷新后保持状态。第一次开启会提示模型费用;保存失败恢复原状态,并显示红色原因。每次打开或从其他 tab 返回图谱都会读取当前记忆库,在内存中计算当前图谱,不写入离线 HTML。离开图谱 tab 时停止 Canvas 动画。
|
|
49
62
|
|
|
50
63
|
**记忆 tab** 按正文或标题搜索(250ms 防抖),可筛选知识、印象、对话、事件、日记,按最近、最老、快忘了或连接最多排序。每页 50 条,底部显示过滤后的总数和「加载更多」。点一条展开全文,再点「编辑」修改正文、分类、重要度或相关人;保存失败保留草稿并显示服务端错误,取消不发送写请求。删除必须再次确认。
|
|
51
64
|
|
|
@@ -83,7 +96,7 @@ peerDependencies 已放宽为 >=0.1.0-rc.8 <0.3.0 并标为 optional——宿主
|
|
|
83
96
|
```json
|
|
84
97
|
{
|
|
85
98
|
"personaFile": "C:/path/to/role.md",
|
|
86
|
-
"personaTitle": "
|
|
99
|
+
"personaTitle": "小夏",
|
|
87
100
|
"spinSpeed": 0.35
|
|
88
101
|
}
|
|
89
102
|
```
|
|
@@ -112,7 +125,8 @@ peerDependencies 已放宽为 >=0.1.0-rc.8 <0.3.0 并标为 optional——宿主
|
|
|
112
125
|
| GET | `/memory-engine/api/map` | `{ nodes, links, review, stats, generatedAt, core, spinSpeed }`;配置角色卡时 nodes[0] 为 isCore,core 含 id/title/preview,否则为 null;stats 统计图谱节点 |
|
|
113
126
|
| GET | `/memory-engine/api/stats` | 记忆总数、分类、衰减状态及存储位置 |
|
|
114
127
|
| GET | `/memory-engine/api/onboard` | `{ empty, prompt, steps, dataDir }`,空库和手动帮助共用 |
|
|
115
|
-
| GET | `/memory-engine/api/config` |
|
|
128
|
+
| GET | `/memory-engine/api/config` | 保留既有字段,`autoMemory` 增加 `dailyCap`,另增加 `showAutoMemoryCostHint`;仍返回 `autoImport: { enabled, sourceCount }` 和 `lastError: { at, stage, message }`,不返回 raw、来源路径或模型配置 |
|
|
129
|
+
| PUT | `/memory-engine/api/config` | 合并合法白名单字段,返回公开配置和 `backup` 文件名 |
|
|
116
130
|
| GET | `/memory-engine/api/list?q=&kind=&sort=&limit=&offset=` | `{ total, allTotal, limit, offset, items }`;包含留存、连接数和 60 字预览 |
|
|
117
131
|
| GET | `/memory-engine/api/item/:id` | 完整条目、留存和连接数;不存在返回 404 |
|
|
118
132
|
| PUT | `/memory-engine/api/item/:id` | 仅更新传入的 text/kind/heat/who;返回更新条目和 backup |
|
|
@@ -120,7 +134,11 @@ peerDependencies 已放宽为 >=0.1.0-rc.8 <0.3.0 并标为 optional——宿主
|
|
|
120
134
|
| GET | `/memory-engine/api/export` | `{ total, items }`,条目原样导出 |
|
|
121
135
|
| POST | `/memory-engine/api/clear` | 请求 `{ confirm: "DELETE" }`,返回 `{ removed, backup }` |
|
|
122
136
|
|
|
123
|
-
JSON 成功返回 `{ "ok": true, "data": ... }`,失败返回 `{ "ok": false, "error": "说明" }
|
|
137
|
+
JSON 成功返回 `{ "ok": true, "data": ... }`,失败返回 `{ "ok": false, "error": "说明" }`。既有接口返回结构保留;stats/list/export 仍只统计实际记忆。接口仅接受本机 Host,严格限制路径,拒绝路径穿越;不允许的方法返回 405。只读接口不接收请求体,PUT 和 POST 只接收 JSON 对象,声明大小和实际分块累计大小都限制在 1MB。
|
|
138
|
+
|
|
139
|
+
配置 PUT 使用嵌套对象,例如 `{ "autoMemory": { "enabled": false } }`。只允许 `autoMemory.enabled`、`autoImport.enabled`、`autoMemory.maxPerTurn`(1~5 整数)、`autoMemory.dailyCap`(0~500 整数)、`spinSpeed`(0.05~1.5 数字)、`showGuideOnEmpty` 和 `hideButton`;所有启用/显示开关只接受布尔值。任何未知字段或类型错误会整次返回 400,不改任何字段。角色卡、来源路径、模型等不能通过此接口修改。
|
|
140
|
+
|
|
141
|
+
配置写入前生成 `config.json.bak-<时间戳>`,同毫秒加序号,保留最近五份;先写临时文件再 rename。备份或写入失败保留原配置。首次成功启用时,服务端自行保存 `showAutoMemoryCostHint: false`,不向客户端额外开放这个字段的写权限。关闭会清空当前收集的对话、取消排队和进行中的抽取,重新开启时重置冷却和失败退避;自动导入独立运行,若要冻结所有自动入库,需同时关闭它。
|
|
124
142
|
|
|
125
143
|
列表默认最近优先、每页 50 条,limit 上限 200;搜索为不区分大小写的包含匹配,kind 精确匹配。留存复用 `decay.retention()`,连接数复用全库 `scoreMemories()`,筛选不会改变连接数。编辑只允许 text(1~2000 字符且非全空白)、kind(1~20 字符)、heat(整数 1/2/3)、who(0~40 字符);不会改动 id、时间、强化次数或触发相似合并。
|
|
126
144
|
|
|
@@ -138,7 +156,7 @@ memory_map(out="D:\\memory-map.html")
|
|
|
138
156
|
|
|
139
157
|
## 自动记忆和自动导入
|
|
140
158
|
|
|
141
|
-
|
|
159
|
+
自动记忆默认关闭;在图谱头部「○ 自动记忆」或设置页可以一键打开,首次开启会显示成本提示。开启后每轮对话会多一次模型调用,异步抽取最多两条事实,不依赖 agent 调用工具。自动导入默认开启,在启动 8 秒后读取 DSH 已有会话,之后每小时续导;导入本身不调用模型。新装空库也会逐步获得历史记录。
|
|
142
160
|
|
|
143
161
|
在现有 `config.json` 中合并:
|
|
144
162
|
|
|
@@ -220,7 +238,7 @@ memory_recall(q="朋友", limit=5)
|
|
|
220
238
|
|
|
221
239
|
召回结果区分直接命中和联想到的记忆。`memory_stats()` 查看总数和分类;`memory_dedupe()` 默认仅预览重复组。`memory_forget` 支持按 id 精确删除;关键词批量删除必须先预览,并经用户确认后传 `confirm=true`。
|
|
222
240
|
|
|
223
|
-
**检索是按字面词匹配,不做语义理解。**
|
|
241
|
+
**检索是按字面词匹配,不做语义理解。** 库里只写“单车”,`memory_recall(q="自行车")` 就查不到。想覆盖不同说法,存记忆时可以同时写出常用名称,例如“小夏把自行车也叫作单车”。
|
|
224
242
|
|
|
225
243
|
**联想到的记忆**来自关键词共享:两条记忆提到同一个词,就会被连起来,查其中一条时另一条一起返回。它不是推理,是顺着关联往外摸。扩散默认一跳(`hops=1`),邻居要留存 > 0.4 才会被带出来。
|
|
226
244
|
|
|
@@ -235,7 +253,7 @@ JSON 数组文件(UTF-8,支持 BOM):
|
|
|
235
253
|
```json
|
|
236
254
|
[
|
|
237
255
|
{"text":"他说要一起养猫","ts":1793291223536,"kind":"对话","heat":1},
|
|
238
|
-
{"text":"
|
|
256
|
+
{"text":"手冲咖啡先按一比十五的粉水比试喝","kind":"知识"}
|
|
239
257
|
]
|
|
240
258
|
```
|
|
241
259
|
|
|
@@ -263,7 +281,7 @@ memory_import(source="markdown", path="D:\\memory-notes", dry_run=false)
|
|
|
263
281
|
|
|
264
282
|
```text
|
|
265
283
|
他说把我当朋友
|
|
266
|
-
|
|
284
|
+
手冲咖啡先按一比十五的粉水比试喝
|
|
267
285
|
```
|
|
268
286
|
|
|
269
287
|
```text
|
|
@@ -355,3 +373,4 @@ node --test test/*.test.mjs
|
|
|
355
373
|
## 许可
|
|
356
374
|
|
|
357
375
|
MIT。可以商用、可以修改、可以再发布,保留版权声明即可(见 [LICENSE](./LICENSE))。
|
|
376
|
+
|
package/lib/auto-memory.js
CHANGED
|
@@ -5,6 +5,7 @@ import { backupForAutomation, service, warningLogger } from './automation-state.
|
|
|
5
5
|
import { createTurnCollector, usefulText } from './session-text.js';
|
|
6
6
|
import { updateAutoLog } from './auto-log.js';
|
|
7
7
|
import { isNoise } from './noise.js';
|
|
8
|
+
import { onConfigUpdate } from './config-write.js';
|
|
8
9
|
|
|
9
10
|
export function extractionPrompt(text, maxPerTurn) {
|
|
10
11
|
return `你是一个记忆抽取器。从下面的对话里,找出「值得长期记住」的事实,最多 ${maxPerTurn} 条。
|
|
@@ -45,12 +46,32 @@ export function createAutoMemory({ ctx, store, now = Date.now, warn = () => {},
|
|
|
45
46
|
let stopped = false;
|
|
46
47
|
let failures = 0;
|
|
47
48
|
let backoffUntil = 0;
|
|
49
|
+
let generation = 0;
|
|
50
|
+
let previousEnabled = config().enabled;
|
|
48
51
|
|
|
49
|
-
|
|
52
|
+
function reset() {
|
|
53
|
+
generation++;
|
|
54
|
+
collector.clear();
|
|
55
|
+
for (const controller of active.values()) controller.abort();
|
|
56
|
+
active.clear();
|
|
57
|
+
lastStarted.clear();
|
|
58
|
+
failures = 0;
|
|
59
|
+
backoffUntil = 0;
|
|
60
|
+
}
|
|
50
61
|
|
|
51
|
-
|
|
62
|
+
function currentSettings() {
|
|
52
63
|
const settings = config();
|
|
64
|
+
if (settings.enabled !== previousEnabled) { reset(); previousEnabled = settings.enabled; }
|
|
65
|
+
return settings;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const updateLog = (change, failure) => updateAutoLog(logFile, change, now(), failure);
|
|
69
|
+
|
|
70
|
+
async function runExtract(turn, submittedGeneration) {
|
|
71
|
+
const settings = currentSettings();
|
|
72
|
+
if (submittedGeneration !== undefined && submittedGeneration !== generation) return;
|
|
53
73
|
if (stopped || !settings.enabled || now() < backoffUntil || active.has(turn.sessionId)) return;
|
|
74
|
+
const startedGeneration = generation;
|
|
54
75
|
if (!usefulText(turn.userText.trim())) return;
|
|
55
76
|
if (settings.excludePatterns.some(pattern => turn.userText.includes(pattern))) { updateLog({ skipped: 1 }); return; }
|
|
56
77
|
const llm = service(ctx, 'llm');
|
|
@@ -70,6 +91,7 @@ export function createAutoMemory({ ctx, store, now = Date.now, warn = () => {},
|
|
|
70
91
|
const info = await llm.resolveModelInfo(settings.provider, settings.model, signal);
|
|
71
92
|
reasoningEffort = info?.reasoning?.efforts?.[0]?.id;
|
|
72
93
|
}
|
|
94
|
+
signal.throwIfAborted();
|
|
73
95
|
const stream = llm.stream({ provider: settings.provider, model: settings.model,
|
|
74
96
|
messages: [{ role: 'user', content: [{ type: 'text', text: extractionPrompt(turn.text, settings.maxPerTurn) }] }],
|
|
75
97
|
maxTokens: 400, temperature: 0.2, signal, ...(reasoningEffort ? { reasoningEffort } : {}) });
|
|
@@ -84,7 +106,7 @@ export function createAutoMemory({ ctx, store, now = Date.now, warn = () => {},
|
|
|
84
106
|
if (chunk.type === 'finish' && chunk.reason?.kind === 'max-tokens') throw new Error('抽取达到输出上限,丢弃不完整结果');
|
|
85
107
|
}
|
|
86
108
|
if (signal.aborted) throw signal.reason;
|
|
87
|
-
if (stopped || !
|
|
109
|
+
if (stopped || !currentSettings().enabled || startedGeneration !== generation) return;
|
|
88
110
|
stage = 'parse';
|
|
89
111
|
const candidates = JSON.parse(raw);
|
|
90
112
|
if (!Array.isArray(candidates)) throw new Error('抽取结果不是 JSON 数组');
|
|
@@ -123,21 +145,28 @@ export function createAutoMemory({ ctx, store, now = Date.now, warn = () => {},
|
|
|
123
145
|
updateLog({ failed: 1 }, { stage, error, raw });
|
|
124
146
|
warning(`自动记忆抽取失败:${error.message}${!parsed && raw ? `;raw: ${Array.from(raw).slice(0, 200).join('')}` : ''}`);
|
|
125
147
|
} finally {
|
|
126
|
-
active.delete(turn.sessionId);
|
|
148
|
+
if (active.get(turn.sessionId) === controller) active.delete(turn.sessionId);
|
|
127
149
|
}
|
|
128
150
|
}
|
|
129
151
|
|
|
130
152
|
function submit(turn) {
|
|
131
|
-
const
|
|
153
|
+
const submittedGeneration = generation;
|
|
154
|
+
const task = Promise.resolve().then(() => runExtract(turn, submittedGeneration));
|
|
132
155
|
tasks.add(task);
|
|
133
156
|
void task.catch(error => warning(`自动记忆任务失败:${error.message}`)).finally(() => tasks.delete(task));
|
|
134
157
|
}
|
|
135
158
|
|
|
136
159
|
const collector = createTurnCollector(submit);
|
|
160
|
+
const removeConfigListener = onConfigUpdate(home, changes => {
|
|
161
|
+
if (changes.autoMemory && Object.hasOwn(changes.autoMemory, 'enabled')) {
|
|
162
|
+
reset();
|
|
163
|
+
previousEnabled = config().enabled;
|
|
164
|
+
}
|
|
165
|
+
});
|
|
137
166
|
let removeListener;
|
|
138
167
|
if (config().enabled && typeof service(ctx, 'llm')?.stream !== 'function') warning('llm 服务不可用,自动记忆已停用;现有工具不受影响', { once: true });
|
|
139
168
|
if (typeof ctx?.on === 'function') removeListener = ctx.on('session/event', (session, event) => {
|
|
140
|
-
try { if (!stopped) collector.accept(session, event); }
|
|
169
|
+
try { if (!stopped && currentSettings().enabled) collector.accept(session, event); }
|
|
141
170
|
catch (error) { warning(`收集本轮对话失败:${error.message}`); }
|
|
142
171
|
});
|
|
143
172
|
else warning('session/event 不可用,自动记忆已停用', { once: true });
|
|
@@ -148,6 +177,7 @@ export function createAutoMemory({ ctx, store, now = Date.now, warn = () => {},
|
|
|
148
177
|
dispose() {
|
|
149
178
|
stopped = true;
|
|
150
179
|
if (typeof removeListener === 'function') removeListener();
|
|
180
|
+
removeConfigListener();
|
|
151
181
|
collector.clear();
|
|
152
182
|
for (const controller of active.values()) controller.abort();
|
|
153
183
|
lastStarted.clear();
|
package/lib/automation-config.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const memoryDefaults = Object.freeze({ enabled:
|
|
1
|
+
export const memoryDefaults = Object.freeze({ enabled: false, provider: 'deepseek-account', model: 'deepseek-flash',
|
|
2
2
|
maxPerTurn: 2, minChars: 12, dailyCap: 40, cooldownSec: 30,
|
|
3
3
|
excludePatterns: Object.freeze(['密码', '身份证', '银行卡', '手机号', '验证码']) });
|
|
4
4
|
|
package/lib/client.js
CHANGED
|
@@ -403,7 +403,7 @@ function frame(now) {
|
|
|
403
403
|
|
|
404
404
|
|
|
405
405
|
|
|
406
|
-
var defaultSettings = { hideButton: false, showGuideOnEmpty: true };
|
|
406
|
+
var defaultSettings = { hideButton: false, showGuideOnEmpty: true, autoMemory: { enabled: false, maxPerTurn: 2, dailyCap: 40 }, showAutoMemoryCostHint: true };
|
|
407
407
|
var actionStyle = { padding: "7px 12px", borderRadius: "8px", border: "1px solid #6999b8", background: "#102236", color: "#e2f4ff", font: "inherit", cursor: "pointer" };
|
|
408
408
|
var fieldStyle = { boxSizing: "border-box", padding: "10px 12px", borderRadius: "8px", border: "1px solid #35536b", background: "#091422", color: "#e2f4ff", font: "inherit", minWidth: 0 };
|
|
409
409
|
var kinds = ["知识", "印象", "对话", "事件", "日记"];
|
|
@@ -823,6 +823,17 @@ function frame(now) {
|
|
|
823
823
|
onClick: function () { setOffset(page.items.length); }, style: Object.assign({}, actionStyle, { marginLeft: "12px", opacity: loading ? 0.5 : 1 }) }, "加载更多") : null));
|
|
824
824
|
}
|
|
825
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
|
+
|
|
826
837
|
function MemorySettings(props) {
|
|
827
838
|
var statsState = React.useState(null);
|
|
828
839
|
var stats = statsState[0], setStats = statsState[1];
|
|
@@ -888,6 +899,10 @@ function frame(now) {
|
|
|
888
899
|
return React.createElement("section", { "aria-label": "记忆设置", "data-memory-engine-settings": "true",
|
|
889
900
|
style: { position: "absolute", inset: "76px 0 0", overflowY: "auto", padding: "0 24px 32px", color: "#dbeaf8", font: "14px/1.7 system-ui,sans-serif" } },
|
|
890
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 条,可随时关闭。大约每轮几分钱,实际费用以模型及计费方案为准。自动导入是独立设置,不受此开关控制。")),
|
|
891
906
|
React.createElement("h2", { style: { margin: "0 0 20px", fontSize: "20px", color: "#b7e9ff" } }, "数据管理"),
|
|
892
907
|
stats ? React.createElement(React.Fragment, null,
|
|
893
908
|
React.createElement("div", { style: { display: "flex", alignItems: "center", flexWrap: "wrap", gap: "12px" } },
|
|
@@ -914,10 +929,18 @@ function frame(now) {
|
|
|
914
929
|
}
|
|
915
930
|
|
|
916
931
|
function MemoryButton(props) {
|
|
917
|
-
var settings = props.settings;
|
|
918
932
|
var state = React.useState(false);
|
|
919
933
|
var open = state[0];
|
|
920
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);
|
|
921
944
|
var guideState = React.useState(false);
|
|
922
945
|
var guideOpen = guideState[0];
|
|
923
946
|
var setGuideOpen = guideState[1];
|
|
@@ -940,6 +963,43 @@ function frame(now) {
|
|
|
940
963
|
var pointer = React.useRef(null);
|
|
941
964
|
var help = React.useRef(null);
|
|
942
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
|
+
}
|
|
943
1003
|
function closeGuide() {
|
|
944
1004
|
dismissed.current = true;
|
|
945
1005
|
setGuideOpen(false);
|
|
@@ -952,7 +1012,7 @@ function frame(now) {
|
|
|
952
1012
|
setTab("map");
|
|
953
1013
|
setOpen(Date.now());
|
|
954
1014
|
}
|
|
955
|
-
function closeMap() { setSelected(null); setOpen(false); }
|
|
1015
|
+
function closeMap() { setSelected(null); setCostHint(false); setOpen(false); }
|
|
956
1016
|
function startPointer(event) {
|
|
957
1017
|
if (event.target.closest("[data-memory-engine-detail], [data-memory-engine-guide-layer], [data-memory-engine-controls]")) { pointer.current = null; return; }
|
|
958
1018
|
pointer.current = { x: event.clientX, y: event.clientY, moved: 0, version: selectionVersion.current };
|
|
@@ -1016,7 +1076,8 @@ function frame(now) {
|
|
|
1016
1076
|
style: { position: "fixed", inset: 0, maxWidth: "none", maxHeight: "none", zIndex: 9999, background: "#03060c" },
|
|
1017
1077
|
}, tab === "map" ? React.createElement(MemoryCanvas, { key: open, rendererRef: renderer, selectionVersion: selectionVersion, onSelect: setSelected })
|
|
1018
1078
|
: tab === "memories" ? React.createElement(MemoryList, { onChanged: function () { setRetry(function (value) { return value + 1; }); } })
|
|
1019
|
-
: React.createElement(MemorySettings, {
|
|
1079
|
+
: React.createElement(MemorySettings, { settings: settings, configBusy: configBusy, onToggle: toggleAutoMemory,
|
|
1080
|
+
onCleared: function () { openMap(); setGuideOpen(true); } }),
|
|
1020
1081
|
tab !== "map" ? React.createElement("strong", { style: { position: "absolute", top: "23px", left: "24px", font: "600 18px system-ui,sans-serif", color: "#b7e9ff" } }, "记忆管理") : null,
|
|
1021
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}}"),
|
|
1022
1083
|
React.createElement("div", {
|
|
@@ -1027,7 +1088,7 @@ function frame(now) {
|
|
|
1027
1088
|
return React.createElement("button", { key: entry[0], type: "button", role: "tab", "aria-selected": tab === entry[0], "data-memory-engine-tab": entry[0],
|
|
1028
1089
|
onClick: function () { setSelected(null); setTab(entry[0]); },
|
|
1029
1090
|
style: Object.assign({}, actionStyle, { background: tab === entry[0] ? "#204762" : "#091422", borderColor: tab === entry[0] ? "#8acded" : "#35536b" }) }, entry[1]);
|
|
1030
|
-
})), React.createElement("button", {
|
|
1091
|
+
})), React.createElement(AutoMemoryToggle, { settings: settings, busy: configBusy, onToggle: toggleAutoMemory, location: "header" }), React.createElement("button", {
|
|
1031
1092
|
ref: help, type: "button", onClick: function () { setGuideOpen(true); },
|
|
1032
1093
|
"aria-label": "查看使用指南", title: "使用指南", style: actionStyle,
|
|
1033
1094
|
}, "?"), React.createElement("button", {
|
|
@@ -1035,7 +1096,11 @@ function frame(now) {
|
|
|
1035
1096
|
onClick: closeMap,
|
|
1036
1097
|
"aria-label": "关闭记忆图谱",
|
|
1037
1098
|
style: actionStyle,
|
|
1038
|
-
}, "关闭 ×")),
|
|
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, {
|
|
1039
1104
|
node: selected, onClose: function () { setSelected(null); },
|
|
1040
1105
|
onHighlight: function () { if (renderer.current) renderer.current.fireNeuron(selected.id); },
|
|
1041
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 = ["知识", "印象", "对话", "事件", "日记"];
|
|
@@ -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
|
@@ -14,7 +14,8 @@ 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
|
-
autoMemory: { enabled: config.autoMemory.enabled, maxPerTurn: config.autoMemory.maxPerTurn },
|
|
17
|
+
autoMemory: { enabled: config.autoMemory.enabled, maxPerTurn: config.autoMemory.maxPerTurn, dailyCap: config.autoMemory.dailyCap },
|
|
18
|
+
showAutoMemoryCostHint: config.showAutoMemoryCostHint !== false,
|
|
18
19
|
autoImport: { enabled: config.autoImport.enabled, sourceCount: config.autoImport.sources.length },
|
|
19
20
|
lastError: readLastError(path.join(home, 'memory-engine', 'auto-log.json'), options.warn) };
|
|
20
21
|
}
|
package/lib/index.js
CHANGED
|
@@ -46,7 +46,7 @@ export function createTools(defineTool, store = new MemoryStore(), { map = gener
|
|
|
46
46
|
}),
|
|
47
47
|
defineTool({
|
|
48
48
|
name: 'memory_recall',
|
|
49
|
-
description: '按话题查跨会话的本地记忆,并联想相关条目。q
|
|
49
|
+
description: '按话题查跨会话的本地记忆,并联想相关条目。q 使用具体关键词,如「手冲」「阅读 书签」。与 QQ 桥接记忆独立。',
|
|
50
50
|
parameters: {
|
|
51
51
|
q: { type: 'string', required: true, description: '要查的话题或关键词' },
|
|
52
52
|
limit: { type: 'number', description: '默认 8,最大 20' },
|
package/lib/onboard.js
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
+
// 引导文案(0.7.0):先说自动记忆,再说手动的路
|
|
2
|
+
// 原则:看到「三条」就知道该干什么,不需要读文档。
|
|
3
|
+
|
|
4
|
+
// 可选的手动提示词:自动记忆关闭时才需要它。
|
|
5
|
+
// 不要再把它说成「必须加进角色卡」——0.7.0 起自动记忆能自己抽。
|
|
1
6
|
export const ONBOARD_PROMPT = [
|
|
7
|
+
'(自动记忆已开启时可以不用这一段)',
|
|
8
|
+
'',
|
|
2
9
|
'聊天时遇到这些,用 memory_save 记下来:',
|
|
3
10
|
' · 对方的称呼、偏好、忌讳、习惯(kind="印象",who 填对方名字)',
|
|
4
11
|
' · 答应过的事、约定、承诺(kind="对话")',
|
|
@@ -11,8 +18,16 @@ export const ONBOARD_PROMPT = [
|
|
|
11
18
|
].join('\n');
|
|
12
19
|
|
|
13
20
|
export const ONBOARD_STEPS = [
|
|
14
|
-
{
|
|
15
|
-
|
|
16
|
-
|
|
21
|
+
{
|
|
22
|
+
title: '先打开自动记忆',
|
|
23
|
+
body: '图谱右上角有个「自动记忆」开关,默认是关的。点一下变成 ● 之后,每轮对话结束会自动抽 0~2 条记下来——这时候你什么都不用做。会调用模型,大约每轮几分钱,随时可以再点掉。',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
title: '想把旧记录也收进来',
|
|
27
|
+
body: '跟 AI 说「把 D:\\bot\\memories.json 导进记忆库」,它会调 memory_import。支持 JSON、Markdown、纯文本和 DSH 会话记录;默认只预览,确认后才写入。',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
title: '回来这里看',
|
|
31
|
+
body: '「记忆」页签能搜索、改错字、删除、导出;「图谱」页签是所有记忆画成的球体,中间那颗金色的是你的人设卡(在 config.json 里配 personaFile)。拖拽旋转,点任意光点看它连着什么。数据存在本机 ~/.dsh/memory-engine/,纯 JSON 明文,不会上传到任何地方。',
|
|
32
|
+
},
|
|
17
33
|
];
|
|
18
|
-
|
package/lib/routes.js
CHANGED
|
@@ -3,6 +3,7 @@ import { computeMapData } from './map.js';
|
|
|
3
3
|
import { renderHtml } from './render/render.mjs';
|
|
4
4
|
import { ONBOARD_PROMPT, ONBOARD_STEPS } from './onboard.js';
|
|
5
5
|
import { readUiConfig } from './config.js';
|
|
6
|
+
import { updateConfig } from './config-write.js';
|
|
6
7
|
import { chars, retention, titleOf } from './decay.js';
|
|
7
8
|
import { scoreMemories } from './render/score.mjs';
|
|
8
9
|
|
|
@@ -74,7 +75,7 @@ function listMemories(store, query) {
|
|
|
74
75
|
items: items.slice(offset, offset + size).map(row => ({ ...row, preview: chars(row.text, 60) + (Array.from(row.text).length > 60 ? '…' : '') })) };
|
|
75
76
|
}
|
|
76
77
|
|
|
77
|
-
export function createMemoryHandler({ store, warn = () => {} }) {
|
|
78
|
+
export function createMemoryHandler({ store, warn = () => {}, beforeConfigUpdate = () => {} }) {
|
|
78
79
|
return async (request, response) => {
|
|
79
80
|
let methods = ['GET'];
|
|
80
81
|
const reply = (status, payload, html = false) => {
|
|
@@ -98,12 +99,16 @@ export function createMemoryHandler({ store, warn = () => {} }) {
|
|
|
98
99
|
catch { return fail(404, '找不到这个记忆接口'); }
|
|
99
100
|
const item = /^\/memory-engine\/api\/item\/([^/%\\]+)$/u.exec(pathname);
|
|
100
101
|
if (pathname.includes('..') || pathname.includes('\\') || (!paths.has(pathname) && !item)) return fail(404, '找不到这个记忆接口');
|
|
101
|
-
methods = item ? ['GET', 'PUT', 'DELETE'] : pathname === `${base}/api/clear` ? ['POST'] : ['GET'];
|
|
102
|
+
methods = item ? ['GET', 'PUT', 'DELETE'] : pathname === `${base}/api/clear` ? ['POST'] : pathname === `${base}/api/config` ? ['GET', 'PUT'] : ['GET'];
|
|
102
103
|
if (!allowedMethods.has(request.method) || !methods.includes(request.method)) return fail(405, `此接口仅支持 ${methods.join('、')}`);
|
|
103
104
|
if (Number(request.headers['content-length']) > 1024 * 1024) return fail(413, '请求体不能超过 1MB');
|
|
104
105
|
if (request.method === 'PUT' || request.method === 'POST') {
|
|
105
106
|
if (!/^application\/json(?:\s*;|$)/i.test(request.headers['content-type'] ?? '')) return fail(400, '请求体必须使用 application/json');
|
|
106
107
|
const body = await readBody(request);
|
|
108
|
+
if (pathname === `${base}/api/config`) {
|
|
109
|
+
beforeConfigUpdate(body);
|
|
110
|
+
return reply(200, { ok: true, data: updateConfig(path.dirname(path.dirname(store.file)), body, { warn }) });
|
|
111
|
+
}
|
|
107
112
|
return reply(200, { ok: true, data: item ? store.updateItem(item[1], body) : store.clear(body) });
|
|
108
113
|
}
|
|
109
114
|
if (request.headers['transfer-encoding'] || Number(request.headers['content-length']) > 0) return fail(400, request.method === 'GET' ? '此只读接口不接受请求体' : '此接口不接受请求体');
|
|
@@ -130,7 +135,8 @@ export function createMemoryHandler({ store, warn = () => {} }) {
|
|
|
130
135
|
} catch (error) {
|
|
131
136
|
if ([400, 404, 413].includes(error.status)) return fail(error.status, error.message);
|
|
132
137
|
warn(`图谱请求失败:${error.message}`);
|
|
133
|
-
return fail(500, '
|
|
138
|
+
return fail(500, request.method === 'PUT' && (request.url ?? '').split('?')[0] === `${base}/api/config`
|
|
139
|
+
? '配置保存失败,请检查数据目录权限或本地插件日志' : '读取记忆失败,请检查本地插件日志');
|
|
134
140
|
}
|
|
135
141
|
};
|
|
136
142
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-memory-engine",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Local DSH memory engine with neuron network visualization and private file import",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"visualization"
|
|
15
15
|
],
|
|
16
16
|
"license": "MIT",
|
|
17
|
-
"author": "
|
|
17
|
+
"author": "dsh-memory-engine contributors",
|
|
18
18
|
"type": "module",
|
|
19
19
|
"main": "./lib/index.js",
|
|
20
20
|
"exports": {
|
|
@@ -27,7 +27,10 @@
|
|
|
27
27
|
},
|
|
28
28
|
"files": [
|
|
29
29
|
"lib/",
|
|
30
|
-
"scripts/",
|
|
30
|
+
"scripts/render-map.mjs",
|
|
31
|
+
"scripts/sync-qq.mjs",
|
|
32
|
+
"scripts/build-client.mjs",
|
|
33
|
+
"scripts/lib/",
|
|
31
34
|
"cordis.patch.yml",
|
|
32
35
|
"README.md",
|
|
33
36
|
"LICENSE"
|