qwenproxy-cli 1.4.7 → 1.4.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/clean-cache.js +17 -5
- package/dist/delete-chats.js +213 -69
- package/dist/index.js +215 -71
- package/dist/login.js +161 -59
- package/dist/reset-cooldowns.js +17 -5
- package/package.json +1 -1
- package/src/core/database.ts +10 -5
- package/src/core/version.ts +1 -1
- package/src/routes/chat/retry-policy.ts +4 -3
- package/src/services/playwright.ts +180 -58
- package/src/services/qwen-chat-pool.ts +9 -0
- package/src/services/qwen.ts +43 -10
|
@@ -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
|
|
|
@@ -77,6 +77,15 @@ async function createQwenChatSession(
|
|
|
77
77
|
rawLower.includes("unauthorized") ||
|
|
78
78
|
rawLower.includes("permission to access")
|
|
79
79
|
) {
|
|
80
|
+
if (accountId) {
|
|
81
|
+
try {
|
|
82
|
+
const { refreshAccountToken } = await import("./playwright.ts");
|
|
83
|
+
const refreshed = await refreshAccountToken(accountId);
|
|
84
|
+
if (refreshed.success) {
|
|
85
|
+
return createQwenChatSession(headers, model, accountId, chatMode);
|
|
86
|
+
}
|
|
87
|
+
} catch {}
|
|
88
|
+
}
|
|
80
89
|
throw new QwenUpstreamError(
|
|
81
90
|
`Qwen create chat unauthorized: ${raw.substring(0, 300)}`,
|
|
82
91
|
"Unauthorized",
|
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
|
});
|