@modusensus/dsh-mneme 0.12.0 → 0.14.0-beta.1

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.
Files changed (182) hide show
  1. package/README.md +51 -20
  2. package/bin/dsh-mneme-choro.mjs +9 -1
  3. package/docs/CONFIGURATION.md +47 -6
  4. package/docs/DIRECTORIES.md +114 -0
  5. package/docs/DISTILL.md +64 -0
  6. package/docs/DSH-GO.md +97 -0
  7. package/docs/ENTITIES.md +5 -3
  8. package/docs/FORGET.md +77 -0
  9. package/docs/KB.md +110 -0
  10. package/docs/SLEEP.md +28 -3
  11. package/docs/STORAGE.md +23 -0
  12. package/lib/api-standalone.js +4 -1
  13. package/lib/api.js +788 -30
  14. package/lib/attachment-text.js +122 -0
  15. package/lib/choro-command.js +9 -2
  16. package/lib/client.js +4627 -1409
  17. package/lib/config.js +166 -1
  18. package/lib/continuity.js +7 -6
  19. package/lib/diary/auto.js +190 -0
  20. package/lib/diary/compose.js +440 -0
  21. package/lib/diary/dir.js +59 -0
  22. package/lib/diary/export.js +97 -0
  23. package/lib/diary/index.js +26 -0
  24. package/lib/diary/library.js +189 -0
  25. package/lib/diary/qq-inbox.js +142 -0
  26. package/lib/diary/rollover.js +61 -0
  27. package/lib/diary/session-provider.js +122 -0
  28. package/lib/dream/decisions.js +95 -6
  29. package/lib/dream/sleep.js +760 -19
  30. package/lib/dream/triage.js +139 -0
  31. package/lib/dream.js +168 -21
  32. package/lib/dsh-go/host.js +162 -0
  33. package/lib/dsh-go/panel.js +2091 -0
  34. package/lib/dsh-go/text.js +342 -0
  35. package/lib/dsh-go/views/common.js +68 -0
  36. package/lib/dsh-go/views/entity.js +48 -0
  37. package/lib/dsh-go/views/memory.js +198 -0
  38. package/lib/dsh-go/views/root.js +30 -0
  39. package/lib/dsh-go/views/settings.js +398 -0
  40. package/lib/dsh-go/views/status.js +51 -0
  41. package/lib/embedding.js +42 -8
  42. package/lib/entities/extractor.js +36 -7
  43. package/lib/hot-memory.js +22 -11
  44. package/lib/index.js +123 -3
  45. package/lib/inject.js +203 -20
  46. package/lib/kb/index.js +309 -0
  47. package/lib/kb/routes.js +220 -0
  48. package/lib/kb/tools.js +148 -0
  49. package/lib/lang.js +98 -4
  50. package/lib/peak-hours.js +57 -2
  51. package/lib/semantic.js +347 -60
  52. package/lib/service.js +268 -69
  53. package/lib/settings.js +128 -15
  54. package/lib/store.js +585 -26
  55. package/lib/summarize.js +38 -11
  56. package/lib/tools.js +35 -4
  57. package/lib/user-settings.js +128 -32
  58. package/lib/vector-index.js +76 -6
  59. package/lib/wiki/claims.js +267 -0
  60. package/lib/wiki/cli-hint.js +90 -0
  61. package/lib/wiki/queries.js +282 -2
  62. package/lib/wiki/render.js +74 -0
  63. package/lib/wiki/t0.js +43 -6
  64. package/lib/wiki/t1-jobs.js +232 -0
  65. package/lib/wiki/t1.js +145 -32
  66. package/package.json +39 -1
  67. package/scripts/release-docs.mjs +7 -1
  68. package/scripts/release-notes.mjs +4 -1
  69. package/src/api-standalone.js +4 -1
  70. package/src/api.js +788 -30
  71. package/src/attachment-text.js +122 -0
  72. package/src/choro-command.js +9 -2
  73. package/src/config.js +166 -1
  74. package/src/continuity.js +7 -6
  75. package/src/diary/auto.js +190 -0
  76. package/src/diary/compose.js +440 -0
  77. package/src/diary/dir.js +59 -0
  78. package/src/diary/export.js +97 -0
  79. package/src/diary/index.js +26 -0
  80. package/src/diary/library.js +189 -0
  81. package/src/diary/qq-inbox.js +142 -0
  82. package/src/diary/rollover.js +61 -0
  83. package/src/diary/session-provider.js +122 -0
  84. package/src/dream/decisions.js +95 -6
  85. package/src/dream/sleep.js +760 -19
  86. package/src/dream/triage.js +139 -0
  87. package/src/dream.js +168 -21
  88. package/src/dsh-go/host.js +162 -0
  89. package/src/dsh-go/panel.js +2091 -0
  90. package/src/dsh-go/text.js +342 -0
  91. package/src/dsh-go/views/common.js +68 -0
  92. package/src/dsh-go/views/entity.js +48 -0
  93. package/src/dsh-go/views/memory.js +198 -0
  94. package/src/dsh-go/views/root.js +30 -0
  95. package/src/dsh-go/views/settings.js +398 -0
  96. package/src/dsh-go/views/status.js +51 -0
  97. package/src/embedding.js +42 -8
  98. package/src/entities/extractor.js +36 -7
  99. package/src/hot-memory.js +22 -11
  100. package/src/index.js +123 -3
  101. package/src/inject.js +203 -20
  102. package/src/kb/index.js +309 -0
  103. package/src/kb/routes.js +220 -0
  104. package/src/kb/tools.js +148 -0
  105. package/src/lang.js +98 -4
  106. package/src/peak-hours.js +57 -2
  107. package/src/semantic.js +347 -60
  108. package/src/service.js +268 -69
  109. package/src/settings.js +128 -15
  110. package/src/store.js +585 -26
  111. package/src/summarize.js +38 -11
  112. package/src/tools.js +35 -4
  113. package/src/user-settings.js +128 -32
  114. package/src/vector-index.js +76 -6
  115. package/src/wiki/claims.js +267 -0
  116. package/src/wiki/cli-hint.js +90 -0
  117. package/src/wiki/queries.js +282 -2
  118. package/src/wiki/render.js +74 -0
  119. package/src/wiki/t0.js +43 -6
  120. package/src/wiki/t1-jobs.js +232 -0
  121. package/src/wiki/t1.js +145 -32
  122. package/test/api-routes-gaps.test.js +63 -0
  123. package/test/api.test.js +385 -4
  124. package/test/attachment-text.test.js +140 -0
  125. package/test/bm25-corpus-cache.test.js +145 -0
  126. package/test/capacity-warning.test.js +262 -0
  127. package/test/client.test.js +1051 -185
  128. package/test/conflict-freeze.test.js +45 -1
  129. package/test/conflict-ledger-enforce.test.js +452 -0
  130. package/test/conflict-ledger.test.js +242 -0
  131. package/test/continuity.test.js +8 -1
  132. package/test/contrast.test.js +124 -12
  133. package/test/diary-library.test.js +187 -0
  134. package/test/diary.test.js +576 -0
  135. package/test/dream-baseline-clamp.test.js +174 -0
  136. package/test/dream-maxtokens-fallback.test.js +3 -3
  137. package/test/dream.test.js +62 -1
  138. package/test/dsh-go-host.test.js +134 -0
  139. package/test/dsh-go-panel-actions.test.js +854 -0
  140. package/test/dsh-go-panel.test.js +251 -0
  141. package/test/embedding-ready.test.js +33 -3
  142. package/test/entities.test.js +73 -0
  143. package/test/forget-corrections.test.js +70 -0
  144. package/test/graph-api.test.js +124 -0
  145. package/test/hot-memory.test.js +48 -0
  146. package/test/image-memory.test.js +171 -0
  147. package/test/inject-parent-gate.test.js +6 -3
  148. package/test/inject.test.js +59 -0
  149. package/test/issue-617-step1-query-and-fusion.test.js +361 -0
  150. package/test/kb-api-wiring.test.js +128 -0
  151. package/test/kb-collections.test.js +272 -0
  152. package/test/kb-routes.test.js +379 -0
  153. package/test/mint-source.test.js +26 -0
  154. package/test/panel-chart-scale.test.js +90 -0
  155. package/test/panel-tokens.test.js +245 -0
  156. package/test/peak-hours-observability.test.js +70 -0
  157. package/test/persona-api.test.js +3 -3
  158. package/test/persona-by-session.test.js +568 -0
  159. package/test/persona-settings.test.js +7 -7
  160. package/test/reflection-update-gate.test.js +129 -0
  161. package/test/reindex-backfill.test.js +334 -0
  162. package/test/reindex-scheduler.test.js +369 -0
  163. package/test/release-docs.test.js +20 -0
  164. package/test/release-notes.test.js +25 -0
  165. package/test/scope-candidates.test.js +249 -0
  166. package/test/security-doc.test.js +10 -0
  167. package/test/sleep-action-set.test.js +14 -0
  168. package/test/sleep-heat.test.js +49 -0
  169. package/test/sleep.test.js +635 -0
  170. package/test/standalone-api.test.js +24 -0
  171. package/test/store.test.js +20 -0
  172. package/test/summarize.test.js +26 -0
  173. package/test/user-settings-segments.test.js +528 -0
  174. package/test/vector-index.test.js +120 -6
  175. package/test/wiki-api.test.js +463 -1
  176. package/test/wiki-claims.test.js +194 -0
  177. package/test/wiki-cli-hint.test.js +118 -0
  178. package/test/wiki-provenance.test.js +202 -0
  179. package/test/wiki-queries.test.js +128 -0
  180. package/test/wiki-t0.test.js +77 -1
  181. package/test/wiki-t1-jobs.test.js +125 -0
  182. package/test/wiki-t1.test.js +226 -1
