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.
- package/CHANGELOG.md +599 -0
- package/LICENSE +21 -0
- package/README.md +439 -0
- package/bin/ctx.js +8 -0
- package/dist/web/assets/Analytics-DN_YsnkW.js +39 -0
- package/dist/web/assets/Analytics-DuYvId7u.css +1 -0
- package/dist/web/assets/ConfigTemplates-Bidwfdf2.css +1 -0
- package/dist/web/assets/ConfigTemplates-DpXIMy0p.js +1 -0
- package/dist/web/assets/Home-38JTUlYt.js +1 -0
- package/dist/web/assets/Home-CjupSEWE.css +1 -0
- package/dist/web/assets/PluginManager-CX2tgq2H.js +1 -0
- package/dist/web/assets/PluginManager-ROyoZ-6m.css +1 -0
- package/dist/web/assets/ProjectList-C1lDcsn6.js +1 -0
- package/dist/web/assets/ProjectList-oJIyIRkP.css +1 -0
- package/dist/web/assets/SessionList-C55tjV7i.css +1 -0
- package/dist/web/assets/SessionList-CZ7T6rVx.js +1 -0
- package/dist/web/assets/SkillManager-D7pd-d_P.css +1 -0
- package/dist/web/assets/SkillManager-DLN9f79y.js +1 -0
- package/dist/web/assets/WorkspaceManager-CrwgQgmP.css +1 -0
- package/dist/web/assets/WorkspaceManager-DxlHZkpZ.js +1 -0
- package/dist/web/assets/icons-DRrXwWZi.js +1 -0
- package/dist/web/assets/index-CetESrXw.css +1 -0
- package/dist/web/assets/index-Cfvn-2Gb.js +2 -0
- package/dist/web/assets/markdown-BfC0goYb.css +10 -0
- package/dist/web/assets/markdown-C9MYpaSi.js +1 -0
- package/dist/web/assets/naive-ui-DlpKk-8M.js +1 -0
- package/dist/web/assets/vendors-DMjSfzlv.js +7 -0
- package/dist/web/assets/vue-vendor-DET08QYg.js +45 -0
- package/dist/web/favicon.ico +0 -0
- package/dist/web/index.html +20 -0
- package/dist/web/logo.png +0 -0
- package/docs/bannel.png +0 -0
- package/docs/home.png +0 -0
- package/docs/logo.png +0 -0
- package/docs/model-redirection.md +251 -0
- package/docs/multi-channel-load-balancing.md +249 -0
- package/package.json +80 -0
- package/src/commands/channels.js +551 -0
- package/src/commands/cli-type.js +101 -0
- package/src/commands/daemon.js +365 -0
- package/src/commands/doctor.js +333 -0
- package/src/commands/export-config.js +205 -0
- package/src/commands/list.js +222 -0
- package/src/commands/logs.js +261 -0
- package/src/commands/plugin.js +585 -0
- package/src/commands/port-config.js +135 -0
- package/src/commands/proxy-control.js +264 -0
- package/src/commands/proxy.js +152 -0
- package/src/commands/resume.js +137 -0
- package/src/commands/search.js +190 -0
- package/src/commands/security.js +37 -0
- package/src/commands/stats.js +398 -0
- package/src/commands/switch.js +48 -0
- package/src/commands/toggle-proxy.js +247 -0
- package/src/commands/ui.js +99 -0
- package/src/commands/update.js +97 -0
- package/src/commands/workspace.js +454 -0
- package/src/config/default.js +69 -0
- package/src/config/loader.js +149 -0
- package/src/config/model-metadata.js +167 -0
- package/src/config/model-metadata.json +125 -0
- package/src/config/model-pricing.js +35 -0
- package/src/config/paths.js +190 -0
- package/src/index.js +680 -0
- package/src/plugins/constants.js +15 -0
- package/src/plugins/event-bus.js +54 -0
- package/src/plugins/manifest-validator.js +129 -0
- package/src/plugins/plugin-api.js +128 -0
- package/src/plugins/plugin-installer.js +601 -0
- package/src/plugins/plugin-loader.js +229 -0
- package/src/plugins/plugin-manager.js +170 -0
- package/src/plugins/registry.js +152 -0
- package/src/plugins/schema/plugin-manifest.json +115 -0
- package/src/reset-config.js +94 -0
- package/src/server/api/agents.js +826 -0
- package/src/server/api/aliases.js +36 -0
- package/src/server/api/channels.js +368 -0
- package/src/server/api/claude-hooks.js +480 -0
- package/src/server/api/codex-channels.js +417 -0
- package/src/server/api/codex-projects.js +104 -0
- package/src/server/api/codex-proxy.js +195 -0
- package/src/server/api/codex-sessions.js +483 -0
- package/src/server/api/codex-statistics.js +57 -0
- package/src/server/api/commands.js +482 -0
- package/src/server/api/config-export.js +212 -0
- package/src/server/api/config-registry.js +357 -0
- package/src/server/api/config-sync.js +155 -0
- package/src/server/api/config-templates.js +248 -0
- package/src/server/api/config.js +521 -0
- package/src/server/api/convert.js +260 -0
- package/src/server/api/dashboard.js +142 -0
- package/src/server/api/env.js +144 -0
- package/src/server/api/favorites.js +77 -0
- package/src/server/api/gemini-channels.js +366 -0
- package/src/server/api/gemini-projects.js +91 -0
- package/src/server/api/gemini-proxy.js +173 -0
- package/src/server/api/gemini-sessions.js +376 -0
- package/src/server/api/gemini-statistics.js +57 -0
- package/src/server/api/health-check.js +31 -0
- package/src/server/api/mcp.js +399 -0
- package/src/server/api/opencode-channels.js +419 -0
- package/src/server/api/opencode-projects.js +99 -0
- package/src/server/api/opencode-proxy.js +207 -0
- package/src/server/api/opencode-sessions.js +327 -0
- package/src/server/api/opencode-statistics.js +57 -0
- package/src/server/api/plugins.js +463 -0
- package/src/server/api/pm2-autostart.js +269 -0
- package/src/server/api/projects.js +124 -0
- package/src/server/api/prompts.js +279 -0
- package/src/server/api/proxy.js +306 -0
- package/src/server/api/security.js +53 -0
- package/src/server/api/sessions.js +514 -0
- package/src/server/api/settings.js +142 -0
- package/src/server/api/skills.js +570 -0
- package/src/server/api/statistics.js +238 -0
- package/src/server/api/ui-config.js +64 -0
- package/src/server/api/workspaces.js +456 -0
- package/src/server/codex-proxy-server.js +681 -0
- package/src/server/dev-server.js +26 -0
- package/src/server/gemini-proxy-server.js +610 -0
- package/src/server/index.js +422 -0
- package/src/server/opencode-proxy-server.js +4771 -0
- package/src/server/proxy-server.js +669 -0
- package/src/server/services/agents-service.js +1137 -0
- package/src/server/services/alias.js +71 -0
- package/src/server/services/channel-health.js +234 -0
- package/src/server/services/channel-scheduler.js +240 -0
- package/src/server/services/channels.js +447 -0
- package/src/server/services/codex-channels.js +705 -0
- package/src/server/services/codex-config.js +90 -0
- package/src/server/services/codex-parser.js +322 -0
- package/src/server/services/codex-sessions.js +936 -0
- package/src/server/services/codex-settings-manager.js +619 -0
- package/src/server/services/codex-speed-test-template.json +24 -0
- package/src/server/services/codex-statistics-service.js +161 -0
- package/src/server/services/commands-service.js +574 -0
- package/src/server/services/config-export-service.js +1165 -0
- package/src/server/services/config-registry-service.js +828 -0
- package/src/server/services/config-sync-manager.js +941 -0
- package/src/server/services/config-sync-service.js +504 -0
- package/src/server/services/config-templates-service.js +913 -0
- package/src/server/services/enhanced-cache.js +196 -0
- package/src/server/services/env-checker.js +409 -0
- package/src/server/services/env-manager.js +436 -0
- package/src/server/services/favorites.js +165 -0
- package/src/server/services/format-converter.js +620 -0
- package/src/server/services/gemini-channels.js +459 -0
- package/src/server/services/gemini-config.js +73 -0
- package/src/server/services/gemini-sessions.js +689 -0
- package/src/server/services/gemini-settings-manager.js +263 -0
- package/src/server/services/gemini-statistics-service.js +157 -0
- package/src/server/services/health-check.js +85 -0
- package/src/server/services/mcp-client.js +790 -0
- package/src/server/services/mcp-service.js +1732 -0
- package/src/server/services/model-detector.js +1245 -0
- package/src/server/services/network-access.js +80 -0
- package/src/server/services/opencode-channels.js +366 -0
- package/src/server/services/opencode-gateway-adapters.js +1168 -0
- package/src/server/services/opencode-gateway-converter.js +639 -0
- package/src/server/services/opencode-sessions.js +931 -0
- package/src/server/services/opencode-settings-manager.js +478 -0
- package/src/server/services/opencode-statistics-service.js +161 -0
- package/src/server/services/plugins-service.js +1268 -0
- package/src/server/services/prompts-service.js +534 -0
- package/src/server/services/proxy-runtime.js +79 -0
- package/src/server/services/repo-scanner-base.js +708 -0
- package/src/server/services/request-logger.js +130 -0
- package/src/server/services/response-decoder.js +21 -0
- package/src/server/services/security-config.js +131 -0
- package/src/server/services/session-cache.js +127 -0
- package/src/server/services/session-converter.js +577 -0
- package/src/server/services/sessions.js +900 -0
- package/src/server/services/settings-manager.js +163 -0
- package/src/server/services/skill-service.js +1482 -0
- package/src/server/services/speed-test.js +1146 -0
- package/src/server/services/statistics-service.js +1043 -0
- package/src/server/services/ui-config.js +132 -0
- package/src/server/services/workspace-service.js +830 -0
- package/src/server/utils/pricing.js +73 -0
- package/src/server/websocket-server.js +513 -0
- package/src/ui/menu.js +139 -0
- package/src/ui/prompts.js +100 -0
- package/src/utils/format.js +43 -0
- package/src/utils/port-helper.js +108 -0
- 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;
|