@adversity/coding-tool-x 3.1.0 → 3.1.2

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 (142) hide show
  1. package/CHANGELOG.md +39 -18
  2. package/README.md +8 -8
  3. package/dist/web/assets/ConfigTemplates-Bidwfdf2.css +1 -0
  4. package/dist/web/assets/ConfigTemplates-DvcbKKdS.js +1 -0
  5. package/dist/web/assets/Home-BJKPCBuk.css +1 -0
  6. package/dist/web/assets/Home-Cw-F_Wnu.js +1 -0
  7. package/dist/web/assets/PluginManager-ROyoZ-6m.css +1 -0
  8. package/dist/web/assets/PluginManager-jy_4GVxI.js +1 -0
  9. package/dist/web/assets/ProjectList-C1fQb9OW.css +1 -0
  10. package/dist/web/assets/ProjectList-Df1-NcNr.js +1 -0
  11. package/dist/web/assets/SessionList-BGJWyneI.css +1 -0
  12. package/dist/web/assets/SessionList-UWcZtC2r.js +1 -0
  13. package/dist/web/assets/SkillManager-D7pd-d_P.css +1 -0
  14. package/dist/web/assets/SkillManager-IRdseMKB.js +1 -0
  15. package/dist/web/assets/Terminal-BasTyDut.js +1 -0
  16. package/dist/web/assets/Terminal-DGNJeVtc.css +1 -0
  17. package/dist/web/assets/WorkspaceManager-CrwgQgmP.css +1 -0
  18. package/dist/web/assets/WorkspaceManager-D-D2kK1V.js +1 -0
  19. package/dist/web/assets/icons-kcfLIMBB.js +1 -0
  20. package/dist/web/assets/index-CoB3zF0K.css +1 -0
  21. package/dist/web/assets/index-CryrSLv8.js +2 -0
  22. package/dist/web/assets/markdown-BfC0goYb.css +10 -0
  23. package/dist/web/assets/markdown-C9MYpaSi.js +1 -0
  24. package/dist/web/assets/naive-ui-CSrLusZZ.js +1 -0
  25. package/dist/web/assets/{vendors-D2HHw_aW.js → vendors-CO3Upi1d.js} +2 -2
  26. package/dist/web/assets/vue-vendor-DqyWIXEb.js +45 -0
  27. package/dist/web/assets/xterm-6GBZ9nXN.css +32 -0
  28. package/dist/web/assets/xterm-BJzAjXCH.js +13 -0
  29. package/dist/web/index.html +8 -6
  30. package/package.json +4 -2
  31. package/src/commands/channels.js +48 -1
  32. package/src/commands/cli-type.js +4 -2
  33. package/src/commands/daemon.js +81 -12
  34. package/src/commands/doctor.js +10 -9
  35. package/src/commands/list.js +1 -1
  36. package/src/commands/logs.js +6 -4
  37. package/src/commands/port-config.js +24 -4
  38. package/src/commands/proxy-control.js +12 -6
  39. package/src/commands/search.js +1 -1
  40. package/src/commands/security.js +3 -2
  41. package/src/commands/stats.js +226 -52
  42. package/src/commands/switch.js +1 -1
  43. package/src/commands/toggle-proxy.js +31 -6
  44. package/src/commands/update.js +97 -0
  45. package/src/commands/workspace.js +1 -1
  46. package/src/config/default.js +41 -2
  47. package/src/config/loader.js +74 -8
  48. package/src/config/model-metadata.js +415 -0
  49. package/src/config/model-pricing.js +23 -93
  50. package/src/config/paths.js +105 -33
  51. package/src/index.js +64 -3
  52. package/src/plugins/constants.js +3 -2
  53. package/src/plugins/plugin-api.js +1 -1
  54. package/src/reset-config.js +4 -2
  55. package/src/server/api/agents.js +57 -14
  56. package/src/server/api/channels.js +112 -33
  57. package/src/server/api/codex-channels.js +111 -18
  58. package/src/server/api/codex-proxy.js +14 -8
  59. package/src/server/api/commands.js +71 -18
  60. package/src/server/api/config-export.js +0 -6
  61. package/src/server/api/config-registry.js +11 -3
  62. package/src/server/api/config.js +376 -5
  63. package/src/server/api/convert.js +133 -0
  64. package/src/server/api/dashboard.js +22 -6
  65. package/src/server/api/gemini-channels.js +107 -18
  66. package/src/server/api/gemini-proxy.js +14 -8
  67. package/src/server/api/gemini-sessions.js +1 -1
  68. package/src/server/api/health-check.js +4 -3
  69. package/src/server/api/mcp.js +3 -3
  70. package/src/server/api/opencode-channels.js +497 -0
  71. package/src/server/api/opencode-projects.js +99 -0
  72. package/src/server/api/opencode-proxy.js +207 -0
  73. package/src/server/api/opencode-sessions.js +345 -0
  74. package/src/server/api/opencode-statistics.js +57 -0
  75. package/src/server/api/plugins.js +66 -19
  76. package/src/server/api/prompts.js +2 -2
  77. package/src/server/api/proxy.js +7 -4
  78. package/src/server/api/sessions.js +3 -0
  79. package/src/server/api/settings.js +111 -0
  80. package/src/server/api/skills.js +69 -18
  81. package/src/server/api/workspaces.js +78 -6
  82. package/src/server/codex-proxy-server.js +36 -22
  83. package/src/server/dev-server.js +1 -1
  84. package/src/server/gemini-proxy-server.js +21 -7
  85. package/src/server/index.js +174 -58
  86. package/src/server/opencode-proxy-server.js +5486 -0
  87. package/src/server/proxy-server.js +33 -22
  88. package/src/server/services/agents-service.js +61 -24
  89. package/src/server/services/channel-scheduler.js +9 -5
  90. package/src/server/services/channels.js +64 -37
  91. package/src/server/services/codex-channels.js +56 -43
  92. package/src/server/services/codex-sessions.js +105 -6
  93. package/src/server/services/codex-settings-manager.js +271 -49
  94. package/src/server/services/codex-statistics-service.js +2 -2
  95. package/src/server/services/commands-service.js +84 -25
  96. package/src/server/services/config-export-service.js +7 -45
  97. package/src/server/services/config-registry-service.js +63 -17
  98. package/src/server/services/config-sync-manager.js +160 -7
  99. package/src/server/services/config-templates-service.js +204 -51
  100. package/src/server/services/env-checker.js +50 -13
  101. package/src/server/services/env-manager.js +155 -19
  102. package/src/server/services/favorites.js +5 -3
  103. package/src/server/services/gemini-channels.js +33 -44
  104. package/src/server/services/gemini-statistics-service.js +2 -2
  105. package/src/server/services/mcp-service.js +350 -9
  106. package/src/server/services/model-detector.js +707 -221
  107. package/src/server/services/network-access.js +80 -0
  108. package/src/server/services/opencode-channels.js +208 -0
  109. package/src/server/services/opencode-gateway-converter.js +639 -0
  110. package/src/server/services/opencode-sessions.js +931 -0
  111. package/src/server/services/opencode-settings-manager.js +478 -0
  112. package/src/server/services/opencode-statistics-service.js +255 -0
  113. package/src/server/services/plugins-service.js +479 -22
  114. package/src/server/services/prompts-service.js +53 -11
  115. package/src/server/services/proxy-runtime.js +1 -1
  116. package/src/server/services/repo-scanner-base.js +1 -1
  117. package/src/server/services/response-decoder.js +21 -0
  118. package/src/server/services/security-config.js +1 -1
  119. package/src/server/services/session-cache.js +1 -1
  120. package/src/server/services/skill-service.js +300 -46
  121. package/src/server/services/speed-test.js +464 -186
  122. package/src/server/services/statistics-service.js +2 -2
  123. package/src/server/services/terminal-commands.js +10 -3
  124. package/src/server/services/terminal-config.js +1 -1
  125. package/src/server/services/ui-config.js +1 -1
  126. package/src/server/services/workspace-service.js +57 -100
  127. package/src/server/websocket-server.js +156 -8
  128. package/src/ui/menu.js +49 -40
  129. package/src/utils/port-helper.js +22 -8
  130. package/src/utils/session.js +5 -4
  131. package/dist/web/assets/icons-CO_2OFES.js +0 -1
  132. package/dist/web/assets/index-DI8QOi-E.js +0 -14
  133. package/dist/web/assets/index-uLHGdeZh.css +0 -41
  134. package/dist/web/assets/naive-ui-B1re3c-e.js +0 -1
  135. package/dist/web/assets/vue-vendor-6JaYHOiI.js +0 -44
  136. package/src/server/api/oauth.js +0 -294
  137. package/src/server/api/permissions.js +0 -385
  138. package/src/server/config/oauth-providers.js +0 -68
  139. package/src/server/services/oauth-callback-server.js +0 -284
  140. package/src/server/services/oauth-service.js +0 -378
  141. package/src/server/services/oauth-token-storage.js +0 -135
  142. package/src/server/services/permission-templates-service.js +0 -308
