qwenproxy-cli 1.4.3 → 1.4.7

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.
@@ -995,8 +995,29 @@ export function getStealthScript(profile: FingerprintProfile): string {
995
995
  const _toBlob = HTMLCanvasElement.prototype.toBlob;
996
996
  const _getImageData = CanvasRenderingContext2D.prototype.getImageData;
997
997
 
998
+ function isCaptchaCanvas(canvas) {
999
+ try {
1000
+ if (!canvas) return false;
1001
+ const id = (canvas.id || "").toLowerCase();
1002
+ const cls = (canvas.className || "").toLowerCase();
1003
+ return (
1004
+ id.includes("baxia") ||
1005
+ id.includes("nc_") ||
1006
+ id.includes("captcha") ||
1007
+ id.includes("punish") ||
1008
+ cls.includes("baxia") ||
1009
+ cls.includes("nc_") ||
1010
+ cls.includes("captcha") ||
1011
+ Boolean(canvas.closest?.("#baxia-dialog, #baxia-punish, [class*='baxia'], [class*='nc_'], .auth-layout"))
1012
+ );
1013
+ } catch(e) {
1014
+ return false;
1015
+ }
1016
+ }
1017
+
998
1018
  function addNoise(canvas) {
999
1019
  try {
1020
+ if (isCaptchaCanvas(canvas)) return;
1000
1021
  const ctx = canvas.getContext('2d');
1001
1022
  if (!ctx) return;
1002
1023
  const style = ctx.fillStyle;
@@ -1020,6 +1041,9 @@ export function getStealthScript(profile: FingerprintProfile): string {
1020
1041
 
1021
1042
  CanvasRenderingContext2D.prototype.getImageData = function(x, y, w, h) {
1022
1043
  const imageData = _getImageData.apply(this, arguments);
1044
+ if (isCaptchaCanvas(this.canvas)) {
1045
+ return imageData;
1046
+ }
1023
1047
  const data = imageData.data;
1024
1048
  const maxPixels = Math.min(data.length / 4, 2500);
1025
1049
  for (let i = 0; i < maxPixels * 4; i += 4) {
@@ -2206,41 +2230,65 @@ async function loginViaApi(
2206
2230
  .createHash("sha256")
2207
2231
  .update(password)
2208
2232
  .digest("hex");
2209
- const signinUrl = qwenUrl("/api/v2/auths/signin");
2210
-
2211
- let signinSuccess = false;
2212
- let data: any = null;
2213
2233
 
2214
- if (page.request && typeof page.request.post === "function") {
2215
- try {
2216
- const response = await page.request.post(signinUrl, {
2217
- data: {
2218
- email,
2219
- password: hashedPassword,
2220
- login_type: "email",
2221
- },
2222
- headers: {
2223
- "content-type": "application/json",
2224
- accept: "application/json, text/plain, */*",
2225
- referer: qwenUrl("/auth"),
2226
- origin: qwenOrigin(),
2227
- source: "web",
2228
- },
2229
- timeout: 10_000,
2230
- });
2231
- data = await response.json().catch(() => null);
2232
- signinSuccess = Boolean(data && (data.success === true || data.token));
2233
- } catch {}
2234
+ // Ensure the page is loaded at /auth so that same-site/CORS credentials and Baxia WAF headers are active
2235
+ if (!page.url().includes("/auth")) {
2236
+ await page.goto(qwenUrl("/auth"), {
2237
+ waitUntil: "domcontentloaded",
2238
+ timeout: config.timeouts.navigation,
2239
+ }).catch(() => {});
2240
+ await sleep(1000);
2234
2241
  }
2235
2242
 
2236
- if (!signinSuccess && !data) {
2237
- // Fallback to in-page evaluate fetch if page.request was unavailable
2238
- const requestId = crypto.randomUUID();
2239
- const evalRes = await page
2243
+ const signinUrl = "https://auth.qwen.ai/api/v2/auths/signin";
2244
+ const requestId = crypto.randomUUID();
2245
+
2246
+ const evalRes = await page
2247
+ .evaluate(
2248
+ async ({ email, password, signinUrl, requestId }) => {
2249
+ try {
2250
+ const response = await fetch(signinUrl, {
2251
+ method: "POST",
2252
+ signal: AbortSignal.timeout(15_000),
2253
+ credentials: "include",
2254
+ headers: {
2255
+ accept: "application/json, text/plain, */*",
2256
+ "content-type": "application/json",
2257
+ source: "web",
2258
+ version: "0.3.11",
2259
+ "x-request-origin": "https://chat.qwen.ai",
2260
+ timezone: new Date().toString().split(" (")[0],
2261
+ "x-request-id": requestId,
2262
+ },
2263
+ body: JSON.stringify({ email, password }),
2264
+ });
2265
+ const json = await response.json().catch(() => null);
2266
+ return { ok: response.ok, status: response.status, data: json };
2267
+ } catch (e: any) {
2268
+ return { ok: false, error: e.message };
2269
+ }
2270
+ },
2271
+ { email, password: hashedPassword, signinUrl, requestId },
2272
+ )
2273
+ .catch(() => null);
2274
+
2275
+ let data: any = evalRes?.data;
2276
+ let signinSuccess = Boolean(
2277
+ data &&
2278
+ (data.success === true ||
2279
+ data.token ||
2280
+ data.data?.access_token ||
2281
+ data.data?.token),
2282
+ );
2283
+
2284
+ // Fallback to legacy chat.qwen.ai endpoint if auth.qwen.ai did not succeed
2285
+ if (!signinSuccess && !data?.data?.code) {
2286
+ const fallbackUrl = qwenUrl("/api/v2/auths/signin");
2287
+ const fallbackRes = await page
2240
2288
  .evaluate(
2241
- async ({ email, password, signinUrl, requestId }) => {
2289
+ async ({ email, password, fallbackUrl, requestId }) => {
2242
2290
  try {
2243
- const response = await fetch(signinUrl, {
2291
+ const response = await fetch(fallbackUrl, {
2244
2292
  method: "POST",
2245
2293
  signal: AbortSignal.timeout(10_000),
2246
2294
  credentials: "include",
@@ -2248,10 +2296,11 @@ async function loginViaApi(
2248
2296
  accept: "application/json, text/plain, */*",
2249
2297
  "content-type": "application/json",
2250
2298
  source: "web",
2299
+ version: "0.3.11",
2251
2300
  timezone: new Date().toString().split(" (")[0],
2252
2301
  "x-request-id": requestId,
2253
2302
  },
2254
- body: JSON.stringify({ email, password, login_type: "email" }),
2303
+ body: JSON.stringify({ email, password }),
2255
2304
  });
2256
2305
  const json = await response.json().catch(() => null);
2257
2306
  return { ok: response.ok, status: response.status, data: json };
@@ -2259,13 +2308,19 @@ async function loginViaApi(
2259
2308
  return { ok: false, error: e.message };
2260
2309
  }
2261
2310
  },
2262
- { email, password: hashedPassword, signinUrl, requestId },
2311
+ { email, password: hashedPassword, fallbackUrl, requestId },
2263
2312
  )
2264
2313
  .catch(() => null);
2265
2314
 
2266
- if (evalRes?.data) {
2267
- data = evalRes.data;
2268
- signinSuccess = Boolean(data && (data.success === true || data.token));
2315
+ if (fallbackRes?.data) {
2316
+ data = fallbackRes.data;
2317
+ signinSuccess = Boolean(
2318
+ data &&
2319
+ (data.success === true ||
2320
+ data.token ||
2321
+ data.data?.access_token ||
2322
+ data.data?.token),
2323
+ );
2269
2324
  }
2270
2325
  }
2271
2326
 
@@ -2281,7 +2336,14 @@ async function loginViaApi(
2281
2336
  }
2282
2337
 
2283
2338
  if (signinSuccess) {
2284
- const token = data?.data?.token || data?.token;
2339
+ const token =
2340
+ data?.data?.access_token ||
2341
+ data?.data?.token ||
2342
+ data?.access_token ||
2343
+ data?.token;
2344
+ const refreshToken =
2345
+ data?.data?.refresh_token || data?.refresh_token;
2346
+
2285
2347
  if (token) {
2286
2348
  try {
2287
2349
  await page.context().addCookies([
@@ -2308,13 +2370,18 @@ async function loginViaApi(
2308
2370
 
2309
2371
  if (token) {
2310
2372
  await page
2311
- .evaluate((tok) => {
2312
- try {
2313
- localStorage.removeItem("qwen_token_logged_out_marker");
2314
- localStorage.setItem("token", tok);
2315
- document.cookie = `token=${encodeURIComponent(tok)}; path=/; domain=.qwen.ai; max-age=31536000`;
2316
- } catch {}
2317
- }, token)
2373
+ .evaluate(
2374
+ ({ tok, rTok }) => {
2375
+ try {
2376
+ localStorage.removeItem("qwen_token_logged_out_marker");
2377
+ localStorage.setItem("token", tok);
2378
+ localStorage.setItem("access_token", tok);
2379
+ if (rTok) localStorage.setItem("refresh_token", rTok);
2380
+ document.cookie = `token=${encodeURIComponent(tok)}; path=/; domain=.qwen.ai; max-age=31536000`;
2381
+ } catch {}
2382
+ },
2383
+ { tok: token, rTok: refreshToken },
2384
+ )
2318
2385
  .catch(() => {});
2319
2386
  await page.reload({ waitUntil: "domcontentloaded" }).catch(() => {});
2320
2387
  return { success: true };
@@ -2346,11 +2413,28 @@ async function loginViaUi(
2346
2413
  password: string,
2347
2414
  ): Promise<LoginAttemptResult> {
2348
2415
  try {
2349
- await page.goto(qwenUrl("/auth"), {
2350
- waitUntil: "domcontentloaded",
2351
- timeout: config.timeouts.navigation,
2352
- });
2353
- await sleep(1500);
2416
+ // 0. If the page is ALREADY logged in, avoid touching /auth altogether!
2417
+ if (!page.url().includes("/auth") && (await isPageLoggedIn(page, 2000).catch(() => false))) {
2418
+ return { success: true };
2419
+ }
2420
+
2421
+ try {
2422
+ await page.goto(qwenUrl("/auth"), {
2423
+ waitUntil: "domcontentloaded",
2424
+ timeout: Math.min(config.timeouts.navigation, 15_000),
2425
+ });
2426
+ await sleep(1000);
2427
+ } catch (gotoErr: any) {
2428
+ // If navigation was aborted because the page redirected to / (because already logged in):
2429
+ if (!page.url().includes("/auth") && (await isPageLoggedIn(page, 3000).catch(() => false))) {
2430
+ return { success: true };
2431
+ }
2432
+ }
2433
+
2434
+ // Check if redirect occurred after navigation
2435
+ if (!page.url().includes("/auth") && (await isPageLoggedIn(page, 3000).catch(() => false))) {
2436
+ return { success: true };
2437
+ }
2354
2438
 
2355
2439
  // Wait for email input
2356
2440
  const emailSelector = [
@@ -2362,7 +2446,7 @@ async function loginViaUi(
2362
2446
  ].join(", ");
2363
2447
  try {
2364
2448
  await page.waitForSelector(emailSelector, {
2365
- timeout: Math.min(config.timeouts.page, 8_000),
2449
+ timeout: Math.min(config.timeouts.page, 5_000),
2366
2450
  });
2367
2451
  } catch {
2368
2452
  if (!page.url().includes("/auth") && (await isPageLoggedIn(page, 3000))) {
@@ -2379,35 +2463,77 @@ async function loginViaUi(
2379
2463
  // (supporting English, Portuguese, Spanish, Chinese) to reveal the standard email + password form.
2380
2464
  await clearVisibleChallenge(page);
2381
2465
 
2382
- const findPasswordModeButton = () => {
2383
- return page
2384
- .locator('button, [role="button"], a, div')
2466
+ const isPasswordInputPresent = async () => {
2467
+ return (await page.locator('input[type="password"], input[name="password"]').count().catch(() => 0)) > 0;
2468
+ };
2469
+
2470
+ const revealPasswordMode = async () => {
2471
+ // If already on /forget, navigate back to /auth
2472
+ if (page.url().includes("/forget")) {
2473
+ await page.goto(qwenUrl("/auth"), { waitUntil: "domcontentloaded", timeout: 10_000 }).catch(() => {});
2474
+ await sleep(1000);
2475
+ }
2476
+
2477
+ // If password input is already in DOM, do NOT click any switch buttons!
2478
+ if (await isPasswordInputPresent()) return;
2479
+
2480
+ // 1. Playwright button locator with multilingual matching (strictly excluding "Esqueci a senha" / "Forgot password")
2481
+ const btn = page
2482
+ .locator("button")
2385
2483
  .filter({
2386
2484
  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,
2387
2485
  })
2486
+ .filter({
2487
+ hasNotText: /(?:esqueci|forgot|olvid|reset|recover)/i,
2488
+ })
2388
2489
  .first();
2389
- };
2390
2490
 
2391
- try {
2392
- const pwdModeBtn = findPasswordModeButton();
2393
- if (await pwdModeBtn.isVisible({ timeout: 2000 }).catch(() => false)) {
2394
- await pwdModeBtn.click({ timeout: 2000 }).catch(() => {});
2491
+ if (await btn.isVisible({ timeout: 1500 }).catch(() => false)) {
2492
+ await btn.click({ timeout: 2000 }).catch(() => {});
2395
2493
  await sleep(500);
2396
2494
  }
2397
- } catch {}
2398
2495
 
2399
- // Fill email
2400
- await page.fill(emailSelector, email);
2401
- await sleep(300);
2496
+ if (await isPasswordInputPresent()) return;
2497
+
2498
+ // 2. In-page evaluate fail-safe to trigger direct DOM click on the password button
2499
+ await page
2500
+ .evaluate(() => {
2501
+ try {
2502
+ const buttons = Array.from(document.querySelectorAll("button, [role='button'], a"));
2503
+ for (const b of buttons) {
2504
+ const txt = ((b as HTMLElement).innerText || "").trim().toLowerCase();
2505
+ if (
2506
+ /(?: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.test(txt) &&
2507
+ !txt.includes("esqueci") && !txt.includes("forgot") && !txt.includes("olvid") && !txt.includes("reset")
2508
+ ) {
2509
+ (b as HTMLElement).click();
2510
+ return true;
2511
+ }
2512
+ }
2513
+ } catch {}
2514
+ return false;
2515
+ })
2516
+ .catch(() => false);
2517
+ await sleep(500);
2518
+ };
2402
2519
 
2403
- // If "Log in with a password" button appeared after typing email, click it
2520
+ await revealPasswordMode();
2521
+
2522
+ // Fill email
2404
2523
  try {
2405
- const pwdModeBtn = findPasswordModeButton();
2406
- if (await pwdModeBtn.isVisible({ timeout: 1500 }).catch(() => false)) {
2407
- await pwdModeBtn.click({ timeout: 2000 }).catch(() => {});
2408
- await sleep(500);
2524
+ await page.fill(emailSelector, email, { timeout: 8_000 });
2525
+ await sleep(300);
2526
+ } catch {
2527
+ if (!page.url().includes("/auth") && (await isPageLoggedIn(page, 3000))) {
2528
+ return { success: true };
2409
2529
  }
2410
- } catch {}
2530
+ throw new Error(`Campo de e-mail não encontrado na tela de autenticação`);
2531
+ }
2532
+
2533
+ // If password field is still not visible after typing email, trigger reveal again
2534
+ if (!(await isPasswordInputPresent())) {
2535
+ await revealPasswordMode();
2536
+ }
2411
2537
 
2412
2538
  // In Qwen Web, the password field is present on the same form.
2413
2539
  // NEVER press Enter after filling email alone, as Qwen interprets that
@@ -2432,21 +2558,54 @@ async function loginViaUi(
2432
2558
  }
2433
2559
 
2434
2560
  // Fill password
2435
- await page.fill(passwordSelector, password);
2436
- await sleep(500);
2561
+ try {
2562
+ await page.fill(passwordSelector, password, { timeout: 8_000 });
2563
+ await sleep(500);
2564
+ } catch {
2565
+ if (!page.url().includes("/auth") && (await isPageLoggedIn(page, 3000))) {
2566
+ return { success: true };
2567
+ }
2568
+ throw new Error(`Campo de senha não encontrado na tela de autenticação`);
2569
+ }
2570
+
2571
+ // Dispatch input/change/blur events so Ant Design / React enables the submit button
2572
+ await page
2573
+ .evaluate(() => {
2574
+ try {
2575
+ const emailEl = document.querySelector('input[name="email"], input[type="email"], input[type="text"]');
2576
+ const passEl = document.querySelector('input[type="password"], input[name="password"]');
2577
+ if (emailEl) {
2578
+ emailEl.dispatchEvent(new Event("input", { bubbles: true }));
2579
+ emailEl.dispatchEvent(new Event("change", { bubbles: true }));
2580
+ emailEl.dispatchEvent(new Event("blur", { bubbles: true }));
2581
+ }
2582
+ if (passEl) {
2583
+ passEl.dispatchEvent(new Event("input", { bubbles: true }));
2584
+ passEl.dispatchEvent(new Event("change", { bubbles: true }));
2585
+ passEl.dispatchEvent(new Event("blur", { bubbles: true }));
2586
+ }
2587
+ } catch {}
2588
+ })
2589
+ .catch(() => {});
2437
2590
 
2438
- // Prefer clicking the submit button; fall back to pressing Enter.
2439
- // The button starts disabled and only enables once both fields are filled.
2591
+ // Prefer clicking the submit button; do NOT press Enter if disabled
2440
2592
  const submitSelector =
2441
2593
  '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("登录")';
2442
2594
  const submitButton = page.locator(submitSelector).first();
2443
2595
  try {
2444
2596
  await page.waitForSelector('button[type="submit"]:not([disabled])', {
2445
- timeout: 5_000,
2597
+ timeout: 4_000,
2446
2598
  });
2447
2599
  await submitButton.click();
2448
2600
  } catch {
2449
- await page.keyboard.press("Enter");
2601
+ const isDisabled = await submitButton.isDisabled().catch(() => true);
2602
+ if (!isDisabled) {
2603
+ await submitButton.click({ force: true }).catch(() => {});
2604
+ } else {
2605
+ if (!page.url().includes("/auth") && (await isPageLoggedIn(page, 2000))) {
2606
+ return { success: true };
2607
+ }
2608
+ }
2450
2609
  }
2451
2610
  // Post-submit verification loop: wait up to 15s for captcha, redirect, cookies, or errors
2452
2611
  const postSubmitDeadline = Date.now() + 15_000;
@@ -3251,6 +3410,18 @@ async function refreshHeadersInternal(
3251
3410
  };
3252
3411
 
3253
3412
  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
+ }
3424
+ }
3254
3425
  await executeReauth();
3255
3426
  } else {
3256
3427
  try {
@@ -3848,21 +4019,53 @@ export async function keepAlivePlaywrightAccount(
3848
4019
  const now = Date.now();
3849
4020
  const currentUrl = page.url();
3850
4021
 
3851
- // Proactive session check: if token expires within 45 minutes, refresh proactively
4022
+ // Proactive session check: if token expires within 2 minutes, refresh in-page silently
3852
4023
  const cookie = await getCookies(accountId);
3853
- if (cookie && isTokenExpiringSoon(cookie, 45)) {
3854
- console.log(
3855
- `💓 [SessionKeeper] Account ${accountId} token expires within 45m; proactively renewing session...`,
3856
- );
4024
+ if (cookie && isTokenExpiringSoon(cookie, 2)) {
3857
4025
  try {
3858
- await refreshHeadersInternal(accountId, config.timeouts.headers, true);
3859
- lastKeepAliveNavigation.set(accountId, now);
3860
- touchAccountActivity(accountId);
3861
- return true;
3862
- } catch (err) {
3863
- console.warn(
3864
- `[SessionKeeper] Proactive session renewal failed for ${accountId}: ${getErrorMessage(err)}`,
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...`,
3865
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
+ }
3866
4069
  }
3867
4070
  }
3868
4071