@modusensus/dsh-mneme 0.2.3 → 0.2.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 +27 -9
- package/lib/api.js +54 -4
- package/lib/client.js +58 -14
- package/lib/config.js +14 -2
- package/lib/dream/decisions.js +174 -62
- package/lib/dream.js +30 -5
- package/lib/index.js +4 -2
- package/lib/mirror.js +7 -1
- package/lib/service.js +117 -3
- package/lib/store.js +40 -0
- package/lib/tools.js +47 -4
- package/package.json +3 -1
- package/scripts/benchmark-embed.js +201 -0
- package/scripts/benchmark-rerank.js +166 -0
- package/scripts/e2e-dsh.js +216 -0
- package/scripts/stress-dsh.js +255 -0
- package/scripts/sync-lib.js +47 -0
- package/src/api.js +54 -4
- package/src/config.js +14 -2
- package/src/dream/decisions.js +174 -62
- package/src/dream.js +30 -5
- package/src/index.js +4 -2
- package/src/mirror.js +7 -1
- package/src/service.js +117 -3
- package/src/store.js +40 -0
- package/src/tools.js +47 -4
- package/test/api.test.js +385 -0
- package/test/audit.test.js +290 -0
- package/test/client.test.js +26 -0
- package/test/clustering.test.js +100 -0
- package/test/commands.test.js +69 -0
- package/test/config.test.js +31 -0
- package/test/dream.test.js +526 -0
- package/test/helpers/dream-mock.js +82 -0
- package/test/inject.test.js +82 -0
- package/test/local-embedder.test.js +227 -0
- package/test/mirror.test.js +249 -0
- package/test/reflection.test.js +226 -0
- package/test/reranker.test.js +197 -0
- package/test/semantic.test.js +123 -0
- package/test/service-search.test.js +169 -0
- package/test/service.test.js +198 -0
- package/test/settings.test.js +101 -0
- package/test/store.test.js +293 -0
- package/test/stress.test.js +209 -0
- package/test/summarize.test.js +156 -0
- package/test/tools.test.js +265 -0
- package/test/vector-index.test.js +205 -0
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://www.npmjs.com/package/@modusensus/dsh-mneme)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
[](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin)
|
|
8
|
-
[](https://github.com/modusensus/dsh-mneme)
|
|
9
9
|
|
|
10
10
|
> 给 DeepSeek Harness 的跨会话记忆插件:让 Agent 记住你、记住项目、自动整理记忆。**Mneme**(Μνήμη)——希腊记忆女神 Mnemosyne 之名,掌管记忆与梦境,正如 autoDream 在后台巩固记忆。
|
|
11
11
|
|
|
@@ -19,16 +19,17 @@
|
|
|
19
19
|
- **Markdown 镜像**:`preferences.md` / `projects.md` / `decisions.md` / `history.md` / `summary.md`,人类可读、可手工编辑(**人工修改优先**合并回库)
|
|
20
20
|
- **4+1 种记忆类型**:`preference`(偏好)/ `project`(项目)/ `decision`(决策)/ `history`(历史)/ `summary`(总览)
|
|
21
21
|
|
|
22
|
-
### 模型工具(
|
|
22
|
+
### 模型工具(7 个)
|
|
23
23
|
|
|
24
24
|
| 工具 | 功能 |
|
|
25
25
|
|------|------|
|
|
26
26
|
| `memory_save` | 记录一条记忆(自动按标题去重合并) |
|
|
27
27
|
| `memory_search` | 全文搜索(中文子串友好,可启用向量语义搜索) |
|
|
28
|
-
| `memory_list` |
|
|
28
|
+
| `memory_list` | 按类型分页列出(`include_archived=true` 可查看已归档) |
|
|
29
29
|
| `memory_update` | 修改已有记忆 |
|
|
30
30
|
| `memory_delete` | 删除记忆 |
|
|
31
31
|
| `memory_forget` | 抑制注入(降权不删除,可恢复) |
|
|
32
|
+
| `memory_archive` | 归档/恢复记忆(v0.2.5;归档后隐藏于列表/搜索/注入/整理,`archived=false` 可恢复) |
|
|
32
33
|
|
|
33
34
|
### 自动注入 + 会话摘要
|
|
34
35
|
|
|
@@ -158,15 +159,33 @@ dsh web
|
|
|
158
159
|
| `dreamThresholdChars` | `5000` | 触发整理的总字符阈值 |
|
|
159
160
|
| `dreamDelayMs` | `2000` | 整理异步延迟(去抖) |
|
|
160
161
|
| `dreamProvider` / `dreamModel` | 空 | dream 的 LLM 路由回退(默认用 agent 默认模型) |
|
|
162
|
+
| `dreamMaxTokens` | `4096` | dream LLM 调用最大 token 数 |
|
|
163
|
+
| `apiToken` | 空 | 可选 API 鉴权 token;设置后写操作与密钥接口要求 `Authorization: Bearer <apiToken>` |
|
|
161
164
|
| `embedProvider` | `openai` | 语义后端:`openai`(默认,兼容 v0.1)/ `local`(ONNX 离线)/ `ollama` |
|
|
162
165
|
| `localEmbedModel` | `Xenova/bge-small-zh-v1.5` | 本地 ONNX embedding 模型 |
|
|
163
166
|
| `localEmbedDimension` | `512` | 本地 embedding 向量维度 |
|
|
167
|
+
| `localEmbedDevice` | `cpu` | 本地推理设备:`cpu` / `gpu` |
|
|
164
168
|
| `localEmbedBatchSize` | `8` | 本地 embedding 批大小(1-64) |
|
|
165
169
|
| `ollamaBaseUrl` | `http://localhost:11434` | Ollama 服务地址 |
|
|
166
170
|
| `ollamaModel` | `nomic-embed-text` | Ollama embedding 模型 |
|
|
167
|
-
| `
|
|
171
|
+
| `embedModelCacheDir` | 空 | 模型缓存目录(空 = transformers 默认缓存) |
|
|
172
|
+
| `embedModelMirror` | `https://hf-mirror.com` | 模型下载镜像源 |
|
|
173
|
+
| `vectorSearchTopK` | `20` | 向量搜索返回 Top-K |
|
|
174
|
+
| `vectorSearchThreshold` | `0.65` | 向量搜索相似度阈值 |
|
|
175
|
+
| `hybridSearchVectorWeight` | `0.6` | 混合搜索向量权重 |
|
|
176
|
+
| `hybridSearchKeywordWeight` | `0.4` | 混合搜索关键词权重 |
|
|
177
|
+
| `rerankEnabled` | `false` | 是否启用 Rerank 精排(显式开启才加载本地 onnxruntime 模型) |
|
|
178
|
+
| `rerankProvider` | `none` | Rerank 后端:`local` / `none`(默认 `none`) |
|
|
168
179
|
| `rerankModel` | `Xenova/bge-reranker-base` | Rerank 交叉编码模型 |
|
|
180
|
+
| `rerankBatchSize` | `8` | Rerank 批大小 |
|
|
181
|
+
| `rerankMaxCandidates` | `30` | Rerank 最大候选数 |
|
|
169
182
|
| `rerankScoreThreshold` | `0.1` | Rerank 分数阈值(低于丢弃) |
|
|
183
|
+
| `reflectionUpdateEnabled` | `true` | update 决策总开关 |
|
|
184
|
+
| `reflectionFailureTracking` | `true` | 失败追踪总开关 |
|
|
185
|
+
| `reflectionUpdateMaxPerRun` | `2` | 每次整理最多 update 数 |
|
|
186
|
+
| `reflectionUpdateMinAgeHours` | `24` | 新建记忆保护期(小时) |
|
|
187
|
+
|
|
188
|
+
> 🔐 **API 安全**:DSH 无内置鉴权且默认仅监听 `127.0.0.1`。插件 API 默认开放(便于 Web 面板即装即用)。如需防护(如局域网暴露),在配置中设置 `apiToken`:写操作(画像/规则/命令)与密钥端点(`vector-config`、`vector-reindex`)需携带 `Authorization: Bearer <token>`(前端设置面板可填入同一 token),只读的 `list` / `search` / `semantic` 保持开放。`/api/dsh-mneme/vector-config` 返回的 `apiKey` 已掩码(`sk-***…`),存储仍保留明文供调用;前端回传空或掩码值表示"不改 key"。
|
|
170
189
|
|
|
171
190
|
## 🏗️ 架构
|
|
172
191
|
|
|
@@ -178,7 +197,7 @@ dsh web
|
|
|
178
197
|
│ 服务层:saveWithDedupe / injectCandidates │
|
|
179
198
|
│ / mergeHumanEdits / onWrite 钩子 │
|
|
180
199
|
├─────────────────────────────────────────────────┤
|
|
181
|
-
│ 模型接口:
|
|
200
|
+
│ 模型接口:7 个工具 + 自动注入 + 会话摘要 │
|
|
182
201
|
├─────────────────────────────────────────────────┤
|
|
183
202
|
│ autoDream:阈值调度 → LLM 决策清单 │
|
|
184
203
|
│ → 校验(fail-safe)→ 应用 → 摘要 │
|
|
@@ -195,7 +214,7 @@ src/
|
|
|
195
214
|
├── mirror.js # Markdown 镜像(渲染/解析,人工优先)
|
|
196
215
|
├── service.js # 领域逻辑(去重合并、注入筛选、写入钩子)
|
|
197
216
|
├── config.js # schemastery 配置 schema
|
|
198
|
-
├── tools.js #
|
|
217
|
+
├── tools.js # 7 个模型工具(defineTool)
|
|
199
218
|
├── inject.js # systemPrompt.context 动态注入
|
|
200
219
|
├── summarize.js # 会话结束 LLM 摘要
|
|
201
220
|
├── dream.js # autoDream 调度 + runDream(LLM 决策 + 摘要)
|
|
@@ -206,7 +225,7 @@ src/
|
|
|
206
225
|
lib/
|
|
207
226
|
├── client.js # Web 面板(手写 ModuleLoader bundle)
|
|
208
227
|
└── *.js # src 的同步分发产物
|
|
209
|
-
test/ #
|
|
228
|
+
test/ # 258 个 node:test 测试(含审计与三轴线压测不变量)
|
|
210
229
|
scripts/ # e2e-dsh.js 端到端演示 · stress-dsh.js 三轴线压测 · sync-lib.js 同步
|
|
211
230
|
```
|
|
212
231
|
|
|
@@ -215,14 +234,13 @@ scripts/ # e2e-dsh.js 端到端演示 · stress-dsh.js 三轴线压
|
|
|
215
234
|
```bash
|
|
216
235
|
cd dsh-mneme
|
|
217
236
|
npm install # 安装 peer 依赖(以 devDependencies 形式,用于本地测试)
|
|
218
|
-
npm test # 运行
|
|
237
|
+
npm test # 运行 258 个测试
|
|
219
238
|
npm run stress # 三轴线压测:长会话检索 / 冲突仲裁 / 多 Agent 并发(离线 mock LLM)
|
|
220
239
|
npm run sync # 把 src/ 同步到 lib/(发布时由 prepack 钩子自动执行)
|
|
221
240
|
```
|
|
222
241
|
|
|
223
242
|
> 压测(`npm run stress`)三条轴线:**长会话检索**(Recall@k、陈旧残留率)、**冲突裁决**(可重放仲裁集:审计快照 hash + receipt + 幂等回放)、**多 Agent 并发**(丢更新、重复合并、事务/崩溃恢复)。每次 autoDream 运行都会写入审计表 `dream_runs`(输入快照 digest + 决策清单 + 逐 id 去向 + receipt),让高通过率下也能定位静默错误。
|
|
224
243
|
|
|
225
|
-
> 注:`npm test` 使用 `--test-isolation=none` 适配受限沙箱;普通环境可直接 `node --test`。
|
|
226
244
|
> `lib/` 是 `src/` 的同步分发产物(`npm run sync`),其中 `lib/client.js` 为手写 Web 面板源码,不受同步影响。
|
|
227
245
|
|
|
228
246
|
## 📄 设计文档
|
package/lib/api.js
CHANGED
|
@@ -1,10 +1,43 @@
|
|
|
1
1
|
import { URL } from "node:url";
|
|
2
|
+
import { timingSafeEqual } from "node:crypto";
|
|
2
3
|
|
|
3
4
|
function sendJson(res, status, payload) {
|
|
4
5
|
res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
|
|
5
6
|
res.end(JSON.stringify(payload));
|
|
6
7
|
}
|
|
7
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Mask an API key for client display: keep a recognizable prefix and suffix,
|
|
11
|
+
* hide the middle. Empty keys stay empty; short keys are fully hidden.
|
|
12
|
+
* The mask only exists in the API layer — storage keeps the real key.
|
|
13
|
+
*/
|
|
14
|
+
function maskApiKey(key) {
|
|
15
|
+
if (!key) return "";
|
|
16
|
+
if (key.length <= 8) return "***";
|
|
17
|
+
return `${key.slice(0, 3)}***${key.slice(-4)}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** True when the request carries the configured apiToken (or no token is set). */
|
|
21
|
+
function isAuthorized(req, apiToken) {
|
|
22
|
+
if (!apiToken) return true;
|
|
23
|
+
const raw = req.headers?.authorization ?? req.headers?.["x-dsh-mneme-token"] ?? "";
|
|
24
|
+
const token = raw.startsWith("Bearer ") ? raw.slice(7).trim() : raw.trim();
|
|
25
|
+
if (token === "" || token.length !== apiToken.length) return false;
|
|
26
|
+
// Constant-time comparison: avoid leaking the token via timing when the API
|
|
27
|
+
// is exposed beyond loopback.
|
|
28
|
+
return timingSafeEqual(Buffer.from(token), Buffer.from(apiToken));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Reject a request with 401 when auth is enabled and the token is missing or
|
|
33
|
+
* wrong. Returns true when the request may proceed.
|
|
34
|
+
*/
|
|
35
|
+
function requireAuth(req, res, apiToken) {
|
|
36
|
+
if (isAuthorized(req, apiToken)) return true;
|
|
37
|
+
sendJson(res, 401, { error: "unauthorized" });
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
8
41
|
/** Collect the request body as text (tolerant of empty/invalid bodies). */
|
|
9
42
|
function readBody(req) {
|
|
10
43
|
return new Promise((resolve) => {
|
|
@@ -23,7 +56,7 @@ function parseBody(text) {
|
|
|
23
56
|
}
|
|
24
57
|
}
|
|
25
58
|
|
|
26
|
-
export function createApi(ctx, service, settings, commands, embedder, semantic = null) {
|
|
59
|
+
export function createApi(ctx, service, settings, commands, embedder, semantic = null, apiToken = "") {
|
|
27
60
|
const disposers = [];
|
|
28
61
|
|
|
29
62
|
// Ensure the service has an embedder when the API layer was handed one
|
|
@@ -113,6 +146,7 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
|
|
|
113
146
|
handler(req, res) {
|
|
114
147
|
try {
|
|
115
148
|
if (req.method === "PUT" || req.method === "POST") {
|
|
149
|
+
if (!requireAuth(req, res, apiToken)) return;
|
|
116
150
|
return readBody(req).then((text) => {
|
|
117
151
|
const body = parseBody(text);
|
|
118
152
|
settings.setProfile(typeof body.profile === "string" ? body.profile : "");
|
|
@@ -133,6 +167,7 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
|
|
|
133
167
|
handler(req, res) {
|
|
134
168
|
try {
|
|
135
169
|
if (req.method === "PUT" || req.method === "POST") {
|
|
170
|
+
if (!requireAuth(req, res, apiToken)) return;
|
|
136
171
|
return readBody(req).then((text) => {
|
|
137
172
|
const body = parseBody(text);
|
|
138
173
|
settings.setRules(Array.isArray(body.rules) ? body.rules : []);
|
|
@@ -152,19 +187,31 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
|
|
|
152
187
|
path: "/api/dsh-mneme/vector-config",
|
|
153
188
|
handler(req, res) {
|
|
154
189
|
try {
|
|
190
|
+
// Secret-bearing endpoint: fully protected when apiToken is set.
|
|
191
|
+
if (!requireAuth(req, res, apiToken)) return;
|
|
155
192
|
if (req.method === "PUT" || req.method === "POST") {
|
|
156
193
|
return readBody(req).then((text) => {
|
|
157
194
|
const body = parseBody(text);
|
|
195
|
+
// An empty apiKey, or one that already looks masked (round-trips
|
|
196
|
+
// through maskApiKey unchanged), means "keep the existing key".
|
|
197
|
+
// Only a fresh, unmasked key is treated as a real replacement.
|
|
198
|
+
const prev = settings.getVectorConfig();
|
|
199
|
+
const incoming = typeof body.apiKey === "string" ? body.apiKey.trim() : "";
|
|
200
|
+
const isMaskedOrEmpty = incoming === "" || maskApiKey(incoming) === incoming;
|
|
201
|
+
const key = isMaskedOrEmpty
|
|
202
|
+
? (prev?.apiKey ?? "")
|
|
203
|
+
: incoming;
|
|
158
204
|
const cfg = settings.setVectorConfig({
|
|
159
205
|
enabled: body.enabled,
|
|
160
206
|
baseUrl: body.baseUrl,
|
|
161
|
-
apiKey:
|
|
207
|
+
apiKey: key,
|
|
162
208
|
model: body.model
|
|
163
209
|
});
|
|
164
|
-
sendJson(res, 200, { config: cfg });
|
|
210
|
+
sendJson(res, 200, { config: { ...cfg, apiKey: maskApiKey(cfg.apiKey) } });
|
|
165
211
|
});
|
|
166
212
|
}
|
|
167
|
-
|
|
213
|
+
const cfg = settings.getVectorConfig() ?? { enabled: false, baseUrl: "", apiKey: "", model: "" };
|
|
214
|
+
sendJson(res, 200, { config: { ...cfg, apiKey: maskApiKey(cfg.apiKey) } });
|
|
168
215
|
} catch {
|
|
169
216
|
sendJson(res, 500, { error: "internal" });
|
|
170
217
|
}
|
|
@@ -177,6 +224,7 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
|
|
|
177
224
|
path: "/api/dsh-mneme/vector-reindex",
|
|
178
225
|
handler(req, res) {
|
|
179
226
|
try {
|
|
227
|
+
if (!requireAuth(req, res, apiToken)) return;
|
|
180
228
|
if (!embedder) {
|
|
181
229
|
sendJson(res, 200, { indexed: 0, skipped: 0, error: "vector-unavailable" });
|
|
182
230
|
return;
|
|
@@ -227,6 +275,7 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
|
|
|
227
275
|
handler(req, res) {
|
|
228
276
|
try {
|
|
229
277
|
if (req.method === "POST") {
|
|
278
|
+
if (!requireAuth(req, res, apiToken)) return;
|
|
230
279
|
return readBody(req).then((text) => {
|
|
231
280
|
const body = parseBody(text);
|
|
232
281
|
try {
|
|
@@ -242,6 +291,7 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
|
|
|
242
291
|
});
|
|
243
292
|
}
|
|
244
293
|
if (req.method === "DELETE") {
|
|
294
|
+
if (!requireAuth(req, res, apiToken)) return;
|
|
245
295
|
const url = new URL(req.url, "http://localhost");
|
|
246
296
|
const id = url.searchParams.get("id");
|
|
247
297
|
const removed = id ? commands.remove(id) : false;
|
package/lib/client.js
CHANGED
|
@@ -9,6 +9,19 @@ window.__ModuleLoader__.load({
|
|
|
9
9
|
let { useState, useEffect, useCallback, useRef } = react;
|
|
10
10
|
let { createPortal } = reactDom;
|
|
11
11
|
|
|
12
|
+
// Unified API fetcher: attaches the optional apiToken (set in the settings
|
|
13
|
+
// panel, persisted in localStorage) as a Bearer header. When no token has
|
|
14
|
+
// been configured the header is omitted and the API stays open (default).
|
|
15
|
+
const API_TOKEN_KEY = "dsh-mneme-api-token";
|
|
16
|
+
function apiFetch(path, opts = {}) {
|
|
17
|
+
const token = (typeof window !== "undefined" && window.localStorage)
|
|
18
|
+
? window.localStorage.getItem(API_TOKEN_KEY) || ""
|
|
19
|
+
: "";
|
|
20
|
+
const headers = { ...(opts.headers || {}) };
|
|
21
|
+
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
22
|
+
return fetch(path, { ...opts, headers });
|
|
23
|
+
}
|
|
24
|
+
|
|
12
25
|
const inject = ["slots", "locale"];
|
|
13
26
|
|
|
14
27
|
const NS = "memory";
|
|
@@ -53,7 +66,12 @@ window.__ModuleLoader__.load({
|
|
|
53
66
|
"memory.settings.vectorSaved": "配置已保存",
|
|
54
67
|
"memory.settings.vectorReindex": "重建索引",
|
|
55
68
|
"memory.settings.vectorReindexing": "索引中…",
|
|
56
|
-
"memory.settings.vectorReindexDone": "已索引 {n} 条"
|
|
69
|
+
"memory.settings.vectorReindexDone": "已索引 {n} 条",
|
|
70
|
+
"memory.settings.apiTokenTitle": "API Token(可选)",
|
|
71
|
+
"memory.settings.apiTokenHint": "设置后,写操作与密钥接口(画像/规则/命令/向量配置)需携带 Authorization: Bearer <token>;面板只读操作不受影响。清空并保存可关闭鉴权。",
|
|
72
|
+
"memory.settings.apiTokenPlaceholder": "留空 = 不鉴权(默认)",
|
|
73
|
+
"memory.settings.apiTokenSave": "保存 Token",
|
|
74
|
+
"memory.settings.apiTokenSaved": "Token 已保存"
|
|
57
75
|
},
|
|
58
76
|
en: {
|
|
59
77
|
"memory.panel.title": "Memory",
|
|
@@ -94,7 +112,12 @@ window.__ModuleLoader__.load({
|
|
|
94
112
|
"memory.settings.vectorSaved": "Config saved",
|
|
95
113
|
"memory.settings.vectorReindex": "Reindex",
|
|
96
114
|
"memory.settings.vectorReindexing": "Indexing…",
|
|
97
|
-
"memory.settings.vectorReindexDone": "Indexed {n} items"
|
|
115
|
+
"memory.settings.vectorReindexDone": "Indexed {n} items",
|
|
116
|
+
"memory.settings.apiTokenTitle": "API Token (optional)",
|
|
117
|
+
"memory.settings.apiTokenHint": "When set, write operations and secret endpoints (profile/rules/commands/vector config) require Authorization: Bearer <token>. Read-only panel calls stay open. Save empty to disable.",
|
|
118
|
+
"memory.settings.apiTokenPlaceholder": "Empty = no auth (default)",
|
|
119
|
+
"memory.settings.apiTokenSave": "Save Token",
|
|
120
|
+
"memory.settings.apiTokenSaved": "Token saved"
|
|
98
121
|
}
|
|
99
122
|
};
|
|
100
123
|
|
|
@@ -120,7 +143,7 @@ window.__ModuleLoader__.load({
|
|
|
120
143
|
const abortRef = useRef(null);
|
|
121
144
|
|
|
122
145
|
useEffect(() => {
|
|
123
|
-
|
|
146
|
+
apiFetch("/api/dsh-mneme/vector-config")
|
|
124
147
|
.then((res) => res.json())
|
|
125
148
|
.then((d) => setVecEnabled(!!d.config?.enabled))
|
|
126
149
|
.catch(() => {});
|
|
@@ -137,7 +160,7 @@ window.__ModuleLoader__.load({
|
|
|
137
160
|
const url = query.trim()
|
|
138
161
|
? `/api/dsh-mneme/search?q=${encodeURIComponent(query.trim())}&mode=${semantic ? "vector" : "auto"}`
|
|
139
162
|
: `/api/dsh-mneme/list?${params.toString()}`;
|
|
140
|
-
const res = await
|
|
163
|
+
const res = await apiFetch(url, { signal: controller.signal });
|
|
141
164
|
const data = await res.json();
|
|
142
165
|
setItems(data.items || []);
|
|
143
166
|
} catch (error) {
|
|
@@ -224,14 +247,18 @@ window.__ModuleLoader__.load({
|
|
|
224
247
|
const [vectorSaved, setVectorSaved] = react.useState(false);
|
|
225
248
|
const [reindexing, setReindexing] = react.useState(false);
|
|
226
249
|
const [reindexMsg, setReindexMsg] = react.useState("");
|
|
250
|
+
const [apiToken, setApiToken] = react.useState(() =>
|
|
251
|
+
(typeof window !== "undefined" && window.localStorage) ? window.localStorage.getItem("dsh-mneme-api-token") || "" : ""
|
|
252
|
+
);
|
|
253
|
+
const [apiTokenSaved, setApiTokenSaved] = react.useState(false);
|
|
227
254
|
|
|
228
255
|
const load = react.useCallback(async () => {
|
|
229
256
|
try {
|
|
230
257
|
const [p, r, c, v] = await Promise.all([
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
258
|
+
apiFetch("/api/dsh-mneme/profile").then((res) => res.json()),
|
|
259
|
+
apiFetch("/api/dsh-mneme/rules").then((res) => res.json()),
|
|
260
|
+
apiFetch("/api/dsh-mneme/commands").then((res) => res.json()),
|
|
261
|
+
apiFetch("/api/dsh-mneme/vector-config").then((res) => res.json())
|
|
235
262
|
]);
|
|
236
263
|
setProfile(p.profile || "");
|
|
237
264
|
setRules(Array.isArray(r.rules) ? r.rules : []);
|
|
@@ -244,7 +271,7 @@ window.__ModuleLoader__.load({
|
|
|
244
271
|
|
|
245
272
|
async function saveProfile() {
|
|
246
273
|
try {
|
|
247
|
-
await
|
|
274
|
+
await apiFetch("/api/dsh-mneme/profile", {
|
|
248
275
|
method: "PUT",
|
|
249
276
|
headers: { "Content-Type": "application/json" },
|
|
250
277
|
body: JSON.stringify({ profile })
|
|
@@ -255,7 +282,7 @@ window.__ModuleLoader__.load({
|
|
|
255
282
|
}
|
|
256
283
|
|
|
257
284
|
async function putRules(next) {
|
|
258
|
-
await
|
|
285
|
+
await apiFetch("/api/dsh-mneme/rules", {
|
|
259
286
|
method: "PUT",
|
|
260
287
|
headers: { "Content-Type": "application/json" },
|
|
261
288
|
body: JSON.stringify({ rules: next })
|
|
@@ -282,7 +309,7 @@ window.__ModuleLoader__.load({
|
|
|
282
309
|
const instruction = newCmd.instruction.trim();
|
|
283
310
|
if (!name || !instruction) return;
|
|
284
311
|
try {
|
|
285
|
-
const res = await
|
|
312
|
+
const res = await apiFetch("/api/dsh-mneme/commands", {
|
|
286
313
|
method: "POST",
|
|
287
314
|
headers: { "Content-Type": "application/json" },
|
|
288
315
|
body: JSON.stringify({ name, description: newCmd.description, instruction })
|
|
@@ -296,13 +323,13 @@ window.__ModuleLoader__.load({
|
|
|
296
323
|
}
|
|
297
324
|
|
|
298
325
|
async function removeCommand(id) {
|
|
299
|
-
await
|
|
326
|
+
await apiFetch(`/api/dsh-mneme/commands?id=${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
300
327
|
setCommands(commands.filter((c) => c.id !== id));
|
|
301
328
|
}
|
|
302
329
|
|
|
303
330
|
async function saveVector() {
|
|
304
331
|
try {
|
|
305
|
-
await
|
|
332
|
+
await apiFetch("/api/dsh-mneme/vector-config", {
|
|
306
333
|
method: "PUT",
|
|
307
334
|
headers: { "Content-Type": "application/json" },
|
|
308
335
|
body: JSON.stringify(vector)
|
|
@@ -316,7 +343,7 @@ window.__ModuleLoader__.load({
|
|
|
316
343
|
setReindexing(true);
|
|
317
344
|
setReindexMsg("");
|
|
318
345
|
try {
|
|
319
|
-
const res = await
|
|
346
|
+
const res = await apiFetch("/api/dsh-mneme/vector-reindex");
|
|
320
347
|
const data = await res.json();
|
|
321
348
|
const n = data.indexed ?? 0;
|
|
322
349
|
setReindexMsg(t("memory.settings.vectorReindexDone").replace("{n}", String(n)));
|
|
@@ -324,6 +351,15 @@ window.__ModuleLoader__.load({
|
|
|
324
351
|
setReindexing(false);
|
|
325
352
|
}
|
|
326
353
|
|
|
354
|
+
function saveToken() {
|
|
355
|
+
try {
|
|
356
|
+
if (apiToken.trim()) window.localStorage.setItem("dsh-mneme-api-token", apiToken.trim());
|
|
357
|
+
else window.localStorage.removeItem("dsh-mneme-api-token");
|
|
358
|
+
setApiTokenSaved(true);
|
|
359
|
+
setTimeout(() => setApiTokenSaved(false), 1500);
|
|
360
|
+
} catch { /* ignore */ }
|
|
361
|
+
}
|
|
362
|
+
|
|
327
363
|
const inputStyle = { ...styles.search, marginBottom: 8 };
|
|
328
364
|
const labelStyle = { fontSize: 12, fontWeight: 600, margin: "12px 0 4px" };
|
|
329
365
|
const hintStyle = { fontSize: 11, color: "var(--dsw-alias-label-tertiary, #999)", marginBottom: 8 };
|
|
@@ -341,6 +377,14 @@ window.__ModuleLoader__.load({
|
|
|
341
377
|
sectionTab === "memory"
|
|
342
378
|
? h(MemoryPanel, { t, embedded: true })
|
|
343
379
|
: h("div", { style: { overflowY: "auto" } },
|
|
380
|
+
// api token (optional)
|
|
381
|
+
h("div", { style: { ...labelStyle, marginTop: 20 } }, t("memory.settings.apiTokenTitle")),
|
|
382
|
+
h("div", { style: hintStyle }, t("memory.settings.apiTokenHint")),
|
|
383
|
+
h("div", { style: { display: "flex", gap: 6 } },
|
|
384
|
+
h("input", { style: { ...inputStyle, flex: 1, marginBottom: 0 }, type: "password", value: apiToken, placeholder: t("memory.settings.apiTokenPlaceholder"), onChange: (e) => setApiToken(e.target.value) }),
|
|
385
|
+
h("button", { style: styles.footerButton, onClick: saveToken }, t("memory.settings.apiTokenSave"))
|
|
386
|
+
),
|
|
387
|
+
apiTokenSaved && h("div", { style: { fontSize: 12, color: "var(--dsw-alias-state-success, #2a7)" } }, t("memory.settings.apiTokenSaved")),
|
|
344
388
|
// profile
|
|
345
389
|
h("div", { style: labelStyle }, t("memory.settings.profile")),
|
|
346
390
|
h("div", { style: hintStyle }, t("memory.settings.profileHint")),
|
package/lib/config.js
CHANGED
|
@@ -14,6 +14,15 @@ export const Config = z.object({
|
|
|
14
14
|
dreamModel: z.string(),
|
|
15
15
|
dreamMaxTokens: z.natural().min(256).max(32768).default(4096),
|
|
16
16
|
|
|
17
|
+
// --- API protection ------------------------------------------------------
|
|
18
|
+
// Optional shared token for the plugin's HTTP API. Empty (default) keeps
|
|
19
|
+
// the API open (DSH binds to 127.0.0.1 and has no built-in auth); when set,
|
|
20
|
+
// sensitive endpoints (vector-config, vector-reindex, and all write ops on
|
|
21
|
+
// profile/rules/commands) require `Authorization: Bearer <apiToken>` (or
|
|
22
|
+
// `X-DSH-Mneme-Token`). Read-only list/search/semantic stay open so the
|
|
23
|
+
// Web panel keeps working without the token.
|
|
24
|
+
apiToken: z.string(),
|
|
25
|
+
|
|
17
26
|
// --- semantic: local embedding provider (v0.2) --------------------------
|
|
18
27
|
// "openai" keeps the legacy external-API path (settings vector config);
|
|
19
28
|
// "local" runs an ONNX model in-process; "ollama" calls a local Ollama.
|
|
@@ -40,8 +49,11 @@ export const Config = z.object({
|
|
|
40
49
|
hybridSearchKeywordWeight: z.number().min(0).max(1).default(0.4),
|
|
41
50
|
|
|
42
51
|
// --- semantic: rerank layer (v0.2) --------------------------------------
|
|
43
|
-
|
|
44
|
-
|
|
52
|
+
// Opt-in by default (item ⑥): the local cross-encoder pulls in onnxruntime
|
|
53
|
+
// (transformers.js) at init, so a bare install must not load it. Only an
|
|
54
|
+
// explicit rerankEnabled=true + rerankProvider="local" constructs LocalReranker.
|
|
55
|
+
rerankEnabled: z.boolean().default(false),
|
|
56
|
+
rerankProvider: z.union([z.const("local"), z.const("none")]).default("none"),
|
|
45
57
|
rerankModel: z.string().default("Xenova/bge-reranker-base"),
|
|
46
58
|
rerankBatchSize: z.natural().min(1).max(64).default(8),
|
|
47
59
|
rerankMaxCandidates: z.natural().min(5).max(100).default(30),
|