@adversity/coding-tool-x 3.1.2 → 3.1.3
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 +17 -0
- package/dist/web/assets/Analytics-BIqc8Rin.css +1 -0
- package/dist/web/assets/Analytics-D2V09DHH.js +39 -0
- package/dist/web/assets/{ConfigTemplates-DvcbKKdS.js → ConfigTemplates-Bf_11LhH.js} +1 -1
- package/dist/web/assets/{Home-Cw-F_Wnu.js → Home-BRnW4FTS.js} +1 -1
- package/dist/web/assets/{Home-BJKPCBuk.css → Home-CyCIx4BA.css} +1 -1
- package/dist/web/assets/{PluginManager-jy_4GVxI.js → PluginManager-B9J32GhW.js} +1 -1
- package/dist/web/assets/{ProjectList-Df1-NcNr.js → ProjectList-5a19MWJk.js} +1 -1
- package/dist/web/assets/SessionList-CXUr6S7w.css +1 -0
- package/dist/web/assets/SessionList-Cxg5bAdT.js +1 -0
- package/dist/web/assets/{SkillManager-IRdseMKB.js → SkillManager-CVBr0CLi.js} +1 -1
- package/dist/web/assets/{Terminal-BasTyDut.js → Terminal-D2Xe_Q0H.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-D-D2kK1V.js → WorkspaceManager-C7dwV94C.js} +1 -1
- package/dist/web/assets/icons-BxcwoY5F.js +1 -0
- package/dist/web/assets/index-BS9RA6SN.js +2 -0
- package/dist/web/assets/index-DUNAVDGb.css +1 -0
- package/dist/web/assets/naive-ui-BIXcURHZ.js +1 -0
- package/dist/web/assets/{vendors-CO3Upi1d.js → vendors-i5CBGnlm.js} +1 -1
- package/dist/web/assets/{vue-vendor-DqyWIXEb.js → vue-vendor-PKd8utv_.js} +1 -1
- package/dist/web/index.html +6 -6
- package/package.json +1 -1
- package/src/config/default.js +7 -29
- package/src/config/loader.js +6 -3
- package/src/config/model-metadata.js +102 -350
- package/src/config/model-metadata.json +125 -0
- package/src/server/api/channels.js +16 -39
- package/src/server/api/codex-channels.js +15 -43
- package/src/server/api/commands.js +0 -77
- package/src/server/api/config.js +4 -1
- package/src/server/api/gemini-channels.js +16 -40
- package/src/server/api/opencode-channels.js +25 -51
- package/src/server/api/opencode-proxy.js +1 -1
- package/src/server/api/opencode-sessions.js +0 -7
- package/src/server/api/sessions.js +11 -68
- package/src/server/api/settings.js +66 -39
- package/src/server/api/skills.js +0 -44
- package/src/server/api/statistics.js +115 -1
- package/src/server/codex-proxy-server.js +26 -55
- package/src/server/gemini-proxy-server.js +15 -14
- package/src/server/index.js +0 -3
- package/src/server/opencode-proxy-server.js +45 -121
- package/src/server/proxy-server.js +2 -4
- package/src/server/services/commands-service.js +0 -29
- package/src/server/services/config-templates-service.js +38 -28
- package/src/server/services/env-checker.js +73 -8
- package/src/server/services/plugins-service.js +37 -28
- package/src/server/services/pty-manager.js +22 -18
- package/src/server/services/skill-service.js +1 -49
- package/src/server/services/speed-test.js +40 -3
- package/src/server/services/statistics-service.js +238 -1
- package/src/server/utils/pricing.js +51 -60
- package/dist/web/assets/SessionList-BGJWyneI.css +0 -1
- package/dist/web/assets/SessionList-UWcZtC2r.js +0 -1
- package/dist/web/assets/icons-kcfLIMBB.js +0 -1
- package/dist/web/assets/index-CoB3zF0K.css +0 -1
- package/dist/web/assets/index-CryrSLv8.js +0 -2
- package/dist/web/assets/naive-ui-CSrLusZZ.js +0 -1
- package/src/server/api/convert.js +0 -260
- package/src/server/services/session-converter.js +0 -577
|
@@ -48,6 +48,14 @@ class PtyManager {
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
isDirectoryPath(candidate) {
|
|
52
|
+
try {
|
|
53
|
+
return fs.existsSync(candidate) && fs.statSync(candidate).isDirectory();
|
|
54
|
+
} catch (err) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
51
59
|
resolveWorkingDirectory(cwd) {
|
|
52
60
|
const fallback = os.homedir();
|
|
53
61
|
if (typeof cwd !== 'string') {
|
|
@@ -69,28 +77,22 @@ class PtyManager {
|
|
|
69
77
|
}
|
|
70
78
|
|
|
71
79
|
// 先尝试直接使用(支持相对路径)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
return path.isAbsolute(normalized) ? normalized : path.resolve(process.cwd(), normalized);
|
|
75
|
-
}
|
|
76
|
-
} catch (err) {
|
|
77
|
-
// 忽略错误,继续尝试其他候选路径
|
|
80
|
+
if (this.isDirectoryPath(normalized)) {
|
|
81
|
+
return path.isAbsolute(normalized) ? normalized : path.resolve(process.cwd(), normalized);
|
|
78
82
|
}
|
|
79
83
|
|
|
80
84
|
// 相对路径:优先按进程 cwd 解析,其次按用户 home 解析(用于 .codex 这类隐藏目录)
|
|
81
85
|
if (!path.isAbsolute(normalized)) {
|
|
82
86
|
const candidates = [
|
|
83
87
|
path.resolve(process.cwd(), normalized),
|
|
88
|
+
path.resolve(process.cwd(), '..', normalized),
|
|
89
|
+
path.resolve(process.cwd(), '..', '..', normalized),
|
|
84
90
|
path.resolve(os.homedir(), normalized)
|
|
85
91
|
];
|
|
86
92
|
|
|
87
93
|
for (const candidate of candidates) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
return candidate;
|
|
91
|
-
}
|
|
92
|
-
} catch (err) {
|
|
93
|
-
// 忽略错误
|
|
94
|
+
if (this.isDirectoryPath(candidate)) {
|
|
95
|
+
return candidate;
|
|
94
96
|
}
|
|
95
97
|
}
|
|
96
98
|
}
|
|
@@ -269,16 +271,18 @@ class PtyManager {
|
|
|
269
271
|
console.log(`[PTY] Resolved cwd: ${originalCwd} -> ${cwd}`);
|
|
270
272
|
}
|
|
271
273
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
+
if (!this.isDirectoryPath(cwd)) {
|
|
275
|
+
const fallbackCandidates = [process.cwd(), os.homedir()];
|
|
276
|
+
const fallbackCwd = fallbackCandidates.find((candidate) => this.isDirectoryPath(candidate));
|
|
277
|
+
|
|
278
|
+
if (fallbackCwd) {
|
|
279
|
+
console.warn(`[PTY] Working directory not found: ${cwd}, fallback to ${fallbackCwd}`);
|
|
280
|
+
cwd = fallbackCwd;
|
|
281
|
+
} else {
|
|
274
282
|
const error = `Working directory not found: ${cwd}`;
|
|
275
283
|
console.error('[PTY]', error);
|
|
276
284
|
throw new Error(error);
|
|
277
285
|
}
|
|
278
|
-
} catch (err) {
|
|
279
|
-
const error = `Working directory not found: ${cwd}`;
|
|
280
|
-
console.error('[PTY]', error);
|
|
281
|
-
throw new Error(error);
|
|
282
286
|
}
|
|
283
287
|
|
|
284
288
|
console.log(`[PTY] Creating terminal: shell=${shell}, cwd=${cwd}`);
|
|
@@ -15,9 +15,6 @@ const { pipeline } = require('stream/promises');
|
|
|
15
15
|
const AdmZip = require('adm-zip');
|
|
16
16
|
const {
|
|
17
17
|
parseSkillContent,
|
|
18
|
-
detectSkillFormat,
|
|
19
|
-
convertSkillToCodex,
|
|
20
|
-
convertSkillToClaude
|
|
21
18
|
} = require('./format-converter');
|
|
22
19
|
const { NATIVE_PATHS } = require('../../config/paths');
|
|
23
20
|
|
|
@@ -705,25 +702,6 @@ class SkillService {
|
|
|
705
702
|
return this.validateOpenCodeSkillMetadata(metadata, directory);
|
|
706
703
|
}
|
|
707
704
|
|
|
708
|
-
/**
|
|
709
|
-
* 转换技能格式
|
|
710
|
-
* @param {string} content - 技能内容
|
|
711
|
-
* @param {string} targetFormat - 目标格式 ('claude' | 'codex')
|
|
712
|
-
*/
|
|
713
|
-
convertSkillFormat(content, targetFormat) {
|
|
714
|
-
const sourceFormat = detectSkillFormat(content);
|
|
715
|
-
|
|
716
|
-
if (sourceFormat === targetFormat) {
|
|
717
|
-
return { content, warnings: [], format: targetFormat };
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
if (targetFormat === 'codex') {
|
|
721
|
-
return convertSkillToCodex(content);
|
|
722
|
-
} else {
|
|
723
|
-
return convertSkillToClaude(content);
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
|
|
727
705
|
/**
|
|
728
706
|
* 检查技能是否已安装
|
|
729
707
|
*/
|
|
@@ -880,9 +858,7 @@ class SkillService {
|
|
|
880
858
|
fs.mkdirSync(dest, { recursive: true });
|
|
881
859
|
this.copyDirRecursive(sourceDir, dest);
|
|
882
860
|
|
|
883
|
-
if (this.platform === '
|
|
884
|
-
this.convertInstalledSkillToCodex(dest);
|
|
885
|
-
} else if (this.platform === 'opencode') {
|
|
861
|
+
if (this.platform === 'opencode') {
|
|
886
862
|
const skillMdPath = path.join(dest, 'SKILL.md');
|
|
887
863
|
if (fs.existsSync(skillMdPath)) {
|
|
888
864
|
const validationError = this.validateOpenCodeSkillContent(
|
|
@@ -976,22 +952,6 @@ class SkillService {
|
|
|
976
952
|
}
|
|
977
953
|
}
|
|
978
954
|
|
|
979
|
-
/**
|
|
980
|
-
* 将安装后的 SKILL.md 转换为 Codex 兼容格式
|
|
981
|
-
*/
|
|
982
|
-
convertInstalledSkillToCodex(skillDir) {
|
|
983
|
-
const skillMdPath = path.join(skillDir, 'SKILL.md');
|
|
984
|
-
if (!fs.existsSync(skillMdPath)) return;
|
|
985
|
-
|
|
986
|
-
try {
|
|
987
|
-
const content = fs.readFileSync(skillMdPath, 'utf-8');
|
|
988
|
-
const converted = convertSkillToCodex(content);
|
|
989
|
-
fs.writeFileSync(skillMdPath, converted.content, 'utf-8');
|
|
990
|
-
} catch (err) {
|
|
991
|
-
console.warn('[SkillService] Convert skill to codex format failed:', err.message);
|
|
992
|
-
}
|
|
993
|
-
}
|
|
994
|
-
|
|
995
955
|
/**
|
|
996
956
|
* 创建自定义技能
|
|
997
957
|
*/
|
|
@@ -1051,10 +1011,6 @@ ${content}
|
|
|
1051
1011
|
// 写入文件
|
|
1052
1012
|
fs.writeFileSync(path.join(dest, 'SKILL.md'), skillMdContent, 'utf-8');
|
|
1053
1013
|
|
|
1054
|
-
if (this.platform === 'codex') {
|
|
1055
|
-
this.convertInstalledSkillToCodex(dest);
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
1014
|
// 清除缓存,让列表刷新
|
|
1059
1015
|
this.skillsCache = null;
|
|
1060
1016
|
this.cacheTime = 0;
|
|
@@ -1122,10 +1078,6 @@ ${content}
|
|
|
1122
1078
|
}
|
|
1123
1079
|
}
|
|
1124
1080
|
|
|
1125
|
-
if (this.platform === 'codex') {
|
|
1126
|
-
this.convertInstalledSkillToCodex(dest);
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
1081
|
// 清除缓存
|
|
1130
1082
|
this.skillsCache = null;
|
|
1131
1083
|
this.cacheTime = 0;
|
|
@@ -12,6 +12,7 @@ const { getEffectiveApiKey: getClaudeEffectiveApiKey } = require('./channels');
|
|
|
12
12
|
const { getEffectiveApiKey: getCodexEffectiveApiKey } = require('./codex-channels');
|
|
13
13
|
const { getEffectiveApiKey: getGeminiEffectiveApiKey } = require('./gemini-channels');
|
|
14
14
|
const { getEffectiveApiKey: getOpenCodeEffectiveApiKey } = require('./opencode-channels');
|
|
15
|
+
const { getDefaultSpeedTestModelByToolType } = require('../../config/model-metadata');
|
|
15
16
|
|
|
16
17
|
// 测试结果缓存
|
|
17
18
|
const testResultsCache = new Map();
|
|
@@ -87,6 +88,21 @@ function resolveExplicitModel(channel, model) {
|
|
|
87
88
|
);
|
|
88
89
|
}
|
|
89
90
|
|
|
91
|
+
function resolveToolTypeForSpeedTest(channelType, channel) {
|
|
92
|
+
if (channelType === 'claude' || channelType === 'codex' || channelType === 'gemini') {
|
|
93
|
+
return channelType;
|
|
94
|
+
}
|
|
95
|
+
const gatewaySourceType = normalizeNonEmptyString(channel?.gatewaySourceType);
|
|
96
|
+
if (gatewaySourceType === 'claude' || gatewaySourceType === 'codex' || gatewaySourceType === 'gemini') {
|
|
97
|
+
return gatewaySourceType;
|
|
98
|
+
}
|
|
99
|
+
return 'codex';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getConfiguredDefaultSpeedTestModel(toolType) {
|
|
103
|
+
return normalizeNonEmptyString(getDefaultSpeedTestModelByToolType(toolType));
|
|
104
|
+
}
|
|
105
|
+
|
|
90
106
|
function resolveEffectiveApiKey(channel, channelType) {
|
|
91
107
|
switch (channelType) {
|
|
92
108
|
case 'codex':
|
|
@@ -441,6 +457,21 @@ async function testAPIFunctionality(baseUrl, apiKey, timeout, channelType = 'cla
|
|
|
441
457
|
};
|
|
442
458
|
console.log(`[SpeedTest] Using explicit model: ${explicitModel}`);
|
|
443
459
|
} else {
|
|
460
|
+
const defaultSpeedTestModel = getConfiguredDefaultSpeedTestModel(
|
|
461
|
+
resolveToolTypeForSpeedTest(channelType, channel)
|
|
462
|
+
);
|
|
463
|
+
if (defaultSpeedTestModel) {
|
|
464
|
+
modelProbe = {
|
|
465
|
+
preferredTestModel: defaultSpeedTestModel,
|
|
466
|
+
availableModels: [defaultSpeedTestModel],
|
|
467
|
+
cached: false,
|
|
468
|
+
method: 'default_config'
|
|
469
|
+
};
|
|
470
|
+
console.log(`[SpeedTest] Using default speedTestModel from config: ${defaultSpeedTestModel}`);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (!modelProbe) {
|
|
444
475
|
// Fall back to auto-detection
|
|
445
476
|
try {
|
|
446
477
|
modelProbe = await probeModelAvailability(channel, channelType, { stopOnFirstAvailable: true });
|
|
@@ -512,7 +543,9 @@ async function testAPIFunctionality(baseUrl, apiKey, timeout, channelType = 'cla
|
|
|
512
543
|
}
|
|
513
544
|
apiPath += '?beta=true';
|
|
514
545
|
|
|
515
|
-
testModel = modelProbe?.preferredTestModel
|
|
546
|
+
testModel = modelProbe?.preferredTestModel
|
|
547
|
+
|| normalizeNonEmptyString(model)
|
|
548
|
+
|| getConfiguredDefaultSpeedTestModel('claude');
|
|
516
549
|
const sessionId = Math.random().toString(36).substring(2, 15);
|
|
517
550
|
primaryRequestConfig = {
|
|
518
551
|
apiPath,
|
|
@@ -550,7 +583,9 @@ async function testAPIFunctionality(baseUrl, apiKey, timeout, channelType = 'cla
|
|
|
550
583
|
};
|
|
551
584
|
} else if (channelType === 'codex') {
|
|
552
585
|
const apiPath = buildCodexResponsesPath(parsedUrl);
|
|
553
|
-
testModel = modelProbe?.preferredTestModel
|
|
586
|
+
testModel = modelProbe?.preferredTestModel
|
|
587
|
+
|| normalizeNonEmptyString(model)
|
|
588
|
+
|| getConfiguredDefaultSpeedTestModel('codex');
|
|
554
589
|
const codexSessionId = `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
|
|
555
590
|
|
|
556
591
|
const baseBody = {
|
|
@@ -588,7 +623,9 @@ async function testAPIFunctionality(baseUrl, apiKey, timeout, channelType = 'cla
|
|
|
588
623
|
isStreamingResponse: true
|
|
589
624
|
};
|
|
590
625
|
} else if (channelType === 'gemini') {
|
|
591
|
-
testModel = modelProbe?.preferredTestModel
|
|
626
|
+
testModel = modelProbe?.preferredTestModel
|
|
627
|
+
|| normalizeNonEmptyString(model)
|
|
628
|
+
|| getConfiguredDefaultSpeedTestModel('gemini');
|
|
592
629
|
const useCliFormat = shouldUseGeminiCliFormat(parsedUrl);
|
|
593
630
|
|
|
594
631
|
const cliRequestConfig = {
|
|
@@ -378,9 +378,246 @@ function getTodayStatistics() {
|
|
|
378
378
|
return loadDailyStats(today);
|
|
379
379
|
}
|
|
380
380
|
|
|
381
|
+
/**
|
|
382
|
+
* 从统计对象中提取指定指标值
|
|
383
|
+
*/
|
|
384
|
+
function extractMetric(stats, metric) {
|
|
385
|
+
if (!stats) return 0;
|
|
386
|
+
if (metric === 'tokens') return stats.tokens?.total || stats.tokens || 0;
|
|
387
|
+
if (metric === 'cost') return stats.cost || 0;
|
|
388
|
+
if (metric === 'requests') return stats.requests || 0;
|
|
389
|
+
return 0;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* 从 JSONL 日志文件读取指定日期+小时的数据(按 model 或 channel 聚合)
|
|
394
|
+
*/
|
|
395
|
+
function readJsonlForHour(year, month, day, hour, groupBy) {
|
|
396
|
+
const filePath = getRequestLogFilePath(year, month, day);
|
|
397
|
+
const result = {};
|
|
398
|
+
|
|
399
|
+
try {
|
|
400
|
+
if (!fs.existsSync(filePath)) return result;
|
|
401
|
+
const lines = fs.readFileSync(filePath, 'utf8').split('\n');
|
|
402
|
+
|
|
403
|
+
for (const line of lines) {
|
|
404
|
+
if (!line.trim()) continue;
|
|
405
|
+
let entry;
|
|
406
|
+
try { entry = JSON.parse(line); } catch { continue; }
|
|
407
|
+
|
|
408
|
+
const ts = new Date(entry.timestamp);
|
|
409
|
+
if (ts.getHours() !== hour) continue;
|
|
410
|
+
|
|
411
|
+
let key;
|
|
412
|
+
if (groupBy === 'model') key = entry.model || 'unknown';
|
|
413
|
+
else if (groupBy === 'channel') key = entry.channel || entry.channelId || 'unknown';
|
|
414
|
+
else continue;
|
|
415
|
+
|
|
416
|
+
if (!result[key]) result[key] = { tokens: { total: 0 }, cost: 0, requests: 0 };
|
|
417
|
+
result[key].tokens.total += entry.tokens?.total || 0;
|
|
418
|
+
result[key].cost += entry.cost || 0;
|
|
419
|
+
result[key].requests += 1;
|
|
420
|
+
}
|
|
421
|
+
} catch (err) {
|
|
422
|
+
console.error('Failed to read JSONL for hour:', err);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return result;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* 获取趋势统计数据
|
|
430
|
+
* @param {Object} options
|
|
431
|
+
* @param {string} options.startDate - YYYY-MM-DD
|
|
432
|
+
* @param {string} options.endDate - YYYY-MM-DD
|
|
433
|
+
* @param {string} options.granularity - 'day' | 'hour'
|
|
434
|
+
* @param {string} options.groupBy - 'model' | 'channel' | 'toolType'
|
|
435
|
+
* @param {string} options.metric - 'tokens' | 'cost' | 'requests'
|
|
436
|
+
*/
|
|
437
|
+
|
|
438
|
+
// 工具类型到 daily-stats 目录前缀的映射
|
|
439
|
+
const TOOL_PREFIXES = {
|
|
440
|
+
'claude-code': '',
|
|
441
|
+
'codex': 'codex-',
|
|
442
|
+
'gemini': 'gemini-',
|
|
443
|
+
'opencode': 'opencode-'
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
// 加载指定工具的某天统计(toolPrefix 为 '' | 'codex-' | 'gemini-' | 'opencode-')
|
|
447
|
+
function loadDailyStatsByTool(dateStr, toolPrefix) {
|
|
448
|
+
const dir = path.join(os.homedir(), '.cc-tool', `${toolPrefix}daily-stats`);
|
|
449
|
+
const filePath = path.join(dir, `${dateStr}.json`);
|
|
450
|
+
try {
|
|
451
|
+
if (fs.existsSync(filePath)) {
|
|
452
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
453
|
+
}
|
|
454
|
+
} catch (e) {}
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// 合并所有工具的某天 daily-stats,groupBy 决定合并维度
|
|
459
|
+
// 对于 toolType 分组:key 就是工具名(claude-code/codex/gemini/opencode)
|
|
460
|
+
// 对于 model/channel 分组:合并各工具的 byModel/byChannel,key 可能重名(不同工具用同名模型)则加前缀
|
|
461
|
+
function mergeAllToolsDailyStats(dateStr, groupBy) {
|
|
462
|
+
const merged = {};
|
|
463
|
+
|
|
464
|
+
for (const [toolType, prefix] of Object.entries(TOOL_PREFIXES)) {
|
|
465
|
+
const stats = loadDailyStatsByTool(dateStr, prefix);
|
|
466
|
+
if (!stats) continue;
|
|
467
|
+
|
|
468
|
+
if (groupBy === 'toolType') {
|
|
469
|
+
// key = toolType,汇总整天 summary
|
|
470
|
+
if (!merged[toolType]) merged[toolType] = { requests: 0, tokens: { total: 0 }, cost: 0 };
|
|
471
|
+
const s = stats.summary || {};
|
|
472
|
+
merged[toolType].requests += s.requests || 0;
|
|
473
|
+
merged[toolType].tokens.total += (s.tokens && typeof s.tokens === 'object' ? s.tokens.total : s.tokens) || 0;
|
|
474
|
+
merged[toolType].cost += s.cost || 0;
|
|
475
|
+
} else if (groupBy === 'model') {
|
|
476
|
+
const byModel = stats.byModel || {};
|
|
477
|
+
for (const [model, mStats] of Object.entries(byModel)) {
|
|
478
|
+
if (!merged[model]) merged[model] = { requests: 0, tokens: { total: 0 }, cost: 0 };
|
|
479
|
+
merged[model].requests += mStats.requests || 0;
|
|
480
|
+
const t = mStats.tokens || {};
|
|
481
|
+
merged[model].tokens.total += (typeof t === 'object' ? t.total : t) || 0;
|
|
482
|
+
merged[model].cost += mStats.cost || 0;
|
|
483
|
+
}
|
|
484
|
+
} else if (groupBy === 'channel') {
|
|
485
|
+
const byChannel = stats.byChannel || {};
|
|
486
|
+
for (const [chId, chStats] of Object.entries(byChannel)) {
|
|
487
|
+
const key = chStats.name || chId;
|
|
488
|
+
if (!merged[key]) merged[key] = { requests: 0, tokens: { total: 0 }, cost: 0 };
|
|
489
|
+
merged[key].requests += chStats.requests || 0;
|
|
490
|
+
const t = chStats.tokens || {};
|
|
491
|
+
merged[key].tokens.total += (typeof t === 'object' ? t.total : t) || 0;
|
|
492
|
+
merged[key].cost += chStats.cost || 0;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
return merged;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
async function getTrendStatistics({ startDate, endDate, granularity = 'day', step = 1, groupBy = 'model', metric = 'tokens' }) {
|
|
500
|
+
step = parseInt(step) || 1;
|
|
501
|
+
const labels = [];
|
|
502
|
+
const seriesMap = {}; // { dimensionName: number[] }
|
|
503
|
+
const totals = {};
|
|
504
|
+
|
|
505
|
+
const start = new Date(startDate + 'T00:00:00');
|
|
506
|
+
const end = new Date(endDate + 'T00:00:00');
|
|
507
|
+
|
|
508
|
+
// Iterate each day
|
|
509
|
+
for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
|
|
510
|
+
const year = d.getFullYear();
|
|
511
|
+
const month = d.getMonth() + 1;
|
|
512
|
+
const day = d.getDate();
|
|
513
|
+
const dateStr = `${year}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}`;
|
|
514
|
+
|
|
515
|
+
if (granularity === 'day') {
|
|
516
|
+
labels.push(dateStr);
|
|
517
|
+
const byDimension = mergeAllToolsDailyStats(dateStr, groupBy);
|
|
518
|
+
|
|
519
|
+
// Accumulate dimensions seen so far with 0 for this label position
|
|
520
|
+
const labelIdx = labels.length - 1;
|
|
521
|
+
|
|
522
|
+
// Fill existing series with 0 for this position first
|
|
523
|
+
for (const key of Object.keys(seriesMap)) {
|
|
524
|
+
seriesMap[key].push(0);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
for (const [key, stats] of Object.entries(byDimension)) {
|
|
528
|
+
const val = extractMetric(stats, metric);
|
|
529
|
+
if (!seriesMap[key]) {
|
|
530
|
+
// New dimension: backfill with zeros for previous labels
|
|
531
|
+
seriesMap[key] = new Array(labelIdx).fill(0);
|
|
532
|
+
seriesMap[key].push(val);
|
|
533
|
+
} else {
|
|
534
|
+
// Already pushed 0 above, replace last element
|
|
535
|
+
seriesMap[key][labelIdx] = val;
|
|
536
|
+
}
|
|
537
|
+
totals[key] = (totals[key] || 0) + val;
|
|
538
|
+
}
|
|
539
|
+
} else {
|
|
540
|
+
// granularity === 'hour'
|
|
541
|
+
for (let h = 0; h < 24; h += step) {
|
|
542
|
+
const hourEnd = Math.min(h + step, 24);
|
|
543
|
+
const hourStr = h.toString().padStart(2, '0');
|
|
544
|
+
const label = step === 1
|
|
545
|
+
? `${dateStr} ${hourStr}:00`
|
|
546
|
+
: `${dateStr} ${hourStr}:00-${String(hourEnd).padStart(2, '0')}:00`;
|
|
547
|
+
labels.push(label);
|
|
548
|
+
const labelIdx = labels.length - 1;
|
|
549
|
+
|
|
550
|
+
// Fill existing series with 0 for this label
|
|
551
|
+
for (const key of Object.keys(seriesMap)) {
|
|
552
|
+
seriesMap[key].push(0);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Accumulate all hours in this step bucket
|
|
556
|
+
for (let hh = h; hh < hourEnd; hh++) {
|
|
557
|
+
const hhStr = hh.toString().padStart(2, '0');
|
|
558
|
+
let byDimension = {};
|
|
559
|
+
|
|
560
|
+
if (groupBy === 'toolType') {
|
|
561
|
+
// 合并所有工具的该小时数据
|
|
562
|
+
for (const [toolType, prefix] of Object.entries(TOOL_PREFIXES)) {
|
|
563
|
+
const ds = loadDailyStatsByTool(dateStr, prefix);
|
|
564
|
+
const hourData = ds?.hourly?.[hhStr];
|
|
565
|
+
const val = hourData ? extractMetric(hourData, metric) : 0;
|
|
566
|
+
if (val > 0) {
|
|
567
|
+
if (!byDimension[toolType]) byDimension[toolType] = { requests: 0, tokens: { total: 0 }, cost: 0 };
|
|
568
|
+
byDimension[toolType].requests += hourData.requests || 0;
|
|
569
|
+
byDimension[toolType].tokens.total += (hourData.tokens?.total || 0);
|
|
570
|
+
byDimension[toolType].cost += hourData.cost || 0;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
} else {
|
|
574
|
+
byDimension = readJsonlForHour(year, month, day, hh, groupBy);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
for (const [key, stats] of Object.entries(byDimension)) {
|
|
578
|
+
const val = extractMetric(stats, metric);
|
|
579
|
+
if (!seriesMap[key]) {
|
|
580
|
+
seriesMap[key] = new Array(labelIdx).fill(0);
|
|
581
|
+
seriesMap[key].push(val);
|
|
582
|
+
} else {
|
|
583
|
+
if (seriesMap[key].length <= labelIdx) seriesMap[key].push(0);
|
|
584
|
+
seriesMap[key][labelIdx] = (seriesMap[key][labelIdx] || 0) + val;
|
|
585
|
+
}
|
|
586
|
+
totals[key] = (totals[key] || 0) + val;
|
|
587
|
+
}
|
|
588
|
+
} // end hh loop
|
|
589
|
+
} // end h loop
|
|
590
|
+
} // end else (hour granularity)
|
|
591
|
+
} // end for day loop
|
|
592
|
+
|
|
593
|
+
// Sort series by total desc, keep top 10, merge rest into 'Other'
|
|
594
|
+
const sorted = Object.entries(totals).sort((a, b) => b[1] - a[1]);
|
|
595
|
+
const top10 = sorted.slice(0, 10);
|
|
596
|
+
const rest = sorted.slice(10);
|
|
597
|
+
|
|
598
|
+
const series = top10.map(([name]) => ({
|
|
599
|
+
name,
|
|
600
|
+
data: seriesMap[name] || []
|
|
601
|
+
}));
|
|
602
|
+
|
|
603
|
+
if (rest.length > 0) {
|
|
604
|
+
const otherData = labels.map((_, i) =>
|
|
605
|
+
rest.reduce((sum, [name]) => sum + (seriesMap[name]?.[i] || 0), 0)
|
|
606
|
+
);
|
|
607
|
+
const otherTotal = rest.reduce((sum, [, total]) => sum + total, 0);
|
|
608
|
+
series.push({ name: 'Other', data: otherData });
|
|
609
|
+
totals['Other'] = otherTotal;
|
|
610
|
+
// Remove merged keys from totals
|
|
611
|
+
for (const [name] of rest) delete totals[name];
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
return { labels, series, totals };
|
|
615
|
+
}
|
|
616
|
+
|
|
381
617
|
module.exports = {
|
|
382
618
|
recordRequest,
|
|
383
619
|
getStatistics,
|
|
384
620
|
getDailyStatistics,
|
|
385
|
-
getTodayStatistics
|
|
621
|
+
getTodayStatistics,
|
|
622
|
+
getTrendStatistics
|
|
386
623
|
};
|
|
@@ -1,79 +1,70 @@
|
|
|
1
1
|
const { loadConfig } = require('../../config/loader');
|
|
2
|
-
const
|
|
3
|
-
const { CLAUDE_MODEL_PRICING, CLAUDE_MODEL_ALIASES } = require('../../config/model-pricing');
|
|
2
|
+
const { resolveModelMetadata } = require('../../config/model-metadata');
|
|
4
3
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
function getPricingConfig(toolKey) {
|
|
8
|
-
try {
|
|
9
|
-
const config = loadConfig();
|
|
10
|
-
if (config.pricing && config.pricing[toolKey]) {
|
|
11
|
-
return config.pricing[toolKey];
|
|
12
|
-
}
|
|
13
|
-
} catch (err) {
|
|
14
|
-
console.error('[Pricing] Failed to load pricing config:', err);
|
|
15
|
-
}
|
|
16
|
-
return DEFAULT_CONFIG.pricing[toolKey];
|
|
4
|
+
function normalizeModelId(model) {
|
|
5
|
+
return String(model || '').trim().toLowerCase();
|
|
17
6
|
}
|
|
18
7
|
|
|
19
|
-
function
|
|
20
|
-
|
|
21
|
-
const
|
|
8
|
+
function getModelMetadataOverride(overrides, model) {
|
|
9
|
+
if (!overrides || typeof overrides !== 'object') return null;
|
|
10
|
+
const modelId = normalizeModelId(model);
|
|
11
|
+
if (!modelId) return null;
|
|
22
12
|
|
|
23
|
-
|
|
24
|
-
|
|
13
|
+
let directMatch = null;
|
|
14
|
+
for (const [id, override] of Object.entries(overrides)) {
|
|
15
|
+
if (normalizeModelId(id) === modelId) {
|
|
16
|
+
directMatch = override;
|
|
17
|
+
break;
|
|
18
|
+
}
|
|
25
19
|
}
|
|
20
|
+
if (directMatch) return directMatch;
|
|
26
21
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
22
|
+
let bestMatch = null;
|
|
23
|
+
let bestLen = 0;
|
|
24
|
+
for (const [id, override] of Object.entries(overrides)) {
|
|
25
|
+
const key = normalizeModelId(id);
|
|
26
|
+
if (!key) continue;
|
|
27
|
+
if (modelId.startsWith(key) || key.startsWith(modelId)) {
|
|
28
|
+
if (key.length > bestLen) {
|
|
29
|
+
bestLen = key.length;
|
|
30
|
+
bestMatch = override;
|
|
32
31
|
}
|
|
33
|
-
}
|
|
34
|
-
return result;
|
|
32
|
+
}
|
|
35
33
|
}
|
|
36
|
-
|
|
37
|
-
return base;
|
|
34
|
+
return bestMatch;
|
|
38
35
|
}
|
|
39
36
|
|
|
40
|
-
function
|
|
41
|
-
|
|
37
|
+
function resolveMetadataPricing(model) {
|
|
38
|
+
if (!model) return null;
|
|
42
39
|
|
|
43
|
-
|
|
44
|
-
const modelConfig = config?.models?.[model];
|
|
45
|
-
if (modelConfig && modelConfig.mode === 'custom') {
|
|
46
|
-
const result = { ...hardcodedPricing };
|
|
47
|
-
RATE_KEYS.forEach((key) => {
|
|
48
|
-
if (typeof modelConfig[key] === 'number' && Number.isFinite(modelConfig[key])) {
|
|
49
|
-
result[key] = modelConfig[key];
|
|
50
|
-
}
|
|
51
|
-
});
|
|
52
|
-
return result;
|
|
53
|
-
}
|
|
40
|
+
const builtInPricing = resolveModelMetadata(model)?.pricing || null;
|
|
54
41
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
|
|
42
|
+
try {
|
|
43
|
+
const config = loadConfig();
|
|
44
|
+
const override = getModelMetadataOverride(config.modelMetadataOverrides, model);
|
|
45
|
+
const overridePricing = override?.pricing || null;
|
|
46
|
+
if (!builtInPricing && !overridePricing) return null;
|
|
47
|
+
return {
|
|
48
|
+
...(builtInPricing || {}),
|
|
49
|
+
...(overridePricing || {})
|
|
50
|
+
};
|
|
51
|
+
} catch (err) {
|
|
52
|
+
console.error('[Pricing] Failed to load model metadata overrides:', err);
|
|
53
|
+
return builtInPricing;
|
|
64
54
|
}
|
|
55
|
+
}
|
|
65
56
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const centralizedPricing = CLAUDE_MODEL_PRICING[normalizedModel];
|
|
70
|
-
|
|
71
|
-
if (centralizedPricing) {
|
|
72
|
-
return { ...defaultPricing, ...centralizedPricing };
|
|
73
|
-
}
|
|
57
|
+
function resolvePricing(_toolKey, modelPricing = {}, defaultPricing = {}) {
|
|
58
|
+
return { ...defaultPricing, ...(modelPricing || {}) };
|
|
59
|
+
}
|
|
74
60
|
|
|
75
|
-
|
|
76
|
-
|
|
61
|
+
function resolveModelPricing(_toolKey, model, fallbackPricing = {}, defaultPricing = {}) {
|
|
62
|
+
const pricingFromMetadata = resolveMetadataPricing(model);
|
|
63
|
+
return {
|
|
64
|
+
...defaultPricing,
|
|
65
|
+
...(fallbackPricing || {}),
|
|
66
|
+
...(pricingFromMetadata || {})
|
|
67
|
+
};
|
|
77
68
|
}
|
|
78
69
|
|
|
79
70
|
module.exports = {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
.session-list-container[data-v-5b86eea5]{width:100%;height:100%;display:flex;flex-direction:column;box-sizing:border-box}.header[data-v-5b86eea5]{flex-shrink:0;padding:24px 24px 16px;background:var(--bg-primary);border-bottom:1px solid var(--border-primary)}.content[data-v-5b86eea5]{flex:1;overflow-y:auto;padding:16px 24px 24px}.back-button[data-v-5b86eea5]{flex-shrink:0;margin-right:12px}.title-bar[data-v-5b86eea5]{display:flex;align-items:center;gap:16px}.title-section[data-v-5b86eea5]{flex:1;min-width:0}.title-with-count[data-v-5b86eea5]{display:flex;align-items:baseline;gap:8px;margin-bottom:2px}.title-section h2[data-v-5b86eea5]{margin:0;font-size:20px}.session-count[data-v-5b86eea5]{font-size:14px;color:#666}.total-size-tag[data-v-5b86eea5]{margin-left:8px}.project-path[data-v-5b86eea5]{font-size:13px;display:block;color:#666;margin-bottom:2px}.search-input[data-v-5b86eea5]{width:320px;flex-shrink:0}.loading-container[data-v-5b86eea5]{display:flex;justify-content:center;align-items:center;min-height:400px}.session-item[data-v-5b86eea5]{display:flex;align-items:center;gap:12px;padding:16px;background:var(--bg-primary);border:1px solid var(--border-primary);border-radius:8px;margin-bottom:8px;transition:all .2s;cursor:pointer}.session-item[data-v-5b86eea5]:hover{border-color:#18a058;box-shadow:0 2px 8px #18a0581a}.drag-handle[data-v-5b86eea5]{cursor:move;width:24px;height:24px;padding:4px;opacity:.4;transition:all .2s;flex-shrink:0;display:flex;align-items:center;justify-content:center}.session-item:hover .drag-handle[data-v-5b86eea5]{opacity:1;background-color:#18a0581a;border-radius:4px}.session-left[data-v-5b86eea5]{flex:1;display:flex;align-items:center;gap:16px;min-width:0}.session-icon[data-v-5b86eea5]{flex-shrink:0}.session-info[data-v-5b86eea5]{flex:1;min-width:0}.session-header[data-v-5b86eea5]{display:flex;align-items:center;margin-bottom:6px}.session-title-row[data-v-5b86eea5]{display:flex;align-items:center;gap:8px}.session-title[data-v-5b86eea5]{font-size:15px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex-shrink:1;min-width:0}.session-meta[data-v-5b86eea5]{display:flex;align-items:center;gap:8px;margin-bottom:6px;font-size:13px}.session-message[data-v-5b86eea5]{display:block;max-width:600px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:13px}.session-message-empty[data-v-5b86eea5]{font-style:italic;opacity:.5}.session-right[data-v-5b86eea5]{display:flex;flex-direction:column;justify-content:flex-start;align-items:flex-end;min-width:280px;flex-shrink:0;gap:12px}.session-tags-area[data-v-5b86eea5]{min-height:24px;display:flex;align-items:flex-start;justify-content:flex-end}.session-actions[data-v-5b86eea5]{display:flex;align-items:center;margin-top:auto}.ghost[data-v-5b86eea5]{opacity:.4}.chosen[data-v-5b86eea5]{box-shadow:0 4px 16px #00000026}.search-result-item[data-v-5b86eea5]{margin-bottom:16px;padding:12px;border:1px solid var(--border-primary);border-radius:6px;background:var(--bg-elevated)}.search-result-header[data-v-5b86eea5]{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}.search-result-title[data-v-5b86eea5]{display:flex;align-items:center;gap:8px}.search-match[data-v-5b86eea5]{display:flex;align-items:flex-start;gap:8px;margin-top:6px;padding:6px;background:var(--bg-secondary);border-radius:4px}.search-match-text[data-v-5b86eea5]{flex:1;line-height:1.6}
|