qwenproxy-cli 1.4.2 → 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.
- package/dist/clean-cache.js +242 -109
- package/dist/delete-chats.js +295 -120
- package/dist/index.js +296 -121
- package/dist/login.js +297 -121
- package/dist/reset-cooldowns.js +242 -109
- package/package.json +1 -1
- package/src/core/version.ts +1 -1
- package/src/login.ts +1 -1
- package/src/services/playwright.ts +352 -130
|
@@ -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) {
|
|
@@ -2163,8 +2187,9 @@ async function loginToQwen(
|
|
|
2163
2187
|
|
|
2164
2188
|
if (attempt < maxAttempts) {
|
|
2165
2189
|
const backoffMs = attempt * 5_000;
|
|
2190
|
+
const failReason = uiResult.reason || apiResult.reason || "falha temporária";
|
|
2166
2191
|
console.warn(
|
|
2167
|
-
`⚠️ [Playwright] Login attempt ${attempt}/${maxAttempts} failed for ${maskEmail(email)} (${
|
|
2192
|
+
`⚠️ [Playwright] Login attempt ${attempt}/${maxAttempts} failed for ${maskEmail(email)} (${failReason}), retrying in ${backoffMs / 1000}s`,
|
|
2168
2193
|
);
|
|
2169
2194
|
await sleep(backoffMs);
|
|
2170
2195
|
}
|
|
@@ -2205,41 +2230,65 @@ async function loginViaApi(
|
|
|
2205
2230
|
.createHash("sha256")
|
|
2206
2231
|
.update(password)
|
|
2207
2232
|
.digest("hex");
|
|
2208
|
-
const signinUrl = qwenUrl("/api/v2/auths/signin");
|
|
2209
|
-
|
|
2210
|
-
let signinSuccess = false;
|
|
2211
|
-
let data: any = null;
|
|
2212
2233
|
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
},
|
|
2221
|
-
headers: {
|
|
2222
|
-
"content-type": "application/json",
|
|
2223
|
-
accept: "application/json, text/plain, */*",
|
|
2224
|
-
referer: qwenUrl("/auth"),
|
|
2225
|
-
origin: qwenOrigin(),
|
|
2226
|
-
source: "web",
|
|
2227
|
-
},
|
|
2228
|
-
timeout: 10_000,
|
|
2229
|
-
});
|
|
2230
|
-
data = await response.json().catch(() => null);
|
|
2231
|
-
signinSuccess = Boolean(data && (data.success === true || data.token));
|
|
2232
|
-
} 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);
|
|
2233
2241
|
}
|
|
2234
2242
|
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
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
|
|
2239
2288
|
.evaluate(
|
|
2240
|
-
async ({ email, password,
|
|
2289
|
+
async ({ email, password, fallbackUrl, requestId }) => {
|
|
2241
2290
|
try {
|
|
2242
|
-
const response = await fetch(
|
|
2291
|
+
const response = await fetch(fallbackUrl, {
|
|
2243
2292
|
method: "POST",
|
|
2244
2293
|
signal: AbortSignal.timeout(10_000),
|
|
2245
2294
|
credentials: "include",
|
|
@@ -2247,10 +2296,11 @@ async function loginViaApi(
|
|
|
2247
2296
|
accept: "application/json, text/plain, */*",
|
|
2248
2297
|
"content-type": "application/json",
|
|
2249
2298
|
source: "web",
|
|
2299
|
+
version: "0.3.11",
|
|
2250
2300
|
timezone: new Date().toString().split(" (")[0],
|
|
2251
2301
|
"x-request-id": requestId,
|
|
2252
2302
|
},
|
|
2253
|
-
body: JSON.stringify({ email, password
|
|
2303
|
+
body: JSON.stringify({ email, password }),
|
|
2254
2304
|
});
|
|
2255
2305
|
const json = await response.json().catch(() => null);
|
|
2256
2306
|
return { ok: response.ok, status: response.status, data: json };
|
|
@@ -2258,13 +2308,19 @@ async function loginViaApi(
|
|
|
2258
2308
|
return { ok: false, error: e.message };
|
|
2259
2309
|
}
|
|
2260
2310
|
},
|
|
2261
|
-
{ email, password: hashedPassword,
|
|
2311
|
+
{ email, password: hashedPassword, fallbackUrl, requestId },
|
|
2262
2312
|
)
|
|
2263
2313
|
.catch(() => null);
|
|
2264
2314
|
|
|
2265
|
-
if (
|
|
2266
|
-
data =
|
|
2267
|
-
signinSuccess = Boolean(
|
|
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
|
+
);
|
|
2268
2324
|
}
|
|
2269
2325
|
}
|
|
2270
2326
|
|
|
@@ -2280,7 +2336,14 @@ async function loginViaApi(
|
|
|
2280
2336
|
}
|
|
2281
2337
|
|
|
2282
2338
|
if (signinSuccess) {
|
|
2283
|
-
const 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
|
+
|
|
2284
2347
|
if (token) {
|
|
2285
2348
|
try {
|
|
2286
2349
|
await page.context().addCookies([
|
|
@@ -2307,13 +2370,18 @@ async function loginViaApi(
|
|
|
2307
2370
|
|
|
2308
2371
|
if (token) {
|
|
2309
2372
|
await page
|
|
2310
|
-
.evaluate(
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
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
|
+
)
|
|
2317
2385
|
.catch(() => {});
|
|
2318
2386
|
await page.reload({ waitUntil: "domcontentloaded" }).catch(() => {});
|
|
2319
2387
|
return { success: true };
|
|
@@ -2345,11 +2413,28 @@ async function loginViaUi(
|
|
|
2345
2413
|
password: string,
|
|
2346
2414
|
): Promise<LoginAttemptResult> {
|
|
2347
2415
|
try {
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
}
|
|
2352
|
-
|
|
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
|
+
}
|
|
2353
2438
|
|
|
2354
2439
|
// Wait for email input
|
|
2355
2440
|
const emailSelector = [
|
|
@@ -2361,7 +2446,7 @@ async function loginViaUi(
|
|
|
2361
2446
|
].join(", ");
|
|
2362
2447
|
try {
|
|
2363
2448
|
await page.waitForSelector(emailSelector, {
|
|
2364
|
-
timeout: Math.min(config.timeouts.page,
|
|
2449
|
+
timeout: Math.min(config.timeouts.page, 5_000),
|
|
2365
2450
|
});
|
|
2366
2451
|
} catch {
|
|
2367
2452
|
if (!page.url().includes("/auth") && (await isPageLoggedIn(page, 3000))) {
|
|
@@ -2378,35 +2463,77 @@ async function loginViaUi(
|
|
|
2378
2463
|
// (supporting English, Portuguese, Spanish, Chinese) to reveal the standard email + password form.
|
|
2379
2464
|
await clearVisibleChallenge(page);
|
|
2380
2465
|
|
|
2381
|
-
const
|
|
2382
|
-
return page
|
|
2383
|
-
|
|
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")
|
|
2384
2483
|
.filter({
|
|
2385
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,
|
|
2386
2485
|
})
|
|
2486
|
+
.filter({
|
|
2487
|
+
hasNotText: /(?:esqueci|forgot|olvid|reset|recover)/i,
|
|
2488
|
+
})
|
|
2387
2489
|
.first();
|
|
2388
|
-
};
|
|
2389
2490
|
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
if (await pwdModeBtn.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|
2393
|
-
await pwdModeBtn.click({ timeout: 2000 }).catch(() => {});
|
|
2491
|
+
if (await btn.isVisible({ timeout: 1500 }).catch(() => false)) {
|
|
2492
|
+
await btn.click({ timeout: 2000 }).catch(() => {});
|
|
2394
2493
|
await sleep(500);
|
|
2395
2494
|
}
|
|
2396
|
-
} catch {}
|
|
2397
2495
|
|
|
2398
|
-
|
|
2399
|
-
await page.fill(emailSelector, email);
|
|
2400
|
-
await sleep(300);
|
|
2496
|
+
if (await isPasswordInputPresent()) return;
|
|
2401
2497
|
|
|
2402
|
-
|
|
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
|
+
};
|
|
2519
|
+
|
|
2520
|
+
await revealPasswordMode();
|
|
2521
|
+
|
|
2522
|
+
// Fill email
|
|
2403
2523
|
try {
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
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 };
|
|
2408
2529
|
}
|
|
2409
|
-
|
|
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
|
+
}
|
|
2410
2537
|
|
|
2411
2538
|
// In Qwen Web, the password field is present on the same form.
|
|
2412
2539
|
// NEVER press Enter after filling email alone, as Qwen interprets that
|
|
@@ -2431,74 +2558,125 @@ async function loginViaUi(
|
|
|
2431
2558
|
}
|
|
2432
2559
|
|
|
2433
2560
|
// Fill password
|
|
2434
|
-
|
|
2435
|
-
|
|
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(() => {});
|
|
2436
2590
|
|
|
2437
|
-
// Prefer clicking the submit button;
|
|
2438
|
-
// The button starts disabled and only enables once both fields are filled.
|
|
2591
|
+
// Prefer clicking the submit button; do NOT press Enter if disabled
|
|
2439
2592
|
const submitSelector =
|
|
2440
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("登录")';
|
|
2441
2594
|
const submitButton = page.locator(submitSelector).first();
|
|
2442
2595
|
try {
|
|
2443
2596
|
await page.waitForSelector('button[type="submit"]:not([disabled])', {
|
|
2444
|
-
timeout:
|
|
2597
|
+
timeout: 4_000,
|
|
2445
2598
|
});
|
|
2446
2599
|
await submitButton.click();
|
|
2447
2600
|
} catch {
|
|
2448
|
-
await
|
|
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
|
+
}
|
|
2449
2609
|
}
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2610
|
+
// Post-submit verification loop: wait up to 15s for captcha, redirect, cookies, or errors
|
|
2611
|
+
const postSubmitDeadline = Date.now() + 15_000;
|
|
2612
|
+
while (Date.now() < postSubmitDeadline) {
|
|
2613
|
+
if (page.isClosed()) break;
|
|
2614
|
+
|
|
2615
|
+
// If a slider puzzle captcha appeared after submit, solve it automatically
|
|
2616
|
+
if (config.captcha.enabled) {
|
|
2617
|
+
await solveBaxiaCaptcha(page, {
|
|
2618
|
+
waitForMs: 1500,
|
|
2619
|
+
maxAttempts: config.captcha.maxAttempts,
|
|
2620
|
+
retryDelayMs: config.captcha.retryDelayMs,
|
|
2621
|
+
}).catch(() => {});
|
|
2622
|
+
}
|
|
2458
2623
|
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2624
|
+
// Check for UI error elements in DOM (Ant Design errors, alerts, toasts)
|
|
2625
|
+
const errorSelector = [
|
|
2626
|
+
".qwen-chat-v2-toast-text",
|
|
2627
|
+
".qwen-chat-v2-toast-content",
|
|
2628
|
+
".ant-form-item-explain-error",
|
|
2629
|
+
".ant-message-error",
|
|
2630
|
+
".ant-message-notice",
|
|
2631
|
+
'[role="alert"]',
|
|
2632
|
+
".qwenchat-auth-error",
|
|
2633
|
+
".auth-error-message",
|
|
2634
|
+
].join(", ");
|
|
2635
|
+
const errorEl = page.locator(errorSelector).first();
|
|
2636
|
+
if (await errorEl.isVisible().catch(() => false)) {
|
|
2637
|
+
const errorText = (await errorEl.innerText().catch(() => "")).trim();
|
|
2638
|
+
if (errorText) {
|
|
2639
|
+
const classified = classifyQwenAuthError(undefined, errorText);
|
|
2640
|
+
return {
|
|
2641
|
+
success: false,
|
|
2642
|
+
permanentFailure: classified.isPermanent,
|
|
2643
|
+
reason: `Formulário: ${classified.reason}`,
|
|
2644
|
+
};
|
|
2645
|
+
}
|
|
2646
|
+
}
|
|
2469
2647
|
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
".ant-message-error",
|
|
2476
|
-
".ant-message-notice",
|
|
2477
|
-
'[role="alert"]',
|
|
2478
|
-
".qwenchat-auth-error",
|
|
2479
|
-
".auth-error-message",
|
|
2480
|
-
].join(", ");
|
|
2481
|
-
const errorEl = page.locator(errorSelector).first();
|
|
2482
|
-
if (await errorEl.isVisible().catch(() => false)) {
|
|
2483
|
-
const errorText = (await errorEl.innerText().catch(() => "")).trim();
|
|
2484
|
-
if (errorText) {
|
|
2485
|
-
const classified = classifyQwenAuthError(undefined, errorText);
|
|
2648
|
+
// Check if an OTP code verification screen appeared (e.g. 2FA / email verification code)
|
|
2649
|
+
const codeSelector =
|
|
2650
|
+
'input[placeholder*="code" i], input[placeholder*="código" i], input[name*="code" i]';
|
|
2651
|
+
const codeInput = page.locator(codeSelector).first();
|
|
2652
|
+
if (await codeInput.isVisible().catch(() => false)) {
|
|
2486
2653
|
return {
|
|
2487
2654
|
success: false,
|
|
2488
|
-
|
|
2489
|
-
reason: `Formulário: ${classified.reason}`,
|
|
2655
|
+
reason: "Conta requer código de verificação enviado por e-mail (2FA/OTP)",
|
|
2490
2656
|
};
|
|
2491
2657
|
}
|
|
2492
|
-
}
|
|
2493
2658
|
|
|
2494
|
-
|
|
2495
|
-
|
|
2659
|
+
// Check if auth cookie 'token' appeared
|
|
2660
|
+
try {
|
|
2661
|
+
const liveCookies = await page.context().cookies();
|
|
2662
|
+
if (liveCookies.some((c) => c.name === "token")) {
|
|
2663
|
+
return { success: true };
|
|
2664
|
+
}
|
|
2665
|
+
} catch {}
|
|
2496
2666
|
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2667
|
+
// Check if page redirected away from /auth and is logged in
|
|
2668
|
+
const currentUrl = page.url();
|
|
2669
|
+
if (!currentUrl.includes("/auth") && !currentUrl.includes("/login")) {
|
|
2670
|
+
if (await isPageLoggedIn(page, 3000)) {
|
|
2671
|
+
return { success: true };
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
2674
|
+
|
|
2675
|
+
await sleep(1000);
|
|
2676
|
+
}
|
|
2677
|
+
|
|
2678
|
+
// Final check
|
|
2679
|
+
if (await isPageLoggedIn(page, 3000)) {
|
|
2502
2680
|
return { success: true };
|
|
2503
2681
|
}
|
|
2504
2682
|
|
|
@@ -3232,6 +3410,18 @@ async function refreshHeadersInternal(
|
|
|
3232
3410
|
};
|
|
3233
3411
|
|
|
3234
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
|
+
}
|
|
3235
3425
|
await executeReauth();
|
|
3236
3426
|
} else {
|
|
3237
3427
|
try {
|
|
@@ -3829,21 +4019,53 @@ export async function keepAlivePlaywrightAccount(
|
|
|
3829
4019
|
const now = Date.now();
|
|
3830
4020
|
const currentUrl = page.url();
|
|
3831
4021
|
|
|
3832
|
-
// Proactive session check: if token expires within
|
|
4022
|
+
// Proactive session check: if token expires within 2 minutes, refresh in-page silently
|
|
3833
4023
|
const cookie = await getCookies(accountId);
|
|
3834
|
-
if (cookie && isTokenExpiringSoon(cookie,
|
|
3835
|
-
console.log(
|
|
3836
|
-
`💓 [SessionKeeper] Account ${accountId} token expires within 45m; proactively renewing session...`,
|
|
3837
|
-
);
|
|
4024
|
+
if (cookie && isTokenExpiringSoon(cookie, 2)) {
|
|
3838
4025
|
try {
|
|
3839
|
-
await
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
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...`,
|
|
3846
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
|
+
}
|
|
3847
4069
|
}
|
|
3848
4070
|
}
|
|
3849
4071
|
|