qwenproxy-cli 1.4.7 → 1.5.0
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 +38 -5
- package/dist/delete-chats.js +262 -74
- package/dist/index.js +327 -111
- package/dist/login.js +169 -59
- package/dist/reset-cooldowns.js +25 -5
- package/package.json +1 -1
- package/src/core/account-manager.ts +9 -0
- package/src/core/database.ts +10 -5
- package/src/core/server-log-buffer.ts +10 -3
- package/src/core/version.ts +1 -1
- package/src/routes/chat/account.ts +22 -10
- package/src/routes/chat/helpers.ts +29 -1
- package/src/routes/chat/retry-policy.ts +9 -8
- package/src/routes/chat/streaming.ts +12 -5
- package/src/routes/upload.ts +19 -14
- package/src/services/playwright.ts +180 -58
- package/src/services/qwen-chat-pool.ts +18 -5
- package/src/services/qwen-headers.ts +2 -1
- package/src/services/qwen.ts +65 -15
|
@@ -479,21 +479,21 @@ export function classifyRetryAction(
|
|
|
479
479
|
// Agent instructions ride ONLY the account-level personalization. An
|
|
480
480
|
// unconfirmed sync means this account cannot serve the request as-is —
|
|
481
481
|
// rotate to another account (each attempt re-syncs on its own account).
|
|
482
|
-
//
|
|
483
|
-
//
|
|
482
|
+
// Do NOT park the account with a 300s cooldown: rotation across accounts
|
|
483
|
+
// is already guarded by triedAccounts, preventing pool collapse.
|
|
484
484
|
if (err instanceof PersonalizationSyncError) {
|
|
485
485
|
return makeRetryAction("personalization_sync_failed", {
|
|
486
486
|
switchAccount: true,
|
|
487
487
|
forceNewChat: true,
|
|
488
488
|
retryAfterMs: baseDelayMs,
|
|
489
|
-
accountCooldownMs:
|
|
489
|
+
accountCooldownMs: 0,
|
|
490
490
|
accountCooldownReason: "PersonalizationFailed",
|
|
491
491
|
});
|
|
492
492
|
}
|
|
493
493
|
|
|
494
494
|
// Upstream 401 / Unauthorized on chat creation or API requests:
|
|
495
|
-
//
|
|
496
|
-
//
|
|
495
|
+
// Access token expired (15m window). Rotate without locking into a 300s cooldown
|
|
496
|
+
// so the account can immediately be renewed via refresh_token.
|
|
497
497
|
if (
|
|
498
498
|
code === "createchatinvalidresponse" ||
|
|
499
499
|
code === "createchatfailed" ||
|
|
@@ -502,15 +502,16 @@ export function classifyRetryAction(
|
|
|
502
502
|
message.includes("não tem permissão para acessar") ||
|
|
503
503
|
message.includes("401 unauthorized") ||
|
|
504
504
|
message.includes('"code":"unauthorized"') ||
|
|
505
|
-
message.includes('"code": "unauthorized"')
|
|
505
|
+
message.includes('"code": "unauthorized"') ||
|
|
506
|
+
message.includes("token has expired")
|
|
506
507
|
) {
|
|
507
508
|
return makeRetryAction("account_initialization_failed", {
|
|
508
509
|
switchAccount: true,
|
|
509
510
|
forceNewChat: true,
|
|
510
511
|
retryWithFullPrompt: true,
|
|
511
512
|
retryAfterMs: Math.min(baseDelayMs, 1_000),
|
|
512
|
-
accountCooldownMs:
|
|
513
|
-
accountCooldownReason: "
|
|
513
|
+
accountCooldownMs: 0,
|
|
514
|
+
accountCooldownReason: "AuthExpired",
|
|
514
515
|
});
|
|
515
516
|
}
|
|
516
517
|
// Specialized recoveries first (even if wrapped as RetryableQwenStreamError)
|
|
@@ -68,6 +68,7 @@ import {
|
|
|
68
68
|
import {
|
|
69
69
|
getIncrementalDelta,
|
|
70
70
|
isThinkingPhase,
|
|
71
|
+
isAnswerPhase,
|
|
71
72
|
extractThinkingContent,
|
|
72
73
|
formatThinkingSummaryContent,
|
|
73
74
|
shouldSuppressStreamAbort,
|
|
@@ -470,7 +471,7 @@ export async function processNonStreamingResponse(
|
|
|
470
471
|
foundStr = true;
|
|
471
472
|
}
|
|
472
473
|
}
|
|
473
|
-
} else if (delta.phase
|
|
474
|
+
} else if (isAnswerPhase(delta.phase)) {
|
|
474
475
|
isThinkingChunk = false;
|
|
475
476
|
if (delta.content !== undefined) {
|
|
476
477
|
const newContent = delta.content || "";
|
|
@@ -1436,7 +1437,7 @@ export async function processStreamingResponse(
|
|
|
1436
1437
|
|
|
1437
1438
|
if (
|
|
1438
1439
|
switchAccount &&
|
|
1439
|
-
(policy.accountCooldownMs
|
|
1440
|
+
(policy.accountCooldownMs ?? 0) > 0
|
|
1440
1441
|
) {
|
|
1441
1442
|
markAccountRateLimited(
|
|
1442
1443
|
currentAccountId,
|
|
@@ -1445,6 +1446,12 @@ export async function processStreamingResponse(
|
|
|
1445
1446
|
);
|
|
1446
1447
|
}
|
|
1447
1448
|
|
|
1449
|
+
if (policy.accountCooldownReason === "AuthExpired") {
|
|
1450
|
+
import("../../services/playwright.ts")
|
|
1451
|
+
.then(({ refreshAccountToken }) => refreshAccountToken(currentAccountId))
|
|
1452
|
+
.catch(() => {});
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1448
1455
|
retryContext.releaseAccountLease?.();
|
|
1449
1456
|
retryContext.releaseAccountLease = null;
|
|
1450
1457
|
await reader.cancel().catch(() => undefined);
|
|
@@ -1776,7 +1783,7 @@ export async function processStreamingResponse(
|
|
|
1776
1783
|
// "phase":"answer"} delta and NO trailing [DONE]. Treat it as
|
|
1777
1784
|
// the terminal event so we don't wait on the keep-alive
|
|
1778
1785
|
// connection to close (up to the 60s/10min idle timeout).
|
|
1779
|
-
if (delta.phase
|
|
1786
|
+
if (isAnswerPhase(delta.phase) && delta.status === "finished") {
|
|
1780
1787
|
upstreamDone = true;
|
|
1781
1788
|
if (!clientDisconnected) flushWrites();
|
|
1782
1789
|
break; // Exit the for loop; the while check leaves the read loop
|
|
@@ -1800,7 +1807,7 @@ export async function processStreamingResponse(
|
|
|
1800
1807
|
foundStr = true;
|
|
1801
1808
|
}
|
|
1802
1809
|
}
|
|
1803
|
-
} else if (delta.phase
|
|
1810
|
+
} else if (isAnswerPhase(delta.phase)) {
|
|
1804
1811
|
isThinkingChunk = false;
|
|
1805
1812
|
if (delta.content !== undefined) {
|
|
1806
1813
|
const newContent = delta.content || "";
|
|
@@ -2290,7 +2297,7 @@ export async function processStreamingResponse(
|
|
|
2290
2297
|
// The retry stream may also terminate with an answer-finished
|
|
2291
2298
|
// delta when upstream sends no [DONE]. Leave the read loop
|
|
2292
2299
|
// immediately so we don't stall on the keep-alive connection.
|
|
2293
|
-
if (delta.phase
|
|
2300
|
+
if (isAnswerPhase(delta.phase) && delta.status === "finished") {
|
|
2294
2301
|
upstreamDone = true;
|
|
2295
2302
|
break retryReadLoop;
|
|
2296
2303
|
}
|
package/src/routes/upload.ts
CHANGED
|
@@ -387,15 +387,18 @@ async function getSTSToken(
|
|
|
387
387
|
}
|
|
388
388
|
data = await response.json().catch(() => null);
|
|
389
389
|
}
|
|
390
|
+
const codeLower = (data?.data?.code || data?.code || "").toLowerCase();
|
|
391
|
+
const detailsLower = (typeof data?.data?.details === "string" ? data.data.details : typeof data?.message === "string" ? data.message : "").toLowerCase();
|
|
390
392
|
const is401 =
|
|
391
393
|
data?.success === false &&
|
|
392
|
-
(
|
|
393
|
-
(
|
|
394
|
-
|
|
394
|
+
(codeLower === "unauthorized" ||
|
|
395
|
+
detailsLower.includes("401") ||
|
|
396
|
+
detailsLower.includes("token has expired") ||
|
|
397
|
+
detailsLower.includes("unauthorized"));
|
|
395
398
|
|
|
396
399
|
if (is401) {
|
|
397
400
|
try {
|
|
398
|
-
const {
|
|
401
|
+
const { refreshAccountToken } = await import("../services/playwright.ts");
|
|
399
402
|
const { getBasicHeaders } = await import("../services/auth-playwright.ts");
|
|
400
403
|
const { loadAccounts } = await import("../core/accounts.ts");
|
|
401
404
|
const resolvedId = accountId ?? loadAccounts()[0]?.id;
|
|
@@ -403,16 +406,18 @@ async function getSTSToken(
|
|
|
403
406
|
console.warn(
|
|
404
407
|
`[Upload] STS token 401 — refreshing session with re-auth and retrying...`,
|
|
405
408
|
);
|
|
406
|
-
await
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
409
|
+
const refreshResult = await refreshAccountToken(resolvedId);
|
|
410
|
+
if (refreshResult.success) {
|
|
411
|
+
const fresh = await getBasicHeaders(resolvedId);
|
|
412
|
+
headers.cookie = fresh.cookie;
|
|
413
|
+
headers["user-agent"] = fresh.userAgent;
|
|
414
|
+
headers["bx-v"] = fresh.bxV;
|
|
415
|
+
if (fresh.bxUa) headers["bx-ua"] = fresh.bxUa;
|
|
416
|
+
if (fresh.bxUmidtoken) headers["bx-umidtoken"] = fresh.bxUmidtoken;
|
|
417
|
+
const retryRes = await doFetch(headers);
|
|
418
|
+
if (retryRes.ok) {
|
|
419
|
+
data = await retryRes.json().catch(() => null);
|
|
420
|
+
}
|
|
416
421
|
}
|
|
417
422
|
}
|
|
418
423
|
} catch (refreshErr) {
|
|
@@ -2005,6 +2005,8 @@ export interface LoginAttemptResult {
|
|
|
2005
2005
|
success: boolean;
|
|
2006
2006
|
permanentFailure?: boolean;
|
|
2007
2007
|
reason?: string;
|
|
2008
|
+
token?: string;
|
|
2009
|
+
refreshToken?: string;
|
|
2008
2010
|
}
|
|
2009
2011
|
|
|
2010
2012
|
export function classifyQwenAuthError(
|
|
@@ -2120,6 +2122,7 @@ async function loginToQwen(
|
|
|
2120
2122
|
secChUaPlatform: cache.headers["sec-ch-ua-platform"] || undefined,
|
|
2121
2123
|
version: cache.headers["version"] || undefined,
|
|
2122
2124
|
tokenExpiresAt: exp || undefined,
|
|
2125
|
+
refreshToken: apiResult.refreshToken,
|
|
2123
2126
|
capturedAt: Date.now(),
|
|
2124
2127
|
});
|
|
2125
2128
|
}
|
|
@@ -2155,6 +2158,12 @@ async function loginToQwen(
|
|
|
2155
2158
|
markAccountHeadersReady(accountId);
|
|
2156
2159
|
const { saveAuthSession } = await import("../core/database.ts");
|
|
2157
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
|
+
}
|
|
2158
2167
|
saveAuthSession(accountId, {
|
|
2159
2168
|
cookie: cookieStr,
|
|
2160
2169
|
userAgent: cache.headers["user-agent"] || "",
|
|
@@ -2166,6 +2175,7 @@ async function loginToQwen(
|
|
|
2166
2175
|
secChUaPlatform: cache.headers["sec-ch-ua-platform"] || undefined,
|
|
2167
2176
|
version: cache.headers["version"] || undefined,
|
|
2168
2177
|
tokenExpiresAt: exp || undefined,
|
|
2178
|
+
refreshToken: rTok,
|
|
2169
2179
|
capturedAt: Date.now(),
|
|
2170
2180
|
});
|
|
2171
2181
|
}
|
|
@@ -2384,7 +2394,7 @@ async function loginViaApi(
|
|
|
2384
2394
|
)
|
|
2385
2395
|
.catch(() => {});
|
|
2386
2396
|
await page.reload({ waitUntil: "domcontentloaded" }).catch(() => {});
|
|
2387
|
-
return { success: true };
|
|
2397
|
+
return { success: true, token, refreshToken };
|
|
2388
2398
|
}
|
|
2389
2399
|
}
|
|
2390
2400
|
|
|
@@ -2690,6 +2700,161 @@ async function loginViaUi(
|
|
|
2690
2700
|
}
|
|
2691
2701
|
}
|
|
2692
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
|
+
|
|
2693
2858
|
// ─── Header Capture ───────────────────────────────────────────────────────────
|
|
2694
2859
|
|
|
2695
2860
|
/**
|
|
@@ -3410,17 +3575,10 @@ async function refreshHeadersInternal(
|
|
|
3410
3575
|
};
|
|
3411
3576
|
|
|
3412
3577
|
if (forceReauth) {
|
|
3413
|
-
//
|
|
3414
|
-
const
|
|
3415
|
-
if (
|
|
3416
|
-
|
|
3417
|
-
if (liveCookies.some((c) => c.name === "token")) {
|
|
3418
|
-
const cookieStr = liveCookies.map((c) => `${c.name}=${c.value}`).join("; ");
|
|
3419
|
-
cache.headers.cookie = cookieStr;
|
|
3420
|
-
cache.lastRefresh = Date.now();
|
|
3421
|
-
markAccountHeadersReady(accountId);
|
|
3422
|
-
return;
|
|
3423
|
-
}
|
|
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;
|
|
3424
3582
|
}
|
|
3425
3583
|
await executeReauth();
|
|
3426
3584
|
} else {
|
|
@@ -4019,53 +4177,17 @@ export async function keepAlivePlaywrightAccount(
|
|
|
4019
4177
|
const now = Date.now();
|
|
4020
4178
|
const currentUrl = page.url();
|
|
4021
4179
|
|
|
4022
|
-
// Proactive session check: if token expires within
|
|
4180
|
+
// Proactive session check: if token expires within 5 minutes, refresh in-page silently using refreshAccountToken
|
|
4023
4181
|
const cookie = await getCookies(accountId);
|
|
4024
|
-
if (cookie && isTokenExpiringSoon(cookie,
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
method: "GET",
|
|
4034
|
-
credentials: "include",
|
|
4035
|
-
}).catch(() => {});
|
|
4036
|
-
})
|
|
4037
|
-
.catch(() => {});
|
|
4038
|
-
|
|
4039
|
-
const liveCookies = await page.context().cookies();
|
|
4040
|
-
const tokenCookie = liveCookies.find((c) => c.name === "token");
|
|
4041
|
-
if (tokenCookie) {
|
|
4042
|
-
const cookieStr = liveCookies.map((c) => `${c.name}=${c.value}`).join("; ");
|
|
4043
|
-
const cache = getHeaderCache(accountId);
|
|
4044
|
-
cache.headers.cookie = cookieStr;
|
|
4045
|
-
cache.lastRefresh = Date.now();
|
|
4046
|
-
markAccountHeadersReady(accountId);
|
|
4047
|
-
lastKeepAliveNavigation.set(accountId, now);
|
|
4048
|
-
touchAccountActivity(accountId);
|
|
4049
|
-
return true;
|
|
4050
|
-
}
|
|
4051
|
-
} catch {}
|
|
4052
|
-
|
|
4053
|
-
// If token refresh probe didn't restore cookies and the page is genuinely logged out:
|
|
4054
|
-
const loggedIn = await isPageLoggedIn(page, 2000).catch(() => false);
|
|
4055
|
-
if (!loggedIn) {
|
|
4056
|
-
console.log(
|
|
4057
|
-
`💓 [SessionKeeper] Account ${accountId} session expired; renewing session...`,
|
|
4058
|
-
);
|
|
4059
|
-
try {
|
|
4060
|
-
await refreshHeadersInternal(accountId, config.timeouts.headers, true);
|
|
4061
|
-
lastKeepAliveNavigation.set(accountId, now);
|
|
4062
|
-
touchAccountActivity(accountId);
|
|
4063
|
-
return true;
|
|
4064
|
-
} catch (err) {
|
|
4065
|
-
console.warn(
|
|
4066
|
-
`[SessionKeeper] Proactive session renewal failed for ${accountId}: ${getErrorMessage(err)}`,
|
|
4067
|
-
);
|
|
4068
|
-
}
|
|
4182
|
+
if (cookie && isTokenExpiringSoon(cookie, 5)) {
|
|
4183
|
+
console.log(
|
|
4184
|
+
`💓 [SessionKeeper] Account ${accountId} access token expiring soon (<5m); silently refreshing...`,
|
|
4185
|
+
);
|
|
4186
|
+
const res = await refreshAccountToken(accountId);
|
|
4187
|
+
if (res.success) {
|
|
4188
|
+
lastKeepAliveNavigation.set(accountId, now);
|
|
4189
|
+
touchAccountActivity(accountId);
|
|
4190
|
+
return true;
|
|
4069
4191
|
}
|
|
4070
4192
|
}
|
|
4071
4193
|
|
|
@@ -71,12 +71,25 @@ async function createQwenChatSession(
|
|
|
71
71
|
} catch {}
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
|
|
75
|
-
json?.data?.code === "
|
|
76
|
-
json?.code === "
|
|
74
|
+
const isUnauthorized =
|
|
75
|
+
json?.data?.code?.toLowerCase() === "unauthorized" ||
|
|
76
|
+
json?.code?.toLowerCase() === "unauthorized" ||
|
|
77
77
|
rawLower.includes("unauthorized") ||
|
|
78
|
-
rawLower.includes("permission to access")
|
|
79
|
-
|
|
78
|
+
rawLower.includes("permission to access") ||
|
|
79
|
+
rawLower.includes("token has expired") ||
|
|
80
|
+
rawLower.includes("401");
|
|
81
|
+
|
|
82
|
+
if (isUnauthorized) {
|
|
83
|
+
if (accountId) {
|
|
84
|
+
try {
|
|
85
|
+
const { refreshAccountToken } = await import("./playwright.ts");
|
|
86
|
+
const refreshed = await refreshAccountToken(accountId);
|
|
87
|
+
if (refreshed.success) {
|
|
88
|
+
const { headers: freshHeaders } = await getQwenHeaders(false, accountId);
|
|
89
|
+
return createQwenChatSession(freshHeaders, model, accountId, chatMode);
|
|
90
|
+
}
|
|
91
|
+
} catch {}
|
|
92
|
+
}
|
|
80
93
|
throw new QwenUpstreamError(
|
|
81
94
|
`Qwen create chat unauthorized: ${raw.substring(0, 300)}`,
|
|
82
95
|
"Unauthorized",
|
|
@@ -79,7 +79,8 @@ export function buildQwenRequestHeaders(
|
|
|
79
79
|
"bx-v": opts.bxV || "2.5.37",
|
|
80
80
|
source: "web",
|
|
81
81
|
version: opts.version || getQwenWebVersion(),
|
|
82
|
-
timezone: opts.extra?.timezone || new Date().toString().split(" (")[0],
|
|
82
|
+
timezone: opts.extra?.timezone || opts.extra?.Timezone || new Date().toString().split(" (")[0],
|
|
83
|
+
Timezone: opts.extra?.Timezone || opts.extra?.timezone || new Date().toString().split(" (")[0],
|
|
83
84
|
// Use the real browser client-hints when captured (anti-hardcoded); fall
|
|
84
85
|
// back to the static fingerprint otherwise.
|
|
85
86
|
"sec-ch-ua":
|
package/src/services/qwen.ts
CHANGED
|
@@ -929,16 +929,28 @@ export async function requestQwenTextInBrowser(
|
|
|
929
929
|
// If 401 Unauthorized in browser, try silent in-page token refresh before giving up
|
|
930
930
|
if (response.status === 401) {
|
|
931
931
|
try {
|
|
932
|
-
const
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
932
|
+
const rTok = localStorage.getItem("refresh_token");
|
|
933
|
+
if (rTok) {
|
|
934
|
+
const refreshRes = await fetch("https://auth.qwen.ai/api/v2/auths/refresh", {
|
|
935
|
+
method: "GET",
|
|
936
|
+
credentials: "include",
|
|
937
|
+
headers: {
|
|
938
|
+
accept: "application/json, text/plain, */*",
|
|
939
|
+
source: "web",
|
|
940
|
+
version: "0.3.11",
|
|
941
|
+
"x-request-origin": "https://chat.qwen.ai",
|
|
942
|
+
timezone: new Date().toString().split(" (")[0],
|
|
943
|
+
authorization: `Bearer ${rTok}`,
|
|
944
|
+
},
|
|
945
|
+
signal: AbortSignal.timeout(5000),
|
|
946
|
+
});
|
|
938
947
|
const refreshJson: any = await refreshRes.json().catch(() => null);
|
|
939
|
-
|
|
940
|
-
|
|
948
|
+
const freshTok = refreshJson?.data?.access_token || refreshJson?.data?.token;
|
|
949
|
+
const nextRTok = refreshJson?.data?.refresh_token;
|
|
950
|
+
if (refreshJson?.success === true && freshTok) {
|
|
941
951
|
localStorage.setItem("token", freshTok);
|
|
952
|
+
localStorage.setItem("access_token", freshTok);
|
|
953
|
+
if (nextRTok) localStorage.setItem("refresh_token", nextRTok);
|
|
942
954
|
document.cookie = `token=${encodeURIComponent(freshTok)}; path=/; domain=.qwen.ai; max-age=31536000`;
|
|
943
955
|
if (headers["authorization"] || headers["Authorization"]) {
|
|
944
956
|
headers["authorization"] = `Bearer ${freshTok}`;
|
|
@@ -979,7 +991,7 @@ export async function requestQwenTextInBrowser(
|
|
|
979
991
|
// Settings and personalization requests run as same-origin in-browser fetch
|
|
980
992
|
// with appropriate Referer, keeping the page on the stable chat UI without
|
|
981
993
|
// expensive page.goto navigations that can time out under load.
|
|
982
|
-
|
|
994
|
+
let response = await withQwenBrowserPage<BrowserTextResponse>(
|
|
983
995
|
accountId,
|
|
984
996
|
evaluateRequest,
|
|
985
997
|
undefined,
|
|
@@ -987,6 +999,23 @@ export async function requestQwenTextInBrowser(
|
|
|
987
999
|
recoverOnTimeout,
|
|
988
1000
|
);
|
|
989
1001
|
|
|
1002
|
+
// If evaluateRequest still received 401, trigger outer token refresh and retry once
|
|
1003
|
+
if (response.status === 401 && accountId) {
|
|
1004
|
+
try {
|
|
1005
|
+
const { refreshAccountToken } = await import("./playwright.ts");
|
|
1006
|
+
const refreshed = await refreshAccountToken(accountId);
|
|
1007
|
+
if (refreshed.success) {
|
|
1008
|
+
response = await withQwenBrowserPage<BrowserTextResponse>(
|
|
1009
|
+
accountId,
|
|
1010
|
+
evaluateRequest,
|
|
1011
|
+
undefined,
|
|
1012
|
+
options.timeoutMs,
|
|
1013
|
+
recoverOnTimeout,
|
|
1014
|
+
);
|
|
1015
|
+
}
|
|
1016
|
+
} catch {}
|
|
1017
|
+
}
|
|
1018
|
+
|
|
990
1019
|
return new Response(response.raw, {
|
|
991
1020
|
status: response.status,
|
|
992
1021
|
headers: response.contentType
|
|
@@ -1714,7 +1743,11 @@ export async function syncQwenRequestPersonalization(
|
|
|
1714
1743
|
);
|
|
1715
1744
|
try {
|
|
1716
1745
|
currentSettings = null;
|
|
1717
|
-
const {
|
|
1746
|
+
const { refreshAccountToken } = await import("./playwright.ts");
|
|
1747
|
+
if (accountId) {
|
|
1748
|
+
await refreshAccountToken(accountId);
|
|
1749
|
+
}
|
|
1750
|
+
const { headers: freshHeaders } = await getQwenHeaders(false, accountId);
|
|
1718
1751
|
requestHeaders = buildCapturedQwenHeaders(freshHeaders, {
|
|
1719
1752
|
referer: qwenUrl("/settings/personalization"),
|
|
1720
1753
|
});
|
|
@@ -2730,10 +2763,19 @@ async function createQwenStreamInternal(
|
|
|
2730
2763
|
};
|
|
2731
2764
|
|
|
2732
2765
|
// A new logical chat session should reuse the warmed header cache when available.
|
|
2733
|
-
//
|
|
2734
|
-
|
|
2766
|
+
// Proactively check if the access token is expiring soon (<3m) and refresh quietly before starting.
|
|
2767
|
+
let forceRefresh = options?.forceNewChat === true;
|
|
2768
|
+
if (!forceRefresh && accountId) {
|
|
2769
|
+
try {
|
|
2770
|
+
const basic = await getBasicHeaders(accountId);
|
|
2771
|
+
if (isTokenExpiringSoon(basic.cookie, 3)) {
|
|
2772
|
+
forceRefresh = true;
|
|
2773
|
+
}
|
|
2774
|
+
} catch {}
|
|
2775
|
+
}
|
|
2776
|
+
|
|
2735
2777
|
const captured = await getQwenHeaders(
|
|
2736
|
-
|
|
2778
|
+
forceRefresh,
|
|
2737
2779
|
accountId,
|
|
2738
2780
|
);
|
|
2739
2781
|
ensureNotAborted();
|
|
@@ -2749,7 +2791,7 @@ async function createQwenStreamInternal(
|
|
|
2749
2791
|
if (options && "chatSessionId" in options) {
|
|
2750
2792
|
if (options.chatSessionId === null || options.chatSessionId === "") {
|
|
2751
2793
|
const acquired = await acquireNewQwenChatSession(
|
|
2752
|
-
|
|
2794
|
+
activeHeaders,
|
|
2753
2795
|
model,
|
|
2754
2796
|
accountId,
|
|
2755
2797
|
options?.chatMode ?? "thread",
|
|
@@ -2757,6 +2799,10 @@ async function createQwenStreamInternal(
|
|
|
2757
2799
|
chatSessionId = acquired.chatId;
|
|
2758
2800
|
leasedWarmChat = acquired.leasedFromPool;
|
|
2759
2801
|
createdNewChat = true;
|
|
2802
|
+
try {
|
|
2803
|
+
const fresh = await getQwenHeaders(false, accountId);
|
|
2804
|
+
activeHeaders = fresh.headers;
|
|
2805
|
+
} catch {}
|
|
2760
2806
|
} else {
|
|
2761
2807
|
chatSessionId = options.chatSessionId;
|
|
2762
2808
|
}
|
|
@@ -2764,7 +2810,7 @@ async function createQwenStreamInternal(
|
|
|
2764
2810
|
chatSessionId = captured.chatSessionId;
|
|
2765
2811
|
if (!chatSessionId) {
|
|
2766
2812
|
const acquired = await acquireNewQwenChatSession(
|
|
2767
|
-
|
|
2813
|
+
activeHeaders,
|
|
2768
2814
|
model,
|
|
2769
2815
|
accountId,
|
|
2770
2816
|
options?.chatMode ?? "thread",
|
|
@@ -2772,6 +2818,10 @@ async function createQwenStreamInternal(
|
|
|
2772
2818
|
chatSessionId = acquired.chatId;
|
|
2773
2819
|
leasedWarmChat = acquired.leasedFromPool;
|
|
2774
2820
|
createdNewChat = true;
|
|
2821
|
+
try {
|
|
2822
|
+
const fresh = await getQwenHeaders(false, accountId);
|
|
2823
|
+
activeHeaders = fresh.headers;
|
|
2824
|
+
} catch {}
|
|
2775
2825
|
}
|
|
2776
2826
|
}
|
|
2777
2827
|
|