coding-tool-x 3.5.4 → 3.5.6

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 (40) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +8 -4
  3. package/dist/web/assets/{Analytics-CmN09J9U.js → Analytics-CRNCHeui.js} +1 -1
  4. package/dist/web/assets/{ConfigTemplates-CeTAPmep.js → ConfigTemplates-C0erJdo2.js} +1 -1
  5. package/dist/web/assets/{Home-BYtCM3rK.js → Home-CL5z6Q4d.js} +1 -1
  6. package/dist/web/assets/{PluginManager-OAH1eMO0.js → PluginManager-hDx0XMO_.js} +1 -1
  7. package/dist/web/assets/{ProjectList-B0pIy1cv.js → ProjectList-BNsz96av.js} +1 -1
  8. package/dist/web/assets/{SessionList-DbB6ASiA.js → SessionList-CG1UhFo3.js} +1 -1
  9. package/dist/web/assets/{SkillManager-wp1dhL1z.js → SkillManager-D6Vwpajh.js} +1 -1
  10. package/dist/web/assets/{WorkspaceManager-Ce6wQoKb.js → WorkspaceManager-C3TjeOPy.js} +1 -1
  11. package/dist/web/assets/{icons-DlxD2wZJ.js → icons-CQuif85v.js} +1 -1
  12. package/dist/web/assets/index-GuER-BmS.js +2 -0
  13. package/dist/web/assets/{index-B02wDWNC.css → index-VGAxnLqi.css} +1 -1
  14. package/dist/web/index.html +3 -3
  15. package/package.json +1 -1
  16. package/src/commands/stats.js +41 -4
  17. package/src/index.js +1 -0
  18. package/src/server/api/codex-sessions.js +6 -3
  19. package/src/server/api/dashboard.js +25 -1
  20. package/src/server/api/gemini-sessions.js +6 -3
  21. package/src/server/api/hooks.js +17 -1
  22. package/src/server/api/opencode-sessions.js +6 -3
  23. package/src/server/api/plugins.js +24 -33
  24. package/src/server/api/sessions.js +6 -3
  25. package/src/server/codex-proxy-server.js +24 -59
  26. package/src/server/gemini-proxy-server.js +25 -66
  27. package/src/server/index.js +6 -4
  28. package/src/server/opencode-proxy-server.js +24 -59
  29. package/src/server/proxy-server.js +18 -30
  30. package/src/server/services/base/response-usage-parser.js +187 -0
  31. package/src/server/services/codex-sessions.js +107 -9
  32. package/src/server/services/network-access.js +14 -0
  33. package/src/server/services/notification-hooks.js +175 -16
  34. package/src/server/services/plugins-service.js +502 -44
  35. package/src/server/services/proxy-log-helper.js +21 -3
  36. package/src/server/services/session-launch-command.js +81 -0
  37. package/src/server/services/sessions.js +103 -33
  38. package/src/server/services/statistics-service.js +7 -0
  39. package/src/server/websocket-server.js +25 -1
  40. package/dist/web/assets/index-CHwVofQH.js +0 -2
@@ -5,14 +5,14 @@
5
5
  <link rel="icon" href="/favicon.ico">
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
7
  <title>CC-TOOL - ClaudeCode增强工作助手</title>
8
- <script type="module" crossorigin src="/assets/index-CHwVofQH.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-GuER-BmS.js"></script>
9
9
  <link rel="modulepreload" crossorigin href="/assets/markdown-DyTJGI4N.js">
10
10
  <link rel="modulepreload" crossorigin href="/assets/vue-vendor-aWwwFAao.js">
11
11
  <link rel="modulepreload" crossorigin href="/assets/vendors-Fza9uSYn.js">
12
12
  <link rel="modulepreload" crossorigin href="/assets/naive-ui-BaTCPPL5.js">
