qwenproxy-cli 1.5.3 β†’ 1.6.1

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.
@@ -18,6 +18,7 @@ import {
18
18
  setToolCapNotice,
19
19
  } from "../../services/qwen.ts";
20
20
  import { acquireUpstreamStream } from "./account.ts";
21
+ import { loadAccounts } from "../../core/accounts.ts";
21
22
  import { markAccountRateLimited, computeQuotaCooldownMs } from "../../core/account-manager.ts";
22
23
  import {
23
24
  clearTemporaryBusy,
@@ -60,6 +61,7 @@ import {
60
61
  logTokenEstimationSample,
61
62
  type TokenEstimationContext,
62
63
  } from "../../services/token-estimation-metrics.ts";
64
+ import { recordTurnUsage } from "../../core/daily-usage.ts";
63
65
  import {
64
66
  enrichUsageWithContextMeter,
65
67
  getContextMeterHeaders,
@@ -781,6 +783,10 @@ export async function processNonStreamingResponse(
781
783
  context: currentTokenEstimationContext,
782
784
  });
783
785
 
786
+ if (activeAccountId && usage) {
787
+ recordTurnUsage(activeAccountId, usage, body.model);
788
+ }
789
+
784
790
  // The response was fully processed: persist the next-turn parent.
785
791
  if (pendingParentId) {
786
792
  rememberParent(pendingParentId);
@@ -900,6 +906,9 @@ export async function processStreamingResponse(
900
906
  let activeReader: ReadableStreamDefaultReader<Uint8Array> | null = null;
901
907
  let invalidInputSameAccountRetries = 0;
902
908
  let chatInProgressSameAccountRetries = 0;
909
+ let antiBotSameAccountRetries = 0;
910
+ const totalConfiguredAccounts = loadAccounts().length;
911
+ let quotaRotationsLeft = Math.min(Math.max(1, totalConfiguredAccounts - 1), 6);
903
912
  // Set when the terminal [DONE] reached the client. The `| recovered`
904
913
  // suffix on Stream done must only appear for attempts that COMPLETED after
905
914
  // a mid-stream retry, not for failed attempts that consumed retries.
@@ -1372,7 +1381,6 @@ export async function processStreamingResponse(
1372
1381
  !normalizedError ||
1373
1382
  (clientDisconnected && !gracePending) ||
1374
1383
  c.req.raw.signal.aborted ||
1375
- retryContext.retriesLeft <= 0 ||
1376
1384
  !midStreamRetry ||
1377
1385
  emittedModelOutput
1378
1386
  ) {
@@ -1384,6 +1392,19 @@ export async function processStreamingResponse(
1384
1392
  });
1385
1393
  if (!policy.retryable) return false;
1386
1394
 
1395
+ const isRealQuotaError =
1396
+ policy.reason === "quota_or_rate_limit" &&
1397
+ (policy.accountCooldownReason === "RateLimited" ||
1398
+ policy.accountCooldownReason === "QuotaExceeded");
1399
+
1400
+ const hasBudget = isRealQuotaError
1401
+ ? quotaRotationsLeft > 0
1402
+ : retryContext.retriesLeft > 0;
1403
+
1404
+ if (!hasBudget) {
1405
+ return false;
1406
+ }
1407
+
1387
1408
  // Full recovery decision β€” same rationale as the create-path policy
1388
1409
  // log: the failure line shows the error, this line shows WHY the
1389
1410
  // chosen action (retry same / switch / new chat / cooldown) was taken.
@@ -1397,9 +1418,13 @@ export async function processStreamingResponse(
1397
1418
  invalidateLogicalThreadParent(midStreamRetry.sessionId);
1398
1419
  }
1399
1420
 
1400
- retryContext.retriesLeft--;
1421
+ if (isRealQuotaError) {
1422
+ quotaRotationsLeft--;
1423
+ } else {
1424
+ retryContext.retriesLeft--;
1425
+ }
1401
1426
  console.warn(
1402
- `πŸ”„ [Chat] Stream recovery | account=${currentAccountId} | reason=${policy.reason} | error=${normalizedError.message.substring(0, 150)} | retries_left=${retryContext.retriesLeft}`,
1427
+ `πŸ”„ [Chat] Stream recovery | account=${currentAccountId} | reason=${policy.reason} | error=${normalizedError.message.substring(0, 150)} | retries_left=${isRealQuotaError ? quotaRotationsLeft : retryContext.retriesLeft}`,
1403
1428
  );
1404
1429
 
1405
1430
  const retryInvalidInputOnSameAccount =
@@ -1432,9 +1457,19 @@ export async function processStreamingResponse(
1432
1457
  );
1433
1458
  return false;
1434
1459
  }
1435
- const switchAccount =
1460
+ let switchAccount =
1436
1461
  policy.switchAccount && !retryInvalidInputOnSameAccount;
1437
1462
 
1463
+ if (policy.reason === "anti_bot") {
1464
+ if (antiBotSameAccountRetries === 0) {
1465
+ antiBotSameAccountRetries++;
1466
+ switchAccount = false;
1467
+ } else {
1468
+ switchAccount = true;
1469
+ markAccountRateLimited(currentAccountId, 60_000, "AntiBotWafChallenge");
1470
+ }
1471
+ }
1472
+
1438
1473
  if (
1439
1474
  switchAccount &&
1440
1475
  (policy.accountCooldownMs ?? 0) > 0
@@ -1452,6 +1487,35 @@ export async function processStreamingResponse(
1452
1487
  .catch(() => {});
1453
1488
  }
1454
1489
 
1490
+ // Preventative stop: signal Qwen to stop generating on the backend before switching
1491
+ if (currentUiSessionId && targetResponseId) {
1492
+ const streamData = getStream(completionId);
1493
+ const stopHeaders = streamData?.headers;
1494
+ if (stopHeaders?.cookie && stopHeaders["user-agent"]) {
1495
+ void requestQwenTextInBrowser(
1496
+ currentAccountId,
1497
+ "POST",
1498
+ `/api/v2/chat/completions/stop?chat_id=${encodeURIComponent(currentUiSessionId)}`,
1499
+ buildQwenRequestHeaders({
1500
+ cookie: stopHeaders.cookie,
1501
+ userAgent: stopHeaders["user-agent"],
1502
+ bxUa: stopHeaders["bx-ua"],
1503
+ bxUmidtoken: stopHeaders["bx-umidtoken"],
1504
+ bxV: stopHeaders["bx-v"],
1505
+ chatSessionId: currentUiSessionId,
1506
+ }),
1507
+ JSON.stringify({
1508
+ chat_id: currentUiSessionId,
1509
+ response_id: targetResponseId,
1510
+ }),
1511
+ {
1512
+ referrer: qwenUrl(`/c/${encodeURIComponent(currentUiSessionId)}`),
1513
+ noMutexRecovery: true,
1514
+ },
1515
+ ).catch(() => undefined);
1516
+ }
1517
+ }
1518
+
1455
1519
  retryContext.releaseAccountLease?.();
1456
1520
  retryContext.releaseAccountLease = null;
1457
1521
  await reader.cancel().catch(() => undefined);
@@ -2566,6 +2630,10 @@ export async function processStreamingResponse(
2566
2630
  mode: "stream",
2567
2631
  context: currentTokenEstimationContext,
2568
2632
  });
2633
+
2634
+ if (activeAccountId && usage) {
2635
+ recordTurnUsage(activeAccountId, usage, body.model);
2636
+ }
2569
2637
  } else {
2570
2638
  if (isToolcallDebugEnabled()) {
2571
2639
  logger.debug(
@@ -21,6 +21,11 @@ const CHALLENGE_PATH_MARKER = "_____tmd_____";
21
21
  */
22
22
  const FAILED_RECOVERY_BACKOFF_MS = 30_000;
23
23
  const lastFailedRecoveryAt = new Map<string, number>();
24
+ const activeCaptchaRecoveries = new Set<string>();
25
+
26
+ export function isCaptchaRecoveryActive(accountId: string): boolean {
27
+ return activeCaptchaRecoveries.has(accountId);
28
+ }
24
29
 
25
30
  async function gotoBestEffort(page: Page, url: string): Promise<void> {
26
31
  // A WAF-blocked navigation can time out while still having rendered the
@@ -105,13 +110,6 @@ export async function recoverBaxiaCaptcha(
105
110
 
106
111
  metrics.increment("captcha.challenges.detected", 1, { solver });
107
112
 
108
- const challengeUrl = options.challengeBody
109
- ? extractBaxiaChallengeUrl(options.challengeBody, config.qwen.baseUrl)
110
- : null;
111
-
112
- // The slider itself waits up to 5s for each attempt. Keep the page
113
- // operation alive for the full bounded solver budget so a slow challenge
114
- // cannot be mistaken for a stuck browser. Do not clamp to timeouts.page.
115
113
  // Two navigations (open the challenge, return to the chat page) are part of
116
114
  // the recovery, so their budget belongs in the same total.
117
115
  const solverOperationTimeoutMs = Math.max(
@@ -122,6 +120,11 @@ export async function recoverBaxiaCaptcha(
122
120
  2 * CHALLENGE_NAVIGATION_TIMEOUT_MS,
123
121
  );
124
122
 
123
+ const challengeUrl = options.challengeBody
124
+ ? extractBaxiaChallengeUrl(options.challengeBody, config.qwen.baseUrl)
125
+ : null;
126
+
127
+ activeCaptchaRecoveries.add(accountId);
125
128
  try {
126
129
  // recoverOnTimeout: false ensures that if the captcha solve times out,
127
130
  // withAccountPage does NOT destructively kill the browser context, which
@@ -160,5 +163,7 @@ export async function recoverBaxiaCaptcha(
160
163
  detail,
161
164
  });
162
165
  return false;
166
+ } finally {
167
+ activeCaptchaRecoveries.delete(accountId);
163
168
  }
164
169
  }
@@ -108,6 +108,11 @@ export async function deleteChatsForConfiguredAccounts(keepBrowserOpen = false):
108
108
  `❌ [DeleteChats] [${currentIdx}/${totalCount}] Falha ao apagar conversas de ${maskEmail(account.email)}:`,
109
109
  error instanceof Error ? error.message : String(error),
110
110
  );
111
+ } finally {
112
+ if (!keepBrowserOpen) {
113
+ const { closePlaywrightForAccount } = await import("./playwright.ts");
114
+ await closePlaywrightForAccount(account.id).catch(() => {});
115
+ }
111
116
  }
112
117
  }
113
118
  } finally {
@@ -287,7 +287,7 @@ export function loadStorageState(accountId: string): string | undefined {
287
287
  export async function saveStorageState(
288
288
  context: BrowserContext,
289
289
  accountId: string,
290
- timeoutMs = 5_000,
290
+ timeoutMs = 10_000,
291
291
  ): Promise<void> {
292
292
  try {
293
293
  const stateFile = getStorageStatePath(accountId);
@@ -295,8 +295,12 @@ export async function saveStorageState(
295
295
  if (!fs.existsSync(dir)) {
296
296
  fs.mkdirSync(dir, { recursive: true });
297
297
  }
298
+ const rawPromise = context.storageState({ path: stateFile }).catch(() => {
299
+ // Swallowed on context teardown to avoid unhandled rejection if withTimeout already fired
300
+ return undefined as any;
301
+ });
298
302
  await withTimeout(
299
- context.storageState({ path: stateFile }),
303
+ rawPromise,
300
304
  timeoutMs,
301
305
  `storageState timed out after ${timeoutMs}ms`,
302
306
  );
@@ -512,6 +516,7 @@ const COOKIE_CACHE_TTL = 15 * 60 * 1000; // 15 minutes
512
516
  const cookieCaches = new Map<string, { cookie: string; timestamp: number }>();
513
517
  const lastAccountActivity = new Map<string, number>();
514
518
  const lastKeepAliveNavigation = new Map<string, number>();
519
+ const lastProactiveTokenRefresh = new Map<string, number>();
515
520
  const profileResetQueue = new Map<string, Promise<void>>();
516
521
  let profileResetChain: Promise<void> = Promise.resolve();
517
522
  let closingAllPlaywright = false;
@@ -1228,6 +1233,11 @@ export async function getBasicHeaders(accountId: string): Promise<{
1228
1233
  throw new Error(`Playwright not initialized for account: ${accountId}`);
1229
1234
  }
1230
1235
 
1236
+ const inFlightInit = inFlightAccountInits.get(accountId);
1237
+ if (inFlightInit) {
1238
+ await inFlightInit.catch(() => {});
1239
+ }
1240
+
1231
1241
  // Acquire mutex to prevent concurrent browser access
1232
1242
  const release = await acquireAccountMutex(
1233
1243
  accountId,
@@ -3068,6 +3078,12 @@ export async function captureQwenHeaders(
3068
3078
  const { saveAuthSession } = await import("../core/database.ts");
3069
3079
  const { parseJwtExpiry } = await import("../utils/jwt.ts");
3070
3080
  const tokenExpiry = parseJwtExpiry(capturedHeaders.cookie);
3081
+ let rTok: string | undefined;
3082
+ try {
3083
+ rTok =
3084
+ (await page.evaluate(() => localStorage.getItem("refresh_token") || "")).trim() ||
3085
+ undefined;
3086
+ } catch {}
3071
3087
  saveAuthSession(accountId, {
3072
3088
  cookie: capturedHeaders.cookie,
3073
3089
  userAgent: capturedHeaders["user-agent"],
@@ -3079,6 +3095,7 @@ export async function captureQwenHeaders(
3079
3095
  secChUaPlatform: capturedHeaders["sec-ch-ua-platform"],
3080
3096
  version: capturedHeaders["version"],
3081
3097
  tokenExpiresAt: tokenExpiry || undefined,
3098
+ refreshToken: rTok,
3082
3099
  capturedAt: Date.now(),
3083
3100
  });
3084
3101
  } catch {}
@@ -3121,40 +3138,61 @@ export async function captureQwenHeaders(
3121
3138
  const isAuthUrl = currentUrl.includes("/auth") || currentUrl.includes("/login");
3122
3139
  const isSuspectedGuest = isAuthUrl || (attempt >= 2 && !(await isPageLoggedIn(page, 5000)));
3123
3140
  if (isSuspectedGuest) {
3124
- const { getAccountCredentials } = await import("../core/accounts.ts");
3125
- const creds = getAccountCredentials(accountId);
3126
- if (creds && creds.email && creds.password) {
3127
- console.warn(
3128
- `⚠️ [Playwright] Session expired or guest state detected during header capture for ${accountId}; re-authenticating...`,
3141
+ // Try fast silent token refresh first using the 30-day refresh_token in-page (~200ms)
3142
+ const silentRefresh = await refreshAccountToken(accountId);
3143
+ if (silentRefresh.success) {
3144
+ console.log(
3145
+ `⚑ [Playwright] Token refreshed silently during header capture for ${accountId}`,
3129
3146
  );
3130
- armOverallDeadline();
3131
- const ok = await loginToQwen(accountId, creds.email, creds.password);
3132
- if (ok) {
3133
- // Re-login navigated; load the chat page and wait for hydration so
3134
- // the check below probes a live authenticated chat page.
3147
+ if (isAuthUrl) {
3135
3148
  await page.goto(qwenUrl("/"), {
3136
3149
  waitUntil: "domcontentloaded",
3137
3150
  timeout: Math.min(config.timeouts.navigation, timeoutMs),
3138
3151
  });
3139
- await sleep(2000);
3152
+ await sleep(1500);
3153
+ }
3154
+ if (await isPageLoggedIn(page, 5000)) {
3155
+ armOverallDeadline();
3140
3156
  }
3141
- if (!ok || !(await isPageLoggedIn(page, 6000))) {
3157
+ }
3158
+
3159
+ const stillSuspected = !silentRefresh.success || !(await isPageLoggedIn(page, 2000));
3160
+ if (stillSuspected) {
3161
+ const { getAccountCredentials } = await import("../core/accounts.ts");
3162
+ const creds = getAccountCredentials(accountId);
3163
+ if (creds && creds.email && creds.password) {
3164
+ console.warn(
3165
+ `⚠️ [Playwright] Session expired or guest state detected during header capture for ${accountId}; re-authenticating...`,
3166
+ );
3167
+ armOverallDeadline();
3168
+ const ok = await loginToQwen(accountId, creds.email, creds.password);
3169
+ if (ok) {
3170
+ // Re-login navigated; load the chat page and wait for hydration so
3171
+ // the check below probes a live authenticated chat page.
3172
+ await page.goto(qwenUrl("/"), {
3173
+ waitUntil: "domcontentloaded",
3174
+ timeout: Math.min(config.timeouts.navigation, timeoutMs),
3175
+ });
3176
+ await sleep(2000);
3177
+ }
3178
+ if (!ok || !(await isPageLoggedIn(page, 6000))) {
3179
+ settle(
3180
+ new Error(
3181
+ `Header capture failed for ${accountId}: re-login after session expiry did not succeed`,
3182
+ ),
3183
+ );
3184
+ return;
3185
+ }
3186
+ armOverallDeadline();
3187
+ if (settled || page.isClosed()) return;
3188
+ } else {
3142
3189
  settle(
3143
3190
  new Error(
3144
- `Header capture failed for ${accountId}: re-login after session expiry did not succeed`,
3191
+ `Header capture failed for ${accountId}: session expired and no credentials available for re-login (run 'qpx login')`,
3145
3192
  ),
3146
3193
  );
3147
3194
  return;
3148
3195
  }
3149
- armOverallDeadline();
3150
- if (settled || page.isClosed()) return;
3151
- } else {
3152
- settle(
3153
- new Error(
3154
- `Header capture failed for ${accountId}: session expired and no credentials available for re-login (run 'qpx login')`,
3155
- ),
3156
- );
3157
- return;
3158
3196
  }
3159
3197
  }
3160
3198
 
@@ -3593,7 +3631,15 @@ async function refreshHeadersInternal(
3593
3631
  `[Playwright] Navigation check failed during refresh for ${accountId}:`,
3594
3632
  (navErr as Error).message,
3595
3633
  );
3596
- await executeReauth();
3634
+ if (await isPageLoggedIn(page, 3000)) {
3635
+ // Still authenticated despite navigation timeout; do not escalate to slow form re-auth
3636
+ } else {
3637
+ // Try silent in-page token refresh before executing full form login
3638
+ const refreshed = await refreshAccountToken(accountId);
3639
+ if (!refreshed.success) {
3640
+ await executeReauth();
3641
+ }
3642
+ }
3597
3643
  }
3598
3644
  }
3599
3645
 
@@ -3610,6 +3656,12 @@ async function refreshHeadersInternal(
3610
3656
  const { saveAuthSession } = await import("../core/database.ts");
3611
3657
  const tokenCookie = liveCookies.find((c) => c.name === "token");
3612
3658
  const exp = tokenCookie ? parseJwtExpiry(tokenCookie.value) : undefined;
3659
+ let rTok: string | undefined;
3660
+ try {
3661
+ rTok =
3662
+ (await page.evaluate(() => localStorage.getItem("refresh_token") || "")).trim() ||
3663
+ undefined;
3664
+ } catch {}
3613
3665
  saveAuthSession(accountId, {
3614
3666
  cookie: cookieStr,
3615
3667
  userAgent: cache.headers["user-agent"] || "",
@@ -3621,6 +3673,7 @@ async function refreshHeadersInternal(
3621
3673
  secChUaPlatform: cache.headers["sec-ch-ua-platform"] || undefined,
3622
3674
  version: cache.headers["version"] || undefined,
3623
3675
  tokenExpiresAt: exp || undefined,
3676
+ refreshToken: rTok,
3624
3677
  capturedAt: Date.now(),
3625
3678
  });
3626
3679
  } catch {}
@@ -4169,8 +4222,10 @@ export async function keepAlivePlaywrightAccount(
4169
4222
  const currentUrl = page.url();
4170
4223
 
4171
4224
  // Proactive session check: if token expires within 5 minutes, refresh in-page silently using refreshAccountToken
4225
+ const lastRefreshAttempt = lastProactiveTokenRefresh.get(accountId) ?? 0;
4172
4226
  const cookie = await getCookies(accountId);
4173
- if (cookie && isTokenExpiringSoon(cookie, 5)) {
4227
+ if (cookie && isTokenExpiringSoon(cookie, 5) && now - lastRefreshAttempt > 60_000) {
4228
+ lastProactiveTokenRefresh.set(accountId, now);
4174
4229
  console.log(
4175
4230
  `πŸ’“ [SessionKeeper] Account ${accountId} access token expiring soon (<5m); silently refreshing...`,
4176
4231
  );
@@ -4233,6 +4288,7 @@ function cleanupPlaywrightAccountState(accountId: string): void {
4233
4288
  cookieCaches.delete(accountId);
4234
4289
  lastAccountActivity.delete(accountId);
4235
4290
  lastKeepAliveNavigation.delete(accountId);
4291
+ lastProactiveTokenRefresh.delete(accountId);
4236
4292
  clearFingerprintCache(accountId);
4237
4293
  // The account's context died/closed β€” its captured headers are stale or the
4238
4294
  // page is gone, so it must not be selected by the rotation gate until a
@@ -1540,6 +1540,12 @@ async function createQwenBrowserResponse(
1540
1540
  }
1541
1541
  }
1542
1542
 
1543
+ const inFlightPersonalizationSyncs = new Map<string, Promise<boolean>>();
1544
+
1545
+ export function getInFlightPersonalizationCount(): number {
1546
+ return inFlightPersonalizationSyncs.size;
1547
+ }
1548
+
1543
1549
  export async function syncQwenRequestPersonalization(
1544
1550
  instruction: string,
1545
1551
  accountId?: string,
@@ -1552,15 +1558,67 @@ export async function syncQwenRequestPersonalization(
1552
1558
  forceSync?: boolean;
1553
1559
  } = {},
1554
1560
  ): Promise<boolean> {
1555
- if (isAuthMockEnabled()) {
1556
- // Test hook: force the sync to report "not applied" so the fail-fast
1557
- // contract (personalization-required suite) is exercisable in mock mode.
1558
- if (process.env.TEST_PERSONALIZATION_SYNC_FAIL === "true") return false;
1559
- return true;
1561
+ const cacheKey = accountId || "global";
1562
+ const sent = textSize(instruction);
1563
+ const syncHash = sent.hash ? `${sent.hash}:${QWEN_SAFE_SETTINGS_HASH}` : null;
1564
+ const inFlightKey = `${cacheKey}:${syncHash || "empty"}`;
1565
+
1566
+ const existingInFlight = inFlightPersonalizationSyncs.get(inFlightKey);
1567
+ if (existingInFlight) {
1568
+ logger.debug("[Qwen] Personalization sync already in flight, sharing promise", {
1569
+ accountId: cacheKey,
1570
+ });
1571
+ return existingInFlight;
1560
1572
  }
1561
- // instruction pode ser vazia para limpar personalization
1562
1573
 
1563
- const cacheKey = accountId || "global";
1574
+ let syncPromise!: Promise<boolean>;
1575
+ syncPromise = (async () => {
1576
+ await Promise.resolve();
1577
+ try {
1578
+ if (isAuthMockEnabled()) {
1579
+ const delay = parseInt(process.env.TEST_PERSONALIZATION_DELAY_MS || "0", 10);
1580
+ if (delay > 0) {
1581
+ await sleep(delay);
1582
+ }
1583
+ // Test hook: force the sync to report "not applied" so the fail-fast
1584
+ // contract (personalization-required suite) is exercisable in mock mode.
1585
+ if (process.env.TEST_PERSONALIZATION_SYNC_FAIL === "true") return false;
1586
+ return true;
1587
+ }
1588
+ return await syncQwenRequestPersonalizationInternal(
1589
+ instruction,
1590
+ accountId,
1591
+ metadata,
1592
+ cacheKey,
1593
+ sent,
1594
+ syncHash,
1595
+ );
1596
+ } finally {
1597
+ if (inFlightPersonalizationSyncs.get(inFlightKey) === syncPromise) {
1598
+ inFlightPersonalizationSyncs.delete(inFlightKey);
1599
+ }
1600
+ }
1601
+ })();
1602
+
1603
+ inFlightPersonalizationSyncs.set(inFlightKey, syncPromise);
1604
+ return syncPromise;
1605
+ }
1606
+
1607
+ async function syncQwenRequestPersonalizationInternal(
1608
+ instruction: string,
1609
+ accountId: string | undefined,
1610
+ metadata: {
1611
+ model?: string;
1612
+ toolsCount?: number;
1613
+ sessionId?: string | null;
1614
+ promptChars?: number;
1615
+ forceSync?: boolean;
1616
+ },
1617
+ cacheKey: string,
1618
+ sent: ReturnType<typeof textSize>,
1619
+ syncHash: string | null,
1620
+ ): Promise<boolean> {
1621
+ // instruction pode ser vazia para limpar personalization
1564
1622
 
1565
1623
  // Proactive token renewal: refresh BEFORE attempting personalization
1566
1624
  // to avoid 401 errors that waste time on retry
@@ -1584,8 +1642,6 @@ export async function syncQwenRequestPersonalization(
1584
1642
  let currentSettings: any = null;
1585
1643
  let payload = buildQwenSettingsUpdatePayload(currentSettings, instruction);
1586
1644
 
1587
- const sent = textSize(instruction);
1588
- const syncHash = sent.hash ? `${sent.hash}:${QWEN_SAFE_SETTINGS_HASH}` : null;
1589
1645
  const bypassCache = metadata.forceSync === true;
1590
1646
 
1591
1647
  // 1. Check memory cache (skipped on forceSync)
@@ -15,6 +15,10 @@ import { isPlaywrightInitialized } from "../services/playwright.ts";
15
15
  import { getAccountConcurrencySnapshot } from "../core/account-concurrency.ts";
16
16
  import { getRssUsageSnapshot } from "../core/memory-usage.ts";
17
17
  import { metrics } from "../core/metrics.ts";
18
+ import {
19
+ getAllAccountsDailyUsage,
20
+ getPoolDailyUsageSummary,
21
+ } from "../core/daily-usage.ts";
18
22
  import type { ProxyStatusSnapshot } from "./types.ts";
19
23
 
20
24
  export function maskAccountIdentifier(idOrEmail: string): string {
@@ -153,13 +157,18 @@ export async function fetchProxyStatus(): Promise<ProxyStatusSnapshot> {
153
157
  }
154
158
  } catch {}
155
159
 
156
- // Attach concurrency to accounts
160
+ // Attach concurrency and daily usage to accounts
161
+ const dailyUsageMap = getAllAccountsDailyUsage();
157
162
  const accounts = cachedAccounts.map((acc) => {
158
163
  const concurrency = concurrencyMap.get(acc.id);
164
+ const daily = dailyUsageMap.get(acc.id);
159
165
  return {
160
166
  ...acc,
161
167
  activeStreams: concurrency?.active ?? 0,
162
168
  streamLimit: concurrency?.limit ?? config.concurrency.maxStreamsPerAccount,
169
+ dailyTokens: daily?.completionTokens ?? 0,
170
+ dailyUsagePercent: daily?.usagePercent ?? 0,
171
+ dailyTurns: daily?.turnsCount ?? 0,
163
172
  };
164
173
  });
165
174
  // RAM usage
@@ -220,6 +229,7 @@ export async function fetchProxyStatus(): Promise<ProxyStatusSnapshot> {
220
229
  cacheHitRatio,
221
230
  cacheBytesSaved,
222
231
  },
232
+ dailyUsage: getPoolDailyUsageSummary(cachedAccounts.map((a) => a.id)),
223
233
  accounts,
224
234
  };
225
235
  }
package/src/tui/types.ts CHANGED
@@ -38,6 +38,14 @@ export interface ProxyStatusSnapshot {
38
38
  cacheHitRatio?: number;
39
39
  cacheBytesSaved?: number;
40
40
  };
41
+ dailyUsage?: {
42
+ poolUsagePercent: number;
43
+ totalPromptTokens: number;
44
+ totalCompletionTokens: number;
45
+ totalTokens: number;
46
+ totalTurns: number;
47
+ poolCapacityTokens: number;
48
+ };
41
49
  accounts: Array<{
42
50
  id: string;
43
51
  emailOrName: string;
@@ -50,5 +58,8 @@ export interface ProxyStatusSnapshot {
50
58
  isInitialized?: boolean;
51
59
  activeStreams?: number;
52
60
  streamLimit?: number;
61
+ dailyTokens?: number;
62
+ dailyUsagePercent?: number;
63
+ dailyTurns?: number;
53
64
  }>;
54
65
  }
@@ -19,6 +19,7 @@ import {
19
19
  } from "../../core/accounts.ts";
20
20
  import { ServerManager } from "../server-manager.ts";
21
21
  import { config } from "../../core/config.ts";
22
+ import { renderProgressBar } from "./status-view.ts";
22
23
  export function formatCooldownReason(reason?: string | null, maxLen = 28): string {
23
24
  if (!reason) return theme.yellow("Cooldown ativo");
24
25
  if (
@@ -938,9 +939,11 @@ export class AccountsView implements TuiView {
938
939
  }
939
940
 
940
941
  // Left Panel: Accounts List Table
942
+ const showUsageCol = leftW >= 48;
943
+ const headerUsage = showUsageCol ? "Uso% " : "";
941
944
  const leftContent: string[] = [
942
945
  "",
943
- ` ${theme.dim("# Conta Status")}`,
946
+ ` ${theme.dim(`# Conta ${headerUsage}Status`)}`,
944
947
  ` ${theme.dim("───────────────────────────────────────")}`,
945
948
  ];
946
949
 
@@ -997,7 +1000,14 @@ export class AccountsView implements TuiView {
997
1000
  : theme.muted(`β—‹ Standby `);
998
1001
  }
999
1002
 
1000
- const line = `${pointer}${num}${name}${status}`;
1003
+ const usagePct = acc.dailyUsagePercent ?? 0;
1004
+ const usagePctStr = showUsageCol
1005
+ ? (usagePct > 0
1006
+ ? (usagePct >= 80 ? theme.red(pad(`${usagePct}%`, 5)) : usagePct >= 40 ? theme.yellow(pad(`${usagePct}%`, 5)) : theme.cyan(pad(`${usagePct}%`, 5)))
1007
+ : theme.dim(pad("0%", 5))) + " "
1008
+ : "";
1009
+
1010
+ const line = `${pointer}${num}${name}${usagePctStr}${status}`;
1001
1011
  if (isHovered) {
1002
1012
  leftContent.push(theme.bgHover(line));
1003
1013
  } else if (isFocused) {
@@ -1046,6 +1056,13 @@ export class AccountsView implements TuiView {
1046
1056
  rightContent.push(` ${theme.bold("Sistema ID:")} ${theme.muted(selected.id.slice(0, 14))}`);
1047
1057
  rightContent.push(` ${theme.bold("NΓ­vel:")} ${selected.priority}`);
1048
1058
 
1059
+ const usagePct = selected.dailyUsagePercent ?? 0;
1060
+ const usageTokens = selected.dailyTokens ?? 0;
1061
+ const usageTurns = selected.dailyTurns ?? 0;
1062
+ const usageColor = usagePct >= 80 ? theme.red : usagePct >= 40 ? theme.yellow : theme.cyan;
1063
+ const usageBar = renderProgressBar(usagePct, 6, usageColor);
1064
+ rightContent.push(` ${theme.bold("Cota Hoje:")} [${usageBar}] ${usageColor(`${usagePct}%`)} ${theme.dim(`(~${Math.round(usageTokens / 1000)}k / ${usageTurns} reqs)`)}`);
1065
+
1049
1066
  const cdStatus = selected.onCooldown
1050
1067
  ? theme.yellow(`[!] Cooldown ${Math.round(selected.remainingCooldownMs / 60000)}m`)
1051
1068
  : theme.green(`${glyphs.check} DisponΓ­vel`);