qwenproxy-cli 1.6.1 → 1.6.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.
@@ -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";
@@ -781,7 +826,7 @@ function addAccountsBatch(entries) {
781
826
  }
782
827
  return { added, skipped, invalid };
783
828
  }
784
- function addAccount(email, password, id) {
829
+ function addAccount(email, password = "", id) {
785
830
  if (!email || typeof email !== "string" || email.trim().length === 0) {
786
831
  throw new Error("Email is required");
787
832
  }
@@ -3226,6 +3271,10 @@ async function clearVisibleChallenge(page) {
3226
3271
  "button[aria-label*='Close' i]",
3227
3272
  "button[aria-label*='Fechar' i]",
3228
3273
  ".index-module__whats-new-modal-close",
3274
+ ".age-confirmation-modal button",
3275
+ ".age-confirmation-modal .qwen-chat-v2-btn-black",
3276
+ "button:has-text('Continuar')",
3277
+ "button:has-text('Continue')",
3229
3278
  "button:has-text('Cancel')",
3230
3279
  "button:has-text('Cancelar')",
3231
3280
  "button:has-text('I understand')",
@@ -4729,7 +4778,24 @@ async function refreshAccountToken(accountId) {
4729
4778
  return { success: false, error: "Could not refresh or re-authenticate token" };
4730
4779
  }
4731
4780
  async function captureQwenHeaders(accountId, pageOverride, timeoutMs = config.timeouts.headers, triggerGraceMs = HEADER_CAPTURE_TRIGGER_GRACE_MS) {
4732
- const page = pageOverride ?? accountPages.get(accountId);
4781
+ let page = pageOverride ?? accountPages.get(accountId);
4782
+ if (!page || page.isClosed()) {
4783
+ const { isRunningUnderNodeTest: isRunningUnderNodeTest3 } = await Promise.resolve().then(() => (init_paths(), paths_exports));
4784
+ if (!isRunningUnderNodeTest3()) {
4785
+ const { getAccountCredentials: getAccountCredentials2 } = await Promise.resolve().then(() => (init_accounts(), accounts_exports));
4786
+ const creds = getAccountCredentials2(accountId);
4787
+ if (creds) {
4788
+ await initPlaywrightForAccount(
4789
+ creds,
4790
+ config.playwright.headless,
4791
+ config.playwright.browser,
4792
+ { skipHeaderCapture: true }
4793
+ ).catch(() => {
4794
+ });
4795
+ page = accountPages.get(accountId);
4796
+ }
4797
+ }
4798
+ }
4733
4799
  if (!page || page.isClosed()) {
4734
4800
  throw new Error(`Playwright page unavailable for header capture: ${accountId}`);
4735
4801
  }
@@ -5430,8 +5496,10 @@ async function evictIdlePlaywrightContextsToLimit() {
5430
5496
  if (accountPages.size <= max) return 0;
5431
5497
  const candidates = priorityOrderForEviction(
5432
5498
  Array.from(accountPages.keys()).filter((accountId) => {
5499
+ if (isAccountServingStream(accountId)) return false;
5500
+ if (isAccountRecentlyActive(accountId, 3e4)) return false;
5433
5501
  const mutex = accountMutexes.get(accountId);
5434
- return mutex?.isIdle() && !isAccountServingStream(accountId);
5502
+ return mutex?.isIdle();
5435
5503
  })
5436
5504
  ).map((accountId) => ({
5437
5505
  accountId,
@@ -5544,6 +5612,10 @@ async function closePlaywrightForAccount(accountId) {
5544
5612
  release();
5545
5613
  }
5546
5614
  }
5615
+ function isAccountRecentlyActive(accountId, maxIdleMs = 3e5) {
5616
+ const last = lastAccountActivity.get(accountId) ?? 0;
5617
+ return last > 0 && Date.now() - last < maxIdleMs;
5618
+ }
5547
5619
  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;
5548
5620
  var init_playwright = __esm({
5549
5621
  "src/services/playwright.ts"() {
@@ -5585,8 +5657,8 @@ var init_playwright = __esm({
5585
5657
  PLAYWRIGHT_MUTEX_WAIT_MS = 6e4;
5586
5658
  ACCOUNT_PAGE_OPERATION_TIMEOUT_MS = config.timeouts.page;
5587
5659
  SESSION_PROBE_NAVIGATION_TIMEOUT_MS = 15e3;
5588
- HEADER_CAPTURE_TRIGGER_GRACE_MS = 15e3;
5589
- FIRST_TRIGGER_GRACE_MS = 8e3;
5660
+ HEADER_CAPTURE_TRIGGER_GRACE_MS = 2e4;
5661
+ FIRST_TRIGGER_GRACE_MS = 12e3;
5590
5662
  HEADER_CAPTURE_TRIGGER_ATTEMPTS = 3;
5591
5663
  CHAT_INPUT_APPEAR_TIMEOUT_MS = 15e3;
5592
5664
  CHAT_INPUT_ACTION_TIMEOUT_MS = 1e4;
@@ -5702,6 +5774,15 @@ var init_helpers = __esm({
5702
5774
  }
5703
5775
  });
5704
5776
 
5777
+ // src/utils/overload-detector.ts
5778
+ var OVERLOAD_COOLDOWN_MS;
5779
+ var init_overload_detector = __esm({
5780
+ "src/utils/overload-detector.ts"() {
5781
+ "use strict";
5782
+ OVERLOAD_COOLDOWN_MS = 5 * 60 * 1e3;
5783
+ }
5784
+ });
5785
+
5705
5786
  // src/routes/chat/retry-policy.ts
5706
5787
  var init_retry_policy = __esm({
5707
5788
  "src/routes/chat/retry-policy.ts"() {
@@ -5712,6 +5793,7 @@ var init_retry_policy = __esm({
5712
5793
  init_qwen();
5713
5794
  init_errors();
5714
5795
  init_helpers();
5796
+ init_overload_detector();
5715
5797
  init_account_manager();
5716
5798
  }
5717
5799
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qwenproxy-cli",
3
- "version": "1.6.1",
3
+ "version": "1.6.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": {
@@ -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 src/tests/verify-zero-fake-cooldown.test.ts src/tests/auth-session-refresh-preservation.test.ts src/tests/personalization-in-flight-dedup.test.ts src/tests/daily-usage.test.ts src/tests/resilience-enhancements.test.ts src/tests/pool-quota-failover.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 src/tests/auth-session-refresh-preservation.test.ts src/tests/personalization-in-flight-dedup.test.ts src/tests/daily-usage.test.ts src/tests/resilience-enhancements.test.ts src/tests/pool-quota-failover.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 src/tests/auth-session-refresh-preservation.test.ts src/tests/personalization-in-flight-dedup.test.ts src/tests/daily-usage.test.ts src/tests/resilience-enhancements.test.ts src/tests/pool-quota-failover.test.ts src/tests/process-guard-and-resilience.test.ts src/tests/otp-login.test.ts src/tests/overload-detector.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 src/tests/auth-session-refresh-preservation.test.ts src/tests/personalization-in-flight-dedup.test.ts src/tests/daily-usage.test.ts src/tests/resilience-enhancements.test.ts src/tests/pool-quota-failover.test.ts src/tests/process-guard-and-resilience.test.ts src/tests/otp-login.test.ts src/tests/overload-detector.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",
@@ -322,7 +322,7 @@ export function addAccountsBatch(
322
322
  }
323
323
  export function addAccount(
324
324
  email: string,
325
- password: string,
325
+ password = "",
326
326
  id?: string,
327
327
  ): QwenAccount {
328
328
  if (!email || typeof email !== "string" || email.trim().length === 0) {
@@ -17,6 +17,6 @@ export function getAppVersion(): string {
17
17
  }
18
18
  }
19
19
  } catch {}
20
- cachedVersion = "v1.6.1";
20
+ cachedVersion = "v1.6.3";
21
21
  return cachedVersion;
22
22
  }
package/src/login.ts CHANGED
@@ -50,7 +50,8 @@ async function showMenu() {
50
50
  }
51
51
 
52
52
  console.log("\nOptions:");
53
- console.log(" [A] Add account");
53
+ console.log(" [A] Add account (Password)");
54
+ console.log(" [O] Add account (Passwordless / Email OTP)");
54
55
  if (accounts.length > 0) {
55
56
  console.log(" [R] Remove an account");
56
57
  }
@@ -64,7 +65,22 @@ async function showMenu() {
64
65
  }
65
66
 
66
67
  if (choice === "A") {
67
- await addAccountFlow();
68
+ clear();
69
+ console.log("=== Add New Account ===\n");
70
+ console.log("Authentication method:");
71
+ console.log(" [1] Traditional Password (Email + Password)");
72
+ console.log(" [2] Passwordless (Email Verification Code / OTP)\n");
73
+ const method = (await askQuestion("Select method [1/2] (default 1): ")).trim();
74
+ if (method === "2") {
75
+ await addAccountOtpFlow();
76
+ } else {
77
+ await addAccountPasswordFlow();
78
+ }
79
+ continue;
80
+ }
81
+
82
+ if (choice === "O") {
83
+ await addAccountOtpFlow();
68
84
  continue;
69
85
  }
70
86
 
@@ -75,9 +91,9 @@ async function showMenu() {
75
91
  }
76
92
  }
77
93
 
78
- async function addAccountFlow() {
94
+ async function addAccountPasswordFlow() {
79
95
  clear();
80
- console.log("=== Add New Account ===\n");
96
+ console.log("=== Add New Account (Password Login) ===\n");
81
97
  const email = await askQuestion("Email: ");
82
98
  if (!email || !email.trim()) {
83
99
  console.log("Email is required.");
@@ -151,6 +167,76 @@ async function addAccountFlow() {
151
167
  await askQuestion("\nPress Enter to continue...");
152
168
  }
153
169
 
170
+ async function addAccountOtpFlow() {
171
+ clear();
172
+ console.log("=== Add New Account (Passwordless / Email OTP) ===\n");
173
+ const email = await askQuestion("Email: ");
174
+ if (!email || !email.trim()) {
175
+ console.log("Email is required.");
176
+ await askQuestion("Press Enter to continue...");
177
+ return;
178
+ }
179
+
180
+ const trimmedEmail = email.trim();
181
+ const existingAccounts = listAccounts();
182
+ if (
183
+ existingAccounts.some(
184
+ (a) => a.email.toLowerCase() === trimmedEmail.toLowerCase(),
185
+ )
186
+ ) {
187
+ console.log(
188
+ `\n❌ Error: An account with email "${trimmedEmail}" already exists.`,
189
+ );
190
+ await askQuestion("Press Enter to continue...");
191
+ return;
192
+ }
193
+
194
+ console.log(
195
+ `\n⏳ Requesting OTP verification code for ${maskEmail(trimmedEmail)}...`,
196
+ );
197
+
198
+ const { startEmailOtpLogin, verifyEmailOtpLogin, cancelEmailOtpLogin } =
199
+ await import("./services/playwright.ts");
200
+
201
+ const startRes = await startEmailOtpLogin(trimmedEmail, { headless: false });
202
+ if (!startRes.success || !startRes.sessionId) {
203
+ console.log(
204
+ `\n❌ Failed to request code: ${startRes.error || "Unknown error"}`,
205
+ );
206
+ await askQuestion("Press Enter to continue...");
207
+ return;
208
+ }
209
+
210
+ console.log(`\n✉️ Verification code sent to ${trimmedEmail}!`);
211
+ console.log("Check your email inbox (and spam folder).\n");
212
+
213
+ while (true) {
214
+ const code = await askQuestion("Enter 6-digit code (or 'C' to cancel): ");
215
+ if (!code || !code.trim()) continue;
216
+
217
+ if (code.trim().toUpperCase() === "C") {
218
+ await cancelEmailOtpLogin(startRes.sessionId);
219
+ console.log("\nLogin cancelled.");
220
+ await askQuestion("Press Enter to continue...");
221
+ return;
222
+ }
223
+
224
+ console.log("\n⏳ Verifying code and establishing 30-day session...");
225
+ const verifyRes = await verifyEmailOtpLogin(startRes.sessionId, code.trim());
226
+ if (verifyRes.success && verifyRes.account) {
227
+ console.log(
228
+ `\n✅ Account verified and saved: ${maskEmail(verifyRes.account.email)} (${verifyRes.account.id})`,
229
+ );
230
+ console.log("Passwordless 30-day session active.");
231
+ await askQuestion("\nPress Enter to continue...");
232
+ return;
233
+ } else {
234
+ console.log(`\n❌ ${verifyRes.error || "Invalid code"}`);
235
+ console.log("Please try again or enter 'C' to cancel.\n");
236
+ }
237
+ }
238
+ }
239
+
154
240
  async function removeAccountFlow() {
155
241
  const accounts = listAccounts();
156
242
  if (accounts.length === 0) return;
@@ -82,8 +82,8 @@ const MAX_ANTI_BOT_ROTATIONS = 1;
82
82
  * a stuck account page (closed context / WAF) can otherwise hold each browser
83
83
  * op for 60s and keep the personalization mutex blocked for minutes.
84
84
  */
85
- export const PERSONALIZATION_SYNC_DEADLINE_MS = 45_000;
86
- export const COLD_ACCOUNT_PERSONALIZATION_SYNC_DEADLINE_MS = 60_000;
85
+ export const PERSONALIZATION_SYNC_DEADLINE_MS = 90_000;
86
+ export const COLD_ACCOUNT_PERSONALIZATION_SYNC_DEADLINE_MS = 120_000;
87
87
 
88
88
  export function computePersonalizationDeadlineMs(
89
89
  accountId: string | undefined,
@@ -1282,8 +1282,15 @@ async function tryCreateStreamWithRetry(
1282
1282
  const { isCaptchaRecoveryActive } = await import(
1283
1283
  "../../services/captcha-coordinator.ts"
1284
1284
  );
1285
- if (currentAccountId && isCaptchaRecoveryActive(currentAccountId)) {
1286
- // Captcha solver is actively solving puzzle in background; extend deadline by 30s
1285
+ const { isPlaywrightInitializing } = await import(
1286
+ "../../services/playwright.ts"
1287
+ );
1288
+ if (
1289
+ currentAccountId &&
1290
+ (isCaptchaRecoveryActive(currentAccountId) ||
1291
+ isPlaywrightInitializing(currentAccountId))
1292
+ ) {
1293
+ // Captcha solver is actively solving puzzle or browser is launching/initializing; extend deadline by 30s
1287
1294
  acquireDeadlineTimer = setTimeout(checkDeadline, 30_000);
1288
1295
  acquireDeadlineTimer.unref?.();
1289
1296
  return;
@@ -22,6 +22,7 @@ import {
22
22
  ValidationError,
23
23
  } from "../../core/errors.ts";
24
24
  import { isAbortError } from "./helpers.ts";
25
+ import { isOverloadMessage, OVERLOAD_COOLDOWN_MS } from "../../utils/overload-detector.ts";
25
26
 
26
27
  export type RetryAction = {
27
28
  /** Outer/create-stream layer should retry this failure */
@@ -50,6 +51,9 @@ export type RetryableStreamError = RetryableQwenStreamError & {
50
51
  retryWithFullPrompt?: boolean;
51
52
  switchAccount?: boolean;
52
53
  dropFiles?: boolean;
54
+ accountCooldownMs?: number;
55
+ accountCooldownReason?: string;
56
+ reason?: string;
53
57
  };
54
58
 
55
59
  function errMessage(err: unknown): string {
@@ -231,10 +235,11 @@ export function isAccountInitializationError(err: unknown): boolean {
231
235
  }
232
236
 
233
237
  export function isQuotaLikeError(err: unknown): boolean {
234
- // Chat-not-exist / invalid attachment must never look like quota.
238
+ // Chat-not-exist / invalid attachment / explicit overload must never look like quota.
235
239
  if (isChatNotExistError(err) || isInvalidInputError(err)) return false;
236
-
237
240
  const code = errCode(err).toLowerCase();
241
+ if (code === "server_overloaded" || code === "overload") return false;
242
+
238
243
  const message = errMessage(err).toLowerCase();
239
244
 
240
245
  // Note: RetryableQwenStreamError inherits OpenAI-style code "rate_limit_exceeded".
@@ -424,6 +429,26 @@ export function isInternalServerError(err: unknown): boolean {
424
429
  );
425
430
  }
426
431
 
432
+ /**
433
+ * Detects in-band plain text overload messages from Qwen.
434
+ */
435
+ export function isOverloadError(err: unknown): boolean {
436
+ const code = errCode(err).toLowerCase();
437
+ if (
438
+ code === "quota_limit" ||
439
+ code === "ratelimited" ||
440
+ code === "rate_limit" ||
441
+ code === "rate_limit_exceeded" ||
442
+ code === "membership_limit" ||
443
+ code === "internal_error" ||
444
+ code === "internal_server_error"
445
+ ) {
446
+ return false;
447
+ }
448
+ if (code === "server_overloaded" || code === "overload") return true;
449
+ return !code && isOverloadMessage(errMessage(err));
450
+ }
451
+
427
452
  /**
428
453
  * Build a RetryAction with sane defaults so each classification branch only
429
454
  * spells out the fields it actually changes. Defaults: retryable, no account
@@ -474,6 +499,21 @@ export function classifyRetryAction(
474
499
  return makeRetryAction("terminal_local", { retryable: false });
475
500
  }
476
501
 
502
+ // Agent instructions ride ONLY the account-level personalization. An
503
+ // unconfirmed sync means this account cannot serve the request as-is —
504
+ // rotate to another account (each attempt re-syncs on its own account).
505
+ // Do NOT park the account with a 300s cooldown: rotation across accounts
506
+ // is already guarded by triedAccounts, preventing pool collapse.
507
+ if (err instanceof PersonalizationSyncError) {
508
+ return makeRetryAction("personalization_sync_failed", {
509
+ switchAccount: true,
510
+ forceNewChat: true,
511
+ retryAfterMs: baseDelayMs,
512
+ accountCooldownMs: 0,
513
+ accountCooldownReason: "PersonalizationFailed",
514
+ });
515
+ }
516
+
477
517
  const message = errMessage(err).toLowerCase();
478
518
  const code = errCode(err).toLowerCase();
479
519
  if (isAccountInitializationError(err)) {
@@ -496,21 +536,6 @@ export function classifyRetryAction(
496
536
  });
497
537
  }
498
538
 
499
- // Agent instructions ride ONLY the account-level personalization. An
500
- // unconfirmed sync means this account cannot serve the request as-is —
501
- // rotate to another account (each attempt re-syncs on its own account).
502
- // Do NOT park the account with a 300s cooldown: rotation across accounts
503
- // is already guarded by triedAccounts, preventing pool collapse.
504
- if (err instanceof PersonalizationSyncError) {
505
- return makeRetryAction("personalization_sync_failed", {
506
- switchAccount: true,
507
- forceNewChat: true,
508
- retryAfterMs: baseDelayMs,
509
- accountCooldownMs: 0,
510
- accountCooldownReason: "PersonalizationFailed",
511
- });
512
- }
513
-
514
539
  // Upstream 401 / Unauthorized on chat creation or API requests:
515
540
  // Access token expired (15m window). Rotate without locking into a 300s cooldown
516
541
  // so the account can immediately be renewed via refresh_token.
@@ -610,6 +635,18 @@ export function classifyRetryAction(
610
635
  });
611
636
  }
612
637
 
638
+ if (isOverloadError(err)) {
639
+ const typed = err as RetryableStreamError;
640
+ return makeRetryAction("server_busy", {
641
+ switchAccount: true,
642
+ forceNewChat: true,
643
+ retryWithFullPrompt: true,
644
+ retryAfterMs: typed.retryAfterMs ?? 3_000,
645
+ accountCooldownMs: OVERLOAD_COOLDOWN_MS,
646
+ accountCooldownReason: "ServerOverloaded",
647
+ });
648
+ }
649
+
613
650
  if (isInternalServerError(err) && !(err instanceof QwenUpstreamError)) {
614
651
  const typed = err as RetryableStreamError;
615
652
  return makeRetryAction("upstream_internal_error", {
@@ -654,11 +691,13 @@ export function classifyRetryAction(
654
691
  if (err instanceof RetryableQwenStreamError) {
655
692
  const typed = err as RetryableStreamError;
656
693
  // Default switch unless caller explicitly set switchAccount=false
657
- return makeRetryAction("explicit_retryable", {
694
+ return makeRetryAction(typed.reason || "explicit_retryable", {
658
695
  switchAccount: typed.switchAccount !== false,
659
696
  forceNewChat: typed.forceNewChat === true,
660
697
  retryWithFullPrompt: typed.retryWithFullPrompt === true,
661
698
  retryAfterMs: typed.retryAfterMs ?? baseDelayMs,
699
+ accountCooldownMs: typed.accountCooldownMs,
700
+ accountCooldownReason: typed.accountCooldownReason,
662
701
  });
663
702
  }
664
703
 
@@ -702,6 +741,9 @@ export function toRetryableStreamError(
702
741
  error.retryWithFullPrompt = merged.retryWithFullPrompt;
703
742
  error.switchAccount = merged.switchAccount;
704
743
  error.dropFiles = merged.dropFiles;
744
+ error.accountCooldownMs = merged.accountCooldownMs;
745
+ error.accountCooldownReason = merged.accountCooldownReason;
746
+ error.reason = merged.reason;
705
747
  return error;
706
748
  }
707
749
 
@@ -79,6 +79,7 @@ import {
79
79
  applyUpstreamUsage,
80
80
  buildUsage,
81
81
  } from "./helpers.ts";
82
+ import { isOverloadMessage, OVERLOAD_COOLDOWN_MS } from "../../utils/overload-detector.ts";
82
83
 
83
84
  function firstString(...values: unknown[]): string | null {
84
85
  for (const value of values) {
@@ -477,6 +478,19 @@ export async function processNonStreamingResponse(
477
478
  isThinkingChunk = false;
478
479
  if (delta.content !== undefined) {
479
480
  const newContent = delta.content || "";
481
+ if (isOverloadMessage(newContent)) {
482
+ throw toRetryableStreamError(
483
+ "server_overloaded",
484
+ newContent,
485
+ {
486
+ switchAccount: true,
487
+ forceNewChat: true,
488
+ reason: "server_busy",
489
+ accountCooldownMs: OVERLOAD_COOLDOWN_MS,
490
+ accountCooldownReason: "ServerOverloaded",
491
+ },
492
+ );
493
+ }
480
494
  const result = getIncrementalDelta(
481
495
  lastRawContent,
482
496
  newContent,
@@ -1875,6 +1889,19 @@ export async function processStreamingResponse(
1875
1889
  isThinkingChunk = false;
1876
1890
  if (delta.content !== undefined) {
1877
1891
  const newContent = delta.content || "";
1892
+ if (!emittedModelOutput && isOverloadMessage(newContent)) {
1893
+ throw toRetryableStreamError(
1894
+ "server_overloaded",
1895
+ newContent,
1896
+ {
1897
+ switchAccount: true,
1898
+ forceNewChat: true,
1899
+ reason: "server_busy",
1900
+ accountCooldownMs: OVERLOAD_COOLDOWN_MS,
1901
+ accountCooldownReason: "ServerOverloaded",
1902
+ },
1903
+ );
1904
+ }
1878
1905
  const result = getIncrementalDelta(
1879
1906
  lastRawContent,
1880
1907
  newContent,