qwenproxy-cli 1.2.10 → 1.3.0
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 +14 -2
- package/src/core/accounts.ts +118 -17
- package/src/core/local-auth.ts +52 -0
- package/src/core/paths.ts +3 -0
- package/src/login.ts +55 -8
- package/src/routes/upload.ts +51 -12
- package/src/services/auth-playwright.ts +5 -28
- package/src/services/playwright.ts +279 -95
- package/src/services/qwen.ts +2 -2
- package/src/sync/codex.ts +154 -65
- package/src/sync/index.ts +7 -2
- package/src/tui/server-manager.ts +6 -0
- package/src/tui/views/accounts-view.ts +54 -6
- package/src/tui/views/logs-view.ts +4 -20
- package/src/tui/views/status-view.ts +496 -3
- package/src/tui/views/sync-view.ts +22 -10
- package/src/utils/jwt.ts +40 -0
|
@@ -4,11 +4,12 @@
|
|
|
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, truncate, setClipboardText } from "../theme.ts";
|
|
7
|
+
import { theme, glyphs, drawBox, pad, truncate, setClipboardText, getClipboardText } from "../theme.ts";
|
|
8
8
|
import { fetchProxyStatus, resetAllCooldowns, formatUptime } from "../proxy-client.ts";
|
|
9
9
|
import { ServerManager } from "../server-manager.ts";
|
|
10
|
-
import { getRuntimeChatMode, cycleNextChatMode } from "../../core/config.ts";
|
|
10
|
+
import { getRuntimeChatMode, cycleNextChatMode, config } from "../../core/config.ts";
|
|
11
11
|
import { saveTuiSettings } from "../settings.ts";
|
|
12
|
+
import { persistServerPort, persistCustomApiKey } from "../../core/local-auth.ts";
|
|
12
13
|
|
|
13
14
|
export function renderProgressBar(
|
|
14
15
|
pct: number,
|
|
@@ -72,6 +73,24 @@ export class StatusView implements TuiView {
|
|
|
72
73
|
private lastActionZerarRow = 21;
|
|
73
74
|
private lastActionModoRow = 22;
|
|
74
75
|
private lastActionCopiarRow = 23;
|
|
76
|
+
private lastActionPortRow = 24;
|
|
77
|
+
private lastActionApiKeyRow = 25;
|
|
78
|
+
|
|
79
|
+
public isPortModalOpen = false;
|
|
80
|
+
public portInput = "";
|
|
81
|
+
public portCursor = 0;
|
|
82
|
+
public portError = "";
|
|
83
|
+
public portModalHoveredBtn: "save" | "cancel" | null = null;
|
|
84
|
+
public lastPortModalBtnRow = 0;
|
|
85
|
+
public lastPortModalLeftPad = 0;
|
|
86
|
+
|
|
87
|
+
public isApiKeyModalOpen = false;
|
|
88
|
+
public apiKeyInput = "";
|
|
89
|
+
public apiKeyCursor = 0;
|
|
90
|
+
public apiKeyError = "";
|
|
91
|
+
public apiKeyModalHoveredBtn: "save" | "cancel" | null = null;
|
|
92
|
+
public lastApiKeyModalBtnRow = 0;
|
|
93
|
+
public lastApiKeyModalLeftPad = 0;
|
|
75
94
|
|
|
76
95
|
constructor() {
|
|
77
96
|
this.refresh();
|
|
@@ -92,12 +111,31 @@ export class StatusView implements TuiView {
|
|
|
92
111
|
this.refresh();
|
|
93
112
|
}
|
|
94
113
|
|
|
114
|
+
public isCapturingText(): boolean {
|
|
115
|
+
return this.isPortModalOpen || this.isApiKeyModalOpen;
|
|
116
|
+
}
|
|
117
|
+
|
|
95
118
|
public getShortcuts(): Array<{ key: string; label: string }> {
|
|
119
|
+
if (this.isPortModalOpen) {
|
|
120
|
+
return [
|
|
121
|
+
{ key: "Enter", label: "Salvar Porta" },
|
|
122
|
+
{ key: "Esc", label: "Cancelar" },
|
|
123
|
+
];
|
|
124
|
+
}
|
|
125
|
+
if (this.isApiKeyModalOpen) {
|
|
126
|
+
return [
|
|
127
|
+
{ key: "Enter", label: "Salvar API Key" },
|
|
128
|
+
{ key: "Ctrl+V", label: "Colar" },
|
|
129
|
+
{ key: "Esc", label: "Cancelar" },
|
|
130
|
+
];
|
|
131
|
+
}
|
|
96
132
|
return [
|
|
97
133
|
{ key: "r", label: "Recarregar" },
|
|
98
134
|
{ key: "z", label: "Zerar Cooldowns" },
|
|
99
135
|
{ key: "m", label: "Alternar Modo" },
|
|
100
136
|
{ key: "c", label: "Copiar URL" },
|
|
137
|
+
{ key: "p", label: "Alterar Porta" },
|
|
138
|
+
{ key: "k", label: "Alterar API Key" },
|
|
101
139
|
];
|
|
102
140
|
}
|
|
103
141
|
|
|
@@ -109,7 +147,276 @@ export class StatusView implements TuiView {
|
|
|
109
147
|
}, 4000);
|
|
110
148
|
}
|
|
111
149
|
|
|
150
|
+
private async savePortFromModal(): Promise<boolean> {
|
|
151
|
+
const val = this.portInput.trim();
|
|
152
|
+
const p = parseInt(val, 10);
|
|
153
|
+
if (isNaN(p) || p < 1 || p > 65535) {
|
|
154
|
+
this.portError = "Porta inválida (deve ser entre 1 e 65535)";
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
persistServerPort(p);
|
|
158
|
+
if (config.server) {
|
|
159
|
+
config.server.port = p;
|
|
160
|
+
}
|
|
161
|
+
this.isPortModalOpen = false;
|
|
162
|
+
this.portError = "";
|
|
163
|
+
this.portModalHoveredBtn = null;
|
|
164
|
+
this.setMessage(theme.green(`✓ Porta alterada para ${p} (salva no .env)`));
|
|
165
|
+
await this.refresh();
|
|
166
|
+
const sManager = ServerManager.getInstance();
|
|
167
|
+
if (sManager.getState() === "online") {
|
|
168
|
+
void sManager.restart();
|
|
169
|
+
}
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private async saveApiKeyFromModal(): Promise<boolean> {
|
|
174
|
+
const k = this.apiKeyInput.trim();
|
|
175
|
+
persistCustomApiKey(k);
|
|
176
|
+
this.isApiKeyModalOpen = false;
|
|
177
|
+
this.apiKeyError = "";
|
|
178
|
+
this.apiKeyModalHoveredBtn = null;
|
|
179
|
+
this.setMessage(theme.green(`✓ API Key salva no .env com sucesso`));
|
|
180
|
+
await this.refresh();
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
|
|
112
184
|
public async handleKey(key: KeyEvent): Promise<boolean | void> {
|
|
185
|
+
// 1. Port Modal key & mouse handling
|
|
186
|
+
if (this.isPortModalOpen) {
|
|
187
|
+
// Mouse hover in Port Modal
|
|
188
|
+
if (key.name === "hover" && key.mouse) {
|
|
189
|
+
const { row, col } = key.mouse;
|
|
190
|
+
if (row === this.lastPortModalBtnRow) {
|
|
191
|
+
const relCol = col - this.lastPortModalLeftPad;
|
|
192
|
+
let btn: "save" | "cancel" | null = null;
|
|
193
|
+
if (relCol >= 2 && relCol <= 28) {
|
|
194
|
+
btn = "save";
|
|
195
|
+
} else if (relCol >= 29 && relCol <= 48) {
|
|
196
|
+
btn = "cancel";
|
|
197
|
+
}
|
|
198
|
+
if (this.portModalHoveredBtn !== btn) {
|
|
199
|
+
this.portModalHoveredBtn = btn;
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
if (this.portModalHoveredBtn !== null) {
|
|
205
|
+
this.portModalHoveredBtn = null;
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Mouse click in Port Modal
|
|
212
|
+
if (key.name === "click" && key.mouse) {
|
|
213
|
+
const { row, col } = key.mouse;
|
|
214
|
+
if (row === this.lastPortModalBtnRow) {
|
|
215
|
+
const relCol = col - this.lastPortModalLeftPad;
|
|
216
|
+
if (relCol >= 2 && relCol <= 28) {
|
|
217
|
+
await this.savePortFromModal();
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
if (relCol >= 29 && relCol <= 48) {
|
|
221
|
+
this.isPortModalOpen = false;
|
|
222
|
+
this.portError = "";
|
|
223
|
+
this.portModalHoveredBtn = null;
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (key.name === "escape") {
|
|
231
|
+
this.isPortModalOpen = false;
|
|
232
|
+
this.portError = "";
|
|
233
|
+
this.portModalHoveredBtn = null;
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
if (key.name === "enter") {
|
|
237
|
+
await this.savePortFromModal();
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
if (key.name === "backspace") {
|
|
241
|
+
const cur = Math.max(0, Math.min(this.portInput.length, this.portCursor));
|
|
242
|
+
if (cur > 0) {
|
|
243
|
+
this.portInput =
|
|
244
|
+
this.portInput.slice(0, cur - 1) +
|
|
245
|
+
this.portInput.slice(cur);
|
|
246
|
+
this.portCursor = cur - 1;
|
|
247
|
+
this.portError = "";
|
|
248
|
+
}
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
if (key.name === "delete") {
|
|
252
|
+
const cur = Math.max(0, Math.min(this.portInput.length, this.portCursor));
|
|
253
|
+
if (cur < this.portInput.length) {
|
|
254
|
+
this.portInput =
|
|
255
|
+
this.portInput.slice(0, cur) +
|
|
256
|
+
this.portInput.slice(cur + 1);
|
|
257
|
+
this.portError = "";
|
|
258
|
+
}
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
if (key.name === "left") {
|
|
262
|
+
this.portCursor = Math.max(0, this.portCursor - 1);
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
if (key.name === "right") {
|
|
266
|
+
this.portCursor = Math.min(this.portInput.length, this.portCursor + 1);
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
if (key.name === "home") {
|
|
270
|
+
this.portCursor = 0;
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
if (key.name === "end") {
|
|
274
|
+
this.portCursor = this.portInput.length;
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
277
|
+
// Numeric input
|
|
278
|
+
const ch = key.char || (key.name.length === 1 && /^\d$/.test(key.name) ? key.name : "");
|
|
279
|
+
if (ch && /^\d$/.test(ch) && !key.ctrl && !key.meta) {
|
|
280
|
+
if (this.portInput.length < 5) {
|
|
281
|
+
const cur = Math.max(0, Math.min(this.portInput.length, this.portCursor));
|
|
282
|
+
this.portInput =
|
|
283
|
+
this.portInput.slice(0, cur) +
|
|
284
|
+
ch +
|
|
285
|
+
this.portInput.slice(cur);
|
|
286
|
+
this.portCursor = cur + ch.length;
|
|
287
|
+
this.portError = "";
|
|
288
|
+
}
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
return true; // Swallow all keys in modal
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// 2. API Key Modal key & mouse handling
|
|
295
|
+
if (this.isApiKeyModalOpen) {
|
|
296
|
+
// Mouse hover in API Key Modal
|
|
297
|
+
if (key.name === "hover" && key.mouse) {
|
|
298
|
+
const { row, col } = key.mouse;
|
|
299
|
+
if (row === this.lastApiKeyModalBtnRow) {
|
|
300
|
+
const relCol = col - this.lastApiKeyModalLeftPad;
|
|
301
|
+
let btn: "save" | "cancel" | null = null;
|
|
302
|
+
if (relCol >= 2 && relCol <= 30) {
|
|
303
|
+
btn = "save";
|
|
304
|
+
} else if (relCol >= 31 && relCol <= 50) {
|
|
305
|
+
btn = "cancel";
|
|
306
|
+
}
|
|
307
|
+
if (this.apiKeyModalHoveredBtn !== btn) {
|
|
308
|
+
this.apiKeyModalHoveredBtn = btn;
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
if (this.apiKeyModalHoveredBtn !== null) {
|
|
314
|
+
this.apiKeyModalHoveredBtn = null;
|
|
315
|
+
return true;
|
|
316
|
+
}
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Mouse click in API Key Modal
|
|
321
|
+
if (key.name === "click" && key.mouse) {
|
|
322
|
+
const { row, col } = key.mouse;
|
|
323
|
+
if (row === this.lastApiKeyModalBtnRow) {
|
|
324
|
+
const relCol = col - this.lastApiKeyModalLeftPad;
|
|
325
|
+
if (relCol >= 2 && relCol <= 30) {
|
|
326
|
+
await this.saveApiKeyFromModal();
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
329
|
+
if (relCol >= 31 && relCol <= 50) {
|
|
330
|
+
this.isApiKeyModalOpen = false;
|
|
331
|
+
this.apiKeyError = "";
|
|
332
|
+
this.apiKeyModalHoveredBtn = null;
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return true;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (key.name === "escape") {
|
|
340
|
+
this.isApiKeyModalOpen = false;
|
|
341
|
+
this.apiKeyError = "";
|
|
342
|
+
this.apiKeyModalHoveredBtn = null;
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
if (key.name === "enter") {
|
|
346
|
+
await this.saveApiKeyFromModal();
|
|
347
|
+
return true;
|
|
348
|
+
}
|
|
349
|
+
if (key.ctrl && (key.name === "v" || key.raw === "\x16")) {
|
|
350
|
+
const pasted = getClipboardText();
|
|
351
|
+
if (pasted) {
|
|
352
|
+
const clean = pasted.replace(/[\r\n]/g, "").trim();
|
|
353
|
+
const cur = Math.max(0, Math.min(this.apiKeyInput.length, this.apiKeyCursor));
|
|
354
|
+
this.apiKeyInput =
|
|
355
|
+
this.apiKeyInput.slice(0, cur) +
|
|
356
|
+
clean +
|
|
357
|
+
this.apiKeyInput.slice(cur);
|
|
358
|
+
this.apiKeyCursor = cur + clean.length;
|
|
359
|
+
this.apiKeyError = "";
|
|
360
|
+
}
|
|
361
|
+
return true;
|
|
362
|
+
}
|
|
363
|
+
if (key.ctrl && key.name === "c") {
|
|
364
|
+
this.apiKeyInput = "";
|
|
365
|
+
this.apiKeyCursor = 0;
|
|
366
|
+
return true;
|
|
367
|
+
}
|
|
368
|
+
if (key.name === "backspace") {
|
|
369
|
+
const cur = Math.max(0, Math.min(this.apiKeyInput.length, this.apiKeyCursor));
|
|
370
|
+
if (cur > 0) {
|
|
371
|
+
this.apiKeyInput =
|
|
372
|
+
this.apiKeyInput.slice(0, cur - 1) +
|
|
373
|
+
this.apiKeyInput.slice(cur);
|
|
374
|
+
this.apiKeyCursor = cur - 1;
|
|
375
|
+
this.apiKeyError = "";
|
|
376
|
+
}
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
if (key.name === "delete") {
|
|
380
|
+
const cur = Math.max(0, Math.min(this.apiKeyInput.length, this.apiKeyCursor));
|
|
381
|
+
if (cur < this.apiKeyInput.length) {
|
|
382
|
+
this.apiKeyInput =
|
|
383
|
+
this.apiKeyInput.slice(0, cur) +
|
|
384
|
+
this.apiKeyInput.slice(cur + 1);
|
|
385
|
+
this.apiKeyError = "";
|
|
386
|
+
}
|
|
387
|
+
return true;
|
|
388
|
+
}
|
|
389
|
+
if (key.name === "left") {
|
|
390
|
+
this.apiKeyCursor = Math.max(0, this.apiKeyCursor - 1);
|
|
391
|
+
return true;
|
|
392
|
+
}
|
|
393
|
+
if (key.name === "right") {
|
|
394
|
+
this.apiKeyCursor = Math.min(this.apiKeyInput.length, this.apiKeyCursor + 1);
|
|
395
|
+
return true;
|
|
396
|
+
}
|
|
397
|
+
if (key.name === "home") {
|
|
398
|
+
this.apiKeyCursor = 0;
|
|
399
|
+
return true;
|
|
400
|
+
}
|
|
401
|
+
if (key.name === "end") {
|
|
402
|
+
this.apiKeyCursor = this.apiKeyInput.length;
|
|
403
|
+
return true;
|
|
404
|
+
}
|
|
405
|
+
// Text character input
|
|
406
|
+
const ch = key.char || (key.name.length === 1 && !key.ctrl && !key.meta ? key.name : "");
|
|
407
|
+
if (ch && ch.length === 1 && ch >= " " && !key.ctrl && !key.meta) {
|
|
408
|
+
const cur = Math.max(0, Math.min(this.apiKeyInput.length, this.apiKeyCursor));
|
|
409
|
+
this.apiKeyInput =
|
|
410
|
+
this.apiKeyInput.slice(0, cur) +
|
|
411
|
+
ch +
|
|
412
|
+
this.apiKeyInput.slice(cur);
|
|
413
|
+
this.apiKeyCursor = cur + ch.length;
|
|
414
|
+
this.apiKeyError = "";
|
|
415
|
+
return true;
|
|
416
|
+
}
|
|
417
|
+
return true; // Swallow all keys in modal
|
|
418
|
+
}
|
|
419
|
+
|
|
113
420
|
// Mouse hover over quick actions or Base URL
|
|
114
421
|
if (key.name === "hover" && key.mouse) {
|
|
115
422
|
const { row, col } = key.mouse;
|
|
@@ -121,7 +428,9 @@ export class StatusView implements TuiView {
|
|
|
121
428
|
(row === this.lastActionRecarregarRow ||
|
|
122
429
|
row === this.lastActionZerarRow ||
|
|
123
430
|
row === this.lastActionModoRow ||
|
|
124
|
-
row === this.lastActionCopiarRow
|
|
431
|
+
row === this.lastActionCopiarRow ||
|
|
432
|
+
row === this.lastActionPortRow ||
|
|
433
|
+
row === this.lastActionApiKeyRow);
|
|
125
434
|
|
|
126
435
|
let changed = false;
|
|
127
436
|
if (isOverBaseUrl !== this.isBaseUrlHovered) {
|
|
@@ -174,6 +483,22 @@ export class StatusView implements TuiView {
|
|
|
174
483
|
this.setMessage(theme.green(`✓ Modo global da API: ${nextMode}`));
|
|
175
484
|
return true;
|
|
176
485
|
}
|
|
486
|
+
if (row === this.lastActionPortRow) {
|
|
487
|
+
this.isPortModalOpen = true;
|
|
488
|
+
this.isApiKeyModalOpen = false;
|
|
489
|
+
this.portInput = String(this.statusData?.port || config.server?.port || 7936);
|
|
490
|
+
this.portCursor = this.portInput.length;
|
|
491
|
+
this.portError = "";
|
|
492
|
+
return true;
|
|
493
|
+
}
|
|
494
|
+
if (row === this.lastActionApiKeyRow) {
|
|
495
|
+
this.isApiKeyModalOpen = true;
|
|
496
|
+
this.isPortModalOpen = false;
|
|
497
|
+
this.apiKeyInput = (process.env.API_KEY || "").trim();
|
|
498
|
+
this.apiKeyCursor = this.apiKeyInput.length;
|
|
499
|
+
this.apiKeyError = "";
|
|
500
|
+
return true;
|
|
501
|
+
}
|
|
177
502
|
}
|
|
178
503
|
}
|
|
179
504
|
if ((key.name === "r" || key.name === "R") && !key.ctrl) {
|
|
@@ -208,12 +533,174 @@ export class StatusView implements TuiView {
|
|
|
208
533
|
this.setMessage(theme.green(`✓ Base URL copiada: ${this.lastBaseUrl}`));
|
|
209
534
|
return true;
|
|
210
535
|
}
|
|
536
|
+
|
|
537
|
+
if ((key.name === "p" || key.name === "P") && !key.ctrl) {
|
|
538
|
+
this.isPortModalOpen = true;
|
|
539
|
+
this.isApiKeyModalOpen = false;
|
|
540
|
+
this.portInput = String(this.statusData?.port || config.server?.port || 7936);
|
|
541
|
+
this.portCursor = this.portInput.length;
|
|
542
|
+
this.portError = "";
|
|
543
|
+
return true;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if ((key.name === "k" || key.name === "K") && !key.ctrl) {
|
|
547
|
+
this.isApiKeyModalOpen = true;
|
|
548
|
+
this.isPortModalOpen = false;
|
|
549
|
+
this.apiKeyInput = (process.env.API_KEY || "").trim();
|
|
550
|
+
this.apiKeyCursor = this.apiKeyInput.length;
|
|
551
|
+
this.apiKeyError = "";
|
|
552
|
+
return true;
|
|
553
|
+
}
|
|
211
554
|
}
|
|
212
555
|
public render(width: number, height: number, snapshot?: ProxyStatusSnapshot | null): string[] {
|
|
213
556
|
const data = snapshot || this.statusData;
|
|
214
557
|
const isOnline = data?.online ?? false;
|
|
215
558
|
const contentH = Math.max(10, height);
|
|
216
559
|
|
|
560
|
+
if (this.isPortModalOpen) {
|
|
561
|
+
const modalW = Math.min(width - 4, 62);
|
|
562
|
+
const padCount = Math.max(0, Math.floor((width - modalW) / 2));
|
|
563
|
+
const padStr = " ".repeat(padCount);
|
|
564
|
+
|
|
565
|
+
const safeCursor = Math.max(0, Math.min(this.portInput.length, this.portCursor));
|
|
566
|
+
let inputDisplay: string;
|
|
567
|
+
if (this.portInput.length === 0) {
|
|
568
|
+
inputDisplay = `${theme.inverse(" ")} ${theme.dim("(ex: 7936)")}`;
|
|
569
|
+
} else {
|
|
570
|
+
const before = this.portInput.slice(0, safeCursor);
|
|
571
|
+
const at = this.portInput[safeCursor] || " ";
|
|
572
|
+
const after = this.portInput.slice(safeCursor + 1);
|
|
573
|
+
inputDisplay = `${theme.cyan(before)}${theme.inverse(at)}${theme.cyan(after)}`;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const modalContent: string[] = [
|
|
577
|
+
"",
|
|
578
|
+
` ${theme.bold("Alterar Porta do Servidor:")}`,
|
|
579
|
+
` ${theme.dim("Digite a nova porta para o servidor QwenProxy (1-65535):")}`,
|
|
580
|
+
` ${theme.dim("────────────────────────────────────────────────────────")}`,
|
|
581
|
+
` Porta: [ ${inputDisplay} ]`,
|
|
582
|
+
` ${theme.dim("────────────────────────────────────────────────────────")}`,
|
|
583
|
+
];
|
|
584
|
+
|
|
585
|
+
if (this.portError) {
|
|
586
|
+
modalContent.push(` ${theme.red(`❌ ${this.portError}`)}`);
|
|
587
|
+
} else {
|
|
588
|
+
modalContent.push(` ${theme.dim("Porta padrão: 7936 (mnemônico Q-W-E-N no teclado)")}`);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const isSaveHovered = this.portModalHoveredBtn === "save";
|
|
592
|
+
const isCancelHovered = this.portModalHoveredBtn === "cancel";
|
|
593
|
+
|
|
594
|
+
const saveLabel = " [ Enter ] Salvar Porta ";
|
|
595
|
+
const cancelLabel = " [ Esc ] Cancelar ";
|
|
596
|
+
|
|
597
|
+
const saveBtn = isSaveHovered
|
|
598
|
+
? theme.bgHover(theme.bold(theme.green(saveLabel)))
|
|
599
|
+
: theme.green(saveLabel);
|
|
600
|
+
|
|
601
|
+
const cancelBtn = isCancelHovered
|
|
602
|
+
? theme.bgHover(theme.bold(theme.red(cancelLabel)))
|
|
603
|
+
: theme.muted(cancelLabel);
|
|
604
|
+
|
|
605
|
+
modalContent.push("");
|
|
606
|
+
const btnContentIdx = modalContent.length;
|
|
607
|
+
modalContent.push(
|
|
608
|
+
` ${saveBtn} ${cancelBtn}`,
|
|
609
|
+
"",
|
|
610
|
+
` ${theme.dim("Nota: Salva no arquivo .env e reinicia o servidor se estiver ativo.")}`,
|
|
611
|
+
);
|
|
612
|
+
|
|
613
|
+
const modalBox = drawBox({
|
|
614
|
+
title: "Alterar Porta do Servidor",
|
|
615
|
+
width: modalW,
|
|
616
|
+
height: Math.min(contentH, modalContent.length + 2),
|
|
617
|
+
borderColor: theme.borderActive,
|
|
618
|
+
titleColor: theme.cyan,
|
|
619
|
+
content: modalContent,
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
const topPadCount = Math.max(1, Math.floor((contentH - modalBox.length) / 3));
|
|
623
|
+
this.lastPortModalBtnRow = 5 + topPadCount + btnContentIdx;
|
|
624
|
+
this.lastPortModalLeftPad = padCount;
|
|
625
|
+
|
|
626
|
+
const topPad = Array(topPadCount).fill(" ".repeat(width));
|
|
627
|
+
const res = [...topPad, ...modalBox.map((line) => padStr + line)];
|
|
628
|
+
while (res.length < contentH) res.push(" ".repeat(width));
|
|
629
|
+
return res;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
if (this.isApiKeyModalOpen) {
|
|
633
|
+
const modalW = Math.min(width - 4, 68);
|
|
634
|
+
const padCount = Math.max(0, Math.floor((width - modalW) / 2));
|
|
635
|
+
const padStr = " ".repeat(padCount);
|
|
636
|
+
|
|
637
|
+
const safeCursor = Math.max(0, Math.min(this.apiKeyInput.length, this.apiKeyCursor));
|
|
638
|
+
let inputDisplay: string;
|
|
639
|
+
if (this.apiKeyInput.length === 0) {
|
|
640
|
+
inputDisplay = `${theme.inverse(" ")} ${theme.dim("(vazio = sem autenticação local)")}`;
|
|
641
|
+
} else {
|
|
642
|
+
const before = this.apiKeyInput.slice(0, safeCursor);
|
|
643
|
+
const at = this.apiKeyInput[safeCursor] || " ";
|
|
644
|
+
const after = this.apiKeyInput.slice(safeCursor + 1);
|
|
645
|
+
inputDisplay = `${theme.peach(before)}${theme.inverse(at)}${theme.peach(after)}`;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const isProtected = this.apiKeyInput.trim().length > 0;
|
|
649
|
+
const statusBadge = isProtected
|
|
650
|
+
? theme.green("Protegido (Requer Bearer Token)")
|
|
651
|
+
: theme.yellow("Livre (Apenas Loopback 127.0.0.1)");
|
|
652
|
+
|
|
653
|
+
const isSaveHovered = this.apiKeyModalHoveredBtn === "save";
|
|
654
|
+
const isCancelHovered = this.apiKeyModalHoveredBtn === "cancel";
|
|
655
|
+
|
|
656
|
+
const saveLabel = " [ Enter ] Salvar API Key ";
|
|
657
|
+
const cancelLabel = " [ Esc ] Cancelar ";
|
|
658
|
+
|
|
659
|
+
const saveBtn = isSaveHovered
|
|
660
|
+
? theme.bgHover(theme.bold(theme.green(saveLabel)))
|
|
661
|
+
: theme.green(saveLabel);
|
|
662
|
+
|
|
663
|
+
const cancelBtn = isCancelHovered
|
|
664
|
+
? theme.bgHover(theme.bold(theme.red(cancelLabel)))
|
|
665
|
+
: theme.muted(cancelLabel);
|
|
666
|
+
|
|
667
|
+
const modalContent: string[] = [
|
|
668
|
+
"",
|
|
669
|
+
` ${theme.bold("Configurar Chave de Autenticação (API Key):")}`,
|
|
670
|
+
` ${theme.dim("Digite ou cole (Ctrl+V) a chave Bearer para proteger o proxy:")}`,
|
|
671
|
+
` ${theme.dim("────────────────────────────────────────────────────────────")}`,
|
|
672
|
+
` API Key: [ ${inputDisplay} ]`,
|
|
673
|
+
` ${theme.dim("────────────────────────────────────────────────────────────")}`,
|
|
674
|
+
` Modo de Acesso: ${statusBadge}`,
|
|
675
|
+
];
|
|
676
|
+
|
|
677
|
+
modalContent.push("");
|
|
678
|
+
const btnContentIdx = modalContent.length;
|
|
679
|
+
modalContent.push(
|
|
680
|
+
` ${saveBtn} ${cancelBtn} ${theme.dim("(Ctrl+V colar)")}`,
|
|
681
|
+
"",
|
|
682
|
+
` ${theme.dim("Nota: Salva a chave no arquivo .env. Clientes devem enviar 'Bearer <key>'.")}`,
|
|
683
|
+
);
|
|
684
|
+
|
|
685
|
+
const modalBox = drawBox({
|
|
686
|
+
title: "Configurar API Key",
|
|
687
|
+
width: modalW,
|
|
688
|
+
height: Math.min(contentH, modalContent.length + 2),
|
|
689
|
+
borderColor: theme.borderActive,
|
|
690
|
+
titleColor: theme.peach,
|
|
691
|
+
content: modalContent,
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
const topPadCount = Math.max(1, Math.floor((contentH - modalBox.length) / 3));
|
|
695
|
+
this.lastApiKeyModalBtnRow = 5 + topPadCount + btnContentIdx;
|
|
696
|
+
this.lastApiKeyModalLeftPad = padCount;
|
|
697
|
+
|
|
698
|
+
const topPad = Array(topPadCount).fill(" ".repeat(width));
|
|
699
|
+
const res = [...topPad, ...modalBox.map((line) => padStr + line)];
|
|
700
|
+
while (res.length < contentH) res.push(" ".repeat(width));
|
|
701
|
+
return res;
|
|
702
|
+
}
|
|
703
|
+
|
|
217
704
|
// Two-column layout: give left box 48-52 cols so rich metrics never truncate,
|
|
218
705
|
// and right box takes the remaining width (at least 38 cols).
|
|
219
706
|
const leftW = width >= 96
|
|
@@ -312,17 +799,23 @@ export class StatusView implements TuiView {
|
|
|
312
799
|
const zerarIdx = leftContent.length + 1;
|
|
313
800
|
const modoIdx = leftContent.length + 2;
|
|
314
801
|
const copiarIdx = leftContent.length + 3;
|
|
802
|
+
const portIdx = leftContent.length + 4;
|
|
803
|
+
const apiKeyIdx = leftContent.length + 5;
|
|
315
804
|
|
|
316
805
|
this.lastActionRecarregarRow = 5 + recarregarIdx;
|
|
317
806
|
this.lastActionZerarRow = 5 + zerarIdx;
|
|
318
807
|
this.lastActionModoRow = 5 + modoIdx;
|
|
319
808
|
this.lastActionCopiarRow = 5 + copiarIdx;
|
|
809
|
+
this.lastActionPortRow = 5 + portIdx;
|
|
810
|
+
this.lastActionApiKeyRow = 5 + apiKeyIdx;
|
|
320
811
|
|
|
321
812
|
leftContent.push(
|
|
322
813
|
` ${this.hoveredActionRow === this.lastActionRecarregarRow ? theme.bgHover(` ${theme.cyan("[ R ] Recarregar")} `) : ` ${theme.cyan("[ R ]")} Recarregar`}`,
|
|
323
814
|
` ${this.hoveredActionRow === this.lastActionZerarRow ? theme.bgHover(` ${theme.yellow("[ Z ] Zerar Cooldowns")} `) : ` ${theme.yellow("[ Z ]")} Zerar Cooldowns`}`,
|
|
324
815
|
` ${this.hoveredActionRow === this.lastActionModoRow ? theme.bgHover(` ${theme.lavender("[ M ] Alternar Modo")} `) : ` ${theme.lavender("[ M ]")} Alternar Modo`}`,
|
|
325
816
|
` ${this.hoveredActionRow === this.lastActionCopiarRow ? theme.bgHover(` ${theme.green("[ C ] Copiar URL")} `) : ` ${theme.green("[ C ]")} Copiar URL`}`,
|
|
817
|
+
` ${this.hoveredActionRow === this.lastActionPortRow ? theme.bgHover(` ${theme.cyan("[ P ] Alterar Porta")} `) : ` ${theme.cyan("[ P ]")} Alterar Porta`}`,
|
|
818
|
+
` ${this.hoveredActionRow === this.lastActionApiKeyRow ? theme.bgHover(` ${theme.peach("[ K ] Alterar API Key")} `) : ` ${theme.peach("[ K ]")} Alterar API Key`}`,
|
|
326
819
|
);
|
|
327
820
|
|
|
328
821
|
const boxHeight = Math.max(contentH, leftContent.length + 2);
|
|
@@ -61,6 +61,13 @@ export class SyncView implements TuiView {
|
|
|
61
61
|
} catch {}
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
private appendLog(msg: string): void {
|
|
65
|
+
this.actionLog.push(msg);
|
|
66
|
+
if (this.actionLog.length > 500) {
|
|
67
|
+
this.actionLog.shift();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
64
71
|
private detectClients(): void {
|
|
65
72
|
const paths = getDefaultPaths();
|
|
66
73
|
const defs: Array<{ id: SyncClientName; name: string; path: string }> = [
|
|
@@ -260,7 +267,7 @@ export class SyncView implements TuiView {
|
|
|
260
267
|
for (const c of this.clients) {
|
|
261
268
|
c.selected = !allSelected;
|
|
262
269
|
}
|
|
263
|
-
this.
|
|
270
|
+
this.appendLog(
|
|
264
271
|
allSelected ? "Desmarcados todos os clientes." : "Selecionados todos os clientes.",
|
|
265
272
|
);
|
|
266
273
|
return true;
|
|
@@ -289,11 +296,11 @@ export class SyncView implements TuiView {
|
|
|
289
296
|
.map((c) => c.id);
|
|
290
297
|
|
|
291
298
|
if (selectedTargets.length === 0) {
|
|
292
|
-
this.
|
|
299
|
+
this.appendLog(theme.yellow("⚠ Nenhum cliente selecionado para sincronizar."));
|
|
293
300
|
return;
|
|
294
301
|
}
|
|
295
302
|
const currentModel = this.availableModels[this.modelIndex] || "qwen3.8-max";
|
|
296
|
-
this.
|
|
303
|
+
this.appendLog(
|
|
297
304
|
theme.cyan(`⏳ Sincronizando [${selectedTargets.join(", ")}] com modelo ${currentModel}...`),
|
|
298
305
|
);
|
|
299
306
|
try {
|
|
@@ -308,21 +315,24 @@ export class SyncView implements TuiView {
|
|
|
308
315
|
for (const [key, clientRes] of Object.entries(res.clients)) {
|
|
309
316
|
if (clientRes && clientRes.success) {
|
|
310
317
|
successCount++;
|
|
311
|
-
this.
|
|
318
|
+
this.appendLog(
|
|
312
319
|
theme.green(`✓ [${key}] ${clientRes.message || "Configurado com sucesso"}`),
|
|
313
320
|
);
|
|
314
321
|
} else if (clientRes) {
|
|
315
|
-
this.
|
|
322
|
+
this.appendLog(
|
|
316
323
|
theme.red(`✗ [${key}] Falha: ${clientRes.error || "Erro desconhecido"}`),
|
|
317
324
|
);
|
|
318
325
|
}
|
|
319
326
|
}
|
|
320
327
|
|
|
321
|
-
this.
|
|
328
|
+
this.appendLog(
|
|
322
329
|
theme.green(`🎉 Concluído: ${successCount} cliente(s) sincronizado(s) com zero perdas!`),
|
|
323
330
|
);
|
|
324
331
|
this.detectClients();
|
|
325
332
|
} catch (err: any) {
|
|
333
|
+
this.appendLog(
|
|
334
|
+
theme.red(`✗ Erro na sincronização: ${err?.message || String(err)}`),
|
|
335
|
+
);
|
|
326
336
|
}
|
|
327
337
|
}
|
|
328
338
|
|
|
@@ -332,17 +342,17 @@ export class SyncView implements TuiView {
|
|
|
332
342
|
.map((c) => c.id);
|
|
333
343
|
|
|
334
344
|
const targetDesc = selectedTargets.length > 0 ? `[${selectedTargets.join(", ")}]` : "todos os clientes";
|
|
335
|
-
this.
|
|
345
|
+
this.appendLog(theme.yellow(`⏳ Restaurando backups anteriores de ${targetDesc}...`));
|
|
336
346
|
try {
|
|
337
347
|
const res = restoreAllClients({
|
|
338
348
|
targets: selectedTargets.length > 0 ? selectedTargets : undefined,
|
|
339
349
|
});
|
|
340
|
-
this.
|
|
350
|
+
this.appendLog(
|
|
341
351
|
theme.green(`✓ Rollback concluído: ${res.restoredCount} arquivo(s) restaurados com sucesso.`),
|
|
342
352
|
);
|
|
343
353
|
this.detectClients();
|
|
344
354
|
} catch (err: any) {
|
|
345
|
-
this.
|
|
355
|
+
this.appendLog(theme.red(`✗ Erro ao restaurar backups: ${err?.message || String(err)}`));
|
|
346
356
|
}
|
|
347
357
|
}
|
|
348
358
|
|
|
@@ -446,7 +456,9 @@ export class SyncView implements TuiView {
|
|
|
446
456
|
theme.muted(" Backups (.bak) são criados automaticamente antes de cada alteração."),
|
|
447
457
|
);
|
|
448
458
|
} else {
|
|
449
|
-
|
|
459
|
+
const maxLogs = Math.max(5, contentH - 5);
|
|
460
|
+
const visibleLogs = this.actionLog.slice(-maxLogs);
|
|
461
|
+
for (const log of visibleLogs) {
|
|
450
462
|
rightContent.push(` ${log}`);
|
|
451
463
|
}
|
|
452
464
|
}
|