qwenproxy-cli 1.6.9 → 1.6.10

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.
@@ -365,7 +365,24 @@ export async function isPageLoggedIn(
365
365
  return false;
366
366
  }
367
367
 
368
- const res = await fetch("/api/v1/auths/", { method: "GET" });
368
+ // In Qwen Web 0.3.26+, UI login stores the JWT in localStorage("token")
369
+ // without setting a "token=" cookie in document.cookie. Sync it so
370
+ // same-origin probes and context.cookies() always include it.
371
+ const lsToken = (localStorage.getItem("token") || "").trim();
372
+ if (lsToken && !/(?:^|;\s*)token=/.test(document.cookie || "")) {
373
+ try {
374
+ document.cookie = `token=${encodeURIComponent(lsToken)}; path=/; domain=.qwen.ai; max-age=31536000`;
375
+ } catch {}
376
+ }
377
+ const authHeaders: Record<string, string> = lsToken
378
+ ? { Authorization: `Bearer ${lsToken}` }
379
+ : {};
380
+
381
+ const res = await fetch("/api/v1/auths/", {
382
+ method: "GET",
383
+ headers: authHeaders,
384
+ credentials: "include",
385
+ });
369
386
  if (res.status !== 200) return false;
370
387
  const json: any = await res.json().catch(() => null);
371
388
  if (!json) return false;
