qwenproxy-cli 1.4.0 → 1.4.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qwenproxy-cli",
3
- "version": "1.4.0",
3
+ "version": "1.4.2",
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": {
@@ -32,6 +32,8 @@
32
32
  "test:mock": "tsx --test --test-concurrency=1 --env-file=.env.test src/tests/account-concurrency.test.ts src/tests/concurrency-latest-wins.test.ts src/tests/mutex.test.ts src/tests/context-truncation.test.ts src/tests/streaming-thinking-summary.test.ts src/tests/tool-call-guard.test.ts src/tests/utils-logger-full.test.ts src/tests/logger-toolcall-errors.test.ts src/tests/logger-toolcall-disabled.test.ts src/tests/utils-json-full.test.ts src/tests/utils-session-guard-full.test.ts src/tests/core-logic-full.test.ts src/tests/core-logic-accounts.test.ts src/tests/chat-validation-full.test.ts src/tests/chat-logic-full.test.ts src/tests/account-stickiness.test.ts src/tests/accounts-security.test.ts src/tests/advanced.test.ts src/tests/auth-playwright.test.ts src/tests/auth-session-persistence.test.ts src/tests/captcha-solver.test.ts src/tests/cache-scan-regex.test.ts src/tests/chat-cleanup.test.ts src/tests/chat-cleanup-auto.test.ts src/tests/chromium-args.test.ts src/tests/config-current.test.ts src/tests/context-meter.test.ts src/tests/concurrency.test.ts src/tests/delta.test.ts src/tests/fingerprint.test.ts src/tests/human-behavior.test.ts src/tests/index.test.ts src/tests/json-recovery.test.ts src/tests/jsonFix.test.ts src/tests/memory-cache.test.ts src/tests/model-alias.test.ts src/tests/model-registry-thinking.test.ts src/tests/models-endpoint.test.ts src/tests/multimodal.test.ts src/tests/oss-upload.test.ts src/tests/parser.test.ts src/tests/personalization-flow.test.ts src/tests/personalization-required.test.ts src/tests/retry-policy.test.ts src/tests/rotation.test.ts src/tests/server-lifecycle.test.ts src/tests/session-keeper.test.ts src/tests/session-state.test.ts src/tests/single-account-mode.test.ts src/tests/stream-retry.test.ts src/tests/tool-calls-endpoint.test.ts src/tests/tool-instructions-cache.test.ts src/tests/session-id.test.ts src/tests/watchdog-ram.test.ts src/tests/tool-recovery-t1t2.test.ts src/tests/abort-silent-t4.test.ts src/tests/t3-close-tag-escape.test.ts src/tests/t5-tool-cap.test.ts src/tests/logger-redaction.test.ts src/tests/stall-timeout-retryable.test.ts src/tests/t6-missing-quote-array-close.test.ts src/tests/watchdog-rss.test.ts src/tests/logical-thread-batch.test.ts src/tests/chat-pool-style.test.ts src/tests/queue-wait-cap.test.ts src/tests/context-eviction-order.test.ts src/tests/chat-in-progress-settle.test.ts src/tests/stream-lock-leak.test.ts src/tests/stream-semaphore.test.ts src/tests/waf-isolation.test.ts src/tests/legacy-completions.test.ts src/tests/response-format.test.ts src/tests/cors-headers.test.ts src/tests/local-auth.test.ts src/tests/safe-remote-url.test.ts src/tests/stream-lock-terminal-cancel.test.ts src/tests/parser-truncated-tool-call.test.ts src/tests/metrics-prometheus.test.ts src/tests/context-death-cleanup.test.ts src/tests/personalization-direct-fetch.test.ts src/tests/completion-headers-parity.test.ts src/tests/parallel-escape-repro.test.ts src/tests/chat-in-progress-scale-cost.test.ts src/tests/temp-chat-mode.test.ts src/tests/profile-reset.test.ts src/tests/error-classification.test.ts src/tests/quota-cooldown.test.ts src/tests/account-ready-gate.test.ts src/tests/tool-cap-notice.test.ts src/tests/interaction-flows.test.ts src/tests/headers-gate-cooldown-fallback.test.ts src/tests/unwrapped-tool-calls.test.ts src/tests/anthropic.test.ts src/tests/sync-clients.test.ts src/tests/sync-top10-clients.test.ts src/tests/cli.test.ts src/tests/paths.test.ts src/tests/tool-manifest.test.ts src/tests/tui.test.ts src/tests/tui-settings.test.ts src/tests/personalization-deadline.test.ts src/tests/update-cli.test.ts src/tests/batch-accounts.test.ts src/tests/shared-browser.test.ts src/tests/auth-credential-classification.test.ts src/tests/responses-api.test.ts src/tests/media-generation.test.ts src/tests/infer-tool-name.test.ts src/tests/native-session-headers.test.ts src/tests/omni-thinking.test.ts src/tests/protect-stuck-mutex.test.ts src/tests/stream-error-notice.test.ts src/tests/parser-deep-coverage.test.ts src/tests/tui-logs-copy.test.ts src/tests/auth-jwt-and-validation.test.ts src/tests/tui-port-and-key-modal.test.ts",
