@adversity/coding-tool-x 3.1.0 → 3.1.1
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 +15 -18
- package/README.md +8 -8
- package/dist/web/assets/ConfigTemplates-Bidwfdf2.css +1 -0
- package/dist/web/assets/ConfigTemplates-ZrK_s7ma.js +1 -0
- package/dist/web/assets/Home-B8YfhZ3c.js +1 -0
- package/dist/web/assets/Home-Di2qsylF.css +1 -0
- package/dist/web/assets/PluginManager-BD7QUZbU.js +1 -0
- package/dist/web/assets/PluginManager-ROyoZ-6m.css +1 -0
- package/dist/web/assets/ProjectList-C1fQb9OW.css +1 -0
- package/dist/web/assets/ProjectList-DRb1DuHV.js +1 -0
- package/dist/web/assets/SessionList-BGJWyneI.css +1 -0
- package/dist/web/assets/SessionList-lZ0LKzfT.js +1 -0
- package/dist/web/assets/SkillManager-C1xG5B4Q.js +1 -0
- package/dist/web/assets/SkillManager-D7pd-d_P.css +1 -0
- package/dist/web/assets/Terminal-DGNJeVtc.css +1 -0
- package/dist/web/assets/Terminal-DksBo_lM.js +1 -0
- package/dist/web/assets/WorkspaceManager-Burx7XOo.js +1 -0
- package/dist/web/assets/WorkspaceManager-CrwgQgmP.css +1 -0
- package/dist/web/assets/icons-kcfLIMBB.js +1 -0
- package/dist/web/assets/index-Ufv5rCa5.css +1 -0
- package/dist/web/assets/index-lAkrRC3h.js +2 -0
- package/dist/web/assets/markdown-BfC0goYb.css +10 -0
- package/dist/web/assets/markdown-C9MYpaSi.js +1 -0
- package/dist/web/assets/naive-ui-CSrLusZZ.js +1 -0
- package/dist/web/assets/{vendors-D2HHw_aW.js → vendors-CO3Upi1d.js} +2 -2
- package/dist/web/assets/vue-vendor-DqyWIXEb.js +45 -0
- package/dist/web/assets/xterm-6GBZ9nXN.css +32 -0
- package/dist/web/assets/xterm-BJzAjXCH.js +13 -0
- package/dist/web/index.html +8 -6
- package/package.json +4 -2
- package/src/commands/channels.js +48 -1
- package/src/commands/cli-type.js +4 -2
- package/src/commands/daemon.js +81 -12
- package/src/commands/doctor.js +10 -9
- package/src/commands/list.js +1 -1
- package/src/commands/logs.js +6 -4
- package/src/commands/port-config.js +24 -4
- package/src/commands/proxy-control.js +12 -6
- package/src/commands/search.js +1 -1
- package/src/commands/security.js +3 -2
- package/src/commands/stats.js +226 -52
- package/src/commands/switch.js +1 -1
- package/src/commands/toggle-proxy.js +31 -6
- package/src/commands/update.js +97 -0
- package/src/commands/workspace.js +1 -1
- package/src/config/default.js +39 -2
- package/src/config/loader.js +74 -8
- package/src/config/paths.js +105 -33
- package/src/index.js +64 -3
- package/src/plugins/constants.js +3 -2
- package/src/plugins/plugin-api.js +1 -1
- package/src/reset-config.js +4 -2
- package/src/server/api/agents.js +57 -14
- package/src/server/api/channels.js +112 -33
- package/src/server/api/codex-channels.js +111 -18
- package/src/server/api/codex-proxy.js +14 -8
- package/src/server/api/commands.js +71 -18
- package/src/server/api/config-export.js +0 -6
- package/src/server/api/config-registry.js +11 -3
- package/src/server/api/config.js +376 -5
- package/src/server/api/convert.js +133 -0
- package/src/server/api/dashboard.js +22 -6
- package/src/server/api/gemini-channels.js +107 -18
- package/src/server/api/gemini-proxy.js +14 -8
- package/src/server/api/gemini-sessions.js +1 -1
- package/src/server/api/health-check.js +4 -3
- package/src/server/api/mcp.js +3 -3
- package/src/server/api/opencode-channels.js +419 -0
- package/src/server/api/opencode-projects.js +99 -0
- package/src/server/api/opencode-proxy.js +198 -0
- package/src/server/api/opencode-sessions.js +403 -0
- package/src/server/api/opencode-statistics.js +57 -0
- package/src/server/api/plugins.js +66 -19
- package/src/server/api/prompts.js +2 -2
- package/src/server/api/proxy.js +7 -4
- package/src/server/api/sessions.js +3 -0
- package/src/server/api/skills.js +69 -18
- package/src/server/api/workspaces.js +78 -6
- package/src/server/codex-proxy-server.js +30 -18
- package/src/server/dev-server.js +1 -1
- package/src/server/gemini-proxy-server.js +15 -3
- package/src/server/index.js +165 -58
- package/src/server/opencode-proxy-server.js +4375 -0
- package/src/server/proxy-server.js +27 -18
- package/src/server/services/agents-service.js +61 -24
- package/src/server/services/channel-scheduler.js +9 -5
- package/src/server/services/channels.js +64 -37
- package/src/server/services/codex-channels.js +56 -43
- package/src/server/services/codex-settings-manager.js +271 -49
- package/src/server/services/codex-statistics-service.js +2 -2
- package/src/server/services/commands-service.js +84 -25
- package/src/server/services/config-export-service.js +7 -45
- package/src/server/services/config-registry-service.js +63 -17
- package/src/server/services/config-sync-manager.js +160 -7
- package/src/server/services/config-templates-service.js +204 -51
- package/src/server/services/env-checker.js +26 -12
- package/src/server/services/env-manager.js +126 -18
- package/src/server/services/favorites.js +5 -3
- package/src/server/services/gemini-channels.js +33 -44
- package/src/server/services/gemini-statistics-service.js +2 -2
- package/src/server/services/mcp-service.js +350 -9
- package/src/server/services/model-detector.js +707 -221
- package/src/server/services/network-access.js +80 -0
- package/src/server/services/opencode-channels.js +206 -0
- package/src/server/services/opencode-gateway-converter.js +639 -0
- package/src/server/services/opencode-sessions.js +663 -0
- package/src/server/services/opencode-settings-manager.js +342 -0
- package/src/server/services/opencode-statistics-service.js +255 -0
- package/src/server/services/plugins-service.js +479 -22
- package/src/server/services/prompts-service.js +53 -11
- package/src/server/services/proxy-runtime.js +1 -1
- package/src/server/services/repo-scanner-base.js +1 -1
- package/src/server/services/security-config.js +1 -1
- package/src/server/services/session-cache.js +1 -1
- package/src/server/services/skill-service.js +300 -46
- package/src/server/services/speed-test.js +464 -186
- package/src/server/services/statistics-service.js +2 -2
- package/src/server/services/terminal-commands.js +10 -3
- package/src/server/services/terminal-config.js +1 -1
- package/src/server/services/ui-config.js +1 -1
- package/src/server/services/workspace-service.js +57 -100
- package/src/server/websocket-server.js +132 -3
- package/src/ui/menu.js +49 -40
- package/src/utils/port-helper.js +22 -8
- package/src/utils/session.js +5 -4
- package/dist/web/assets/icons-CO_2OFES.js +0 -1
- package/dist/web/assets/index-DI8QOi-E.js +0 -14
- package/dist/web/assets/index-uLHGdeZh.css +0 -41
- package/dist/web/assets/naive-ui-B1re3c-e.js +0 -1
- package/dist/web/assets/vue-vendor-6JaYHOiI.js +0 -44
- package/src/server/api/oauth.js +0 -294
- package/src/server/api/permissions.js +0 -385
- package/src/server/config/oauth-providers.js +0 -68
- package/src/server/services/oauth-callback-server.js +0 -284
- package/src/server/services/oauth-service.js +0 -378
- package/src/server/services/oauth-token-storage.js +0 -135
- package/src/server/services/permission-templates-service.js +0 -308
package/src/commands/stats.js
CHANGED
|
@@ -2,12 +2,20 @@ const chalk = require('chalk');
|
|
|
2
2
|
const http = require('http');
|
|
3
3
|
const { loadConfig } = require('../config/loader');
|
|
4
4
|
|
|
5
|
+
const TOOL_TYPES = ['claude', 'codex', 'gemini', 'opencode'];
|
|
6
|
+
const TOOL_ENDPOINTS = {
|
|
7
|
+
claude: '/api/statistics',
|
|
8
|
+
codex: '/api/codex/statistics',
|
|
9
|
+
gemini: '/api/gemini/statistics',
|
|
10
|
+
opencode: '/api/opencode/statistics'
|
|
11
|
+
};
|
|
12
|
+
|
|
5
13
|
/**
|
|
6
14
|
* HTTP 请求辅助函数
|
|
7
15
|
*/
|
|
8
16
|
function httpRequest(method, path, data = null) {
|
|
9
17
|
const config = loadConfig();
|
|
10
|
-
const port = config.ports?.webUI ||
|
|
18
|
+
const port = config.ports?.webUI || 19999;
|
|
11
19
|
|
|
12
20
|
return new Promise((resolve, reject) => {
|
|
13
21
|
const postData = data ? JSON.stringify(data) : null;
|
|
@@ -62,13 +70,167 @@ function httpRequest(method, path, data = null) {
|
|
|
62
70
|
*/
|
|
63
71
|
async function checkUIService() {
|
|
64
72
|
try {
|
|
65
|
-
await httpRequest('GET', '/api/
|
|
73
|
+
await httpRequest('GET', '/api/proxy/status');
|
|
66
74
|
return true;
|
|
67
75
|
} catch (err) {
|
|
68
76
|
return false;
|
|
69
77
|
}
|
|
70
78
|
}
|
|
71
79
|
|
|
80
|
+
function validateToolType(type) {
|
|
81
|
+
if (!type) return true;
|
|
82
|
+
return TOOL_TYPES.includes(type);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function getDateString(offsetDays = 0) {
|
|
86
|
+
const date = new Date();
|
|
87
|
+
date.setHours(0, 0, 0, 0);
|
|
88
|
+
date.setDate(date.getDate() - offsetDays);
|
|
89
|
+
const y = date.getFullYear();
|
|
90
|
+
const m = String(date.getMonth() + 1).padStart(2, '0');
|
|
91
|
+
const d = String(date.getDate()).padStart(2, '0');
|
|
92
|
+
return `${y}-${m}-${d}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function emptySummary() {
|
|
96
|
+
return {
|
|
97
|
+
requests: 0,
|
|
98
|
+
tokens: 0,
|
|
99
|
+
cost: 0,
|
|
100
|
+
inputTokens: 0,
|
|
101
|
+
outputTokens: 0,
|
|
102
|
+
cacheCreation: 0,
|
|
103
|
+
cacheRead: 0,
|
|
104
|
+
reasoningTokens: 0,
|
|
105
|
+
cachedTokens: 0
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function normalizeNumber(value) {
|
|
110
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
111
|
+
if (typeof value === 'string') {
|
|
112
|
+
const parsed = Number(value);
|
|
113
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
114
|
+
}
|
|
115
|
+
return 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function extractSummary(stats) {
|
|
119
|
+
const summary = emptySummary();
|
|
120
|
+
const sourceSummary = stats?.summary || {};
|
|
121
|
+
const sourceGlobal = stats?.global || {};
|
|
122
|
+
|
|
123
|
+
summary.requests = normalizeNumber(
|
|
124
|
+
sourceSummary.totalRequests !== undefined ? sourceSummary.totalRequests : sourceSummary.requests
|
|
125
|
+
) || normalizeNumber(sourceGlobal.totalRequests);
|
|
126
|
+
|
|
127
|
+
summary.tokens = normalizeNumber(
|
|
128
|
+
sourceSummary.totalTokens !== undefined ? sourceSummary.totalTokens : sourceSummary.tokens
|
|
129
|
+
) || normalizeNumber(sourceGlobal.totalTokens);
|
|
130
|
+
|
|
131
|
+
summary.cost = normalizeNumber(
|
|
132
|
+
sourceSummary.totalCost !== undefined ? sourceSummary.totalCost : sourceSummary.cost
|
|
133
|
+
) || normalizeNumber(sourceGlobal.totalCost);
|
|
134
|
+
|
|
135
|
+
summary.inputTokens = normalizeNumber(sourceSummary.inputTokens ?? sourceSummary.input);
|
|
136
|
+
summary.outputTokens = normalizeNumber(sourceSummary.outputTokens ?? sourceSummary.output);
|
|
137
|
+
summary.cacheCreation = normalizeNumber(sourceSummary.cacheCreation ?? sourceSummary.cache_creation);
|
|
138
|
+
summary.cacheRead = normalizeNumber(sourceSummary.cacheRead ?? sourceSummary.cache_read);
|
|
139
|
+
summary.reasoningTokens = normalizeNumber(sourceSummary.reasoningTokens ?? sourceSummary.reasoning);
|
|
140
|
+
summary.cachedTokens = normalizeNumber(sourceSummary.cachedTokens ?? sourceSummary.cached);
|
|
141
|
+
|
|
142
|
+
const detailedTotal =
|
|
143
|
+
summary.inputTokens +
|
|
144
|
+
summary.outputTokens +
|
|
145
|
+
summary.cacheCreation +
|
|
146
|
+
summary.cacheRead +
|
|
147
|
+
summary.reasoningTokens +
|
|
148
|
+
summary.cachedTokens;
|
|
149
|
+
|
|
150
|
+
if (!summary.tokens && detailedTotal > 0) {
|
|
151
|
+
summary.tokens = detailedTotal;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return summary;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function mergeSummaries(target, source) {
|
|
158
|
+
target.requests += normalizeNumber(source.requests);
|
|
159
|
+
target.tokens += normalizeNumber(source.tokens);
|
|
160
|
+
target.cost += normalizeNumber(source.cost);
|
|
161
|
+
target.inputTokens += normalizeNumber(source.inputTokens);
|
|
162
|
+
target.outputTokens += normalizeNumber(source.outputTokens);
|
|
163
|
+
target.cacheCreation += normalizeNumber(source.cacheCreation);
|
|
164
|
+
target.cacheRead += normalizeNumber(source.cacheRead);
|
|
165
|
+
target.reasoningTokens += normalizeNumber(source.reasoningTokens);
|
|
166
|
+
target.cachedTokens += normalizeNumber(source.cachedTokens);
|
|
167
|
+
return target;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function getRangeDays(timeRange) {
|
|
171
|
+
if (timeRange === 'week') return 7;
|
|
172
|
+
if (timeRange === 'month') return 30;
|
|
173
|
+
return 0;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function fetchToolStats(toolType, timeRange) {
|
|
177
|
+
const endpointBase = TOOL_ENDPOINTS[toolType];
|
|
178
|
+
if (!endpointBase) {
|
|
179
|
+
throw new Error(`不支持的渠道类型: ${toolType}`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (timeRange === 'today') {
|
|
183
|
+
const response = await httpRequest('GET', `${endpointBase}/today`);
|
|
184
|
+
return extractSummary(response.data);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (timeRange === 'all') {
|
|
188
|
+
const response = await httpRequest('GET', `${endpointBase}/summary`);
|
|
189
|
+
return extractSummary(response.data);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const days = getRangeDays(timeRange);
|
|
193
|
+
const merged = emptySummary();
|
|
194
|
+
for (let i = 0; i < days; i++) {
|
|
195
|
+
const date = getDateString(i);
|
|
196
|
+
const response = await httpRequest('GET', `${endpointBase}/daily/${date}`);
|
|
197
|
+
const dailySummary = extractSummary(response.data);
|
|
198
|
+
mergeSummaries(merged, dailySummary);
|
|
199
|
+
}
|
|
200
|
+
return merged;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function fetchOverallStats(timeRange) {
|
|
204
|
+
const byToolType = {};
|
|
205
|
+
const summary = emptySummary();
|
|
206
|
+
|
|
207
|
+
for (const toolType of TOOL_TYPES) {
|
|
208
|
+
const toolSummary = await fetchToolStats(toolType, timeRange);
|
|
209
|
+
byToolType[toolType] = toolSummary;
|
|
210
|
+
mergeSummaries(summary, toolSummary);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return { summary, byToolType };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function buildDisplayPayload(type, timeRange, data) {
|
|
217
|
+
if (type) {
|
|
218
|
+
return {
|
|
219
|
+
type,
|
|
220
|
+
timeRange,
|
|
221
|
+
summary: data,
|
|
222
|
+
byToolType: null
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
type: null,
|
|
228
|
+
timeRange,
|
|
229
|
+
summary: data.summary,
|
|
230
|
+
byToolType: data.byToolType
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
72
234
|
/**
|
|
73
235
|
* 查看统计信息
|
|
74
236
|
*/
|
|
@@ -84,23 +246,22 @@ async function handleStats(type = null, options = {}) {
|
|
|
84
246
|
const timeRange = options.today ? 'today' : options.week ? 'week' : options.month ? 'month' : 'all';
|
|
85
247
|
|
|
86
248
|
try {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
console.error(chalk.red(`\n❌ 无效的渠道类型: ${type}\n`));
|
|
92
|
-
console.log(chalk.gray('支持的类型: claude, codex, gemini\n'));
|
|
93
|
-
process.exit(1);
|
|
94
|
-
}
|
|
95
|
-
endpoint += `/${type}`;
|
|
249
|
+
if (!validateToolType(type)) {
|
|
250
|
+
console.error(chalk.red(`\n❌ 无效的渠道类型: ${type}\n`));
|
|
251
|
+
console.log(chalk.gray('支持的类型: claude, codex, gemini, opencode\n'));
|
|
252
|
+
process.exit(1);
|
|
96
253
|
}
|
|
97
254
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
255
|
+
let payload;
|
|
256
|
+
if (type) {
|
|
257
|
+
const summary = await fetchToolStats(type, timeRange);
|
|
258
|
+
payload = buildDisplayPayload(type, timeRange, summary);
|
|
259
|
+
} else {
|
|
260
|
+
const overall = await fetchOverallStats(timeRange);
|
|
261
|
+
payload = buildDisplayPayload(null, timeRange, overall);
|
|
262
|
+
}
|
|
102
263
|
|
|
103
|
-
displayStats(
|
|
264
|
+
displayStats(payload);
|
|
104
265
|
} catch (error) {
|
|
105
266
|
console.error(chalk.red(`\n❌ 获取统计失败: ${error.message}\n`));
|
|
106
267
|
process.exit(1);
|
|
@@ -110,7 +271,9 @@ async function handleStats(type = null, options = {}) {
|
|
|
110
271
|
/**
|
|
111
272
|
* 显示统计信息
|
|
112
273
|
*/
|
|
113
|
-
function displayStats(stats
|
|
274
|
+
function displayStats(stats) {
|
|
275
|
+
const type = stats.type;
|
|
276
|
+
const timeRange = stats.timeRange;
|
|
114
277
|
const title = type ? `${type.toUpperCase()} 统计信息` : '总体统计信息';
|
|
115
278
|
const rangeText = {
|
|
116
279
|
today: '今日',
|
|
@@ -132,51 +295,44 @@ function displayStats(stats, type, timeRange) {
|
|
|
132
295
|
|
|
133
296
|
// 请求统计
|
|
134
297
|
console.log(chalk.bold('📊 请求统计:'));
|
|
135
|
-
console.log(chalk.gray(
|
|
136
|
-
console.log(chalk.gray(` 成功请求: `) + chalk.green(summary.successfulRequests || 0));
|
|
137
|
-
console.log(chalk.gray(` 失败请求: `) + chalk.red(summary.failedRequests || 0));
|
|
298
|
+
console.log(chalk.gray(' 总请求数: ') + chalk.cyan(formatNumber(summary.requests)));
|
|
138
299
|
|
|
139
300
|
// Token 使用
|
|
140
|
-
if (summary.
|
|
301
|
+
if (summary.tokens !== undefined) {
|
|
141
302
|
console.log(chalk.bold('\n🎯 Token 使用:'));
|
|
142
|
-
console.log(chalk.gray(
|
|
143
|
-
console.log(chalk.gray(
|
|
144
|
-
console.log(chalk.gray(
|
|
145
|
-
console.log(chalk.gray(
|
|
146
|
-
console.log(chalk.gray(
|
|
303
|
+
console.log(chalk.gray(' 输入 Tokens: ') + chalk.cyan(formatNumber(summary.inputTokens || 0)));
|
|
304
|
+
console.log(chalk.gray(' 输出 Tokens: ') + chalk.cyan(formatNumber(summary.outputTokens || 0)));
|
|
305
|
+
console.log(chalk.gray(' 缓存创建: ') + chalk.cyan(formatNumber(summary.cacheCreation || 0)));
|
|
306
|
+
console.log(chalk.gray(' 缓存读取: ') + chalk.cyan(formatNumber(summary.cacheRead || 0)));
|
|
307
|
+
console.log(chalk.gray(' 推理 Tokens: ') + chalk.cyan(formatNumber(summary.reasoningTokens || 0)));
|
|
308
|
+
console.log(chalk.gray(' 缓存 Tokens: ') + chalk.cyan(formatNumber(summary.cachedTokens || 0)));
|
|
309
|
+
console.log(chalk.gray(' 总计: ') + chalk.bold.cyan(formatNumber(summary.tokens || 0)));
|
|
147
310
|
}
|
|
148
311
|
|
|
149
312
|
// 成本统计
|
|
150
|
-
if (summary.
|
|
313
|
+
if (summary.cost !== undefined) {
|
|
151
314
|
console.log(chalk.bold('\n💰 成本统计:'));
|
|
152
|
-
console.log(chalk.gray(
|
|
153
|
-
if (summary.averageCost !== undefined) {
|
|
154
|
-
console.log(chalk.gray(` 平均成本: `) + chalk.yellow(`$${(summary.averageCost || 0).toFixed(4)}`));
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// 按渠道统计(仅在总体统计时显示)
|
|
159
|
-
if (!type && stats.byChannel) {
|
|
160
|
-
console.log(chalk.bold('\n📡 按渠道统计:'));
|
|
161
|
-
Object.entries(stats.byChannel).forEach(([channel, data]) => {
|
|
162
|
-
const icon = channel === 'claude' ? '🟢' : channel === 'codex' ? '🔵' : '🟣';
|
|
163
|
-
console.log(chalk.gray(` ${icon} ${channel.toUpperCase()}:`));
|
|
164
|
-
console.log(chalk.gray(` 请求: ${data.requests || 0} | Tokens: ${formatNumber(data.tokens || 0)} | 成本: $${(data.cost || 0).toFixed(4)}`));
|
|
165
|
-
});
|
|
315
|
+
console.log(chalk.gray(' 总成本: ') + chalk.yellow(`$${normalizeNumber(summary.cost).toFixed(4)}`));
|
|
166
316
|
}
|
|
167
317
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
console.log(chalk.bold('\n
|
|
171
|
-
|
|
172
|
-
const
|
|
173
|
-
console.log(chalk.gray(` ${
|
|
318
|
+
if (!type && stats.byToolType) {
|
|
319
|
+
const iconMap = { claude: '🟢', codex: '🔵', gemini: '🟣', opencode: '🟠' };
|
|
320
|
+
console.log(chalk.bold('\n📡 分渠道汇总:'));
|
|
321
|
+
TOOL_TYPES.forEach((toolType) => {
|
|
322
|
+
const item = stats.byToolType[toolType] || emptySummary();
|
|
323
|
+
console.log(chalk.gray(` ${iconMap[toolType]} ${toolType.toUpperCase()}:`));
|
|
324
|
+
console.log(
|
|
325
|
+
chalk.gray(
|
|
326
|
+
` 请求: ${formatNumber(item.requests)} | Tokens: ${formatNumber(item.tokens)} | 成本: $${normalizeNumber(item.cost).toFixed(4)}`
|
|
327
|
+
)
|
|
328
|
+
);
|
|
174
329
|
});
|
|
175
330
|
}
|
|
176
331
|
|
|
177
332
|
console.log(chalk.gray('\n💡 提示:'));
|
|
178
333
|
console.log(chalk.gray(' • 使用 ') + chalk.cyan('ctx stats --today') + chalk.gray(' 查看今日统计'));
|
|
179
334
|
console.log(chalk.gray(' • 使用 ') + chalk.cyan('ctx stats claude') + chalk.gray(' 查看特定渠道'));
|
|
335
|
+
console.log(chalk.gray(' • 使用 ') + chalk.cyan('ctx stats opencode') + chalk.gray(' 查看 OpenCode 统计'));
|
|
180
336
|
console.log(chalk.gray(' • 使用 ') + chalk.cyan('ctx stats export') + chalk.gray(' 导出统计数据\n'));
|
|
181
337
|
}
|
|
182
338
|
|
|
@@ -184,7 +340,8 @@ function displayStats(stats, type, timeRange) {
|
|
|
184
340
|
* 格式化数字
|
|
185
341
|
*/
|
|
186
342
|
function formatNumber(num) {
|
|
187
|
-
|
|
343
|
+
const normalized = normalizeNumber(num);
|
|
344
|
+
return normalized.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
188
345
|
}
|
|
189
346
|
|
|
190
347
|
/**
|
|
@@ -200,15 +357,32 @@ async function handleStatsExport(type = null, format = 'json') {
|
|
|
200
357
|
}
|
|
201
358
|
|
|
202
359
|
try {
|
|
203
|
-
|
|
204
|
-
|
|
360
|
+
if (!validateToolType(type)) {
|
|
361
|
+
console.error(chalk.red(`\n❌ 无效的渠道类型: ${type}\n`));
|
|
362
|
+
console.log(chalk.gray('支持的类型: claude, codex, gemini, opencode\n'));
|
|
363
|
+
process.exit(1);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const exportFormat = format || 'json';
|
|
367
|
+
if (exportFormat !== 'json') {
|
|
368
|
+
console.log(chalk.yellow(`⚠️ 暂不支持 ${exportFormat} 格式,已回退为 json`));
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
let payload;
|
|
372
|
+
if (type) {
|
|
373
|
+
const summary = await fetchToolStats(type, 'all');
|
|
374
|
+
payload = buildDisplayPayload(type, 'all', summary);
|
|
375
|
+
} else {
|
|
376
|
+
const overall = await fetchOverallStats('all');
|
|
377
|
+
payload = buildDisplayPayload(null, 'all', overall);
|
|
378
|
+
}
|
|
205
379
|
|
|
206
380
|
const fs = require('fs');
|
|
207
381
|
const path = require('path');
|
|
208
|
-
const filename = `cc-tool-stats-${type || 'all'}-${Date.now()}
|
|
382
|
+
const filename = `cc-tool-stats-${type || 'all'}-${Date.now()}.json`;
|
|
209
383
|
const filepath = path.join(process.cwd(), filename);
|
|
210
384
|
|
|
211
|
-
fs.writeFileSync(filepath, JSON.stringify(
|
|
385
|
+
fs.writeFileSync(filepath, JSON.stringify(payload, null, 2));
|
|
212
386
|
|
|
213
387
|
console.log(chalk.green(`✅ 统计数据已导出\n`));
|
|
214
388
|
console.log(chalk.gray(`文件路径: ${filepath}\n`));
|
package/src/commands/switch.js
CHANGED
|
@@ -9,7 +9,7 @@ const { saveConfig } = require('../config/loader');
|
|
|
9
9
|
* 切换项目
|
|
10
10
|
*/
|
|
11
11
|
async function switchProject(config) {
|
|
12
|
-
const projects = getAvailableProjects(config);
|
|
12
|
+
const projects = await getAvailableProjects(config);
|
|
13
13
|
|
|
14
14
|
if (projects.length === 0) {
|
|
15
15
|
console.log(chalk.yellow('没有找到项目'));
|
|
@@ -5,7 +5,8 @@ const { loadConfig } = require('../config/loader');
|
|
|
5
5
|
const SETTINGS_MANAGERS = {
|
|
6
6
|
claude: () => require('../server/services/settings-manager'),
|
|
7
7
|
codex: () => require('../server/services/codex-settings-manager'),
|
|
8
|
-
gemini: () => require('../server/services/gemini-settings-manager')
|
|
8
|
+
gemini: () => require('../server/services/gemini-settings-manager'),
|
|
9
|
+
opencode: () => require('../server/services/opencode-settings-manager')
|
|
9
10
|
};
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -14,14 +15,14 @@ const SETTINGS_MANAGERS = {
|
|
|
14
15
|
function getProxyServices(cliType) {
|
|
15
16
|
if (cliType === 'claude') {
|
|
16
17
|
const { getProxyStatus, startProxyServer, stopProxyServer } = require('../server/proxy-server');
|
|
17
|
-
return { getProxyStatus, startProxyServer, stopProxyServer, defaultPort:
|
|
18
|
+
return { getProxyStatus, startProxyServer, stopProxyServer, defaultPort: 20088 };
|
|
18
19
|
} else if (cliType === 'codex') {
|
|
19
20
|
const { getCodexProxyStatus, startCodexProxyServer, stopCodexProxyServer } = require('../server/codex-proxy-server');
|
|
20
21
|
return {
|
|
21
22
|
getProxyStatus: getCodexProxyStatus,
|
|
22
23
|
startProxyServer: startCodexProxyServer,
|
|
23
24
|
stopProxyServer: stopCodexProxyServer,
|
|
24
|
-
defaultPort:
|
|
25
|
+
defaultPort: 20089
|
|
25
26
|
};
|
|
26
27
|
} else if (cliType === 'gemini') {
|
|
27
28
|
const { getGeminiProxyStatus, startGeminiProxyServer, stopGeminiProxyServer } = require('../server/gemini-proxy-server');
|
|
@@ -29,7 +30,15 @@ function getProxyServices(cliType) {
|
|
|
29
30
|
getProxyStatus: getGeminiProxyStatus,
|
|
30
31
|
startProxyServer: startGeminiProxyServer,
|
|
31
32
|
stopProxyServer: stopGeminiProxyServer,
|
|
32
|
-
defaultPort:
|
|
33
|
+
defaultPort: 20090
|
|
34
|
+
};
|
|
35
|
+
} else if (cliType === 'opencode') {
|
|
36
|
+
const { getOpenCodeProxyStatus, startOpenCodeProxyServer, stopOpenCodeProxyServer } = require('../server/opencode-proxy-server');
|
|
37
|
+
return {
|
|
38
|
+
getProxyStatus: getOpenCodeProxyStatus,
|
|
39
|
+
startProxyServer: startOpenCodeProxyServer,
|
|
40
|
+
stopProxyServer: stopOpenCodeProxyServer,
|
|
41
|
+
defaultPort: 20091
|
|
33
42
|
};
|
|
34
43
|
}
|
|
35
44
|
}
|
|
@@ -51,6 +60,10 @@ async function handleToggleProxy() {
|
|
|
51
60
|
const config = loadConfig();
|
|
52
61
|
const cliType = config.currentCliType || 'claude';
|
|
53
62
|
const services = getProxyServices(cliType);
|
|
63
|
+
if (!services) {
|
|
64
|
+
console.log(chalk.red(`\n❌ 当前 CLI 类型 (${cliType}) 暂不支持动态切换\n`));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
54
67
|
|
|
55
68
|
const proxyStatus = services.getProxyStatus();
|
|
56
69
|
|
|
@@ -72,7 +85,13 @@ async function handleStartProxy(cliType, services) {
|
|
|
72
85
|
console.log(chalk.bold.cyan('║ 开启动态切换 ║'));
|
|
73
86
|
console.log(chalk.bold.cyan('╚═══════════════════════════════════════╝\n'));
|
|
74
87
|
|
|
75
|
-
const
|
|
88
|
+
const toolNameMap = {
|
|
89
|
+
claude: 'Claude Code',
|
|
90
|
+
codex: 'Codex',
|
|
91
|
+
gemini: 'Gemini',
|
|
92
|
+
opencode: 'OpenCode'
|
|
93
|
+
};
|
|
94
|
+
const toolName = toolNameMap[cliType] || 'Claude Code';
|
|
76
95
|
const defaultPort = services.defaultPort;
|
|
77
96
|
|
|
78
97
|
console.log(chalk.cyan('动态切换功能说明:'));
|
|
@@ -153,7 +172,13 @@ async function handleStopProxy(cliType, services) {
|
|
|
153
172
|
console.log(chalk.bold.cyan('║ 关闭动态切换 ║'));
|
|
154
173
|
console.log(chalk.bold.cyan('╚═══════════════════════════════════════╝\n'));
|
|
155
174
|
|
|
156
|
-
const
|
|
175
|
+
const toolNameMap = {
|
|
176
|
+
claude: 'Claude Code',
|
|
177
|
+
codex: 'Codex',
|
|
178
|
+
gemini: 'Gemini',
|
|
179
|
+
opencode: 'OpenCode'
|
|
180
|
+
};
|
|
181
|
+
const toolName = toolNameMap[cliType] || 'Claude Code';
|
|
157
182
|
const proxyStatus = services.getProxyStatus();
|
|
158
183
|
|
|
159
184
|
console.log(chalk.cyan('当前状态:'));
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
const { spawn } = require('child_process');
|
|
2
|
+
const { promisify } = require('util');
|
|
3
|
+
const { exec } = require('child_process');
|
|
4
|
+
const semver = require('semver');
|
|
5
|
+
const chalk = require('chalk');
|
|
6
|
+
const packageInfo = require('../../package.json');
|
|
7
|
+
|
|
8
|
+
const execAsync = promisify(exec);
|
|
9
|
+
|
|
10
|
+
async function getLatestVersion(packageName) {
|
|
11
|
+
const { stdout } = await execAsync(`npm view ${packageName} version --json`, { timeout: 15000 });
|
|
12
|
+
const parsed = JSON.parse(stdout.trim());
|
|
13
|
+
if (typeof parsed === 'string') return parsed;
|
|
14
|
+
throw new Error('无法解析 npm 返回的版本号');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function runNpmInstall(packageName, version) {
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
const child = spawn('npm', ['install', '-g', `${packageName}@${version}`], {
|
|
20
|
+
stdio: 'inherit'
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
child.on('error', reject);
|
|
24
|
+
child.on('exit', (code) => {
|
|
25
|
+
if (code === 0) {
|
|
26
|
+
resolve();
|
|
27
|
+
} else {
|
|
28
|
+
reject(new Error(`npm install 失败,退出码 ${code}`));
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function handleUpdate(options = {}) {
|
|
35
|
+
const checkOnly = options.checkOnly === true;
|
|
36
|
+
const packageCandidates = Array.from(new Set([
|
|
37
|
+
packageInfo.name,
|
|
38
|
+
'coding-tool-x'
|
|
39
|
+
].filter(Boolean)));
|
|
40
|
+
const currentVersion = packageInfo.version;
|
|
41
|
+
|
|
42
|
+
console.log(chalk.cyan('\n🔍 检查更新中...\n'));
|
|
43
|
+
|
|
44
|
+
let latestVersion;
|
|
45
|
+
let packageName = packageCandidates[0];
|
|
46
|
+
try {
|
|
47
|
+
let lastError = null;
|
|
48
|
+
for (const candidate of packageCandidates) {
|
|
49
|
+
try {
|
|
50
|
+
latestVersion = await getLatestVersion(candidate);
|
|
51
|
+
packageName = candidate;
|
|
52
|
+
lastError = null;
|
|
53
|
+
break;
|
|
54
|
+
} catch (error) {
|
|
55
|
+
lastError = error;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (!latestVersion) {
|
|
59
|
+
throw lastError || new Error('无法获取最新版本');
|
|
60
|
+
}
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error(chalk.red(`❌ 检查更新失败: ${error.message}`));
|
|
63
|
+
console.log(chalk.gray('💡 可手动执行: npm view coding-tool-x version'));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!semver.valid(currentVersion) || !semver.valid(latestVersion)) {
|
|
68
|
+
console.log(chalk.yellow(`⚠️ 版本格式异常,当前: ${currentVersion}, 最新: ${latestVersion}`));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!semver.gt(latestVersion, currentVersion)) {
|
|
73
|
+
console.log(chalk.green(`✅ 已是最新版本: ${currentVersion}\n`));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
console.log(chalk.yellow(`发现新版本: ${currentVersion} -> ${latestVersion}`));
|
|
78
|
+
if (checkOnly) {
|
|
79
|
+
console.log(chalk.gray(`\n可执行更新命令: npm install -g ${packageName}@${latestVersion}\n`));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
console.log(chalk.cyan('\n⬇️ 正在更新...\n'));
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
await runNpmInstall(packageName, latestVersion);
|
|
87
|
+
console.log(chalk.green(`\n✅ 更新完成: ${latestVersion}`));
|
|
88
|
+
console.log(chalk.gray('请重新打开终端或重新执行 ctx --version 验证版本。\n'));
|
|
89
|
+
} catch (error) {
|
|
90
|
+
console.error(chalk.red(`\n❌ 更新失败: ${error.message}`));
|
|
91
|
+
console.log(chalk.gray(`💡 可手动执行: npm install -g ${packageName}@${latestVersion}\n`));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
module.exports = {
|
|
96
|
+
handleUpdate
|
|
97
|
+
};
|
|
@@ -118,7 +118,7 @@ async function createWorkspace() {
|
|
|
118
118
|
let continueAdding = true;
|
|
119
119
|
|
|
120
120
|
while (continueAdding) {
|
|
121
|
-
const availableProjects = getProjectsWithStats(config);
|
|
121
|
+
const availableProjects = await getProjectsWithStats(config);
|
|
122
122
|
|
|
123
123
|
if (availableProjects.length === 0) {
|
|
124
124
|
console.log(chalk.yellow('\n没有可用的项目\n'));
|
package/src/config/default.js
CHANGED
|
@@ -7,15 +7,46 @@ const DEFAULT_CONFIG = {
|
|
|
7
7
|
defaultProject: null,
|
|
8
8
|
maxDisplaySessions: 100,
|
|
9
9
|
pageSize: 15,
|
|
10
|
-
currentCliType: 'claude', // 当前CLI工具类型: claude, codex, gemini
|
|
10
|
+
currentCliType: 'claude', // 当前CLI工具类型: claude, codex, gemini, opencode
|
|
11
11
|
ports: {
|
|
12
12
|
webUI: 19999, // Web UI 页面端口 (同时用于 WebSocket)
|
|
13
13
|
proxy: 20088, // Claude 代理服务端口
|
|
14
14
|
codexProxy: 20089, // Codex 代理服务端口
|
|
15
|
-
geminiProxy: 20090
|
|
15
|
+
geminiProxy: 20090, // Gemini 代理服务端口
|
|
16
|
+
opencodeProxy: 20091 // OpenCode 代理服务端口
|
|
16
17
|
},
|
|
17
18
|
maxLogs: 100,
|
|
18
19
|
statsInterval: 30,
|
|
20
|
+
defaultModels: {
|
|
21
|
+
claude: [
|
|
22
|
+
'claude-opus-4-6',
|
|
23
|
+
'claude-sonnet-4-6',
|
|
24
|
+
'claude-opus-4-5-20251101',
|
|
25
|
+
'claude-sonnet-4-5-20250929',
|
|
26
|
+
'claude-haiku-4-5-20251001'
|
|
27
|
+
],
|
|
28
|
+
codex: [
|
|
29
|
+
'gpt-5.2-codex',
|
|
30
|
+
'gpt-5.1-codex-max',
|
|
31
|
+
'gpt-5.1-codex',
|
|
32
|
+
'gpt-5.1-codex-mini',
|
|
33
|
+
'gpt-5-codex',
|
|
34
|
+
'gpt-5.2',
|
|
35
|
+
'gpt-5.1',
|
|
36
|
+
'gpt-5'
|
|
37
|
+
],
|
|
38
|
+
gemini: [
|
|
39
|
+
'gemini-3-pro-preview',
|
|
40
|
+
'gemini-3-flash-preview',
|
|
41
|
+
'gemini-2.5-pro',
|
|
42
|
+
'gemini-2.5-flash',
|
|
43
|
+
'gemini-2.5-flash-lite'
|
|
44
|
+
]
|
|
45
|
+
},
|
|
46
|
+
modelDiscovery: {
|
|
47
|
+
// 是否优先使用 /v1/models 获取可用模型;默认关闭,直接走默认模型探测
|
|
48
|
+
useV1ModelsEndpoint: false
|
|
49
|
+
},
|
|
19
50
|
pricing: {
|
|
20
51
|
claude: {
|
|
21
52
|
mode: 'auto',
|
|
@@ -45,6 +76,12 @@ const DEFAULT_CONFIG = {
|
|
|
45
76
|
'gemini-2.5-pro': { mode: 'auto' },
|
|
46
77
|
'gemini-2.5-flash': { mode: 'auto' }
|
|
47
78
|
}
|
|
79
|
+
},
|
|
80
|
+
opencode: {
|
|
81
|
+
mode: 'auto',
|
|
82
|
+
input: 2.5,
|
|
83
|
+
output: 10,
|
|
84
|
+
models: {}
|
|
48
85
|
}
|
|
49
86
|
}
|
|
50
87
|
};
|