coding-tool-x 3.7.10 → 3.7.11
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/dist/web/assets/{Analytics-CkUtnoz9.js → Analytics-Ck_00Brj.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-BwPqhE-N.js → ConfigTemplates-Cpw2vUa8.js} +1 -1
- package/dist/web/assets/Home-Bft3Kztl.js +1 -0
- package/dist/web/assets/Home-CHZJkkjH.css +1 -0
- package/dist/web/assets/{PluginManager-JMXWl37b.js → PluginManager-2RlvEJCT.js} +1 -1
- package/dist/web/assets/{ProjectList-CZwDgVvt.js → ProjectList-_ecCngpl.js} +1 -1
- package/dist/web/assets/{SessionList-2fZmN7Au.js → SessionList-DlWcw3hO.js} +1 -1
- package/dist/web/assets/{SkillManager-Bhzhkeis.js → SkillManager-CnY7IXkX.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-B3lhhBD0.js → WorkspaceManager-3mqghi09.js} +1 -1
- package/dist/web/assets/{index-C5nLTbOk.css → index-BceXgSSe.css} +1 -1
- package/dist/web/assets/index-k5oP7c-A.js +2 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/src/commands/toggle-proxy.js +7 -8
- package/src/commands/ui.js +11 -1
- package/src/config/default.js +1 -1
- package/src/index.js +41 -15
- package/src/server/api/omp-channels.js +6 -4
- package/src/server/api/omp-config.js +1 -1
- package/src/server/api/omp-proxy.js +13 -42
- package/src/server/index.js +3 -3
- package/src/server/omp-proxy-server.js +129 -10
- package/src/server/services/channel-scheduler.js +59 -9
- package/src/server/services/omp-auth-gateway-client.js +62 -0
- package/src/server/services/omp-channels.js +109 -12
- package/src/server/services/omp-gateway-routing.js +295 -0
- package/src/server/services/omp-gateway.js +919 -0
- package/src/server/services/omp-session-log-observer.js +138 -0
- package/src/server/services/omp-sessions.js +80 -0
- package/src/server/services/omp-settings-manager.js +335 -86
- package/dist/web/assets/Home-BANl91zG.js +0 -1
- package/dist/web/assets/Home-CqnBXFxj.css +0 -1
- package/dist/web/assets/index-LYvv7wtx.js +0 -2
package/dist/web/index.html
CHANGED
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
<link rel="icon" href="/favicon.ico">
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
7
|
<title>coding-tool-x · AI 编程工作台</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-k5oP7c-A.js"></script>
|
|
9
9
|
<link rel="modulepreload" crossorigin href="/assets/markdown-BNJfEgCA.js">
|
|
10
10
|
<link rel="modulepreload" crossorigin href="/assets/vue-vendor-B5RqjEjQ.js">
|
|
11
11
|
<link rel="modulepreload" crossorigin href="/assets/naive-ui-BALVpMgd.js">
|
|
12
12
|
<link rel="modulepreload" crossorigin href="/assets/icons-nGdSKQn2.js">
|
|
13
13
|
<link rel="modulepreload" crossorigin href="/assets/vendors-DQA4Hw9v.js">
|
|
14
14
|
<link rel="stylesheet" crossorigin href="/assets/markdown-BfC0goYb.css">
|
|
15
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
15
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BceXgSSe.css">
|
|
16
16
|
</head>
|
|
17
17
|
<body>
|
|
18
18
|
<div id="app"></div>
|
package/package.json
CHANGED
|
@@ -205,9 +205,10 @@ async function handleStartProxy(cliType, services) {
|
|
|
205
205
|
|
|
206
206
|
console.log(chalk.cyan('动态切换功能说明:'));
|
|
207
207
|
if (services.managedProviderConfig) {
|
|
208
|
-
console.log(chalk.gray('•
|
|
209
|
-
console.log(chalk.gray(`• 可以通过 Web UI 或"渠道管理"功能调整启用的 ${toolName}
|
|
210
|
-
console.log(chalk.gray(
|
|
208
|
+
console.log(chalk.gray('• 开启后会启动 OMP 专用本地网关,并把受管 provider 指向该网关'));
|
|
209
|
+
console.log(chalk.gray(`• 可以通过 Web UI 或"渠道管理"功能调整启用的 ${toolName} 路由组`));
|
|
210
|
+
console.log(chalk.gray('• 同路由组内由 coding-tool-x 动态选择渠道,跨模型 fallback 仍由 OMP 处理'));
|
|
211
|
+
console.log(chalk.gray(`• OMP 网关地址: http://127.0.0.1:${defaultPort}\n`));
|
|
211
212
|
} else {
|
|
212
213
|
console.log(chalk.gray('• 开启后会在本地启动一个代理服务'));
|
|
213
214
|
console.log(chalk.gray(`• 可以在不重启 ${toolName} 的情况下动态管理渠道`));
|
|
@@ -217,7 +218,7 @@ async function handleStartProxy(cliType, services) {
|
|
|
217
218
|
|
|
218
219
|
console.log(chalk.yellow('[WARN] 重要提示:'));
|
|
219
220
|
if (services.managedProviderConfig) {
|
|
220
|
-
console.log(chalk.yellow('•
|
|
221
|
+
console.log(chalk.yellow('• 启用后会写入指向本地网关的 OMP models.yml provider 配置'));
|
|
221
222
|
console.log(chalk.yellow('• 关闭会移除受管 provider,不会清理 OMP 原生 auth.json\n'));
|
|
222
223
|
} else {
|
|
223
224
|
console.log(chalk.yellow('• 开启期间请勿关闭 CLI 终端窗口'));
|
|
@@ -253,9 +254,7 @@ async function handleStartProxy(cliType, services) {
|
|
|
253
254
|
|
|
254
255
|
if (services.managedProviderConfig) {
|
|
255
256
|
console.log(chalk.green('[OK] OMP models.yml 受管 provider 已启用'));
|
|
256
|
-
|
|
257
|
-
console.log(chalk.gray(`状态端口: http://127.0.0.1:${proxyResult.port}`));
|
|
258
|
-
}
|
|
257
|
+
console.log(chalk.gray('OMP 新增会话用量日志观察已启用'));
|
|
259
258
|
(proxyResult.warnings || []).forEach((warning) => {
|
|
260
259
|
console.log(chalk.yellow(`[WARN] ${warning}`));
|
|
261
260
|
});
|
|
@@ -325,7 +324,7 @@ async function handleStopProxy(cliType, services) {
|
|
|
325
324
|
if (services.managedProviderConfig) {
|
|
326
325
|
console.log(chalk.gray(`• 受管配置: ${chalk.green('已启用')}`));
|
|
327
326
|
console.log(chalk.gray(`• 配置模式: ${proxyStatus.mode || 'models-yml-provider-config'}`));
|
|
328
|
-
console.log(chalk.gray(`•
|
|
327
|
+
console.log(chalk.gray(`• 会话日志观察: ${proxyStatus.sessionLogObserver?.running ? chalk.green('运行中') : chalk.yellow('未运行')}\n`));
|
|
329
328
|
} else {
|
|
330
329
|
console.log(chalk.gray(`• 代理服务: ${chalk.green('运行中')}`));
|
|
331
330
|
console.log(chalk.gray(`• 代理端口: ${proxyStatus.port}`));
|
package/src/commands/ui.js
CHANGED
|
@@ -87,6 +87,16 @@ async function handleUI() {
|
|
|
87
87
|
// 忽略错误
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
// OMP 动态网关持有 models.yml 事务,退出前必须先恢复原生配置。
|
|
91
|
+
try {
|
|
92
|
+
const { getOmpProxyStatus, stopOmpProxyServer } = require('../server/omp-proxy-server');
|
|
93
|
+
if (getOmpProxyStatus().running) {
|
|
94
|
+
await stopOmpProxyServer({ forceAfterMs: 1500 });
|
|
95
|
+
}
|
|
96
|
+
} catch (err) {
|
|
97
|
+
console.error(chalk.yellow(`[WARN] OMP 动态网关退出清理失败: ${err.message}`));
|
|
98
|
+
}
|
|
99
|
+
|
|
90
100
|
console.log(chalk.green('[OK] Web UI 已停止\n'));
|
|
91
101
|
process.exit(0);
|
|
92
102
|
});
|
|
@@ -99,7 +109,7 @@ async function handleUI() {
|
|
|
99
109
|
|
|
100
110
|
} catch (error) {
|
|
101
111
|
console.error(chalk.red('启动服务器失败:'), error.message);
|
|
102
|
-
|
|
112
|
+
throw error;
|
|
103
113
|
}
|
|
104
114
|
}
|
|
105
115
|
|
package/src/config/default.js
CHANGED
|
@@ -21,7 +21,7 @@ const DEFAULT_CONFIG = {
|
|
|
21
21
|
codexProxy: 20089, // Codex 代理服务端口
|
|
22
22
|
geminiProxy: 20090, // Gemini 代理服务端口
|
|
23
23
|
opencodeProxy: 20091, // OpenCode 代理服务端口
|
|
24
|
-
ompProxy: 20092 // OMP
|
|
24
|
+
ompProxy: 20092 // OMP 专用动态网关端口
|
|
25
25
|
},
|
|
26
26
|
maxLogs: 100,
|
|
27
27
|
statsInterval: 30,
|
package/src/index.js
CHANGED
|
@@ -34,9 +34,7 @@ function getVersion() {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
function finishCli(code = 0) {
|
|
37
|
-
|
|
38
|
-
PluginManager.shutdownPlugins();
|
|
39
|
-
process.exit(code);
|
|
37
|
+
return shutdownProcess(code);
|
|
40
38
|
}
|
|
41
39
|
|
|
42
40
|
// 显示帮助信息
|
|
@@ -128,20 +126,49 @@ function showHelp() {
|
|
|
128
126
|
console.log(chalk.gray(' 问题: https://github.com/ZeaoZhang/coding-tool/issues\n'));
|
|
129
127
|
}
|
|
130
128
|
|
|
129
|
+
let processShutdownPromise = null;
|
|
130
|
+
|
|
131
|
+
async function stopOwnedOmpGatewayBeforeExit() {
|
|
132
|
+
try {
|
|
133
|
+
const {
|
|
134
|
+
getOmpProxyStatus,
|
|
135
|
+
stopOmpProxyServer
|
|
136
|
+
} = require('./server/omp-proxy-server');
|
|
137
|
+
if (getOmpProxyStatus().running) {
|
|
138
|
+
await stopOmpProxyServer({ forceAfterMs: 1500 });
|
|
139
|
+
}
|
|
140
|
+
} catch (error) {
|
|
141
|
+
console.error(chalk.yellow(`[WARN] OMP 动态网关退出清理失败: ${error.message}`));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function shutdownProcess(code = 0, error = null) {
|
|
146
|
+
if (processShutdownPromise) return processShutdownPromise;
|
|
147
|
+
processShutdownPromise = (async () => {
|
|
148
|
+
await stopOwnedOmpGatewayBeforeExit();
|
|
149
|
+
eventBus.emitSync('cli:shutdown', {});
|
|
150
|
+
PluginManager.shutdownPlugins();
|
|
151
|
+
if (error) {
|
|
152
|
+
console.error(error);
|
|
153
|
+
}
|
|
154
|
+
process.exit(code);
|
|
155
|
+
})();
|
|
156
|
+
return processShutdownPromise;
|
|
157
|
+
}
|
|
158
|
+
|
|
131
159
|
// 全局错误处理
|
|
132
160
|
process.on('uncaughtException', (err) => {
|
|
133
161
|
// 忽略终端相关的错误(通常在 Ctrl+C 时发生)
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
throw err;
|
|
162
|
+
const terminalError = err.code === 'EIO' || err.code === 'ENOTTY' || err.code === 'EPIPE';
|
|
163
|
+
void shutdownProcess(terminalError ? 0 : 1, terminalError ? null : err);
|
|
138
164
|
});
|
|
139
165
|
|
|
140
|
-
//
|
|
141
|
-
process.on('SIGINT',
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
166
|
+
// 处理进程退出信号,OMP 必须先排空并恢复原生配置。
|
|
167
|
+
process.on('SIGINT', () => {
|
|
168
|
+
void shutdownProcess(0);
|
|
169
|
+
});
|
|
170
|
+
process.on('SIGTERM', () => {
|
|
171
|
+
void shutdownProcess(0);
|
|
145
172
|
});
|
|
146
173
|
|
|
147
174
|
/**
|
|
@@ -416,7 +443,7 @@ async function main() {
|
|
|
416
443
|
eventBus.emitSync('cli:command:after', { command: args[0], args: args.slice(1), result });
|
|
417
444
|
if (!result.success) {
|
|
418
445
|
console.error(chalk.red(result.error));
|
|
419
|
-
|
|
446
|
+
return shutdownProcess(1);
|
|
420
447
|
}
|
|
421
448
|
return;
|
|
422
449
|
}
|
|
@@ -712,6 +739,5 @@ async function main() {
|
|
|
712
739
|
|
|
713
740
|
// 启动应用
|
|
714
741
|
main().catch((error) => {
|
|
715
|
-
|
|
716
|
-
process.exit(1);
|
|
742
|
+
void shutdownProcess(1, error);
|
|
717
743
|
});
|
|
@@ -294,7 +294,7 @@ module.exports = () => {
|
|
|
294
294
|
baseUrl,
|
|
295
295
|
apiKey: apiKey || '',
|
|
296
296
|
gatewaySourceType: gatewaySourceType || 'openai_compatible',
|
|
297
|
-
authMode:
|
|
297
|
+
authMode: ['api_key', 'oauth', 'none'].includes(authMode) ? authMode : 'api_key',
|
|
298
298
|
oauthProviderId: oauthProviderId || '',
|
|
299
299
|
model: model || null,
|
|
300
300
|
speedTestModel: speedTestModel || null,
|
|
@@ -407,6 +407,7 @@ module.exports = () => {
|
|
|
407
407
|
providerApi,
|
|
408
408
|
authMode,
|
|
409
409
|
oauthProviderId,
|
|
410
|
+
routingGroup,
|
|
410
411
|
presetId,
|
|
411
412
|
websiteUrl,
|
|
412
413
|
balanceToken,
|
|
@@ -425,8 +426,8 @@ module.exports = () => {
|
|
|
425
426
|
if (!name || !baseUrl) {
|
|
426
427
|
return res.status(400).json({ error: 'Missing required fields: name and baseUrl' });
|
|
427
428
|
}
|
|
428
|
-
const normalizedAuthMode =
|
|
429
|
-
if (!apiKey && normalizedAuthMode
|
|
429
|
+
const normalizedAuthMode = ['api_key', 'oauth', 'none'].includes(authMode) ? authMode : 'api_key';
|
|
430
|
+
if (!apiKey && normalizedAuthMode === 'api_key') {
|
|
430
431
|
return res.status(400).json({ error: 'API Key is required' });
|
|
431
432
|
}
|
|
432
433
|
|
|
@@ -435,7 +436,8 @@ module.exports = () => {
|
|
|
435
436
|
providerApi: providerApi || wireApi || 'openai-completions',
|
|
436
437
|
providerKey,
|
|
437
438
|
authMode: normalizedAuthMode,
|
|
438
|
-
oauthProviderId: oauthProviderId || providerKey || '',
|
|
439
|
+
oauthProviderId: normalizedAuthMode === 'oauth' ? (oauthProviderId || providerKey || '') : '',
|
|
440
|
+
routingGroup: String(routingGroup || '').trim(),
|
|
439
441
|
enabled,
|
|
440
442
|
weight,
|
|
441
443
|
maxConcurrency,
|
|
@@ -81,7 +81,7 @@ function buildCapabilities() {
|
|
|
81
81
|
},
|
|
82
82
|
notes: [
|
|
83
83
|
'The omp channel is backed by OMP and uses omp as its platform key.',
|
|
84
|
-
'
|
|
84
|
+
'Dynamic mode exposes managed ctx-* providers through the loopback OMP HTTP gateway; disabling it restores native OMP configuration.'
|
|
85
85
|
]
|
|
86
86
|
};
|
|
87
87
|
}
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
const express = require('express');
|
|
2
|
-
const fs = require('fs');
|
|
3
|
-
const path = require('path');
|
|
4
2
|
const router = express.Router();
|
|
5
3
|
const {
|
|
6
4
|
startOmpProxyServer,
|
|
@@ -10,10 +8,10 @@ const {
|
|
|
10
8
|
const {
|
|
11
9
|
getChannels,
|
|
12
10
|
getEnabledChannels,
|
|
13
|
-
markChannelAsRecentlyUsed
|
|
11
|
+
markChannelAsRecentlyUsed,
|
|
12
|
+
loadManagedOmpActiveChannelId
|
|
14
13
|
} = require('../services/omp-channels');
|
|
15
14
|
const { getSchedulerState } = require('../services/channel-scheduler');
|
|
16
|
-
const { PATHS, ensureStorageDirMigrated } = require('../../config/paths');
|
|
17
15
|
|
|
18
16
|
function sanitizeChannel(channel) {
|
|
19
17
|
if (!channel) return null;
|
|
@@ -21,7 +19,10 @@ function sanitizeChannel(channel) {
|
|
|
21
19
|
id: channel.id,
|
|
22
20
|
name: channel.name,
|
|
23
21
|
baseUrl: channel.baseUrl,
|
|
24
|
-
websiteUrl: channel.websiteUrl
|
|
22
|
+
websiteUrl: channel.websiteUrl,
|
|
23
|
+
providerKey: channel.providerKey,
|
|
24
|
+
providerApi: channel.providerApi,
|
|
25
|
+
routingGroup: channel.routingGroup || ''
|
|
25
26
|
};
|
|
26
27
|
}
|
|
27
28
|
|
|
@@ -35,34 +36,6 @@ function selectLatestEnabledChannel(channels) {
|
|
|
35
36
|
}, enabledChannels[0]);
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
function saveActiveChannelId(channelId) {
|
|
39
|
-
ensureStorageDirMigrated();
|
|
40
|
-
const filePath = PATHS.activeChannel.omp;
|
|
41
|
-
const dir = path.dirname(filePath);
|
|
42
|
-
if (!fs.existsSync(dir)) {
|
|
43
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
44
|
-
}
|
|
45
|
-
fs.writeFileSync(filePath, JSON.stringify({ activeChannelId: channelId }, null, 2), 'utf8');
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function loadActiveChannelId() {
|
|
49
|
-
ensureStorageDirMigrated();
|
|
50
|
-
try {
|
|
51
|
-
if (!fs.existsSync(PATHS.activeChannel.omp)) return null;
|
|
52
|
-
const data = JSON.parse(fs.readFileSync(PATHS.activeChannel.omp, 'utf8'));
|
|
53
|
-
return data.activeChannelId || null;
|
|
54
|
-
} catch {
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function removeActiveChannelFile() {
|
|
60
|
-
ensureStorageDirMigrated();
|
|
61
|
-
if (fs.existsSync(PATHS.activeChannel.omp)) {
|
|
62
|
-
fs.unlinkSync(PATHS.activeChannel.omp);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
39
|
function resolveActiveChannel(channels, activeChannelId = null) {
|
|
67
40
|
if (!Array.isArray(channels) || channels.length === 0) return null;
|
|
68
41
|
if (activeChannelId) {
|
|
@@ -80,12 +53,12 @@ router.get('/status', (req, res) => {
|
|
|
80
53
|
const proxy = getOmpProxyStatus();
|
|
81
54
|
const { channels } = getChannels();
|
|
82
55
|
const enabledChannels = channels.filter(ch => ch.enabled !== false);
|
|
83
|
-
const activeChannel = resolveActiveChannel(channels,
|
|
56
|
+
const activeChannel = resolveActiveChannel(channels, loadManagedOmpActiveChannelId());
|
|
84
57
|
res.json({
|
|
85
58
|
proxy,
|
|
86
59
|
config: {
|
|
87
|
-
mode: '
|
|
88
|
-
nativeProxyProtocol:
|
|
60
|
+
mode: 'http-gateway',
|
|
61
|
+
nativeProxyProtocol: true
|
|
89
62
|
},
|
|
90
63
|
activeChannel: sanitizeChannel(activeChannel),
|
|
91
64
|
enabledChannelsCount: enabledChannels.length,
|
|
@@ -107,11 +80,10 @@ router.post('/start', async (req, res) => {
|
|
|
107
80
|
|
|
108
81
|
let currentChannel = selectLatestEnabledChannel(enabledChannels) || enabledChannels[0];
|
|
109
82
|
currentChannel = markChannelAsRecentlyUsed(currentChannel.id);
|
|
110
|
-
saveActiveChannelId(currentChannel.id);
|
|
111
83
|
|
|
112
|
-
const proxyResult = await startOmpProxyServer();
|
|
84
|
+
const proxyResult = await startOmpProxyServer({ activeChannelId: currentChannel.id });
|
|
113
85
|
if (!proxyResult.success) {
|
|
114
|
-
return res.status(500).json({ error: 'Failed to
|
|
86
|
+
return res.status(500).json({ error: 'Failed to start OMP gateway' });
|
|
115
87
|
}
|
|
116
88
|
|
|
117
89
|
const { broadcastProxyState, broadcastSchedulerState } = require('../websocket-server');
|
|
@@ -125,10 +97,10 @@ router.post('/start', async (req, res) => {
|
|
|
125
97
|
success: true,
|
|
126
98
|
port: proxyResult.port,
|
|
127
99
|
activeChannel: sanitizeChannel(activeChannel),
|
|
128
|
-
mode: '
|
|
100
|
+
mode: 'http-gateway',
|
|
129
101
|
sync: proxyResult.sync || null,
|
|
130
102
|
warnings: proxyResult.warnings || [],
|
|
131
|
-
message: `OMP
|
|
103
|
+
message: `OMP gateway and managed providers enabled, active channel: ${activeChannel.name}`
|
|
132
104
|
});
|
|
133
105
|
} catch (error) {
|
|
134
106
|
console.error('[OMP Proxy] Start failed:', error);
|
|
@@ -139,7 +111,6 @@ router.post('/start', async (req, res) => {
|
|
|
139
111
|
router.post('/stop', async (req, res) => {
|
|
140
112
|
try {
|
|
141
113
|
const result = await stopOmpProxyServer();
|
|
142
|
-
removeActiveChannelFile();
|
|
143
114
|
const { broadcastProxyState, broadcastSchedulerState } = require('../websocket-server');
|
|
144
115
|
const { channels } = getChannels();
|
|
145
116
|
broadcastProxyState('omp', getOmpProxyStatus(), null, channels);
|
package/src/server/index.js
CHANGED
|
@@ -465,13 +465,13 @@ function autoRestoreProxies() {
|
|
|
465
465
|
startOmpProxyServer({ preserveStartTime: true })
|
|
466
466
|
.then((result) => {
|
|
467
467
|
if (result.success) {
|
|
468
|
-
console.log(chalk.green(`[OK] OMP
|
|
468
|
+
console.log(chalk.green(`[OK] OMP 动态网关已自动恢复: http://127.0.0.1:${result.port}`));
|
|
469
469
|
} else {
|
|
470
|
-
console.error(chalk.red(`[ERROR] OMP
|
|
470
|
+
console.error(chalk.red(`[ERROR] OMP 动态网关恢复失败: ${result.error || 'Unknown error'}`));
|
|
471
471
|
}
|
|
472
472
|
})
|
|
473
473
|
.catch((err) => {
|
|
474
|
-
console.error(chalk.red(`[ERROR] OMP
|
|
474
|
+
console.error(chalk.red(`[ERROR] OMP 动态网关恢复失败: ${err.message}`));
|
|
475
475
|
});
|
|
476
476
|
}
|
|
477
477
|
}
|
|
@@ -1,17 +1,96 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
1
2
|
const { loadConfig } = require('../config/loader');
|
|
2
3
|
const { saveProxyStartTime, clearProxyStartTime, getProxyStartTime, getProxyRuntime } = require('./services/proxy-runtime');
|
|
3
|
-
const {
|
|
4
|
+
const {
|
|
5
|
+
syncManagedOmpProviders,
|
|
6
|
+
disableManagedOmpProviders,
|
|
7
|
+
getEnabledChannels,
|
|
8
|
+
isManagedOmpModeEnabled,
|
|
9
|
+
enableManagedOmpMode,
|
|
10
|
+
disableManagedOmpMode,
|
|
11
|
+
loadManagedOmpActiveChannelId,
|
|
12
|
+
loadManagedOmpModeState
|
|
13
|
+
} = require('./services/omp-channels');
|
|
14
|
+
const {
|
|
15
|
+
startOmpSessionLogObserver,
|
|
16
|
+
stopOmpSessionLogObserver,
|
|
17
|
+
getOmpSessionLogObserverStatus
|
|
18
|
+
} = require('./services/omp-session-log-observer');
|
|
19
|
+
const { createOmpGateway } = require('./services/omp-gateway');
|
|
20
|
+
const { prepareManagedOmpChannels } = require('./services/omp-gateway-routing');
|
|
21
|
+
const { probeOmpAuthGateways } = require('./services/omp-auth-gateway-client');
|
|
4
22
|
|
|
5
|
-
let running = false;
|
|
6
23
|
let currentPort = null;
|
|
7
24
|
let lastSyncResult = null;
|
|
25
|
+
let lastRoutingResult = { routes: [], unsupportedChannels: [] };
|
|
26
|
+
let lifecycle = Promise.resolve();
|
|
8
27
|
|
|
9
|
-
|
|
28
|
+
const gateway = createOmpGateway({
|
|
29
|
+
getChannels: () => getEnabledChannels()
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
function withLifecycleLock(operation) {
|
|
33
|
+
const next = lifecycle.then(operation, operation);
|
|
34
|
+
lifecycle = next.catch(() => {});
|
|
35
|
+
return next;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function startOmpProxyServerUnlocked(options = {}) {
|
|
39
|
+
if (gateway.status().listening) {
|
|
40
|
+
return {
|
|
41
|
+
success: true,
|
|
42
|
+
port: gateway.status().port,
|
|
43
|
+
sync: lastSyncResult,
|
|
44
|
+
warnings: lastSyncResult?.warnings || []
|
|
45
|
+
};
|
|
46
|
+
}
|
|
10
47
|
const preserveStartTime = options.preserveStartTime || false;
|
|
11
48
|
const config = loadConfig();
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
49
|
+
const wasEnabled = isManagedOmpModeEnabled();
|
|
50
|
+
const previousState = loadManagedOmpModeState?.() || {
|
|
51
|
+
activeChannelId: loadManagedOmpActiveChannelId()
|
|
52
|
+
};
|
|
53
|
+
const channels = getEnabledChannels();
|
|
54
|
+
const secret = crypto.randomBytes(32).toString('hex');
|
|
55
|
+
const desiredPort = options.port ?? config.ports?.ompProxy ?? 20092;
|
|
56
|
+
const gatewayStatus = await gateway.start({
|
|
57
|
+
host: '127.0.0.1',
|
|
58
|
+
port: desiredPort,
|
|
59
|
+
secret
|
|
60
|
+
});
|
|
61
|
+
currentPort = gatewayStatus.port;
|
|
62
|
+
try {
|
|
63
|
+
const oauthSupport = await probeOmpAuthGateways(channels);
|
|
64
|
+
const descriptor = {
|
|
65
|
+
...gateway.descriptor(),
|
|
66
|
+
supportedOAuthChannelIds: oauthSupport.supportedOAuthChannelIds
|
|
67
|
+
};
|
|
68
|
+
lastRoutingResult = prepareManagedOmpChannels(channels, descriptor);
|
|
69
|
+
enableManagedOmpMode(options.activeChannelId, descriptor);
|
|
70
|
+
lastSyncResult = syncManagedOmpProviders(channels, {
|
|
71
|
+
gateway: descriptor,
|
|
72
|
+
activeChannelId: options.activeChannelId
|
|
73
|
+
});
|
|
74
|
+
if (oauthSupport.warnings.length > 0) {
|
|
75
|
+
lastSyncResult = {
|
|
76
|
+
...lastSyncResult,
|
|
77
|
+
warnings: [
|
|
78
|
+
...(lastSyncResult?.warnings || []),
|
|
79
|
+
...oauthSupport.warnings
|
|
80
|
+
]
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
} catch (error) {
|
|
84
|
+
if (wasEnabled) {
|
|
85
|
+
enableManagedOmpMode(previousState.activeChannelId, previousState.gateway || null);
|
|
86
|
+
} else {
|
|
87
|
+
disableManagedOmpMode();
|
|
88
|
+
}
|
|
89
|
+
await gateway.stop().catch(() => {});
|
|
90
|
+
currentPort = null;
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
startOmpSessionLogObserver();
|
|
15
94
|
saveProxyStartTime('omp', preserveStartTime);
|
|
16
95
|
return {
|
|
17
96
|
success: true,
|
|
@@ -21,12 +100,34 @@ async function startOmpProxyServer(options = {}) {
|
|
|
21
100
|
};
|
|
22
101
|
}
|
|
23
102
|
|
|
24
|
-
|
|
103
|
+
function startOmpProxyServer(options = {}) {
|
|
104
|
+
return withLifecycleLock(() => startOmpProxyServerUnlocked(options));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function stopOmpProxyServerUnlocked(options = {}) {
|
|
25
108
|
const clearStartTime = options.clearStartTime !== false;
|
|
26
109
|
const stoppedPort = currentPort;
|
|
27
|
-
|
|
28
|
-
|
|
110
|
+
if (!gateway.status().listening && !isManagedOmpModeEnabled()) {
|
|
111
|
+
return {
|
|
112
|
+
success: true,
|
|
113
|
+
port: stoppedPort,
|
|
114
|
+
sync: lastSyncResult,
|
|
115
|
+
warnings: lastSyncResult?.warnings || []
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
gateway.beginDraining();
|
|
119
|
+
stopOmpSessionLogObserver();
|
|
120
|
+
try {
|
|
121
|
+
lastSyncResult = disableManagedOmpProviders();
|
|
122
|
+
} catch (error) {
|
|
123
|
+
startOmpSessionLogObserver();
|
|
124
|
+
gateway.cancelDraining();
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
disableManagedOmpMode();
|
|
128
|
+
await gateway.stop({ forceAfterMs: options.forceAfterMs });
|
|
29
129
|
currentPort = null;
|
|
130
|
+
lastRoutingResult = { routes: [], unsupportedChannels: [] };
|
|
30
131
|
if (clearStartTime) {
|
|
31
132
|
clearProxyStartTime('omp');
|
|
32
133
|
}
|
|
@@ -38,8 +139,14 @@ async function stopOmpProxyServer(options = {}) {
|
|
|
38
139
|
};
|
|
39
140
|
}
|
|
40
141
|
|
|
142
|
+
function stopOmpProxyServer(options = {}) {
|
|
143
|
+
return withLifecycleLock(() => stopOmpProxyServerUnlocked(options));
|
|
144
|
+
}
|
|
145
|
+
|
|
41
146
|
function getOmpProxyStatus() {
|
|
42
147
|
const config = loadConfig();
|
|
148
|
+
const gatewayStatus = gateway.status();
|
|
149
|
+
const running = gatewayStatus.listening;
|
|
43
150
|
const startTime = getProxyStartTime('omp', { allowRecovery: running });
|
|
44
151
|
const runtime = getProxyRuntime('omp', { allowRecovery: running });
|
|
45
152
|
const enabledCount = getEnabledChannels().length;
|
|
@@ -50,11 +157,23 @@ function getOmpProxyStatus() {
|
|
|
50
157
|
defaultPort: config.ports?.ompProxy || 20092,
|
|
51
158
|
startTime,
|
|
52
159
|
runtime,
|
|
53
|
-
mode: '
|
|
160
|
+
mode: 'http-gateway',
|
|
161
|
+
listening: gatewayStatus.listening,
|
|
162
|
+
draining: gatewayStatus.draining,
|
|
163
|
+
inflightRequests: gatewayStatus.inflightRequests,
|
|
164
|
+
routingGroups: lastRoutingResult.routes.map(route => ({
|
|
165
|
+
providerKey: route.providerKey,
|
|
166
|
+
providerApi: route.providerApi,
|
|
167
|
+
routingGroup: route.routingGroup,
|
|
168
|
+
managedProviderId: route.managedProviderId,
|
|
169
|
+
channelsCount: route.channelIds.length
|
|
170
|
+
})),
|
|
171
|
+
unsupportedChannels: lastRoutingResult.unsupportedChannels,
|
|
54
172
|
enabledChannelsCount: enabledCount,
|
|
55
173
|
modelsPath: lastSyncResult?.modelsPath || lastSyncResult?.path || null,
|
|
56
174
|
backupPath: lastSyncResult?.backupPath || null,
|
|
57
175
|
modelsValidation: validation,
|
|
176
|
+
sessionLogObserver: getOmpSessionLogObserverStatus(),
|
|
58
177
|
warnings: lastSyncResult?.warnings || []
|
|
59
178
|
};
|
|
60
179
|
}
|
|
@@ -101,10 +101,35 @@ function refreshChannels(source = 'claude') {
|
|
|
101
101
|
});
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
function
|
|
104
|
+
function matchesAllocationRequest(channel, options = {}) {
|
|
105
|
+
const candidateIds = Array.isArray(options.candidateIds)
|
|
106
|
+
? new Set(options.candidateIds.filter(Boolean))
|
|
107
|
+
: null;
|
|
108
|
+
const excludedIds = new Set(
|
|
109
|
+
Array.isArray(options.excludeChannelIds) ? options.excludeChannelIds.filter(Boolean) : []
|
|
110
|
+
);
|
|
111
|
+
if (candidateIds && !candidateIds.has(channel.id)) return false;
|
|
112
|
+
if (excludedIds.has(channel.id)) return false;
|
|
113
|
+
if (options.routingGroup && String(channel.routingGroup || channel.id) !== String(options.routingGroup)) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
if (options.providerKey && String(channel.providerKey || channel.provider || '') !== String(options.providerKey)) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
if (
|
|
120
|
+
options.providerApi
|
|
121
|
+
&& String(channel.providerApi || channel.api || channel.wireApi || 'openai-completions') !== String(options.providerApi)
|
|
122
|
+
) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function getAvailableChannels(source = 'claude', options = {}) {
|
|
105
129
|
refreshChannels(source);
|
|
106
130
|
const state = getState(source);
|
|
107
131
|
return state.channels.filter(ch => {
|
|
132
|
+
if (!matchesAllocationRequest(ch, options)) return false;
|
|
108
133
|
if (!isChannelAvailable(ch.id, source)) {
|
|
109
134
|
return false;
|
|
110
135
|
}
|
|
@@ -132,7 +157,7 @@ function tryAllocate(source = 'claude', options = {}) {
|
|
|
132
157
|
const state = getState(source);
|
|
133
158
|
const sessionId = options.sessionId;
|
|
134
159
|
const enableSessionBinding = options.enableSessionBinding !== false; // 默认开启
|
|
135
|
-
const available = getAvailableChannels(source);
|
|
160
|
+
const available = getAvailableChannels(source, options);
|
|
136
161
|
if (!available.length) {
|
|
137
162
|
return null;
|
|
138
163
|
}
|
|
@@ -169,6 +194,7 @@ function drainQueue(source = 'claude') {
|
|
|
169
194
|
if (channel) {
|
|
170
195
|
clearTimeout(entry.timer);
|
|
171
196
|
state.queue.splice(i, 1);
|
|
197
|
+
entry.cleanup?.();
|
|
172
198
|
entry.resolve(channel);
|
|
173
199
|
return drainQueue(source);
|
|
174
200
|
}
|
|
@@ -178,6 +204,9 @@ function drainQueue(source = 'claude') {
|
|
|
178
204
|
function allocateChannel(options = {}) {
|
|
179
205
|
const source = options.source || 'claude';
|
|
180
206
|
const state = getState(source);
|
|
207
|
+
if (options.signal?.aborted) {
|
|
208
|
+
return Promise.reject(Object.assign(new Error('Channel allocation aborted'), { name: 'AbortError' }));
|
|
209
|
+
}
|
|
181
210
|
const channel = tryAllocate(source, options);
|
|
182
211
|
if (channel) {
|
|
183
212
|
return Promise.resolve(channel);
|
|
@@ -186,16 +215,31 @@ function allocateChannel(options = {}) {
|
|
|
186
215
|
if (!state.channels.length) {
|
|
187
216
|
return Promise.reject(new Error('暂无可用渠道,请先添加并启用至少一个渠道'));
|
|
188
217
|
}
|
|
218
|
+
if (!state.channels.some(ch => matchesAllocationRequest(ch, options))) {
|
|
219
|
+
return Promise.reject(new Error('没有匹配当前请求的可用渠道'));
|
|
220
|
+
}
|
|
189
221
|
|
|
190
222
|
// 检查是否所有渠道都被冻结
|
|
191
223
|
const allFrozen = state.channels.every(ch => !isChannelAvailable(ch.id, source));
|
|
192
224
|
|
|
193
225
|
return new Promise((resolve, reject) => {
|
|
226
|
+
let entry;
|
|
227
|
+
const cleanup = () => {
|
|
228
|
+
options.signal?.removeEventListener('abort', onAbort);
|
|
229
|
+
};
|
|
230
|
+
const removeEntry = () => {
|
|
231
|
+
const index = state.queue.indexOf(entry);
|
|
232
|
+
if (index !== -1) state.queue.splice(index, 1);
|
|
233
|
+
};
|
|
234
|
+
const onAbort = () => {
|
|
235
|
+
clearTimeout(timer);
|
|
236
|
+
removeEntry();
|
|
237
|
+
cleanup();
|
|
238
|
+
reject(Object.assign(new Error('Channel allocation aborted'), { name: 'AbortError' }));
|
|
239
|
+
};
|
|
194
240
|
const timer = setTimeout(() => {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
state.queue.splice(index, 1);
|
|
198
|
-
}
|
|
241
|
+
removeEntry();
|
|
242
|
+
cleanup();
|
|
199
243
|
// 根据实际情况返回更准确的错误信息
|
|
200
244
|
if (allFrozen) {
|
|
201
245
|
reject(new Error('所有渠道均已被冻结,请等待健康检查恢复或手动重置'));
|
|
@@ -204,13 +248,19 @@ function allocateChannel(options = {}) {
|
|
|
204
248
|
}
|
|
205
249
|
}, WAIT_TIMEOUT_MS);
|
|
206
250
|
|
|
207
|
-
|
|
251
|
+
entry = {
|
|
208
252
|
source,
|
|
209
253
|
options,
|
|
210
254
|
resolve,
|
|
211
255
|
reject,
|
|
212
|
-
timer
|
|
213
|
-
|
|
256
|
+
timer,
|
|
257
|
+
cleanup
|
|
258
|
+
};
|
|
259
|
+
state.queue.push(entry);
|
|
260
|
+
options.signal?.addEventListener('abort', onAbort, { once: true });
|
|
261
|
+
if (options.signal?.aborted) {
|
|
262
|
+
onAbort();
|
|
263
|
+
}
|
|
214
264
|
});
|
|
215
265
|
}
|
|
216
266
|
|