@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,557 @@
1
+ const WebSocket = require('ws');
2
+ const chalk = require('chalk');
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+ const { loadConfig } = require('../config/loader');
7
+ const { ptyManager } = require('./services/pty-manager');
8
+
9
+ const MAX_PERSISTED_LOGS = 500;
10
+
11
+ function getMaxLogsLimit() {
12
+ try {
13
+ const config = loadConfig();
14
+ const limit = parseInt(config.maxLogs, 10);
15
+ if (!Number.isFinite(limit)) {
16
+ return 100;
17
+ }
18
+ return Math.min(Math.max(limit, 50), MAX_PERSISTED_LOGS);
19
+ } catch (err) {
20
+ console.error('Failed to load log limit from config:', err);
21
+ return 100;
22
+ }
23
+ }
24
+
25
+ let wss = null;
26
+ let wsClients = new Set();
27
+
28
+ // 日志持久化文件路径
29
+ function getLogsFilePath() {
30
+ const ccToolDir = path.join(os.homedir(), '.claude', 'cc-tool');
31
+ if (!fs.existsSync(ccToolDir)) {
32
+ fs.mkdirSync(ccToolDir, { recursive: true });
33
+ }
34
+ return path.join(ccToolDir, 'proxy-logs.json');
35
+ }
36
+
37
+ function getTodayRange() {
38
+ const start = new Date();
39
+ start.setHours(0, 0, 0, 0);
40
+ const startMs = start.getTime();
41
+ const endMs = startMs + 24 * 60 * 60 * 1000;
42
+ return { startMs, endMs };
43
+ }
44
+
45
+ function inferSource(log) {
46
+ if (log.source) {
47
+ return log.source;
48
+ }
49
+ if (log.toolType) {
50
+ if (log.toolType.includes('codex')) return 'codex';
51
+ if (log.toolType.includes('gemini')) return 'gemini';
52
+ }
53
+ if (typeof log.model === 'string') {
54
+ const model = log.model.toLowerCase();
55
+ if (model.includes('gemini')) return 'gemini';
56
+ if (model.includes('gpt') || model.includes('o1') || model.includes('o3')) return 'codex';
57
+ if (model.includes('claude')) return 'claude';
58
+ }
59
+ if (typeof log.action === 'string') {
60
+ if (log.action.includes('codex')) return 'codex';
61
+ if (log.action.includes('gemini')) return 'gemini';
62
+ }
63
+ if (log.channelType === 'codex' || log.channelType === 'gemini') {
64
+ return log.channelType;
65
+ }
66
+ return 'claude';
67
+ }
68
+
69
+ function filterTodayLogs(logs) {
70
+ const { startMs, endMs } = getTodayRange();
71
+ return logs.filter(log => {
72
+ let ts = log.timestamp;
73
+ if (typeof ts !== 'number' || !Number.isFinite(ts)) {
74
+ if (typeof log.timestamp === 'string') {
75
+ const parsed = Date.parse(log.timestamp);
76
+ if (Number.isFinite(parsed)) {
77
+ ts = parsed;
78
+ }
79
+ }
80
+ }
81
+ if (typeof ts !== 'number' || !Number.isFinite(ts)) {
82
+ // 无法解析时间戳,默认为当前时间(视作今日日志)
83
+ ts = Date.now();
84
+ }
85
+ log.timestamp = ts;
86
+ log.source = inferSource(log);
87
+ return ts >= startMs && ts < endMs;
88
+ });
89
+ }
90
+
91
+ function enforcePerSourceLimit(logs) {
92
+ const limit = getMaxLogsLimit();
93
+ if (!limit || limit <= 0) {
94
+ return logs;
95
+ }
96
+
97
+ const counts = {};
98
+ const retained = [];
99
+
100
+ for (let i = logs.length - 1; i >= 0; i--) {
101
+ const log = logs[i];
102
+ const src = log.source || 'claude';
103
+ counts[src] = (counts[src] || 0) + 1;
104
+ if (counts[src] <= limit) {
105
+ retained.push(log);
106
+ }
107
+ }
108
+
109
+ return retained.reverse();
110
+ }
111
+
112
+ // 加载持久化的日志
113
+ function loadPersistedLogs() {
114
+ try {
115
+ const logsFile = getLogsFilePath();
116
+ if (fs.existsSync(logsFile)) {
117
+ const data = fs.readFileSync(logsFile, 'utf8');
118
+ const logs = enforcePerSourceLimit(filterTodayLogs(JSON.parse(data)));
119
+ return Array.isArray(logs) ? logs : [];
120
+ }
121
+ } catch (err) {
122
+ console.error('Failed to load persisted logs:', err);
123
+ }
124
+ return [];
125
+ }
126
+
127
+ // 保存日志到文件
128
+ function saveLogsToFile(logs) {
129
+ try {
130
+ const logsFile = getLogsFilePath();
131
+ // 只保留最新的 MAX_PERSISTED_LOGS 条,且仅保存今日日志
132
+ const todayLogs = enforcePerSourceLimit(filterTodayLogs(logs));
133
+ const logsToSave = todayLogs.slice(-MAX_PERSISTED_LOGS);
134
+ fs.writeFileSync(logsFile, JSON.stringify(logsToSave, null, 2), 'utf8');
135
+ } catch (err) {
136
+ console.error('Failed to save logs to file:', err);
137
+ }
138
+ }
139
+
140
+ // 内存中的日志缓存
141
+ let logsCache = [];
142
+
143
+ // 启动 WebSocket 服务器(附加到现有的 HTTP 服务器)
144
+ function startWebSocketServer(httpServer) {
145
+ if (wss) {
146
+ console.log('WebSocket server already running');
147
+ return;
148
+ }
149
+
150
+ // 加载持久化的日志到缓存
151
+ logsCache = loadPersistedLogs();
152
+ const counts = logsCache.reduce((acc, log) => {
153
+ const source = log.source || 'unknown';
154
+ acc[source] = (acc[source] || 0) + 1;
155
+ return acc;
156
+ }, {});
157
+ console.log(`📝 Loaded ${logsCache.length} persisted logs today ->`, counts);
158
+
159
+ try {
160
+ // 如果传入的是 HTTP server,则附加到该服务器;否则创建独立的 WebSocket 服务器
161
+ if (httpServer) {
162
+ wss = new WebSocket.Server({
163
+ server: httpServer,
164
+ path: '/ws' // 指定 WebSocket 路径
165
+ });
166
+ console.log(`✅ WebSocket server attached to HTTP server at /ws`);
167
+ } else {
168
+ // 创建独立的 WebSocket 服务器,使用配置的 webUI 端口
169
+ const config = loadConfig();
170
+ const port = config.ports?.webUI || 10099;
171
+ wss = new WebSocket.Server({
172
+ port,
173
+ path: '/ws'
174
+ });
175
+ console.log(`✅ WebSocket server started on ws://127.0.0.1:${port}/ws`);
176
+ }
177
+
178
+ wss.on('connection', (ws) => {
179
+ wsClients.add(ws);
180
+
181
+ // 标记客户端存活
182
+ ws.isAlive = true;
183
+ // 终端绑定信息
184
+ ws.terminalId = null;
185
+
186
+ // 发送历史日志给新连接的客户端
187
+ if (logsCache.length > 0) {
188
+ logsCache.forEach(log => {
189
+ if (ws.readyState === WebSocket.OPEN) {
190
+ ws.send(JSON.stringify(log));
191
+ }
192
+ });
193
+ }
194
+
195
+ // 处理客户端消息
196
+ ws.on('message', (data) => {
197
+ try {
198
+ const message = JSON.parse(data.toString());
199
+ handleTerminalMessage(ws, message);
200
+ } catch (err) {
201
+ console.error('Failed to parse WebSocket message:', err.message);
202
+ }
203
+ });
204
+
205
+ // 响应 pong 消息
206
+ ws.on('pong', () => {
207
+ ws.isAlive = true;
208
+ });
209
+
210
+ // 响应客户端的心跳 ping
211
+ ws.on('ping', () => {
212
+ ws.pong();
213
+ });
214
+
215
+ ws.on('close', () => {
216
+ // 解绑终端
217
+ if (ws.terminalId) {
218
+ ptyManager.detachWebSocket(ws.terminalId);
219
+ }
220
+ wsClients.delete(ws);
221
+ });
222
+
223
+ ws.on('error', (error) => {
224
+ console.error('WebSocket error:', error);
225
+ if (ws.terminalId) {
226
+ ptyManager.detachWebSocket(ws.terminalId);
227
+ }
228
+ wsClients.delete(ws);
229
+ });
230
+ });
231
+
232
+ // 心跳检测:每 30 秒 ping 一次所有客户端
233
+ const heartbeatInterval = setInterval(() => {
234
+ wsClients.forEach(ws => {
235
+ if (ws.isAlive === false) {
236
+ // 客户端没有响应 pong,断开连接
237
+ console.log('❌ WebSocket client timeout, terminating');
238
+ wsClients.delete(ws);
239
+ return ws.terminate();
240
+ }
241
+
242
+ // 标记为未响应,等待 pong
243
+ ws.isAlive = false;
244
+ ws.ping();
245
+ });
246
+ }, 30000);
247
+
248
+ // 保存 interval 以便停止时清除
249
+ wss.heartbeatInterval = heartbeatInterval;
250
+
251
+ wss.on('error', (error) => {
252
+ console.error('WebSocket server error:', error);
253
+ if (error.code === 'EADDRINUSE') {
254
+ console.error(chalk.red('\n❌ WebSocket 端口已被占用'));
255
+ console.error(chalk.yellow('\n💡 请检查端口配置\n'));
256
+ wss = null;
257
+ }
258
+ });
259
+ } catch (error) {
260
+ console.error('Failed to start WebSocket server:', error);
261
+ wss = null;
262
+ }
263
+ }
264
+
265
+ // 停止 WebSocket 服务器
266
+ function stopWebSocketServer() {
267
+ if (!wss) {
268
+ return;
269
+ }
270
+
271
+ // 清除心跳定时器
272
+ if (wss.heartbeatInterval) {
273
+ clearInterval(wss.heartbeatInterval);
274
+ wss.heartbeatInterval = null;
275
+ }
276
+
277
+ // 关闭所有客户端连接
278
+ wsClients.forEach(client => {
279
+ client.close();
280
+ });
281
+ wsClients.clear();
282
+
283
+ // 关闭服务器
284
+ wss.close(() => {
285
+ console.log('✅ WebSocket server stopped');
286
+ });
287
+
288
+ wss = null;
289
+ }
290
+
291
+ // 广播日志消息
292
+ function broadcastLog(logData) {
293
+ const timestamp = typeof logData.timestamp === 'number' ? logData.timestamp : Date.now();
294
+ const payload = {
295
+ ...logData,
296
+ timestamp
297
+ };
298
+
299
+ payload.source = payload.source || inferSource(payload);
300
+
301
+ // 添加到缓存
302
+ logsCache.push(payload);
303
+ logsCache = enforcePerSourceLimit(filterTodayLogs(logsCache));
304
+
305
+ if (logsCache.length > MAX_PERSISTED_LOGS) {
306
+ logsCache = logsCache.slice(-MAX_PERSISTED_LOGS);
307
+ }
308
+
309
+ // 保存到文件
310
+ saveLogsToFile(logsCache);
311
+
312
+ // 广播给所有连接的客户端
313
+ if (wss && wsClients.size > 0) {
314
+ const message = JSON.stringify(payload);
315
+
316
+ wsClients.forEach(client => {
317
+ if (client.readyState === WebSocket.OPEN) {
318
+ client.send(message);
319
+ }
320
+ });
321
+ }
322
+ }
323
+
324
+ // 清空所有日志
325
+ function clearAllLogs() {
326
+ logsCache = [];
327
+ saveLogsToFile([]);
328
+ console.log('✅ All logs cleared');
329
+ }
330
+
331
+ // 去掉敏感字段
332
+ function sanitizeChannel(channel) {
333
+ if (!channel || typeof channel !== 'object') {
334
+ return null;
335
+ }
336
+ const { apiKey, ...rest } = channel;
337
+ return rest;
338
+ }
339
+
340
+ function sanitizeChannels(channels) {
341
+ if (!Array.isArray(channels)) {
342
+ return [];
343
+ }
344
+ return channels.map(channel => sanitizeChannel(channel)).filter(Boolean);
345
+ }
346
+
347
+ // 广播代理状态更新
348
+ function broadcastProxyState(source, proxyStatus = {}, activeChannel = null, channels = []) {
349
+ const stateUpdate = {
350
+ type: 'proxy-state',
351
+ source, // 'claude', 'codex', or 'gemini'
352
+ proxy: proxyStatus,
353
+ activeChannel: sanitizeChannel(activeChannel),
354
+ channels: sanitizeChannels(channels),
355
+ timestamp: Date.now()
356
+ };
357
+
358
+ if (wss && wsClients.size > 0) {
359
+ const message = JSON.stringify(stateUpdate);
360
+
361
+ wsClients.forEach(client => {
362
+ if (client.readyState === WebSocket.OPEN) {
363
+ client.send(message);
364
+ }
365
+ });
366
+ }
367
+ }
368
+
369
+ // 广播调度状态更新(实时并发信息)
370
+ function broadcastSchedulerState(source, schedulerState) {
371
+ const stateUpdate = {
372
+ type: 'scheduler-state',
373
+ source, // 'claude', 'codex', or 'gemini'
374
+ scheduler: schedulerState,
375
+ timestamp: Date.now()
376
+ };
377
+
378
+ if (wss && wsClients.size > 0) {
379
+ const message = JSON.stringify(stateUpdate);
380
+
381
+ wsClients.forEach(client => {
382
+ if (client.readyState === WebSocket.OPEN) {
383
+ client.send(message);
384
+ }
385
+ });
386
+ }
387
+ }
388
+
389
+ // ============ 终端 WebSocket 消息处理 ============
390
+
391
+ const { getCommandForChannel } = require('./services/terminal-commands');
392
+
393
+ /**
394
+ * 处理终端相关的 WebSocket 消息
395
+ */
396
+ function handleTerminalMessage(ws, message) {
397
+ const { type } = message;
398
+
399
+ switch (type) {
400
+ case 'terminal:create':
401
+ handleTerminalCreate(ws, message);
402
+ break;
403
+
404
+ case 'terminal:attach':
405
+ handleTerminalAttach(ws, message);
406
+ break;
407
+
408
+ case 'terminal:input':
409
+ handleTerminalInput(ws, message);
410
+ break;
411
+
412
+ case 'terminal:resize':
413
+ handleTerminalResize(ws, message);
414
+ break;
415
+
416
+ case 'terminal:destroy':
417
+ handleTerminalDestroy(ws, message);
418
+ break;
419
+
420
+ default:
421
+ // 非终端消息,忽略
422
+ break;
423
+ }
424
+ }
425
+
426
+ /**
427
+ * 创建新终端
428
+ */
429
+ function handleTerminalCreate(ws, message) {
430
+ const {
431
+ channel = 'claude',
432
+ sessionId = null,
433
+ projectName = null,
434
+ cwd = null
435
+ } = message;
436
+
437
+ try {
438
+ // 确定工作目录
439
+ let workDir = cwd || os.homedir();
440
+
441
+ // 获取启动命令
442
+ const startCommand = getCommandForChannel(channel, sessionId, workDir);
443
+
444
+ // 创建终端
445
+ const terminal = ptyManager.create({
446
+ cwd: workDir,
447
+ channel,
448
+ sessionId,
449
+ projectName,
450
+ startCommand
451
+ });
452
+
453
+ // 绑定 WebSocket
454
+ ws.terminalId = terminal.id;
455
+ ptyManager.attachWebSocket(terminal.id, ws);
456
+
457
+ // 发送创建成功消息
458
+ ws.send(JSON.stringify({
459
+ type: 'terminal:created',
460
+ terminalId: terminal.id,
461
+ metadata: terminal.metadata
462
+ }));
463
+ } catch (err) {
464
+ console.error('Failed to create terminal:', err);
465
+ ws.send(JSON.stringify({
466
+ type: 'terminal:error',
467
+ error: err.message
468
+ }));
469
+ }
470
+ }
471
+
472
+ /**
473
+ * 绑定到已有终端
474
+ */
475
+ function handleTerminalAttach(ws, message) {
476
+ const { terminalId } = message;
477
+
478
+ if (!terminalId) {
479
+ ws.send(JSON.stringify({
480
+ type: 'terminal:error',
481
+ error: 'Missing terminalId'
482
+ }));
483
+ return;
484
+ }
485
+
486
+ const terminal = ptyManager.get(terminalId);
487
+ if (!terminal) {
488
+ ws.send(JSON.stringify({
489
+ type: 'terminal:error',
490
+ error: 'Terminal not found',
491
+ terminalId
492
+ }));
493
+ return;
494
+ }
495
+
496
+ // 绑定 WebSocket
497
+ ws.terminalId = terminalId;
498
+ ptyManager.attachWebSocket(terminalId, ws);
499
+
500
+ ws.send(JSON.stringify({
501
+ type: 'terminal:attached',
502
+ terminalId,
503
+ metadata: terminal.metadata
504
+ }));
505
+ }
506
+
507
+ /**
508
+ * 终端输入
509
+ */
510
+ function handleTerminalInput(ws, message) {
511
+ const { terminalId, data } = message;
512
+
513
+ if (!terminalId || !data) {
514
+ return;
515
+ }
516
+
517
+ ptyManager.write(terminalId, data);
518
+ }
519
+
520
+ /**
521
+ * 调整终端大小
522
+ */
523
+ function handleTerminalResize(ws, message) {
524
+ const { terminalId, cols, rows } = message;
525
+
526
+ if (!terminalId || !cols || !rows) {
527
+ return;
528
+ }
529
+
530
+ ptyManager.resize(terminalId, cols, rows);
531
+ }
532
+
533
+ /**
534
+ * 销毁终端
535
+ */
536
+ function handleTerminalDestroy(ws, message) {
537
+ const { terminalId } = message;
538
+
539
+ if (!terminalId) {
540
+ return;
541
+ }
542
+
543
+ ptyManager.destroy(terminalId);
544
+
545
+ if (ws.terminalId === terminalId) {
546
+ ws.terminalId = null;
547
+ }
548
+ }
549
+
550
+ module.exports = {
551
+ startWebSocketServer,
552
+ stopWebSocketServer,
553
+ broadcastLog,
554
+ clearAllLogs,
555
+ broadcastProxyState,
556
+ broadcastSchedulerState
557
+ };
package/src/ui/menu.js ADDED
@@ -0,0 +1,129 @@
1
+ // 菜单显示
2
+ const inquirer = require('inquirer');
3
+ const chalk = require('chalk');
4
+ const packageInfo = require('../../package.json');
5
+
6
+ /**
7
+ * 显示主菜单
8
+ */
9
+ async function showMainMenu(config) {
10
+ console.log(chalk.bold.cyan('\n╔═══════════════════════════════════════════════╗'));
11
+ console.log(chalk.bold.cyan(`║ Claude Code 会话管理工具 v${packageInfo.version} ║`));
12
+ console.log(chalk.bold.cyan('╚═══════════════════════════════════════════════╝\n'));
13
+
14
+ // 显示当前CLI类型
15
+ const cliTypes = {
16
+ claude: { name: 'Claude Code', color: 'cyan' },
17
+ codex: { name: 'Codex', color: 'green' },
18
+ gemini: { name: 'Gemini', color: 'magenta' }
19
+ };
20
+ const currentType = config.currentCliType || 'claude';
21
+ const typeInfo = cliTypes[currentType];
22
+ console.log(chalk[typeInfo.color](`当前类型: ${typeInfo.name}`));
23
+
24
+ const projectName = config.currentProject
25
+ ? config.currentProject.replace(/-/g, '/').substring(1)
26
+ : '未设置';
27
+ console.log(chalk.gray(`当前项目: ${projectName}`));
28
+
29
+ // 显示当前渠道和代理状态(根据类型显示对应的渠道和代理)
30
+ try {
31
+ let getCurrentChannelFunc, getProxyStatusFunc;
32
+
33
+ if (currentType === 'claude') {
34
+ const { getCurrentChannel } = require('../server/services/channels');
35
+ const { getProxyStatus } = require('../server/proxy-server');
36
+ getCurrentChannelFunc = getCurrentChannel;
37
+ getProxyStatusFunc = getProxyStatus;
38
+ } else if (currentType === 'codex') {
39
+ const { getActiveCodexChannel } = require('../server/services/codex-channels');
40
+ const { getCodexProxyStatus } = require('../server/codex-proxy-server');
41
+ getCurrentChannelFunc = getActiveCodexChannel;
42
+ getProxyStatusFunc = getCodexProxyStatus;
43
+ } else if (currentType === 'gemini') {
44
+ const { getActiveGeminiChannel } = require('../server/services/gemini-channels');
45
+ const { getGeminiProxyStatus } = require('../server/gemini-proxy-server');
46
+ getCurrentChannelFunc = getActiveGeminiChannel;
47
+ getProxyStatusFunc = getGeminiProxyStatus;
48
+ }
49
+
50
+ const currentChannel = getCurrentChannelFunc();
51
+ const proxyStatus = getProxyStatusFunc();
52
+
53
+ if (currentChannel) {
54
+ console.log(chalk.gray(`当前渠道: ${currentChannel.name}`));
55
+ }
56
+
57
+ if (proxyStatus.running) {
58
+ console.log(chalk.green(`动态切换: 已开启 (端口 ${proxyStatus.port})`));
59
+ } else {
60
+ console.log(chalk.gray('动态切换: 未开启'));
61
+ }
62
+ } catch (err) {
63
+ // 忽略错误
64
+ }
65
+
66
+ console.log(chalk.gray('─'.repeat(50)));
67
+
68
+ // 获取代理状态,用于显示动态切换的状态(根据当前类型)
69
+ let proxyStatusText = '未开启';
70
+ try {
71
+ let proxyStatus;
72
+
73
+ if (currentType === 'claude') {
74
+ // 清除缓存确保获取最新状态
75
+ delete require.cache[require.resolve('../server/proxy-server')];
76
+ const { getProxyStatus } = require('../server/proxy-server');
77
+ proxyStatus = getProxyStatus();
78
+ } else if (currentType === 'codex') {
79
+ delete require.cache[require.resolve('../server/codex-proxy-server')];
80
+ const { getCodexProxyStatus } = require('../server/codex-proxy-server');
81
+ proxyStatus = getCodexProxyStatus();
82
+ } else if (currentType === 'gemini') {
83
+ delete require.cache[require.resolve('../server/gemini-proxy-server')];
84
+ const { getGeminiProxyStatus } = require('../server/gemini-proxy-server');
85
+ proxyStatus = getGeminiProxyStatus();
86
+ }
87
+
88
+ if (proxyStatus && proxyStatus.running) {
89
+ proxyStatusText = '已开启';
90
+ }
91
+ } catch (err) {
92
+ // 忽略错误
93
+ console.error('获取代理状态失败:', err.message);
94
+ }
95
+
96
+ const { action } = await inquirer.prompt([
97
+ {
98
+ type: 'list',
99
+ name: 'action',
100
+ message: '请选择操作:',
101
+ pageSize: 16,
102
+ choices: [
103
+ { name: chalk.bold.yellow('切换 CLI 类型'), value: 'switch-cli-type' },
104
+ new inquirer.Separator(chalk.gray('─'.repeat(14))),
105
+ { name: chalk.bold.hex('#00D9FF')('启动 Web UI'), value: 'ui' },
106
+ new inquirer.Separator(chalk.gray('─'.repeat(14))),
107
+ { name: chalk.cyan('列出最新对话'), value: 'list' },
108
+ { name: chalk.green('搜索会话'), value: 'search' },
109
+ { name: chalk.magenta('切换项目'), value: 'switch' },
110
+ { name: chalk.hex('#FF6B35')('工作区管理'), value: 'workspace' },
111
+ new inquirer.Separator(chalk.gray('─'.repeat(14))),
112
+ { name: chalk.cyan('渠道管理'), value: 'switch-channel' },
113
+ { name: chalk.cyan('查看调度状态'), value: 'channel-status' },
114
+ { name: chalk.cyan(`是否开启动态切换 (${proxyStatusText})`), value: 'toggle-proxy' },
115
+ { name: chalk.cyan('添加渠道'), value: 'add-channel' },
116
+ new inquirer.Separator(chalk.gray('─'.repeat(14))),
117
+ { name: chalk.magenta('配置端口'), value: 'port-config' },
118
+ { name: chalk.yellow('恢复默认配置'), value: 'reset' },
119
+ { name: chalk.gray('退出程序'), value: 'exit' },
120
+ ],
121
+ },
122
+ ]);
123
+
124
+ return action;
125
+ }
126
+
127
+ module.exports = {
128
+ showMainMenu,
129
+ };