@modusensus/dsh-mneme 0.7.1 → 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 +418 -152
- package/{dsh-mneme/src → lib}/api.js +6 -3
- package/{dsh-mneme/lib → lib}/client.js +108 -9
- package/{dsh-mneme/src → lib}/config.js +15 -0
- package/{dsh-mneme/lib → lib}/inject.js +31 -2
- package/{dsh-mneme/src → lib}/service.js +14 -0
- package/{dsh-mneme/lib → lib}/settings.js +31 -0
- package/package.json +43 -18
- package/{dsh-mneme/lib → src}/api.js +6 -3
- package/{dsh-mneme/lib → src}/config.js +15 -0
- package/{dsh-mneme/src → src}/inject.js +31 -2
- package/{dsh-mneme/lib → src}/service.js +14 -0
- package/{dsh-mneme/src → src}/settings.js +31 -0
- package/{dsh-mneme/test → test}/api.test.js +27 -4
- package/{dsh-mneme/test → test}/client.test.js +57 -0
- package/{dsh-mneme/test → test}/config.test.js +14 -0
- package/{dsh-mneme/test → test}/inject.test.js +28 -0
- package/{dsh-mneme/test → test}/settings.test.js +22 -0
- package/.github/workflows/test.yml +0 -32
- package/CHANGELOG.md +0 -89
- package/CONTRIBUTING.md +0 -245
- package/SECURITY.md +0 -544
- package/docs/devlog/2026-08-14-dsh-mneme-dev-log.md +0 -247
- package/docs/devlog/2026-08-15-dsh-mneme-audit-stress-dev-log.md +0 -145
- package/docs/devlog/2026-08-15-dsh-mneme-pipeline-dev-log.md +0 -56
- package/docs/devlog/2026-08-15-dsh-mneme-reflection-dev-log.md +0 -77
- package/docs/devlog/2026-08-15-dsh-mneme-review-fixes-dev-log.md +0 -64
- package/docs/devlog/2026-08-15-dsh-mneme-semantic-dev-log.md +0 -90
- package/dsh-mneme/CHANGELOG.md +0 -295
- package/dsh-mneme/LICENSE +0 -21
- package/dsh-mneme/README.md +0 -484
- package/dsh-mneme/docs/AGENT_MEMORY_RESEARCH.md +0 -183
- package/dsh-mneme/docs/ENTITIES.md +0 -245
- package/dsh-mneme/docs/LOCAL_MODEL.md +0 -141
- package/dsh-mneme/docs/MIGRATION.md +0 -127
- package/dsh-mneme/docs/SEMANTIC.md +0 -256
- package/dsh-mneme/docs/SLEEP.md +0 -163
- package/dsh-mneme/package-lock.json +0 -1936
- package/dsh-mneme/package.json +0 -80
- package/v0.7-plan-2026-08-21.md +0 -75
- package//346/250/252/345/271/205.png +0 -0
- /package/{dsh-mneme/cordis.patch.yml → cordis.patch.yml} +0 -0
- /package/{dsh-mneme/lib → lib}/commands.js +0 -0
- /package/{dsh-mneme/lib → lib}/dream/clustering.js +0 -0
- /package/{dsh-mneme/lib → lib}/dream/decisions.js +0 -0
- /package/{dsh-mneme/lib → lib}/dream/sleep.js +0 -0
- /package/{dsh-mneme/lib → lib}/dream/tag-extractor.js +0 -0
- /package/{dsh-mneme/lib → lib}/dream.js +0 -0
- /package/{dsh-mneme/lib → lib}/embedding.js +0 -0
- /package/{dsh-mneme/lib → lib}/entities/extractor.js +0 -0
- /package/{dsh-mneme/lib → lib}/heat.js +0 -0
- /package/{dsh-mneme/lib → lib}/hot-memory.js +0 -0
- /package/{dsh-mneme/lib → lib}/index.js +0 -0
- /package/{dsh-mneme/lib → lib}/local-embedder.js +0 -0
- /package/{dsh-mneme/lib → lib}/mirror.js +0 -0
- /package/{dsh-mneme/lib → lib}/parser/tag.js +0 -0
- /package/{dsh-mneme/lib → lib}/parser/wiki-link.js +0 -0
- /package/{dsh-mneme/lib → lib}/quality-filter.js +0 -0
- /package/{dsh-mneme/lib → lib}/reranker.js +0 -0
- /package/{dsh-mneme/lib → lib}/search/adaptive.js +0 -0
- /package/{dsh-mneme/lib → lib}/search/bm25.js +0 -0
- /package/{dsh-mneme/lib → lib}/search/tag-boost.js +0 -0
- /package/{dsh-mneme/lib → lib}/store.js +0 -0
- /package/{dsh-mneme/lib → lib}/summarize.js +0 -0
- /package/{dsh-mneme/lib → lib}/tools.js +0 -0
- /package/{dsh-mneme/lib → lib}/vector-index.js +0 -0
- /package/{dsh-mneme/scripts → scripts}/benchmark-embed.js +0 -0
- /package/{dsh-mneme/scripts → scripts}/benchmark-recall.js +0 -0
- /package/{dsh-mneme/scripts → scripts}/benchmark-rerank.js +0 -0
- /package/{dsh-mneme/scripts → scripts}/e2e-dsh.js +0 -0
- /package/{dsh-mneme/scripts → scripts}/stress-dsh.js +0 -0
- /package/{dsh-mneme/scripts → scripts}/sync-lib.js +0 -0
- /package/{dsh-mneme/src → src}/commands.js +0 -0
- /package/{dsh-mneme/src → src}/dream/clustering.js +0 -0
- /package/{dsh-mneme/src → src}/dream/decisions.js +0 -0
- /package/{dsh-mneme/src → src}/dream/sleep.js +0 -0
- /package/{dsh-mneme/src → src}/dream/tag-extractor.js +0 -0
- /package/{dsh-mneme/src → src}/dream.js +0 -0
- /package/{dsh-mneme/src → src}/embedding.js +0 -0
- /package/{dsh-mneme/src → src}/entities/extractor.js +0 -0
- /package/{dsh-mneme/src → src}/heat.js +0 -0
- /package/{dsh-mneme/src → src}/hot-memory.js +0 -0
- /package/{dsh-mneme/src → src}/index.js +0 -0
- /package/{dsh-mneme/src → src}/local-embedder.js +0 -0
- /package/{dsh-mneme/src → src}/mirror.js +0 -0
- /package/{dsh-mneme/src → src}/parser/tag.js +0 -0
- /package/{dsh-mneme/src → src}/parser/wiki-link.js +0 -0
- /package/{dsh-mneme/src → src}/quality-filter.js +0 -0
- /package/{dsh-mneme/src → src}/reranker.js +0 -0
- /package/{dsh-mneme/src → src}/search/adaptive.js +0 -0
- /package/{dsh-mneme/src → src}/search/bm25.js +0 -0
- /package/{dsh-mneme/src → src}/search/tag-boost.js +0 -0
- /package/{dsh-mneme/src → src}/store.js +0 -0
- /package/{dsh-mneme/src → src}/summarize.js +0 -0
- /package/{dsh-mneme/src → src}/tools.js +0 -0
- /package/{dsh-mneme/src → src}/vector-index.js +0 -0
- /package/{dsh-mneme/test → test}/audit.test.js +0 -0
- /package/{dsh-mneme/test → test}/benchmark.test.js +0 -0
- /package/{dsh-mneme/test → test}/boundary-v0625.test.js +0 -0
- /package/{dsh-mneme/test → test}/clustering.test.js +0 -0
- /package/{dsh-mneme/test → test}/commands.test.js +0 -0
- /package/{dsh-mneme/test → test}/conflict-freeze.test.js +0 -0
- /package/{dsh-mneme/test → test}/directory.test.js +0 -0
- /package/{dsh-mneme/test → test}/dream.test.js +0 -0
- /package/{dsh-mneme/test → test}/entities.test.js +0 -0
- /package/{dsh-mneme/test → test}/epistemic.test.js +0 -0
- /package/{dsh-mneme/test → test}/fnew-0112.test.js +0 -0
- /package/{dsh-mneme/test → test}/fnew-03.test.js +0 -0
- /package/{dsh-mneme/test → test}/graph-api.test.js +0 -0
- /package/{dsh-mneme/test → test}/heat.test.js +0 -0
- /package/{dsh-mneme/test → test}/helpers/dream-mock.js +0 -0
- /package/{dsh-mneme/test → test}/hot-memory.test.js +0 -0
- /package/{dsh-mneme/test → test}/llm-audit.test.js +0 -0
- /package/{dsh-mneme/test → test}/local-embedder.test.js +0 -0
- /package/{dsh-mneme/test → test}/mirror-dirty.test.js +0 -0
- /package/{dsh-mneme/test → test}/mirror-edit-digest.test.js +0 -0
- /package/{dsh-mneme/test → test}/mirror-generation.test.js +0 -0
- /package/{dsh-mneme/test → test}/mirror.test.js +0 -0
- /package/{dsh-mneme/test → test}/normalize-decisions.test.js +0 -0
- /package/{dsh-mneme/test → test}/peer-blockers.test.js +0 -0
- /package/{dsh-mneme/test → test}/policy-epoch.test.js +0 -0
- /package/{dsh-mneme/test → test}/provenance.test.js +0 -0
- /package/{dsh-mneme/test → test}/quality-filter.test.js +0 -0
- /package/{dsh-mneme/test → test}/reasoning-effort.test.js +0 -0
- /package/{dsh-mneme/test → test}/recall-evals.test.js +0 -0
- /package/{dsh-mneme/test → test}/recall-layer.test.js +0 -0
- /package/{dsh-mneme/test → test}/recall-runs.test.js +0 -0
- /package/{dsh-mneme/test → test}/receipt-chain.test.js +0 -0
- /package/{dsh-mneme/test → test}/reflection.test.js +0 -0
- /package/{dsh-mneme/test → test}/reranker.test.js +0 -0
- /package/{dsh-mneme/test → test}/search-fusion.test.js +0 -0
- /package/{dsh-mneme/test → test}/semantic.test.js +0 -0
- /package/{dsh-mneme/test → test}/service-search.test.js +0 -0
- /package/{dsh-mneme/test → test}/service.test.js +0 -0
- /package/{dsh-mneme/test → test}/sleep-heat.test.js +0 -0
- /package/{dsh-mneme/test → test}/sleep.test.js +0 -0
- /package/{dsh-mneme/test → test}/store.test.js +0 -0
- /package/{dsh-mneme/test → test}/stress.test.js +0 -0
- /package/{dsh-mneme/test → test}/summarize.test.js +0 -0
- /package/{dsh-mneme/test → test}/tag-boost.test.js +0 -0
- /package/{dsh-mneme/test → test}/tag.test.js +0 -0
- /package/{dsh-mneme/test → test}/tools.test.js +0 -0
- /package/{dsh-mneme/test → test}/updated-at-semantics.test.js +0 -0
- /package/{dsh-mneme/test → test}/vector-index.test.js +0 -0
- /package/{dsh-mneme/test → test}/wiki-link.test.js +0 -0
|
@@ -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
|
}
|
|
@@ -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": "自定义指令",
|
|
@@ -122,6 +127,8 @@ window.__ModuleLoader__.load({
|
|
|
122
127
|
"memory.directory.empty": "暂无记忆条目",
|
|
123
128
|
"directory.editToggle": "编辑模式(显示删除)",
|
|
124
129
|
"directory.deleteConfirm": "确定删除这条记忆吗?",
|
|
130
|
+
"directory.deleteConfirmShort": "确认?",
|
|
131
|
+
"directory.deleteError": "删除失败",
|
|
125
132
|
"memory.card.open": "在主区记忆库中查看全文",
|
|
126
133
|
"memory.time.now": "刚刚",
|
|
127
134
|
"memory.time.seconds": "{n}秒前",
|
|
@@ -184,6 +191,11 @@ window.__ModuleLoader__.load({
|
|
|
184
191
|
"memory.settings.autoTagEnabled": "Auto-tagging",
|
|
185
192
|
"memory.settings.autoTagSave": "Save",
|
|
186
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",
|
|
187
199
|
"memory.settings.ruleAdd": "Add Rule",
|
|
188
200
|
"memory.settings.rulePlaceholder": "e.g. Always lead with a conclusion",
|
|
189
201
|
"memory.settings.commands": "Custom Commands",
|
|
@@ -229,6 +241,8 @@ window.__ModuleLoader__.load({
|
|
|
229
241
|
"memory.directory.empty": "No memories yet",
|
|
230
242
|
"directory.editToggle": "Edit mode",
|
|
231
243
|
"directory.deleteConfirm": "Delete this memory?",
|
|
244
|
+
"directory.deleteConfirmShort": "Confirm?",
|
|
245
|
+
"directory.deleteError": "Delete failed",
|
|
232
246
|
"memory.card.open": "Open full text in the Memory tab",
|
|
233
247
|
"memory.time.now": "just now",
|
|
234
248
|
"memory.time.seconds": "{n}s ago",
|
|
@@ -432,6 +446,9 @@ window.__ModuleLoader__.load({
|
|
|
432
446
|
".mneme-dir-del{margin-left:auto;background:none;border:none;color:#e5484d;cursor:pointer;font-size:13px;line-height:1;padding:2px 4px}",
|
|
433
447
|
".mneme-dir-del:hover{filter:brightness(1.25)}",
|
|
434
448
|
".mneme-edit-on .mneme-dir-del{opacity:1}",
|
|
449
|
+
".mneme-dir-del-confirm{font-weight:700;background:rgba(229,72,77,.12);border-radius:4px}",
|
|
450
|
+
".mneme-dir-item.mneme-dir-error{background:rgba(229,72,77,.14)}",
|
|
451
|
+
".mneme-dir-del-err{flex:none;font-size:12px;line-height:16px;color:#e5484d}",
|
|
435
452
|
".mneme-dir-ititle{background:none;border:none;padding:0;font:inherit;color:inherit;cursor:pointer;flex:1;text-align:left;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
436
453
|
".mneme-dir-folder{margin-bottom:4px}",
|
|
437
454
|
".mneme-dir-folderhead{display:flex;align-items:center;gap:6px;width:100%;padding:6px 8px;border:none;border-radius:8px;background:none;color:var(--dsw-alias-label-primary);cursor:pointer;font-family:inherit;font-size:13px;font-weight:500;line-height:18px;text-align:left}",
|
|
@@ -867,6 +884,8 @@ window.__ModuleLoader__.load({
|
|
|
867
884
|
const [apiTokenSaved, setApiTokenSaved] = react.useState(false);
|
|
868
885
|
const [autoTag, setAutoTag] = react.useState(false);
|
|
869
886
|
const [autoTagSaved, setAutoTagSaved] = react.useState(false);
|
|
887
|
+
const [showSidebarTrigger, setShowSidebarTrigger] = react.useState(true);
|
|
888
|
+
const [sidebarTriggerSaved, setSidebarTriggerSaved] = react.useState(false);
|
|
870
889
|
|
|
871
890
|
const load = react.useCallback(async () => {
|
|
872
891
|
try {
|
|
@@ -884,7 +903,9 @@ window.__ModuleLoader__.load({
|
|
|
884
903
|
try {
|
|
885
904
|
const g = await apiFetch("/api/dsh-mneme/config").then((res) => res.json());
|
|
886
905
|
setAutoTag(g.config?.autoTagEnabled === true);
|
|
887
|
-
|
|
906
|
+
// issue #38: sidebar trigger defaults to visible (true).
|
|
907
|
+
setShowSidebarTrigger(g.config?.showSidebarTrigger !== false);
|
|
908
|
+
} catch { /* keep defaults */ }
|
|
888
909
|
} catch { /* ignore */ }
|
|
889
910
|
}, []);
|
|
890
911
|
|
|
@@ -993,6 +1014,18 @@ window.__ModuleLoader__.load({
|
|
|
993
1014
|
} catch { /* ignore */ }
|
|
994
1015
|
}
|
|
995
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
|
+
|
|
996
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 };
|
|
997
1030
|
const labelStyle = { fontSize: 12, fontWeight: 600, margin: "12px 0 4px", color: "var(--dsw-alias-label-primary)" };
|
|
998
1031
|
const hintStyle = { fontSize: 11, color: "var(--dsw-alias-label-tertiary, #999)", marginBottom: 8 };
|
|
@@ -1070,6 +1103,17 @@ window.__ModuleLoader__.load({
|
|
|
1070
1103
|
h("button", { style: styles.footerButton, onClick: saveAutoTag }, t("memory.settings.autoTagSave")),
|
|
1071
1104
|
autoTagSaved && h("span", { style: { fontSize: 12, color: "var(--dsw-alias-state-success, #2a7)" } }, t("memory.settings.autoTagSaved"))
|
|
1072
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
|
+
),
|
|
1073
1117
|
// vector search
|
|
1074
1118
|
h("div", { style: { ...labelStyle, marginTop: 20 } }, t("memory.settings.vectorTitle")),
|
|
1075
1119
|
h("div", { style: hintStyle }, t("memory.settings.vectorHint")),
|
|
@@ -1184,6 +1228,9 @@ window.__ModuleLoader__.load({
|
|
|
1184
1228
|
const [dir, setDir] = useState(null); // { groups: [...], untagged: [...] }
|
|
1185
1229
|
const [status, setStatus] = useState("loading"); // loading | ready | error
|
|
1186
1230
|
const [editMode, setEditMode] = useState(false); // manual tag/delete UI gate
|
|
1231
|
+
const [confirmingId, setConfirmingId] = useState(null); // row waiting for the 2nd confirm click
|
|
1232
|
+
const [delErrorId, setDelErrorId] = useState(null); // row whose delete just failed
|
|
1233
|
+
const confirmTimer = useRef(null);
|
|
1187
1234
|
|
|
1188
1235
|
useEffect(() => {
|
|
1189
1236
|
let cancelled = false;
|
|
@@ -1215,9 +1262,24 @@ window.__ModuleLoader__.load({
|
|
|
1215
1262
|
}).then((res) => { if (res.ok) setEditMode(next); }).catch(() => {});
|
|
1216
1263
|
};
|
|
1217
1264
|
|
|
1265
|
+
const resetConfirm = () => {
|
|
1266
|
+
clearTimeout(confirmTimer.current);
|
|
1267
|
+
setConfirmingId(null);
|
|
1268
|
+
};
|
|
1269
|
+
|
|
1218
1270
|
const handleDelete = (memId, e) => {
|
|
1219
1271
|
e?.stopPropagation?.();
|
|
1220
|
-
|
|
1272
|
+
// 第一击进入「确认?」态,第二击才真正发 DELETE;宿主 web 环境里
|
|
1273
|
+
// window.confirm 不可靠(可能被拦截或直接返回 false),点击会无反应,
|
|
1274
|
+
// 所以改为面板内联两步确认,不依赖任何原生对话框。
|
|
1275
|
+
if (confirmingId !== memId) {
|
|
1276
|
+
setConfirmingId(memId);
|
|
1277
|
+
clearTimeout(confirmTimer.current);
|
|
1278
|
+
confirmTimer.current = setTimeout(() => setConfirmingId((cur) => (cur === memId ? null : cur)), 3000);
|
|
1279
|
+
return;
|
|
1280
|
+
}
|
|
1281
|
+
clearTimeout(confirmTimer.current);
|
|
1282
|
+
setConfirmingId(null);
|
|
1221
1283
|
apiFetch("/api/dsh-mneme/memories", {
|
|
1222
1284
|
method: "DELETE",
|
|
1223
1285
|
headers: { "Content-Type": "application/json" },
|
|
@@ -1225,26 +1287,48 @@ window.__ModuleLoader__.load({
|
|
|
1225
1287
|
})
|
|
1226
1288
|
.then((res) => (res.ok ? res.json() : null))
|
|
1227
1289
|
.then((d) => {
|
|
1228
|
-
if (!d || d.deleted !== true)
|
|
1290
|
+
if (!d || d.deleted !== true) {
|
|
1291
|
+
// 服务端未确认删除也给出可见提示,不静默吞掉
|
|
1292
|
+
setDelErrorId(memId);
|
|
1293
|
+
setTimeout(() => setDelErrorId((cur) => (cur === memId ? null : cur)), 3500);
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1229
1296
|
setDir((prev) => ({
|
|
1230
1297
|
groups: prev.groups.map((g) => ({ ...g, memories: g.memories.filter((m) => m.id !== memId) })),
|
|
1231
1298
|
untagged: prev.untagged.filter((m) => m.id !== memId)
|
|
1232
1299
|
}));
|
|
1233
1300
|
})
|
|
1234
|
-
.catch(() => {
|
|
1301
|
+
.catch(() => {
|
|
1302
|
+
// 网络失败同样给出可见提示
|
|
1303
|
+
setDelErrorId(memId);
|
|
1304
|
+
setTimeout(() => setDelErrorId((cur) => (cur === memId ? null : cur)), 3500);
|
|
1305
|
+
});
|
|
1235
1306
|
};
|
|
1236
1307
|
|
|
1237
1308
|
const renderFolder = (name, memories, extraKey, emptyLabel) => {
|
|
1238
1309
|
const folderKey = extraKey || name;
|
|
1239
1310
|
const open = !collapsed[folderKey];
|
|
1240
|
-
const rows = memories.map((m) =>
|
|
1241
|
-
|
|
1311
|
+
const rows = memories.map((m) => {
|
|
1312
|
+
const confirming = confirmingId === m.id;
|
|
1313
|
+
const err = delErrorId === m.id;
|
|
1314
|
+
return h("div", {
|
|
1315
|
+
key: m.id,
|
|
1316
|
+
className: err ? "mneme-dir-item mneme-dir-error" : "mneme-dir-item",
|
|
1317
|
+
// 点击行内其它地方取消「确认?」态
|
|
1318
|
+
onClick: resetConfirm
|
|
1319
|
+
},
|
|
1242
1320
|
h("span", { className: "mneme-tree-line" }),
|
|
1243
1321
|
h("button", { type: "button", className: "mneme-dir-ititle", onClick: () => onJump(m) }, m.title || m.content?.slice(0, 40)),
|
|
1244
1322
|
h("span", { className: "mneme-dir-itime" }, formatDate(m.updated_at || m.created_at)),
|
|
1245
|
-
editMode && h("button", {
|
|
1246
|
-
|
|
1247
|
-
|
|
1323
|
+
editMode && h("button", {
|
|
1324
|
+
type: "button",
|
|
1325
|
+
className: confirming ? "mneme-dir-del mneme-dir-del-confirm" : "mneme-dir-del",
|
|
1326
|
+
"aria-label": "delete",
|
|
1327
|
+
onClick: (e) => handleDelete(m.id, e)
|
|
1328
|
+
}, confirming ? t("directory.deleteConfirmShort") : "✕"),
|
|
1329
|
+
err && h("span", { className: "mneme-dir-del-err" }, t("directory.deleteError"))
|
|
1330
|
+
);
|
|
1331
|
+
});
|
|
1248
1332
|
return h("div", {
|
|
1249
1333
|
key: folderKey,
|
|
1250
1334
|
className: "mneme-dir-folder",
|
|
@@ -1702,7 +1786,22 @@ window.__ModuleLoader__.load({
|
|
|
1702
1786
|
// opens the full-viewport overlay instead — the library stays reachable
|
|
1703
1787
|
// from every conversation state.
|
|
1704
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
|
+
}, []);
|
|
1705
1803
|
const [, setOpen] = useOverlayOpen();
|
|
1804
|
+
if (!visible) return null;
|
|
1706
1805
|
return h("button", {
|
|
1707
1806
|
type: "button",
|
|
1708
1807
|
className: wide ? "mneme-trigger" : "mneme-trigger mneme-rail",
|
|
@@ -17,6 +17,11 @@ export const Config = z.object({
|
|
|
17
17
|
summarizeModel: z.string().default(""),
|
|
18
18
|
maxInjectedItems: z.natural().min(1).max(20).default(5),
|
|
19
19
|
importanceThreshold: z.natural().min(1).max(5).default(3),
|
|
20
|
+
// 时间前缀注入(issue #34,opt-in,默认关):开启时,新对话开始时把当前
|
|
21
|
+
// 日期时间以 [当前时间: 2026-08-26 周二 19:30] 前缀注入一次,让模型感知
|
|
22
|
+
// "现在几点/周几"。只注入一次(对话开始时),同会话后续渲染不再重复;
|
|
23
|
+
// 关闭时行为与之前完全一致。
|
|
24
|
+
injectTimePrefix: z.boolean().default(false),
|
|
20
25
|
autoDream: z.boolean().default(true),
|
|
21
26
|
dreamThresholdCount: z.natural().min(1).max(1000).default(10),
|
|
22
27
|
dreamThresholdChars: z.natural().min(100).max(100000).default(5000),
|
|
@@ -198,6 +203,16 @@ export const Config = z.object({
|
|
|
198
203
|
// set false to disable the manual write path too.
|
|
199
204
|
manualTagEnabled: z.boolean().default(true),
|
|
200
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
|
+
|
|
201
216
|
// --- tag-weighted re-rank (v0.6.4) -------------------------------------
|
|
202
217
|
// Opt-in: boost candidates whose tags overlap the query/session tags.
|
|
203
218
|
tagBoostEnabled: z.boolean().default(false),
|
|
@@ -75,6 +75,32 @@ export function createInjector(ctx, service, settings, config) {
|
|
|
75
75
|
const maxItems = config.maxInjectedItems ?? 5;
|
|
76
76
|
const threshold = config.importanceThreshold ?? 3;
|
|
77
77
|
|
|
78
|
+
// Time-prefix injection (issue #34): opt-in, off by default. When enabled the
|
|
79
|
+
// current date/time is injected once per conversation — at the first prompt
|
|
80
|
+
// assembly of a new session — so the model can sense what time/day it is.
|
|
81
|
+
// A per-session latch means later assemblies in the same session never
|
|
82
|
+
// re-inject; a bare render ctx (no session id) falls back to once per
|
|
83
|
+
// injector lifetime.
|
|
84
|
+
const WEEKDAYS = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"];
|
|
85
|
+
let timePrefixSession = null;
|
|
86
|
+
|
|
87
|
+
function renderTimePrefix(ctx) {
|
|
88
|
+
if (config.injectTimePrefix !== true) return "";
|
|
89
|
+
const sessionId = ctx?.agent?.session?.id;
|
|
90
|
+
if (sessionId === undefined) {
|
|
91
|
+
if (timePrefixSession !== null) return "";
|
|
92
|
+
timePrefixSession = true;
|
|
93
|
+
} else {
|
|
94
|
+
if (sessionId === timePrefixSession) return "";
|
|
95
|
+
timePrefixSession = sessionId;
|
|
96
|
+
}
|
|
97
|
+
const now = new Date();
|
|
98
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
99
|
+
const date = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
|
|
100
|
+
const time = `${pad(now.getHours())}:${pad(now.getMinutes())}`;
|
|
101
|
+
return `[当前时间: ${date} ${WEEKDAYS[now.getDay()]} ${time}]`;
|
|
102
|
+
}
|
|
103
|
+
|
|
78
104
|
// Bug6: bound the injected memory block. Each entry's content is truncated to
|
|
79
105
|
// MAX_CONTENT chars (trailing `…`); the whole block gets a MAX_BLOCK budget
|
|
80
106
|
// and an entry that would exceed it collapses to its title only, so a long
|
|
@@ -188,8 +214,11 @@ export function createInjector(ctx, service, settings, config) {
|
|
|
188
214
|
// separate context) keeps the prompt assembly stable at two blocks.
|
|
189
215
|
const hotText = renderHotContext(ctx);
|
|
190
216
|
const body = render(candidates);
|
|
191
|
-
|
|
192
|
-
|
|
217
|
+
let out = !hotText ? body : (body ? `${hotText}\n\n${body}` : hotText);
|
|
218
|
+
// Time prefix (issue #34): leads the injection block once per session.
|
|
219
|
+
const timePrefix = renderTimePrefix(ctx);
|
|
220
|
+
if (!timePrefix) return out;
|
|
221
|
+
return out ? `${timePrefix}\n\n${out}` : timePrefix;
|
|
193
222
|
}
|
|
194
223
|
}),
|
|
195
224
|
ctx.systemPrompt.context({
|
|
@@ -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),
|
|
@@ -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
|
-
"
|
|
4
|
-
"
|
|
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.3",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -12,16 +12,23 @@
|
|
|
12
12
|
"url": "https://github.com/modusensus/dsh-mneme/issues"
|
|
13
13
|
},
|
|
14
14
|
"type": "module",
|
|
15
|
-
"main": "
|
|
15
|
+
"main": "lib/index.js",
|
|
16
16
|
"exports": {
|
|
17
17
|
".": {
|
|
18
|
-
"default": "./
|
|
18
|
+
"default": "./lib/index.js"
|
|
19
19
|
},
|
|
20
20
|
"./client": {
|
|
21
|
-
"default": "./
|
|
21
|
+
"default": "./lib/client.js"
|
|
22
22
|
},
|
|
23
|
-
"./package.json": "./
|
|
23
|
+
"./package.json": "./package.json"
|
|
24
24
|
},
|
|
25
|
+
"files": [
|
|
26
|
+
"lib",
|
|
27
|
+
"src",
|
|
28
|
+
"scripts",
|
|
29
|
+
"test",
|
|
30
|
+
"cordis.patch.yml"
|
|
31
|
+
],
|
|
25
32
|
"dsh": {
|
|
26
33
|
"client": {
|
|
27
34
|
"inject": [
|
|
@@ -33,20 +40,9 @@
|
|
|
33
40
|
"platform": "web"
|
|
34
41
|
},
|
|
35
42
|
"bundle": {
|
|
36
|
-
"patch": "./
|
|
37
|
-
},
|
|
38
|
-
"marketplace": {
|
|
39
|
-
"profiles": [
|
|
40
|
-
"web"
|
|
41
|
-
],
|
|
42
|
-
"requiresBuildApproval": false,
|
|
43
|
-
"requiresRestart": true,
|
|
44
|
-
"manualSteps": false
|
|
43
|
+
"patch": "./cordis.patch.yml"
|
|
45
44
|
}
|
|
46
45
|
},
|
|
47
|
-
"scripts": {
|
|
48
|
-
"prepublishOnly": "node -e \"const fs=require('fs');const v=JSON.parse(fs.readFileSync('./dsh-mneme/package.json','utf8')).version;const p=JSON.parse(fs.readFileSync('./package.json','utf8'));p.version=v;fs.writeFileSync('./package.json',JSON.stringify(p,null,2)+'\\n')\""
|
|
49
|
-
},
|
|
50
46
|
"peerDependencies": {
|
|
51
47
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
52
48
|
"@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6",
|
|
@@ -54,5 +50,34 @@
|
|
|
54
50
|
"@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.6",
|
|
55
51
|
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
|
|
56
52
|
"@deepseek-ai/schemastery": "^3.18.1"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
56
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6",
|
|
57
|
+
"@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
|
|
58
|
+
"@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.6",
|
|
59
|
+
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
|
|
60
|
+
"@deepseek-ai/schemastery": "^3.18.1",
|
|
61
|
+
"c8": "^12.0.0"
|
|
62
|
+
},
|
|
63
|
+
"scripts": {
|
|
64
|
+
"sync": "node scripts/sync-lib.js",
|
|
65
|
+
"prepack": "npm run sync",
|
|
66
|
+
"test": "node --test test/*.test.js",
|
|
67
|
+
"test:coverage": "c8 node --test test/*.test.js",
|
|
68
|
+
"e2e": "node scripts/e2e-dsh.js",
|
|
69
|
+
"stress": "node scripts/stress-dsh.js"
|
|
70
|
+
},
|
|
71
|
+
"dependencies": {
|
|
72
|
+
"@huggingface/transformers": "^4.2.0"
|
|
73
|
+
},
|
|
74
|
+
"overrides": {
|
|
75
|
+
"adm-zip": "0.6.0"
|
|
76
|
+
},
|
|
77
|
+
"c8": {
|
|
78
|
+
"reporter": [
|
|
79
|
+
"text",
|
|
80
|
+
"lcov"
|
|
81
|
+
]
|
|
57
82
|
}
|
|
58
83
|
}
|
|
@@ -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
|
}
|
|
@@ -17,6 +17,11 @@ export const Config = z.object({
|
|
|
17
17
|
summarizeModel: z.string().default(""),
|
|
18
18
|
maxInjectedItems: z.natural().min(1).max(20).default(5),
|
|
19
19
|
importanceThreshold: z.natural().min(1).max(5).default(3),
|
|
20
|
+
// 时间前缀注入(issue #34,opt-in,默认关):开启时,新对话开始时把当前
|
|
21
|
+
// 日期时间以 [当前时间: 2026-08-26 周二 19:30] 前缀注入一次,让模型感知
|
|
22
|
+
// "现在几点/周几"。只注入一次(对话开始时),同会话后续渲染不再重复;
|
|
23
|
+
// 关闭时行为与之前完全一致。
|
|
24
|
+
injectTimePrefix: z.boolean().default(false),
|
|
20
25
|
autoDream: z.boolean().default(true),
|
|
21
26
|
dreamThresholdCount: z.natural().min(1).max(1000).default(10),
|
|
22
27
|
dreamThresholdChars: z.natural().min(100).max(100000).default(5000),
|
|
@@ -198,6 +203,16 @@ export const Config = z.object({
|
|
|
198
203
|
// set false to disable the manual write path too.
|
|
199
204
|
manualTagEnabled: z.boolean().default(true),
|
|
200
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
|
+
|
|
201
216
|
// --- tag-weighted re-rank (v0.6.4) -------------------------------------
|
|
202
217
|
// Opt-in: boost candidates whose tags overlap the query/session tags.
|
|
203
218
|
tagBoostEnabled: z.boolean().default(false),
|
|
@@ -75,6 +75,32 @@ export function createInjector(ctx, service, settings, config) {
|
|
|
75
75
|
const maxItems = config.maxInjectedItems ?? 5;
|
|
76
76
|
const threshold = config.importanceThreshold ?? 3;
|
|
77
77
|
|
|
78
|
+
// Time-prefix injection (issue #34): opt-in, off by default. When enabled the
|
|
79
|
+
// current date/time is injected once per conversation — at the first prompt
|
|
80
|
+
// assembly of a new session — so the model can sense what time/day it is.
|
|
81
|
+
// A per-session latch means later assemblies in the same session never
|
|
82
|
+
// re-inject; a bare render ctx (no session id) falls back to once per
|
|
83
|
+
// injector lifetime.
|
|
84
|
+
const WEEKDAYS = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"];
|
|
85
|
+
let timePrefixSession = null;
|
|
86
|
+
|
|
87
|
+
function renderTimePrefix(ctx) {
|
|
88
|
+
if (config.injectTimePrefix !== true) return "";
|
|
89
|
+
const sessionId = ctx?.agent?.session?.id;
|
|
90
|
+
if (sessionId === undefined) {
|
|
91
|
+
if (timePrefixSession !== null) return "";
|
|
92
|
+
timePrefixSession = true;
|
|
93
|
+
} else {
|
|
94
|
+
if (sessionId === timePrefixSession) return "";
|
|
95
|
+
timePrefixSession = sessionId;
|
|
96
|
+
}
|
|
97
|
+
const now = new Date();
|
|
98
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
99
|
+
const date = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
|
|
100
|
+
const time = `${pad(now.getHours())}:${pad(now.getMinutes())}`;
|
|
101
|
+
return `[当前时间: ${date} ${WEEKDAYS[now.getDay()]} ${time}]`;
|
|
102
|
+
}
|
|
103
|
+
|
|
78
104
|
// Bug6: bound the injected memory block. Each entry's content is truncated to
|
|
79
105
|
// MAX_CONTENT chars (trailing `…`); the whole block gets a MAX_BLOCK budget
|
|
80
106
|
// and an entry that would exceed it collapses to its title only, so a long
|
|
@@ -188,8 +214,11 @@ export function createInjector(ctx, service, settings, config) {
|
|
|
188
214
|
// separate context) keeps the prompt assembly stable at two blocks.
|
|
189
215
|
const hotText = renderHotContext(ctx);
|
|
190
216
|
const body = render(candidates);
|
|
191
|
-
|
|
192
|
-
|
|
217
|
+
let out = !hotText ? body : (body ? `${hotText}\n\n${body}` : hotText);
|
|
218
|
+
// Time prefix (issue #34): leads the injection block once per session.
|
|
219
|
+
const timePrefix = renderTimePrefix(ctx);
|
|
220
|
+
if (!timePrefix) return out;
|
|
221
|
+
return out ? `${timePrefix}\n\n${out}` : timePrefix;
|
|
193
222
|
}
|
|
194
223
|
}),
|
|
195
224
|
ctx.systemPrompt.context({
|
|
@@ -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),
|