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.
- package/CHANGELOG.md +7 -0
- package/README.md +8 -4
- package/dist/web/assets/{Analytics-CmN09J9U.js → Analytics-CRNCHeui.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-CeTAPmep.js → ConfigTemplates-C0erJdo2.js} +1 -1
- package/dist/web/assets/{Home-BYtCM3rK.js → Home-CL5z6Q4d.js} +1 -1
- package/dist/web/assets/{PluginManager-OAH1eMO0.js → PluginManager-hDx0XMO_.js} +1 -1
- package/dist/web/assets/{ProjectList-B0pIy1cv.js → ProjectList-BNsz96av.js} +1 -1
- package/dist/web/assets/{SessionList-DbB6ASiA.js → SessionList-CG1UhFo3.js} +1 -1
- package/dist/web/assets/{SkillManager-wp1dhL1z.js → SkillManager-D6Vwpajh.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-Ce6wQoKb.js → WorkspaceManager-C3TjeOPy.js} +1 -1
- package/dist/web/assets/{icons-DlxD2wZJ.js → icons-CQuif85v.js} +1 -1
- package/dist/web/assets/index-GuER-BmS.js +2 -0
- package/dist/web/assets/{index-B02wDWNC.css → index-VGAxnLqi.css} +1 -1
- package/dist/web/index.html +3 -3
- package/package.json +1 -1
- package/src/commands/stats.js +41 -4
- package/src/index.js +1 -0
- package/src/server/api/codex-sessions.js +6 -3
- package/src/server/api/dashboard.js +25 -1
- package/src/server/api/gemini-sessions.js +6 -3
- package/src/server/api/hooks.js +17 -1
- package/src/server/api/opencode-sessions.js +6 -3
- package/src/server/api/plugins.js +24 -33
- package/src/server/api/sessions.js +6 -3
- package/src/server/codex-proxy-server.js +24 -59
- package/src/server/gemini-proxy-server.js +25 -66
- package/src/server/index.js +6 -4
- package/src/server/opencode-proxy-server.js +24 -59
- package/src/server/proxy-server.js +18 -30
- package/src/server/services/base/response-usage-parser.js +187 -0
- package/src/server/services/codex-sessions.js +107 -9
- package/src/server/services/network-access.js +14 -0
- package/src/server/services/notification-hooks.js +175 -16
- package/src/server/services/plugins-service.js +502 -44
- package/src/server/services/proxy-log-helper.js +21 -3
- package/src/server/services/session-launch-command.js +81 -0
- package/src/server/services/sessions.js +103 -33
- package/src/server/services/statistics-service.js +7 -0
- package/src/server/websocket-server.js +25 -1
- package/dist/web/assets/index-CHwVofQH.js +0 -2
package/dist/web/index.html
CHANGED
|
@@ -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-
|
|
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-
|
|
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-
|
|
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
package/src/commands/stats.js
CHANGED
|
@@ -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
|
|
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
|
|
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 =
|
|
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
|
|
519
|
-
|
|
520
|
-
|
|
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
|
|
411
|
-
|
|
412
|
-
|
|
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,
|
package/src/server/api/hooks.js
CHANGED
|
@@ -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
|
|
354
|
-
|
|
355
|
-
|
|
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.
|
|
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
|
|
574
|
-
|
|
575
|
-
|
|
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:
|
|
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
|
|
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('
|
|
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
|
-
|
|
403
|
-
|
|
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
|
-
|
|
450
|
-
|
|
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
|
}
|