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.
package/dist/index.js CHANGED
@@ -150,6 +150,44 @@ var init_paths = __esm({
150
150
  }
151
151
  });
152
152
 
153
+ // src/core/process-guard.ts
154
+ function isPlaywrightAlreadyClosedMessage(message) {
155
+ if (!message) return false;
156
+ return message.includes("Target page, context or browser has been closed") || message.includes("Browser has been closed") || message.includes("Target closed") || message.includes("Target crashed") || message.includes("Page crashed") || message.includes("Assertion error") || message.includes("Cannot find parent object") || message.includes("Connection closed") || message.includes("session closed") || message.includes("Session closed") || message.includes("Network.setCacheDisabled") || message.includes("Protocol error") || message.includes("storageState timed out");
157
+ }
158
+ function isBenignTeardownError(error) {
159
+ if (!error) return false;
160
+ const message = error instanceof Error ? error.message : typeof error === "object" && "message" in error ? String(error.message) : String(error);
161
+ return isPlaywrightAlreadyClosedMessage(message);
162
+ }
163
+ function installProcessTeardownGuards(label = "Process") {
164
+ if (guardsInstalled) return;
165
+ guardsInstalled = true;
166
+ process.on("uncaughtException", (error) => {
167
+ if (isBenignTeardownError(error)) {
168
+ const msg = error instanceof Error ? error.message : typeof error === "object" && error !== null && "message" in error ? String(error.message) : String(error);
169
+ console.warn(`\u26A0\uFE0F [${label}] Handled benign driver teardown exception: ${msg}`);
170
+ return;
171
+ }
172
+ console.error(`\u274C [${label}] Uncaught Exception:`, error);
173
+ });
174
+ process.on("unhandledRejection", (reason) => {
175
+ if (isBenignTeardownError(reason)) {
176
+ const msg = reason instanceof Error ? reason.message : typeof reason === "object" && reason !== null && "message" in reason ? String(reason.message) : String(reason);
177
+ console.warn(`\u26A0\uFE0F [${label}] Handled benign driver teardown rejection: ${msg}`);
178
+ return;
179
+ }
180
+ console.error(`\u274C [${label}] Unhandled Rejection:`, reason);
181
+ });
182
+ }
183
+ var guardsInstalled;
184
+ var init_process_guard = __esm({
185
+ "src/core/process-guard.ts"() {
186
+ "use strict";
187
+ guardsInstalled = false;
188
+ }
189
+ });
190
+
153
191
  // src/core/config.ts
154
192
  import fs2 from "node:fs";
155
193
  import dotenv from "dotenv";
