@yfwu2020/dsh-selection-explain 0.7.1 → 0.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -5
- package/cordis.patch.yml +2 -2
- package/lib/client.js +1440 -74
- package/lib/index.js +366 -25
- package/lib/index.js.map +1 -1
- package/lib/settings.js +228 -0
- package/lib/settings.js.map +1 -0
- package/lib/types/index.d.ts +37 -6
- package/lib/types/settings.d.ts +125 -0
- package/package.json +2 -2
- package/scripts/smoke-panel.mjs +46 -1
- package/scripts/smoke-voice.mjs +3 -1
- package/scripts/test-bridge.mjs +4 -0
- package/scripts/test-client.mjs +643 -7
- package/scripts/test-model-follow.mjs +193 -0
- package/scripts/test-route-cache.mjs +138 -0
- package/scripts/test-settings-behavior.mjs +188 -0
- package/scripts/test-settings-host.mjs +48 -0
- package/scripts/test-settings.mjs +329 -0
- package/src/client/index.js +1440 -74
- package/src/index.ts +402 -20
- package/src/settings.ts +310 -0
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 设置模块契约测试。
|
|
3
|
+
*
|
|
4
|
+
* 保护三件事:
|
|
5
|
+
* ① spec 与 Config **不漂**:设置页能改的键,必须个个都是 Config 里真实存在的字段,
|
|
6
|
+
* 且不能漏掉任何一个可改字段 —— 漏了就是"设置页里找不到这一项",属于静默丢功能。
|
|
7
|
+
* ② 归一函数的边界行为:clamp / 拒未知键 / 拒错类型 / seg 白名单 / text 去空白与超长。
|
|
8
|
+
* ③ 分组结构可用:id 唯一、每项都有 label 与 hint(设置页的说明栏全靠它)。
|
|
9
|
+
*
|
|
10
|
+
* 读取方式与 test-prompt.mjs 一致:Node ≥ 22.14 直接 import src/settings.ts(类型剥离);
|
|
11
|
+
* 旧的 Node 就退化成"读源码文本 + 解析",保证任何环境都能跑。
|
|
12
|
+
*/
|
|
13
|
+
import { readFileSync } from 'node:fs'
|
|
14
|
+
import { resolve, dirname } from 'node:path'
|
|
15
|
+
import { fileURLToPath } from 'node:url'
|
|
16
|
+
|
|
17
|
+
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
18
|
+
const ROOT = resolve(HERE, '..')
|
|
19
|
+
const SETTINGS_TS = resolve(ROOT, 'src', 'settings.ts')
|
|
20
|
+
const INDEX_TS = resolve(ROOT, 'src', 'index.ts')
|
|
21
|
+
|
|
22
|
+
let SETTINGS_GROUPS
|
|
23
|
+
let SETTINGS_KEYS
|
|
24
|
+
let SETTINGS_VIRTUAL_KEYS
|
|
25
|
+
let SETTINGS_ITEMS
|
|
26
|
+
let normalizeSettingsPatch
|
|
27
|
+
let normalizeStoredSettings
|
|
28
|
+
let buildSettingsPayload
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 载入 spec 模块(三档降级,保证在任何 Node 上都能跑):
|
|
32
|
+
*
|
|
33
|
+
* ① `import('src/settings.ts')` —— Node ≥ 22.18 / 23.6 默认开类型剥离(本机 Node 24 走这条);
|
|
34
|
+
* ② Node 22.6~22.17 的类型剥离要显式开关(**CI 钉的是 22.14**):
|
|
35
|
+
* `--experimental-strip-types` 可用时,用它把**自己**重跑一遍 —— 仍然测的是 src;
|
|
36
|
+
* ③ 连开关都没有的旧 Node → 退到构建产物 `lib/settings.js`(`npm test` 本来就跑在
|
|
37
|
+
* `build.sh` 之后:test-settings-host / test-settings-behavior 也是直接 import lib 的)。
|
|
38
|
+
*
|
|
39
|
+
* ⚠️ v0.8.3 发布时踩到过:这里原本只有 ①,`catch` 里直接报错退出 ——
|
|
40
|
+
* CI(Node 22.14)于是报 `Unknown file extension ".ts"`、整个 `npm test` 链断在这儿。
|
|
41
|
+
*/
|
|
42
|
+
let loader = process.execArgv.includes('--experimental-strip-types')
|
|
43
|
+
? 'import src/settings.ts(--experimental-strip-types)'
|
|
44
|
+
: 'import src/settings.ts(Node 默认类型剥离)'
|
|
45
|
+
let mod = null
|
|
46
|
+
try {
|
|
47
|
+
mod = await import(SETTINGS_TS)
|
|
48
|
+
} catch (error) {
|
|
49
|
+
const strip = '--experimental-strip-types'
|
|
50
|
+
const canStrip = process.allowedNodeEnvironmentFlags.has(strip) && !process.execArgv.includes(strip)
|
|
51
|
+
if (canStrip) {
|
|
52
|
+
// ② 用开关把自己重跑一遍(stdio 直连,退出码照传)
|
|
53
|
+
const { spawnSync } = await import('node:child_process')
|
|
54
|
+
const again = spawnSync(process.execPath, [strip, fileURLToPath(import.meta.url)], { stdio: 'inherit' })
|
|
55
|
+
process.exit(again.status ?? 1)
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
// ③ 构建产物(build.sh 之后才有;CI 是先构建再测试)
|
|
59
|
+
mod = await import(resolve(ROOT, 'lib', 'settings.js'))
|
|
60
|
+
loader = `构建产物 lib/settings.js(类型剥离不可用:${error && error.message})`
|
|
61
|
+
} catch (fallbackError) {
|
|
62
|
+
console.error(`✗ 无法加载 src/settings.ts(${error && error.message})`)
|
|
63
|
+
console.error(` 也没有构建产物 lib/settings.js(${fallbackError && fallbackError.message})—— 先 npm run build`)
|
|
64
|
+
process.exit(1)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
console.log(`设置 spec 来源:${loader}\n`)
|
|
68
|
+
|
|
69
|
+
SETTINGS_GROUPS = mod.SETTINGS_GROUPS
|
|
70
|
+
SETTINGS_KEYS = mod.SETTINGS_KEYS
|
|
71
|
+
SETTINGS_VIRTUAL_KEYS = mod.SETTINGS_VIRTUAL_KEYS
|
|
72
|
+
SETTINGS_ITEMS = mod.SETTINGS_ITEMS
|
|
73
|
+
normalizeSettingsPatch = mod.normalizeSettingsPatch
|
|
74
|
+
normalizeStoredSettings = mod.normalizeStoredSettings
|
|
75
|
+
buildSettingsPayload = mod.buildSettingsPayload
|
|
76
|
+
|
|
77
|
+
let failed = 0
|
|
78
|
+
function assert(label, ok, detail) {
|
|
79
|
+
if (ok) {
|
|
80
|
+
console.log(` ✓ ${label}`)
|
|
81
|
+
} else {
|
|
82
|
+
failed += 1
|
|
83
|
+
console.log(` ✗ ${label}${detail === undefined ? '' : ` → ${detail}`}`)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ───────────────────────── ① spec ↔ Config 不漂 ─────────────────────────
|
|
88
|
+
console.log('\n[1] spec 与 host Config 对齐')
|
|
89
|
+
|
|
90
|
+
const indexSrc = readFileSync(INDEX_TS, 'utf8')
|
|
91
|
+
|
|
92
|
+
/** 从 index.ts 里抠出 `export interface Config { ... }` 的字段名。 */
|
|
93
|
+
function configInterfaceKeys(src) {
|
|
94
|
+
const start = src.indexOf('export interface Config {')
|
|
95
|
+
if (start < 0) return null
|
|
96
|
+
// 从 { 开始配对花括号,避免被字段里的对象类型提前截断
|
|
97
|
+
let i = src.indexOf('{', start)
|
|
98
|
+
let depth = 0
|
|
99
|
+
let end = -1
|
|
100
|
+
for (let j = i; j < src.length; j += 1) {
|
|
101
|
+
if (src[j] === '{') depth += 1
|
|
102
|
+
else if (src[j] === '}') {
|
|
103
|
+
depth -= 1
|
|
104
|
+
if (depth === 0) { end = j; break }
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (end < 0) return null
|
|
108
|
+
const body = src.slice(i + 1, end)
|
|
109
|
+
// 顶层字段:`name: type`(跳过注释行、跳过嵌套行——嵌套的缩进更深)
|
|
110
|
+
const keys = []
|
|
111
|
+
for (const rawLine of body.split('\n')) {
|
|
112
|
+
const line = rawLine.replace(/\/\/.*$/, '')
|
|
113
|
+
const m = /^\s{2}([A-Za-z_$][\w$]*)\s*:\s*(.+)$/.exec(line)
|
|
114
|
+
if (!m) continue
|
|
115
|
+
keys.push(m[1])
|
|
116
|
+
}
|
|
117
|
+
return keys
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** 从 index.ts 里抠出 `export const Config = z.object({ ... })` 的键。 */
|
|
121
|
+
function configSchemaKeys(src) {
|
|
122
|
+
const at = src.indexOf('export const Config = z.object({')
|
|
123
|
+
if (at < 0) return null
|
|
124
|
+
let i = src.indexOf('{', at)
|
|
125
|
+
let depth = 0
|
|
126
|
+
let end = -1
|
|
127
|
+
for (let j = i; j < src.length; j += 1) {
|
|
128
|
+
if (src[j] === '{') depth += 1
|
|
129
|
+
else if (src[j] === '}') {
|
|
130
|
+
depth -= 1
|
|
131
|
+
if (depth === 0) { end = j; break }
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (end < 0) return null
|
|
135
|
+
const body = src.slice(i + 1, end)
|
|
136
|
+
const keys = []
|
|
137
|
+
for (const rawLine of body.split('\n')) {
|
|
138
|
+
const line = rawLine.replace(/\/\/.*$/, '')
|
|
139
|
+
const m = /^\s{2}([A-Za-z_$][\w$]*)\s*:/.exec(line)
|
|
140
|
+
if (!m) continue
|
|
141
|
+
// z.xxx() 这种才是字段;`/** ... */` 与嵌套对象跳过(嵌套行缩进更深)
|
|
142
|
+
if (!/z\.[A-Za-z]/.test(line)) continue
|
|
143
|
+
keys.push(m[1])
|
|
144
|
+
}
|
|
145
|
+
return keys
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const ifaceKeys = configInterfaceKeys(indexSrc)
|
|
149
|
+
const schemaKeys = configSchemaKeys(indexSrc)
|
|
150
|
+
|
|
151
|
+
assert('能解析出 Config interface 的字段', Array.isArray(ifaceKeys) && ifaceKeys.length > 0, ifaceKeys ? `${ifaceKeys.length} 个` : '解析失败')
|
|
152
|
+
assert('能解析出 Config schema 的字段', Array.isArray(schemaKeys) && schemaKeys.length > 0, schemaKeys ? `${schemaKeys.length} 个` : '解析失败')
|
|
153
|
+
|
|
154
|
+
// ① -a 每个**可写**键都必须是真实的 Config 字段
|
|
155
|
+
// (虚拟键如 modelChoice 不出现在 SETTINGS_KEYS 里,它已被展开成 provider + model)
|
|
156
|
+
{
|
|
157
|
+
const missing = SETTINGS_KEYS.filter((key) => ifaceKeys && !ifaceKeys.includes(key))
|
|
158
|
+
assert(
|
|
159
|
+
'设置页可写的每个键都是 Config 的真实字段(没有拼错的)',
|
|
160
|
+
missing.length === 0,
|
|
161
|
+
missing.length ? `Config 里没有:${missing.join(', ')}` : `${SETTINGS_KEYS.length} 个键全部命中`,
|
|
162
|
+
)
|
|
163
|
+
// 反过来:每个 spec 条目的 key 也必须"有名有姓"——要么是真实字段,要么声明了 keys
|
|
164
|
+
const loose = SETTINGS_ITEMS.filter(
|
|
165
|
+
(item) => ifaceKeys && !ifaceKeys.includes(item.key) && !Array.isArray(item.keys),
|
|
166
|
+
).map((item) => item.key)
|
|
167
|
+
assert(
|
|
168
|
+
'spec 里没有"既不认识、也没说明覆盖谁"的字段',
|
|
169
|
+
loose.length === 0,
|
|
170
|
+
loose.length ? `来路不明:${loose.join(', ')}` : '全部有出处',
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ① -b Config 里每个字段都要在设置页露面(漏了就等于"改不了")
|
|
175
|
+
{
|
|
176
|
+
const hidden = ['chatProvider', 'chatModel', 'maxTokens', 'temperature', 'resultCacheTtlMs']
|
|
177
|
+
const missing = (ifaceKeys ?? []).filter((key) => !SETTINGS_KEYS.includes(key) && !hidden.includes(key))
|
|
178
|
+
assert(
|
|
179
|
+
'可见配置均可修改,明确移除的兼容字段不展示',
|
|
180
|
+
missing.length === 0,
|
|
181
|
+
missing.length ? `设置页缺:${missing.join(', ')}` : `${(ifaceKeys ?? []).length} 个字段全部覆盖`,
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ① -c interface 与 schema 必须同集合(漂了就是"类型说有、实际不生效")
|
|
186
|
+
{
|
|
187
|
+
const onlyIface = (ifaceKeys ?? []).filter((key) => schemaKeys && !schemaKeys.includes(key))
|
|
188
|
+
const onlySchema = (schemaKeys ?? []).filter((key) => ifaceKeys && !ifaceKeys.includes(key))
|
|
189
|
+
assert(
|
|
190
|
+
'Config 的 interface 与 z.object schema 字段一致',
|
|
191
|
+
onlyIface.length === 0 && onlySchema.length === 0,
|
|
192
|
+
`仅 interface:${onlyIface.join(', ') || '无'}|仅 schema:${onlySchema.join(', ') || '无'}`,
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ───────────────────────── ② 分组结构 ─────────────────────────
|
|
197
|
+
console.log('\n[2] 分组结构')
|
|
198
|
+
{
|
|
199
|
+
const ids = SETTINGS_GROUPS.map((g) => g.id)
|
|
200
|
+
assert('分组 id 唯一', new Set(ids).size === ids.length, ids.join(', '))
|
|
201
|
+
|
|
202
|
+
const allItems = SETTINGS_ITEMS
|
|
203
|
+
const keys = allItems.map((i) => i.key)
|
|
204
|
+
assert('字段 key 全局唯一(没有在两个分组里重复出现)', new Set(keys).size === keys.length,
|
|
205
|
+
keys.filter((k, idx) => keys.indexOf(k) !== idx).join(', ') || '无重复')
|
|
206
|
+
|
|
207
|
+
assert('每项都有非空 label', allItems.every((i) => typeof i.label === 'string' && i.label.length > 0))
|
|
208
|
+
assert('说明为字符串,允许省略重复说明', allItems.every((i) => typeof i.hint === 'string'))
|
|
209
|
+
assert('设置只保留一个模型选择', allItems.filter(i => i.kind === 'model').length === 1)
|
|
210
|
+
assert('移除输出上限、温度、缓存与独立追问模型', !allItems.some(i => ['maxTokens', 'temperature', 'resultCacheTtlMs', 'chatModelChoice'].includes(i.key)))
|
|
211
|
+
for (const group of SETTINGS_GROUPS) {
|
|
212
|
+
const visible = (group.sections || []).flatMap(section => section.keys)
|
|
213
|
+
assert(group.label + ' 的页内分组覆盖每个字段一次', visible.length === group.items.length && new Set(visible).size === visible.length && group.items.every(i => visible.includes(i.key)))
|
|
214
|
+
assert(group.label + ' 的禁用依赖指向开关', group.items.every(i => !i.enabledBy || group.items.some(parent => parent.key === i.enabledBy && parent.kind === 'switch')))
|
|
215
|
+
}
|
|
216
|
+
assert('每组都有非空 label', SETTINGS_GROUPS.every((g) => typeof g.label === 'string' && g.label.length > 0))
|
|
217
|
+
{
|
|
218
|
+
const expanded = allItems.flatMap((i) => i.keys ?? [i.key])
|
|
219
|
+
assert(
|
|
220
|
+
'SETTINGS_KEYS = 各条目展开(虚拟键展开成它覆盖的真实字段)',
|
|
221
|
+
SETTINGS_KEYS.length === expanded.length && SETTINGS_KEYS.every((k) => expanded.includes(k)),
|
|
222
|
+
`${SETTINGS_KEYS.length} vs ${expanded.length}`,
|
|
223
|
+
)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const segs = allItems.filter((i) => i.kind === 'seg')
|
|
227
|
+
assert('每个 seg 都有 options', segs.every((i) => Array.isArray(i.options) && i.options.length > 0),
|
|
228
|
+
segs.filter((i) => !(i.options || []).length).map((i) => i.key).join(', ') || '无')
|
|
229
|
+
|
|
230
|
+
const nums = allItems.filter((i) => i.kind === 'number')
|
|
231
|
+
assert('每个 number 都有 min/max 且 min < max',
|
|
232
|
+
nums.every((i) => typeof i.min === 'number' && typeof i.max === 'number' && i.min < i.max),
|
|
233
|
+
nums.filter((i) => !(i.min < i.max)).map((i) => i.key).join(', ') || '无')
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ───────────────────────── ③ 归一函数 ─────────────────────────
|
|
237
|
+
console.log('\n[3] normalizeSettingsPatch')
|
|
238
|
+
|
|
239
|
+
{
|
|
240
|
+
const r = normalizeSettingsPatch({ maxSelectionChars: 999999 })
|
|
241
|
+
assert('number 超上限被 clamp', r.values.maxSelectionChars === 20000, String(r.values.maxSelectionChars))
|
|
242
|
+
|
|
243
|
+
const r2 = normalizeSettingsPatch({ maxSelectionChars: 1 })
|
|
244
|
+
assert('number 低于下限被 clamp', r2.values.maxSelectionChars === 20, String(r2.values.maxSelectionChars))
|
|
245
|
+
|
|
246
|
+
const r3 = normalizeSettingsPatch({ nope: 1 })
|
|
247
|
+
assert('未知键被拒(不能往配置里塞任意字段)',
|
|
248
|
+
r3.rejected.length === 1 && r3.rejected[0].key === 'nope' && Object.keys(r3.values).length === 0)
|
|
249
|
+
|
|
250
|
+
const r4 = normalizeSettingsPatch({ tools: 'true' })
|
|
251
|
+
assert('switch 只接受真布尔(字符串不猜)', r4.rejected.length === 1 && r4.values.tools === undefined)
|
|
252
|
+
|
|
253
|
+
const r5 = normalizeSettingsPatch({ reasoningEffort: 'max' })
|
|
254
|
+
assert('seg 接受白名单内的值', r5.values.reasoningEffort === 'max')
|
|
255
|
+
|
|
256
|
+
const r6 = normalizeSettingsPatch({ reasoningEffort: 'ultra' })
|
|
257
|
+
assert('seg 拒绝白名单外的值', r6.rejected.length === 1 && r6.values.reasoningEffort === undefined)
|
|
258
|
+
|
|
259
|
+
const r7 = normalizeSettingsPatch({ toolNames: ' advanced_search, platform_search ' })
|
|
260
|
+
assert('text 去掉首尾空白', r7.values.toolNames === 'advanced_search, platform_search', JSON.stringify(r7.values.toolNames))
|
|
261
|
+
|
|
262
|
+
const r8 = normalizeSettingsPatch({ toolNames: 'x'.repeat(3000) })
|
|
263
|
+
assert('text 超长被拒', r8.rejected.length === 1 && r8.values.toolNames === undefined)
|
|
264
|
+
|
|
265
|
+
// 虚拟键(模型下拉)不能直接写:它必须展开成 provider + model 再提交,
|
|
266
|
+
// 否则会悄悄落进一个并不存在的配置字段
|
|
267
|
+
const rv = normalizeSettingsPatch({ modelChoice: 'opencode-go/deepseek-v4.1-flash' })
|
|
268
|
+
assert('虚拟键 modelChoice 被拒(要求分别提交 provider/model)',
|
|
269
|
+
rv.rejected.length === 1 && rv.values.modelChoice === undefined && rv.values.provider === undefined,
|
|
270
|
+
JSON.stringify(rv.rejected))
|
|
271
|
+
|
|
272
|
+
// 模型下拉实际提交的两个键都要能过
|
|
273
|
+
const rm = normalizeSettingsPatch({ provider: 'opencode-go', model: 'deepseek-v4.1-flash' })
|
|
274
|
+
assert('模型下拉展开成 provider + model 后可写入',
|
|
275
|
+
rm.values.provider === 'opencode-go' && rm.values.model === 'deepseek-v4.1-flash' && rm.rejected.length === 0,
|
|
276
|
+
JSON.stringify(rm.values))
|
|
277
|
+
|
|
278
|
+
const r9 = normalizeSettingsPatch({ tools: false, pillIdleMs: 500 })
|
|
279
|
+
assert('一次多个键各自独立处理(坏的拒、好的留)',
|
|
280
|
+
r9.values.tools === false && r9.values.pillIdleMs === 2000 && r9.rejected.length === 0,
|
|
281
|
+
JSON.stringify(r9.values))
|
|
282
|
+
|
|
283
|
+
const r10 = normalizeSettingsPatch(null)
|
|
284
|
+
assert('非对象输入被拒且不抛异常', r10.ok === false && Object.keys(r10.values).length === 0, JSON.stringify(r10.rejected))
|
|
285
|
+
|
|
286
|
+
const r11 = normalizeSettingsPatch({ temperature: -1, chatModel: 'old', resultCacheTtlMs: 200 })
|
|
287
|
+
assert('隐藏的兼容配置不再允许设置页写入', r11.rejected.length === 3)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ───────────────────────── ④ 会话级模型解析(host 源码契约) ─────────────────────────
|
|
291
|
+
// 用户报的问题:括号里显示的模型跟所在会话对不上。
|
|
292
|
+
// 根因:插件解析路由时只取 agentDefaultModel.currentSelection()(**全局默认**),
|
|
293
|
+
// 而会话是各自选模型的 —— 两个会话用不同模型时,插件对两个都用同一个默认值。
|
|
294
|
+
// 修法:从会话原始事件里读最后一条 model/selection,按会话解析。
|
|
295
|
+
console.log('\n[4] 会话级模型解析')
|
|
296
|
+
{
|
|
297
|
+
assert('resolveRoute 接受「本会话的模型」参数',
|
|
298
|
+
/const resolveRoute = \([\s\S]{0,900}?sessionModel\?/.test(indexSrc),
|
|
299
|
+
'resolveRoute 签名里没有 sessionModel')
|
|
300
|
+
assert('会话模型的优先级在插件配置之后、全局默认之前',
|
|
301
|
+
/config\.provider && config\.model\) return[\s\S]{0,400}?sessionModel\?\.provider[\s\S]{0,400}?currentSelection/.test(indexSrc),
|
|
302
|
+
'优先级顺序不对(应为 配置 → 会话 → 全局默认)')
|
|
303
|
+
assert('从会话原始事件里读 model/selection(不是消息表面)',
|
|
304
|
+
/readSession/.test(indexSrc) && /typed\.type !== 'model\/selection'/.test(indexSrc),
|
|
305
|
+
'没有从原始事件里取 model/selection')
|
|
306
|
+
assert('/models 与 /ping 都按 sessionId 解析',
|
|
307
|
+
/resolveRoute\(undefined, await sessionModelOf\(modelsSessionId\)\)/.test(indexSrc) &&
|
|
308
|
+
/resolveRoute\(undefined, await sessionModelOf\(pingSessionId\)\)/.test(indexSrc),
|
|
309
|
+
'/models 或 /ping 没接会话')
|
|
310
|
+
assert('analyze 按请求体的 sessionId 解析',
|
|
311
|
+
/resolveRoute\(override, await sessionModelOf\(sessionIdOf\(body\)\), isChatRound\)/.test(indexSrc),
|
|
312
|
+
'analyze 没接会话')
|
|
313
|
+
assert('旧追问配置不能覆盖统一模型', !/if \(chat && config\.chatProvider/.test(indexSrc))
|
|
314
|
+
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ───────────────────────── ⑤ payload ─────────────────────────
|
|
318
|
+
console.log('\n[5] buildSettingsPayload')
|
|
319
|
+
{
|
|
320
|
+
const values = { tools: true }
|
|
321
|
+
const defaults = { tools: true, provider: '' }
|
|
322
|
+
const p = buildSettingsPayload(values, defaults)
|
|
323
|
+
assert('payload 带上 groups(客户端据此渲染,不做业务判断)', Array.isArray(p.groups) && p.groups.length === SETTINGS_GROUPS.length)
|
|
324
|
+
assert('payload 带上 values 与 defaults', p.values === values && p.defaults === defaults)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
assert('旧配置的隐藏数值继续保留,独立追问模型不再加载', JSON.stringify(normalizeStoredSettings({ maxTokens: 1200, temperature: 0.3, resultCacheTtlMs: 600, chatProvider: 'old', chatModel: 'old', provider: 'new', model: 'shared' })) === JSON.stringify({ provider: 'new', model: 'shared', maxTokens: 1200, temperature: 0.3, resultCacheTtlMs: 600 }))
|
|
328
|
+
console.log(failed === 0 ? '\n全部通过 ✓' : `\n${failed} 项失败 ✗`)
|
|
329
|
+
process.exit(failed === 0 ? 0 : 1)
|