coding-tool-x 3.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 (185) hide show
  1. package/CHANGELOG.md +599 -0
  2. package/LICENSE +21 -0
  3. package/README.md +439 -0
  4. package/bin/ctx.js +8 -0
  5. package/dist/web/assets/Analytics-DN_YsnkW.js +39 -0
  6. package/dist/web/assets/Analytics-DuYvId7u.css +1 -0
  7. package/dist/web/assets/ConfigTemplates-Bidwfdf2.css +1 -0
  8. package/dist/web/assets/ConfigTemplates-DpXIMy0p.js +1 -0
  9. package/dist/web/assets/Home-38JTUlYt.js +1 -0
  10. package/dist/web/assets/Home-CjupSEWE.css +1 -0
  11. package/dist/web/assets/PluginManager-CX2tgq2H.js +1 -0
  12. package/dist/web/assets/PluginManager-ROyoZ-6m.css +1 -0
  13. package/dist/web/assets/ProjectList-C1lDcsn6.js +1 -0
  14. package/dist/web/assets/ProjectList-oJIyIRkP.css +1 -0
  15. package/dist/web/assets/SessionList-C55tjV7i.css +1 -0
  16. package/dist/web/assets/SessionList-CZ7T6rVx.js +1 -0
  17. package/dist/web/assets/SkillManager-D7pd-d_P.css +1 -0
  18. package/dist/web/assets/SkillManager-DLN9f79y.js +1 -0
  19. package/dist/web/assets/WorkspaceManager-CrwgQgmP.css +1 -0
  20. package/dist/web/assets/WorkspaceManager-DxlHZkpZ.js +1 -0
  21. package/dist/web/assets/icons-DRrXwWZi.js +1 -0
  22. package/dist/web/assets/index-CetESrXw.css +1 -0
  23. package/dist/web/assets/index-Cfvn-2Gb.js +2 -0
  24. package/dist/web/assets/markdown-BfC0goYb.css +10 -0
  25. package/dist/web/assets/markdown-C9MYpaSi.js +1 -0
  26. package/dist/web/assets/naive-ui-DlpKk-8M.js +1 -0
  27. package/dist/web/assets/vendors-DMjSfzlv.js +7 -0
  28. package/dist/web/assets/vue-vendor-DET08QYg.js +45 -0
  29. package/dist/web/favicon.ico +0 -0
  30. package/dist/web/index.html +20 -0
  31. package/dist/web/logo.png +0 -0
  32. package/docs/bannel.png +0 -0
  33. package/docs/home.png +0 -0
  34. package/docs/logo.png +0 -0
  35. package/docs/model-redirection.md +251 -0
  36. package/docs/multi-channel-load-balancing.md +249 -0
  37. package/package.json +80 -0
  38. package/src/commands/channels.js +551 -0
  39. package/src/commands/cli-type.js +101 -0
  40. package/src/commands/daemon.js +365 -0
  41. package/src/commands/doctor.js +333 -0
  42. package/src/commands/export-config.js +205 -0
  43. package/src/commands/list.js +222 -0
  44. package/src/commands/logs.js +261 -0
  45. package/src/commands/plugin.js +585 -0
  46. package/src/commands/port-config.js +135 -0
  47. package/src/commands/proxy-control.js +264 -0
  48. package/src/commands/proxy.js +152 -0
  49. package/src/commands/resume.js +137 -0
  50. package/src/commands/search.js +190 -0
  51. package/src/commands/security.js +37 -0
  52. package/src/commands/stats.js +398 -0
  53. package/src/commands/switch.js +48 -0
  54. package/src/commands/toggle-proxy.js +247 -0
  55. package/src/commands/ui.js +99 -0
  56. package/src/commands/update.js +97 -0
  57. package/src/commands/workspace.js +454 -0
  58. package/src/config/default.js +69 -0
  59. package/src/config/loader.js +149 -0
  60. package/src/config/model-metadata.js +167 -0
  61. package/src/config/model-metadata.json +125 -0
  62. package/src/config/model-pricing.js +35 -0
  63. package/src/config/paths.js +190 -0
  64. package/src/index.js +680 -0
  65. package/src/plugins/constants.js +15 -0
  66. package/src/plugins/event-bus.js +54 -0
  67. package/src/plugins/manifest-validator.js +129 -0
  68. package/src/plugins/plugin-api.js +128 -0
  69. package/src/plugins/plugin-installer.js +601 -0
  70. package/src/plugins/plugin-loader.js +229 -0
  71. package/src/plugins/plugin-manager.js +170 -0
  72. package/src/plugins/registry.js +152 -0
  73. package/src/plugins/schema/plugin-manifest.json +115 -0
  74. package/src/reset-config.js +94 -0
  75. package/src/server/api/agents.js +826 -0
  76. package/src/server/api/aliases.js +36 -0
  77. package/src/server/api/channels.js +368 -0
  78. package/src/server/api/claude-hooks.js +480 -0
  79. package/src/server/api/codex-channels.js +417 -0
  80. package/src/server/api/codex-projects.js +104 -0
  81. package/src/server/api/codex-proxy.js +195 -0
  82. package/src/server/api/codex-sessions.js +483 -0
  83. package/src/server/api/codex-statistics.js +57 -0
  84. package/src/server/api/commands.js +482 -0
  85. package/src/server/api/config-export.js +212 -0
  86. package/src/server/api/config-registry.js +357 -0
  87. package/src/server/api/config-sync.js +155 -0
  88. package/src/server/api/config-templates.js +248 -0
  89. package/src/server/api/config.js +521 -0
  90. package/src/server/api/convert.js +260 -0
  91. package/src/server/api/dashboard.js +142 -0
  92. package/src/server/api/env.js +144 -0
  93. package/src/server/api/favorites.js +77 -0
  94. package/src/server/api/gemini-channels.js +366 -0
  95. package/src/server/api/gemini-projects.js +91 -0
  96. package/src/server/api/gemini-proxy.js +173 -0
  97. package/src/server/api/gemini-sessions.js +376 -0
  98. package/src/server/api/gemini-statistics.js +57 -0
  99. package/src/server/api/health-check.js +31 -0
  100. package/src/server/api/mcp.js +399 -0
  101. package/src/server/api/opencode-channels.js +419 -0
  102. package/src/server/api/opencode-projects.js +99 -0
  103. package/src/server/api/opencode-proxy.js +207 -0
  104. package/src/server/api/opencode-sessions.js +327 -0
  105. package/src/server/api/opencode-statistics.js +57 -0
  106. package/src/server/api/plugins.js +463 -0
  107. package/src/server/api/pm2-autostart.js +269 -0
  108. package/src/server/api/projects.js +124 -0
  109. package/src/server/api/prompts.js +279 -0
  110. package/src/server/api/proxy.js +306 -0
  111. package/src/server/api/security.js +53 -0
  112. package/src/server/api/sessions.js +514 -0
  113. package/src/server/api/settings.js +142 -0
  114. package/src/server/api/skills.js +570 -0
  115. package/src/server/api/statistics.js +238 -0
  116. package/src/server/api/ui-config.js +64 -0
  117. package/src/server/api/workspaces.js +456 -0
  118. package/src/server/codex-proxy-server.js +681 -0
  119. package/src/server/dev-server.js +26 -0
  120. package/src/server/gemini-proxy-server.js +610 -0
  121. package/src/server/index.js +422 -0
  122. package/src/server/opencode-proxy-server.js +4771 -0
  123. package/src/server/proxy-server.js +669 -0
  124. package/src/server/services/agents-service.js +1137 -0
  125. package/src/server/services/alias.js +71 -0
  126. package/src/server/services/channel-health.js +234 -0
  127. package/src/server/services/channel-scheduler.js +240 -0
  128. package/src/server/services/channels.js +447 -0
  129. package/src/server/services/codex-channels.js +705 -0
  130. package/src/server/services/codex-config.js +90 -0
  131. package/src/server/services/codex-parser.js +322 -0
  132. package/src/server/services/codex-sessions.js +936 -0
  133. package/src/server/services/codex-settings-manager.js +619 -0
  134. package/src/server/services/codex-speed-test-template.json +24 -0
  135. package/src/server/services/codex-statistics-service.js +161 -0
  136. package/src/server/services/commands-service.js +574 -0
  137. package/src/server/services/config-export-service.js +1165 -0
  138. package/src/server/services/config-registry-service.js +828 -0
  139. package/src/server/services/config-sync-manager.js +941 -0
  140. package/src/server/services/config-sync-service.js +504 -0
  141. package/src/server/services/config-templates-service.js +913 -0
  142. package/src/server/services/enhanced-cache.js +196 -0
  143. package/src/server/services/env-checker.js +409 -0
  144. package/src/server/services/env-manager.js +436 -0
  145. package/src/server/services/favorites.js +165 -0
  146. package/src/server/services/format-converter.js +620 -0
  147. package/src/server/services/gemini-channels.js +459 -0
  148. package/src/server/services/gemini-config.js +73 -0
  149. package/src/server/services/gemini-sessions.js +689 -0
  150. package/src/server/services/gemini-settings-manager.js +263 -0
  151. package/src/server/services/gemini-statistics-service.js +157 -0
  152. package/src/server/services/health-check.js +85 -0
  153. package/src/server/services/mcp-client.js +790 -0
  154. package/src/server/services/mcp-service.js +1732 -0
  155. package/src/server/services/model-detector.js +1245 -0
  156. package/src/server/services/network-access.js +80 -0
  157. package/src/server/services/opencode-channels.js +366 -0
  158. package/src/server/services/opencode-gateway-adapters.js +1168 -0
  159. package/src/server/services/opencode-gateway-converter.js +639 -0
  160. package/src/server/services/opencode-sessions.js +931 -0
  161. package/src/server/services/opencode-settings-manager.js +478 -0
  162. package/src/server/services/opencode-statistics-service.js +161 -0
  163. package/src/server/services/plugins-service.js +1268 -0
  164. package/src/server/services/prompts-service.js +534 -0
  165. package/src/server/services/proxy-runtime.js +79 -0
  166. package/src/server/services/repo-scanner-base.js +708 -0
  167. package/src/server/services/request-logger.js +130 -0
  168. package/src/server/services/response-decoder.js +21 -0
  169. package/src/server/services/security-config.js +131 -0
  170. package/src/server/services/session-cache.js +127 -0
  171. package/src/server/services/session-converter.js +577 -0
  172. package/src/server/services/sessions.js +900 -0
  173. package/src/server/services/settings-manager.js +163 -0
  174. package/src/server/services/skill-service.js +1482 -0
  175. package/src/server/services/speed-test.js +1146 -0
  176. package/src/server/services/statistics-service.js +1043 -0
  177. package/src/server/services/ui-config.js +132 -0
  178. package/src/server/services/workspace-service.js +830 -0
  179. package/src/server/utils/pricing.js +73 -0
  180. package/src/server/websocket-server.js +513 -0
  181. package/src/ui/menu.js +139 -0
  182. package/src/ui/prompts.js +100 -0
  183. package/src/utils/format.js +43 -0
  184. package/src/utils/port-helper.js +108 -0
  185. package/src/utils/session.js +240 -0
