@kikkimo/claude-launcher 3.1.0 → 3.2.0

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.
@@ -0,0 +1,949 @@
1
+ # Provider 模型升级 Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** 将 5 个 provider(anthropic、deepseek、minimax、moonshot、zhipu/zai)的模型定义升级到各家官方最新口径,新增 GLM 固定 tier template,并为已保存的旧 API 配置增加 template 漂移自动迁移。
6
+
7
+ **Architecture:** 改动集中在 `lib/presets/providers.js`(provider 定义 + 新增 `makeGlmTierTemplate` factory)与 `lib/api-manager.js`(`_normalizeApiFields` 内新增漂移检测块)。漂移检测复用 `updateApiField()` 已验证的「保留用户手动覆盖」模式,改写的 3 个字段(`modelEnvVars`/`_autoModelEnvVars`/`smallFastModel`)已被 `_migrateApiEntry` 的 before/after 比较覆盖,自动触发构造函数 `saveConfig()`,无需新增保存点。
8
+
9
+ **Tech Stack:** Node.js ≥20,CommonJS,原生 `assert` 测试框架(每个 test 文件是独立 `node` 脚本,`npm test` 用 `&&` 串联 14 个文件)。无外部依赖。
10
+
11
+ **Spec:** `docs/superpowers/specs/2026-06-14-provider-model-upgrade-design.md`(经 Codex gpt-5.5/xhigh 评审 3 轮通过)
12
+
13
+ ---
14
+
15
+ ## File Structure
16
+
17
+ | 文件 | 职责 | 本次改动 |
18
+ |------|------|---------|
19
+ | `lib/presets/providers.js` | 9 个 provider 预设、template factory、`getLatestModel` | 新增 `makeGlmTierTemplate`;改 anthropic/deepseek/minimax/moonshot/zhipu/zai 六个 provider 定义 |
20
+ | `lib/api-manager.js` | API 配置增删改查、加载迁移、加密持久化 | `_normalizeApiFields`(约 :165-169 之后)新增 template 漂移检测块 |
21
+ | `test/providers.test.js` | provider 模型列表 / versionAliases / 旗舰豁免不变量 | 更新 5 个 provider 的断言期望值 |
22
+ | `test/env-vars-providers.test.js` | modelEnvTemplate.getValues / env vars 断言 | 更新断言 + 新增 GLM tier template 测试 + 新增 moonshot 覆盖优先级测试 |
23
+ | `test/api-manager.test.js` | ApiManager 方法测试 | 新增 3 个漂移迁移测试 |
24
+ | `package.json` | 版本号 | `3.1.0` → `3.2.0` |
25
+ | `CHANGELOG.md` | 变更日志 | 新增 `[3.2.0]` 段 |
26
+
27
+ **不变量(被现有测试自动守护,改动后必须仍成立):**
28
+ - `providers[<id>].models[0]`(旗舰)不得出现在该 provider 的 `versionAliases` key 中(`test/providers.test.js:192-202` 的 `invariant` 测试)
29
+ - `zai` 与 `zhipu`、`minimax_global` 与 `minimax_cn` 的 `models`/`versionAliases` 必须镜像一致
30
+
31
+ **测试运行约定:**
32
+ - 单文件跑:`node test/providers.test.js`(TDD 红绿循环用)
33
+ - 全量跑:`npm test`(14 个文件 `&&` 链,最后一个 task 前必须全绿)
34
+
35
+ **Git 提交约定:**
36
+ - 每个 Task 末尾提交一次,commit message 用 `feat:`/`fix:`/`refactor:`/`docs:` 前缀
37
+ - **不加** `Co-Authored-By`(项目约定)
38
+
39
+ ---
40
+
41
+ ## Task 1: anthropic — 升级 opus 到 claude-opus-4-8
42
+
43
+ **Files:**
44
+ - Modify: `lib/presets/providers.js:43-65`(`anthropicModels` + `anthropic.versionAliases`)
45
+ - Modify: `test/providers.test.js:162-174`
46
+ - Modify: `test/env-vars-providers.test.js:11-34, 105-113`
47
+
48
+ - [ ] **Step 1: 更新 `test/providers.test.js` 的 anthropic 断言(先红)**
49
+
50
+ 用 Edit 把这段(约 :162-174):
51
+ ```js
52
+ test('anthropic: models include new and legacy models', () => {
53
+ const p = getProvider('anthropic');
54
+ assert.ok(p.models.includes('claude-opus-4-7'));
55
+ assert.ok(p.models.includes('claude-sonnet-4-6'));
56
+ assert.ok(p.models.includes('claude-haiku-4-5-20251001'));
57
+ assert.ok(p.models.includes('claude-opus-4-6'));
58
+ assert.ok(p.models.includes('claude-sonnet-4-5'));
59
+ });
60
+
61
+ test('anthropic: versionAliases map opus series to opus-4-7', () => {
62
+ assert.strictEqual(getLatestModel('claude-opus-4', 'anthropic'), 'claude-opus-4-7');
63
+ assert.strictEqual(getLatestModel('claude-opus-4-6', 'anthropic'), 'claude-opus-4-7');
64
+ });
65
+ ```
66
+ 替换为:
67
+ ```js
68
+ test('anthropic: models include new and legacy models', () => {
69
+ const p = getProvider('anthropic');
70
+ assert.ok(p.models.includes('claude-opus-4-8'));
71
+ assert.ok(p.models.includes('claude-opus-4-7'));
72
+ assert.ok(p.models.includes('claude-sonnet-4-6'));
73
+ assert.ok(p.models.includes('claude-haiku-4-5-20251001'));
74
+ assert.ok(p.models.includes('claude-opus-4-6'));
75
+ assert.ok(p.models.includes('claude-sonnet-4-5'));
76
+ });
77
+
78
+ test('anthropic: versionAliases map opus series to opus-4-8', () => {
79
+ assert.strictEqual(getLatestModel('claude-opus-4', 'anthropic'), 'claude-opus-4-8');
80
+ assert.strictEqual(getLatestModel('claude-opus-4-6', 'anthropic'), 'claude-opus-4-8');
81
+ assert.strictEqual(getLatestModel('claude-opus-4-7', 'anthropic'), 'claude-opus-4-8');
82
+ });
83
+ ```
84
+
85
+ - [ ] **Step 2: 更新 `test/env-vars-providers.test.js` 的 anthropic 断言(先红)**
86
+
87
+ 把 `anthropic model list trimmed to current generation`(约 :20-25)的 `assert.strictEqual(m.length, 6)` 改为 `7`。
88
+
89
+ 把 `anthropic versionAlias opus-4-6 → opus-4-7`(约 :29-31)整体替换为:
90
+ ```js
91
+ test('anthropic versionAlias opus-4-6 → opus-4-8', () => {
92
+ assert.strictEqual(getLatestModel('claude-opus-4-6', 'anthropic'), 'claude-opus-4-8');
93
+ });
94
+ ```
95
+
96
+ 把 `anthropic template: tier-based assignment (sonnet selected)`(约 :105-113)里的 `assert.strictEqual(v.ANTHROPIC_DEFAULT_OPUS_MODEL, 'claude-opus-4-7');` 改为 `'claude-opus-4-8'`。
97
+
98
+ - [ ] **Step 3: 跑测试确认失败**
99
+
100
+ Run: `node test/providers.test.js && node test/env-vars-providers.test.js`
101
+ Expected: 两个文件均有 anthropic 相关 `✗` 失败(`claude-opus-4-8` 不存在、opus-4-7 无 alias)
102
+
103
+ - [ ] **Step 4: 改 `lib/presets/providers.js` 的 `anthropicModels`**
104
+
105
+ 把 `const anthropicModels = [...]`(:43-50)整体替换为(`claude-opus-4-8` 置顶,保证 `find('opus')` 命中最新):
106
+ ```js
107
+ const anthropicModels = [
108
+ 'claude-opus-4-8',
109
+ 'claude-opus-4-7',
110
+ 'claude-sonnet-4-6',
111
+ 'claude-haiku-4-5-20251001',
112
+ 'claude-sonnet-4-5',
113
+ 'claude-opus-4-6',
114
+ 'claude-opus-4-5',
115
+ ];
116
+ ```
117
+
118
+ - [ ] **Step 5: 改 `lib/presets/providers.js` 的 `anthropic.versionAliases`**
119
+
120
+ 把 `anthropic.versionAliases`(:57-65)整体替换为:
121
+ ```js
122
+ versionAliases: {
123
+ 'claude-opus-4': 'claude-opus-4-8',
124
+ 'claude-opus-4-1': 'claude-opus-4-8',
125
+ 'claude-opus-4-5': 'claude-opus-4-8',
126
+ 'claude-opus-4-6': 'claude-opus-4-8',
127
+ 'claude-opus-4-7': 'claude-opus-4-8',
128
+ 'claude-sonnet-4': 'claude-sonnet-4-6',
129
+ 'claude-sonnet-4-5': 'claude-sonnet-4-6',
130
+ 'claude-3-7-sonnet': 'claude-sonnet-4-6',
131
+ },
132
+ ```
133
+
134
+ - [ ] **Step 6: 跑测试确认通过**
135
+
136
+ Run: `node test/providers.test.js && node test/env-vars-providers.test.js`
137
+ Expected: 全部 `✓`,0 failed
138
+
139
+ - [ ] **Step 7: Commit**
140
+
141
+ ```bash
142
+ git add lib/presets/providers.js test/providers.test.js test/env-vars-providers.test.js
143
+ git commit -m "feat(providers): upgrade anthropic opus to claude-opus-4-8"
144
+ ```
145
+
146
+ ---
147
+
148
+ ## Task 2: deepseek — Haiku 档去掉 [1m] 后缀
149
+
150
+ **Files:**
151
+ - Modify: `lib/presets/providers.js:202-229`(`deepseek` provider)
152
+ - Modify: `test/providers.test.js:176-183`
153
+ - Modify: `test/env-vars-providers.test.js:37-41, 50-52, 93-104`
154
+
155
+ - [ ] **Step 1: 更新 `test/providers.test.js` 的 deepseek 断言(先红)**
156
+
157
+ 把 `deepseek: models include v4-pro and v4-flash`(:176-183)整体替换为:
158
+ ```js
159
+ test('deepseek: models include v4-pro[1m] and v4-flash', () => {
160
+ const p = getProvider('deepseek');
161
+ assert.ok(p.models.includes('deepseek-v4-pro[1m]'));
162
+ assert.ok(p.models.includes('deepseek-v4-flash'));
163
+ assert.ok(p.models.includes('deepseek-chat'));
164
+ assert.ok(p.models.includes('deepseek-reasoner'));
165
+ assert.strictEqual(p.models.length, 4);
166
+ });
167
+ ```
168
+
169
+ - [ ] **Step 2: 更新 `test/env-vars-providers.test.js` 的 deepseek 断言(先红)**
170
+
171
+ 把 `deepseek models include v4-pro[1m] and v4-flash[1m]`(:37-41)的 `assert.ok(m.includes('deepseek-v4-flash[1m]'));` 改为 `assert.ok(m.includes('deepseek-v4-flash'));`。
172
+
173
+ 把 `deepseek alias chat → v4-flash`(:50-52)整体替换为:
174
+ ```js
175
+ test('deepseek alias chat → v4-flash', () => {
176
+ assert.strictEqual(getLatestModel('deepseek-chat', 'deepseek'), 'deepseek-v4-flash');
177
+ });
178
+ test('deepseek alias v4-flash[1m] → v4-flash (legacy compat)', () => {
179
+ assert.strictEqual(getLatestModel('deepseek-v4-flash[1m]', 'deepseek'), 'deepseek-v4-flash');
180
+ });
181
+ ```
182
+
183
+ 把 `deepseek template: pro model → flash`(:93-99)整体替换为:
184
+ ```js
185
+ test('deepseek template: pro model → flash for haiku/subagent/smallFast', () => {
186
+ const v = getProvider('deepseek').modelEnvTemplate.getValues('deepseek-v4-pro[1m]');
187
+ assert.strictEqual(v.ANTHROPIC_DEFAULT_HAIKU_MODEL, 'deepseek-v4-flash');
188
+ assert.strictEqual(v.CLAUDE_CODE_SUBAGENT_MODEL, 'deepseek-v4-flash');
189
+ assert.strictEqual(v.smallFastModel, 'deepseek-v4-flash');
190
+ assert.strictEqual(v.ANTHROPIC_DEFAULT_OPUS_MODEL, 'deepseek-v4-pro[1m]');
191
+ });
192
+ ```
193
+
194
+ 把 `deepseek template: flash model → all flash`(:100-104)整体替换为:
195
+ ```js
196
+ test('deepseek template: flash model → all flash', () => {
197
+ const v = getProvider('deepseek').modelEnvTemplate.getValues('deepseek-v4-flash');
198
+ assert.strictEqual(v.ANTHROPIC_DEFAULT_HAIKU_MODEL, 'deepseek-v4-flash');
199
+ assert.strictEqual(v.ANTHROPIC_DEFAULT_OPUS_MODEL, 'deepseek-v4-flash');
200
+ });
201
+ ```
202
+
203
+ - [ ] **Step 3: 跑测试确认失败**
204
+
205
+ Run: `node test/providers.test.js && node test/env-vars-providers.test.js`
206
+ Expected: deepseek 相关 `✗` 失败
207
+
208
+ - [ ] **Step 4: 改 `lib/presets/providers.js` 的 `deepseek`**
209
+
210
+ 把整个 `deepseek:` provider 块(:202-229)的三个字段改为(仅列改动行,其余字段 `baseUrl`/`authTokenFormat`/`envVars`/`note` 不变):
211
+
212
+ models(:205-210):
213
+ ```js
214
+ models: [
215
+ 'deepseek-v4-pro[1m]',
216
+ 'deepseek-v4-flash',
217
+ 'deepseek-chat',
218
+ 'deepseek-reasoner',
219
+ ],
220
+ ```
221
+
222
+ versionAliases(:211-214):
223
+ ```js
224
+ versionAliases: {
225
+ 'deepseek-chat': 'deepseek-v4-flash',
226
+ 'deepseek-reasoner': 'deepseek-v4-pro[1m]',
227
+ 'deepseek-v4-flash[1m]': 'deepseek-v4-flash',
228
+ },
229
+ ```
230
+
231
+ modelEnvTemplate(:227):
232
+ ```js
233
+ modelEnvTemplate: makeFastMapTemplate({"deepseek-v4-pro[1m]":"deepseek-v4-flash"}),
234
+ ```
235
+
236
+ - [ ] **Step 5: 跑测试确认通过**
237
+
238
+ Run: `node test/providers.test.js && node test/env-vars-providers.test.js`
239
+ Expected: 全部 `✓`
240
+
241
+ - [ ] **Step 6: Commit**
242
+
243
+ ```bash
244
+ git add lib/presets/providers.js test/providers.test.js test/env-vars-providers.test.js
245
+ git commit -m "fix(providers): align deepseek haiku tier to deepseek-v4-flash (drop [1m])"
246
+ ```
247
+
248
+ ---
249
+
250
+ ## Task 3: minimax — 新增 MiniMax-M3 旗舰
251
+
252
+ **Files:**
253
+ - Modify: `lib/presets/providers.js:142-201`(`minimax_cn` + `minimax_global`)
254
+ - Modify: `test/providers.test.js:122-158`
255
+ - Modify: `test/env-vars-providers.test.js:72-87`
256
+
257
+ - [ ] **Step 1: 更新 `test/providers.test.js` 的 minimax 断言(先红)**
258
+
259
+ 把 `minimax_cn: models list includes highspeed variants and M2`(:122-132)整体替换为:
260
+ ```js
261
+ test('minimax_cn: models list includes M3, highspeed variants and M2', () => {
262
+ const p = getProvider('minimax_cn');
263
+ assert.ok(p.models.includes('MiniMax-M3'));
264
+ assert.ok(p.models.includes('MiniMax-M2.7'));
265
+ assert.ok(p.models.includes('MiniMax-M2.7-highspeed'));
266
+ assert.ok(p.models.includes('MiniMax-M2.5'));
267
+ assert.ok(p.models.includes('MiniMax-M2.5-highspeed'));
268
+ assert.ok(p.models.includes('MiniMax-M2.1'));
269
+ assert.ok(p.models.includes('MiniMax-M2.1-highspeed'));
270
+ assert.ok(p.models.includes('MiniMax-M2'));
271
+ assert.strictEqual(p.models.length, 8);
272
+ });
273
+ ```
274
+
275
+ 把 `minimax_cn: MiniMax-M2.1 aliases to MiniMax-M2.7`(:134-138)整体替换为:
276
+ ```js
277
+ test('minimax_cn: MiniMax-M2.1 aliases to MiniMax-M3', () => {
278
+ assert.strictEqual(getLatestModel('MiniMax-M2.1', 'minimax_cn'), 'MiniMax-M3');
279
+ });
280
+ ```
281
+
282
+ 把 `minimax_cn: MiniMax-M2.5 aliases to MiniMax-M2.7`(:140-142)整体替换为:
283
+ ```js
284
+ test('minimax_cn: MiniMax-M2.5 aliases to MiniMax-M3', () => {
285
+ assert.strictEqual(getLatestModel('MiniMax-M2.5', 'minimax_cn'), 'MiniMax-M3');
286
+ });
287
+ test('minimax_cn: MiniMax-M2.7 aliases to MiniMax-M3', () => {
288
+ assert.strictEqual(getLatestModel('MiniMax-M2.7', 'minimax_cn'), 'MiniMax-M3');
289
+ });
290
+ ```
291
+
292
+ 把 `minimax_cn: latest MiniMax-M2.7 has no alias`(:144-146)整体替换为:
293
+ ```js
294
+ test('minimax_cn: latest MiniMax-M3 has no alias', () => {
295
+ assert.strictEqual(getLatestModel('MiniMax-M3', 'minimax_cn'), null);
296
+ });
297
+ ```
298
+
299
+ - [ ] **Step 2: 更新 `test/env-vars-providers.test.js` 的 minimax 断言(先红)**
300
+
301
+ 在 `minimax_cn models include highspeed variants and M2`(:72-78)的断言块末尾,`assert.ok(m.includes('MiniMax-M2'));` 之后加一行:
302
+ ```js
303
+ assert.ok(m.includes('MiniMax-M3'));
304
+ ```
305
+
306
+ - [ ] **Step 3: 跑测试确认失败**
307
+
308
+ Run: `node test/providers.test.js && node test/env-vars-providers.test.js`
309
+ Expected: minimax 相关 `✗` 失败
310
+
311
+ - [ ] **Step 4: 改 `lib/presets/providers.js` 的 `minimax_cn`**
312
+
313
+ 把 `minimax_cn` 的 models(:145-153)首元素前加 `'MiniMax-M3',`:
314
+ ```js
315
+ models: [
316
+ 'MiniMax-M3',
317
+ 'MiniMax-M2.7',
318
+ 'MiniMax-M2.7-highspeed',
319
+ 'MiniMax-M2.5',
320
+ 'MiniMax-M2.5-highspeed',
321
+ 'MiniMax-M2.1',
322
+ 'MiniMax-M2.1-highspeed',
323
+ 'MiniMax-M2',
324
+ ],
325
+ ```
326
+
327
+ 把 `minimax_cn` 的 versionAliases(:154-157)整体替换为:
328
+ ```js
329
+ versionAliases: {
330
+ 'MiniMax-M2': 'MiniMax-M3',
331
+ 'MiniMax-M2.1': 'MiniMax-M3',
332
+ 'MiniMax-M2.5': 'MiniMax-M3',
333
+ 'MiniMax-M2.7': 'MiniMax-M3'
334
+ },
335
+ ```
336
+
337
+ > `modelEnvTemplate`(:169 `makeFastMapTemplate({M2.x→M2.x-highspeed})`)**不改**:M3 不在 fastMap 中,`fastMap[model] || model` 回退到 M3 自身,所有 tier 均映射 M3。
338
+
339
+ - [ ] **Step 5: 改 `lib/presets/providers.js` 的 `minimax_global`(与 cn 镜像一致)**
340
+
341
+ 对 `minimax_global`(:172-201)做与 `minimax_cn` 完全相同的两处改动(models 加 `'MiniMax-M3'`、versionAliases 四个目标全改 `MiniMax-M3`)。
342
+
343
+ - [ ] **Step 6: 跑测试确认通过**
344
+
345
+ Run: `node test/providers.test.js && node test/env-vars-providers.test.js`
346
+ Expected: 全部 `✓`(含 `minimax_global: versionAliases matches minimax_cn` 镜像断言)
347
+
348
+ - [ ] **Step 7: Commit**
349
+
350
+ ```bash
351
+ git add lib/presets/providers.js test/providers.test.js test/env-vars-providers.test.js
352
+ git commit -m "feat(providers): add MiniMax-M3 as minimax flagship"
353
+ ```
354
+
355
+ ---
356
+
357
+ ## Task 4: moonshot — 收敛为单一旗舰 kimi-k2.7-code + env vars
358
+
359
+ **Files:**
360
+ - Modify: `lib/presets/providers.js:91-121`(`moonshot` provider)
361
+ - Modify: `test/providers.test.js:83-118`
362
+ - Modify: `test/env-vars-providers.test.js:60-69`
363
+
364
+ - [ ] **Step 1: 更新 `test/providers.test.js` 的 moonshot 断言(先红)**
365
+
366
+ 把 `moonshot: models list includes kimi-k2.6`(:83-90)整体替换为:
367
+ ```js
368
+ test('moonshot: models list is single flagship kimi-k2.7-code', () => {
369
+ const p = getProvider('moonshot');
370
+ assert.ok(p.models.includes('kimi-k2.7-code'));
371
+ assert.strictEqual(p.models.length, 1);
372
+ });
373
+ ```
374
+
375
+ 把 :92-118 的所有 moonshot alias 测试(`kimi-k2-0711-preview`、`kimi-k2-0905-preview`、`kimi-k2-turbo-preview`、`kimi-k2-thinking`、`kimi-k2-thinking-turbo`、`kimi-k2.5` → `kimi-k2.6`,以及 `kimi-k2.6 has no alias`)整体替换为:
376
+ ```js
377
+ test('moonshot: kimi-k2.6 aliases to kimi-k2.7-code', () => {
378
+ assert.strictEqual(getLatestModel('kimi-k2.6', 'moonshot'), 'kimi-k2.7-code');
379
+ });
380
+
381
+ test('moonshot: kimi-k2.5 aliases to kimi-k2.7-code', () => {
382
+ assert.strictEqual(getLatestModel('kimi-k2.5', 'moonshot'), 'kimi-k2.7-code');
383
+ });
384
+
385
+ test('moonshot: kimi-k2-thinking aliases to kimi-k2.7-code', () => {
386
+ assert.strictEqual(getLatestModel('kimi-k2-thinking', 'moonshot'), 'kimi-k2.7-code');
387
+ });
388
+
389
+ test('moonshot: kimi-k2-thinking-turbo aliases to kimi-k2.7-code', () => {
390
+ assert.strictEqual(getLatestModel('kimi-k2-thinking-turbo', 'moonshot'), 'kimi-k2.7-code');
391
+ });
392
+
393
+ test('moonshot: kimi-k2-0711-preview aliases to kimi-k2.7-code', () => {
394
+ assert.strictEqual(getLatestModel('kimi-k2-0711-preview', 'moonshot'), 'kimi-k2.7-code');
395
+ });
396
+
397
+ test('moonshot: kimi-k2-0905-preview aliases to kimi-k2.7-code', () => {
398
+ assert.strictEqual(getLatestModel('kimi-k2-0905-preview', 'moonshot'), 'kimi-k2.7-code');
399
+ });
400
+
401
+ test('moonshot: kimi-k2-turbo-preview aliases to kimi-k2.7-code', () => {
402
+ assert.strictEqual(getLatestModel('kimi-k2-turbo-preview', 'moonshot'), 'kimi-k2.7-code');
403
+ });
404
+
405
+ test('moonshot: latest kimi-k2.7-code has no alias', () => {
406
+ assert.strictEqual(getLatestModel('kimi-k2.7-code', 'moonshot'), null);
407
+ });
408
+ ```
409
+
410
+ - [ ] **Step 2: 更新 `test/env-vars-providers.test.js` 的 moonshot 断言(先红)**
411
+
412
+ 把 `moonshot models include kimi-k2.6`(:61-63)替换为:
413
+ ```js
414
+ test('moonshot models include kimi-k2.7-code', () => {
415
+ assert.ok(getProvider('moonshot').models.includes('kimi-k2.7-code'));
416
+ });
417
+ ```
418
+
419
+ 把 `moonshot alias k2-thinking → k2.6`(:64-66)替换为:
420
+ ```js
421
+ test('moonshot alias k2-thinking → k2.7-code', () => {
422
+ assert.strictEqual(getLatestModel('kimi-k2-thinking', 'moonshot'), 'kimi-k2.7-code');
423
+ });
424
+ ```
425
+
426
+ - [ ] **Step 3: 跑测试确认失败**
427
+
428
+ Run: `node test/providers.test.js && node test/env-vars-providers.test.js`
429
+ Expected: moonshot 相关 `✗` 失败
430
+
431
+ - [ ] **Step 4: 改 `lib/presets/providers.js` 的 `moonshot`**
432
+
433
+ 把整个 `moonshot:` provider 块(:91-121)的对应字段改为:
434
+
435
+ name(:92):
436
+ ```js
437
+ name: 'Moonshot AI (Kimi-K2.7-Code)',
438
+ ```
439
+
440
+ models(:94-99):
441
+ ```js
442
+ models: [
443
+ 'kimi-k2.7-code',
444
+ ],
445
+ ```
446
+
447
+ versionAliases(:100-107):
448
+ ```js
449
+ versionAliases: {
450
+ 'kimi-k2.6': 'kimi-k2.7-code',
451
+ 'kimi-k2.5': 'kimi-k2.7-code',
452
+ 'kimi-k2-thinking': 'kimi-k2.7-code',
453
+ 'kimi-k2-thinking-turbo': 'kimi-k2.7-code',
454
+ 'kimi-k2-0711-preview': 'kimi-k2.7-code',
455
+ 'kimi-k2-0905-preview': 'kimi-k2.7-code',
456
+ 'kimi-k2-turbo-preview': 'kimi-k2.7-code',
457
+ },
458
+ ```
459
+
460
+ envVars(:112-118,在末尾 `CLAUDE_CODE_DISABLE_NONSTREAMING_FALLBACK: '1'` 之后加两行):
461
+ ```js
462
+ envVars: {
463
+ API_TIMEOUT_MS: '3000000',
464
+ CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1',
465
+ CLAUDE_CODE_ATTRIBUTION_HEADER: '0',
466
+ CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS: '1',
467
+ CLAUDE_CODE_DISABLE_NONSTREAMING_FALLBACK: '1',
468
+ ENABLE_TOOL_SEARCH: 'false',
469
+ CLAUDE_CODE_AUTO_COMPACT_WINDOW: '262144',
470
+ },
471
+ ```
472
+
473
+ modelEnvTemplate(:119):
474
+ ```js
475
+ modelEnvTemplate: makeSingleTemplate(),
476
+ ```
477
+
478
+ - [ ] **Step 5: 跑测试确认通过**
479
+
480
+ Run: `node test/providers.test.js && node test/env-vars-providers.test.js`
481
+ Expected: 全部 `✓`
482
+
483
+ - [ ] **Step 6: Commit**
484
+
485
+ ```bash
486
+ git add lib/presets/providers.js test/providers.test.js test/env-vars-providers.test.js
487
+ git commit -m "feat(providers): converge moonshot to single flagship kimi-k2.7-code"
488
+ ```
489
+
490
+ ---
491
+
492
+ ## Task 5: zhipu/zai — 升级到 glm-5.2[1m] + 新增 makeGlmTierTemplate
493
+
494
+ **Files:**
495
+ - Modify: `lib/presets/providers.js`(新增 factory 在 :40 之后;改 `zhipu` :230-259、`zai` :260-289)
496
+ - Modify: `test/providers.test.js:30-79`
497
+ - Modify: `test/env-vars-providers.test.js`(新增 GLM tier template 测试)
498
+
499
+ - [ ] **Step 1: 更新 `test/providers.test.js` 的 zhipu/zai 断言(先红)**
500
+
501
+ 把 `zhipu: models list is correct`(:32-35)替换为:
502
+ ```js
503
+ test('zhipu: models list is correct', () => {
504
+ const p = getProvider('zhipu');
505
+ assert.deepStrictEqual(p.models, ['glm-5.2[1m]', 'glm-5.1', 'glm-5-turbo', 'glm-5']);
506
+ });
507
+ ```
508
+
509
+ 把 `zhipu: name includes all current model families`(:37-41)替换为:
510
+ ```js
511
+ test('zhipu: name includes all current model families', () => {
512
+ const p = getProvider('zhipu');
513
+ assert.ok(p.name.includes('GLM-5.2'));
514
+ assert.ok(p.name.includes('5-Turbo'));
515
+ });
516
+ ```
517
+
518
+ 把 :43-65 的整段 zhipu alias 测试(`glm-4.5`/`glm-4.6`/`glm-4.7`/`glm-5`/`glm-5-turbo` aliases to glm-5.1,及 `glm-5.1 has no alias`)整体替换为:
519
+ ```js
520
+ test('zhipu: glm-4.5 aliases to glm-5.2[1m]', () => {
521
+ assert.strictEqual(getLatestModel('glm-4.5', 'zhipu'), 'glm-5.2[1m]');
522
+ });
523
+
524
+ test('zhipu: glm-4.6 aliases to glm-5.2[1m]', () => {
525
+ assert.strictEqual(getLatestModel('glm-4.6', 'zhipu'), 'glm-5.2[1m]');
526
+ });
527
+
528
+ test('zhipu: glm-4.7 aliases to glm-5.2[1m]', () => {
529
+ assert.strictEqual(getLatestModel('glm-4.7', 'zhipu'), 'glm-5.2[1m]');
530
+ });
531
+
532
+ test('zhipu: glm-5 (tier role) has no alias', () => {
533
+ assert.strictEqual(getLatestModel('glm-5', 'zhipu'), null);
534
+ });
535
+
536
+ test('zhipu: glm-5-turbo (haiku tier) has no alias', () => {
537
+ assert.strictEqual(getLatestModel('glm-5-turbo', 'zhipu'), null);
538
+ });
539
+
540
+ test('zhipu: glm-5.1 (optional retained) has no alias', () => {
541
+ assert.strictEqual(getLatestModel('glm-5.1', 'zhipu'), null);
542
+ });
543
+
544
+ test('zhipu: latest glm-5.2[1m] has no alias', () => {
545
+ assert.strictEqual(getLatestModel('glm-5.2[1m]', 'zhipu'), null);
546
+ });
547
+ ```
548
+
549
+ > `zai` 镜像断言(:67-79,`models list matches zhipu` / `versionAliases matches zhipu`)**不改**——它们自动验证 zai 与 zhipu 一致。
550
+
551
+ - [ ] **Step 2: 新增 `test/env-vars-providers.test.js` 的 GLM tier template 测试(先红)**
552
+
553
+ 在该文件末尾 `console.log(...)` 汇总行**之前**插入:
554
+ ```js
555
+ // --- GLM tier template (fixed tiers regardless of selected model) ---
556
+ test('glm tier template: opus=sonnet=glm-5.2[1m], haiku=glm-5-turbo when flagship selected', () => {
557
+ const v = getProvider('zhipu').modelEnvTemplate.getValues('glm-5.2[1m]');
558
+ assert.strictEqual(v.ANTHROPIC_CUSTOM_MODEL_OPTION, 'glm-5.2[1m]');
559
+ assert.strictEqual(v.ANTHROPIC_DEFAULT_OPUS_MODEL, 'glm-5.2[1m]');
560
+ assert.strictEqual(v.ANTHROPIC_DEFAULT_SONNET_MODEL, 'glm-5.2[1m]');
561
+ assert.strictEqual(v.ANTHROPIC_DEFAULT_HAIKU_MODEL, 'glm-5-turbo');
562
+ assert.strictEqual(v.CLAUDE_CODE_SUBAGENT_MODEL, 'glm-5-turbo');
563
+ assert.strictEqual(v.smallFastModel, 'glm-5-turbo');
564
+ });
565
+ test('glm tier template: tiers stay fixed when non-flagship model selected', () => {
566
+ const v = getProvider('zhipu').modelEnvTemplate.getValues('glm-5');
567
+ assert.strictEqual(v.ANTHROPIC_CUSTOM_MODEL_OPTION, 'glm-5');
568
+ assert.strictEqual(v.ANTHROPIC_DEFAULT_OPUS_MODEL, 'glm-5.2[1m]');
569
+ assert.strictEqual(v.ANTHROPIC_DEFAULT_SONNET_MODEL, 'glm-5.2[1m]');
570
+ assert.strictEqual(v.ANTHROPIC_DEFAULT_HAIKU_MODEL, 'glm-5-turbo');
571
+ });
572
+ test('glm tier template: zai uses same factory as zhipu', () => {
573
+ const vz = getProvider('zhipu').modelEnvTemplate.getValues('glm-5.1');
574
+ const va = getProvider('zai').modelEnvTemplate.getValues('glm-5.1');
575
+ assert.deepStrictEqual(va, vz);
576
+ });
577
+ ```
578
+
579
+ - [ ] **Step 3: 跑测试确认失败**
580
+
581
+ Run: `node test/providers.test.js && node test/env-vars-providers.test.js`
582
+ Expected: zhipu 相关 `✗` 失败(models/alias/template 均不符)
583
+
584
+ - [ ] **Step 4: 在 `lib/presets/providers.js` 新增 `makeGlmTierTemplate` factory**
585
+
586
+ 在 `makeSingleTemplate()` 函数闭合(:40 的 `}`)之后、`const anthropicModels`(:42)之前插入:
587
+ ```js
588
+ // Template factory: GLM — fixed tier standards (Opus=Sonnet=flagship, Haiku=turbo),
589
+ // regardless of which model the user picked as CUSTOM. Matches Claude Code's
590
+ // "tier standards are fixed, CUSTOM is the current selection" semantics.
591
+ function makeGlmTierTemplate() {
592
+ return {
593
+ getValues(model) {
594
+ return {
595
+ ANTHROPIC_CUSTOM_MODEL_OPTION: model,
596
+ ANTHROPIC_CUSTOM_MODEL_OPTION_NAME: model,
597
+ ANTHROPIC_DEFAULT_OPUS_MODEL: 'glm-5.2[1m]',
598
+ ANTHROPIC_DEFAULT_SONNET_MODEL: 'glm-5.2[1m]',
599
+ ANTHROPIC_DEFAULT_HAIKU_MODEL: 'glm-5-turbo',
600
+ CLAUDE_CODE_SUBAGENT_MODEL: 'glm-5-turbo',
601
+ smallFastModel: 'glm-5-turbo',
602
+ };
603
+ },
604
+ };
605
+ }
606
+
607
+ ```
608
+
609
+ - [ ] **Step 5: 改 `lib/presets/providers.js` 的 `zhipu`**
610
+
611
+ 把 `zhipu` 的 name(:231)改为:
612
+ ```js
613
+ name: 'ZhiPu AI (GLM-5.2/5-Turbo) - 智谱清言',
614
+ ```
615
+
616
+ 把 models(:233-238)改为:
617
+ ```js
618
+ models: [
619
+ 'glm-5.2[1m]',
620
+ 'glm-5.1',
621
+ 'glm-5-turbo',
622
+ 'glm-5'
623
+ ],
624
+ ```
625
+
626
+ 把 versionAliases(:239-245)改为:
627
+ ```js
628
+ versionAliases: {
629
+ 'glm-4.5': 'glm-5.2[1m]',
630
+ 'glm-4.6': 'glm-5.2[1m]',
631
+ 'glm-4.7': 'glm-5.2[1m]',
632
+ },
633
+ ```
634
+
635
+ 把 envVars(:250-256,在末尾 `CLAUDE_CODE_DISABLE_NONSTREAMING_FALLBACK: '1'` 之后加一行):
636
+ ```js
637
+ envVars: {
638
+ API_TIMEOUT_MS: '3000000',
639
+ CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1',
640
+ CLAUDE_CODE_ATTRIBUTION_HEADER: '0',
641
+ CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS: '1',
642
+ CLAUDE_CODE_DISABLE_NONSTREAMING_FALLBACK: '1',
643
+ CLAUDE_CODE_AUTO_COMPACT_WINDOW: '1000000',
644
+ },
645
+ ```
646
+
647
+ 把 modelEnvTemplate(:257)改为:
648
+ ```js
649
+ modelEnvTemplate: makeGlmTierTemplate(),
650
+ ```
651
+
652
+ - [ ] **Step 6: 改 `lib/presets/providers.js` 的 `zai`(与 zhipu 镜像一致)**
653
+
654
+ 把 `zai` 的 name(:261)改为:
655
+ ```js
656
+ name: 'Z.ai (GLM-5.2/5-Turbo) - ZhiPu Global',
657
+ ```
658
+
659
+ 对 `zai` 的 models(:263-268)、versionAliases(:269-275)、envVars(:280-286,末尾加 `CLAUDE_CODE_AUTO_COMPACT_WINDOW: '1000000',`)、modelEnvTemplate(:287)做与 `zhipu` 完全相同的改动。
660
+
661
+ - [ ] **Step 7: 跑测试确认通过**
662
+
663
+ Run: `node test/providers.test.js && node test/env-vars-providers.test.js`
664
+ Expected: 全部 `✓`
665
+
666
+ - [ ] **Step 8: Commit**
667
+
668
+ ```bash
669
+ git add lib/presets/providers.js test/providers.test.js test/env-vars-providers.test.js
670
+ git commit -m "feat(providers): upgrade zhipu/zai to glm-5.2[1m] with fixed tier template"
671
+ ```
672
+
673
+ ---
674
+
675
+ ## Task 6: api-manager — template 漂移检测
676
+
677
+ **Files:**
678
+ - Modify: `lib/api-manager.js:165-169`(`_normalizeApiFields` 的 smallFastModel sync 之后插入漂移检测块)
679
+ - Modify: `test/api-manager.test.js`(末尾汇总行之前新增 3 个测试)
680
+
681
+ **背景:** 当 provider template 升级(如本次 GLM 从 fastMap 改为固定 tier),已保存的旧 API 配置的 `_autoModelEnvVars` 仍是旧快照。`_normalizeApiFields` 现有逻辑只在 `_autoModelEnvVars` 缺失时重建、否则仅补缺失 key,不会刷新已存在但过时的字段。漂移检测在「快照已存在」时比较快照与当前 template,发现不一致则刷新仍等于旧快照的字段(保留用户手动覆盖)。
682
+
683
+ - [ ] **Step 1: 在 `test/api-manager.test.js` 新增 3 个漂移测试(先红)**
684
+
685
+ 在文件末尾 `console.log(\`\n ${passed} passed, ${failed} failed\n\`);`(:185)**之前**插入:
686
+ ```js
687
+ // --- template drift migration (_migrateApiEntry / _normalizeApiFields) ---
688
+
689
+ console.log('\ntemplate drift migration:');
690
+
691
+ function makeOldGlmFastMapSnapshot() {
692
+ // 升级前 makeFastMapTemplate({"glm-5.1":"glm-5-turbo"}) 对 glm-5.1 生成的快照
693
+ return {
694
+ ANTHROPIC_CUSTOM_MODEL_OPTION: 'glm-5.1',
695
+ ANTHROPIC_CUSTOM_MODEL_OPTION_NAME: 'glm-5.1',
696
+ ANTHROPIC_DEFAULT_SONNET_MODEL: 'glm-5.1',
697
+ ANTHROPIC_DEFAULT_OPUS_MODEL: 'glm-5.1',
698
+ ANTHROPIC_DEFAULT_HAIKU_MODEL: 'glm-5-turbo',
699
+ CLAUDE_CODE_SUBAGENT_MODEL: 'glm-5-turbo',
700
+ smallFastModel: 'glm-5-turbo',
701
+ };
702
+ }
703
+
704
+ test('_migrateApiEntry drifts zhipu glm-5.1: tier refreshed to glm-5.2[1m], migrated=true', () => {
705
+ const { PREDEFINED_MODEL_ENV_KEYS, PREDEFINED_RUNTIME_KEYS } = require('../lib/validators');
706
+ const mgr = new ApiManager();
707
+ mgr.saveConfig = () => {};
708
+ const oldAuto = makeOldGlmFastMapSnapshot();
709
+ const api = {
710
+ provider: 'zhipu', model: 'glm-5.1',
711
+ modelEnvVars: { ...oldAuto },
712
+ _autoModelEnvVars: { ...oldAuto },
713
+ smallFastModel: 'glm-5-turbo',
714
+ runtimeEnvVars: {}, _runtimeEnvSources: {}, customEnvVars: {},
715
+ };
716
+ const migrated = mgr._migrateApiEntry(api, PREDEFINED_MODEL_ENV_KEYS, PREDEFINED_RUNTIME_KEYS);
717
+ assert.strictEqual(migrated, true);
718
+ assert.strictEqual(api.modelEnvVars.ANTHROPIC_DEFAULT_OPUS_MODEL, 'glm-5.2[1m]');
719
+ assert.strictEqual(api.modelEnvVars.ANTHROPIC_DEFAULT_SONNET_MODEL, 'glm-5.2[1m]');
720
+ assert.strictEqual(api.modelEnvVars.ANTHROPIC_DEFAULT_HAIKU_MODEL, 'glm-5-turbo');
721
+ assert.strictEqual(api._autoModelEnvVars.ANTHROPIC_DEFAULT_OPUS_MODEL, 'glm-5.2[1m]');
722
+ assert.strictEqual(api.smallFastModel, 'glm-5-turbo');
723
+ });
724
+
725
+ test('_migrateApiEntry drifts: preserves user manual HAIKU override', () => {
726
+ const { PREDEFINED_MODEL_ENV_KEYS, PREDEFINED_RUNTIME_KEYS } = require('../lib/validators');
727
+ const mgr = new ApiManager();
728
+ mgr.saveConfig = () => {};
729
+ const oldAuto = makeOldGlmFastMapSnapshot();
730
+ // 用户手动把 HAIKU 设为 glm-5.1(≠ 旧 auto 的 glm-5-turbo)→ 漂移后应保留
731
+ const overridden = { ...oldAuto, ANTHROPIC_DEFAULT_HAIKU_MODEL: 'glm-5.1' };
732
+ const api = {
733
+ provider: 'zhipu', model: 'glm-5.1',
734
+ modelEnvVars: overridden,
735
+ _autoModelEnvVars: { ...oldAuto },
736
+ smallFastModel: 'glm-5-turbo',
737
+ runtimeEnvVars: {}, _runtimeEnvSources: {}, customEnvVars: {},
738
+ };
739
+ mgr._migrateApiEntry(api, PREDEFINED_MODEL_ENV_KEYS, PREDEFINED_RUNTIME_KEYS);
740
+ assert.strictEqual(api.modelEnvVars.ANTHROPIC_DEFAULT_HAIKU_MODEL, 'glm-5.1');
741
+ // 未被覆盖的 OPUS 仍刷新
742
+ assert.strictEqual(api.modelEnvVars.ANTHROPIC_DEFAULT_OPUS_MODEL, 'glm-5.2[1m]');
743
+ });
744
+
745
+ test('_normalizeApiFields no drift: tier values unchanged when snapshot matches template', () => {
746
+ const { getProvider } = require('../lib/presets/providers');
747
+ const mgr = new ApiManager();
748
+ mgr.saveConfig = () => {};
749
+ const currentTemplate = getProvider('zhipu').modelEnvTemplate.getValues('glm-5.2[1m]');
750
+ const api = {
751
+ provider: 'zhipu', model: 'glm-5.2[1m]',
752
+ modelEnvVars: { ...currentTemplate },
753
+ _autoModelEnvVars: { ...currentTemplate },
754
+ smallFastModel: currentTemplate.smallFastModel,
755
+ runtimeEnvVars: {}, _runtimeEnvSources: {}, customEnvVars: {},
756
+ };
757
+ mgr._normalizeApiFields(api);
758
+ assert.strictEqual(api.modelEnvVars.ANTHROPIC_DEFAULT_OPUS_MODEL, 'glm-5.2[1m]');
759
+ assert.strictEqual(api.modelEnvVars.ANTHROPIC_DEFAULT_SONNET_MODEL, 'glm-5.2[1m]');
760
+ assert.strictEqual(api.modelEnvVars.ANTHROPIC_DEFAULT_HAIKU_MODEL, 'glm-5-turbo');
761
+ assert.strictEqual(api._autoModelEnvVars.ANTHROPIC_DEFAULT_OPUS_MODEL, 'glm-5.2[1m]');
762
+ });
763
+ ```
764
+
765
+ > 注:第 3 个测试直接调 `_normalizeApiFields` 而非 `_migrateApiEntry`,因为 `_migrateApiEntry` 的 before/after 还会捕获 runtime 字段规范化(空对象 → 填充 6 key)导致 `migrated=true`,无法纯粹反映「无漂移」。核心验证目标是「快照与 template 一致时 tier 值不被改写」,用 `_normalizeApiFields` 直接断言最稳健。
766
+
767
+ - [ ] **Step 2: 跑测试确认失败**
768
+
769
+ Run: `node test/api-manager.test.js`
770
+ Expected: 前两个 `✗` 失败(OPUS 仍是 `glm-5.1`,漂移未触发);第三个 `✓`(当前 template 快照本就不变)
771
+
772
+ - [ ] **Step 3: 在 `lib/api-manager.js` 的 `_normalizeApiFields` 插入漂移检测块**
773
+
774
+ 找到 smallFastModel sync 段(:165-169):
775
+ ```js
776
+ // smallFastModel — sync with template
777
+ if (!api.smallFastModel || typeof api.smallFastModel !== 'string'
778
+ || !hadAutoModelEnvVars || smallFastWasFixed) {
779
+ api.smallFastModel = template.smallFastModel;
780
+ }
781
+ ```
782
+ 在其**之后**(即 `}` 闭合后的空行处,:170 位置,runtime 段 `// runtimeEnvVars` 之前)插入:
783
+ ```js
784
+
785
+ // template 漂移检测:provider template 升级后首次加载旧配置时,_autoModelEnvVars
786
+ // 仍是旧快照。检测到漂移则刷新仍等于旧快照的 tier 字段(保留用户手动覆盖),
787
+ // 复用 updateApiField() 的保留覆盖模式。_migrateApiEntry 的 before/after 比较
788
+ // 已覆盖 modelEnvVars/_autoModelEnvVars/smallFastModel,会据此返回 migrated=true
789
+ // 并由构造函数统一 saveConfig()。
790
+ if (hadAutoModelEnvVars) {
791
+ let drifted = false;
792
+ for (const k of PREDEFINED_MODEL_ENV_KEYS) {
793
+ if (api._autoModelEnvVars[k] !== template[k]) { drifted = true; break; }
794
+ }
795
+ if (!drifted && api._autoModelEnvVars.smallFastModel !== template.smallFastModel) {
796
+ drifted = true;
797
+ }
798
+ if (drifted) {
799
+ for (const k of PREDEFINED_MODEL_ENV_KEYS) {
800
+ if (api.modelEnvVars[k] === api._autoModelEnvVars[k]) {
801
+ api.modelEnvVars[k] = template[k] || '';
802
+ }
803
+ }
804
+ if (api.smallFastModel === api._autoModelEnvVars.smallFastModel) {
805
+ api.smallFastModel = template.smallFastModel;
806
+ }
807
+ api._autoModelEnvVars = { ...template };
808
+ }
809
+ }
810
+ ```
811
+
812
+ > 实现要点:`PREDEFINED_MODEL_ENV_KEYS` 已在函数顶部 :112 解构,直接复用。守卫 `hadAutoModelEnvVars`(:116)确保仅对「已存在旧快照」的配置生效;`_autoModelEnvVars` 缺失的配置走 :131 重建分支,天然等于当前 template,不触发漂移。
813
+
814
+ - [ ] **Step 4: 跑测试确认通过**
815
+
816
+ Run: `node test/api-manager.test.js`
817
+ Expected: 全部 `✓`(含 3 个新漂移测试)
818
+
819
+ - [ ] **Step 5: 跑全量回归确认无副作用**
820
+
821
+ Run: `npm test`
822
+ Expected: 14 个文件全部 0 failed
823
+
824
+ - [ ] **Step 6: Commit**
825
+
826
+ ```bash
827
+ git add lib/api-manager.js test/api-manager.test.js
828
+ git commit -m "feat(api-manager): auto-refresh tier defaults on provider template drift"
829
+ ```
830
+
831
+ ---
832
+
833
+ ## Task 7: moonshot env vars 覆盖优先级测试
834
+
835
+ **Files:**
836
+ - Modify: `test/env-vars-providers.test.js`(新增 2 个 `getProviderEnvVars` 测试)
837
+
838
+ **背景:** moonshot 新增的 `ENABLE_TOOL_SEARCH` / `CLAUDE_CODE_AUTO_COMPACT_WINDOW` 是 provider-only 默认(不在 `PREDEFINED_RUNTIME_KEYS` 白名单,Runtime UI 不暴露)。用户若需覆盖走 Custom Vars(`launcher.js:260-265` 的 customEnvVars 合并在 provider envVars 之后,且这两个 key 不在 `allPredefined` 集合,可覆盖)。本测试固化该优先级,防回归。
839
+
840
+ - [ ] **Step 1: 在 `test/env-vars-providers.test.js` 顶部加 require**
841
+
842
+ 在现有 `const { getProvider, getLatestModel } = require('../lib/presets/providers');`(:8)之后加:
843
+ ```js
844
+ const { getProviderEnvVars } = require('../lib/launcher');
845
+ const { encrypt } = require('../lib/crypto');
846
+
847
+ function makeMoonshotApi(customEnvVars = {}) {
848
+ return {
849
+ provider: 'moonshot',
850
+ baseUrl: 'https://api.moonshot.cn/anthropic',
851
+ authToken: encrypt('test-token').value,
852
+ model: 'kimi-k2.7-code',
853
+ smallFastModel: 'kimi-k2.7-code',
854
+ modelEnvVars: {},
855
+ runtimeEnvVars: {},
856
+ customEnvVars,
857
+ };
858
+ }
859
+ ```
860
+
861
+ > 若 `require('../lib/launcher')` 在该测试环境触发 UI 初始化副作用导致报错,则把这两个测试连同 require 移到 `test/launcher.test.js` 末尾汇总行之前(该文件已 require launcher,无此风险)。优先尝试放 `env-vars-providers.test.js`(与其它 provider 行为测试同文件更内聚)。
862
+
863
+ - [ ] **Step 2: 在该文件末尾汇总行之前新增 2 个测试**
864
+
865
+ 在 `console.log(...)` 汇总行**之前**插入:
866
+ ```js
867
+ // --- moonshot provider-default env vars vs Custom Vars override priority ---
868
+
869
+ test('moonshot getProviderEnvVars: emits provider defaults ENABLE_TOOL_SEARCH / AUTO_COMPACT_WINDOW', () => {
870
+ const env = getProviderEnvVars(makeMoonshotApi());
871
+ assert.strictEqual(env.ENABLE_TOOL_SEARCH, 'false');
872
+ assert.strictEqual(env.CLAUDE_CODE_AUTO_COMPACT_WINDOW, '262144');
873
+ });
874
+
875
+ test('moonshot getProviderEnvVars: customEnvVars overrides provider default', () => {
876
+ const env = getProviderEnvVars(makeMoonshotApi({ ENABLE_TOOL_SEARCH: 'true' }));
877
+ assert.strictEqual(env.ENABLE_TOOL_SEARCH, 'true');
878
+ });
879
+ ```
880
+
881
+ - [ ] **Step 3: 跑测试确认通过**
882
+
883
+ Run: `node test/env-vars-providers.test.js`
884
+ Expected: 全部 `✓`(launcher.js 的合并优先级已是现有行为,测试应直接通过——这是回归保护测试)
885
+
886
+ - [ ] **Step 4: Commit**
887
+
888
+ ```bash
889
+ git add test/env-vars-providers.test.js
890
+ git commit -m "test(providers): cover moonshot provider-default vs custom override priority"
891
+ ```
892
+
893
+ ---
894
+
895
+ ## Task 8: 版本号 + CHANGELOG
896
+
897
+ **Files:**
898
+ - Modify: `package.json:3`
899
+ - Modify: `CHANGELOG.md`(顶部 [3.1.0] 之前插入 [3.2.0] 段)
900
+
901
+ - [ ] **Step 1: 更新 `package.json` 版本**
902
+
903
+ 把 `"version": "3.1.0",`(:3)改为 `"version": "3.2.0",`。
904
+
905
+ - [ ] **Step 2: 在 `CHANGELOG.md` 顶部插入 [3.2.0] 段**
906
+
907
+ 在 `## [3.1.0] - 2026-05-09`(:8)**之前**插入:
908
+ ```markdown
909
+ ## [3.2.0] - 2026-06-14
910
+
911
+ ### Added
912
+ - **New Flagship Models**: `claude-opus-4-8` (anthropic), `kimi-k2.7-code` (moonshot), `MiniMax-M3` (minimax), `glm-5.2[1m]` (zhipu/zai).
913
+ - **Provider Template Drift Migration**: On first load after a provider template upgrade, saved API configs now auto-refresh tier defaults (`ANTHROPIC_DEFAULT_OPUS_MODEL` etc.) while preserving user manual overrides. Backed by `_normalizeApiFields` drift detection; persistence flows through the existing `_migrateApiEntry` → constructor `saveConfig()` lifecycle.
914
+
915
+ ### Changed
916
+ - **GLM Fixed Tier Template**: zhipu/zai now use a fixed tier template (`Opus = Sonnet = glm-5.2[1m]`, `Haiku = glm-5-turbo`) regardless of selected model, replacing the old `glm-5.1 → glm-5-turbo` fast map. `glm-5.1` retained as an optional model (no upgrade alias).
917
+ - **DeepSeek Haiku Alignment**: Haiku tier standardized to `deepseek-v4-flash` (dropped `[1m]`); legacy `deepseek-v4-flash[1m]` configs auto-upgrade via alias.
918
+ - **Moonshot Single Flagship**: Converged to `kimi-k2.7-code` as the sole model; legacy `kimi-k2.6`/`k2.5`/`k2-thinking`/`k2-thinking-turbo`/preview variants now upgrade aliases. Added `ENABLE_TOOL_SEARCH: 'false'` and `CLAUDE_CODE_AUTO_COMPACT_WINDOW: '262144'` (provider-only defaults; override via Custom Vars).
919
+ - **GLM Context Window**: zhipu/zai added `CLAUDE_CODE_AUTO_COMPACT_WINDOW: '1000000'` to match the 1M context of `glm-5.2[1m]`.
920
+
921
+ ### Removed
922
+ - **Moonshot Main List**: Removed `kimi-k2.5`/`kimi-k2-thinking`/`kimi-k2-thinking-turbo` from the model list (retained as upgrade aliases).
923
+
924
+ ```
925
+
926
+ - [ ] **Step 3: 跑全量测试最终验证**
927
+
928
+ Run: `npm test`
929
+ Expected: 14 个文件全部 0 failed
930
+
931
+ - [ ] **Step 4: Commit**
932
+
933
+ ```bash
934
+ git add package.json CHANGELOG.md
935
+ git commit -m "chore: bump version to 3.2.0 and update changelog"
936
+ ```
937
+
938
+ ---
939
+
940
+ ## 验收清单(全部 Task 完成后核对)
941
+
942
+ - [ ] `npm test` 14 个文件全部 0 failed
943
+ - [ ] `getLatestModel('claude-opus-4-7','anthropic') === 'claude-opus-4-8'`
944
+ - [ ] `getLatestModel('glm-4.7','zhipu') === 'glm-5.2[1m]'`,且 `getLatestModel('glm-5','zhipu') === null`
945
+ - [ ] `getLatestModel('kimi-k2.6','moonshot') === 'kimi-k2.7-code'`,且 `getLatestModel('kimi-k2.7-code','moonshot') === null`
946
+ - [ ] `getLatestModel('MiniMax-M2.7','minimax_cn') === 'MiniMax-M3'`
947
+ - [ ] `getLatestModel('deepseek-v4-flash[1m]','deepseek') === 'deepseek-v4-flash'`
948
+ - [ ] 每个 provider 的 `models[0]` 不在该 provider 的 `versionAliases` key 中(`invariant` 测试守护)
949
+ - [ ] 加载一个升级前保存的旧配置(`provider=zhipu, model=glm-5.1`),`_migrateApiEntry` 返回 `migrated=true`,`modelEnvVars.ANTHROPIC_DEFAULT_OPUS_MODEL === 'glm-5.2[1m]'`