qwenproxy-cli 1.1.0 → 1.2.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/README.es.md CHANGED
@@ -164,9 +164,7 @@ El sincronizador configura de forma transparente:
164
164
  - **OpenAI Codex CLI** (`~/.codex/config.toml`) — Protocolo nativo Responses (`/v1/responses`).
165
165
  - **OpenCode** (`~/.config/opencode/opencode.jsonc`) — Proveedor compatible con OpenAI.
166
166
  - **Cline, OMP, Zed, Kilo Code y Hermes Agent**.
167
-
168
- > **Consejo de Restauración:** Puedes restaurar las configuraciones originales en cualquier momento ejecutando `qpx sync -- --restore`.
169
-
167
+ > **Consejo de Restauración:** Puedes restaurar las configuraciones originales en cualquier momento ejecutando `qpx sync --restore` (o `npm run sync:restore`).
170
168
  ### 5. ¡Listo para Programar!
171
169
 
172
170
  Ejecuta tu agente favorito con total normalidad:
package/README.md CHANGED
@@ -164,9 +164,7 @@ The synchronizer configures:
164
164
  - **OpenAI Codex CLI** (`~/.codex/config.toml`) — Native Responses protocol (`/v1/responses`).
165
165
  - **OpenCode** (`~/.config/opencode/opencode.jsonc`) — OpenAI-compatible provider.
166
166
  - **Cline, OMP, Zed, Kilo Code, and Hermes Agent**.
167
-
168
- > **Rollback tip:** Restore previous client configuration backups anytime with `qpx sync -- --restore`.
169
-
167
+ > **Rollback tip:** Restore previous client configuration backups anytime with `qpx sync --restore` (or `npm run sync:restore`).
170
168
  ### 5. Start Coding!
171
169
 
172
170
  Launch your favorite coding tool as you normally would:
package/README.pt-BR.md CHANGED
@@ -321,9 +321,7 @@ O sincronizador detecta e configura automaticamente:
321
321
  - **OpenAI Codex CLI** (`~/.codex/config.toml`) — Usa o protocolo nativo Responses (`/v1/responses`).
322
322
  - **OpenCode** (`~/.config/opencode/opencode.jsonc`) — Configura provider OpenAI-compatible.
323
323
  - **Cline, OMP, Zed, Kilo Code e Hermes Agent**.
324
-
325
- > **Dica de Rollback:** Se quiser desfazer a configuração e restaurar os arquivos originais a qualquer momento, execute `qpx sync -- --restore`.
326
-
324
+ > **Dica de Rollback:** Se quiser desfazer a configuração e restaurar os arquivos originais a qualquer momento, execute `qpx sync --restore` (ou `npm run sync:restore`).
327
325
  ### 5. Pronto para Trabalhar!
328
326
 
329
327
  Agora basta abrir o seu agente favorito normalmente:
@@ -501,7 +499,7 @@ npm run typecheck # tipos
501
499
  | `DELETE_ALL_CHATS_ON_SHUTDOWN` | `false` | Limpa chats no shutdown |
502
500
  | `QWEN_PERSONALIZATION_FROM_REQUEST` | `true` | Envia system + tools via `/settings/personalization` |
503
501
  | `QWEN_PERSONALIZATION_VERIFY_GET` | `true` | Confirma personalization com GET |
504
- | `QWEN_MAX_PERSONALIZATION_BYTES` | `200000` | Teto UTF-8 para personalization por request; acima disso as instruções seguem inline |
502
+ | `QWEN_MAX_PERSONALIZATION_BYTES` | `500000` | Teto UTF-8 para personalization por request (500KB); acomoda centenas de MCP tools com folga |
505
503
  | `QWEN_CHAT_POOL_SIZE` | `1` | Warm pool de chats por modelo; fica desativado quando personalization por request está ativa |
506
504
  | `QWEN_CHAT_POOL_MODELS` | `qwen3.7-plus` | Modelos aquecidos no warm pool |
