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.
- package/dist/web/assets/{Analytics-B9Shk-uq.js → Analytics-D24Q2IkD.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-Dlkn-_zW.js → ConfigTemplates-xNm_dyGS.js} +1 -1
- package/dist/web/assets/{Home-AGf32LvJ.js → Home-YQrz9dNs.js} +1 -1
- package/dist/web/assets/{PluginManager-BnAjHN4u.js → PluginManager-7ULrS-17.js} +1 -1
- package/dist/web/assets/{ProjectList-7YV_zk9L.js → ProjectList-Fy4xyQpg.js} +1 -1
- package/dist/web/assets/{SessionList-D0un-UKS.js → SessionList-DcLW9x3Z.js} +1 -1
- package/dist/web/assets/{SkillManager-DhYBmbtd.js → SkillManager-RnoRbzHA.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-JOZIPAGo.js → WorkspaceManager-B_BWS-xG.js} +1 -1
- package/dist/web/assets/index-Bvdezm5-.js +2 -0
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- package/src/commands/daemon.js +256 -80
- package/src/config/channel-preset-websites.js +1 -0
- package/src/server/api/channels.js +62 -17
- package/src/server/api/codex-channels.js +23 -3
- package/src/server/api/pm2-autostart.js +59 -24
- package/src/server/proxy-server.js +97 -5
- package/src/server/services/channels.js +62 -4
- package/src/server/services/claude-openai-gateway.js +1259 -0
- package/src/server/services/claude-token-recovery.js +386 -0
- package/src/server/services/codex-channels.js +8 -7
- package/dist/web/assets/index-DM3yF1Oo.js +0 -2
package/dist/web/index.html
CHANGED
|
@@ -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-
|
|
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
package/src/commands/daemon.js
CHANGED
|
@@ -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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
|
|
280
|
-
|
|
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
|
-
|
|
283
|
-
|
|
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
|
-
|
|
313
|
-
|
|
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 (
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
-
|
|
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
|
-
|
|
477
|
+
return;
|
|
339
478
|
}
|
|
340
479
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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 =
|
|
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
|
// 清除该渠道的模型重定向日志缓存,使下次请求时重新打印
|