@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.
Files changed (59) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/web/assets/Analytics-BIqc8Rin.css +1 -0
  3. package/dist/web/assets/Analytics-D2V09DHH.js +39 -0
  4. package/dist/web/assets/{ConfigTemplates-DvcbKKdS.js → ConfigTemplates-Bf_11LhH.js} +1 -1
  5. package/dist/web/assets/{Home-Cw-F_Wnu.js → Home-BRnW4FTS.js} +1 -1
  6. package/dist/web/assets/{Home-BJKPCBuk.css → Home-CyCIx4BA.css} +1 -1
  7. package/dist/web/assets/{PluginManager-jy_4GVxI.js → PluginManager-B9J32GhW.js} +1 -1
  8. package/dist/web/assets/{ProjectList-Df1-NcNr.js → ProjectList-5a19MWJk.js} +1 -1
  9. package/dist/web/assets/SessionList-CXUr6S7w.css +1 -0
  10. package/dist/web/assets/SessionList-Cxg5bAdT.js +1 -0
  11. package/dist/web/assets/{SkillManager-IRdseMKB.js → SkillManager-CVBr0CLi.js} +1 -1
  12. package/dist/web/assets/{Terminal-BasTyDut.js → Terminal-D2Xe_Q0H.js} +1 -1
  13. package/dist/web/assets/{WorkspaceManager-D-D2kK1V.js → WorkspaceManager-C7dwV94C.js} +1 -1
  14. package/dist/web/assets/icons-BxcwoY5F.js +1 -0
  15. package/dist/web/assets/index-BS9RA6SN.js +2 -0
  16. package/dist/web/assets/index-DUNAVDGb.css +1 -0
  17. package/dist/web/assets/naive-ui-BIXcURHZ.js +1 -0
  18. package/dist/web/assets/{vendors-CO3Upi1d.js → vendors-i5CBGnlm.js} +1 -1
  19. package/dist/web/assets/{vue-vendor-DqyWIXEb.js → vue-vendor-PKd8utv_.js} +1 -1
  20. package/dist/web/index.html +6 -6
  21. package/package.json +1 -1
  22. package/src/config/default.js +7 -29
  23. package/src/config/loader.js +6 -3
  24. package/src/config/model-metadata.js +102 -350
  25. package/src/config/model-metadata.json +125 -0
  26. package/src/server/api/channels.js +16 -39
  27. package/src/server/api/codex-channels.js +15 -43
  28. package/src/server/api/commands.js +0 -77
  29. package/src/server/api/config.js +4 -1
  30. package/src/server/api/gemini-channels.js +16 -40
  31. package/src/server/api/opencode-channels.js +25 -51
  32. package/src/server/api/opencode-proxy.js +1 -1
  33. package/src/server/api/opencode-sessions.js +0 -7
  34. package/src/server/api/sessions.js +11 -68
  35. package/src/server/api/settings.js +66 -39
  36. package/src/server/api/skills.js +0 -44
  37. package/src/server/api/statistics.js +115 -1
  38. package/src/server/codex-proxy-server.js +26 -55
  39. package/src/server/gemini-proxy-server.js +15 -14
  40. package/src/server/index.js +0 -3
  41. package/src/server/opencode-proxy-server.js +45 -121
  42. package/src/server/proxy-server.js +2 -4
  43. package/src/server/services/commands-service.js +0 -29
  44. package/src/server/services/config-templates-service.js +38 -28
  45. package/src/server/services/env-checker.js +73 -8
  46. package/src/server/services/plugins-service.js +37 -28
  47. package/src/server/services/pty-manager.js +22 -18
  48. package/src/server/services/skill-service.js +1 -49
  49. package/src/server/services/speed-test.js +40 -3
  50. package/src/server/services/statistics-service.js +238 -1
  51. package/src/server/utils/pricing.js +51 -60
  52. package/dist/web/assets/SessionList-BGJWyneI.css +0 -1
  53. package/dist/web/assets/SessionList-UWcZtC2r.js +0 -1
  54. package/dist/web/assets/icons-kcfLIMBB.js +0 -1
  55. package/dist/web/assets/index-CoB3zF0K.css +0 -1
  56. package/dist/web/assets/index-CryrSLv8.js +0 -2
  57. package/dist/web/assets/naive-ui-CSrLusZZ.js +0 -1
  58. package/src/server/api/convert.js +0 -260
  59. 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(finalSessionFile, 'utf8');
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, finalSessionId + '.jsonl');
460
+ const cwdSessionFile = path.join(cwdSessionsDir, sessionId + '.jsonl');
518
461
 
519
462
  // 如果会话文件不在 cwd 的 sessions 目录,复制过去
