qwenproxy-cli 1.0.21 → 1.0.23
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/mutex.ts +10 -0
- package/src/routes/chat/account.ts +11 -4
- package/src/routes/chat/streaming.ts +4 -3
- package/src/services/captcha-coordinator.ts +11 -10
- package/src/services/chat-cleanup.ts +34 -11
- package/src/services/playwright.ts +51 -13
- package/src/services/qwen.ts +34 -52
- package/src/tools/parser.ts +22 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qwenproxy-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.23",
|
|
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/mutex.ts
CHANGED
|
@@ -102,6 +102,16 @@ export class Mutex {
|
|
|
102
102
|
this.lockedByKey = "";
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Heartbeat the lock: extend lockedAt while the holder is actively making progress.
|
|
107
|
+
* Prevents premature force-release during long-running streaming generations.
|
|
108
|
+
*/
|
|
109
|
+
touch(key?: string): void {
|
|
110
|
+
if (this.locked && (!key || this.lockedByKey === key)) {
|
|
111
|
+
this.lockedAt = Date.now();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
105
115
|
/** Returns true if the mutex is not locked and has no waiting queue. */
|
|
106
116
|
isIdle(): boolean {
|
|
107
117
|
return !this.locked && this.queue.length === 0;
|
|
@@ -112,10 +112,11 @@ const personalizationLocks = new Map<string, Mutex>();
|
|
|
112
112
|
mutex = new Mutex(
|
|
113
113
|
`chat:${chatId.substring(0, 8)}`,
|
|
114
114
|
// The chat lock is held for the whole stream lifetime. A long
|
|
115
|
-
// generation (reasoning + huge context) can legitimately
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
|
|
115
|
+
// generation (reasoning + huge context) can legitimately take 4-5 min;
|
|
116
|
+
// use config.timeouts.totalRequestTimeout (600s) as the max hold limit, while
|
|
117
|
+
// touchChatLock extends it on every chunk so active streams are never
|
|
118
|
+
// force-released mid-generation.
|
|
119
|
+
config.timeouts.totalRequestTimeout,
|
|
119
120
|
);
|
|
120
121
|
chatLocks.set(chatId, mutex);
|
|
121
122
|
}
|
|
@@ -141,6 +142,12 @@ const personalizationLocks = new Map<string, Mutex>();
|
|
|
141
142
|
};
|
|
142
143
|
}
|
|
143
144
|
|
|
145
|
+
export function touchChatLock(chatId: string | undefined): void {
|
|
146
|
+
if (!chatId) return;
|
|
147
|
+
const mutex = chatLocks.get(chatId);
|
|
148
|
+
mutex?.touch();
|
|
149
|
+
}
|
|
150
|
+
|
|
144
151
|
async function acquirePersonalizationLock(
|
|
145
152
|
accountId: string,
|
|
146
153
|
): Promise<() => void> {
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
shouldRetryInvalidInputOnSameAccount,
|
|
31
31
|
} from "./retry-policy.ts";
|
|
32
32
|
import type { Message, OpenAIRequest, Usage } from "../../utils/types.ts";
|
|
33
|
+
import { touchChatLock } from "./account.ts";
|
|
33
34
|
import { StreamingToolParser } from "../../tools/parser.ts";
|
|
34
35
|
import {
|
|
35
36
|
getStream,
|
|
@@ -351,7 +352,7 @@ export async function processNonStreamingResponse(
|
|
|
351
352
|
while (true) {
|
|
352
353
|
const { done, value } = await reader.read();
|
|
353
354
|
if (done) break;
|
|
354
|
-
|
|
355
|
+
touchChatLock(currentUiSessionId);
|
|
355
356
|
const decoded = decoder.decode(value, { stream: true });
|
|
356
357
|
if (!sawSseProtocol) {
|
|
357
358
|
protocolBuffer += decoded;
|
|
@@ -1598,7 +1599,7 @@ export async function processStreamingResponse(
|
|
|
1598
1599
|
throw readError;
|
|
1599
1600
|
}
|
|
1600
1601
|
if (readResult.done) break;
|
|
1601
|
-
|
|
1602
|
+
touchChatLock(currentUiSessionId);
|
|
1602
1603
|
buffer += decoder.decode(readResult.value, { stream: true });
|
|
1603
1604
|
lineEnd = buffer.indexOf("\n");
|
|
1604
1605
|
if (lineEnd === -1) continue;
|
|
@@ -2141,7 +2142,7 @@ export async function processStreamingResponse(
|
|
|
2141
2142
|
retryReadLoop: while (true) {
|
|
2142
2143
|
const { done, value } = await retryReader.read();
|
|
2143
2144
|
if (done) break;
|
|
2144
|
-
|
|
2145
|
+
touchChatLock(currentUiSessionId);
|
|
2145
2146
|
retryBuf += retryDecoder.decode(value, { stream: true });
|
|
2146
2147
|
let lineStart = 0;
|
|
2147
2148
|
let lineEnd = retryBuf.indexOf("\n", lineStart);
|
|
@@ -111,26 +111,27 @@ export async function recoverBaxiaCaptcha(
|
|
|
111
111
|
|
|
112
112
|
// The slider itself waits up to 5s for each attempt. Keep the page
|
|
113
113
|
// operation alive for the full bounded solver budget so a slow challenge
|
|
114
|
-
// cannot be mistaken for a stuck browser
|
|
114
|
+
// cannot be mistaken for a stuck browser. Do not clamp to timeouts.page.
|
|
115
115
|
// Two navigations (open the challenge, return to the chat page) are part of
|
|
116
116
|
// the recovery, so their budget belongs in the same total.
|
|
117
|
-
const solverOperationTimeoutMs = Math.
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
(3_000 + config.captcha.retryDelayMs + config.captcha.settleMs) +
|
|
124
|
-
2 * CHALLENGE_NAVIGATION_TIMEOUT_MS,
|
|
125
|
-
),
|
|
117
|
+
const solverOperationTimeoutMs = Math.max(
|
|
118
|
+
15_000,
|
|
119
|
+
config.captcha.timeoutMs +
|
|
120
|
+
config.captcha.maxAttempts *
|
|
121
|
+
(3_000 + config.captcha.retryDelayMs + config.captcha.settleMs) +
|
|
122
|
+
2 * CHALLENGE_NAVIGATION_TIMEOUT_MS,
|
|
126
123
|
);
|
|
127
124
|
|
|
128
125
|
try {
|
|
126
|
+
// recoverOnTimeout: false ensures that if the captcha solve times out,
|
|
127
|
+
// withAccountPage does NOT destructively kill the browser context, which
|
|
128
|
+
// previously caused "Target page, context or browser has been closed" mid-slider.
|
|
129
129
|
const solved = await withAccountPage(
|
|
130
130
|
accountId,
|
|
131
131
|
(page) => solveChallengeOnPage(page, challengeUrl),
|
|
132
132
|
solverOperationTimeoutMs,
|
|
133
133
|
Math.min(config.timeouts.page, 5_000),
|
|
134
|
+
false,
|
|
134
135
|
);
|
|
135
136
|
|
|
136
137
|
metrics.histogram("captcha.solve.duration", Date.now() - startedAt, {
|
|
@@ -10,32 +10,52 @@ import {
|
|
|
10
10
|
closeAllPlaywright,
|
|
11
11
|
} from "./playwright.ts";
|
|
12
12
|
import { isAuthMockEnabled } from "./auth-playwright.ts";
|
|
13
|
+
import { maskEmail } from "../core/logger.ts";
|
|
13
14
|
export interface DeleteChatsResult {
|
|
14
15
|
attempted: number;
|
|
15
16
|
succeeded: number;
|
|
16
17
|
mode: "accounts";
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
async function ensurePlaywrightSession(
|
|
20
|
+
async function ensurePlaywrightSession(
|
|
21
|
+
account: QwenAccount,
|
|
22
|
+
index = 1,
|
|
23
|
+
total = 1,
|
|
24
|
+
): Promise<void> {
|
|
20
25
|
if (isPlaywrightInitialized(account.id) || isAuthMockEnabled()) return;
|
|
21
26
|
|
|
22
27
|
const credentials = getAccountCredentials(account.id);
|
|
23
28
|
if (!credentials) {
|
|
24
|
-
throw new Error(`
|
|
29
|
+
throw new Error(`Credenciais da conta ${account.id} não encontradas.`);
|
|
25
30
|
}
|
|
26
31
|
|
|
27
32
|
console.log(
|
|
28
|
-
`[DeleteChats]
|
|
33
|
+
`[DeleteChats] [${index}/${total}] Abrindo navegador para ${maskEmail(account.email)}...`,
|
|
29
34
|
);
|
|
30
|
-
await initPlaywrightForAccount(credentials
|
|
35
|
+
await initPlaywrightForAccount(credentials, true, "chromium", {
|
|
36
|
+
skipHeaderCapture: true,
|
|
37
|
+
});
|
|
31
38
|
console.log(
|
|
32
|
-
`✅ [DeleteChats]
|
|
39
|
+
`✅ [DeleteChats] [${index}/${total}] Sessão pronta para ${maskEmail(account.email)}.`,
|
|
33
40
|
);
|
|
34
41
|
}
|
|
35
42
|
|
|
36
|
-
export async function deleteChatsForAccount(
|
|
37
|
-
|
|
38
|
-
|
|
43
|
+
export async function deleteChatsForAccount(
|
|
44
|
+
account: QwenAccount,
|
|
45
|
+
index = 1,
|
|
46
|
+
total = 1,
|
|
47
|
+
): Promise<boolean> {
|
|
48
|
+
await ensurePlaywrightSession(account, index, total);
|
|
49
|
+
console.log(
|
|
50
|
+
`🗑️ [DeleteChats] [${index}/${total}] Apagando conversas remotas de ${maskEmail(account.email)}...`,
|
|
51
|
+
);
|
|
52
|
+
const ok = await deleteAllQwenChats(account.id);
|
|
53
|
+
if (ok) {
|
|
54
|
+
console.log(
|
|
55
|
+
`✅ [DeleteChats] [${index}/${total}] Conversas apagadas com sucesso para ${maskEmail(account.email)}.`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
return ok;
|
|
39
59
|
}
|
|
40
60
|
|
|
41
61
|
export async function deleteChatsForAccountId(accountId: string): Promise<boolean> {
|
|
@@ -65,13 +85,16 @@ export async function deleteChatsForConfiguredAccounts(keepBrowserOpen = false):
|
|
|
65
85
|
let succeeded = 0;
|
|
66
86
|
|
|
67
87
|
try {
|
|
68
|
-
for (
|
|
88
|
+
for (let i = 0; i < accounts.length; i++) {
|
|
89
|
+
const account = accounts[i];
|
|
90
|
+
const currentIdx = i + 1;
|
|
91
|
+
const totalCount = accounts.length;
|
|
69
92
|
try {
|
|
70
|
-
const ok = await deleteChatsForAccount(account);
|
|
93
|
+
const ok = await deleteChatsForAccount(account, currentIdx, totalCount);
|
|
71
94
|
if (ok) succeeded++;
|
|
72
95
|
} catch (error) {
|
|
73
96
|
console.error(
|
|
74
|
-
|
|
97
|
+
`❌ [DeleteChats] [${currentIdx}/${totalCount}] Falha ao apagar conversas de ${maskEmail(account.email)}:`,
|
|
75
98
|
error instanceof Error ? error.message : String(error),
|
|
76
99
|
);
|
|
77
100
|
}
|
|
@@ -199,7 +199,11 @@ async function recoverStuckAccountMutex(
|
|
|
199
199
|
// A normal request can recover the browser on the next attempt. Avoid
|
|
200
200
|
// recursively scheduling another reset when the reset/close path itself was
|
|
201
201
|
// the operation that timed out.
|
|
202
|
-
if (
|
|
202
|
+
if (
|
|
203
|
+
!key.startsWith("profile-reset:") &&
|
|
204
|
+
!key.startsWith("close:") &&
|
|
205
|
+
!key.startsWith("init:")
|
|
206
|
+
) {
|
|
203
207
|
schedulePlaywrightProfileReset(accountId);
|
|
204
208
|
}
|
|
205
209
|
}
|
|
@@ -232,7 +236,7 @@ async function acquireAccountMutex(
|
|
|
232
236
|
const accountContexts = new Map<string, BrowserContext>();
|
|
233
237
|
const accountPages = new Map<string, Page>();
|
|
234
238
|
const cachedUserAgents = new Map<string, string>();
|
|
235
|
-
|
|
239
|
+
const inFlightAccountInits = new Map<string, Promise<void>>();
|
|
236
240
|
let sharedBrowser: Browser | null = null;
|
|
237
241
|
let sharedBrowserPromise: Promise<Browser> | null = null;
|
|
238
242
|
|
|
@@ -1286,6 +1290,7 @@ export async function initPlaywrightForAccount(
|
|
|
1286
1290
|
rawAccount: QwenAccount,
|
|
1287
1291
|
headless = true,
|
|
1288
1292
|
browserType: BrowserType = "chromium",
|
|
1293
|
+
options: { skipHeaderCapture?: boolean } = {},
|
|
1289
1294
|
): Promise<void> {
|
|
1290
1295
|
const account = await resolveAccountCredentials(rawAccount);
|
|
1291
1296
|
if (accountPages.has(account.id)) {
|
|
@@ -1294,12 +1299,19 @@ export async function initPlaywrightForAccount(
|
|
|
1294
1299
|
);
|
|
1295
1300
|
return;
|
|
1296
1301
|
}
|
|
1302
|
+
const existingInit = inFlightAccountInits.get(account.id);
|
|
1303
|
+
if (existingInit) {
|
|
1304
|
+
await existingInit;
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
1297
1307
|
|
|
1298
|
-
const
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1308
|
+
const initPromise = (async () => {
|
|
1309
|
+
const release = await acquireAccountMutex(
|
|
1310
|
+
account.id,
|
|
1311
|
+
`init:${account.id.substring(0, 12)}`,
|
|
1312
|
+
Math.max(PLAYWRIGHT_MUTEX_WAIT_MS, 120_000),
|
|
1313
|
+
);
|
|
1314
|
+
try {
|
|
1303
1315
|
// Double-check after acquiring lock
|
|
1304
1316
|
if (accountPages.has(account.id)) {
|
|
1305
1317
|
return;
|
|
@@ -1335,11 +1347,19 @@ export async function initPlaywrightForAccount(
|
|
|
1335
1347
|
|
|
1336
1348
|
let acctContext: BrowserContext;
|
|
1337
1349
|
try {
|
|
1338
|
-
acctContext = await
|
|
1350
|
+
acctContext = await withTimeout(
|
|
1351
|
+
engine.launchPersistentContext(profilePath, launchOptions),
|
|
1352
|
+
30_000,
|
|
1353
|
+
`O navegador não iniciou em 30s para a conta ${maskEmail(account.email)}. Se o QwenProxy estiver rodando em outro terminal, feche-o antes de executar este comando.`,
|
|
1354
|
+
);
|
|
1339
1355
|
} catch (launchErr: any) {
|
|
1340
1356
|
if (launchErr?.message?.includes("Executable doesn't exist")) {
|
|
1341
1357
|
autoInstallPlaywrightChromium();
|
|
1342
|
-
acctContext = await
|
|
1358
|
+
acctContext = await withTimeout(
|
|
1359
|
+
engine.launchPersistentContext(profilePath, launchOptions),
|
|
1360
|
+
30_000,
|
|
1361
|
+
`O navegador não iniciou em 30s para a conta ${maskEmail(account.email)}.`,
|
|
1362
|
+
);
|
|
1343
1363
|
} else {
|
|
1344
1364
|
throw launchErr;
|
|
1345
1365
|
}
|
|
@@ -1461,7 +1481,9 @@ export async function initPlaywrightForAccount(
|
|
|
1461
1481
|
);
|
|
1462
1482
|
throw validationError;
|
|
1463
1483
|
}
|
|
1464
|
-
|
|
1484
|
+
if (!options.skipHeaderCapture) {
|
|
1485
|
+
await captureQwenHeaders(account.id);
|
|
1486
|
+
}
|
|
1465
1487
|
|
|
1466
1488
|
// Header capture may leave the UI on a generated chat page. Return the
|
|
1467
1489
|
// primary tab to the canonical chat home.
|
|
@@ -1485,8 +1507,16 @@ export async function initPlaywrightForAccount(
|
|
|
1485
1507
|
cleanupPlaywrightAccountState(account.id);
|
|
1486
1508
|
throw error;
|
|
1487
1509
|
}
|
|
1510
|
+
} finally {
|
|
1511
|
+
release();
|
|
1512
|
+
}
|
|
1513
|
+
})();
|
|
1514
|
+
|
|
1515
|
+
inFlightAccountInits.set(account.id, initPromise);
|
|
1516
|
+
try {
|
|
1517
|
+
await initPromise;
|
|
1488
1518
|
} finally {
|
|
1489
|
-
|
|
1519
|
+
inFlightAccountInits.delete(account.id);
|
|
1490
1520
|
}
|
|
1491
1521
|
}
|
|
1492
1522
|
|
|
@@ -1540,11 +1570,19 @@ export async function validateAccountLogin(
|
|
|
1540
1570
|
|
|
1541
1571
|
let acctContext: BrowserContext;
|
|
1542
1572
|
try {
|
|
1543
|
-
acctContext = await
|
|
1573
|
+
acctContext = await withTimeout(
|
|
1574
|
+
engine.launchPersistentContext(profilePath, launchOptions),
|
|
1575
|
+
30_000,
|
|
1576
|
+
`O navegador não iniciou em 30s para a conta ${maskEmail(account.email)}. Se o QwenProxy estiver rodando em outro terminal, feche-o antes de executar este comando.`,
|
|
1577
|
+
);
|
|
1544
1578
|
} catch (launchErr: any) {
|
|
1545
1579
|
if (launchErr?.message?.includes("Executable doesn't exist")) {
|
|
1546
1580
|
autoInstallPlaywrightChromium();
|
|
1547
|
-
acctContext = await
|
|
1581
|
+
acctContext = await withTimeout(
|
|
1582
|
+
engine.launchPersistentContext(profilePath, launchOptions),
|
|
1583
|
+
30_000,
|
|
1584
|
+
`O navegador não iniciou em 30s para a conta ${maskEmail(account.email)}.`,
|
|
1585
|
+
);
|
|
1548
1586
|
} else {
|
|
1549
1587
|
throw launchErr;
|
|
1550
1588
|
}
|
package/src/services/qwen.ts
CHANGED
|
@@ -733,14 +733,15 @@ async function withQwenBrowserPage<T>(
|
|
|
733
733
|
operationTimeoutMs = config.timeouts.page,
|
|
734
734
|
recoverOnTimeout = true,
|
|
735
735
|
): Promise<T> {
|
|
736
|
-
//
|
|
737
|
-
//
|
|
738
|
-
//
|
|
739
|
-
|
|
740
|
-
const targetUrl = qwenUrl(
|
|
736
|
+
// When targetPath is omitted, any page under the Qwen origin can run the
|
|
737
|
+
// in-page evaluate/fetch without an expensive, stream-breaking page.goto.
|
|
738
|
+
// Only navigate when targetPath is explicitly provided or when the page is not
|
|
739
|
+
// on the Qwen origin yet (e.g. about:blank).
|
|
740
|
+
const targetUrl = qwenUrl(targetPath || "/");
|
|
741
741
|
const targetOrigin = new URL(targetUrl).origin;
|
|
742
|
-
const normalizedTargetPath =
|
|
743
|
-
new URL(targetUrl).pathname.replace(/\/+$/, "") || "/"
|
|
742
|
+
const normalizedTargetPath = targetPath
|
|
743
|
+
? new URL(targetUrl).pathname.replace(/\/+$/, "") || "/"
|
|
744
|
+
: null;
|
|
744
745
|
|
|
745
746
|
return withAccountPage(
|
|
746
747
|
accountId,
|
|
@@ -755,10 +756,11 @@ async function withQwenBrowserPage<T>(
|
|
|
755
756
|
// Navigate below when the current page has no usable URL.
|
|
756
757
|
}
|
|
757
758
|
|
|
758
|
-
|
|
759
|
+
const needsNavigation =
|
|
759
760
|
currentOrigin !== targetOrigin ||
|
|
760
|
-
(normalizedTargetPath && currentPath !== normalizedTargetPath)
|
|
761
|
-
|
|
761
|
+
(normalizedTargetPath !== null && currentPath !== normalizedTargetPath);
|
|
762
|
+
|
|
763
|
+
if (needsNavigation) {
|
|
762
764
|
await page.goto(targetUrl, {
|
|
763
765
|
waitUntil: "domcontentloaded",
|
|
764
766
|
timeout: Math.min(config.timeouts.navigation, operationTimeoutMs),
|
|
@@ -773,44 +775,7 @@ async function withQwenBrowserPage<T>(
|
|
|
773
775
|
);
|
|
774
776
|
}
|
|
775
777
|
|
|
776
|
-
|
|
777
|
-
accountId: string,
|
|
778
|
-
fn: (page: Page) => Promise<T>,
|
|
779
|
-
operationTimeoutMs = config.timeouts.page,
|
|
780
|
-
recoverOnTimeout = true,
|
|
781
|
-
): Promise<T> {
|
|
782
|
-
return withQwenBrowserPage(
|
|
783
|
-
accountId,
|
|
784
|
-
async (page) => {
|
|
785
|
-
try {
|
|
786
|
-
return await fn(page);
|
|
787
|
-
} finally {
|
|
788
|
-
if (!page.isClosed()) {
|
|
789
|
-
try {
|
|
790
|
-
const currentUrl = new URL(page.url());
|
|
791
|
-
const currentPath = currentUrl.pathname.replace(/\/+$/, "") || "/";
|
|
792
|
-
if (currentUrl.origin !== qwenOrigin() || currentPath !== "/") {
|
|
793
|
-
await page.goto(qwenUrl("/"), {
|
|
794
|
-
waitUntil: "domcontentloaded",
|
|
795
|
-
timeout: Math.min(config.timeouts.navigation, operationTimeoutMs),
|
|
796
|
-
});
|
|
797
|
-
}
|
|
798
|
-
} catch (error) {
|
|
799
|
-
// Do not mask the personalization request result if restoring the
|
|
800
|
-
// normal chat page fails; the next normal operation will retry it.
|
|
801
|
-
logger.warn("[Qwen] Could not restore chat page after personalization", {
|
|
802
|
-
accountId,
|
|
803
|
-
error: error instanceof Error ? error.message : String(error),
|
|
804
|
-
});
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
},
|
|
809
|
-
"/settings/personalization",
|
|
810
|
-
operationTimeoutMs,
|
|
811
|
-
recoverOnTimeout,
|
|
812
|
-
);
|
|
813
|
-
}
|
|
778
|
+
|
|
814
779
|
|
|
815
780
|
/**
|
|
816
781
|
* Build minimal headers for browser-side fetch. The browser automatically
|
|
@@ -1951,14 +1916,31 @@ function formatPublicQwenModel(model: Record<string, unknown>): PublicQwenModel
|
|
|
1951
1916
|
}
|
|
1952
1917
|
|
|
1953
1918
|
export async function deleteAllQwenChats(accountId?: string): Promise<boolean> {
|
|
1954
|
-
|
|
1919
|
+
let requestHeaders: Record<string, string>;
|
|
1920
|
+
if (isAuthMockEnabled()) {
|
|
1921
|
+
const { headers } = await getQwenHeaders(false, accountId);
|
|
1922
|
+
requestHeaders = buildCapturedQwenHeaders(headers, {
|
|
1923
|
+
referer: qwenUrl("/settings/chats"),
|
|
1924
|
+
});
|
|
1925
|
+
} else {
|
|
1926
|
+
// In live mode, requestQwenTextInBrowser executes inside the authenticated
|
|
1927
|
+
// browser page where session cookies are attached automatically.
|
|
1928
|
+
// Bypassing getQwenHeaders avoids triggering captureQwenHeaders (which sends
|
|
1929
|
+
// a dummy chat completion to intercept anti-fraud tokens not needed for deletions).
|
|
1930
|
+
requestHeaders = {
|
|
1931
|
+
source: "web",
|
|
1932
|
+
version: "0.2.89",
|
|
1933
|
+
timezone: new Date().toString().split(" (")[0],
|
|
1934
|
+
"x-request-id": crypto.randomUUID(),
|
|
1935
|
+
Referer: qwenUrl("/settings/chats"),
|
|
1936
|
+
};
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1955
1939
|
const response = await requestQwenTextInBrowser(
|
|
1956
1940
|
accountId,
|
|
1957
1941
|
"DELETE",
|
|
1958
1942
|
"/api/v2/chats/",
|
|
1959
|
-
|
|
1960
|
-
referer: qwenUrl("/settings/chats"),
|
|
1961
|
-
}),
|
|
1943
|
+
requestHeaders,
|
|
1962
1944
|
undefined,
|
|
1963
1945
|
{ referrer: qwenUrl("/settings/chats") },
|
|
1964
1946
|
);
|
package/src/tools/parser.ts
CHANGED
|
@@ -302,6 +302,14 @@ function advanceMarkdownCodeState(
|
|
|
302
302
|
let delimiterLength = initialDelimiterLength;
|
|
303
303
|
|
|
304
304
|
for (let i = 0; i < text.length;) {
|
|
305
|
+
// Inline code spans (1 or 2 backticks) cannot cross line breaks per CommonMark §6.1.
|
|
306
|
+
// Only fenced code blocks (3+ backticks) span multiple lines.
|
|
307
|
+
if (text[i] === "\n" && delimiterLength < 3) {
|
|
308
|
+
delimiterLength = 0;
|
|
309
|
+
i++;
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
|
|
305
313
|
if (text[i] !== "`") {
|
|
306
314
|
i++;
|
|
307
315
|
continue;
|
|
@@ -320,7 +328,6 @@ function advanceMarkdownCodeState(
|
|
|
320
328
|
|
|
321
329
|
i += runLength;
|
|
322
330
|
}
|
|
323
|
-
|
|
324
331
|
return delimiterLength;
|
|
325
332
|
}
|
|
326
333
|
|
|
@@ -331,6 +338,13 @@ function findNextToolOpenTagOutsideMarkdownCode(
|
|
|
331
338
|
let delimiterLength = initialDelimiterLength;
|
|
332
339
|
|
|
333
340
|
for (let i = 0; i < buffer.length;) {
|
|
341
|
+
// Inline code spans (1 or 2 backticks) cannot cross line breaks per CommonMark §6.1.
|
|
342
|
+
if (buffer[i] === "\n" && delimiterLength < 3) {
|
|
343
|
+
delimiterLength = 0;
|
|
344
|
+
i++;
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
|
|
334
348
|
if (buffer[i] === "`") {
|
|
335
349
|
let runLength = 1;
|
|
336
350
|
while (i + runLength < buffer.length && buffer[i + runLength] === "`") {
|
|
@@ -346,7 +360,6 @@ function findNextToolOpenTagOutsideMarkdownCode(
|
|
|
346
360
|
i += runLength;
|
|
347
361
|
continue;
|
|
348
362
|
}
|
|
349
|
-
|
|
350
363
|
if (delimiterLength === 0 && buffer[i] === "<") {
|
|
351
364
|
const sub = buffer.substring(i);
|
|
352
365
|
for (const name of getOpenNames()) {
|
|
@@ -371,6 +384,13 @@ function findPartialToolOpenIndexOutsideMarkdownCode(
|
|
|
371
384
|
const openNames = getOpenNames();
|
|
372
385
|
|
|
373
386
|
for (let i = 0; i < buffer.length;) {
|
|
387
|
+
// Inline code spans (1 or 2 backticks) cannot cross line breaks per CommonMark §6.1.
|
|
388
|
+
if (buffer[i] === "\n" && delimiterLength < 3) {
|
|
389
|
+
delimiterLength = 0;
|
|
390
|
+
i++;
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
|
|
374
394
|
if (buffer[i] === "`") {
|
|
375
395
|
let runLength = 1;
|
|
376
396
|
while (i + runLength < buffer.length && buffer[i + runLength] === "`") {
|
|
@@ -386,7 +406,6 @@ function findPartialToolOpenIndexOutsideMarkdownCode(
|
|
|
386
406
|
i += runLength;
|
|
387
407
|
continue;
|
|
388
408
|
}
|
|
389
|
-
|
|
390
409
|
if (delimiterLength === 0 && buffer[i] === "<") {
|
|
391
410
|
const tailLower = buffer.substring(i).toLowerCase();
|
|
392
411
|
if (!tailLower.includes(">")) {
|