@@ -0,0 +1,398 @@
1
+ // 设置 / 状态: two sibling areas, mirroring mneme's own panel where 设置 and
2
+ // 状态 are separate top-level tabs. The app's tab bar only fits three tabs at
3
+ // 80dp on a phone, so the third tab holds a two-item switch (设置 | 状态) and
4
+ // each area keeps its own inner navigation: 设置 is a card menu (like mneme's
5
+ // Settings cards) and 状态 is mneme's Status page with its grouped cards.
6
+ import { ENUM_FLAGS, FLAG_GROUPS, MODE_OPTIONS, flagAction, flagPath } from "../text.js";
7
+ import {
8
+ act,
9
+ actions,
10
+ b,
11
+ caption,
12
+ divider,
13
+ empty,
14
+ fold,
15
+ heading,
16
+ kv,
17
+ metric,
18
+ mono,
19
+ select,
20
+ text,
21
+ toggle,
22
+ } from "./common.js";
23
+ import { conflictPage } from "./memory.js";
24
+ import { statusSection } from "./status.js";
25
+
26
+ const SETTINGS_VIEWS = ["settings", "status"];
27
+
28
+ // 设置页的顺序与分组对齐 mneme 电脑端面板(个性化 / 记忆引擎 / 搜索 / 连接与
29
+ // 安全 / 帮助)。功能开关是记忆引擎的一部分,所以不再藏进子页面,直接铺在设置
30
+ // 页上;只有需要表单的子页(命令 / 向量 / 外部访问 / 工作动态 / 关于)才进入
31
+ // 「点一行 → 子页面」的导航。
32
+ const PAGES = [
33
+ { key: "commands", title: "自定义指令", hint: "把常用提示词存成 / 命令,聊天时直接键入调用。", icon: "command" },
34
+ {
35
+ key: "vector",
36
+ title: "向量检索",
37
+ hint: "搜索不再只认字面:接入任意 OpenAI 兼容的 embedding 接口后,意思相近的记忆也能被召回。",
38
+ icon: "chart",
39
+ },
40
+ {
41
+ key: "external",
42
+ title: "外部访问",
43
+ hint: "开放一个本机接口,让其他工具(插件、命令行、桌面应用)也能读写这份记忆库,默认只允许本机连接。改动保存后重启 DSH 生效。",
44
+ icon: "globe",
45
+ },
46
+ {
47
+ key: "choro",
48
+ title: "代码库地志",
49
+ hint: "给常用仓库生成一份代码骨架:模块划分、文件数、约定与构建方式,开会话时按档位注入。",
50
+ icon: "code",
51
+ },
52
+ { key: "help", title: "关于", icon: "info" },
53
+ ];
54
+
55
+ const pageRow = (page) => ({
56
+ title: page.title,
57
+ ...(page.hint === undefined ? {} : { subtitle: page.hint }),
58
+ icon: page.icon,
59
+ action: { id: "open-panel", args: { k: page.key } },
60
+ });
61
+
62
+ const rows = (keys) => ({
63
+ type: "list",
64
+ items: PAGES.filter((page) => keys.includes(page.key)).map(pageRow),
65
+ });
66
+
67
+ function personalRows() {
68
+ return {
69
+ type: "list",
70
+ items: [
71
+ {
72
+ title: "用户画像",
73
+ subtitle: "写一段自我介绍(角色、背景、偏好),Agent 每轮对话都会自动带上。",
74
+ icon: "user",
75
+ action: { id: "open-profile", args: { k: "profile" } },
76
+ },
77
+ {
78
+ title: "规则",
79
+ subtitle: "给 Agent 立下必须遵守的规矩,随时增删,下一轮即生效。",
80
+ icon: "list",
81
+ action: { id: "open-profile", args: { k: "rules" } },
82
+ },
83
+ {
84
+ title: "Agent 眼中的你",
85
+ subtitle: "dream 巩固后自动维护的一份逐条事实清单;这里可以逐条改、加、归档。",
86
+ icon: "sparkle",
87
+ action: { id: "open-panel", args: { k: "facts" } },
88
+ },
89
+ {
90
+ title: "人格定义(beta)",
91
+ subtitle: "给这位 Agent 一张角色卡:名字、性格、回复要求。",
92
+ icon: "robot",
93
+ action: { id: "open-profile", args: { k: "persona" } },
94
+ },
95
+ ],
96
+ };
97
+ }
98
+
99
+ function settingsMenu(state) {
100
+ const group = FLAG_GROUPS.find((g) => g.key === state.flagGroup) ?? FLAG_GROUPS[0];
101
+ return [
102
+ heading("个性化"),
103
+ personalRows(),
104
+ heading("记忆引擎"),
105
+ {
106
+ type: "segmented",
107
+ label: "\u200b",
108
+ bind: "data.mode",
109
+ options: MODE_OPTIONS,
110
+ onChange: { id: "mode" },
111
+ },
112
+ caption(
113
+ "轻量模式只保留核心能力:记忆读写、自动注入、热记忆;巩固、实体抽取、语义搜索等全部关闭,适合低配机器或只想「记住偏好」的场景。标准模式开启全部功能。",
114
+ ),
115
+ rows(["choro"]),
116
+ heading("功能开关"),
117
+ select(
118
+ "分组",
119
+ "data.flagGroup",
120
+ FLAG_GROUPS.map((g) => ({ label: g.label, value: g.key })),
121
+ "switch-group",
122
+ ),
123
+ caption(group.note),
124
+ ...group.items.map(([key, label]) => toggle(label, flagPath(key), flagAction(key))),
125
+ // 枚举项不是开关:给一组下拉(目前只有地志注入档位,挂在工作流组下)。
126
+ ...ENUM_FLAGS.filter((flag) => group.key === "workflow").flatMap((flag) => [
127
+ select(flag.label, flagPath(flag.key), flag.options, flagAction(flag.key)),
128
+ caption(flag.hint),
129
+ ]),
130
+ // 「其它」组按 /features 的 effective 动态列出未收录的布尔旗标:视图结构
131
+ // 不能靠数据分支,所以用 list + 数据行(点一行翻转一侧)。
132
+ ...(group.key === "extra" ? [{ type: "list", items: b("data.extraRows") }] : []),
133
+ caption(b("data.flagsMeta")),
134
+ caption(
135
+ "控制插件在后台做哪些事。改动即时保存,重启 DSH 后生效;嵌套开关(如「质量过滤 · 启用」)在电脑端面板里按点号归类。",
136
+ ),
137
+ heading("搜索"),
138
+ rows(["vector"]),
139
+ heading("连接与安全"),
140
+ rows(["external"]),
141
+ heading("帮助"),
142
+ rows(["commands", "help"]),
143
+ divider(),
144
+ actions(act("刷新", "refresh", undefined, "refresh")),
145
+ ];
146
+ }
147
+
148
+ function workbenchPanel() {
149
+ const nodes = [
150
+ kv(b("data.wbKv")),
151
+ heading("已沉淀"),
152
+ { type: "list", items: b("data.wbDeposited") },
153
+ caption("沉淀 = 被 mneme 当成工作流或项目状态写下来的条目。"),
154
+ heading("归档"),
155
+ ];
156
+ nodes.push({ type: "list", items: b("data.wbArchived") });
157
+ nodes.push(caption("点按任意一条可以恢复它。"));
158
+ return nodes;
159
+ }
160
+
161
+ function commandsPanel(state) {
162
+ const nodes = [{ type: "list", items: b("data.cmdRows") }];
163
+ if (state.cmdCount === 0) nodes.push(empty("还没有自定义命令", "在下面填一条,之后就能随时叫它。", "command"));
164
+ nodes.push({
165
+ type: "form",
166
+ submit: { id: "cmd-add" },
167
+ submitLabel: "新增命令",
168
+ children: [
169
+ { type: "textField", label: "命令名", bind: "data.sCmdName", required: true, maxLength: 40 },
170
+ { type: "textField", label: "说明", bind: "data.sCmdDesc", maxLength: 120 },
171
+ { type: "textField", label: "指令正文", bind: "data.sCmdIns", multiline: true, maxLines: 4, maxLength: 2000 },
172
+ ],
173
+ });
174
+ if (state.cmdCount > 0) {
175
+ nodes.push(
176
+ select(
177
+ "要删除的命令",
178
+ "data.sCmdPick",
179
+ [
180
+ { label: "不删除", value: "" },
181
+ ...state.commands.map((cmd) => ({ label: String(cmd.name ?? cmd.id), value: String(cmd.id) })),
182
+ ],
183
+ "cmd-pick",
184
+ ),
185
+ );
186
+ if (state.sCmdPick !== "") {
187
+ nodes.push({ type: "confirm", id: "cmd-delete", confirmLabel: "删除命令", cancelLabel: "取消" });
188
+ }
189
+ }
190
+ return nodes;
191
+ }
192
+
193
+ function vectorPanel() {
194
+ return [
195
+ kv(b("data.vectorKv")),
196
+ toggle("启用向量检索", "data.sVecEnabled", "vector-toggle"),
197
+ {
198
+ type: "form",
199
+ submit: { id: "vector-save" },
200
+ submitLabel: "保存向量配置",
201
+ children: [
202
+ { type: "textField", label: "接口地址", bind: "data.sVecUrl", placeholder: "https://…/v1", maxLength: 200 },
203
+ {
204
+ type: "textField",
205
+ label: "模型",
206
+ bind: "data.sVecModel",
207
+ placeholder: "text-embedding-3-small",
208
+ maxLength: 80,
209
+ },
210
+ ],
211
+ },
212
+ actions(act("重建索引", "vector-reindex", undefined, "refresh")),
213
+ caption(b("data.vectorNote")),
214
+ ];
215
+ }
216
+
217
+ function externalPanel() {
218
+ return [
219
+ toggle("开启外部访问", "data.sExtEnabled", "external-toggle"),
220
+ {
221
+ type: "form",
222
+ submit: { id: "external-save" },
223
+ submitLabel: "保存外部访问",
224
+ children: [
225
+ { type: "textField", label: "端口", bind: "data.sExtPort", placeholder: "8790", maxLength: 5 },
226
+ { type: "textField", label: "监听地址", bind: "data.sExtHost", placeholder: "127.0.0.1", maxLength: 60 },
227
+ ],
228
+ },
229
+ caption("令牌由 mneme 自己管理,面板不读取也不显示明文;需要令牌时到电脑端的 mneme 设置里看。"),
230
+ ];
231
+ }
232
+
233
+ function helpPanel() {
234
+ return [
235
+ kv(b("data.helpKv")),
236
+ actions(act("检测新版本", "check-version", undefined, "download")),
237
+ caption(b("data.versionNote")),
238
+ ];
239
+ }
240
+
241
+ const PANEL_TITLES = {
242
+ facts: "Agent 眼中的你",
243
+ choro: "代码库地志",
244
+ workbench: "工作台",
245
+ commands: "自定义命令",
246
+ vector: "向量检索",
247
+ external: "外部访问",
248
+ help: "关于",
249
+ };
250
+
251
+ function factsPanel() {
252
+ // 结构必须与数据无关(宿主先给视图、后推数据;按数据分支的地方会在首屏定形)。
253
+ // 所以「编辑框 / 归档按钮」永远在树里,点的哪条事实由 data.factEditHint 说明、
254
+ // 内容由 data.sFactEdit 填。
255
+ return [
256
+ select(
257
+ "哪张卡",
258
+ "data.factsAgent",
259
+ b("data.factsCardOptions"),
260
+ "facts-agent",
261
+ ),
262
+ { type: "list", items: b("data.factsRows") },
263
+ { type: "textField", label: "修改这条事实", bind: "data.sFactEdit", multiline: true, maxLines: 3, maxLength: 300 },
264
+ text(b("data.factsEditHint"), { size: "caption" }),
265
+ actions(act("保存修改", "facts-save", undefined, "save")),
266
+ { type: "confirm", id: "facts-archive", confirmLabel: "归档这条", cancelLabel: "取消" },
267
+ {
268
+ type: "form",
269
+ submit: { id: "facts-add" },
270
+ submitLabel: "添加一条",
271
+ children: [
272
+ { type: "textField", label: "新事实(一句话)", bind: "data.sFactNew", required: true, maxLength: 200 },
273
+ ],
274
+ },
275
+ actions(act("立即更新", "facts-run", undefined, "sparkle")),
276
+ caption(b("data.factsRunText")),
277
+ caption("事实由模型在 dream 巩固后自己维护;手填的画像永远优先,事实不会覆盖它。"),
278
+ ];
279
+ }
280
+
281
+ function choroPanel(state) {
282
+ return [
283
+ select("仓库", "data.choroRepo", b("data.choroRepoOptions"), "choro-repo"),
284
+ { type: "list", items: b("data.choroRows") },
285
+ text(b("data.choroNote"), { size: "caption" }),
286
+ fold("模块页", [mono(b("data.choroPageText"))], false),
287
+ { type: "confirm", id: "choro-run", confirmLabel: "重建骨架", cancelLabel: "取消" },
288
+ caption("重建会把已富化的模块页退回骨架;语义段要命令行 --t1 重新生成。"),
289
+ ];
290
+ }
291
+
292
+ function panelBody(state) {
293
+ switch (state.sPanel) {
294
+ case "facts":
295
+ return factsPanel(state);
296
+ case "choro":
297
+ return choroPanel(state);
298
+ case "workbench":
299
+ return workbenchPanel();
300
+ case "commands":
301
+ return commandsPanel(state);
302
+ case "vector":
303
+ return vectorPanel();
304
+ case "external":
305
+ return externalPanel();
306
+ case "help":
307
+ return helpPanel();
308
+ default:
309
+ return [];
310
+ }
311
+ }
312
+
313
+ function panelPage(state) {
314
+ return [
315
+ actions(act("返回", "back", undefined, "reply"), act("刷新", "refresh", undefined, "refresh")),
316
+ heading(PANEL_TITLES[state.sPanel] ?? "设置"),
317
+ ...panelBody(state),
318
+ ];
319
+ }
320
+
321
+ function settingsBody(state) {
322
+ if (state.page === "profile") return profilePage(state);
323
+ if (state.page === "conflict") return conflictPage(state);
324
+ if (state.sPanel !== "menu") return panelPage(state);
325
+ return settingsMenu(state);
326
+ }
327
+
328
+ function statusBody(state) {
329
+ return [
330
+ {
331
+ type: "grid",
332
+ columns: 2,
333
+ children: [
334
+ metric("记忆总数", "data.totalText"),
335
+ metric("近 30 天注入", "data.injectedText"),
336
+ metric("注入命中率", "data.slotRateText", "good"),
337
+ metric("待确认冲突", "data.conflictsText", state.pendingConflicts > 0 ? "warn" : undefined),
338
+ ],
339
+ },
340
+ ...statusSection(state),
341
+ actions(act("刷新", "refresh", undefined, "refresh")),
342
+ ];
343
+ }
344
+
345
+ export function settingsTab(state) {
346
+ const view = SETTINGS_VIEWS.includes(state.settingsView) ? state.settingsView : "settings";
347
+ return [
348
+ {
349
+ type: "segmented",
350
+ label: "\u200b",
351
+ bind: "data.settingsView",
352
+ options: [
353
+ { label: "设置", value: "settings" },
354
+ { label: "状态", value: "status" },
355
+ ],
356
+ onChange: { id: "switch-settings-view" },
357
+ },
358
+ ...(view === "status" ? statusBody(state) : settingsBody(state)),
359
+ ];
360
+ }
361
+
362
+ export function profilePage(state) {
363
+ const nodes = [
364
+ actions(act("返回", "back", undefined, "reply"), act("刷新", "refresh", undefined, "refresh")),
365
+ {
366
+ type: "segmented",
367
+ label: "\u200b",
368
+ bind: "data.profileTab",
369
+ options: [
370
+ { label: "画像", value: "profile" },
371
+ { label: "规则", value: "rules" },
372
+ { label: "人格", value: "persona" },
373
+ ],
374
+ onChange: { id: "profile-tab" },
375
+ },
376
+ ];
377
+ if (state.profileTab === "persona") {
378
+ nodes.push(kv(b("data.previewItems")));
379
+ nodes.push(fold("人格设定", [mono(b("data.personaText"))], false));
380
+ return nodes;
381
+ }
382
+ nodes.push({
383
+ type: "textField",
384
+ label: state.profileTab === "rules" ? "规则(JSON 数组)" : "用户画像",
385
+ bind: "data.editText",
386
+ multiline: true,
387
+ maxLines: 8,
388
+ maxLength: 4000,
389
+ });
390
+ nodes.push(actions(act("保存", "profile-save", undefined, "save")));
391
+ if (state.profileTab === "profile") {
392
+ nodes.push(actions(act("从记忆提取草稿", "profile-extract", undefined, "sparkle")));
393
+ if (state.extractText !== "") nodes.push(text(b("data.extractText"), { size: "caption" }));
394
+ }
395
+ return nodes;
396
+ }
397
+
398
+ export { panelPage };
@@ -0,0 +1,51 @@
1
+ // 运行状态: the runtime picture mneme's own Status tab shows, grouped into
2
+ // collapsible blocks so the settings tab stays short.
3
+ import { act, actions, b, caption, chart, fold, heading, kv } from "./common.js";
4
+
5
+ export function statusSection(state) {
6
+ return [
7
+ fold("库内一览", [kv(b("data.statusKv"))], true),
8
+ fold("注入与召回", [kv(b("data.injectKv")), heading("常被召回"), { type: "list", items: b("data.topRows") }], true),
9
+ fold("注入预览", [caption(b("data.injectPreviewText"))], false),
10
+ fold("后台巩固", [kv(b("data.dreamKv")), { type: "list", items: b("data.runRows") }], false),
11
+ fold(
12
+ "模型用量",
13
+ [
14
+ {
15
+ type: "chart",
16
+ kind: "bar",
17
+ categories: b("data.tokenCategories"),
18
+ series: b("data.tokenSeries"),
19
+ unit: "次",
20
+ caption: "近 28 天",
21
+ },
22
+ kv(b("data.tokenKv")),
23
+ ],
24
+ false,
25
+ ),
26
+ fold(
27
+ "热度分布",
28
+ [
29
+ { type: "chart", kind: "bar", categories: ["热", "温", "冷"], series: b("data.heatSeries"), unit: "条" },
30
+ caption(b("data.heatNote")),
31
+ ],
32
+ false,
33
+ ),
34
+ fold("冲突队列", [actions(act("打开冲突队列", "open-conflict", undefined, "warning"))], false),
35
+ fold("工作动态", [actions(act("打开工作动态", "open-panel", { k: "workbench" }, "todo"))], false),
36
+ fold(
37
+ "维护",
38
+ [
39
+ actions(
40
+ act("检查可回收", "reclaim", undefined, "clear"),
41
+ act("真跑回收", "reclaim-prepare", undefined, "delete"),
42
+ ),
43
+ ...(state.reclaimConfirm === true
44
+ ? [{ type: "confirm", id: "reclaim-run", confirmLabel: "真跑回收", cancelLabel: "取消" }]
45
+ : []),
46
+ caption(b("data.reclaimText")),
47
+ ],
48
+ false,
49
+ ),
50
+ ];
51
+ }
package/lib/embedding.js CHANGED
@@ -265,6 +265,20 @@ export function createEmbedder({ store, settings, logger, vectorIndex }) {
265
265
  return null;
266
266
  },
