qwenproxy-cli 1.0.9 → 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 +26 -0
- package/package.json +1 -1
- package/src/api/server.ts +22 -8
- package/src/core/config.ts +1 -1
- package/src/routes/chat/account.ts +7 -3
- package/src/services/captcha-coordinator.ts +9 -7
- package/src/services/playwright.ts +178 -36
- package/src/tui/app.ts +16 -6
- package/src/tui/screen.ts +2 -0
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.
|
|
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,20 +738,35 @@ 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) {
|
|
764
|
+
console.warn(
|
|
765
|
+
`⚠️ [Server] Standby validation finished: ${validated} ok, ${failed} failed`,
|
|
766
|
+
);
|
|
767
|
+
} else if (validated > 0) {
|
|
754
768
|
console.log(
|
|
755
|
-
|
|
769
|
+
`✅ [Server] Standby validation complete: all ${validated} account(s) ready`,
|
|
756
770
|
);
|
|
757
771
|
}
|
|
758
772
|
})().catch((error) => {
|
package/src/core/config.ts
CHANGED
|
@@ -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("
|
|
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
|
-
|
|
483
|
-
|
|
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 =
|
|
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.
|
|
117
|
+
const solverOperationTimeoutMs = Math.min(
|
|
118
118
|
config.timeouts.page,
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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 =
|
|
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<
|
|
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
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
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
|
-
|
|
1615
|
-
|
|
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
|
-
|
|
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
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
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<
|
|
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
|
-
|
|
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
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
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,
|
|
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
|
-
|
|
85
|
-
|
|
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
|
-
|
|
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
|
-
|
|
223
|
-
|
|
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
|
-
|
|
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
|
|