qwenproxy-cli 1.0.4 → 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 +2 -2
- 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/login.ts +5 -0
- package/src/services/playwright.ts +105 -8
- package/src/tui/app.ts +1 -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 +5 -3
- package/src/tui/views/status-view.ts +4 -4
- package/src/tui/views/storage-view.ts +11 -7
package/README.md
CHANGED
|
@@ -284,7 +284,7 @@ qpx
|
|
|
284
284
|
| `qpx login` | Abre navegador visível para autenticar novas contas interativamente |
|
|
285
285
|
| `qpx sync` | Configura e sincroniza clientes (Claude Code, Codex, OpenCode, OMP) |
|
|
286
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
|
|
287
|
+
| `qpx clean:all` | Limpa caches e remove versões antigas de navegadores órfãos em disco |
|
|
288
288
|
| `qpx purge` | Limpa o histórico de conversas remotas no Qwen de todas as contas |
|
|
289
289
|
| `qpx reset` | Reseta cooldowns e rate limits salvos no banco de dados |
|
|
290
290
|
### Opção 2: Execução Instantânea (Zero Instalação)
|
|
@@ -845,7 +845,7 @@ QwenProxy/
|
|
|
845
845
|
| `npm start` | Iniciar apenas o servidor QwenProxy em modo headless |
|
|
846
846
|
| `npm run sync` | Sincronizar clientes (Claude Code, Codex, OpenCode, OMP) com backup |
|
|
847
847
|
| `npm run clean` | Limpar caches temporários dos perfis Chromium (~4.5MB por conta) |
|
|
848
|
-
| `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 |
|
|
849
849
|
| `npm run reset` | Zerar cooldowns de contas no banco de dados |
|
|
850
850
|
| `npm run login` | Adicionar/autenticar novas contas visualmente no navegador |
|
|
851
851
|
| `npm run purge` | Limpar chats remotos do Qwen nas contas configuradas |
|
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/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.");
|
|
@@ -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
|
|
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/screen.ts
CHANGED
|
@@ -33,6 +33,10 @@ export class Screen {
|
|
|
33
33
|
private exitHandler: (() => void) | null = null;
|
|
34
34
|
private prevRenderedRows: string[] = [];
|
|
35
35
|
private originalStdinEmit: typeof process.stdin.emit | null = null;
|
|
36
|
+
private lastHoverCol = -1;
|
|
37
|
+
private lastHoverRow = -1;
|
|
38
|
+
private lastHoverTime = 0;
|
|
39
|
+
private pendingHoverTimeout: NodeJS.Timeout | null = null;
|
|
36
40
|
|
|
37
41
|
constructor() {
|
|
38
42
|
this.exitHandler = () => this.stop();
|
|
@@ -116,13 +120,42 @@ export class Screen {
|
|
|
116
120
|
mouse: { type: "drag", button: "left", col, row },
|
|
117
121
|
});
|
|
118
122
|
} else if (btn === 35) {
|
|
119
|
-
self.
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
123
|
+
if (col === self.lastHoverCol && row === self.lastHoverRow) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const now = Date.now();
|
|
127
|
+
const timeSinceLast = now - self.lastHoverTime;
|
|
128
|
+
const emitHover = (c: number, r: number) => {
|
|
129
|
+
if (!self.active) return;
|
|
130
|
+
self.lastHoverCol = c;
|
|
131
|
+
self.lastHoverRow = r;
|
|
132
|
+
self.lastHoverTime = Date.now();
|
|
133
|
+
self.dispatchKey({
|
|
134
|
+
name: "hover",
|
|
135
|
+
ctrl: false,
|
|
136
|
+
shift: false,
|
|
137
|
+
meta: false,
|
|
138
|
+
mouse: { type: "hover", col: c, row: r },
|
|
139
|
+
});
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
if (timeSinceLast >= 30) {
|
|
143
|
+
if (self.pendingHoverTimeout) {
|
|
144
|
+
clearTimeout(self.pendingHoverTimeout);
|
|
145
|
+
self.pendingHoverTimeout = null;
|
|
146
|
+
}
|
|
147
|
+
emitHover(col, row);
|
|
148
|
+
} else {
|
|
149
|
+
if (self.pendingHoverTimeout) {
|
|
150
|
+
clearTimeout(self.pendingHoverTimeout);
|
|
151
|
+
}
|
|
152
|
+
self.pendingHoverTimeout = setTimeout(() => {
|
|
153
|
+
self.pendingHoverTimeout = null;
|
|
154
|
+
if (col !== self.lastHoverCol || row !== self.lastHoverRow) {
|
|
155
|
+
emitHover(col, row);
|
|
156
|
+
}
|
|
157
|
+
}, 30 - timeSinceLast);
|
|
158
|
+
}
|
|
126
159
|
}
|
|
127
160
|
}
|
|
128
161
|
if (handled) {
|
|
@@ -212,6 +245,13 @@ export class Screen {
|
|
|
212
245
|
process.stdin.emit = this.originalStdinEmit;
|
|
213
246
|
this.originalStdinEmit = null;
|
|
214
247
|
}
|
|
248
|
+
if (this.pendingHoverTimeout) {
|
|
249
|
+
clearTimeout(this.pendingHoverTimeout);
|
|
250
|
+
this.pendingHoverTimeout = null;
|
|
251
|
+
}
|
|
252
|
+
this.lastHoverCol = -1;
|
|
253
|
+
this.lastHoverRow = -1;
|
|
254
|
+
this.lastHoverTime = 0;
|
|
215
255
|
|
|
216
256
|
// Restore original screen buffer, cursor, and disable all mouse tracking modes synchronously
|
|
217
257
|
const restoreSeq = ANSI.disableMouse + ANSI.exitAltScreen + ANSI.showCursor + ANSI.reset;
|
|
@@ -95,15 +95,23 @@ export class ServerManager {
|
|
|
95
95
|
let line = raw.trim();
|
|
96
96
|
if (!line) continue;
|
|
97
97
|
|
|
98
|
-
// Filter out raw ASCII box frames
|
|
98
|
+
// Filter out raw ASCII box frames and border rows entirely
|
|
99
99
|
if (/^[+\-=#]{5,}$/.test(line)) continue;
|
|
100
100
|
if (/^\|\s*\|$/.test(line)) continue;
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
101
|
+
if (line.startsWith("|") && line.endsWith("|")) continue;
|
|
102
|
+
|
|
103
|
+
// Filter out any box remnants (startup banner is for headless npm start only)
|
|
104
|
+
if (
|
|
105
|
+
line === "QwenProxy" ||
|
|
106
|
+
line === "OpenAI & Anthropic Compatible API" ||
|
|
107
|
+
/^Endpoint\s+http/i.test(line) ||
|
|
108
|
+
/^Port\s+\d+/i.test(line) ||
|
|
109
|
+
/^Accounts\s+\d+\/\d+/i.test(line) ||
|
|
110
|
+
/^API Key\s+/i.test(line) ||
|
|
111
|
+
/^Status\s+●/i.test(line)
|
|
112
|
+
) {
|
|
113
|
+
continue;
|
|
105
114
|
}
|
|
106
|
-
|
|
107
115
|
if (!line) continue;
|
|
108
116
|
|
|
109
117
|
// Prevent identical consecutive duplicate logs in the same second
|
|
@@ -224,7 +232,7 @@ export class ServerManager {
|
|
|
224
232
|
this.state = "online";
|
|
225
233
|
this.appendLog(
|
|
226
234
|
"INFO",
|
|
227
|
-
|
|
235
|
+
`✨ [Server] Conectado à instância em execução na porta ${port}`,
|
|
228
236
|
);
|
|
229
237
|
return;
|
|
230
238
|
}
|
|
@@ -234,25 +242,25 @@ export class ServerManager {
|
|
|
234
242
|
this.state = "warming";
|
|
235
243
|
this.appendLog(
|
|
236
244
|
"INFO",
|
|
237
|
-
|
|
245
|
+
`🚀 [Server] Iniciando servidor na porta ${port}...`,
|
|
238
246
|
);
|
|
239
247
|
|
|
240
248
|
this.interceptLogs();
|
|
241
249
|
|
|
242
250
|
this.startPromise = (async () => {
|
|
243
251
|
try {
|
|
244
|
-
await startServer({ installSignalHandlers: false });
|
|
252
|
+
await startServer({ installSignalHandlers: false, showBanner: false });
|
|
245
253
|
this.state = "online";
|
|
246
254
|
this.appendLog(
|
|
247
255
|
"INFO",
|
|
248
|
-
|
|
256
|
+
`✨ [Server] QwenProxy pronto e online em http://${cleanHost}:${port}/v1`,
|
|
249
257
|
);
|
|
250
258
|
} catch (err: any) {
|
|
251
259
|
this.state = "error";
|
|
252
260
|
this.lastError = err?.message || String(err);
|
|
253
261
|
this.appendLog(
|
|
254
262
|
"ERROR",
|
|
255
|
-
|
|
263
|
+
`❌ [Server] Falha ao iniciar servidor: ${this.lastError}`,
|
|
256
264
|
);
|
|
257
265
|
} finally {
|
|
258
266
|
this.startPromise = null;
|
package/src/tui/theme.ts
CHANGED
|
@@ -16,7 +16,7 @@ export const ANSI = {
|
|
|
16
16
|
showCursor: "\x1b[?25h",
|
|
17
17
|
enterAltScreen: "\x1b[?1049h",
|
|
18
18
|
exitAltScreen: "\x1b[?1049l",
|
|
19
|
-
enableMouse: "\x1b[?1000h\x1b[?1002h\x1b[?1006h",
|
|
19
|
+
enableMouse: "\x1b[?1000h\x1b[?1002h\x1b[?1003h\x1b[?1006h",
|
|
20
20
|
disableMouse: "\x1b[?1006l\x1b[?1005l\x1b[?1004l\x1b[?1003l\x1b[?1002l\x1b[?1000l\x1b[?1015l",
|
|
21
21
|
};
|
|
22
22
|
|
|
@@ -181,22 +181,39 @@ const ANSI_REGEX =
|
|
|
181
181
|
export function stripAnsi(str: string): string {
|
|
182
182
|
return str.replace(ANSI_REGEX, "");
|
|
183
183
|
}
|
|
184
|
+
const graphemeSegmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
|
|
184
185
|
|
|
185
186
|
/**
|
|
186
|
-
* Computes visual display width of string, accounting for wide characters
|
|
187
|
+
* Computes visual display width of string, accounting for wide characters,
|
|
188
|
+
* grapheme clusters, variation selectors (VS16), and terminal-wide emojis.
|
|
187
189
|
*/
|
|
188
190
|
export function stringWidth(str: string): number {
|
|
189
191
|
const clean = stripAnsi(str).replace(/\t/g, " ").replace(/\r/g, "");
|
|
190
192
|
let width = 0;
|
|
191
|
-
for (const
|
|
192
|
-
const code =
|
|
193
|
-
//
|
|
194
|
-
if (
|
|
193
|
+
for (const { segment } of graphemeSegmenter.segment(clean)) {
|
|
194
|
+
const code = segment.codePointAt(0) || 0;
|
|
195
|
+
// Standalone zero-width characters (variation selectors, zero-width space/joiner)
|
|
196
|
+
if (
|
|
197
|
+
segment === "\u200b" ||
|
|
198
|
+
segment === "\u200c" ||
|
|
199
|
+
segment === "\u200d" ||
|
|
200
|
+
(code >= 0xfe00 && code <= 0xfe0f && segment.length === 1)
|
|
201
|
+
) {
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
// Graphemes containing variation selector 16 (emoji presentation)
|
|
205
|
+
if (segment.includes("\ufe0f")) {
|
|
206
|
+
width += 2;
|
|
195
207
|
continue;
|
|
196
208
|
}
|
|
197
|
-
// Specific BMP emojis and symbols that occupy 2 visual terminal cells (e.g. ⚠️, ⚡, ✅, ❌, ✨,
|
|
209
|
+
// Specific BMP emojis and symbols that occupy 2 visual terminal cells (e.g. ⚠️, ⏱, ⚡, ✅, ❌, ✨, ☕, ⚙)
|
|
198
210
|
const isBmpEmoji =
|
|
199
211
|
code === 0x26a0 || // ⚠️ (WARNING SIGN)
|
|
212
|
+
code === 0x23f1 || // ⏱ (STOPWATCH)
|
|
213
|
+
code === 0x23f0 || // ⏰
|
|
214
|
+
code === 0x23f3 || // ⏳
|
|
215
|
+
code === 0x231a || // ⌚
|
|
216
|
+
code === 0x231b || // ⌛
|
|
200
217
|
code === 0x2705 || // ✅
|
|
201
218
|
code === 0x2728 || // ✨
|
|
202
219
|
code === 0x274c || // ❌
|
|
@@ -207,10 +224,9 @@ export function stringWidth(str: string): number {
|
|
|
207
224
|
code === 0x2b55 || // ⭕
|
|
208
225
|
code === 0x26a1 || // ⚡
|
|
209
226
|
code === 0x2615 || // ☕
|
|
210
|
-
code ===
|
|
211
|
-
code ===
|
|
212
|
-
|
|
213
|
-
code === 0x23f3; // ⏳
|
|
227
|
+
code === 0x2699 || // ⚙
|
|
228
|
+
code === 0x2709; // ✉
|
|
229
|
+
|
|
214
230
|
// Common emoji and CJK full-width ranges (SMP Emojis 0x1f300 - 0x1faff)
|
|
215
231
|
if (
|
|
216
232
|
isBmpEmoji ||
|
|
@@ -244,15 +260,15 @@ export function truncate(str: string, maxWidth: number, ellipsis = "…"): strin
|
|
|
244
260
|
const targetW = Math.max(0, maxWidth - ellipsisW);
|
|
245
261
|
|
|
246
262
|
let currentW = 0;
|
|
247
|
-
let
|
|
248
|
-
for (const
|
|
249
|
-
const
|
|
250
|
-
if (currentW +
|
|
251
|
-
currentW +=
|
|
252
|
-
|
|
263
|
+
let result = "";
|
|
264
|
+
for (const { segment } of graphemeSegmenter.segment(clean)) {
|
|
265
|
+
const segW = stringWidth(segment);
|
|
266
|
+
if (currentW + segW > targetW) break;
|
|
267
|
+
currentW += segW;
|
|
268
|
+
result += segment;
|
|
253
269
|
}
|
|
254
270
|
|
|
255
|
-
return
|
|
271
|
+
return result + ellipsis;
|
|
256
272
|
}
|
|
257
273
|
|
|
258
274
|
/**
|
|
@@ -328,6 +344,7 @@ export interface BoxOptions {
|
|
|
328
344
|
borderColor?: (s: string) => string;
|
|
329
345
|
titleColor?: (s: string) => string;
|
|
330
346
|
footerColor?: (s: string) => string;
|
|
347
|
+
wrap?: boolean;
|
|
331
348
|
content: string[];
|
|
332
349
|
}
|
|
333
350
|
|
|
@@ -379,16 +396,17 @@ export function drawBox(options: BoxOptions): string[] {
|
|
|
379
396
|
}
|
|
380
397
|
lines.push(borderColor(b.tl) + topHeader + borderColor(b.tr));
|
|
381
398
|
|
|
382
|
-
// Flatten
|
|
383
|
-
//
|
|
399
|
+
// Flatten content lines and optionally wrap prose lines.
|
|
400
|
+
// Fixed-height boxes do not auto-wrap by default to preserve row and scrollbar alignment.
|
|
401
|
+
const shouldWrap = options.wrap === true || (!options.height && options.wrap !== false);
|
|
384
402
|
const expandedContent: string[] = [];
|
|
385
403
|
for (const item of content) {
|
|
386
404
|
const subItems = String(item ?? "").split(/\r?\n/);
|
|
387
405
|
for (const sub of subItems) {
|
|
388
|
-
if (stringWidth(sub)
|
|
389
|
-
expandedContent.push(sub);
|
|
390
|
-
} else {
|
|
406
|
+
if (shouldWrap && stringWidth(sub) > innerW) {
|
|
391
407
|
expandedContent.push(...wrapContentLine(sub, innerW));
|
|
408
|
+
} else {
|
|
409
|
+
expandedContent.push(sub);
|
|
392
410
|
}
|
|
393
411
|
}
|
|
394
412
|
}
|
|
@@ -425,8 +425,10 @@ export class AccountsView implements TuiView {
|
|
|
425
425
|
onConfirm: async () => {
|
|
426
426
|
removeAccount(selected.id);
|
|
427
427
|
try {
|
|
428
|
-
const { closePlaywrightForAccount } = await import("../../services/playwright.ts");
|
|
428
|
+
const { closePlaywrightForAccount, removePlaywrightProfile } = await import("../../services/playwright.ts");
|
|
429
|
+
const { getAccountProfilePath } = await import("../../core/paths.ts");
|
|
429
430
|
await closePlaywrightForAccount(selected.id);
|
|
431
|
+
removePlaywrightProfile(getAccountProfilePath(selected.id));
|
|
430
432
|
} catch {}
|
|
431
433
|
await this.refresh();
|
|
432
434
|
this.setStatusMessage(theme.green(`✓ Conta ${selected.emailOrName} removida com sucesso`));
|
|
@@ -633,12 +635,12 @@ export class AccountsView implements TuiView {
|
|
|
633
635
|
const isHovered = idx === this.hoveredAccountIndex;
|
|
634
636
|
const pointer = isFocused ? theme.cyan(`${glyphs.pointer} `) : " ";
|
|
635
637
|
const num = pad(String(idx + 1) + ".", 4);
|
|
636
|
-
const name = pad(acc.emailOrName, 22);
|
|
638
|
+
const name = pad(truncate(acc.emailOrName, 20), 22);
|
|
637
639
|
|
|
638
640
|
let status = theme.green(`${glyphs.bullet} Pronto `);
|
|
639
641
|
if (acc.onCooldown) {
|
|
640
642
|
const mins = Math.max(1, Math.round(acc.remainingCooldownMs / 60000));
|
|
641
|
-
status = theme.yellow(
|
|
643
|
+
status = theme.yellow(`⚠️ ${mins}m cd `);
|
|
642
644
|
} else if (!acc.headersReady) {
|
|
643
645
|
status = acc.isInitialized
|
|
644
646
|
? theme.yellow(`◐ Aquecendo...`)
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import type { TuiView, ProxyStatusSnapshot } from "../types.ts";
|
|
6
6
|
import type { KeyEvent } from "../screen.ts";
|
|
7
|
-
import { theme, glyphs, drawBox, pad } from "../theme.ts";
|
|
7
|
+
import { theme, glyphs, drawBox, pad, truncate } from "../theme.ts";
|
|
8
8
|
import { fetchProxyStatus, resetAllCooldowns, formatUptime } from "../proxy-client.ts";
|
|
9
9
|
import { ServerManager } from "../server-manager.ts";
|
|
10
10
|
|
|
@@ -169,17 +169,17 @@ export class StatusView implements TuiView {
|
|
|
169
169
|
} else {
|
|
170
170
|
accounts.slice(0, contentH - 5).forEach((acc, idx) => {
|
|
171
171
|
const num = pad(String(idx + 1), 3);
|
|
172
|
-
const name = pad(acc.emailOrName,
|
|
172
|
+
const name = pad(truncate(acc.emailOrName, 20), 20);
|
|
173
173
|
let status = theme.green(`${glyphs.bullet} Pronto`);
|
|
174
174
|
if (acc.onCooldown) {
|
|
175
175
|
const mins = Math.max(1, Math.round(acc.remainingCooldownMs / 60000));
|
|
176
|
-
status = theme.yellow(
|
|
176
|
+
status = theme.yellow(`⚠️ Cooldown ${mins}m`);
|
|
177
177
|
} else if (!acc.headersReady) {
|
|
178
178
|
status = acc.isInitialized
|
|
179
179
|
? theme.yellow(`◐ Aquecendo...`)
|
|
180
180
|
: theme.muted(`○ Standby`);
|
|
181
181
|
}
|
|
182
|
-
rightContent.push(` ${num} ${name}
|
|
182
|
+
rightContent.push(` ${num} ${name} ${status}`);
|
|
183
183
|
});
|
|
184
184
|
}
|
|
185
185
|
|
|
@@ -7,7 +7,7 @@ import path from "node:path";
|
|
|
7
7
|
import type { TuiView } from "../types.ts";
|
|
8
8
|
import type { KeyEvent } from "../screen.ts";
|
|
9
9
|
import { theme, glyphs, drawBox, pad } from "../theme.ts";
|
|
10
|
-
import { pruneAllPlaywrightProfiles } from "../../services/playwright.ts";
|
|
10
|
+
import { pruneAllPlaywrightProfiles, cleanupOrphanProfiles } from "../../services/playwright.ts";
|
|
11
11
|
import { getProfilesDir } from "../../core/paths.ts";
|
|
12
12
|
import {
|
|
13
13
|
formatBytes,
|
|
@@ -79,6 +79,9 @@ export class StorageView implements TuiView {
|
|
|
79
79
|
this.isScanning = true;
|
|
80
80
|
|
|
81
81
|
try {
|
|
82
|
+
// 0. Auto-prune orphan directories from removed accounts
|
|
83
|
+
cleanupOrphanProfiles();
|
|
84
|
+
|
|
82
85
|
// 1. Scan profiles
|
|
83
86
|
const profilesDir = getProfilesDir();
|
|
84
87
|
let totalBytes = 0;
|
|
@@ -241,11 +244,12 @@ export class StorageView implements TuiView {
|
|
|
241
244
|
if ((key.name === "p" || key.name === "P") && !key.ctrl) {
|
|
242
245
|
try {
|
|
243
246
|
const res = pruneAllPlaywrightProfiles();
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
)
|
|
248
|
-
|
|
247
|
+
const orphanRes = cleanupOrphanProfiles();
|
|
248
|
+
let logMsg = `✓ Caches limpos: ${formatBytes(res.totalFreedBytes)} liberados em ${res.totalFreedFiles} arquivos (${res.profilesCleaned} perfis)`;
|
|
249
|
+
if (orphanRes.removedCount > 0) {
|
|
250
|
+
logMsg += ` + ${orphanRes.removedCount} perfil(is) órfão(s) removido(s)`;
|
|
251
|
+
}
|
|
252
|
+
this.addLog(theme.green(logMsg));
|
|
249
253
|
await this.refresh();
|
|
250
254
|
} catch (err: any) {
|
|
251
255
|
this.addLog(theme.red(`✗ Falha ao limpar perfis: ${err?.message || String(err)}`));
|
|
@@ -260,7 +264,7 @@ export class StorageView implements TuiView {
|
|
|
260
264
|
if (res.freedBytes > 0 || res.unusedDirs.length > 0) {
|
|
261
265
|
this.addLog(
|
|
262
266
|
theme.green(
|
|
263
|
-
`✓ Navegadores limpos: ${formatBytes(res.freedBytes)} recuperados
|
|
267
|
+
`✓ Navegadores limpos: ${formatBytes(res.freedBytes)} recuperados em disco (${res.unusedDirs.length} versões removidas)`,
|
|
264
268
|
),
|
|
265
269
|
);
|
|
266
270
|
} else {
|