coding-tool-x 3.7.10 → 3.7.12
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 +3 -0
- package/README.md +1 -1
- package/dist/web/assets/{Analytics-CkUtnoz9.js → Analytics-8dRv1aYW.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-BwPqhE-N.js → ConfigTemplates-BQRXSICO.js} +1 -1
- package/dist/web/assets/Home-BtzWhKIa.js +1 -0
- package/dist/web/assets/Home-DYVy4cGZ.css +1 -0
- package/dist/web/assets/{PluginManager-JMXWl37b.js → PluginManager-BTa-Ygf6.js} +1 -1
- package/dist/web/assets/{ProjectList-CZwDgVvt.js → ProjectList-F_cB8ldA.js} +1 -1
- package/dist/web/assets/{SessionList-2fZmN7Au.js → SessionList-LOrbPQvu.js} +1 -1
- package/dist/web/assets/SkillManager-BsX8Qpj0.css +1 -0
- package/dist/web/assets/SkillManager-BzI_t-yV.js +1 -0
- package/dist/web/assets/{WorkspaceManager-B3lhhBD0.js → WorkspaceManager-Db9RGiz7.js} +1 -1
- package/dist/web/assets/index-DIWJmyL6.js +2 -0
- package/dist/web/assets/{index-C5nLTbOk.css → index-bJRG3RXH.css} +1 -1
- package/dist/web/index.html +2 -2
- package/docs/superpowers/plans/2026-08-01-omp-skill-scan-settings.md +759 -0
- package/docs/superpowers/specs/2026-08-01-omp-skill-scan-settings-design.md +224 -0
- package/package.json +1 -1
- package/src/commands/proxy-control.js +12 -12
- package/src/commands/toggle-proxy.js +16 -17
- package/src/commands/ui.js +11 -1
- package/src/config/default.js +1 -1
- package/src/config/paths.js +52 -29
- package/src/index.js +42 -16
- 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/api/plugins.js +119 -47
- package/src/server/api/skills.js +63 -10
- package/src/server/index.js +3 -3
- package/src/server/omp-proxy-server.js +142 -10
- package/src/server/services/channel-scheduler.js +59 -9
- package/src/server/services/model-definition-schema.js +2 -1
- package/src/server/services/omp-auth-gateway-client.js +62 -0
- package/src/server/services/omp-channels.js +168 -12
- package/src/server/services/omp-config.js +71 -6
- package/src/server/services/omp-gateway-routing.js +295 -0
- package/src/server/services/omp-gateway.js +919 -0
- package/src/server/services/omp-native-plugin-adapter.js +456 -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 +338 -87
- package/src/server/services/omp-skill-discovery.js +358 -0
- package/src/server/services/omp-skill-settings-service.js +97 -0
- package/src/server/services/pi-omp-migration.js +273 -0
- package/src/server/services/platform-resolution.js +44 -0
- package/src/server/services/plugins-service.js +85 -561
- package/src/server/services/project-path-validation.js +65 -0
- package/src/server/services/skill-service.js +169 -77
- package/dist/web/assets/Home-BANl91zG.js +0 -1
- package/dist/web/assets/Home-CqnBXFxj.css +0 -1
- package/dist/web/assets/SkillManager-Bhzhkeis.js +0 -1
- package/dist/web/assets/SkillManager-D7pd-d_P.css +0 -1
- package/dist/web/assets/index-LYvv7wtx.js +0 -2
|
@@ -1,17 +1,97 @@
|
|
|
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
|
+
activateStaticOmpChannel,
|
|
8
|
+
getEnabledChannels,
|
|
9
|
+
isManagedOmpModeEnabled,
|
|
10
|
+
enableManagedOmpMode,
|
|
11
|
+
disableManagedOmpMode,
|
|
12
|
+
loadManagedOmpActiveChannelId,
|
|
13
|
+
loadManagedOmpModeState
|
|
14
|
+
} = require('./services/omp-channels');
|
|
15
|
+
const {
|
|
16
|
+
startOmpSessionLogObserver,
|
|
17
|
+
stopOmpSessionLogObserver,
|
|
18
|
+
getOmpSessionLogObserverStatus
|
|
19
|
+
} = require('./services/omp-session-log-observer');
|
|
20
|
+
const { createOmpGateway } = require('./services/omp-gateway');
|
|
21
|
+
const { prepareManagedOmpChannels } = require('./services/omp-gateway-routing');
|
|
22
|
+
const { probeOmpAuthGateways } = require('./services/omp-auth-gateway-client');
|
|
4
23
|
|
|
5
|
-
let running = false;
|
|
6
24
|
let currentPort = null;
|
|
7
25
|
let lastSyncResult = null;
|
|
26
|
+
let lastRoutingResult = { routes: [], unsupportedChannels: [] };
|
|
27
|
+
let lifecycle = Promise.resolve();
|
|
8
28
|
|
|
9
|
-
|
|
29
|
+
const gateway = createOmpGateway({
|
|
30
|
+
getChannels: () => getEnabledChannels()
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
function withLifecycleLock(operation) {
|
|
34
|
+
const next = lifecycle.then(operation, operation);
|
|
35
|
+
lifecycle = next.catch(() => {});
|
|
36
|
+
return next;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function startOmpProxyServerUnlocked(options = {}) {
|
|
40
|
+
if (gateway.status().listening) {
|
|
41
|
+
return {
|
|
42
|
+
success: true,
|
|
43
|
+
port: gateway.status().port,
|
|
44
|
+
sync: lastSyncResult,
|
|
45
|
+
warnings: lastSyncResult?.warnings || []
|
|
46
|
+
};
|
|
47
|
+
}
|
|
10
48
|
const preserveStartTime = options.preserveStartTime || false;
|
|
11
49
|
const config = loadConfig();
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
50
|
+
const wasEnabled = isManagedOmpModeEnabled();
|
|
51
|
+
const previousState = loadManagedOmpModeState?.() || {
|
|
52
|
+
activeChannelId: loadManagedOmpActiveChannelId()
|
|
53
|
+
};
|
|
54
|
+
const channels = getEnabledChannels();
|
|
55
|
+
const secret = crypto.randomBytes(32).toString('hex');
|
|
56
|
+
const desiredPort = options.port ?? config.ports?.ompProxy ?? 20092;
|
|
57
|
+
const gatewayStatus = await gateway.start({
|
|
58
|
+
host: '127.0.0.1',
|
|
59
|
+
port: desiredPort,
|
|
60
|
+
secret
|
|
61
|
+
});
|
|
62
|
+
currentPort = gatewayStatus.port;
|
|
63
|
+
try {
|
|
64
|
+
const oauthSupport = await probeOmpAuthGateways(channels);
|
|
65
|
+
const descriptor = {
|
|
66
|
+
...gateway.descriptor(),
|
|
67
|
+
supportedOAuthChannelIds: oauthSupport.supportedOAuthChannelIds
|
|
68
|
+
};
|
|
69
|
+
lastRoutingResult = prepareManagedOmpChannels(channels, descriptor);
|
|
70
|
+
enableManagedOmpMode(options.activeChannelId, descriptor);
|
|
71
|
+
lastSyncResult = syncManagedOmpProviders(channels, {
|
|
72
|
+
gateway: descriptor,
|
|
73
|
+
activeChannelId: options.activeChannelId
|
|
74
|
+
});
|
|
75
|
+
if (oauthSupport.warnings.length > 0) {
|
|
76
|
+
lastSyncResult = {
|
|
77
|
+
...lastSyncResult,
|
|
78
|
+
warnings: [
|
|
79
|
+
...(lastSyncResult?.warnings || []),
|
|
80
|
+
...oauthSupport.warnings
|
|
81
|
+
]
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if (wasEnabled) {
|
|
86
|
+
enableManagedOmpMode(previousState.activeChannelId, previousState.gateway || null);
|
|
87
|
+
} else {
|
|
88
|
+
disableManagedOmpMode();
|
|
89
|
+
}
|
|
90
|
+
await gateway.stop().catch(() => {});
|
|
91
|
+
currentPort = null;
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
startOmpSessionLogObserver();
|
|
15
95
|
saveProxyStartTime('omp', preserveStartTime);
|
|
16
96
|
return {
|
|
17
97
|
success: true,
|
|
@@ -21,12 +101,46 @@ async function startOmpProxyServer(options = {}) {
|
|
|
21
101
|
};
|
|
22
102
|
}
|
|
23
103
|
|
|
24
|
-
|
|
104
|
+
function startOmpProxyServer(options = {}) {
|
|
105
|
+
return withLifecycleLock(() => startOmpProxyServerUnlocked(options));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function stopOmpProxyServerUnlocked(options = {}) {
|
|
25
109
|
const clearStartTime = options.clearStartTime !== false;
|
|
26
110
|
const stoppedPort = currentPort;
|
|
27
|
-
|
|
28
|
-
|
|
111
|
+
if (!gateway.status().listening && !isManagedOmpModeEnabled()) {
|
|
112
|
+
return {
|
|
113
|
+
success: true,
|
|
114
|
+
port: stoppedPort,
|
|
115
|
+
sync: lastSyncResult,
|
|
116
|
+
warnings: lastSyncResult?.warnings || []
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
const state = loadManagedOmpModeState?.() || {
|
|
120
|
+
activeChannelId: loadManagedOmpActiveChannelId()
|
|
121
|
+
};
|
|
122
|
+
const enabledChannels = getEnabledChannels();
|
|
123
|
+
const activeChannel = enabledChannels.find(channel => channel.id === state.activeChannelId)
|
|
124
|
+
|| enabledChannels[0]
|
|
125
|
+
|| null;
|
|
126
|
+
gateway.beginDraining();
|
|
127
|
+
stopOmpSessionLogObserver();
|
|
128
|
+
try {
|
|
129
|
+
if (activeChannel) {
|
|
130
|
+
const handoff = activateStaticOmpChannel(activeChannel.id);
|
|
131
|
+
lastSyncResult = handoff?.sync || lastSyncResult;
|
|
132
|
+
} else {
|
|
133
|
+
lastSyncResult = disableManagedOmpProviders();
|
|
134
|
+
}
|
|
135
|
+
} catch (error) {
|
|
136
|
+
startOmpSessionLogObserver();
|
|
137
|
+
gateway.cancelDraining();
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
disableManagedOmpMode();
|
|
141
|
+
await gateway.stop({ forceAfterMs: options.forceAfterMs });
|
|
29
142
|
currentPort = null;
|
|
143
|
+
lastRoutingResult = { routes: [], unsupportedChannels: [] };
|
|
30
144
|
if (clearStartTime) {
|
|
31
145
|
clearProxyStartTime('omp');
|
|
32
146
|
}
|
|
@@ -38,8 +152,14 @@ async function stopOmpProxyServer(options = {}) {
|
|
|
38
152
|
};
|
|
39
153
|
}
|
|
40
154
|
|
|
155
|
+
function stopOmpProxyServer(options = {}) {
|
|
156
|
+
return withLifecycleLock(() => stopOmpProxyServerUnlocked(options));
|
|
157
|
+
}
|
|
158
|
+
|
|
41
159
|
function getOmpProxyStatus() {
|
|
42
160
|
const config = loadConfig();
|
|
161
|
+
const gatewayStatus = gateway.status();
|
|
162
|
+
const running = gatewayStatus.listening;
|
|
43
163
|
const startTime = getProxyStartTime('omp', { allowRecovery: running });
|
|
44
164
|
const runtime = getProxyRuntime('omp', { allowRecovery: running });
|
|
45
165
|
const enabledCount = getEnabledChannels().length;
|
|
@@ -50,11 +170,23 @@ function getOmpProxyStatus() {
|
|
|
50
170
|
defaultPort: config.ports?.ompProxy || 20092,
|
|
51
171
|
startTime,
|
|
52
172
|
runtime,
|
|
53
|
-
mode: '
|
|
173
|
+
mode: 'http-gateway',
|
|
174
|
+
listening: gatewayStatus.listening,
|
|
175
|
+
draining: gatewayStatus.draining,
|
|
176
|
+
inflightRequests: gatewayStatus.inflightRequests,
|
|
177
|
+
routingGroups: lastRoutingResult.routes.map(route => ({
|
|
178
|
+
providerKey: route.providerKey,
|
|
179
|
+
providerApi: route.providerApi,
|
|
180
|
+
routingGroup: route.routingGroup,
|
|
181
|
+
managedProviderId: route.managedProviderId,
|
|
182
|
+
channelsCount: route.channelIds.length
|
|
183
|
+
})),
|
|
184
|
+
unsupportedChannels: lastRoutingResult.unsupportedChannels,
|
|
54
185
|
enabledChannelsCount: enabledCount,
|
|
55
186
|
modelsPath: lastSyncResult?.modelsPath || lastSyncResult?.path || null,
|
|
56
187
|
backupPath: lastSyncResult?.backupPath || null,
|
|
57
188
|
modelsValidation: validation,
|
|
189
|
+
sessionLogObserver: getOmpSessionLogObserverStatus(),
|
|
58
190
|
warnings: lastSyncResult?.warnings || []
|
|
59
191
|
};
|
|
60
192
|
}
|
|
@@ -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
|
|
|
@@ -390,7 +390,8 @@ function normalizeCatalogModelList(value) {
|
|
|
390
390
|
const warnings = [];
|
|
391
391
|
for (const rawModel of Array.isArray(value) ? value : []) {
|
|
392
392
|
const rawId = String(rawModel?.id || rawModel?.name || '').trim();
|
|
393
|
-
const
|
|
393
|
+
const { provider: _provider, selector: _selector, ...modelFields } = isPlainObject(rawModel) ? rawModel : {};
|
|
394
|
+
const normalized = normalizeModelSpec(modelFields, rawId);
|
|
394
395
|
if (!normalized.spec?.id) continue;
|
|
395
396
|
warnings.push(...normalized.warnings.map(message => `${normalized.spec.id}: ${message}`));
|
|
396
397
|
const key = normalized.spec.id.toLowerCase();
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const http = require('http');
|
|
4
|
+
const https = require('https');
|
|
5
|
+
|
|
6
|
+
function isPiNativeOAuthChannel(channel = {}) {
|
|
7
|
+
const transport = String(channel.transport || channel.providerConfig?.transport || '').trim();
|
|
8
|
+
return channel.authMode === 'oauth'
|
|
9
|
+
&& transport === 'pi-native'
|
|
10
|
+
&& Boolean(String(channel.apiKey || '').trim())
|
|
11
|
+
&& Boolean(String(channel.baseUrl || '').trim());
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function probeHealth(channel, timeoutMs) {
|
|
15
|
+
return new Promise((resolve) => {
|
|
16
|
+
let target;
|
|
17
|
+
try {
|
|
18
|
+
target = new URL('/healthz', channel.baseUrl);
|
|
19
|
+
} catch {
|
|
20
|
+
resolve(false);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const transport = target.protocol === 'https:' ? https : http;
|
|
24
|
+
const request = transport.request(target, {
|
|
25
|
+
method: 'GET',
|
|
26
|
+
headers: {
|
|
27
|
+
accept: 'application/json'
|
|
28
|
+
}
|
|
29
|
+
}, (response) => {
|
|
30
|
+
response.resume();
|
|
31
|
+
response.once('end', () => {
|
|
32
|
+
resolve((response.statusCode || 0) >= 200 && (response.statusCode || 0) < 300);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
request.setTimeout(timeoutMs, () => {
|
|
36
|
+
request.destroy(new Error('OMP auth gateway health probe timed out'));
|
|
37
|
+
});
|
|
38
|
+
request.once('error', () => resolve(false));
|
|
39
|
+
request.end();
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function probeOmpAuthGateways(channels = [], options = {}) {
|
|
44
|
+
const candidates = channels.filter(isPiNativeOAuthChannel);
|
|
45
|
+
const timeoutMs = Math.max(100, Number(options.timeoutMs) || 1000);
|
|
46
|
+
const results = await Promise.all(candidates.map(async (channel) => ({
|
|
47
|
+
channel,
|
|
48
|
+
healthy: await probeHealth(channel, timeoutMs)
|
|
49
|
+
})));
|
|
50
|
+
return {
|
|
51
|
+
supportedOAuthChannelIds: results
|
|
52
|
+
.filter(result => result.healthy)
|
|
53
|
+
.map(result => result.channel.id),
|
|
54
|
+
warnings: results
|
|
55
|
+
.filter(result => !result.healthy)
|
|
56
|
+
.map(result => `OMP OAuth auth gateway for channel "${result.channel.name || result.channel.id}" is unavailable; the native OMP provider remains active.`)
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = {
|
|
61
|
+
probeOmpAuthGateways
|
|
62
|
+
};
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const { execFileSync } = require('child_process');
|
|
4
|
-
const { PATHS } = require('../../config/paths');
|
|
4
|
+
const { PATHS, ensureStorageDirMigrated } = require('../../config/paths');
|
|
5
5
|
const BaseChannelService = require('./base/base-channel-service');
|
|
6
6
|
const ompConfig = require('./omp-config');
|
|
7
7
|
const {
|
|
8
8
|
writeManagedOmpProviders,
|
|
9
9
|
removeManagedOmpProviders,
|
|
10
|
-
isManagedOmpProvidersActive,
|
|
11
10
|
getLastManagedOmpSyncResult,
|
|
12
11
|
readModelsConfig,
|
|
13
12
|
readOmpSettingsConfig,
|
|
@@ -23,13 +22,100 @@ const {
|
|
|
23
22
|
|
|
24
23
|
const OMP_THINKING_SUFFIX_RE = /:(minimal|low|medium|high|xhigh|off)$/;
|
|
25
24
|
|
|
25
|
+
function clearOmpChannelBalanceCache(channel) {
|
|
26
|
+
try {
|
|
27
|
+
require('./channel-balance').clearChannelBalanceCache('omp', channel);
|
|
28
|
+
} catch (_) {
|
|
29
|
+
// Balance cache invalidation is an optimization; channel activation must still succeed.
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function selectLatestEnabledChannel(channels = []) {
|
|
34
|
+
const enabledChannels = (channels || []).filter(channel => channel?.enabled !== false);
|
|
35
|
+
if (enabledChannels.length === 0) return null;
|
|
36
|
+
return enabledChannels.reduce((latest, current) => {
|
|
37
|
+
const latestTs = Number(latest?.updatedAt || latest?.createdAt || 0);
|
|
38
|
+
const currentTs = Number(current?.updatedAt || current?.createdAt || 0);
|
|
39
|
+
return currentTs > latestTs ? current : latest;
|
|
40
|
+
}, enabledChannels[0]);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function loadManagedOmpModeState() {
|
|
44
|
+
ensureStorageDirMigrated();
|
|
45
|
+
try {
|
|
46
|
+
if (!fs.existsSync(PATHS.activeChannel.omp)) return null;
|
|
47
|
+
const data = JSON.parse(fs.readFileSync(PATHS.activeChannel.omp, 'utf8'));
|
|
48
|
+
return data && typeof data === 'object' && !Array.isArray(data) ? data : null;
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function loadManagedOmpActiveChannelId() {
|
|
55
|
+
return loadManagedOmpModeState()?.activeChannelId || null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isManagedOmpModeEnabled() {
|
|
59
|
+
ensureStorageDirMigrated();
|
|
60
|
+
return fs.existsSync(PATHS.activeChannel.omp);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function writeManagedOmpModeState(filePath, state) {
|
|
64
|
+
const temporaryPath = `${filePath}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
65
|
+
try {
|
|
66
|
+
fs.writeFileSync(temporaryPath, JSON.stringify(state, null, 2), {
|
|
67
|
+
encoding: 'utf8',
|
|
68
|
+
mode: 0o600
|
|
69
|
+
});
|
|
70
|
+
fs.renameSync(temporaryPath, filePath);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (fs.existsSync(temporaryPath)) fs.unlinkSync(temporaryPath);
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function enableManagedOmpMode(activeChannelId = null, gateway = null) {
|
|
78
|
+
ensureStorageDirMigrated();
|
|
79
|
+
const filePath = PATHS.activeChannel.omp;
|
|
80
|
+
const dir = path.dirname(filePath);
|
|
81
|
+
if (!fs.existsSync(dir)) {
|
|
82
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
83
|
+
}
|
|
84
|
+
const nextActiveChannelId = activeChannelId || loadManagedOmpActiveChannelId();
|
|
85
|
+
const previousState = loadManagedOmpModeState();
|
|
86
|
+
const nextGateway = gateway || previousState?.gateway || null;
|
|
87
|
+
const state = {
|
|
88
|
+
version: 2,
|
|
89
|
+
activeChannelId: nextActiveChannelId || null
|
|
90
|
+
};
|
|
91
|
+
if (nextGateway) {
|
|
92
|
+
state.gateway = {
|
|
93
|
+
host: String(nextGateway.host || '127.0.0.1'),
|
|
94
|
+
port: Number(nextGateway.port),
|
|
95
|
+
secret: String(nextGateway.secret || ''),
|
|
96
|
+
supportedOAuthChannelIds: Array.isArray(nextGateway.supportedOAuthChannelIds)
|
|
97
|
+
? [...nextGateway.supportedOAuthChannelIds]
|
|
98
|
+
: []
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
writeManagedOmpModeState(filePath, state);
|
|
102
|
+
return nextActiveChannelId || null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function disableManagedOmpMode() {
|
|
106
|
+
ensureStorageDirMigrated();
|
|
107
|
+
if (fs.existsSync(PATHS.activeChannel.omp)) {
|
|
108
|
+
fs.unlinkSync(PATHS.activeChannel.omp);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
26
112
|
class OmpChannelService extends BaseChannelService {
|
|
27
113
|
constructor() {
|
|
28
114
|
super({
|
|
29
115
|
platform: 'omp',
|
|
30
116
|
channelsFilePath: PATHS.channels.omp,
|
|
31
117
|
defaultGatewaySource: 'openai_compatible',
|
|
32
|
-
isProxyRunning: () =>
|
|
118
|
+
isProxyRunning: () => isManagedOmpModeEnabled()
|
|
33
119
|
});
|
|
34
120
|
}
|
|
35
121
|
|
|
@@ -37,10 +123,16 @@ class OmpChannelService extends BaseChannelService {
|
|
|
37
123
|
const normalized = super._applyDefaults(channel);
|
|
38
124
|
normalized.providerKey = normalized.providerKey || normalized.provider || normalized.name || normalized.id;
|
|
39
125
|
normalized.providerApi = normalized.providerApi || normalized.api || normalized.wireApi || 'openai-completions';
|
|
40
|
-
normalized.authMode =
|
|
126
|
+
normalized.authMode = ['api_key', 'oauth', 'none'].includes(normalized.authMode)
|
|
127
|
+
? normalized.authMode
|
|
128
|
+
: 'api_key';
|
|
129
|
+
normalized.routingGroup = String(normalized.routingGroup || '').trim();
|
|
41
130
|
normalized.oauthProviderId = normalized.oauthProviderId || (normalized.authMode === 'oauth' ? normalized.providerKey : '');
|
|
42
131
|
if (normalized.authMode === 'oauth') {
|
|
43
132
|
normalized.apiKey = normalized.apiKey || '';
|
|
133
|
+
} else if (normalized.authMode === 'none') {
|
|
134
|
+
normalized.apiKey = '';
|
|
135
|
+
normalized.oauthProviderId = '';
|
|
44
136
|
}
|
|
45
137
|
normalized.model = normalized.model || null;
|
|
46
138
|
normalized.models = Array.isArray(normalized.models) ? normalized.models : [];
|
|
@@ -59,28 +151,49 @@ class OmpChannelService extends BaseChannelService {
|
|
|
59
151
|
}
|
|
60
152
|
|
|
61
153
|
_applyToNativeSettings(channel) {
|
|
62
|
-
|
|
154
|
+
if (isManagedOmpModeEnabled()) {
|
|
155
|
+
const state = loadManagedOmpModeState();
|
|
156
|
+
enableManagedOmpMode(channel.id, state?.gateway || null);
|
|
157
|
+
writeManagedOmpProviders([channel], state?.gateway ? {
|
|
158
|
+
gateway: state.gateway,
|
|
159
|
+
activeChannelId: channel.id
|
|
160
|
+
} : {});
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
writeManagedOmpProviders([channel], {});
|
|
63
164
|
}
|
|
64
165
|
|
|
65
166
|
_onAfterCreate(_channel, allChannels) {
|
|
66
|
-
this.
|
|
167
|
+
this.syncOmpProvidersForCurrentMode(allChannels);
|
|
67
168
|
}
|
|
68
169
|
|
|
69
170
|
_onAfterUpdate(_oldChannel, _newChannel, allChannels) {
|
|
70
|
-
this.
|
|
171
|
+
this.syncOmpProvidersForCurrentMode(allChannels);
|
|
71
172
|
}
|
|
72
173
|
|
|
73
174
|
_onAfterDelete(_channel, allChannels) {
|
|
74
|
-
this.
|
|
175
|
+
this.syncOmpProvidersForCurrentMode(allChannels);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
syncOmpProvidersForCurrentMode(channels = this.getChannels().channels) {
|
|
179
|
+
if (isManagedOmpModeEnabled()) {
|
|
180
|
+
const state = loadManagedOmpModeState();
|
|
181
|
+
return this.syncManagedOmpProviders(channels, state?.gateway ? {
|
|
182
|
+
gateway: state.gateway,
|
|
183
|
+
activeChannelId: state.activeChannelId
|
|
184
|
+
} : {});
|
|
185
|
+
}
|
|
186
|
+
const activeChannel = selectLatestEnabledChannel(channels);
|
|
187
|
+
return this.syncManagedOmpProviders(activeChannel ? [activeChannel] : [], {});
|
|
75
188
|
}
|
|
76
189
|
|
|
77
|
-
syncManagedOmpProviders(channels = this.getChannels().channels) {
|
|
190
|
+
syncManagedOmpProviders(channels = this.getChannels().channels, options = {}) {
|
|
78
191
|
const enabledChannels = (channels || []).filter(channel => channel.enabled !== false);
|
|
79
192
|
if (enabledChannels.length === 0) {
|
|
80
193
|
removeManagedOmpProviders();
|
|
81
194
|
return getLastManagedOmpSyncResult();
|
|
82
195
|
}
|
|
83
|
-
writeManagedOmpProviders(enabledChannels);
|
|
196
|
+
writeManagedOmpProviders(enabledChannels, options);
|
|
84
197
|
return getLastManagedOmpSyncResult();
|
|
85
198
|
}
|
|
86
199
|
|
|
@@ -89,8 +202,45 @@ class OmpChannelService extends BaseChannelService {
|
|
|
89
202
|
return getLastManagedOmpSyncResult();
|
|
90
203
|
}
|
|
91
204
|
|
|
205
|
+
activateStaticOmpChannel(channelId) {
|
|
206
|
+
const data = this.loadChannels();
|
|
207
|
+
const channel = data.channels.find(item => item.id === channelId);
|
|
208
|
+
if (!channel) {
|
|
209
|
+
throw new Error('Channel not found');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const previousData = JSON.parse(JSON.stringify(data));
|
|
213
|
+
const wasEnabled = channel.enabled !== false;
|
|
214
|
+
data.channels.forEach(item => {
|
|
215
|
+
item.enabled = item.id === channelId;
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
this.saveChannels(data);
|
|
220
|
+
const sync = this.syncManagedOmpProviders([channel], {});
|
|
221
|
+
if (!wasEnabled) {
|
|
222
|
+
clearOmpChannelBalanceCache(channel);
|
|
223
|
+
}
|
|
224
|
+
return { channel, sync };
|
|
225
|
+
} catch (error) {
|
|
226
|
+
try {
|
|
227
|
+
this.saveChannels(previousData);
|
|
228
|
+
} catch (rollbackError) {
|
|
229
|
+
error.rollbackError = rollbackError;
|
|
230
|
+
}
|
|
231
|
+
throw error;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
applyChannelToSettings(channelId) {
|
|
236
|
+
if (isManagedOmpModeEnabled()) {
|
|
237
|
+
return super.applyChannelToSettings(channelId);
|
|
238
|
+
}
|
|
239
|
+
return this.activateStaticOmpChannel(channelId).channel;
|
|
240
|
+
}
|
|
241
|
+
|
|
92
242
|
syncManagedProviderExtension(channels = this.getChannels().channels) {
|
|
93
|
-
return this.
|
|
243
|
+
return this.syncOmpProvidersForCurrentMode(channels);
|
|
94
244
|
}
|
|
95
245
|
|
|
96
246
|
disableManagedProviderExtension() {
|
|
@@ -394,11 +544,17 @@ module.exports = {
|
|
|
394
544
|
getEnabledChannels: () => service.getEnabledChannels(),
|
|
395
545
|
saveChannelOrder: (order) => service.saveChannelOrder(order),
|
|
396
546
|
applyChannelToSettings: (id) => service.applyChannelToSettings(id),
|
|
547
|
+
activateStaticOmpChannel: (id) => service.activateStaticOmpChannel(id),
|
|
397
548
|
disableAllChannels: () => service.disableAllChannels(),
|
|
398
549
|
getEffectiveApiKey: (channel) => channel?.apiKey || null,
|
|
399
|
-
syncManagedOmpProviders: (channels) => service.syncManagedOmpProviders(channels),
|
|
550
|
+
syncManagedOmpProviders: (channels, options) => service.syncManagedOmpProviders(channels, options),
|
|
400
551
|
disableManagedOmpProviders: () => service.disableManagedOmpProviders(),
|
|
401
552
|
syncManagedProviderExtension: (channels) => service.syncManagedProviderExtension(channels),
|
|
402
553
|
disableManagedProviderExtension: () => service.disableManagedProviderExtension(),
|
|
554
|
+
isManagedOmpModeEnabled,
|
|
555
|
+
enableManagedOmpMode,
|
|
556
|
+
disableManagedOmpMode,
|
|
557
|
+
loadManagedOmpModeState,
|
|
558
|
+
loadManagedOmpActiveChannelId,
|
|
403
559
|
syncCurrentOmpChannel
|
|
404
560
|
};
|