yingclaw 2.5.25 → 2.5.26

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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Claude Code × 国产大模型,一键接入。
4
4
 
5
- 支持 DeepSeek、Kimi、火山方舟 Coding Plan、阿里云百炼(Qwen)、MiniMax、智谱 GLM、小米 MiMo,也支持自定义 Anthropic 兼容接口和自定义 OpenAI 兼容接口,无需梯子即可使用 Claude Code。
5
+ 支持 DeepSeek、Kimi、火山方舟 Coding Plan、阿里云百炼(Qwen)、MiniMax、智谱 GLM、小米 MiMo、B.AI、OpenCode Zen/Go,也支持自定义 Anthropic 兼容接口和自定义 OpenAI 兼容接口,无需梯子即可使用 Claude Code。
6
6
 
7
7
  ![yingclaw 交互菜单](https://raw.githubusercontent.com/DengShiyingA/yingclaw/main/screenshot.png)
8
8
 
@@ -69,6 +69,10 @@ claw desktop
69
69
  | MiniMax | MiniMax-M2.7 | MiniMax-M2.7-Turbo |
70
70
  | 智谱 GLM | GLM-4.7 | GLM-5-Turbo |
71
71
  | 小米 MiMo | mimo-v2.5-pro | mimo-v2.5 |
72
+ | B.AI | claude-sonnet-4.6 | claude-haiku-4.5 |
73
+ | OpenCode Zen(按量) | claude-sonnet-4-6 | deepseek-v4-flash-free |
74
+ | OpenCode Go(订阅制) | glm-5.1 | deepseek-v4-flash |
75
+ | OpenRouter | deepseek/deepseek-r1-0528 | deepseek/deepseek-chat-v3-0324 |
72
76
  | 自定义 Anthropic 兼容接口 | 自动获取或手动输入 | 手动选择 |
73
77
  | 自定义 OpenAI 兼容接口 | 自动获取或手动输入 | 手动选择 |
74
78
 
package/bin/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  const { Command } = require('commander');
4
- const { select, input, confirm } = require('@inquirer/prompts');
4
+ const { select, input, confirm, search, checkbox } = require('@inquirer/prompts');
5
5
  const {
6
6
  loadConfig,
7
7
  saveConfig,
@@ -54,16 +54,19 @@ const { normalizeOpenAiBaseUrl } = require('../lib/openai');
54
54
 
55
55
  const program = new Command();
56
56
 
57
+ let _bannerCache;
57
58
  async function getBanner() {
59
+ if (_bannerCache) return _bannerCache;
58
60
  const chalk = (await import('chalk')).default;
59
61
  const figlet = require('figlet');
60
62
  const boxen = (await import('boxen')).default;
61
63
  const title = figlet.textSync('yingclaw', { font: 'Small', horizontalLayout: 'fitted' });
62
64
  const subtitle = chalk.dim('Claude Code × 国产大模型 一键接入') + ' ' + chalk.cyan(`v${pkg.version}`);
63
- return boxen(
65
+ _bannerCache = boxen(
64
66
  chalk.cyan.bold(title) + '\n' + subtitle,
65
67
  { padding: { top: 0, bottom: 0, left: 2, right: 2 }, borderStyle: 'round', borderColor: 'cyan', margin: { top: 1, bottom: 0 } }
66
68
  );
69
+ return _bannerCache;
67
70
  }
68
71
 
69
72
  function getConfigValidationMessage(config) {
@@ -164,15 +167,36 @@ async function offerDesktopSync(chalk, ora, config) {
164
167
  }
165
168
  }
166
169
 
170
+ const SEARCH_THRESHOLD = 15;
171
+
167
172
  async function promptModelFromChoices({ chalk, choices, message, backLabel = '↩ 返回上一步', allowManual = true }) {
168
- const selected = await select({ loop: false,
169
- message: chalk.cyan(message),
170
- choices: [
171
- ...choices,
172
- ...(allowManual ? [{ name: '手动输入模型名', value: '__MANUAL__' }] : []),
173
- { name: chalk.dim(backLabel), value: '__BACK__' },
174
- ],
175
- });
173
+ const extraChoices = [
174
+ ...(allowManual ? [{ name: '手动输入模型名', value: '__MANUAL__' }] : []),
175
+ { name: chalk.dim(backLabel), value: '__BACK__' },
176
+ ];
177
+
178
+ let selected;
179
+ if (choices.length > SEARCH_THRESHOLD) {
180
+ selected = await search({
181
+ message: chalk.cyan(message),
182
+ source: (term) => {
183
+ const filtered = term
184
+ ? choices.filter((c) =>
185
+ String(c.name || '').toLowerCase().includes(term.toLowerCase()) ||
186
+ String(c.value || '').toLowerCase().includes(term.toLowerCase())
187
+ )
188
+ : choices;
189
+ return term ? [...filtered, ...extraChoices] : [...extraChoices, ...filtered];
190
+ },
191
+ pageSize: 15,
192
+ });
193
+ } else {
194
+ selected = await select({ loop: false,
195
+ message: chalk.cyan(message),
196
+ choices: [...choices, ...extraChoices],
197
+ });
198
+ }
199
+
176
200
  if (selected === '__BACK__') return '__BACK__';
177
201
  if (selected !== '__MANUAL__') return selected;
178
202
 
@@ -182,10 +206,115 @@ async function promptModelFromChoices({ chalk, choices, message, backLabel = '
182
206
  }).then(v => v.trim());
183
207
  }
184
208
 
209
+ const DESKTOP_MODEL_SELECT_THRESHOLD = 10;
210
+ const DESKTOP_MODEL_MAX = 10;
211
+
212
+ async function promptDesktopModels(chalk, config, chatModels) {
213
+ const savedModels = Array.isArray(config.desktopModels)
214
+ ? config.desktopModels.filter((m) => chatModels.includes(m))
215
+ : [];
216
+
217
+ // If there is a saved selection, ask whether to keep it or reselect
218
+ if (savedModels.length > 0) {
219
+ console.log(chalk.dim(`上次选择(${savedModels.length} 个):${savedModels.join('、')}`));
220
+ let keep;
221
+ try {
222
+ keep = await confirm({ message: chalk.cyan('保留上次选择?'), default: true });
223
+ } catch (e) {
224
+ if (e?.name === 'ExitPromptError') return null;
225
+ throw e;
226
+ }
227
+ if (keep) return savedModels;
228
+ }
229
+
230
+ const selected = savedModels.length > 0
231
+ ? [...savedModels]
232
+ : [config.model].filter((m) => m && chatModels.includes(m));
233
+
234
+ const choices = chatModels.map((m) => ({ name: m, value: m }));
235
+
236
+ const DONE = '__DONE__';
237
+ const BACK = '__BACK__';
238
+
239
+ while (true) {
240
+ if (selected.length > 0) {
241
+ console.log(chalk.green(`已选 ${selected.length} 个:${selected.join('、')}`));
242
+ }
243
+ const remaining = choices.filter((c) => !selected.includes(c.value));
244
+
245
+ let picked;
246
+ try {
247
+ picked = await search({
248
+ message: chalk.cyan(selected.length > 0 ? `继续添加桌面模型` : `选择桌面模型`) + chalk.dim(` 共 ${chatModels.length} 个可用`),
249
+ source: (term) => {
250
+ const filtered = term
251
+ ? remaining.filter((c) => c.value.toLowerCase().includes(term.toLowerCase()))
252
+ : remaining;
253
+ return [
254
+ ...(selected.length > 0 ? [{ name: chalk.green('✔ 完成'), value: DONE, short: '完成' }] : []),
255
+ { name: chalk.dim('↩ 返回主菜单'), value: BACK, short: '返回主菜单' },
256
+ ...filtered,
257
+ ];
258
+ },
259
+ pageSize: 15,
260
+ });
261
+ } catch (e) {
262
+ if (e?.name === 'ExitPromptError') return null;
263
+ throw e;
264
+ }
265
+
266
+ if (picked === BACK) return null;
267
+ if (picked === DONE) return selected;
268
+
269
+ selected.push(picked);
270
+ if (selected.length >= DESKTOP_MODEL_MAX) return selected;
271
+ }
272
+ }
273
+
185
274
  function getChatModelChoices(models) {
186
275
  return models.filter(isDesktopChatModel).map(id => ({ name: id, value: id }));
187
276
  }
188
277
 
278
+ const OPENCODE_GROUP_VALUE = '__opencode__';
279
+
280
+ async function promptProviderKey(chalk, message = '选择 AI 厂商') {
281
+ const buildMainChoices = () => {
282
+ const entries = Object.entries(PROVIDERS)
283
+ .filter(([key]) => key !== 'opencode_zen' && key !== 'opencode_go')
284
+ .map(([value, p]) => ({ name: p.name, value }));
285
+ const openrouterIdx = entries.findIndex((c) => c.value === 'openrouter');
286
+ entries.splice(openrouterIdx, 0, { name: 'OpenCode', value: OPENCODE_GROUP_VALUE });
287
+ return [...entries, { name: chalk.dim('↩ 返回主菜单'), value: '__BACK__' }];
288
+ };
289
+
290
+ while (true) {
291
+ let mainKey;
292
+ try {
293
+ mainKey = await select({ loop: false, message: chalk.cyan(message), choices: buildMainChoices() });
294
+ } catch (e) {
295
+ if (e?.name === 'ExitPromptError') return null;
296
+ throw e;
297
+ }
298
+ if (mainKey !== OPENCODE_GROUP_VALUE) return mainKey;
299
+
300
+ let sub;
301
+ try {
302
+ sub = await select({ loop: false,
303
+ message: chalk.cyan('选择 OpenCode 服务'),
304
+ choices: [
305
+ { name: 'Zen(按量计费)', value: 'opencode_zen' },
306
+ { name: 'Go(订阅制 $10/月)', value: 'opencode_go' },
307
+ { name: chalk.dim('↩ 返回上一级'), value: '__BACK__' },
308
+ ],
309
+ });
310
+ } catch (e) {
311
+ if (e?.name === 'ExitPromptError') return null;
312
+ throw e;
313
+ }
314
+ if (sub !== '__BACK__') return sub;
315
+ }
316
+ }
317
+
189
318
  function sortModelChoicesByRelatedFamily(choices, mainModel) {
190
319
  const order = sortRelatedModelIds(choices.map((choice) => choice.value), mainModel);
191
320
  const rank = new Map(order.map((value, index) => [value, index]));
@@ -384,14 +513,8 @@ async function runConfigFlow({ writeCodeEnv = false } = {}) {
384
513
 
385
514
  while (true) {
386
515
  if (step === 'provider') {
387
- providerKey = await select({ loop: false,
388
- message: chalk.cyan('选择 AI 厂商'),
389
- choices: [
390
- ...Object.entries(PROVIDERS).map(([value, p]) => ({ name: p.name, value })),
391
- { name: chalk.dim('↩ 返回主菜单'), value: '__BACK__' },
392
- ],
393
- });
394
- if (providerKey === '__BACK__') return;
516
+ providerKey = await promptProviderKey(chalk);
517
+ if (!providerKey || providerKey === '__BACK__') return;
395
518
  provider = PROVIDERS[providerKey];
396
519
  if (provider.custom) {
397
520
  customConfig = await configureCustomProvider({ chalk, ora, providerKey });
@@ -429,12 +552,12 @@ async function runConfigFlow({ writeCodeEnv = false } = {}) {
429
552
  availableModels = provider.models.map(m => m.value);
430
553
  }
431
554
 
432
- const m = await select({ loop: false,
433
- message: chalk.cyan('选择模型'),
434
- choices: [
435
- ...modelChoices,
436
- { name: chalk.dim('↩ 返回上一步(重新输入 Key)'), value: '__BACK__' },
437
- ],
555
+ const m = await promptModelFromChoices({
556
+ chalk,
557
+ choices: modelChoices,
558
+ message: '选择模型',
559
+ backLabel: '↩ 返回上一步(重新输入 Key)',
560
+ allowManual: false,
438
561
  });
439
562
  if (m === '__BACK__') { step = 'apikey'; continue; }
440
563
  model = m;
@@ -474,7 +597,6 @@ async function runConfigFlow({ writeCodeEnv = false } = {}) {
474
597
  { padding: { top: 0, bottom: 0, left: 2, right: 2 }, borderStyle: 'round', borderColor: 'green', margin: { top: 1, bottom: 1 } }
475
598
  ));
476
599
 
477
- await offerDesktopSync(chalk, ora, cfg);
478
600
  }
479
601
 
480
602
  program
@@ -613,7 +735,7 @@ program
613
735
  }
614
736
 
615
737
  console.log(chalk.dim(getStorageHint(file)));
616
- const displayedConfig = ensureGatewayForOpenAiConfig(config);
738
+ const displayedConfig = effectiveConfig;
617
739
  console.log(boxen(
618
740
  chalk.bold('Claude Code 终端已接入\n\n') +
619
741
  chalk.dim('Base URL ') + chalk.cyan(buildClaudeEnv(displayedConfig).ANTHROPIC_BASE_URL) + '\n' +
@@ -692,10 +814,11 @@ program
692
814
  if (effectiveConfig !== config) saveConfig(effectiveConfig);
693
815
 
694
816
  const vscodeModelChoices = getChatModelChoices(buildVsCodeModelSelectionIds(effectiveConfig));
817
+ const vscodeModelCount = vscodeModelChoices.length;
695
818
  const vscodeModel = await promptModelFromChoices({
696
819
  chalk,
697
820
  choices: vscodeModelChoices,
698
- message: '选择 VS Code Claude Code 主模型',
821
+ message: `选择 VS Code Claude Code 主模型 共 ${vscodeModelCount} 个可用`,
699
822
  allowManual: true,
700
823
  });
701
824
  if (vscodeModel === '__BACK__') return;
@@ -703,7 +826,7 @@ program
703
826
  const vscodeFastModel = await promptModelFromChoices({
704
827
  chalk,
705
828
  choices: sortModelChoicesByRelatedFamily(vscodeModelChoices, vscodeModel),
706
- message: '选择 VS Code Claude Code 快速模型 / 备用模型',
829
+ message: `选择 VS Code Claude Code 快速模型 / 备用模型 共 ${vscodeModelCount} 个可用`,
707
830
  allowManual: true,
708
831
  });
709
832
  if (vscodeFastModel === '__BACK__') return;
@@ -799,14 +922,8 @@ program
799
922
  return;
800
923
  }
801
924
 
802
- const providerKey = await select({ loop: false,
803
- message: chalk.cyan('选择 AI 厂商'),
804
- choices: [
805
- ...Object.entries(PROVIDERS).map(([value, p]) => ({ name: p.name, value })),
806
- { name: chalk.dim('↩ 返回主菜单'), value: '__BACK__' },
807
- ],
808
- });
809
- if (providerKey === '__BACK__') return;
925
+ const providerKey = await promptProviderKey(chalk);
926
+ if (!providerKey || providerKey === '__BACK__') return;
810
927
 
811
928
  const provider = PROVIDERS[providerKey];
812
929
  if (provider.custom) {
@@ -818,7 +935,6 @@ program
818
935
  saveConfig(savedConfig);
819
936
  spinner.succeed(chalk.green(`API 连接已切换至 ${savedConfig.providerName} · ${savedConfig.model}`));
820
937
  console.log(chalk.dim('如需让外部 claude 命令使用新模型,请运行 claw code。'));
821
- await offerDesktopSync(chalk, ora, savedConfig);
822
938
  return;
823
939
  }
824
940
 
@@ -859,12 +975,12 @@ program
859
975
  availableModels = provider.models.map(m => m.value);
860
976
  }
861
977
 
862
- const model = await select({ loop: false,
863
- message: chalk.cyan('选择模型'),
864
- choices: [
865
- ...modelChoices,
866
- { name: chalk.dim('↩ 返回主菜单'), value: '__BACK__' },
867
- ],
978
+ const model = await promptModelFromChoices({
979
+ chalk,
980
+ choices: modelChoices,
981
+ message: '选择模型',
982
+ backLabel: '↩ 返回主菜单',
983
+ allowManual: false,
868
984
  });
869
985
  if (model === '__BACK__') return;
870
986
 
@@ -874,7 +990,6 @@ program
874
990
  saveConfig(newConfig);
875
991
  spinner.succeed(chalk.green(`API 连接已切换至 ${provider.name} · ${model}`));
876
992
  console.log(chalk.dim('如需让外部 claude 命令使用新模型,请运行 claw code。'));
877
- await offerDesktopSync(chalk, ora, newConfig);
878
993
  });
879
994
 
880
995
  program
@@ -963,6 +1078,16 @@ program
963
1078
  if (desktopConfig.desktopGatewayKey !== config.desktopGatewayKey || desktopConfig.desktopGatewayPort !== config.desktopGatewayPort) {
964
1079
  saveConfig(desktopConfig);
965
1080
  }
1081
+
1082
+ const chatModels = Array.isArray(desktopConfig.availableModels)
1083
+ ? desktopConfig.availableModels.filter(isDesktopChatModel)
1084
+ : [];
1085
+ if (chatModels.length > DESKTOP_MODEL_SELECT_THRESHOLD) {
1086
+ const selectedModels = await promptDesktopModels(chalk, desktopConfig, chatModels);
1087
+ if (!selectedModels) return;
1088
+ saveConfig({ ...desktopConfig, desktopModels: selectedModels });
1089
+ desktopConfig = { ...desktopConfig, availableModels: selectedModels };
1090
+ }
966
1091
  }
967
1092
 
968
1093
  const spinner = ora('写入 Claude 桌面应用配置...').start();
@@ -1272,15 +1397,18 @@ program
1272
1397
  { padding: { top: 0, bottom: 0, left: 2, right: 2 }, borderStyle: 'round', borderColor: 'green', margin: { top: 1, bottom: 1 } }
1273
1398
  ));
1274
1399
  process.exit(0);
1400
+ } else if (result.status === null) {
1401
+ console.log(chalk.red('\nnpm 命令未找到,请确认 Node.js 和 npm 已正确安装后手动运行:'));
1402
+ console.log(chalk.cyan('npm install -g yingclaw@latest'));
1275
1403
  } else {
1276
1404
  console.log(chalk.red('\n升级失败,请手动运行:'));
1277
1405
  console.log(chalk.cyan('npm install -g yingclaw@latest'));
1278
1406
  }
1279
1407
  });
1280
1408
 
1281
- async function renderStatusBar(apiStatus) {
1409
+ async function renderStatusBar(apiStatus, config) {
1282
1410
  const chalk = (await import('chalk')).default;
1283
- const config = loadConfig();
1411
+ if (config === undefined) config = loadConfig();
1284
1412
  const claudeInstalled = isClaudeInstalled();
1285
1413
 
1286
1414
  const claudeIcon = claudeInstalled ? chalk.green('●') : chalk.red('●');
@@ -1408,7 +1536,7 @@ async function runMenu() {
1408
1536
  console.log(chalk.red(` ● 配置无效:${configProblem}`));
1409
1537
  console.log(chalk.dim(' 请先选择"配置 API 连接"重新配置'));
1410
1538
  } else {
1411
- console.log(await renderStatusBar(apiStatus));
1539
+ console.log(await renderStatusBar(apiStatus, config));
1412
1540
  }
1413
1541
  console.log();
1414
1542
 
package/lib/config.js CHANGED
@@ -109,6 +109,66 @@ const PROVIDERS = {
109
109
  { name: 'MiMo V2 Flash(快速)', value: 'mimo-v2-flash' },
110
110
  ],
111
111
  },
112
+ bai: {
113
+ name: 'B.AI',
114
+ baseUrl: 'https://api.b.ai',
115
+ modelsUrl: 'https://api.b.ai/v1/models',
116
+ fastModel: 'claude-haiku-4.5',
117
+ models: [
118
+ { name: 'Claude Sonnet 4.6(推荐)', value: 'claude-sonnet-4.6' },
119
+ { name: 'Claude Opus 4.7(强力)', value: 'claude-opus-4.7' },
120
+ { name: 'Claude Haiku 4.5(快速)', value: 'claude-haiku-4.5' },
121
+ { name: 'DeepSeek V4 Pro', value: 'deepseek-v4-pro' },
122
+ { name: 'GLM-5.1', value: 'glm-5.1' },
123
+ { name: 'MiniMax M2.7', value: 'minimax-m2.7' },
124
+ ],
125
+ },
126
+ opencode_zen: {
127
+ name: 'OpenCode Zen(按量)',
128
+ protocol: 'openai',
129
+ baseUrl: 'https://opencode.ai/zen',
130
+ modelsUrl: 'https://opencode.ai/zen/v1/models',
131
+ fastModel: 'deepseek-v4-flash-free',
132
+ models: [
133
+ { name: 'Claude Sonnet 4.6(推荐)', value: 'claude-sonnet-4-6' },
134
+ { name: 'Claude Opus 4.7(强力)', value: 'claude-opus-4-7' },
135
+ { name: 'Claude Haiku 4.5(快速)', value: 'claude-haiku-4-5' },
136
+ { name: 'GLM 5.1', value: 'glm-5.1' },
137
+ { name: 'MiniMax M2.7', value: 'minimax-m2.7' },
138
+ { name: 'Kimi K2.5', value: 'kimi-k2.5' },
139
+ { name: 'DeepSeek V4 Flash(免费)', value: 'deepseek-v4-flash-free' },
140
+ ],
141
+ },
142
+ opencode_go: {
143
+ name: 'OpenCode Go(订阅制)',
144
+ protocol: 'openai',
145
+ baseUrl: 'https://opencode.ai/zen/go',
146
+ modelsUrl: 'https://opencode.ai/zen/go/v1/models',
147
+ fastModel: 'deepseek-v4-flash',
148
+ models: [
149
+ { name: 'GLM-5.1', value: 'glm-5.1' },
150
+ { name: 'Kimi K2.5', value: 'kimi-k2.5' },
151
+ { name: 'DeepSeek V4 Pro(强力)', value: 'deepseek-v4-pro' },
152
+ { name: 'DeepSeek V4 Flash(快速)', value: 'deepseek-v4-flash' },
153
+ { name: 'MiniMax M2.7', value: 'minimax-m2.7' },
154
+ { name: 'MiniMax M2.5', value: 'minimax-m2.5' },
155
+ { name: 'MiMo V2.5 Pro', value: 'mimo-v2.5-pro' },
156
+ { name: 'Qwen3.6 Plus', value: 'qwen3.6-plus' },
157
+ ],
158
+ },
159
+ openrouter: {
160
+ name: 'OpenRouter',
161
+ protocol: 'openai',
162
+ baseUrl: 'https://openrouter.ai/api/v1',
163
+ modelsUrl: 'https://openrouter.ai/api/v1/models',
164
+ fastModel: 'deepseek/deepseek-chat-v3-0324',
165
+ models: [
166
+ { name: 'DeepSeek R1 0528(推理)', value: 'deepseek/deepseek-r1-0528' },
167
+ { name: 'DeepSeek Chat V3(快速)', value: 'deepseek/deepseek-chat-v3-0324' },
168
+ { name: 'Gemini 2.5 Pro(Google)', value: 'google/gemini-2.5-pro' },
169
+ { name: 'Claude Sonnet 4.5(Anthropic)', value: 'anthropic/claude-sonnet-4-5' },
170
+ ],
171
+ },
112
172
  custom: {
113
173
  name: '自定义 Anthropic 兼容接口',
114
174
  custom: true,
@@ -128,6 +188,8 @@ const PREFERRED_MODEL_IDS = {
128
188
  minimax: ['MiniMax-M2.7', 'MiniMax-M2.7-Turbo', 'MiniMax-M2.5'],
129
189
  glm: ['GLM-4.7', 'GLM-5.1', 'GLM-5-Turbo', 'GLM-4.5-Air'],
130
190
  mimo: ['mimo-v2.5-pro', 'mimo-v2.5', 'mimo-v2-flash'],
191
+ opencode_zen: ['claude-sonnet-4-6', 'claude-opus-4-7', 'claude-haiku-4-5', 'deepseek-v4-flash-free'],
192
+ opencode_go: ['glm-5.1', 'deepseek-v4-pro', 'deepseek-v4-flash'],
131
193
  };
132
194
 
133
195
  function sortPreferredModelIds(providerKey, ids) {
@@ -143,7 +205,7 @@ function isChatModelId(model) {
143
205
  const value = String(model || '');
144
206
  return !/(^|[-_])(tts|voice|voiceclone|voicedesign|speech|audio|image|video|embedding|embed|rerank|moderation)([-_]|$)/i.test(value)
145
207
  && !/(^|[-_])(i2v|t2v|t2i|i2i|v2v|video-generation|image-generation)([-_]|$)/i.test(value)
146
- && !/(cogview|cogvideo|wanx|^wan\d|seedance|seedream|seededit|vision|hyper3d|hitem3d|seed3d|flux|sdxl|stable-diffusion)/i.test(value);
208
+ && !/(cogview|cogvideo|wanx|^wan\d|seedance|seedream|seededit|vision|hyper3d|hitem3d|seed3d|flux|sdxl|stable-diffusion|dall-e|whisper)/i.test(value);
147
209
  }
148
210
 
149
211
  function normalizeModelIds(providerKey, ids) {
@@ -185,6 +247,9 @@ function parseModelIdsResponse(providerKey, data) {
185
247
  if (providerKey === 'volcengine') {
186
248
  return normalizeModelIds(providerKey, ids.filter(isChatModelId));
187
249
  }
250
+ if (providerKey === 'bai') {
251
+ return normalizeModelIds(providerKey, ids.filter(id => !id.includes('/')));
252
+ }
188
253
  return normalizeModelIds(providerKey, ids);
189
254
  }
190
255
 
@@ -403,15 +468,17 @@ function runWindowsEnvCommands(commands, runner = spawnSync, { ignoreErrors = fa
403
468
 
404
469
  function classifyValidationStatus(statusCode) {
405
470
  if (statusCode >= 200 && statusCode < 300) return true;
471
+ if (statusCode === 429) return true; // 限流 = Key 有效
406
472
  if (statusCode === 401 || statusCode === 403) return false;
407
473
  return null;
408
474
  }
409
475
 
410
476
  // 发一次最小的 /v1/messages 请求验证 Key(true=有效, false=无效, null=网络/服务异常)
411
477
  async function validateKey(config, options = {}) {
478
+ const protocol = getProviderProtocol(config);
412
479
  let url;
413
480
  try {
414
- url = getProviderProtocol(config) === 'openai'
481
+ url = protocol === 'openai'
415
482
  ? openAiChatCompletionsUrl(config.baseUrl)
416
483
  : `${normalizeAnthropicBaseUrl(config.baseUrl)}/v1/messages`;
417
484
  new URL(url);
@@ -428,20 +495,13 @@ async function validateKey(config, options = {}) {
428
495
  headers: {
429
496
  'content-type': 'application/json',
430
497
  ...buildProviderAuthHeaders(config.provider, config.apiKey),
431
- ...(getProviderProtocol(config) === 'anthropic' ? { 'x-api-key': config.apiKey } : {}),
432
- ...(getProviderProtocol(config) === 'anthropic' ? { 'anthropic-version': '2023-06-01' } : {}),
498
+ ...(protocol === 'anthropic' ? { 'x-api-key': config.apiKey, 'anthropic-version': '2023-06-01' } : {}),
433
499
  },
434
- body: JSON.stringify(getProviderProtocol(config) === 'openai'
435
- ? {
436
- model: config.model,
437
- max_tokens: 1,
438
- messages: [{ role: 'user', content: 'hi' }],
439
- }
440
- : {
441
- model: config.model,
442
- max_tokens: 1,
443
- messages: [{ role: 'user', content: 'hi' }],
444
- }),
500
+ body: JSON.stringify({
501
+ model: config.model,
502
+ max_tokens: 16,
503
+ messages: [{ role: 'user', content: 'hi' }],
504
+ }),
445
505
  });
446
506
  return classifyValidationStatus(res.status);
447
507
  } catch {
package/lib/doctor.js CHANGED
@@ -110,7 +110,7 @@ async function runDoctorChecks(options = {}) {
110
110
  checks.push({
111
111
  name: 'VS Code Claude Code',
112
112
  status: vscodeSettings.configured ? STATUS_OK : STATUS_WARN,
113
- message: vscodeSettings.configured ? '共享 settings.json 已写入' : `${vscodeSettings.missing.length} 个变量未写入共享设置`,
113
+ message: vscodeSettings.configured ? 'VS Code 扩展设置已配置' : `${vscodeSettings.missing.length} 项配置未写入 VS Code 扩展设置`,
114
114
  fix: vscodeSettings.configured ? null : '运行 claw vscode,并重启 VS Code',
115
115
  });
116
116
 
package/lib/gateway.js CHANGED
@@ -74,6 +74,25 @@ function isDesktopChatModel(model) {
74
74
  return isChatModelId(model);
75
75
  }
76
76
 
77
+ function isNativeClaudeModel(modelId) {
78
+ return /^claude-/i.test(String(modelId || ''));
79
+ }
80
+
81
+ function stripThinkingFromBody(body) {
82
+ const cleaned = { ...body };
83
+ delete cleaned.thinking;
84
+ if (Array.isArray(cleaned.messages)) {
85
+ cleaned.messages = cleaned.messages.map((msg) => {
86
+ if (!Array.isArray(msg.content)) return msg;
87
+ const content = msg.content.filter(
88
+ (block) => block.type !== 'thinking' && block.type !== 'redacted_thinking',
89
+ );
90
+ return { ...msg, content: content.length > 0 ? content : [{ type: 'text', text: '' }] };
91
+ });
92
+ }
93
+ return cleaned;
94
+ }
95
+
77
96
  function buildDesktopGatewayRoutes(config) {
78
97
  const fastModel = config.fastModel || config.model;
79
98
  const configuredModels = [config.model, fastModel];
@@ -224,6 +243,10 @@ async function proxyMessages(req, res, config) {
224
243
  return;
225
244
  }
226
245
 
246
+ if (!isNativeClaudeModel(body.model)) {
247
+ body = stripThinkingFromBody(body);
248
+ }
249
+
227
250
  if (getProviderProtocol(config) === 'openai') {
228
251
  await proxyOpenAiMessages(res, config, body);
229
252
  return;
@@ -306,10 +329,8 @@ async function proxyOpenAiMessages(res, config, body) {
306
329
  const state = { model: body.model, started: false, finished: false };
307
330
  let buffer = '';
308
331
  const decoder = new TextDecoder();
309
- for await (const chunk of upstream.body) {
310
- buffer += decoder.decode(chunk, { stream: true });
311
- const frames = buffer.split(/\n\n/);
312
- buffer = frames.pop() || '';
332
+
333
+ function processFrames(frames) {
313
334
  for (const frame of frames) {
314
335
  const dataLine = frame.split(/\n/).find((line) => line.startsWith('data:'));
315
336
  if (!dataLine) continue;
@@ -324,6 +345,17 @@ async function proxyOpenAiMessages(res, config, body) {
324
345
  res.write(openAiStreamChunkToAnthropicEvents(parsed, state));
325
346
  }
326
347
  }
348
+
349
+ for await (const chunk of upstream.body) {
350
+ buffer += decoder.decode(chunk, { stream: true });
351
+ const frames = buffer.split(/\n\n/);
352
+ buffer = frames.pop() || '';
353
+ processFrames(frames);
354
+ }
355
+ // flush any remaining buffer content not terminated by \n\n
356
+ if (buffer.trim()) {
357
+ processFrames([buffer]);
358
+ }
327
359
  if (!state.finished) {
328
360
  res.write(openAiStreamChunkToAnthropicEvents({ model: body.model, choices: [{ delta: {}, finish_reason: 'stop' }] }, state));
329
361
  }
package/lib/openai.js CHANGED
@@ -44,13 +44,74 @@ function normalizeSystemMessages(system) {
44
44
  return [];
45
45
  }
46
46
 
47
+ function anthropicToolUseToOpenAi(block) {
48
+ return {
49
+ id: block.id,
50
+ type: 'function',
51
+ function: {
52
+ name: block.name,
53
+ arguments: typeof block.input === 'string' ? block.input : JSON.stringify(block.input || {}),
54
+ },
55
+ };
56
+ }
57
+
58
+ function anthropicToolResultContentToText(content) {
59
+ if (typeof content === 'string') return content;
60
+ if (Array.isArray(content)) {
61
+ return content.filter(p => p?.type === 'text').map(p => p.text || '').join('\n');
62
+ }
63
+ return '';
64
+ }
65
+
66
+ function anthropicMessageToOpenAi(message) {
67
+ const content = message.content;
68
+ if (message.role === 'assistant') {
69
+ const textParts = Array.isArray(content)
70
+ ? content.filter(p => p?.type === 'text').map(p => p.text || '').join('')
71
+ : typeof content === 'string' ? content : '';
72
+ const toolUses = Array.isArray(content) ? content.filter(p => p?.type === 'tool_use') : [];
73
+ const msg = { role: 'assistant', content: textParts || null };
74
+ if (toolUses.length > 0) {
75
+ msg.tool_calls = toolUses.map(anthropicToolUseToOpenAi);
76
+ }
77
+ if (msg.content === null && !msg.tool_calls) msg.content = '';
78
+ return [msg];
79
+ }
80
+
81
+ // user role: split tool_result blocks into separate 'tool' messages
82
+ if (!Array.isArray(content)) {
83
+ return [{ role: 'user', content: anthropicTextFromContent(content) }];
84
+ }
85
+ const toolResults = content.filter(p => p?.type === 'tool_result');
86
+ const other = content.filter(p => p?.type !== 'tool_result');
87
+ const messages = toolResults.map(tr => ({
88
+ role: 'tool',
89
+ tool_call_id: tr.tool_use_id,
90
+ content: anthropicToolResultContentToText(tr.content),
91
+ }));
92
+ const text = anthropicTextFromContent(other);
93
+ if (text) messages.push({ role: 'user', content: text });
94
+ if (messages.length === 0) messages.push({ role: 'user', content: '' });
95
+ return messages;
96
+ }
97
+
98
+ function anthropicToolsToOpenAi(tools) {
99
+ if (!Array.isArray(tools)) return undefined;
100
+ const converted = tools.map(tool => ({
101
+ type: 'function',
102
+ function: {
103
+ name: tool.name,
104
+ description: tool.description || '',
105
+ parameters: tool.input_schema || { type: 'object', properties: {} },
106
+ },
107
+ }));
108
+ return converted.length > 0 ? converted : undefined;
109
+ }
110
+
47
111
  function anthropicToOpenAiChatRequest(body) {
48
112
  const messages = [
49
113
  ...normalizeSystemMessages(body.system),
50
- ...(Array.isArray(body.messages) ? body.messages : []).map((message) => ({
51
- role: message.role === 'assistant' ? 'assistant' : 'user',
52
- content: anthropicTextFromContent(message.content),
53
- })),
114
+ ...(Array.isArray(body.messages) ? body.messages : []).flatMap(anthropicMessageToOpenAi),
54
115
  ];
55
116
 
56
117
  const request = {
@@ -63,6 +124,8 @@ function anthropicToOpenAiChatRequest(body) {
63
124
  if (body.temperature !== undefined) request.temperature = body.temperature;
64
125
  if (body.top_p !== undefined) request.top_p = body.top_p;
65
126
  if (body.stop_sequences !== undefined) request.stop = body.stop_sequences;
127
+ const tools = anthropicToolsToOpenAi(body.tools);
128
+ if (tools) request.tools = tools;
66
129
  return request;
67
130
  }
68
131
 
@@ -73,18 +136,38 @@ function openAiFinishReasonToAnthropic(reason) {
73
136
  return reason || 'end_turn';
74
137
  }
75
138
 
139
+ function openAiToolCallToAnthropic(toolCall) {
140
+ let input = {};
141
+ try { input = JSON.parse(toolCall.function?.arguments || '{}'); } catch {}
142
+ return {
143
+ type: 'tool_use',
144
+ id: toolCall.id || `tool_${Date.now()}`,
145
+ name: toolCall.function?.name || '',
146
+ input,
147
+ };
148
+ }
149
+
76
150
  function openAiToAnthropicMessage(body, fallbackModel) {
77
151
  const choice = body?.choices?.[0] || {};
78
- const content = choice.message?.content ?? choice.text ?? '';
79
- const text = Array.isArray(content)
80
- ? content.map((part) => part?.text || '').join('')
81
- : String(content || '');
152
+ const message = choice.message || {};
153
+ const rawContent = message.content ?? choice.text ?? '';
154
+ const text = Array.isArray(rawContent)
155
+ ? rawContent.map((part) => part?.text || '').join('')
156
+ : String(rawContent || '');
157
+
158
+ const contentBlocks = [];
159
+ if (text) contentBlocks.push({ type: 'text', text });
160
+ if (Array.isArray(message.tool_calls)) {
161
+ for (const tc of message.tool_calls) contentBlocks.push(openAiToolCallToAnthropic(tc));
162
+ }
163
+ if (contentBlocks.length === 0) contentBlocks.push({ type: 'text', text: '' });
164
+
82
165
  return {
83
166
  id: body?.id || `msg_${Date.now()}`,
84
167
  type: 'message',
85
168
  role: 'assistant',
86
169
  model: body?.model || fallbackModel,
87
- content: [{ type: 'text', text }],
170
+ content: contentBlocks,
88
171
  stop_reason: openAiFinishReasonToAnthropic(choice.finish_reason),
89
172
  stop_sequence: null,
90
173
  usage: {
@@ -102,8 +185,14 @@ function openAiStreamChunkToAnthropicEvents(chunk, state) {
102
185
  const choice = chunk?.choices?.[0] || {};
103
186
  const delta = choice.delta || {};
104
187
  const events = [];
188
+
105
189
  if (!state.started) {
106
190
  state.started = true;
191
+ // index 0 is always the text block
192
+ state.textBlockOpen = false;
193
+ // map tool_call index → anthropic content block index
194
+ state.toolIndexMap = {};
195
+ state.nextBlockIndex = 0;
107
196
  events.push(anthropicSse('message_start', {
108
197
  type: 'message_start',
109
198
  message: {
@@ -114,32 +203,85 @@ function openAiStreamChunkToAnthropicEvents(chunk, state) {
114
203
  content: [],
115
204
  stop_reason: null,
116
205
  stop_sequence: null,
117
- usage: { input_tokens: 0, output_tokens: 0 },
206
+ usage: { input_tokens: chunk?.usage?.prompt_tokens ?? 0, output_tokens: 0 },
118
207
  },
119
208
  }));
120
- events.push(anthropicSse('content_block_start', {
121
- type: 'content_block_start',
122
- index: 0,
123
- content_block: { type: 'text', text: '' },
124
- }));
125
209
  }
210
+
126
211
  if (delta.content) {
212
+ if (!state.textBlockOpen) {
213
+ state.textBlockOpen = true;
214
+ state.textBlockIndex = state.nextBlockIndex++;
215
+ events.push(anthropicSse('content_block_start', {
216
+ type: 'content_block_start',
217
+ index: state.textBlockIndex,
218
+ content_block: { type: 'text', text: '' },
219
+ }));
220
+ }
127
221
  events.push(anthropicSse('content_block_delta', {
128
222
  type: 'content_block_delta',
129
- index: 0,
223
+ index: state.textBlockIndex,
130
224
  delta: { type: 'text_delta', text: delta.content },
131
225
  }));
132
226
  }
227
+
228
+ if (Array.isArray(delta.tool_calls)) {
229
+ for (const tc of delta.tool_calls) {
230
+ const tcIdx = tc.index ?? 0;
231
+ if (!(tcIdx in state.toolIndexMap)) {
232
+ // close text block first if open
233
+ if (state.textBlockOpen && !state.textBlockClosed) {
234
+ state.textBlockClosed = true;
235
+ events.push(anthropicSse('content_block_stop', { type: 'content_block_stop', index: state.textBlockIndex }));
236
+ }
237
+ const blockIndex = state.nextBlockIndex++;
238
+ state.toolIndexMap[tcIdx] = blockIndex;
239
+ let input = {};
240
+ try { if (tc.function?.arguments) input = JSON.parse(tc.function.arguments); } catch {}
241
+ events.push(anthropicSse('content_block_start', {
242
+ type: 'content_block_start',
243
+ index: blockIndex,
244
+ content_block: {
245
+ type: 'tool_use',
246
+ id: tc.id || `tool_${Date.now()}_${tcIdx}`,
247
+ name: tc.function?.name || '',
248
+ input,
249
+ },
250
+ }));
251
+ }
252
+ if (tc.function?.arguments) {
253
+ events.push(anthropicSse('content_block_delta', {
254
+ type: 'content_block_delta',
255
+ index: state.toolIndexMap[tcIdx],
256
+ delta: { type: 'input_json_delta', partial_json: tc.function.arguments },
257
+ }));
258
+ }
259
+ }
260
+ }
261
+
133
262
  if (choice.finish_reason) {
134
263
  state.finished = true;
135
- events.push(anthropicSse('content_block_stop', { type: 'content_block_stop', index: 0 }));
264
+ // close any open blocks
265
+ if (state.textBlockOpen && !state.textBlockClosed) {
266
+ events.push(anthropicSse('content_block_stop', { type: 'content_block_stop', index: state.textBlockIndex }));
267
+ }
268
+ for (const blockIndex of Object.values(state.toolIndexMap)) {
269
+ events.push(anthropicSse('content_block_stop', { type: 'content_block_stop', index: blockIndex }));
270
+ }
271
+ // if nothing was opened at all, emit an empty text block
272
+ if (!state.textBlockOpen && Object.keys(state.toolIndexMap).length === 0) {
273
+ events.push(anthropicSse('content_block_start', { type: 'content_block_start', index: 0, content_block: { type: 'text', text: '' } }));
274
+ events.push(anthropicSse('content_block_stop', { type: 'content_block_stop', index: 0 }));
275
+ }
276
+ const outputTokens = chunk?.usage?.completion_tokens ?? chunk?.usage?.output_tokens ?? 0;
136
277
  events.push(anthropicSse('message_delta', {
137
278
  type: 'message_delta',
138
279
  delta: { stop_reason: openAiFinishReasonToAnthropic(choice.finish_reason), stop_sequence: null },
139
- usage: { output_tokens: 0 },
280
+ usage: { output_tokens: outputTokens },
140
281
  }));
141
282
  events.push(anthropicSse('message_stop', { type: 'message_stop' }));
142
283
  }
284
+
143
285
  return events.join('');
144
286
  }
145
287
 
package/lib/panel.js CHANGED
@@ -94,7 +94,9 @@ function buildMenuStatusLines(view, options = {}) {
94
94
  } else if (options.platform === 'win32') {
95
95
  lines.push('终端未生效:重新打开 PowerShell / CMD');
96
96
  } else {
97
- lines.push('终端未生效:source ~/.zshrc 或重开终端');
97
+ const shell = options.shell || process.env.SHELL || '';
98
+ const rcFile = shell.includes('bash') ? '~/.bashrc' : '~/.zshrc';
99
+ lines.push(`终端未生效:source ${rcFile} 或重开终端`);
98
100
  }
99
101
 
100
102
  const modelCountText = view.availableModelCount ? ` · 可用 ${view.availableModelCount}` : '';
package/lib/vscode.js CHANGED
@@ -2,7 +2,7 @@ const fs = require('fs');
2
2
  const os = require('os');
3
3
  const path = require('path');
4
4
  const { buildClaudeEnv, resolveFastModel, PROVIDERS, CLEAR_CLAUDE_ENV_KEYS } = require('./config');
5
- const { isDesktopChatModel } = require('./gateway');
5
+ const { isDesktopChatModel, buildDesktopGatewayRoutes, buildDesktopGatewayUrl } = require('./gateway');
6
6
 
7
7
  const CLAUDE_CODE_SETTINGS_SCHEMA = 'https://json.schemastore.org/claude-code-settings.json';
8
8
  const VSCODE_CLAUDE_OPEN_URI = 'vscode://anthropic.claude-code/open';
@@ -43,14 +43,10 @@ function getVsCodeUserSettingsPath(options = {}) {
43
43
  }
44
44
 
45
45
  function buildClaudeCodeSettingsPatch(config) {
46
- const env = buildVsCodeClaudeEnv(config);
47
46
  const availableModels = buildVsCodeAvailableModels(config);
48
- const model = config.vscodeModel || config.model;
49
47
  return {
50
48
  $schema: CLAUDE_CODE_SETTINGS_SCHEMA,
51
- model: availableModels.includes(model) ? model : availableModels[0],
52
49
  availableModels,
53
- env,
54
50
  };
55
51
  }
56
52
 
@@ -62,7 +58,7 @@ function buildVsCodeClaudeEnv(config) {
62
58
  const env = {
63
59
  ANTHROPIC_BASE_URL: claudeEnv.ANTHROPIC_BASE_URL,
64
60
  ANTHROPIC_AUTH_TOKEN: claudeEnv.ANTHROPIC_AUTH_TOKEN,
65
- CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY: '1',
61
+ ANTHROPIC_MODEL: claudeEnv.ANTHROPIC_MODEL,
66
62
  CLAUDE_CODE_SUBAGENT_MODEL: resolvedFastModel,
67
63
  CLAUDE_CODE_EFFORT_LEVEL: 'max',
68
64
  };
@@ -91,32 +87,47 @@ function writeClaudeCodeSettings(config, options = {}) {
91
87
  const file = getClaudeCodeSettingsPath(options);
92
88
  const current = readJsonFile(file);
93
89
  const patch = buildClaudeCodeSettingsPatch(config);
90
+
91
+ // Clean up any yingclaw env vars previously written to settings.json
94
92
  const nextEnv = { ...(current.env && typeof current.env === 'object' ? current.env : {}) };
95
93
  for (const key of CLEAR_CLAUDE_ENV_KEYS) {
96
94
  delete nextEnv[key];
97
95
  }
98
- Object.assign(nextEnv, patch.env);
99
96
 
100
97
  const next = {
101
98
  ...current,
102
99
  $schema: current.$schema || patch.$schema,
103
- model: patch.model,
104
100
  availableModels: patch.availableModels,
105
- env: nextEnv,
106
101
  };
102
+
103
+ // Remove model from settings.json — it would also affect the terminal
104
+ delete next.model;
105
+
106
+ if (Object.keys(nextEnv).length > 0) {
107
+ next.env = nextEnv;
108
+ } else {
109
+ delete next.env;
110
+ }
111
+
107
112
  writeJsonFile(file, next);
108
113
  try {
109
114
  fs.chmodSync(file, 0o600);
110
115
  } catch {}
111
- return { result: 'updated', file, env: patch.env };
116
+ return { result: 'updated', file };
112
117
  }
113
118
 
114
- function writeVsCodeExtensionSettings(options = {}) {
119
+ function buildVsCodeExtensionEnvVars(config) {
120
+ const env = buildVsCodeClaudeEnv(config);
121
+ return Object.entries(env).map(([name, value]) => ({ name, value: String(value) }));
122
+ }
123
+
124
+ function writeVsCodeExtensionSettings(config, options = {}) {
115
125
  const file = getVsCodeUserSettingsPath(options);
116
126
  const current = readJsonFile(file);
117
127
  const next = {
118
128
  ...current,
119
129
  'claudeCode.disableLoginPrompt': true,
130
+ 'claudeCode.environmentVariables': buildVsCodeExtensionEnvVars(config),
120
131
  };
121
132
  if (options.useTerminal === true || options.useTerminal === false) {
122
133
  next['claudeCode.useTerminal'] = options.useTerminal;
@@ -127,7 +138,7 @@ function writeVsCodeExtensionSettings(options = {}) {
127
138
 
128
139
  function writeVsCodeIntegration(config, options = {}) {
129
140
  const claudeSettings = writeClaudeCodeSettings(config, options);
130
- const vscodeSettings = writeVsCodeExtensionSettings(options);
141
+ const vscodeSettings = writeVsCodeExtensionSettings(config, options);
131
142
  return {
132
143
  result: 'updated',
133
144
  files: [claudeSettings.file, vscodeSettings.file],
@@ -178,7 +189,7 @@ function clearVsCodeExtensionSettings(options = {}) {
178
189
  const current = readJsonFile(file);
179
190
  const next = { ...current };
180
191
  let changed = false;
181
- for (const key of ['claudeCode.disableLoginPrompt', 'claudeCode.useTerminal']) {
192
+ for (const key of ['claudeCode.disableLoginPrompt', 'claudeCode.useTerminal', 'claudeCode.environmentVariables']) {
182
193
  if (Object.prototype.hasOwnProperty.call(next, key)) {
183
194
  delete next[key];
184
195
  changed = true;
@@ -206,21 +217,33 @@ function clearVsCodeIntegration(options = {}) {
206
217
  }
207
218
 
208
219
  function checkClaudeCodeSettingsEnv(config, options = {}) {
209
- const file = getClaudeCodeSettingsPath(options);
210
- const current = readJsonFile(file);
211
- const env = current.env && typeof current.env === 'object' ? current.env : {};
212
- const expected = buildVsCodeClaudeEnv(config);
213
- const missing = Object.entries(expected)
214
- .filter(([key, value]) => env[key] !== value)
215
- .map(([key]) => key);
220
+ const settingsFile = getClaudeCodeSettingsPath(options);
221
+ const vsCodeFile = getVsCodeUserSettingsPath(options);
222
+ const settings = readJsonFile(settingsFile);
223
+ const vsCodeSettings = readJsonFile(vsCodeFile);
224
+ const missing = [];
225
+
226
+ // Check availableModels in settings.json
216
227
  const expectedModels = buildVsCodeAvailableModels(config);
217
- const expectedModel = expectedModels.includes(config.model) ? config.model : expectedModels[0];
218
- if (current.model !== expectedModel) missing.push('model');
219
- if (!Array.isArray(current.availableModels) || current.availableModels.join(',') !== expectedModels.join(',')) {
228
+ if (!Array.isArray(settings.availableModels) || settings.availableModels.join(',') !== expectedModels.join(',')) {
220
229
  missing.push('availableModels');
221
230
  }
231
+
232
+ // Check claudeCode.environmentVariables in VS Code extension settings
233
+ const expectedEnv = buildVsCodeClaudeEnv(config);
234
+ const actualEnvArray = vsCodeSettings['claudeCode.environmentVariables'];
235
+ if (!Array.isArray(actualEnvArray)) {
236
+ missing.push('claudeCode.environmentVariables');
237
+ } else {
238
+ const actualEnvMap = {};
239
+ for (const { name, value } of actualEnvArray) actualEnvMap[name] = value;
240
+ for (const [key, value] of Object.entries(expectedEnv)) {
241
+ if (actualEnvMap[key] !== String(value)) missing.push(key);
242
+ }
243
+ }
244
+
222
245
  return {
223
- file,
246
+ file: settingsFile,
224
247
  configured: missing.length === 0,
225
248
  missing,
226
249
  };
@@ -242,6 +265,7 @@ module.exports = {
242
265
  buildClaudeCodeSettingsPatch,
243
266
  buildVsCodeAvailableModels,
244
267
  buildVsCodeClaudeEnv,
268
+ buildVsCodeExtensionEnvVars,
245
269
  buildVsCodeModelSelectionIds,
246
270
  buildVsCodeOpenCommand,
247
271
  checkClaudeCodeSettingsEnv,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yingclaw",
3
- "version": "2.5.25",
3
+ "version": "2.5.26",
4
4
  "description": "Claude Code × 国产大模型一键接入:DeepSeek、Kimi、火山方舟、Qwen、MiniMax、GLM、MiMo、自定义接口",
5
5
  "main": "index.js",
6
6
  "bin": {