@adversity/coding-tool-x 3.1.0 → 3.1.2
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/CHANGELOG.md +39 -18
- package/README.md +8 -8
- package/dist/web/assets/ConfigTemplates-Bidwfdf2.css +1 -0
- package/dist/web/assets/ConfigTemplates-DvcbKKdS.js +1 -0
- package/dist/web/assets/Home-BJKPCBuk.css +1 -0
- package/dist/web/assets/Home-Cw-F_Wnu.js +1 -0
- package/dist/web/assets/PluginManager-ROyoZ-6m.css +1 -0
- package/dist/web/assets/PluginManager-jy_4GVxI.js +1 -0
- package/dist/web/assets/ProjectList-C1fQb9OW.css +1 -0
- package/dist/web/assets/ProjectList-Df1-NcNr.js +1 -0
- package/dist/web/assets/SessionList-BGJWyneI.css +1 -0
- package/dist/web/assets/SessionList-UWcZtC2r.js +1 -0
- package/dist/web/assets/SkillManager-D7pd-d_P.css +1 -0
- package/dist/web/assets/SkillManager-IRdseMKB.js +1 -0
- package/dist/web/assets/Terminal-BasTyDut.js +1 -0
- package/dist/web/assets/Terminal-DGNJeVtc.css +1 -0
- package/dist/web/assets/WorkspaceManager-CrwgQgmP.css +1 -0
- package/dist/web/assets/WorkspaceManager-D-D2kK1V.js +1 -0
- package/dist/web/assets/icons-kcfLIMBB.js +1 -0
- package/dist/web/assets/index-CoB3zF0K.css +1 -0
- package/dist/web/assets/index-CryrSLv8.js +2 -0
- package/dist/web/assets/markdown-BfC0goYb.css +10 -0
- package/dist/web/assets/markdown-C9MYpaSi.js +1 -0
- package/dist/web/assets/naive-ui-CSrLusZZ.js +1 -0
- package/dist/web/assets/{vendors-D2HHw_aW.js → vendors-CO3Upi1d.js} +2 -2
- package/dist/web/assets/vue-vendor-DqyWIXEb.js +45 -0
- package/dist/web/assets/xterm-6GBZ9nXN.css +32 -0
- package/dist/web/assets/xterm-BJzAjXCH.js +13 -0
- package/dist/web/index.html +8 -6
- package/package.json +4 -2
- package/src/commands/channels.js +48 -1
- package/src/commands/cli-type.js +4 -2
- package/src/commands/daemon.js +81 -12
- package/src/commands/doctor.js +10 -9
- package/src/commands/list.js +1 -1
- package/src/commands/logs.js +6 -4
- package/src/commands/port-config.js +24 -4
- package/src/commands/proxy-control.js +12 -6
- package/src/commands/search.js +1 -1
- package/src/commands/security.js +3 -2
- package/src/commands/stats.js +226 -52
- package/src/commands/switch.js +1 -1
- package/src/commands/toggle-proxy.js +31 -6
- package/src/commands/update.js +97 -0
- package/src/commands/workspace.js +1 -1
- package/src/config/default.js +41 -2
- package/src/config/loader.js +74 -8
- package/src/config/model-metadata.js +415 -0
- package/src/config/model-pricing.js +23 -93
- package/src/config/paths.js +105 -33
- package/src/index.js +64 -3
- package/src/plugins/constants.js +3 -2
- package/src/plugins/plugin-api.js +1 -1
- package/src/reset-config.js +4 -2
- package/src/server/api/agents.js +57 -14
- package/src/server/api/channels.js +112 -33
- package/src/server/api/codex-channels.js +111 -18
- package/src/server/api/codex-proxy.js +14 -8
- package/src/server/api/commands.js +71 -18
- package/src/server/api/config-export.js +0 -6
- package/src/server/api/config-registry.js +11 -3
- package/src/server/api/config.js +376 -5
- package/src/server/api/convert.js +133 -0
- package/src/server/api/dashboard.js +22 -6
- package/src/server/api/gemini-channels.js +107 -18
- package/src/server/api/gemini-proxy.js +14 -8
- package/src/server/api/gemini-sessions.js +1 -1
- package/src/server/api/health-check.js +4 -3
- package/src/server/api/mcp.js +3 -3
- package/src/server/api/opencode-channels.js +497 -0
- package/src/server/api/opencode-projects.js +99 -0
- package/src/server/api/opencode-proxy.js +207 -0
- package/src/server/api/opencode-sessions.js +345 -0
- package/src/server/api/opencode-statistics.js +57 -0
- package/src/server/api/plugins.js +66 -19
- package/src/server/api/prompts.js +2 -2
- package/src/server/api/proxy.js +7 -4
- package/src/server/api/sessions.js +3 -0
- package/src/server/api/settings.js +111 -0
- package/src/server/api/skills.js +69 -18
- package/src/server/api/workspaces.js +78 -6
- package/src/server/codex-proxy-server.js +36 -22
- package/src/server/dev-server.js +1 -1
- package/src/server/gemini-proxy-server.js +21 -7
- package/src/server/index.js +174 -58
- package/src/server/opencode-proxy-server.js +5486 -0
- package/src/server/proxy-server.js +33 -22
- package/src/server/services/agents-service.js +61 -24
- package/src/server/services/channel-scheduler.js +9 -5
- package/src/server/services/channels.js +64 -37
- package/src/server/services/codex-channels.js +56 -43
- package/src/server/services/codex-sessions.js +105 -6
- package/src/server/services/codex-settings-manager.js +271 -49
- package/src/server/services/codex-statistics-service.js +2 -2
- package/src/server/services/commands-service.js +84 -25
- package/src/server/services/config-export-service.js +7 -45
- package/src/server/services/config-registry-service.js +63 -17
- package/src/server/services/config-sync-manager.js +160 -7
- package/src/server/services/config-templates-service.js +204 -51
- package/src/server/services/env-checker.js +50 -13
- package/src/server/services/env-manager.js +155 -19
- package/src/server/services/favorites.js +5 -3
- package/src/server/services/gemini-channels.js +33 -44
- package/src/server/services/gemini-statistics-service.js +2 -2
- package/src/server/services/mcp-service.js +350 -9
- package/src/server/services/model-detector.js +707 -221
- package/src/server/services/network-access.js +80 -0
- package/src/server/services/opencode-channels.js +208 -0
- package/src/server/services/opencode-gateway-converter.js +639 -0
- package/src/server/services/opencode-sessions.js +931 -0
- package/src/server/services/opencode-settings-manager.js +478 -0
- package/src/server/services/opencode-statistics-service.js +255 -0
- package/src/server/services/plugins-service.js +479 -22
- package/src/server/services/prompts-service.js +53 -11
- package/src/server/services/proxy-runtime.js +1 -1
- package/src/server/services/repo-scanner-base.js +1 -1
- package/src/server/services/response-decoder.js +21 -0
- package/src/server/services/security-config.js +1 -1
- package/src/server/services/session-cache.js +1 -1
- package/src/server/services/skill-service.js +300 -46
- package/src/server/services/speed-test.js +464 -186
- package/src/server/services/statistics-service.js +2 -2
- package/src/server/services/terminal-commands.js +10 -3
- package/src/server/services/terminal-config.js +1 -1
- package/src/server/services/ui-config.js +1 -1
- package/src/server/services/workspace-service.js +57 -100
- package/src/server/websocket-server.js +156 -8
- package/src/ui/menu.js +49 -40
- package/src/utils/port-helper.js +22 -8
- package/src/utils/session.js +5 -4
- package/dist/web/assets/icons-CO_2OFES.js +0 -1
- package/dist/web/assets/index-DI8QOi-E.js +0 -14
- package/dist/web/assets/index-uLHGdeZh.css +0 -41
- package/dist/web/assets/naive-ui-B1re3c-e.js +0 -1
- package/dist/web/assets/vue-vendor-6JaYHOiI.js +0 -44
- package/src/server/api/oauth.js +0 -294
- package/src/server/api/permissions.js +0 -385
- package/src/server/config/oauth-providers.js +0 -68
- package/src/server/services/oauth-callback-server.js +0 -284
- package/src/server/services/oauth-service.js +0 -378
- package/src/server/services/oauth-token-storage.js +0 -135
- package/src/server/services/permission-templates-service.js +0 -308
|
@@ -12,10 +12,19 @@ const {
|
|
|
12
12
|
} = require('../services/channels');
|
|
13
13
|
const { getSchedulerState } = require('../services/channel-scheduler');
|
|
14
14
|
const { getChannelHealthStatus, getAllChannelHealthStatus, resetChannelHealth } = require('../services/channel-health');
|
|
15
|
-
const {
|
|
16
|
-
|
|
15
|
+
const {
|
|
16
|
+
testChannelSpeed,
|
|
17
|
+
getLatencyLevel,
|
|
18
|
+
sanitizeBatchConcurrency,
|
|
19
|
+
runWithConcurrencyLimit
|
|
20
|
+
} = require('../services/speed-test');
|
|
21
|
+
const {
|
|
22
|
+
probeModelAvailability,
|
|
23
|
+
fetchModelsFromProvider
|
|
24
|
+
} = require('../services/model-detector');
|
|
17
25
|
const { broadcastLog, broadcastProxyState, broadcastSchedulerState } = require('../websocket-server');
|
|
18
26
|
const { clearRedirectCache } = require('../proxy-server');
|
|
27
|
+
const CLAUDE_GATEWAY_SOURCE_TYPE = 'claude';
|
|
19
28
|
|
|
20
29
|
// GET /api/channels - Get all channels with health status
|
|
21
30
|
router.get('/', (req, res) => {
|
|
@@ -66,16 +75,40 @@ router.get('/current', (req, res) => {
|
|
|
66
75
|
// POST /api/channels - Create new channel
|
|
67
76
|
router.post('/', (req, res) => {
|
|
68
77
|
try {
|
|
69
|
-
const {
|
|
78
|
+
const {
|
|
79
|
+
name,
|
|
80
|
+
baseUrl,
|
|
81
|
+
apiKey,
|
|
82
|
+
websiteUrl,
|
|
83
|
+
enabled,
|
|
84
|
+
weight,
|
|
85
|
+
maxConcurrency,
|
|
86
|
+
presetId,
|
|
87
|
+
modelConfig,
|
|
88
|
+
modelRedirects,
|
|
89
|
+
proxyUrl,
|
|
90
|
+
speedTestModel,
|
|
91
|
+
gatewaySourceType
|
|
92
|
+
} = req.body;
|
|
93
|
+
|
|
94
|
+
if (!name || !baseUrl) {
|
|
95
|
+
return res.status(400).json({ error: 'Missing required fields: name, baseUrl' });
|
|
96
|
+
}
|
|
70
97
|
|
|
71
|
-
if (!
|
|
72
|
-
return res.status(400).json({ error: 'Missing required fields' });
|
|
98
|
+
if (!apiKey) {
|
|
99
|
+
return res.status(400).json({ error: 'Missing required fields: apiKey' });
|
|
73
100
|
}
|
|
74
101
|
|
|
75
102
|
const channel = createChannel(name, baseUrl, apiKey, websiteUrl, {
|
|
76
103
|
enabled,
|
|
77
104
|
weight,
|
|
78
|
-
maxConcurrency
|
|
105
|
+
maxConcurrency,
|
|
106
|
+
presetId,
|
|
107
|
+
modelConfig,
|
|
108
|
+
modelRedirects: modelRedirects || [],
|
|
109
|
+
proxyUrl: proxyUrl || '',
|
|
110
|
+
speedTestModel: speedTestModel || null,
|
|
111
|
+
gatewaySourceType
|
|
79
112
|
});
|
|
80
113
|
res.json({ channel });
|
|
81
114
|
broadcastSchedulerState('claude', getSchedulerState('claude'));
|
|
@@ -114,10 +147,10 @@ router.put('/:id', (req, res) => {
|
|
|
114
147
|
});
|
|
115
148
|
|
|
116
149
|
// DELETE /api/channels/:id - Delete channel
|
|
117
|
-
router.delete('/:id', (req, res) => {
|
|
150
|
+
router.delete('/:id', async (req, res) => {
|
|
118
151
|
try {
|
|
119
152
|
const { id } = req.params;
|
|
120
|
-
const result = deleteChannel(id);
|
|
153
|
+
const result = await deleteChannel(id);
|
|
121
154
|
res.json(result);
|
|
122
155
|
broadcastSchedulerState('claude', getSchedulerState('claude'));
|
|
123
156
|
} catch (error) {
|
|
@@ -213,9 +246,10 @@ router.post('/:id/speed-test', async (req, res) => {
|
|
|
213
246
|
return res.status(404).json({ error: '渠道不存在' });
|
|
214
247
|
}
|
|
215
248
|
|
|
216
|
-
|
|
217
|
-
const result = await testChannelSpeed(channel, timeout,
|
|
249
|
+
const speedTestType = CLAUDE_GATEWAY_SOURCE_TYPE;
|
|
250
|
+
const result = await testChannelSpeed(channel, timeout, speedTestType);
|
|
218
251
|
result.level = getLatencyLevel(result.latency);
|
|
252
|
+
result.gatewaySourceType = speedTestType;
|
|
219
253
|
|
|
220
254
|
res.json(result);
|
|
221
255
|
} catch (error) {
|
|
@@ -228,12 +262,6 @@ router.post('/:id/speed-test', async (req, res) => {
|
|
|
228
262
|
router.get('/:id/models', async (req, res) => {
|
|
229
263
|
try {
|
|
230
264
|
const { id } = req.params;
|
|
231
|
-
const VALID_CHANNEL_TYPES = ['claude', 'codex', 'gemini', 'openai_compatible'];
|
|
232
|
-
const { type = 'claude' } = req.query;
|
|
233
|
-
|
|
234
|
-
if (!VALID_CHANNEL_TYPES.includes(type)) {
|
|
235
|
-
return res.status(400).json({ error: 'Invalid channel type', channelId: id });
|
|
236
|
-
}
|
|
237
265
|
|
|
238
266
|
const channels = getAllChannels();
|
|
239
267
|
const channel = channels.find(ch => ch.id === id);
|
|
@@ -242,23 +270,53 @@ router.get('/:id/models', async (req, res) => {
|
|
|
242
270
|
return res.status(404).json({ error: '渠道不存在' });
|
|
243
271
|
}
|
|
244
272
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
273
|
+
const gatewaySourceType = CLAUDE_GATEWAY_SOURCE_TYPE;
|
|
274
|
+
const listResult = await fetchModelsFromProvider(channel, 'openai_compatible');
|
|
275
|
+
const listedModels = Array.isArray(listResult.models) ? listResult.models : [];
|
|
276
|
+
let result;
|
|
277
|
+
|
|
278
|
+
if (listedModels.length > 0) {
|
|
279
|
+
result = {
|
|
280
|
+
models: listedModels,
|
|
281
|
+
supported: true,
|
|
282
|
+
cached: !!listResult.cached,
|
|
283
|
+
fallbackUsed: false,
|
|
284
|
+
lastChecked: listResult.lastChecked || new Date().toISOString(),
|
|
285
|
+
error: null,
|
|
286
|
+
errorHint: null
|
|
287
|
+
};
|
|
288
|
+
} else {
|
|
289
|
+
const usingConfiguredProbe = !!listResult.disabledByConfig;
|
|
290
|
+
const probe = await probeModelAvailability(channel, gatewaySourceType, {
|
|
291
|
+
stopOnFirstAvailable: false
|
|
292
|
+
});
|
|
293
|
+
const probedModels = Array.isArray(probe.availableModels) ? probe.availableModels : [];
|
|
294
|
+
|
|
295
|
+
result = {
|
|
296
|
+
models: probedModels,
|
|
297
|
+
supported: probedModels.length > 0,
|
|
298
|
+
cached: !!probe.cached || !!listResult.cached,
|
|
299
|
+
fallbackUsed: probedModels.length > 0,
|
|
300
|
+
lastChecked: probe.lastChecked || listResult.lastChecked || new Date().toISOString(),
|
|
301
|
+
error: probedModels.length > 0 ? null : (listResult.error || '无法获取可用模型'),
|
|
302
|
+
errorHint: probedModels.length > 0
|
|
303
|
+
? (usingConfiguredProbe ? '已按设置跳过 /v1/models,使用默认模型探测结果' : '模型列表接口不可用,已使用模型探测结果')
|
|
304
|
+
: (listResult.errorHint || (usingConfiguredProbe
|
|
305
|
+
? '已按设置跳过 /v1/models,且默认模型探测无可用结果'
|
|
306
|
+
: '模型列表接口不可用且模型探测无可用结果'))
|
|
307
|
+
};
|
|
251
308
|
}
|
|
252
309
|
|
|
253
|
-
const result = await fetchModelsFromProvider(channel, channelType);
|
|
254
|
-
|
|
255
310
|
res.json({
|
|
256
311
|
channelId: id,
|
|
312
|
+
gatewaySourceType,
|
|
257
313
|
models: result.models,
|
|
258
314
|
supported: result.supported,
|
|
315
|
+
fallbackUsed: result.fallbackUsed,
|
|
259
316
|
cached: result.cached,
|
|
260
317
|
fetchedAt: result.lastChecked || new Date().toISOString(),
|
|
261
|
-
error: result.error
|
|
318
|
+
error: result.error,
|
|
319
|
+
errorHint: result.errorHint
|
|
262
320
|
});
|
|
263
321
|
} catch (error) {
|
|
264
322
|
console.error('Error fetching channel models:', error);
|
|
@@ -272,18 +330,36 @@ router.get('/:id/models', async (req, res) => {
|
|
|
272
330
|
// POST /api/channels/speed-test-all - Test all channels speed
|
|
273
331
|
router.post('/speed-test-all', async (req, res) => {
|
|
274
332
|
try {
|
|
275
|
-
const { timeout = 10000 } = req.body;
|
|
333
|
+
const { timeout = 10000, concurrency } = req.body || {};
|
|
276
334
|
const channels = getAllChannels();
|
|
335
|
+
const safeConcurrency = sanitizeBatchConcurrency(concurrency);
|
|
277
336
|
|
|
278
337
|
if (channels.length === 0) {
|
|
279
338
|
return res.json({ results: [], message: '没有可测试的渠道' });
|
|
280
339
|
}
|
|
281
340
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
341
|
+
const results = await runWithConcurrencyLimit(
|
|
342
|
+
channels,
|
|
343
|
+
safeConcurrency,
|
|
344
|
+
async channel => {
|
|
345
|
+
const speedTestType = CLAUDE_GATEWAY_SOURCE_TYPE;
|
|
346
|
+
const result = await testChannelSpeed(channel, timeout, speedTestType);
|
|
347
|
+
result.level = getLatencyLevel(result.latency);
|
|
348
|
+
result.gatewaySourceType = speedTestType;
|
|
349
|
+
return result;
|
|
350
|
+
}
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
// 与其他渠道保持一致:成功在前,成功结果按延迟升序
|
|
354
|
+
results.sort((a, b) => {
|
|
355
|
+
if (a.success && !b.success) return -1;
|
|
356
|
+
if (!a.success && b.success) return 1;
|
|
357
|
+
if (a.success && b.success) {
|
|
358
|
+
const aLatency = (a.latency === null || a.latency === undefined) ? Infinity : a.latency;
|
|
359
|
+
const bLatency = (b.latency === null || b.latency === undefined) ? Infinity : b.latency;
|
|
360
|
+
return aLatency - bLatency;
|
|
361
|
+
}
|
|
362
|
+
return 0;
|
|
287
363
|
});
|
|
288
364
|
|
|
289
365
|
res.json({
|
|
@@ -292,7 +368,8 @@ router.post('/speed-test-all', async (req, res) => {
|
|
|
292
368
|
total: results.length,
|
|
293
369
|
success: results.filter(r => r.success).length,
|
|
294
370
|
failed: results.filter(r => !r.success).length,
|
|
295
|
-
avgLatency: calculateAvgLatency(results)
|
|
371
|
+
avgLatency: calculateAvgLatency(results),
|
|
372
|
+
concurrency: safeConcurrency
|
|
296
373
|
}
|
|
297
374
|
});
|
|
298
375
|
} catch (error) {
|
|
@@ -303,7 +380,9 @@ router.post('/speed-test-all', async (req, res) => {
|
|
|
303
380
|
|
|
304
381
|
// 计算平均延迟
|
|
305
382
|
function calculateAvgLatency(results) {
|
|
306
|
-
const successResults = results.filter(
|
|
383
|
+
const successResults = results.filter(
|
|
384
|
+
r => r.success && r.latency !== null && r.latency !== undefined
|
|
385
|
+
);
|
|
307
386
|
if (successResults.length === 0) return null;
|
|
308
387
|
const sum = successResults.reduce((acc, r) => acc + r.latency, 0);
|
|
309
388
|
return Math.round(sum / successResults.length);
|
|
@@ -13,9 +13,18 @@ const { getSchedulerState } = require('../services/channel-scheduler');
|
|
|
13
13
|
const { getChannelHealthStatus, resetChannelHealth } = require('../services/channel-health');
|
|
14
14
|
const { broadcastSchedulerState, broadcastLog } = require('../websocket-server');
|
|
15
15
|
const { isCodexInstalled } = require('../services/codex-config');
|
|
16
|
-
const {
|
|
16
|
+
const {
|
|
17
|
+
testChannelSpeed,
|
|
18
|
+
getLatencyLevel,
|
|
19
|
+
sanitizeBatchConcurrency,
|
|
20
|
+
runWithConcurrencyLimit
|
|
21
|
+
} = require('../services/speed-test');
|
|
17
22
|
const { clearCodexRedirectCache } = require('../codex-proxy-server');
|
|
18
|
-
const {
|
|
23
|
+
const {
|
|
24
|
+
fetchModelsFromProvider,
|
|
25
|
+
probeModelAvailability,
|
|
26
|
+
} = require('../services/model-detector');
|
|
27
|
+
const CODEX_GATEWAY_SOURCE_TYPE = 'codex';
|
|
19
28
|
|
|
20
29
|
module.exports = (config) => {
|
|
21
30
|
/**
|
|
@@ -58,11 +67,50 @@ module.exports = (config) => {
|
|
|
58
67
|
return res.status(404).json({ error: '渠道不存在' });
|
|
59
68
|
}
|
|
60
69
|
|
|
61
|
-
|
|
62
|
-
const
|
|
70
|
+
const gatewaySourceType = CODEX_GATEWAY_SOURCE_TYPE;
|
|
71
|
+
const listResult = await fetchModelsFromProvider(channel, 'openai_compatible');
|
|
72
|
+
const listedModels = Array.isArray(listResult.models) ? listResult.models : [];
|
|
73
|
+
let result;
|
|
74
|
+
|
|
75
|
+
if (listedModels.length > 0) {
|
|
76
|
+
result = listResult;
|
|
77
|
+
} else {
|
|
78
|
+
const usingConfiguredProbe = !!listResult.disabledByConfig;
|
|
79
|
+
const probe = await probeModelAvailability(channel, 'codex');
|
|
80
|
+
const probedModels = Array.isArray(probe.availableModels) ? probe.availableModels : [];
|
|
81
|
+
|
|
82
|
+
if (probedModels.length > 0) {
|
|
83
|
+
result = {
|
|
84
|
+
models: probedModels,
|
|
85
|
+
supported: true,
|
|
86
|
+
cached: !!probe.cached,
|
|
87
|
+
fallbackUsed: false,
|
|
88
|
+
lastChecked: probe.lastChecked || listResult.lastChecked || new Date().toISOString(),
|
|
89
|
+
error: null,
|
|
90
|
+
errorHint: listResult.error
|
|
91
|
+
? (usingConfiguredProbe
|
|
92
|
+
? '已按设置跳过 /v1/models,使用默认模型探测结果'
|
|
93
|
+
: '模型列表接口不可用,已自动切换为模型探测结果')
|
|
94
|
+
: null
|
|
95
|
+
};
|
|
96
|
+
} else {
|
|
97
|
+
result = {
|
|
98
|
+
models: [],
|
|
99
|
+
supported: false,
|
|
100
|
+
cached: !!probe.cached || !!listResult.cached,
|
|
101
|
+
fallbackUsed: false,
|
|
102
|
+
lastChecked: probe.lastChecked || listResult.lastChecked || new Date().toISOString(),
|
|
103
|
+
error: listResult.error || '无法探测可用模型',
|
|
104
|
+
errorHint: listResult.errorHint || (usingConfiguredProbe
|
|
105
|
+
? '已按设置跳过 /v1/models,且默认模型探测无可用结果'
|
|
106
|
+
: '模型列表接口不可用且模型探测无可用结果')
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
63
110
|
|
|
64
111
|
res.json({
|
|
65
112
|
channelId: id,
|
|
113
|
+
gatewaySourceType,
|
|
66
114
|
models: result.models,
|
|
67
115
|
supported: result.supported,
|
|
68
116
|
cached: result.cached,
|
|
@@ -91,10 +139,27 @@ module.exports = (config) => {
|
|
|
91
139
|
return res.status(404).json({ error: 'Codex CLI not installed' });
|
|
92
140
|
}
|
|
93
141
|
|
|
94
|
-
const {
|
|
142
|
+
const {
|
|
143
|
+
name,
|
|
144
|
+
providerKey,
|
|
145
|
+
baseUrl,
|
|
146
|
+
apiKey,
|
|
147
|
+
websiteUrl,
|
|
148
|
+
enabled,
|
|
149
|
+
weight,
|
|
150
|
+
maxConcurrency,
|
|
151
|
+
modelRedirects,
|
|
152
|
+
speedTestModel,
|
|
153
|
+
presetId,
|
|
154
|
+
gatewaySourceType
|
|
155
|
+
} = req.body;
|
|
156
|
+
|
|
157
|
+
if (!name || !providerKey || !baseUrl) {
|
|
158
|
+
return res.status(400).json({ error: 'Missing required fields: name, providerKey, baseUrl' });
|
|
159
|
+
}
|
|
95
160
|
|
|
96
|
-
if (!
|
|
97
|
-
return res.status(400).json({ error: 'Missing required fields' });
|
|
161
|
+
if (!apiKey) {
|
|
162
|
+
return res.status(400).json({ error: 'Missing required fields: apiKey' });
|
|
98
163
|
}
|
|
99
164
|
|
|
100
165
|
// wireApi 固定为 'responses' (OpenAI Responses API 格式)
|
|
@@ -102,7 +167,11 @@ module.exports = (config) => {
|
|
|
102
167
|
websiteUrl,
|
|
103
168
|
enabled,
|
|
104
169
|
weight,
|
|
105
|
-
maxConcurrency
|
|
170
|
+
maxConcurrency,
|
|
171
|
+
modelRedirects: modelRedirects || [],
|
|
172
|
+
speedTestModel: speedTestModel || null,
|
|
173
|
+
presetId: presetId || null,
|
|
174
|
+
gatewaySourceType
|
|
106
175
|
});
|
|
107
176
|
res.json(channel);
|
|
108
177
|
broadcastSchedulerState('codex', getSchedulerState('codex'));
|
|
@@ -140,14 +209,14 @@ module.exports = (config) => {
|
|
|
140
209
|
* DELETE /api/codex/channels/:channelId
|
|
141
210
|
* 删除渠道
|
|
142
211
|
*/
|
|
143
|
-
router.delete('/:channelId', (req, res) => {
|
|
212
|
+
router.delete('/:channelId', async (req, res) => {
|
|
144
213
|
try {
|
|
145
214
|
if (!isCodexInstalled()) {
|
|
146
215
|
return res.status(404).json({ error: 'Codex CLI not installed' });
|
|
147
216
|
}
|
|
148
217
|
|
|
149
218
|
const { channelId } = req.params;
|
|
150
|
-
const result = deleteChannel(channelId);
|
|
219
|
+
const result = await deleteChannel(channelId);
|
|
151
220
|
res.json(result);
|
|
152
221
|
broadcastSchedulerState('codex', getSchedulerState('codex'));
|
|
153
222
|
} catch (err) {
|
|
@@ -217,8 +286,10 @@ module.exports = (config) => {
|
|
|
217
286
|
return res.status(404).json({ error: '渠道不存在' });
|
|
218
287
|
}
|
|
219
288
|
|
|
220
|
-
const
|
|
289
|
+
const speedTestType = CODEX_GATEWAY_SOURCE_TYPE;
|
|
290
|
+
const result = await testChannelSpeed(channel, timeout, speedTestType);
|
|
221
291
|
result.level = getLatencyLevel(result.latency);
|
|
292
|
+
result.gatewaySourceType = speedTestType;
|
|
222
293
|
|
|
223
294
|
res.json(result);
|
|
224
295
|
} catch (error) {
|
|
@@ -237,18 +308,37 @@ module.exports = (config) => {
|
|
|
237
308
|
return res.json({ results: [], message: 'Codex CLI not installed' });
|
|
238
309
|
}
|
|
239
310
|
|
|
240
|
-
const { timeout = 10000 } = req.body;
|
|
311
|
+
const { timeout = 10000, concurrency } = req.body || {};
|
|
241
312
|
const data = getChannels();
|
|
242
313
|
const channels = data.channels || [];
|
|
314
|
+
const safeConcurrency = sanitizeBatchConcurrency(concurrency);
|
|
243
315
|
|
|
244
316
|
if (channels.length === 0) {
|
|
245
317
|
return res.json({ results: [], message: '没有可测试的渠道' });
|
|
246
318
|
}
|
|
247
319
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
320
|
+
const results = await runWithConcurrencyLimit(
|
|
321
|
+
channels,
|
|
322
|
+
safeConcurrency,
|
|
323
|
+
async channel => {
|
|
324
|
+
const speedTestType = CODEX_GATEWAY_SOURCE_TYPE;
|
|
325
|
+
const result = await testChannelSpeed(channel, timeout, speedTestType);
|
|
326
|
+
result.level = getLatencyLevel(result.latency);
|
|
327
|
+
result.gatewaySourceType = speedTestType;
|
|
328
|
+
return result;
|
|
329
|
+
}
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
// 成功在前,成功结果按延迟升序
|
|
333
|
+
results.sort((a, b) => {
|
|
334
|
+
if (a.success && !b.success) return -1;
|
|
335
|
+
if (!a.success && b.success) return 1;
|
|
336
|
+
if (a.success && b.success) {
|
|
337
|
+
const aLatency = (a.latency === null || a.latency === undefined) ? Infinity : a.latency;
|
|
338
|
+
const bLatency = (b.latency === null || b.latency === undefined) ? Infinity : b.latency;
|
|
339
|
+
return aLatency - bLatency;
|
|
340
|
+
}
|
|
341
|
+
return 0;
|
|
252
342
|
});
|
|
253
343
|
|
|
254
344
|
res.json({
|
|
@@ -257,7 +347,8 @@ module.exports = (config) => {
|
|
|
257
347
|
total: results.length,
|
|
258
348
|
success: results.filter(r => r.success).length,
|
|
259
349
|
failed: results.filter(r => !r.success).length,
|
|
260
|
-
avgLatency: calculateAvgLatency(results)
|
|
350
|
+
avgLatency: calculateAvgLatency(results),
|
|
351
|
+
concurrency: safeConcurrency
|
|
261
352
|
}
|
|
262
353
|
});
|
|
263
354
|
} catch (error) {
|
|
@@ -345,7 +436,9 @@ module.exports = (config) => {
|
|
|
345
436
|
|
|
346
437
|
// 计算平均延迟
|
|
347
438
|
function calculateAvgLatency(results) {
|
|
348
|
-
const successResults = results.filter(
|
|
439
|
+
const successResults = results.filter(
|
|
440
|
+
r => r.success && r.latency !== null && r.latency !== undefined
|
|
441
|
+
);
|
|
349
442
|
if (successResults.length === 0) return null;
|
|
350
443
|
const sum = successResults.reduce((acc, r) => acc + r.latency, 0);
|
|
351
444
|
return Math.round(sum / successResults.length);
|
|
@@ -15,9 +15,9 @@ const {
|
|
|
15
15
|
} = require('../services/codex-settings-manager');
|
|
16
16
|
const { getChannels, getEnabledChannels } = require('../services/codex-channels');
|
|
17
17
|
const { clearAllLogs } = require('../websocket-server');
|
|
18
|
+
const { PATHS, ensureStorageDirMigrated } = require('../../config/paths');
|
|
18
19
|
const fs = require('fs');
|
|
19
20
|
const path = require('path');
|
|
20
|
-
const os = require('os');
|
|
21
21
|
|
|
22
22
|
function sanitizeChannel(channel) {
|
|
23
23
|
if (!channel) return null;
|
|
@@ -32,14 +32,24 @@ function sanitizeChannel(channel) {
|
|
|
32
32
|
|
|
33
33
|
// 保存激活渠道ID
|
|
34
34
|
function saveActiveChannelId(channelId) {
|
|
35
|
-
|
|
35
|
+
ensureStorageDirMigrated();
|
|
36
|
+
const filePath = PATHS.activeChannel.codex;
|
|
37
|
+
const dir = path.dirname(filePath);
|
|
36
38
|
if (!fs.existsSync(dir)) {
|
|
37
39
|
fs.mkdirSync(dir, { recursive: true });
|
|
38
40
|
}
|
|
39
|
-
const filePath = path.join(dir, 'codex-active-channel.json');
|
|
40
41
|
fs.writeFileSync(filePath, JSON.stringify({ activeChannelId: channelId }, null, 2), 'utf8');
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
function removeActiveChannelFile() {
|
|
45
|
+
ensureStorageDirMigrated();
|
|
46
|
+
const filePath = PATHS.activeChannel.codex;
|
|
47
|
+
if (fs.existsSync(filePath)) {
|
|
48
|
+
fs.unlinkSync(filePath);
|
|
49
|
+
console.log('[Codex Proxy] Removed codex-active-channel.json');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
43
53
|
// 获取代理状态
|
|
44
54
|
router.get('/status', (req, res) => {
|
|
45
55
|
try {
|
|
@@ -147,11 +157,7 @@ router.post('/stop', async (req, res) => {
|
|
|
147
157
|
console.log('[Codex Proxy] Restored settings from backup');
|
|
148
158
|
|
|
149
159
|
// 删除 active-channel.json
|
|
150
|
-
|
|
151
|
-
if (fs.existsSync(activeChannelPath)) {
|
|
152
|
-
fs.unlinkSync(activeChannelPath);
|
|
153
|
-
console.log('[Codex Proxy] Removed codex-active-channel.json');
|
|
154
|
-
}
|
|
160
|
+
removeActiveChannelFile();
|
|
155
161
|
|
|
156
162
|
const response = {
|
|
157
163
|
success: true,
|