qwenproxy-cli 1.4.8 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/login.js CHANGED
@@ -2016,7 +2016,15 @@ function defaultCooldownDurationMs() {
2016
2016
  return computeQuotaCooldownMs(Date.now());
2017
2017
  }
2018
2018
  function markAccountRateLimited(accountId, cooldownMs, reason, options = {}) {
2019
+ if (cooldownMs !== void 0 && cooldownMs <= 0) {
2020
+ clearAccountCooldown(accountId);
2021
+ return;
2022
+ }
2019
2023
  const duration = cooldownMs ?? defaultCooldownDurationMs();
2024
+ if (duration <= 0) {
2025
+ clearAccountCooldown(accountId);
2026
+ return;
2027
+ }
2020
2028
  const until = Date.now() + duration;
2021
2029
  const cooldownReason = reason ?? "RateLimited";
2022
2030
  cooldowns.set(accountId, {
@@ -3396,26 +3404,6 @@ async function isPageLoggedIn(page, timeoutMs = SESSION_PROBE_NAVIGATION_TIMEOUT
3396
3404
  }
3397
3405
  } catch {
3398
3406
  }
3399
- try {
3400
- const refreshRes = await fetch("https://auth.qwen.ai/api/v2/auths/refresh", {
3401
- method: "GET",
3402
- credentials: "include",
3403
- signal: AbortSignal.timeout(3e3)
3404
- });
3405
- if (refreshRes.status === 200) {
3406
- const refreshJson = await refreshRes.json().catch(() => null);
3407
- if (refreshJson && refreshJson.success === false) {
3408
- const code = refreshJson.data?.code || refreshJson.code;
3409
- const details = String(refreshJson.data?.details || "");
3410
- if (code === "Unauthorized" || details.includes("revogado") || details.includes("revoked")) {
3411
- return false;
3412
- }
3413
- }
3414
- } else if (refreshRes.status === 401 || refreshRes.status === 403) {
3415
- return false;
3416
- }
3417
- } catch {
3418
- }
3419
3407
  return true;
3420
3408
  } catch {
3421
3409
  return false;
@@ -4543,7 +4531,7 @@ async function initPlaywrightForAccount(rawAccount, headless = config.playwright
4543
4531
  `\u26A0\uFE0F [Playwright] Initial navigation check for ${maskEmail(account.email)}: ${err.message}`
4544
4532
  );
4545
4533
  }
4546
- if (!options.skipHeaderCapture) {
4534
+ if (!options.skipHeaderCapture && !isAccountHeadersReady(account.id)) {
4547
4535
  acctPage.__qwenChatHomeLoaded = true;
4548
4536
  await captureQwenHeaders(account.id);
4549
4537
  }
@@ -4873,7 +4861,7 @@ async function loginToQwen(accountId, email, password) {
4873
4861
  );
4874
4862
  markAccountRateLimited(
4875
4863
  accountId,
4876
- 24 * 3600 * 1e3,
4864
+ 300 * 1e3,
4877
4865
  "AuthFailed: All login methods exhausted"
4878
4866
  );
4879
4867
  return false;
@@ -5150,38 +5138,49 @@ async function loginViaUi(page, email, password) {
5150
5138
  try {
5151
5139
  const emailEl = document.querySelector('input[name="email"], input[type="email"], input[type="text"]');
5152
5140
  const passEl = document.querySelector('input[type="password"], input[name="password"]');
5153
- if (emailEl) {
5154
- emailEl.dispatchEvent(new Event("input", { bubbles: true }));
5155
- emailEl.dispatchEvent(new Event("change", { bubbles: true }));
5156
- emailEl.dispatchEvent(new Event("blur", { bubbles: true }));
5157
- }
5158
- if (passEl) {
5159
- passEl.dispatchEvent(new Event("input", { bubbles: true }));
5160
- passEl.dispatchEvent(new Event("change", { bubbles: true }));
5161
- passEl.dispatchEvent(new Event("blur", { bubbles: true }));
5162
- }
5141
+ const setReactValue = (el, val) => {
5142
+ const proto = Object.getPrototypeOf(el);
5143
+ const setter = Object.getOwnPropertyDescriptor(proto, "value")?.set;
5144
+ if (setter) {
5145
+ setter.call(el, val);
5146
+ } else {
5147
+ el.value = val;
5148
+ }
5149
+ el.dispatchEvent(new Event("input", { bubbles: true }));
5150
+ el.dispatchEvent(new Event("change", { bubbles: true }));
5151
+ el.dispatchEvent(new Event("blur", { bubbles: true }));
5152
+ };
5153
+ if (emailEl) setReactValue(emailEl, emailEl.value);
5154
+ if (passEl) setReactValue(passEl, passEl.value);
5163
5155
  } catch {
5164
5156
  }
5165
5157
  }).catch(() => {
5166
5158
  });
5159
+ await sleep2(500);
5167
5160
  const submitSelector = 'button.qwenchat-auth-pc-submit-button, button[type="submit"], button:has-text("Log in"), button:has-text("Sign in"), button:has-text("Fazer login"), button:has-text("Entrar"), button:has-text("Iniciar sesi\xF3n"), button:has-text("\u767B\u5F55")';
5168
5161
  const submitButton = page.locator(submitSelector).first();
5162
+ await page.evaluate((sel) => {
5163
+ try {
5164
+ const btn = document.querySelector(sel);
5165
+ if (btn) {
5166
+ btn.removeAttribute("disabled");
5167
+ btn.classList.remove("ant-btn-disabled");
5168
+ btn.click();
5169
+ }
5170
+ } catch {
5171
+ }
5172
+ }, submitSelector).catch(() => {
5173
+ });
5169
5174
  try {
5170
- await page.waitForSelector('button[type="submit"]:not([disabled])', {
5171
- timeout: 4e3
5172
- });
5173
- await submitButton.click();
5174
- } catch {
5175
- const isDisabled = await submitButton.isDisabled().catch(() => true);
5176
- if (!isDisabled) {
5177
- await submitButton.click({ force: true }).catch(() => {
5175
+ if (await submitButton.isVisible({ timeout: 2e3 }).catch(() => false)) {
5176
+ await submitButton.click({ force: true, timeout: 2e3 }).catch(() => {
5178
5177
  });
5179
- } else {
5180
- if (!page.url().includes("/auth") && await isPageLoggedIn(page, 2e3)) {
5181
- return { success: true };
5182
- }
5183
5178
  }
5179
+ } catch {
5184
5180
  }
5181
+ const passInput = page.locator(passwordSelector).first();
5182
+ await passInput.press("Enter", { timeout: 2e3 }).catch(() => {
5183
+ });
5185
5184
  const postSubmitDeadline = Date.now() + 15e3;
5186
5185
  while (Date.now() < postSubmitDeadline) {
5187
5186
  if (page.isClosed()) break;
@@ -1110,7 +1110,15 @@ function defaultCooldownDurationMs() {
1110
1110
  return computeQuotaCooldownMs(Date.now());
1111
1111
  }
1112
1112
  function markAccountRateLimited(accountId, cooldownMs, reason, options = {}) {
1113
+ if (cooldownMs !== void 0 && cooldownMs <= 0) {
1114
+ clearAccountCooldown(accountId);
1115
+ return;
1116
+ }
1113
1117
  const duration = cooldownMs ?? defaultCooldownDurationMs();
1118
+ if (duration <= 0) {
1119
+ clearAccountCooldown(accountId);
1120
+ return;
1121
+ }
1114
1122
  const until = Date.now() + duration;
1115
1123
  const cooldownReason = reason ?? "RateLimited";
1116
1124
  cooldowns.set(accountId, {
@@ -1166,6 +1174,9 @@ function unmarkAccountHeadersReady(accountId) {
1166
1174
  if (!accountId) return;
1167
1175
  headersReadyAccounts.delete(accountId);
1168
1176
  }
1177
+ function isAccountHeadersReady(accountId) {
1178
+ return headersReadyAccounts.has(accountId);
1179
+ }
1169
1180
  var cooldowns, headersReadyAccounts;
1170
1181
  var init_account_manager = __esm({
1171
1182
  "src/core/account-manager.ts"() {
@@ -3113,26 +3124,6 @@ async function isPageLoggedIn(page, timeoutMs = SESSION_PROBE_NAVIGATION_TIMEOUT
3113
3124
  }
3114
3125
  } catch {
3115
3126
  }
3116
- try {
3117
- const refreshRes = await fetch("https://auth.qwen.ai/api/v2/auths/refresh", {
3118
- method: "GET",
3119
- credentials: "include",
3120
- signal: AbortSignal.timeout(3e3)
3121
- });
3122
- if (refreshRes.status === 200) {
3123
- const refreshJson = await refreshRes.json().catch(() => null);
3124
- if (refreshJson && refreshJson.success === false) {
3125
- const code = refreshJson.data?.code || refreshJson.code;
3126
- const details = String(refreshJson.data?.details || "");
3127
- if (code === "Unauthorized" || details.includes("revogado") || details.includes("revoked")) {
3128
- return false;
3129
- }
3130
- }
3131
- } else if (refreshRes.status === 401 || refreshRes.status === 403) {
3132
- return false;
3133
- }
3134
- } catch {
3135
- }
3136
3127
  return true;
3137
3128
  } catch {
3138
3129
  return false;
@@ -3938,7 +3929,7 @@ async function initPlaywrightForAccount(rawAccount, headless = config.playwright
3938
3929
  `\u26A0\uFE0F [Playwright] Initial navigation check for ${maskEmail(account.email)}: ${err.message}`
3939
3930
  );
3940
3931
  }
3941
- if (!options.skipHeaderCapture) {
3932
+ if (!options.skipHeaderCapture && !isAccountHeadersReady(account.id)) {
3942
3933
  acctPage.__qwenChatHomeLoaded = true;
3943
3934
  await captureQwenHeaders(account.id);
3944
3935
  }
@@ -4148,7 +4139,7 @@ async function loginToQwen(accountId, email, password) {
4148
4139
  );
4149
4140
  markAccountRateLimited(
4150
4141
  accountId,
4151
- 24 * 3600 * 1e3,
4142
+ 300 * 1e3,
4152
4143
  "AuthFailed: All login methods exhausted"
4153
4144
  );
4154
4145
  return false;
@@ -4425,38 +4416,49 @@ async function loginViaUi(page, email, password) {
4425
4416
  try {
4426
4417
  const emailEl = document.querySelector('input[name="email"], input[type="email"], input[type="text"]');
4427
4418
  const passEl = document.querySelector('input[type="password"], input[name="password"]');
4428
- if (emailEl) {
4429
- emailEl.dispatchEvent(new Event("input", { bubbles: true }));
4430
- emailEl.dispatchEvent(new Event("change", { bubbles: true }));
4431
- emailEl.dispatchEvent(new Event("blur", { bubbles: true }));
4432
- }
4433
- if (passEl) {
4434
- passEl.dispatchEvent(new Event("input", { bubbles: true }));
4435
- passEl.dispatchEvent(new Event("change", { bubbles: true }));
4436
- passEl.dispatchEvent(new Event("blur", { bubbles: true }));
4437
- }
4419
+ const setReactValue = (el, val) => {
4420
+ const proto = Object.getPrototypeOf(el);
4421
+ const setter = Object.getOwnPropertyDescriptor(proto, "value")?.set;
4422
+ if (setter) {
4423
+ setter.call(el, val);
4424
+ } else {
4425
+ el.value = val;
4426
+ }
4427
+ el.dispatchEvent(new Event("input", { bubbles: true }));
4428
+ el.dispatchEvent(new Event("change", { bubbles: true }));
4429
+ el.dispatchEvent(new Event("blur", { bubbles: true }));
4430
+ };
4431
+ if (emailEl) setReactValue(emailEl, emailEl.value);
4432
+ if (passEl) setReactValue(passEl, passEl.value);
4438
4433
  } catch {
4439
4434
  }
4440
4435
  }).catch(() => {
4441
4436
  });
4437
+ await sleep2(500);
4442
4438
  const submitSelector = 'button.qwenchat-auth-pc-submit-button, button[type="submit"], button:has-text("Log in"), button:has-text("Sign in"), button:has-text("Fazer login"), button:has-text("Entrar"), button:has-text("Iniciar sesi\xF3n"), button:has-text("\u767B\u5F55")';
4443
4439
  const submitButton = page.locator(submitSelector).first();
4440
+ await page.evaluate((sel) => {
4441
+ try {
4442
+ const btn = document.querySelector(sel);
4443
+ if (btn) {
4444
+ btn.removeAttribute("disabled");
4445
+ btn.classList.remove("ant-btn-disabled");
4446
+ btn.click();
4447
+ }
4448
+ } catch {
4449
+ }
4450
+ }, submitSelector).catch(() => {
4451
+ });
4444
4452
  try {
4445
- await page.waitForSelector('button[type="submit"]:not([disabled])', {
4446
- timeout: 4e3
4447
- });
4448
- await submitButton.click();
4449
- } catch {
4450
- const isDisabled = await submitButton.isDisabled().catch(() => true);
4451
- if (!isDisabled) {
4452
- await submitButton.click({ force: true }).catch(() => {
4453
+ if (await submitButton.isVisible({ timeout: 2e3 }).catch(() => false)) {
4454
+ await submitButton.click({ force: true, timeout: 2e3 }).catch(() => {
4453
4455
  });
4454
- } else {
4455
- if (!page.url().includes("/auth") && await isPageLoggedIn(page, 2e3)) {
4456
- return { success: true };
4457
- }
4458
4456
  }
4457
+ } catch {
4459
4458
  }
4459
+ const passInput = page.locator(passwordSelector).first();
4460
+ await passInput.press("Enter", { timeout: 2e3 }).catch(() => {
4461
+ });
4460
4462
  const postSubmitDeadline = Date.now() + 15e3;
4461
4463
  while (Date.now() < postSubmitDeadline) {
4462
4464
  if (page.isClosed()) break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qwenproxy-cli",
3
- "version": "1.4.8",
3
+ "version": "1.5.1",
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": {
@@ -29,8 +29,8 @@
29
29
  "reset": "npx tsx src/reset-cooldowns.ts",
30
30
  "purge": "npx tsx src/delete-chats.ts",
31
31
  "test": "npm run test:mock && npm run test:live",
32
- "test:mock": "tsx --test --test-concurrency=1 --env-file=.env.test src/tests/account-concurrency.test.ts src/tests/concurrency-latest-wins.test.ts src/tests/mutex.test.ts src/tests/context-truncation.test.ts src/tests/streaming-thinking-summary.test.ts src/tests/tool-call-guard.test.ts src/tests/utils-logger-full.test.ts src/tests/logger-toolcall-errors.test.ts src/tests/logger-toolcall-disabled.test.ts src/tests/utils-json-full.test.ts src/tests/utils-session-guard-full.test.ts src/tests/core-logic-full.test.ts src/tests/core-logic-accounts.test.ts src/tests/chat-validation-full.test.ts src/tests/chat-logic-full.test.ts src/tests/account-stickiness.test.ts src/tests/accounts-security.test.ts src/tests/advanced.test.ts src/tests/auth-playwright.test.ts src/tests/auth-session-persistence.test.ts src/tests/captcha-solver.test.ts src/tests/cache-scan-regex.test.ts src/tests/chat-cleanup.test.ts src/tests/chat-cleanup-auto.test.ts src/tests/chromium-args.test.ts src/tests/config-current.test.ts src/tests/context-meter.test.ts src/tests/concurrency.test.ts src/tests/delta.test.ts src/tests/fingerprint.test.ts src/tests/human-behavior.test.ts src/tests/index.test.ts src/tests/json-recovery.test.ts src/tests/jsonFix.test.ts src/tests/memory-cache.test.ts src/tests/model-alias.test.ts src/tests/model-registry-thinking.test.ts src/tests/models-endpoint.test.ts src/tests/multimodal.test.ts src/tests/oss-upload.test.ts src/tests/parser.test.ts src/tests/personalization-flow.test.ts src/tests/personalization-required.test.ts src/tests/retry-policy.test.ts src/tests/rotation.test.ts src/tests/server-lifecycle.test.ts src/tests/session-keeper.test.ts src/tests/session-state.test.ts src/tests/single-account-mode.test.ts src/tests/stream-retry.test.ts src/tests/tool-calls-endpoint.test.ts src/tests/tool-instructions-cache.test.ts src/tests/session-id.test.ts src/tests/watchdog-ram.test.ts src/tests/tool-recovery-t1t2.test.ts src/tests/abort-silent-t4.test.ts src/tests/t3-close-tag-escape.test.ts src/tests/t5-tool-cap.test.ts src/tests/logger-redaction.test.ts src/tests/stall-timeout-retryable.test.ts src/tests/t6-missing-quote-array-close.test.ts src/tests/watchdog-rss.test.ts src/tests/logical-thread-batch.test.ts src/tests/chat-pool-style.test.ts src/tests/queue-wait-cap.test.ts src/tests/context-eviction-order.test.ts src/tests/chat-in-progress-settle.test.ts src/tests/stream-lock-leak.test.ts src/tests/stream-semaphore.test.ts src/tests/waf-isolation.test.ts src/tests/legacy-completions.test.ts src/tests/response-format.test.ts src/tests/cors-headers.test.ts src/tests/local-auth.test.ts src/tests/safe-remote-url.test.ts src/tests/stream-lock-terminal-cancel.test.ts src/tests/parser-truncated-tool-call.test.ts src/tests/metrics-prometheus.test.ts src/tests/context-death-cleanup.test.ts src/tests/personalization-direct-fetch.test.ts src/tests/completion-headers-parity.test.ts src/tests/parallel-escape-repro.test.ts src/tests/chat-in-progress-scale-cost.test.ts src/tests/temp-chat-mode.test.ts src/tests/profile-reset.test.ts src/tests/error-classification.test.ts src/tests/quota-cooldown.test.ts src/tests/account-ready-gate.test.ts src/tests/tool-cap-notice.test.ts src/tests/interaction-flows.test.ts src/tests/headers-gate-cooldown-fallback.test.ts src/tests/unwrapped-tool-calls.test.ts src/tests/anthropic.test.ts src/tests/sync-clients.test.ts src/tests/sync-top10-clients.test.ts src/tests/cli.test.ts src/tests/paths.test.ts src/tests/tool-manifest.test.ts src/tests/tui.test.ts src/tests/tui-settings.test.ts src/tests/personalization-deadline.test.ts src/tests/update-cli.test.ts src/tests/batch-accounts.test.ts src/tests/shared-browser.test.ts src/tests/auth-credential-classification.test.ts src/tests/responses-api.test.ts src/tests/media-generation.test.ts src/tests/infer-tool-name.test.ts src/tests/native-session-headers.test.ts src/tests/omni-thinking.test.ts src/tests/protect-stuck-mutex.test.ts src/tests/stream-error-notice.test.ts src/tests/parser-deep-coverage.test.ts src/tests/tui-logs-copy.test.ts src/tests/auth-jwt-and-validation.test.ts src/tests/tui-port-and-key-modal.test.ts",
33
- "test:mock:fast": "tsx --test --test-concurrency=2 --env-file=.env.test src/tests/account-concurrency.test.ts src/tests/concurrency-latest-wins.test.ts src/tests/mutex.test.ts src/tests/context-truncation.test.ts src/tests/streaming-thinking-summary.test.ts src/tests/tool-call-guard.test.ts src/tests/utils-logger-full.test.ts src/tests/logger-toolcall-errors.test.ts src/tests/logger-toolcall-disabled.test.ts src/tests/utils-json-full.test.ts src/tests/utils-session-guard-full.test.ts src/tests/core-logic-full.test.ts src/tests/core-logic-accounts.test.ts src/tests/chat-validation-full.test.ts src/tests/chat-logic-full.test.ts src/tests/account-stickiness.test.ts src/tests/accounts-security.test.ts src/tests/advanced.test.ts src/tests/auth-playwright.test.ts src/tests/auth-session-persistence.test.ts src/tests/captcha-solver.test.ts src/tests/cache-scan-regex.test.ts src/tests/chat-cleanup.test.ts src/tests/chat-cleanup-auto.test.ts src/tests/chromium-args.test.ts src/tests/config-current.test.ts src/tests/context-meter.test.ts src/tests/concurrency.test.ts src/tests/delta.test.ts src/tests/fingerprint.test.ts src/tests/human-behavior.test.ts src/tests/index.test.ts src/tests/json-recovery.test.ts src/tests/jsonFix.test.ts src/tests/memory-cache.test.ts src/tests/model-alias.test.ts src/tests/model-registry-thinking.test.ts src/tests/models-endpoint.test.ts src/tests/multimodal.test.ts src/tests/oss-upload.test.ts src/tests/parser.test.ts src/tests/personalization-flow.test.ts src/tests/personalization-required.test.ts src/tests/retry-policy.test.ts src/tests/rotation.test.ts src/tests/server-lifecycle.test.ts src/tests/session-keeper.test.ts src/tests/session-state.test.ts src/tests/single-account-mode.test.ts src/tests/stream-retry.test.ts src/tests/tool-calls-endpoint.test.ts src/tests/tool-instructions-cache.test.ts src/tests/session-id.test.ts src/tests/watchdog-ram.test.ts src/tests/tool-recovery-t1t2.test.ts src/tests/abort-silent-t4.test.ts src/tests/t3-close-tag-escape.test.ts src/tests/t5-tool-cap.test.ts src/tests/logger-redaction.test.ts src/tests/stall-timeout-retryable.test.ts src/tests/t6-missing-quote-array-close.test.ts src/tests/watchdog-rss.test.ts src/tests/logical-thread-batch.test.ts src/tests/chat-pool-style.test.ts src/tests/queue-wait-cap.test.ts src/tests/context-eviction-order.test.ts src/tests/chat-in-progress-settle.test.ts src/tests/stream-lock-leak.test.ts src/tests/stream-semaphore.test.ts src/tests/waf-isolation.test.ts src/tests/legacy-completions.test.ts src/tests/response-format.test.ts src/tests/cors-headers.test.ts src/tests/local-auth.test.ts src/tests/safe-remote-url.test.ts src/tests/stream-lock-terminal-cancel.test.ts src/tests/parser-truncated-tool-call.test.ts src/tests/metrics-prometheus.test.ts src/tests/context-death-cleanup.test.ts src/tests/personalization-direct-fetch.test.ts src/tests/completion-headers-parity.test.ts src/tests/parallel-escape-repro.test.ts src/tests/chat-in-progress-scale-cost.test.ts src/tests/temp-chat-mode.test.ts src/tests/profile-reset.test.ts src/tests/error-classification.test.ts src/tests/quota-cooldown.test.ts src/tests/account-ready-gate.test.ts src/tests/tool-cap-notice.test.ts src/tests/interaction-flows.test.ts src/tests/headers-gate-cooldown-fallback.test.ts src/tests/unwrapped-tool-calls.test.ts src/tests/anthropic.test.ts src/tests/sync-clients.test.ts src/tests/sync-top10-clients.test.ts src/tests/cli.test.ts src/tests/paths.test.ts src/tests/tool-manifest.test.ts src/tests/tui.test.ts src/tests/tui-settings.test.ts src/tests/personalization-deadline.test.ts src/tests/update-cli.test.ts src/tests/batch-accounts.test.ts src/tests/shared-browser.test.ts src/tests/auth-credential-classification.test.ts src/tests/responses-api.test.ts src/tests/media-generation.test.ts src/tests/infer-tool-name.test.ts src/tests/native-session-headers.test.ts src/tests/omni-thinking.test.ts src/tests/protect-stuck-mutex.test.ts src/tests/stream-error-notice.test.ts src/tests/parser-deep-coverage.test.ts src/tests/tui-logs-copy.test.ts src/tests/auth-jwt-and-validation.test.ts src/tests/tui-port-and-key-modal.test.ts",
32
+ "test:mock": "tsx --test --test-concurrency=1 --env-file=.env.test src/tests/account-concurrency.test.ts src/tests/concurrency-latest-wins.test.ts src/tests/mutex.test.ts src/tests/context-truncation.test.ts src/tests/streaming-thinking-summary.test.ts src/tests/tool-call-guard.test.ts src/tests/utils-logger-full.test.ts src/tests/logger-toolcall-errors.test.ts src/tests/logger-toolcall-disabled.test.ts src/tests/utils-json-full.test.ts src/tests/utils-session-guard-full.test.ts src/tests/core-logic-full.test.ts src/tests/core-logic-accounts.test.ts src/tests/chat-validation-full.test.ts src/tests/chat-logic-full.test.ts src/tests/account-stickiness.test.ts src/tests/accounts-security.test.ts src/tests/advanced.test.ts src/tests/auth-playwright.test.ts src/tests/auth-session-persistence.test.ts src/tests/captcha-solver.test.ts src/tests/cache-scan-regex.test.ts src/tests/chat-cleanup.test.ts src/tests/chat-cleanup-auto.test.ts src/tests/chromium-args.test.ts src/tests/config-current.test.ts src/tests/context-meter.test.ts src/tests/concurrency.test.ts src/tests/delta.test.ts src/tests/fingerprint.test.ts src/tests/human-behavior.test.ts src/tests/index.test.ts src/tests/json-recovery.test.ts src/tests/jsonFix.test.ts src/tests/memory-cache.test.ts src/tests/model-alias.test.ts src/tests/model-registry-thinking.test.ts src/tests/models-endpoint.test.ts src/tests/multimodal.test.ts src/tests/oss-upload.test.ts src/tests/parser.test.ts src/tests/personalization-flow.test.ts src/tests/personalization-required.test.ts src/tests/retry-policy.test.ts src/tests/rotation.test.ts src/tests/server-lifecycle.test.ts src/tests/session-keeper.test.ts src/tests/session-state.test.ts src/tests/single-account-mode.test.ts src/tests/stream-retry.test.ts src/tests/tool-calls-endpoint.test.ts src/tests/tool-instructions-cache.test.ts src/tests/session-id.test.ts src/tests/watchdog-ram.test.ts src/tests/tool-recovery-t1t2.test.ts src/tests/abort-silent-t4.test.ts src/tests/t3-close-tag-escape.test.ts src/tests/t5-tool-cap.test.ts src/tests/logger-redaction.test.ts src/tests/stall-timeout-retryable.test.ts src/tests/t6-missing-quote-array-close.test.ts src/tests/watchdog-rss.test.ts src/tests/logical-thread-batch.test.ts src/tests/chat-pool-style.test.ts src/tests/queue-wait-cap.test.ts src/tests/context-eviction-order.test.ts src/tests/chat-in-progress-settle.test.ts src/tests/stream-lock-leak.test.ts src/tests/stream-semaphore.test.ts src/tests/waf-isolation.test.ts src/tests/legacy-completions.test.ts src/tests/response-format.test.ts src/tests/cors-headers.test.ts src/tests/local-auth.test.ts src/tests/safe-remote-url.test.ts src/tests/stream-lock-terminal-cancel.test.ts src/tests/parser-truncated-tool-call.test.ts src/tests/metrics-prometheus.test.ts src/tests/context-death-cleanup.test.ts src/tests/personalization-direct-fetch.test.ts src/tests/completion-headers-parity.test.ts src/tests/parallel-escape-repro.test.ts src/tests/chat-in-progress-scale-cost.test.ts src/tests/temp-chat-mode.test.ts src/tests/profile-reset.test.ts src/tests/error-classification.test.ts src/tests/quota-cooldown.test.ts src/tests/account-ready-gate.test.ts src/tests/tool-cap-notice.test.ts src/tests/interaction-flows.test.ts src/tests/headers-gate-cooldown-fallback.test.ts src/tests/unwrapped-tool-calls.test.ts src/tests/anthropic.test.ts src/tests/sync-clients.test.ts src/tests/sync-top10-clients.test.ts src/tests/cli.test.ts src/tests/paths.test.ts src/tests/tool-manifest.test.ts src/tests/tui.test.ts src/tests/tui-settings.test.ts src/tests/personalization-deadline.test.ts src/tests/update-cli.test.ts src/tests/batch-accounts.test.ts src/tests/shared-browser.test.ts src/tests/auth-credential-classification.test.ts src/tests/responses-api.test.ts src/tests/media-generation.test.ts src/tests/infer-tool-name.test.ts src/tests/native-session-headers.test.ts src/tests/omni-thinking.test.ts src/tests/protect-stuck-mutex.test.ts src/tests/stream-error-notice.test.ts src/tests/parser-deep-coverage.test.ts src/tests/tui-logs-copy.test.ts src/tests/auth-jwt-and-validation.test.ts src/tests/tui-port-and-key-modal.test.ts src/tests/verify-zero-fake-cooldown.test.ts",
33
+ "test:mock:fast": "tsx --test --test-concurrency=2 --env-file=.env.test src/tests/account-concurrency.test.ts src/tests/concurrency-latest-wins.test.ts src/tests/mutex.test.ts src/tests/context-truncation.test.ts src/tests/streaming-thinking-summary.test.ts src/tests/tool-call-guard.test.ts src/tests/utils-logger-full.test.ts src/tests/logger-toolcall-errors.test.ts src/tests/logger-toolcall-disabled.test.ts src/tests/utils-json-full.test.ts src/tests/utils-session-guard-full.test.ts src/tests/core-logic-full.test.ts src/tests/core-logic-accounts.test.ts src/tests/chat-validation-full.test.ts src/tests/chat-logic-full.test.ts src/tests/account-stickiness.test.ts src/tests/accounts-security.test.ts src/tests/advanced.test.ts src/tests/auth-playwright.test.ts src/tests/auth-session-persistence.test.ts src/tests/captcha-solver.test.ts src/tests/cache-scan-regex.test.ts src/tests/chat-cleanup.test.ts src/tests/chat-cleanup-auto.test.ts src/tests/chromium-args.test.ts src/tests/config-current.test.ts src/tests/context-meter.test.ts src/tests/concurrency.test.ts src/tests/delta.test.ts src/tests/fingerprint.test.ts src/tests/human-behavior.test.ts src/tests/index.test.ts src/tests/json-recovery.test.ts src/tests/jsonFix.test.ts src/tests/memory-cache.test.ts src/tests/model-alias.test.ts src/tests/model-registry-thinking.test.ts src/tests/models-endpoint.test.ts src/tests/multimodal.test.ts src/tests/oss-upload.test.ts src/tests/parser.test.ts src/tests/personalization-flow.test.ts src/tests/personalization-required.test.ts src/tests/retry-policy.test.ts src/tests/rotation.test.ts src/tests/server-lifecycle.test.ts src/tests/session-keeper.test.ts src/tests/session-state.test.ts src/tests/single-account-mode.test.ts src/tests/stream-retry.test.ts src/tests/tool-calls-endpoint.test.ts src/tests/tool-instructions-cache.test.ts src/tests/session-id.test.ts src/tests/watchdog-ram.test.ts src/tests/tool-recovery-t1t2.test.ts src/tests/abort-silent-t4.test.ts src/tests/t3-close-tag-escape.test.ts src/tests/t5-tool-cap.test.ts src/tests/logger-redaction.test.ts src/tests/stall-timeout-retryable.test.ts src/tests/t6-missing-quote-array-close.test.ts src/tests/watchdog-rss.test.ts src/tests/logical-thread-batch.test.ts src/tests/chat-pool-style.test.ts src/tests/queue-wait-cap.test.ts src/tests/context-eviction-order.test.ts src/tests/chat-in-progress-settle.test.ts src/tests/stream-lock-leak.test.ts src/tests/stream-semaphore.test.ts src/tests/waf-isolation.test.ts src/tests/legacy-completions.test.ts src/tests/response-format.test.ts src/tests/cors-headers.test.ts src/tests/local-auth.test.ts src/tests/safe-remote-url.test.ts src/tests/stream-lock-terminal-cancel.test.ts src/tests/parser-truncated-tool-call.test.ts src/tests/metrics-prometheus.test.ts src/tests/context-death-cleanup.test.ts src/tests/personalization-direct-fetch.test.ts src/tests/completion-headers-parity.test.ts src/tests/parallel-escape-repro.test.ts src/tests/chat-in-progress-scale-cost.test.ts src/tests/temp-chat-mode.test.ts src/tests/profile-reset.test.ts src/tests/error-classification.test.ts src/tests/quota-cooldown.test.ts src/tests/account-ready-gate.test.ts src/tests/tool-cap-notice.test.ts src/tests/interaction-flows.test.ts src/tests/headers-gate-cooldown-fallback.test.ts src/tests/unwrapped-tool-calls.test.ts src/tests/anthropic.test.ts src/tests/sync-clients.test.ts src/tests/sync-top10-clients.test.ts src/tests/cli.test.ts src/tests/paths.test.ts src/tests/tool-manifest.test.ts src/tests/tui.test.ts src/tests/tui-settings.test.ts src/tests/personalization-deadline.test.ts src/tests/update-cli.test.ts src/tests/batch-accounts.test.ts src/tests/shared-browser.test.ts src/tests/auth-credential-classification.test.ts src/tests/responses-api.test.ts src/tests/media-generation.test.ts src/tests/infer-tool-name.test.ts src/tests/native-session-headers.test.ts src/tests/omni-thinking.test.ts src/tests/protect-stuck-mutex.test.ts src/tests/stream-error-notice.test.ts src/tests/parser-deep-coverage.test.ts src/tests/tui-logs-copy.test.ts src/tests/auth-jwt-and-validation.test.ts src/tests/tui-port-and-key-modal.test.ts src/tests/verify-zero-fake-cooldown.test.ts",
34
34
  "test:live": "tsx --test src/tests/concurrentChat.test.ts src/tests/agenticStress.test.ts",
35
35
  "test:stress": "npx tsx src/benchmarks/stress-test.ts",
36
36
  "test:stress:personalization": "npx tsx src/benchmarks/stress-test-personalization.ts",
@@ -50,7 +50,16 @@ export function markAccountRateLimited(
50
50
  reason?: string,
51
51
  options: { silent?: boolean } = {},
52
52
  ): void {
53
+ // If cooldown is explicitly 0 or negative, do not park the account.
54
+ if (cooldownMs !== undefined && cooldownMs <= 0) {
55
+ clearAccountCooldown(accountId);
56
+ return;
57
+ }
53
58
  const duration = cooldownMs ?? defaultCooldownDurationMs();
59
+ if (duration <= 0) {
60
+ clearAccountCooldown(accountId);
61
+ return;
62
+ }
54
63
  const until = Date.now() + duration;
55
64
  const cooldownReason = reason ?? "RateLimited";
56
65
 
@@ -11,6 +11,7 @@ export interface ServerLogMessage {
11
11
  time: string;
12
12
  level: "INFO" | "WARN" | "ERROR";
13
13
  message: string;
14
+ _ts?: number;
14
15
  }
15
16
 
16
17
  const MAX_LOG_HISTORY = 2000;
@@ -80,15 +81,21 @@ export function recordServerLog(level: "INFO" | "WARN" | "ERROR", text: string):
80
81
  .replace(/([\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}\u{2300}-\u{23FF}]\uFE0F?)\s{2,}/gu, "$1 ");
81
82
  if (!line) continue;
82
83
 
83
- // Prevent identical duplicate logs within the same second window
84
+ // Prevent identical duplicate logs within the same second or 1.5s window
85
+ const now = Date.now();
84
86
  const isDuplicate = logHistory
85
87
  .slice(-10)
86
- .some((entry) => entry.time === time && entry.level === level && entry.message === line);
88
+ .some(
89
+ (entry) =>
90
+ (entry.time === time || (entry._ts && now - entry._ts < 1500)) &&
91
+ entry.level === level &&
92
+ entry.message === line,
93
+ );
87
94
  if (isDuplicate) {
88
95
  continue;
89
96
  }
90
97
 
91
- const entry: ServerLogMessage = { time, level, message: line };
98
+ const entry: ServerLogMessage = { time, level, message: line, _ts: now };
92
99
  logHistory.push(entry);
93
100
  if (logHistory.length > MAX_LOG_HISTORY) {
94
101
  logHistory.shift();
@@ -17,6 +17,6 @@ export function getAppVersion(): string {
17
17
  }
18
18
  }
19
19
  } catch {}
20
- cachedVersion = "v1.4.8";
20
+ cachedVersion = "v1.5.1";
21
21
  return cachedVersion;
22
22
  }
@@ -1681,15 +1681,27 @@ async function tryCreateStreamWithRetry(
1681
1681
  }
1682
1682
 
1683
1683
  if (policy.reason === "account_initialization_failed") {
1684
- console.warn(
1685
- `⚠️ [Chat] Account initialization failed | ${currentAccountEmail} | cooldown=${Math.round((policy.accountCooldownMs ?? 0) / 1000)}s`,
1686
- );
1687
- markAccountFailed(currentAccountId);
1688
- markAccountRateLimited(
1689
- currentAccountId,
1690
- policy.accountCooldownMs,
1691
- policy.accountCooldownReason,
1692
- );
1684
+ const cooldownMs = policy.accountCooldownMs ?? 0;
1685
+ if (cooldownMs > 0) {
1686
+ console.warn(
1687
+ `⚠️ [Chat] Account initialization failed | ${currentAccountEmail} | cooldown=${Math.round(cooldownMs / 1000)}s`,
1688
+ );
1689
+ markAccountFailed(currentAccountId);
1690
+ markAccountRateLimited(
1691
+ currentAccountId,
1692
+ cooldownMs,
1693
+ policy.accountCooldownReason,
1694
+ );
1695
+ } else {
1696
+ console.warn(
1697
+ `⚠️ [Chat] Account auth expired/retryable | ${currentAccountEmail} | rotating without cooldown`,
1698
+ );
1699
+ if (policy.accountCooldownReason === "AuthExpired") {
1700
+ import("../../services/playwright.ts")
1701
+ .then(({ refreshAccountToken }) => refreshAccountToken(currentAccountId))
1702
+ .catch(() => {});
1703
+ }
1704
+ }
1693
1705
  return { success: false, error: err };
1694
1706
  }
1695
1707
 
@@ -1712,7 +1724,7 @@ async function tryCreateStreamWithRetry(
1712
1724
  console.warn(
1713
1725
  `🔄 [Chat] Switching account after ${policy.reason} | ${currentAccountEmail} -> ${maskEmail(nextAccount.email)}`,
1714
1726
  );
1715
- if (policy.accountCooldownMs || policy.accountCooldownReason) {
1727
+ if ((policy.accountCooldownMs ?? 0) > 0) {
1716
1728
  markAccountRateLimited(
1717
1729
  currentAccountId,
1718
1730
  policy.accountCooldownMs,
@@ -113,13 +113,41 @@ export function formatThinkingSummaryContent(delta: any): string {
113
113
  return sections.join("\n\n");
114
114
  }
115
115
 
116
+ /**
117
+ * Verified answer/content phases from Qwen Web:
118
+ * - "answer": standard text completions
119
+ * - "deep_research_answer", "ReportGeneration", "PdfMdGen": deep research & search modes
120
+ * - "image", "image_generation", "image_gen", "t2i": multimodal/image phases
121
+ */
122
+ export const ANSWER_PHASES = new Set([
123
+ "answer",
124
+ "deep_research_answer",
125
+ "ReportGeneration",
126
+ "PdfMdGen",
127
+ "image",
128
+ "image_generation",
129
+ "image_gen",
130
+ "t2i",
131
+ ]);
132
+
133
+ export function isAnswerPhase(phase: unknown): boolean {
134
+ return typeof phase === "string" && ANSWER_PHASES.has(phase);
135
+ }
136
+
116
137
  /**
117
138
  * Matches thinking phases verified in upstream Qwen Web HAR:
118
139
  * - "thinking_summary": structured thinking in Qwen Max / Plus
119
140
  * - "think": incremental thinking deltas in Qwen Omni
141
+ * - "DeepThinking": deep thinking models
120
142
  */
143
+ export const THINKING_PHASES = new Set([
144
+ "think",
145
+ "thinking_summary",
146
+ "DeepThinking",
147
+ ]);
148
+
121
149
  export function isThinkingPhase(phase: unknown): boolean {
122
- return phase === "think" || phase === "thinking_summary";
150
+ return typeof phase === "string" && THINKING_PHASES.has(phase);
123
151
  }
124
152
 
125
153
  /**
@@ -486,14 +486,14 @@ export function classifyRetryAction(
486
486
  switchAccount: true,
487
487
  forceNewChat: true,
488
488
  retryAfterMs: baseDelayMs,
489
- accountCooldownMs: config.concurrency.initFailureCooldownMs,
489
+ accountCooldownMs: 0,
490
490
  accountCooldownReason: "PersonalizationFailed",
491
491
  });
492
492
  }
493
493
 
494
494
  // Upstream 401 / Unauthorized on chat creation or API requests:
495
- // Account session is invalid or expired. Cool down account with AuthInitFailed so the
496
- // proxy rotates to a valid account instead of looping endlessly on 503s.
495
+ // Access token expired (15m window). Rotate without locking into a 300s cooldown
496
+ // so the account can immediately be renewed via refresh_token.
497
497
  if (
498
498
  code === "createchatinvalidresponse" ||
499
499
  code === "createchatfailed" ||
@@ -510,8 +510,8 @@ export function classifyRetryAction(
510
510
  forceNewChat: true,
511
511
  retryWithFullPrompt: true,
512
512
  retryAfterMs: Math.min(baseDelayMs, 1_000),
513
- accountCooldownMs: config.concurrency.initFailureCooldownMs,
514
- accountCooldownReason: "AuthInitFailed",
513
+ accountCooldownMs: 0,
514
+ accountCooldownReason: "AuthExpired",
515
515
  });
516
516
  }
517
517
  // Specialized recoveries first (even if wrapped as RetryableQwenStreamError)
@@ -68,6 +68,7 @@ import {
68
68
  import {
69
69
  getIncrementalDelta,
70
70
  isThinkingPhase,
71
+ isAnswerPhase,
71
72
  extractThinkingContent,
72
73
  formatThinkingSummaryContent,
73
74
  shouldSuppressStreamAbort,
@@ -470,7 +471,7 @@ export async function processNonStreamingResponse(
470
471
  foundStr = true;
471
472
  }
472
473
  }
473
- } else if (delta.phase === "answer") {
474
+ } else if (isAnswerPhase(delta.phase)) {
474
475
  isThinkingChunk = false;
475
476
  if (delta.content !== undefined) {
476
477
  const newContent = delta.content || "";
@@ -1436,7 +1437,7 @@ export async function processStreamingResponse(
1436
1437
 
1437
1438
  if (
1438
1439
  switchAccount &&
1439
- (policy.accountCooldownMs || policy.accountCooldownReason)
1440
+ (policy.accountCooldownMs ?? 0) > 0
1440
1441
  ) {
1441
1442
  markAccountRateLimited(
1442
1443
  currentAccountId,
@@ -1445,6 +1446,12 @@ export async function processStreamingResponse(
1445
1446
  );
1446
1447
  }
1447
1448
 
1449
+ if (policy.accountCooldownReason === "AuthExpired") {
1450
+ import("../../services/playwright.ts")
1451
+ .then(({ refreshAccountToken }) => refreshAccountToken(currentAccountId))
1452
+ .catch(() => {});
1453
+ }
1454
+
1448
1455
  retryContext.releaseAccountLease?.();
1449
1456
  retryContext.releaseAccountLease = null;
1450
1457
  await reader.cancel().catch(() => undefined);
@@ -1776,7 +1783,7 @@ export async function processStreamingResponse(
1776
1783
  // "phase":"answer"} delta and NO trailing [DONE]. Treat it as
1777
1784
  // the terminal event so we don't wait on the keep-alive
1778
1785
  // connection to close (up to the 60s/10min idle timeout).
1779
- if (delta.phase === "answer" && delta.status === "finished") {
1786
+ if (isAnswerPhase(delta.phase) && delta.status === "finished") {
1780
1787
  upstreamDone = true;
1781
1788
  if (!clientDisconnected) flushWrites();
1782
1789
  break; // Exit the for loop; the while check leaves the read loop
@@ -1800,7 +1807,7 @@ export async function processStreamingResponse(
1800
1807
  foundStr = true;
1801
1808
  }
1802
1809
  }
1803
- } else if (delta.phase === "answer") {
1810
+ } else if (isAnswerPhase(delta.phase)) {
1804
1811
  isThinkingChunk = false;
1805
1812
  if (delta.content !== undefined) {
1806
1813
  const newContent = delta.content || "";
@@ -2068,7 +2075,7 @@ export async function processStreamingResponse(
2068
2075
  if (!toolParser) break;
2069
2076
  const allToolsFailed =
2070
2077
  toolParser.getMalformedToolCalls().length > 0 &&
2071
- toolParser.getEmittedToolCallCount() === 0;
2078
+ toolParser.getSuccessfulToolCallCount() === 0;
2072
2079
  if (!allToolsFailed) break;
2073
2080
 
2074
2081
  const malformedCalls = toolParser.getMalformedToolCalls();
@@ -2290,7 +2297,7 @@ export async function processStreamingResponse(
2290
2297
  // The retry stream may also terminate with an answer-finished
2291
2298
  // delta when upstream sends no [DONE]. Leave the read loop
2292
2299
  // immediately so we don't stall on the keep-alive connection.
2293
- if (delta.phase === "answer" && delta.status === "finished") {
2300
+ if (isAnswerPhase(delta.phase) && delta.status === "finished") {
2294
2301
  upstreamDone = true;
2295
2302
  break retryReadLoop;
2296
2303
  }
@@ -387,15 +387,18 @@ async function getSTSToken(
387
387
  }
388
388
  data = await response.json().catch(() => null);
389
389
  }
390
+ const codeLower = (data?.data?.code || data?.code || "").toLowerCase();
391
+ const detailsLower = (typeof data?.data?.details === "string" ? data.data.details : typeof data?.message === "string" ? data.message : "").toLowerCase();
390
392
  const is401 =
391
393
  data?.success === false &&
392
- (data?.data?.code === "Unauthorized" ||
393
- (typeof data?.data?.details === "string" &&
394
- data.data.details.includes("401")));
394
+ (codeLower === "unauthorized" ||
395
+ detailsLower.includes("401") ||
396
+ detailsLower.includes("token has expired") ||
397
+ detailsLower.includes("unauthorized"));
395
398
 
396
399
  if (is401) {
397
400
  try {
398
- const { refreshHeaders } = await import("../services/playwright.ts");
401
+ const { refreshAccountToken } = await import("../services/playwright.ts");
399
402
  const { getBasicHeaders } = await import("../services/auth-playwright.ts");
400
403
  const { loadAccounts } = await import("../core/accounts.ts");
401
404
  const resolvedId = accountId ?? loadAccounts()[0]?.id;
@@ -403,16 +406,18 @@ async function getSTSToken(
403
406
  console.warn(
404
407
  `[Upload] STS token 401 — refreshing session with re-auth and retrying...`,
405
408
  );
406
- await refreshHeaders(resolvedId, undefined, true);
407
- const fresh = await getBasicHeaders(resolvedId);
408
- headers.cookie = fresh.cookie;
409
- headers["user-agent"] = fresh.userAgent;
410
- headers["bx-v"] = fresh.bxV;
411
- if (fresh.bxUa) headers["bx-ua"] = fresh.bxUa;
412
- if (fresh.bxUmidtoken) headers["bx-umidtoken"] = fresh.bxUmidtoken;
413
- const retryRes = await doFetch(headers);
414
- if (retryRes.ok) {
415
- data = await retryRes.json().catch(() => null);
409
+ const refreshResult = await refreshAccountToken(resolvedId);
410
+ if (refreshResult.success) {
411
+ const fresh = await getBasicHeaders(resolvedId);
412
+ headers.cookie = fresh.cookie;
413
+ headers["user-agent"] = fresh.userAgent;
414
+ headers["bx-v"] = fresh.bxV;
415
+ if (fresh.bxUa) headers["bx-ua"] = fresh.bxUa;
416
+ if (fresh.bxUmidtoken) headers["bx-umidtoken"] = fresh.bxUmidtoken;
417
+ const retryRes = await doFetch(headers);
418
+ if (retryRes.ok) {
419
+ data = await retryRes.json().catch(() => null);
420
+ }
416
421
  }
417
422
  }
418
423
  } catch (refreshErr) {