coding-tool-x 3.7.2 → 3.7.3
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-D5kLOhUo.js → Analytics-BqDjJUJl.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-D_-16blp.js → ConfigTemplates-DV2KFuC4.js} +1 -1
- package/dist/web/assets/{Home-DM_56Fyr.js → Home-D7hovkk9.js} +1 -1
- package/dist/web/assets/{PluginManager-Cv_rv8Dh.js → PluginManager-Bd3hhc5A.js} +1 -1
- package/dist/web/assets/{ProjectList-H_m4Tx4E.js → ProjectList-BB4Whns8.js} +1 -1
- package/dist/web/assets/{SessionList-DcUQvsbR.js → SessionList-CvurKSRK.js} +1 -1
- package/dist/web/assets/{SkillManager-CRav3m2a.js → SkillManager-DQjRXOFg.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-B3f8R4EF.js → WorkspaceManager-CZeXnxu1.js} +1 -1
- package/dist/web/assets/{icons-Dop5j7Bf.js → icons-nGdSKQn2.js} +1 -1
- package/dist/web/assets/{index-Bnni1zPN.js → index-BovYCFsd.js} +2 -2
- package/dist/web/assets/index-kQCC5dlu.css +1 -0
- package/dist/web/index.html +3 -3
- package/package.json +1 -1
- package/src/server/api/channels.js +14 -1
- package/src/server/api/codex-channels.js +21 -1
- package/src/server/api/gemini-channels.js +21 -1
- package/src/server/api/omp-channels.js +17 -1
- package/src/server/api/opencode-channels.js +21 -1
- package/src/server/services/channel-sync-utils.js +368 -0
- package/src/server/services/channels.js +119 -0
- package/src/server/services/codex-channels.js +125 -0
- package/src/server/services/gemini-channels.js +123 -0
- package/src/server/services/omp-channels.js +299 -2
- package/src/server/services/omp-settings-manager.js +2 -0
- package/src/server/services/opencode-channels.js +172 -4
- package/dist/web/assets/index-CfMuHG8u.css +0 -1
|
@@ -5,6 +5,13 @@ const { PATHS, NATIVE_PATHS } = require('../../config/paths');
|
|
|
5
5
|
const { resolveChannelWebsiteUrl } = require('../../config/channel-preset-websites');
|
|
6
6
|
const { clearNativeOAuth } = require('./native-oauth-adapters');
|
|
7
7
|
const { normalizeGatewaySourceType } = require('./base/proxy-utils');
|
|
8
|
+
const {
|
|
9
|
+
createSkippedResult,
|
|
10
|
+
isLocalProxyBaseUrl,
|
|
11
|
+
resolveApiKeyValue,
|
|
12
|
+
resolveExistingActiveChannel,
|
|
13
|
+
upsertSyncedChannels
|
|
14
|
+
} = require('./channel-sync-utils');
|
|
8
15
|
|
|
9
16
|
const GEMINI_API_FORMATS = new Set(['gemini_api', 'vertex_ai_v1']);
|
|
10
17
|
|
|
@@ -516,6 +523,121 @@ function disableAllChannels() {
|
|
|
516
523
|
saveChannels(data);
|
|
517
524
|
}
|
|
518
525
|
|
|
526
|
+
function readGeminiSettings() {
|
|
527
|
+
const settingsPath = NATIVE_PATHS.gemini.settings;
|
|
528
|
+
if (!settingsPath || !fs.existsSync(settingsPath)) {
|
|
529
|
+
return {};
|
|
530
|
+
}
|
|
531
|
+
try {
|
|
532
|
+
return JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
533
|
+
} catch {
|
|
534
|
+
return {};
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function normalizeGeminiChannel(channel = {}) {
|
|
539
|
+
const normalized = {
|
|
540
|
+
...channel,
|
|
541
|
+
enabled: channel.enabled !== false,
|
|
542
|
+
weight: channel.weight || 1,
|
|
543
|
+
maxConcurrency: channel.maxConcurrency || null,
|
|
544
|
+
balanceToken: channel.balanceToken || '',
|
|
545
|
+
balanceUserId: channel.balanceUserId || null,
|
|
546
|
+
modelRedirects: channel.modelRedirects || [],
|
|
547
|
+
speedTestModel: channel.speedTestModel || null,
|
|
548
|
+
apiFormat: normalizeGeminiApiFormat(channel.apiFormat),
|
|
549
|
+
gatewaySourceType: normalizeGatewaySourceType(channel.gatewaySourceType, 'gemini')
|
|
550
|
+
};
|
|
551
|
+
normalized.websiteUrl = resolveChannelWebsiteUrl('gemini', normalized);
|
|
552
|
+
return normalized;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function loadStoredGeminiChannels() {
|
|
556
|
+
const filePath = getChannelsFilePath();
|
|
557
|
+
if (!fs.existsSync(filePath)) {
|
|
558
|
+
return { channels: [] };
|
|
559
|
+
}
|
|
560
|
+
try {
|
|
561
|
+
const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
562
|
+
return {
|
|
563
|
+
...data,
|
|
564
|
+
channels: Array.isArray(data?.channels)
|
|
565
|
+
? data.channels.map(normalizeGeminiChannel)
|
|
566
|
+
: []
|
|
567
|
+
};
|
|
568
|
+
} catch (error) {
|
|
569
|
+
console.error('[Gemini Channels] Failed to parse channels file for sync:', error.message);
|
|
570
|
+
return { channels: [] };
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function buildGeminiSyncCandidate(env, channels) {
|
|
575
|
+
const baseUrl = String(env.GOOGLE_GEMINI_BASE_URL || '').trim();
|
|
576
|
+
if (isLocalProxyBaseUrl(baseUrl)) {
|
|
577
|
+
const existing = resolveExistingActiveChannel('gemini', channels);
|
|
578
|
+
if (existing) {
|
|
579
|
+
return {
|
|
580
|
+
skip: true,
|
|
581
|
+
channel: existing,
|
|
582
|
+
warning: 'Gemini 当前配置指向 ctx 代理;对应渠道已在列表中,未重复导入。'
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
return {
|
|
586
|
+
skip: true,
|
|
587
|
+
warning: 'Gemini 当前配置指向 ctx 代理,但没有找到可匹配的现有渠道。'
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const settings = readGeminiSettings();
|
|
592
|
+
const selectedAuthType = String(settings?.security?.auth?.selectedType || '').trim();
|
|
593
|
+
const existing = baseUrl
|
|
594
|
+
? channels.find(channel => String(channel.baseUrl || '').trim() === baseUrl)
|
|
595
|
+
: null;
|
|
596
|
+
const credential = resolveApiKeyValue(env.GEMINI_API_KEY || '');
|
|
597
|
+
const apiKey = credential.value || existing?.apiKey || '';
|
|
598
|
+
if (!apiKey) {
|
|
599
|
+
return {
|
|
600
|
+
skip: true,
|
|
601
|
+
channel: existing || null,
|
|
602
|
+
warning: selectedAuthType.includes('oauth')
|
|
603
|
+
? 'Gemini 当前配置是 OAuth/登录态,OAuth 渠道不支持同步导入。'
|
|
604
|
+
: 'Gemini 当前 .env 缺少 GEMINI_API_KEY,无法同步导入。'
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
return {
|
|
609
|
+
name: existing?.name || 'Gemini 当前配置',
|
|
610
|
+
baseUrl: baseUrl || 'https://generativelanguage.googleapis.com/v1beta',
|
|
611
|
+
apiKey,
|
|
612
|
+
model: env.GEMINI_MODEL || existing?.model || 'gemini-2.5-pro',
|
|
613
|
+
apiFormat: existing?.apiFormat || 'gemini_api',
|
|
614
|
+
gatewaySourceType: existing?.gatewaySourceType || 'gemini',
|
|
615
|
+
credentialSource: credential.value ? credential.source : 'existing-channel'
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function syncCurrentGeminiChannel() {
|
|
620
|
+
const data = loadStoredGeminiChannels();
|
|
621
|
+
const channels = Array.isArray(data.channels) ? data.channels : [];
|
|
622
|
+
const env = readExistingGeminiEnv();
|
|
623
|
+
const candidate = buildGeminiSyncCandidate(env, channels);
|
|
624
|
+
if (candidate?.skip) {
|
|
625
|
+
return createSkippedResult('gemini', candidate.warning, candidate.channel);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
return upsertSyncedChannels({
|
|
629
|
+
toolType: 'gemini',
|
|
630
|
+
loadChannels: loadStoredGeminiChannels,
|
|
631
|
+
saveChannels,
|
|
632
|
+
applyDefaults: normalizeGeminiChannel,
|
|
633
|
+
candidates: [candidate],
|
|
634
|
+
matchers: [
|
|
635
|
+
(channel, current) => channel.baseUrl === current.baseUrl && channel.apiKey === current.apiKey,
|
|
636
|
+
(channel, current) => channel.baseUrl === current.baseUrl && channel.model === current.model
|
|
637
|
+
]
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
|
|
519
641
|
module.exports = {
|
|
520
642
|
getChannels,
|
|
521
643
|
createChannel,
|
|
@@ -529,6 +651,7 @@ module.exports = {
|
|
|
529
651
|
getGeminiDir,
|
|
530
652
|
applyChannelToSettings,
|
|
531
653
|
disableAllChannels,
|
|
654
|
+
syncCurrentGeminiChannel,
|
|
532
655
|
_test: {
|
|
533
656
|
normalizeGeminiApiFormat
|
|
534
657
|
}
|
|
@@ -1,11 +1,27 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { execFileSync } = require('child_process');
|
|
1
4
|
const { PATHS } = require('../../config/paths');
|
|
2
5
|
const BaseChannelService = require('./base/base-channel-service');
|
|
6
|
+
const ompConfig = require('./omp-config');
|
|
3
7
|
const {
|
|
4
8
|
writeManagedOmpProviders,
|
|
5
9
|
removeManagedOmpProviders,
|
|
6
10
|
isManagedOmpProvidersActive,
|
|
7
|
-
getLastManagedOmpSyncResult
|
|
11
|
+
getLastManagedOmpSyncResult,
|
|
12
|
+
readModelsConfig,
|
|
13
|
+
readOmpSettingsConfig,
|
|
14
|
+
normalizeProviderId,
|
|
15
|
+
normalizeProviderApi
|
|
8
16
|
} = require('./omp-settings-manager');
|
|
17
|
+
const {
|
|
18
|
+
createSkippedResult,
|
|
19
|
+
resolveApiKeyValue,
|
|
20
|
+
resolveExistingActiveChannel,
|
|
21
|
+
upsertSyncedChannels
|
|
22
|
+
} = require('./channel-sync-utils');
|
|
23
|
+
|
|
24
|
+
const OMP_THINKING_SUFFIX_RE = /:(minimal|low|medium|high|xhigh)$/;
|
|
9
25
|
|
|
10
26
|
class OmpChannelService extends BaseChannelService {
|
|
11
27
|
constructor() {
|
|
@@ -76,6 +92,286 @@ class OmpChannelService extends BaseChannelService {
|
|
|
76
92
|
|
|
77
93
|
const service = new OmpChannelService();
|
|
78
94
|
|
|
95
|
+
function splitOmpModelRef(value = '') {
|
|
96
|
+
const raw = String(value || '').trim();
|
|
97
|
+
if (!raw || !raw.includes('/')) {
|
|
98
|
+
return { providerId: '', modelId: normalizeOmpModelId(raw) };
|
|
99
|
+
}
|
|
100
|
+
const index = raw.indexOf('/');
|
|
101
|
+
return {
|
|
102
|
+
providerId: raw.slice(0, index).trim(),
|
|
103
|
+
modelId: normalizeOmpModelId(raw.slice(index + 1))
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function getOriginalOmpProviderId(providerId = '') {
|
|
108
|
+
const normalized = String(providerId || '').trim();
|
|
109
|
+
return normalized.startsWith('ctx-') ? normalized.slice(4) : normalized;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function normalizeOmpModelId(value = '') {
|
|
113
|
+
return String(value || '').trim().replace(OMP_THINKING_SUFFIX_RE, '');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function pushUnique(values, value) {
|
|
117
|
+
const item = String(value || '').trim();
|
|
118
|
+
if (item && !values.includes(item)) {
|
|
119
|
+
values.push(item);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function collectOmpModelIds(provider = {}) {
|
|
124
|
+
const models = Array.isArray(provider.models) ? provider.models : [];
|
|
125
|
+
return models
|
|
126
|
+
.map(model => typeof model === 'string' ? model : (model?.id || model?.name || ''))
|
|
127
|
+
.map(normalizeOmpModelId)
|
|
128
|
+
.filter(Boolean);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function getOmpProviders(modelsConfig = {}) {
|
|
132
|
+
return modelsConfig?.providers && typeof modelsConfig.providers === 'object' && !Array.isArray(modelsConfig.providers)
|
|
133
|
+
? modelsConfig.providers
|
|
134
|
+
: {};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function getKnownOmpProviderId(providers = {}, providerId = '') {
|
|
138
|
+
const raw = String(providerId || '').trim();
|
|
139
|
+
if (providers[raw]) return raw;
|
|
140
|
+
const normalizedRaw = normalizeProviderId(raw);
|
|
141
|
+
return Object.keys(providers).find((key) => {
|
|
142
|
+
return normalizeProviderId(key) === normalizedRaw
|
|
143
|
+
|| normalizeProviderId(getOriginalOmpProviderId(key)) === normalizedRaw;
|
|
144
|
+
}) || raw;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function createOmpSelection(providerId = '') {
|
|
148
|
+
return {
|
|
149
|
+
providerId,
|
|
150
|
+
modelIds: [],
|
|
151
|
+
roleModelIds: []
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function collectOmpSyncSelections(modelsConfig = {}, settings = {}) {
|
|
156
|
+
const providers = getOmpProviders(modelsConfig);
|
|
157
|
+
const selections = new Map();
|
|
158
|
+
|
|
159
|
+
const addRef = (value, source = 'enabled') => {
|
|
160
|
+
const { providerId, modelId } = splitOmpModelRef(value);
|
|
161
|
+
if (!providerId) return;
|
|
162
|
+
const knownProviderId = getKnownOmpProviderId(providers, providerId);
|
|
163
|
+
const selection = selections.get(knownProviderId) || createOmpSelection(knownProviderId);
|
|
164
|
+
if (modelId) {
|
|
165
|
+
pushUnique(selection.modelIds, modelId);
|
|
166
|
+
if (source === 'role') {
|
|
167
|
+
pushUnique(selection.roleModelIds, modelId);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
selections.set(knownProviderId, selection);
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
(Array.isArray(settings.enabledModels) ? settings.enabledModels : []).forEach(item => addRef(item, 'enabled'));
|
|
174
|
+
|
|
175
|
+
const roles = settings?.modelRoles && typeof settings.modelRoles === 'object' && !Array.isArray(settings.modelRoles)
|
|
176
|
+
? settings.modelRoles
|
|
177
|
+
: {};
|
|
178
|
+
addRef(roles.default, 'role');
|
|
179
|
+
Object.entries(roles).forEach(([roleName, value]) => {
|
|
180
|
+
if (roleName !== 'default') addRef(value, 'role');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const fallbackChains = settings?.retry?.fallbackChains;
|
|
184
|
+
if (fallbackChains && typeof fallbackChains === 'object' && !Array.isArray(fallbackChains)) {
|
|
185
|
+
Object.values(fallbackChains).forEach((chain) => {
|
|
186
|
+
(Array.isArray(chain) ? chain : [chain]).forEach(item => addRef(item, 'fallback'));
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (selections.size === 0) {
|
|
191
|
+
const providerIds = Object.keys(providers);
|
|
192
|
+
if (providerIds.length === 1) {
|
|
193
|
+
selections.set(providerIds[0], createOmpSelection(providerIds[0]));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return [...selections.values()];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function findOmpExistingChannel(channels = [], providerId = '', baseUrl = '') {
|
|
201
|
+
const rawProvider = String(providerId || '').trim();
|
|
202
|
+
const originalProvider = getOriginalOmpProviderId(rawProvider);
|
|
203
|
+
const normalizedProvider = normalizeProviderId(originalProvider || rawProvider);
|
|
204
|
+
return channels.find(channel => {
|
|
205
|
+
const keys = [
|
|
206
|
+
channel.providerKey,
|
|
207
|
+
channel.provider,
|
|
208
|
+
channel.name,
|
|
209
|
+
getOriginalOmpProviderId(channel.providerKey)
|
|
210
|
+
].map(value => normalizeProviderId(value || '')).filter(Boolean);
|
|
211
|
+
return keys.includes(normalizedProvider);
|
|
212
|
+
}) || channels.find(channel => baseUrl && channel.baseUrl === baseUrl) || null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function filterOmpProviderModels(models = [], allowedModels = []) {
|
|
216
|
+
if (!Array.isArray(models) || allowedModels.length === 0) return Array.isArray(models) ? models : [];
|
|
217
|
+
const allowed = new Set(allowedModels.map(normalizeOmpModelId));
|
|
218
|
+
return models.filter((model) => {
|
|
219
|
+
const id = normalizeOmpModelId(typeof model === 'string' ? model : (model?.id || model?.name || ''));
|
|
220
|
+
return allowed.has(id);
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function parseOmpApiKeyCredentialData(data) {
|
|
225
|
+
if (!data) return '';
|
|
226
|
+
try {
|
|
227
|
+
const parsed = JSON.parse(String(data));
|
|
228
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return '';
|
|
229
|
+
return String(parsed.key || parsed.apiKey || parsed.api_key || parsed.value || parsed.token || '').trim();
|
|
230
|
+
} catch {
|
|
231
|
+
return '';
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function runOmpAuthDbQuery(dbPath, providerIds = []) {
|
|
236
|
+
if (!dbPath || !fs.existsSync(dbPath) || providerIds.length === 0) return [];
|
|
237
|
+
const escapedProviders = providerIds
|
|
238
|
+
.map(providerId => `'${String(providerId || '').replace(/'/g, "''")}'`)
|
|
239
|
+
.join(',');
|
|
240
|
+
const sql = [
|
|
241
|
+
'select provider, credential_type, data',
|
|
242
|
+
'from auth_credentials',
|
|
243
|
+
`where provider in (${escapedProviders})`,
|
|
244
|
+
"and credential_type = 'api_key'",
|
|
245
|
+
'and (disabled_cause is null or disabled_cause = \'\')',
|
|
246
|
+
'order by updated_at desc',
|
|
247
|
+
'limit 5'
|
|
248
|
+
].join(' ');
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
const output = execFileSync('sqlite3', ['-json', dbPath, sql], {
|
|
252
|
+
encoding: 'utf8',
|
|
253
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
254
|
+
maxBuffer: 1024 * 1024,
|
|
255
|
+
windowsHide: true
|
|
256
|
+
}).trim();
|
|
257
|
+
if (!output) return [];
|
|
258
|
+
const parsed = JSON.parse(output);
|
|
259
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
260
|
+
} catch {
|
|
261
|
+
return [];
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function readOmpApiKeyCredential(providerId = '') {
|
|
266
|
+
const rawProviderId = String(providerId || '').trim();
|
|
267
|
+
const originalProviderId = getOriginalOmpProviderId(rawProviderId);
|
|
268
|
+
const providerIds = [...new Set([rawProviderId, originalProviderId].filter(Boolean))];
|
|
269
|
+
if (providerIds.length === 0) return { value: '', source: '', envName: '' };
|
|
270
|
+
|
|
271
|
+
let dbPath = '';
|
|
272
|
+
try {
|
|
273
|
+
const paths = typeof ompConfig.getOmpPaths === 'function' ? ompConfig.getOmpPaths() : {};
|
|
274
|
+
dbPath = path.join(paths.agentDir || path.dirname(paths.modelsYml || ''), 'agent.db');
|
|
275
|
+
} catch {
|
|
276
|
+
dbPath = '';
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const rows = runOmpAuthDbQuery(dbPath, providerIds);
|
|
280
|
+
for (const row of rows) {
|
|
281
|
+
const value = parseOmpApiKeyCredentialData(row?.data);
|
|
282
|
+
if (value) {
|
|
283
|
+
return { value, source: 'omp-auth-db', envName: '' };
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return { value: '', source: '', envName: '' };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function buildOmpSyncCandidate(modelsConfig, selection, channels) {
|
|
290
|
+
const providerId = selection?.providerId || '';
|
|
291
|
+
const provider = modelsConfig?.providers?.[providerId] || null;
|
|
292
|
+
if (!provider || typeof provider !== 'object') {
|
|
293
|
+
return {
|
|
294
|
+
skip: true,
|
|
295
|
+
warning: `OMP 当前 provider "${providerId}" 未在 models.yml 中定义。`
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const baseUrl = String(provider.baseUrl || provider.base_url || '').trim();
|
|
300
|
+
const originalProviderId = getOriginalOmpProviderId(providerId);
|
|
301
|
+
const existing = findOmpExistingChannel(channels, providerId, baseUrl);
|
|
302
|
+
let credential = resolveApiKeyValue(provider.apiKey || provider.api_key || '');
|
|
303
|
+
if (!credential.value) {
|
|
304
|
+
credential = readOmpApiKeyCredential(providerId);
|
|
305
|
+
}
|
|
306
|
+
const apiKey = credential.value || existing?.apiKey || '';
|
|
307
|
+
const providerModelIds = collectOmpModelIds(provider);
|
|
308
|
+
const selectedModelIds = Array.isArray(selection?.modelIds) ? selection.modelIds : [];
|
|
309
|
+
const allowedModels = selectedModelIds.length > 0 ? selectedModelIds : providerModelIds;
|
|
310
|
+
const preferredModel = selection?.roleModelIds?.[0]
|
|
311
|
+
|| selectedModelIds[0]
|
|
312
|
+
|| existing?.model
|
|
313
|
+
|| allowedModels[0]
|
|
314
|
+
|| null;
|
|
315
|
+
if (!apiKey) {
|
|
316
|
+
return {
|
|
317
|
+
skip: true,
|
|
318
|
+
channel: existing || null,
|
|
319
|
+
warning: `OMP 当前 provider "${providerId}" 缺少可解析 API Key,OAuth/登录态渠道不支持同步导入。`
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
name: existing?.name || originalProviderId || providerId,
|
|
325
|
+
providerKey: originalProviderId || providerId,
|
|
326
|
+
baseUrl,
|
|
327
|
+
apiKey,
|
|
328
|
+
providerApi: normalizeProviderApi(provider.api),
|
|
329
|
+
wireApi: provider.api || 'openai-completions',
|
|
330
|
+
authMode: 'api_key',
|
|
331
|
+
oauthProviderId: '',
|
|
332
|
+
model: preferredModel,
|
|
333
|
+
allowedModels,
|
|
334
|
+
models: filterOmpProviderModels(provider.models, allowedModels),
|
|
335
|
+
gatewaySourceType: existing?.gatewaySourceType || 'openai_compatible',
|
|
336
|
+
credentialSource: credential.value ? credential.source : 'existing-channel'
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function syncCurrentOmpChannel() {
|
|
341
|
+
let modelsConfig = { providers: {} };
|
|
342
|
+
let settings = {};
|
|
343
|
+
try {
|
|
344
|
+
modelsConfig = readModelsConfig();
|
|
345
|
+
settings = readOmpSettingsConfig();
|
|
346
|
+
} catch (error) {
|
|
347
|
+
return createSkippedResult('omp', `OMP 配置读取失败:${error.message}`);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const data = service.loadChannels();
|
|
351
|
+
const channels = Array.isArray(data.channels) ? data.channels : [];
|
|
352
|
+
const selections = collectOmpSyncSelections(modelsConfig, settings);
|
|
353
|
+
if (selections.length === 0) {
|
|
354
|
+
const active = resolveExistingActiveChannel('omp', channels);
|
|
355
|
+
if (active) {
|
|
356
|
+
return createSkippedResult('omp', 'OMP 当前配置未明确 provider;已找到当前面板中的 active 渠道,未重复导入。', active);
|
|
357
|
+
}
|
|
358
|
+
return createSkippedResult('omp', 'OMP 当前配置未明确 provider,无法同步当前渠道。');
|
|
359
|
+
}
|
|
360
|
+
const candidates = selections.map(selection => buildOmpSyncCandidate(modelsConfig, selection, channels));
|
|
361
|
+
|
|
362
|
+
return upsertSyncedChannels({
|
|
363
|
+
toolType: 'omp',
|
|
364
|
+
loadChannels: () => service.loadChannels(),
|
|
365
|
+
saveChannels: payload => service.saveChannels(payload),
|
|
366
|
+
applyDefaults: channel => service._applyDefaults(channel),
|
|
367
|
+
candidates,
|
|
368
|
+
matchers: [
|
|
369
|
+
(channel, current) => channel.providerKey && channel.providerKey === current.providerKey,
|
|
370
|
+
(channel, current) => channel.baseUrl === current.baseUrl && channel.apiKey === current.apiKey
|
|
371
|
+
]
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
79
375
|
module.exports = {
|
|
80
376
|
getChannels: () => service.getChannels(),
|
|
81
377
|
createChannel: (name, baseUrl, apiKey, extra = {}) => service.createChannel({
|
|
@@ -95,5 +391,6 @@ module.exports = {
|
|
|
95
391
|
syncManagedOmpProviders: (channels) => service.syncManagedOmpProviders(channels),
|
|
96
392
|
disableManagedOmpProviders: () => service.disableManagedOmpProviders(),
|
|
97
393
|
syncManagedProviderExtension: (channels) => service.syncManagedProviderExtension(channels),
|
|
98
|
-
disableManagedProviderExtension: () => service.disableManagedProviderExtension()
|
|
394
|
+
disableManagedProviderExtension: () => service.disableManagedProviderExtension(),
|
|
395
|
+
syncCurrentOmpChannel
|
|
99
396
|
};
|
|
@@ -965,6 +965,8 @@ module.exports = {
|
|
|
965
965
|
isManagedOmpProvidersActive,
|
|
966
966
|
isManagedProviderExtensionActive: isManagedOmpProvidersActive,
|
|
967
967
|
normalizeProviderId,
|
|
968
|
+
normalizeProviderApi,
|
|
969
|
+
readOmpSettingsConfig,
|
|
968
970
|
readModelsConfig,
|
|
969
971
|
removeManagedOmpProviders,
|
|
970
972
|
removeManagedProviderExtension: removeManagedOmpProviders,
|
|
@@ -5,9 +5,18 @@ const { PATHS } = require('../../config/paths');
|
|
|
5
5
|
const { resolveChannelWebsiteUrl } = require('../../config/channel-preset-websites');
|
|
6
6
|
const {
|
|
7
7
|
setChannelConfig,
|
|
8
|
-
clearManagedChannelConfig
|
|
8
|
+
clearManagedChannelConfig,
|
|
9
|
+
readConfig,
|
|
10
|
+
selectConfigPath
|
|
9
11
|
} = require('./opencode-settings-manager');
|
|
10
12
|
const { normalizeGatewaySourceType } = require('./base/proxy-utils');
|
|
13
|
+
const {
|
|
14
|
+
createSkippedResult,
|
|
15
|
+
isLocalProxyBaseUrl,
|
|
16
|
+
resolveApiKeyValue,
|
|
17
|
+
resolveExistingActiveChannel,
|
|
18
|
+
upsertSyncedChannels
|
|
19
|
+
} = require('./channel-sync-utils');
|
|
11
20
|
|
|
12
21
|
function clearChannelBalanceCache(channel) {
|
|
13
22
|
try {
|
|
@@ -93,7 +102,7 @@ function loadChannels() {
|
|
|
93
102
|
gatewaySourceType: normalizeGatewaySourceType(ch.gatewaySourceType, 'codex'),
|
|
94
103
|
allowedModels: ch.allowedModels || []
|
|
95
104
|
};
|
|
96
|
-
normalized.providerKey = deriveProviderKey(normalized);
|
|
105
|
+
normalized.providerKey = ch.providerKey || deriveProviderKey(normalized);
|
|
97
106
|
normalized.websiteUrl = resolveChannelWebsiteUrl('opencode', normalized);
|
|
98
107
|
return normalized;
|
|
99
108
|
});
|
|
@@ -113,6 +122,15 @@ function deriveProviderKey(channel) {
|
|
|
113
122
|
return `opencode_${base}`;
|
|
114
123
|
}
|
|
115
124
|
|
|
125
|
+
function sanitizeOpenCodeProviderId(value) {
|
|
126
|
+
return String(value || '')
|
|
127
|
+
.toLowerCase()
|
|
128
|
+
.replace(/[^a-z0-9-]/g, '-')
|
|
129
|
+
.replace(/-+/g, '-')
|
|
130
|
+
.replace(/^-|-$/g, '')
|
|
131
|
+
|| 'channel';
|
|
132
|
+
}
|
|
133
|
+
|
|
116
134
|
function getOpenCodeProxyRunning() {
|
|
117
135
|
const { getOpenCodeProxyStatus } = require('../opencode-proxy-server');
|
|
118
136
|
return Boolean(getOpenCodeProxyStatus()?.running);
|
|
@@ -229,7 +247,7 @@ function updateChannel(channelId, updates) {
|
|
|
229
247
|
),
|
|
230
248
|
updatedAt: Date.now()
|
|
231
249
|
};
|
|
232
|
-
merged.providerKey = updates.providerKey || deriveProviderKey(merged);
|
|
250
|
+
merged.providerKey = updates.providerKey || oldChannel.providerKey || deriveProviderKey(merged);
|
|
233
251
|
merged.websiteUrl = resolveChannelWebsiteUrl('opencode', merged);
|
|
234
252
|
data.channels[index] = merged;
|
|
235
253
|
|
|
@@ -463,6 +481,155 @@ function disableAllChannels() {
|
|
|
463
481
|
saveChannels(data);
|
|
464
482
|
}
|
|
465
483
|
|
|
484
|
+
function getCurrentOpenCodeProviderId(config = {}) {
|
|
485
|
+
const modelRef = String(config?.model || '').trim();
|
|
486
|
+
if (modelRef.includes('/')) {
|
|
487
|
+
return modelRef.split('/')[0].trim();
|
|
488
|
+
}
|
|
489
|
+
const providers = config?.provider && typeof config.provider === 'object'
|
|
490
|
+
? Object.keys(config.provider).filter(Boolean)
|
|
491
|
+
: [];
|
|
492
|
+
return providers.length === 1 ? providers[0] : '';
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function findOpenCodeExistingByProvider(channels = [], providerId = '', baseUrl = '') {
|
|
496
|
+
const normalizedProviderId = sanitizeOpenCodeProviderId(providerId);
|
|
497
|
+
const url = String(baseUrl || '').trim();
|
|
498
|
+
return channels.find(channel => {
|
|
499
|
+
const providerKeys = [
|
|
500
|
+
channel.providerKey,
|
|
501
|
+
sanitizeOpenCodeProviderId(channel.providerKey),
|
|
502
|
+
sanitizeOpenCodeProviderId(channel.name)
|
|
503
|
+
].filter(Boolean);
|
|
504
|
+
return providerKeys.includes(providerId) || providerKeys.includes(normalizedProviderId);
|
|
505
|
+
}) || channels.find(channel => url && channel.baseUrl === url) || null;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function normalizeOpenCodeChannel(channel = {}) {
|
|
509
|
+
const normalized = {
|
|
510
|
+
...channel,
|
|
511
|
+
enabled: channel.enabled !== false,
|
|
512
|
+
weight: channel.weight || 1,
|
|
513
|
+
maxConcurrency: channel.maxConcurrency || null,
|
|
514
|
+
balanceToken: channel.balanceToken || '',
|
|
515
|
+
balanceUserId: channel.balanceUserId || null,
|
|
516
|
+
modelRedirects: channel.modelRedirects || [],
|
|
517
|
+
speedTestModel: channel.speedTestModel || null,
|
|
518
|
+
wireApi: channel.wireApi || 'openai',
|
|
519
|
+
gatewaySourceType: normalizeGatewaySourceType(channel.gatewaySourceType, 'codex'),
|
|
520
|
+
allowedModels: Array.isArray(channel.allowedModels) ? channel.allowedModels : []
|
|
521
|
+
};
|
|
522
|
+
normalized.providerKey = normalized.providerKey || deriveProviderKey(normalized);
|
|
523
|
+
normalized.websiteUrl = resolveChannelWebsiteUrl('opencode', normalized);
|
|
524
|
+
return normalized;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function collectOpenCodeProviderModels(provider = {}) {
|
|
528
|
+
const models = provider?.models && typeof provider.models === 'object' && !Array.isArray(provider.models)
|
|
529
|
+
? Object.keys(provider.models)
|
|
530
|
+
: [];
|
|
531
|
+
return models.map(model => String(model || '').trim()).filter(Boolean);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function buildOpenCodeSyncCandidate(config, channels) {
|
|
535
|
+
const providerId = getCurrentOpenCodeProviderId(config);
|
|
536
|
+
if (!providerId) {
|
|
537
|
+
return {
|
|
538
|
+
skip: true,
|
|
539
|
+
warning: 'OpenCode 原生配置未明确当前 provider,无法同步当前渠道。'
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const provider = config?.provider?.[providerId] || null;
|
|
544
|
+
if (!provider || typeof provider !== 'object') {
|
|
545
|
+
return {
|
|
546
|
+
skip: true,
|
|
547
|
+
warning: `OpenCode 当前 provider "${providerId}" 未在配置中定义。`
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const baseUrl = String(provider?.options?.baseURL || provider?.options?.baseUrl || provider?.baseUrl || provider?.baseURL || '').trim();
|
|
552
|
+
const rawApiKey = provider?.options?.apiKey || provider?.apiKey || provider?.key || '';
|
|
553
|
+
const existing = findOpenCodeExistingByProvider(channels, providerId, baseUrl);
|
|
554
|
+
const providerIsProxy = providerId === 'ctx-proxy'
|
|
555
|
+
|| rawApiKey === 'PROXY_KEY'
|
|
556
|
+
|| isLocalProxyBaseUrl(baseUrl);
|
|
557
|
+
|
|
558
|
+
if (providerIsProxy) {
|
|
559
|
+
const activeExisting = resolveExistingActiveChannel('opencode', channels)
|
|
560
|
+
|| existing;
|
|
561
|
+
if (activeExisting) {
|
|
562
|
+
return {
|
|
563
|
+
skip: true,
|
|
564
|
+
channel: activeExisting,
|
|
565
|
+
warning: 'OpenCode 当前配置指向 ctx 代理;对应渠道已在列表中,未重复导入。'
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
return {
|
|
569
|
+
skip: true,
|
|
570
|
+
warning: 'OpenCode 当前配置指向 ctx 代理,但没有找到可匹配的现有渠道。'
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const credential = resolveApiKeyValue(rawApiKey);
|
|
575
|
+
const apiKey = credential.value || getEffectiveApiKeyCandidates(existing || {})[0] || '';
|
|
576
|
+
if (!apiKey) {
|
|
577
|
+
return {
|
|
578
|
+
skip: true,
|
|
579
|
+
channel: existing || null,
|
|
580
|
+
warning: `OpenCode 当前 provider "${providerId}" 缺少可解析 API Key,无法同步导入。`
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const modelRef = String(config?.model || '').trim();
|
|
585
|
+
const model = modelRef.startsWith(`${providerId}/`) ? modelRef.slice(providerId.length + 1) : '';
|
|
586
|
+
const allowedModels = collectOpenCodeProviderModels(provider);
|
|
587
|
+
return {
|
|
588
|
+
name: existing?.name || provider.name || providerId,
|
|
589
|
+
providerKey: providerId,
|
|
590
|
+
baseUrl,
|
|
591
|
+
apiKey,
|
|
592
|
+
wireApi: existing?.wireApi || 'openai',
|
|
593
|
+
model: model || existing?.model || allowedModels[0] || null,
|
|
594
|
+
allowedModels,
|
|
595
|
+
gatewaySourceType: existing?.gatewaySourceType || 'codex',
|
|
596
|
+
credentialSource: credential.value ? credential.source : 'existing-channel'
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function syncCurrentOpenCodeChannel() {
|
|
601
|
+
if (typeof readConfig !== 'function' || typeof selectConfigPath !== 'function') {
|
|
602
|
+
return createSkippedResult('opencode', 'OpenCode 配置读取能力不可用,无法同步当前渠道。');
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const data = loadChannels();
|
|
606
|
+
const channels = Array.isArray(data.channels) ? data.channels : [];
|
|
607
|
+
let config = {};
|
|
608
|
+
try {
|
|
609
|
+
const configPath = selectConfigPath();
|
|
610
|
+
config = readConfig(configPath);
|
|
611
|
+
} catch (error) {
|
|
612
|
+
return createSkippedResult('opencode', `OpenCode 配置读取失败:${error.message}`);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const candidate = buildOpenCodeSyncCandidate(config, channels);
|
|
616
|
+
if (candidate?.skip) {
|
|
617
|
+
return createSkippedResult('opencode', candidate.warning, candidate.channel);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
return upsertSyncedChannels({
|
|
621
|
+
toolType: 'opencode',
|
|
622
|
+
loadChannels,
|
|
623
|
+
saveChannels,
|
|
624
|
+
applyDefaults: normalizeOpenCodeChannel,
|
|
625
|
+
candidates: [candidate],
|
|
626
|
+
matchers: [
|
|
627
|
+
(channel, current) => channel.providerKey && channel.providerKey === current.providerKey,
|
|
628
|
+
(channel, current) => channel.baseUrl === current.baseUrl && channel.apiKey === current.apiKey
|
|
629
|
+
]
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
|
|
466
633
|
module.exports = {
|
|
467
634
|
getChannels,
|
|
468
635
|
createChannel,
|
|
@@ -474,5 +641,6 @@ module.exports = {
|
|
|
474
641
|
applyChannelToSettings,
|
|
475
642
|
getEffectiveApiKey,
|
|
476
643
|
getEffectiveApiKeyCandidates,
|
|
477
|
-
disableAllChannels
|
|
644
|
+
disableAllChannels,
|
|
645
|
+
syncCurrentOpenCodeChannel
|
|
478
646
|
};
|