qwenproxy-cli 1.6.1 → 1.6.2
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/clean-cache.js +7 -3
- package/dist/delete-chats.js +325 -8
- package/dist/index.js +667 -94
- package/dist/login.js +383 -9
- package/dist/reset-cooldowns.js +17 -3
- package/package.json +3 -3
- package/src/core/accounts.ts +1 -1
- package/src/core/version.ts +1 -1
- package/src/login.ts +90 -4
- package/src/routes/chat/account.ts +11 -4
- package/src/routes/chat/retry-policy.ts +45 -3
- package/src/routes/chat/streaming.ts +27 -0
- package/src/services/playwright.ts +378 -4
- package/src/services/qwen.ts +22 -6
- package/src/tui/views/accounts-view.ts +313 -96
- package/src/utils/overload-detector.ts +55 -0
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
|
-
|
|
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
|
|
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 =
|
|
86
|
-
export const COLD_ACCOUNT_PERSONALIZATION_SYNC_DEADLINE_MS =
|
|
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
|
-
|
|
1286
|
-
|
|
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
|
|
@@ -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,
|