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
|
@@ -6,6 +6,11 @@ const path = require('path');
|
|
|
6
6
|
const { URL } = require('url');
|
|
7
7
|
const { PATHS } = require('../../config/paths');
|
|
8
8
|
const { loadUIConfig } = require('./ui-config');
|
|
9
|
+
const {
|
|
10
|
+
getSnapshot,
|
|
11
|
+
refreshSnapshot,
|
|
12
|
+
invalidateSnapshot
|
|
13
|
+
} = require('./snapshot-cache');
|
|
9
14
|
|
|
10
15
|
const CACHE_TTL_MS = 5 * 60 * 1000;
|
|
11
16
|
const REQUEST_TIMEOUT_MS = 8000;
|
|
@@ -1489,6 +1494,14 @@ function buildCacheKey(source, channel) {
|
|
|
1489
1494
|
].join(':');
|
|
1490
1495
|
}
|
|
1491
1496
|
|
|
1497
|
+
function buildBalanceSnapshotKey(source, channel) {
|
|
1498
|
+
return `channel-balances:${buildCacheKey(source, channel)}`;
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
function makeFallbackBalanceSnapshot(currentTime = Date.now()) {
|
|
1502
|
+
return makeHiddenSnapshot(null, { stale: true, updatedAt: nowIso(currentTime) });
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1492
1505
|
async function refreshChannelBalanceSnapshot(source, channel, options = {}) {
|
|
1493
1506
|
const force = options.force === true;
|
|
1494
1507
|
const currentTime = options.now || Date.now();
|
|
@@ -1588,6 +1601,7 @@ function clearChannelBalanceCache(source, channel) {
|
|
|
1588
1601
|
let strategyChanged = false;
|
|
1589
1602
|
|
|
1590
1603
|
balanceCache.delete(exactKey);
|
|
1604
|
+
invalidateSnapshot(`channel-balances:${exactKey}`);
|
|
1591
1605
|
for (const key of Array.from(balanceCache.keys())) {
|
|
1592
1606
|
if (key.startsWith(prefix)) {
|
|
1593
1607
|
balanceCache.delete(key);
|
|
@@ -1665,19 +1679,45 @@ async function mapWithConcurrency(items, limit, worker) {
|
|
|
1665
1679
|
async function getChannelBalances(source, options = {}) {
|
|
1666
1680
|
const normalizedSource = validateSource(source);
|
|
1667
1681
|
if (!isBalanceDisplayEnabled()) {
|
|
1668
|
-
return {
|
|
1682
|
+
return {
|
|
1683
|
+
enabled: false,
|
|
1684
|
+
source: normalizedSource,
|
|
1685
|
+
balances: {},
|
|
1686
|
+
refreshing: false,
|
|
1687
|
+
updatedAt: null,
|
|
1688
|
+
stale: false
|
|
1689
|
+
};
|
|
1669
1690
|
}
|
|
1670
1691
|
|
|
1671
1692
|
const channels = getEnabledBalanceChannels(normalizedSource);
|
|
1693
|
+
const currentTime = options.now || Date.now();
|
|
1672
1694
|
const entries = await mapWithConcurrency(channels, 4, async (channel) => {
|
|
1673
|
-
const snapshot = await
|
|
1674
|
-
|
|
1695
|
+
const snapshot = await getSnapshot(buildBalanceSnapshotKey(normalizedSource, channel), {
|
|
1696
|
+
ttlMs: CACHE_TTL_MS,
|
|
1697
|
+
fallbackValue: makeFallbackBalanceSnapshot(currentTime),
|
|
1698
|
+
refresh: () => refreshChannelBalanceSnapshot(normalizedSource, channel, { ...options, force: true, now: Date.now() })
|
|
1699
|
+
});
|
|
1700
|
+
const value = {
|
|
1701
|
+
...snapshot.value,
|
|
1702
|
+
stale: Boolean(snapshot.value?.stale || snapshot.meta.stale)
|
|
1703
|
+
};
|
|
1704
|
+
return [channel.id, value, snapshot.meta];
|
|
1675
1705
|
});
|
|
1676
1706
|
|
|
1707
|
+
const metas = entries.map((entry) => entry[2]).filter(Boolean);
|
|
1708
|
+
const updatedAt = metas
|
|
1709
|
+
.map(meta => meta.generatedAt)
|
|
1710
|
+
.filter(Boolean)
|
|
1711
|
+
.sort()
|
|
1712
|
+
.pop() || null;
|
|
1713
|
+
|
|
1677
1714
|
return {
|
|
1678
1715
|
enabled: true,
|
|
1679
1716
|
source: normalizedSource,
|
|
1680
|
-
balances: Object.fromEntries(entries.filter(([id]) => id))
|
|
1717
|
+
balances: Object.fromEntries(entries.filter(([id]) => id).map(([id, snapshot]) => [id, snapshot])),
|
|
1718
|
+
refreshing: metas.some(meta => meta.refreshing),
|
|
1719
|
+
updatedAt,
|
|
1720
|
+
stale: metas.some(meta => meta.stale)
|
|
1681
1721
|
};
|
|
1682
1722
|
}
|
|
1683
1723
|
|
|
@@ -1695,7 +1735,11 @@ async function refreshChannelBalance(source, channelId) {
|
|
|
1695
1735
|
throw error;
|
|
1696
1736
|
}
|
|
1697
1737
|
|
|
1698
|
-
const
|
|
1738
|
+
const entry = await refreshSnapshot(
|
|
1739
|
+
buildBalanceSnapshotKey(normalizedSource, channel),
|
|
1740
|
+
() => refreshChannelBalanceSnapshot(normalizedSource, channel, { force: true })
|
|
1741
|
+
);
|
|
1742
|
+
const balance = entry.value;
|
|
1699
1743
|
return {
|
|
1700
1744
|
enabled: true,
|
|
1701
1745
|
source: normalizedSource,
|
|
@@ -1707,6 +1751,7 @@ async function refreshChannelBalance(source, channelId) {
|
|
|
1707
1751
|
function clearBalanceCache() {
|
|
1708
1752
|
balanceCache.clear();
|
|
1709
1753
|
balanceStrategyCache.clear();
|
|
1754
|
+
invalidateSnapshot('channel-balances:');
|
|
1710
1755
|
strategyCacheLoaded = false;
|
|
1711
1756
|
}
|
|
1712
1757
|
|
|
@@ -1739,6 +1784,7 @@ module.exports = {
|
|
|
1739
1784
|
build88CodeSubscriptionSnapshot,
|
|
1740
1785
|
buildNewCliDashboardSnapshot,
|
|
1741
1786
|
buildBalanceProbeStrategies,
|
|
1787
|
+
buildBalanceSnapshotKey,
|
|
1742
1788
|
getEnabledBalanceChannels,
|
|
1743
1789
|
runBalanceStrategy,
|
|
1744
1790
|
refreshChannelBalanceSnapshot,
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const { PATHS, ensureStorageDirMigrated } = require('../../config/paths');
|
|
5
|
+
|
|
6
|
+
function normalizeString(value) {
|
|
7
|
+
return String(value || '').trim();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function normalizeLower(value) {
|
|
11
|
+
return normalizeString(value).toLowerCase();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function normalizeUrl(value) {
|
|
15
|
+
const raw = normalizeString(value);
|
|
16
|
+
if (!raw) return '';
|
|
17
|
+
return raw.replace(/\/+$/, '').toLowerCase();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isLocalProxyBaseUrl(value) {
|
|
21
|
+
const raw = normalizeString(value);
|
|
22
|
+
if (!raw) return false;
|
|
23
|
+
try {
|
|
24
|
+
const parsed = new URL(raw);
|
|
25
|
+
const host = normalizeLower(parsed.hostname).replace(/^\[|\]$/g, '');
|
|
26
|
+
return host === 'localhost' || host === '127.0.0.1' || host === '::1';
|
|
27
|
+
} catch {
|
|
28
|
+
return raw.includes('127.0.0.1') || raw.includes('localhost');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isProxyCredential(value) {
|
|
33
|
+
const raw = normalizeString(value);
|
|
34
|
+
return raw === 'PROXY_KEY' || raw === 'CC_PROXY_KEY';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parseEnvReference(value) {
|
|
38
|
+
const raw = normalizeString(value);
|
|
39
|
+
if (!raw) return '';
|
|
40
|
+
let match = raw.match(/^\$\{?([A-Z_][A-Z0-9_]*)\}?$/);
|
|
41
|
+
if (match) return match[1];
|
|
42
|
+
match = raw.match(/^env:([A-Z_][A-Z0-9_]*)$/i);
|
|
43
|
+
if (match) return match[1].toUpperCase();
|
|
44
|
+
match = raw.match(/^process\.env\.([A-Z_][A-Z0-9_]*)$/);
|
|
45
|
+
if (match) return match[1];
|
|
46
|
+
if (/^[A-Z_][A-Z0-9_]*$/.test(raw) && /(?:API|KEY|TOKEN|SECRET|AUTH)/.test(raw)) {
|
|
47
|
+
return raw;
|
|
48
|
+
}
|
|
49
|
+
return '';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function resolveApiKeyValue(value, env = process.env) {
|
|
53
|
+
const raw = normalizeString(value);
|
|
54
|
+
if (!raw || isProxyCredential(raw)) {
|
|
55
|
+
return { value: '', source: '', envName: '' };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const envName = parseEnvReference(raw);
|
|
59
|
+
if (envName) {
|
|
60
|
+
const envValue = normalizeString(env?.[envName]);
|
|
61
|
+
if (isProxyCredential(envValue)) {
|
|
62
|
+
return { value: '', source: 'proxy-env', envName };
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
value: envValue,
|
|
66
|
+
source: envValue ? 'env' : 'env-missing',
|
|
67
|
+
envName
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return { value: raw, source: 'config', envName: '' };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function readActiveChannelId(platform) {
|
|
75
|
+
try {
|
|
76
|
+
ensureStorageDirMigrated?.();
|
|
77
|
+
const filePath = PATHS.activeChannel?.[platform];
|
|
78
|
+
if (!filePath || !fs.existsSync(filePath)) return null;
|
|
79
|
+
const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
80
|
+
return data?.activeChannelId || null;
|
|
81
|
+
} catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function selectLatestEnabledChannel(channels = []) {
|
|
87
|
+
const enabledChannels = (Array.isArray(channels) ? channels : [])
|
|
88
|
+
.filter(channel => channel && channel.enabled !== false);
|
|
89
|
+
if (enabledChannels.length === 0) return null;
|
|
90
|
+
return enabledChannels.reduce((latest, current) => {
|
|
91
|
+
const latestTs = Number(latest?.updatedAt || latest?.createdAt || 0);
|
|
92
|
+
const currentTs = Number(current?.updatedAt || current?.createdAt || 0);
|
|
93
|
+
return currentTs > latestTs ? current : latest;
|
|
94
|
+
}, enabledChannels[0]);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function resolveExistingActiveChannel(platform, channels = []) {
|
|
98
|
+
const allChannels = Array.isArray(channels) ? channels : [];
|
|
99
|
+
const activeChannelId = readActiveChannelId(platform);
|
|
100
|
+
if (activeChannelId) {
|
|
101
|
+
const matched = allChannels.find(channel => channel.id === activeChannelId);
|
|
102
|
+
if (matched) return matched;
|
|
103
|
+
}
|
|
104
|
+
return selectLatestEnabledChannel(allChannels)
|
|
105
|
+
|| allChannels.find(channel => channel.enabled !== false)
|
|
106
|
+
|| allChannels[0]
|
|
107
|
+
|| null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function secretHash(value) {
|
|
111
|
+
const raw = normalizeString(value);
|
|
112
|
+
if (!raw) return '';
|
|
113
|
+
return crypto.createHash('sha256').update(raw).digest('hex');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function channelIdentity(channel = {}) {
|
|
117
|
+
return {
|
|
118
|
+
id: normalizeString(channel.id),
|
|
119
|
+
providerKey: normalizeLower(channel.providerKey || channel.provider),
|
|
120
|
+
baseUrl: normalizeUrl(channel.baseUrl),
|
|
121
|
+
apiKeyHash: secretHash(channel.apiKey || channel.key),
|
|
122
|
+
model: normalizeLower(channel.model),
|
|
123
|
+
name: normalizeLower(channel.name)
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function defaultFindExistingChannel(channels = [], candidate = {}, options = {}) {
|
|
128
|
+
const target = channelIdentity(candidate);
|
|
129
|
+
const candidateId = normalizeString(candidate.id);
|
|
130
|
+
if (candidateId) {
|
|
131
|
+
const byId = channels.find(channel => channel.id === candidateId);
|
|
132
|
+
if (byId) return byId;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const matchers = Array.isArray(options.matchers) ? options.matchers : [];
|
|
136
|
+
for (const matcher of matchers) {
|
|
137
|
+
const matched = channels.find(channel => matcher(channel, candidate));
|
|
138
|
+
if (matched) return matched;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (target.providerKey) {
|
|
142
|
+
const byProvider = channels.find(channel => channelIdentity(channel).providerKey === target.providerKey);
|
|
143
|
+
if (byProvider) return byProvider;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (target.baseUrl && target.apiKeyHash) {
|
|
147
|
+
const byUrlAndKey = channels.find((channel) => {
|
|
148
|
+
const identity = channelIdentity(channel);
|
|
149
|
+
return identity.baseUrl === target.baseUrl && identity.apiKeyHash === target.apiKeyHash;
|
|
150
|
+
});
|
|
151
|
+
if (byUrlAndKey) return byUrlAndKey;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (target.baseUrl && target.model) {
|
|
155
|
+
const byUrlAndModel = channels.find((channel) => {
|
|
156
|
+
const identity = channelIdentity(channel);
|
|
157
|
+
return identity.baseUrl === target.baseUrl && identity.model === target.model;
|
|
158
|
+
});
|
|
159
|
+
if (byUrlAndModel) return byUrlAndModel;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (target.baseUrl) {
|
|
163
|
+
return channels.find(channel => channelIdentity(channel).baseUrl === target.baseUrl) || null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function shouldPreserveField(field, existing, candidate) {
|
|
170
|
+
if (field === 'name') {
|
|
171
|
+
return normalizeString(existing?.name) && normalizeLower(existing.name) !== 'default';
|
|
172
|
+
}
|
|
173
|
+
return Object.prototype.hasOwnProperty.call(existing || {}, field)
|
|
174
|
+
&& !Object.prototype.hasOwnProperty.call(candidate || {}, field);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function mergeSyncedChannel(existing, candidate, options = {}) {
|
|
178
|
+
const preserveFields = options.preserveFields || [
|
|
179
|
+
'id',
|
|
180
|
+
'createdAt',
|
|
181
|
+
'name',
|
|
182
|
+
'enabled',
|
|
183
|
+
'weight',
|
|
184
|
+
'maxConcurrency',
|
|
185
|
+
'balanceToken',
|
|
186
|
+
'balanceUserId'
|
|
187
|
+
];
|
|
188
|
+
|
|
189
|
+
const merged = {
|
|
190
|
+
...existing,
|
|
191
|
+
...candidate,
|
|
192
|
+
id: existing.id,
|
|
193
|
+
createdAt: existing.createdAt || candidate.createdAt || Date.now(),
|
|
194
|
+
updatedAt: Date.now()
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
for (const field of preserveFields) {
|
|
198
|
+
if (shouldPreserveField(field, existing, candidate)) {
|
|
199
|
+
merged[field] = existing[field];
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return merged;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function stableComparableValue(value) {
|
|
207
|
+
if (Array.isArray(value)) {
|
|
208
|
+
return value.map(stableComparableValue);
|
|
209
|
+
}
|
|
210
|
+
if (!value || typeof value !== 'object') {
|
|
211
|
+
return value;
|
|
212
|
+
}
|
|
213
|
+
return Object.keys(value)
|
|
214
|
+
.sort()
|
|
215
|
+
.reduce((acc, key) => {
|
|
216
|
+
acc[key] = stableComparableValue(value[key]);
|
|
217
|
+
return acc;
|
|
218
|
+
}, {});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function comparableChannel(channel = {}) {
|
|
222
|
+
const copy = { ...channel };
|
|
223
|
+
delete copy.updatedAt;
|
|
224
|
+
delete copy.health;
|
|
225
|
+
delete copy.websiteUrl;
|
|
226
|
+
return JSON.stringify(stableComparableValue(copy));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function sanitizeChannel(channel) {
|
|
230
|
+
if (!channel) return null;
|
|
231
|
+
const sanitized = {
|
|
232
|
+
id: channel.id,
|
|
233
|
+
name: channel.name,
|
|
234
|
+
baseUrl: channel.baseUrl,
|
|
235
|
+
enabled: channel.enabled !== false
|
|
236
|
+
};
|
|
237
|
+
if (channel.providerKey) sanitized.providerKey = channel.providerKey;
|
|
238
|
+
if (channel.model) sanitized.model = channel.model;
|
|
239
|
+
if (channel.credentialSource) sanitized.credentialSource = channel.credentialSource;
|
|
240
|
+
return sanitized;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function createSkippedResult(toolType, warning, channel = null) {
|
|
244
|
+
return {
|
|
245
|
+
success: true,
|
|
246
|
+
toolType,
|
|
247
|
+
added: 0,
|
|
248
|
+
updated: 0,
|
|
249
|
+
skipped: 1,
|
|
250
|
+
channels: channel ? [sanitizeChannel(channel)] : [],
|
|
251
|
+
warnings: warning ? [warning] : []
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function upsertSyncedChannels({
|
|
256
|
+
toolType,
|
|
257
|
+
loadChannels,
|
|
258
|
+
saveChannels,
|
|
259
|
+
applyDefaults,
|
|
260
|
+
candidates,
|
|
261
|
+
matchers,
|
|
262
|
+
preserveFields
|
|
263
|
+
}) {
|
|
264
|
+
const data = loadChannels();
|
|
265
|
+
const channels = Array.isArray(data?.channels) ? [...data.channels] : [];
|
|
266
|
+
const result = {
|
|
267
|
+
success: true,
|
|
268
|
+
toolType,
|
|
269
|
+
added: 0,
|
|
270
|
+
updated: 0,
|
|
271
|
+
skipped: 0,
|
|
272
|
+
channels: [],
|
|
273
|
+
warnings: []
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
let changed = false;
|
|
277
|
+
for (const rawCandidate of candidates || []) {
|
|
278
|
+
const candidate = rawCandidate || {};
|
|
279
|
+
if (candidate.warning) {
|
|
280
|
+
result.warnings.push(candidate.warning);
|
|
281
|
+
}
|
|
282
|
+
if (candidate.skip) {
|
|
283
|
+
result.skipped += 1;
|
|
284
|
+
if (candidate.channel) result.channels.push(sanitizeChannel(candidate.channel));
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
if (!normalizeString(candidate.baseUrl)) {
|
|
288
|
+
result.skipped += 1;
|
|
289
|
+
result.warnings.push(`${toolType}: missing baseUrl, skipped current channel sync`);
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
if (!normalizeString(candidate.apiKey)) {
|
|
293
|
+
result.skipped += 1;
|
|
294
|
+
result.warnings.push(`${toolType}: missing API key or OAuth-only config, skipped current channel sync`);
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const normalizedCandidate = applyDefaults ? applyDefaults(candidate) : candidate;
|
|
299
|
+
const existing = defaultFindExistingChannel(channels, normalizedCandidate, { matchers });
|
|
300
|
+
if (existing) {
|
|
301
|
+
const merged = applyDefaults
|
|
302
|
+
? applyDefaults(mergeSyncedChannel(existing, normalizedCandidate, { preserveFields }))
|
|
303
|
+
: mergeSyncedChannel(existing, normalizedCandidate, { preserveFields });
|
|
304
|
+
if (comparableChannel(existing) === comparableChannel(merged)) {
|
|
305
|
+
result.skipped += 1;
|
|
306
|
+
result.channels.push(sanitizeChannel(existing));
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
const index = channels.findIndex(channel => channel.id === existing.id);
|
|
310
|
+
channels[index] = merged;
|
|
311
|
+
changed = true;
|
|
312
|
+
result.updated += 1;
|
|
313
|
+
result.channels.push(sanitizeChannel(merged));
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const now = Date.now();
|
|
318
|
+
const created = applyDefaults ? applyDefaults({
|
|
319
|
+
id: candidate.id || crypto.randomUUID(),
|
|
320
|
+
enabled: candidate.enabled !== false,
|
|
321
|
+
weight: candidate.weight || 1,
|
|
322
|
+
maxConcurrency: candidate.maxConcurrency || null,
|
|
323
|
+
...normalizedCandidate,
|
|
324
|
+
createdAt: now,
|
|
325
|
+
updatedAt: now
|
|
326
|
+
}) : {
|
|
327
|
+
id: candidate.id || crypto.randomUUID(),
|
|
328
|
+
enabled: candidate.enabled !== false,
|
|
329
|
+
weight: candidate.weight || 1,
|
|
330
|
+
maxConcurrency: candidate.maxConcurrency || null,
|
|
331
|
+
...normalizedCandidate,
|
|
332
|
+
createdAt: now,
|
|
333
|
+
updatedAt: now
|
|
334
|
+
};
|
|
335
|
+
channels.push(created);
|
|
336
|
+
changed = true;
|
|
337
|
+
result.added += 1;
|
|
338
|
+
result.channels.push(sanitizeChannel(created));
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (changed) {
|
|
342
|
+
const dir = path.dirname(loadChannels.filePath || '');
|
|
343
|
+
if (dir && dir !== '.' && !fs.existsSync(dir)) {
|
|
344
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
345
|
+
}
|
|
346
|
+
saveChannels({ ...data, channels });
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
result.warnings = [...new Set(result.warnings.filter(Boolean))];
|
|
350
|
+
return result;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
module.exports = {
|
|
354
|
+
createSkippedResult,
|
|
355
|
+
defaultFindExistingChannel,
|
|
356
|
+
isLocalProxyBaseUrl,
|
|
357
|
+
isProxyCredential,
|
|
358
|
+
normalizeLower,
|
|
359
|
+
normalizeString,
|
|
360
|
+
normalizeUrl,
|
|
361
|
+
readActiveChannelId,
|
|
362
|
+
resolveApiKeyValue,
|
|
363
|
+
resolveExistingActiveChannel,
|
|
364
|
+
sanitizeChannel,
|
|
365
|
+
secretHash,
|
|
366
|
+
selectLatestEnabledChannel,
|
|
367
|
+
upsertSyncedChannels
|
|
368
|
+
};
|
|
@@ -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
|
};
|