qwenproxy-cli 1.4.3 → 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.
- package/dist/clean-cache.js +205 -75
- package/dist/delete-chats.js +397 -93
- package/dist/index.js +399 -95
- package/dist/login.js +345 -83
- package/dist/reset-cooldowns.js +205 -75
- package/package.json +1 -1
- package/src/core/database.ts +10 -5
- package/src/core/version.ts +1 -1
- package/src/routes/chat/retry-policy.ts +4 -3
- package/src/services/playwright.ts +408 -83
- package/src/services/qwen-chat-pool.ts +9 -0
- package/src/services/qwen.ts +43 -10
|
@@ -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) {
|
|
@@ -1981,6 +2005,8 @@ export interface LoginAttemptResult {
|
|
|
1981
2005
|
success: boolean;
|
|
1982
2006
|
permanentFailure?: boolean;
|
|
1983
2007
|
reason?: string;
|
|
2008
|
+
token?: string;
|
|
2009
|
+
refreshToken?: string;
|
|
1984
2010
|
}
|
|
1985
2011
|
|
|
1986
2012
|
export function classifyQwenAuthError(
|
|
@@ -2096,6 +2122,7 @@ async function loginToQwen(
|
|
|
2096
2122
|
secChUaPlatform: cache.headers["sec-ch-ua-platform"] || undefined,
|
|
2097
2123
|
version: cache.headers["version"] || undefined,
|
|
2098
2124
|
tokenExpiresAt: exp || undefined,
|
|
2125
|
+
refreshToken: apiResult.refreshToken,
|
|
2099
2126
|
capturedAt: Date.now(),
|
|
2100
2127
|
});
|
|
2101
2128
|
}
|
|
@@ -2131,6 +2158,12 @@ async function loginToQwen(
|
|
|
2131
2158
|
markAccountHeadersReady(accountId);
|
|
2132
2159
|
const { saveAuthSession } = await import("../core/database.ts");
|
|
2133
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
|
+
}
|
|
2134
2167
|
saveAuthSession(accountId, {
|
|
2135
2168
|
cookie: cookieStr,
|
|
2136
2169
|
userAgent: cache.headers["user-agent"] || "",
|
|
@@ -2142,6 +2175,7 @@ async function loginToQwen(
|
|
|
2142
2175
|
secChUaPlatform: cache.headers["sec-ch-ua-platform"] || undefined,
|
|
2143
2176
|
version: cache.headers["version"] || undefined,
|
|
2144
2177
|
tokenExpiresAt: exp || undefined,
|
|
2178
|
+
refreshToken: rTok,
|
|
2145
2179
|
capturedAt: Date.now(),
|
|
2146
2180
|
});
|
|
2147
2181
|
}
|
|
@@ -2206,41 +2240,65 @@ async function loginViaApi(
|
|
|
2206
2240
|
.createHash("sha256")
|
|
2207
2241
|
.update(password)
|
|
2208
2242
|
.digest("hex");
|
|
2209
|
-
const signinUrl = qwenUrl("/api/v2/auths/signin");
|
|
2210
|
-
|
|
2211
|
-
let signinSuccess = false;
|
|
2212
|
-
let data: any = null;
|
|
2213
2243
|
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
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 {}
|
|
2244
|
+
// Ensure the page is loaded at /auth so that same-site/CORS credentials and Baxia WAF headers are active
|
|
2245
|
+
if (!page.url().includes("/auth")) {
|
|
2246
|
+
await page.goto(qwenUrl("/auth"), {
|
|
2247
|
+
waitUntil: "domcontentloaded",
|
|
2248
|
+
timeout: config.timeouts.navigation,
|
|
2249
|
+
}).catch(() => {});
|
|
2250
|
+
await sleep(1000);
|
|
2234
2251
|
}
|
|
2235
2252
|
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2253
|
+
const signinUrl = "https://auth.qwen.ai/api/v2/auths/signin";
|
|
2254
|
+
const requestId = crypto.randomUUID();
|
|
2255
|
+
|
|
2256
|
+
const evalRes = await page
|
|
2257
|
+
.evaluate(
|
|
2258
|
+
async ({ email, password, signinUrl, requestId }) => {
|
|
2259
|
+
try {
|
|
2260
|
+
const response = await fetch(signinUrl, {
|
|
2261
|
+
method: "POST",
|
|
2262
|
+
signal: AbortSignal.timeout(15_000),
|
|
2263
|
+
credentials: "include",
|
|
2264
|
+
headers: {
|
|
2265
|
+
accept: "application/json, text/plain, */*",
|
|
2266
|
+
"content-type": "application/json",
|
|
2267
|
+
source: "web",
|
|
2268
|
+
version: "0.3.11",
|
|
2269
|
+
"x-request-origin": "https://chat.qwen.ai",
|
|
2270
|
+
timezone: new Date().toString().split(" (")[0],
|
|
2271
|
+
"x-request-id": requestId,
|
|
2272
|
+
},
|
|
2273
|
+
body: JSON.stringify({ email, password }),
|
|
2274
|
+
});
|
|
2275
|
+
const json = await response.json().catch(() => null);
|
|
2276
|
+
return { ok: response.ok, status: response.status, data: json };
|
|
2277
|
+
} catch (e: any) {
|
|
2278
|
+
return { ok: false, error: e.message };
|
|
2279
|
+
}
|
|
2280
|
+
},
|
|
2281
|
+
{ email, password: hashedPassword, signinUrl, requestId },
|
|
2282
|
+
)
|
|
2283
|
+
.catch(() => null);
|
|
2284
|
+
|
|
2285
|
+
let data: any = evalRes?.data;
|
|
2286
|
+
let signinSuccess = Boolean(
|
|
2287
|
+
data &&
|
|
2288
|
+
(data.success === true ||
|
|
2289
|
+
data.token ||
|
|
2290
|
+
data.data?.access_token ||
|
|
2291
|
+
data.data?.token),
|
|
2292
|
+
);
|
|
2293
|
+
|
|
2294
|
+
// Fallback to legacy chat.qwen.ai endpoint if auth.qwen.ai did not succeed
|
|
2295
|
+
if (!signinSuccess && !data?.data?.code) {
|
|
2296
|
+
const fallbackUrl = qwenUrl("/api/v2/auths/signin");
|
|
2297
|
+
const fallbackRes = await page
|
|
2240
2298
|
.evaluate(
|
|
2241
|
-
async ({ email, password,
|
|
2299
|
+
async ({ email, password, fallbackUrl, requestId }) => {
|
|
2242
2300
|
try {
|
|
2243
|
-
const response = await fetch(
|
|
2301
|
+
const response = await fetch(fallbackUrl, {
|
|
2244
2302
|
method: "POST",
|
|
2245
2303
|
signal: AbortSignal.timeout(10_000),
|
|
2246
2304
|
credentials: "include",
|
|
@@ -2248,10 +2306,11 @@ async function loginViaApi(
|
|
|
2248
2306
|
accept: "application/json, text/plain, */*",
|
|
2249
2307
|
"content-type": "application/json",
|
|
2250
2308
|
source: "web",
|
|
2309
|
+
version: "0.3.11",
|
|
2251
2310
|
timezone: new Date().toString().split(" (")[0],
|
|
2252
2311
|
"x-request-id": requestId,
|
|
2253
2312
|
},
|
|
2254
|
-
body: JSON.stringify({ email, password
|
|
2313
|
+
body: JSON.stringify({ email, password }),
|
|
2255
2314
|
});
|
|
2256
2315
|
const json = await response.json().catch(() => null);
|
|
2257
2316
|
return { ok: response.ok, status: response.status, data: json };
|
|
@@ -2259,13 +2318,19 @@ async function loginViaApi(
|
|
|
2259
2318
|
return { ok: false, error: e.message };
|
|
2260
2319
|
}
|
|
2261
2320
|
},
|
|
2262
|
-
{ email, password: hashedPassword,
|
|
2321
|
+
{ email, password: hashedPassword, fallbackUrl, requestId },
|
|
2263
2322
|
)
|
|
2264
2323
|
.catch(() => null);
|
|
2265
2324
|
|
|
2266
|
-
if (
|
|
2267
|
-
data =
|
|
2268
|
-
signinSuccess = Boolean(
|
|
2325
|
+
if (fallbackRes?.data) {
|
|
2326
|
+
data = fallbackRes.data;
|
|
2327
|
+
signinSuccess = Boolean(
|
|
2328
|
+
data &&
|
|
2329
|
+
(data.success === true ||
|
|
2330
|
+
data.token ||
|
|
2331
|
+
data.data?.access_token ||
|
|
2332
|
+
data.data?.token),
|
|
2333
|
+
);
|
|
2269
2334
|
}
|
|
2270
2335
|
}
|
|
2271
2336
|
|
|
@@ -2281,7 +2346,14 @@ async function loginViaApi(
|
|
|
2281
2346
|
}
|
|
2282
2347
|
|
|
2283
2348
|
if (signinSuccess) {
|
|
2284
|
-
const token =
|
|
2349
|
+
const token =
|
|
2350
|
+
data?.data?.access_token ||
|
|
2351
|
+
data?.data?.token ||
|
|
2352
|
+
data?.access_token ||
|
|
2353
|
+
data?.token;
|
|
2354
|
+
const refreshToken =
|
|
2355
|
+
data?.data?.refresh_token || data?.refresh_token;
|
|
2356
|
+
|
|
2285
2357
|
if (token) {
|
|
2286
2358
|
try {
|
|
2287
2359
|
await page.context().addCookies([
|
|
@@ -2308,16 +2380,21 @@ async function loginViaApi(
|
|
|
2308
2380
|
|
|
2309
2381
|
if (token) {
|
|
2310
2382
|
await page
|
|
2311
|
-
.evaluate(
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2383
|
+
.evaluate(
|
|
2384
|
+
({ tok, rTok }) => {
|
|
2385
|
+
try {
|
|
2386
|
+
localStorage.removeItem("qwen_token_logged_out_marker");
|
|
2387
|
+
localStorage.setItem("token", tok);
|
|
2388
|
+
localStorage.setItem("access_token", tok);
|
|
2389
|
+
if (rTok) localStorage.setItem("refresh_token", rTok);
|
|
2390
|
+
document.cookie = `token=${encodeURIComponent(tok)}; path=/; domain=.qwen.ai; max-age=31536000`;
|
|
2391
|
+
} catch {}
|
|
2392
|
+
},
|
|
2393
|
+
{ tok: token, rTok: refreshToken },
|
|
2394
|
+
)
|
|
2318
2395
|
.catch(() => {});
|
|
2319
2396
|
await page.reload({ waitUntil: "domcontentloaded" }).catch(() => {});
|
|
2320
|
-
return { success: true };
|
|
2397
|
+
return { success: true, token, refreshToken };
|
|
2321
2398
|
}
|
|
2322
2399
|
}
|
|
2323
2400
|
|
|
@@ -2346,11 +2423,28 @@ async function loginViaUi(
|
|
|
2346
2423
|
password: string,
|
|
2347
2424
|
): Promise<LoginAttemptResult> {
|
|
2348
2425
|
try {
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
}
|
|
2353
|
-
|
|
2426
|
+
// 0. If the page is ALREADY logged in, avoid touching /auth altogether!
|
|
2427
|
+
if (!page.url().includes("/auth") && (await isPageLoggedIn(page, 2000).catch(() => false))) {
|
|
2428
|
+
return { success: true };
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
try {
|
|
2432
|
+
await page.goto(qwenUrl("/auth"), {
|
|
2433
|
+
waitUntil: "domcontentloaded",
|
|
2434
|
+
timeout: Math.min(config.timeouts.navigation, 15_000),
|
|
2435
|
+
});
|
|
2436
|
+
await sleep(1000);
|
|
2437
|
+
} catch (gotoErr: any) {
|
|
2438
|
+
// If navigation was aborted because the page redirected to / (because already logged in):
|
|
2439
|
+
if (!page.url().includes("/auth") && (await isPageLoggedIn(page, 3000).catch(() => false))) {
|
|
2440
|
+
return { success: true };
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2444
|
+
// Check if redirect occurred after navigation
|
|
2445
|
+
if (!page.url().includes("/auth") && (await isPageLoggedIn(page, 3000).catch(() => false))) {
|
|
2446
|
+
return { success: true };
|
|
2447
|
+
}
|
|
2354
2448
|
|
|
2355
2449
|
// Wait for email input
|
|
2356
2450
|
const emailSelector = [
|
|
@@ -2362,7 +2456,7 @@ async function loginViaUi(
|
|
|
2362
2456
|
].join(", ");
|
|
2363
2457
|
try {
|
|
2364
2458
|
await page.waitForSelector(emailSelector, {
|
|
2365
|
-
timeout: Math.min(config.timeouts.page,
|
|
2459
|
+
timeout: Math.min(config.timeouts.page, 5_000),
|
|
2366
2460
|
});
|
|
2367
2461
|
} catch {
|
|
2368
2462
|
if (!page.url().includes("/auth") && (await isPageLoggedIn(page, 3000))) {
|
|
@@ -2379,35 +2473,77 @@ async function loginViaUi(
|
|
|
2379
2473
|
// (supporting English, Portuguese, Spanish, Chinese) to reveal the standard email + password form.
|
|
2380
2474
|
await clearVisibleChallenge(page);
|
|
2381
2475
|
|
|
2382
|
-
const
|
|
2383
|
-
return page
|
|
2384
|
-
|
|
2476
|
+
const isPasswordInputPresent = async () => {
|
|
2477
|
+
return (await page.locator('input[type="password"], input[name="password"]').count().catch(() => 0)) > 0;
|
|
2478
|
+
};
|
|
2479
|
+
|
|
2480
|
+
const revealPasswordMode = async () => {
|
|
2481
|
+
// If already on /forget, navigate back to /auth
|
|
2482
|
+
if (page.url().includes("/forget")) {
|
|
2483
|
+
await page.goto(qwenUrl("/auth"), { waitUntil: "domcontentloaded", timeout: 10_000 }).catch(() => {});
|
|
2484
|
+
await sleep(1000);
|
|
2485
|
+
}
|
|
2486
|
+
|
|
2487
|
+
// If password input is already in DOM, do NOT click any switch buttons!
|
|
2488
|
+
if (await isPasswordInputPresent()) return;
|
|
2489
|
+
|
|
2490
|
+
// 1. Playwright button locator with multilingual matching (strictly excluding "Esqueci a senha" / "Forgot password")
|
|
2491
|
+
const btn = page
|
|
2492
|
+
.locator("button")
|
|
2385
2493
|
.filter({
|
|
2386
2494
|
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
2495
|
})
|
|
2496
|
+
.filter({
|
|
2497
|
+
hasNotText: /(?:esqueci|forgot|olvid|reset|recover)/i,
|
|
2498
|
+
})
|
|
2388
2499
|
.first();
|
|
2389
|
-
};
|
|
2390
2500
|
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
if (await pwdModeBtn.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|
2394
|
-
await pwdModeBtn.click({ timeout: 2000 }).catch(() => {});
|
|
2501
|
+
if (await btn.isVisible({ timeout: 1500 }).catch(() => false)) {
|
|
2502
|
+
await btn.click({ timeout: 2000 }).catch(() => {});
|
|
2395
2503
|
await sleep(500);
|
|
2396
2504
|
}
|
|
2397
|
-
} catch {}
|
|
2398
2505
|
|
|
2399
|
-
|
|
2400
|
-
await page.fill(emailSelector, email);
|
|
2401
|
-
await sleep(300);
|
|
2506
|
+
if (await isPasswordInputPresent()) return;
|
|
2402
2507
|
|
|
2403
|
-
|
|
2508
|
+
// 2. In-page evaluate fail-safe to trigger direct DOM click on the password button
|
|
2509
|
+
await page
|
|
2510
|
+
.evaluate(() => {
|
|
2511
|
+
try {
|
|
2512
|
+
const buttons = Array.from(document.querySelectorAll("button, [role='button'], a"));
|
|
2513
|
+
for (const b of buttons) {
|
|
2514
|
+
const txt = ((b as HTMLElement).innerText || "").trim().toLowerCase();
|
|
2515
|
+
if (
|
|
2516
|
+
/(?: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) &&
|
|
2517
|
+
!txt.includes("esqueci") && !txt.includes("forgot") && !txt.includes("olvid") && !txt.includes("reset")
|
|
2518
|
+
) {
|
|
2519
|
+
(b as HTMLElement).click();
|
|
2520
|
+
return true;
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
} catch {}
|
|
2524
|
+
return false;
|
|
2525
|
+
})
|
|
2526
|
+
.catch(() => false);
|
|
2527
|
+
await sleep(500);
|
|
2528
|
+
};
|
|
2529
|
+
|
|
2530
|
+
await revealPasswordMode();
|
|
2531
|
+
|
|
2532
|
+
// Fill email
|
|
2404
2533
|
try {
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2534
|
+
await page.fill(emailSelector, email, { timeout: 8_000 });
|
|
2535
|
+
await sleep(300);
|
|
2536
|
+
} catch {
|
|
2537
|
+
if (!page.url().includes("/auth") && (await isPageLoggedIn(page, 3000))) {
|
|
2538
|
+
return { success: true };
|
|
2409
2539
|
}
|
|
2410
|
-
|
|
2540
|
+
throw new Error(`Campo de e-mail não encontrado na tela de autenticação`);
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2543
|
+
// If password field is still not visible after typing email, trigger reveal again
|
|
2544
|
+
if (!(await isPasswordInputPresent())) {
|
|
2545
|
+
await revealPasswordMode();
|
|
2546
|
+
}
|
|
2411
2547
|
|
|
2412
2548
|
// In Qwen Web, the password field is present on the same form.
|
|
2413
2549
|
// NEVER press Enter after filling email alone, as Qwen interprets that
|
|
@@ -2432,21 +2568,54 @@ async function loginViaUi(
|
|
|
2432
2568
|
}
|
|
2433
2569
|
|
|
2434
2570
|
// Fill password
|
|
2435
|
-
|
|
2436
|
-
|
|
2571
|
+
try {
|
|
2572
|
+
await page.fill(passwordSelector, password, { timeout: 8_000 });
|
|
2573
|
+
await sleep(500);
|
|
2574
|
+
} catch {
|
|
2575
|
+
if (!page.url().includes("/auth") && (await isPageLoggedIn(page, 3000))) {
|
|
2576
|
+
return { success: true };
|
|
2577
|
+
}
|
|
2578
|
+
throw new Error(`Campo de senha não encontrado na tela de autenticação`);
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2581
|
+
// Dispatch input/change/blur events so Ant Design / React enables the submit button
|
|
2582
|
+
await page
|
|
2583
|
+
.evaluate(() => {
|
|
2584
|
+
try {
|
|
2585
|
+
const emailEl = document.querySelector('input[name="email"], input[type="email"], input[type="text"]');
|
|
2586
|
+
const passEl = document.querySelector('input[type="password"], input[name="password"]');
|
|
2587
|
+
if (emailEl) {
|
|
2588
|
+
emailEl.dispatchEvent(new Event("input", { bubbles: true }));
|
|
2589
|
+
emailEl.dispatchEvent(new Event("change", { bubbles: true }));
|
|
2590
|
+
emailEl.dispatchEvent(new Event("blur", { bubbles: true }));
|
|
2591
|
+
}
|
|
2592
|
+
if (passEl) {
|
|
2593
|
+
passEl.dispatchEvent(new Event("input", { bubbles: true }));
|
|
2594
|
+
passEl.dispatchEvent(new Event("change", { bubbles: true }));
|
|
2595
|
+
passEl.dispatchEvent(new Event("blur", { bubbles: true }));
|
|
2596
|
+
}
|
|
2597
|
+
} catch {}
|
|
2598
|
+
})
|
|
2599
|
+
.catch(() => {});
|
|
2437
2600
|
|
|
2438
|
-
// Prefer clicking the submit button;
|
|
2439
|
-
// The button starts disabled and only enables once both fields are filled.
|
|
2601
|
+
// Prefer clicking the submit button; do NOT press Enter if disabled
|
|
2440
2602
|
const submitSelector =
|
|
2441
2603
|
'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
2604
|
const submitButton = page.locator(submitSelector).first();
|
|
2443
2605
|
try {
|
|
2444
2606
|
await page.waitForSelector('button[type="submit"]:not([disabled])', {
|
|
2445
|
-
timeout:
|
|
2607
|
+
timeout: 4_000,
|
|
2446
2608
|
});
|
|
2447
2609
|
await submitButton.click();
|
|
2448
2610
|
} catch {
|
|
2449
|
-
await
|
|
2611
|
+
const isDisabled = await submitButton.isDisabled().catch(() => true);
|
|
2612
|
+
if (!isDisabled) {
|
|
2613
|
+
await submitButton.click({ force: true }).catch(() => {});
|
|
2614
|
+
} else {
|
|
2615
|
+
if (!page.url().includes("/auth") && (await isPageLoggedIn(page, 2000))) {
|
|
2616
|
+
return { success: true };
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2450
2619
|
}
|
|
2451
2620
|
// Post-submit verification loop: wait up to 15s for captcha, redirect, cookies, or errors
|
|
2452
2621
|
const postSubmitDeadline = Date.now() + 15_000;
|
|
@@ -2531,6 +2700,161 @@ async function loginViaUi(
|
|
|
2531
2700
|
}
|
|
2532
2701
|
}
|
|
2533
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
|
+
|
|
2534
2858
|
// ─── Header Capture ───────────────────────────────────────────────────────────
|
|
2535
2859
|
|
|
2536
2860
|
/**
|
|
@@ -3251,6 +3575,11 @@ async function refreshHeadersInternal(
|
|
|
3251
3575
|
};
|
|
3252
3576
|
|
|
3253
3577
|
if (forceReauth) {
|
|
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;
|
|
3582
|
+
}
|
|
3254
3583
|
await executeReauth();
|
|
3255
3584
|
} else {
|
|
3256
3585
|
try {
|
|
@@ -3848,21 +4177,17 @@ export async function keepAlivePlaywrightAccount(
|
|
|
3848
4177
|
const now = Date.now();
|
|
3849
4178
|
const currentUrl = page.url();
|
|
3850
4179
|
|
|
3851
|
-
// Proactive session check: if token expires within
|
|
4180
|
+
// Proactive session check: if token expires within 5 minutes, refresh in-page silently using refreshAccountToken
|
|
3852
4181
|
const cookie = await getCookies(accountId);
|
|
3853
|
-
if (cookie && isTokenExpiringSoon(cookie,
|
|
4182
|
+
if (cookie && isTokenExpiringSoon(cookie, 5)) {
|
|
3854
4183
|
console.log(
|
|
3855
|
-
`💓 [SessionKeeper] Account ${accountId} token
|
|
4184
|
+
`💓 [SessionKeeper] Account ${accountId} access token expiring soon (<5m); silently refreshing...`,
|
|
3856
4185
|
);
|
|
3857
|
-
|
|
3858
|
-
|
|
4186
|
+
const res = await refreshAccountToken(accountId);
|
|
4187
|
+
if (res.success) {
|
|
3859
4188
|
lastKeepAliveNavigation.set(accountId, now);
|
|
3860
4189
|
touchAccountActivity(accountId);
|
|
3861
4190
|
return true;
|
|
3862
|
-
} catch (err) {
|
|
3863
|
-
console.warn(
|
|
3864
|
-
`[SessionKeeper] Proactive session renewal failed for ${accountId}: ${getErrorMessage(err)}`,
|
|
3865
|
-
);
|
|
3866
4191
|
}
|
|
3867
4192
|
}
|
|
3868
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",
|