@lightharu/krouter 1.8.0

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 (61) hide show
  1. package/LICENSE +679 -0
  2. package/README.md +238 -0
  3. package/dist-web/assets/index-CM4-0adf.css +1 -0
  4. package/dist-web/assets/index-DCslvfUR.js +139 -0
  5. package/dist-web/favicon.svg +9 -0
  6. package/dist-web/icon.svg +9 -0
  7. package/dist-web/index.html +19 -0
  8. package/out-server/main/kiroAuthSync.js +249 -0
  9. package/out-server/main/kproxy/certManager.js +262 -0
  10. package/out-server/main/kproxy/index.js +254 -0
  11. package/out-server/main/kproxy/mitmProxy.js +475 -0
  12. package/out-server/main/kproxy/types.js +23 -0
  13. package/out-server/main/proxy/accountPool.js +543 -0
  14. package/out-server/main/proxy/clientConfig.js +596 -0
  15. package/out-server/main/proxy/index.js +25 -0
  16. package/out-server/main/proxy/kiroApi.js +1996 -0
  17. package/out-server/main/proxy/logger.js +407 -0
  18. package/out-server/main/proxy/modelCatalog.js +75 -0
  19. package/out-server/main/proxy/promptCacheTracker.js +301 -0
  20. package/out-server/main/proxy/proxyServer.js +3543 -0
  21. package/out-server/main/proxy/selfSignedCert.js +179 -0
  22. package/out-server/main/proxy/systemProxy.js +250 -0
  23. package/out-server/main/proxy/tokenCounter.js +164 -0
  24. package/out-server/main/proxy/toolNameRegistry.js +57 -0
  25. package/out-server/main/proxy/translator.js +1084 -0
  26. package/out-server/main/proxy/types.js +3 -0
  27. package/out-server/main/registration/browser-identity.js +184 -0
  28. package/out-server/main/registration/chainProxy.js +349 -0
  29. package/out-server/main/registration/config.js +58 -0
  30. package/out-server/main/registration/email-service.js +801 -0
  31. package/out-server/main/registration/fingerprint.js +352 -0
  32. package/out-server/main/registration/http-utils.js +148 -0
  33. package/out-server/main/registration/jwe.js +74 -0
  34. package/out-server/main/registration/names.js +142 -0
  35. package/out-server/main/registration/proton-mail-window.js +339 -0
  36. package/out-server/main/registration/registrar.js +1715 -0
  37. package/out-server/main/registration/tlsClientPool.js +70 -0
  38. package/out-server/main/registration/xxtea.js +161 -0
  39. package/out-server/main/runtimePaths.js +19 -0
  40. package/out-server/main/utils/redact.js +95 -0
  41. package/out-server/server/index.js +1272 -0
  42. package/out-server/server/services/accountExtras.js +105 -0
  43. package/out-server/server/services/accountProfileHydration.js +95 -0
  44. package/out-server/server/services/authFlows.js +509 -0
  45. package/out-server/server/services/dashboardTunnel.js +315 -0
  46. package/out-server/server/services/diagnostics.js +326 -0
  47. package/out-server/server/services/kiroAccounts.js +431 -0
  48. package/out-server/server/services/kiroSettings.js +260 -0
  49. package/out-server/server/services/kproxyRuntime.js +264 -0
  50. package/out-server/server/services/localKiroCredentials.js +320 -0
  51. package/out-server/server/services/machineIdRuntime.js +327 -0
  52. package/out-server/server/services/protonBrowserRuntime.js +724 -0
  53. package/out-server/server/services/proxyRuntime.js +523 -0
  54. package/out-server/server/services/registrationRuntime.js +106 -0
  55. package/out-server/server/store.js +266 -0
  56. package/package.json +113 -0
  57. package/resources/tls-client-xgo-1.14.0-windows-amd64.dll +0 -0
  58. package/scripts/kiro-manager-cli.cjs +3 -0
  59. package/scripts/krouter-cli.cjs +509 -0
  60. package/src/renderer/src/assets/krouter-logo.svg +11 -0
  61. package/src/renderer/src/assets/krouter-mark.svg +9 -0
