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,521 @@
1
+ const express = require('express');
2
+ const router = express.Router();
3
+ const { loadConfig, saveConfig } = require('../../config/loader');
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');
12
+
13
+ function clampNumber(value, fallback) {
14
+ const num = typeof value === 'number' ? value : parseFloat(value);
15
+ if (!Number.isFinite(num)) {
16
+ return fallback;
17
+ }
18
+ if (num < 0) return 0;
19
+ if (num > 1000) return 1000;
20
+ return Math.round(num * 1000000) / 1000000;
21
+ }
22
+
23
+ function sanitizePricing(inputPricing, currentPricing) {
24
+ const defaults = DEFAULT_CONFIG.pricing;
25
+ const sanitized = {};
26
+
27
+ Object.keys(defaults).forEach((toolKey) => {
28
+ const defaultValue = defaults[toolKey];
29
+ const existingValue = currentPricing?.[toolKey] || {};
30
+ const payload = inputPricing?.[toolKey] || {};
31
+
32
+ const mode = payload.mode === 'custom' ? 'custom' : (existingValue.mode || defaultValue.mode || 'auto');
33
+ sanitized[toolKey] = { mode };
34
+
35
+ Object.keys(defaultValue)
36
+ .filter((key) => key !== 'mode')
37
+ .forEach((rateKey) => {
38
+ const fallback = existingValue[rateKey] !== undefined ? existingValue[rateKey] : defaultValue[rateKey];
39
+ sanitized[toolKey][rateKey] = clampNumber(payload[rateKey], fallback);
40
+ });
41
+ });
42
+
43
+ return sanitized;
44
+ }
45
+
46
+ function normalizeModelDiscovery(modelDiscovery, currentValue = DEFAULT_CONFIG.modelDiscovery) {
47
+ const defaultModelDiscovery = DEFAULT_CONFIG.modelDiscovery && typeof DEFAULT_CONFIG.modelDiscovery === 'object'
48
+ ? DEFAULT_CONFIG.modelDiscovery
49
+ : { useV1ModelsEndpoint: false };
50
+ const current = currentValue && typeof currentValue === 'object'
51
+ ? currentValue
52
+ : defaultModelDiscovery;
53
+ const input = modelDiscovery && typeof modelDiscovery === 'object'
54
+ ? modelDiscovery
55
+ : {};
56
+
57
+ return {
58
+ useV1ModelsEndpoint: input.useV1ModelsEndpoint !== undefined
59
+ ? input.useV1ModelsEndpoint === true
60
+ : current.useV1ModelsEndpoint === true
61
+ };
62
+ }
63
+
64
+ function uniqueModels(models = []) {
65
+ const seen = new Set();
66
+ const result = [];
67
+
68
+ models.forEach((model) => {
69
+ if (typeof model !== 'string') return;
70
+ const trimmed = model.trim();
71
+ if (!trimmed) return;
72
+ const key = trimmed.toLowerCase();
73
+ if (seen.has(key)) return;
74
+ seen.add(key);
75
+ result.push(trimmed);
76
+ });
77
+
78
+ return result;
79
+ }
80
+
81
+ function collectChannelPreferredModels(channel) {
82
+ const candidates = [];
83
+ if (!channel || typeof channel !== 'object') return candidates;
84
+
85
+ candidates.push(channel.model);
86
+ candidates.push(channel.speedTestModel);
87
+
88
+ const modelConfig = channel.modelConfig;
89
+ if (modelConfig && typeof modelConfig === 'object') {
90
+ candidates.push(modelConfig.model);
91
+ candidates.push(modelConfig.opusModel);
92
+ candidates.push(modelConfig.sonnetModel);
93
+ candidates.push(modelConfig.haikuModel);
94
+ }
95
+
96
+ if (Array.isArray(channel.modelRedirects)) {
97
+ channel.modelRedirects.forEach((rule) => {
98
+ candidates.push(rule?.from);
99
+ candidates.push(rule?.to);
100
+ });
101
+ }
102
+
103
+ return uniqueModels(candidates);
104
+ }
105
+
106
+ function parseBooleanQuery(value, defaultValue = false) {
107
+ if (value === undefined || value === null || value === '') return defaultValue;
108
+ const normalized = String(value).trim().toLowerCase();
109
+ return ['1', 'true', 'yes', 'on'].includes(normalized);
110
+ }
111
+
112
+ async function probeModelsForSingleChannel(channel, channelType, options = {}) {
113
+ const builtInPreferred = Array.isArray(DEFAULT_CONFIG.defaultModels?.[channelType])
114
+ ? DEFAULT_CONFIG.defaultModels[channelType]
115
+ : [];
116
+ const preferredModels = uniqueModels([
117
+ ...collectChannelPreferredModels(channel),
118
+ ...builtInPreferred
119
+ ]);
120
+
121
+ try {
122
+ if (channelType === 'codex') {
123
+ const listResult = await fetchModelsFromProvider(channel, 'openai_compatible');
124
+ const listedModels = Array.isArray(listResult?.models) ? listResult.models : [];
125
+ if (listedModels.length > 0) {
126
+ return uniqueModels(listedModels);
127
+ }
128
+ }
129
+
130
+ const probe = await probeModelAvailability(channel, channelType, {
131
+ forceRefresh: !!options.forceRefresh,
132
+ stopOnFirstAvailable: false,
133
+ preferredModels
134
+ });
135
+ const probedModels = Array.isArray(probe?.availableModels) ? probe.availableModels : [];
136
+ if (probedModels.length > 0) {
137
+ return uniqueModels(probedModels);
138
+ }
139
+ } catch (error) {
140
+ console.warn(`[Config API] Probe failed for channel ${channel?.name || channel?.id || 'unknown'}: ${error.message}`);
141
+ }
142
+
143
+ return preferredModels;
144
+ }
145
+
146
+ async function probeModelsForChannels(channels = [], channelType, options = {}) {
147
+ const enabledChannels = (channels || []).filter(ch => ch && ch.enabled !== false);
148
+ if (enabledChannels.length === 0) return [];
149
+
150
+ const resultSets = [];
151
+ // 模型探测改为串行,避免并发触发上游会话窗口限流
152
+ for (const channel of enabledChannels) {
153
+ // eslint-disable-next-line no-await-in-loop
154
+ const models = await probeModelsForSingleChannel(channel, channelType, options);
155
+ resultSets.push(models);
156
+ }
157
+ return uniqueModels(resultSets.flat());
158
+ }
159
+
160
+ function mergeProbedAndConfiguredModels(probedModels, configuredModels, toolType) {
161
+ const safeConfigured = Array.isArray(configuredModels) ? configuredModels : [];
162
+ const safeProbed = Array.isArray(probedModels) ? probedModels : [];
163
+ const builtInDefaults = Array.isArray(DEFAULT_CONFIG.defaultModels?.[toolType])
164
+ ? DEFAULT_CONFIG.defaultModels[toolType]
165
+ : [];
166
+ if (safeProbed.length > 0) {
167
+ return uniqueModels([...safeProbed, ...safeConfigured, ...builtInDefaults]);
168
+ }
169
+ return uniqueModels([...safeConfigured, ...builtInDefaults]);
170
+ }
171
+
172
+ /**
173
+ * Validate model list
174
+ * @param {Array} models - Array of model names
175
+ * @param {string} toolType - Tool type (claude, codex, gemini)
176
+ * @returns {Object} { valid: boolean, cleaned: array, error?: string }
177
+ */
178
+ function validateModelList(models, toolType) {
179
+ if (!Array.isArray(models)) {
180
+ return { valid: false, error: `${toolType}: models must be an array` };
181
+ }
182
+
183
+ if (models.length === 0) {
184
+ return { valid: false, error: `${toolType}: model list cannot be empty` };
185
+ }
186
+
187
+ if (models.length > 50) {
188
+ return { valid: false, error: `${toolType}: maximum 50 models allowed, got ${models.length}` };
189
+ }
190
+
191
+ const modelNamePattern = /^[a-zA-Z0-9._\-/:]+$/;
192
+ const cleaned = [];
193
+ const seen = new Set();
194
+
195
+ for (let i = 0; i < models.length; i++) {
196
+ const model = models[i];
197
+
198
+ if (typeof model !== 'string') {
199
+ return { valid: false, error: `${toolType}: model at index ${i} must be a string, got ${typeof model}` };
200
+ }
201
+
202
+ const trimmed = model.trim();
203
+
204
+ if (trimmed.length === 0) {
205
+ return { valid: false, error: `${toolType}: model at index ${i} is empty or whitespace` };
206
+ }
207
+
208
+ if (!modelNamePattern.test(trimmed)) {
209
+ return { valid: false, error: `${toolType}: model "${trimmed}" contains invalid characters (allowed: a-z A-Z 0-9 . _ - / :)` };
210
+ }
211
+
212
+ if (!seen.has(trimmed)) {
213
+ seen.add(trimmed);
214
+ cleaned.push(trimmed);
215
+ }
216
+ }
217
+
218
+ return { valid: true, cleaned };
219
+ }
220
+
221
+ /**
222
+ * GET /api/config/default-models
223
+ * 获取默认模型列表
224
+ */
225
+ router.get('/default-models', async (req, res) => {
226
+ try {
227
+ const config = loadConfig();
228
+ const configuredDefaultModels = config.defaultModels || DEFAULT_CONFIG.defaultModels;
229
+ const probe = parseBooleanQuery(req.query.probe, false);
230
+
231
+ if (!probe) {
232
+ return res.json({
233
+ defaultModels: configuredDefaultModels,
234
+ probed: false
235
+ });
236
+ }
237
+
238
+ const forceRefresh = parseBooleanQuery(req.query.forceRefresh, true);
239
+ const claudeChannels = getAllChannels();
240
+ const codexData = getCodexChannels();
241
+ const geminiData = getGeminiChannels();
242
+
243
+ // 各工具类型也按串行探测,进一步降低并发压力
244
+ const claudeProbed = await probeModelsForChannels(claudeChannels || [], 'claude', { forceRefresh });
245
+ const codexProbed = await probeModelsForChannels(codexData?.channels || [], 'codex', { forceRefresh });
246
+ const geminiProbed = await probeModelsForChannels(geminiData?.channels || [], 'gemini', { forceRefresh });
247
+
248
+ const defaultModels = {
249
+ claude: mergeProbedAndConfiguredModels(
250
+ claudeProbed,
251
+ configuredDefaultModels.claude,
252
+ 'claude'
253
+ ),
254
+ codex: mergeProbedAndConfiguredModels(
255
+ codexProbed,
256
+ configuredDefaultModels.codex,
257
+ 'codex'
258
+ ),
259
+ gemini: mergeProbedAndConfiguredModels(
260
+ geminiProbed,
261
+ configuredDefaultModels.gemini,
262
+ 'gemini'
263
+ )
264
+ };
265
+
266
+ res.json({
267
+ defaultModels,
268
+ probed: true,
269
+ forceRefresh
270
+ });
271
+ } catch (error) {
272
+ console.error('[Config API] Failed to get default models:', error);
273
+ res.status(500).json({ error: error.message });
274
+ }
275
+ });
276
+
277
+ /**
278
+ * POST /api/config/default-models
279
+ * 更新默认模型列表
280
+ */
281
+ router.post('/default-models', (req, res) => {
282
+ try {
283
+ const { defaultModels } = req.body;
284
+
285
+ if (!defaultModels || typeof defaultModels !== 'object') {
286
+ return res.status(400).json({
287
+ error: 'defaultModels must be an object'
288
+ });
289
+ }
290
+
291
+ const validToolTypes = ['claude', 'codex', 'gemini'];
292
+ const providedTypes = Object.keys(defaultModels);
293
+
294
+ // Validate that only valid tool types are provided
295
+ for (const toolType of providedTypes) {
296
+ if (!validToolTypes.includes(toolType)) {
297
+ return res.status(400).json({
298
+ error: `Invalid tool type: ${toolType}. Valid types: ${validToolTypes.join(', ')}`
299
+ });
300
+ }
301
+ }
302
+
303
+ // Validate each model list
304
+ const validated = {};
305
+ const errors = {};
306
+
307
+ for (const toolType of providedTypes) {
308
+ const result = validateModelList(defaultModels[toolType], toolType);
309
+ if (!result.valid) {
310
+ errors[toolType] = result.error;
311
+ } else {
312
+ validated[toolType] = result.cleaned;
313
+ }
314
+ }
315
+
316
+ if (Object.keys(errors).length > 0) {
317
+ return res.status(400).json({
318
+ error: 'Validation failed',
319
+ details: errors
320
+ });
321
+ }
322
+
323
+ // Load current config and merge
324
+ const config = loadConfig();
325
+ const newDefaultModels = {
326
+ ...(config.defaultModels || DEFAULT_CONFIG.defaultModels),
327
+ ...validated
328
+ };
329
+
330
+ // Save config
331
+ const newConfig = {
332
+ ...config,
333
+ projectsDir: config.projectsDir.replace(require('os').homedir(), '~'),
334
+ defaultModels: newDefaultModels
335
+ };
336
+
337
+ saveConfig(newConfig);
338
+
339
+ res.json({
340
+ success: true,
341
+ defaultModels: newDefaultModels
342
+ });
343
+ } catch (error) {
344
+ console.error('[Config API] Failed to save default models:', error);
345
+ res.status(500).json({ error: error.message });
346
+ }
347
+ });
348
+
349
+ /**
350
+ * POST /api/config/default-models/reset
351
+ * 重置默认模型列表
352
+ */
353
+ router.post('/default-models/reset', (req, res) => {
354
+ try {
355
+ const { toolType } = req.body;
356
+
357
+ const config = loadConfig();
358
+ let newDefaultModels;
359
+
360
+ if (toolType) {
361
+ // Reset specific tool type
362
+ const validToolTypes = ['claude', 'codex', 'gemini'];
363
+ if (!validToolTypes.includes(toolType)) {
364
+ return res.status(400).json({
365
+ error: `Invalid tool type: ${toolType}. Valid types: ${validToolTypes.join(', ')}`
366
+ });
367
+ }
368
+
369
+ newDefaultModels = {
370
+ ...(config.defaultModels || DEFAULT_CONFIG.defaultModels),
371
+ [toolType]: DEFAULT_CONFIG.defaultModels[toolType]
372
+ };
373
+ } else {
374
+ // Reset all tool types
375
+ newDefaultModels = { ...DEFAULT_CONFIG.defaultModels };
376
+ }
377
+
378
+ // Save config
379
+ const newConfig = {
380
+ ...config,
381
+ projectsDir: config.projectsDir.replace(require('os').homedir(), '~'),
382
+ defaultModels: newDefaultModels
383
+ };
384
+
385
+ saveConfig(newConfig);
386
+
387
+ res.json({
388
+ success: true,
389
+ defaultModels: newDefaultModels
390
+ });
391
+ } catch (error) {
392
+ console.error('[Config API] Failed to reset default models:', error);
393
+ res.status(500).json({ error: error.message });
394
+ }
395
+ });
396
+
397
+ /**
398
+ * GET /api/config/advanced
399
+ * 获取高级配置(端口、日志、性能等)
400
+ */
401
+ router.get('/advanced', (req, res) => {
402
+ try {
403
+ const config = loadConfig();
404
+ const modelDiscovery = normalizeModelDiscovery(config.modelDiscovery);
405
+ res.json({
406
+ ports: {
407
+ webUI: config.ports?.webUI || 19999,
408
+ proxy: config.ports?.proxy || 20088,
409
+ codexProxy: config.ports?.codexProxy || 20089,
410
+ geminiProxy: config.ports?.geminiProxy || 20090,
411
+ opencodeProxy: config.ports?.opencodeProxy || 20091
412
+ },
413
+ maxLogs: config.maxLogs || 100,
414
+ statsInterval: config.statsInterval || 30,
415
+ enableSessionBinding: config.enableSessionBinding !== false, // 默认开启
416
+ modelDiscovery,
417
+ pricing: config.pricing || DEFAULT_CONFIG.pricing
418
+ });
419
+ } catch (error) {
420
+ console.error('[Config API] Failed to get advanced config:', error);
421
+ res.status(500).json({ error: error.message });
422
+ }
423
+ });
424
+
425
+ /**
426
+ * POST /api/config/advanced
427
+ * 保存高级配置
428
+ */
429
+ router.post('/advanced', (req, res) => {
430
+ try {
431
+ const {
432
+ ports,
433
+ maxLogs,
434
+ statsInterval,
435
+ pricing,
436
+ enableSessionBinding,
437
+ modelDiscovery
438
+ } = req.body;
439
+
440
+ // 验证端口
441
+ if (ports) {
442
+ for (const [key, value] of Object.entries(ports)) {
443
+ const port = parseInt(value);
444
+ if (isNaN(port) || port < 1024 || port > 65535) {
445
+ return res.status(400).json({
446
+ error: `Invalid port for ${key}: must be between 1024-65535`
447
+ });
448
+ }
449
+ }
450
+ }
451
+
452
+ // 验证日志数量
453
+ if (maxLogs !== undefined) {
454
+ const logs = parseInt(maxLogs);
455
+ if (isNaN(logs) || logs < 50 || logs > 500) {
456
+ return res.status(400).json({
457
+ error: 'maxLogs must be between 50-500'
458
+ });
459
+ }
460
+ }
461
+
462
+ // 验证刷新间隔
463
+ if (statsInterval !== undefined) {
464
+ const interval = parseInt(statsInterval);
465
+ if (isNaN(interval) || interval < 10 || interval > 300) {
466
+ return res.status(400).json({
467
+ error: 'statsInterval must be between 10-300'
468
+ });
469
+ }
470
+ }
471
+
472
+ // 加载当前配置
473
+ const config = loadConfig();
474
+ const sanitizedPricing = sanitizePricing(pricing, config.pricing);
475
+ const normalizedModelDiscovery = normalizeModelDiscovery(
476
+ modelDiscovery,
477
+ config.modelDiscovery || DEFAULT_CONFIG.modelDiscovery
478
+ );
479
+
480
+ let normalizedPorts = config.ports;
481
+ if (ports) {
482
+ normalizedPorts = { ...config.ports };
483
+ Object.entries(ports).forEach(([key, value]) => {
484
+ const port = parseInt(value);
485
+ normalizedPorts[key] = port;
486
+ });
487
+ }
488
+
489
+ // 更新配置
490
+ const newConfig = {
491
+ ...config,
492
+ projectsDir: config.projectsDir.replace(require('os').homedir(), '~'),
493
+ ports: normalizedPorts,
494
+ maxLogs: maxLogs !== undefined ? parseInt(maxLogs) : config.maxLogs,
495
+ statsInterval: statsInterval !== undefined ? parseInt(statsInterval) : config.statsInterval,
496
+ enableSessionBinding: enableSessionBinding !== undefined ? enableSessionBinding : (config.enableSessionBinding !== false),
497
+ modelDiscovery: normalizedModelDiscovery,
498
+ pricing: sanitizedPricing
499
+ };
500
+
501
+ // 保存配置
502
+ saveConfig(newConfig);
503
+
504
+ res.json({
505
+ success: true,
506
+ config: {
507
+ ports: newConfig.ports,
508
+ maxLogs: newConfig.maxLogs,
509
+ statsInterval: newConfig.statsInterval,
510
+ enableSessionBinding: newConfig.enableSessionBinding,
511
+ modelDiscovery: newConfig.modelDiscovery,
512
+ pricing: newConfig.pricing
513
+ }
514
+ });
515
+ } catch (error) {
516
+ console.error('[Config API] Failed to save advanced config:', error);
517
+ res.status(500).json({ error: error.message });
518
+ }
519
+ });
520
+
521
+ module.exports = router;