@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,125 @@
1
+ const express = require('express');
2
+ const router = express.Router();
3
+ const { loadConfig } = require('../../config/loader');
4
+
5
+ // Services
6
+ const { loadUIConfig } = require('../services/ui-config');
7
+ const { loadFavorites } = require('../services/favorites');
8
+ const { getAllChannels } = require('../services/channels');
9
+ const { getProxyStatus } = require('../proxy-server');
10
+ const { getCodexProxyStatus } = require('../codex-proxy-server');
11
+ const { getGeminiProxyStatus } = require('../gemini-proxy-server');
12
+ const { getProjectAndSessionCounts: getClaudeCounts } = require('../services/sessions');
13
+ const { getProjectAndSessionCounts: getCodexCounts } = require('../services/codex-sessions');
14
+ const { getProjectAndSessionCounts: getGeminiCounts } = require('../services/gemini-sessions');
15
+
16
+ // Channel-specific services
17
+ const { getChannels: getCodexChannels } = require('../services/codex-channels');
18
+ const { getChannels: getGeminiChannels } = require('../services/gemini-channels');
19
+
20
+ // Statistics
21
+ const { getTodayStatistics } = require('../services/statistics-service');
22
+ const { getTodayStatistics: getCodexTodayStatistics } = require('../services/codex-statistics-service');
23
+ const { getTodayStatistics: getGeminiTodayStatistics } = require('../services/gemini-statistics-service');
24
+
25
+ /**
26
+ * GET /api/dashboard/init
27
+ * 聚合首页所需的所有数据,一次请求返回
28
+ */
29
+ router.get('/init', async (req, res) => {
30
+ try {
31
+ const config = loadConfig();
32
+
33
+ // 并行获取所有数据
34
+ const [
35
+ uiConfig,
36
+ favorites,
37
+ claudeChannels,
38
+ codexChannels,
39
+ geminiChannels,
40
+ claudeProxyStatus,
41
+ codexProxyStatus,
42
+ geminiProxyStatus,
43
+ claudeTodayStats,
44
+ codexTodayStats,
45
+ geminiTodayStats,
46
+ claudeCounts,
47
+ codexCounts,
48
+ geminiCounts
49
+ ] = await Promise.all([
50
+ // UI Config
51
+ Promise.resolve(loadUIConfig()),
52
+
53
+ // Favorites
54
+ Promise.resolve(loadFavorites()),
55
+
56
+ // Channels
57
+ Promise.resolve(getAllChannels()),
58
+ Promise.resolve(getCodexChannels()),
59
+ Promise.resolve(getGeminiChannels()),
60
+
61
+ // Proxy Status
62
+ Promise.resolve(getProxyStatus()),
63
+ Promise.resolve(getCodexProxyStatus()),
64
+ Promise.resolve(getGeminiProxyStatus()),
65
+
66
+ // Today Stats (所有平台)
67
+ Promise.resolve(getTodayStatistics()),
68
+ Promise.resolve(getCodexTodayStatistics()),
69
+ Promise.resolve(getGeminiTodayStatistics()),
70
+
71
+ // 轻量级统计
72
+ Promise.resolve(getClaudeCounts(config)),
73
+ Promise.resolve(getCodexCounts()),
74
+ Promise.resolve(getGeminiCounts())
75
+ ]);
76
+
77
+ // 格式化统计数据:取 summary 中的数据
78
+ const formatStats = (stats) => {
79
+ if (stats && stats.summary) {
80
+ return {
81
+ requests: stats.summary.requests || 0,
82
+ tokens: stats.summary.tokens || 0,
83
+ cost: stats.summary.cost || 0
84
+ };
85
+ }
86
+ return { requests: 0, tokens: 0, cost: 0 };
87
+ };
88
+
89
+ res.json({
90
+ success: true,
91
+ data: {
92
+ uiConfig,
93
+ favorites,
94
+ channels: {
95
+ claude: claudeChannels,
96
+ codex: codexChannels,
97
+ gemini: geminiChannels
98
+ },
99
+ proxyStatus: {
100
+ claude: claudeProxyStatus,
101
+ codex: codexProxyStatus,
102
+ gemini: geminiProxyStatus
103
+ },
104
+ counts: {
105
+ claude: claudeCounts || { projectCount: 0, sessionCount: 0 },
106
+ codex: codexCounts || { projectCount: 0, sessionCount: 0 },
107
+ gemini: geminiCounts || { projectCount: 0, sessionCount: 0 }
108
+ },
109
+ todayStats: {
110
+ claude: formatStats(claudeTodayStats),
111
+ codex: formatStats(codexTodayStats),
112
+ gemini: formatStats(geminiTodayStats)
113
+ }
114
+ }
115
+ });
116
+ } catch (error) {
117
+ console.error('Dashboard init error:', error);
118
+ res.status(500).json({
119
+ success: false,
120
+ message: error.message
121
+ });
122
+ }
123
+ });
124
+
125
+ module.exports = router;
@@ -0,0 +1,144 @@
1
+ /**
2
+ * 环境变量检测 API 路由
3
+ */
4
+
5
+ const express = require('express');
6
+ const router = express.Router();
7
+ const envChecker = require('../services/env-checker');
8
+ const envManager = require('../services/env-manager');
9
+
10
+ /**
11
+ * GET /api/env/check
12
+ * 检测环境变量冲突
13
+ * Query: platform (可选) - claude/codex/gemini
14
+ */
15
+ router.get('/check', (req, res) => {
16
+ try {
17
+ const { platform } = req.query;
18
+ const conflicts = envChecker.checkEnvConflicts(platform || null);
19
+ const stats = envChecker.getConflictStats(conflicts);
20
+
21
+ res.json({
22
+ success: true,
23
+ conflicts,
24
+ stats
25
+ });
26
+ } catch (error) {
27
+ console.error('[Env API] Check failed:', error);
28
+ res.status(500).json({
29
+ success: false,
30
+ error: error.message
31
+ });
32
+ }
33
+ });
34
+
35
+ /**
36
+ * POST /api/env/delete
37
+ * 删除选中的环境变量(带自动备份)
38
+ * Body: { conflicts: Array }
39
+ */
40
+ router.post('/delete', (req, res) => {
41
+ try {
42
+ const { conflicts } = req.body;
43
+
44
+ if (!conflicts || !Array.isArray(conflicts) || conflicts.length === 0) {
45
+ return res.status(400).json({
46
+ success: false,
47
+ error: '请选择要删除的环境变量'
48
+ });
49
+ }
50
+
51
+ const result = envManager.deleteEnvVars(conflicts);
52
+
53
+ res.json({
54
+ success: true,
55
+ ...result,
56
+ message: `已删除 ${result.results.filter(r => r.success).length} 个环境变量,备份已保存`
57
+ });
58
+ } catch (error) {
59
+ console.error('[Env API] Delete failed:', error);
60
+ res.status(400).json({
61
+ success: false,
62
+ error: error.message
63
+ });
64
+ }
65
+ });
66
+
67
+ /**
68
+ * GET /api/env/backups
69
+ * 获取备份列表
70
+ */
71
+ router.get('/backups', (req, res) => {
72
+ try {
73
+ const backups = envManager.getBackupList();
74
+
75
+ res.json({
76
+ success: true,
77
+ backups
78
+ });
79
+ } catch (error) {
80
+ console.error('[Env API] Get backups failed:', error);
81
+ res.status(500).json({
82
+ success: false,
83
+ error: error.message
84
+ });
85
+ }
86
+ });
87
+
88
+ /**
89
+ * POST /api/env/restore
90
+ * 从备份恢复
91
+ * Body: { backupPath: string }
92
+ */
93
+ router.post('/restore', (req, res) => {
94
+ try {
95
+ const { backupPath } = req.body;
96
+
97
+ if (!backupPath) {
98
+ return res.status(400).json({
99
+ success: false,
100
+ error: '请指定备份文件路径'
101
+ });
102
+ }
103
+
104
+ const result = envManager.restoreFromBackup(backupPath);
105
+
106
+ res.json({
107
+ success: true,
108
+ ...result,
109
+ message: '环境变量已恢复'
110
+ });
111
+ } catch (error) {
112
+ console.error('[Env API] Restore failed:', error);
113
+ res.status(400).json({
114
+ success: false,
115
+ error: error.message
116
+ });
117
+ }
118
+ });
119
+
120
+ /**
121
+ * DELETE /api/env/backups/:fileName
122
+ * 删除备份文件
123
+ */
124
+ router.delete('/backups/:fileName', (req, res) => {
125
+ try {
126
+ const { fileName } = req.params;
127
+ const backupPath = `${envManager.BACKUP_DIR}/${fileName}`;
128
+
129
+ envManager.deleteBackup(backupPath);
130
+
131
+ res.json({
132
+ success: true,
133
+ message: '备份已删除'
134
+ });
135
+ } catch (error) {
136
+ console.error('[Env API] Delete backup failed:', error);
137
+ res.status(400).json({
138
+ success: false,
139
+ error: error.message
140
+ });
141
+ }
142
+ });
143
+
144
+ module.exports = router;
@@ -0,0 +1,77 @@
1
+ const express = require('express');
2
+ const router = express.Router();
3
+ const {
4
+ addFavorite,
5
+ removeFavorite,
6
+ isFavorite,
7
+ getFavorites,
8
+ getAllFavorites
9
+ } = require('../services/favorites');
10
+
11
+ // Get all favorites
12
+ router.get('/', (req, res) => {
13
+ try {
14
+ const favorites = getAllFavorites();
15
+ res.json({ success: true, favorites });
16
+ } catch (error) {
17
+ console.error('Error getting favorites:', error);
18
+ res.status(500).json({ error: error.message });
19
+ }
20
+ });
21
+
22
+ // Get favorites for a specific channel
23
+ router.get('/:channel', (req, res) => {
24
+ try {
25
+ const { channel } = req.params;
26
+ const favorites = getFavorites(channel);
27
+ res.json({ success: true, favorites });
28
+ } catch (error) {
29
+ console.error('Error getting channel favorites:', error);
30
+ res.status(500).json({ error: error.message });
31
+ }
32
+ });
33
+
34
+ // Add a favorite
35
+ router.post('/', (req, res) => {
36
+ try {
37
+ const { channel, sessionData } = req.body;
38
+
39
+ if (!channel || !sessionData) {
40
+ return res.status(400).json({ error: 'Missing required fields' });
41
+ }
42
+
43
+ const result = addFavorite(channel, sessionData);
44
+ res.json(result);
45
+ } catch (error) {
46
+ console.error('Error adding favorite:', error);
47
+ res.status(500).json({ error: error.message });
48
+ }
49
+ });
50
+
51
+ // Remove a favorite
52
+ router.delete('/:channel/:projectName/:sessionId', (req, res) => {
53
+ try {
54
+ const { channel, projectName, sessionId } = req.params;
55
+
56
+ const result = removeFavorite(channel, decodeURIComponent(projectName), sessionId);
57
+ res.json(result);
58
+ } catch (error) {
59
+ console.error('Error removing favorite:', error);
60
+ res.status(500).json({ error: error.message });
61
+ }
62
+ });
63
+
64
+ // Check if favorited
65
+ router.get('/check/:channel/:projectName/:sessionId', (req, res) => {
66
+ try {
67
+ const { channel, projectName, sessionId } = req.params;
68
+
69
+ const favorited = isFavorite(channel, decodeURIComponent(projectName), sessionId);
70
+ res.json({ success: true, isFavorite: favorited });
71
+ } catch (error) {
72
+ console.error('Error checking favorite:', error);
73
+ res.status(500).json({ error: error.message });
74
+ }
75
+ });
76
+
77
+ module.exports = router;
@@ -0,0 +1,261 @@
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 { testChannelSpeed, testMultipleChannels, getLatencyLevel } = require('../services/speed-test');
16
+
17
+ module.exports = (config) => {
18
+ /**
19
+ * GET /api/gemini/channels
20
+ * 获取所有 Gemini 渠道(包含健康状态)
21
+ */
22
+ router.get('/', (req, res) => {
23
+ try {
24
+ if (!isGeminiInstalled()) {
25
+ return res.json({
26
+ channels: [],
27
+ error: 'Gemini CLI not installed'
28
+ });
29
+ }
30
+
31
+ const data = getChannels();
32
+ // 为每个渠道添加健康状态
33
+ const channelsWithHealth = (data.channels || []).map(ch => ({
34
+ ...ch,
35
+ health: getChannelHealthStatus(ch.id, 'gemini')
36
+ }));
37
+ res.json({ channels: channelsWithHealth });
38
+ } catch (err) {
39
+ console.error('[Gemini Channels API] Failed to get channels:', err);
40
+ res.status(500).json({ error: err.message });
41
+ }
42
+ });
43
+
44
+ /**
45
+ * POST /api/gemini/channels
46
+ * 创建新渠道
47
+ * Body: { name, baseUrl, apiKey, model, websiteUrl }
48
+ */
49
+ router.post('/', (req, res) => {
50
+ try {
51
+ if (!isGeminiInstalled()) {
52
+ return res.status(404).json({ error: 'Gemini CLI not installed' });
53
+ }
54
+
55
+ const { name, baseUrl, apiKey, model, websiteUrl, enabled, weight, maxConcurrency } = req.body;
56
+
57
+ if (!name || !baseUrl || !apiKey) {
58
+ return res.status(400).json({ error: 'Missing required fields: name, baseUrl, apiKey' });
59
+ }
60
+
61
+ const channel = createChannel(name, baseUrl, apiKey, model || 'gemini-2.5-pro', {
62
+ websiteUrl,
63
+ enabled,
64
+ weight,
65
+ maxConcurrency
66
+ });
67
+ res.json(channel);
68
+ broadcastSchedulerState('gemini', getSchedulerState('gemini'));
69
+ } catch (err) {
70
+ console.error('[Gemini Channels API] Failed to create channel:', err);
71
+ res.status(500).json({ error: err.message });
72
+ }
73
+ });
74
+
75
+ /**
76
+ * PUT /api/gemini/channels/:channelId
77
+ * 更新渠道
78
+ */
79
+ router.put('/:channelId', (req, res) => {
80
+ try {
81
+ if (!isGeminiInstalled()) {
82
+ return res.status(404).json({ error: 'Gemini CLI not installed' });
83
+ }
84
+
85
+ const { channelId } = req.params;
86
+ const updates = req.body;
87
+
88
+ const channel = updateChannel(channelId, updates);
89
+ res.json(channel);
90
+ broadcastSchedulerState('gemini', getSchedulerState('gemini'));
91
+ } catch (err) {
92
+ console.error('[Gemini Channels API] Failed to update channel:', err);
93
+ res.status(500).json({ error: err.message });
94
+ }
95
+ });
96
+
97
+ /**
98
+ * DELETE /api/gemini/channels/:channelId
99
+ * 删除渠道
100
+ */
101
+ router.delete('/:channelId', (req, res) => {
102
+ try {
103
+ if (!isGeminiInstalled()) {
104
+ return res.status(404).json({ error: 'Gemini CLI not installed' });
105
+ }
106
+
107
+ const { channelId } = req.params;
108
+ const result = deleteChannel(channelId);
109
+ res.json(result);
110
+ broadcastSchedulerState('gemini', getSchedulerState('gemini'));
111
+ } catch (err) {
112
+ console.error('[Gemini Channels API] Failed to delete channel:', err);
113
+ res.status(500).json({ error: err.message });
114
+ }
115
+ });
116
+
117
+ /**
118
+ * POST /api/gemini/channels/order
119
+ * 保存渠道顺序
120
+ */
121
+ router.post('/order', (req, res) => {
122
+ try {
123
+ if (!isGeminiInstalled()) {
124
+ return res.status(404).json({ error: 'Gemini CLI not installed' });
125
+ }
126
+
127
+ const { order } = req.body;
128
+
129
+ if (!Array.isArray(order)) {
130
+ return res.status(400).json({ error: 'order must be an array' });
131
+ }
132
+
133
+ saveChannelOrder(order);
134
+ res.json({ success: true });
135
+ } catch (err) {
136
+ console.error('[Gemini Channels API] Failed to save channel order:', err);
137
+ res.status(500).json({ error: err.message });
138
+ }
139
+ });
140
+
141
+ /**
142
+ * GET /api/gemini/channels/enabled
143
+ * 获取所有启用的渠道(供调度器使用)
144
+ */
145
+ router.get('/enabled', (req, res) => {
146
+ try {
147
+ if (!isGeminiInstalled()) {
148
+ return res.json({ channels: [] });
149
+ }
150
+
151
+ const channels = getEnabledChannels();
152
+ res.json({ channels });
153
+ } catch (err) {
154
+ console.error('[Gemini Channels API] Failed to get enabled channels:', err);
155
+ res.status(500).json({ error: err.message });
156
+ }
157
+ });
158
+
159
+ /**
160
+ * POST /api/gemini/channels/:channelId/speed-test
161
+ * 测试单个渠道速度
162
+ */
163
+ router.post('/:channelId/speed-test', async (req, res) => {
164
+ try {
165
+ if (!isGeminiInstalled()) {
166
+ return res.status(404).json({ error: 'Gemini CLI not installed' });
167
+ }
168
+
169
+ const { channelId } = req.params;
170
+ const { timeout = 10000 } = req.body;
171
+ const data = getChannels();
172
+ const channel = data.channels.find(ch => ch.id === channelId);
173
+
174
+ if (!channel) {
175
+ return res.status(404).json({ error: '渠道不存在' });
176
+ }
177
+
178
+ const result = await testChannelSpeed(channel, timeout, 'gemini');
179
+ result.level = getLatencyLevel(result.latency);
180
+
181
+ res.json(result);
182
+ } catch (error) {
183
+ console.error('[Gemini Channels API] Error testing channel speed:', error);
184
+ res.status(500).json({ error: error.message });
185
+ }
186
+ });
187
+
188
+ /**
189
+ * POST /api/gemini/channels/speed-test-all
190
+ * 测试所有渠道速度
191
+ */
192
+ router.post('/speed-test-all', async (req, res) => {
193
+ try {
194
+ if (!isGeminiInstalled()) {
195
+ return res.json({ results: [], message: 'Gemini CLI not installed' });
196
+ }
197
+
198
+ const { timeout = 10000 } = req.body;
199
+ const data = getChannels();
200
+ const channels = data.channels || [];
201
+
202
+ if (channels.length === 0) {
203
+ return res.json({ results: [], message: '没有可测试的渠道' });
204
+ }
205
+
206
+ // Gemini 渠道使用 'gemini' 类型
207
+ const results = await testMultipleChannels(channels, timeout, 'gemini');
208
+ results.forEach(r => {
209
+ r.level = getLatencyLevel(r.latency);
210
+ });
211
+
212
+ res.json({
213
+ results,
214
+ summary: {
215
+ total: results.length,
216
+ success: results.filter(r => r.success).length,
217
+ failed: results.filter(r => !r.success).length,
218
+ avgLatency: calculateAvgLatency(results)
219
+ }
220
+ });
221
+ } catch (error) {
222
+ console.error('[Gemini Channels API] Error testing all channels speed:', error);
223
+ res.status(500).json({ error: error.message });
224
+ }
225
+ });
226
+
227
+ /**
228
+ * POST /api/gemini/channels/:channelId/reset-health
229
+ * 重置渠道健康状态
230
+ */
231
+ router.post('/:channelId/reset-health', (req, res) => {
232
+ try {
233
+ if (!isGeminiInstalled()) {
234
+ return res.status(404).json({ error: 'Gemini CLI not installed' });
235
+ }
236
+
237
+ const { channelId } = req.params;
238
+ resetChannelHealth(channelId, 'gemini');
239
+ broadcastSchedulerState('gemini', getSchedulerState('gemini'));
240
+
241
+ res.json({
242
+ success: true,
243
+ message: '渠道健康状态已重置',
244
+ health: getChannelHealthStatus(channelId, 'gemini')
245
+ });
246
+ } catch (error) {
247
+ console.error('[Gemini Channels API] Error resetting channel health:', error);
248
+ res.status(500).json({ error: error.message });
249
+ }
250
+ });
251
+
252
+ return router;
253
+ };
254
+
255
+ // 计算平均延迟
256
+ function calculateAvgLatency(results) {
257
+ const successResults = results.filter(r => r.success && r.latency);
258
+ if (successResults.length === 0) return null;
259
+ const sum = successResults.reduce((acc, r) => acc + r.latency, 0);
260
+ return Math.round(sum / successResults.length);
261
+ }
@@ -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
+ };