dsh-memory-engine 0.6.1 → 0.7.0-beta.2b
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 +73 -14
- package/lib/client.js +583 -26
- package/lib/client.src.js +397 -0
- package/lib/config.js +5 -0
- package/lib/index.js +4 -5
- package/lib/map.js +15 -10
- package/lib/onboard.js +18 -0
- package/lib/render/canvas.js +259 -0
- package/lib/render/render.mjs +15 -175
- package/lib/routes.js +56 -0
- package/package.json +2 -1
- package/scripts/build-client.mjs +22 -0
- package/scripts/lib/strip-export.mjs +7 -0
- package/lib/serve.js +0 -150
package/lib/serve.js
DELETED
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
import { createServer } from 'node:http';
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import { generateMap } from './map.js';
|
|
5
|
-
import { readConfig } from './config.js';
|
|
6
|
-
|
|
7
|
-
export const DEFAULT_UI_PORT = 18886;
|
|
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
|
-
const escapeBridge = '<script>window.addEventListener("keydown",function(e){if(e.key==="Escape")parent.postMessage({type:"dsh-memory-engine-close"},"*")});</script>';
|
|
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
|
-
|
|
91
|
-
export async function startMapServer({ store, port, warn = () => {} }) {
|
|
92
|
-
const directory = path.dirname(store.file);
|
|
93
|
-
const config = readConfig(path.dirname(directory));
|
|
94
|
-
port ??= config.uiPort ?? DEFAULT_UI_PORT;
|
|
95
|
-
if (!Number.isInteger(port) || port < 0 || port > 65535) throw new Error('uiPort 必须是有效端口号');
|
|
96
|
-
let state;
|
|
97
|
-
const server = createServer((request, response) => {
|
|
98
|
-
const reply = (status, body = '') => {
|
|
99
|
-
response.writeHead(status, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store',
|
|
100
|
-
'X-Content-Type-Options': 'nosniff', 'Referrer-Policy': 'no-referrer' });
|
|
101
|
-
response.end(request.method === 'HEAD' ? '' : body);
|
|
102
|
-
};
|
|
103
|
-
try {
|
|
104
|
-
if (request.headers.host !== `127.0.0.1:${server.address()?.port}`) return reply(403);
|
|
105
|
-
if (!['GET', 'HEAD'].includes(request.method)) return reply(405);
|
|
106
|
-
const url = new URL(request.url, 'http://127.0.0.1');
|
|
107
|
-
const rootPage = ['/', '/index.html', '/starmap.html'].includes(url.pathname);
|
|
108
|
-
if (!rootPage && !/^\/pages\/[1-9]\d*\.html$/u.test(url.pathname)) return reply(404);
|
|
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);
|
|
114
|
-
let signature = 'empty';
|
|
115
|
-
try { const info = fs.statSync(store.file); signature = `${info.mtimeMs}:${info.size}`; }
|
|
116
|
-
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
117
|
-
// A new opening gets a new v token. Polling its loading page reuses
|
|
118
|
-
// that generation instead of continually starting another render.
|
|
119
|
-
const key = `${url.searchParams.get('v') ?? ''}:${signature}`;
|
|
120
|
-
if (!state || state.key !== key) {
|
|
121
|
-
const generation = { key, status: 'pending' };
|
|
122
|
-
state = generation;
|
|
123
|
-
setImmediate(() => {
|
|
124
|
-
if (!server.listening) return;
|
|
125
|
-
try { generateMap(store); generation.status = 'ready'; }
|
|
126
|
-
catch (error) { generation.status = 'error'; warn(`图谱生成失败:${error.message}`); }
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
if (state.status === 'pending') return reply(200, pending + escapeBridge);
|
|
130
|
-
if (state.status === 'error') return reply(500, '<meta charset="utf-8">图谱生成失败,请检查本地插件日志。' + escapeBridge);
|
|
131
|
-
}
|
|
132
|
-
const file = rootPage ? path.join(directory, 'starmap.html') : path.join(directory, url.pathname.slice(1));
|
|
133
|
-
let html;
|
|
134
|
-
try { html = fs.readFileSync(file, 'utf8'); }
|
|
135
|
-
catch (error) { if (error.code === 'ENOENT') return reply(404); throw error; }
|
|
136
|
-
// This transport-only key bridge leaves the copied renderer untouched.
|
|
137
|
-
reply(200, html.replace('</body>', escapeBridge + '</body>'));
|
|
138
|
-
} catch (error) {
|
|
139
|
-
warn(`图谱页面读取失败:${error.message}`);
|
|
140
|
-
reply(500, '<meta charset="utf-8">图谱页面暂不可用,请检查本地插件日志。');
|
|
141
|
-
}
|
|
142
|
-
});
|
|
143
|
-
await new Promise((resolve, reject) => {
|
|
144
|
-
server.once('error', reject);
|
|
145
|
-
server.listen(port, '127.0.0.1', () => { server.removeListener('error', reject); resolve(); });
|
|
146
|
-
});
|
|
147
|
-
server.on('error', error => warn(`图谱服务异常:${error.message}`));
|
|
148
|
-
server.unref();
|
|
149
|
-
return server;
|
|
150
|
-
}
|