qwenproxy-cli 1.6.9 → 1.6.11

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.
@@ -1236,6 +1236,26 @@ function clearAllAccountCooldowns() {
1236
1236
  cooldowns.clear();
1237
1237
  return count;
1238
1238
  }
1239
+ function getAccountCooldownInfo(accountId) {
1240
+ const entry = cooldowns.get(accountId);
1241
+ if (!entry) return null;
1242
+ const remaining = entry.until - Date.now();
1243
+ if (remaining <= 0) {
1244
+ cooldowns.delete(accountId);
1245
+ if (accountId !== "global") {
1246
+ try {
1247
+ updateAccountCooldown(accountId, 0, null);
1248
+ } catch (err) {
1249
+ console.error(
1250
+ `\u274C [AccountManager] Failed to clear expired cooldown in DB:`,
1251
+ err.message
1252
+ );
1253
+ }
1254
+ }
1255
+ return null;
1256
+ }
1257
+ return { onCooldown: true, remainingMs: remaining, reason: entry.reason };
1258
+ }
1239
1259
  function markAccountHeadersReady(accountId) {
1240
1260
  if (!accountId || accountId === "global") return;
1241
1261
  headersReadyAccounts.add(accountId);
@@ -3256,7 +3276,19 @@ async function isPageLoggedIn(page, timeoutMs = SESSION_PROBE_NAVIGATION_TIMEOUT
3256
3276
  if (localStorage.getItem("qwen_token_logged_out_marker")) {
3257
3277
  return false;
3258
3278
  }
3259
- const res = await fetch("/api/v1/auths/", { method: "GET" });
3279
+ const lsToken = (localStorage.getItem("token") || "").trim();
3280
+ if (lsToken && !/(?:^|;\s*)token=/.test(document.cookie || "")) {
3281
+ try {
3282
+ document.cookie = `token=${encodeURIComponent(lsToken)}; path=/; domain=.qwen.ai; max-age=31536000`;
3283
+ } catch {
3284
+ }
3285
+ }
3286
+ const authHeaders = lsToken ? { Authorization: `Bearer ${lsToken}` } : {};
3287
+ const res = await fetch("/api/v1/auths/", {
3288
+ method: "GET",
3289
+ headers: authHeaders,
3290
+ credentials: "include"
3291
+ });
3260
3292
  if (res.status !== 200) return false;
3261
3293
  const json = await res.json().catch(() => null);
3262
3294
  if (!json) return false;
@@ -3274,6 +3306,7 @@ async function isPageLoggedIn(page, timeoutMs = SESSION_PROBE_NAVIGATION_TIMEOUT
3274
3306
  try {
3275
3307
  const settingsRes = await fetch("/api/v2/users/user/settings", {
3276
3308
  method: "GET",
3309
+ headers: authHeaders,
3277
3310
  credentials: "include",
3278
3311
  signal: AbortSignal.timeout(3e3)
3279
3312
  });
@@ -3876,8 +3909,12 @@ async function initPlaywrightForAccount(rawAccount, headless = config.playwright
3876
3909
  if (accountPages.has(account.id)) {
3877
3910
  return;
3878
3911
  }
3879
- await evictIdlePlaywrightContextsToLimit().catch(() => {
3880
- });
3912
+ if (config.playwright.maxActiveContexts > 0) {
3913
+ await evictIdlePlaywrightContextsToLimit(
3914
+ Math.max(1, config.playwright.maxActiveContexts - 1)
3915
+ ).catch(() => {
3916
+ });
3917
+ }
3881
3918
  const profilePath = getAccountProfilePath(account.id);
3882
3919
  const fingerprint = getFingerprintProfile(account.id);
3883
3920
  const { engine, channel } = resolveBrowserEngine(browserType);
@@ -4102,8 +4139,15 @@ async function initPlaywrightForAccount(rawAccount, headless = config.playwright
4102
4139
  const hasAuthCookie = cookies.some(
4103
4140
  (c) => c.name.toLowerCase().includes("token") || c.name.toLowerCase().includes("session")
4104
4141
  );
4142
+ let initialLoginAttempted = false;
4105
4143
  if (!hasAuthCookie && account.email && account.password) {
4106
- await loginToQwen(account.id, account.email, account.password);
4144
+ initialLoginAttempted = true;
4145
+ const loggedIn = await loginToQwen(account.id, account.email, account.password);
4146
+ if (!loggedIn) {
4147
+ throw new Error(
4148
+ `Authentication failed for account ${maskEmail(account.email)}`
4149
+ );
4150
+ }
4107
4151
  }
4108
4152
  try {
4109
4153
  await acctPage.goto(qwenUrl("/"), {
@@ -4113,13 +4157,21 @@ async function initPlaywrightForAccount(rawAccount, headless = config.playwright
4113
4157
  await sleep2(1500);
4114
4158
  const currentUrl = acctPage.url();
4115
4159
  const isAuthUrl = currentUrl.includes("/auth") || currentUrl.includes("/login");
4116
- if (isAuthUrl && account.email && account.password) {
4160
+ if (isAuthUrl && !initialLoginAttempted && account.email && account.password) {
4117
4161
  console.log(
4118
4162
  `[Playwright] Initial login needed for ${maskEmail(account.email)}, authenticating...`
4119
4163
  );
4120
- await loginToQwen(account.id, account.email, account.password);
4164
+ const loggedIn = await loginToQwen(account.id, account.email, account.password);
4165
+ if (!loggedIn) {
4166
+ throw new Error(
4167
+ `Authentication failed for account ${maskEmail(account.email)}`
4168
+ );
4169
+ }
4121
4170
  }
4122
4171
  } catch (err) {
4172
+ if (err?.message?.includes("Authentication failed for account")) {
4173
+ throw err;
4174
+ }
4123
4175
  console.warn(
4124
4176
  `\u26A0\uFE0F [Playwright] Initial navigation check for ${maskEmail(account.email)}: ${err.message}`
4125
4177
  );
@@ -4131,7 +4183,7 @@ async function initPlaywrightForAccount(rawAccount, headless = config.playwright
4131
4183
  if (!acctPage.isClosed()) {
4132
4184
  try {
4133
4185
  const currentUrl = new URL(acctPage.url());
4134
- if (currentUrl.origin !== qwenOrigin() || currentUrl.pathname !== "/") {
4186
+ if (currentUrl.origin !== qwenOrigin() || currentUrl.pathname.includes("/auth") || currentUrl.pathname.includes("/login") || currentUrl.pathname.includes("_____tmd_____")) {
4135
4187
  await acctPage.goto(qwenUrl("/"), {
4136
4188
  waitUntil: "domcontentloaded",
4137
4189
  timeout: config.timeouts.navigation
@@ -4149,6 +4201,8 @@ async function initPlaywrightForAccount(rawAccount, headless = config.playwright
4149
4201
  } finally {
4150
4202
  release();
4151
4203
  }
4204
+ await evictIdlePlaywrightContextsToLimit().catch(() => {
4205
+ });
4152
4206
  })();
4153
4207
  try {
4154
4208
  await initPromise;
@@ -4262,10 +4316,30 @@ async function loginToQwen(accountId, email, password) {
4262
4316
  const uiResult = await loginViaUi(page, email, password);
4263
4317
  if (uiResult.success) {
4264
4318
  cookieCaches.delete(accountId);
4265
- await saveStorageState(page.context(), accountId);
4266
4319
  try {
4267
- const liveCookies = await page.context().cookies();
4268
- const tokenCookie = liveCookies.find((c) => c.name === "token");
4320
+ let liveCookies = await page.context().cookies();
4321
+ let tokenCookie = liveCookies.find((c) => c.name === "token");
4322
+ if (!tokenCookie) {
4323
+ const lsTok = uiResult.token || await page.evaluate(() => (localStorage.getItem("token") || "").trim()).catch(() => "");
4324
+ if (lsTok) {
4325
+ await page.context().addCookies([
4326
+ {
4327
+ name: "token",
4328
+ value: lsTok,
4329
+ domain: ".qwen.ai",
4330
+ path: "/",
4331
+ expires: Math.floor(Date.now() / 1e3) + 31536e3,
4332
+ httpOnly: false,
4333
+ secure: true,
4334
+ sameSite: "Lax"
4335
+ }
4336
+ ]).catch(() => {
4337
+ });
4338
+ liveCookies = await page.context().cookies();
4339
+ tokenCookie = liveCookies.find((c) => c.name === "token");
4340
+ }
4341
+ }
4342
+ await saveStorageState(page.context(), accountId);
4269
4343
  if (tokenCookie) {
4270
4344
  const cookieStr = liveCookies.map((c) => `${c.name}=${c.value}`).join("; ");
4271
4345
  const cache = getHeaderCache(accountId);
@@ -4274,7 +4348,7 @@ async function loginToQwen(accountId, email, password) {
4274
4348
  markAccountHeadersReady(accountId);
4275
4349
  const { saveAuthSession: saveAuthSession2 } = await Promise.resolve().then(() => (init_database(), database_exports));
4276
4350
  const exp = parseJwtExpiry(tokenCookie.value);
4277
- let rTok = uiResult.refreshToken;
4351
+ let rTok = uiResult.refreshToken || liveCookies.find((c) => c.name === "refresh_token")?.value;
4278
4352
  if (!rTok) {
4279
4353
  try {
4280
4354
  rTok = await page.evaluate(() => localStorage.getItem("refresh_token") || "") || void 0;
@@ -4355,8 +4429,9 @@ async function loginViaApi(page, email, password) {
4355
4429
  }
4356
4430
  const signinUrl = "https://auth.qwen.ai/api/v2/auths/signin";
4357
4431
  const requestId = crypto3.randomUUID();
4432
+ const webVersion = getQwenWebVersion() || "0.3.26";
4358
4433
  const evalRes = await page.evaluate(
4359
- async ({ email: email2, password: password2, signinUrl: signinUrl2, requestId: requestId2, tz }) => {
4434
+ async ({ email: email2, password: password2, signinUrl: signinUrl2, requestId: requestId2, tz, version }) => {
4360
4435
  try {
4361
4436
  const response = await fetch(signinUrl2, {
4362
4437
  method: "POST",
@@ -4366,7 +4441,7 @@ async function loginViaApi(page, email, password) {
4366
4441
  accept: "application/json, text/plain, */*",
4367
4442
  "content-type": "application/json",
4368
4443
  source: "web",
4369
- version: getQwenWebVersion() || "0.3.26",
4444
+ version,
4370
4445
  "x-request-origin": "https://chat.qwen.ai",
4371
4446
  timezone: tz,
4372
4447
  "x-request-id": requestId2
@@ -4379,7 +4454,14 @@ async function loginViaApi(page, email, password) {
4379
4454
  return { ok: false, error: e.message };
4380
4455
  }
4381
4456
  },
4382
- { email, password: hashedPassword, signinUrl, requestId, tz: formatAsciiTimezone() }
4457
+ {
4458
+ email,
4459
+ password: hashedPassword,
4460
+ signinUrl,
4461
+ requestId,
4462
+ tz: formatAsciiTimezone(),
4463
+ version: webVersion
4464
+ }
4383
4465
  ).catch(() => null);
4384
4466
  let data = evalRes?.data;
4385
4467
  let signinSuccess = Boolean(
@@ -4388,7 +4470,7 @@ async function loginViaApi(page, email, password) {
4388
4470
  if (!signinSuccess && !data?.data?.code) {
4389
4471
  const fallbackUrl = qwenUrl("/api/v2/auths/signin");
4390
4472
  const fallbackRes = await page.evaluate(
4391
- async ({ email: email2, password: password2, fallbackUrl: fallbackUrl2, requestId: requestId2, tz }) => {
4473
+ async ({ email: email2, password: password2, fallbackUrl: fallbackUrl2, requestId: requestId2, tz, version }) => {
4392
4474
  try {
4393
4475
  const response = await fetch(fallbackUrl2, {
4394
4476
  method: "POST",
@@ -4398,7 +4480,7 @@ async function loginViaApi(page, email, password) {
4398
4480
  accept: "application/json, text/plain, */*",
4399
4481
  "content-type": "application/json",
4400
4482
  source: "web",
4401
- version: getQwenWebVersion() || "0.3.26",
4483
+ version,
4402
4484
  timezone: tz,
4403
4485
  "x-request-id": requestId2
4404
4486
  },
@@ -4410,7 +4492,14 @@ async function loginViaApi(page, email, password) {
4410
4492
  return { ok: false, error: e.message };
4411
4493
  }
4412
4494
  },
4413
- { email, password: hashedPassword, fallbackUrl, requestId, tz: formatAsciiTimezone() }
4495
+ {
4496
+ email,
4497
+ password: hashedPassword,
4498
+ fallbackUrl,
4499
+ requestId,
4500
+ tz: formatAsciiTimezone(),
4501
+ version: webVersion
4502
+ }
4414
4503
  ).catch(() => null);
4415
4504
  if (fallbackRes?.data) {
4416
4505
  data = fallbackRes.data;
@@ -4635,28 +4724,34 @@ async function loginViaUi(page, email, password) {
4635
4724
  await sleep2(500);
4636
4725
  const submitSelector = 'button.qwenchat-auth-pc-submit-button, button[type="submit"], button:has-text("Log in"), button:has-text("Sign in"), button:has-text("Fazer login"), button:has-text("Entrar"), button:has-text("Iniciar sesi\xF3n"), button:has-text("\u767B\u5F55")';
4637
4726
  const submitButton = page.locator(submitSelector).first();
4638
- await page.evaluate((sel) => {
4639
- try {
4640
- const btn = document.querySelector(sel);
4641
- if (btn) {
4642
- btn.removeAttribute("disabled");
4643
- btn.classList.remove("ant-btn-disabled");
4644
- btn.click();
4645
- }
4646
- } catch {
4647
- }
4648
- }, submitSelector).catch(() => {
4649
- });
4727
+ let submitted = false;
4650
4728
  try {
4651
4729
  if (await submitButton.isVisible({ timeout: 2e3 }).catch(() => false)) {
4652
- await submitButton.click({ force: true, timeout: 2e3 }).catch(() => {
4730
+ await submitButton.click({ timeout: 2e3 }).catch(() => {
4653
4731
  });
4732
+ submitted = true;
4654
4733
  }
4655
4734
  } catch {
4656
4735
  }
4657
- const passInput = page.locator(passwordSelector).first();
4658
- await passInput.press("Enter", { timeout: 2e3 }).catch(() => {
4659
- });
4736
+ if (!submitted) {
4737
+ await page.evaluate(() => {
4738
+ try {
4739
+ const btn = document.querySelector(
4740
+ 'button.qwenchat-auth-pc-submit-button, button[type="submit"]'
4741
+ );
4742
+ if (btn) {
4743
+ btn.removeAttribute("disabled");
4744
+ btn.classList.remove("ant-btn-disabled");
4745
+ btn.click();
4746
+ }
4747
+ } catch {
4748
+ }
4749
+ }).catch(() => {
4750
+ });
4751
+ const passInput = page.locator(passwordSelector).first();
4752
+ await passInput.press("Enter", { timeout: 2e3 }).catch(() => {
4753
+ });
4754
+ }
4660
4755
  const postSubmitDeadline = Date.now() + 15e3;
4661
4756
  while (Date.now() < postSubmitDeadline) {
4662
4757
  if (page.isClosed()) break;
@@ -4674,9 +4769,12 @@ async function loginViaUi(page, email, password) {
4674
4769
  ".ant-form-item-explain-error",
4675
4770
  ".ant-message-error",
4676
4771
  ".ant-message-notice",
4772
+ ".ant-message-custom-content",
4773
+ ".ant-notification-notice",
4677
4774
  '[role="alert"]',
4678
4775
  ".qwenchat-auth-error",
4679
- ".auth-error-message"
4776
+ ".auth-error-message",
4777
+ ".qwenchat-toast"
4680
4778
  ].join(", ");
4681
4779
  const errorEl = page.locator(errorSelector).first();
4682
4780
  if (await errorEl.isVisible().catch(() => false)) {
@@ -4690,7 +4788,15 @@ async function loginViaUi(page, email, password) {
4690
4788
  };
4691
4789
  }
4692
4790
  }
4693
- const codeSelector = 'input[placeholder*="code" i], input[placeholder*="c\xF3digo" i], input[name*="code" i]';
4791
+ const codeSelector = [
4792
+ 'input[placeholder*="code" i]',
4793
+ 'input[placeholder*="c\xF3digo" i]',
4794
+ 'input[name*="code" i]',
4795
+ ".qwenchat-verification-code-input",
4796
+ ".qwenchat-verification-code-input-cell",
4797
+ '[aria-label*="Verification code" i]',
4798
+ '[aria-label*="c\xF3digo de verifica\xE7\xE3o" i]'
4799
+ ].join(", ");
4694
4800
  const codeInput = page.locator(codeSelector).first();
4695
4801
  if (await codeInput.isVisible().catch(() => false)) {
4696
4802
  return {
@@ -4700,16 +4806,44 @@ async function loginViaUi(page, email, password) {
4700
4806
  }
4701
4807
  try {
4702
4808
  const liveCookies = await page.context().cookies();
4703
- if (liveCookies.some((c) => c.name === "token")) {
4704
- return { success: true };
4809
+ const rTokCookie = liveCookies.find((c) => c.name === "refresh_token")?.value;
4810
+ const tokCookie = liveCookies.find((c) => c.name === "token")?.value;
4811
+ if (tokCookie) {
4812
+ return { success: true, token: tokCookie, refreshToken: rTokCookie };
4705
4813
  }
4706
- } catch {
4707
- }
4708
- const currentUrl = page.url();
4709
- if (!currentUrl.includes("/auth") && !currentUrl.includes("/login")) {
4710
- if (await isPageLoggedIn(page, 3e3)) {
4711
- return { success: true };
4814
+ const currentUrl = page.url();
4815
+ if (!currentUrl.includes("/auth") && !currentUrl.includes("/login")) {
4816
+ const lsTok = await page.evaluate(() => {
4817
+ const t = (localStorage.getItem("token") || "").trim();
4818
+ if (t) {
4819
+ try {
4820
+ document.cookie = `token=${encodeURIComponent(t)}; path=/; domain=.qwen.ai; max-age=31536000`;
4821
+ } catch {
4822
+ }
4823
+ }
4824
+ return t;
4825
+ }).catch(() => "");
4826
+ if (lsTok) {
4827
+ await page.context().addCookies([
4828
+ {
4829
+ name: "token",
4830
+ value: lsTok,
4831
+ domain: ".qwen.ai",
4832
+ path: "/",
4833
+ expires: Math.floor(Date.now() / 1e3) + 31536e3,
4834
+ httpOnly: false,
4835
+ secure: true,
4836
+ sameSite: "Lax"
4837
+ }
4838
+ ]).catch(() => {
4839
+ });
4840
+ return { success: true, token: lsTok, refreshToken: rTokCookie };
4841
+ }
4842
+ if (await isPageLoggedIn(page, 3e3)) {
4843
+ return { success: true, refreshToken: rTokCookie };
4844
+ }
4712
4845
  }
4846
+ } catch {
4713
4847
  }
4714
4848
  await sleep2(1e3);
4715
4849
  }
@@ -4739,10 +4873,18 @@ async function refreshAccountToken(accountId) {
4739
4873
  } catch {
4740
4874
  }
4741
4875
  }
4876
+ if (!refreshToken) {
4877
+ try {
4878
+ const liveCookies = await page.context().cookies();
4879
+ refreshToken = liveCookies.find((c) => c.name === "refresh_token")?.value;
4880
+ } catch {
4881
+ }
4882
+ }
4742
4883
  if (refreshToken) {
4743
4884
  try {
4885
+ const webVersion = getQwenWebVersion() || "0.3.26";
4744
4886
  const refreshResult = await page.evaluate(
4745
- async ({ rTok, tz }) => {
4887
+ async ({ rTok, tz, version }) => {
4746
4888
  try {
4747
4889
  const response = await fetch("https://auth.qwen.ai/api/v2/auths/refresh", {
4748
4890
  method: "GET",
@@ -4750,7 +4892,7 @@ async function refreshAccountToken(accountId) {
4750
4892
  headers: {
4751
4893
  accept: "application/json, text/plain, */*",
4752
4894
  source: "web",
4753
- version: getQwenWebVersion() || "0.3.26",
4895
+ version,
4754
4896
  "x-request-origin": "https://chat.qwen.ai",
4755
4897
  timezone: tz,
4756
4898
  authorization: `Bearer ${rTok}`
@@ -4762,7 +4904,7 @@ async function refreshAccountToken(accountId) {
4762
4904
  return { ok: false, error: e.message };
4763
4905
  }
4764
4906
  },
4765
- { rTok: refreshToken, tz: formatAsciiTimezone() }
4907
+ { rTok: refreshToken, tz: formatAsciiTimezone(), version: webVersion }
4766
4908
  );
4767
4909
  const json = refreshResult?.data;
4768
4910
  if (json && json.success === true && json.data) {
@@ -5563,7 +5705,11 @@ function priorityOrderForEviction(accountIds) {
5563
5705
  const ranked = getAccountsByPriority(accountIds.map((id) => ({ id })));
5564
5706
  const rank = /* @__PURE__ */ new Map();
5565
5707
  for (const [index, account] of ranked.entries()) {
5566
- rank.set(account.id, index);
5708
+ if (getAccountCooldownInfo(account.id)) {
5709
+ rank.set(account.id, Number.MAX_SAFE_INTEGER);
5710
+ } else {
5711
+ rank.set(account.id, index);
5712
+ }
5567
5713
  }
5568
5714
  return orderContextsForEviction(
5569
5715
  accountIds,
@@ -5571,14 +5717,15 @@ function priorityOrderForEviction(accountIds) {
5571
5717
  (id) => lastAccountActivity.get(id) ?? 0
5572
5718
  );
5573
5719
  }
5574
- async function evictIdlePlaywrightContextsToLimit() {
5575
- const max = config.playwright.maxActiveContexts;
5576
- if (max <= 0) return 0;
5720
+ async function evictIdlePlaywrightContextsToLimit(targetLimit) {
5721
+ const configuredMax = config.playwright.maxActiveContexts;
5722
+ if (configuredMax <= 0) return 0;
5723
+ const max = targetLimit !== void 0 ? Math.max(0, targetLimit) : configuredMax;
5577
5724
  if (accountPages.size <= max) return 0;
5578
5725
  const candidates = priorityOrderForEviction(
5579
5726
  Array.from(accountPages.keys()).filter((accountId) => {
5580
5727
  if (isAccountServingStream(accountId)) return false;
5581
- if (isAccountRecentlyActive(accountId, 3e4)) return false;
5728
+ if (hasActiveAccountLease(accountId)) return false;
5582
5729
  const mutex = accountMutexes.get(accountId);
5583
5730
  return mutex?.isIdle();
5584
5731
  })
@@ -5591,6 +5738,7 @@ async function evictIdlePlaywrightContextsToLimit() {
5591
5738
  for (const candidate of candidates) {
5592
5739
  if (accountPages.size <= max) break;
5593
5740
  if (isAccountServingStream(candidate.accountId)) continue;
5741
+ if (hasActiveAccountLease(candidate.accountId)) continue;
5594
5742
  const mutex = accountMutexes.get(candidate.accountId);
5595
5743
  if (!mutex?.isIdle()) continue;
5596
5744
  await closePlaywrightForAccount(candidate.accountId).catch((error) => {
@@ -5693,10 +5841,6 @@ async function closePlaywrightForAccount(accountId) {
5693
5841
  release();
5694
5842
  }
5695
5843
  }
5696
- function isAccountRecentlyActive(accountId, maxIdleMs = 3e5) {
5697
- const last = lastAccountActivity.get(accountId) ?? 0;
5698
- return last > 0 && Date.now() - last < maxIdleMs;
5699
- }
5700
5844
  var requireLocal, contextInitHooks, ACCOUNT_MUTEX_MAX_HOLD_MS, accountMutexes, accountContexts, accountPages, inFlightAccountInits, headerCaches, HEADER_CACHE_TTL, 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;
5701
5845
  var init_playwright = __esm({
5702
5846
  "src/services/playwright.ts"() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qwenproxy-cli",
3
- "version": "1.6.9",
3
+ "version": "1.6.11",
4
4
  "description": "High-performance OpenAI & Anthropic compatible API gateway for Qwen with multi-account rotation, interactive TUI, and resilient tool calling.",
5
5
  "main": "src/index.ts",
6
6
  "bin": {
@@ -58,7 +58,7 @@
58
58
  "bugs": {
59
59
  "url": "https://github.com/johngbl/qwenproxy/issues"
60
60
  },
61
- "author": "johngbl <147551701+johngbl@users.noreply.github.com>",
61
+ "author": "johngbl",
62
62
  "license": "ISC",
63
63
  "dependencies": {
64
64
  "@hono/node-server": "^2.1.1",
package/src/api/server.ts CHANGED
@@ -883,10 +883,14 @@ export async function startServer(options?: {
883
883
  void (async () => {
884
884
  const { ensureAccountInPriority } = await import("../core/account-priority.ts");
885
885
 
886
- const targetWarmTotal = Math.min(
887
- config.playwright.startupWarmAccounts,
888
- totalAccounts,
889
- );
886
+ const maxWarmCap =
887
+ config.playwright.maxActiveContexts > 0
888
+ ? Math.min(
889
+ config.playwright.startupWarmAccounts,
890
+ config.playwright.maxActiveContexts,
891
+ )
892
+ : config.playwright.startupWarmAccounts;
893
+ const targetWarmTotal = Math.min(maxWarmCap, totalAccounts);
890
894
  let currentReadyCount = readyAccountIds.size;
891
895
 
892
896
  if (currentReadyCount < targetWarmTotal && remainingAccounts.length > 0) {
@@ -17,6 +17,6 @@ export function getAppVersion(): string {
17
17
  }
18
18
  }
19
19
  } catch {}
20
- cachedVersion = "v1.6.9";
20
+ cachedVersion = "v1.6.11";
21
21
  return cachedVersion;
22
22
  }
@@ -148,14 +148,15 @@ export class Watchdog extends EventEmitter {
148
148
  // (idle mutex, no active stream, preserves the max-active-context minimum)
149
149
  // so the next health check sees the freed RSS.
150
150
  try {
151
- const { closeIdlePlaywrightAccounts } = await import(
151
+ const { closeIdlePlaywrightAccounts, evictIdlePlaywrightContextsToLimit } = await import(
152
152
  "../services/playwright.js"
153
153
  );
154
+ const evicted = await evictIdlePlaywrightContextsToLimit(1);
154
155
  const closed = await closeIdlePlaywrightAccounts(
155
156
  config.sessionKeeper.idleMs,
156
157
  );
157
- if (closed > 0) {
158
- this.emit("recovery:ram:contexts-closed", closed);
158
+ if (evicted + closed > 0) {
159
+ this.emit("recovery:ram:contexts-closed", evicted + closed);
159
160
  }
160
161
  } catch {
161
162
  // Playwright may not be initialized (mock mode / pre-start); RSS relief
@@ -8,6 +8,7 @@ import {
8
8
  } from "../../core/account-manager.ts";
9
9
  import { markAccountSuccessful, markAccountFailed, getAccountsByPriority } from "../../core/account-priority.ts";
10
10
  import { recordWafHardBlock, noteWafRecovery } from "../../core/waf-isolation.ts";
11
+ import { isCaptchaRecoveryInBackoff } from "../../services/captcha-coordinator.ts";
11
12
  import { loadAccounts, type QwenAccount } from "../../core/accounts.ts";
12
13
  import { config, type ChatMode } from "../../core/config.ts";
13
14
  import { ClientAbortedError, UpstreamRateLimit, ValidationError } from "../../core/errors.ts";
@@ -1615,6 +1616,17 @@ async function tryCreateStreamWithRetry(
1615
1616
  invalidateLogicalThreadParent(params.sessionId);
1616
1617
  }
1617
1618
 
1619
+ // If captcha recovery on this account already failed and is in its 30s
1620
+ // backoff window, same-account retries will only hit `recovery_skipped`
1621
+ // and waste time/RAM re-sending large payloads. Exit immediately so the
1622
+ // outer loop quarantines this account and switches to a fresh one.
1623
+ if (
1624
+ policy.reason === "anti_bot" &&
1625
+ isCaptchaRecoveryInBackoff(currentAccountId)
1626
+ ) {
1627
+ return { success: false, error: err };
1628
+ }
1629
+
1618
1630
  // A generic invalid_input is often a stale/corrupted upstream chat rather
1619
1631
  // than an account failure. Rebuild it once on the same account first. If the
1620
1632
  // fresh chat fails again, the normal policy is allowed to rotate.
@@ -1259,8 +1259,8 @@ export async function processStreamingResponse(
1259
1259
  };
1260
1260
 
1261
1261
  const emitAnswerText = async (textChunk: string) => {
1262
- if (textChunk) emittedModelOutput = true;
1263
1262
  if (!toolParser) {
1263
+ if (textChunk) emittedModelOutput = true;
1264
1264
  finalContent += textChunk;
1265
1265
  writeDeltaEvent({ content: textChunk });
1266
1266
  return;
@@ -1777,6 +1777,23 @@ export async function processStreamingResponse(
1777
1777
  .replace(/\\\\/g, "\\");
1778
1778
 
1779
1779
  if (unescaped) {
1780
+ if (!emittedModelOutput && isOverloadMessage(unescaped)) {
1781
+ const overloadErr = toRetryableStreamError(
1782
+ "server_overloaded",
1783
+ unescaped,
1784
+ {
1785
+ switchAccount: true,
1786
+ forceNewChat: true,
1787
+ reason: "server_busy",
1788
+ accountCooldownMs: OVERLOAD_COOLDOWN_MS,
1789
+ accountCooldownReason: "ServerOverloaded",
1790
+ },
1791
+ );
1792
+ if (await recoverFromStreamError(overloadErr)) {
1793
+ break;
1794
+ }
1795
+ throw overloadErr;
1796
+ }
1780
1797
  const result = getIncrementalDelta(
1781
1798
  lastRawContent,
1782
1799
  unescaped,
@@ -1942,7 +1959,6 @@ export async function processStreamingResponse(
1942
1959
  if (vStr === "FINISHED") continue;
1943
1960
 
1944
1961
  if (isThinkingChunk) {
1945
- emittedModelOutput = true;
1946
1962
  reasoningBuffer += vStr;
1947
1963
  writeDeltaEvent({ reasoning_content: vStr });
1948
1964
  } else {
@@ -92,6 +92,12 @@ export async function solveChallengeOnPage(
92
92
  * a punish document that is never rendered; opening that document in the page
93
93
  * is what gives the solver a slider to drive.
94
94
  */
95
+ export function isCaptchaRecoveryInBackoff(accountId: string | undefined): boolean {
96
+ if (!accountId) return false;
97
+ const lastFailure = lastFailedRecoveryAt.get(accountId) ?? 0;
98
+ return Date.now() - lastFailure < FAILED_RECOVERY_BACKOFF_MS;
99
+ }
100
+
95
101
  export async function recoverBaxiaCaptcha(
96
102
  accountId: string | undefined,
97
103
  label: string,
@@ -126,19 +132,41 @@ export async function recoverBaxiaCaptcha(
126
132
 
127
133
  activeCaptchaRecoveries.add(accountId);
128
134
  try {
129
- const { abortActiveBrowserStreamsForAccount } = await import("./qwen.ts");
130
- abortActiveBrowserStreamsForAccount(
131
- accountId,
132
- "Account stream interrupted by captcha challenge recovery",
133
- );
134
- } catch {}
135
- try {
135
+ const { hasActiveBrowserStreamsForAccount } = await import("./qwen.ts");
136
+ const hasConcurrentStream = hasActiveBrowserStreamsForAccount(accountId);
137
+
136
138
  // recoverOnTimeout: false ensures that if the captcha solve times out,
137
139
  // withAccountPage does NOT destructively kill the browser context, which
138
140
  // previously caused "Target page, context or browser has been closed" mid-slider.
139
141
  const solved = await withAccountPage(
140
142
  accountId,
141
- (page) => solveChallengeOnPage(page, challengeUrl),
143
+ async (page) => {
144
+ // If another stream is actively running on this account's primary page,
145
+ // NEVER navigate `page` away (which would unload its DOM and kill the stream).
146
+ // Instead, try in-place modal solve first, or open an ephemeral tab in the
147
+ // same BrowserContext so cookies/x5sec are solved without touching `page`.
148
+ if (hasConcurrentStream) {
149
+ const solverOptions = {
150
+ maxAttempts: config.captcha.maxAttempts,
151
+ retryDelayMs: config.captcha.retryDelayMs,
152
+ settleMs: config.captcha.settleMs,
153
+ };
154
+ if (await solveBaxiaCaptcha(page, { ...solverOptions, waitForMs: 0 })) {
155
+ return true;
156
+ }
157
+ const ctx = typeof page.context === "function" ? page.context() : null;
158
+ if (ctx && typeof ctx.newPage === "function") {
159
+ const tempPage = await ctx.newPage();
160
+ try {
161
+ return await solveChallengeOnPage(tempPage, challengeUrl);
162
+ } finally {
163
+ await tempPage.close({ runBeforeUnload: false }).catch(() => {});
164
+ }
165
+ }
166
+ return false;
167
+ }
168
+ return await solveChallengeOnPage(page, challengeUrl);
169
+ },
142
170
  solverOperationTimeoutMs,
143
171
  solverOperationTimeoutMs,
144
172
  false,