@adversity/coding-tool-x 2.2.0

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 (125) hide show
  1. package/CHANGELOG.md +333 -0
  2. package/LICENSE +21 -0
  3. package/README.md +404 -0
  4. package/bin/ctx.js +8 -0
  5. package/dist/web/assets/index-D1AYlFLZ.js +3220 -0
  6. package/dist/web/assets/index-aL3cKxSK.css +41 -0
  7. package/dist/web/favicon.ico +0 -0
  8. package/dist/web/index.html +14 -0
  9. package/dist/web/logo.png +0 -0
  10. package/docs/CHANGELOG.md +582 -0
  11. package/docs/DIRECTORY_MIGRATION.md +112 -0
  12. package/docs/PROJECT_STRUCTURE.md +396 -0
  13. package/docs/bannel.png +0 -0
  14. package/docs/home.png +0 -0
  15. package/docs/logo.png +0 -0
  16. package/docs/multi-channel-load-balancing.md +249 -0
  17. package/package.json +73 -0
  18. package/src/commands/channels.js +504 -0
  19. package/src/commands/cli-type.js +99 -0
  20. package/src/commands/daemon.js +286 -0
  21. package/src/commands/doctor.js +332 -0
  22. package/src/commands/list.js +222 -0
  23. package/src/commands/logs.js +259 -0
  24. package/src/commands/port-config.js +115 -0
  25. package/src/commands/proxy-control.js +258 -0
  26. package/src/commands/proxy.js +152 -0
  27. package/src/commands/resume.js +137 -0
  28. package/src/commands/search.js +190 -0
  29. package/src/commands/stats.js +224 -0
  30. package/src/commands/switch.js +48 -0
  31. package/src/commands/toggle-proxy.js +222 -0
  32. package/src/commands/ui.js +92 -0
  33. package/src/commands/workspace.js +454 -0
  34. package/src/config/default.js +40 -0
  35. package/src/config/loader.js +75 -0
  36. package/src/config/paths.js +121 -0
  37. package/src/index.js +373 -0
  38. package/src/reset-config.js +92 -0
  39. package/src/server/api/agents.js +248 -0
  40. package/src/server/api/aliases.js +36 -0
  41. package/src/server/api/channels.js +258 -0
  42. package/src/server/api/claude-hooks.js +480 -0
  43. package/src/server/api/codex-channels.js +312 -0
  44. package/src/server/api/codex-projects.js +91 -0
  45. package/src/server/api/codex-proxy.js +182 -0
  46. package/src/server/api/codex-sessions.js +491 -0
  47. package/src/server/api/codex-statistics.js +57 -0
  48. package/src/server/api/commands.js +245 -0
  49. package/src/server/api/config-templates.js +182 -0
  50. package/src/server/api/config.js +147 -0
  51. package/src/server/api/convert.js +127 -0
  52. package/src/server/api/dashboard.js +125 -0
  53. package/src/server/api/env.js +144 -0
  54. package/src/server/api/favorites.js +77 -0
  55. package/src/server/api/gemini-channels.js +261 -0
  56. package/src/server/api/gemini-projects.js +91 -0
  57. package/src/server/api/gemini-proxy.js +160 -0
  58. package/src/server/api/gemini-sessions.js +397 -0
  59. package/src/server/api/gemini-statistics.js +57 -0
  60. package/src/server/api/health-check.js +118 -0
  61. package/src/server/api/mcp.js +336 -0
  62. package/src/server/api/pm2-autostart.js +269 -0
  63. package/src/server/api/projects.js +124 -0
  64. package/src/server/api/prompts.js +279 -0
  65. package/src/server/api/proxy.js +235 -0
  66. package/src/server/api/rules.js +271 -0
  67. package/src/server/api/sessions.js +595 -0
  68. package/src/server/api/settings.js +61 -0
  69. package/src/server/api/skills.js +305 -0
  70. package/src/server/api/statistics.js +91 -0
  71. package/src/server/api/terminal.js +202 -0
  72. package/src/server/api/ui-config.js +64 -0
  73. package/src/server/api/workspaces.js +407 -0
  74. package/src/server/codex-proxy-server.js +538 -0
  75. package/src/server/dev-server.js +26 -0
  76. package/src/server/gemini-proxy-server.js +518 -0
  77. package/src/server/index.js +305 -0
  78. package/src/server/proxy-server.js +469 -0
  79. package/src/server/services/agents-service.js +354 -0
  80. package/src/server/services/alias.js +71 -0
  81. package/src/server/services/channel-health.js +234 -0
  82. package/src/server/services/channel-scheduler.js +234 -0
  83. package/src/server/services/channels.js +347 -0
  84. package/src/server/services/codex-channels.js +625 -0
  85. package/src/server/services/codex-config.js +90 -0
  86. package/src/server/services/codex-parser.js +322 -0
  87. package/src/server/services/codex-sessions.js +665 -0
  88. package/src/server/services/codex-settings-manager.js +397 -0
  89. package/src/server/services/codex-speed-test-template.json +24 -0
  90. package/src/server/services/codex-statistics-service.js +255 -0
  91. package/src/server/services/commands-service.js +360 -0
  92. package/src/server/services/config-templates-service.js +732 -0
  93. package/src/server/services/env-checker.js +307 -0
  94. package/src/server/services/env-manager.js +300 -0
  95. package/src/server/services/favorites.js +163 -0
  96. package/src/server/services/gemini-channels.js +333 -0
  97. package/src/server/services/gemini-config.js +73 -0
  98. package/src/server/services/gemini-sessions.js +689 -0
  99. package/src/server/services/gemini-settings-manager.js +263 -0
  100. package/src/server/services/gemini-statistics-service.js +253 -0
  101. package/src/server/services/health-check.js +399 -0
  102. package/src/server/services/mcp-service.js +1188 -0
  103. package/src/server/services/prompts-service.js +492 -0
  104. package/src/server/services/proxy-runtime.js +79 -0
  105. package/src/server/services/pty-manager.js +435 -0
  106. package/src/server/services/rules-service.js +401 -0
  107. package/src/server/services/session-cache.js +127 -0
  108. package/src/server/services/session-converter.js +577 -0
  109. package/src/server/services/sessions.js +757 -0
  110. package/src/server/services/settings-manager.js +163 -0
  111. package/src/server/services/skill-service.js +965 -0
  112. package/src/server/services/speed-test.js +545 -0
  113. package/src/server/services/statistics-service.js +386 -0
  114. package/src/server/services/terminal-commands.js +155 -0
  115. package/src/server/services/terminal-config.js +140 -0
  116. package/src/server/services/terminal-detector.js +306 -0
  117. package/src/server/services/ui-config.js +130 -0
  118. package/src/server/services/workspace-service.js +662 -0
  119. package/src/server/utils/pricing.js +41 -0
  120. package/src/server/websocket-server.js +557 -0
  121. package/src/ui/menu.js +129 -0
  122. package/src/ui/prompts.js +100 -0
  123. package/src/utils/format.js +43 -0
  124. package/src/utils/port-helper.js +94 -0
  125. package/src/utils/session.js +239 -0
