@modusensus/dsh-mneme 0.7.2 → 0.7.3
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 +3 -0
- package/lib/api.js +6 -3
- package/lib/client.js +53 -1
- package/lib/config.js +10 -0
- package/lib/service.js +14 -0
- package/lib/settings.js +31 -0
- package/package.json +5 -2
- package/src/api.js +6 -3
- package/src/config.js +10 -0
- package/src/service.js +14 -0
- package/src/settings.js +31 -0
- package/test/api.test.js +27 -4
- package/test/client.test.js +20 -0
- package/test/config.test.js +7 -0
- package/test/settings.test.js +22 -0
package/README.md
CHANGED
|
@@ -241,6 +241,7 @@ v0.3.0 起新增**记忆基因**层:从记忆里抽取**命名实体**、**带
|
|
|
241
241
|
|
|
242
242
|
| 版本 | 亮点 |
|
|
243
243
|
|------|------|
|
|
244
|
+
| **v0.7.3** | issue #38 新功能:左下角入口按钮可选开关 `showSidebarTrigger`(默认开)——与 dsh-cost-meter 等抢占 footer slot 的插件冲突时可在 Web 面板「设置」一键关闭,仅隐藏按钮、记忆库标签不受影响;776 测试全绿 |
|
|
244
245
|
| **v0.7.2** | issue #35 修复:目录页删除按钮改面板内联两步确认(不再依赖宿主 `window.confirm`)+ 删除失败可见报错;issue #34 新功能:opt-in `injectTimePrefix` 对话开始自动注入当前时间一次(默认关);770 测试全绿 |
|
|
245
246
|
| **v0.7.1** | issue #31 修复:memory_save/memory_update 的 tags 桥接进 entity_attrs 标签存储(目录/`tag:` 检索/tagBoost 立即可见,`tags: []` 清空移回 untagged)+ `store.setMemoryTags` 反向同步 `memories.tags` 列 + autoTag 面板开关成为运行时消费方(settings 覆盖 config);764 测试全绿 |
|
|
246
247
|
| **v0.7.0** | 自进化记忆(heat 热度模型 + per-type 差异化半衰期 + sleep 热联合双保护)+ updated_at 语义修正(不算访问)+ recall_runs injected 两档标记 + 90 天滚动清理 + 实体热投影(ego-graph node heat → 前端节点大小/明暗);757 测试全绿 |
|
|
@@ -289,6 +290,7 @@ v0.3.0 起新增**记忆基因**层:从记忆里抽取**命名实体**、**带
|
|
|
289
290
|
| **v0.7.0** | ✅ 完成 | 自进化记忆 | heat 幂律衰减 + per-type 差异化半衰期(TYPE_DECAY)+ sleep 热联合双保护 + updated_at 语义修正 + recall_runs injected 两档标记 + 90 天清理 + 实体热投影(前端节点大小/明暗);757 测试全绿 |
|
|
290
291
|
| **v0.7.1** | ✅ 完成 | issue #31 修复 | memory_save/update tags 桥接 entity_attrs 标签存储 + 列反向同步 + autoTag 面板开关生效(settings 覆盖 config);764 测试全绿 |
|
|
291
292
|
| **v0.7.2** | ✅ 完成 | issue #34 + #35 修复 | 目录页删除按钮改内联两步确认 + 删除失败可见报错;opt-in `injectTimePrefix` 对话开始注入当前时间一次(默认关);770 测试全绿 |
|
|
293
|
+
| **v0.7.3** | ✅ 完成 | issue #38 新功能 | 左下角入口按钮可选开关 `showSidebarTrigger`(默认开,settings-over-config);Web 面板设置一键关闭,与 dsh-cost-meter 等 footer 插件冲突可隐藏按钮、记忆库标签不受影响;776 测试全绿 |
|
|
292
294
|
| **v0.8.0** | 🚧 计划中(9 月末) | 图谱增强 | 兴趣漂移可视化 + 跨 workspace 记忆共享 + 更多 heat 信号 |
|
|
293
295
|
|
|
294
296
|
> 新能力一律做成**可开关的功能**(配置启用/关闭),默认保守开启、不破坏现有行为。`failure_memories` 表与 autoDream 决策引擎已为后续反思性成长铺好路。
|
|
@@ -409,6 +411,7 @@ dsh web
|
|
|
409
411
|
| `memoryQualityFilter` | `{enabled:true, archiveThreshold:30, degradeThreshold:60, minContentLength:10}` | 记忆质量过滤(v0.4.6,默认开):写库前启发式打分 0-100,元记忆词汇/自指/过短/重复/近似重复扣分;≥60 正常存储,30-60 降权(注入排序按 importance×quality/100),<30 归档标记 `low_quality`(显式搜索仍可召回,永不自动注入) |
|
|
410
412
|
| `llmAudit` | `{enabled:true, retentionDays:90}` | LLM 消耗审计(v0.4.6,默认开):每次后台 LLM 调用(autoDream/autoSummarize)写 `llm_audit_logs`(tokens/duration/status/source);失败记 error 不阻塞;只读 API `/api/dsh-mneme/semantic/llm-audit` + `/llm-audit/stats` |
|
|
411
413
|
| `sessionLifecycleEnabled` | `false` | 会话生命周期(v0.6.0,默认关):开启后会话被删除/销毁时自动把该会话出生的记忆软隐藏(`session_disposed_at`,与 `archived` 正交、可恢复);存量无 `session_id` 的记忆永不参与清理 |
|
|
414
|
+
| `showSidebarTrigger` | `true` | 侧边栏底部(左下角)记忆入口按钮(issue #38,默认开):与其他插件(如 dsh-cost-meter)抢占同一 footer slot 导致 UI 冲突时可关掉;仅隐藏按钮,记忆库仍可通过顶部「记忆库」标签访问。Web 面板「设置」里有对应开关 |
|
|
412
415
|
|
|
413
416
|
> 🔐 **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"。
|
|
414
417
|
|
package/lib/api.js
CHANGED
|
@@ -238,19 +238,22 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
|
|
|
238
238
|
}
|
|
239
239
|
return true;
|
|
240
240
|
};
|
|
241
|
-
if (!setIfBoolean("autoTagEnabled") || !setIfBoolean("manualTagEnabled")) return;
|
|
241
|
+
if (!setIfBoolean("autoTagEnabled") || !setIfBoolean("manualTagEnabled") || !setIfBoolean("showSidebarTrigger")) return;
|
|
242
242
|
if (Object.keys(patch).length === 0) {
|
|
243
243
|
sendJson(res, 400, { error: "no valid config field provided" });
|
|
244
244
|
return;
|
|
245
245
|
}
|
|
246
|
+
// Each setter ignores the keys it doesn't own, so one patch can feed
|
|
247
|
+
// both stores safely (tag toggles → "autoTag", UI prefs → "ui").
|
|
246
248
|
settings.setAutoTagConfig(patch);
|
|
249
|
+
settings.setUiConfig(patch);
|
|
247
250
|
// Respond with the effective (settings-over-config merged) toggles
|
|
248
251
|
// so the panel always sees booleans that actually gate runtime
|
|
249
252
|
// behaviour (issue #31).
|
|
250
|
-
sendJson(res, 200, { config: service.
|
|
253
|
+
sendJson(res, 200, { config: service.getAppConfig() });
|
|
251
254
|
return;
|
|
252
255
|
}
|
|
253
|
-
sendJson(res, 200, { config: service.
|
|
256
|
+
sendJson(res, 200, { config: service.getAppConfig() });
|
|
254
257
|
} catch {
|
|
255
258
|
sendJson(res, 500, { error: "internal" });
|
|
256
259
|
}
|
package/lib/client.js
CHANGED
|
@@ -77,6 +77,11 @@ window.__ModuleLoader__.load({
|
|
|
77
77
|
"memory.settings.autoTagEnabled": "自动打标签",
|
|
78
78
|
"memory.settings.autoTagSave": "保存",
|
|
79
79
|
"memory.settings.autoTagSaved": "已保存",
|
|
80
|
+
"memory.settings.sidebarTriggerTitle": "侧边栏入口按钮",
|
|
81
|
+
"memory.settings.sidebarTriggerHint": "关闭后隐藏侧边栏底部(左下角)的记忆入口按钮。与其他插件 UI 冲突时建议关闭;记忆库仍可通过顶部「记忆库」标签访问",
|
|
82
|
+
"memory.settings.sidebarTriggerEnabled": "显示左下角入口按钮",
|
|
83
|
+
"memory.settings.sidebarTriggerSave": "保存",
|
|
84
|
+
"memory.settings.sidebarTriggerSaved": "已保存",
|
|
80
85
|
"memory.settings.ruleAdd": "添加规则",
|
|
81
86
|
"memory.settings.rulePlaceholder": "例如:回答时总是先给结论",
|
|
82
87
|
"memory.settings.commands": "自定义指令",
|
|
@@ -186,6 +191,11 @@ window.__ModuleLoader__.load({
|
|
|
186
191
|
"memory.settings.autoTagEnabled": "Auto-tagging",
|
|
187
192
|
"memory.settings.autoTagSave": "Save",
|
|
188
193
|
"memory.settings.autoTagSaved": "Saved",
|
|
194
|
+
"memory.settings.sidebarTriggerTitle": "Sidebar trigger",
|
|
195
|
+
"memory.settings.sidebarTriggerHint": "Hide the memory entrance button at the sidebar footer (bottom-left) when it clashes with other plugins. The library stays reachable via the 记忆库 conversation tab",
|
|
196
|
+
"memory.settings.sidebarTriggerEnabled": "Show the bottom-left trigger",
|
|
197
|
+
"memory.settings.sidebarTriggerSave": "Save",
|
|
198
|
+
"memory.settings.sidebarTriggerSaved": "Saved",
|
|
189
199
|
"memory.settings.ruleAdd": "Add Rule",
|
|
190
200
|
"memory.settings.rulePlaceholder": "e.g. Always lead with a conclusion",
|
|
191
201
|
"memory.settings.commands": "Custom Commands",
|
|
@@ -874,6 +884,8 @@ window.__ModuleLoader__.load({
|
|
|
874
884
|
const [apiTokenSaved, setApiTokenSaved] = react.useState(false);
|
|
875
885
|
const [autoTag, setAutoTag] = react.useState(false);
|
|
876
886
|
const [autoTagSaved, setAutoTagSaved] = react.useState(false);
|
|
887
|
+
const [showSidebarTrigger, setShowSidebarTrigger] = react.useState(true);
|
|
888
|
+
const [sidebarTriggerSaved, setSidebarTriggerSaved] = react.useState(false);
|
|
877
889
|
|
|
878
890
|
const load = react.useCallback(async () => {
|
|
879
891
|
try {
|
|
@@ -891,7 +903,9 @@ window.__ModuleLoader__.load({
|
|
|
891
903
|
try {
|
|
892
904
|
const g = await apiFetch("/api/dsh-mneme/config").then((res) => res.json());
|
|
893
905
|
setAutoTag(g.config?.autoTagEnabled === true);
|
|
894
|
-
|
|
906
|
+
// issue #38: sidebar trigger defaults to visible (true).
|
|
907
|
+
setShowSidebarTrigger(g.config?.showSidebarTrigger !== false);
|
|
908
|
+
} catch { /* keep defaults */ }
|
|
895
909
|
} catch { /* ignore */ }
|
|
896
910
|
}, []);
|
|
897
911
|
|
|
@@ -1000,6 +1014,18 @@ window.__ModuleLoader__.load({
|
|
|
1000
1014
|
} catch { /* ignore */ }
|
|
1001
1015
|
}
|
|
1002
1016
|
|
|
1017
|
+
async function saveSidebarTrigger() {
|
|
1018
|
+
try {
|
|
1019
|
+
await apiFetch("/api/dsh-mneme/config", {
|
|
1020
|
+
method: "PUT",
|
|
1021
|
+
headers: { "Content-Type": "application/json" },
|
|
1022
|
+
body: JSON.stringify({ showSidebarTrigger })
|
|
1023
|
+
});
|
|
1024
|
+
setSidebarTriggerSaved(true);
|
|
1025
|
+
setTimeout(() => setSidebarTriggerSaved(false), 1500);
|
|
1026
|
+
} catch { /* ignore */ }
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1003
1029
|
const inputStyle = { boxSizing: "border-box", width: "100%", height: 34, padding: "0 12px", borderRadius: 10, border: "1px solid var(--dsw-alias-border-l2, #ddd)", background: "var(--dsw-alias-bg-base, transparent)", color: "var(--dsw-alias-label-primary)", fontFamily: "inherit", fontSize: 13, outline: "none", marginBottom: 8 };
|
|
1004
1030
|
const labelStyle = { fontSize: 12, fontWeight: 600, margin: "12px 0 4px", color: "var(--dsw-alias-label-primary)" };
|
|
1005
1031
|
const hintStyle = { fontSize: 11, color: "var(--dsw-alias-label-tertiary, #999)", marginBottom: 8 };
|
|
@@ -1077,6 +1103,17 @@ window.__ModuleLoader__.load({
|
|
|
1077
1103
|
h("button", { style: styles.footerButton, onClick: saveAutoTag }, t("memory.settings.autoTagSave")),
|
|
1078
1104
|
autoTagSaved && h("span", { style: { fontSize: 12, color: "var(--dsw-alias-state-success, #2a7)" } }, t("memory.settings.autoTagSaved"))
|
|
1079
1105
|
),
|
|
1106
|
+
// sidebar trigger switch (issue #38)
|
|
1107
|
+
h("div", { style: { ...labelStyle, marginTop: 20 } }, t("memory.settings.sidebarTriggerTitle")),
|
|
1108
|
+
h("div", { style: hintStyle }, t("memory.settings.sidebarTriggerHint")),
|
|
1109
|
+
h("label", { style: { display: "flex", alignItems: "center", gap: 6, marginBottom: 8, fontSize: 13 } },
|
|
1110
|
+
h("input", { type: "checkbox", checked: showSidebarTrigger, onChange: (e) => setShowSidebarTrigger(e.target.checked) }),
|
|
1111
|
+
h("span", null, t("memory.settings.sidebarTriggerEnabled"))
|
|
1112
|
+
),
|
|
1113
|
+
h("div", { style: { display: "flex", alignItems: "center", gap: 6 } },
|
|
1114
|
+
h("button", { style: styles.footerButton, onClick: saveSidebarTrigger }, t("memory.settings.sidebarTriggerSave")),
|
|
1115
|
+
sidebarTriggerSaved && h("span", { style: { fontSize: 12, color: "var(--dsw-alias-state-success, #2a7)" } }, t("memory.settings.sidebarTriggerSaved"))
|
|
1116
|
+
),
|
|
1080
1117
|
// vector search
|
|
1081
1118
|
h("div", { style: { ...labelStyle, marginTop: 20 } }, t("memory.settings.vectorTitle")),
|
|
1082
1119
|
h("div", { style: hintStyle }, t("memory.settings.vectorHint")),
|
|
@@ -1749,7 +1786,22 @@ window.__ModuleLoader__.load({
|
|
|
1749
1786
|
// opens the full-viewport overlay instead — the library stays reachable
|
|
1750
1787
|
// from every conversation state.
|
|
1751
1788
|
function SidebarTrigger({ wide, t }) {
|
|
1789
|
+
// issue #38: the entrance button is now optional (settings-over-config
|
|
1790
|
+
// toggle, persisted via /api/dsh-mneme/config). While the value loads we
|
|
1791
|
+
// render the button (default = visible) so it never flashes out; if the
|
|
1792
|
+
// user disabled it, it drops away after the config resolves. The library
|
|
1793
|
+
// itself is unaffected — the 记忆库 conversation tab stays.
|
|
1794
|
+
const [visible, setVisible] = useState(true);
|
|
1795
|
+
useEffect(() => {
|
|
1796
|
+
let cancelled = false;
|
|
1797
|
+
apiFetch("/api/dsh-mneme/config")
|
|
1798
|
+
.then((res) => res.json())
|
|
1799
|
+
.then((d) => { if (!cancelled) setVisible(d.config?.showSidebarTrigger !== false); })
|
|
1800
|
+
.catch(() => { if (!cancelled) setVisible(true); });
|
|
1801
|
+
return () => { cancelled = true; };
|
|
1802
|
+
}, []);
|
|
1752
1803
|
const [, setOpen] = useOverlayOpen();
|
|
1804
|
+
if (!visible) return null;
|
|
1753
1805
|
return h("button", {
|
|
1754
1806
|
type: "button",
|
|
1755
1807
|
className: wide ? "mneme-trigger" : "mneme-trigger mneme-rail",
|
package/lib/config.js
CHANGED
|
@@ -203,6 +203,16 @@ export const Config = z.object({
|
|
|
203
203
|
// set false to disable the manual write path too.
|
|
204
204
|
manualTagEnabled: z.boolean().default(true),
|
|
205
205
|
|
|
206
|
+
// --- sidebar trigger (issue #38) ---------------------------------------
|
|
207
|
+
// The memory library is reachable from two UI surfaces: a "记忆库" tab in the
|
|
208
|
+
// conversation header AND an entrance button at the sidebar footer (bottom
|
|
209
|
+
// left). Some sidebars get crowded there — dsh-cost-meter and friends also
|
|
210
|
+
// claim the footer slot — so the entrance button is now optional. Off just
|
|
211
|
+
// hides the button; the conversation tab stays, so no functionality is lost.
|
|
212
|
+
// The Web panel exposes this as a toggle (settings-over-config, see
|
|
213
|
+
// settings.getUiConfig); the value here is the plugin-config default.
|
|
214
|
+
showSidebarTrigger: z.boolean().default(true),
|
|
215
|
+
|
|
206
216
|
// --- tag-weighted re-rank (v0.6.4) -------------------------------------
|
|
207
217
|
// Opt-in: boost candidates whose tags overlap the query/session tags.
|
|
208
218
|
tagBoostEnabled: z.boolean().default(false),
|
package/lib/service.js
CHANGED
|
@@ -105,6 +105,19 @@ export function createService({ store, mirror, config, onWrite, logger, settings
|
|
|
105
105
|
};
|
|
106
106
|
};
|
|
107
107
|
|
|
108
|
+
// App-wide effective config for the Web panel (/api/dsh-mneme/config): the
|
|
109
|
+
// tag toggles plus UI preferences (issue #38). Settings overrides plugin
|
|
110
|
+
// config when explicitly stored; unset keys fall back to the zod default.
|
|
111
|
+
const appConfig = () => {
|
|
112
|
+
const stored = settings?.getUiConfig?.() ?? {};
|
|
113
|
+
return {
|
|
114
|
+
...tagConfig(),
|
|
115
|
+
showSidebarTrigger: typeof stored.showSidebarTrigger === "boolean"
|
|
116
|
+
? stored.showSidebarTrigger
|
|
117
|
+
: (config.showSidebarTrigger ?? true)
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
|
|
108
121
|
// Optional recall recorder, installed via setRecallRecorder after creation.
|
|
109
122
|
// When searchMemories is called with recordRecall=true it receives the
|
|
110
123
|
// actual merged recall scene (candidates + scores + source + threshold) so
|
|
@@ -1803,6 +1816,7 @@ export function createService({ store, mirror, config, onWrite, logger, settings
|
|
|
1803
1816
|
// Effective tag toggle pair (settings-over-config merge) for the panel
|
|
1804
1817
|
// (/api/dsh-mneme/config GET/PUT).
|
|
1805
1818
|
getTagConfig: tagConfig,
|
|
1819
|
+
getAppConfig: appConfig,
|
|
1806
1820
|
applyMemoryTags: (memoryId, tags) => store.setMemoryTags(memoryId, tags),
|
|
1807
1821
|
saveAttr: (r) => store.saveAttr(r),
|
|
1808
1822
|
createEntity: (r) => store.createEntity(r),
|
package/lib/settings.js
CHANGED
|
@@ -176,6 +176,37 @@ export function createSettings(db) {
|
|
|
176
176
|
else if (cur.manualTagEnabled !== undefined) cfg.manualTagEnabled = cur.manualTagEnabled === true;
|
|
177
177
|
setSetting("autoTag", JSON.stringify(cfg));
|
|
178
178
|
return cfg;
|
|
179
|
+
},
|
|
180
|
+
/** UI preferences (issue #38): same settings-over-config pattern as the
|
|
181
|
+
* tag toggles. null = never explicitly stored → callers fall back to the
|
|
182
|
+
* plugin-config default, so a partial update can't invent a value for a
|
|
183
|
+
* key the user never touched. */
|
|
184
|
+
getUiConfig() {
|
|
185
|
+
const raw = getSetting("ui");
|
|
186
|
+
let stored = {};
|
|
187
|
+
if (raw) {
|
|
188
|
+
try {
|
|
189
|
+
const j = JSON.parse(raw);
|
|
190
|
+
if (j && typeof j === "object") stored = j;
|
|
191
|
+
} catch { /* fall through to defaults */ }
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
showSidebarTrigger: typeof stored.showSidebarTrigger === "boolean" ? stored.showSidebarTrigger : null
|
|
195
|
+
};
|
|
196
|
+
},
|
|
197
|
+
setUiConfig(partial) {
|
|
198
|
+
const cur = (() => {
|
|
199
|
+
const raw = getSetting("ui");
|
|
200
|
+
try {
|
|
201
|
+
const j = JSON.parse(raw);
|
|
202
|
+
return j && typeof j === "object" ? j : {};
|
|
203
|
+
} catch { return {}; }
|
|
204
|
+
})();
|
|
205
|
+
const cfg = {};
|
|
206
|
+
if (partial.showSidebarTrigger !== undefined) cfg.showSidebarTrigger = partial.showSidebarTrigger === true;
|
|
207
|
+
else if (cur.showSidebarTrigger !== undefined) cfg.showSidebarTrigger = cur.showSidebarTrigger === true;
|
|
208
|
+
setSetting("ui", JSON.stringify(cfg));
|
|
209
|
+
return cfg;
|
|
179
210
|
}
|
|
180
211
|
};
|
|
181
212
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@modusensus/dsh-mneme",
|
|
3
3
|
"description": "Cross-session memory plugin for DeepSeek Harness with autoDream consolidation: SQLite store, Markdown mirrors, 7 model tools, automatic injection, session summarization, user profile/rules, custom slash commands, vector (semantic) search, and a Web GUI panel",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.3",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -75,6 +75,9 @@
|
|
|
75
75
|
"adm-zip": "0.6.0"
|
|
76
76
|
},
|
|
77
77
|
"c8": {
|
|
78
|
-
"reporter": [
|
|
78
|
+
"reporter": [
|
|
79
|
+
"text",
|
|
80
|
+
"lcov"
|
|
81
|
+
]
|
|
79
82
|
}
|
|
80
83
|
}
|
package/src/api.js
CHANGED
|
@@ -238,19 +238,22 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
|
|
|
238
238
|
}
|
|
239
239
|
return true;
|
|
240
240
|
};
|
|
241
|
-
if (!setIfBoolean("autoTagEnabled") || !setIfBoolean("manualTagEnabled")) return;
|
|
241
|
+
if (!setIfBoolean("autoTagEnabled") || !setIfBoolean("manualTagEnabled") || !setIfBoolean("showSidebarTrigger")) return;
|
|
242
242
|
if (Object.keys(patch).length === 0) {
|
|
243
243
|
sendJson(res, 400, { error: "no valid config field provided" });
|
|
244
244
|
return;
|
|
245
245
|
}
|
|
246
|
+
// Each setter ignores the keys it doesn't own, so one patch can feed
|
|
247
|
+
// both stores safely (tag toggles → "autoTag", UI prefs → "ui").
|
|
246
248
|
settings.setAutoTagConfig(patch);
|
|
249
|
+
settings.setUiConfig(patch);
|
|
247
250
|
// Respond with the effective (settings-over-config merged) toggles
|
|
248
251
|
// so the panel always sees booleans that actually gate runtime
|
|
249
252
|
// behaviour (issue #31).
|
|
250
|
-
sendJson(res, 200, { config: service.
|
|
253
|
+
sendJson(res, 200, { config: service.getAppConfig() });
|
|
251
254
|
return;
|
|
252
255
|
}
|
|
253
|
-
sendJson(res, 200, { config: service.
|
|
256
|
+
sendJson(res, 200, { config: service.getAppConfig() });
|
|
254
257
|
} catch {
|
|
255
258
|
sendJson(res, 500, { error: "internal" });
|
|
256
259
|
}
|
package/src/config.js
CHANGED
|
@@ -203,6 +203,16 @@ export const Config = z.object({
|
|
|
203
203
|
// set false to disable the manual write path too.
|
|
204
204
|
manualTagEnabled: z.boolean().default(true),
|
|
205
205
|
|
|
206
|
+
// --- sidebar trigger (issue #38) ---------------------------------------
|
|
207
|
+
// The memory library is reachable from two UI surfaces: a "记忆库" tab in the
|
|
208
|
+
// conversation header AND an entrance button at the sidebar footer (bottom
|
|
209
|
+
// left). Some sidebars get crowded there — dsh-cost-meter and friends also
|
|
210
|
+
// claim the footer slot — so the entrance button is now optional. Off just
|
|
211
|
+
// hides the button; the conversation tab stays, so no functionality is lost.
|
|
212
|
+
// The Web panel exposes this as a toggle (settings-over-config, see
|
|
213
|
+
// settings.getUiConfig); the value here is the plugin-config default.
|
|
214
|
+
showSidebarTrigger: z.boolean().default(true),
|
|
215
|
+
|
|
206
216
|
// --- tag-weighted re-rank (v0.6.4) -------------------------------------
|
|
207
217
|
// Opt-in: boost candidates whose tags overlap the query/session tags.
|
|
208
218
|
tagBoostEnabled: z.boolean().default(false),
|
package/src/service.js
CHANGED
|
@@ -105,6 +105,19 @@ export function createService({ store, mirror, config, onWrite, logger, settings
|
|
|
105
105
|
};
|
|
106
106
|
};
|
|
107
107
|
|
|
108
|
+
// App-wide effective config for the Web panel (/api/dsh-mneme/config): the
|
|
109
|
+
// tag toggles plus UI preferences (issue #38). Settings overrides plugin
|
|
110
|
+
// config when explicitly stored; unset keys fall back to the zod default.
|
|
111
|
+
const appConfig = () => {
|
|
112
|
+
const stored = settings?.getUiConfig?.() ?? {};
|
|
113
|
+
return {
|
|
114
|
+
...tagConfig(),
|
|
115
|
+
showSidebarTrigger: typeof stored.showSidebarTrigger === "boolean"
|
|
116
|
+
? stored.showSidebarTrigger
|
|
117
|
+
: (config.showSidebarTrigger ?? true)
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
|
|
108
121
|
// Optional recall recorder, installed via setRecallRecorder after creation.
|
|
109
122
|
// When searchMemories is called with recordRecall=true it receives the
|
|
110
123
|
// actual merged recall scene (candidates + scores + source + threshold) so
|
|
@@ -1803,6 +1816,7 @@ export function createService({ store, mirror, config, onWrite, logger, settings
|
|
|
1803
1816
|
// Effective tag toggle pair (settings-over-config merge) for the panel
|
|
1804
1817
|
// (/api/dsh-mneme/config GET/PUT).
|
|
1805
1818
|
getTagConfig: tagConfig,
|
|
1819
|
+
getAppConfig: appConfig,
|
|
1806
1820
|
applyMemoryTags: (memoryId, tags) => store.setMemoryTags(memoryId, tags),
|
|
1807
1821
|
saveAttr: (r) => store.saveAttr(r),
|
|
1808
1822
|
createEntity: (r) => store.createEntity(r),
|
package/src/settings.js
CHANGED
|
@@ -176,6 +176,37 @@ export function createSettings(db) {
|
|
|
176
176
|
else if (cur.manualTagEnabled !== undefined) cfg.manualTagEnabled = cur.manualTagEnabled === true;
|
|
177
177
|
setSetting("autoTag", JSON.stringify(cfg));
|
|
178
178
|
return cfg;
|
|
179
|
+
},
|
|
180
|
+
/** UI preferences (issue #38): same settings-over-config pattern as the
|
|
181
|
+
* tag toggles. null = never explicitly stored → callers fall back to the
|
|
182
|
+
* plugin-config default, so a partial update can't invent a value for a
|
|
183
|
+
* key the user never touched. */
|
|
184
|
+
getUiConfig() {
|
|
185
|
+
const raw = getSetting("ui");
|
|
186
|
+
let stored = {};
|
|
187
|
+
if (raw) {
|
|
188
|
+
try {
|
|
189
|
+
const j = JSON.parse(raw);
|
|
190
|
+
if (j && typeof j === "object") stored = j;
|
|
191
|
+
} catch { /* fall through to defaults */ }
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
showSidebarTrigger: typeof stored.showSidebarTrigger === "boolean" ? stored.showSidebarTrigger : null
|
|
195
|
+
};
|
|
196
|
+
},
|
|
197
|
+
setUiConfig(partial) {
|
|
198
|
+
const cur = (() => {
|
|
199
|
+
const raw = getSetting("ui");
|
|
200
|
+
try {
|
|
201
|
+
const j = JSON.parse(raw);
|
|
202
|
+
return j && typeof j === "object" ? j : {};
|
|
203
|
+
} catch { return {}; }
|
|
204
|
+
})();
|
|
205
|
+
const cfg = {};
|
|
206
|
+
if (partial.showSidebarTrigger !== undefined) cfg.showSidebarTrigger = partial.showSidebarTrigger === true;
|
|
207
|
+
else if (cur.showSidebarTrigger !== undefined) cfg.showSidebarTrigger = cur.showSidebarTrigger === true;
|
|
208
|
+
setSetting("ui", JSON.stringify(cfg));
|
|
209
|
+
return cfg;
|
|
179
210
|
}
|
|
180
211
|
};
|
|
181
212
|
}
|
package/test/api.test.js
CHANGED
|
@@ -557,12 +557,26 @@ test("GET/PUT /api/dsh-mneme/config round-trips autoTag switches", async () => {
|
|
|
557
557
|
await route.handler(req("/api/dsh-mneme/config"), res1);
|
|
558
558
|
assert.equal(res1.statusCode, 200);
|
|
559
559
|
// unset → effective config falls back to plugin-config defaults:
|
|
560
|
-
// autoTagEnabled=false, manualTagEnabled=true (issue #31 default unification)
|
|
561
|
-
|
|
560
|
+
// autoTagEnabled=false, manualTagEnabled=true (issue #31 default unification),
|
|
561
|
+
// showSidebarTrigger=true (issue #38 default)
|
|
562
|
+
assert.deepEqual(JSON.parse(res1.body).config, { autoTagEnabled: false, manualTagEnabled: true, showSidebarTrigger: true });
|
|
562
563
|
const res2 = new FakeRes();
|
|
563
564
|
await route.handler(req("/api/dsh-mneme/config", "PUT", { autoTagEnabled: true, manualTagEnabled: false }), res2);
|
|
564
565
|
assert.equal(res2.statusCode, 200);
|
|
565
|
-
assert.deepEqual(JSON.parse(res2.body).config, { autoTagEnabled: true, manualTagEnabled: false });
|
|
566
|
+
assert.deepEqual(JSON.parse(res2.body).config, { autoTagEnabled: true, manualTagEnabled: false, showSidebarTrigger: true });
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
test("GET/PUT /api/dsh-mneme/config round-trips the sidebar trigger toggle (issue #38)", async () => {
|
|
570
|
+
const { routes } = setup();
|
|
571
|
+
const route = routes.find((r) => r.path === "/api/dsh-mneme/config");
|
|
572
|
+
const res = new FakeRes();
|
|
573
|
+
await route.handler(req("/api/dsh-mneme/config", "PUT", { showSidebarTrigger: false }), res);
|
|
574
|
+
assert.equal(res.statusCode, 200);
|
|
575
|
+
// Toggling the UI pref must not disturb the tag toggles (settings-over-config).
|
|
576
|
+
assert.deepEqual(JSON.parse(res.body).config, { autoTagEnabled: false, manualTagEnabled: true, showSidebarTrigger: false });
|
|
577
|
+
const res2 = new FakeRes();
|
|
578
|
+
await route.handler(req("/api/dsh-mneme/config"), res2);
|
|
579
|
+
assert.deepEqual(JSON.parse(res2.body).config, { autoTagEnabled: false, manualTagEnabled: true, showSidebarTrigger: false });
|
|
566
580
|
});
|
|
567
581
|
|
|
568
582
|
test("DELETE /api/dsh-mneme/memories removes by id and reports misses", async () => {
|
|
@@ -597,5 +611,14 @@ test("PUT /api/dsh-mneme/config supports partial update", async () => {
|
|
|
597
611
|
// Partial PUT only stores autoTagEnabled; manualTagEnabled stays unset and
|
|
598
612
|
// falls back to the plugin-config default (true) — a lone autoTag toggle must
|
|
599
613
|
// never silently disable manual tagging (issue #31).
|
|
600
|
-
assert.deepEqual(JSON.parse(res.body).config, { autoTagEnabled: true, manualTagEnabled: true });
|
|
614
|
+
assert.deepEqual(JSON.parse(res.body).config, { autoTagEnabled: true, manualTagEnabled: true, showSidebarTrigger: true });
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
test("PUT /api/dsh-mneme/config rejects non-boolean showSidebarTrigger", async () => {
|
|
618
|
+
const { routes } = setup();
|
|
619
|
+
const route = routes.find((r) => r.path === "/api/dsh-mneme/config");
|
|
620
|
+
const res = new FakeRes();
|
|
621
|
+
await route.handler(req("/api/dsh-mneme/config", "PUT", { showSidebarTrigger: "no" }), res);
|
|
622
|
+
assert.equal(res.statusCode, 400);
|
|
623
|
+
assert.match(JSON.parse(res.body).error, /showSidebarTrigger/);
|
|
601
624
|
});
|
package/test/client.test.js
CHANGED
|
@@ -403,3 +403,23 @@ test("delete failure surfaces visible error feedback", () => {
|
|
|
403
403
|
"a failed row must get a visible error style"
|
|
404
404
|
);
|
|
405
405
|
});
|
|
406
|
+
|
|
407
|
+
// issue #38: the sidebar foot trigger must be disableable so it stops colliding
|
|
408
|
+
// with other plugins that also claim the bottom-left footer slot (dsh-cost-meter
|
|
409
|
+
// et al). The client reads the settings-over-config toggle from the /config
|
|
410
|
+
// endpoint and renders no button when it is off; the 记忆库 conversation tab is
|
|
411
|
+
// unaffected, so nothing is lost by hiding the entry.
|
|
412
|
+
test("sidebar trigger is hidden when showSidebarTrigger is false (issue #38)", () => {
|
|
413
|
+
assert.ok(
|
|
414
|
+
clientSource.includes('apiFetch("/api/dsh-mneme/config")'),
|
|
415
|
+
"trigger must read the effective config from /api/dsh-mneme/config"
|
|
416
|
+
);
|
|
417
|
+
assert.ok(
|
|
418
|
+
/d\.config\?\.showSidebarTrigger !== false/.test(clientSource),
|
|
419
|
+
"trigger must hide when showSidebarTrigger is explicitly false and stay visible by default"
|
|
420
|
+
);
|
|
421
|
+
assert.ok(
|
|
422
|
+
clientSource.includes('"memory.settings.sidebarTriggerTitle"'),
|
|
423
|
+
"settings panel must expose a sidebar-trigger toggle"
|
|
424
|
+
);
|
|
425
|
+
});
|
package/test/config.test.js
CHANGED
|
@@ -55,3 +55,10 @@ test("startup probe: the reranker module never statically imports transformers/o
|
|
|
55
55
|
);
|
|
56
56
|
assert.match(src, /await import\("@huggingface\/transformers"\)/, "transformers.js loads lazily via dynamic import");
|
|
57
57
|
});
|
|
58
|
+
|
|
59
|
+
test("showSidebarTrigger is on by default, disabled explicitly (issue #38)", () => {
|
|
60
|
+
const cfg = Config({});
|
|
61
|
+
assert.equal(cfg.showSidebarTrigger, true, "sidebar trigger defaults to visible (behavior unchanged)");
|
|
62
|
+
const off = Config({ showSidebarTrigger: false });
|
|
63
|
+
assert.equal(off.showSidebarTrigger, false, "explicit false hides the trigger");
|
|
64
|
+
});
|
package/test/settings.test.js
CHANGED
|
@@ -120,3 +120,25 @@ test("autoTag config coerces non-boolean to false", () => {
|
|
|
120
120
|
assert.deepEqual(settings.getAutoTagConfig(), { autoTagEnabled: false, manualTagEnabled: false });
|
|
121
121
|
store.close();
|
|
122
122
|
});
|
|
123
|
+
|
|
124
|
+
test("ui config (sidebar trigger) defaults to null and round-trips only explicit keys", () => {
|
|
125
|
+
const { store, settings } = setup();
|
|
126
|
+
// Issue #38: same settings-over-config pattern as the tag toggles — an unset
|
|
127
|
+
// key is null so consumers fall back to the plugin-config default (visible).
|
|
128
|
+
assert.deepEqual(settings.getUiConfig(), { showSidebarTrigger: null });
|
|
129
|
+
settings.setUiConfig({ showSidebarTrigger: false });
|
|
130
|
+
assert.deepEqual(settings.getUiConfig(), { showSidebarTrigger: false });
|
|
131
|
+
settings.setUiConfig({ showSidebarTrigger: true });
|
|
132
|
+
assert.deepEqual(settings.getUiConfig(), { showSidebarTrigger: true });
|
|
133
|
+
// A partial update with no showSidebarTrigger keeps the stored value.
|
|
134
|
+
settings.setUiConfig({});
|
|
135
|
+
assert.deepEqual(settings.getUiConfig(), { showSidebarTrigger: true });
|
|
136
|
+
store.close();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("ui config coerces non-boolean to false", () => {
|
|
140
|
+
const { store, settings } = setup();
|
|
141
|
+
settings.setUiConfig({ showSidebarTrigger: "yes" });
|
|
142
|
+
assert.deepEqual(settings.getUiConfig(), { showSidebarTrigger: false });
|
|
143
|
+
store.close();
|
|
144
|
+
});
|