coding-tool-x 3.5.13 → 3.5.14

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.
@@ -5,7 +5,7 @@
5
5
  <link rel="icon" href="/favicon.ico">
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
7
  <title>coding-tool-x · AI 编程工作台</title>
8
- <script type="module" crossorigin src="/assets/index-DM3yF1Oo.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-Bvdezm5-.js"></script>
9
9
  <link rel="modulepreload" crossorigin href="/assets/markdown-BNJfEgCA.js">
10
10
  <link rel="modulepreload" crossorigin href="/assets/vue-vendor-B5RqjEjQ.js">
11
11
  <link rel="modulepreload" crossorigin href="/assets/naive-ui-DM897mIP.js">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coding-tool-x",
3
- "version": "3.5.13",
3
+ "version": "3.5.14",
4
4
  "description": "Vibe Coding 增强工作助手 - 智能会话管理、动态渠道切换、全局搜索、实时监控",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -1,3 +1,4 @@
1
+ const fs = require('fs');
1
2
  const pm2 = require('pm2');
2
3
  const path = require('path');
3
4
  const chalk = require('chalk');
@@ -12,6 +13,16 @@ const {
12
13
  } = require('../utils/port-helper');
13
14
 
14
15
  const PM2_APP_NAME = 'cc-tool';
16
+ const STARTUP_LOG_FILE = 'cc-tool-out.log';
17
+ const CURRENT_PM2_FORK_PATH = resolveCurrentPm2ForkPath();
18
+
19
+ function resolveCurrentPm2ForkPath() {
20
+ try {
21
+ return require.resolve('pm2/lib/ProcessContainerFork');
22
+ } catch {
23
+ return '';
24
+ }
25
+ }
15
26
 
