qwenproxy-cli 1.0.3 → 1.0.5

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.md CHANGED
@@ -7,9 +7,10 @@
7
7
  Gateway e API de alta performance compatível com **OpenAI** e **Anthropic** que conecta clientes e agentes (Codex, Claude Code CLI, Grok, Cursor) ao **Qwen (`chat.qwen.ai`)** com suporte a múltiplas contas, failover inteligente, tool calling robusto, thread-native, geração de fotos e vídeos, **Responses API completa com memória persistente** e sessões persistentes. Inclui Playwright com stealth, retries para erros transitórios, variantes públicas base/`-fast`/`-thinking`, cache comprimido, registro de capabilities por modelo e observabilidade.
8
8
 
9
9
  [![CI](https://github.com/johngbl/QwenProxy/actions/workflows/ci.yml/badge.svg)](https://github.com/johngbl/QwenProxy/actions/workflows/ci.yml)
10
+ [![npm version](https://img.shields.io/npm/v/qwenproxy-cli.svg)](https://www.npmjs.com/package/qwenproxy-cli)
10
11
  [![TypeScript](https://img.shields.io/badge/TypeScript-7.0-blue)](https://www.typescriptlang.org/)
11
12
  [![Hono](https://img.shields.io/badge/Hono-4.13-green)](https://hono.dev/)
12
- [![Playwright](https://img.shields.io/badge/Playwright-1.62-blueviolet)](https://playwright.dev/)
13
+ [![Patchright](https://img.shields.io/badge/Patchright-Stealth-blueviolet)](https://github.com/kaliiiiiiiiii/patchright)
13
14
  [![License: ISC](https://img.shields.io/badge/License-ISC-yellow.svg)](LICENSE)
14
15
  [![GitHub Sponsors](https://img.shields.io/badge/sponsor-GitHub%20Sponsors-ea4aaa?logo=githubsponsors&logoColor=white)](https://github.com/sponsors/johngbl)
15
16
  [![Ko-fi](https://img.shields.io/badge/Donate-Ko--fi-ff5e5b?logo=kofi&logoColor=white)](https://ko-fi.com/johngbl)
@@ -29,10 +30,11 @@ Toda contribuição é muito bem-vinda e ajuda a cobrir custos de infraestrutura
29
30
  - **Compatibilidade OpenAI & Anthropic** — `/v1/chat/completions`, `/v1/completions` (legado), `/v1/models`, `/v1/messages` (**Anthropic Messages API** nativa com suporte total a **Claude Code CLI** e `@anthropic-ai/sdk`), `/v1/messages/count_tokens` e **Responses API** `/v1/responses`.
30
31
  - **Responses API completa** — SSE com `event:` + `data:` + `sequence_number`, memória persistente via `previous_response_id` (SQLite durável), `last_response_id`, multimodal (`input_image`/`input_file`), reasoning effort normalization, lifecycle events de reasoning e usage real do upstream.
31
32
  - **Thread-native** — Reutiliza sessão/pai no Qwen; preservação de contexto entre turns
32
- - **Dois modos de conversa** — `thread` (default, reutiliza chat e envia delta) e `temp` (novo chat temporário `chat_mode:"local"` por request, envia histórico completo; zero `chat_in_progress` e zero chats órfãos)
33
- - **Playwright + stealth** — Headers reais (`bx-ua`, `bx-umidtoken`, `bx-v`) por conta; fingerprint estável e cleanup de processos.
34
- - **Transporte Qwen via Chromium** — No fluxo principal de chat, modelos, criação de sessão, personalização, completion e stop usam o contexto Playwright; o completion o `ReadableStream` incrementalmente e preserva o SSE sem bufferizar a resposta inteira.
35
- - **Startup rápido multi-conta** — Sobe com a **primeira conta pronta**; as demais continuam preparando em background.
33
+ - **Três modos de conversa** — `thread` (default, encadeamento nativo e delta de turns), `temp-thread` (recomendado para chat/TUI, efêmero `chat_mode:"local"` com contexto contínuo, zero poluição no `chat.qwen.ai`), e `temp` (stateless padrão OpenAI, novo chat efêmero por requisição).
34
+ - **Instância única de Chromium ultra-leve** — 1 único processo de navegador compartilhado para todas as contas com isolamento seguro de `BrowserContext` e `storageState` JSON, consumindo ~200MB de RAM (economia de >65%).
35
+ - **Dashboard TUI completo & responsivo** — Interface visual interativa no terminal (`qpx`) com monitoramento em tempo real, abas dedicadas para Chat, Sincronização de Agentes, Diagnóstico de Armazenamento, Gerenciamento de Contas e Logs, com confirmações de segurança para ações irreversíveis.
36
+ - **Atualizador inteligente embutido** — Comando `qpx update` que detecta automaticamente seu gerenciador de pacotes (`npm`, `pnpm`, `bun`, `yarn`) e atualiza com um único comando.
37
+ - **Startup sob demanda e multi-conta** — Sobe instantaneamente com a **primeira conta pronta**; as contas reservas ficam em repouso (*Standby*) e inicializam sem esforço apenas sob demanda.
36
38
  - **Retries resilientes** — 502/503/504, erros de rede (`fetch failed`), anti-bot, quota e `invalid_input` com recriação de chat.
37
39
  - **Parser de tools robusto** — stream fragmentado, JSON malformado, fuzzy de nomes (`readFile` → `read_file`), JSON duplamente escapado e `</tool_call>` case-insensitive.
38
40
  - **Personalization sync** — system + tools completos são sincronizados em `/settings/personalization` via `POST /api/v2/users/user/settings/update`; o cache por conteúdo evita updates repetidos e instruções acima do limite seguem inline; aplica settings seguras (`largeTextAsFile=false`, memory off, tools internas off).
@@ -86,19 +88,19 @@ Se `API_KEY` estiver definido, as rotas `/v1/*` (e `/metrics`) exigem uma das fo
86
88
  - `Authorization: Bearer <API_KEY>` (OpenAI / Responses)
87
89
  - `x-api-key: <API_KEY>` (clients bearer-style)
88
90
 
89
- QwenProxy usa **Playwright por padrão**. Cada conta abre uma sessão real de browser para capturar cookies e headers anti-bot.
91
+ QwenProxy utiliza **Patchright com arquitetura de navegador compartilhado**. Um único processo Chromium é mantido aberto com aceleração WebGL ativa, enquanto cada conta opera em um `BrowserContext` isolado com persistência leve em `storage_state.json` (~200MB de RAM para todas as contas).
90
92
 
91
93
  ```env
92
94
  PLAYWRIGHT_HEADLESS=true
93
95
  PLAYWRIGHT_BROWSER=chromium
94
96
  ```
95
97
 
96
- **Requisitos:**
98
+ **Instalação do navegador:**
99
+ O Chromium é instalado automaticamente na primeira execução de `qpx`. Se desejar instalar manualmente:
97
100
 
98
101
  ```bash
99
- npx playwright install chromium
102
+ npx patchright install chromium
100
103
  ```
101
-
102
104
  Senhas das contas são armazenadas **criptografadas** no SQLite (`data/`).
103
105
 
104
106
  ### Transporte upstream e streaming
@@ -272,6 +274,19 @@ qpx
272
274
  ```
273
275
  *(Abre diretamente o dashboard interativo da TUI com o servidor e proxy integrados).*
274
276
 
277
+ #### Comandos Rápidos do CLI (`qpx`)
278
+
279
+ | Comando | Descrição |
280
+ | :--- | :--- |
281
+ | `qpx` *(ou `qwenproxy`)* | Abre o dashboard visual interativo da TUI com servidor proxy integrado |
282
+ | `qpx start` *(ou `--server`)* | Inicia apenas o servidor HTTP/SSE em modo headless (sem interface gráfica) |
283
+ | `qpx update` | Verifica e atualiza o QwenProxy automaticamente via npm/pnpm/bun |
284
+ | `qpx login` | Abre navegador visível para autenticar novas contas interativamente |
285
+ | `qpx sync` | Configura e sincroniza clientes (Claude Code, Codex, OpenCode, OMP) |
286
+ | `qpx clean` | Limpa caches temporários dos perfis Chromium (~4.5MB por conta) |
287
+ | `qpx clean:all` | Limpa caches e remove versões antigas de navegadores órfãos em disco |
288
+ | `qpx purge` | Limpa o histórico de conversas remotas no Qwen de todas as contas |
289
+ | `qpx reset` | Reseta cooldowns e rate limits salvos no banco de dados |
275
290
  ### Opção 2: Execução Instantânea (Zero Instalação)
276
291
  Experimente ou execute pontualmente sem instalar nada permanentemente:
277
292
  ```bash
@@ -317,14 +332,10 @@ npm start
317
332
 
318
333
  > **Nota:** o servidor não inicia sem pelo menos uma conta configurada (via `.env`/`QWEN_ACCOUNTS`, `npm run login` ou banco de contas).
319
334
 
320
- ### Startup multi-conta
321
-
322
- 1. Prepara as contas em sequência, reutilizando o profile persistente quando ele está autenticado.
323
- 2. Se o profile não tiver uma sessão válida, autentica com as credenciais da conta e salva a sessão em `data/qwen_profiles/<accountId>`.
324
- 3. O servidor sobe após a primeira conta ficar pronta e continua preparando as demais em background.
325
- 4. Com `PLAYWRIGHT_MAX_ACTIVE_CONTEXTS=2` (padrão), 2 contextos ficam abertos após o warmup ({principal + reserva}, cobrindo o failover comum); contextos extras (uso simultâneo ou failover) fecham ao ficar idle. O watchdog RSS fecha contextos idle sob pressão de RAM.
326
- 5. Use `PLAYWRIGHT_PREPARE_ALL_ON_STARTUP=false` para voltar ao modo econômico, preparando as contas adicionais somente quando forem necessárias.
327
-
335
+ 1. O servidor inicia instantaneamente com a **primeira conta pronta** para responder requisições imediatamente.
336
+ 2. Com `PLAYWRIGHT_PREPARE_ALL_ON_STARTUP=false` (padrão econômico), as contas adicionais permanecem em **Standby**, com credenciais validadas no banco, sendo inicializadas apenas sob demanda (failover ou rotação), poupando RAM e CPU.
337
+ 3. Todas as contas ativas compartilham a mesma instância Chromium única, mantendo sessões isoladas via `BrowserContext` e arquivos leves de estado `storage_state.json`.
338
+ 4. O watchdog RSS do sistema monitora a pressão de memória e fecha contextos ociosos automaticamente.
328
339
  Exemplo de log:
329
340
 
330
341
  ```text
@@ -379,7 +390,7 @@ npm run typecheck # tipos
379
390
  | `QWEN_MAX_PERSONALIZATION_BYTES` | `200000` | Teto UTF-8 para personalization por request; acima disso as instruções seguem inline |
380
391
  | `QWEN_CHAT_POOL_SIZE` | `1` | Warm pool de chats por modelo; fica desativado quando personalization por request está ativa |
381
392
  | `QWEN_CHAT_POOL_MODELS` | `qwen3.7-plus` | Modelos aquecidos no warm pool |
382
- | `QWEN_CHAT_MODE` | `thread` | Modo de conversa: `thread` (reutiliza o chat upstream via `parent_id` e envia o delta) ou `temp` (cria um chat temporário `chat_mode:"local"` a cada request e envia o histórico completo). Override por request via header `X-QwenProxy-Chat-Mode: thread/temp` |
393
+ | `QWEN_CHAT_MODE` | `thread` | Modo de conversa: `thread` (reutiliza o chat upstream via `parent_id` e envia o delta), `temp-thread` (chat efêmero `chat_mode:"local"` com contexto contínuo, zero poluição no Qwen Web) ou `temp` (stateless padrão OpenAI, novo chat efêmero por requisição). Override por request via header `X-QwenProxy-Chat-Mode: thread/temp/temp-thread` |
383
394
 
384
395
 
385
396
  ### Playwright / processos
@@ -390,14 +401,14 @@ npm run typecheck # tipos
390
401
  | `PLAYWRIGHT_HEADLESS` | `true` | Browser sem janela |
391
402
  | `PLAYWRIGHT_BROWSER` | `chromium` | `chromium` / `chrome` / `edge` |
392
403
  | `PLAYWRIGHT_INIT_BATCH_SIZE` | `1` | Contas em paralelo no background init |
393
- | `PLAYWRIGHT_PREPARE_ALL_ON_STARTUP` | `true` | Prepara todas as contas no boot (`false` = quando necessárias) |
404
+ | `PLAYWRIGHT_PREPARE_ALL_ON_STARTUP` | `false` | Prepara somente a primeira conta no boot (`false` = modo econômico sob demanda; `true` = aquece todas) |
394
405
  | `PLAYWRIGHT_MAX_ACTIVE_CONTEXTS` | `2` | Contextos idle mantidos quentes ({principal + reserva}); streams ativos nunca são fechados; uso simultâneo abre mais. Contas em cooldown (rate limit) ficam idle e são evictadas |
395
406
  | `PLAYWRIGHT_CONTEXT_CLOSE_TIMEOUT_MS` | `10000` | Timeout de close antes do kill |
396
407
  | `PLAYWRIGHT_IDLE_CONTEXT_TTL_MS` | `60000` | Fecha contextos idle acima do cap (`0` desativa) |
397
408
  | `PLAYWRIGHT_JS_HEAP_MB` | `256` | Cap V8 do Chromium (`--max-old-space-size`) |
398
409
  | `PLAYWRIGHT_LOW_MEMORY_FLAGS` | `true` | Flags de baixa RAM (heap cap, cache mínimo, renderer limit) |
399
410
  | `OSS_MULTIPART_THRESHOLD_MB` | `5` | Acima disso usa multipart OSS; abaixo `putStream` |
400
- | `SESSION_KEEP_ALIVE_ENABLED` | `false` | Keep-alive opt-in (evita Chromes permanentes) |
411
+ | `SESSION_KEEP_ALIVE_ENABLED` | `true` | Simula navegações leves periódicas a cada 3min para manter sessões ativas sem desconectar |
401
412
  | `SESSION_KEEP_ALIVE_INTERVAL_MS` | `180000` | Intervalo do ciclo de keep-alive/cleanup |
402
413
  | `SESSION_KEEP_ALIVE_IDLE_MS` | `120000` | Idle mínimo para keep-alive |
403
414
  | `SESSION_KEEP_ALIVE_NAVIGATION_INTERVAL_MS` | `480000` | Intervalo de navegação leve |
@@ -830,16 +841,16 @@ QwenProxy/
830
841
 
831
842
  | Comando | Descrição |
832
843
  | ------------------- | ----------------------------------------------------------------------- |
833
- | `npm start` | Iniciar o servidor QwenProxy |
844
+ | `npm run tui` | Abrir o dashboard interativo da TUI com proxy embutido |
845
+ | `npm start` | Iniciar apenas o servidor QwenProxy em modo headless |
834
846
  | `npm run sync` | Sincronizar clientes (Claude Code, Codex, OpenCode, OMP) com backup |
835
847
  | `npm run clean` | Limpar caches temporários dos perfis Chromium (~4.5MB por conta) |
836
- | `npm run clean:all` | Limpar caches + remover navegadores órfãos e versões antigas no SSD |
848
+ | `npm run clean:all` | Limpar caches + remover navegadores órfãos e versões antigas em disco |
837
849
  | `npm run reset` | Zerar cooldowns de contas no banco de dados |
838
850
  | `npm run login` | Adicionar/autenticar novas contas visualmente no navegador |
839
851
  | `npm run purge` | Limpar chats remotos do Qwen nas contas configuradas |
840
852
  | `npm test` | Executar suíte de testes completa |
841
853
  | `npm run typecheck` | Checagem estrita de tipos do TypeScript |
842
-
843
854
  ---
844
855
 
845
856
  ## Scripts de instalação, início e atualização
package/bin/qwenproxy.js CHANGED
@@ -115,19 +115,40 @@ try {
115
115
  tsxLoaderArg = pathToFileURL(tsxEntry).href;
116
116
  } catch {}
117
117
 
118
- // Ensure Playwright Chromium is installed for first-time global users
119
- try {
120
- const { chromium } = await import("patchright");
121
- const execPath = chromium.executablePath();
122
- if (!fs.existsSync(execPath)) {
123
- console.log("⏳ [QwenProxy] Instalando o navegador Chromium pela primeira vez...");
124
- spawnSync("npx", ["patchright", "install", "chromium"], {
125
- stdio: "inherit",
126
- shell: true,
127
- });
128
- console.log("✓ [QwenProxy] Navegador instalado com sucesso!\n");
129
- }
130
- } catch {}
118
+ // Ensure Playwright Chromium is installed only for commands that need the browser
119
+ const browserCommands = ["start", "tui", "login"];
120
+ const isBrowserCommand =
121
+ !firstArg ||
122
+ browserCommands.includes(firstArg) ||
123
+ rawArgs.includes("--tui") ||
124
+ rawArgs.includes("--server");
125
+
126
+ if (isBrowserCommand) {
127
+ try {
128
+ const { chromium } = await import("patchright");
129
+ const execPath = chromium.executablePath();
130
+ if (!fs.existsSync(execPath)) {
131
+ console.log("⏳ [QwenProxy] Instalando o navegador Chromium pela primeira vez...");
132
+ let cliPath = "";
133
+ try {
134
+ const patchrightEntry = require.resolve("patchright");
135
+ cliPath = path.join(path.dirname(patchrightEntry), "cli.js");
136
+ } catch {}
137
+
138
+ if (cliPath && fs.existsSync(cliPath)) {
139
+ spawnSync(process.execPath, [cliPath, "install", "chromium"], {
140
+ stdio: "inherit",
141
+ });
142
+ } else {
143
+ const cmd = process.platform === "win32" ? "npx.cmd" : "npx";
144
+ spawnSync(cmd, ["--yes", "patchright", "install", "chromium"], {
145
+ stdio: "inherit",
146
+ });
147
+ }
148
+ console.log("✓ [QwenProxy] Navegador instalado com sucesso!\n");
149
+ }
150
+ } catch {}
151
+ }
131
152
 
132
153
  const child = spawn(process.execPath, ["--import", tsxLoaderArg, targetPath, ...scriptArgs], {
133
154
  stdio: "inherit",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qwenproxy-cli",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
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": {
package/src/api/server.ts CHANGED
@@ -597,6 +597,7 @@ export async function stopServer(): Promise<void> {
597
597
 
598
598
  export async function startServer(options?: {
599
599
  installSignalHandlers?: boolean;
600
+ showBanner?: boolean;
600
601
  }): Promise<StartedServerInfo> {
601
602
  if (server) {
602
603
  if (options?.installSignalHandlers !== false) installSignalHandlers();
@@ -829,7 +830,8 @@ export async function startServer(options?: {
829
830
 
830
831
  const endpoint = `${started.url}/v1`;
831
832
 
832
- console.log(`
833
+ if (options?.showBanner !== false) {
834
+ console.log(`
833
835
  +${"-".repeat(W)}+
834
836
  |${blank()}|
835
837
  |${center("QwenProxy")}|
@@ -845,6 +847,7 @@ export async function startServer(options?: {
845
847
  |${blank()}|
846
848
  +${"-".repeat(W)}+
847
849
  `);
850
+ }
848
851
  return started;
849
852
  })();
850
853
 
@@ -3,7 +3,7 @@ import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import os from "node:os";
5
5
  import { chromium } from "patchright";
6
- import { pruneAllPlaywrightProfiles } from "./services/playwright.ts";
6
+ import { pruneAllPlaywrightProfiles, cleanupOrphanProfiles } from "./services/playwright.ts";
7
7
 
8
8
  export function formatBytes(bytes: number): string {
9
9
  if (bytes < 1024) return `${bytes} B`;
@@ -141,13 +141,19 @@ async function main() {
141
141
  // 1. Profile Transient Cache Pruning (V8 Code Cache, GPU Cache)
142
142
  console.log("1. Limpando caches transitórios dos perfis (data/qwen_profiles/)...");
143
143
  const profileResult = pruneAllPlaywrightProfiles();
144
- if (profileResult.totalFreedFiles > 0) {
145
- console.log(
146
- ` [OK] Perfis limpos com sucesso!`,
147
- );
148
- console.log(
149
- ` Espaço liberado: ${formatBytes(profileResult.totalFreedBytes)} em ${profileResult.totalFreedFiles} arquivos (${profileResult.profilesCleaned} perfil(is)).`,
150
- );
144
+ const orphanResult = cleanupOrphanProfiles();
145
+ if (profileResult.totalFreedFiles > 0 || orphanResult.removedCount > 0) {
146
+ console.log(` [OK] Perfis limpos com sucesso!`);
147
+ if (profileResult.totalFreedFiles > 0) {
148
+ console.log(
149
+ ` Espaço liberado: ${formatBytes(profileResult.totalFreedBytes)} em ${profileResult.totalFreedFiles} arquivos (${profileResult.profilesCleaned} perfil(is)).`,
150
+ );
151
+ }
152
+ if (orphanResult.removedCount > 0) {
153
+ console.log(
154
+ ` Perfis órfãos removidos: ${orphanResult.removedCount} (${formatBytes(orphanResult.freedBytes)} liberados).`,
155
+ );
156
+ }
151
157
  console.log(
152
158
  ` (Todos os cookies, sessões e logins foram 100% preservados!)`,
153
159
  );
@@ -172,13 +178,13 @@ async function main() {
172
178
  for (const d of browserResult.unusedDirs) {
173
179
  console.log(` - ${d.name} (${d.size})`);
174
180
  }
175
- console.log(` Total recuperado no SSD: ${formatBytes(browserResult.freedBytes)}!`);
181
+ console.log(` Total recuperado em disco: ${formatBytes(browserResult.freedBytes)}!`);
176
182
  } else {
177
- console.log(` [INFO] Encontrados ${browserResult.unusedDirs.length} navegador(es) legados/não utilizados no seu SSD:`);
183
+ console.log(` [INFO] Encontrados ${browserResult.unusedDirs.length} navegador(es) legados/não utilizados em disco:`);
178
184
  for (const d of browserResult.unusedDirs) {
179
185
  console.log(` - ${d.name} (${d.size})`);
180
186
  }
181
- console.log(` Espaço recuperável no SSD: ${formatBytes(totalReclaimable)}.`);
187
+ console.log(` Espaço recuperável em disco: ${formatBytes(totalReclaimable)}.`);
182
188
  console.log(` Para liberar esse espaço automaticamente, execute:`);
183
189
  console.log(` npm run clean:all\n`);
184
190
  }
@@ -34,7 +34,7 @@ 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"]).default("thread"),
37
+ QWEN_CHAT_MODE: z.enum(["thread", "temp", "temp-thread"]).default("thread"),
38
38
  PLAYWRIGHT_HEADLESS: z.string().default("true"),
39
39
  PLAYWRIGHT_BROWSER: z
40
40
  .enum(["chromium", "chrome", "edge"])
@@ -379,5 +379,5 @@ export const config = {
379
379
 
380
380
  export type Config = typeof config;
381
381
 
382
- /** Conversation mode: thread-native reuse vs ephemeral temp chat per request. */
383
- export type ChatMode = "thread" | "temp";
382
+ /** Conversation mode: thread-native reuse vs ephemeral temp chat per request vs temp-thread (ephemeral continuous session). */
383
+ export type ChatMode = "thread" | "temp" | "temp-thread";
package/src/index.ts CHANGED
@@ -55,7 +55,11 @@ if (isTui) {
55
55
  const message = error instanceof Error ? error.message : String(error)
56
56
  // Expected configuration errors are already formatted with an emoji and
57
57
  // actionable guidance; print only the message to avoid leaking stack traces.
58
- if (message.includes('[Server]')) {
58
+ if (message.includes('No Qwen accounts configured')) {
59
+ console.error(message)
60
+ console.log('\n👉 Dica: Execute a interface interativa com "qpx" (ou "npm run tui") para gerenciar contas,')
61
+ console.log(' ou configure a variável QWEN_ACCOUNTS no seu arquivo .env.\n')
62
+ } else if (message.includes('[Server]')) {
59
63
  console.error(message)
60
64
  } else {
61
65
  console.error('❌ [Server] Failed to start:', message)
package/src/login.ts CHANGED
@@ -130,6 +130,11 @@ async function removeAccountFlow() {
130
130
  const confirm = await askQuestion(`\nRemove ${account.email}? (y/N): `);
131
131
  if (confirm.toLowerCase() === "y") {
132
132
  if (removeAccount(account.id)) {
133
+ try {
134
+ const { removePlaywrightProfile } = await import("./services/playwright.ts");
135
+ const { getAccountProfilePath } = await import("./core/paths.ts");
136
+ removePlaywrightProfile(getAccountProfilePath(account.id));
137
+ } catch {}
133
138
  console.log(`Account ${maskEmail(account.email)} removed.`);
134
139
  } else {
135
140
  console.log("Failed to remove account.");
@@ -49,7 +49,12 @@ function formatTimingHeader(timings: Record<string, number>): string {
49
49
  * else silently uses the configured default.
50
50
  */
51
51
  function resolveChatMode(headerValue: string | undefined): ChatMode {
52
- if (headerValue === "thread" || headerValue === "temp") return headerValue;
52
+ if (headerValue === "thread" || headerValue === "temp" || headerValue === "temp-thread") {
53
+ return headerValue;
54
+ }
55
+ if (headerValue === "temp_thread") {
56
+ return "temp-thread";
57
+ }
53
58
  return config.qwen.chatMode;
54
59
  }
55
60
 
@@ -9,7 +9,7 @@ import {
9
9
  isPlaywrightInitialized,
10
10
  closeAllPlaywright,
11
11
  } from "./playwright.ts";
12
-
12
+ import { isAuthMockEnabled } from "./auth-playwright.ts";
13
13
  export interface DeleteChatsResult {
14
14
  attempted: number;
15
15
  succeeded: number;
@@ -17,7 +17,7 @@ export interface DeleteChatsResult {
17
17
  }
18
18
 
19
19
  async function ensurePlaywrightSession(account: QwenAccount): Promise<void> {
20
- if (isPlaywrightInitialized(account.id)) return;
20
+ if (isPlaywrightInitialized(account.id) || isAuthMockEnabled()) return;
21
21
 
22
22
  const credentials = getAccountCredentials(account.id);
23
23
  if (!credentials) {
@@ -33,12 +33,25 @@ async function ensurePlaywrightSession(account: QwenAccount): Promise<void> {
33
33
  );
34
34
  }
35
35
 
36
- async function deleteChatsForAccount(account: QwenAccount): Promise<boolean> {
36
+ export async function deleteChatsForAccount(account: QwenAccount): Promise<boolean> {
37
37
  await ensurePlaywrightSession(account);
38
38
  return deleteAllQwenChats(account.id);
39
39
  }
40
40
 
41
- export async function deleteChatsForConfiguredAccounts(): Promise<DeleteChatsResult> {
41
+ export async function deleteChatsForAccountId(accountId: string): Promise<boolean> {
42
+ const accounts = loadAccounts();
43
+ const account = accounts.find((a) => a.id === accountId);
44
+ if (!account) {
45
+ throw new Error(`Conta ${accountId} não encontrada.`);
46
+ }
47
+ const credentials = getAccountCredentials(account.id);
48
+ if (!credentials) {
49
+ throw new Error(`Credenciais da conta ${account.email} não encontradas.`);
50
+ }
51
+ return deleteChatsForAccount(credentials);
52
+ }
53
+
54
+ export async function deleteChatsForConfiguredAccounts(keepBrowserOpen = false): Promise<DeleteChatsResult> {
42
55
  // Playwright requests are account-scoped. Use every account persisted in the
43
56
  // database, including accounts created through `npm run login`, instead of
44
57
  // falling back to a global request without an account context.
@@ -64,12 +77,14 @@ export async function deleteChatsForConfiguredAccounts(): Promise<DeleteChatsRes
64
77
  }
65
78
  }
66
79
  } finally {
67
- await closeAllPlaywright().catch((error) => {
68
- console.warn(
69
- `[DeleteChats] Failed to close Playwright sessions:`,
70
- error instanceof Error ? error.message : String(error),
71
- );
72
- });
80
+ if (!keepBrowserOpen) {
81
+ await closeAllPlaywright().catch((error) => {
82
+ console.warn(
83
+ `[DeleteChats] Failed to close Playwright sessions:`,
84
+ error instanceof Error ? error.message : String(error),
85
+ );
86
+ });
87
+ }
73
88
  }
74
89
 
75
90
  return {
@@ -7,7 +7,31 @@ import { chromium, type Browser, type BrowserContext, type Page } from "patchrig
7
7
  import path from "path";
8
8
  import fs from "fs";
9
9
  import crypto from "crypto";
10
- import type { QwenAccount } from "../core/accounts.ts";
10
+ import { spawnSync } from "child_process";
11
+ import { createRequire } from "module";
12
+
13
+ const requireLocal = createRequire(import.meta.url);
14
+
15
+ function autoInstallPlaywrightChromium(): void {
16
+ try {
17
+ const patchrightEntry = requireLocal.resolve("patchright");
18
+ const cliPath = path.join(path.dirname(patchrightEntry), "cli.js");
19
+ if (fs.existsSync(cliPath)) {
20
+ console.log("⏳ [Playwright] Navegador Chromium não encontrado. Instalando automaticamente...");
21
+ spawnSync(process.execPath, [cliPath, "install", "chromium"], {
22
+ stdio: "inherit",
23
+ });
24
+ return;
25
+ }
26
+ } catch {}
27
+
28
+ console.log("⏳ [Playwright] Navegador Chromium não encontrado. Instalando via npx...");
29
+ const cmd = process.platform === "win32" ? "npx.cmd" : "npx";
30
+ spawnSync(cmd, ["--yes", "patchright", "install", "chromium"], {
31
+ stdio: "inherit",
32
+ });
33
+ }
34
+ import { loadAccounts, type QwenAccount } from "../core/accounts.ts";
11
35
  // Imported here rather than injected from session-keeper.ts: account-concurrency
12
36
  // only depends on config/logger, so playwright -> account-concurrency stays
13
37
  // acyclic, while the reverse direction would drag the browser layer into core.
@@ -260,15 +284,30 @@ export async function getOrLaunchSharedBrowser(
260
284
  const launchArgs = buildChromiumLaunchArgs(defaultViewport);
261
285
 
262
286
  console.log(
263
- `[Playwright] Launching single shared ${browserType} browser...`,
287
+ `🌐 [Playwright] Launching single shared ${browserType} browser...`,
264
288
  );
265
289
 
266
- const browser = await engine.launch({
267
- headless,
268
- channel,
269
- ignoreDefaultArgs: ["--enable-automation", "--enable-blink-features"],
270
- args: launchArgs,
271
- });
290
+ let browser: Browser;
291
+ try {
292
+ browser = await engine.launch({
293
+ headless,
294
+ channel,
295
+ ignoreDefaultArgs: ["--enable-automation", "--enable-blink-features"],
296
+ args: launchArgs,
297
+ });
298
+ } catch (launchErr: any) {
299
+ if (launchErr?.message?.includes("Executable doesn't exist")) {
300
+ autoInstallPlaywrightChromium();
301
+ browser = await engine.launch({
302
+ headless,
303
+ channel,
304
+ ignoreDefaultArgs: ["--enable-automation", "--enable-blink-features"],
305
+ args: launchArgs,
306
+ });
307
+ } else {
308
+ throw launchErr;
309
+ }
310
+ }
272
311
 
273
312
  browser.on("disconnected", () => {
274
313
  console.warn("[Playwright] Shared browser disconnected");
@@ -2338,6 +2377,64 @@ export function pruneAllPlaywrightProfiles(baseDir = getProfilesDir()): {
2338
2377
 
2339
2378
  return { totalFreedBytes, totalFreedFiles, profilesCleaned };
2340
2379
  }
2380
+ /**
2381
+ * Removes profile directories in data/qwen_profiles that do not belong to any
2382
+ * active account configured in the database or environment, plus any lingering
2383
+ * .stale-* directories from previous lock renames.
2384
+ */
2385
+ export function cleanupOrphanProfiles(
2386
+ baseDir = getProfilesDir(),
2387
+ activeAccountIds?: Set<string>,
2388
+ ): {
2389
+ removedCount: number;
2390
+ freedBytes: number;
2391
+ } {
2392
+ let removedCount = 0;
2393
+ let freedBytes = 0;
2394
+
2395
+ try {
2396
+ if (!fs.existsSync(baseDir)) {
2397
+ return { removedCount, freedBytes };
2398
+ }
2399
+
2400
+ const activeIds =
2401
+ activeAccountIds ?? new Set(loadAccounts().map((a) => a.id));
2402
+
2403
+ const entries = fs.readdirSync(baseDir, { withFileTypes: true });
2404
+ for (const entry of entries) {
2405
+ if (!entry.isDirectory()) continue;
2406
+ const isStale = entry.name.includes(".stale-");
2407
+ const isOrphan = !isStale && !activeIds.has(entry.name);
2408
+
2409
+ if (isStale || isOrphan) {
2410
+ const targetPath = path.join(baseDir, entry.name);
2411
+ try {
2412
+ let bytes = 0;
2413
+ const countSize = (d: string) => {
2414
+ try {
2415
+ const subEntries = fs.readdirSync(d, { withFileTypes: true });
2416
+ for (const e of subEntries) {
2417
+ const full = path.join(d, e.name);
2418
+ if (e.isDirectory()) countSize(full);
2419
+ else if (e.isFile()) {
2420
+ try { bytes += fs.statSync(full).size; } catch {}
2421
+ }
2422
+ }
2423
+ } catch {}
2424
+ };
2425
+ countSize(targetPath);
2426
+ removePlaywrightProfile(targetPath);
2427
+ if (!fs.existsSync(targetPath)) {
2428
+ removedCount++;
2429
+ freedBytes += bytes;
2430
+ }
2431
+ } catch {}
2432
+ }
2433
+ }
2434
+ } catch {}
2435
+
2436
+ return { removedCount, freedBytes };
2437
+ }
2341
2438
 
2342
2439
  const PROFILE_RESET_TIMEOUT_MS = Math.max(90_000, config.timeouts.headers);
2343
2440
 
@@ -83,8 +83,9 @@ export function buildChatNewBody(
83
83
  project_id: "",
84
84
  timestamp: Date.now(),
85
85
  chat_type: "t2t",
86
- // thread → normal (persisted), temp → local (ephemeral, not listed).
87
- chat_mode: chatMode === "temp" ? "local" : "normal",
86
+ // thread → normal (persisted), temp / temp-thread → local (ephemeral, not listed).
87
+ chat_mode:
88
+ chatMode === "temp" || chatMode === "temp-thread" ? "local" : "normal",
88
89
  };
89
90
  }
90
91
 
@@ -2653,7 +2653,10 @@ async function createQwenStreamInternal(
2653
2653
  chatId: chatSessionId || null,
2654
2654
  parentId: actualParentId ?? "",
2655
2655
  chat_id: chatSessionId || null,
2656
- chat_mode: options?.chatMode === "temp" ? "local" : "normal",
2656
+ chat_mode:
2657
+ options?.chatMode === "temp" || options?.chatMode === "temp-thread"
2658
+ ? "local"
2659
+ : "normal",
2657
2660
  model: model,
2658
2661
  parent_id: actualParentId,
2659
2662
  messages: [
package/src/tui/app.ts CHANGED
@@ -11,7 +11,7 @@ import fs from "node:fs";
11
11
  import path from "node:path";
12
12
  import { fileURLToPath } from "node:url";
13
13
 
14
- let cachedAppVersion = "v1.0.2";
14
+ let cachedAppVersion = "v1.0.5";
15
15
  try {
16
16
  const currentDir = path.dirname(fileURLToPath(import.meta.url));
17
17
  const pkgPath = path.resolve(currentDir, "../../package.json");
@@ -2,7 +2,7 @@
2
2
  * QwenProxy TUI - Proxy Data Provider & Live State Client
3
3
  */
4
4
 
5
- import { config } from "../core/config.ts";
5
+ import { config, type ChatMode } from "../core/config.ts";
6
6
  import { loadAccounts, type QwenAccount } from "../core/accounts.ts";
7
7
  import {
8
8
  getAccountCooldownInfo,
@@ -176,6 +176,7 @@ export interface StreamChatOptions {
176
176
  model: string;
177
177
  reasoning_effort?: "low" | "medium" | "high";
178
178
  messages: Array<{ role: "system" | "user" | "assistant"; content: string }>;
179
+ chatMode?: ChatMode;
179
180
  onToken: (text: string) => void;
180
181
  onReasoning?: (text: string) => void;
181
182
  signal?: AbortSignal;
@@ -197,11 +198,13 @@ export async function streamChatCompletions(
197
198
 
198
199
  let resp: Response;
199
200
  try {
201
+ const chatMode = options.chatMode ?? "temp-thread";
200
202
  resp = await fetch(`http://${host}:${port}/v1/chat/completions`, {
201
203
  method: "POST",
202
204
  headers: {
203
205
  "Content-Type": "application/json",
204
206
  Authorization: `Bearer ${apiKey}`,
207
+ "x-qwenproxy-chat-mode": chatMode,
205
208
  },
206
209
  body: JSON.stringify({
207
210
  model: options.model,