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.
Files changed (28) hide show
  1. package/dist/web/assets/{Analytics-D0BDj7bo.js → Analytics-CcGKjDkP.js} +1 -1
  2. package/dist/web/assets/{ConfigTemplates-BnwWmyV7.js → ConfigTemplates-DPOa1w5D.js} +1 -1
  3. package/dist/web/assets/{Home-Yw5bTgrH.js → Home-BP1LxGtp.js} +1 -1
  4. package/dist/web/assets/{PluginManager-CRdRLKsS.js → PluginManager-BVbHUdey.js} +1 -1
  5. package/dist/web/assets/{ProjectList-CDIYAOyV.js → ProjectList-BQrz2FKo.js} +1 -1
  6. package/dist/web/assets/SessionList-CRGer64T.js +1 -0
  7. package/dist/web/assets/{SkillManager-DaM0Ob6l.js → SkillManager-BhJKtWYM.js} +1 -1
  8. package/dist/web/assets/{WorkspaceManager-Y-EZ7NxE.js → WorkspaceManager-pc6tpF3m.js} +1 -1
  9. package/dist/web/assets/{icons-U50_Cfbe.js → icons-QFXzEgqM.js} +1 -1
  10. package/dist/web/assets/index-0isNzXNY.css +1 -0
  11. package/dist/web/assets/index-BsdIJwLf.js +2 -0
  12. package/dist/web/assets/{naive-ui-Cjh6GLto.js → naive-ui-DM897mIP.js} +1 -1
  13. package/dist/web/assets/{vendors--KP7Ux0s.js → vendors-DQA4Hw9v.js} +1 -1
  14. package/dist/web/assets/{vue-vendor-C5aMkRmm.js → vue-vendor-B5RqjEjQ.js} +1 -1
  15. package/dist/web/index.html +6 -6
  16. package/package.json +1 -1
  17. package/src/commands/daemon.js +13 -4
  18. package/src/server/api/channels.js +6 -2
  19. package/src/server/api/codex-channels.js +5 -1
  20. package/src/server/api/codex-sessions.js +24 -1
  21. package/src/server/api/gemini-channels.js +6 -2
  22. package/src/server/api/opencode-channels.js +6 -2
  23. package/src/server/services/channel-balance.js +948 -67
  24. package/src/server/services/gemini-channels.js +8 -0
  25. package/src/server/services/opencode-channels.js +6 -0
  26. package/dist/web/assets/SessionList-BJ2kbl-3.js +0 -1
  27. package/dist/web/assets/index-BkLQex1R.css +0 -1
  28. 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, { headers = {}, timeoutMs = REQUEST_TIMEOUT_MS } = {}) {
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: 'GET',
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
- try {
187
- resolve(raw ? JSON.parse(raw) : null);
188
- } catch (error) {
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(baseUrl) {
211
- const value = String(baseUrl || '').toLowerCase();
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.success !== true) return null;
224
- const systemName = typeof payload?.data?.system_name === 'string'
225
- ? payload.data.system_name.toLowerCase()
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 payload?.data?.version === 'string'
228
- ? payload.data.version.toLowerCase()
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
- async function detectPlatform(baseUrl, token, hintedPlatform = null) {
250
- const headers = buildAuthHeaders(token);
251
-
252
- if (hintedPlatform === 'sub2api') {
253
- return 'sub2api';
254
- }
255
-
256
- const status = await tryRequestJson(joinUrl(baseUrl, '/api/status'), { headers: { Accept: 'application/json' } });
257
- const statusPlatform = resolveStatusPlatform(status, hintedPlatform);
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' ? VELOERA_QUOTA_UNIT : QUOTA_UNIT;
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
- async function fetchSub2ApiBalance(baseUrl, token) {
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 requestJson(joinUrl(baseUrl, '/api/v1/auth/me'), { headers });
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 fetchHubBalance(baseUrl, token, platform) {
372
- const headers = buildAuthHeaders(token);
373
- const payload = await requestJson(joinUrl(baseUrl, '/api/user/self'), { headers });
374
- return buildHubBalanceSnapshot(platform, payload);
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 fetchBalanceFromBase(baseUrl, channel) {
378
- const token = stripBearer(channel.apiKey || channel.token || channel.authToken || '');
379
- if (!baseUrl || !token) return makeHiddenSnapshot();
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
- const hintedPlatform = detectPlatformByUrlHint(channel.baseUrl || baseUrl);
382
- const platform = await detectPlatform(baseUrl, token, hintedPlatform);
383
- if (!platform) return makeHiddenSnapshot();
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
- if (platform === 'sub2api') {
386
- return fetchSub2ApiBalance(baseUrl, token);
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
- if (['new-api', 'one-api', 'one-hub', 'done-hub', 'veloera', 'anyrouter'].includes(platform)) {
390
- return fetchHubBalance(baseUrl, token, platform);
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
- return makeHiddenSnapshot(platform);
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 = stripBearer(channel.apiKey || channel.token || channel.authToken || '');
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 || !stripBearer(channel.apiKey || channel.token || channel.authToken || '')) {
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
- try {
427
- const snapshot = await fetchBalanceFromBase(baseUrl, channel);
428
- if (snapshot?.visible) {
429
- balanceCache.set(key, {
430
- snapshot,
431
- expiresAt: currentTime + CACHE_TTL_MS
432
- });
433
- return snapshot;
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