coding-tool-x 3.7.1 → 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.
Files changed (36) hide show
  1. package/README.md +5 -5
  2. package/dist/web/assets/{Analytics-Dpje9L84.js → Analytics-BqDjJUJl.js} +1 -1
  3. package/dist/web/assets/{ConfigTemplates-Cz6kasek.js → ConfigTemplates-DV2KFuC4.js} +1 -1
  4. package/dist/web/assets/{Home-BwTaYu3q.js → Home-D7hovkk9.js} +1 -1
  5. package/dist/web/assets/{PluginManager-DsN-xNXx.js → PluginManager-Bd3hhc5A.js} +1 -1
  6. package/dist/web/assets/{ProjectList-4BCObimU.js → ProjectList-BB4Whns8.js} +1 -1
  7. package/dist/web/assets/{SessionList-D4c8FRzq.js → SessionList-CvurKSRK.js} +1 -1
  8. package/dist/web/assets/{SkillManager-C-KYbXB5.js → SkillManager-DQjRXOFg.js} +1 -1
  9. package/dist/web/assets/{WorkspaceManager-B-BlPJHX.js → WorkspaceManager-CZeXnxu1.js} +1 -1
  10. package/dist/web/assets/{icons-Dop5j7Bf.js → icons-nGdSKQn2.js} +1 -1
  11. package/dist/web/assets/index-BovYCFsd.js +2 -0
  12. package/dist/web/assets/index-kQCC5dlu.css +1 -0
  13. package/dist/web/index.html +3 -3
  14. package/package.json +1 -1
  15. package/src/commands/ui.js +1 -0
  16. package/src/index.js +3 -2
  17. package/src/server/api/channels.js +14 -1
  18. package/src/server/api/codex-channels.js +21 -1
  19. package/src/server/api/gemini-channels.js +21 -1
  20. package/src/server/api/oauth-credentials.js +15 -2
  21. package/src/server/api/omp-channels.js +117 -39
  22. package/src/server/api/opencode-channels.js +21 -1
  23. package/src/server/index.js +13 -5
  24. package/src/server/services/channel-sync-utils.js +368 -0
  25. package/src/server/services/channels.js +119 -0
  26. package/src/server/services/codex-channels.js +125 -0
  27. package/src/server/services/gemini-channels.js +123 -0
  28. package/src/server/services/model-detector.js +15 -2
  29. package/src/server/services/native-oauth-adapters.js +212 -1
  30. package/src/server/services/network-access.js +5 -0
  31. package/src/server/services/oauth-credentials-service.js +110 -3
  32. package/src/server/services/omp-channels.js +304 -2
  33. package/src/server/services/omp-settings-manager.js +2 -0
  34. package/src/server/services/opencode-channels.js +172 -4
  35. package/dist/web/assets/index-Btfvh0GG.js +0 -2
  36. package/dist/web/assets/index-CLNa6DhH.css +0 -1
@@ -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() {
@@ -21,6 +37,11 @@ class OmpChannelService extends BaseChannelService {
21
37
  const normalized = super._applyDefaults(channel);
22
38
  normalized.providerKey = normalized.providerKey || normalized.provider || normalized.name || normalized.id;
23
39
  normalized.providerApi = normalized.providerApi || normalized.api || normalized.wireApi || 'openai-completions';
40
+ normalized.authMode = normalized.authMode === 'oauth' ? 'oauth' : 'api_key';
41
+ normalized.oauthProviderId = normalized.oauthProviderId || (normalized.authMode === 'oauth' ? normalized.providerKey : '');
42
+ if (normalized.authMode === 'oauth') {
43
+ normalized.apiKey = normalized.apiKey || '';
44
+ }
24
45
  normalized.model = normalized.model || null;
25
46
  normalized.models = Array.isArray(normalized.models) ? normalized.models : [];
26
47
  normalized.allowedModels = Array.isArray(normalized.allowedModels) ? normalized.allowedModels : [];
@@ -71,6 +92,286 @@ class OmpChannelService extends BaseChannelService {
71
92
 
72
93
  const service = new OmpChannelService();
73
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
+
74
375
  module.exports = {
75
376
  getChannels: () => service.getChannels(),
76
377
  createChannel: (name, baseUrl, apiKey, extra = {}) => service.createChannel({
@@ -90,5 +391,6 @@ module.exports = {
90
391
  syncManagedOmpProviders: (channels) => service.syncManagedOmpProviders(channels),
91
392
  disableManagedOmpProviders: () => service.disableManagedOmpProviders(),
92
393
  syncManagedProviderExtension: (channels) => service.syncManagedProviderExtension(channels),
93
- disableManagedProviderExtension: () => service.disableManagedProviderExtension()
394
+ disableManagedProviderExtension: () => service.disableManagedProviderExtension(),
395
+ syncCurrentOmpChannel
94
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
  };