33
33
  "test:mock:fast": "tsx --test --test-concurrency=2 --env-file=.env.test src/tests/account-concurrency.test.ts src/tests/concurrency-latest-wins.test.ts src/tests/mutex.test.ts src/tests/context-truncation.test.ts src/tests/streaming-thinking-summary.test.ts src/tests/tool-call-guard.test.ts src/tests/utils-logger-full.test.ts src/tests/logger-toolcall-errors.test.ts src/tests/logger-toolcall-disabled.test.ts src/tests/utils-json-full.test.ts src/tests/utils-session-guard-full.test.ts src/tests/core-logic-full.test.ts src/tests/core-logic-accounts.test.ts src/tests/chat-validation-full.test.ts src/tests/chat-logic-full.test.ts src/tests/account-stickiness.test.ts src/tests/accounts-security.test.ts src/tests/advanced.test.ts src/tests/auth-playwright.test.ts src/tests/auth-session-persistence.test.ts src/tests/captcha-solver.test.ts src/tests/cache-scan-regex.test.ts src/tests/chat-cleanup.test.ts src/tests/chat-cleanup-auto.test.ts src/tests/chromium-args.test.ts src/tests/config-current.test.ts src/tests/context-meter.test.ts src/tests/concurrency.test.ts src/tests/delta.test.ts src/tests/fingerprint.test.ts src/tests/human-behavior.test.ts src/tests/index.test.ts src/tests/json-recovery.test.ts src/tests/jsonFix.test.ts src/tests/memory-cache.test.ts src/tests/model-alias.test.ts src/tests/model-registry-thinking.test.ts src/tests/models-endpoint.test.ts src/tests/multimodal.test.ts src/tests/oss-upload.test.ts src/tests/parser.test.ts src/tests/personalization-flow.test.ts src/tests/personalization-required.test.ts src/tests/retry-policy.test.ts src/tests/rotation.test.ts src/tests/server-lifecycle.test.ts src/tests/session-keeper.test.ts src/tests/session-state.test.ts src/tests/single-account-mode.test.ts src/tests/stream-retry.test.ts src/tests/tool-calls-endpoint.test.ts src/tests/tool-instructions-cache.test.ts src/tests/session-id.test.ts src/tests/watchdog-ram.test.ts src/tests/tool-recovery-t1t2.test.ts src/tests/abort-silent-t4.test.ts src/tests/t3-close-tag-escape.test.ts src/tests/t5-tool-cap.test.ts src/tests/logger-redaction.test.ts src/tests/stall-timeout-retryable.test.ts src/tests/t6-missing-quote-array-close.test.ts src/tests/watchdog-rss.test.ts src/tests/logical-thread-batch.test.ts src/tests/chat-pool-style.test.ts src/tests/queue-wait-cap.test.ts src/tests/context-eviction-order.test.ts src/tests/chat-in-progress-settle.test.ts src/tests/stream-lock-leak.test.ts src/tests/stream-semaphore.test.ts src/tests/waf-isolation.test.ts src/tests/legacy-completions.test.ts src/tests/response-format.test.ts src/tests/cors-headers.test.ts src/tests/local-auth.test.ts src/tests/safe-remote-url.test.ts src/tests/stream-lock-terminal-cancel.test.ts src/tests/parser-truncated-tool-call.test.ts src/tests/metrics-prometheus.test.ts src/tests/context-death-cleanup.test.ts src/tests/personalization-direct-fetch.test.ts src/tests/completion-headers-parity.test.ts src/tests/parallel-escape-repro.test.ts src/tests/chat-in-progress-scale-cost.test.ts src/tests/temp-chat-mode.test.ts src/tests/profile-reset.test.ts src/tests/error-classification.test.ts src/tests/quota-cooldown.test.ts src/tests/account-ready-gate.test.ts src/tests/tool-cap-notice.test.ts src/tests/interaction-flows.test.ts src/tests/headers-gate-cooldown-fallback.test.ts src/tests/unwrapped-tool-calls.test.ts src/tests/anthropic.test.ts src/tests/sync-clients.test.ts src/tests/sync-top10-clients.test.ts src/tests/cli.test.ts src/tests/paths.test.ts src/tests/tool-manifest.test.ts src/tests/tui.test.ts src/tests/tui-settings.test.ts src/tests/personalization-deadline.test.ts src/tests/update-cli.test.ts src/tests/batch-accounts.test.ts src/tests/shared-browser.test.ts src/tests/auth-credential-classification.test.ts src/tests/responses-api.test.ts src/tests/media-generation.test.ts src/tests/infer-tool-name.test.ts src/tests/native-session-headers.test.ts src/tests/omni-thinking.test.ts src/tests/protect-stuck-mutex.test.ts src/tests/stream-error-notice.test.ts src/tests/parser-deep-coverage.test.ts src/tests/tui-logs-copy.test.ts src/tests/auth-jwt-and-validation.test.ts src/tests/tui-port-and-key-modal.test.ts",
