@adversity/coding-tool-x 3.1.2 → 3.1.4
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 +23 -0
- package/dist/web/assets/Analytics-Blo8_rhE.css +1 -0
- package/dist/web/assets/Analytics-DEIGaSFz.js +39 -0
- package/dist/web/assets/{ConfigTemplates-DvcbKKdS.js → ConfigTemplates-BZaehll1.js} +1 -1
- package/dist/web/assets/{Home-BJKPCBuk.css → Home-CyCIx4BA.css} +1 -1
- package/dist/web/assets/{Home-Cw-F_Wnu.js → Home-DIIH5bAk.js} +1 -1
- package/dist/web/assets/{PluginManager-jy_4GVxI.js → PluginManager-b4IlavFA.js} +1 -1
- package/dist/web/assets/{ProjectList-Df1-NcNr.js → ProjectList-QaqzjEea.js} +1 -1
- package/dist/web/assets/SessionList-CXUr6S7w.css +1 -0
- package/dist/web/assets/SessionList-Cz_hrGmL.js +1 -0
- package/dist/web/assets/{SkillManager-IRdseMKB.js → SkillManager-B-Rcb9xY.js} +1 -1
- package/dist/web/assets/{Terminal-BasTyDut.js → Terminal-C8CFJEkx.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-D-D2kK1V.js → WorkspaceManager-XaQj8BRE.js} +1 -1
- package/dist/web/assets/icons-BxcwoY5F.js +1 -0
- package/dist/web/assets/index-B_kPXCbH.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
|
@@ -391,7 +391,6 @@ module.exports = (config) => {
|
|
|
391
391
|
router.post('/:projectName/:sessionId/launch', async (req, res) => {
|
|
392
392
|
try {
|
|
393
393
|
const { projectName, sessionId } = req.params;
|
|
394
|
-
const { targetTool } = req.body; // 'claude', 'codex', 或 'gemini'
|
|
395
394
|
const { exec } = require('child_process');
|
|
396
395
|
const path = require('path');
|
|
397
396
|
const fs = require('fs');
|
|
@@ -440,66 +439,10 @@ module.exports = (config) => {
|
|
|
440
439
|
});
|
|
441
440
|
}
|
|
442
441
|
|
|
443
|
-
// 判断会话来源类型
|
|
444
|
-
let sourceType = 'claude'; // 默认
|
|
445
|
-
if (sessionFile.includes('/.codex/') || sessionFile.includes('\\.codex\\')) {
|
|
446
|
-
sourceType = 'codex';
|
|
447
|
-
} else if (sessionFile.includes('/.gemini/') || sessionFile.includes('\\.gemini\\')) {
|
|
448
|
-
sourceType = 'gemini';
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
// 如果指定了 targetTool 且与 sourceType 不同,则需要转换
|
|
452
|
-
let finalSessionFile = sessionFile;
|
|
453
|
-
let finalSessionId = sessionId;
|
|
454
|
-
|
|
455
|
-
if (targetTool && targetTool !== sourceType) {
|
|
456
|
-
console.log(`跨工具启动:${sourceType} -> ${targetTool},会话 ${sessionId}`);
|
|
457
|
-
|
|
458
|
-
try {
|
|
459
|
-
const { convertSession } = require('../services/session-converter');
|
|
460
|
-
|
|
461
|
-
// 执行转换
|
|
462
|
-
const convertResult = await convertSession(
|
|
463
|
-
sourceType,
|
|
464
|
-
targetTool,
|
|
465
|
-
sessionId,
|
|
466
|
-
{
|
|
467
|
-
sourcePath: sessionFile,
|
|
468
|
-
preserveTimestamps: true,
|
|
469
|
-
targetProjectPath: fullPath
|
|
470
|
-
}
|
|
471
|
-
);
|
|
472
|
-
|
|
473
|
-
if (convertResult.success) {
|
|
474
|
-
finalSessionFile = convertResult.targetPath;
|
|
475
|
-
finalSessionId = convertResult.targetSessionId;
|
|
476
|
-
console.log(`转换成功:${finalSessionFile}`);
|
|
477
|
-
|
|
478
|
-
// 广播转换日志
|
|
479
|
-
broadcastLog({
|
|
480
|
-
type: 'action',
|
|
481
|
-
action: 'auto_convert_session',
|
|
482
|
-
message: `自动转换会话:${sourceType} -> ${targetTool}`,
|
|
483
|
-
sessionId: finalSessionId,
|
|
484
|
-
timestamp: Date.now()
|
|
485
|
-
});
|
|
486
|
-
} else {
|
|
487
|
-
return res.status(500).json({
|
|
488
|
-
error: '会话转换失败:' + (convertResult.error || '未知错误')
|
|
489
|
-
});
|
|
490
|
-
}
|
|
491
|
-
} catch (convertError) {
|
|
492
|
-
console.error('会话转换出错:', convertError);
|
|
493
|
-
return res.status(500).json({
|
|
494
|
-
error: '会话转换出错:' + convertError.message
|
|
495
|
-
});
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
|
|
499
442
|
// Extract working directory from session file
|
|
500
443
|
let cwd = fullPath; // Default to project directory
|
|
501
444
|
try {
|
|
502
|
-
const content = fs.readFileSync(
|
|
445
|
+
const content = fs.readFileSync(sessionFile, 'utf8');
|
|
503
446
|
const firstLine = content.split('\n')[0];
|
|
504
447
|
if (firstLine) {
|
|
505
448
|
const json = JSON.parse(firstLine);
|
|
@@ -514,15 +457,15 @@ module.exports = (config) => {
|
|
|
514
457
|
// 确保会话文件在 cwd 的 .claude/sessions/ 目录下
|
|
515
458
|
// 这样 claude -r 才能找到文件
|
|
516
459
|
const cwdSessionsDir = path.join(cwd, '.claude', 'sessions');
|
|
517
|
-
const cwdSessionFile = path.join(cwdSessionsDir,
|
|
460
|
+
const cwdSessionFile = path.join(cwdSessionsDir, sessionId + '.jsonl');
|
|
518
461
|
|
|
519
462
|
// 如果会话文件不在 cwd 的 sessions 目录,复制过去
|
|
520
|
-
if (
|
|
463
|
+
if (sessionFile !== cwdSessionFile && !fs.existsSync(cwdSessionFile)) {
|
|
521
464
|
try {
|
|
522
465
|
if (!fs.existsSync(cwdSessionsDir)) {
|
|
523
466
|
fs.mkdirSync(cwdSessionsDir, { recursive: true });
|
|
524
467
|
}
|
|
525
|
-
fs.copyFileSync(
|
|
468
|
+
fs.copyFileSync(sessionFile, cwdSessionFile);
|
|
526
469
|
console.log(`[Launch] Copied session to cwd: ${cwdSessionFile}`);
|
|
527
470
|
} catch (copyError) {
|
|
528
471
|
console.warn('[Launch] Failed to copy session file to cwd:', copyError.message);
|
|
@@ -536,16 +479,16 @@ module.exports = (config) => {
|
|
|
536
479
|
|
|
537
480
|
// Get alias
|
|
538
481
|
const aliases = loadAliases();
|
|
539
|
-
const alias = aliases[
|
|
482
|
+
const alias = aliases[sessionId];
|
|
540
483
|
|
|
541
484
|
// 广播行为日志
|
|
542
485
|
broadcastLog({
|
|
543
486
|
type: 'action',
|
|
544
487
|
action: 'launch_session',
|
|
545
|
-
message: `启动会话 ${alias ||
|
|
546
|
-
sessionId
|
|
488
|
+
message: `启动会话 ${alias || sessionId.substring(0, 8)} (claude)`,
|
|
489
|
+
sessionId,
|
|
547
490
|
alias: alias || null,
|
|
548
|
-
tool:
|
|
491
|
+
tool: 'claude',
|
|
549
492
|
timestamp: Date.now()
|
|
550
493
|
});
|
|
551
494
|
|
|
@@ -556,11 +499,11 @@ module.exports = (config) => {
|
|
|
556
499
|
// Windows 路径需要转换为反斜杠格式
|
|
557
500
|
const normalizedCwd = process.platform === 'win32' ? cwd.replace(/\//g, '\\') : cwd;
|
|
558
501
|
|
|
559
|
-
//
|
|
502
|
+
// 获取 Claude 会话启动命令
|
|
560
503
|
const { command, terminalId, terminalName } = getTerminalLaunchCommand(
|
|
561
504
|
normalizedCwd,
|
|
562
|
-
|
|
563
|
-
|
|
505
|
+
sessionId,
|
|
506
|
+
'claude'
|
|
564
507
|
);
|
|
565
508
|
|
|
566
509
|
console.log(`Launching terminal: ${terminalName} (${terminalId})`);
|
|
@@ -2,7 +2,12 @@ const express = require('express');
|
|
|
2
2
|
const router = express.Router();
|
|
3
3
|
const { detectAvailableTerminals } = require('../services/terminal-detector');
|
|
4
4
|
const { loadTerminalConfig, saveTerminalConfig, getSelectedTerminal } = require('../services/terminal-config');
|
|
5
|
-
const {
|
|
5
|
+
const {
|
|
6
|
+
MODEL_METADATA,
|
|
7
|
+
METADATA_LAST_UPDATED,
|
|
8
|
+
getDefaultSpeedTestModels,
|
|
9
|
+
saveDefaultSpeedTestModels
|
|
10
|
+
} = require('../../config/model-metadata');
|
|
6
11
|
const { loadConfig, saveConfig } = require('../../config/loader');
|
|
7
12
|
|
|
8
13
|
// GET /api/settings/terminals - 获取可用终端列表
|
|
@@ -60,23 +65,23 @@ router.post('/terminal-config', (req, res) => {
|
|
|
60
65
|
}
|
|
61
66
|
});
|
|
62
67
|
|
|
63
|
-
|
|
64
|
-
router.get('/model-metadata', (req, res) => {
|
|
68
|
+
function handleGetModelSettings(req, res) {
|
|
65
69
|
try {
|
|
66
|
-
// Return built-in defaults merged with any user overrides
|
|
67
70
|
const config = loadConfig();
|
|
68
71
|
const overrides = config.modelMetadataOverrides || {};
|
|
72
|
+
const defaultSpeedTestModels = getDefaultSpeedTestModels();
|
|
69
73
|
|
|
70
74
|
// Build merged table: built-in + user overrides
|
|
71
75
|
const merged = {};
|
|
72
76
|
for (const [id, meta] of Object.entries(MODEL_METADATA)) {
|
|
73
77
|
merged[id] = overrides[id]
|
|
74
78
|
? {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
79
|
+
limit: { ...meta.limit, ...(overrides[id].limit || {}) },
|
|
80
|
+
pricing: { ...meta.pricing, ...(overrides[id].pricing || {}) }
|
|
81
|
+
}
|
|
78
82
|
: meta;
|
|
79
83
|
}
|
|
84
|
+
|
|
80
85
|
// Also include any user-added custom models from overrides
|
|
81
86
|
for (const [id, meta] of Object.entries(overrides)) {
|
|
82
87
|
if (!merged[id]) {
|
|
@@ -87,46 +92,53 @@ router.get('/model-metadata', (req, res) => {
|
|
|
87
92
|
res.json({
|
|
88
93
|
models: merged,
|
|
89
94
|
overrides,
|
|
90
|
-
|
|
95
|
+
builtinModelIds: Object.keys(MODEL_METADATA),
|
|
96
|
+
lastUpdated: METADATA_LAST_UPDATED,
|
|
97
|
+
defaultSpeedTestModels
|
|
91
98
|
});
|
|
92
99
|
} catch (error) {
|
|
93
100
|
console.error('Error getting model metadata:', error);
|
|
94
101
|
res.status(500).json({ error: error.message });
|
|
95
102
|
}
|
|
96
|
-
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// GET /api/settings/model-settings - 获取模型设置(元数据 + 默认测速模型)
|
|
106
|
+
router.get('/model-settings', handleGetModelSettings);
|
|
107
|
+
// backward compatibility
|
|
108
|
+
router.get('/model-metadata', handleGetModelSettings);
|
|
97
109
|
|
|
98
|
-
|
|
99
|
-
// Body: { overrides: { [modelId]: { limit?: {...}, pricing?: {...} } } }
|
|
100
|
-
router.post('/model-metadata', (req, res) => {
|
|
110
|
+
function handleSaveModelSettings(req, res) {
|
|
101
111
|
try {
|
|
102
|
-
const { overrides } = req.body;
|
|
103
|
-
if (
|
|
112
|
+
const { overrides, defaultSpeedTestModels } = req.body || {};
|
|
113
|
+
if (overrides !== undefined && (typeof overrides !== 'object' || overrides === null || Array.isArray(overrides))) {
|
|
104
114
|
return res.status(400).json({ error: 'overrides must be an object' });
|
|
105
115
|
}
|
|
106
116
|
|
|
107
117
|
// Validate each override entry
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (meta.limit !== undefined) {
|
|
113
|
-
if (typeof meta.limit !== 'object') {
|
|
114
|
-
return res.status(400).json({ error: `${modelId}: limit must be an object` });
|
|
118
|
+
if (overrides && typeof overrides === 'object') {
|
|
119
|
+
for (const [modelId, meta] of Object.entries(overrides)) {
|
|
120
|
+
if (typeof modelId !== 'string' || !modelId.trim()) {
|
|
121
|
+
return res.status(400).json({ error: `Invalid model ID: "${modelId}"` });
|
|
115
122
|
}
|
|
116
|
-
if (meta.limit
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
123
|
+
if (meta.limit !== undefined) {
|
|
124
|
+
if (typeof meta.limit !== 'object') {
|
|
125
|
+
return res.status(400).json({ error: `${modelId}: limit must be an object` });
|
|
126
|
+
}
|
|
127
|
+
if (meta.limit.context !== undefined && (typeof meta.limit.context !== 'number' || meta.limit.context <= 0)) {
|
|
128
|
+
return res.status(400).json({ error: `${modelId}: limit.context must be a positive number` });
|
|
129
|
+
}
|
|
130
|
+
if (meta.limit.output !== undefined && (typeof meta.limit.output !== 'number' || meta.limit.output <= 0)) {
|
|
131
|
+
return res.status(400).json({ error: `${modelId}: limit.output must be a positive number` });
|
|
132
|
+
}
|
|
126
133
|
}
|
|
127
|
-
|
|
128
|
-
if (
|
|
129
|
-
return res.status(400).json({ error: `${modelId}: pricing
|
|
134
|
+
if (meta.pricing !== undefined) {
|
|
135
|
+
if (typeof meta.pricing !== 'object') {
|
|
136
|
+
return res.status(400).json({ error: `${modelId}: pricing must be an object` });
|
|
137
|
+
}
|
|
138
|
+
for (const field of ['input', 'output']) {
|
|
139
|
+
if (meta.pricing[field] !== undefined && (typeof meta.pricing[field] !== 'number' || meta.pricing[field] < 0)) {
|
|
140
|
+
return res.status(400).json({ error: `${modelId}: pricing.${field} must be a non-negative number` });
|
|
141
|
+
}
|
|
130
142
|
}
|
|
131
143
|
}
|
|
132
144
|
}
|
|
@@ -136,19 +148,31 @@ router.post('/model-metadata', (req, res) => {
|
|
|
136
148
|
const newConfig = {
|
|
137
149
|
...config,
|
|
138
150
|
projectsDir: config.projectsDir.replace(require('os').homedir(), '~'),
|
|
139
|
-
modelMetadataOverrides: overrides
|
|
151
|
+
modelMetadataOverrides: overrides && typeof overrides === 'object'
|
|
152
|
+
? overrides
|
|
153
|
+
: (config.modelMetadataOverrides || {})
|
|
140
154
|
};
|
|
141
155
|
saveConfig(newConfig);
|
|
156
|
+
const persistedDefaultSpeedTestModels = saveDefaultSpeedTestModels(defaultSpeedTestModels);
|
|
142
157
|
|
|
143
|
-
res.json({
|
|
158
|
+
res.json({
|
|
159
|
+
success: true,
|
|
160
|
+
overrides: newConfig.modelMetadataOverrides,
|
|
161
|
+
defaultSpeedTestModels: persistedDefaultSpeedTestModels
|
|
162
|
+
});
|
|
144
163
|
} catch (error) {
|
|
145
164
|
console.error('Error saving model metadata:', error);
|
|
146
165
|
res.status(500).json({ error: error.message });
|
|
147
166
|
}
|
|
148
|
-
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// POST /api/settings/model-settings - 保存模型设置
|
|
170
|
+
router.post('/model-settings', handleSaveModelSettings);
|
|
171
|
+
// backward compatibility
|
|
172
|
+
router.post('/model-metadata', handleSaveModelSettings);
|
|
149
173
|
|
|
150
174
|
// DELETE /api/settings/model-metadata/:modelId - 删除单个模型覆盖项(恢复内置默认值)
|
|
151
|
-
|
|
175
|
+
function handleDeleteModelOverride(req, res) {
|
|
152
176
|
try {
|
|
153
177
|
const modelId = decodeURIComponent(req.params.modelId);
|
|
154
178
|
const config = loadConfig();
|
|
@@ -167,6 +191,9 @@ router.delete('/model-metadata/:modelId', (req, res) => {
|
|
|
167
191
|
console.error('Error deleting model metadata override:', error);
|
|
168
192
|
res.status(500).json({ error: error.message });
|
|
169
193
|
}
|
|
170
|
-
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
router.delete('/model-settings/:modelId', handleDeleteModelOverride);
|
|
197
|
+
router.delete('/model-metadata/:modelId', handleDeleteModelOverride);
|
|
171
198
|
|
|
172
199
|
module.exports = router;
|
package/src/server/api/skills.js
CHANGED
|
@@ -567,48 +567,4 @@ router.put('/:directory/file/*', (req, res) => {
|
|
|
567
567
|
}
|
|
568
568
|
});
|
|
569
569
|
|
|
570
|
-
// ==================== 格式转换 API ====================
|
|
571
|
-
|
|
572
|
-
/**
|
|
573
|
-
* 转换技能格式
|
|
574
|
-
* POST /api/skills/convert
|
|
575
|
-
* Body: { content, targetFormat }
|
|
576
|
-
* - content: 技能内容
|
|
577
|
-
* - targetFormat: 目标格式 ('claude' | 'codex')
|
|
578
|
-
*/
|
|
579
|
-
router.post('/convert', (req, res) => {
|
|
580
|
-
try {
|
|
581
|
-
const { platform, service } = getSkillService(req);
|
|
582
|
-
const { content, targetFormat } = req.body;
|
|
583
|
-
|
|
584
|
-
if (!content) {
|
|
585
|
-
return res.status(400).json({
|
|
586
|
-
success: false,
|
|
587
|
-
message: '请提供技能内容'
|
|
588
|
-
});
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
if (!['claude', 'codex'].includes(targetFormat)) {
|
|
592
|
-
return res.status(400).json({
|
|
593
|
-
success: false,
|
|
594
|
-
message: '目标格式必须是 claude 或 codex'
|
|
595
|
-
});
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
const result = service.convertSkillFormat(content, targetFormat);
|
|
599
|
-
|
|
600
|
-
res.json({
|
|
601
|
-
success: true,
|
|
602
|
-
platform,
|
|
603
|
-
...result
|
|
604
|
-
});
|
|
605
|
-
} catch (err) {
|
|
606
|
-
console.error('[Skills API] Convert skill error:', err);
|
|
607
|
-
res.status(500).json({
|
|
608
|
-
success: false,
|
|
609
|
-
message: err.message
|
|
610
|
-
});
|
|
611
|
-
}
|
|
612
|
-
});
|
|
613
|
-
|
|
614
570
|
module.exports = router;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const express = require('express');
|
|
2
2
|
const router = express.Router();
|
|
3
|
-
const { getStatistics, getDailyStatistics, getTodayStatistics } = require('../services/statistics-service');
|
|
3
|
+
const { getStatistics, getDailyStatistics, getTodayStatistics, getTrendStatistics } = require('../services/statistics-service');
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* 获取总体统计数据
|
|
@@ -88,4 +88,118 @@ router.get('/recent', (req, res) => {
|
|
|
88
88
|
}
|
|
89
89
|
});
|
|
90
90
|
|
|
91
|
+
/**
|
|
92
|
+
* 获取趋势统计数据
|
|
93
|
+
* GET /api/statistics/trend
|
|
94
|
+
*
|
|
95
|
+
* @query {string} startDate - YYYY-MM-DD (required)
|
|
96
|
+
* @query {string} endDate - YYYY-MM-DD (required)
|
|
97
|
+
* @query {string} granularity - 'day' | 'hour' (default: 'day')
|
|
98
|
+
* @query {string} groupBy - 'model' | 'channel' | 'toolType' (default: 'model')
|
|
99
|
+
* @query {string} metric - 'tokens' | 'cost' | 'requests' (default: 'tokens')
|
|
100
|
+
*/
|
|
101
|
+
router.get('/trend', async (req, res) => {
|
|
102
|
+
try {
|
|
103
|
+
const { startDate, endDate, granularity = 'day', step = 1, groupBy = 'model', metric = 'tokens' } = req.query;
|
|
104
|
+
|
|
105
|
+
if (!startDate || !endDate) {
|
|
106
|
+
return res.status(400).json({ error: 'startDate and endDate are required' });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
|
|
110
|
+
if (!dateRegex.test(startDate) || !dateRegex.test(endDate)) {
|
|
111
|
+
return res.status(400).json({ error: 'Invalid date format. Expected YYYY-MM-DD' });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const start = new Date(startDate);
|
|
115
|
+
const end = new Date(endDate);
|
|
116
|
+
const diffDays = Math.round((end - start) / (1000 * 60 * 60 * 24)) + 1;
|
|
117
|
+
|
|
118
|
+
if (diffDays < 1) {
|
|
119
|
+
return res.status(400).json({ error: 'endDate must be >= startDate' });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (granularity === 'hour' && diffDays > 7) {
|
|
123
|
+
return res.status(400).json({ error: 'Hour granularity is limited to 7 days' });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (diffDays > 90) {
|
|
127
|
+
return res.status(400).json({ error: 'Date range cannot exceed 90 days' });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const result = await getTrendStatistics({ startDate, endDate, granularity, step, groupBy, metric });
|
|
131
|
+
res.json({ success: true, ...result });
|
|
132
|
+
} catch (error) {
|
|
133
|
+
console.error('Failed to get trend statistics:', error);
|
|
134
|
+
res.status(500).json({ error: 'Failed to get trend statistics' });
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 导出趋势统计数据
|
|
140
|
+
* GET /api/statistics/trend/export
|
|
141
|
+
*
|
|
142
|
+
* @query {string} startDate - YYYY-MM-DD (required)
|
|
143
|
+
* @query {string} endDate - YYYY-MM-DD (required)
|
|
144
|
+
* @query {string} granularity - 'day' | 'hour' (default: 'day')
|
|
145
|
+
* @query {string} groupBy - 'model' | 'channel' | 'toolType' (default: 'model')
|
|
146
|
+
* @query {string} metric - 'tokens' | 'cost' | 'requests' (default: 'tokens')
|
|
147
|
+
* @query {string} format - 'csv' | 'json' (default: 'csv')
|
|
148
|
+
*/
|
|
149
|
+
router.get('/trend/export', async (req, res) => {
|
|
150
|
+
try {
|
|
151
|
+
const { startDate, endDate, granularity = 'day', step = 1, groupBy = 'model', metric = 'tokens', format = 'csv' } = req.query;
|
|
152
|
+
|
|
153
|
+
if (!startDate || !endDate) {
|
|
154
|
+
return res.status(400).json({ error: 'startDate and endDate are required' });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
|
|
158
|
+
if (!dateRegex.test(startDate) || !dateRegex.test(endDate)) {
|
|
159
|
+
return res.status(400).json({ error: 'Invalid date format. Expected YYYY-MM-DD' });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const start = new Date(startDate);
|
|
163
|
+
const end = new Date(endDate);
|
|
164
|
+
const diffDays = Math.round((end - start) / (1000 * 60 * 60 * 24)) + 1;
|
|
165
|
+
|
|
166
|
+
if (diffDays < 1) {
|
|
167
|
+
return res.status(400).json({ error: 'endDate must be >= startDate' });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (granularity === 'hour' && diffDays > 7) {
|
|
171
|
+
return res.status(400).json({ error: 'Hour granularity is limited to 7 days' });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (diffDays > 90) {
|
|
175
|
+
return res.status(400).json({ error: 'Date range cannot exceed 90 days' });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const result = await getTrendStatistics({ startDate, endDate, granularity, step, groupBy, metric });
|
|
179
|
+
|
|
180
|
+
const filename = `analytics-${startDate}-${endDate}-${groupBy}-${metric}.${format}`;
|
|
181
|
+
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
|
|
182
|
+
|
|
183
|
+
if (format === 'json') {
|
|
184
|
+
res.setHeader('Content-Type', 'application/json');
|
|
185
|
+
return res.json(result);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// CSV format
|
|
189
|
+
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
|
|
190
|
+
const seriesNames = result.series.map(s => s.name);
|
|
191
|
+
const header = ['Date/Time', ...seriesNames, 'Total'].join(',');
|
|
192
|
+
const rows = result.labels.map((label, i) => {
|
|
193
|
+
const values = result.series.map(s => s.data[i] || 0);
|
|
194
|
+
const total = values.reduce((a, b) => a + b, 0);
|
|
195
|
+
return [label, ...values, total].join(',');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
res.send([header, ...rows].join('\n'));
|
|
199
|
+
} catch (error) {
|
|
200
|
+
console.error('Failed to export trend statistics:', error);
|
|
201
|
+
res.status(500).json({ error: 'Failed to export trend statistics' });
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
91
205
|
module.exports = router;
|
|
@@ -7,12 +7,11 @@ const { allocateChannel, releaseChannel, getSchedulerState } = require('./servic
|
|
|
7
7
|
const { recordSuccess, recordFailure } = require('./services/channel-health');
|
|
8
8
|
const { loadConfig } = require('../config/loader');
|
|
9
9
|
const DEFAULT_CONFIG = require('../config/default');
|
|
10
|
-
const {
|
|
10
|
+
const { resolveModelPricing } = require('./utils/pricing');
|
|
11
11
|
const { recordRequest: recordCodexRequest } = require('./services/codex-statistics-service');
|
|
12
12
|
const { saveProxyStartTime, clearProxyStartTime, getProxyStartTime, getProxyRuntime } = require('./services/proxy-runtime');
|
|
13
13
|
const { createDecodedStream } = require('./services/response-decoder');
|
|
14
14
|
const { getEnabledChannels, writeCodexConfigForMultiChannel, getEffectiveApiKey } = require('./services/codex-channels');
|
|
15
|
-
const { CLAUDE_MODEL_PRICING } = require('../config/model-pricing');
|
|
16
15
|
|
|
17
16
|
let proxyServer = null;
|
|
18
17
|
let proxyApp = null;
|
|
@@ -26,7 +25,7 @@ const requestMetadata = new Map();
|
|
|
26
25
|
const printedRedirectCache = new Map();
|
|
27
26
|
|
|
28
27
|
// OpenAI 模型定价(每百万 tokens 的价格,单位:美元)
|
|
29
|
-
//
|
|
28
|
+
// 作为 model-metadata 未覆盖时的兜底值
|
|
30
29
|
const PRICING = {
|
|
31
30
|
'gpt-4o': { input: 2.5, output: 10 },
|
|
32
31
|
'gpt-4o-2024-11-20': { input: 2.5, output: 10 },
|
|
@@ -143,61 +142,33 @@ function resolveCodexTarget(baseUrl = '', requestPath = '') {
|
|
|
143
142
|
* 计算请求成本
|
|
144
143
|
*/
|
|
145
144
|
function calculateCost(model, tokens) {
|
|
146
|
-
let
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
if (
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
if (
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// 非 Claude 模型,使用 PRICING 对象(OpenAI 等)
|
|
170
|
-
pricing = PRICING[model];
|
|
171
|
-
|
|
172
|
-
// 如果没有精确匹配,尝试模糊匹配
|
|
173
|
-
if (!pricing) {
|
|
174
|
-
const modelLower = model.toLowerCase();
|
|
175
|
-
if (modelLower.includes('gpt-4o-mini')) {
|
|
176
|
-
pricing = PRICING['gpt-4o-mini'];
|
|
177
|
-
} else if (modelLower.includes('gpt-4o')) {
|
|
178
|
-
pricing = PRICING['gpt-4o'];
|
|
179
|
-
} else if (modelLower.includes('gpt-4')) {
|
|
180
|
-
pricing = PRICING['gpt-4'];
|
|
181
|
-
} else if (modelLower.includes('gpt-3.5')) {
|
|
182
|
-
pricing = PRICING['gpt-3.5-turbo'];
|
|
183
|
-
} else if (modelLower.includes('o1-mini')) {
|
|
184
|
-
pricing = PRICING['o1-mini'];
|
|
185
|
-
} else if (modelLower.includes('o1-pro')) {
|
|
186
|
-
pricing = PRICING['o1-pro'];
|
|
187
|
-
} else if (modelLower.includes('o1')) {
|
|
188
|
-
pricing = PRICING['o1'];
|
|
189
|
-
} else if (modelLower.includes('o3-mini')) {
|
|
190
|
-
pricing = PRICING['o3-mini'];
|
|
191
|
-
} else if (modelLower.includes('o3')) {
|
|
192
|
-
pricing = PRICING['o3'];
|
|
193
|
-
} else if (modelLower.includes('o4-mini')) {
|
|
194
|
-
pricing = PRICING['o4-mini'];
|
|
195
|
-
}
|
|
145
|
+
let fallbackPricing = PRICING[model];
|
|
146
|
+
if (!fallbackPricing) {
|
|
147
|
+
const modelLower = String(model || '').toLowerCase();
|
|
148
|
+
if (modelLower.includes('gpt-4o-mini')) {
|
|
149
|
+
fallbackPricing = PRICING['gpt-4o-mini'];
|
|
150
|
+
} else if (modelLower.includes('gpt-4o')) {
|
|
151
|
+
fallbackPricing = PRICING['gpt-4o'];
|
|
152
|
+
} else if (modelLower.includes('gpt-4')) {
|
|
153
|
+
fallbackPricing = PRICING['gpt-4'];
|
|
154
|
+
} else if (modelLower.includes('gpt-3.5')) {
|
|
155
|
+
fallbackPricing = PRICING['gpt-3.5-turbo'];
|
|
156
|
+
} else if (modelLower.includes('o1-mini')) {
|
|
157
|
+
fallbackPricing = PRICING['o1-mini'];
|
|
158
|
+
} else if (modelLower.includes('o1-pro')) {
|
|
159
|
+
fallbackPricing = PRICING['o1-pro'];
|
|
160
|
+
} else if (modelLower.includes('o1')) {
|
|
161
|
+
fallbackPricing = PRICING['o1'];
|
|
162
|
+
} else if (modelLower.includes('o3-mini')) {
|
|
163
|
+
fallbackPricing = PRICING['o3-mini'];
|
|
164
|
+
} else if (modelLower.includes('o3')) {
|
|
165
|
+
fallbackPricing = PRICING['o3'];
|
|
166
|
+
} else if (modelLower.includes('o4-mini')) {
|
|
167
|
+
fallbackPricing = PRICING['o4-mini'];
|
|
196
168
|
}
|
|
197
169
|
}
|
|
198
170
|
|
|
199
|
-
|
|
200
|
-
pricing = resolvePricing('codex', pricing, CODEX_BASE_PRICING);
|
|
171
|
+
const pricing = resolveModelPricing('codex', model, fallbackPricing, CODEX_BASE_PRICING);
|
|
201
172
|
const inputRate = typeof pricing.input === 'number' ? pricing.input : CODEX_BASE_PRICING.input;
|
|
202
173
|
const outputRate = typeof pricing.output === 'number' ? pricing.output : CODEX_BASE_PRICING.output;
|
|
203
174
|
|
|
@@ -7,7 +7,7 @@ const { allocateChannel, releaseChannel, getSchedulerState } = require('./servic
|
|
|
7
7
|
const { recordSuccess, recordFailure } = require('./services/channel-health');
|
|
8
8
|
const { loadConfig } = require('../config/loader');
|
|
9
9
|
const DEFAULT_CONFIG = require('../config/default');
|
|
10
|
-
const {
|
|
10
|
+
const { resolveModelPricing } = require('./utils/pricing');
|
|
11
11
|
const { recordRequest: recordGeminiRequest } = require('./services/gemini-statistics-service');
|
|
12
12
|
const { saveProxyStartTime, clearProxyStartTime, getProxyStartTime, getProxyRuntime } = require('./services/proxy-runtime');
|
|
13
13
|
const { createDecodedStream } = require('./services/response-decoder');
|
|
@@ -25,6 +25,7 @@ const requestMetadata = new Map();
|
|
|
25
25
|
const printedGeminiRedirectCache = new Map();
|
|
26
26
|
|
|
27
27
|
// Gemini 模型定价(每百万 tokens 的价格,单位:美元)
|
|
28
|
+
// 作为 model-metadata 未覆盖时的兜底值
|
|
28
29
|
const PRICING = {
|
|
29
30
|
'gemini-2.5-pro': { input: 1.25, output: 5 },
|
|
30
31
|
'gemini-2.5-flash': { input: 0.075, output: 0.3 },
|
|
@@ -79,33 +80,33 @@ function redirectModel(originalModel, channel) {
|
|
|
79
80
|
*/
|
|
80
81
|
function calculateCost(model, tokens) {
|
|
81
82
|
// 尝试精确匹配
|
|
82
|
-
let
|
|
83
|
+
let fallbackPricing = PRICING[model];
|
|
83
84
|
|
|
84
85
|
// 如果没有精确匹配,尝试模糊匹配
|
|
85
|
-
if (!
|
|
86
|
-
const modelLower = model.toLowerCase();
|
|
86
|
+
if (!fallbackPricing) {
|
|
87
|
+
const modelLower = String(model || '').toLowerCase();
|
|
87
88
|
if (modelLower.includes('gemini-2.5-pro')) {
|
|
88
|
-
|
|
89
|
+
fallbackPricing = PRICING['gemini-2.5-pro'];
|
|
89
90
|
} else if (modelLower.includes('gemini-2.5-flash')) {
|
|
90
|
-
|
|
91
|
+
fallbackPricing = PRICING['gemini-2.5-flash'];
|
|
91
92
|
} else if (modelLower.includes('gemini-2.0-flash-thinking')) {
|
|
92
|
-
|
|
93
|
+
fallbackPricing = PRICING['gemini-2.0-flash-thinking-exp-1219'];
|
|
93
94
|
} else if (modelLower.includes('gemini-2.0-flash')) {
|
|
94
|
-
|
|
95
|
+
fallbackPricing = PRICING['gemini-2.0-flash-exp'];
|
|
95
96
|
} else if (modelLower.includes('gemini-1.5-pro')) {
|
|
96
|
-
|
|
97
|
+
fallbackPricing = PRICING['gemini-1.5-pro'];
|
|
97
98
|
} else if (modelLower.includes('gemini-1.5-flash-8b')) {
|
|
98
|
-
|
|
99
|
+
fallbackPricing = PRICING['gemini-1.5-flash-8b'];
|
|
99
100
|
} else if (modelLower.includes('gemini-1.5-flash')) {
|
|
100
|
-
|
|
101
|
+
fallbackPricing = PRICING['gemini-1.5-flash'];
|
|
101
102
|
} else if (modelLower.includes('gemini-1.0-pro')) {
|
|
102
|
-
|
|
103
|
+
fallbackPricing = PRICING['gemini-1.0-pro'];
|
|
103
104
|
} else if (modelLower.includes('gemini-pro')) {
|
|
104
|
-
|
|
105
|
+
fallbackPricing = PRICING['gemini-pro'];
|
|
105
106
|
}
|
|
106
107
|
}
|
|
107
108
|
|
|
108
|
-
pricing =
|
|
109
|
+
const pricing = resolveModelPricing('gemini', model, fallbackPricing, GEMINI_BASE_PRICING);
|
|
109
110
|
const inputRate = typeof pricing.input === 'number' ? pricing.input : GEMINI_BASE_PRICING.input;
|
|
110
111
|
const outputRate = typeof pricing.output === 'number' ? pricing.output : GEMINI_BASE_PRICING.output;
|
|
111
112
|
|
package/src/server/index.js
CHANGED
|
@@ -158,9 +158,6 @@ async function startServer(port, host = '127.0.0.1', options = {}) {
|
|
|
158
158
|
app.use('/api/opencode/proxy', require('./api/opencode-proxy'));
|
|
159
159
|
app.use('/api/opencode/statistics', require('./api/opencode-statistics'));
|
|
160
160
|
|
|
161
|
-
// 会话格式转换 API
|
|
162
|
-
app.use('/api/convert', require('./api/convert'));
|
|
163
|
-
|
|
164
161
|
app.use('/api/aliases', require('./api/aliases')());
|
|
165
162
|
app.use('/api/favorites', require('./api/favorites'));
|
|
166
163
|
app.use('/api/ui-config', require('./api/ui-config'));
|