13
- <link rel="modulepreload" crossorigin href="/assets/icons-DlxD2wZJ.js">
13
+ <link rel="modulepreload" crossorigin href="/assets/icons-CQuif85v.js">
14
14
  <link rel="stylesheet" crossorigin href="/assets/markdown-BfC0goYb.css">
15
- <link rel="stylesheet" crossorigin href="/assets/index-B02wDWNC.css">
15
+ <link rel="stylesheet" crossorigin href="/assets/index-VGAxnLqi.css">
16
16
  </head>
17
17
  <body>
18
18
  <div id="app"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coding-tool-x",
3
- "version": "3.5.4",
3
+ "version": "3.5.6",
4
4
  "description": "Vibe Coding 增强工作助手 - 智能会话管理、动态渠道切换、全局搜索、实时监控",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -9,6 +9,9 @@ const TOOL_ENDPOINTS = {
9
9
  gemini: '/api/gemini/statistics',
10
10
  opencode: '/api/opencode/statistics'
11
11
  };
12
+ const SHARED_TOOL_TYPE_BY_CLI = {
13
+ claude: 'claude-code'
14
+ };
12
15
 
13
16
  /**
14
17
  * HTTP 请求辅助函数
@@ -154,6 +157,34 @@ function extractSummary(stats) {
154
157
  return summary;
155
158
  }
156
159
 
160
+ function extractToolSummary(stats, toolType) {
161
+ const sharedToolType = SHARED_TOOL_TYPE_BY_CLI[toolType];
162
+ if (!sharedToolType) {
163
+ return extractSummary(stats);
164
+ }
165
+
166
+ // `/api/statistics/*` is shared across tools, so Claude must be scoped explicitly.
167
+ const toolStats = stats?.byToolType?.[sharedToolType] || {};
168
+ return extractSummary({
169
+ summary: {
170
+ requests: normalizeNumber(toolStats.requests),
171
+ tokens: normalizeNumber(toolStats.tokens?.total),
172
+ cost: normalizeNumber(toolStats.cost),
173
+ inputTokens: normalizeNumber(toolStats.tokens?.input),
174
+ outputTokens: normalizeNumber(toolStats.tokens?.output),
175
+ cacheCreation: normalizeNumber(toolStats.tokens?.cacheCreation),
176
+ cacheRead: normalizeNumber(toolStats.tokens?.cacheRead),
177
+ reasoningTokens: normalizeNumber(toolStats.tokens?.reasoning),
178
+ cachedTokens: normalizeNumber(toolStats.tokens?.cached)
179
+ },
180
+ global: {
181
+ totalRequests: normalizeNumber(toolStats.requests),
182
+ totalTokens: normalizeNumber(toolStats.tokens?.total),
183
+ totalCost: normalizeNumber(toolStats.cost)
184
+ }
185
+ });
186
+ }
187
+
157
188
  function mergeSummaries(target, source) {
158
189
  target.requests += normalizeNumber(source.requests);
159
190
  target.tokens += normalizeNumber(source.tokens);
@@ -181,12 +212,12 @@ async function fetchToolStats(toolType, timeRange) {
181
212
 
182
213
  if (timeRange === 'today') {
183
214
  const response = await httpRequest('GET', `${endpointBase}/today`);
184
- return extractSummary(response.data);
215
+ return extractToolSummary(response.data, toolType);
185
216
  }
186
217
 
187
218
  if (timeRange === 'all') {
188
219
  const response = await httpRequest('GET', `${endpointBase}/summary`);
189
- return extractSummary(response.data);
220
+ return extractToolSummary(response.data, toolType);
190
221
  }
191
222
 
192
223
  const days = getRangeDays(timeRange);
@@ -194,7 +225,7 @@ async function fetchToolStats(toolType, timeRange) {
194
225
  for (let i = 0; i < days; i++) {
195
226
  const date = getDateString(i);
196
227
  const response = await httpRequest('GET', `${endpointBase}/daily/${date}`);
197
- const dailySummary = extractSummary(response.data);
228
+ const dailySummary = extractToolSummary(response.data, toolType);
198
229
  mergeSummaries(merged, dailySummary);
199
230
  }
200
231
  return merged;
@@ -394,5 +425,11 @@ async function handleStatsExport(type = null, format = 'json') {
394
425
 
395
426
  module.exports = {
396
427
  handleStats,
397
- handleStatsExport
428
+ handleStatsExport,
429
+ _test: {
430
+ extractSummary,
431
+ extractToolSummary,
432
+ fetchToolStats,
433
+ fetchOverallStats
434
+ }
398
435
  };
package/src/index.js CHANGED
@@ -39,6 +39,7 @@ function showHelp() {
39
39
 
40
40
  console.log(chalk.yellow('[START] 服务管理:'));
41
41
  console.log(' ctx start 启动所有服务(后台运行)');
42
+ console.log(' ctx start --host 启动所有服务(后台运行,允许 LAN 访问)');
42
43
  console.log(' ctx stop 停止所有服务');
43
44
  console.log(' ctx restart 重启所有服务');
44
45
  console.log(' ctx status 查看服务状态\n');
@@ -11,6 +11,7 @@ const {
11
11
  } = require('../services/codex-sessions');
12
12
  const { isCodexInstalled } = require('../services/codex-config');
13
13
  const { loadAliases } = require('../services/alias');
14
+ const { buildLaunchCommand } = require('../services/session-launch-command');
14
15
 
15
16
  const DEBUG_CODEX_PERF = process.env.DEBUG_CODEX_PERF === '1';
16
17
 
@@ -515,9 +516,11 @@ module.exports = (config) => {
515
516
  timestamp: Date.now()
516
517
  });
517
518
 
518
- const command = `codex resume ${sessionId}`;
519
- const quotedCwd = `"${String(cwd).replace(/"/g, '\\"')}"`;
520
- const copyCommand = `cd ${quotedCwd} && ${command}`;
519
+ const { command, copyCommand } = buildLaunchCommand({
520
+ cwd,
521
+ executable: 'codex',
522
+ args: ['resume', sessionId]
523
+ });
521
524
 
522
525
  res.json({
523
526
  success: true,
@@ -26,6 +26,30 @@ const { getTodayStatistics: getCodexTodayStatistics } = require('../services/cod
26
26
  const { getTodayStatistics: getGeminiTodayStatistics } = require('../services/gemini-statistics-service');
27
27
  const { getTodayStatistics: getOpenCodeTodayStatistics } = require('../services/opencode-statistics-service');
28
28
 
29
+ function filterEntriesByToolType(entries = {}, toolType) {
30
+ return Object.fromEntries(
31
+ Object.entries(entries || {}).filter(([, value]) => !toolType || !value?.toolType || value.toolType === toolType)
32
+ );
33
+ }
34
+
35
+ function extractToolScopedStats(stats, toolType) {
36
+ if (!stats || !toolType || !stats.byToolType) {
37
+ return stats;
38
+ }
39
+
40
+ const toolScope = stats.byToolType[toolType] || {};
41
+ return {
42
+ ...stats,
43
+ summary: {
44
+ requests: toolScope.requests || 0,
45
+ tokens: toolScope.tokens?.total || 0,
46
+ cost: toolScope.cost || 0
47
+ },
48
+ byChannel: filterEntriesByToolType(stats.byChannel, toolType),
49
+ byModel: filterEntriesByToolType(stats.byModel, toolType)
50
+ };
51
+ }
52
+
29
53
  /**
30
54
  * GET /api/dashboard/init
31
55
  * 聚合首页所需的所有数据,一次请求返回
@@ -123,7 +147,7 @@ router.get('/init', async (req, res) => {
123
147
  opencode: opencodeCounts || { projectCount: 0, sessionCount: 0 }
124
148
  },
125
149
  todayStats: {
126
- claude: formatStats(claudeTodayStats),
150
+ claude: formatStats(extractToolScopedStats(claudeTodayStats, 'claude-code')),
127
151
  codex: formatStats(codexTodayStats),
128
152
  gemini: formatStats(geminiTodayStats),
129
153
  opencode: formatStats(opencodeTodayStats)
@@ -13,6 +13,7 @@ const {
13
13
  } = require('../services/gemini-sessions');
14
14
  const { isGeminiInstalled } = require('../services/gemini-config');
15
15
  const { loadAliases } = require('../services/alias');
16
+ const { buildLaunchCommand } = require('../services/session-launch-command');
16
17
 
17
18
  module.exports = (config) => {
18
19
  /**
@@ -407,9 +408,11 @@ module.exports = (config) => {
407
408
  const resumeIndex = sessionIndex + 1;
408
409
 
409
410
  // 构建 Gemini CLI 命令(使用 --resume <index> 恢复特定会话)
410
- const command = `gemini --resume ${resumeIndex}`;
411
- const quotedCwd = `"${String(projectPath).replace(/"/g, '\\"')}"`;
412
- const copyCommand = `cd ${quotedCwd} && ${command}`;
411
+ const { command, copyCommand } = buildLaunchCommand({
412
+ cwd: projectPath,
413
+ executable: 'gemini',
414
+ args: ['--resume', String(resumeIndex)]
415
+ });
413
416
 
414
417
  res.json({
415
418
  success: true,
@@ -1,7 +1,7 @@
1
1
  const express = require('express');
2
2
  const router = express.Router();
3
3
  const notificationHooks = require('../services/notification-hooks');
4
- const { createSameOriginGuard } = require('../services/network-access');
4
+ const { createSameOriginGuard, isLoopbackRequest } = require('../services/network-access');
5
5
 
6
6
  router.use(createSameOriginGuard({
7
7
  message: '禁止跨站访问通知配置接口'
@@ -42,4 +42,20 @@ router.post('/test', async (req, res) => {
42
42
  }
43
43
  });
44
44
 
45
+ router.post('/browser-event', (req, res) => {
46
+ try {
47
+ if (!isLoopbackRequest(req)) {
48
+ return res.status(403).json({
49
+ error: '浏览器提醒事件仅允许本机触发'
50
+ });
51
+ }
52
+
53
+ notificationHooks.emitBrowserNotificationEvent(req.body || {});
54
+ res.json({ success: true });
55
+ } catch (error) {
56
+ console.error('Error emitting browser notification event:', error);
57
+ res.status(error.statusCode || 500).json({ error: error.message });
58
+ }
59
+ });
60
+
45
61
  module.exports = router;
@@ -14,6 +14,7 @@ const {
14
14
  } = require('../services/opencode-sessions');
15
15
  const { loadAliases } = require('../services/alias');
16
16
  const { broadcastLog } = require('../websocket-server');
17
+ const { buildLaunchCommand } = require('../services/session-launch-command');
17
18
  const { HOME_DIR } = require('../../config/paths');
18
19
 
19
20
  function isNotFoundError(error) {
@@ -350,9 +351,11 @@ module.exports = (config) => {
350
351
  const projects = getProjects();
351
352
  const project = projects.find(p => p.name === projectName);
352
353
  const cwd = session.directory || project?.fullPath || HOME_DIR;
353
- const command = `opencode -r ${sessionId}`;
354
- const quotedCwd = `"${String(cwd).replace(/"/g, '\\"')}"`;
355
- const copyCommand = `cd ${quotedCwd} && ${command}`;
354
+ const { command, copyCommand } = buildLaunchCommand({
355
+ cwd,
356
+ executable: 'opencode',
357
+ args: ['-r', sessionId]
358
+ });
356
359
 
357
360
  broadcastLog({
358
361
  type: 'action',
@@ -45,6 +45,24 @@ function extractRepoPayload(source = {}) {
45
45
  };
46
46
  }
47
47
 
48
+ function extractPluginQueryInfo(name, query = {}) {
49
+ return {
50
+ name,
51
+ repoId: query.repoId || '',
52
+ repoProvider: query.repoProvider || '',
53
+ repoHost: query.repoHost || '',
54
+ repoOwner: query.repoOwner || '',
55
+ repoName: query.repoName || '',
56
+ repoBranch: query.repoBranch || '',
57
+ directory: query.directory || '',
58
+ source: query.source || '',
59
+ repoUrl: query.repoUrl || '',
60
+ repoProjectPath: query.repoProjectPath || '',
61
+ repoLocalPath: query.repoLocalPath || '',
62
+ installPath: query.installPath || ''
63
+ };
64
+ }
65
+
48
66
  function sanitizeRepo(repo = {}) {
49
67
  const token = String(repo.token || '').trim();
50
68
  const sanitized = {
@@ -431,36 +449,7 @@ router.get('/:name/readme', async (req, res) => {
431
449
  try {
432
450
  const { platform, service } = getPluginsService(req);
433
451
  const { name } = req.params;
434
- const {
435
- repoId,
436
- repoProvider,
437
- repoHost,
438
- repoOwner,
439
- repoName,
440
- repoBranch,
441
- directory,
442
- source,
443
- repoUrl,
444
- repoProjectPath,
445
- repoLocalPath,
446
- installPath
447
- } = req.query;
448
-
449
- const pluginInfo = {
450
- name,
451
- repoId,
452
- repoProvider,
453
- repoHost,
454
- repoOwner,
455
- repoName,
456
- repoBranch,
457
- directory,
458
- source,
459
- repoUrl,
460
- repoProjectPath,
461
- repoLocalPath,
462
- installPath
463
- };
452
+ const pluginInfo = extractPluginQueryInfo(name, req.query);
464
453
 
465
454
  const readme = await service.getPluginReadme(pluginInfo);
466
455
 
@@ -483,12 +472,14 @@ router.get('/:name/readme', async (req, res) => {
483
472
  * 获取单个插件详情
484
473
  * GET /api/plugins/:name
485
474
  */
