qwenproxy-cli 1.0.25 → 1.0.27
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/package.json +3 -3
- package/src/api/server.ts +85 -4
- package/src/core/config.ts +9 -2
- package/src/core/metrics.ts +6 -1
- package/src/core/paths.ts +7 -0
- package/src/core/server-log-buffer.ts +105 -0
- package/src/routes/chat/account.ts +9 -2
- package/src/routes/chat/index.ts +6 -0
- package/src/services/chat-cleanup.ts +175 -3
- package/src/services/qwen-thread-state.ts +37 -0
- package/src/services/qwen.ts +110 -0
- package/src/sync/index.ts +47 -11
- package/src/tools/parser.ts +4 -1
- package/src/tui/app.ts +29 -20
- package/src/tui/index.ts +2 -2
- package/src/tui/proxy-client.ts +58 -4
- package/src/tui/server-manager.ts +46 -1
- package/src/tui/settings.ts +100 -0
- package/src/tui/theme.ts +15 -9
- package/src/tui/types.ts +19 -0
- package/src/tui/views/accounts-view.ts +50 -3
- package/src/tui/views/chat-view.ts +41 -2
- package/src/tui/views/logs-view.ts +22 -18
- package/src/tui/views/status-view.ts +180 -41
package/src/tui/theme.ts
CHANGED
|
@@ -47,11 +47,14 @@ export const theme = {
|
|
|
47
47
|
|
|
48
48
|
import { execSync, spawnSync } from "node:child_process";
|
|
49
49
|
|
|
50
|
+
let memoryClipboard = "";
|
|
51
|
+
|
|
50
52
|
/**
|
|
51
53
|
* Safely writes text to the system clipboard on Windows/macOS/Linux.
|
|
52
54
|
* Also emits OSC 52 to copy inside terminal emulators supporting it.
|
|
53
55
|
*/
|
|
54
56
|
export function setClipboardText(text: string): boolean {
|
|
57
|
+
memoryClipboard = text;
|
|
55
58
|
try {
|
|
56
59
|
// 1. Emit OSC 52 sequence for terminal emulators supporting it natively (only when interactive TTY)
|
|
57
60
|
try {
|
|
@@ -64,8 +67,7 @@ export function setClipboardText(text: string): boolean {
|
|
|
64
67
|
// 2. OS-level clipboard utility
|
|
65
68
|
if (process.platform === "win32") {
|
|
66
69
|
const p = spawnSync("clip.exe", {
|
|
67
|
-
input: text,
|
|
68
|
-
encoding: "utf-8",
|
|
70
|
+
input: Buffer.from(text, "utf16le"),
|
|
69
71
|
windowsHide: true,
|
|
70
72
|
});
|
|
71
73
|
return p.status === 0;
|
|
@@ -90,17 +92,21 @@ export function setClipboardText(text: string): boolean {
|
|
|
90
92
|
export function getClipboardText(): string {
|
|
91
93
|
try {
|
|
92
94
|
if (process.platform === "win32") {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
95
|
+
const res = execSync(
|
|
96
|
+
'powershell -NoProfile -Command "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; Get-Clipboard"',
|
|
97
|
+
{
|
|
98
|
+
timeout: 2000,
|
|
99
|
+
windowsHide: true,
|
|
100
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
101
|
+
},
|
|
102
|
+
)
|
|
103
|
+
.toString("utf8")
|
|
99
104
|
.replace(/\r?\n/g, "")
|
|
100
105
|
.trim();
|
|
106
|
+
if (res) return res;
|
|
101
107
|
}
|
|
102
108
|
} catch {}
|
|
103
|
-
return
|
|
109
|
+
return memoryClipboard;
|
|
104
110
|
}
|
|
105
111
|
|
|
106
112
|
export const glyphs = {
|
package/src/tui/types.ts
CHANGED
|
@@ -21,6 +21,22 @@ export interface ProxyStatusSnapshot {
|
|
|
21
21
|
systemMemoryPct?: number;
|
|
22
22
|
activeStreams?: number;
|
|
23
23
|
waitingStreams?: number;
|
|
24
|
+
metrics?: {
|
|
25
|
+
requestsTotal: number;
|
|
26
|
+
requestsErrors: number;
|
|
27
|
+
successRate: number;
|
|
28
|
+
latencyAvgMs: number;
|
|
29
|
+
deltasCount: number;
|
|
30
|
+
fullReplaysCount: number;
|
|
31
|
+
deltaRatio: number;
|
|
32
|
+
toolCallsCount: number;
|
|
33
|
+
toolCallsRecovered: number;
|
|
34
|
+
captchasDetected: number;
|
|
35
|
+
captchasSolved: number;
|
|
36
|
+
chatsCleaned: number;
|
|
37
|
+
cacheHitRatio?: number;
|
|
38
|
+
cacheBytesSaved?: number;
|
|
39
|
+
};
|
|
24
40
|
accounts: Array<{
|
|
25
41
|
id: string;
|
|
26
42
|
emailOrName: string;
|
|
@@ -28,7 +44,10 @@ export interface ProxyStatusSnapshot {
|
|
|
28
44
|
cooldownUntil: number | null;
|
|
29
45
|
onCooldown: boolean;
|
|
30
46
|
remainingCooldownMs: number;
|
|
47
|
+
cooldownReason?: string | null;
|
|
31
48
|
headersReady: boolean;
|
|
32
49
|
isInitialized?: boolean;
|
|
50
|
+
activeStreams?: number;
|
|
51
|
+
streamLimit?: number;
|
|
33
52
|
}>;
|
|
34
53
|
}
|
|
@@ -13,6 +13,33 @@ import {
|
|
|
13
13
|
import { addAccount, removeAccount } from "../../core/accounts.ts";
|
|
14
14
|
import { ServerManager } from "../server-manager.ts";
|
|
15
15
|
import { config } from "../../core/config.ts";
|
|
16
|
+
export function formatCooldownReason(reason?: string | null, maxLen = 28): string {
|
|
17
|
+
if (!reason) return theme.yellow("Cooldown ativo");
|
|
18
|
+
if (
|
|
19
|
+
reason.startsWith("AuthFailed") ||
|
|
20
|
+
reason.startsWith("AuthPermanentFailure") ||
|
|
21
|
+
reason.includes("All login methods exhausted")
|
|
22
|
+
) {
|
|
23
|
+
return theme.red(truncate("❌ Senha/Login inválido", maxLen));
|
|
24
|
+
}
|
|
25
|
+
if (reason === "AuthInitFailed") {
|
|
26
|
+
return theme.yellow(truncate("⚠️ Timeout Inicial (WAF/Headers)", maxLen));
|
|
27
|
+
}
|
|
28
|
+
if (reason === "RateLimited" || reason === "QuotaExceeded") {
|
|
29
|
+
return theme.yellow(truncate("⏳ Cota Excedida (Reset 00:00 UTC)", maxLen));
|
|
30
|
+
}
|
|
31
|
+
if (reason === "WafChallenge") {
|
|
32
|
+
return theme.peach(truncate("🛡️ Bloqueio WAF/Anti-Bot", maxLen));
|
|
33
|
+
}
|
|
34
|
+
if (reason.startsWith("StandbyValidationError")) {
|
|
35
|
+
return theme.red(truncate("❌ Falha Validação Standby", maxLen));
|
|
36
|
+
}
|
|
37
|
+
if (reason === "MediaGenFailed") {
|
|
38
|
+
return theme.yellow(truncate("⚠️ Falha Geração de Mídia", maxLen));
|
|
39
|
+
}
|
|
40
|
+
return theme.yellow(truncate(reason, maxLen));
|
|
41
|
+
}
|
|
42
|
+
|
|
16
43
|
export class AccountsView implements TuiView {
|
|
17
44
|
public readonly id = "accounts";
|
|
18
45
|
public readonly title = "Contas";
|
|
@@ -662,8 +689,22 @@ export class AccountsView implements TuiView {
|
|
|
662
689
|
|
|
663
690
|
let status = theme.green(`${glyphs.bullet} Pronto `);
|
|
664
691
|
if (acc.onCooldown) {
|
|
665
|
-
const
|
|
666
|
-
|
|
692
|
+
const reason = acc.cooldownReason || "";
|
|
693
|
+
if (
|
|
694
|
+
reason.startsWith("AuthFailed") ||
|
|
695
|
+
reason.startsWith("AuthPermanentFailure") ||
|
|
696
|
+
reason.includes("All login methods exhausted")
|
|
697
|
+
) {
|
|
698
|
+
status = theme.red(`❌ Auth Fail `);
|
|
699
|
+
} else if (reason === "WafChallenge") {
|
|
700
|
+
status = theme.peach(`🛡️ WAF Block `);
|
|
701
|
+
} else if (reason === "AuthInitFailed") {
|
|
702
|
+
const mins = Math.max(1, Math.round(acc.remainingCooldownMs / 60000));
|
|
703
|
+
status = theme.yellow(`⚠️ ${mins}m init `);
|
|
704
|
+
} else {
|
|
705
|
+
const mins = Math.max(1, Math.round(acc.remainingCooldownMs / 60000));
|
|
706
|
+
status = theme.yellow(`⚠️ ${mins}m cd `);
|
|
707
|
+
}
|
|
667
708
|
} else if (!acc.headersReady) {
|
|
668
709
|
status = acc.isInitialized
|
|
669
710
|
? theme.yellow(`◐ Aquecendo...`)
|
|
@@ -724,7 +765,13 @@ export class AccountsView implements TuiView {
|
|
|
724
765
|
? theme.yellow(`◐ Aquecendo...`)
|
|
725
766
|
: theme.muted(`${glyphs.circle} Standby (Sob Demanda)`);
|
|
726
767
|
rightContent.push(` ${theme.bold("Headers:")} ${hStatus}`);
|
|
727
|
-
|
|
768
|
+
if (selected.onCooldown && selected.cooldownReason) {
|
|
769
|
+
const maxReasonW = Math.max(16, rightW - 14);
|
|
770
|
+
const cdReason = formatCooldownReason(selected.cooldownReason, maxReasonW);
|
|
771
|
+
rightContent.push(` ${theme.bold("Motivo:")} ${cdReason}`);
|
|
772
|
+
} else {
|
|
773
|
+
rightContent.push("");
|
|
774
|
+
}
|
|
728
775
|
rightContent.push(` ${theme.dim("─────────────────────────────────")}`);
|
|
729
776
|
rightContent.push(` ${this.hoveredActionRow === 15 ? theme.bgHover(` ${theme.cyan("[ A ] Adicionar Conta")} `) : `${theme.cyan("[ A ]")} Adicionar Conta`}`);
|
|
730
777
|
rightContent.push(` ${this.hoveredActionRow === 16 ? theme.bgHover(` ${theme.red("[ D ] Remover Conta")} `) : `${theme.red("[ D ]")} Remover Conta`}`);
|
|
@@ -8,6 +8,7 @@ import { theme, glyphs, drawBox, stringWidth, truncate, stripAnsi, pad, wrapCont
|
|
|
8
8
|
import { streamChatCompletions, fetchLiveModels } from "../proxy-client.ts";
|
|
9
9
|
import { ServerManager } from "../server-manager.ts";
|
|
10
10
|
import { formatMarkdown, formatReasoning } from "../markdown.ts";
|
|
11
|
+
import { loadTuiSettings, saveTuiSettings } from "../settings.ts";
|
|
11
12
|
|
|
12
13
|
interface ChatMessage {
|
|
13
14
|
role: "user" | "assistant";
|
|
@@ -122,6 +123,21 @@ export class ChatView implements TuiView {
|
|
|
122
123
|
|
|
123
124
|
constructor(onNeedsRender?: () => void) {
|
|
124
125
|
this.onNeedsRender = onNeedsRender;
|
|
126
|
+
const saved = loadTuiSettings();
|
|
127
|
+
if (saved.chat?.model) {
|
|
128
|
+
const idx = this.availableModels.indexOf(saved.chat.model);
|
|
129
|
+
if (idx !== -1) {
|
|
130
|
+
this.selectedModelIndex = idx;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const savedEffort = saved.chat?.effort;
|
|
134
|
+
if (savedEffort && ["high", "medium", "low"].includes(savedEffort)) {
|
|
135
|
+
this.selectedEffort = savedEffort;
|
|
136
|
+
const effIdx = this.availableEfforts.findIndex((e) => e.id === savedEffort);
|
|
137
|
+
if (effIdx !== -1) {
|
|
138
|
+
this.effortSelectedIndex = effIdx;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
125
141
|
void this.refreshModels();
|
|
126
142
|
}
|
|
127
143
|
public onActivate(): void {
|
|
@@ -138,7 +154,13 @@ export class ChatView implements TuiView {
|
|
|
138
154
|
if (live.length > 0) {
|
|
139
155
|
const current = this.availableModels[this.selectedModelIndex];
|
|
140
156
|
this.availableModels = live;
|
|
141
|
-
|
|
157
|
+
let foundIdx = this.availableModels.indexOf(current);
|
|
158
|
+
if (foundIdx === -1) {
|
|
159
|
+
const saved = loadTuiSettings();
|
|
160
|
+
if (saved.chat?.model) {
|
|
161
|
+
foundIdx = this.availableModels.indexOf(saved.chat.model);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
142
164
|
this.selectedModelIndex = foundIdx !== -1 ? foundIdx : 0;
|
|
143
165
|
this.onNeedsRender?.();
|
|
144
166
|
}
|
|
@@ -169,7 +191,12 @@ export class ChatView implements TuiView {
|
|
|
169
191
|
if (!chosen) return;
|
|
170
192
|
this.selectedModelIndex = idx;
|
|
171
193
|
this.isModelModalOpen = false;
|
|
172
|
-
|
|
194
|
+
saveTuiSettings({
|
|
195
|
+
chat: {
|
|
196
|
+
model: chosen,
|
|
197
|
+
effort: this.selectedEffort,
|
|
198
|
+
},
|
|
199
|
+
});
|
|
173
200
|
const info = classifyModel(chosen);
|
|
174
201
|
if (info.category === "Texto & Raciocínio") {
|
|
175
202
|
this.isEffortModalOpen = true;
|
|
@@ -249,6 +276,12 @@ export class ChatView implements TuiView {
|
|
|
249
276
|
this.selectedEffort = this.availableEfforts[row - 9].id;
|
|
250
277
|
this.isEffortModalOpen = false;
|
|
251
278
|
const currentM = this.availableModels[this.selectedModelIndex];
|
|
279
|
+
saveTuiSettings({
|
|
280
|
+
chat: {
|
|
281
|
+
model: currentM,
|
|
282
|
+
effort: this.selectedEffort,
|
|
283
|
+
},
|
|
284
|
+
});
|
|
252
285
|
this.statusNote = `Modelo: ${currentM} | Effort: ${this.availableEfforts[row - 9].label}`;
|
|
253
286
|
this.onNeedsRender?.();
|
|
254
287
|
return true;
|
|
@@ -274,6 +307,12 @@ export class ChatView implements TuiView {
|
|
|
274
307
|
this.selectedEffort = this.availableEfforts[this.effortSelectedIndex].id;
|
|
275
308
|
this.isEffortModalOpen = false;
|
|
276
309
|
const currentM = this.availableModels[this.selectedModelIndex];
|
|
310
|
+
saveTuiSettings({
|
|
311
|
+
chat: {
|
|
312
|
+
model: currentM,
|
|
313
|
+
effort: this.selectedEffort,
|
|
314
|
+
},
|
|
315
|
+
});
|
|
277
316
|
this.statusNote = `Modelo: ${currentM} | Effort: ${this.availableEfforts[this.effortSelectedIndex].label}`;
|
|
278
317
|
this.onNeedsRender?.();
|
|
279
318
|
return true;
|
|
@@ -7,6 +7,7 @@ import type { TuiView } from "../types.ts";
|
|
|
7
7
|
import type { KeyEvent } from "../screen.ts";
|
|
8
8
|
import { theme, drawBox, stringWidth, truncate, pad, stripAnsi, setClipboardText } from "../theme.ts";
|
|
9
9
|
import { ServerManager } from "../server-manager.ts";
|
|
10
|
+
import { loadTuiSettings, saveTuiSettings } from "../settings.ts";
|
|
10
11
|
|
|
11
12
|
export class LogsView implements TuiView {
|
|
12
13
|
public readonly id = "logs";
|
|
@@ -14,6 +15,21 @@ export class LogsView implements TuiView {
|
|
|
14
15
|
public readonly tabNumber = 6;
|
|
15
16
|
|
|
16
17
|
private filter: "all" | "warn" | "error" = "all";
|
|
18
|
+
|
|
19
|
+
constructor() {
|
|
20
|
+
const saved = loadTuiSettings();
|
|
21
|
+
if (saved.logs?.filter && ["all", "warn", "error"].includes(saved.logs.filter)) {
|
|
22
|
+
this.filter = saved.logs.filter;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private setFilter(newFilter: "all" | "warn" | "error"): void {
|
|
27
|
+
this.filter = newFilter;
|
|
28
|
+
this.scrollOffset = 0;
|
|
29
|
+
this.selectedLogIndex = null;
|
|
30
|
+
saveTuiSettings({ logs: { filter: newFilter } });
|
|
31
|
+
}
|
|
32
|
+
|
|
17
33
|
private scrollOffset = 0; // 0 = at the bottom (follow newest)
|
|
18
34
|
private hoveredChip: "all" | "warn" | "error" | "copy" | "clear" | null = null;
|
|
19
35
|
private selectedLogIndex: number | null = null;
|
|
@@ -103,21 +119,15 @@ export class LogsView implements TuiView {
|
|
|
103
119
|
for (const c of chips) {
|
|
104
120
|
if (col >= c.startCol && col <= c.endCol) {
|
|
105
121
|
if (c.id === "all") {
|
|
106
|
-
this.
|
|
107
|
-
this.scrollOffset = 0;
|
|
108
|
-
this.selectedLogIndex = null;
|
|
122
|
+
this.setFilter("all");
|
|
109
123
|
return true;
|
|
110
124
|
}
|
|
111
125
|
if (c.id === "warn") {
|
|
112
|
-
this.
|
|
113
|
-
this.scrollOffset = 0;
|
|
114
|
-
this.selectedLogIndex = null;
|
|
126
|
+
this.setFilter("warn");
|
|
115
127
|
return true;
|
|
116
128
|
}
|
|
117
129
|
if (c.id === "error") {
|
|
118
|
-
this.
|
|
119
|
-
this.scrollOffset = 0;
|
|
120
|
-
this.selectedLogIndex = null;
|
|
130
|
+
this.setFilter("error");
|
|
121
131
|
return true;
|
|
122
132
|
}
|
|
123
133
|
if (c.id === "copy") {
|
|
@@ -202,21 +212,15 @@ export class LogsView implements TuiView {
|
|
|
202
212
|
|
|
203
213
|
// Filter toggles
|
|
204
214
|
if ((key.name === "t" || key.name === "T") && !key.ctrl) {
|
|
205
|
-
this.
|
|
206
|
-
this.scrollOffset = 0;
|
|
207
|
-
this.selectedLogIndex = null;
|
|
215
|
+
this.setFilter("all");
|
|
208
216
|
return true;
|
|
209
217
|
}
|
|
210
218
|
if ((key.name === "w" || key.name === "W") && !key.ctrl) {
|
|
211
|
-
this.
|
|
212
|
-
this.scrollOffset = 0;
|
|
213
|
-
this.selectedLogIndex = null;
|
|
219
|
+
this.setFilter("warn");
|
|
214
220
|
return true;
|
|
215
221
|
}
|
|
216
222
|
if ((key.name === "e" || key.name === "E") && !key.ctrl) {
|
|
217
|
-
this.
|
|
218
|
-
this.scrollOffset = 0;
|
|
219
|
-
this.selectedLogIndex = null;
|
|
223
|
+
this.setFilter("error");
|
|
220
224
|
return true;
|
|
221
225
|
}
|
|
222
226
|
|
|
@@ -8,6 +8,48 @@ 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
|
|
|
11
|
+
export function renderProgressBar(
|
|
12
|
+
pct: number,
|
|
13
|
+
width = 8,
|
|
14
|
+
colorFn: (s: string) => string = theme.cyan,
|
|
15
|
+
): string {
|
|
16
|
+
const safePct = Math.max(0, Math.min(100, isNaN(pct) ? 0 : pct));
|
|
17
|
+
const rawFilled = Math.round((safePct / 100) * width);
|
|
18
|
+
const filled = safePct > 0.05 ? Math.max(1, rawFilled) : 0;
|
|
19
|
+
const empty = Math.max(0, width - filled);
|
|
20
|
+
return colorFn("█".repeat(filled)) + theme.muted("░".repeat(empty));
|
|
21
|
+
}
|
|
22
|
+
export function getAccountReadinessScore(acc: {
|
|
23
|
+
onCooldown: boolean;
|
|
24
|
+
headersReady: boolean;
|
|
25
|
+
activeStreams?: number;
|
|
26
|
+
isInitialized?: boolean;
|
|
27
|
+
cooldownReason?: string | null;
|
|
28
|
+
}): number {
|
|
29
|
+
if (acc.onCooldown) {
|
|
30
|
+
const reason = acc.cooldownReason || "";
|
|
31
|
+
if (
|
|
32
|
+
reason.startsWith("AuthFailed") ||
|
|
33
|
+
reason.startsWith("AuthPermanentFailure") ||
|
|
34
|
+
reason.includes("login methods exhausted")
|
|
35
|
+
) {
|
|
36
|
+
return 0; // Auth Fail at the very bottom
|
|
37
|
+
}
|
|
38
|
+
return 10; // Other cooldowns (RateLimited, WafChallenge, etc.)
|
|
39
|
+
}
|
|
40
|
+
if (acc.activeStreams && acc.activeStreams > 0 && acc.headersReady) {
|
|
41
|
+
return 100; // Actively generating code
|
|
42
|
+
}
|
|
43
|
+
if (acc.headersReady) {
|
|
44
|
+
return 80; // Ready / Warm
|
|
45
|
+
}
|
|
46
|
+
if (acc.isInitialized) {
|
|
47
|
+
return 60; // Warming up
|
|
48
|
+
}
|
|
49
|
+
return 40; // Healthy standby
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
11
53
|
export class StatusView implements TuiView {
|
|
12
54
|
public readonly id = "status";
|
|
13
55
|
public readonly title = "Status";
|
|
@@ -17,8 +59,9 @@ export class StatusView implements TuiView {
|
|
|
17
59
|
private actionMessage = "";
|
|
18
60
|
private actionMessageTimeout: NodeJS.Timeout | null = null;
|
|
19
61
|
private hoveredActionRow: number | null = null;
|
|
20
|
-
private lastLeftW =
|
|
21
|
-
|
|
62
|
+
private lastLeftW = 38;
|
|
63
|
+
private lastActionRecarregarRow = 20;
|
|
64
|
+
private lastActionZerarRow = 21;
|
|
22
65
|
constructor() {
|
|
23
66
|
this.refresh();
|
|
24
67
|
}
|
|
@@ -58,8 +101,12 @@ export class StatusView implements TuiView {
|
|
|
58
101
|
// Mouse hover over quick actions
|
|
59
102
|
if (key.name === "hover" && key.mouse) {
|
|
60
103
|
const { row, col } = key.mouse;
|
|
61
|
-
const leftW = this.lastLeftW ||
|
|
62
|
-
if (
|
|
104
|
+
const leftW = this.lastLeftW || 38;
|
|
105
|
+
if (
|
|
106
|
+
col >= 2 &&
|
|
107
|
+
col <= leftW - 1 &&
|
|
108
|
+
(row === this.lastActionRecarregarRow || row === this.lastActionZerarRow)
|
|
109
|
+
) {
|
|
63
110
|
if (this.hoveredActionRow !== row) {
|
|
64
111
|
this.hoveredActionRow = row;
|
|
65
112
|
return true;
|
|
@@ -73,14 +120,14 @@ export class StatusView implements TuiView {
|
|
|
73
120
|
// Mouse click interactions
|
|
74
121
|
if (key.name === "click" && key.mouse) {
|
|
75
122
|
const { row, col } = key.mouse;
|
|
76
|
-
const leftW = this.lastLeftW ||
|
|
123
|
+
const leftW = this.lastLeftW || 38;
|
|
77
124
|
if (col >= 2 && col <= leftW - 1) {
|
|
78
|
-
if (row ===
|
|
125
|
+
if (row === this.lastActionRecarregarRow) {
|
|
79
126
|
await this.refresh();
|
|
80
127
|
this.setMessage(theme.green("✓ Status atualizado"));
|
|
81
128
|
return true;
|
|
82
129
|
}
|
|
83
|
-
if (row ===
|
|
130
|
+
if (row === this.lastActionZerarRow) {
|
|
84
131
|
const cleared = resetAllCooldowns();
|
|
85
132
|
await this.refresh();
|
|
86
133
|
this.setMessage(theme.green(`✓ Cooldowns zerados: ${cleared} conta(s) liberada(s)`));
|
|
@@ -108,12 +155,13 @@ export class StatusView implements TuiView {
|
|
|
108
155
|
const isOnline = data?.online ?? false;
|
|
109
156
|
const contentH = Math.max(10, height);
|
|
110
157
|
|
|
111
|
-
// Two-column layout
|
|
112
|
-
|
|
158
|
+
// Two-column layout: give left box 48-52 cols so rich metrics never truncate,
|
|
159
|
+
// and right box takes the remaining width (at least 38 cols).
|
|
160
|
+
const leftW = width >= 96
|
|
161
|
+
? Math.min(52, Math.max(48, Math.floor(width * 0.48)))
|
|
162
|
+
: Math.min(46, Math.max(38, Math.floor(width * 0.50)));
|
|
113
163
|
this.lastLeftW = leftW;
|
|
114
|
-
const rightW = Math.max(
|
|
115
|
-
|
|
116
|
-
// Left Column: System & Proxy Status
|
|
164
|
+
const rightW = Math.max(36, width - leftW - 1);
|
|
117
165
|
const serverState = ServerManager.getInstance().getState();
|
|
118
166
|
let onlineBadge: string;
|
|
119
167
|
if (isOnline || serverState === "online") {
|
|
@@ -128,67 +176,158 @@ export class StatusView implements TuiView {
|
|
|
128
176
|
|
|
129
177
|
const uptimeSecs = data?.uptimeSeconds || Math.floor(process.uptime());
|
|
130
178
|
const uptimeStr = formatUptime(uptimeSecs);
|
|
131
|
-
|
|
132
179
|
const baseUrl = `http://${data?.host || "127.0.0.1"}:${data?.port || 7936}/v1`;
|
|
133
180
|
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
181
|
+
const m = data?.metrics;
|
|
182
|
+
const reqsTotal = m?.requestsTotal ?? 0;
|
|
183
|
+
const reqsErrors = m?.requestsErrors ?? 0;
|
|
184
|
+
const successPct = m?.successRate ?? (reqsTotal > 0 ? Number((((reqsTotal - reqsErrors) / reqsTotal) * 100).toFixed(1)) : 100);
|
|
185
|
+
const latencyAvg = m?.latencyAvgMs ? `${m.latencyAvgMs}ms` : "–";
|
|
186
|
+
const deltasCount = m?.deltasCount ?? 0;
|
|
187
|
+
const fullCount = m?.fullReplaysCount ?? 0;
|
|
188
|
+
const deltaRatio = m?.deltaRatio != null ? `${m.deltaRatio}%` : "–";
|
|
189
|
+
const toolCalls = m?.toolCallsCount ?? 0;
|
|
190
|
+
const toolRecovered = m?.toolCallsRecovered ?? 0;
|
|
191
|
+
const captchasDetected = m?.captchasDetected ?? 0;
|
|
192
|
+
const captchasSolved = m?.captchasSolved ?? 0;
|
|
193
|
+
const chatsCleaned = m?.chatsCleaned ?? 0;
|
|
194
|
+
const lbl = (s: string) => pad(s, 13);
|
|
195
|
+
const innerLeftW = Math.max(30, leftW - 2);
|
|
196
|
+
const deltaDetail = innerLeftW < 44
|
|
197
|
+
? `(${deltasCount}d / ${fullCount}f)`
|
|
198
|
+
: `(${deltasCount} delta / ${fullCount} full)`;
|
|
199
|
+
const ramPct = data?.systemMemoryPct || 0;
|
|
200
|
+
const ramBarColor = ramPct >= 80 ? theme.red : ramPct >= 60 ? theme.yellow : theme.cyan;
|
|
201
|
+
const ramBar = renderProgressBar(ramPct, 7, ramBarColor);
|
|
202
|
+
const ramStr = `${data?.rssMb || 0} MB [${ramBar}] ${ramPct}%`;
|
|
203
|
+
|
|
204
|
+
let connsStr = "0 ativas";
|
|
205
|
+
if (data?.activeStreams && data.activeStreams > 0) {
|
|
206
|
+
connsStr = theme.yellow(`⚡ ${data.activeStreams} ativa(s)`);
|
|
207
|
+
} else {
|
|
208
|
+
connsStr = theme.dim("0 ativas");
|
|
209
|
+
}
|
|
210
|
+
if (data?.waitingStreams && data.waitingStreams > 0) {
|
|
211
|
+
connsStr += theme.peach(` (${data.waitingStreams} na fila)`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const leftContent: string[] = [
|
|
215
|
+
` ${theme.bold(lbl("Status:"))} ${onlineBadge}`,
|
|
216
|
+
` ${theme.bold(lbl("Base URL:"))} ${theme.cyan(baseUrl)}`,
|
|
217
|
+
` ${theme.bold(lbl("Uptime:"))} ${theme.cyan(uptimeStr)}`,
|
|
218
|
+
` ${theme.bold(lbl("Memória:"))} ${theme.cyan(ramStr)}`,
|
|
219
|
+
` ${theme.bold(lbl("Conexões:"))} ${connsStr}`,
|
|
220
|
+
` ${theme.dim("───────────────────────────────────────")}`,
|
|
221
|
+
` ${theme.bold("Tráfego & Performance:")}`,
|
|
222
|
+
` ${theme.dim(lbl("Requisições:"))} ${theme.cyan(String(reqsTotal))} ${theme.green(`(${successPct}% ok)`)} · ${reqsErrors > 0 ? theme.red(`${reqsErrors} err`) : theme.dim("0 err")}`,
|
|
223
|
+
` ${theme.dim(lbl("Latência:"))} ${theme.yellow(latencyAvg)} méd`,
|
|
224
|
+
` ${theme.dim(lbl("Deltas:"))} ${theme.green(deltaRatio)} ${theme.dim(deltaDetail)}`,
|
|
225
|
+
` ${theme.dim("───────────────────────────────────────")}`,
|
|
226
|
+
` ${theme.bold("Agentes & Operações:")}`,
|
|
227
|
+
` ${theme.dim(lbl("Tool Calls:"))} ${theme.cyan(String(toolCalls))} ${toolRecovered > 0 ? theme.green(`(${toolRecovered} curadas)`) : ""}`,
|
|
228
|
+
` ${theme.dim(lbl("Captchas:"))} ${captchasSolved > 0 ? theme.green(`${captchasSolved}/${captchasDetected} resolvidos`) : theme.dim(`${captchasDetected} detectados`)}`,
|
|
229
|
+
` ${theme.dim(lbl("Chats Limpos:"))} ${theme.cyan(String(chatsCleaned))} ${theme.dim("excluídos (>24h)")}`,
|
|
230
|
+
` ${theme.dim("───────────────────────────────────────")}`,
|
|
142
231
|
` ${theme.bold("Ações:")}`,
|
|
143
|
-
` ${this.hoveredActionRow === 13 ? theme.bgHover(` ${theme.cyan("[ R ] Recarregar")} `) : `${theme.cyan("[ R ]")} Recarregar`}`,
|
|
144
|
-
` ${this.hoveredActionRow === 14 ? theme.bgHover(` ${theme.yellow("[ Z ] Zerar Cooldowns")} `) : `${theme.yellow("[ Z ]")} Zerar Cooldowns`}`,
|
|
145
|
-
"",
|
|
146
|
-
this.actionMessage ? ` ${this.actionMessage}` : "",
|
|
147
232
|
];
|
|
148
233
|
|
|
234
|
+
const recarregarIdx = leftContent.length;
|
|
235
|
+
const zerarIdx = leftContent.length + 1;
|
|
236
|
+
this.lastActionRecarregarRow = 5 + recarregarIdx;
|
|
237
|
+
this.lastActionZerarRow = 5 + zerarIdx;
|
|
238
|
+
|
|
239
|
+
leftContent.push(
|
|
240
|
+
` ${this.hoveredActionRow === this.lastActionRecarregarRow ? theme.bgHover(` ${theme.cyan("[ R ] Recarregar")} `) : ` ${theme.cyan("[ R ]")} Recarregar`}`,
|
|
241
|
+
` ${this.hoveredActionRow === this.lastActionZerarRow ? theme.bgHover(` ${theme.yellow("[ Z ] Zerar Cooldowns")} `) : ` ${theme.yellow("[ Z ]")} Zerar Cooldowns`}`,
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
const boxHeight = Math.max(contentH, leftContent.length + 2);
|
|
245
|
+
|
|
149
246
|
const leftBox = drawBox({
|
|
150
|
-
title: "Sistema",
|
|
247
|
+
title: "Sistema & Performance",
|
|
151
248
|
width: leftW,
|
|
152
|
-
height:
|
|
249
|
+
height: boxHeight,
|
|
153
250
|
borderColor: theme.borderInactive,
|
|
154
251
|
titleColor: theme.cyan,
|
|
252
|
+
footer: this.actionMessage || undefined,
|
|
155
253
|
content: leftContent,
|
|
156
254
|
});
|
|
157
|
-
|
|
158
255
|
// Right Column: Accounts Pool Status
|
|
159
|
-
const
|
|
256
|
+
const rawAccounts = data?.accounts || [];
|
|
257
|
+
const accounts = [...rawAccounts].sort((a, b) => {
|
|
258
|
+
const scoreA = getAccountReadinessScore(a);
|
|
259
|
+
const scoreB = getAccountReadinessScore(b);
|
|
260
|
+
if (scoreB !== scoreA) {
|
|
261
|
+
return scoreB - scoreA;
|
|
262
|
+
}
|
|
263
|
+
return 0;
|
|
264
|
+
});
|
|
265
|
+
const availableCount = accounts.filter((a) => !a.onCooldown).length;
|
|
160
266
|
const readyCount = accounts.filter((a) => !a.onCooldown && a.headersReady).length;
|
|
267
|
+
const standbyCount = accounts.filter((a) => !a.onCooldown && !a.headersReady).length;
|
|
268
|
+
const cooldownCount = accounts.filter((a) => a.onCooldown).length;
|
|
269
|
+
const poolPct = accounts.length > 0 ? Math.round((availableCount / accounts.length) * 100) : 0;
|
|
270
|
+
const poolColor = poolPct >= 70 ? theme.green : poolPct >= 40 ? theme.yellow : theme.red;
|
|
271
|
+
const poolBar = renderProgressBar(poolPct, 8, poolColor);
|
|
272
|
+
const innerRightW = Math.max(30, rightW - 2);
|
|
273
|
+
const emailWidth = Math.max(16, Math.min(20, innerRightW - 27));
|
|
161
274
|
const rightContent: string[] = [
|
|
162
|
-
""
|
|
163
|
-
` ${theme.dim("
|
|
164
|
-
` ${theme.dim("
|
|
275
|
+
` ${theme.bold("Disponibilidade:")} [${poolBar}] ${poolColor(`${availableCount}/${accounts.length} (${poolPct}%)`)}`,
|
|
276
|
+
` ${theme.dim("─".repeat(Math.max(32, innerRightW - 2)))}`,
|
|
277
|
+
` ${theme.dim(`# ${pad("Conta", emailWidth)} Carga Status`)}`,
|
|
278
|
+
` ${theme.dim("─".repeat(Math.max(32, innerRightW - 2)))}`,
|
|
165
279
|
];
|
|
166
280
|
|
|
167
281
|
if (accounts.length === 0) {
|
|
168
282
|
rightContent.push(` ${theme.muted("Nenhuma conta adicionada. (Vá em [5] Contas)")}`);
|
|
169
283
|
} else {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const
|
|
284
|
+
const maxVisibleAccounts = Math.max(6, boxHeight - 7);
|
|
285
|
+
accounts.slice(0, maxVisibleAccounts).forEach((acc, idx) => {
|
|
286
|
+
const num = pad(String(idx + 1) + ".", 4);
|
|
287
|
+
const name = pad(truncate(acc.emailOrName, emailWidth - 1), emailWidth);
|
|
288
|
+
const active = acc.activeStreams || 0;
|
|
289
|
+
const limit = acc.streamLimit || 1;
|
|
290
|
+
const loadBadge = active > 0 ? theme.yellow(`[${active}/${limit}]`) : theme.dim(`[0/${limit}]`);
|
|
291
|
+
|
|
173
292
|
let status = theme.green(`${glyphs.bullet} Pronto`);
|
|
174
|
-
if (acc.onCooldown) {
|
|
175
|
-
|
|
176
|
-
|
|
293
|
+
if (active > 0 && !acc.onCooldown && acc.headersReady) {
|
|
294
|
+
status = theme.yellow(`● Gerando `);
|
|
295
|
+
} else if (acc.onCooldown) {
|
|
296
|
+
const reason = acc.cooldownReason || "";
|
|
297
|
+
if (
|
|
298
|
+
reason.startsWith("AuthFailed") ||
|
|
299
|
+
reason.startsWith("AuthPermanentFailure") ||
|
|
300
|
+
reason.includes("login methods exhausted")
|
|
301
|
+
) {
|
|
302
|
+
status = theme.red(`❌ Auth Fail`);
|
|
303
|
+
} else if (reason === "WafChallenge") {
|
|
304
|
+
status = theme.peach(`🛡️ WAF Block`);
|
|
305
|
+
} else {
|
|
306
|
+
const mins = Math.max(1, Math.round(acc.remainingCooldownMs / 60000));
|
|
307
|
+
status = theme.yellow(`⚠️ ${mins}m cd`);
|
|
308
|
+
}
|
|
177
309
|
} else if (!acc.headersReady) {
|
|
178
310
|
status = acc.isInitialized
|
|
179
311
|
? theme.yellow(`◐ Aquecendo...`)
|
|
180
312
|
: theme.muted(`○ Standby`);
|
|
181
313
|
}
|
|
182
|
-
rightContent.push(` ${num}
|
|
314
|
+
rightContent.push(` ${num}${name} ${loadBadge} ${status}`);
|
|
183
315
|
});
|
|
184
316
|
}
|
|
185
317
|
|
|
318
|
+
const summaryParts: string[] = [];
|
|
319
|
+
if (readyCount > 0) summaryParts.push(`${readyCount} warm`);
|
|
320
|
+
if (standbyCount > 0) summaryParts.push(`${standbyCount} standby`);
|
|
321
|
+
if (cooldownCount > 0) summaryParts.push(`${cooldownCount} cd`);
|
|
322
|
+
const rightFooter = summaryParts.length > 0 ? `💡 ${summaryParts.join(" · ")} · [5] Contas` : undefined;
|
|
323
|
+
|
|
186
324
|
const rightBox = drawBox({
|
|
187
|
-
title: `Contas (${
|
|
325
|
+
title: `Contas Pool (${availableCount}/${accounts.length})`,
|
|
188
326
|
width: rightW,
|
|
189
|
-
height:
|
|
327
|
+
height: boxHeight,
|
|
190
328
|
borderColor: theme.borderInactive,
|
|
191
329
|
titleColor: theme.lavender,
|
|
330
|
+
footer: rightFooter,
|
|
192
331
|
content: rightContent,
|
|
193
332
|
});
|
|
194
333
|
|