qwenproxy-cli 1.0.20 → 1.0.22
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/playwright.ts +120 -16
- package/src/services/qwen.ts +13 -48
- 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.22",
|
|
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, {
|
|
@@ -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
|
|
|
@@ -308,8 +312,17 @@ async function hasValidAuthCookie(context: BrowserContext, timeoutMs = 3_000): P
|
|
|
308
312
|
* Detect whether the page is authenticated.
|
|
309
313
|
* Probes the authoritative /api/v1/auths/ endpoint from the page context (verified via HAR forensics)
|
|
310
314
|
* and falls back to inspecting the presence of visible "Log in" / "Sign up" buttons.
|
|
315
|
+
*
|
|
316
|
+
* The probe is bounded: page.evaluate ignores Playwright's default timeouts, so a
|
|
317
|
+
* WAF-blocked page whose in-page fetch never settles would hang the caller with no
|
|
318
|
+
* error at all. A probe that cannot answer is treated as "not logged in" — the
|
|
319
|
+
* caller re-authenticates or reloads, which is strictly better than burning the
|
|
320
|
+
* whole header budget waiting on a frozen page.
|
|
311
321
|
*/
|
|
312
|
-
export async function isPageLoggedIn(
|
|
322
|
+
export async function isPageLoggedIn(
|
|
323
|
+
page: Page,
|
|
324
|
+
timeoutMs = SESSION_PROBE_NAVIGATION_TIMEOUT_MS,
|
|
325
|
+
): Promise<boolean> {
|
|
313
326
|
if (!page) return false;
|
|
314
327
|
if (typeof page.isClosed === "function" && page.isClosed()) return false;
|
|
315
328
|
try {
|
|
@@ -317,7 +330,7 @@ export async function isPageLoggedIn(page: Page): Promise<boolean> {
|
|
|
317
330
|
if (url.includes("/auth") || url.includes("/login")) return false;
|
|
318
331
|
if (typeof page.evaluate !== "function") return true;
|
|
319
332
|
|
|
320
|
-
|
|
333
|
+
const probe = page
|
|
321
334
|
.evaluate(async () => {
|
|
322
335
|
try {
|
|
323
336
|
const res = await fetch("/api/v1/auths/", { method: "GET" });
|
|
@@ -330,6 +343,12 @@ export async function isPageLoggedIn(page: Page): Promise<boolean> {
|
|
|
330
343
|
}
|
|
331
344
|
})
|
|
332
345
|
.catch(() => false);
|
|
346
|
+
|
|
347
|
+
return await withTimeout(
|
|
348
|
+
probe,
|
|
349
|
+
Math.max(1_000, timeoutMs),
|
|
350
|
+
`session probe timed out after ${timeoutMs}ms`,
|
|
351
|
+
);
|
|
333
352
|
} catch {
|
|
334
353
|
return false;
|
|
335
354
|
}
|
|
@@ -456,6 +475,16 @@ const FIRST_TRIGGER_GRACE_MS = 3_000;
|
|
|
456
475
|
* sends cover it while still failing a page that never produces them.
|
|
457
476
|
*/
|
|
458
477
|
const HEADER_CAPTURE_TRIGGER_ATTEMPTS = 3;
|
|
478
|
+
/**
|
|
479
|
+
* A healthy Qwen chat page renders its input within a couple of seconds. When
|
|
480
|
+
* it never does, the page is blocked (WAF interstitial, punish document, or a
|
|
481
|
+
* cold SPA that failed to hydrate) and page.focus would wait out the 60s page
|
|
482
|
+
* default, freezing the whole capture. Bound the wait well under the header
|
|
483
|
+
* budget so a stuck page reloads and retries instead of hanging.
|
|
484
|
+
*/
|
|
485
|
+
const CHAT_INPUT_APPEAR_TIMEOUT_MS = 15_000;
|
|
486
|
+
/** Per-action bound for focus/fill/type once the input is already visible. */
|
|
487
|
+
const CHAT_INPUT_ACTION_TIMEOUT_MS = 10_000;
|
|
459
488
|
|
|
460
489
|
/**
|
|
461
490
|
* A challenge blocking the chat page makes the send button inert, so header
|
|
@@ -1269,12 +1298,19 @@ export async function initPlaywrightForAccount(
|
|
|
1269
1298
|
);
|
|
1270
1299
|
return;
|
|
1271
1300
|
}
|
|
1301
|
+
const existingInit = inFlightAccountInits.get(account.id);
|
|
1302
|
+
if (existingInit) {
|
|
1303
|
+
await existingInit;
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1272
1306
|
|
|
1273
|
-
const
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1307
|
+
const initPromise = (async () => {
|
|
1308
|
+
const release = await acquireAccountMutex(
|
|
1309
|
+
account.id,
|
|
1310
|
+
`init:${account.id.substring(0, 12)}`,
|
|
1311
|
+
Math.max(PLAYWRIGHT_MUTEX_WAIT_MS, 120_000),
|
|
1312
|
+
);
|
|
1313
|
+
try {
|
|
1278
1314
|
// Double-check after acquiring lock
|
|
1279
1315
|
if (accountPages.has(account.id)) {
|
|
1280
1316
|
return;
|
|
@@ -1460,8 +1496,16 @@ export async function initPlaywrightForAccount(
|
|
|
1460
1496
|
cleanupPlaywrightAccountState(account.id);
|
|
1461
1497
|
throw error;
|
|
1462
1498
|
}
|
|
1499
|
+
} finally {
|
|
1500
|
+
release();
|
|
1501
|
+
}
|
|
1502
|
+
})();
|
|
1503
|
+
|
|
1504
|
+
inFlightAccountInits.set(account.id, initPromise);
|
|
1505
|
+
try {
|
|
1506
|
+
await initPromise;
|
|
1463
1507
|
} finally {
|
|
1464
|
-
|
|
1508
|
+
inFlightAccountInits.delete(account.id);
|
|
1465
1509
|
}
|
|
1466
1510
|
}
|
|
1467
1511
|
|
|
@@ -1992,6 +2036,7 @@ export async function captureQwenHeaders(
|
|
|
1992
2036
|
let headersCaptured = false;
|
|
1993
2037
|
let retriggerRequested = false;
|
|
1994
2038
|
let lastAttemptGraceTimedOut = false;
|
|
2039
|
+
let lastAttemptInputMissing = false;
|
|
1995
2040
|
let graceTimeoutCount = 0;
|
|
1996
2041
|
let wakeTriggerLoop: (() => void) | undefined;
|
|
1997
2042
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -2199,7 +2244,10 @@ export async function captureQwenHeaders(
|
|
|
2199
2244
|
// burn every trigger attempt on a textarea that does not exist. Re-login
|
|
2200
2245
|
// immediately when credentials are available; otherwise fail fast with a
|
|
2201
2246
|
// clear diagnosis instead of 3 pointless grace timeouts.
|
|
2202
|
-
const loggedIn = await isPageLoggedIn(
|
|
2247
|
+
const loggedIn = await isPageLoggedIn(
|
|
2248
|
+
page,
|
|
2249
|
+
Math.max(1_000, Math.min(remainingBudgetMs(), SESSION_PROBE_NAVIGATION_TIMEOUT_MS)),
|
|
2250
|
+
);
|
|
2203
2251
|
if (!loggedIn) {
|
|
2204
2252
|
const { getAccountCredentials } = await import("../core/accounts.ts");
|
|
2205
2253
|
const creds = getAccountCredentials(accountId);
|
|
@@ -2237,11 +2285,59 @@ export async function captureQwenHeaders(
|
|
|
2237
2285
|
// Mirrors upstream 5b3fd3e (robust account header capture).
|
|
2238
2286
|
const inputSelector =
|
|
2239
2287
|
'textarea.message-input-textarea:visible, textarea:visible, [contenteditable="true"]:visible';
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2288
|
+
// Bound the appearance wait: a page that never renders the chat input is
|
|
2289
|
+
// blocked (WAF interstitial, punish document, or failed SPA hydration).
|
|
2290
|
+
// Unbounded, page.focus would burn its 60s default timeout on every
|
|
2291
|
+
// attempt, freezing the whole capture and cooling a healthy account with
|
|
2292
|
+
// AuthInitFailed. A miss marks the attempt for a reload instead.
|
|
2293
|
+
try {
|
|
2294
|
+
await page
|
|
2295
|
+
.locator(inputSelector)
|
|
2296
|
+
.first()
|
|
2297
|
+
.waitFor({
|
|
2298
|
+
state: "visible",
|
|
2299
|
+
timeout: Math.max(
|
|
2300
|
+
1,
|
|
2301
|
+
Math.min(CHAT_INPUT_APPEAR_TIMEOUT_MS, remainingBudgetMs()),
|
|
2302
|
+
),
|
|
2303
|
+
});
|
|
2304
|
+
} catch {
|
|
2305
|
+
if (settled || page.isClosed()) return;
|
|
2306
|
+
console.warn(
|
|
2307
|
+
`⏱️ [Playwright] Chat input never appeared for ${accountId} (attempt ${attempt}); reloading`,
|
|
2308
|
+
);
|
|
2309
|
+
lastAttemptInputMissing = true;
|
|
2310
|
+
retriggerRequested = true;
|
|
2311
|
+
wakeTrigger();
|
|
2312
|
+
return;
|
|
2313
|
+
}
|
|
2243
2314
|
if (settled || page.isClosed()) return;
|
|
2244
|
-
|
|
2315
|
+
const inputActionTimeoutMs = Math.max(
|
|
2316
|
+
1,
|
|
2317
|
+
Math.min(CHAT_INPUT_ACTION_TIMEOUT_MS, remainingBudgetMs()),
|
|
2318
|
+
);
|
|
2319
|
+
try {
|
|
2320
|
+
await page.focus(inputSelector, { timeout: inputActionTimeoutMs });
|
|
2321
|
+
if (settled || page.isClosed()) return;
|
|
2322
|
+
await page.fill(inputSelector, "", { timeout: inputActionTimeoutMs });
|
|
2323
|
+
if (settled || page.isClosed()) return;
|
|
2324
|
+
await page.type(inputSelector, "a", {
|
|
2325
|
+
delay: 100,
|
|
2326
|
+
timeout: inputActionTimeoutMs,
|
|
2327
|
+
});
|
|
2328
|
+
} catch {
|
|
2329
|
+
// The input detached mid-interaction (challenge overlay, SPA
|
|
2330
|
+
// re-render): same diagnosis as never appearing — only a fresh load
|
|
2331
|
+
// recovers it.
|
|
2332
|
+
if (settled || page.isClosed()) return;
|
|
2333
|
+
console.warn(
|
|
2334
|
+
`⏱️ [Playwright] Chat input interaction failed for ${accountId} (attempt ${attempt}); reloading`,
|
|
2335
|
+
);
|
|
2336
|
+
lastAttemptInputMissing = true;
|
|
2337
|
+
retriggerRequested = true;
|
|
2338
|
+
wakeTrigger();
|
|
2339
|
+
return;
|
|
2340
|
+
}
|
|
2245
2341
|
if (settled || page.isClosed()) return;
|
|
2246
2342
|
await sleep(2000);
|
|
2247
2343
|
if (settled || page.isClosed()) return;
|
|
@@ -2309,8 +2405,16 @@ export async function captureQwenHeaders(
|
|
|
2309
2405
|
// no request (indicating a stuck page/challenge that needs a fresh load).
|
|
2310
2406
|
// Attempt 2 preserves the page from attempt 1 so the bx SDK that just
|
|
2311
2407
|
// finished initializing in the background is not thrown away.
|
|
2312
|
-
|
|
2408
|
+
// A missing chat input is the exception: the page never rendered the
|
|
2409
|
+
// chat UI, so there is no warm SDK state to protect and only a fresh
|
|
2410
|
+
// load can recover it.
|
|
2411
|
+
if (
|
|
2412
|
+
attempt === 1 ||
|
|
2413
|
+
lastAttemptInputMissing ||
|
|
2414
|
+
(lastAttemptGraceTimedOut && attempt >= 3)
|
|
2415
|
+
) {
|
|
2313
2416
|
lastAttemptGraceTimedOut = false;
|
|
2417
|
+
lastAttemptInputMissing = false;
|
|
2314
2418
|
await openChatPage();
|
|
2315
2419
|
}
|
|
2316
2420
|
if (settled) return;
|
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
|
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(">")) {
|