coding-tool-x 3.2.0
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 +599 -0
- package/LICENSE +21 -0
- package/README.md +439 -0
- package/bin/ctx.js +8 -0
- package/dist/web/assets/Analytics-DN_YsnkW.js +39 -0
- package/dist/web/assets/Analytics-DuYvId7u.css +1 -0
- package/dist/web/assets/ConfigTemplates-Bidwfdf2.css +1 -0
- package/dist/web/assets/ConfigTemplates-DpXIMy0p.js +1 -0
- package/dist/web/assets/Home-38JTUlYt.js +1 -0
- package/dist/web/assets/Home-CjupSEWE.css +1 -0
- package/dist/web/assets/PluginManager-CX2tgq2H.js +1 -0
- package/dist/web/assets/PluginManager-ROyoZ-6m.css +1 -0
- package/dist/web/assets/ProjectList-C1lDcsn6.js +1 -0
- package/dist/web/assets/ProjectList-oJIyIRkP.css +1 -0
- package/dist/web/assets/SessionList-C55tjV7i.css +1 -0
- package/dist/web/assets/SessionList-CZ7T6rVx.js +1 -0
- package/dist/web/assets/SkillManager-D7pd-d_P.css +1 -0
- package/dist/web/assets/SkillManager-DLN9f79y.js +1 -0
- package/dist/web/assets/WorkspaceManager-CrwgQgmP.css +1 -0
- package/dist/web/assets/WorkspaceManager-DxlHZkpZ.js +1 -0
- package/dist/web/assets/icons-DRrXwWZi.js +1 -0
- package/dist/web/assets/index-CetESrXw.css +1 -0
- package/dist/web/assets/index-Cfvn-2Gb.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-DlpKk-8M.js +1 -0
- package/dist/web/assets/vendors-DMjSfzlv.js +7 -0
- package/dist/web/assets/vue-vendor-DET08QYg.js +45 -0
- package/dist/web/favicon.ico +0 -0
- package/dist/web/index.html +20 -0
- package/dist/web/logo.png +0 -0
- package/docs/bannel.png +0 -0
- package/docs/home.png +0 -0
- package/docs/logo.png +0 -0
- package/docs/model-redirection.md +251 -0
- package/docs/multi-channel-load-balancing.md +249 -0
- package/package.json +80 -0
- package/src/commands/channels.js +551 -0
- package/src/commands/cli-type.js +101 -0
- package/src/commands/daemon.js +365 -0
- package/src/commands/doctor.js +333 -0
- package/src/commands/export-config.js +205 -0
- package/src/commands/list.js +222 -0
- package/src/commands/logs.js +261 -0
- package/src/commands/plugin.js +585 -0
- package/src/commands/port-config.js +135 -0
- package/src/commands/proxy-control.js +264 -0
- package/src/commands/proxy.js +152 -0
- package/src/commands/resume.js +137 -0
- package/src/commands/search.js +190 -0
- package/src/commands/security.js +37 -0
- package/src/commands/stats.js +398 -0
- package/src/commands/switch.js +48 -0
- package/src/commands/toggle-proxy.js +247 -0
- package/src/commands/ui.js +99 -0
- package/src/commands/update.js +97 -0
- package/src/commands/workspace.js +454 -0
- package/src/config/default.js +69 -0
- package/src/config/loader.js +149 -0
- package/src/config/model-metadata.js +167 -0
- package/src/config/model-metadata.json +125 -0
- package/src/config/model-pricing.js +35 -0
- package/src/config/paths.js +190 -0
- package/src/index.js +680 -0
- package/src/plugins/constants.js +15 -0
- package/src/plugins/event-bus.js +54 -0
- package/src/plugins/manifest-validator.js +129 -0
- package/src/plugins/plugin-api.js +128 -0
- package/src/plugins/plugin-installer.js +601 -0
- package/src/plugins/plugin-loader.js +229 -0
- package/src/plugins/plugin-manager.js +170 -0
- package/src/plugins/registry.js +152 -0
- package/src/plugins/schema/plugin-manifest.json +115 -0
- package/src/reset-config.js +94 -0
- package/src/server/api/agents.js +826 -0
- package/src/server/api/aliases.js +36 -0
- package/src/server/api/channels.js +368 -0
- package/src/server/api/claude-hooks.js +480 -0
- package/src/server/api/codex-channels.js +417 -0
- package/src/server/api/codex-projects.js +104 -0
- package/src/server/api/codex-proxy.js +195 -0
- package/src/server/api/codex-sessions.js +483 -0
- package/src/server/api/codex-statistics.js +57 -0
- package/src/server/api/commands.js +482 -0
- package/src/server/api/config-export.js +212 -0
- package/src/server/api/config-registry.js +357 -0
- package/src/server/api/config-sync.js +155 -0
- package/src/server/api/config-templates.js +248 -0
- package/src/server/api/config.js +521 -0
- package/src/server/api/convert.js +260 -0
- package/src/server/api/dashboard.js +142 -0
- package/src/server/api/env.js +144 -0
- package/src/server/api/favorites.js +77 -0
- package/src/server/api/gemini-channels.js +366 -0
- package/src/server/api/gemini-projects.js +91 -0
- package/src/server/api/gemini-proxy.js +173 -0
- package/src/server/api/gemini-sessions.js +376 -0
- package/src/server/api/gemini-statistics.js +57 -0
- package/src/server/api/health-check.js +31 -0
- package/src/server/api/mcp.js +399 -0
- package/src/server/api/opencode-channels.js +419 -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 +327 -0
- package/src/server/api/opencode-statistics.js +57 -0
- package/src/server/api/plugins.js +463 -0
- package/src/server/api/pm2-autostart.js +269 -0
- package/src/server/api/projects.js +124 -0
- package/src/server/api/prompts.js +279 -0
- package/src/server/api/proxy.js +306 -0
- package/src/server/api/security.js +53 -0
- package/src/server/api/sessions.js +514 -0
- package/src/server/api/settings.js +142 -0
- package/src/server/api/skills.js +570 -0
- package/src/server/api/statistics.js +238 -0
- package/src/server/api/ui-config.js +64 -0
- package/src/server/api/workspaces.js +456 -0
- package/src/server/codex-proxy-server.js +681 -0
- package/src/server/dev-server.js +26 -0
- package/src/server/gemini-proxy-server.js +610 -0
- package/src/server/index.js +422 -0
- package/src/server/opencode-proxy-server.js +4771 -0
- package/src/server/proxy-server.js +669 -0
- package/src/server/services/agents-service.js +1137 -0
- package/src/server/services/alias.js +71 -0
- package/src/server/services/channel-health.js +234 -0
- package/src/server/services/channel-scheduler.js +240 -0
- package/src/server/services/channels.js +447 -0
- package/src/server/services/codex-channels.js +705 -0
- package/src/server/services/codex-config.js +90 -0
- package/src/server/services/codex-parser.js +322 -0
- package/src/server/services/codex-sessions.js +936 -0
- package/src/server/services/codex-settings-manager.js +619 -0
- package/src/server/services/codex-speed-test-template.json +24 -0
- package/src/server/services/codex-statistics-service.js +161 -0
- package/src/server/services/commands-service.js +574 -0
- package/src/server/services/config-export-service.js +1165 -0
- package/src/server/services/config-registry-service.js +828 -0
- package/src/server/services/config-sync-manager.js +941 -0
- package/src/server/services/config-sync-service.js +504 -0
- package/src/server/services/config-templates-service.js +913 -0
- package/src/server/services/enhanced-cache.js +196 -0
- package/src/server/services/env-checker.js +409 -0
- package/src/server/services/env-manager.js +436 -0
- package/src/server/services/favorites.js +165 -0
- package/src/server/services/format-converter.js +620 -0
- package/src/server/services/gemini-channels.js +459 -0
- package/src/server/services/gemini-config.js +73 -0
- package/src/server/services/gemini-sessions.js +689 -0
- package/src/server/services/gemini-settings-manager.js +263 -0
- package/src/server/services/gemini-statistics-service.js +157 -0
- package/src/server/services/health-check.js +85 -0
- package/src/server/services/mcp-client.js +790 -0
- package/src/server/services/mcp-service.js +1732 -0
- package/src/server/services/model-detector.js +1245 -0
- package/src/server/services/network-access.js +80 -0
- package/src/server/services/opencode-channels.js +366 -0
- package/src/server/services/opencode-gateway-adapters.js +1168 -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 +161 -0
- package/src/server/services/plugins-service.js +1268 -0
- package/src/server/services/prompts-service.js +534 -0
- package/src/server/services/proxy-runtime.js +79 -0
- package/src/server/services/repo-scanner-base.js +708 -0
- package/src/server/services/request-logger.js +130 -0
- package/src/server/services/response-decoder.js +21 -0
- package/src/server/services/security-config.js +131 -0
- package/src/server/services/session-cache.js +127 -0
- package/src/server/services/session-converter.js +577 -0
- package/src/server/services/sessions.js +900 -0
- package/src/server/services/settings-manager.js +163 -0
- package/src/server/services/skill-service.js +1482 -0
- package/src/server/services/speed-test.js +1146 -0
- package/src/server/services/statistics-service.js +1043 -0
- package/src/server/services/ui-config.js +132 -0
- package/src/server/services/workspace-service.js +830 -0
- package/src/server/utils/pricing.js +73 -0
- package/src/server/websocket-server.js +513 -0
- package/src/ui/menu.js +139 -0
- package/src/ui/prompts.js +100 -0
- package/src/utils/format.js +43 -0
- package/src/utils/port-helper.js +108 -0
- package/src/utils/session.js +240 -0
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const {
|
|
4
|
+
getChannels,
|
|
5
|
+
createChannel,
|
|
6
|
+
updateChannel,
|
|
7
|
+
deleteChannel,
|
|
8
|
+
getEnabledChannels,
|
|
9
|
+
saveChannelOrder,
|
|
10
|
+
applyChannelToSettings
|
|
11
|
+
} = require('../services/codex-channels');
|
|
12
|
+
const { getSchedulerState } = require('../services/channel-scheduler');
|
|
13
|
+
const { getChannelHealthStatus, resetChannelHealth } = require('../services/channel-health');
|
|
14
|
+
const { broadcastSchedulerState, broadcastLog } = require('../websocket-server');
|
|
15
|
+
const { isCodexInstalled } = require('../services/codex-config');
|
|
16
|
+
const {
|
|
17
|
+
testChannelSpeed,
|
|
18
|
+
getLatencyLevel,
|
|
19
|
+
sanitizeBatchConcurrency,
|
|
20
|
+
runWithConcurrencyLimit
|
|
21
|
+
} = require('../services/speed-test');
|
|
22
|
+
const { clearCodexRedirectCache } = require('../codex-proxy-server');
|
|
23
|
+
const { getDefaultSpeedTestModelByToolType } = require('../../config/model-metadata');
|
|
24
|
+
const CODEX_GATEWAY_SOURCE_TYPE = 'codex';
|
|
25
|
+
|
|
26
|
+
function getDefaultCodexModel() {
|
|
27
|
+
return getDefaultSpeedTestModelByToolType('codex');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = (config) => {
|
|
31
|
+
/**
|
|
32
|
+
* GET /api/codex/channels
|
|
33
|
+
* 获取所有 Codex 渠道(包含健康状态)
|
|
34
|
+
*/
|
|
35
|
+
router.get('/', (req, res) => {
|
|
36
|
+
try {
|
|
37
|
+
if (!isCodexInstalled()) {
|
|
38
|
+
return res.json({
|
|
39
|
+
channels: [],
|
|
40
|
+
error: 'Codex CLI not installed'
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const data = getChannels();
|
|
45
|
+
// 为每个渠道添加健康状态
|
|
46
|
+
const channelsWithHealth = (data.channels || []).map(ch => ({
|
|
47
|
+
...ch,
|
|
48
|
+
health: getChannelHealthStatus(ch.id, 'codex')
|
|
49
|
+
}));
|
|
50
|
+
res.json({ channels: channelsWithHealth });
|
|
51
|
+
} catch (err) {
|
|
52
|
+
console.error('[Codex Channels API] Failed to get channels:', err);
|
|
53
|
+
res.status(500).json({ error: err.message });
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* GET /api/codex/channels/:id/models
|
|
59
|
+
* 获取渠道可用模型列表
|
|
60
|
+
*/
|
|
61
|
+
router.get('/:id/models', async (req, res) => {
|
|
62
|
+
try {
|
|
63
|
+
const { id } = req.params;
|
|
64
|
+
const channels = getChannels().channels || [];
|
|
65
|
+
const channel = channels.find(ch => ch.id === id);
|
|
66
|
+
|
|
67
|
+
if (!channel) {
|
|
68
|
+
return res.status(404).json({ error: '渠道不存在' });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const gatewaySourceType = CODEX_GATEWAY_SOURCE_TYPE;
|
|
72
|
+
const models = [getDefaultCodexModel()];
|
|
73
|
+
const result = {
|
|
74
|
+
models,
|
|
75
|
+
supported: models.length > 0,
|
|
76
|
+
cached: false,
|
|
77
|
+
fallbackUsed: false,
|
|
78
|
+
lastChecked: new Date().toISOString(),
|
|
79
|
+
error: models.length > 0 ? null : '未配置默认模型列表',
|
|
80
|
+
errorHint: models.length > 0 ? null : '请在设置中配置 Codex 默认模型'
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
res.json({
|
|
84
|
+
channelId: id,
|
|
85
|
+
gatewaySourceType,
|
|
86
|
+
models: result.models,
|
|
87
|
+
supported: result.supported,
|
|
88
|
+
cached: result.cached,
|
|
89
|
+
fallbackUsed: result.fallbackUsed,
|
|
90
|
+
fetchedAt: result.lastChecked || new Date().toISOString(),
|
|
91
|
+
error: result.error,
|
|
92
|
+
errorHint: result.errorHint
|
|
93
|
+
});
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.error('[Codex Channels API] Error fetching models:', error);
|
|
96
|
+
res.status(500).json({
|
|
97
|
+
error: '获取模型列表失败',
|
|
98
|
+
channelId: req.params.id
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* POST /api/codex/channels
|
|
105
|
+
* 创建新渠道
|
|
106
|
+
* Body: { name, providerKey, baseUrl, apiKey, websiteUrl }
|
|
107
|
+
*/
|
|
108
|
+
router.post('/', (req, res) => {
|
|
109
|
+
try {
|
|
110
|
+
if (!isCodexInstalled()) {
|
|
111
|
+
return res.status(404).json({ error: 'Codex CLI not installed' });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const {
|
|
115
|
+
name,
|
|
116
|
+
providerKey,
|
|
117
|
+
baseUrl,
|
|
118
|
+
apiKey,
|
|
119
|
+
websiteUrl,
|
|
120
|
+
enabled,
|
|
121
|
+
weight,
|
|
122
|
+
maxConcurrency,
|
|
123
|
+
modelRedirects,
|
|
124
|
+
speedTestModel,
|
|
125
|
+
presetId,
|
|
126
|
+
gatewaySourceType
|
|
127
|
+
} = req.body;
|
|
128
|
+
|
|
129
|
+
if (!name || !providerKey || !baseUrl) {
|
|
130
|
+
return res.status(400).json({ error: 'Missing required fields: name, providerKey, baseUrl' });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (!apiKey) {
|
|
134
|
+
return res.status(400).json({ error: 'Missing required fields: apiKey' });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// wireApi 固定为 'responses' (OpenAI Responses API 格式)
|
|
138
|
+
const channel = createChannel(name, providerKey, baseUrl, apiKey, 'responses', {
|
|
139
|
+
websiteUrl,
|
|
140
|
+
enabled,
|
|
141
|
+
weight,
|
|
142
|
+
maxConcurrency,
|
|
143
|
+
modelRedirects: modelRedirects || [],
|
|
144
|
+
speedTestModel: speedTestModel || null,
|
|
145
|
+
presetId: presetId || null,
|
|
146
|
+
gatewaySourceType
|
|
147
|
+
});
|
|
148
|
+
res.json(channel);
|
|
149
|
+
broadcastSchedulerState('codex', getSchedulerState('codex'));
|
|
150
|
+
} catch (err) {
|
|
151
|
+
console.error('[Codex Channels API] Failed to create channel:', err);
|
|
152
|
+
res.status(500).json({ error: err.message });
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* PUT /api/codex/channels/:channelId
|
|
158
|
+
* 更新渠道
|
|
159
|
+
*/
|
|
160
|
+
router.put('/:channelId', (req, res) => {
|
|
161
|
+
try {
|
|
162
|
+
if (!isCodexInstalled()) {
|
|
163
|
+
return res.status(404).json({ error: 'Codex CLI not installed' });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const { channelId } = req.params;
|
|
167
|
+
const updates = req.body;
|
|
168
|
+
|
|
169
|
+
const channel = updateChannel(channelId, updates);
|
|
170
|
+
// 清除该渠道的模型重定向日志缓存,使下次请求时重新打印
|
|
171
|
+
clearCodexRedirectCache(channelId);
|
|
172
|
+
res.json(channel);
|
|
173
|
+
broadcastSchedulerState('codex', getSchedulerState('codex'));
|
|
174
|
+
} catch (err) {
|
|
175
|
+
console.error('[Codex Channels API] Failed to update channel:', err);
|
|
176
|
+
res.status(500).json({ error: err.message });
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* DELETE /api/codex/channels/:channelId
|
|
182
|
+
* 删除渠道
|
|
183
|
+
*/
|
|
184
|
+
router.delete('/:channelId', async (req, res) => {
|
|
185
|
+
try {
|
|
186
|
+
if (!isCodexInstalled()) {
|
|
187
|
+
return res.status(404).json({ error: 'Codex CLI not installed' });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const { channelId } = req.params;
|
|
191
|
+
const result = await deleteChannel(channelId);
|
|
192
|
+
res.json(result);
|
|
193
|
+
broadcastSchedulerState('codex', getSchedulerState('codex'));
|
|
194
|
+
} catch (err) {
|
|
195
|
+
console.error('[Codex Channels API] Failed to delete channel:', err);
|
|
196
|
+
res.status(500).json({ error: err.message });
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* POST /api/codex/channels/order
|
|
202
|
+
* 保存渠道顺序
|
|
203
|
+
*/
|
|
204
|
+
router.post('/order', (req, res) => {
|
|
205
|
+
try {
|
|
206
|
+
if (!isCodexInstalled()) {
|
|
207
|
+
return res.status(404).json({ error: 'Codex CLI not installed' });
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const { order } = req.body;
|
|
211
|
+
|
|
212
|
+
if (!Array.isArray(order)) {
|
|
213
|
+
return res.status(400).json({ error: 'order must be an array' });
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
saveChannelOrder(order);
|
|
217
|
+
res.json({ success: true });
|
|
218
|
+
} catch (err) {
|
|
219
|
+
console.error('[Codex Channels API] Failed to save channel order:', err);
|
|
220
|
+
res.status(500).json({ error: err.message });
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* GET /api/codex/channels/enabled
|
|
226
|
+
* 获取所有启用的渠道(供调度器使用)
|
|
227
|
+
*/
|
|
228
|
+
router.get('/enabled', (req, res) => {
|
|
229
|
+
try {
|
|
230
|
+
if (!isCodexInstalled()) {
|
|
231
|
+
return res.json({ channels: [] });
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const channels = getEnabledChannels();
|
|
235
|
+
res.json({ channels });
|
|
236
|
+
} catch (err) {
|
|
237
|
+
console.error('[Codex Channels API] Failed to get enabled channels:', err);
|
|
238
|
+
res.status(500).json({ error: err.message });
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* POST /api/codex/channels/:channelId/speed-test
|
|
244
|
+
* 测试单个渠道速度
|
|
245
|
+
*/
|
|
246
|
+
router.post('/:channelId/speed-test', async (req, res) => {
|
|
247
|
+
try {
|
|
248
|
+
if (!isCodexInstalled()) {
|
|
249
|
+
return res.status(404).json({ error: 'Codex CLI not installed' });
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const { channelId } = req.params;
|
|
253
|
+
const { timeout = 10000 } = req.body;
|
|
254
|
+
const data = getChannels();
|
|
255
|
+
const channel = data.channels.find(ch => ch.id === channelId);
|
|
256
|
+
|
|
257
|
+
if (!channel) {
|
|
258
|
+
return res.status(404).json({ error: '渠道不存在' });
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const speedTestType = CODEX_GATEWAY_SOURCE_TYPE;
|
|
262
|
+
const result = await testChannelSpeed(channel, timeout, speedTestType);
|
|
263
|
+
result.level = getLatencyLevel(result.latency);
|
|
264
|
+
result.gatewaySourceType = speedTestType;
|
|
265
|
+
|
|
266
|
+
res.json(result);
|
|
267
|
+
} catch (error) {
|
|
268
|
+
console.error('[Codex Channels API] Error testing channel speed:', error);
|
|
269
|
+
res.status(500).json({ error: error.message });
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* POST /api/codex/channels/speed-test-all
|
|
275
|
+
* 测试所有渠道速度
|
|
276
|
+
*/
|
|
277
|
+
router.post('/speed-test-all', async (req, res) => {
|
|
278
|
+
try {
|
|
279
|
+
if (!isCodexInstalled()) {
|
|
280
|
+
return res.json({ results: [], message: 'Codex CLI not installed' });
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const { timeout = 10000, concurrency } = req.body || {};
|
|
284
|
+
const data = getChannels();
|
|
285
|
+
const channels = data.channels || [];
|
|
286
|
+
const safeConcurrency = sanitizeBatchConcurrency(concurrency);
|
|
287
|
+
|
|
288
|
+
if (channels.length === 0) {
|
|
289
|
+
return res.json({ results: [], message: '没有可测试的渠道' });
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const results = await runWithConcurrencyLimit(
|
|
293
|
+
channels,
|
|
294
|
+
safeConcurrency,
|
|
295
|
+
async channel => {
|
|
296
|
+
const speedTestType = CODEX_GATEWAY_SOURCE_TYPE;
|
|
297
|
+
const result = await testChannelSpeed(channel, timeout, speedTestType);
|
|
298
|
+
result.level = getLatencyLevel(result.latency);
|
|
299
|
+
result.gatewaySourceType = speedTestType;
|
|
300
|
+
return result;
|
|
301
|
+
}
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
// 成功在前,成功结果按延迟升序
|
|
305
|
+
results.sort((a, b) => {
|
|
306
|
+
if (a.success && !b.success) return -1;
|
|
307
|
+
if (!a.success && b.success) return 1;
|
|
308
|
+
if (a.success && b.success) {
|
|
309
|
+
const aLatency = (a.latency === null || a.latency === undefined) ? Infinity : a.latency;
|
|
310
|
+
const bLatency = (b.latency === null || b.latency === undefined) ? Infinity : b.latency;
|
|
311
|
+
return aLatency - bLatency;
|
|
312
|
+
}
|
|
313
|
+
return 0;
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
res.json({
|
|
317
|
+
results,
|
|
318
|
+
summary: {
|
|
319
|
+
total: results.length,
|
|
320
|
+
success: results.filter(r => r.success).length,
|
|
321
|
+
failed: results.filter(r => !r.success).length,
|
|
322
|
+
avgLatency: calculateAvgLatency(results),
|
|
323
|
+
concurrency: safeConcurrency
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
} catch (error) {
|
|
327
|
+
console.error('[Codex Channels API] Error testing all channels speed:', error);
|
|
328
|
+
res.status(500).json({ error: error.message });
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* POST /api/codex/channels/:channelId/apply-to-settings
|
|
334
|
+
* 将渠道应用到 Codex 配置文件
|
|
335
|
+
*/
|
|
336
|
+
router.post('/:channelId/apply-to-settings', async (req, res) => {
|
|
337
|
+
try {
|
|
338
|
+
if (!isCodexInstalled()) {
|
|
339
|
+
return res.status(404).json({ error: 'Codex CLI not installed' });
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const { channelId } = req.params;
|
|
343
|
+
const channel = applyChannelToSettings(channelId);
|
|
344
|
+
|
|
345
|
+
// 如果代理正在运行,停止它
|
|
346
|
+
const { getCodexProxyStatus, stopCodexProxyServer } = require('../codex-proxy-server');
|
|
347
|
+
const proxyStatus = getCodexProxyStatus();
|
|
348
|
+
|
|
349
|
+
if (proxyStatus && proxyStatus.running) {
|
|
350
|
+
console.log(`Codex proxy is running, stopping to apply channel settings: ${channel.name}`);
|
|
351
|
+
await stopCodexProxyServer({ clearStartTime: false });
|
|
352
|
+
|
|
353
|
+
broadcastLog({
|
|
354
|
+
type: 'action',
|
|
355
|
+
action: 'stop_proxy',
|
|
356
|
+
message: `已停止动态切换,默认使用当前渠道`,
|
|
357
|
+
timestamp: Date.now(),
|
|
358
|
+
source: 'codex'
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
broadcastLog({
|
|
363
|
+
type: 'action',
|
|
364
|
+
action: 'apply_settings',
|
|
365
|
+
message: `已将 (${channel.name}) 渠道写入配置文件中`,
|
|
366
|
+
channelName: channel.name,
|
|
367
|
+
timestamp: Date.now(),
|
|
368
|
+
source: 'codex'
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
res.json({
|
|
372
|
+
message: `已将 (${channel.name}) 渠道写入配置文件中`,
|
|
373
|
+
channel
|
|
374
|
+
});
|
|
375
|
+
} catch (error) {
|
|
376
|
+
console.error('[Codex Channels API] Error applying channel to settings:', error);
|
|
377
|
+
res.status(500).json({ error: error.message });
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* POST /api/codex/channels/:channelId/reset-health
|
|
383
|
+
* 重置渠道健康状态
|
|
384
|
+
*/
|
|
385
|
+
router.post('/:channelId/reset-health', (req, res) => {
|
|
386
|
+
try {
|
|
387
|
+
if (!isCodexInstalled()) {
|
|
388
|
+
return res.status(404).json({ error: 'Codex CLI not installed' });
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const { channelId } = req.params;
|
|
392
|
+
resetChannelHealth(channelId, 'codex');
|
|
393
|
+
broadcastSchedulerState('codex', getSchedulerState('codex'));
|
|
394
|
+
|
|
395
|
+
res.json({
|
|
396
|
+
success: true,
|
|
397
|
+
message: '渠道健康状态已重置',
|
|
398
|
+
health: getChannelHealthStatus(channelId, 'codex')
|
|
399
|
+
});
|
|
400
|
+
} catch (error) {
|
|
401
|
+
console.error('[Codex Channels API] Error resetting channel health:', error);
|
|
402
|
+
res.status(500).json({ error: error.message });
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
return router;
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
// 计算平均延迟
|
|
410
|
+
function calculateAvgLatency(results) {
|
|
411
|
+
const successResults = results.filter(
|
|
412
|
+
r => r.success && r.latency !== null && r.latency !== undefined
|
|
413
|
+
);
|
|
414
|
+
if (successResults.length === 0) return null;
|
|
415
|
+
const sum = successResults.reduce((acc, r) => acc + r.latency, 0);
|
|
416
|
+
return Math.round(sum / successResults.length);
|
|
417
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const { getProjects, saveProjectOrder, deleteProject } = require('../services/codex-sessions');
|
|
4
|
+
const { isCodexInstalled } = require('../services/codex-config');
|
|
5
|
+
|
|
6
|
+
const DEBUG_CODEX_PERF = process.env.DEBUG_CODEX_PERF === '1';
|
|
7
|
+
|
|
8
|
+
function logPerf(route, startMs, detail = '') {
|
|
9
|
+
if (!DEBUG_CODEX_PERF) return;
|
|
10
|
+
const duration = Date.now() - startMs;
|
|
11
|
+
const suffix = detail ? ` | ${detail}` : '';
|
|
12
|
+
console.log(`[Codex Perf] ${route}: ${duration}ms${suffix}`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
module.exports = (config) => {
|
|
16
|
+
/**
|
|
17
|
+
* GET /api/codex/projects
|
|
18
|
+
* 获取所有 Codex 项目列表
|
|
19
|
+
*/
|
|
20
|
+
router.get('/', (req, res) => {
|
|
21
|
+
const startMs = Date.now();
|
|
22
|
+
try {
|
|
23
|
+
// 检查 Codex 是否安装
|
|
24
|
+
if (!isCodexInstalled()) {
|
|
25
|
+
logPerf('GET /api/codex/projects', startMs, 'codex not installed');
|
|
26
|
+
return res.json({
|
|
27
|
+
projects: [],
|
|
28
|
+
currentProject: null,
|
|
29
|
+
error: 'Codex CLI not installed or not found'
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const projects = getProjects();
|
|
34
|
+
logPerf('GET /api/codex/projects', startMs, `projects=${projects.length}`);
|
|
35
|
+
|
|
36
|
+
res.json({
|
|
37
|
+
projects,
|
|
38
|
+
currentProject: projects[0] ? projects[0].name : null
|
|
39
|
+
});
|
|
40
|
+
} catch (err) {
|
|
41
|
+
console.error('[Codex API] Failed to get projects:', err);
|
|
42
|
+
logPerf('GET /api/codex/projects', startMs, `error=${err.message}`);
|
|
43
|
+
|
|
44
|
+
if (err.code === 'ENOENT') {
|
|
45
|
+
return res.status(404).json({
|
|
46
|
+
error: 'Codex sessions directory not found',
|
|
47
|
+
projects: []
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
res.status(500).json({
|
|
52
|
+
error: err.message,
|
|
53
|
+
projects: []
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* POST /api/codex/projects/order
|
|
60
|
+
* 保存项目排序
|
|
61
|
+
*/
|
|
62
|
+
router.post('/order', (req, res) => {
|
|
63
|
+
try {
|
|
64
|
+
if (!isCodexInstalled()) {
|
|
65
|
+
return res.status(404).json({ error: 'Codex CLI not installed' });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const { order } = req.body;
|
|
69
|
+
|
|
70
|
+
if (!Array.isArray(order)) {
|
|
71
|
+
return res.status(400).json({ error: 'order must be an array' });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
saveProjectOrder(order);
|
|
75
|
+
|
|
76
|
+
res.json({ success: true });
|
|
77
|
+
} catch (err) {
|
|
78
|
+
console.error('[Codex API] Failed to save project order:', err);
|
|
79
|
+
res.status(500).json({ error: err.message });
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* DELETE /api/codex/projects/:projectName
|
|
85
|
+
* 删除项目(删除项目下所有会话)
|
|
86
|
+
*/
|
|
87
|
+
router.delete('/:projectName', (req, res) => {
|
|
88
|
+
try {
|
|
89
|
+
if (!isCodexInstalled()) {
|
|
90
|
+
return res.status(404).json({ error: 'Codex CLI not installed' });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const { projectName } = req.params;
|
|
94
|
+
const result = deleteProject(projectName);
|
|
95
|
+
|
|
96
|
+
res.json(result);
|
|
97
|
+
} catch (err) {
|
|
98
|
+
console.error('[Codex API] Failed to delete project:', err);
|
|
99
|
+
res.status(500).json({ error: err.message });
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
return router;
|
|
104
|
+
};
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const {
|
|
4
|
+
startCodexProxyServer,
|
|
5
|
+
stopCodexProxyServer,
|
|
6
|
+
getCodexProxyStatus
|
|
7
|
+
} = require('../codex-proxy-server');
|
|
8
|
+
const {
|
|
9
|
+
setProxyConfig,
|
|
10
|
+
restoreSettings,
|
|
11
|
+
isProxyConfig,
|
|
12
|
+
getCurrentProxyPort,
|
|
13
|
+
configExists,
|
|
14
|
+
hasBackup
|
|
15
|
+
} = require('../services/codex-settings-manager');
|
|
16
|
+
const { getChannels, getEnabledChannels } = require('../services/codex-channels');
|
|
17
|
+
const { clearAllLogs } = require('../websocket-server');
|
|
18
|
+
const { PATHS, ensureStorageDirMigrated } = require('../../config/paths');
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
|
|
22
|
+
function sanitizeChannel(channel) {
|
|
23
|
+
if (!channel) return null;
|
|
24
|
+
return {
|
|
25
|
+
id: channel.id,
|
|
26
|
+
name: channel.name,
|
|
27
|
+
baseUrl: channel.baseUrl,
|
|
28
|
+
websiteUrl: channel.websiteUrl,
|
|
29
|
+
providerKey: channel.providerKey
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 保存激活渠道ID
|
|
34
|
+
function saveActiveChannelId(channelId) {
|
|
35
|
+
ensureStorageDirMigrated();
|
|
36
|
+
const filePath = PATHS.activeChannel.codex;
|
|
37
|
+
const dir = path.dirname(filePath);
|
|
38
|
+
if (!fs.existsSync(dir)) {
|
|
39
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
40
|
+
}
|
|
41
|
+
fs.writeFileSync(filePath, JSON.stringify({ activeChannelId: channelId }, null, 2), 'utf8');
|
|
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
|
+
|
|
53
|
+
// 获取代理状态
|
|
54
|
+
router.get('/status', (req, res) => {
|
|
55
|
+
try {
|
|
56
|
+
const proxyStatus = getCodexProxyStatus();
|
|
57
|
+
const { channels } = getChannels();
|
|
58
|
+
const enabledChannels = channels.filter(ch => ch.enabled !== false);
|
|
59
|
+
const activeChannel = enabledChannels[0]; // 多渠道模式:第一个启用的渠道
|
|
60
|
+
const configStatus = {
|
|
61
|
+
isProxyConfig: isProxyConfig(),
|
|
62
|
+
configExists: configExists(),
|
|
63
|
+
hasBackup: hasBackup(),
|
|
64
|
+
currentProxyPort: getCurrentProxyPort()
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
res.json({
|
|
68
|
+
proxy: proxyStatus,
|
|
69
|
+
config: configStatus,
|
|
70
|
+
activeChannel: sanitizeChannel(activeChannel)
|
|
71
|
+
});
|
|
72
|
+
} catch (error) {
|
|
73
|
+
res.status(500).json({ error: error.message });
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// 启动代理
|
|
78
|
+
router.post('/start', async (req, res) => {
|
|
79
|
+
try {
|
|
80
|
+
// 1. 检查 Codex 配置文件是否存在
|
|
81
|
+
if (!configExists()) {
|
|
82
|
+
return res.status(400).json({
|
|
83
|
+
error: 'Codex config.toml not found. Please run Codex CLI at least once.'
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 2. 获取当前启用的渠道(多渠道模式)
|
|
88
|
+
const enabledChannels = getEnabledChannels();
|
|
89
|
+
const currentChannel = enabledChannels[0];
|
|
90
|
+
if (!currentChannel) {
|
|
91
|
+
return res.status(400).json({
|
|
92
|
+
error: 'No enabled Codex channel found. Please create and enable a channel first.'
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 3. 保存当前激活渠道ID(用于代理模式)
|
|
97
|
+
saveActiveChannelId(currentChannel.id);
|
|
98
|
+
console.log(`[Codex Proxy] Saved active channel: ${currentChannel.name} (${currentChannel.id})`);
|
|
99
|
+
|
|
100
|
+
// 4. 启动代理服务器
|
|
101
|
+
const proxyResult = await startCodexProxyServer();
|
|
102
|
+
|
|
103
|
+
if (!proxyResult.success) {
|
|
104
|
+
return res.status(500).json({ error: 'Failed to start Codex proxy server' });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 5. 设置代理配置(备份并修改 config.toml 和 auth.json)
|
|
108
|
+
const configResult = setProxyConfig(proxyResult.port);
|
|
109
|
+
|
|
110
|
+
const updatedStatus = getCodexProxyStatus();
|
|
111
|
+
const { channels: allChannels } = getChannels();
|
|
112
|
+
const activeChannel = allChannels.filter(ch => ch.enabled !== false)[0];
|
|
113
|
+
const { broadcastProxyState } = require('../websocket-server');
|
|
114
|
+
broadcastProxyState('codex', updatedStatus, activeChannel, allChannels);
|
|
115
|
+
|
|
116
|
+
// 构建响应消息,包含环境变量提示(仅首次)
|
|
117
|
+
let message = `Codex proxy started on port ${proxyResult.port}, active channel: ${currentChannel.name}`;
|
|
118
|
+
let envHint = null;
|
|
119
|
+
|
|
120
|
+
// 只有首次注入环境变量时才提示用户执行 source 命令
|
|
121
|
+
if (configResult.envInjected && configResult.isFirstTime) {
|
|
122
|
+
envHint = {
|
|
123
|
+
command: configResult.sourceCommand,
|
|
124
|
+
message: `首次启用需在 Codex 终端执行: ${configResult.sourceCommand}`
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
res.json({
|
|
129
|
+
success: true,
|
|
130
|
+
port: proxyResult.port,
|
|
131
|
+
activeChannel: sanitizeChannel(currentChannel),
|
|
132
|
+
message,
|
|
133
|
+
envHint
|
|
134
|
+
});
|
|
135
|
+
} catch (error) {
|
|
136
|
+
console.error('[Codex Proxy] Error starting proxy:', error);
|
|
137
|
+
res.status(500).json({ error: error.message });
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// 停止代理
|
|
142
|
+
router.post('/stop', async (req, res) => {
|
|
143
|
+
try {
|
|
144
|
+
// 1. 获取当前启用的渠道(多渠道模式)
|
|
145
|
+
const { channels } = getChannels();
|
|
146
|
+
const enabledChannels = channels.filter(ch => ch.enabled !== false);
|
|
147
|
+
const activeChannel = enabledChannels[0];
|
|
148
|
+
|
|
149
|
+
// 2. 停止代理服务器
|
|
150
|
+
const proxyResult = await stopCodexProxyServer();
|
|
151
|
+
|
|
152
|
+
// 3. 恢复原始配置
|
|
153
|
+
const { broadcastProxyState } = require('../websocket-server');
|
|
154
|
+
|
|
155
|
+
if (hasBackup()) {
|
|
156
|
+
restoreSettings();
|
|
157
|
+
console.log('[Codex Proxy] Restored settings from backup');
|
|
158
|
+
|
|
159
|
+
// Enforce single-channel mode: apply the active channel and disable all others
|
|
160
|
+
if (activeChannel) {
|
|
161
|
+
const { applyChannelToSettings } = require('../services/codex-channels');
|
|
162
|
+
applyChannelToSettings(activeChannel.id);
|
|
163
|
+
console.log(`[Codex Proxy] Single-channel mode enforced: ${activeChannel.name}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 删除 active-channel.json
|
|
167
|
+
removeActiveChannelFile();
|
|
168
|
+
|
|
169
|
+
const response = {
|
|
170
|
+
success: true,
|
|
171
|
+
message: `Codex proxy stopped, settings restored${activeChannel ? ' (channel: ' + activeChannel.name + ')' : ''}`,
|
|
172
|
+
port: proxyResult.port,
|
|
173
|
+
restoredChannel: activeChannel?.name
|
|
174
|
+
};
|
|
175
|
+
res.json(response);
|
|
176
|
+
|
|
177
|
+
const updatedStatus = getCodexProxyStatus();
|
|
178
|
+
broadcastProxyState('codex', updatedStatus, activeChannel, channels);
|
|
179
|
+
} else {
|
|
180
|
+
res.json({
|
|
181
|
+
success: true,
|
|
182
|
+
message: 'Codex proxy stopped (no backup to restore)',
|
|
183
|
+
port: proxyResult.port
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const updatedStatus = getCodexProxyStatus();
|
|
187
|
+
broadcastProxyState('codex', updatedStatus, activeChannel, channels);
|
|
188
|
+
}
|
|
189
|
+
} catch (error) {
|
|
190
|
+
console.error('[Codex Proxy] Error stopping proxy:', error);
|
|
191
|
+
res.status(500).json({ error: error.message });
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
module.exports = router;
|