coding-tool-x 3.5.10 → 3.5.12
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 +14 -1
- package/README.md +11 -1
- package/dist/web/assets/{Analytics-DPb1zErn.js → Analytics-GuTri2mr.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-Bki2k4Lo.js → ConfigTemplates-DILbDhRj.js} +1 -1
- package/dist/web/assets/Home-Bm7mK2qz.css +1 -0
- package/dist/web/assets/Home-ri7cXeJp.js +1 -0
- package/dist/web/assets/{PluginManager-jgeQOE24.js → PluginManager-DQPeUFkr.js} +1 -1
- package/dist/web/assets/{ProjectList-CF67RzP_.js → ProjectList-Cpf82Lgb.js} +1 -1
- package/dist/web/assets/SessionList-D12muXvx.css +1 -0
- package/dist/web/assets/{SessionList-DhHsY5q9.js → SessionList-DXhPIwJI.js} +1 -1
- package/dist/web/assets/{SkillManager-CtEz5gGp.js → SkillManager-DNVyDlEv.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-D3B0Y5XE.js → WorkspaceManager-Cp0t7cjn.js} +1 -1
- package/dist/web/assets/icons-QFXzEgqM.js +1 -0
- package/dist/web/assets/index-C5wcoiDy.js +2 -0
- package/dist/web/assets/{index-BS7aplYp.css → index-DfikIh5S.css} +1 -1
- package/dist/web/index.html +4 -4
- package/package.json +2 -1
- package/src/commands/daemon.js +6 -2
- package/src/commands/ui.js +8 -3
- package/src/index.js +2 -0
- package/src/server/api/codex-channels.js +4 -0
- package/src/server/api/codex-proxy.js +42 -1
- package/src/server/api/hooks.js +18 -1
- package/src/server/codex-proxy-server.js +22 -6
- package/src/server/gemini-proxy-server.js +3 -1
- package/src/server/index.js +37 -5
- package/src/server/opencode-proxy-server.js +27 -8
- package/src/server/proxy-server.js +3 -1
- package/src/server/services/base/proxy-utils.js +41 -0
- package/src/server/services/base/response-usage-parser.js +109 -58
- package/src/server/services/codex-sessions.js +13 -3
- package/src/server/services/codex-statistics-service.js +1 -0
- package/src/server/services/gemini-sessions.js +12 -4
- package/src/server/services/gemini-statistics-service.js +1 -0
- package/src/server/services/https-cert.js +202 -0
- package/src/server/services/notification-hooks.js +138 -6
- package/src/server/services/opencode-gateway-converter.js +3 -0
- package/src/server/services/opencode-statistics-service.js +1 -0
- package/src/server/services/proxy-log-helper.js +44 -12
- package/src/server/services/sessions.js +13 -3
- package/src/server/services/statistics-service.js +73 -28
- package/src/server/services/web-build.js +172 -0
- package/src/server/websocket-server.js +18 -1
- package/dist/web/assets/Home-BKYHKSax.js +0 -1
- package/dist/web/assets/Home-BQxQ1LhR.css +0 -1
- package/dist/web/assets/SessionList-lz23U6ZJ.css +0 -1
- package/dist/web/assets/icons-lKCTySl7.js +0 -1
- package/dist/web/assets/index-BnyNm74G.js +0 -2
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
* - OpenAI Chat Completions API (/v1/chat/completions)
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
const { ensureOpenAiStreamUsage } = require('./base/proxy-utils');
|
|
15
|
+
|
|
14
16
|
const SUPPORTED_SOURCE_TYPES = ['claude', 'codex', 'gemini'];
|
|
15
17
|
const SUPPORTED_TARGET_APIS = ['responses', 'chat.completions'];
|
|
16
18
|
|
|
@@ -554,6 +556,7 @@ function buildChatCompletionsRequest(normalized, options, warnings) {
|
|
|
554
556
|
if (chatTools.length > 0) request.tools = chatTools;
|
|
555
557
|
}
|
|
556
558
|
|
|
559
|
+
ensureOpenAiStreamUsage(request);
|
|
557
560
|
return request;
|
|
558
561
|
}
|
|
559
562
|
|
|
@@ -110,6 +110,7 @@ function recordRequest(requestData = {}) {
|
|
|
110
110
|
input: normalizedTokens.input,
|
|
111
111
|
output: normalizedTokens.output,
|
|
112
112
|
reasoning: normalizedTokens.reasoning,
|
|
113
|
+
cached: normalizedTokens.cached,
|
|
113
114
|
cacheCreation: normalizedTokens.cacheCreation,
|
|
114
115
|
cacheRead: normalizedTokens.cacheRead,
|
|
115
116
|
total: normalizedTokens.total
|
|
@@ -12,6 +12,30 @@ function normalizeToolSource(source = '') {
|
|
|
12
12
|
return 'claude';
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
function normalizeModelName(value = '') {
|
|
16
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function resolveActualModel(model = '', metadata = {}) {
|
|
20
|
+
const candidates = [
|
|
21
|
+
model,
|
|
22
|
+
metadata.redirectedModel,
|
|
23
|
+
metadata.modelFromUrl,
|
|
24
|
+
metadata.requestModel,
|
|
25
|
+
metadata.model,
|
|
26
|
+
metadata.originalModel
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
for (const candidate of candidates) {
|
|
30
|
+
const normalized = normalizeModelName(candidate);
|
|
31
|
+
if (normalized) {
|
|
32
|
+
return normalized;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return '';
|
|
37
|
+
}
|
|
38
|
+
|
|
15
39
|
function normalizeUsageTokens(source, tokens = {}) {
|
|
16
40
|
const normalizedSource = normalizeToolSource(source);
|
|
17
41
|
const input = toNumber(tokens.input);
|
|
@@ -68,7 +92,8 @@ function buildSuccessLogPayload({
|
|
|
68
92
|
redirectedModel,
|
|
69
93
|
tokens,
|
|
70
94
|
cost = 0,
|
|
71
|
-
timestamp = Date.now()
|
|
95
|
+
timestamp = Date.now(),
|
|
96
|
+
usageMissing = false
|
|
72
97
|
}) {
|
|
73
98
|
const normalized = normalizeUsageTokens(source, tokens);
|
|
74
99
|
const payload = {
|
|
@@ -87,7 +112,8 @@ function buildSuccessLogPayload({
|
|
|
87
112
|
totalTokens: normalized.total,
|
|
88
113
|
cost,
|
|
89
114
|
source: normalizeToolSource(source),
|
|
90
|
-
timestamp
|
|
115
|
+
timestamp,
|
|
116
|
+
usageMissing: Boolean(usageMissing)
|
|
91
117
|
};
|
|
92
118
|
if (originalModel) {
|
|
93
119
|
payload.originalModel = originalModel;
|
|
@@ -151,40 +177,43 @@ function publishUsageLog({
|
|
|
151
177
|
recordSuccess,
|
|
152
178
|
allowBroadcast = true
|
|
153
179
|
}) {
|
|
154
|
-
if (!hasMeaningfulUsage(source, tokens)) {
|
|
155
|
-
return null;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
180
|
const normalizedSource = normalizeToolSource(source);
|
|
181
|
+
const actualModel = resolveActualModel(model, metadata);
|
|
159
182
|
const normalizedTokens = normalizeUsageTokens(normalizedSource, tokens);
|
|
183
|
+
const usageAvailable = hasMeaningfulUsage(normalizedSource, normalizedTokens);
|
|
160
184
|
const requestId = metadata.id || `${normalizedSource}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
161
185
|
const timestamp = Date.now();
|
|
162
186
|
const cost = typeof calculateCost === 'function'
|
|
163
|
-
? calculateCost(
|
|
187
|
+
? calculateCost(actualModel || '', normalizedTokens)
|
|
164
188
|
: 0;
|
|
165
189
|
|
|
190
|
+
if (!usageAvailable && !actualModel) {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
|
|
166
194
|
if (allowBroadcast && typeof broadcastLog === 'function') {
|
|
167
195
|
broadcastLog(buildSuccessLogPayload({
|
|
168
196
|
source: normalizedSource,
|
|
169
197
|
requestId,
|
|
170
198
|
channel: metadata.channel,
|
|
171
|
-
model,
|
|
199
|
+
model: actualModel,
|
|
172
200
|
originalModel: metadata.originalModel,
|
|
173
201
|
redirectedModel: metadata.redirectedModel,
|
|
174
202
|
tokens: normalizedTokens,
|
|
175
203
|
cost,
|
|
176
|
-
timestamp
|
|
204
|
+
timestamp,
|
|
205
|
+
usageMissing: !usageAvailable
|
|
177
206
|
}));
|
|
178
207
|
}
|
|
179
208
|
|
|
180
|
-
if (typeof recordRequest === 'function') {
|
|
209
|
+
if (usageAvailable && typeof recordRequest === 'function') {
|
|
181
210
|
const entry = {
|
|
182
211
|
id: requestId,
|
|
183
212
|
timestamp: new Date(metadata.startTime || timestamp).toISOString(),
|
|
184
213
|
toolType: normalizedSource === 'claude' ? 'claude-code' : normalizedSource,
|
|
185
214
|
channel: metadata.channel,
|
|
186
215
|
channelId: metadata.channelId,
|
|
187
|
-
model:
|
|
216
|
+
model: actualModel || '',
|
|
188
217
|
tokens: {
|
|
189
218
|
input: normalizedTokens.input,
|
|
190
219
|
output: normalizedTokens.output,
|
|
@@ -213,8 +242,10 @@ function publishUsageLog({
|
|
|
213
242
|
|
|
214
243
|
return {
|
|
215
244
|
cost,
|
|
245
|
+
model: actualModel,
|
|
216
246
|
tokens: normalizedTokens,
|
|
217
|
-
timestamp
|
|
247
|
+
timestamp,
|
|
248
|
+
usageMissing: !usageAvailable
|
|
218
249
|
};
|
|
219
250
|
}
|
|
220
251
|
|
|
@@ -250,6 +281,7 @@ function publishFailureLog({
|
|
|
250
281
|
module.exports = {
|
|
251
282
|
toNumber,
|
|
252
283
|
normalizeToolSource,
|
|
284
|
+
resolveActualModel,
|
|
253
285
|
normalizeUsageTokens,
|
|
254
286
|
hasMeaningfulUsage,
|
|
255
287
|
formatRealtimeTime,
|
|
@@ -135,6 +135,7 @@ function sliceClaudeSessionContentByUserMessage(content, afterUserMessageNumber)
|
|
|
135
135
|
const { lines, eol, hasTrailingEol } = splitTextPreserveEol(content);
|
|
136
136
|
const keptLines = [];
|
|
137
137
|
let matchedUserMessages = 0;
|
|
138
|
+
let targetUserReached = false;
|
|
138
139
|
|
|
139
140
|
for (const line of lines) {
|
|
140
141
|
if (!line.trim()) {
|
|
@@ -150,23 +151,32 @@ function sliceClaudeSessionContentByUserMessage(content, afterUserMessageNumber)
|
|
|
150
151
|
continue;
|
|
151
152
|
}
|
|
152
153
|
|
|
153
|
-
keptLines.push(line);
|
|
154
|
-
|
|
155
154
|
if (json.type !== 'user') {
|
|
155
|
+
keptLines.push(line);
|
|
156
156
|
continue;
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
const userText = getClaudeForkMessageText(json.message?.content);
|
|
160
160
|
if (!userText) {
|
|
161
|
+
keptLines.push(line);
|
|
161
162
|
continue;
|
|
162
163
|
}
|
|
163
164
|
|
|
165
|
+
if (targetUserReached) {
|
|
166
|
+
return joinTextPreserveEol(keptLines, eol, hasTrailingEol);
|
|
167
|
+
}
|
|
168
|
+
|
|
164
169
|
matchedUserMessages += 1;
|
|
170
|
+
keptLines.push(line);
|
|
165
171
|
if (matchedUserMessages >= afterUserMessageNumber) {
|
|
166
|
-
|
|
172
|
+
targetUserReached = true;
|
|
167
173
|
}
|
|
168
174
|
}
|
|
169
175
|
|
|
176
|
+
if (targetUserReached) {
|
|
177
|
+
return joinTextPreserveEol(keptLines, eol, hasTrailingEol);
|
|
178
|
+
}
|
|
179
|
+
|
|
170
180
|
throw new Error(`afterUserMessageNumber ${afterUserMessageNumber} exceeds available user messages (${matchedUserMessages})`);
|
|
171
181
|
}
|
|
172
182
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const { PATHS } = require('../../config/paths');
|
|
4
|
+
const { normalizeUsageTokens, resolveActualModel } = require('./proxy-log-helper');
|
|
4
5
|
|
|
5
6
|
// 北京时间辅助(UTC+8),统一所有时间计算
|
|
6
7
|
const CST_OFFSET_MS = 8 * 60 * 60 * 1000;
|
|
@@ -189,6 +190,8 @@ function initStatsObject() {
|
|
|
189
190
|
output: 0,
|
|
190
191
|
cacheCreation: 0,
|
|
191
192
|
cacheRead: 0,
|
|
193
|
+
cached: 0,
|
|
194
|
+
reasoning: 0,
|
|
192
195
|
total: 0
|
|
193
196
|
},
|
|
194
197
|
cost: 0
|
|
@@ -202,7 +205,9 @@ function updateStats(stats, tokens, cost) {
|
|
|
202
205
|
stats.tokens.output += tokens.output || 0;
|
|
203
206
|
stats.tokens.cacheCreation += tokens.cacheCreation || 0;
|
|
204
207
|
stats.tokens.cacheRead += tokens.cacheRead || 0;
|
|
205
|
-
stats.tokens.
|
|
208
|
+
stats.tokens.cached += tokens.cached || 0;
|
|
209
|
+
stats.tokens.reasoning += tokens.reasoning || 0;
|
|
210
|
+
stats.tokens.total += getTokenTotal(tokens);
|
|
206
211
|
stats.cost += cost || 0;
|
|
207
212
|
}
|
|
208
213
|
|
|
@@ -226,6 +231,9 @@ function recordRequest(requestData) {
|
|
|
226
231
|
session,
|
|
227
232
|
project
|
|
228
233
|
} = requestData;
|
|
234
|
+
const resolvedModel = resolveActualModel(model, requestData);
|
|
235
|
+
const modelKey = resolvedModel || 'unknown';
|
|
236
|
+
const totalTokens = getTokenTotal(tokens);
|
|
229
237
|
|
|
230
238
|
// 1. 写入详细日志
|
|
231
239
|
const logEntry = {
|
|
@@ -234,7 +242,7 @@ function recordRequest(requestData) {
|
|
|
234
242
|
toolType,
|
|
235
243
|
channel,
|
|
236
244
|
channelId,
|
|
237
|
-
model,
|
|
245
|
+
model: resolvedModel,
|
|
238
246
|
tokens,
|
|
239
247
|
duration,
|
|
240
248
|
success,
|
|
@@ -256,7 +264,7 @@ function recordRequest(requestData) {
|
|
|
256
264
|
|
|
257
265
|
// 更新全局统计
|
|
258
266
|
globalStats.global.totalRequests += 1;
|
|
259
|
-
globalStats.global.totalTokens +=
|
|
267
|
+
globalStats.global.totalTokens += totalTokens;
|
|
260
268
|
globalStats.global.totalCost += cost || 0;
|
|
261
269
|
|
|
262
270
|
// 按工具类型统计
|
|
@@ -283,10 +291,10 @@ function recordRequest(requestData) {
|
|
|
283
291
|
updateStats(globalStats.byToolType[toolType].channels[channelId], tokens, cost);
|
|
284
292
|
|
|
285
293
|
// 按工具类型 -> 模型统计
|
|
286
|
-
if (!globalStats.byToolType[toolType].models[
|
|
287
|
-
globalStats.byToolType[toolType].models[
|
|
294
|
+
if (!globalStats.byToolType[toolType].models[modelKey]) {
|
|
295
|
+
globalStats.byToolType[toolType].models[modelKey] = initStatsObject();
|
|
288
296
|
}
|
|
289
|
-
updateStats(globalStats.byToolType[toolType].models[
|
|
297
|
+
updateStats(globalStats.byToolType[toolType].models[modelKey], tokens, cost);
|
|
290
298
|
|
|
291
299
|
// 按渠道统计(跨工具)
|
|
292
300
|
if (!globalStats.byChannel[channelId]) {
|
|
@@ -303,13 +311,13 @@ function recordRequest(requestData) {
|
|
|
303
311
|
updateStats(globalStats.byChannel[channelId], tokens, cost);
|
|
304
312
|
|
|
305
313
|
// 按模型统计(跨工具)
|
|
306
|
-
if (!globalStats.byModel[
|
|
307
|
-
globalStats.byModel[
|
|
314
|
+
if (!globalStats.byModel[modelKey]) {
|
|
315
|
+
globalStats.byModel[modelKey] = {
|
|
308
316
|
toolType,
|
|
309
317
|
...initStatsObject()
|
|
310
318
|
};
|
|
311
319
|
}
|
|
312
|
-
updateStats(globalStats.byModel[
|
|
320
|
+
updateStats(globalStats.byModel[modelKey], tokens, cost);
|
|
313
321
|
|
|
314
322
|
saveStatistics(globalStats);
|
|
315
323
|
|
|
@@ -321,7 +329,7 @@ function recordRequest(requestData) {
|
|
|
321
329
|
|
|
322
330
|
// 更新每日汇总
|
|
323
331
|
dailyStats.summary.requests += 1;
|
|
324
|
-
dailyStats.summary.tokens +=
|
|
332
|
+
dailyStats.summary.tokens += totalTokens;
|
|
325
333
|
dailyStats.summary.cost += cost || 0;
|
|
326
334
|
|
|
327
335
|
// 按小时统计
|
|
@@ -361,6 +369,15 @@ function recordRequest(requestData) {
|
|
|
361
369
|
}
|
|
362
370
|
updateStats(dailyStats.byToolType[toolType].channels[channelId], tokens, cost);
|
|
363
371
|
|
|
372
|
+
// 按工具类型 -> 模型
|
|
373
|
+
if (!dailyStats.byToolType[toolType].models) {
|
|
374
|
+
dailyStats.byToolType[toolType].models = {};
|
|
375
|
+
}
|
|
376
|
+
if (!dailyStats.byToolType[toolType].models[modelKey]) {
|
|
377
|
+
dailyStats.byToolType[toolType].models[modelKey] = initStatsObject();
|
|
378
|
+
}
|
|
379
|
+
updateStats(dailyStats.byToolType[toolType].models[modelKey], tokens, cost);
|
|
380
|
+
|
|
364
381
|
// 按渠道统计
|
|
365
382
|
if (!dailyStats.byChannel[channelId]) {
|
|
366
383
|
dailyStats.byChannel[channelId] = {
|
|
@@ -372,13 +389,13 @@ function recordRequest(requestData) {
|
|
|
372
389
|
updateStats(dailyStats.byChannel[channelId], tokens, cost);
|
|
373
390
|
|
|
374
391
|
// 按模型统计
|
|
375
|
-
if (!dailyStats.byModel[
|
|
376
|
-
dailyStats.byModel[
|
|
392
|
+
if (!dailyStats.byModel[modelKey]) {
|
|
393
|
+
dailyStats.byModel[modelKey] = {
|
|
377
394
|
toolType,
|
|
378
395
|
...initStatsObject()
|
|
379
396
|
};
|
|
380
397
|
}
|
|
381
|
-
updateStats(dailyStats.byModel[
|
|
398
|
+
updateStats(dailyStats.byModel[modelKey], tokens, cost);
|
|
382
399
|
|
|
383
400
|
saveDailyStats(date, dailyStats);
|
|
384
401
|
|
|
@@ -447,21 +464,23 @@ function readJsonlForHour(year, month, day, hour, groupBy, filters) {
|
|
|
447
464
|
const ts = new Date(entry.timestamp);
|
|
448
465
|
if (getCSTHour(ts) !== hour) continue;
|
|
449
466
|
|
|
467
|
+
const actualModel = resolveActualModel(entry.model, entry);
|
|
468
|
+
|
|
450
469
|
// Apply filters
|
|
451
470
|
if (filters) {
|
|
452
471
|
if (filters.toolType && entry.toolType !== filters.toolType) continue;
|
|
453
472
|
if (filters.channel && entry.channel !== filters.channel) continue;
|
|
454
|
-
if (filters.model &&
|
|
473
|
+
if (filters.model && actualModel !== filters.model) continue;
|
|
455
474
|
}
|
|
456
475
|
|
|
457
476
|
let key;
|
|
458
|
-
if (groupBy === 'model') key =
|
|
477
|
+
if (groupBy === 'model') key = actualModel || 'unknown';
|
|
459
478
|
else if (groupBy === 'channel') key = entry.channel || entry.channelId || 'unknown';
|
|
460
479
|
else if (groupBy === 'toolType') key = entry.toolType || 'claude-code';
|
|
461
480
|
else continue;
|
|
462
481
|
|
|
463
482
|
if (!result[key]) result[key] = { tokens: { total: 0 }, cost: 0, requests: 0 };
|
|
464
|
-
result[key].tokens.total += entry.tokens
|
|
483
|
+
result[key].tokens.total += getTokenTotal(entry.tokens);
|
|
465
484
|
result[key].cost += entry.cost || 0;
|
|
466
485
|
result[key].requests += 1;
|
|
467
486
|
}
|
|
@@ -493,21 +512,23 @@ function readJsonlForDay(year, month, day, groupBy, filters) {
|
|
|
493
512
|
let entry;
|
|
494
513
|
try { entry = JSON.parse(line); } catch { continue; }
|
|
495
514
|
|
|
515
|
+
const actualModel = resolveActualModel(entry.model, entry);
|
|
516
|
+
|
|
496
517
|
// Apply filters
|
|
497
518
|
if (filters) {
|
|
498
519
|
if (filters.toolType && entry.toolType !== filters.toolType) continue;
|
|
499
520
|
if (filters.channel && entry.channel !== filters.channel) continue;
|
|
500
|
-
if (filters.model &&
|
|
521
|
+
if (filters.model && actualModel !== filters.model) continue;
|
|
501
522
|
}
|
|
502
523
|
|
|
503
524
|
let key;
|
|
504
|
-
if (groupBy === 'model') key =
|
|
525
|
+
if (groupBy === 'model') key = actualModel || 'unknown';
|
|
505
526
|
else if (groupBy === 'channel') key = entry.channel || entry.channelId || 'unknown';
|
|
506
527
|
else if (groupBy === 'toolType') key = entry.toolType || 'claude-code';
|
|
507
528
|
else continue;
|
|
508
529
|
|
|
509
530
|
if (!result[key]) result[key] = { tokens: { total: 0 }, cost: 0, requests: 0 };
|
|
510
|
-
result[key].tokens.total += entry.tokens
|
|
531
|
+
result[key].tokens.total += getTokenTotal(entry.tokens);
|
|
511
532
|
result[key].cost += entry.cost || 0;
|
|
512
533
|
result[key].requests += 1;
|
|
513
534
|
}
|
|
@@ -541,7 +562,7 @@ function filterProxyLogsForHour(logs, dateStr, hour, groupBy) {
|
|
|
541
562
|
const result = {};
|
|
542
563
|
|
|
543
564
|
for (const entry of logs) {
|
|
544
|
-
if (!entry || entry.type === 'action') continue;
|
|
565
|
+
if (!entry || entry.type === 'action' || entry.status === 'error') continue;
|
|
545
566
|
|
|
546
567
|
const ts = new Date(entry.timestamp || Date.now());
|
|
547
568
|
if (Number.isNaN(ts.getTime())) continue;
|
|
@@ -549,11 +570,22 @@ function filterProxyLogsForHour(logs, dateStr, hour, groupBy) {
|
|
|
549
570
|
const entryDate = getCSTDateStr(ts);
|
|
550
571
|
if (entryDate !== dateStr || getCSTHour(ts) !== hour) continue;
|
|
551
572
|
|
|
573
|
+
const actualModel = resolveActualModel(entry.model, entry);
|
|
574
|
+
const normalizedTokens = normalizeUsageTokens(entry.source || mapSourceToToolType(entry.source), {
|
|
575
|
+
input: entry.inputTokens ?? entry.tokens?.input,
|
|
576
|
+
output: entry.outputTokens ?? entry.tokens?.output,
|
|
577
|
+
cacheCreation: entry.cacheCreation ?? entry.tokens?.cacheCreation,
|
|
578
|
+
cacheRead: entry.cacheRead ?? entry.tokens?.cacheRead,
|
|
579
|
+
cached: entry.cachedTokens ?? entry.tokens?.cached,
|
|
580
|
+
reasoning: entry.reasoningTokens ?? entry.tokens?.reasoning,
|
|
581
|
+
total: entry.totalTokens ?? entry.tokens?.total
|
|
582
|
+
});
|
|
583
|
+
|
|
552
584
|
let key;
|
|
553
585
|
if (groupBy === 'toolType') {
|
|
554
586
|
key = mapSourceToToolType(entry.source);
|
|
555
587
|
} else if (groupBy === 'model') {
|
|
556
|
-
key =
|
|
588
|
+
key = actualModel || 'unknown';
|
|
557
589
|
} else if (groupBy === 'channel') {
|
|
558
590
|
key = entry.channel || 'unknown';
|
|
559
591
|
} else {
|
|
@@ -564,11 +596,7 @@ function filterProxyLogsForHour(logs, dateStr, hour, groupBy) {
|
|
|
564
596
|
result[key] = { tokens: { total: 0 }, cost: 0, requests: 0 };
|
|
565
597
|
}
|
|
566
598
|
|
|
567
|
-
|
|
568
|
-
entry.tokens?.total ||
|
|
569
|
-
(entry.inputTokens || 0) + (entry.outputTokens || 0) + (entry.reasoningTokens || 0);
|
|
570
|
-
|
|
571
|
-
result[key].tokens.total += totalTokens || 0;
|
|
599
|
+
result[key].tokens.total += normalizedTokens.total || 0;
|
|
572
600
|
result[key].cost += entry.cost || 0;
|
|
573
601
|
result[key].requests += 1;
|
|
574
602
|
}
|
|
@@ -741,7 +769,7 @@ function aggregateDailyStatistics(dateStr) {
|
|
|
741
769
|
}
|
|
742
770
|
|
|
743
771
|
for (const [modelName, modelStats] of Object.entries(toolStats.models || {})) {
|
|
744
|
-
const key =
|
|
772
|
+
const key = modelName || 'unknown';
|
|
745
773
|
if (!aggregated.byModel[key]) {
|
|
746
774
|
aggregated.byModel[key] = createEmptyEntry(toolType);
|
|
747
775
|
}
|
|
@@ -835,6 +863,7 @@ function getAvailableFilters(startDate, endDate) {
|
|
|
835
863
|
const toolTypes = new Set();
|
|
836
864
|
const channels = new Set();
|
|
837
865
|
const models = new Set();
|
|
866
|
+
const includeProxyLogs = startDate <= getCSTDateStr(Date.now()) && getCSTDateStr(Date.now()) <= endDate;
|
|
838
867
|
|
|
839
868
|
const start = new Date(startDate + 'T00:00:00');
|
|
840
869
|
const end = new Date(endDate + 'T00:00:00');
|
|
@@ -852,15 +881,31 @@ function getAvailableFilters(startDate, endDate) {
|
|
|
852
881
|
if (!line.trim()) continue;
|
|
853
882
|
let entry;
|
|
854
883
|
try { entry = JSON.parse(line); } catch { continue; }
|
|
884
|
+
const actualModel = resolveActualModel(entry.model, entry);
|
|
855
885
|
if (entry.toolType) toolTypes.add(entry.toolType);
|
|
856
886
|
if (entry.channel) channels.add(entry.channel);
|
|
857
|
-
if (
|
|
887
|
+
if (actualModel) models.add(actualModel);
|
|
858
888
|
}
|
|
859
889
|
} catch (err) {
|
|
860
890
|
console.error('Failed to scan JSONL for filters:', err);
|
|
861
891
|
}
|
|
862
892
|
}
|
|
863
893
|
|
|
894
|
+
if (includeProxyLogs) {
|
|
895
|
+
for (const entry of loadProxyLogs()) {
|
|
896
|
+
if (!entry || entry.type === 'action' || entry.status === 'error') continue;
|
|
897
|
+
const ts = new Date(entry.timestamp || Date.now());
|
|
898
|
+
if (Number.isNaN(ts.getTime())) continue;
|
|
899
|
+
const entryDate = getCSTDateStr(ts);
|
|
900
|
+
if (entryDate < startDate || entryDate > endDate) continue;
|
|
901
|
+
|
|
902
|
+
toolTypes.add(mapSourceToToolType(entry.source));
|
|
903
|
+
if (entry.channel) channels.add(entry.channel);
|
|
904
|
+
const actualModel = resolveActualModel(entry.model, entry);
|
|
905
|
+
if (actualModel) models.add(actualModel);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
864
909
|
return {
|
|
865
910
|
toolTypes: Array.from(toolTypes).sort(),
|
|
866
911
|
channels: Array.from(channels).sort(),
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { spawn } = require('child_process');
|
|
6
|
+
|
|
7
|
+
const DEFAULT_PROJECT_ROOT = path.resolve(__dirname, '../../..');
|
|
8
|
+
const IGNORED_DIR_NAMES = new Set(['node_modules', '.git', 'dist']);
|
|
9
|
+
const WATCHED_WEB_FILES = ['index.html', 'package.json', 'package-lock.json', 'pnpm-lock.yaml', 'vite.config.js'];
|
|
10
|
+
const WATCHED_WEB_DIRS = ['src', 'public'];
|
|
11
|
+
|
|
12
|
+
function pathExists(targetPath) {
|
|
13
|
+
return fs.existsSync(targetPath);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getLatestMtimeMs(targetPath) {
|
|
17
|
+
if (!pathExists(targetPath)) {
|
|
18
|
+
return 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const stat = fs.statSync(targetPath);
|
|
22
|
+
if (!stat.isDirectory()) {
|
|
23
|
+
return stat.mtimeMs;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let latest = stat.mtimeMs;
|
|
27
|
+
for (const entry of fs.readdirSync(targetPath, { withFileTypes: true })) {
|
|
28
|
+
if (entry.isDirectory() && IGNORED_DIR_NAMES.has(entry.name)) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const childPath = path.join(targetPath, entry.name);
|
|
32
|
+
latest = Math.max(latest, getLatestMtimeMs(childPath));
|
|
33
|
+
}
|
|
34
|
+
return latest;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getWatchedWebSourceMtimeMs(sourceRoot) {
|
|
38
|
+
let latest = 0;
|
|
39
|
+
for (const fileName of WATCHED_WEB_FILES) {
|
|
40
|
+
latest = Math.max(latest, getLatestMtimeMs(path.join(sourceRoot, fileName)));
|
|
41
|
+
}
|
|
42
|
+
for (const dirName of WATCHED_WEB_DIRS) {
|
|
43
|
+
latest = Math.max(latest, getLatestMtimeMs(path.join(sourceRoot, dirName)));
|
|
44
|
+
}
|
|
45
|
+
return latest;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function inspectWebBuildState(options = {}) {
|
|
49
|
+
const projectRoot = options.projectRoot || DEFAULT_PROJECT_ROOT;
|
|
50
|
+
const sourceRoot = options.sourceRoot || path.join(projectRoot, 'src/web');
|
|
51
|
+
const distRoot = options.distRoot || path.join(projectRoot, 'dist/web');
|
|
52
|
+
const distIndexPath = path.join(distRoot, 'index.html');
|
|
53
|
+
const sourceAvailable = pathExists(sourceRoot) && pathExists(path.join(sourceRoot, 'package.json'));
|
|
54
|
+
const distExists = pathExists(distIndexPath);
|
|
55
|
+
|
|
56
|
+
if (!sourceAvailable) {
|
|
57
|
+
return {
|
|
58
|
+
projectRoot,
|
|
59
|
+
sourceRoot,
|
|
60
|
+
distRoot,
|
|
61
|
+
distIndexPath,
|
|
62
|
+
sourceAvailable: false,
|
|
63
|
+
distExists,
|
|
64
|
+
sourceMtimeMs: 0,
|
|
65
|
+
distMtimeMs: getLatestMtimeMs(distRoot),
|
|
66
|
+
needsBuild: false,
|
|
67
|
+
reason: 'source-unavailable'
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const sourceMtimeMs = getWatchedWebSourceMtimeMs(sourceRoot);
|
|
72
|
+
const distMtimeMs = getLatestMtimeMs(distRoot);
|
|
73
|
+
const hasDistAssets = distExists && distMtimeMs > 0;
|
|
74
|
+
const isStale = hasDistAssets && sourceMtimeMs > distMtimeMs + 1;
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
projectRoot,
|
|
78
|
+
sourceRoot,
|
|
79
|
+
distRoot,
|
|
80
|
+
distIndexPath,
|
|
81
|
+
sourceAvailable: true,
|
|
82
|
+
distExists,
|
|
83
|
+
sourceMtimeMs,
|
|
84
|
+
distMtimeMs,
|
|
85
|
+
needsBuild: !hasDistAssets || isStale,
|
|
86
|
+
reason: !hasDistAssets ? 'dist-missing' : (isStale ? 'dist-stale' : 'up-to-date')
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function getNpmCommand() {
|
|
91
|
+
return process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function runWebBuild(options = {}) {
|
|
95
|
+
const sourceRoot = options.sourceRoot || path.join(DEFAULT_PROJECT_ROOT, 'src/web');
|
|
96
|
+
const npmCommand = options.npmCommand || getNpmCommand();
|
|
97
|
+
const env = options.env || process.env;
|
|
98
|
+
const stdio = options.stdio || 'inherit';
|
|
99
|
+
|
|
100
|
+
return new Promise((resolve, reject) => {
|
|
101
|
+
const child = spawn(npmCommand, ['run', 'build'], {
|
|
102
|
+
cwd: sourceRoot,
|
|
103
|
+
env,
|
|
104
|
+
stdio
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
child.once('error', (error) => {
|
|
108
|
+
reject(error);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
child.once('close', (code) => {
|
|
112
|
+
if (code === 0) {
|
|
113
|
+
resolve();
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
reject(new Error(`Web UI build exited with code ${code}`));
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function ensureWebDistReady(options = {}) {
|
|
122
|
+
const initialState = options.state || inspectWebBuildState(options);
|
|
123
|
+
if (!initialState.needsBuild) {
|
|
124
|
+
return {
|
|
125
|
+
...initialState,
|
|
126
|
+
built: false,
|
|
127
|
+
skipped: true,
|
|
128
|
+
previousReason: initialState.reason
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const runBuild = options.runBuild || runWebBuild;
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
await runBuild({
|
|
136
|
+
sourceRoot: initialState.sourceRoot,
|
|
137
|
+
distRoot: initialState.distRoot,
|
|
138
|
+
npmCommand: options.npmCommand || getNpmCommand(),
|
|
139
|
+
env: options.env,
|
|
140
|
+
stdio: options.stdio
|
|
141
|
+
});
|
|
142
|
+
} catch (error) {
|
|
143
|
+
const dependencyHint = pathExists(path.join(initialState.sourceRoot, 'node_modules'))
|
|
144
|
+
? ''
|
|
145
|
+
: '(未检测到 src/web/node_modules,前端依赖可能尚未安装)';
|
|
146
|
+
throw new Error(`Web UI 静态资源构建失败: ${error.message}${dependencyHint}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const finalState = inspectWebBuildState({
|
|
150
|
+
projectRoot: initialState.projectRoot,
|
|
151
|
+
sourceRoot: initialState.sourceRoot,
|
|
152
|
+
distRoot: initialState.distRoot
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
...finalState,
|
|
157
|
+
built: true,
|
|
158
|
+
skipped: false,
|
|
159
|
+
previousReason: initialState.reason
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
module.exports = {
|
|
164
|
+
inspectWebBuildState,
|
|
165
|
+
ensureWebDistReady,
|
|
166
|
+
_test: {
|
|
167
|
+
getLatestMtimeMs,
|
|
168
|
+
getWatchedWebSourceMtimeMs,
|
|
169
|
+
getNpmCommand,
|
|
170
|
+
runWebBuild
|
|
171
|
+
}
|
|
172
|
+
};
|
|
@@ -504,11 +504,28 @@ function broadcastSchedulerState(source, schedulerState) {
|
|
|
504
504
|
}
|
|
505
505
|
}
|
|
506
506
|
|
|
507
|
+
function broadcastBrowserNotification(notification = {}) {
|
|
508
|
+
if (wss && wsClients.size > 0) {
|
|
509
|
+
const message = JSON.stringify({
|
|
510
|
+
type: 'browser-notification',
|
|
511
|
+
...notification,
|
|
512
|
+
timestamp: notification.timestamp || Date.now()
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
wsClients.forEach(client => {
|
|
516
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
517
|
+
client.send(message);
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
507
523
|
module.exports = {
|
|
508
524
|
startWebSocketServer,
|
|
509
525
|
stopWebSocketServer,
|
|
510
526
|
broadcastLog,
|
|
511
527
|
clearAllLogs,
|
|
512
528
|
broadcastProxyState,
|
|
513
|
-
broadcastSchedulerState
|
|
529
|
+
broadcastSchedulerState,
|
|
530
|
+
broadcastBrowserNotification
|
|
514
531
|
};
|