16
27
  /**
17
28
  * 连接到 PM2
@@ -86,6 +97,78 @@ function sleep(ms) {
86
97
  return new Promise(resolve => setTimeout(resolve, ms));
87
98
  }
88
99
 
100
+ function normalizePathForComparison(filePath) {
101
+ return path.normalize(String(filePath || ''));
102
+ }
103
+
104
+ function getFileSize(filePath) {
105
+ try {
106
+ return fs.statSync(filePath).size;
107
+ } catch {
108
+ return 0;
109
+ }
110
+ }
111
+
112
+ function readLogChunkSince(filePath, startOffset = 0, maxBytes = 32768) {
113
+ try {
114
+ const stat = fs.statSync(filePath);
115
+ if (stat.size <= startOffset) {
116
+ return '';
117
+ }
118
+
119
+ const safeOffset = Math.max(0, startOffset);
120
+ const readStart = Math.max(safeOffset, stat.size - maxBytes);
121
+ const length = stat.size - readStart;
122
+ if (length <= 0) {
123
+ return '';
124
+ }
125
+
126
+ const fd = fs.openSync(filePath, 'r');
127
+ try {
128
+ const buffer = Buffer.alloc(length);
129
+ fs.readSync(fd, buffer, 0, length, readStart);
130
+ return buffer.toString('utf8');
131
+ } finally {
132
+ fs.closeSync(fd);
133
+ }
134
+ } catch {
135
+ return '';
136
+ }
137
+ }
138
+
139
+ function extractMissingPm2ForkScriptPath(logText = '') {
140
+ const match = String(logText).match(/Cannot find module '([^']*ProcessContainerFork\.js)'/);
141
+ return match ? match[1] : '';
142
+ }
143
+
144
+ function detectStalePm2RuntimeIssue(logText = '', currentForkPath = CURRENT_PM2_FORK_PATH) {
145
+ const missingPath = extractMissingPm2ForkScriptPath(logText);
146
+ if (!missingPath || !currentForkPath) {
147
+ return null;
148
+ }
149
+
150
+ if (normalizePathForComparison(missingPath) === normalizePathForComparison(currentForkPath)) {
151
+ return null;
152
+ }
153
+
154
+ return {
155
+ missingPath,
156
+ currentPath: currentForkPath
157
+ };
158
+ }
159
+
160
+ function updatePM2Daemon() {
161
+ return new Promise((resolve, reject) => {
162
+ pm2.update((err, result) => {
163
+ if (err) {
164
+ reject(err);
165
+ } else {
166
+ resolve(result);
167
+ }
168
+ });
169
+ });
170
+ }
171
+
89
172
  function isPortOwnedByPid(port, pid) {
90
173
  if (!pid || pid <= 0) {
91
174
  return false;
@@ -257,103 +340,195 @@ async function waitForServiceReady(port, timeoutMs = 15000, intervalMs = 500) {
257
340
  };
258
341
  }
259
342
 
260
- /**
261
- * 启动服务(后台)
262
- */
263
- async function handleStart() {
264
- try {
265
- await connectPM2();
343
+ function buildStartOptions(port, enableHost, enableHttps) {
344
+ const pmArgs = ['ui', '--daemon'];
345
+ if (enableHost) {
346
+ pmArgs.push('--host');
347
+ }
348
+ if (enableHttps) {
349
+ pmArgs.push('--https');
350
+ }
266
351
 
267
- // 检查是否已经在运行
268
- const existing = await getCCToolProcess();
269
- if (existing && existing.pm2_env.status === 'online') {
270
- console.log(chalk.yellow('\n[WARN] 服务已在运行中\n'));
271
- console.log(chalk.gray(`进程 ID: ${existing.pid}`));
272
- console.log(chalk.gray(`运行时长: ${formatUptime(existing.pm2_env.pm_uptime)}`));
273
- console.log(chalk.gray('\n使用 ') + chalk.cyan('ctx status') + chalk.gray(' 查看详细状态'));
274
- console.log(chalk.gray('使用 ') + chalk.cyan('ctx restart') + chalk.gray(' 重启服务\n'));
275
- disconnectPM2();
276
- return;
277
- }
352
+ return {
353
+ name: PM2_APP_NAME,
354
+ script: path.join(__dirname, '../index.js'),
355
+ args: pmArgs,
356
+ interpreter: 'node',
357
+ autorestart: true,
358
+ max_memory_restart: '500M',
359
+ env: {
360
+ NODE_ENV: 'production',
361
+ CC_TOOL_PORT: port
362
+ },
363
+ output: path.join(PATHS.logs, STARTUP_LOG_FILE),
364
+ error: path.join(PATHS.logs, STARTUP_LOG_FILE),
365
+ merge_logs: true,
366
+ log_date_format: 'YYYY-MM-DD HH:mm:ss'
367
+ };
368
+ }
278
369
 
279
- const config = loadConfig();
280
- const port = config.ports?.webUI || 19999;
370
+ async function attemptStartService(startOptions, port, options = {}) {
371
+ const allowPm2Refresh = options.allowPm2Refresh !== false;
372
+ const logPath = path.join(PATHS.logs, STARTUP_LOG_FILE);
373
+ const logOffset = getFileSize(logPath);
281
374
 
282
- // 检查是否启用 LAN 访问 (--host 标志)
283
- const enableHost = process.argv.includes('--host');
284
- const enableHttps = process.argv.includes('--https');
285
- const pmArgs = ['ui', '--daemon'];
286
- if (enableHost) {
287
- pmArgs.push('--host');
288
- }
289
- if (enableHttps) {
290
- pmArgs.push('--https');
291
- }
292
- require('fs').mkdirSync(PATHS.logs, { recursive: true });
293
-
294
- // 启动 PM2 进程
295
- pm2.start({
296
- name: PM2_APP_NAME,
297
- script: path.join(__dirname, '../index.js'),
298
- args: pmArgs,
299
- interpreter: 'node',
300
- autorestart: true,
301
- max_memory_restart: '500M',
302
- env: {
303
- NODE_ENV: 'production',
304
- CC_TOOL_PORT: port
305
- },
306
- output: path.join(PATHS.logs, 'cc-tool-out.log'),
307
- error: path.join(PATHS.logs, 'cc-tool-out.log'),
308
- merge_logs: true,
309
- log_date_format: 'YYYY-MM-DD HH:mm:ss'
310
- }, async (err) => {
375
+ return new Promise((resolve, reject) => {
376
+ pm2.start(startOptions, async (err) => {
311
377
  if (err) {
312
- console.error(chalk.red('\n[ERROR] 启动服务失败:'), err.message);
313
- disconnectPM2();
314
- process.exit(1);
378
+ reject(err);
379
+ return;
315
380
  }
316
381
 
317
- let readyState = null;
318
382
  try {
319
- readyState = await waitForServiceReady(port);
320
- if (!readyState.ready) {
321
- const statusText = readyState.process?.pm2_env?.status || 'unknown';
322
- console.error(chalk.red('\n[ERROR] Coding-Tool 服务启动失败,进程未就绪\n'));
323
- console.error(chalk.gray(`PM2 状态: ${statusText}`));
324
- printPortToolIssue(readyState.degradedPortCheckIssue);
325
- console.error(chalk.yellow('[TIP] 请使用 ctx logs ui 查看详细日志\n'));
326
-
327
- pm2.delete(PM2_APP_NAME, () => {
328
- pm2.dump(() => {
329
- disconnectPM2();
330
- process.exit(1);
383
+ const readyState = await waitForServiceReady(port);
384
+ if (readyState.ready) {
385
+ resolve({ ok: true, readyState });
386
+ return;
387
+ }
388
+
389
+ const recentLogText = readLogChunkSince(logPath, logOffset);
390
+ const staleRuntimeIssue = allowPm2Refresh ? detectStalePm2RuntimeIssue(recentLogText) : null;
391
+
392
+ if (staleRuntimeIssue) {
393
+ console.log(chalk.yellow('\n[WARN] 检测到 PM2 守护进程仍引用旧的 pm2 运行时,正在自动刷新...'));
394
+ console.log(chalk.gray(`旧路径: ${staleRuntimeIssue.missingPath}`));
395
+ console.log(chalk.gray(`当前路径: ${staleRuntimeIssue.currentPath}`));
396
+
397
+ await updatePM2Daemon();
398
+
399
+ const recoveredState = await waitForServiceReady(port, 5000, 500);
400
+ if (recoveredState.ready) {
401
+ resolve({
402
+ ok: true,
403
+ readyState: recoveredState,
404
+ recoveredFromStalePm2: staleRuntimeIssue
331
405
  });
406
+ return;
407
+ }
408
+
409
+ try {
410
+ await deletePM2Process(PM2_APP_NAME);
411
+ } catch {
412
+ // ignore stale process cleanup errors; the next retry will surface real failures
413
+ }
414
+
415
+ resolve({
416
+ retry: true,
417
+ recoveredFromStalePm2: staleRuntimeIssue
332
418
  });
333
419
  return;
334
420
  }
421
+
422
+ resolve({
423
+ ok: false,
424
+ readyState
425
+ });
335
426
  } catch (checkError) {
336
- console.error(chalk.red('\n[ERROR] 启动后健康检查失败:'), checkError.message);
427
+ reject(checkError);
428
+ }
429
+ });
430
+ });
431
+ }
432
+
433
+ function cleanupFailedStart() {
434
+ return new Promise((resolve) => {
435
+ pm2.delete(PM2_APP_NAME, () => {
436
+ pm2.dump(() => {
437
+ resolve();
438
+ });
439
+ });
440
+ });
441
+ }
442
+
443
+ /**
444
+ * 启动服务(后台)
445
+ */
446
+ async function handleStart() {
447
+ try {
448
+ const config = loadConfig();
449
+ const port = config.ports?.webUI || 19999;
450
+ const enableHost = process.argv.includes('--host');
451
+ const enableHttps = process.argv.includes('--https');
452
+ fs.mkdirSync(PATHS.logs, { recursive: true });
453
+
454
+ let readyState = null;
455
+ let recoveredFromStalePm2 = null;
456
+
457
+ for (let attempt = 0; attempt < 2; attempt += 1) {
458
+ await connectPM2();
459
+
460
+ const existing = await getCCToolProcess();
461
+ if (existing && existing.pm2_env.status === 'online') {
462
+ if (recoveredFromStalePm2) {
463
+ readyState = {
464
+ ready: true,
465
+ process: existing,
466
+ degradedPortCheckIssue: getPortToolIssue()
467
+ };
468
+ break;
469
+ }
470
+
471
+ console.log(chalk.yellow('\n[WARN] 服务已在运行中\n'));
472
+ console.log(chalk.gray(`进程 ID: ${existing.pid}`));
473
+ console.log(chalk.gray(`运行时长: ${formatUptime(existing.pm2_env.pm_uptime)}`));
474
+ console.log(chalk.gray('\n使用 ') + chalk.cyan('ctx status') + chalk.gray(' 查看详细状态'));
475
+ console.log(chalk.gray('使用 ') + chalk.cyan('ctx restart') + chalk.gray(' 重启服务\n'));
337
476
  disconnectPM2();
338
- process.exit(1);
477
+ return;
339
478
  }
340
479
 
341
- console.log(chalk.green('\n[OK] Coding-Tool 服务已启动(后台运行)\n'));
342
- console.log(chalk.gray(`Web UI: ${enableHttps ? 'https' : 'http'}://localhost:${port}`));
343
- printPortToolIssue(readyState.degradedPortCheckIssue);
344
- if (enableHost) {
345
- console.log(chalk.yellow(`[WARN] LAN 访问已启用 (${enableHttps ? 'https' : 'http'}://<your-ip>:${port})`));
480
+ const startResult = await attemptStartService(
481
+ buildStartOptions(port, enableHost, enableHttps),
482
+ port,
483
+ { allowPm2Refresh: attempt === 0 }
484
+ );
485
+
486
+ if (startResult.retry) {
487
+ recoveredFromStalePm2 = startResult.recoveredFromStalePm2 || recoveredFromStalePm2;
488
+ disconnectPM2();
489
+ continue;
346
490
  }
347
- console.log(chalk.gray('\n可以安全关闭此终端窗口'));
348
- console.log(chalk.gray('\n常用命令:'));
349
- console.log(chalk.gray(' ') + chalk.cyan('ctx status') + chalk.gray(' - 查看服务状态'));
350
- console.log(chalk.gray(' ') + chalk.cyan('ctx logs') + chalk.gray(' - 查看实时日志'));
351
- console.log(chalk.gray(' ') + chalk.cyan('ctx stop') + chalk.gray(' - 停止服务\n'));
352
491
 
353
- // 保存进程列表
354
- pm2.dump((err) => {
492
+ if (!startResult.ok) {
493
+ readyState = startResult.readyState;
494
+ const statusText = readyState.process?.pm2_env?.status || 'unknown';
495
+ console.error(chalk.red('\n[ERROR] Coding-Tool 服务启动失败,进程未就绪\n'));
496
+ console.error(chalk.gray(`PM2 状态: ${statusText}`));
497
+ printPortToolIssue(readyState.degradedPortCheckIssue);
498
+ console.error(chalk.yellow('[TIP] 请使用 ctx logs ui 查看详细日志\n'));
499
+ await cleanupFailedStart();
355
500
  disconnectPM2();
356
- });
501
+ process.exit(1);
502
+ }
503
+
504
+ readyState = startResult.readyState;
505
+ recoveredFromStalePm2 = startResult.recoveredFromStalePm2 || recoveredFromStalePm2;
506
+ break;
507
+ }
508
+
509
+ if (!readyState || !readyState.ready) {
510
+ console.error(chalk.red('\n[ERROR] Coding-Tool 服务启动失败,PM2 守护进程刷新后仍未恢复\n'));
511
+ disconnectPM2();
512
+ process.exit(1);
513
+ }
514
+
515
+ console.log(chalk.green('\n[OK] Coding-Tool 服务已启动(后台运行)\n'));
516
+ console.log(chalk.gray(`Web UI: ${enableHttps ? 'https' : 'http'}://localhost:${port}`));
517
+ if (recoveredFromStalePm2) {
518
+ console.log(chalk.green('[OK] 已自动修复旧 PM2 运行时路径残留'));
519
+ }
520
+ printPortToolIssue(readyState.degradedPortCheckIssue);
521
+ if (enableHost) {
522
+ console.log(chalk.yellow(`[WARN] LAN 访问已启用 (${enableHttps ? 'https' : 'http'}://<your-ip>:${port})`));
523
+ }
524
+ console.log(chalk.gray('\n可以安全关闭此终端窗口'));
525
+ console.log(chalk.gray('\n常用命令:'));
526
+ console.log(chalk.gray(' ') + chalk.cyan('ctx status') + chalk.gray(' - 查看服务状态'));
527
+ console.log(chalk.gray(' ') + chalk.cyan('ctx logs') + chalk.gray(' - 查看实时日志'));
528
+ console.log(chalk.gray(' ') + chalk.cyan('ctx stop') + chalk.gray(' - 停止服务\n'));
529
+
530
+ pm2.dump(() => {
531
+ disconnectPM2();
357
532
  });
358
533
  } catch (error) {
359
534
  console.error(chalk.red('启动失败:'), error.message);
@@ -446,7 +621,6 @@ async function handleStatus() {
446
621
  }
447
622
 
448
623
  // 代理服务状态(从运行时文件检测)
449
- const fs = require('fs');
450
624
  ensureStorageDirMigrated();
451
625
  const claudeActive = fs.existsSync(PATHS.activeChannel.claude);
452
626
  const codexActive = fs.existsSync(PATHS.activeChannel.codex);
@@ -516,6 +690,8 @@ module.exports = {
516
690
  handleRestart,
517
691
  handleStatus,
518
692
  _test: {
693
+ detectStalePm2RuntimeIssue,
694
+ extractMissingPm2ForkScriptPath,
519
695
  shouldTreatPortOwnershipAsReady,
520
696
  getManagedPorts,
521
697
  shouldStopPM2Process
@@ -1,6 +1,7 @@
1
1
  const PRESET_WEBSITE_SOURCES = {
2
2
  claude: [
3
3
  { presetId: 'official', baseUrl: 'https://api.anthropic.com', websiteUrl: 'https://www.anthropic.com' },
4
+ { presetId: 'openai', baseUrl: 'https://api.openai.com/v1', websiteUrl: 'https://platform.openai.com' },
4
5
  { presetId: 'deepseek', baseUrl: 'https://api.deepseek.com/anthropic', websiteUrl: 'https://platform.deepseek.com' },
5
6
  { presetId: 'zhipu', baseUrl: 'https://open.bigmodel.cn/api/anthropic', websiteUrl: 'https://open.bigmodel.cn' },
6
7
  { presetId: 'kimi', baseUrl: 'https://api.moonshot.cn/anthropic', websiteUrl: 'https://platform.moonshot.cn' },
@@ -13,12 +13,14 @@ const {
13
13
  } = require('../services/channels');
14
14
  const { getSchedulerState } = require('../services/channel-scheduler');
15
15
  const { getChannelHealthStatus, getAllChannelHealthStatus, resetChannelHealth } = require('../services/channel-health');
16
+ const { fetchModelsFromProvider } = require('../services/model-detector');
16
17
  const {
17
18
  testChannelSpeed,
18
19
  getLatencyLevel,
19
20
  sanitizeBatchConcurrency,
20
21
  runWithConcurrencyLimit
21
22
  } = require('../services/speed-test');
23
+ const { normalizeGatewaySourceType } = require('../services/base/proxy-utils');
22
24
  const { PATHS } = require('../../config/paths');
23
25
  const { deleteBackup } = require('../services/settings-manager');
24
26
  const { getDefaultSpeedTestModelByToolType } = require('../../config/model-metadata');
@@ -30,6 +32,19 @@ function getDefaultClaudeModel() {
30
32
  return getDefaultSpeedTestModelByToolType('claude');
31
33
  }
32
34
 
35
+ function resolveClaudeGatewaySourceType(channel, requestedType) {
36
+ return normalizeGatewaySourceType(requestedType || channel?.gatewaySourceType, 'claude');
37
+ }
38
+
39
+ function getDefaultModelsForGatewaySourceType(gatewaySourceType) {
40
+ if (gatewaySourceType === 'openai_compatible' || gatewaySourceType === 'codex') {
41
+ const model = getDefaultSpeedTestModelByToolType('codex');
42
+ return model ? [model] : [];
43
+ }
44
+ const model = getDefaultClaudeModel();
45
+ return model ? [model] : [];
46
+ }
47
+
33
48
  // GET /api/channels - Get all channels with health status
34
49
  router.get('/', (req, res) => {
35
50
  try {
@@ -92,7 +107,8 @@ router.post('/', (req, res) => {
92
107
  modelRedirects,
93
108
  proxyUrl,
94
109
  speedTestModel,
95
- gatewaySourceType
110
+ gatewaySourceType,
111
+ targetApi
96
112
  } = req.body;
97
113
 
98
114
  if (!name || !baseUrl) {
@@ -112,7 +128,8 @@ router.post('/', (req, res) => {
112
128
  modelRedirects: modelRedirects || [],
113
129
  proxyUrl: proxyUrl || '',
114
130
  speedTestModel: speedTestModel || null,
115
- gatewaySourceType
131
+ gatewaySourceType,
132
+ targetApi
116
133
  });
117
134
  res.json({ channel });
118
135
  broadcastSchedulerState('claude', getSchedulerState('claude'));
@@ -224,7 +241,7 @@ router.post('/:id/apply-to-settings', async (req, res) => {
224
241
  });
225
242
  } catch (error) {
226
243
  console.error('Error applying channel to settings:', error);
227
- res.status(500).json({ error: error.message });
244
+ res.status(error.statusCode || 500).json({ error: error.message });
228
245
  }
229
246
  });
230
247
 
@@ -256,7 +273,7 @@ router.post('/:id/speed-test', async (req, res) => {
256
273
  return res.status(404).json({ error: '渠道不存在' });
257
274
  }
258
275
 
259
- const speedTestType = CLAUDE_GATEWAY_SOURCE_TYPE;
276
+ const speedTestType = resolveClaudeGatewaySourceType(channel);
260
277
  const result = await testChannelSpeed(channel, timeout, speedTestType);
261
278
  result.level = getLatencyLevel(result.latency);
262
279
  result.gatewaySourceType = speedTestType;
@@ -280,18 +297,46 @@ router.get('/:id/models', async (req, res) => {
280
297
  return res.status(404).json({ error: '渠道不存在' });
281
298
  }
282
299
 
283
- const gatewaySourceType = CLAUDE_GATEWAY_SOURCE_TYPE;
284
- const models = [getDefaultClaudeModel()];
285
- const now = new Date().toISOString();
286
- const result = {
287
- models,
288
- supported: models.length > 0,
289
- cached: false,
290
- fallbackUsed: false,
291
- lastChecked: now,
292
- error: models.length > 0 ? null : '未配置默认模型列表',
293
- errorHint: models.length > 0 ? null : '请在设置中配置 Claude 默认模型'
294
- };
300
+ const gatewaySourceType = resolveClaudeGatewaySourceType(channel, req.query.type);
301
+ const fallbackModels = getDefaultModelsForGatewaySourceType(gatewaySourceType);
302
+ let result;
303
+
304
+ if (gatewaySourceType === 'openai_compatible' || gatewaySourceType === 'codex') {
305
+ try {
306
+ result = await fetchModelsFromProvider(channel, gatewaySourceType);
307
+ } catch (error) {
308
+ result = {
309
+ models: [],
310
+ supported: true,
311
+ cached: false,
312
+ fallbackUsed: true,
313
+ lastChecked: new Date().toISOString(),
314
+ error: error.message || '获取模型列表失败',
315
+ errorHint: '已回退到默认模型列表'
316
+ };
317
+ }
318
+
319
+ if (!Array.isArray(result.models) || result.models.length === 0) {
320
+ result = {
321
+ ...result,
322
+ models: fallbackModels,
323
+ fallbackUsed: true,
324
+ error: result.error || (fallbackModels.length > 0 ? null : '未配置默认模型列表'),
325
+ errorHint: result.errorHint || (fallbackModels.length > 0 ? '已回退到默认模型列表' : '请在设置中配置默认模型')
326
+ };
327
+ }
328
+ } else {
329
+ const now = new Date().toISOString();
330
+ result = {
331
+ models: fallbackModels,
332
+ supported: fallbackModels.length > 0,
333
+ cached: false,
334
+ fallbackUsed: false,
335
+ lastChecked: now,
336
+ error: fallbackModels.length > 0 ? null : '未配置默认模型列表',
337
+ errorHint: fallbackModels.length > 0 ? null : '请在设置中配置 Claude 默认模型'
338
+ };
339
+ }
295
340
 
296
341
  res.json({
297
342
  channelId: id,
@@ -328,7 +373,7 @@ router.post('/speed-test-all', async (req, res) => {
328
373
  channels,
329
374
  safeConcurrency,
330
375
  async channel => {
331
- const speedTestType = CLAUDE_GATEWAY_SOURCE_TYPE;
376
+ const speedTestType = resolveClaudeGatewaySourceType(channel);
332
377
  const result = await testChannelSpeed(channel, timeout, speedTestType);
333
378
  result.level = getLatencyLevel(result.latency);
334
379
  result.gatewaySourceType = speedTestType;
@@ -134,6 +134,7 @@ module.exports = (config) => {
134
134
  providerKey,
135
135
  baseUrl,
136
136
  apiKey,
137
+ requiresOpenaiAuth,
137
138
  websiteUrl,
138
139
  enabled,
139
140
  weight,
@@ -148,7 +149,7 @@ module.exports = (config) => {
148
149
  return res.status(400).json({ error: 'Missing required fields: name, providerKey, baseUrl' });
149
150
  }
150
151
 
151
- if (!apiKey) {
152
+ if (!apiKey && requiresOpenaiAuth !== true) {
152
153
  return res.status(400).json({ error: 'Missing required fields: apiKey' });
153
154
  }
154
155
 
@@ -158,7 +159,7 @@ module.exports = (config) => {
158
159
  }
159
160
 
160
161
  // wireApi 固定为 'responses' (OpenAI Responses API 格式)
161
- const channel = createChannel(name, providerKey, baseUrl, apiKey, 'responses', {
162
+ const channel = createChannel(name, providerKey, baseUrl, requiresOpenaiAuth === true ? '' : apiKey, 'responses', {
162
163
  websiteUrl,
163
164
  enabled,
164
165
  weight,
@@ -166,7 +167,8 @@ module.exports = (config) => {
166
167
  modelRedirects: modelRedirects || [],
167
168
  speedTestModel: speedTestModel || null,
168
169
  presetId: presetId || null,
169
- gatewaySourceType
170
+ gatewaySourceType,
171
+ requiresOpenaiAuth: requiresOpenaiAuth === true
170
172
  });
171
173
  res.json(channel);
172
174
  broadcastSchedulerState('codex', getSchedulerState('codex'));
@@ -188,12 +190,30 @@ module.exports = (config) => {
188
190
 
189
191
  const { channelId } = req.params;
190
192
  const updates = req.body;
193
+ const currentChannel = getChannels().channels.find(channel => channel.id === channelId) || null;
194
+ const finalRequiresOpenaiAuth = Object.prototype.hasOwnProperty.call(updates, 'requiresOpenaiAuth')
195
+ ? updates.requiresOpenaiAuth === true
196
+ : currentChannel?.requiresOpenaiAuth === true;
197
+ const finalApiKey = Object.prototype.hasOwnProperty.call(updates, 'apiKey')
198
+ ? updates.apiKey
199
+ : currentChannel?.apiKey;
191
200
  if (Object.prototype.hasOwnProperty.call(updates, 'providerKey')) {
192
201
  const providerKeyError = validateCodexProviderKey(updates.providerKey);
193
202
  if (providerKeyError) {
194
203
  return res.status(400).json({ error: providerKeyError });
195
204
  }
196
205
  }
206
+ if (currentChannel) {
207
+ if (!finalRequiresOpenaiAuth && !finalApiKey) {
208
+ return res.status(400).json({ error: 'Missing required fields: apiKey' });
209
+ }
210
+ if (Object.prototype.hasOwnProperty.call(updates, 'requiresOpenaiAuth')) {
211
+ updates.requiresOpenaiAuth = finalRequiresOpenaiAuth;
212
+ }
213
+ if (finalRequiresOpenaiAuth) {
214
+ updates.apiKey = '';
215
+ }
216
+ }
197
217
 
198
218
  const channel = updateChannel(channelId, updates);
199
219
  // 清除该渠道的模型重定向日志缓存,使下次请求时重新打印