coding-tool-x 3.6.2 → 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-DLEiiIbw.js → Analytics-BTmqXkR-.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-BxEw8kS_.js → ConfigTemplates-z3w0kyNl.js} +1 -1
- package/dist/web/assets/{Home-Bk6Bsjmm.js → Home-DU-NVre0.js} +1 -1
- package/dist/web/assets/{PluginManager-PCYV0eXK.js → PluginManager-44r9LaWV.js} +1 -1
- package/dist/web/assets/{ProjectList-AU3SqUA9.js → ProjectList-DW_MEKRy.js} +1 -1
- package/dist/web/assets/SessionList-B8upyqsp.js +1 -0
- package/dist/web/assets/{SkillManager-DfNofdEQ.js → SkillManager-DnzRfF_o.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-DPCSLn59.js → WorkspaceManager-BuRpVJRU.js} +1 -1
- package/dist/web/assets/{icons-QFXzEgqM.js → icons-U50_Cfbe.js} +1 -1
- package/dist/web/assets/index-BkLQex1R.css +1 -0
- package/dist/web/assets/index-gIbIMdSO.js +2 -0
- package/dist/web/assets/{naive-ui-DM897mIP.js → naive-ui-Cjh6GLto.js} +1 -1
- package/dist/web/assets/{vendors-DQA4Hw9v.js → vendors--KP7Ux0s.js} +1 -1
- package/dist/web/assets/{vue-vendor-B5RqjEjQ.js → vue-vendor-C5aMkRmm.js} +1 -1
- package/dist/web/index.html +6 -6
- package/package.json +1 -1
- package/src/server/api/channel-balances.js +36 -0
- 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/index.js +1 -0
- package/src/server/services/channel-balance.js +1355 -0
- package/src/server/services/ui-config.js +4 -0
- package/dist/web/assets/SessionList-DIXB0QPe.js +0 -1
- package/dist/web/assets/index-C2yKfEbU.js +0 -2
- package/dist/web/assets/index-dzKgnRhx.css +0 -1
|
@@ -0,0 +1,1355 @@
|
|
|
1
|
+
const http = require('http');
|
|
2
|
+
const https = require('https');
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const { URL } = require('url');
|
|
5
|
+
const { loadUIConfig } = require('./ui-config');
|
|
6
|
+
|
|
7
|
+
const CACHE_TTL_MS = 5 * 60 * 1000;
|
|
8
|
+
const REQUEST_TIMEOUT_MS = 8000;
|
|
9
|
+
const MAX_RESPONSE_BYTES = 1024 * 1024;
|
|
10
|
+
const QUOTA_UNIT = 500000;
|
|
11
|
+
const VELOERA_QUOTA_UNIT = 1000000;
|
|
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']);
|
|
16
|
+
|
|
17
|
+
const balanceCache = new Map();
|
|
18
|
+
const balanceStrategyCache = new Map();
|
|
19
|
+
|
|
20
|
+
function nowIso(now = Date.now()) {
|
|
21
|
+
return new Date(now).toISOString();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function stripBearer(value) {
|
|
25
|
+
const token = String(value || '').trim();
|
|
26
|
+
return token.toLowerCase().startsWith('bearer ') ? token.slice(7).trim() : token;
|
|
27
|
+
}
|
|
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 buildAuthHeaders(token, extra = {}) {
|
|
42
|
+
return {
|
|
43
|
+
Accept: 'application/json',
|
|
44
|
+
Authorization: `Bearer ${stripBearer(token)}`,
|
|
45
|
+
...extra
|
|
46
|
+
};
|
|
47
|
+
}
|
|
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
|
+
|
|
57
|
+
function parseFiniteNumber(value) {
|
|
58
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
59
|
+
if (typeof value === 'string') {
|
|
60
|
+
const parsed = Number(value.trim());
|
|
61
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function roundMoney(value) {
|
|
67
|
+
if (!Number.isFinite(value)) return value;
|
|
68
|
+
return Math.round(value * 1000000) / 1000000;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function normalizeQuotaUnit(value, fallback = QUOTA_UNIT) {
|
|
72
|
+
const parsed = parseFiniteNumber(value);
|
|
73
|
+
return parsed != null && parsed > 0 ? parsed : fallback;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function formatMoney(value) {
|
|
77
|
+
if (!Number.isFinite(value)) return '';
|
|
78
|
+
return `$${value.toFixed(2)}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function makeHiddenSnapshot(platform = null, { stale = false, updatedAt = nowIso() } = {}) {
|
|
82
|
+
return {
|
|
83
|
+
visible: false,
|
|
84
|
+
platform,
|
|
85
|
+
updatedAt,
|
|
86
|
+
stale
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function makeVisibleSnapshot(platform, input, { stale = false, updatedAt = nowIso() } = {}) {
|
|
91
|
+
const remaining = parseFiniteNumber(input.remaining);
|
|
92
|
+
const monthlyRemaining = parseFiniteNumber(input.monthlyRemaining);
|
|
93
|
+
if (remaining == null && monthlyRemaining == null) {
|
|
94
|
+
return makeHiddenSnapshot(platform, { stale, updatedAt });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const labelValue = remaining != null ? remaining : monthlyRemaining;
|
|
98
|
+
const labelPrefix = remaining != null ? '余额' : '月余';
|
|
99
|
+
const snapshot = {
|
|
100
|
+
visible: true,
|
|
101
|
+
platform,
|
|
102
|
+
label: `${labelPrefix} ${formatMoney(labelValue)}`,
|
|
103
|
+
updatedAt,
|
|
104
|
+
stale
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
if (remaining != null) snapshot.remaining = roundMoney(remaining);
|
|
108
|
+
const used = parseFiniteNumber(input.used);
|
|
109
|
+
if (used != null) snapshot.used = roundMoney(used);
|
|
110
|
+
const total = parseFiniteNumber(input.total);
|
|
111
|
+
if (total != null) snapshot.total = roundMoney(total);
|
|
112
|
+
if (monthlyRemaining != null) snapshot.monthlyRemaining = roundMoney(monthlyRemaining);
|
|
113
|
+
return snapshot;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function normalizeBaseUrl(value) {
|
|
117
|
+
const raw = String(value || '').trim();
|
|
118
|
+
if (!raw) return '';
|
|
119
|
+
|
|
120
|
+
let parsed;
|
|
121
|
+
try {
|
|
122
|
+
parsed = new URL(raw);
|
|
123
|
+
} catch {
|
|
124
|
+
return '';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
parsed.hash = '';
|
|
128
|
+
parsed.search = '';
|
|
129
|
+
let pathname = parsed.pathname || '/';
|
|
130
|
+
pathname = pathname.replace(/\/+$/, '') || '/';
|
|
131
|
+
const lower = pathname.toLowerCase();
|
|
132
|
+
const suffixes = [
|
|
133
|
+
'/v1/chat/completions',
|
|
134
|
+
'/v1/responses',
|
|
135
|
+
'/v1/messages',
|
|
136
|
+
'/v1beta/models',
|
|
137
|
+
'/chat/completions',
|
|
138
|
+
'/responses',
|
|
139
|
+
'/messages',
|
|
140
|
+
'/v1beta',
|
|
141
|
+
'/v1'
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
for (const suffix of suffixes) {
|
|
145
|
+
if (lower === suffix || lower.endsWith(suffix)) {
|
|
146
|
+
pathname = pathname.slice(0, pathname.length - suffix.length) || '/';
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
parsed.pathname = pathname.replace(/\/+$/, '') || '/';
|
|
152
|
+
return parsed.toString().replace(/\/$/, '');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function buildBaseCandidates(baseUrl) {
|
|
156
|
+
const normalized = normalizeBaseUrl(baseUrl);
|
|
157
|
+
if (!normalized) return [];
|
|
158
|
+
|
|
159
|
+
const candidates = [normalized];
|
|
160
|
+
try {
|
|
161
|
+
const parsed = new URL(normalized);
|
|
162
|
+
const origin = parsed.origin;
|
|
163
|
+
if (origin && !candidates.includes(origin)) {
|
|
164
|
+
candidates.push(origin);
|
|
165
|
+
}
|
|
166
|
+
} catch {}
|
|
167
|
+
return candidates;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function joinUrl(baseUrl, endpoint) {
|
|
171
|
+
const base = String(baseUrl || '').replace(/\/+$/, '');
|
|
172
|
+
const path = String(endpoint || '').startsWith('/') ? endpoint : `/${endpoint}`;
|
|
173
|
+
return `${base}${path}`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function requestJson(url, {
|
|
177
|
+
method = 'GET',
|
|
178
|
+
headers = {},
|
|
179
|
+
body,
|
|
180
|
+
timeoutMs = REQUEST_TIMEOUT_MS,
|
|
181
|
+
redirectCount = 0
|
|
182
|
+
} = {}) {
|
|
183
|
+
return new Promise((resolve, reject) => {
|
|
184
|
+
let parsed;
|
|
185
|
+
try {
|
|
186
|
+
parsed = new URL(url);
|
|
187
|
+
} catch (error) {
|
|
188
|
+
reject(error);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
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
|
+
|
|
201
|
+
const req = client.request({
|
|
202
|
+
protocol: parsed.protocol,
|
|
203
|
+
hostname: parsed.hostname,
|
|
204
|
+
port: parsed.port,
|
|
205
|
+
path: `${parsed.pathname}${parsed.search}`,
|
|
206
|
+
method,
|
|
207
|
+
headers: requestHeaders
|
|
208
|
+
}, (res) => {
|
|
209
|
+
const chunks = [];
|
|
210
|
+
let total = 0;
|
|
211
|
+
|
|
212
|
+
res.on('data', (chunk) => {
|
|
213
|
+
total += chunk.length;
|
|
214
|
+
if (total > MAX_RESPONSE_BYTES) {
|
|
215
|
+
req.destroy(new Error('response too large'));
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
chunks.push(chunk);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
res.on('end', () => {
|
|
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
|
+
}
|
|
247
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
248
|
+
const error = new Error(`HTTP ${res.statusCode}`);
|
|
249
|
+
error.statusCode = res.statusCode;
|
|
250
|
+
error.payload = payload;
|
|
251
|
+
reject(error);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
if (parseError) {
|
|
255
|
+
reject(parseError);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
resolve(payload);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
req.setTimeout(timeoutMs, () => {
|
|
263
|
+
req.destroy(new Error('request timeout'));
|
|
264
|
+
});
|
|
265
|
+
req.on('error', reject);
|
|
266
|
+
if (requestBody != null) {
|
|
267
|
+
req.write(requestBody);
|
|
268
|
+
}
|
|
269
|
+
req.end();
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async function tryRequestJson(url, options) {
|
|
274
|
+
try {
|
|
275
|
+
return await requestJson(url, options);
|
|
276
|
+
} catch (error) {
|
|
277
|
+
if (error?.payload && typeof error.payload === 'object') {
|
|
278
|
+
return error.payload;
|
|
279
|
+
}
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
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';
|
|
292
|
+
if (value.includes('sub2api')) return 'sub2api';
|
|
293
|
+
if (value.includes('anyrouter')) return 'anyrouter';
|
|
294
|
+
if (value.includes('donehub') || value.includes('done-hub')) return 'done-hub';
|
|
295
|
+
if (value.includes('onehub') || value.includes('one-hub')) return 'one-hub';
|
|
296
|
+
if (value.includes('veloera')) return 'veloera';
|
|
297
|
+
if (value.includes('oneapi') || value.includes('one-api')) return 'one-api';
|
|
298
|
+
if (value.includes('newapi') || value.includes('new-api')) return 'new-api';
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function resolveStatusPlatform(payload, hintedPlatform = null) {
|
|
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()
|
|
307
|
+
: '';
|
|
308
|
+
const version = typeof data?.version === 'string'
|
|
309
|
+
? data.version.toLowerCase()
|
|
310
|
+
: '';
|
|
311
|
+
if (payload.success !== true && !systemName && !version) return null;
|
|
312
|
+
|
|
313
|
+
if (hintedPlatform === 'aihubmix' || systemName.includes('aihubmix')) return 'aihubmix';
|
|
314
|
+
if (hintedPlatform === 'anyrouter') return 'anyrouter';
|
|
315
|
+
if (hintedPlatform === 'done-hub') return 'done-hub';
|
|
316
|
+
if (hintedPlatform === 'one-hub') return 'one-hub';
|
|
317
|
+
if (systemName.includes('veloera') || version.includes('veloera')) return 'veloera';
|
|
318
|
+
if (systemName) return hintedPlatform === 'one-api' ? 'one-api' : 'new-api';
|
|
319
|
+
return hintedPlatform || 'one-api';
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function extractSub2ApiData(payload) {
|
|
323
|
+
if (!payload || typeof payload !== 'object') return null;
|
|
324
|
+
if (typeof payload.code === 'number') {
|
|
325
|
+
if (payload.code !== 0 || payload.data === undefined) return null;
|
|
326
|
+
return payload.data;
|
|
327
|
+
}
|
|
328
|
+
if (payload.data && typeof payload.data === 'object') return payload.data;
|
|
329
|
+
return payload;
|
|
330
|
+
}
|
|
331
|
+
|
|
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');
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function extractUserSelfData(payload) {
|
|
344
|
+
if (!payload || typeof payload !== 'object') return null;
|
|
345
|
+
const data = payload.data && typeof payload.data === 'object' ? payload.data : payload;
|
|
346
|
+
if (!data || typeof data !== 'object') return null;
|
|
347
|
+
if (!Object.prototype.hasOwnProperty.call(data, 'quota')) return null;
|
|
348
|
+
return data;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function buildHubBalanceSnapshot(platform, payload) {
|
|
352
|
+
const data = extractUserSelfData(payload);
|
|
353
|
+
if (!data) return makeHiddenSnapshot(platform);
|
|
354
|
+
|
|
355
|
+
const unit = platform === 'veloera'
|
|
356
|
+
? VELOERA_QUOTA_UNIT
|
|
357
|
+
: normalizeQuotaUnit(data.quota_per_unit || data.quotaPerUnit);
|
|
358
|
+
const quotaRaw = parseFiniteNumber(data.quota);
|
|
359
|
+
const usedRaw = parseFiniteNumber(data.used_quota);
|
|
360
|
+
if (quotaRaw == null) return makeHiddenSnapshot(platform);
|
|
361
|
+
|
|
362
|
+
const quota = quotaRaw / unit;
|
|
363
|
+
const used = (usedRaw || 0) / unit;
|
|
364
|
+
let remaining;
|
|
365
|
+
let total;
|
|
366
|
+
|
|
367
|
+
if (platform === 'new-api' || platform === 'anyrouter' || platform === 'done-hub' || platform === 'aihubmix') {
|
|
368
|
+
remaining = quota;
|
|
369
|
+
total = quota + used;
|
|
370
|
+
} else {
|
|
371
|
+
remaining = quota - used;
|
|
372
|
+
total = quota;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return makeVisibleSnapshot(platform, {
|
|
376
|
+
remaining,
|
|
377
|
+
used,
|
|
378
|
+
total
|
|
379
|
+
});
|
|
380
|
+
}
|
|
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
|
+
|
|
478
|
+
function parseSubscriptionItems(payload) {
|
|
479
|
+
const data = extractSub2ApiData(payload);
|
|
480
|
+
const candidates = [
|
|
481
|
+
data?.subscriptions,
|
|
482
|
+
data?.items,
|
|
483
|
+
data?.data,
|
|
484
|
+
payload?.subscriptions,
|
|
485
|
+
payload?.items,
|
|
486
|
+
Array.isArray(data) ? data : null
|
|
487
|
+
];
|
|
488
|
+
for (const candidate of candidates) {
|
|
489
|
+
if (Array.isArray(candidate)) return candidate;
|
|
490
|
+
}
|
|
491
|
+
return [];
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function readMoneyField(item, ...keys) {
|
|
495
|
+
for (const key of keys) {
|
|
496
|
+
const value = parseFiniteNumber(item?.[key]);
|
|
497
|
+
if (value != null) return value;
|
|
498
|
+
}
|
|
499
|
+
return null;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
async function fetchSub2ApiSubscriptionSummary(baseUrl, headers) {
|
|
503
|
+
const endpoints = ['/api/v1/subscriptions/summary', '/api/v1/subscriptions/active'];
|
|
504
|
+
for (const endpoint of endpoints) {
|
|
505
|
+
const payload = await tryRequestJson(joinUrl(baseUrl, endpoint), { headers });
|
|
506
|
+
if (!payload) continue;
|
|
507
|
+
const items = parseSubscriptionItems(payload);
|
|
508
|
+
if (!items.length) continue;
|
|
509
|
+
|
|
510
|
+
let monthlyRemaining = 0;
|
|
511
|
+
let hasMonthlyLimit = false;
|
|
512
|
+
for (const item of items) {
|
|
513
|
+
const used = readMoneyField(item, 'monthlyUsedUsd', 'monthly_used_usd', 'monthly_used', 'used_monthly_usd') || 0;
|
|
514
|
+
const limit = readMoneyField(item, 'monthlyLimitUsd', 'monthly_limit_usd', 'monthly_limit', 'limit_monthly_usd');
|
|
515
|
+
if (limit == null) continue;
|
|
516
|
+
hasMonthlyLimit = true;
|
|
517
|
+
monthlyRemaining += Math.max(0, limit - used);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (hasMonthlyLimit) {
|
|
521
|
+
return { monthlyRemaining };
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return {};
|
|
525
|
+
}
|
|
526
|
+
|
|
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) {
|
|
972
|
+
const headers = buildAuthHeaders(token);
|
|
973
|
+
const payload = await tryRequestJson(joinUrl(strategy.baseUrl, '/api/v1/auth/me'), { headers });
|
|
974
|
+
const data = extractSub2ApiData(payload);
|
|
975
|
+
const balance = parseFiniteNumber(data?.balance);
|
|
976
|
+
if (balance == null) return makeHiddenSnapshot('sub2api');
|
|
977
|
+
|
|
978
|
+
const summary = await fetchSub2ApiSubscriptionSummary(strategy.baseUrl, headers);
|
|
979
|
+
return makeVisibleSnapshot('sub2api', {
|
|
980
|
+
remaining: balance,
|
|
981
|
+
used: 0,
|
|
982
|
+
total: balance,
|
|
983
|
+
monthlyRemaining: summary.monthlyRemaining
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
|
|
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) {
|
|
995
|
+
const headers = buildAuthHeaders(token);
|
|
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 });
|
|
1027
|
+
}
|
|
1028
|
+
|
|
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
|
+
}
|
|
1043
|
+
|
|
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
|
+
}
|
|
1050
|
+
|
|
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 };
|
|
1068
|
+
}
|
|
1069
|
+
|
|
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
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
|
|
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;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
function buildCacheKey(source, channel) {
|
|
1163
|
+
const token = resolveBalanceToken(channel);
|
|
1164
|
+
const tokenHash = token
|
|
1165
|
+
? crypto.createHash('sha256').update(token).digest('hex').slice(0, 16)
|
|
1166
|
+
: 'no-token';
|
|
1167
|
+
return [
|
|
1168
|
+
source,
|
|
1169
|
+
channel.id || channel.name || 'unknown',
|
|
1170
|
+
normalizeBaseUrl(channel.baseUrl || ''),
|
|
1171
|
+
tokenHash
|
|
1172
|
+
].join(':');
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
async function refreshChannelBalanceSnapshot(source, channel, options = {}) {
|
|
1176
|
+
const force = options.force === true;
|
|
1177
|
+
const currentTime = options.now || Date.now();
|
|
1178
|
+
const key = buildCacheKey(source, channel);
|
|
1179
|
+
const cached = balanceCache.get(key);
|
|
1180
|
+
|
|
1181
|
+
if (!force && cached && cached.expiresAt > currentTime) {
|
|
1182
|
+
return cached.snapshot;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
const bases = buildBaseCandidates(channel.baseUrl);
|
|
1186
|
+
if (!bases.length || !resolveBalanceToken(channel)) {
|
|
1187
|
+
return makeHiddenSnapshot(null, { updatedAt: nowIso(currentTime) });
|
|
1188
|
+
}
|
|
1189
|
+
|
|
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
|
+
|
|
1206
|
+
for (const baseUrl of bases) {
|
|
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;
|
|
1215
|
+
}
|
|
1216
|
+
lastSnapshot = result?.snapshot || makeHiddenSnapshot(null, { updatedAt: nowIso(currentTime) });
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
if (cached?.snapshot?.visible) {
|
|
1220
|
+
const staleSnapshot = {
|
|
1221
|
+
...cached.snapshot,
|
|
1222
|
+
stale: true
|
|
1223
|
+
};
|
|
1224
|
+
balanceCache.set(key, {
|
|
1225
|
+
snapshot: staleSnapshot,
|
|
1226
|
+
expiresAt: currentTime + CACHE_TTL_MS
|
|
1227
|
+
});
|
|
1228
|
+
return staleSnapshot;
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
return lastSnapshot || makeHiddenSnapshot(null, { updatedAt: nowIso(currentTime) });
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
function getChannelsForSource(source) {
|
|
1235
|
+
if (source === 'claude') {
|
|
1236
|
+
return require('./channels').getAllChannels() || [];
|
|
1237
|
+
}
|
|
1238
|
+
if (source === 'codex') {
|
|
1239
|
+
return require('./codex-channels').getChannels().channels || [];
|
|
1240
|
+
}
|
|
1241
|
+
if (source === 'gemini') {
|
|
1242
|
+
return require('./gemini-channels').getChannels().channels || [];
|
|
1243
|
+
}
|
|
1244
|
+
if (source === 'opencode') {
|
|
1245
|
+
return require('./opencode-channels').getChannels().channels || [];
|
|
1246
|
+
}
|
|
1247
|
+
return [];
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
function isBalanceDisplayEnabled() {
|
|
1251
|
+
try {
|
|
1252
|
+
return loadUIConfig().channelBalance?.showRemaining === true;
|
|
1253
|
+
} catch {
|
|
1254
|
+
return false;
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
function validateSource(source) {
|
|
1259
|
+
const normalized = String(source || '').trim().toLowerCase();
|
|
1260
|
+
if (!VALID_SOURCES.has(normalized)) {
|
|
1261
|
+
throw new Error('Invalid channel balance source');
|
|
1262
|
+
}
|
|
1263
|
+
return normalized;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
async function mapWithConcurrency(items, limit, worker) {
|
|
1267
|
+
const results = new Array(items.length);
|
|
1268
|
+
let nextIndex = 0;
|
|
1269
|
+
const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
|
|
1270
|
+
while (nextIndex < items.length) {
|
|
1271
|
+
const index = nextIndex;
|
|
1272
|
+
nextIndex += 1;
|
|
1273
|
+
results[index] = await worker(items[index], index);
|
|
1274
|
+
}
|
|
1275
|
+
});
|
|
1276
|
+
await Promise.all(workers);
|
|
1277
|
+
return results;
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
async function getChannelBalances(source, options = {}) {
|
|
1281
|
+
const normalizedSource = validateSource(source);
|
|
1282
|
+
if (!isBalanceDisplayEnabled()) {
|
|
1283
|
+
return { enabled: false, source: normalizedSource, balances: {} };
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
const channels = getChannelsForSource(normalizedSource);
|
|
1287
|
+
const entries = await mapWithConcurrency(channels, 4, async (channel) => {
|
|
1288
|
+
const snapshot = await refreshChannelBalanceSnapshot(normalizedSource, channel, options);
|
|
1289
|
+
return [channel.id, snapshot];
|
|
1290
|
+
});
|
|
1291
|
+
|
|
1292
|
+
return {
|
|
1293
|
+
enabled: true,
|
|
1294
|
+
source: normalizedSource,
|
|
1295
|
+
balances: Object.fromEntries(entries.filter(([id]) => id))
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
async function refreshChannelBalance(source, channelId) {
|
|
1300
|
+
const normalizedSource = validateSource(source);
|
|
1301
|
+
if (!isBalanceDisplayEnabled()) {
|
|
1302
|
+
return { enabled: false, source: normalizedSource, channelId, balance: makeHiddenSnapshot() };
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
const channels = getChannelsForSource(normalizedSource);
|
|
1306
|
+
const channel = channels.find(item => String(item.id) === String(channelId));
|
|
1307
|
+
if (!channel) {
|
|
1308
|
+
const error = new Error('Channel not found');
|
|
1309
|
+
error.statusCode = 404;
|
|
1310
|
+
throw error;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
const balance = await refreshChannelBalanceSnapshot(normalizedSource, channel, { force: true });
|
|
1314
|
+
return {
|
|
1315
|
+
enabled: true,
|
|
1316
|
+
source: normalizedSource,
|
|
1317
|
+
channelId,
|
|
1318
|
+
balance
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
function clearBalanceCache() {
|
|
1323
|
+
balanceCache.clear();
|
|
1324
|
+
balanceStrategyCache.clear();
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
module.exports = {
|
|
1328
|
+
getChannelBalances,
|
|
1329
|
+
refreshChannelBalance,
|
|
1330
|
+
_test: {
|
|
1331
|
+
clearBalanceCache,
|
|
1332
|
+
normalizeBaseUrl,
|
|
1333
|
+
buildBaseCandidates,
|
|
1334
|
+
buildHubBalanceSnapshot,
|
|
1335
|
+
buildNewApiTokenUsageSnapshot,
|
|
1336
|
+
buildOpenRouterKeySnapshot,
|
|
1337
|
+
buildOpenRouterCreditsSnapshot,
|
|
1338
|
+
buildSiliconFlowUserInfoSnapshot,
|
|
1339
|
+
detectPlatformByUrlHint,
|
|
1340
|
+
isSub2ApiUsagePayload,
|
|
1341
|
+
buildSub2ApiUsageSnapshot,
|
|
1342
|
+
resolveStatusPlatform,
|
|
1343
|
+
resolveBalanceToken,
|
|
1344
|
+
build88CodeApiBaseCandidates,
|
|
1345
|
+
buildOpenRouterApiBaseCandidates,
|
|
1346
|
+
buildSiliconFlowApiBaseCandidates,
|
|
1347
|
+
build88CodeUsageSnapshot,
|
|
1348
|
+
build88CodeSubscriptionSnapshot,
|
|
1349
|
+
buildBalanceProbeStrategies,
|
|
1350
|
+
runBalanceStrategy,
|
|
1351
|
+
refreshChannelBalanceSnapshot,
|
|
1352
|
+
makeVisibleSnapshot,
|
|
1353
|
+
requestJson
|
|
1354
|
+
}
|
|
1355
|
+
};
|