@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.
Files changed (137) hide show
  1. package/CHANGELOG.md +15 -18
  2. package/README.md +8 -8
  3. package/dist/web/assets/ConfigTemplates-Bidwfdf2.css +1 -0
  4. package/dist/web/assets/ConfigTemplates-ZrK_s7ma.js +1 -0
  5. package/dist/web/assets/Home-B8YfhZ3c.js +1 -0
  6. package/dist/web/assets/Home-Di2qsylF.css +1 -0
  7. package/dist/web/assets/PluginManager-BD7QUZbU.js +1 -0
  8. package/dist/web/assets/PluginManager-ROyoZ-6m.css +1 -0
  9. package/dist/web/assets/ProjectList-C1fQb9OW.css +1 -0
  10. package/dist/web/assets/ProjectList-DRb1DuHV.js +1 -0
  11. package/dist/web/assets/SessionList-BGJWyneI.css +1 -0
  12. package/dist/web/assets/SessionList-lZ0LKzfT.js +1 -0
  13. package/dist/web/assets/SkillManager-C1xG5B4Q.js +1 -0
  14. package/dist/web/assets/SkillManager-D7pd-d_P.css +1 -0
  15. package/dist/web/assets/Terminal-DGNJeVtc.css +1 -0
  16. package/dist/web/assets/Terminal-DksBo_lM.js +1 -0
  17. package/dist/web/assets/WorkspaceManager-Burx7XOo.js +1 -0
  18. package/dist/web/assets/WorkspaceManager-CrwgQgmP.css +1 -0
  19. package/dist/web/assets/icons-kcfLIMBB.js +1 -0
  20. package/dist/web/assets/index-Ufv5rCa5.css +1 -0
  21. package/dist/web/assets/index-lAkrRC3h.js +2 -0
  22. package/dist/web/assets/markdown-BfC0goYb.css +10 -0
  23. package/dist/web/assets/markdown-C9MYpaSi.js +1 -0
  24. package/dist/web/assets/naive-ui-CSrLusZZ.js +1 -0
  25. package/dist/web/assets/{vendors-D2HHw_aW.js → vendors-CO3Upi1d.js} +2 -2
  26. package/dist/web/assets/vue-vendor-DqyWIXEb.js +45 -0
  27. package/dist/web/assets/xterm-6GBZ9nXN.css +32 -0
  28. package/dist/web/assets/xterm-BJzAjXCH.js +13 -0
  29. package/dist/web/index.html +8 -6
  30. package/package.json +4 -2
  31. package/src/commands/channels.js +48 -1
  32. package/src/commands/cli-type.js +4 -2
  33. package/src/commands/daemon.js +81 -12
  34. package/src/commands/doctor.js +10 -9
  35. package/src/commands/list.js +1 -1
  36. package/src/commands/logs.js +6 -4
  37. package/src/commands/port-config.js +24 -4
  38. package/src/commands/proxy-control.js +12 -6
  39. package/src/commands/search.js +1 -1
  40. package/src/commands/security.js +3 -2
  41. package/src/commands/stats.js +226 -52
  42. package/src/commands/switch.js +1 -1
  43. package/src/commands/toggle-proxy.js +31 -6
  44. package/src/commands/update.js +97 -0
  45. package/src/commands/workspace.js +1 -1
  46. package/src/config/default.js +39 -2
  47. package/src/config/loader.js +74 -8
  48. package/src/config/paths.js +105 -33
  49. package/src/index.js +64 -3
  50. package/src/plugins/constants.js +3 -2
  51. package/src/plugins/plugin-api.js +1 -1
  52. package/src/reset-config.js +4 -2
  53. package/src/server/api/agents.js +57 -14
  54. package/src/server/api/channels.js +112 -33
  55. package/src/server/api/codex-channels.js +111 -18
  56. package/src/server/api/codex-proxy.js +14 -8
  57. package/src/server/api/commands.js +71 -18
  58. package/src/server/api/config-export.js +0 -6
  59. package/src/server/api/config-registry.js +11 -3
  60. package/src/server/api/config.js +376 -5
  61. package/src/server/api/convert.js +133 -0
  62. package/src/server/api/dashboard.js +22 -6
  63. package/src/server/api/gemini-channels.js +107 -18
  64. package/src/server/api/gemini-proxy.js +14 -8
  65. package/src/server/api/gemini-sessions.js +1 -1
  66. package/src/server/api/health-check.js +4 -3
  67. package/src/server/api/mcp.js +3 -3
  68. package/src/server/api/opencode-channels.js +419 -0
  69. package/src/server/api/opencode-projects.js +99 -0
  70. package/src/server/api/opencode-proxy.js +198 -0
  71. package/src/server/api/opencode-sessions.js +403 -0
  72. package/src/server/api/opencode-statistics.js +57 -0
  73. package/src/server/api/plugins.js +66 -19
  74. package/src/server/api/prompts.js +2 -2
  75. package/src/server/api/proxy.js +7 -4
  76. package/src/server/api/sessions.js +3 -0
  77. package/src/server/api/skills.js +69 -18
  78. package/src/server/api/workspaces.js +78 -6
  79. package/src/server/codex-proxy-server.js +30 -18
  80. package/src/server/dev-server.js +1 -1
  81. package/src/server/gemini-proxy-server.js +15 -3
  82. package/src/server/index.js +165 -58
  83. package/src/server/opencode-proxy-server.js +4375 -0
  84. package/src/server/proxy-server.js +27 -18
  85. package/src/server/services/agents-service.js +61 -24
  86. package/src/server/services/channel-scheduler.js +9 -5
  87. package/src/server/services/channels.js +64 -37
  88. package/src/server/services/codex-channels.js +56 -43
  89. package/src/server/services/codex-settings-manager.js +271 -49
  90. package/src/server/services/codex-statistics-service.js +2 -2
  91. package/src/server/services/commands-service.js +84 -25
  92. package/src/server/services/config-export-service.js +7 -45
  93. package/src/server/services/config-registry-service.js +63 -17
  94. package/src/server/services/config-sync-manager.js +160 -7
  95. package/src/server/services/config-templates-service.js +204 -51
  96. package/src/server/services/env-checker.js +26 -12
  97. package/src/server/services/env-manager.js +126 -18
  98. package/src/server/services/favorites.js +5 -3
  99. package/src/server/services/gemini-channels.js +33 -44
  100. package/src/server/services/gemini-statistics-service.js +2 -2
  101. package/src/server/services/mcp-service.js +350 -9
  102. package/src/server/services/model-detector.js +707 -221
  103. package/src/server/services/network-access.js +80 -0
  104. package/src/server/services/opencode-channels.js +206 -0
  105. package/src/server/services/opencode-gateway-converter.js +639 -0
  106. package/src/server/services/opencode-sessions.js +663 -0
  107. package/src/server/services/opencode-settings-manager.js +342 -0
  108. package/src/server/services/opencode-statistics-service.js +255 -0
  109. package/src/server/services/plugins-service.js +479 -22
  110. package/src/server/services/prompts-service.js +53 -11
  111. package/src/server/services/proxy-runtime.js +1 -1
  112. package/src/server/services/repo-scanner-base.js +1 -1
  113. package/src/server/services/security-config.js +1 -1
  114. package/src/server/services/session-cache.js +1 -1
  115. package/src/server/services/skill-service.js +300 -46
  116. package/src/server/services/speed-test.js +464 -186
  117. package/src/server/services/statistics-service.js +2 -2
  118. package/src/server/services/terminal-commands.js +10 -3
  119. package/src/server/services/terminal-config.js +1 -1
  120. package/src/server/services/ui-config.js +1 -1
  121. package/src/server/services/workspace-service.js +57 -100
  122. package/src/server/websocket-server.js +132 -3
  123. package/src/ui/menu.js +49 -40
  124. package/src/utils/port-helper.js +22 -8
  125. package/src/utils/session.js +5 -4
  126. package/dist/web/assets/icons-CO_2OFES.js +0 -1
  127. package/dist/web/assets/index-DI8QOi-E.js +0 -14
  128. package/dist/web/assets/index-uLHGdeZh.css +0 -41
  129. package/dist/web/assets/naive-ui-B1re3c-e.js +0 -1
  130. package/dist/web/assets/vue-vendor-6JaYHOiI.js +0 -44
  131. package/src/server/api/oauth.js +0 -294
  132. package/src/server/api/permissions.js +0 -385
  133. package/src/server/config/oauth-providers.js +0 -68
  134. package/src/server/services/oauth-callback-server.js +0 -284
  135. package/src/server/services/oauth-service.js +0 -378
  136. package/src/server/services/oauth-token-storage.js +0 -135
  137. package/src/server/services/permission-templates-service.js +0 -308
