@adversity/coding-tool-x 3.0.6 → 3.1.1

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 (133) hide show
  1. package/CHANGELOG.md +38 -18
  2. package/README.md +8 -8
  3. package/dist/web/assets/ConfigTemplates-Bidwfdf2.css +1 -0
  4. package/dist/web/assets/ConfigTemplates-ZrK_s7ma.js +1 -0
  5. package/dist/web/assets/Home-B8YfhZ3c.js +1 -0
  6. package/dist/web/assets/Home-Di2qsylF.css +1 -0
  7. package/dist/web/assets/PluginManager-BD7QUZbU.js +1 -0
  8. package/dist/web/assets/PluginManager-ROyoZ-6m.css +1 -0
  9. package/dist/web/assets/ProjectList-C1fQb9OW.css +1 -0
  10. package/dist/web/assets/ProjectList-DRb1DuHV.js +1 -0
  11. package/dist/web/assets/SessionList-BGJWyneI.css +1 -0
  12. package/dist/web/assets/SessionList-lZ0LKzfT.js +1 -0
  13. package/dist/web/assets/SkillManager-C1xG5B4Q.js +1 -0
  14. package/dist/web/assets/SkillManager-D7pd-d_P.css +1 -0
  15. package/dist/web/assets/Terminal-DGNJeVtc.css +1 -0
  16. package/dist/web/assets/Terminal-DksBo_lM.js +1 -0
  17. package/dist/web/assets/WorkspaceManager-Burx7XOo.js +1 -0
  18. package/dist/web/assets/WorkspaceManager-CrwgQgmP.css +1 -0
  19. package/dist/web/assets/icons-kcfLIMBB.js +1 -0
  20. package/dist/web/assets/index-Ufv5rCa5.css +1 -0
  21. package/dist/web/assets/index-lAkrRC3h.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 +92 -13
  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/ui.js +8 -1
  45. package/src/commands/update.js +97 -0
  46. package/src/commands/workspace.js +1 -1
  47. package/src/config/default.js +39 -2
  48. package/src/config/loader.js +74 -8
  49. package/src/config/paths.js +105 -33
  50. package/src/index.js +67 -4
  51. package/src/plugins/constants.js +3 -2
  52. package/src/plugins/plugin-api.js +1 -1
  53. package/src/reset-config.js +4 -2
  54. package/src/server/api/agents.js +57 -14
  55. package/src/server/api/channels.js +112 -33
  56. package/src/server/api/codex-channels.js +111 -18
  57. package/src/server/api/codex-proxy.js +14 -8
  58. package/src/server/api/commands.js +71 -18
  59. package/src/server/api/config-export.js +0 -6
  60. package/src/server/api/config-registry.js +11 -3
  61. package/src/server/api/config.js +376 -5
  62. package/src/server/api/convert.js +133 -0
  63. package/src/server/api/dashboard.js +22 -6
  64. package/src/server/api/gemini-channels.js +107 -18
  65. package/src/server/api/gemini-proxy.js +14 -8
  66. package/src/server/api/gemini-sessions.js +1 -1
  67. package/src/server/api/health-check.js +4 -3
  68. package/src/server/api/mcp.js +3 -3
  69. package/src/server/api/opencode-channels.js +419 -0
  70. package/src/server/api/opencode-projects.js +99 -0
  71. package/src/server/api/opencode-proxy.js +198 -0
  72. package/src/server/api/opencode-sessions.js +403 -0
  73. package/src/server/api/opencode-statistics.js +57 -0
  74. package/src/server/api/plugins.js +66 -19
  75. package/src/server/api/prompts.js +2 -2
  76. package/src/server/api/proxy.js +7 -4
  77. package/src/server/api/sessions.js +3 -0
  78. package/src/server/api/skills.js +69 -18
  79. package/src/server/api/workspaces.js +78 -6
  80. package/src/server/codex-proxy-server.js +32 -19
  81. package/src/server/dev-server.js +1 -1
  82. package/src/server/gemini-proxy-server.js +17 -3
  83. package/src/server/index.js +164 -48
  84. package/src/server/opencode-proxy-server.js +4375 -0
  85. package/src/server/proxy-server.js +30 -19
  86. package/src/server/services/agents-service.js +61 -24
  87. package/src/server/services/channel-scheduler.js +9 -5
  88. package/src/server/services/channels.js +70 -12
  89. package/src/server/services/codex-channels.js +61 -23
  90. package/src/server/services/codex-settings-manager.js +271 -49
  91. package/src/server/services/codex-statistics-service.js +2 -2
  92. package/src/server/services/commands-service.js +84 -25
  93. package/src/server/services/config-export-service.js +7 -45
  94. package/src/server/services/config-registry-service.js +63 -17
  95. package/src/server/services/config-sync-manager.js +160 -7
  96. package/src/server/services/config-templates-service.js +204 -51
  97. package/src/server/services/env-checker.js +26 -12
  98. package/src/server/services/env-manager.js +126 -18
  99. package/src/server/services/favorites.js +5 -3
  100. package/src/server/services/gemini-channels.js +37 -15
  101. package/src/server/services/gemini-statistics-service.js +2 -2
  102. package/src/server/services/mcp-service.js +350 -9
  103. package/src/server/services/model-detector.js +707 -221
  104. package/src/server/services/network-access.js +80 -0
  105. package/src/server/services/opencode-channels.js +206 -0
  106. package/src/server/services/opencode-gateway-converter.js +639 -0
  107. package/src/server/services/opencode-sessions.js +663 -0
  108. package/src/server/services/opencode-settings-manager.js +342 -0
  109. package/src/server/services/opencode-statistics-service.js +255 -0
  110. package/src/server/services/plugins-service.js +479 -22
  111. package/src/server/services/prompts-service.js +53 -11
  112. package/src/server/services/proxy-runtime.js +1 -1
  113. package/src/server/services/repo-scanner-base.js +1 -1
  114. package/src/server/services/security-config.js +1 -1
  115. package/src/server/services/session-cache.js +1 -1
  116. package/src/server/services/skill-service.js +300 -46
  117. package/src/server/services/speed-test.js +464 -186
  118. package/src/server/services/statistics-service.js +2 -2
  119. package/src/server/services/terminal-commands.js +10 -3
  120. package/src/server/services/terminal-config.js +1 -1
  121. package/src/server/services/ui-config.js +1 -1
  122. package/src/server/services/workspace-service.js +57 -100
  123. package/src/server/websocket-server.js +132 -3
  124. package/src/ui/menu.js +49 -40
  125. package/src/utils/port-helper.js +22 -8
  126. package/src/utils/session.js +5 -4
  127. package/dist/web/assets/icons-BxudHPiX.js +0 -1
  128. package/dist/web/assets/index-D2VfwJBa.js +0 -14
  129. package/dist/web/assets/index-oXBzu0bd.css +0 -41
  130. package/dist/web/assets/naive-ui-DT-Uur8K.js +0 -1
  131. package/dist/web/assets/vue-vendor-6JaYHOiI.js +0 -44
  132. package/src/server/api/permissions.js +0 -385
  133. package/src/server/services/permission-templates-service.js +0 -308