507
505
  | `QWEN_CHAT_MODE` | `thread` | Modo de conversa padrão: `thread`, `thread-temp`, `stateless` ou `stateless-temp`. Override dinâmico via TUI (`M`/`F4`), HTTP (`/v1/chat/mode`) ou header `X-QwenProxy-Chat-Mode`. |
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.1.0",
3
+ "version": "1.2.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/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",
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",
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
@@ -167,7 +167,7 @@ const envSchema = z
167
167
  QWEN_BROWSER_ONLY_FETCH: z.string().default("true"),
168
168
  QWEN_MAP_OPENAI_MODELS: z.string().default("true"),
169
169
  QWEN_MAX_PROMPT_BYTES: z.string().default("0"),
170
- QWEN_MAX_PERSONALIZATION_BYTES: z.string().default("200000"),
170
+ QWEN_MAX_PERSONALIZATION_BYTES: z.string().default("500000"),
171
171
  CONTEXT_METER_ENABLED: z.string().default("true"),
172
172
  CONTEXT_METER_WINDOW_TOKENS: z.string().default("0"),
173
173
  CONTEXT_METER_REPORT_USAGE: z.string().default("true"),
@@ -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
  *
@@ -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();
@@ -130,7 +130,7 @@ export async function recoverBaxiaCaptcha(
130
130
  accountId,
131
131
  (page) => solveChallengeOnPage(page, challengeUrl),
132
132
  solverOperationTimeoutMs,
133
- Math.min(config.timeouts.page, 5_000),
133
+ solverOperationTimeoutMs,
134
134
  false,
135
135
  );
136
136
 
@@ -2335,19 +2335,30 @@ export async function captureQwenHeaders(
2335
2335
  1,
2336
2336
  Math.min(CHAT_INPUT_ACTION_TIMEOUT_MS, remainingBudgetMs()),
2337
2337
  );
