qwenproxy-cli 1.0.32 → 1.2.0

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
@@ -67,6 +67,11 @@ Options:
67
67
  `);
68
68
  process.exit(0);
69
69
  }
70
+ // 2.5 Pure Node update execution (bypasses tsx and esbuild.exe locks)
71
+ if (firstArg === "update") {
72
+ const { runUpdateCommand } = await import("./update.js");
73
+ await runUpdateCommand();
74
+ } else {
70
75
 
71
76
  // 3. Command dispatcher
72
77
  let scriptFile = "src/index.ts";
@@ -93,12 +98,10 @@ if (firstArg === "start" || rawArgs.includes("--server")) {
93
98
  } else if (firstArg === "purge") {
94
99
  scriptFile = "src/delete-chats.ts";
95
100
  scriptArgs = rawArgs.slice(1);
96
- } else if (firstArg === "update") {
97
- scriptFile = "src/update-cli.ts";
98
- scriptArgs = rawArgs.slice(1);
99
101
  } else if (firstArg === "login") {
100
102
  scriptFile = "src/login.ts";
101
103
  scriptArgs = rawArgs.slice(1);
104
+ } else {
102
105
  // Pass through remaining options to index.ts with default TUI flag
103
106
  scriptFile = "src/index.ts";
104
107
  scriptArgs = ["--tui", ...rawArgs];
@@ -228,3 +231,4 @@ process.on("SIGTERM", () => {
228
231
  });
229
232
 
230
233
  process.on("exit", restoreTerminal);
234
+ }
@@ -0,0 +1,11 @@
1
+ export type PackageManager = "bun" | "pnpm" | "yarn" | "npm";
2
+
3
+ export function detectPackageManager(): PackageManager;
4
+ export function getUpdateArgs(
5
+ pm: PackageManager,
6
+ packageName: string,
7
+ targetVersion?: string,
8
+ ): { cmd: string; args: string[] };
9
+ export function isNewerVersion(current: string, latest: string): boolean;
10
+ export function fetchLatestNpmVersion(packageName: string): Promise<string>;
11
+ export function runUpdateCommand(): Promise<void>;
package/bin/update.js ADDED
@@ -0,0 +1,155 @@
1
+ /**
2
+ * QwenProxy - Fast, Pure-Node CLI Self-Updater
3
+ *
4
+ * Runs natively in Node.js without compiling via tsx or spawning esbuild.exe,
5
+ * ensuring zero file locks on native binaries during global npm/pnpm/bun updates on Windows.
6
+ */
7
+
8
+ import { spawnSync } from "node:child_process";
9
+ import fs from "node:fs";
10
+ import path from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+
13
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
14
+ const packageRoot = path.resolve(__dirname, "..");
15
+ const packageJsonPath = path.join(packageRoot, "package.json");
16
+
17
+ export function detectPackageManager() {
18
+ const userAgent = process.env.npm_config_user_agent || "";
19
+ if (userAgent.startsWith("bun")) return "bun";
20
+ if (userAgent.startsWith("pnpm")) return "pnpm";
21
+ if (userAgent.startsWith("yarn")) return "yarn";
22
+
23
+ const execPath = process.execPath.toLowerCase();
24
+ if (execPath.includes("bun")) return "bun";
25
+
26
+ const currentPath = packageRoot.toLowerCase();
27
+ if (currentPath.includes(".pnpm") || currentPath.includes("pnpm")) return "pnpm";
28
+ if (currentPath.includes(".bun") || currentPath.includes("bun")) return "bun";
29
+ if (currentPath.includes("yarn")) return "yarn";
30
+
31
+ return "npm";
32
+ }
33
+
34
+ export function getUpdateArgs(pm, packageName, targetVersion) {
35
+ const versionTag = targetVersion ? `@${targetVersion}` : "@latest";
36
+ switch (pm) {
37
+ case "bun":
38
+ return { cmd: "bun", args: ["add", "-g", `${packageName}${versionTag}`] };
39
+ case "pnpm":
40
+ return {
41
+ cmd: "pnpm",
42
+ args: targetVersion
43
+ ? ["add", "-g", `${packageName}@${targetVersion}`]
44
+ : ["update", "-g", packageName],
45
+ };
46
+ case "yarn":
47
+ return {
48
+ cmd: "yarn",
49
+ args: targetVersion
50
+ ? ["global", "add", `${packageName}@${targetVersion}`]
51
+ : ["global", "upgrade", packageName],
52
+ };
53
+ case "npm":
54
+ default:
55
+ return { cmd: "npm", args: ["install", "-g", `${packageName}${versionTag}`] };
56
+ }
57
+ }
58
+
59
+ export function isNewerVersion(current, latest) {
60
+ const c = current.replace(/^v/i, "").split(".").map((n) => parseInt(n, 10));
61
+ const l = latest.replace(/^v/i, "").split(".").map((n) => parseInt(n, 10));
62
+ for (let i = 0; i < 3; i++) {
63
+ const cv = c[i] || 0;
64
+ const lv = l[i] || 0;
65
+ if (lv > cv) return true;
66
+ if (lv < cv) return false;
67
+ }
68
+ return false;
69
+ }
70
+
71
+ export async function fetchLatestNpmVersion(packageName) {
72
+ try {
73
+ const controller = new AbortController();
74
+ const timer = setTimeout(() => controller.abort(), 6000);
75
+ const res = await fetch(`https://registry.npmjs.org/${packageName}/latest`, {
76
+ headers: { Accept: "application/json" },
77
+ signal: controller.signal,
78
+ });
79
+ clearTimeout(timer);
80
+ if (res.ok) {
81
+ const data = await res.json();
82
+ if (data.version && typeof data.version === "string") {
83
+ return data.version.trim();
84
+ }
85
+ }
86
+ } catch {}
87
+
88
+ try {
89
+ const fullCmd = `npm view ${packageName} version`;
90
+ const res = spawnSync(fullCmd, {
91
+ encoding: "utf-8",
92
+ shell: true,
93
+ timeout: 30000,
94
+ });
95
+ if (res.stdout) {
96
+ const lines = res.stdout.trim().split(/\r?\n/);
97
+ for (const line of lines.reverse()) {
98
+ const cleaned = line.trim().replace(/^v/i, "");
99
+ if (/^\d+\.\d+\.\d+/.test(cleaned)) {
100
+ return cleaned;
101
+ }
102
+ }
103
+ }
104
+ } catch {}
105
+
106
+ return "";
107
+ }
108
+
109
+ export async function runUpdateCommand() {
110
+ let pkg = { name: "qwenproxy-cli", version: "1.0.0" };
111
+ try {
112
+ pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
113
+ } catch {}
114
+
115
+ const currentVersion = pkg.version || "1.0.0";
116
+ const packageName = pkg.name || "qwenproxy-cli";
117
+ const pm = detectPackageManager();
118
+
119
+ console.log(`\n📦 [QwenProxy] Versão local instalada: v${currentVersion}`);
120
+ console.log(`⚙️ [QwenProxy] Gerenciador de pacotes detectado: ${pm}`);
121
+ console.log(`🔍 [QwenProxy] Verificando se há novas versões de ${packageName} no npm registry...`);
122
+
123
+ const latestVersion = await fetchLatestNpmVersion(packageName);
124
+
125
+ if (!latestVersion) {
126
+ console.warn("⚠️ [QwenProxy] Não foi possível consultar o registro online.");
127
+ const manual = getUpdateArgs(pm, packageName);
128
+ console.log(`👉 Você pode forçar a atualização manualmente com:\n ${manual.cmd} ${manual.args.join(" ")}\n`);
129
+ return;
130
+ }
131
+
132
+ console.log(`🌐 [QwenProxy] Versão mais recente disponível: v${latestVersion}`);
133
+
134
+ if (!isNewerVersion(currentVersion, latestVersion)) {
135
+ console.log(`\n✨ Você já está utilizando a versão mais recente (v${currentVersion})!\n`);
136
+ return;
137
+ }
138
+
139
+ console.log(`\n🚀 Nova versão disponível: v${currentVersion} ➔ v${latestVersion}`);
140
+ const { cmd, args } = getUpdateArgs(pm, packageName, latestVersion);
141
+ console.log(`⏳ Atualizando globalmente via ${pm} (${cmd} ${args.join(" ")})...`);
142
+ const fullUpdateCmd = `${cmd} ${args.join(" ")}`;
143
+ const updateProc = spawnSync(fullUpdateCmd, {
144
+ stdio: "inherit",
145
+ shell: true,
146
+ });
147
+
148
+ if (updateProc.status === 0) {
149
+ console.log(`\n✅ [QwenProxy] Atualizado com sucesso para a versão v${latestVersion}!`);
150
+ console.log("👉 Digite 'qpx' para iniciar a nova versão.\n");
151
+ } else {
152
+ console.error(`\n❌ [QwenProxy] Falha ao atualizar automaticamente com ${cmd}.`);
153
+ console.log(`👉 Tente executar manualmente:\n ${cmd} ${args.join(" ")}\n`);
154
+ }
155
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qwenproxy-cli",
3
- "version": "1.0.32",
3
+ "version": "1.2.0",
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": {
@@ -11,7 +11,7 @@
11
11
  "src",
12
12
  "!src/tests",
13
13
  "!src/benchmarks",
14
- "README.md",
14
+ "README*.md",
15
15
  "LICENSE"
16
16
  ],