@@ -0,0 +1,419 @@
1
+ const express = require('express');
2
+ const router = express.Router();
3
+ const {
4
+ getChannels,
5
+ createChannel,
6
+ updateChannel,
7
+ deleteChannel,
8
+ saveChannelOrder
9
+ } = require('../services/opencode-channels');
10
+ const { isOpenCodeInstalled } = require('../services/opencode-sessions');
11
+ const { getSchedulerState } = require('../services/channel-scheduler');
12
+ const { getChannelHealthStatus, resetChannelHealth } = require('../services/channel-health');
13
+ const { broadcastSchedulerState } = require('../websocket-server');
14
+ const {
15
+ testChannelSpeed,
16
+ sanitizeBatchConcurrency,
17
+ runWithConcurrencyLimit
18
+ } = require('../services/speed-test');
19
+ const { clearOpenCodeRedirectCache } = require('../opencode-proxy-server');
20
+ const {
21
+ fetchModelsFromProvider,
22
+ probeModelAvailability
23
+ } = require('../services/model-detector');
24
+
25
+ module.exports = (config) => {
26
+ function uniqueModels(models = []) {
27
+ const seen = new Set();
28
+ const result = [];
29
+ models.forEach((model) => {
30
+ if (typeof model !== 'string') return;
31
+ const trimmed = model.trim();
32
+ if (!trimmed) return;
33
+ const key = trimmed.toLowerCase();
34
+ if (seen.has(key)) return;
35
+ seen.add(key);
36
+ result.push(trimmed);
37
+ });
38
+ return result;
39
+ }
40
+
41
+ function collectChannelPreferredModels(channel) {
42
+ const candidates = [];
43
+ if (!channel || typeof channel !== 'object') return candidates;
44
+
45
+ candidates.push(channel.model);
46
+ candidates.push(channel.speedTestModel);
47
+
48
+ const modelConfig = channel.modelConfig;
49
+ if (modelConfig && typeof modelConfig === 'object') {
50
+ candidates.push(modelConfig.model);
51
+ candidates.push(modelConfig.opusModel);
52
+ candidates.push(modelConfig.sonnetModel);
53
+ candidates.push(modelConfig.haikuModel);
54
+ }
55
+
56
+ if (Array.isArray(channel.modelRedirects)) {
57
+ channel.modelRedirects.forEach((rule) => {
58
+ candidates.push(rule?.from);
59
+ candidates.push(rule?.to);
60
+ });
61
+ }
62
+
63
+ return uniqueModels(candidates);
64
+ }
65
+
66
+ function resolveGatewaySourceType(channel) {
67
+ const value = String(channel?.gatewaySourceType || '').trim().toLowerCase();
68
+ if (value === 'claude') return 'claude';
69
+ if (value === 'gemini') return 'gemini';
70
+ return 'codex';
71
+ }
72
+
73
+ function mapGatewaySourceTypeToSpeedTestType(channel) {
74
+ return resolveGatewaySourceType(channel);
75
+ }
76
+
77
+ function isConverterPresetChannel(channel) {
78
+ const presetId = String(channel?.presetId || '').trim().toLowerCase();
79
+ return presetId === 'entry_claude' || presetId === 'entry_codex' || presetId === 'entry_gemini';
80
+ }
81
+
82
+ /**
83
+ * GET /api/opencode/channels
84
+ * 获取所有 OpenCode 渠道
85
+ */
86
+ router.get('/', (req, res) => {
87
+ try {
88
+ if (!isOpenCodeInstalled()) {
89
+ return res.json({
90
+ channels: [],
91
+ installed: false,
92
+ error: 'OpenCode CLI not installed'
93
+ });
94
+ }
95
+ const data = getChannels();
96
+ const channelsWithHealth = (data.channels || []).map(ch => ({
97
+ ...ch,
98
+ health: getChannelHealthStatus(ch.id, 'opencode')
99
+ }));
100
+ res.json({ channels: channelsWithHealth, installed: true });
101
+ } catch (err) {
102
+ console.error('[OpenCode Channels API] Failed to get channels:', err);
103
+ res.status(500).json({ error: err.message });
104
+ }
105
+ });
106
+
107
+ /**
108
+ * GET /api/opencode/channels/enabled
109
+ * 获取所有已启用的 OpenCode 渠道
110
+ */
111
+ router.get('/enabled', (req, res) => {
112
+ try {
113
+ if (!isOpenCodeInstalled()) {
114
+ return res.json({
115
+ channels: [],
116
+ installed: false,
117
+ error: 'OpenCode CLI not installed'
118
+ });
119
+ }
120
+ const data = getChannels();
121
+ const enabledChannels = (data.channels || []).filter(ch => ch.enabled !== false);
122
+ const channelsWithHealth = enabledChannels.map(ch => ({
123
+ ...ch,
124
+ health: getChannelHealthStatus(ch.id, 'opencode')
125
+ }));
126
+ res.json({ channels: channelsWithHealth, installed: true });
127
+ } catch (err) {
128
+ console.error('[OpenCode Channels API] Failed to get enabled channels:', err);
129
+ res.status(500).json({ error: err.message });
130
+ }
131
+ });
132
+
133
+ /**
134
+ * GET /api/opencode/channels/:channelId/models
135
+ * 获取渠道可用模型列表
136
+ */
137
+ router.get('/:channelId/models', async (req, res) => {
138
+ try {
139
+ const { channelId } = req.params;
140
+ const channels = getChannels().channels || [];
141
+ const channel = channels.find(ch => ch.id === channelId);
142
+
143
+ if (!channel) {
144
+ return res.status(404).json({ error: 'Channel not found' });
145
+ }
146
+
147
+ const gatewaySourceType = resolveGatewaySourceType(channel);
148
+ const preferredModels = collectChannelPreferredModels(channel);
149
+ const listResult = await fetchModelsFromProvider(channel, 'openai_compatible');
150
+ const listedModels = Array.isArray(listResult.models) ? uniqueModels(listResult.models) : [];
151
+ const shouldProbeByDefault = !!listResult.disabledByConfig;
152
+ let result;
153
+
154
+ if (listedModels.length > 0) {
155
+ result = {
156
+ models: listedModels,
157
+ supported: true,
158
+ cached: !!listResult.cached,
159
+ fallbackUsed: false,
160
+ lastChecked: listResult.lastChecked || new Date().toISOString(),
161
+ error: null,
162
+ errorHint: null
163
+ };
164
+ } else if (shouldProbeByDefault || isConverterPresetChannel(channel)) {
165
+ const probe = await probeModelAvailability(channel, gatewaySourceType, {
166
+ stopOnFirstAvailable: false,
167
+ preferredModels
168
+ });
169
+ const probedModels = Array.isArray(probe.availableModels) ? uniqueModels(probe.availableModels) : [];
170
+
171
+ result = {
172
+ models: probedModels,
173
+ supported: probedModels.length > 0,
174
+ cached: !!probe.cached || !!listResult.cached,
175
+ fallbackUsed: false,
176
+ lastChecked: probe.lastChecked || listResult.lastChecked || new Date().toISOString(),
177
+ error: probedModels.length > 0 ? null : (listResult.error || '无法获取可用模型'),
178
+ errorHint: probedModels.length > 0
179
+ ? (shouldProbeByDefault ? '已按设置跳过 /v1/models,使用默认模型探测结果' : '模型列表接口不可用,已自动切换为模型探测结果')
180
+ : (listResult.errorHint || (shouldProbeByDefault
181
+ ? '已按设置跳过 /v1/models,且默认模型探测无可用结果'
182
+ : '模型列表接口不可用且模型探测无可用结果'))
183
+ };
184
+ } else {
185
+ // 非入口转换器渠道:只请求 /v1/models,失败则返回空列表
186
+ result = {
187
+ models: [],
188
+ supported: false,
189
+ cached: !!listResult.cached,
190
+ fallbackUsed: false,
191
+ lastChecked: listResult.lastChecked || new Date().toISOString(),
192
+ error: listResult.error || '该渠道未返回可用模型列表',
193
+ errorHint: listResult.errorHint || '此类型渠道不执行模型探测,请检查 /v1/models 接口'
194
+ };
195
+ }
196
+
197
+ res.json({
198
+ channelId: channelId,
199
+ gatewaySourceType,
200
+ models: result.models,
201
+ supported: result.supported,
202
+ cached: result.cached,
203
+ fallbackUsed: result.fallbackUsed,
204
+ fetchedAt: result.lastChecked || new Date().toISOString(),
205
+ error: result.error,
206
+ errorHint: result.errorHint
207
+ });
208
+ } catch (error) {
209
+ console.error('[OpenCode Channels API] Error fetching models:', error);
210
+ res.status(500).json({
211
+ error: 'Failed to fetch model list',
212
+ channelId: req.params.channelId
213
+ });
214
+ }
215
+ });
216
+
217
+ /**
218
+ * POST /api/opencode/channels
219
+ * 创建新渠道
220
+ */
221
+ router.post('/', (req, res) => {
222
+ try {
223
+ const {
224
+ name,
225
+ baseUrl,
226
+ apiKey,
227
+ wireApi,
228
+ enabled,
229
+ weight,
230
+ maxConcurrency,
231
+ model,
232
+ gatewaySourceType,
233
+ modelRedirects,
234
+ speedTestModel,
235
+ presetId,
236
+ websiteUrl
237
+ } = req.body;
238
+
239
+ if (!name || !baseUrl) {
240
+ return res.status(400).json({ error: 'Missing required fields: name and baseUrl' });
241
+ }
242
+
243
+ if (!apiKey) {
244
+ return res.status(400).json({ error: 'API Key is required' });
245
+ }
246
+
247
+ const channel = createChannel(name, baseUrl, apiKey, {
248
+ wireApi: wireApi || 'openai',
249
+ enabled,
250
+ weight,
251
+ maxConcurrency,
252
+ model,
253
+ gatewaySourceType,
254
+ modelRedirects: modelRedirects || [],
255
+ speedTestModel: speedTestModel || null,
256
+ presetId,
257
+ websiteUrl
258
+ });
259
+
260
+ res.json(channel);
261
+ broadcastSchedulerState('opencode', getSchedulerState('opencode'));
262
+ } catch (err) {
263
+ console.error('[OpenCode Channels API] Failed to create channel:', err);
264
+ res.status(500).json({ error: err.message });
265
+ }
266
+ });
267
+
268
+ /**
269
+ * PUT /api/opencode/channels/:channelId
270
+ * 更新渠道
271
+ */
272
+ router.put('/:channelId', (req, res) => {
273
+ try {
274
+ const { channelId } = req.params;
275
+ const updates = req.body;
276
+
277
+ const channel = updateChannel(channelId, updates);
278
+ clearOpenCodeRedirectCache(channelId);
279
+ res.json(channel);
280
+ broadcastSchedulerState('opencode', getSchedulerState('opencode'));
281
+ } catch (err) {
282
+ console.error('[OpenCode Channels API] Failed to update channel:', err);
283
+ res.status(500).json({ error: err.message });
284
+ }
285
+ });
286
+
287
+ /**
288
+ * DELETE /api/opencode/channels/:channelId
289
+ * 删除渠道
290
+ */
291
+ router.delete('/:channelId', async (req, res) => {
292
+ try {
293
+ const { channelId } = req.params;
294
+ const result = await deleteChannel(channelId);
295
+ res.json(result);
296
+ broadcastSchedulerState('opencode', getSchedulerState('opencode'));
297
+ } catch (err) {
298
+ console.error('[OpenCode Channels API] Failed to delete channel:', err);
299
+ res.status(500).json({ error: err.message });
300
+ }
301
+ });
302
+
303
+ /**
304
+ * POST /api/opencode/channels/order
305
+ * 保存渠道顺序
306
+ */
307
+ router.post('/order', (req, res) => {
308
+ try {
309
+ const { order } = req.body;
310
+ if (!Array.isArray(order)) {
311
+ return res.status(400).json({ error: 'Order must be an array' });
312
+ }
313
+ saveChannelOrder(order);
314
+ res.json({ success: true });
315
+ broadcastSchedulerState('opencode', getSchedulerState('opencode'));
316
+ } catch (err) {
317
+ console.error('[OpenCode Channels API] Failed to save order:', err);
318
+ res.status(500).json({ error: err.message });
319
+ }
320
+ });
321
+
322
+ /**
323
+ * POST /api/opencode/channels/:channelId/reset-health
324
+ * 重置渠道健康状态
325
+ */
326
+ router.post('/:channelId/reset-health', (req, res) => {
327
+ try {
328
+ const { channelId } = req.params;
329
+ resetChannelHealth(channelId, 'opencode');
330
+ res.json({ success: true });
331
+ broadcastSchedulerState('opencode', getSchedulerState('opencode'));
332
+ } catch (err) {
333
+ console.error('[OpenCode Channels API] Failed to reset health:', err);
334
+ res.status(500).json({ error: err.message });
335
+ }
336
+ });
337
+
338
+ /**
339
+ * POST /api/opencode/channels/:channelId/speed-test
340
+ * 测试渠道速度
341
+ */
342
+ router.post('/:channelId/speed-test', async (req, res) => {
343
+ try {
344
+ const { channelId } = req.params;
345
+ const { timeout = 20000 } = req.body;
346
+
347
+ const channels = getChannels().channels || [];
348
+ const channel = channels.find(ch => ch.id === channelId);
349
+
350
+ if (!channel) {
351
+ return res.status(404).json({ error: 'Channel not found' });
352
+ }
353
+
354
+ const speedTestType = mapGatewaySourceTypeToSpeedTestType(channel);
355
+ const result = await testChannelSpeed(channel, timeout, speedTestType);
356
+ res.json(result);
357
+ } catch (error) {
358
+ console.error('[OpenCode Channels API] Speed test failed:', error);
359
+ res.status(500).json({ error: error.message });
360
+ }
361
+ });
362
+
363
+ /**
364
+ * POST /api/opencode/channels/speed-test-all
365
+ * 测试所有渠道速度
366
+ */
367
+ router.post('/speed-test-all', async (req, res) => {
368
+ try {
369
+ const { timeout = 20000, concurrency } = req.body || {};
370
+ const channels = getChannels().channels || [];
371
+ const safeConcurrency = sanitizeBatchConcurrency(concurrency);
372
+
373
+ const results = await runWithConcurrencyLimit(
374
+ channels,
375
+ safeConcurrency,
376
+ channel => {
377
+ const speedTestType = mapGatewaySourceTypeToSpeedTestType(channel);
378
+ return testChannelSpeed(channel, timeout, speedTestType);
379
+ }
380
+ );
381
+
382
+ // 与 testMultipleChannels 保持一致的排序:成功在前,成功按延迟升序
383
+ results.sort((a, b) => {
384
+ if (a.success && !b.success) return -1;
385
+ if (!a.success && b.success) return 1;
386
+ if (a.success && b.success) {
387
+ const aLatency = (a.latency === null || a.latency === undefined) ? Infinity : a.latency;
388
+ const bLatency = (b.latency === null || b.latency === undefined) ? Infinity : b.latency;
389
+ return aLatency - bLatency;
390
+ }
391
+ return 0;
392
+ });
393
+
394
+ // 添加摘要统计
395
+ const successResults = results.filter(r => r.success);
396
+ const successWithLatency = successResults.filter(
397
+ r => r.latency !== null && r.latency !== undefined
398
+ );
399
+ const summary = {
400
+ total: results.length,
401
+ success: successResults.length,
402
+ failed: results.length - successResults.length,
403
+ avgLatency: successWithLatency.length > 0
404
+ ? Math.round(
405
+ successWithLatency.reduce((sum, r) => sum + r.latency, 0) / successWithLatency.length
406
+ )
407
+ : null,
408
+ concurrency: safeConcurrency
409
+ };
410
+
411
+ res.json({ results, summary });
412
+ } catch (error) {
413
+ console.error('[OpenCode Channels API] Speed test all failed:', error);
414
+ res.status(500).json({ error: error.message });
415
+ }
416
+ });
417
+
418
+ return router;
419
+ };
@@ -0,0 +1,99 @@
1
+ const express = require('express');
2
+ const router = express.Router();
3
+ const {
4
+ getProjects,
5
+ saveProjectOrder,
6
+ deleteProject,
7
+ isOpenCodeInstalled
8
+ } = require('../services/opencode-sessions');
9
+
10
+ function isNotFoundError(error) {
11
+ return !!(error && error.message === 'Project not found');
12
+ }
13
+
14
+ module.exports = (config) => {
15
+ /**
16
+ * GET /api/opencode/projects
17
+ * 获取所有 OpenCode 项目列表
18
+ */
19
+ router.get('/', (req, res) => {
20
+ try {
21
+ if (!isOpenCodeInstalled()) {
22
+ return res.json({
23
+ projects: [],
24
+ currentProject: null,
25
+ error: 'OpenCode CLI not installed or not found'
26
+ });
27
+ }
28
+
29
+ const projects = getProjects();
30
+
31
+ res.json({
32
+ projects,
33
+ currentProject: projects[0] ? projects[0].name : null
34
+ });
35
+ } catch (err) {
36
+ console.error('[OpenCode API] Failed to get projects:', err);
37
+
38
+ if (err.code === 'ENOENT') {
39
+ return res.status(404).json({
40
+ error: 'OpenCode data directory not found',
41
+ projects: []
42
+ });
43
+ }
44
+
45
+ res.status(500).json({
46
+ error: err.message,
47
+ projects: []
48
+ });
49
+ }
50
+ });
51
+
52
+ /**
53
+ * POST /api/opencode/projects/order
54
+ * 保存项目排序
55
+ */
56
+ router.post('/order', (req, res) => {
57
+ try {
58
+ if (!isOpenCodeInstalled()) {
59
+ return res.status(404).json({ error: 'OpenCode CLI not installed' });
60
+ }
61
+
62
+ const { order } = req.body;
63
+ if (!Array.isArray(order)) {
64
+ return res.status(400).json({ error: 'order must be an array' });
65
+ }
66
+
67
+ saveProjectOrder(order);
68
+ res.json({ success: true });
69
+ } catch (err) {
70
+ console.error('[OpenCode API] Failed to save project order:', err);
71
+ res.status(500).json({ error: err.message });
72
+ }
73
+ });
74
+
75
+ /**
76
+ * DELETE /api/opencode/projects/:projectName
77
+ * 删除项目(删除项目下所有会话)
78
+ */
79
+ router.delete('/:projectName', (req, res) => {
80
+ try {
81
+ if (!isOpenCodeInstalled()) {
82
+ return res.status(404).json({ error: 'OpenCode CLI not installed' });
83
+ }
84
+
85
+ const { projectName } = req.params;
86
+ const result = deleteProject(projectName);
87
+ res.json(result);
88
+ } catch (err) {
89
+ if (isNotFoundError(err)) {
90
+ console.warn('[OpenCode API] Delete project target not found:', err.message);
91
+ return res.status(404).json({ error: err.message });
92
+ }
93
+ console.error('[OpenCode API] Failed to delete project:', err);
94
+ res.status(500).json({ error: err.message });
95
+ }
96
+ });
97
+
98
+ return router;
99
+ };