@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
@@ -2,6 +2,13 @@ const express = require('express');
2
2
  const router = express.Router();
3
3
  const { loadConfig, saveConfig } = require('../../config/loader');
4
4
  const DEFAULT_CONFIG = require('../../config/default');
5
+ const { getAllChannels } = require('../services/channels');
6
+ const { getChannels: getCodexChannels } = require('../services/codex-channels');
7
+ const { getChannels: getGeminiChannels } = require('../services/gemini-channels');
8
+ const {
9
+ probeModelAvailability,
10
+ fetchModelsFromProvider
11
+ } = require('../services/model-detector');
5
12
 
6
13
  function clampNumber(value, fallback) {
7
14
  const num = typeof value === 'number' ? value : parseFloat(value);
@@ -36,6 +43,354 @@ function sanitizePricing(inputPricing, currentPricing) {
36
43
  return sanitized;
37
44
  }
38
45
 
46
+ function normalizeModelDiscovery(modelDiscovery, currentValue = DEFAULT_CONFIG.modelDiscovery) {
47
+ const current = currentValue && typeof currentValue === 'object'
48
+ ? currentValue
49
+ : DEFAULT_CONFIG.modelDiscovery;
50
+ const input = modelDiscovery && typeof modelDiscovery === 'object'
51
+ ? modelDiscovery
52
+ : {};
53
+
54
+ return {
55
+ useV1ModelsEndpoint: input.useV1ModelsEndpoint !== undefined
56
+ ? input.useV1ModelsEndpoint === true
57
+ : current.useV1ModelsEndpoint === true
58
+ };
59
+ }
60
+
61
+ function uniqueModels(models = []) {
62
+ const seen = new Set();
63
+ const result = [];
64
+
65
+ models.forEach((model) => {
66
+ if (typeof model !== 'string') return;
67
+ const trimmed = model.trim();
68
+ if (!trimmed) return;
69
+ const key = trimmed.toLowerCase();
70
+ if (seen.has(key)) return;
71
+ seen.add(key);
72
+ result.push(trimmed);
73
+ });
74
+
75
+ return result;
76
+ }
77
+
78
+ function collectChannelPreferredModels(channel) {
79
+ const candidates = [];
80
+ if (!channel || typeof channel !== 'object') return candidates;
81
+
82
+ candidates.push(channel.model);
83
+ candidates.push(channel.speedTestModel);
84
+
85
+ const modelConfig = channel.modelConfig;
86
+ if (modelConfig && typeof modelConfig === 'object') {
87
+ candidates.push(modelConfig.model);
88
+ candidates.push(modelConfig.opusModel);
89
+ candidates.push(modelConfig.sonnetModel);
90
+ candidates.push(modelConfig.haikuModel);
91
+ }
92
+
93
+ if (Array.isArray(channel.modelRedirects)) {
94
+ channel.modelRedirects.forEach((rule) => {
95
+ candidates.push(rule?.from);
96
+ candidates.push(rule?.to);
97
+ });
98
+ }
99
+
100
+ return uniqueModels(candidates);
101
+ }
102
+
103
+ function parseBooleanQuery(value, defaultValue = false) {
104
+ if (value === undefined || value === null || value === '') return defaultValue;
105
+ const normalized = String(value).trim().toLowerCase();
106
+ return ['1', 'true', 'yes', 'on'].includes(normalized);
107
+ }
108
+
109
+ async function probeModelsForSingleChannel(channel, channelType, options = {}) {
110
+ const builtInPreferred = Array.isArray(DEFAULT_CONFIG.defaultModels?.[channelType])
111
+ ? DEFAULT_CONFIG.defaultModels[channelType]
112
+ : [];
113
+ const preferredModels = uniqueModels([
114
+ ...collectChannelPreferredModels(channel),
115
+ ...builtInPreferred
116
+ ]);
117
+
118
+ try {
119
+ if (channelType === 'codex') {
120
+ const listResult = await fetchModelsFromProvider(channel, 'openai_compatible');
121
+ const listedModels = Array.isArray(listResult?.models) ? listResult.models : [];
122
+ if (listedModels.length > 0) {
123
+ return uniqueModels(listedModels);
124
+ }
125
+ }
126
+
127
+ const probe = await probeModelAvailability(channel, channelType, {
128
+ forceRefresh: !!options.forceRefresh,
129
+ stopOnFirstAvailable: false,
130
+ preferredModels
131
+ });
132
+ const probedModels = Array.isArray(probe?.availableModels) ? probe.availableModels : [];
133
+ if (probedModels.length > 0) {
134
+ return uniqueModels(probedModels);
135
+ }
136
+ } catch (error) {
137
+ console.warn(`[Config API] Probe failed for channel ${channel?.name || channel?.id || 'unknown'}: ${error.message}`);
138
+ }
139
+
140
+ return preferredModels;
141
+ }
142
+
143
+ async function probeModelsForChannels(channels = [], channelType, options = {}) {
144
+ const enabledChannels = (channels || []).filter(ch => ch && ch.enabled !== false);
145
+ if (enabledChannels.length === 0) return [];
146
+
147
+ const resultSets = [];
148
+ // 模型探测改为串行,避免并发触发上游会话窗口限流
149
+ for (const channel of enabledChannels) {
150
+ // eslint-disable-next-line no-await-in-loop
151
+ const models = await probeModelsForSingleChannel(channel, channelType, options);
152
+ resultSets.push(models);
153
+ }
154
+ return uniqueModels(resultSets.flat());
155
+ }
156
+
157
+ function mergeProbedAndConfiguredModels(probedModels, configuredModels, toolType) {
158
+ const safeConfigured = Array.isArray(configuredModels) ? configuredModels : [];
159
+ const safeProbed = Array.isArray(probedModels) ? probedModels : [];
160
+ const builtInDefaults = Array.isArray(DEFAULT_CONFIG.defaultModels?.[toolType])
161
+ ? DEFAULT_CONFIG.defaultModels[toolType]
162
+ : [];
163
+ if (safeProbed.length > 0) {
164
+ return uniqueModels([...safeProbed, ...safeConfigured, ...builtInDefaults]);
165
+ }
166
+ return uniqueModels([...safeConfigured, ...builtInDefaults]);
167
+ }
168
+
169
+ /**
170
+ * Validate model list
171
+ * @param {Array} models - Array of model names
172
+ * @param {string} toolType - Tool type (claude, codex, gemini)
173
+ * @returns {Object} { valid: boolean, cleaned: array, error?: string }
174
+ */
175
+ function validateModelList(models, toolType) {
176
+ if (!Array.isArray(models)) {
177
+ return { valid: false, error: `${toolType}: models must be an array` };
178
+ }
179
+
180
+ if (models.length === 0) {
181
+ return { valid: false, error: `${toolType}: model list cannot be empty` };
182
+ }
183
+
184
+ if (models.length > 50) {
185
+ return { valid: false, error: `${toolType}: maximum 50 models allowed, got ${models.length}` };
186
+ }
187
+
188
+ const modelNamePattern = /^[a-zA-Z0-9._\-/:]+$/;
189
+ const cleaned = [];
190
+ const seen = new Set();
191
+
192
+ for (let i = 0; i < models.length; i++) {
193
+ const model = models[i];
194
+
195
+ if (typeof model !== 'string') {
196
+ return { valid: false, error: `${toolType}: model at index ${i} must be a string, got ${typeof model}` };
197
+ }
198
+
199
+ const trimmed = model.trim();
200
+
201
+ if (trimmed.length === 0) {
202
+ return { valid: false, error: `${toolType}: model at index ${i} is empty or whitespace` };
203
+ }
204
+
205
+ if (!modelNamePattern.test(trimmed)) {
206
+ return { valid: false, error: `${toolType}: model "${trimmed}" contains invalid characters (allowed: a-z A-Z 0-9 . _ - / :)` };
207
+ }
208
+
209
+ if (!seen.has(trimmed)) {
210
+ seen.add(trimmed);
211
+ cleaned.push(trimmed);
212
+ }
213
+ }
214
+
215
+ return { valid: true, cleaned };
216
+ }
217
+
218
+ /**
219
+ * GET /api/config/default-models
220
+ * 获取默认模型列表
221
+ */
222
+ router.get('/default-models', async (req, res) => {
223
+ try {
224
+ const config = loadConfig();
225
+ const configuredDefaultModels = config.defaultModels || DEFAULT_CONFIG.defaultModels;
226
+ const probe = parseBooleanQuery(req.query.probe, false);
227
+
228
+ if (!probe) {
229
+ return res.json({
230
+ defaultModels: configuredDefaultModels,
231
+ probed: false
232
+ });
233
+ }
234
+
235
+ const forceRefresh = parseBooleanQuery(req.query.forceRefresh, true);
236
+ const claudeChannels = getAllChannels();
237
+ const codexData = getCodexChannels();
238
+ const geminiData = getGeminiChannels();
239
+
240
+ // 各工具类型也按串行探测,进一步降低并发压力
241
+ const claudeProbed = await probeModelsForChannels(claudeChannels || [], 'claude', { forceRefresh });
242
+ const codexProbed = await probeModelsForChannels(codexData?.channels || [], 'codex', { forceRefresh });
243
+ const geminiProbed = await probeModelsForChannels(geminiData?.channels || [], 'gemini', { forceRefresh });
244
+
245
+ const defaultModels = {
246
+ claude: mergeProbedAndConfiguredModels(
247
+ claudeProbed,
248
+ configuredDefaultModels.claude,
249
+ 'claude'
250
+ ),
251
+ codex: mergeProbedAndConfiguredModels(
252
+ codexProbed,
253
+ configuredDefaultModels.codex,
254
+ 'codex'
255
+ ),
256
+ gemini: mergeProbedAndConfiguredModels(
257
+ geminiProbed,
258
+ configuredDefaultModels.gemini,
259
+ 'gemini'
260
+ )
261
+ };
262
+
263
+ res.json({
264
+ defaultModels,
265
+ probed: true,
266
+ forceRefresh
267
+ });
268
+ } catch (error) {
269
+ console.error('[Config API] Failed to get default models:', error);
270
+ res.status(500).json({ error: error.message });
271
+ }
272
+ });
273
+
274
+ /**
275
+ * POST /api/config/default-models
276
+ * 更新默认模型列表
277
+ */
278
+ router.post('/default-models', (req, res) => {
279
+ try {
280
+ const { defaultModels } = req.body;
281
+
282
+ if (!defaultModels || typeof defaultModels !== 'object') {
283
+ return res.status(400).json({
284
+ error: 'defaultModels must be an object'
285
+ });
286
+ }
287
+
288
+ const validToolTypes = ['claude', 'codex', 'gemini'];
289
+ const providedTypes = Object.keys(defaultModels);
290
+
291
+ // Validate that only valid tool types are provided
292
+ for (const toolType of providedTypes) {
293
+ if (!validToolTypes.includes(toolType)) {
294
+ return res.status(400).json({
295
+ error: `Invalid tool type: ${toolType}. Valid types: ${validToolTypes.join(', ')}`
296
+ });
297
+ }
298
+ }
299
+
300
+ // Validate each model list
301
+ const validated = {};
302
+ const errors = {};
303
+
304
+ for (const toolType of providedTypes) {
305
+ const result = validateModelList(defaultModels[toolType], toolType);
306
+ if (!result.valid) {
307
+ errors[toolType] = result.error;
308
+ } else {
309
+ validated[toolType] = result.cleaned;
310
+ }
311
+ }
312
+
313
+ if (Object.keys(errors).length > 0) {
314
+ return res.status(400).json({
315
+ error: 'Validation failed',
316
+ details: errors
317
+ });
318
+ }
319
+
320
+ // Load current config and merge
321
+ const config = loadConfig();
322
+ const newDefaultModels = {
323
+ ...(config.defaultModels || DEFAULT_CONFIG.defaultModels),
324
+ ...validated
325
+ };
326
+
327
+ // Save config
328
+ const newConfig = {
329
+ ...config,
330
+ projectsDir: config.projectsDir.replace(require('os').homedir(), '~'),
331
+ defaultModels: newDefaultModels
332
+ };
333
+
334
+ saveConfig(newConfig);
335
+
336
+ res.json({
337
+ success: true,
338
+ defaultModels: newDefaultModels
339
+ });
340
+ } catch (error) {
341
+ console.error('[Config API] Failed to save default models:', error);
342
+ res.status(500).json({ error: error.message });
343
+ }
344
+ });
345
+
346
+ /**
347
+ * POST /api/config/default-models/reset
348
+ * 重置默认模型列表
349
+ */
350
+ router.post('/default-models/reset', (req, res) => {
351
+ try {
352
+ const { toolType } = req.body;
353
+
354
+ const config = loadConfig();
355
+ let newDefaultModels;
356
+
357
+ if (toolType) {
358
+ // Reset specific tool type
359
+ const validToolTypes = ['claude', 'codex', 'gemini'];
360
+ if (!validToolTypes.includes(toolType)) {
361
+ return res.status(400).json({
362
+ error: `Invalid tool type: ${toolType}. Valid types: ${validToolTypes.join(', ')}`
363
+ });
364
+ }
365
+
366
+ newDefaultModels = {
367
+ ...(config.defaultModels || DEFAULT_CONFIG.defaultModels),
368
+ [toolType]: DEFAULT_CONFIG.defaultModels[toolType]
369
+ };
370
+ } else {
371
+ // Reset all tool types
372
+ newDefaultModels = { ...DEFAULT_CONFIG.defaultModels };
373
+ }
374
+
375
+ // Save config
376
+ const newConfig = {
377
+ ...config,
378
+ projectsDir: config.projectsDir.replace(require('os').homedir(), '~'),
379
+ defaultModels: newDefaultModels
380
+ };
381
+
382
+ saveConfig(newConfig);
383
+
384
+ res.json({
385
+ success: true,
386
+ defaultModels: newDefaultModels
387
+ });
388
+ } catch (error) {
389
+ console.error('[Config API] Failed to reset default models:', error);
390
+ res.status(500).json({ error: error.message });
391
+ }
392
+ });
393
+
39
394
  /**
40
395
  * GET /api/config/advanced
41
396
  * 获取高级配置(端口、日志、性能等)
@@ -43,16 +398,19 @@ function sanitizePricing(inputPricing, currentPricing) {
43
398
  router.get('/advanced', (req, res) => {
44
399
  try {
45
400
  const config = loadConfig();
401
+ const modelDiscovery = normalizeModelDiscovery(config.modelDiscovery);
46
402
  res.json({
47
403
  ports: {
48
- webUI: config.ports?.webUI || 10099,
49
- proxy: config.ports?.proxy || 10088,
50
- codexProxy: config.ports?.codexProxy || 10089,
51
- geminiProxy: config.ports?.geminiProxy || 10090
404
+ webUI: config.ports?.webUI || 19999,
405
+ proxy: config.ports?.proxy || 20088,
406
+ codexProxy: config.ports?.codexProxy || 20089,
407
+ geminiProxy: config.ports?.geminiProxy || 20090,
408
+ opencodeProxy: config.ports?.opencodeProxy || 20091
52
409
  },
53
410
  maxLogs: config.maxLogs || 100,
54
411
  statsInterval: config.statsInterval || 30,
55
412
  enableSessionBinding: config.enableSessionBinding !== false, // 默认开启
413
+ modelDiscovery,
56
414
  pricing: config.pricing || DEFAULT_CONFIG.pricing
57
415
  });
58
416
  } catch (error) {
@@ -67,7 +425,14 @@ router.get('/advanced', (req, res) => {
67
425
  */
68
426
  router.post('/advanced', (req, res) => {
69
427
  try {
70
- const { ports, maxLogs, statsInterval, pricing, enableSessionBinding } = req.body;
428
+ const {
429
+ ports,
430
+ maxLogs,
431
+ statsInterval,
432
+ pricing,
433
+ enableSessionBinding,
434
+ modelDiscovery
435
+ } = req.body;
71
436
 
72
437
  // 验证端口
73
438
  if (ports) {
@@ -104,6 +469,10 @@ router.post('/advanced', (req, res) => {
104
469
  // 加载当前配置
105
470
  const config = loadConfig();
106
471
  const sanitizedPricing = sanitizePricing(pricing, config.pricing);
472
+ const normalizedModelDiscovery = normalizeModelDiscovery(
473
+ modelDiscovery,
474
+ config.modelDiscovery || DEFAULT_CONFIG.modelDiscovery
475
+ );
107
476
 
108
477
  let normalizedPorts = config.ports;
109
478
  if (ports) {
@@ -122,6 +491,7 @@ router.post('/advanced', (req, res) => {
122
491
  maxLogs: maxLogs !== undefined ? parseInt(maxLogs) : config.maxLogs,
123
492
  statsInterval: statsInterval !== undefined ? parseInt(statsInterval) : config.statsInterval,
124
493
  enableSessionBinding: enableSessionBinding !== undefined ? enableSessionBinding : (config.enableSessionBinding !== false),
494
+ modelDiscovery: normalizedModelDiscovery,
125
495
  pricing: sanitizedPricing
126
496
  };
127
497
 
@@ -135,6 +505,7 @@ router.post('/advanced', (req, res) => {
135
505
  maxLogs: newConfig.maxLogs,
136
506
  statsInterval: newConfig.statsInterval,
137
507
  enableSessionBinding: newConfig.enableSessionBinding,
508
+ modelDiscovery: newConfig.modelDiscovery,
138
509
  pricing: newConfig.pricing
139
510
  }
140
511
  });
@@ -1,6 +1,15 @@
1
1
  const express = require('express');
2
2
  const router = express.Router();
3
3
  const { convertSession, previewConversion } = require('../services/session-converter');
4
+ const {
5
+ SUPPORTED_SOURCE_TYPES,
6
+ SUPPORTED_TARGET_APIS,
7
+ convertToOpenCodePayload,
8
+ convertClaudeToOpenCodePayload,
9
+ convertCodexToOpenCodePayload,
10
+ convertGeminiToOpenCodePayload,
11
+ normalizeSourceType
12
+ } = require('../services/opencode-gateway-converter');
4
13
 
5
14
  /**
6
15
  * 获取支持的格式列表
@@ -42,6 +51,130 @@ router.get('/formats', (req, res) => {
42
51
  });
43
52
  });
44
53
 
54
+ /**
55
+ * 获取 OpenCode 网关支持格式
56
+ * GET /api/convert/opencode/formats
57
+ */
58
+ router.get('/opencode/formats', (req, res) => {
59
+ res.json({
60
+ sourceTypes: SUPPORTED_SOURCE_TYPES.map(type => ({
61
+ id: type,
62
+ name: type === 'claude'
63
+ ? 'Claude Code'
64
+ : type === 'codex'
65
+ ? 'Codex'
66
+ : 'Gemini'
67
+ })),
68
+ target: 'opencode',
69
+ targetApis: SUPPORTED_TARGET_APIS,
70
+ defaultTargetApi: 'responses',
71
+ endpoints: {
72
+ responses: '/v1/responses',
73
+ 'chat.completions': '/v1/chat/completions'
74
+ },
75
+ sourceEndpoints: {
76
+ claude: '/api/convert/opencode/claude',
77
+ codex: '/api/convert/opencode/codex',
78
+ gemini: '/api/convert/opencode/gemini'
79
+ }
80
+ });
81
+ });
82
+
83
+ function handleOpenCodeConvert(req, res, sourceType, converter) {
84
+ try {
85
+ const { payload, options = {} } = req.body || {};
86
+
87
+ if (!payload) {
88
+ return res.status(400).json({
89
+ success: false,
90
+ error: 'Missing required parameters: payload'
91
+ });
92
+ }
93
+
94
+ const result = converter({ payload, options });
95
+ return res.json({
96
+ success: true,
97
+ ...result
98
+ });
99
+ } catch (error) {
100
+ console.error(`[Convert API] OpenCode ${sourceType} convert error:`, error);
101
+ return res.status(500).json({
102
+ success: false,
103
+ error: error.message
104
+ });
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Claude Code -> OpenCode
110
+ * POST /api/convert/opencode/claude
111
+ * Body: { payload, options? }
112
+ */
113
+ router.post('/opencode/claude', (req, res) => {
114
+ return handleOpenCodeConvert(req, res, 'claude', convertClaudeToOpenCodePayload);
115
+ });
116
+
117
+ /**
118
+ * Codex -> OpenCode
119
+ * POST /api/convert/opencode/codex
120
+ * Body: { payload, options? }
121
+ */
122
+ router.post('/opencode/codex', (req, res) => {
123
+ return handleOpenCodeConvert(req, res, 'codex', convertCodexToOpenCodePayload);
124
+ });
125
+
126
+ /**
127
+ * Gemini -> OpenCode
128
+ * POST /api/convert/opencode/gemini
129
+ * Body: { payload, options? }
130
+ */
131
+ router.post('/opencode/gemini', (req, res) => {
132
+ return handleOpenCodeConvert(req, res, 'gemini', convertGeminiToOpenCodePayload);
133
+ });
134
+
135
+ /**
136
+ * 在线转换为 OpenCode 可处理格式
137
+ * POST /api/convert/opencode
138
+ * Body: { sourceType, payload, options? }
139
+ */
140
+ router.post('/opencode', (req, res) => {
141
+ try {
142
+ const { sourceType, payload, options = {} } = req.body || {};
143
+
144
+ if (!sourceType || !payload) {
145
+ return res.status(400).json({
146
+ success: false,
147
+ error: 'Missing required parameters: sourceType, payload'
148
+ });
149
+ }
150
+
151
+ const normalized = normalizeSourceType(sourceType);
152
+ if (!SUPPORTED_SOURCE_TYPES.includes(normalized)) {
153
+ return res.status(400).json({
154
+ success: false,
155
+ error: `Invalid sourceType: ${sourceType}. Must be one of: ${SUPPORTED_SOURCE_TYPES.join(', ')}`
156
+ });
157
+ }
158
+
159
+ const result = convertToOpenCodePayload({
160
+ sourceType: normalized,
161
+ payload,
162
+ options
163
+ });
164
+
165
+ return res.json({
166
+ success: true,
167
+ ...result
168
+ });
169
+ } catch (error) {
170
+ console.error('[Convert API] OpenCode gateway convert error:', error);
171
+ return res.status(500).json({
172
+ success: false,
173
+ error: error.message
174
+ });
175
+ }
176
+ });
177
+
45
178
  /**
46
179
  * 预览转换结果
47
180
  * POST /api/convert/preview
@@ -9,18 +9,22 @@ const { getAllChannels } = require('../services/channels');
9
9
  const { getProxyStatus } = require('../proxy-server');
10
10
  const { getCodexProxyStatus } = require('../codex-proxy-server');
11
11
  const { getGeminiProxyStatus } = require('../gemini-proxy-server');
12
+ const { getOpenCodeProxyStatus } = require('../opencode-proxy-server');
12
13
  const { getProjectAndSessionCounts: getClaudeCounts } = require('../services/sessions');
13
14
  const { getProjectAndSessionCounts: getCodexCounts } = require('../services/codex-sessions');
14
15
  const { getProjectAndSessionCounts: getGeminiCounts } = require('../services/gemini-sessions');
16
+ const { getProjectAndSessionCounts: getOpenCodeCounts } = require('../services/opencode-sessions');
15
17
 
16
18
  // Channel-specific services
17
19
  const { getChannels: getCodexChannels } = require('../services/codex-channels');
18
20
  const { getChannels: getGeminiChannels } = require('../services/gemini-channels');
21
+ const { getChannels: getOpenCodeChannels } = require('../services/opencode-channels');
19
22
 
20
23
  // Statistics
21
24
  const { getTodayStatistics } = require('../services/statistics-service');
22
25
  const { getTodayStatistics: getCodexTodayStatistics } = require('../services/codex-statistics-service');
23
26
  const { getTodayStatistics: getGeminiTodayStatistics } = require('../services/gemini-statistics-service');
27
+ const { getTodayStatistics: getOpenCodeTodayStatistics } = require('../services/opencode-statistics-service');
24
28
 
25
29
  /**
26
30
  * GET /api/dashboard/init
@@ -37,15 +41,19 @@ router.get('/init', async (req, res) => {
37
41
  claudeChannels,
38
42
  codexChannels,
39
43
  geminiChannels,
44
+ opencodeChannels,
40
45
  claudeProxyStatus,
41
46
  codexProxyStatus,
42
47
  geminiProxyStatus,
48
+ opencodeProxyStatus,
43
49
  claudeTodayStats,
44
50
  codexTodayStats,
45
51
  geminiTodayStats,
52
+ opencodeTodayStats,
46
53
  claudeCounts,
47
54
  codexCounts,
48
- geminiCounts
55
+ geminiCounts,
56
+ opencodeCounts
49
57
  ] = await Promise.all([
50
58
  // UI Config
51
59
  Promise.resolve(loadUIConfig()),
@@ -57,21 +65,25 @@ router.get('/init', async (req, res) => {
57
65
  Promise.resolve(getAllChannels()),
58
66
  Promise.resolve(getCodexChannels()),
59
67
  Promise.resolve(getGeminiChannels()),
68
+ Promise.resolve(getOpenCodeChannels()),
60
69
 
61
70
  // Proxy Status
62
71
  Promise.resolve(getProxyStatus()),
63
72
  Promise.resolve(getCodexProxyStatus()),
64
73
  Promise.resolve(getGeminiProxyStatus()),
74
+ Promise.resolve(getOpenCodeProxyStatus()),
65
75
 
66
76
  // Today Stats (所有平台)
67
77
  Promise.resolve(getTodayStatistics()),
68
78
  Promise.resolve(getCodexTodayStatistics()),
69
79
  Promise.resolve(getGeminiTodayStatistics()),
80
+ Promise.resolve(getOpenCodeTodayStatistics()),
70
81
 
71
82
  // 轻量级统计
72
83
  Promise.resolve(getClaudeCounts(config)),
73
84
  Promise.resolve(getCodexCounts()),
74
- Promise.resolve(getGeminiCounts())
85
+ Promise.resolve(getGeminiCounts()),
86
+ Promise.resolve(getOpenCodeCounts())
75
87
  ]);
76
88
 
77
89
  // 格式化统计数据:取 summary 和 byModel 中的数据
@@ -95,22 +107,26 @@ router.get('/init', async (req, res) => {
95
107
  channels: {
96
108
  claude: claudeChannels,
97
109
  codex: codexChannels,
98
- gemini: geminiChannels
110
+ gemini: geminiChannels,
111
+ opencode: opencodeChannels.channels || []
99
112
  },
100
113
  proxyStatus: {
101
114
  claude: claudeProxyStatus,
102
115
  codex: codexProxyStatus,
103
- gemini: geminiProxyStatus
116
+ gemini: geminiProxyStatus,
117
+ opencode: opencodeProxyStatus
104
118
  },
105
119
  counts: {
106
120
  claude: claudeCounts || { projectCount: 0, sessionCount: 0 },
107
121
  codex: codexCounts || { projectCount: 0, sessionCount: 0 },
108
- gemini: geminiCounts || { projectCount: 0, sessionCount: 0 }
122
+ gemini: geminiCounts || { projectCount: 0, sessionCount: 0 },
123
+ opencode: opencodeCounts || { projectCount: 0, sessionCount: 0 }
109
124
  },
110
125
  todayStats: {
111
126
  claude: formatStats(claudeTodayStats),
112
127
  codex: formatStats(codexTodayStats),
113
- gemini: formatStats(geminiTodayStats)
128
+ gemini: formatStats(geminiTodayStats),
129
+ opencode: formatStats(opencodeTodayStats)
114
130
  }
115
131
  }
116
132
  });