@@ -289,11 +327,11 @@ var init_config = __esm({
289
327
  // nothing in this window is dead and should fail fast, retryable).
290
328
  QWEN_FIRST_CHUNK_TIMEOUT: z.string().default("60000"),
291
329
  TOTAL_REQUEST_TIMEOUT: z.string().default("600000"),
292
- // Mid-stream silence window for thinking models: 3 min with ZERO upstream
330
+ // Mid-stream silence window for thinking models: 5 min with ZERO upstream
293
331
  // bytes is a dead stream (WAF swallow / dropped connection) — fail fast and
294
332
  // let the retry policy rotate accounts. Flowing reasoning chunks RESET this
295
333
  // timer, so legitimate slow thinking is never cut; only total silence is.
296
- REASONING_MODEL_TIMEOUT: z.string().default("180000"),
334
+ REASONING_MODEL_TIMEOUT: z.string().default("300000"),
297
335
  CACHE_TTL: z.string().default("3600"),
298
336
  RESPONSE_TTL: z.string().default("1800"),
299
337
  CACHE_COMPRESSION_ENABLED: z.string().default("true"),
@@ -1592,7 +1630,26 @@ function runMigrations(db2) {
1592
1630
  instruction_hash TEXT NOT NULL,
1593
1631
  updated_at TEXT NOT NULL DEFAULT (datetime('now'))
1594
1632
  );
1633
+
1634
+ CREATE TABLE IF NOT EXISTS account_daily_usage (
1635
+ account_id TEXT NOT NULL,
1636
+ date_utc TEXT NOT NULL,
1637
+ prompt_tokens INTEGER NOT NULL DEFAULT 0,
1638
+ completion_tokens INTEGER NOT NULL DEFAULT 0,
1639
+ weighted_tokens REAL NOT NULL DEFAULT 0,
1640
+ total_tokens INTEGER NOT NULL DEFAULT 0,
1641
+ turns_count INTEGER NOT NULL DEFAULT 0,
1642
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
1643
+ PRIMARY KEY (account_id, date_utc)
1644
+ );
1645
+
1646
+ CREATE INDEX IF NOT EXISTS idx_daily_usage_date ON account_daily_usage(date_utc);
1595
1647
  `);
1648
+ try {
1649
+ db2.exec(`ALTER TABLE account_daily_usage ADD COLUMN weighted_tokens REAL DEFAULT 0;`);
1650
+ } catch (err) {
1651
+ if (!isDuplicateColumnError(err)) throw err;
1652
+ }
1596
1653
  try {
1597
1654
  db2.exec(
1598
1655
  `ALTER TABLE accounts ADD COLUMN cooldown_until INTEGER DEFAULT 0;`
@@ -1693,7 +1750,11 @@ function saveAuthSession(accountId, session) {
1693
1750
  ) VALUES (
1694
1751
  ?, ?, ?, ?, ?, ?,
1695
1752
  ?, ?, ?, ?,
1696
- ?, ?, ?, ?, datetime('now')
1753
+ COALESCE(?, (SELECT user_id FROM qwen_auth_sessions WHERE account_id = ?)),
1754
+ ?,
1755
+ COALESCE(?, (SELECT refresh_token FROM qwen_auth_sessions WHERE account_id = ?)),
1756
+ ?,
1757
+ datetime('now')
1697
1758
  )
1698
1759
  `);
1699
1760
  stmt.run(
@@ -1708,8 +1769,10 @@ function saveAuthSession(accountId, session) {
1708
1769
  session.secChUaPlatform || null,
1709
1770
  session.version || null,
1710
1771
  session.userId || null,
1772
+ accountId,
1711
1773
  session.tokenExpiresAt || null,
1712
1774
  session.refreshToken || null,
1775
+ accountId,
1713
1776
  session.capturedAt ?? Date.now()
1714
1777
  );
1715
1778
  }
@@ -4235,6 +4298,8 @@ var init_clean_cache = __esm({
4235
4298
  "src/clean-cache.ts"() {
4236
4299
  "use strict";
4237
4300
  init_playwright();
4301
+ init_process_guard();
4302
+ installProcessTeardownGuards("CleanCache");
4238
4303
  isDirectRun = process.argv[1] && (process.argv[1].endsWith("clean-cache.ts") || process.argv[1].endsWith("clean-cache.js"));
4239
4304
  if (isDirectRun) {
4240
4305
  main().catch((err) => {
@@ -4459,15 +4524,18 @@ function loadStorageState(accountId) {
4459
4524
  return void 0;
4460
4525
  }
4461
4526
  }
4462
- async function saveStorageState(context, accountId, timeoutMs = 5e3) {
4527
+ async function saveStorageState(context, accountId, timeoutMs = 1e4) {
4463
4528
  try {
4464
4529
  const stateFile = getStorageStatePath(accountId);
4465
4530
  const dir = path6.dirname(stateFile);
4466
4531
  if (!fs9.existsSync(dir)) {
4467
4532
  fs9.mkdirSync(dir, { recursive: true });
4468
4533
  }
4534
+ const rawPromise = context.storageState({ path: stateFile }).catch(() => {
4535
+ return void 0;
4536
+ });
4469
4537
  await withTimeout(
4470
- context.storageState({ path: stateFile }),
4538
+ rawPromise,
4471
4539
  timeoutMs,
4472
4540
  `storageState timed out after ${timeoutMs}ms`
4473
4541
  );
@@ -5194,6 +5262,11 @@ async function getBasicHeaders(accountId) {
5194
5262
  if (!page) {
5195
5263
  throw new Error(`Playwright not initialized for account: ${accountId}`);
5196
5264
  }
5265
+ const inFlightInit = inFlightAccountInits.get(accountId);
5266
+ if (inFlightInit) {
5267
+ await inFlightInit.catch(() => {
5268
+ });
5269
+ }
5197
5270
  const release = await acquireAccountMutex(
5198
5271
  accountId,
5199
5272
  `headers:${accountId.substring(0, 12)}`
@@ -6668,6 +6741,11 @@ async function captureQwenHeaders(accountId, pageOverride, timeoutMs = config.ti
6668
6741
  const { saveAuthSession: saveAuthSession2 } = await Promise.resolve().then(() => (init_database(), database_exports));
6669
6742
  const { parseJwtExpiry: parseJwtExpiry2 } = await Promise.resolve().then(() => (init_jwt(), jwt_exports));
6670
6743
  const tokenExpiry = parseJwtExpiry2(capturedHeaders.cookie);
6744
+ let rTok;
6745
+ try {
6746
+ rTok = (await page.evaluate(() => localStorage.getItem("refresh_token") || "")).trim() || void 0;
6747
+ } catch {
6748
+ }
6671
6749
  saveAuthSession2(accountId, {
6672
6750
  cookie: capturedHeaders.cookie,
6673
6751
  userAgent: capturedHeaders["user-agent"],
@@ -6679,6 +6757,7 @@ async function captureQwenHeaders(accountId, pageOverride, timeoutMs = config.ti
6679
6757
  secChUaPlatform: capturedHeaders["sec-ch-ua-platform"],
6680
6758
  version: capturedHeaders["version"],
6681
6759
  tokenExpiresAt: tokenExpiry || void 0,
6760
+ refreshToken: rTok,
6682
6761
  capturedAt: Date.now()
6683
6762
  });
6684
6763
  } catch {
@@ -6710,38 +6789,57 @@ async function captureQwenHeaders(accountId, pageOverride, timeoutMs = config.ti
6710
6789
  const isAuthUrl = currentUrl.includes("/auth") || currentUrl.includes("/login");
6711
6790
  const isSuspectedGuest = isAuthUrl || attempt >= 2 && !await isPageLoggedIn(page, 5e3);
6712
6791
  if (isSuspectedGuest) {
6713
- const { getAccountCredentials: getAccountCredentials2 } = await Promise.resolve().then(() => (init_accounts(), accounts_exports));
6714
- const creds = getAccountCredentials2(accountId);
6715
- if (creds && creds.email && creds.password) {
6716
- console.warn(
6717
- `\u26A0\uFE0F [Playwright] Session expired or guest state detected during header capture for ${accountId}; re-authenticating...`
6792
+ const silentRefresh = await refreshAccountToken(accountId);
6793
+ if (silentRefresh.success) {
6794
+ console.log(
6795
+ `\u26A1 [Playwright] Token refreshed silently during header capture for ${accountId}`
6718
6796
  );
6719
- armOverallDeadline();
6720
- const ok = await loginToQwen(accountId, creds.email, creds.password);
6721
- if (ok) {
6797
+ if (isAuthUrl) {
6722
6798
  await page.goto(qwenUrl("/"), {
6723
6799
  waitUntil: "domcontentloaded",
6724
6800
  timeout: Math.min(config.timeouts.navigation, timeoutMs)
6725
6801
  });
6726
- await sleep2(2e3);
6802
+ await sleep2(1500);
6803
+ }
6804
+ if (await isPageLoggedIn(page, 5e3)) {
6805
+ armOverallDeadline();
6727
6806
  }
6728
- if (!ok || !await isPageLoggedIn(page, 6e3)) {
6807
+ }
6808
+ const stillSuspected = !silentRefresh.success || !await isPageLoggedIn(page, 2e3);
6809
+ if (stillSuspected) {
6810
+ const { getAccountCredentials: getAccountCredentials2 } = await Promise.resolve().then(() => (init_accounts(), accounts_exports));
6811
+ const creds = getAccountCredentials2(accountId);
6812
+ if (creds && creds.email && creds.password) {
6813
+ console.warn(
6814
+ `\u26A0\uFE0F [Playwright] Session expired or guest state detected during header capture for ${accountId}; re-authenticating...`
6815
+ );
6816
+ armOverallDeadline();
6817
+ const ok = await loginToQwen(accountId, creds.email, creds.password);
6818
+ if (ok) {
6819
+ await page.goto(qwenUrl("/"), {
6820
+ waitUntil: "domcontentloaded",
6821
+ timeout: Math.min(config.timeouts.navigation, timeoutMs)
6822
+ });
6823
+ await sleep2(2e3);
6824
+ }
6825
+ if (!ok || !await isPageLoggedIn(page, 6e3)) {
6826
+ settle(
6827
+ new Error(
6828
+ `Header capture failed for ${accountId}: re-login after session expiry did not succeed`
6829
+ )
6830
+ );
6831
+ return;
6832
+ }
6833
+ armOverallDeadline();
6834
+ if (settled || page.isClosed()) return;
6835
+ } else {
6729
6836
  settle(
6730
6837
  new Error(
6731
- `Header capture failed for ${accountId}: re-login after session expiry did not succeed`
6838
+ `Header capture failed for ${accountId}: session expired and no credentials available for re-login (run 'qpx login')`
6732
6839
  )
6733
6840
  );
6734
6841
  return;
6735
6842
  }
6736
- armOverallDeadline();
6737
- if (settled || page.isClosed()) return;
6738
- } else {
6739
- settle(
6740
- new Error(
6741
- `Header capture failed for ${accountId}: session expired and no credentials available for re-login (run 'qpx login')`
6742
- )
6743
- );
6744
- return;
6745
6843
  }
6746
6844
  }
6747
6845
  if (settled || page.isClosed()) return;
@@ -7057,7 +7155,13 @@ async function refreshHeadersInternal(accountId, timeoutMs = config.timeouts.hea
7057
7155
  `[Playwright] Navigation check failed during refresh for ${accountId}:`,
7058
7156
  navErr.message
7059
7157
  );
7060
- await executeReauth();
7158
+ if (await isPageLoggedIn(page, 3e3)) {
7159
+ } else {
7160
+ const refreshed = await refreshAccountToken(accountId);
7161
+ if (!refreshed.success) {
7162
+ await executeReauth();
7163
+ }
7164
+ }
7061
7165
  }
7062
7166
  }
7063
7167
  if (hasRequiredQwenHeaders(cache3.headers)) {
@@ -7071,6 +7175,11 @@ async function refreshHeadersInternal(accountId, timeoutMs = config.timeouts.hea
7071
7175
  const { saveAuthSession: saveAuthSession2 } = await Promise.resolve().then(() => (init_database(), database_exports));
7072
7176
  const tokenCookie = liveCookies.find((c) => c.name === "token");
7073
7177
  const exp = tokenCookie ? parseJwtExpiry(tokenCookie.value) : void 0;
7178
+ let rTok;
7179
+ try {
7180
+ rTok = (await page.evaluate(() => localStorage.getItem("refresh_token") || "")).trim() || void 0;
7181
+ } catch {
7182
+ }
7074
7183
  saveAuthSession2(accountId, {
7075
7184
  cookie: cookieStr,
7076
7185
  userAgent: cache3.headers["user-agent"] || "",
@@ -7082,6 +7191,7 @@ async function refreshHeadersInternal(accountId, timeoutMs = config.timeouts.hea
7082
7191
  secChUaPlatform: cache3.headers["sec-ch-ua-platform"] || void 0,
7083
7192
  version: cache3.headers["version"] || void 0,
7084
7193
  tokenExpiresAt: exp || void 0,
7194
+ refreshToken: rTok,
7085
7195
  capturedAt: Date.now()
7086
7196
  });
7087
7197
  } catch {
@@ -7468,8 +7578,10 @@ async function keepAlivePlaywrightAccount(accountId) {
7468
7578
  if (!page || page.isClosed()) return false;
7469
7579
  const now = Date.now();
7470
7580
  const currentUrl = page.url();
7581
+ const lastRefreshAttempt = lastProactiveTokenRefresh.get(accountId) ?? 0;
7471
7582
  const cookie = await getCookies(accountId);
7472
- if (cookie && isTokenExpiringSoon(cookie, 5)) {
7583
+ if (cookie && isTokenExpiringSoon(cookie, 5) && now - lastRefreshAttempt > 6e4) {
7584
+ lastProactiveTokenRefresh.set(accountId, now);
7473
7585
  console.log(
7474
7586
  `\u{1F493} [SessionKeeper] Account ${accountId} access token expiring soon (<5m); silently refreshing...`
7475
7587
  );
@@ -7513,6 +7625,7 @@ function cleanupPlaywrightAccountState(accountId) {
7513
7625
  cookieCaches.delete(accountId);
7514
7626
  lastAccountActivity.delete(accountId);
7515
7627
  lastKeepAliveNavigation.delete(accountId);
7628
+ lastProactiveTokenRefresh.delete(accountId);
7516
7629
  clearFingerprintCache(accountId);
7517
7630
  unmarkAccountHeadersReady(accountId);
7518
7631
  }
@@ -7696,7 +7809,7 @@ async function getTokenDiagnostics(accountId) {
7696
7809
  }
7697
7810
  };
7698
7811
  }
7699
- var requireLocal, contextInitHooks, ACCOUNT_MUTEX_MAX_HOLD_MS, accountMutexes, accountContexts, accountPages, cachedUserAgents, inFlightAccountInits, sharedBrowser, sharedBrowserPromise, headerCaches, HEADER_CACHE_TTL, HEADER_REFRESH_THRESHOLD, COOKIE_CACHE_TTL, cookieCaches, lastAccountActivity, lastKeepAliveNavigation, profileResetQueue, profileResetChain, closingAllPlaywright, sleep2, HEADER_CAPTURE_SETTLE_MS, PLAYWRIGHT_MUTEX_WAIT_MS, ACCOUNT_PAGE_OPERATION_TIMEOUT_MS, SESSION_PROBE_NAVIGATION_TIMEOUT_MS, HEADER_CAPTURE_TRIGGER_GRACE_MS, FIRST_TRIGGER_GRACE_MS, HEADER_CAPTURE_TRIGGER_ATTEMPTS, CHAT_INPUT_APPEAR_TIMEOUT_MS, CHAT_INPUT_ACTION_TIMEOUT_MS, PROFILE_RESET_TIMEOUT_MS;
7812
+ var requireLocal, contextInitHooks, ACCOUNT_MUTEX_MAX_HOLD_MS, accountMutexes, accountContexts, accountPages, cachedUserAgents, inFlightAccountInits, sharedBrowser, sharedBrowserPromise, headerCaches, HEADER_CACHE_TTL, HEADER_REFRESH_THRESHOLD, COOKIE_CACHE_TTL, cookieCaches, lastAccountActivity, lastKeepAliveNavigation, lastProactiveTokenRefresh, profileResetQueue, profileResetChain, closingAllPlaywright, sleep2, HEADER_CAPTURE_SETTLE_MS, PLAYWRIGHT_MUTEX_WAIT_MS, ACCOUNT_PAGE_OPERATION_TIMEOUT_MS, SESSION_PROBE_NAVIGATION_TIMEOUT_MS, HEADER_CAPTURE_TRIGGER_GRACE_MS, FIRST_TRIGGER_GRACE_MS, HEADER_CAPTURE_TRIGGER_ATTEMPTS, CHAT_INPUT_APPEAR_TIMEOUT_MS, CHAT_INPUT_ACTION_TIMEOUT_MS, PROFILE_RESET_TIMEOUT_MS;
7700
7813
  var init_playwright = __esm({
7701
7814
  "src/services/playwright.ts"() {
7702
7815
  "use strict";
@@ -7732,6 +7845,7 @@ var init_playwright = __esm({
7732
7845
  cookieCaches = /* @__PURE__ */ new Map();
7733
7846
  lastAccountActivity = /* @__PURE__ */ new Map();
7734
7847
  lastKeepAliveNavigation = /* @__PURE__ */ new Map();
7848
+ lastProactiveTokenRefresh = /* @__PURE__ */ new Map();
7735
7849
  profileResetQueue = /* @__PURE__ */ new Map();
7736
7850
  profileResetChain = Promise.resolve();
7737
7851
  closingAllPlaywright = false;
@@ -8547,9 +8661,13 @@ var init_model_registry = __esm({
8547
8661
  // src/services/captcha-coordinator.ts
8548
8662
  var captcha_coordinator_exports = {};
8549
8663
  __export(captcha_coordinator_exports, {
8664
+ isCaptchaRecoveryActive: () => isCaptchaRecoveryActive,
8550
8665
  recoverBaxiaCaptcha: () => recoverBaxiaCaptcha,
8551
8666
  solveChallengeOnPage: () => solveChallengeOnPage
8552
8667
  });
8668
+ function isCaptchaRecoveryActive(accountId) {
8669
+ return activeCaptchaRecoveries.has(accountId);
8670
+ }
8553
8671
  async function gotoBestEffort(page, url) {
8554
8672
  await page.goto(url, {
8555
8673
  waitUntil: "domcontentloaded",
@@ -8592,11 +8710,12 @@ async function recoverBaxiaCaptcha(accountId, label, options = {}) {
8592
8710
  return false;
8593
8711
  }
8594
8712
  metrics.increment("captcha.challenges.detected", 1, { solver });
8595
- const challengeUrl = options.challengeBody ? extractBaxiaChallengeUrl(options.challengeBody, config.qwen.baseUrl) : null;
8596
8713
  const solverOperationTimeoutMs = Math.max(
8597
8714
  15e3,
8598
8715
  config.captcha.timeoutMs + config.captcha.maxAttempts * (3e3 + config.captcha.retryDelayMs + config.captcha.settleMs) + 2 * CHALLENGE_NAVIGATION_TIMEOUT_MS
8599
8716
  );
8717
+ const challengeUrl = options.challengeBody ? extractBaxiaChallengeUrl(options.challengeBody, config.qwen.baseUrl) : null;
8718
+ activeCaptchaRecoveries.add(accountId);
8600
8719
  try {
8601
8720
  const solved = await withAccountPage(
8602
8721
  accountId,
@@ -8629,9 +8748,11 @@ async function recoverBaxiaCaptcha(accountId, label, options = {}) {
8629
8748
  detail
8630
8749
  });
8631
8750
  return false;
8751
+ } finally {
8752
+ activeCaptchaRecoveries.delete(accountId);
8632
8753
  }
8633
8754
  }
8634
- var CHALLENGE_NAVIGATION_TIMEOUT_MS, CHALLENGE_PATH_MARKER, FAILED_RECOVERY_BACKOFF_MS, lastFailedRecoveryAt;
8755
+ var CHALLENGE_NAVIGATION_TIMEOUT_MS, CHALLENGE_PATH_MARKER, FAILED_RECOVERY_BACKOFF_MS, lastFailedRecoveryAt, activeCaptchaRecoveries;
8635
8756
  var init_captcha_coordinator = __esm({
8636
8757
  "src/services/captcha-coordinator.ts"() {
8637
8758
  "use strict";
@@ -8644,6 +8765,7 @@ var init_captcha_coordinator = __esm({
8644
8765
  CHALLENGE_PATH_MARKER = "_____tmd_____";
8645
8766
  FAILED_RECOVERY_BACKOFF_MS = 3e4;
8646
8767
  lastFailedRecoveryAt = /* @__PURE__ */ new Map();
8768
+ activeCaptchaRecoveries = /* @__PURE__ */ new Set();
8647
8769
  }
8648
8770
  });
8649
8771
 
@@ -9336,6 +9458,11 @@ function isCorruptedChatHistoryError(err) {
9336
9458
  const message = errMessage(err).toLowerCase();
9337
9459
  return message.includes("first message must not") || message.includes("first message must be") || message.includes("must not assistant message") || message.includes("must not be assistant");
9338
9460
  }
9461
+ function isInternalServerError(err) {
9462
+ const code = errCode(err).toLowerCase();
9463
+ const message = errMessage(err).toLowerCase();
9464
+ return code === "internal_error" || code === "internal_server_error" || code === "server_error" || code === "server_busy" || code === "busy" || message.includes("internal_error") || message.includes("ocorreu um erro inesperado") || message.includes("unexpected error") || message.includes("internal server error");
9465
+ }
9339
9466
  function makeRetryAction(reason, overrides = {}) {
9340
9467
  return {
9341
9468
  retryable: true,
@@ -9442,6 +9569,15 @@ function classifyRetryAction(err, options) {
9442
9569
  accountCooldownReason: quota.accountCooldownReason
9443
9570
  });
9444
9571
  }
9572
+ if (isInternalServerError(err) && !(err instanceof QwenUpstreamError)) {
9573
+ const typed = err;
9574
+ return makeRetryAction("upstream_internal_error", {
9575
+ switchAccount: typed.switchAccount !== false,
9576
+ forceNewChat: true,
9577
+ retryWithFullPrompt: true,
9578
+ retryAfterMs: typed.retryAfterMs ?? Math.min(baseDelayMs * 2, 3e3)
9579
+ });
9580
+ }
9445
9581
  if (isNetworkLikeError(err) || err instanceof QwenUpstreamUnavailableError || err instanceof QwenUpstreamError || isAbortError(err)) {
9446
9582
  const typed = err;
9447
9583
  return makeRetryAction(
@@ -11110,6 +11246,7 @@ __export(qwen_exports, {
11110
11246
  fetchRemoteQwenChats: () => fetchRemoteQwenChats,
11111
11247
  flushLogicalThreadState: () => flushLogicalThreadState,
11112
11248
  getAnyCachedQwenModels: () => getAnyCachedQwenModels,
11249
+ getInFlightPersonalizationCount: () => getInFlightPersonalizationCount,
11113
11250
  getLogicalThreadState: () => getLogicalThreadState,
11114
11251
  getQwenErrorCode: () => getQwenErrorCode,
11115
11252
  getSessionParent: () => getSessionParent,
@@ -12042,12 +12179,51 @@ async function createQwenBrowserResponse(accountId, url, method, headers, body,
12042
12179
  captchaWatcher?.stop();
12043
12180
  }
12044
12181
  }
12182
+ function getInFlightPersonalizationCount() {
12183
+ return inFlightPersonalizationSyncs.size;
12184
+ }
12045
12185
  async function syncQwenRequestPersonalization(instruction, accountId, metadata = {}) {
12046
- if (isAuthMockEnabled()) {
12047
- if (process.env.TEST_PERSONALIZATION_SYNC_FAIL === "true") return false;
12048
- return true;
12049
- }
12050
12186
  const cacheKey = accountId || "global";
12187
+ const sent = textSize(instruction);
12188
+ const syncHash = sent.hash ? `${sent.hash}:${QWEN_SAFE_SETTINGS_HASH}` : null;
12189
+ const inFlightKey = `${cacheKey}:${syncHash || "empty"}`;
12190
+ const existingInFlight = inFlightPersonalizationSyncs.get(inFlightKey);
12191
+ if (existingInFlight) {
12192
+ logger.debug("[Qwen] Personalization sync already in flight, sharing promise", {
12193
+ accountId: cacheKey
12194
+ });
12195
+ return existingInFlight;
12196
+ }
12197
+ let syncPromise;
12198
+ syncPromise = (async () => {
12199
+ await Promise.resolve();
12200
+ try {
12201
+ if (isAuthMockEnabled()) {
12202
+ const delay = parseInt(process.env.TEST_PERSONALIZATION_DELAY_MS || "0", 10);
12203
+ if (delay > 0) {
12204
+ await sleep5(delay);
12205
+ }
12206
+ if (process.env.TEST_PERSONALIZATION_SYNC_FAIL === "true") return false;
12207
+ return true;
12208
+ }
12209
+ return await syncQwenRequestPersonalizationInternal(
12210
+ instruction,
12211
+ accountId,
12212
+ metadata,
12213
+ cacheKey,
12214
+ sent,
12215
+ syncHash
12216
+ );
12217
+ } finally {
12218
+ if (inFlightPersonalizationSyncs.get(inFlightKey) === syncPromise) {
12219
+ inFlightPersonalizationSyncs.delete(inFlightKey);
12220
+ }
12221
+ }
12222
+ })();
12223
+ inFlightPersonalizationSyncs.set(inFlightKey, syncPromise);
12224
+ return syncPromise;
12225
+ }
12226
+ async function syncQwenRequestPersonalizationInternal(instruction, accountId, metadata, cacheKey, sent, syncHash) {
12051
12227
  let forceRefresh = false;
12052
12228
  try {
12053
12229
  const basic = await getBasicHeaders2(accountId);
@@ -12065,8 +12241,6 @@ async function syncQwenRequestPersonalization(instruction, accountId, metadata =
12065
12241
  });
12066
12242
  let currentSettings = null;
12067
12243
  let payload = buildQwenSettingsUpdatePayload(currentSettings, instruction);
12068
- const sent = textSize(instruction);
12069
- const syncHash = sent.hash ? `${sent.hash}:${QWEN_SAFE_SETTINGS_HASH}` : null;
12070
12244
  const bypassCache = metadata.forceSync === true;
12071
12245
  const cachedHash = lastSyncedPersonalizationHashes.get(cacheKey);
12072
12246
  if (!bypassCache && syncHash && cachedHash === syncHash) {
@@ -13411,7 +13585,7 @@ async function createQwenStreamInternal(prompt, enableThinking, modelId, forcedP
13411
13585
  clearTimeout(timeoutId);
13412
13586
  }
13413
13587
  }
13414
- var sleep5, BROWSER_STREAM_BINDING, BROWSER_ABORTERS_KEY, BROWSER_STREAM_FLUSH_BYTES, BROWSER_STREAM_FLUSH_MS, METADATA_TIMEOUT_PER_PAYLOAD_MB_MS, POST_CAPTCHA_METADATA_GRACE_MS, browserStreamStates, browserStreamBindingPages, browserStreamBindingContexts, MODEL_CACHE_TTL_MS, modelsCache, nativeToolsDisabled, disablingNativeToolsInProgress, lastSyncedPersonalizationHashes, directSettingsFetchConsecutiveFailures, DIRECT_SETTINGS_FETCH_BLOCK_THRESHOLD, directSettingsFetchBlocked, DIRECT_SETTINGS_FETCH_TIMEOUT_MS, activePersonalizationByAccount, accountStreamMutexes, QWEN_SAFE_SETTINGS_PATCH, QWEN_SAFE_SETTINGS_HASH, DISABLE_TOOLS_MAX_RETRIES, DISABLE_TOOLS_BACKOFF_MS, UPSTREAM_RESPONSE_PREVIEW_BYTES;
13588
+ var sleep5, BROWSER_STREAM_BINDING, BROWSER_ABORTERS_KEY, BROWSER_STREAM_FLUSH_BYTES, BROWSER_STREAM_FLUSH_MS, METADATA_TIMEOUT_PER_PAYLOAD_MB_MS, POST_CAPTCHA_METADATA_GRACE_MS, browserStreamStates, browserStreamBindingPages, browserStreamBindingContexts, MODEL_CACHE_TTL_MS, modelsCache, nativeToolsDisabled, disablingNativeToolsInProgress, lastSyncedPersonalizationHashes, directSettingsFetchConsecutiveFailures, DIRECT_SETTINGS_FETCH_BLOCK_THRESHOLD, directSettingsFetchBlocked, DIRECT_SETTINGS_FETCH_TIMEOUT_MS, activePersonalizationByAccount, accountStreamMutexes, QWEN_SAFE_SETTINGS_PATCH, QWEN_SAFE_SETTINGS_HASH, inFlightPersonalizationSyncs, DISABLE_TOOLS_MAX_RETRIES, DISABLE_TOOLS_BACKOFF_MS, UPSTREAM_RESPONSE_PREVIEW_BYTES;
13415
13589
  var init_qwen = __esm({
13416
13590
  "src/services/qwen.ts"() {
13417
13591
  "use strict";
@@ -13498,6 +13672,7 @@ var init_qwen = __esm({
13498
13672
  }
13499
13673
  };
13500
13674
  QWEN_SAFE_SETTINGS_HASH = crypto7.createHash("sha256").update(JSON.stringify(QWEN_SAFE_SETTINGS_PATCH)).digest("hex").slice(0, 12);
13675
+ inFlightPersonalizationSyncs = /* @__PURE__ */ new Map();
13501
13676
  DISABLE_TOOLS_MAX_RETRIES = 3;
13502
13677
  DISABLE_TOOLS_BACKOFF_MS = 2e3;
13503
13678
  UPSTREAM_RESPONSE_PREVIEW_BYTES = 8 * 1024;
@@ -15945,6 +16120,12 @@ async function deleteChatsForConfiguredAccounts(keepBrowserOpen = false) {
15945
16120
  `\u274C [DeleteChats] [${currentIdx}/${totalCount}] Falha ao apagar conversas de ${maskEmail(account.email)}:`,
15946
16121
  error instanceof Error ? error.message : String(error)
15947
16122
  );
16123
+ } finally {
16124
+ if (!keepBrowserOpen) {
16125
+ const { closePlaywrightForAccount: closePlaywrightForAccount2 } = await Promise.resolve().then(() => (init_playwright(), playwright_exports));
16126
+ await closePlaywrightForAccount2(account.id).catch(() => {
16127
+ });
16128
+ }
15948
16129
  }
15949
16130
  }
15950
16131
  } finally {
@@ -16430,8 +16611,9 @@ function resolveInitialAccount(preferredAccountId, excludeAccountIds) {
16430
16611
  syncCooldownsFromDb(configuredAccounts);
16431
16612
  const excluded = new Set(excludeAccountIds ?? []);
16432
16613
  if (typeof preferredAccountId === "string" && preferredAccountId) {
16614
+ const needle = preferredAccountId.toLowerCase();
16433
16615
  const preferred = configuredAccounts.find(
16434
- (candidate) => candidate.id === preferredAccountId
16616
+ (candidate) => candidate.id === preferredAccountId || candidate.id.toLowerCase().startsWith(needle) || candidate.email.toLowerCase() === needle || candidate.email.toLowerCase().includes(needle)
16435
16617
  );
16436
16618
  if (preferred && !getAccountCooldownInfo(preferred.id)) {
16437
16619
  return { account: preferred, configuredAccounts };
@@ -16786,7 +16968,12 @@ function shouldWaitQueueForever(isThreadOwnerWaiting, heldByOtherSession, hasFre
16786
16968
  }
16787
16969
  async function tryCreateStreamWithRetry(params, accountId, accountEmail) {
16788
16970
  const maxAttempts = Math.max(1, config.retry.maxAttempts);
16789
- const maxAccountSwitches = Math.max(0, config.retry.maxAccountSwitches);
16971
+ const accounts = loadAccounts();
16972
+ const configuredAccountCount = accounts.length;
16973
+ const maxAccountSwitches = Math.max(
16974
+ config.retry.maxAccountSwitches,
16975
+ Math.min(configuredAccountCount - 1, 6)
16976
+ );
16790
16977
  let attemptsLeft = maxAttempts;
16791
16978
  let retryDelay = config.retry.baseDelayMs;
16792
16979
  let attempt = 0;
@@ -16798,7 +16985,6 @@ async function tryCreateStreamWithRetry(params, accountId, accountEmail) {
16798
16985
  let chatInProgressOriginAccountEmail = null;
16799
16986
  let lastAttemptError = null;
16800
16987
  let invalidInputSameAccountRetried = false;
16801
- const accounts = loadAccounts();
16802
16988
  const isSingleAccount = accounts.length <= 1;
16803
16989
  let currentAccountId = accountId;
16804
16990
  let currentAccountEmail = accountEmail;
@@ -17011,14 +17197,24 @@ ${loopReminder}` : truncation.prompt;
17011
17197
  const acquireDeadlineMs = config.concurrency.acquireDeadlineMs;
17012
17198
  let acquireDeadlineTimer;
17013
17199
  const acquireDeadline = new Promise((_, reject) => {
17014
- acquireDeadlineTimer = setTimeout(() => {
17200
+ const checkDeadline = async () => {
17201
+ try {
17202
+ const { isCaptchaRecoveryActive: isCaptchaRecoveryActive2 } = await Promise.resolve().then(() => (init_captcha_coordinator(), captcha_coordinator_exports));
17203
+ if (currentAccountId && isCaptchaRecoveryActive2(currentAccountId)) {
17204
+ acquireDeadlineTimer = setTimeout(checkDeadline, 3e4);
17205
+ acquireDeadlineTimer.unref?.();
17206
+ return;
17207
+ }
17208
+ } catch {
17209
+ }
17015
17210
  acquireAbort.abort();
17016
17211
  const err2 = new Error(
17017
17212
  `Acquire deadline (${acquireDeadlineMs}ms) exceeded creating stream on ${currentAccountEmail}`
17018
17213
  );
17019
17214
  err2.code = "acquire_deadline";
17020
17215
  reject(err2);
17021
- }, acquireDeadlineMs);
17216
+ };
17217
+ acquireDeadlineTimer = setTimeout(checkDeadline, acquireDeadlineMs);
17022
17218
  acquireDeadlineTimer.unref?.();
17023
17219
  });
17024
17220
  result = await Promise.race([
@@ -17300,6 +17496,10 @@ ${loopReminder}` : truncation.prompt;
17300
17496
  currentAccountId = nextAccount.id;
17301
17497
  currentAccountEmail = maskEmail(nextAccount.email);
17302
17498
  accountSwitches++;
17499
+ const isRealQuotaError = policy.reason === "quota_or_rate_limit" && (policy.accountCooldownReason === "RateLimited" || policy.accountCooldownReason === "QuotaExceeded");
17500
+ if (isRealQuotaError) {
17501
+ attemptsLeft = Math.max(attemptsLeft, 1);
17502
+ }
17303
17503
  if (params.useThreadNative) {
17304
17504
  const abandonedChatId = params.existingThread?.chatSessionId;
17305
17505
  if (abandonedChatId) {
@@ -19920,6 +20120,199 @@ var init_token_estimation_metrics = __esm({
19920
20120
  }
19921
20121
  });
19922
20122
 
20123
+ // src/core/daily-usage.ts
20124
+ var daily_usage_exports = {};
20125
+ __export(daily_usage_exports, {
20126
+ DAILY_ACCOUNT_TOKEN_BASELINE: () => DAILY_ACCOUNT_TOKEN_BASELINE,
20127
+ MODEL_DAILY_OUTPUT_BASELINES: () => MODEL_DAILY_OUTPUT_BASELINES,
20128
+ STANDARD_DAILY_TOKEN_BASELINE: () => STANDARD_DAILY_TOKEN_BASELINE,
20129
+ getAccountDailyUsage: () => getAccountDailyUsage,
20130
+ getAllAccountsDailyUsage: () => getAllAccountsDailyUsage,
20131
+ getModelQuotaWeight: () => getModelQuotaWeight,
20132
+ getPoolDailyUsageSummary: () => getPoolDailyUsageSummary,
20133
+ getUtcDateString: () => getUtcDateString,
20134
+ recordTurnUsage: () => recordTurnUsage
20135
+ });
20136
+ function getModelQuotaWeight(model) {
20137
+ if (!model) return 1;
20138
+ const resolved = mapClientModelToQwen(model).toLowerCase();
20139
+ if (resolved.includes("max")) {
20140
+ return STANDARD_DAILY_TOKEN_BASELINE / MODEL_DAILY_OUTPUT_BASELINES.max;
20141
+ }
20142
+ if (resolved.includes("omni") || resolved.includes("flash")) {
20143
+ return STANDARD_DAILY_TOKEN_BASELINE / MODEL_DAILY_OUTPUT_BASELINES.omni;
20144
+ }
20145
+ return 1;
20146
+ }
20147
+ function getUtcDateString(timestampMs = Date.now()) {
20148
+ return new Date(timestampMs).toISOString().slice(0, 10);
20149
+ }
20150
+ function recordTurnUsage(accountId, usage, modelOrDate, maybeDateUtc) {
20151
+ if (!accountId || accountId === "global") return;
20152
+ let model;
20153
+ let dateUtc;
20154
+ if (modelOrDate && /^\d{4}-\d{2}-\d{2}$/.test(modelOrDate)) {
20155
+ dateUtc = modelOrDate;
20156
+ model = void 0;
20157
+ } else {
20158
+ model = modelOrDate;
20159
+ dateUtc = maybeDateUtc || getUtcDateString();
20160
+ }
20161
+ const prompt = Math.max(0, Number(usage.prompt_tokens) || 0);
20162
+ const completion = Math.max(0, Number(usage.completion_tokens) || 0);
20163
+ const total = Math.max(
20164
+ prompt + completion,
20165
+ Number(usage.total_tokens) || prompt + completion
20166
+ );
20167
+ const weight = getModelQuotaWeight(model);
20168
+ const weighted = completion * weight;
20169
+ try {
20170
+ const db2 = getDatabase();
20171
+ db2.prepare(
20172
+ `
20173
+ INSERT INTO account_daily_usage (
20174
+ account_id, date_utc, prompt_tokens, completion_tokens, weighted_tokens, total_tokens, turns_count, updated_at
20175
+ ) VALUES (
20176
+ ?, ?, ?, ?, ?, ?, 1, datetime('now')
20177
+ )
20178
+ ON CONFLICT(account_id, date_utc) DO UPDATE SET
20179
+ prompt_tokens = prompt_tokens + excluded.prompt_tokens,
20180
+ completion_tokens = completion_tokens + excluded.completion_tokens,
20181
+ weighted_tokens = weighted_tokens + excluded.weighted_tokens,
20182
+ total_tokens = total_tokens + excluded.total_tokens,
20183
+ turns_count = turns_count + 1,
20184
+ updated_at = datetime('now')
20185
+ `
20186
+ ).run(accountId, dateUtc, prompt, completion, weighted, total);
20187
+ } catch (err) {
20188
+ }
20189
+ }
20190
+ function getAccountDailyUsage(accountId, dateUtc = getUtcDateString()) {
20191
+ try {
20192
+ const db2 = getDatabase();
20193
+ const row = db2.prepare(
20194
+ `SELECT prompt_tokens, completion_tokens, weighted_tokens, total_tokens, turns_count
20195
+ FROM account_daily_usage
20196
+ WHERE account_id = ? AND date_utc = ?`
20197
+ ).get(accountId, dateUtc);
20198
+ const promptTokens = Number(row?.prompt_tokens) || 0;
20199
+ const completionTokens = Number(row?.completion_tokens) || 0;
20200
+ const weightedTokens = Number(row?.weighted_tokens) || completionTokens;
20201
+ const totalTokens = Number(row?.total_tokens) || 0;
20202
+ const turnsCount = Number(row?.turns_count) || 0;
20203
+ const usagePercent = Math.min(
20204
+ 100,
20205
+ Math.round(weightedTokens / STANDARD_DAILY_TOKEN_BASELINE * 100)
20206
+ );
20207
+ return {
20208
+ accountId,
20209
+ dateUtc,
20210
+ promptTokens,
20211
+ completionTokens,
20212
+ weightedTokens,
20213
+ totalTokens,
20214
+ turnsCount,
20215
+ usagePercent
20216
+ };
20217
+ } catch {
20218
+ return {
20219
+ accountId,
20220
+ dateUtc,
20221
+ promptTokens: 0,
20222
+ completionTokens: 0,
20223
+ weightedTokens: 0,
20224
+ totalTokens: 0,
20225
+ turnsCount: 0,
20226
+ usagePercent: 0
20227
+ };
20228
+ }
20229
+ }
20230
+ function getAllAccountsDailyUsage(dateUtc = getUtcDateString()) {
20231
+ const result = /* @__PURE__ */ new Map();
20232
+ try {
20233
+ const db2 = getDatabase();
20234
+ const rows = db2.prepare(
20235
+ `SELECT account_id, prompt_tokens, completion_tokens, weighted_tokens, total_tokens, turns_count
20236
+ FROM account_daily_usage
20237
+ WHERE date_utc = ?`
20238
+ ).all(dateUtc);
20239
+ for (const row of rows) {
20240
+ const promptTokens = Number(row.prompt_tokens) || 0;
20241
+ const completionTokens = Number(row.completion_tokens) || 0;
20242
+ const weightedTokens = Number(row.weighted_tokens) || completionTokens;
20243
+ const totalTokens = Number(row.total_tokens) || 0;
20244
+ const turnsCount = Number(row.turns_count) || 0;
20245
+ const usagePercent = Math.min(
20246
+ 100,
20247
+ Math.round(weightedTokens / STANDARD_DAILY_TOKEN_BASELINE * 100)
20248
+ );
20249
+ result.set(row.account_id, {
20250
+ accountId: row.account_id,
20251
+ dateUtc,
20252
+ promptTokens,
20253
+ completionTokens,
20254
+ weightedTokens,
20255
+ totalTokens,
20256
+ turnsCount,
20257
+ usagePercent
20258
+ });
20259
+ }
20260
+ } catch {
20261
+ }
20262
+ return result;
20263
+ }
20264
+ function getPoolDailyUsageSummary(targetAccountIds, dateUtc = getUtcDateString()) {
20265
+ const accounts = targetAccountIds && targetAccountIds.length > 0 ? targetAccountIds : loadAccounts().map((a) => a.id);
20266
+ const accountCount = Math.max(1, accounts.length);
20267
+ const poolCapacityTokens = accountCount * STANDARD_DAILY_TOKEN_BASELINE;
20268
+ const usageMap = getAllAccountsDailyUsage(dateUtc);
20269
+ let totalPromptTokens = 0;
20270
+ let totalCompletionTokens = 0;
20271
+ let totalWeightedTokens = 0;
20272
+ let totalTokens = 0;
20273
+ let totalTurns = 0;
20274
+ for (const id of accounts) {
20275
+ const usage = usageMap.get(id);
20276
+ if (usage) {
20277
+ totalPromptTokens += usage.promptTokens;
20278
+ totalCompletionTokens += usage.completionTokens;
20279
+ totalWeightedTokens += usage.weightedTokens;
20280
+ totalTokens += usage.totalTokens;
20281
+ totalTurns += usage.turnsCount;
20282
+ }
20283
+ }
20284
+ const poolUsagePercent = Math.min(
20285
+ 100,
20286
+ Math.round(totalWeightedTokens / poolCapacityTokens * 100)
20287
+ );
20288
+ return {
20289
+ dateUtc,
20290
+ totalPromptTokens,
20291
+ totalCompletionTokens,
20292
+ totalWeightedTokens,
20293
+ totalTokens,
20294
+ totalTurns,
20295
+ poolCapacityTokens,
20296
+ poolUsagePercent
20297
+ };
20298
+ }
20299
+ var STANDARD_DAILY_TOKEN_BASELINE, DAILY_ACCOUNT_TOKEN_BASELINE, MODEL_DAILY_OUTPUT_BASELINES;
20300
+ var init_daily_usage = __esm({
20301
+ "src/core/daily-usage.ts"() {
20302
+ "use strict";
20303
+ init_database();
20304
+ init_accounts();
20305
+ init_model_alias();
20306
+ STANDARD_DAILY_TOKEN_BASELINE = 5e5;
20307
+ DAILY_ACCOUNT_TOKEN_BASELINE = STANDARD_DAILY_TOKEN_BASELINE;
20308
+ MODEL_DAILY_OUTPUT_BASELINES = {
20309
+ max: 28e4,
20310
+ plus: 5e5,
20311
+ omni: 62e4
20312
+ };
20313
+ }
20314
+ });
20315
+
19923
20316
  // src/routes/chat/streaming.ts
19924
20317
  import { stream as honoStream } from "hono/streaming";
19925
20318
  function firstString(...values) {
@@ -20425,6 +20818,9 @@ Please retry your tool call(s) with correct JSON and valid tool names from the a
20425
20818
  mode: "non-stream",
20426
20819
  context: currentTokenEstimationContext
20427
20820
  });
20821
+ if (activeAccountId && usage) {
20822
+ recordTurnUsage(activeAccountId, usage, body.model);
20823
+ }
20428
20824
  if (pendingParentId) {
20429
20825
  rememberParent(pendingParentId);
20430
20826
  }
@@ -20520,6 +20916,9 @@ async function processStreamingResponse(params) {
20520
20916
  let activeReader = null;
20521
20917
  let invalidInputSameAccountRetries = 0;
20522
20918
  let chatInProgressSameAccountRetries = 0;
20919
+ let antiBotSameAccountRetries = 0;
20920
+ const totalConfiguredAccounts = loadAccounts().length;
20921
+ let quotaRotationsLeft = Math.min(Math.max(1, totalConfiguredAccounts - 1), 6);
20523
20922
  let streamCompletedOk = false;
20524
20923
  let stoppedByToolCap = false;
20525
20924
  const runDisconnectTeardown = () => {
@@ -20877,13 +21276,18 @@ async function processStreamingResponse(params) {
20877
21276
  reason: "network"
20878
21277
  }
20879
21278
  ) : null;
20880
- if (!normalizedError || clientDisconnected && !gracePending || c.req.raw.signal.aborted || retryContext.retriesLeft <= 0 || !midStreamRetry || emittedModelOutput) {
21279
+ if (!normalizedError || clientDisconnected && !gracePending || c.req.raw.signal.aborted || !midStreamRetry || emittedModelOutput) {
20881
21280
  return false;
20882
21281
  }
20883
21282
  const policy = classifyRetryAction(normalizedError, {
20884
21283
  requestAborted: c.req.raw.signal.aborted
20885
21284
  });
20886
21285
  if (!policy.retryable) return false;
21286
+ const isRealQuotaError = policy.reason === "quota_or_rate_limit" && (policy.accountCooldownReason === "RateLimited" || policy.accountCooldownReason === "QuotaExceeded");
21287
+ const hasBudget = isRealQuotaError ? quotaRotationsLeft > 0 : retryContext.retriesLeft > 0;
21288
+ if (!hasBudget) {
21289
+ return false;
21290
+ }
20887
21291
  if (logger.isLevelEnabled("info")) {
20888
21292
  console.log(
20889
21293
  `\u{1F9ED} [Chat] Stream recovery policy | account=${currentAccountId} | reason=${policy.reason} | retryable=${policy.retryable} | switch=${policy.switchAccount} | newChat=${policy.forceNewChat} | fullPrompt=${policy.retryWithFullPrompt} | retryAfter=${policy.retryAfterMs}ms`
@@ -20892,9 +21296,13 @@ async function processStreamingResponse(params) {
20892
21296
  if (policy.reason === "corrupted_chat_history") {
20893
21297
  invalidateLogicalThreadParent(midStreamRetry.sessionId);
20894
21298
  }
20895
- retryContext.retriesLeft--;
21299
+ if (isRealQuotaError) {
21300
+ quotaRotationsLeft--;
21301
+ } else {
21302
+ retryContext.retriesLeft--;
21303
+ }
20896
21304
  console.warn(
20897
- `\u{1F504} [Chat] Stream recovery | account=${currentAccountId} | reason=${policy.reason} | error=${normalizedError.message.substring(0, 150)} | retries_left=${retryContext.retriesLeft}`
21305
+ `\u{1F504} [Chat] Stream recovery | account=${currentAccountId} | reason=${policy.reason} | error=${normalizedError.message.substring(0, 150)} | retries_left=${isRealQuotaError ? quotaRotationsLeft : retryContext.retriesLeft}`
20898
21306
  );
20899
21307
  const retryInvalidInputOnSameAccount = shouldRetryInvalidInputOnSameAccount(
20900
21308
  policy.reason,
@@ -20916,7 +21324,16 @@ async function processStreamingResponse(params) {
20916
21324
  );
20917
21325
  return false;
20918
21326
  }
20919
- const switchAccount = policy.switchAccount && !retryInvalidInputOnSameAccount;
21327
+ let switchAccount = policy.switchAccount && !retryInvalidInputOnSameAccount;
21328
+ if (policy.reason === "anti_bot") {
21329
+ if (antiBotSameAccountRetries === 0) {
21330
+ antiBotSameAccountRetries++;
21331
+ switchAccount = false;
21332
+ } else {
21333
+ switchAccount = true;
21334
+ markAccountRateLimited(currentAccountId, 6e4, "AntiBotWafChallenge");
21335
+ }
21336
+ }
20920
21337
  if (switchAccount && (policy.accountCooldownMs ?? 0) > 0) {
20921
21338
  markAccountRateLimited(
20922
21339
  currentAccountId,
@@ -20928,6 +21345,33 @@ async function processStreamingResponse(params) {
20928
21345
  Promise.resolve().then(() => (init_playwright(), playwright_exports)).then(({ refreshAccountToken: refreshAccountToken2 }) => refreshAccountToken2(currentAccountId)).catch(() => {
20929
21346
  });
20930
21347
  }
21348
+ if (currentUiSessionId && targetResponseId) {
21349
+ const streamData = getStream(completionId);
21350
+ const stopHeaders = streamData?.headers;
21351
+ if (stopHeaders?.cookie && stopHeaders["user-agent"]) {
21352
+ void requestQwenTextInBrowser(
21353
+ currentAccountId,
21354
+ "POST",
21355
+ `/api/v2/chat/completions/stop?chat_id=${encodeURIComponent(currentUiSessionId)}`,
21356
+ buildQwenRequestHeaders({
21357
+ cookie: stopHeaders.cookie,
21358
+ userAgent: stopHeaders["user-agent"],
21359
+ bxUa: stopHeaders["bx-ua"],
21360
+ bxUmidtoken: stopHeaders["bx-umidtoken"],
21361
+ bxV: stopHeaders["bx-v"],
21362
+ chatSessionId: currentUiSessionId
21363
+ }),
21364
+ JSON.stringify({
21365
+ chat_id: currentUiSessionId,
21366
+ response_id: targetResponseId
21367
+ }),
21368
+ {
21369
+ referrer: qwenUrl(`/c/${encodeURIComponent(currentUiSessionId)}`),
21370
+ noMutexRecovery: true
21371
+ }
21372
+ ).catch(() => void 0);
21373
+ }
21374
+ }
20931
21375
  retryContext.releaseAccountLease?.();
20932
21376
  retryContext.releaseAccountLease = null;
20933
21377
  await reader.cancel().catch(() => void 0);
@@ -21775,6 +22219,9 @@ Please retry your tool call(s) with correct JSON and valid tool names from the a
21775
22219
  mode: "stream",
21776
22220
  context: currentTokenEstimationContext
21777
22221
  });
22222
+ if (activeAccountId && usage) {
22223
+ recordTurnUsage(activeAccountId, usage, body.model);
22224
+ }
21778
22225
  } else {
21779
22226
  if (isToolcallDebugEnabled()) {
21780
22227
  logger.debug(
@@ -21980,6 +22427,7 @@ var init_streaming = __esm({
21980
22427
  init_qwen_url();
21981
22428
  init_qwen();
21982
22429
  init_account();
22430
+ init_accounts();
21983
22431
  init_account_manager();
21984
22432
  init_account_concurrency();
21985
22433
  init_retry_policy();
@@ -21995,6 +22443,7 @@ var init_streaming = __esm({
21995
22443
  init_errors2();
21996
22444
  init_retry_policy();
21997
22445
  init_token_estimation_metrics();
22446
+ init_daily_usage();
21998
22447
  init_context_meter();
21999
22448
  init_helpers();
22000
22449
  MAX_INITIAL_PROTOCOL_BYTES = 64 * 1024;
@@ -22434,7 +22883,7 @@ async function chatCompletions(c) {
22434
22883
  updateLogicalThread: parallelEscape ? false : ctx.updateLogicalThread,
22435
22884
  allowThreadReuse: ctx.allowThreadReuse,
22436
22885
  forceNewChat: parallelEscape,
22437
- preferredAccountId: void 0,
22886
+ preferredAccountId: c.req.header("x-qwenproxy-account") || void 0,
22438
22887
  messageCount: msgCount,
22439
22888
  fullMessageCount: parsed.messageCount,
22440
22889
  toolsCount: declaredTools.length || void 0,
@@ -25936,7 +26385,7 @@ function getAppVersion() {
25936
26385
  }
25937
26386
  } catch {
25938
26387
  }
25939
- cachedVersion = "v1.5.3";
26388
+ cachedVersion = "v1.6.1";
25940
26389
  return cachedVersion;
25941
26390
  }
25942
26391
  var cachedVersion;
@@ -26645,7 +27094,8 @@ var init_server = __esm({
26645
27094
  captchasDetected: Number(metrics.get("captcha.challenges.detected")?.value ?? 0),
26646
27095
  captchasSolved: Number(metrics.get("captcha.solves.succeeded")?.value ?? 0),
26647
27096
  chatsCleaned: Number(metrics.get("chats.cleaned")?.value ?? 0)
26648
- }
27097
+ },
27098
+ dailyUsage: (await Promise.resolve().then(() => (init_daily_usage(), daily_usage_exports))).getPoolDailyUsageSummary()
26649
27099
  });
26650
27100
  });
26651
27101
  app5.get("/diagnostics/tokens", async (c) => {
@@ -27407,12 +27857,17 @@ async function fetchProxyStatus() {
27407
27857
  }
27408
27858
  } catch {
27409
27859
  }
27860
+ const dailyUsageMap = getAllAccountsDailyUsage();
27410
27861
  const accounts = cachedAccounts.map((acc) => {
27411
27862
  const concurrency = concurrencyMap.get(acc.id);
27863
+ const daily = dailyUsageMap.get(acc.id);
27412
27864
  return {
27413
27865
  ...acc,
27414
27866
  activeStreams: concurrency?.active ?? 0,
27415
- streamLimit: concurrency?.limit ?? config.concurrency.maxStreamsPerAccount
27867
+ streamLimit: concurrency?.limit ?? config.concurrency.maxStreamsPerAccount,
27868
+ dailyTokens: daily?.completionTokens ?? 0,
27869
+ dailyUsagePercent: daily?.usagePercent ?? 0,
27870
+ dailyTurns: daily?.turnsCount ?? 0
27416
27871
  };
27417
27872
  });
27418
27873
  let rssMb = 0;
@@ -27471,6 +27926,7 @@ async function fetchProxyStatus() {
27471
27926
  cacheHitRatio,
27472
27927
  cacheBytesSaved
27473
27928
  },
27929
+ dailyUsage: getPoolDailyUsageSummary(cachedAccounts.map((a) => a.id)),
27474
27930
  accounts
27475
27931
  };
27476
27932
  }
@@ -27611,6 +28067,7 @@ var init_proxy_client = __esm({
27611
28067
  init_account_concurrency();
27612
28068
  init_memory_usage();
27613
28069
  init_metrics();
28070
+ init_daily_usage();
27614
28071
  init_model_alias();
27615
28072
  init_model_alias();
27616
28073
  cachedAccounts = [];
@@ -28341,12 +28798,24 @@ var init_status_view = __esm({
28341
28798
  const poolPct = accounts.length > 0 ? Math.round(availableCount / accounts.length * 100) : 0;
28342
28799
  const poolColor = poolPct >= 70 ? theme.green : poolPct >= 40 ? theme.yellow : theme.red;
28343
28800
  const poolBar = renderProgressBar(poolPct, 8, poolColor);
28801
+ const poolDailyUsage = data?.dailyUsage;
28802
+ const poolUsagePct = poolDailyUsage?.poolUsagePercent ?? 0;
28803
+ const poolUsageColor = poolUsagePct >= 80 ? theme.red : poolUsagePct >= 40 ? theme.yellow : theme.cyan;
28804
+ const poolUsageBar = renderProgressBar(poolUsagePct, 8, poolUsageColor);
28344
28805
  const innerRightW = Math.max(30, rightW - 2);
28345
- const emailWidth = Math.max(16, Math.min(20, innerRightW - 27));
28806
+ const showUsageCol = innerRightW >= 52;
28807
+ const emailWidth = showUsageCol ? Math.max(14, Math.min(18, innerRightW - 33)) : Math.max(16, Math.min(20, innerRightW - 27));
28346
28808
  const rightContent = [
28347
28809
  ` ${theme.bold("Disponibilidade:")} [${poolBar}] ${poolColor(`${availableCount}/${accounts.length} (${poolPct}%)`)}`,
28810
+ ...poolDailyUsage ? [
28811
+ ` ${theme.bold("Cota Pool Hoje:")} [${poolUsageBar}] ${poolUsageColor(
28812
+ `${poolUsagePct}%`
28813
+ )} ${theme.dim(
28814
+ `(~${Math.round(poolDailyUsage.totalTokens / 1e3)}k tok)`
28815
+ )}`
28816
+ ] : [],
28348
28817
  ` ${theme.dim("\u2500".repeat(Math.max(32, innerRightW - 2)))}`,
28349
- ` ${theme.dim(`# ${pad("Conta", emailWidth)} Carga Status`)}`,
28818
+ ` ${theme.dim(`# ${pad("Conta", emailWidth)} Carga ${showUsageCol ? "Uso% " : ""}Status`)}`,
28350
28819
  ` ${theme.dim("\u2500".repeat(Math.max(32, innerRightW - 2)))}`
28351
28820
  ];
28352
28821
  if (accounts.length === 0) {
@@ -28358,7 +28827,17 @@ var init_status_view = __esm({
28358
28827
  const name = pad(truncate(acc.emailOrName, emailWidth - 1), emailWidth);
28359
28828
  const active = acc.activeStreams || 0;
28360
28829
  const limit = acc.streamLimit || 1;
28361
- const loadBadge = active > 0 ? theme.yellow(`[${active}/${limit}]`) : theme.dim(`[0/${limit}]`);
28830
+ const usagePct = acc.dailyUsagePercent ?? 0;
28831
+ let loadBadge;
28832
+ if (active > 0) {
28833
+ loadBadge = theme.yellow(`[${active}/${limit}]`);
28834
+ } else if (!showUsageCol && usagePct > 0) {
28835
+ const pctColor = usagePct >= 80 ? theme.red : usagePct >= 40 ? theme.yellow : theme.cyan;
28836
+ loadBadge = pctColor(pad(`[${usagePct}%]`, 5));
28837
+ } else {
28838
+ loadBadge = theme.dim(`[0/${limit}]`);
28839
+ }
28840
+ const usageCol = showUsageCol ? (usagePct > 0 ? usagePct >= 80 ? theme.red(pad(`${usagePct}%`, 5)) : usagePct >= 40 ? theme.yellow(pad(`${usagePct}%`, 5)) : theme.cyan(pad(`${usagePct}%`, 5)) : theme.dim(pad("0%", 5))) + " " : "";
28362
28841
  let status = theme.green(`${glyphs.bullet} Pronto`);
28363
28842
  if (active > 0 && !acc.onCooldown && acc.headersReady) {
28364
28843
  status = theme.yellow(`\u25CF Gerando `);
@@ -28375,7 +28854,7 @@ var init_status_view = __esm({
28375
28854
  } else if (!acc.headersReady) {
28376
28855
  status = acc.isInitialized ? theme.yellow(`\u25D0 Aquecendo...`) : theme.muted(`\u25CB Standby`);
28377
28856
  }
28378
- rightContent.push(` ${num}${name} ${loadBadge} ${status}`);
28857
+ rightContent.push(` ${num}${name} ${loadBadge} ${usageCol}${status}`);
28379
28858
  });
28380
28859
  }
28381
28860
  const summaryParts = [];
@@ -32757,6 +33236,7 @@ var init_accounts_view = __esm({
32757
33236
  init_accounts();
32758
33237
  init_server_manager();
32759
33238
  init_config();
33239
+ init_status_view();
32760
33240
  AccountsView = class {
32761
33241
  id = "accounts";
32762
33242
  title = "Contas";
@@ -33522,9 +34002,11 @@ var init_accounts_view = __esm({
33522
34002
  if (this.selectedIndex >= accounts.length && accounts.length > 0) {
33523
34003
  this.selectedIndex = accounts.length - 1;
33524
34004
  }
34005
+ const showUsageCol = leftW >= 48;
34006
+ const headerUsage = showUsageCol ? "Uso% " : "";
33525
34007
  const leftContent = [
33526
34008
  "",
33527
- ` ${theme.dim("# Conta Status")}`,
34009
+ ` ${theme.dim(`# Conta ${headerUsage}Status`)}`,
33528
34010
  ` ${theme.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500")}`
33529
34011
  ];
33530
34012
  if (accounts.length === 0) {
@@ -33569,7 +34051,9 @@ var init_accounts_view = __esm({
33569
34051
  } else if (!acc.headersReady) {
33570
34052
  status = acc.isInitialized ? theme.yellow(`\u25D0 Aquecendo...`) : theme.muted(`\u25CB Standby `);
33571
34053
  }
33572
- const line = `${pointer}${num}${name}${status}`;
34054
+ const usagePct = acc.dailyUsagePercent ?? 0;
34055
+ const usagePctStr = showUsageCol ? (usagePct > 0 ? usagePct >= 80 ? theme.red(pad(`${usagePct}%`, 5)) : usagePct >= 40 ? theme.yellow(pad(`${usagePct}%`, 5)) : theme.cyan(pad(`${usagePct}%`, 5)) : theme.dim(pad("0%", 5))) + " " : "";
34056
+ const line = `${pointer}${num}${name}${usagePctStr}${status}`;
33573
34057
  if (isHovered) {
33574
34058
  leftContent.push(theme.bgHover(line));
33575
34059
  } else if (isFocused) {
@@ -33610,6 +34094,12 @@ var init_accounts_view = __esm({
33610
34094
  rightContent.push(` ${theme.bold("Conta:")} ${theme.cyan(email)}`);
33611
34095
  rightContent.push(` ${theme.bold("Sistema ID:")} ${theme.muted(selected.id.slice(0, 14))}`);
33612
34096
  rightContent.push(` ${theme.bold("N\xEDvel:")} ${selected.priority}`);
34097
+ const usagePct = selected.dailyUsagePercent ?? 0;
34098
+ const usageTokens = selected.dailyTokens ?? 0;
34099
+ const usageTurns = selected.dailyTurns ?? 0;
34100
+ const usageColor = usagePct >= 80 ? theme.red : usagePct >= 40 ? theme.yellow : theme.cyan;
34101
+ const usageBar = renderProgressBar(usagePct, 6, usageColor);
34102
+ rightContent.push(` ${theme.bold("Cota Hoje:")} [${usageBar}] ${usageColor(`${usagePct}%`)} ${theme.dim(`(~${Math.round(usageTokens / 1e3)}k / ${usageTurns} reqs)`)}`);
33613
34103
  const cdStatus = selected.onCooldown ? theme.yellow(`[!] Cooldown ${Math.round(selected.remainingCooldownMs / 6e4)}m`) : theme.green(`${glyphs.check} Dispon\xEDvel`);
33614
34104
  rightContent.push(` ${theme.bold("Estado:")} ${cdStatus}`);
33615
34105
  const hStatus = selected.headersReady ? theme.green(`${glyphs.check} Capturados`) : selected.isInitialized ? theme.yellow(`\u25D0 Aquecendo...`) : theme.muted(`${glyphs.circle} Standby (Sob Demanda)`);
@@ -34418,6 +34908,7 @@ var init_app = __esm({
34418
34908
 
34419
34909
  // src/index.ts
34420
34910
  init_paths();
34911
+ init_process_guard();
34421
34912
  init_server();
34422
34913
  import dotenv2 from "dotenv";
34423
34914
  import fs26 from "node:fs";
@@ -34433,24 +34924,7 @@ if (fs26.existsSync(envPath)) {
34433
34924
  } else {
34434
34925
  dotenv2.config({ quiet: true });
34435
34926
  }
34436
- process.on("uncaughtException", async (error) => {
34437
- const { isPlaywrightAlreadyClosedError: isPlaywrightAlreadyClosedError2 } = await Promise.resolve().then(() => (init_playwright(), playwright_exports));
34438
- if (isPlaywrightAlreadyClosedError2(error)) {
34439
- const msg = error instanceof Error ? error.message : typeof error === "object" && error !== null && "message" in error ? String(error.message) : String(error);
34440
- console.warn(`\u26A0\uFE0F [Playwright] Handled benign driver teardown exception: ${msg}`);
34441
- return;
34442
- }
34443
- console.error("\u274C [Process] Uncaught Exception:", error);
34444
- });
34445
- process.on("unhandledRejection", async (reason) => {
34446
- const { isPlaywrightAlreadyClosedError: isPlaywrightAlreadyClosedError2 } = await Promise.resolve().then(() => (init_playwright(), playwright_exports));
34447
- if (isPlaywrightAlreadyClosedError2(reason)) {
34448
- const msg = reason instanceof Error ? reason.message : typeof reason === "object" && reason !== null && "message" in reason ? String(reason.message) : String(reason);
34449
- console.warn(`\u26A0\uFE0F [Playwright] Handled benign driver teardown rejection: ${msg}`);
34450
- return;
34451
- }
34452
- console.error("\u274C [Process] Unhandled Rejection:", reason);
34453
- });
34927
+ installProcessTeardownGuards("Playwright");
34454
34928
  var isTui = process.argv.includes("--tui") || process.env.QWEN_TUI === "true";
34455
34929
  if (isTui) {
34456
34930
  const { TuiApp: TuiApp2 } = await Promise.resolve().then(() => (init_app(), app_exports));