coding-tool-x 3.7.12 → 3.8.0
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
CHANGED
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
该项目遵循 [Semantic Versioning](https://semver.org/)。
|
|
6
6
|
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- **OMP 渠道失败不再冻结** - OMP 动态切换开启后,失败渠道不再进入冻结状态。OMP 网关按请求即时回退其他渠道,冻结会导致短暂故障的渠道在恢复前被排除在后续分配之外
|
|
11
|
+
- **OMP 动态切换凭据稳定** - 同步当前 OMP 渠道时不再把动态切换生成的本地网关 `base_url` 和 capability API Key 覆盖真实渠道配置;同一渠道的受管 provider 生成值现在复用持久化网关密钥保持稳定
|
|
12
|
+
|
|
7
13
|
## [3.6.7] - 2026-05-16
|
|
8
14
|
|
|
9
15
|
### Added
|
package/package.json
CHANGED
package/src/config/paths.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
const crypto = require('crypto');
|
|
2
1
|
const { loadConfig } = require('../config/loader');
|
|
3
2
|
const { saveProxyStartTime, clearProxyStartTime, getProxyStartTime, getProxyRuntime } = require('./services/proxy-runtime');
|
|
4
3
|
const {
|
|
@@ -10,7 +9,8 @@ const {
|
|
|
10
9
|
enableManagedOmpMode,
|
|
11
10
|
disableManagedOmpMode,
|
|
12
11
|
loadManagedOmpActiveChannelId,
|
|
13
|
-
loadManagedOmpModeState
|
|
12
|
+
loadManagedOmpModeState,
|
|
13
|
+
getOrCreateOmpGatewaySecret
|
|
14
14
|
} = require('./services/omp-channels');
|
|
15
15
|
const {
|
|
16
16
|
startOmpSessionLogObserver,
|
|
@@ -52,7 +52,7 @@ async function startOmpProxyServerUnlocked(options = {}) {
|
|
|
52
52
|
activeChannelId: loadManagedOmpActiveChannelId()
|
|
53
53
|
};
|
|
54
54
|
const channels = getEnabledChannels();
|
|
55
|
-
const secret =
|
|
55
|
+
const secret = getOrCreateOmpGatewaySecret();
|
|
56
56
|
const desiredPort = options.port ?? config.ports?.ompProxy ?? 20092;
|
|
57
57
|
const gatewayStatus = await gateway.start({
|
|
58
58
|
host: '127.0.0.1',
|
|
@@ -143,6 +143,11 @@ function recordFailure(channelId, source = 'claude', error) {
|
|
|
143
143
|
health.consecutiveSuccesses = 0;
|
|
144
144
|
health.lastCheckTime = now;
|
|
145
145
|
|
|
146
|
+
// omp 动态切换按请求即时回退其他渠道,失败渠道无需冻结(冻结会将其排除在后续分配之外)
|
|
147
|
+
if (source === 'omp') {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
146
151
|
// 如果当前是健康状态或检测中状态,检查是否需要冻结
|
|
147
152
|
if (health.status === 'healthy' || health.status === 'checking') {
|
|
148
153
|
if (health.consecutiveFailures >= healthConfig.failureThreshold) {
|
|
@@ -289,7 +289,7 @@ function upsertSyncedChannels({
|
|
|
289
289
|
result.warnings.push(`${toolType}: missing baseUrl, skipped current channel sync`);
|
|
290
290
|
continue;
|
|
291
291
|
}
|
|
292
|
-
if (!normalizeString(candidate.apiKey)) {
|
|
292
|
+
if (!normalizeString(candidate.apiKey) && candidate.authMode !== 'none') {
|
|
293
293
|
result.skipped += 1;
|
|
294
294
|
result.warnings.push(`${toolType}: missing API key or OAuth-only config, skipped current channel sync`);
|
|
295
295
|
continue;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
1
2
|
const fs = require('fs');
|
|
2
3
|
const path = require('path');
|
|
3
4
|
const { execFileSync } = require('child_process');
|
|
@@ -15,6 +16,7 @@ const {
|
|
|
15
16
|
} = require('./omp-settings-manager');
|
|
16
17
|
const {
|
|
17
18
|
createSkippedResult,
|
|
19
|
+
isLocalProxyBaseUrl,
|
|
18
20
|
resolveApiKeyValue,
|
|
19
21
|
resolveExistingActiveChannel,
|
|
20
22
|
upsertSyncedChannels
|
|
@@ -74,6 +76,54 @@ function writeManagedOmpModeState(filePath, state) {
|
|
|
74
76
|
}
|
|
75
77
|
}
|
|
76
78
|
|
|
79
|
+
function getOmpGatewaySecretPath() {
|
|
80
|
+
return PATHS.ompGatewaySecret
|
|
81
|
+
|| path.join(path.dirname(PATHS.activeChannel.omp), 'omp-gateway-secret');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function writeOmpGatewaySecret(filePath, secret) {
|
|
85
|
+
const dir = path.dirname(filePath);
|
|
86
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
87
|
+
const temporaryPath = `${filePath}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
88
|
+
try {
|
|
89
|
+
fs.writeFileSync(temporaryPath, `${secret}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
90
|
+
fs.chmodSync(temporaryPath, 0o600);
|
|
91
|
+
fs.renameSync(temporaryPath, filePath);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
if (fs.existsSync(temporaryPath)) fs.unlinkSync(temporaryPath);
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function getOrCreateOmpGatewaySecret() {
|
|
99
|
+
ensureStorageDirMigrated();
|
|
100
|
+
const filePath = getOmpGatewaySecretPath();
|
|
101
|
+
const previousStateSecret = loadManagedOmpModeState()?.gateway?.secret;
|
|
102
|
+
const fileExists = fs.existsSync(filePath);
|
|
103
|
+
|
|
104
|
+
if (fileExists) {
|
|
105
|
+
try {
|
|
106
|
+
const stored = fs.readFileSync(filePath, 'utf8').trim();
|
|
107
|
+
if (stored) return stored;
|
|
108
|
+
} catch (error) {
|
|
109
|
+
if (previousStateSecret) {
|
|
110
|
+
writeOmpGatewaySecret(filePath, previousStateSecret);
|
|
111
|
+
return previousStateSecret;
|
|
112
|
+
}
|
|
113
|
+
throw new Error(`Failed to read persisted OMP gateway secret: ${error.message}`);
|
|
114
|
+
}
|
|
115
|
+
if (!previousStateSecret) {
|
|
116
|
+
throw new Error('Persisted OMP gateway secret is empty');
|
|
117
|
+
}
|
|
118
|
+
writeOmpGatewaySecret(filePath, previousStateSecret);
|
|
119
|
+
return previousStateSecret;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const secret = String(previousStateSecret || crypto.randomBytes(32).toString('hex'));
|
|
123
|
+
writeOmpGatewaySecret(filePath, secret);
|
|
124
|
+
return secret;
|
|
125
|
+
}
|
|
126
|
+
|
|
77
127
|
function enableManagedOmpMode(activeChannelId = null, gateway = null) {
|
|
78
128
|
ensureStorageDirMigrated();
|
|
79
129
|
const filePath = PATHS.activeChannel.omp;
|
|
@@ -267,6 +317,24 @@ function getOriginalOmpProviderId(providerId = '') {
|
|
|
267
317
|
return normalized.startsWith('ctx-') ? normalized.slice(4) : normalized;
|
|
268
318
|
}
|
|
269
319
|
|
|
320
|
+
function isManagedOmpProviderId(providerId = '') {
|
|
321
|
+
return String(providerId || '').trim().startsWith('ctx-');
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function isGeneratedOmpApiKey(value = '') {
|
|
325
|
+
return /^ctx_[a-f0-9]{40}$/i.test(String(value || '').trim());
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function isGeneratedOmpBaseUrl(value = '') {
|
|
329
|
+
const raw = String(value || '').trim();
|
|
330
|
+
if (!isLocalProxyBaseUrl(raw)) return false;
|
|
331
|
+
try {
|
|
332
|
+
return /^\/omp\/[a-f0-9]{24}(?:\/|$)/i.test(new URL(raw).pathname);
|
|
333
|
+
} catch {
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
270
338
|
function normalizeOmpModelId(value = '') {
|
|
271
339
|
return String(value || '').trim().replace(OMP_THINKING_SUFFIX_RE, '');
|
|
272
340
|
}
|
|
@@ -355,6 +423,28 @@ function collectOmpSyncSelections(modelsConfig = {}, settings = {}) {
|
|
|
355
423
|
return [...selections.values()];
|
|
356
424
|
}
|
|
357
425
|
|
|
426
|
+
function findOmpOriginalProvider(providers = {}, providerId = '') {
|
|
427
|
+
const normalizedProviderId = normalizeProviderId(getOriginalOmpProviderId(providerId));
|
|
428
|
+
const candidates = Object.entries(providers)
|
|
429
|
+
.filter(([key]) => !isManagedOmpProviderId(key))
|
|
430
|
+
.map(([id, provider]) => ({
|
|
431
|
+
id,
|
|
432
|
+
provider,
|
|
433
|
+
normalizedId: normalizeProviderId(id)
|
|
434
|
+
}));
|
|
435
|
+
const exactMatches = candidates.filter(item => item.normalizedId === normalizedProviderId);
|
|
436
|
+
if (exactMatches.length === 1) return exactMatches[0];
|
|
437
|
+
if (exactMatches.length > 1) return null;
|
|
438
|
+
|
|
439
|
+
const prefixMatches = candidates
|
|
440
|
+
.filter(item => normalizedProviderId.startsWith(`${item.normalizedId}-`))
|
|
441
|
+
.sort((left, right) => right.normalizedId.length - left.normalizedId.length);
|
|
442
|
+
if (prefixMatches.length === 0) return null;
|
|
443
|
+
const longestLength = prefixMatches[0].normalizedId.length;
|
|
444
|
+
const longestMatches = prefixMatches.filter(item => item.normalizedId.length === longestLength);
|
|
445
|
+
return longestMatches.length === 1 ? longestMatches[0] : null;
|
|
446
|
+
}
|
|
447
|
+
|
|
358
448
|
function findOmpExistingChannel(channels = [], providerId = '', baseUrl = '') {
|
|
359
449
|
const rawProvider = String(providerId || '').trim();
|
|
360
450
|
const originalProvider = getOriginalOmpProviderId(rawProvider);
|
|
@@ -444,9 +534,18 @@ function readOmpApiKeyCredential(providerId = '') {
|
|
|
444
534
|
return { value: '', source: '', envName: '' };
|
|
445
535
|
}
|
|
446
536
|
|
|
537
|
+
function resolveOmpAuthMode(provider = {}, sourceProvider = {}, existing = null) {
|
|
538
|
+
const rawAuth = String(sourceProvider.auth || provider.auth || '').trim().toLowerCase();
|
|
539
|
+
if (rawAuth === 'none') return 'none';
|
|
540
|
+
if (existing?.authMode === 'none') return 'none';
|
|
541
|
+
if (existing?.authMode === 'oauth') return 'oauth';
|
|
542
|
+
return 'api_key';
|
|
543
|
+
}
|
|
544
|
+
|
|
447
545
|
function buildOmpSyncCandidate(modelsConfig, selection, channels) {
|
|
546
|
+
const providers = getOmpProviders(modelsConfig);
|
|
448
547
|
const providerId = selection?.providerId || '';
|
|
449
|
-
const provider =
|
|
548
|
+
const provider = providers[providerId] || null;
|
|
450
549
|
if (!provider || typeof provider !== 'object') {
|
|
451
550
|
return {
|
|
452
551
|
skip: true,
|
|
@@ -454,14 +553,42 @@ function buildOmpSyncCandidate(modelsConfig, selection, channels) {
|
|
|
454
553
|
};
|
|
455
554
|
}
|
|
456
555
|
|
|
457
|
-
const
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
|
|
556
|
+
const managedProvider = isManagedOmpProviderId(providerId);
|
|
557
|
+
const originalProviderEntry = managedProvider
|
|
558
|
+
? findOmpOriginalProvider(providers, providerId)
|
|
559
|
+
: null;
|
|
560
|
+
const sourceProvider = originalProviderEntry?.provider || (managedProvider ? {} : provider);
|
|
561
|
+
const originalProviderId = originalProviderEntry?.id || getOriginalOmpProviderId(providerId);
|
|
562
|
+
const providerBaseUrl = String(sourceProvider.baseUrl || sourceProvider.base_url || '').trim();
|
|
563
|
+
const managedBaseUrl = String(provider.baseUrl || provider.base_url || '').trim();
|
|
564
|
+
const existing = findOmpExistingChannel(
|
|
565
|
+
channels,
|
|
566
|
+
originalProviderId || providerId,
|
|
567
|
+
managedProvider ? '' : managedBaseUrl
|
|
568
|
+
);
|
|
569
|
+
const baseUrl = managedProvider && (
|
|
570
|
+
!providerBaseUrl || isGeneratedOmpBaseUrl(providerBaseUrl)
|
|
571
|
+
)
|
|
572
|
+
? existing?.baseUrl || ''
|
|
573
|
+
: providerBaseUrl || existing?.baseUrl || managedBaseUrl;
|
|
574
|
+
const authMode = resolveOmpAuthMode(provider, sourceProvider, existing);
|
|
575
|
+
|
|
576
|
+
let credential = resolveApiKeyValue(sourceProvider.apiKey || sourceProvider.api_key || '');
|
|
461
577
|
if (!credential.value) {
|
|
462
|
-
credential = readOmpApiKeyCredential(providerId);
|
|
578
|
+
credential = readOmpApiKeyCredential(originalProviderId || providerId);
|
|
579
|
+
}
|
|
580
|
+
const existingCredential = resolveApiKeyValue(existing?.apiKey || '');
|
|
581
|
+
const apiKey = credential.value && !isGeneratedOmpApiKey(credential.value)
|
|
582
|
+
? credential.value
|
|
583
|
+
: existingCredential.value && !isGeneratedOmpApiKey(existingCredential.value)
|
|
584
|
+
? existingCredential.value
|
|
585
|
+
: '';
|
|
586
|
+
if (!credential.value || isGeneratedOmpApiKey(credential.value)) {
|
|
587
|
+
credential = existingCredential.value && !isGeneratedOmpApiKey(existingCredential.value)
|
|
588
|
+
? { ...existingCredential, source: 'existing-channel' }
|
|
589
|
+
: credential;
|
|
463
590
|
}
|
|
464
|
-
|
|
591
|
+
|
|
465
592
|
const providerModelIds = collectOmpModelIds(provider);
|
|
466
593
|
const selectedModelIds = Array.isArray(selection?.modelIds) ? selection.modelIds : [];
|
|
467
594
|
const allowedModels = selectedModelIds.length > 0 ? selectedModelIds : providerModelIds;
|
|
@@ -470,11 +597,11 @@ function buildOmpSyncCandidate(modelsConfig, selection, channels) {
|
|
|
470
597
|
|| existing?.model
|
|
471
598
|
|| allowedModels[0]
|
|
472
599
|
|| null;
|
|
473
|
-
if (!apiKey) {
|
|
600
|
+
if (!baseUrl || (authMode !== 'none' && !apiKey)) {
|
|
474
601
|
return {
|
|
475
602
|
skip: true,
|
|
476
603
|
channel: existing || null,
|
|
477
|
-
warning: `OMP 当前 provider "${providerId}"
|
|
604
|
+
warning: `OMP 当前 provider "${providerId}" 缺少可解析的上游 Base URL 或 API Key,OAuth/登录态渠道不支持同步导入。`
|
|
478
605
|
};
|
|
479
606
|
}
|
|
480
607
|
|
|
@@ -482,11 +609,11 @@ function buildOmpSyncCandidate(modelsConfig, selection, channels) {
|
|
|
482
609
|
name: existing?.name || originalProviderId || providerId,
|
|
483
610
|
providerKey: originalProviderId || providerId,
|
|
484
611
|
baseUrl,
|
|
485
|
-
apiKey,
|
|
486
|
-
providerApi: normalizeProviderApi(provider.api),
|
|
487
|
-
wireApi: provider.api || 'openai-completions',
|
|
488
|
-
authMode
|
|
489
|
-
oauthProviderId: '',
|
|
612
|
+
apiKey: authMode === 'none' ? '' : apiKey,
|
|
613
|
+
providerApi: normalizeProviderApi(sourceProvider.api || provider.api),
|
|
614
|
+
wireApi: sourceProvider.api || provider.api || 'openai-completions',
|
|
615
|
+
authMode,
|
|
616
|
+
oauthProviderId: authMode === 'oauth' ? (existing?.oauthProviderId || originalProviderId || providerId) : '',
|
|
490
617
|
model: preferredModel,
|
|
491
618
|
allowedModels,
|
|
492
619
|
models: filterOmpProviderModels(provider.models, allowedModels),
|
|
@@ -552,6 +679,7 @@ module.exports = {
|
|
|
552
679
|
syncManagedProviderExtension: (channels) => service.syncManagedProviderExtension(channels),
|
|
553
680
|
disableManagedProviderExtension: () => service.disableManagedProviderExtension(),
|
|
554
681
|
isManagedOmpModeEnabled,
|
|
682
|
+
getOrCreateOmpGatewaySecret,
|
|
555
683
|
enableManagedOmpMode,
|
|
556
684
|
disableManagedOmpMode,
|
|
557
685
|
loadManagedOmpModeState,
|