@@ -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 || 10099;
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/ping');
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
- let endpoint = '/api/statistics';
88
- if (type) {
89
- // 特定渠道统计
90
- if (!['claude', 'codex', 'gemini'].includes(type)) {
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
- endpoint += `?range=${timeRange}`;
99
-
100
- const response = await httpRequest('GET', endpoint);
101
- const stats = response.data;
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(stats, type, timeRange);
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, type, timeRange) {
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(` 总请求数: `) + chalk.cyan(summary.totalRequests || 0));
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.totalTokens !== undefined) {
301
+ if (summary.tokens !== undefined) {
141
302
  console.log(chalk.bold('\n🎯 Token 使用:'));
142
- console.log(chalk.gray(` 输入 Tokens: `) + chalk.cyan(formatNumber(summary.inputTokens || 0)));
143
- console.log(chalk.gray(` 输出 Tokens: `) + chalk.cyan(formatNumber(summary.outputTokens || 0)));
144
- console.log(chalk.gray(` 缓存创建: `) + chalk.cyan(formatNumber(summary.cacheCreation || 0)));
145
- console.log(chalk.gray(` 缓存读取: `) + chalk.cyan(formatNumber(summary.cacheRead || 0)));
146
- console.log(chalk.gray(` 总计: `) + chalk.bold.cyan(formatNumber(summary.totalTokens || 0)));
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.totalCost !== undefined) {
313
+ if (summary.cost !== undefined) {
151
314
  console.log(chalk.bold('\n💰 成本统计:'));
152
- console.log(chalk.gray(` 总成本: `) + chalk.yellow(`$${(summary.totalCost || 0).toFixed(4)}`));
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
- if (stats.recentActivity && stats.recentActivity.length > 0) {
170
- console.log(chalk.bold('\n🕐 最近活动:'));
171
- stats.recentActivity.slice(0, 5).forEach(activity => {
172
- const time = new Date(activity.timestamp).toLocaleString('zh-CN');
173
- console.log(chalk.gray(` ${time} | ${activity.channel} | ${formatNumber(activity.tokens)} tokens | $${activity.cost.toFixed(4)}`));
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
- return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
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
- const endpoint = type ? `/api/statistics/${type}/export` : '/api/statistics/export';
204
- const response = await httpRequest('GET', `${endpoint}?format=${format}`);
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()}.${format}`;
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(response.data, null, 2));
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`));
@@ -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: 10088 };
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: 10089
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: 10090
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 toolName = cliType === 'claude' ? 'Claude Code' : (cliType === 'codex' ? 'Codex' : 'Gemini');
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 toolName = cliType === 'claude' ? 'Claude Code' : (cliType === 'codex' ? 'Codex' : 'Gemini');
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'));
@@ -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 // Gemini 代理服务端口
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
  };