qwenproxy-cli 1.3.1 → 1.3.3
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/package.json +1 -1
- package/src/services/playwright.ts +130 -88
- package/src/services/qwen.ts +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qwenproxy-cli",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.3",
|
|
4
4
|
"description": "High-performance OpenAI & Anthropic compatible API gateway for Qwen with multi-account rotation, interactive TUI, and resilient tool calling.",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"bin": {
|
|
@@ -510,7 +510,7 @@ const HEADER_CAPTURE_TRIGGER_GRACE_MS = 15_000;
|
|
|
510
510
|
* send. Fail it fast and let the retry loop reload + re-send against the warm
|
|
511
511
|
* SDK instead of stalling the boot for the full 15s.
|
|
512
512
|
*/
|
|
513
|
-
const FIRST_TRIGGER_GRACE_MS =
|
|
513
|
+
const FIRST_TRIGGER_GRACE_MS = 8_000;
|
|
514
514
|
/**
|
|
515
515
|
* Sends (the initial one plus re-triggers) header capture may spend on getting a
|
|
516
516
|
* completion request that actually carries the bx headers. The in-page SDK can
|
|
@@ -1535,6 +1535,7 @@ export async function initPlaywrightForAccount(
|
|
|
1535
1535
|
throw validationError;
|
|
1536
1536
|
}
|
|
1537
1537
|
if (!options.skipHeaderCapture) {
|
|
1538
|
+
(acctPage as any).__qwenChatHomeLoaded = true;
|
|
1538
1539
|
await captureQwenHeaders(account.id);
|
|
1539
1540
|
}
|
|
1540
1541
|
|
|
@@ -1889,94 +1890,125 @@ async function loginViaApi(
|
|
|
1889
1890
|
.digest("hex");
|
|
1890
1891
|
const signinUrl = qwenUrl("/api/v2/auths/signin");
|
|
1891
1892
|
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
}
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1893
|
+
let signinSuccess = false;
|
|
1894
|
+
let data: any = null;
|
|
1895
|
+
|
|
1896
|
+
if (page.request && typeof page.request.post === "function") {
|
|
1897
|
+
try {
|
|
1898
|
+
const response = await page.request.post(signinUrl, {
|
|
1899
|
+
data: {
|
|
1900
|
+
email,
|
|
1901
|
+
password: hashedPassword,
|
|
1902
|
+
login_type: "email",
|
|
1903
|
+
},
|
|
1904
|
+
headers: {
|
|
1905
|
+
"content-type": "application/json",
|
|
1906
|
+
accept: "application/json, text/plain, */*",
|
|
1907
|
+
referer: qwenUrl("/auth"),
|
|
1908
|
+
origin: qwenOrigin(),
|
|
1909
|
+
},
|
|
1910
|
+
timeout: 10_000,
|
|
1911
|
+
});
|
|
1912
|
+
data = await response.json().catch(() => null);
|
|
1913
|
+
signinSuccess = Boolean(data && (data.success === true || data.token));
|
|
1914
|
+
} catch {}
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
if (!signinSuccess && !data) {
|
|
1918
|
+
// Fallback to in-page evaluate fetch if page.request was unavailable
|
|
1919
|
+
const requestId = crypto.randomUUID();
|
|
1920
|
+
const evalRes = await page
|
|
1921
|
+
.evaluate(
|
|
1922
|
+
async ({ email, password, signinUrl, requestId }) => {
|
|
1912
1923
|
try {
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
+
const response = await fetch(signinUrl, {
|
|
1925
|
+
method: "POST",
|
|
1926
|
+
signal: AbortSignal.timeout(10_000),
|
|
1927
|
+
credentials: "include",
|
|
1928
|
+
headers: {
|
|
1929
|
+
accept: "application/json, text/plain, */*",
|
|
1930
|
+
"content-type": "application/json",
|
|
1931
|
+
source: "web",
|
|
1932
|
+
timezone: new Date().toString().split(" (")[0],
|
|
1933
|
+
"x-request-id": requestId,
|
|
1934
|
+
},
|
|
1935
|
+
body: JSON.stringify({ email, password, login_type: "email" }),
|
|
1936
|
+
});
|
|
1937
|
+
const json = await response.json().catch(() => null);
|
|
1938
|
+
return { ok: response.ok, status: response.status, data: json };
|
|
1939
|
+
} catch (e: any) {
|
|
1940
|
+
return { ok: false, error: e.message };
|
|
1941
|
+
}
|
|
1942
|
+
},
|
|
1943
|
+
{ email, password: hashedPassword, signinUrl, requestId },
|
|
1944
|
+
)
|
|
1945
|
+
.catch(() => null);
|
|
1924
1946
|
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
.goto(qwenUrl("/"), {
|
|
1929
|
-
waitUntil: "domcontentloaded",
|
|
1930
|
-
timeout: config.timeouts.navigation,
|
|
1931
|
-
})
|
|
1932
|
-
.catch(() => {});
|
|
1933
|
-
await sleep(1000);
|
|
1934
|
-
const url = page.url();
|
|
1935
|
-
const isLogged = !(url.includes("/auth") || url.includes("/login"));
|
|
1936
|
-
if (isLogged) {
|
|
1937
|
-
return { success: true };
|
|
1938
|
-
}
|
|
1939
|
-
const loggedIn = await isPageLoggedIn(page);
|
|
1940
|
-
if (loggedIn) {
|
|
1941
|
-
return { success: true };
|
|
1942
|
-
}
|
|
1943
|
-
} else if (result.data.success === false) {
|
|
1944
|
-
const code = result.data?.data?.code || result.data?.code;
|
|
1945
|
-
const details =
|
|
1946
|
-
result.data?.data?.details ||
|
|
1947
|
-
result.data?.details ||
|
|
1948
|
-
result.data?.message;
|
|
1949
|
-
const classified = classifyQwenAuthError(code, details);
|
|
1950
|
-
return {
|
|
1951
|
-
success: false,
|
|
1952
|
-
permanentFailure: classified.isPermanent,
|
|
1953
|
-
reason: classified.reason,
|
|
1954
|
-
};
|
|
1947
|
+
if (evalRes?.data) {
|
|
1948
|
+
data = evalRes.data;
|
|
1949
|
+
signinSuccess = Boolean(data && (data.success === true || data.token));
|
|
1955
1950
|
}
|
|
1956
1951
|
}
|
|
1957
1952
|
|
|
1958
|
-
if (
|
|
1953
|
+
if (data?.success === false) {
|
|
1954
|
+
const code = data?.data?.code || data?.code;
|
|
1955
|
+
const details = data?.data?.details || data?.details || data?.message;
|
|
1956
|
+
const classified = classifyQwenAuthError(code, details);
|
|
1957
|
+
return {
|
|
1958
|
+
success: false,
|
|
1959
|
+
permanentFailure: classified.isPermanent,
|
|
1960
|
+
reason: classified.reason,
|
|
1961
|
+
};
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
if (signinSuccess) {
|
|
1965
|
+
const token = data?.data?.token || data?.token;
|
|
1966
|
+
if (token) {
|
|
1967
|
+
try {
|
|
1968
|
+
await page.context().addCookies([
|
|
1969
|
+
{
|
|
1970
|
+
name: "token",
|
|
1971
|
+
value: token,
|
|
1972
|
+
domain: ".qwen.ai",
|
|
1973
|
+
path: "/",
|
|
1974
|
+
expires: Math.floor(Date.now() / 1000) + 31536000,
|
|
1975
|
+
httpOnly: false,
|
|
1976
|
+
secure: true,
|
|
1977
|
+
sameSite: "Lax",
|
|
1978
|
+
},
|
|
1979
|
+
]);
|
|
1980
|
+
} catch {}
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1959
1983
|
await page
|
|
1960
1984
|
.goto(qwenUrl("/"), {
|
|
1961
1985
|
waitUntil: "domcontentloaded",
|
|
1962
1986
|
timeout: config.timeouts.navigation,
|
|
1963
1987
|
})
|
|
1964
1988
|
.catch(() => {});
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1989
|
+
|
|
1990
|
+
if (token) {
|
|
1991
|
+
await page
|
|
1992
|
+
.evaluate((tok) => {
|
|
1993
|
+
try {
|
|
1994
|
+
localStorage.setItem("token", tok);
|
|
1995
|
+
document.cookie = `token=${encodeURIComponent(tok)}; path=/; domain=.qwen.ai; max-age=31536000`;
|
|
1996
|
+
} catch {}
|
|
1997
|
+
}, token)
|
|
1998
|
+
.catch(() => {});
|
|
1999
|
+
await page.reload({ waitUntil: "domcontentloaded" }).catch(() => {});
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
await sleep(1500);
|
|
2003
|
+
const loggedIn = await isPageLoggedIn(page, 5_000);
|
|
2004
|
+
if (loggedIn) {
|
|
1969
2005
|
return { success: true };
|
|
1970
2006
|
}
|
|
1971
|
-
const loggedIn = await isPageLoggedIn(page);
|
|
1972
|
-
return {
|
|
1973
|
-
success: loggedIn,
|
|
1974
|
-
reason: loggedIn ? undefined : "Sessão não autenticada após signin",
|
|
1975
|
-
};
|
|
1976
2007
|
}
|
|
2008
|
+
|
|
1977
2009
|
return {
|
|
1978
2010
|
success: false,
|
|
1979
|
-
reason:
|
|
2011
|
+
reason: "API signin não confirmou sessão autenticada",
|
|
1980
2012
|
};
|
|
1981
2013
|
} catch (err: any) {
|
|
1982
2014
|
const errMsg = err?.message || String(err);
|
|
@@ -2385,8 +2417,13 @@ export async function captureQwenHeaders(
|
|
|
2385
2417
|
// Navigate to the stable chat page. Only the first attempt pays for this:
|
|
2386
2418
|
// a re-trigger types into the page that is already loaded, and reloading
|
|
2387
2419
|
// would throw away the bx SDK state that just finished warming up.
|
|
2388
|
-
const openChatPage = async () => {
|
|
2420
|
+
const openChatPage = async (forceReload = false) => {
|
|
2389
2421
|
if (settled || page.isClosed()) return;
|
|
2422
|
+
if (!forceReload && (page as any).__qwenChatHomeLoaded) {
|
|
2423
|
+
delete (page as any).__qwenChatHomeLoaded;
|
|
2424
|
+
await sleep(500);
|
|
2425
|
+
return;
|
|
2426
|
+
}
|
|
2390
2427
|
await page.goto(qwenUrl("/"), {
|
|
2391
2428
|
waitUntil: "domcontentloaded",
|
|
2392
2429
|
timeout: Math.min(config.timeouts.navigation, timeoutMs),
|
|
@@ -2401,28 +2438,33 @@ export async function captureQwenHeaders(
|
|
|
2401
2438
|
await clearVisibleChallenge(page);
|
|
2402
2439
|
if (settled || page.isClosed()) return;
|
|
2403
2440
|
|
|
2404
|
-
// Session-expiry
|
|
2405
|
-
//
|
|
2406
|
-
//
|
|
2407
|
-
//
|
|
2408
|
-
//
|
|
2441
|
+
// Session-expiry check: if the page redirected to /auth or /login,
|
|
2442
|
+
// re-login immediately.
|
|
2443
|
+
// On attempts 1 and 2, do NOT run a tight 3s fetch probe because the session
|
|
2444
|
+
// was already verified during init. Only run isPageLoggedIn if attempt >= 3
|
|
2445
|
+
// (as a last-resort recovery before giving up) and with a generous 8s timeout.
|
|
2409
2446
|
const currentUrl = page.url();
|
|
2410
2447
|
const isAuthUrl = currentUrl.includes("/auth") || currentUrl.includes("/login");
|
|
2411
|
-
const
|
|
2412
|
-
if (
|
|
2448
|
+
const isSuspectedGuest = attempt >= 3 && !(await isPageLoggedIn(page, 8000));
|
|
2449
|
+
if (isAuthUrl || isSuspectedGuest) {
|
|
2413
2450
|
const { getAccountCredentials } = await import("../core/accounts.ts");
|
|
2414
2451
|
const creds = getAccountCredentials(accountId);
|
|
2415
2452
|
if (creds && creds.email && creds.password) {
|
|
2416
2453
|
console.warn(
|
|
2417
2454
|
`⚠️ [Playwright] Session expired or guest state detected during header capture for ${accountId}; re-authenticating...`,
|
|
2418
2455
|
);
|
|
2456
|
+
armOverallDeadline();
|
|
2419
2457
|
const ok = await loginToQwen(accountId, creds.email, creds.password);
|
|
2420
2458
|
if (ok) {
|
|
2421
2459
|
// Re-login navigated; load the chat page and wait for hydration so
|
|
2422
2460
|
// the check below probes a live authenticated chat page.
|
|
2423
|
-
await
|
|
2461
|
+
await page.goto(qwenUrl("/"), {
|
|
2462
|
+
waitUntil: "domcontentloaded",
|
|
2463
|
+
timeout: Math.min(config.timeouts.navigation, timeoutMs),
|
|
2464
|
+
});
|
|
2465
|
+
await sleep(2000);
|
|
2424
2466
|
}
|
|
2425
|
-
if (!ok || !(await isPageLoggedIn(page,
|
|
2467
|
+
if (!ok || !(await isPageLoggedIn(page, 6000))) {
|
|
2426
2468
|
settle(
|
|
2427
2469
|
new Error(
|
|
2428
2470
|
`Header capture failed for ${accountId}: re-login after session expiry did not succeed`,
|
|
@@ -2430,6 +2472,7 @@ export async function captureQwenHeaders(
|
|
|
2430
2472
|
);
|
|
2431
2473
|
return;
|
|
2432
2474
|
}
|
|
2475
|
+
armOverallDeadline();
|
|
2433
2476
|
if (settled || page.isClosed()) return;
|
|
2434
2477
|
} else {
|
|
2435
2478
|
settle(
|
|
@@ -2609,14 +2652,13 @@ export async function captureQwenHeaders(
|
|
|
2609
2652
|
// A missing chat input is the exception: the page never rendered the
|
|
2610
2653
|
// chat UI, so there is no warm SDK state to protect and only a fresh
|
|
2611
2654
|
// load can recover it.
|
|
2612
|
-
|
|
2613
|
-
attempt === 1 ||
|
|
2655
|
+
const needReload =
|
|
2614
2656
|
lastAttemptInputMissing ||
|
|
2615
|
-
(lastAttemptGraceTimedOut && attempt >= 3)
|
|
2616
|
-
) {
|
|
2657
|
+
(lastAttemptGraceTimedOut && attempt >= 3);
|
|
2658
|
+
if (attempt === 1 || needReload) {
|
|
2617
2659
|
lastAttemptGraceTimedOut = false;
|
|
2618
2660
|
lastAttemptInputMissing = false;
|
|
2619
|
-
await openChatPage();
|
|
2661
|
+
await openChatPage(needReload);
|
|
2620
2662
|
}
|
|
2621
2663
|
if (settled) return;
|
|
2622
2664
|
await triggerSend(attempt);
|
package/src/services/qwen.ts
CHANGED
|
@@ -985,12 +985,12 @@ export async function requestQwenSettingsDirectFetch(
|
|
|
985
985
|
// passes the WAF.
|
|
986
986
|
let json: any = null;
|
|
987
987
|
let okShape = false;
|
|
988
|
-
if (contentType.includes("html")) {
|
|
988
|
+
if (contentType.includes("html") || !response.ok) {
|
|
989
989
|
okShape = false;
|
|
990
990
|
} else {
|
|
991
991
|
try {
|
|
992
992
|
json = JSON.parse(raw);
|
|
993
|
-
okShape = json && typeof json === "object" &&
|
|
993
|
+
okShape = json && typeof json === "object" && json.success === true;
|
|
994
994
|
} catch {
|
|
995
995
|
okShape = false;
|
|
996
996
|
}
|