34
34
  "test:live": "tsx --test src/tests/concurrentChat.test.ts src/tests/agenticStress.test.ts",
35
+ "test:stress": "npx tsx src/benchmarks/stress-test.ts",
36
+ "test:stress:personalization": "npx tsx src/benchmarks/stress-test-personalization.ts",
35
37
  "benchmark:proxy": "npx tsx src/benchmarks/proxy-baseline.ts",
36
38
  "typecheck": "tsc --noEmit"
37
39
  },
@@ -62,16 +64,16 @@
62
64
  "@hono/node-server": "^2.1.1",
63
65
  "ali-oss": "^6.23.0",
64
66
  "better-sqlite3": "^13.0.3",
65
- "dotenv": "^17.4.2",
66
- "hono": "^4.13.7",
67
+ "dotenv": "^18.0.4",
68
+ "hono": "^4.13.9",
67
69
  "patchright": "1.63.0",
68
- "tsx": "^4.23.13",
70
+ "tsx": "^4.23.15",
69
71
  "uuid": "^14.0.2",
70
- "zod": "^4.5.4"
72
+ "zod": "^4.6.5"
71
73
  },
72
74
  "devDependencies": {
73
75
  "@types/better-sqlite3": "^9.6.0",
74
- "@types/node": "^26.4.1",
76
+ "@types/node": "^26.6.3",
75
77
  "esbuild": "^0.28.2",
76
78
  "typescript": "^7.0.2"
77
79
  },
