coding-tool-x 3.3.7 → 3.3.9
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 +20 -0
- package/README.md +253 -326
- package/dist/web/assets/{Analytics-IW6eAy9u.js → Analytics-D6LzK9hk.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-BPtkTMSc.js → ConfigTemplates-BUDYuxRi.js} +1 -1
- package/dist/web/assets/Home-BQxQ1LhR.css +1 -0
- package/dist/web/assets/Home-D7KX7iF8.js +1 -0
- package/dist/web/assets/{PluginManager-BGx9MSDV.js → PluginManager-DTgQ--vB.js} +1 -1
- package/dist/web/assets/{ProjectList-BCn-mrCx.js → ProjectList-DMCiGmCT.js} +1 -1
- package/dist/web/assets/{SessionList-CzLfebJQ.js → SessionList-CRBsdVRe.js} +1 -1
- package/dist/web/assets/{SkillManager-CXz2vBQx.js → SkillManager-DMwx2Q4k.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-CHtgMfKc.js → WorkspaceManager-DapB4ljL.js} +1 -1
- package/dist/web/assets/{icons-B29onFfZ.js → icons-B5Pl4lrD.js} +1 -1
- package/dist/web/assets/index-CL-qpoJ_.js +2 -0
- package/dist/web/assets/index-D_5dRFOL.css +1 -0
- package/dist/web/assets/{markdown-C9MYpaSi.js → markdown-DyTJGI4N.js} +1 -1
- package/dist/web/assets/{naive-ui-CxpuzdjU.js → naive-ui-Bdxp09n2.js} +1 -1
- package/dist/web/assets/{vendors-DMjSfzlv.js → vendors-CKPV1OAU.js} +2 -2
- package/dist/web/assets/{vue-vendor-DET08QYg.js → vue-vendor-3bf-fPGP.js} +1 -1
- package/dist/web/index.html +7 -7
- package/docs/home.png +0 -0
- package/package.json +14 -5
- package/src/commands/daemon.js +3 -2
- package/src/commands/security.js +1 -2
- package/src/commands/toggle-proxy.js +100 -5
- package/src/config/paths.js +718 -90
- package/src/server/api/agents.js +1 -1
- package/src/server/api/channels.js +9 -0
- package/src/server/api/claude-hooks.js +13 -8
- package/src/server/api/codex-channels.js +9 -0
- package/src/server/api/codex-proxy.js +27 -15
- package/src/server/api/gemini-proxy.js +22 -11
- package/src/server/api/hooks.js +45 -0
- package/src/server/api/oauth-credentials.js +163 -0
- package/src/server/api/opencode-proxy.js +22 -10
- package/src/server/api/plugins.js +2 -1
- package/src/server/api/proxy.js +39 -44
- package/src/server/api/skills.js +91 -13
- package/src/server/api/ui-config.js +5 -0
- package/src/server/codex-proxy-server.js +90 -70
- package/src/server/gemini-proxy-server.js +107 -88
- package/src/server/index.js +2 -0
- package/src/server/opencode-proxy-server.js +381 -225
- package/src/server/proxy-server.js +86 -60
- package/src/server/services/alias.js +3 -3
- package/src/server/services/channels.js +21 -24
- package/src/server/services/codex-channels.js +158 -255
- package/src/server/services/codex-config.js +2 -5
- package/src/server/services/codex-env-manager.js +423 -0
- package/src/server/services/codex-settings-manager.js +21 -357
- package/src/server/services/codex-statistics-service.js +3 -27
- package/src/server/services/config-export-service.js +43 -9
- package/src/server/services/config-registry-service.js +3 -2
- package/src/server/services/config-sync-manager.js +1 -1
- package/src/server/services/favorites.js +4 -3
- package/src/server/services/gemini-channels.js +14 -12
- package/src/server/services/gemini-statistics-service.js +3 -25
- package/src/server/services/mcp-service.js +35 -19
- package/src/server/services/model-detector.js +4 -3
- package/src/server/services/native-keychain.js +243 -0
- package/src/server/services/native-oauth-adapters.js +891 -0
- package/src/server/services/network-access.js +39 -1
- package/src/server/services/notification-hooks.js +951 -0
- package/src/server/services/oauth-credentials-service.js +786 -0
- package/src/server/services/oauth-utils.js +49 -0
- package/src/server/services/opencode-channels.js +19 -15
- package/src/server/services/opencode-sessions.js +2 -2
- package/src/server/services/opencode-settings-manager.js +169 -16
- package/src/server/services/opencode-statistics-service.js +3 -27
- package/src/server/services/plugins-service.js +115 -15
- package/src/server/services/prompts-service.js +2 -3
- package/src/server/services/proxy-log-helper.js +242 -0
- package/src/server/services/proxy-runtime.js +6 -4
- package/src/server/services/repo-scanner-base.js +12 -4
- package/src/server/services/request-logger.js +7 -7
- package/src/server/services/security-config.js +4 -4
- package/src/server/services/session-cache.js +2 -2
- package/src/server/services/sessions.js +2 -2
- package/src/server/services/settings-manager.js +13 -0
- package/src/server/services/skill-service.js +867 -368
- package/src/server/services/statistics-service.js +5 -5
- package/src/server/services/ui-config.js +4 -3
- package/src/server/services/workspace-service.js +1 -1
- package/src/server/websocket-server.js +5 -4
- package/dist/web/assets/Home-BsSioaaB.css +0 -1
- package/dist/web/assets/Home-obifg_9E.js +0 -1
- package/dist/web/assets/index-C7LPdVsN.js +0 -2
- package/dist/web/assets/index-eEmjZKWP.css +0 -1
- package/docs/bannel.png +0 -0
- package/docs/model-redirection.md +0 -251
package/src/server/api/proxy.js
CHANGED
|
@@ -4,6 +4,7 @@ const { startProxyServer, stopProxyServer, getProxyStatus } = require('../proxy-
|
|
|
4
4
|
const {
|
|
5
5
|
setProxyConfig,
|
|
6
6
|
restoreSettings,
|
|
7
|
+
deleteBackup,
|
|
7
8
|
isProxyConfig,
|
|
8
9
|
getCurrentProxyPort,
|
|
9
10
|
settingsExists,
|
|
@@ -11,6 +12,7 @@ const {
|
|
|
11
12
|
readSettings
|
|
12
13
|
} = require('../services/settings-manager');
|
|
13
14
|
const { getAllChannels } = require('../services/channels');
|
|
15
|
+
const { clearNativeOAuth } = require('../services/native-oauth-adapters');
|
|
14
16
|
const { clearAllLogs } = require('../websocket-server');
|
|
15
17
|
const { PATHS, NATIVE_PATHS, ensureStorageDirMigrated } = require('../../config/paths');
|
|
16
18
|
const fs = require('fs');
|
|
@@ -37,6 +39,24 @@ function selectLatestEnabledChannel(channels) {
|
|
|
37
39
|
}, enabledChannels[0]);
|
|
38
40
|
}
|
|
39
41
|
|
|
42
|
+
function resolveActiveChannel(channels, activeChannelId = null) {
|
|
43
|
+
if (!Array.isArray(channels) || channels.length === 0) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (activeChannelId) {
|
|
48
|
+
const matched = channels.find(channel => channel.id === activeChannelId);
|
|
49
|
+
if (matched) {
|
|
50
|
+
return matched;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return selectLatestEnabledChannel(channels)
|
|
55
|
+
|| channels.find(channel => channel.enabled !== false)
|
|
56
|
+
|| channels[0]
|
|
57
|
+
|| null;
|
|
58
|
+
}
|
|
59
|
+
|
|
40
60
|
// 保存激活渠道ID
|
|
41
61
|
function saveActiveChannelId(channelId) {
|
|
42
62
|
ensureStorageDirMigrated();
|
|
@@ -70,11 +90,14 @@ function findActiveChannelFromSettings() {
|
|
|
70
90
|
try {
|
|
71
91
|
const settings = readSettings();
|
|
72
92
|
const baseUrl = settings?.env?.ANTHROPIC_BASE_URL || '';
|
|
93
|
+
const { readNativeOAuth } = require('../services/native-oauth-adapters');
|
|
94
|
+
const nativeOAuth = readNativeOAuth('claude');
|
|
73
95
|
|
|
74
96
|
// 兼容多种 API Key 格式(与 channels.js 保持一致)
|
|
75
|
-
let apiKey = settings?.env?.ANTHROPIC_API_KEY ||
|
|
76
|
-
|
|
77
|
-
|
|
97
|
+
let apiKey = settings?.env?.ANTHROPIC_API_KEY || '';
|
|
98
|
+
if (!apiKey && !nativeOAuth) {
|
|
99
|
+
apiKey = settings?.env?.ANTHROPIC_AUTH_TOKEN || '';
|
|
100
|
+
}
|
|
78
101
|
|
|
79
102
|
// 如果 apiKey 仍为空,尝试从 apiKeyHelper 提取
|
|
80
103
|
if (!apiKey && settings?.apiKeyHelper) {
|
|
@@ -195,6 +218,7 @@ router.post('/start', async (req, res) => {
|
|
|
195
218
|
}
|
|
196
219
|
|
|
197
220
|
// 5. 设置代理配置(备份并修改 settings.json)
|
|
221
|
+
clearNativeOAuth('claude');
|
|
198
222
|
setProxyConfig(proxyResult.port);
|
|
199
223
|
|
|
200
224
|
const updatedStatus = getProxyStatus();
|
|
@@ -221,57 +245,28 @@ router.post('/start', async (req, res) => {
|
|
|
221
245
|
router.post('/stop', async (req, res) => {
|
|
222
246
|
try {
|
|
223
247
|
const channelsBeforeStop = getAllChannels();
|
|
224
|
-
const
|
|
248
|
+
const activeChannelId = loadActiveChannelId();
|
|
249
|
+
let restoredChannel = resolveActiveChannel(channelsBeforeStop, activeChannelId);
|
|
225
250
|
|
|
226
251
|
// 1. 停止代理服务器
|
|
227
252
|
const proxyResult = await stopProxyServer();
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
// 2. 恢复配置(优先从备份,否则选择权重最高的启用渠道)
|
|
231
|
-
let restoredChannel = null;
|
|
253
|
+
const hadBackup = hasBackup();
|
|
232
254
|
|
|
233
|
-
//
|
|
234
|
-
if (
|
|
255
|
+
// 2. 恢复配置(优先恢复当前活动渠道,避免覆盖动态切换期间的设置变更)
|
|
256
|
+
if (restoredChannel) {
|
|
257
|
+
if (hadBackup) {
|
|
258
|
+
deleteBackup();
|
|
259
|
+
console.log('✅ Discarded backup snapshot');
|
|
260
|
+
}
|
|
261
|
+
} else if (hadBackup) {
|
|
235
262
|
restoreSettings();
|
|
236
263
|
console.log('✅ Restored settings from backup');
|
|
237
|
-
|
|
238
|
-
// 尝试找到匹配的渠道
|
|
239
264
|
const channels = getAllChannels();
|
|
240
265
|
const currentSettings = require('../services/channels').getCurrentSettings();
|
|
241
266
|
if (currentSettings) {
|
|
242
267
|
restoredChannel = channels.find(ch =>
|
|
243
268
|
ch.baseUrl === currentSettings.baseUrl && ch.apiKey === currentSettings.apiKey
|
|
244
|
-
);
|
|
245
|
-
}
|
|
246
|
-
// Fallback: keep latest enabled channel when leaving dynamic switching mode
|
|
247
|
-
if (!restoredChannel && latestEnabledChannel) {
|
|
248
|
-
restoredChannel = channels.find(ch => ch.id === latestEnabledChannel.id) || latestEnabledChannel;
|
|
249
|
-
}
|
|
250
|
-
// Fallback: use previously active channel id
|
|
251
|
-
if (!restoredChannel && activeChannelId) {
|
|
252
|
-
restoredChannel = channels.find(ch => ch.id === activeChannelId);
|
|
253
|
-
}
|
|
254
|
-
// Fallback: use first enabled channel
|
|
255
|
-
if (!restoredChannel) {
|
|
256
|
-
restoredChannel = channels.find(ch => ch.enabled !== false) || channels[0];
|
|
257
|
-
}
|
|
258
|
-
} else {
|
|
259
|
-
// 没有备份,选择权重最高的启用渠道
|
|
260
|
-
const { getBestChannelForRestore, updateClaudeSettings } = require('../services/channels');
|
|
261
|
-
const channels = getAllChannels();
|
|
262
|
-
restoredChannel = latestEnabledChannel
|
|
263
|
-
? channels.find(ch => ch.id === latestEnabledChannel.id)
|
|
264
|
-
: null;
|
|
265
|
-
if (!restoredChannel && activeChannelId) {
|
|
266
|
-
restoredChannel = channels.find(ch => ch.id === activeChannelId);
|
|
267
|
-
}
|
|
268
|
-
if (!restoredChannel) {
|
|
269
|
-
restoredChannel = getBestChannelForRestore();
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
if (restoredChannel) {
|
|
273
|
-
updateClaudeSettings(restoredChannel.baseUrl, restoredChannel.apiKey);
|
|
274
|
-
console.log(`✅ Restored settings to best channel: ${restoredChannel.name}`);
|
|
269
|
+
) || null;
|
|
275
270
|
}
|
|
276
271
|
}
|
|
277
272
|
|
|
@@ -283,7 +278,7 @@ router.post('/stop', async (req, res) => {
|
|
|
283
278
|
}
|
|
284
279
|
|
|
285
280
|
// 3. 删除备份文件和active-channel.json
|
|
286
|
-
if (
|
|
281
|
+
if (hadBackup && !restoredChannel) {
|
|
287
282
|
const backupPath = NATIVE_PATHS.claude.settingsBackup;
|
|
288
283
|
if (fs.existsSync(backupPath)) {
|
|
289
284
|
fs.unlinkSync(backupPath);
|
package/src/server/api/skills.js
CHANGED
|
@@ -25,6 +25,22 @@ function getSkillService(req) {
|
|
|
25
25
|
return { platform, service: skillServices.get(platform) };
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
function extractRepoPayload(source = {}) {
|
|
29
|
+
const repo = source.repo && typeof source.repo === 'object' ? source.repo : source;
|
|
30
|
+
return {
|
|
31
|
+
id: repo.id || source.repoId || '',
|
|
32
|
+
provider: repo.provider || source.provider || '',
|
|
33
|
+
host: repo.host || source.host || '',
|
|
34
|
+
owner: repo.owner || source.owner || '',
|
|
35
|
+
name: repo.name || source.name || '',
|
|
36
|
+
branch: repo.branch || source.branch || 'main',
|
|
37
|
+
directory: repo.directory || source.directory || '',
|
|
38
|
+
projectPath: repo.projectPath || source.projectPath || '',
|
|
39
|
+
localPath: repo.localPath || source.localPath || '',
|
|
40
|
+
repoUrl: repo.repoUrl || source.repoUrl || ''
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
28
44
|
/**
|
|
29
45
|
* 获取技能列表
|
|
30
46
|
* GET /api/skills
|
|
@@ -66,7 +82,9 @@ router.get('/detail/*', async (req, res) => {
|
|
|
66
82
|
});
|
|
67
83
|
}
|
|
68
84
|
|
|
69
|
-
const
|
|
85
|
+
const repoHint = extractRepoPayload(req.query || {});
|
|
86
|
+
const hasRepoHint = Object.values(repoHint).some(Boolean);
|
|
87
|
+
const result = await service.getSkillDetail(directory, hasRepoHint ? repoHint : null, req.query.fullDirectory || '');
|
|
70
88
|
res.json({
|
|
71
89
|
success: true,
|
|
72
90
|
platform,
|
|
@@ -106,7 +124,7 @@ router.get('/installed', (req, res) => {
|
|
|
106
124
|
/**
|
|
107
125
|
* 安装技能
|
|
108
126
|
* POST /api/skills/install
|
|
109
|
-
* Body: { directory, fullDirectory, repo
|
|
127
|
+
* Body: { directory, fullDirectory, repo }
|
|
110
128
|
* - directory: 本地安装目录(相对路径)
|
|
111
129
|
* - fullDirectory: 仓库中的完整路径(当指定了仓库子目录时使用)
|
|
112
130
|
*/
|
|
@@ -122,7 +140,7 @@ router.post('/install', async (req, res) => {
|
|
|
122
140
|
});
|
|
123
141
|
}
|
|
124
142
|
|
|
125
|
-
if (!repo
|
|
143
|
+
if (!repo) {
|
|
126
144
|
return res.status(400).json({
|
|
127
145
|
success: false,
|
|
128
146
|
message: 'Missing repo info'
|
|
@@ -131,11 +149,7 @@ router.post('/install', async (req, res) => {
|
|
|
131
149
|
|
|
132
150
|
const result = await service.installSkill(
|
|
133
151
|
directory,
|
|
134
|
-
{
|
|
135
|
-
owner: repo.owner,
|
|
136
|
-
name: repo.name,
|
|
137
|
-
branch: repo.branch || 'main'
|
|
138
|
-
},
|
|
152
|
+
extractRepoPayload({ repo }),
|
|
139
153
|
fullDirectory || null // 传递 fullDirectory 用于从仓库子目录下载
|
|
140
154
|
);
|
|
141
155
|
|
|
@@ -153,6 +167,28 @@ router.post('/install', async (req, res) => {
|
|
|
153
167
|
}
|
|
154
168
|
});
|
|
155
169
|
|
|
170
|
+
/**
|
|
171
|
+
* 安装本地 cc-tool 托管的技能
|
|
172
|
+
* POST /api/skills/install-local
|
|
173
|
+
* Body: { directory }
|
|
174
|
+
*/
|
|
175
|
+
router.post('/install-local', (req, res) => {
|
|
176
|
+
try {
|
|
177
|
+
const { platform, service } = getSkillService(req);
|
|
178
|
+
const { directory } = req.body;
|
|
179
|
+
|
|
180
|
+
if (!directory) {
|
|
181
|
+
return res.status(400).json({ success: false, message: 'Missing directory' });
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const result = service.installLocalSkill(directory);
|
|
185
|
+
res.json({ success: true, platform, ...result });
|
|
186
|
+
} catch (err) {
|
|
187
|
+
console.error('[Skills API] Install local skill error:', err);
|
|
188
|
+
res.status(500).json({ success: false, message: err.message });
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
156
192
|
/**
|
|
157
193
|
* 创建自定义技能
|
|
158
194
|
* POST /api/skills/create
|
|
@@ -264,22 +300,23 @@ router.get('/repos', (req, res) => {
|
|
|
264
300
|
/**
|
|
265
301
|
* 添加仓库
|
|
266
302
|
* POST /api/skills/repos
|
|
267
|
-
* Body: { owner, name, branch, directory, enabled }
|
|
303
|
+
* Body: { provider, owner, name, host, projectPath, localPath, branch, directory, enabled }
|
|
268
304
|
* - directory: 可选,指定扫描的子目录路径
|
|
269
305
|
*/
|
|
270
306
|
router.post('/repos', (req, res) => {
|
|
271
307
|
try {
|
|
272
308
|
const { platform, service } = getSkillService(req);
|
|
273
|
-
const
|
|
309
|
+
const repo = extractRepoPayload(req.body);
|
|
310
|
+
repo.enabled = req.body.enabled !== false;
|
|
274
311
|
|
|
275
|
-
if (!owner || !name) {
|
|
312
|
+
if (!repo.localPath && !repo.projectPath && (!repo.owner || !repo.name)) {
|
|
276
313
|
return res.status(400).json({
|
|
277
314
|
success: false,
|
|
278
|
-
message: 'Missing
|
|
315
|
+
message: 'Missing repo info'
|
|
279
316
|
});
|
|
280
317
|
}
|
|
281
318
|
|
|
282
|
-
const repos = service.addRepo(
|
|
319
|
+
const repos = service.addRepo(repo);
|
|
283
320
|
|
|
284
321
|
res.json({
|
|
285
322
|
success: true,
|
|
@@ -295,6 +332,47 @@ router.post('/repos', (req, res) => {
|
|
|
295
332
|
}
|
|
296
333
|
});
|
|
297
334
|
|
|
335
|
+
router.delete('/repos', (req, res) => {
|
|
336
|
+
try {
|
|
337
|
+
const { platform, service } = getSkillService(req);
|
|
338
|
+
const { id = '', owner = '', name = '', directory = '' } = req.query;
|
|
339
|
+
const repos = service.removeRepo(owner, name, directory, id);
|
|
340
|
+
|
|
341
|
+
res.json({
|
|
342
|
+
success: true,
|
|
343
|
+
platform,
|
|
344
|
+
repos
|
|
345
|
+
});
|
|
346
|
+
} catch (err) {
|
|
347
|
+
console.error('[Skills API] Remove repo error:', err);
|
|
348
|
+
res.status(500).json({
|
|
349
|
+
success: false,
|
|
350
|
+
message: err.message
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
router.put('/repos/toggle', (req, res) => {
|
|
356
|
+
try {
|
|
357
|
+
const { platform, service } = getSkillService(req);
|
|
358
|
+
const { id = '', owner = '', name = '', enabled, directory = '' } = req.body;
|
|
359
|
+
|
|
360
|
+
const repos = service.toggleRepo(owner, name, directory, enabled, id);
|
|
361
|
+
|
|
362
|
+
res.json({
|
|
363
|
+
success: true,
|
|
364
|
+
platform,
|
|
365
|
+
repos
|
|
366
|
+
});
|
|
367
|
+
} catch (err) {
|
|
368
|
+
console.error('[Skills API] Toggle repo error:', err);
|
|
369
|
+
res.status(500).json({
|
|
370
|
+
success: false,
|
|
371
|
+
message: err.message
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
|
|
298
376
|
/**
|
|
299
377
|
* 删除仓库
|
|
300
378
|
* DELETE /api/skills/repos/:owner/:name
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const express = require('express');
|
|
2
2
|
const router = express.Router();
|
|
3
|
+
const { createSameOriginGuard } = require('../services/network-access');
|
|
3
4
|
const {
|
|
4
5
|
loadUIConfig,
|
|
5
6
|
saveUIConfig,
|
|
@@ -7,6 +8,10 @@ const {
|
|
|
7
8
|
updateNestedUIConfig
|
|
8
9
|
} = require('../services/ui-config');
|
|
9
10
|
|
|
11
|
+
router.use(createSameOriginGuard({
|
|
12
|
+
message: '禁止跨站访问 UI 配置接口'
|
|
13
|
+
}));
|
|
14
|
+
|
|
10
15
|
// Get all UI config
|
|
11
16
|
router.get('/', (req, res) => {
|
|
12
17
|
try {
|
|
@@ -11,8 +11,9 @@ const { resolveModelPricing } = require('./utils/pricing');
|
|
|
11
11
|
const { recordRequest: recordCodexRequest } = require('./services/codex-statistics-service');
|
|
12
12
|
const { saveProxyStartTime, clearProxyStartTime, getProxyStartTime, getProxyRuntime } = require('./services/proxy-runtime');
|
|
13
13
|
const { createDecodedStream } = require('./services/response-decoder');
|
|
14
|
-
const {
|
|
14
|
+
const { getEffectiveApiKey } = require('./services/codex-channels');
|
|
15
15
|
const { persistProxyRequestSnapshot } = require('./services/request-logger');
|
|
16
|
+
const { publishUsageLog, publishFailureLog } = require('./services/proxy-log-helper');
|
|
16
17
|
|
|
17
18
|
let proxyServer = null;
|
|
18
19
|
let proxyApp = null;
|
|
@@ -270,6 +271,14 @@ async function startCodexProxyServer(options = {}) {
|
|
|
270
271
|
const effectiveKey = getEffectiveApiKey(channel);
|
|
271
272
|
if (!effectiveKey) {
|
|
272
273
|
release();
|
|
274
|
+
publishFailureLog({
|
|
275
|
+
source: 'codex',
|
|
276
|
+
channel: channel.name,
|
|
277
|
+
message: 'API key not configured or expired. Please update your channel key.',
|
|
278
|
+
statusCode: 401,
|
|
279
|
+
stage: 'preflight',
|
|
280
|
+
broadcastLog
|
|
281
|
+
});
|
|
273
282
|
return res.status(401).json({
|
|
274
283
|
error: {
|
|
275
284
|
message: 'API key not configured or expired. Please update your channel key.',
|
|
@@ -324,6 +333,20 @@ async function startCodexProxyServer(options = {}) {
|
|
|
324
333
|
release();
|
|
325
334
|
if (err) {
|
|
326
335
|
recordFailure(channel.id, 'codex', err);
|
|
336
|
+
const metadata = requestMetadata.get(req) || {
|
|
337
|
+
channel: channel.name,
|
|
338
|
+
channelId: channel.id,
|
|
339
|
+
startTime: Date.now()
|
|
340
|
+
};
|
|
341
|
+
publishFailureLog({
|
|
342
|
+
source: 'codex',
|
|
343
|
+
metadata,
|
|
344
|
+
message: err.message,
|
|
345
|
+
error: err,
|
|
346
|
+
statusCode: 502,
|
|
347
|
+
stage: 'proxy_web',
|
|
348
|
+
broadcastLog
|
|
349
|
+
});
|
|
327
350
|
console.error('Codex proxy error:', err);
|
|
328
351
|
if (res && !res.headersSent) {
|
|
329
352
|
res.status(502).json({
|
|
@@ -337,6 +360,13 @@ async function startCodexProxyServer(options = {}) {
|
|
|
337
360
|
});
|
|
338
361
|
} catch (error) {
|
|
339
362
|
console.error('Codex channel allocation error:', error);
|
|
363
|
+
publishFailureLog({
|
|
364
|
+
source: 'codex',
|
|
365
|
+
message: error.message || 'No Codex channel available',
|
|
366
|
+
statusCode: 503,
|
|
367
|
+
stage: 'allocate_channel',
|
|
368
|
+
broadcastLog
|
|
369
|
+
});
|
|
340
370
|
if (!res.headersSent) {
|
|
341
371
|
res.status(503).json({
|
|
342
372
|
error: {
|
|
@@ -389,8 +419,40 @@ async function startCodexProxyServer(options = {}) {
|
|
|
389
419
|
totalTokens: 0,
|
|
390
420
|
model: ''
|
|
391
421
|
};
|
|
422
|
+
let usageRecorded = false;
|
|
392
423
|
const parsedStream = createDecodedStream(proxyRes);
|
|
393
424
|
|
|
425
|
+
function recordUsageIfReady() {
|
|
426
|
+
if (usageRecorded) {
|
|
427
|
+
return false;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const result = publishUsageLog({
|
|
431
|
+
source: 'codex',
|
|
432
|
+
metadata,
|
|
433
|
+
model: tokenData.model,
|
|
434
|
+
tokens: {
|
|
435
|
+
input: tokenData.inputTokens,
|
|
436
|
+
output: tokenData.outputTokens,
|
|
437
|
+
cached: tokenData.cachedTokens,
|
|
438
|
+
reasoning: tokenData.reasoningTokens,
|
|
439
|
+
total: tokenData.totalTokens
|
|
440
|
+
},
|
|
441
|
+
calculateCost,
|
|
442
|
+
broadcastLog,
|
|
443
|
+
recordRequest: recordCodexRequest,
|
|
444
|
+
recordSuccess,
|
|
445
|
+
allowBroadcast: !isResponseClosed
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
if (!result) {
|
|
449
|
+
return false;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
usageRecorded = true;
|
|
453
|
+
return true;
|
|
454
|
+
}
|
|
455
|
+
|
|
394
456
|
parsedStream.on('data', (chunk) => {
|
|
395
457
|
// 如果响应已关闭,停止处理
|
|
396
458
|
if (isResponseClosed) {
|
|
@@ -455,7 +517,10 @@ async function startCodexProxyServer(options = {}) {
|
|
|
455
517
|
// 兼容 Responses API 和 Chat Completions API
|
|
456
518
|
tokenData.inputTokens = parsed.usage.input_tokens || parsed.usage.prompt_tokens || 0;
|
|
457
519
|
tokenData.outputTokens = parsed.usage.output_tokens || parsed.usage.completion_tokens || 0;
|
|
520
|
+
tokenData.totalTokens = parsed.usage.total_tokens || (tokenData.inputTokens + tokenData.outputTokens);
|
|
458
521
|
}
|
|
522
|
+
|
|
523
|
+
recordUsageIfReady();
|
|
459
524
|
} catch (err) {
|
|
460
525
|
// 忽略解析错误
|
|
461
526
|
}
|
|
@@ -475,71 +540,14 @@ async function startCodexProxyServer(options = {}) {
|
|
|
475
540
|
// 兼容两种格式
|
|
476
541
|
tokenData.inputTokens = parsed.usage.input_tokens || parsed.usage.prompt_tokens || 0;
|
|
477
542
|
tokenData.outputTokens = parsed.usage.output_tokens || parsed.usage.completion_tokens || 0;
|
|
543
|
+
tokenData.totalTokens = parsed.usage.total_tokens || (tokenData.inputTokens + tokenData.outputTokens);
|
|
478
544
|
}
|
|
479
545
|
} catch (err) {
|
|
480
546
|
// 忽略解析错误
|
|
481
547
|
}
|
|
482
548
|
}
|
|
483
549
|
|
|
484
|
-
|
|
485
|
-
if (tokenData.inputTokens > 0 || tokenData.outputTokens > 0) {
|
|
486
|
-
const now = new Date();
|
|
487
|
-
const time = now.toLocaleTimeString('zh-CN', {
|
|
488
|
-
hour12: false,
|
|
489
|
-
hour: '2-digit',
|
|
490
|
-
minute: '2-digit',
|
|
491
|
-
second: '2-digit'
|
|
492
|
-
});
|
|
493
|
-
|
|
494
|
-
// 记录统计数据(先计算)
|
|
495
|
-
const tokens = {
|
|
496
|
-
input: tokenData.inputTokens,
|
|
497
|
-
output: tokenData.outputTokens,
|
|
498
|
-
total: tokenData.inputTokens + tokenData.outputTokens
|
|
499
|
-
};
|
|
500
|
-
const cost = calculateCost(tokenData.model, tokens);
|
|
501
|
-
|
|
502
|
-
// 广播日志(仅当响应仍然开放时)
|
|
503
|
-
if (!isResponseClosed) {
|
|
504
|
-
broadcastLog({
|
|
505
|
-
type: 'log',
|
|
506
|
-
id: metadata.id,
|
|
507
|
-
time: time,
|
|
508
|
-
channel: metadata.channel,
|
|
509
|
-
model: tokenData.model,
|
|
510
|
-
inputTokens: tokenData.inputTokens,
|
|
511
|
-
outputTokens: tokenData.outputTokens,
|
|
512
|
-
cachedTokens: tokenData.cachedTokens,
|
|
513
|
-
reasoningTokens: tokenData.reasoningTokens,
|
|
514
|
-
totalTokens: tokenData.totalTokens,
|
|
515
|
-
cost: cost,
|
|
516
|
-
source: 'codex'
|
|
517
|
-
});
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
const duration = Date.now() - metadata.startTime;
|
|
521
|
-
|
|
522
|
-
recordCodexRequest({
|
|
523
|
-
id: metadata.id,
|
|
524
|
-
timestamp: new Date(metadata.startTime).toISOString(),
|
|
525
|
-
toolType: 'codex',
|
|
526
|
-
channel: metadata.channel,
|
|
527
|
-
channelId: metadata.channelId,
|
|
528
|
-
model: tokenData.model,
|
|
529
|
-
tokens: {
|
|
530
|
-
input: tokenData.inputTokens,
|
|
531
|
-
output: tokenData.outputTokens,
|
|
532
|
-
reasoning: tokenData.reasoningTokens,
|
|
533
|
-
cached: tokenData.cachedTokens,
|
|
534
|
-
total: tokens.total
|
|
535
|
-
},
|
|
536
|
-
duration: duration,
|
|
537
|
-
success: true,
|
|
538
|
-
cost: cost
|
|
539
|
-
});
|
|
540
|
-
|
|
541
|
-
recordSuccess(metadata.channelId, 'codex');
|
|
542
|
-
}
|
|
550
|
+
recordUsageIfReady();
|
|
543
551
|
|
|
544
552
|
if (!isResponseClosed) {
|
|
545
553
|
requestMetadata.delete(req);
|
|
@@ -553,6 +561,15 @@ async function startCodexProxyServer(options = {}) {
|
|
|
553
561
|
}
|
|
554
562
|
isResponseClosed = true;
|
|
555
563
|
recordFailure(metadata.channelId, 'codex', err);
|
|
564
|
+
publishFailureLog({
|
|
565
|
+
source: 'codex',
|
|
566
|
+
metadata,
|
|
567
|
+
message: err.message,
|
|
568
|
+
error: err,
|
|
569
|
+
statusCode: proxyRes.statusCode,
|
|
570
|
+
stage: 'response_stream',
|
|
571
|
+
broadcastLog
|
|
572
|
+
});
|
|
556
573
|
requestMetadata.delete(req);
|
|
557
574
|
});
|
|
558
575
|
});
|
|
@@ -565,6 +582,19 @@ async function startCodexProxyServer(options = {}) {
|
|
|
565
582
|
releaseChannel(req.selectedChannel.id, 'codex');
|
|
566
583
|
broadcastSchedulerState('codex', getSchedulerState('codex'));
|
|
567
584
|
}
|
|
585
|
+
publishFailureLog({
|
|
586
|
+
source: 'codex',
|
|
587
|
+
metadata: (req && requestMetadata.get(req)) || {
|
|
588
|
+
channel: req?.selectedChannel?.name,
|
|
589
|
+
channelId: req?.selectedChannel?.id,
|
|
590
|
+
model: req?.body?.model
|
|
591
|
+
},
|
|
592
|
+
message: err.message,
|
|
593
|
+
error: err,
|
|
594
|
+
statusCode: 502,
|
|
595
|
+
stage: 'proxy',
|
|
596
|
+
broadcastLog
|
|
597
|
+
});
|
|
568
598
|
if (res && !res.headersSent) {
|
|
569
599
|
res.status(502).json({
|
|
570
600
|
error: {
|
|
@@ -585,16 +615,6 @@ async function startCodexProxyServer(options = {}) {
|
|
|
585
615
|
// 保存代理启动时间(如果是切换渠道,保留原有启动时间)
|
|
586
616
|
saveProxyStartTime('codex', preserveStartTime);
|
|
587
617
|
|
|
588
|
-
// 启动代理时同步配置到 Codex 的 config.toml
|
|
589
|
-
try {
|
|
590
|
-
const enabledChannels = getEnabledChannels();
|
|
591
|
-
if (enabledChannels.length > 0) {
|
|
592
|
-
writeCodexConfigForMultiChannel(enabledChannels);
|
|
593
|
-
}
|
|
594
|
-
} catch (err) {
|
|
595
|
-
// ignore sync error
|
|
596
|
-
}
|
|
597
|
-
|
|
598
618
|
resolve({ success: true, port });
|
|
599
619
|
});
|
|
600
620
|
|