qwenproxy-cli 1.6.11 → 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 +350 -18
- package/dist/delete-chats.js +900 -474
- package/dist/index.js +484 -58
- package/dist/login.js +373 -27
- package/dist/reset-cooldowns.js +684 -350
- package/dist/sync-clients.js +4 -1
- package/package.json +1 -1
- package/src/core/config.ts +4 -1
- package/src/core/model-alias.ts +1 -0
- package/src/core/version.ts +1 -1
- package/src/services/auth-playwright.ts +3 -2
- package/src/services/captcha-coordinator.ts +6 -4
- package/src/services/captcha-solver.ts +400 -2
- package/src/services/playwright.ts +62 -22
- package/src/services/qwen-chat-pool.ts +52 -9
- package/src/services/qwen-headers.ts +18 -1
- package/src/services/qwen.ts +6 -4
- package/src/services/session-keeper.ts +70 -8
|
@@ -78,10 +78,15 @@ import {
|
|
|
78
78
|
type FingerprintProfile,
|
|
79
79
|
} from "./fingerprint.ts";
|
|
80
80
|
import { subtlePageActivity } from "./human-behavior.ts";
|
|
81
|
-
import {
|
|
81
|
+
import { solveVisibleCaptcha } from "./captcha-solver.ts";
|
|
82
82
|
import { qwenOrigin, qwenUrl } from "./qwen-url.ts";
|
|
83
83
|
import { setWafContextResetListener } from "../core/waf-isolation.ts";
|
|
84
|
-
import {
|
|
84
|
+
import {
|
|
85
|
+
getQwenBxVersion,
|
|
86
|
+
getQwenWebVersion,
|
|
87
|
+
updateQwenBxVersion,
|
|
88
|
+
updateQwenWebVersion,
|
|
89
|
+
} from "./qwen-headers.ts";
|
|
85
90
|
import { getAccountProfilePath, getProfilesDir } from "../core/paths.ts";
|
|
86
91
|
import { parseJwtExpiry, isTokenExpiringSoon } from "../utils/jwt.ts";
|
|
87
92
|
import { formatAsciiTimezone } from "../utils/timezone.ts";
|
|
@@ -598,13 +603,14 @@ const CHAT_INPUT_ACTION_TIMEOUT_MS = 10_000;
|
|
|
598
603
|
*/
|
|
599
604
|
async function clearVisibleChallenge(page: Page): Promise<void> {
|
|
600
605
|
if (page.isClosed()) return;
|
|
601
|
-
// 1. Solve slider
|
|
606
|
+
// 1. Solve a slider/jigsaw challenge if present (Baxia NC or Aliyun WAF)
|
|
602
607
|
if (config.captcha.enabled) {
|
|
603
|
-
await
|
|
608
|
+
await solveVisibleCaptcha(page, {
|
|
604
609
|
waitForMs: 0,
|
|
605
610
|
maxAttempts: config.captcha.maxAttempts,
|
|
606
611
|
retryDelayMs: config.captcha.retryDelayMs,
|
|
607
612
|
settleMs: config.captcha.settleMs,
|
|
613
|
+
sliderTimeoutMs: config.captcha.sliderTimeoutMs,
|
|
608
614
|
}).catch(() => false);
|
|
609
615
|
}
|
|
610
616
|
|
|
@@ -1307,13 +1313,22 @@ export async function getBasicHeaders(accountId: string): Promise<{
|
|
|
1307
1313
|
mobile = data.mobile ? "?1" : "?0";
|
|
1308
1314
|
}
|
|
1309
1315
|
let bundleVersion: string | null = null;
|
|
1316
|
+
let baxiaVersion: string | null = null;
|
|
1310
1317
|
try {
|
|
1311
1318
|
const el = document.querySelector('link[href*="qwen-chat-fe"], script[src*="qwen-chat-fe"], img[src*="qwen-chat-fe"]');
|
|
1312
1319
|
const src = el ? (el.getAttribute("href") || el.getAttribute("src") || "") : "";
|
|
1313
1320
|
const match = src.match(/qwen-chat-fe\/(\d+\.\d+\.\d+)/) || document.documentElement.innerHTML.match(/qwen-chat-fe\/(\d+\.\d+\.\d+)/);
|
|
1314
1321
|
if (match) bundleVersion = match[1];
|
|
1315
1322
|
} catch {}
|
|
1316
|
-
|
|
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 };
|
|
1317
1332
|
}),
|
|
1318
1333
|
config.timeouts.page,
|
|
1319
1334
|
`User-agent lookup timed out for ${accountId}`,
|
|
@@ -1322,6 +1337,9 @@ export async function getBasicHeaders(accountId: string): Promise<{
|
|
|
1322
1337
|
if (nav.bundleVersion) {
|
|
1323
1338
|
updateQwenWebVersion(nav.bundleVersion);
|
|
1324
1339
|
}
|
|
1340
|
+
if (nav.baxiaVersion) {
|
|
1341
|
+
updateQwenBxVersion(nav.baxiaVersion);
|
|
1342
|
+
}
|
|
1325
1343
|
cachedUserAgents.set(accountId, userAgent);
|
|
1326
1344
|
const hints = getHeaderCache(accountId).headers;
|
|
1327
1345
|
clientHints = {
|
|
@@ -1359,7 +1377,7 @@ export async function getBasicHeaders(accountId: string): Promise<{
|
|
|
1359
1377
|
markAccountHeadersReady(accountId);
|
|
1360
1378
|
const bxUa = cache.headers["bx-ua"];
|
|
1361
1379
|
const bxUmidtoken = cache.headers["bx-umidtoken"];
|
|
1362
|
-
const bxV = cache.headers["bx-v"] ||
|
|
1380
|
+
const bxV = cache.headers["bx-v"] || getQwenBxVersion();
|
|
1363
1381
|
const cookie = await getCookies(accountId);
|
|
1364
1382
|
return { cookie, userAgent, bxV, bxUa, bxUmidtoken, ...clientHints };
|
|
1365
1383
|
}
|
|
@@ -1383,7 +1401,7 @@ export async function getBasicHeaders(accountId: string): Promise<{
|
|
|
1383
1401
|
cookieCaches.set(accountId, { cookie, timestamp: Date.now() });
|
|
1384
1402
|
const bxUa = cache.headers["bx-ua"];
|
|
1385
1403
|
const bxUmidtoken = cache.headers["bx-umidtoken"];
|
|
1386
|
-
const bxV = cache.headers["bx-v"] ||
|
|
1404
|
+
const bxV = cache.headers["bx-v"] || getQwenBxVersion();
|
|
1387
1405
|
cache.lastRefresh = Date.now();
|
|
1388
1406
|
markAccountHeadersReady(accountId);
|
|
1389
1407
|
console.log(
|
|
@@ -1465,6 +1483,9 @@ export async function getBasicHeaders(accountId: string): Promise<{
|
|
|
1465
1483
|
if (persisted.version) {
|
|
1466
1484
|
updateQwenWebVersion(persisted.version);
|
|
1467
1485
|
}
|
|
1486
|
+
if (persisted.bxV) {
|
|
1487
|
+
updateQwenBxVersion(persisted.bxV);
|
|
1488
|
+
}
|
|
1468
1489
|
cache.lastRefresh = persisted.capturedAt;
|
|
1469
1490
|
markAccountHeadersReady(accountId);
|
|
1470
1491
|
}
|
|
@@ -1494,7 +1515,7 @@ export async function getBasicHeaders(accountId: string): Promise<{
|
|
|
1494
1515
|
markAccountHeadersReady(accountId);
|
|
1495
1516
|
const bxUa = cache.headers["bx-ua"];
|
|
1496
1517
|
const bxUmidtoken = cache.headers["bx-umidtoken"];
|
|
1497
|
-
const bxV = cache.headers["bx-v"] ||
|
|
1518
|
+
const bxV = cache.headers["bx-v"] || getQwenBxVersion();
|
|
1498
1519
|
|
|
1499
1520
|
// Read cookie AFTER all refreshes (re-login may have updated it)
|
|
1500
1521
|
cookieCaches.delete(accountId);
|
|
@@ -1786,6 +1807,9 @@ export async function initPlaywrightForAccount(
|
|
|
1786
1807
|
if (persisted.version) {
|
|
1787
1808
|
updateQwenWebVersion(persisted.version);
|
|
1788
1809
|
}
|
|
1810
|
+
if (persisted.bxV) {
|
|
1811
|
+
updateQwenBxVersion(persisted.bxV);
|
|
1812
|
+
}
|
|
1789
1813
|
cache.lastRefresh = persisted.capturedAt;
|
|
1790
1814
|
markAccountHeadersReady(account.id);
|
|
1791
1815
|
restoredFromDb = true;
|
|
@@ -2239,7 +2263,7 @@ export async function startEmailOtpLogin(
|
|
|
2239
2263
|
{
|
|
2240
2264
|
email: normalizedEmail,
|
|
2241
2265
|
requestId,
|
|
2242
|
-
version: getQwenWebVersion()
|
|
2266
|
+
version: getQwenWebVersion(),
|
|
2243
2267
|
timezone: formatAsciiTimezone(),
|
|
2244
2268
|
},
|
|
2245
2269
|
).catch((err: any) => ({ ok: false, error: err?.message || String(err) }));
|
|
@@ -2351,7 +2375,7 @@ export async function verifyEmailOtpLogin(
|
|
|
2351
2375
|
email: session.email,
|
|
2352
2376
|
code: cleanCode,
|
|
2353
2377
|
requestId,
|
|
2354
|
-
version: getQwenWebVersion()
|
|
2378
|
+
version: getQwenWebVersion(),
|
|
2355
2379
|
timezone: formatAsciiTimezone(),
|
|
2356
2380
|
},
|
|
2357
2381
|
).catch((err: any) => ({ ok: false, error: err?.message || String(err) }));
|
|
@@ -2408,8 +2432,8 @@ export async function verifyEmailOtpLogin(
|
|
|
2408
2432
|
saveAuthSession(session.accountId, {
|
|
2409
2433
|
cookie: `token=${accessToken}`,
|
|
2410
2434
|
userAgent: session.fingerprint.userAgent,
|
|
2411
|
-
bxV:
|
|
2412
|
-
version: getQwenWebVersion()
|
|
2435
|
+
bxV: getQwenBxVersion(),
|
|
2436
|
+
version: getQwenWebVersion(),
|
|
2413
2437
|
userId: userId || undefined,
|
|
2414
2438
|
tokenExpiresAt: exp || undefined,
|
|
2415
2439
|
refreshToken: refreshToken || undefined,
|
|
@@ -2565,7 +2589,7 @@ async function loginToQwen(
|
|
|
2565
2589
|
saveAuthSession(accountId, {
|
|
2566
2590
|
cookie: cookieStr,
|
|
2567
2591
|
userAgent: cache.headers["user-agent"] || "",
|
|
2568
|
-
bxV: cache.headers["bx-v"] ||
|
|
2592
|
+
bxV: cache.headers["bx-v"] || getQwenBxVersion(),
|
|
2569
2593
|
bxUa: cache.headers["bx-ua"] || "",
|
|
2570
2594
|
bxUmidtoken: cache.headers["bx-umidtoken"] || "",
|
|
2571
2595
|
secChUa: cache.headers["sec-ch-ua"] || undefined,
|
|
@@ -2641,7 +2665,7 @@ async function loginToQwen(
|
|
|
2641
2665
|
saveAuthSession(accountId, {
|
|
2642
2666
|
cookie: cookieStr,
|
|
2643
2667
|
userAgent: cache.headers["user-agent"] || "",
|
|
2644
|
-
bxV: cache.headers["bx-v"] ||
|
|
2668
|
+
bxV: cache.headers["bx-v"] || getQwenBxVersion(),
|
|
2645
2669
|
bxUa: cache.headers["bx-ua"] || "",
|
|
2646
2670
|
bxUmidtoken: cache.headers["bx-umidtoken"] || "",
|
|
2647
2671
|
secChUa: cache.headers["sec-ch-ua"] || undefined,
|
|
@@ -2726,7 +2750,7 @@ async function loginViaApi(
|
|
|
2726
2750
|
|
|
2727
2751
|
const signinUrl = "https://auth.qwen.ai/api/v2/auths/signin";
|
|
2728
2752
|
const requestId = crypto.randomUUID();
|
|
2729
|
-
const webVersion = getQwenWebVersion()
|
|
2753
|
+
const webVersion = getQwenWebVersion();
|
|
2730
2754
|
|
|
2731
2755
|
const evalRes = await page
|
|
2732
2756
|
.evaluate(
|
|
@@ -2935,6 +2959,18 @@ async function loginViaUi(
|
|
|
2935
2959
|
return { success: true };
|
|
2936
2960
|
}
|
|
2937
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
|
+
|
|
2938
2974
|
// Wait for email input
|
|
2939
2975
|
const emailSelector = [
|
|
2940
2976
|
'input[type="email"]',
|
|
@@ -3129,12 +3165,13 @@ async function loginViaUi(
|
|
|
3129
3165
|
while (Date.now() < postSubmitDeadline) {
|
|
3130
3166
|
if (page.isClosed()) break;
|
|
3131
3167
|
|
|
3132
|
-
// If a slider
|
|
3168
|
+
// If a slider/jigsaw challenge appeared after submit, solve it automatically
|
|
3133
3169
|
if (config.captcha.enabled) {
|
|
3134
|
-
await
|
|
3170
|
+
await solveVisibleCaptcha(page, {
|
|
3135
3171
|
waitForMs: 1500,
|
|
3136
3172
|
maxAttempts: config.captcha.maxAttempts,
|
|
3137
3173
|
retryDelayMs: config.captcha.retryDelayMs,
|
|
3174
|
+
sliderTimeoutMs: config.captcha.sliderTimeoutMs,
|
|
3138
3175
|
}).catch(() => {});
|
|
3139
3176
|
}
|
|
3140
3177
|
|
|
@@ -3281,7 +3318,7 @@ export async function refreshAccountToken(
|
|
|
3281
3318
|
// 2. If we have a refresh token, invoke the working refresh endpoint in-page
|
|
3282
3319
|
if (refreshToken) {
|
|
3283
3320
|
try {
|
|
3284
|
-
const webVersion = getQwenWebVersion()
|
|
3321
|
+
const webVersion = getQwenWebVersion();
|
|
3285
3322
|
const refreshResult = await page.evaluate(
|
|
3286
3323
|
async ({ rTok, tz, version }) => {
|
|
3287
3324
|
try {
|
|
@@ -3354,7 +3391,7 @@ export async function refreshAccountToken(
|
|
|
3354
3391
|
saveAuthSession(accountId, {
|
|
3355
3392
|
cookie: cookieStr,
|
|
3356
3393
|
userAgent: cache.headers["user-agent"] || session?.userAgent || "",
|
|
3357
|
-
bxV: cache.headers["bx-v"] || session?.bxV ||
|
|
3394
|
+
bxV: cache.headers["bx-v"] || session?.bxV || getQwenBxVersion(),
|
|
3358
3395
|
bxUa: cache.headers["bx-ua"] || session?.bxUa || "",
|
|
3359
3396
|
bxUmidtoken: cache.headers["bx-umidtoken"] || session?.bxUmidtoken || "",
|
|
3360
3397
|
secChUa: cache.headers["sec-ch-ua"] || session?.secChUa,
|
|
@@ -3391,7 +3428,7 @@ export async function refreshAccountToken(
|
|
|
3391
3428
|
saveAuthSession(accountId, {
|
|
3392
3429
|
cookie: cookieStr,
|
|
3393
3430
|
userAgent: cache.headers["user-agent"] || session?.userAgent || "",
|
|
3394
|
-
bxV: cache.headers["bx-v"] || session?.bxV ||
|
|
3431
|
+
bxV: cache.headers["bx-v"] || session?.bxV || getQwenBxVersion(),
|
|
3395
3432
|
bxUa: cache.headers["bx-ua"] || session?.bxUa || "",
|
|
3396
3433
|
bxUmidtoken: cache.headers["bx-umidtoken"] || session?.bxUmidtoken || "",
|
|
3397
3434
|
secChUa: cache.headers["sec-ch-ua"] || session?.secChUa,
|
|
@@ -3594,7 +3631,7 @@ export async function captureQwenHeaders(
|
|
|
3594
3631
|
cookie: reqHeaders["cookie"] || "",
|
|
3595
3632
|
"bx-ua": reqHeaders["bx-ua"] || "",
|
|
3596
3633
|
"bx-umidtoken": reqHeaders["bx-umidtoken"] || "",
|
|
3597
|
-
"bx-v": reqHeaders["bx-v"] ||
|
|
3634
|
+
"bx-v": reqHeaders["bx-v"] || getQwenBxVersion(),
|
|
3598
3635
|
"user-agent": reqHeaders["user-agent"] || "",
|
|
3599
3636
|
"x-request-id": reqHeaders["x-request-id"] || "",
|
|
3600
3637
|
"version": reqHeaders["version"] || "",
|
|
@@ -3638,6 +3675,9 @@ export async function captureQwenHeaders(
|
|
|
3638
3675
|
if (capturedHeaders["version"]) {
|
|
3639
3676
|
updateQwenWebVersion(capturedHeaders["version"]);
|
|
3640
3677
|
}
|
|
3678
|
+
if (capturedHeaders["bx-v"]) {
|
|
3679
|
+
updateQwenBxVersion(capturedHeaders["bx-v"]);
|
|
3680
|
+
}
|
|
3641
3681
|
markAccountHeadersReady(accountId);
|
|
3642
3682
|
cache.lastRefresh = Date.now();
|
|
3643
3683
|
// Header interception can set challenge/session cookies, so do not reuse
|
|
@@ -4250,7 +4290,7 @@ async function refreshHeadersInternal(
|
|
|
4250
4290
|
saveAuthSession(accountId, {
|
|
4251
4291
|
cookie: cookieStr,
|
|
4252
4292
|
userAgent: cache.headers["user-agent"] || "",
|
|
4253
|
-
bxV: cache.headers["bx-v"] ||
|
|
4293
|
+
bxV: cache.headers["bx-v"] || getQwenBxVersion(),
|
|
4254
4294
|
bxUa: cache.headers["bx-ua"] || "",
|
|
4255
4295
|
bxUmidtoken: cache.headers["bx-umidtoken"] || "",
|
|
4256
4296
|
secChUa: cache.headers["sec-ch-ua"] || undefined,
|
|
@@ -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(),
|
package/src/services/qwen.ts
CHANGED
|
@@ -1006,7 +1006,7 @@ export async function requestQwenTextInBrowser(
|
|
|
1006
1006
|
|
|
1007
1007
|
const evaluateRequest = (page: Page) =>
|
|
1008
1008
|
page.evaluate(
|
|
1009
|
-
async ({ url, method, headers, body, referrer, timeoutMs, tz }: {
|
|
1009
|
+
async ({ url, method, headers, body, referrer, timeoutMs, tz, version }: {
|
|
1010
1010
|
url: string;
|
|
1011
1011
|
method: "GET" | "POST" | "DELETE";
|
|
1012
1012
|
headers: Record<string, string>;
|
|
@@ -1014,6 +1014,7 @@ export async function requestQwenTextInBrowser(
|
|
|
1014
1014
|
referrer?: string;
|
|
1015
1015
|
timeoutMs: number;
|
|
1016
1016
|
tz: string;
|
|
1017
|
+
version: string;
|
|
1017
1018
|
}): Promise<BrowserTextResponse> => {
|
|
1018
1019
|
const controller = new AbortController();
|
|
1019
1020
|
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
@@ -1038,7 +1039,7 @@ export async function requestQwenTextInBrowser(
|
|
|
1038
1039
|
headers: {
|
|
1039
1040
|
accept: "application/json, text/plain, */*",
|
|
1040
1041
|
source: "web",
|
|
1041
|
-
version
|
|
1042
|
+
version,
|
|
1042
1043
|
"x-request-origin": "https://chat.qwen.ai",
|
|
1043
1044
|
timezone: tz,
|
|
1044
1045
|
authorization: `Bearer ${rTok}`,
|
|
@@ -1087,6 +1088,7 @@ export async function requestQwenTextInBrowser(
|
|
|
1087
1088
|
referrer: options.referrer,
|
|
1088
1089
|
timeoutMs: options.timeoutMs ?? Math.min(config.timeouts.page, 20_000),
|
|
1089
1090
|
tz: formatAsciiTimezone(),
|
|
1091
|
+
version: getQwenWebVersion(),
|
|
1090
1092
|
},
|
|
1091
1093
|
);
|
|
1092
1094
|
const recoverOnTimeout = !options.noMutexRecovery;
|
|
@@ -2395,7 +2397,7 @@ export async function deleteSingleQwenChat(
|
|
|
2395
2397
|
|
|
2396
2398
|
const requestHeaders: Record<string, string> = {
|
|
2397
2399
|
source: "web",
|
|
2398
|
-
version: getQwenWebVersion()
|
|
2400
|
+
version: getQwenWebVersion(),
|
|
2399
2401
|
timezone: formatAsciiTimezone(),
|
|
2400
2402
|
"x-request-id": crypto.randomUUID(),
|
|
2401
2403
|
Referer: qwenUrl(`/c/${encodeURIComponent(chatId)}`),
|
|
@@ -2448,7 +2450,7 @@ export async function fetchRemoteQwenChats(
|
|
|
2448
2450
|
}
|
|
2449
2451
|
|
|
2450
2452
|
const requestHeaders: Record<string, string> = {
|
|
2451
|
-
version: getQwenWebVersion()
|
|
2453
|
+
version: getQwenWebVersion(),
|
|
2452
2454
|
timezone: formatAsciiTimezone(),
|
|
2453
2455
|
"x-request-id": crypto.randomUUID(),
|
|
2454
2456
|
Referer: qwenUrl("/settings/chats"),
|
|
@@ -13,6 +13,55 @@ let running = false;
|
|
|
13
13
|
let intervalId: ReturnType<typeof setInterval> | null = null;
|
|
14
14
|
let cycleInProgress = false;
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* A keep-alive failure (navigation timeout, transient WAF, closed context
|
|
18
|
+
* that is not yet reported as closed) must not retry on every cycle: it would
|
|
19
|
+
* hammer the same broken page and bury the logs. Back off per account with the
|
|
20
|
+
* usual exponential schedule, capped at five minutes.
|
|
21
|
+
*/
|
|
22
|
+
const KEEP_ALIVE_BACKOFF_BASE_MS = 2_000;
|
|
23
|
+
const KEEP_ALIVE_BACKOFF_MAX_MS = 5 * 60_000;
|
|
24
|
+
|
|
25
|
+
interface KeepAliveRetryState {
|
|
26
|
+
failures: number;
|
|
27
|
+
nextAttemptAt: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const keepAliveRetries = new Map<string, KeepAliveRetryState>();
|
|
31
|
+
|
|
32
|
+
export function computeSessionKeeperBackoffMs(failures: number): number {
|
|
33
|
+
const exponent = Math.max(0, Math.min(failures - 1, 8));
|
|
34
|
+
return Math.min(
|
|
35
|
+
KEEP_ALIVE_BACKOFF_MAX_MS,
|
|
36
|
+
KEEP_ALIVE_BACKOFF_BASE_MS * 2 ** exponent,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isKeepAliveBackedOff(accountId: string): boolean {
|
|
41
|
+
const state = keepAliveRetries.get(accountId);
|
|
42
|
+
return Boolean(state && Date.now() < state.nextAttemptAt);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function recordKeepAliveFailure(accountId: string): number {
|
|
46
|
+
const state = keepAliveRetries.get(accountId) ?? {
|
|
47
|
+
failures: 0,
|
|
48
|
+
nextAttemptAt: 0,
|
|
49
|
+
};
|
|
50
|
+
state.failures += 1;
|
|
51
|
+
const backoffMs = computeSessionKeeperBackoffMs(state.failures);
|
|
52
|
+
state.nextAttemptAt = Date.now() + backoffMs;
|
|
53
|
+
keepAliveRetries.set(accountId, state);
|
|
54
|
+
return backoffMs;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function resetSessionKeeperRetryStateForTests(): void {
|
|
58
|
+
keepAliveRetries.clear();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function isSessionKeeperAccountBackedOff(accountId: string): boolean {
|
|
62
|
+
return isKeepAliveBackedOff(accountId);
|
|
63
|
+
}
|
|
64
|
+
|
|
16
65
|
export function isSessionKeeperRunning(): boolean {
|
|
17
66
|
return running;
|
|
18
67
|
}
|
|
@@ -24,19 +73,31 @@ async function runKeepAliveCycle(): Promise<void> {
|
|
|
24
73
|
if (config.sessionKeeper.enabled) {
|
|
25
74
|
const accountIds = getActivePlaywrightAccountIds();
|
|
26
75
|
for (const accountId of accountIds) {
|
|
27
|
-
|
|
76
|
+
if (isKeepAliveBackedOff(accountId)) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
const keptAlive = await keepAlivePlaywrightAccount(accountId);
|
|
81
|
+
if (keptAlive) {
|
|
82
|
+
keepAliveRetries.delete(accountId);
|
|
83
|
+
}
|
|
84
|
+
} catch (error) {
|
|
28
85
|
// Shutdown/eviction closes contexts while a cycle is in flight; the
|
|
29
86
|
// resulting "already closed" rejection is benign. The old substring
|
|
30
87
|
// filter ("Target closed"/"Page is closed") missed Playwright's real
|
|
31
88
|
// message ("Target page, context or browser has been closed") and
|
|
32
89
|
// leaked a warning on every Ctrl+C.
|
|
33
|
-
if (isPlaywrightAlreadyClosedError(error))
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
90
|
+
if (isPlaywrightAlreadyClosedError(error)) {
|
|
91
|
+
keepAliveRetries.delete(accountId);
|
|
92
|
+
} else {
|
|
93
|
+
const message =
|
|
94
|
+
error instanceof Error ? error.message : String(error);
|
|
95
|
+
const backoffMs = recordKeepAliveFailure(accountId);
|
|
96
|
+
console.warn(
|
|
97
|
+
`[SessionKeeper] Keep-alive failed for ${accountId}: ${message} (retry in ${Math.round(backoffMs / 1000)}s)`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
40
101
|
await sleep(humanDelay(250, 900));
|
|
41
102
|
}
|
|
42
103
|
}
|
|
@@ -81,6 +142,7 @@ export function stopSessionKeeper(): void {
|
|
|
81
142
|
intervalId = null;
|
|
82
143
|
}
|
|
83
144
|
cycleInProgress = false;
|
|
145
|
+
keepAliveRetries.clear();
|
|
84
146
|
}
|
|
85
147
|
|
|
86
148
|
export async function runSessionKeeperOnceForTesting(): Promise<void> {
|