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/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
@@ -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,