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.
- package/README.md +5 -5
- package/dist/web/assets/{Analytics-Dpje9L84.js → Analytics-BqDjJUJl.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-Cz6kasek.js → ConfigTemplates-DV2KFuC4.js} +1 -1
- package/dist/web/assets/{Home-BwTaYu3q.js → Home-D7hovkk9.js} +1 -1
- package/dist/web/assets/{PluginManager-DsN-xNXx.js → PluginManager-Bd3hhc5A.js} +1 -1
- package/dist/web/assets/{ProjectList-4BCObimU.js → ProjectList-BB4Whns8.js} +1 -1
- package/dist/web/assets/{SessionList-D4c8FRzq.js → SessionList-CvurKSRK.js} +1 -1
- package/dist/web/assets/{SkillManager-C-KYbXB5.js → SkillManager-DQjRXOFg.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-B-BlPJHX.js → WorkspaceManager-CZeXnxu1.js} +1 -1
- package/dist/web/assets/{icons-Dop5j7Bf.js → icons-nGdSKQn2.js} +1 -1
- package/dist/web/assets/index-BovYCFsd.js +2 -0
- package/dist/web/assets/index-kQCC5dlu.css +1 -0
- package/dist/web/index.html +3 -3
- package/package.json +1 -1
- package/src/commands/ui.js +1 -0
- package/src/index.js +3 -2
- 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/oauth-credentials.js +15 -2
- package/src/server/api/omp-channels.js +117 -39
- package/src/server/api/opencode-channels.js +21 -1
- package/src/server/index.js +13 -5
- 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/model-detector.js +15 -2
- package/src/server/services/native-oauth-adapters.js +212 -1
- package/src/server/services/network-access.js +5 -0
- package/src/server/services/oauth-credentials-service.js +110 -3
- package/src/server/services/omp-channels.js +304 -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-Btfvh0GG.js +0 -2
- package/dist/web/assets/index-CLNa6DhH.css +0 -1
|
@@ -6,6 +6,12 @@ const { PATHS, NATIVE_PATHS } = require('../../config/paths');
|
|
|
6
6
|
const { clearNativeOAuth } = require('./native-oauth-adapters');
|
|
7
7
|
const { isWindowsLikePlatform } = require('../../utils/home-dir');
|
|
8
8
|
const { normalizeGatewaySourceType } = require('./base/proxy-utils');
|
|
9
|
+
const {
|
|
10
|
+
createSkippedResult,
|
|
11
|
+
isLocalProxyBaseUrl,
|
|
12
|
+
resolveExistingActiveChannel,
|
|
13
|
+
upsertSyncedChannels
|
|
14
|
+
} = require('./channel-sync-utils');
|
|
9
15
|
|
|
10
16
|
// ── Claude 特有工具函数 ──
|
|
11
17
|
|
|
@@ -338,6 +344,118 @@ function getCurrentSettings() {
|
|
|
338
344
|
};
|
|
339
345
|
}
|
|
340
346
|
|
|
347
|
+
function readClaudeNativeSettings() {
|
|
348
|
+
const settingsPath = getClaudeSettingsPath();
|
|
349
|
+
if (!fs.existsSync(settingsPath)) {
|
|
350
|
+
return {};
|
|
351
|
+
}
|
|
352
|
+
try {
|
|
353
|
+
return JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
354
|
+
} catch (error) {
|
|
355
|
+
throw new Error(`Failed to read Claude settings.json: ${error.message}`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function readClaudeNativeOAuth() {
|
|
360
|
+
try {
|
|
361
|
+
const adapters = require('./native-oauth-adapters');
|
|
362
|
+
return typeof adapters.readNativeOAuth === 'function'
|
|
363
|
+
? adapters.readNativeOAuth('claude')
|
|
364
|
+
: null;
|
|
365
|
+
} catch {
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function buildClaudeSyncCandidate(settings, channels) {
|
|
371
|
+
const env = settings?.env && typeof settings.env === 'object' ? settings.env : {};
|
|
372
|
+
const baseUrl = String(env.ANTHROPIC_BASE_URL || '').trim();
|
|
373
|
+
|
|
374
|
+
if (isLocalProxyBaseUrl(baseUrl)) {
|
|
375
|
+
const existing = resolveExistingActiveChannel('claude', channels);
|
|
376
|
+
if (existing) {
|
|
377
|
+
return {
|
|
378
|
+
skip: true,
|
|
379
|
+
channel: existing,
|
|
380
|
+
warning: 'Claude 当前配置指向 ctx 代理;对应渠道已在列表中,未重复导入。'
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
return {
|
|
384
|
+
skip: true,
|
|
385
|
+
warning: 'Claude 当前配置指向 ctx 代理,但没有找到可匹配的现有渠道。请先确认 ctx 渠道列表。'
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const helperKey = extractApiKeyFromHelper(settings?.apiKeyHelper);
|
|
390
|
+
const apiKey = String(env.ANTHROPIC_API_KEY || env.ANTHROPIC_AUTH_TOKEN || helperKey || '').trim();
|
|
391
|
+
const hasOAuth = Boolean(env.CLAUDE_CODE_OAUTH_TOKEN || readClaudeNativeOAuth());
|
|
392
|
+
const existingByBaseUrl = baseUrl
|
|
393
|
+
? channels.find(ch => String(ch.baseUrl || '').trim() === baseUrl)
|
|
394
|
+
: null;
|
|
395
|
+
|
|
396
|
+
if (!apiKey) {
|
|
397
|
+
if (existingByBaseUrl) {
|
|
398
|
+
return {
|
|
399
|
+
skip: true,
|
|
400
|
+
channel: existingByBaseUrl,
|
|
401
|
+
warning: 'Claude 当前配置未暴露 API Key;已找到同 baseUrl 渠道,未重复导入。'
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
return {
|
|
405
|
+
skip: true,
|
|
406
|
+
warning: hasOAuth
|
|
407
|
+
? 'Claude 当前配置是 OAuth/登录态,OAuth 渠道不支持同步导入。'
|
|
408
|
+
: 'Claude 当前配置缺少 API Key,无法同步导入。'
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const modelConfig = {
|
|
413
|
+
model: env.ANTHROPIC_MODEL || '',
|
|
414
|
+
haikuModel: env.ANTHROPIC_DEFAULT_HAIKU_MODEL || '',
|
|
415
|
+
sonnetModel: env.ANTHROPIC_DEFAULT_SONNET_MODEL || '',
|
|
416
|
+
opusModel: env.ANTHROPIC_DEFAULT_OPUS_MODEL || ''
|
|
417
|
+
};
|
|
418
|
+
Object.keys(modelConfig).forEach((key) => {
|
|
419
|
+
if (!modelConfig[key]) delete modelConfig[key];
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
const finalBaseUrl = baseUrl || 'https://api.anthropic.com';
|
|
423
|
+
return {
|
|
424
|
+
name: existingByBaseUrl?.name || 'Claude 当前配置',
|
|
425
|
+
baseUrl: finalBaseUrl,
|
|
426
|
+
apiKey,
|
|
427
|
+
presetId: finalBaseUrl.includes('api.anthropic.com') ? 'official' : 'custom',
|
|
428
|
+
modelConfig,
|
|
429
|
+
proxyUrl: env.HTTPS_PROXY || env.HTTP_PROXY || '',
|
|
430
|
+
targetApi: 'responses',
|
|
431
|
+
gatewaySourceType: 'claude',
|
|
432
|
+
credentialSource: env.ANTHROPIC_API_KEY || env.ANTHROPIC_AUTH_TOKEN ? 'config' : 'apiKeyHelper'
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function syncCurrentClaudeChannel() {
|
|
437
|
+
const data = service.loadChannels();
|
|
438
|
+
const channels = Array.isArray(data.channels) ? data.channels : [];
|
|
439
|
+
const settings = readClaudeNativeSettings();
|
|
440
|
+
const candidate = buildClaudeSyncCandidate(settings, channels);
|
|
441
|
+
|
|
442
|
+
if (candidate?.skip) {
|
|
443
|
+
return createSkippedResult('claude', candidate.warning, candidate.channel);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return upsertSyncedChannels({
|
|
447
|
+
toolType: 'claude',
|
|
448
|
+
loadChannels: () => service.loadChannels(),
|
|
449
|
+
saveChannels: payload => service.saveChannels(payload),
|
|
450
|
+
applyDefaults: channel => service._applyDefaults(channel),
|
|
451
|
+
candidates: [candidate],
|
|
452
|
+
matchers: [
|
|
453
|
+
(channel, current) => channel.baseUrl === current.baseUrl && channel.apiKey === current.apiKey,
|
|
454
|
+
(channel, current) => channel.baseUrl === current.baseUrl && channel.targetApi === current.targetApi
|
|
455
|
+
]
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
|
|
341
459
|
function getBestChannelForRestore() {
|
|
342
460
|
const channels = getAllChannels();
|
|
343
461
|
const enabled = channels.filter(ch => ch.enabled !== false);
|
|
@@ -394,4 +512,5 @@ module.exports = {
|
|
|
394
512
|
getEffectiveApiKey,
|
|
395
513
|
disableAllChannels,
|
|
396
514
|
extractApiKeyFromHelper,
|
|
515
|
+
syncCurrentClaudeChannel,
|
|
397
516
|
};
|
|
@@ -8,6 +8,13 @@ const { getCodexDir } = require('./codex-config');
|
|
|
8
8
|
const { isProxyConfig, readConfig } = require('./codex-settings-manager');
|
|
9
9
|
const { syncCodexUserEnvironment } = require('./codex-env-manager');
|
|
10
10
|
const BaseChannelService = require('./base/base-channel-service');
|
|
11
|
+
const {
|
|
12
|
+
createSkippedResult,
|
|
13
|
+
isLocalProxyBaseUrl,
|
|
14
|
+
resolveApiKeyValue,
|
|
15
|
+
resolveExistingActiveChannel,
|
|
16
|
+
upsertSyncedChannels
|
|
17
|
+
} = require('./channel-sync-utils');
|
|
11
18
|
|
|
12
19
|
const CODEX_MANAGED_ENV_KEY = 'CC_PROXY_KEY';
|
|
13
20
|
const CODEX_PROXY_ENV_VALUE = 'PROXY_KEY';
|
|
@@ -269,6 +276,123 @@ function applyChannelToSettings(id) { return service.applyChannelToSettings(id);
|
|
|
269
276
|
function getEffectiveApiKey(channel) { return service.getEffectiveApiKey(channel); }
|
|
270
277
|
function disableAllChannels() { return service.disableAllChannels(); }
|
|
271
278
|
|
|
279
|
+
function findCodexExistingChannel(channels = [], providerKey = '', baseUrl = '') {
|
|
280
|
+
const key = String(providerKey || '').trim();
|
|
281
|
+
const url = String(baseUrl || '').trim();
|
|
282
|
+
return channels.find(channel => key && channel.providerKey === key)
|
|
283
|
+
|| channels.find(channel => url && channel.baseUrl === url)
|
|
284
|
+
|| null;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function buildDefaultCodexProvider(providerKey = '') {
|
|
288
|
+
if (providerKey !== 'openai') return null;
|
|
289
|
+
return {
|
|
290
|
+
name: 'OpenAI',
|
|
291
|
+
base_url: 'https://api.openai.com/v1',
|
|
292
|
+
env_key: 'OPENAI_API_KEY',
|
|
293
|
+
wire_api: 'responses'
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function buildCodexSyncCandidate(config, channels) {
|
|
298
|
+
const configuredProvider = String(config?.model_provider || '').trim();
|
|
299
|
+
const currentProvider = configuredProvider || (process.env.OPENAI_API_KEY ? 'openai' : '');
|
|
300
|
+
if (!currentProvider) {
|
|
301
|
+
return {
|
|
302
|
+
skip: true,
|
|
303
|
+
warning: 'Codex config.toml 未配置 model_provider,且未找到 OPENAI_API_KEY,无法同步当前渠道。'
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const provider = config?.model_providers?.[currentProvider]
|
|
308
|
+
|| buildDefaultCodexProvider(currentProvider)
|
|
309
|
+
|| null;
|
|
310
|
+
const baseUrl = String(provider?.base_url || provider?.baseUrl || '').trim();
|
|
311
|
+
if (currentProvider === 'cc-proxy' || isLocalProxyBaseUrl(baseUrl)) {
|
|
312
|
+
const existing = resolveExistingActiveChannel('codex', channels);
|
|
313
|
+
if (existing) {
|
|
314
|
+
return {
|
|
315
|
+
skip: true,
|
|
316
|
+
channel: existing,
|
|
317
|
+
warning: 'Codex 当前配置指向 ctx 代理;对应渠道已在列表中,未重复导入。'
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
return {
|
|
321
|
+
skip: true,
|
|
322
|
+
warning: 'Codex 当前配置指向 ctx 代理,但没有找到可匹配的现有渠道。'
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (!provider || typeof provider !== 'object') {
|
|
327
|
+
return {
|
|
328
|
+
skip: true,
|
|
329
|
+
warning: `Codex 当前 provider "${currentProvider}" 未在 model_providers 中定义。`
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const existing = findCodexExistingChannel(channels, currentProvider, baseUrl);
|
|
334
|
+
const directKey = provider.api_key || provider.apiKey || provider.key || '';
|
|
335
|
+
let credential = resolveApiKeyValue(directKey);
|
|
336
|
+
if (!credential.value && provider.env_key) {
|
|
337
|
+
credential = resolveApiKeyValue(String(provider.env_key || ''));
|
|
338
|
+
}
|
|
339
|
+
const apiKey = credential.value || existing?.apiKey || '';
|
|
340
|
+
if (!apiKey) {
|
|
341
|
+
return {
|
|
342
|
+
skip: true,
|
|
343
|
+
channel: existing || null,
|
|
344
|
+
warning: provider.requires_openai_auth
|
|
345
|
+
? 'Codex 当前 provider 使用 OAuth/OpenAI 登录态,OAuth 渠道不支持同步导入。'
|
|
346
|
+
: `Codex 当前 provider "${currentProvider}" 缺少可解析 API Key,无法同步导入。`
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return {
|
|
351
|
+
name: existing?.name || provider.name || currentProvider,
|
|
352
|
+
providerKey: currentProvider,
|
|
353
|
+
baseUrl,
|
|
354
|
+
apiKey,
|
|
355
|
+
wireApi: provider.wire_api || provider.wireApi || 'responses',
|
|
356
|
+
envKey: CODEX_MANAGED_ENV_KEY,
|
|
357
|
+
requiresOpenaiAuth: false,
|
|
358
|
+
queryParams: provider.query_params || provider.queryParams || {},
|
|
359
|
+
gatewaySourceType: existing?.gatewaySourceType || 'codex',
|
|
360
|
+
credentialSource: credential.value ? credential.source : 'existing-channel'
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function syncCurrentCodexChannel() {
|
|
365
|
+
const data = service.loadChannels();
|
|
366
|
+
const channels = Array.isArray(data.channels) ? data.channels : [];
|
|
367
|
+
let config = {};
|
|
368
|
+
try {
|
|
369
|
+
config = readConfig();
|
|
370
|
+
} catch (error) {
|
|
371
|
+
if (/ENOENT|no such file|config\.toml not found/i.test(String(error?.message || ''))) {
|
|
372
|
+
config = {};
|
|
373
|
+
} else {
|
|
374
|
+
return createSkippedResult('codex', `Codex config.toml 读取失败:${error.message}`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const candidate = buildCodexSyncCandidate(config, channels);
|
|
379
|
+
if (candidate?.skip) {
|
|
380
|
+
return createSkippedResult('codex', candidate.warning, candidate.channel);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return upsertSyncedChannels({
|
|
384
|
+
toolType: 'codex',
|
|
385
|
+
loadChannels: () => service.loadChannels(),
|
|
386
|
+
saveChannels: payload => service.saveChannels(payload),
|
|
387
|
+
applyDefaults: channel => service._applyDefaults(channel),
|
|
388
|
+
candidates: [candidate],
|
|
389
|
+
matchers: [
|
|
390
|
+
(channel, current) => channel.providerKey && channel.providerKey === current.providerKey,
|
|
391
|
+
(channel, current) => channel.baseUrl === current.baseUrl && channel.apiKey === current.apiKey
|
|
392
|
+
]
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
272
396
|
// 服务启动时自动同步环境变量
|
|
273
397
|
try {
|
|
274
398
|
const data = service.loadChannels();
|
|
@@ -292,6 +416,7 @@ module.exports = {
|
|
|
292
416
|
applyChannelToSettings,
|
|
293
417
|
getEffectiveApiKey,
|
|
294
418
|
disableAllChannels,
|
|
419
|
+
syncCurrentCodexChannel,
|
|
295
420
|
_test: {
|
|
296
421
|
buildManagedCodexEnvMap,
|
|
297
422
|
CODEX_MANAGED_ENV_KEY,
|
|
@@ -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
|
}
|
|
@@ -95,6 +95,13 @@ function getModelPriority(channelType, options = {}) {
|
|
|
95
95
|
console.warn(`[ModelDetector] Failed to load default models config: ${error.message}`);
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
for (const toolType of candidateTypes) {
|
|
99
|
+
const models = MODEL_PRIORITY[toolType];
|
|
100
|
+
if (Array.isArray(models) && models.length > 0) {
|
|
101
|
+
return normalizeModelCandidates(models);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
98
105
|
return [];
|
|
99
106
|
}
|
|
100
107
|
|
|
@@ -779,6 +786,12 @@ function buildChannelCacheSignature(channel, payload = {}) {
|
|
|
779
786
|
return crypto.createHash('sha1').update(raw).digest('hex');
|
|
780
787
|
}
|
|
781
788
|
|
|
789
|
+
function getChannelCacheKey(channel, signature) {
|
|
790
|
+
const id = String(channel?.id || '').trim();
|
|
791
|
+
if (id) return id;
|
|
792
|
+
return `temporary:${signature}`;
|
|
793
|
+
}
|
|
794
|
+
|
|
782
795
|
function isSignatureCacheValid(cacheEntry, signatureKey, expectedSignature) {
|
|
783
796
|
if (!cacheEntry || !signatureKey || !expectedSignature) return false;
|
|
784
797
|
return cacheEntry[signatureKey] === expectedSignature;
|
|
@@ -846,7 +859,6 @@ async function probeModelAvailability(channel, channelType, options = {}) {
|
|
|
846
859
|
const toolType = options.toolType;
|
|
847
860
|
const stopOnFirstAvailable = !!options.stopOnFirstAvailable;
|
|
848
861
|
const cache = loadModelCache();
|
|
849
|
-
const cacheKey = channel.id;
|
|
850
862
|
const preferredModels = normalizeModelCandidates(options.preferredModels);
|
|
851
863
|
const probeSignature = buildChannelCacheSignature(channel, {
|
|
852
864
|
type: 'probe',
|
|
@@ -855,6 +867,7 @@ async function probeModelAvailability(channel, channelType, options = {}) {
|
|
|
855
867
|
stopOnFirstAvailable,
|
|
856
868
|
preferredModels
|
|
857
869
|
});
|
|
870
|
+
const cacheKey = getChannelCacheKey(channel, probeSignature);
|
|
858
871
|
|
|
859
872
|
// Return cached result if channel and probe options are unchanged
|
|
860
873
|
if (!forceRefresh && isSignatureCacheValid(cache[cacheKey], 'probeSignature', probeSignature)) {
|
|
@@ -1007,11 +1020,11 @@ async function fetchModelsFromProvider(channel, channelType, options = {}) {
|
|
|
1007
1020
|
}
|
|
1008
1021
|
|
|
1009
1022
|
const cache = loadModelCache();
|
|
1010
|
-
const cacheKey = channel.id;
|
|
1011
1023
|
const listSignature = buildChannelCacheSignature(channel, {
|
|
1012
1024
|
type: 'model-list',
|
|
1013
1025
|
channelType: String(channelType || '').trim().toLowerCase()
|
|
1014
1026
|
});
|
|
1027
|
+
const cacheKey = getChannelCacheKey(channel, listSignature);
|
|
1015
1028
|
|
|
1016
1029
|
// Check cache first, and only reuse when channel/list context is unchanged
|
|
1017
1030
|
if (!forceRefresh
|