qwenproxy-cli 1.6.10 → 1.6.11
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 +40 -12
- package/dist/delete-chats.js +110 -35
- package/dist/index.js +131 -45
- package/dist/login.js +44 -24
- package/dist/reset-cooldowns.js +40 -12
- package/package.json +2 -2
- package/src/api/server.ts +8 -4
- package/src/core/version.ts +1 -1
- package/src/core/watchdog.ts +4 -3
- package/src/routes/chat/account.ts +12 -0
- package/src/routes/chat/streaming.ts +18 -2
- package/src/services/captcha-coordinator.ts +36 -8
- package/src/services/chat-cleanup.ts +11 -2
- package/src/services/playwright.ts +73 -28
- package/src/services/qwen.ts +29 -3
package/src/core/version.ts
CHANGED
package/src/core/watchdog.ts
CHANGED
|
@@ -148,14 +148,15 @@ export class Watchdog extends EventEmitter {
|
|
|
148
148
|
// (idle mutex, no active stream, preserves the max-active-context minimum)
|
|
149
149
|
// so the next health check sees the freed RSS.
|
|
150
150
|
try {
|
|
151
|
-
const { closeIdlePlaywrightAccounts } = await import(
|
|
151
|
+
const { closeIdlePlaywrightAccounts, evictIdlePlaywrightContextsToLimit } = await import(
|
|
152
152
|
"../services/playwright.js"
|
|
153
153
|
);
|
|
154
|
+
const evicted = await evictIdlePlaywrightContextsToLimit(1);
|
|
154
155
|
const closed = await closeIdlePlaywrightAccounts(
|
|
155
156
|
config.sessionKeeper.idleMs,
|
|
156
157
|
);
|
|
157
|
-
if (closed > 0) {
|
|
158
|
-
this.emit("recovery:ram:contexts-closed", closed);
|
|
158
|
+
if (evicted + closed > 0) {
|
|
159
|
+
this.emit("recovery:ram:contexts-closed", evicted + closed);
|
|
159
160
|
}
|
|
160
161
|
} catch {
|
|
161
162
|
// Playwright may not be initialized (mock mode / pre-start); RSS relief
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
} from "../../core/account-manager.ts";
|
|
9
9
|
import { markAccountSuccessful, markAccountFailed, getAccountsByPriority } from "../../core/account-priority.ts";
|
|
10
10
|
import { recordWafHardBlock, noteWafRecovery } from "../../core/waf-isolation.ts";
|
|
11
|
+
import { isCaptchaRecoveryInBackoff } from "../../services/captcha-coordinator.ts";
|
|
11
12
|
import { loadAccounts, type QwenAccount } from "../../core/accounts.ts";
|
|
12
13
|
import { config, type ChatMode } from "../../core/config.ts";
|
|
13
14
|
import { ClientAbortedError, UpstreamRateLimit, ValidationError } from "../../core/errors.ts";
|
|
@@ -1615,6 +1616,17 @@ async function tryCreateStreamWithRetry(
|
|
|
1615
1616
|
invalidateLogicalThreadParent(params.sessionId);
|
|
1616
1617
|
}
|
|
1617
1618
|
|
|
1619
|
+
// If captcha recovery on this account already failed and is in its 30s
|
|
1620
|
+
// backoff window, same-account retries will only hit `recovery_skipped`
|
|
1621
|
+
// and waste time/RAM re-sending large payloads. Exit immediately so the
|
|
1622
|
+
// outer loop quarantines this account and switches to a fresh one.
|
|
1623
|
+
if (
|
|
1624
|
+
policy.reason === "anti_bot" &&
|
|
1625
|
+
isCaptchaRecoveryInBackoff(currentAccountId)
|
|
1626
|
+
) {
|
|
1627
|
+
return { success: false, error: err };
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1618
1630
|
// A generic invalid_input is often a stale/corrupted upstream chat rather
|
|
1619
1631
|
// than an account failure. Rebuild it once on the same account first. If the
|
|
1620
1632
|
// fresh chat fails again, the normal policy is allowed to rotate.
|
|
@@ -1259,8 +1259,8 @@ export async function processStreamingResponse(
|
|
|
1259
1259
|
};
|
|
1260
1260
|
|
|
1261
1261
|
const emitAnswerText = async (textChunk: string) => {
|
|
1262
|
-
if (textChunk) emittedModelOutput = true;
|
|
1263
1262
|
if (!toolParser) {
|
|
1263
|
+
if (textChunk) emittedModelOutput = true;
|
|
1264
1264
|
finalContent += textChunk;
|
|
1265
1265
|
writeDeltaEvent({ content: textChunk });
|
|
1266
1266
|
return;
|
|
@@ -1777,6 +1777,23 @@ export async function processStreamingResponse(
|
|
|
1777
1777
|
.replace(/\\\\/g, "\\");
|
|
1778
1778
|
|
|
1779
1779
|
if (unescaped) {
|
|
1780
|
+
if (!emittedModelOutput && isOverloadMessage(unescaped)) {
|
|
1781
|
+
const overloadErr = toRetryableStreamError(
|
|
1782
|
+
"server_overloaded",
|
|
1783
|
+
unescaped,
|
|
1784
|
+
{
|
|
1785
|
+
switchAccount: true,
|
|
1786
|
+
forceNewChat: true,
|
|
1787
|
+
reason: "server_busy",
|
|
1788
|
+
accountCooldownMs: OVERLOAD_COOLDOWN_MS,
|
|
1789
|
+
accountCooldownReason: "ServerOverloaded",
|
|
1790
|
+
},
|
|
1791
|
+
);
|
|
1792
|
+
if (await recoverFromStreamError(overloadErr)) {
|
|
1793
|
+
break;
|
|
1794
|
+
}
|
|
1795
|
+
throw overloadErr;
|
|
1796
|
+
}
|
|
1780
1797
|
const result = getIncrementalDelta(
|
|
1781
1798
|
lastRawContent,
|
|
1782
1799
|
unescaped,
|
|
@@ -1942,7 +1959,6 @@ export async function processStreamingResponse(
|
|
|
1942
1959
|
if (vStr === "FINISHED") continue;
|
|
1943
1960
|
|
|
1944
1961
|
if (isThinkingChunk) {
|
|
1945
|
-
emittedModelOutput = true;
|
|
1946
1962
|
reasoningBuffer += vStr;
|
|
1947
1963
|
writeDeltaEvent({ reasoning_content: vStr });
|
|
1948
1964
|
} else {
|
|
@@ -92,6 +92,12 @@ export async function solveChallengeOnPage(
|
|
|
92
92
|
* a punish document that is never rendered; opening that document in the page
|
|
93
93
|
* is what gives the solver a slider to drive.
|
|
94
94
|
*/
|
|
95
|
+
export function isCaptchaRecoveryInBackoff(accountId: string | undefined): boolean {
|
|
96
|
+
if (!accountId) return false;
|
|
97
|
+
const lastFailure = lastFailedRecoveryAt.get(accountId) ?? 0;
|
|
98
|
+
return Date.now() - lastFailure < FAILED_RECOVERY_BACKOFF_MS;
|
|
99
|
+
}
|
|
100
|
+
|
|
95
101
|
export async function recoverBaxiaCaptcha(
|
|
96
102
|
accountId: string | undefined,
|
|
97
103
|
label: string,
|
|
@@ -126,19 +132,41 @@ export async function recoverBaxiaCaptcha(
|
|
|
126
132
|
|
|
127
133
|
activeCaptchaRecoveries.add(accountId);
|
|
128
134
|
try {
|
|
129
|
-
const {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
"Account stream interrupted by captcha challenge recovery",
|
|
133
|
-
);
|
|
134
|
-
} catch {}
|
|
135
|
-
try {
|
|
135
|
+
const { hasActiveBrowserStreamsForAccount } = await import("./qwen.ts");
|
|
136
|
+
const hasConcurrentStream = hasActiveBrowserStreamsForAccount(accountId);
|
|
137
|
+
|
|
136
138
|
// recoverOnTimeout: false ensures that if the captcha solve times out,
|
|
137
139
|
// withAccountPage does NOT destructively kill the browser context, which
|
|
138
140
|
// previously caused "Target page, context or browser has been closed" mid-slider.
|
|
139
141
|
const solved = await withAccountPage(
|
|
140
142
|
accountId,
|
|
141
|
-
(page) =>
|
|
143
|
+
async (page) => {
|
|
144
|
+
// If another stream is actively running on this account's primary page,
|
|
145
|
+
// NEVER navigate `page` away (which would unload its DOM and kill the stream).
|
|
146
|
+
// Instead, try in-place modal solve first, or open an ephemeral tab in the
|
|
147
|
+
// same BrowserContext so cookies/x5sec are solved without touching `page`.
|
|
148
|
+
if (hasConcurrentStream) {
|
|
149
|
+
const solverOptions = {
|
|
150
|
+
maxAttempts: config.captcha.maxAttempts,
|
|
151
|
+
retryDelayMs: config.captcha.retryDelayMs,
|
|
152
|
+
settleMs: config.captcha.settleMs,
|
|
153
|
+
};
|
|
154
|
+
if (await solveBaxiaCaptcha(page, { ...solverOptions, waitForMs: 0 })) {
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
const ctx = typeof page.context === "function" ? page.context() : null;
|
|
158
|
+
if (ctx && typeof ctx.newPage === "function") {
|
|
159
|
+
const tempPage = await ctx.newPage();
|
|
160
|
+
try {
|
|
161
|
+
return await solveChallengeOnPage(tempPage, challengeUrl);
|
|
162
|
+
} finally {
|
|
163
|
+
await tempPage.close({ runBeforeUnload: false }).catch(() => {});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
return await solveChallengeOnPage(page, challengeUrl);
|
|
169
|
+
},
|
|
142
170
|
solverOperationTimeoutMs,
|
|
143
171
|
solverOperationTimeoutMs,
|
|
144
172
|
false,
|
|
@@ -79,7 +79,15 @@ export async function deleteChatsForAccountId(accountId: string): Promise<boolea
|
|
|
79
79
|
if (!credentials) {
|
|
80
80
|
throw new Error(`Credenciais da conta ${account.email} não encontradas.`);
|
|
81
81
|
}
|
|
82
|
-
|
|
82
|
+
const wasInitialized = isPlaywrightInitialized(account.id);
|
|
83
|
+
try {
|
|
84
|
+
return await deleteChatsForAccount(credentials);
|
|
85
|
+
} finally {
|
|
86
|
+
if (!wasInitialized && !isAuthMockEnabled()) {
|
|
87
|
+
const { closePlaywrightForAccount } = await import("./playwright.ts");
|
|
88
|
+
await closePlaywrightForAccount(account.id).catch(() => {});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
83
91
|
}
|
|
84
92
|
|
|
85
93
|
export async function deleteChatsForConfiguredAccounts(keepBrowserOpen = false): Promise<DeleteChatsResult> {
|
|
@@ -100,6 +108,7 @@ export async function deleteChatsForConfiguredAccounts(keepBrowserOpen = false):
|
|
|
100
108
|
const account = accounts[i];
|
|
101
109
|
const currentIdx = i + 1;
|
|
102
110
|
const totalCount = accounts.length;
|
|
111
|
+
const wasInitialized = isPlaywrightInitialized(account.id);
|
|
103
112
|
try {
|
|
104
113
|
const ok = await deleteChatsForAccount(account, currentIdx, totalCount);
|
|
105
114
|
if (ok) succeeded++;
|
|
@@ -109,7 +118,7 @@ export async function deleteChatsForConfiguredAccounts(keepBrowserOpen = false):
|
|
|
109
118
|
error instanceof Error ? error.message : String(error),
|
|
110
119
|
);
|
|
111
120
|
} finally {
|
|
112
|
-
if (!keepBrowserOpen) {
|
|
121
|
+
if ((!keepBrowserOpen || !wasInitialized) && !isAuthMockEnabled()) {
|
|
113
122
|
const { closePlaywrightForAccount } = await import("./playwright.ts");
|
|
114
123
|
await closePlaywrightForAccount(account.id).catch(() => {});
|
|
115
124
|
}
|
|
@@ -68,6 +68,7 @@ import {
|
|
|
68
68
|
unmarkAccountHeadersReady,
|
|
69
69
|
isAccountHeadersReady,
|
|
70
70
|
markAccountRateLimited,
|
|
71
|
+
getAccountCooldownInfo,
|
|
71
72
|
} from "../core/account-manager.ts";
|
|
72
73
|
import { getAccountsByPriority } from "../core/account-priority.ts";
|
|
73
74
|
import {
|
|
@@ -1408,18 +1409,19 @@ export async function getBasicHeaders(accountId: string): Promise<{
|
|
|
1408
1409
|
} else if (
|
|
1409
1410
|
hadUsableHeaders &&
|
|
1410
1411
|
headersAge > HEADER_CACHE_TTL * HEADER_REFRESH_THRESHOLD &&
|
|
1411
|
-
!cache.refreshInProgress
|
|
1412
|
+
!cache.refreshInProgress &&
|
|
1413
|
+
// Header capture types into the chat input and sends a probe message on
|
|
1414
|
+
// the account's page. Doing that while the account is serving a stream
|
|
1415
|
+
// would mutate the live conversation (and can navigate the SPA), so the
|
|
1416
|
+
// proactive refresh waits until the account has no active lease.
|
|
1417
|
+
!hasActiveAccountLease(accountId)
|
|
1412
1418
|
) {
|
|
1413
1419
|
// Background refresh at 70% TTL: keep headers fresh without blocking
|
|
1414
|
-
cache.refreshInProgress = true;
|
|
1415
1420
|
refreshHeadersInternal(accountId)
|
|
1416
1421
|
.catch((error) => {
|
|
1417
1422
|
console.warn(
|
|
1418
1423
|
`⚠️ [Playwright] Background header refresh failed for ${accountId}: ${getErrorMessage(error)}`,
|
|
1419
1424
|
);
|
|
1420
|
-
})
|
|
1421
|
-
.finally(() => {
|
|
1422
|
-
cache.refreshInProgress = false;
|
|
1423
1425
|
});
|
|
1424
1426
|
}
|
|
1425
1427
|
|
|
@@ -1569,8 +1571,13 @@ export async function initPlaywrightForAccount(
|
|
|
1569
1571
|
return;
|
|
1570
1572
|
}
|
|
1571
1573
|
|
|
1572
|
-
// If a context limit is configured, make room by
|
|
1573
|
-
|
|
1574
|
+
// If a context limit is configured, make room BEFORE launching by evicting down to (max - 1)
|
|
1575
|
+
// so the new context brings total active contexts to at most max.
|
|
1576
|
+
if (config.playwright.maxActiveContexts > 0) {
|
|
1577
|
+
await evictIdlePlaywrightContextsToLimit(
|
|
1578
|
+
Math.max(1, config.playwright.maxActiveContexts - 1),
|
|
1579
|
+
).catch(() => {});
|
|
1580
|
+
}
|
|
1574
1581
|
|
|
1575
1582
|
const profilePath = getAccountProfilePath(account.id);
|
|
1576
1583
|
const fingerprint = getFingerprintProfile(account.id);
|
|
@@ -1882,12 +1889,17 @@ export async function initPlaywrightForAccount(
|
|
|
1882
1889
|
await captureQwenHeaders(account.id);
|
|
1883
1890
|
}
|
|
1884
1891
|
|
|
1885
|
-
// Header capture may leave the UI on a
|
|
1886
|
-
// primary tab to the canonical chat home.
|
|
1892
|
+
// Header capture may leave the UI on a punish or auth page. Return the
|
|
1893
|
+
// primary tab to the canonical chat home only when not already on a clean Qwen page.
|
|
1887
1894
|
if (!acctPage.isClosed()) {
|
|
1888
1895
|
try {
|
|
1889
1896
|
const currentUrl = new URL(acctPage.url());
|
|
1890
|
-
if (
|
|
1897
|
+
if (
|
|
1898
|
+
currentUrl.origin !== qwenOrigin() ||
|
|
1899
|
+
currentUrl.pathname.includes("/auth") ||
|
|
1900
|
+
currentUrl.pathname.includes("/login") ||
|
|
1901
|
+
currentUrl.pathname.includes("_____tmd_____")
|
|
1902
|
+
) {
|
|
1891
1903
|
await acctPage.goto(qwenUrl("/"), {
|
|
1892
1904
|
waitUntil: "domcontentloaded",
|
|
1893
1905
|
timeout: config.timeouts.navigation,
|
|
@@ -1907,6 +1919,7 @@ export async function initPlaywrightForAccount(
|
|
|
1907
1919
|
} finally {
|
|
1908
1920
|
release();
|
|
1909
1921
|
}
|
|
1922
|
+
await evictIdlePlaywrightContextsToLimit().catch(() => {});
|
|
1910
1923
|
})();
|
|
1911
1924
|
|
|
1912
1925
|
try {
|
|
@@ -4171,19 +4184,32 @@ async function refreshHeadersInternal(
|
|
|
4171
4184
|
await executeReauth();
|
|
4172
4185
|
} else {
|
|
4173
4186
|
try {
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4187
|
+
const currentUrl = typeof page.url === "function" ? page.url() : "";
|
|
4188
|
+
const needsNavigation =
|
|
4189
|
+
!currentUrl.startsWith(qwenOrigin()) ||
|
|
4190
|
+
currentUrl.includes("/auth") ||
|
|
4191
|
+
currentUrl.includes("/login") ||
|
|
4192
|
+
currentUrl.includes("_____tmd_____");
|
|
4193
|
+
|
|
4194
|
+
if (needsNavigation) {
|
|
4195
|
+
await page.goto(qwenUrl("/"), {
|
|
4196
|
+
waitUntil: "domcontentloaded",
|
|
4197
|
+
timeout: Math.min(
|
|
4198
|
+
config.timeouts.navigation,
|
|
4199
|
+
boundedTimeoutMs,
|
|
4200
|
+
SESSION_PROBE_NAVIGATION_TIMEOUT_MS,
|
|
4201
|
+
),
|
|
4202
|
+
});
|
|
4203
|
+
await sleep(2000);
|
|
4204
|
+
}
|
|
4205
|
+
|
|
4206
|
+
const url = typeof page.url === "function" ? page.url() : "";
|
|
4184
4207
|
const isAuthUrl = url.includes("auth") || url.includes("login");
|
|
4185
4208
|
if (isAuthUrl) {
|
|
4186
|
-
await
|
|
4209
|
+
const refreshed = await refreshAccountToken(accountId);
|
|
4210
|
+
if (!refreshed.success) {
|
|
4211
|
+
await executeReauth();
|
|
4212
|
+
}
|
|
4187
4213
|
}
|
|
4188
4214
|
} catch (navErr) {
|
|
4189
4215
|
console.warn(
|
|
@@ -4243,12 +4269,18 @@ async function refreshHeadersInternal(
|
|
|
4243
4269
|
|
|
4244
4270
|
await captureQwenHeaders(accountId, undefined, boundedTimeoutMs);
|
|
4245
4271
|
|
|
4246
|
-
// Best-effort restore: header capture
|
|
4272
|
+
// Best-effort restore: if header capture left the tab on a punish/auth URL,
|
|
4273
|
+
// return to the canonical chat home without reloading an already-clean Qwen page.
|
|
4247
4274
|
const capturedPage = accountPages.get(accountId);
|
|
4248
4275
|
if (capturedPage && !capturedPage.isClosed()) {
|
|
4249
4276
|
try {
|
|
4250
4277
|
const currentUrl = new URL(capturedPage.url());
|
|
4251
|
-
if (
|
|
4278
|
+
if (
|
|
4279
|
+
currentUrl.origin !== qwenOrigin() ||
|
|
4280
|
+
currentUrl.pathname.includes("/auth") ||
|
|
4281
|
+
currentUrl.pathname.includes("/login") ||
|
|
4282
|
+
currentUrl.pathname.includes("_____tmd_____")
|
|
4283
|
+
) {
|
|
4252
4284
|
await capturedPage.goto(qwenUrl("/"), {
|
|
4253
4285
|
waitUntil: "domcontentloaded",
|
|
4254
4286
|
timeout: Math.min(config.timeouts.navigation, boundedTimeoutMs),
|
|
@@ -4693,7 +4725,13 @@ function priorityOrderForEviction(accountIds: string[]): string[] {
|
|
|
4693
4725
|
const ranked = getAccountsByPriority(accountIds.map((id) => ({ id })));
|
|
4694
4726
|
const rank = new Map<string, number>();
|
|
4695
4727
|
for (const [index, account] of ranked.entries()) {
|
|
4696
|
-
|
|
4728
|
+
// Accounts currently on cooldown (RateLimited / MembershipLimit / WafChallenge)
|
|
4729
|
+
// cannot serve traffic and must be evicted before any healthy warm account.
|
|
4730
|
+
if (getAccountCooldownInfo(account.id)) {
|
|
4731
|
+
rank.set(account.id, Number.MAX_SAFE_INTEGER);
|
|
4732
|
+
} else {
|
|
4733
|
+
rank.set(account.id, index);
|
|
4734
|
+
}
|
|
4697
4735
|
}
|
|
4698
4736
|
return orderContextsForEviction(
|
|
4699
4737
|
accountIds,
|
|
@@ -4750,15 +4788,21 @@ export async function closeIdlePlaywrightAccounts(
|
|
|
4750
4788
|
* PLAYWRIGHT_MAX_ACTIVE_CONTEXTS. Never closes a context whose account mutex
|
|
4751
4789
|
* is busy, so active streams are preserved.
|
|
4752
4790
|
*/
|
|
4753
|
-
export async function evictIdlePlaywrightContextsToLimit(
|
|
4754
|
-
|
|
4755
|
-
|
|
4791
|
+
export async function evictIdlePlaywrightContextsToLimit(
|
|
4792
|
+
targetLimit?: number,
|
|
4793
|
+
): Promise<number> {
|
|
4794
|
+
const configuredMax = config.playwright.maxActiveContexts;
|
|
4795
|
+
if (configuredMax <= 0) return 0;
|
|
4796
|
+
const max = targetLimit !== undefined ? Math.max(0, targetLimit) : configuredMax;
|
|
4756
4797
|
if (accountPages.size <= max) return 0;
|
|
4757
4798
|
|
|
4758
4799
|
const candidates = priorityOrderForEviction(
|
|
4759
4800
|
Array.from(accountPages.keys()).filter((accountId) => {
|
|
4760
4801
|
if (isAccountServingStream(accountId)) return false;
|
|
4761
|
-
|
|
4802
|
+
// A request may hold a stream lease while still acquiring the page mutex
|
|
4803
|
+
// (pre-stream setup). Closing that context would fail the request even
|
|
4804
|
+
// though the mutex still looks idle, so treat any lease as "in use".
|
|
4805
|
+
if (hasActiveAccountLease(accountId)) return false;
|
|
4762
4806
|
const mutex = accountMutexes.get(accountId);
|
|
4763
4807
|
return mutex?.isIdle();
|
|
4764
4808
|
}),
|
|
@@ -4772,6 +4816,7 @@ export async function evictIdlePlaywrightContextsToLimit(): Promise<number> {
|
|
|
4772
4816
|
for (const candidate of candidates) {
|
|
4773
4817
|
if (accountPages.size <= max) break;
|
|
4774
4818
|
if (isAccountServingStream(candidate.accountId)) continue;
|
|
4819
|
+
if (hasActiveAccountLease(candidate.accountId)) continue;
|
|
4775
4820
|
const mutex = accountMutexes.get(candidate.accountId);
|
|
4776
4821
|
if (!mutex?.isIdle()) continue;
|
|
4777
4822
|
|
package/src/services/qwen.ts
CHANGED
|
@@ -129,6 +129,20 @@ const browserStreamStates = new Map<string, BrowserStreamState>();
|
|
|
129
129
|
const browserStreamBindingPages = new WeakSet<object>();
|
|
130
130
|
const browserStreamBindingContexts = new WeakSet<object>();
|
|
131
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Whether the account currently has an in-flight browser stream. Captcha
|
|
134
|
+
* recovery uses this to avoid navigating the page that is feeding a live
|
|
135
|
+
* stream (it opens an ephemeral tab instead).
|
|
136
|
+
*/
|
|
137
|
+
export function hasActiveBrowserStreamsForAccount(accountId: string): boolean {
|
|
138
|
+
for (const state of browserStreamStates.values()) {
|
|
139
|
+
if (state.accountId === accountId && !state.done) {
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
|
|
132
146
|
/**
|
|
133
147
|
* Abort all active browser streams for an account immediately.
|
|
134
148
|
* Called when an account page is navigated by captcha recovery, closed, or reset,
|
|
@@ -1364,12 +1378,22 @@ async function createQwenBrowserResponse(
|
|
|
1364
1378
|
await ensureBrowserStreamBinding(page);
|
|
1365
1379
|
if (typeof (page as any).on === "function") {
|
|
1366
1380
|
const onFrameNavigated = (frame: any) => {
|
|
1367
|
-
if (
|
|
1368
|
-
|
|
1369
|
-
|
|
1381
|
+
if (
|
|
1382
|
+
!settled &&
|
|
1383
|
+
typeof (page as any).mainFrame === "function" &&
|
|
1384
|
+
frame === (page as any).mainFrame()
|
|
1385
|
+
) {
|
|
1386
|
+
const url =
|
|
1387
|
+
typeof frame.url === "function"
|
|
1388
|
+
? frame.url()
|
|
1389
|
+
: String(frame.url || "");
|
|
1390
|
+
// Same-origin SPA routing within Qwen Web (e.g. /c/<id>, /#) keeps
|
|
1391
|
+
// the live DOM and MUST NOT abort the stream. Only a genuine
|
|
1392
|
+
// unload (auth, Baxia punish, or another origin) kills the fetch.
|
|
1370
1393
|
if (
|
|
1371
1394
|
url.startsWith(qwenOrigin()) &&
|
|
1372
1395
|
!url.includes("/auth") &&
|
|
1396
|
+
!url.includes("/login") &&
|
|
1373
1397
|
!url.includes("_____tmd_____") &&
|
|
1374
1398
|
!url.includes("/punish")
|
|
1375
1399
|
) {
|
|
@@ -1393,10 +1417,12 @@ async function createQwenBrowserResponse(
|
|
|
1393
1417
|
};
|
|
1394
1418
|
(page as any).on("framenavigated", onFrameNavigated);
|
|
1395
1419
|
(page as any).on("close", onPageClosed);
|
|
1420
|
+
(page as any).on("crash", onPageClosed);
|
|
1396
1421
|
pageCleanup = () => {
|
|
1397
1422
|
try {
|
|
1398
1423
|
(page as any).off?.("framenavigated", onFrameNavigated);
|
|
1399
1424
|
(page as any).off?.("close", onPageClosed);
|
|
1425
|
+
(page as any).off?.("crash", onPageClosed);
|
|
1400
1426
|
} catch {}
|
|
1401
1427
|
};
|
|
1402
1428
|
}
|