qwenproxy-cli 1.6.2 → 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.
@@ -14,6 +14,25 @@ var __export = (target, all) => {
14
14
  };
15
15
 
16
16
  // src/core/paths.ts
17
+ var paths_exports = {};
18
+ __export(paths_exports, {
19
+ ensureDataDirs: () => ensureDataDirs,
20
+ getAccountPriorityPath: () => getAccountPriorityPath,
21
+ getAccountProfilePath: () => getAccountProfilePath,
22
+ getDataDir: () => getDataDir,
23
+ getDbDir: () => getDbDir,
24
+ getDbPath: () => getDbPath,
25
+ getEncryptionKeyPath: () => getEncryptionKeyPath,
26
+ getEnvFilePath: () => getEnvFilePath,
27
+ getLogsDir: () => getLogsDir,
28
+ getOsGlobalDataDir: () => getOsGlobalDataDir,
29
+ getProfilesDir: () => getProfilesDir,
30
+ getServerLogFilePath: () => getServerLogFilePath,
31
+ getSyncStatePath: () => getSyncStatePath,
32
+ getTuiSettingsPath: () => getTuiSettingsPath,
33
+ isRunningUnderNodeTest: () => isRunningUnderNodeTest,
34
+ resolveDataDir: () => resolveDataDir
35
+ });
17
36
  import fs from "node:fs";
18
37
  import path from "node:path";
19
38
  import os from "node:os";
