yingclaw 2.5.25 → 2.5.27

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,12 +735,17 @@ program
613
735
  }
614
736
 
615
737
  console.log(chalk.dim(getStorageHint(file)));
616
- const displayedConfig = ensureGatewayForOpenAiConfig(config);
738
+ const displayedConfig = effectiveConfig;
739
+ const needsGatewayHint = process.platform !== 'darwin' && process.platform !== 'win32'
740
+ && isOpenAiCompatibleConfig(effectiveConfig);
617
741
  console.log(boxen(
618
742
  chalk.bold('Claude Code 终端已接入\n\n') +
619
743
  chalk.dim('Base URL ') + chalk.cyan(buildClaudeEnv(displayedConfig).ANTHROPIC_BASE_URL) + '\n' +
620
744
  chalk.dim('模型 ') + chalk.yellow(displayedConfig.model) + '\n\n' +
621
- chalk.white('需要启动时,在主菜单选择“启动 Claude Code”,或直接输入 ') + chalk.cyan.bold('claude') + '\n' +
745
+ (needsGatewayHint
746
+ ? chalk.yellow('注:OpenAI 兼容接口需要本机 Gateway,请在新终端运行 ') + chalk.cyan.bold('claw gateway') + chalk.yellow(' 并保持运行。\n\n')
747
+ : '') +
748
+ chalk.white('需要启动时,在主菜单选择”启动 Claude Code”,或直接输入 ') + chalk.cyan.bold('claude') + '\n' +
622
749
  chalk.dim(getActivationHint(file)),
623
750
  { padding: { top: 0, bottom: 0, left: 2, right: 2 }, borderStyle: 'round', borderColor: 'green', margin: { top: 1, bottom: 1 } }
624
751
  ));
@@ -692,10 +819,11 @@ program
692
819
  if (effectiveConfig !== config) saveConfig(effectiveConfig);
693
820
 
694
821
  const vscodeModelChoices = getChatModelChoices(buildVsCodeModelSelectionIds(effectiveConfig));
822
+ const vscodeModelCount = vscodeModelChoices.length;
695
823
  const vscodeModel = await promptModelFromChoices({
696
824
  chalk,
697
825
  choices: vscodeModelChoices,
698
- message: '选择 VS Code Claude Code 主模型',
826
+ message: `选择 VS Code Claude Code 主模型 共 ${vscodeModelCount} 个可用`,
699
827
  allowManual: true,
700
828
  });
701
829
  if (vscodeModel === '__BACK__') return;
@@ -703,7 +831,7 @@ program
703
831
  const vscodeFastModel = await promptModelFromChoices({
704
832
  chalk,
705
833
  choices: sortModelChoicesByRelatedFamily(vscodeModelChoices, vscodeModel),
706
- message: '选择 VS Code Claude Code 快速模型 / 备用模型',
834
+ message: `选择 VS Code Claude Code 快速模型 / 备用模型 共 ${vscodeModelCount} 个可用`,
707
835
  allowManual: true,
708
836
  });
709
837
  if (vscodeFastModel === '__BACK__') return;
@@ -725,12 +853,17 @@ program
725
853
  return;
726
854
  }
727
855
 
856
+ const vscodeNeedsGatewayHint = process.platform !== 'darwin' && process.platform !== 'win32'
857
+ && isOpenAiCompatibleConfig(effectiveConfig);
728
858
  console.log(boxen(
729
859
  chalk.bold('VS Code Claude Code 扩展已配置\n\n') +
730
860
  chalk.dim('共享设置 ') + chalk.cyan(result.claudeSettings.file) + '\n' +
731
861
  chalk.dim('VS Code ') + chalk.cyan(result.vscodeSettings.file) + '\n' +
732
862
  chalk.dim('主模型 ') + chalk.yellow(vscodeModel) + '\n' +
733
863
  chalk.dim('快速模型 ') + chalk.yellow(vscodeFastModel) + '\n\n' +
864
+ (vscodeNeedsGatewayHint
865
+ ? chalk.yellow('注:OpenAI 兼容接口需要本机 Gateway,请在新终端运行 ') + chalk.cyan.bold('claw gateway') + chalk.yellow(' 并保持运行。\n\n')
866
+ : '') +
734
867
  chalk.white('已写入 Claude Code settings.json 的 env,并关闭 VS Code 扩展登录提示。\n') +
735
868
  chalk.dim('如果 VS Code 已打开,请执行 Developer: Reload Window 或完全重启 VS Code。'),
736
869
  { padding: { top: 0, bottom: 0, left: 2, right: 2 }, borderStyle: 'round', borderColor: 'green', margin: { top: 1, bottom: 1 } }
@@ -799,14 +932,8 @@ program
799
932
  return;
800
933
  }
801
934
 
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;
935
+ const providerKey = await promptProviderKey(chalk);
936
+ if (!providerKey || providerKey === '__BACK__') return;
810
937
 
811
938
  const provider = PROVIDERS[providerKey];
812
939
  if (provider.custom) {
@@ -818,7 +945,6 @@ program
818
945
  saveConfig(savedConfig);
819
946
  spinner.succeed(chalk.green(`API 连接已切换至 ${savedConfig.providerName} · ${savedConfig.model}`));
820
947
  console.log(chalk.dim('如需让外部 claude 命令使用新模型,请运行 claw code。'));
821
- await offerDesktopSync(chalk, ora, savedConfig);
822
948
  return;
823
949
  }
824
950
 
@@ -859,12 +985,12 @@ program
859
985
  availableModels = provider.models.map(m => m.value);
860
986
  }
