qwenproxy-cli 1.3.2 → 1.3.4

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.3.2",
3
+ "version": "1.3.4",
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": {
@@ -80,7 +80,7 @@ const envSchema = z
80
80
  CHAT_TIMEOUT: z.string().default("180000"),
81
81
  NAVIGATION_TIMEOUT: z.string().default("60000"),
82
82
  PAGE_TIMEOUT: z.string().default("60000"),
83
- HEADERS_TIMEOUT: z.string().default("90000"),
83
+ HEADERS_TIMEOUT: z.string().default("120000"),
84
84
  TIME_TO_FIRST_BYTE: z.string().default("60000"),
85
85
  IDLE_STREAM_TIMEOUT: z.string().default("60000"),
86
86
  // Deadline for the FIRST upstream chunk on thinking models (the reasoning
@@ -510,7 +510,7 @@ const HEADER_CAPTURE_TRIGGER_GRACE_MS = 15_000;
510
510
  * send. Fail it fast and let the retry loop reload + re-send against the warm
511
511
  * SDK instead of stalling the boot for the full 15s.
512
512
  */
513
- const FIRST_TRIGGER_GRACE_MS = 3_000;
513
+ const FIRST_TRIGGER_GRACE_MS = 8_000;
514
514
  /**
515
515
  * Sends (the initial one plus re-triggers) header capture may spend on getting a
516
516
  * completion request that actually carries the bx headers. The in-page SDK can
@@ -1535,6 +1535,7 @@ export async function initPlaywrightForAccount(
1535
1535
  throw validationError;
1536
1536
  }
1537
1537
  if (!options.skipHeaderCapture) {
1538
+ (acctPage as any).__qwenChatHomeLoaded = true;
1538
1539
  await captureQwenHeaders(account.id);
1539
1540
  }
1540
1541
 
@@ -1961,14 +1962,45 @@ async function loginViaApi(
1961
1962
  }
1962
1963
 
1963
1964
  if (signinSuccess) {
1965
+ const token = data?.data?.token || data?.token;
1966
+ if (token) {
1967
+ try {
1968
+ await page.context().addCookies([
1969
+ {
1970
+ name: "token",
1971
+ value: token,
1972
+ domain: ".qwen.ai",
1973
+ path: "/",
1974
+ expires: Math.floor(Date.now() / 1000) + 31536000,
1975
+ httpOnly: false,
1976
+ secure: true,
1977
+ sameSite: "Lax",
1978
+ },
1979
+ ]);
1980
+ } catch {}
1981
+ }
1982
+
1964
1983
  await page
1965
1984
  .goto(qwenUrl("/"), {
1966
1985
  waitUntil: "domcontentloaded",
1967
1986
  timeout: config.timeouts.navigation,
1968
1987
  })
1969
1988
  .catch(() => {});
1989
+
1990
+ if (token) {
1991
+ await page
1992
+ .evaluate((tok) => {
1993
+ try {
1994
+ localStorage.setItem("token", tok);
1995
+ document.cookie = `token=${encodeURIComponent(tok)}; path=/; domain=.qwen.ai; max-age=31536000`;
1996
+ } catch {}
1997
+ }, token)
1998
+ .catch(() => {});
1999
+ await page.reload({ waitUntil: "domcontentloaded" }).catch(() => {});
2000
+ }
2001
+
1970
2002
  await sleep(1500);
1971
- const loggedIn = await isPageLoggedIn(page, 4_000);
2003
+ const loggedIn = await isPageLoggedIn(page, 5_000);
1972
2004
  if (loggedIn) {
1973
2005
  return { success: true };
1974
2006
  }
@@ -2385,8 +2417,13 @@ export async function captureQwenHeaders(
2385
2417
  // Navigate to the stable chat page. Only the first attempt pays for this:
2386
2418
  // a re-trigger types into the page that is already loaded, and reloading
2387
2419
  // would throw away the bx SDK state that just finished warming up.
2388
- const openChatPage = async () => {
2420
+ const openChatPage = async (forceReload = false) => {
2389
2421
  if (settled || page.isClosed()) return;
2422
+ if (!forceReload && (page as any).__qwenChatHomeLoaded) {
2423
+ delete (page as any).__qwenChatHomeLoaded;
2424
+ await sleep(500);
2425
+ return;
2426
+ }
2390
2427
  await page.goto(qwenUrl("/"), {
2391
2428
  waitUntil: "domcontentloaded",
2392
2429
  timeout: Math.min(config.timeouts.navigation, timeoutMs),
@@ -2401,28 +2438,33 @@ export async function captureQwenHeaders(
2401
2438
  await clearVisibleChallenge(page);
2402
2439
  if (settled || page.isClosed()) return;
2403
2440
 
2404
- // Session-expiry fast path: if the page landed on the auth/login screen
2405
- // or in a logged-out guest state, typing into the chat input would
2406
- // burn every trigger attempt on a textarea that does not submit. Re-login
2407
- // immediately when credentials are available; otherwise fail fast with a
2408
- // clear diagnosis instead of 3 pointless grace timeouts.
2441
+ // Session-expiry check: if the page redirected to /auth or /login,
2442
+ // re-login immediately.
2443
+ // On attempts 1 and 2, do NOT run a tight 3s fetch probe because the session
2444
+ // was already verified during init. Only run isPageLoggedIn if attempt >= 3
2445
+ // (as a last-resort recovery before giving up) and with a generous 8s timeout.
2409
2446
  const currentUrl = page.url();
2410
2447
  const isAuthUrl = currentUrl.includes("/auth") || currentUrl.includes("/login");
2411
- const loggedIn = !isAuthUrl && (await isPageLoggedIn(page, 3000));
2412
- if (!loggedIn) {
2448
+ const isSuspectedGuest = attempt >= 3 && !(await isPageLoggedIn(page, 8000));
2449
+ if (isAuthUrl || isSuspectedGuest) {
2413
2450
  const { getAccountCredentials } = await import("../core/accounts.ts");
2414
2451
  const creds = getAccountCredentials(accountId);
2415
2452
  if (creds && creds.email && creds.password) {
2416
2453
  console.warn(
2417
2454
  `⚠️ [Playwright] Session expired or guest state detected during header capture for ${accountId}; re-authenticating...`,
2418
2455
  );
2456
+ armOverallDeadline();
2419
2457
  const ok = await loginToQwen(accountId, creds.email, creds.password);
2420
2458
  if (ok) {
2421
2459
  // Re-login navigated; load the chat page and wait for hydration so
2422
2460
  // the check below probes a live authenticated chat page.
2423
- await openChatPage();
2461
+ await page.goto(qwenUrl("/"), {
2462
+ waitUntil: "domcontentloaded",
2463
+ timeout: Math.min(config.timeouts.navigation, timeoutMs),
2464
+ });
2465
+ await sleep(2000);
2424
2466
  }
2425
- if (!ok || !(await isPageLoggedIn(page, 4000))) {
2467
+ if (!ok || !(await isPageLoggedIn(page, 6000))) {
2426
2468
  settle(
2427
2469
  new Error(
2428
2470
  `Header capture failed for ${accountId}: re-login after session expiry did not succeed`,
@@ -2430,6 +2472,7 @@ export async function captureQwenHeaders(
2430
2472
  );
2431
2473
  return;
2432
2474
  }
2475
+ armOverallDeadline();
2433
2476
  if (settled || page.isClosed()) return;
2434
2477
  } else {
2435
2478
  settle(
@@ -2486,6 +2529,16 @@ export async function captureQwenHeaders(
2486
2529
  await page.focus(inputSelector, { timeout: inputActionTimeoutMs });
2487
2530
  if (settled || page.isClosed()) return;
2488
2531
  await page.fill(inputSelector, "a", { timeout: inputActionTimeoutMs });
2532
+ // Dispatch native React input events and keyboard stroke to guarantee React updates state
2533
+ if (typeof page.keyboard?.press === "function") {
2534
+ await page.keyboard.press("End").catch(() => {});
2535
+ }
2536
+ if (typeof (page.keyboard as any)?.type === "function") {
2537
+ await (page.keyboard as any).type(" ").catch(() => {});
2538
+ }
2539
+ if (typeof page.keyboard?.press === "function") {
2540
+ await page.keyboard.press("Backspace").catch(() => {});
2541
+ }
2489
2542
  if (typeof page.evaluate === "function") {
2490
2543
  await page.evaluate((sel) => {
2491
2544
  const el = document.querySelector(sel) as HTMLTextAreaElement | null;
@@ -2527,7 +2580,18 @@ export async function captureQwenHeaders(
2527
2580
  return;
2528
2581
  }
2529
2582
  if (settled || page.isClosed()) return;
2530
- await sleep(2000);
2583
+
2584
+ // Wait up to 3s for React to enable the send button after typing
2585
+ try {
2586
+ await page.waitForFunction(() => {
2587
+ const btn = document.querySelector(
2588
+ ".message-input-right-button-send .send-button, .chat-prompt-send-button, button.send-button, button[aria-label*='Send' i], button[aria-label*='Enviar' i], .send-button-container button"
2589
+ ) as HTMLButtonElement | null;
2590
+ return btn && !btn.disabled && !btn.classList.contains("disabled") && btn.getAttribute("aria-disabled") !== "true";
2591
+ }, { timeout: 3000 });
2592
+ } catch {
2593
+ await sleep(1000);
2594
+ }
2531
2595
  if (settled || page.isClosed()) return;
2532
2596
 
2533
2597
  const sendSelectors = [
@@ -2609,14 +2673,13 @@ export async function captureQwenHeaders(
2609
2673
  // A missing chat input is the exception: the page never rendered the
2610
2674
  // chat UI, so there is no warm SDK state to protect and only a fresh
2611
2675
  // load can recover it.
2612
- if (
2613
- attempt === 1 ||
2676
+ const needReload =
2614
2677
  lastAttemptInputMissing ||
2615
- (lastAttemptGraceTimedOut && attempt >= 3)
2616
- ) {
2678
+ (lastAttemptGraceTimedOut && attempt >= 3);
2679
+ if (attempt === 1 || needReload) {
2617
2680
  lastAttemptGraceTimedOut = false;
2618
2681
  lastAttemptInputMissing = false;
2619
- await openChatPage();
2682
+ await openChatPage(needReload);
2620
2683
  }
2621
2684
  if (settled) return;
2622
2685
  await triggerSend(attempt);