qwenproxy-cli 1.6.5 → 1.6.7
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/dist/clean-cache.js +17 -2
- package/dist/delete-chats.js +89 -4
- package/dist/index.js +113 -24
- package/dist/login.js +17 -2
- package/dist/reset-cooldowns.js +17 -2
- package/package.json +1 -1
- package/src/core/model-alias.ts +6 -0
- package/src/core/process-guard.ts +8 -1
- package/src/core/version.ts +1 -1
- package/src/routes/chat/streaming.ts +35 -22
- package/src/services/captcha-coordinator.ts +7 -0
- package/src/services/playwright.ts +18 -5
- package/src/services/qwen.ts +71 -11
package/dist/clean-cache.js
CHANGED
|
@@ -3588,6 +3588,15 @@ async function initPlaywrightForAccount(rawAccount, headless = config.playwright
|
|
|
3588
3588
|
await existingInit;
|
|
3589
3589
|
return;
|
|
3590
3590
|
}
|
|
3591
|
+
let resolveInit;
|
|
3592
|
+
let rejectInit;
|
|
3593
|
+
const inFlightDeferred = new Promise((res, rej) => {
|
|
3594
|
+
resolveInit = res;
|
|
3595
|
+
rejectInit = rej;
|
|
3596
|
+
});
|
|
3597
|
+
inFlightDeferred.catch(() => {
|
|
3598
|
+
});
|
|
3599
|
+
inFlightAccountInits.set(account.id, inFlightDeferred);
|
|
3591
3600
|
const initPromise = (async () => {
|
|
3592
3601
|
const release = await acquireAccountMutex(
|
|
3593
3602
|
account.id,
|
|
@@ -3872,9 +3881,12 @@ async function initPlaywrightForAccount(rawAccount, headless = config.playwright
|
|
|
3872
3881
|
release();
|
|
3873
3882
|
}
|
|
3874
3883
|
})();
|
|
3875
|
-
inFlightAccountInits.set(account.id, initPromise);
|
|
3876
3884
|
try {
|
|
3877
3885
|
await initPromise;
|
|
3886
|
+
resolveInit();
|
|
3887
|
+
} catch (error) {
|
|
3888
|
+
rejectInit(error);
|
|
3889
|
+
throw error;
|
|
3878
3890
|
} finally {
|
|
3879
3891
|
inFlightAccountInits.delete(account.id);
|
|
3880
3892
|
}
|
|
@@ -5481,7 +5493,10 @@ function isPlaywrightAlreadyClosedMessage(message) {
|
|
|
5481
5493
|
return message.includes("Target page, context or browser has been closed") || message.includes("Browser has been closed") || message.includes("Target closed") || message.includes("Target crashed") || message.includes("Page crashed") || message.includes("Assertion error") || message.includes("Cannot find parent object") || message.includes("Connection closed") || message.includes("session closed") || message.includes("Session closed") || message.includes("Network.setCacheDisabled") || message.includes("Protocol error") || message.includes("storageState timed out");
|
|
5482
5494
|
}
|
|
5483
5495
|
function isBenignTeardownError(error) {
|
|
5484
|
-
if (!error) return
|
|
5496
|
+
if (!error) return true;
|
|
5497
|
+
if (typeof error === "object" && Object.keys(error).length === 0 && !(error instanceof Error)) {
|
|
5498
|
+
return true;
|
|
5499
|
+
}
|
|
5485
5500
|
const message = error instanceof Error ? error.message : typeof error === "object" && "message" in error ? String(error.message) : String(error);
|
|
5486
5501
|
return isPlaywrightAlreadyClosedMessage(message);
|
|
5487
5502
|
}
|
package/dist/delete-chats.js
CHANGED
|
@@ -19,7 +19,10 @@ function isPlaywrightAlreadyClosedMessage(message) {
|
|
|
19
19
|
return message.includes("Target page, context or browser has been closed") || message.includes("Browser has been closed") || message.includes("Target closed") || message.includes("Target crashed") || message.includes("Page crashed") || message.includes("Assertion error") || message.includes("Cannot find parent object") || message.includes("Connection closed") || message.includes("session closed") || message.includes("Session closed") || message.includes("Network.setCacheDisabled") || message.includes("Protocol error") || message.includes("storageState timed out");
|
|
20
20
|
}
|
|
21
21
|
function isBenignTeardownError(error) {
|
|
22
|
-
if (!error) return
|
|
22
|
+
if (!error) return true;
|
|
23
|
+
if (typeof error === "object" && Object.keys(error).length === 0 && !(error instanceof Error)) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
23
26
|
const message = error instanceof Error ? error.message : typeof error === "object" && "message" in error ? String(error.message) : String(error);
|
|
24
27
|
return isPlaywrightAlreadyClosedMessage(message);
|
|
25
28
|
}
|
|
@@ -4447,6 +4450,15 @@ async function initPlaywrightForAccount(rawAccount, headless = config.playwright
|
|
|
4447
4450
|
await existingInit;
|
|
4448
4451
|
return;
|
|
4449
4452
|
}
|
|
4453
|
+
let resolveInit;
|
|
4454
|
+
let rejectInit;
|
|
4455
|
+
const inFlightDeferred = new Promise((res, rej) => {
|
|
4456
|
+
resolveInit = res;
|
|
4457
|
+
rejectInit = rej;
|
|
4458
|
+
});
|
|
4459
|
+
inFlightDeferred.catch(() => {
|
|
4460
|
+
});
|
|
4461
|
+
inFlightAccountInits.set(account.id, inFlightDeferred);
|
|
4450
4462
|
const initPromise = (async () => {
|
|
4451
4463
|
const release = await acquireAccountMutex(
|
|
4452
4464
|
account.id,
|
|
@@ -4731,9 +4743,12 @@ async function initPlaywrightForAccount(rawAccount, headless = config.playwright
|
|
|
4731
4743
|
release();
|
|
4732
4744
|
}
|
|
4733
4745
|
})();
|
|
4734
|
-
inFlightAccountInits.set(account.id, initPromise);
|
|
4735
4746
|
try {
|
|
4736
4747
|
await initPromise;
|
|
4748
|
+
resolveInit();
|
|
4749
|
+
} catch (error) {
|
|
4750
|
+
rejectInit(error);
|
|
4751
|
+
throw error;
|
|
4737
4752
|
} finally {
|
|
4738
4753
|
inFlightAccountInits.delete(account.id);
|
|
4739
4754
|
}
|
|
@@ -7370,6 +7385,10 @@ function stripThinkingSuffix(model) {
|
|
|
7370
7385
|
reasoningMode: "thinking"
|
|
7371
7386
|
};
|
|
7372
7387
|
}
|
|
7388
|
+
const lower = normalizedModel.toLowerCase();
|
|
7389
|
+
if (lower.includes("omni-flash") || lower.includes("turbo")) {
|
|
7390
|
+
return { baseModel: normalizedModel, enableThinking: false, reasoningMode: "fast" };
|
|
7391
|
+
}
|
|
7373
7392
|
return { baseModel: normalizedModel, enableThinking: true, reasoningMode: "auto" };
|
|
7374
7393
|
}
|
|
7375
7394
|
function mapKnownModelAlias(model) {
|
|
@@ -7737,6 +7756,14 @@ async function recoverBaxiaCaptcha(accountId, label, options = {}) {
|
|
|
7737
7756
|
);
|
|
7738
7757
|
const challengeUrl = options.challengeBody ? extractBaxiaChallengeUrl(options.challengeBody, config.qwen.baseUrl) : null;
|
|
7739
7758
|
activeCaptchaRecoveries.add(accountId);
|
|
7759
|
+
try {
|
|
7760
|
+
const { abortActiveBrowserStreamsForAccount: abortActiveBrowserStreamsForAccount2 } = await Promise.resolve().then(() => (init_qwen(), qwen_exports));
|
|
7761
|
+
abortActiveBrowserStreamsForAccount2(
|
|
7762
|
+
accountId,
|
|
7763
|
+
"Account stream interrupted by captcha challenge recovery"
|
|
7764
|
+
);
|
|
7765
|
+
} catch {
|
|
7766
|
+
}
|
|
7740
7767
|
try {
|
|
7741
7768
|
const solved = await withAccountPage(
|
|
7742
7769
|
accountId,
|
|
@@ -8513,6 +8540,7 @@ __export(qwen_exports, {
|
|
|
8513
8540
|
QwenUpstreamUnavailableError: () => QwenUpstreamUnavailableError,
|
|
8514
8541
|
RetryableQwenStreamError: () => RetryableQwenStreamError,
|
|
8515
8542
|
STREAM_SLOT_MAX_HOLD_MS: () => STREAM_SLOT_MAX_HOLD_MS,
|
|
8543
|
+
abortActiveBrowserStreamsForAccount: () => abortActiveBrowserStreamsForAccount,
|
|
8516
8544
|
acquireAccountStreamLock: () => acquireAccountStreamLock,
|
|
8517
8545
|
acquireNewQwenChatSession: () => acquireNewQwenChatSession,
|
|
8518
8546
|
buildCapturedQwenHeaders: () => buildCapturedQwenHeaders,
|
|
@@ -8559,6 +8587,27 @@ __export(qwen_exports, {
|
|
|
8559
8587
|
});
|
|
8560
8588
|
import crypto5 from "crypto";
|
|
8561
8589
|
import { v4 as uuidv43 } from "uuid";
|
|
8590
|
+
function abortActiveBrowserStreamsForAccount(accountId, reason = "Account browser stream interrupted (page context unloaded)") {
|
|
8591
|
+
let count = 0;
|
|
8592
|
+
for (const [requestId, state] of browserStreamStates) {
|
|
8593
|
+
if (state.accountId === accountId && !state.done) {
|
|
8594
|
+
count++;
|
|
8595
|
+
handleBrowserStreamEvent(requestId, {
|
|
8596
|
+
type: "error",
|
|
8597
|
+
message: reason,
|
|
8598
|
+
errorName: "AbortError"
|
|
8599
|
+
});
|
|
8600
|
+
}
|
|
8601
|
+
}
|
|
8602
|
+
if (count > 0) {
|
|
8603
|
+
logger.debug("[Qwen] Aborted active browser streams for account", {
|
|
8604
|
+
accountId,
|
|
8605
|
+
count,
|
|
8606
|
+
reason
|
|
8607
|
+
});
|
|
8608
|
+
}
|
|
8609
|
+
return count;
|
|
8610
|
+
}
|
|
8562
8611
|
async function registerBrowserContextStreamBinding(context) {
|
|
8563
8612
|
if (browserStreamBindingContexts.has(context)) return;
|
|
8564
8613
|
browserStreamBindingContexts.add(context);
|
|
@@ -9253,6 +9302,7 @@ async function createQwenBrowserResponse(accountId, url, method, headers, body,
|
|
|
9253
9302
|
}
|
|
9254
9303
|
const requestId = uuidv43();
|
|
9255
9304
|
const state = {
|
|
9305
|
+
accountId,
|
|
9256
9306
|
chunks: [],
|
|
9257
9307
|
done: false,
|
|
9258
9308
|
error: null,
|
|
@@ -9269,10 +9319,15 @@ async function createQwenBrowserResponse(accountId, url, method, headers, body,
|
|
|
9269
9319
|
let settled = false;
|
|
9270
9320
|
let abortListener;
|
|
9271
9321
|
let cancelPromise;
|
|
9322
|
+
let pageCleanup;
|
|
9272
9323
|
const cleanup = () => {
|
|
9273
9324
|
if (settled) return;
|
|
9274
9325
|
settled = true;
|
|
9275
9326
|
if (abortListener) signal.removeEventListener("abort", abortListener);
|
|
9327
|
+
try {
|
|
9328
|
+
pageCleanup?.();
|
|
9329
|
+
} catch {
|
|
9330
|
+
}
|
|
9276
9331
|
browserStreamStates.delete(requestId);
|
|
9277
9332
|
};
|
|
9278
9333
|
const cancel = () => {
|
|
@@ -9306,6 +9361,35 @@ async function createQwenBrowserResponse(accountId, url, method, headers, body,
|
|
|
9306
9361
|
accountId,
|
|
9307
9362
|
async (page) => {
|
|
9308
9363
|
await ensureBrowserStreamBinding(page);
|
|
9364
|
+
if (typeof page.on === "function") {
|
|
9365
|
+
const onFrameNavigated = (frame) => {
|
|
9366
|
+
if (!settled && typeof page.mainFrame === "function" && frame === page.mainFrame()) {
|
|
9367
|
+
handleBrowserStreamEvent(requestId, {
|
|
9368
|
+
type: "error",
|
|
9369
|
+
message: "Qwen browser page navigated during stream (session interrupted)",
|
|
9370
|
+
errorName: "AbortError"
|
|
9371
|
+
});
|
|
9372
|
+
}
|
|
9373
|
+
};
|
|
9374
|
+
const onPageClosed = () => {
|
|
9375
|
+
if (!settled) {
|
|
9376
|
+
handleBrowserStreamEvent(requestId, {
|
|
9377
|
+
type: "error",
|
|
9378
|
+
message: "Qwen browser page closed during stream",
|
|
9379
|
+
errorName: "AbortError"
|
|
9380
|
+
});
|
|
9381
|
+
}
|
|
9382
|
+
};
|
|
9383
|
+
page.on("framenavigated", onFrameNavigated);
|
|
9384
|
+
page.on("close", onPageClosed);
|
|
9385
|
+
pageCleanup = () => {
|
|
9386
|
+
try {
|
|
9387
|
+
page.off?.("framenavigated", onFrameNavigated);
|
|
9388
|
+
page.off?.("close", onPageClosed);
|
|
9389
|
+
} catch {
|
|
9390
|
+
}
|
|
9391
|
+
};
|
|
9392
|
+
}
|
|
9309
9393
|
if (config.captcha.enabled) {
|
|
9310
9394
|
captchaWatcher = startBaxiaCaptchaWatcher(
|
|
9311
9395
|
page,
|
|
@@ -10490,7 +10574,7 @@ async function createQwenStreamInternal(prompt, enableThinking, modelId, forcedP
|
|
|
10490
10574
|
}
|
|
10491
10575
|
});
|
|
10492
10576
|
}
|
|
10493
|
-
const baseTimeoutMs =
|
|
10577
|
+
const baseTimeoutMs = config.timeouts.idleStreamTimeout;
|
|
10494
10578
|
const dynamicIdleTimeoutMs = computeDynamicIdleTimeout({
|
|
10495
10579
|
enableThinking,
|
|
10496
10580
|
parallelEscape: options?.parallelEscape,
|
|
@@ -10506,9 +10590,10 @@ async function createQwenStreamInternal(prompt, enableThinking, modelId, forcedP
|
|
|
10506
10590
|
dynamicTimeout: dynamicIdleTimeoutMs
|
|
10507
10591
|
});
|
|
10508
10592
|
const firstChunkDeadlineMs = enableThinking ? Math.max(
|
|
10593
|
+
config.timeouts.reasoningModelTimeout,
|
|
10509
10594
|
config.timeouts.firstChunkTimeout,
|
|
10510
10595
|
config.timeouts.timeToFirstByte
|
|
10511
|
-
) :
|
|
10596
|
+
) : Math.max(config.timeouts.firstChunkTimeout, config.timeouts.timeToFirstByte);
|
|
10512
10597
|
return addIdleTimeoutToStream(
|
|
10513
10598
|
stream,
|
|
10514
10599
|
controller2,
|
package/dist/index.js
CHANGED
|
@@ -156,7 +156,10 @@ function isPlaywrightAlreadyClosedMessage(message) {
|
|
|
156
156
|
return message.includes("Target page, context or browser has been closed") || message.includes("Browser has been closed") || message.includes("Target closed") || message.includes("Target crashed") || message.includes("Page crashed") || message.includes("Assertion error") || message.includes("Cannot find parent object") || message.includes("Connection closed") || message.includes("session closed") || message.includes("Session closed") || message.includes("Network.setCacheDisabled") || message.includes("Protocol error") || message.includes("storageState timed out");
|
|
157
157
|
}
|
|
158
158
|
function isBenignTeardownError(error) {
|
|
159
|
-
if (!error) return
|
|
159
|
+
if (!error) return true;
|
|
160
|
+
if (typeof error === "object" && Object.keys(error).length === 0 && !(error instanceof Error)) {
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
160
163
|
const message = error instanceof Error ? error.message : typeof error === "object" && "message" in error ? String(error.message) : String(error);
|
|
161
164
|
return isPlaywrightAlreadyClosedMessage(message);
|
|
162
165
|
}
|
|
@@ -5557,6 +5560,15 @@ async function initPlaywrightForAccount(rawAccount, headless = config.playwright
|
|
|
5557
5560
|
await existingInit;
|
|
5558
5561
|
return;
|
|
5559
5562
|
}
|
|
5563
|
+
let resolveInit;
|
|
5564
|
+
let rejectInit;
|
|
5565
|
+
const inFlightDeferred = new Promise((res, rej) => {
|
|
5566
|
+
resolveInit = res;
|
|
5567
|
+
rejectInit = rej;
|
|
5568
|
+
});
|
|
5569
|
+
inFlightDeferred.catch(() => {
|
|
5570
|
+
});
|
|
5571
|
+
inFlightAccountInits.set(account.id, inFlightDeferred);
|
|
5560
5572
|
const initPromise = (async () => {
|
|
5561
5573
|
const release = await acquireAccountMutex(
|
|
5562
5574
|
account.id,
|
|
@@ -5841,9 +5853,12 @@ async function initPlaywrightForAccount(rawAccount, headless = config.playwright
|
|
|
5841
5853
|
release();
|
|
5842
5854
|
}
|
|
5843
5855
|
})();
|
|
5844
|
-
inFlightAccountInits.set(account.id, initPromise);
|
|
5845
5856
|
try {
|
|
5846
5857
|
await initPromise;
|
|
5858
|
+
resolveInit();
|
|
5859
|
+
} catch (error) {
|
|
5860
|
+
rejectInit(error);
|
|
5861
|
+
throw error;
|
|
5847
5862
|
} finally {
|
|
5848
5863
|
inFlightAccountInits.delete(account.id);
|
|
5849
5864
|
}
|
|
@@ -8718,6 +8733,10 @@ function stripThinkingSuffix(model) {
|
|
|
8718
8733
|
reasoningMode: "thinking"
|
|
8719
8734
|
};
|
|
8720
8735
|
}
|
|
8736
|
+
const lower = normalizedModel.toLowerCase();
|
|
8737
|
+
if (lower.includes("omni-flash") || lower.includes("turbo")) {
|
|
8738
|
+
return { baseModel: normalizedModel, enableThinking: false, reasoningMode: "fast" };
|
|
8739
|
+
}
|
|
8721
8740
|
return { baseModel: normalizedModel, enableThinking: true, reasoningMode: "auto" };
|
|
8722
8741
|
}
|
|
8723
8742
|
function mapKnownModelAlias(model) {
|
|
@@ -9127,6 +9146,14 @@ async function recoverBaxiaCaptcha(accountId, label, options = {}) {
|
|
|
9127
9146
|
);
|
|
9128
9147
|
const challengeUrl = options.challengeBody ? extractBaxiaChallengeUrl(options.challengeBody, config.qwen.baseUrl) : null;
|
|
9129
9148
|
activeCaptchaRecoveries.add(accountId);
|
|
9149
|
+
try {
|
|
9150
|
+
const { abortActiveBrowserStreamsForAccount: abortActiveBrowserStreamsForAccount2 } = await Promise.resolve().then(() => (init_qwen(), qwen_exports));
|
|
9151
|
+
abortActiveBrowserStreamsForAccount2(
|
|
9152
|
+
accountId,
|
|
9153
|
+
"Account stream interrupted by captcha challenge recovery"
|
|
9154
|
+
);
|
|
9155
|
+
} catch {
|
|
9156
|
+
}
|
|
9130
9157
|
try {
|
|
9131
9158
|
const solved = await withAccountPage(
|
|
9132
9159
|
accountId,
|
|
@@ -11703,6 +11730,7 @@ __export(qwen_exports, {
|
|
|
11703
11730
|
QwenUpstreamUnavailableError: () => QwenUpstreamUnavailableError,
|
|
11704
11731
|
RetryableQwenStreamError: () => RetryableQwenStreamError,
|
|
11705
11732
|
STREAM_SLOT_MAX_HOLD_MS: () => STREAM_SLOT_MAX_HOLD_MS,
|
|
11733
|
+
abortActiveBrowserStreamsForAccount: () => abortActiveBrowserStreamsForAccount,
|
|
11706
11734
|
acquireAccountStreamLock: () => acquireAccountStreamLock,
|
|
11707
11735
|
acquireNewQwenChatSession: () => acquireNewQwenChatSession,
|
|
11708
11736
|
buildCapturedQwenHeaders: () => buildCapturedQwenHeaders,
|
|
@@ -11749,6 +11777,27 @@ __export(qwen_exports, {
|
|
|
11749
11777
|
});
|
|
11750
11778
|
import crypto7 from "crypto";
|
|
11751
11779
|
import { v4 as uuidv43 } from "uuid";
|
|
11780
|
+
function abortActiveBrowserStreamsForAccount(accountId, reason = "Account browser stream interrupted (page context unloaded)") {
|
|
11781
|
+
let count = 0;
|
|
11782
|
+
for (const [requestId, state] of browserStreamStates) {
|
|
11783
|
+
if (state.accountId === accountId && !state.done) {
|
|
11784
|
+
count++;
|
|
11785
|
+
handleBrowserStreamEvent(requestId, {
|
|
11786
|
+
type: "error",
|
|
11787
|
+
message: reason,
|
|
11788
|
+
errorName: "AbortError"
|
|
11789
|
+
});
|
|
11790
|
+
}
|
|
11791
|
+
}
|
|
11792
|
+
if (count > 0) {
|
|
11793
|
+
logger.debug("[Qwen] Aborted active browser streams for account", {
|
|
11794
|
+
accountId,
|
|
11795
|
+
count,
|
|
11796
|
+
reason
|
|
11797
|
+
});
|
|
11798
|
+
}
|
|
11799
|
+
return count;
|
|
11800
|
+
}
|
|
11752
11801
|
async function registerBrowserContextStreamBinding(context) {
|
|
11753
11802
|
if (browserStreamBindingContexts.has(context)) return;
|
|
11754
11803
|
browserStreamBindingContexts.add(context);
|
|
@@ -12443,6 +12492,7 @@ async function createQwenBrowserResponse(accountId, url, method, headers, body,
|
|
|
12443
12492
|
}
|
|
12444
12493
|
const requestId = uuidv43();
|
|
12445
12494
|
const state = {
|
|
12495
|
+
accountId,
|
|
12446
12496
|
chunks: [],
|
|
12447
12497
|
done: false,
|
|
12448
12498
|
error: null,
|
|
@@ -12459,10 +12509,15 @@ async function createQwenBrowserResponse(accountId, url, method, headers, body,
|
|
|
12459
12509
|
let settled = false;
|
|
12460
12510
|
let abortListener;
|
|
12461
12511
|
let cancelPromise;
|
|
12512
|
+
let pageCleanup;
|
|
12462
12513
|
const cleanup = () => {
|
|
12463
12514
|
if (settled) return;
|
|
12464
12515
|
settled = true;
|
|
12465
12516
|
if (abortListener) signal.removeEventListener("abort", abortListener);
|
|
12517
|
+
try {
|
|
12518
|
+
pageCleanup?.();
|
|
12519
|
+
} catch {
|
|
12520
|
+
}
|
|
12466
12521
|
browserStreamStates.delete(requestId);
|
|
12467
12522
|
};
|
|
12468
12523
|
const cancel = () => {
|
|
@@ -12496,6 +12551,35 @@ async function createQwenBrowserResponse(accountId, url, method, headers, body,
|
|
|
12496
12551
|
accountId,
|
|
12497
12552
|
async (page) => {
|
|
12498
12553
|
await ensureBrowserStreamBinding(page);
|
|
12554
|
+
if (typeof page.on === "function") {
|
|
12555
|
+
const onFrameNavigated = (frame) => {
|
|
12556
|
+
if (!settled && typeof page.mainFrame === "function" && frame === page.mainFrame()) {
|
|
12557
|
+
handleBrowserStreamEvent(requestId, {
|
|
12558
|
+
type: "error",
|
|
12559
|
+
message: "Qwen browser page navigated during stream (session interrupted)",
|
|
12560
|
+
errorName: "AbortError"
|
|
12561
|
+
});
|
|
12562
|
+
}
|
|
12563
|
+
};
|
|
12564
|
+
const onPageClosed = () => {
|
|
12565
|
+
if (!settled) {
|
|
12566
|
+
handleBrowserStreamEvent(requestId, {
|
|
12567
|
+
type: "error",
|
|
12568
|
+
message: "Qwen browser page closed during stream",
|
|
12569
|
+
errorName: "AbortError"
|
|
12570
|
+
});
|
|
12571
|
+
}
|
|
12572
|
+
};
|
|
12573
|
+
page.on("framenavigated", onFrameNavigated);
|
|
12574
|
+
page.on("close", onPageClosed);
|
|
12575
|
+
pageCleanup = () => {
|
|
12576
|
+
try {
|
|
12577
|
+
page.off?.("framenavigated", onFrameNavigated);
|
|
12578
|
+
page.off?.("close", onPageClosed);
|
|
12579
|
+
} catch {
|
|
12580
|
+
}
|
|
12581
|
+
};
|
|
12582
|
+
}
|
|
12499
12583
|
if (config.captcha.enabled) {
|
|
12500
12584
|
captchaWatcher = startBaxiaCaptchaWatcher(
|
|
12501
12585
|
page,
|
|
@@ -13680,7 +13764,7 @@ async function createQwenStreamInternal(prompt, enableThinking, modelId, forcedP
|
|
|
13680
13764
|
}
|
|
13681
13765
|
});
|
|
13682
13766
|
}
|
|
13683
|
-
const baseTimeoutMs =
|
|
13767
|
+
const baseTimeoutMs = config.timeouts.idleStreamTimeout;
|
|
13684
13768
|
const dynamicIdleTimeoutMs = computeDynamicIdleTimeout({
|
|
13685
13769
|
enableThinking,
|
|
13686
13770
|
parallelEscape: options?.parallelEscape,
|
|
@@ -13696,9 +13780,10 @@ async function createQwenStreamInternal(prompt, enableThinking, modelId, forcedP
|
|
|
13696
13780
|
dynamicTimeout: dynamicIdleTimeoutMs
|
|
13697
13781
|
});
|
|
13698
13782
|
const firstChunkDeadlineMs = enableThinking ? Math.max(
|
|
13783
|
+
config.timeouts.reasoningModelTimeout,
|
|
13699
13784
|
config.timeouts.firstChunkTimeout,
|
|
13700
13785
|
config.timeouts.timeToFirstByte
|
|
13701
|
-
) :
|
|
13786
|
+
) : Math.max(config.timeouts.firstChunkTimeout, config.timeouts.timeToFirstByte);
|
|
13702
13787
|
return addIdleTimeoutToStream(
|
|
13703
13788
|
stream,
|
|
13704
13789
|
controller2,
|
|
@@ -21513,7 +21598,7 @@ async function processStreamingResponse(params) {
|
|
|
21513
21598
|
let chatInProgressSameAccountRetries = 0;
|
|
21514
21599
|
let antiBotSameAccountRetries = 0;
|
|
21515
21600
|
const totalConfiguredAccounts = loadAccounts().length;
|
|
21516
|
-
let quotaRotationsLeft = Math.
|
|
21601
|
+
let quotaRotationsLeft = Math.max(1, totalConfiguredAccounts - 1);
|
|
21517
21602
|
let streamCompletedOk = false;
|
|
21518
21603
|
let stoppedByToolCap = false;
|
|
21519
21604
|
const runDisconnectTeardown = () => {
|
|
@@ -22938,7 +23023,7 @@ Please retry your tool call(s) with correct JSON and valid tool names from the a
|
|
|
22938
23023
|
stack: err.stack?.split("\n").slice(0, 3).join(" | "),
|
|
22939
23024
|
completionId
|
|
22940
23025
|
};
|
|
22941
|
-
if (retryable) {
|
|
23026
|
+
if (retryable || isNetworkLikeError(err) || isAbortError(err)) {
|
|
22942
23027
|
if (activeAccountId && isNetworkLikeError(err)) {
|
|
22943
23028
|
noteMidStreamNetworkFailure(activeAccountId);
|
|
22944
23029
|
}
|
|
@@ -22950,28 +23035,32 @@ Please retry your tool call(s) with correct JSON and valid tool names from the a
|
|
|
22950
23035
|
logger.error("[Chat] Stream callback error", errorDetails);
|
|
22951
23036
|
}
|
|
22952
23037
|
try {
|
|
22953
|
-
const
|
|
23038
|
+
const isNetwork = isNetworkLikeError(err);
|
|
23039
|
+
const isModeration = normalizedErrorCode === "data_inspection_failed" || normalizedErrorCode === "fail_sys_user_validate" || err.message?.toLowerCase().includes("content moderation") || err.message?.toLowerCase().includes("security verification");
|
|
23040
|
+
if (!isNetwork && isModeration) {
|
|
23041
|
+
const userFriendlyNotice = `
|
|
22954
23042
|
|
|
22955
|
-
\u26A0\uFE0F **[Qwen
|
|
23043
|
+
\u26A0\uFE0F **[Qwen Modera\xE7\xE3o / Seguran\xE7a]** ${err.message || "A resposta foi interrompida pelas pol\xEDticas de seguran\xE7a do Qwen."}
|
|
22956
23044
|
|
|
22957
23045
|
`;
|
|
22958
|
-
|
|
22959
|
-
|
|
22960
|
-
|
|
22961
|
-
|
|
22962
|
-
|
|
22963
|
-
|
|
22964
|
-
|
|
22965
|
-
|
|
22966
|
-
|
|
22967
|
-
|
|
22968
|
-
|
|
22969
|
-
|
|
22970
|
-
|
|
22971
|
-
|
|
22972
|
-
|
|
23046
|
+
const errorDelta = {
|
|
23047
|
+
id: completionId,
|
|
23048
|
+
object: "chat.completion.chunk",
|
|
23049
|
+
created: Math.floor(Date.now() / 1e3),
|
|
23050
|
+
model: body.model,
|
|
23051
|
+
choices: [
|
|
23052
|
+
{
|
|
23053
|
+
index: 0,
|
|
23054
|
+
delta: { content: userFriendlyNotice },
|
|
23055
|
+
logprobs: null,
|
|
23056
|
+
finish_reason: "content_filter"
|
|
23057
|
+
}
|
|
23058
|
+
]
|
|
23059
|
+
};
|
|
23060
|
+
await errorStream.write(`data: ${JSON.stringify(errorDelta)}
|
|
22973
23061
|
|
|
22974
23062
|
`);
|
|
23063
|
+
}
|
|
22975
23064
|
await errorStream.write(
|
|
22976
23065
|
`data: ${JSON.stringify({
|
|
22977
23066
|
error: {
|
|
@@ -27004,7 +27093,7 @@ function getAppVersion() {
|
|
|
27004
27093
|
}
|
|
27005
27094
|
} catch {
|
|
27006
27095
|
}
|
|
27007
|
-
cachedVersion = "v1.6.
|
|
27096
|
+
cachedVersion = "v1.6.7";
|
|
27008
27097
|
return cachedVersion;
|
|
27009
27098
|
}
|
|
27010
27099
|
var cachedVersion;
|
package/dist/login.js
CHANGED
|
@@ -1108,7 +1108,10 @@ function isPlaywrightAlreadyClosedMessage(message) {
|
|
|
1108
1108
|
return message.includes("Target page, context or browser has been closed") || message.includes("Browser has been closed") || message.includes("Target closed") || message.includes("Target crashed") || message.includes("Page crashed") || message.includes("Assertion error") || message.includes("Cannot find parent object") || message.includes("Connection closed") || message.includes("session closed") || message.includes("Session closed") || message.includes("Network.setCacheDisabled") || message.includes("Protocol error") || message.includes("storageState timed out");
|
|
1109
1109
|
}
|
|
1110
1110
|
function isBenignTeardownError(error) {
|
|
1111
|
-
if (!error) return
|
|
1111
|
+
if (!error) return true;
|
|
1112
|
+
if (typeof error === "object" && Object.keys(error).length === 0 && !(error instanceof Error)) {
|
|
1113
|
+
return true;
|
|
1114
|
+
}
|
|
1112
1115
|
const message = error instanceof Error ? error.message : typeof error === "object" && "message" in error ? String(error.message) : String(error);
|
|
1113
1116
|
return isPlaywrightAlreadyClosedMessage(message);
|
|
1114
1117
|
}
|
|
@@ -4420,6 +4423,15 @@ async function initPlaywrightForAccount(rawAccount, headless = config.playwright
|
|
|
4420
4423
|
await existingInit;
|
|
4421
4424
|
return;
|
|
4422
4425
|
}
|
|
4426
|
+
let resolveInit;
|
|
4427
|
+
let rejectInit;
|
|
4428
|
+
const inFlightDeferred = new Promise((res, rej) => {
|
|
4429
|
+
resolveInit = res;
|
|
4430
|
+
rejectInit = rej;
|
|
4431
|
+
});
|
|
4432
|
+
inFlightDeferred.catch(() => {
|
|
4433
|
+
});
|
|
4434
|
+
inFlightAccountInits.set(account.id, inFlightDeferred);
|
|
4423
4435
|
const initPromise = (async () => {
|
|
4424
4436
|
const release = await acquireAccountMutex(
|
|
4425
4437
|
account.id,
|
|
@@ -4704,9 +4716,12 @@ async function initPlaywrightForAccount(rawAccount, headless = config.playwright
|
|
|
4704
4716
|
release();
|
|
4705
4717
|
}
|
|
4706
4718
|
})();
|
|
4707
|
-
inFlightAccountInits.set(account.id, initPromise);
|
|
4708
4719
|
try {
|
|
4709
4720
|
await initPromise;
|
|
4721
|
+
resolveInit();
|
|
4722
|
+
} catch (error) {
|
|
4723
|
+
rejectInit(error);
|
|
4724
|
+
throw error;
|
|
4710
4725
|
} finally {
|
|
4711
4726
|
inFlightAccountInits.delete(account.id);
|
|
4712
4727
|
}
|
package/dist/reset-cooldowns.js
CHANGED
|
@@ -2825,7 +2825,10 @@ function isPlaywrightAlreadyClosedMessage(message) {
|
|
|
2825
2825
|
return message.includes("Target page, context or browser has been closed") || message.includes("Browser has been closed") || message.includes("Target closed") || message.includes("Target crashed") || message.includes("Page crashed") || message.includes("Assertion error") || message.includes("Cannot find parent object") || message.includes("Connection closed") || message.includes("session closed") || message.includes("Session closed") || message.includes("Network.setCacheDisabled") || message.includes("Protocol error") || message.includes("storageState timed out");
|
|
2826
2826
|
}
|
|
2827
2827
|
function isBenignTeardownError(error) {
|
|
2828
|
-
if (!error) return
|
|
2828
|
+
if (!error) return true;
|
|
2829
|
+
if (typeof error === "object" && Object.keys(error).length === 0 && !(error instanceof Error)) {
|
|
2830
|
+
return true;
|
|
2831
|
+
}
|
|
2829
2832
|
const message = error instanceof Error ? error.message : typeof error === "object" && "message" in error ? String(error.message) : String(error);
|
|
2830
2833
|
return isPlaywrightAlreadyClosedMessage(message);
|
|
2831
2834
|
}
|
|
@@ -3851,6 +3854,15 @@ async function initPlaywrightForAccount(rawAccount, headless = config.playwright
|
|
|
3851
3854
|
await existingInit;
|
|
3852
3855
|
return;
|
|
3853
3856
|
}
|
|
3857
|
+
let resolveInit;
|
|
3858
|
+
let rejectInit;
|
|
3859
|
+
const inFlightDeferred = new Promise((res, rej) => {
|
|
3860
|
+
resolveInit = res;
|
|
3861
|
+
rejectInit = rej;
|
|
3862
|
+
});
|
|
3863
|
+
inFlightDeferred.catch(() => {
|
|
3864
|
+
});
|
|
3865
|
+
inFlightAccountInits.set(account.id, inFlightDeferred);
|
|
3854
3866
|
const initPromise = (async () => {
|
|
3855
3867
|
const release = await acquireAccountMutex(
|
|
3856
3868
|
account.id,
|
|
@@ -4135,9 +4147,12 @@ async function initPlaywrightForAccount(rawAccount, headless = config.playwright
|
|
|
4135
4147
|
release();
|
|
4136
4148
|
}
|
|
4137
4149
|
})();
|
|
4138
|
-
inFlightAccountInits.set(account.id, initPromise);
|
|
4139
4150
|
try {
|
|
4140
4151
|
await initPromise;
|
|
4152
|
+
resolveInit();
|
|
4153
|
+
} catch (error) {
|
|
4154
|
+
rejectInit(error);
|
|
4155
|
+
throw error;
|
|
4141
4156
|
} finally {
|
|
4142
4157
|
inFlightAccountInits.delete(account.id);
|
|
4143
4158
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qwenproxy-cli",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.7",
|
|
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/model-alias.ts
CHANGED
|
@@ -76,6 +76,12 @@ export function stripThinkingSuffix(model: string): {
|
|
|
76
76
|
};
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
// Default for non-thinking models (omni-flash, turbo)
|
|
80
|
+
const lower = normalizedModel.toLowerCase();
|
|
81
|
+
if (lower.includes("omni-flash") || lower.includes("turbo")) {
|
|
82
|
+
return { baseModel: normalizedModel, enableThinking: false, reasoningMode: "fast" };
|
|
83
|
+
}
|
|
84
|
+
|
|
79
85
|
// Default: auto thinking (Qwen decides)
|
|
80
86
|
return { baseModel: normalizedModel, enableThinking: true, reasoningMode: "auto" };
|
|
81
87
|
}
|
|
@@ -27,7 +27,14 @@ export function isPlaywrightAlreadyClosedMessage(message: string): boolean {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
export function isBenignTeardownError(error: unknown): boolean {
|
|
30
|
-
if (!error) return
|
|
30
|
+
if (!error) return true;
|
|
31
|
+
if (
|
|
32
|
+
typeof error === "object" &&
|
|
33
|
+
Object.keys(error).length === 0 &&
|
|
34
|
+
!(error instanceof Error)
|
|
35
|
+
) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
31
38
|
const message =
|
|
32
39
|
error instanceof Error
|
|
33
40
|
? error.message
|
package/src/core/version.ts
CHANGED
|
@@ -937,7 +937,7 @@ export async function processStreamingResponse(
|
|
|
937
937
|
let chatInProgressSameAccountRetries = 0;
|
|
938
938
|
let antiBotSameAccountRetries = 0;
|
|
939
939
|
const totalConfiguredAccounts = loadAccounts().length;
|
|
940
|
-
let quotaRotationsLeft = Math.
|
|
940
|
+
let quotaRotationsLeft = Math.max(1, totalConfiguredAccounts - 1);
|
|
941
941
|
// Set when the terminal [DONE] reached the client. The `| recovered`
|
|
942
942
|
// suffix on Stream done must only appear for attempts that COMPLETED after
|
|
943
943
|
// a mid-stream retry, not for failed attempts that consumed retries.
|
|
@@ -2819,7 +2819,7 @@ export async function processStreamingResponse(
|
|
|
2819
2819
|
completionId,
|
|
2820
2820
|
};
|
|
2821
2821
|
|
|
2822
|
-
if (retryable) {
|
|
2822
|
+
if (retryable || isNetworkLikeError(err) || isAbortError(err)) {
|
|
2823
2823
|
if (activeAccountId && isNetworkLikeError(err)) {
|
|
2824
2824
|
noteMidStreamNetworkFailure(activeAccountId);
|
|
2825
2825
|
}
|
|
@@ -2831,28 +2831,41 @@ export async function processStreamingResponse(
|
|
|
2831
2831
|
logger.error("[Chat] Stream callback error", errorDetails);
|
|
2832
2832
|
}
|
|
2833
2833
|
// The HTTP response is already committed at this point.
|
|
2834
|
-
// 1. Emit an explicit assistant message delta
|
|
2835
|
-
//
|
|
2836
|
-
//
|
|
2837
|
-
// 2.
|
|
2834
|
+
// 1. Emit an explicit assistant message delta ONLY if this was an upstream content moderation /
|
|
2835
|
+
// policy violation (with finish_reason: "content_filter"), so the user understands why the
|
|
2836
|
+
// prompt was blocked.
|
|
2837
|
+
// 2. For network/transient/quota errors, NEVER emit fake assistant content with finish_reason: "stop".
|
|
2838
|
+
// Doing so masks network failures as successful AI answers, preventing client agent retries and
|
|
2839
|
+
// poisoning chat history with error banners.
|
|
2840
|
+
// 3. Emit a terminal OpenAI-compatible SSE error event + [DONE].
|
|
2838
2841
|
try {
|
|
2839
|
-
const
|
|
2840
|
-
const
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2842
|
+
const isNetwork = isNetworkLikeError(err);
|
|
2843
|
+
const isModeration =
|
|
2844
|
+
normalizedErrorCode === "data_inspection_failed" ||
|
|
2845
|
+
normalizedErrorCode === "fail_sys_user_validate" ||
|
|
2846
|
+
err.message?.toLowerCase().includes("content moderation") ||
|
|
2847
|
+
err.message?.toLowerCase().includes("security verification");
|
|
2848
|
+
|
|
2849
|
+
if (!isNetwork && isModeration) {
|
|
2850
|
+
const userFriendlyNotice = `\n\n⚠️ **[Qwen Moderação / Segurança]** ${err.message || "A resposta foi interrompida pelas políticas de segurança do Qwen."}\n\n`;
|
|
2851
|
+
const errorDelta = {
|
|
2852
|
+
id: completionId,
|
|
2853
|
+
object: "chat.completion.chunk",
|
|
2854
|
+
created: Math.floor(Date.now() / 1000),
|
|
2855
|
+
model: body.model,
|
|
2856
|
+
choices: [
|
|
2857
|
+
{
|
|
2858
|
+
index: 0,
|
|
2859
|
+
delta: { content: userFriendlyNotice },
|
|
2860
|
+
logprobs: null,
|
|
2861
|
+
finish_reason: "content_filter",
|
|
2862
|
+
},
|
|
2863
|
+
],
|
|
2864
|
+
};
|
|
2865
|
+
|
|
2866
|
+
await errorStream.write(`data: ${JSON.stringify(errorDelta)}\n\n`);
|
|
2867
|
+
}
|
|
2854
2868
|
|
|
2855
|
-
await errorStream.write(`data: ${JSON.stringify(errorDelta)}\n\n`);
|
|
2856
2869
|
await errorStream.write(
|
|
2857
2870
|
`data: ${JSON.stringify({
|
|
2858
2871
|
error: {
|
|
@@ -125,6 +125,13 @@ export async function recoverBaxiaCaptcha(
|
|
|
125
125
|
: null;
|
|
126
126
|
|
|
127
127
|
activeCaptchaRecoveries.add(accountId);
|
|
128
|
+
try {
|
|
129
|
+
const { abortActiveBrowserStreamsForAccount } = await import("./qwen.ts");
|
|
130
|
+
abortActiveBrowserStreamsForAccount(
|
|
131
|
+
accountId,
|
|
132
|
+
"Account stream interrupted by captcha challenge recovery",
|
|
133
|
+
);
|
|
134
|
+
} catch {}
|
|
128
135
|
try {
|
|
129
136
|
// recoverOnTimeout: false ensures that if the captcha solve times out,
|
|
130
137
|
// withAccountPage does NOT destructively kill the browser context, which
|
|
@@ -1529,6 +1529,16 @@ export async function initPlaywrightForAccount(
|
|
|
1529
1529
|
return;
|
|
1530
1530
|
}
|
|
1531
1531
|
|
|
1532
|
+
let resolveInit!: () => void;
|
|
1533
|
+
let rejectInit!: (err: any) => void;
|
|
1534
|
+
const inFlightDeferred = new Promise<void>((res, rej) => {
|
|
1535
|
+
resolveInit = res;
|
|
1536
|
+
rejectInit = rej;
|
|
1537
|
+
});
|
|
1538
|
+
// Prevent UnhandledRejection if rejectInit is called when no concurrent callers are awaiting
|
|
1539
|
+
inFlightDeferred.catch(() => {});
|
|
1540
|
+
inFlightAccountInits.set(account.id, inFlightDeferred);
|
|
1541
|
+
|
|
1532
1542
|
const initPromise = (async () => {
|
|
1533
1543
|
const release = await acquireAccountMutex(
|
|
1534
1544
|
account.id,
|
|
@@ -1536,10 +1546,10 @@ export async function initPlaywrightForAccount(
|
|
|
1536
1546
|
Math.max(PLAYWRIGHT_MUTEX_WAIT_MS, 120_000),
|
|
1537
1547
|
);
|
|
1538
1548
|
try {
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1549
|
+
// Double-check after acquiring lock
|
|
1550
|
+
if (accountPages.has(account.id)) {
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1543
1553
|
|
|
1544
1554
|
// If a context limit is configured, make room by closing idle contexts.
|
|
1545
1555
|
await evictIdlePlaywrightContextsToLimit().catch(() => {});
|
|
@@ -1866,9 +1876,12 @@ export async function initPlaywrightForAccount(
|
|
|
1866
1876
|
}
|
|
1867
1877
|
})();
|
|
1868
1878
|
|
|
1869
|
-
inFlightAccountInits.set(account.id, initPromise);
|
|
1870
1879
|
try {
|
|
1871
1880
|
await initPromise;
|
|
1881
|
+
resolveInit();
|
|
1882
|
+
} catch (error) {
|
|
1883
|
+
rejectInit(error);
|
|
1884
|
+
throw error;
|
|
1872
1885
|
} finally {
|
|
1873
1886
|
inFlightAccountInits.delete(account.id);
|
|
1874
1887
|
}
|
package/src/services/qwen.ts
CHANGED
|
@@ -117,6 +117,7 @@ interface BrowserStreamMetadata {
|
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
interface BrowserStreamState {
|
|
120
|
+
accountId?: string;
|
|
120
121
|
chunks: Uint8Array[];
|
|
121
122
|
done: boolean;
|
|
122
123
|
error: Error | null;
|
|
@@ -128,6 +129,36 @@ const browserStreamStates = new Map<string, BrowserStreamState>();
|
|
|
128
129
|
const browserStreamBindingPages = new WeakSet<object>();
|
|
129
130
|
const browserStreamBindingContexts = new WeakSet<object>();
|
|
130
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Abort all active browser streams for an account immediately.
|
|
134
|
+
* Called when an account page is navigated by captcha recovery, closed, or reset,
|
|
135
|
+
* so in-flight streams fail fast (retryable) instead of stalling for minutes.
|
|
136
|
+
*/
|
|
137
|
+
export function abortActiveBrowserStreamsForAccount(
|
|
138
|
+
accountId: string,
|
|
139
|
+
reason = "Account browser stream interrupted (page context unloaded)",
|
|
140
|
+
): number {
|
|
141
|
+
let count = 0;
|
|
142
|
+
for (const [requestId, state] of browserStreamStates) {
|
|
143
|
+
if (state.accountId === accountId && !state.done) {
|
|
144
|
+
count++;
|
|
145
|
+
handleBrowserStreamEvent(requestId, {
|
|
146
|
+
type: "error",
|
|
147
|
+
message: reason,
|
|
148
|
+
errorName: "AbortError",
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (count > 0) {
|
|
153
|
+
logger.debug("[Qwen] Aborted active browser streams for account", {
|
|
154
|
+
accountId,
|
|
155
|
+
count,
|
|
156
|
+
reason,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
return count;
|
|
160
|
+
}
|
|
161
|
+
|
|
131
162
|
export async function registerBrowserContextStreamBinding(
|
|
132
163
|
context: BrowserContext,
|
|
133
164
|
): Promise<void> {
|
|
@@ -1263,6 +1294,7 @@ async function createQwenBrowserResponse(
|
|
|
1263
1294
|
|
|
1264
1295
|
const requestId = uuidv4();
|
|
1265
1296
|
const state: BrowserStreamState = {
|
|
1297
|
+
accountId,
|
|
1266
1298
|
chunks: [],
|
|
1267
1299
|
done: false,
|
|
1268
1300
|
error: null,
|
|
@@ -1283,10 +1315,14 @@ async function createQwenBrowserResponse(
|
|
|
1283
1315
|
let settled = false;
|
|
1284
1316
|
let abortListener: (() => void) | undefined;
|
|
1285
1317
|
let cancelPromise: Promise<void> | undefined;
|
|
1318
|
+
let pageCleanup: (() => void) | undefined;
|
|
1286
1319
|
const cleanup = () => {
|
|
1287
1320
|
if (settled) return;
|
|
1288
1321
|
settled = true;
|
|
1289
1322
|
if (abortListener) signal.removeEventListener("abort", abortListener);
|
|
1323
|
+
try {
|
|
1324
|
+
pageCleanup?.();
|
|
1325
|
+
} catch {}
|
|
1290
1326
|
browserStreamStates.delete(requestId);
|
|
1291
1327
|
};
|
|
1292
1328
|
const cancel = () => {
|
|
@@ -1327,6 +1363,34 @@ async function createQwenBrowserResponse(
|
|
|
1327
1363
|
accountId,
|
|
1328
1364
|
async (page) => {
|
|
1329
1365
|
await ensureBrowserStreamBinding(page);
|
|
1366
|
+
if (typeof (page as any).on === "function") {
|
|
1367
|
+
const onFrameNavigated = (frame: any) => {
|
|
1368
|
+
if (!settled && typeof (page as any).mainFrame === "function" && frame === (page as any).mainFrame()) {
|
|
1369
|
+
handleBrowserStreamEvent(requestId, {
|
|
1370
|
+
type: "error",
|
|
1371
|
+
message: "Qwen browser page navigated during stream (session interrupted)",
|
|
1372
|
+
errorName: "AbortError",
|
|
1373
|
+
});
|
|
1374
|
+
}
|
|
1375
|
+
};
|
|
1376
|
+
const onPageClosed = () => {
|
|
1377
|
+
if (!settled) {
|
|
1378
|
+
handleBrowserStreamEvent(requestId, {
|
|
1379
|
+
type: "error",
|
|
1380
|
+
message: "Qwen browser page closed during stream",
|
|
1381
|
+
errorName: "AbortError",
|
|
1382
|
+
});
|
|
1383
|
+
}
|
|
1384
|
+
};
|
|
1385
|
+
(page as any).on("framenavigated", onFrameNavigated);
|
|
1386
|
+
(page as any).on("close", onPageClosed);
|
|
1387
|
+
pageCleanup = () => {
|
|
1388
|
+
try {
|
|
1389
|
+
(page as any).off?.("framenavigated", onFrameNavigated);
|
|
1390
|
+
(page as any).off?.("close", onPageClosed);
|
|
1391
|
+
} catch {}
|
|
1392
|
+
};
|
|
1393
|
+
}
|
|
1330
1394
|
if (config.captcha.enabled) {
|
|
1331
1395
|
captchaWatcher = startBaxiaCaptchaWatcher(
|
|
1332
1396
|
page,
|
|
@@ -3011,13 +3075,9 @@ async function createQwenStreamInternal(
|
|
|
3011
3075
|
}
|
|
3012
3076
|
|
|
3013
3077
|
// Dynamic idle timeout based on model type and payload size
|
|
3014
|
-
//
|
|
3015
|
-
//
|
|
3016
|
-
|
|
3017
|
-
// ONLY when non-thinking (see computeDynamicIdleTimeout).
|
|
3018
|
-
const baseTimeoutMs = enableThinking
|
|
3019
|
-
? config.timeouts.reasoningModelTimeout
|
|
3020
|
-
: config.timeouts.idleStreamTimeout;
|
|
3078
|
+
// Base timeout for gaps between chunks is governed by IDLE_STREAM_TIMEOUT (default 60s)
|
|
3079
|
+
// plus dynamic payload scaling (30s per MB).
|
|
3080
|
+
const baseTimeoutMs = config.timeouts.idleStreamTimeout;
|
|
3021
3081
|
const dynamicIdleTimeoutMs = computeDynamicIdleTimeout({
|
|
3022
3082
|
enableThinking,
|
|
3023
3083
|
parallelEscape: options?.parallelEscape,
|
|
@@ -3034,15 +3094,15 @@ async function createQwenStreamInternal(
|
|
|
3034
3094
|
dynamicTimeout: dynamicIdleTimeoutMs,
|
|
3035
3095
|
});
|
|
3036
3096
|
|
|
3037
|
-
// Thinking models
|
|
3038
|
-
//
|
|
3039
|
-
// fast (retryable) so the account slot is not held for 10 minutes.
|
|
3097
|
+
// Thinking models legitimately pause before first chunk (up to reasoningModelTimeout).
|
|
3098
|
+
// After the first chunk, the normal dynamicIdleTimeoutMs (60s+) governs chunk gaps.
|
|
3040
3099
|
const firstChunkDeadlineMs = enableThinking
|
|
3041
3100
|
? Math.max(
|
|
3101
|
+
config.timeouts.reasoningModelTimeout,
|
|
3042
3102
|
config.timeouts.firstChunkTimeout,
|
|
3043
3103
|
config.timeouts.timeToFirstByte,
|
|
3044
3104
|
)
|
|
3045
|
-
:
|
|
3105
|
+
: Math.max(config.timeouts.firstChunkTimeout, config.timeouts.timeToFirstByte);
|
|
3046
3106
|
|
|
3047
3107
|
return addIdleTimeoutToStream(
|
|
3048
3108
|
stream,
|