@modusensus/dsh-mneme 0.1.3 → 0.1.5
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 +13 -3
- package/lib/api.js +196 -10
- package/lib/client.js +311 -37
- package/lib/commands.js +64 -0
- package/lib/embedding.js +97 -0
- package/lib/index.js +28 -3
- package/lib/inject.js +33 -8
- package/lib/service.js +17 -0
- package/lib/settings.js +142 -0
- package/lib/store.js +84 -5
- package/package.json +58 -58
- package/src/api.js +196 -10
- package/src/commands.js +64 -0
- package/src/embedding.js +97 -0
- package/src/index.js +28 -3
- package/src/inject.js +33 -8
- package/src/service.js +17 -0
- package/src/settings.js +142 -0
- package/src/store.js +84 -5
package/lib/client.js
CHANGED
|
@@ -23,7 +23,37 @@ window.__ModuleLoader__.load({
|
|
|
23
23
|
"memory.tab.preference": "偏好",
|
|
24
24
|
"memory.tab.project": "项目",
|
|
25
25
|
"memory.tab.decision": "决策",
|
|
26
|
-
"memory.tab.history": "历史"
|
|
26
|
+
"memory.tab.history": "历史",
|
|
27
|
+
"memory.settings.open": "设置",
|
|
28
|
+
"memory.settings.title": "记忆库设置",
|
|
29
|
+
"memory.settings.profile": "用户画像",
|
|
30
|
+
"memory.settings.profileHint": "描述你自己(角色、背景、偏好),Agent 会在每轮遵循",
|
|
31
|
+
"memory.settings.profileSave": "保存画像",
|
|
32
|
+
"memory.settings.profileSaved": "画像已保存",
|
|
33
|
+
"memory.settings.rules": "规则",
|
|
34
|
+
"memory.settings.rulesHint": "Agent 必须遵守的行为规则,每轮注入",
|
|
35
|
+
"memory.settings.ruleAdd": "添加规则",
|
|
36
|
+
"memory.settings.rulePlaceholder": "例如:回答时总是先给结论",
|
|
37
|
+
"memory.settings.commands": "自定义指令",
|
|
38
|
+
"memory.settings.commandsHint": "注册斜杠命令(/名称),触发时把指令内容交给 Agent",
|
|
39
|
+
"memory.settings.cmdName": "命令名",
|
|
40
|
+
"memory.settings.cmdDesc": "描述",
|
|
41
|
+
"memory.settings.cmdInstruction": "指令内容",
|
|
42
|
+
"memory.settings.cmdAdd": "添加命令",
|
|
43
|
+
"memory.settings.cmdDelete": "删除",
|
|
44
|
+
"memory.settings.empty": "暂无内容",
|
|
45
|
+
"memory.panel.semantic": "语义",
|
|
46
|
+
"memory.settings.vectorTitle": "向量搜索",
|
|
47
|
+
"memory.settings.vectorHint": "接入 OpenAI 兼容的 embeddings API 做语义搜索,可匹配字面不同但语义相近的记忆",
|
|
48
|
+
"memory.settings.vectorEnabled": "启用向量搜索",
|
|
49
|
+
"memory.settings.vectorBaseUrl": "API 地址 (Base URL)",
|
|
50
|
+
"memory.settings.vectorApiKey": "API Key",
|
|
51
|
+
"memory.settings.vectorModel": "模型名",
|
|
52
|
+
"memory.settings.vectorSave": "保存配置",
|
|
53
|
+
"memory.settings.vectorSaved": "配置已保存",
|
|
54
|
+
"memory.settings.vectorReindex": "重建索引",
|
|
55
|
+
"memory.settings.vectorReindexing": "索引中…",
|
|
56
|
+
"memory.settings.vectorReindexDone": "已索引 {n} 条"
|
|
27
57
|
},
|
|
28
58
|
en: {
|
|
29
59
|
"memory.panel.title": "Memory",
|
|
@@ -34,7 +64,37 @@ window.__ModuleLoader__.load({
|
|
|
34
64
|
"memory.tab.preference": "Preferences",
|
|
35
65
|
"memory.tab.project": "Projects",
|
|
36
66
|
"memory.tab.decision": "Decisions",
|
|
37
|
-
"memory.tab.history": "History"
|
|
67
|
+
"memory.tab.history": "History",
|
|
68
|
+
"memory.settings.open": "Settings",
|
|
69
|
+
"memory.settings.title": "Memory Settings",
|
|
70
|
+
"memory.settings.profile": "User Profile",
|
|
71
|
+
"memory.settings.profileHint": "Describe yourself — the agent follows this every turn",
|
|
72
|
+
"memory.settings.profileSave": "Save Profile",
|
|
73
|
+
"memory.settings.profileSaved": "Profile saved",
|
|
74
|
+
"memory.settings.rules": "Rules",
|
|
75
|
+
"memory.settings.rulesHint": "Behavior rules the agent must follow every turn",
|
|
76
|
+
"memory.settings.ruleAdd": "Add Rule",
|
|
77
|
+
"memory.settings.rulePlaceholder": "e.g. Always lead with a conclusion",
|
|
78
|
+
"memory.settings.commands": "Custom Commands",
|
|
79
|
+
"memory.settings.commandsHint": "Register slash commands (/name) whose instruction is handed to the agent",
|
|
80
|
+
"memory.settings.cmdName": "Name",
|
|
81
|
+
"memory.settings.cmdDesc": "Description",
|
|
82
|
+
"memory.settings.cmdInstruction": "Instruction",
|
|
83
|
+
"memory.settings.cmdAdd": "Add Command",
|
|
84
|
+
"memory.settings.cmdDelete": "Delete",
|
|
85
|
+
"memory.settings.empty": "Nothing yet",
|
|
86
|
+
"memory.panel.semantic": "Semantic",
|
|
87
|
+
"memory.settings.vectorTitle": "Vector Search",
|
|
88
|
+
"memory.settings.vectorHint": "Connect an OpenAI-compatible embeddings API for semantic search by meaning, not just keywords",
|
|
89
|
+
"memory.settings.vectorEnabled": "Enable vector search",
|
|
90
|
+
"memory.settings.vectorBaseUrl": "Base URL",
|
|
91
|
+
"memory.settings.vectorApiKey": "API Key",
|
|
92
|
+
"memory.settings.vectorModel": "Model",
|
|
93
|
+
"memory.settings.vectorSave": "Save Config",
|
|
94
|
+
"memory.settings.vectorSaved": "Config saved",
|
|
95
|
+
"memory.settings.vectorReindex": "Reindex",
|
|
96
|
+
"memory.settings.vectorReindexing": "Indexing…",
|
|
97
|
+
"memory.settings.vectorReindexDone": "Indexed {n} items"
|
|
38
98
|
}
|
|
39
99
|
};
|
|
40
100
|
|
|
@@ -50,13 +110,22 @@ window.__ModuleLoader__.load({
|
|
|
50
110
|
return Number.isNaN(date.getTime()) ? "—" : date.toLocaleString();
|
|
51
111
|
}
|
|
52
112
|
|
|
53
|
-
function MemoryPanel({ t, onClose }) {
|
|
113
|
+
function MemoryPanel({ t, onClose, embedded }) {
|
|
54
114
|
const [tab, setTab] = useState("all");
|
|
55
115
|
const [query, setQuery] = useState("");
|
|
116
|
+
const [semantic, setSemantic] = useState(false);
|
|
117
|
+
const [vecEnabled, setVecEnabled] = useState(false);
|
|
56
118
|
const [items, setItems] = useState([]);
|
|
57
119
|
const [loading, setLoading] = useState(false);
|
|
58
120
|
const abortRef = useRef(null);
|
|
59
121
|
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
fetch("/api/dsh-mneme/vector-config")
|
|
124
|
+
.then((res) => res.json())
|
|
125
|
+
.then((d) => setVecEnabled(!!d.config?.enabled))
|
|
126
|
+
.catch(() => {});
|
|
127
|
+
}, []);
|
|
128
|
+
|
|
60
129
|
const load = useCallback(async () => {
|
|
61
130
|
abortRef.current?.abort();
|
|
62
131
|
const controller = new AbortController();
|
|
@@ -66,7 +135,7 @@ window.__ModuleLoader__.load({
|
|
|
66
135
|
const params = new URLSearchParams();
|
|
67
136
|
if (tab !== "all") params.set("type", tab);
|
|
68
137
|
const url = query.trim()
|
|
69
|
-
? `/api/dsh-mneme/search?q=${encodeURIComponent(query.trim())}`
|
|
138
|
+
? `/api/dsh-mneme/search?q=${encodeURIComponent(query.trim())}&mode=${semantic ? "vector" : "auto"}`
|
|
70
139
|
: `/api/dsh-mneme/list?${params.toString()}`;
|
|
71
140
|
const res = await fetch(url, { signal: controller.signal });
|
|
72
141
|
const data = await res.json();
|
|
@@ -77,7 +146,7 @@ window.__ModuleLoader__.load({
|
|
|
77
146
|
} finally {
|
|
78
147
|
setLoading(false);
|
|
79
148
|
}
|
|
80
|
-
}, [tab, query]);
|
|
149
|
+
}, [tab, query, semantic]);
|
|
81
150
|
|
|
82
151
|
useEffect(() => {
|
|
83
152
|
load();
|
|
@@ -86,19 +155,24 @@ window.__ModuleLoader__.load({
|
|
|
86
155
|
|
|
87
156
|
const tabs = ["all", "preference", "project", "decision", "history"];
|
|
88
157
|
|
|
89
|
-
|
|
90
|
-
react.createElement("div", { style: styles.
|
|
91
|
-
react.createElement("
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
158
|
+
const body = react.createElement("div", { style: embedded ? { ...styles.panel, width: "100%", maxWidth: "100%", maxHeight: "none" } : styles.panel },
|
|
159
|
+
!embedded && react.createElement("div", { style: styles.header },
|
|
160
|
+
react.createElement("span", { style: styles.title }, t("memory.panel.title")),
|
|
161
|
+
react.createElement("button", { style: styles.close, onClick: onClose }, "×")
|
|
162
|
+
),
|
|
163
|
+
react.createElement("div", { style: { display: "flex", gap: 6, alignItems: "center", marginBottom: 12 } },
|
|
164
|
+
react.createElement("input", {
|
|
165
|
+
style: { ...styles.search, flex: 1, marginBottom: 0 },
|
|
166
|
+
placeholder: t("memory.panel.search"),
|
|
167
|
+
value: query,
|
|
168
|
+
onChange: (e) => setQuery(e.target.value)
|
|
169
|
+
}),
|
|
170
|
+
vecEnabled && react.createElement("button", {
|
|
171
|
+
style: { ...styles.tab, ...(semantic ? styles.tabActive : {}) },
|
|
172
|
+
title: t("memory.settings.vectorTitle"),
|
|
173
|
+
onClick: () => setSemantic(!semantic)
|
|
174
|
+
}, t("memory.panel.semantic"))
|
|
95
175
|
),
|
|
96
|
-
react.createElement("input", {
|
|
97
|
-
style: styles.search,
|
|
98
|
-
placeholder: t("memory.panel.search"),
|
|
99
|
-
value: query,
|
|
100
|
-
onChange: (e) => setQuery(e.target.value)
|
|
101
|
-
}),
|
|
102
176
|
react.createElement("div", { style: styles.tabs },
|
|
103
177
|
tabs.map((key) =>
|
|
104
178
|
react.createElement("button", {
|
|
@@ -128,10 +202,218 @@ window.__ModuleLoader__.load({
|
|
|
128
202
|
)
|
|
129
203
|
)
|
|
130
204
|
)
|
|
131
|
-
|
|
205
|
+
);
|
|
206
|
+
return embedded
|
|
207
|
+
? body
|
|
208
|
+
: createPortal(react.createElement("div", { style: styles.overlay }, body), document.body);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const h = react.createElement;
|
|
212
|
+
|
|
213
|
+
// --- Settings panel: user profile, rules, custom commands ---
|
|
214
|
+
function SettingsPanel({ t, onClose, embedded }) {
|
|
215
|
+
const [sectionTab, setSectionTab] = react.useState("settings");
|
|
216
|
+
const [profile, setProfile] = react.useState("");
|
|
217
|
+
const [rules, setRules] = react.useState([]);
|
|
218
|
+
const [commands, setCommands] = react.useState([]);
|
|
219
|
+
const [newRule, setNewRule] = react.useState("");
|
|
220
|
+
const [newCmd, setNewCmd] = react.useState({ name: "", description: "", instruction: "" });
|
|
221
|
+
const [saved, setSaved] = react.useState(false);
|
|
222
|
+
const [cmdError, setCmdError] = react.useState("");
|
|
223
|
+
const [vector, setVector] = react.useState({ enabled: false, baseUrl: "", apiKey: "", model: "" });
|
|
224
|
+
const [vectorSaved, setVectorSaved] = react.useState(false);
|
|
225
|
+
const [reindexing, setReindexing] = react.useState(false);
|
|
226
|
+
const [reindexMsg, setReindexMsg] = react.useState("");
|
|
227
|
+
|
|
228
|
+
const load = react.useCallback(async () => {
|
|
229
|
+
try {
|
|
230
|
+
const [p, r, c, v] = await Promise.all([
|
|
231
|
+
fetch("/api/dsh-mneme/profile").then((res) => res.json()),
|
|
232
|
+
fetch("/api/dsh-mneme/rules").then((res) => res.json()),
|
|
233
|
+
fetch("/api/dsh-mneme/commands").then((res) => res.json()),
|
|
234
|
+
fetch("/api/dsh-mneme/vector-config").then((res) => res.json())
|
|
235
|
+
]);
|
|
236
|
+
setProfile(p.profile || "");
|
|
237
|
+
setRules(Array.isArray(r.rules) ? r.rules : []);
|
|
238
|
+
setCommands(Array.isArray(c.commands) ? c.commands : []);
|
|
239
|
+
setVector(v.config || { enabled: false, baseUrl: "", apiKey: "", model: "" });
|
|
240
|
+
} catch { /* ignore */ }
|
|
241
|
+
}, []);
|
|
242
|
+
|
|
243
|
+
react.useEffect(() => { load(); }, [load]);
|
|
244
|
+
|
|
245
|
+
async function saveProfile() {
|
|
246
|
+
try {
|
|
247
|
+
await fetch("/api/dsh-mneme/profile", {
|
|
248
|
+
method: "PUT",
|
|
249
|
+
headers: { "Content-Type": "application/json" },
|
|
250
|
+
body: JSON.stringify({ profile })
|
|
251
|
+
});
|
|
252
|
+
setSaved(true);
|
|
253
|
+
setTimeout(() => setSaved(false), 1500);
|
|
254
|
+
} catch { /* ignore */ }
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function putRules(next) {
|
|
258
|
+
await fetch("/api/dsh-mneme/rules", {
|
|
259
|
+
method: "PUT",
|
|
260
|
+
headers: { "Content-Type": "application/json" },
|
|
261
|
+
body: JSON.stringify({ rules: next })
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async function addRule() {
|
|
266
|
+
const text = newRule.trim();
|
|
267
|
+
if (!text) return;
|
|
268
|
+
const next = [...rules, text];
|
|
269
|
+
await putRules(next);
|
|
270
|
+
setRules(next);
|
|
271
|
+
setNewRule("");
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function removeRule(index) {
|
|
275
|
+
const next = rules.filter((_, i) => i !== index);
|
|
276
|
+
await putRules(next);
|
|
277
|
+
setRules(next);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function addCommand() {
|
|
281
|
+
const name = newCmd.name.trim();
|
|
282
|
+
const instruction = newCmd.instruction.trim();
|
|
283
|
+
if (!name || !instruction) return;
|
|
284
|
+
try {
|
|
285
|
+
const res = await fetch("/api/dsh-mneme/commands", {
|
|
286
|
+
method: "POST",
|
|
287
|
+
headers: { "Content-Type": "application/json" },
|
|
288
|
+
body: JSON.stringify({ name, description: newCmd.description, instruction })
|
|
289
|
+
});
|
|
290
|
+
const data = await res.json();
|
|
291
|
+
if (!res.ok) { setCmdError(data.error || "failed"); return; }
|
|
292
|
+
setCmdError("");
|
|
293
|
+
setCommands([...commands, data.command]);
|
|
294
|
+
setNewCmd({ name: "", description: "", instruction: "" });
|
|
295
|
+
} catch { setCmdError("failed"); }
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async function removeCommand(id) {
|
|
299
|
+
await fetch(`/api/dsh-mneme/commands?id=${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
300
|
+
setCommands(commands.filter((c) => c.id !== id));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function saveVector() {
|
|
304
|
+
try {
|
|
305
|
+
await fetch("/api/dsh-mneme/vector-config", {
|
|
306
|
+
method: "PUT",
|
|
307
|
+
headers: { "Content-Type": "application/json" },
|
|
308
|
+
body: JSON.stringify(vector)
|
|
309
|
+
});
|
|
310
|
+
setVectorSaved(true);
|
|
311
|
+
setTimeout(() => setVectorSaved(false), 1500);
|
|
312
|
+
} catch { /* ignore */ }
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async function reindex() {
|
|
316
|
+
setReindexing(true);
|
|
317
|
+
setReindexMsg("");
|
|
318
|
+
try {
|
|
319
|
+
const res = await fetch("/api/dsh-mneme/vector-reindex");
|
|
320
|
+
const data = await res.json();
|
|
321
|
+
const n = data.indexed ?? 0;
|
|
322
|
+
setReindexMsg(t("memory.settings.vectorReindexDone").replace("{n}", String(n)));
|
|
323
|
+
} catch { setReindexMsg(""); }
|
|
324
|
+
setReindexing(false);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const inputStyle = { ...styles.search, marginBottom: 8 };
|
|
328
|
+
const labelStyle = { fontSize: 12, fontWeight: 600, margin: "12px 0 4px" };
|
|
329
|
+
const hintStyle = { fontSize: 11, color: "var(--dsw-alias-label-tertiary, #999)", marginBottom: 8 };
|
|
330
|
+
const rowStyle = { display: "flex", justifyContent: "space-between", alignItems: "center", gap: 8, padding: "6px 0", borderBottom: "1px solid var(--dsw-alias-border-l1, #eee)" };
|
|
331
|
+
|
|
332
|
+
const body = h("div", { style: embedded ? { ...styles.panel, width: "100%", maxWidth: "100%", maxHeight: "none" } : styles.panel },
|
|
333
|
+
!embedded && h("div", { style: styles.header },
|
|
334
|
+
h("span", { style: styles.title }, t("memory.settings.title")),
|
|
335
|
+
h("button", { style: styles.close, onClick: onClose }, "×")
|
|
336
|
+
),
|
|
337
|
+
h("div", { style: styles.tabs },
|
|
338
|
+
h("button", { style: { ...styles.tab, ...(sectionTab === "memory" ? styles.tabActive : {}) }, onClick: () => setSectionTab("memory") }, t("memory.panel.open")),
|
|
339
|
+
h("button", { style: { ...styles.tab, ...(sectionTab === "settings" ? styles.tabActive : {}) }, onClick: () => setSectionTab("settings") }, t("memory.settings.title"))
|
|
132
340
|
),
|
|
133
|
-
|
|
341
|
+
sectionTab === "memory"
|
|
342
|
+
? h(MemoryPanel, { t, embedded: true })
|
|
343
|
+
: h("div", { style: { overflowY: "auto" } },
|
|
344
|
+
// profile
|
|
345
|
+
h("div", { style: labelStyle }, t("memory.settings.profile")),
|
|
346
|
+
h("div", { style: hintStyle }, t("memory.settings.profileHint")),
|
|
347
|
+
h("textarea", {
|
|
348
|
+
style: { ...inputStyle, minHeight: 72, resize: "both", fontFamily: "inherit" },
|
|
349
|
+
value: profile,
|
|
350
|
+
placeholder: t("memory.settings.profile"),
|
|
351
|
+
onChange: (e) => setProfile(e.target.value)
|
|
352
|
+
}),
|
|
353
|
+
h("div", null,
|
|
354
|
+
h("button", { style: styles.footerButton, onClick: saveProfile }, t("memory.settings.profileSave")),
|
|
355
|
+
saved && h("span", { style: { fontSize: 12, color: "var(--dsw-alias-state-success, #2a7)" } }, t("memory.settings.profileSaved"))
|
|
356
|
+
),
|
|
357
|
+
// rules
|
|
358
|
+
h("div", { style: labelStyle }, t("memory.settings.rules")),
|
|
359
|
+
h("div", { style: hintStyle }, t("memory.settings.rulesHint")),
|
|
360
|
+
rules.length === 0 && h("div", { style: styles.hint }, t("memory.settings.empty")),
|
|
361
|
+
rules.map((rule, i) =>
|
|
362
|
+
h("div", { key: i, style: rowStyle },
|
|
363
|
+
h("span", { style: { fontSize: 13, flex: 1 } }, rule),
|
|
364
|
+
h("button", { style: { ...styles.footerButton, color: "var(--dsw-alias-state-error, #c33)" }, onClick: () => removeRule(i) }, "×")
|
|
365
|
+
)
|
|
366
|
+
),
|
|
367
|
+
h("div", { style: { display: "flex", gap: 6 } },
|
|
368
|
+
h("input", {
|
|
369
|
+
style: { ...inputStyle, flex: 1, marginBottom: 0 },
|
|
370
|
+
value: newRule,
|
|
371
|
+
placeholder: t("memory.settings.rulePlaceholder"),
|
|
372
|
+
onChange: (e) => setNewRule(e.target.value)
|
|
373
|
+
}),
|
|
374
|
+
h("button", { style: styles.footerButton, onClick: addRule }, t("memory.settings.ruleAdd"))
|
|
375
|
+
),
|
|
376
|
+
// custom commands
|
|
377
|
+
h("div", { style: labelStyle }, t("memory.settings.commands")),
|
|
378
|
+
h("div", { style: hintStyle }, t("memory.settings.commandsHint")),
|
|
379
|
+
commands.length === 0 && h("div", { style: styles.hint }, t("memory.settings.empty")),
|
|
380
|
+
commands.map((cmd) =>
|
|
381
|
+
h("div", { key: cmd.id, style: rowStyle },
|
|
382
|
+
h("div", { style: { flex: 1 } },
|
|
383
|
+
h("div", { style: { fontSize: 13, fontWeight: 600 } }, `/${cmd.name}`),
|
|
384
|
+
h("div", { style: { fontSize: 11, color: "var(--dsw-alias-label-tertiary, #999)" } }, cmd.description || cmd.instruction)
|
|
385
|
+
),
|
|
386
|
+
h("button", { style: { ...styles.footerButton, color: "var(--dsw-alias-state-error, #c33)" }, onClick: () => removeCommand(cmd.id) }, t("memory.settings.cmdDelete"))
|
|
387
|
+
)
|
|
388
|
+
),
|
|
389
|
+
h("div", { style: { display: "grid", gap: 6, marginTop: 6 } },
|
|
390
|
+
h("input", { style: { ...inputStyle, marginBottom: 0 }, value: newCmd.name, placeholder: t("memory.settings.cmdName"), onChange: (e) => setNewCmd({ ...newCmd, name: e.target.value }) }),
|
|
391
|
+
h("input", { style: { ...inputStyle, marginBottom: 0 }, value: newCmd.description, placeholder: t("memory.settings.cmdDesc"), onChange: (e) => setNewCmd({ ...newCmd, description: e.target.value }) }),
|
|
392
|
+
h("textarea", { style: { ...inputStyle, marginBottom: 0, minHeight: 48, resize: "both", fontFamily: "inherit" }, value: newCmd.instruction, placeholder: t("memory.settings.cmdInstruction"), onChange: (e) => setNewCmd({ ...newCmd, instruction: e.target.value }) }),
|
|
393
|
+
h("button", { style: styles.footerButton, onClick: addCommand }, t("memory.settings.cmdAdd")),
|
|
394
|
+
cmdError && h("div", { style: { fontSize: 12, color: "var(--dsw-alias-state-error, #c33)" } }, cmdError)
|
|
395
|
+
),
|
|
396
|
+
// vector search
|
|
397
|
+
h("div", { style: { ...labelStyle, marginTop: 20 } }, t("memory.settings.vectorTitle")),
|
|
398
|
+
h("div", { style: hintStyle }, t("memory.settings.vectorHint")),
|
|
399
|
+
h("label", { style: { display: "flex", alignItems: "center", gap: 6, marginBottom: 8, fontSize: 13 } },
|
|
400
|
+
h("input", { type: "checkbox", checked: !!vector.enabled, onChange: (e) => setVector({ ...vector, enabled: e.target.checked }) }),
|
|
401
|
+
h("span", null, t("memory.settings.vectorEnabled"))
|
|
402
|
+
),
|
|
403
|
+
h("input", { style: inputStyle, value: vector.baseUrl, placeholder: t("memory.settings.vectorBaseUrl"), onChange: (e) => setVector({ ...vector, baseUrl: e.target.value }) }),
|
|
404
|
+
h("input", { style: inputStyle, type: "password", value: vector.apiKey, placeholder: t("memory.settings.vectorApiKey"), onChange: (e) => setVector({ ...vector, apiKey: e.target.value }) }),
|
|
405
|
+
h("input", { style: inputStyle, value: vector.model, placeholder: t("memory.settings.vectorModel"), onChange: (e) => setVector({ ...vector, model: e.target.value }) }),
|
|
406
|
+
h("div", { style: { display: "flex", alignItems: "center", gap: 6, flexWrap: "wrap" } },
|
|
407
|
+
h("button", { style: styles.footerButton, onClick: saveVector }, t("memory.settings.vectorSave")),
|
|
408
|
+
vectorSaved && h("span", { style: { fontSize: 12, color: "var(--dsw-alias-state-success, #2a7)" } }, t("memory.settings.vectorSaved")),
|
|
409
|
+
h("button", { style: styles.footerButton, onClick: reindex, disabled: reindexing }, reindexing ? t("memory.settings.vectorReindexing") : t("memory.settings.vectorReindex")),
|
|
410
|
+
reindexMsg && h("span", { style: { fontSize: 12, color: "var(--dsw-alias-label-secondary, #666)" } }, reindexMsg)
|
|
411
|
+
)
|
|
412
|
+
)
|
|
134
413
|
);
|
|
414
|
+
return embedded
|
|
415
|
+
? body
|
|
416
|
+
: createPortal(h("div", { style: styles.overlay }, body), document.body);
|
|
135
417
|
}
|
|
136
418
|
|
|
137
419
|
const styles = {
|
|
@@ -158,26 +440,18 @@ window.__ModuleLoader__.load({
|
|
|
158
440
|
function apply(ctx) {
|
|
159
441
|
ctx.effect(() => ctx.locale.register(NS, dictionaries), "dsh-mneme: dictionaries");
|
|
160
442
|
|
|
443
|
+
// Register a custom section inside the DSH settings panel (nav label +
|
|
444
|
+
// embedded memory/settings content). Access is via the official settings
|
|
445
|
+
// panel only; no sidebar buttons are registered.
|
|
161
446
|
ctx.effect(() => {
|
|
162
447
|
const t = ctx.locale.bind(NS);
|
|
163
|
-
return ctx.slots.
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const [open, setOpen] = react.useState(false);
|
|
171
|
-
return react.createElement(react.Fragment, null,
|
|
172
|
-
react.createElement("button", {
|
|
173
|
-
onClick: () => setOpen(true),
|
|
174
|
-
style: { ...styles.footerButton, ...(open ? styles.footerButtonActive : {}) }
|
|
175
|
-
}, t("memory.panel.open")),
|
|
176
|
-
open && react.createElement(MemoryPanel, { t, onClose: () => setOpen(false) })
|
|
177
|
-
);
|
|
178
|
-
})
|
|
179
|
-
);
|
|
180
|
-
}, "dsh-mneme: sidebar action");
|
|
448
|
+
return ctx.slots.register({
|
|
449
|
+
name: "settings.section",
|
|
450
|
+
id: "dsh-mneme",
|
|
451
|
+
label: () => t("memory.settings.title"),
|
|
452
|
+
inject: () => ({})
|
|
453
|
+
}, () => react.createElement(SettingsPanel, { t, embedded: true, onClose: () => {} }));
|
|
454
|
+
}, "dsh-mneme: settings section");
|
|
181
455
|
}
|
|
182
456
|
|
|
183
457
|
exports.apply = apply;
|
package/lib/commands.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Custom slash-command manager: keeps the DSH command registry in sync with
|
|
2
|
+
// user-defined commands persisted in SQLite. Commands are registered on boot
|
|
3
|
+
// and (re)registered on add/remove through the API.
|
|
4
|
+
//
|
|
5
|
+
// Each custom command's handler returns the user-authored instruction as a
|
|
6
|
+
// success result; the DSH UI surfaces it as a model-directed instruction.
|
|
7
|
+
export function createCommandManager({ ctx, settings, logger }) {
|
|
8
|
+
const registered = new Map(); // name -> disposer
|
|
9
|
+
|
|
10
|
+
function registerOne(command) {
|
|
11
|
+
if (registered.has(command.name)) return;
|
|
12
|
+
let dispose;
|
|
13
|
+
try {
|
|
14
|
+
dispose = ctx.commands.register({
|
|
15
|
+
name: command.name,
|
|
16
|
+
description: command.description || `自定义指令 ${command.name}`,
|
|
17
|
+
handler: () => ({ kind: "success", text: command.instruction })
|
|
18
|
+
});
|
|
19
|
+
} catch (error) {
|
|
20
|
+
logger?.warn?.(`dsh-mneme: failed to register command /${command.name}: ${String(error)}`);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
registered.set(command.name, dispose);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function unregisterOne(name) {
|
|
27
|
+
const dispose = registered.get(name);
|
|
28
|
+
if (dispose) {
|
|
29
|
+
try {
|
|
30
|
+
dispose();
|
|
31
|
+
} catch {
|
|
32
|
+
/* ignore double-dispose */
|
|
33
|
+
}
|
|
34
|
+
registered.delete(name);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Register every stored command (boot-time sync). */
|
|
39
|
+
function sync() {
|
|
40
|
+
for (const command of settings.listCommands()) registerOne(command);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Add (or replace) a command and register it live. */
|
|
44
|
+
function add({ name, description, instruction }) {
|
|
45
|
+
const command = settings.addCommand({ name, description, instruction });
|
|
46
|
+
registerOne(command);
|
|
47
|
+
return command;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Remove a command by id and unregister it live. */
|
|
51
|
+
function remove(id) {
|
|
52
|
+
const existing = settings.listCommands().find((c) => c.id === id);
|
|
53
|
+
if (!existing) return false;
|
|
54
|
+
if (!settings.removeCommand(id)) return false;
|
|
55
|
+
unregisterOne(existing.name);
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function dispose() {
|
|
60
|
+
for (const name of [...registered.keys()]) unregisterOne(name);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { sync, add, remove, list: () => settings.listCommands(), dispose };
|
|
64
|
+
}
|
package/lib/embedding.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// OpenAI-compatible embedding client for vector search. DSH's LLM service is
|
|
2
|
+
// chat-only, so dsh-mneme calls an external `/embeddings` endpoint itself.
|
|
3
|
+
// Works with OpenAI, SiliconFlow, Zhipu, local Ollama (via OpenAI-compatible
|
|
4
|
+
// proxy) and any provider exposing the standard embeddings API.
|
|
5
|
+
const DEFAULT_TIMEOUT_MS = 15000;
|
|
6
|
+
|
|
7
|
+
/** Normalize a configured baseUrl into the full embeddings endpoint URL. */
|
|
8
|
+
function embeddingsUrl(baseUrl) {
|
|
9
|
+
const base = String(baseUrl ?? "").trim().replace(/\/+$/, "");
|
|
10
|
+
if (!base) return "";
|
|
11
|
+
// Accept both "https://host/v1" and a full path ending in /embeddings.
|
|
12
|
+
if (/\/embeddings$/i.test(base)) return base;
|
|
13
|
+
return `${base}/embeddings`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Call the embeddings API for one text. Resolves to a Float64 array, or null
|
|
18
|
+
* when the provider is not configured, the call fails, or the response is
|
|
19
|
+
* unusable. Never throws: failures degrade to keyword search.
|
|
20
|
+
*/
|
|
21
|
+
export async function embedText({ baseUrl, apiKey, model }, text) {
|
|
22
|
+
const url = embeddingsUrl(baseUrl);
|
|
23
|
+
if (!url || !apiKey || !model || !text) return null;
|
|
24
|
+
let res;
|
|
25
|
+
try {
|
|
26
|
+
res = await fetch(url, {
|
|
27
|
+
method: "POST",
|
|
28
|
+
headers: {
|
|
29
|
+
"Content-Type": "application/json",
|
|
30
|
+
"Authorization": `Bearer ${apiKey}`
|
|
31
|
+
},
|
|
32
|
+
body: JSON.stringify({ model, input: String(text).slice(0, 8000) }),
|
|
33
|
+
signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS)
|
|
34
|
+
});
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
if (!res.ok) return null;
|
|
39
|
+
let body;
|
|
40
|
+
try {
|
|
41
|
+
body = await res.json();
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
const vec = body?.data?.[0]?.embedding;
|
|
46
|
+
return Array.isArray(vec) && vec.length ? Array.from(vec) : null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Embedder bound to the current settings + store: on each write it re-embeds
|
|
51
|
+
* the row's title+content and stores the vector. Failures are swallowed so a
|
|
52
|
+
* flaky embedding endpoint never breaks memory writes.
|
|
53
|
+
*/
|
|
54
|
+
export function createEmbedder({ store, settings, logger }) {
|
|
55
|
+
async function embedFor(id, title, content) {
|
|
56
|
+
const cfg = settings.getVectorConfig();
|
|
57
|
+
if (!cfg?.enabled || !cfg.baseUrl || !cfg.apiKey || !cfg.model) return;
|
|
58
|
+
const text = [title, content].filter(Boolean).join("\n");
|
|
59
|
+
const vector = await embedText(cfg, text);
|
|
60
|
+
if (vector) {
|
|
61
|
+
store.setEmbedding(id, vector);
|
|
62
|
+
logger?.info?.(`[dsh-mneme] embedded memory ${id} (dim=${vector.length})`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
/** Fire-and-forget re-embed of a memory after any write. */
|
|
68
|
+
schedule(memory) {
|
|
69
|
+
if (!memory?.id) return;
|
|
70
|
+
embedFor(memory.id, memory.title, memory.content).catch(() => {});
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
/** Embed one text and return its vector (null on failure/disabled). */
|
|
74
|
+
async embed(query) {
|
|
75
|
+
const cfg = settings.getVectorConfig();
|
|
76
|
+
if (!cfg?.enabled || !cfg.baseUrl || !cfg.apiKey || !cfg.model) return null;
|
|
77
|
+
return embedText(cfg, query);
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
/** Batch re-index rows still missing an embedding. */
|
|
81
|
+
async reindexMissing(limit = 50) {
|
|
82
|
+
const cfg = settings.getVectorConfig();
|
|
83
|
+
if (!cfg?.enabled) return { indexed: 0, skipped: 0 };
|
|
84
|
+
const rows = store.needsEmbedding(limit);
|
|
85
|
+
let indexed = 0;
|
|
86
|
+
for (const row of rows) {
|
|
87
|
+
const text = [row.title, row.content].filter(Boolean).join("\n");
|
|
88
|
+
const vector = await embedText(cfg, text);
|
|
89
|
+
if (vector) {
|
|
90
|
+
store.setEmbedding(row.id, vector);
|
|
91
|
+
indexed++;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return { indexed, skipped: rows.length - indexed };
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -6,13 +6,16 @@ import { createInjector } from "./inject.js";
|
|
|
6
6
|
import { createSummarizer } from "./summarize.js";
|
|
7
7
|
import { createDreamScheduler } from "./dream.js";
|
|
8
8
|
import { createApi } from "./api.js";
|
|
9
|
+
import { createSettings } from "./settings.js";
|
|
10
|
+
import { createCommandManager } from "./commands.js";
|
|
11
|
+
import { createEmbedder } from "./embedding.js";
|
|
9
12
|
import { Config } from "./config.js";
|
|
10
13
|
import { mkdirSync } from "node:fs";
|
|
11
14
|
import { join } from "node:path";
|
|
12
15
|
import { homedir } from "node:os";
|
|
13
16
|
|
|
14
17
|
export const name = "dsh-mneme";
|
|
15
|
-
export const inject = ["tools", "systemPrompt", "webServer", "llm", "agentDefaultModel"];
|
|
18
|
+
export const inject = ["tools", "systemPrompt", "webServer", "llm", "agentDefaultModel", "commands"];
|
|
16
19
|
export { Config };
|
|
17
20
|
|
|
18
21
|
// Arrow (not function declaration): cordis 4 treats any apply with a
|
|
@@ -33,6 +36,23 @@ export const apply = (ctx, config) => {
|
|
|
33
36
|
const mirror = createMirror(memoryDir);
|
|
34
37
|
const service = createService({ store, mirror, config: cfg });
|
|
35
38
|
|
|
39
|
+
// User-configurable settings (profile, rules) and custom commands share the
|
|
40
|
+
// same SQLite file but live in dedicated tables, isolated from memories.
|
|
41
|
+
const settings = createSettings(store.db);
|
|
42
|
+
|
|
43
|
+
// Vector search: embedder calls the configured OpenAI-compatible embeddings
|
|
44
|
+
// endpoint on writes and for queries. service re-embeds after each write.
|
|
45
|
+
const embedder = createEmbedder({ store, settings, logger: ctx.logger });
|
|
46
|
+
service.setEmbedder(embedder);
|
|
47
|
+
|
|
48
|
+
// Custom commands: register persisted commands into the DSH command registry
|
|
49
|
+
// on boot; add/remove re-register live through the API.
|
|
50
|
+
let commands = null;
|
|
51
|
+
if (ctx.commands) {
|
|
52
|
+
commands = createCommandManager({ ctx, settings, logger: ctx.logger });
|
|
53
|
+
commands.sync();
|
|
54
|
+
}
|
|
55
|
+
|
|
36
56
|
// Human edits in mirror files win on every sync; merge them back first.
|
|
37
57
|
// TYPE_FILE maps each memory type to its mirror filename. Read every type's
|
|
38
58
|
// edits up front: mergeHumanEdits re-renders ALL mirror files on success, so
|
|
@@ -67,7 +87,7 @@ export const apply = (ctx, config) => {
|
|
|
67
87
|
const disposers = [];
|
|
68
88
|
|
|
69
89
|
ctx.inject(["systemPrompt"], (promptCtx) => {
|
|
70
|
-
if (cfg.autoInject) disposers.push(createInjector(promptCtx, service, cfg));
|
|
90
|
+
if (cfg.autoInject) disposers.push(createInjector(promptCtx, service, settings, cfg));
|
|
71
91
|
});
|
|
72
92
|
|
|
73
93
|
ctx.inject(["tools"], (toolsCtx) => {
|
|
@@ -78,7 +98,11 @@ export const apply = (ctx, config) => {
|
|
|
78
98
|
disposers.push(summarizer.dispose);
|
|
79
99
|
|
|
80
100
|
if (ctx.webServer) {
|
|
81
|
-
const api = createApi(ctx, service
|
|
101
|
+
const api = createApi(ctx, service, settings, commands ?? {
|
|
102
|
+
add: () => { throw new Error("commands unavailable"); },
|
|
103
|
+
remove: () => false,
|
|
104
|
+
list: () => []
|
|
105
|
+
}, embedder);
|
|
82
106
|
disposers.push(api.dispose);
|
|
83
107
|
}
|
|
84
108
|
|
|
@@ -89,6 +113,7 @@ export const apply = (ctx, config) => {
|
|
|
89
113
|
for (const dispose of disposers) {
|
|
90
114
|
if (typeof dispose === "function") dispose();
|
|
91
115
|
}
|
|
116
|
+
commands?.dispose();
|
|
92
117
|
if (dream) await dream.dispose();
|
|
93
118
|
store.close();
|
|
94
119
|
};
|