17
17
  "scripts": {
@@ -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/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",
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/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",
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/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",
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/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",
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/models.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { createHash } from "crypto";
2
2
  import { Hono } from "hono";
3
- import { fetchQwenModels } from "../services/qwen.js";
3
+ import { fetchQwenModels, getAnyCachedQwenModels } from "../services/qwen.js";
4
+ import { DEFAULT_FALLBACK_MODELS } from "../core/model-alias.ts";
4
5
  import { loadAccounts } from "../core/accounts.ts";
5
6
  import { getAccountCooldownInfo } from "../core/account-manager.ts";
6
7
  import { getAccountsByPriority } from "../core/account-priority.ts";
@@ -141,7 +142,22 @@ async function loadModelsWithVariants(): Promise<{
141
142
  accountId?: string;
142
143
  }> {
143
144
  const accountId = getPreferredModelsAccountId();
144
- const models = (await fetchQwenModels(accountId)) as unknown as PublicModel[];
145
+ let models: PublicModel[] = [];
146
+ try {
147
+ models = (await fetchQwenModels(accountId)) as unknown as PublicModel[];
148
+ } catch {
149
+ const cached = getAnyCachedQwenModels();
150
+ if (cached && cached.length > 0) {
151
+ models = cached as unknown as PublicModel[];
152
+ } else {
153
+ models = DEFAULT_FALLBACK_MODELS.map((id) => ({
154
+ id,
155
+ object: "model",
156
+ created: MEDIA_MODELS_CREATED_AT,
157
+ owned_by: "qwen",
158
+ }));
159
+ }
160
+ }
145
161
  const expanded = expandModelVariants(models, accountId);
146
162
 
147
163
  // Advertise media generation models so clients can discover them via
package/src/api/server.ts CHANGED
@@ -3,7 +3,8 @@ import net from "node:net";
3
3
  import { v4 as uuidv4 } from "uuid";
4
4
  import { Hono, type Context } from "hono";
5
5
  import { serve } from "@hono/node-server";
6
- import { config } from "../core/config.js";
6
+ import { config, getRuntimeChatMode, setRuntimeChatMode } from "../core/config.js";
7
+ import { saveTuiSettings } from "../tui/settings.ts";
7
8
  import { metrics } from "../core/metrics.js";
8
9
  import { logger, maskEmail } from "../core/logger.js";
9
10
  import { MemoryCache } from "../cache/memory-cache.js";
@@ -248,7 +249,22 @@ app.post("/v1/upload", uploadFile);
248
249
  app.post("/v1/images/generations", imagesGenerations);
249
250
  app.post("/v1/videos/generations", videosGenerations);
250
251
  app.get("/v1/tasks/status/:taskId", videoTaskStatus);
251
-
252
+ app.get("/v1/chat/mode", (c) => {
253
+ const error = verifyApiKey(c);
254
+ if (error) return error;
255
+ return c.json({ mode: getRuntimeChatMode() });
256
+ });
257
+ app.post("/v1/chat/mode", async (c) => {
258
+ const error = verifyApiKey(c);
259
+ if (error) return error;
260
+ const body = (await c.req.json().catch(() => ({}))) as { mode?: string };
261
+ if (!body.mode) {
262
+ return c.json({ error: { message: "Field 'mode' is required" } }, 400);
263
+ }
264
+ const updated = setRuntimeChatMode(body.mode);
265
+ saveTuiSettings({ chat: { mode: updated } });
266
+ return c.json({ success: true, mode: updated });
267
+ });
252
268
  // OpenAI Responses API compatible routes
253
269
  app.route("", responsesApp);
254
270
  app.route("", anthropicApp);
@@ -122,7 +122,137 @@ export function invalidateAccountsCache(): void {
122
122
  accountsCache = null;
123
123
  accountsCacheTime = 0;
124
124
  }
125
+ export interface BatchAccountEntry {
126
+ email: string;
127
+ password: string;
128
+ }
129
+
130
+ export function parseBatchAccounts(rawInput: string): {
131
+ entries: BatchAccountEntry[];
132
+ invalid: string[];
133
+ } {
134
+ if (!rawInput || typeof rawInput !== "string") {
135
+ return { entries: [], invalid: [] };
136
+ }
137
+
138
+ let text = rawInput.trim();
139
+ // Strip optional QWEN_ACCOUNTS= prefix and surrounding quotes
140
+ text = text.replace(/^QWEN_ACCOUNTS\s*=\s*["']?/i, "").replace(/["']?\s*$/i, "");
141
+
142
+ const lines = text.split(/\r?\n/);
143
+ const entries: BatchAccountEntry[] = [];
144
+ const invalid: string[] = [];
145
+ const seenEmails = new Set<string>();
146
+
147
+ for (const rawLine of lines) {
148
+ const line = rawLine.trim();
149
+ if (!line || line.startsWith("#") || line.startsWith("//")) continue;
150
+
151
+ // Check if line contains multiple accounts separated by comma or semicolon
152
+ const segments =
153
+ (line.includes(",") || line.includes(";")) && (line.match(/@/g) || []).length > 1
154
+ ? line.split(/[,;]/).map((s) => s.trim()).filter(Boolean)
155
+ : [line];
156
+
157
+ for (const seg of segments) {
158
+ let email = "";
159
+ let password = "";
160
+
161
+ if (seg.includes("---")) {
162
+ const parts = seg.split("---");
163
+ email = parts[0].trim();
164
+ password = parts.slice(1).join("---").trim();
165
+ } else if (seg.includes("\t")) {
166
+ const parts = seg.split("\t");
167
+ email = parts[0].trim();
168
+ password = parts.slice(1).join("\t").trim();
169
+ } else if (seg.includes(" | ")) {
170
+ const parts = seg.split(" | ");
171
+ email = parts[0].trim();
172
+ password = parts.slice(1).join(" | ").trim();
173
+ } else if (seg.includes(":")) {
174
+ const colonIdx = seg.indexOf(":");
175
+ email = seg.slice(0, colonIdx).trim();
176
+ password = seg.slice(colonIdx + 1).trim();
177
+ } else if (seg.includes(",")) {
178
+ const commaIdx = seg.indexOf(",");
179
+ email = seg.slice(0, commaIdx).trim();
180
+ password = seg.slice(commaIdx + 1).trim();
181
+ } else {
182
+ invalid.push(seg);
183
+ continue;
184
+ }
185
+
186
+ // Validate email format
187
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
188
+ if (!email || !password || !emailRegex.test(email)) {
189
+ invalid.push(seg);
190
+ continue;
191
+ }
192
+
193
+ const normalizedEmail = email.toLowerCase();
194
+ if (!seenEmails.has(normalizedEmail)) {
195
+ seenEmails.add(normalizedEmail);
196
+ entries.push({ email, password });
197
+ }
198
+ }
199
+ }
200
+
201
+ return { entries, invalid };
202
+ }
125
203
 
204
+ export function addAccountsBatch(
205
+ entries: BatchAccountEntry[],
206
+ ): { added: QwenAccount[]; skipped: string[]; invalid: string[] } {
207
+ const db = getDatabase();
208
+ const added: QwenAccount[] = [];
209
+ const skipped: string[] = [];
210
+ const invalid: string[] = [];
211
+
212
+ const existingEmails = new Set(
213
+ (db.prepare("SELECT email FROM accounts").all() as Array<{ email: string }>).map((r) =>
214
+ r.email.toLowerCase(),
215
+ ),
216
+ );
217
+
218
+ const insertStmt = db.prepare(
219
+ "INSERT INTO accounts (id, email, password) VALUES (?, ?, ?)",
220
+ );
221
+
222
+ const insertBatch = db.transaction((items: BatchAccountEntry[]) => {
223
+ for (const item of items) {
224
+ const email = item.email.trim();
225
+ const password = item.password;
226
+ if (!email || !password) {
227
+ invalid.push(email || "(empty)");
228
+ continue;
229
+ }
230
+
231
+ if (existingEmails.has(email.toLowerCase())) {
232
+ skipped.push(email);
233
+ continue;
234
+ }
235
+
236
+ const newAccount: QwenAccount = {
237
+ id: crypto.randomUUID(),
238
+ email,
239
+ password,
240
+ };
241
+
242
+ insertStmt.run(newAccount.id, newAccount.email, encrypt(newAccount.password));
243
+ existingEmails.add(email.toLowerCase());
244
+ added.push(newAccount);
245
+ }
246
+ });
247
+
248
+ insertBatch(entries);
249
+
250
+ if (added.length > 0) {
251
+ invalidateAccountsCache();
252
+ }
253
+
254
+ return { added, skipped, invalid };
255
+ }
126
256
  export function addAccount(
127
257
  email: string,
128
258
  password: string,
@@ -34,7 +34,18 @@ const envSchema = z
34
34
  // temp chat (chat_mode:"local") for every request and sends the full
35
35
  // history inline (OpenAI standard). Temp chats are ephemeral and never
36
36
  // appear in the account's chat list (live-probed).
37
- QWEN_CHAT_MODE: z.enum(["thread", "temp", "temp-thread"]).default("thread"),
37
+ QWEN_CHAT_MODE: z
38
+ .enum([
39
+ "thread",
40
+ "temp",
41
+ "temp-thread",
42
+ "thread-temp",
43
+ "stateless",
44
+ "stateles",
45
+ "stateless-temp",
46
+ "stateles-temp",
47
+ ])
48
+ .default("thread"),
38
49
  PLAYWRIGHT_HEADLESS: z.string().default("true"),
39
50
  PLAYWRIGHT_BROWSER: z
40
51
  .enum(["chromium", "chrome", "edge"])
@@ -156,7 +167,7 @@ const envSchema = z
156
167
  QWEN_BROWSER_ONLY_FETCH: z.string().default("true"),
157
168
  QWEN_MAP_OPENAI_MODELS: z.string().default("true"),
158
169
  QWEN_MAX_PROMPT_BYTES: z.string().default("0"),
159
- QWEN_MAX_PERSONALIZATION_BYTES: z.string().default("200000"),
170
+ QWEN_MAX_PERSONALIZATION_BYTES: z.string().default("500000"),
160
171
  CONTEXT_METER_ENABLED: z.string().default("true"),
161
172
  CONTEXT_METER_WINDOW_TOKENS: z.string().default("0"),
162
173
  CONTEXT_METER_REPORT_USAGE: z.string().default("true"),
@@ -362,7 +373,7 @@ export const config = {
362
373
  /** When true, all requests (personalization, models, media, chat) route exclusively through the browser page (no direct Node fetch). */
363
374
  browserOnlyFetch: env.QWEN_BROWSER_ONLY_FETCH !== "false",
364
375
  mapOpenAiModels: env.QWEN_MAP_OPENAI_MODELS !== "false",
365
- chatMode: env.QWEN_CHAT_MODE,
376
+ chatMode: normalizeChatMode(env.QWEN_CHAT_MODE),
366
377
  maxPromptBytes: Math.max(0, parseInt(env.QWEN_MAX_PROMPT_BYTES)),
367
378
  maxPersonalizationBytes: Math.max(
368
379
  0,
@@ -388,5 +399,85 @@ export const config = {
388
399
 
389
400
  export type Config = typeof config;
390
401
 
391
- /** Conversation mode: thread-native reuse vs ephemeral temp chat per request vs temp-thread (ephemeral continuous session). */
392
- export type ChatMode = "thread" | "temp" | "temp-thread";
402
+ /**
403
+ * Conversation modes:
404
+ * - "thread": thread-native delta (~1KB), saved in Qwen account sidebar (default)
405
+ * - "thread-temp" (alias "temp-thread"): thread-native delta (~1KB), ephemeral local chat (not saved)
406
+ * - "stateless": full history re-sent every turn (OpenAI standard), saved in Qwen account sidebar
407
+ * - "stateless-temp" (alias "temp"): full history re-sent every turn (OpenAI standard), ephemeral local chat (not saved)
408
+ */
409
+ export type ChatMode =
410
+ | "thread"
411
+ | "thread-temp"
412
+ | "temp-thread"
413
+ | "stateless"
414
+ | "stateles"
415
+ | "stateless-temp"
416
+ | "stateles-temp"
417
+ | "temp";
418
+
419
+ export function isStatelessChatMode(mode?: string | null): boolean {
420
+ if (!mode) return false;
421
+ const m = mode.trim().toLowerCase().replace(/_/g, "-");
422
+ return (
423
+ m === "stateless" ||
424
+ m === "stateles" ||
425
+ m === "stateless-temp" ||
426
+ m === "stateles-temp" ||
427
+ m === "temp"
428
+ );
429
+ }
430
+
431
+ export function isLocalChatMode(mode?: string | null): boolean {
432
+ if (!mode) return false;
433
+ const m = mode.trim().toLowerCase().replace(/_/g, "-");
434
+ return (
435
+ m === "stateless-temp" ||
436
+ m === "stateles-temp" ||
437
+ m === "temp" ||
438
+ m === "thread-temp" ||
439
+ m === "temp-thread"
440
+ );
441
+ }
442
+
443
+ export function normalizeChatMode(
444
+ mode?: string | null,
445
+ ): "thread" | "thread-temp" | "stateless" | "stateless-temp" {
446
+ if (!mode) return "thread";
447
+ const m = mode.trim().toLowerCase().replace(/_/g, "-");
448
+ if (m === "stateless-temp" || m === "stateles-temp" || m === "temp") {
449
+ return "stateless-temp";
450
+ }
451
+ if (m === "stateless" || m === "stateles") {
452
+ return "stateless";
453
+ }
454
+ if (m === "thread-temp" || m === "temp-thread") {
455
+ return "thread-temp";
456
+ }
457
+ return "thread";
458
+ }
459
+
460
+ export function setRuntimeChatMode(
461
+ mode?: string | null,
462
+ ): "thread" | "thread-temp" | "stateless" | "stateless-temp" {
463
+ const normalized = normalizeChatMode(mode);
464
+ (config.qwen as { chatMode: "thread" | "thread-temp" | "stateless" | "stateless-temp" }).chatMode = normalized;
465
+ return normalized;
466
+ }
467
+
468
+ export function getRuntimeChatMode(): "thread" | "thread-temp" | "stateless" | "stateless-temp" {
469
+ return config.qwen.chatMode as "thread" | "thread-temp" | "stateless" | "stateless-temp";
470
+ }
471
+
472
+ export function cycleNextChatMode(): "thread" | "thread-temp" | "stateless" | "stateless-temp" {
473
+ const current = getRuntimeChatMode();
474
+ const order: Array<"thread" | "thread-temp" | "stateless-temp" | "stateless"> = [
475
+ "thread",
476
+ "thread-temp",
477
+ "stateless-temp",
478
+ "stateless",
479
+ ];
480
+ const idx = order.indexOf(current);
481
+ const next = order[(idx + 1) % order.length];
482
+ return setRuntimeChatMode(next);
483
+ }
@@ -6,6 +6,23 @@
6
6
  */
7
7
  export type ReasoningMode = "auto" | "thinking" | "fast";
8
8
 
9
+ export const DEFAULT_FALLBACK_MODELS = [
10
+ "qwen3.8-max",
11
+ "qwen3.7-plus",
12
+ "qwen3.8-omni-flash",
13
+ "qwen3.7-max",
14
+ "qwen3.6-plus",
15
+ "qwen3.5-plus",
16
+ "qwen3.5-omni-plus",
17
+ "z-image-turbo",
18
+ "qwen-image-3.0-pro",
19
+ "qwen-image-3.0",
20
+ "wan2.7-image-pro",
21
+ "wan2.7-image",
22
+ "wan3.0-video",
23
+ "wan2.7-t2v",
24
+ "wan2.7-i2v",
25
+ ];
9
26
  /**
10
27
  * Resolução de variantes de raciocínio dos modelos Qwen públicos.
11
28
  *
package/src/index.ts CHANGED
@@ -1,3 +1,6 @@
1
+ try {
2
+ process.title = "QwenProxy";
3
+ } catch {}
1
4
  process.env.DOTENV_CONFIG_QUIET = 'true'
2
5
  import dotenv from 'dotenv'
3
6
  import fs from 'node:fs'
@@ -1,11 +1,15 @@
1
1
  import { clearAllAccountCooldowns } from "./core/account-manager.ts";
2
2
  import { loadAccounts } from "./core/accounts.ts";
3
+ import { clearPersonalizationDbCache } from "./services/qwen.ts";
3
4
 
4
5
  function main() {
5
6
  const accounts = loadAccounts();
6
7
  console.log(`🔍 Checking cooldowns for ${accounts.length} configured account(s)...`);
7
8
  const cleared = clearAllAccountCooldowns();
8
- console.log(`✅ Cooldowns reset successfully: ${cleared} account(s) cleared.`);
9
+ const clearedPersonalization = clearPersonalizationDbCache();
10
+ console.log(
11
+ `✅ Cooldowns reset successfully: ${cleared} account(s) cleared. Personalization cache cleared: ${clearedPersonalization} account(s).`,
12
+ );
9
13
  }
10
14
 
11
15
  main();