@modusensus/dsh-mneme 0.8.14 → 0.8.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +655 -654
- package/assets/icon.svg +3 -3
- package/bin/cli.mjs +689 -689
- package/bin/dsh-mneme-mcp.mjs +510 -510
- package/bin/dsh-mneme-serve.mjs +151 -151
- package/cordis.patch.yml +21 -21
- package/docs/CONFIGURATION.md +263 -0
- package/docs/CONTINUITY.md +124 -0
- package/docs/DAEMON.md +46 -0
- package/docs/ENTITIES.md +245 -0
- package/docs/LOCAL_MODEL.md +222 -0
- package/docs/SEMANTIC.md +256 -0
- package/docs/SLEEP.md +164 -0
- package/docs/STORAGE.md +89 -0
- package/lib/api-standalone.js +673 -673
- package/lib/api.js +1437 -1433
- package/lib/bootstrap.js +211 -211
- package/lib/client.js +5444 -5295
- package/lib/commands.js +95 -95
- package/lib/config.js +880 -843
- package/lib/content-hash.js +54 -54
- package/lib/continuity.js +220 -220
- package/lib/document-index.js +94 -94
- package/lib/document.js +303 -303
- package/lib/dream/clustering.js +118 -118
- package/lib/dream/decisions.js +674 -674
- package/lib/dream/narratives.js +44 -44
- package/lib/dream/sleep.js +819 -819
- package/lib/dream.js +1501 -1501
- package/lib/embedding.js +177 -177
- package/lib/entities/extractor.js +252 -252
- package/lib/graph/anchoring.js +179 -179
- package/lib/guide.js +74 -74
- package/lib/heat.js +144 -144
- package/lib/hot-memory.js +46 -46
- package/lib/index.js +535 -503
- package/lib/inject.js +464 -450
- package/lib/lang.js +248 -229
- package/lib/local-embedder.js +358 -358
- package/lib/maintenance.js +142 -142
- package/lib/mirror.js +330 -330
- package/lib/organize.js +450 -450
- package/lib/peak-hours.js +117 -117
- package/lib/placement.js +13 -0
- package/lib/pre-inject-gate.js +340 -0
- package/lib/quality-filter.js +126 -126
- package/lib/recall-stats.js +175 -175
- package/lib/reranker.js +269 -269
- package/lib/runtime/adopt.js +176 -176
- package/lib/runtime/closure.js +172 -172
- package/lib/runtime/download.js +247 -247
- package/lib/runtime/layout.js +265 -265
- package/lib/runtime/loader.js +252 -252
- package/lib/runtime/model-fetch.js +361 -361
- package/lib/runtime/provision.js +237 -237
- package/lib/runtime/tarball.js +130 -130
- package/lib/runtime/verify.js +223 -204
- package/lib/scope.js +146 -146
- package/lib/search/adaptive.js +60 -60
- package/lib/search/bm25.js +96 -96
- package/lib/semantic.js +229 -229
- package/lib/sensitive-scan.js +209 -209
- package/lib/serve.js +215 -215
- package/lib/service.js +2663 -2605
- package/lib/settings.js +517 -511
- package/lib/store.js +3007 -3007
- package/lib/summarize.js +891 -856
- package/lib/tools.js +743 -737
- package/lib/vector-index.js +110 -110
- package/lib/version-check.js +122 -122
- package/lib/write-admission.js +402 -402
- package/locale/en.json +6 -6
- package/locale/zh.json +6 -6
- package/package.json +118 -118
- package/runtime-manifest.json +526 -526
- package/scripts/benchmark-embed.js +201 -201
- package/scripts/benchmark-recall.js +249 -249
- package/scripts/benchmark-rerank.js +166 -166
- package/scripts/build-runtime-manifest.mjs +224 -224
- package/scripts/changelog-prep.mjs +36 -36
- package/scripts/check-sync.js +42 -42
- package/scripts/e2e-dsh.js +220 -220
- package/scripts/mneme-runtime.mjs +277 -277
- package/scripts/stress-dsh.js +255 -255
- package/scripts/sync-lib.js +47 -47
- package/scripts/sync-test-badge.mjs +69 -69
- package/scripts/test-count-sync.mjs +44 -44
- package/src/api-standalone.js +673 -673
- package/src/api.js +1437 -1433
- package/src/bootstrap.js +211 -211
- package/src/commands.js +95 -95
- package/src/config.js +880 -843
- package/src/content-hash.js +54 -54
- package/src/continuity.js +220 -220
- package/src/document-index.js +94 -94
- package/src/document.js +303 -303
- package/src/dream/clustering.js +118 -118
- package/src/dream/decisions.js +674 -674
- package/src/dream/narratives.js +44 -44
- package/src/dream/sleep.js +819 -819
- package/src/dream.js +1501 -1501
- package/src/embedding.js +177 -177
- package/src/entities/extractor.js +252 -252
- package/src/graph/anchoring.js +179 -179
- package/src/guide.js +74 -74
- package/src/heat.js +144 -144
- package/src/hot-memory.js +46 -46
- package/src/index.js +535 -503
- package/src/inject.js +464 -450
- package/src/lang.js +248 -229
- package/src/local-embedder.js +358 -358
- package/src/maintenance.js +142 -142
- package/src/mirror.js +330 -330
- package/src/organize.js +450 -450
- package/src/peak-hours.js +117 -117
- package/src/placement.js +13 -0
- package/src/pre-inject-gate.js +340 -0
- package/src/quality-filter.js +126 -126
- package/src/recall-stats.js +175 -175
- package/src/reranker.js +269 -269
- package/src/runtime/adopt.js +176 -176
- package/src/runtime/closure.js +172 -172
- package/src/runtime/download.js +247 -247
- package/src/runtime/layout.js +265 -265
- package/src/runtime/loader.js +252 -252
- package/src/runtime/model-fetch.js +361 -361
- package/src/runtime/provision.js +237 -237
- package/src/runtime/tarball.js +130 -130
- package/src/runtime/verify.js +223 -204
- package/src/scope.js +146 -146
- package/src/search/adaptive.js +60 -60
- package/src/search/bm25.js +96 -96
- package/src/semantic.js +229 -229
- package/src/sensitive-scan.js +209 -209
- package/src/serve.js +215 -215
- package/src/service.js +2663 -2605
- package/src/settings.js +517 -511
- package/src/store.js +3007 -3007
- package/src/summarize.js +891 -856
- package/src/tools.js +743 -737
- package/src/vector-index.js +110 -110
- package/src/version-check.js +122 -122
- package/src/write-admission.js +402 -402
- package/test/adaptive-inject-budget.test.js +85 -85
- package/test/api-routes-gaps.test.js +218 -218
- package/test/api.test.js +1702 -1690
- package/test/audit.test.js +448 -448
- package/test/baseline-persist.test.js +248 -248
- package/test/benchmark.test.js +94 -94
- package/test/bootstrap.test.js +220 -220
- package/test/changelog-prep.test.js +60 -60
- package/test/client.test.js +1230 -1169
- package/test/clustering.test.js +100 -100
- package/test/commands.test.js +106 -106
- package/test/config.test.js +66 -66
- package/test/conflict-freeze.test.js +300 -300
- package/test/content-hash.test.js +204 -204
- package/test/continuity.test.js +333 -333
- package/test/document-dir.test.js +233 -233
- package/test/document.test.js +671 -671
- package/test/dream-archive-guardrail.test.js +116 -116
- package/test/dream-candidate-hybrid.test.js +281 -281
- package/test/dream-coverage-degrade.test.js +86 -86
- package/test/dream-failure-backoff.test.js +173 -173
- package/test/dream-peak-hours.test.js +194 -194
- package/test/dream.test.js +1385 -1383
- package/test/embedding-ready.test.js +196 -196
- package/test/entities.test.js +627 -547
- package/test/entity-recall.test.js +179 -179
- package/test/epistemic.test.js +325 -325
- package/test/fnew-0112.test.js +311 -311
- package/test/fnew-03.test.js +422 -422
- package/test/graph-anchoring.test.js +184 -184
- package/test/graph-api.test.js +175 -175
- package/test/graph-inject-budget.test.js +103 -103
- package/test/graph-passive-confirm.test.js +91 -91
- package/test/graph-weight-evolution.test.js +104 -104
- package/test/heat.test.js +175 -175
- package/test/helpers/dream-mock.js +82 -82
- package/test/helpers/peer-worker.mjs +25 -25
- package/test/helpers/runtime-source.js +60 -60
- package/test/helpers/tar-builder.js +80 -80
- package/test/helpers/write-admission-samples.js +308 -308
- package/test/hot-memory.test.js +145 -145
- package/test/inject-content-cap.test.js +91 -91
- package/test/inject-first-round-bm25.test.js +89 -89
- package/test/inject-heat.test.js +77 -77
- package/test/inject-parent-gate.test.js +106 -106
- package/test/inject-pools.test.js +194 -194
- package/test/inject-recall.test.js +105 -105
- package/test/inject-rotation.test.js +161 -161
- package/test/inject.test.js +420 -420
- package/test/lang.test.js +87 -87
- package/test/lastrunat-persist.test.js +101 -101
- package/test/lib-smoke.test.js +96 -96
- package/test/llm-audit.test.js +598 -598
- package/test/llm-message-source.test.js +207 -207
- package/test/local-embedder.test.js +292 -292
- package/test/maintenance.test.js +391 -391
- package/test/mcp-stdio.test.js +385 -385
- package/test/mirror-dirty.test.js +424 -424
- package/test/mirror-edit-digest.test.js +187 -187
- package/test/mirror-generation.test.js +499 -499
- package/test/mirror-types.test.js +208 -208
- package/test/mirror.test.js +249 -249
- package/test/narrative-bars.test.js +162 -162
- package/test/organize.test.js +418 -418
- package/test/peer-blockers.test.js +193 -193
- package/test/placement.test.js +36 -0
- package/test/policy-epoch.test.js +259 -259
- package/test/pre-inject-gate.test.js +434 -0
- package/test/quality-filter.test.js +186 -186
- package/test/reasoning-effort.test.js +752 -752
- package/test/recall-anti-update.test.js +82 -82
- package/test/recall-evals.test.js +253 -253
- package/test/recall-layer.test.js +326 -326
- package/test/recall-stats.test.js +189 -189
- package/test/receipt-chain.test.js +451 -451
- package/test/reflection.test.js +226 -226
- package/test/reindex-backfill.test.js +62 -62
- package/test/reranker-single-logit.test.js +73 -73
- package/test/reranker.test.js +197 -197
- package/test/resident-status.test.js +83 -83
- package/test/runtime-adopt.test.js +206 -206
- package/test/runtime-cli.test.js +89 -89
- package/test/runtime-closure.test.js +193 -193
- package/test/runtime-download.test.js +302 -302
- package/test/runtime-layout.test.js +163 -163
- package/test/runtime-loader.test.js +255 -255
- package/test/runtime-manifest.test.js +159 -159
- package/test/runtime-model-fetch.test.js +348 -348
- package/test/runtime-provision.test.js +101 -101
- package/test/runtime-tarball.test.js +81 -81
- package/test/runtime-verify.test.js +319 -279
- package/test/scope-candidates.test.js +239 -239
- package/test/scope-explicit.test.js +452 -452
- package/test/scope-inject-soft.test.js +106 -106
- package/test/scope-retrieval.test.js +197 -197
- package/test/scope-storage.test.js +255 -255
- package/test/scope-strict.test.js +367 -367
- package/test/search-fusion.test.js +90 -90
- package/test/semantic.test.js +124 -124
- package/test/sensitive-scan.test.js +207 -207
- package/test/serve-bin.test.js +121 -121
- package/test/serve.test.js +196 -196
- package/test/service-search.test.js +174 -174
- package/test/service.test.js +405 -405
- package/test/settings.test.js +261 -261
- package/test/skip-invalid-restore.test.js +262 -262
- package/test/sleep-action-set.test.js +422 -422
- package/test/sleep-heat.test.js +126 -126
- package/test/sleep-scheduler-rearm.test.js +89 -89
- package/test/sleep.test.js +527 -527
- package/test/standalone-api.test.js +751 -751
- package/test/store-vector-cache.test.js +98 -98
- package/test/store.test.js +396 -396
- package/test/stress.test.js +209 -209
- package/test/summarize-occurred-at.test.js +127 -0
- package/test/summarize-reasoning-effort.test.js +189 -189
- package/test/summarize.test.js +1465 -1465
- package/test/supersede-state-tracking.test.js +394 -0
- package/test/test-count-sync.test.js +60 -60
- package/test/tools-dto-schema-matrix.test.js +325 -325
- package/test/tools-memory-get-schema.test.js +65 -65
- package/test/tools.test.js +428 -374
- package/test/updated-at-semantics.test.js +113 -113
- package/test/vector-index.test.js +221 -221
- package/test/version-check.test.js +152 -152
- package/test/webServer-optional.test.js +82 -82
- package/test/write-admission.test.js +668 -668
package/src/settings.js
CHANGED
|
@@ -1,511 +1,517 @@
|
|
|
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
|
-
// --- feature flags(功能开关)白名单 -----------------------------------------
|
|
35
|
-
// 面板可逐项开关的后端能力。设计约束:
|
|
36
|
-
// 1. 键名与类型必须和 config.js schema 同名同型,这份白名单是唯一校验源
|
|
37
|
-
// (api.js 复用它计算 effective),schema 增删能力键时要同步改这里。
|
|
38
|
-
// 2. 持久化(kv "feature_flags")只落白名单键;读到未知键、类型损坏或越界的
|
|
39
|
-
// 值一律丢弃而不是报错——kv 会残留旧版本写入的键,读路径必须向前兼容。
|
|
40
|
-
// 3. 写入是逐键校验的合并写:未知键/类型/范围不符抛 TypeError(消息含键名,
|
|
41
|
-
// 供 API 透传给前端定位),校验不通过不落库,坏值永远进不了 kv。
|
|
42
|
-
const FEATURE_FLAG_BOOLEANS = [
|
|
43
|
-
"autoInject",
|
|
44
|
-
"autoSummarize",
|
|
45
|
-
"hotMemoryEnabled",
|
|
46
|
-
// Issue #34(v0.7.2 引入;v0.7.11 误删后随 #333 恢复):对话开始注入当前时间。
|
|
47
|
-
// 顶层扁平键,随白名单回面板(FEATURE_GROUPS 的 group.core)。
|
|
48
|
-
"injectTimePrefix",
|
|
49
|
-
// Issue #239 第 5 项:注入条数的查询自适应(确定性强则收缩注入条数,默认关)。
|
|
50
|
-
"injectUncertaintyAdaptive",
|
|
51
|
-
// Issue #249:能力说明(工具描述判断指引 + order 150 总则段)。第二批起它是
|
|
52
|
-
// 注入父开关 autoInject 的子项、默认开(父关时不生效,闸门见 config.js)。
|
|
53
|
-
"injectGuidanceEnabled",
|
|
54
|
-
// Issue #249 N3:压缩边缘双落点——上下文即将精简前落一条连续性提案 + 往
|
|
55
|
-
// 序列末尾追加同一份快照。默认关(新注入表面,见 config.js),父开关 autoInject。
|
|
56
|
-
"continuityRescueEnabled",
|
|
57
|
-
"entityExtractionEnabled",
|
|
58
|
-
// Issue #219:图召回轴——查询命中实体名时把挂联记忆并入检索融合池
|
|
59
|
-
// (默认关;依赖实体抽取产出,lightMode 强制关闭)。
|
|
60
|
-
"entityRecallEnabled",
|
|
61
|
-
// Issue #24 块1:图谱锚定层级联——命中实体作种子、沿关系表向邻居扩散
|
|
62
|
-
// (默认关;依赖实体抽取产出,lightMode 强制关闭)。
|
|
63
|
-
"graphAnchoringEnabled",
|
|
64
|
-
// Issue #24 块2:边权重演化总闸——关着时任何触达都不改边权(默认关;
|
|
65
|
-
// 依赖实体抽取产出,lightMode 强制关闭)。块4 的被动确认是它底下的一个通道。
|
|
66
|
-
"graphWeightEnabled",
|
|
67
|
-
// Issue #24 块3:关联提示防幻觉——图召回线索行以「[检索线索]」标注进注入
|
|
68
|
-
// (默认关=线索只参与排序不进块;依赖实体抽取产出,lightMode 强制关闭)。
|
|
69
|
-
"graphInjectHint",
|
|
70
|
-
// Issue #24 块4:被动确认通道——正常触达即对该记忆挂联的边 bump 一格
|
|
71
|
-
// (默认关;需总闸 graphWeightEnabled 同开)。
|
|
72
|
-
"graphPassiveConfirm",
|
|
73
|
-
// Issue #164:叙述条——dream 期间按 tag 主题簇合成叙述落库(source=
|
|
74
|
-
// narrative,evidence 回链簇内记忆;按需检索不常驻注入;默认关)。
|
|
75
|
-
"dreamNarrativeEnabled",
|
|
76
|
-
// Issue #230:document 型记忆——agent 产长文档的指针行(注册校验/摘要+
|
|
77
|
-
// doc_path 落库/C2 去重/supersede 记账;全文归 agent;默认关,lightMode 强制关)。
|
|
78
|
-
"documentMemoryEnabled",
|
|
79
|
-
"codingRetrospect",
|
|
80
|
-
"autoDream",
|
|
81
|
-
// Issue #292:autoDream 连续失败退避(默认关)。基数是 dreamMinIntervalMinutes,
|
|
82
|
-
// 有效间隔 = 基数 × 2^连续失败数(成功清零),封顶 30 分钟;面板可启停。
|
|
83
|
-
"autoDreamFailureBackoff",
|
|
84
|
-
"sleepModeEnabled",
|
|
85
|
-
"heatEnabled",
|
|
86
|
-
// issue #218 / E5:注入侧 heat 独立开关(默认关,老用户 injectHeatEnabled=true 回滚)。
|
|
87
|
-
"injectHeatEnabled",
|
|
88
|
-
"hybridInject",
|
|
89
|
-
"selectiveInjectEnabled",
|
|
90
|
-
"searchSemanticDedup",
|
|
91
|
-
"rerankEnabled",
|
|
92
|
-
"resilientModelDownload",
|
|
93
|
-
"adaptiveThresholdEnabled",
|
|
94
|
-
"reflectionUpdateEnabled",
|
|
95
|
-
"reflectionFailureTracking",
|
|
96
|
-
"bm25SearchEnabled",
|
|
97
|
-
"conflictFreezeEnabled",
|
|
98
|
-
"trustEpistemicWeighting",
|
|
99
|
-
// Plan #2: attach per-source {keyword, vector, bm25, final} signals to each
|
|
100
|
-
// search result for transparency/debugging. Default off, purely decorative.
|
|
101
|
-
"signalTransparency",
|
|
102
|
-
// Issue #89:宽容校验回归(默认开)+ 跨类型合并显式放宽(默认关)。
|
|
103
|
-
"dreamSkipInvalid",
|
|
104
|
-
"allowCrossTypeMerge",
|
|
105
|
-
// Issue #339 / E8:guarded 类型 merge
|
|
106
|
-
// 长保留类型的 merge
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
//
|
|
117
|
-
|
|
118
|
-
"
|
|
119
|
-
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
|
|
123
|
-
"
|
|
124
|
-
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
"writeAdmission.
|
|
129
|
-
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
// Issue #
|
|
157
|
-
|
|
158
|
-
// Issue #
|
|
159
|
-
|
|
160
|
-
// Issue #
|
|
161
|
-
|
|
162
|
-
// Issue #
|
|
163
|
-
|
|
164
|
-
// Issue #
|
|
165
|
-
|
|
166
|
-
// Issue #
|
|
167
|
-
|
|
168
|
-
// Issue #
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
//
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
// Issue #
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
//
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
"
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
"
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
"
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
//
|
|
209
|
-
|
|
210
|
-
//
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
//
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
if (typeof value !== "
|
|
278
|
-
throw new TypeError(`feature flag "${key}" must be a
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
if (
|
|
288
|
-
throw new TypeError(`feature flag "${key}" must be
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
for (const
|
|
317
|
-
if (typeof raw[key] === "
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
for (const
|
|
333
|
-
if (
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
},
|
|
388
|
-
|
|
389
|
-
/**
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
return
|
|
422
|
-
},
|
|
423
|
-
|
|
424
|
-
/**
|
|
425
|
-
|
|
426
|
-
const
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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
|
+
// --- feature flags(功能开关)白名单 -----------------------------------------
|
|
35
|
+
// 面板可逐项开关的后端能力。设计约束:
|
|
36
|
+
// 1. 键名与类型必须和 config.js schema 同名同型,这份白名单是唯一校验源
|
|
37
|
+
// (api.js 复用它计算 effective),schema 增删能力键时要同步改这里。
|
|
38
|
+
// 2. 持久化(kv "feature_flags")只落白名单键;读到未知键、类型损坏或越界的
|
|
39
|
+
// 值一律丢弃而不是报错——kv 会残留旧版本写入的键,读路径必须向前兼容。
|
|
40
|
+
// 3. 写入是逐键校验的合并写:未知键/类型/范围不符抛 TypeError(消息含键名,
|
|
41
|
+
// 供 API 透传给前端定位),校验不通过不落库,坏值永远进不了 kv。
|
|
42
|
+
const FEATURE_FLAG_BOOLEANS = [
|
|
43
|
+
"autoInject",
|
|
44
|
+
"autoSummarize",
|
|
45
|
+
"hotMemoryEnabled",
|
|
46
|
+
// Issue #34(v0.7.2 引入;v0.7.11 误删后随 #333 恢复):对话开始注入当前时间。
|
|
47
|
+
// 顶层扁平键,随白名单回面板(FEATURE_GROUPS 的 group.core)。
|
|
48
|
+
"injectTimePrefix",
|
|
49
|
+
// Issue #239 第 5 项:注入条数的查询自适应(确定性强则收缩注入条数,默认关)。
|
|
50
|
+
"injectUncertaintyAdaptive",
|
|
51
|
+
// Issue #249:能力说明(工具描述判断指引 + order 150 总则段)。第二批起它是
|
|
52
|
+
// 注入父开关 autoInject 的子项、默认开(父关时不生效,闸门见 config.js)。
|
|
53
|
+
"injectGuidanceEnabled",
|
|
54
|
+
// Issue #249 N3:压缩边缘双落点——上下文即将精简前落一条连续性提案 + 往
|
|
55
|
+
// 序列末尾追加同一份快照。默认关(新注入表面,见 config.js),父开关 autoInject。
|
|
56
|
+
"continuityRescueEnabled",
|
|
57
|
+
"entityExtractionEnabled",
|
|
58
|
+
// Issue #219:图召回轴——查询命中实体名时把挂联记忆并入检索融合池
|
|
59
|
+
// (默认关;依赖实体抽取产出,lightMode 强制关闭)。
|
|
60
|
+
"entityRecallEnabled",
|
|
61
|
+
// Issue #24 块1:图谱锚定层级联——命中实体作种子、沿关系表向邻居扩散
|
|
62
|
+
// (默认关;依赖实体抽取产出,lightMode 强制关闭)。
|
|
63
|
+
"graphAnchoringEnabled",
|
|
64
|
+
// Issue #24 块2:边权重演化总闸——关着时任何触达都不改边权(默认关;
|
|
65
|
+
// 依赖实体抽取产出,lightMode 强制关闭)。块4 的被动确认是它底下的一个通道。
|
|
66
|
+
"graphWeightEnabled",
|
|
67
|
+
// Issue #24 块3:关联提示防幻觉——图召回线索行以「[检索线索]」标注进注入
|
|
68
|
+
// (默认关=线索只参与排序不进块;依赖实体抽取产出,lightMode 强制关闭)。
|
|
69
|
+
"graphInjectHint",
|
|
70
|
+
// Issue #24 块4:被动确认通道——正常触达即对该记忆挂联的边 bump 一格
|
|
71
|
+
// (默认关;需总闸 graphWeightEnabled 同开)。
|
|
72
|
+
"graphPassiveConfirm",
|
|
73
|
+
// Issue #164:叙述条——dream 期间按 tag 主题簇合成叙述落库(source=
|
|
74
|
+
// narrative,evidence 回链簇内记忆;按需检索不常驻注入;默认关)。
|
|
75
|
+
"dreamNarrativeEnabled",
|
|
76
|
+
// Issue #230:document 型记忆——agent 产长文档的指针行(注册校验/摘要+
|
|
77
|
+
// doc_path 落库/C2 去重/supersede 记账;全文归 agent;默认关,lightMode 强制关)。
|
|
78
|
+
"documentMemoryEnabled",
|
|
79
|
+
"codingRetrospect",
|
|
80
|
+
"autoDream",
|
|
81
|
+
// Issue #292:autoDream 连续失败退避(默认关)。基数是 dreamMinIntervalMinutes,
|
|
82
|
+
// 有效间隔 = 基数 × 2^连续失败数(成功清零),封顶 30 分钟;面板可启停。
|
|
83
|
+
"autoDreamFailureBackoff",
|
|
84
|
+
"sleepModeEnabled",
|
|
85
|
+
"heatEnabled",
|
|
86
|
+
// issue #218 / E5:注入侧 heat 独立开关(默认关,老用户 injectHeatEnabled=true 回滚)。
|
|
87
|
+
"injectHeatEnabled",
|
|
88
|
+
"hybridInject",
|
|
89
|
+
"selectiveInjectEnabled",
|
|
90
|
+
"searchSemanticDedup",
|
|
91
|
+
"rerankEnabled",
|
|
92
|
+
"resilientModelDownload",
|
|
93
|
+
"adaptiveThresholdEnabled",
|
|
94
|
+
"reflectionUpdateEnabled",
|
|
95
|
+
"reflectionFailureTracking",
|
|
96
|
+
"bm25SearchEnabled",
|
|
97
|
+
"conflictFreezeEnabled",
|
|
98
|
+
"trustEpistemicWeighting",
|
|
99
|
+
// Plan #2: attach per-source {keyword, vector, bm25, final} signals to each
|
|
100
|
+
// search result for transparency/debugging. Default off, purely decorative.
|
|
101
|
+
"signalTransparency",
|
|
102
|
+
// Issue #89:宽容校验回归(默认开)+ 跨类型合并显式放宽(默认关)。
|
|
103
|
+
"dreamSkipInvalid",
|
|
104
|
+
"allowCrossTypeMerge",
|
|
105
|
+
// Issue #339 / E8:guarded 类型 merge 护栏(默认开)——合并对象命中
|
|
106
|
+
// 长保留类型的 merge 决策整条跳过。关掉 = guarded 类型的 merge 不再被跳过,
|
|
107
|
+
// 「更精炼的摘要」重新成为约束失真的主通道(面板可关,供需要旧行为的库回退)。
|
|
108
|
+
"dreamMergeGuard",
|
|
109
|
+
// Issue #17(v0.8.0 A1):scope 隔离存储层总开关。开启后写入标注
|
|
110
|
+
// agent_scope/workspace_scope、去重键扩展(含 sensitivity);检索侧过滤在
|
|
111
|
+
// A2/A3。默认关=行为与 A1 前逐字节一致。
|
|
112
|
+
"scopeEnabled",
|
|
113
|
+
// Issue #17(v0.8.0 A3):strictScope 硬过滤——他 scope 完全不可见(关闭时
|
|
114
|
+
// 为 A2 软隔离:降权保留可见)。
|
|
115
|
+
"strictScope",
|
|
116
|
+
// 工具暴露开关(v0.8.5):记忆已每轮自动注入,memory_search/memory_archive
|
|
117
|
+
// 在慢/轻量模型上是多余往返,面板可关(默认关=行为不变)。
|
|
118
|
+
"disableMemorySearch",
|
|
119
|
+
"disableMemoryArchive",
|
|
120
|
+
// 嵌套对象开关:config.js 里是 memoryQualityFilter / llmAudit 对象的 enabled
|
|
121
|
+
// 子字段。kv 按点号键平铺存("memoryQualityFilter.enabled": false),index.js
|
|
122
|
+
// 合并时展开回嵌套对象,api.js 的 effective 从对象子字段取值。
|
|
123
|
+
"memoryQualityFilter.enabled",
|
|
124
|
+
"llmAudit.enabled",
|
|
125
|
+
// Issue #254:写入准入(第 1 级确定性拒绝)。两个键分层——enabled 跑判据、
|
|
126
|
+
// enforce 真拦;都默认关。同上,点号键平铺存、合并时展开回
|
|
127
|
+
// writeAdmission 对象。
|
|
128
|
+
"writeAdmission.enabled",
|
|
129
|
+
"writeAdmission.enforce",
|
|
130
|
+
// Issue #380:注入前判定(preInjectGate)。两级语义同 writeAdmission——
|
|
131
|
+
// enabled 跑判定(观察档)、enforce 缓存命中帧真滤除;都默认关。点号键平铺存、
|
|
132
|
+
// 合并时展开回 preInjectGate 对象。
|
|
133
|
+
"preInjectGate.enabled",
|
|
134
|
+
"preInjectGate.enforce",
|
|
135
|
+
// Issue #164 A2:写入边界的密钥 / PII 判据(src/sensitive-scan.js)。与上面两个
|
|
136
|
+
// 键分层——本键决定「这类判据参不参与」(默认关),命中之后是仅告警还是真拦仍由
|
|
137
|
+
// writeAdmission.enforce 决定(#164 口径:默认仅告警、拦截 opt-in)。
|
|
138
|
+
"sensitiveScanEnabled"
|
|
139
|
+
];
|
|
140
|
+
// 整数开关的闭区间,与 config.js 里 z.natural().min().max() 对齐。
|
|
141
|
+
const FEATURE_FLAG_INT_RANGES = {
|
|
142
|
+
distillRateLimitIntervalMs: [0, 60000],
|
|
143
|
+
distillRateLimitRetries: [0, 10],
|
|
144
|
+
distillRateLimitBaseDelayMs: [100, 60000],
|
|
145
|
+
distillMaxChars: [1000, 200000],
|
|
146
|
+
codingBoostFactor: [1, 5],
|
|
147
|
+
dreamMinIntervalMinutes: [0, 10080],
|
|
148
|
+
dreamMaxTokens: [256, 131072],
|
|
149
|
+
// Issue #127:autoSummarize 节流三键(0 = 零行为变化,等同现状)。
|
|
150
|
+
summarizeMinIntervalMinutes: [0, 10080],
|
|
151
|
+
summarizeMaxEntriesPerRun: [0, 50],
|
|
152
|
+
summarizeDedupeWindowHours: [0, 168],
|
|
153
|
+
// Issue #239:蒸馏零 LLM 预判(窗口最小字符数)与每会话 run 预算(0 = 零行为变化)。
|
|
154
|
+
summarizeMinWindowChars: [0, 100000],
|
|
155
|
+
summarizeMaxRunsPerSession: [0, 1000],
|
|
156
|
+
// Issue #239 第 4 项:高峰顺延上限(分钟,0 = 不设上限)。
|
|
157
|
+
summarizePeakMaxDeferMinutes: [0, 1440],
|
|
158
|
+
// Issue #239 第 4 项镜像到巩固:同一口径(分钟,0 = 不设上限)。
|
|
159
|
+
dreamPeakMaxDeferMinutes: [0, 1440],
|
|
160
|
+
// Issue #125:hybrid 候选量上限(0 = 复用 dreamMaxSnapshotSize)。
|
|
161
|
+
dreamCandidateMax: [0, 5000],
|
|
162
|
+
// Issue #164①:注入单条正文截断上限(默认 300 = 既有行为)。
|
|
163
|
+
injectContentMaxChars: [60, 4000],
|
|
164
|
+
// Issue #164:叙述条成簇门槛(共享同一 tag 的记忆数下限)。
|
|
165
|
+
dreamNarrativeMinCluster: [2, 20],
|
|
166
|
+
// Issue #230:document 摘要行的注入预算(次优先档内最多几条指针行)。
|
|
167
|
+
documentInjectBudget: [1, 5],
|
|
168
|
+
// Issue #249 第一批:B1 pin 池(约束/偏好)的独立条数预算(0 = 关闭/现状)。
|
|
169
|
+
pinnedInjectBudget: [0, 5],
|
|
170
|
+
// Issue #257:sleep 冲突/模式阶段的 LLM 输出预算(原硬编码 2048,实测不足)。
|
|
171
|
+
sleepMaxTokens: [256, 131072],
|
|
172
|
+
// Issue #258:总览(dream_summarize)输入条数硬上限(0 = 不设上限)。
|
|
173
|
+
dreamSummaryMaxInputs: [0, 100000],
|
|
174
|
+
// Issue #24 块1:锚定种子上限与级联深度(闭区间与 config.js 的 z.natural() 对齐)。
|
|
175
|
+
graphSeedCap: [1, 30],
|
|
176
|
+
graphCascadeDepth: [1, 3],
|
|
177
|
+
// Issue #24 块3:图线索行的独立注入预算(0 = 线索不进块)。
|
|
178
|
+
graphInjectBudget: [0, 5]
|
|
179
|
+
};
|
|
180
|
+
// 浮点开关的闭区间(与 config.js 的 z.number().min().max() 对齐)。与整数开关
|
|
181
|
+
// 分开:面板的整数控件要求 Number.isInteger,而余弦相似度阈值必须允许小数。
|
|
182
|
+
const FEATURE_FLAG_NUMBER_RANGES = {
|
|
183
|
+
// Issue #127:vector 去重档的并入阈值。
|
|
184
|
+
summarizeDedupeMinSim: [0.5, 0.99],
|
|
185
|
+
// Issue #125:hybrid 判"高相似"的阈值。
|
|
186
|
+
dreamCandidateMinSim: [0.5, 0.99],
|
|
187
|
+
// Issue #24 块2:单次有效触达的边权抬升幅度(与 config.js 的闭区间对齐)。
|
|
188
|
+
graphWeightDelta: [0, 1]
|
|
189
|
+
};
|
|
190
|
+
// 自由字符串开关(与 config.js 的 z.string() 同名同型):trim 后 ≤200 字符,
|
|
191
|
+
// 空串合法(= 跟随主对话模型/默认路径,面板显示 placeholder)。
|
|
192
|
+
const FEATURE_FLAG_STRINGS = [
|
|
193
|
+
"dreamProvider",
|
|
194
|
+
"dreamModel",
|
|
195
|
+
// 睡眠侧专用路由(sleep.js 的 config-first 第三层):面板下拉随
|
|
196
|
+
// /llm-providers 端点一起提供,留空 = 用巩固模型或当前模型。
|
|
197
|
+
"sleepProvider",
|
|
198
|
+
"sleepModel",
|
|
199
|
+
// 实体抽取侧专用路由(issue #109):provider/model 显式指定,
|
|
200
|
+
// 留空 = 用当前默认模型。
|
|
201
|
+
"entityExtractionProvider",
|
|
202
|
+
"entityExtractionModel",
|
|
203
|
+
// Issue #258:总览(dream_summarize)专用路由,留空 = 用巩固模型。
|
|
204
|
+
"dreamSummaryProvider",
|
|
205
|
+
"dreamSummaryModel",
|
|
206
|
+
"localEmbedModel",
|
|
207
|
+
"ollamaModel",
|
|
208
|
+
// Issue #239 第 4 项:高峰时段串("09:00-18:00",空串 = 关闭)。
|
|
209
|
+
"summarizePeakHours",
|
|
210
|
+
// Issue #239 第 4 项镜像到巩固:同一份时段语法,空串 = 关闭(行为与现状一致)。
|
|
211
|
+
"dreamPeakHours"
|
|
212
|
+
];
|
|
213
|
+
// URL 字符串开关:trim 后必须为空或合法 http/https URL(new URL() 校验协议,
|
|
214
|
+
// 拒绝其余协议——这是 SSRF 防线的一部分)。
|
|
215
|
+
const FEATURE_FLAG_URLS = ["ollamaBaseUrl"];
|
|
216
|
+
// 枚举开关(与 config.js 的 z.union(z.const(...)) 对齐):仅允许列出的值。
|
|
217
|
+
const FEATURE_FLAG_ENUMS = {
|
|
218
|
+
embedProvider: ["openai", "local", "ollama"],
|
|
219
|
+
// Plan #1: recall fusion recipe. blend = legacy (default); rrf / minmax are
|
|
220
|
+
// rank/scale-aware alternatives selected by the panel.
|
|
221
|
+
recallFusion: ["blend", "rrf", "minmax"],
|
|
222
|
+
// 实体抽取思考强度(issue #109):与 dreamReasoningEffort 枚举对齐。
|
|
223
|
+
entityExtractionReasoning: ["low", "medium", "high", "none"],
|
|
224
|
+
// 蒸馏思考强度(issue #315):与 entityExtractionReasoning 枚举对齐,
|
|
225
|
+
// 多一个 off(显式关思考,思考型模型蒸馏防推理烧预算)。
|
|
226
|
+
summarizeReasoningEffort: ["off", "low", "medium", "high", "none"],
|
|
227
|
+
// Issue #127:落库前去重档位(off 默认,等同现状)。
|
|
228
|
+
summarizeDedupeMode: ["off", "title", "vector"],
|
|
229
|
+
// Issue #126:sleep 冲突阶段的动作集(conflict 默认 = 现状;full = 六分支)。
|
|
230
|
+
sleepActionSet: ["conflict", "full"],
|
|
231
|
+
// Issue #125:dream 候选集构造方式(window 默认 = 现状;hybrid 并入向量组)。
|
|
232
|
+
dreamCandidateMode: ["window", "hybrid"],
|
|
233
|
+
// 本地嵌入池化:auto = 按模型族判定(BGE → cls),可显式覆盖为 cls / mean。
|
|
234
|
+
localEmbedPooling: ["auto", "cls", "mean"]
|
|
235
|
+
};
|
|
236
|
+
const FEATURE_FLAG_STRING_MAX = 200;
|
|
237
|
+
|
|
238
|
+
// 供 api.js 复用同一份白名单(effective 只在白名单键上计算)。
|
|
239
|
+
export const FEATURE_FLAG_SPEC = {
|
|
240
|
+
booleans: FEATURE_FLAG_BOOLEANS,
|
|
241
|
+
ints: FEATURE_FLAG_INT_RANGES,
|
|
242
|
+
numbers: FEATURE_FLAG_NUMBER_RANGES,
|
|
243
|
+
strings: FEATURE_FLAG_STRINGS,
|
|
244
|
+
urls: FEATURE_FLAG_URLS,
|
|
245
|
+
enums: FEATURE_FLAG_ENUMS
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
/** ollamaBaseUrl 的协议白名单:只接受 http/https(SSRF 防线的一部分)。 */
|
|
249
|
+
function isHttpUrl(value) {
|
|
250
|
+
try {
|
|
251
|
+
const protocol = new URL(value).protocol;
|
|
252
|
+
return protocol === "http:" || protocol === "https:";
|
|
253
|
+
} catch {
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** 校验单个开关值;不合法抛 TypeError(消息含键名)。 */
|
|
259
|
+
function validateFlag(key, value) {
|
|
260
|
+
if (FEATURE_FLAG_BOOLEANS.includes(key)) {
|
|
261
|
+
if (typeof value !== "boolean") {
|
|
262
|
+
throw new TypeError(`feature flag "${key}" must be a boolean`);
|
|
263
|
+
}
|
|
264
|
+
return value;
|
|
265
|
+
}
|
|
266
|
+
const range = FEATURE_FLAG_INT_RANGES[key];
|
|
267
|
+
if (range) {
|
|
268
|
+
const [min, max] = range;
|
|
269
|
+
if (!Number.isInteger(value) || value < min || value > max) {
|
|
270
|
+
throw new TypeError(`feature flag "${key}" must be an integer in [${min}, ${max}]`);
|
|
271
|
+
}
|
|
272
|
+
return value;
|
|
273
|
+
}
|
|
274
|
+
const numRange = FEATURE_FLAG_NUMBER_RANGES[key];
|
|
275
|
+
if (numRange) {
|
|
276
|
+
const [min, max] = numRange;
|
|
277
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < min || value > max) {
|
|
278
|
+
throw new TypeError(`feature flag "${key}" must be a number in [${min}, ${max}]`);
|
|
279
|
+
}
|
|
280
|
+
return value;
|
|
281
|
+
}
|
|
282
|
+
if (FEATURE_FLAG_STRINGS.includes(key)) {
|
|
283
|
+
if (typeof value !== "string") {
|
|
284
|
+
throw new TypeError(`feature flag "${key}" must be a string`);
|
|
285
|
+
}
|
|
286
|
+
const trimmed = value.trim();
|
|
287
|
+
if (trimmed.length > FEATURE_FLAG_STRING_MAX) {
|
|
288
|
+
throw new TypeError(`feature flag "${key}" must be at most ${FEATURE_FLAG_STRING_MAX} characters`);
|
|
289
|
+
}
|
|
290
|
+
return trimmed; // 空串合法 = 跟随默认
|
|
291
|
+
}
|
|
292
|
+
if (FEATURE_FLAG_URLS.includes(key)) {
|
|
293
|
+
if (typeof value !== "string") {
|
|
294
|
+
throw new TypeError(`feature flag "${key}" must be a string`);
|
|
295
|
+
}
|
|
296
|
+
const trimmed = value.trim();
|
|
297
|
+
if (trimmed && !isHttpUrl(trimmed)) {
|
|
298
|
+
throw new TypeError(`feature flag "${key}" must be empty or a valid http(s) URL`);
|
|
299
|
+
}
|
|
300
|
+
return trimmed; // 空串合法 = 跟随默认
|
|
301
|
+
}
|
|
302
|
+
const allowed = FEATURE_FLAG_ENUMS[key];
|
|
303
|
+
if (allowed) {
|
|
304
|
+
if (typeof value !== "string" || !allowed.includes(value)) {
|
|
305
|
+
throw new TypeError(`feature flag "${key}" must be one of: ${allowed.join(", ")}`);
|
|
306
|
+
}
|
|
307
|
+
return value;
|
|
308
|
+
}
|
|
309
|
+
throw new TypeError(`unknown feature flag "${key}"`);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** 清洗已存的 feature_flags 对象:只保留白名单键,类型/范围损坏的键丢弃。 */
|
|
313
|
+
function sanitizeFlags(raw) {
|
|
314
|
+
const out = {};
|
|
315
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return out;
|
|
316
|
+
for (const key of FEATURE_FLAG_BOOLEANS) {
|
|
317
|
+
if (typeof raw[key] === "boolean") out[key] = raw[key];
|
|
318
|
+
}
|
|
319
|
+
for (const [key, [min, max]] of Object.entries(FEATURE_FLAG_INT_RANGES)) {
|
|
320
|
+
if (Number.isInteger(raw[key]) && raw[key] >= min && raw[key] <= max) out[key] = raw[key];
|
|
321
|
+
}
|
|
322
|
+
for (const [key, [min, max]] of Object.entries(FEATURE_FLAG_NUMBER_RANGES)) {
|
|
323
|
+
if (typeof raw[key] === "number" && Number.isFinite(raw[key]) && raw[key] >= min && raw[key] <= max) {
|
|
324
|
+
out[key] = raw[key];
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
for (const key of FEATURE_FLAG_STRINGS) {
|
|
328
|
+
if (typeof raw[key] === "string" && raw[key].trim().length <= FEATURE_FLAG_STRING_MAX) {
|
|
329
|
+
out[key] = raw[key].trim();
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
for (const key of FEATURE_FLAG_URLS) {
|
|
333
|
+
if (typeof raw[key] === "string") {
|
|
334
|
+
const trimmed = raw[key].trim();
|
|
335
|
+
if (!trimmed || isHttpUrl(trimmed)) out[key] = trimmed;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
for (const [key, allowed] of Object.entries(FEATURE_FLAG_ENUMS)) {
|
|
339
|
+
if (allowed.includes(raw[key])) out[key] = raw[key];
|
|
340
|
+
}
|
|
341
|
+
return out;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export function createSettings(db) {
|
|
345
|
+
db.exec(SCHEMA);
|
|
346
|
+
|
|
347
|
+
function getSetting(key) {
|
|
348
|
+
const row = db.prepare("SELECT value FROM user_settings WHERE key = ?").get(key);
|
|
349
|
+
return row?.value ?? undefined;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function setSetting(key, value) {
|
|
353
|
+
db.prepare(
|
|
354
|
+
`INSERT INTO user_settings (key, value) VALUES (?, ?)
|
|
355
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value`
|
|
356
|
+
).run(key, value);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function toCommand(row) {
|
|
360
|
+
if (!row) return undefined;
|
|
361
|
+
return {
|
|
362
|
+
id: row.id,
|
|
363
|
+
name: row.name,
|
|
364
|
+
description: row.description,
|
|
365
|
+
instruction: row.instruction,
|
|
366
|
+
created_at: row.created_at,
|
|
367
|
+
updated_at: row.updated_at
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return {
|
|
372
|
+
/** The user's self-description (free text) or "" when unset. */
|
|
373
|
+
getProfile() {
|
|
374
|
+
return getSetting("profile") ?? "";
|
|
375
|
+
},
|
|
376
|
+
setProfile(text) {
|
|
377
|
+
setSetting("profile", String(text ?? ""));
|
|
378
|
+
},
|
|
379
|
+
|
|
380
|
+
/** Behavior rules as an array of strings. */
|
|
381
|
+
getRules() {
|
|
382
|
+
return parseList(getSetting("rules") ?? "[]").filter((r) => typeof r === "string");
|
|
383
|
+
},
|
|
384
|
+
setRules(rules) {
|
|
385
|
+
const list = Array.isArray(rules) ? rules.filter((r) => typeof r === "string") : [];
|
|
386
|
+
setSetting("rules", JSON.stringify(list));
|
|
387
|
+
},
|
|
388
|
+
|
|
389
|
+
/** All custom commands, sorted by name. */
|
|
390
|
+
listCommands() {
|
|
391
|
+
const rows = db.prepare("SELECT * FROM custom_commands ORDER BY name ASC").all();
|
|
392
|
+
return rows.map(toCommand);
|
|
393
|
+
},
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Add or replace a custom command by name.
|
|
397
|
+
* @returns the stored command.
|
|
398
|
+
* @throws when name is invalid or does not match DSH's command-name grammar.
|
|
399
|
+
*/
|
|
400
|
+
addCommand({ name, description = "", instruction }) {
|
|
401
|
+
const cmdName = String(name ?? "").trim();
|
|
402
|
+
if (!COMMAND_NAME.test(cmdName)) {
|
|
403
|
+
throw new Error(`invalid command name "${cmdName}": must match /^[a-z][a-z0-9_-]*$/`);
|
|
404
|
+
}
|
|
405
|
+
if (typeof instruction !== "string" || !instruction.trim()) {
|
|
406
|
+
throw new Error("command instruction must be a non-empty string");
|
|
407
|
+
}
|
|
408
|
+
const now = new Date().toISOString();
|
|
409
|
+
const existing = db.prepare("SELECT id FROM custom_commands WHERE name = ?").get(cmdName);
|
|
410
|
+
if (existing) {
|
|
411
|
+
db.prepare(
|
|
412
|
+
"UPDATE custom_commands SET description = ?, instruction = ?, updated_at = ? WHERE id = ?"
|
|
413
|
+
).run(String(description ?? ""), instruction, now, existing.id);
|
|
414
|
+
return toCommand(db.prepare("SELECT * FROM custom_commands WHERE id = ?").get(existing.id));
|
|
415
|
+
}
|
|
416
|
+
const id = randomUUID();
|
|
417
|
+
db.prepare(
|
|
418
|
+
`INSERT INTO custom_commands (id, name, description, instruction, created_at, updated_at)
|
|
419
|
+
VALUES (?, ?, ?, ?, ?, ?)`
|
|
420
|
+
).run(id, cmdName, String(description ?? ""), instruction, now, now);
|
|
421
|
+
return toCommand(db.prepare("SELECT * FROM custom_commands WHERE id = ?").get(id));
|
|
422
|
+
},
|
|
423
|
+
|
|
424
|
+
/** Remove a custom command by id; returns true when removed. */
|
|
425
|
+
removeCommand(id) {
|
|
426
|
+
const result = db.prepare("DELETE FROM custom_commands WHERE id = ?").run(id);
|
|
427
|
+
return result.changes > 0;
|
|
428
|
+
},
|
|
429
|
+
|
|
430
|
+
/** Vector-search provider config (OpenAI-compatible embeddings endpoint). */
|
|
431
|
+
getVectorConfig() {
|
|
432
|
+
const raw = getSetting("vector");
|
|
433
|
+
if (!raw) return undefined;
|
|
434
|
+
try {
|
|
435
|
+
const cfg = JSON.parse(raw);
|
|
436
|
+
return typeof cfg === "object" && cfg !== null ? cfg : undefined;
|
|
437
|
+
} catch {
|
|
438
|
+
return undefined;
|
|
439
|
+
}
|
|
440
|
+
},
|
|
441
|
+
setVectorConfig({ enabled, baseUrl, apiKey, model }) {
|
|
442
|
+
const cfg = {
|
|
443
|
+
enabled: enabled === true || enabled === 1,
|
|
444
|
+
baseUrl: String(baseUrl ?? "").trim().replace(/\/+$/, ""),
|
|
445
|
+
apiKey: String(apiKey ?? "").trim(),
|
|
446
|
+
model: String(model ?? "").trim()
|
|
447
|
+
};
|
|
448
|
+
setSetting("vector", JSON.stringify(cfg));
|
|
449
|
+
return cfg;
|
|
450
|
+
},
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Standalone external API settings (kv "external_api"): {enabled, port,
|
|
454
|
+
* token}. The Bearer token is auto-generated on first boot and persisted
|
|
455
|
+
* here. Partial writes preserve the keys they don't mention.
|
|
456
|
+
*/
|
|
457
|
+
getExternalApi() {
|
|
458
|
+
const raw = getSetting("external_api");
|
|
459
|
+
if (!raw) return undefined;
|
|
460
|
+
try {
|
|
461
|
+
const cfg = JSON.parse(raw);
|
|
462
|
+
return typeof cfg === "object" && cfg !== null ? cfg : undefined;
|
|
463
|
+
} catch {
|
|
464
|
+
return undefined;
|
|
465
|
+
}
|
|
466
|
+
},
|
|
467
|
+
setExternalApi(patch = {}) {
|
|
468
|
+
const prev = this.getExternalApi() ?? {};
|
|
469
|
+
const port = Number(patch.port ?? prev.port);
|
|
470
|
+
const host = typeof patch.host === "string" && patch.host.trim() ? patch.host.trim() : (prev.host ?? "127.0.0.1");
|
|
471
|
+
const cfg = {
|
|
472
|
+
enabled: patch.enabled !== undefined ? patch.enabled === true : prev.enabled === true,
|
|
473
|
+
port: Number.isInteger(port) && port > 0 ? port : 8790,
|
|
474
|
+
host,
|
|
475
|
+
token: String(patch.token ?? prev.token ?? "")
|
|
476
|
+
};
|
|
477
|
+
setSetting("external_api", JSON.stringify(cfg));
|
|
478
|
+
return cfg;
|
|
479
|
+
},
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Web panel mode (kv "panel_mode"): "light" (low-resource preset) or
|
|
483
|
+
* "standard" (full feature set). Unset reads as "standard".
|
|
484
|
+
*/
|
|
485
|
+
getPanelMode() {
|
|
486
|
+
return getSetting("panel_mode") === "light" ? "light" : "standard";
|
|
487
|
+
},
|
|
488
|
+
setPanelMode(mode) {
|
|
489
|
+
setSetting("panel_mode", mode === "light" ? "light" : "standard");
|
|
490
|
+
},
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Feature flags(kv "feature_flags"):面板对后端能力的显式覆盖。读取只
|
|
494
|
+
* 返回白名单内的合法键(默认 {}),写入是逐键校验后的合并持久化。
|
|
495
|
+
*/
|
|
496
|
+
getFeatureFlags() {
|
|
497
|
+
const raw = getSetting("feature_flags");
|
|
498
|
+
if (!raw) return {};
|
|
499
|
+
try {
|
|
500
|
+
return sanitizeFlags(JSON.parse(raw));
|
|
501
|
+
} catch {
|
|
502
|
+
return {};
|
|
503
|
+
}
|
|
504
|
+
},
|
|
505
|
+
setFeatureFlags(patch) {
|
|
506
|
+
if (typeof patch !== "object" || patch === null || Array.isArray(patch)) {
|
|
507
|
+
throw new TypeError("feature flags patch must be a plain object");
|
|
508
|
+
}
|
|
509
|
+
const merged = this.getFeatureFlags();
|
|
510
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
511
|
+
merged[key] = validateFlag(key, value);
|
|
512
|
+
}
|
|
513
|
+
setSetting("feature_flags", JSON.stringify(merged));
|
|
514
|
+
return merged;
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
}
|