qwenproxy-cli 1.4.7 → 1.5.0

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.
@@ -479,21 +479,21 @@ export function classifyRetryAction(
479
479
  // Agent instructions ride ONLY the account-level personalization. An
480
480
  // unconfirmed sync means this account cannot serve the request as-is —
481
481
  // rotate to another account (each attempt re-syncs on its own account).
482
- // Park the failing account with PersonalizationFailed cooldown so it does
483
- // not enter an infinite ping-pong loop when multiple accounts fail.
482
+ // Do NOT park the account with a 300s cooldown: rotation across accounts
483
+ // is already guarded by triedAccounts, preventing pool collapse.
484
484
  if (err instanceof PersonalizationSyncError) {
485
485
  return makeRetryAction("personalization_sync_failed", {
486
486
  switchAccount: true,
487
487
  forceNewChat: true,
488
488
  retryAfterMs: baseDelayMs,
489
- accountCooldownMs: config.concurrency.initFailureCooldownMs,
489
+ accountCooldownMs: 0,
490
490
  accountCooldownReason: "PersonalizationFailed",
491
491
  });
492
492
  }
493
493
 
494
494
  // Upstream 401 / Unauthorized on chat creation or API requests:
495
- // Account session is invalid or expired. Cool down account with AuthInitFailed so the
496
- // proxy rotates to a valid account instead of looping endlessly on 503s.
495
+ // Access token expired (15m window). Rotate without locking into a 300s cooldown
496
+ // so the account can immediately be renewed via refresh_token.
497
497
  if (
498
498
  code === "createchatinvalidresponse" ||
499
499
  code === "createchatfailed" ||
@@ -502,15 +502,16 @@ export function classifyRetryAction(
502
502
  message.includes("não tem permissão para acessar") ||
503
503
  message.includes("401 unauthorized") ||
504
504
  message.includes('"code":"unauthorized"') ||
505
- message.includes('"code": "unauthorized"')
505
+ message.includes('"code": "unauthorized"') ||
506
+ message.includes("token has expired")
506
507
  ) {
507
508
  return makeRetryAction("account_initialization_failed", {
508
509
  switchAccount: true,
509
510
  forceNewChat: true,
510
511
  retryWithFullPrompt: true,
511
512
  retryAfterMs: Math.min(baseDelayMs, 1_000),
512
- accountCooldownMs: config.concurrency.initFailureCooldownMs,
513
- accountCooldownReason: "AuthInitFailed",
513
+ accountCooldownMs: 0,
514
+ accountCooldownReason: "AuthExpired",
514
515
  });
515
516
  }
516
517
  // Specialized recoveries first (even if wrapped as RetryableQwenStreamError)
@@ -68,6 +68,7 @@ import {
68
68
  import {
69
69
  getIncrementalDelta,
70
70
  isThinkingPhase,
71
+ isAnswerPhase,
71
72
  extractThinkingContent,
72
73
  formatThinkingSummaryContent,
73
74
  shouldSuppressStreamAbort,
@@ -470,7 +471,7 @@ export async function processNonStreamingResponse(
470
471
  foundStr = true;
471
472
  }
472
473
  }
473
- } else if (delta.phase === "answer") {
474
+ } else if (isAnswerPhase(delta.phase)) {
474
475
  isThinkingChunk = false;
475
476
  if (delta.content !== undefined) {
476
477
  const newContent = delta.content || "";
@@ -1436,7 +1437,7 @@ export async function processStreamingResponse(
1436
1437
 
1437
1438
  if (
1438
1439
  switchAccount &&
1439
- (policy.accountCooldownMs || policy.accountCooldownReason)
1440
+ (policy.accountCooldownMs ?? 0) > 0
1440
1441
  ) {
1441
1442
  markAccountRateLimited(
1442
1443
  currentAccountId,
@@ -1445,6 +1446,12 @@ export async function processStreamingResponse(
1445
1446
  );
1446
1447
  }
1447
1448
 
1449
+ if (policy.accountCooldownReason === "AuthExpired") {
1450
+ import("../../services/playwright.ts")
1451
+ .then(({ refreshAccountToken }) => refreshAccountToken(currentAccountId))
1452
+ .catch(() => {});
1453
+ }
1454
+
1448
1455
  retryContext.releaseAccountLease?.();
1449
1456
  retryContext.releaseAccountLease = null;
1450
1457
  await reader.cancel().catch(() => undefined);
@@ -1776,7 +1783,7 @@ export async function processStreamingResponse(
1776
1783
  // "phase":"answer"} delta and NO trailing [DONE]. Treat it as
1777
1784
  // the terminal event so we don't wait on the keep-alive
1778
1785
  // connection to close (up to the 60s/10min idle timeout).
1779
- if (delta.phase === "answer" && delta.status === "finished") {
1786
+ if (isAnswerPhase(delta.phase) && delta.status === "finished") {
1780
1787
  upstreamDone = true;
1781
1788
  if (!clientDisconnected) flushWrites();
1782
1789
  break; // Exit the for loop; the while check leaves the read loop
@@ -1800,7 +1807,7 @@ export async function processStreamingResponse(
1800
1807
  foundStr = true;
1801
1808
  }
1802
1809
  }
1803
- } else if (delta.phase === "answer") {
1810
+ } else if (isAnswerPhase(delta.phase)) {
1804
1811
  isThinkingChunk = false;
1805
1812
  if (delta.content !== undefined) {
1806
1813
  const newContent = delta.content || "";
@@ -2290,7 +2297,7 @@ export async function processStreamingResponse(
2290
2297
  // The retry stream may also terminate with an answer-finished
2291
2298
  // delta when upstream sends no [DONE]. Leave the read loop
2292
2299
  // immediately so we don't stall on the keep-alive connection.
2293
- if (delta.phase === "answer" && delta.status === "finished") {
2300
+ if (isAnswerPhase(delta.phase) && delta.status === "finished") {
2294
2301
  upstreamDone = true;
2295
2302
  break retryReadLoop;
2296
2303
  }
@@ -387,15 +387,18 @@ async function getSTSToken(
387
387
  }
388
388
  data = await response.json().catch(() => null);
389
389
  }
390
+ const codeLower = (data?.data?.code || data?.code || "").toLowerCase();
391
+ const detailsLower = (typeof data?.data?.details === "string" ? data.data.details : typeof data?.message === "string" ? data.message : "").toLowerCase();
390
392
  const is401 =
391
393
  data?.success === false &&
392
- (data?.data?.code === "Unauthorized" ||
393
- (typeof data?.data?.details === "string" &&
394
- data.data.details.includes("401")));
394
+ (codeLower === "unauthorized" ||
395
+ detailsLower.includes("401") ||
396
+ detailsLower.includes("token has expired") ||
397
+ detailsLower.includes("unauthorized"));
395
398
 
396
399
  if (is401) {
397
400
  try {
398
- const { refreshHeaders } = await import("../services/playwright.ts");
401
+ const { refreshAccountToken } = await import("../services/playwright.ts");
399
402
  const { getBasicHeaders } = await import("../services/auth-playwright.ts");
400
403
  const { loadAccounts } = await import("../core/accounts.ts");
401
404
  const resolvedId = accountId ?? loadAccounts()[0]?.id;
@@ -403,16 +406,18 @@ async function getSTSToken(
403
406
  console.warn(
404
407
  `[Upload] STS token 401 — refreshing session with re-auth and retrying...`,
405
408
  );
406
- await refreshHeaders(resolvedId, undefined, true);
407
- const fresh = await getBasicHeaders(resolvedId);
408
- headers.cookie = fresh.cookie;
409
- headers["user-agent"] = fresh.userAgent;
410
- headers["bx-v"] = fresh.bxV;
411
- if (fresh.bxUa) headers["bx-ua"] = fresh.bxUa;
412
- if (fresh.bxUmidtoken) headers["bx-umidtoken"] = fresh.bxUmidtoken;
413
- const retryRes = await doFetch(headers);
414
- if (retryRes.ok) {
415
- data = await retryRes.json().catch(() => null);
409
+ const refreshResult = await refreshAccountToken(resolvedId);
410
+ if (refreshResult.success) {
411
+ const fresh = await getBasicHeaders(resolvedId);
412
+ headers.cookie = fresh.cookie;
413
+ headers["user-agent"] = fresh.userAgent;
414
+ headers["bx-v"] = fresh.bxV;
415
+ if (fresh.bxUa) headers["bx-ua"] = fresh.bxUa;
416
+ if (fresh.bxUmidtoken) headers["bx-umidtoken"] = fresh.bxUmidtoken;
417
+ const retryRes = await doFetch(headers);
418
+ if (retryRes.ok) {
419
+ data = await retryRes.json().catch(() => null);
420
+ }
416
421
  }
417
422
  }
418
423
  } catch (refreshErr) {
@@ -2005,6 +2005,8 @@ export interface LoginAttemptResult {
2005
2005
  success: boolean;
2006
2006
  permanentFailure?: boolean;
2007
2007
  reason?: string;
2008
+ token?: string;
2009
+ refreshToken?: string;
2008
2010
  }
2009
2011
 
2010
2012
  export function classifyQwenAuthError(
@@ -2120,6 +2122,7 @@ async function loginToQwen(
2120
2122
  secChUaPlatform: cache.headers["sec-ch-ua-platform"] || undefined,
2121
2123
  version: cache.headers["version"] || undefined,
2122
2124
  tokenExpiresAt: exp || undefined,
2125
+ refreshToken: apiResult.refreshToken,
2123
2126
  capturedAt: Date.now(),
2124
2127
  });
2125
2128
  }
@@ -2155,6 +2158,12 @@ async function loginToQwen(
2155
2158
  markAccountHeadersReady(accountId);
2156
2159
  const { saveAuthSession } = await import("../core/database.ts");
2157
2160
  const exp = parseJwtExpiry(tokenCookie.value);
2161
+ let rTok = uiResult.refreshToken;
2162
+ if (!rTok) {
2163
+ try {
2164
+ rTok = (await page.evaluate(() => localStorage.getItem("refresh_token") || "")) || undefined;
2165
+ } catch {}
2166
+ }
2158
2167
  saveAuthSession(accountId, {
2159
2168
  cookie: cookieStr,
2160
2169
  userAgent: cache.headers["user-agent"] || "",
@@ -2166,6 +2175,7 @@ async function loginToQwen(
2166
2175
  secChUaPlatform: cache.headers["sec-ch-ua-platform"] || undefined,
2167
2176
  version: cache.headers["version"] || undefined,
2168
2177
  tokenExpiresAt: exp || undefined,
2178
+ refreshToken: rTok,
2169
2179
  capturedAt: Date.now(),
2170
2180
  });
2171
2181
  }
@@ -2384,7 +2394,7 @@ async function loginViaApi(
2384
2394
  )
2385
2395
  .catch(() => {});
2386
2396
  await page.reload({ waitUntil: "domcontentloaded" }).catch(() => {});
2387
- return { success: true };
2397
+ return { success: true, token, refreshToken };
2388
2398
  }
2389
2399
  }
2390
2400
 
@@ -2690,6 +2700,161 @@ async function loginViaUi(
2690
2700
  }
2691
2701
  }
2692
2702
 
2703
+ /**
2704
+ * Silently refresh the short-lived access token (15m) using the 30-day refresh_token.
2705
+ * Uses official in-page fetch matching Qwen Web /api/v2/auths/refresh verified in HAR.
2706
+ */
2707
+ export async function refreshAccountToken(
2708
+ accountId: string,
2709
+ ): Promise<{ success: boolean; token?: string; error?: string }> {
2710
+ const page = accountPages.get(accountId);
2711
+ if (!page || page.isClosed()) {
2712
+ return { success: false, error: "Account page not active" };
2713
+ }
2714
+
2715
+ // 1. Get refresh token from SQLite session or localStorage
2716
+ const { getValidAuthSession, saveAuthSession } = await import("../core/database.ts");
2717
+ const session = getValidAuthSession(accountId);
2718
+ let refreshToken = session?.refreshToken;
2719
+
2720
+ if (!refreshToken) {
2721
+ try {
2722
+ refreshToken =
2723
+ (await page.evaluate(() => localStorage.getItem("refresh_token") || "")) ||
2724
+ undefined;
2725
+ } catch {}
2726
+ }
2727
+
2728
+ // 2. If we have a refresh token, invoke the working refresh endpoint in-page
2729
+ if (refreshToken) {
2730
+ try {
2731
+ const refreshResult = await page.evaluate(
2732
+ async ({ rTok }) => {
2733
+ try {
2734
+ const response = await fetch("https://auth.qwen.ai/api/v2/auths/refresh", {
2735
+ method: "GET",
2736
+ credentials: "include",
2737
+ headers: {
2738
+ accept: "application/json, text/plain, */*",
2739
+ source: "web",
2740
+ version: "0.3.11",
2741
+ "x-request-origin": "https://chat.qwen.ai",
2742
+ timezone: new Date().toString().split(" (")[0],
2743
+ authorization: `Bearer ${rTok}`,
2744
+ },
2745
+ });
2746
+ const json = await response.json().catch(() => null);
2747
+ return { ok: response.ok, status: response.status, data: json };
2748
+ } catch (e: any) {
2749
+ return { ok: false, error: e.message };
2750
+ }
2751
+ },
2752
+ { rTok: refreshToken },
2753
+ );
2754
+
2755
+ const json = refreshResult?.data;
2756
+ if (json && json.success === true && json.data) {
2757
+ const newAccessToken = json.data.access_token || json.data.token;
2758
+ const newRefreshToken = json.data.refresh_token || refreshToken;
2759
+
2760
+ if (newAccessToken) {
2761
+ // Update browser cookie and localStorage
2762
+ await page.context().addCookies([
2763
+ {
2764
+ name: "token",
2765
+ value: newAccessToken,
2766
+ domain: ".qwen.ai",
2767
+ path: "/",
2768
+ expires: Math.floor(Date.now() / 1000) + 31536000,
2769
+ httpOnly: false,
2770
+ secure: true,
2771
+ sameSite: "Lax",
2772
+ },
2773
+ ]);
2774
+
2775
+ await page
2776
+ .evaluate(
2777
+ ({ tok, rTok }) => {
2778
+ try {
2779
+ localStorage.removeItem("qwen_token_logged_out_marker");
2780
+ localStorage.setItem("token", tok);
2781
+ localStorage.setItem("access_token", tok);
2782
+ if (rTok) localStorage.setItem("refresh_token", rTok);
2783
+ document.cookie = `token=${encodeURIComponent(tok)}; path=/; domain=.qwen.ai; max-age=31536000`;
2784
+ } catch {}
2785
+ },
2786
+ { tok: newAccessToken, rTok: newRefreshToken },
2787
+ )
2788
+ .catch(() => {});
2789
+
2790
+ // Update memory cache
2791
+ const liveCookies = await page.context().cookies();
2792
+ const cookieStr = liveCookies.map((c) => `${c.name}=${c.value}`).join("; ");
2793
+ const cache = getHeaderCache(accountId);
2794
+ cache.headers.cookie = cookieStr;
2795
+ cache.lastRefresh = Date.now();
2796
+ markAccountHeadersReady(accountId);
2797
+
2798
+ // Update SQLite session with new tokens
2799
+ saveAuthSession(accountId, {
2800
+ cookie: cookieStr,
2801
+ userAgent: cache.headers["user-agent"] || session?.userAgent || "",
2802
+ bxV: cache.headers["bx-v"] || session?.bxV || "2.5.37",
2803
+ bxUa: cache.headers["bx-ua"] || session?.bxUa || "",
2804
+ bxUmidtoken: cache.headers["bx-umidtoken"] || session?.bxUmidtoken || "",
2805
+ secChUa: cache.headers["sec-ch-ua"] || session?.secChUa,
2806
+ secChUaMobile: cache.headers["sec-ch-ua-mobile"] || session?.secChUaMobile,
2807
+ secChUaPlatform: cache.headers["sec-ch-ua-platform"] || session?.secChUaPlatform,
2808
+ version: cache.headers["version"] || session?.version,
2809
+ userId: session?.userId,
2810
+ tokenExpiresAt: parseJwtExpiry(newAccessToken) || undefined,
2811
+ refreshToken: newRefreshToken,
2812
+ capturedAt: Date.now(),
2813
+ });
2814
+
2815
+ return { success: true, token: newAccessToken };
2816
+ }
2817
+ }
2818
+ } catch {}
2819
+ }
2820
+
2821
+ // 3. Fallback: if refresh_token was invalid/absent or refresh failed, do clean API login
2822
+ const { getAccountCredentials } = await import("../core/accounts.ts");
2823
+ const creds = getAccountCredentials(accountId);
2824
+ if (creds && creds.email && creds.password) {
2825
+ const apiResult = await loginViaApi(page, creds.email, creds.password);
2826
+ if (apiResult.success) {
2827
+ const liveCookies = await page.context().cookies();
2828
+ const tokenCookie = liveCookies.find((c) => c.name === "token");
2829
+ if (tokenCookie) {
2830
+ const cookieStr = liveCookies.map((c) => `${c.name}=${c.value}`).join("; ");
2831
+ const cache = getHeaderCache(accountId);
2832
+ cache.headers.cookie = cookieStr;
2833
+ cache.lastRefresh = Date.now();
2834
+ markAccountHeadersReady(accountId);
2835
+ saveAuthSession(accountId, {
2836
+ cookie: cookieStr,
2837
+ userAgent: cache.headers["user-agent"] || session?.userAgent || "",
2838
+ bxV: cache.headers["bx-v"] || session?.bxV || "2.5.37",
2839
+ bxUa: cache.headers["bx-ua"] || session?.bxUa || "",
2840
+ bxUmidtoken: cache.headers["bx-umidtoken"] || session?.bxUmidtoken || "",
2841
+ secChUa: cache.headers["sec-ch-ua"] || session?.secChUa,
2842
+ secChUaMobile: cache.headers["sec-ch-ua-mobile"] || session?.secChUaMobile,
2843
+ secChUaPlatform: cache.headers["sec-ch-ua-platform"] || session?.secChUaPlatform,
2844
+ version: cache.headers["version"] || session?.version,
2845
+ userId: session?.userId,
2846
+ tokenExpiresAt: parseJwtExpiry(tokenCookie.value) || undefined,
2847
+ refreshToken: apiResult.refreshToken,
2848
+ capturedAt: Date.now(),
2849
+ });
2850
+ return { success: true, token: tokenCookie.value };
2851
+ }
2852
+ }
2853
+ }
2854
+
2855
+ return { success: false, error: "Could not refresh or re-authenticate token" };
2856
+ }
2857
+
2693
2858
  // ─── Header Capture ───────────────────────────────────────────────────────────
2694
2859
 
2695
2860
  /**
@@ -3410,17 +3575,10 @@ async function refreshHeadersInternal(
3410
3575
  };
3411
3576
 
3412
3577
  if (forceReauth) {
3413
- // If the page is already logged in, do NOT execute destructive password re-login!
3414
- const alreadyIn = await isPageLoggedIn(page, 2000).catch(() => false);
3415
- if (alreadyIn) {
3416
- const liveCookies = await page.context().cookies();
3417
- if (liveCookies.some((c) => c.name === "token")) {
3418
- const cookieStr = liveCookies.map((c) => `${c.name}=${c.value}`).join("; ");
3419
- cache.headers.cookie = cookieStr;
3420
- cache.lastRefresh = Date.now();
3421
- markAccountHeadersReady(accountId);
3422
- return;
3423
- }
3578
+ // Try silent token refresh first using the 30-day refresh_token or direct API signin!
3579
+ const refreshed = await refreshAccountToken(accountId);
3580
+ if (refreshed.success) {
3581
+ return;
3424
3582
  }
3425
3583
  await executeReauth();
3426
3584
  } else {
@@ -4019,53 +4177,17 @@ export async function keepAlivePlaywrightAccount(
4019
4177
  const now = Date.now();
4020
4178
  const currentUrl = page.url();
4021
4179
 
4022
- // Proactive session check: if token expires within 2 minutes, refresh in-page silently
4180
+ // Proactive session check: if token expires within 5 minutes, refresh in-page silently using refreshAccountToken
4023
4181
  const cookie = await getCookies(accountId);
4024
- if (cookie && isTokenExpiringSoon(cookie, 2)) {
4025
- try {
4026
- await page
4027
- .evaluate(async () => {
4028
- await fetch("https://auth.qwen.ai/api/v2/auths/refresh", {
4029
- method: "GET",
4030
- credentials: "include",
4031
- }).catch(() => {});
4032
- await fetch("/api/v1/auths/", {
4033
- method: "GET",
4034
- credentials: "include",
4035
- }).catch(() => {});
4036
- })
4037
- .catch(() => {});
4038
-
4039
- const liveCookies = await page.context().cookies();
4040
- const tokenCookie = liveCookies.find((c) => c.name === "token");
4041
- if (tokenCookie) {
4042
- const cookieStr = liveCookies.map((c) => `${c.name}=${c.value}`).join("; ");
4043
- const cache = getHeaderCache(accountId);
4044
- cache.headers.cookie = cookieStr;
4045
- cache.lastRefresh = Date.now();
4046
- markAccountHeadersReady(accountId);
4047
- lastKeepAliveNavigation.set(accountId, now);
4048
- touchAccountActivity(accountId);
4049
- return true;
4050
- }
4051
- } catch {}
4052
-
4053
- // If token refresh probe didn't restore cookies and the page is genuinely logged out:
4054
- const loggedIn = await isPageLoggedIn(page, 2000).catch(() => false);
4055
- if (!loggedIn) {
4056
- console.log(
4057
- `💓 [SessionKeeper] Account ${accountId} session expired; renewing session...`,
4058
- );
4059
- try {
4060
- await refreshHeadersInternal(accountId, config.timeouts.headers, true);
4061
- lastKeepAliveNavigation.set(accountId, now);
4062
- touchAccountActivity(accountId);
4063
- return true;
4064
- } catch (err) {
4065
- console.warn(
4066
- `[SessionKeeper] Proactive session renewal failed for ${accountId}: ${getErrorMessage(err)}`,
4067
- );
4068
- }
4182
+ if (cookie && isTokenExpiringSoon(cookie, 5)) {
4183
+ console.log(
4184
+ `💓 [SessionKeeper] Account ${accountId} access token expiring soon (<5m); silently refreshing...`,
4185
+ );
4186
+ const res = await refreshAccountToken(accountId);
4187
+ if (res.success) {
4188
+ lastKeepAliveNavigation.set(accountId, now);
4189
+ touchAccountActivity(accountId);
4190
+ return true;
4069
4191
  }
4070
4192
  }
4071
4193
 
@@ -71,12 +71,25 @@ async function createQwenChatSession(
71
71
  } catch {}
72
72
  }
73
73
 
74
- if (
75
- json?.data?.code === "Unauthorized" ||
76
- json?.code === "Unauthorized" ||
74
+ const isUnauthorized =
75
+ json?.data?.code?.toLowerCase() === "unauthorized" ||
76
+ json?.code?.toLowerCase() === "unauthorized" ||
77
77
  rawLower.includes("unauthorized") ||
78
- rawLower.includes("permission to access")
79
- ) {
78
+ rawLower.includes("permission to access") ||
79
+ rawLower.includes("token has expired") ||
80
+ rawLower.includes("401");
81
+
82
+ if (isUnauthorized) {
83
+ if (accountId) {
84
+ try {
85
+ const { refreshAccountToken } = await import("./playwright.ts");
86
+ const refreshed = await refreshAccountToken(accountId);
87
+ if (refreshed.success) {
88
+ const { headers: freshHeaders } = await getQwenHeaders(false, accountId);
89
+ return createQwenChatSession(freshHeaders, model, accountId, chatMode);
90
+ }
91
+ } catch {}
92
+ }
80
93
  throw new QwenUpstreamError(
81
94
  `Qwen create chat unauthorized: ${raw.substring(0, 300)}`,
82
95
  "Unauthorized",
@@ -79,7 +79,8 @@ export function buildQwenRequestHeaders(
79
79
  "bx-v": opts.bxV || "2.5.37",
80
80
  source: "web",
81
81
  version: opts.version || getQwenWebVersion(),
82
- timezone: opts.extra?.timezone || new Date().toString().split(" (")[0],
82
+ timezone: opts.extra?.timezone || opts.extra?.Timezone || new Date().toString().split(" (")[0],
83
+ Timezone: opts.extra?.Timezone || opts.extra?.timezone || new Date().toString().split(" (")[0],
83
84
  // Use the real browser client-hints when captured (anti-hardcoded); fall
84
85
  // back to the static fingerprint otherwise.
85
86
  "sec-ch-ua":
@@ -929,16 +929,28 @@ export async function requestQwenTextInBrowser(
929
929
  // If 401 Unauthorized in browser, try silent in-page token refresh before giving up
930
930
  if (response.status === 401) {
931
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) {
932
+ const rTok = localStorage.getItem("refresh_token");
933
+ if (rTok) {
934
+ const refreshRes = await fetch("https://auth.qwen.ai/api/v2/auths/refresh", {
935
+ method: "GET",
936
+ credentials: "include",
937
+ headers: {
938
+ accept: "application/json, text/plain, */*",
939
+ source: "web",
940
+ version: "0.3.11",
941
+ "x-request-origin": "https://chat.qwen.ai",
942
+ timezone: new Date().toString().split(" (")[0],
943
+ authorization: `Bearer ${rTok}`,
944
+ },
945
+ signal: AbortSignal.timeout(5000),
946
+ });
938
947
  const refreshJson: any = await refreshRes.json().catch(() => null);
939
- if (refreshJson && refreshJson.success === true && refreshJson.data?.token) {
940
- const freshTok = refreshJson.data.token;
948
+ const freshTok = refreshJson?.data?.access_token || refreshJson?.data?.token;
949
+ const nextRTok = refreshJson?.data?.refresh_token;
950
+ if (refreshJson?.success === true && freshTok) {
941
951
  localStorage.setItem("token", freshTok);
952
+ localStorage.setItem("access_token", freshTok);
953
+ if (nextRTok) localStorage.setItem("refresh_token", nextRTok);
942
954
  document.cookie = `token=${encodeURIComponent(freshTok)}; path=/; domain=.qwen.ai; max-age=31536000`;
943
955
  if (headers["authorization"] || headers["Authorization"]) {
944
956
  headers["authorization"] = `Bearer ${freshTok}`;
@@ -979,7 +991,7 @@ export async function requestQwenTextInBrowser(
979
991
  // Settings and personalization requests run as same-origin in-browser fetch
980
992
  // with appropriate Referer, keeping the page on the stable chat UI without
981
993
  // expensive page.goto navigations that can time out under load.
982
- const response = await withQwenBrowserPage<BrowserTextResponse>(
994
+ let response = await withQwenBrowserPage<BrowserTextResponse>(
983
995
  accountId,
984
996
  evaluateRequest,
985
997
  undefined,
@@ -987,6 +999,23 @@ export async function requestQwenTextInBrowser(
987
999
  recoverOnTimeout,
988
1000
  );
989
1001
 
1002
+ // If evaluateRequest still received 401, trigger outer token refresh and retry once
1003
+ if (response.status === 401 && accountId) {
1004
+ try {
1005
+ const { refreshAccountToken } = await import("./playwright.ts");
1006
+ const refreshed = await refreshAccountToken(accountId);
1007
+ if (refreshed.success) {
1008
+ response = await withQwenBrowserPage<BrowserTextResponse>(
1009
+ accountId,
1010
+ evaluateRequest,
1011
+ undefined,
1012
+ options.timeoutMs,
1013
+ recoverOnTimeout,
1014
+ );
1015
+ }
1016
+ } catch {}
1017
+ }
1018
+
990
1019
  return new Response(response.raw, {
991
1020
  status: response.status,
992
1021
  headers: response.contentType
@@ -1714,7 +1743,11 @@ export async function syncQwenRequestPersonalization(
1714
1743
  );
1715
1744
  try {
1716
1745
  currentSettings = null;
1717
- const { headers: freshHeaders } = await getQwenHeaders(true, accountId, true);
1746
+ const { refreshAccountToken } = await import("./playwright.ts");
1747
+ if (accountId) {
1748
+ await refreshAccountToken(accountId);
1749
+ }
1750
+ const { headers: freshHeaders } = await getQwenHeaders(false, accountId);
1718
1751
  requestHeaders = buildCapturedQwenHeaders(freshHeaders, {
1719
1752
  referer: qwenUrl("/settings/personalization"),
1720
1753
  });
@@ -2730,10 +2763,19 @@ async function createQwenStreamInternal(
2730
2763
  };
2731
2764
 
2732
2765
  // A new logical chat session should reuse the warmed header cache when available.
2733
- // Header recapture is much more expensive and should be reserved for real refresh/login cases,
2734
- // not for ordinary first prompts that simply need parent_id reset.
2766
+ // Proactively check if the access token is expiring soon (<3m) and refresh quietly before starting.
2767
+ let forceRefresh = options?.forceNewChat === true;
2768
+ if (!forceRefresh && accountId) {
2769
+ try {
2770
+ const basic = await getBasicHeaders(accountId);
2771
+ if (isTokenExpiringSoon(basic.cookie, 3)) {
2772
+ forceRefresh = true;
2773
+ }
2774
+ } catch {}
2775
+ }
2776
+
2735
2777
  const captured = await getQwenHeaders(
2736
- options?.forceNewChat === true,
2778
+ forceRefresh,
2737
2779
  accountId,
2738
2780
  );
2739
2781
  ensureNotAborted();
@@ -2749,7 +2791,7 @@ async function createQwenStreamInternal(
2749
2791
  if (options && "chatSessionId" in options) {
2750
2792
  if (options.chatSessionId === null || options.chatSessionId === "") {
2751
2793
  const acquired = await acquireNewQwenChatSession(
2752
- headers,
2794
+ activeHeaders,
2753
2795
  model,
2754
2796
  accountId,
2755
2797
  options?.chatMode ?? "thread",
@@ -2757,6 +2799,10 @@ async function createQwenStreamInternal(
2757
2799
  chatSessionId = acquired.chatId;
2758
2800
  leasedWarmChat = acquired.leasedFromPool;
2759
2801
  createdNewChat = true;
2802
+ try {
2803
+ const fresh = await getQwenHeaders(false, accountId);
2804
+ activeHeaders = fresh.headers;
2805
+ } catch {}
2760
2806
  } else {
2761
2807
  chatSessionId = options.chatSessionId;
2762
2808
  }
@@ -2764,7 +2810,7 @@ async function createQwenStreamInternal(
2764
2810
  chatSessionId = captured.chatSessionId;
2765
2811
  if (!chatSessionId) {
2766
2812
  const acquired = await acquireNewQwenChatSession(
2767
- headers,
2813
+ activeHeaders,
2768
2814
  model,
2769
2815
  accountId,
2770
2816
  options?.chatMode ?? "thread",
@@ -2772,6 +2818,10 @@ async function createQwenStreamInternal(
2772
2818
  chatSessionId = acquired.chatId;
2773
2819
  leasedWarmChat = acquired.leasedFromPool;
2774
2820
  createdNewChat = true;
2821
+ try {
2822
+ const fresh = await getQwenHeaders(false, accountId);
2823
+ activeHeaders = fresh.headers;
2824
+ } catch {}
2775
2825
  }
2776
2826
  }
2777
2827