qwenproxy-cli 1.6.10 → 1.6.12
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 +390 -30
- package/dist/delete-chats.js +946 -445
- package/dist/index.js +612 -100
- package/dist/login.js +417 -51
- package/dist/reset-cooldowns.js +769 -407
- package/dist/sync-clients.js +4 -1
- package/package.json +2 -2
- package/src/api/server.ts +8 -4
- package/src/core/config.ts +4 -1
- package/src/core/model-alias.ts +1 -0
- 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/auth-playwright.ts +3 -2
- package/src/services/captcha-coordinator.ts +41 -11
- package/src/services/captcha-solver.ts +400 -2
- package/src/services/chat-cleanup.ts +11 -2
- package/src/services/playwright.ts +135 -50
- package/src/services/qwen-chat-pool.ts +52 -9
- package/src/services/qwen-headers.ts +18 -1
- package/src/services/qwen.ts +35 -7
- package/src/services/session-keeper.ts +70 -8
|
@@ -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 {
|
|
@@ -77,10 +78,15 @@ import {
|
|
|
77
78
|
type FingerprintProfile,
|
|
78
79
|
} from "./fingerprint.ts";
|
|
79
80
|
import { subtlePageActivity } from "./human-behavior.ts";
|
|
80
|
-
import {
|
|
81
|
+
import { solveVisibleCaptcha } from "./captcha-solver.ts";
|
|
81
82
|
import { qwenOrigin, qwenUrl } from "./qwen-url.ts";
|
|
82
83
|
import { setWafContextResetListener } from "../core/waf-isolation.ts";
|
|
83
|
-
import {
|
|
84
|
+
import {
|
|
85
|
+
getQwenBxVersion,
|
|
86
|
+
getQwenWebVersion,
|
|
87
|
+
updateQwenBxVersion,
|
|
88
|
+
updateQwenWebVersion,
|
|
89
|
+
} from "./qwen-headers.ts";
|
|
84
90
|
import { getAccountProfilePath, getProfilesDir } from "../core/paths.ts";
|
|
85
91
|
import { parseJwtExpiry, isTokenExpiringSoon } from "../utils/jwt.ts";
|
|
86
92
|
import { formatAsciiTimezone } from "../utils/timezone.ts";
|
|
@@ -597,13 +603,14 @@ const CHAT_INPUT_ACTION_TIMEOUT_MS = 10_000;
|
|
|
597
603
|
*/
|
|
598
604
|
async function clearVisibleChallenge(page: Page): Promise<void> {
|
|
599
605
|
if (page.isClosed()) return;
|
|
600
|
-
// 1. Solve slider
|
|
606
|
+
// 1. Solve a slider/jigsaw challenge if present (Baxia NC or Aliyun WAF)
|
|
601
607
|
if (config.captcha.enabled) {
|
|
602
|
-
await
|
|
608
|
+
await solveVisibleCaptcha(page, {
|
|
603
609
|
waitForMs: 0,
|
|
604
610
|
maxAttempts: config.captcha.maxAttempts,
|
|
605
611
|
retryDelayMs: config.captcha.retryDelayMs,
|
|
606
612
|
settleMs: config.captcha.settleMs,
|
|
613
|
+
sliderTimeoutMs: config.captcha.sliderTimeoutMs,
|
|
607
614
|
}).catch(() => false);
|
|
608
615
|
}
|
|
609
616
|
|
|
@@ -1306,13 +1313,22 @@ export async function getBasicHeaders(accountId: string): Promise<{
|
|
|
1306
1313
|
mobile = data.mobile ? "?1" : "?0";
|
|
1307
1314
|
}
|
|
1308
1315
|
let bundleVersion: string | null = null;
|
|
1316
|
+
let baxiaVersion: string | null = null;
|
|
1309
1317
|
try {
|
|
1310
1318
|
const el = document.querySelector('link[href*="qwen-chat-fe"], script[src*="qwen-chat-fe"], img[src*="qwen-chat-fe"]');
|
|
1311
1319
|
const src = el ? (el.getAttribute("href") || el.getAttribute("src") || "") : "";
|
|
1312
1320
|
const match = src.match(/qwen-chat-fe\/(\d+\.\d+\.\d+)/) || document.documentElement.innerHTML.match(/qwen-chat-fe\/(\d+\.\d+\.\d+)/);
|
|
1313
1321
|
if (match) bundleVersion = match[1];
|
|
1314
1322
|
} catch {}
|
|
1315
|
-
|
|
1323
|
+
try {
|
|
1324
|
+
const baxia = (window as unknown as {
|
|
1325
|
+
baxiaCommon?: { version?: unknown };
|
|
1326
|
+
}).baxiaCommon;
|
|
1327
|
+
if (typeof baxia?.version === "string") {
|
|
1328
|
+
baxiaVersion = baxia.version;
|
|
1329
|
+
}
|
|
1330
|
+
} catch {}
|
|
1331
|
+
return { ua, secChUa, platform, mobile, bundleVersion, baxiaVersion };
|
|
1316
1332
|
}),
|
|
1317
1333
|
config.timeouts.page,
|
|
1318
1334
|
`User-agent lookup timed out for ${accountId}`,
|
|
@@ -1321,6 +1337,9 @@ export async function getBasicHeaders(accountId: string): Promise<{
|
|
|
1321
1337
|
if (nav.bundleVersion) {
|
|
1322
1338
|
updateQwenWebVersion(nav.bundleVersion);
|
|
1323
1339
|
}
|
|
1340
|
+
if (nav.baxiaVersion) {
|
|
1341
|
+
updateQwenBxVersion(nav.baxiaVersion);
|
|
1342
|
+
}
|
|
1324
1343
|
cachedUserAgents.set(accountId, userAgent);
|
|
1325
1344
|
const hints = getHeaderCache(accountId).headers;
|
|
1326
1345
|
clientHints = {
|
|
@@ -1358,7 +1377,7 @@ export async function getBasicHeaders(accountId: string): Promise<{
|
|
|
1358
1377
|
markAccountHeadersReady(accountId);
|
|
1359
1378
|
const bxUa = cache.headers["bx-ua"];
|
|
1360
1379
|
const bxUmidtoken = cache.headers["bx-umidtoken"];
|
|
1361
|
-
const bxV = cache.headers["bx-v"] ||
|
|
1380
|
+
const bxV = cache.headers["bx-v"] || getQwenBxVersion();
|
|
1362
1381
|
const cookie = await getCookies(accountId);
|
|
1363
1382
|
return { cookie, userAgent, bxV, bxUa, bxUmidtoken, ...clientHints };
|
|
1364
1383
|
}
|
|
@@ -1382,7 +1401,7 @@ export async function getBasicHeaders(accountId: string): Promise<{
|
|
|
1382
1401
|
cookieCaches.set(accountId, { cookie, timestamp: Date.now() });
|
|
1383
1402
|
const bxUa = cache.headers["bx-ua"];
|
|
1384
1403
|
const bxUmidtoken = cache.headers["bx-umidtoken"];
|
|
1385
|
-
const bxV = cache.headers["bx-v"] ||
|
|
1404
|
+
const bxV = cache.headers["bx-v"] || getQwenBxVersion();
|
|
1386
1405
|
cache.lastRefresh = Date.now();
|
|
1387
1406
|
markAccountHeadersReady(accountId);
|
|
1388
1407
|
console.log(
|
|
@@ -1408,18 +1427,19 @@ export async function getBasicHeaders(accountId: string): Promise<{
|
|
|
1408
1427
|
} else if (
|
|
1409
1428
|
hadUsableHeaders &&
|
|
1410
1429
|
headersAge > HEADER_CACHE_TTL * HEADER_REFRESH_THRESHOLD &&
|
|
1411
|
-
!cache.refreshInProgress
|
|
1430
|
+
!cache.refreshInProgress &&
|
|
1431
|
+
// Header capture types into the chat input and sends a probe message on
|
|
1432
|
+
// the account's page. Doing that while the account is serving a stream
|
|
1433
|
+
// would mutate the live conversation (and can navigate the SPA), so the
|
|
1434
|
+
// proactive refresh waits until the account has no active lease.
|
|
1435
|
+
!hasActiveAccountLease(accountId)
|
|
1412
1436
|
) {
|
|
1413
1437
|
// Background refresh at 70% TTL: keep headers fresh without blocking
|
|
1414
|
-
cache.refreshInProgress = true;
|
|
1415
1438
|
refreshHeadersInternal(accountId)
|
|
1416
1439
|
.catch((error) => {
|
|
1417
1440
|
console.warn(
|
|
1418
1441
|
`⚠️ [Playwright] Background header refresh failed for ${accountId}: ${getErrorMessage(error)}`,
|
|
1419
1442
|
);
|
|
1420
|
-
})
|
|
1421
|
-
.finally(() => {
|
|
1422
|
-
cache.refreshInProgress = false;
|
|
1423
1443
|
});
|
|
1424
1444
|
}
|
|
1425
1445
|
|
|
@@ -1463,6 +1483,9 @@ export async function getBasicHeaders(accountId: string): Promise<{
|
|
|
1463
1483
|
if (persisted.version) {
|
|
1464
1484
|
updateQwenWebVersion(persisted.version);
|
|
1465
1485
|
}
|
|
1486
|
+
if (persisted.bxV) {
|
|
1487
|
+
updateQwenBxVersion(persisted.bxV);
|
|
1488
|
+
}
|
|
1466
1489
|
cache.lastRefresh = persisted.capturedAt;
|
|
1467
1490
|
markAccountHeadersReady(accountId);
|
|
1468
1491
|
}
|
|
@@ -1492,7 +1515,7 @@ export async function getBasicHeaders(accountId: string): Promise<{
|
|
|
1492
1515
|
markAccountHeadersReady(accountId);
|
|
1493
1516
|
const bxUa = cache.headers["bx-ua"];
|
|
1494
1517
|
const bxUmidtoken = cache.headers["bx-umidtoken"];
|
|
1495
|
-
const bxV = cache.headers["bx-v"] ||
|
|
1518
|
+
const bxV = cache.headers["bx-v"] || getQwenBxVersion();
|
|
1496
1519
|
|
|
1497
1520
|
// Read cookie AFTER all refreshes (re-login may have updated it)
|
|
1498
1521
|
cookieCaches.delete(accountId);
|
|
@@ -1569,8 +1592,13 @@ export async function initPlaywrightForAccount(
|
|
|
1569
1592
|
return;
|
|
1570
1593
|
}
|
|
1571
1594
|
|
|
1572
|
-
// If a context limit is configured, make room by
|
|
1573
|
-
|
|
1595
|
+
// If a context limit is configured, make room BEFORE launching by evicting down to (max - 1)
|
|
1596
|
+
// so the new context brings total active contexts to at most max.
|
|
1597
|
+
if (config.playwright.maxActiveContexts > 0) {
|
|
1598
|
+
await evictIdlePlaywrightContextsToLimit(
|
|
1599
|
+
Math.max(1, config.playwright.maxActiveContexts - 1),
|
|
1600
|
+
).catch(() => {});
|
|
1601
|
+
}
|
|
1574
1602
|
|
|
1575
1603
|
const profilePath = getAccountProfilePath(account.id);
|
|
1576
1604
|
const fingerprint = getFingerprintProfile(account.id);
|
|
@@ -1779,6 +1807,9 @@ export async function initPlaywrightForAccount(
|
|
|
1779
1807
|
if (persisted.version) {
|
|
1780
1808
|
updateQwenWebVersion(persisted.version);
|
|
1781
1809
|
}
|
|
1810
|
+
if (persisted.bxV) {
|
|
1811
|
+
updateQwenBxVersion(persisted.bxV);
|
|
1812
|
+
}
|
|
1782
1813
|
cache.lastRefresh = persisted.capturedAt;
|
|
1783
1814
|
markAccountHeadersReady(account.id);
|
|
1784
1815
|
restoredFromDb = true;
|
|
@@ -1882,12 +1913,17 @@ export async function initPlaywrightForAccount(
|
|
|
1882
1913
|
await captureQwenHeaders(account.id);
|
|
1883
1914
|
}
|
|
1884
1915
|
|
|
1885
|
-
// Header capture may leave the UI on a
|
|
1886
|
-
// primary tab to the canonical chat home.
|
|
1916
|
+
// Header capture may leave the UI on a punish or auth page. Return the
|
|
1917
|
+
// primary tab to the canonical chat home only when not already on a clean Qwen page.
|
|
1887
1918
|
if (!acctPage.isClosed()) {
|
|
1888
1919
|
try {
|
|
1889
1920
|
const currentUrl = new URL(acctPage.url());
|
|
1890
|
-
if (
|
|
1921
|
+
if (
|
|
1922
|
+
currentUrl.origin !== qwenOrigin() ||
|
|
1923
|
+
currentUrl.pathname.includes("/auth") ||
|
|
1924
|
+
currentUrl.pathname.includes("/login") ||
|
|
1925
|
+
currentUrl.pathname.includes("_____tmd_____")
|
|
1926
|
+
) {
|
|
1891
1927
|
await acctPage.goto(qwenUrl("/"), {
|
|
1892
1928
|
waitUntil: "domcontentloaded",
|
|
1893
1929
|
timeout: config.timeouts.navigation,
|
|
@@ -1907,6 +1943,7 @@ export async function initPlaywrightForAccount(
|
|
|
1907
1943
|
} finally {
|
|
1908
1944
|
release();
|
|
1909
1945
|
}
|
|
1946
|
+
await evictIdlePlaywrightContextsToLimit().catch(() => {});
|
|
1910
1947
|
})();
|
|
1911
1948
|
|
|
1912
1949
|
try {
|
|
@@ -2226,7 +2263,7 @@ export async function startEmailOtpLogin(
|
|
|
2226
2263
|
{
|
|
2227
2264
|
email: normalizedEmail,
|
|
2228
2265
|
requestId,
|
|
2229
|
-
version: getQwenWebVersion()
|
|
2266
|
+
version: getQwenWebVersion(),
|
|
2230
2267
|
timezone: formatAsciiTimezone(),
|
|
2231
2268
|
},
|
|
2232
2269
|
).catch((err: any) => ({ ok: false, error: err?.message || String(err) }));
|
|
@@ -2338,7 +2375,7 @@ export async function verifyEmailOtpLogin(
|
|
|
2338
2375
|
email: session.email,
|
|
2339
2376
|
code: cleanCode,
|
|
2340
2377
|
requestId,
|
|
2341
|
-
version: getQwenWebVersion()
|
|
2378
|
+
version: getQwenWebVersion(),
|
|
2342
2379
|
timezone: formatAsciiTimezone(),
|
|
2343
2380
|
},
|
|
2344
2381
|
).catch((err: any) => ({ ok: false, error: err?.message || String(err) }));
|
|
@@ -2395,8 +2432,8 @@ export async function verifyEmailOtpLogin(
|
|
|
2395
2432
|
saveAuthSession(session.accountId, {
|
|
2396
2433
|
cookie: `token=${accessToken}`,
|
|
2397
2434
|
userAgent: session.fingerprint.userAgent,
|
|
2398
|
-
bxV:
|
|
2399
|
-
version: getQwenWebVersion()
|
|
2435
|
+
bxV: getQwenBxVersion(),
|
|
2436
|
+
version: getQwenWebVersion(),
|
|
2400
2437
|
userId: userId || undefined,
|
|
2401
2438
|
tokenExpiresAt: exp || undefined,
|
|
2402
2439
|
refreshToken: refreshToken || undefined,
|
|
@@ -2552,7 +2589,7 @@ async function loginToQwen(
|
|
|
2552
2589
|
saveAuthSession(accountId, {
|
|
2553
2590
|
cookie: cookieStr,
|
|
2554
2591
|
userAgent: cache.headers["user-agent"] || "",
|
|
2555
|
-
bxV: cache.headers["bx-v"] ||
|
|
2592
|
+
bxV: cache.headers["bx-v"] || getQwenBxVersion(),
|
|
2556
2593
|
bxUa: cache.headers["bx-ua"] || "",
|
|
2557
2594
|
bxUmidtoken: cache.headers["bx-umidtoken"] || "",
|
|
2558
2595
|
secChUa: cache.headers["sec-ch-ua"] || undefined,
|
|
@@ -2628,7 +2665,7 @@ async function loginToQwen(
|
|
|
2628
2665
|
saveAuthSession(accountId, {
|
|
2629
2666
|
cookie: cookieStr,
|
|
2630
2667
|
userAgent: cache.headers["user-agent"] || "",
|
|
2631
|
-
bxV: cache.headers["bx-v"] ||
|
|
2668
|
+
bxV: cache.headers["bx-v"] || getQwenBxVersion(),
|
|
2632
2669
|
bxUa: cache.headers["bx-ua"] || "",
|
|
2633
2670
|
bxUmidtoken: cache.headers["bx-umidtoken"] || "",
|
|
2634
2671
|
secChUa: cache.headers["sec-ch-ua"] || undefined,
|
|
@@ -2713,7 +2750,7 @@ async function loginViaApi(
|
|
|
2713
2750
|
|
|
2714
2751
|
const signinUrl = "https://auth.qwen.ai/api/v2/auths/signin";
|
|
2715
2752
|
const requestId = crypto.randomUUID();
|
|
2716
|
-
const webVersion = getQwenWebVersion()
|
|
2753
|
+
const webVersion = getQwenWebVersion();
|
|
2717
2754
|
|
|
2718
2755
|
const evalRes = await page
|
|
2719
2756
|
.evaluate(
|
|
@@ -2922,6 +2959,18 @@ async function loginViaUi(
|
|
|
2922
2959
|
return { success: true };
|
|
2923
2960
|
}
|
|
2924
2961
|
|
|
2962
|
+
// A WAF jigsaw can render before the form is usable. Clear it first so the
|
|
2963
|
+
// email/password fields are not covered and the submit is not rejected.
|
|
2964
|
+
if (config.captcha.enabled) {
|
|
2965
|
+
await solveVisibleCaptcha(page, {
|
|
2966
|
+
waitForMs: 1_500,
|
|
2967
|
+
maxAttempts: config.captcha.maxAttempts,
|
|
2968
|
+
retryDelayMs: config.captcha.retryDelayMs,
|
|
2969
|
+
settleMs: config.captcha.settleMs,
|
|
2970
|
+
sliderTimeoutMs: config.captcha.sliderTimeoutMs,
|
|
2971
|
+
}).catch(() => false);
|
|
2972
|
+
}
|
|
2973
|
+
|
|
2925
2974
|
// Wait for email input
|
|
2926
2975
|
const emailSelector = [
|
|
2927
2976
|
'input[type="email"]',
|
|
@@ -3116,12 +3165,13 @@ async function loginViaUi(
|
|
|
3116
3165
|
while (Date.now() < postSubmitDeadline) {
|
|
3117
3166
|
if (page.isClosed()) break;
|
|
3118
3167
|
|
|
3119
|
-
// If a slider
|
|
3168
|
+
// If a slider/jigsaw challenge appeared after submit, solve it automatically
|
|
3120
3169
|
if (config.captcha.enabled) {
|
|
3121
|
-
await
|
|
3170
|
+
await solveVisibleCaptcha(page, {
|
|
3122
3171
|
waitForMs: 1500,
|
|
3123
3172
|
maxAttempts: config.captcha.maxAttempts,
|
|
3124
3173
|
retryDelayMs: config.captcha.retryDelayMs,
|
|
3174
|
+
sliderTimeoutMs: config.captcha.sliderTimeoutMs,
|
|
3125
3175
|
}).catch(() => {});
|
|
3126
3176
|
}
|
|
3127
3177
|
|
|
@@ -3268,7 +3318,7 @@ export async function refreshAccountToken(
|
|
|
3268
3318
|
// 2. If we have a refresh token, invoke the working refresh endpoint in-page
|
|
3269
3319
|
if (refreshToken) {
|
|
3270
3320
|
try {
|
|
3271
|
-
const webVersion = getQwenWebVersion()
|
|
3321
|
+
const webVersion = getQwenWebVersion();
|
|
3272
3322
|
const refreshResult = await page.evaluate(
|
|
3273
3323
|
async ({ rTok, tz, version }) => {
|
|
3274
3324
|
try {
|
|
@@ -3341,7 +3391,7 @@ export async function refreshAccountToken(
|
|
|
3341
3391
|
saveAuthSession(accountId, {
|
|
3342
3392
|
cookie: cookieStr,
|
|
3343
3393
|
userAgent: cache.headers["user-agent"] || session?.userAgent || "",
|
|
3344
|
-
bxV: cache.headers["bx-v"] || session?.bxV ||
|
|
3394
|
+
bxV: cache.headers["bx-v"] || session?.bxV || getQwenBxVersion(),
|
|
3345
3395
|
bxUa: cache.headers["bx-ua"] || session?.bxUa || "",
|
|
3346
3396
|
bxUmidtoken: cache.headers["bx-umidtoken"] || session?.bxUmidtoken || "",
|
|
3347
3397
|
secChUa: cache.headers["sec-ch-ua"] || session?.secChUa,
|
|
@@ -3378,7 +3428,7 @@ export async function refreshAccountToken(
|
|
|
3378
3428
|
saveAuthSession(accountId, {
|
|
3379
3429
|
cookie: cookieStr,
|
|
3380
3430
|
userAgent: cache.headers["user-agent"] || session?.userAgent || "",
|
|
3381
|
-
bxV: cache.headers["bx-v"] || session?.bxV ||
|
|
3431
|
+
bxV: cache.headers["bx-v"] || session?.bxV || getQwenBxVersion(),
|
|
3382
3432
|
bxUa: cache.headers["bx-ua"] || session?.bxUa || "",
|
|
3383
3433
|
bxUmidtoken: cache.headers["bx-umidtoken"] || session?.bxUmidtoken || "",
|
|
3384
3434
|
secChUa: cache.headers["sec-ch-ua"] || session?.secChUa,
|
|
@@ -3581,7 +3631,7 @@ export async function captureQwenHeaders(
|
|
|
3581
3631
|
cookie: reqHeaders["cookie"] || "",
|
|
3582
3632
|
"bx-ua": reqHeaders["bx-ua"] || "",
|
|
3583
3633
|
"bx-umidtoken": reqHeaders["bx-umidtoken"] || "",
|
|
3584
|
-
"bx-v": reqHeaders["bx-v"] ||
|
|
3634
|
+
"bx-v": reqHeaders["bx-v"] || getQwenBxVersion(),
|
|
3585
3635
|
"user-agent": reqHeaders["user-agent"] || "",
|
|
3586
3636
|
"x-request-id": reqHeaders["x-request-id"] || "",
|
|
3587
3637
|
"version": reqHeaders["version"] || "",
|
|
@@ -3625,6 +3675,9 @@ export async function captureQwenHeaders(
|
|
|
3625
3675
|
if (capturedHeaders["version"]) {
|
|
3626
3676
|
updateQwenWebVersion(capturedHeaders["version"]);
|
|
3627
3677
|
}
|
|
3678
|
+
if (capturedHeaders["bx-v"]) {
|
|
3679
|
+
updateQwenBxVersion(capturedHeaders["bx-v"]);
|
|
3680
|
+
}
|
|
3628
3681
|
markAccountHeadersReady(accountId);
|
|
3629
3682
|
cache.lastRefresh = Date.now();
|
|
3630
3683
|
// Header interception can set challenge/session cookies, so do not reuse
|
|
@@ -4171,19 +4224,32 @@ async function refreshHeadersInternal(
|
|
|
4171
4224
|
await executeReauth();
|
|
4172
4225
|
} else {
|
|
4173
4226
|
try {
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4227
|
+
const currentUrl = typeof page.url === "function" ? page.url() : "";
|
|
4228
|
+
const needsNavigation =
|
|
4229
|
+
!currentUrl.startsWith(qwenOrigin()) ||
|
|
4230
|
+
currentUrl.includes("/auth") ||
|
|
4231
|
+
currentUrl.includes("/login") ||
|
|
4232
|
+
currentUrl.includes("_____tmd_____");
|
|
4233
|
+
|
|
4234
|
+
if (needsNavigation) {
|
|
4235
|
+
await page.goto(qwenUrl("/"), {
|
|
4236
|
+
waitUntil: "domcontentloaded",
|
|
4237
|
+
timeout: Math.min(
|
|
4238
|
+
config.timeouts.navigation,
|
|
4239
|
+
boundedTimeoutMs,
|
|
4240
|
+
SESSION_PROBE_NAVIGATION_TIMEOUT_MS,
|
|
4241
|
+
),
|
|
4242
|
+
});
|
|
4243
|
+
await sleep(2000);
|
|
4244
|
+
}
|
|
4245
|
+
|
|
4246
|
+
const url = typeof page.url === "function" ? page.url() : "";
|
|
4184
4247
|
const isAuthUrl = url.includes("auth") || url.includes("login");
|
|
4185
4248
|
if (isAuthUrl) {
|
|
4186
|
-
await
|
|
4249
|
+
const refreshed = await refreshAccountToken(accountId);
|
|
4250
|
+
if (!refreshed.success) {
|
|
4251
|
+
await executeReauth();
|
|
4252
|
+
}
|
|
4187
4253
|
}
|
|
4188
4254
|
} catch (navErr) {
|
|
4189
4255
|
console.warn(
|
|
@@ -4224,7 +4290,7 @@ async function refreshHeadersInternal(
|
|
|
4224
4290
|
saveAuthSession(accountId, {
|
|
4225
4291
|
cookie: cookieStr,
|
|
4226
4292
|
userAgent: cache.headers["user-agent"] || "",
|
|
4227
|
-
bxV: cache.headers["bx-v"] ||
|
|
4293
|
+
bxV: cache.headers["bx-v"] || getQwenBxVersion(),
|
|
4228
4294
|
bxUa: cache.headers["bx-ua"] || "",
|
|
4229
4295
|
bxUmidtoken: cache.headers["bx-umidtoken"] || "",
|
|
4230
4296
|
secChUa: cache.headers["sec-ch-ua"] || undefined,
|
|
@@ -4243,12 +4309,18 @@ async function refreshHeadersInternal(
|
|
|
4243
4309
|
|
|
4244
4310
|
await captureQwenHeaders(accountId, undefined, boundedTimeoutMs);
|
|
4245
4311
|
|
|
4246
|
-
// Best-effort restore: header capture
|
|
4312
|
+
// Best-effort restore: if header capture left the tab on a punish/auth URL,
|
|
4313
|
+
// return to the canonical chat home without reloading an already-clean Qwen page.
|
|
4247
4314
|
const capturedPage = accountPages.get(accountId);
|
|
4248
4315
|
if (capturedPage && !capturedPage.isClosed()) {
|
|
4249
4316
|
try {
|
|
4250
4317
|
const currentUrl = new URL(capturedPage.url());
|
|
4251
|
-
if (
|
|
4318
|
+
if (
|
|
4319
|
+
currentUrl.origin !== qwenOrigin() ||
|
|
4320
|
+
currentUrl.pathname.includes("/auth") ||
|
|
4321
|
+
currentUrl.pathname.includes("/login") ||
|
|
4322
|
+
currentUrl.pathname.includes("_____tmd_____")
|
|
4323
|
+
) {
|
|
4252
4324
|
await capturedPage.goto(qwenUrl("/"), {
|
|
4253
4325
|
waitUntil: "domcontentloaded",
|
|
4254
4326
|
timeout: Math.min(config.timeouts.navigation, boundedTimeoutMs),
|
|
@@ -4693,7 +4765,13 @@ function priorityOrderForEviction(accountIds: string[]): string[] {
|
|
|
4693
4765
|
const ranked = getAccountsByPriority(accountIds.map((id) => ({ id })));
|
|
4694
4766
|
const rank = new Map<string, number>();
|
|
4695
4767
|
for (const [index, account] of ranked.entries()) {
|
|
4696
|
-
|
|
4768
|
+
// Accounts currently on cooldown (RateLimited / MembershipLimit / WafChallenge)
|
|
4769
|
+
// cannot serve traffic and must be evicted before any healthy warm account.
|
|
4770
|
+
if (getAccountCooldownInfo(account.id)) {
|
|
4771
|
+
rank.set(account.id, Number.MAX_SAFE_INTEGER);
|
|
4772
|
+
} else {
|
|
4773
|
+
rank.set(account.id, index);
|
|
4774
|
+
}
|
|
4697
4775
|
}
|
|
4698
4776
|
return orderContextsForEviction(
|
|
4699
4777
|
accountIds,
|
|
@@ -4750,15 +4828,21 @@ export async function closeIdlePlaywrightAccounts(
|
|
|
4750
4828
|
* PLAYWRIGHT_MAX_ACTIVE_CONTEXTS. Never closes a context whose account mutex
|
|
4751
4829
|
* is busy, so active streams are preserved.
|
|
4752
4830
|
*/
|
|
4753
|
-
export async function evictIdlePlaywrightContextsToLimit(
|
|
4754
|
-
|
|
4755
|
-
|
|
4831
|
+
export async function evictIdlePlaywrightContextsToLimit(
|
|
4832
|
+
targetLimit?: number,
|
|
4833
|
+
): Promise<number> {
|
|
4834
|
+
const configuredMax = config.playwright.maxActiveContexts;
|
|
4835
|
+
if (configuredMax <= 0) return 0;
|
|
4836
|
+
const max = targetLimit !== undefined ? Math.max(0, targetLimit) : configuredMax;
|
|
4756
4837
|
if (accountPages.size <= max) return 0;
|
|
4757
4838
|
|
|
4758
4839
|
const candidates = priorityOrderForEviction(
|
|
4759
4840
|
Array.from(accountPages.keys()).filter((accountId) => {
|
|
4760
4841
|
if (isAccountServingStream(accountId)) return false;
|
|
4761
|
-
|
|
4842
|
+
// A request may hold a stream lease while still acquiring the page mutex
|
|
4843
|
+
// (pre-stream setup). Closing that context would fail the request even
|
|
4844
|
+
// though the mutex still looks idle, so treat any lease as "in use".
|
|
4845
|
+
if (hasActiveAccountLease(accountId)) return false;
|
|
4762
4846
|
const mutex = accountMutexes.get(accountId);
|
|
4763
4847
|
return mutex?.isIdle();
|
|
4764
4848
|
}),
|
|
@@ -4772,6 +4856,7 @@ export async function evictIdlePlaywrightContextsToLimit(): Promise<number> {
|
|
|
4772
4856
|
for (const candidate of candidates) {
|
|
4773
4857
|
if (accountPages.size <= max) break;
|
|
4774
4858
|
if (isAccountServingStream(candidate.accountId)) continue;
|
|
4859
|
+
if (hasActiveAccountLease(candidate.accountId)) continue;
|
|
4775
4860
|
const mutex = accountMutexes.get(candidate.accountId);
|
|
4776
4861
|
if (!mutex?.isIdle()) continue;
|
|
4777
4862
|
|
|
@@ -4,8 +4,10 @@ import { config, type ChatMode, isLocalChatMode, isStatelessChatMode } from "../
|
|
|
4
4
|
import { logger, isToolcallDebugEnabled } from "../core/logger.ts";
|
|
5
5
|
import {
|
|
6
6
|
computeQuotaCooldownMs,
|
|
7
|
+
getAccountCooldownInfo,
|
|
7
8
|
markAccountRateLimited,
|
|
8
9
|
} from "../core/account-manager.ts";
|
|
10
|
+
import { recordWafHardBlock } from "../core/waf-isolation.ts";
|
|
9
11
|
import { mapClientModelToQwen } from "../core/model-alias.ts";
|
|
10
12
|
import { qwenUrl } from "./qwen-url.ts";
|
|
11
13
|
import { QwenUpstreamError } from "./qwen-errors.ts";
|
|
@@ -23,6 +25,7 @@ async function createQwenChatSession(
|
|
|
23
25
|
model: string,
|
|
24
26
|
accountId?: string,
|
|
25
27
|
chatMode: ChatMode = "thread",
|
|
28
|
+
captchaRecoveryAttempts = 1,
|
|
26
29
|
): Promise<string> {
|
|
27
30
|
if (isAuthMockEnabled()) {
|
|
28
31
|
return process.env.TEST_SESSION_ID || "mock-session";
|
|
@@ -60,15 +63,32 @@ async function createQwenChatSession(
|
|
|
60
63
|
if (!chatId || typeof chatId !== "string") {
|
|
61
64
|
const rawLower = raw.toLowerCase();
|
|
62
65
|
if (looksLikeAntiBotChallengeText(raw)) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
66
|
+
if (captchaRecoveryAttempts > 0) {
|
|
67
|
+
try {
|
|
68
|
+
const { recoverBaxiaCaptcha } = await import("./captcha-coordinator.ts");
|
|
69
|
+
const solved = await recoverBaxiaCaptcha(accountId, "createQwenChatSession", {
|
|
70
|
+
challengeBody: raw,
|
|
71
|
+
});
|
|
72
|
+
if (solved) {
|
|
73
|
+
return createQwenChatSession(
|
|
74
|
+
headers,
|
|
75
|
+
model,
|
|
76
|
+
accountId,
|
|
77
|
+
chatMode,
|
|
78
|
+
captchaRecoveryAttempts - 1,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
} catch {}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Surface the WAF category to the caller so the warm pool can quarantine
|
|
85
|
+
// the account instead of treating this as a generic invalid payload
|
|
86
|
+
// (which would retry the same flagged device identity forever).
|
|
87
|
+
throw new QwenUpstreamError(
|
|
88
|
+
`Qwen create chat blocked by WAF challenge: ${raw.substring(0, 300)}`,
|
|
89
|
+
"FAIL_SYS_USER_VALIDATE",
|
|
90
|
+
403,
|
|
91
|
+
);
|
|
72
92
|
}
|
|
73
93
|
|
|
74
94
|
const isUnauthorized =
|
|
@@ -346,6 +366,29 @@ async function refillQwenChatPool(
|
|
|
346
366
|
console.warn(
|
|
347
367
|
`[WarmPool] Account ${accountId || "global"} rate-limited during chat creation. Marked for cooldown.`,
|
|
348
368
|
);
|
|
369
|
+
} else if (
|
|
370
|
+
accountId &&
|
|
371
|
+
(err.upstreamCode === "FAIL_SYS_USER_VALIDATE" ||
|
|
372
|
+
err.upstreamCode === "RGV587_ERROR" ||
|
|
373
|
+
/_____tmd_____|rgv587_error|fail_sys_user_validate/i.test(
|
|
374
|
+
err.message,
|
|
375
|
+
))
|
|
376
|
+
) {
|
|
377
|
+
// A background warm-up that hits an unsolvable challenge must park the
|
|
378
|
+
// account: otherwise the next refill repeats the same flagged request
|
|
379
|
+
// and spreads the WAF block. Guarded so an already-quarantined account
|
|
380
|
+
// is not escalated twice by concurrent refills.
|
|
381
|
+
const existing = getAccountCooldownInfo(accountId);
|
|
382
|
+
if (existing?.reason === "WafChallenge") {
|
|
383
|
+
console.warn(
|
|
384
|
+
`[WarmPool] Account ${accountId} still on WAF quarantine (${Math.ceil(existing.remainingMs / 1000)}s left); skipping re-escalation.`,
|
|
385
|
+
);
|
|
386
|
+
} else {
|
|
387
|
+
const block = recordWafHardBlock(accountId);
|
|
388
|
+
console.warn(
|
|
389
|
+
`[WarmPool] Unsolved WAF challenge on ${accountId} during chat creation; quarantined ${Math.ceil(block.cooldownMs / 1000)}s and rotated the fingerprint.`,
|
|
390
|
+
);
|
|
391
|
+
}
|
|
349
392
|
}
|
|
350
393
|
}
|
|
351
394
|
if (isToolcallDebugEnabled()) {
|
|
@@ -21,6 +21,7 @@ export function getDefaultQwenUserAgent(major = getChromeMajor()): string {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
let dynamicWebVersion: string | null = null;
|
|
24
|
+
let dynamicBxVersion: string | null = null;
|
|
24
25
|
|
|
25
26
|
export function updateQwenWebVersion(version?: string | null): void {
|
|
26
27
|
if (version && typeof version === "string" && /^\d+\.\d+\.\d+/.test(version)) {
|
|
@@ -32,6 +33,22 @@ export function getQwenWebVersion(): string {
|
|
|
32
33
|
return dynamicWebVersion || config.qwen.webVersion;
|
|
33
34
|
}
|
|
34
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Aliyun Baxia publishes its SDK version at `window.baxiaCommon.version` and
|
|
38
|
+
* also sends it as the `bx-v` header. Detecting it at runtime keeps the
|
|
39
|
+
* anti-bot fingerprint aligned when Alibaba ships a new SDK; the static
|
|
40
|
+
* QWEN_BX_V value remains as a last-resort fallback.
|
|
41
|
+
*/
|
|
42
|
+
export function updateQwenBxVersion(version?: string | null): void {
|
|
43
|
+
if (version && typeof version === "string" && /^\d+\.\d+\.\d+/.test(version)) {
|
|
44
|
+
dynamicBxVersion = version.trim();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function getQwenBxVersion(): string {
|
|
49
|
+
return dynamicBxVersion || config.auth.bxV;
|
|
50
|
+
}
|
|
51
|
+
|
|
35
52
|
export const QWEN_WEB_VERSION = config.qwen.webVersion;
|
|
36
53
|
export const DEFAULT_QWEN_USER_AGENT = getDefaultQwenUserAgent(151);
|
|
37
54
|
const QWEN_TIMEZONE_HEADER = new Date().toString().split(" (")[0];
|
|
@@ -77,7 +94,7 @@ export function buildQwenRequestHeaders(
|
|
|
77
94
|
Connection: "keep-alive",
|
|
78
95
|
"User-Agent": opts.userAgent || getDefaultQwenUserAgent(),
|
|
79
96
|
"X-Request-Id": uuidv4(),
|
|
80
|
-
"bx-v": opts.bxV ||
|
|
97
|
+
"bx-v": opts.bxV || getQwenBxVersion(),
|
|
81
98
|
source: "web",
|
|
82
99
|
version: opts.version || getQwenWebVersion(),
|
|
83
100
|
timezone: opts.extra?.timezone || opts.extra?.Timezone || formatAsciiTimezone(),
|