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.
Files changed (89) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +253 -326
  3. package/dist/web/assets/{Analytics-IW6eAy9u.js → Analytics-D6LzK9hk.js} +1 -1
  4. package/dist/web/assets/{ConfigTemplates-BPtkTMSc.js → ConfigTemplates-BUDYuxRi.js} +1 -1
  5. package/dist/web/assets/Home-BQxQ1LhR.css +1 -0
  6. package/dist/web/assets/Home-D7KX7iF8.js +1 -0
  7. package/dist/web/assets/{PluginManager-BGx9MSDV.js → PluginManager-DTgQ--vB.js} +1 -1
  8. package/dist/web/assets/{ProjectList-BCn-mrCx.js → ProjectList-DMCiGmCT.js} +1 -1
  9. package/dist/web/assets/{SessionList-CzLfebJQ.js → SessionList-CRBsdVRe.js} +1 -1
  10. package/dist/web/assets/{SkillManager-CXz2vBQx.js → SkillManager-DMwx2Q4k.js} +1 -1
  11. package/dist/web/assets/{WorkspaceManager-CHtgMfKc.js → WorkspaceManager-DapB4ljL.js} +1 -1
  12. package/dist/web/assets/{icons-B29onFfZ.js → icons-B5Pl4lrD.js} +1 -1
  13. package/dist/web/assets/index-CL-qpoJ_.js +2 -0
  14. package/dist/web/assets/index-D_5dRFOL.css +1 -0
  15. package/dist/web/assets/{markdown-C9MYpaSi.js → markdown-DyTJGI4N.js} +1 -1
  16. package/dist/web/assets/{naive-ui-CxpuzdjU.js → naive-ui-Bdxp09n2.js} +1 -1
  17. package/dist/web/assets/{vendors-DMjSfzlv.js → vendors-CKPV1OAU.js} +2 -2
  18. package/dist/web/assets/{vue-vendor-DET08QYg.js → vue-vendor-3bf-fPGP.js} +1 -1
  19. package/dist/web/index.html +7 -7
  20. package/docs/home.png +0 -0
  21. package/package.json +14 -5
  22. package/src/commands/daemon.js +3 -2
  23. package/src/commands/security.js +1 -2
  24. package/src/commands/toggle-proxy.js +100 -5
  25. package/src/config/paths.js +718 -90
  26. package/src/server/api/agents.js +1 -1
  27. package/src/server/api/channels.js +9 -0
  28. package/src/server/api/claude-hooks.js +13 -8
  29. package/src/server/api/codex-channels.js +9 -0
  30. package/src/server/api/codex-proxy.js +27 -15
  31. package/src/server/api/gemini-proxy.js +22 -11
  32. package/src/server/api/hooks.js +45 -0
  33. package/src/server/api/oauth-credentials.js +163 -0
  34. package/src/server/api/opencode-proxy.js +22 -10
  35. package/src/server/api/plugins.js +2 -1
  36. package/src/server/api/proxy.js +39 -44
  37. package/src/server/api/skills.js +91 -13
  38. package/src/server/api/ui-config.js +5 -0
  39. package/src/server/codex-proxy-server.js +90 -70
  40. package/src/server/gemini-proxy-server.js +107 -88
  41. package/src/server/index.js +2 -0
  42. package/src/server/opencode-proxy-server.js +381 -225
  43. package/src/server/proxy-server.js +86 -60
  44. package/src/server/services/alias.js +3 -3
  45. package/src/server/services/channels.js +21 -24
  46. package/src/server/services/codex-channels.js +158 -255
  47. package/src/server/services/codex-config.js +2 -5
  48. package/src/server/services/codex-env-manager.js +423 -0
  49. package/src/server/services/codex-settings-manager.js +21 -357
  50. package/src/server/services/codex-statistics-service.js +3 -27
  51. package/src/server/services/config-export-service.js +43 -9
  52. package/src/server/services/config-registry-service.js +3 -2
  53. package/src/server/services/config-sync-manager.js +1 -1
  54. package/src/server/services/favorites.js +4 -3
  55. package/src/server/services/gemini-channels.js +14 -12
  56. package/src/server/services/gemini-statistics-service.js +3 -25
  57. package/src/server/services/mcp-service.js +35 -19
  58. package/src/server/services/model-detector.js +4 -3
  59. package/src/server/services/native-keychain.js +243 -0
  60. package/src/server/services/native-oauth-adapters.js +891 -0
  61. package/src/server/services/network-access.js +39 -1
  62. package/src/server/services/notification-hooks.js +951 -0
  63. package/src/server/services/oauth-credentials-service.js +786 -0
  64. package/src/server/services/oauth-utils.js +49 -0
  65. package/src/server/services/opencode-channels.js +19 -15
  66. package/src/server/services/opencode-sessions.js +2 -2
  67. package/src/server/services/opencode-settings-manager.js +169 -16
  68. package/src/server/services/opencode-statistics-service.js +3 -27
  69. package/src/server/services/plugins-service.js +115 -15
  70. package/src/server/services/prompts-service.js +2 -3
  71. package/src/server/services/proxy-log-helper.js +242 -0
  72. package/src/server/services/proxy-runtime.js +6 -4
  73. package/src/server/services/repo-scanner-base.js +12 -4
  74. package/src/server/services/request-logger.js +7 -7
  75. package/src/server/services/security-config.js +4 -4
  76. package/src/server/services/session-cache.js +2 -2
  77. package/src/server/services/sessions.js +2 -2
  78. package/src/server/services/settings-manager.js +13 -0
  79. package/src/server/services/skill-service.js +867 -368
  80. package/src/server/services/statistics-service.js +5 -5
  81. package/src/server/services/ui-config.js +4 -3
  82. package/src/server/services/workspace-service.js +1 -1
  83. package/src/server/websocket-server.js +5 -4
  84. package/dist/web/assets/Home-BsSioaaB.css +0 -1
  85. package/dist/web/assets/Home-obifg_9E.js +0 -1
  86. package/dist/web/assets/index-C7LPdVsN.js +0 -2
  87. package/dist/web/assets/index-eEmjZKWP.css +0 -1
  88. package/docs/bannel.png +0 -0
  89. package/docs/model-redirection.md +0 -251
