qwenproxy-cli 1.0.10 → 1.0.11

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/bin/qwenproxy.js CHANGED
@@ -196,11 +196,37 @@ const child = spawn(process.execPath, ["--import", tsxLoaderArg, targetPath, ...
196
196
  env: process.env,
197
197
  });
198
198
 
199
+ const restoreTerminal = () => {
200
+ try {
201
+ const RESET_SEQ = "\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l\x1b[?1049l\x1b[?25h\x1b[0m";
202
+ fs.writeSync(1, RESET_SEQ);
203
+ } catch {}
204
+ if (process.stdin.setRawMode) {
205
+ try {
206
+ process.stdin.setRawMode(false);
207
+ } catch {}
208
+ }
209
+ };
210
+
199
211
  child.on("exit", (code, signal) => {
212
+ restoreTerminal();
200
213
  process.exit(code ?? (signal ? 1 : 0));
201
214
  });
202
215
 
203
216
  child.on("error", (err) => {
217
+ restoreTerminal();
204
218
  console.error("❌ [QwenProxy] Failed to execute CLI script:", err.message);
205
219
  process.exit(1);
206
220
  });
221
+
222
+ process.on("SIGINT", () => {
223
+ restoreTerminal();
224
+ process.exit(130);
225
+ });
226
+
227
+ process.on("SIGTERM", () => {
228
+ restoreTerminal();
229
+ process.exit(143);
230
+ });
231
+
232
+ process.on("exit", restoreTerminal);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qwenproxy-cli",
3
- "version": "1.0.10",
3
+ "version": "1.0.11",
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
@@ -721,9 +721,6 @@ export async function startServer(options?: {
721
721
 
722
722
  for (const account of remainingAccounts) {
723
723
  try {
724
- // Add to priority list first (initial priority based on config order)
725
- ensureAccountInPriority(account.id);
726
-
727
724
  // Validate login in background
728
725
  const ok = await validateAccountLogin(
729
726
  account,
@@ -732,6 +729,8 @@ export async function startServer(options?: {
732
729
  );
733
730
 
734
731
  if (ok) {
732
+ // Add to priority list only once validated
733
+ ensureAccountInPriority(account.id);
735
734
  validated++;
736
735
  console.log(
737
736
  `✅ [Server] Standby account validated: ${maskEmail(account.email)}`,
@@ -739,17 +738,28 @@ export async function startServer(options?: {
739
738
  } else {
740
739
  failed++;
741
740
  console.warn(
742
- `⚠️ [Server] Standby account login failed: ${maskEmail(account.email)}`,
741
+ `⚠️ [Server] Standby account login failed: ${maskEmail(account.email)} (quarantined)`,
742
+ );
743
+ const { markAccountRateLimited } = await import("../core/account-manager.ts");
744
+ markAccountRateLimited(
745
+ account.id,
746
+ 24 * 3600 * 1000,
747
+ "AuthFailed: Standby validation failed",
743
748
  );
744
749
  }
745
750
  } catch (error) {
746
751
  failed++;
747
752
  console.warn(
748
- `⚠️ [Server] Standby account validation error: ${maskEmail(account.email)}: ${getErrorMessage(error)}`,
753
+ `⚠️ [Server] Standby account validation error: ${maskEmail(account.email)}: ${getErrorMessage(error)} (quarantined)`,
754
+ );
755
+ const { markAccountRateLimited } = await import("../core/account-manager.ts");
756
+ markAccountRateLimited(
757
+ account.id,
758
+ 24 * 3600 * 1000,
759
+ `AuthFailed: ${getErrorMessage(error)}`,
749
760
  );
750
761
  }
751
762
  }
752
-
753
763
  if (failed > 0) {
754
764
  console.warn(
755
765
  `⚠️ [Server] Standby validation finished: ${validated} ok, ${failed} failed`,
@@ -75,7 +75,7 @@ const envSchema = z
75
75
  // Deadline for the FIRST upstream chunk on thinking models (the reasoning
76
76
  // idle of 600s is for gaps AFTER data flows; a stream that produced
77
77
  // nothing in this window is dead and should fail fast, retryable).
78
- QWEN_FIRST_CHUNK_TIMEOUT: z.string().default("180000"),
78
+ QWEN_FIRST_CHUNK_TIMEOUT: z.string().default("60000"),
79
79
  TOTAL_REQUEST_TIMEOUT: z.string().default("600000"),
80
80
  // Mid-stream silence window for thinking models: 3 min with ZERO upstream
81
81
  // bytes is a dead stream (WAF swallow / dropped connection) — fail fast and
@@ -474,13 +474,17 @@ export async function acquireUpstreamStream(
474
474
  if (
475
475
  isAccountTemporarilyBusy(accountId) &&
476
476
  params.allowTemporarilyBusyAccountId !== accountId &&
477
- accountId !== stickyThreadAccountId
477
+ accountId !== stickyThreadAccountId &&
478
+ hasFreeAlternateAccount(configuredAccounts, accountId, triedAccountIds)
478
479
  ) {
479
480
  console.log(
480
481
  `⏭️ [Chat] Skipping account ${accountEmail} (${accountId}) temporarily busy (chat in progress)`,
481
482
  );
482
- account = getNextAvailableAccount(triedAccountIds);
483
- continue;
483
+ const nextCandidate = getNextAvailableAccount(triedAccountIds);
484
+ if (nextCandidate && !getAccountCooldownInfo(nextCandidate.id)) {
485
+ account = nextCandidate;
486
+ continue;
487
+ }
484
488
  }
485
489
 
486
490
  // Do not wait 30 seconds on a saturated account when another account is
@@ -10,7 +10,7 @@ import {
10
10
  solveBaxiaCaptcha,
11
11
  } from "./captcha-solver.ts";
12
12
 
13
- const CHALLENGE_NAVIGATION_TIMEOUT_MS = 20_000;
13
+ const CHALLENGE_NAVIGATION_TIMEOUT_MS = 8_000;
14
14
  const CHALLENGE_PATH_MARKER = "_____tmd_____";
15
15
 
16
16
  /**
@@ -114,13 +114,15 @@ export async function recoverBaxiaCaptcha(
114
114
  // cannot be mistaken for a stuck browser and reset the account context.
115
115
  // Two navigations (open the challenge, return to the chat page) are part of
116
116
  // the recovery, so their budget belongs in the same total.
117
- const solverOperationTimeoutMs = Math.max(
117
+ const solverOperationTimeoutMs = Math.min(
118
118
  config.timeouts.page,
119
- config.captcha.timeoutMs +
120
- config.captcha.maxAttempts *
121
- (5_000 + config.captcha.retryDelayMs + config.captcha.settleMs) +
122
- 2 * CHALLENGE_NAVIGATION_TIMEOUT_MS +
123
- 5_000,
119
+ Math.max(
120
+ 15_000,
121
+ config.captcha.timeoutMs +
122
+ config.captcha.maxAttempts *
123
+ (3_000 + config.captcha.retryDelayMs + config.captcha.settleMs) +
124
+ 2 * CHALLENGE_NAVIGATION_TIMEOUT_MS,
125
+ ),
124
126
  );
125
127
 
126
128
  try {
@@ -66,6 +66,7 @@ import { Mutex } from "../core/mutex.ts";
66
66
  import {
67
67
  markAccountHeadersReady,
68
68
  unmarkAccountHeadersReady,
69
+ markAccountRateLimited,
69
70
  } from "../core/account-manager.ts";
70
71
  import { getAccountsByPriority } from "../core/account-priority.ts";
71
72
  import {
@@ -160,7 +161,7 @@ export function buildChromiumLaunchArgs(viewport: {
160
161
  // to the pool quickly (the 2026-08-22 log showed a lock held for 154s before
161
162
  // the waiter's recovery path finally ran). The chat lock keeps its own longer
162
163
  // hold budget (see acquireChatLock).
163
- const ACCOUNT_MUTEX_MAX_HOLD_MS = 60_000;
164
+ const ACCOUNT_MUTEX_MAX_HOLD_MS = 180_000;
164
165
  const accountMutexes = new Map<string, Mutex>();
165
166
 
166
167
  function getAccountMutex(accountId: string): Mutex {
@@ -1529,9 +1530,67 @@ export async function validateAccountLogin(
1529
1530
  release();
1530
1531
  }
1531
1532
  }
1532
-
1533
1533
  // ─── Login ────────────────────────────────────────────────────────────────────
1534
1534
 
1535
+ export interface LoginAttemptResult {
1536
+ success: boolean;
1537
+ permanentFailure?: boolean;
1538
+ reason?: string;
1539
+ }
1540
+
1541
+ export function classifyQwenAuthError(
1542
+ code?: string,
1543
+ details?: string,
1544
+ ): { isPermanent: boolean; reason: string } {
1545
+ const c = (code || "").trim().toLowerCase();
1546
+ const d = (details || "").trim().toLowerCase();
1547
+ const combined = `${c} ${d}`;
1548
+
1549
+ if (
1550
+ combined.includes("frozen") ||
1551
+ combined.includes("blocked") ||
1552
+ combined.includes("suspend") ||
1553
+ combined.includes("bloquead")
1554
+ ) {
1555
+ return {
1556
+ isPermanent: true,
1557
+ reason: `Conta bloqueada ou suspensa (${details || code || "AccountSuspended"})`,
1558
+ };
1559
+ }
1560
+
1561
+ if (
1562
+ combined.includes("password") ||
1563
+ combined.includes("senha") ||
1564
+ combined.includes("credential")
1565
+ ) {
1566
+ return {
1567
+ isPermanent: true,
1568
+ reason: `Senha incorreta (${details || code || "PasswordError"})`,
1569
+ };
1570
+ }
1571
+
1572
+ if (
1573
+ combined.includes("user") ||
1574
+ combined.includes("email") ||
1575
+ combined.includes("account") ||
1576
+ combined.includes("not exist") ||
1577
+ combined.includes("not found") ||
1578
+ combined.includes("not registered") ||
1579
+ combined.includes("não encontrado") ||
1580
+ combined.includes("não existe")
1581
+ ) {
1582
+ return {
1583
+ isPermanent: true,
1584
+ reason: `E-mail/usuário não encontrado (${details || code || "UserNotExist"})`,
1585
+ };
1586
+ }
1587
+
1588
+ return {
1589
+ isPermanent: false,
1590
+ reason: details || code || "Falha desconhecida no login",
1591
+ };
1592
+ }
1593
+
1535
1594
  async function loginToQwen(
1536
1595
  accountId: string,
1537
1596
  email: string,
@@ -1544,22 +1603,46 @@ async function loginToQwen(
1544
1603
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
1545
1604
  // Try API login first
1546
1605
  const apiResult = await loginViaApi(page, email, password);
1547
- if (apiResult) {
1606
+ if (apiResult.success) {
1548
1607
  await saveStorageState(page.context(), accountId);
1549
1608
  return true;
1550
1609
  }
1551
1610
 
1611
+ if (apiResult.permanentFailure) {
1612
+ console.error(
1613
+ `❌ [Playwright] Falha irrecuperável de autenticação para ${maskEmail(email)}: ${apiResult.reason}`,
1614
+ );
1615
+ markAccountRateLimited(
1616
+ accountId,
1617
+ 24 * 3600 * 1000,
1618
+ `AuthPermanentFailure: ${apiResult.reason}`,
1619
+ );
1620
+ return false;
1621
+ }
1622
+
1552
1623
  // Fallback to UI login
1553
1624
  const uiResult = await loginViaUi(page, email, password);
1554
- if (uiResult) {
1625
+ if (uiResult.success) {
1555
1626
  await saveStorageState(page.context(), accountId);
1556
1627
  return true;
1557
1628
  }
1558
1629
 
1630
+ if (uiResult.permanentFailure) {
1631
+ console.error(
1632
+ `❌ [Playwright] Falha irrecuperável de autenticação para ${maskEmail(email)}: ${uiResult.reason}`,
1633
+ );
1634
+ markAccountRateLimited(
1635
+ accountId,
1636
+ 24 * 3600 * 1000,
1637
+ `AuthPermanentFailure: ${uiResult.reason}`,
1638
+ );
1639
+ return false;
1640
+ }
1641
+
1559
1642
  if (attempt < maxAttempts) {
1560
1643
  const backoffMs = attempt * 5_000;
1561
1644
  console.warn(
1562
- `⚠️ [Playwright] Login attempt ${attempt}/${maxAttempts} failed for ${maskEmail(email)}, retrying in ${backoffMs / 1000}s`,
1645
+ `⚠️ [Playwright] Login attempt ${attempt}/${maxAttempts} failed for ${maskEmail(email)} (${apiResult.reason || uiResult.reason || "falha temporária"}), retrying in ${backoffMs / 1000}s`,
1563
1646
  );
1564
1647
  await sleep(backoffMs);
1565
1648
  }
@@ -1568,6 +1651,11 @@ async function loginToQwen(
1568
1651
  console.error(
1569
1652
  `❌ [Playwright] All login methods failed for ${maskEmail(email)}`,
1570
1653
  );
1654
+ markAccountRateLimited(
1655
+ accountId,
1656
+ 24 * 3600 * 1000,
1657
+ "AuthFailed: All login methods exhausted",
1658
+ );
1571
1659
  return false;
1572
1660
  }
1573
1661
 
@@ -1575,7 +1663,7 @@ async function loginViaApi(
1575
1663
  page: Page,
1576
1664
  email: string,
1577
1665
  password: string,
1578
- ): Promise<boolean> {
1666
+ ): Promise<LoginAttemptResult> {
1579
1667
  try {
1580
1668
  await page.goto(qwenUrl("/auth"), {
1581
1669
  waitUntil: "domcontentloaded",
@@ -1585,7 +1673,7 @@ async function loginViaApi(
1585
1673
 
1586
1674
  // Check if already logged in
1587
1675
  if (!page.url().includes("/auth")) {
1588
- return true;
1676
+ return { success: true };
1589
1677
  }
1590
1678
 
1591
1679
  const hashedPassword = crypto
@@ -1597,22 +1685,20 @@ async function loginViaApi(
1597
1685
  const result = await page.evaluate(
1598
1686
  async ({ email, password, signinUrl }) => {
1599
1687
  try {
1600
- const response = await fetch(signinUrl,
1601
- {
1602
- method: "POST",
1603
- signal: AbortSignal.timeout(10_000),
1604
- headers: {
1605
- accept: "application/json, text/plain, */*",
1606
- "content-type": "application/json",
1607
- source: "web",
1608
- timezone: new Date().toString().split(" (")[0],
1609
- "x-request-id": crypto.randomUUID(),
1610
- },
1611
- body: JSON.stringify({ email, password, login_type: "email" }),
1688
+ const response = await fetch(signinUrl, {
1689
+ method: "POST",
1690
+ signal: AbortSignal.timeout(10_000),
1691
+ headers: {
1692
+ accept: "application/json, text/plain, */*",
1693
+ "content-type": "application/json",
1694
+ source: "web",
1695
+ timezone: new Date().toString().split(" (")[0],
1696
+ "x-request-id": crypto.randomUUID(),
1612
1697
  },
1613
- );
1614
- const data = await response.json();
1615
- return { ok: response.ok, data };
1698
+ body: JSON.stringify({ email, password, login_type: "email" }),
1699
+ });
1700
+ const data = await response.json().catch(() => null);
1701
+ return { ok: response.ok, status: response.status, data };
1616
1702
  } catch (e: any) {
1617
1703
  return { ok: false, error: e.message };
1618
1704
  }
@@ -1620,18 +1706,48 @@ async function loginViaApi(
1620
1706
  { email, password: hashedPassword, signinUrl },
1621
1707
  );
1622
1708
 
1709
+ if (result.data) {
1710
+ if (result.data.success === true) {
1711
+ await page.goto(qwenUrl("/"), {
1712
+ waitUntil: "domcontentloaded",
1713
+ timeout: config.timeouts.navigation,
1714
+ });
1715
+ const loggedIn = !page.url().includes("auth") && !page.url().includes("login");
1716
+ if (loggedIn) {
1717
+ return { success: true };
1718
+ }
1719
+ } else if (result.data.success === false) {
1720
+ const code = result.data?.data?.code || result.data?.code;
1721
+ const details =
1722
+ result.data?.data?.details || result.data?.details || result.data?.message;
1723
+ const classified = classifyQwenAuthError(code, details);
1724
+ return {
1725
+ success: false,
1726
+ permanentFailure: classified.isPermanent,
1727
+ reason: classified.reason,
1728
+ };
1729
+ }
1730
+ }
1731
+
1623
1732
  if (result.ok) {
1624
1733
  await page.goto(qwenUrl("/"), {
1625
1734
  waitUntil: "domcontentloaded",
1626
1735
  timeout: config.timeouts.navigation,
1627
1736
  });
1628
- return !page.url().includes("auth") && !page.url().includes("login");
1737
+ const loggedIn = !page.url().includes("auth") && !page.url().includes("login");
1738
+ return {
1739
+ success: loggedIn,
1740
+ reason: loggedIn ? undefined : "Redirecionado de volta para /auth após signin",
1741
+ };
1629
1742
  }
1630
1743
 
1631
- return false;
1632
- } catch (err) {
1633
- console.warn(`⚠️ [Playwright] API login error: ${err}`);
1634
- return false;
1744
+ return {
1745
+ success: false,
1746
+ reason: result.error || `HTTP ${result.status || "desconhecido"} sem corpo JSON válido`,
1747
+ };
1748
+ } catch (err: any) {
1749
+ console.warn(`⚠️ [Playwright] API login error: ${err?.message || err}`);
1750
+ return { success: false, reason: err?.message || String(err) };
1635
1751
  }
1636
1752
  }
1637
1753
 
@@ -1639,7 +1755,7 @@ async function loginViaUi(
1639
1755
  page: Page,
1640
1756
  email: string,
1641
1757
  password: string,
1642
- ): Promise<boolean> {
1758
+ ): Promise<LoginAttemptResult> {
1643
1759
  try {
1644
1760
  await page.goto(qwenUrl("/auth"), {
1645
1761
  waitUntil: "domcontentloaded",
@@ -1649,7 +1765,7 @@ async function loginViaUi(
1649
1765
 
1650
1766
  // Check if already logged in
1651
1767
  if (!page.url().includes("/auth")) {
1652
- return true;
1768
+ return { success: true };
1653
1769
  }
1654
1770
 
1655
1771
  // Wait for email input
@@ -1665,11 +1781,11 @@ async function loginViaUi(
1665
1781
  timeout: config.timeouts.page,
1666
1782
  });
1667
1783
  } catch {
1668
- if (!page.url().includes("/auth")) return true;
1784
+ if (!page.url().includes("/auth")) return { success: true };
1669
1785
  console.warn(
1670
1786
  `⚠️ [Playwright] Email input not found on ${page.url()} (possible captcha or anti-bot challenge)`,
1671
1787
  );
1672
- throw new Error("Email input not found");
1788
+ return { success: false, reason: "Campo de e-mail não encontrado (possível captcha)" };
1673
1789
  }
1674
1790
 
1675
1791
  // Fill email
@@ -1711,6 +1827,28 @@ async function loginViaUi(
1711
1827
  }
1712
1828
  await sleep(3000);
1713
1829
 
1830
+ // Check for UI error elements in DOM (Ant Design errors, alerts, toasts)
1831
+ const errorSelector = [
1832
+ ".ant-form-item-explain-error",
1833
+ ".ant-message-error",
1834
+ ".ant-message-notice",
1835
+ '[role="alert"]',
1836
+ ".qwenchat-auth-error",
1837
+ ".auth-error-message",
1838
+ ].join(", ");
1839
+ const errorEl = page.locator(errorSelector).first();
1840
+ if (await errorEl.isVisible().catch(() => false)) {
1841
+ const errorText = (await errorEl.innerText().catch(() => "")).trim();
1842
+ if (errorText) {
1843
+ const classified = classifyQwenAuthError(undefined, errorText);
1844
+ return {
1845
+ success: false,
1846
+ permanentFailure: classified.isPermanent,
1847
+ reason: `Formulário: ${classified.reason}`,
1848
+ };
1849
+ }
1850
+ }
1851
+
1714
1852
  // Check if login was successful
1715
1853
  const isLoggedIn =
1716
1854
  !page.url().includes("auth") && !page.url().includes("login");
@@ -1720,12 +1858,16 @@ async function loginViaUi(
1720
1858
  waitUntil: "domcontentloaded",
1721
1859
  timeout: config.timeouts.navigation,
1722
1860
  });
1861
+ return { success: true };
1723
1862
  }
1724
1863
 
1725
- return isLoggedIn;
1726
- } catch (err) {
1727
- console.warn(`⚠️ [Playwright] UI login error: ${err}`);
1728
- return false;
1864
+ return {
1865
+ success: false,
1866
+ reason: "A página permaneceu na tela de autenticação após envio do formulário",
1867
+ };
1868
+ } catch (err: any) {
1869
+ console.warn(`⚠️ [Playwright] UI login error: ${err?.message || err}`);
1870
+ return { success: false, reason: err?.message || String(err) };
1729
1871
  }
1730
1872
  }
1731
1873
 
@@ -2780,7 +2922,7 @@ export async function keepAlivePlaywrightAccount(
2780
2922
  if (shouldNavigate) {
2781
2923
  await page.goto(qwenUrl("/"), {
2782
2924
  waitUntil: "domcontentloaded",
2783
- timeout: Math.min(config.timeouts.navigation, 15_000),
2925
+ timeout: Math.min(config.timeouts.navigation, 30_000),
2784
2926
  });
2785
2927
  lastKeepAliveNavigation.set(accountId, now);
2786
2928
  } else {
package/src/tui/app.ts CHANGED
@@ -81,13 +81,19 @@ export class TuiApp {
81
81
  // Listen for key events
82
82
  // Listen for key and mouse events with microtask coalescing
83
83
  this.screen.onKey(async (key) => {
84
- await this.handleKey(key);
85
- this.requestRender();
84
+ try {
85
+ await this.handleKey(key);
86
+ this.requestRender();
87
+ } catch (err: any) {
88
+ console.error(`[TUI] Erro ao processar tecla: ${err?.message || String(err)}`);
89
+ }
86
90
  });
87
91
 
88
92
  // Listen for terminal resize
89
93
  this.screen.onResize(() => {
90
- this.render();
94
+ try {
95
+ this.render();
96
+ } catch {}
91
97
  });
92
98
 
93
99
  // Start server in-process together with TUI ("tudo junto uma coisa só")
@@ -219,8 +225,9 @@ export class TuiApp {
219
225
  }
220
226
  private render(): void {
221
227
  if (!this.isRunning) return;
222
- const { cols, rows } = this.screen.getSize();
223
- const frame: string[] = [];
228
+ try {
229
+ const { cols, rows } = this.screen.getSize();
230
+ const frame: string[] = [];
224
231
 
225
232
  // 1. Header Deck (Tabs & Title)
226
233
  const serverState = ServerManager.getInstance().getState();
@@ -266,6 +273,9 @@ export class TuiApp {
266
273
  const availableContentRows = Math.max(8, rows - 3);
267
274
  const viewLines = activeView.render(cols, availableContentRows, this.statusSnapshot);
268
275
  frame.push(...viewLines);
269
- this.screen.render(frame);
276
+ this.screen.render(frame);
277
+ } catch (err: any) {
278
+ console.error(`[TUI] Erro na renderização: ${err?.message || String(err)}`);
279
+ }
270
280
  }
271
281
  }
package/src/tui/screen.ts CHANGED
@@ -207,6 +207,7 @@ export class Screen {
207
207
  process.on("SIGTERM", this.exitHandler!);
208
208
  process.on("SIGBREAK", this.exitHandler!);
209
209
  process.on("exit", this.exitHandler!);
210
+ process.on("uncaughtException", this.exitHandler!);
210
211
  return true;
211
212
  }
212
213
 
@@ -234,6 +235,7 @@ export class Screen {
234
235
  process.removeListener("SIGTERM", this.exitHandler);
235
236
  process.removeListener("SIGBREAK", this.exitHandler);
236
237
  process.removeListener("exit", this.exitHandler);
238
+ process.removeListener("uncaughtException", this.exitHandler);
237
239
  this.exitHandler = null;
238
240
  }
239
241