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

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 (543) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +830 -830
  3. package/assets/icon.svg +3 -3
  4. package/bin/cli.mjs +964 -964
  5. package/bin/dsh-mneme-choro.mjs +262 -262
  6. package/bin/dsh-mneme-mcp.mjs +510 -510
  7. package/bin/dsh-mneme-serve.mjs +151 -151
  8. package/cordis.patch.yml +21 -21
  9. package/docs/CONFIGURATION.md +407 -404
  10. package/docs/CONTINUITY.md +139 -139
  11. package/docs/DAEMON.md +57 -57
  12. package/docs/DIRECTORIES.md +114 -114
  13. package/docs/DISTILL.md +397 -371
  14. package/docs/DSH-GO.md +97 -97
  15. package/docs/ENTITIES.md +247 -247
  16. package/docs/FORGET.md +77 -77
  17. package/docs/KB.md +110 -110
  18. package/docs/LOCAL_MODEL.md +222 -222
  19. package/docs/SEMANTIC.md +256 -256
  20. package/docs/SHARE.md +552 -552
  21. package/docs/SLEEP.md +202 -202
  22. package/docs/STORAGE.md +122 -122
  23. package/docs/WORKFLOWS.md +366 -366
  24. package/lib/api-standalone.js +1007 -1007
  25. package/lib/api.js +3600 -3600
  26. package/lib/attachment-text.js +122 -122
  27. package/lib/bootstrap-http.js +105 -105
  28. package/lib/bootstrap.js +215 -215
  29. package/lib/candidates.js +454 -454
  30. package/lib/capabilities.js +819 -819
  31. package/lib/choro-command.js +111 -111
  32. package/lib/client.js +14420 -14322
  33. package/lib/commands.js +95 -95
  34. package/lib/config-keys.js +144 -144
  35. package/lib/config.js +1361 -1344
  36. package/lib/content-hash.js +54 -54
  37. package/lib/continuity.js +258 -258
  38. package/lib/date-hint.js +32 -0
  39. package/lib/diary/auto.js +190 -190
  40. package/lib/diary/compose.js +440 -440
  41. package/lib/diary/dir.js +59 -59
  42. package/lib/diary/export.js +97 -97
  43. package/lib/diary/index.js +26 -26
  44. package/lib/diary/library.js +189 -189
  45. package/lib/diary/qq-inbox.js +142 -142
  46. package/lib/diary/rollover.js +61 -61
  47. package/lib/diary/session-provider.js +122 -122
  48. package/lib/document-index.js +94 -94
  49. package/lib/document.js +322 -322
  50. package/lib/dream/clustering.js +123 -123
  51. package/lib/dream/decisions.js +945 -945
  52. package/lib/dream/narratives.js +44 -44
  53. package/lib/dream/sleep.js +2027 -2027
  54. package/lib/dream/triage.js +139 -139
  55. package/lib/dream.js +1879 -1879
  56. package/lib/dsh-go/host.js +162 -162
  57. package/lib/dsh-go/panel.js +2091 -2091
  58. package/lib/dsh-go/text.js +342 -342
  59. package/lib/dsh-go/views/common.js +68 -68
  60. package/lib/dsh-go/views/entity.js +48 -48
  61. package/lib/dsh-go/views/memory.js +198 -198
  62. package/lib/dsh-go/views/root.js +30 -30
  63. package/lib/dsh-go/views/settings.js +398 -398
  64. package/lib/dsh-go/views/status.js +51 -51
  65. package/lib/embedding.js +342 -342
  66. package/lib/entities/extractor.js +285 -285
  67. package/lib/fence.js +24 -24
  68. package/lib/graph/anchoring.js +184 -184
  69. package/lib/guide.js +74 -74
  70. package/lib/heat.js +144 -144
  71. package/lib/hot-memory.js +119 -119
  72. package/lib/image-describe.js +158 -0
  73. package/lib/index.js +852 -852
  74. package/lib/inject.js +656 -651
  75. package/lib/kb/index.js +309 -309
  76. package/lib/kb/routes.js +219 -219
  77. package/lib/kb/tools.js +148 -148
  78. package/lib/lang.js +607 -595
  79. package/lib/local-embedder.js +358 -358
  80. package/lib/maintenance.js +162 -162
  81. package/lib/memory-source.js +9 -9
  82. package/lib/mirror.js +330 -330
  83. package/lib/organize.js +456 -456
  84. package/lib/peak-hours.js +172 -172
  85. package/lib/pipeline-activities.js +178 -178
  86. package/lib/pipeline-attribution.js +165 -165
  87. package/lib/pipeline-command.js +279 -279
  88. package/lib/pipeline-confirm.js +142 -142
  89. package/lib/pipeline-exec.js +174 -174
  90. package/lib/pipeline-nudge-auto.js +129 -129
  91. package/lib/pipeline-nudge.js +1623 -1623
  92. package/lib/pipeline-observations.js +181 -181
  93. package/lib/pipeline-observe.js +133 -133
  94. package/lib/pipeline-runner.js +361 -361
  95. package/lib/pipeline-runs.js +442 -442
  96. package/lib/pipeline.js +716 -716
  97. package/lib/placement.js +13 -13
  98. package/lib/pre-inject-gate.js +354 -354
  99. package/lib/profile-extract.js +164 -164
  100. package/lib/profile-facts.js +356 -356
  101. package/lib/provenance-http.js +251 -251
  102. package/lib/quality-filter.js +162 -162
  103. package/lib/recall-stats.js +190 -190
  104. package/lib/reclaim-http.js +150 -150
  105. package/lib/reranker.js +269 -269
  106. package/lib/runtime/adopt.js +176 -176
  107. package/lib/runtime/closure.js +172 -172
  108. package/lib/runtime/download.js +247 -247
  109. package/lib/runtime/layout.js +265 -265
  110. package/lib/runtime/loader.js +252 -252
  111. package/lib/runtime/model-fetch.js +361 -361
  112. package/lib/runtime/provision.js +237 -237
  113. package/lib/runtime/tarball.js +130 -130
  114. package/lib/runtime/verify.js +223 -223
  115. package/lib/safe-slice.js +18 -18
  116. package/lib/scope-state.js +277 -277
  117. package/lib/scope.js +187 -187
  118. package/lib/search/adaptive.js +60 -60
  119. package/lib/search/bm25.js +96 -96
  120. package/lib/semantic.js +572 -572
  121. package/lib/sensitive-scan.js +209 -209
  122. package/lib/serve.js +228 -228
  123. package/lib/service.js +3268 -3268
  124. package/lib/session-gate.js +91 -91
  125. package/lib/session-usage.js +350 -350
  126. package/lib/settings.js +827 -817
  127. package/lib/share-http.js +281 -281
  128. package/lib/share-import.js +234 -234
  129. package/lib/share.js +335 -335
  130. package/lib/store.js +5895 -5895
  131. package/lib/summarize.js +1388 -1343
  132. package/lib/tools.js +1370 -1356
  133. package/lib/user-settings.js +230 -230
  134. package/lib/vector-index.js +180 -180
  135. package/lib/version-check.js +122 -122
  136. package/lib/wiki/claims.js +267 -267
  137. package/lib/wiki/cli-hint.js +90 -90
  138. package/lib/wiki/inject.js +164 -164
  139. package/lib/wiki/partition.js +200 -200
  140. package/lib/wiki/plan.js +214 -214
  141. package/lib/wiki/queries.js +640 -640
  142. package/lib/wiki/render.js +261 -261
  143. package/lib/wiki/scan.js +371 -371
  144. package/lib/wiki/t0.js +500 -500
  145. package/lib/wiki/t1-jobs.js +232 -232
  146. package/lib/wiki/t1.js +609 -609
  147. package/lib/write-admission.js +404 -404
  148. package/locale/en.json +6 -6
  149. package/locale/zh.json +6 -6
  150. package/package.json +1 -1
  151. package/runtime-manifest.json +526 -526
  152. package/scripts/benchmark-embed.js +201 -201
  153. package/scripts/benchmark-recall.js +249 -249
  154. package/scripts/benchmark-rerank.js +166 -166
  155. package/scripts/build-runtime-manifest.mjs +224 -224
  156. package/scripts/changelog-prep.mjs +91 -91
  157. package/scripts/check-sync.js +44 -44
  158. package/scripts/e2e-dsh.js +220 -220
  159. package/scripts/mneme-runtime.mjs +277 -277
  160. package/scripts/publish-gate.mjs +197 -197
  161. package/scripts/release-docs.mjs +210 -210
  162. package/scripts/release-notes.mjs +164 -164
  163. package/scripts/stress-dsh.js +255 -255
  164. package/scripts/sync-lib.js +47 -47
  165. package/scripts/sync-test-badge.mjs +69 -69
  166. package/scripts/test-count-sync.mjs +48 -48
  167. package/src/api-standalone.js +1007 -1007
  168. package/src/api.js +3600 -3600
  169. package/src/attachment-text.js +122 -122
  170. package/src/bootstrap-http.js +105 -105
  171. package/src/bootstrap.js +215 -215
  172. package/src/candidates.js +454 -454
  173. package/src/capabilities.js +819 -819
  174. package/src/choro-command.js +111 -111
  175. package/src/commands.js +95 -95
  176. package/src/config-keys.js +144 -144
  177. package/src/config.js +1361 -1344
  178. package/src/content-hash.js +54 -54
  179. package/src/continuity.js +258 -258
  180. package/src/date-hint.js +32 -0
  181. package/src/diary/auto.js +190 -190
  182. package/src/diary/compose.js +440 -440
  183. package/src/diary/dir.js +59 -59
  184. package/src/diary/export.js +97 -97
  185. package/src/diary/index.js +26 -26
  186. package/src/diary/library.js +189 -189
  187. package/src/diary/qq-inbox.js +142 -142
  188. package/src/diary/rollover.js +61 -61
  189. package/src/diary/session-provider.js +122 -122
  190. package/src/document-index.js +94 -94
  191. package/src/document.js +322 -322
  192. package/src/dream/clustering.js +123 -123
  193. package/src/dream/decisions.js +945 -945
  194. package/src/dream/narratives.js +44 -44
  195. package/src/dream/sleep.js +2027 -2027
  196. package/src/dream/triage.js +139 -139
  197. package/src/dream.js +1879 -1879
  198. package/src/dsh-go/host.js +162 -162
  199. package/src/dsh-go/panel.js +2091 -2091
  200. package/src/dsh-go/text.js +342 -342
  201. package/src/dsh-go/views/common.js +68 -68
  202. package/src/dsh-go/views/entity.js +48 -48
  203. package/src/dsh-go/views/memory.js +198 -198
  204. package/src/dsh-go/views/root.js +30 -30
  205. package/src/dsh-go/views/settings.js +398 -398
  206. package/src/dsh-go/views/status.js +51 -51
  207. package/src/embedding.js +342 -342
  208. package/src/entities/extractor.js +285 -285
  209. package/src/fence.js +24 -24
  210. package/src/graph/anchoring.js +184 -184
  211. package/src/guide.js +74 -74
  212. package/src/heat.js +144 -144
  213. package/src/hot-memory.js +119 -119
  214. package/src/image-describe.js +158 -0
  215. package/src/index.js +852 -852
  216. package/src/inject.js +656 -651
  217. package/src/kb/index.js +309 -309
  218. package/src/kb/routes.js +219 -219
  219. package/src/kb/tools.js +148 -148
  220. package/src/lang.js +607 -595
  221. package/src/local-embedder.js +358 -358
  222. package/src/maintenance.js +162 -162
  223. package/src/memory-source.js +9 -9
  224. package/src/mirror.js +330 -330
  225. package/src/organize.js +456 -456
  226. package/src/peak-hours.js +172 -172
  227. package/src/pipeline-activities.js +178 -178
  228. package/src/pipeline-attribution.js +165 -165
  229. package/src/pipeline-command.js +279 -279
  230. package/src/pipeline-confirm.js +142 -142
  231. package/src/pipeline-exec.js +174 -174
  232. package/src/pipeline-nudge-auto.js +129 -129
  233. package/src/pipeline-nudge.js +1623 -1623
  234. package/src/pipeline-observations.js +181 -181
  235. package/src/pipeline-observe.js +133 -133
  236. package/src/pipeline-runner.js +361 -361
  237. package/src/pipeline-runs.js +442 -442
  238. package/src/pipeline.js +716 -716
  239. package/src/placement.js +13 -13
  240. package/src/pre-inject-gate.js +354 -354
  241. package/src/profile-extract.js +164 -164
  242. package/src/profile-facts.js +356 -356
  243. package/src/provenance-http.js +251 -251
  244. package/src/quality-filter.js +162 -162
  245. package/src/recall-stats.js +190 -190
  246. package/src/reclaim-http.js +150 -150
  247. package/src/reranker.js +269 -269
  248. package/src/runtime/adopt.js +176 -176
  249. package/src/runtime/closure.js +172 -172
  250. package/src/runtime/download.js +247 -247
  251. package/src/runtime/layout.js +265 -265
  252. package/src/runtime/loader.js +252 -252
  253. package/src/runtime/model-fetch.js +361 -361
  254. package/src/runtime/provision.js +237 -237
  255. package/src/runtime/tarball.js +130 -130
  256. package/src/runtime/verify.js +223 -223
  257. package/src/safe-slice.js +18 -18
  258. package/src/scope-state.js +277 -277
  259. package/src/scope.js +187 -187
  260. package/src/search/adaptive.js +60 -60
  261. package/src/search/bm25.js +96 -96
  262. package/src/semantic.js +572 -572
  263. package/src/sensitive-scan.js +209 -209
  264. package/src/serve.js +228 -228
  265. package/src/service.js +3268 -3268
  266. package/src/session-gate.js +91 -91
  267. package/src/session-usage.js +350 -350
  268. package/src/settings.js +827 -817
  269. package/src/share-http.js +281 -281
  270. package/src/share-import.js +234 -234
  271. package/src/share.js +335 -335
  272. package/src/store.js +5895 -5895
  273. package/src/summarize.js +1388 -1343
  274. package/src/tools.js +1370 -1356
  275. package/src/user-settings.js +230 -230
  276. package/src/vector-index.js +180 -180
  277. package/src/version-check.js +122 -122
  278. package/src/wiki/claims.js +267 -267
  279. package/src/wiki/cli-hint.js +90 -90
  280. package/src/wiki/inject.js +164 -164
  281. package/src/wiki/partition.js +200 -200
  282. package/src/wiki/plan.js +214 -214
  283. package/src/wiki/queries.js +640 -640
  284. package/src/wiki/render.js +261 -261
  285. package/src/wiki/scan.js +371 -371
  286. package/src/wiki/t0.js +500 -500
  287. package/src/wiki/t1-jobs.js +232 -232
  288. package/src/wiki/t1.js +609 -609
  289. package/src/write-admission.js +404 -404
  290. package/test/adaptive-inject-budget.test.js +85 -85
  291. package/test/api-hardening.test.js +173 -173
  292. package/test/api-nudge-route.test.js +259 -259
  293. package/test/api-routes-gaps.test.js +1092 -1092
  294. package/test/api.test.js +3007 -2998
  295. package/test/attachment-text.test.js +139 -139
  296. package/test/audit.test.js +448 -448
  297. package/test/baseline-persist.test.js +248 -248
  298. package/test/benchmark.test.js +94 -94
  299. package/test/bm25-corpus-cache.test.js +145 -145
  300. package/test/bootstrap.test.js +228 -228
  301. package/test/candidates.test.js +345 -345
  302. package/test/capabilities.test.js +577 -577
  303. package/test/capacity-warning.test.js +262 -262
  304. package/test/changelog-prep.test.js +125 -125
  305. package/test/choro-command.test.js +218 -218
  306. package/test/cli-enums.test.js +37 -37
  307. package/test/cli-flag-value.test.js +86 -86
  308. package/test/cli-receive.test.js +390 -390
  309. package/test/cli-share.test.js +437 -437
  310. package/test/client-share.test.js +225 -225
  311. package/test/client.test.js +4595 -4425
  312. package/test/clustering.test.js +100 -100
  313. package/test/commands.test.js +106 -106
  314. package/test/config-keys-routes.test.js +233 -233
  315. package/test/config.test.js +153 -153
  316. package/test/conflict-freeze.test.js +344 -344
  317. package/test/conflict-ledger-enforce.test.js +452 -452
  318. package/test/conflict-ledger.test.js +242 -242
  319. package/test/conflict-reviewed.test.js +79 -79
  320. package/test/content-hash.test.js +204 -204
  321. package/test/continuity.test.js +452 -452
  322. package/test/contrast.test.js +432 -432
  323. package/test/date-hint.test.js +156 -0
  324. package/test/dedupe-candidate-window.test.js +180 -180
  325. package/test/dependency-overrides.test.js +78 -78
  326. package/test/diary-library.test.js +187 -187
  327. package/test/diary.test.js +576 -576
  328. package/test/differentiate-feedback.test.js +248 -248
  329. package/test/distill-tx-embed-gap.test.js +110 -110
  330. package/test/docs-config-keys.test.js +65 -65
  331. package/test/document-dir.test.js +233 -233
  332. package/test/document.test.js +703 -703
  333. package/test/dream-archive-guardrail.test.js +116 -116
  334. package/test/dream-baseline-clamp.test.js +174 -174
  335. package/test/dream-candidate-hybrid.test.js +281 -281
  336. package/test/dream-coverage-degrade.test.js +86 -86
  337. package/test/dream-decision-evidence.test.js +314 -314
  338. package/test/dream-failure-backoff.test.js +173 -173
  339. package/test/dream-fence.test.js +207 -207
  340. package/test/dream-maxtokens-fallback.test.js +132 -132
  341. package/test/dream-merge-guardrail.test.js +219 -219
  342. package/test/dream-peak-hours.test.js +194 -194
  343. package/test/dream.test.js +1528 -1528
  344. package/test/dsh-go-host.test.js +134 -134
  345. package/test/dsh-go-panel-actions.test.js +854 -854
  346. package/test/dsh-go-panel.test.js +251 -251
  347. package/test/embed-ready-poll.test.js +94 -94
  348. package/test/embed-text-reasons.test.js +112 -112
  349. package/test/embedding-cache-f32.test.js +276 -276
  350. package/test/embedding-ready.test.js +226 -226
  351. package/test/entities.test.js +711 -711
  352. package/test/entity-recall.test.js +179 -179
  353. package/test/epistemic.test.js +325 -325
  354. package/test/failure-query.test.js +147 -147
  355. package/test/failure-retention.test.js +105 -105
  356. package/test/fnew-0112.test.js +311 -311
  357. package/test/fnew-03.test.js +422 -422
  358. package/test/forget-corrections.test.js +70 -70
  359. package/test/graph-anchoring.test.js +184 -184
  360. package/test/graph-api.test.js +333 -333
  361. package/test/graph-boosted-relations.test.js +189 -189
  362. package/test/graph-inject-budget.test.js +103 -103
  363. package/test/graph-multipath.test.js +269 -269
  364. package/test/graph-passive-confirm.test.js +91 -91
  365. package/test/graph-weight-evolution.test.js +104 -104
  366. package/test/heat.test.js +175 -175
  367. package/test/helpers/dream-mock.js +86 -86
  368. package/test/helpers/peer-worker.mjs +25 -25
  369. package/test/helpers/runtime-source.js +60 -60
  370. package/test/helpers/tar-builder.js +80 -80
  371. package/test/helpers/write-admission-samples.js +312 -312
  372. package/test/hot-memory.test.js +259 -259
  373. package/test/image-description.test.js +271 -0
  374. package/test/image-memory.test.js +170 -170
  375. package/test/inject-content-cap.test.js +91 -91
  376. package/test/inject-first-round-bm25.test.js +89 -89
  377. package/test/inject-heat.test.js +77 -77
  378. package/test/inject-parent-gate.test.js +134 -109
  379. package/test/inject-pin-explicit.test.js +300 -300
  380. package/test/inject-pin-vector-crowd.test.js +225 -225
  381. package/test/inject-pools.test.js +195 -195
  382. package/test/inject-recall.test.js +172 -172
  383. package/test/inject-rotation.test.js +161 -161
  384. package/test/inject.test.js +553 -553
  385. package/test/issue-617-step1-query-and-fusion.test.js +361 -361
  386. package/test/kb-api-wiring.test.js +128 -128
  387. package/test/kb-collections.test.js +272 -272
  388. package/test/kb-routes.test.js +379 -379
  389. package/test/lang.test.js +98 -98
  390. package/test/lastrunat-persist.test.js +101 -101
  391. package/test/lib-smoke.test.js +116 -116
  392. package/test/llm-audit.test.js +815 -815
  393. package/test/llm-message-source.test.js +207 -207
  394. package/test/local-embedder.test.js +292 -292
  395. package/test/maintenance.test.js +501 -501
  396. package/test/mcp-stdio.test.js +385 -385
  397. package/test/mint-source.test.js +311 -311
  398. package/test/mirror-dirty.test.js +424 -424
  399. package/test/mirror-edit-digest.test.js +187 -187
  400. package/test/mirror-generation.test.js +499 -499
  401. package/test/mirror-types.test.js +208 -208
  402. package/test/mirror.test.js +249 -249
  403. package/test/narrative-bars.test.js +162 -162
  404. package/test/organize-tool.test.js +232 -232
  405. package/test/organize.test.js +418 -418
  406. package/test/panel-chart-scale.test.js +90 -90
  407. package/test/panel-tokens.test.js +245 -245
  408. package/test/peak-hours-observability.test.js +70 -70
  409. package/test/peer-blockers.test.js +193 -193
  410. package/test/persona-addressing.test.js +83 -83
  411. package/test/persona-api.test.js +172 -172
  412. package/test/persona-by-session.test.js +568 -568
  413. package/test/persona-flag.test.js +36 -36
  414. package/test/persona-inject.test.js +145 -145
  415. package/test/persona-settings.test.js +198 -198
  416. package/test/pipeline-activities.test.js +155 -155
  417. package/test/pipeline-attribution.test.js +175 -175
  418. package/test/pipeline-command.test.js +243 -243
  419. package/test/pipeline-confirm.test.js +148 -148
  420. package/test/pipeline-exec.test.js +172 -172
  421. package/test/pipeline-nudge-auto.test.js +248 -248
  422. package/test/pipeline-nudge.test.js +1278 -1278
  423. package/test/pipeline-observations.test.js +157 -157
  424. package/test/pipeline-observe.test.js +127 -127
  425. package/test/pipeline-r1r2.test.js +278 -278
  426. package/test/pipeline-runner.test.js +468 -468
  427. package/test/pipeline-runs.test.js +767 -767
  428. package/test/pipeline.test.js +636 -636
  429. package/test/placement.test.js +36 -36
  430. package/test/policy-epoch.test.js +259 -259
  431. package/test/pre-inject-gate.test.js +444 -444
  432. package/test/profile-extract.test.js +269 -269
  433. package/test/profile-facts.test.js +492 -492
  434. package/test/prompt-stance.test.js +128 -128
  435. package/test/provenance-http.test.js +367 -367
  436. package/test/publish-gate.test.js +272 -272
  437. package/test/quality-backfill.test.js +290 -290
  438. package/test/quality-filter.test.js +295 -295
  439. package/test/readme-claims.test.js +113 -113
  440. package/test/reasoning-effort.test.js +752 -752
  441. package/test/recall-anti-update.test.js +82 -82
  442. package/test/recall-evals.test.js +253 -253
  443. package/test/recall-layer.test.js +326 -326
  444. package/test/recall-retention.test.js +128 -128
  445. package/test/recall-stats.test.js +274 -274
  446. package/test/receipt-chain.test.js +451 -451
  447. package/test/reclaim-http.test.js +209 -209
  448. package/test/reflection-update-gate.test.js +129 -129
  449. package/test/reflection.test.js +226 -226
  450. package/test/reindex-backfill.test.js +475 -475
  451. package/test/reindex-scheduler.test.js +369 -369
  452. package/test/release-docs.test.js +156 -156
  453. package/test/release-notes.test.js +265 -265
  454. package/test/release-workflow.test.js +93 -93
  455. package/test/reranker-single-logit.test.js +73 -73
  456. package/test/reranker.test.js +197 -197
  457. package/test/resident-status.test.js +83 -83
  458. package/test/route-policy.test.js +113 -113
  459. package/test/runtime-adopt.test.js +206 -206
  460. package/test/runtime-cli.test.js +89 -89
  461. package/test/runtime-closure.test.js +193 -193
  462. package/test/runtime-download.test.js +302 -302
  463. package/test/runtime-layout.test.js +163 -163
  464. package/test/runtime-loader.test.js +255 -255
  465. package/test/runtime-manifest.test.js +159 -159
  466. package/test/runtime-model-fetch.test.js +348 -348
  467. package/test/runtime-provision.test.js +101 -101
  468. package/test/runtime-tarball.test.js +81 -81
  469. package/test/runtime-verify.test.js +319 -319
  470. package/test/scope-candidates.test.js +491 -491
  471. package/test/scope-distill-stamp.test.js +134 -134
  472. package/test/scope-explicit.test.js +452 -452
  473. package/test/scope-inject-soft.test.js +106 -106
  474. package/test/scope-optional-service.test.js +110 -110
  475. package/test/scope-retrieval.test.js +197 -197
  476. package/test/scope-state.test.js +610 -610
  477. package/test/scope-storage.test.js +255 -255
  478. package/test/scope-strict.test.js +367 -367
  479. package/test/search-fusion.test.js +90 -90
  480. package/test/security-doc.test.js +188 -188
  481. package/test/semantic.test.js +124 -124
  482. package/test/sensitive-scan.test.js +251 -251
  483. package/test/serve-bin.test.js +121 -121
  484. package/test/serve.test.js +258 -258
  485. package/test/service-search.test.js +174 -174
  486. package/test/service.test.js +410 -410
  487. package/test/session-gate.test.js +264 -264
  488. package/test/session-usage.test.js +317 -317
  489. package/test/settings.test.js +273 -273
  490. package/test/share-foreign-rows.test.js +144 -144
  491. package/test/share-http.test.js +144 -144
  492. package/test/share-import.test.js +577 -577
  493. package/test/share-imported-guard.test.js +311 -311
  494. package/test/share-imported-identity.test.js +111 -111
  495. package/test/share-pack.test.js +511 -511
  496. package/test/share-receive-http.test.js +756 -756
  497. package/test/share-routes.test.js +337 -337
  498. package/test/share-store.test.js +133 -133
  499. package/test/share-validate.test.js +101 -101
  500. package/test/skip-invalid-restore.test.js +262 -262
  501. package/test/sleep-action-set.test.js +441 -441
  502. package/test/sleep-catchup.test.js +315 -315
  503. package/test/sleep-failure-backoff.test.js +156 -156
  504. package/test/sleep-heat.test.js +174 -174
  505. package/test/sleep-scheduler-rearm.test.js +89 -89
  506. package/test/sleep.test.js +1455 -1455
  507. package/test/standalone-api.test.js +1344 -1344
  508. package/test/store-vector-cache.test.js +98 -98
  509. package/test/store-write-path-columns.test.js +191 -191
  510. package/test/store.test.js +744 -744
  511. package/test/stress.test.js +209 -209
  512. package/test/summarize-occurred-at.test.js +127 -127
  513. package/test/summarize-parse-diagnostics.test.js +75 -75
  514. package/test/summarize-reasoning-effort.test.js +189 -189
  515. package/test/summarize.test.js +2251 -2251
  516. package/test/supersede-state-tracking.test.js +394 -394
  517. package/test/test-count-sync.test.js +71 -71
  518. package/test/tool-state-ignore.test.js +65 -65
  519. package/test/tools-dto-schema-matrix.test.js +398 -398
  520. package/test/tools-memory-get-schema.test.js +65 -65
  521. package/test/tools.test.js +585 -585
  522. package/test/updated-at-semantics.test.js +113 -113
  523. package/test/user-settings-preview.test.js +127 -127
  524. package/test/user-settings-segments.test.js +527 -527
  525. package/test/vector-index.test.js +334 -334
  526. package/test/version-check.test.js +152 -152
  527. package/test/webServer-optional.test.js +82 -82
  528. package/test/wiki-api.test.js +734 -734
  529. package/test/wiki-claims.test.js +194 -194
  530. package/test/wiki-cli-hint.test.js +118 -118
  531. package/test/wiki-inject.test.js +265 -265
  532. package/test/wiki-partition.test.js +112 -112
  533. package/test/wiki-plan.test.js +80 -80
  534. package/test/wiki-provenance.test.js +202 -202
  535. package/test/wiki-queries.test.js +398 -398
  536. package/test/wiki-scan.test.js +208 -208
  537. package/test/wiki-store.test.js +190 -190
  538. package/test/wiki-t0.test.js +456 -456
  539. package/test/wiki-t1-jobs.test.js +125 -125
  540. package/test/wiki-t1.test.js +460 -460
  541. package/test/wiki-tools.test.js +176 -176
  542. package/test/workflow-guards.test.js +155 -155
  543. package/test/write-admission.test.js +668 -668
