yingclaw 2.5.27 → 2.5.38

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、B.AI、OpenCode Zen/Go,也支持自定义 Anthropic 兼容接口和自定义 OpenAI 兼容接口,无需梯子即可使用 Claude Code。
5
+ 支持 DeepSeek、Kimi、火山方舟 Coding Plan、阿里云百炼(Qwen)、MiniMax、智谱 GLM、小米 MiMo、B.AI、OpenCode Zen/Go,也支持自定义 AnthropicOpenAI、Google/Gemini 兼容接口,无需梯子即可使用 Claude Code。
6
6
 
7
7
  ![yingclaw 交互菜单](https://raw.githubusercontent.com/DengShiyingA/yingclaw/main/screenshot.png)
8
8
 
@@ -56,8 +56,34 @@ claw desktop
56
56
  ```
57
57
  `claw desktop` 会把 Claude Desktop 指向 yingclaw 本机 Gateway,并只暴露 `claude-sonnet-4-6`、`claude-opus-4-7`、`claude-haiku-4-5` 这类 Claude Desktop 可接受的模型名。真实第三方模型仍按 `claw config` 保存的主模型和快速模型转发。
58
58
 
59
+ `claw desktop` 默认只接入 Claude 桌面应用的 Cowork / Gateway,不写入 Claude Code 共享设置,因此不会自动接入 Claude Code 终端。交互面板会询问是否同时接入 Claude 桌面 Code 模式,默认选 No;选择 Yes 时才会写入 Claude Code 共享设置。
60
+
61
+ 如果你需要同时接入 Claude Desktop 的 Code 模式,再显式运行:
62
+
63
+ ```bash
64
+ claw desktop --with-code
65
+ ```
66
+
67
+ `--with-code` 会写入 `~/.claude/settings.json`,这个文件也会被 Claude Code CLI / VS Code Claude Code 读取。只想用桌面 Cowork 时不要加这个参数。
68
+
59
69
  `claw desktop` 会设置 Gateway 登录自动运行;如果自动启动失败,可以手动运行 `claw gateway`。
60
70
 
71
+ 出现问题时先运行:
72
+
73
+ ```bash
74
+ claw doctor
75
+ claw support -o yingclaw-support.json
76
+ ```
77
+
78
+ Windows 桌面应用 / Gateway 问题建议运行:
79
+
80
+ ```bash
81
+ claw doctor --windows
82
+ claw support --windows -o yingclaw-support.json
83
+ ```
84
+
85
+ `support` 生成的是脱敏排障包,不包含 API Key / Gateway Key 明文。
86
+
61
87
  ## 支持的厂商
62
88
 
63
89
  | 厂商 | 主模型 | 快速模型 |
@@ -75,6 +101,7 @@ claw desktop
75
101
  | OpenRouter | deepseek/deepseek-r1-0528 | deepseek/deepseek-chat-v3-0324 |
76
102
  | 自定义 Anthropic 兼容接口 | 自动获取或手动输入 | 手动选择 |
77
103
  | 自定义 OpenAI 兼容接口 | 自动获取或手动输入 | 手动选择 |
104
+ | 自定义 Google/Gemini 兼容接口 | 自动获取或手动输入 | 手动选择 |
78
105
 
79
106
  DeepSeek 的 `[1m]` 后缀是真实 API 模型 ID,表示 100 万 token 上下文窗口([官方说明](https://api-docs.deepseek.com/zh-cn/quick_start/agent_integrations/claude_code))。
80
107
 
@@ -89,10 +116,15 @@ claw code # 接入 Claude Code 终端
89
116
  claw vscode # 接入 VS Code Claude Code 扩展
90
117
  claw vscode-reset # 恢复 VS Code Claude Code 扩展默认配置
91
118
  claw desktop # 接入 Claude 桌面应用
119
+ claw desktop --with-code # 同时接入 Claude Desktop Code 模式,会写 Claude Code 共享设置
92
120
  claw gateway # 启动 Claude 桌面应用本机 Gateway
93
121
  claw desktop --direct # 高级:直连写入厂商 Gateway URL
94
122
  claw switch # 快速切换厂商或模型
95
123
  claw status # 查看当前配置,验证 Key 是否有效
124
+ claw doctor # 自检终端、VS Code、桌面、Gateway 和 API
125
+ claw doctor --windows # 追加 Windows 桌面/Gateway 专项检查
126
+ claw support # 生成脱敏排障包
127
+ claw support --windows -o yingclaw-support.json
96
128
  claw update # 检查并升级到最新版本
97
129
 
98
130
  claw code-reset # 恢复 Claude Code 终端默认配置
@@ -132,9 +164,11 @@ CLAUDE_CODE_EFFORT_LEVEL
132
164
  - macOS / Windows:写入 `Claude-3p/configLibrary/` 中的 yingclaw entry
133
165
  - Claude Desktop 访问 `http://127.0.0.1:18080/yingclaw`
134
166
  - Gateway 再转发到当前保存的 Anthropic 兼容接口;如当前为 Kimi 或 OpenAI 兼容接口,则自动转换为 `/v1/chat/completions`
167
+ - Google/Gemini 兼容接口会通过 Gateway 转换为 Gemini `generateContent` / `streamGenerateContent`
135
168
  - macOS 使用 LaunchAgent 登录启动 Gateway,Windows 使用 Startup 目录脚本登录启动 Gateway
136
169
  - Windows 会同时写入 Roaming / Local 两个 Claude-3p 位置,以兼容不同 Claude Desktop 安装方式
137
- - Anthropic 兼容接口的终端接入仍直接使用 `ANTHROPIC_*` 环境变量;Kimi OpenAI 兼容接口的终端/VS Code/桌面会通过本机 Gateway 转换协议
170
+ - Anthropic 兼容接口的终端接入仍直接使用 `ANTHROPIC_*` 环境变量;Kimi、OpenAI 兼容接口和 Google/Gemini 兼容接口会通过本机 Gateway 转换协议
171
+ - 默认不会写入 `~/.claude/settings.json`,避免影响 Claude Code 终端或 VS Code 扩展;面板中选择同时接入 Code,或使用 `claw desktop --with-code` 时才写入
138
172
 
139
173
  使用 `inferenceProvider=gateway`、`inferenceGatewayAuthScheme=bearer`,将 Gateway Base URL 指向 yingclaw 本机 Gateway。高级用户可用 `claw desktop --direct` 保留旧式直连写入,但新版 Claude Desktop 可能拒绝非 Claude 模型名。
140
174
 
@@ -149,6 +183,66 @@ CLAUDE_CODE_EFFORT_LEVEL
149
183
  - 扩展与 CLI 共享 Claude Code 设置;如果 VS Code 已打开,需重载窗口或重启后生效
150
184
  - 如需恢复默认,运行 `claw vscode-reset`,只移除 yingclaw 写入的 Claude Code env 和 VS Code Claude Code 扩展项,保留其它用户设置
151
185
 
186
+ ## 排障
187
+
188
+ ### Gateway 未运行或端口被占用
189
+
190
+ Claude Desktop 报 `Can't reach 127.0.0.1:18080` 时,先确认 Gateway:
191
+
192
+ ```bash
193
+ claw gateway
194
+ ```
195
+
196
+ 如果端口被占用,`claw gateway` 会提示占用进程和 PID。可以关闭旧进程,或换端口后重新接入:
197
+
198
+ ```bash
199
+ claw gateway --port 18081
200
+ claw desktop
201
+ ```
202
+
203
+ ### Windows 桌面应用不生效
204
+
205
+ Windows 只关闭窗口可能不会退出 Claude 进程,配置也可能被 Roaming / Local 两个目录读取差异影响。建议运行:
206
+
207
+ ```bash
208
+ claw doctor --windows
209
+ ```
210
+
211
+ 它会检查:
212
+
213
+ - Gateway 端口是否监听
214
+ - Gateway 登录启动脚本是否存在
215
+ - `%APPDATA%\Claude-3p` 和 `%LOCALAPPDATA%\Claude-3p` 配置是否一致
216
+ - 是否仍有 Claude 进程在后台运行
217
+
218
+ 如果提示配置未生效,请从系统托盘退出 Claude,或在任务管理器结束所有 Claude 进程后重新打开。
219
+
220
+ ### OpenAI / Kimi / Gemini 为什么要 Gateway
221
+
222
+ Claude Code 和 Claude Desktop 原生期望 Anthropic Messages 协议。OpenAI 兼容接口使用 `/v1/chat/completions`,Gemini 使用 `generateContent`。yingclaw 会在本机 Gateway 中做协议转换,所以这类接口需要保持 Gateway 运行。
223
+
224
+ ### 上游错误说明
225
+
226
+ Gateway 会把常见上游错误转换为中文说明:
227
+
228
+ | 类型 | 含义 | 建议 |
229
+ |------|------|------|
230
+ | 401 / 403 | Key 无效、过期或无权限 | 运行 `claw config` 重新输入 Key |
231
+ | 404 | 模型不存在或账号不可见 | 运行 `claw switch` 重新选择模型 |
232
+ | 408 / 504 / timeout | 上游响应超时 | 检查网络/VPN,或换快速模型 |
233
+ | subscription / quota | 套餐、余额或权限不足 | 到厂商控制台确认套餐和模型权限 |
234
+ | 429 | 请求限流 | 稍后重试或更换 Key / 模型 |
235
+
236
+ ### 生成排障包
237
+
238
+ 需要反馈问题时运行:
239
+
240
+ ```bash
241
+ claw support --windows -o yingclaw-support.json
242
+ ```
243
+
244
+ 排障包会包含当前配置摘要、doctor 检查、Gateway 状态、Desktop Code 状态和 Windows 专项检查,并脱敏 API Key / Gateway Key。公开发送前仍建议快速看一眼 Base URL 是否包含私有域名。
245
+
152
246
  ## 卸载
153
247
 
154
248
  ```bash
package/bin/cli.js CHANGED
@@ -39,17 +39,30 @@ const {
39
39
  getNodeInstallGuide,
40
40
  getInstallFailureHints,
41
41
  } = require('../lib/install');
42
- const { clearClaudeDesktopConfig, isDesktopConfigured, openClaudeDesktop, writeClaudeDesktopConfig } = require('../lib/desktop');
42
+ const {
43
+ buildClaudeDesktopCodeEnv,
44
+ checkClaudeDesktopCodeSettingsEnv,
45
+ clearClaudeDesktopCodeSettings,
46
+ clearClaudeDesktopConfig,
47
+ isDesktopConfigured,
48
+ openClaudeDesktop,
49
+ shouldPromptClaudeDesktopCodeSettings,
50
+ shouldWriteClaudeDesktopCodeSettings,
51
+ writeClaudeDesktopCodeSettings,
52
+ writeClaudeDesktopConfig,
53
+ } = require('../lib/desktop');
43
54
  const {
44
55
  DEFAULT_DESKTOP_GATEWAY_PORT,
45
56
  YINGCLAW_GATEWAY_PREFIX,
46
57
  buildDesktopGatewayMappingRows,
47
58
  checkDesktopGatewayStatus,
59
+ checkGatewayPortOwner,
48
60
  createGatewayServer,
49
61
  ensureDesktopGatewayConfig,
62
+ explainGatewayListenError,
50
63
  isDesktopChatModel,
51
64
  } = require('../lib/gateway');
52
- const { runDoctorChecks, summarize, STATUS_OK, STATUS_FAIL, STATUS_WARN, STATUS_INFO } = require('../lib/doctor');
65
+ const { buildDiagnosticReport, buildSupportBundle, runDoctorChecks, summarize, writeDiagnosticReport, STATUS_OK, STATUS_FAIL, STATUS_WARN, STATUS_INFO } = require('../lib/doctor');
53
66
  const { normalizeOpenAiBaseUrl } = require('../lib/openai');
54
67
 
55
68
  const program = new Command();
@@ -141,7 +154,8 @@ async function offerDesktopSync(chalk, ora, config) {
141
154
  if (!syncDesktop) return;
142
155
  const spinner = ora('同步 Claude 桌面应用配置...').start();
143
156
  try {
144
- writeClaudeDesktopConfig(config);
157
+ const desktopConfig = ensureDesktopGatewayConfig(config);
158
+ writeClaudeDesktopConfig(desktopConfig);
145
159
  spinner.succeed(chalk.green('Claude 桌面应用配置已同步'));
146
160
  } catch (e) {
147
161
  spinner.fail(chalk.red(`桌面配置同步失败: ${e.message}`));
@@ -338,8 +352,13 @@ function isOpenAiCompatibleConfig(config) {
338
352
  return getProviderProtocol(config) === 'openai';
339
353
  }
340
354
 
355
+ function needsLocalGateway(config) {
356
+ const p = getProviderProtocol(config);
357
+ return p === 'openai' || p === 'google';
358
+ }
359
+
341
360
  function ensureGatewayForOpenAiConfig(config) {
342
- return isOpenAiCompatibleConfig(config) ? ensureDesktopGatewayConfig(config) : config;
361
+ return needsLocalGateway(config) ? ensureDesktopGatewayConfig(config) : config;
343
362
  }
344
363
 
345
364
  function writeClaudeCodeEnv(config) {
@@ -352,15 +371,26 @@ function writeClaudeCodeEnv(config) {
352
371
  );
353
372
  }
354
373
 
374
+ function customBaseUrlPrompt(protocol) {
375
+ if (protocol === 'openai') return 'OpenAI Base URL';
376
+ if (protocol === 'google') return 'Google / Gemini Base URL';
377
+ return 'Anthropic Base URL';
378
+ }
379
+
380
+ function normalizeCustomBaseUrl(protocol, baseUrl) {
381
+ if (protocol === 'openai') return normalizeOpenAiBaseUrl(baseUrl);
382
+ return normalizeAnthropicBaseUrl(baseUrl);
383
+ }
384
+
355
385
  async function configureCustomProvider({ chalk, ora, existingConfig, providerKey = 'custom' }) {
356
386
  const provider = PROVIDERS[providerKey];
357
387
  const protocol = provider.protocol || 'anthropic';
358
388
  const sameProvider = existingConfig?.provider === providerKey;
359
389
  const baseUrl = await input({
360
- message: chalk.cyan(protocol === 'openai' ? 'OpenAI Base URL' : 'Anthropic Base URL'),
390
+ message: chalk.cyan(customBaseUrlPrompt(protocol)),
361
391
  default: sameProvider ? existingConfig.baseUrl : undefined,
362
392
  validate: (v) => v.trim().length > 0 && isValidUrl(v.trim()) ? true : '请输入有效 URL',
363
- }).then(v => protocol === 'openai' ? normalizeOpenAiBaseUrl(v.trim()) : normalizeAnthropicBaseUrl(v.trim()));
393
+ }).then(v => normalizeCustomBaseUrl(protocol, v.trim()));
364
394
 
365
395
  let apiKey = sameProvider ? existingConfig.apiKey : '';
366
396
  if (apiKey) {
@@ -449,6 +479,7 @@ async function showStatus() {
449
479
  claudeInstalled: isClaudeInstalled(),
450
480
  desktopGatewayStatus: await checkDesktopGatewayStatus(config),
451
481
  desktopGatewayAutostartStatus: getGatewayAutostartStatus(),
482
+ desktopCodeStatus: checkClaudeDesktopCodeSettingsEnv(config),
452
483
  env: process.env,
453
484
  });
454
485
 
@@ -461,6 +492,8 @@ async function showStatus() {
461
492
  ? chalk.cyan(value)
462
493
  : label === 'Desktop Gateway'
463
494
  ? value.includes('已运行') ? chalk.green(value) : chalk.yellow(value)
495
+ : label === 'Desktop Code'
496
+ ? value.includes('已配置') ? chalk.green(value) : chalk.yellow(value)
464
497
  : label === '当前终端' && value === '未生效'
465
498
  ? chalk.yellow(value)
466
499
  : value;
@@ -737,7 +770,7 @@ program
737
770
  console.log(chalk.dim(getStorageHint(file)));
738
771
  const displayedConfig = effectiveConfig;
739
772
  const needsGatewayHint = process.platform !== 'darwin' && process.platform !== 'win32'
740
- && isOpenAiCompatibleConfig(effectiveConfig);
773
+ && needsLocalGateway(effectiveConfig);
741
774
  console.log(boxen(
742
775
  chalk.bold('Claude Code 终端已接入\n\n') +
743
776
  chalk.dim('Base URL ') + chalk.cyan(buildClaudeEnv(displayedConfig).ANTHROPIC_BASE_URL) + '\n' +
@@ -854,7 +887,7 @@ program
854
887
  }
855
888
 
856
889
  const vscodeNeedsGatewayHint = process.platform !== 'darwin' && process.platform !== 'win32'
857
- && isOpenAiCompatibleConfig(effectiveConfig);
890
+ && needsLocalGateway(effectiveConfig);
858
891
  console.log(boxen(
859
892
  chalk.bold('VS Code Claude Code 扩展已配置\n\n') +
860
893
  chalk.dim('共享设置 ') + chalk.cyan(result.claudeSettings.file) + '\n' +
@@ -1045,7 +1078,8 @@ program
1045
1078
  ));
1046
1079
  });
1047
1080
  server.on('error', (error) => {
1048
- console.error(chalk.red(`Gateway 启动失败: ${error.message}`));
1081
+ const owner = checkGatewayPortOwner(port);
1082
+ console.error(chalk.red(explainGatewayListenError(error, owner)));
1049
1083
  process.exitCode = 1;
1050
1084
  });
1051
1085
  });
@@ -1054,6 +1088,7 @@ program
1054
1088
  .command('desktop')
1055
1089
  .description('接入 Claude 桌面应用使用当前模型')
1056
1090
  .option('--direct', '高级:直接写入当前厂商 Gateway URL,不启用本机模型映射')
1091
+ .option('--with-code', '同时接入 Claude Desktop 的 Code 模式(会写入 Claude Code 共享设置)')
1057
1092
  .action(async (options) => {
1058
1093
  const chalk = (await import('chalk')).default;
1059
1094
  const ora = (await import('ora')).default;
@@ -1098,16 +1133,27 @@ program
1098
1133
  saveConfig({ ...desktopConfig, desktopModels: selectedModels });
1099
1134
  desktopConfig = { ...desktopConfig, availableModels: selectedModels };
1100
1135
  }
1136
+
1137
+ if (shouldPromptClaudeDesktopCodeSettings(options)) {
1138
+ options.withCode = await confirm({
1139
+ message: '是否同时接入 Claude 桌面 Code 模式?(会写入 Claude Code 共享设置)',
1140
+ default: false,
1141
+ });
1142
+ }
1101
1143
  }
1102
1144
 
1103
1145
  const spinner = ora('写入 Claude 桌面应用配置...').start();
1104
1146
  let result;
1147
+ let codeSettingsResult = null;
1105
1148
  try {
1106
1149
  result = writeClaudeDesktopConfig(desktopConfig, { direct: options.direct });
1107
1150
  if (result.result === 'unsupported') {
1108
1151
  spinner.warn(chalk.yellow('当前系统暂不支持自动配置 Claude 桌面应用'));
1109
1152
  return;
1110
1153
  }
1154
+ if (shouldWriteClaudeDesktopCodeSettings(options)) {
1155
+ codeSettingsResult = writeClaudeDesktopCodeSettings(desktopConfig);
1156
+ }
1111
1157
  const extraFiles = Array.isArray(result.files) && result.files.length > 1
1112
1158
  ? chalk.dim(`(已同步 ${result.files.length} 个配置位置)`)
1113
1159
  : '';
@@ -1137,6 +1183,9 @@ program
1137
1183
  : chalk.dim('Gateway ') + chalk.cyan(gatewayUrl) + '\n' +
1138
1184
  chalk.dim('主模型 ') + chalk.yellow(desktopConfig.model) + '\n' +
1139
1185
  chalk.dim('快速模型 ') + chalk.yellow(desktopConfig.fastModel || desktopConfig.model) + '\n' +
1186
+ (codeSettingsResult
1187
+ ? chalk.dim('Code 模式 ') + chalk.cyan(codeSettingsResult.file) + chalk.dim('(已写入 Claude Code 共享设置)') + '\n'
1188
+ : chalk.dim('Code 模式 ') + chalk.dim('未接入;下次运行 claw desktop 时选择 Yes,或使用 claw desktop --with-code') + '\n') +
1140
1189
  chalk.dim('菜单映射 ') + buildDesktopGatewayMappingRows(desktopConfig)
1141
1190
  .map((row) => `${chalk.white(row.desktopLabel)} ${chalk.dim('→')} ${chalk.yellow(row.upstreamModel)}`)
1142
1191
  .join('\n' + chalk.dim(' ')) + '\n\n' +
@@ -1156,7 +1205,7 @@ program
1156
1205
  if (shouldOpen) {
1157
1206
  const openSpinner = ora('正在重启 Claude 桌面应用...').start();
1158
1207
  try {
1159
- await openClaudeDesktop();
1208
+ await openClaudeDesktop(shouldWriteClaudeDesktopCodeSettings(options) ? { injectEnv: buildClaudeDesktopCodeEnv(desktopConfig) } : {});
1160
1209
  openSpinner.succeed(chalk.green('Claude 桌面应用已重新打开(旧实例已退出,新配置生效)'));
1161
1210
  } catch (e) {
1162
1211
  openSpinner.fail(chalk.red(`自动重启失败: ${e.message}`));
@@ -1201,10 +1250,14 @@ program
1201
1250
 
1202
1251
  const spinner = ora('正在恢复 Claude 桌面应用默认配置...').start();
1203
1252
  const result = clearClaudeDesktopConfig();
1253
+ const codeSettingsResult = clearClaudeDesktopCodeSettings();
1204
1254
 
1205
- if (result.result === 'updated') {
1255
+ if (result.result === 'updated' || codeSettingsResult.result === 'updated') {
1206
1256
  spinner.succeed(chalk.green('Claude 桌面应用已恢复默认'));
1207
- const cleared = desktopConfigClearedPaths(result);
1257
+ const cleared = [
1258
+ ...desktopConfigClearedPaths(result),
1259
+ ...(codeSettingsResult.result === 'updated' ? [codeSettingsResult.file] : []),
1260
+ ];
1208
1261
  console.log(boxen(
1209
1262
  chalk.bold('已清除 Claude 桌面应用第三方推理配置:\n\n') +
1210
1263
  cleared.map(f => chalk.cyan(' • ' + f)).join('\n'),
@@ -1291,7 +1344,9 @@ program
1291
1344
  program
1292
1345
  .command('doctor')
1293
1346
  .description('诊断当前环境,列出所有问题和修复建议')
1294
- .action(async () => {
1347
+ .option('--export [file]', '导出脱敏诊断报告 JSON')
1348
+ .option('--windows', '包含 Windows 专项诊断')
1349
+ .action(async (options) => {
1295
1350
  const chalk = (await import('chalk')).default;
1296
1351
  const ora = (await import('ora')).default;
1297
1352
  const boxen = (await import('boxen')).default;
@@ -1299,7 +1354,7 @@ program
1299
1354
  console.log(await getBanner());
1300
1355
 
1301
1356
  const spinner = ora('运行诊断检查...').start();
1302
- const checks = await runDoctorChecks();
1357
+ const checks = await runDoctorChecks({ windows: options.windows });
1303
1358
  spinner.stop();
1304
1359
 
1305
1360
  const icons = {
@@ -1332,6 +1387,53 @@ program
1332
1387
  margin: { top: 1, bottom: 1 },
1333
1388
  }
1334
1389
  ));
1390
+
1391
+ if (options.export !== undefined) {
1392
+ const config = loadConfig();
1393
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
1394
+ const file = typeof options.export === 'string'
1395
+ ? options.export
1396
+ : `yingclaw-diagnostic-${stamp}.json`;
1397
+ const report = buildDiagnosticReport({
1398
+ packageVersion: pkg.version,
1399
+ config,
1400
+ checks,
1401
+ env: process.env,
1402
+ expectedEnv: config ? buildClaudeEnv(config) : {},
1403
+ desktopCodeStatus: config ? checkClaudeDesktopCodeSettingsEnv(config) : null,
1404
+ });
1405
+ writeDiagnosticReport(file, report);
1406
+ console.log(chalk.green(`诊断报告已导出:${file}`));
1407
+ }
1408
+ });
1409
+
1410
+ program
1411
+ .command('support')
1412
+ .description('生成脱敏排障包,便于反馈 Windows / Desktop / Gateway 问题')
1413
+ .option('-o, --output <file>', '输出文件', `yingclaw-support-${new Date().toISOString().replace(/[:.]/g, '-')}.json`)
1414
+ .option('--windows', '包含 Windows 专项诊断')
1415
+ .action(async (options) => {
1416
+ const chalk = (await import('chalk')).default;
1417
+ const ora = (await import('ora')).default;
1418
+
1419
+ console.log(await getBanner());
1420
+
1421
+ const config = loadConfig();
1422
+ const spinner = ora('生成脱敏排障包...').start();
1423
+ const checks = await runDoctorChecks({ windows: options.windows });
1424
+ const gatewayStatus = config ? await checkDesktopGatewayStatus(config, { timeoutMs: 600 }) : null;
1425
+ const bundle = buildSupportBundle({
1426
+ packageVersion: pkg.version,
1427
+ config,
1428
+ checks,
1429
+ env: process.env,
1430
+ expectedEnv: config ? buildClaudeEnv(config) : {},
1431
+ gatewayStatus,
1432
+ desktopCodeStatus: config ? checkClaudeDesktopCodeSettingsEnv(config) : null,
1433
+ });
1434
+ writeDiagnosticReport(options.output, bundle);
1435
+ spinner.succeed(chalk.green(`排障包已生成:${options.output}`));
1436
+ console.log(chalk.dim('已脱敏 API Key / Gateway Key;公开发送前仍建议快速看一眼 Base URL 是否含私有信息。'));
1335
1437
  });
1336
1438
 
1337
1439
  program
@@ -1430,6 +1532,7 @@ async function renderStatusBar(apiStatus, config) {
1430
1532
  claudeInstalled,
1431
1533
  desktopGatewayStatus: await checkDesktopGatewayStatus(config, { timeoutMs: 250 }),
1432
1534
  desktopGatewayAutostartStatus: getGatewayAutostartStatus(),
1535
+ desktopCodeStatus: checkClaudeDesktopCodeSettingsEnv(config),
1433
1536
  env: process.env,
1434
1537
  });
1435
1538
  const statusLines = buildMenuStatusLines(view, { apiStatus, claudeInstalled, platform: process.platform });
@@ -1440,6 +1543,7 @@ async function renderStatusBar(apiStatus, config) {
1440
1543
  if (line.startsWith('快速模型跨系列')) return chalk.yellow(line);
1441
1544
  if (line.startsWith('Desktop Gateway 未运行')) return chalk.yellow(line);
1442
1545
  if (line.startsWith('Desktop Gateway 已运行')) return chalk.green(line);
1546
+ if (line.startsWith('Desktop Code 未配置')) return chalk.yellow(line);
1443
1547
  if (line.startsWith('模型')) return line.replace(view.mainModel, chalk.yellow(view.mainModel)).replace(view.fastModel, chalk.yellow(view.fastModel));
1444
1548
  return line;
1445
1549
  }).join('\n ');
package/index.js CHANGED
@@ -4,6 +4,7 @@ module.exports = {
4
4
  ...require('./lib/desktop'),
5
5
  ...require('./lib/gateway'),
6
6
  ...require('./lib/install'),
7
+ ...require('./lib/google'),
7
8
  ...require('./lib/openai'),
8
9
  ...require('./lib/panel'),
9
10
  ...require('./lib/vscode'),
package/lib/config.js CHANGED
@@ -3,6 +3,7 @@ const path = require('path');
3
3
  const os = require('os');
4
4
  const { spawnSync } = require('child_process');
5
5
  const { normalizeOpenAiBaseUrl, openAiChatCompletionsUrl } = require('./openai');
6
+ const { googleGenerateContentUrl, normalizeGoogleBaseUrl } = require('./google');
6
7
 
7
8
  const CONFIG_FILE = path.join(os.homedir(), '.clawai.json');
8
9
  const WINDOWS_ENV_LABEL = 'Windows 用户环境变量';
@@ -25,6 +26,7 @@ const VSCODE_CLAUDE_ENV_KEYS = [
25
26
  ];
26
27
  const LEGACY_CLAUDE_ENV_KEYS = [
27
28
  'ANTHROPIC_API_KEY',
29
+ 'CLAUDE_CODE_SIMPLE',
28
30
  ];
29
31
  const CLEAR_CLAUDE_ENV_KEYS = [...new Set([...CLAUDE_ENV_KEYS, ...VSCODE_CLAUDE_ENV_KEYS, ...LEGACY_CLAUDE_ENV_KEYS])];
30
32
 
@@ -181,10 +183,16 @@ const PROVIDERS = {
181
183
  protocol: 'openai',
182
184
  models: [],
183
185
  },
186
+ custom_google: {
187
+ name: '自定义 Google 兼容接口',
188
+ custom: true,
189
+ protocol: 'google',
190
+ models: [],
191
+ },
184
192
  };
185
193
 
186
194
  const PREFERRED_MODEL_IDS = {
187
- qwen: ['qwen3-max', 'qwen3-plus', 'qwen3.5-plus'],
195
+ qwen: ['qwen3-max', 'qwen3-plus', 'qwen3.5-plus'],
188
196
  minimax: ['MiniMax-M2.7', 'MiniMax-M2.7-Turbo', 'MiniMax-M2.5'],
189
197
  glm: ['GLM-4.7', 'GLM-5.1', 'GLM-5-Turbo', 'GLM-4.5-Air'],
190
198
  mimo: ['mimo-v2.5-pro', 'mimo-v2.5', 'mimo-v2-flash'],
@@ -250,6 +258,16 @@ function parseModelIdsResponse(providerKey, data) {
250
258
  if (providerKey === 'bai') {
251
259
  return normalizeModelIds(providerKey, ids.filter(id => !id.includes('/')));
252
260
  }
261
+ if (providerKey === 'custom_google') {
262
+ const chatList = list.filter((m) =>
263
+ !Array.isArray(m.supportedGenerationMethods) || m.supportedGenerationMethods.includes('generateContent'),
264
+ );
265
+ const googleIds = chatList
266
+ .map((m) => (m.id || m.model || m.name || '').replace(/^models\//, ''))
267
+ .filter(Boolean)
268
+ .filter(isChatModelId);
269
+ return normalizeModelIds(providerKey, googleIds);
270
+ }
253
271
  return normalizeModelIds(providerKey, ids);
254
272
  }
255
273
 
@@ -288,6 +306,14 @@ function buildModelUrlCandidates(baseUrl) {
288
306
  add(`${withoutAnthropic}/v1/models`);
289
307
  }
290
308
 
309
+ // Google Gemini: /v1beta/models
310
+ const v1betaIdx = pathname.indexOf('/v1beta');
311
+ if (v1betaIdx !== -1) {
312
+ add(`${pathname.slice(0, v1betaIdx + '/v1beta'.length)}/models`);
313
+ } else {
314
+ add(`${pathname}/v1beta/models`);
315
+ }
316
+
291
317
  add('/v1/models');
292
318
 
293
319
  return [...new Set(candidates)];
@@ -391,8 +417,12 @@ function getProviderProtocol(config = {}) {
391
417
  }
392
418
 
393
419
  function buildProviderAuthHeaders(providerKey, apiKey) {
420
+ const protocol = getProviderProtocol({ provider: providerKey });
421
+ if (protocol === 'google') {
422
+ return { 'x-goog-api-key': apiKey };
423
+ }
394
424
  const headers = { authorization: `Bearer ${apiKey}` };
395
- if (getProviderProtocol({ provider: providerKey }) !== 'openai') {
425
+ if (protocol !== 'openai') {
396
426
  headers['api-key'] = apiKey; // MiMo 用这个 header
397
427
  }
398
428
  return headers;
@@ -430,7 +460,8 @@ function buildGatewayBaseUrlForEnv(config) {
430
460
  function buildClaudeEnv(config) {
431
461
  const { provider, baseUrl, apiKey, model, fastModel } = config;
432
462
  const resolvedFastModel = fastModel || resolveFastModel(PROVIDERS[provider], model);
433
- const useLocalGateway = getProviderProtocol(config) === 'openai' && config.desktopGatewayKey;
463
+ const protocol = getProviderProtocol(config);
464
+ const useLocalGateway = (protocol === 'openai' || protocol === 'google') && config.desktopGatewayKey;
434
465
  return {
435
466
  ANTHROPIC_BASE_URL: useLocalGateway ? buildGatewayBaseUrlForEnv(config) : baseUrl,
436
467
  ANTHROPIC_AUTH_TOKEN: useLocalGateway ? config.desktopGatewayKey : apiKey,
@@ -478,9 +509,13 @@ async function validateKey(config, options = {}) {
478
509
  const protocol = getProviderProtocol(config);
479
510
  let url;
480
511
  try {
481
- url = protocol === 'openai'
482
- ? openAiChatCompletionsUrl(config.baseUrl)
483
- : `${normalizeAnthropicBaseUrl(config.baseUrl)}/v1/messages`;
512
+ if (protocol === 'openai') {
513
+ url = openAiChatCompletionsUrl(config.baseUrl);
514
+ } else if (protocol === 'google') {
515
+ url = googleGenerateContentUrl(config.baseUrl, config.model, false);
516
+ } else {
517
+ url = `${normalizeAnthropicBaseUrl(config.baseUrl)}/v1/messages`;
518
+ }
484
519
  new URL(url);
485
520
  } catch {
486
521
  return null;
@@ -497,11 +532,9 @@ async function validateKey(config, options = {}) {
497
532
  ...buildProviderAuthHeaders(config.provider, config.apiKey),
498
533
  ...(protocol === 'anthropic' ? { 'x-api-key': config.apiKey, 'anthropic-version': '2023-06-01' } : {}),
499
534
  },
500
- body: JSON.stringify({
501
- model: config.model,
502
- max_tokens: 16,
503
- messages: [{ role: 'user', content: 'hi' }],
504
- }),
535
+ body: JSON.stringify(protocol === 'google'
536
+ ? { contents: [{ role: 'user', parts: [{ text: 'hi' }] }], generationConfig: { maxOutputTokens: 16 } }
537
+ : { model: config.model, max_tokens: 16, messages: [{ role: 'user', content: 'hi' }] }),
505
538
  });
506
539
  return classifyValidationStatus(res.status);
507
540
  } catch {