dsh-memory-engine 0.5.4 → 0.5.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/lib/index.js +12 -2
- package/lib/serve.js +84 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,6 +10,8 @@ DSH 插件的本地记忆引擎,带神经网络可视化。它让 agent 保存
|
|
|
10
10
|
|
|
11
11
|
点击后全屏显示图谱,右上角 **关闭 ×** 或 **Escape** 退出。每次打开都会读取当前记忆库重新生成;大库会先显示“正在生成”。
|
|
12
12
|
|
|
13
|
+
**记忆库为空时,这个按钮打开的不是星图,而是一张上手引导页**:说明为什么是空的、给出可一键复制的提示词、列出导入命令。第一次装完点开不会看到一片黑。
|
|
14
|
+
|
|
13
15
|
拖拽旋转、滚轮缩放、点击神经元看原文。服务默认监听 `127.0.0.1:18886`;端口被占用时记忆工具仍可用,可以运行 `node <插件目录>/scripts/render-map.mjs --open`,或直接打开 `~/.dsh/memory-engine/starmap.html`。修改端口需要同时修改 config.json 的 uiPort 和 client.js 的 VIEW_URL,见后文。
|
|
14
16
|
|
|
15
17
|
也可以让 agent 调用 `memory_map()`,生成可双击、离线打开的单文件 HTML:
|
|
@@ -86,6 +88,10 @@ memory_recall(q="朋友", limit=5)
|
|
|
86
88
|
|
|
87
89
|
召回结果区分直接命中和联想到的记忆。`memory_stats()` 查看总数和分类;`memory_dedupe()` 默认仅预览重复组。`memory_forget` 支持按 id 精确删除;关键词批量删除必须先预览,并经用户确认后传 `confirm=true`。
|
|
88
90
|
|
|
91
|
+
**检索是按字面词匹配,不做语义理解。** 库里写的是「古茗」,`memory_recall(q="奶茶")` 就查不到——必须先有「奶茶」这两个字。想覆盖这种情况,存记忆时把常见说法一起写进去(例如「阿木喝奶茶只点古茗的超A芝士葡萄」)。
|
|
92
|
+
|
|
93
|
+
**联想到的记忆**来自关键词共享:两条记忆提到同一个词,就会被连起来,查其中一条时另一条一起返回。它不是推理,是顺着关联往外摸。扩散默认一跳(`hops=1`),邻居要留存 > 0.4 才会被带出来。
|
|
94
|
+
|
|
89
95
|
## 导入自己机器人的旧记忆
|
|
90
96
|
|
|
91
97
|
三种格式都是你明确指定的本地文件或目录。如果旧格式均不匹配,可让有文件读取能力的 agent 理解文件,再通过 `memory_save` 逐条写入。导入先预览,用户确认后再传 `dry_run=false`。
|
package/lib/index.js
CHANGED
|
@@ -26,7 +26,9 @@ export function createTools(defineTool, store = new MemoryStore(), { map = gener
|
|
|
26
26
|
return [
|
|
27
27
|
defineTool({
|
|
28
28
|
name: 'memory_save',
|
|
29
|
-
description: '把值得记住的事保存到跨会话的本地记忆。与 QQ 桥接记忆独立。重复正文自动合并。'
|
|
29
|
+
description: '把值得记住的事保存到跨会话的本地记忆。与 QQ 桥接记忆独立。重复正文自动合并。'
|
|
30
|
+
+ '该存:对方的称呼/偏好/忌讳/习惯、答应过的事、情绪特别重的瞬间、新学到的知识、当天小结。'
|
|
31
|
+
+ '不该存:寒暄、重复内容、纯事务性对话。一天三五条足够,存前可先用 memory_recall 查重。',
|
|
30
32
|
parameters: {
|
|
31
33
|
text: { type: 'string', required: true, description: '记忆正文,最多 2000 字' },
|
|
32
34
|
kind: { type: 'string', description: '分类,默认对话;可用知识/印象/日记/对话/事件或自定义' },
|
|
@@ -135,10 +137,18 @@ export function createTools(defineTool, store = new MemoryStore(), { map = gener
|
|
|
135
137
|
async execute() {
|
|
136
138
|
const stats = store.stats();
|
|
137
139
|
const kinds = Object.entries(stats.kinds).map(([kind, count]) => `${kind} ${count}`).join(' / ');
|
|
138
|
-
|
|
140
|
+
const body = `记忆引擎状态\n- 总条数:${stats.total}${kinds ? `(${kinds})` : ''}\n`
|
|
139
141
|
+ `- 快忘了的(留存<0.3):${stats.fading} 条\n`
|
|
140
142
|
+ `- 最老:${stats.oldest === null ? '暂无' : `${new Date(stats.oldest).toISOString().slice(0, 10)}(${stats.ageDays} 天前)`}\n`
|
|
141
143
|
+ `- 存储:${stats.file}(${(stats.bytes / 1024).toFixed(1)} KB)`;
|
|
144
|
+
// 库是空的时,替 agent 把「接下来该做什么」说出来(见 README「装完是空库」)。
|
|
145
|
+
if (stats.total === 0) {
|
|
146
|
+
return body + '\n\n记忆库还是空的。插件不会自己读聊天记录,记忆要靠两件事之一进来:\n'
|
|
147
|
+
+ '① 在提示词里告诉 agent 该记什么,聊天时就调 memory_save(推荐)\n'
|
|
148
|
+
+ '② 用 memory_import 把旧记忆导进来(支持 json / markdown / text)\n'
|
|
149
|
+
+ '建议主动把这两条简短告诉主人,并提醒他点开图谱可以看到上手引导页。';
|
|
150
|
+
}
|
|
151
|
+
return body;
|
|
142
152
|
},
|
|
143
153
|
}),
|
|
144
154
|
];
|
package/lib/serve.js
CHANGED
|
@@ -8,6 +8,86 @@ export const DEFAULT_UI_PORT = 18886;
|
|
|
8
8
|
const pending = '<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta http-equiv="refresh" content="1"><title>记忆图谱生成中</title><body style="background:#03060c;color:#d8e9f8;font:16px system-ui;padding:40px">正在生成记忆图谱,请稍候……</body></html>';
|
|
9
9
|
const escapeBridge = '<script>window.addEventListener("keydown",function(e){if(e.key==="Escape")parent.postMessage({type:"dsh-memory-engine-close"},"*")});</script>';
|
|
10
10
|
|
|
11
|
+
// 空库时的上手引导页。
|
|
12
|
+
// 为什么要有这一页:新装用户点开图谱看到的是一片黑,只会以为插件坏了。
|
|
13
|
+
// 这个页面在他最想用的那一刻告诉他「为什么是空的、两件事怎么做」。
|
|
14
|
+
const ONBOARD_PROMPT = [
|
|
15
|
+
'聊天时遇到这些,用 memory_save 记下来:',
|
|
16
|
+
' · 对方的称呼、偏好、忌讳、习惯(kind="印象",who 填对方名字)',
|
|
17
|
+
' · 答应过的事、约定、承诺(kind="对话")',
|
|
18
|
+
' · 情绪特别重的瞬间(kind="事件",heat=1)',
|
|
19
|
+
' · 新学到的知识、名词、梗的解释(kind="知识")',
|
|
20
|
+
' · 每天结束时,用一两句写当天小结(kind="日记")',
|
|
21
|
+
'',
|
|
22
|
+
'不用每条都记。一天三五条就够,宁可少存也别灌水。',
|
|
23
|
+
'存之前先 memory_recall 查一下,避免重复。',
|
|
24
|
+
].join('\n');
|
|
25
|
+
|
|
26
|
+
const onboard = `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8">
|
|
27
|
+
<title>还没有记忆</title>
|
|
28
|
+
<style>
|
|
29
|
+
:root{color-scheme:dark}
|
|
30
|
+
*{box-sizing:border-box}
|
|
31
|
+
body{margin:0;min-height:100vh;background:radial-gradient(ellipse at 50% 0%,#0b1a2e 0%,#03060c 60%);
|
|
32
|
+
color:#dbeaf8;font:15px/1.75 "Microsoft YaHei",system-ui,sans-serif;display:flex;align-items:center;justify-content:center;padding:32px}
|
|
33
|
+
.card{width:min(760px,100%)}
|
|
34
|
+
h1{font-size:26px;margin:0 0 6px;font-weight:600;letter-spacing:.5px}
|
|
35
|
+
.sub{color:#7f9dbb;margin:0 0 26px;font-size:14px}
|
|
36
|
+
.step{border-left:3px solid #1f4d73;padding:2px 0 2px 16px;margin:0 0 24px}
|
|
37
|
+
.step h2{font-size:16px;margin:0 0 8px;color:#9fd0f5;font-weight:600}
|
|
38
|
+
.step p{margin:0 0 10px;color:#b6cde2;font-size:14px}
|
|
39
|
+
pre{background:#07121f;border:1px solid #17324b;border-radius:8px;padding:14px;overflow:auto;
|
|
40
|
+
font:13px/1.7 ui-monospace,Consolas,monospace;color:#b9d8f2;margin:0 0 10px;white-space:pre-wrap}
|
|
41
|
+
button{background:#123a5c;color:#cfe9ff;border:1px solid #2a6698;border-radius:7px;
|
|
42
|
+
padding:8px 16px;font:14px "Microsoft YaHei",system-ui;cursor:pointer}
|
|
43
|
+
button:hover{background:#17496f}
|
|
44
|
+
button:active{transform:translateY(1px)}
|
|
45
|
+
code{background:#0d1e30;border-radius:4px;padding:1px 5px;color:#8fd0ff;font-size:13px}
|
|
46
|
+
.foot{color:#5f7a93;font-size:13px;margin-top:8px}
|
|
47
|
+
</style></head><body><div class="card">
|
|
48
|
+
<h1>记忆库还是空的</h1>
|
|
49
|
+
<p class="sub">插件不会自己读你的聊天记录。记忆要靠下面两件事之一进来。</p>
|
|
50
|
+
|
|
51
|
+
<div class="step">
|
|
52
|
+
<h2>① 让它边聊边记(推荐)</h2>
|
|
53
|
+
<p>把这段加进 agent 的提示词或角色卡里:</p>
|
|
54
|
+
<pre id="prompt">${ONBOARD_PROMPT.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')}</pre>
|
|
55
|
+
<button id="copy">复制这段提示词</button>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<div class="step">
|
|
59
|
+
<h2>② 或者把旧记忆导进来</h2>
|
|
60
|
+
<p>让 agent 调用 <code>memory_import</code>,支持三种格式:</p>
|
|
61
|
+
<pre>memory_import(source="json", path="D:\\\\bot\\\\memories.json")
|
|
62
|
+
memory_import(source="markdown", path="D:\\\\bot\\\\notes")
|
|
63
|
+
memory_import(source="text", path="D:\\\\bot\\\\memories.txt")</pre>
|
|
64
|
+
<p class="foot">会先预览「能新增多少条、多少条重复被跳过」,确认后再写。</p>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<div class="step">
|
|
68
|
+
<h2>③ 然后就有图了</h2>
|
|
69
|
+
<p>有了几条记忆之后,回到这个按钮,就能看到神经网络。</p>
|
|
70
|
+
<p class="foot">记忆只存在本机 <code>~/.dsh/memory-engine/</code>,不上传、不联网。</p>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
<script>
|
|
74
|
+
var btn = document.getElementById('copy');
|
|
75
|
+
btn.addEventListener('click', function () {
|
|
76
|
+
var text = document.getElementById('prompt').textContent;
|
|
77
|
+
var done = function () { btn.textContent = '已复制 ✓'; setTimeout(function () { btn.textContent = '复制这段提示词'; }, 1800); };
|
|
78
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
79
|
+
navigator.clipboard.writeText(text).then(done, fallback);
|
|
80
|
+
} else { fallback(); }
|
|
81
|
+
function fallback() {
|
|
82
|
+
var area = document.createElement('textarea');
|
|
83
|
+
area.value = text; document.body.appendChild(area); area.select();
|
|
84
|
+
try { document.execCommand('copy'); done(); } catch (e) { btn.textContent = '请手动选中复制'; }
|
|
85
|
+
document.body.removeChild(area);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
</script>
|
|
89
|
+
</body></html>`;
|
|
90
|
+
|
|
11
91
|
export async function startMapServer({ store, port, warn = () => {} }) {
|
|
12
92
|
const directory = path.dirname(store.file);
|
|
13
93
|
const config = readConfig(path.dirname(directory));
|
|
@@ -27,6 +107,10 @@ export async function startMapServer({ store, port, warn = () => {} }) {
|
|
|
27
107
|
const rootPage = ['/', '/index.html', '/starmap.html'].includes(url.pathname);
|
|
28
108
|
if (!rootPage && !/^\/pages\/[1-9]\d*\.html$/u.test(url.pathname)) return reply(404);
|
|
29
109
|
if (rootPage) {
|
|
110
|
+
// 空库直接返回上手引导页:不要让人对着黑屏猜「是不是坏了」
|
|
111
|
+
let rows = [];
|
|
112
|
+
try { rows = store.read(); } catch (error) { warn(`记忆库读取失败:${error.message}`); }
|
|
113
|
+
if (!Array.isArray(rows) || rows.length === 0) return reply(200, onboard + escapeBridge);
|
|
30
114
|
let signature = 'empty';
|
|
31
115
|
try { const info = fs.statSync(store.file); signature = `${info.mtimeMs}:${info.size}`; }
|
|
32
116
|
catch (error) { if (error.code !== 'ENOENT') throw error; }
|