520
- if (finalSessionFile !== cwdSessionFile && !fs.existsSync(cwdSessionFile)) {
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(finalSessionFile, cwdSessionFile);
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[finalSessionId];
482
+ const alias = aliases[sessionId];
540
483
 
541
484
  // 广播行为日志
542
485
  broadcastLog({
543
486
  type: 'action',
544
487
  action: 'launch_session',
545
- message: `启动会话 ${alias || finalSessionId.substring(0, 8)} (${targetTool || sourceType})`,
546
- sessionId: finalSessionId,
488
+ message: `启动会话 ${alias || sessionId.substring(0, 8)} (claude)`,
489
+ sessionId,
547
490
  alias: alias || null,
548
- tool: targetTool || sourceType,
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
- // 获取启动命令(需要传入 targetTool)
502
+ // 获取 Claude 会话启动命令
560
503
  const { command, terminalId, terminalName } = getTerminalLaunchCommand(
561
504
  normalizedCwd,
562
- finalSessionId,
563
- targetTool || sourceType
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 { MODEL_METADATA, resolveModelMetadata, METADATA_LAST_UPDATED } = require('../../config/model-metadata');
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
- // GET /api/settings/model-metadata - 获取内置模型元数据表(limit + pricing)
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
- limit: { ...meta.limit, ...(overrides[id].limit || {}) },
76
- pricing: { ...meta.pricing, ...(overrides[id].pricing || {}) }
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
- lastUpdated: METADATA_LAST_UPDATED
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
- // POST /api/settings/model-metadata - 保存模型元数据覆盖项
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 (!overrides || typeof overrides !== 'object') {
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
- for (const [modelId, meta] of Object.entries(overrides)) {
109
- if (typeof modelId !== 'string' || !modelId.trim()) {
110
- return res.status(400).json({ error: `Invalid model ID: "${modelId}"` });
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.context !== undefined && (typeof meta.limit.context !== 'number' || meta.limit.context <= 0)) {
117
- return res.status(400).json({ error: `${modelId}: limit.context must be a positive number` });
118
- }
119
- if (meta.limit.output !== undefined && (typeof meta.limit.output !== 'number' || meta.limit.output <= 0)) {
120
- return res.status(400).json({ error: `${modelId}: limit.output must be a positive number` });
121
- }
122
- }
123
- if (meta.pricing !== undefined) {
124
- if (typeof meta.pricing !== 'object') {
125
- return res.status(400).json({ error: `${modelId}: pricing must be an object` });
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
- for (const field of ['input', 'output']) {
128
- if (meta.pricing[field] !== undefined && (typeof meta.pricing[field] !== 'number' || meta.pricing[field] < 0)) {
129
- return res.status(400).json({ error: `${modelId}: pricing.${field} must be a non-negative number` });
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({ success: true, overrides });
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
- router.delete('/model-metadata/:modelId', (req, res) => {
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;
@@ -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 { resolvePricing } = require('./utils/pricing');
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
- // Claude 模型使用 config/model-pricing.js 中的集中定价
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 pricing;
147
-
148
- // 首先检查是否是 Claude 模型,使用集中定价
149
- if (model.startsWith('claude-') || model.toLowerCase().includes('claude')) {
150
- pricing = CLAUDE_MODEL_PRICING[model];
151
-
152
- // 如果没有精确匹配,尝试模糊匹配 Claude 模型
153
- if (!pricing) {
154
- const modelLower = model.toLowerCase();
155
- // 查找最接近的 Claude 模型
156
- for (const [key, value] of Object.entries(CLAUDE_MODEL_PRICING)) {
157
- if (key.toLowerCase().includes(modelLower) || modelLower.includes(key.toLowerCase())) {
158
- pricing = value;
159
- break;
160
- }
161
- }
162
- }
163
-
164
- // 如果仍然没有找到,使用默认 Sonnet 定价
165
- if (!pricing) {
166
- pricing = CLAUDE_MODEL_PRICING['claude-sonnet-4-5-20250929'];
167
- }
168
- } else {
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 { resolvePricing } = require('./utils/pricing');
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 pricing = PRICING[model];
83
+ let fallbackPricing = PRICING[model];
83
84
 
84
85
  // 如果没有精确匹配,尝试模糊匹配
85
- if (!pricing) {
86
- const modelLower = model.toLowerCase();
86
+ if (!fallbackPricing) {
87
+ const modelLower = String(model || '').toLowerCase();
87
88
  if (modelLower.includes('gemini-2.5-pro')) {
88
- pricing = PRICING['gemini-2.5-pro'];
89
+ fallbackPricing = PRICING['gemini-2.5-pro'];
89
90
  } else if (modelLower.includes('gemini-2.5-flash')) {
90
- pricing = PRICING['gemini-2.5-flash'];
91
+ fallbackPricing = PRICING['gemini-2.5-flash'];
91
92
  } else if (modelLower.includes('gemini-2.0-flash-thinking')) {
92
- pricing = PRICING['gemini-2.0-flash-thinking-exp-1219'];
93
+ fallbackPricing = PRICING['gemini-2.0-flash-thinking-exp-1219'];
93
94
  } else if (modelLower.includes('gemini-2.0-flash')) {
94
- pricing = PRICING['gemini-2.0-flash-exp'];
95
+ fallbackPricing = PRICING['gemini-2.0-flash-exp'];
95
96
  } else if (modelLower.includes('gemini-1.5-pro')) {
96
- pricing = PRICING['gemini-1.5-pro'];
97
+ fallbackPricing = PRICING['gemini-1.5-pro'];
97
98
  } else if (modelLower.includes('gemini-1.5-flash-8b')) {
98
- pricing = PRICING['gemini-1.5-flash-8b'];
99
+ fallbackPricing = PRICING['gemini-1.5-flash-8b'];
99
100
  } else if (modelLower.includes('gemini-1.5-flash')) {
100
- pricing = PRICING['gemini-1.5-flash'];
101
+ fallbackPricing = PRICING['gemini-1.5-flash'];
101
102
  } else if (modelLower.includes('gemini-1.0-pro')) {
102
- pricing = PRICING['gemini-1.0-pro'];
103
+ fallbackPricing = PRICING['gemini-1.0-pro'];
103
104
  } else if (modelLower.includes('gemini-pro')) {
104
- pricing = PRICING['gemini-pro'];
105
+ fallbackPricing = PRICING['gemini-pro'];
105
106
  }
106
107
  }
107
108
 
108
- pricing = resolvePricing('gemini', pricing, GEMINI_BASE_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
 
@@ -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'));