@@ -398,6 +415,7 @@ export async function isPageLoggedIn(
398
415
  try {
399
416
  const settingsRes = await fetch("/api/v2/users/user/settings", {
400
417
  method: "GET",
418
+ headers: authHeaders,
401
419
  credentials: "include",
402
420
  signal: AbortSignal.timeout(3000),
403
421
  });
@@ -1819,8 +1837,15 @@ export async function initPlaywrightForAccount(
1819
1837
  c.name.toLowerCase().includes("session"),
1820
1838
  );
1821
1839
 
1840
+ let initialLoginAttempted = false;
1822
1841
  if (!hasAuthCookie && account.email && account.password) {
1823
- await loginToQwen(account.id, account.email, account.password);
1842
+ initialLoginAttempted = true;
1843
+ const loggedIn = await loginToQwen(account.id, account.email, account.password);
1844
+ if (!loggedIn) {
1845
+ throw new Error(
1846
+ `Authentication failed for account ${maskEmail(account.email)}`,
1847
+ );
1848
+ }
1824
1849
  }
1825
1850
 
1826
1851
  // Initial navigation to chat home
@@ -1832,13 +1857,21 @@ export async function initPlaywrightForAccount(
1832
1857
  await sleep(1500);
1833
1858
  const currentUrl = acctPage.url();
1834
1859
  const isAuthUrl = currentUrl.includes("/auth") || currentUrl.includes("/login");
1835
- if (isAuthUrl && account.email && account.password) {
1860
+ if (isAuthUrl && !initialLoginAttempted && account.email && account.password) {
1836
1861
  console.log(
1837
1862
  `[Playwright] Initial login needed for ${maskEmail(account.email)}, authenticating...`,
1838
1863
  );
1839
- await loginToQwen(account.id, account.email, account.password);
1864
+ const loggedIn = await loginToQwen(account.id, account.email, account.password);
1865
+ if (!loggedIn) {
1866
+ throw new Error(
1867
+ `Authentication failed for account ${maskEmail(account.email)}`,
1868
+ );
1869
+ }
1840
1870
  }
1841
1871
  } catch (err: any) {
1872
+ if (err?.message?.includes("Authentication failed for account")) {
1873
+ throw err;
1874
+ }
1842
1875
  console.warn(
1843
1876
  `⚠️ [Playwright] Initial navigation check for ${maskEmail(account.email)}: ${err.message}`,
1844
1877
  );
@@ -2551,10 +2584,31 @@ async function loginToQwen(
2551
2584
  const uiResult = await loginViaUi(page, email, password);
2552
2585
  if (uiResult.success) {
2553
2586
  cookieCaches.delete(accountId);
2554
- await saveStorageState(page.context(), accountId);
2555
2587
  try {
2556
- const liveCookies = await page.context().cookies();
2557
- const tokenCookie = liveCookies.find((c) => c.name === "token");
2588
+ let liveCookies = await page.context().cookies();
2589
+ let tokenCookie = liveCookies.find((c) => c.name === "token");
2590
+ if (!tokenCookie) {
2591
+ const lsTok =
2592
+ uiResult.token ||
2593
+ (await page.evaluate(() => (localStorage.getItem("token") || "").trim()).catch(() => ""));
2594
+ if (lsTok) {
2595
+ await page.context().addCookies([
2596
+ {
2597
+ name: "token",
2598
+ value: lsTok,
2599
+ domain: ".qwen.ai",
2600
+ path: "/",
2601
+ expires: Math.floor(Date.now() / 1000) + 31536000,
2602
+ httpOnly: false,
2603
+ secure: true,
2604
+ sameSite: "Lax",
2605
+ },
2606
+ ]).catch(() => {});
2607
+ liveCookies = await page.context().cookies();
2608
+ tokenCookie = liveCookies.find((c) => c.name === "token");
2609
+ }
2610
+ }
2611
+ await saveStorageState(page.context(), accountId);
2558
2612
  if (tokenCookie) {
2559
2613
  const cookieStr = liveCookies.map((c) => `${c.name}=${c.value}`).join("; ");
2560
2614
  const cache = getHeaderCache(accountId);
@@ -2563,7 +2617,9 @@ async function loginToQwen(
2563
2617
  markAccountHeadersReady(accountId);
2564
2618
  const { saveAuthSession } = await import("../core/database.ts");
2565
2619
  const exp = parseJwtExpiry(tokenCookie.value);
2566
- let rTok = uiResult.refreshToken;
2620
+ let rTok =
2621
+ uiResult.refreshToken ||
2622
+ liveCookies.find((c) => c.name === "refresh_token")?.value;
2567
2623
  if (!rTok) {
2568
2624
  try {
2569
2625
  rTok = (await page.evaluate(() => localStorage.getItem("refresh_token") || "")) || undefined;
@@ -2657,10 +2713,11 @@ async function loginViaApi(
2657
2713
 
2658
2714
  const signinUrl = "https://auth.qwen.ai/api/v2/auths/signin";
2659
2715
  const requestId = crypto.randomUUID();
2716
+ const webVersion = getQwenWebVersion() || "0.3.26";
2660
2717
 
2661
2718
  const evalRes = await page
2662
2719
  .evaluate(
2663
- async ({ email, password, signinUrl, requestId, tz }) => {
2720
+ async ({ email, password, signinUrl, requestId, tz, version }) => {
2664
2721
  try {
2665
2722
  const response = await fetch(signinUrl, {
2666
2723
  method: "POST",
@@ -2670,7 +2727,7 @@ async function loginViaApi(
2670
2727
  accept: "application/json, text/plain, */*",
2671
2728
  "content-type": "application/json",
2672
2729
  source: "web",
2673
- version: getQwenWebVersion() || "0.3.26",
2730
+ version,
2674
2731
  "x-request-origin": "https://chat.qwen.ai",
2675
2732
  timezone: tz,
2676
2733
  "x-request-id": requestId,
@@ -2683,7 +2740,14 @@ async function loginViaApi(
2683
2740
  return { ok: false, error: e.message };
2684
2741
  }
2685
2742
  },
2686
- { email, password: hashedPassword, signinUrl, requestId, tz: formatAsciiTimezone() },
2743
+ {
2744
+ email,
2745
+ password: hashedPassword,
2746
+ signinUrl,
2747
+ requestId,
2748
+ tz: formatAsciiTimezone(),
2749
+ version: webVersion,
2750
+ },
2687
2751
  )
2688
2752
  .catch(() => null);
2689
2753
 
@@ -2701,7 +2765,7 @@ async function loginViaApi(
2701
2765
  const fallbackUrl = qwenUrl("/api/v2/auths/signin");
2702
2766
  const fallbackRes = await page
2703
2767
  .evaluate(
2704
- async ({ email, password, fallbackUrl, requestId, tz }) => {
2768
+ async ({ email, password, fallbackUrl, requestId, tz, version }) => {
2705
2769
  try {
2706
2770
  const response = await fetch(fallbackUrl, {
2707
2771
  method: "POST",
@@ -2711,7 +2775,7 @@ async function loginViaApi(
2711
2775
  accept: "application/json, text/plain, */*",
2712
2776
  "content-type": "application/json",
2713
2777
  source: "web",
2714
- version: getQwenWebVersion() || "0.3.26",
2778
+ version,
2715
2779
  timezone: tz,
2716
2780
  "x-request-id": requestId,
2717
2781
  },
@@ -2723,7 +2787,14 @@ async function loginViaApi(
2723
2787
  return { ok: false, error: e.message };
2724
2788
  }
2725
2789
  },
2726
- { email, password: hashedPassword, fallbackUrl, requestId, tz: formatAsciiTimezone() },
2790
+ {
2791
+ email,
2792
+ password: hashedPassword,
2793
+ fallbackUrl,
2794
+ requestId,
2795
+ tz: formatAsciiTimezone(),
2796
+ version: webVersion,
2797
+ },
2727
2798
  )
2728
2799
  .catch(() => null);
2729
2800
 
@@ -3009,30 +3080,37 @@ async function loginViaUi(
3009
3080
 
3010
3081
  await sleep(500);
3011
3082
 
3012
- // Multi-modal submit: DOM click, Playwright force click, and Enter press
3083
+ // Submit the form: click the submit button if visible; only press Enter as fallback
3084
+ // (pressing Enter inside <form> after clicking submit triggers a native HTML form reload on Qwen 0.3.26).
3013
3085
  const submitSelector =
3014
3086
  '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("登录")';
3015
3087
  const submitButton = page.locator(submitSelector).first();
3016
3088
 
3017
- await page.evaluate((sel) => {
3018
- try {
3019
- const btn = document.querySelector(sel) as HTMLButtonElement | null;
3020
- if (btn) {
3021
- btn.removeAttribute("disabled");
3022
- btn.classList.remove("ant-btn-disabled");
3023
- btn.click();
3024
- }
3025
- } catch {}
3026
- }, submitSelector).catch(() => {});
3027
-
3089
+ let submitted = false;
3028
3090
  try {
3029
3091
  if (await submitButton.isVisible({ timeout: 2000 }).catch(() => false)) {
3030
- await submitButton.click({ force: true, timeout: 2000 }).catch(() => {});
3092
+ await submitButton.click({ timeout: 2000 }).catch(() => {});
3093
+ submitted = true;
3031
3094
  }
3032
3095
  } catch {}
3033
3096
 
3034
- const passInput = page.locator(passwordSelector).first();
3035
- await passInput.press("Enter", { timeout: 2000 }).catch(() => {});
3097
+ if (!submitted) {
3098
+ await page.evaluate(() => {
3099
+ try {
3100
+ const btn = document.querySelector(
3101
+ 'button.qwenchat-auth-pc-submit-button, button[type="submit"]',
3102
+ ) as HTMLButtonElement | null;
3103
+ if (btn) {
3104
+ btn.removeAttribute("disabled");
3105
+ btn.classList.remove("ant-btn-disabled");
3106
+ btn.click();
3107
+ }
3108
+ } catch {}
3109
+ }).catch(() => {});
3110
+
3111
+ const passInput = page.locator(passwordSelector).first();
3112
+ await passInput.press("Enter", { timeout: 2000 }).catch(() => {});
3113
+ }
3036
3114
  // Post-submit verification loop: wait up to 15s for captcha, redirect, cookies, or errors
3037
3115
  const postSubmitDeadline = Date.now() + 15_000;
3038
3116
  while (Date.now() < postSubmitDeadline) {
@@ -3054,9 +3132,12 @@ async function loginViaUi(
3054
3132
  ".ant-form-item-explain-error",
3055
3133
  ".ant-message-error",
3056
3134
  ".ant-message-notice",
3135
+ ".ant-message-custom-content",
3136
+ ".ant-notification-notice",
3057
3137
  '[role="alert"]',
3058
3138
  ".qwenchat-auth-error",
3059
3139
  ".auth-error-message",
3140
+ ".qwenchat-toast",
3060
3141
  ].join(", ");
3061
3142
  const errorEl = page.locator(errorSelector).first();
3062
3143
  if (await errorEl.isVisible().catch(() => false)) {
@@ -3072,8 +3153,15 @@ async function loginViaUi(
3072
3153
  }
3073
3154
 
3074
3155
  // Check if an OTP code verification screen appeared (e.g. 2FA / email verification code)
3075
- const codeSelector =
3076
- 'input[placeholder*="code" i], input[placeholder*="código" i], input[name*="code" i]';
3156
+ const codeSelector = [
3157
+ 'input[placeholder*="code" i]',
3158
+ 'input[placeholder*="código" i]',
3159
+ 'input[name*="code" i]',
3160
+ ".qwenchat-verification-code-input",
3161
+ ".qwenchat-verification-code-input-cell",
3162
+ '[aria-label*="Verification code" i]',
3163
+ '[aria-label*="código de verificação" i]',
3164
+ ].join(", ");
3077
3165
  const codeInput = page.locator(codeSelector).first();
3078
3166
  if (await codeInput.isVisible().catch(() => false)) {
3079
3167
  return {
@@ -3082,21 +3170,50 @@ async function loginViaUi(
3082
3170
  };
3083
3171
  }
3084
3172
 
3085
- // Check if auth cookie 'token' appeared
3173
+ // Check if auth cookie 'token' appeared or localStorage 'token' was populated (Qwen 0.3.26+)
3086
3174
  try {
3087
3175
  const liveCookies = await page.context().cookies();
3088
- if (liveCookies.some((c) => c.name === "token")) {
3089
- return { success: true };
3176
+ const rTokCookie = liveCookies.find((c) => c.name === "refresh_token")?.value;
3177
+ const tokCookie = liveCookies.find((c) => c.name === "token")?.value;
3178
+ if (tokCookie) {
3179
+ return { success: true, token: tokCookie, refreshToken: rTokCookie };
3090
3180
  }
3091
- } catch {}
3092
-
3093
- // Check if page redirected away from /auth and is logged in
3094
- const currentUrl = page.url();
3095
- if (!currentUrl.includes("/auth") && !currentUrl.includes("/login")) {
3096
- if (await isPageLoggedIn(page, 3000)) {
3097
- return { success: true };
3181
+ const currentUrl = page.url();
3182
+ if (!currentUrl.includes("/auth") && !currentUrl.includes("/login")) {
3183
+ const lsTok = await page
3184
+ .evaluate(() => {
3185
+ const t = (localStorage.getItem("token") || "").trim();
3186
+ if (t) {
3187
+ try {
3188
+ document.cookie = `token=${encodeURIComponent(t)}; path=/; domain=.qwen.ai; max-age=31536000`;
3189
+ } catch {}
3190
+ }
3191
+ return t;
3192
+ })
3193
+ .catch(() => "");
3194
+ if (lsTok) {
3195
+ await page
3196
+ .context()
3197
+ .addCookies([
3198
+ {
3199
+ name: "token",
3200
+ value: lsTok,
3201
+ domain: ".qwen.ai",
3202
+ path: "/",
3203
+ expires: Math.floor(Date.now() / 1000) + 31536000,
3204
+ httpOnly: false,
3205
+ secure: true,
3206
+ sameSite: "Lax",
3207
+ },
3208
+ ])
3209
+ .catch(() => {});
3210
+ return { success: true, token: lsTok, refreshToken: rTokCookie };
3211
+ }
3212
+ if (await isPageLoggedIn(page, 3000)) {
3213
+ return { success: true, refreshToken: rTokCookie };
3214
+ }
3098
3215
  }
3099
- }
3216
+ } catch {}
3100
3217
 
3101
3218
  await sleep(1000);
3102
3219
  }
@@ -3128,7 +3245,7 @@ export async function refreshAccountToken(
3128
3245
  return { success: false, error: "Account page not active" };
3129
3246
  }
3130
3247
 
3131
- // 1. Get refresh token from SQLite session or localStorage
3248
+ // 1. Get refresh token from SQLite session, localStorage, or browser cookies (Qwen 0.3.26+)
3132
3249
  const { getValidAuthSession, saveAuthSession } = await import("../core/database.ts");
3133
3250
  const session = getValidAuthSession(accountId);
3134
3251
  let refreshToken = session?.refreshToken;
@@ -3141,11 +3258,19 @@ export async function refreshAccountToken(
3141
3258
  } catch {}
3142
3259
  }
3143
3260
 
3261
+ if (!refreshToken) {
3262
+ try {
3263
+ const liveCookies = await page.context().cookies();
3264
+ refreshToken = liveCookies.find((c) => c.name === "refresh_token")?.value;
3265
+ } catch {}
3266
+ }
3267
+
3144
3268
  // 2. If we have a refresh token, invoke the working refresh endpoint in-page
3145
3269
  if (refreshToken) {
3146
3270
  try {
3271
+ const webVersion = getQwenWebVersion() || "0.3.26";
3147
3272
  const refreshResult = await page.evaluate(
3148
- async ({ rTok, tz }) => {
3273
+ async ({ rTok, tz, version }) => {
3149
3274
  try {
3150
3275
  const response = await fetch("https://auth.qwen.ai/api/v2/auths/refresh", {
3151
3276
  method: "GET",
@@ -3153,7 +3278,7 @@ export async function refreshAccountToken(
3153
3278
  headers: {
3154
3279
  accept: "application/json, text/plain, */*",
3155
3280
  source: "web",
3156
- version: getQwenWebVersion() || "0.3.26",
3281
+ version,
3157
3282
  "x-request-origin": "https://chat.qwen.ai",
3158
3283
  timezone: tz,
3159
3284
  authorization: `Bearer ${rTok}`,
@@ -3165,7 +3290,7 @@ export async function refreshAccountToken(
3165
3290
  return { ok: false, error: e.message };
3166
3291
  }
3167
3292
  },
3168
- { rTok: refreshToken, tz: formatAsciiTimezone() },
3293
+ { rTok: refreshToken, tz: formatAsciiTimezone(), version: webVersion },
3169
3294
  );
3170
3295
 
3171
3296
  const json = refreshResult?.data;