@@ -0,0 +1,543 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AccountPool = exports.ErrorType = void 0;
4
+ exports.classifyError = classifyError;
5
+ exports.isBillingOrQuotaError = isBillingOrQuotaError;
6
+ exports.isThrottleError = isThrottleError;
7
+ // 错误类型分类(决定 failover 策略)
8
+ var ErrorType;
9
+ (function (ErrorType) {
10
+ ErrorType["FATAL"] = "fatal";
11
+ ErrorType["RECOVERABLE"] = "recoverable"; // 账号问题 → 切换到下一个账号
12
+ })(ErrorType || (exports.ErrorType = ErrorType = {}));
13
+ // 根据 HTTP 状态码和错误原因分类错误
14
+ function classifyError(statusCode, reason) {
15
+ if (reason && (isThrottleError(reason) || isBillingOrQuotaError(reason)))
16
+ return ErrorType.RECOVERABLE;
17
+ // RECOVERABLE: 配额/计费问题
18
+ if (statusCode === 402)
19
+ return ErrorType.RECOVERABLE;
20
+ // RECOVERABLE: Token 过期/无效
21
+ if (statusCode === 403)
22
+ return ErrorType.RECOVERABLE;
23
+ // RECOVERABLE: 限流
24
+ if (statusCode === 429)
25
+ return ErrorType.RECOVERABLE;
26
+ // 400: 根据原因细分
27
+ if (statusCode === 400) {
28
+ // 上下文超限 → 所有账号都会失败
29
+ if (reason === 'CONTENT_LENGTH_EXCEEDS_THRESHOLD')
30
+ return ErrorType.FATAL;
31
+ return ErrorType.FATAL;
32
+ }
33
+ // 422: 请求格式错误
34
+ if (statusCode === 422)
35
+ return ErrorType.FATAL;
36
+ // 5xx: 服务端错误
37
+ if (statusCode >= 500)
38
+ return ErrorType.FATAL;
39
+ return ErrorType.FATAL;
40
+ }
41
+ /** Account-specific billing/quota failures that should immediately fail over. */
42
+ function isBillingOrQuotaError(message) {
43
+ if (isEndpointRateLimitError(message))
44
+ return false;
45
+ return /\b402\b|payment required|billing (?:error|issue|problem)|out of credits?|run out of credits?|insufficient (?:credits?|balance)|credit balance|no (?:remaining )?credits?|credits? (?:exhausted|depleted)|quota (?:exhausted|exceeded|reached)|servicequotaexceededexception|service quota exceeded|reached (?:the|your) (?:usage )?limit|usage limit (?:reached|exceeded)|monthly limit (?:reached|exceeded)/i.test(message);
46
+ }
47
+ /** Temporary account/endpoint throttling that should use a short cooldown. */
48
+ function isThrottleError(message) {
49
+ return isEndpointRateLimitError(message) || /\b429\b|throttl|too many requests|rate[ _-]?limit/i.test(message);
50
+ }
51
+ function isEndpointRateLimitError(message) {
52
+ return /quota exhausted on (?:amazonq|codewhisperer|amazonqcli)|endpoint .*rate[ _-]?limited/i.test(message);
53
+ }
54
+ const DEFAULT_CONFIG = {
55
+ baseCooldownMs: 60000, // 60s 基础冷却
56
+ maxBackoffMultiplier: 1440, // 最大 1440 倍 = 24h
57
+ quotaResetMs: 3600000, // 1h 配额重置
58
+ probabilisticRetryChance: 0.1 // 10% 概率重试
59
+ };
60
+ class AccountPool {
61
+ accounts = new Map();
62
+ accountStats = new Map();
63
+ currentIndex = 0;
64
+ config;
65
+ // 默认 round-robin: 选中账号时立即前进,避免并发请求集中到同一账号
66
+ // sticky: 一个账号成功就粘住 (保留 prompt cache 命中)
67
+ strategy = 'round-robin';
68
+ constructor(config = {}) {
69
+ this.config = { ...DEFAULT_CONFIG, ...config };
70
+ }
71
+ // 切换账号选择策略
72
+ setStrategy(strategy) {
73
+ if (this.strategy !== strategy) {
74
+ console.log(`[AccountPool] Strategy changed: ${this.strategy} → ${strategy}`);
75
+ this.strategy = strategy;
76
+ }
77
+ }
78
+ getStrategy() {
79
+ return this.strategy;
80
+ }
81
+ // 添加账号
82
+ // 如果传入的 account 已带 suspended 字段(启动复原场景),保留其 suspended 状态
83
+ addAccount(account) {
84
+ const suspended = this.isSuspended(account);
85
+ this.accounts.set(account.id, {
86
+ ...account,
87
+ isAvailable: suspended ? false : account.isAvailable ?? true,
88
+ requestCount: account.requestCount ?? 0,
89
+ errorCount: account.errorCount ?? 0,
90
+ lastUsed: account.lastUsed ?? 0
91
+ });
92
+ this.accountStats.set(account.id, {
93
+ requests: 0,
94
+ tokens: 0,
95
+ inputTokens: 0,
96
+ outputTokens: 0,
97
+ errors: 0,
98
+ lastUsed: 0,
99
+ avgResponseTime: 0,
100
+ totalResponseTime: 0
101
+ });
102
+ if (suspended) {
103
+ console.warn(`[AccountPool] Added SUSPENDED account: ${account.email || account.id} (${account.suspendReason})`);
104
+ }
105
+ else {
106
+ console.log(`[AccountPool] Added account: ${account.email || account.id}`);
107
+ }
108
+ }
109
+ // 移除账号
110
+ removeAccount(accountId) {
111
+ this.accounts.delete(accountId);
112
+ this.accountStats.delete(accountId);
113
+ console.log(`[AccountPool] Removed account: ${accountId}`);
114
+ }
115
+ // 更新账号
116
+ updateAccount(accountId, updates) {
117
+ const account = this.accounts.get(accountId);
118
+ if (account) {
119
+ this.accounts.set(accountId, { ...account, ...updates });
120
+ }
121
+ }
122
+ // 获取下一个可用账号(粘滞 + 断路器 + 指数退避 + 概率重试)
123
+ getNextAccount(excludeIds) {
124
+ const accountList = Array.from(this.accounts.values());
125
+ if (accountList.length === 0) {
126
+ return null;
127
+ }
128
+ // 单账号特殊处理:绕过断路器,直接返回(让用户看到真实 API 错误)
129
+ if (accountList.length === 1) {
130
+ const account = accountList[0];
131
+ if (excludeIds?.has(account.id))
132
+ return null;
133
+ return account;
134
+ }
135
+ const now = Date.now();
136
+ if (this.strategy === 'least-used') {
137
+ return this.getLeastUsedAccount(accountList, now, excludeIds);
138
+ }
139
+ // 从当前粘滞索引开始遍历所有账号
140
+ const startIndex = this.currentIndex;
141
+ for (let i = 0; i < accountList.length; i++) {
142
+ const idx = (startIndex + i) % accountList.length;
143
+ const account = accountList[idx];
144
+ // 跳过当前请求已试过的账号
145
+ if (excludeIds?.has(account.id))
146
+ continue;
147
+ // 检查账号是否可用(含断路器状态)
148
+ if (this.isAccountAvailable(account, now)) {
149
+ this.reserveSelection(idx, accountList.length);
150
+ return account;
151
+ }
152
+ }
153
+ // 没有可用账号:检查是否全部因配额耗尽
154
+ const candidates = excludeIds
155
+ ? accountList.filter(a => !excludeIds.has(a.id))
156
+ : accountList;
157
+ const allExhausted = candidates.length > 0 && candidates.every(a => this.isQuotaExhausted(a, now));
158
+ if (allExhausted) {
159
+ console.log(`[AccountPool] All ${candidates.length} accounts quota exhausted, no fallback available`);
160
+ return null;
161
+ }
162
+ return null;
163
+ }
164
+ // 获取特定账号
165
+ getAccount(accountId) {
166
+ return this.accounts.get(accountId) || null;
167
+ }
168
+ // 获取下一个可用账号(排除指定账号;支持单 ID 或 ID 集合)
169
+ // 集合形式用于「请求级累计已试账号」,避免重试时循环命中已经失败过的账号
170
+ getNextAvailableAccount(exclude) {
171
+ const excludeSet = typeof exclude === 'string' ? new Set([exclude]) : exclude;
172
+ const accountList = Array.from(this.accounts.values());
173
+ if (accountList.length === 0)
174
+ return null;
175
+ const now = Date.now();
176
+ if (this.strategy === 'least-used') {
177
+ return this.getLeastUsedAccount(accountList, now, excludeSet);
178
+ }
179
+ // 从轮询指针开始找,failover 也均匀分配到健康账号。
180
+ const startIndex = this.currentIndex;
181
+ for (let i = 0; i < accountList.length; i++) {
182
+ const idx = (startIndex + i) % accountList.length;
183
+ const account = accountList[idx];
184
+ if (!excludeSet.has(account.id) && this.isAccountAvailable(account, now)) {
185
+ this.reserveSelection(idx, accountList.length);
186
+ return account;
187
+ }
188
+ }
189
+ return null;
190
+ }
191
+ // 获取所有账号
192
+ getAllAccounts() {
193
+ return Array.from(this.accounts.values());
194
+ }
195
+ // 检查账号是否可用(断路器 + 指数退避 + 概率重试)
196
+ isAccountAvailable(account, now = Date.now()) {
197
+ // 检查是否被 Kiro 后端封禁(需人工解封)
198
+ if (this.isSuspended(account)) {
199
+ return false;
200
+ }
201
+ // 检查配额是否耗尽
202
+ if (this.isQuotaExhausted(account, now)) {
203
+ return false;
204
+ }
205
+ // 检查 token 是否过期
206
+ // - 无 refreshToken 时直接判为不可用(无法刷新)
207
+ // - 有 refreshToken 时让账号通过 —— proxyServer.getAvailableAccount 会检测
208
+ // isTokenExpiringSoon 并主动调用 refreshToken;若刷新失败会通过 markNeedsRefresh
209
+ // 设置 isAvailable=false,下次循环再被本函数 line 210 跳过,形成闭环
210
+ if (account.expiresAt && account.expiresAt < now && !account.refreshToken) {
211
+ return false;
212
+ }
213
+ if (account.isAvailable === false) {
214
+ return false;
215
+ }
216
+ if (account.cooldownUntil && account.cooldownUntil > now) {
217
+ return false;
218
+ }
219
+ // 断路器检查:指数退避 + 概率重试
220
+ const failures = account.errorCount || 0;
221
+ if (failures > 0 && account.lastUsed) {
222
+ const timeSinceFailure = now - account.lastUsed;
223
+ // 指数退避:base * 2^(failures-1),封顶为 maxBackoffMultiplier
224
+ const backoffMultiplier = Math.min(Math.pow(2, failures - 1), this.config.maxBackoffMultiplier);
225
+ const effectiveCooldown = account.lastErrorStatus === 429
226
+ ? Math.min(2_000 * backoffMultiplier, 5 * 60_000)
227
+ : this.config.baseCooldownMs * backoffMultiplier;
228
+ if (timeSinceFailure < effectiveCooldown) {
229
+ // 未超出冷却期,用概率重试
230
+ if (Math.random() > this.config.probabilisticRetryChance) {
231
+ return false;
232
+ }
233
+ console.log(`[AccountPool] Probabilistic retry for ${account.email || account.id} (failures=${failures}, cooldown=${Math.round(effectiveCooldown / 1000)}s)`);
234
+ }
235
+ // else: 冷却期已过,Half-Open 状态,允许重试
236
+ }
237
+ return true;
238
+ }
239
+ // 检查账号是否被长期封禁(TEMPORARILY_SUSPENDED / AccountSuspendedException 等风控触发)
240
+ // 不同于临时 errorCount 冷却,需要人工解封或调用 clearSuspended
241
+ isSuspended(account) {
242
+ return typeof account.suspendedAt === 'number' && account.suspendedAt > 0;
243
+ }
244
+ // 标记账号为被封禁状态,账号池会持续跳过该账号直到 clearSuspended
245
+ markSuspended(accountId, reason, message) {
246
+ const account = this.accounts.get(accountId);
247
+ if (!account)
248
+ return false;
249
+ if (this.isSuspended(account) && account.suspendReason === reason) {
250
+ // 已标记过同样原因,不重复记录
251
+ return false;
252
+ }
253
+ this.accounts.set(accountId, {
254
+ ...account,
255
+ suspendedAt: Date.now(),
256
+ suspendReason: reason,
257
+ suspendMessage: message,
258
+ isAvailable: false
259
+ });
260
+ console.warn(`[AccountPool] Account ${account.email || accountId} SUSPENDED (${reason})`);
261
+ return true;
262
+ }
263
+ // 解除账号封禁标记(供手动重置或检测到被解封后调用)
264
+ clearSuspended(accountId) {
265
+ const account = this.accounts.get(accountId);
266
+ if (!account || !this.isSuspended(account))
267
+ return;
268
+ this.accounts.set(accountId, {
269
+ ...account,
270
+ suspendedAt: undefined,
271
+ suspendReason: undefined,
272
+ suspendMessage: undefined,
273
+ isAvailable: true,
274
+ errorCount: 0,
275
+ lastErrorStatus: undefined
276
+ });
277
+ console.log(`[AccountPool] Account ${account.email || accountId} unsuspended`);
278
+ }
279
+ // 检查账号配额是否耗尽
280
+ isQuotaExhausted(account, now = Date.now()) {
281
+ // 如果配额已重置(过了重置时间),不再视为耗尽
282
+ if (account.quotaResetAt && account.quotaResetAt <= now) {
283
+ return false;
284
+ }
285
+ // 有明确的耗尽标记
286
+ if (account.quotaExhaustedAt && account.quotaExhaustedAt > 0) {
287
+ return true;
288
+ }
289
+ // 有配额数据且已用尽
290
+ if (account.quotaLimit && account.quotaLimit > 0 && (account.quotaUsed ?? 0) >= account.quotaLimit) {
291
+ return true;
292
+ }
293
+ return false;
294
+ }
295
+ reserveSelection(selectedIndex, accountCount) {
296
+ if (this.strategy === 'round-robin' && accountCount > 0) {
297
+ this.currentIndex = (selectedIndex + 1) % accountCount;
298
+ }
299
+ }
300
+ getLeastUsedAccount(accountList, now, excludeIds) {
301
+ let best = null;
302
+ for (const account of accountList) {
303
+ if (excludeIds?.has(account.id))
304
+ continue;
305
+ if (!this.isAccountAvailable(account, now))
306
+ continue;
307
+ if (!best) {
308
+ best = account;
309
+ continue;
310
+ }
311
+ const accountRequests = account.requestCount || 0;
312
+ const bestRequests = best.requestCount || 0;
313
+ if (accountRequests < bestRequests) {
314
+ best = account;
315
+ }
316
+ else if (accountRequests === bestRequests && (account.lastUsed || 0) < (best.lastUsed || 0)) {
317
+ best = account;
318
+ }
319
+ }
320
+ if (best) {
321
+ this.accounts.set(best.id, { ...best, lastUsed: now });
322
+ }
323
+ return best;
324
+ }
325
+ // 记录请求成功(重置断路器 + 粘滞到当前账号)
326
+ recordSuccess(accountId, tokens = 0) {
327
+ const account = this.accounts.get(accountId);
328
+ if (account) {
329
+ this.accounts.set(accountId, {
330
+ ...account,
331
+ requestCount: (account.requestCount || 0) + 1,
332
+ errorCount: 0, // 重置断路器失败计数
333
+ lastErrorStatus: undefined,
334
+ lastUsed: Date.now(),
335
+ isAvailable: true,
336
+ cooldownUntil: undefined,
337
+ quotaExhaustedAt: undefined
338
+ });
339
+ const accountList = Array.from(this.accounts.keys());
340
+ const successIndex = accountList.indexOf(accountId);
341
+ if (successIndex >= 0 && accountList.length > 0) {
342
+ if (this.strategy === 'sticky') {
343
+ // 粘滞: 成功后将全局索引固定在这个账号 (保留 prompt cache 命中)
344
+ this.currentIndex = successIndex;
345
+ }
346
+ }
347
+ }
348
+ const stats = this.accountStats.get(accountId);
349
+ if (stats) {
350
+ this.accountStats.set(accountId, {
351
+ ...stats,
352
+ requests: stats.requests + 1,
353
+ tokens: stats.tokens + tokens,
354
+ lastUsed: Date.now()
355
+ });
356
+ }
357
+ }
358
+ // 记录请求失败(区分错误类型)
359
+ recordError(accountId, errorType = ErrorType.RECOVERABLE, statusCode) {
360
+ const account = this.accounts.get(accountId);
361
+ if (!account)
362
+ return;
363
+ const now = Date.now();
364
+ const stats = this.accountStats.get(accountId);
365
+ if (stats) {
366
+ this.accountStats.set(accountId, { ...stats, errors: stats.errors + 1, lastUsed: now });
367
+ }
368
+ // FATAL 错误不增加失败计数(是请求的问题,不是账号的问题)
369
+ if (errorType === ErrorType.FATAL)
370
+ return;
371
+ // RECOVERABLE: 增加失败计数,断路器指数退避自动生效
372
+ const errorCount = (account.errorCount || 0) + 1;
373
+ let quotaExhaustedAt = account.quotaExhaustedAt;
374
+ // 402 表示账号配额/计费耗尽;429 只做短期节流冷却。
375
+ const isQuotaError = statusCode === 402;
376
+ if (isQuotaError) {
377
+ quotaExhaustedAt = now;
378
+ }
379
+ // 计算当前退避时间用于日志
380
+ const backoffMultiplier = Math.min(Math.pow(2, errorCount - 1), this.config.maxBackoffMultiplier);
381
+ const effectiveCooldown = statusCode === 429
382
+ ? Math.min(2_000 * backoffMultiplier, 5 * 60_000)
383
+ : this.config.baseCooldownMs * backoffMultiplier;
384
+ const cooldownStr = effectiveCooldown < 60000 ? `${Math.round(effectiveCooldown / 1000)}s`
385
+ : effectiveCooldown < 3600000 ? `${Math.round(effectiveCooldown / 60000)}m`
386
+ : `${Math.round(effectiveCooldown / 3600000)}h`;
387
+ console.log(`[AccountPool] Account ${account.email || accountId} failure #${errorCount}: status=${statusCode || '?'}, cooldown=${cooldownStr}`);
388
+ this.accounts.set(accountId, {
389
+ ...account,
390
+ errorCount,
391
+ lastErrorStatus: statusCode,
392
+ quotaExhaustedAt,
393
+ quotaResetAt: isQuotaError
394
+ ? (account.quotaResetAt && account.quotaResetAt > now ? account.quotaResetAt : now + this.config.quotaResetMs)
395
+ : account.quotaResetAt,
396
+ cooldownUntil: isQuotaError ? undefined : now + effectiveCooldown,
397
+ lastUsed: now
398
+ });
399
+ }
400
+ /** Replace credentials/config while preserving runtime health and quota state. */
401
+ replaceAccounts(accounts) {
402
+ const previousAccounts = this.accounts;
403
+ const previousStats = this.accountStats;
404
+ this.accounts = new Map();
405
+ this.accountStats = new Map();
406
+ for (const account of accounts) {
407
+ const previous = previousAccounts.get(account.id);
408
+ this.addAccount(previous ? {
409
+ ...account,
410
+ requestCount: previous.requestCount,
411
+ errorCount: previous.errorCount,
412
+ lastErrorStatus: previous.lastErrorStatus,
413
+ lastUsed: previous.lastUsed,
414
+ isAvailable: previous.isAvailable,
415
+ cooldownUntil: previous.cooldownUntil,
416
+ quotaUsed: previous.quotaUsed,
417
+ quotaLimit: previous.quotaLimit,
418
+ quotaExhaustedAt: previous.quotaExhaustedAt,
419
+ quotaResetAt: previous.quotaResetAt,
420
+ suspendedAt: previous.suspendedAt,
421
+ suspendReason: previous.suspendReason,
422
+ suspendMessage: previous.suspendMessage
423
+ } : account);
424
+ const stats = previousStats.get(account.id);
425
+ if (stats)
426
+ this.accountStats.set(account.id, stats);
427
+ }
428
+ this.currentIndex = this.accounts.size > 0 ? this.currentIndex % this.accounts.size : 0;
429
+ }
430
+ markQuotaExhausted(accountId) {
431
+ this.recordError(accountId, ErrorType.RECOVERABLE, 402);
432
+ }
433
+ // 更新账号配额信息
434
+ updateQuota(accountId, used, limit, resetAt) {
435
+ const account = this.accounts.get(accountId);
436
+ if (!account)
437
+ return;
438
+ const wasExhausted = this.isQuotaExhausted(account);
439
+ this.accounts.set(accountId, {
440
+ ...account,
441
+ quotaUsed: used,
442
+ quotaLimit: limit,
443
+ quotaResetAt: resetAt,
444
+ // 如果配额从耗尽恢复,清除耗尽标记
445
+ quotaExhaustedAt: (used < limit) ? undefined : account.quotaExhaustedAt,
446
+ lastErrorStatus: (used < limit && account.lastErrorStatus === 402) ? undefined : account.lastErrorStatus
447
+ });
448
+ if (!wasExhausted && used >= limit) {
449
+ console.log(`[AccountPool] Account ${account.email || accountId} quota reached: ${used}/${limit}`);
450
+ }
451
+ else if (wasExhausted && used < limit) {
452
+ console.log(`[AccountPool] Account ${account.email || accountId} quota recovered: ${used}/${limit}`);
453
+ }
454
+ }
455
+ // 获取配额状态摘要
456
+ getQuotaStatus() {
457
+ const now = Date.now();
458
+ const all = Array.from(this.accounts.values());
459
+ let available = 0;
460
+ let exhausted = 0;
461
+ let cooldown = 0;
462
+ for (const account of all) {
463
+ if (this.isQuotaExhausted(account, now)) {
464
+ exhausted++;
465
+ }
466
+ else if (account.cooldownUntil && account.cooldownUntil > now) {
467
+ cooldown++;
468
+ }
469
+ else if (this.isAccountAvailable(account, now)) {
470
+ available++;
471
+ }
472
+ }
473
+ return { total: all.length, available, exhausted, cooldown };
474
+ }
475
+ // 标记账号需要刷新 Token
476
+ markNeedsRefresh(accountId) {
477
+ const account = this.accounts.get(accountId);
478
+ if (account) {
479
+ this.accounts.set(accountId, {
480
+ ...account,
481
+ isAvailable: false
482
+ });
483
+ }
484
+ }
485
+ // 获取统计信息
486
+ getStats() {
487
+ let totalRequests = 0;
488
+ let totalTokens = 0;
489
+ let totalErrors = 0;
490
+ for (const stats of this.accountStats.values()) {
491
+ totalRequests += stats.requests;
492
+ totalTokens += stats.tokens;
493
+ totalErrors += stats.errors;
494
+ }
495
+ return {
496
+ accounts: new Map(this.accountStats),
497
+ total: {
498
+ requests: totalRequests,
499
+ tokens: totalTokens,
500
+ errors: totalErrors
501
+ }
502
+ };
503
+ }
504
+ // 重置所有账号状态(含封禁标记 — 手动重置表示用户已确认可用)
505
+ reset() {
506
+ for (const [id, account] of this.accounts) {
507
+ this.accounts.set(id, {
508
+ ...account,
509
+ isAvailable: true,
510
+ errorCount: 0,
511
+ lastErrorStatus: undefined,
512
+ cooldownUntil: undefined,
513
+ quotaExhaustedAt: undefined,
514
+ suspendedAt: undefined,
515
+ suspendReason: undefined,
516
+ suspendMessage: undefined
517
+ });
518
+ }
519
+ this.currentIndex = 0;
520
+ }
521
+ // 清空所有账号
522
+ clear() {
523
+ this.accounts.clear();
524
+ this.accountStats.clear();
525
+ this.currentIndex = 0;
526
+ }
527
+ // 获取账号数量
528
+ get size() {
529
+ return this.accounts.size;
530
+ }
531
+ // 获取可用账号数量
532
+ get availableCount() {
533
+ const now = Date.now();
534
+ let count = 0;
535
+ for (const account of this.accounts.values()) {
536
+ if (this.isAccountAvailable(account, now)) {
537
+ count++;
538
+ }
539
+ }
540
+ return count;
541
+ }
542
+ }
543
+ exports.AccountPool = AccountPool;