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.
Files changed (33) hide show
  1. package/dist/web/assets/{Analytics-CkUtnoz9.js → Analytics-Ck_00Brj.js} +1 -1
  2. package/dist/web/assets/{ConfigTemplates-BwPqhE-N.js → ConfigTemplates-Cpw2vUa8.js} +1 -1
  3. package/dist/web/assets/Home-Bft3Kztl.js +1 -0
  4. package/dist/web/assets/Home-CHZJkkjH.css +1 -0
  5. package/dist/web/assets/{PluginManager-JMXWl37b.js → PluginManager-2RlvEJCT.js} +1 -1
  6. package/dist/web/assets/{ProjectList-CZwDgVvt.js → ProjectList-_ecCngpl.js} +1 -1
  7. package/dist/web/assets/{SessionList-2fZmN7Au.js → SessionList-DlWcw3hO.js} +1 -1
  8. package/dist/web/assets/{SkillManager-Bhzhkeis.js → SkillManager-CnY7IXkX.js} +1 -1
  9. package/dist/web/assets/{WorkspaceManager-B3lhhBD0.js → WorkspaceManager-3mqghi09.js} +1 -1
  10. package/dist/web/assets/{index-C5nLTbOk.css → index-BceXgSSe.css} +1 -1
  11. package/dist/web/assets/index-k5oP7c-A.js +2 -0
  12. package/dist/web/index.html +2 -2
  13. package/package.json +1 -1
  14. package/src/commands/toggle-proxy.js +7 -8
  15. package/src/commands/ui.js +11 -1
  16. package/src/config/default.js +1 -1
  17. package/src/index.js +41 -15
  18. package/src/server/api/omp-channels.js +6 -4
  19. package/src/server/api/omp-config.js +1 -1
  20. package/src/server/api/omp-proxy.js +13 -42
  21. package/src/server/index.js +3 -3
  22. package/src/server/omp-proxy-server.js +129 -10
  23. package/src/server/services/channel-scheduler.js +59 -9
  24. package/src/server/services/omp-auth-gateway-client.js +62 -0
  25. package/src/server/services/omp-channels.js +109 -12
  26. package/src/server/services/omp-gateway-routing.js +295 -0
  27. package/src/server/services/omp-gateway.js +919 -0
  28. package/src/server/services/omp-session-log-observer.js +138 -0
  29. package/src/server/services/omp-sessions.js +80 -0
  30. package/src/server/services/omp-settings-manager.js +335 -86
  31. package/dist/web/assets/Home-BANl91zG.js +0 -1
  32. package/dist/web/assets/Home-CqnBXFxj.css +0 -1
  33. package/dist/web/assets/index-LYvv7wtx.js +0 -2
@@ -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-LYvv7wtx.js"></script>
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-C5nLTbOk.css">
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coding-tool-x",
3
- "version": "3.7.10",
3
+ "version": "3.7.11",
4
4
  "description": "Vibe Coding 增强工作助手 - 智能会话管理、动态渠道切换、全局搜索、实时监控",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -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('• 开启后会把已启用的 OMP 渠道写入受管 models.yml provider 配置'));
209
- console.log(chalk.gray(`• 可以通过 Web UI 或"渠道管理"功能调整启用的 ${toolName} provider`));
210
- console.log(chalk.gray(`• 状态端口: http://127.0.0.1:${defaultPort}\n`));
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('• 启用后会写入 Coding-Tool 管理的 OMP models.yml provider 配置'));
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
- if (proxyResult.port) {
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(`• 状态端口: http://127.0.0.1:${proxyStatus.port || proxyStatus.defaultPort}\n`));
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}`));
@@ -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
- process.exit(1);
112
+ throw error;
103
113
  }
104
114
  }
105
115
 
@@ -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 托管 provider 状态端口占位
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
- eventBus.emitSync('cli:shutdown', {});
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
- if (err.code === 'EIO' || err.code === 'ENOTTY' || err.code === 'EPIPE') {
135
- process.exit(0);
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
- // 处理 SIGINT 信号(Ctrl+C)
141
- process.on('SIGINT', async () => {
142
- eventBus.emitSync('cli:shutdown', {});
143
- PluginManager.shutdownPlugins();
144
- process.exit(0);
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
- process.exit(1);
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
- console.error('程序出错:', error);
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: authMode === 'oauth' ? 'oauth' : 'api_key',
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 = authMode === 'oauth' ? 'oauth' : 'api_key';
429
- if (!apiKey && normalizedAuthMode !== 'oauth') {
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
- 'Provider channels are enabled by merging managed ctx-* providers into OMP models.yml.'
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, loadActiveChannelId());
56
+ const activeChannel = resolveActiveChannel(channels, loadManagedOmpActiveChannelId());
84
57
  res.json({
85
58
  proxy,
86
59
  config: {
87
- mode: 'models-yml-provider-config',
88
- nativeProxyProtocol: false
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 enable OMP models.yml provider config' });
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: 'models-yml-provider-config',
100
+ mode: 'http-gateway',
129
101
  sync: proxyResult.sync || null,
130
102
  warnings: proxyResult.warnings || [],
131
- message: `OMP models.yml provider config enabled, active channel: ${activeChannel.name}`
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);
@@ -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 models.yml 受管 provider 已自动启用,端口: ${result.port}`));
468
+ console.log(chalk.green(`[OK] OMP 动态网关已自动恢复: http://127.0.0.1:${result.port}`));
469
469
  } else {
470
- console.error(chalk.red(`[ERROR] OMP models.yml 受管 provider 恢复失败: ${result.error || 'Unknown error'}`));
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 models.yml 受管 provider 恢复失败: ${err.message}`));
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 { syncManagedOmpProviders, disableManagedOmpProviders, getEnabledChannels } = require('./services/omp-channels');
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
- async function startOmpProxyServer(options = {}) {
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
- currentPort = config.ports?.ompProxy || 20092;
13
- lastSyncResult = syncManagedOmpProviders();
14
- running = true;
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
- async function stopOmpProxyServer(options = {}) {
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
- lastSyncResult = disableManagedOmpProviders();
28
- running = false;
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: 'models-yml-provider-config',
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 getAvailableChannels(source = 'claude') {
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
- const index = state.queue.findIndex(item => item.timer === timer);
196
- if (index !== -1) {
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
- state.queue.push({
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