qwenproxy-cli 1.0.26 → 1.0.28
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 +82 -4
- package/src/core/account-manager.ts +1 -1
- package/src/core/metrics.ts +6 -1
- package/src/core/paths.ts +7 -0
- package/src/core/server-log-buffer.ts +111 -0
- package/src/routes/chat/index.ts +6 -0
- package/src/services/chat-cleanup.ts +3 -0
- package/src/services/media-generation.ts +2 -2
- package/src/services/playwright.ts +4 -1
- 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 +51 -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/proxy-client.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import { isPlaywrightInitialized } from "../services/playwright.ts";
|
|
14
14
|
import { getAccountConcurrencySnapshot } from "../core/account-concurrency.ts";
|
|
15
15
|
import { getRssUsageSnapshot } from "../core/memory-usage.ts";
|
|
16
|
+
import { metrics } from "../core/metrics.ts";
|
|
16
17
|
import type { ProxyStatusSnapshot } from "./types.ts";
|
|
17
18
|
|
|
18
19
|
export function maskAccountIdentifier(idOrEmail: string): string {
|
|
@@ -55,6 +56,7 @@ let lastOnlineState = false;
|
|
|
55
56
|
let lastOverallStatus = "offline";
|
|
56
57
|
let lastServerReadyAccounts: Set<string> | null = null;
|
|
57
58
|
let lastServerActiveAccounts: Set<string> | null = null;
|
|
59
|
+
let lastMetricsData: any = null;
|
|
58
60
|
export async function fetchProxyStatus(): Promise<ProxyStatusSnapshot> {
|
|
59
61
|
const port = config.server?.port || 7936;
|
|
60
62
|
const configuredHost = config.server?.host;
|
|
@@ -79,6 +81,9 @@ export async function fetchProxyStatus(): Promise<ProxyStatusSnapshot> {
|
|
|
79
81
|
if (Array.isArray(data.activeAccounts)) {
|
|
80
82
|
lastServerActiveAccounts = new Set(data.activeAccounts);
|
|
81
83
|
}
|
|
84
|
+
if (data.metrics) {
|
|
85
|
+
lastMetricsData = data.metrics;
|
|
86
|
+
}
|
|
82
87
|
} else {
|
|
83
88
|
lastOnlineState = false;
|
|
84
89
|
lastServerReadyAccounts = null;
|
|
@@ -106,8 +111,9 @@ export async function fetchProxyStatus(): Promise<ProxyStatusSnapshot> {
|
|
|
106
111
|
|
|
107
112
|
cachedAccounts = rawAccounts.map((acc) => {
|
|
108
113
|
const cooldownInfo = getAccountCooldownInfo(acc.id);
|
|
109
|
-
const onCooldown = Boolean(cooldownInfo?.onCooldown);
|
|
110
|
-
const remainingCooldownMs = cooldownInfo?.remainingMs || 0;
|
|
114
|
+
const onCooldown = Boolean(cooldownInfo?.onCooldown || (acc.cooldown_until && acc.cooldown_until > now));
|
|
115
|
+
const remainingCooldownMs = cooldownInfo?.remainingMs || (acc.cooldown_until && acc.cooldown_until > now ? acc.cooldown_until - now : 0);
|
|
116
|
+
const cooldownReason = cooldownInfo?.reason || acc.cooldown_reason || (onCooldown ? "RateLimited" : null);
|
|
111
117
|
const headersReady = lastServerReadyAccounts !== null
|
|
112
118
|
? lastServerReadyAccounts.has(acc.id)
|
|
113
119
|
: isAccountHeadersReady(acc.id);
|
|
@@ -121,26 +127,37 @@ export async function fetchProxyStatus(): Promise<ProxyStatusSnapshot> {
|
|
|
121
127
|
cooldownUntil: acc.cooldown_until || null,
|
|
122
128
|
onCooldown,
|
|
123
129
|
remainingCooldownMs,
|
|
130
|
+
cooldownReason,
|
|
124
131
|
headersReady,
|
|
125
132
|
isInitialized,
|
|
126
133
|
};
|
|
127
134
|
});
|
|
128
135
|
}
|
|
129
|
-
const accounts = cachedAccounts;
|
|
130
136
|
const online = lastOnlineState;
|
|
131
137
|
const overallStatus = lastOverallStatus;
|
|
132
138
|
|
|
133
139
|
// Concurrency stats
|
|
134
140
|
let activeStreams = 0;
|
|
135
141
|
let waitingStreams = 0;
|
|
142
|
+
const concurrencyMap = new Map<string, { active: number; waiting: number; limit: number }>();
|
|
136
143
|
try {
|
|
137
144
|
const snapshot = getAccountConcurrencySnapshot();
|
|
138
145
|
for (const item of snapshot) {
|
|
139
146
|
activeStreams += item.active;
|
|
140
147
|
waitingStreams += item.waiting;
|
|
148
|
+
concurrencyMap.set(item.accountId, item);
|
|
141
149
|
}
|
|
142
150
|
} catch {}
|
|
143
151
|
|
|
152
|
+
// Attach concurrency to accounts
|
|
153
|
+
const accounts = cachedAccounts.map((acc) => {
|
|
154
|
+
const concurrency = concurrencyMap.get(acc.id);
|
|
155
|
+
return {
|
|
156
|
+
...acc,
|
|
157
|
+
activeStreams: concurrency?.active ?? 0,
|
|
158
|
+
streamLimit: concurrency?.limit ?? config.concurrency.maxStreamsPerAccount,
|
|
159
|
+
};
|
|
160
|
+
});
|
|
144
161
|
// RAM usage
|
|
145
162
|
let rssMb = 0;
|
|
146
163
|
let systemMemoryPct = 0;
|
|
@@ -150,6 +167,28 @@ export async function fetchProxyStatus(): Promise<ProxyStatusSnapshot> {
|
|
|
150
167
|
systemMemoryPct = Math.round(rssSnap.usagePercent * 10) / 10;
|
|
151
168
|
} catch {}
|
|
152
169
|
|
|
170
|
+
const totalReqs = lastMetricsData?.requestsTotal ?? Number(metrics.get("requests.total")?.value ?? 0);
|
|
171
|
+
const totalErrs = lastMetricsData?.requestsErrors ?? Number(metrics.get("requests.errors")?.value ?? 0);
|
|
172
|
+
const successRate = totalReqs > 0 ? Number((((totalReqs - totalErrs) / totalReqs) * 100).toFixed(1)) : 100;
|
|
173
|
+
const latencyAvgMs = lastMetricsData?.latencyAvgMs ?? (() => {
|
|
174
|
+
const hist = metrics.get("latency.request")?.value;
|
|
175
|
+
if (hist && typeof hist === "object" && (hist as any).count > 0) {
|
|
176
|
+
return Math.round((hist as any).sum / (hist as any).count);
|
|
177
|
+
}
|
|
178
|
+
return 0;
|
|
179
|
+
})();
|
|
180
|
+
const deltasCount = lastMetricsData?.deltasCount ?? Number(metrics.get("requests.delta")?.value ?? 0);
|
|
181
|
+
const fullReplaysCount = lastMetricsData?.fullReplaysCount ?? Number(metrics.get("requests.full")?.value ?? 0);
|
|
182
|
+
const totalModes = deltasCount + fullReplaysCount;
|
|
183
|
+
const deltaRatio = totalModes > 0 ? Number(((deltasCount / totalModes) * 100).toFixed(1)) : (deltasCount > 0 ? 100 : 0);
|
|
184
|
+
const toolCallsCount = lastMetricsData?.toolCallsCount ?? Number(metrics.get("toolcalls.total")?.value ?? 0);
|
|
185
|
+
const toolCallsRecovered = lastMetricsData?.toolCallsRecovered ?? Number(metrics.get("toolcalls.recovered")?.value ?? 0);
|
|
186
|
+
const captchasDetected = lastMetricsData?.captchasDetected ?? Number(metrics.get("captcha.challenges.detected")?.value ?? 0);
|
|
187
|
+
const captchasSolved = lastMetricsData?.captchasSolved ?? Number(metrics.get("captcha.solves.succeeded")?.value ?? 0);
|
|
188
|
+
const chatsCleaned = lastMetricsData?.chatsCleaned ?? Number(metrics.get("chats.cleaned")?.value ?? 0);
|
|
189
|
+
const cacheHitRatio = lastMetricsData?.cache?.hitRatio;
|
|
190
|
+
const cacheBytesSaved = lastMetricsData?.cache?.bytesSaved;
|
|
191
|
+
|
|
153
192
|
return {
|
|
154
193
|
online,
|
|
155
194
|
port,
|
|
@@ -160,10 +199,25 @@ export async function fetchProxyStatus(): Promise<ProxyStatusSnapshot> {
|
|
|
160
199
|
systemMemoryPct,
|
|
161
200
|
activeStreams,
|
|
162
201
|
waitingStreams,
|
|
202
|
+
metrics: {
|
|
203
|
+
requestsTotal: totalReqs,
|
|
204
|
+
requestsErrors: totalErrs,
|
|
205
|
+
successRate,
|
|
206
|
+
latencyAvgMs,
|
|
207
|
+
deltasCount,
|
|
208
|
+
fullReplaysCount,
|
|
209
|
+
deltaRatio,
|
|
210
|
+
toolCallsCount,
|
|
211
|
+
toolCallsRecovered,
|
|
212
|
+
captchasDetected,
|
|
213
|
+
captchasSolved,
|
|
214
|
+
chatsCleaned,
|
|
215
|
+
cacheHitRatio,
|
|
216
|
+
cacheBytesSaved,
|
|
217
|
+
},
|
|
163
218
|
accounts,
|
|
164
219
|
};
|
|
165
220
|
}
|
|
166
|
-
|
|
167
221
|
export function resetAllCooldowns(): number {
|
|
168
222
|
return clearAllAccountCooldowns();
|
|
169
223
|
}
|
|
@@ -31,6 +31,7 @@ export class ServerManager {
|
|
|
31
31
|
private intercepted = false;
|
|
32
32
|
private isTuiRendering = false;
|
|
33
33
|
private startPromise: Promise<void> | null = null;
|
|
34
|
+
private remoteLogAbort: AbortController | null = null;
|
|
34
35
|
|
|
35
36
|
public static getInstance(): ServerManager {
|
|
36
37
|
if (!ServerManager.instance) {
|
|
@@ -112,6 +113,11 @@ export class ServerManager {
|
|
|
112
113
|
) {
|
|
113
114
|
continue;
|
|
114
115
|
}
|
|
116
|
+
// Clean redundant leading level tags (e.g. "WARN [Qwen]" -> "[Qwen]")
|
|
117
|
+
// and normalize multi-space gaps after emojis
|
|
118
|
+
line = line
|
|
119
|
+
.replace(/^(?:\[?(?:INFO|WARN|WARNING|ERROR|ERR|DEBUG)\]?\s+)+/i, "")
|
|
120
|
+
.replace(/([\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}\u{2300}-\u{23FF}]\uFE0F?)\s{2,}/gu, "$1 ");
|
|
115
121
|
if (!line) continue;
|
|
116
122
|
|
|
117
123
|
// Prevent identical consecutive duplicate logs in the same second
|
|
@@ -234,6 +240,7 @@ export class ServerManager {
|
|
|
234
240
|
"INFO",
|
|
235
241
|
`✨ [Server] Conectado à instância em execução na porta ${port}`,
|
|
236
242
|
);
|
|
243
|
+
this.startRemoteLogStream(cleanHost, port);
|
|
237
244
|
return;
|
|
238
245
|
}
|
|
239
246
|
} catch {}
|
|
@@ -269,10 +276,53 @@ export class ServerManager {
|
|
|
269
276
|
}
|
|
270
277
|
|
|
271
278
|
public async stop(): Promise<void> {
|
|
279
|
+
if (this.remoteLogAbort) {
|
|
280
|
+
this.remoteLogAbort.abort();
|
|
281
|
+
this.remoteLogAbort = null;
|
|
282
|
+
}
|
|
272
283
|
this.restoreLogs();
|
|
273
284
|
try {
|
|
274
|
-
await stopServer();
|
|
275
285
|
this.state = "offline";
|
|
276
286
|
} catch {}
|
|
277
287
|
}
|
|
288
|
+
|
|
289
|
+
public startRemoteLogStream(host: string, port: number): void {
|
|
290
|
+
if (this.remoteLogAbort) {
|
|
291
|
+
this.remoteLogAbort.abort();
|
|
292
|
+
}
|
|
293
|
+
const abort = new AbortController();
|
|
294
|
+
this.remoteLogAbort = abort;
|
|
295
|
+
|
|
296
|
+
(async () => {
|
|
297
|
+
try {
|
|
298
|
+
const resp = await fetch(`http://${host}:${port}/logs/live`, {
|
|
299
|
+
signal: abort.signal,
|
|
300
|
+
headers: { Accept: "text/event-stream" },
|
|
301
|
+
});
|
|
302
|
+
if (!resp.ok || !resp.body) return;
|
|
303
|
+
const reader = resp.body.getReader();
|
|
304
|
+
const decoder = new TextDecoder();
|
|
305
|
+
let buf = "";
|
|
306
|
+
|
|
307
|
+
while (!abort.signal.aborted) {
|
|
308
|
+
const { done, value } = await reader.read();
|
|
309
|
+
if (done) break;
|
|
310
|
+
buf += decoder.decode(value, { stream: true });
|
|
311
|
+
const lines = buf.split("\n");
|
|
312
|
+
buf = lines.pop() ?? "";
|
|
313
|
+
|
|
314
|
+
for (const line of lines) {
|
|
315
|
+
if (line.startsWith("data: ")) {
|
|
316
|
+
try {
|
|
317
|
+
const data = JSON.parse(line.slice(6));
|
|
318
|
+
if (data && data.message) {
|
|
319
|
+
this.appendLog(data.level || "INFO", data.message);
|
|
320
|
+
}
|
|
321
|
+
} catch {}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
} catch {}
|
|
326
|
+
})();
|
|
327
|
+
}
|
|
278
328
|
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QwenProxy TUI - User Preferences & Settings Persistence
|
|
3
|
+
* Automatically saves and restores chosen model, reasoning effort, last active tab, and log filters.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { getTuiSettingsPath } from "../core/paths.ts";
|
|
9
|
+
|
|
10
|
+
export interface TuiSettings {
|
|
11
|
+
lastTab?: number;
|
|
12
|
+
chat?: {
|
|
13
|
+
model?: string;
|
|
14
|
+
effort?: "high" | "medium" | "low";
|
|
15
|
+
};
|
|
16
|
+
logs?: {
|
|
17
|
+
filter?: "all" | "warn" | "error";
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const defaultSettings: TuiSettings = {
|
|
22
|
+
lastTab: 1,
|
|
23
|
+
chat: {
|
|
24
|
+
model: "qwen3.8-max",
|
|
25
|
+
effort: "high",
|
|
26
|
+
},
|
|
27
|
+
logs: {
|
|
28
|
+
filter: "all",
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
let cachedSettings: TuiSettings | null = null;
|
|
33
|
+
|
|
34
|
+
export function resetTuiSettingsCacheForTests(): void {
|
|
35
|
+
cachedSettings = null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function loadTuiSettings(): TuiSettings {
|
|
39
|
+
if (cachedSettings) {
|
|
40
|
+
return {
|
|
41
|
+
...cachedSettings,
|
|
42
|
+
chat: { ...cachedSettings.chat },
|
|
43
|
+
logs: { ...cachedSettings.logs },
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const filePath = getTuiSettingsPath();
|
|
48
|
+
try {
|
|
49
|
+
if (fs.existsSync(filePath)) {
|
|
50
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
51
|
+
const parsed = JSON.parse(raw);
|
|
52
|
+
const loaded: TuiSettings = {
|
|
53
|
+
...defaultSettings,
|
|
54
|
+
...parsed,
|
|
55
|
+
chat: { ...defaultSettings.chat, ...(parsed.chat || {}) },
|
|
56
|
+
logs: { ...defaultSettings.logs, ...(parsed.logs || {}) },
|
|
57
|
+
};
|
|
58
|
+
cachedSettings = loaded;
|
|
59
|
+
return {
|
|
60
|
+
...loaded,
|
|
61
|
+
chat: { ...loaded.chat },
|
|
62
|
+
logs: { ...loaded.logs },
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
} catch {}
|
|
66
|
+
|
|
67
|
+
const finalSettings: TuiSettings = {
|
|
68
|
+
...defaultSettings,
|
|
69
|
+
chat: { ...defaultSettings.chat },
|
|
70
|
+
logs: { ...defaultSettings.logs },
|
|
71
|
+
};
|
|
72
|
+
cachedSettings = finalSettings;
|
|
73
|
+
return {
|
|
74
|
+
...finalSettings,
|
|
75
|
+
chat: { ...finalSettings.chat },
|
|
76
|
+
logs: { ...finalSettings.logs },
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function saveTuiSettings(updates: Partial<TuiSettings>): void {
|
|
81
|
+
try {
|
|
82
|
+
const current = loadTuiSettings();
|
|
83
|
+
const merged: TuiSettings = {
|
|
84
|
+
...current,
|
|
85
|
+
...updates,
|
|
86
|
+
chat: { ...current.chat, ...(updates.chat || {}) },
|
|
87
|
+
logs: { ...current.logs, ...(updates.logs || {}) },
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
cachedSettings = merged;
|
|
91
|
+
|
|
92
|
+
const filePath = getTuiSettingsPath();
|
|
93
|
+
const dir = path.dirname(filePath);
|
|
94
|
+
if (!fs.existsSync(dir)) {
|
|
95
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
fs.writeFileSync(filePath, JSON.stringify(merged, null, 2), "utf-8");
|
|
99
|
+
} catch {}
|
|
100
|
+
}
|
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
|
|