qwenproxy-cli 1.2.10 → 1.3.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qwenproxy-cli",
3
- "version": "1.2.10",
3
+ "version": "1.3.1",
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": {
@@ -26,8 +26,8 @@
26
26
  "reset": "npx tsx src/reset-cooldowns.ts",
27
27
  "purge": "npx tsx src/delete-chats.ts",
28
28
  "test": "npm run test:mock && npm run test:live",
29
- "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/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",
30
- "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/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",
29
+ "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/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",
30
+ "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/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",
31
31
  "test:live": "tsx --test src/tests/concurrentChat.test.ts src/tests/agenticStress.test.ts",
32
32
  "benchmark:proxy": "npx tsx src/benchmarks/proxy-baseline.ts",
33
33
  "typecheck": "tsc --noEmit"
package/src/api/server.ts CHANGED
@@ -172,7 +172,13 @@ app.use("*", async (c, next) => {
172
172
  c.req.path === "/logs" ||
173
173
  c.req.path.startsWith("/logs") ||
174
174
  c.req.path.startsWith("/diagnostics") ||
175
- c.req.path === "/favicon.ico";
175
+ c.req.path === "/favicon.ico" ||
176
+ c.req.path === "/v1/models" ||
177
+ c.req.path.startsWith("/v1/models/") ||
178
+ c.req.path === "/models" ||
179
+ c.req.path.startsWith("/models/") ||
180
+ c.req.path === "/v1/chat/mode" ||
181
+ c.req.path === "/chat/mode";
176
182
 
177
183
  if (!isProbe) {
178
184
  metrics.increment("requests.total");
@@ -445,7 +451,13 @@ app.onError((err, c) => {
445
451
  c.req.path === "/health" ||
446
452
  c.req.path === "/metrics" ||
447
453
  c.req.path.startsWith("/diagnostics") ||
448
- c.req.path === "/favicon.ico";
454
+ c.req.path === "/favicon.ico" ||
455
+ c.req.path === "/v1/models" ||
456
+ c.req.path.startsWith("/v1/models/") ||
457
+ c.req.path === "/models" ||
458
+ c.req.path.startsWith("/models/") ||
459
+ c.req.path === "/v1/chat/mode" ||
460
+ c.req.path === "/chat/mode";
449
461
  if (!isProbe) {
450
462
  metrics.increment("requests.errors");
451
463
  }
@@ -4,7 +4,8 @@ import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  import { getDatabase } from "./database.ts";
6
6
  import { decrypt, encrypt } from "./crypto-utils.ts";
7
- import { getAccountProfilePath, getProfilesDir } from "./paths.ts";
7
+ import { getAccountProfilePath, getProfilesDir, isRunningUnderNodeTest } from "./paths.ts";
8
+ import { updateEnvVariable } from "./local-auth.ts";
8
9
 
9
10
  export interface QwenAccount {
10
11
  id: string;
@@ -14,6 +15,29 @@ export interface QwenAccount {
14
15
  cooldown_reason?: string | null;
15
16
  }
16
17
 
18
+ export function isPlaceholderAccountEmail(email: string, password?: string): boolean {
19
+ if (!email || typeof email !== "string") return false;
20
+ const clean = email.trim().toLowerCase();
21
+ if (
22
+ clean === "email1@example.com" ||
23
+ clean === "email2@example.com" ||
24
+ clean === "user@example.com" ||
25
+ clean === "test@example.com" ||
26
+ clean === "your-email@example.com" ||
27
+ clean.startsWith("email1@") ||
28
+ clean.startsWith("email2@")
29
+ ) {
30
+ return true;
31
+ }
32
+ if (
33
+ password &&
34
+ (password === "password1" || password === "password2" || password === "changeme")
35
+ ) {
36
+ return true;
37
+ }
38
+ return false;
39
+ }
40
+
17
41
  function generateId(email: string): string {
18
42
  return crypto
19
43
  .createHash("md5")
@@ -59,10 +83,12 @@ function parseEnvAccounts(): QwenAccount[] {
59
83
  }
60
84
  const email = clean(trimmed.substring(0, colonIdx));
61
85
  const password = clean(trimmed.substring(colonIdx + 1));
62
- if (!email || !password) {
63
- console.warn(
64
- `[Accounts] Invalid QWEN_ACCOUNTS entry at index ${index}: "${trimmed}"`,
65
- );
86
+ if (!email || !password || isPlaceholderAccountEmail(email, password)) {
87
+ if (!email || !password) {
88
+ console.warn(
89
+ `[Accounts] Invalid QWEN_ACCOUNTS entry at index ${index}: "${trimmed}"`,
90
+ );
91
+ }
66
92
  return null;
67
93
  }
68
94
  return {
@@ -79,6 +105,17 @@ let lastSyncTime = 0;
79
105
  const SYNC_INTERVAL = 30_000;
80
106
 
81
107
  function syncEnvAccounts(): void {
108
+ const db = getDatabase();
109
+ // Automatically purge any dummy placeholder accounts that may have been seeded from .env.example
110
+ try {
111
+ db.prepare(`
112
+ DELETE FROM accounts
113
+ WHERE lower(email) IN ('email1@example.com', 'email2@example.com', 'user@example.com', 'test@example.com', 'your-email@example.com')
114
+ OR lower(email) LIKE 'email1@%'
115
+ OR lower(email) LIKE 'email2@%'
116
+ `).run();
117
+ } catch {}
118
+
82
119
  const envAccounts = process.env.QWEN_ACCOUNTS || "";
83
120
  const now = Date.now();
84
121
  if (envAccounts === lastSyncedEnv && now - lastSyncTime < SYNC_INTERVAL)
@@ -90,7 +127,6 @@ function syncEnvAccounts(): void {
90
127
  const accounts = parseEnvAccounts();
91
128
  if (accounts.length === 0) return;
92
129
 
93
- const db = getDatabase();
94
130
  const upsert = db.prepare(`
95
131
  INSERT INTO accounts (id, email, password) VALUES (?, ?, ?)
96
132
  ON CONFLICT(email) DO UPDATE SET password = excluded.password, updated_at = datetime('now')
@@ -124,15 +160,17 @@ function getCachedAccounts(): QwenAccount[] {
124
160
  )
125
161
  .all() as QwenAccount[];
126
162
 
127
- accountsCache = rows.map((row) => {
128
- let password = "";
129
- try {
130
- password = decrypt(row.password);
131
- } catch {
132
- password = "";
133
- }
134
- return { ...row, password };
135
- });
163
+ accountsCache = rows
164
+ .filter((row) => !isPlaceholderAccountEmail(row.email))
165
+ .map((row) => {
166
+ let password = "";
167
+ try {
168
+ password = decrypt(row.password);
169
+ } catch {
170
+ password = "";
171
+ }
172
+ return { ...row, password };
173
+ });
136
174
  accountsCacheTime = now;
137
175
  return accountsCache;
138
176
  }
@@ -316,22 +354,85 @@ export function addAccount(
316
354
  return newAccount;
317
355
  }
318
356
 
319
- function wipeAccountSessionFiles(id: string): void {
357
+ export function wipeAccountSessionFiles(id: string): void {
320
358
  const profilePath = getAccountProfilePath(id);
321
359
  try {
322
360
  fs.rmSync(profilePath, { recursive: true, force: true });
323
- } catch {}
361
+ } catch (error) {
362
+ const message =
363
+ error instanceof Error ? error.message : String(error ?? "");
364
+ if (
365
+ message.includes("EPERM") ||
366
+ message.includes("EBUSY") ||
367
+ message.includes("Permission denied")
368
+ ) {
369
+ try {
370
+ const stalePath = `${profilePath}.stale-${Date.now()}`;
371
+ fs.renameSync(profilePath, stalePath);
372
+ } catch {}
373
+ }
374
+ }
324
375
  const siblingState = path.join(getProfilesDir(), `${id}_state.json`);
325
376
  try {
326
377
  fs.rmSync(siblingState, { force: true });
327
378
  } catch {}
328
379
  }
329
380
 
381
+ export function removeAccountFromEnv(emailToRemove: string): void {
382
+ const target = emailToRemove.trim().toLowerCase();
383
+ const rawEnv = process.env.QWEN_ACCOUNTS;
384
+ if (!rawEnv) return;
385
+
386
+ const clean = (s: string) => s.trim().replace(/^[,;\s"'`]+|[,;\s"'`]+$/g, "").trim();
387
+ const lines = rawEnv.split(/[\r\n;]+/);
388
+ const remaining: string[] = [];
389
+
390
+ for (const rawLine of lines) {
391
+ const line = clean(rawLine);
392
+ if (!line) continue;
393
+
394
+ if (line.includes(",") && (line.match(/@/g) || []).length > 1) {
395
+ for (const seg of line.split(",")) {
396
+ const trimmed = clean(seg);
397
+ if (!trimmed) continue;
398
+ const colonIdx = trimmed.indexOf(":");
399
+ const email = clean(colonIdx !== -1 ? trimmed.substring(0, colonIdx) : trimmed);
400
+ if (email.toLowerCase() !== target && !isPlaceholderAccountEmail(email)) {
401
+ remaining.push(trimmed);
402
+ }
403
+ }
404
+ } else {
405
+ const colonIdx = line.indexOf(":");
406
+ const email = clean(colonIdx !== -1 ? line.substring(0, colonIdx) : line);
407
+ if (email.toLowerCase() !== target && !isPlaceholderAccountEmail(email)) {
408
+ remaining.push(line);
409
+ }
410
+ }
411
+ }
412
+
413
+ const newEnvValue = remaining.join(",");
414
+ process.env.QWEN_ACCOUNTS = newEnvValue;
415
+ lastSyncedEnv = newEnvValue;
416
+ lastSyncTime = Date.now();
417
+
418
+ if (!isRunningUnderNodeTest()) {
419
+ try {
420
+ updateEnvVariable("QWEN_ACCOUNTS", newEnvValue);
421
+ } catch {}
422
+ }
423
+ }
424
+
330
425
  export function removeAccount(id: string): boolean {
331
426
  const db = getDatabase();
427
+ const account = db
428
+ .prepare("SELECT email FROM accounts WHERE id = ?")
429
+ .get(id) as { email: string } | undefined;
332
430
  const result = db.prepare("DELETE FROM accounts WHERE id = ?").run(id);
333
431
  if (result.changes > 0) {
334
432
  wipeAccountSessionFiles(id);
433
+ if (account?.email) {
434
+ removeAccountFromEnv(account.email);
435
+ }
335
436
  }
336
437
  invalidateAccountsCache();
337
438
  return result.changes > 0;
@@ -43,6 +43,58 @@ export function assertBindAllowed(host: string, apiKey: string): void {
43
43
  );
44
44
  }
45
45
 
46
+ export function updateEnvVariable(
47
+ key: string,
48
+ value: string,
49
+ envPath = getEnvFilePath(),
50
+ ): void {
51
+ try {
52
+ const existing = fs.existsSync(envPath)
53
+ ? fs.readFileSync(envPath, "utf-8")
54
+ : "";
55
+ const regex = new RegExp(`^([ \\t]*)${key}[ \\t]*=.*$`, "m");
56
+ const assignment = regex.exec(existing);
57
+
58
+ let updated: string;
59
+ if (assignment) {
60
+ const start = assignment.index;
61
+ const end = start + assignment[0].length;
62
+ updated =
63
+ existing.slice(0, start) +
64
+ `${assignment[1]}${key}=${value}` +
65
+ existing.slice(end);
66
+ } else {
67
+ const lineEnding = existing.includes("\r\n") ? "\r\n" : "\n";
68
+ const prefix =
69
+ existing.length === 0 || existing.endsWith("\n") ? "" : lineEnding;
70
+ updated = `${existing}${prefix}${key}=${value}${lineEnding}`;
71
+ }
72
+
73
+ fs.writeFileSync(envPath, updated, { encoding: "utf-8", mode: 0o600 });
74
+ } catch (err) {
75
+ console.warn(
76
+ `⚠️ [Config] Could not update ${key} in .env:`,
77
+ err instanceof Error ? err.message : String(err),
78
+ );
79
+ }
80
+ }
81
+
82
+ export function persistServerPort(
83
+ port: number,
84
+ envPath = getEnvFilePath(),
85
+ ): void {
86
+ updateEnvVariable("PORT", String(port), envPath);
87
+ process.env.PORT = String(port);
88
+ }
89
+
90
+ export function persistCustomApiKey(
91
+ apiKey: string,
92
+ envPath = getEnvFilePath(),
93
+ ): void {
94
+ updateEnvVariable("API_KEY", apiKey.trim(), envPath);
95
+ process.env.API_KEY = apiKey.trim();
96
+ }
97
+
46
98
  export function persistApiKey(
47
99
  apiKey: string,
48
100
  envPath = getEnvFilePath(),
package/src/core/paths.ts CHANGED
@@ -181,6 +181,9 @@ export function getTuiSettingsPath(customDataDir?: string): string {
181
181
  * Path to user configuration .env file (either local or in global data dir).
182
182
  */
183
183
  export function getEnvFilePath(customDataDir?: string): string {
184
+ if (isRunningUnderNodeTest()) {
185
+ return path.join(customDataDir || getDataDir(), ".env");
186
+ }
184
187
  const localEnv = path.resolve(".env");
185
188
  if (fs.existsSync(localEnv)) {
186
189
  return localEnv;
package/src/login.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import crypto from "crypto";
1
2
  import {
2
3
  addAccount,
3
4
  removeAccount,
@@ -76,30 +77,76 @@ async function addAccountFlow() {
76
77
  clear();
77
78
  console.log("=== Add New Account ===\n");
78
79
  const email = await askQuestion("Email: ");
79
- if (!email) {
80
+ if (!email || !email.trim()) {
80
81
  console.log("Email is required.");
81
82
  await askQuestion("Press Enter to continue...");
82
83
  return;
83
84
  }
84
85
 
85
86
  const password = await askQuestion("Password: ");
86
- if (!password) {
87
+ if (!password || !password.trim()) {
87
88
  console.log("Password is required.");
88
89
  await askQuestion("Press Enter to continue...");
89
90
  return;
90
91
  }
91
92
 
93
+ const trimmedEmail = email.trim();
94
+ const trimmedPassword = password.trim();
95
+
96
+ const existingAccounts = listAccounts();
97
+ if (
98
+ existingAccounts.some(
99
+ (a) => a.email.toLowerCase() === trimmedEmail.toLowerCase(),
100
+ )
101
+ ) {
102
+ console.log(
103
+ `\n❌ Error: An account with email "${trimmedEmail}" already exists.`,
104
+ );
105
+ await askQuestion("Press Enter to continue...");
106
+ return;
107
+ }
108
+
109
+ const tempId = crypto.randomUUID();
110
+ console.log(
111
+ `\n⏳ Validating credentials for ${maskEmail(trimmedEmail)} with Qwen via Playwright...`,
112
+ );
113
+
92
114
  let account: QwenAccount | null = null;
93
115
  try {
94
- account = addAccount(email, password);
95
- console.log(`Account added: ${maskEmail(account.email)} (${account.id})`);
96
- console.log("Credentials will be validated by Playwright on server start.");
116
+ const { validateAccountLogin } = await import("./services/playwright.ts");
117
+ const { wipeAccountSessionFiles } = await import("./core/accounts.ts");
118
+ const { clearAccountCooldown } = await import("./core/account-manager.ts");
119
+ const { config } = await import("./core/config.ts");
120
+
121
+ const ok = await validateAccountLogin(
122
+ { id: tempId, email: trimmedEmail, password: trimmedPassword },
123
+ config.playwright.headless,
124
+ config.playwright.browser,
125
+ );
126
+
127
+ if (ok) {
128
+ account = addAccount(trimmedEmail, trimmedPassword, tempId);
129
+ console.log(
130
+ `\n✅ Account verified and saved: ${maskEmail(account.email)} (${account.id})`,
131
+ );
132
+ } else {
133
+ wipeAccountSessionFiles(tempId);
134
+ clearAccountCooldown(tempId);
135
+ console.log(
136
+ `\n❌ Authentication failed: Invalid credentials or unhandled login challenge.`,
137
+ );
138
+ console.log("The account was NOT saved.");
139
+ }
97
140
  } catch (err: any) {
98
- if (account) removeAccount(account.id);
99
- console.log(`\nError: ${err.message}`);
141
+ const { wipeAccountSessionFiles } = await import("./core/accounts.ts");
142
+ const { clearAccountCooldown } = await import("./core/account-manager.ts");
143
+ wipeAccountSessionFiles(tempId);
144
+ clearAccountCooldown(tempId);
145
+ console.log(`\n❌ Validation error: ${err?.message || String(err)}`);
146
+ console.log("The account was NOT saved.");
100
147
  }
101
148
 
102
- await askQuestion("Press Enter to continue...");
149
+ await askQuestion("\nPress Enter to continue...");
103
150
  }
104
151
 
105
152
  async function removeAccountFlow() {
@@ -34,6 +34,14 @@ export function parseQwenErrorPayload(
34
34
 
35
35
  try {
36
36
  const payload = JSON.parse(text);
37
+ if (payload && (payload.update_member || payload.data?.update_member)) {
38
+ return {
39
+ code: "MembershipLimit",
40
+ details: "Account membership limit reached",
41
+ message: "Qwen upstream error: MembershipLimit: Account membership limit reached",
42
+ status: 429,
43
+ };
44
+ }
37
45
  if (payload && payload.success === false) {
38
46
  const code = payload.data?.code || payload.code || "UpstreamError";
39
47
  const details =
@@ -242,6 +242,8 @@ export function isQuotaLikeError(err: unknown): boolean {
242
242
  return (
243
243
  code === "quota_limit" ||
244
244
  code === "ratelimited" ||
245
+ code === "membership_limit" ||
246
+ code === "membershiplimit" ||
245
247
  message.includes("quota_limit") ||
246
248
  message.includes("quota exceeded") ||
247
249
  message.includes("allocated quota") ||
@@ -253,11 +255,16 @@ export function isQuotaLikeError(err: unknown): boolean {
253
255
  message.includes("rate increased too quickly") ||
254
256
  message.includes("upper limit for today's usage") ||
255
257
  message.includes("you've reached the upper limit") ||
258
+ message.includes("membership_limit") ||
259
+ message.includes("membership limit") ||
260
+ message.includes("update_member") ||
261
+ message.includes("update member") ||
256
262
  // Accept local rate_limit code only when message also looks like quota/rate
257
263
  (code === "rate_limit_exceeded" &&
258
264
  (message.includes("quota") ||
259
265
  message.includes("rate") ||
260
266
  message.includes("limit") ||
267
+ message.includes("membership") ||
261
268
  message.includes("demanda") ||
262
269
  message.includes("demand")))
263
270
  );
@@ -18,7 +18,7 @@ import {
18
18
  setToolCapNotice,
19
19
  } from "../../services/qwen.ts";
20
20
  import { acquireUpstreamStream } from "./account.ts";
21
- import { markAccountRateLimited } from "../../core/account-manager.ts";
21
+ import { markAccountRateLimited, computeQuotaCooldownMs } from "../../core/account-manager.ts";
22
22
  import {
23
23
  clearTemporaryBusy,
24
24
  markAccountTemporarilyBusy,
@@ -438,6 +438,20 @@ export async function processNonStreamingResponse(
438
438
  ) {
439
439
  const delta = chunk.choices[0].delta;
440
440
 
441
+ if (delta.extra?.update_member || chunk.update_member) {
442
+ throw toRetryableStreamError(
443
+ "membership_limit",
444
+ "Qwen upstream membership limit reached (update_member); rotating account",
445
+ {
446
+ switchAccount: true,
447
+ forceNewChat: true,
448
+ reason: "membership_limit",
449
+ accountCooldownMs: computeQuotaCooldownMs(Date.now()),
450
+ accountCooldownReason: "MembershipLimit",
451
+ },
452
+ );
453
+ }
454
+
441
455
  if (isThinkingPhase(delta.phase)) {
442
456
  isThinkingChunk = true;
443
457
  const formattedSummary = extractThinkingContent(delta);
@@ -952,6 +966,11 @@ export async function processStreamingResponse(
952
966
 
953
967
  // Release the lease immediately. The stop request below is best-effort
954
968
  // and must never hold the account slot or block the next tool turn.
969
+ if (onStreamComplete) {
970
+ try {
971
+ onStreamComplete();
972
+ } catch {}
973
+ }
955
974
  retryContext.releaseAccountLease?.();
956
975
  retryContext.releaseAccountLease = null;
957
976
  removeStream(completionId);
@@ -1739,6 +1758,20 @@ export async function processStreamingResponse(
1739
1758
  ) {
1740
1759
  const delta = chunk.choices[0].delta;
1741
1760
 
1761
+ if (delta.extra?.update_member || chunk.update_member) {
1762
+ throw toRetryableStreamError(
1763
+ "membership_limit",
1764
+ "Qwen upstream membership limit reached (update_member); rotating account",
1765
+ {
1766
+ switchAccount: true,
1767
+ forceNewChat: true,
1768
+ reason: "membership_limit",
1769
+ accountCooldownMs: computeQuotaCooldownMs(Date.now()),
1770
+ accountCooldownReason: "MembershipLimit",
1771
+ },
1772
+ );
1773
+ }
1774
+
1742
1775
  // Qwen streams may end with a {"status":"finished",
1743
1776
  // "phase":"answer"} delta and NO trailing [DONE]. Treat it as
1744
1777
  // the terminal event so we don't wait on the keep-alive
@@ -2624,11 +2657,17 @@ export async function processStreamingResponse(
2624
2657
  }
2625
2658
 
2626
2659
  // Release locks now that the stream is fully done
2627
- if (onStreamComplete) onStreamComplete();
2660
+ if (onStreamComplete) {
2661
+ try {
2662
+ onStreamComplete();
2663
+ } catch {}
2664
+ }
2628
2665
 
2629
2666
  // Release account lease from transparent retry if active
2630
2667
  if (retryContext.releaseAccountLease) {
2631
- retryContext.releaseAccountLease();
2668
+ try {
2669
+ retryContext.releaseAccountLease();
2670
+ } catch {}
2632
2671
  retryContext.releaseAccountLease = null;
2633
2672
  }
2634
2673
  }