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