qwenproxy-cli 1.0.16 → 1.0.18
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 +39 -21
- package/src/index.ts +26 -32
- package/src/routes/chat/validation.ts +14 -9
- package/src/services/playwright.ts +27 -10
- package/src/tools/parser.ts +56 -5
- package/src/tui/index.ts +8 -0
- package/src/utils/json.ts +7 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qwenproxy-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.18",
|
|
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
|
@@ -653,16 +653,12 @@ export async function startServer(options?: {
|
|
|
653
653
|
const totalAccounts = accounts.length;
|
|
654
654
|
|
|
655
655
|
// Warm accounts in priority order (recently successful accounts first),
|
|
656
|
-
// skipping accounts still on cooldown. Warm
|
|
657
|
-
//
|
|
658
|
-
//
|
|
656
|
+
// skipping accounts still on cooldown. Warm the primary account first so
|
|
657
|
+
// the server binds the port and goes online immediately (~15-20s).
|
|
658
|
+
// Reserve account(s) and standby validations run seamlessly in background.
|
|
659
659
|
const warmOrder = getAccountsByPriority(accounts).filter(
|
|
660
660
|
(account) => !getAccountCooldownInfo(account.id),
|
|
661
661
|
);
|
|
662
|
-
const targetWarmCount = Math.min(
|
|
663
|
-
warmOrder.length,
|
|
664
|
-
Math.max(1, config.playwright.maxActiveContexts),
|
|
665
|
-
);
|
|
666
662
|
const readyAccountIds = new Set<string>();
|
|
667
663
|
|
|
668
664
|
for (let i = 0; i < warmOrder.length; i++) {
|
|
@@ -676,11 +672,9 @@ export async function startServer(options?: {
|
|
|
676
672
|
if (ok) {
|
|
677
673
|
readyAccountIds.add(warmOrder[i].id);
|
|
678
674
|
console.log(
|
|
679
|
-
`✅ [Server] Account ready (
|
|
675
|
+
`✅ [Server] Account ready (1/${totalAccounts}): ${maskEmail(warmOrder[i].email)}`,
|
|
680
676
|
);
|
|
681
|
-
|
|
682
|
-
break;
|
|
683
|
-
}
|
|
677
|
+
break;
|
|
684
678
|
}
|
|
685
679
|
}
|
|
686
680
|
|
|
@@ -717,16 +711,43 @@ export async function startServer(options?: {
|
|
|
717
711
|
`🪶 [Server] ${remainingAccounts.length} standby account(s) will initialize on demand`,
|
|
718
712
|
);
|
|
719
713
|
|
|
720
|
-
//
|
|
721
|
-
//
|
|
714
|
+
// In background: warm 1 reserve account (if maxActiveContexts > 1) and
|
|
715
|
+
// validate the rest of the standby accounts
|
|
722
716
|
void (async () => {
|
|
723
717
|
const { validateAccountLogin } = await import("../services/playwright.ts");
|
|
724
718
|
const { ensureAccountInPriority } = await import("../core/account-priority.ts");
|
|
725
719
|
|
|
720
|
+
let accountsToValidate = remainingAccounts;
|
|
721
|
+
|
|
722
|
+
// Warm reserve account in background for fast failover without delaying startup
|
|
723
|
+
if (config.playwright.maxActiveContexts > 1 && remainingAccounts.length > 0) {
|
|
724
|
+
const reserveAccount = remainingAccounts[0];
|
|
725
|
+
accountsToValidate = remainingAccounts.slice(1);
|
|
726
|
+
try {
|
|
727
|
+
const ok = await prepareAccountRuntime(
|
|
728
|
+
reserveAccount,
|
|
729
|
+
getAccountCredentials,
|
|
730
|
+
initPlaywrightForAccount,
|
|
731
|
+
disableNativeTools,
|
|
732
|
+
warmQwenChatPool,
|
|
733
|
+
);
|
|
734
|
+
if (ok) {
|
|
735
|
+
ensureAccountInPriority(reserveAccount.id);
|
|
736
|
+
console.log(
|
|
737
|
+
`✅ [Server] Reserve account ready (2/${totalAccounts}): ${maskEmail(reserveAccount.email)}`,
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
} catch (err) {
|
|
741
|
+
console.warn(
|
|
742
|
+
`⚠️ [Server] Failed to warm reserve account ${maskEmail(reserveAccount.email)}: ${getErrorMessage(err)}`,
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
726
747
|
let validated = 0;
|
|
727
748
|
let failed = 0;
|
|
728
749
|
|
|
729
|
-
for (const account of
|
|
750
|
+
for (const account of accountsToValidate) {
|
|
730
751
|
try {
|
|
731
752
|
const creds = getAccountCredentials(account.id) ?? account;
|
|
732
753
|
// Validate login in background with real unmasked credentials
|
|
@@ -757,17 +778,14 @@ export async function startServer(options?: {
|
|
|
757
778
|
markAccountRateLimited(
|
|
758
779
|
account.id,
|
|
759
780
|
24 * 3600 * 1000,
|
|
760
|
-
`
|
|
781
|
+
`StandbyValidationError: ${getErrorMessage(error)}`,
|
|
761
782
|
);
|
|
762
783
|
}
|
|
763
784
|
}
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
`⚠️ [Server] Standby validation finished: ${validated} ok, ${failed} failed`,
|
|
767
|
-
);
|
|
768
|
-
} else if (validated > 0) {
|
|
785
|
+
|
|
786
|
+
if (validated > 0 || failed > 0) {
|
|
769
787
|
console.log(
|
|
770
|
-
`✅ [Server] Standby validation complete:
|
|
788
|
+
`✅ [Server] Standby validation complete: ${validated} account(s) ready${failed > 0 ? `, ${failed} failed` : ""}`,
|
|
771
789
|
);
|
|
772
790
|
}
|
|
773
791
|
})().catch((error) => {
|
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
|
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
} from "../../core/reasoning-effort.ts";
|
|
19
19
|
|
|
20
20
|
import { TOOL_CALL_OPEN, TOOL_CALL_CLOSE } from "../../tools/toolcall-tags.ts";
|
|
21
|
+
import { robustParseJSON } from "../../utils/json.ts";
|
|
21
22
|
|
|
22
23
|
export interface ParsedRequest {
|
|
23
24
|
body: OpenAIRequest;
|
|
@@ -224,15 +225,19 @@ async function buildPromptFromMessages(
|
|
|
224
225
|
if (typeof args === "string") {
|
|
225
226
|
try {
|
|
226
227
|
parsedArgs = JSON.parse(args);
|
|
227
|
-
} catch
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
228
|
+
} catch {
|
|
229
|
+
try {
|
|
230
|
+
parsedArgs = robustParseJSON(args);
|
|
231
|
+
} catch (parseErr) {
|
|
232
|
+
// Malformed JSON: preserve raw string for model visibility
|
|
233
|
+
logger.warn("[chat] Failed to parse tool_call arguments", {
|
|
234
|
+
toolCallId: tc.id,
|
|
235
|
+
toolName: tc.function?.name,
|
|
236
|
+
error: parseErr instanceof Error ? parseErr.message : "Unknown",
|
|
237
|
+
rawArgs: args.substring(0, 200),
|
|
238
|
+
});
|
|
239
|
+
parsedArgs = { _raw: args };
|
|
240
|
+
}
|
|
236
241
|
}
|
|
237
242
|
} else if (args && typeof args === "object") {
|
|
238
243
|
parsedArgs = args;
|
|
@@ -492,7 +492,7 @@ function isAccountServingStream(accountId: string): boolean {
|
|
|
492
492
|
return true;
|
|
493
493
|
}
|
|
494
494
|
|
|
495
|
-
function getStealthScript(profile: FingerprintProfile): string {
|
|
495
|
+
export function getStealthScript(profile: FingerprintProfile): string {
|
|
496
496
|
const profileJson = JSON.stringify(profile).replace(/</g, "\\u003c");
|
|
497
497
|
return `
|
|
498
498
|
(function() {
|
|
@@ -740,6 +740,14 @@ function getStealthScript(profile: FingerprintProfile): string {
|
|
|
740
740
|
function makeMime(desc, suffixes, type) {
|
|
741
741
|
return { description: desc, suffixes: suffixes, type: type };
|
|
742
742
|
}
|
|
743
|
+
function attachPlugin(mime, plugin) {
|
|
744
|
+
Object.defineProperty(mime, 'enabledPlugin', {
|
|
745
|
+
value: plugin,
|
|
746
|
+
enumerable: false,
|
|
747
|
+
configurable: true,
|
|
748
|
+
writable: true,
|
|
749
|
+
});
|
|
750
|
+
}
|
|
743
751
|
const pdfMime = makeMime('Portable Document Format', 'pdf', 'application/pdf');
|
|
744
752
|
const pdfxMime = makeMime('Portable Document Format', 'pdf', 'text/pdf');
|
|
745
753
|
const pdfPlugin = {
|
|
@@ -750,8 +758,8 @@ function getStealthScript(profile: FingerprintProfile): string {
|
|
|
750
758
|
0: pdfMime,
|
|
751
759
|
1: pdfxMime,
|
|
752
760
|
};
|
|
753
|
-
pdfMime
|
|
754
|
-
pdfxMime
|
|
761
|
+
attachPlugin(pdfMime, pdfPlugin);
|
|
762
|
+
attachPlugin(pdfxMime, pdfPlugin);
|
|
755
763
|
|
|
756
764
|
const chromePdfMime = makeMime('Portable Document Format', 'pdf', 'application/pdf');
|
|
757
765
|
const chromePdfMime2 = makeMime('Portable Document Format', 'pdf', 'text/pdf');
|
|
@@ -763,8 +771,8 @@ function getStealthScript(profile: FingerprintProfile): string {
|
|
|
763
771
|
0: chromePdfMime,
|
|
764
772
|
1: chromePdfMime2,
|
|
765
773
|
};
|
|
766
|
-
chromePdfMime
|
|
767
|
-
chromePdfMime2
|
|
774
|
+
attachPlugin(chromePdfMime, chromePdfPlugin);
|
|
775
|
+
attachPlugin(chromePdfMime2, chromePdfPlugin);
|
|
768
776
|
|
|
769
777
|
const nativePlugin = {
|
|
770
778
|
name: 'Native Client',
|
|
@@ -774,9 +782,8 @@ function getStealthScript(profile: FingerprintProfile): string {
|
|
|
774
782
|
0: makeMime('Native Client Executable', '', 'application/x-nacl'),
|
|
775
783
|
1: makeMime('Portable Native Client Executable', '', 'application/x-pnacl'),
|
|
776
784
|
};
|
|
777
|
-
nativePlugin[0]
|
|
778
|
-
nativePlugin[1]
|
|
779
|
-
|
|
785
|
+
attachPlugin(nativePlugin[0], nativePlugin);
|
|
786
|
+
attachPlugin(nativePlugin[1], nativePlugin);
|
|
780
787
|
const pluginsList = [pdfPlugin, chromePdfPlugin, nativePlugin];
|
|
781
788
|
const mimeList = [pdfMime, pdfxMime, chromePdfMime, chromePdfMime2, nativePlugin[0], nativePlugin[1]];
|
|
782
789
|
|
|
@@ -3078,7 +3085,13 @@ async function closePlaywrightForAccountLocked(
|
|
|
3078
3085
|
* that must not be logged as keep-alive failures.
|
|
3079
3086
|
*/
|
|
3080
3087
|
export function isPlaywrightAlreadyClosedError(error: unknown): boolean {
|
|
3081
|
-
|
|
3088
|
+
if (!error) return false;
|
|
3089
|
+
const message =
|
|
3090
|
+
error instanceof Error
|
|
3091
|
+
? error.message
|
|
3092
|
+
: typeof error === "object" && "message" in error
|
|
3093
|
+
? String((error as any).message)
|
|
3094
|
+
: String(error);
|
|
3082
3095
|
return (
|
|
3083
3096
|
message.includes("Target page, context or browser has been closed") ||
|
|
3084
3097
|
message.includes("Browser has been closed") ||
|
|
@@ -3087,7 +3100,11 @@ export function isPlaywrightAlreadyClosedError(error: unknown): boolean {
|
|
|
3087
3100
|
message.includes("Page crashed") ||
|
|
3088
3101
|
message.includes("Assertion error") ||
|
|
3089
3102
|
message.includes("Cannot find parent object") ||
|
|
3090
|
-
message.includes("Connection closed")
|
|
3103
|
+
message.includes("Connection closed") ||
|
|
3104
|
+
message.includes("session closed") ||
|
|
3105
|
+
message.includes("Session closed") ||
|
|
3106
|
+
message.includes("Network.setCacheDisabled") ||
|
|
3107
|
+
message.includes("Protocol error")
|
|
3091
3108
|
);
|
|
3092
3109
|
}
|
|
3093
3110
|
|
package/src/tools/parser.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
getOpenNames,
|
|
10
10
|
getCloseNames,
|
|
11
11
|
matchToolCloseAt,
|
|
12
|
+
stripTrailingStrayCloses,
|
|
12
13
|
} from "./toolcall-tags.ts";
|
|
13
14
|
|
|
14
15
|
export interface ToolCallDelta {
|
|
@@ -190,6 +191,45 @@ function closeTagContentIsParseable(buffer: string, endIdx: number): boolean {
|
|
|
190
191
|
return tryParseJsonToolPayload(content);
|
|
191
192
|
}
|
|
192
193
|
|
|
194
|
+
function balanceClosingBrackets(content: string): string {
|
|
195
|
+
let inString = false;
|
|
196
|
+
let escaped = false;
|
|
197
|
+
const stack: string[] = [];
|
|
198
|
+
for (let i = 0; i < content.length; i++) {
|
|
199
|
+
const ch = content[i];
|
|
200
|
+
if (inString) {
|
|
201
|
+
if (escaped) {
|
|
202
|
+
escaped = false;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (ch === "\\") {
|
|
206
|
+
escaped = true;
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if (ch === '"') inString = false;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (ch === '"') {
|
|
213
|
+
inString = true;
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (ch === "{" || ch === "[") stack.push(ch);
|
|
217
|
+
else if (ch === "}" || ch === "]") {
|
|
218
|
+
const top = stack[stack.length - 1];
|
|
219
|
+
if ((top === "{" && ch === "}") || (top === "[" && ch === "]")) {
|
|
220
|
+
stack.pop();
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (inString) return content;
|
|
225
|
+
let out = content;
|
|
226
|
+
while (stack.length > 0) {
|
|
227
|
+
const open = stack.pop();
|
|
228
|
+
out += open === "{" ? "}" : "]";
|
|
229
|
+
}
|
|
230
|
+
return out;
|
|
231
|
+
}
|
|
232
|
+
|
|
193
233
|
/**
|
|
194
234
|
* Plain-JSON.parse based candidate checks, in increasing tolerance order:
|
|
195
235
|
* raw payload -> narrow typo repairs -> doubled trailing brace/bracket ->
|
|
@@ -214,9 +254,13 @@ function tryParseJsonToolPayload(content: string): boolean {
|
|
|
214
254
|
|
|
215
255
|
const candidates = [repaired, stripped];
|
|
216
256
|
if (repaired !== content) candidates.push(strippedRepaired);
|
|
217
|
-
candidates.push(`{
|
|
218
|
-
if (repaired !== content) candidates.push(`{
|
|
257
|
+
candidates.push(`{"${content}`, `{${content}`);
|
|
258
|
+
if (repaired !== content) candidates.push(`{"${repaired}`, `{${repaired}`);
|
|
219
259
|
|
|
260
|
+
const balanced = balanceClosingBrackets(content);
|
|
261
|
+
if (balanced !== content) candidates.push(balanced);
|
|
262
|
+
const balancedRepaired = balanceClosingBrackets(repaired);
|
|
263
|
+
if (balancedRepaired !== content) candidates.push(balancedRepaired);
|
|
220
264
|
return candidates.some((candidate) => tryParse(candidate));
|
|
221
265
|
}
|
|
222
266
|
|
|
@@ -918,7 +962,7 @@ function repairCommonMalformedToolJson(content: string): string {
|
|
|
918
962
|
// a bare-word value, so inserting the quote is safe (true/false/null and
|
|
919
963
|
// numbers are excluded). The trailing `\"` escapes are preserved, so the
|
|
920
964
|
// model's closing quote still terminates the string.
|
|
921
|
-
/([,{]\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,
|
|
922
966
|
'$1"',
|
|
923
967
|
);
|
|
924
968
|
return repairMissingArrayClose(repaired);
|
|
@@ -999,6 +1043,10 @@ function isJsonPayloadTruncated(content: string): boolean {
|
|
|
999
1043
|
if (content.includes('\\"')) {
|
|
1000
1044
|
alt.push(content.replace(/\\"/g, '"'));
|
|
1001
1045
|
}
|
|
1046
|
+
const balanced = balanceClosingBrackets(trimmed);
|
|
1047
|
+
if (balanced !== trimmed) {
|
|
1048
|
+
alt.push(balanced);
|
|
1049
|
+
}
|
|
1002
1050
|
for (const candidate of alt) {
|
|
1003
1051
|
if (!scanJsonStructureIncomplete(candidate)) return false;
|
|
1004
1052
|
}
|
|
@@ -1864,7 +1912,7 @@ export class StreamingToolParser {
|
|
|
1864
1912
|
// argument values (e.g. `{"a": "1</tool_call>"}`). Genuine unclosed
|
|
1865
1913
|
// streams (cut mid-payload) have no trailing tag, so this is a no-op
|
|
1866
1914
|
// for them.
|
|
1867
|
-
const trimmed = rawTrimmed.
|
|
1915
|
+
const trimmed = stripTrailingStrayCloses(rawTrimmed).trim();
|
|
1868
1916
|
if (trimmed.length > 0) {
|
|
1869
1917
|
if (isToolcallDebugEnabled()) {
|
|
1870
1918
|
logger.debug(
|
|
@@ -2570,10 +2618,13 @@ export class StreamingToolParser {
|
|
|
2570
2618
|
// malformed tracking fires and the model re-emits cleanly.
|
|
2571
2619
|
if (isJsonPayloadTruncated(block)) return null;
|
|
2572
2620
|
const variants = [block];
|
|
2621
|
+
const balanced = balanceClosingBrackets(block);
|
|
2622
|
+
if (balanced !== block) {
|
|
2623
|
+
variants.push(balanced);
|
|
2624
|
+
}
|
|
2573
2625
|
if (block.includes('\\"')) {
|
|
2574
2626
|
variants.push(block.replace(/\\"/g, '"'));
|
|
2575
2627
|
}
|
|
2576
|
-
|
|
2577
2628
|
for (const variant of variants) {
|
|
2578
2629
|
try {
|
|
2579
2630
|
const parsed = robustParseJSON(variant);
|
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 {}
|
package/src/utils/json.ts
CHANGED
|
@@ -191,6 +191,13 @@ export function robustParseJSON(str: string): any {
|
|
|
191
191
|
jsonPart = jsonPart.replace(/\\\\"/g, '\\"');
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
+
// Heal stray backslashes escaping quotes on property keys or values
|
|
195
|
+
// e.g. "key\": \"val\" or "key\": "val" -> "key": "val"
|
|
196
|
+
jsonPart = jsonPart
|
|
197
|
+
.replace(/([{,]\s*)"([a-zA-Z0-9_-]+)\\"(\s*:)/g, '$1"$2"$3')
|
|
198
|
+
.replace(/:\s*\\"([^"\\]*)\\"(\s*[,}\]])/g, ': "$1"$2')
|
|
199
|
+
.replace(/:\s*\\"([^"\\]*)("(?:\s*[,}\]]))/g, ': "$1$2')
|
|
200
|
+
.replace(/:\s*"([^"\\]*)\\"(\s*[,}\]])/g, ': "$1"$2');
|
|
194
201
|
let currentJson = jsonPart.replace(
|
|
195
202
|
/([{,]\s*)([a-zA-Z_][a-zA-Z0-9_]*)(\s*:)/g,
|
|
196
203
|
'$1"$2"$3',
|