qwenproxy-cli 1.0.17 → 1.0.19
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/api/server.ts +23 -18
- package/src/index.ts +26 -32
- package/src/services/playwright.ts +160 -94
- package/src/tools/parser.ts +1 -1
- package/src/tui/index.ts +8 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qwenproxy-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.19",
|
|
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/api/server.ts
CHANGED
|
@@ -721,27 +721,32 @@ export async function startServer(options?: {
|
|
|
721
721
|
|
|
722
722
|
// Warm reserve account in background for fast failover without delaying startup
|
|
723
723
|
if (config.playwright.maxActiveContexts > 1 && remainingAccounts.length > 0) {
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
724
|
+
let reserveCandidateIdx = 0;
|
|
725
|
+
for (; reserveCandidateIdx < remainingAccounts.length; reserveCandidateIdx++) {
|
|
726
|
+
const reserveAccount = remainingAccounts[reserveCandidateIdx];
|
|
727
|
+
try {
|
|
728
|
+
const ok = await prepareAccountRuntime(
|
|
729
|
+
reserveAccount,
|
|
730
|
+
getAccountCredentials,
|
|
731
|
+
initPlaywrightForAccount,
|
|
732
|
+
disableNativeTools,
|
|
733
|
+
warmQwenChatPool,
|
|
734
|
+
);
|
|
735
|
+
if (ok) {
|
|
736
|
+
ensureAccountInPriority(reserveAccount.id);
|
|
737
|
+
console.log(
|
|
738
|
+
`✅ [Server] Reserve account ready (2/${totalAccounts}): ${maskEmail(reserveAccount.email)}`,
|
|
739
|
+
);
|
|
740
|
+
reserveCandidateIdx++;
|
|
741
|
+
break;
|
|
742
|
+
}
|
|
743
|
+
} catch (err) {
|
|
744
|
+
console.warn(
|
|
745
|
+
`⚠️ [Server] Failed to warm reserve account ${maskEmail(reserveAccount.email)}: ${getErrorMessage(err)}`,
|
|
738
746
|
);
|
|
739
747
|
}
|
|
740
|
-
} catch (err) {
|
|
741
|
-
console.warn(
|
|
742
|
-
`⚠️ [Server] Failed to warm reserve account ${maskEmail(reserveAccount.email)}: ${getErrorMessage(err)}`,
|
|
743
|
-
);
|
|
744
748
|
}
|
|
749
|
+
accountsToValidate = remainingAccounts.slice(reserveCandidateIdx);
|
|
745
750
|
}
|
|
746
751
|
|
|
747
752
|
let validated = 0;
|
package/src/index.ts
CHANGED
|
@@ -14,41 +14,35 @@ if (fs.existsSync(envPath)) {
|
|
|
14
14
|
dotenv.config({ quiet: true })
|
|
15
15
|
}
|
|
16
16
|
// Prevent benign asynchronous driver/browser teardown exceptions from crashing the server
|
|
17
|
-
process.on('uncaughtException', (error: unknown) => {
|
|
18
|
-
const
|
|
19
|
-
if (
|
|
20
|
-
msg
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
) {
|
|
29
|
-
console.warn(`⚠️ [Playwright] Handled benign driver teardown exception: ${msg}`)
|
|
30
|
-
return
|
|
17
|
+
process.on('uncaughtException', async (error: unknown) => {
|
|
18
|
+
const { isPlaywrightAlreadyClosedError } = await import('./services/playwright.ts');
|
|
19
|
+
if (isPlaywrightAlreadyClosedError(error)) {
|
|
20
|
+
const msg =
|
|
21
|
+
error instanceof Error
|
|
22
|
+
? error.message
|
|
23
|
+
: typeof error === 'object' && error !== null && 'message' in error
|
|
24
|
+
? String((error as any).message)
|
|
25
|
+
: String(error);
|
|
26
|
+
console.warn(`⚠️ [Playwright] Handled benign driver teardown exception: ${msg}`);
|
|
27
|
+
return;
|
|
31
28
|
}
|
|
32
|
-
console.error('❌ [Process] Uncaught Exception:', error)
|
|
33
|
-
})
|
|
29
|
+
console.error('❌ [Process] Uncaught Exception:', error);
|
|
30
|
+
});
|
|
34
31
|
|
|
35
|
-
process.on('unhandledRejection', (reason: unknown) => {
|
|
36
|
-
const
|
|
37
|
-
if (
|
|
38
|
-
msg
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
) {
|
|
47
|
-
console.warn(`⚠️ [Playwright] Handled benign driver teardown rejection: ${msg}`)
|
|
48
|
-
return
|
|
32
|
+
process.on('unhandledRejection', async (reason: unknown) => {
|
|
33
|
+
const { isPlaywrightAlreadyClosedError } = await import('./services/playwright.ts');
|
|
34
|
+
if (isPlaywrightAlreadyClosedError(reason)) {
|
|
35
|
+
const msg =
|
|
36
|
+
reason instanceof Error
|
|
37
|
+
? reason.message
|
|
38
|
+
: typeof reason === 'object' && reason !== null && 'message' in reason
|
|
39
|
+
? String((reason as any).message)
|
|
40
|
+
: String(reason);
|
|
41
|
+
console.warn(`⚠️ [Playwright] Handled benign driver teardown rejection: ${msg}`);
|
|
42
|
+
return;
|
|
49
43
|
}
|
|
50
|
-
console.error('❌ [Process] Unhandled Rejection:', reason)
|
|
51
|
-
})
|
|
44
|
+
console.error('❌ [Process] Unhandled Rejection:', reason);
|
|
45
|
+
});
|
|
52
46
|
import { startServer } from './api/server.js'
|
|
53
47
|
const isTui = process.argv.includes('--tui') || process.env.QWEN_TUI === 'true'
|
|
54
48
|
|
|
@@ -279,9 +279,11 @@ export async function saveStorageState(
|
|
|
279
279
|
`storageState timed out after ${timeoutMs}ms`,
|
|
280
280
|
);
|
|
281
281
|
} catch (error) {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
282
|
+
if (!isPlaywrightAlreadyClosedError(error)) {
|
|
283
|
+
console.warn(
|
|
284
|
+
`[Playwright] Failed to save storage state for ${accountId}: ${getErrorMessage(error)}`,
|
|
285
|
+
);
|
|
286
|
+
}
|
|
285
287
|
}
|
|
286
288
|
}
|
|
287
289
|
|
|
@@ -302,6 +304,37 @@ async function hasValidAuthCookie(context: BrowserContext, timeoutMs = 3_000): P
|
|
|
302
304
|
}
|
|
303
305
|
}
|
|
304
306
|
|
|
307
|
+
/**
|
|
308
|
+
* Detect whether the page is authenticated.
|
|
309
|
+
* Probes the authoritative /api/v1/auths/ endpoint from the page context (verified via HAR forensics)
|
|
310
|
+
* and falls back to inspecting the presence of visible "Log in" / "Sign up" buttons.
|
|
311
|
+
*/
|
|
312
|
+
export async function isPageLoggedIn(page: Page): Promise<boolean> {
|
|
313
|
+
if (!page) return false;
|
|
314
|
+
if (typeof page.isClosed === "function" && page.isClosed()) return false;
|
|
315
|
+
try {
|
|
316
|
+
const url = typeof page.url === "function" ? page.url() : "";
|
|
317
|
+
if (url.includes("/auth") || url.includes("/login")) return false;
|
|
318
|
+
if (typeof page.evaluate !== "function") return true;
|
|
319
|
+
|
|
320
|
+
return await page
|
|
321
|
+
.evaluate(async () => {
|
|
322
|
+
try {
|
|
323
|
+
const res = await fetch("/api/v1/auths/", { method: "GET" });
|
|
324
|
+
return res.status === 200;
|
|
325
|
+
} catch {
|
|
326
|
+
const btn = document.querySelector(
|
|
327
|
+
".header-right-auth-button, button.header-right-auth-button, a[href*='/auth'], a[href*='/login']",
|
|
328
|
+
);
|
|
329
|
+
return !btn || (btn as HTMLElement).offsetWidth === 0;
|
|
330
|
+
}
|
|
331
|
+
})
|
|
332
|
+
.catch(() => false);
|
|
333
|
+
} catch {
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
305
338
|
export async function getOrLaunchSharedBrowser(
|
|
306
339
|
browserType: BrowserType = "chromium",
|
|
307
340
|
headless = true,
|
|
@@ -492,7 +525,7 @@ function isAccountServingStream(accountId: string): boolean {
|
|
|
492
525
|
return true;
|
|
493
526
|
}
|
|
494
527
|
|
|
495
|
-
function getStealthScript(profile: FingerprintProfile): string {
|
|
528
|
+
export function getStealthScript(profile: FingerprintProfile): string {
|
|
496
529
|
const profileJson = JSON.stringify(profile).replace(/</g, "\\u003c");
|
|
497
530
|
return `
|
|
498
531
|
(function() {
|
|
@@ -740,6 +773,14 @@ function getStealthScript(profile: FingerprintProfile): string {
|
|
|
740
773
|
function makeMime(desc, suffixes, type) {
|
|
741
774
|
return { description: desc, suffixes: suffixes, type: type };
|
|
742
775
|
}
|
|
776
|
+
function attachPlugin(mime, plugin) {
|
|
777
|
+
Object.defineProperty(mime, 'enabledPlugin', {
|
|
778
|
+
value: plugin,
|
|
779
|
+
enumerable: false,
|
|
780
|
+
configurable: true,
|
|
781
|
+
writable: true,
|
|
782
|
+
});
|
|
783
|
+
}
|
|
743
784
|
const pdfMime = makeMime('Portable Document Format', 'pdf', 'application/pdf');
|
|
744
785
|
const pdfxMime = makeMime('Portable Document Format', 'pdf', 'text/pdf');
|
|
745
786
|
const pdfPlugin = {
|
|
@@ -750,8 +791,8 @@ function getStealthScript(profile: FingerprintProfile): string {
|
|
|
750
791
|
0: pdfMime,
|
|
751
792
|
1: pdfxMime,
|
|
752
793
|
};
|
|
753
|
-
pdfMime
|
|
754
|
-
pdfxMime
|
|
794
|
+
attachPlugin(pdfMime, pdfPlugin);
|
|
795
|
+
attachPlugin(pdfxMime, pdfPlugin);
|
|
755
796
|
|
|
756
797
|
const chromePdfMime = makeMime('Portable Document Format', 'pdf', 'application/pdf');
|
|
757
798
|
const chromePdfMime2 = makeMime('Portable Document Format', 'pdf', 'text/pdf');
|
|
@@ -763,8 +804,8 @@ function getStealthScript(profile: FingerprintProfile): string {
|
|
|
763
804
|
0: chromePdfMime,
|
|
764
805
|
1: chromePdfMime2,
|
|
765
806
|
};
|
|
766
|
-
chromePdfMime
|
|
767
|
-
chromePdfMime2
|
|
807
|
+
attachPlugin(chromePdfMime, chromePdfPlugin);
|
|
808
|
+
attachPlugin(chromePdfMime2, chromePdfPlugin);
|
|
768
809
|
|
|
769
810
|
const nativePlugin = {
|
|
770
811
|
name: 'Native Client',
|
|
@@ -774,9 +815,8 @@ function getStealthScript(profile: FingerprintProfile): string {
|
|
|
774
815
|
0: makeMime('Native Client Executable', '', 'application/x-nacl'),
|
|
775
816
|
1: makeMime('Portable Native Client Executable', '', 'application/x-pnacl'),
|
|
776
817
|
};
|
|
777
|
-
nativePlugin[0]
|
|
778
|
-
nativePlugin[1]
|
|
779
|
-
|
|
818
|
+
attachPlugin(nativePlugin[0], nativePlugin);
|
|
819
|
+
attachPlugin(nativePlugin[1], nativePlugin);
|
|
780
820
|
const pluginsList = [pdfPlugin, chromePdfPlugin, nativePlugin];
|
|
781
821
|
const mimeList = [pdfMime, pdfxMime, chromePdfMime, chromePdfMime2, nativePlugin[0], nativePlugin[1]];
|
|
782
822
|
|
|
@@ -1358,17 +1398,24 @@ export async function initPlaywrightForAccount(
|
|
|
1358
1398
|
waitUntil: "domcontentloaded",
|
|
1359
1399
|
timeout: config.timeouts.navigation,
|
|
1360
1400
|
});
|
|
1361
|
-
const
|
|
1362
|
-
if (
|
|
1401
|
+
const loggedIn = await isPageLoggedIn(acctPage);
|
|
1402
|
+
if (!loggedIn) {
|
|
1363
1403
|
if (account.email && account.password) {
|
|
1364
1404
|
console.warn(
|
|
1365
1405
|
`⚠️ [Playwright] Session expired for ${maskEmail(account.email)}, re-authenticating...`,
|
|
1366
1406
|
);
|
|
1367
|
-
await loginToQwen(account.id, account.email, account.password);
|
|
1407
|
+
const ok = await loginToQwen(account.id, account.email, account.password);
|
|
1408
|
+
if (!ok) {
|
|
1409
|
+
validationError = new Error(
|
|
1410
|
+
`Session expired for ${maskEmail(account.email)} and re-authentication failed`,
|
|
1411
|
+
);
|
|
1412
|
+
continue;
|
|
1413
|
+
}
|
|
1368
1414
|
} else {
|
|
1369
|
-
|
|
1370
|
-
`
|
|
1415
|
+
validationError = new Error(
|
|
1416
|
+
`Session expired for account ${account.id} but no credentials available for re-login (run 'qpx login')`,
|
|
1371
1417
|
);
|
|
1418
|
+
break;
|
|
1372
1419
|
}
|
|
1373
1420
|
}
|
|
1374
1421
|
validationError = null;
|
|
@@ -1389,8 +1436,6 @@ export async function initPlaywrightForAccount(
|
|
|
1389
1436
|
);
|
|
1390
1437
|
throw validationError;
|
|
1391
1438
|
}
|
|
1392
|
-
|
|
1393
|
-
// Capture headers by navigating and intercepting
|
|
1394
1439
|
await captureQwenHeaders(account.id);
|
|
1395
1440
|
|
|
1396
1441
|
// Header capture may leave the UI on a generated chat page. Return the
|
|
@@ -1411,7 +1456,7 @@ export async function initPlaywrightForAccount(
|
|
|
1411
1456
|
|
|
1412
1457
|
touchAccountActivity(account.id);
|
|
1413
1458
|
} catch (error) {
|
|
1414
|
-
await closePlaywrightContextBestEffort(account.id, acctContext);
|
|
1459
|
+
await closePlaywrightContextBestEffort(account.id, acctContext, { skipStorageSave: true });
|
|
1415
1460
|
cleanupPlaywrightAccountState(account.id);
|
|
1416
1461
|
throw error;
|
|
1417
1462
|
}
|
|
@@ -1521,23 +1566,20 @@ export async function validateAccountLogin(
|
|
|
1521
1566
|
} finally {
|
|
1522
1567
|
accountPages.delete(account.id);
|
|
1523
1568
|
}
|
|
1524
|
-
} else
|
|
1525
|
-
// Validate session by navigating to chat page
|
|
1569
|
+
} else {
|
|
1570
|
+
// Validate session by navigating to chat page and checking login state
|
|
1526
1571
|
try {
|
|
1527
1572
|
await acctPage.goto(qwenUrl("/"), {
|
|
1528
1573
|
waitUntil: "domcontentloaded",
|
|
1529
1574
|
timeout: config.timeouts.navigation,
|
|
1530
1575
|
});
|
|
1531
|
-
|
|
1532
|
-
if (
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
} finally {
|
|
1539
|
-
accountPages.delete(account.id);
|
|
1540
|
-
}
|
|
1576
|
+
loggedIn = await isPageLoggedIn(acctPage);
|
|
1577
|
+
if (!loggedIn && account.email && account.password) {
|
|
1578
|
+
accountPages.set(account.id, acctPage);
|
|
1579
|
+
try {
|
|
1580
|
+
loggedIn = await loginToQwen(account.id, account.email, account.password);
|
|
1581
|
+
} finally {
|
|
1582
|
+
accountPages.delete(account.id);
|
|
1541
1583
|
}
|
|
1542
1584
|
}
|
|
1543
1585
|
} catch {
|
|
@@ -1939,59 +1981,64 @@ export async function captureQwenHeaders(
|
|
|
1939
1981
|
touchAccountActivity(accountId);
|
|
1940
1982
|
const cache = getHeaderCache(accountId);
|
|
1941
1983
|
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1984
|
+
let cleanupRoute = async () => {};
|
|
1985
|
+
try {
|
|
1986
|
+
return await new Promise<void>((resolve, reject) => {
|
|
1987
|
+
let settled = false;
|
|
1988
|
+
let routeRegistered = false;
|
|
1989
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
1990
|
+
let routeHandler: (route: any, request: any) => Promise<void>;
|
|
1991
|
+
let sawIncompleteHeaders = false;
|
|
1992
|
+
let headersCaptured = false;
|
|
1993
|
+
let retriggerRequested = false;
|
|
1994
|
+
let lastAttemptGraceTimedOut = false;
|
|
1995
|
+
let graceTimeoutCount = 0;
|
|
1996
|
+
let wakeTriggerLoop: (() => void) | undefined;
|
|
1997
|
+
const deadline = Date.now() + timeoutMs;
|
|
1998
|
+
const remainingBudgetMs = () => deadline - Date.now();
|
|
1999
|
+
|
|
2000
|
+
cleanupRoute = async () => {
|
|
2001
|
+
if (!routeRegistered) return;
|
|
2002
|
+
routeRegistered = false;
|
|
2003
|
+
if (!page.isClosed()) {
|
|
2004
|
+
try {
|
|
2005
|
+
await page.unroute("**/api/v2/chat/completions*", routeHandler);
|
|
2006
|
+
} catch {}
|
|
2007
|
+
}
|
|
2008
|
+
};
|
|
1962
2009
|
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
2010
|
+
const wakeTrigger = () => {
|
|
2011
|
+
const wake = wakeTriggerLoop;
|
|
2012
|
+
wakeTriggerLoop = undefined;
|
|
2013
|
+
wake?.();
|
|
2014
|
+
};
|
|
1968
2015
|
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
2016
|
+
const settle = (error?: Error) => {
|
|
2017
|
+
if (settled) return;
|
|
2018
|
+
settled = true;
|
|
2019
|
+
if (timeout) clearTimeout(timeout);
|
|
2020
|
+
void cleanupRoute();
|
|
2021
|
+
// A trigger loop parked between attempts has to be released, otherwise it
|
|
2022
|
+
// stays pending forever behind an already-settled capture.
|
|
2023
|
+
wakeTrigger();
|
|
2024
|
+
// When a trigger grace period expired (page fired no completion request),
|
|
2025
|
+
// log the OUTCOME so the operator can see whether the retry loop
|
|
2026
|
+
// recovered or the account is being rotated into cooldown — the bare
|
|
2027
|
+
// per-attempt warning leaves that dangling.
|
|
2028
|
+
if (graceTimeoutCount > 0) {
|
|
2029
|
+
if (headersCaptured) {
|
|
2030
|
+
console.log(
|
|
2031
|
+
`✅ [Playwright] Header capture recovered for ${accountId} after ${graceTimeoutCount} silent send(s)`,
|
|
2032
|
+
);
|
|
2033
|
+
} else {
|
|
2034
|
+
console.warn(
|
|
2035
|
+
`❌ [Playwright] Header capture failed for ${accountId} after ${graceTimeoutCount} silent send(s): ${error?.message ?? "no completion request"}`,
|
|
2036
|
+
);
|
|
2037
|
+
}
|
|
1990
2038
|
}
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
};
|
|
2039
|
+
if (error) reject(error);
|
|
2040
|
+
else resolve();
|
|
2041
|
+
};
|
|
1995
2042
|
|
|
1996
2043
|
const incompleteHeadersError = () =>
|
|
1997
2044
|
new Error(
|
|
@@ -2152,8 +2199,8 @@ export async function captureQwenHeaders(
|
|
|
2152
2199
|
// burn every trigger attempt on a textarea that does not exist. Re-login
|
|
2153
2200
|
// immediately when credentials are available; otherwise fail fast with a
|
|
2154
2201
|
// clear diagnosis instead of 3 pointless grace timeouts.
|
|
2155
|
-
const
|
|
2156
|
-
if (
|
|
2202
|
+
const loggedIn = await isPageLoggedIn(page);
|
|
2203
|
+
if (!loggedIn) {
|
|
2157
2204
|
const { getAccountCredentials } = await import("../core/accounts.ts");
|
|
2158
2205
|
const creds = getAccountCredentials(accountId);
|
|
2159
2206
|
if (creds && creds.email && creds.password) {
|
|
@@ -2176,7 +2223,7 @@ export async function captureQwenHeaders(
|
|
|
2176
2223
|
} else {
|
|
2177
2224
|
settle(
|
|
2178
2225
|
new Error(
|
|
2179
|
-
`Header capture failed for ${accountId}: session expired and no credentials available for re-login`,
|
|
2226
|
+
`Header capture failed for ${accountId}: session expired and no credentials available for re-login (run 'qpx login')`,
|
|
2180
2227
|
),
|
|
2181
2228
|
);
|
|
2182
2229
|
return;
|
|
@@ -2320,11 +2367,12 @@ export async function captureQwenHeaders(
|
|
|
2320
2367
|
: new Error(`Header capture route registration failed for ${accountId}`),
|
|
2321
2368
|
);
|
|
2322
2369
|
});
|
|
2323
|
-
|
|
2370
|
+
});
|
|
2371
|
+
} finally {
|
|
2372
|
+
await cleanupRoute();
|
|
2373
|
+
}
|
|
2324
2374
|
}
|
|
2325
|
-
|
|
2326
2375
|
type CookieSnapshot = Awaited<ReturnType<BrowserContext["cookies"]>>;
|
|
2327
|
-
|
|
2328
2376
|
/**
|
|
2329
2377
|
* Fetch the account context cookies once. The snapshot feeds every validity
|
|
2330
2378
|
* check and the cookie string build, avoiding repeated CDP round-trips.
|
|
@@ -3022,15 +3070,23 @@ function cleanupPlaywrightAccountState(accountId: string): void {
|
|
|
3022
3070
|
async function closePlaywrightContextBestEffort(
|
|
3023
3071
|
accountId: string,
|
|
3024
3072
|
context: BrowserContext,
|
|
3073
|
+
options?: { skipStorageSave?: boolean },
|
|
3025
3074
|
): Promise<void> {
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
await
|
|
3029
|
-
|
|
3030
|
-
|
|
3075
|
+
if (!options?.skipStorageSave) {
|
|
3076
|
+
try {
|
|
3077
|
+
if (await hasValidAuthCookie(context)) {
|
|
3078
|
+
await saveStorageState(context, accountId);
|
|
3079
|
+
}
|
|
3080
|
+
} catch {}
|
|
3081
|
+
}
|
|
3031
3082
|
|
|
3032
3083
|
try {
|
|
3033
3084
|
const pages = context.pages();
|
|
3085
|
+
for (const page of pages) {
|
|
3086
|
+
if (!page.isClosed()) {
|
|
3087
|
+
await (page as any).unrouteAll?.({ behavior: "ignoreErrors" }).catch(() => {});
|
|
3088
|
+
}
|
|
3089
|
+
}
|
|
3034
3090
|
await Promise.all(
|
|
3035
3091
|
pages.map((page) =>
|
|
3036
3092
|
withTimeout(
|
|
@@ -3078,7 +3134,13 @@ async function closePlaywrightForAccountLocked(
|
|
|
3078
3134
|
* that must not be logged as keep-alive failures.
|
|
3079
3135
|
*/
|
|
3080
3136
|
export function isPlaywrightAlreadyClosedError(error: unknown): boolean {
|
|
3081
|
-
|
|
3137
|
+
if (!error) return false;
|
|
3138
|
+
const message =
|
|
3139
|
+
error instanceof Error
|
|
3140
|
+
? error.message
|
|
3141
|
+
: typeof error === "object" && "message" in error
|
|
3142
|
+
? String((error as any).message)
|
|
3143
|
+
: String(error);
|
|
3082
3144
|
return (
|
|
3083
3145
|
message.includes("Target page, context or browser has been closed") ||
|
|
3084
3146
|
message.includes("Browser has been closed") ||
|
|
@@ -3087,7 +3149,11 @@ export function isPlaywrightAlreadyClosedError(error: unknown): boolean {
|
|
|
3087
3149
|
message.includes("Page crashed") ||
|
|
3088
3150
|
message.includes("Assertion error") ||
|
|
3089
3151
|
message.includes("Cannot find parent object") ||
|
|
3090
|
-
message.includes("Connection closed")
|
|
3152
|
+
message.includes("Connection closed") ||
|
|
3153
|
+
message.includes("session closed") ||
|
|
3154
|
+
message.includes("Session closed") ||
|
|
3155
|
+
message.includes("Network.setCacheDisabled") ||
|
|
3156
|
+
message.includes("Protocol error")
|
|
3091
3157
|
);
|
|
3092
3158
|
}
|
|
3093
3159
|
|
package/src/tools/parser.ts
CHANGED
|
@@ -962,7 +962,7 @@ function repairCommonMalformedToolJson(content: string): string {
|
|
|
962
962
|
// a bare-word value, so inserting the quote is safe (true/false/null and
|
|
963
963
|
// numbers are excluded). The trailing `\"` escapes are preserved, so the
|
|
964
964
|
// model's closing quote still terminates the string.
|
|
965
|
-
/([,{]\s*"[a-zA-Z_][a-zA-Z0-9_]*"\s*:\s*)(?=(?!true|false|null)[
|
|
965
|
+
/([,{]\s*"[a-zA-Z_][a-zA-Z0-9_]*"\s*:\s*)(?=(?!true|false|null|\d|\[|\{|")[^\s])/g,
|
|
966
966
|
'$1"',
|
|
967
967
|
);
|
|
968
968
|
return repairMissingArrayClose(repaired);
|
package/src/tui/index.ts
CHANGED
|
@@ -37,6 +37,10 @@ async function main() {
|
|
|
37
37
|
const app = new TuiApp(initialTab);
|
|
38
38
|
|
|
39
39
|
process.on("uncaughtException", async (err) => {
|
|
40
|
+
const { isPlaywrightAlreadyClosedError } = await import("../services/playwright.ts");
|
|
41
|
+
if (isPlaywrightAlreadyClosedError(err)) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
40
44
|
try {
|
|
41
45
|
await app.stop();
|
|
42
46
|
} catch {}
|
|
@@ -45,6 +49,10 @@ async function main() {
|
|
|
45
49
|
});
|
|
46
50
|
|
|
47
51
|
process.on("unhandledRejection", async (err) => {
|
|
52
|
+
const { isPlaywrightAlreadyClosedError } = await import("../services/playwright.ts");
|
|
53
|
+
if (isPlaywrightAlreadyClosedError(err)) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
48
56
|
try {
|
|
49
57
|
await app.stop();
|
|
50
58
|
} catch {}
|