861
987
 
862
- const model = await select({ loop: false,
863
- message: chalk.cyan('选择模型'),
864
- choices: [
865
- ...modelChoices,
866
- { name: chalk.dim('↩ 返回主菜单'), value: '__BACK__' },
867
- ],
988
+ const model = await promptModelFromChoices({
989
+ chalk,
990
+ choices: modelChoices,
991
+ message: '选择模型',
992
+ backLabel: '↩ 返回主菜单',
993
+ allowManual: false,
868
994
  });
869
995
  if (model === '__BACK__') return;
870
996
 
@@ -874,7 +1000,6 @@ program
874
1000
  saveConfig(newConfig);
875
1001
  spinner.succeed(chalk.green(`API 连接已切换至 ${provider.name} · ${model}`));
876
1002
  console.log(chalk.dim('如需让外部 claude 命令使用新模型,请运行 claw code。'));
877
- await offerDesktopSync(chalk, ora, newConfig);
878
1003
  });
879
1004
 
880
1005
  program
@@ -963,6 +1088,16 @@ program
963
1088
  if (desktopConfig.desktopGatewayKey !== config.desktopGatewayKey || desktopConfig.desktopGatewayPort !== config.desktopGatewayPort) {
964
1089
  saveConfig(desktopConfig);
965
1090
  }
1091
+
1092
+ const chatModels = Array.isArray(desktopConfig.availableModels)
1093
+ ? desktopConfig.availableModels.filter(isDesktopChatModel)
1094
+ : [];
1095
+ if (chatModels.length > DESKTOP_MODEL_SELECT_THRESHOLD) {
1096
+ const selectedModels = await promptDesktopModels(chalk, desktopConfig, chatModels);
1097
+ if (!selectedModels) return;
1098
+ saveConfig({ ...desktopConfig, desktopModels: selectedModels });
1099
+ desktopConfig = { ...desktopConfig, availableModels: selectedModels };
1100
+ }
966
1101
  }
967
1102
 
968
1103
  const spinner = ora('写入 Claude 桌面应用配置...').start();
@@ -1273,14 +1408,16 @@ program
1273
1408
  ));
1274
1409
  process.exit(0);
1275
1410
  } else {
1276
- console.log(chalk.red('\n升级失败,请手动运行:'));
1277
- console.log(chalk.cyan('npm install -g yingclaw@latest'));
1411
+ const hints = getInstallFailureHints(result, chalk);
1412
+ console.log(chalk.red('\n升级失败'));
1413
+ if (hints.length > 0) console.log(...hints);
1414
+ console.log(chalk.dim('\n手动运行:') + chalk.cyan('npm install -g yingclaw@latest'));
1278
1415
  }
1279
1416
  });
1280
1417
 
1281
- async function renderStatusBar(apiStatus) {
1418
+ async function renderStatusBar(apiStatus, config) {
1282
1419
  const chalk = (await import('chalk')).default;
1283
- const config = loadConfig();
1420
+ if (config === undefined) config = loadConfig();
1284
1421
  const claudeInstalled = isClaudeInstalled();
1285
1422
 
1286
1423
  const claudeIcon = claudeInstalled ? chalk.green('●') : chalk.red('●');
@@ -1408,7 +1545,7 @@ async function runMenu() {
1408
1545
  console.log(chalk.red(` ● 配置无效:${configProblem}`));
1409
1546
  console.log(chalk.dim(' 请先选择"配置 API 连接"重新配置'));
1410
1547
  } else {
1411
- console.log(await renderStatusBar(apiStatus));
1548
+ console.log(await renderStatusBar(apiStatus, config));
1412
1549
  }
1413
1550
  console.log();
1414
1551
 
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/install.js CHANGED
@@ -1,3 +1,4 @@
1
+ const path = require('path');
1
2
  const { execSync } = require('child_process');
2
3
 
3
4
  function buildClaudeInstallCommand(network) {
@@ -20,9 +21,12 @@ function quoteWindowsBatchArg(value) {
20
21
  return `"${String(value).replace(/"/g, '""')}"`;
21
22
  }
22
23
 
23
- function buildWindowsYingclawUpgradeScript(network) {
24
- const { command, args } = buildYingclawUpgradeCommand(network);
25
- const commandLine = [command, ...args].map(quoteWindowsBatchArg).join(' ');
24
+ function buildWindowsYingclawUpgradeScript(network, options = {}) {
25
+ const nodePath = options.nodePath || process.execPath;
26
+ // 用绝对路径避免 PATH 里的残留 npm 被优先找到(System32 等位置)
27
+ const npmCmd = options.npmCmd || path.win32.join(path.win32.dirname(nodePath), 'npm.cmd');
28
+ const { args } = buildYingclawUpgradeCommand(network);
29
+ const commandLine = `call ${quoteWindowsBatchArg(npmCmd)} ${args.map(quoteWindowsBatchArg).join(' ')}`;
26
30
  return [
27
31
  '@echo off',
28
32
  'chcp 65001 >nul',
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.27",
4
4
  "description": "Claude Code × 国产大模型一键接入:DeepSeek、Kimi、火山方舟、Qwen、MiniMax、GLM、MiMo、自定义接口",
5
5
  "main": "index.js",
6
6
  "bin": {