486
- router.get('/:name', (req, res) => {
475
+ router.get('/:name', async (req, res) => {
487
476
  try {
488
477
  const { platform, service } = getPluginsService(req);
489
478
  const { name } = req.params;
490
-
491
- const plugin = service.getPlugin(name);
479
+ const pluginInfo = extractPluginQueryInfo(name, req.query);
480
+ const plugin = typeof service.getPluginDetail === 'function'
481
+ ? await service.getPluginDetail(pluginInfo)
482
+ : service.getPlugin(name);
492
483
 
493
484
  if (!plugin) {
494
485
  return res.status(404).json({
@@ -6,6 +6,7 @@ const readline = require('readline');
6
6
  const { getSessionsForProject, deleteSession, forkSession, saveSessionOrder, parseRealProjectPath, searchSessions, getRecentSessions, searchSessionsAcrossProjects, hasActualMessages } = require('../services/sessions');
7
7
  const { loadAliases } = require('../services/alias');
8
8
  const { broadcastLog } = require('../websocket-server');
9
+ const { buildLaunchCommand } = require('../services/session-launch-command');
9
10
  const { NATIVE_PATHS } = require('../../config/paths');
10
11
  const CLAUDE_PROJECTS_DIR = NATIVE_PATHS.claude.projects;
11
12
 
@@ -570,9 +571,11 @@ module.exports = (config) => {
570
571
  timestamp: Date.now()
571
572
  });
572
573
 
573
- const command = `claude -r ${sessionId}`;
574
- const quotedCwd = `"${String(cwd).replace(/"/g, '\\"')}"`;
575
- const copyCommand = `cd ${quotedCwd} && ${command}`;
574
+ const { command, copyCommand } = buildLaunchCommand({
575
+ cwd,
576
+ executable: 'claude',
577
+ args: ['-r', sessionId]
578
+ });
576
579
 
577
580
  res.json({
578
581
  success: true,
@@ -15,6 +15,7 @@ const { getEffectiveApiKey } = require('./services/codex-channels');
15
15
  const { persistProxyRequestSnapshot } = require('./services/request-logger');
16
16
  const { publishUsageLog, publishFailureLog } = require('./services/proxy-log-helper');
17
17
  const { redirectModel, resolveTargetUrl } = require('./services/base/proxy-utils');
18
+ const { parseSSEUsage, parseNonStreamingUsage, mergeUsageIntoTokenData, createTokenData } = require('./services/base/response-usage-parser');
18
19
  const { attachServerShutdownHandling, expediteServerShutdown } = require('./services/server-shutdown');
19
20
 
20
21
  let proxyServer = null;
@@ -225,6 +226,13 @@ async function startCodexProxyServer(options = {}) {
225
226
  // 更新 rawBody 以匹配修改后的 body
226
227
  req.rawBody = Buffer.from(JSON.stringify(req.body));
227
228
 
229
+ // 将原始模型和重定向模型存入 metadata,用于日志记录
230
+ const meta = requestMetadata.get(req);
231
+ if (meta) {
232
+ meta.originalModel = originalModel;
233
+ meta.redirectedModel = redirectedModel;
234
+ }
235
+
228
236
  // 只在重定向规则变化时打印日志(避免每次请求都打印)
229
237
  const cachedRedirects = printedRedirectCache.get(channel.id) || {};
230
238
  if (cachedRedirects[originalModel] !== redirectedModel) {
@@ -324,14 +332,7 @@ async function startCodexProxyServer(options = {}) {
324
332
  });
325
333
 
326
334
  let buffer = '';
327
- let tokenData = {
328
- inputTokens: 0,
329
- outputTokens: 0,
330
- cachedTokens: 0,
331
- reasoningTokens: 0,
332
- totalTokens: 0,
333
- model: ''
334
- };
335
+ let tokenData = createTokenData();
335
336
  let usageRecorded = false;
336
337
  const parsedStream = createDecodedStream(proxyRes);
337
338
 
@@ -347,6 +348,8 @@ async function startCodexProxyServer(options = {}) {
347
348
  tokens: {
348
349
  input: tokenData.inputTokens,
349
350
  output: tokenData.outputTokens,
351
+ cacheCreation: tokenData.cacheCreation,
352
+ cacheRead: tokenData.cacheRead,
350
353
  cached: tokenData.cachedTokens,
351
354
  reasoning: tokenData.reasoningTokens,
352
355
  total: tokenData.totalTokens
@@ -355,7 +358,7 @@ async function startCodexProxyServer(options = {}) {
355
358
  broadcastLog,
356
359
  recordRequest: recordCodexRequest,
357
360
  recordSuccess,
358
- allowBroadcast: !isResponseClosed
361
+ allowBroadcast: true
359
362
  });
360
363
 
361
364
  if (!result) {
@@ -367,7 +370,6 @@ async function startCodexProxyServer(options = {}) {
367
370
  }
368
371
 
369
372
  parsedStream.on('data', (chunk) => {
370
- // 如果响应已关闭,停止处理
371
373
  if (isResponseClosed) {
372
374
  return;
373
375
  }
@@ -376,64 +378,34 @@ async function startCodexProxyServer(options = {}) {
376
378
 
377
379
  // 检查是否是 SSE 流
378
380
  if (proxyRes.headers['content-type']?.includes('text/event-stream')) {
379
- // 处理 SSE 事件
380
381
  const events = buffer.split('\n\n');
381
382
  buffer = events.pop() || '';
382
383
 
383
- events.forEach((eventText, index) => {
384
+ events.forEach((eventText) => {
384
385
  if (!eventText.trim()) return;
385
386
 
386
387
  try {
387
388
  const lines = eventText.split('\n');
389
+ let eventType = '';
388
390
  let data = '';
389
391
 
390
392
  lines.forEach(line => {
391
- if (line.startsWith('data:')) {
393
+ if (line.startsWith('event:')) {
394
+ eventType = line.substring(6).trim();
395
+ } else if (line.startsWith('data:')) {
392
396
  data = line.substring(5).trim();
393
397
  }
394
398
  });
395
399
 
396
- if (!data) return;
397
-
398
- if (data === '[DONE]') return;
400
+ if (!data || data === '[DONE]') return;
399
401
 
400
402
  const parsed = JSON.parse(data);
403
+ const usage = parseSSEUsage(parsed, eventType);
404
+ mergeUsageIntoTokenData(tokenData, usage);
401
405
 
402
- // OpenAI Responses API: 在 response.completed 事件中获取 usage
403
- if (parsed.type === 'response.completed' && parsed.response) {
404
- // 从 response 对象中提取模型和 usage
405
- if (parsed.response.model) {
406
- tokenData.model = parsed.response.model;
407
- }
408
-
409
- if (parsed.response.usage) {
410
- tokenData.inputTokens = parsed.response.usage.input_tokens || 0;
411
- tokenData.outputTokens = parsed.response.usage.output_tokens || 0;
412
- tokenData.totalTokens = parsed.response.usage.total_tokens || 0;
413
-
414
- // 提取详细信息
415
- if (parsed.response.usage.input_tokens_details) {
416
- tokenData.cachedTokens = parsed.response.usage.input_tokens_details.cached_tokens || 0;
417
- }
418
- if (parsed.response.usage.output_tokens_details) {
419
- tokenData.reasoningTokens = parsed.response.usage.output_tokens_details.reasoning_tokens || 0;
420
- }
421
- }
406
+ if (usage.isDone) {
407
+ recordUsageIfReady();
422
408
  }
423
-
424
- // 兼容其他格式:直接在顶层的 model 和 usage
425
- if (parsed.model && !tokenData.model) {
426
- tokenData.model = parsed.model;
427
- }
428
-
429
- if (parsed.usage && tokenData.inputTokens === 0) {
430
- // 兼容 Responses API 和 Chat Completions API
431
- tokenData.inputTokens = parsed.usage.input_tokens || parsed.usage.prompt_tokens || 0;
432
- tokenData.outputTokens = parsed.usage.output_tokens || parsed.usage.completion_tokens || 0;
433
- tokenData.totalTokens = parsed.usage.total_tokens || (tokenData.inputTokens + tokenData.outputTokens);
434
- }
435
-
436
- recordUsageIfReady();
437
409
  } catch (err) {
438
410
  // 忽略解析错误
439
411
  }
@@ -446,15 +418,8 @@ async function startCodexProxyServer(options = {}) {
446
418
  if (!proxyRes.headers['content-type']?.includes('text/event-stream')) {
447
419
  try {
448
420
  const parsed = JSON.parse(buffer);
449
- if (parsed.model) {
450
- tokenData.model = parsed.model;
451
- }
452
- if (parsed.usage) {
453
- // 兼容两种格式
454
- tokenData.inputTokens = parsed.usage.input_tokens || parsed.usage.prompt_tokens || 0;
455
- tokenData.outputTokens = parsed.usage.output_tokens || parsed.usage.completion_tokens || 0;
456
- tokenData.totalTokens = parsed.usage.total_tokens || (tokenData.inputTokens + tokenData.outputTokens);
457
- }
421
+ const usage = parseNonStreamingUsage(parsed);
422
+ mergeUsageIntoTokenData(tokenData, usage);
458
423
  } catch (err) {
459
424
  // 忽略解析错误
460
425
  }