@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.
- package/CHANGELOG.md +38 -18
- package/README.md +8 -8
- package/dist/web/assets/ConfigTemplates-Bidwfdf2.css +1 -0
- package/dist/web/assets/ConfigTemplates-ZrK_s7ma.js +1 -0
- package/dist/web/assets/Home-B8YfhZ3c.js +1 -0
- package/dist/web/assets/Home-Di2qsylF.css +1 -0
- package/dist/web/assets/PluginManager-BD7QUZbU.js +1 -0
- package/dist/web/assets/PluginManager-ROyoZ-6m.css +1 -0
- package/dist/web/assets/ProjectList-C1fQb9OW.css +1 -0
- package/dist/web/assets/ProjectList-DRb1DuHV.js +1 -0
- package/dist/web/assets/SessionList-BGJWyneI.css +1 -0
- package/dist/web/assets/SessionList-lZ0LKzfT.js +1 -0
- package/dist/web/assets/SkillManager-C1xG5B4Q.js +1 -0
- package/dist/web/assets/SkillManager-D7pd-d_P.css +1 -0
- package/dist/web/assets/Terminal-DGNJeVtc.css +1 -0
- package/dist/web/assets/Terminal-DksBo_lM.js +1 -0
- package/dist/web/assets/WorkspaceManager-Burx7XOo.js +1 -0
- package/dist/web/assets/WorkspaceManager-CrwgQgmP.css +1 -0
- package/dist/web/assets/icons-kcfLIMBB.js +1 -0
- package/dist/web/assets/index-Ufv5rCa5.css +1 -0
- package/dist/web/assets/index-lAkrRC3h.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-CSrLusZZ.js +1 -0
- package/dist/web/assets/{vendors-D2HHw_aW.js → vendors-CO3Upi1d.js} +2 -2
- package/dist/web/assets/vue-vendor-DqyWIXEb.js +45 -0
- package/dist/web/assets/xterm-6GBZ9nXN.css +32 -0
- package/dist/web/assets/xterm-BJzAjXCH.js +13 -0
- package/dist/web/index.html +8 -6
- package/package.json +4 -2
- package/src/commands/channels.js +48 -1
- package/src/commands/cli-type.js +4 -2
- package/src/commands/daemon.js +92 -13
- package/src/commands/doctor.js +10 -9
- package/src/commands/list.js +1 -1
- package/src/commands/logs.js +6 -4
- package/src/commands/port-config.js +24 -4
- package/src/commands/proxy-control.js +12 -6
- package/src/commands/search.js +1 -1
- package/src/commands/security.js +3 -2
- package/src/commands/stats.js +226 -52
- package/src/commands/switch.js +1 -1
- package/src/commands/toggle-proxy.js +31 -6
- package/src/commands/ui.js +8 -1
- package/src/commands/update.js +97 -0
- package/src/commands/workspace.js +1 -1
- package/src/config/default.js +39 -2
- package/src/config/loader.js +74 -8
- package/src/config/paths.js +105 -33
- package/src/index.js +67 -4
- package/src/plugins/constants.js +3 -2
- package/src/plugins/plugin-api.js +1 -1
- package/src/reset-config.js +4 -2
- package/src/server/api/agents.js +57 -14
- package/src/server/api/channels.js +112 -33
- package/src/server/api/codex-channels.js +111 -18
- package/src/server/api/codex-proxy.js +14 -8
- package/src/server/api/commands.js +71 -18
- package/src/server/api/config-export.js +0 -6
- package/src/server/api/config-registry.js +11 -3
- package/src/server/api/config.js +376 -5
- package/src/server/api/convert.js +133 -0
- package/src/server/api/dashboard.js +22 -6
- package/src/server/api/gemini-channels.js +107 -18
- package/src/server/api/gemini-proxy.js +14 -8
- package/src/server/api/gemini-sessions.js +1 -1
- package/src/server/api/health-check.js +4 -3
- package/src/server/api/mcp.js +3 -3
- 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 +198 -0
- package/src/server/api/opencode-sessions.js +403 -0
- package/src/server/api/opencode-statistics.js +57 -0
- package/src/server/api/plugins.js +66 -19
- package/src/server/api/prompts.js +2 -2
- package/src/server/api/proxy.js +7 -4
- package/src/server/api/sessions.js +3 -0
- package/src/server/api/skills.js +69 -18
- package/src/server/api/workspaces.js +78 -6
- package/src/server/codex-proxy-server.js +32 -19
- package/src/server/dev-server.js +1 -1
- package/src/server/gemini-proxy-server.js +17 -3
- package/src/server/index.js +164 -48
- package/src/server/opencode-proxy-server.js +4375 -0
- package/src/server/proxy-server.js +30 -19
- package/src/server/services/agents-service.js +61 -24
- package/src/server/services/channel-scheduler.js +9 -5
- package/src/server/services/channels.js +70 -12
- package/src/server/services/codex-channels.js +61 -23
- package/src/server/services/codex-settings-manager.js +271 -49
- package/src/server/services/codex-statistics-service.js +2 -2
- package/src/server/services/commands-service.js +84 -25
- package/src/server/services/config-export-service.js +7 -45
- package/src/server/services/config-registry-service.js +63 -17
- package/src/server/services/config-sync-manager.js +160 -7
- package/src/server/services/config-templates-service.js +204 -51
- package/src/server/services/env-checker.js +26 -12
- package/src/server/services/env-manager.js +126 -18
- package/src/server/services/favorites.js +5 -3
- package/src/server/services/gemini-channels.js +37 -15
- package/src/server/services/gemini-statistics-service.js +2 -2
- package/src/server/services/mcp-service.js +350 -9
- package/src/server/services/model-detector.js +707 -221
- package/src/server/services/network-access.js +80 -0
- package/src/server/services/opencode-channels.js +206 -0
- package/src/server/services/opencode-gateway-converter.js +639 -0
- package/src/server/services/opencode-sessions.js +663 -0
- package/src/server/services/opencode-settings-manager.js +342 -0
- package/src/server/services/opencode-statistics-service.js +255 -0
- package/src/server/services/plugins-service.js +479 -22
- package/src/server/services/prompts-service.js +53 -11
- package/src/server/services/proxy-runtime.js +1 -1
- package/src/server/services/repo-scanner-base.js +1 -1
- package/src/server/services/security-config.js +1 -1
- package/src/server/services/session-cache.js +1 -1
- package/src/server/services/skill-service.js +300 -46
- package/src/server/services/speed-test.js +464 -186
- package/src/server/services/statistics-service.js +2 -2
- package/src/server/services/terminal-commands.js +10 -3
- package/src/server/services/terminal-config.js +1 -1
- package/src/server/services/ui-config.js +1 -1
- package/src/server/services/workspace-service.js +57 -100
- package/src/server/websocket-server.js +132 -3
- package/src/ui/menu.js +49 -40
- package/src/utils/port-helper.js +22 -8
- package/src/utils/session.js +5 -4
- package/dist/web/assets/icons-BxudHPiX.js +0 -1
- package/dist/web/assets/index-D2VfwJBa.js +0 -14
- package/dist/web/assets/index-oXBzu0bd.css +0 -41
- package/dist/web/assets/naive-ui-DT-Uur8K.js +0 -1
- package/dist/web/assets/vue-vendor-6JaYHOiI.js +0 -44
- package/src/server/api/permissions.js +0 -385
- package/src/server/services/permission-templates-service.js +0 -308
package/src/server/api/config.js
CHANGED
|
@@ -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 ||
|
|
49
|
-
proxy: config.ports?.proxy ||
|
|
50
|
-
codexProxy: config.ports?.codexProxy ||
|
|
51
|
-
geminiProxy: config.ports?.geminiProxy ||
|
|
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 {
|
|
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
|
});
|