qwenproxy-cli 1.6.9 → 1.6.11
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 +199 -55
- package/dist/delete-chats.js +269 -78
- package/dist/index.js +290 -88
- package/dist/login.js +203 -67
- package/dist/reset-cooldowns.js +199 -55
- package/package.json +2 -2
- package/src/api/server.ts +8 -4
- package/src/core/version.ts +1 -1
- package/src/core/watchdog.ts +4 -3
- package/src/routes/chat/account.ts +12 -0
- package/src/routes/chat/streaming.ts +18 -2
- package/src/services/captcha-coordinator.ts +36 -8
- package/src/services/chat-cleanup.ts +11 -2
- package/src/services/playwright.ts +244 -74
- package/src/services/qwen.ts +29 -3
|
@@ -79,7 +79,15 @@ export async function deleteChatsForAccountId(accountId: string): Promise<boolea
|
|
|
79
79
|
if (!credentials) {
|
|
80
80
|
throw new Error(`Credenciais da conta ${account.email} não encontradas.`);
|
|
81
81
|
}
|
|
82
|
-
|
|
82
|
+
const wasInitialized = isPlaywrightInitialized(account.id);
|
|
83
|
+
try {
|
|
84
|
+
return await deleteChatsForAccount(credentials);
|
|
85
|
+
} finally {
|
|
86
|
+
if (!wasInitialized && !isAuthMockEnabled()) {
|
|
87
|
+
const { closePlaywrightForAccount } = await import("./playwright.ts");
|
|
88
|
+
await closePlaywrightForAccount(account.id).catch(() => {});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
83
91
|
}
|
|
84
92
|
|
|
85
93
|
export async function deleteChatsForConfiguredAccounts(keepBrowserOpen = false): Promise<DeleteChatsResult> {
|
|
@@ -100,6 +108,7 @@ export async function deleteChatsForConfiguredAccounts(keepBrowserOpen = false):
|
|
|
100
108
|
const account = accounts[i];
|
|
101
109
|
const currentIdx = i + 1;
|
|
102
110
|
const totalCount = accounts.length;
|
|
111
|
+
const wasInitialized = isPlaywrightInitialized(account.id);
|
|
103
112
|
try {
|
|
104
113
|
const ok = await deleteChatsForAccount(account, currentIdx, totalCount);
|
|
105
114
|
if (ok) succeeded++;
|
|
@@ -109,7 +118,7 @@ export async function deleteChatsForConfiguredAccounts(keepBrowserOpen = false):
|
|
|
109
118
|
error instanceof Error ? error.message : String(error),
|
|
110
119
|
);
|
|
111
120
|
} finally {
|
|
112
|
-
if (!keepBrowserOpen) {
|
|
121
|
+
if ((!keepBrowserOpen || !wasInitialized) && !isAuthMockEnabled()) {
|
|
113
122
|
const { closePlaywrightForAccount } = await import("./playwright.ts");
|
|
114
123
|
await closePlaywrightForAccount(account.id).catch(() => {});
|
|
115
124
|
}
|
|
@@ -68,6 +68,7 @@ import {
|
|
|
68
68
|
unmarkAccountHeadersReady,
|
|
69
69
|
isAccountHeadersReady,
|
|
70
70
|
markAccountRateLimited,
|
|
71
|
+
getAccountCooldownInfo,
|
|
71
72
|
} from "../core/account-manager.ts";
|
|
72
73
|
import { getAccountsByPriority } from "../core/account-priority.ts";
|
|
73
74
|
import {
|
|
@@ -365,7 +366,24 @@ export async function isPageLoggedIn(
|
|
|
365
366
|
return false;
|
|
366
367
|
}
|
|
367
368
|
|
|
368
|
-
|
|
369
|
+
// In Qwen Web 0.3.26+, UI login stores the JWT in localStorage("token")
|
|
370
|
+
// without setting a "token=" cookie in document.cookie. Sync it so
|
|
371
|
+
// same-origin probes and context.cookies() always include it.
|
|
372
|
+
const lsToken = (localStorage.getItem("token") || "").trim();
|
|
373
|
+
if (lsToken && !/(?:^|;\s*)token=/.test(document.cookie || "")) {
|
|
374
|
+
try {
|
|
375
|
+
document.cookie = `token=${encodeURIComponent(lsToken)}; path=/; domain=.qwen.ai; max-age=31536000`;
|
|
376
|
+
} catch {}
|
|
377
|
+
}
|
|
378
|
+
const authHeaders: Record<string, string> = lsToken
|
|
379
|
+
? { Authorization: `Bearer ${lsToken}` }
|
|
380
|
+
: {};
|
|
381
|
+
|
|
382
|
+
const res = await fetch("/api/v1/auths/", {
|
|
383
|
+
method: "GET",
|
|
384
|
+
headers: authHeaders,
|
|
385
|
+
credentials: "include",
|
|
386
|
+
});
|
|
369
387
|
if (res.status !== 200) return false;
|
|
370
388
|
const json: any = await res.json().catch(() => null);
|
|
371
389
|
if (!json) return false;
|
|
@@ -398,6 +416,7 @@ export async function isPageLoggedIn(
|
|
|
398
416
|
try {
|
|
399
417
|
const settingsRes = await fetch("/api/v2/users/user/settings", {
|
|
400
418
|
method: "GET",
|
|
419
|
+
headers: authHeaders,
|
|
401
420
|
credentials: "include",
|
|
402
421
|
signal: AbortSignal.timeout(3000),
|
|
403
422
|
});
|
|
@@ -1390,18 +1409,19 @@ export async function getBasicHeaders(accountId: string): Promise<{
|
|
|
1390
1409
|
} else if (
|
|
1391
1410
|
hadUsableHeaders &&
|
|
1392
1411
|
headersAge > HEADER_CACHE_TTL * HEADER_REFRESH_THRESHOLD &&
|
|
1393
|
-
!cache.refreshInProgress
|
|
1412
|
+
!cache.refreshInProgress &&
|
|
1413
|
+
// Header capture types into the chat input and sends a probe message on
|
|
1414
|
+
// the account's page. Doing that while the account is serving a stream
|
|
1415
|
+
// would mutate the live conversation (and can navigate the SPA), so the
|
|
1416
|
+
// proactive refresh waits until the account has no active lease.
|
|
1417
|
+
!hasActiveAccountLease(accountId)
|
|
1394
1418
|
) {
|
|
1395
1419
|
// Background refresh at 70% TTL: keep headers fresh without blocking
|
|
1396
|
-
cache.refreshInProgress = true;
|
|
1397
1420
|
refreshHeadersInternal(accountId)
|
|
1398
1421
|
.catch((error) => {
|
|
1399
1422
|
console.warn(
|
|
1400
1423
|
`⚠️ [Playwright] Background header refresh failed for ${accountId}: ${getErrorMessage(error)}`,
|
|
1401
1424
|
);
|
|
1402
|
-
})
|
|
1403
|
-
.finally(() => {
|
|
1404
|
-
cache.refreshInProgress = false;
|
|
1405
1425
|
});
|
|
1406
1426
|
}
|
|
1407
1427
|
|
|
@@ -1551,8 +1571,13 @@ export async function initPlaywrightForAccount(
|
|
|
1551
1571
|
return;
|
|
1552
1572
|
}
|
|
1553
1573
|
|
|
1554
|
-
// If a context limit is configured, make room by
|
|
1555
|
-
|
|
1574
|
+
// If a context limit is configured, make room BEFORE launching by evicting down to (max - 1)
|
|
1575
|
+
// so the new context brings total active contexts to at most max.
|
|
1576
|
+
if (config.playwright.maxActiveContexts > 0) {
|
|
1577
|
+
await evictIdlePlaywrightContextsToLimit(
|
|
1578
|
+
Math.max(1, config.playwright.maxActiveContexts - 1),
|
|
1579
|
+
).catch(() => {});
|
|
1580
|
+
}
|
|
1556
1581
|
|
|
1557
1582
|
const profilePath = getAccountProfilePath(account.id);
|
|
1558
1583
|
const fingerprint = getFingerprintProfile(account.id);
|
|
@@ -1819,8 +1844,15 @@ export async function initPlaywrightForAccount(
|
|
|
1819
1844
|
c.name.toLowerCase().includes("session"),
|
|
1820
1845
|
);
|
|
1821
1846
|
|
|
1847
|
+
let initialLoginAttempted = false;
|
|
1822
1848
|
if (!hasAuthCookie && account.email && account.password) {
|
|
1823
|
-
|
|
1849
|
+
initialLoginAttempted = true;
|
|
1850
|
+
const loggedIn = await loginToQwen(account.id, account.email, account.password);
|
|
1851
|
+
if (!loggedIn) {
|
|
1852
|
+
throw new Error(
|
|
1853
|
+
`Authentication failed for account ${maskEmail(account.email)}`,
|
|
1854
|
+
);
|
|
1855
|
+
}
|
|
1824
1856
|
}
|
|
1825
1857
|
|
|
1826
1858
|
// Initial navigation to chat home
|
|
@@ -1832,13 +1864,21 @@ export async function initPlaywrightForAccount(
|
|
|
1832
1864
|
await sleep(1500);
|
|
1833
1865
|
const currentUrl = acctPage.url();
|
|
1834
1866
|
const isAuthUrl = currentUrl.includes("/auth") || currentUrl.includes("/login");
|
|
1835
|
-
if (isAuthUrl && account.email && account.password) {
|
|
1867
|
+
if (isAuthUrl && !initialLoginAttempted && account.email && account.password) {
|
|
1836
1868
|
console.log(
|
|
1837
1869
|
`[Playwright] Initial login needed for ${maskEmail(account.email)}, authenticating...`,
|
|
1838
1870
|
);
|
|
1839
|
-
await loginToQwen(account.id, account.email, account.password);
|
|
1871
|
+
const loggedIn = await loginToQwen(account.id, account.email, account.password);
|
|
1872
|
+
if (!loggedIn) {
|
|
1873
|
+
throw new Error(
|
|
1874
|
+
`Authentication failed for account ${maskEmail(account.email)}`,
|
|
1875
|
+
);
|
|
1876
|
+
}
|
|
1840
1877
|
}
|
|
1841
1878
|
} catch (err: any) {
|
|
1879
|
+
if (err?.message?.includes("Authentication failed for account")) {
|
|
1880
|
+
throw err;
|
|
1881
|
+
}
|
|
1842
1882
|
console.warn(
|
|
1843
1883
|
`⚠️ [Playwright] Initial navigation check for ${maskEmail(account.email)}: ${err.message}`,
|
|
1844
1884
|
);
|
|
@@ -1849,12 +1889,17 @@ export async function initPlaywrightForAccount(
|
|
|
1849
1889
|
await captureQwenHeaders(account.id);
|
|
1850
1890
|
}
|
|
1851
1891
|
|
|
1852
|
-
// Header capture may leave the UI on a
|
|
1853
|
-
// primary tab to the canonical chat home.
|
|
1892
|
+
// Header capture may leave the UI on a punish or auth page. Return the
|
|
1893
|
+
// primary tab to the canonical chat home only when not already on a clean Qwen page.
|
|
1854
1894
|
if (!acctPage.isClosed()) {
|
|
1855
1895
|
try {
|
|
1856
1896
|
const currentUrl = new URL(acctPage.url());
|
|
1857
|
-
if (
|
|
1897
|
+
if (
|
|
1898
|
+
currentUrl.origin !== qwenOrigin() ||
|
|
1899
|
+
currentUrl.pathname.includes("/auth") ||
|
|
1900
|
+
currentUrl.pathname.includes("/login") ||
|
|
1901
|
+
currentUrl.pathname.includes("_____tmd_____")
|
|
1902
|
+
) {
|
|
1858
1903
|
await acctPage.goto(qwenUrl("/"), {
|
|
1859
1904
|
waitUntil: "domcontentloaded",
|
|
1860
1905
|
timeout: config.timeouts.navigation,
|
|
@@ -1874,6 +1919,7 @@ export async function initPlaywrightForAccount(
|
|
|
1874
1919
|
} finally {
|
|
1875
1920
|
release();
|
|
1876
1921
|
}
|
|
1922
|
+
await evictIdlePlaywrightContextsToLimit().catch(() => {});
|
|
1877
1923
|
})();
|
|
1878
1924
|
|
|
1879
1925
|
try {
|
|
@@ -2551,10 +2597,31 @@ async function loginToQwen(
|
|
|
2551
2597
|
const uiResult = await loginViaUi(page, email, password);
|
|
2552
2598
|
if (uiResult.success) {
|
|
2553
2599
|
cookieCaches.delete(accountId);
|
|
2554
|
-
await saveStorageState(page.context(), accountId);
|
|
2555
2600
|
try {
|
|
2556
|
-
|
|
2557
|
-
|
|
2601
|
+
let liveCookies = await page.context().cookies();
|
|
2602
|
+
let tokenCookie = liveCookies.find((c) => c.name === "token");
|
|
2603
|
+
if (!tokenCookie) {
|
|
2604
|
+
const lsTok =
|
|
2605
|
+
uiResult.token ||
|
|
2606
|
+
(await page.evaluate(() => (localStorage.getItem("token") || "").trim()).catch(() => ""));
|
|
2607
|
+
if (lsTok) {
|
|
2608
|
+
await page.context().addCookies([
|
|
2609
|
+
{
|
|
2610
|
+
name: "token",
|
|
2611
|
+
value: lsTok,
|
|
2612
|
+
domain: ".qwen.ai",
|
|
2613
|
+
path: "/",
|
|
2614
|
+
expires: Math.floor(Date.now() / 1000) + 31536000,
|
|
2615
|
+
httpOnly: false,
|
|
2616
|
+
secure: true,
|
|
2617
|
+
sameSite: "Lax",
|
|
2618
|
+
},
|
|
2619
|
+
]).catch(() => {});
|
|
2620
|
+
liveCookies = await page.context().cookies();
|
|
2621
|
+
tokenCookie = liveCookies.find((c) => c.name === "token");
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
await saveStorageState(page.context(), accountId);
|
|
2558
2625
|
if (tokenCookie) {
|
|
2559
2626
|
const cookieStr = liveCookies.map((c) => `${c.name}=${c.value}`).join("; ");
|
|
2560
2627
|
const cache = getHeaderCache(accountId);
|
|
@@ -2563,7 +2630,9 @@ async function loginToQwen(
|
|
|
2563
2630
|
markAccountHeadersReady(accountId);
|
|
2564
2631
|
const { saveAuthSession } = await import("../core/database.ts");
|
|
2565
2632
|
const exp = parseJwtExpiry(tokenCookie.value);
|
|
2566
|
-
let rTok =
|
|
2633
|
+
let rTok =
|
|
2634
|
+
uiResult.refreshToken ||
|
|
2635
|
+
liveCookies.find((c) => c.name === "refresh_token")?.value;
|
|
2567
2636
|
if (!rTok) {
|
|
2568
2637
|
try {
|
|
2569
2638
|
rTok = (await page.evaluate(() => localStorage.getItem("refresh_token") || "")) || undefined;
|
|
@@ -2657,10 +2726,11 @@ async function loginViaApi(
|
|
|
2657
2726
|
|
|
2658
2727
|
const signinUrl = "https://auth.qwen.ai/api/v2/auths/signin";
|
|
2659
2728
|
const requestId = crypto.randomUUID();
|
|
2729
|
+
const webVersion = getQwenWebVersion() || "0.3.26";
|
|
2660
2730
|
|
|
2661
2731
|
const evalRes = await page
|
|
2662
2732
|
.evaluate(
|
|
2663
|
-
async ({ email, password, signinUrl, requestId, tz }) => {
|
|
2733
|
+
async ({ email, password, signinUrl, requestId, tz, version }) => {
|
|
2664
2734
|
try {
|
|
2665
2735
|
const response = await fetch(signinUrl, {
|
|
2666
2736
|
method: "POST",
|
|
@@ -2670,7 +2740,7 @@ async function loginViaApi(
|
|
|
2670
2740
|
accept: "application/json, text/plain, */*",
|
|
2671
2741
|
"content-type": "application/json",
|
|
2672
2742
|
source: "web",
|
|
2673
|
-
version
|
|
2743
|
+
version,
|
|
2674
2744
|
"x-request-origin": "https://chat.qwen.ai",
|
|
2675
2745
|
timezone: tz,
|
|
2676
2746
|
"x-request-id": requestId,
|
|
@@ -2683,7 +2753,14 @@ async function loginViaApi(
|
|
|
2683
2753
|
return { ok: false, error: e.message };
|
|
2684
2754
|
}
|
|
2685
2755
|
},
|
|
2686
|
-
{
|
|
2756
|
+
{
|
|
2757
|
+
email,
|
|
2758
|
+
password: hashedPassword,
|
|
2759
|
+
signinUrl,
|
|
2760
|
+
requestId,
|
|
2761
|
+
tz: formatAsciiTimezone(),
|
|
2762
|
+
version: webVersion,
|
|
2763
|
+
},
|
|
2687
2764
|
)
|
|
2688
2765
|
.catch(() => null);
|
|
2689
2766
|
|
|
@@ -2701,7 +2778,7 @@ async function loginViaApi(
|
|
|
2701
2778
|
const fallbackUrl = qwenUrl("/api/v2/auths/signin");
|
|
2702
2779
|
const fallbackRes = await page
|
|
2703
2780
|
.evaluate(
|
|
2704
|
-
async ({ email, password, fallbackUrl, requestId, tz }) => {
|
|
2781
|
+
async ({ email, password, fallbackUrl, requestId, tz, version }) => {
|
|
2705
2782
|
try {
|
|
2706
2783
|
const response = await fetch(fallbackUrl, {
|
|
2707
2784
|
method: "POST",
|
|
@@ -2711,7 +2788,7 @@ async function loginViaApi(
|
|
|
2711
2788
|
accept: "application/json, text/plain, */*",
|
|
2712
2789
|
"content-type": "application/json",
|
|
2713
2790
|
source: "web",
|
|
2714
|
-
version
|
|
2791
|
+
version,
|
|
2715
2792
|
timezone: tz,
|
|
2716
2793
|
"x-request-id": requestId,
|
|
2717
2794
|
},
|
|
@@ -2723,7 +2800,14 @@ async function loginViaApi(
|
|
|
2723
2800
|
return { ok: false, error: e.message };
|
|
2724
2801
|
}
|
|
2725
2802
|
},
|
|
2726
|
-
{
|
|
2803
|
+
{
|
|
2804
|
+
email,
|
|
2805
|
+
password: hashedPassword,
|
|
2806
|
+
fallbackUrl,
|
|
2807
|
+
requestId,
|
|
2808
|
+
tz: formatAsciiTimezone(),
|
|
2809
|
+
version: webVersion,
|
|
2810
|
+
},
|
|
2727
2811
|
)
|
|
2728
2812
|
.catch(() => null);
|
|
2729
2813
|
|
|
@@ -3009,30 +3093,37 @@ async function loginViaUi(
|
|
|
3009
3093
|
|
|
3010
3094
|
await sleep(500);
|
|
3011
3095
|
|
|
3012
|
-
//
|
|
3096
|
+
// Submit the form: click the submit button if visible; only press Enter as fallback
|
|
3097
|
+
// (pressing Enter inside <form> after clicking submit triggers a native HTML form reload on Qwen 0.3.26).
|
|
3013
3098
|
const submitSelector =
|
|
3014
3099
|
'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
3100
|
const submitButton = page.locator(submitSelector).first();
|
|
3016
3101
|
|
|
3017
|
-
|
|
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
|
-
|
|
3102
|
+
let submitted = false;
|
|
3028
3103
|
try {
|
|
3029
3104
|
if (await submitButton.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|
3030
|
-
await submitButton.click({
|
|
3105
|
+
await submitButton.click({ timeout: 2000 }).catch(() => {});
|
|
3106
|
+
submitted = true;
|
|
3031
3107
|
}
|
|
3032
3108
|
} catch {}
|
|
3033
3109
|
|
|
3034
|
-
|
|
3035
|
-
|
|
3110
|
+
if (!submitted) {
|
|
3111
|
+
await page.evaluate(() => {
|
|
3112
|
+
try {
|
|
3113
|
+
const btn = document.querySelector(
|
|
3114
|
+
'button.qwenchat-auth-pc-submit-button, button[type="submit"]',
|
|
3115
|
+
) as HTMLButtonElement | null;
|
|
3116
|
+
if (btn) {
|
|
3117
|
+
btn.removeAttribute("disabled");
|
|
3118
|
+
btn.classList.remove("ant-btn-disabled");
|
|
3119
|
+
btn.click();
|
|
3120
|
+
}
|
|
3121
|
+
} catch {}
|
|
3122
|
+
}).catch(() => {});
|
|
3123
|
+
|
|
3124
|
+
const passInput = page.locator(passwordSelector).first();
|
|
3125
|
+
await passInput.press("Enter", { timeout: 2000 }).catch(() => {});
|
|
3126
|
+
}
|
|
3036
3127
|
// Post-submit verification loop: wait up to 15s for captcha, redirect, cookies, or errors
|
|
3037
3128
|
const postSubmitDeadline = Date.now() + 15_000;
|
|
3038
3129
|
while (Date.now() < postSubmitDeadline) {
|
|
@@ -3054,9 +3145,12 @@ async function loginViaUi(
|
|
|
3054
3145
|
".ant-form-item-explain-error",
|
|
3055
3146
|
".ant-message-error",
|
|
3056
3147
|
".ant-message-notice",
|
|
3148
|
+
".ant-message-custom-content",
|
|
3149
|
+
".ant-notification-notice",
|
|
3057
3150
|
'[role="alert"]',
|
|
3058
3151
|
".qwenchat-auth-error",
|
|
3059
3152
|
".auth-error-message",
|
|
3153
|
+
".qwenchat-toast",
|
|
3060
3154
|
].join(", ");
|
|
3061
3155
|
const errorEl = page.locator(errorSelector).first();
|
|
3062
3156
|
if (await errorEl.isVisible().catch(() => false)) {
|
|
@@ -3072,8 +3166,15 @@ async function loginViaUi(
|
|
|
3072
3166
|
}
|
|
3073
3167
|
|
|
3074
3168
|
// Check if an OTP code verification screen appeared (e.g. 2FA / email verification code)
|
|
3075
|
-
const codeSelector =
|
|
3076
|
-
'input[placeholder*="code" i],
|
|
3169
|
+
const codeSelector = [
|
|
3170
|
+
'input[placeholder*="code" i]',
|
|
3171
|
+
'input[placeholder*="código" i]',
|
|
3172
|
+
'input[name*="code" i]',
|
|
3173
|
+
".qwenchat-verification-code-input",
|
|
3174
|
+
".qwenchat-verification-code-input-cell",
|
|
3175
|
+
'[aria-label*="Verification code" i]',
|
|
3176
|
+
'[aria-label*="código de verificação" i]',
|
|
3177
|
+
].join(", ");
|
|
3077
3178
|
const codeInput = page.locator(codeSelector).first();
|
|
3078
3179
|
if (await codeInput.isVisible().catch(() => false)) {
|
|
3079
3180
|
return {
|
|
@@ -3082,21 +3183,50 @@ async function loginViaUi(
|
|
|
3082
3183
|
};
|
|
3083
3184
|
}
|
|
3084
3185
|
|
|
3085
|
-
// Check if auth cookie 'token' appeared
|
|
3186
|
+
// Check if auth cookie 'token' appeared or localStorage 'token' was populated (Qwen 0.3.26+)
|
|
3086
3187
|
try {
|
|
3087
3188
|
const liveCookies = await page.context().cookies();
|
|
3088
|
-
|
|
3089
|
-
|
|
3189
|
+
const rTokCookie = liveCookies.find((c) => c.name === "refresh_token")?.value;
|
|
3190
|
+
const tokCookie = liveCookies.find((c) => c.name === "token")?.value;
|
|
3191
|
+
if (tokCookie) {
|
|
3192
|
+
return { success: true, token: tokCookie, refreshToken: rTokCookie };
|
|
3090
3193
|
}
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3194
|
+
const currentUrl = page.url();
|
|
3195
|
+
if (!currentUrl.includes("/auth") && !currentUrl.includes("/login")) {
|
|
3196
|
+
const lsTok = await page
|
|
3197
|
+
.evaluate(() => {
|
|
3198
|
+
const t = (localStorage.getItem("token") || "").trim();
|
|
3199
|
+
if (t) {
|
|
3200
|
+
try {
|
|
3201
|
+
document.cookie = `token=${encodeURIComponent(t)}; path=/; domain=.qwen.ai; max-age=31536000`;
|
|
3202
|
+
} catch {}
|
|
3203
|
+
}
|
|
3204
|
+
return t;
|
|
3205
|
+
})
|
|
3206
|
+
.catch(() => "");
|
|
3207
|
+
if (lsTok) {
|
|
3208
|
+
await page
|
|
3209
|
+
.context()
|
|
3210
|
+
.addCookies([
|
|
3211
|
+
{
|
|
3212
|
+
name: "token",
|
|
3213
|
+
value: lsTok,
|
|
3214
|
+
domain: ".qwen.ai",
|
|
3215
|
+
path: "/",
|
|
3216
|
+
expires: Math.floor(Date.now() / 1000) + 31536000,
|
|
3217
|
+
httpOnly: false,
|
|
3218
|
+
secure: true,
|
|
3219
|
+
sameSite: "Lax",
|
|
3220
|
+
},
|
|
3221
|
+
])
|
|
3222
|
+
.catch(() => {});
|
|
3223
|
+
return { success: true, token: lsTok, refreshToken: rTokCookie };
|
|
3224
|
+
}
|
|
3225
|
+
if (await isPageLoggedIn(page, 3000)) {
|
|
3226
|
+
return { success: true, refreshToken: rTokCookie };
|
|
3227
|
+
}
|
|
3098
3228
|
}
|
|
3099
|
-
}
|
|
3229
|
+
} catch {}
|
|
3100
3230
|
|
|
3101
3231
|
await sleep(1000);
|
|
3102
3232
|
}
|
|
@@ -3128,7 +3258,7 @@ export async function refreshAccountToken(
|
|
|
3128
3258
|
return { success: false, error: "Account page not active" };
|
|
3129
3259
|
}
|
|
3130
3260
|
|
|
3131
|
-
// 1. Get refresh token from SQLite session or
|
|
3261
|
+
// 1. Get refresh token from SQLite session, localStorage, or browser cookies (Qwen 0.3.26+)
|
|
3132
3262
|
const { getValidAuthSession, saveAuthSession } = await import("../core/database.ts");
|
|
3133
3263
|
const session = getValidAuthSession(accountId);
|
|
3134
3264
|
let refreshToken = session?.refreshToken;
|
|
@@ -3141,11 +3271,19 @@ export async function refreshAccountToken(
|
|
|
3141
3271
|
} catch {}
|
|
3142
3272
|
}
|
|
3143
3273
|
|
|
3274
|
+
if (!refreshToken) {
|
|
3275
|
+
try {
|
|
3276
|
+
const liveCookies = await page.context().cookies();
|
|
3277
|
+
refreshToken = liveCookies.find((c) => c.name === "refresh_token")?.value;
|
|
3278
|
+
} catch {}
|
|
3279
|
+
}
|
|
3280
|
+
|
|
3144
3281
|
// 2. If we have a refresh token, invoke the working refresh endpoint in-page
|
|
3145
3282
|
if (refreshToken) {
|
|
3146
3283
|
try {
|
|
3284
|
+
const webVersion = getQwenWebVersion() || "0.3.26";
|
|
3147
3285
|
const refreshResult = await page.evaluate(
|
|
3148
|
-
async ({ rTok, tz }) => {
|
|
3286
|
+
async ({ rTok, tz, version }) => {
|
|
3149
3287
|
try {
|
|
3150
3288
|
const response = await fetch("https://auth.qwen.ai/api/v2/auths/refresh", {
|
|
3151
3289
|
method: "GET",
|
|
@@ -3153,7 +3291,7 @@ export async function refreshAccountToken(
|
|
|
3153
3291
|
headers: {
|
|
3154
3292
|
accept: "application/json, text/plain, */*",
|
|
3155
3293
|
source: "web",
|
|
3156
|
-
version
|
|
3294
|
+
version,
|
|
3157
3295
|
"x-request-origin": "https://chat.qwen.ai",
|
|
3158
3296
|
timezone: tz,
|
|
3159
3297
|
authorization: `Bearer ${rTok}`,
|
|
@@ -3165,7 +3303,7 @@ export async function refreshAccountToken(
|
|
|
3165
3303
|
return { ok: false, error: e.message };
|
|
3166
3304
|
}
|
|
3167
3305
|
},
|
|
3168
|
-
{ rTok: refreshToken, tz: formatAsciiTimezone() },
|
|
3306
|
+
{ rTok: refreshToken, tz: formatAsciiTimezone(), version: webVersion },
|
|
3169
3307
|
);
|
|
3170
3308
|
|
|
3171
3309
|
const json = refreshResult?.data;
|
|
@@ -4046,19 +4184,32 @@ async function refreshHeadersInternal(
|
|
|
4046
4184
|
await executeReauth();
|
|
4047
4185
|
} else {
|
|
4048
4186
|
try {
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4187
|
+
const currentUrl = typeof page.url === "function" ? page.url() : "";
|
|
4188
|
+
const needsNavigation =
|
|
4189
|
+
!currentUrl.startsWith(qwenOrigin()) ||
|
|
4190
|
+
currentUrl.includes("/auth") ||
|
|
4191
|
+
currentUrl.includes("/login") ||
|
|
4192
|
+
currentUrl.includes("_____tmd_____");
|
|
4193
|
+
|
|
4194
|
+
if (needsNavigation) {
|
|
4195
|
+
await page.goto(qwenUrl("/"), {
|
|
4196
|
+
waitUntil: "domcontentloaded",
|
|
4197
|
+
timeout: Math.min(
|
|
4198
|
+
config.timeouts.navigation,
|
|
4199
|
+
boundedTimeoutMs,
|
|
4200
|
+
SESSION_PROBE_NAVIGATION_TIMEOUT_MS,
|
|
4201
|
+
),
|
|
4202
|
+
});
|
|
4203
|
+
await sleep(2000);
|
|
4204
|
+
}
|
|
4205
|
+
|
|
4206
|
+
const url = typeof page.url === "function" ? page.url() : "";
|
|
4059
4207
|
const isAuthUrl = url.includes("auth") || url.includes("login");
|
|
4060
4208
|
if (isAuthUrl) {
|
|
4061
|
-
await
|
|
4209
|
+
const refreshed = await refreshAccountToken(accountId);
|
|
4210
|
+
if (!refreshed.success) {
|
|
4211
|
+
await executeReauth();
|
|
4212
|
+
}
|
|
4062
4213
|
}
|
|
4063
4214
|
} catch (navErr) {
|
|
4064
4215
|
console.warn(
|
|
@@ -4118,12 +4269,18 @@ async function refreshHeadersInternal(
|
|
|
4118
4269
|
|
|
4119
4270
|
await captureQwenHeaders(accountId, undefined, boundedTimeoutMs);
|
|
4120
4271
|
|
|
4121
|
-
// Best-effort restore: header capture
|
|
4272
|
+
// Best-effort restore: if header capture left the tab on a punish/auth URL,
|
|
4273
|
+
// return to the canonical chat home without reloading an already-clean Qwen page.
|
|
4122
4274
|
const capturedPage = accountPages.get(accountId);
|
|
4123
4275
|
if (capturedPage && !capturedPage.isClosed()) {
|
|
4124
4276
|
try {
|
|
4125
4277
|
const currentUrl = new URL(capturedPage.url());
|
|
4126
|
-
if (
|
|
4278
|
+
if (
|
|
4279
|
+
currentUrl.origin !== qwenOrigin() ||
|
|
4280
|
+
currentUrl.pathname.includes("/auth") ||
|
|
4281
|
+
currentUrl.pathname.includes("/login") ||
|
|
4282
|
+
currentUrl.pathname.includes("_____tmd_____")
|
|
4283
|
+
) {
|
|
4127
4284
|
await capturedPage.goto(qwenUrl("/"), {
|
|
4128
4285
|
waitUntil: "domcontentloaded",
|
|
4129
4286
|
timeout: Math.min(config.timeouts.navigation, boundedTimeoutMs),
|
|
@@ -4568,7 +4725,13 @@ function priorityOrderForEviction(accountIds: string[]): string[] {
|
|
|
4568
4725
|
const ranked = getAccountsByPriority(accountIds.map((id) => ({ id })));
|
|
4569
4726
|
const rank = new Map<string, number>();
|
|
4570
4727
|
for (const [index, account] of ranked.entries()) {
|
|
4571
|
-
|
|
4728
|
+
// Accounts currently on cooldown (RateLimited / MembershipLimit / WafChallenge)
|
|
4729
|
+
// cannot serve traffic and must be evicted before any healthy warm account.
|
|
4730
|
+
if (getAccountCooldownInfo(account.id)) {
|
|
4731
|
+
rank.set(account.id, Number.MAX_SAFE_INTEGER);
|
|
4732
|
+
} else {
|
|
4733
|
+
rank.set(account.id, index);
|
|
4734
|
+
}
|
|
4572
4735
|
}
|
|
4573
4736
|
return orderContextsForEviction(
|
|
4574
4737
|
accountIds,
|
|
@@ -4625,15 +4788,21 @@ export async function closeIdlePlaywrightAccounts(
|
|
|
4625
4788
|
* PLAYWRIGHT_MAX_ACTIVE_CONTEXTS. Never closes a context whose account mutex
|
|
4626
4789
|
* is busy, so active streams are preserved.
|
|
4627
4790
|
*/
|
|
4628
|
-
export async function evictIdlePlaywrightContextsToLimit(
|
|
4629
|
-
|
|
4630
|
-
|
|
4791
|
+
export async function evictIdlePlaywrightContextsToLimit(
|
|
4792
|
+
targetLimit?: number,
|
|
4793
|
+
): Promise<number> {
|
|
4794
|
+
const configuredMax = config.playwright.maxActiveContexts;
|
|
4795
|
+
if (configuredMax <= 0) return 0;
|
|
4796
|
+
const max = targetLimit !== undefined ? Math.max(0, targetLimit) : configuredMax;
|
|
4631
4797
|
if (accountPages.size <= max) return 0;
|
|
4632
4798
|
|
|
4633
4799
|
const candidates = priorityOrderForEviction(
|
|
4634
4800
|
Array.from(accountPages.keys()).filter((accountId) => {
|
|
4635
4801
|
if (isAccountServingStream(accountId)) return false;
|
|
4636
|
-
|
|
4802
|
+
// A request may hold a stream lease while still acquiring the page mutex
|
|
4803
|
+
// (pre-stream setup). Closing that context would fail the request even
|
|
4804
|
+
// though the mutex still looks idle, so treat any lease as "in use".
|
|
4805
|
+
if (hasActiveAccountLease(accountId)) return false;
|
|
4637
4806
|
const mutex = accountMutexes.get(accountId);
|
|
4638
4807
|
return mutex?.isIdle();
|
|
4639
4808
|
}),
|
|
@@ -4647,6 +4816,7 @@ export async function evictIdlePlaywrightContextsToLimit(): Promise<number> {
|
|
|
4647
4816
|
for (const candidate of candidates) {
|
|
4648
4817
|
if (accountPages.size <= max) break;
|
|
4649
4818
|
if (isAccountServingStream(candidate.accountId)) continue;
|
|
4819
|
+
if (hasActiveAccountLease(candidate.accountId)) continue;
|
|
4650
4820
|
const mutex = accountMutexes.get(candidate.accountId);
|
|
4651
4821
|
if (!mutex?.isIdle()) continue;
|
|
4652
4822
|
|
package/src/services/qwen.ts
CHANGED
|
@@ -129,6 +129,20 @@ const browserStreamStates = new Map<string, BrowserStreamState>();
|
|
|
129
129
|
const browserStreamBindingPages = new WeakSet<object>();
|
|
130
130
|
const browserStreamBindingContexts = new WeakSet<object>();
|
|
131
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Whether the account currently has an in-flight browser stream. Captcha
|
|
134
|
+
* recovery uses this to avoid navigating the page that is feeding a live
|
|
135
|
+
* stream (it opens an ephemeral tab instead).
|
|
136
|
+
*/
|
|
137
|
+
export function hasActiveBrowserStreamsForAccount(accountId: string): boolean {
|
|
138
|
+
for (const state of browserStreamStates.values()) {
|
|
139
|
+
if (state.accountId === accountId && !state.done) {
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
|
|
132
146
|
/**
|
|
133
147
|
* Abort all active browser streams for an account immediately.
|
|
134
148
|
* Called when an account page is navigated by captcha recovery, closed, or reset,
|
|
@@ -1364,12 +1378,22 @@ async function createQwenBrowserResponse(
|
|
|
1364
1378
|
await ensureBrowserStreamBinding(page);
|
|
1365
1379
|
if (typeof (page as any).on === "function") {
|
|
1366
1380
|
const onFrameNavigated = (frame: any) => {
|
|
1367
|
-
if (
|
|
1368
|
-
|
|
1369
|
-
|
|
1381
|
+
if (
|
|
1382
|
+
!settled &&
|
|
1383
|
+
typeof (page as any).mainFrame === "function" &&
|
|
1384
|
+
frame === (page as any).mainFrame()
|
|
1385
|
+
) {
|
|
1386
|
+
const url =
|
|
1387
|
+
typeof frame.url === "function"
|
|
1388
|
+
? frame.url()
|
|
1389
|
+
: String(frame.url || "");
|
|
1390
|
+
// Same-origin SPA routing within Qwen Web (e.g. /c/<id>, /#) keeps
|
|
1391
|
+
// the live DOM and MUST NOT abort the stream. Only a genuine
|
|
1392
|
+
// unload (auth, Baxia punish, or another origin) kills the fetch.
|
|
1370
1393
|
if (
|
|
1371
1394
|
url.startsWith(qwenOrigin()) &&
|
|
1372
1395
|
!url.includes("/auth") &&
|
|
1396
|
+
!url.includes("/login") &&
|
|
1373
1397
|
!url.includes("_____tmd_____") &&
|
|
1374
1398
|
!url.includes("/punish")
|
|
1375
1399
|
) {
|
|
@@ -1393,10 +1417,12 @@ async function createQwenBrowserResponse(
|
|
|
1393
1417
|
};
|
|
1394
1418
|
(page as any).on("framenavigated", onFrameNavigated);
|
|
1395
1419
|
(page as any).on("close", onPageClosed);
|
|
1420
|
+
(page as any).on("crash", onPageClosed);
|
|
1396
1421
|
pageCleanup = () => {
|
|
1397
1422
|
try {
|
|
1398
1423
|
(page as any).off?.("framenavigated", onFrameNavigated);
|
|
1399
1424
|
(page as any).off?.("close", onPageClosed);
|
|
1425
|
+
(page as any).off?.("crash", onPageClosed);
|
|
1400
1426
|
} catch {}
|
|
1401
1427
|
};
|
|
1402
1428
|
}
|