@@ -0,0 +1,366 @@
1
+ const express = require('express');
2
+ const router = express.Router();
3
+ const {
4
+ getChannels,
5
+ createChannel,
6
+ updateChannel,
7
+ deleteChannel,
8
+ getEnabledChannels,
9
+ saveChannelOrder
10
+ } = require('../services/gemini-channels');
11
+ const { getSchedulerState } = require('../services/channel-scheduler');
12
+ const { getChannelHealthStatus, resetChannelHealth } = require('../services/channel-health');
13
+ const { broadcastSchedulerState } = require('../websocket-server');
14
+ const { isGeminiInstalled } = require('../services/gemini-config');
15
+ const {
16
+ testChannelSpeed,
17
+ getLatencyLevel,
18
+ sanitizeBatchConcurrency,
19
+ runWithConcurrencyLimit
20
+ } = require('../services/speed-test');
21
+ const { clearGeminiRedirectCache } = require('../gemini-proxy-server');
22
+ const { getDefaultSpeedTestModelByToolType } = require('../../config/model-metadata');
23
+ const GEMINI_GATEWAY_SOURCE_TYPE = 'gemini';
24
+
25
+ function getDefaultGeminiModel() {
26
+ return getDefaultSpeedTestModelByToolType('gemini');
27
+ }
28
+
29
+ module.exports = (config) => {
30
+ /**
31
+ * GET /api/gemini/channels
32
+ * 获取所有 Gemini 渠道(包含健康状态)
33
+ */
34
+ router.get('/', (req, res) => {
35
+ try {
36
+ if (!isGeminiInstalled()) {
37
+ return res.json({
38
+ channels: [],
39
+ error: 'Gemini CLI not installed'
40
+ });
41
+ }
42
+
43
+ const data = getChannels();
44
+ // 为每个渠道添加健康状态
45
+ const channelsWithHealth = (data.channels || []).map(ch => ({
46
+ ...ch,
47
+ health: getChannelHealthStatus(ch.id, 'gemini')
48
+ }));
49
+ res.json({ channels: channelsWithHealth });
50
+ } catch (err) {
51
+ console.error('[Gemini Channels API] Failed to get channels:', err);
52
+ res.status(500).json({ error: err.message });
53
+ }
54
+ });
55
+
56
+ /**
57
+ * GET /api/gemini/channels/:id/models
58
+ * 获取渠道可用模型列表
59
+ */
60
+ router.get('/:id/models', async (req, res) => {
61
+ try {
62
+ const { id } = req.params;
63
+ const channels = getChannels().channels || [];
64
+ const channel = channels.find(ch => ch.id === id);
65
+
66
+ if (!channel) {
67
+ return res.status(404).json({ error: '渠道不存在' });
68
+ }
69
+
70
+ const gatewaySourceType = GEMINI_GATEWAY_SOURCE_TYPE;
71
+ const models = [getDefaultGeminiModel()];
72
+ const result = {
73
+ models,
74
+ supported: models.length > 0,
75
+ cached: false,
76
+ fallbackUsed: false,
77
+ lastChecked: new Date().toISOString(),
78
+ error: models.length > 0 ? null : '未配置默认模型列表',
79
+ errorHint: models.length > 0 ? null : '请在设置中配置 Gemini 默认模型'
80
+ };
81
+
82
+ res.json({
83
+ channelId: id,
84
+ gatewaySourceType,
85
+ models: result.models,
86
+ supported: result.supported,
87
+ cached: result.cached,
88
+ fallbackUsed: result.fallbackUsed,
89
+ fetchedAt: result.lastChecked || new Date().toISOString(),
90
+ error: result.error,
91
+ errorHint: result.errorHint
92
+ });
93
+ } catch (error) {
94
+ console.error('[Gemini Channels API] Error fetching models:', error);
95
+ res.status(500).json({
96
+ error: '获取模型列表失败',
97
+ channelId: req.params.id
98
+ });
99
+ }
100
+ });
101
+
102
+ /**
103
+ * POST /api/gemini/channels
104
+ * 创建新渠道
105
+ * Body: { name, baseUrl, apiKey, model, websiteUrl }
106
+ */
107
+ router.post('/', (req, res) => {
108
+ try {
109
+ if (!isGeminiInstalled()) {
110
+ return res.status(404).json({ error: 'Gemini CLI not installed' });
111
+ }
112
+
113
+ const {
114
+ name,
115
+ baseUrl,
116
+ apiKey,
117
+ model,
118
+ websiteUrl,
119
+ enabled,
120
+ weight,
121
+ maxConcurrency,
122
+ modelRedirects,
123
+ speedTestModel,
124
+ presetId,
125
+ gatewaySourceType
126
+ } = req.body;
127
+
128
+ if (!name || !baseUrl) {
129
+ return res.status(400).json({ error: 'Missing required fields: name, baseUrl' });
130
+ }
131
+
132
+ if (!apiKey) {
133
+ return res.status(400).json({ error: 'Missing required fields: apiKey' });
134
+ }
135
+
136
+ const channel = createChannel(name, baseUrl, apiKey, model || getDefaultGeminiModel(), {
137
+ websiteUrl,
138
+ enabled,
139
+ weight,
140
+ maxConcurrency,
141
+ modelRedirects: modelRedirects || [],
142
+ speedTestModel: speedTestModel || null,
143
+ presetId: presetId || null,
144
+ gatewaySourceType
145
+ });
146
+ res.json(channel);
147
+ broadcastSchedulerState('gemini', getSchedulerState('gemini'));
148
+ } catch (err) {
149
+ console.error('[Gemini Channels API] Failed to create channel:', err);
150
+ res.status(500).json({ error: err.message });
151
+ }
152
+ });
153
+
154
+ /**
155
+ * PUT /api/gemini/channels/:channelId
156
+ * 更新渠道
157
+ */
158
+ router.put('/:channelId', (req, res) => {
159
+ try {
160
+ if (!isGeminiInstalled()) {
161
+ return res.status(404).json({ error: 'Gemini CLI not installed' });
162
+ }
163
+
164
+ const { channelId } = req.params;
165
+ const updates = req.body;
166
+
167
+ const channel = updateChannel(channelId, updates);
168
+ // 清除该渠道的模型重定向日志缓存,使下次请求时重新打印
169
+ clearGeminiRedirectCache(channelId);
170
+ res.json(channel);
171
+ broadcastSchedulerState('gemini', getSchedulerState('gemini'));
172
+ } catch (err) {
173
+ console.error('[Gemini Channels API] Failed to update channel:', err);
174
+ res.status(500).json({ error: err.message });
175
+ }
176
+ });
177
+
178
+ /**
179
+ * DELETE /api/gemini/channels/:channelId
180
+ * 删除渠道
181
+ */
182
+ router.delete('/:channelId', async (req, res) => {
183
+ try {
184
+ if (!isGeminiInstalled()) {
185
+ return res.status(404).json({ error: 'Gemini CLI not installed' });
186
+ }
187
+
188
+ const { channelId } = req.params;
189
+ const result = await deleteChannel(channelId);
190
+ res.json(result);
191
+ broadcastSchedulerState('gemini', getSchedulerState('gemini'));
192
+ } catch (err) {
193
+ console.error('[Gemini Channels API] Failed to delete channel:', err);
194
+ res.status(500).json({ error: err.message });
195
+ }
196
+ });
197
+
198
+ /**
199
+ * POST /api/gemini/channels/order
200
+ * 保存渠道顺序
201
+ */
202
+ router.post('/order', (req, res) => {
203
+ try {
204
+ if (!isGeminiInstalled()) {
205
+ return res.status(404).json({ error: 'Gemini CLI not installed' });
206
+ }
207
+
208
+ const { order } = req.body;
209
+
210
+ if (!Array.isArray(order)) {
211
+ return res.status(400).json({ error: 'order must be an array' });
212
+ }
213
+
214
+ saveChannelOrder(order);
215
+ res.json({ success: true });
216
+ } catch (err) {
217
+ console.error('[Gemini Channels API] Failed to save channel order:', err);
218
+ res.status(500).json({ error: err.message });
219
+ }
220
+ });
221
+
222
+ /**
223
+ * GET /api/gemini/channels/enabled
224
+ * 获取所有启用的渠道(供调度器使用)
225
+ */
226
+ router.get('/enabled', (req, res) => {
227
+ try {
228
+ if (!isGeminiInstalled()) {
229
+ return res.json({ channels: [] });
230
+ }
231
+
232
+ const channels = getEnabledChannels();
233
+ res.json({ channels });
234
+ } catch (err) {
235
+ console.error('[Gemini Channels API] Failed to get enabled channels:', err);
236
+ res.status(500).json({ error: err.message });
237
+ }
238
+ });
239
+
240
+ /**
241
+ * POST /api/gemini/channels/:channelId/speed-test
242
+ * 测试单个渠道速度
243
+ */
244
+ router.post('/:channelId/speed-test', async (req, res) => {
245
+ try {
246
+ if (!isGeminiInstalled()) {
247
+ return res.status(404).json({ error: 'Gemini CLI not installed' });
248
+ }
249
+
250
+ const { channelId } = req.params;
251
+ const { timeout = 10000 } = req.body;
252
+ const data = getChannels();
253
+ const channel = data.channels.find(ch => ch.id === channelId);
254
+
255
+ if (!channel) {
256
+ return res.status(404).json({ error: '渠道不存在' });
257
+ }
258
+
259
+ const speedTestType = GEMINI_GATEWAY_SOURCE_TYPE;
260
+ const result = await testChannelSpeed(channel, timeout, speedTestType);
261
+ result.level = getLatencyLevel(result.latency);
262
+ result.gatewaySourceType = speedTestType;
263
+
264
+ res.json(result);
265
+ } catch (error) {
266
+ console.error('[Gemini Channels API] Error testing channel speed:', error);
267
+ res.status(500).json({ error: error.message });
268
+ }
269
+ });
270
+
271
+ /**
272
+ * POST /api/gemini/channels/speed-test-all
273
+ * 测试所有渠道速度
274
+ */
275
+ router.post('/speed-test-all', async (req, res) => {
276
+ try {
277
+ if (!isGeminiInstalled()) {
278
+ return res.json({ results: [], message: 'Gemini CLI not installed' });
279
+ }
280
+
281
+ const { timeout = 10000, concurrency } = req.body || {};
282
+ const data = getChannels();
283
+ const channels = data.channels || [];
284
+ const safeConcurrency = sanitizeBatchConcurrency(concurrency);
285
+
286
+ if (channels.length === 0) {
287
+ return res.json({ results: [], message: '没有可测试的渠道' });
288
+ }
289
+
290
+ const results = await runWithConcurrencyLimit(
291
+ channels,
292
+ safeConcurrency,
293
+ async channel => {
294
+ const speedTestType = GEMINI_GATEWAY_SOURCE_TYPE;
295
+ const result = await testChannelSpeed(channel, timeout, speedTestType);
296
+ result.level = getLatencyLevel(result.latency);
297
+ result.gatewaySourceType = speedTestType;
298
+ return result;
299
+ }
300
+ );
301
+
302
+ // 成功在前,成功结果按延迟升序
303
+ results.sort((a, b) => {
304
+ if (a.success && !b.success) return -1;
305
+ if (!a.success && b.success) return 1;
306
+ if (a.success && b.success) {
307
+ const aLatency = (a.latency === null || a.latency === undefined) ? Infinity : a.latency;
308
+ const bLatency = (b.latency === null || b.latency === undefined) ? Infinity : b.latency;
309
+ return aLatency - bLatency;
310
+ }
311
+ return 0;
312
+ });
313
+
314
+ res.json({
315
+ results,
316
+ summary: {
317
+ total: results.length,
318
+ success: results.filter(r => r.success).length,
319
+ failed: results.filter(r => !r.success).length,
320
+ avgLatency: calculateAvgLatency(results),
321
+ concurrency: safeConcurrency
322
+ }
323
+ });
324
+ } catch (error) {
325
+ console.error('[Gemini Channels API] Error testing all channels speed:', error);
326
+ res.status(500).json({ error: error.message });
327
+ }
328
+ });
329
+
330
+ /**
331
+ * POST /api/gemini/channels/:channelId/reset-health
332
+ * 重置渠道健康状态
333
+ */
334
+ router.post('/:channelId/reset-health', (req, res) => {
335
+ try {
336
+ if (!isGeminiInstalled()) {
337
+ return res.status(404).json({ error: 'Gemini CLI not installed' });
338
+ }
339
+
340
+ const { channelId } = req.params;
341
+ resetChannelHealth(channelId, 'gemini');
342
+ broadcastSchedulerState('gemini', getSchedulerState('gemini'));
343
+
344
+ res.json({
345
+ success: true,
346
+ message: '渠道健康状态已重置',
347
+ health: getChannelHealthStatus(channelId, 'gemini')
348
+ });
349
+ } catch (error) {
350
+ console.error('[Gemini Channels API] Error resetting channel health:', error);
351
+ res.status(500).json({ error: error.message });
352
+ }
353
+ });
354
+
355
+ return router;
356
+ };
357
+
358
+ // 计算平均延迟
359
+ function calculateAvgLatency(results) {
360
+ const successResults = results.filter(
361
+ r => r.success && r.latency !== null && r.latency !== undefined
362
+ );
363
+ if (successResults.length === 0) return null;
364
+ const sum = successResults.reduce((acc, r) => acc + r.latency, 0);
365
+ return Math.round(sum / successResults.length);
366
+ }
@@ -0,0 +1,91 @@
1
+ const express = require('express');
2
+ const router = express.Router();
3
+ const { getProjects, saveProjectOrder, deleteProject } = require('../services/gemini-sessions');
4
+ const { isGeminiInstalled } = require('../services/gemini-config');
5
+
6
+ module.exports = (config) => {
7
+ /**
8
+ * GET /api/gemini/projects
9
+ * 获取所有 Gemini 项目列表
10
+ */
11
+ router.get('/', (req, res) => {
12
+ try {
13
+ // 检查 Gemini 是否安装
14
+ if (!isGeminiInstalled()) {
15
+ return res.json({
16
+ projects: [],
17
+ currentProject: null,
18
+ error: 'Gemini CLI not installed or not found'
19
+ });
20
+ }
21
+
22
+ const projects = getProjects();
23
+
24
+ res.json({
25
+ projects,
26
+ currentProject: projects[0] ? projects[0].name : null
27
+ });
28
+ } catch (err) {
29
+ console.error('[Gemini API] Failed to get projects:', err);
30
+
31
+ if (err.code === 'ENOENT') {
32
+ return res.status(404).json({
33
+ error: 'Gemini tmp directory not found',
34
+ projects: []
35
+ });
36
+ }
37
+
38
+ res.status(500).json({
39
+ error: err.message,
40
+ projects: []
41
+ });
42
+ }
43
+ });
44
+
45
+ /**
46
+ * POST /api/gemini/projects/order
47
+ * 保存项目排序
48
+ */
49
+ router.post('/order', (req, res) => {
50
+ try {
51
+ if (!isGeminiInstalled()) {
52
+ return res.status(404).json({ error: 'Gemini CLI not installed' });
53
+ }
54
+
55
+ const { order } = req.body;
56
+
57
+ if (!Array.isArray(order)) {
58
+ return res.status(400).json({ error: 'order must be an array' });
59
+ }
60
+
61
+ saveProjectOrder(order);
62
+
63
+ res.json({ success: true });
64
+ } catch (err) {
65
+ console.error('[Gemini API] Failed to save project order:', err);
66
+ res.status(500).json({ error: err.message });
67
+ }
68
+ });
69
+
70
+ /**
71
+ * DELETE /api/gemini/projects/:projectHash
72
+ * 删除项目(删除项目下所有会话)
73
+ */
74
+ router.delete('/:projectHash', (req, res) => {
75
+ try {
76
+ if (!isGeminiInstalled()) {
77
+ return res.status(404).json({ error: 'Gemini CLI not installed' });
78
+ }
79
+
80
+ const { projectHash } = req.params;
81
+ const result = deleteProject(projectHash);
82
+
83
+ res.json(result);
84
+ } catch (err) {
85
+ console.error('[Gemini API] Failed to delete project:', err);
86
+ res.status(500).json({ error: err.message });
87
+ }
88
+ });
89
+
90
+ return router;
91
+ };
@@ -0,0 +1,173 @@
1
+ const express = require('express');
2
+ const router = express.Router();
3
+ const {
4
+ startGeminiProxyServer,
5
+ stopGeminiProxyServer,
6
+ getGeminiProxyStatus
7
+ } = require('../gemini-proxy-server');
8
+ const {
9
+ setProxyConfig,
10
+ restoreSettings,
11
+ isProxyConfig,
12
+ getCurrentProxyPort,
13
+ configExists,
14
+ hasBackup
15
+ } = require('../services/gemini-settings-manager');
16
+ const { getChannels, getEnabledChannels } = require('../services/gemini-channels');
17
+ const { PATHS, ensureStorageDirMigrated } = require('../../config/paths');
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+
21
+ function sanitizeChannel(channel) {
22
+ if (!channel) return null;
23
+ const { apiKey, ...rest } = channel;
24
+ return rest;
25
+ }
26
+
27
+ // 保存激活渠道ID
28
+ function saveActiveChannelId(channelId) {
29
+ ensureStorageDirMigrated();
30
+ const filePath = PATHS.activeChannel.gemini;
31
+ const dir = path.dirname(filePath);
32
+ if (!fs.existsSync(dir)) {
33
+ fs.mkdirSync(dir, { recursive: true });
34
+ }
35
+ fs.writeFileSync(filePath, JSON.stringify({ activeChannelId: channelId }, null, 2), 'utf8');
36
+ }
37
+
38
+ function removeActiveChannelFile() {
39
+ ensureStorageDirMigrated();
40
+ const filePath = PATHS.activeChannel.gemini;
41
+ if (fs.existsSync(filePath)) {
42
+ fs.unlinkSync(filePath);
43
+ console.log('[Gemini Proxy] Removed gemini-active-channel.json');
44
+ }
45
+ }
46
+
47
+ // 获取代理状态
48
+ router.get('/status', (req, res) => {
49
+ try {
50
+ const proxyStatus = getGeminiProxyStatus();
51
+ const configStatus = {
52
+ isProxyConfig: isProxyConfig(),
53
+ configExists: configExists(),
54
+ hasBackup: hasBackup(),
55
+ currentProxyPort: getCurrentProxyPort()
56
+ };
57
+ const { channels } = getChannels();
58
+ const enabledChannels = channels.filter(ch => ch.enabled !== false);
59
+ const activeChannel = enabledChannels[0]; // 多渠道模式:第一个启用的渠道
60
+
61
+ res.json({
62
+ proxy: proxyStatus,
63
+ config: configStatus,
64
+ activeChannel: sanitizeChannel(activeChannel)
65
+ });
66
+ } catch (error) {
67
+ res.status(500).json({ error: error.message });
68
+ }
69
+ });
70
+
71
+ // 启动代理
72
+ router.post('/start', async (req, res) => {
73
+ try {
74
+ // 1. 检查 Gemini 配置文件是否存在
75
+ if (!configExists()) {
76
+ return res.status(400).json({
77
+ error: 'Gemini .env not found. Please run Gemini CLI at least once or create ~/.gemini/.env manually.'
78
+ });
79
+ }
80
+
81
+ // 2. 获取当前启用的渠道(多渠道模式)
82
+ const enabledChannels = getEnabledChannels();
83
+ const currentChannel = enabledChannels[0];
84
+ if (!currentChannel) {
85
+ return res.status(400).json({
86
+ error: 'No enabled Gemini channel found. Please create and enable a channel first.'
87
+ });
88
+ }
89
+
90
+ // 3. 保存当前激活渠道ID(用于代理模式)
91
+ saveActiveChannelId(currentChannel.id);
92
+ console.log(`[Gemini Proxy] Saved active channel: ${currentChannel.name} (${currentChannel.id})`);
93
+
94
+ // 4. 启动代理服务器
95
+ const proxyResult = await startGeminiProxyServer();
96
+
97
+ if (!proxyResult.success) {
98
+ return res.status(500).json({ error: 'Failed to start Gemini proxy server' });
99
+ }
100
+
101
+ // 5. 设置代理配置(备份并修改 .env 和 settings.json)
102
+ setProxyConfig(proxyResult.port);
103
+
104
+ const { broadcastProxyState } = require('../websocket-server');
105
+ const proxyStatus = getGeminiProxyStatus();
106
+ const { channels: allChannels } = getChannels();
107
+ const activeChannel = allChannels.filter(ch => ch.enabled !== false)[0];
108
+ broadcastProxyState('gemini', proxyStatus, activeChannel, allChannels);
109
+
110
+ res.json({
111
+ success: true,
112
+ port: proxyResult.port,
113
+ activeChannel: sanitizeChannel(currentChannel),
114
+ message: `Gemini proxy started on port ${proxyResult.port}, active channel: ${currentChannel.name}`
115
+ });
116
+ } catch (error) {
117
+ console.error('[Gemini Proxy] Error starting proxy:', error);
118
+ res.status(500).json({ error: error.message });
119
+ }
120
+ });
121
+
122
+ // 停止代理
123
+ router.post('/stop', async (req, res) => {
124
+ try {
125
+ // 1. 获取当前启用的渠道(多渠道模式)
126
+ const { channels } = getChannels();
127
+ const enabledChannels = channels.filter(ch => ch.enabled !== false);
128
+ const activeChannel = enabledChannels[0];
129
+
130
+ // 2. 停止代理服务器
131
+ const proxyResult = await stopGeminiProxyServer();
132
+
133
+ // 3. 恢复原始配置
134
+ const { broadcastProxyState } = require('../websocket-server');
135
+ if (hasBackup()) {
136
+ restoreSettings();
137
+ console.log('[Gemini Proxy] Restored settings from backup');
138
+
139
+ // Enforce single-channel mode: apply the active channel and disable all others
140
+ if (activeChannel) {
141
+ const { applyChannelToSettings } = require('../services/gemini-channels');
142
+ applyChannelToSettings(activeChannel.id);
143
+ console.log(`[Gemini Proxy] Single-channel mode enforced: ${activeChannel.name}`);
144
+ }
145
+
146
+ // 删除 gemini-active-channel.json
147
+ removeActiveChannelFile();
148
+
149
+ const response = {
150
+ success: true,
151
+ message: `Gemini proxy stopped, settings restored${activeChannel ? ' (channel: ' + activeChannel.name + ')' : ''}`,
152
+ port: proxyResult.port,
153
+ restoredChannel: activeChannel?.name
154
+ };
155
+ res.json(response);
156
+ } else {
157
+ res.json({
158
+ success: true,
159
+ message: 'Gemini proxy stopped (no backup to restore)',
160
+ port: proxyResult.port
161
+ });
162
+ }
163
+
164
+ const proxyStatus = getGeminiProxyStatus();
165
+ const { channels: latestChannels } = getChannels();
166
+ broadcastProxyState('gemini', proxyStatus, activeChannel, latestChannels);
167
+ } catch (error) {
168
+ console.error('[Gemini Proxy] Error stopping proxy:', error);
169
+ res.status(500).json({ error: error.message });
170
+ }
171
+ });
172
+
173
+ module.exports = router;