yingclaw 2.5.20 → 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/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,
@@ -13,7 +13,9 @@ const {
13
13
  validateConfig,
14
14
  validateKey,
15
15
  normalizeAnthropicBaseUrl,
16
+ getProviderProtocol,
16
17
  resolveFastModel,
18
+ sortRelatedModelIds,
17
19
  buildClaudeEnv,
18
20
  PROVIDERS,
19
21
  CLAUDE_ENV_KEYS,
@@ -23,6 +25,12 @@ const { execSync, spawn, spawnSync } = require('child_process');
23
25
  const pkg = require('../package.json');
24
26
  const { getGatewayAutostartStatus, installGatewayAutostart, removeGatewayAutostart } = require('../lib/autostart');
25
27
  const { buildMenuStatusLines, buildStatusView } = require('../lib/panel');
28
+ const {
29
+ buildVsCodeOpenCommand,
30
+ buildVsCodeModelSelectionIds,
31
+ clearVsCodeIntegration,
32
+ writeVsCodeIntegration,
33
+ } = require('../lib/vscode');
26
34
  const {
27
35
  buildClaudeInstallCommand,
28
36
  buildYingclawUpgradeCommand,
@@ -42,19 +50,23 @@ const {
42
50
  isDesktopChatModel,
43
51
  } = require('../lib/gateway');
44
52
  const { runDoctorChecks, summarize, STATUS_OK, STATUS_FAIL, STATUS_WARN, STATUS_INFO } = require('../lib/doctor');
53
+ const { normalizeOpenAiBaseUrl } = require('../lib/openai');
45
54
 
46
55
  const program = new Command();
47
56
 
57
+ let _bannerCache;
48
58
  async function getBanner() {
59
+ if (_bannerCache) return _bannerCache;
49
60
  const chalk = (await import('chalk')).default;
50
61
  const figlet = require('figlet');
51
62
  const boxen = (await import('boxen')).default;
52
63
  const title = figlet.textSync('yingclaw', { font: 'Small', horizontalLayout: 'fitted' });
53
64
  const subtitle = chalk.dim('Claude Code × 国产大模型 一键接入') + ' ' + chalk.cyan(`v${pkg.version}`);
54
- return boxen(
65
+ _bannerCache = boxen(
55
66
  chalk.cyan.bold(title) + '\n' + subtitle,
56
67
  { padding: { top: 0, bottom: 0, left: 2, right: 2 }, borderStyle: 'round', borderColor: 'cyan', margin: { top: 1, bottom: 0 } }
57
68
  );
69
+ return _bannerCache;
58
70
  }
59
71
 
60
72
  function getConfigValidationMessage(config) {
@@ -155,15 +167,36 @@ async function offerDesktopSync(chalk, ora, config) {
155
167
  }
156
168
  }
157
169
 
170
+ const SEARCH_THRESHOLD = 15;
171
+
158
172
  async function promptModelFromChoices({ chalk, choices, message, backLabel = '↩ 返回上一步', allowManual = true }) {
159
- const selected = await select({ loop: false,
160
- message: chalk.cyan(message),
161
- choices: [
162
- ...choices,
163
- ...(allowManual ? [{ name: '手动输入模型名', value: '__MANUAL__' }] : []),
164
- { name: chalk.dim(backLabel), value: '__BACK__' },
165
- ],
166
- });
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
+
167
200
  if (selected === '__BACK__') return '__BACK__';
168
201
  if (selected !== '__MANUAL__') return selected;
169
202
 
@@ -173,10 +206,121 @@ async function promptModelFromChoices({ chalk, choices, message, backLabel = '
173
206
  }).then(v => v.trim());
174
207
  }
175
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
+
176
274
  function getChatModelChoices(models) {
177
275
  return models.filter(isDesktopChatModel).map(id => ({ name: id, value: id }));
178
276
  }
179
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
+
318
+ function sortModelChoicesByRelatedFamily(choices, mainModel) {
319
+ const order = sortRelatedModelIds(choices.map((choice) => choice.value), mainModel);
320
+ const rank = new Map(order.map((value, index) => [value, index]));
321
+ return [...choices].sort((a, b) => (rank.get(a.value) ?? 9999) - (rank.get(b.value) ?? 9999));
322
+ }
323
+
180
324
  function formatFetchedModelCount(chalk, total, chatCount) {
181
325
  if (total === chatCount) return chalk.green(`已获取 ${chatCount} 个可用聊天模型`);
182
326
  return chalk.green(`已获取 ${total} 个模型,其中 ${chatCount} 个可用于 Claude/桌面聊天`);
@@ -190,14 +334,35 @@ async function promptManualModel(chalk, message, defaultValue) {
190
334
  }).then(v => v.trim());
191
335
  }
192
336
 
193
- async function configureCustomProvider({ chalk, ora, existingConfig }) {
337
+ function isOpenAiCompatibleConfig(config) {
338
+ return getProviderProtocol(config) === 'openai';
339
+ }
340
+
341
+ function ensureGatewayForOpenAiConfig(config) {
342
+ return isOpenAiCompatibleConfig(config) ? ensureDesktopGatewayConfig(config) : config;
343
+ }
344
+
345
+ function writeClaudeCodeEnv(config) {
346
+ const effectiveConfig = buildClaudeEnv(config);
347
+ return writeEnvToZshrc(
348
+ effectiveConfig.ANTHROPIC_BASE_URL,
349
+ effectiveConfig.ANTHROPIC_AUTH_TOKEN,
350
+ effectiveConfig.ANTHROPIC_MODEL,
351
+ effectiveConfig.CLAUDE_CODE_SUBAGENT_MODEL,
352
+ );
353
+ }
354
+
355
+ async function configureCustomProvider({ chalk, ora, existingConfig, providerKey = 'custom' }) {
356
+ const provider = PROVIDERS[providerKey];
357
+ const protocol = provider.protocol || 'anthropic';
358
+ const sameProvider = existingConfig?.provider === providerKey;
194
359
  const baseUrl = await input({
195
- message: chalk.cyan('Anthropic Base URL'),
196
- default: existingConfig?.provider === 'custom' ? existingConfig.baseUrl : undefined,
360
+ message: chalk.cyan(protocol === 'openai' ? 'OpenAI Base URL' : 'Anthropic Base URL'),
361
+ default: sameProvider ? existingConfig.baseUrl : undefined,
197
362
  validate: (v) => v.trim().length > 0 && isValidUrl(v.trim()) ? true : '请输入有效 URL',
198
- }).then(v => normalizeAnthropicBaseUrl(v.trim()));
363
+ }).then(v => protocol === 'openai' ? normalizeOpenAiBaseUrl(v.trim()) : normalizeAnthropicBaseUrl(v.trim()));
199
364
 
200
- let apiKey = existingConfig?.provider === 'custom' ? existingConfig.apiKey : '';
365
+ let apiKey = sameProvider ? existingConfig.apiKey : '';
201
366
  if (apiKey) {
202
367
  const keepKey = await confirm({ message: '沿用当前 API Key?', default: true });
203
368
  if (!keepKey) apiKey = '';
@@ -213,7 +378,7 @@ async function configureCustomProvider({ chalk, ora, existingConfig }) {
213
378
  let modelChoices = [];
214
379
  let modelsUrl;
215
380
  const fetchSpinner = ora('正在自动获取可用模型...').start();
216
- const onlineResult = await fetchModelsFromBaseUrl('custom', apiKey, baseUrl);
381
+ const onlineResult = await fetchModelsFromBaseUrl(providerKey, apiKey, baseUrl);
217
382
  if (onlineResult) {
218
383
  modelsUrl = onlineResult.modelsUrl;
219
384
  modelChoices = getChatModelChoices(onlineResult.models);
@@ -231,7 +396,11 @@ async function configureCustomProvider({ chalk, ora, existingConfig }) {
231
396
  if (modelChoices.length > 0) {
232
397
  model = await promptModelFromChoices({ chalk, choices: modelChoices, message: '选择主模型' });
233
398
  if (model === '__BACK__') return null;
234
- fastModel = await promptModelFromChoices({ chalk, choices: modelChoices, message: '选择快速模型 / Subagent 模型' });
399
+ fastModel = await promptModelFromChoices({
400
+ chalk,
401
+ choices: sortModelChoicesByRelatedFamily(modelChoices, model),
402
+ message: '选择快速模型 / Subagent 模型',
403
+ });
235
404
  if (fastModel === '__BACK__') return null;
236
405
  } else {
237
406
  model = await promptManualModel(chalk, '输入主模型名');
@@ -239,8 +408,9 @@ async function configureCustomProvider({ chalk, ora, existingConfig }) {
239
408
  }
240
409
 
241
410
  return {
242
- provider: 'custom',
243
- providerName: '自定义接口',
411
+ provider: providerKey,
412
+ providerName: provider.name,
413
+ protocol,
244
414
  baseUrl,
245
415
  modelsUrl: modelsUrl || undefined,
246
416
  apiKey,
@@ -343,17 +513,11 @@ async function runConfigFlow({ writeCodeEnv = false } = {}) {
343
513
 
344
514
  while (true) {
345
515
  if (step === 'provider') {
346
- providerKey = await select({ loop: false,
347
- message: chalk.cyan('选择 AI 厂商'),
348
- choices: [
349
- ...Object.entries(PROVIDERS).map(([value, p]) => ({ name: p.name, value })),
350
- { name: chalk.dim('↩ 返回主菜单'), value: '__BACK__' },
351
- ],
352
- });
353
- if (providerKey === '__BACK__') return;
516
+ providerKey = await promptProviderKey(chalk);
517
+ if (!providerKey || providerKey === '__BACK__') return;
354
518
  provider = PROVIDERS[providerKey];
355
519
  if (provider.custom) {
356
- customConfig = await configureCustomProvider({ chalk, ora });
520
+ customConfig = await configureCustomProvider({ chalk, ora, providerKey });
357
521
  if (!customConfig) { step = 'provider'; continue; }
358
522
  break;
359
523
  }
@@ -388,12 +552,12 @@ async function runConfigFlow({ writeCodeEnv = false } = {}) {
388
552
  availableModels = provider.models.map(m => m.value);
389
553
  }
390
554
 
391
- const m = await select({ loop: false,
392
- message: chalk.cyan('选择模型'),
393
- choices: [
394
- ...modelChoices,
395
- { name: chalk.dim('↩ 返回上一步(重新输入 Key)'), value: '__BACK__' },
396
- ],
555
+ const m = await promptModelFromChoices({
556
+ chalk,
557
+ choices: modelChoices,
558
+ message: '选择模型',
559
+ backLabel: '↩ 返回上一步(重新输入 Key)',
560
+ allowManual: false,
397
561
  });
398
562
  if (m === '__BACK__') { step = 'apikey'; continue; }
399
563
  model = m;
@@ -407,9 +571,10 @@ async function runConfigFlow({ writeCodeEnv = false } = {}) {
407
571
  try {
408
572
  const fastModel = customConfig?.fastModel || resolveFastModel(provider, model);
409
573
  cfg = customConfig || { provider: providerKey, model, fastModel, apiKey, baseUrl: provider.baseUrl, availableModels };
574
+ cfg = ensureGatewayForOpenAiConfig(cfg);
410
575
  saveConfig(cfg);
411
576
  if (writeCodeEnv) {
412
- ({ file } = writeEnvToZshrc(cfg.baseUrl, cfg.apiKey, cfg.model, cfg.fastModel));
577
+ ({ file } = writeClaudeCodeEnv(cfg));
413
578
  }
414
579
  spinner.succeed(chalk.green(writeCodeEnv ? 'API 连接已保存,Claude Code 终端已接入' : 'API 连接已保存'));
415
580
  } catch (e) {
@@ -432,7 +597,6 @@ async function runConfigFlow({ writeCodeEnv = false } = {}) {
432
597
  { padding: { top: 0, bottom: 0, left: 2, right: 2 }, borderStyle: 'round', borderColor: 'green', margin: { top: 1, bottom: 1 } }
433
598
  ));
434
599
 
435
- await offerDesktopSync(chalk, ora, cfg);
436
600
  }
437
601
 
438
602
  program
@@ -546,7 +710,7 @@ program
546
710
 
547
711
  console.log(await getBanner());
548
712
 
549
- const config = loadConfig();
713
+ let config = loadConfig();
550
714
  if (!config) {
551
715
  console.log(chalk.red('\n未配置 API 连接,请先运行: claw config\n'));
552
716
  return;
@@ -561,7 +725,9 @@ program
561
725
  const spinner = ora('写入 Claude Code 终端环境变量...').start();
562
726
  let file;
563
727
  try {
564
- ({ file } = writeEnvToZshrc(config.baseUrl, config.apiKey, config.model, config.fastModel));
728
+ const effectiveConfig = ensureGatewayForOpenAiConfig(config);
729
+ if (effectiveConfig !== config) saveConfig(effectiveConfig);
730
+ ({ file } = writeClaudeCodeEnv(effectiveConfig));
565
731
  spinner.succeed(chalk.green(`Claude Code 终端已接入 → ${file}`));
566
732
  } catch (e) {
567
733
  spinner.fail(chalk.red(`写入失败: ${e.message}`));
@@ -569,10 +735,11 @@ program
569
735
  }
570
736
 
571
737
  console.log(chalk.dim(getStorageHint(file)));
738
+ const displayedConfig = effectiveConfig;
572
739
  console.log(boxen(
573
740
  chalk.bold('Claude Code 终端已接入\n\n') +
574
- chalk.dim('Base URL ') + chalk.cyan(config.baseUrl) + '\n' +
575
- chalk.dim('模型 ') + chalk.yellow(config.model) + '\n\n' +
741
+ chalk.dim('Base URL ') + chalk.cyan(buildClaudeEnv(displayedConfig).ANTHROPIC_BASE_URL) + '\n' +
742
+ chalk.dim('模型 ') + chalk.yellow(displayedConfig.model) + '\n\n' +
576
743
  chalk.white('需要启动时,在主菜单选择“启动 Claude Code”,或直接输入 ') + chalk.cyan.bold('claude') + '\n' +
577
744
  chalk.dim(getActivationHint(file)),
578
745
  { padding: { top: 0, bottom: 0, left: 2, right: 2 }, borderStyle: 'round', borderColor: 'green', margin: { top: 1, bottom: 1 } }
@@ -620,6 +787,120 @@ program
620
787
  }
621
788
  });
622
789
 
790
+ program
791
+ .command('vscode')
792
+ .description('接入 VS Code Claude Code 扩展')
793
+ .option('--terminal', '让 Claude Code 扩展默认使用终端模式')
794
+ .action(async (options) => {
795
+ const chalk = (await import('chalk')).default;
796
+ const ora = (await import('ora')).default;
797
+ const boxen = (await import('boxen')).default;
798
+
799
+ console.log(await getBanner());
800
+
801
+ const config = loadConfig();
802
+ if (!config) {
803
+ console.log(chalk.red('\n未配置 API 连接,请先运行: claw config\n'));
804
+ return;
805
+ }
806
+ const configProblem = getConfigValidationMessage(config);
807
+ if (configProblem) {
808
+ console.log(chalk.red(`\n配置无效:${configProblem}`));
809
+ console.log(chalk.dim('请运行 claw config 重新配置。\n'));
810
+ return;
811
+ }
812
+
813
+ let effectiveConfig = ensureGatewayForOpenAiConfig(config);
814
+ if (effectiveConfig !== config) saveConfig(effectiveConfig);
815
+
816
+ const vscodeModelChoices = getChatModelChoices(buildVsCodeModelSelectionIds(effectiveConfig));
817
+ const vscodeModelCount = vscodeModelChoices.length;
818
+ const vscodeModel = await promptModelFromChoices({
819
+ chalk,
820
+ choices: vscodeModelChoices,
821
+ message: `选择 VS Code Claude Code 主模型 共 ${vscodeModelCount} 个可用`,
822
+ allowManual: true,
823
+ });
824
+ if (vscodeModel === '__BACK__') return;
825
+
826
+ const vscodeFastModel = await promptModelFromChoices({
827
+ chalk,
828
+ choices: sortModelChoicesByRelatedFamily(vscodeModelChoices, vscodeModel),
829
+ message: `选择 VS Code Claude Code 快速模型 / 备用模型 共 ${vscodeModelCount} 个可用`,
830
+ allowManual: true,
831
+ });
832
+ if (vscodeFastModel === '__BACK__') return;
833
+
834
+ const vscodeConfig = {
835
+ ...config,
836
+ ...effectiveConfig,
837
+ vscodeModel,
838
+ vscodeFastModel,
839
+ };
840
+
841
+ const spinner = ora('写入 VS Code Claude Code 配置...').start();
842
+ let result;
843
+ try {
844
+ result = writeVsCodeIntegration(vscodeConfig, options.terminal ? { useTerminal: true } : {});
845
+ spinner.succeed(chalk.green('VS Code Claude Code 已接入'));
846
+ } catch (e) {
847
+ spinner.fail(chalk.red(`写入失败: ${e.message}`));
848
+ return;
849
+ }
850
+
851
+ console.log(boxen(
852
+ chalk.bold('VS Code Claude Code 扩展已配置\n\n') +
853
+ chalk.dim('共享设置 ') + chalk.cyan(result.claudeSettings.file) + '\n' +
854
+ chalk.dim('VS Code ') + chalk.cyan(result.vscodeSettings.file) + '\n' +
855
+ chalk.dim('主模型 ') + chalk.yellow(vscodeModel) + '\n' +
856
+ chalk.dim('快速模型 ') + chalk.yellow(vscodeFastModel) + '\n\n' +
857
+ chalk.white('已写入 Claude Code settings.json 的 env,并关闭 VS Code 扩展登录提示。\n') +
858
+ chalk.dim('如果 VS Code 已打开,请执行 Developer: Reload Window 或完全重启 VS Code。'),
859
+ { padding: { top: 0, bottom: 0, left: 2, right: 2 }, borderStyle: 'round', borderColor: 'green', margin: { top: 1, bottom: 1 } }
860
+ ));
861
+
862
+ const shouldOpen = await confirm({ message: '是否现在打开 Claude Code 的 VS Code 面板?', default: false });
863
+ if (shouldOpen) {
864
+ const openCommand = buildVsCodeOpenCommand(process.platform);
865
+ spawnSync(openCommand.command, openCommand.args, { stdio: 'ignore', windowsHide: true });
866
+ }
867
+ });
868
+
869
+ program
870
+ .command('vscode-reset')
871
+ .description('恢复 VS Code Claude Code 扩展默认配置')
872
+ .action(async () => {
873
+ const chalk = (await import('chalk')).default;
874
+ const ora = (await import('ora')).default;
875
+ const boxen = (await import('boxen')).default;
876
+
877
+ console.log(await getBanner());
878
+
879
+ const yes = await confirm({
880
+ message: chalk.red('确定要恢复 VS Code Claude Code 扩展默认配置吗?API 连接、终端和桌面配置不会被清除'),
881
+ default: false,
882
+ });
883
+ if (!yes) {
884
+ console.log(chalk.dim('已取消'));
885
+ return;
886
+ }
887
+
888
+ const spinner = ora('正在恢复 VS Code Claude Code 默认配置...').start();
889
+ const result = clearVsCodeIntegration();
890
+
891
+ if (result.result === 'updated') {
892
+ spinner.succeed(chalk.green('VS Code Claude Code 已恢复默认'));
893
+ console.log(boxen(
894
+ chalk.bold('已清除以下 VS Code / Claude Code 扩展配置:\n\n') +
895
+ result.files.map(f => chalk.cyan(' • ' + f)).join('\n') +
896
+ '\n\n' + chalk.dim('如果 VS Code 已打开,请执行 Developer: Reload Window 或完全重启 VS Code。'),
897
+ { padding: { top: 0, bottom: 0, left: 2, right: 2 }, borderStyle: 'round', borderColor: 'green', margin: { top: 1, bottom: 1 } }
898
+ ));
899
+ } else {
900
+ spinner.warn(chalk.yellow('没有找到 VS Code Claude Code 的 yingclaw 配置,无需恢复'));
901
+ }
902
+ });
903
+
623
904
  program
624
905
  .command('switch')
625
906
  .description('快速切换模型(只更新 API 连接)')
@@ -641,25 +922,19 @@ program
641
922
  return;
642
923
  }
643
924
 
644
- const providerKey = await select({ loop: false,
645
- message: chalk.cyan('选择 AI 厂商'),
646
- choices: [
647
- ...Object.entries(PROVIDERS).map(([value, p]) => ({ name: p.name, value })),
648
- { name: chalk.dim('↩ 返回主菜单'), value: '__BACK__' },
649
- ],
650
- });
651
- if (providerKey === '__BACK__') return;
925
+ const providerKey = await promptProviderKey(chalk);
926
+ if (!providerKey || providerKey === '__BACK__') return;
652
927
 
653
928
  const provider = PROVIDERS[providerKey];
654
929
  if (provider.custom) {
655
- const customConfig = await configureCustomProvider({ chalk, ora, existingConfig: config });
930
+ const customConfig = await configureCustomProvider({ chalk, ora, existingConfig: config, providerKey });
656
931
  if (!customConfig) return;
657
932
 
658
933
  const spinner = ora('切换中...').start();
659
- saveConfig(customConfig);
660
- spinner.succeed(chalk.green(`API 连接已切换至 ${customConfig.providerName} · ${customConfig.model}`));
934
+ const savedConfig = ensureGatewayForOpenAiConfig(customConfig);
935
+ saveConfig(savedConfig);
936
+ spinner.succeed(chalk.green(`API 连接已切换至 ${savedConfig.providerName} · ${savedConfig.model}`));
661
937
  console.log(chalk.dim('如需让外部 claude 命令使用新模型,请运行 claw code。'));
662
- await offerDesktopSync(chalk, ora, customConfig);
663
938
  return;
664
939
  }
665
940
 
@@ -700,12 +975,12 @@ program
700
975
  availableModels = provider.models.map(m => m.value);
701
976
  }
702
977
 
703
- const model = await select({ loop: false,
704
- message: chalk.cyan('选择模型'),
705
- choices: [
706
- ...modelChoices,
707
- { name: chalk.dim('↩ 返回主菜单'), value: '__BACK__' },
708
- ],
978
+ const model = await promptModelFromChoices({
979
+ chalk,
980
+ choices: modelChoices,
981
+ message: '选择模型',
982
+ backLabel: '↩ 返回主菜单',
983
+ allowManual: false,
709
984
  });
710
985
  if (model === '__BACK__') return;
711
986
 
@@ -715,7 +990,6 @@ program
715
990
  saveConfig(newConfig);
716
991
  spinner.succeed(chalk.green(`API 连接已切换至 ${provider.name} · ${model}`));
717
992
  console.log(chalk.dim('如需让外部 claude 命令使用新模型,请运行 claw code。'));
718
- await offerDesktopSync(chalk, ora, newConfig);
719
993
  });
720
994
 
721
995
  program
@@ -793,6 +1067,10 @@ program
793
1067
  console.log(chalk.yellow('\nClaude 桌面应用 3P 配置目前仅支持 macOS / Windows。\n'));
794
1068
  return;
795
1069
  }
1070
+ if (options.direct && isOpenAiCompatibleConfig(config)) {
1071
+ console.log(chalk.yellow('\nOpenAI 兼容接口必须通过本机 Gateway 转换协议,不能使用直连模式。\n'));
1072
+ return;
1073
+ }
796
1074
 
797
1075
  let desktopConfig = config;
798
1076
  if (!options.direct) {
@@ -800,6 +1078,16 @@ program
800
1078
  if (desktopConfig.desktopGatewayKey !== config.desktopGatewayKey || desktopConfig.desktopGatewayPort !== config.desktopGatewayPort) {
801
1079
  saveConfig(desktopConfig);
802
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
+ }
803
1091
  }
804
1092
 
805
1093
  const spinner = ora('写入 Claude 桌面应用配置...').start();
@@ -942,7 +1230,7 @@ program
942
1230
 
943
1231
  program
944
1232
  .command('reset')
945
- .description('清除 API 连接、终端环境变量和桌面配置')
1233
+ .description('清除 API 连接、终端环境变量、VS Code 和桌面配置')
946
1234
  .action(async () => {
947
1235
  const chalk = (await import('chalk')).default;
948
1236
  const ora = (await import('ora')).default;
@@ -965,6 +1253,10 @@ program
965
1253
  if (desktopCleared.result === 'updated') {
966
1254
  cleared.push(...desktopConfigClearedPaths(desktopCleared));
967
1255
  }
1256
+ const vscodeCleared = clearVsCodeIntegration();
1257
+ if (vscodeCleared.result === 'updated') {
1258
+ cleared.push(...vscodeCleared.files);
1259
+ }
968
1260
  const autostartCleared = removeGatewayAutostart();
969
1261
  if (autostartCleared.result === 'removed' && autostartCleared.file) {
970
1262
  cleared.push(autostartCleared.file);
@@ -1105,15 +1397,18 @@ program
1105
1397
  { padding: { top: 0, bottom: 0, left: 2, right: 2 }, borderStyle: 'round', borderColor: 'green', margin: { top: 1, bottom: 1 } }
1106
1398
  ));
1107
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'));
1108
1403
  } else {
1109
1404
  console.log(chalk.red('\n升级失败,请手动运行:'));
1110
1405
  console.log(chalk.cyan('npm install -g yingclaw@latest'));
1111
1406
  }
1112
1407
  });
1113
1408
 
1114
- async function renderStatusBar(apiStatus) {
1409
+ async function renderStatusBar(apiStatus, config) {
1115
1410
  const chalk = (await import('chalk')).default;
1116
- const config = loadConfig();
1411
+ if (config === undefined) config = loadConfig();
1117
1412
  const claudeInstalled = isClaudeInstalled();
1118
1413
 
1119
1414
  const claudeIcon = claudeInstalled ? chalk.green('●') : chalk.red('●');
@@ -1131,11 +1426,12 @@ async function renderStatusBar(apiStatus) {
1131
1426
  const statusLines = buildMenuStatusLines(view, { apiStatus, claudeInstalled, platform: process.platform });
1132
1427
  cfgPart = statusLines.map((line, index) => {
1133
1428
  if (index === 0) return line.replace('API 正常', chalk.green('API 正常')).replace('API Key 无效', chalk.red('API Key 无效')).replace('网络/服务异常', chalk.yellow('网络/服务异常'));
1134
- if (line.startsWith('环境变量未生效')) return chalk.yellow(line);
1429
+ if (line.startsWith('终端未生效')) return chalk.yellow(line);
1135
1430
  if (line.startsWith('旧模型名')) return chalk.yellow(line);
1431
+ if (line.startsWith('快速模型跨系列')) return chalk.yellow(line);
1136
1432
  if (line.startsWith('Desktop Gateway 未运行')) return chalk.yellow(line);
1137
1433
  if (line.startsWith('Desktop Gateway 已运行')) return chalk.green(line);
1138
- if (line.startsWith('主模型')) return line.replace(view.mainModel, chalk.yellow(view.mainModel)).replace(view.fastModel, chalk.yellow(view.fastModel));
1434
+ if (line.startsWith('模型')) return line.replace(view.mainModel, chalk.yellow(view.mainModel)).replace(view.fastModel, chalk.yellow(view.fastModel));
1139
1435
  return line;
1140
1436
  }).join('\n ');
1141
1437
  } else {
@@ -1198,6 +1494,7 @@ async function runAdvancedMenu(chalk, hasConfig) {
1198
1494
  { name: '🩺 诊断(一键自检并给出修复建议)', value: 'doctor' },
1199
1495
  { name: '🔁 重新检测 API', value: 'recheck', disabled: !hasConfig && ADVANCED_DISABLED_HINT },
1200
1496
  { name: '↩️ 恢复 Claude Code 终端默认', value: 'code-reset' },
1497
+ { name: '↩️ 恢复 VS Code Claude Code 默认', value: 'vscode-reset' },
1201
1498
  { name: '↩️ 恢复 Claude 桌面默认', value: 'desktop-reset' },
1202
1499
  { name: '🗑 清除所有 yingclaw 配置', value: 'reset' },
1203
1500
  { name: '⬆️ 检查更新', value: 'update' },
@@ -1239,7 +1536,7 @@ async function runMenu() {
1239
1536
  console.log(chalk.red(` ● 配置无效:${configProblem}`));
1240
1537
  console.log(chalk.dim(' 请先选择"配置 API 连接"重新配置'));
1241
1538
  } else {
1242
- console.log(await renderStatusBar(apiStatus));
1539
+ console.log(await renderStatusBar(apiStatus, config));
1243
1540
  }
1244
1541
  console.log();
1245
1542
 
@@ -1253,6 +1550,7 @@ async function runMenu() {
1253
1550
  { name: config ? '🔑 重新配置 API 连接' : '🔑 配置 API 连接', value: 'config' },
1254
1551
  { name: '🔄 切换厂商或模型', value: 'switch', disabled: disabledHint },
1255
1552
  { name: '💻 接入 Claude Code 终端', value: 'code', disabled: disabledHint },
1553
+ { name: '🧩 接入 VS Code Claude Code 扩展', value: 'vscode', disabled: disabledHint },
1256
1554
  { name: '🖥 接入 Claude 桌面应用', value: 'desktop', disabled: disabledHint },
1257
1555
  { name: '📊 查看当前配置', value: 'status', disabled: !config && '需先配置 API 连接' },
1258
1556
  { name: '🛠 高级 ›', value: 'advanced' },
@@ -1276,8 +1574,13 @@ async function runMenu() {
1276
1574
  }
1277
1575
 
1278
1576
  if (resolvedAction === 'launch') {
1279
- const cfg = loadConfig();
1577
+ let cfg = loadConfig();
1280
1578
  if (!cfg || getConfigValidationMessage(cfg)) continue;
1579
+ const effectiveConfig = ensureGatewayForOpenAiConfig(cfg);
1580
+ if (effectiveConfig !== cfg) {
1581
+ saveConfig(effectiveConfig);
1582
+ cfg = effectiveConfig;
1583
+ }
1281
1584
  await new Promise((resolve) => {
1282
1585
  const child = spawn('claude', [], {
1283
1586
  stdio: 'inherit',
@@ -1297,7 +1600,9 @@ async function runMenu() {
1297
1600
  install: 'install-claude',
1298
1601
  config: 'config',
1299
1602
  code: 'code',
1603
+ vscode: 'vscode',
1300
1604
  'code-reset': 'code-reset',
1605
+ 'vscode-reset': 'vscode-reset',
1301
1606
  switch: 'switch',
1302
1607
  desktop: 'desktop',
1303
1608
  'desktop-reset': 'desktop-reset',
@@ -1315,7 +1620,7 @@ async function runMenu() {
1315
1620
  });
1316
1621
 
1317
1622
  // 改 config 的命令需要刷新缓存
1318
- if (['config', 'switch', 'reset', 'code-reset', 'desktop-reset'].includes(resolvedAction)) {
1623
+ if (['config', 'switch', 'reset', 'code-reset', 'vscode-reset', 'desktop-reset', 'vscode'].includes(resolvedAction)) {
1319
1624
  lastCheckResult = undefined;
1320
1625
  lastCheckedHash = null;
1321
1626
  }