qwenproxy-cli 1.6.10 → 1.6.12

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.
@@ -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,
@@ -992,7 +1006,7 @@ export async function requestQwenTextInBrowser(
992
1006
 
993
1007
  const evaluateRequest = (page: Page) =>
994
1008
  page.evaluate(
995
- async ({ url, method, headers, body, referrer, timeoutMs, tz }: {
1009
+ async ({ url, method, headers, body, referrer, timeoutMs, tz, version }: {
996
1010
  url: string;
997
1011
  method: "GET" | "POST" | "DELETE";
998
1012
  headers: Record<string, string>;
@@ -1000,6 +1014,7 @@ export async function requestQwenTextInBrowser(
1000
1014
  referrer?: string;
1001
1015
  timeoutMs: number;
1002
1016
  tz: string;
1017
+ version: string;
1003
1018
  }): Promise<BrowserTextResponse> => {
1004
1019
  const controller = new AbortController();
1005
1020
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
@@ -1024,7 +1039,7 @@ export async function requestQwenTextInBrowser(
1024
1039
  headers: {
1025
1040
  accept: "application/json, text/plain, */*",
1026
1041
  source: "web",
1027
- version: "0.3.11",
1042
+ version,
1028
1043
  "x-request-origin": "https://chat.qwen.ai",
1029
1044
  timezone: tz,
1030
1045
  authorization: `Bearer ${rTok}`,
@@ -1073,6 +1088,7 @@ export async function requestQwenTextInBrowser(
1073
1088
  referrer: options.referrer,
1074
1089
  timeoutMs: options.timeoutMs ?? Math.min(config.timeouts.page, 20_000),
1075
1090
  tz: formatAsciiTimezone(),
1091
+ version: getQwenWebVersion(),
1076
1092
  },
1077
1093
  );
1078
1094
  const recoverOnTimeout = !options.noMutexRecovery;
@@ -1364,12 +1380,22 @@ async function createQwenBrowserResponse(
1364
1380
  await ensureBrowserStreamBinding(page);
1365
1381
  if (typeof (page as any).on === "function") {
1366
1382
  const onFrameNavigated = (frame: any) => {
1367
- if (!settled && typeof (page as any).mainFrame === "function" && frame === (page as any).mainFrame()) {
1368
- const url = typeof frame.url === "function" ? frame.url() : String(frame.url || "");
1369
- // Safe SPA in-app routing within Qwen Web (e.g. /c/<id>, /#) must NEVER abort active streams
1383
+ if (
1384
+ !settled &&
1385
+ typeof (page as any).mainFrame === "function" &&
1386
+ frame === (page as any).mainFrame()
1387
+ ) {
1388
+ const url =
1389
+ typeof frame.url === "function"
1390
+ ? frame.url()
1391
+ : String(frame.url || "");
1392
+ // Same-origin SPA routing within Qwen Web (e.g. /c/<id>, /#) keeps
1393
+ // the live DOM and MUST NOT abort the stream. Only a genuine
1394
+ // unload (auth, Baxia punish, or another origin) kills the fetch.
1370
1395
  if (
1371
1396
  url.startsWith(qwenOrigin()) &&
1372
1397
  !url.includes("/auth") &&
1398
+ !url.includes("/login") &&
1373
1399
  !url.includes("_____tmd_____") &&
1374
1400
  !url.includes("/punish")
1375
1401
  ) {
@@ -1393,10 +1419,12 @@ async function createQwenBrowserResponse(
1393
1419
  };
1394
1420
  (page as any).on("framenavigated", onFrameNavigated);
1395
1421
  (page as any).on("close", onPageClosed);
1422
+ (page as any).on("crash", onPageClosed);
1396
1423
  pageCleanup = () => {
1397
1424
  try {
1398
1425
  (page as any).off?.("framenavigated", onFrameNavigated);
1399
1426
  (page as any).off?.("close", onPageClosed);
1427
+ (page as any).off?.("crash", onPageClosed);
1400
1428
  } catch {}
1401
1429
  };
1402
1430
  }
@@ -2369,7 +2397,7 @@ export async function deleteSingleQwenChat(
2369
2397
 
2370
2398
  const requestHeaders: Record<string, string> = {
2371
2399
  source: "web",
2372
- version: getQwenWebVersion() || "0.3.26",
2400
+ version: getQwenWebVersion(),
2373
2401
  timezone: formatAsciiTimezone(),
2374
2402
  "x-request-id": crypto.randomUUID(),
2375
2403
  Referer: qwenUrl(`/c/${encodeURIComponent(chatId)}`),
@@ -2422,7 +2450,7 @@ export async function fetchRemoteQwenChats(
2422
2450
  }
2423
2451
 
2424
2452
  const requestHeaders: Record<string, string> = {
2425
- version: getQwenWebVersion() || "0.3.26",
2453
+ version: getQwenWebVersion(),
2426
2454
  timezone: formatAsciiTimezone(),
2427
2455
  "x-request-id": crypto.randomUUID(),
2428
2456
  Referer: qwenUrl("/settings/chats"),
@@ -13,6 +13,55 @@ let running = false;
13
13
  let intervalId: ReturnType<typeof setInterval> | null = null;
14
14
  let cycleInProgress = false;
15
15
 
16
+ /**
17
+ * A keep-alive failure (navigation timeout, transient WAF, closed context
18
+ * that is not yet reported as closed) must not retry on every cycle: it would
19
+ * hammer the same broken page and bury the logs. Back off per account with the
20
+ * usual exponential schedule, capped at five minutes.
21
+ */
22
+ const KEEP_ALIVE_BACKOFF_BASE_MS = 2_000;
23
+ const KEEP_ALIVE_BACKOFF_MAX_MS = 5 * 60_000;
24
+
25
+ interface KeepAliveRetryState {
26
+ failures: number;
27
+ nextAttemptAt: number;
28
+ }
29
+
30
+ const keepAliveRetries = new Map<string, KeepAliveRetryState>();
31
+
32
+ export function computeSessionKeeperBackoffMs(failures: number): number {
33
+ const exponent = Math.max(0, Math.min(failures - 1, 8));
34
+ return Math.min(
35
+ KEEP_ALIVE_BACKOFF_MAX_MS,
36
+ KEEP_ALIVE_BACKOFF_BASE_MS * 2 ** exponent,
37
+ );
38
+ }
39
+
40
+ function isKeepAliveBackedOff(accountId: string): boolean {
41
+ const state = keepAliveRetries.get(accountId);
42
+ return Boolean(state && Date.now() < state.nextAttemptAt);
43
+ }
44
+
45
+ function recordKeepAliveFailure(accountId: string): number {
46
+ const state = keepAliveRetries.get(accountId) ?? {
47
+ failures: 0,
48
+ nextAttemptAt: 0,
49
+ };
50
+ state.failures += 1;
51
+ const backoffMs = computeSessionKeeperBackoffMs(state.failures);
52
+ state.nextAttemptAt = Date.now() + backoffMs;
53
+ keepAliveRetries.set(accountId, state);
54
+ return backoffMs;
55
+ }
56
+
57
+ export function resetSessionKeeperRetryStateForTests(): void {
58
+ keepAliveRetries.clear();
59
+ }
60
+
61
+ export function isSessionKeeperAccountBackedOff(accountId: string): boolean {
62
+ return isKeepAliveBackedOff(accountId);
63
+ }
64
+
16
65
  export function isSessionKeeperRunning(): boolean {
17
66
  return running;
18
67
  }
@@ -24,19 +73,31 @@ async function runKeepAliveCycle(): Promise<void> {
24
73
  if (config.sessionKeeper.enabled) {
25
74
  const accountIds = getActivePlaywrightAccountIds();
26
75
  for (const accountId of accountIds) {
27
- await keepAlivePlaywrightAccount(accountId).catch((error) => {
76
+ if (isKeepAliveBackedOff(accountId)) {
77
+ continue;
78
+ }
79
+ try {
80
+ const keptAlive = await keepAlivePlaywrightAccount(accountId);
81
+ if (keptAlive) {
82
+ keepAliveRetries.delete(accountId);
83
+ }
84
+ } catch (error) {
28
85
  // Shutdown/eviction closes contexts while a cycle is in flight; the
29
86
  // resulting "already closed" rejection is benign. The old substring
30
87
  // filter ("Target closed"/"Page is closed") missed Playwright's real
31
88
  // message ("Target page, context or browser has been closed") and
32
89
  // leaked a warning on every Ctrl+C.
33
- if (isPlaywrightAlreadyClosedError(error)) return;
34
- const message =
35
- error instanceof Error ? error.message : String(error);
36
- console.warn(
37
- `[SessionKeeper] Keep-alive failed for ${accountId}: ${message}`,
38
- );
39
- });
90
+ if (isPlaywrightAlreadyClosedError(error)) {
91
+ keepAliveRetries.delete(accountId);
92
+ } else {
93
+ const message =
94
+ error instanceof Error ? error.message : String(error);
95
+ const backoffMs = recordKeepAliveFailure(accountId);
96
+ console.warn(
97
+ `[SessionKeeper] Keep-alive failed for ${accountId}: ${message} (retry in ${Math.round(backoffMs / 1000)}s)`,
98
+ );
99
+ }
100
+ }
40
101
  await sleep(humanDelay(250, 900));
41
102
  }
42
103
  }
@@ -81,6 +142,7 @@ export function stopSessionKeeper(): void {
81
142
  intervalId = null;
82
143
  }
83
144
  cycleInProgress = false;
145
+ keepAliveRetries.clear();
84
146
  }
85
147
 
86
148
  export async function runSessionKeeperOnceForTesting(): Promise<void> {