qwenproxy-cli 1.5.1 → 1.5.3

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.
@@ -965,6 +965,7 @@ var init_config = __esm({
965
965
  // own context while serving. Accounts in cooldown (rate-limited) sit idle,
966
966
  // drop out of the warm set and get evicted.
967
967
  PLAYWRIGHT_MAX_ACTIVE_CONTEXTS: z.string().default("2"),
968
+ PLAYWRIGHT_STARTUP_WARM_ACCOUNTS: z.string().default("4"),
968
969
  PLAYWRIGHT_PREPARE_ALL_ON_STARTUP: z.string().default("false"),
969
970
  CAPTCHA_SOLVER_ENABLED: z.string().default("true"),
970
971
  CAPTCHA_SOLVER_MAX_ATTEMPTS: z.string().default("3"),
@@ -1113,6 +1114,7 @@ var init_config = __esm({
1113
1114
  jsHeapMb: Math.max(64, parseInt(env.PLAYWRIGHT_JS_HEAP_MB)),
1114
1115
  lowMemoryFlags: env.PLAYWRIGHT_LOW_MEMORY_FLAGS !== "false",
1115
1116
  maxActiveContexts: Math.max(0, parseInt(env.PLAYWRIGHT_MAX_ACTIVE_CONTEXTS)),
1117
+ startupWarmAccounts: Math.max(1, parseInt(env.PLAYWRIGHT_STARTUP_WARM_ACCOUNTS)),
1116
1118
  prepareAllOnStartup: env.PLAYWRIGHT_PREPARE_ALL_ON_STARTUP !== "false"
1117
1119
  },
1118
1120
  captcha: {
@@ -1020,6 +1020,7 @@ var init_config = __esm({
1020
1020
  // own context while serving. Accounts in cooldown (rate-limited) sit idle,
1021
1021
  // drop out of the warm set and get evicted.
1022
1022
  PLAYWRIGHT_MAX_ACTIVE_CONTEXTS: z.string().default("2"),
1023
+ PLAYWRIGHT_STARTUP_WARM_ACCOUNTS: z.string().default("4"),
1023
1024
  PLAYWRIGHT_PREPARE_ALL_ON_STARTUP: z.string().default("false"),
1024
1025
  CAPTCHA_SOLVER_ENABLED: z.string().default("true"),
1025
1026
  CAPTCHA_SOLVER_MAX_ATTEMPTS: z.string().default("3"),
@@ -1168,6 +1169,7 @@ var init_config = __esm({
1168
1169
  jsHeapMb: Math.max(64, parseInt(env.PLAYWRIGHT_JS_HEAP_MB)),
1169
1170
  lowMemoryFlags: env.PLAYWRIGHT_LOW_MEMORY_FLAGS !== "false",
1170
1171
  maxActiveContexts: Math.max(0, parseInt(env.PLAYWRIGHT_MAX_ACTIVE_CONTEXTS)),
1172
+ startupWarmAccounts: Math.max(1, parseInt(env.PLAYWRIGHT_STARTUP_WARM_ACCOUNTS)),
1171
1173
  prepareAllOnStartup: env.PLAYWRIGHT_PREPARE_ALL_ON_STARTUP !== "false"
1172
1174
  },
1173
1175
  captcha: {
@@ -1901,6 +1903,14 @@ var init_mutex = __esm({
1901
1903
  });
1902
1904
 
1903
1905
  // src/core/account-priority.ts
1906
+ var account_priority_exports = {};
1907
+ __export(account_priority_exports, {
1908
+ ensureAccountInPriority: () => ensureAccountInPriority,
1909
+ getAccountsByPriority: () => getAccountsByPriority,
1910
+ invalidatePriorityCache: () => invalidatePriorityCache,
1911
+ markAccountFailed: () => markAccountFailed,
1912
+ markAccountSuccessful: () => markAccountSuccessful
1913
+ });
1904
1914
  import {
1905
1915
  existsSync,
1906
1916
  mkdirSync,
@@ -1910,6 +1920,9 @@ import {
1910
1920
  function isPersistableAccount(accountId) {
1911
1921
  return accountId !== MOCK_ACCOUNT_ID;
1912
1922
  }
1923
+ function invalidatePriorityCache() {
1924
+ priorityCache = null;
1925
+ }
1913
1926
  function loadPriority() {
1914
1927
  if (priorityCache) return priorityCache;
1915
1928
  try {
@@ -1930,6 +1943,41 @@ function loadPriority() {
1930
1943
  priorityCache = { accountOrder: [], lastUpdated: 0 };
1931
1944
  return priorityCache;
1932
1945
  }
1946
+ function savePriority(data) {
1947
+ try {
1948
+ mkdirSync(DATA_DIR3, { recursive: true });
1949
+ writeFileSync(PRIORITY_FILE, JSON.stringify(data, null, 2), "utf-8");
1950
+ priorityCache = data;
1951
+ } catch (err) {
1952
+ console.error("\u274C [AccountPriority] Failed to save priority file:", err.message);
1953
+ }
1954
+ }
1955
+ function markAccountSuccessful(accountId) {
1956
+ if (!isPersistableAccount(accountId)) return;
1957
+ const data = loadPriority();
1958
+ data.accountOrder = data.accountOrder.filter((id) => id !== accountId);
1959
+ data.accountOrder.unshift(accountId);
1960
+ data.lastUpdated = Date.now();
1961
+ savePriority(data);
1962
+ }
1963
+ function markAccountFailed(accountId) {
1964
+ if (!isPersistableAccount(accountId)) return;
1965
+ const data = loadPriority();
1966
+ data.accountOrder = data.accountOrder.filter((id) => id !== accountId);
1967
+ data.accountOrder.push(accountId);
1968
+ data.lastUpdated = Date.now();
1969
+ savePriority(data);
1970
+ }
1971
+ function ensureAccountInPriority(accountId) {
1972
+ if (!isPersistableAccount(accountId)) return;
1973
+ const data = loadPriority();
1974
+ if (data.accountOrder.includes(accountId)) {
1975
+ return;
1976
+ }
1977
+ data.accountOrder.push(accountId);
1978
+ data.lastUpdated = Date.now();
1979
+ savePriority(data);
1980
+ }
1933
1981
  function getAccountsByPriority(accounts) {
1934
1982
  const data = loadPriority();
1935
1983
  if (data.accountOrder.length === 0) {
@@ -6583,6 +6631,11 @@ async function ensurePlaywrightInitialized(accountId) {
6583
6631
  });
6584
6632
  } catch {
6585
6633
  }
6634
+ try {
6635
+ const { ensureAccountInPriority: ensureAccountInPriority2 } = await Promise.resolve().then(() => (init_account_priority(), account_priority_exports));
6636
+ ensureAccountInPriority2(accountId);
6637
+ } catch {
6638
+ }
6586
6639
  }
6587
6640
  async function getBasicHeaders2(accountId) {
6588
6641
  if (isAuthMockEnabled()) {
@@ -9514,9 +9567,13 @@ function parseQwenJsonError(raw, status, accountId) {
9514
9567
  }
9515
9568
  if (errorJson?.success === false) {
9516
9569
  const code = errorJson.data?.code || errorJson.code || "UpstreamError";
9517
- if (status === 401 || code === "Unauthorized" || typeof details === "string" && (details.includes("login") || details.includes("session"))) {
9570
+ const detailsStr = typeof details === "string" ? details : "";
9571
+ const codeStr = String(code).toLowerCase();
9572
+ const detailsLower = detailsStr.toLowerCase();
9573
+ const isAuthExpired = status === 401 || codeStr.includes("unauthorized") || detailsLower.includes("token has expired") || detailsLower.includes("log in") || detailsLower.includes("login") || detailsLower.includes("session") || detailsLower.includes("n\xE3o autorizado") || detailsLower.includes("unauthorized");
9574
+ if (isAuthExpired) {
9518
9575
  return new QwenSessionExpiredError(
9519
- `Session expired: ${details}`,
9576
+ `Session expired: ${detailsStr || codeStr}`,
9520
9577
  accountId || "global"
9521
9578
  );
9522
9579
  }
@@ -10055,8 +10112,24 @@ async function createQwenStreamInternal(prompt, enableThinking, modelId, forcedP
10055
10112
  )
10056
10113
  );
10057
10114
  }
10115
+ const parsedJsonErr = parseQwenJsonError(errText, response.status, accountId);
10116
+ if (parsedJsonErr instanceof QwenSessionExpiredError && accountId && !retriedNonSseResponse) {
10117
+ retriedNonSseResponse = true;
10118
+ logger.warn(
10119
+ "[Qwen] Token expired on completion fetch (200 JSON); refreshing session and retrying",
10120
+ { accountId, chatId: chatSessionId ?? "new" }
10121
+ );
10122
+ const { refreshAccountToken: refreshAccountToken2 } = await Promise.resolve().then(() => (init_playwright(), playwright_exports));
10123
+ const refreshed = await refreshAccountToken2(accountId);
10124
+ if (refreshed.success) {
10125
+ const fresh = await getQwenHeaders(false, accountId);
10126
+ activeHeaders = fresh.headers;
10127
+ response = await fetchCompletion(activeHeaders);
10128
+ continue;
10129
+ }
10130
+ }
10058
10131
  throw withCreatedChatMetadata(
10059
- parseQwenJsonError(errText, response.status, accountId) ?? new QwenUpstreamError(
10132
+ parsedJsonErr ?? new QwenUpstreamError(
10060
10133
  `Qwen returned non-stream JSON response: ${errText.substring(0, 300)}`,
10061
10134
  "NonStreamJsonResponse",
10062
10135
  502
@@ -10107,6 +10180,31 @@ async function createQwenStreamInternal(prompt, enableThinking, modelId, forcedP
10107
10180
  response.status,
10108
10181
  accountId
10109
10182
  );
10183
+ if (parsedError instanceof QwenSessionExpiredError && accountId && !retriedNonSseResponse) {
10184
+ retriedNonSseResponse = true;
10185
+ logger.warn(
10186
+ "[Qwen] Token expired on completion fetch (!response.ok); refreshing session and retrying",
10187
+ { accountId, chatId: chatSessionId ?? "new", status: response.status }
10188
+ );
10189
+ const { refreshAccountToken: refreshAccountToken2 } = await Promise.resolve().then(() => (init_playwright(), playwright_exports));
10190
+ const refreshed = await refreshAccountToken2(accountId);
10191
+ if (refreshed.success) {
10192
+ const fresh = await getQwenHeaders(false, accountId);
10193
+ activeHeaders = fresh.headers;
10194
+ response = await fetchCompletion(activeHeaders);
10195
+ if (response.ok && response.body) {
10196
+ return {
10197
+ stream: wrapUpstreamStream(response.body, controller),
10198
+ headers: activeHeaders,
10199
+ uiSessionId: chatSessionId || "",
10200
+ controller,
10201
+ accountId: accountId ?? "global",
10202
+ createdNewChat,
10203
+ tokenEstimationContext
10204
+ };
10205
+ }
10206
+ }
10207
+ }
10110
10208
  if (parsedError) {
10111
10209
  throw withCreatedChatMetadata(parsedError);
10112
10210
  }
package/dist/index.js CHANGED
@@ -263,6 +263,7 @@ var init_config = __esm({
263
263
  // own context while serving. Accounts in cooldown (rate-limited) sit idle,
264
264
  // drop out of the warm set and get evicted.
265
265
  PLAYWRIGHT_MAX_ACTIVE_CONTEXTS: z.string().default("2"),
266
+ PLAYWRIGHT_STARTUP_WARM_ACCOUNTS: z.string().default("4"),
266
267
  PLAYWRIGHT_PREPARE_ALL_ON_STARTUP: z.string().default("false"),
267
268
  CAPTCHA_SOLVER_ENABLED: z.string().default("true"),
268
269
  CAPTCHA_SOLVER_MAX_ATTEMPTS: z.string().default("3"),
@@ -411,6 +412,7 @@ var init_config = __esm({
411
412
  jsHeapMb: Math.max(64, parseInt(env.PLAYWRIGHT_JS_HEAP_MB)),
412
413
  lowMemoryFlags: env.PLAYWRIGHT_LOW_MEMORY_FLAGS !== "false",
413
414
  maxActiveContexts: Math.max(0, parseInt(env.PLAYWRIGHT_MAX_ACTIVE_CONTEXTS)),
415
+ startupWarmAccounts: Math.max(1, parseInt(env.PLAYWRIGHT_STARTUP_WARM_ACCOUNTS)),
414
416
  prepareAllOnStartup: env.PLAYWRIGHT_PREPARE_ALL_ON_STARTUP !== "false"
415
417
  },
416
418
  captcha: {
@@ -8022,6 +8024,11 @@ async function ensurePlaywrightInitialized(accountId) {
8022
8024
  });
8023
8025
  } catch {
8024
8026
  }
8027
+ try {
8028
+ const { ensureAccountInPriority: ensureAccountInPriority2 } = await Promise.resolve().then(() => (init_account_priority(), account_priority_exports));
8029
+ ensureAccountInPriority2(accountId);
8030
+ } catch {
8031
+ }
8025
8032
  }
8026
8033
  async function getBasicHeaders2(accountId) {
8027
8034
  if (isAuthMockEnabled()) {
@@ -12729,9 +12736,13 @@ function parseQwenJsonError(raw, status, accountId) {
12729
12736
  }
12730
12737
  if (errorJson?.success === false) {
12731
12738
  const code = errorJson.data?.code || errorJson.code || "UpstreamError";
12732
- if (status === 401 || code === "Unauthorized" || typeof details === "string" && (details.includes("login") || details.includes("session"))) {
12739
+ const detailsStr = typeof details === "string" ? details : "";
12740
+ const codeStr = String(code).toLowerCase();
12741
+ const detailsLower = detailsStr.toLowerCase();
12742
+ const isAuthExpired = status === 401 || codeStr.includes("unauthorized") || detailsLower.includes("token has expired") || detailsLower.includes("log in") || detailsLower.includes("login") || detailsLower.includes("session") || detailsLower.includes("n\xE3o autorizado") || detailsLower.includes("unauthorized");
12743
+ if (isAuthExpired) {
12733
12744
  return new QwenSessionExpiredError(
12734
- `Session expired: ${details}`,
12745
+ `Session expired: ${detailsStr || codeStr}`,
12735
12746
  accountId || "global"
12736
12747
  );
12737
12748
  }
@@ -13270,8 +13281,24 @@ async function createQwenStreamInternal(prompt, enableThinking, modelId, forcedP
13270
13281
  )
13271
13282
  );
13272
13283
  }
13284
+ const parsedJsonErr = parseQwenJsonError(errText, response.status, accountId);
13285
+ if (parsedJsonErr instanceof QwenSessionExpiredError && accountId && !retriedNonSseResponse) {
13286
+ retriedNonSseResponse = true;
13287
+ logger.warn(
13288
+ "[Qwen] Token expired on completion fetch (200 JSON); refreshing session and retrying",
13289
+ { accountId, chatId: chatSessionId ?? "new" }
13290
+ );
13291
+ const { refreshAccountToken: refreshAccountToken2 } = await Promise.resolve().then(() => (init_playwright(), playwright_exports));
13292
+ const refreshed = await refreshAccountToken2(accountId);
13293
+ if (refreshed.success) {
13294
+ const fresh = await getQwenHeaders(false, accountId);
13295
+ activeHeaders = fresh.headers;
13296
+ response = await fetchCompletion(activeHeaders);
13297
+ continue;
13298
+ }
13299
+ }
13273
13300
  throw withCreatedChatMetadata(
13274
- parseQwenJsonError(errText, response.status, accountId) ?? new QwenUpstreamError(
13301
+ parsedJsonErr ?? new QwenUpstreamError(
13275
13302
  `Qwen returned non-stream JSON response: ${errText.substring(0, 300)}`,
13276
13303
  "NonStreamJsonResponse",
13277
13304
  502
@@ -13322,6 +13349,31 @@ async function createQwenStreamInternal(prompt, enableThinking, modelId, forcedP
13322
13349
  response.status,
13323
13350
  accountId
13324
13351
  );
13352
+ if (parsedError instanceof QwenSessionExpiredError && accountId && !retriedNonSseResponse) {
13353
+ retriedNonSseResponse = true;
13354
+ logger.warn(
13355
+ "[Qwen] Token expired on completion fetch (!response.ok); refreshing session and retrying",
13356
+ { accountId, chatId: chatSessionId ?? "new", status: response.status }
13357
+ );
13358
+ const { refreshAccountToken: refreshAccountToken2 } = await Promise.resolve().then(() => (init_playwright(), playwright_exports));
13359
+ const refreshed = await refreshAccountToken2(accountId);
13360
+ if (refreshed.success) {
13361
+ const fresh = await getQwenHeaders(false, accountId);
13362
+ activeHeaders = fresh.headers;
13363
+ response = await fetchCompletion(activeHeaders);
13364
+ if (response.ok && response.body) {
13365
+ return {
13366
+ stream: wrapUpstreamStream(response.body, controller),
13367
+ headers: activeHeaders,
13368
+ uiSessionId: chatSessionId || "",
13369
+ controller,
13370
+ accountId: accountId ?? "global",
13371
+ createdNewChat,
13372
+ tokenEstimationContext
13373
+ };
13374
+ }
13375
+ }
13376
+ }
13325
13377
  if (parsedError) {
13326
13378
  throw withCreatedChatMetadata(parsedError);
13327
13379
  }
@@ -17099,16 +17151,16 @@ ${loopReminder}` : truncation.prompt;
17099
17151
  error: new Error("Failed to create Qwen stream")
17100
17152
  };
17101
17153
  }
17102
- if (err instanceof QwenSessionExpiredError || err.name === "QwenSessionExpiredError") {
17154
+ const isSessionExpired = err instanceof QwenSessionExpiredError || err?.name === "QwenSessionExpiredError" || err instanceof Error && (err.message.toLowerCase().includes("token has expired") || err.message.toLowerCase().includes("session expired") || err.message.toLowerCase().includes("401 n\xE3o autorizado") || err.message.toLowerCase().includes("unauthorized"));
17155
+ if (isSessionExpired) {
17103
17156
  console.warn(
17104
- `\u{1F504} [Chat] Session expired for ${currentAccountEmail} (${currentAccountId}). Attempting re-login...`
17157
+ `\u{1F504} [Chat] Session expired for ${currentAccountEmail} (${currentAccountId}). Attempting silent token refresh/re-login...`
17105
17158
  );
17106
17159
  const reLoginOk = await attemptRelogin(
17107
17160
  currentAccountId,
17108
17161
  currentAccountEmail
17109
17162
  );
17110
17163
  if (reLoginOk) continue;
17111
- return { success: false, error: err };
17112
17164
  }
17113
17165
  if (isAccountUnavailableError(err)) {
17114
17166
  const quotaMsg = err.message || "Unknown quota error";
@@ -25884,7 +25936,7 @@ function getAppVersion() {
25884
25936
  }
25885
25937
  } catch {
25886
25938
  }
25887
- cachedVersion = "v1.5.1";
25939
+ cachedVersion = "v1.5.3";
25888
25940
  return cachedVersion;
25889
25941
  }
25890
25942
  var cachedVersion;
@@ -26288,17 +26340,16 @@ async function startServer(options) {
26288
26340
  );
26289
26341
  });
26290
26342
  } else if (remainingAccounts.length > 0) {
26291
- console.log(
26292
- `\u{1FAB6} [Server] ${remainingAccounts.length} standby account(s) will initialize on demand`
26293
- );
26294
26343
  void (async () => {
26295
- const { validateAccountLogin: validateAccountLogin2 } = await Promise.resolve().then(() => (init_playwright(), playwright_exports));
26296
26344
  const { ensureAccountInPriority: ensureAccountInPriority2 } = await Promise.resolve().then(() => (init_account_priority(), account_priority_exports));
26297
- let accountsToValidate = remainingAccounts;
26298
- if (config.playwright.maxActiveContexts > 1 && remainingAccounts.length > 0) {
26299
- let reserveCandidateIdx = 0;
26300
- for (; reserveCandidateIdx < remainingAccounts.length; reserveCandidateIdx++) {
26301
- const reserveAccount = remainingAccounts[reserveCandidateIdx];
26345
+ const targetWarmTotal = Math.min(
26346
+ config.playwright.startupWarmAccounts,
26347
+ totalAccounts
26348
+ );
26349
+ let currentReadyCount = readyAccountIds.size;
26350
+ if (currentReadyCount < targetWarmTotal && remainingAccounts.length > 0) {
26351
+ for (const reserveAccount of remainingAccounts) {
26352
+ if (currentReadyCount >= targetWarmTotal) break;
26302
26353
  try {
26303
26354
  const ok = await prepareAccountRuntime(
26304
26355
  reserveAccount,
@@ -26308,12 +26359,12 @@ async function startServer(options) {
26308
26359
  warmQwenChatPool2
26309
26360
  );
26310
26361
  if (ok) {
26362
+ readyAccountIds.add(reserveAccount.id);
26311
26363
  ensureAccountInPriority2(reserveAccount.id);
26364
+ currentReadyCount++;
26312
26365
  console.log(
26313
- `\u2705 [Server] Reserve account ready (2/${totalAccounts}): ${maskEmail(reserveAccount.email)}`
26366
+ `\u2705 [Server] Reserve account ready (${currentReadyCount}/${totalAccounts}): ${maskEmail(reserveAccount.email)}`
26314
26367
  );
26315
- reserveCandidateIdx++;
26316
- break;
26317
26368
  }
26318
26369
  } catch (err) {
26319
26370
  console.warn(
@@ -26321,51 +26372,16 @@ async function startServer(options) {
26321
26372
  );
26322
26373
  }
26323
26374
  }
26324
- accountsToValidate = remainingAccounts.slice(reserveCandidateIdx);
26325
- }
26326
- let validated = 0;
26327
- let failed = 0;
26328
- for (const account of accountsToValidate) {
26329
- try {
26330
- const creds = getAccountCredentials2(account.id) ?? account;
26331
- const ok = await validateAccountLogin2(
26332
- creds,
26333
- config.playwright.headless,
26334
- config.playwright.browser
26335
- );
26336
- if (ok) {
26337
- ensureAccountInPriority2(account.id);
26338
- validated++;
26339
- console.log(
26340
- `\u2705 [Server] Standby account validated: ${maskEmail(account.email)}`
26341
- );
26342
- } else {
26343
- failed++;
26344
- console.warn(
26345
- `\u26A0\uFE0F [Server] Standby account login failed: ${maskEmail(account.email)} (quarantined)`
26346
- );
26347
- }
26348
- } catch (error) {
26349
- failed++;
26350
- console.warn(
26351
- `\u26A0\uFE0F [Server] Standby account validation error: ${maskEmail(account.email)}: ${getErrorMessage2(error)} (quarantined)`
26352
- );
26353
- const { markAccountRateLimited: markAccountRateLimited2 } = await Promise.resolve().then(() => (init_account_manager(), account_manager_exports));
26354
- markAccountRateLimited2(
26355
- account.id,
26356
- 24 * 3600 * 1e3,
26357
- `StandbyValidationError: ${getErrorMessage2(error)}`
26358
- );
26359
- }
26360
26375
  }
26361
- if (validated > 0 || failed > 0) {
26376
+ const standbyCount = totalAccounts - currentReadyCount;
26377
+ if (standbyCount > 0) {
26362
26378
  console.log(
26363
- `\u2705 [Server] Standby validation complete: ${validated} account(s) ready${failed > 0 ? `, ${failed} failed` : ""}`
26379
+ `\u{1FAB6} [Server] ${standbyCount} standby account(s) ready in pool (will initialize on demand when active accounts rotate)`
26364
26380
  );
26365
26381
  }
26366
26382
  })().catch((error) => {
26367
26383
  console.warn(
26368
- `\u274C [Server] Background standby validation failed: ${getErrorMessage2(error)}`
26384
+ `\u274C [Server] Background account preparation failed: ${getErrorMessage2(error)}`
26369
26385
  );
26370
26386
  });
26371
26387
  }
package/dist/login.js CHANGED
@@ -1200,6 +1200,7 @@ var init_config = __esm({
1200
1200
  // own context while serving. Accounts in cooldown (rate-limited) sit idle,
1201
1201
  // drop out of the warm set and get evicted.
1202
1202
  PLAYWRIGHT_MAX_ACTIVE_CONTEXTS: z.string().default("2"),
1203
+ PLAYWRIGHT_STARTUP_WARM_ACCOUNTS: z.string().default("4"),
1203
1204
  PLAYWRIGHT_PREPARE_ALL_ON_STARTUP: z.string().default("false"),
1204
1205
  CAPTCHA_SOLVER_ENABLED: z.string().default("true"),
1205
1206
  CAPTCHA_SOLVER_MAX_ATTEMPTS: z.string().default("3"),
@@ -1348,6 +1349,7 @@ var init_config = __esm({
1348
1349
  jsHeapMb: Math.max(64, parseInt(env.PLAYWRIGHT_JS_HEAP_MB)),
1349
1350
  lowMemoryFlags: env.PLAYWRIGHT_LOW_MEMORY_FLAGS !== "false",
1350
1351
  maxActiveContexts: Math.max(0, parseInt(env.PLAYWRIGHT_MAX_ACTIVE_CONTEXTS)),
1352
+ startupWarmAccounts: Math.max(1, parseInt(env.PLAYWRIGHT_STARTUP_WARM_ACCOUNTS)),
1351
1353
  prepareAllOnStartup: env.PLAYWRIGHT_PREPARE_ALL_ON_STARTUP !== "false"
1352
1354
  },
1353
1355
  captcha: {
@@ -1302,6 +1302,7 @@ var init_config = __esm({
1302
1302
  // own context while serving. Accounts in cooldown (rate-limited) sit idle,
1303
1303
  // drop out of the warm set and get evicted.
1304
1304
  PLAYWRIGHT_MAX_ACTIVE_CONTEXTS: z.string().default("2"),
1305
+ PLAYWRIGHT_STARTUP_WARM_ACCOUNTS: z.string().default("4"),
1305
1306
  PLAYWRIGHT_PREPARE_ALL_ON_STARTUP: z.string().default("false"),
1306
1307
  CAPTCHA_SOLVER_ENABLED: z.string().default("true"),
1307
1308
  CAPTCHA_SOLVER_MAX_ATTEMPTS: z.string().default("3"),
@@ -1450,6 +1451,7 @@ var init_config = __esm({
1450
1451
  jsHeapMb: Math.max(64, parseInt(env.PLAYWRIGHT_JS_HEAP_MB)),
1451
1452
  lowMemoryFlags: env.PLAYWRIGHT_LOW_MEMORY_FLAGS !== "false",
1452
1453
  maxActiveContexts: Math.max(0, parseInt(env.PLAYWRIGHT_MAX_ACTIVE_CONTEXTS)),
1454
+ startupWarmAccounts: Math.max(1, parseInt(env.PLAYWRIGHT_STARTUP_WARM_ACCOUNTS)),
1453
1455
  prepareAllOnStartup: env.PLAYWRIGHT_PREPARE_ALL_ON_STARTUP !== "false"
1454
1456
  },
1455
1457
  captcha: {
@@ -157,6 +157,7 @@ var envSchema = z.object({
157
157
  // own context while serving. Accounts in cooldown (rate-limited) sit idle,
158
158
  // drop out of the warm set and get evicted.
159
159
  PLAYWRIGHT_MAX_ACTIVE_CONTEXTS: z.string().default("2"),
160
+ PLAYWRIGHT_STARTUP_WARM_ACCOUNTS: z.string().default("4"),
160
161
  PLAYWRIGHT_PREPARE_ALL_ON_STARTUP: z.string().default("false"),
161
162
  CAPTCHA_SOLVER_ENABLED: z.string().default("true"),
162
163
  CAPTCHA_SOLVER_MAX_ATTEMPTS: z.string().default("3"),
@@ -305,6 +306,7 @@ var config = {
305
306
  jsHeapMb: Math.max(64, parseInt(env.PLAYWRIGHT_JS_HEAP_MB)),
306
307
  lowMemoryFlags: env.PLAYWRIGHT_LOW_MEMORY_FLAGS !== "false",
307
308
  maxActiveContexts: Math.max(0, parseInt(env.PLAYWRIGHT_MAX_ACTIVE_CONTEXTS)),
309
+ startupWarmAccounts: Math.max(1, parseInt(env.PLAYWRIGHT_STARTUP_WARM_ACCOUNTS)),
308
310
  prepareAllOnStartup: env.PLAYWRIGHT_PREPARE_ALL_ON_STARTUP !== "false"
309
311
  },
310
312
  captcha: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qwenproxy-cli",
3
- "version": "1.5.1",
3
+ "version": "1.5.3",
4
4
  "description": "High-performance OpenAI & Anthropic compatible API gateway for Qwen with multi-account rotation, interactive TUI, and resilient tool calling.",
5
5
  "main": "src/index.ts",
6
6
  "bin": {
package/src/api/server.ts CHANGED
@@ -877,23 +877,20 @@ export async function startServer(options?: {
877
877
  );
878
878
  });
879
879
  } else if (remainingAccounts.length > 0) {
880
- console.log(
881
- `🪶 [Server] ${remainingAccounts.length} standby account(s) will initialize on demand`,
882
- );
883
-
884
- // In background: warm 1 reserve account (if maxActiveContexts > 1) and
885
- // validate the rest of the standby accounts
880
+ // In background: warm reserve accounts up to a safe limit (default 4 total ready accounts)
881
+ // to prevent IP-level login flood / anti-bot challenge rate-limits on Alibaba.
886
882
  void (async () => {
887
- const { validateAccountLogin } = await import("../services/playwright.ts");
888
883
  const { ensureAccountInPriority } = await import("../core/account-priority.ts");
889
884
 
890
- let accountsToValidate = remainingAccounts;
885
+ const targetWarmTotal = Math.min(
886
+ config.playwright.startupWarmAccounts,
887
+ totalAccounts,
888
+ );
889
+ let currentReadyCount = readyAccountIds.size;
891
890
 
892
- // Warm reserve account in background for fast failover without delaying startup
893
- if (config.playwright.maxActiveContexts > 1 && remainingAccounts.length > 0) {
894
- let reserveCandidateIdx = 0;
895
- for (; reserveCandidateIdx < remainingAccounts.length; reserveCandidateIdx++) {
896
- const reserveAccount = remainingAccounts[reserveCandidateIdx];
891
+ if (currentReadyCount < targetWarmTotal && remainingAccounts.length > 0) {
892
+ for (const reserveAccount of remainingAccounts) {
893
+ if (currentReadyCount >= targetWarmTotal) break;
897
894
  try {
898
895
  const ok = await prepareAccountRuntime(
899
896
  reserveAccount,
@@ -903,12 +900,12 @@ export async function startServer(options?: {
903
900
  warmQwenChatPool,
904
901
  );
905
902
  if (ok) {
903
+ readyAccountIds.add(reserveAccount.id);
906
904
  ensureAccountInPriority(reserveAccount.id);
905
+ currentReadyCount++;
907
906
  console.log(
908
- `✅ [Server] Reserve account ready (2/${totalAccounts}): ${maskEmail(reserveAccount.email)}`,
907
+ `✅ [Server] Reserve account ready (${currentReadyCount}/${totalAccounts}): ${maskEmail(reserveAccount.email)}`,
909
908
  );
910
- reserveCandidateIdx++;
911
- break;
912
909
  }
913
910
  } catch (err) {
914
911
  console.warn(
@@ -916,56 +913,17 @@ export async function startServer(options?: {
916
913
  );
917
914
  }
918
915
  }
919
- accountsToValidate = remainingAccounts.slice(reserveCandidateIdx);
920
916
  }
921
917
 
922
- let validated = 0;
923
- let failed = 0;
924
-
925
- for (const account of accountsToValidate) {
926
- try {
927
- const creds = getAccountCredentials(account.id) ?? account;
928
- // Validate login in background with real unmasked credentials
929
- const ok = await validateAccountLogin(
930
- creds,
931
- config.playwright.headless,
932
- config.playwright.browser,
933
- );
934
- if (ok) {
935
- // Add to priority list only once validated
936
- ensureAccountInPriority(account.id);
937
- validated++;
938
- console.log(
939
- `✅ [Server] Standby account validated: ${maskEmail(account.email)}`,
940
- );
941
- } else {
942
- failed++;
943
- console.warn(
944
- `⚠️ [Server] Standby account login failed: ${maskEmail(account.email)} (quarantined)`,
945
- );
946
- }
947
- } catch (error) {
948
- failed++;
949
- console.warn(
950
- `⚠️ [Server] Standby account validation error: ${maskEmail(account.email)}: ${getErrorMessage(error)} (quarantined)`,
951
- );
952
- const { markAccountRateLimited } = await import("../core/account-manager.ts");
953
- markAccountRateLimited(
954
- account.id,
955
- 24 * 3600 * 1000,
956
- `StandbyValidationError: ${getErrorMessage(error)}`,
957
- );
958
- }
959
- }
960
-
961
- if (validated > 0 || failed > 0) {
918
+ const standbyCount = totalAccounts - currentReadyCount;
919
+ if (standbyCount > 0) {
962
920
  console.log(
963
- `✅ [Server] Standby validation complete: ${validated} account(s) ready${failed > 0 ? `, ${failed} failed` : ""}`,
921
+ `🪶 [Server] ${standbyCount} standby account(s) ready in pool (will initialize on demand when active accounts rotate)`,
964
922
  );
965
923
  }
966
924
  })().catch((error) => {
967
925
  console.warn(
968
- `❌ [Server] Background standby validation failed: ${getErrorMessage(error)}`,
926
+ `❌ [Server] Background account preparation failed: ${getErrorMessage(error)}`,
969
927
  );
970
928
  });
971
929
  }
@@ -76,6 +76,7 @@ const envSchema = z
76
76
  // own context while serving. Accounts in cooldown (rate-limited) sit idle,
77
77
  // drop out of the warm set and get evicted.
78
78
  PLAYWRIGHT_MAX_ACTIVE_CONTEXTS: z.string().default("2"),
79
+ PLAYWRIGHT_STARTUP_WARM_ACCOUNTS: z.string().default("4"),
79
80
  PLAYWRIGHT_PREPARE_ALL_ON_STARTUP: z.string().default("false"),
80
81
  CAPTCHA_SOLVER_ENABLED: z.string().default("true"),
81
82
  CAPTCHA_SOLVER_MAX_ATTEMPTS: z.string().default("3"),
@@ -235,6 +236,7 @@ export const config = {
235
236
  jsHeapMb: Math.max(64, parseInt(env.PLAYWRIGHT_JS_HEAP_MB)),
236
237
  lowMemoryFlags: env.PLAYWRIGHT_LOW_MEMORY_FLAGS !== "false",
237
238
  maxActiveContexts: Math.max(0, parseInt(env.PLAYWRIGHT_MAX_ACTIVE_CONTEXTS)),
239
+ startupWarmAccounts: Math.max(1, parseInt(env.PLAYWRIGHT_STARTUP_WARM_ACCOUNTS)),
238
240
  prepareAllOnStartup: env.PLAYWRIGHT_PREPARE_ALL_ON_STARTUP !== "false",
239
241
  },
240
242
  captcha: {
@@ -17,6 +17,6 @@ export function getAppVersion(): string {
17
17
  }
18
18
  }
19
19
  } catch {}
20
- cachedVersion = "v1.5.1";
20
+ cachedVersion = "v1.5.3";
21
21
  return cachedVersion;
22
22
  }
@@ -1487,19 +1487,24 @@ async function tryCreateStreamWithRetry(
1487
1487
  };
1488
1488
  }
1489
1489
 
1490
- if (
1490
+ const isSessionExpired =
1491
1491
  err instanceof QwenSessionExpiredError ||
1492
- err.name === "QwenSessionExpiredError"
1493
- ) {
1492
+ err?.name === "QwenSessionExpiredError" ||
1493
+ (err instanceof Error &&
1494
+ (err.message.toLowerCase().includes("token has expired") ||
1495
+ err.message.toLowerCase().includes("session expired") ||
1496
+ err.message.toLowerCase().includes("401 não autorizado") ||
1497
+ err.message.toLowerCase().includes("unauthorized")));
1498
+
1499
+ if (isSessionExpired) {
1494
1500
  console.warn(
1495
- `🔄 [Chat] Session expired for ${currentAccountEmail} (${currentAccountId}). Attempting re-login...`,
1501
+ `🔄 [Chat] Session expired for ${currentAccountEmail} (${currentAccountId}). Attempting silent token refresh/re-login...`,
1496
1502
  );
1497
1503
  const reLoginOk = await attemptRelogin(
1498
1504
  currentAccountId,
1499
1505
  currentAccountEmail,
1500
1506
  );
1501
1507
  if (reLoginOk) continue;
1502
- return { success: false, error: err };
1503
1508
  }
1504
1509
 
1505
1510
 
@@ -56,6 +56,11 @@ async function ensurePlaywrightInitialized(accountId: string): Promise<void> {
56
56
  } catch {
57
57
  // Non-fatal: chat creation will still work with default account settings.
58
58
  }
59
+
60
+ try {
61
+ const { ensureAccountInPriority } = await import("../core/account-priority.ts");
62
+ ensureAccountInPriority(accountId);
63
+ } catch {}
59
64
  }
60
65
 
61
66
  export async function getBasicHeaders(accountId?: string): Promise<{
@@ -2502,15 +2502,23 @@ function parseQwenJsonError(
2502
2502
 
2503
2503
  if (errorJson?.success === false) {
2504
2504
  const code = errorJson.data?.code || errorJson.code || "UpstreamError";
2505
+ const detailsStr = typeof details === "string" ? details : "";
2506
+ const codeStr = String(code).toLowerCase();
2507
+ const detailsLower = detailsStr.toLowerCase();
2505
2508
 
2506
- if (
2509
+ const isAuthExpired =
2507
2510
  status === 401 ||
2508
- code === "Unauthorized" ||
2509
- (typeof details === "string" &&
2510
- (details.includes("login") || details.includes("session")))
2511
- ) {
2511
+ codeStr.includes("unauthorized") ||
2512
+ detailsLower.includes("token has expired") ||
2513
+ detailsLower.includes("log in") ||
2514
+ detailsLower.includes("login") ||
2515
+ detailsLower.includes("session") ||
2516
+ detailsLower.includes("não autorizado") ||
2517
+ detailsLower.includes("unauthorized");
2518
+
2519
+ if (isAuthExpired) {
2512
2520
  return new QwenSessionExpiredError(
2513
- `Session expired: ${details}`,
2521
+ `Session expired: ${detailsStr || codeStr}`,
2514
2522
  accountId || "global",
2515
2523
  );
2516
2524
  }
@@ -3301,8 +3309,29 @@ async function createQwenStreamInternal(
3301
3309
  );
3302
3310
  }
3303
3311
 
3312
+ const parsedJsonErr = parseQwenJsonError(errText, response.status, accountId);
3313
+ if (
3314
+ parsedJsonErr instanceof QwenSessionExpiredError &&
3315
+ accountId &&
3316
+ !retriedNonSseResponse
3317
+ ) {
3318
+ retriedNonSseResponse = true;
3319
+ logger.warn(
3320
+ "[Qwen] Token expired on completion fetch (200 JSON); refreshing session and retrying",
3321
+ { accountId, chatId: chatSessionId ?? "new" },
3322
+ );
3323
+ const { refreshAccountToken } = await import("./playwright.ts");
3324
+ const refreshed = await refreshAccountToken(accountId);
3325
+ if (refreshed.success) {
3326
+ const fresh = await getQwenHeaders(false, accountId);
3327
+ activeHeaders = fresh.headers;
3328
+ response = await fetchCompletion(activeHeaders);
3329
+ continue;
3330
+ }
3331
+ }
3332
+
3304
3333
  throw withCreatedChatMetadata(
3305
- parseQwenJsonError(errText, response.status, accountId) ??
3334
+ parsedJsonErr ??
3306
3335
  new QwenUpstreamError(
3307
3336
  `Qwen returned non-stream JSON response: ${errText.substring(0, 300)}`,
3308
3337
  "NonStreamJsonResponse",
@@ -3378,6 +3407,35 @@ async function createQwenStreamInternal(
3378
3407
  response.status,
3379
3408
  accountId,
3380
3409
  );
3410
+ if (
3411
+ parsedError instanceof QwenSessionExpiredError &&
3412
+ accountId &&
3413
+ !retriedNonSseResponse
3414
+ ) {
3415
+ retriedNonSseResponse = true;
3416
+ logger.warn(
3417
+ "[Qwen] Token expired on completion fetch (!response.ok); refreshing session and retrying",
3418
+ { accountId, chatId: chatSessionId ?? "new", status: response.status },
3419
+ );
3420
+ const { refreshAccountToken } = await import("./playwright.ts");
3421
+ const refreshed = await refreshAccountToken(accountId);
3422
+ if (refreshed.success) {
3423
+ const fresh = await getQwenHeaders(false, accountId);
3424
+ activeHeaders = fresh.headers;
3425
+ response = await fetchCompletion(activeHeaders);
3426
+ if (response.ok && response.body) {
3427
+ return {
3428
+ stream: wrapUpstreamStream(response.body, controller),
3429
+ headers: activeHeaders,
3430
+ uiSessionId: chatSessionId || "",
3431
+ controller,
3432
+ accountId: accountId ?? "global",
3433
+ createdNewChat,
3434
+ tokenEstimationContext,
3435
+ };
3436
+ }
3437
+ }
3438
+ }
3381
3439
  if (parsedError) {
3382
3440
  throw withCreatedChatMetadata(parsedError);
3383
3441
  }