@@ -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
- settings?.env?.ANTHROPIC_AUTH_TOKEN || // 88code等平台格式
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 latestEnabledChannel = selectLatestEnabledChannel(channelsBeforeStop);
248
+ const activeChannelId = loadActiveChannelId();
249
+ let restoredChannel = resolveActiveChannel(channelsBeforeStop, activeChannelId);
225
250
 
226
251
  // 1. 停止代理服务器
227
252
  const proxyResult = await stopProxyServer();
228
- const activeChannelId = loadActiveChannelId();
229
-
230
- // 2. 恢复配置(优先从备份,否则选择权重最高的启用渠道)
231
- let restoredChannel = null;
253
+ const hadBackup = hasBackup();
232
254
 
233
- // 优先尝试从备份恢复
234
- if (hasBackup()) {
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 (hasBackup()) {
281
+ if (hadBackup && !restoredChannel) {
287
282
  const backupPath = NATIVE_PATHS.claude.settingsBackup;
288
283
  if (fs.existsSync(backupPath)) {
289
284
  fs.unlinkSync(backupPath);
@@ -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 result = await service.getSkillDetail(directory);
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: { owner, name, branch } }
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 || !repo.owner || !repo.name) {
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 { owner, name, branch = 'main', directory = '', enabled = true } = req.body;
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 owner or name'
315
+ message: 'Missing repo info'
279
316
  });
280
317
  }
281
318
 
282
- const repos = service.addRepo({ owner, name, branch, directory, enabled });
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 { getEnabledChannels, writeCodexConfigForMultiChannel, getEffectiveApiKey } = require('./services/codex-channels');
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
- // 只有当有 token 数据时才记录
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