qwenproxy-cli 1.3.2 → 1.3.4
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/core/config.ts +1 -1
- package/src/services/playwright.ts +81 -18
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qwenproxy-cli",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.4",
|
|
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": {
|
package/src/core/config.ts
CHANGED
|
@@ -80,7 +80,7 @@ const envSchema = z
|
|
|
80
80
|
CHAT_TIMEOUT: z.string().default("180000"),
|
|
81
81
|
NAVIGATION_TIMEOUT: z.string().default("60000"),
|
|
82
82
|
PAGE_TIMEOUT: z.string().default("60000"),
|
|
83
|
-
HEADERS_TIMEOUT: z.string().default("
|
|
83
|
+
HEADERS_TIMEOUT: z.string().default("120000"),
|
|
84
84
|
TIME_TO_FIRST_BYTE: z.string().default("60000"),
|
|
85
85
|
IDLE_STREAM_TIMEOUT: z.string().default("60000"),
|
|
86
86
|
// Deadline for the FIRST upstream chunk on thinking models (the reasoning
|
|
@@ -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
|
|
|
@@ -1961,14 +1962,45 @@ async function loginViaApi(
|
|
|
1961
1962
|
}
|
|
1962
1963
|
|
|
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
|
+
|
|
1964
1983
|
await page
|
|
1965
1984
|
.goto(qwenUrl("/"), {
|
|
1966
1985
|
waitUntil: "domcontentloaded",
|
|
1967
1986
|
timeout: config.timeouts.navigation,
|
|
1968
1987
|
})
|
|
1969
1988
|
.catch(() => {});
|
|
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
|
+
|
|
1970
2002
|
await sleep(1500);
|
|
1971
|
-
const loggedIn = await isPageLoggedIn(page,
|
|
2003
|
+
const loggedIn = await isPageLoggedIn(page, 5_000);
|
|
1972
2004
|
if (loggedIn) {
|
|
1973
2005
|
return { success: true };
|
|
1974
2006
|
}
|
|
@@ -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(
|
|
@@ -2486,6 +2529,16 @@ export async function captureQwenHeaders(
|
|
|
2486
2529
|
await page.focus(inputSelector, { timeout: inputActionTimeoutMs });
|
|
2487
2530
|
if (settled || page.isClosed()) return;
|
|
2488
2531
|
await page.fill(inputSelector, "a", { timeout: inputActionTimeoutMs });
|
|
2532
|
+
// Dispatch native React input events and keyboard stroke to guarantee React updates state
|
|
2533
|
+
if (typeof page.keyboard?.press === "function") {
|
|
2534
|
+
await page.keyboard.press("End").catch(() => {});
|
|
2535
|
+
}
|
|
2536
|
+
if (typeof (page.keyboard as any)?.type === "function") {
|
|
2537
|
+
await (page.keyboard as any).type(" ").catch(() => {});
|
|
2538
|
+
}
|
|
2539
|
+
if (typeof page.keyboard?.press === "function") {
|
|
2540
|
+
await page.keyboard.press("Backspace").catch(() => {});
|
|
2541
|
+
}
|
|
2489
2542
|
if (typeof page.evaluate === "function") {
|
|
2490
2543
|
await page.evaluate((sel) => {
|
|
2491
2544
|
const el = document.querySelector(sel) as HTMLTextAreaElement | null;
|
|
@@ -2527,7 +2580,18 @@ export async function captureQwenHeaders(
|
|
|
2527
2580
|
return;
|
|
2528
2581
|
}
|
|
2529
2582
|
if (settled || page.isClosed()) return;
|
|
2530
|
-
|
|
2583
|
+
|
|
2584
|
+
// Wait up to 3s for React to enable the send button after typing
|
|
2585
|
+
try {
|
|
2586
|
+
await page.waitForFunction(() => {
|
|
2587
|
+
const btn = document.querySelector(
|
|
2588
|
+
".message-input-right-button-send .send-button, .chat-prompt-send-button, button.send-button, button[aria-label*='Send' i], button[aria-label*='Enviar' i], .send-button-container button"
|
|
2589
|
+
) as HTMLButtonElement | null;
|
|
2590
|
+
return btn && !btn.disabled && !btn.classList.contains("disabled") && btn.getAttribute("aria-disabled") !== "true";
|
|
2591
|
+
}, { timeout: 3000 });
|
|
2592
|
+
} catch {
|
|
2593
|
+
await sleep(1000);
|
|
2594
|
+
}
|
|
2531
2595
|
if (settled || page.isClosed()) return;
|
|
2532
2596
|
|
|
2533
2597
|
const sendSelectors = [
|
|
@@ -2609,14 +2673,13 @@ export async function captureQwenHeaders(
|
|
|
2609
2673
|
// A missing chat input is the exception: the page never rendered the
|
|
2610
2674
|
// chat UI, so there is no warm SDK state to protect and only a fresh
|
|
2611
2675
|
// load can recover it.
|
|
2612
|
-
|
|
2613
|
-
attempt === 1 ||
|
|
2676
|
+
const needReload =
|
|
2614
2677
|
lastAttemptInputMissing ||
|
|
2615
|
-
(lastAttemptGraceTimedOut && attempt >= 3)
|
|
2616
|
-
) {
|
|
2678
|
+
(lastAttemptGraceTimedOut && attempt >= 3);
|
|
2679
|
+
if (attempt === 1 || needReload) {
|
|
2617
2680
|
lastAttemptGraceTimedOut = false;
|
|
2618
2681
|
lastAttemptInputMissing = false;
|
|
2619
|
-
await openChatPage();
|
|
2682
|
+
await openChatPage(needReload);
|
|
2620
2683
|
}
|
|
2621
2684
|
if (settled) return;
|
|
2622
2685
|
await triggerSend(attempt);
|