coding-tool-x 3.6.3 → 3.6.5
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-D0BDj7bo.js → Analytics-CcGKjDkP.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-BnwWmyV7.js → ConfigTemplates-DPOa1w5D.js} +1 -1
- package/dist/web/assets/{Home-Yw5bTgrH.js → Home-BP1LxGtp.js} +1 -1
- package/dist/web/assets/{PluginManager-CRdRLKsS.js → PluginManager-BVbHUdey.js} +1 -1
- package/dist/web/assets/{ProjectList-CDIYAOyV.js → ProjectList-BQrz2FKo.js} +1 -1
- package/dist/web/assets/SessionList-CRGer64T.js +1 -0
- package/dist/web/assets/{SkillManager-DaM0Ob6l.js → SkillManager-BhJKtWYM.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-Y-EZ7NxE.js → WorkspaceManager-pc6tpF3m.js} +1 -1
- package/dist/web/assets/{icons-U50_Cfbe.js → icons-QFXzEgqM.js} +1 -1
- package/dist/web/assets/index-0isNzXNY.css +1 -0
- package/dist/web/assets/index-BsdIJwLf.js +2 -0
- package/dist/web/assets/{naive-ui-Cjh6GLto.js → naive-ui-DM897mIP.js} +1 -1
- package/dist/web/assets/{vendors--KP7Ux0s.js → vendors-DQA4Hw9v.js} +1 -1
- package/dist/web/assets/{vue-vendor-C5aMkRmm.js → vue-vendor-B5RqjEjQ.js} +1 -1
- package/dist/web/index.html +6 -6
- package/package.json +1 -1
- package/src/commands/daemon.js +13 -4
- package/src/server/api/channels.js +6 -2
- package/src/server/api/codex-channels.js +5 -1
- package/src/server/api/codex-sessions.js +24 -1
- package/src/server/api/gemini-channels.js +6 -2
- package/src/server/api/opencode-channels.js +6 -2
- package/src/server/services/channel-balance.js +948 -67
- package/src/server/services/gemini-channels.js +8 -0
- package/src/server/services/opencode-channels.js +6 -0
- package/dist/web/assets/SessionList-BJ2kbl-3.js +0 -1
- package/dist/web/assets/index-BkLQex1R.css +0 -1
- package/dist/web/assets/index-QeTxUv96.js +0 -2
|
@@ -10,8 +10,12 @@ const MAX_RESPONSE_BYTES = 1024 * 1024;
|
|
|
10
10
|
const QUOTA_UNIT = 500000;
|
|
11
11
|
const VELOERA_QUOTA_UNIT = 1000000;
|
|
12
12
|
const VALID_SOURCES = new Set(['claude', 'codex', 'gemini', 'opencode']);
|
|
13
|
+
const GATEWAY_PATH_SEGMENTS = new Set(['openai', 'anthropic', 'claude', 'gemini', 'codex']);
|
|
14
|
+
const HUB_PLATFORMS = new Set(['new-api', 'one-api', 'one-hub', 'done-hub', 'veloera', 'anyrouter', 'aihubmix']);
|
|
15
|
+
const UNSUPPORTED_BALANCE_PLATFORMS = new Set(['dashscope', 'modelscope']);
|
|
13
16
|
|
|
14
17
|
const balanceCache = new Map();
|
|
18
|
+
const balanceStrategyCache = new Map();
|
|
15
19
|
|
|
16
20
|
function nowIso(now = Date.now()) {
|
|
17
21
|
return new Date(now).toISOString();
|
|
@@ -22,6 +26,41 @@ function stripBearer(value) {
|
|
|
22
26
|
return token.toLowerCase().startsWith('bearer ') ? token.slice(7).trim() : token;
|
|
23
27
|
}
|
|
24
28
|
|
|
29
|
+
function resolveBalanceToken(channel = {}) {
|
|
30
|
+
return stripBearer(
|
|
31
|
+
channel.balanceToken
|
|
32
|
+
|| channel.balanceApiKey
|
|
33
|
+
|| channel.balanceAuthToken
|
|
34
|
+
|| channel.apiKey
|
|
35
|
+
|| channel.token
|
|
36
|
+
|| channel.authToken
|
|
37
|
+
|| ''
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function resolveBalanceUserId(channel = {}) {
|
|
42
|
+
const raw = channel.balanceUserId
|
|
43
|
+
?? channel.platformUserId
|
|
44
|
+
?? channel.newApiUserId
|
|
45
|
+
?? null;
|
|
46
|
+
const parsed = parseFiniteNumber(raw);
|
|
47
|
+
if (parsed == null || parsed <= 0) return null;
|
|
48
|
+
return Math.trunc(parsed);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function buildUserIdHeaders(userId) {
|
|
52
|
+
if (!userId) return {};
|
|
53
|
+
const value = String(userId);
|
|
54
|
+
return {
|
|
55
|
+
'New-Api-User': value,
|
|
56
|
+
'Veloera-User': value,
|
|
57
|
+
'voapi-user': value,
|
|
58
|
+
'User-id': value,
|
|
59
|
+
'Rix-Api-User': value,
|
|
60
|
+
'neo-api-user': value
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
25
64
|
function buildAuthHeaders(token, extra = {}) {
|
|
26
65
|
return {
|
|
27
66
|
Accept: 'application/json',
|
|
@@ -30,6 +69,46 @@ function buildAuthHeaders(token, extra = {}) {
|
|
|
30
69
|
};
|
|
31
70
|
}
|
|
32
71
|
|
|
72
|
+
function hasDedicatedBalanceToken(channel = {}) {
|
|
73
|
+
return !!String(
|
|
74
|
+
channel.balanceToken
|
|
75
|
+
|| channel.balanceApiKey
|
|
76
|
+
|| channel.balanceAuthToken
|
|
77
|
+
|| ''
|
|
78
|
+
).trim();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function buildCookieCandidates(token) {
|
|
82
|
+
const raw = stripBearer(token);
|
|
83
|
+
if (!raw) return [];
|
|
84
|
+
|
|
85
|
+
const candidates = [];
|
|
86
|
+
const normalized = raw.replace(/^cookie:\s*/i, '').trim();
|
|
87
|
+
if (!normalized) return [];
|
|
88
|
+
if (normalized.includes('=')) {
|
|
89
|
+
candidates.push(normalized);
|
|
90
|
+
}
|
|
91
|
+
candidates.push(`session=${normalized}`);
|
|
92
|
+
candidates.push(`token=${normalized}`);
|
|
93
|
+
return Array.from(new Set(candidates));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function shouldTryCookieAuth(token, channel = {}, hintedPlatform = null) {
|
|
97
|
+
const raw = stripBearer(token);
|
|
98
|
+
if (!raw) return false;
|
|
99
|
+
return hasDedicatedBalanceToken(channel)
|
|
100
|
+
|| raw.includes('=')
|
|
101
|
+
|| (hintedPlatform === 'anyrouter' && !isLikelyModelApiKey(raw));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function isLikelyModelApiKey(token) {
|
|
105
|
+
return /^sk-[a-z0-9]/i.test(stripBearer(token));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function isLikelyOpenRouterApiKey(token) {
|
|
109
|
+
return /^sk-or-/i.test(stripBearer(token));
|
|
110
|
+
}
|
|
111
|
+
|
|
33
112
|
function parseFiniteNumber(value) {
|
|
34
113
|
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
35
114
|
if (typeof value === 'string') {
|
|
@@ -44,6 +123,11 @@ function roundMoney(value) {
|
|
|
44
123
|
return Math.round(value * 1000000) / 1000000;
|
|
45
124
|
}
|
|
46
125
|
|
|
126
|
+
function normalizeQuotaUnit(value, fallback = QUOTA_UNIT) {
|
|
127
|
+
const parsed = parseFiniteNumber(value);
|
|
128
|
+
return parsed != null && parsed > 0 ? parsed : fallback;
|
|
129
|
+
}
|
|
130
|
+
|
|
47
131
|
function formatMoney(value) {
|
|
48
132
|
if (!Number.isFinite(value)) return '';
|
|
49
133
|
return `$${value.toFixed(2)}`;
|
|
@@ -144,7 +228,13 @@ function joinUrl(baseUrl, endpoint) {
|
|
|
144
228
|
return `${base}${path}`;
|
|
145
229
|
}
|
|
146
230
|
|
|
147
|
-
function requestJson(url, {
|
|
231
|
+
function requestJson(url, {
|
|
232
|
+
method = 'GET',
|
|
233
|
+
headers = {},
|
|
234
|
+
body,
|
|
235
|
+
timeoutMs = REQUEST_TIMEOUT_MS,
|
|
236
|
+
redirectCount = 0
|
|
237
|
+
} = {}) {
|
|
148
238
|
return new Promise((resolve, reject) => {
|
|
149
239
|
let parsed;
|
|
150
240
|
try {
|
|
@@ -155,13 +245,21 @@ function requestJson(url, { headers = {}, timeoutMs = REQUEST_TIMEOUT_MS } = {})
|
|
|
155
245
|
}
|
|
156
246
|
|
|
157
247
|
const client = parsed.protocol === 'http:' ? http : https;
|
|
248
|
+
const requestBody = body === undefined
|
|
249
|
+
? null
|
|
250
|
+
: (typeof body === 'string' || Buffer.isBuffer(body) ? body : JSON.stringify(body));
|
|
251
|
+
const requestHeaders = { ...headers };
|
|
252
|
+
if (requestBody != null && !Object.keys(requestHeaders).some(key => key.toLowerCase() === 'content-length')) {
|
|
253
|
+
requestHeaders['Content-Length'] = Buffer.byteLength(requestBody);
|
|
254
|
+
}
|
|
255
|
+
|
|
158
256
|
const req = client.request({
|
|
159
257
|
protocol: parsed.protocol,
|
|
160
258
|
hostname: parsed.hostname,
|
|
161
259
|
port: parsed.port,
|
|
162
260
|
path: `${parsed.pathname}${parsed.search}`,
|
|
163
|
-
method
|
|
164
|
-
headers
|
|
261
|
+
method,
|
|
262
|
+
headers: requestHeaders
|
|
165
263
|
}, (res) => {
|
|
166
264
|
const chunks = [];
|
|
167
265
|
let total = 0;
|
|
@@ -177,17 +275,42 @@ function requestJson(url, { headers = {}, timeoutMs = REQUEST_TIMEOUT_MS } = {})
|
|
|
177
275
|
|
|
178
276
|
res.on('end', () => {
|
|
179
277
|
const raw = Buffer.concat(chunks).toString('utf8');
|
|
278
|
+
let payload = null;
|
|
279
|
+
let parseError = null;
|
|
280
|
+
if (raw) {
|
|
281
|
+
try {
|
|
282
|
+
payload = JSON.parse(raw);
|
|
283
|
+
} catch (error) {
|
|
284
|
+
parseError = error;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if ([301, 302, 303, 307, 308].includes(res.statusCode) && res.headers.location && redirectCount < 3) {
|
|
288
|
+
try {
|
|
289
|
+
const nextUrl = new URL(res.headers.location, parsed).toString();
|
|
290
|
+
resolve(requestJson(nextUrl, {
|
|
291
|
+
method: res.statusCode === 303 ? 'GET' : method,
|
|
292
|
+
headers,
|
|
293
|
+
body: res.statusCode === 303 ? undefined : body,
|
|
294
|
+
timeoutMs,
|
|
295
|
+
redirectCount: redirectCount + 1
|
|
296
|
+
}));
|
|
297
|
+
} catch (error) {
|
|
298
|
+
reject(error);
|
|
299
|
+
}
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
180
302
|
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
181
303
|
const error = new Error(`HTTP ${res.statusCode}`);
|
|
182
304
|
error.statusCode = res.statusCode;
|
|
305
|
+
error.payload = payload;
|
|
183
306
|
reject(error);
|
|
184
307
|
return;
|
|
185
308
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
reject(error);
|
|
309
|
+
if (parseError) {
|
|
310
|
+
reject(parseError);
|
|
311
|
+
return;
|
|
190
312
|
}
|
|
313
|
+
resolve(payload);
|
|
191
314
|
});
|
|
192
315
|
});
|
|
193
316
|
|
|
@@ -195,6 +318,9 @@ function requestJson(url, { headers = {}, timeoutMs = REQUEST_TIMEOUT_MS } = {})
|
|
|
195
318
|
req.destroy(new Error('request timeout'));
|
|
196
319
|
});
|
|
197
320
|
req.on('error', reject);
|
|
321
|
+
if (requestBody != null) {
|
|
322
|
+
req.write(requestBody);
|
|
323
|
+
}
|
|
198
324
|
req.end();
|
|
199
325
|
});
|
|
200
326
|
}
|
|
@@ -202,13 +328,22 @@ function requestJson(url, { headers = {}, timeoutMs = REQUEST_TIMEOUT_MS } = {})
|
|
|
202
328
|
async function tryRequestJson(url, options) {
|
|
203
329
|
try {
|
|
204
330
|
return await requestJson(url, options);
|
|
205
|
-
} catch {
|
|
331
|
+
} catch (error) {
|
|
332
|
+
if (error?.payload && typeof error.payload === 'object') {
|
|
333
|
+
return error.payload;
|
|
334
|
+
}
|
|
206
335
|
return null;
|
|
207
336
|
}
|
|
208
337
|
}
|
|
209
338
|
|
|
210
|
-
function detectPlatformByUrlHint(
|
|
211
|
-
const value = String(
|
|
339
|
+
function detectPlatformByUrlHint(...values) {
|
|
340
|
+
const value = values.map(item => String(item || '')).join(' ').toLowerCase();
|
|
341
|
+
if (value.includes('88code') || value.includes('88-code') || value.includes('rainapp.top')) return '88code';
|
|
342
|
+
if (value.includes('openrouter.ai') || value.includes('openrouter')) return 'openrouter';
|
|
343
|
+
if (value.includes('aihubmix')) return 'aihubmix';
|
|
344
|
+
if (value.includes('dashscope') || value.includes('dashscope.aliyuncs.com') || value.includes('compatible-mode')) return 'dashscope';
|
|
345
|
+
if (value.includes('modelscope') || value.includes('api-inference.modelscope.cn')) return 'modelscope';
|
|
346
|
+
if (value.includes('siliconflow') || value.includes('siliconflow.cn')) return 'siliconflow';
|
|
212
347
|
if (value.includes('sub2api')) return 'sub2api';
|
|
213
348
|
if (value.includes('anyrouter')) return 'anyrouter';
|
|
214
349
|
if (value.includes('donehub') || value.includes('done-hub')) return 'done-hub';
|
|
@@ -220,14 +355,17 @@ function detectPlatformByUrlHint(baseUrl) {
|
|
|
220
355
|
}
|
|
221
356
|
|
|
222
357
|
function resolveStatusPlatform(payload, hintedPlatform = null) {
|
|
223
|
-
if (!payload || payload
|
|
224
|
-
const
|
|
225
|
-
|
|
358
|
+
if (!payload || typeof payload !== 'object') return null;
|
|
359
|
+
const data = payload.data && typeof payload.data === 'object' ? payload.data : payload;
|
|
360
|
+
const systemName = typeof data?.system_name === 'string'
|
|
361
|
+
? data.system_name.toLowerCase()
|
|
226
362
|
: '';
|
|
227
|
-
const version = typeof
|
|
228
|
-
?
|
|
363
|
+
const version = typeof data?.version === 'string'
|
|
364
|
+
? data.version.toLowerCase()
|
|
229
365
|
: '';
|
|
366
|
+
if (payload.success !== true && !systemName && !version) return null;
|
|
230
367
|
|
|
368
|
+
if (hintedPlatform === 'aihubmix' || systemName.includes('aihubmix')) return 'aihubmix';
|
|
231
369
|
if (hintedPlatform === 'anyrouter') return 'anyrouter';
|
|
232
370
|
if (hintedPlatform === 'done-hub') return 'done-hub';
|
|
233
371
|
if (hintedPlatform === 'one-hub') return 'one-hub';
|
|
@@ -246,24 +384,15 @@ function extractSub2ApiData(payload) {
|
|
|
246
384
|
return payload;
|
|
247
385
|
}
|
|
248
386
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
if (
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
if (statusPlatform) return statusPlatform;
|
|
259
|
-
|
|
260
|
-
const sub2Payload = await tryRequestJson(joinUrl(baseUrl, '/api/v1/auth/me'), { headers });
|
|
261
|
-
const sub2Data = extractSub2ApiData(sub2Payload);
|
|
262
|
-
if (sub2Data && Object.prototype.hasOwnProperty.call(sub2Data, 'balance')) {
|
|
263
|
-
return 'sub2api';
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
return hintedPlatform;
|
|
387
|
+
function isSub2ApiUsagePayload(payload) {
|
|
388
|
+
if (!payload || typeof payload !== 'object') return false;
|
|
389
|
+
const mode = typeof payload.mode === 'string' ? payload.mode.trim().toLowerCase() : '';
|
|
390
|
+
if (!['quota_limited', 'unrestricted'].includes(mode)) return false;
|
|
391
|
+
return Object.prototype.hasOwnProperty.call(payload, 'remaining')
|
|
392
|
+
|| Object.prototype.hasOwnProperty.call(payload, 'balance')
|
|
393
|
+
|| (payload.quota && typeof payload.quota === 'object')
|
|
394
|
+
|| (payload.subscription && typeof payload.subscription === 'object')
|
|
395
|
+
|| (payload.usage && typeof payload.usage === 'object');
|
|
267
396
|
}
|
|
268
397
|
|
|
269
398
|
function extractUserSelfData(payload) {
|
|
@@ -278,7 +407,9 @@ function buildHubBalanceSnapshot(platform, payload) {
|
|
|
278
407
|
const data = extractUserSelfData(payload);
|
|
279
408
|
if (!data) return makeHiddenSnapshot(platform);
|
|
280
409
|
|
|
281
|
-
const unit = platform === 'veloera'
|
|
410
|
+
const unit = platform === 'veloera'
|
|
411
|
+
? VELOERA_QUOTA_UNIT
|
|
412
|
+
: normalizeQuotaUnit(data.quota_per_unit || data.quotaPerUnit);
|
|
282
413
|
const quotaRaw = parseFiniteNumber(data.quota);
|
|
283
414
|
const usedRaw = parseFiniteNumber(data.used_quota);
|
|
284
415
|
if (quotaRaw == null) return makeHiddenSnapshot(platform);
|
|
@@ -288,7 +419,7 @@ function buildHubBalanceSnapshot(platform, payload) {
|
|
|
288
419
|
let remaining;
|
|
289
420
|
let total;
|
|
290
421
|
|
|
291
|
-
if (platform === 'new-api' || platform === 'anyrouter' || platform === 'done-hub') {
|
|
422
|
+
if (platform === 'new-api' || platform === 'anyrouter' || platform === 'done-hub' || platform === 'aihubmix') {
|
|
292
423
|
remaining = quota;
|
|
293
424
|
total = quota + used;
|
|
294
425
|
} else {
|
|
@@ -303,6 +434,102 @@ function buildHubBalanceSnapshot(platform, payload) {
|
|
|
303
434
|
});
|
|
304
435
|
}
|
|
305
436
|
|
|
437
|
+
function extractNewApiTokenUsageData(payload) {
|
|
438
|
+
if (!payload || typeof payload !== 'object') return null;
|
|
439
|
+
const data = payload.data && typeof payload.data === 'object' ? payload.data : payload;
|
|
440
|
+
if (!data || typeof data !== 'object') return null;
|
|
441
|
+
if (!Object.prototype.hasOwnProperty.call(data, 'total_available')) return null;
|
|
442
|
+
return data;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function buildNewApiTokenUsageSnapshot(payload, { quotaUnit = QUOTA_UNIT } = {}) {
|
|
446
|
+
const data = extractNewApiTokenUsageData(payload);
|
|
447
|
+
if (!data) return makeHiddenSnapshot('new-api');
|
|
448
|
+
|
|
449
|
+
const availableRaw = parseFiniteNumber(data.total_available);
|
|
450
|
+
if (availableRaw == null) return makeHiddenSnapshot('new-api');
|
|
451
|
+
const totalRaw = parseFiniteNumber(data.total_granted);
|
|
452
|
+
const usedRaw = parseFiniteNumber(data.total_used);
|
|
453
|
+
const unit = normalizeQuotaUnit(data.quota_per_unit || data.quotaPerUnit || quotaUnit);
|
|
454
|
+
|
|
455
|
+
const remaining = Math.max(0, availableRaw / unit);
|
|
456
|
+
return makeVisibleSnapshot('new-api', {
|
|
457
|
+
remaining,
|
|
458
|
+
used: usedRaw != null ? usedRaw / unit : undefined,
|
|
459
|
+
total: totalRaw != null ? totalRaw / unit : undefined
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function extractOpenRouterData(payload) {
|
|
464
|
+
if (!payload || typeof payload !== 'object') return null;
|
|
465
|
+
const data = payload.data && typeof payload.data === 'object' ? payload.data : payload;
|
|
466
|
+
return data && typeof data === 'object' ? data : null;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function buildOpenRouterKeySnapshot(payload) {
|
|
470
|
+
const data = extractOpenRouterData(payload);
|
|
471
|
+
if (!data) return makeHiddenSnapshot('openrouter');
|
|
472
|
+
|
|
473
|
+
const limitRemaining = parseFiniteNumber(data.limit_remaining ?? data.limitRemaining);
|
|
474
|
+
const limit = parseFiniteNumber(data.limit);
|
|
475
|
+
const usage = parseFiniteNumber(data.usage);
|
|
476
|
+
if (limitRemaining != null) {
|
|
477
|
+
return makeVisibleSnapshot('openrouter', {
|
|
478
|
+
remaining: Math.max(0, limitRemaining),
|
|
479
|
+
used: usage,
|
|
480
|
+
total: limit
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (limit != null && usage != null) {
|
|
485
|
+
return makeVisibleSnapshot('openrouter', {
|
|
486
|
+
remaining: Math.max(0, limit - usage),
|
|
487
|
+
used: usage,
|
|
488
|
+
total: limit
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return makeHiddenSnapshot('openrouter');
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function buildOpenRouterCreditsSnapshot(payload) {
|
|
496
|
+
const data = extractOpenRouterData(payload);
|
|
497
|
+
if (!data) return makeHiddenSnapshot('openrouter');
|
|
498
|
+
|
|
499
|
+
const totalCredits = parseFiniteNumber(data.total_credits ?? data.totalCredits);
|
|
500
|
+
const totalUsage = parseFiniteNumber(data.total_usage ?? data.totalUsage);
|
|
501
|
+
if (totalCredits == null || totalUsage == null) {
|
|
502
|
+
return makeHiddenSnapshot('openrouter');
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return makeVisibleSnapshot('openrouter', {
|
|
506
|
+
remaining: Math.max(0, totalCredits - totalUsage),
|
|
507
|
+
used: totalUsage,
|
|
508
|
+
total: totalCredits
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function buildSiliconFlowUserInfoSnapshot(payload) {
|
|
513
|
+
const data = payload?.data && typeof payload.data === 'object' ? payload.data : payload;
|
|
514
|
+
if (!data || typeof data !== 'object') return makeHiddenSnapshot('siliconflow');
|
|
515
|
+
|
|
516
|
+
const remaining = parseFiniteNumber(data.balance);
|
|
517
|
+
if (remaining == null) return makeHiddenSnapshot('siliconflow');
|
|
518
|
+
const chargeBalance = parseFiniteNumber(data.chargeBalance);
|
|
519
|
+
const totalBalance = parseFiniteNumber(data.totalBalance);
|
|
520
|
+
return makeVisibleSnapshot('siliconflow', {
|
|
521
|
+
remaining,
|
|
522
|
+
used: totalBalance != null ? Math.max(0, totalBalance - remaining) : undefined,
|
|
523
|
+
total: totalBalance ?? chargeBalance ?? remaining
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function extractQuotaUnitFromStatus(payload) {
|
|
528
|
+
if (!payload || typeof payload !== 'object') return null;
|
|
529
|
+
const data = payload.data && typeof payload.data === 'object' ? payload.data : payload;
|
|
530
|
+
return normalizeQuotaUnit(data?.quota_per_unit || data?.quotaPerUnit, null);
|
|
531
|
+
}
|
|
532
|
+
|
|
306
533
|
function parseSubscriptionItems(payload) {
|
|
307
534
|
const data = extractSub2ApiData(payload);
|
|
308
535
|
const candidates = [
|
|
@@ -352,14 +579,460 @@ async function fetchSub2ApiSubscriptionSummary(baseUrl, headers) {
|
|
|
352
579
|
return {};
|
|
353
580
|
}
|
|
354
581
|
|
|
355
|
-
|
|
582
|
+
function readSub2ApiMonthlyRemaining(subscription) {
|
|
583
|
+
if (!subscription || typeof subscription !== 'object') return null;
|
|
584
|
+
const used = readMoneyField(subscription, 'monthlyUsedUsd', 'monthly_usage_usd', 'monthly_used_usd', 'monthly_used', 'used_monthly_usd') || 0;
|
|
585
|
+
const limit = readMoneyField(subscription, 'monthlyLimitUsd', 'monthly_limit_usd', 'monthly_limit', 'limit_monthly_usd');
|
|
586
|
+
if (limit == null) return null;
|
|
587
|
+
return Math.max(0, limit - used);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function nonNegativeMoney(value) {
|
|
591
|
+
const parsed = parseFiniteNumber(value);
|
|
592
|
+
return parsed != null && parsed >= 0 ? parsed : null;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function buildSub2ApiUsageSnapshot(payload) {
|
|
596
|
+
if (!isSub2ApiUsagePayload(payload)) return makeHiddenSnapshot('sub2api');
|
|
597
|
+
if (payload.isValid === false) return makeHiddenSnapshot('sub2api');
|
|
598
|
+
|
|
599
|
+
const quota = payload.quota && typeof payload.quota === 'object' ? payload.quota : null;
|
|
600
|
+
if (quota) {
|
|
601
|
+
const remaining = nonNegativeMoney(quota.remaining) ?? nonNegativeMoney(payload.remaining);
|
|
602
|
+
if (remaining != null) {
|
|
603
|
+
return makeVisibleSnapshot('sub2api', {
|
|
604
|
+
remaining,
|
|
605
|
+
used: readMoneyField(quota, 'used', 'quota_used', 'used_usd'),
|
|
606
|
+
total: readMoneyField(quota, 'limit', 'quota', 'total')
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const remaining = nonNegativeMoney(payload.balance) ?? nonNegativeMoney(payload.remaining);
|
|
612
|
+
const monthlyRemaining = readSub2ApiMonthlyRemaining(payload.subscription);
|
|
613
|
+
return makeVisibleSnapshot('sub2api', {
|
|
614
|
+
remaining,
|
|
615
|
+
monthlyRemaining
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function addUnique(list, value) {
|
|
620
|
+
if (value && !list.includes(value)) {
|
|
621
|
+
list.push(value);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function toOriginApiBase(value) {
|
|
626
|
+
try {
|
|
627
|
+
const parsed = new URL(value);
|
|
628
|
+
return `${parsed.origin}/api`;
|
|
629
|
+
} catch {
|
|
630
|
+
return '';
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function toPathApiBase(value) {
|
|
635
|
+
try {
|
|
636
|
+
const normalized = normalizeBaseUrl(value);
|
|
637
|
+
if (!normalized) return '';
|
|
638
|
+
return normalized.toLowerCase().endsWith('/api') ? normalized : joinUrl(normalized, '/api');
|
|
639
|
+
} catch {
|
|
640
|
+
return '';
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function toOpenRouterApiBase(value) {
|
|
645
|
+
try {
|
|
646
|
+
const parsed = new URL(String(value || '').trim());
|
|
647
|
+
parsed.hash = '';
|
|
648
|
+
parsed.search = '';
|
|
649
|
+
const origin = parsed.origin;
|
|
650
|
+
const host = parsed.hostname.toLowerCase();
|
|
651
|
+
const pathname = (parsed.pathname || '').replace(/\/+$/, '');
|
|
652
|
+
const lower = pathname.toLowerCase();
|
|
653
|
+
|
|
654
|
+
if (!pathname || pathname === '/') {
|
|
655
|
+
return `${origin}/api/v1`;
|
|
656
|
+
}
|
|
657
|
+
if (lower.endsWith('/api/v1')) {
|
|
658
|
+
return `${origin}${pathname}`;
|
|
659
|
+
}
|
|
660
|
+
if (lower.endsWith('/api')) {
|
|
661
|
+
return `${origin}${pathname}/v1`;
|
|
662
|
+
}
|
|
663
|
+
if (lower.endsWith('/v1')) {
|
|
664
|
+
const parent = pathname.slice(0, -3).replace(/\/+$/, '');
|
|
665
|
+
if (!parent || parent === '') return `${origin}/api/v1`;
|
|
666
|
+
if (parent.toLowerCase().endsWith('/api')) return `${origin}${parent}/v1`;
|
|
667
|
+
return `${origin}${parent}/api/v1`;
|
|
668
|
+
}
|
|
669
|
+
if (host === 'openrouter.ai' || host.endsWith('.openrouter.ai')) {
|
|
670
|
+
return `${origin}/api/v1`;
|
|
671
|
+
}
|
|
672
|
+
return `${origin}${pathname}/api/v1`;
|
|
673
|
+
} catch {
|
|
674
|
+
return '';
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function buildOpenRouterApiBaseCandidates(baseUrl, channel = {}) {
|
|
679
|
+
const candidates = [];
|
|
680
|
+
const inputs = [
|
|
681
|
+
channel.balanceBaseUrl,
|
|
682
|
+
channel.websiteUrl,
|
|
683
|
+
channel.baseUrl,
|
|
684
|
+
baseUrl
|
|
685
|
+
].filter(Boolean);
|
|
686
|
+
|
|
687
|
+
for (const input of inputs) {
|
|
688
|
+
addUnique(candidates, toOpenRouterApiBase(input));
|
|
689
|
+
}
|
|
690
|
+
return candidates;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function buildSiliconFlowApiBaseCandidates(baseUrl, channel = {}) {
|
|
694
|
+
const candidates = [];
|
|
695
|
+
const inputs = [
|
|
696
|
+
channel.balanceBaseUrl,
|
|
697
|
+
channel.websiteUrl,
|
|
698
|
+
channel.baseUrl,
|
|
699
|
+
baseUrl
|
|
700
|
+
].filter(Boolean);
|
|
701
|
+
|
|
702
|
+
for (const input of inputs) {
|
|
703
|
+
try {
|
|
704
|
+
const parsed = new URL(input);
|
|
705
|
+
const origin = parsed.origin;
|
|
706
|
+
const pathname = (parsed.pathname || '').replace(/\/+$/, '');
|
|
707
|
+
const lower = pathname.toLowerCase();
|
|
708
|
+
if (lower.endsWith('/v1')) {
|
|
709
|
+
addUnique(candidates, `${origin}${pathname}`);
|
|
710
|
+
} else {
|
|
711
|
+
addUnique(candidates, `${origin}/v1`);
|
|
712
|
+
}
|
|
713
|
+
} catch {}
|
|
714
|
+
}
|
|
715
|
+
return candidates;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function hasGatewayPathSegment(value) {
|
|
719
|
+
try {
|
|
720
|
+
const parsed = new URL(normalizeBaseUrl(value));
|
|
721
|
+
return parsed.pathname
|
|
722
|
+
.split('/')
|
|
723
|
+
.filter(Boolean)
|
|
724
|
+
.some(segment => GATEWAY_PATH_SEGMENTS.has(segment.toLowerCase()));
|
|
725
|
+
} catch {
|
|
726
|
+
return false;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function build88CodeApiBaseCandidates(baseUrl, channel = {}) {
|
|
731
|
+
const candidates = [];
|
|
732
|
+
const inputs = [
|
|
733
|
+
channel.balanceBaseUrl,
|
|
734
|
+
channel.websiteUrl,
|
|
735
|
+
channel.baseUrl,
|
|
736
|
+
baseUrl
|
|
737
|
+
].filter(Boolean);
|
|
738
|
+
|
|
739
|
+
for (const input of inputs) {
|
|
740
|
+
const originApiBase = toOriginApiBase(input);
|
|
741
|
+
const pathApiBase = toPathApiBase(input);
|
|
742
|
+
if (hasGatewayPathSegment(input)) {
|
|
743
|
+
addUnique(candidates, originApiBase);
|
|
744
|
+
addUnique(candidates, pathApiBase);
|
|
745
|
+
} else {
|
|
746
|
+
addUnique(candidates, pathApiBase);
|
|
747
|
+
addUnique(candidates, originApiBase);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
return candidates;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function get88CodeApiBaseFromStrategy(strategy = {}) {
|
|
754
|
+
return strategy.apiBase || strategy.baseUrl;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function get88CodeRequestBody(channel = {}) {
|
|
758
|
+
const model = String(
|
|
759
|
+
channel.model
|
|
760
|
+
|| channel.speedTestModel
|
|
761
|
+
|| channel.defaultModel
|
|
762
|
+
|| ''
|
|
763
|
+
).trim();
|
|
764
|
+
return model ? { model } : {};
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function extract88CodeData(payload) {
|
|
768
|
+
if (!payload || typeof payload !== 'object') return null;
|
|
769
|
+
if (!Object.prototype.hasOwnProperty.call(payload, 'data')) return null;
|
|
770
|
+
return payload.data;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function normalize88CodeSubscription(subscription = {}) {
|
|
774
|
+
const plan = subscription.subscriptionPlan && typeof subscription.subscriptionPlan === 'object'
|
|
775
|
+
? subscription.subscriptionPlan
|
|
776
|
+
: {};
|
|
777
|
+
const creditLimit = parseFiniteNumber(subscription.creditLimit)
|
|
778
|
+
?? parseFiniteNumber(plan.creditLimit)
|
|
779
|
+
?? 0;
|
|
780
|
+
return {
|
|
781
|
+
id: parseFiniteNumber(subscription.id) ?? 0,
|
|
782
|
+
planName: String(subscription.subscriptionPlanName || subscription.subscriptionName || plan.subscriptionName || ''),
|
|
783
|
+
status: String(subscription.subscriptionStatus || subscription.status || ''),
|
|
784
|
+
isActive: subscription.isActive === true,
|
|
785
|
+
remainingDays: parseFiniteNumber(subscription.remainingDays) ?? 0,
|
|
786
|
+
creditLimit,
|
|
787
|
+
currentCredits: parseFiniteNumber(subscription.currentCredits) ?? 0
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function get88CodeBillingPriority(planName) {
|
|
792
|
+
const normalized = String(planName || '').trim().toUpperCase();
|
|
793
|
+
if (normalized === 'FREE') return 3;
|
|
794
|
+
if (normalized === 'PAYGO') return 2;
|
|
795
|
+
return 1;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function select88CodeSubscription(subscriptions = []) {
|
|
799
|
+
const active = subscriptions
|
|
800
|
+
.map(normalize88CodeSubscription)
|
|
801
|
+
.filter(subscription => subscription.isActive)
|
|
802
|
+
.filter(subscription => !subscription.status || subscription.status === '活跃中' || subscription.remainingDays > 0)
|
|
803
|
+
.sort((left, right) => {
|
|
804
|
+
const priorityDiff = get88CodeBillingPriority(left.planName) - get88CodeBillingPriority(right.planName);
|
|
805
|
+
if (priorityDiff !== 0) return priorityDiff;
|
|
806
|
+
return left.id - right.id;
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
return active.find(subscription => (
|
|
810
|
+
subscription.planName.toUpperCase() !== 'FREE'
|
|
811
|
+
&& subscription.creditLimit > 0
|
|
812
|
+
&& subscription.currentCredits < subscription.creditLimit
|
|
813
|
+
))
|
|
814
|
+
|| active.find(subscription => (
|
|
815
|
+
subscription.planName.toUpperCase() !== 'FREE'
|
|
816
|
+
&& subscription.currentCredits > 0
|
|
817
|
+
))
|
|
818
|
+
|| null;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function hasOnly88CodeFreeSubscription(entities = []) {
|
|
822
|
+
const active = Array.isArray(entities)
|
|
823
|
+
? entities.filter(item => item?.isActive === true)
|
|
824
|
+
: [];
|
|
825
|
+
if (!active.length) return false;
|
|
826
|
+
return !active.some((item) => {
|
|
827
|
+
const name = String(item.subscriptionName || item.subscriptionPlanName || '').toUpperCase();
|
|
828
|
+
return name && name !== 'FREE' && name !== 'PAYGO';
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function build88CodeBalanceSnapshotFromValues(values = {}) {
|
|
833
|
+
const remainingRaw = parseFiniteNumber(values.currentCredits);
|
|
834
|
+
const totalRaw = parseFiniteNumber(values.creditLimit);
|
|
835
|
+
if (remainingRaw == null && totalRaw == null) {
|
|
836
|
+
return makeHiddenSnapshot('88code');
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
const remaining = Math.max(0, remainingRaw ?? 0);
|
|
840
|
+
const total = totalRaw != null && totalRaw > 0 ? totalRaw : undefined;
|
|
841
|
+
const used = total != null ? Math.max(0, total - remaining) : undefined;
|
|
842
|
+
return makeVisibleSnapshot('88code', {
|
|
843
|
+
remaining,
|
|
844
|
+
used,
|
|
845
|
+
total
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function build88CodeUsageSnapshot(data) {
|
|
850
|
+
if (!data || typeof data !== 'object') return makeHiddenSnapshot('88code');
|
|
851
|
+
const entities = Array.isArray(data.subscriptionEntityList) ? data.subscriptionEntityList : [];
|
|
852
|
+
const activeEntity = entities
|
|
853
|
+
.filter(item => item?.isActive === true)
|
|
854
|
+
.filter(item => String(item.subscriptionName || '').toUpperCase() !== 'FREE')
|
|
855
|
+
.find((item) => {
|
|
856
|
+
const total = parseFiniteNumber(item.creditLimit);
|
|
857
|
+
const remaining = parseFiniteNumber(item.currentCredits);
|
|
858
|
+
return total != null && remaining != null && remaining < total;
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
const selected = activeEntity || data;
|
|
862
|
+
return build88CodeBalanceSnapshotFromValues({
|
|
863
|
+
creditLimit: selected.creditLimit,
|
|
864
|
+
currentCredits: selected.currentCredits
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
function is88CodeUsageDataValid(data) {
|
|
869
|
+
if (!data || typeof data !== 'object') return false;
|
|
870
|
+
const creditLimit = parseFiniteNumber(data.creditLimit) ?? 0;
|
|
871
|
+
const entities = Array.isArray(data.subscriptionEntityList) ? data.subscriptionEntityList : [];
|
|
872
|
+
return creditLimit > 0 || entities.length > 0;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
function build88CodeSubscriptionSnapshot(payload) {
|
|
876
|
+
const data = extract88CodeData(payload);
|
|
877
|
+
const subscriptions = Array.isArray(data) ? data : [];
|
|
878
|
+
const selected = select88CodeSubscription(subscriptions);
|
|
879
|
+
if (!selected) return makeHiddenSnapshot('88code');
|
|
880
|
+
return build88CodeBalanceSnapshotFromValues({
|
|
881
|
+
creditLimit: selected.creditLimit,
|
|
882
|
+
currentCredits: selected.currentCredits
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
async function post88CodeJson(url, token, channel) {
|
|
887
|
+
return requestJson(url, {
|
|
888
|
+
method: 'POST',
|
|
889
|
+
headers: buildAuthHeaders(token, { 'Content-Type': 'application/json' }),
|
|
890
|
+
body: get88CodeRequestBody(channel)
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
async function run88CodeUsageStrategy(strategy, token, channel = {}) {
|
|
895
|
+
const apiBase = get88CodeApiBaseFromStrategy(strategy);
|
|
896
|
+
const payload = await post88CodeJson(joinUrl(apiBase, '/usage'), token, channel);
|
|
897
|
+
const usageData = extract88CodeData(payload);
|
|
898
|
+
if (!is88CodeUsageDataValid(usageData)) return makeHiddenSnapshot('88code');
|
|
899
|
+
const snapshot = build88CodeUsageSnapshot(usageData);
|
|
900
|
+
if (
|
|
901
|
+
snapshot.visible
|
|
902
|
+
&& !hasOnly88CodeFreeSubscription(usageData.subscriptionEntityList)
|
|
903
|
+
&& Number(snapshot.remaining || 0) > 0
|
|
904
|
+
) {
|
|
905
|
+
return snapshot;
|
|
906
|
+
}
|
|
907
|
+
return makeHiddenSnapshot('88code');
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
async function run88CodeSubscriptionStrategy(strategy, token, channel = {}) {
|
|
911
|
+
const apiBase = get88CodeApiBaseFromStrategy(strategy);
|
|
912
|
+
const payload = await post88CodeJson(joinUrl(apiBase, '/subscription'), token, channel);
|
|
913
|
+
return build88CodeSubscriptionSnapshot(payload);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
function buildBalanceProbeStrategies(baseUrl, channel = {}, token = '') {
|
|
917
|
+
const hintedPlatform = detectPlatformByUrlHint(
|
|
918
|
+
channel.providerKey,
|
|
919
|
+
channel.name,
|
|
920
|
+
channel.websiteUrl,
|
|
921
|
+
channel.baseUrl || baseUrl
|
|
922
|
+
);
|
|
923
|
+
const strategies = [];
|
|
924
|
+
const seen = new Set();
|
|
925
|
+
const add = (strategy) => {
|
|
926
|
+
const key = JSON.stringify(strategy);
|
|
927
|
+
if (!seen.has(key)) {
|
|
928
|
+
seen.add(key);
|
|
929
|
+
strategies.push(strategy);
|
|
930
|
+
}
|
|
931
|
+
};
|
|
932
|
+
const addSub2ApiStrategies = () => {
|
|
933
|
+
if (isLikelyModelApiKey(token)) {
|
|
934
|
+
add({ type: 'sub2api-usage', baseUrl });
|
|
935
|
+
} else {
|
|
936
|
+
add({ type: 'sub2api-auth', baseUrl });
|
|
937
|
+
add({ type: 'sub2api-usage', baseUrl });
|
|
938
|
+
}
|
|
939
|
+
};
|
|
940
|
+
const addHubStrategy = () => {
|
|
941
|
+
add({
|
|
942
|
+
type: 'hub-user-self',
|
|
943
|
+
baseUrl,
|
|
944
|
+
platform: HUB_PLATFORMS.has(hintedPlatform) ? hintedPlatform : null,
|
|
945
|
+
hintedPlatform,
|
|
946
|
+
userId: resolveBalanceUserId(channel),
|
|
947
|
+
allowCookieAuth: shouldTryCookieAuth(token, channel, hintedPlatform)
|
|
948
|
+
});
|
|
949
|
+
};
|
|
950
|
+
const addNewApiTokenUsageStrategy = () => {
|
|
951
|
+
add({ type: 'new-api-token-usage', baseUrl });
|
|
952
|
+
};
|
|
953
|
+
const addNewApiTokenUsageWithStatusStrategy = () => {
|
|
954
|
+
add({ type: 'new-api-token-usage-status', baseUrl });
|
|
955
|
+
};
|
|
956
|
+
const add88CodeStrategies = () => {
|
|
957
|
+
for (const apiBase of build88CodeApiBaseCandidates(baseUrl, channel)) {
|
|
958
|
+
add({ type: '88code-usage', baseUrl, apiBase });
|
|
959
|
+
add({ type: '88code-subscription', baseUrl, apiBase });
|
|
960
|
+
}
|
|
961
|
+
};
|
|
962
|
+
const addOpenRouterStrategies = () => {
|
|
963
|
+
for (const apiBase of buildOpenRouterApiBaseCandidates(baseUrl, channel)) {
|
|
964
|
+
add({ type: 'openrouter-key', baseUrl, apiBase, endpoint: '/key' });
|
|
965
|
+
add({ type: 'openrouter-key', baseUrl, apiBase, endpoint: '/auth/key' });
|
|
966
|
+
add({ type: 'openrouter-credits', baseUrl, apiBase });
|
|
967
|
+
}
|
|
968
|
+
};
|
|
969
|
+
const addSiliconFlowStrategies = () => {
|
|
970
|
+
for (const apiBase of buildSiliconFlowApiBaseCandidates(baseUrl, channel)) {
|
|
971
|
+
add({ type: 'siliconflow-user-info', baseUrl, apiBase });
|
|
972
|
+
}
|
|
973
|
+
};
|
|
974
|
+
|
|
975
|
+
if (UNSUPPORTED_BALANCE_PLATFORMS.has(hintedPlatform)) {
|
|
976
|
+
add({ type: 'unsupported-balance', platform: hintedPlatform, baseUrl });
|
|
977
|
+
return strategies;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
if (hintedPlatform === 'siliconflow') {
|
|
981
|
+
addSiliconFlowStrategies();
|
|
982
|
+
return strategies;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
if (hintedPlatform === '88code') {
|
|
986
|
+
add88CodeStrategies();
|
|
987
|
+
addSub2ApiStrategies();
|
|
988
|
+
addHubStrategy();
|
|
989
|
+
return strategies;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
if (hintedPlatform === 'openrouter' || isLikelyOpenRouterApiKey(token)) {
|
|
993
|
+
addOpenRouterStrategies();
|
|
994
|
+
addSub2ApiStrategies();
|
|
995
|
+
addNewApiTokenUsageWithStatusStrategy();
|
|
996
|
+
addHubStrategy();
|
|
997
|
+
return strategies;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
if (hintedPlatform === 'sub2api') {
|
|
1001
|
+
addSub2ApiStrategies();
|
|
1002
|
+
addNewApiTokenUsageWithStatusStrategy();
|
|
1003
|
+
addHubStrategy();
|
|
1004
|
+
return strategies;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
if (HUB_PLATFORMS.has(hintedPlatform)) {
|
|
1008
|
+
if (hintedPlatform === 'new-api') {
|
|
1009
|
+
addNewApiTokenUsageWithStatusStrategy();
|
|
1010
|
+
}
|
|
1011
|
+
addHubStrategy();
|
|
1012
|
+
addSub2ApiStrategies();
|
|
1013
|
+
return strategies;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
if (isLikelyModelApiKey(token)) {
|
|
1017
|
+
addSub2ApiStrategies();
|
|
1018
|
+
addNewApiTokenUsageWithStatusStrategy();
|
|
1019
|
+
addHubStrategy();
|
|
1020
|
+
} else {
|
|
1021
|
+
addHubStrategy();
|
|
1022
|
+
addNewApiTokenUsageWithStatusStrategy();
|
|
1023
|
+
addSub2ApiStrategies();
|
|
1024
|
+
}
|
|
1025
|
+
return strategies;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
async function runSub2ApiAuthStrategy(strategy, token) {
|
|
356
1029
|
const headers = buildAuthHeaders(token);
|
|
357
|
-
const payload = await
|
|
1030
|
+
const payload = await tryRequestJson(joinUrl(strategy.baseUrl, '/api/v1/auth/me'), { headers });
|
|
358
1031
|
const data = extractSub2ApiData(payload);
|
|
359
1032
|
const balance = parseFiniteNumber(data?.balance);
|
|
360
1033
|
if (balance == null) return makeHiddenSnapshot('sub2api');
|
|
361
1034
|
|
|
362
|
-
const summary = await fetchSub2ApiSubscriptionSummary(baseUrl, headers);
|
|
1035
|
+
const summary = await fetchSub2ApiSubscriptionSummary(strategy.baseUrl, headers);
|
|
363
1036
|
return makeVisibleSnapshot('sub2api', {
|
|
364
1037
|
remaining: balance,
|
|
365
1038
|
used: 0,
|
|
@@ -368,33 +1041,212 @@ async function fetchSub2ApiBalance(baseUrl, token) {
|
|
|
368
1041
|
});
|
|
369
1042
|
}
|
|
370
1043
|
|
|
371
|
-
async function
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
|
|
1044
|
+
async function runSub2ApiUsageStrategy(strategy, token) {
|
|
1045
|
+
const payload = await tryRequestJson(joinUrl(strategy.baseUrl, '/v1/usage'), {
|
|
1046
|
+
headers: buildAuthHeaders(token)
|
|
1047
|
+
});
|
|
1048
|
+
return buildSub2ApiUsageSnapshot(payload);
|
|
375
1049
|
}
|
|
376
1050
|
|
|
377
|
-
async function
|
|
378
|
-
const
|
|
379
|
-
|
|
1051
|
+
async function fetchHubUserSelfWithCookie(baseUrl, token, platform, quotaUnit, userId) {
|
|
1052
|
+
for (const cookie of buildCookieCandidates(token)) {
|
|
1053
|
+
const payload = await tryRequestJson(joinUrl(baseUrl, '/api/user/self'), {
|
|
1054
|
+
headers: {
|
|
1055
|
+
Accept: 'application/json',
|
|
1056
|
+
Cookie: cookie,
|
|
1057
|
+
...buildUserIdHeaders(userId)
|
|
1058
|
+
}
|
|
1059
|
+
});
|
|
1060
|
+
const snapshot = buildHubBalanceSnapshot(platform, {
|
|
1061
|
+
...payload,
|
|
1062
|
+
data: payload?.data && typeof payload.data === 'object'
|
|
1063
|
+
? { ...payload.data, quota_per_unit: quotaUnit || payload.data.quota_per_unit }
|
|
1064
|
+
: payload?.data
|
|
1065
|
+
});
|
|
1066
|
+
if (snapshot.visible) return snapshot;
|
|
1067
|
+
}
|
|
1068
|
+
return makeHiddenSnapshot(platform);
|
|
1069
|
+
}
|
|
380
1070
|
|
|
381
|
-
|
|
382
|
-
const
|
|
383
|
-
|
|
1071
|
+
async function runHubUserSelfStrategy(strategy, token) {
|
|
1072
|
+
const userId = strategy.userId || null;
|
|
1073
|
+
const headers = buildAuthHeaders(token, buildUserIdHeaders(userId));
|
|
1074
|
+
let platform = strategy.platform;
|
|
1075
|
+
let quotaUnit = null;
|
|
1076
|
+
if (!HUB_PLATFORMS.has(platform)) {
|
|
1077
|
+
const status = await tryRequestJson(joinUrl(strategy.baseUrl, '/api/status'), { headers: { Accept: 'application/json' } });
|
|
1078
|
+
platform = resolveStatusPlatform(status, strategy.hintedPlatform);
|
|
1079
|
+
quotaUnit = extractQuotaUnitFromStatus(status);
|
|
1080
|
+
}
|
|
1081
|
+
if (!HUB_PLATFORMS.has(platform)) return { snapshot: makeHiddenSnapshot(platform), strategy };
|
|
384
1082
|
|
|
385
|
-
|
|
386
|
-
|
|
1083
|
+
let snapshot = makeHiddenSnapshot(platform);
|
|
1084
|
+
try {
|
|
1085
|
+
const payload = await requestJson(joinUrl(strategy.baseUrl, '/api/user/self'), { headers });
|
|
1086
|
+
snapshot = buildHubBalanceSnapshot(platform, {
|
|
1087
|
+
...payload,
|
|
1088
|
+
data: payload?.data && typeof payload.data === 'object'
|
|
1089
|
+
? { ...payload.data, quota_per_unit: quotaUnit || payload.data.quota_per_unit }
|
|
1090
|
+
: payload?.data
|
|
1091
|
+
});
|
|
1092
|
+
} catch (error) {
|
|
1093
|
+
if (!strategy.allowCookieAuth) throw error;
|
|
1094
|
+
}
|
|
1095
|
+
if (!snapshot.visible && strategy.allowCookieAuth) {
|
|
1096
|
+
snapshot = await fetchHubUserSelfWithCookie(strategy.baseUrl, token, platform, quotaUnit, userId);
|
|
387
1097
|
}
|
|
1098
|
+
return {
|
|
1099
|
+
snapshot,
|
|
1100
|
+
strategy: {
|
|
1101
|
+
...strategy,
|
|
1102
|
+
platform,
|
|
1103
|
+
quotaUnit: quotaUnit || strategy.quotaUnit
|
|
1104
|
+
}
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
async function runNewApiTokenUsageStrategy(strategy, token) {
|
|
1109
|
+
const payload = await requestJson(joinUrl(strategy.baseUrl, '/api/usage/token'), {
|
|
1110
|
+
headers: buildAuthHeaders(token)
|
|
1111
|
+
});
|
|
1112
|
+
return buildNewApiTokenUsageSnapshot(payload, { quotaUnit: strategy.quotaUnit || QUOTA_UNIT });
|
|
1113
|
+
}
|
|
388
1114
|
|
|
389
|
-
|
|
390
|
-
|
|
1115
|
+
async function runNewApiTokenUsageWithStatusStrategy(strategy, token) {
|
|
1116
|
+
const status = await tryRequestJson(joinUrl(strategy.baseUrl, '/api/status'), {
|
|
1117
|
+
headers: { Accept: 'application/json' }
|
|
1118
|
+
});
|
|
1119
|
+
const quotaUnit = extractQuotaUnitFromStatus(status) || QUOTA_UNIT;
|
|
1120
|
+
const snapshot = await runNewApiTokenUsageStrategy({ ...strategy, quotaUnit }, token);
|
|
1121
|
+
return {
|
|
1122
|
+
snapshot,
|
|
1123
|
+
strategy: {
|
|
1124
|
+
...strategy,
|
|
1125
|
+
quotaUnit
|
|
1126
|
+
}
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
async function runOpenRouterKeyStrategy(strategy, token) {
|
|
1131
|
+
const payload = await requestJson(joinUrl(strategy.apiBase || strategy.baseUrl, strategy.endpoint || '/key'), {
|
|
1132
|
+
headers: buildAuthHeaders(token)
|
|
1133
|
+
});
|
|
1134
|
+
return buildOpenRouterKeySnapshot(payload);
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
async function runOpenRouterCreditsStrategy(strategy, token) {
|
|
1138
|
+
const payload = await requestJson(joinUrl(strategy.apiBase || strategy.baseUrl, '/credits'), {
|
|
1139
|
+
headers: buildAuthHeaders(token)
|
|
1140
|
+
});
|
|
1141
|
+
return buildOpenRouterCreditsSnapshot(payload);
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
async function runSiliconFlowUserInfoStrategy(strategy, token) {
|
|
1145
|
+
const payload = await requestJson(joinUrl(strategy.apiBase || strategy.baseUrl, '/user/info'), {
|
|
1146
|
+
headers: buildAuthHeaders(token)
|
|
1147
|
+
});
|
|
1148
|
+
return buildSiliconFlowUserInfoSnapshot(payload);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
async function runBalanceStrategy(strategy, token, channel = {}) {
|
|
1152
|
+
if (!strategy || !strategy.type) {
|
|
1153
|
+
return { snapshot: makeHiddenSnapshot(), strategy };
|
|
391
1154
|
}
|
|
392
1155
|
|
|
393
|
-
|
|
1156
|
+
if (strategy.type === '88code-usage') {
|
|
1157
|
+
return {
|
|
1158
|
+
snapshot: await run88CodeUsageStrategy(strategy, token, channel),
|
|
1159
|
+
strategy
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
if (strategy.type === '88code-subscription') {
|
|
1163
|
+
return {
|
|
1164
|
+
snapshot: await run88CodeSubscriptionStrategy(strategy, token, channel),
|
|
1165
|
+
strategy
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
if (strategy.type === 'sub2api-auth') {
|
|
1169
|
+
return {
|
|
1170
|
+
snapshot: await runSub2ApiAuthStrategy(strategy, token),
|
|
1171
|
+
strategy
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
if (strategy.type === 'sub2api-usage') {
|
|
1175
|
+
return {
|
|
1176
|
+
snapshot: await runSub2ApiUsageStrategy(strategy, token),
|
|
1177
|
+
strategy
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
if (strategy.type === 'hub-user-self') {
|
|
1181
|
+
return runHubUserSelfStrategy(strategy, token);
|
|
1182
|
+
}
|
|
1183
|
+
if (strategy.type === 'new-api-token-usage') {
|
|
1184
|
+
return {
|
|
1185
|
+
snapshot: await runNewApiTokenUsageStrategy(strategy, token),
|
|
1186
|
+
strategy
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
if (strategy.type === 'new-api-token-usage-status') {
|
|
1190
|
+
return runNewApiTokenUsageWithStatusStrategy(strategy, token);
|
|
1191
|
+
}
|
|
1192
|
+
if (strategy.type === 'openrouter-key') {
|
|
1193
|
+
return {
|
|
1194
|
+
snapshot: await runOpenRouterKeyStrategy(strategy, token),
|
|
1195
|
+
strategy
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
if (strategy.type === 'openrouter-credits') {
|
|
1199
|
+
return {
|
|
1200
|
+
snapshot: await runOpenRouterCreditsStrategy(strategy, token),
|
|
1201
|
+
strategy
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
if (strategy.type === 'unsupported-balance') {
|
|
1205
|
+
return {
|
|
1206
|
+
snapshot: makeHiddenSnapshot(strategy.platform),
|
|
1207
|
+
strategy
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
if (strategy.type === 'siliconflow-user-info') {
|
|
1211
|
+
return {
|
|
1212
|
+
snapshot: await runSiliconFlowUserInfoStrategy(strategy, token),
|
|
1213
|
+
strategy
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
return { snapshot: makeHiddenSnapshot(), strategy };
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
async function tryBalanceStrategy(strategy, token, channel = {}) {
|
|
1221
|
+
try {
|
|
1222
|
+
const result = await runBalanceStrategy(strategy, token, channel);
|
|
1223
|
+
if (result?.snapshot?.visible) return result;
|
|
1224
|
+
} catch {
|
|
1225
|
+
// Probe errors are intentionally silent; unsupported balances stay hidden.
|
|
1226
|
+
}
|
|
1227
|
+
return null;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
async function probeBalanceFromBase(baseUrl, channel, token) {
|
|
1231
|
+
let lastSnapshot = null;
|
|
1232
|
+
for (const strategy of buildBalanceProbeStrategies(baseUrl, channel, token)) {
|
|
1233
|
+
const result = await tryBalanceStrategy(strategy, token, channel);
|
|
1234
|
+
if (result) return result;
|
|
1235
|
+
if (!lastSnapshot) {
|
|
1236
|
+
lastSnapshot = makeHiddenSnapshot(
|
|
1237
|
+
strategy.type?.startsWith('88code') ? '88code'
|
|
1238
|
+
: strategy.type?.startsWith('openrouter') ? 'openrouter'
|
|
1239
|
+
: strategy.type?.startsWith('siliconflow') ? 'siliconflow'
|
|
1240
|
+
: strategy.type?.startsWith('sub2api') ? 'sub2api'
|
|
1241
|
+
: strategy.platform || null
|
|
1242
|
+
);
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
return lastSnapshot ? { snapshot: lastSnapshot, strategy: null } : null;
|
|
394
1246
|
}
|
|
395
1247
|
|
|
396
1248
|
function buildCacheKey(source, channel) {
|
|
397
|
-
const token =
|
|
1249
|
+
const token = resolveBalanceToken(channel);
|
|
398
1250
|
const tokenHash = token
|
|
399
1251
|
? crypto.createHash('sha256').update(token).digest('hex').slice(0, 16)
|
|
400
1252
|
: 'no-token';
|
|
@@ -417,25 +1269,37 @@ async function refreshChannelBalanceSnapshot(source, channel, options = {}) {
|
|
|
417
1269
|
}
|
|
418
1270
|
|
|
419
1271
|
const bases = buildBaseCandidates(channel.baseUrl);
|
|
420
|
-
if (!bases.length || !
|
|
1272
|
+
if (!bases.length || !resolveBalanceToken(channel)) {
|
|
421
1273
|
return makeHiddenSnapshot(null, { updatedAt: nowIso(currentTime) });
|
|
422
1274
|
}
|
|
423
1275
|
|
|
424
1276
|
let lastSnapshot = null;
|
|
1277
|
+
const token = resolveBalanceToken(channel);
|
|
1278
|
+
const cachedStrategy = balanceStrategyCache.get(key);
|
|
1279
|
+
if (cachedStrategy) {
|
|
1280
|
+
const strategyResult = await tryBalanceStrategy(cachedStrategy, token, channel);
|
|
1281
|
+
if (strategyResult?.snapshot?.visible) {
|
|
1282
|
+
balanceStrategyCache.set(key, strategyResult.strategy);
|
|
1283
|
+
balanceCache.set(key, {
|
|
1284
|
+
snapshot: strategyResult.snapshot,
|
|
1285
|
+
expiresAt: currentTime + CACHE_TTL_MS
|
|
1286
|
+
});
|
|
1287
|
+
return strategyResult.snapshot;
|
|
1288
|
+
}
|
|
1289
|
+
balanceStrategyCache.delete(key);
|
|
1290
|
+
}
|
|
1291
|
+
|
|
425
1292
|
for (const baseUrl of bases) {
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
}
|
|
435
|
-
lastSnapshot = snapshot || lastSnapshot;
|
|
436
|
-
} catch {
|
|
437
|
-
// Try the next normalized base candidate.
|
|
1293
|
+
const result = await probeBalanceFromBase(baseUrl, channel, token);
|
|
1294
|
+
if (result?.snapshot?.visible) {
|
|
1295
|
+
balanceStrategyCache.set(key, result.strategy);
|
|
1296
|
+
balanceCache.set(key, {
|
|
1297
|
+
snapshot: result.snapshot,
|
|
1298
|
+
expiresAt: currentTime + CACHE_TTL_MS
|
|
1299
|
+
});
|
|
1300
|
+
return result.snapshot;
|
|
438
1301
|
}
|
|
1302
|
+
lastSnapshot = result?.snapshot || makeHiddenSnapshot(null, { updatedAt: nowIso(currentTime) });
|
|
439
1303
|
}
|
|
440
1304
|
|
|
441
1305
|
if (cached?.snapshot?.visible) {
|
|
@@ -543,6 +1407,7 @@ async function refreshChannelBalance(source, channelId) {
|
|
|
543
1407
|
|
|
544
1408
|
function clearBalanceCache() {
|
|
545
1409
|
balanceCache.clear();
|
|
1410
|
+
balanceStrategyCache.clear();
|
|
546
1411
|
}
|
|
547
1412
|
|
|
548
1413
|
module.exports = {
|
|
@@ -553,8 +1418,24 @@ module.exports = {
|
|
|
553
1418
|
normalizeBaseUrl,
|
|
554
1419
|
buildBaseCandidates,
|
|
555
1420
|
buildHubBalanceSnapshot,
|
|
1421
|
+
buildNewApiTokenUsageSnapshot,
|
|
1422
|
+
buildOpenRouterKeySnapshot,
|
|
1423
|
+
buildOpenRouterCreditsSnapshot,
|
|
1424
|
+
buildSiliconFlowUserInfoSnapshot,
|
|
556
1425
|
detectPlatformByUrlHint,
|
|
1426
|
+
isSub2ApiUsagePayload,
|
|
1427
|
+
buildSub2ApiUsageSnapshot,
|
|
557
1428
|
resolveStatusPlatform,
|
|
1429
|
+
resolveBalanceToken,
|
|
1430
|
+
resolveBalanceUserId,
|
|
1431
|
+
buildCookieCandidates,
|
|
1432
|
+
build88CodeApiBaseCandidates,
|
|
1433
|
+
buildOpenRouterApiBaseCandidates,
|
|
1434
|
+
buildSiliconFlowApiBaseCandidates,
|
|
1435
|
+
build88CodeUsageSnapshot,
|
|
1436
|
+
build88CodeSubscriptionSnapshot,
|
|
1437
|
+
buildBalanceProbeStrategies,
|
|
1438
|
+
runBalanceStrategy,
|
|
558
1439
|
refreshChannelBalanceSnapshot,
|
|
559
1440
|
makeVisibleSnapshot,
|
|
560
1441
|
requestJson
|