2338
- try {
2339
- await page.focus(inputSelector, { timeout: inputActionTimeoutMs });
2340
- if (settled || page.isClosed()) return;
2341
- await page.fill(inputSelector, "", { timeout: inputActionTimeoutMs });
2338
+ let interactionSucceeded = false;
2339
+ for (let interactionTry = 1; interactionTry <= 2; interactionTry++) {
2342
2340
  if (settled || page.isClosed()) return;
2343
- await page.type(inputSelector, "a", {
2344
- delay: 100,
2345
- timeout: inputActionTimeoutMs,
2346
- });
2347
- } catch {
2341
+ try {
2342
+ await page.focus(inputSelector, { timeout: inputActionTimeoutMs });
2343
+ if (settled || page.isClosed()) return;
2344
+ await page.fill(inputSelector, "", { timeout: inputActionTimeoutMs });
2345
+ if (settled || page.isClosed()) return;
2346
+ await page.type(inputSelector, "a", {
2347
+ delay: 100,
2348
+ timeout: inputActionTimeoutMs,
2349
+ });
2350
+ interactionSucceeded = true;
2351
+ break;
2352
+ } catch {
2353
+ if (settled || page.isClosed()) return;
2354
+ if (interactionTry === 1) {
2355
+ await sleep(300);
2356
+ }
2357
+ }
2358
+ }
2359
+ if (!interactionSucceeded) {
2348
2360
  // The input detached mid-interaction (challenge overlay, SPA
2349
- // re-render): same diagnosis as never appearing — only a fresh load
2350
- // recovers it.
2361
+ // re-render): only a fresh load recovers it.
2351
2362
  if (settled || page.isClosed()) return;
2352
2363
  console.warn(
2353
2364
  `⏱️ [Playwright] Chat input interaction failed for ${accountId} (attempt ${attempt}); reloading`,
@@ -440,6 +440,14 @@ const modelsCache = new Map<
440
440
  { models: PublicQwenModel[]; fetchedAt: number }
441
441
  >();
442
442
 
443
+ export function getAnyCachedQwenModels(): PublicQwenModel[] | undefined {
444
+ for (const entry of modelsCache.values()) {
445
+ if (entry.models && entry.models.length > 0) {
446
+ return entry.models;
447
+ }
448
+ }
449
+ return undefined;
450
+ }
443
451
  const nativeToolsDisabled = new Set<string>();
444
452
  const disablingNativeToolsInProgress = new Set<string>();
445
453
  const lastSyncedPersonalizationHashes = new Map<string, string>();
@@ -492,6 +500,25 @@ function setPersonalizationHashInDb(accountId: string, hash: string): void {
492
500
  );
493
501
  }
494
502
  }
503
+ export function clearPersonalizationDbCache(accountId?: string): number {
504
+ try {
505
+ const db = getDatabase();
506
+ if (accountId) {
507
+ const info = db
508
+ .prepare("DELETE FROM personalization_cache WHERE account_id = ?")
509
+ .run(accountId);
510
+ lastSyncedPersonalizationHashes.delete(accountId);
511
+ activePersonalizationByAccount.delete(accountId);
512
+ return info.changes;
513
+ }
514
+ const info = db.prepare("DELETE FROM personalization_cache").run();
515
+ lastSyncedPersonalizationHashes.clear();
516
+ activePersonalizationByAccount.clear();
517
+ return info.changes;
518
+ } catch {
519
+ return 0;
520
+ }
521
+ }
495
522
 
496
523
  function shortContentHash(value: string): string {
497
524
  return crypto.createHash("sha256").update(value).digest("hex").slice(0, 12);
@@ -666,23 +693,14 @@ export function buildQwenSettingsUpdatePayload(
666
693
  currentSettings: any,
667
694
  instruction: string,
668
695
  ): Record<string, unknown> {
669
- // The real client (HAR networkv2) POSTs ONLY `{personalization: {...}}` to
670
- // /api/v2/users/user/settings/update. Live probes confirmed the personalization
671
- // object accepts the GET-personalization spread + enable_for_new_chat, but the
672
- // FULL-settings spread this used to send (ui/memory/tools_enabled + every GET
673
- // field like tts_speaker_v2, code_settings, manage_cookies) is rejected with
674
- // RequestValidationError. Safe-settings are applied by disableNativeTools as
675
- // their own combined partial POST (probe-accepted). NOTE: the persistent
676
- // RequestValidationError that haunted the sync was NOT the payload — it was a
677
- // missing Content-Type header (attemptPost received the raw getQwenHeaders
678
- // map); the body was not parsed as a JSON object ("Field '': Input should be
679
- // a valid dictionary...").
680
696
  const currentPersonalization =
681
697
  currentSettings?.personalization &&
682
698
  typeof currentSettings.personalization === "object"
683
699
  ? currentSettings.personalization
684
700
  : {};
685
701
 
702
+ const hasInstruction = instruction.trim().length > 0;
703
+
686
704
  return {
687
705
  personalization: {
688
706
  ...currentPersonalization,
@@ -693,7 +711,7 @@ export function buildQwenSettingsUpdatePayload(
693
711
  : currentPersonalization.description,
694
712
  style: null,
695
713
  instruction,
696
- enable_for_new_chat: true,
714
+ enable_for_new_chat: hasInstruction,
697
715
  },
698
716
  };
699
717
  }
@@ -1519,7 +1537,12 @@ export async function syncQwenRequestPersonalization(
1519
1537
  }
1520
1538
 
1521
1539
  // 2. Check DB cache (survives restarts) (skipped on forceSync)
1522
- if (!bypassCache && syncHash && !cachedHash) {
1540
+ const isEmptyInstruction = instruction.trim().length === 0;
1541
+
1542
+ // 2. Check DB cache (survives restarts) (skipped on forceSync)
1543
+ // For empty instructions, do not blindly trust DB cache without verifying
1544
+ // because external agents or web sessions might have altered personalization.
1545
+ if (!bypassCache && syncHash && !cachedHash && !isEmptyInstruction) {
1523
1546
  const dbHash = getPersonalizationHashFromDb(cacheKey);
1524
1547
  if (dbHash === syncHash) {
1525
1548
  lastSyncedPersonalizationHashes.set(cacheKey, syncHash);
@@ -1528,7 +1551,6 @@ export async function syncQwenRequestPersonalization(
1528
1551
  return true;
1529
1552
  }
1530
1553
  }
1531
-
1532
1554
  let existing = { chars: null, bytes: null, hash: null } as ReturnType<
1533
1555
  typeof textSize
1534
1556
  >;
@@ -1544,7 +1566,9 @@ export async function syncQwenRequestPersonalization(
1544
1566
  );
1545
1567
  currentSettings = existingJson?.data ?? null;
1546
1568
  payload = buildQwenSettingsUpdatePayload(currentSettings, instruction);
1547
- existing = textSize(existingJson?.data?.personalization?.instruction);
1569
+ const existingInstruction = existingJson?.data?.personalization?.instruction;
1570
+ const existingEnabled = existingJson?.data?.personalization?.enable_for_new_chat === true;
1571
+ existing = textSize(existingInstruction);
1548
1572
  const existingSafeSettingsApplied =
1549
1573
  existingJson?.data?.ui?.largeTextAsFile === false &&
1550
1574
  existingJson?.data?.ui?.splitLargeChunks === false &&
@@ -1554,8 +1578,12 @@ export async function syncQwenRequestPersonalization(
1554
1578
  existingJson?.data?.memory?.enable_history_memory === false &&
1555
1579
  existingJson?.data?.tools_enabled?.web_search === false &&
1556
1580
  existingJson?.data?.tools_enabled?.code_interpreter === false;
1557
- if (existing.hash === sent.hash && existingSafeSettingsApplied) {
1558
- lastSyncedPersonalizationHashes.set(cacheKey, syncHash);
1581
+ const isEmptyAndCleared =
1582
+ isEmptyInstruction &&
1583
+ (!existingInstruction || existingInstruction.trim().length === 0) &&
1584
+ !existingEnabled;
1585
+ const isContentMatched = existing.hash !== null && existing.hash === sent.hash;
1586
+ if ((isContentMatched || isEmptyAndCleared) && existingSafeSettingsApplied) {
1559
1587
  setPersonalizationHashInDb(cacheKey, syncHash);
1560
1588
  rememberActivePersonalization(
1561
1589
  cacheKey,
@@ -1669,6 +1697,7 @@ export async function syncQwenRequestPersonalization(
1669
1697
  typeof textSize
1670
1698
  >;
1671
1699
 
1700
+ let verifyData: any = null;
1672
1701
  if (config.qwen.personalizationVerifyGet) {
1673
1702
  const { json: verifyJson } =
1674
1703
  await requestQwenPersonalizationInBrowser(
@@ -1677,11 +1706,18 @@ export async function syncQwenRequestPersonalization(
1677
1706
  "/api/v2/users/user/settings",
1678
1707
  requestHeaders,
1679
1708
  );
1680
- stored = textSize(verifyJson?.data?.personalization?.instruction);
1681
- }
1682
-
1683
- const matchReturned = returned.hash !== null && returned.hash === sent.hash;
1684
- const matchStored = stored.hash === null ? null : stored.hash === sent.hash;
1709
+ verifyData = verifyJson?.data?.personalization;
1710
+ stored = textSize(verifyData?.instruction);
1711
+ }
1712
+
1713
+ const matchReturned =
1714
+ (isEmptyInstruction && (!returnedInstruction || returned.chars === 0)) ||
1715
+ (returned.hash !== null && returned.hash === sent.hash);
1716
+ const matchStored =
1717
+ stored.hash === null
1718
+ ? null
1719
+ : (isEmptyInstruction && (!verifyData?.instruction || stored.chars === 0)) ||
1720
+ stored.hash === sent.hash;
1685
1721
  const applied = matchReturned || matchStored === true;
1686
1722
  if (syncHash && applied) {
1687
1723
  lastSyncedPersonalizationHashes.set(cacheKey, syncHash);