@@ -78,6 +97,12 @@ function getAccountProfilePath(accountId, customDataDir) {
78
97
  function getAccountPriorityPath(customDataDir) {
79
98
  return path.join(customDataDir || getDataDir(), "account-priority.json");
80
99
  }
100
+ function getSyncStatePath(customDataDir) {
101
+ return path.join(customDataDir || getDataDir(), "sync-state.json");
102
+ }
103
+ function getTuiSettingsPath(customDataDir) {
104
+ return path.join(customDataDir || getDataDir(), "tui-settings.json");
105
+ }
81
106
  function getEnvFilePath(customDataDir) {
82
107
  if (isRunningUnderNodeTest()) {
83
108
  return path.join(customDataDir || getDataDir(), ".env");
@@ -88,6 +113,26 @@ function getEnvFilePath(customDataDir) {
88
113
  }
89
114
  return path.join(customDataDir || getDataDir(), ".env");
90
115
  }
116
+ function getLogsDir(customDataDir) {
117
+ return path.join(customDataDir || getDataDir(), "logs");
118
+ }
119
+ function getServerLogFilePath(customDataDir) {
120
+ return path.join(getLogsDir(customDataDir), "server.log");
121
+ }
122
+ function ensureDataDirs(targetDataDir) {
123
+ const root = targetDataDir || getDataDir();
124
+ const dbDir = getDbDir(root);
125
+ const profilesDir = getProfilesDir(root);
126
+ const logsDir = getLogsDir(root);
127
+ try {
128
+ if (!fs.existsSync(root)) fs.mkdirSync(root, { recursive: true });
129
+ if (!fs.existsSync(dbDir)) fs.mkdirSync(dbDir, { recursive: true });
130
+ if (!fs.existsSync(profilesDir)) fs.mkdirSync(profilesDir, { recursive: true });
131
+ if (!fs.existsSync(logsDir)) fs.mkdirSync(logsDir, { recursive: true });
132
+ } catch (err) {
133
+ console.error(`[Paths] Error ensuring data directories at ${root}:`, err?.message || String(err));
134
+ }
135
+ }
91
136
  var init_paths = __esm({
92
137
  "src/core/paths.ts"() {
93
138
  "use strict";
@@ -2796,9 +2841,10 @@ function getAccountMutex(accountId) {
2796
2841
  async function recoverStuckAccountMutex(accountId, mutex, key) {
2797
2842
  if (accountMutexes.get(accountId) !== mutex) return;
2798
2843
  const lockState = mutex.state();
2799
- 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)) {
2800
2846
  logger.warn(
2801
- `[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)}`
2802
2848
  );
2803
2849
  return;
2804
2850
  }
@@ -4470,7 +4516,24 @@ async function refreshAccountToken(accountId) {
4470
4516
  return { success: false, error: "Could not refresh or re-authenticate token" };
4471
4517
  }
4472
4518
  async function captureQwenHeaders(accountId, pageOverride, timeoutMs = config.timeouts.headers, triggerGraceMs = HEADER_CAPTURE_TRIGGER_GRACE_MS) {
4473
- const page = pageOverride ?? accountPages.get(accountId);
4519
+ let page = pageOverride ?? accountPages.get(accountId);
4520
+ if (!page || page.isClosed()) {
4521
+ const { isRunningUnderNodeTest: isRunningUnderNodeTest3 } = await Promise.resolve().then(() => (init_paths(), paths_exports));
4522
+ if (!isRunningUnderNodeTest3()) {
4523
+ const { getAccountCredentials: getAccountCredentials2 } = await Promise.resolve().then(() => (init_accounts(), accounts_exports));
4524
+ const creds = getAccountCredentials2(accountId);
4525
+ if (creds) {
4526
+ await initPlaywrightForAccount(
4527
+ creds,
4528
+ config.playwright.headless,
4529
+ config.playwright.browser,
4530
+ { skipHeaderCapture: true }
4531
+ ).catch(() => {
4532
+ });
4533
+ page = accountPages.get(accountId);
4534
+ }
4535
+ }
4536
+ }
4474
4537
  if (!page || page.isClosed()) {
4475
4538
  throw new Error(`Playwright page unavailable for header capture: ${accountId}`);
4476
4539
  }
@@ -5171,8 +5234,10 @@ async function evictIdlePlaywrightContextsToLimit() {
5171
5234
  if (accountPages.size <= max) return 0;
5172
5235
  const candidates = priorityOrderForEviction(
5173
5236
  Array.from(accountPages.keys()).filter((accountId) => {
5237
+ if (isAccountServingStream(accountId)) return false;
5238
+ if (isAccountRecentlyActive(accountId, 3e4)) return false;
5174
5239
  const mutex = accountMutexes.get(accountId);
5175
- return mutex?.isIdle() && !isAccountServingStream(accountId);
5240
+ return mutex?.isIdle();
5176
5241
  })
5177
5242
  ).map((accountId) => ({
5178
5243
  accountId,
@@ -5285,6 +5350,10 @@ async function closePlaywrightForAccount(accountId) {
5285
5350
  release();
5286
5351
  }
5287
5352
  }
5353
+ function isAccountRecentlyActive(accountId, maxIdleMs = 3e5) {
5354
+ const last = lastAccountActivity.get(accountId) ?? 0;
5355
+ return last > 0 && Date.now() - last < maxIdleMs;
5356
+ }
5288
5357
  var requireLocal, contextInitHooks, ACCOUNT_MUTEX_MAX_HOLD_MS, accountMutexes, accountContexts, accountPages, inFlightAccountInits, headerCaches, HEADER_CACHE_TTL, COOKIE_CACHE_TTL, cookieCaches, lastAccountActivity, lastKeepAliveNavigation, lastProactiveTokenRefresh, profileResetQueue, profileResetChain, closingAllPlaywright, sleep2, HEADER_CAPTURE_SETTLE_MS, PLAYWRIGHT_MUTEX_WAIT_MS, ACCOUNT_PAGE_OPERATION_TIMEOUT_MS, SESSION_PROBE_NAVIGATION_TIMEOUT_MS, HEADER_CAPTURE_TRIGGER_GRACE_MS, FIRST_TRIGGER_GRACE_MS, HEADER_CAPTURE_TRIGGER_ATTEMPTS, CHAT_INPUT_APPEAR_TIMEOUT_MS, CHAT_INPUT_ACTION_TIMEOUT_MS, PROFILE_RESET_TIMEOUT_MS;
5289
5358
  var init_playwright = __esm({
5290
5359
  "src/services/playwright.ts"() {
@@ -52,6 +52,25 @@ var init_process_guard = __esm({
52
52
  });
53
53
 
54
54
  // src/core/paths.ts
55
+ var paths_exports = {};
56
+ __export(paths_exports, {
57
+ ensureDataDirs: () => ensureDataDirs,
58
+ getAccountPriorityPath: () => getAccountPriorityPath,
59
+ getAccountProfilePath: () => getAccountProfilePath,
60
+ getDataDir: () => getDataDir,
61
+ getDbDir: () => getDbDir,
62
+ getDbPath: () => getDbPath,
63
+ getEncryptionKeyPath: () => getEncryptionKeyPath,
64
+ getEnvFilePath: () => getEnvFilePath,
65
+ getLogsDir: () => getLogsDir,
66
+ getOsGlobalDataDir: () => getOsGlobalDataDir,
67
+ getProfilesDir: () => getProfilesDir,
68
+ getServerLogFilePath: () => getServerLogFilePath,
69
+ getSyncStatePath: () => getSyncStatePath,
70
+ getTuiSettingsPath: () => getTuiSettingsPath,
71
+ isRunningUnderNodeTest: () => isRunningUnderNodeTest,
72
+ resolveDataDir: () => resolveDataDir
73
+ });
55
74
  import fs from "node:fs";
56
75
  import path from "node:path";
57
76
  import os from "node:os";
@@ -116,6 +135,12 @@ function getAccountProfilePath(accountId, customDataDir) {
116
135
  function getAccountPriorityPath(customDataDir) {
117
136
  return path.join(customDataDir || getDataDir(), "account-priority.json");
118
137
  }
138
+ function getSyncStatePath(customDataDir) {
139
+ return path.join(customDataDir || getDataDir(), "sync-state.json");
140
+ }
141
+ function getTuiSettingsPath(customDataDir) {
142
+ return path.join(customDataDir || getDataDir(), "tui-settings.json");
143
+ }
119
144
  function getEnvFilePath(customDataDir) {
120
145
  if (isRunningUnderNodeTest()) {
121
146
  return path.join(customDataDir || getDataDir(), ".env");
@@ -126,6 +151,26 @@ function getEnvFilePath(customDataDir) {
126
151
  }
127
152
  return path.join(customDataDir || getDataDir(), ".env");
128
153
  }
154
+ function getLogsDir(customDataDir) {
155
+ return path.join(customDataDir || getDataDir(), "logs");
156
+ }
157
+ function getServerLogFilePath(customDataDir) {
158
+ return path.join(getLogsDir(customDataDir), "server.log");
159
+ }
160
+ function ensureDataDirs(targetDataDir) {
161
+ const root = targetDataDir || getDataDir();
162
+ const dbDir = getDbDir(root);
163
+ const profilesDir = getProfilesDir(root);
164
+ const logsDir = getLogsDir(root);
165
+ try {
166
+ if (!fs.existsSync(root)) fs.mkdirSync(root, { recursive: true });
167
+ if (!fs.existsSync(dbDir)) fs.mkdirSync(dbDir, { recursive: true });
168
+ if (!fs.existsSync(profilesDir)) fs.mkdirSync(profilesDir, { recursive: true });
169
+ if (!fs.existsSync(logsDir)) fs.mkdirSync(logsDir, { recursive: true });
170
+ } catch (err) {
171
+ console.error(`[Paths] Error ensuring data directories at ${root}:`, err?.message || String(err));
172
+ }
173
+ }
129
174
  var init_paths = __esm({
130
175
  "src/core/paths.ts"() {
131
176
  "use strict";
@@ -3168,6 +3213,7 @@ __export(playwright_exports, {
3168
3213
  closeIdlePlaywrightAccounts: () => closeIdlePlaywrightAccounts,
3169
3214
  closePlaywrightForAccount: () => closePlaywrightForAccount,
3170
3215
  evictIdlePlaywrightContextsToLimit: () => evictIdlePlaywrightContextsToLimit,
3216
+ getAccountMutex: () => getAccountMutex,
3171
3217
  getActivePlaywrightAccountIds: () => getActivePlaywrightAccountIds,
3172
3218
  getBasicHeaders: () => getBasicHeaders,
3173
3219
  getCookies: () => getCookies,
@@ -3317,9 +3363,10 @@ function getAccountMutex(accountId) {
3317
3363
  async function recoverStuckAccountMutex(accountId, mutex, key) {
3318
3364
  if (accountMutexes.get(accountId) !== mutex) return;
3319
3365
  const lockState = mutex.state();
3320
- 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)) {
3321
3368
  logger.warn(
3322
- `[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)}`
3323
3370
  );
3324
3371
  return;
3325
3372
  }
@@ -4121,7 +4168,8 @@ async function getBasicHeaders(accountId) {
4121
4168
  }
4122
4169
  const release = await acquireAccountMutex(
4123
4170
  accountId,
4124
- `headers:${accountId.substring(0, 12)}`
4171
+ `headers:${accountId.substring(0, 12)}`,
4172
+ Math.max(PLAYWRIGHT_MUTEX_WAIT_MS, config.timeouts.headers + 3e4)
4125
4173
  );
4126
4174
  try {
4127
4175
  touchAccountActivity(accountId);
@@ -4214,8 +4262,10 @@ async function getBasicHeaders(accountId) {
4214
4262
  return { cookie: cookie2, userAgent, bxV: bxV2, bxUa: bxUa2, bxUmidtoken: bxUmidtoken2, ...clientHints };
4215
4263
  }
4216
4264
  }
4265
+ let refreshedInThisCall = false;
4217
4266
  if (headersAge > HEADER_CACHE_TTL && !cache.refreshInProgress) {
4218
4267
  try {
4268
+ refreshedInThisCall = true;
4219
4269
  await refreshHeadersInternal(accountId);
4220
4270
  } catch (error) {
4221
4271
  if (!hadUsableHeaders) throw error;
@@ -4284,7 +4334,7 @@ async function getBasicHeaders(accountId) {
4284
4334
  } catch {
4285
4335
  }
4286
4336
  }
4287
- if (!hasRequiredQwenHeaders(cache.headers)) {
4337
+ if (!hasRequiredQwenHeaders(cache.headers) && !refreshedInThisCall) {
4288
4338
  console.log(
4289
4339
  `\u{1F504} [Playwright] Missing required anti-bot headers for ${accountId}, triggering header interception...`
4290
4340
  );
@@ -5727,7 +5777,24 @@ async function refreshAccountToken(accountId) {
5727
5777
  return { success: false, error: "Could not refresh or re-authenticate token" };
5728
5778
  }
5729
5779
  async function captureQwenHeaders(accountId, pageOverride, timeoutMs = config.timeouts.headers, triggerGraceMs = HEADER_CAPTURE_TRIGGER_GRACE_MS) {
5730
- const page = pageOverride ?? accountPages.get(accountId);
5780
+ let page = pageOverride ?? accountPages.get(accountId);
5781
+ if (!page || page.isClosed()) {
5782
+ const { isRunningUnderNodeTest: isRunningUnderNodeTest4 } = await Promise.resolve().then(() => (init_paths(), paths_exports));
5783
+ if (!isRunningUnderNodeTest4()) {
5784
+ const { getAccountCredentials: getAccountCredentials2 } = await Promise.resolve().then(() => (init_accounts(), accounts_exports));
5785
+ const creds = getAccountCredentials2(accountId);
5786
+ if (creds) {
5787
+ await initPlaywrightForAccount(
5788
+ creds,
5789
+ config.playwright.headless,
5790
+ config.playwright.browser,
5791
+ { skipHeaderCapture: true }
5792
+ ).catch(() => {
5793
+ });
5794
+ page = accountPages.get(accountId);
5795
+ }
5796
+ }
5797
+ }
5731
5798
  if (!page || page.isClosed()) {
5732
5799
  throw new Error(`Playwright page unavailable for header capture: ${accountId}`);
5733
5800
  }
@@ -6370,16 +6437,38 @@ async function refreshHeaders(accountId, timeoutMs = config.timeouts.headers, fo
6370
6437
  release();
6371
6438
  }
6372
6439
  }
6373
- async function withAccountPage(accountId, fn, timeoutMs = ACCOUNT_PAGE_OPERATION_TIMEOUT_MS, mutexTimeoutMs = PLAYWRIGHT_MUTEX_WAIT_MS, recoverOnTimeout = true) {
6440
+ async function ensureAccountPageAvailable(accountId) {
6374
6441
  const inFlightInit = inFlightAccountInits.get(accountId);
6375
6442
  if (inFlightInit) {
6376
6443
  await inFlightInit.catch(() => {
6377
6444
  });
6378
6445
  }
6379
- const page = accountPages.get(accountId);
6446
+ let page = accountPages.get(accountId);
6447
+ if (page && !page.isClosed()) {
6448
+ return page;
6449
+ }
6450
+ const { isRunningUnderNodeTest: isRunningUnderNodeTest4 } = await Promise.resolve().then(() => (init_paths(), paths_exports));
6451
+ if (isRunningUnderNodeTest4()) {
6452
+ throw new Error(`Playwright page unavailable for account: ${accountId}`);
6453
+ }
6454
+ const { getAccountCredentials: getAccountCredentials2 } = await Promise.resolve().then(() => (init_accounts(), accounts_exports));
6455
+ const creds = getAccountCredentials2(accountId);
6456
+ if (!creds) {
6457
+ throw new Error(`Playwright page unavailable for account: ${accountId}`);
6458
+ }
6459
+ await initPlaywrightForAccount(
6460
+ creds,
6461
+ config.playwright.headless,
6462
+ config.playwright.browser
6463
+ );
6464
+ page = accountPages.get(accountId);
6380
6465
  if (!page || page.isClosed()) {
6381
6466
  throw new Error(`Playwright page unavailable for account: ${accountId}`);
6382
6467
  }
6468
+ return page;
6469
+ }
6470
+ async function withAccountPage(accountId, fn, timeoutMs = ACCOUNT_PAGE_OPERATION_TIMEOUT_MS, mutexTimeoutMs = PLAYWRIGHT_MUTEX_WAIT_MS, recoverOnTimeout = true) {
6471
+ const page = await ensureAccountPageAvailable(accountId);
6383
6472
  const release = await acquireAccountMutex(
6384
6473
  accountId,
6385
6474
  `page:${accountId.substring(0, 12)}`,
@@ -6680,8 +6769,10 @@ async function evictIdlePlaywrightContextsToLimit() {
6680
6769
  if (accountPages.size <= max) return 0;
6681
6770
  const candidates = priorityOrderForEviction(
6682
6771
  Array.from(accountPages.keys()).filter((accountId) => {
6772
+ if (isAccountServingStream(accountId)) return false;
6773
+ if (isAccountRecentlyActive(accountId, 3e4)) return false;
6683
6774
  const mutex = accountMutexes.get(accountId);
6684
- return mutex?.isIdle() && !isAccountServingStream(accountId);
6775
+ return mutex?.isIdle();
6685
6776
  })
6686
6777
  ).map((accountId) => ({
6687
6778
  accountId,
@@ -8771,7 +8862,7 @@ async function withQwenBrowserPage(accountId, fn, targetPath, operationTimeoutMs
8771
8862
  return fn(page);
8772
8863
  },
8773
8864
  operationTimeoutMs,
8774
- Math.min(config.timeouts.page, 3e4),
8865
+ Math.max(6e4, operationTimeoutMs),
8775
8866
  recoverOnTimeout
8776
8867
  );
8777
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
  );
@@ -6882,7 +6887,24 @@ async function refreshAccountToken(accountId) {
6882
6887
  return { success: false, error: "Could not refresh or re-authenticate token" };
6883
6888
  }
6884
6889
  async function captureQwenHeaders(accountId, pageOverride, timeoutMs = config.timeouts.headers, triggerGraceMs = HEADER_CAPTURE_TRIGGER_GRACE_MS) {
6885
- const page = pageOverride ?? accountPages.get(accountId);
6890
+ let page = pageOverride ?? accountPages.get(accountId);
6891
+ if (!page || page.isClosed()) {
6892
+ const { isRunningUnderNodeTest: isRunningUnderNodeTest4 } = await Promise.resolve().then(() => (init_paths(), paths_exports));
6893
+ if (!isRunningUnderNodeTest4()) {
6894
+ const { getAccountCredentials: getAccountCredentials2 } = await Promise.resolve().then(() => (init_accounts(), accounts_exports));
6895
+ const creds = getAccountCredentials2(accountId);
6896
+ if (creds) {
6897
+ await initPlaywrightForAccount(
6898
+ creds,
6899
+ config.playwright.headless,
6900
+ config.playwright.browser,
6901
+ { skipHeaderCapture: true }
6902
+ ).catch(() => {
6903
+ });
6904
+ page = accountPages.get(accountId);
6905
+ }
6906
+ }
6907
+ }
6886
6908
  if (!page || page.isClosed()) {
6887
6909
  throw new Error(`Playwright page unavailable for header capture: ${accountId}`);
6888
6910
  }
@@ -7525,16 +7547,38 @@ async function refreshHeaders(accountId, timeoutMs = config.timeouts.headers, fo
7525
7547
  release();
7526
7548
  }
7527
7549
  }
7528
- async function withAccountPage(accountId, fn, timeoutMs = ACCOUNT_PAGE_OPERATION_TIMEOUT_MS, mutexTimeoutMs = PLAYWRIGHT_MUTEX_WAIT_MS, recoverOnTimeout = true) {
7550
+ async function ensureAccountPageAvailable(accountId) {
7529
7551
  const inFlightInit = inFlightAccountInits.get(accountId);
7530
7552
  if (inFlightInit) {
7531
7553
  await inFlightInit.catch(() => {
7532
7554
  });
7533
7555
  }
7534
- const page = accountPages.get(accountId);
7556
+ let page = accountPages.get(accountId);
7557
+ if (page && !page.isClosed()) {
7558
+ return page;
7559
+ }
7560
+ const { isRunningUnderNodeTest: isRunningUnderNodeTest4 } = await Promise.resolve().then(() => (init_paths(), paths_exports));
7561
+ if (isRunningUnderNodeTest4()) {
7562
+ throw new Error(`Playwright page unavailable for account: ${accountId}`);
7563
+ }
7564
+ const { getAccountCredentials: getAccountCredentials2 } = await Promise.resolve().then(() => (init_accounts(), accounts_exports));
7565
+ const creds = getAccountCredentials2(accountId);
7566
+ if (!creds) {
7567
+ throw new Error(`Playwright page unavailable for account: ${accountId}`);
7568
+ }
7569
+ await initPlaywrightForAccount(
7570
+ creds,
7571
+ config.playwright.headless,
7572
+ config.playwright.browser
7573
+ );
7574
+ page = accountPages.get(accountId);
7535
7575
  if (!page || page.isClosed()) {
7536
7576
  throw new Error(`Playwright page unavailable for account: ${accountId}`);
7537
7577
  }
7578
+ return page;
7579
+ }
7580
+ async function withAccountPage(accountId, fn, timeoutMs = ACCOUNT_PAGE_OPERATION_TIMEOUT_MS, mutexTimeoutMs = PLAYWRIGHT_MUTEX_WAIT_MS, recoverOnTimeout = true) {
7581
+ const page = await ensureAccountPageAvailable(accountId);
7538
7582
  const release = await acquireAccountMutex(
7539
7583
  accountId,
7540
7584
  `page:${accountId.substring(0, 12)}`,
@@ -7835,8 +7879,10 @@ async function evictIdlePlaywrightContextsToLimit() {
7835
7879
  if (accountPages.size <= max) return 0;
7836
7880
  const candidates = priorityOrderForEviction(
7837
7881
  Array.from(accountPages.keys()).filter((accountId) => {
7882
+ if (isAccountServingStream(accountId)) return false;
7883
+ if (isAccountRecentlyActive(accountId, 3e4)) return false;
7838
7884
  const mutex = accountMutexes.get(accountId);
7839
- return mutex?.isIdle() && !isAccountServingStream(accountId);
7885
+ return mutex?.isIdle();
7840
7886
  })
7841
7887
  ).map((accountId) => ({
7842
7888
  accountId,
@@ -9784,7 +9830,7 @@ function isModelNotFoundError(err) {
9784
9830
  function isNetworkLikeError(err) {
9785
9831
  if (err instanceof QwenNetworkError) return true;
9786
9832
  const message = errMessage(err).toLowerCase();
9787
- 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");
9788
9834
  }
9789
9835
  function isCorruptedChatHistoryError(err) {
9790
9836
  const message = errMessage(err).toLowerCase();
@@ -9827,6 +9873,15 @@ function classifyRetryAction(err, options) {
9827
9873
  if (isTerminalLocalError(err)) {
9828
9874
  return makeRetryAction("terminal_local", { retryable: false });
9829
9875
  }
9876
+ if (err instanceof PersonalizationSyncError) {
9877
+ return makeRetryAction("personalization_sync_failed", {
9878
+ switchAccount: true,
9879
+ forceNewChat: true,
9880
+ retryAfterMs: baseDelayMs,
9881
+ accountCooldownMs: 0,
9882
+ accountCooldownReason: "PersonalizationFailed"
9883
+ });
9884
+ }
9830
9885
  const message = errMessage(err).toLowerCase();
9831
9886
  const code = errCode(err).toLowerCase();
9832
9887
  if (isAccountInitializationError(err)) {
@@ -9843,15 +9898,6 @@ function classifyRetryAction(err, options) {
9843
9898
  retryAfterMs: Math.min(baseDelayMs, 1e3)
9844
9899
  });
9845
9900
  }
9846
- if (err instanceof PersonalizationSyncError) {
9847
- return makeRetryAction("personalization_sync_failed", {
9848
- switchAccount: true,
9849
- forceNewChat: true,
9850
- retryAfterMs: baseDelayMs,
9851
- accountCooldownMs: 0,
9852
- accountCooldownReason: "PersonalizationFailed"
9853
- });
9854
- }
9855
9901
  if (code === "createchatinvalidresponse" || code === "createchatfailed" || code === "unauthorized" || message.includes("401 n\xE3o autorizado") || message.includes("n\xE3o tem permiss\xE3o para acessar") || message.includes("401 unauthorized") || message.includes('"code":"unauthorized"') || message.includes('"code": "unauthorized"') || message.includes("token has expired")) {
9856
9902
  return makeRetryAction("account_initialization_failed", {
9857
9903
  switchAccount: true,
@@ -12006,7 +12052,7 @@ async function withQwenBrowserPage(accountId, fn, targetPath, operationTimeoutMs
12006
12052
  return fn(page);
12007
12053
  },
12008
12054
  operationTimeoutMs,
12009
- Math.min(config.timeouts.page, 3e4),
12055
+ Math.max(6e4, operationTimeoutMs),
12010
12056
  recoverOnTimeout
12011
12057
  );
12012
12058
  }
@@ -26782,7 +26828,7 @@ function getAppVersion() {
26782
26828
  }
26783
26829
  } catch {
26784
26830
  }
26785
- cachedVersion = "v1.6.2";
26831
+ cachedVersion = "v1.6.4";
26786
26832
  return cachedVersion;
26787
26833
  }
26788
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
  );
@@ -5749,7 +5754,24 @@ async function refreshAccountToken(accountId) {
5749
5754
  return { success: false, error: "Could not refresh or re-authenticate token" };
5750
5755
  }
5751
5756
  async function captureQwenHeaders(accountId, pageOverride, timeoutMs = config.timeouts.headers, triggerGraceMs = HEADER_CAPTURE_TRIGGER_GRACE_MS) {
5752
- const page = pageOverride ?? accountPages.get(accountId);
5757
+ let page = pageOverride ?? accountPages.get(accountId);
5758
+ if (!page || page.isClosed()) {
5759
+ const { isRunningUnderNodeTest: isRunningUnderNodeTest3 } = await Promise.resolve().then(() => (init_paths(), paths_exports));
5760
+ if (!isRunningUnderNodeTest3()) {
5761
+ const { getAccountCredentials: getAccountCredentials2 } = await Promise.resolve().then(() => (init_accounts(), accounts_exports));
5762
+ const creds = getAccountCredentials2(accountId);
5763
+ if (creds) {
5764
+ await initPlaywrightForAccount(
5765
+ creds,
5766
+ config.playwright.headless,
5767
+ config.playwright.browser,
5768
+ { skipHeaderCapture: true }
5769
+ ).catch(() => {
5770
+ });
5771
+ page = accountPages.get(accountId);
5772
+ }
5773
+ }
5774
+ }
5753
5775
  if (!page || page.isClosed()) {
5754
5776
  throw new Error(`Playwright page unavailable for header capture: ${accountId}`);
5755
5777
  }
@@ -6392,16 +6414,38 @@ async function refreshHeaders(accountId, timeoutMs = config.timeouts.headers, fo
6392
6414
  release();
6393
6415
  }
6394
6416
  }
6395
- async function withAccountPage(accountId, fn, timeoutMs = ACCOUNT_PAGE_OPERATION_TIMEOUT_MS, mutexTimeoutMs = PLAYWRIGHT_MUTEX_WAIT_MS, recoverOnTimeout = true) {
6417
+ async function ensureAccountPageAvailable(accountId) {
6396
6418
  const inFlightInit = inFlightAccountInits.get(accountId);
6397
6419
  if (inFlightInit) {
6398
6420
  await inFlightInit.catch(() => {
6399
6421
  });
6400
6422
  }
6401
- const page = accountPages.get(accountId);
6423
+ let page = accountPages.get(accountId);
6424
+ if (page && !page.isClosed()) {
6425
+ return page;
6426
+ }
6427
+ const { isRunningUnderNodeTest: isRunningUnderNodeTest3 } = await Promise.resolve().then(() => (init_paths(), paths_exports));
6428
+ if (isRunningUnderNodeTest3()) {
6429
+ throw new Error(`Playwright page unavailable for account: ${accountId}`);
6430
+ }
6431
+ const { getAccountCredentials: getAccountCredentials2 } = await Promise.resolve().then(() => (init_accounts(), accounts_exports));
6432
+ const creds = getAccountCredentials2(accountId);
6433
+ if (!creds) {
6434
+ throw new Error(`Playwright page unavailable for account: ${accountId}`);
6435
+ }
6436
+ await initPlaywrightForAccount(
6437
+ creds,
6438
+ config.playwright.headless,
6439
+ config.playwright.browser
6440
+ );
6441
+ page = accountPages.get(accountId);
6402
6442
  if (!page || page.isClosed()) {
6403
6443
  throw new Error(`Playwright page unavailable for account: ${accountId}`);
6404
6444
  }
6445
+ return page;
6446
+ }
6447
+ async function withAccountPage(accountId, fn, timeoutMs = ACCOUNT_PAGE_OPERATION_TIMEOUT_MS, mutexTimeoutMs = PLAYWRIGHT_MUTEX_WAIT_MS, recoverOnTimeout = true) {
6448
+ const page = await ensureAccountPageAvailable(accountId);
6405
6449
  const release = await acquireAccountMutex(
6406
6450
  accountId,
6407
6451
  `page:${accountId.substring(0, 12)}`,
@@ -6702,8 +6746,10 @@ async function evictIdlePlaywrightContextsToLimit() {
6702
6746
  if (accountPages.size <= max) return 0;
6703
6747
  const candidates = priorityOrderForEviction(
6704
6748
  Array.from(accountPages.keys()).filter((accountId) => {
6749
+ if (isAccountServingStream(accountId)) return false;
6750
+ if (isAccountRecentlyActive(accountId, 3e4)) return false;
6705
6751
  const mutex = accountMutexes.get(accountId);
6706
- return mutex?.isIdle() && !isAccountServingStream(accountId);
6752
+ return mutex?.isIdle();
6707
6753
  })
6708
6754
  ).map((accountId) => ({
6709
6755
  accountId,
@@ -14,6 +14,25 @@ var __export = (target, all) => {
14
14
  };
15
15
 
16
16
  // src/core/paths.ts
17
+ var paths_exports = {};
18
+ __export(paths_exports, {
19
+ ensureDataDirs: () => ensureDataDirs,
20
+ getAccountPriorityPath: () => getAccountPriorityPath,
21
+ getAccountProfilePath: () => getAccountProfilePath,
22
+ getDataDir: () => getDataDir,
23
+ getDbDir: () => getDbDir,
24
+ getDbPath: () => getDbPath,
25
+ getEncryptionKeyPath: () => getEncryptionKeyPath,
26
+ getEnvFilePath: () => getEnvFilePath,
27
+ getLogsDir: () => getLogsDir,
28
+ getOsGlobalDataDir: () => getOsGlobalDataDir,
29
+ getProfilesDir: () => getProfilesDir,
30
+ getServerLogFilePath: () => getServerLogFilePath,
31
+ getSyncStatePath: () => getSyncStatePath,
32
+ getTuiSettingsPath: () => getTuiSettingsPath,
33
+ isRunningUnderNodeTest: () => isRunningUnderNodeTest,
34
+ resolveDataDir: () => resolveDataDir
35
+ });
17
36
  import fs from "node:fs";
18
37
  import path from "node:path";
19
38
  import os from "node:os";
@@ -78,6 +97,12 @@ function getAccountProfilePath(accountId, customDataDir) {
78
97
  function getAccountPriorityPath(customDataDir) {
79
98
  return path.join(customDataDir || getDataDir(), "account-priority.json");
80
99
  }
100
+ function getSyncStatePath(customDataDir) {
101
+ return path.join(customDataDir || getDataDir(), "sync-state.json");
102
+ }
103
+ function getTuiSettingsPath(customDataDir) {
104
+ return path.join(customDataDir || getDataDir(), "tui-settings.json");
105
+ }
81
106
  function getEnvFilePath(customDataDir) {
82
107
  if (isRunningUnderNodeTest()) {
83
108
  return path.join(customDataDir || getDataDir(), ".env");
@@ -88,6 +113,26 @@ function getEnvFilePath(customDataDir) {
88
113
  }
89
114
  return path.join(customDataDir || getDataDir(), ".env");
90
115
  }
116
+ function getLogsDir(customDataDir) {
117
+ return path.join(customDataDir || getDataDir(), "logs");
118
+ }
119
+ function getServerLogFilePath(customDataDir) {
120
+ return path.join(getLogsDir(customDataDir), "server.log");
121
+ }
122
+ function ensureDataDirs(targetDataDir) {
123
+ const root = targetDataDir || getDataDir();
124
+ const dbDir = getDbDir(root);
125
+ const profilesDir = getProfilesDir(root);
126
+ const logsDir = getLogsDir(root);
127
+ try {
128
+ if (!fs.existsSync(root)) fs.mkdirSync(root, { recursive: true });
129
+ if (!fs.existsSync(dbDir)) fs.mkdirSync(dbDir, { recursive: true });
130
+ if (!fs.existsSync(profilesDir)) fs.mkdirSync(profilesDir, { recursive: true });
131
+ if (!fs.existsSync(logsDir)) fs.mkdirSync(logsDir, { recursive: true });
132
+ } catch (err) {
133
+ console.error(`[Paths] Error ensuring data directories at ${root}:`, err?.message || String(err));
134
+ }
135
+ }
91
136
  var init_paths = __esm({
92
137
  "src/core/paths.ts"() {
93
138
  "use strict";
@@ -3059,9 +3104,10 @@ function getAccountMutex(accountId) {
3059
3104
  async function recoverStuckAccountMutex(accountId, mutex, key) {
3060
3105
  if (accountMutexes.get(accountId) !== mutex) return;
3061
3106
  const lockState = mutex.state();
3062
- 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)) {
3063
3109
  logger.warn(
3064
- `[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)}`
3065
3111
  );
3066
3112
  return;
3067
3113
  }
@@ -4733,7 +4779,24 @@ async function refreshAccountToken(accountId) {
4733
4779
  return { success: false, error: "Could not refresh or re-authenticate token" };
4734
4780
  }
4735
4781
  async function captureQwenHeaders(accountId, pageOverride, timeoutMs = config.timeouts.headers, triggerGraceMs = HEADER_CAPTURE_TRIGGER_GRACE_MS) {
4736
- const page = pageOverride ?? accountPages.get(accountId);
4782
+ let page = pageOverride ?? accountPages.get(accountId);
4783
+ if (!page || page.isClosed()) {
4784
+ const { isRunningUnderNodeTest: isRunningUnderNodeTest3 } = await Promise.resolve().then(() => (init_paths(), paths_exports));
4785
+ if (!isRunningUnderNodeTest3()) {
4786
+ const { getAccountCredentials: getAccountCredentials2 } = await Promise.resolve().then(() => (init_accounts(), accounts_exports));
4787
+ const creds = getAccountCredentials2(accountId);
4788
+ if (creds) {
4789
+ await initPlaywrightForAccount(
4790
+ creds,
4791
+ config.playwright.headless,
4792
+ config.playwright.browser,
4793
+ { skipHeaderCapture: true }
4794
+ ).catch(() => {
4795
+ });
4796
+ page = accountPages.get(accountId);
4797
+ }
4798
+ }
4799
+ }
4737
4800
  if (!page || page.isClosed()) {
4738
4801
  throw new Error(`Playwright page unavailable for header capture: ${accountId}`);
4739
4802
  }
@@ -5434,8 +5497,10 @@ async function evictIdlePlaywrightContextsToLimit() {
5434
5497
  if (accountPages.size <= max) return 0;
5435
5498
  const candidates = priorityOrderForEviction(
5436
5499
  Array.from(accountPages.keys()).filter((accountId) => {
5500
+ if (isAccountServingStream(accountId)) return false;
5501
+ if (isAccountRecentlyActive(accountId, 3e4)) return false;
5437
5502
  const mutex = accountMutexes.get(accountId);
5438
- return mutex?.isIdle() && !isAccountServingStream(accountId);
5503
+ return mutex?.isIdle();
5439
5504
  })
5440
5505
  ).map((accountId) => ({
5441
5506
  accountId,
@@ -5548,6 +5613,10 @@ async function closePlaywrightForAccount(accountId) {
5548
5613
  release();
5549
5614
  }
5550
5615
  }
5616
+ function isAccountRecentlyActive(accountId, maxIdleMs = 3e5) {
5617
+ const last = lastAccountActivity.get(accountId) ?? 0;
5618
+ return last > 0 && Date.now() - last < maxIdleMs;
5619
+ }
5551
5620
  var requireLocal, contextInitHooks, ACCOUNT_MUTEX_MAX_HOLD_MS, accountMutexes, accountContexts, accountPages, inFlightAccountInits, headerCaches, HEADER_CACHE_TTL, COOKIE_CACHE_TTL, cookieCaches, lastAccountActivity, lastKeepAliveNavigation, lastProactiveTokenRefresh, profileResetQueue, profileResetChain, closingAllPlaywright, sleep2, HEADER_CAPTURE_SETTLE_MS, PLAYWRIGHT_MUTEX_WAIT_MS, ACCOUNT_PAGE_OPERATION_TIMEOUT_MS, SESSION_PROBE_NAVIGATION_TIMEOUT_MS, HEADER_CAPTURE_TRIGGER_GRACE_MS, FIRST_TRIGGER_GRACE_MS, HEADER_CAPTURE_TRIGGER_ATTEMPTS, CHAT_INPUT_APPEAR_TIMEOUT_MS, CHAT_INPUT_ACTION_TIMEOUT_MS, PROFILE_RESET_TIMEOUT_MS;
5552
5621
  var init_playwright = __esm({
5553
5622
  "src/services/playwright.ts"() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qwenproxy-cli",
3
- "version": "1.6.2",
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.2";
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
 
@@ -499,6 +501,21 @@ export function classifyRetryAction(
499
501
  return makeRetryAction("terminal_local", { retryable: false });
500
502
  }
501
503
 
504
+ // Agent instructions ride ONLY the account-level personalization. An
505
+ // unconfirmed sync means this account cannot serve the request as-is —
506
+ // rotate to another account (each attempt re-syncs on its own account).
507
+ // Do NOT park the account with a 300s cooldown: rotation across accounts
508
+ // is already guarded by triedAccounts, preventing pool collapse.
509
+ if (err instanceof PersonalizationSyncError) {
510
+ return makeRetryAction("personalization_sync_failed", {
511
+ switchAccount: true,
512
+ forceNewChat: true,
513
+ retryAfterMs: baseDelayMs,
514
+ accountCooldownMs: 0,
515
+ accountCooldownReason: "PersonalizationFailed",
516
+ });
517
+ }
518
+
502
519
  const message = errMessage(err).toLowerCase();
503
520
  const code = errCode(err).toLowerCase();
504
521
  if (isAccountInitializationError(err)) {
@@ -521,21 +538,6 @@ export function classifyRetryAction(
521
538
  });
522
539
  }
523
540
 
524
- // Agent instructions ride ONLY the account-level personalization. An
525
- // unconfirmed sync means this account cannot serve the request as-is —
526
- // rotate to another account (each attempt re-syncs on its own account).
527
- // Do NOT park the account with a 300s cooldown: rotation across accounts
528
- // is already guarded by triedAccounts, preventing pool collapse.
529
- if (err instanceof PersonalizationSyncError) {
530
- return makeRetryAction("personalization_sync_failed", {
531
- switchAccount: true,
532
- forceNewChat: true,
533
- retryAfterMs: baseDelayMs,
534
- accountCooldownMs: 0,
535
- accountCooldownReason: "PersonalizationFailed",
536
- });
537
- }
538
-
539
541
  // Upstream 401 / Unauthorized on chat creation or API requests:
540
542
  // Access token expired (15m window). Rotate without locking into a 300s cooldown
541
543
  // so the account can immediately be renewed via refresh_token.
@@ -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
  );
@@ -3242,7 +3250,24 @@ export async function captureQwenHeaders(
3242
3250
  timeoutMs = config.timeouts.headers,
3243
3251
  triggerGraceMs = HEADER_CAPTURE_TRIGGER_GRACE_MS,
3244
3252
  ): Promise<void> {
3245
- const page = pageOverride ?? accountPages.get(accountId);
3253
+ let page = pageOverride ?? accountPages.get(accountId);
3254
+ if (!page || page.isClosed()) {
3255
+ const { isRunningUnderNodeTest } = await import("../core/paths.ts");
3256
+ if (!isRunningUnderNodeTest()) {
3257
+ const { getAccountCredentials } = await import("../core/accounts.ts");
3258
+ const creds = getAccountCredentials(accountId);
3259
+ if (creds) {
3260
+ await initPlaywrightForAccount(
3261
+ creds,
3262
+ config.playwright.headless,
3263
+ config.playwright.browser,
3264
+ { skipHeaderCapture: true },
3265
+ ).catch(() => {});
3266
+ page = accountPages.get(accountId);
3267
+ }
3268
+ }
3269
+ }
3270
+
3246
3271
  if (!page || page.isClosed()) {
3247
3272
  throw new Error(`Playwright page unavailable for header capture: ${accountId}`);
3248
3273
  }
@@ -4100,22 +4125,51 @@ export async function refreshHeaders(
4100
4125
  * Run work against the account Playwright page under the per-account mutex.
4101
4126
  * Used by captcha recovery so it cannot race header capture / login.
4102
4127
  */
4103
- export async function withAccountPage<T>(
4104
- accountId: string,
4105
- fn: (page: Page) => Promise<T>,
4106
- timeoutMs = ACCOUNT_PAGE_OPERATION_TIMEOUT_MS,
4107
- mutexTimeoutMs = PLAYWRIGHT_MUTEX_WAIT_MS,
4108
- recoverOnTimeout = true,
4109
- ): Promise<T> {
4128
+ async function ensureAccountPageAvailable(accountId: string): Promise<Page> {
4110
4129
  const inFlightInit = inFlightAccountInits.get(accountId);
4111
4130
  if (inFlightInit) {
4112
4131
  await inFlightInit.catch(() => {});
4113
4132
  }
4114
4133
 
4115
- const page = accountPages.get(accountId);
4134
+ let page = accountPages.get(accountId);
4135
+ if (page && !page.isClosed()) {
4136
+ return page;
4137
+ }
4138
+
4139
+ // Under Node.js test suites, preserve strict mock failure contract
4140
+ const { isRunningUnderNodeTest } = await import("../core/paths.ts");
4141
+ if (isRunningUnderNodeTest()) {
4142
+ throw new Error(`Playwright page unavailable for account: ${accountId}`);
4143
+ }
4144
+
4145
+ // Lazy boot / recovery: if the account was evicted or in standby, initialize it transparently on demand
4146
+ const { getAccountCredentials } = await import("../core/accounts.ts");
4147
+ const creds = getAccountCredentials(accountId);
4148
+ if (!creds) {
4149
+ throw new Error(`Playwright page unavailable for account: ${accountId}`);
4150
+ }
4151
+
4152
+ await initPlaywrightForAccount(
4153
+ creds,
4154
+ config.playwright.headless,
4155
+ config.playwright.browser,
4156
+ );
4157
+
4158
+ page = accountPages.get(accountId);
4116
4159
  if (!page || page.isClosed()) {
4117
4160
  throw new Error(`Playwright page unavailable for account: ${accountId}`);
4118
4161
  }
4162
+ return page;
4163
+ }
4164
+
4165
+ export async function withAccountPage<T>(
4166
+ accountId: string,
4167
+ fn: (page: Page) => Promise<T>,
4168
+ timeoutMs = ACCOUNT_PAGE_OPERATION_TIMEOUT_MS,
4169
+ mutexTimeoutMs = PLAYWRIGHT_MUTEX_WAIT_MS,
4170
+ recoverOnTimeout = true,
4171
+ ): Promise<T> {
4172
+ const page = await ensureAccountPageAvailable(accountId);
4119
4173
  const release = await acquireAccountMutex(
4120
4174
  accountId,
4121
4175
  `page:${accountId.substring(0, 12)}`,
@@ -4542,8 +4596,10 @@ export async function evictIdlePlaywrightContextsToLimit(): Promise<number> {
4542
4596
 
4543
4597
  const candidates = priorityOrderForEviction(
4544
4598
  Array.from(accountPages.keys()).filter((accountId) => {
4599
+ if (isAccountServingStream(accountId)) return false;
4600
+ if (isAccountRecentlyActive(accountId, 30_000)) return false;
4545
4601
  const mutex = accountMutexes.get(accountId);
4546
- return mutex?.isIdle() && !isAccountServingStream(accountId);
4602
+ return mutex?.isIdle();
4547
4603
  }),
4548
4604
  ).map((accountId) => ({
4549
4605
  accountId,
@@ -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
  }