@@ -1,4 +1,17 @@
1
+ import fs from "node:fs";
2
+ import dotenv from "dotenv";
1
3
  import { z } from "zod";
4
+ import { getEnvFilePath, isRunningUnderNodeTest } from "./paths.ts";
5
+
6
+ // Ensure .env is loaded before parsing schema across CLI, TUI, and server modes
7
+ if (!isRunningUnderNodeTest()) {
8
+ const envPath = getEnvFilePath();
9
+ if (fs.existsSync(envPath)) {
10
+ dotenv.config({ path: envPath, quiet: true });
11
+ } else {
12
+ dotenv.config({ quiet: true });
13
+ }
14
+ }
2
15
 
3
16
  const envSchema = z
4
17
  .object({
@@ -17,6 +17,6 @@ export function getAppVersion(): string {
17
17
  }
18
18
  }
19
19
  } catch {}
20
- cachedVersion = "v1.4.0";
20
+ cachedVersion = "v1.4.2";
21
21
  return cachedVersion;
22
22
  }
@@ -279,6 +279,7 @@ export async function chatCompletions(c: Context) {
279
279
  )) {
280
280
  c.header(name, value);
281
281
  }
282
+ c.header("X-QwenProxy-Account", streamResult.activeAccountLabel);
282
283
 
283
284
  // A full-context replay (account switch / missing thread parent) hides its
284
285
  // real cost behind the thread-native delta numbers: surface it explicitly