@@ -0,0 +1,207 @@
1
+ const express = require('express');
2
+ const router = express.Router();
3
+ const {
4
+ startOpenCodeProxyServer,
5
+ stopOpenCodeProxyServer,
6
+ getOpenCodeProxyStatus,
7
+ collectProxyModelList
8
+ } = require('../opencode-proxy-server');
9
+ const {
10
+ configExists,
11
+ hasBackup,
12
+ setProxyConfig,
13
+ restoreSettings,
14
+ isProxyConfig,
15
+ getCurrentProxyPort
16
+ } = require('../services/opencode-settings-manager');
17
+ const { getChannels, getEnabledChannels } = require('../services/opencode-channels');
18
+ const { PATHS, ensureStorageDirMigrated } = require('../../config/paths');
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+
22
+ function sanitizeChannel(channel) {
23
+ if (!channel) return null;
24
+ return {
25
+ id: channel.id,
26
+ name: channel.name,
27
+ baseUrl: channel.baseUrl,
28
+ websiteUrl: channel.websiteUrl
29
+ };
30
+ }
31
+
32
+ // 保存激活渠道ID
33
+ function saveActiveChannelId(channelId) {
34
+ ensureStorageDirMigrated();
35
+ const filePath = PATHS.activeChannel.opencode;
36
+ const dir = path.dirname(filePath);
37
+ if (!fs.existsSync(dir)) {
38
+ fs.mkdirSync(dir, { recursive: true });
39
+ }
40
+ fs.writeFileSync(filePath, JSON.stringify({ activeChannelId: channelId }, null, 2), 'utf8');
41
+ }
42
+
43
+ // 删除激活渠道文件
44
+ function removeActiveChannelFile() {
45
+ ensureStorageDirMigrated();
46
+ const filePath = PATHS.activeChannel.opencode;
47
+ if (fs.existsSync(filePath)) {
48
+ fs.unlinkSync(filePath);
49
+ console.log('[OpenCode Proxy] Removed opencode-active-channel.json');
50
+ }
51
+ }
52
+
53
+ // 获取代理状态
54
+ router.get('/status', (req, res) => {
55
+ try {
56
+ const proxyStatus = getOpenCodeProxyStatus();
57
+ const { channels } = getChannels();
58
+ const enabledChannels = channels.filter(ch => ch.enabled !== false);
59
+ const activeChannel = enabledChannels[0];
60
+ const configStatus = {
61
+ isProxyConfig: isProxyConfig(),
62
+ configExists: configExists(),
63
+ hasBackup: hasBackup(),
64
+ currentProxyPort: getCurrentProxyPort()
65
+ };
66
+
67
+ res.json({
68
+ proxy: proxyStatus,
69
+ config: configStatus,
70
+ activeChannel: sanitizeChannel(activeChannel),
71
+ enabledChannelsCount: enabledChannels.length,
72
+ totalChannelsCount: channels.length
73
+ });
74
+ } catch (error) {
75
+ res.status(500).json({ error: error.message });
76
+ }
77
+ });
78
+
79
+ // 启动代理
80
+ router.post('/start', async (req, res) => {
81
+ try {
82
+ // 1. 获取当前启用的渠道
83
+ const enabledChannels = getEnabledChannels();
84
+ const currentChannel = enabledChannels[0];
85
+
86
+ if (!currentChannel) {
87
+ return res.status(400).json({
88
+ error: 'No enabled OpenCode channel found. Please create and enable a channel first.'
89
+ });
90
+ }
91
+
92
+ // 2. 保存当前激活渠道ID
93
+ saveActiveChannelId(currentChannel.id);
94
+ console.log(`[OpenCode Proxy] Saved active channel: ${currentChannel.name} (${currentChannel.id})`);
95
+
96
+ // 3. 启动代理服务器
97
+ const proxyResult = await startOpenCodeProxyServer();
98
+
99
+ if (!proxyResult.success) {
100
+ return res.status(500).json({ error: 'Failed to start OpenCode proxy server' });
101
+ }
102
+
103
+ // 4. 设置代理配置(写入 OpenCode 配置文件)
104
+ // 收集每个渠道的模型列表,生成 per-channel provider 配置
105
+
106
+ // 若渠道未显式填写模型,回退使用代理聚合模型(含 /v1/models 与模型探测结果)。
107
+ let detectedModels = [];
108
+ try {
109
+ detectedModels = await collectProxyModelList(enabledChannels, {
110
+ useCacheOnly: true
111
+ }) || [];
112
+ } catch (error) {
113
+ console.warn('[OpenCode Proxy] Failed to collect proxy models before writing config:', error.message);
114
+ }
115
+
116
+ const channelPayloads = enabledChannels.map((ch) => {
117
+ let models;
118
+ if (Array.isArray(ch.allowedModels) && ch.allowedModels.length > 0) {
119
+ models = ch.allowedModels;
120
+ } else {
121
+ const seen = new Set();
122
+ const collected = [];
123
+ const add = (m) => {
124
+ if (typeof m !== 'string') return;
125
+ const t = m.trim();
126
+ if (!t) return;
127
+ const k = t.toLowerCase();
128
+ if (seen.has(k)) return;
129
+ seen.add(k);
130
+ collected.push(t);
131
+ };
132
+ [ch.model, ch.speedTestModel].forEach(add);
133
+ if (ch.modelConfig && typeof ch.modelConfig === 'object') {
134
+ [ch.modelConfig.model, ch.modelConfig.opusModel, ch.modelConfig.sonnetModel, ch.modelConfig.haikuModel].forEach(add);
135
+ }
136
+ if (Array.isArray(ch.modelRedirects)) {
137
+ ch.modelRedirects.forEach(r => { add(r && r.from); add(r && r.to); });
138
+ }
139
+ detectedModels.forEach(add);
140
+ models = collected;
141
+ }
142
+ return {
143
+ name: ch.name,
144
+ providerKey: ch.providerKey || ch.name,
145
+ model: ch.model || null,
146
+ models
147
+ };
148
+ });
149
+
150
+ const activeModel = currentChannel.model || currentChannel.speedTestModel || null;
151
+ setProxyConfig(proxyResult.port, { channels: channelPayloads, model: activeModel });
152
+
153
+ // 5. 广播状态更新
154
+ const { broadcastProxyState } = require('../websocket-server');
155
+ const updatedStatus = getOpenCodeProxyStatus();
156
+ const { channels: allChannels } = getChannels();
157
+ broadcastProxyState('opencode', updatedStatus, currentChannel, allChannels);
158
+
159
+ res.json({
160
+ success: true,
161
+ port: proxyResult.port,
162
+ activeChannel: sanitizeChannel(currentChannel),
163
+ message: `OpenCode proxy started on port ${proxyResult.port}, active channel: ${currentChannel.name}`
164
+ });
165
+ } catch (error) {
166
+ console.error('[OpenCode Proxy] Error starting proxy:', error);
167
+ res.status(500).json({ error: error.message });
168
+ }
169
+ });
170
+
171
+ // 停止代理
172
+ router.post('/stop', async (req, res) => {
173
+ try {
174
+ // 1. 获取当前渠道信息
175
+ const { channels } = getChannels();
176
+ const enabledChannels = channels.filter(ch => ch.enabled !== false);
177
+ const activeChannel = enabledChannels[0];
178
+
179
+ // 2. 停止代理服务器
180
+ const proxyResult = await stopOpenCodeProxyServer();
181
+
182
+ // 3. 删除激活渠道文件
183
+ removeActiveChannelFile();
184
+
185
+ // 4. 恢复原始配置
186
+ if (hasBackup()) {
187
+ restoreSettings();
188
+ console.log('[OpenCode Proxy] Restored settings from backup');
189
+ }
190
+
191
+ // 5. 广播状态更新
192
+ const { broadcastProxyState } = require('../websocket-server');
193
+ const updatedStatus = getOpenCodeProxyStatus();
194
+ broadcastProxyState('opencode', updatedStatus, activeChannel, channels);
195
+
196
+ res.json({
197
+ success: true,
198
+ message: `OpenCode proxy stopped${activeChannel ? ' (channel: ' + activeChannel.name + ')' : ''}`,
199
+ port: proxyResult.port
200
+ });
201
+ } catch (error) {
202
+ console.error('[OpenCode Proxy] Error stopping proxy:', error);
203
+ res.status(500).json({ error: error.message });
204
+ }
205
+ });
206
+
207
+ module.exports = router;
@@ -0,0 +1,345 @@
1
+ const express = require('express');
2
+ const router = express.Router();
3
+ const {
4
+ getProjects,
5
+ getSessionsByProject,
6
+ getSessionById,
7
+ getSessionMessages,
8
+ getRecentSessions,
9
+ searchSessions,
10
+ deleteSession,
11
+ forkSession,
12
+ saveSessionOrder,
13
+ isOpenCodeInstalled
14
+ } = require('../services/opencode-sessions');
15
+ const { loadAliases } = require('../services/alias');
16
+ const { getTerminalLaunchCommand } = require('../services/terminal-config');
17
+ const { broadcastLog } = require('../websocket-server');
18
+ const os = require('os');
19
+
20
+ function isNotFoundError(error) {
21
+ if (!error || !error.message) {
22
+ return false;
23
+ }
24
+ return error.message === 'Session not found' || error.message === 'Project not found';
25
+ }
26
+
27
+ module.exports = (config) => {
28
+ /**
29
+ * GET /api/opencode/sessions/search/global?keyword=xxx
30
+ * 全局搜索
31
+ */
32
+ router.get('/search/global', (req, res) => {
33
+ try {
34
+ if (!isOpenCodeInstalled()) {
35
+ return res.status(404).json({ error: 'OpenCode CLI not installed' });
36
+ }
37
+
38
+ const { keyword } = req.query;
39
+ const parsedContextLength = req.query.context ? parseInt(req.query.context, 10) : 35;
40
+ const contextLength = Number.isFinite(parsedContextLength) ? parsedContextLength : 35;
41
+
42
+ if (!keyword) {
43
+ return res.status(400).json({ error: 'Keyword is required' });
44
+ }
45
+
46
+ const results = searchSessions(keyword, contextLength);
47
+ const totalMatches = results.reduce((sum, session) => sum + (session.matchCount || 0), 0);
48
+
49
+ res.json({
50
+ keyword,
51
+ totalMatches,
52
+ sessions: results,
53
+ source: 'opencode'
54
+ });
55
+ } catch (err) {
56
+ console.error('[OpenCode API] Failed to search sessions:', err);
57
+ res.status(500).json({ error: err.message });
58
+ }
59
+ });
60
+
61
+ /**
62
+ * GET /api/opencode/sessions/recent/list?limit=10
63
+ * 获取最近会话
64
+ */
65
+ router.get('/recent/list', (req, res) => {
66
+ try {
67
+ if (!isOpenCodeInstalled()) {
68
+ return res.status(404).json({ error: 'OpenCode CLI not installed' });
69
+ }
70
+
71
+ const limit = parseInt(req.query.limit, 10) || 5;
72
+ const sessions = getRecentSessions(limit);
73
+
74
+ res.json({
75
+ sessions,
76
+ source: 'opencode'
77
+ });
78
+ } catch (err) {
79
+ console.error('[OpenCode API] Failed to get recent sessions:', err);
80
+ res.status(500).json({ error: err.message });
81
+ }
82
+ });
83
+
84
+ /**
85
+ * GET /api/opencode/sessions/:projectName/search
86
+ * 项目内搜索
87
+ */
88
+ router.get('/:projectName/search', (req, res) => {
89
+ try {
90
+ if (!isOpenCodeInstalled()) {
91
+ return res.status(404).json({ error: 'OpenCode CLI not installed' });
92
+ }
93
+
94
+ const { projectName } = req.params;
95
+ const { keyword } = req.query;
96
+ const parsedContextLength = req.query.context ? parseInt(req.query.context, 10) : 35;
97
+ const contextLength = Number.isFinite(parsedContextLength) ? parsedContextLength : 35;
98
+
99
+ if (!keyword) {
100
+ return res.status(400).json({ error: 'Keyword is required' });
101
+ }
102
+
103
+ const results = searchSessions(keyword, contextLength, projectName);
104
+ const totalMatches = results.reduce((sum, session) => sum + (session.matchCount || 0), 0);
105
+
106
+ res.json({
107
+ keyword,
108
+ totalMatches,
109
+ sessions: results
110
+ });
111
+ } catch (err) {
112
+ console.error('[OpenCode API] Failed to search project sessions:', err);
113
+ res.status(500).json({ error: err.message });
114
+ }
115
+ });
116
+
117
+ /**
118
+ * GET /api/opencode/sessions/:projectName
119
+ * 获取项目的所有会话
120
+ */
121
+ router.get('/:projectName', (req, res) => {
122
+ try {
123
+ if (!isOpenCodeInstalled()) {
124
+ return res.status(404).json({ error: 'OpenCode CLI not installed' });
125
+ }
126
+
127
+ const { projectName } = req.params;
128
+ const sessions = getSessionsByProject(projectName);
129
+ const aliases = loadAliases();
130
+ const projects = getProjects();
131
+ const project = projects.find(p => p.name === projectName);
132
+
133
+ // 计算总大小
134
+ const totalSize = sessions.reduce((sum, session) => {
135
+ return sum + (session.size || 0);
136
+ }, 0);
137
+
138
+ res.json({
139
+ sessions,
140
+ totalSize,
141
+ aliases,
142
+ projectInfo: {
143
+ name: projectName,
144
+ fullPath: project?.fullPath || projectName,
145
+ path: project?.path || projectName,
146
+ displayName: project?.displayName || projectName
147
+ }
148
+ });
149
+ } catch (err) {
150
+ console.error('[OpenCode API] Failed to get sessions:', err);
151
+ res.status(500).json({ error: err.message });
152
+ }
153
+ });
154
+
155
+ /**
156
+ * GET /api/opencode/sessions/:projectName/:sessionId/messages
157
+ * 获取会话的消息列表
158
+ */
159
+ router.get('/:projectName/:sessionId/messages', (req, res) => {
160
+ try {
161
+ if (!isOpenCodeInstalled()) {
162
+ return res.status(404).json({ error: 'OpenCode CLI not installed' });
163
+ }
164
+
165
+ const { sessionId } = req.params;
166
+ const { page = 1, limit = 20, order = 'desc' } = req.query;
167
+ const session = getSessionById(sessionId);
168
+ if (!session) {
169
+ return res.status(404).json({ error: 'Session not found' });
170
+ }
171
+ const convertedMessages = getSessionMessages(sessionId);
172
+
173
+ // 分页处理
174
+ const pageNum = parseInt(page);
175
+ const limitNum = parseInt(limit);
176
+
177
+ let messages = convertedMessages;
178
+ if (order === 'desc') {
179
+ messages = [...messages].reverse();
180
+ }
181
+
182
+ const totalMessages = messages.length;
183
+ const start = (pageNum - 1) * limitNum;
184
+ const end = start + limitNum;
185
+ const paginatedMessages = messages.slice(start, end);
186
+
187
+ res.json({
188
+ messages: paginatedMessages,
189
+ metadata: {
190
+ gitBranch: null,
191
+ gitRepository: null,
192
+ cwd: session?.directory || null,
193
+ model: 'opencode'
194
+ },
195
+ pagination: {
196
+ page: pageNum,
197
+ limit: limitNum,
198
+ total: totalMessages,
199
+ hasMore: end < totalMessages
200
+ }
201
+ });
202
+ } catch (err) {
203
+ console.error('[OpenCode API] Failed to get session messages:', err);
204
+ res.status(500).json({ error: err.message });
205
+ }
206
+ });
207
+
208
+ /**
209
+ * DELETE /api/opencode/sessions/:projectName/:sessionId
210
+ * 删除会话
211
+ */
212
+ router.delete('/:projectName/:sessionId', (req, res) => {
213
+ try {
214
+ if (!isOpenCodeInstalled()) {
215
+ return res.status(404).json({ error: 'OpenCode CLI not installed' });
216
+ }
217
+
218
+ const { sessionId } = req.params;
219
+ const result = deleteSession(sessionId);
220
+
221
+ res.json(result);
222
+ } catch (err) {
223
+ if (isNotFoundError(err)) {
224
+ console.warn('[OpenCode API] Delete session target not found:', err.message);
225
+ return res.status(404).json({ error: err.message });
226
+ }
227
+ console.error('[OpenCode API] Failed to delete session:', err);
228
+ res.status(500).json({ error: err.message });
229
+ }
230
+ });
231
+
232
+ /**
233
+ * POST /api/opencode/sessions/:projectName/:sessionId/fork
234
+ * Fork 一个会话
235
+ */
236
+ router.post('/:projectName/:sessionId/fork', (req, res) => {
237
+ try {
238
+ if (!isOpenCodeInstalled()) {
239
+ return res.status(404).json({ error: 'OpenCode CLI not installed' });
240
+ }
241
+
242
+ const { sessionId } = req.params;
243
+ const result = forkSession(sessionId);
244
+ res.json(result);
245
+ } catch (err) {
246
+ if (isNotFoundError(err)) {
247
+ console.warn('[OpenCode API] Fork session target not found:', err.message);
248
+ return res.status(404).json({ error: err.message });
249
+ }
250
+ console.error('[OpenCode API] Failed to fork session:', err);
251
+ res.status(500).json({ error: err.message });
252
+ }
253
+ });
254
+
255
+ /**
256
+ * POST /api/opencode/sessions/:projectName/order
257
+ * 保存会话排序
258
+ */
259
+ router.post('/:projectName/order', (req, res) => {
260
+ try {
261
+ if (!isOpenCodeInstalled()) {
262
+ return res.status(404).json({ error: 'OpenCode CLI not installed' });
263
+ }
264
+
265
+ const { projectName } = req.params;
266
+ const { order } = req.body;
267
+
268
+ if (!Array.isArray(order)) {
269
+ return res.status(400).json({ error: 'order must be an array' });
270
+ }
271
+
272
+ saveSessionOrder(projectName, order);
273
+ res.json({ success: true });
274
+ } catch (err) {
275
+ console.error('[OpenCode API] Failed to save session order:', err);
276
+ res.status(500).json({ error: err.message });
277
+ }
278
+ });
279
+
280
+ /**
281
+ * POST /api/opencode/sessions/:projectName/:sessionId/launch
282
+ * 启动会话(打开终端)
283
+ */
284
+ router.post('/:projectName/:sessionId/launch', (req, res) => {
285
+ try {
286
+ if (!isOpenCodeInstalled()) {
287
+ return res.status(404).json({ error: 'OpenCode CLI not installed' });
288
+ }
289
+
290
+ const { exec } = require('child_process');
291
+ const { projectName, sessionId } = req.params;
292
+ const { targetTool } = req.body || {};
293
+
294
+ if (targetTool && targetTool !== 'opencode') {
295
+ return res.status(400).json({
296
+ error: 'OpenCode 会话暂不支持直接切换到其他 CLI,请使用 OpenCode 启动'
297
+ });
298
+ }
299
+
300
+ const session = getSessionById(sessionId);
301
+ if (!session) {
302
+ return res.status(404).json({ error: 'Session not found' });
303
+ }
304
+
305
+ const projects = getProjects();
306
+ const project = projects.find(p => p.name === projectName);
307
+ const cwd = session.directory || project?.fullPath || os.homedir();
308
+ const normalizedCwd = process.platform === 'win32' ? cwd.replace(/\//g, '\\') : cwd;
309
+
310
+ const { command, terminalId, terminalName } = getTerminalLaunchCommand(
311
+ normalizedCwd,
312
+ sessionId,
313
+ 'opencode'
314
+ );
315
+
316
+ broadcastLog({
317
+ type: 'action',
318
+ action: 'launch_opencode_session',
319
+ message: `启动 OpenCode 会话 ${sessionId.substring(0, 8)}`,
320
+ sessionId,
321
+ tool: 'opencode',
322
+ timestamp: Date.now()
323
+ });
324
+
325
+ const shellOption = process.platform === 'win32' ? { shell: 'cmd.exe' } : { shell: true };
326
+ exec(command, shellOption, (error) => {
327
+ if (error) {
328
+ console.error('[OpenCode] Failed to launch terminal:', error.message);
329
+ }
330
+ });
331
+
332
+ res.json({
333
+ success: true,
334
+ cwd,
335
+ terminal: terminalName,
336
+ terminalId
337
+ });
338
+ } catch (err) {
339
+ console.error('[OpenCode API] Failed to launch session:', err);
340
+ res.status(500).json({ error: err.message });
341
+ }
342
+ });
343
+
344
+ return router;
345
+ };
@@ -0,0 +1,57 @@
1
+ const express = require('express');
2
+ const router = express.Router();
3
+ const {
4
+ getStatistics,
5
+ getDailyStatistics,
6
+ getTodayStatistics
7
+ } = require('../services/opencode-statistics-service');
8
+
9
+ /**
10
+ * 获取 OpenCode 总体统计数据
11
+ * GET /api/opencode/statistics/summary
12
+ */
13
+ router.get('/summary', (req, res) => {
14
+ try {
15
+ const stats = getStatistics();
16
+ res.json(stats);
17
+ } catch (error) {
18
+ console.error('[OpenCode] Failed to get statistics:', error);
19
+ res.status(500).json({ error: 'Failed to get statistics' });
20
+ }
21
+ });
22
+
23
+ /**
24
+ * 获取 OpenCode 今日统计数据
25
+ * GET /api/opencode/statistics/today
26
+ */
27
+ router.get('/today', (req, res) => {
28
+ try {
29
+ const stats = getTodayStatistics();
30
+ res.json(stats);
31
+ } catch (error) {
32
+ console.error('[OpenCode] Failed to get today statistics:', error);
33
+ res.status(500).json({ error: 'Failed to get today statistics' });
34
+ }
35
+ });
36
+
37
+ /**
38
+ * 获取 OpenCode 指定日期的统计数据
39
+ * GET /api/opencode/statistics/daily/:date
40
+ */
41
+ router.get('/daily/:date', (req, res) => {
42
+ try {
43
+ const { date } = req.params;
44
+
45
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) {
46
+ return res.status(400).json({ error: 'Invalid date format. Expected YYYY-MM-DD' });
47
+ }
48
+
49
+ const stats = getDailyStatistics(date);
50
+ res.json(stats);
51
+ } catch (error) {
52
+ console.error('[OpenCode] Failed to get daily statistics:', error);
53
+ res.status(500).json({ error: 'Failed to get daily statistics' });
54
+ }
55
+ });
56
+
57
+ module.exports = router;