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,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,327 @@
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 { broadcastLog } = require('../websocket-server');
17
+ const os = require('os');
18
+
19
+ function isNotFoundError(error) {
20
+ if (!error || !error.message) {
21
+ return false;
22
+ }
23
+ return error.message === 'Session not found' || error.message === 'Project not found';
24
+ }
25
+
26
+ module.exports = (config) => {
27
+ /**
28
+ * GET /api/opencode/sessions/search/global?keyword=xxx
29
+ * 全局搜索
30
+ */
31
+ router.get('/search/global', (req, res) => {
32
+ try {
33
+ if (!isOpenCodeInstalled()) {
34
+ return res.status(404).json({ error: 'OpenCode CLI not installed' });
35
+ }
36
+
37
+ const { keyword } = req.query;
38
+ const parsedContextLength = req.query.context ? parseInt(req.query.context, 10) : 35;
39
+ const contextLength = Number.isFinite(parsedContextLength) ? parsedContextLength : 35;
40
+
41
+ if (!keyword) {
42
+ return res.status(400).json({ error: 'Keyword is required' });
43
+ }
44
+
45
+ const results = searchSessions(keyword, contextLength);
46
+ const totalMatches = results.reduce((sum, session) => sum + (session.matchCount || 0), 0);
47
+
48
+ res.json({
49
+ keyword,
50
+ totalMatches,
51
+ sessions: results,
52
+ source: 'opencode'
53
+ });
54
+ } catch (err) {
55
+ console.error('[OpenCode API] Failed to search sessions:', err);
56
+ res.status(500).json({ error: err.message });
57
+ }
58
+ });
59
+
60
+ /**
61
+ * GET /api/opencode/sessions/recent/list?limit=10
62
+ * 获取最近会话
63
+ */
64
+ router.get('/recent/list', (req, res) => {
65
+ try {
66
+ if (!isOpenCodeInstalled()) {
67
+ return res.status(404).json({ error: 'OpenCode CLI not installed' });
68
+ }
69
+
70
+ const limit = parseInt(req.query.limit, 10) || 5;
71
+ const sessions = getRecentSessions(limit);
72
+
73
+ res.json({
74
+ sessions,
75
+ source: 'opencode'
76
+ });
77
+ } catch (err) {
78
+ console.error('[OpenCode API] Failed to get recent sessions:', err);
79
+ res.status(500).json({ error: err.message });
80
+ }
81
+ });
82
+
83
+ /**
84
+ * GET /api/opencode/sessions/:projectName/search
85
+ * 项目内搜索
86
+ */
87
+ router.get('/:projectName/search', (req, res) => {
88
+ try {
89
+ if (!isOpenCodeInstalled()) {
90
+ return res.status(404).json({ error: 'OpenCode CLI not installed' });
91
+ }
92
+
93
+ const { projectName } = req.params;
94
+ const { keyword } = req.query;
95
+ const parsedContextLength = req.query.context ? parseInt(req.query.context, 10) : 35;
96
+ const contextLength = Number.isFinite(parsedContextLength) ? parsedContextLength : 35;
97
+
98
+ if (!keyword) {
99
+ return res.status(400).json({ error: 'Keyword is required' });
100
+ }
101
+
102
+ const results = searchSessions(keyword, contextLength, projectName);
103
+ const totalMatches = results.reduce((sum, session) => sum + (session.matchCount || 0), 0);
104
+
105
+ res.json({
106
+ keyword,
107
+ totalMatches,
108
+ sessions: results
109
+ });
110
+ } catch (err) {
111
+ console.error('[OpenCode API] Failed to search project sessions:', err);
112
+ res.status(500).json({ error: err.message });
113
+ }
114
+ });
115
+
116
+ /**
117
+ * GET /api/opencode/sessions/:projectName
118
+ * 获取项目的所有会话
119
+ */
120
+ router.get('/:projectName', (req, res) => {
121
+ try {
122
+ if (!isOpenCodeInstalled()) {
123
+ return res.status(404).json({ error: 'OpenCode CLI not installed' });
124
+ }
125
+
126
+ const { projectName } = req.params;
127
+ const sessions = getSessionsByProject(projectName);
128
+ const aliases = loadAliases();
129
+ const projects = getProjects();
130
+ const project = projects.find(p => p.name === projectName);
131
+
132
+ // 计算总大小
133
+ const totalSize = sessions.reduce((sum, session) => {
134
+ return sum + (session.size || 0);
135
+ }, 0);
136
+
137
+ res.json({
138
+ sessions,
139
+ totalSize,
140
+ aliases,
141
+ projectInfo: {
142
+ name: projectName,
143
+ fullPath: project?.fullPath || projectName,
144
+ path: project?.path || projectName,
145
+ displayName: project?.displayName || projectName
146
+ }
147
+ });
148
+ } catch (err) {
149
+ console.error('[OpenCode API] Failed to get sessions:', err);
150
+ res.status(500).json({ error: err.message });
151
+ }
152
+ });
153
+
154
+ /**
155
+ * GET /api/opencode/sessions/:projectName/:sessionId/messages
156
+ * 获取会话的消息列表
157
+ */
158
+ router.get('/:projectName/:sessionId/messages', (req, res) => {
159
+ try {
160
+ if (!isOpenCodeInstalled()) {
161
+ return res.status(404).json({ error: 'OpenCode CLI not installed' });
162
+ }
163
+
164
+ const { sessionId } = req.params;
165
+ const { page = 1, limit = 20, order = 'desc' } = req.query;
166
+ const session = getSessionById(sessionId);
167
+ if (!session) {
168
+ return res.status(404).json({ error: 'Session not found' });
169
+ }
170
+ const convertedMessages = getSessionMessages(sessionId);
171
+
172
+ // 分页处理
173
+ const pageNum = parseInt(page);
174
+ const limitNum = parseInt(limit);
175
+
176
+ let messages = convertedMessages;
177
+ if (order === 'desc') {
178
+ messages = [...messages].reverse();
179
+ }
180
+
181
+ const totalMessages = messages.length;
182
+ const start = (pageNum - 1) * limitNum;
183
+ const end = start + limitNum;
184
+ const paginatedMessages = messages.slice(start, end);
185
+
186
+ res.json({
187
+ messages: paginatedMessages,
188
+ metadata: {
189
+ gitBranch: null,
190
+ gitRepository: null,
191
+ cwd: session?.directory || null,
192
+ model: 'opencode'
193
+ },
194
+ pagination: {
195
+ page: pageNum,
196
+ limit: limitNum,
197
+ total: totalMessages,
198
+ hasMore: end < totalMessages
199
+ }
200
+ });
201
+ } catch (err) {
202
+ console.error('[OpenCode API] Failed to get session messages:', err);
203
+ res.status(500).json({ error: err.message });
204
+ }
205
+ });
206
+
207
+ /**
208
+ * DELETE /api/opencode/sessions/:projectName/:sessionId
209
+ * 删除会话
210
+ */
211
+ router.delete('/:projectName/:sessionId', (req, res) => {
212
+ try {
213
+ if (!isOpenCodeInstalled()) {
214
+ return res.status(404).json({ error: 'OpenCode CLI not installed' });
215
+ }
216
+
217
+ const { sessionId } = req.params;
218
+ const result = deleteSession(sessionId);
219
+
220
+ res.json(result);
221
+ } catch (err) {
222
+ if (isNotFoundError(err)) {
223
+ console.warn('[OpenCode API] Delete session target not found:', err.message);
224
+ return res.status(404).json({ error: err.message });
225
+ }
226
+ console.error('[OpenCode API] Failed to delete session:', err);
227
+ res.status(500).json({ error: err.message });
228
+ }
229
+ });
230
+
231
+ /**
232
+ * POST /api/opencode/sessions/:projectName/:sessionId/fork
233
+ * Fork 一个会话
234
+ */
235
+ router.post('/:projectName/:sessionId/fork', (req, res) => {
236
+ try {
237
+ if (!isOpenCodeInstalled()) {
238
+ return res.status(404).json({ error: 'OpenCode CLI not installed' });
239
+ }
240
+
241
+ const { sessionId } = req.params;
242
+ const result = forkSession(sessionId);
243
+ res.json(result);
244
+ } catch (err) {
245
+ if (isNotFoundError(err)) {
246
+ console.warn('[OpenCode API] Fork session target not found:', err.message);
247
+ return res.status(404).json({ error: err.message });
248
+ }
249
+ console.error('[OpenCode API] Failed to fork session:', err);
250
+ res.status(500).json({ error: err.message });
251
+ }
252
+ });
253
+
254
+ /**
255
+ * POST /api/opencode/sessions/:projectName/order
256
+ * 保存会话排序
257
+ */
258
+ router.post('/:projectName/order', (req, res) => {
259
+ try {
260
+ if (!isOpenCodeInstalled()) {
261
+ return res.status(404).json({ error: 'OpenCode CLI not installed' });
262
+ }
263
+
264
+ const { projectName } = req.params;
265
+ const { order } = req.body;
266
+
267
+ if (!Array.isArray(order)) {
268
+ return res.status(400).json({ error: 'order must be an array' });
269
+ }
270
+
271
+ saveSessionOrder(projectName, order);
272
+ res.json({ success: true });
273
+ } catch (err) {
274
+ console.error('[OpenCode API] Failed to save session order:', err);
275
+ res.status(500).json({ error: err.message });
276
+ }
277
+ });
278
+
279
+ /**
280
+ * POST /api/opencode/sessions/:projectName/:sessionId/launch
281
+ * 获取会话启动命令(用于复制)
282
+ */
283
+ router.post('/:projectName/:sessionId/launch', (req, res) => {
284
+ try {
285
+ if (!isOpenCodeInstalled()) {
286
+ return res.status(404).json({ error: 'OpenCode CLI not installed' });
287
+ }
288
+
289
+ const { projectName, sessionId } = req.params;
290
+
291
+ const session = getSessionById(sessionId);
292
+ if (!session) {
293
+ return res.status(404).json({ error: 'Session not found' });
294
+ }
295
+
296
+ const projects = getProjects();
297
+ const project = projects.find(p => p.name === projectName);
298
+ const cwd = session.directory || project?.fullPath || os.homedir();
299
+ const command = `opencode -r ${sessionId}`;
300
+ const quotedCwd = `"${String(cwd).replace(/"/g, '\\"')}"`;
301
+ const copyCommand = `cd ${quotedCwd} && ${command}`;
302
+
303
+ broadcastLog({
304
+ type: 'action',
305
+ action: 'launch_opencode_session',
306
+ message: `复制 OpenCode 会话启动命令 ${sessionId.substring(0, 8)}`,
307
+ sessionId,
308
+ tool: 'opencode',
309
+ timestamp: Date.now()
310
+ });
311
+
312
+ res.json({
313
+ success: true,
314
+ cwd,
315
+ sessionId,
316
+ tool: 'opencode',
317
+ command,
318
+ copyCommand
319
+ });
320
+ } catch (err) {
321
+ console.error('[OpenCode API] Failed to prepare launch command:', err);
322
+ res.status(500).json({ error: err.message });
323
+ }
324
+ });
325
+
326
+ return router;
327
+ };
@@ -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;