@@ -450,7 +450,7 @@ export async function isPageLoggedIn(
450
450
 
451
451
  export async function getOrLaunchSharedBrowser(
452
452
  browserType: BrowserType = "chromium",
453
- headless = true,
453
+ headless = config.playwright.headless,
454
454
  ): Promise<Browser> {
455
455
  if (sharedBrowser && sharedBrowser.isConnected()) {
456
456
  return sharedBrowser;
@@ -587,15 +587,42 @@ const CHAT_INPUT_ACTION_TIMEOUT_MS = 10_000;
587
587
  * account for five minutes.
588
588
  */
589
589
  async function clearVisibleChallenge(page: Page): Promise<void> {
590
- if (!config.captcha.enabled) return;
591
- // waitForMs 0: a single detection pass, so the common no-challenge case adds
592
- // no measurable cost to header capture.
593
- await solveBaxiaCaptcha(page, {
594
- waitForMs: 0,
595
- maxAttempts: config.captcha.maxAttempts,
596
- retryDelayMs: config.captcha.retryDelayMs,
597
- settleMs: config.captcha.settleMs,
598
- }).catch(() => false);
590
+ if (page.isClosed()) return;
591
+ // 1. Solve slider puzzle captcha if present
592
+ if (config.captcha.enabled) {
593
+ await solveBaxiaCaptcha(page, {
594
+ waitForMs: 0,
595
+ maxAttempts: config.captcha.maxAttempts,
596
+ retryDelayMs: config.captcha.retryDelayMs,
597
+ settleMs: config.captcha.settleMs,
598
+ }).catch(() => false);
599
+ }
600
+
601
+ // 2. Dismiss any overlay modals (Welcome to Qwen, Terms/Consent, Whats New, etc.)
602
+ // that can block the chat textarea or send button
603
+ try {
604
+ const modalCloseSelectors = [
605
+ ".ant-modal-close",
606
+ ".ant-modal-close-x",
607
+ "button.ant-modal-close",
608
+ "button[aria-label*='Close' i]",
609
+ "button[aria-label*='Fechar' i]",
610
+ ".index-module__whats-new-modal-close",
611
+ "button:has-text('Cancel')",
612
+ "button:has-text('Cancelar')",
613
+ "button:has-text('I understand')",
614
+ "button:has-text('Entendi')",
615
+ "button:has-text('OK')",
616
+ ];
617
+ for (const sel of modalCloseSelectors) {
618
+ if (page.isClosed()) return;
619
+ const btn = page.locator(sel).first();
620
+ if (await btn.isVisible({ timeout: 150 }).catch(() => false)) {
621
+ await btn.click({ timeout: 500 }).catch(() => {});
622
+ await sleep(200);
623
+ }
624
+ }
625
+ } catch {}
599
626
  }
600
627
 
601
628
  function getErrorMessage(error: unknown): string {
@@ -1051,6 +1078,21 @@ export function getStealthScript(profile: FingerprintProfile): string {
1051
1078
  spoofedFunctions.add(window.performance.getEntriesByType);
1052
1079
  }
1053
1080
  } catch(e) {}
1081
+
1082
+ // --- Auto-resolve birthday compliance & eliminate false logout markers ---
1083
+ try {
1084
+ localStorage.removeItem("qwen_token_logged_out_marker");
1085
+ const b = localStorage.getItem("qwen_account_birthday");
1086
+ if (b) {
1087
+ try {
1088
+ const parsed = JSON.parse(b);
1089
+ if (!parsed.birthday) {
1090
+ parsed.birthday = "1995-06-15";
1091
+ localStorage.setItem("qwen_account_birthday", JSON.stringify(parsed));
1092
+ }
1093
+ } catch {}
1094
+ }
1095
+ } catch(e) {}
1054
1096
  })();
1055
1097
  `;
1056
1098
  }
@@ -1443,7 +1485,7 @@ async function resolveAccountCredentials(account: QwenAccount): Promise<QwenAcco
1443
1485
 
1444
1486
  export async function initPlaywrightForAccount(
1445
1487
  rawAccount: QwenAccount,
1446
- headless = true,
1488
+ headless = config.playwright.headless,
1447
1489
  browserType: BrowserType = "chromium",
1448
1490
  options: { skipHeaderCapture?: boolean } = {},
1449
1491
  ): Promise<void> {
@@ -1695,10 +1737,19 @@ export async function initPlaywrightForAccount(
1695
1737
 
1696
1738
  if (restoredFromDb) {
1697
1739
  if (!acctPage.isClosed() && (acctPage.url() === "about:blank" || !acctPage.url().startsWith(qwenOrigin()))) {
1698
- void acctPage.goto(qwenUrl("/"), {
1699
- waitUntil: "domcontentloaded",
1700
- timeout: config.timeouts.navigation,
1701
- }).catch(() => {});
1740
+ try {
1741
+ await acctPage.goto(qwenUrl("/"), {
1742
+ waitUntil: "domcontentloaded",
1743
+ timeout: config.timeouts.navigation,
1744
+ });
1745
+ await sleep(300);
1746
+ } catch (err: any) {
1747
+ if (!err?.message?.includes("ERR_ABORTED")) {
1748
+ console.warn(
1749
+ `⚠️ [Playwright] Background navigation warning for ${maskEmail(account.email)}: ${err.message}`,
1750
+ );
1751
+ }
1752
+ }
1702
1753
  }
1703
1754
  return;
1704
1755
  }
@@ -2022,17 +2073,33 @@ async function loginToQwen(
2022
2073
  const apiResult = await loginViaApi(page, email, password);
2023
2074
  if (apiResult.success) {
2024
2075
  cookieCaches.delete(accountId);
2025
- const cache = headerCaches.get(accountId);
2026
- if (cache) {
2027
- cache.headers = {};
2028
- cache.lastRefresh = 0;
2029
- }
2030
- unmarkAccountHeadersReady(accountId);
2076
+ await saveStorageState(page.context(), accountId);
2031
2077
  try {
2032
- const { deleteAuthSession } = await import("../core/database.ts");
2033
- deleteAuthSession(accountId);
2078
+ const liveCookies = await page.context().cookies();
2079
+ const tokenCookie = liveCookies.find((c) => c.name === "token");
2080
+ if (tokenCookie) {
2081
+ const cookieStr = liveCookies.map((c) => `${c.name}=${c.value}`).join("; ");
2082
+ const cache = getHeaderCache(accountId);
2083
+ cache.headers.cookie = cookieStr;
2084
+ cache.lastRefresh = Date.now();
2085
+ markAccountHeadersReady(accountId);
2086
+ const { saveAuthSession } = await import("../core/database.ts");
2087
+ const exp = parseJwtExpiry(tokenCookie.value);
2088
+ saveAuthSession(accountId, {
2089
+ cookie: cookieStr,
2090
+ userAgent: cache.headers["user-agent"] || "",
2091
+ bxV: cache.headers["bx-v"] || "2.5.37",
2092
+ bxUa: cache.headers["bx-ua"] || "",
2093
+ bxUmidtoken: cache.headers["bx-umidtoken"] || "",
2094
+ secChUa: cache.headers["sec-ch-ua"] || undefined,
2095
+ secChUaMobile: cache.headers["sec-ch-ua-mobile"] || undefined,
2096
+ secChUaPlatform: cache.headers["sec-ch-ua-platform"] || undefined,
2097
+ version: cache.headers["version"] || undefined,
2098
+ tokenExpiresAt: exp || undefined,
2099
+ capturedAt: Date.now(),
2100
+ });
2101
+ }
2034
2102
  } catch {}
2035
- await saveStorageState(page.context(), accountId);
2036
2103
  return true;
2037
2104
  }
2038
2105
 
@@ -2052,17 +2119,33 @@ async function loginToQwen(
2052
2119
  const uiResult = await loginViaUi(page, email, password);
2053
2120
  if (uiResult.success) {
2054
2121
  cookieCaches.delete(accountId);
2055
- const cache = headerCaches.get(accountId);
2056
- if (cache) {
2057
- cache.headers = {};
2058
- cache.lastRefresh = 0;
2059
- }
2060
- unmarkAccountHeadersReady(accountId);
2122
+ await saveStorageState(page.context(), accountId);
2061
2123
  try {
2062
- const { deleteAuthSession } = await import("../core/database.ts");
2063
- deleteAuthSession(accountId);
2124
+ const liveCookies = await page.context().cookies();
2125
+ const tokenCookie = liveCookies.find((c) => c.name === "token");
2126
+ if (tokenCookie) {
2127
+ const cookieStr = liveCookies.map((c) => `${c.name}=${c.value}`).join("; ");
2128
+ const cache = getHeaderCache(accountId);
2129
+ cache.headers.cookie = cookieStr;
2130
+ cache.lastRefresh = Date.now();
2131
+ markAccountHeadersReady(accountId);
2132
+ const { saveAuthSession } = await import("../core/database.ts");
2133
+ const exp = parseJwtExpiry(tokenCookie.value);
2134
+ saveAuthSession(accountId, {
2135
+ cookie: cookieStr,
2136
+ userAgent: cache.headers["user-agent"] || "",
2137
+ bxV: cache.headers["bx-v"] || "2.5.37",
2138
+ bxUa: cache.headers["bx-ua"] || "",
2139
+ bxUmidtoken: cache.headers["bx-umidtoken"] || "",
2140
+ secChUa: cache.headers["sec-ch-ua"] || undefined,
2141
+ secChUaMobile: cache.headers["sec-ch-ua-mobile"] || undefined,
2142
+ secChUaPlatform: cache.headers["sec-ch-ua-platform"] || undefined,
2143
+ version: cache.headers["version"] || undefined,
2144
+ tokenExpiresAt: exp || undefined,
2145
+ capturedAt: Date.now(),
2146
+ });
2147
+ }
2064
2148
  } catch {}
2065
- await saveStorageState(page.context(), accountId);
2066
2149
  return true;
2067
2150
  }
2068
2151
 
@@ -2292,11 +2375,22 @@ async function loginViaUi(
2292
2375
  }
2293
2376
 
2294
2377
  // In Qwen Web, if the page opens on the default email OTP panel, click "Log in with a password"
2295
- // to reveal the standard email + password form.
2378
+ // (supporting English, Portuguese, Spanish, Chinese) to reveal the standard email + password form.
2379
+ await clearVisibleChallenge(page);
2380
+
2381
+ const findPasswordModeButton = () => {
2382
+ return page
2383
+ .locator('button, [role="button"], a, div')
2384
+ .filter({
2385
+ hasText: /(?:log\s*in\s*with\s*(?:a\s*)?password|fazer\s*login\s*com\s*senha|entrar\s*com\s*(?:uma\s*)?senha|iniciar\s*sesi[oó]n\s*con\s*contrase[nñ]a|密码登录)/i,
2386
+ })
2387
+ .first();
2388
+ };
2389
+
2296
2390
  try {
2297
- const pwdModeBtn = page.getByText(/Log in with a password/i).first();
2298
- if (await pwdModeBtn.isVisible({ timeout: 1500 }).catch(() => false)) {
2299
- await pwdModeBtn.click().catch(() => {});
2391
+ const pwdModeBtn = findPasswordModeButton();
2392
+ if (await pwdModeBtn.isVisible({ timeout: 2000 }).catch(() => false)) {
2393
+ await pwdModeBtn.click({ timeout: 2000 }).catch(() => {});
2300
2394
  await sleep(500);
2301
2395
  }
2302
2396
  } catch {}
@@ -2307,9 +2401,9 @@ async function loginViaUi(
2307
2401
 
2308
2402
  // If "Log in with a password" button appeared after typing email, click it
2309
2403
  try {
2310
- const pwdModeBtn = page.getByText(/Log in with a password/i).first();
2311
- if (await pwdModeBtn.isVisible({ timeout: 1000 }).catch(() => false)) {
2312
- await pwdModeBtn.click().catch(() => {});
2404
+ const pwdModeBtn = findPasswordModeButton();
2405
+ if (await pwdModeBtn.isVisible({ timeout: 1500 }).catch(() => false)) {
2406
+ await pwdModeBtn.click({ timeout: 2000 }).catch(() => {});
2313
2407
  await sleep(500);
2314
2408
  }
2315
2409
  } catch {}
@@ -2343,7 +2437,7 @@ async function loginViaUi(
2343
2437
  // Prefer clicking the submit button; fall back to pressing Enter.
2344
2438
  // The button starts disabled and only enables once both fields are filled.
2345
2439
  const submitSelector =
2346
- 'button.qwenchat-auth-pc-submit-button, button[type="submit"], button:has-text("Log in"), button:has-text("Sign in")';
2440
+ '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ón"), button:has-text("登录")';
2347
2441
  const submitButton = page.locator(submitSelector).first();
2348
2442
  try {
2349
2443
  await page.waitForSelector('button[type="submit"]:not([disabled])', {
@@ -3164,9 +3258,9 @@ async function refreshHeadersInternal(
3164
3258
  }
3165
3259
  }
3166
3260
 
3167
- // Fast re-auth completion: if re-auth succeeded and required anti-bot tokens are already cached,
3261
+ // Fast refresh completion: if required anti-bot tokens are already cached,
3168
3262
  // refresh cookies directly without running slow UI typing interception.
3169
- if (reauthExecuted && hasRequiredQwenHeaders(cache.headers)) {
3263
+ if (hasRequiredQwenHeaders(cache.headers)) {
3170
3264
  const liveCookies = await page.context().cookies();
3171
3265
  if (liveCookies.some((c) => c.name === "token")) {
3172
3266
  const cookieStr = liveCookies.map((c) => `${c.name}=${c.value}`).join("; ");
@@ -800,10 +800,19 @@ async function withQwenBrowserPage<T>(
800
800
  (normalizedTargetPath !== null && currentPath !== normalizedTargetPath);
801
801
 
802
802
  if (needsNavigation) {
803
- await page.goto(targetUrl, {
804
- waitUntil: "domcontentloaded",
805
- timeout: Math.min(config.timeouts.navigation, operationTimeoutMs),
806
- });
803
+ try {
804
+ await page.goto(targetUrl, {
805
+ waitUntil: "domcontentloaded",
806
+ timeout: Math.min(config.timeouts.navigation, operationTimeoutMs),
807
+ });
808
+ } catch (err: any) {
809
+ // If navigation was aborted because page reached target origin or redirect finished, ignore benign abort
810
+ if (err?.message?.includes("ERR_ABORTED") && page.url().startsWith(targetOrigin)) {
811
+ // benign navigation interruption
812
+ } else {
813
+ throw err;
814
+ }
815
+ }
807
816
  }
808
817
 
809
818
  return fn(page);
@@ -908,7 +917,7 @@ export async function requestQwenTextInBrowser(
908
917
  const controller = new AbortController();
909
918
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
910
919
  try {
911
- const response = await fetch(url, {
920
+ let response = await fetch(url, {
912
921
  method,
913
922
  credentials: "include",
914
923
  headers,
@@ -916,6 +925,38 @@ export async function requestQwenTextInBrowser(
916
925
  signal: controller.signal,
917
926
  ...(referrer ? { referrer } : {}),
918
927
  });
928
+
929
+ // If 401 Unauthorized in browser, try silent in-page token refresh before giving up
930
+ if (response.status === 401) {
931
+ try {
932
+ const refreshRes = await fetch("https://auth.qwen.ai/api/v2/auths/refresh", {
933
+ method: "GET",
934
+ credentials: "include",
935
+ signal: AbortSignal.timeout(3000),
936
+ });
937
+ if (refreshRes.status === 200) {
938
+ const refreshJson: any = await refreshRes.json().catch(() => null);
939
+ if (refreshJson && refreshJson.success === true && refreshJson.data?.token) {
940
+ const freshTok = refreshJson.data.token;
941
+ localStorage.setItem("token", freshTok);
942
+ document.cookie = `token=${encodeURIComponent(freshTok)}; path=/; domain=.qwen.ai; max-age=31536000`;
943
+ if (headers["authorization"] || headers["Authorization"]) {
944
+ headers["authorization"] = `Bearer ${freshTok}`;
945
+ headers["Authorization"] = `Bearer ${freshTok}`;
946
+ }
947
+ response = await fetch(url, {
948
+ method,
949
+ credentials: "include",
950
+ headers,
951
+ body,
952
+ signal: controller.signal,
953
+ ...(referrer ? { referrer } : {}),
954
+ });
955
+ }
956
+ }
957
+ } catch {}
958
+ }
959
+
919
960
  return {
920
961
  status: response.status,
921
962
  contentType: response.headers.get("content-type") || "",
@@ -1059,20 +1100,7 @@ async function requestQwenPersonalizationInBrowser(
1059
1100
  headers: Record<string, string>,
1060
1101
  payload?: Record<string, unknown>,
1061
1102
  ): Promise<{ status: number; raw: string; json: any }> {
1062
- // If browser-only fetch is disabled, try direct Node fetch as fast-path
1063
- if (!config.qwen.browserOnlyFetch && !isAuthMockEnabled()) {
1064
- const direct = await requestQwenSettingsDirectFetch(
1065
- accountId,
1066
- method,
1067
- path,
1068
- headers,
1069
- payload,
1070
- );
1071
- if (direct) {
1072
- return direct;
1073
- }
1074
- }
1075
-
1103
+ // Always route personalization through stealth browser page (user constraint: tudo via browser stealth)
1076
1104
  const response = await requestQwenTextInBrowser(
1077
1105
  accountId,
1078
1106
  method,