package/src/settings.js CHANGED
@@ -1,817 +1,827 @@
1
- // User-configurable settings: profile (user self-description), rules (behavior
2
- // rules the agent must follow), and custom slash commands. Stored in the same
3
- // SQLite database via dedicated tables, isolated from the memories store.
4
- import { randomUUID } from "node:crypto";
5
-
6
- const SCHEMA = `
7
- CREATE TABLE IF NOT EXISTS user_settings (
8
- key TEXT PRIMARY KEY,
9
- value TEXT NOT NULL
10
- );
11
- CREATE TABLE IF NOT EXISTS custom_commands (
12
- id TEXT PRIMARY KEY,
13
- name TEXT NOT NULL UNIQUE,
14
- description TEXT NOT NULL DEFAULT '',
15
- instruction TEXT NOT NULL,
16
- created_at TEXT NOT NULL,
17
- updated_at TEXT NOT NULL
18
- );
19
- `;
20
-
21
- // DSH command names must match this (lowercase, start with a letter).
22
- const COMMAND_NAME = /^[a-z][a-z0-9_-]*$/;
23
-
24
- /** Parse a JSON array out of a stored string, tolerant of corruption. */
25
- function parseList(raw) {
26
- try {
27
- const value = JSON.parse(raw);
28
- return Array.isArray(value) ? value : [];
29
- } catch {
30
- return [];
31
- }
32
- }
33
-
34
- // persona 对象的字段白名单:写入时只挑这些键,未知字段丢弃(设计稿①:字段从简,
35
- // beta 收到真实反馈再加;provenance 是 v1.5 协作式人格的预留位,v1 不写管道)。
36
- // 读侧损坏即回空对象——同一容错口径照 parseList(坏值不抛错、面板显示为空可重写)。
37
- // scenario / mesExample(#606):对标 Character Card V2 的同名字段,是「人格卡能装
38
- // 下完整角色」所需的最小补充。两者都可选、都不写时渲染零输出(老用户逐字节不变)。
39
- const PERSONA_FIELDS = ["name", "personality", "replyStyle", "scenario", "mesExample", "enabled", "provenance"];
40
-
41
- function parsePersona(raw) {
42
- try {
43
- const value = JSON.parse(raw);
44
- if (typeof value !== "object" || value === null || Array.isArray(value)) return {};
45
- const out = {};
46
- for (const key of PERSONA_FIELDS) {
47
- if (value[key] !== undefined) out[key] = value[key];
48
- }
49
- return out;
50
- } catch {
51
- return {};
52
- }
53
- }
54
-
55
- // 多卡模型(v1.2):存储形为 { default: <卡>, byAgent: { <agentId>: <卡>, … } }。
56
- // 兼容层:v1 存的是单对象(有 PERSONA_FIELDS 键、无 default/byAgent 键),读侧
57
- // 判形状——单对象原样落 default 位,旧数据零迁移。byAgent 的键是 agent 身份
58
- // id(注入时按会话身份寻址,取不到落 default)。default 卡不可删(兜底位)。
59
- //
60
- // 绑会话(v1.3,#606):加第三维 bySession: { <sessionId>: <卡> }。为什么需要它——
61
- // byAgent 的键来自 session.header.agentPreset,而 DS Bot 这类宿主把它写成常量
62
- // (所有 bot 会话都是 "standard",实测 43/50),按 bot 名字建卡永远不命中。会话 id
63
- // 才是那个「一个 bot 一张卡」真正能分开的键。bySession 同样是纯加法:没有这个键
64
- // 的老数据读成 {},注入文本逐字节不变。
65
- function parsePersonaStore(raw) {
66
- try {
67
- const value = JSON.parse(raw);
68
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
69
- return { default: {}, byAgent: {}, bySession: {} };
70
- }
71
- // 新形:含 default / byAgent / bySession 任一键 → 按多卡解析
72
- if ("default" in value || "byAgent" in value || "bySession" in value) {
73
- const byAgent = {};
74
- if (value.byAgent && typeof value.byAgent === "object" && !Array.isArray(value.byAgent)) {
75
- for (const [agentId, card] of Object.entries(value.byAgent)) {
76
- const parsed = parsePersona(JSON.stringify(card ?? {}));
77
- if (Object.keys(parsed).length) byAgent[String(agentId)] = parsed;
78
- }
79
- }
80
- const bySession = {};
81
- if (value.bySession && typeof value.bySession === "object" && !Array.isArray(value.bySession)) {
82
- for (const [sessionId, card] of Object.entries(value.bySession)) {
83
- const parsed = parsePersona(JSON.stringify(card ?? {}));
84
- if (Object.keys(parsed).length) bySession[String(sessionId)] = parsed;
85
- }
86
- }
87
- return { default: parsePersona(JSON.stringify(value.default ?? {})), byAgent, bySession };
88
- }
89
- // 旧形:v1 单对象 → 整体作 default(字段白名单在 parsePersona 内裁)
90
- return { default: parsePersona(raw), byAgent: {}, bySession: {} };
91
- } catch {
92
- return { default: {}, byAgent: {}, bySession: {} };
93
- }
94
- }
95
-
96
- function serializePersonaStore(store) {
97
- const out = {
98
- default: parsePersona(JSON.stringify(store?.default ?? {})),
99
- byAgent: {},
100
- bySession: {}
101
- };
102
- if (store?.byAgent && typeof store.byAgent === "object") {
103
- for (const [agentId, card] of Object.entries(store.byAgent)) {
104
- const parsed = parsePersona(JSON.stringify(card ?? {}));
105
- if (Object.keys(parsed).length) out.byAgent[String(agentId)] = parsed;
106
- }
107
- }
108
- if (store?.bySession && typeof store.bySession === "object") {
109
- for (const [sessionId, card] of Object.entries(store.bySession)) {
110
- const parsed = parsePersona(JSON.stringify(card ?? {}));
111
- if (Object.keys(parsed).length) out.bySession[String(sessionId)] = parsed;
112
- }
113
- }
114
- return JSON.stringify(out);
115
- }
116
-
117
- // --- feature flags(功能开关)白名单 -----------------------------------------
118
- // 面板可逐项开关的后端能力。设计约束:
119
- // 1. 键名与类型必须和 config.js schema 同名同型,这份白名单是唯一校验源
120
- // (api.js 复用它计算 effective),schema 增删能力键时要同步改这里。
121
- // 2. 持久化(kv "feature_flags")只落白名单键;读到未知键、类型损坏或越界的
122
- // 值一律丢弃而不是报错——kv 会残留旧版本写入的键,读路径必须向前兼容。
123
- // 3. 写入是逐键校验的合并写:未知键/类型/范围不符抛 TypeError(消息含键名,
124
- // 供 API 透传给前端定位),校验不通过不落库,坏值永远进不了 kv。
125
- const FEATURE_FLAG_BOOLEANS = [
126
- "autoInject",
127
- "autoSummarize",
128
- "hotMemoryEnabled",
129
- // Issue #34(v0.7.2 引入;v0.7.11 误删后随 #333 恢复):对话开始注入当前时间。
130
- // 顶层扁平键,随白名单回面板(FEATURE_GROUPS 的 group.core)。
131
- "injectTimePrefix",
132
- // Issue #239 第 5 项:注入条数的查询自适应(确定性强则收缩注入条数,默认关)。
133
- "injectUncertaintyAdaptive",
134
- // Issue #249:能力说明(工具描述判断指引 + order 150 总则段)。第二批起它是
135
- // 注入父开关 autoInject 的子项、默认开(父关时不生效,闸门见 config.js)。
136
- "injectGuidanceEnabled",
137
- // Issue #249 N3:压缩边缘双落点——上下文即将精简前落一条连续性提案 + 往
138
- // 序列末尾追加同一份快照。默认关(新注入表面,见 config.js),父开关 autoInject。
139
- "continuityRescueEnabled",
140
- "entityExtractionEnabled",
141
- // Issue #219:图召回轴——查询命中实体名时把挂联记忆并入检索融合池
142
- // (默认关;依赖实体抽取产出,lightMode 强制关闭)。
143
- "entityRecallEnabled",
144
- // Issue #24 块1:图谱锚定层级联——命中实体作种子、沿关系表向邻居扩散
145
- // (默认关;依赖实体抽取产出,lightMode 强制关闭)。
146
- "graphAnchoringEnabled",
147
- // Issue #24 块2:边权重演化总闸——关着时任何触达都不改边权(默认关;
148
- // 依赖实体抽取产出,lightMode 强制关闭)。块4 的被动确认是它底下的一个通道。
149
- "graphWeightEnabled",
150
- // Issue #24 块3:关联提示防幻觉——图召回线索行以「[检索线索]」标注进注入
151
- // (默认关=线索只参与排序不进块;依赖实体抽取产出,lightMode 强制关闭)。
152
- "graphInjectHint",
153
- // Issue #24 块4:被动确认通道——正常触达即对该记忆挂联的边 bump 一格
154
- // (默认关;需总闸 graphWeightEnabled 同开)。
155
- "graphPassiveConfirm",
156
- // Issue #164:叙述条——dream 期间按 tag 主题簇合成叙述落库(source=
157
- // narrative,evidence 回链簇内记忆;按需检索不常驻注入;默认关)。
158
- "dreamNarrativeEnabled",
159
- // Issue #230:document 型记忆——agent 产长文档的指针行(注册校验/摘要+
160
- // doc_path 落库/C2 去重/supersede 记账;全文归 agent;默认关,lightMode 强制关)。
161
- "documentMemoryEnabled",
162
- // 日记本:跨天惰性判定 + 启动补写,正文落 <memoryDir>/diaries/<项目键>/。
163
- // 默认关,进 LIGHT_MODE_OFF(要调模型,与低资源档互斥)。与 documentMemoryEnabled
164
- // 成对但不相依:关掉文档记忆只影响检索,正文照样写。
165
- "diaryEnabled",
166
- "codingRetrospect",
167
- "autoDream",
168
- // Issue #292:autoDream 连续失败退避(默认关)。基数是 dreamMinIntervalMinutes,
169
- // 有效间隔 = 基数 × 2^连续失败数(成功清零),封顶 30 分钟;面板可启停。
170
- "autoDreamFailureBackoff",
171
- // Issue #557:dream 审计行输入快照瘦身(默认关)。开启后 auto 轮只落骨架
172
- // (不含 content),organize 轮的重放载荷不受影响;面板可启停。
173
- "dreamInputSummaryEnabled",
174
- "sleepModeEnabled",
175
- // Issue #529:sleep 连续失败退避(默认关)。基数是 sleepMinIntervalHours,
176
- // 有效间隔 = 基数 × 2^连续失败数(成功清零),封顶 max(基数, 24h);面板可启停。
177
- "sleepFailureBackoff",
178
- // Issue #619:情感归因相位(默认关)。把「某人某场合的情绪」写成 occasion-bound
179
- // 的 affective_context 关系,只有跨够 sleepAffectiveTraitMinSessions 个独立会话
180
- // 才允许升成稳定特质。要调模型且依赖实体抽取,所以必须显式打开、可随时关掉;
181
- // 它是 sleepModeEnabled 的子相:父开关关着时整个 sleep 不跑。
182
- "sleepAffectiveEnabled",
183
- "heatEnabled",
184
- // issue #218 / E5:注入侧 heat 独立开关(默认关,老用户 injectHeatEnabled=true 回滚)。
185
- "injectHeatEnabled",
186
- "hybridInject",
187
- "selectiveInjectEnabled",
188
- "searchSemanticDedup",
189
- "rerankEnabled",
190
- "resilientModelDownload",
191
- "adaptiveThresholdEnabled",
192
- "reflectionUpdateEnabled",
193
- "reflectionFailureTracking",
194
- "bm25SearchEnabled",
195
- "conflictFreezeEnabled",
196
- // Issue #603 第 4 层:自动裁决档(默认关)。相似度极高的同 scope 重复对直接
197
- // 保留新条、归档旧条,不进人工队列;模糊地带照旧留给人。会归档记忆,所以必须
198
- // 显式打开,面板可启停。阈值见 FEATURE_FLAG_NUMBER_RANGES 的
199
- // conflictAutoResolveMinSimilarity。
200
- "conflictAutoResolveEnabled",
201
- // Issue #641(beta 第一阶段):conflictLedgerMode / conflictLedgerDupMinSimilarity
202
- // 不在本表——前者是枚举键(FEATURE_FLAG_ENUMS),后者是数值键
203
- // (FEATURE_FLAG_NUMBER_RANGES),flagKeysFromSpec 会把三张表合成一份白名单,
204
- // 在这里重复登记会让同键被收两次(config-keys 的覆盖完整性锁抓过)。
205
- "trustEpistemicWeighting",
206
- // Plan #2: attach per-source {keyword, vector, bm25, final} signals to each
207
- // search result for transparency/debugging. Default off, purely decorative.
208
- "signalTransparency",
209
- // Issue #89:宽容校验回归(默认开)+ 跨类型合并显式放宽(默认关)。
210
- "dreamSkipInvalid",
211
- "allowCrossTypeMerge",
212
- // Issue #339 / E8:guarded 类型 merge 护栏(默认开)——合并对象命中
213
- // 长保留类型的 merge 决策整条跳过。关掉 = guarded 类型的 merge 不再被跳过,
214
- // 「更精炼的摘要」重新成为约束失真的主通道(面板可关,供需要旧行为的库回退)。
215
- "dreamMergeGuard",
216
- // Issue #17(v0.8.0 A1):scope 隔离存储层总开关。开启后写入标注
217
- // agent_scope/workspace_scope、去重键扩展(含 sensitivity);检索侧过滤在
218
- // A2/A3。默认关=行为与 A1 前逐字节一致。
219
- "scopeEnabled",
220
- // Issue #17(v0.8.0 A3):strictScope 硬过滤——他 scope 完全不可见(关闭时
221
- // 为 A2 软隔离:降权保留可见)。
222
- "strictScope",
223
- // Issue #471:政策轴——作用域级可逆闸门(scopeState)。与上面两个键是两条不同的
224
- // 轴:那两个管「这条记忆属于谁」(事实轴),这两个管「这个作用域现在通不通」
225
- // (政策轴,按作用域整体按下、跨会话存续、松开即恢复)。两级语义同
226
- // writeAdmission/preInjectGate——enabled 跑判定(观察档,留审计行但照常注入)、
227
- // enforce 真拦(不注入);都默认关。点号键平铺存、合并时展开回 scopeState 对象。
228
- "scopeState.enabled",
229
- "scopeState.enforce",
230
- // 工具暴露开关(v0.8.5):记忆已每轮自动注入,memory_search/memory_archive
231
- // 在慢/轻量模型上是多余往返,面板可关(默认关=行为不变)。
232
- "disableMemorySearch",
233
- "disableMemoryArchive",
234
- // Issue #231 第二批:agent 主动整理(memory_organize 工具)的暴露闸。与上面两个
235
- // 反向——那两个藏默认在场的工具,本键开默认不在场的能力(默认关=工具根本不存在,
236
- // 行为逐字节不变)。面板可启停=线上回滚开关。
237
- "organizeEnabled",
238
- // chorography(仓库地志)的暴露闸:开默认不在场的三个只读工具 + 面板浏览页
239
- // (默认关=模型工具列表里根本没有它们,行为逐字节不变)。同 organizeEnabled
240
- // 的反向语义——见 config.js 的键注释。
241
- "chorographyEnabled",
242
- // 嵌套对象开关:config.js 里是 memoryQualityFilter / llmAudit 对象的 enabled
243
- // 子字段。kv 按点号键平铺存("memoryQualityFilter.enabled": false),index.js
244
- // 合并时展开回嵌套对象,api.js 的 effective 从对象子字段取值。
245
- "memoryQualityFilter.enabled",
246
- "llmAudit.enabled",
247
- // Issue #254:写入准入(第 1 级确定性拒绝)。两个键分层——enabled 跑判据、
248
- // enforce 真拦;都默认关。同上,点号键平铺存、合并时展开回
249
- // writeAdmission 对象。
250
- "writeAdmission.enabled",
251
- "writeAdmission.enforce",
252
- // Issue #380:注入前判定(preInjectGate)。两级语义同 writeAdmission——
253
- // enabled 跑判定(观察档)、enforce 缓存命中帧真滤除;都默认关。点号键平铺存、
254
- // 合并时展开回 preInjectGate 对象。
255
- "preInjectGate.enabled",
256
- "preInjectGate.enforce",
257
- // Issue #164 A2:写入边界的密钥 / PII 判据(src/sensitive-scan.js)。与上面两个
258
- // 键分层——本键决定「这类判据参不参与」(默认关),命中之后是仅告警还是真拦仍由
259
- // writeAdmission.enforce 决定(#164 口径:默认仅告警、拦截 opt-in)。
260
- "sensitiveScanEnabled",
261
- // Issue #399:蒸馏原文留存 + 记忆级溯源。enabled 决定蒸馏时是否把公开正文原样
262
- // 落盘(distill_records)并给记忆挂窗口链接(memory_provenance);默认关。
263
- // 点号键平铺存、合并时展开回 distillRecords 对象(同 llmAudit.enabled)。
264
- // retentionDays 是 int,走下面的 INT_RANGES 表,不进开关白名单。
265
- "distillRecords.enabled",
266
- // v0.9.0:M.O.(Modus Operandi,beta)客户端视图开关。客户端读 /features
267
- // 的 effective
268
- // 决定是否注册座位(会话头图标 / 浮层 / 右栏 / 对话行卡片),服务端只负责把
269
- // 这个键稳定暴露出来——键若不在 effective 里,客户端静默少键、一个座位都不注册。
270
- // 默认关(beta、opt-in);顶层扁平键,因此不需要 NESTED_FLAG_PATHS 条目。
271
- "actionPipelineEnabled",
272
- // v0.9.0 第二批:M.O. 执行记账(`/mo-run` 开窗 → 窗口内命令被记成一条 run)。
273
- // 与上面那个键分开:那个管「看图的座位」,这个管「往账本里写」。默认关(beta、
274
- // opt-in),顶层扁平键,不需要 NESTED_FLAG_PATHS 条目。
275
- "actionRunEnabled",
276
- // v0.9.0 第三批:M.O. 自动沉淀(会话跑完一轮 → 挖这个项目自己的会话日志 → 落提议)。
277
- // 与前两个分开:这个管「从日志里长出候选」,那个管记账、另一个管看图。默认关
278
- // (beta、opt-in);顶层扁平键,不需要 NESTED_FLAG_PATHS 条目。进 LIGHT_MODE_OFF。
279
- "actionNudgeEnabled",
280
- // 人格定义页(beta):注入器渲染人格段的顶层总闸(双闸门的 config 半边;对象内
281
- // 的 persona.enabled 是 per-agent 半边,存 user_settings 的 "persona" 键,见
282
- // getPersona/setPersona)。默认关(beta、opt-in,老用户行为逐字节不变);不进
283
- // LIGHT_MODE_OFF——人格段是用户自写的一小段轻量常驻文本,不引入任何重型后台
284
- // 路径,轻量档用户显式勾选仍然赢(同 actionPipelineEnabled 的定性)。顶层扁平
285
- // 键,不需要 NESTED_FLAG_PATHS 条目。
286
- "personaEnabled",
287
- // 人格寻址口径(上一条打开时才有意义):开 = 身份没建卡/卡被关时不注入人格,
288
- // default 只兜解析不到身份的会话。默认关(历史行为,升级零变化)。
289
- "personaStrictAddressing",
290
- // Issue #497:画像提取草稿(POST /profile/extract)。默认关——每次调用一次 LLM 且
291
- // 读上百行记忆;产物只回草稿,绝不写 user_settings。进 LIGHT_MODE_OFF(预设关)。
292
- "profileExtractEnabled",
293
- // 「Agent 眼中的你」自动事实(2026-11-06):dream 后逐卡维护 + 手动轮 + 注入段,
294
- // 事实住 profile_facts 独立三表,绝不写 user_settings(#497 口径不变)。默认关,
295
- // 进 LIGHT_MODE_OFF(自动维护轮与低资源档互斥)。顶层扁平键,与 config.js schema
296
- // 成对出现(计数锁 test/api.test.js 钉住)。
297
- "profileFactsEnabled",
298
- // #539:逐会话无痕默认关闭;子代理开关受总开关约束。手动 session id 名单留在配置,
299
- // 不进入只支持标量的 feature_flags 白名单。
300
- "sessionGateEnabled",
301
- "sessionGateSubagents",
302
- // 2026-11-08 kb-qoder parity(契约 §3.1):chorography 面板闭环的两个授权闸 +
303
- // kb 子系统总闸。三条都与 config.js schema 成对出现,计数锁 test/api.test.js 钉住。
304
- // 不进 LIGHT_MODE_OFF —— 它们是「谁按下了按钮」而不是「每轮付多少」,理由写在
305
- // config.js 那三个键的注释里。kbEnabled 本车道只登记、不接线(B 线做子系统)。
306
- "chorographyAutoRefresh",
307
- "chorographyT1FromPanel",
308
- "kbEnabled"
309
- ];
310
- // 整数开关的闭区间,与 config.js 里 z.natural().min().max() 对齐。
311
- const FEATURE_FLAG_INT_RANGES = {
312
- distillRateLimitIntervalMs: [0, 60000],
313
- distillRateLimitRetries: [0, 10],
314
- distillRateLimitBaseDelayMs: [100, 60000],
315
- distillMaxChars: [1000, 200000],
316
- codingBoostFactor: [1, 5],
317
- dreamMinIntervalMinutes: [0, 10080],
318
- // Issue #637:注入跨轮轮换的窗口(0 = 关,#205 既有语义;config.js 的
319
- // z.natural().min(0).max(20) 与此成对)。此前不在白名单——报告者想开只能写
320
- // 宿主 patch 文件(cordis.patch.yml),面板上根本看不见这个键的存在。
321
- injectRotationTurns: [0, 20],
322
- dreamMaxTokens: [256, 131072],
323
- // Issue #127:autoSummarize 节流三键(0 = 零行为变化,等同现状)。
324
- summarizeMinIntervalMinutes: [0, 10080],
325
- summarizeMaxEntriesPerRun: [0, 50],
326
- summarizeDedupeWindowHours: [0, 168],
327
- // Issue #239:蒸馏零 LLM 预判(窗口最小字符数)与每会话 run 预算(0 = 零行为变化)。
328
- summarizeMinWindowChars: [0, 100000],
329
- summarizeMaxRunsPerSession: [0, 1000],
330
- // Issue #493:连续解析失败几次后放弃当前窗口(0 = 关)。与 config.js 的
331
- // z.natural().min(0).max(10) 成对。
332
- summarizeParseFailureMaxRetries: [0, 10],
333
- // Issue #239 第 4 项:高峰顺延上限(分钟,0 = 不设上限)。
334
- summarizePeakMaxDeferMinutes: [0, 1440],
335
- // Issue #239 第 4 项镜像到巩固:同一口径(分钟,0 = 不设上限)。
336
- dreamPeakMaxDeferMinutes: [0, 1440],
337
- // v0.13 日记定时写:每隔 N 分钟唤醒一次跨天判定(0 = 关,只在 turn/end 与
338
- // 启动时判)。上限一天——判定单位是天,更长的间隔没有意义。
339
- diaryWriteEveryMinutes: [0, 1440],
340
- // Issue #125:hybrid 候选量上限(0 = 复用 dreamMaxSnapshotSize)。
341
- dreamCandidateMax: [0, 5000],
342
- // #135 成本审计派生:单轮 merge 决策上限(merge 是唯一回吐正文的动作,默认 40
343
- // = 实测分布上沿;与 config.js 的 z.natural().min(1).max(200) 对齐)。
344
- dreamMaxMergePerRun: [1, 200],
345
- // Issue #164①:注入单条正文截断上限(默认 300 = 既有行为)。
346
- injectContentMaxChars: [60, 4000],
347
- // Issue #164:叙述条成簇门槛(共享同一 tag 的记忆数下限)。
348
- dreamNarrativeMinCluster: [2, 20],
349
- // Issue #230:document 摘要行的注入预算(次优先档内最多几条指针行)。
350
- documentInjectBudget: [1, 5],
351
- // Issue #249 第一批:B1 pin 池(约束/偏好)的独立条数预算(0 = 关闭/现状)。
352
- pinnedInjectBudget: [0, 5],
353
- // Issue #257:sleep 冲突/模式阶段的 LLM 输出预算(原硬编码 2048,实测不足)。
354
- sleepMaxTokens: [256, 131072],
355
- // Issue #619:情感归因的两个闸——升特质所需的最小独立会话数(下界 2:N=1 就是
356
- // 单次场合,正是必须禁止升特质的那个情形),以及单轮可写的 affective_context
357
- // 关系条数上限(0 = 关闭关系通道)。闭区间与 config.js 的 z.natural() 对齐。
358
- sleepAffectiveTraitMinSessions: [2, 10],
359
- sleepAffectiveMaxRelationsPerRun: [0, 50],
360
- // Issue #529:sleep 冲突阶段的输入侧预算(此前完全没有——全部候选对一次进一个
361
- // prompt,实测 163,054 token 撞服务端 163,840 上限)。两个键都是 0 = 不设上限,
362
- // 与 config.js 的 z.natural().min(0).max() 成对。
363
- sleepConflictMaxPairs: [0, 500],
364
- sleepConflictMaxPairsPerRun: [0, 5000],
365
- // Issue #258:总览(dream_summarize)输入条数硬上限(0 = 不设上限)。
366
- dreamSummaryMaxInputs: [0, 100000],
367
- // Issue #24 块1:锚定种子上限与级联深度(闭区间与 config.js 的 z.natural() 对齐)。
368
- graphSeedCap: [1, 30],
369
- graphCascadeDepth: [1, 3],
370
- // Issue #24 块3:图线索行的独立注入预算(0 = 线索不进块)。
371
- graphInjectBudget: [0, 5],
372
- // Issue #399:蒸馏原文记录的保留天数(与 config.js 的 z.natural().min(1).max(3650)
373
- // 对齐;同 llmAudit.retentionDays,是 int 而非开关)。
374
- "distillRecords.retentionDays": [1, 3650],
375
- // #275 容量预警阈值(嵌套点号键,必须同时进 api.js 的 NESTED_FLAG_PATHS)。
376
- // 0 = 显式关掉那一条检查(阈值判级的启用条件写在 service.getCapacityHealth)。
377
- // 上限与 config.js 的 z.natural().min(0).max() 成对。
378
- "capacityWarning.dbSizeWarnMb": [0, 1048576],
379
- "capacityWarning.archivedRowsWarn": [0, 10000000]
380
- };
381
- // 浮点开关的闭区间(与 config.js 的 z.number().min().max() 对齐)。与整数开关
382
- // 分开:面板的整数控件要求 Number.isInteger,而余弦相似度阈值必须允许小数。
383
- const FEATURE_FLAG_NUMBER_RANGES = {
384
- // Issue #127:vector 去重档的并入阈值。
385
- summarizeDedupeMinSim: [0.5, 0.99],
386
- // Issue #125:hybrid 判"高相似"的阈值。
387
- dreamCandidateMinSim: [0.5, 0.99],
388
- // Issue #24 块2:单次有效触达的边权抬升幅度(与 config.js 的闭区间对齐)。
389
- graphWeightDelta: [0, 1],
390
- // Issue #603 第 4 层:自动裁决的相似度门槛。下界 0.85 与冲突检测阈值同源——
391
- // 低于它就不是「重复」而是「相似」,不该自动归档任何一条。
392
- conflictAutoResolveMinSimilarity: [0.85, 1],
393
- // Issue #641:T1(dup)档的相似度下限。检测阈值(normal 0.85)与自动档 autoMin
394
- // (0.95,#616 重测在现库覆盖为 0)之间的空带由它接管,下界与检测阈值同源。
395
- conflictLedgerDupMinSimilarity: [0.85, 1],
396
- // #275 容量预警的归档占比阈值(0 = 关):健康形态本身就落在 0.8~0.9,默认
397
- // 0.95 起才算「归档区吞掉活跃区」——0.9 会对着健康库天天报警(依据见 config.js
398
- // capacityWarning 的注释)。浮点键,同 conflictAutoResolveMinSimilarity 的缘由。
399
- "capacityWarning.archivedRatioWarn": [0, 1]
400
- };
401
- // 自由字符串开关(与 config.js 的 z.string() 同名同型):trim 后 ≤200 字符,
402
- // 空串合法(= 跟随主对话模型/默认路径,面板显示 placeholder)。
403
- const FEATURE_FLAG_STRINGS = [
404
- "dreamProvider",
405
- "dreamModel",
406
- // 睡眠侧专用路由(sleep.js 的 config-first 第三层):面板下拉随
407
- // /llm-providers 端点一起提供,留空 = 用巩固模型或当前模型。
408
- "sleepProvider",
409
- "sleepModel",
410
- // 实体抽取侧专用路由(issue #109):provider/model 显式指定,
411
- // 留空 = 用当前默认模型。
412
- "entityExtractionProvider",
413
- "entityExtractionModel",
414
- // Issue #258:总览(dream_summarize)专用路由,留空 = 用巩固模型。
415
- "dreamSummaryProvider",
416
- "dreamSummaryModel",
417
- "localEmbedModel",
418
- "ollamaModel",
419
- // Issue #239 第 4 项:高峰时段串("09:00-18:00",空串 = 关闭)。
420
- "summarizePeakHours",
421
- // Issue #239 第 4 项镜像到巩固:同一份时段语法,空串 = 关闭(行为与现状一致)。
422
- "dreamPeakHours"
423
- ];
424
- // URL 字符串开关:trim 后必须为空或合法 http/https URL(new URL() 校验协议,
425
- // 拒绝其余协议——这是 SSRF 防线的一部分)。
426
- const FEATURE_FLAG_URLS = ["ollamaBaseUrl"];
427
- // 枚举开关(与 config.js 的 z.union(z.const(...)) 对齐):仅允许列出的值。
428
- const FEATURE_FLAG_ENUMS = {
429
- embedProvider: ["openai", "local", "ollama"],
430
- // Plan #1: recall fusion recipe. blend = legacy (default); rrf / minmax are
431
- // rank/scale-aware alternatives selected by the panel.
432
- recallFusion: ["blend", "rrf", "minmax"],
433
- // 实体抽取思考强度(issue #109):与 dreamReasoningEffort 枚举对齐。
434
- entityExtractionReasoning: ["low", "medium", "high", "none"],
435
- // 蒸馏思考强度(issue #315):与 entityExtractionReasoning 枚举对齐,
436
- // 多一个 off(显式关思考,思考型模型蒸馏防推理烧预算)。
437
- summarizeReasoningEffort: ["off", "low", "medium", "high", "none"],
438
- // Issue #127:落库前去重档位(off 默认,等同现状)。
439
- summarizeDedupeMode: ["off", "title", "vector"],
440
- // Issue #126:sleep 冲突阶段的动作集(conflict 默认 = 现状;full = 六分支)。
441
- sleepActionSet: ["conflict", "full"],
442
- // Issue #125:dream 候选集构造方式(window 默认 = 现状;hybrid 并入向量组)。
443
- dreamCandidateMode: ["window", "hybrid"],
444
- // 本地嵌入池化:auto = 按模型族判定(BGE → cls),可显式覆盖为 cls / mean。
445
- localEmbedPooling: ["auto", "cls", "mean"],
446
- // M3 开局注入档位(spec §7):轻量档在消费侧压成 off,见 wiki/inject.js 的
447
- // resolveInjectTier——LIGHT_MODE_OFF 对平铺键一律赋 false,枚举值进去会静默失效。
448
- chorographyInject: ["off", "light", "full"],
449
- // Issue #641:冲突分诊账本档位。off(默认,零行为)/ shadow(只记账)/ enforce
450
- // (确定性处置:T1 信息量优先 supersede,T2-T4 记账不停车不送 LLM)。三个档位
451
- // 都有真实行为——「设了但没行为」的档位不进枚举(#616「假成功」教训)。
452
- conflictLedgerMode: ["off", "shadow", "enforce"]
453
- };
454
- const FEATURE_FLAG_STRING_MAX = 200;
455
-
456
- // 供 api.js 复用同一份白名单(effective 只在白名单键上计算)。
457
- export const FEATURE_FLAG_SPEC = {
458
- booleans: FEATURE_FLAG_BOOLEANS,
459
- ints: FEATURE_FLAG_INT_RANGES,
460
- numbers: FEATURE_FLAG_NUMBER_RANGES,
461
- strings: FEATURE_FLAG_STRINGS,
462
- urls: FEATURE_FLAG_URLS,
463
- enums: FEATURE_FLAG_ENUMS
464
- };
465
-
466
- /** ollamaBaseUrl 的协议白名单:只接受 http/https(SSRF 防线的一部分)。 */
467
- function isHttpUrl(value) {
468
- try {
469
- const protocol = new URL(value).protocol;
470
- return protocol === "http:" || protocol === "https:";
471
- } catch {
472
- return false;
473
- }
474
- }
475
-
476
- /** 校验单个开关值;不合法抛 TypeError(消息含键名)。 */
477
- function validateFlag(key, value) {
478
- if (FEATURE_FLAG_BOOLEANS.includes(key)) {
479
- if (typeof value !== "boolean") {
480
- throw new TypeError(`feature flag "${key}" must be a boolean`);
481
- }
482
- return value;
483
- }
484
- const range = FEATURE_FLAG_INT_RANGES[key];
485
- if (range) {
486
- const [min, max] = range;
487
- if (!Number.isInteger(value) || value < min || value > max) {
488
- throw new TypeError(`feature flag "${key}" must be an integer in [${min}, ${max}]`);
489
- }
490
- return value;
491
- }
492
- const numRange = FEATURE_FLAG_NUMBER_RANGES[key];
493
- if (numRange) {
494
- const [min, max] = numRange;
495
- if (typeof value !== "number" || !Number.isFinite(value) || value < min || value > max) {
496
- throw new TypeError(`feature flag "${key}" must be a number in [${min}, ${max}]`);
497
- }
498
- return value;
499
- }
500
- if (FEATURE_FLAG_STRINGS.includes(key)) {
501
- if (typeof value !== "string") {
502
- throw new TypeError(`feature flag "${key}" must be a string`);
503
- }
504
- const trimmed = value.trim();
505
- if (trimmed.length > FEATURE_FLAG_STRING_MAX) {
506
- throw new TypeError(`feature flag "${key}" must be at most ${FEATURE_FLAG_STRING_MAX} characters`);
507
- }
508
- return trimmed; // 空串合法 = 跟随默认
509
- }
510
- if (FEATURE_FLAG_URLS.includes(key)) {
511
- if (typeof value !== "string") {
512
- throw new TypeError(`feature flag "${key}" must be a string`);
513
- }
514
- const trimmed = value.trim();
515
- if (trimmed && !isHttpUrl(trimmed)) {
516
- throw new TypeError(`feature flag "${key}" must be empty or a valid http(s) URL`);
517
- }
518
- return trimmed; // 空串合法 = 跟随默认
519
- }
520
- const allowed = FEATURE_FLAG_ENUMS[key];
521
- if (allowed) {
522
- if (typeof value !== "string" || !allowed.includes(value)) {
523
- throw new TypeError(`feature flag "${key}" must be one of: ${allowed.join(", ")}`);
524
- }
525
- return value;
526
- }
527
- throw new TypeError(`unknown feature flag "${key}"`);
528
- }
529
-
530
- /** 清洗已存的 feature_flags 对象:只保留白名单键,类型/范围损坏的键丢弃。 */
531
- function sanitizeFlags(raw) {
532
- const out = {};
533
- if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return out;
534
- for (const key of FEATURE_FLAG_BOOLEANS) {
535
- if (typeof raw[key] === "boolean") out[key] = raw[key];
536
- }
537
- for (const [key, [min, max]] of Object.entries(FEATURE_FLAG_INT_RANGES)) {
538
- if (Number.isInteger(raw[key]) && raw[key] >= min && raw[key] <= max) out[key] = raw[key];
539
- }
540
- for (const [key, [min, max]] of Object.entries(FEATURE_FLAG_NUMBER_RANGES)) {
541
- if (typeof raw[key] === "number" && Number.isFinite(raw[key]) && raw[key] >= min && raw[key] <= max) {
542
- out[key] = raw[key];
543
- }
544
- }
545
- for (const key of FEATURE_FLAG_STRINGS) {
546
- if (typeof raw[key] === "string" && raw[key].trim().length <= FEATURE_FLAG_STRING_MAX) {
547
- out[key] = raw[key].trim();
548
- }
549
- }
550
- for (const key of FEATURE_FLAG_URLS) {
551
- if (typeof raw[key] === "string") {
552
- const trimmed = raw[key].trim();
553
- if (!trimmed || isHttpUrl(trimmed)) out[key] = trimmed;
554
- }
555
- }
556
- for (const [key, allowed] of Object.entries(FEATURE_FLAG_ENUMS)) {
557
- if (allowed.includes(raw[key])) out[key] = raw[key];
558
- }
559
- return out;
560
- }
561
-
562
- export function createSettings(db) {
563
- db.exec(SCHEMA);
564
-
565
- function getSetting(key) {
566
- const row = db.prepare("SELECT value FROM user_settings WHERE key = ?").get(key);
567
- return row?.value ?? undefined;
568
- }
569
-
570
- function setSetting(key, value) {
571
- db.prepare(
572
- `INSERT INTO user_settings (key, value) VALUES (?, ?)
573
- ON CONFLICT(key) DO UPDATE SET value = excluded.value`
574
- ).run(key, value);
575
- }
576
-
577
- function toCommand(row) {
578
- if (!row) return undefined;
579
- return {
580
- id: row.id,
581
- name: row.name,
582
- description: row.description,
583
- instruction: row.instruction,
584
- created_at: row.created_at,
585
- updated_at: row.updated_at
586
- };
587
- }
588
-
589
- return {
590
- /** The user's self-description (free text) or "" when unset. */
591
- getProfile() {
592
- return getSetting("profile") ?? "";
593
- },
594
- setProfile(text) {
595
- setSetting("profile", String(text ?? ""));
596
- },
597
-
598
- /** Behavior rules as an array of strings. */
599
- getRules() {
600
- return parseList(getSetting("rules") ?? "[]").filter((r) => typeof r === "string");
601
- },
602
- setRules(rules) {
603
- const list = Array.isArray(rules) ? rules.filter((r) => typeof r === "string") : [];
604
- setSetting("rules", JSON.stringify(list));
605
- },
606
-
607
- /**
608
- * Agent persona (name / personality / replyStyle + enabled gate), stored as
609
- * one JSON key. v1.2 多卡:存储形 { default, byAgent },旧 v1 单对象读作
610
- * default 兼容。getPersona 返回全量多卡形(面板与注入寻址都用它)。
611
- * 寻址与兜底口径不在这里——住 src/user-settings.js 的 resolvePersonaCard,
612
- * 注入与面板预览共用那一份;这里若再留一个 getPersonaFor,就是两张皮。
613
- */
614
- getPersona() {
615
- return parsePersonaStore(getSetting("persona") ?? "{}");
616
- },
617
- setPersona(input) {
618
- // 双形兼容:单对象形 = 只更新 default、byAgent/bySession 保留(旧面板/旧调用方
619
- // 合并语义);多卡形 = 全量替换。坏体(非对象)在此不落库不抛错,保值由调用方守卫。
620
- if (typeof input !== "object" || input === null || Array.isArray(input)) return;
621
- if ("default" in input || "byAgent" in input || "bySession" in input) {
622
- setSetting("persona", serializePersonaStore(input));
623
- return;
624
- }
625
- const current = parsePersonaStore(getSetting("persona") ?? "{}");
626
- current.default = parsePersona(JSON.stringify(input));
627
- setSetting("persona", serializePersonaStore(current));
628
- },
629
- /** Replace one byAgent card wholesale (panel save); default 不可经此改。 */
630
- setPersonaFor(agentId, card) {
631
- const id = typeof agentId === "string" && agentId.trim() ? agentId.trim() : "";
632
- if (!id || id === "default") return false;
633
- const store = parsePersonaStore(getSetting("persona") ?? "{}");
634
- const clean = parsePersona(JSON.stringify(
635
- typeof card === "object" && card !== null && !Array.isArray(card) ? card : {}
636
- ));
637
- if (Object.keys(clean).length) store.byAgent[id] = clean;
638
- else delete store.byAgent[id];
639
- setSetting("persona", serializePersonaStore(store));
640
- return true;
641
- },
642
- /** byAgent 键列表(面板卡列表用,default 不在其中)。 */
643
- listPersonas() {
644
- return Object.keys(parsePersonaStore(getSetting("persona") ?? "{}").byAgent);
645
- },
646
- /** 删一张 byAgent 卡;default 不可删(返回 false)。 */
647
- removePersona(agentId) {
648
- const id = typeof agentId === "string" && agentId.trim() ? agentId.trim() : "";
649
- if (!id || id === "default") return false;
650
- const store = parsePersonaStore(getSetting("persona") ?? "{}");
651
- if (!(id in store.byAgent)) return false;
652
- delete store.byAgent[id];
653
- setSetting("persona", serializePersonaStore(store));
654
- return true;
655
- },
656
-
657
- /**
658
- * 绑会话卡(#606):按 sessionId 存/删/列。形状与 setPersonaFor 一族逐一对应,
659
- * 只是换了一个维度——为什么需要第二维见 parsePersonaStore 的注释。
660
- * sessionId 为空或 "default" 拒绝(与 byAgent 同一条纪律:兜底位不可被顶替)。
661
- */
662
- setPersonaForSession(sessionId, card) {
663
- const id = typeof sessionId === "string" && sessionId.trim() ? sessionId.trim() : "";
664
- if (!id || id === "default") return false;
665
- const store = parsePersonaStore(getSetting("persona") ?? "{}");
666
- const clean = parsePersona(JSON.stringify(
667
- typeof card === "object" && card !== null && !Array.isArray(card) ? card : {}
668
- ));
669
- if (Object.keys(clean).length) store.bySession[id] = clean;
670
- else delete store.bySession[id];
671
- setSetting("persona", serializePersonaStore(store));
672
- return true;
673
- },
674
- /** bySession 键列表(面板「按会话建的卡」用,default 不在其中)。 */
675
- listPersonaSessions() {
676
- return Object.keys(parsePersonaStore(getSetting("persona") ?? "{}").bySession);
677
- },
678
- /** 删一张 bySession 卡;不存在返回 false。 */
679
- removePersonaForSession(sessionId) {
680
- const id = typeof sessionId === "string" && sessionId.trim() ? sessionId.trim() : "";
681
- if (!id || id === "default") return false;
682
- const store = parsePersonaStore(getSetting("persona") ?? "{}");
683
- if (!(id in store.bySession)) return false;
684
- delete store.bySession[id];
685
- setSetting("persona", serializePersonaStore(store));
686
- return true;
687
- },
688
-
689
- /** All custom commands, sorted by name. */
690
- listCommands() {
691
- const rows = db.prepare("SELECT * FROM custom_commands ORDER BY name ASC").all();
692
- return rows.map(toCommand);
693
- },
694
-
695
- /**
696
- * Add or replace a custom command by name.
697
- * @returns the stored command.
698
- * @throws when name is invalid or does not match DSH's command-name grammar.
699
- */
700
- addCommand({ name, description = "", instruction }) {
701
- const cmdName = String(name ?? "").trim();
702
- if (!COMMAND_NAME.test(cmdName)) {
703
- throw new Error(`invalid command name "${cmdName}": must match /^[a-z][a-z0-9_-]*$/`);
704
- }
705
- if (typeof instruction !== "string" || !instruction.trim()) {
706
- throw new Error("command instruction must be a non-empty string");
707
- }
708
- const now = new Date().toISOString();
709
- const existing = db.prepare("SELECT id FROM custom_commands WHERE name = ?").get(cmdName);
710
- if (existing) {
711
- db.prepare(
712
- "UPDATE custom_commands SET description = ?, instruction = ?, updated_at = ? WHERE id = ?"
713
- ).run(String(description ?? ""), instruction, now, existing.id);
714
- return toCommand(db.prepare("SELECT * FROM custom_commands WHERE id = ?").get(existing.id));
715
- }
716
- const id = randomUUID();
717
- db.prepare(
718
- `INSERT INTO custom_commands (id, name, description, instruction, created_at, updated_at)
719
- VALUES (?, ?, ?, ?, ?, ?)`
720
- ).run(id, cmdName, String(description ?? ""), instruction, now, now);
721
- return toCommand(db.prepare("SELECT * FROM custom_commands WHERE id = ?").get(id));
722
- },
723
-
724
- /** Remove a custom command by id; returns true when removed. */
725
- removeCommand(id) {
726
- const result = db.prepare("DELETE FROM custom_commands WHERE id = ?").run(id);
727
- return result.changes > 0;
728
- },
729
-
730
- /** Vector-search provider config (OpenAI-compatible embeddings endpoint). */
731
- getVectorConfig() {
732
- const raw = getSetting("vector");
733
- if (!raw) return undefined;
734
- try {
735
- const cfg = JSON.parse(raw);
736
- return typeof cfg === "object" && cfg !== null ? cfg : undefined;
737
- } catch {
738
- return undefined;
739
- }
740
- },
741
- setVectorConfig({ enabled, baseUrl, apiKey, model }) {
742
- const cfg = {
743
- enabled: enabled === true || enabled === 1,
744
- baseUrl: String(baseUrl ?? "").trim().replace(/\/+$/, ""),
745
- apiKey: String(apiKey ?? "").trim(),
746
- model: String(model ?? "").trim()
747
- };
748
- setSetting("vector", JSON.stringify(cfg));
749
- return cfg;
750
- },
751
-
752
- /**
753
- * Standalone external API settings (kv "external_api"): {enabled, port,
754
- * token}. The Bearer token is auto-generated on first boot and persisted
755
- * here. Partial writes preserve the keys they don't mention.
756
- */
757
- getExternalApi() {
758
- const raw = getSetting("external_api");
759
- if (!raw) return undefined;
760
- try {
761
- const cfg = JSON.parse(raw);
762
- return typeof cfg === "object" && cfg !== null ? cfg : undefined;
763
- } catch {
764
- return undefined;
765
- }
766
- },
767
- setExternalApi(patch = {}) {
768
- const prev = this.getExternalApi() ?? {};
769
- const port = Number(patch.port ?? prev.port);
770
- const host = typeof patch.host === "string" && patch.host.trim() ? patch.host.trim() : (prev.host ?? "127.0.0.1");
771
- const cfg = {
772
- enabled: patch.enabled !== undefined ? patch.enabled === true : prev.enabled === true,
773
- port: Number.isInteger(port) && port > 0 ? port : 8790,
774
- host,
775
- token: String(patch.token ?? prev.token ?? "")
776
- };
777
- setSetting("external_api", JSON.stringify(cfg));
778
- return cfg;
779
- },
780
-
781
- /**
782
- * Web panel mode (kv "panel_mode"): "light" (low-resource preset) or
783
- * "standard" (full feature set). Unset reads as "standard".
784
- */
785
- getPanelMode() {
786
- return getSetting("panel_mode") === "light" ? "light" : "standard";
787
- },
788
- setPanelMode(mode) {
789
- setSetting("panel_mode", mode === "light" ? "light" : "standard");
790
- },
791
-
792
- /**
793
- * Feature flags(kv "feature_flags"):面板对后端能力的显式覆盖。读取只
794
- * 返回白名单内的合法键(默认 {}),写入是逐键校验后的合并持久化。
795
- */
796
- getFeatureFlags() {
797
- const raw = getSetting("feature_flags");
798
- if (!raw) return {};
799
- try {
800
- return sanitizeFlags(JSON.parse(raw));
801
- } catch {
802
- return {};
803
- }
804
- },
805
- setFeatureFlags(patch) {
806
- if (typeof patch !== "object" || patch === null || Array.isArray(patch)) {
807
- throw new TypeError("feature flags patch must be a plain object");
808
- }
809
- const merged = this.getFeatureFlags();
810
- for (const [key, value] of Object.entries(patch)) {
811
- merged[key] = validateFlag(key, value);
812
- }
813
- setSetting("feature_flags", JSON.stringify(merged));
814
- return merged;
815
- }
816
- };
817
- }
1
+ // User-configurable settings: profile (user self-description), rules (behavior
2
+ // rules the agent must follow), and custom slash commands. Stored in the same
3
+ // SQLite database via dedicated tables, isolated from the memories store.
4
+ import { randomUUID } from "node:crypto";
5
+
6
+ const SCHEMA = `
7
+ CREATE TABLE IF NOT EXISTS user_settings (
8
+ key TEXT PRIMARY KEY,
9
+ value TEXT NOT NULL
10
+ );
11
+ CREATE TABLE IF NOT EXISTS custom_commands (
12
+ id TEXT PRIMARY KEY,
13
+ name TEXT NOT NULL UNIQUE,
14
+ description TEXT NOT NULL DEFAULT '',
15
+ instruction TEXT NOT NULL,
16
+ created_at TEXT NOT NULL,
17
+ updated_at TEXT NOT NULL
18
+ );
19
+ `;
20
+
21
+ // DSH command names must match this (lowercase, start with a letter).
22
+ const COMMAND_NAME = /^[a-z][a-z0-9_-]*$/;
23
+
24
+ /** Parse a JSON array out of a stored string, tolerant of corruption. */
25
+ function parseList(raw) {
26
+ try {
27
+ const value = JSON.parse(raw);
28
+ return Array.isArray(value) ? value : [];
29
+ } catch {
30
+ return [];
31
+ }
32
+ }
33
+
34
+ // persona 对象的字段白名单:写入时只挑这些键,未知字段丢弃(设计稿①:字段从简,
35
+ // beta 收到真实反馈再加;provenance 是 v1.5 协作式人格的预留位,v1 不写管道)。
36
+ // 读侧损坏即回空对象——同一容错口径照 parseList(坏值不抛错、面板显示为空可重写)。
37
+ // scenario / mesExample(#606):对标 Character Card V2 的同名字段,是「人格卡能装
38
+ // 下完整角色」所需的最小补充。两者都可选、都不写时渲染零输出(老用户逐字节不变)。
39
+ const PERSONA_FIELDS = ["name", "personality", "replyStyle", "scenario", "mesExample", "enabled", "provenance"];
40
+
41
+ function parsePersona(raw) {
42
+ try {
43
+ const value = JSON.parse(raw);
44
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return {};
45
+ const out = {};
46
+ for (const key of PERSONA_FIELDS) {
47
+ if (value[key] !== undefined) out[key] = value[key];
48
+ }
49
+ return out;
50
+ } catch {
51
+ return {};
52
+ }
53
+ }
54
+
55
+ // 多卡模型(v1.2):存储形为 { default: <卡>, byAgent: { <agentId>: <卡>, … } }。
56
+ // 兼容层:v1 存的是单对象(有 PERSONA_FIELDS 键、无 default/byAgent 键),读侧
57
+ // 判形状——单对象原样落 default 位,旧数据零迁移。byAgent 的键是 agent 身份
58
+ // id(注入时按会话身份寻址,取不到落 default)。default 卡不可删(兜底位)。
59
+ //
60
+ // 绑会话(v1.3,#606):加第三维 bySession: { <sessionId>: <卡> }。为什么需要它——
61
+ // byAgent 的键来自 session.header.agentPreset,而 DS Bot 这类宿主把它写成常量
62
+ // (所有 bot 会话都是 "standard",实测 43/50),按 bot 名字建卡永远不命中。会话 id
63
+ // 才是那个「一个 bot 一张卡」真正能分开的键。bySession 同样是纯加法:没有这个键
64
+ // 的老数据读成 {},注入文本逐字节不变。
65
+ function parsePersonaStore(raw) {
66
+ try {
67
+ const value = JSON.parse(raw);
68
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
69
+ return { default: {}, byAgent: {}, bySession: {} };
70
+ }
71
+ // 新形:含 default / byAgent / bySession 任一键 → 按多卡解析
72
+ if ("default" in value || "byAgent" in value || "bySession" in value) {
73
+ const byAgent = {};
74
+ if (value.byAgent && typeof value.byAgent === "object" && !Array.isArray(value.byAgent)) {
75
+ for (const [agentId, card] of Object.entries(value.byAgent)) {
76
+ const parsed = parsePersona(JSON.stringify(card ?? {}));
77
+ if (Object.keys(parsed).length) byAgent[String(agentId)] = parsed;
78
+ }
79
+ }
80
+ const bySession = {};
81
+ if (value.bySession && typeof value.bySession === "object" && !Array.isArray(value.bySession)) {
82
+ for (const [sessionId, card] of Object.entries(value.bySession)) {
83
+ const parsed = parsePersona(JSON.stringify(card ?? {}));
84
+ if (Object.keys(parsed).length) bySession[String(sessionId)] = parsed;
85
+ }
86
+ }
87
+ return { default: parsePersona(JSON.stringify(value.default ?? {})), byAgent, bySession };
88
+ }
89
+ // 旧形:v1 单对象 → 整体作 default(字段白名单在 parsePersona 内裁)
90
+ return { default: parsePersona(raw), byAgent: {}, bySession: {} };
91
+ } catch {
92
+ return { default: {}, byAgent: {}, bySession: {} };
93
+ }
94
+ }
95
+
96
+ function serializePersonaStore(store) {
97
+ const out = {
98
+ default: parsePersona(JSON.stringify(store?.default ?? {})),
99
+ byAgent: {},
100
+ bySession: {}
101
+ };
102
+ if (store?.byAgent && typeof store.byAgent === "object") {
103
+ for (const [agentId, card] of Object.entries(store.byAgent)) {
104
+ const parsed = parsePersona(JSON.stringify(card ?? {}));
105
+ if (Object.keys(parsed).length) out.byAgent[String(agentId)] = parsed;
106
+ }
107
+ }
108
+ if (store?.bySession && typeof store.bySession === "object") {
109
+ for (const [sessionId, card] of Object.entries(store.bySession)) {
110
+ const parsed = parsePersona(JSON.stringify(card ?? {}));
111
+ if (Object.keys(parsed).length) out.bySession[String(sessionId)] = parsed;
112
+ }
113
+ }
114
+ return JSON.stringify(out);
115
+ }
116
+
117
+ // --- feature flags(功能开关)白名单 -----------------------------------------
118
+ // 面板可逐项开关的后端能力。设计约束:
119
+ // 1. 键名与类型必须和 config.js schema 同名同型,这份白名单是唯一校验源
120
+ // (api.js 复用它计算 effective),schema 增删能力键时要同步改这里。
121
+ // 2. 持久化(kv "feature_flags")只落白名单键;读到未知键、类型损坏或越界的
122
+ // 值一律丢弃而不是报错——kv 会残留旧版本写入的键,读路径必须向前兼容。
123
+ // 3. 写入是逐键校验的合并写:未知键/类型/范围不符抛 TypeError(消息含键名,
124
+ // 供 API 透传给前端定位),校验不通过不落库,坏值永远进不了 kv。
125
+ const FEATURE_FLAG_BOOLEANS = [
126
+ "autoInject",
127
+ "autoSummarize",
128
+ "hotMemoryEnabled",
129
+ // Issue #34(v0.7.2 引入;v0.7.11 误删后随 #333 恢复):对话开始注入当前时间。
130
+ // 顶层扁平键,随白名单回面板(FEATURE_GROUPS 的 group.core)。
131
+ "injectTimePrefix",
132
+ // Issue #239 第 5 项:注入条数的查询自适应(确定性强则收缩注入条数,默认关)。
133
+ "injectUncertaintyAdaptive",
134
+ // Issue #249:能力说明(工具描述判断指引 + order 150 总则段)。第二批起它是
135
+ // 注入父开关 autoInject 的子项、默认开(父关时不生效,闸门见 config.js)。
136
+ "injectGuidanceEnabled",
137
+ // Issue #249 N3:压缩边缘双落点——上下文即将精简前落一条连续性提案 + 往
138
+ // 序列末尾追加同一份快照。默认关(新注入表面,见 config.js),父开关 autoInject。
139
+ "continuityRescueEnabled",
140
+ "entityExtractionEnabled",
141
+ // Issue #219:图召回轴——查询命中实体名时把挂联记忆并入检索融合池
142
+ // (默认关;依赖实体抽取产出,lightMode 强制关闭)。
143
+ "entityRecallEnabled",
144
+ // Issue #24 块1:图谱锚定层级联——命中实体作种子、沿关系表向邻居扩散
145
+ // (默认关;依赖实体抽取产出,lightMode 强制关闭)。
146
+ "graphAnchoringEnabled",
147
+ // Issue #24 块2:边权重演化总闸——关着时任何触达都不改边权(默认关;
148
+ // 依赖实体抽取产出,lightMode 强制关闭)。块4 的被动确认是它底下的一个通道。
149
+ "graphWeightEnabled",
150
+ // Issue #24 块3:关联提示防幻觉——图召回线索行以「[检索线索]」标注进注入
151
+ // (默认关=线索只参与排序不进块;依赖实体抽取产出,lightMode 强制关闭)。
152
+ "graphInjectHint",
153
+ // Issue #24 块4:被动确认通道——正常触达即对该记忆挂联的边 bump 一格
154
+ // (默认关;需总闸 graphWeightEnabled 同开)。
155
+ "graphPassiveConfirm",
156
+ // Issue #164:叙述条——dream 期间按 tag 主题簇合成叙述落库(source=
157
+ // narrative,evidence 回链簇内记忆;按需检索不常驻注入;默认关)。
158
+ "dreamNarrativeEnabled",
159
+ // Issue #230:document 型记忆——agent 产长文档的指针行(注册校验/摘要+
160
+ // doc_path 落库/C2 去重/supersede 记账;全文归 agent;默认关,lightMode 强制关)。
161
+ "documentMemoryEnabled",
162
+ // 日记本:跨天惰性判定 + 启动补写,正文落 <memoryDir>/diaries/<项目键>/。
163
+ // 默认关,进 LIGHT_MODE_OFF(要调模型,与低资源档互斥)。与 documentMemoryEnabled
164
+ // 成对但不相依:关掉文档记忆只影响检索,正文照样写。
165
+ "diaryEnabled",
166
+ "codingRetrospect",
167
+ "autoDream",
168
+ // Issue #292:autoDream 连续失败退避(默认关)。基数是 dreamMinIntervalMinutes,
169
+ // 有效间隔 = 基数 × 2^连续失败数(成功清零),封顶 30 分钟;面板可启停。
170
+ "autoDreamFailureBackoff",
171
+ // Issue #557:dream 审计行输入快照瘦身(默认关)。开启后 auto 轮只落骨架
172
+ // (不含 content),organize 轮的重放载荷不受影响;面板可启停。
173
+ "dreamInputSummaryEnabled",
174
+ "sleepModeEnabled",
175
+ // Issue #529:sleep 连续失败退避(默认关)。基数是 sleepMinIntervalHours,
176
+ // 有效间隔 = 基数 × 2^连续失败数(成功清零),封顶 max(基数, 24h);面板可启停。
177
+ "sleepFailureBackoff",
178
+ // Issue #619:情感归因相位(默认关)。把「某人某场合的情绪」写成 occasion-bound
179
+ // 的 affective_context 关系,只有跨够 sleepAffectiveTraitMinSessions 个独立会话
180
+ // 才允许升成稳定特质。要调模型且依赖实体抽取,所以必须显式打开、可随时关掉;
181
+ // 它是 sleepModeEnabled 的子相:父开关关着时整个 sleep 不跑。
182
+ "sleepAffectiveEnabled",
183
+ "heatEnabled",
184
+ // issue #218 / E5:注入侧 heat 独立开关(默认关,老用户 injectHeatEnabled=true 回滚)。
185
+ "injectHeatEnabled",
186
+ "hybridInject",
187
+ "selectiveInjectEnabled",
188
+ "searchSemanticDedup",
189
+ "rerankEnabled",
190
+ "resilientModelDownload",
191
+ "adaptiveThresholdEnabled",
192
+ "reflectionUpdateEnabled",
193
+ "reflectionFailureTracking",
194
+ "bm25SearchEnabled",
195
+ "conflictFreezeEnabled",
196
+ // Issue #603 第 4 层:自动裁决档(默认关)。相似度极高的同 scope 重复对直接
197
+ // 保留新条、归档旧条,不进人工队列;模糊地带照旧留给人。会归档记忆,所以必须
198
+ // 显式打开,面板可启停。阈值见 FEATURE_FLAG_NUMBER_RANGES 的
199
+ // conflictAutoResolveMinSimilarity。
200
+ "conflictAutoResolveEnabled",
201
+ // Issue #641(beta 第一阶段):conflictLedgerMode / conflictLedgerDupMinSimilarity
202
+ // 不在本表——前者是枚举键(FEATURE_FLAG_ENUMS),后者是数值键
203
+ // (FEATURE_FLAG_NUMBER_RANGES),flagKeysFromSpec 会把三张表合成一份白名单,
204
+ // 在这里重复登记会让同键被收两次(config-keys 的覆盖完整性锁抓过)。
205
+ "trustEpistemicWeighting",
206
+ // Plan #2: attach per-source {keyword, vector, bm25, final} signals to each
207
+ // search result for transparency/debugging. Default off, purely decorative.
208
+ "signalTransparency",
209
+ // Issue #89:宽容校验回归(默认开)+ 跨类型合并显式放宽(默认关)。
210
+ "dreamSkipInvalid",
211
+ "allowCrossTypeMerge",
212
+ // Issue #339 / E8:guarded 类型 merge 护栏(默认开)——合并对象命中
213
+ // 长保留类型的 merge 决策整条跳过。关掉 = guarded 类型的 merge 不再被跳过,
214
+ // 「更精炼的摘要」重新成为约束失真的主通道(面板可关,供需要旧行为的库回退)。
215
+ "dreamMergeGuard",
216
+ // Issue #17(v0.8.0 A1):scope 隔离存储层总开关。开启后写入标注
217
+ // agent_scope/workspace_scope、去重键扩展(含 sensitivity);检索侧过滤在
218
+ // A2/A3。默认关=行为与 A1 前逐字节一致。
219
+ "scopeEnabled",
220
+ // Issue #17(v0.8.0 A3):strictScope 硬过滤——他 scope 完全不可见(关闭时
221
+ // 为 A2 软隔离:降权保留可见)。
222
+ "strictScope",
223
+ // Issue #471:政策轴——作用域级可逆闸门(scopeState)。与上面两个键是两条不同的
224
+ // 轴:那两个管「这条记忆属于谁」(事实轴),这两个管「这个作用域现在通不通」
225
+ // (政策轴,按作用域整体按下、跨会话存续、松开即恢复)。两级语义同
226
+ // writeAdmission/preInjectGate——enabled 跑判定(观察档,留审计行但照常注入)、
227
+ // enforce 真拦(不注入);都默认关。点号键平铺存、合并时展开回 scopeState 对象。
228
+ "scopeState.enabled",
229
+ "scopeState.enforce",
230
+ // 工具暴露开关(v0.8.5):记忆已每轮自动注入,memory_search/memory_archive
231
+ // 在慢/轻量模型上是多余往返,面板可关(默认关=行为不变)。
232
+ "disableMemorySearch",
233
+ "disableMemoryArchive",
234
+ // Issue #231 第二批:agent 主动整理(memory_organize 工具)的暴露闸。与上面两个
235
+ // 反向——那两个藏默认在场的工具,本键开默认不在场的能力(默认关=工具根本不存在,
236
+ // 行为逐字节不变)。面板可启停=线上回滚开关。
237
+ "organizeEnabled",
238
+ // chorography(仓库地志)的暴露闸:开默认不在场的三个只读工具 + 面板浏览页
239
+ // (默认关=模型工具列表里根本没有它们,行为逐字节不变)。同 organizeEnabled
240
+ // 的反向语义——见 config.js 的键注释。
241
+ "chorographyEnabled",
242
+ // 嵌套对象开关:config.js 里是 memoryQualityFilter / llmAudit 对象的 enabled
243
+ // 子字段。kv 按点号键平铺存("memoryQualityFilter.enabled": false),index.js
244
+ // 合并时展开回嵌套对象,api.js 的 effective 从对象子字段取值。
245
+ "memoryQualityFilter.enabled",
246
+ "llmAudit.enabled",
247
+ // Issue #254:写入准入(第 1 级确定性拒绝)。两个键分层——enabled 跑判据、
248
+ // enforce 真拦;都默认关。同上,点号键平铺存、合并时展开回
249
+ // writeAdmission 对象。
250
+ "writeAdmission.enabled",
251
+ "writeAdmission.enforce",
252
+ // Issue #380:注入前判定(preInjectGate)。两级语义同 writeAdmission——
253
+ // enabled 跑判定(观察档)、enforce 缓存命中帧真滤除;都默认关。点号键平铺存、
254
+ // 合并时展开回 preInjectGate 对象。
255
+ "preInjectGate.enabled",
256
+ "preInjectGate.enforce",
257
+ // Issue #164 A2:写入边界的密钥 / PII 判据(src/sensitive-scan.js)。与上面两个
258
+ // 键分层——本键决定「这类判据参不参与」(默认关),命中之后是仅告警还是真拦仍由
259
+ // writeAdmission.enforce 决定(#164 口径:默认仅告警、拦截 opt-in)。
260
+ "sensitiveScanEnabled",
261
+ // Issue #399:蒸馏原文留存 + 记忆级溯源。enabled 决定蒸馏时是否把公开正文原样
262
+ // 落盘(distill_records)并给记忆挂窗口链接(memory_provenance);默认关。
263
+ // 点号键平铺存、合并时展开回 distillRecords 对象(同 llmAudit.enabled)。
264
+ // retentionDays 是 int,走下面的 INT_RANGES 表,不进开关白名单。
265
+ "distillRecords.enabled",
266
+ // v0.9.0:M.O.(Modus Operandi,beta)客户端视图开关。客户端读 /features
267
+ // 的 effective
268
+ // 决定是否注册座位(会话头图标 / 浮层 / 右栏 / 对话行卡片),服务端只负责把
269
+ // 这个键稳定暴露出来——键若不在 effective 里,客户端静默少键、一个座位都不注册。
270
+ // 默认关(beta、opt-in);顶层扁平键,因此不需要 NESTED_FLAG_PATHS 条目。
271
+ "actionPipelineEnabled",
272
+ // v0.9.0 第二批:M.O. 执行记账(`/mo-run` 开窗 → 窗口内命令被记成一条 run)。
273
+ // 与上面那个键分开:那个管「看图的座位」,这个管「往账本里写」。默认关(beta、
274
+ // opt-in),顶层扁平键,不需要 NESTED_FLAG_PATHS 条目。
275
+ "actionRunEnabled",
276
+ // v0.9.0 第三批:M.O. 自动沉淀(会话跑完一轮 → 挖这个项目自己的会话日志 → 落提议)。
277
+ // 与前两个分开:这个管「从日志里长出候选」,那个管记账、另一个管看图。默认关
278
+ // (beta、opt-in);顶层扁平键,不需要 NESTED_FLAG_PATHS 条目。进 LIGHT_MODE_OFF。
279
+ "actionNudgeEnabled",
280
+ // 人格定义页(beta):注入器渲染人格段的顶层总闸(双闸门的 config 半边;对象内
281
+ // 的 persona.enabled 是 per-agent 半边,存 user_settings 的 "persona" 键,见
282
+ // getPersona/setPersona)。默认关(beta、opt-in,老用户行为逐字节不变);不进
283
+ // LIGHT_MODE_OFF——人格段是用户自写的一小段轻量常驻文本,不引入任何重型后台
284
+ // 路径,轻量档用户显式勾选仍然赢(同 actionPipelineEnabled 的定性)。顶层扁平
285
+ // 键,不需要 NESTED_FLAG_PATHS 条目。
286
+ "personaEnabled",
287
+ // 人格寻址口径(上一条打开时才有意义):开 = 身份没建卡/卡被关时不注入人格,
288
+ // default 只兜解析不到身份的会话。默认关(历史行为,升级零变化)。
289
+ "personaStrictAddressing",
290
+ // Issue #497:画像提取草稿(POST /profile/extract)。默认关——每次调用一次 LLM 且
291
+ // 读上百行记忆;产物只回草稿,绝不写 user_settings。进 LIGHT_MODE_OFF(预设关)。
292
+ "profileExtractEnabled",
293
+ // 「Agent 眼中的你」自动事实(2026-11-06):dream 后逐卡维护 + 手动轮 + 注入段,
294
+ // 事实住 profile_facts 独立三表,绝不写 user_settings(#497 口径不变)。默认关,
295
+ // 进 LIGHT_MODE_OFF(自动维护轮与低资源档互斥)。顶层扁平键,与 config.js schema
296
+ // 成对出现(计数锁 test/api.test.js 钉住)。
297
+ "profileFactsEnabled",
298
+ // 图像描述档(docs/DISTILL.md:346):蒸馏前给窗口里的图出描述。默认关,进
299
+ // LIGHT_MODE_OFF —— 它是一次额外的视觉调用,与低资源档互斥。路由两个键走
300
+ // FEATURE_FLAG_STRINGS(留空 = 不描述,理由写在 config.js 那三个键的注释里)。
301
+ "imageDescribeEnabled",
302
+ // #539:逐会话无痕默认关闭;子代理开关受总开关约束。手动 session id 名单留在配置,
303
+ // 不进入只支持标量的 feature_flags 白名单。
304
+ "sessionGateEnabled",
305
+ "sessionGateSubagents",
306
+ // 2026-11-08 kb-qoder parity(契约 §3.1):chorography 面板闭环的两个授权闸 +
307
+ // kb 子系统总闸。三条都与 config.js schema 成对出现,计数锁 test/api.test.js 钉住。
308
+ // 不进 LIGHT_MODE_OFF —— 它们是「谁按下了按钮」而不是「每轮付多少」,理由写在
309
+ // config.js 那三个键的注释里。kbEnabled 本车道只登记、不接线(B 线做子系统)。
310
+ "chorographyAutoRefresh",
311
+ "chorographyT1FromPanel",
312
+ "kbEnabled"
313
+ ];
314
+ // 整数开关的闭区间,与 config.js 里 z.natural().min().max() 对齐。
315
+ const FEATURE_FLAG_INT_RANGES = {
316
+ distillRateLimitIntervalMs: [0, 60000],
317
+ distillRateLimitRetries: [0, 10],
318
+ distillRateLimitBaseDelayMs: [100, 60000],
319
+ distillMaxChars: [1000, 200000],
320
+ codingBoostFactor: [1, 5],
321
+ dreamMinIntervalMinutes: [0, 10080],
322
+ // Issue #637:注入跨轮轮换的窗口(0 = 关,#205 既有语义;config.js 的
323
+ // z.natural().min(0).max(20) 与此成对)。此前不在白名单——报告者想开只能写
324
+ // 宿主 patch 文件(cordis.patch.yml),面板上根本看不见这个键的存在。
325
+ injectRotationTurns: [0, 20],
326
+ dreamMaxTokens: [256, 131072],
327
+ // Issue #127:autoSummarize 节流三键(0 = 零行为变化,等同现状)。
328
+ summarizeMinIntervalMinutes: [0, 10080],
329
+ summarizeMaxEntriesPerRun: [0, 50],
330
+ summarizeDedupeWindowHours: [0, 168],
331
+ // Issue #239:蒸馏零 LLM 预判(窗口最小字符数)与每会话 run 预算(0 = 零行为变化)。
332
+ summarizeMinWindowChars: [0, 100000],
333
+ summarizeMaxRunsPerSession: [0, 1000],
334
+ // Issue #493:连续解析失败几次后放弃当前窗口(0 = 关)。与 config.js 的
335
+ // z.natural().min(0).max(10) 成对。
336
+ summarizeParseFailureMaxRetries: [0, 10],
337
+ // Issue #239 第 4 项:高峰顺延上限(分钟,0 = 不设上限)。
338
+ summarizePeakMaxDeferMinutes: [0, 1440],
339
+ // Issue #239 第 4 项镜像到巩固:同一口径(分钟,0 = 不设上限)。
340
+ dreamPeakMaxDeferMinutes: [0, 1440],
341
+ // v0.13 日记定时写:每隔 N 分钟唤醒一次跨天判定(0 = 关,只在 turn/end 与
342
+ // 启动时判)。上限一天——判定单位是天,更长的间隔没有意义。
343
+ diaryWriteEveryMinutes: [0, 1440],
344
+ // Issue #125:hybrid 候选量上限(0 = 复用 dreamMaxSnapshotSize)。
345
+ dreamCandidateMax: [0, 5000],
346
+ // #135 成本审计派生:单轮 merge 决策上限(merge 是唯一回吐正文的动作,默认 40
347
+ // = 实测分布上沿;与 config.js 的 z.natural().min(1).max(200) 对齐)。
348
+ dreamMaxMergePerRun: [1, 200],
349
+ // Issue #164①:注入单条正文截断上限(默认 300 = 既有行为)。
350
+ injectContentMaxChars: [60, 4000],
351
+ // Issue #164:叙述条成簇门槛(共享同一 tag 的记忆数下限)。
352
+ dreamNarrativeMinCluster: [2, 20],
353
+ // Issue #230:document 摘要行的注入预算(次优先档内最多几条指针行)。
354
+ documentInjectBudget: [1, 5],
355
+ // Issue #249 第一批:B1 pin 池(约束/偏好)的独立条数预算(0 = 关闭/现状)。
356
+ pinnedInjectBudget: [0, 5],
357
+ // Issue #257:sleep 冲突/模式阶段的 LLM 输出预算(原硬编码 2048,实测不足)。
358
+ sleepMaxTokens: [256, 131072],
359
+ // Issue #619:情感归因的两个闸——升特质所需的最小独立会话数(下界 2:N=1 就是
360
+ // 单次场合,正是必须禁止升特质的那个情形),以及单轮可写的 affective_context
361
+ // 关系条数上限(0 = 关闭关系通道)。闭区间与 config.js 的 z.natural() 对齐。
362
+ sleepAffectiveTraitMinSessions: [2, 10],
363
+ sleepAffectiveMaxRelationsPerRun: [0, 50],
364
+ // Issue #529:sleep 冲突阶段的输入侧预算(此前完全没有——全部候选对一次进一个
365
+ // prompt,实测 163,054 token 撞服务端 163,840 上限)。两个键都是 0 = 不设上限,
366
+ // 与 config.js 的 z.natural().min(0).max() 成对。
367
+ sleepConflictMaxPairs: [0, 500],
368
+ sleepConflictMaxPairsPerRun: [0, 5000],
369
+ // Issue #258:总览(dream_summarize)输入条数硬上限(0 = 不设上限)。
370
+ dreamSummaryMaxInputs: [0, 100000],
371
+ // Issue #24 块1:锚定种子上限与级联深度(闭区间与 config.js 的 z.natural() 对齐)。
372
+ graphSeedCap: [1, 30],
373
+ graphCascadeDepth: [1, 3],
374
+ // Issue #24 块3:图线索行的独立注入预算(0 = 线索不进块)。
375
+ graphInjectBudget: [0, 5],
376
+ // Issue #399:蒸馏原文记录的保留天数(与 config.js 的 z.natural().min(1).max(3650)
377
+ // 对齐;同 llmAudit.retentionDays,是 int 而非开关)。
378
+ "distillRecords.retentionDays": [1, 3650],
379
+ // #275 容量预警阈值(嵌套点号键,必须同时进 api.js 的 NESTED_FLAG_PATHS)。
380
+ // 0 = 显式关掉那一条检查(阈值判级的启用条件写在 service.getCapacityHealth)。
381
+ // 上限与 config.js 的 z.natural().min(0).max() 成对。
382
+ "capacityWarning.dbSizeWarnMb": [0, 1048576],
383
+ "capacityWarning.archivedRowsWarn": [0, 10000000]
384
+ };
385
+ // 浮点开关的闭区间(与 config.js 的 z.number().min().max() 对齐)。与整数开关
386
+ // 分开:面板的整数控件要求 Number.isInteger,而余弦相似度阈值必须允许小数。
387
+ const FEATURE_FLAG_NUMBER_RANGES = {
388
+ // Issue #127:vector 去重档的并入阈值。
389
+ summarizeDedupeMinSim: [0.5, 0.99],
390
+ // Issue #125:hybrid 判"高相似"的阈值。
391
+ dreamCandidateMinSim: [0.5, 0.99],
392
+ // Issue #24 块2:单次有效触达的边权抬升幅度(与 config.js 的闭区间对齐)。
393
+ graphWeightDelta: [0, 1],
394
+ // Issue #603 第 4 层:自动裁决的相似度门槛。下界 0.85 与冲突检测阈值同源——
395
+ // 低于它就不是「重复」而是「相似」,不该自动归档任何一条。
396
+ conflictAutoResolveMinSimilarity: [0.85, 1],
397
+ // Issue #641:T1(dup)档的相似度下限。检测阈值(normal 0.85)与自动档 autoMin
398
+ // (0.95,#616 重测在现库覆盖为 0)之间的空带由它接管,下界与检测阈值同源。
399
+ conflictLedgerDupMinSimilarity: [0.85, 1],
400
+ // #275 容量预警的归档占比阈值(0 = 关):健康形态本身就落在 0.8~0.9,默认
401
+ // 0.95 起才算「归档区吞掉活跃区」——0.9 会对着健康库天天报警(依据见 config.js
402
+ // capacityWarning 的注释)。浮点键,同 conflictAutoResolveMinSimilarity 的缘由。
403
+ "capacityWarning.archivedRatioWarn": [0, 1]
404
+ };
405
+ // 自由字符串开关(与 config.js 的 z.string() 同名同型):trim 后 ≤200 字符,
406
+ // 空串合法(= 跟随主对话模型/默认路径,面板显示 placeholder)。
407
+ const FEATURE_FLAG_STRINGS = [
408
+ "dreamProvider",
409
+ "dreamModel",
410
+ // 睡眠侧专用路由(sleep.js 的 config-first 第三层):面板下拉随
411
+ // /llm-providers 端点一起提供,留空 = 用巩固模型或当前模型。
412
+ "sleepProvider",
413
+ "sleepModel",
414
+ // 实体抽取侧专用路由(issue #109):provider/model 显式指定,
415
+ // 留空 = 用当前默认模型。
416
+ "entityExtractionProvider",
417
+ "entityExtractionModel",
418
+ // Issue #258:总览(dream_summarize)专用路由,留空 = 用巩固模型。
419
+ "dreamSummaryProvider",
420
+ "dreamSummaryModel",
421
+ "localEmbedModel",
422
+ "ollamaModel",
423
+ // Issue #239 第 4 项:高峰时段串("09:00-18:00",空串 = 关闭)。
424
+ "summarizePeakHours",
425
+ // Issue #239 第 4 项镜像到巩固:同一份时段语法,空串 = 关闭(行为与现状一致)。
426
+ "dreamPeakHours",
427
+ // 图像描述档专用路由(docs/DISTILL.md:346):与上面几条不同,这两条留空不是
428
+ // 「跟随当前模型」而是「整档不描述」——蒸馏路由与会话主模型都可能是纯文本
429
+ // 模型,跟着它们跑只会拿回一行 textOnly 占位。所以面板上它们是空串 + 说明,
430
+ // 不是下拉默认值。
431
+ "imageDescribeProvider",
432
+ "imageDescribeModel"
433
+ ];
434
+ // URL 字符串开关:trim 后必须为空或合法 http/https URL(new URL() 校验协议,
435
+ // 拒绝其余协议——这是 SSRF 防线的一部分)。
436
+ const FEATURE_FLAG_URLS = ["ollamaBaseUrl"];
437
+ // 枚举开关(与 config.js 的 z.union(z.const(...)) 对齐):仅允许列出的值。
438
+ const FEATURE_FLAG_ENUMS = {
439
+ embedProvider: ["openai", "local", "ollama"],
440
+ // Plan #1: recall fusion recipe. blend = legacy (default); rrf / minmax are
441
+ // rank/scale-aware alternatives selected by the panel.
442
+ recallFusion: ["blend", "rrf", "minmax"],
443
+ // 实体抽取思考强度(issue #109):与 dreamReasoningEffort 枚举对齐。
444
+ entityExtractionReasoning: ["low", "medium", "high", "none"],
445
+ // 蒸馏思考强度(issue #315):与 entityExtractionReasoning 枚举对齐,
446
+ // 多一个 off(显式关思考,思考型模型蒸馏防推理烧预算)。
447
+ summarizeReasoningEffort: ["off", "low", "medium", "high", "none"],
448
+ // Issue #127:落库前去重档位(off 默认,等同现状)。
449
+ summarizeDedupeMode: ["off", "title", "vector"],
450
+ // Issue #126:sleep 冲突阶段的动作集(conflict 默认 = 现状;full = 六分支)。
451
+ sleepActionSet: ["conflict", "full"],
452
+ // Issue #125:dream 候选集构造方式(window 默认 = 现状;hybrid 并入向量组)。
453
+ dreamCandidateMode: ["window", "hybrid"],
454
+ // 本地嵌入池化:auto = 按模型族判定(BGE → cls),可显式覆盖为 cls / mean。
455
+ localEmbedPooling: ["auto", "cls", "mean"],
456
+ // M3 开局注入档位(spec §7):轻量档在消费侧压成 off,见 wiki/inject.js 的
457
+ // resolveInjectTier——LIGHT_MODE_OFF 对平铺键一律赋 false,枚举值进去会静默失效。
458
+ chorographyInject: ["off", "light", "full"],
459
+ // Issue #641:冲突分诊账本档位。off(默认,零行为)/ shadow(只记账)/ enforce
460
+ // (确定性处置:T1 信息量优先 supersede,T2-T4 记账不停车不送 LLM)。三个档位
461
+ // 都有真实行为——「设了但没行为」的档位不进枚举(#616「假成功」教训)。
462
+ conflictLedgerMode: ["off", "shadow", "enforce"]
463
+ };
464
+ const FEATURE_FLAG_STRING_MAX = 200;
465
+
466
+ // 供 api.js 复用同一份白名单(effective 只在白名单键上计算)。
467
+ export const FEATURE_FLAG_SPEC = {
468
+ booleans: FEATURE_FLAG_BOOLEANS,
469
+ ints: FEATURE_FLAG_INT_RANGES,
470
+ numbers: FEATURE_FLAG_NUMBER_RANGES,
471
+ strings: FEATURE_FLAG_STRINGS,
472
+ urls: FEATURE_FLAG_URLS,
473
+ enums: FEATURE_FLAG_ENUMS
474
+ };
475
+
476
+ /** ollamaBaseUrl 的协议白名单:只接受 http/https(SSRF 防线的一部分)。 */
477
+ function isHttpUrl(value) {
478
+ try {
479
+ const protocol = new URL(value).protocol;
480
+ return protocol === "http:" || protocol === "https:";
481
+ } catch {
482
+ return false;
483
+ }
484
+ }
485
+
486
+ /** 校验单个开关值;不合法抛 TypeError(消息含键名)。 */
487
+ function validateFlag(key, value) {
488
+ if (FEATURE_FLAG_BOOLEANS.includes(key)) {
489
+ if (typeof value !== "boolean") {
490
+ throw new TypeError(`feature flag "${key}" must be a boolean`);
491
+ }
492
+ return value;
493
+ }
494
+ const range = FEATURE_FLAG_INT_RANGES[key];
495
+ if (range) {
496
+ const [min, max] = range;
497
+ if (!Number.isInteger(value) || value < min || value > max) {
498
+ throw new TypeError(`feature flag "${key}" must be an integer in [${min}, ${max}]`);
499
+ }
500
+ return value;
501
+ }
502
+ const numRange = FEATURE_FLAG_NUMBER_RANGES[key];
503
+ if (numRange) {
504
+ const [min, max] = numRange;
505
+ if (typeof value !== "number" || !Number.isFinite(value) || value < min || value > max) {
506
+ throw new TypeError(`feature flag "${key}" must be a number in [${min}, ${max}]`);
507
+ }
508
+ return value;
509
+ }
510
+ if (FEATURE_FLAG_STRINGS.includes(key)) {
511
+ if (typeof value !== "string") {
512
+ throw new TypeError(`feature flag "${key}" must be a string`);
513
+ }
514
+ const trimmed = value.trim();
515
+ if (trimmed.length > FEATURE_FLAG_STRING_MAX) {
516
+ throw new TypeError(`feature flag "${key}" must be at most ${FEATURE_FLAG_STRING_MAX} characters`);
517
+ }
518
+ return trimmed; // 空串合法 = 跟随默认
519
+ }
520
+ if (FEATURE_FLAG_URLS.includes(key)) {
521
+ if (typeof value !== "string") {
522
+ throw new TypeError(`feature flag "${key}" must be a string`);
523
+ }
524
+ const trimmed = value.trim();
525
+ if (trimmed && !isHttpUrl(trimmed)) {
526
+ throw new TypeError(`feature flag "${key}" must be empty or a valid http(s) URL`);
527
+ }
528
+ return trimmed; // 空串合法 = 跟随默认
529
+ }
530
+ const allowed = FEATURE_FLAG_ENUMS[key];
531
+ if (allowed) {
532
+ if (typeof value !== "string" || !allowed.includes(value)) {
533
+ throw new TypeError(`feature flag "${key}" must be one of: ${allowed.join(", ")}`);
534
+ }
535
+ return value;
536
+ }
537
+ throw new TypeError(`unknown feature flag "${key}"`);
538
+ }
539
+
540
+ /** 清洗已存的 feature_flags 对象:只保留白名单键,类型/范围损坏的键丢弃。 */
541
+ function sanitizeFlags(raw) {
542
+ const out = {};
543
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return out;
544
+ for (const key of FEATURE_FLAG_BOOLEANS) {
545
+ if (typeof raw[key] === "boolean") out[key] = raw[key];
546
+ }
547
+ for (const [key, [min, max]] of Object.entries(FEATURE_FLAG_INT_RANGES)) {
548
+ if (Number.isInteger(raw[key]) && raw[key] >= min && raw[key] <= max) out[key] = raw[key];
549
+ }
550
+ for (const [key, [min, max]] of Object.entries(FEATURE_FLAG_NUMBER_RANGES)) {
551
+ if (typeof raw[key] === "number" && Number.isFinite(raw[key]) && raw[key] >= min && raw[key] <= max) {
552
+ out[key] = raw[key];
553
+ }
554
+ }
555
+ for (const key of FEATURE_FLAG_STRINGS) {
556
+ if (typeof raw[key] === "string" && raw[key].trim().length <= FEATURE_FLAG_STRING_MAX) {
557
+ out[key] = raw[key].trim();
558
+ }
559
+ }
560
+ for (const key of FEATURE_FLAG_URLS) {
561
+ if (typeof raw[key] === "string") {
562
+ const trimmed = raw[key].trim();
563
+ if (!trimmed || isHttpUrl(trimmed)) out[key] = trimmed;
564
+ }
565
+ }
566
+ for (const [key, allowed] of Object.entries(FEATURE_FLAG_ENUMS)) {
567
+ if (allowed.includes(raw[key])) out[key] = raw[key];
568
+ }
569
+ return out;
570
+ }
571
+
572
+ export function createSettings(db) {
573
+ db.exec(SCHEMA);
574
+
575
+ function getSetting(key) {
576
+ const row = db.prepare("SELECT value FROM user_settings WHERE key = ?").get(key);
577
+ return row?.value ?? undefined;
578
+ }
579
+
580
+ function setSetting(key, value) {
581
+ db.prepare(
582
+ `INSERT INTO user_settings (key, value) VALUES (?, ?)
583
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value`
584
+ ).run(key, value);
585
+ }
586
+
587
+ function toCommand(row) {
588
+ if (!row) return undefined;
589
+ return {
590
+ id: row.id,
591
+ name: row.name,
592
+ description: row.description,
593
+ instruction: row.instruction,
594
+ created_at: row.created_at,
595
+ updated_at: row.updated_at
596
+ };
597
+ }
598
+
599
+ return {
600
+ /** The user's self-description (free text) or "" when unset. */
601
+ getProfile() {
602
+ return getSetting("profile") ?? "";
603
+ },
604
+ setProfile(text) {
605
+ setSetting("profile", String(text ?? ""));
606
+ },
607
+
608
+ /** Behavior rules as an array of strings. */
609
+ getRules() {
610
+ return parseList(getSetting("rules") ?? "[]").filter((r) => typeof r === "string");
611
+ },
612
+ setRules(rules) {
613
+ const list = Array.isArray(rules) ? rules.filter((r) => typeof r === "string") : [];
614
+ setSetting("rules", JSON.stringify(list));
615
+ },
616
+
617
+ /**
618
+ * Agent persona (name / personality / replyStyle + enabled gate), stored as
619
+ * one JSON key. v1.2 多卡:存储形 { default, byAgent },旧 v1 单对象读作
620
+ * default 兼容。getPersona 返回全量多卡形(面板与注入寻址都用它)。
621
+ * 寻址与兜底口径不在这里——住 src/user-settings.js 的 resolvePersonaCard,
622
+ * 注入与面板预览共用那一份;这里若再留一个 getPersonaFor,就是两张皮。
623
+ */
624
+ getPersona() {
625
+ return parsePersonaStore(getSetting("persona") ?? "{}");
626
+ },
627
+ setPersona(input) {
628
+ // 双形兼容:单对象形 = 只更新 default、byAgent/bySession 保留(旧面板/旧调用方
629
+ // 合并语义);多卡形 = 全量替换。坏体(非对象)在此不落库不抛错,保值由调用方守卫。
630
+ if (typeof input !== "object" || input === null || Array.isArray(input)) return;
631
+ if ("default" in input || "byAgent" in input || "bySession" in input) {
632
+ setSetting("persona", serializePersonaStore(input));
633
+ return;
634
+ }
635
+ const current = parsePersonaStore(getSetting("persona") ?? "{}");
636
+ current.default = parsePersona(JSON.stringify(input));
637
+ setSetting("persona", serializePersonaStore(current));
638
+ },
639
+ /** Replace one byAgent card wholesale (panel save); default 不可经此改。 */
640
+ setPersonaFor(agentId, card) {
641
+ const id = typeof agentId === "string" && agentId.trim() ? agentId.trim() : "";
642
+ if (!id || id === "default") return false;
643
+ const store = parsePersonaStore(getSetting("persona") ?? "{}");
644
+ const clean = parsePersona(JSON.stringify(
645
+ typeof card === "object" && card !== null && !Array.isArray(card) ? card : {}
646
+ ));
647
+ if (Object.keys(clean).length) store.byAgent[id] = clean;
648
+ else delete store.byAgent[id];
649
+ setSetting("persona", serializePersonaStore(store));
650
+ return true;
651
+ },
652
+ /** byAgent 键列表(面板卡列表用,default 不在其中)。 */
653
+ listPersonas() {
654
+ return Object.keys(parsePersonaStore(getSetting("persona") ?? "{}").byAgent);
655
+ },
656
+ /** 删一张 byAgent 卡;default 不可删(返回 false)。 */
657
+ removePersona(agentId) {
658
+ const id = typeof agentId === "string" && agentId.trim() ? agentId.trim() : "";
659
+ if (!id || id === "default") return false;
660
+ const store = parsePersonaStore(getSetting("persona") ?? "{}");
661
+ if (!(id in store.byAgent)) return false;
662
+ delete store.byAgent[id];
663
+ setSetting("persona", serializePersonaStore(store));
664
+ return true;
665
+ },
666
+
667
+ /**
668
+ * 绑会话卡(#606):按 sessionId 存/删/列。形状与 setPersonaFor 一族逐一对应,
669
+ * 只是换了一个维度——为什么需要第二维见 parsePersonaStore 的注释。
670
+ * sessionId 为空或 "default" 拒绝(与 byAgent 同一条纪律:兜底位不可被顶替)。
671
+ */
672
+ setPersonaForSession(sessionId, card) {
673
+ const id = typeof sessionId === "string" && sessionId.trim() ? sessionId.trim() : "";
674
+ if (!id || id === "default") return false;
675
+ const store = parsePersonaStore(getSetting("persona") ?? "{}");
676
+ const clean = parsePersona(JSON.stringify(
677
+ typeof card === "object" && card !== null && !Array.isArray(card) ? card : {}
678
+ ));
679
+ if (Object.keys(clean).length) store.bySession[id] = clean;
680
+ else delete store.bySession[id];
681
+ setSetting("persona", serializePersonaStore(store));
682
+ return true;
683
+ },
684
+ /** bySession 键列表(面板「按会话建的卡」用,default 不在其中)。 */
685
+ listPersonaSessions() {
686
+ return Object.keys(parsePersonaStore(getSetting("persona") ?? "{}").bySession);
687
+ },
688
+ /** 删一张 bySession 卡;不存在返回 false。 */
689
+ removePersonaForSession(sessionId) {
690
+ const id = typeof sessionId === "string" && sessionId.trim() ? sessionId.trim() : "";
691
+ if (!id || id === "default") return false;
692
+ const store = parsePersonaStore(getSetting("persona") ?? "{}");
693
+ if (!(id in store.bySession)) return false;
694
+ delete store.bySession[id];
695
+ setSetting("persona", serializePersonaStore(store));
696
+ return true;
697
+ },
698
+
699
+ /** All custom commands, sorted by name. */
700
+ listCommands() {
701
+ const rows = db.prepare("SELECT * FROM custom_commands ORDER BY name ASC").all();
702
+ return rows.map(toCommand);
703
+ },
704
+
705
+ /**
706
+ * Add or replace a custom command by name.
707
+ * @returns the stored command.
708
+ * @throws when name is invalid or does not match DSH's command-name grammar.
709
+ */
710
+ addCommand({ name, description = "", instruction }) {
711
+ const cmdName = String(name ?? "").trim();
712
+ if (!COMMAND_NAME.test(cmdName)) {
713
+ throw new Error(`invalid command name "${cmdName}": must match /^[a-z][a-z0-9_-]*$/`);
714
+ }
715
+ if (typeof instruction !== "string" || !instruction.trim()) {
716
+ throw new Error("command instruction must be a non-empty string");
717
+ }
718
+ const now = new Date().toISOString();
719
+ const existing = db.prepare("SELECT id FROM custom_commands WHERE name = ?").get(cmdName);
720
+ if (existing) {
721
+ db.prepare(
722
+ "UPDATE custom_commands SET description = ?, instruction = ?, updated_at = ? WHERE id = ?"
723
+ ).run(String(description ?? ""), instruction, now, existing.id);
724
+ return toCommand(db.prepare("SELECT * FROM custom_commands WHERE id = ?").get(existing.id));
725
+ }
726
+ const id = randomUUID();
727
+ db.prepare(
728
+ `INSERT INTO custom_commands (id, name, description, instruction, created_at, updated_at)
729
+ VALUES (?, ?, ?, ?, ?, ?)`
730
+ ).run(id, cmdName, String(description ?? ""), instruction, now, now);
731
+ return toCommand(db.prepare("SELECT * FROM custom_commands WHERE id = ?").get(id));
732
+ },
733
+
734
+ /** Remove a custom command by id; returns true when removed. */
735
+ removeCommand(id) {
736
+ const result = db.prepare("DELETE FROM custom_commands WHERE id = ?").run(id);
737
+ return result.changes > 0;
738
+ },
739
+
740
+ /** Vector-search provider config (OpenAI-compatible embeddings endpoint). */
741
+ getVectorConfig() {
742
+ const raw = getSetting("vector");
743
+ if (!raw) return undefined;
744
+ try {
745
+ const cfg = JSON.parse(raw);
746
+ return typeof cfg === "object" && cfg !== null ? cfg : undefined;
747
+ } catch {
748
+ return undefined;
749
+ }
750
+ },
751
+ setVectorConfig({ enabled, baseUrl, apiKey, model }) {
752
+ const cfg = {
753
+ enabled: enabled === true || enabled === 1,
754
+ baseUrl: String(baseUrl ?? "").trim().replace(/\/+$/, ""),
755
+ apiKey: String(apiKey ?? "").trim(),
756
+ model: String(model ?? "").trim()
757
+ };
758
+ setSetting("vector", JSON.stringify(cfg));
759
+ return cfg;
760
+ },
761
+
762
+ /**
763
+ * Standalone external API settings (kv "external_api"): {enabled, port,
764
+ * token}. The Bearer token is auto-generated on first boot and persisted
765
+ * here. Partial writes preserve the keys they don't mention.
766
+ */
767
+ getExternalApi() {
768
+ const raw = getSetting("external_api");
769
+ if (!raw) return undefined;
770
+ try {
771
+ const cfg = JSON.parse(raw);
772
+ return typeof cfg === "object" && cfg !== null ? cfg : undefined;
773
+ } catch {
774
+ return undefined;
775
+ }
776
+ },
777
+ setExternalApi(patch = {}) {
778
+ const prev = this.getExternalApi() ?? {};
779
+ const port = Number(patch.port ?? prev.port);
780
+ const host = typeof patch.host === "string" && patch.host.trim() ? patch.host.trim() : (prev.host ?? "127.0.0.1");
781
+ const cfg = {
782
+ enabled: patch.enabled !== undefined ? patch.enabled === true : prev.enabled === true,
783
+ port: Number.isInteger(port) && port > 0 ? port : 8790,
784
+ host,
785
+ token: String(patch.token ?? prev.token ?? "")
786
+ };
787
+ setSetting("external_api", JSON.stringify(cfg));
788
+ return cfg;
789
+ },
790
+
791
+ /**
792
+ * Web panel mode (kv "panel_mode"): "light" (low-resource preset) or
793
+ * "standard" (full feature set). Unset reads as "standard".
794
+ */
795
+ getPanelMode() {
796
+ return getSetting("panel_mode") === "light" ? "light" : "standard";
797
+ },
798
+ setPanelMode(mode) {
799
+ setSetting("panel_mode", mode === "light" ? "light" : "standard");
800
+ },
801
+
802
+ /**
803
+ * Feature flags(kv "feature_flags"):面板对后端能力的显式覆盖。读取只
804
+ * 返回白名单内的合法键(默认 {}),写入是逐键校验后的合并持久化。
805
+ */
806
+ getFeatureFlags() {
807
+ const raw = getSetting("feature_flags");
808
+ if (!raw) return {};
809
+ try {
810
+ return sanitizeFlags(JSON.parse(raw));
811
+ } catch {
812
+ return {};
813
+ }
814
+ },
815
+ setFeatureFlags(patch) {
816
+ if (typeof patch !== "object" || patch === null || Array.isArray(patch)) {
817
+ throw new TypeError("feature flags patch must be a plain object");
818
+ }
819
+ const merged = this.getFeatureFlags();
820
+ for (const [key, value] of Object.entries(patch)) {
821
+ merged[key] = validateFlag(key, value);
822
+ }
823
+ setSetting("feature_flags", JSON.stringify(merged));
824
+ return merged;
825
+ }
826
+ };
827
+ }