@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,518 @@
1
+ const express = require('express');
2
+ const httpProxy = require('http-proxy');
3
+ const http = require('http');
4
+ const chalk = require('chalk');
5
+ const { broadcastLog, broadcastSchedulerState } = require('./websocket-server');
6
+ const { allocateChannel, releaseChannel, getSchedulerState } = require('./services/channel-scheduler');
7
+ const { recordSuccess, recordFailure } = require('./services/channel-health');
8
+ const { loadConfig } = require('../config/loader');
9
+ const DEFAULT_CONFIG = require('../config/default');
10
+ const { resolvePricing } = require('./utils/pricing');
11
+ const { recordRequest: recordGeminiRequest } = require('./services/gemini-statistics-service');
12
+ const { saveProxyStartTime, clearProxyStartTime, getProxyStartTime, getProxyRuntime } = require('./services/proxy-runtime');
13
+
14
+ let proxyServer = null;
15
+ let proxyApp = null;
16
+ let currentPort = null;
17
+
18
+ // 用于存储每个请求的元数据
19
+ const requestMetadata = new Map();
20
+
21
+ // Gemini 模型定价(每百万 tokens 的价格,单位:美元)
22
+ const PRICING = {
23
+ 'gemini-2.5-pro': { input: 1.25, output: 5 },
24
+ 'gemini-2.5-flash': { input: 0.075, output: 0.3 },
25
+ 'gemini-2.0-flash-exp': { input: 0, output: 0 }, // 实验性免费
26
+ 'gemini-2.0-flash-thinking-exp-1219': { input: 0, output: 0 }, // 实验性免费
27
+ 'gemini-1.5-pro': { input: 1.25, output: 5 },
28
+ 'gemini-1.5-flash': { input: 0.075, output: 0.3 },
29
+ 'gemini-1.5-flash-8b': { input: 0.0375, output: 0.15 },
30
+ 'gemini-1.0-pro': { input: 0.5, output: 1.5 },
31
+ // 旧版本别名
32
+ 'gemini-pro': { input: 0.5, output: 1.5 },
33
+ 'gemini-pro-vision': { input: 0.5, output: 1.5 }
34
+ };
35
+
36
+ const GEMINI_BASE_PRICING = DEFAULT_CONFIG.pricing.gemini;
37
+ const ONE_MILLION = 1000000;
38
+
39
+ function resolveGeminiTarget(baseUrl = '', requestPath = '') {
40
+ let target = baseUrl || '';
41
+ if (target.endsWith('/')) {
42
+ target = target.slice(0, -1);
43
+ }
44
+ if (target.endsWith('/v1') && requestPath.startsWith('/v1')) {
45
+ target = target.slice(0, -3);
46
+ }
47
+ return target;
48
+ }
49
+
50
+ /**
51
+ * 计算请求成本
52
+ */
53
+ function calculateCost(model, tokens) {
54
+ // 尝试精确匹配
55
+ let pricing = PRICING[model];
56
+
57
+ // 如果没有精确匹配,尝试模糊匹配
58
+ if (!pricing) {
59
+ const modelLower = model.toLowerCase();
60
+ if (modelLower.includes('gemini-2.5-pro')) {
61
+ pricing = PRICING['gemini-2.5-pro'];
62
+ } else if (modelLower.includes('gemini-2.5-flash')) {
63
+ pricing = PRICING['gemini-2.5-flash'];
64
+ } else if (modelLower.includes('gemini-2.0-flash-thinking')) {
65
+ pricing = PRICING['gemini-2.0-flash-thinking-exp-1219'];
66
+ } else if (modelLower.includes('gemini-2.0-flash')) {
67
+ pricing = PRICING['gemini-2.0-flash-exp'];
68
+ } else if (modelLower.includes('gemini-1.5-pro')) {
69
+ pricing = PRICING['gemini-1.5-pro'];
70
+ } else if (modelLower.includes('gemini-1.5-flash-8b')) {
71
+ pricing = PRICING['gemini-1.5-flash-8b'];
72
+ } else if (modelLower.includes('gemini-1.5-flash')) {
73
+ pricing = PRICING['gemini-1.5-flash'];
74
+ } else if (modelLower.includes('gemini-1.0-pro')) {
75
+ pricing = PRICING['gemini-1.0-pro'];
76
+ } else if (modelLower.includes('gemini-pro')) {
77
+ pricing = PRICING['gemini-pro'];
78
+ }
79
+ }
80
+
81
+ pricing = resolvePricing('gemini', pricing, GEMINI_BASE_PRICING);
82
+ const inputRate = typeof pricing.input === 'number' ? pricing.input : GEMINI_BASE_PRICING.input;
83
+ const outputRate = typeof pricing.output === 'number' ? pricing.output : GEMINI_BASE_PRICING.output;
84
+
85
+ return (
86
+ (tokens.input || 0) * inputRate / ONE_MILLION +
87
+ (tokens.output || 0) * outputRate / ONE_MILLION
88
+ );
89
+ }
90
+
91
+ // 启动 Gemini 代理服务器
92
+ async function startGeminiProxyServer(options = {}) {
93
+ // options.preserveStartTime - 是否保留现有的启动时间(用于切换渠道时)
94
+ const preserveStartTime = options.preserveStartTime || false;
95
+
96
+ if (proxyServer) {
97
+ console.log('Gemini proxy server already running on port', currentPort);
98
+ return { success: true, port: currentPort };
99
+ }
100
+
101
+ try {
102
+ const config = loadConfig();
103
+ const port = config.ports?.geminiProxy || 10090;
104
+ currentPort = port;
105
+
106
+ proxyApp = express();
107
+ const proxy = httpProxy.createProxyServer({});
108
+
109
+ proxy.on('proxyReq', (proxyReq, req) => {
110
+ const activeChannel = req.selectedChannel;
111
+ if (!activeChannel) return;
112
+
113
+ const requestId = `gemini-${Date.now()}-${Math.random()}`;
114
+ let modelFromUrl = '';
115
+ const urlMatch = req.url.match(/\/models\/([\w.-]+):/);
116
+ if (urlMatch) {
117
+ modelFromUrl = urlMatch[1];
118
+ }
119
+
120
+ requestMetadata.set(req, {
121
+ id: requestId,
122
+ channel: activeChannel.name,
123
+ channelId: activeChannel.id,
124
+ startTime: Date.now(),
125
+ modelFromUrl
126
+ });
127
+
128
+ proxyReq.removeHeader('authorization');
129
+ proxyReq.removeHeader('x-goog-api-key');
130
+ proxyReq.setHeader('authorization', `Bearer ${activeChannel.apiKey}`);
131
+ if (!proxyReq.getHeader('content-type')) {
132
+ proxyReq.setHeader('content-type', 'application/json');
133
+ }
134
+ });
135
+
136
+ proxyApp.use(async (req, res) => {
137
+ try {
138
+ const channel = await allocateChannel({ source: 'gemini', enableSessionBinding: false });
139
+ req.selectedChannel = channel;
140
+
141
+ const release = (() => {
142
+ let released = false;
143
+ return () => {
144
+ if (released) return;
145
+ released = true;
146
+ releaseChannel(channel.id, 'gemini');
147
+ broadcastSchedulerState('gemini', getSchedulerState('gemini'));
148
+ };
149
+ })();
150
+
151
+ res.on('close', release);
152
+ res.on('error', release);
153
+
154
+ broadcastSchedulerState('gemini', getSchedulerState('gemini'));
155
+
156
+ const target = resolveGeminiTarget(channel.baseUrl, req.url);
157
+
158
+ proxy.web(req, res, {
159
+ target,
160
+ changeOrigin: true,
161
+ proxyTimeout: 120000, // 代理连接超时 2 分钟
162
+ timeout: 120000 // 请求超时 2 分钟
163
+ }, (err) => {
164
+ release();
165
+ if (err) {
166
+ recordFailure(channel.id, 'gemini', err);
167
+ console.error('Gemini proxy error:', err);
168
+ if (res && !res.headersSent) {
169
+ res.status(502).json({
170
+ error: {
171
+ message: 'Proxy error: ' + err.message,
172
+ type: 'proxy_error'
173
+ }
174
+ });
175
+ }
176
+ }
177
+ });
178
+ } catch (error) {
179
+ console.error('Gemini channel allocation error:', error);
180
+ if (!res.headersSent) {
181
+ res.status(503).json({
182
+ error: {
183
+ message: error.message || 'No Gemini channel available',
184
+ type: 'channel_pool_exhausted'
185
+ }
186
+ });
187
+ }
188
+ }
189
+ });
190
+
191
+ // 监听代理响应 (OpenAI 兼容格式)
192
+ proxy.on('proxyRes', (proxyRes, req, res) => {
193
+ const metadata = requestMetadata.get(req);
194
+ if (!metadata) {
195
+ return;
196
+ }
197
+
198
+ // 检查响应是否已关闭
199
+ if (res.writableEnded || res.destroyed) {
200
+ requestMetadata.delete(req);
201
+ return;
202
+ }
203
+
204
+ // 标记响应是否已关闭
205
+ let isResponseClosed = false;
206
+
207
+ // 监听响应关闭事件
208
+ res.on('close', () => {
209
+ isResponseClosed = true;
210
+ requestMetadata.delete(req);
211
+ });
212
+
213
+ // 监听响应错误事件
214
+ res.on('error', (err) => {
215
+ isResponseClosed = true;
216
+ // 忽略客户端断开连接的常见错误
217
+ if (err.code !== 'EPIPE' && err.code !== 'ECONNRESET') {
218
+ console.error('Response error:', err);
219
+ }
220
+ requestMetadata.delete(req);
221
+ });
222
+
223
+ let buffer = '';
224
+ let tokenData = {
225
+ inputTokens: 0,
226
+ outputTokens: 0,
227
+ cachedTokens: 0,
228
+ reasoningTokens: 0,
229
+ totalTokens: 0,
230
+ model: ''
231
+ };
232
+
233
+ proxyRes.on('data', (chunk) => {
234
+ // 如果响应已关闭,停止处理
235
+ if (isResponseClosed) {
236
+ return;
237
+ }
238
+
239
+ buffer += chunk.toString();
240
+
241
+ // 检查是否是 SSE 流
242
+ if (proxyRes.headers['content-type']?.includes('text/event-stream')) {
243
+ // 处理 SSE 事件
244
+ const events = buffer.split('\n\n');
245
+ buffer = events.pop() || '';
246
+
247
+ events.forEach((eventText, index) => {
248
+ if (!eventText.trim()) return;
249
+
250
+ try {
251
+ const lines = eventText.split('\n');
252
+ let data = '';
253
+
254
+ lines.forEach(line => {
255
+ if (line.startsWith('data:')) {
256
+ data = line.substring(5).trim();
257
+ }
258
+ });
259
+
260
+ if (!data) return;
261
+
262
+ if (data === '[DONE]') return;
263
+
264
+ const parsed = JSON.parse(data);
265
+
266
+ // 提取模型信息
267
+ if (parsed.model && !tokenData.model) {
268
+ tokenData.model = parsed.model;
269
+ }
270
+
271
+ // 提取 usage 信息 (支持 OpenAI 和 Gemini 原生格式)
272
+ if (tokenData.inputTokens === 0) {
273
+ // OpenAI 格式
274
+ if (parsed.usage) {
275
+ tokenData.inputTokens = parsed.usage.prompt_tokens || parsed.usage.input_tokens || 0;
276
+ tokenData.outputTokens = parsed.usage.completion_tokens || parsed.usage.output_tokens || 0;
277
+ tokenData.totalTokens = parsed.usage.total_tokens || 0;
278
+
279
+ // Gemini 可能包含缓存信息
280
+ if (parsed.usage.prompt_tokens_details) {
281
+ tokenData.cachedTokens = parsed.usage.prompt_tokens_details.cached_tokens || 0;
282
+ }
283
+ }
284
+ // Gemini 原生格式
285
+ else if (parsed.usageMetadata) {
286
+ tokenData.inputTokens = parsed.usageMetadata.promptTokenCount || 0;
287
+ tokenData.outputTokens = parsed.usageMetadata.candidatesTokenCount || 0;
288
+ tokenData.totalTokens = parsed.usageMetadata.totalTokenCount || 0;
289
+
290
+ // Gemini 缓存信息
291
+ if (parsed.usageMetadata.cachedContentTokenCount) {
292
+ tokenData.cachedTokens = parsed.usageMetadata.cachedContentTokenCount;
293
+ }
294
+ }
295
+ }
296
+ } catch (err) {
297
+ // 忽略解析错误
298
+ }
299
+ });
300
+ }
301
+ });
302
+
303
+ proxyRes.on('end', () => {
304
+ // 如果不是流式响应,尝试从完整响应中解析
305
+ if (!proxyRes.headers['content-type']?.includes('text/event-stream')) {
306
+ try {
307
+ const parsed = JSON.parse(buffer);
308
+ if (parsed.model) {
309
+ tokenData.model = parsed.model;
310
+ }
311
+
312
+ // OpenAI 格式
313
+ if (parsed.usage) {
314
+ tokenData.inputTokens = parsed.usage.prompt_tokens || parsed.usage.input_tokens || 0;
315
+ tokenData.outputTokens = parsed.usage.completion_tokens || parsed.usage.output_tokens || 0;
316
+ tokenData.totalTokens = parsed.usage.total_tokens || 0;
317
+
318
+ if (parsed.usage.prompt_tokens_details) {
319
+ tokenData.cachedTokens = parsed.usage.prompt_tokens_details.cached_tokens || 0;
320
+ }
321
+ }
322
+ // Gemini 原生格式
323
+ else if (parsed.usageMetadata) {
324
+ tokenData.inputTokens = parsed.usageMetadata.promptTokenCount || 0;
325
+ tokenData.outputTokens = parsed.usageMetadata.candidatesTokenCount || 0;
326
+ tokenData.totalTokens = parsed.usageMetadata.totalTokenCount || 0;
327
+
328
+ if (parsed.usageMetadata.cachedContentTokenCount) {
329
+ tokenData.cachedTokens = parsed.usageMetadata.cachedContentTokenCount;
330
+ }
331
+ }
332
+ } catch (err) {
333
+ // 忽略解析错误
334
+ }
335
+ }
336
+
337
+ // 如果没有从响应中提取到模型,使用 URL 中的模型
338
+ if (!tokenData.model && metadata.modelFromUrl) {
339
+ tokenData.model = metadata.modelFromUrl;
340
+ }
341
+
342
+ // 记录日志和统计
343
+ const now = new Date();
344
+ const time = now.toLocaleTimeString('zh-CN', {
345
+ hour12: false,
346
+ hour: '2-digit',
347
+ minute: '2-digit',
348
+ second: '2-digit'
349
+ });
350
+
351
+ // 记录统计数据(先计算)
352
+ const tokens = {
353
+ input: tokenData.inputTokens,
354
+ output: tokenData.outputTokens,
355
+ total: tokenData.totalTokens || (tokenData.inputTokens + tokenData.outputTokens)
356
+ };
357
+ const cost = calculateCost(tokenData.model, tokens);
358
+
359
+ // 只有在有 token 数据时才广播日志和记录统计
360
+ if (tokenData.inputTokens > 0 || tokenData.outputTokens > 0 || tokenData.totalTokens > 0) {
361
+ // 广播日志(仅当响应仍然开放时)
362
+ if (!isResponseClosed) {
363
+ const logData = {
364
+ type: 'log',
365
+ id: metadata.id,
366
+ time: time,
367
+ channel: metadata.channel,
368
+ model: tokenData.model,
369
+ inputTokens: tokenData.inputTokens,
370
+ outputTokens: tokenData.outputTokens,
371
+ cachedTokens: tokenData.cachedTokens,
372
+ reasoningTokens: tokenData.reasoningTokens,
373
+ totalTokens: tokenData.totalTokens || (tokenData.inputTokens + tokenData.outputTokens),
374
+ cost: cost,
375
+ source: 'gemini'
376
+ };
377
+
378
+ broadcastLog(logData);
379
+ }
380
+
381
+ // 记录统计
382
+ const duration = Date.now() - metadata.startTime;
383
+
384
+ recordGeminiRequest({
385
+ id: metadata.id,
386
+ timestamp: new Date(metadata.startTime).toISOString(),
387
+ toolType: 'gemini',
388
+ channel: metadata.channel,
389
+ channelId: metadata.channelId,
390
+ model: tokenData.model,
391
+ tokens: {
392
+ input: tokenData.inputTokens,
393
+ output: tokenData.outputTokens,
394
+ cached: tokenData.cachedTokens,
395
+ total: tokens.total
396
+ },
397
+ duration: duration,
398
+ success: true,
399
+ cost: cost
400
+ });
401
+
402
+ recordSuccess(metadata.channelId, 'gemini');
403
+ }
404
+
405
+ if (!isResponseClosed) {
406
+ requestMetadata.delete(req);
407
+ }
408
+ });
409
+
410
+ proxyRes.on('error', (err) => {
411
+ // 忽略代理响应错误(可能是网络问题)
412
+ if (err.code !== 'EPIPE' && err.code !== 'ECONNRESET') {
413
+ console.error('Proxy response error:', err);
414
+ }
415
+ isResponseClosed = true;
416
+ recordFailure(metadata.channelId, 'gemini', err);
417
+ requestMetadata.delete(req);
418
+ });
419
+ });
420
+
421
+ // 处理代理错误
422
+ proxy.on('error', (err, req, res) => {
423
+ console.error('Gemini proxy error:', err);
424
+ if (req && req.selectedChannel) {
425
+ recordFailure(req.selectedChannel.id, 'gemini', err);
426
+ releaseChannel(req.selectedChannel.id, 'gemini');
427
+ broadcastSchedulerState('gemini', getSchedulerState('gemini'));
428
+ }
429
+ if (res && !res.headersSent) {
430
+ res.status(502).json({
431
+ error: {
432
+ message: 'Proxy error: ' + err.message,
433
+ type: 'proxy_error'
434
+ }
435
+ });
436
+ }
437
+ });
438
+
439
+ // 启动服务器
440
+ proxyServer = http.createServer(proxyApp);
441
+
442
+ return new Promise((resolve, reject) => {
443
+ proxyServer.listen(port, '127.0.0.1', () => {
444
+ console.log(`Gemini proxy server started on http://127.0.0.1:${port}`);
445
+
446
+ // 保存代理启动时间(如果是切换渠道,保留原有启动时间)
447
+ saveProxyStartTime('gemini', preserveStartTime);
448
+
449
+ resolve({ success: true, port });
450
+ });
451
+
452
+ proxyServer.on('error', (err) => {
453
+ if (err.code === 'EADDRINUSE') {
454
+ console.error(chalk.red(`\nGemini proxy port ${port} is already in use`));
455
+ } else {
456
+ console.error('Failed to start Gemini proxy server:', err);
457
+ }
458
+ proxyServer = null;
459
+ proxyApp = null;
460
+ currentPort = null;
461
+ reject(err);
462
+ });
463
+ });
464
+ } catch (err) {
465
+ console.error('Error starting Gemini proxy server:', err);
466
+ throw err;
467
+ }
468
+ }
469
+
470
+ // 停止 Gemini 代理服务器
471
+ async function stopGeminiProxyServer(options = {}) {
472
+ // options.clearStartTime - 是否清除启动时间(默认 true)
473
+ const clearStartTime = options.clearStartTime !== false;
474
+
475
+ if (!proxyServer) {
476
+ return { success: true, message: 'Gemini proxy server not running' };
477
+ }
478
+
479
+ requestMetadata.clear();
480
+
481
+ return new Promise((resolve) => {
482
+ proxyServer.close(() => {
483
+ console.log('Gemini proxy server stopped');
484
+
485
+ // 清除代理启动时间(仅当明确要求时)
486
+ if (clearStartTime) {
487
+ clearProxyStartTime('gemini');
488
+ }
489
+
490
+ proxyServer = null;
491
+ proxyApp = null;
492
+ const stoppedPort = currentPort;
493
+ currentPort = null;
494
+ resolve({ success: true, port: stoppedPort });
495
+ });
496
+ });
497
+ }
498
+
499
+ // 获取代理服务器状态
500
+ function getGeminiProxyStatus() {
501
+ const config = loadConfig();
502
+ const startTime = getProxyStartTime('gemini');
503
+ const runtime = getProxyRuntime('gemini');
504
+
505
+ return {
506
+ running: !!proxyServer,
507
+ port: currentPort,
508
+ defaultPort: config.ports?.geminiProxy || 10090,
509
+ startTime,
510
+ runtime
511
+ };
512
+ }
513
+
514
+ module.exports = {
515
+ startGeminiProxyServer,
516
+ stopGeminiProxyServer,
517
+ getGeminiProxyStatus
518
+ };