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 +34 -23
- package/bin/qwenproxy.js +34 -13
- package/package.json +1 -1
- package/src/api/server.ts +4 -1
- package/src/clean-cache.ts +17 -11
- package/src/core/config.ts +3 -3
- package/src/index.ts +5 -1
- package/src/login.ts +5 -0
- package/src/routes/chat/index.ts +6 -1
- package/src/services/chat-cleanup.ts +25 -10
- package/src/services/playwright.ts +105 -8
- package/src/services/qwen-chat-pool.ts +3 -2
- package/src/services/qwen.ts +4 -1
- package/src/tui/app.ts +1 -1
- package/src/tui/proxy-client.ts +4 -1
- package/src/tui/screen.ts +47 -7
- package/src/tui/server-manager.ts +19 -11
- package/src/tui/theme.ts +41 -23
- package/src/tui/views/accounts-view.ts +197 -18
- package/src/tui/views/status-view.ts +4 -4
- package/src/tui/views/storage-view.ts +153 -14
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
|
[](https://github.com/johngbl/QwenProxy/actions/workflows/ci.yml)
|
|
10
|
+
[](https://www.npmjs.com/package/qwenproxy-cli)
|
|
10
11
|
[](https://www.typescriptlang.org/)
|
|
11
12
|
[](https://hono.dev/)
|
|
12
|
-
[](https://github.com/kaliiiiiiiiii/patchright)
|
|
13
14
|
[](LICENSE)
|
|
14
15
|
[](https://github.com/sponsors/johngbl)
|
|
15
16
|
[](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
|
-
- **
|
|
33
|
-
- **
|
|
34
|
-
- **
|
|
35
|
-
- **
|
|
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
|
|
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
|
-
**
|
|
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
|
|
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
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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)
|
|
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` | `
|
|
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` | `
|
|
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
|
|
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
|
|
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
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
+
"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
|
-
|
|
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
|
|
package/src/clean-cache.ts
CHANGED
|
@@ -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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
)
|
|
148
|
-
|
|
149
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
}
|
package/src/core/config.ts
CHANGED
|
@@ -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('
|
|
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.");
|
package/src/routes/chat/index.ts
CHANGED
|
@@ -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"
|
|
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
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
|
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
|
-
|
|
287
|
+
`🌐 [Playwright] Launching single shared ${browserType} browser...`,
|
|
264
288
|
);
|
|
265
289
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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:
|
|
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
|
|
package/src/services/qwen.ts
CHANGED
|
@@ -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:
|
|
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.
|
|
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");
|
package/src/tui/proxy-client.ts
CHANGED
|
@@ -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,
|