qwenproxy-cli 1.6.3 → 1.6.4

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.
@@ -2841,9 +2841,10 @@ function getAccountMutex(accountId) {
2841
2841
  async function recoverStuckAccountMutex(accountId, mutex, key) {
2842
2842
  if (accountMutexes.get(accountId) !== mutex) return;
2843
2843
  const lockState = mutex.state();
2844
- if (lockState.heldForMs < PLAYWRIGHT_MUTEX_WAIT_MS || lockState.heldBy.startsWith("init:") || isAccountServingStream(accountId)) {
2844
+ const minLockTimeout = Math.max(PLAYWRIGHT_MUTEX_WAIT_MS, config.timeouts.headers);
2845
+ if (lockState.heldForMs < minLockTimeout || lockState.heldBy.startsWith("init:") || lockState.heldBy.startsWith("headers:") || lockState.heldBy.startsWith("refresh:") || lockState.heldBy.startsWith("profile-reset:") || isAccountServingStream(accountId)) {
2845
2846
  logger.warn(
2846
- `[Playwright] Skipping destructive mutex recovery | account=${accountId} | heldBy=${lockState.heldBy} | heldFor=${lockState.heldForMs}ms | limit=${PLAYWRIGHT_MUTEX_WAIT_MS}ms | servingStream=${isAccountServingStream(accountId)}`
2847
+ `[Playwright] Skipping destructive mutex recovery | account=${accountId} | heldBy=${lockState.heldBy} | heldFor=${lockState.heldForMs}ms | limit=${minLockTimeout}ms | servingStream=${isAccountServingStream(accountId)}`
2847
2848
  );
2848
2849
  return;
2849
2850
  }
@@ -3213,6 +3213,7 @@ __export(playwright_exports, {
3213
3213
  closeIdlePlaywrightAccounts: () => closeIdlePlaywrightAccounts,
3214
3214
  closePlaywrightForAccount: () => closePlaywrightForAccount,
3215
3215
  evictIdlePlaywrightContextsToLimit: () => evictIdlePlaywrightContextsToLimit,
3216
+ getAccountMutex: () => getAccountMutex,
3216
3217
  getActivePlaywrightAccountIds: () => getActivePlaywrightAccountIds,
3217
3218
  getBasicHeaders: () => getBasicHeaders,
3218
3219
  getCookies: () => getCookies,
@@ -3362,9 +3363,10 @@ function getAccountMutex(accountId) {
3362
3363
  async function recoverStuckAccountMutex(accountId, mutex, key) {
3363
3364
  if (accountMutexes.get(accountId) !== mutex) return;
3364
3365
  const lockState = mutex.state();
3365
- if (lockState.heldForMs < PLAYWRIGHT_MUTEX_WAIT_MS || lockState.heldBy.startsWith("init:") || isAccountServingStream(accountId)) {
3366
+ const minLockTimeout = Math.max(PLAYWRIGHT_MUTEX_WAIT_MS, config.timeouts.headers);
3367
+ if (lockState.heldForMs < minLockTimeout || lockState.heldBy.startsWith("init:") || lockState.heldBy.startsWith("headers:") || lockState.heldBy.startsWith("refresh:") || lockState.heldBy.startsWith("profile-reset:") || isAccountServingStream(accountId)) {
3366
3368
  logger.warn(
3367
- `[Playwright] Skipping destructive mutex recovery | account=${accountId} | heldBy=${lockState.heldBy} | heldFor=${lockState.heldForMs}ms | limit=${PLAYWRIGHT_MUTEX_WAIT_MS}ms | servingStream=${isAccountServingStream(accountId)}`
3369
+ `[Playwright] Skipping destructive mutex recovery | account=${accountId} | heldBy=${lockState.heldBy} | heldFor=${lockState.heldForMs}ms | limit=${minLockTimeout}ms | servingStream=${isAccountServingStream(accountId)}`
3368
3370
  );
3369
3371
  return;
3370
3372
  }
@@ -4166,7 +4168,8 @@ async function getBasicHeaders(accountId) {
4166
4168
  }
4167
4169
  const release = await acquireAccountMutex(
4168
4170
  accountId,
4169
- `headers:${accountId.substring(0, 12)}`
4171
+ `headers:${accountId.substring(0, 12)}`,
4172
+ Math.max(PLAYWRIGHT_MUTEX_WAIT_MS, config.timeouts.headers + 3e4)
4170
4173
  );
4171
4174
  try {
4172
4175
  touchAccountActivity(accountId);
@@ -4259,8 +4262,10 @@ async function getBasicHeaders(accountId) {
4259
4262
  return { cookie: cookie2, userAgent, bxV: bxV2, bxUa: bxUa2, bxUmidtoken: bxUmidtoken2, ...clientHints };
4260
4263
  }
4261
4264
  }
4265
+ let refreshedInThisCall = false;
4262
4266
  if (headersAge > HEADER_CACHE_TTL && !cache.refreshInProgress) {
4263
4267
  try {
4268
+ refreshedInThisCall = true;
4264
4269
  await refreshHeadersInternal(accountId);
4265
4270
  } catch (error) {
4266
4271
  if (!hadUsableHeaders) throw error;
@@ -4329,7 +4334,7 @@ async function getBasicHeaders(accountId) {
4329
4334
  } catch {
4330
4335
  }
4331
4336
  }
4332
- if (!hasRequiredQwenHeaders(cache.headers)) {
4337
+ if (!hasRequiredQwenHeaders(cache.headers) && !refreshedInThisCall) {
4333
4338
  console.log(
4334
4339
  `\u{1F504} [Playwright] Missing required anti-bot headers for ${accountId}, triggering header interception...`
4335
4340
  );
@@ -8857,7 +8862,7 @@ async function withQwenBrowserPage(accountId, fn, targetPath, operationTimeoutMs
8857
8862
  return fn(page);
8858
8863
  },
8859
8864
  operationTimeoutMs,
8860
- Math.min(config.timeouts.page, 3e4),
8865
+ Math.max(6e4, operationTimeoutMs),
8861
8866
  recoverOnTimeout
8862
8867
  );
8863
8868
  }
package/dist/index.js CHANGED
@@ -4323,6 +4323,7 @@ __export(playwright_exports, {
4323
4323
  closeIdlePlaywrightAccounts: () => closeIdlePlaywrightAccounts,
4324
4324
  closePlaywrightForAccount: () => closePlaywrightForAccount,
4325
4325
  evictIdlePlaywrightContextsToLimit: () => evictIdlePlaywrightContextsToLimit,
4326
+ getAccountMutex: () => getAccountMutex,
4326
4327
  getActivePlaywrightAccountIds: () => getActivePlaywrightAccountIds,
4327
4328
  getBasicHeaders: () => getBasicHeaders,
4328
4329
  getCookies: () => getCookies,
@@ -4472,9 +4473,10 @@ function getAccountMutex(accountId) {
4472
4473
  async function recoverStuckAccountMutex(accountId, mutex, key) {
4473
4474
  if (accountMutexes.get(accountId) !== mutex) return;
4474
4475
  const lockState = mutex.state();
4475
- if (lockState.heldForMs < PLAYWRIGHT_MUTEX_WAIT_MS || lockState.heldBy.startsWith("init:") || isAccountServingStream(accountId)) {
4476
+ const minLockTimeout = Math.max(PLAYWRIGHT_MUTEX_WAIT_MS, config.timeouts.headers);
4477
+ if (lockState.heldForMs < minLockTimeout || lockState.heldBy.startsWith("init:") || lockState.heldBy.startsWith("headers:") || lockState.heldBy.startsWith("refresh:") || lockState.heldBy.startsWith("profile-reset:") || isAccountServingStream(accountId)) {
4476
4478
  logger.warn(
4477
- `[Playwright] Skipping destructive mutex recovery | account=${accountId} | heldBy=${lockState.heldBy} | heldFor=${lockState.heldForMs}ms | limit=${PLAYWRIGHT_MUTEX_WAIT_MS}ms | servingStream=${isAccountServingStream(accountId)}`
4479
+ `[Playwright] Skipping destructive mutex recovery | account=${accountId} | heldBy=${lockState.heldBy} | heldFor=${lockState.heldForMs}ms | limit=${minLockTimeout}ms | servingStream=${isAccountServingStream(accountId)}`
4478
4480
  );
4479
4481
  return;
4480
4482
  }
@@ -5276,7 +5278,8 @@ async function getBasicHeaders(accountId) {
5276
5278
  }
5277
5279
  const release = await acquireAccountMutex(
5278
5280
  accountId,
5279
- `headers:${accountId.substring(0, 12)}`
5281
+ `headers:${accountId.substring(0, 12)}`,
5282
+ Math.max(PLAYWRIGHT_MUTEX_WAIT_MS, config.timeouts.headers + 3e4)
5280
5283
  );
5281
5284
  try {
5282
5285
  touchAccountActivity(accountId);
@@ -5369,8 +5372,10 @@ async function getBasicHeaders(accountId) {
5369
5372
  return { cookie: cookie2, userAgent, bxV: bxV2, bxUa: bxUa2, bxUmidtoken: bxUmidtoken2, ...clientHints };
5370
5373
  }
5371
5374
  }
5375
+ let refreshedInThisCall = false;
5372
5376
  if (headersAge > HEADER_CACHE_TTL && !cache3.refreshInProgress) {
5373
5377
  try {
5378
+ refreshedInThisCall = true;
5374
5379
  await refreshHeadersInternal(accountId);
5375
5380
  } catch (error) {
5376
5381
  if (!hadUsableHeaders) throw error;
@@ -5439,7 +5444,7 @@ async function getBasicHeaders(accountId) {
5439
5444
  } catch {
5440
5445
  }
5441
5446
  }
5442
- if (!hasRequiredQwenHeaders(cache3.headers)) {
5447
+ if (!hasRequiredQwenHeaders(cache3.headers) && !refreshedInThisCall) {
5443
5448
  console.log(
5444
5449
  `\u{1F504} [Playwright] Missing required anti-bot headers for ${accountId}, triggering header interception...`
5445
5450
  );
@@ -9825,7 +9830,7 @@ function isModelNotFoundError(err) {
9825
9830
  function isNetworkLikeError(err) {
9826
9831
  if (err instanceof QwenNetworkError) return true;
9827
9832
  const message = errMessage(err).toLowerCase();
9828
- return message === "network error" || message.includes("failed to fetch") || message.includes("fetch failed") || message.includes("network connection was lost") || message.includes("connection reset") || message.includes("connection closed") || message.includes("socket hang up") || message.includes("econnreset") || message.includes("econnrefused") || message.includes("etimedout");
9833
+ return message === "network error" || message.includes("failed to fetch") || message.includes("fetch failed") || message.includes("network connection was lost") || message.includes("connection reset") || message.includes("connection closed") || message.includes("socket hang up") || message.includes("econnreset") || message.includes("econnrefused") || message.includes("etimedout") || message.includes("stream ended before response headers") || message.includes("timed out waiting for response headers");
9829
9834
  }
9830
9835
  function isCorruptedChatHistoryError(err) {
9831
9836
  const message = errMessage(err).toLowerCase();
@@ -12047,7 +12052,7 @@ async function withQwenBrowserPage(accountId, fn, targetPath, operationTimeoutMs
12047
12052
  return fn(page);
12048
12053
  },
12049
12054
  operationTimeoutMs,
12050
- Math.min(config.timeouts.page, 3e4),
12055
+ Math.max(6e4, operationTimeoutMs),
12051
12056
  recoverOnTimeout
12052
12057
  );
12053
12058
  }
@@ -26823,7 +26828,7 @@ function getAppVersion() {
26823
26828
  }
26824
26829
  } catch {
26825
26830
  }
26826
- cachedVersion = "v1.6.3";
26831
+ cachedVersion = "v1.6.4";
26827
26832
  return cachedVersion;
26828
26833
  }
26829
26834
  var cachedVersion;
package/dist/login.js CHANGED
@@ -3190,6 +3190,7 @@ __export(playwright_exports, {
3190
3190
  closeIdlePlaywrightAccounts: () => closeIdlePlaywrightAccounts,
3191
3191
  closePlaywrightForAccount: () => closePlaywrightForAccount,
3192
3192
  evictIdlePlaywrightContextsToLimit: () => evictIdlePlaywrightContextsToLimit,
3193
+ getAccountMutex: () => getAccountMutex,
3193
3194
  getActivePlaywrightAccountIds: () => getActivePlaywrightAccountIds,
3194
3195
  getBasicHeaders: () => getBasicHeaders,
3195
3196
  getCookies: () => getCookies,
@@ -3339,9 +3340,10 @@ function getAccountMutex(accountId) {
3339
3340
  async function recoverStuckAccountMutex(accountId, mutex, key) {
3340
3341
  if (accountMutexes.get(accountId) !== mutex) return;
3341
3342
  const lockState = mutex.state();
3342
- if (lockState.heldForMs < PLAYWRIGHT_MUTEX_WAIT_MS || lockState.heldBy.startsWith("init:") || isAccountServingStream(accountId)) {
3343
+ const minLockTimeout = Math.max(PLAYWRIGHT_MUTEX_WAIT_MS, config.timeouts.headers);
3344
+ if (lockState.heldForMs < minLockTimeout || lockState.heldBy.startsWith("init:") || lockState.heldBy.startsWith("headers:") || lockState.heldBy.startsWith("refresh:") || lockState.heldBy.startsWith("profile-reset:") || isAccountServingStream(accountId)) {
3343
3345
  logger.warn(
3344
- `[Playwright] Skipping destructive mutex recovery | account=${accountId} | heldBy=${lockState.heldBy} | heldFor=${lockState.heldForMs}ms | limit=${PLAYWRIGHT_MUTEX_WAIT_MS}ms | servingStream=${isAccountServingStream(accountId)}`
3346
+ `[Playwright] Skipping destructive mutex recovery | account=${accountId} | heldBy=${lockState.heldBy} | heldFor=${lockState.heldForMs}ms | limit=${minLockTimeout}ms | servingStream=${isAccountServingStream(accountId)}`
3345
3347
  );
3346
3348
  return;
3347
3349
  }
@@ -4143,7 +4145,8 @@ async function getBasicHeaders(accountId) {
4143
4145
  }
4144
4146
  const release = await acquireAccountMutex(
4145
4147
  accountId,
4146
- `headers:${accountId.substring(0, 12)}`
4148
+ `headers:${accountId.substring(0, 12)}`,
4149
+ Math.max(PLAYWRIGHT_MUTEX_WAIT_MS, config.timeouts.headers + 3e4)
4147
4150
  );
4148
4151
  try {
4149
4152
  touchAccountActivity(accountId);
@@ -4236,8 +4239,10 @@ async function getBasicHeaders(accountId) {
4236
4239
  return { cookie: cookie2, userAgent, bxV: bxV2, bxUa: bxUa2, bxUmidtoken: bxUmidtoken2, ...clientHints };
4237
4240
  }
4238
4241
  }
4242
+ let refreshedInThisCall = false;
4239
4243
  if (headersAge > HEADER_CACHE_TTL && !cache.refreshInProgress) {
4240
4244
  try {
4245
+ refreshedInThisCall = true;
4241
4246
  await refreshHeadersInternal(accountId);
4242
4247
  } catch (error) {
4243
4248
  if (!hadUsableHeaders) throw error;
@@ -4306,7 +4311,7 @@ async function getBasicHeaders(accountId) {
4306
4311
  } catch {
4307
4312
  }
4308
4313
  }
4309
- if (!hasRequiredQwenHeaders(cache.headers)) {
4314
+ if (!hasRequiredQwenHeaders(cache.headers) && !refreshedInThisCall) {
4310
4315
  console.log(
4311
4316
  `\u{1F504} [Playwright] Missing required anti-bot headers for ${accountId}, triggering header interception...`
4312
4317
  );
@@ -3104,9 +3104,10 @@ function getAccountMutex(accountId) {
3104
3104
  async function recoverStuckAccountMutex(accountId, mutex, key) {
3105
3105
  if (accountMutexes.get(accountId) !== mutex) return;
3106
3106
  const lockState = mutex.state();
3107
- if (lockState.heldForMs < PLAYWRIGHT_MUTEX_WAIT_MS || lockState.heldBy.startsWith("init:") || isAccountServingStream(accountId)) {
3107
+ const minLockTimeout = Math.max(PLAYWRIGHT_MUTEX_WAIT_MS, config.timeouts.headers);
3108
+ if (lockState.heldForMs < minLockTimeout || lockState.heldBy.startsWith("init:") || lockState.heldBy.startsWith("headers:") || lockState.heldBy.startsWith("refresh:") || lockState.heldBy.startsWith("profile-reset:") || isAccountServingStream(accountId)) {
3108
3109
  logger.warn(
3109
- `[Playwright] Skipping destructive mutex recovery | account=${accountId} | heldBy=${lockState.heldBy} | heldFor=${lockState.heldForMs}ms | limit=${PLAYWRIGHT_MUTEX_WAIT_MS}ms | servingStream=${isAccountServingStream(accountId)}`
3110
+ `[Playwright] Skipping destructive mutex recovery | account=${accountId} | heldBy=${lockState.heldBy} | heldFor=${lockState.heldForMs}ms | limit=${minLockTimeout}ms | servingStream=${isAccountServingStream(accountId)}`
3110
3111
  );
3111
3112
  return;
3112
3113
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qwenproxy-cli",
3
- "version": "1.6.3",
3
+ "version": "1.6.4",
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": {
@@ -17,6 +17,6 @@ export function getAppVersion(): string {
17
17
  }
18
18
  }
19
19
  } catch {}
20
- cachedVersion = "v1.6.3";
20
+ cachedVersion = "v1.6.4";
21
21
  return cachedVersion;
22
22
  }
@@ -390,7 +390,9 @@ export function isNetworkLikeError(err: unknown): boolean {
390
390
  message.includes("socket hang up") ||
391
391
  message.includes("econnreset") ||
392
392
  message.includes("econnrefused") ||
393
- message.includes("etimedout")
393
+ message.includes("etimedout") ||
394
+ message.includes("stream ended before response headers") ||
395
+ message.includes("timed out waiting for response headers")
394
396
  );
395
397
  }
396
398
 
@@ -166,7 +166,7 @@ export function buildChromiumLaunchArgs(viewport: {
166
166
  const ACCOUNT_MUTEX_MAX_HOLD_MS = 180_000;
167
167
  const accountMutexes = new Map<string, Mutex>();
168
168
 
169
- function getAccountMutex(accountId: string): Mutex {
169
+ export function getAccountMutex(accountId: string): Mutex {
170
170
  let mutex = accountMutexes.get(accountId);
171
171
  if (!mutex) {
172
172
  mutex = new Mutex(`playwright:${accountId.substring(0, 8)}`, ACCOUNT_MUTEX_MAX_HOLD_MS);
@@ -189,15 +189,20 @@ async function recoverStuckAccountMutex(
189
189
  const lockState = mutex.state();
190
190
  // A lock is only considered stuck if it has been held for at least PLAYWRIGHT_MUTEX_WAIT_MS (60s).
191
191
  // A waiter with a short timeout (e.g. 5s) timing out does NOT mean the lock holder is stuck!
192
- // Furthermore, never nuke an account while it is initializing (init: takes 20-30s)
193
- // or while it is actively serving a stream to a user.
192
+ // Furthermore, never nuke an account while it is initializing (init: takes 20-30s),
193
+ // capturing/refreshing headers (headers/refresh can take up to HEADERS_TIMEOUT),
194
+ // running page operations, or while it is actively serving a stream to a user.
195
+ const minLockTimeout = Math.max(PLAYWRIGHT_MUTEX_WAIT_MS, config.timeouts.headers);
194
196
  if (
195
- lockState.heldForMs < PLAYWRIGHT_MUTEX_WAIT_MS ||
197
+ lockState.heldForMs < minLockTimeout ||
196
198
  lockState.heldBy.startsWith("init:") ||
199
+ lockState.heldBy.startsWith("headers:") ||
200
+ lockState.heldBy.startsWith("refresh:") ||
201
+ lockState.heldBy.startsWith("profile-reset:") ||
197
202
  isAccountServingStream(accountId)
198
203
  ) {
199
204
  logger.warn(
200
- `[Playwright] Skipping destructive mutex recovery | account=${accountId} | heldBy=${lockState.heldBy} | heldFor=${lockState.heldForMs}ms | limit=${PLAYWRIGHT_MUTEX_WAIT_MS}ms | servingStream=${isAccountServingStream(accountId)}`,
205
+ `[Playwright] Skipping destructive mutex recovery | account=${accountId} | heldBy=${lockState.heldBy} | heldFor=${lockState.heldForMs}ms | limit=${minLockTimeout}ms | servingStream=${isAccountServingStream(accountId)}`,
201
206
  );
202
207
  return;
203
208
  }
@@ -1246,6 +1251,7 @@ export async function getBasicHeaders(accountId: string): Promise<{
1246
1251
  const release = await acquireAccountMutex(
1247
1252
  accountId,
1248
1253
  `headers:${accountId.substring(0, 12)}`,
1254
+ Math.max(PLAYWRIGHT_MUTEX_WAIT_MS, config.timeouts.headers + 30_000),
1249
1255
  );
1250
1256
  try {
1251
1257
  touchAccountActivity(accountId);
@@ -1369,8 +1375,10 @@ export async function getBasicHeaders(accountId: string): Promise<{
1369
1375
 
1370
1376
  // Refresh headers if stale. A valid cached set remains usable when a
1371
1377
  // browser recapture transiently fails; a cold/partial cache must fail.
1378
+ let refreshedInThisCall = false;
1372
1379
  if (headersAge > HEADER_CACHE_TTL && !cache.refreshInProgress) {
1373
1380
  try {
1381
+ refreshedInThisCall = true;
1374
1382
  await refreshHeadersInternal(accountId);
1375
1383
  } catch (error) {
1376
1384
  if (!hadUsableHeaders) throw error;
@@ -1443,7 +1451,7 @@ export async function getBasicHeaders(accountId: string): Promise<{
1443
1451
  } catch {}
1444
1452
  }
1445
1453
 
1446
- if (!hasRequiredQwenHeaders(cache.headers)) {
1454
+ if (!hasRequiredQwenHeaders(cache.headers) && !refreshedInThisCall) {
1447
1455
  console.log(
1448
1456
  `🔄 [Playwright] Missing required anti-bot headers for ${accountId}, triggering header interception...`,
1449
1457
  );
@@ -818,7 +818,7 @@ async function withQwenBrowserPage<T>(
818
818
  return fn(page);
819
819
  },
820
820
  operationTimeoutMs,
821
- Math.min(config.timeouts.page, 30_000),
821
+ Math.max(60_000, operationTimeoutMs),
822
822
  recoverOnTimeout,
823
823
  );
824
824
  }