qwenproxy-cli 1.0.3 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -23
- package/bin/qwenproxy.js +34 -13
- package/package.json +1 -1
- package/src/api/server.ts +4 -1
- package/src/clean-cache.ts +17 -11
- package/src/core/config.ts +3 -3
- package/src/index.ts +5 -1
- package/src/login.ts +5 -0
- package/src/routes/chat/index.ts +6 -1
- package/src/services/chat-cleanup.ts +25 -10
- package/src/services/playwright.ts +105 -8
- package/src/services/qwen-chat-pool.ts +3 -2
- package/src/services/qwen.ts +4 -1
- package/src/tui/app.ts +1 -1
- package/src/tui/proxy-client.ts +4 -1
- package/src/tui/screen.ts +47 -7
- package/src/tui/server-manager.ts +19 -11
- package/src/tui/theme.ts +41 -23
- package/src/tui/views/accounts-view.ts +197 -18
- package/src/tui/views/status-view.ts +4 -4
- package/src/tui/views/storage-view.ts +153 -14
package/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
|
}
|
|
@@ -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,83 @@ 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, removePlaywrightProfile } = await import("../../services/playwright.ts");
|
|
429
|
+
const { getAccountProfilePath } = await import("../../core/paths.ts");
|
|
430
|
+
await closePlaywrightForAccount(selected.id);
|
|
431
|
+
removePlaywrightProfile(getAccountProfilePath(selected.id));
|
|
432
|
+
} catch {}
|
|
433
|
+
await this.refresh();
|
|
434
|
+
this.setStatusMessage(theme.green(`✓ Conta ${selected.emailOrName} removida com sucesso`));
|
|
435
|
+
},
|
|
436
|
+
};
|
|
437
|
+
return true;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Delete chats of selected account with 'x' or 'X' (requires confirmation)
|
|
441
|
+
if ((key.name === "x" || key.name === "X") && !key.ctrl) {
|
|
442
|
+
const selected = accounts[this.selectedIndex];
|
|
443
|
+
if (!selected) {
|
|
444
|
+
this.setStatusMessage(theme.yellow("[!] Nenhuma conta selecionada"));
|
|
445
|
+
return true;
|
|
446
|
+
}
|
|
447
|
+
this.confirmDialog = {
|
|
448
|
+
type: "delete_account_chats",
|
|
449
|
+
title: "⚠️ Apagar Chats Remotos no Qwen",
|
|
450
|
+
message: `Apagar TODOS os chats no Qwen da conta ${selected.emailOrName}?`,
|
|
451
|
+
detail: "Esta ação é irreversível e limpará todas as conversas em chat.qwen.ai.",
|
|
452
|
+
onConfirm: async () => {
|
|
453
|
+
this.setStatusMessage(theme.yellow(`⏳ Apagando chats no Qwen para ${selected.emailOrName}...`));
|
|
454
|
+
try {
|
|
455
|
+
const { deleteChatsForAccountId } = await import("../../services/chat-cleanup.ts");
|
|
456
|
+
await deleteChatsForAccountId(selected.id);
|
|
457
|
+
await this.refresh();
|
|
458
|
+
this.setStatusMessage(theme.green(`✓ Todos os chats de ${selected.emailOrName} foram apagados no Qwen!`));
|
|
459
|
+
} catch (err: any) {
|
|
460
|
+
this.setStatusMessage(theme.red(`✗ Falha ao apagar chats: ${err?.message || String(err)}`));
|
|
461
|
+
}
|
|
462
|
+
},
|
|
463
|
+
};
|
|
464
|
+
return true;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Delete chats of all accounts with 'l' or 'L' (requires confirmation)
|
|
468
|
+
if ((key.name === "l" || key.name === "L") && !key.ctrl) {
|
|
469
|
+
if (accounts.length === 0) {
|
|
470
|
+
this.setStatusMessage(theme.yellow("[!] Nenhuma conta configurada"));
|
|
471
|
+
return true;
|
|
472
|
+
}
|
|
473
|
+
this.confirmDialog = {
|
|
474
|
+
type: "delete_all_chats",
|
|
475
|
+
title: "⚠️ Apagar Chats de TODAS as Contas",
|
|
476
|
+
message: `Apagar TODOS os chats remotos de TODAS as ${accounts.length} contas no Qwen?`,
|
|
477
|
+
detail: "Esta ação é irreversível e limpará o histórico no chat.qwen.ai.",
|
|
478
|
+
onConfirm: async () => {
|
|
479
|
+
this.setStatusMessage(theme.yellow(`⏳ Apagando chats no Qwen de todas as contas...`));
|
|
480
|
+
try {
|
|
481
|
+
const { deleteChatsForConfiguredAccounts } = await import("../../services/chat-cleanup.ts");
|
|
482
|
+
const res = await deleteChatsForConfiguredAccounts(true);
|
|
483
|
+
await this.refresh();
|
|
484
|
+
this.setStatusMessage(theme.green(`✓ Chats apagados no Qwen: ${res.succeeded}/${res.attempted} contas limpas!`));
|
|
485
|
+
} catch (err: any) {
|
|
486
|
+
this.setStatusMessage(theme.red(`✗ Falha ao apagar chats: ${err?.message || String(err)}`));
|
|
487
|
+
}
|
|
488
|
+
},
|
|
489
|
+
};
|
|
354
490
|
return true;
|
|
355
491
|
}
|
|
356
492
|
|
|
@@ -371,9 +507,9 @@ export class AccountsView implements TuiView {
|
|
|
371
507
|
return true;
|
|
372
508
|
}
|
|
373
509
|
|
|
374
|
-
// Right panel action buttons hover (rows 15 to
|
|
510
|
+
// Right panel action buttons hover (rows 15 to 20)
|
|
375
511
|
if (col >= leftW) {
|
|
376
|
-
if (row >= 15 && row <=
|
|
512
|
+
if (row >= 15 && row <= 20) {
|
|
377
513
|
if (this.hoveredActionRow !== row) {
|
|
378
514
|
this.hoveredActionRow = row;
|
|
379
515
|
return true;
|
|
@@ -387,7 +523,6 @@ export class AccountsView implements TuiView {
|
|
|
387
523
|
return true;
|
|
388
524
|
}
|
|
389
525
|
}
|
|
390
|
-
|
|
391
526
|
// Mouse click on account rows or action buttons
|
|
392
527
|
if (key.name === "click" && key.mouse) {
|
|
393
528
|
const { row, col } = key.mouse;
|
|
@@ -416,9 +551,16 @@ export class AccountsView implements TuiView {
|
|
|
416
551
|
await this.handleKey({ name: "z", ctrl: false, shift: false, meta: false });
|
|
417
552
|
return true;
|
|
418
553
|
}
|
|
554
|
+
if (row === 19) {
|
|
555
|
+
await this.handleKey({ name: "x", ctrl: false, shift: false, meta: false });
|
|
556
|
+
return true;
|
|
557
|
+
}
|
|
558
|
+
if (row === 20) {
|
|
559
|
+
await this.handleKey({ name: "l", ctrl: false, shift: false, meta: false });
|
|
560
|
+
return true;
|
|
561
|
+
}
|
|
419
562
|
}
|
|
420
563
|
}
|
|
421
|
-
// Navigate accounts list (Mouse wheel or Up/Down keys)
|
|
422
564
|
if (key.name === "up" || key.name === "wheelup" || (key.name === "k" && !key.ctrl)) {
|
|
423
565
|
if (accounts.length > 0) {
|
|
424
566
|
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
|
@@ -469,6 +611,9 @@ export class AccountsView implements TuiView {
|
|
|
469
611
|
this.lastLeftW = leftW;
|
|
470
612
|
const rightW = Math.max(30, width - leftW - 1);
|
|
471
613
|
|
|
614
|
+
if (snapshot) {
|
|
615
|
+
this.statusData = snapshot;
|
|
616
|
+
}
|
|
472
617
|
const data = snapshot || this.statusData;
|
|
473
618
|
const accounts = data?.accounts || [];
|
|
474
619
|
const selected = accounts[this.selectedIndex];
|
|
@@ -490,12 +635,12 @@ export class AccountsView implements TuiView {
|
|
|
490
635
|
const isHovered = idx === this.hoveredAccountIndex;
|
|
491
636
|
const pointer = isFocused ? theme.cyan(`${glyphs.pointer} `) : " ";
|
|
492
637
|
const num = pad(String(idx + 1) + ".", 4);
|
|
493
|
-
const name = pad(acc.emailOrName, 22);
|
|
638
|
+
const name = pad(truncate(acc.emailOrName, 20), 22);
|
|
494
639
|
|
|
495
640
|
let status = theme.green(`${glyphs.bullet} Pronto `);
|
|
496
641
|
if (acc.onCooldown) {
|
|
497
642
|
const mins = Math.max(1, Math.round(acc.remainingCooldownMs / 60000));
|
|
498
|
-
status = theme.yellow(
|
|
643
|
+
status = theme.yellow(`⚠️ ${mins}m cd `);
|
|
499
644
|
} else if (!acc.headersReady) {
|
|
500
645
|
status = acc.isInitialized
|
|
501
646
|
? theme.yellow(`◐ Aquecendo...`)
|
|
@@ -562,8 +707,9 @@ export class AccountsView implements TuiView {
|
|
|
562
707
|
rightContent.push(` ${this.hoveredActionRow === 16 ? theme.bgHover(` ${theme.red("[ D ] Remover Conta")} `) : `${theme.red("[ D ]")} Remover Conta`}`);
|
|
563
708
|
rightContent.push(` ${this.hoveredActionRow === 17 ? theme.bgHover(` ${theme.yellow("[ C ] Zerar Cooldown")} `) : `${theme.yellow("[ C ]")} Zerar Cooldown`}`);
|
|
564
709
|
rightContent.push(` ${this.hoveredActionRow === 18 ? theme.bgHover(` ${theme.green("[ Z ] Zerar Todas")} `) : `${theme.green("[ Z ]")} Zerar Todas`}`);
|
|
710
|
+
rightContent.push(` ${this.hoveredActionRow === 19 ? theme.bgHover(` ${theme.peach("[ X ] Limpar Chats (Conta)")} `) : `${theme.peach("[ X ]")} Limpar Chats (Conta)`}`);
|
|
711
|
+
rightContent.push(` ${this.hoveredActionRow === 20 ? theme.bgHover(` ${theme.red("[ L ] Limpar Todos os Chats")} `) : `${theme.red("[ L ]")} Limpar Todos os Chats`}`);
|
|
565
712
|
}
|
|
566
|
-
|
|
567
713
|
const rightBox = drawBox({
|
|
568
714
|
title: "Inspeção de Conta",
|
|
569
715
|
width: rightW,
|
|
@@ -655,6 +801,39 @@ export class AccountsView implements TuiView {
|
|
|
655
801
|
return modalBox.map((line) => padStr + line);
|
|
656
802
|
}
|
|
657
803
|
|
|
804
|
+
if (this.confirmDialog) {
|
|
805
|
+
const modalW = Math.min(width - 4, 66);
|
|
806
|
+
this.lastConfirmModalLeftPad = Math.max(0, Math.floor((width - modalW) / 2));
|
|
807
|
+
const confirmBtn =
|
|
808
|
+
this.confirmDialogHovered === "confirm"
|
|
809
|
+
? theme.bgHover(theme.red(" [ S / Enter ] Sim, Confirmar "))
|
|
810
|
+
: theme.red("[ S / Enter ] Sim, Confirmar");
|
|
811
|
+
const cancelBtn =
|
|
812
|
+
this.confirmDialogHovered === "cancel"
|
|
813
|
+
? theme.bgHover(theme.green(" [ N / Esc ] Cancelar "))
|
|
814
|
+
: theme.green("[ N / Esc ] Cancelar");
|
|
815
|
+
|
|
816
|
+
const modalContent = [
|
|
817
|
+
"",
|
|
818
|
+
` ${theme.bold(this.confirmDialog.message)}`,
|
|
819
|
+
` ${theme.muted(this.confirmDialog.detail)}`,
|
|
820
|
+
"",
|
|
821
|
+
` ${confirmBtn} ${cancelBtn}`,
|
|
822
|
+
];
|
|
823
|
+
|
|
824
|
+
const modalBox = drawBox({
|
|
825
|
+
title: this.confirmDialog.title,
|
|
826
|
+
width: modalW,
|
|
827
|
+
height: Math.min(contentH, 8),
|
|
828
|
+
borderColor: theme.red,
|
|
829
|
+
titleColor: theme.red,
|
|
830
|
+
content: modalContent,
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
const padStr = " ".repeat(this.lastConfirmModalLeftPad);
|
|
834
|
+
return modalBox.map((line) => padStr + line);
|
|
835
|
+
}
|
|
836
|
+
|
|
658
837
|
return mergedLines;
|
|
659
838
|
}
|
|
660
839
|
}
|
|
@@ -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
|
|