267
267
 
268
+ // Same as embedSingle, but keeps the reason a vector did not come back.
269
+ // embedSingle() drops it on purpose — its callers only care whether a
270
+ // vector exists — but the boot backfill has to tell "this row is broken"
271
+ // from "the provider was unreachable", and a bare null erases that
272
+ // distinction. Callers feature-detect this method and fall back to
273
+ // embedSingle() so older adapters keep working unchanged.
274
+ async embedSingleDetailed(text) {
275
+ const cfg = settings.getVectorConfig();
276
+ if (!configComplete(cfg)) return { vector: null, reason: "config-incomplete" };
277
+ const out = await embedTextDetailed(cfg, text);
278
+ if (out.vector) _dimension = out.vector.length;
279
+ return out;
280
+ },
281
+
268
282
  /** Model fingerprint (model#hex), or undefined when not configured. */
269
283
  get modelHash() {
270
284
  const cfg = settings.getVectorConfig();
@@ -284,7 +298,7 @@ export function createEmbedder({ store, settings, logger, vectorIndex }) {
284
298
  */
285
299
  async reindexMissing(limit = 50) {
286
300
  const cfg = settings.getVectorConfig();
287
- if (!cfg?.enabled) return { indexed: 0, skipped: 0 };
301
+ if (!cfg?.enabled) return { indexed: 0, failed: 0, skipped: 0 };
288
302
  const model = modelHashOf(cfg.model);
289
303
  // Fall back to the plain selector for stores that predate the
290
304
  // provenance column (older hosts, test doubles).
@@ -292,17 +306,37 @@ export function createEmbedder({ store, settings, logger, vectorIndex }) {
292
306
  ? store.needsEmbeddingFor(model, limit)
293
307
  : store.needsEmbedding(limit);
294
308
  let indexed = 0;
309
+ let failed = 0;
295
310
  for (const row of rows) {
296
- const text = [row.title, row.content].filter(Boolean).join("\n");
297
- const vector = await embedText(cfg, text);
298
- if (vector) {
299
- store.setEmbedding(row.id, vector, model);
300
- _dimension = vector.length;
301
- indexed++;
311
+ try {
312
+ const text = [row.title, row.content].filter(Boolean).join("\n");
313
+ // Prefer the reason-preserving call for the same reason the boot
314
+ // backfill does: a bare null cannot tell "this row is unembeddable"
315
+ // from "the provider was unreachable", and the receipt ledger files
316
+ // both under one indistinguishable entry.
317
+ const detailed = typeof this.embedSingleDetailed === "function"
318
+ ? await this.embedSingleDetailed(text)
319
+ : null;
320
+ const vector = detailed ? detailed.vector : await embedText(cfg, text);
321
+ if (vector?.length) {
322
+ store.setEmbedding(row.id, vector, model);
323
+ _dimension = vector.length;
324
+ indexed++;
325
+ } else {
326
+ failed++;
327
+ }
328
+ } catch (error) {
329
+ // A throw used to abort the whole loop and surface as a rejected
330
+ // promise from the API route; one bad row must not stop the rest.
331
+ failed++;
332
+ logger?.warn?.(`[dsh-mneme] reindex embed failed for ${row.id}: ${String(error)}`);
302
333
  }
303
334
  }
304
335
  if (indexed > 0) markModel(cfg, _dimension || undefined);
305
- return { indexed, skipped: rows.length - indexed };
336
+ // #604: `skipped: rows.length - indexed` folded "tried and got nothing"
337
+ // into "never touched". Every selected row is attempted here, so nothing
338
+ // is skipped; what failed is reported as failed.
339
+ return { indexed, failed, skipped: 0 };
306
340
  }
307
341
  };
308
342
  }
@@ -1,4 +1,9 @@
1
1
  const VALID_TYPES = new Set(["person", "project", "concept", "technology", "organization"]);
2
+ // #619:affective_context **刻意不在**这张白名单里。它不是结构关系,是「某人
3
+ // 在某一次场合对某事的情绪」——判它需要 provenance(这条记忆来自哪个会话),
4
+ // 而实体抽取发生在写入当下、手里没有会话边界,判不了「一次场合」与「反复出现
5
+ // 的特质」。让它在这里也能写,就等于给 entity_relations 开第二个只带 {model}
6
+ // 的写入口,同一张表两种 schema。唯一的写入者是 sleep 的 affective 相位。
2
7
  const VALID_RELATIONS = new Set(["uses", "depends_on", "part_of", "related_to"]);
3
8
  const MAX_MEMORY_CHARS = 4000;
4
9
 
@@ -25,6 +30,8 @@ RULES:
25
30
 
26
31
  CONSTRAINTS:
27
32
  - "type" must be one of: person, project, concept, technology, organization. If unsure, use "concept".
33
+ - Relation "type" must be one of: uses, depends_on, part_of, related_to. If unsure, use related_to.
34
+ - Do NOT emit emotional or affective relations. An emotion belongs to one occasion and needs session provenance to judge it; that is recorded separately, not here.
28
35
  - "relations" from/to must reference entity names from the "entities" list.
29
36
  - Maximum ${maxEntities} entities. Maximum ${maxAttrs} attributes per entity. Truncate if exceeded.
30
37
  - Only include entity-related attributes. Ignore irrelevant miscellaneous details.
@@ -72,12 +79,19 @@ function sanitizeConfidence(conf) {
72
79
  return Math.min(1, Math.max(0, num));
73
80
  }
74
81
 
82
+ // #619:未知关系类型**不再静默降级成 related_to**。旧行为是「写入成功、不报错、
83
+ // 图上没有那条轴」——最难排查的一种失败:模型抽对了,库里却读不出它抽的是情感边
84
+ // 还是普通关联。现在返回 null,由调用方丢弃并 logger.warn 带原始类型名。
85
+ // 注意这与 sanitizeType(未知实体类型 → concept)刻意不同:实体类型是可容错的
86
+ // 粗分类,关系类型是语义轴本身,猜错一条边就是给图加错误事实。
75
87
  function sanitizeRelationType(type) {
76
- if (VALID_RELATIONS.has(type)) return type;
77
- return "related_to";
88
+ if (typeof type !== "string") return null;
89
+ const normalized = type.trim();
90
+ if (VALID_RELATIONS.has(normalized)) return normalized;
91
+ return null;
78
92
  }
79
93
 
80
- function sanitizeExtractedData(data, config) {
94
+ function sanitizeExtractedData(data, config, logger = null) {
81
95
  const maxEntities = config.entityExtractionMaxEntities ?? 10;
82
96
  const maxAttrs = config.entityExtractionMaxAttrs ?? 20;
83
97
 
@@ -115,19 +129,31 @@ function sanitizeExtractedData(data, config) {
115
129
  }
116
130
 
117
131
  const relations = [];
132
+ const rejectedRelationTypes = [];
118
133
  if (Array.isArray(data.relations)) {
119
134
  for (const rawRel of data.relations) {
120
135
  if (!rawRel || typeof rawRel.from !== "string" || typeof rawRel.to !== "string") continue;
121
136
  if (!seenNames.has(rawRel.from) || !seenNames.has(rawRel.to)) continue;
137
+ const relationType = sanitizeRelationType(rawRel.type);
138
+ if (!relationType) {
139
+ // #619:不再降级成 related_to——记下原始类型名,交给调用方 warn。
140
+ rejectedRelationTypes.push(String(rawRel.type));
141
+ continue;
142
+ }
122
143
  relations.push({
123
144
  from: rawRel.from,
124
145
  to: rawRel.to,
125
- type: sanitizeRelationType(rawRel.type)
146
+ type: relationType
126
147
  });
127
148
  }
128
149
  }
129
-
130
- return { entities, relations };
150
+ if (rejectedRelationTypes.length > 0) {
151
+ logger?.warn?.(
152
+ `[extractor] dropped ${rejectedRelationTypes.length} relation(s) with unknown type(s): ${[...new Set(rejectedRelationTypes)].join(", ")}`
153
+ );
154
+ }
155
+
156
+ return { entities, relations, rejectedRelationTypes };
131
157
  }
132
158
 
133
159
  async function resolveEntity(entity, store) {
@@ -178,7 +204,7 @@ export async function extractEntities(memory, { store, config, callLLM, logger }
178
204
  return { ok: false, error: "Failed to parse JSON from LLM response" };
179
205
  }
180
206
 
181
- const { entities, relations } = sanitizeExtractedData(rawData, config);
207
+ const { entities, relations, rejectedRelationTypes } = sanitizeExtractedData(rawData, config, logger);
182
208
 
183
209
  const resolvedEntities = [];
184
210
  const entityIdMap = new Map();
@@ -247,6 +273,9 @@ export async function extractEntities(memory, { store, config, callLLM, logger }
247
273
  entities: resolvedEntities,
248
274
  attrs,
249
275
  relations: savedRelations,
276
+ // #619:被丢弃的未知关系类型(含原始名字)。调用方据此判断「模型没抽」
277
+ // 与「抽了但类型不认识」——两者的运维动作完全不同。
278
+ rejectedRelationTypes,
250
279
  skipped: skipCount
251
280
  };
252
281
 
package/lib/hot-memory.js CHANGED
@@ -1,4 +1,6 @@
1
1
  import { safeSlice } from "./safe-slice.js";
2
+ // Issue #611:图片块改用占位文本,不再落进空串(见 roundsFromEvents 里的注释)。
3
+ import { contentToText } from "./attachment-text.js";
2
4
 
3
5
  // Session-scoped hot memory (v0.5.0 召回率优化 1.3): a short-term buffer of
4
6
  // the latest dialogue rounds, kept strictly apart from the long-term memory
@@ -58,7 +60,19 @@ export function createHotMemory({ maxRounds = 5, maxTokens = 2000 } = {}) {
58
60
  // 配对规则与 inject.js 原实现一致:user/message(source.kind 缺省或 "user")开启
59
61
  // 新一轮;下一条 assistant 起源的事件关闭该轮。tool/system 等携带正文的事件不得
60
62
  // 冒充模型回复;rewind 标记(source.kind = "dsh-rewind")不是用户提问。
61
- export function roundsFromEvents(events, maxRounds, visibleSeqs = null) {
63
+ //
64
+ // omitPendingRound(Issue #637):尾部 pending 轮是「正在组装的这一轮」——它的
65
+ // 提问已经以真实 user message 出现在 prompt 里,热记忆再渲染一份只有空 `A:` 的
66
+ // 行是纯冗余;更糟的是回答落地后这一行会从空 `A:` 抖成正文,而宿主对动态
67
+ // systemPrompt context 的契约是「文本变了就追加一份快照」(旧份永久留在会话
68
+ // 历史里),于是同一轮内注入块反复变体、旧快照越积越多(报告实测 4 轮 → 10 份
69
+ // runtime-context、75% 重复率,其中记忆与设置两段是恒定的,抖动全来自热记忆)。
70
+ // 只省略尾部这一条;循环中途被新 user/message 顶掉的历史 pending 是已被放弃的
71
+ // 提问,渲染成空 `A:` 是既有语义,不动。注入侧传 true:一轮内块文本恒定,回答
72
+ // 落地时块恰好变一次 = 每轮一份快照(该机制下的理论最小值)。默认 false = 既有
73
+ // 行为逐字节不变——后台渲染与直调方(含既有测试锁的「已完成日志渲染」形状)
74
+ // 照旧拿到 pending 轮。
75
+ export function roundsFromEvents(events, maxRounds, visibleSeqs = null, { omitPendingRound = false } = {}) {
62
76
  const rounds = [];
63
77
  let pendingQuery = null;
64
78
  const textOf = (event) => {
@@ -73,15 +87,12 @@ export function roundsFromEvents(events, maxRounds, visibleSeqs = null) {
73
87
  // reasoning 里出现 Go template / Vue / Handlebars / 正则 / 日志原文的概率远高于
74
88
  // 正文,把它带进 hot memory 会让 `{{...}}` 模板片段注入 prompt。旧的扁平形状
75
89
  // (字符串 part)照旧取用,无 type 字段的兜底视为正文。
76
- return parts
77
- .map((p) => {
78
- if (typeof p === "string") return p;
79
- if (p?.type && p.type !== "text") return "";
80
- return p?.text ?? "";
81
- })
82
- .filter(Boolean)
83
- .join("\n")
84
- .trim();
90
+ //
91
+ // Issue #611:图片 part 原先在这里也落进空串分支,于是「用户发了截图」这件事
92
+ // 在 hot memory 里完全消失。改走 attachment-text 的占位口径:`[图: 名称 宽x高]`。
93
+ // hot memory 每轮都进 prompt,占位刻意保持短——它不是描述,只是「这里有过一张
94
+ // 图」的坐标。
95
+ return contentToText(parts).trim();
85
96
  };
86
97
  for (const event of events) {
87
98
  // surface 投影:不在「模型可见面」上的事件(被 rewind 撤回、被 compact 替换)
@@ -103,6 +114,6 @@ export function roundsFromEvents(events, maxRounds, visibleSeqs = null) {
103
114
  rounds.push({ query: pendingQuery, response: safeSlice(body, 800) });
104
115
  pendingQuery = null;
105
116
  }
106
- if (pendingQuery) rounds.push({ query: pendingQuery, response: "" });
117
+ if (pendingQuery && !omitPendingRound) rounds.push({ query: pendingQuery, response: "" });
107
118
  return rounds.slice(-maxRounds);
108
119
  }