coding-tool-x 3.6.3 → 3.6.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-D0BDj7bo.js → Analytics-BTmqXkR-.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-BnwWmyV7.js → ConfigTemplates-z3w0kyNl.js} +1 -1
- package/dist/web/assets/{Home-Yw5bTgrH.js → Home-DU-NVre0.js} +1 -1
- package/dist/web/assets/{PluginManager-CRdRLKsS.js → PluginManager-44r9LaWV.js} +1 -1
- package/dist/web/assets/{ProjectList-CDIYAOyV.js → ProjectList-DW_MEKRy.js} +1 -1
- package/dist/web/assets/{SessionList-BJ2kbl-3.js → SessionList-B8upyqsp.js} +1 -1
- package/dist/web/assets/{SkillManager-DaM0Ob6l.js → SkillManager-DnzRfF_o.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-Y-EZ7NxE.js → WorkspaceManager-BuRpVJRU.js} +1 -1
- package/dist/web/assets/index-gIbIMdSO.js +2 -0
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- package/src/server/api/channels.js +4 -2
- package/src/server/api/codex-channels.js +3 -1
- package/src/server/api/gemini-channels.js +4 -2
- package/src/server/api/opencode-channels.js +4 -2
- package/src/server/services/channel-balance.js +859 -66
- 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,18 @@ 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
|
+
|
|
25
41
|
function buildAuthHeaders(token, extra = {}) {
|
|
26
42
|
return {
|
|
27
43
|
Accept: 'application/json',
|
|
@@ -30,6 +46,14 @@ function buildAuthHeaders(token, extra = {}) {
|
|
|
30
46
|
};
|
|
31
47
|
}
|
|
32
48
|
|
|
49
|
+
function isLikelyModelApiKey(token) {
|
|
50
|
+
return /^sk-[a-z0-9]/i.test(stripBearer(token));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isLikelyOpenRouterApiKey(token) {
|
|
54
|
+
return /^sk-or-/i.test(stripBearer(token));
|
|
55
|
+
}
|
|
56
|
+
|
|
33
57
|
function parseFiniteNumber(value) {
|
|
34
58
|
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
35
59
|
if (typeof value === 'string') {
|
|
@@ -44,6 +68,11 @@ function roundMoney(value) {
|
|
|
44
68
|
return Math.round(value * 1000000) / 1000000;
|
|
45
69
|
}
|
|
46
70
|
|
|
71
|
+
function normalizeQuotaUnit(value, fallback = QUOTA_UNIT) {
|
|
72
|
+
const parsed = parseFiniteNumber(value);
|
|
73
|
+
return parsed != null && parsed > 0 ? parsed : fallback;
|
|
74
|
+
}
|
|
75
|
+
|
|
47
76
|
function formatMoney(value) {
|
|
48
77
|
if (!Number.isFinite(value)) return '';
|
|
49
78
|
return `$${value.toFixed(2)}`;
|
|
@@ -144,7 +173,13 @@ function joinUrl(baseUrl, endpoint) {
|
|
|
144
173
|
return `${base}${path}`;
|
|
145
174
|
}
|
|
146
175
|
|
|
147
|
-
function requestJson(url, {
|
|
176
|
+
function requestJson(url, {
|
|
177
|
+
method = 'GET',
|
|
178
|
+
headers = {},
|
|
179
|
+
body,
|
|
180
|
+
timeoutMs = REQUEST_TIMEOUT_MS,
|
|
181
|
+
redirectCount = 0
|
|
182
|
+
} = {}) {
|
|
148
183
|
return new Promise((resolve, reject) => {
|
|
149
184
|
let parsed;
|
|
150
185
|
try {
|
|
@@ -155,13 +190,21 @@ function requestJson(url, { headers = {}, timeoutMs = REQUEST_TIMEOUT_MS } = {})
|
|
|
155
190
|
}
|
|
156
191
|
|
|
157
192
|
const client = parsed.protocol === 'http:' ? http : https;
|
|
193
|
+
const requestBody = body === undefined
|
|
194
|
+
? null
|
|
195
|
+
: (typeof body === 'string' || Buffer.isBuffer(body) ? body : JSON.stringify(body));
|
|
196
|
+
const requestHeaders = { ...headers };
|
|
197
|
+
if (requestBody != null && !Object.keys(requestHeaders).some(key => key.toLowerCase() === 'content-length')) {
|
|
198
|
+
requestHeaders['Content-Length'] = Buffer.byteLength(requestBody);
|
|
199
|
+
}
|
|
200
|
+
|
|
158
201
|
const req = client.request({
|
|
159
202
|
protocol: parsed.protocol,
|
|
160
203
|
hostname: parsed.hostname,
|
|
161
204
|
port: parsed.port,
|
|
162
205
|
path: `${parsed.pathname}${parsed.search}`,
|
|
163
|
-
method
|
|
164
|
-
headers
|
|
206
|
+
method,
|
|
207
|
+
headers: requestHeaders
|
|
165
208
|
}, (res) => {
|
|
166
209
|
const chunks = [];
|
|
167
210
|
let total = 0;
|
|
@@ -177,17 +220,42 @@ function requestJson(url, { headers = {}, timeoutMs = REQUEST_TIMEOUT_MS } = {})
|
|
|
177
220
|
|
|
178
221
|
res.on('end', () => {
|
|
179
222
|
const raw = Buffer.concat(chunks).toString('utf8');
|
|
223
|
+
let payload = null;
|
|
224
|
+
let parseError = null;
|
|
225
|
+
if (raw) {
|
|
226
|
+
try {
|
|
227
|
+
payload = JSON.parse(raw);
|
|
228
|
+
} catch (error) {
|
|
229
|
+
parseError = error;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if ([301, 302, 303, 307, 308].includes(res.statusCode) && res.headers.location && redirectCount < 3) {
|
|
233
|
+
try {
|
|
234
|
+
const nextUrl = new URL(res.headers.location, parsed).toString();
|
|
235
|
+
resolve(requestJson(nextUrl, {
|
|
236
|
+
method: res.statusCode === 303 ? 'GET' : method,
|
|
237
|
+
headers,
|
|
238
|
+
body: res.statusCode === 303 ? undefined : body,
|
|
239
|
+
timeoutMs,
|
|
240
|
+
redirectCount: redirectCount + 1
|
|
241
|
+
}));
|
|
242
|
+
} catch (error) {
|
|
243
|
+
reject(error);
|
|
244
|
+
}
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
180
247
|
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
181
248
|
const error = new Error(`HTTP ${res.statusCode}`);
|
|
182
249
|
error.statusCode = res.statusCode;
|
|
250
|
+
error.payload = payload;
|
|
183
251
|
reject(error);
|
|
184
252
|
return;
|
|
185
253
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
reject(error);
|
|
254
|
+
if (parseError) {
|
|
255
|
+
reject(parseError);
|
|
256
|
+
return;
|
|
190
257
|
}
|
|
258
|
+
resolve(payload);
|
|
191
259
|
});
|
|
192
260
|
});
|
|
193
261
|
|
|
@@ -195,6 +263,9 @@ function requestJson(url, { headers = {}, timeoutMs = REQUEST_TIMEOUT_MS } = {})
|
|
|
195
263
|
req.destroy(new Error('request timeout'));
|
|
196
264
|
});
|
|
197
265
|
req.on('error', reject);
|
|
266
|
+
if (requestBody != null) {
|
|
267
|
+
req.write(requestBody);
|
|
268
|
+
}
|
|
198
269
|
req.end();
|
|
199
270
|
});
|
|
200
271
|
}
|
|
@@ -202,13 +273,22 @@ function requestJson(url, { headers = {}, timeoutMs = REQUEST_TIMEOUT_MS } = {})
|
|
|
202
273
|
async function tryRequestJson(url, options) {
|
|
203
274
|
try {
|
|
204
275
|
return await requestJson(url, options);
|
|
205
|
-
} catch {
|
|
276
|
+
} catch (error) {
|
|
277
|
+
if (error?.payload && typeof error.payload === 'object') {
|
|
278
|
+
return error.payload;
|
|
279
|
+
}
|
|
206
280
|
return null;
|
|
207
281
|
}
|
|
208
282
|
}
|
|
209
283
|
|
|
210
|
-
function detectPlatformByUrlHint(
|
|
211
|
-
const value = String(
|
|
284
|
+
function detectPlatformByUrlHint(...values) {
|
|
285
|
+
const value = values.map(item => String(item || '')).join(' ').toLowerCase();
|
|
286
|
+
if (value.includes('88code') || value.includes('88-code') || value.includes('rainapp.top')) return '88code';
|
|
287
|
+
if (value.includes('openrouter.ai') || value.includes('openrouter')) return 'openrouter';
|
|
288
|
+
if (value.includes('aihubmix')) return 'aihubmix';
|
|
289
|
+
if (value.includes('dashscope') || value.includes('dashscope.aliyuncs.com') || value.includes('compatible-mode')) return 'dashscope';
|
|
290
|
+
if (value.includes('modelscope') || value.includes('api-inference.modelscope.cn')) return 'modelscope';
|
|
291
|
+
if (value.includes('siliconflow') || value.includes('siliconflow.cn')) return 'siliconflow';
|
|
212
292
|
if (value.includes('sub2api')) return 'sub2api';
|
|
213
293
|
if (value.includes('anyrouter')) return 'anyrouter';
|
|
214
294
|
if (value.includes('donehub') || value.includes('done-hub')) return 'done-hub';
|
|
@@ -220,14 +300,17 @@ function detectPlatformByUrlHint(baseUrl) {
|
|
|
220
300
|
}
|
|
221
301
|
|
|
222
302
|
function resolveStatusPlatform(payload, hintedPlatform = null) {
|
|
223
|
-
if (!payload || payload
|
|
224
|
-
const
|
|
225
|
-
|
|
303
|
+
if (!payload || typeof payload !== 'object') return null;
|
|
304
|
+
const data = payload.data && typeof payload.data === 'object' ? payload.data : payload;
|
|
305
|
+
const systemName = typeof data?.system_name === 'string'
|
|
306
|
+
? data.system_name.toLowerCase()
|
|
226
307
|
: '';
|
|
227
|
-
const version = typeof
|
|
228
|
-
?
|
|
308
|
+
const version = typeof data?.version === 'string'
|
|
309
|
+
? data.version.toLowerCase()
|
|
229
310
|
: '';
|
|
311
|
+
if (payload.success !== true && !systemName && !version) return null;
|
|
230
312
|
|
|
313
|
+
if (hintedPlatform === 'aihubmix' || systemName.includes('aihubmix')) return 'aihubmix';
|
|
231
314
|
if (hintedPlatform === 'anyrouter') return 'anyrouter';
|
|
232
315
|
if (hintedPlatform === 'done-hub') return 'done-hub';
|
|
233
316
|
if (hintedPlatform === 'one-hub') return 'one-hub';
|
|
@@ -246,24 +329,15 @@ function extractSub2ApiData(payload) {
|
|
|
246
329
|
return payload;
|
|
247
330
|
}
|
|
248
331
|
|
|
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;
|
|
332
|
+
function isSub2ApiUsagePayload(payload) {
|
|
333
|
+
if (!payload || typeof payload !== 'object') return false;
|
|
334
|
+
const mode = typeof payload.mode === 'string' ? payload.mode.trim().toLowerCase() : '';
|
|
335
|
+
if (!['quota_limited', 'unrestricted'].includes(mode)) return false;
|
|
336
|
+
return Object.prototype.hasOwnProperty.call(payload, 'remaining')
|
|
337
|
+
|| Object.prototype.hasOwnProperty.call(payload, 'balance')
|
|
338
|
+
|| (payload.quota && typeof payload.quota === 'object')
|
|
339
|
+
|| (payload.subscription && typeof payload.subscription === 'object')
|
|
340
|
+
|| (payload.usage && typeof payload.usage === 'object');
|
|
267
341
|
}
|
|
268
342
|
|
|
269
343
|
function extractUserSelfData(payload) {
|
|
@@ -278,7 +352,9 @@ function buildHubBalanceSnapshot(platform, payload) {
|
|
|
278
352
|
const data = extractUserSelfData(payload);
|
|
279
353
|
if (!data) return makeHiddenSnapshot(platform);
|
|
280
354
|
|
|
281
|
-
const unit = platform === 'veloera'
|
|
355
|
+
const unit = platform === 'veloera'
|
|
356
|
+
? VELOERA_QUOTA_UNIT
|
|
357
|
+
: normalizeQuotaUnit(data.quota_per_unit || data.quotaPerUnit);
|
|
282
358
|
const quotaRaw = parseFiniteNumber(data.quota);
|
|
283
359
|
const usedRaw = parseFiniteNumber(data.used_quota);
|
|
284
360
|
if (quotaRaw == null) return makeHiddenSnapshot(platform);
|
|
@@ -288,7 +364,7 @@ function buildHubBalanceSnapshot(platform, payload) {
|
|
|
288
364
|
let remaining;
|
|
289
365
|
let total;
|
|
290
366
|
|
|
291
|
-
if (platform === 'new-api' || platform === 'anyrouter' || platform === 'done-hub') {
|
|
367
|
+
if (platform === 'new-api' || platform === 'anyrouter' || platform === 'done-hub' || platform === 'aihubmix') {
|
|
292
368
|
remaining = quota;
|
|
293
369
|
total = quota + used;
|
|
294
370
|
} else {
|
|
@@ -303,6 +379,102 @@ function buildHubBalanceSnapshot(platform, payload) {
|
|
|
303
379
|
});
|
|
304
380
|
}
|
|
305
381
|
|
|
382
|
+
function extractNewApiTokenUsageData(payload) {
|
|
383
|
+
if (!payload || typeof payload !== 'object') return null;
|
|
384
|
+
const data = payload.data && typeof payload.data === 'object' ? payload.data : payload;
|
|
385
|
+
if (!data || typeof data !== 'object') return null;
|
|
386
|
+
if (!Object.prototype.hasOwnProperty.call(data, 'total_available')) return null;
|
|
387
|
+
return data;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function buildNewApiTokenUsageSnapshot(payload, { quotaUnit = QUOTA_UNIT } = {}) {
|
|
391
|
+
const data = extractNewApiTokenUsageData(payload);
|
|
392
|
+
if (!data) return makeHiddenSnapshot('new-api');
|
|
393
|
+
|
|
394
|
+
const availableRaw = parseFiniteNumber(data.total_available);
|
|
395
|
+
if (availableRaw == null) return makeHiddenSnapshot('new-api');
|
|
396
|
+
const totalRaw = parseFiniteNumber(data.total_granted);
|
|
397
|
+
const usedRaw = parseFiniteNumber(data.total_used);
|
|
398
|
+
const unit = normalizeQuotaUnit(data.quota_per_unit || data.quotaPerUnit || quotaUnit);
|
|
399
|
+
|
|
400
|
+
const remaining = Math.max(0, availableRaw / unit);
|
|
401
|
+
return makeVisibleSnapshot('new-api', {
|
|
402
|
+
remaining,
|
|
403
|
+
used: usedRaw != null ? usedRaw / unit : undefined,
|
|
404
|
+
total: totalRaw != null ? totalRaw / unit : undefined
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function extractOpenRouterData(payload) {
|
|
409
|
+
if (!payload || typeof payload !== 'object') return null;
|
|
410
|
+
const data = payload.data && typeof payload.data === 'object' ? payload.data : payload;
|
|
411
|
+
return data && typeof data === 'object' ? data : null;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function buildOpenRouterKeySnapshot(payload) {
|
|
415
|
+
const data = extractOpenRouterData(payload);
|
|
416
|
+
if (!data) return makeHiddenSnapshot('openrouter');
|
|
417
|
+
|
|
418
|
+
const limitRemaining = parseFiniteNumber(data.limit_remaining ?? data.limitRemaining);
|
|
419
|
+
const limit = parseFiniteNumber(data.limit);
|
|
420
|
+
const usage = parseFiniteNumber(data.usage);
|
|
421
|
+
if (limitRemaining != null) {
|
|
422
|
+
return makeVisibleSnapshot('openrouter', {
|
|
423
|
+
remaining: Math.max(0, limitRemaining),
|
|
424
|
+
used: usage,
|
|
425
|
+
total: limit
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (limit != null && usage != null) {
|
|
430
|
+
return makeVisibleSnapshot('openrouter', {
|
|
431
|
+
remaining: Math.max(0, limit - usage),
|
|
432
|
+
used: usage,
|
|
433
|
+
total: limit
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return makeHiddenSnapshot('openrouter');
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function buildOpenRouterCreditsSnapshot(payload) {
|
|
441
|
+
const data = extractOpenRouterData(payload);
|
|
442
|
+
if (!data) return makeHiddenSnapshot('openrouter');
|
|
443
|
+
|
|
444
|
+
const totalCredits = parseFiniteNumber(data.total_credits ?? data.totalCredits);
|
|
445
|
+
const totalUsage = parseFiniteNumber(data.total_usage ?? data.totalUsage);
|
|
446
|
+
if (totalCredits == null || totalUsage == null) {
|
|
447
|
+
return makeHiddenSnapshot('openrouter');
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return makeVisibleSnapshot('openrouter', {
|
|
451
|
+
remaining: Math.max(0, totalCredits - totalUsage),
|
|
452
|
+
used: totalUsage,
|
|
453
|
+
total: totalCredits
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function buildSiliconFlowUserInfoSnapshot(payload) {
|
|
458
|
+
const data = payload?.data && typeof payload.data === 'object' ? payload.data : payload;
|
|
459
|
+
if (!data || typeof data !== 'object') return makeHiddenSnapshot('siliconflow');
|
|
460
|
+
|
|
461
|
+
const remaining = parseFiniteNumber(data.balance);
|
|
462
|
+
if (remaining == null) return makeHiddenSnapshot('siliconflow');
|
|
463
|
+
const chargeBalance = parseFiniteNumber(data.chargeBalance);
|
|
464
|
+
const totalBalance = parseFiniteNumber(data.totalBalance);
|
|
465
|
+
return makeVisibleSnapshot('siliconflow', {
|
|
466
|
+
remaining,
|
|
467
|
+
used: totalBalance != null ? Math.max(0, totalBalance - remaining) : undefined,
|
|
468
|
+
total: totalBalance ?? chargeBalance ?? remaining
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function extractQuotaUnitFromStatus(payload) {
|
|
473
|
+
if (!payload || typeof payload !== 'object') return null;
|
|
474
|
+
const data = payload.data && typeof payload.data === 'object' ? payload.data : payload;
|
|
475
|
+
return normalizeQuotaUnit(data?.quota_per_unit || data?.quotaPerUnit, null);
|
|
476
|
+
}
|
|
477
|
+
|
|
306
478
|
function parseSubscriptionItems(payload) {
|
|
307
479
|
const data = extractSub2ApiData(payload);
|
|
308
480
|
const candidates = [
|
|
@@ -352,14 +524,458 @@ async function fetchSub2ApiSubscriptionSummary(baseUrl, headers) {
|
|
|
352
524
|
return {};
|
|
353
525
|
}
|
|
354
526
|
|
|
355
|
-
|
|
527
|
+
function readSub2ApiMonthlyRemaining(subscription) {
|
|
528
|
+
if (!subscription || typeof subscription !== 'object') return null;
|
|
529
|
+
const used = readMoneyField(subscription, 'monthlyUsedUsd', 'monthly_usage_usd', 'monthly_used_usd', 'monthly_used', 'used_monthly_usd') || 0;
|
|
530
|
+
const limit = readMoneyField(subscription, 'monthlyLimitUsd', 'monthly_limit_usd', 'monthly_limit', 'limit_monthly_usd');
|
|
531
|
+
if (limit == null) return null;
|
|
532
|
+
return Math.max(0, limit - used);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function nonNegativeMoney(value) {
|
|
536
|
+
const parsed = parseFiniteNumber(value);
|
|
537
|
+
return parsed != null && parsed >= 0 ? parsed : null;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function buildSub2ApiUsageSnapshot(payload) {
|
|
541
|
+
if (!isSub2ApiUsagePayload(payload)) return makeHiddenSnapshot('sub2api');
|
|
542
|
+
if (payload.isValid === false) return makeHiddenSnapshot('sub2api');
|
|
543
|
+
|
|
544
|
+
const quota = payload.quota && typeof payload.quota === 'object' ? payload.quota : null;
|
|
545
|
+
if (quota) {
|
|
546
|
+
const remaining = nonNegativeMoney(quota.remaining) ?? nonNegativeMoney(payload.remaining);
|
|
547
|
+
if (remaining != null) {
|
|
548
|
+
return makeVisibleSnapshot('sub2api', {
|
|
549
|
+
remaining,
|
|
550
|
+
used: readMoneyField(quota, 'used', 'quota_used', 'used_usd'),
|
|
551
|
+
total: readMoneyField(quota, 'limit', 'quota', 'total')
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const remaining = nonNegativeMoney(payload.balance) ?? nonNegativeMoney(payload.remaining);
|
|
557
|
+
const monthlyRemaining = readSub2ApiMonthlyRemaining(payload.subscription);
|
|
558
|
+
return makeVisibleSnapshot('sub2api', {
|
|
559
|
+
remaining,
|
|
560
|
+
monthlyRemaining
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function addUnique(list, value) {
|
|
565
|
+
if (value && !list.includes(value)) {
|
|
566
|
+
list.push(value);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function toOriginApiBase(value) {
|
|
571
|
+
try {
|
|
572
|
+
const parsed = new URL(value);
|
|
573
|
+
return `${parsed.origin}/api`;
|
|
574
|
+
} catch {
|
|
575
|
+
return '';
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function toPathApiBase(value) {
|
|
580
|
+
try {
|
|
581
|
+
const normalized = normalizeBaseUrl(value);
|
|
582
|
+
if (!normalized) return '';
|
|
583
|
+
return normalized.toLowerCase().endsWith('/api') ? normalized : joinUrl(normalized, '/api');
|
|
584
|
+
} catch {
|
|
585
|
+
return '';
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function toOpenRouterApiBase(value) {
|
|
590
|
+
try {
|
|
591
|
+
const parsed = new URL(String(value || '').trim());
|
|
592
|
+
parsed.hash = '';
|
|
593
|
+
parsed.search = '';
|
|
594
|
+
const origin = parsed.origin;
|
|
595
|
+
const host = parsed.hostname.toLowerCase();
|
|
596
|
+
const pathname = (parsed.pathname || '').replace(/\/+$/, '');
|
|
597
|
+
const lower = pathname.toLowerCase();
|
|
598
|
+
|
|
599
|
+
if (!pathname || pathname === '/') {
|
|
600
|
+
return `${origin}/api/v1`;
|
|
601
|
+
}
|
|
602
|
+
if (lower.endsWith('/api/v1')) {
|
|
603
|
+
return `${origin}${pathname}`;
|
|
604
|
+
}
|
|
605
|
+
if (lower.endsWith('/api')) {
|
|
606
|
+
return `${origin}${pathname}/v1`;
|
|
607
|
+
}
|
|
608
|
+
if (lower.endsWith('/v1')) {
|
|
609
|
+
const parent = pathname.slice(0, -3).replace(/\/+$/, '');
|
|
610
|
+
if (!parent || parent === '') return `${origin}/api/v1`;
|
|
611
|
+
if (parent.toLowerCase().endsWith('/api')) return `${origin}${parent}/v1`;
|
|
612
|
+
return `${origin}${parent}/api/v1`;
|
|
613
|
+
}
|
|
614
|
+
if (host === 'openrouter.ai' || host.endsWith('.openrouter.ai')) {
|
|
615
|
+
return `${origin}/api/v1`;
|
|
616
|
+
}
|
|
617
|
+
return `${origin}${pathname}/api/v1`;
|
|
618
|
+
} catch {
|
|
619
|
+
return '';
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function buildOpenRouterApiBaseCandidates(baseUrl, channel = {}) {
|
|
624
|
+
const candidates = [];
|
|
625
|
+
const inputs = [
|
|
626
|
+
channel.balanceBaseUrl,
|
|
627
|
+
channel.websiteUrl,
|
|
628
|
+
channel.baseUrl,
|
|
629
|
+
baseUrl
|
|
630
|
+
].filter(Boolean);
|
|
631
|
+
|
|
632
|
+
for (const input of inputs) {
|
|
633
|
+
addUnique(candidates, toOpenRouterApiBase(input));
|
|
634
|
+
}
|
|
635
|
+
return candidates;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function buildSiliconFlowApiBaseCandidates(baseUrl, channel = {}) {
|
|
639
|
+
const candidates = [];
|
|
640
|
+
const inputs = [
|
|
641
|
+
channel.balanceBaseUrl,
|
|
642
|
+
channel.websiteUrl,
|
|
643
|
+
channel.baseUrl,
|
|
644
|
+
baseUrl
|
|
645
|
+
].filter(Boolean);
|
|
646
|
+
|
|
647
|
+
for (const input of inputs) {
|
|
648
|
+
try {
|
|
649
|
+
const parsed = new URL(input);
|
|
650
|
+
const origin = parsed.origin;
|
|
651
|
+
const pathname = (parsed.pathname || '').replace(/\/+$/, '');
|
|
652
|
+
const lower = pathname.toLowerCase();
|
|
653
|
+
if (lower.endsWith('/v1')) {
|
|
654
|
+
addUnique(candidates, `${origin}${pathname}`);
|
|
655
|
+
} else {
|
|
656
|
+
addUnique(candidates, `${origin}/v1`);
|
|
657
|
+
}
|
|
658
|
+
} catch {}
|
|
659
|
+
}
|
|
660
|
+
return candidates;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function hasGatewayPathSegment(value) {
|
|
664
|
+
try {
|
|
665
|
+
const parsed = new URL(normalizeBaseUrl(value));
|
|
666
|
+
return parsed.pathname
|
|
667
|
+
.split('/')
|
|
668
|
+
.filter(Boolean)
|
|
669
|
+
.some(segment => GATEWAY_PATH_SEGMENTS.has(segment.toLowerCase()));
|
|
670
|
+
} catch {
|
|
671
|
+
return false;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function build88CodeApiBaseCandidates(baseUrl, channel = {}) {
|
|
676
|
+
const candidates = [];
|
|
677
|
+
const inputs = [
|
|
678
|
+
channel.balanceBaseUrl,
|
|
679
|
+
channel.websiteUrl,
|
|
680
|
+
channel.baseUrl,
|
|
681
|
+
baseUrl
|
|
682
|
+
].filter(Boolean);
|
|
683
|
+
|
|
684
|
+
for (const input of inputs) {
|
|
685
|
+
const originApiBase = toOriginApiBase(input);
|
|
686
|
+
const pathApiBase = toPathApiBase(input);
|
|
687
|
+
if (hasGatewayPathSegment(input)) {
|
|
688
|
+
addUnique(candidates, originApiBase);
|
|
689
|
+
addUnique(candidates, pathApiBase);
|
|
690
|
+
} else {
|
|
691
|
+
addUnique(candidates, pathApiBase);
|
|
692
|
+
addUnique(candidates, originApiBase);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
return candidates;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function get88CodeApiBaseFromStrategy(strategy = {}) {
|
|
699
|
+
return strategy.apiBase || strategy.baseUrl;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function get88CodeRequestBody(channel = {}) {
|
|
703
|
+
const model = String(
|
|
704
|
+
channel.model
|
|
705
|
+
|| channel.speedTestModel
|
|
706
|
+
|| channel.defaultModel
|
|
707
|
+
|| ''
|
|
708
|
+
).trim();
|
|
709
|
+
return model ? { model } : {};
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function extract88CodeData(payload) {
|
|
713
|
+
if (!payload || typeof payload !== 'object') return null;
|
|
714
|
+
if (!Object.prototype.hasOwnProperty.call(payload, 'data')) return null;
|
|
715
|
+
return payload.data;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function normalize88CodeSubscription(subscription = {}) {
|
|
719
|
+
const plan = subscription.subscriptionPlan && typeof subscription.subscriptionPlan === 'object'
|
|
720
|
+
? subscription.subscriptionPlan
|
|
721
|
+
: {};
|
|
722
|
+
const creditLimit = parseFiniteNumber(subscription.creditLimit)
|
|
723
|
+
?? parseFiniteNumber(plan.creditLimit)
|
|
724
|
+
?? 0;
|
|
725
|
+
return {
|
|
726
|
+
id: parseFiniteNumber(subscription.id) ?? 0,
|
|
727
|
+
planName: String(subscription.subscriptionPlanName || subscription.subscriptionName || plan.subscriptionName || ''),
|
|
728
|
+
status: String(subscription.subscriptionStatus || subscription.status || ''),
|
|
729
|
+
isActive: subscription.isActive === true,
|
|
730
|
+
remainingDays: parseFiniteNumber(subscription.remainingDays) ?? 0,
|
|
731
|
+
creditLimit,
|
|
732
|
+
currentCredits: parseFiniteNumber(subscription.currentCredits) ?? 0
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function get88CodeBillingPriority(planName) {
|
|
737
|
+
const normalized = String(planName || '').trim().toUpperCase();
|
|
738
|
+
if (normalized === 'FREE') return 3;
|
|
739
|
+
if (normalized === 'PAYGO') return 2;
|
|
740
|
+
return 1;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function select88CodeSubscription(subscriptions = []) {
|
|
744
|
+
const active = subscriptions
|
|
745
|
+
.map(normalize88CodeSubscription)
|
|
746
|
+
.filter(subscription => subscription.isActive)
|
|
747
|
+
.filter(subscription => !subscription.status || subscription.status === '活跃中' || subscription.remainingDays > 0)
|
|
748
|
+
.sort((left, right) => {
|
|
749
|
+
const priorityDiff = get88CodeBillingPriority(left.planName) - get88CodeBillingPriority(right.planName);
|
|
750
|
+
if (priorityDiff !== 0) return priorityDiff;
|
|
751
|
+
return left.id - right.id;
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
return active.find(subscription => (
|
|
755
|
+
subscription.planName.toUpperCase() !== 'FREE'
|
|
756
|
+
&& subscription.creditLimit > 0
|
|
757
|
+
&& subscription.currentCredits < subscription.creditLimit
|
|
758
|
+
))
|
|
759
|
+
|| active.find(subscription => (
|
|
760
|
+
subscription.planName.toUpperCase() !== 'FREE'
|
|
761
|
+
&& subscription.currentCredits > 0
|
|
762
|
+
))
|
|
763
|
+
|| null;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function hasOnly88CodeFreeSubscription(entities = []) {
|
|
767
|
+
const active = Array.isArray(entities)
|
|
768
|
+
? entities.filter(item => item?.isActive === true)
|
|
769
|
+
: [];
|
|
770
|
+
if (!active.length) return false;
|
|
771
|
+
return !active.some((item) => {
|
|
772
|
+
const name = String(item.subscriptionName || item.subscriptionPlanName || '').toUpperCase();
|
|
773
|
+
return name && name !== 'FREE' && name !== 'PAYGO';
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
function build88CodeBalanceSnapshotFromValues(values = {}) {
|
|
778
|
+
const remainingRaw = parseFiniteNumber(values.currentCredits);
|
|
779
|
+
const totalRaw = parseFiniteNumber(values.creditLimit);
|
|
780
|
+
if (remainingRaw == null && totalRaw == null) {
|
|
781
|
+
return makeHiddenSnapshot('88code');
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
const remaining = Math.max(0, remainingRaw ?? 0);
|
|
785
|
+
const total = totalRaw != null && totalRaw > 0 ? totalRaw : undefined;
|
|
786
|
+
const used = total != null ? Math.max(0, total - remaining) : undefined;
|
|
787
|
+
return makeVisibleSnapshot('88code', {
|
|
788
|
+
remaining,
|
|
789
|
+
used,
|
|
790
|
+
total
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function build88CodeUsageSnapshot(data) {
|
|
795
|
+
if (!data || typeof data !== 'object') return makeHiddenSnapshot('88code');
|
|
796
|
+
const entities = Array.isArray(data.subscriptionEntityList) ? data.subscriptionEntityList : [];
|
|
797
|
+
const activeEntity = entities
|
|
798
|
+
.filter(item => item?.isActive === true)
|
|
799
|
+
.filter(item => String(item.subscriptionName || '').toUpperCase() !== 'FREE')
|
|
800
|
+
.find((item) => {
|
|
801
|
+
const total = parseFiniteNumber(item.creditLimit);
|
|
802
|
+
const remaining = parseFiniteNumber(item.currentCredits);
|
|
803
|
+
return total != null && remaining != null && remaining < total;
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
const selected = activeEntity || data;
|
|
807
|
+
return build88CodeBalanceSnapshotFromValues({
|
|
808
|
+
creditLimit: selected.creditLimit,
|
|
809
|
+
currentCredits: selected.currentCredits
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function is88CodeUsageDataValid(data) {
|
|
814
|
+
if (!data || typeof data !== 'object') return false;
|
|
815
|
+
const creditLimit = parseFiniteNumber(data.creditLimit) ?? 0;
|
|
816
|
+
const entities = Array.isArray(data.subscriptionEntityList) ? data.subscriptionEntityList : [];
|
|
817
|
+
return creditLimit > 0 || entities.length > 0;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
function build88CodeSubscriptionSnapshot(payload) {
|
|
821
|
+
const data = extract88CodeData(payload);
|
|
822
|
+
const subscriptions = Array.isArray(data) ? data : [];
|
|
823
|
+
const selected = select88CodeSubscription(subscriptions);
|
|
824
|
+
if (!selected) return makeHiddenSnapshot('88code');
|
|
825
|
+
return build88CodeBalanceSnapshotFromValues({
|
|
826
|
+
creditLimit: selected.creditLimit,
|
|
827
|
+
currentCredits: selected.currentCredits
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
async function post88CodeJson(url, token, channel) {
|
|
832
|
+
return requestJson(url, {
|
|
833
|
+
method: 'POST',
|
|
834
|
+
headers: buildAuthHeaders(token, { 'Content-Type': 'application/json' }),
|
|
835
|
+
body: get88CodeRequestBody(channel)
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
async function run88CodeUsageStrategy(strategy, token, channel = {}) {
|
|
840
|
+
const apiBase = get88CodeApiBaseFromStrategy(strategy);
|
|
841
|
+
const payload = await post88CodeJson(joinUrl(apiBase, '/usage'), token, channel);
|
|
842
|
+
const usageData = extract88CodeData(payload);
|
|
843
|
+
if (!is88CodeUsageDataValid(usageData)) return makeHiddenSnapshot('88code');
|
|
844
|
+
const snapshot = build88CodeUsageSnapshot(usageData);
|
|
845
|
+
if (
|
|
846
|
+
snapshot.visible
|
|
847
|
+
&& !hasOnly88CodeFreeSubscription(usageData.subscriptionEntityList)
|
|
848
|
+
&& Number(snapshot.remaining || 0) > 0
|
|
849
|
+
) {
|
|
850
|
+
return snapshot;
|
|
851
|
+
}
|
|
852
|
+
return makeHiddenSnapshot('88code');
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
async function run88CodeSubscriptionStrategy(strategy, token, channel = {}) {
|
|
856
|
+
const apiBase = get88CodeApiBaseFromStrategy(strategy);
|
|
857
|
+
const payload = await post88CodeJson(joinUrl(apiBase, '/subscription'), token, channel);
|
|
858
|
+
return build88CodeSubscriptionSnapshot(payload);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function buildBalanceProbeStrategies(baseUrl, channel = {}, token = '') {
|
|
862
|
+
const hintedPlatform = detectPlatformByUrlHint(
|
|
863
|
+
channel.providerKey,
|
|
864
|
+
channel.name,
|
|
865
|
+
channel.websiteUrl,
|
|
866
|
+
channel.baseUrl || baseUrl
|
|
867
|
+
);
|
|
868
|
+
const strategies = [];
|
|
869
|
+
const seen = new Set();
|
|
870
|
+
const add = (strategy) => {
|
|
871
|
+
const key = JSON.stringify(strategy);
|
|
872
|
+
if (!seen.has(key)) {
|
|
873
|
+
seen.add(key);
|
|
874
|
+
strategies.push(strategy);
|
|
875
|
+
}
|
|
876
|
+
};
|
|
877
|
+
const addSub2ApiStrategies = () => {
|
|
878
|
+
if (isLikelyModelApiKey(token)) {
|
|
879
|
+
add({ type: 'sub2api-usage', baseUrl });
|
|
880
|
+
} else {
|
|
881
|
+
add({ type: 'sub2api-auth', baseUrl });
|
|
882
|
+
add({ type: 'sub2api-usage', baseUrl });
|
|
883
|
+
}
|
|
884
|
+
};
|
|
885
|
+
const addHubStrategy = () => {
|
|
886
|
+
add({
|
|
887
|
+
type: 'hub-user-self',
|
|
888
|
+
baseUrl,
|
|
889
|
+
platform: HUB_PLATFORMS.has(hintedPlatform) ? hintedPlatform : null,
|
|
890
|
+
hintedPlatform
|
|
891
|
+
});
|
|
892
|
+
};
|
|
893
|
+
const addNewApiTokenUsageStrategy = () => {
|
|
894
|
+
add({ type: 'new-api-token-usage', baseUrl });
|
|
895
|
+
};
|
|
896
|
+
const addNewApiTokenUsageWithStatusStrategy = () => {
|
|
897
|
+
add({ type: 'new-api-token-usage-status', baseUrl });
|
|
898
|
+
};
|
|
899
|
+
const add88CodeStrategies = () => {
|
|
900
|
+
for (const apiBase of build88CodeApiBaseCandidates(baseUrl, channel)) {
|
|
901
|
+
add({ type: '88code-usage', baseUrl, apiBase });
|
|
902
|
+
add({ type: '88code-subscription', baseUrl, apiBase });
|
|
903
|
+
}
|
|
904
|
+
};
|
|
905
|
+
const addOpenRouterStrategies = () => {
|
|
906
|
+
for (const apiBase of buildOpenRouterApiBaseCandidates(baseUrl, channel)) {
|
|
907
|
+
add({ type: 'openrouter-key', baseUrl, apiBase, endpoint: '/key' });
|
|
908
|
+
add({ type: 'openrouter-key', baseUrl, apiBase, endpoint: '/auth/key' });
|
|
909
|
+
add({ type: 'openrouter-credits', baseUrl, apiBase });
|
|
910
|
+
}
|
|
911
|
+
};
|
|
912
|
+
const addSiliconFlowStrategies = () => {
|
|
913
|
+
for (const apiBase of buildSiliconFlowApiBaseCandidates(baseUrl, channel)) {
|
|
914
|
+
add({ type: 'siliconflow-user-info', baseUrl, apiBase });
|
|
915
|
+
}
|
|
916
|
+
};
|
|
917
|
+
|
|
918
|
+
if (UNSUPPORTED_BALANCE_PLATFORMS.has(hintedPlatform)) {
|
|
919
|
+
add({ type: 'unsupported-balance', platform: hintedPlatform, baseUrl });
|
|
920
|
+
return strategies;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
if (hintedPlatform === 'siliconflow') {
|
|
924
|
+
addSiliconFlowStrategies();
|
|
925
|
+
return strategies;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
if (hintedPlatform === '88code') {
|
|
929
|
+
add88CodeStrategies();
|
|
930
|
+
addSub2ApiStrategies();
|
|
931
|
+
addHubStrategy();
|
|
932
|
+
return strategies;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
if (hintedPlatform === 'openrouter' || isLikelyOpenRouterApiKey(token)) {
|
|
936
|
+
addOpenRouterStrategies();
|
|
937
|
+
addSub2ApiStrategies();
|
|
938
|
+
addNewApiTokenUsageWithStatusStrategy();
|
|
939
|
+
addHubStrategy();
|
|
940
|
+
return strategies;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
if (hintedPlatform === 'sub2api') {
|
|
944
|
+
addSub2ApiStrategies();
|
|
945
|
+
addNewApiTokenUsageWithStatusStrategy();
|
|
946
|
+
addHubStrategy();
|
|
947
|
+
return strategies;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
if (HUB_PLATFORMS.has(hintedPlatform)) {
|
|
951
|
+
if (hintedPlatform === 'new-api') {
|
|
952
|
+
addNewApiTokenUsageWithStatusStrategy();
|
|
953
|
+
}
|
|
954
|
+
addHubStrategy();
|
|
955
|
+
addSub2ApiStrategies();
|
|
956
|
+
return strategies;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
if (isLikelyModelApiKey(token)) {
|
|
960
|
+
addSub2ApiStrategies();
|
|
961
|
+
addNewApiTokenUsageWithStatusStrategy();
|
|
962
|
+
addHubStrategy();
|
|
963
|
+
} else {
|
|
964
|
+
addHubStrategy();
|
|
965
|
+
addNewApiTokenUsageWithStatusStrategy();
|
|
966
|
+
addSub2ApiStrategies();
|
|
967
|
+
}
|
|
968
|
+
return strategies;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
async function runSub2ApiAuthStrategy(strategy, token) {
|
|
356
972
|
const headers = buildAuthHeaders(token);
|
|
357
|
-
const payload = await
|
|
973
|
+
const payload = await tryRequestJson(joinUrl(strategy.baseUrl, '/api/v1/auth/me'), { headers });
|
|
358
974
|
const data = extractSub2ApiData(payload);
|
|
359
975
|
const balance = parseFiniteNumber(data?.balance);
|
|
360
976
|
if (balance == null) return makeHiddenSnapshot('sub2api');
|
|
361
977
|
|
|
362
|
-
const summary = await fetchSub2ApiSubscriptionSummary(baseUrl, headers);
|
|
978
|
+
const summary = await fetchSub2ApiSubscriptionSummary(strategy.baseUrl, headers);
|
|
363
979
|
return makeVisibleSnapshot('sub2api', {
|
|
364
980
|
remaining: balance,
|
|
365
981
|
used: 0,
|
|
@@ -368,33 +984,183 @@ async function fetchSub2ApiBalance(baseUrl, token) {
|
|
|
368
984
|
});
|
|
369
985
|
}
|
|
370
986
|
|
|
371
|
-
async function
|
|
987
|
+
async function runSub2ApiUsageStrategy(strategy, token) {
|
|
988
|
+
const payload = await tryRequestJson(joinUrl(strategy.baseUrl, '/v1/usage'), {
|
|
989
|
+
headers: buildAuthHeaders(token)
|
|
990
|
+
});
|
|
991
|
+
return buildSub2ApiUsageSnapshot(payload);
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
async function runHubUserSelfStrategy(strategy, token) {
|
|
372
995
|
const headers = buildAuthHeaders(token);
|
|
373
|
-
|
|
374
|
-
|
|
996
|
+
let platform = strategy.platform;
|
|
997
|
+
let quotaUnit = null;
|
|
998
|
+
if (!HUB_PLATFORMS.has(platform)) {
|
|
999
|
+
const status = await tryRequestJson(joinUrl(strategy.baseUrl, '/api/status'), { headers: { Accept: 'application/json' } });
|
|
1000
|
+
platform = resolveStatusPlatform(status, strategy.hintedPlatform);
|
|
1001
|
+
quotaUnit = extractQuotaUnitFromStatus(status);
|
|
1002
|
+
}
|
|
1003
|
+
if (!HUB_PLATFORMS.has(platform)) return { snapshot: makeHiddenSnapshot(platform), strategy };
|
|
1004
|
+
|
|
1005
|
+
const payload = await requestJson(joinUrl(strategy.baseUrl, '/api/user/self'), { headers });
|
|
1006
|
+
const snapshot = buildHubBalanceSnapshot(platform, {
|
|
1007
|
+
...payload,
|
|
1008
|
+
data: payload?.data && typeof payload.data === 'object'
|
|
1009
|
+
? { ...payload.data, quota_per_unit: quotaUnit || payload.data.quota_per_unit }
|
|
1010
|
+
: payload?.data
|
|
1011
|
+
});
|
|
1012
|
+
return {
|
|
1013
|
+
snapshot,
|
|
1014
|
+
strategy: {
|
|
1015
|
+
...strategy,
|
|
1016
|
+
platform,
|
|
1017
|
+
quotaUnit: quotaUnit || strategy.quotaUnit
|
|
1018
|
+
}
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
async function runNewApiTokenUsageStrategy(strategy, token) {
|
|
1023
|
+
const payload = await requestJson(joinUrl(strategy.baseUrl, '/api/usage/token'), {
|
|
1024
|
+
headers: buildAuthHeaders(token)
|
|
1025
|
+
});
|
|
1026
|
+
return buildNewApiTokenUsageSnapshot(payload, { quotaUnit: strategy.quotaUnit || QUOTA_UNIT });
|
|
375
1027
|
}
|
|
376
1028
|
|
|
377
|
-
async function
|
|
378
|
-
const
|
|
379
|
-
|
|
1029
|
+
async function runNewApiTokenUsageWithStatusStrategy(strategy, token) {
|
|
1030
|
+
const status = await tryRequestJson(joinUrl(strategy.baseUrl, '/api/status'), {
|
|
1031
|
+
headers: { Accept: 'application/json' }
|
|
1032
|
+
});
|
|
1033
|
+
const quotaUnit = extractQuotaUnitFromStatus(status) || QUOTA_UNIT;
|
|
1034
|
+
const snapshot = await runNewApiTokenUsageStrategy({ ...strategy, quotaUnit }, token);
|
|
1035
|
+
return {
|
|
1036
|
+
snapshot,
|
|
1037
|
+
strategy: {
|
|
1038
|
+
...strategy,
|
|
1039
|
+
quotaUnit
|
|
1040
|
+
}
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
380
1043
|
|
|
381
|
-
|
|
382
|
-
const
|
|
383
|
-
|
|
1044
|
+
async function runOpenRouterKeyStrategy(strategy, token) {
|
|
1045
|
+
const payload = await requestJson(joinUrl(strategy.apiBase || strategy.baseUrl, strategy.endpoint || '/key'), {
|
|
1046
|
+
headers: buildAuthHeaders(token)
|
|
1047
|
+
});
|
|
1048
|
+
return buildOpenRouterKeySnapshot(payload);
|
|
1049
|
+
}
|
|
384
1050
|
|
|
385
|
-
|
|
386
|
-
|
|
1051
|
+
async function runOpenRouterCreditsStrategy(strategy, token) {
|
|
1052
|
+
const payload = await requestJson(joinUrl(strategy.apiBase || strategy.baseUrl, '/credits'), {
|
|
1053
|
+
headers: buildAuthHeaders(token)
|
|
1054
|
+
});
|
|
1055
|
+
return buildOpenRouterCreditsSnapshot(payload);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
async function runSiliconFlowUserInfoStrategy(strategy, token) {
|
|
1059
|
+
const payload = await requestJson(joinUrl(strategy.apiBase || strategy.baseUrl, '/user/info'), {
|
|
1060
|
+
headers: buildAuthHeaders(token)
|
|
1061
|
+
});
|
|
1062
|
+
return buildSiliconFlowUserInfoSnapshot(payload);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
async function runBalanceStrategy(strategy, token, channel = {}) {
|
|
1066
|
+
if (!strategy || !strategy.type) {
|
|
1067
|
+
return { snapshot: makeHiddenSnapshot(), strategy };
|
|
387
1068
|
}
|
|
388
1069
|
|
|
389
|
-
if (
|
|
390
|
-
return
|
|
1070
|
+
if (strategy.type === '88code-usage') {
|
|
1071
|
+
return {
|
|
1072
|
+
snapshot: await run88CodeUsageStrategy(strategy, token, channel),
|
|
1073
|
+
strategy
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
if (strategy.type === '88code-subscription') {
|
|
1077
|
+
return {
|
|
1078
|
+
snapshot: await run88CodeSubscriptionStrategy(strategy, token, channel),
|
|
1079
|
+
strategy
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
if (strategy.type === 'sub2api-auth') {
|
|
1083
|
+
return {
|
|
1084
|
+
snapshot: await runSub2ApiAuthStrategy(strategy, token),
|
|
1085
|
+
strategy
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
if (strategy.type === 'sub2api-usage') {
|
|
1089
|
+
return {
|
|
1090
|
+
snapshot: await runSub2ApiUsageStrategy(strategy, token),
|
|
1091
|
+
strategy
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
if (strategy.type === 'hub-user-self') {
|
|
1095
|
+
return runHubUserSelfStrategy(strategy, token);
|
|
1096
|
+
}
|
|
1097
|
+
if (strategy.type === 'new-api-token-usage') {
|
|
1098
|
+
return {
|
|
1099
|
+
snapshot: await runNewApiTokenUsageStrategy(strategy, token),
|
|
1100
|
+
strategy
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
if (strategy.type === 'new-api-token-usage-status') {
|
|
1104
|
+
return runNewApiTokenUsageWithStatusStrategy(strategy, token);
|
|
1105
|
+
}
|
|
1106
|
+
if (strategy.type === 'openrouter-key') {
|
|
1107
|
+
return {
|
|
1108
|
+
snapshot: await runOpenRouterKeyStrategy(strategy, token),
|
|
1109
|
+
strategy
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
if (strategy.type === 'openrouter-credits') {
|
|
1113
|
+
return {
|
|
1114
|
+
snapshot: await runOpenRouterCreditsStrategy(strategy, token),
|
|
1115
|
+
strategy
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
if (strategy.type === 'unsupported-balance') {
|
|
1119
|
+
return {
|
|
1120
|
+
snapshot: makeHiddenSnapshot(strategy.platform),
|
|
1121
|
+
strategy
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
if (strategy.type === 'siliconflow-user-info') {
|
|
1125
|
+
return {
|
|
1126
|
+
snapshot: await runSiliconFlowUserInfoStrategy(strategy, token),
|
|
1127
|
+
strategy
|
|
1128
|
+
};
|
|
391
1129
|
}
|
|
392
1130
|
|
|
393
|
-
return makeHiddenSnapshot(
|
|
1131
|
+
return { snapshot: makeHiddenSnapshot(), strategy };
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
async function tryBalanceStrategy(strategy, token, channel = {}) {
|
|
1135
|
+
try {
|
|
1136
|
+
const result = await runBalanceStrategy(strategy, token, channel);
|
|
1137
|
+
if (result?.snapshot?.visible) return result;
|
|
1138
|
+
} catch {
|
|
1139
|
+
// Probe errors are intentionally silent; unsupported balances stay hidden.
|
|
1140
|
+
}
|
|
1141
|
+
return null;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
async function probeBalanceFromBase(baseUrl, channel, token) {
|
|
1145
|
+
let lastSnapshot = null;
|
|
1146
|
+
for (const strategy of buildBalanceProbeStrategies(baseUrl, channel, token)) {
|
|
1147
|
+
const result = await tryBalanceStrategy(strategy, token, channel);
|
|
1148
|
+
if (result) return result;
|
|
1149
|
+
if (!lastSnapshot) {
|
|
1150
|
+
lastSnapshot = makeHiddenSnapshot(
|
|
1151
|
+
strategy.type?.startsWith('88code') ? '88code'
|
|
1152
|
+
: strategy.type?.startsWith('openrouter') ? 'openrouter'
|
|
1153
|
+
: strategy.type?.startsWith('siliconflow') ? 'siliconflow'
|
|
1154
|
+
: strategy.type?.startsWith('sub2api') ? 'sub2api'
|
|
1155
|
+
: strategy.platform || null
|
|
1156
|
+
);
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
return lastSnapshot ? { snapshot: lastSnapshot, strategy: null } : null;
|
|
394
1160
|
}
|
|
395
1161
|
|
|
396
1162
|
function buildCacheKey(source, channel) {
|
|
397
|
-
const token =
|
|
1163
|
+
const token = resolveBalanceToken(channel);
|
|
398
1164
|
const tokenHash = token
|
|
399
1165
|
? crypto.createHash('sha256').update(token).digest('hex').slice(0, 16)
|
|
400
1166
|
: 'no-token';
|
|
@@ -417,25 +1183,37 @@ async function refreshChannelBalanceSnapshot(source, channel, options = {}) {
|
|
|
417
1183
|
}
|
|
418
1184
|
|
|
419
1185
|
const bases = buildBaseCandidates(channel.baseUrl);
|
|
420
|
-
if (!bases.length || !
|
|
1186
|
+
if (!bases.length || !resolveBalanceToken(channel)) {
|
|
421
1187
|
return makeHiddenSnapshot(null, { updatedAt: nowIso(currentTime) });
|
|
422
1188
|
}
|
|
423
1189
|
|
|
424
1190
|
let lastSnapshot = null;
|
|
1191
|
+
const token = resolveBalanceToken(channel);
|
|
1192
|
+
const cachedStrategy = balanceStrategyCache.get(key);
|
|
1193
|
+
if (cachedStrategy) {
|
|
1194
|
+
const strategyResult = await tryBalanceStrategy(cachedStrategy, token, channel);
|
|
1195
|
+
if (strategyResult?.snapshot?.visible) {
|
|
1196
|
+
balanceStrategyCache.set(key, strategyResult.strategy);
|
|
1197
|
+
balanceCache.set(key, {
|
|
1198
|
+
snapshot: strategyResult.snapshot,
|
|
1199
|
+
expiresAt: currentTime + CACHE_TTL_MS
|
|
1200
|
+
});
|
|
1201
|
+
return strategyResult.snapshot;
|
|
1202
|
+
}
|
|
1203
|
+
balanceStrategyCache.delete(key);
|
|
1204
|
+
}
|
|
1205
|
+
|
|
425
1206
|
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.
|
|
1207
|
+
const result = await probeBalanceFromBase(baseUrl, channel, token);
|
|
1208
|
+
if (result?.snapshot?.visible) {
|
|
1209
|
+
balanceStrategyCache.set(key, result.strategy);
|
|
1210
|
+
balanceCache.set(key, {
|
|
1211
|
+
snapshot: result.snapshot,
|
|
1212
|
+
expiresAt: currentTime + CACHE_TTL_MS
|
|
1213
|
+
});
|
|
1214
|
+
return result.snapshot;
|
|
438
1215
|
}
|
|
1216
|
+
lastSnapshot = result?.snapshot || makeHiddenSnapshot(null, { updatedAt: nowIso(currentTime) });
|
|
439
1217
|
}
|
|
440
1218
|
|
|
441
1219
|
if (cached?.snapshot?.visible) {
|
|
@@ -543,6 +1321,7 @@ async function refreshChannelBalance(source, channelId) {
|
|
|
543
1321
|
|
|
544
1322
|
function clearBalanceCache() {
|
|
545
1323
|
balanceCache.clear();
|
|
1324
|
+
balanceStrategyCache.clear();
|
|
546
1325
|
}
|
|
547
1326
|
|
|
548
1327
|
module.exports = {
|
|
@@ -553,8 +1332,22 @@ module.exports = {
|
|
|
553
1332
|
normalizeBaseUrl,
|
|
554
1333
|
buildBaseCandidates,
|
|
555
1334
|
buildHubBalanceSnapshot,
|
|
1335
|
+
buildNewApiTokenUsageSnapshot,
|
|
1336
|
+
buildOpenRouterKeySnapshot,
|
|
1337
|
+
buildOpenRouterCreditsSnapshot,
|
|
1338
|
+
buildSiliconFlowUserInfoSnapshot,
|
|
556
1339
|
detectPlatformByUrlHint,
|
|
1340
|
+
isSub2ApiUsagePayload,
|
|
1341
|
+
buildSub2ApiUsageSnapshot,
|
|
557
1342
|
resolveStatusPlatform,
|
|
1343
|
+
resolveBalanceToken,
|
|
1344
|
+
build88CodeApiBaseCandidates,
|
|
1345
|
+
buildOpenRouterApiBaseCandidates,
|
|
1346
|
+
buildSiliconFlowApiBaseCandidates,
|
|
1347
|
+
build88CodeUsageSnapshot,
|
|
1348
|
+
build88CodeSubscriptionSnapshot,
|
|
1349
|
+
buildBalanceProbeStrategies,
|
|
1350
|
+
runBalanceStrategy,
|
|
558
1351
|
refreshChannelBalanceSnapshot,
|
|
559
1352
|
makeVisibleSnapshot,
|
|
560
1353
|
requestJson
|