@@ -0,0 +1,190 @@
1
+ // 搜索会话命令
2
+ const chalk = require('chalk');
3
+ const ora = require('ora');
4
+ const inquirer = require('inquirer');
5
+ const { promptSelectSession, promptSearchKeyword, promptForkConfirm } = require('../ui/prompts');
6
+ const { resumeSession } = require('./resume');
7
+ const { getProjects, searchSessions: searchSessionsInProject, parseRealProjectPath } = require('../server/services/sessions');
8
+ const { loadAliases } = require('../server/services/alias');
9
+
10
+ /**
11
+ * 跨所有项目搜索会话内容
12
+ */
13
+ async function searchSessionsAcrossProjects(config, keyword) {
14
+ const spinner = ora(`🔍 正在搜索 "${keyword}"...`).start();
15
+
16
+ const projects = getProjects(config);
17
+ const aliases = loadAliases();
18
+ const allResults = [];
19
+
20
+ // 跨所有项目搜索
21
+ for (const projectName of projects) {
22
+ try {
23
+ const { projectName: displayName } = parseRealProjectPath(projectName);
24
+ spinner.text = `🔍 正在搜索项目: ${displayName}...`;
25
+ const results = searchSessionsInProject(config, projectName, keyword, 15);
26
+
27
+ if (results.length > 0) {
28
+ results.forEach(result => {
29
+ allResults.push({
30
+ ...result,
31
+ projectName: projectName,
32
+ projectDisplayName: displayName,
33
+ alias: aliases[result.sessionId] || null
34
+ });
35
+ });
36
+ }
37
+ } catch (e) {
38
+ // 忽略单个项目的错误
39
+ }
40
+ }
41
+
42
+ spinner.stop();
43
+ spinner.clear();
44
+
45
+ if (allResults.length === 0) {
46
+ console.clear();
47
+ console.log(chalk.red(`\n❌ 未找到包含 "${keyword}" 的对话\n`));
48
+ return [];
49
+ }
50
+
51
+ // 按匹配数量排序
52
+ allResults.sort((a, b) => b.matchCount - a.matchCount);
53
+
54
+ // 统计总匹配数
55
+ const totalMatches = allResults.reduce((sum, r) => sum + r.matchCount, 0);
56
+
57
+ console.clear();
58
+ console.log(chalk.green(`\n✨ 找到 ${allResults.length} 个对话,共 ${totalMatches} 处匹配\n`));
59
+
60
+ const choices = [];
61
+
62
+ allResults.forEach((result, index) => {
63
+ // 构建显示名称
64
+ let displayName = '';
65
+
66
+ // 序号
67
+ displayName += chalk.bold.white(`${index + 1}. `);
68
+
69
+ // 项目名(洋红色高亮)
70
+ displayName += chalk.magenta.bold(`[${result.projectDisplayName}] `);
71
+
72
+ // 会话别名或 ID
73
+ if (result.alias) {
74
+ displayName += chalk.yellow.bold(`[${result.alias}] `);
75
+ } else {
76
+ displayName += chalk.gray(`[${result.sessionId.substring(0, 8)}] `);
77
+ }
78
+
79
+ // 匹配数量
80
+ displayName += chalk.cyan(`(${result.matchCount} 处匹配)`);
81
+
82
+ choices.push({
83
+ name: displayName,
84
+ value: { sessionId: result.sessionId, projectName: result.projectName },
85
+ short: result.alias || result.sessionId.substring(0, 8)
86
+ });
87
+
88
+ // 显示前 3 个匹配的上下文
89
+ const matchesToShow = result.matches.slice(0, 3);
90
+ matchesToShow.forEach((match, idx) => {
91
+ const roleColor = match.role === 'user' ? chalk.blue : chalk.green;
92
+ const roleLabel = match.role === 'user' ? '用户' : '助手';
93
+
94
+ choices.push({
95
+ name: ` ${roleColor(`[${roleLabel}]`)} ${chalk.gray(match.context)}`,
96
+ value: null,
97
+ disabled: true
98
+ });
99
+ });
100
+
101
+ // 如果还有更多匹配,显示提示
102
+ if (result.matches.length > 3) {
103
+ choices.push({
104
+ name: chalk.gray(` ... 还有 ${result.matches.length - 3} 处匹配`),
105
+ value: null,
106
+ disabled: true
107
+ });
108
+ }
109
+
110
+ // 添加分隔线(不是最后一个)
111
+ if (index < allResults.length - 1) {
112
+ choices.push(new inquirer.Separator(chalk.gray('─'.repeat(10))));
113
+ }
114
+ });
115
+
116
+ return choices;
117
+ }
118
+
119
+ /**
120
+ * 处理搜索会话
121
+ */
122
+ async function handleSearch(config, switchProjectCallback) {
123
+ while (true) {
124
+ const keyword = await promptSearchKeyword();
125
+ const choices = await searchSessionsAcrossProjects(config, keyword);
126
+
127
+ if (choices.length === 0) {
128
+ const { action } = await inquirer.prompt([
129
+ {
130
+ type: 'list',
131
+ name: 'action',
132
+ message: '未找到匹配的对话',
133
+ choices: [
134
+ { name: chalk.blue('↩️ 返回主菜单'), value: 'back' },
135
+ { name: chalk.cyan('🔎 重新搜索'), value: 'retry' },
136
+ ],
137
+ },
138
+ ]);
139
+
140
+ if (action === 'back') return;
141
+ if (action === 'retry') continue;
142
+ }
143
+
144
+ // 添加操作选项
145
+ choices.push(new inquirer.Separator(chalk.gray('═'.repeat(80))));
146
+ choices.push({ name: chalk.blue('↩️ 返回主菜单'), value: 'back' });
147
+ choices.push({ name: chalk.cyan('🔎 重新搜索'), value: 'retry' });
148
+
149
+ // 使用自定义 pageSize 以便显示更多结果
150
+ const { selected } = await inquirer.prompt([
151
+ {
152
+ type: 'list',
153
+ name: 'selected',
154
+ message: '选择对话:',
155
+ pageSize: 20,
156
+ choices: choices,
157
+ },
158
+ ]);
159
+
160
+ if (selected === 'back') {
161
+ return;
162
+ }
163
+
164
+ if (selected === 'retry') {
165
+ continue;
166
+ }
167
+
168
+ // selected 是 { sessionId, projectName }
169
+ const sessionId = selected.sessionId;
170
+ const projectName = selected.projectName;
171
+
172
+ // 切换到该项目
173
+ config.currentProject = projectName;
174
+
175
+ // 询问是否 fork
176
+ const action = await promptForkConfirm();
177
+
178
+ if (action === 'back') {
179
+ continue;
180
+ }
181
+
182
+ const fork = action === 'fork';
183
+ await resumeSession(config, sessionId, fork);
184
+ }
185
+ }
186
+
187
+ module.exports = {
188
+ searchSessionsAcrossProjects,
189
+ handleSearch,
190
+ };
@@ -0,0 +1,224 @@
1
+ const chalk = require('chalk');
2
+ const http = require('http');
3
+ const { loadConfig } = require('../config/loader');
4
+
5
+ /**
6
+ * HTTP 请求辅助函数
7
+ */
8
+ function httpRequest(method, path, data = null) {
9
+ const config = loadConfig();
10
+ const port = config.ports?.webUI || 10099;
11
+
12
+ return new Promise((resolve, reject) => {
13
+ const postData = data ? JSON.stringify(data) : null;
14
+ const options = {
15
+ hostname: 'localhost',
16
+ port: port,
17
+ path: path,
18
+ method: method,
19
+ headers: {
20
+ 'Content-Type': 'application/json',
21
+ ...(postData && { 'Content-Length': Buffer.byteLength(postData) })
22
+ },
23
+ timeout: 5000
24
+ };
25
+
26
+ const req = http.request(options, (res) => {
27
+ let responseData = '';
28
+
29
+ res.on('data', (chunk) => {
30
+ responseData += chunk;
31
+ });
32
+
33
+ res.on('end', () => {
34
+ try {
35
+ const json = JSON.parse(responseData);
36
+ resolve({ data: json, status: res.statusCode });
37
+ } catch (err) {
38
+ reject(new Error('Invalid JSON response'));
39
+ }
40
+ });
41
+ });
42
+
43
+ req.on('error', (err) => {
44
+ reject(err);
45
+ });
46
+
47
+ req.on('timeout', () => {
48
+ req.destroy();
49
+ reject(new Error('Request timeout'));
50
+ });
51
+
52
+ if (postData) {
53
+ req.write(postData);
54
+ }
55
+
56
+ req.end();
57
+ });
58
+ }
59
+
60
+ /**
61
+ * 检查 UI 服务是否运行
62
+ */
63
+ async function checkUIService() {
64
+ try {
65
+ await httpRequest('GET', '/api/ping');
66
+ return true;
67
+ } catch (err) {
68
+ return false;
69
+ }
70
+ }
71
+
72
+ /**
73
+ * 查看统计信息
74
+ */
75
+ async function handleStats(type = null, options = {}) {
76
+ // 检查 UI 服务
77
+ const uiRunning = await checkUIService();
78
+ if (!uiRunning) {
79
+ console.error(chalk.red('\n❌ UI 服务未运行\n'));
80
+ console.log(chalk.yellow('💡 请先启动 UI 服务: ') + chalk.cyan('ctx start\n'));
81
+ process.exit(1);
82
+ }
83
+
84
+ const timeRange = options.today ? 'today' : options.week ? 'week' : options.month ? 'month' : 'all';
85
+
86
+ 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}`;
96
+ }
97
+
98
+ endpoint += `?range=${timeRange}`;
99
+
100
+ const response = await httpRequest('GET', endpoint);
101
+ const stats = response.data;
102
+
103
+ displayStats(stats, type, timeRange);
104
+ } catch (error) {
105
+ console.error(chalk.red(`\n❌ 获取统计失败: ${error.message}\n`));
106
+ process.exit(1);
107
+ }
108
+ }
109
+
110
+ /**
111
+ * 显示统计信息
112
+ */
113
+ function displayStats(stats, type, timeRange) {
114
+ const title = type ? `${type.toUpperCase()} 统计信息` : '总体统计信息';
115
+ const rangeText = {
116
+ today: '今日',
117
+ week: '本周',
118
+ month: '本月',
119
+ all: '全部'
120
+ }[timeRange];
121
+
122
+ console.log(chalk.bold.cyan(`\n╔══════════════════════════════════════╗`));
123
+ console.log(chalk.bold.cyan(`║ ${title} (${rangeText}) ║`));
124
+ console.log(chalk.bold.cyan(`╚══════════════════════════════════════╝\n`));
125
+
126
+ if (!stats || !stats.summary) {
127
+ console.log(chalk.gray(' 暂无统计数据\n'));
128
+ return;
129
+ }
130
+
131
+ const summary = stats.summary;
132
+
133
+ // 请求统计
134
+ 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));
138
+
139
+ // Token 使用
140
+ if (summary.totalTokens !== undefined) {
141
+ 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)));
147
+ }
148
+
149
+ // 成本统计
150
+ if (summary.totalCost !== undefined) {
151
+ 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
+ });
166
+ }
167
+
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)}`));
174
+ });
175
+ }
176
+
177
+ console.log(chalk.gray('\n💡 提示:'));
178
+ console.log(chalk.gray(' • 使用 ') + chalk.cyan('ctx stats --today') + chalk.gray(' 查看今日统计'));
179
+ console.log(chalk.gray(' • 使用 ') + chalk.cyan('ctx stats claude') + chalk.gray(' 查看特定渠道'));
180
+ console.log(chalk.gray(' • 使用 ') + chalk.cyan('ctx stats export') + chalk.gray(' 导出统计数据\n'));
181
+ }
182
+
183
+ /**
184
+ * 格式化数字
185
+ */
186
+ function formatNumber(num) {
187
+ return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
188
+ }
189
+
190
+ /**
191
+ * 导出统计数据
192
+ */
193
+ async function handleStatsExport(type = null, format = 'json') {
194
+ console.log(chalk.cyan('\n📤 导出统计数据...\n'));
195
+
196
+ const uiRunning = await checkUIService();
197
+ if (!uiRunning) {
198
+ console.error(chalk.red('❌ UI 服务未运行\n'));
199
+ process.exit(1);
200
+ }
201
+
202
+ try {
203
+ const endpoint = type ? `/api/statistics/${type}/export` : '/api/statistics/export';
204
+ const response = await httpRequest('GET', `${endpoint}?format=${format}`);
205
+
206
+ const fs = require('fs');
207
+ const path = require('path');
208
+ const filename = `cc-tool-stats-${type || 'all'}-${Date.now()}.${format}`;
209
+ const filepath = path.join(process.cwd(), filename);
210
+
211
+ fs.writeFileSync(filepath, JSON.stringify(response.data, null, 2));
212
+
213
+ console.log(chalk.green(`✅ 统计数据已导出\n`));
214
+ console.log(chalk.gray(`文件路径: ${filepath}\n`));
215
+ } catch (error) {
216
+ console.error(chalk.red(`\n❌ 导出失败: ${error.message}\n`));
217
+ process.exit(1);
218
+ }
219
+ }
220
+
221
+ module.exports = {
222
+ handleStats,
223
+ handleStatsExport
224
+ };
@@ -0,0 +1,48 @@
1
+ // 切换项目命令
2
+ const chalk = require('chalk');
3
+ const os = require('os');
4
+ const { getAvailableProjects } = require('../utils/session');
5
+ const { promptSelectProject } = require('../ui/prompts');
6
+ const { saveConfig } = require('../config/loader');
7
+
8
+ /**
9
+ * 切换项目
10
+ */
11
+ async function switchProject(config) {
12
+ const projects = getAvailableProjects(config);
13
+
14
+ if (projects.length === 0) {
15
+ console.log(chalk.yellow('没有找到项目'));
16
+ return false;
17
+ }
18
+
19
+ const selectedProject = await promptSelectProject(projects);
20
+
21
+ // 用户取消切换
22
+ if (!selectedProject) {
23
+ console.log(chalk.gray('\n取消切换\n'));
24
+ return false;
25
+ }
26
+
27
+ // 更新配置
28
+ config.currentProject = selectedProject;
29
+ config.defaultProject = selectedProject;
30
+
31
+ // 保存到配置文件(保留其余字段)
32
+ saveConfig({
33
+ ...config,
34
+ projectsDir: config.projectsDir.replace(os.homedir(), '~')
35
+ });
36
+
37
+ // 使用解析后的名称显示
38
+ const { parseRealProjectPath } = require('../server/services/sessions');
39
+ const { displayName, fullPath } = parseRealProjectPath(selectedProject);
40
+
41
+ console.log(chalk.green(`\n✅ 已切换到: ${displayName}\n`));
42
+ console.log(chalk.gray(` 路径: ${fullPath}\n`));
43
+ return true;
44
+ }
45
+
46
+ module.exports = {
47
+ switchProject,
48
+ };
@@ -0,0 +1,222 @@
1
+ // 动态切换开关命令
2
+ const chalk = require('chalk');
3
+ const inquirer = require('inquirer');
4
+ const { loadConfig } = require('../config/loader');
5
+ const SETTINGS_MANAGERS = {
6
+ claude: () => require('../server/services/settings-manager'),
7
+ codex: () => require('../server/services/codex-settings-manager'),
8
+ gemini: () => require('../server/services/gemini-settings-manager')
9
+ };
10
+
11
+ /**
12
+ * 获取当前类型的代理服务
13
+ */
14
+ function getProxyServices(cliType) {
15
+ if (cliType === 'claude') {
16
+ const { getProxyStatus, startProxyServer, stopProxyServer } = require('../server/proxy-server');
17
+ return { getProxyStatus, startProxyServer, stopProxyServer, defaultPort: 10088 };
18
+ } else if (cliType === 'codex') {
19
+ const { getCodexProxyStatus, startCodexProxyServer, stopCodexProxyServer } = require('../server/codex-proxy-server');
20
+ return {
21
+ getProxyStatus: getCodexProxyStatus,
22
+ startProxyServer: startCodexProxyServer,
23
+ stopProxyServer: stopCodexProxyServer,
24
+ defaultPort: 10089
25
+ };
26
+ } else if (cliType === 'gemini') {
27
+ const { getGeminiProxyStatus, startGeminiProxyServer, stopGeminiProxyServer } = require('../server/gemini-proxy-server');
28
+ return {
29
+ getProxyStatus: getGeminiProxyStatus,
30
+ startProxyServer: startGeminiProxyServer,
31
+ stopProxyServer: stopGeminiProxyServer,
32
+ defaultPort: 10090
33
+ };
34
+ }
35
+ }
36
+
37
+ function getSettingsManager(cliType) {
38
+ const loader = SETTINGS_MANAGERS[cliType] || SETTINGS_MANAGERS.claude;
39
+ const manager = loader();
40
+ return {
41
+ setProxyConfig: manager.setProxyConfig,
42
+ restoreSettings: manager.restoreSettings,
43
+ hasBackup: manager.hasBackup
44
+ };
45
+ }
46
+
47
+ /**
48
+ * 切换动态切换功能
49
+ */
50
+ async function handleToggleProxy() {
51
+ const config = loadConfig();
52
+ const cliType = config.currentCliType || 'claude';
53
+ const services = getProxyServices(cliType);
54
+
55
+ const proxyStatus = services.getProxyStatus();
56
+
57
+ if (proxyStatus.running) {
58
+ // 当前代理正在运行,提示关闭
59
+ await handleStopProxy(cliType, services);
60
+ } else {
61
+ // 当前代理未运行,提示开启
62
+ await handleStartProxy(cliType, services);
63
+ }
64
+ }
65
+
66
+ /**
67
+ * 开启动态切换
68
+ */
69
+ async function handleStartProxy(cliType, services) {
70
+ console.clear();
71
+ console.log(chalk.bold.cyan('\n╔═══════════════════════════════════════╗'));
72
+ console.log(chalk.bold.cyan('║ 开启动态切换 ║'));
73
+ console.log(chalk.bold.cyan('╚═══════════════════════════════════════╝\n'));
74
+
75
+ const toolName = cliType === 'claude' ? 'Claude Code' : (cliType === 'codex' ? 'Codex' : 'Gemini');
76
+ const defaultPort = services.defaultPort;
77
+
78
+ console.log(chalk.cyan('动态切换功能说明:'));
79
+ console.log(chalk.gray('• 开启后会在本地启动一个代理服务'));
80
+ console.log(chalk.gray(`• 可以在不重启 ${toolName} 的情况下动态管理渠道`));
81
+ console.log(chalk.gray('• 通过 Web UI 或"渠道管理"功能快速调整启用的线路'));
82
+ console.log(chalk.gray(`• 代理服务地址: http://127.0.0.1:${defaultPort}\n`));
83
+
84
+ console.log(chalk.yellow('⚠️ 重要提示:'));
85
+ console.log(chalk.yellow('• 开启期间请勿关闭 CLI 终端窗口'));
86
+ console.log(chalk.yellow('• 如果异常关闭导致代理失效,请运行: ctx reset'));
87
+ console.log(chalk.yellow('• 或使用主菜单的"恢复默认配置"功能\n'));
88
+
89
+ const { confirm } = await inquirer.prompt([
90
+ {
91
+ type: 'confirm',
92
+ name: 'confirm',
93
+ message: '是否开启动态切换?',
94
+ default: true,
95
+ },
96
+ ]);
97
+
98
+ if (!confirm) {
99
+ console.log(chalk.gray('\n已取消\n'));
100
+ return;
101
+ }
102
+
103
+ try {
104
+ console.log(chalk.cyan('\n🚀 正在启动代理服务...\n'));
105
+
106
+ // 启动代理服务器
107
+ const proxyResult = await services.startProxyServer();
108
+
109
+ if (!proxyResult.success) {
110
+ throw new Error('代理服务器启动失败');
111
+ }
112
+
113
+ console.log(chalk.green(`✅ 代理服务已启动: http://127.0.0.1:${proxyResult.port}`));
114
+
115
+ // 修改配置文件
116
+ const settingsManager = getSettingsManager(cliType);
117
+ settingsManager.setProxyConfig(proxyResult.port);
118
+ console.log(chalk.green('✅ 配置文件已更新'));
119
+
120
+ if (settingsManager.hasBackup()) {
121
+ console.log(chalk.green('✅ 原配置已备份'));
122
+ }
123
+
124
+ console.log(chalk.cyan('\n💡 动态切换已启用!'));
125
+ console.log(chalk.gray(` 现在可以通过"渠道管理"功能快速调整,无需重启 ${toolName}\n`));
126
+
127
+ await inquirer.prompt([
128
+ {
129
+ type: 'input',
130
+ name: 'continue',
131
+ message: '按回车继续...',
132
+ },
133
+ ]);
134
+ } catch (error) {
135
+ console.log(chalk.red(`\n❌ 启动失败: ${error.message}\n`));
136
+
137
+ await inquirer.prompt([
138
+ {
139
+ type: 'input',
140
+ name: 'continue',
141
+ message: '按回车继续...',
142
+ },
143
+ ]);
144
+ }
145
+ }
146
+
147
+ /**
148
+ * 关闭动态切换
149
+ */
150
+ async function handleStopProxy(cliType, services) {
151
+ console.clear();
152
+ console.log(chalk.bold.cyan('\n╔═══════════════════════════════════════╗'));
153
+ console.log(chalk.bold.cyan('║ 关闭动态切换 ║'));
154
+ console.log(chalk.bold.cyan('╚═══════════════════════════════════════╝\n'));
155
+
156
+ const toolName = cliType === 'claude' ? 'Claude Code' : (cliType === 'codex' ? 'Codex' : 'Gemini');
157
+ const proxyStatus = services.getProxyStatus();
158
+
159
+ console.log(chalk.cyan('当前状态:'));
160
+ console.log(chalk.gray(`• 代理服务: ${chalk.green('运行中')}`));
161
+ console.log(chalk.gray(`• 代理端口: ${proxyStatus.port}`));
162
+ console.log(chalk.gray(`• 代理地址: http://127.0.0.1:${proxyStatus.port}\n`));
163
+
164
+ console.log(chalk.yellow('关闭后:'));
165
+ console.log(chalk.gray('• 代理服务将被停止'));
166
+ console.log(chalk.gray('• 配置将恢复到关闭前的状态'));
167
+ console.log(chalk.gray(`• 之后管理渠道将需要重启 ${toolName}\n`));
168
+
169
+ const { confirm } = await inquirer.prompt([
170
+ {
171
+ type: 'confirm',
172
+ name: 'confirm',
173
+ message: '是否关闭动态切换?',
174
+ default: false,
175
+ },
176
+ ]);
177
+
178
+ if (!confirm) {
179
+ console.log(chalk.gray('\n已取消\n'));
180
+ return;
181
+ }
182
+
183
+ try {
184
+ console.log(chalk.cyan('\n⏹️ 正在停止代理服务...\n'));
185
+
186
+ // 停止代理服务器
187
+ await services.stopProxyServer();
188
+ console.log(chalk.green('✅ 代理服务已停止'));
189
+
190
+ // 恢复配置文件
191
+ const settingsManager = getSettingsManager(cliType);
192
+ if (settingsManager.hasBackup()) {
193
+ settingsManager.restoreSettings();
194
+ console.log(chalk.green('✅ 配置文件已恢复'));
195
+ }
196
+
197
+ console.log(chalk.cyan('\n💡 动态切换已关闭'));
198
+ console.log(chalk.gray(` 现在调整渠道需要重启 ${toolName} 才能生效\n`));
199
+
200
+ await inquirer.prompt([
201
+ {
202
+ type: 'input',
203
+ name: 'continue',
204
+ message: '按回车继续...',
205
+ },
206
+ ]);
207
+ } catch (error) {
208
+ console.log(chalk.red(`\n❌ 停止失败: ${error.message}\n`));
209
+
210
+ await inquirer.prompt([
211
+ {
212
+ type: 'input',
213
+ name: 'continue',
214
+ message: '按回车继续...',
215
+ },
216
+ ]);
217
+ }
218
+ }
219
+
220
+ module.exports = {
221
+ handleToggleProxy,
222
+ };