qwenproxy-cli 1.4.7 → 1.4.8

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.
@@ -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
 
@@ -77,6 +77,15 @@ async function createQwenChatSession(
77
77
  rawLower.includes("unauthorized") ||
78
78
  rawLower.includes("permission to access")
79
79
  ) {
80
+ if (accountId) {
81
+ try {
82
+ const { refreshAccountToken } = await import("./playwright.ts");
83
+ const refreshed = await refreshAccountToken(accountId);
84
+ if (refreshed.success) {
85
+ return createQwenChatSession(headers, model, accountId, chatMode);
86
+ }
87
+ } catch {}
88
+ }
80
89
  throw new QwenUpstreamError(
81
90
  `Qwen create chat unauthorized: ${raw.substring(0, 300)}`,
82
91
  "Unauthorized",
@@ -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
  });