coding-tool-x 3.7.2 → 3.7.4
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-BmPIW7oK.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-D_-16blp.js → ConfigTemplates-CG4j4ua4.js} +1 -1
- package/dist/web/assets/Home-B9yAAZrD.js +1 -0
- package/dist/web/assets/Home-DSTUSxrT.css +1 -0
- package/dist/web/assets/{PluginManager-Cv_rv8Dh.js → PluginManager-CCJ7mo35.js} +1 -1
- package/dist/web/assets/{ProjectList-H_m4Tx4E.js → ProjectList-DVUs3o8x.js} +1 -1
- package/dist/web/assets/SessionList-DwssBydw.js +1 -0
- package/dist/web/assets/{SkillManager-CRav3m2a.js → SkillManager-DmOTHcQ7.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-B3f8R4EF.js → WorkspaceManager-DLVj0zlu.js} +1 -1
- package/dist/web/assets/{icons-Dop5j7Bf.js → icons-nGdSKQn2.js} +1 -1
- package/dist/web/assets/index-BjdNYwSs.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/config/paths.js +2 -0
- package/src/server/api/channels.js +14 -1
- package/src/server/api/codex-channels.js +21 -1
- package/src/server/api/codex-projects.js +25 -8
- package/src/server/api/dashboard.js +167 -75
- package/src/server/api/gemini-channels.js +21 -1
- package/src/server/api/gemini-projects.js +23 -7
- package/src/server/api/omp-channels.js +17 -1
- package/src/server/api/omp-projects.js +23 -6
- package/src/server/api/opencode-channels.js +21 -1
- package/src/server/api/opencode-projects.js +23 -7
- package/src/server/api/projects.js +38 -21
- package/src/server/services/channel-balance.js +51 -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/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/src/server/services/project-snapshots.js +52 -0
- package/src/server/services/snapshot-cache.js +295 -0
- package/dist/web/assets/Home-DM_56Fyr.js +0 -1
- package/dist/web/assets/Home-Jom_ZcdY.css +0 -1
- package/dist/web/assets/SessionList-DcUQvsbR.js +0 -1
- package/dist/web/assets/index-Bnni1zPN.js +0 -2
- package/dist/web/assets/index-CfMuHG8u.css +0 -1
|
@@ -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
|
}
|
|
@@ -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,
|