@adversity/coding-tool-x 3.1.1 → 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 +41 -0
- package/dist/web/assets/Analytics-BIqc8Rin.css +1 -0
- package/dist/web/assets/Analytics-D2V09DHH.js +39 -0
- package/dist/web/assets/{ConfigTemplates-ZrK_s7ma.js → ConfigTemplates-Bf_11LhH.js} +1 -1
- package/dist/web/assets/Home-BRnW4FTS.js +1 -0
- package/dist/web/assets/Home-CyCIx4BA.css +1 -0
- package/dist/web/assets/{PluginManager-BD7QUZbU.js → PluginManager-B9J32GhW.js} +1 -1
- package/dist/web/assets/{ProjectList-DRb1DuHV.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-C1xG5B4Q.js → SkillManager-CVBr0CLi.js} +1 -1
- package/dist/web/assets/{Terminal-DksBo_lM.js → Terminal-D2Xe_Q0H.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-Burx7XOo.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 -27
- package/src/config/loader.js +6 -3
- package/src/config/model-metadata.js +167 -0
- package/src/config/model-metadata.json +125 -0
- package/src/config/model-pricing.js +23 -93
- 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 +108 -56
- package/src/server/api/opencode-proxy.js +42 -33
- package/src/server/api/opencode-sessions.js +4 -69
- package/src/server/api/sessions.js +11 -68
- package/src/server/api/settings.js +138 -0
- package/src/server/api/skills.js +0 -44
- package/src/server/api/statistics.js +115 -1
- package/src/server/codex-proxy-server.js +32 -59
- package/src/server/gemini-proxy-server.js +21 -18
- package/src/server/index.js +13 -7
- package/src/server/opencode-proxy-server.js +1232 -197
- package/src/server/proxy-server.js +8 -8
- package/src/server/services/codex-sessions.js +105 -6
- 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 +97 -9
- package/src/server/services/env-manager.js +29 -1
- package/src/server/services/opencode-channels.js +3 -1
- package/src/server/services/opencode-sessions.js +486 -218
- package/src/server/services/opencode-settings-manager.js +172 -36
- package/src/server/services/plugins-service.js +37 -28
- package/src/server/services/pty-manager.js +22 -18
- package/src/server/services/response-decoder.js +21 -0
- 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/src/server/websocket-server.js +24 -5
- package/dist/web/assets/Home-B8YfhZ3c.js +0 -1
- package/dist/web/assets/Home-Di2qsylF.css +0 -1
- package/dist/web/assets/SessionList-BGJWyneI.css +0 -1
- package/dist/web/assets/SessionList-lZ0LKzfT.js +0 -1
- package/dist/web/assets/icons-kcfLIMBB.js +0 -1
- package/dist/web/assets/index-Ufv5rCa5.css +0 -1
- package/dist/web/assets/index-lAkrRC3h.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
|
@@ -4,6 +4,7 @@ const {
|
|
|
4
4
|
getProjects,
|
|
5
5
|
getSessionsByProject,
|
|
6
6
|
getSessionById,
|
|
7
|
+
getSessionMessages,
|
|
7
8
|
getRecentSessions,
|
|
8
9
|
searchSessions,
|
|
9
10
|
deleteSession,
|
|
@@ -14,8 +15,6 @@ const {
|
|
|
14
15
|
const { loadAliases } = require('../services/alias');
|
|
15
16
|
const { getTerminalLaunchCommand } = require('../services/terminal-config');
|
|
16
17
|
const { broadcastLog } = require('../websocket-server');
|
|
17
|
-
const fs = require('fs');
|
|
18
|
-
const path = require('path');
|
|
19
18
|
const os = require('os');
|
|
20
19
|
|
|
21
20
|
function isNotFoundError(error) {
|
|
@@ -156,7 +155,6 @@ module.exports = (config) => {
|
|
|
156
155
|
/**
|
|
157
156
|
* GET /api/opencode/sessions/:projectName/:sessionId/messages
|
|
158
157
|
* 获取会话的消息列表
|
|
159
|
-
* Note: OpenCode 的消息存储在单独的 message 目录,暂时返回基本信息
|
|
160
158
|
*/
|
|
161
159
|
router.get('/:projectName/:sessionId/messages', (req, res) => {
|
|
162
160
|
try {
|
|
@@ -167,66 +165,10 @@ module.exports = (config) => {
|
|
|
167
165
|
const { sessionId } = req.params;
|
|
168
166
|
const { page = 1, limit = 20, order = 'desc' } = req.query;
|
|
169
167
|
const session = getSessionById(sessionId);
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const messagesDir = path.join(
|
|
173
|
-
os.homedir(), '.local', 'share', 'opencode', 'storage', 'message', sessionId
|
|
174
|
-
);
|
|
175
|
-
|
|
176
|
-
const convertedMessages = [];
|
|
177
|
-
|
|
178
|
-
if (fs.existsSync(messagesDir)) {
|
|
179
|
-
const files = fs.readdirSync(messagesDir)
|
|
180
|
-
.filter(f => f.endsWith('.json'))
|
|
181
|
-
.sort();
|
|
182
|
-
|
|
183
|
-
for (const file of files) {
|
|
184
|
-
try {
|
|
185
|
-
const content = fs.readFileSync(path.join(messagesDir, file), 'utf8');
|
|
186
|
-
const msg = JSON.parse(content);
|
|
187
|
-
|
|
188
|
-
if (msg.role === 'user') {
|
|
189
|
-
// 提取用户消息内容
|
|
190
|
-
let textContent = '';
|
|
191
|
-
if (Array.isArray(msg.content)) {
|
|
192
|
-
textContent = msg.content
|
|
193
|
-
.filter(c => c.type === 'text')
|
|
194
|
-
.map(c => c.text || '')
|
|
195
|
-
.join('\n');
|
|
196
|
-
} else if (typeof msg.content === 'string') {
|
|
197
|
-
textContent = msg.content;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
convertedMessages.push({
|
|
201
|
-
type: 'user',
|
|
202
|
-
content: textContent || '[空消息]',
|
|
203
|
-
timestamp: msg.time?.created ? new Date(msg.time.created).toISOString() : null,
|
|
204
|
-
model: null
|
|
205
|
-
});
|
|
206
|
-
} else if (msg.role === 'assistant') {
|
|
207
|
-
// 提取助手消息内容
|
|
208
|
-
let textContent = '';
|
|
209
|
-
if (Array.isArray(msg.content)) {
|
|
210
|
-
textContent = msg.content
|
|
211
|
-
.filter(c => c.type === 'text')
|
|
212
|
-
.map(c => c.text || '')
|
|
213
|
-
.join('\n');
|
|
214
|
-
} else if (typeof msg.content === 'string') {
|
|
215
|
-
textContent = msg.content;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
convertedMessages.push({
|
|
219
|
-
type: 'assistant',
|
|
220
|
-
content: textContent || '[空消息]',
|
|
221
|
-
timestamp: msg.time?.created ? new Date(msg.time.created).toISOString() : null,
|
|
222
|
-
model: msg.model || 'opencode'
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
} catch (parseErr) {
|
|
226
|
-
// 忽略解析错误
|
|
227
|
-
}
|
|
228
|
-
}
|
|
168
|
+
if (!session) {
|
|
169
|
+
return res.status(404).json({ error: 'Session not found' });
|
|
229
170
|
}
|
|
171
|
+
const convertedMessages = getSessionMessages(sessionId);
|
|
230
172
|
|
|
231
173
|
// 分页处理
|
|
232
174
|
const pageNum = parseInt(page);
|
|
@@ -347,13 +289,6 @@ module.exports = (config) => {
|
|
|
347
289
|
|
|
348
290
|
const { exec } = require('child_process');
|
|
349
291
|
const { projectName, sessionId } = req.params;
|
|
350
|
-
const { targetTool } = req.body || {};
|
|
351
|
-
|
|
352
|
-
if (targetTool && targetTool !== 'opencode') {
|
|
353
|
-
return res.status(400).json({
|
|
354
|
-
error: 'OpenCode 会话暂不支持直接切换到其他 CLI,请使用 OpenCode 启动'
|
|
355
|
-
});
|
|
356
|
-
}
|
|
357
292
|
|
|
358
293
|
const session = getSessionById(sessionId);
|
|
359
294
|
if (!session) {
|
|
@@ -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,6 +2,13 @@ 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 {
|
|
6
|
+
MODEL_METADATA,
|
|
7
|
+
METADATA_LAST_UPDATED,
|
|
8
|
+
getDefaultSpeedTestModels,
|
|
9
|
+
saveDefaultSpeedTestModels
|
|
10
|
+
} = require('../../config/model-metadata');
|
|
11
|
+
const { loadConfig, saveConfig } = require('../../config/loader');
|
|
5
12
|
|
|
6
13
|
// GET /api/settings/terminals - 获取可用终端列表
|
|
7
14
|
router.get('/terminals', (req, res) => {
|
|
@@ -58,4 +65,135 @@ router.post('/terminal-config', (req, res) => {
|
|
|
58
65
|
}
|
|
59
66
|
});
|
|
60
67
|
|
|
68
|
+
function handleGetModelSettings(req, res) {
|
|
69
|
+
try {
|
|
70
|
+
const config = loadConfig();
|
|
71
|
+
const overrides = config.modelMetadataOverrides || {};
|
|
72
|
+
const defaultSpeedTestModels = getDefaultSpeedTestModels();
|
|
73
|
+
|
|
74
|
+
// Build merged table: built-in + user overrides
|
|
75
|
+
const merged = {};
|
|
76
|
+
for (const [id, meta] of Object.entries(MODEL_METADATA)) {
|
|
77
|
+
merged[id] = overrides[id]
|
|
78
|
+
? {
|
|
79
|
+
limit: { ...meta.limit, ...(overrides[id].limit || {}) },
|
|
80
|
+
pricing: { ...meta.pricing, ...(overrides[id].pricing || {}) }
|
|
81
|
+
}
|
|
82
|
+
: meta;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Also include any user-added custom models from overrides
|
|
86
|
+
for (const [id, meta] of Object.entries(overrides)) {
|
|
87
|
+
if (!merged[id]) {
|
|
88
|
+
merged[id] = meta;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
res.json({
|
|
93
|
+
models: merged,
|
|
94
|
+
overrides,
|
|
95
|
+
builtinModelIds: Object.keys(MODEL_METADATA),
|
|
96
|
+
lastUpdated: METADATA_LAST_UPDATED,
|
|
97
|
+
defaultSpeedTestModels
|
|
98
|
+
});
|
|
99
|
+
} catch (error) {
|
|
100
|
+
console.error('Error getting model metadata:', error);
|
|
101
|
+
res.status(500).json({ error: error.message });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// GET /api/settings/model-settings - 获取模型设置(元数据 + 默认测速模型)
|
|
106
|
+
router.get('/model-settings', handleGetModelSettings);
|
|
107
|
+
// backward compatibility
|
|
108
|
+
router.get('/model-metadata', handleGetModelSettings);
|
|
109
|
+
|
|
110
|
+
function handleSaveModelSettings(req, res) {
|
|
111
|
+
try {
|
|
112
|
+
const { overrides, defaultSpeedTestModels } = req.body || {};
|
|
113
|
+
if (overrides !== undefined && (typeof overrides !== 'object' || overrides === null || Array.isArray(overrides))) {
|
|
114
|
+
return res.status(400).json({ error: 'overrides must be an object' });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Validate each override entry
|
|
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}"` });
|
|
122
|
+
}
|
|
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
|
+
}
|
|
133
|
+
}
|
|
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
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const config = loadConfig();
|
|
148
|
+
const newConfig = {
|
|
149
|
+
...config,
|
|
150
|
+
projectsDir: config.projectsDir.replace(require('os').homedir(), '~'),
|
|
151
|
+
modelMetadataOverrides: overrides && typeof overrides === 'object'
|
|
152
|
+
? overrides
|
|
153
|
+
: (config.modelMetadataOverrides || {})
|
|
154
|
+
};
|
|
155
|
+
saveConfig(newConfig);
|
|
156
|
+
const persistedDefaultSpeedTestModels = saveDefaultSpeedTestModels(defaultSpeedTestModels);
|
|
157
|
+
|
|
158
|
+
res.json({
|
|
159
|
+
success: true,
|
|
160
|
+
overrides: newConfig.modelMetadataOverrides,
|
|
161
|
+
defaultSpeedTestModels: persistedDefaultSpeedTestModels
|
|
162
|
+
});
|
|
163
|
+
} catch (error) {
|
|
164
|
+
console.error('Error saving model metadata:', error);
|
|
165
|
+
res.status(500).json({ error: error.message });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// POST /api/settings/model-settings - 保存模型设置
|
|
170
|
+
router.post('/model-settings', handleSaveModelSettings);
|
|
171
|
+
// backward compatibility
|
|
172
|
+
router.post('/model-metadata', handleSaveModelSettings);
|
|
173
|
+
|
|
174
|
+
// DELETE /api/settings/model-metadata/:modelId - 删除单个模型覆盖项(恢复内置默认值)
|
|
175
|
+
function handleDeleteModelOverride(req, res) {
|
|
176
|
+
try {
|
|
177
|
+
const modelId = decodeURIComponent(req.params.modelId);
|
|
178
|
+
const config = loadConfig();
|
|
179
|
+
const overrides = { ...(config.modelMetadataOverrides || {}) };
|
|
180
|
+
delete overrides[modelId];
|
|
181
|
+
|
|
182
|
+
const newConfig = {
|
|
183
|
+
...config,
|
|
184
|
+
projectsDir: config.projectsDir.replace(require('os').homedir(), '~'),
|
|
185
|
+
modelMetadataOverrides: overrides
|
|
186
|
+
};
|
|
187
|
+
saveConfig(newConfig);
|
|
188
|
+
|
|
189
|
+
res.json({ success: true, modelId });
|
|
190
|
+
} catch (error) {
|
|
191
|
+
console.error('Error deleting model metadata override:', error);
|
|
192
|
+
res.status(500).json({ error: error.message });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
router.delete('/model-settings/:modelId', handleDeleteModelOverride);
|
|
197
|
+
router.delete('/model-metadata/:modelId', handleDeleteModelOverride);
|
|
198
|
+
|
|
61
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;
|