qwenproxy-cli 1.0.2 → 1.0.4
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 +33 -22
- package/package.json +1 -1
- package/src/api/server.ts +1 -0
- package/src/core/config.ts +3 -3
- package/src/index.ts +5 -1
- package/src/routes/chat/index.ts +6 -1
- package/src/services/chat-cleanup.ts +25 -10
- package/src/services/qwen-chat-pool.ts +3 -2
- package/src/services/qwen.ts +4 -1
- package/src/tui/app.ts +13 -1
- package/src/tui/proxy-client.ts +15 -2
- package/src/tui/types.ts +1 -0
- package/src/tui/views/accounts-view.ts +199 -18
- package/src/tui/views/status-view.ts +3 -1
- package/src/tui/views/storage-view.ts +142 -7
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 no SSD |
|
|
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,7 +841,8 @@ 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
848
|
| `npm run clean:all` | Limpar caches + remover navegadores órfãos e versões antigas no SSD |
|
|
@@ -839,7 +851,6 @@ QwenProxy/
|
|
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qwenproxy-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
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
|
@@ -269,6 +269,7 @@ app.get("/health", async (c) => {
|
|
|
269
269
|
: undefined,
|
|
270
270
|
timestamp: Date.now(),
|
|
271
271
|
readyAccounts: (await import("../core/account-manager.js")).getHeadersReadyAccountIds(),
|
|
272
|
+
activeAccounts: (await import("../services/playwright.js")).getActivePlaywrightAccountIds(),
|
|
272
273
|
metrics: {
|
|
273
274
|
cache: await cache?.getStats(),
|
|
274
275
|
},
|
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/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 {
|
|
@@ -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
|
@@ -7,7 +7,19 @@ import type { TuiView, ProxyStatusSnapshot } from "./types.ts";
|
|
|
7
7
|
import { theme, glyphs, drawBox, stringWidth } from "./theme.ts";
|
|
8
8
|
import { fetchProxyStatus } from "./proxy-client.ts";
|
|
9
9
|
import { ServerManager } from "./server-manager.ts";
|
|
10
|
+
import fs from "node:fs";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
10
13
|
|
|
14
|
+
let cachedAppVersion = "v1.0.4";
|
|
15
|
+
try {
|
|
16
|
+
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const pkgPath = path.resolve(currentDir, "../../package.json");
|
|
18
|
+
if (fs.existsSync(pkgPath)) {
|
|
19
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
20
|
+
if (pkg.version) cachedAppVersion = `v${pkg.version}`;
|
|
21
|
+
}
|
|
22
|
+
} catch {}
|
|
11
23
|
import { StatusView } from "./views/status-view.ts";
|
|
12
24
|
import { ChatView } from "./views/chat-view.ts";
|
|
13
25
|
import { SyncView } from "./views/sync-view.ts";
|
|
@@ -240,7 +252,7 @@ export class TuiApp {
|
|
|
240
252
|
|
|
241
253
|
const headerContent = [` ${tabsBar}`];
|
|
242
254
|
const headerBox = drawBox({
|
|
243
|
-
title: `QwenProxy
|
|
255
|
+
title: `QwenProxy ${cachedAppVersion} ${statusChip}`,
|
|
244
256
|
width: cols,
|
|
245
257
|
height: 3,
|
|
246
258
|
borderColor: theme.borderActive,
|
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,
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
clearAccountCooldown,
|
|
11
11
|
isAccountHeadersReady,
|
|
12
12
|
} from "../core/account-manager.ts";
|
|
13
|
+
import { isPlaywrightInitialized } from "../services/playwright.ts";
|
|
13
14
|
import { getAccountConcurrencySnapshot } from "../core/account-concurrency.ts";
|
|
14
15
|
import { getRssUsageSnapshot } from "../core/memory-usage.ts";
|
|
15
16
|
import type { ProxyStatusSnapshot } from "./types.ts";
|
|
@@ -46,12 +47,14 @@ let cachedAccounts: Array<{
|
|
|
46
47
|
onCooldown: boolean;
|
|
47
48
|
remainingCooldownMs: number;
|
|
48
49
|
headersReady: boolean;
|
|
50
|
+
isInitialized: boolean;
|
|
49
51
|
}> = [];
|
|
50
52
|
let lastAccountsFetch = 0;
|
|
51
53
|
let isHealthCheckPending = false;
|
|
52
54
|
let lastOnlineState = false;
|
|
53
55
|
let lastOverallStatus = "offline";
|
|
54
56
|
let lastServerReadyAccounts: Set<string> | null = null;
|
|
57
|
+
let lastServerActiveAccounts: Set<string> | null = null;
|
|
55
58
|
export async function fetchProxyStatus(): Promise<ProxyStatusSnapshot> {
|
|
56
59
|
const port = config.server?.port || 7936;
|
|
57
60
|
const configuredHost = config.server?.host;
|
|
@@ -73,9 +76,13 @@ export async function fetchProxyStatus(): Promise<ProxyStatusSnapshot> {
|
|
|
73
76
|
if (Array.isArray(data.readyAccounts)) {
|
|
74
77
|
lastServerReadyAccounts = new Set(data.readyAccounts);
|
|
75
78
|
}
|
|
79
|
+
if (Array.isArray(data.activeAccounts)) {
|
|
80
|
+
lastServerActiveAccounts = new Set(data.activeAccounts);
|
|
81
|
+
}
|
|
76
82
|
} else {
|
|
77
83
|
lastOnlineState = false;
|
|
78
84
|
lastServerReadyAccounts = null;
|
|
85
|
+
lastServerActiveAccounts = null;
|
|
79
86
|
}
|
|
80
87
|
})
|
|
81
88
|
.catch(() => {
|
|
@@ -104,6 +111,9 @@ export async function fetchProxyStatus(): Promise<ProxyStatusSnapshot> {
|
|
|
104
111
|
const headersReady = lastServerReadyAccounts !== null
|
|
105
112
|
? lastServerReadyAccounts.has(acc.id)
|
|
106
113
|
: isAccountHeadersReady(acc.id);
|
|
114
|
+
const isInitialized = lastServerActiveAccounts !== null
|
|
115
|
+
? lastServerActiveAccounts.has(acc.id)
|
|
116
|
+
: isPlaywrightInitialized(acc.id);
|
|
107
117
|
return {
|
|
108
118
|
id: acc.id,
|
|
109
119
|
emailOrName: maskAccountIdentifier(acc.email || acc.id),
|
|
@@ -112,10 +122,10 @@ export async function fetchProxyStatus(): Promise<ProxyStatusSnapshot> {
|
|
|
112
122
|
onCooldown,
|
|
113
123
|
remainingCooldownMs,
|
|
114
124
|
headersReady,
|
|
125
|
+
isInitialized,
|
|
115
126
|
};
|
|
116
127
|
});
|
|
117
128
|
}
|
|
118
|
-
|
|
119
129
|
const accounts = cachedAccounts;
|
|
120
130
|
const online = lastOnlineState;
|
|
121
131
|
const overallStatus = lastOverallStatus;
|
|
@@ -166,6 +176,7 @@ export interface StreamChatOptions {
|
|
|
166
176
|
model: string;
|
|
167
177
|
reasoning_effort?: "low" | "medium" | "high";
|
|
168
178
|
messages: Array<{ role: "system" | "user" | "assistant"; content: string }>;
|
|
179
|
+
chatMode?: ChatMode;
|
|
169
180
|
onToken: (text: string) => void;
|
|
170
181
|
onReasoning?: (text: string) => void;
|
|
171
182
|
signal?: AbortSignal;
|
|
@@ -187,11 +198,13 @@ export async function streamChatCompletions(
|
|
|
187
198
|
|
|
188
199
|
let resp: Response;
|
|
189
200
|
try {
|
|
201
|
+
const chatMode = options.chatMode ?? "temp-thread";
|
|
190
202
|
resp = await fetch(`http://${host}:${port}/v1/chat/completions`, {
|
|
191
203
|
method: "POST",
|
|
192
204
|
headers: {
|
|
193
205
|
"Content-Type": "application/json",
|
|
194
206
|
Authorization: `Bearer ${apiKey}`,
|
|
207
|
+
"x-qwenproxy-chat-mode": chatMode,
|
|
195
208
|
},
|
|
196
209
|
body: JSON.stringify({
|
|
197
210
|
model: options.model,
|
package/src/tui/types.ts
CHANGED
|
@@ -33,6 +33,16 @@ export class AccountsView implements TuiView {
|
|
|
33
33
|
private modalHoveredField: "email" | "password" | "save" | "cancel" | null = null;
|
|
34
34
|
private lastModalLeftPad = 0;
|
|
35
35
|
private lastLeftW = 46;
|
|
36
|
+
private confirmDialog: {
|
|
37
|
+
type: "remove_account" | "delete_account_chats" | "delete_all_chats";
|
|
38
|
+
title: string;
|
|
39
|
+
message: string;
|
|
40
|
+
detail: string;
|
|
41
|
+
onConfirm: () => Promise<void>;
|
|
42
|
+
} | null = null;
|
|
43
|
+
private confirmDialogHovered: "confirm" | "cancel" | null = null;
|
|
44
|
+
private lastConfirmModalLeftPad = 0;
|
|
45
|
+
private lastConfirmModalStartRow = 0;
|
|
36
46
|
constructor() {
|
|
37
47
|
this.refresh();
|
|
38
48
|
}
|
|
@@ -42,9 +52,15 @@ export class AccountsView implements TuiView {
|
|
|
42
52
|
}
|
|
43
53
|
|
|
44
54
|
public isCapturingText(): boolean {
|
|
45
|
-
return this.isAddModalOpen;
|
|
55
|
+
return this.isAddModalOpen || this.confirmDialog !== null;
|
|
46
56
|
}
|
|
47
57
|
public getShortcuts(): Array<{ key: string; label: string }> {
|
|
58
|
+
if (this.confirmDialog) {
|
|
59
|
+
return [
|
|
60
|
+
{ key: "S / Enter", label: "Confirmar" },
|
|
61
|
+
{ key: "N / Esc", label: "Cancelar" },
|
|
62
|
+
];
|
|
63
|
+
}
|
|
48
64
|
if (this.isAddModalOpen) {
|
|
49
65
|
return [
|
|
50
66
|
{ key: "↑↓/Mouse", label: "Alternar" },
|
|
@@ -55,6 +71,8 @@ export class AccountsView implements TuiView {
|
|
|
55
71
|
return [
|
|
56
72
|
{ key: "a", label: "Adicionar Conta" },
|
|
57
73
|
{ key: "d", label: "Remover Conta" },
|
|
74
|
+
{ key: "x", label: "Limpar Chats" },
|
|
75
|
+
{ key: "l", label: "Limpar Todos Chats" },
|
|
58
76
|
{ key: "c", label: "Zerar Cooldown" },
|
|
59
77
|
{ key: "z", label: "Zerar Todas" },
|
|
60
78
|
];
|
|
@@ -125,6 +143,63 @@ export class AccountsView implements TuiView {
|
|
|
125
143
|
}
|
|
126
144
|
}
|
|
127
145
|
public async handleKey(key: KeyEvent): Promise<boolean | void> {
|
|
146
|
+
// 0. Confirm Dialog Active
|
|
147
|
+
if (this.confirmDialog) {
|
|
148
|
+
if (key.name === "s" || key.name === "S" || key.name === "enter" || key.name === "return") {
|
|
149
|
+
const dialog = this.confirmDialog;
|
|
150
|
+
this.confirmDialog = null;
|
|
151
|
+
this.confirmDialogHovered = null;
|
|
152
|
+
await dialog.onConfirm();
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
if (key.name === "escape" || key.name === "n" || key.name === "N") {
|
|
156
|
+
this.confirmDialog = null;
|
|
157
|
+
this.confirmDialogHovered = null;
|
|
158
|
+
this.setStatusMessage(theme.muted("Ação cancelada"));
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
if (key.name === "hover" && key.mouse) {
|
|
162
|
+
const { row, col } = key.mouse;
|
|
163
|
+
const btnRow = this.lastConfirmModalStartRow + 4;
|
|
164
|
+
if (row === btnRow) {
|
|
165
|
+
const startCol = this.lastConfirmModalLeftPad + 2;
|
|
166
|
+
if (col >= startCol && col <= startCol + 25) {
|
|
167
|
+
this.confirmDialogHovered = "confirm";
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
if (col >= startCol + 26 && col <= startCol + 50) {
|
|
171
|
+
this.confirmDialogHovered = "cancel";
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (this.confirmDialogHovered !== null) {
|
|
176
|
+
this.confirmDialogHovered = null;
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (key.name === "click" && key.mouse) {
|
|
181
|
+
const { row, col } = key.mouse;
|
|
182
|
+
const btnRow = this.lastConfirmModalStartRow + 4;
|
|
183
|
+
if (row === btnRow) {
|
|
184
|
+
const startCol = this.lastConfirmModalLeftPad + 2;
|
|
185
|
+
if (col >= startCol && col <= startCol + 25) {
|
|
186
|
+
const dialog = this.confirmDialog;
|
|
187
|
+
this.confirmDialog = null;
|
|
188
|
+
this.confirmDialogHovered = null;
|
|
189
|
+
await dialog.onConfirm();
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
if (col >= startCol + 26 && col <= startCol + 50) {
|
|
193
|
+
this.confirmDialog = null;
|
|
194
|
+
this.confirmDialogHovered = null;
|
|
195
|
+
this.setStatusMessage(theme.muted("Ação cancelada"));
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
|
|
128
203
|
// 1. Add Account Modal Active
|
|
129
204
|
if (this.isAddModalOpen) {
|
|
130
205
|
if (key.name === "escape") {
|
|
@@ -335,22 +410,81 @@ export class AccountsView implements TuiView {
|
|
|
335
410
|
return true;
|
|
336
411
|
}
|
|
337
412
|
|
|
338
|
-
// Delete selected account with 'd' or 'D'
|
|
413
|
+
// Delete selected account with 'd' or 'D' (requires confirmation)
|
|
339
414
|
if ((key.name === "d" || key.name === "D") && !key.ctrl) {
|
|
340
415
|
const selected = accounts[this.selectedIndex];
|
|
341
416
|
if (!selected) {
|
|
342
417
|
this.setStatusMessage(theme.yellow("[!] Nenhuma conta selecionada para remover"));
|
|
343
418
|
return true;
|
|
344
419
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
420
|
+
this.confirmDialog = {
|
|
421
|
+
type: "remove_account",
|
|
422
|
+
title: "⚠️ Confirmar Remoção de Conta",
|
|
423
|
+
message: `Deseja remover a conta ${selected.emailOrName}?`,
|
|
424
|
+
detail: "A conta será excluída do banco de dados e sua sessão encerrada.",
|
|
425
|
+
onConfirm: async () => {
|
|
426
|
+
removeAccount(selected.id);
|
|
427
|
+
try {
|
|
428
|
+
const { closePlaywrightForAccount } = await import("../../services/playwright.ts");
|
|
429
|
+
await closePlaywrightForAccount(selected.id);
|
|
430
|
+
} catch {}
|
|
431
|
+
await this.refresh();
|
|
432
|
+
this.setStatusMessage(theme.green(`✓ Conta ${selected.emailOrName} removida com sucesso`));
|
|
433
|
+
},
|
|
434
|
+
};
|
|
435
|
+
return true;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Delete chats of selected account with 'x' or 'X' (requires confirmation)
|
|
439
|
+
if ((key.name === "x" || key.name === "X") && !key.ctrl) {
|
|
440
|
+
const selected = accounts[this.selectedIndex];
|
|
441
|
+
if (!selected) {
|
|
442
|
+
this.setStatusMessage(theme.yellow("[!] Nenhuma conta selecionada"));
|
|
443
|
+
return true;
|
|
444
|
+
}
|
|
445
|
+
this.confirmDialog = {
|
|
446
|
+
type: "delete_account_chats",
|
|
447
|
+
title: "⚠️ Apagar Chats Remotos no Qwen",
|
|
448
|
+
message: `Apagar TODOS os chats no Qwen da conta ${selected.emailOrName}?`,
|
|
449
|
+
detail: "Esta ação é irreversível e limpará todas as conversas em chat.qwen.ai.",
|
|
450
|
+
onConfirm: async () => {
|
|
451
|
+
this.setStatusMessage(theme.yellow(`⏳ Apagando chats no Qwen para ${selected.emailOrName}...`));
|
|
452
|
+
try {
|
|
453
|
+
const { deleteChatsForAccountId } = await import("../../services/chat-cleanup.ts");
|
|
454
|
+
await deleteChatsForAccountId(selected.id);
|
|
455
|
+
await this.refresh();
|
|
456
|
+
this.setStatusMessage(theme.green(`✓ Todos os chats de ${selected.emailOrName} foram apagados no Qwen!`));
|
|
457
|
+
} catch (err: any) {
|
|
458
|
+
this.setStatusMessage(theme.red(`✗ Falha ao apagar chats: ${err?.message || String(err)}`));
|
|
459
|
+
}
|
|
460
|
+
},
|
|
461
|
+
};
|
|
462
|
+
return true;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Delete chats of all accounts with 'l' or 'L' (requires confirmation)
|
|
466
|
+
if ((key.name === "l" || key.name === "L") && !key.ctrl) {
|
|
467
|
+
if (accounts.length === 0) {
|
|
468
|
+
this.setStatusMessage(theme.yellow("[!] Nenhuma conta configurada"));
|
|
469
|
+
return true;
|
|
470
|
+
}
|
|
471
|
+
this.confirmDialog = {
|
|
472
|
+
type: "delete_all_chats",
|
|
473
|
+
title: "⚠️ Apagar Chats de TODAS as Contas",
|
|
474
|
+
message: `Apagar TODOS os chats remotos de TODAS as ${accounts.length} contas no Qwen?`,
|
|
475
|
+
detail: "Esta ação é irreversível e limpará o histórico no chat.qwen.ai.",
|
|
476
|
+
onConfirm: async () => {
|
|
477
|
+
this.setStatusMessage(theme.yellow(`⏳ Apagando chats no Qwen de todas as contas...`));
|
|
478
|
+
try {
|
|
479
|
+
const { deleteChatsForConfiguredAccounts } = await import("../../services/chat-cleanup.ts");
|
|
480
|
+
const res = await deleteChatsForConfiguredAccounts(true);
|
|
481
|
+
await this.refresh();
|
|
482
|
+
this.setStatusMessage(theme.green(`✓ Chats apagados no Qwen: ${res.succeeded}/${res.attempted} contas limpas!`));
|
|
483
|
+
} catch (err: any) {
|
|
484
|
+
this.setStatusMessage(theme.red(`✗ Falha ao apagar chats: ${err?.message || String(err)}`));
|
|
485
|
+
}
|
|
486
|
+
},
|
|
487
|
+
};
|
|
354
488
|
return true;
|
|
355
489
|
}
|
|
356
490
|
|
|
@@ -371,9 +505,9 @@ export class AccountsView implements TuiView {
|
|
|
371
505
|
return true;
|
|
372
506
|
}
|
|
373
507
|
|
|
374
|
-
// Right panel action buttons hover (rows 15 to
|
|
508
|
+
// Right panel action buttons hover (rows 15 to 20)
|
|
375
509
|
if (col >= leftW) {
|
|
376
|
-
if (row >= 15 && row <=
|
|
510
|
+
if (row >= 15 && row <= 20) {
|
|
377
511
|
if (this.hoveredActionRow !== row) {
|
|
378
512
|
this.hoveredActionRow = row;
|
|
379
513
|
return true;
|
|
@@ -387,7 +521,6 @@ export class AccountsView implements TuiView {
|
|
|
387
521
|
return true;
|
|
388
522
|
}
|
|
389
523
|
}
|
|
390
|
-
|
|
391
524
|
// Mouse click on account rows or action buttons
|
|
392
525
|
if (key.name === "click" && key.mouse) {
|
|
393
526
|
const { row, col } = key.mouse;
|
|
@@ -416,9 +549,16 @@ export class AccountsView implements TuiView {
|
|
|
416
549
|
await this.handleKey({ name: "z", ctrl: false, shift: false, meta: false });
|
|
417
550
|
return true;
|
|
418
551
|
}
|
|
552
|
+
if (row === 19) {
|
|
553
|
+
await this.handleKey({ name: "x", ctrl: false, shift: false, meta: false });
|
|
554
|
+
return true;
|
|
555
|
+
}
|
|
556
|
+
if (row === 20) {
|
|
557
|
+
await this.handleKey({ name: "l", ctrl: false, shift: false, meta: false });
|
|
558
|
+
return true;
|
|
559
|
+
}
|
|
419
560
|
}
|
|
420
561
|
}
|
|
421
|
-
// Navigate accounts list (Mouse wheel or Up/Down keys)
|
|
422
562
|
if (key.name === "up" || key.name === "wheelup" || (key.name === "k" && !key.ctrl)) {
|
|
423
563
|
if (accounts.length > 0) {
|
|
424
564
|
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
|
@@ -469,6 +609,9 @@ export class AccountsView implements TuiView {
|
|
|
469
609
|
this.lastLeftW = leftW;
|
|
470
610
|
const rightW = Math.max(30, width - leftW - 1);
|
|
471
611
|
|
|
612
|
+
if (snapshot) {
|
|
613
|
+
this.statusData = snapshot;
|
|
614
|
+
}
|
|
472
615
|
const data = snapshot || this.statusData;
|
|
473
616
|
const accounts = data?.accounts || [];
|
|
474
617
|
const selected = accounts[this.selectedIndex];
|
|
@@ -497,7 +640,9 @@ export class AccountsView implements TuiView {
|
|
|
497
640
|
const mins = Math.max(1, Math.round(acc.remainingCooldownMs / 60000));
|
|
498
641
|
status = theme.yellow(`⚠ ${mins}m cd `);
|
|
499
642
|
} else if (!acc.headersReady) {
|
|
500
|
-
status =
|
|
643
|
+
status = acc.isInitialized
|
|
644
|
+
? theme.yellow(`◐ Aquecendo...`)
|
|
645
|
+
: theme.muted(`○ Standby `);
|
|
501
646
|
}
|
|
502
647
|
|
|
503
648
|
const line = `${pointer}${num}${name}${status}`;
|
|
@@ -550,7 +695,9 @@ export class AccountsView implements TuiView {
|
|
|
550
695
|
|
|
551
696
|
const hStatus = selected.headersReady
|
|
552
697
|
? theme.green(`${glyphs.check} Capturados`)
|
|
553
|
-
:
|
|
698
|
+
: selected.isInitialized
|
|
699
|
+
? theme.yellow(`◐ Aquecendo...`)
|
|
700
|
+
: theme.muted(`${glyphs.circle} Standby (Sob Demanda)`);
|
|
554
701
|
rightContent.push(` ${theme.bold("Headers:")} ${hStatus}`);
|
|
555
702
|
rightContent.push("");
|
|
556
703
|
rightContent.push(` ${theme.dim("─────────────────────────────────")}`);
|
|
@@ -558,8 +705,9 @@ export class AccountsView implements TuiView {
|
|
|
558
705
|
rightContent.push(` ${this.hoveredActionRow === 16 ? theme.bgHover(` ${theme.red("[ D ] Remover Conta")} `) : `${theme.red("[ D ]")} Remover Conta`}`);
|
|
559
706
|
rightContent.push(` ${this.hoveredActionRow === 17 ? theme.bgHover(` ${theme.yellow("[ C ] Zerar Cooldown")} `) : `${theme.yellow("[ C ]")} Zerar Cooldown`}`);
|
|
560
707
|
rightContent.push(` ${this.hoveredActionRow === 18 ? theme.bgHover(` ${theme.green("[ Z ] Zerar Todas")} `) : `${theme.green("[ Z ]")} Zerar Todas`}`);
|
|
708
|
+
rightContent.push(` ${this.hoveredActionRow === 19 ? theme.bgHover(` ${theme.peach("[ X ] Limpar Chats (Conta)")} `) : `${theme.peach("[ X ]")} Limpar Chats (Conta)`}`);
|
|
709
|
+
rightContent.push(` ${this.hoveredActionRow === 20 ? theme.bgHover(` ${theme.red("[ L ] Limpar Todos os Chats")} `) : `${theme.red("[ L ]")} Limpar Todos os Chats`}`);
|
|
561
710
|
}
|
|
562
|
-
|
|
563
711
|
const rightBox = drawBox({
|
|
564
712
|
title: "Inspeção de Conta",
|
|
565
713
|
width: rightW,
|
|
@@ -651,6 +799,39 @@ export class AccountsView implements TuiView {
|
|
|
651
799
|
return modalBox.map((line) => padStr + line);
|
|
652
800
|
}
|
|
653
801
|
|
|
802
|
+
if (this.confirmDialog) {
|
|
803
|
+
const modalW = Math.min(width - 4, 66);
|
|
804
|
+
this.lastConfirmModalLeftPad = Math.max(0, Math.floor((width - modalW) / 2));
|
|
805
|
+
const confirmBtn =
|
|
806
|
+
this.confirmDialogHovered === "confirm"
|
|
807
|
+
? theme.bgHover(theme.red(" [ S / Enter ] Sim, Confirmar "))
|
|
808
|
+
: theme.red("[ S / Enter ] Sim, Confirmar");
|
|
809
|
+
const cancelBtn =
|
|
810
|
+
this.confirmDialogHovered === "cancel"
|
|
811
|
+
? theme.bgHover(theme.green(" [ N / Esc ] Cancelar "))
|
|
812
|
+
: theme.green("[ N / Esc ] Cancelar");
|
|
813
|
+
|
|
814
|
+
const modalContent = [
|
|
815
|
+
"",
|
|
816
|
+
` ${theme.bold(this.confirmDialog.message)}`,
|
|
817
|
+
` ${theme.muted(this.confirmDialog.detail)}`,
|
|
818
|
+
"",
|
|
819
|
+
` ${confirmBtn} ${cancelBtn}`,
|
|
820
|
+
];
|
|
821
|
+
|
|
822
|
+
const modalBox = drawBox({
|
|
823
|
+
title: this.confirmDialog.title,
|
|
824
|
+
width: modalW,
|
|
825
|
+
height: Math.min(contentH, 8),
|
|
826
|
+
borderColor: theme.red,
|
|
827
|
+
titleColor: theme.red,
|
|
828
|
+
content: modalContent,
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
const padStr = " ".repeat(this.lastConfirmModalLeftPad);
|
|
832
|
+
return modalBox.map((line) => padStr + line);
|
|
833
|
+
}
|
|
834
|
+
|
|
654
835
|
return mergedLines;
|
|
655
836
|
}
|
|
656
837
|
}
|
|
@@ -175,7 +175,9 @@ export class StatusView implements TuiView {
|
|
|
175
175
|
const mins = Math.max(1, Math.round(acc.remainingCooldownMs / 60000));
|
|
176
176
|
status = theme.yellow(`⚠ Cooldown ${mins}m`);
|
|
177
177
|
} else if (!acc.headersReady) {
|
|
178
|
-
status =
|
|
178
|
+
status = acc.isInitialized
|
|
179
|
+
? theme.yellow(`◐ Aquecendo...`)
|
|
180
|
+
: theme.muted(`○ Standby`);
|
|
179
181
|
}
|
|
180
182
|
rightContent.push(` ${num} ${name} ${status}`);
|
|
181
183
|
});
|
|
@@ -34,8 +34,7 @@ export class StorageView implements TuiView {
|
|
|
34
34
|
private isScanning = false;
|
|
35
35
|
|
|
36
36
|
private addLog(message: string): void {
|
|
37
|
-
|
|
38
|
-
this.actionLogs.push(`${theme.dim(`[${time}]`)} ${message}`);
|
|
37
|
+
this.actionLogs.push(message);
|
|
39
38
|
if (this.actionLogs.length > 50) {
|
|
40
39
|
this.actionLogs.shift();
|
|
41
40
|
}
|
|
@@ -46,15 +45,35 @@ export class StorageView implements TuiView {
|
|
|
46
45
|
}
|
|
47
46
|
private hoveredActionRow: number | null = null;
|
|
48
47
|
private lastLeftW = 48;
|
|
48
|
+
private confirmDialog: {
|
|
49
|
+
title: string;
|
|
50
|
+
message: string;
|
|
51
|
+
detail: string;
|
|
52
|
+
onConfirm: () => Promise<void>;
|
|
53
|
+
} | null = null;
|
|
54
|
+
private confirmDialogHovered: "confirm" | "cancel" | null = null;
|
|
55
|
+
private lastConfirmModalLeftPad = 0;
|
|
56
|
+
private lastConfirmModalStartRow = 0;
|
|
57
|
+
|
|
58
|
+
public isCapturingText(): boolean {
|
|
59
|
+
return this.confirmDialog !== null;
|
|
60
|
+
}
|
|
61
|
+
|
|
49
62
|
public getShortcuts(): Array<{ key: string; label: string }> {
|
|
63
|
+
if (this.confirmDialog) {
|
|
64
|
+
return [
|
|
65
|
+
{ key: "S / Enter", label: "Confirmar" },
|
|
66
|
+
{ key: "N / Esc", label: "Cancelar" },
|
|
67
|
+
];
|
|
68
|
+
}
|
|
50
69
|
return [
|
|
51
70
|
{ key: "p", label: "Podar Caches" },
|
|
52
71
|
{ key: "b", label: "Limpar Navegadores" },
|
|
53
72
|
{ key: "z", label: "Zerar Cooldowns" },
|
|
73
|
+
{ key: "l", label: "Limpar Chats Qwen" },
|
|
54
74
|
{ key: "r", label: "Atualizar Disco" },
|
|
55
75
|
];
|
|
56
76
|
}
|
|
57
|
-
|
|
58
77
|
public async refresh(): Promise<void> {
|
|
59
78
|
if (this.isScanning) return;
|
|
60
79
|
this.isScanning = true;
|
|
@@ -102,11 +121,68 @@ export class StorageView implements TuiView {
|
|
|
102
121
|
}
|
|
103
122
|
|
|
104
123
|
public async handleKey(key: KeyEvent): Promise<boolean | void> {
|
|
124
|
+
// 0. Confirm Dialog Active
|
|
125
|
+
if (this.confirmDialog) {
|
|
126
|
+
if (key.name === "s" || key.name === "S" || key.name === "enter" || key.name === "return") {
|
|
127
|
+
const dialog = this.confirmDialog;
|
|
128
|
+
this.confirmDialog = null;
|
|
129
|
+
this.confirmDialogHovered = null;
|
|
130
|
+
await dialog.onConfirm();
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
if (key.name === "escape" || key.name === "n" || key.name === "N") {
|
|
134
|
+
this.confirmDialog = null;
|
|
135
|
+
this.confirmDialogHovered = null;
|
|
136
|
+
this.addLog(theme.muted("Ação cancelada"));
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
if (key.name === "hover" && key.mouse) {
|
|
140
|
+
const { row, col } = key.mouse;
|
|
141
|
+
const btnRow = this.lastConfirmModalStartRow + 4;
|
|
142
|
+
if (row === btnRow) {
|
|
143
|
+
const startCol = this.lastConfirmModalLeftPad + 2;
|
|
144
|
+
if (col >= startCol && col <= startCol + 25) {
|
|
145
|
+
this.confirmDialogHovered = "confirm";
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
if (col >= startCol + 26 && col <= startCol + 50) {
|
|
149
|
+
this.confirmDialogHovered = "cancel";
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (this.confirmDialogHovered !== null) {
|
|
154
|
+
this.confirmDialogHovered = null;
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (key.name === "click" && key.mouse) {
|
|
159
|
+
const { row, col } = key.mouse;
|
|
160
|
+
const btnRow = this.lastConfirmModalStartRow + 4;
|
|
161
|
+
if (row === btnRow) {
|
|
162
|
+
const startCol = this.lastConfirmModalLeftPad + 2;
|
|
163
|
+
if (col >= startCol && col <= startCol + 25) {
|
|
164
|
+
const dialog = this.confirmDialog;
|
|
165
|
+
this.confirmDialog = null;
|
|
166
|
+
this.confirmDialogHovered = null;
|
|
167
|
+
await dialog.onConfirm();
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
if (col >= startCol + 26 && col <= startCol + 50) {
|
|
171
|
+
this.confirmDialog = null;
|
|
172
|
+
this.confirmDialogHovered = null;
|
|
173
|
+
this.addLog(theme.muted("Ação cancelada"));
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
|
|
105
181
|
// Mouse hover over quick actions
|
|
106
182
|
if (key.name === "hover" && key.mouse) {
|
|
107
183
|
const { row, col } = key.mouse;
|
|
108
184
|
const leftW = this.lastLeftW || 48;
|
|
109
|
-
if (col >= 2 && col <= leftW - 1 && row >= 13 && row <=
|
|
185
|
+
if (col >= 2 && col <= leftW - 1 && row >= 13 && row <= 17) {
|
|
110
186
|
if (this.hoveredActionRow !== row) {
|
|
111
187
|
this.hoveredActionRow = row;
|
|
112
188
|
return true;
|
|
@@ -135,12 +211,15 @@ export class StorageView implements TuiView {
|
|
|
135
211
|
return true;
|
|
136
212
|
}
|
|
137
213
|
if (row === 16) {
|
|
214
|
+
this.handleKey({ name: "l", ctrl: false, shift: false, meta: false });
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
if (row === 17) {
|
|
138
218
|
this.handleKey({ name: "r", ctrl: false, shift: false, meta: false });
|
|
139
219
|
return true;
|
|
140
220
|
}
|
|
141
221
|
}
|
|
142
222
|
}
|
|
143
|
-
|
|
144
223
|
// Reset cooldowns with 'z'
|
|
145
224
|
if ((key.name === "z" || key.name === "Z") && !key.ctrl) {
|
|
146
225
|
const cleared = resetAllCooldowns();
|
|
@@ -187,7 +266,7 @@ export class StorageView implements TuiView {
|
|
|
187
266
|
} else {
|
|
188
267
|
this.addLog(
|
|
189
268
|
theme.green(
|
|
190
|
-
`✓ Navegadores verificados: nenhum navegador antigo encontrado
|
|
269
|
+
`✓ Navegadores verificados: nenhum navegador antigo encontrado`,
|
|
191
270
|
),
|
|
192
271
|
);
|
|
193
272
|
}
|
|
@@ -199,6 +278,29 @@ export class StorageView implements TuiView {
|
|
|
199
278
|
}
|
|
200
279
|
return true;
|
|
201
280
|
}
|
|
281
|
+
// Delete all remote chats with 'l' or 'L' (requires confirmation)
|
|
282
|
+
if ((key.name === "l" || key.name === "L") && !key.ctrl) {
|
|
283
|
+
this.confirmDialog = {
|
|
284
|
+
title: "⚠️ Confirmar Exclusão de Chats Remotos",
|
|
285
|
+
message: "Apagar TODOS os chats remotos de TODAS as contas no Qwen?",
|
|
286
|
+
detail: "Esta ação apagará permanentemente todas as conversas em chat.qwen.ai.",
|
|
287
|
+
onConfirm: async () => {
|
|
288
|
+
this.addLog(theme.yellow("⏳ Apagando chats no Qwen de todas as contas..."));
|
|
289
|
+
try {
|
|
290
|
+
const { deleteChatsForConfiguredAccounts } = await import("../../services/chat-cleanup.ts");
|
|
291
|
+
const res = await deleteChatsForConfiguredAccounts(true);
|
|
292
|
+
this.addLog(
|
|
293
|
+
theme.green(
|
|
294
|
+
`✓ Todos os chats remotos foram apagados no Qwen (${res.succeeded}/${res.attempted} contas)`,
|
|
295
|
+
),
|
|
296
|
+
);
|
|
297
|
+
} catch (err: any) {
|
|
298
|
+
this.addLog(theme.red(`✗ Falha ao apagar chats: ${err?.message || String(err)}`));
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
};
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
202
304
|
}
|
|
203
305
|
public render(width: number, height: number): string[] {
|
|
204
306
|
const contentH = Math.max(12, height);
|
|
@@ -233,7 +335,8 @@ export class StorageView implements TuiView {
|
|
|
233
335
|
` ${this.hoveredActionRow === 13 ? theme.bgHover(` ${theme.cyan("[ P ] Podar Caches")} `) : `${theme.cyan("[ P ]")} Podar Caches`}`,
|
|
234
336
|
` ${this.hoveredActionRow === 14 ? theme.bgHover(` ${theme.yellow("[ B ] Limpar Navegadores")} `) : `${theme.yellow("[ B ]")} Limpar Navegadores`}`,
|
|
235
337
|
` ${this.hoveredActionRow === 15 ? theme.bgHover(` ${theme.green("[ Z ] Zerar Todos os Cooldowns")} `) : `${theme.green("[ Z ]")} Zerar Todos os Cooldowns`}`,
|
|
236
|
-
` ${this.hoveredActionRow === 16 ? theme.bgHover(` ${theme.
|
|
338
|
+
` ${this.hoveredActionRow === 16 ? theme.bgHover(` ${theme.red("[ L ] Limpar Todos os Chats (Qwen)")} `) : `${theme.red("[ L ]")} Limpar Todos os Chats (Qwen)`}`,
|
|
339
|
+
` ${this.hoveredActionRow === 17 ? theme.bgHover(` ${theme.muted("[ R ] Atualizar Disco")} `) : `${theme.muted("[ R ]")} Atualizar Disco`}`,
|
|
237
340
|
"",
|
|
238
341
|
];
|
|
239
342
|
|
|
@@ -299,6 +402,38 @@ export class StorageView implements TuiView {
|
|
|
299
402
|
mergedLines.push(leftRow + " " + rightRow);
|
|
300
403
|
}
|
|
301
404
|
|
|
405
|
+
if (this.confirmDialog) {
|
|
406
|
+
const modalW = Math.min(width - 4, 66);
|
|
407
|
+
this.lastConfirmModalLeftPad = Math.max(0, Math.floor((width - modalW) / 2));
|
|
408
|
+
const confirmBtn =
|
|
409
|
+
this.confirmDialogHovered === "confirm"
|
|
410
|
+
? theme.bgHover(theme.red(" [ S / Enter ] Sim, Confirmar "))
|
|
411
|
+
: theme.red("[ S / Enter ] Sim, Confirmar");
|
|
412
|
+
const cancelBtn =
|
|
413
|
+
this.confirmDialogHovered === "cancel"
|
|
414
|
+
? theme.bgHover(theme.green(" [ N / Esc ] Cancelar "))
|
|
415
|
+
: theme.green("[ N / Esc ] Cancelar");
|
|
416
|
+
|
|
417
|
+
const modalContent = [
|
|
418
|
+
"",
|
|
419
|
+
` ${theme.bold(this.confirmDialog.message)}`,
|
|
420
|
+
` ${theme.muted(this.confirmDialog.detail)}`,
|
|
421
|
+
"",
|
|
422
|
+
` ${confirmBtn} ${cancelBtn}`,
|
|
423
|
+
];
|
|
424
|
+
|
|
425
|
+
const modalBox = drawBox({
|
|
426
|
+
title: this.confirmDialog.title,
|
|
427
|
+
width: modalW,
|
|
428
|
+
height: Math.min(contentH, 8),
|
|
429
|
+
borderColor: theme.red,
|
|
430
|
+
titleColor: theme.red,
|
|
431
|
+
content: modalContent,
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
const padStr = " ".repeat(this.lastConfirmModalLeftPad);
|
|
435
|
+
return modalBox.map((line) => padStr + line);
|
|
436
|
+
}
|
|
302
437
|
return mergedLines;
|
|
303
438
|
}
|
|
304
439
|
}
|