qwenproxy-cli 1.0.32 → 1.2.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/README.es.md +267 -0
- package/README.md +174 -783
- package/README.pt-BR.md +1031 -0
- package/bin/qwenproxy.js +7 -3
- package/bin/update.d.ts +11 -0
- package/bin/update.js +155 -0
- package/package.json +4 -4
- package/src/api/models.ts +18 -2
- package/src/api/server.ts +18 -2
- package/src/core/accounts.ts +130 -0
- package/src/core/config.ts +96 -5
- package/src/core/model-alias.ts +17 -0
- package/src/index.ts +3 -0
- package/src/reset-cooldowns.ts +5 -1
- package/src/routes/chat/context.ts +9 -9
- package/src/routes/chat/index.ts +15 -6
- package/src/services/qwen-chat-pool.ts +4 -5
- package/src/services/qwen.ts +60 -27
- package/src/sync/claude-code.ts +41 -4
- package/src/sync/cline.ts +34 -4
- package/src/sync/codex.ts +29 -4
- package/src/sync/index.ts +98 -70
- package/src/sync/omp.ts +23 -4
- package/src/sync/opencode.ts +28 -4
- package/src/sync/utils.ts +32 -3
- package/src/sync/zed.ts +28 -4
- package/src/sync-clients.ts +3 -4
- package/src/tui/app.ts +27 -10
- package/src/tui/index.ts +3 -0
- package/src/tui/proxy-client.ts +20 -16
- package/src/tui/screen.ts +10 -3
- package/src/tui/settings.ts +2 -0
- package/src/tui/theme.ts +2 -0
- package/src/tui/types.ts +1 -0
- package/src/tui/views/accounts-view.ts +343 -23
- package/src/tui/views/chat-view.ts +302 -71
- package/src/tui/views/status-view.ts +99 -15
- package/src/tui/views/sync-view.ts +87 -32
- package/src/update-cli.ts +10 -154
|
@@ -4,9 +4,11 @@
|
|
|
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 } from "../theme.ts";
|
|
7
|
+
import { theme, glyphs, drawBox, pad, truncate, setClipboardText } 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";
|
|
11
|
+
import { saveTuiSettings } from "../settings.ts";
|
|
10
12
|
|
|
11
13
|
export function renderProgressBar(
|
|
12
14
|
pct: number,
|
|
@@ -59,13 +61,21 @@ export class StatusView implements TuiView {
|
|
|
59
61
|
private actionMessage = "";
|
|
60
62
|
private actionMessageTimeout: NodeJS.Timeout | null = null;
|
|
61
63
|
private hoveredActionRow: number | null = null;
|
|
64
|
+
private isBaseUrlHovered = false;
|
|
65
|
+
private copiedRecently = false;
|
|
66
|
+
private copiedTimeout: NodeJS.Timeout | null = null;
|
|
67
|
+
private lastBaseUrl = "http://127.0.0.1:7936/v1";
|
|
68
|
+
private lastBaseUrlRow = 6;
|
|
69
|
+
private lastModoApiRow = 7;
|
|
62
70
|
private lastLeftW = 38;
|
|
63
71
|
private lastActionRecarregarRow = 20;
|
|
64
72
|
private lastActionZerarRow = 21;
|
|
73
|
+
private lastActionModoRow = 22;
|
|
74
|
+
private lastActionCopiarRow = 23;
|
|
75
|
+
|
|
65
76
|
constructor() {
|
|
66
77
|
this.refresh();
|
|
67
78
|
}
|
|
68
|
-
|
|
69
79
|
public async refresh(): Promise<void> {
|
|
70
80
|
try {
|
|
71
81
|
if (process.stdout.isTTY && !process.env.NODE_TEST_CONTEXT) {
|
|
@@ -86,6 +96,8 @@ export class StatusView implements TuiView {
|
|
|
86
96
|
return [
|
|
87
97
|
{ key: "r", label: "Recarregar" },
|
|
88
98
|
{ key: "z", label: "Zerar Cooldowns" },
|
|
99
|
+
{ key: "m", label: "Alternar Modo" },
|
|
100
|
+
{ key: "c", label: "Copiar URL" },
|
|
89
101
|
];
|
|
90
102
|
}
|
|
91
103
|
|
|
@@ -98,23 +110,34 @@ export class StatusView implements TuiView {
|
|
|
98
110
|
}
|
|
99
111
|
|
|
100
112
|
public async handleKey(key: KeyEvent): Promise<boolean | void> {
|
|
101
|
-
// Mouse hover over quick actions
|
|
113
|
+
// Mouse hover over quick actions or Base URL
|
|
102
114
|
if (key.name === "hover" && key.mouse) {
|
|
103
115
|
const { row, col } = key.mouse;
|
|
104
116
|
const leftW = this.lastLeftW || 38;
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
117
|
+
const isOverLeft = col >= 2 && col <= leftW - 1;
|
|
118
|
+
const isOverBaseUrl = isOverLeft && row === this.lastBaseUrlRow;
|
|
119
|
+
const isOverAction =
|
|
120
|
+
isOverLeft &&
|
|
121
|
+
(row === this.lastActionRecarregarRow ||
|
|
122
|
+
row === this.lastActionZerarRow ||
|
|
123
|
+
row === this.lastActionModoRow ||
|
|
124
|
+
row === this.lastActionCopiarRow);
|
|
125
|
+
|
|
126
|
+
let changed = false;
|
|
127
|
+
if (isOverBaseUrl !== this.isBaseUrlHovered) {
|
|
128
|
+
this.isBaseUrlHovered = isOverBaseUrl;
|
|
129
|
+
changed = true;
|
|
130
|
+
}
|
|
131
|
+
if (isOverAction) {
|
|
110
132
|
if (this.hoveredActionRow !== row) {
|
|
111
133
|
this.hoveredActionRow = row;
|
|
112
|
-
|
|
134
|
+
changed = true;
|
|
113
135
|
}
|
|
114
136
|
} else if (this.hoveredActionRow !== null) {
|
|
115
137
|
this.hoveredActionRow = null;
|
|
116
|
-
|
|
138
|
+
changed = true;
|
|
117
139
|
}
|
|
140
|
+
if (changed) return true;
|
|
118
141
|
}
|
|
119
142
|
|
|
120
143
|
// Mouse click interactions
|
|
@@ -122,6 +145,17 @@ export class StatusView implements TuiView {
|
|
|
122
145
|
const { row, col } = key.mouse;
|
|
123
146
|
const leftW = this.lastLeftW || 38;
|
|
124
147
|
if (col >= 2 && col <= leftW - 1) {
|
|
148
|
+
if (row === this.lastBaseUrlRow || row === this.lastActionCopiarRow) {
|
|
149
|
+
setClipboardText(this.lastBaseUrl);
|
|
150
|
+
this.copiedRecently = true;
|
|
151
|
+
if (this.copiedTimeout) clearTimeout(this.copiedTimeout);
|
|
152
|
+
this.copiedTimeout = setTimeout(() => {
|
|
153
|
+
this.copiedRecently = false;
|
|
154
|
+
this.copiedTimeout = null;
|
|
155
|
+
}, 2500);
|
|
156
|
+
this.setMessage(theme.green(`✓ Base URL copiada: ${this.lastBaseUrl}`));
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
125
159
|
if (row === this.lastActionRecarregarRow) {
|
|
126
160
|
await this.refresh();
|
|
127
161
|
this.setMessage(theme.green("✓ Status atualizado"));
|
|
@@ -133,9 +167,15 @@ export class StatusView implements TuiView {
|
|
|
133
167
|
this.setMessage(theme.green(`✓ Cooldowns zerados: ${cleared} conta(s) liberada(s)`));
|
|
134
168
|
return true;
|
|
135
169
|
}
|
|
170
|
+
if (row === this.lastActionModoRow || row === this.lastModoApiRow) {
|
|
171
|
+
const nextMode = cycleNextChatMode();
|
|
172
|
+
saveTuiSettings({ chat: { mode: nextMode } });
|
|
173
|
+
await this.refresh();
|
|
174
|
+
this.setMessage(theme.green(`✓ Modo global da API: ${nextMode}`));
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
136
177
|
}
|
|
137
178
|
}
|
|
138
|
-
|
|
139
179
|
if ((key.name === "r" || key.name === "R") && !key.ctrl) {
|
|
140
180
|
await this.refresh();
|
|
141
181
|
this.setMessage(theme.green("✓ Status atualizado"));
|
|
@@ -148,8 +188,27 @@ export class StatusView implements TuiView {
|
|
|
148
188
|
this.setMessage(theme.green(`✓ Cooldowns zerados: ${cleared} conta(s) liberada(s)`));
|
|
149
189
|
return true;
|
|
150
190
|
}
|
|
151
|
-
}
|
|
152
191
|
|
|
192
|
+
if ((key.name === "m" || key.name === "M") && !key.ctrl) {
|
|
193
|
+
const nextMode = cycleNextChatMode();
|
|
194
|
+
saveTuiSettings({ chat: { mode: nextMode } });
|
|
195
|
+
await this.refresh();
|
|
196
|
+
this.setMessage(theme.green(`✓ Modo global da API: ${nextMode}`));
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if ((key.name === "c" || key.name === "C") && !key.ctrl && !key.meta) {
|
|
201
|
+
setClipboardText(this.lastBaseUrl);
|
|
202
|
+
this.copiedRecently = true;
|
|
203
|
+
if (this.copiedTimeout) clearTimeout(this.copiedTimeout);
|
|
204
|
+
this.copiedTimeout = setTimeout(() => {
|
|
205
|
+
this.copiedRecently = false;
|
|
206
|
+
this.copiedTimeout = null;
|
|
207
|
+
}, 2500);
|
|
208
|
+
this.setMessage(theme.green(`✓ Base URL copiada: ${this.lastBaseUrl}`));
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
153
212
|
public render(width: number, height: number, snapshot?: ProxyStatusSnapshot | null): string[] {
|
|
154
213
|
const data = snapshot || this.statusData;
|
|
155
214
|
const isOnline = data?.online ?? false;
|
|
@@ -177,7 +236,9 @@ export class StatusView implements TuiView {
|
|
|
177
236
|
const uptimeSecs = data?.uptimeSeconds || Math.floor(process.uptime());
|
|
178
237
|
const uptimeStr = formatUptime(uptimeSecs);
|
|
179
238
|
const baseUrl = `http://${data?.host || "127.0.0.1"}:${data?.port || 7936}/v1`;
|
|
180
|
-
|
|
239
|
+
this.lastBaseUrl = baseUrl;
|
|
240
|
+
this.lastBaseUrlRow = 6;
|
|
241
|
+
this.lastModoApiRow = 7;
|
|
181
242
|
const m = data?.metrics;
|
|
182
243
|
const reqsTotal = m?.requestsTotal ?? 0;
|
|
183
244
|
const reqsErrors = m?.requestsErrors ?? 0;
|
|
@@ -210,12 +271,28 @@ export class StatusView implements TuiView {
|
|
|
210
271
|
if (data?.waitingStreams && data.waitingStreams > 0) {
|
|
211
272
|
connsStr += theme.peach(` (${data.waitingStreams} na fila)`);
|
|
212
273
|
}
|
|
274
|
+
let urlDisplay: string;
|
|
275
|
+
if (this.copiedRecently) {
|
|
276
|
+
urlDisplay = theme.bold(theme.green(baseUrl));
|
|
277
|
+
} else if (this.isBaseUrlHovered) {
|
|
278
|
+
urlDisplay = theme.bold(theme.underline(theme.cyan(baseUrl)));
|
|
279
|
+
} else {
|
|
280
|
+
urlDisplay = theme.cyan(baseUrl);
|
|
281
|
+
}
|
|
213
282
|
|
|
214
283
|
const leftContent: string[] = [
|
|
215
284
|
` ${theme.bold(lbl("Status:"))} ${onlineBadge}`,
|
|
216
|
-
` ${theme.bold(lbl("Base URL:"))} ${
|
|
285
|
+
` ${theme.bold(lbl("Base URL:"))} ${urlDisplay}`,
|
|
286
|
+
` ${theme.bold(lbl("Modo API:"))} ${
|
|
287
|
+
getRuntimeChatMode() === "thread"
|
|
288
|
+
? theme.cyan("[thread]")
|
|
289
|
+
: getRuntimeChatMode() === "thread-temp"
|
|
290
|
+
? theme.green("[thread-temp]")
|
|
291
|
+
: getRuntimeChatMode() === "stateless-temp"
|
|
292
|
+
? theme.yellow("[stateless-temp]")
|
|
293
|
+
: theme.lavender("[stateless]")
|
|
294
|
+
} ${theme.dim("('M' alternar)")}`,
|
|
217
295
|
` ${theme.bold(lbl("Uptime:"))} ${theme.cyan(uptimeStr)}`,
|
|
218
|
-
` ${theme.bold(lbl("Memória:"))} ${theme.cyan(ramStr)}`,
|
|
219
296
|
` ${theme.bold(lbl("Conexões:"))} ${connsStr}`,
|
|
220
297
|
` ${theme.dim("───────────────────────────────────────")}`,
|
|
221
298
|
` ${theme.bold("Tráfego & Performance:")}`,
|
|
@@ -233,12 +310,19 @@ export class StatusView implements TuiView {
|
|
|
233
310
|
|
|
234
311
|
const recarregarIdx = leftContent.length;
|
|
235
312
|
const zerarIdx = leftContent.length + 1;
|
|
313
|
+
const modoIdx = leftContent.length + 2;
|
|
314
|
+
const copiarIdx = leftContent.length + 3;
|
|
315
|
+
|
|
236
316
|
this.lastActionRecarregarRow = 5 + recarregarIdx;
|
|
237
317
|
this.lastActionZerarRow = 5 + zerarIdx;
|
|
318
|
+
this.lastActionModoRow = 5 + modoIdx;
|
|
319
|
+
this.lastActionCopiarRow = 5 + copiarIdx;
|
|
238
320
|
|
|
239
321
|
leftContent.push(
|
|
240
322
|
` ${this.hoveredActionRow === this.lastActionRecarregarRow ? theme.bgHover(` ${theme.cyan("[ R ] Recarregar")} `) : ` ${theme.cyan("[ R ]")} Recarregar`}`,
|
|
241
323
|
` ${this.hoveredActionRow === this.lastActionZerarRow ? theme.bgHover(` ${theme.yellow("[ Z ] Zerar Cooldowns")} `) : ` ${theme.yellow("[ Z ]")} Zerar Cooldowns`}`,
|
|
324
|
+
` ${this.hoveredActionRow === this.lastActionModoRow ? theme.bgHover(` ${theme.lavender("[ M ] Alternar Modo")} `) : ` ${theme.lavender("[ M ]")} Alternar Modo`}`,
|
|
325
|
+
` ${this.hoveredActionRow === this.lastActionCopiarRow ? theme.bgHover(` ${theme.green("[ C ] Copiar URL")} `) : ` ${theme.green("[ C ]")} Copiar URL`}`,
|
|
242
326
|
);
|
|
243
327
|
|
|
244
328
|
const boxHeight = Math.max(contentH, leftContent.length + 2);
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
inspectClientSyncStatus,
|
|
13
13
|
} from "../../sync/index.ts";
|
|
14
14
|
import type { SyncClientName } from "../../sync/types.ts";
|
|
15
|
-
import { fetchLiveModels } from "../proxy-client.ts";
|
|
15
|
+
import { fetchLiveModels, DEFAULT_FALLBACK_MODELS } from "../proxy-client.ts";
|
|
16
16
|
|
|
17
17
|
interface ClientOption {
|
|
18
18
|
id: SyncClientName;
|
|
@@ -30,18 +30,17 @@ export class SyncView implements TuiView {
|
|
|
30
30
|
private clients: ClientOption[] = [];
|
|
31
31
|
private selectedRowIndex = 0; // 0..9 for clients, 10 for model, 11 for scope, 12 for sync, 13 for restore
|
|
32
32
|
private hoveredActionRow: number | null = null;
|
|
33
|
-
private availableModels = [
|
|
34
|
-
"qwen3.8-max",
|
|
35
|
-
"qwen3.7-plus",
|
|
36
|
-
"qwen3.7-max",
|
|
37
|
-
"z-image-turbo",
|
|
38
|
-
"qwen-image-3.0-pro",
|
|
39
|
-
"wan3.0-video",
|
|
40
|
-
];
|
|
33
|
+
private availableModels = [...DEFAULT_FALLBACK_MODELS];
|
|
41
34
|
private modelIndex = 0;
|
|
42
35
|
private syncAllModels = true;
|
|
43
36
|
private actionLog: string[] = [];
|
|
44
37
|
private lastLeftW = 46;
|
|
38
|
+
private lastClientStartRow = 8;
|
|
39
|
+
private lastClientEndRow = 17;
|
|
40
|
+
private lastModelRow = 20;
|
|
41
|
+
private lastScopeRow = 21;
|
|
42
|
+
private lastSyncRow = 24;
|
|
43
|
+
private lastRestoreRow = 25;
|
|
45
44
|
constructor() {
|
|
46
45
|
this.detectClients();
|
|
47
46
|
}
|
|
@@ -50,11 +49,14 @@ export class SyncView implements TuiView {
|
|
|
50
49
|
this.detectClients();
|
|
51
50
|
void this.refreshModels();
|
|
52
51
|
}
|
|
53
|
-
|
|
52
|
+
public async refreshModels(): Promise<void> {
|
|
54
53
|
try {
|
|
55
54
|
const live = await fetchLiveModels();
|
|
56
55
|
if (live && live.length > 0) {
|
|
56
|
+
const current = this.availableModels[this.modelIndex];
|
|
57
57
|
this.availableModels = live;
|
|
58
|
+
const foundIdx = this.availableModels.indexOf(current);
|
|
59
|
+
this.modelIndex = foundIdx !== -1 ? foundIdx : 0;
|
|
58
60
|
}
|
|
59
61
|
} catch {}
|
|
60
62
|
}
|
|
@@ -103,27 +105,62 @@ export class SyncView implements TuiView {
|
|
|
103
105
|
const { row, col } = key.mouse;
|
|
104
106
|
const leftW = this.lastLeftW || 46;
|
|
105
107
|
if (col >= 2 && col <= leftW - 1) {
|
|
106
|
-
if (row >=
|
|
107
|
-
const targetRow = row -
|
|
108
|
+
if (row >= this.lastClientStartRow && row <= this.lastClientEndRow) {
|
|
109
|
+
const targetRow = row - this.lastClientStartRow;
|
|
110
|
+
let changed = false;
|
|
108
111
|
if (this.selectedRowIndex !== targetRow) {
|
|
109
112
|
this.selectedRowIndex = targetRow;
|
|
110
|
-
|
|
113
|
+
changed = true;
|
|
114
|
+
}
|
|
115
|
+
if (this.hoveredActionRow !== null) {
|
|
116
|
+
this.hoveredActionRow = null;
|
|
117
|
+
changed = true;
|
|
111
118
|
}
|
|
112
|
-
|
|
119
|
+
if (changed) return true;
|
|
120
|
+
} else if (row === this.lastModelRow) {
|
|
121
|
+
let changed = false;
|
|
113
122
|
if (this.selectedRowIndex !== 10) {
|
|
114
123
|
this.selectedRowIndex = 10;
|
|
115
|
-
|
|
124
|
+
changed = true;
|
|
116
125
|
}
|
|
117
|
-
|
|
126
|
+
if (this.hoveredActionRow !== null) {
|
|
127
|
+
this.hoveredActionRow = null;
|
|
128
|
+
changed = true;
|
|
129
|
+
}
|
|
130
|
+
if (changed) return true;
|
|
131
|
+
} else if (row === this.lastScopeRow) {
|
|
132
|
+
let changed = false;
|
|
118
133
|
if (this.selectedRowIndex !== 11) {
|
|
119
134
|
this.selectedRowIndex = 11;
|
|
120
|
-
|
|
135
|
+
changed = true;
|
|
121
136
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
137
|
+
if (this.hoveredActionRow !== null) {
|
|
138
|
+
this.hoveredActionRow = null;
|
|
139
|
+
changed = true;
|
|
140
|
+
}
|
|
141
|
+
if (changed) return true;
|
|
142
|
+
} else if (row === this.lastSyncRow) {
|
|
143
|
+
let changed = false;
|
|
144
|
+
if (this.selectedRowIndex !== 12) {
|
|
145
|
+
this.selectedRowIndex = 12;
|
|
146
|
+
changed = true;
|
|
147
|
+
}
|
|
148
|
+
if (this.hoveredActionRow !== this.lastSyncRow) {
|
|
149
|
+
this.hoveredActionRow = this.lastSyncRow;
|
|
150
|
+
changed = true;
|
|
151
|
+
}
|
|
152
|
+
if (changed) return true;
|
|
153
|
+
} else if (row === this.lastRestoreRow) {
|
|
154
|
+
let changed = false;
|
|
155
|
+
if (this.selectedRowIndex !== 13) {
|
|
156
|
+
this.selectedRowIndex = 13;
|
|
157
|
+
changed = true;
|
|
126
158
|
}
|
|
159
|
+
if (this.hoveredActionRow !== this.lastRestoreRow) {
|
|
160
|
+
this.hoveredActionRow = this.lastRestoreRow;
|
|
161
|
+
changed = true;
|
|
162
|
+
}
|
|
163
|
+
if (changed) return true;
|
|
127
164
|
} else if (this.hoveredActionRow !== null) {
|
|
128
165
|
this.hoveredActionRow = null;
|
|
129
166
|
return true;
|
|
@@ -140,35 +177,40 @@ export class SyncView implements TuiView {
|
|
|
140
177
|
const leftW = this.lastLeftW || 46;
|
|
141
178
|
if (col >= 2 && col <= leftW - 1) {
|
|
142
179
|
// Rows 8..17: Toggle client
|
|
143
|
-
if (row >=
|
|
144
|
-
const client = this.clients[row -
|
|
180
|
+
if (row >= this.lastClientStartRow && row <= this.lastClientEndRow) {
|
|
181
|
+
const client = this.clients[row - this.lastClientStartRow];
|
|
145
182
|
if (client) {
|
|
146
183
|
client.selected = !client.selected;
|
|
147
|
-
this.selectedRowIndex = row -
|
|
184
|
+
this.selectedRowIndex = row - this.lastClientStartRow;
|
|
185
|
+
this.hoveredActionRow = null;
|
|
148
186
|
return true;
|
|
149
187
|
}
|
|
150
188
|
}
|
|
151
189
|
// Model selector
|
|
152
|
-
if (row ===
|
|
190
|
+
if (row === this.lastModelRow) {
|
|
153
191
|
this.modelIndex = (this.modelIndex + 1) % this.availableModels.length;
|
|
154
192
|
this.selectedRowIndex = 10;
|
|
193
|
+
this.hoveredActionRow = null;
|
|
155
194
|
return true;
|
|
156
195
|
}
|
|
157
196
|
// Scope selector
|
|
158
|
-
if (row ===
|
|
197
|
+
if (row === this.lastScopeRow) {
|
|
159
198
|
this.syncAllModels = !this.syncAllModels;
|
|
160
199
|
this.selectedRowIndex = 11;
|
|
200
|
+
this.hoveredActionRow = null;
|
|
161
201
|
return true;
|
|
162
202
|
}
|
|
163
203
|
// Sincronizar button
|
|
164
|
-
if (row ===
|
|
204
|
+
if (row === this.lastSyncRow) {
|
|
165
205
|
this.selectedRowIndex = 12;
|
|
206
|
+
this.hoveredActionRow = this.lastSyncRow;
|
|
166
207
|
this.executeSync();
|
|
167
208
|
return true;
|
|
168
209
|
}
|
|
169
210
|
// Restaurar button
|
|
170
|
-
if (row ===
|
|
211
|
+
if (row === this.lastRestoreRow) {
|
|
171
212
|
this.selectedRowIndex = 13;
|
|
213
|
+
this.hoveredActionRow = this.lastRestoreRow;
|
|
172
214
|
this.executeRollback();
|
|
173
215
|
return true;
|
|
174
216
|
}
|
|
@@ -282,9 +324,16 @@ export class SyncView implements TuiView {
|
|
|
282
324
|
}
|
|
283
325
|
|
|
284
326
|
private executeRollback(): void {
|
|
285
|
-
|
|
327
|
+
const selectedTargets = this.clients
|
|
328
|
+
.filter((c) => c.selected)
|
|
329
|
+
.map((c) => c.id);
|
|
330
|
+
|
|
331
|
+
const targetDesc = selectedTargets.length > 0 ? `[${selectedTargets.join(", ")}]` : "todos os clientes";
|
|
332
|
+
this.actionLog.unshift(theme.yellow(`⏳ Restaurando backups anteriores de ${targetDesc}...`));
|
|
286
333
|
try {
|
|
287
|
-
const res = restoreAllClients(
|
|
334
|
+
const res = restoreAllClients({
|
|
335
|
+
targets: selectedTargets.length > 0 ? selectedTargets : undefined,
|
|
336
|
+
});
|
|
288
337
|
this.actionLog.unshift(
|
|
289
338
|
theme.green(`✓ Rollback concluído: ${res.restoredCount} arquivo(s) restaurados com sucesso.`),
|
|
290
339
|
);
|
|
@@ -298,6 +347,12 @@ export class SyncView implements TuiView {
|
|
|
298
347
|
const contentH = Math.max(22, height);
|
|
299
348
|
const leftW = Math.max(48, Math.floor(width * 0.52));
|
|
300
349
|
this.lastLeftW = leftW;
|
|
350
|
+
this.lastClientStartRow = 8;
|
|
351
|
+
this.lastClientEndRow = 8 + this.clients.length - 1;
|
|
352
|
+
this.lastModelRow = this.lastClientEndRow + 3;
|
|
353
|
+
this.lastScopeRow = this.lastModelRow + 1;
|
|
354
|
+
this.lastSyncRow = this.lastScopeRow + 3;
|
|
355
|
+
this.lastRestoreRow = this.lastSyncRow + 1;
|
|
301
356
|
const rightW = Math.max(30, width - leftW - 1);
|
|
302
357
|
|
|
303
358
|
// Left Panel: Options and Selectors
|
|
@@ -351,13 +406,13 @@ export class SyncView implements TuiView {
|
|
|
351
406
|
leftContent.push(` ${theme.bold("Ações:")}`);
|
|
352
407
|
// Row index 12: Sincronizar
|
|
353
408
|
const isSyncFocused = this.selectedRowIndex === 12;
|
|
354
|
-
const isSyncHovered = this.hoveredActionRow ===
|
|
409
|
+
const isSyncHovered = this.hoveredActionRow === this.lastSyncRow;
|
|
355
410
|
const syncLine = ` ${isSyncHovered || isSyncFocused ? theme.bgHover(` ${theme.cyan("[ Enter ] Sincronizar")} `) : `${theme.cyan("[ Enter ]")} Sincronizar`}`;
|
|
356
411
|
leftContent.push(syncLine);
|
|
357
412
|
|
|
358
413
|
// Row index 13: Restaurar
|
|
359
414
|
const isRestoreFocused = this.selectedRowIndex === 13;
|
|
360
|
-
const isRestoreHovered = this.hoveredActionRow ===
|
|
415
|
+
const isRestoreHovered = this.hoveredActionRow === this.lastRestoreRow;
|
|
361
416
|
const restoreLine = ` ${isRestoreHovered || isRestoreFocused ? theme.bgHover(` ${theme.yellow("[ R ] Restaurar")} `) : `${theme.yellow("[ R ]")} Restaurar`}`;
|
|
362
417
|
leftContent.push(restoreLine);
|
|
363
418
|
|
package/src/update-cli.ts
CHANGED
|
@@ -1,161 +1,17 @@
|
|
|
1
|
-
import { spawnSync } from "node:child_process";
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
-
|
|
6
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
-
const packageRoot = path.resolve(__dirname, "..");
|
|
8
|
-
const packageJsonPath = path.join(packageRoot, "package.json");
|
|
9
|
-
|
|
10
1
|
export type PackageManager = "bun" | "pnpm" | "yarn" | "npm";
|
|
11
2
|
|
|
12
|
-
export
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const execPath = process.execPath.toLowerCase();
|
|
20
|
-
if (execPath.includes("bun")) return "bun";
|
|
21
|
-
|
|
22
|
-
// Check if installation path contains pnpm / bun / yarn markers
|
|
23
|
-
const currentPath = packageRoot.toLowerCase();
|
|
24
|
-
if (currentPath.includes(".pnpm") || currentPath.includes("pnpm")) return "pnpm";
|
|
25
|
-
if (currentPath.includes(".bun") || currentPath.includes("bun")) return "bun";
|
|
26
|
-
if (currentPath.includes("yarn")) return "yarn";
|
|
27
|
-
|
|
28
|
-
return "npm";
|
|
29
|
-
}
|
|
30
|
-
export function getUpdateArgs(
|
|
31
|
-
pm: PackageManager,
|
|
32
|
-
packageName: string,
|
|
33
|
-
targetVersion?: string,
|
|
34
|
-
): { cmd: string; args: string[] } {
|
|
35
|
-
const versionTag = targetVersion ? `@${targetVersion}` : "@latest";
|
|
36
|
-
switch (pm) {
|
|
37
|
-
case "bun":
|
|
38
|
-
return { cmd: "bun", args: ["add", "-g", `${packageName}${versionTag}`] };
|
|
39
|
-
case "pnpm":
|
|
40
|
-
return {
|
|
41
|
-
cmd: "pnpm",
|
|
42
|
-
args: targetVersion
|
|
43
|
-
? ["add", "-g", `${packageName}@${targetVersion}`]
|
|
44
|
-
: ["update", "-g", packageName],
|
|
45
|
-
};
|
|
46
|
-
case "yarn":
|
|
47
|
-
return {
|
|
48
|
-
cmd: "yarn",
|
|
49
|
-
args: targetVersion
|
|
50
|
-
? ["global", "add", `${packageName}@${targetVersion}`]
|
|
51
|
-
: ["global", "upgrade", packageName],
|
|
52
|
-
};
|
|
53
|
-
case "npm":
|
|
54
|
-
default:
|
|
55
|
-
return { cmd: "npm", args: ["install", "-g", `${packageName}${versionTag}`] };
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function isNewerVersion(current: string, latest: string): boolean {
|
|
60
|
-
const c = current.replace(/^v/i, "").split(".").map((n) => parseInt(n, 10));
|
|
61
|
-
const l = latest.replace(/^v/i, "").split(".").map((n) => parseInt(n, 10));
|
|
62
|
-
for (let i = 0; i < 3; i++) {
|
|
63
|
-
const cv = c[i] || 0;
|
|
64
|
-
const lv = l[i] || 0;
|
|
65
|
-
if (lv > cv) return true;
|
|
66
|
-
if (lv < cv) return false;
|
|
67
|
-
}
|
|
68
|
-
return false;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export async function fetchLatestNpmVersion(packageName: string): Promise<string> {
|
|
72
|
-
// 1. Direct fast HTTP query to registry.npmjs.org (bypasses child process & Windows npm.cmd cold start)
|
|
73
|
-
try {
|
|
74
|
-
const controller = new AbortController();
|
|
75
|
-
const timer = setTimeout(() => controller.abort(), 6000);
|
|
76
|
-
const res = await fetch(`https://registry.npmjs.org/${packageName}/latest`, {
|
|
77
|
-
headers: { Accept: "application/json" },
|
|
78
|
-
signal: controller.signal,
|
|
79
|
-
});
|
|
80
|
-
clearTimeout(timer);
|
|
81
|
-
if (res.ok) {
|
|
82
|
-
const data = (await res.json()) as { version?: string };
|
|
83
|
-
if (data.version && typeof data.version === "string") {
|
|
84
|
-
return data.version.trim();
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
} catch {}
|
|
88
|
-
|
|
89
|
-
// 2. Fallback: npm view via CLI with a generous 30s timeout and clean semver extraction
|
|
90
|
-
try {
|
|
91
|
-
const fullCmd = `npm view ${packageName} version`;
|
|
92
|
-
const res = spawnSync(fullCmd, {
|
|
93
|
-
encoding: "utf-8",
|
|
94
|
-
shell: true,
|
|
95
|
-
timeout: 30000,
|
|
96
|
-
});
|
|
97
|
-
if (res.stdout) {
|
|
98
|
-
const lines = res.stdout.trim().split(/\r?\n/);
|
|
99
|
-
for (const line of lines.reverse()) {
|
|
100
|
-
const cleaned = line.trim().replace(/^v/i, "");
|
|
101
|
-
if (/^\d+\.\d+\.\d+/.test(cleaned)) {
|
|
102
|
-
return cleaned;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
} catch {}
|
|
3
|
+
export {
|
|
4
|
+
detectPackageManager,
|
|
5
|
+
getUpdateArgs,
|
|
6
|
+
isNewerVersion,
|
|
7
|
+
fetchLatestNpmVersion,
|
|
8
|
+
runUpdateCommand,
|
|
9
|
+
} from "../bin/update.js";
|
|
107
10
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
export async function runUpdateCommand(): Promise<void> {
|
|
112
|
-
let pkg: any = { name: "qwenproxy-cli", version: "1.0.0" };
|
|
113
|
-
try {
|
|
114
|
-
pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
115
|
-
} catch {}
|
|
116
|
-
|
|
117
|
-
const currentVersion = pkg.version || "1.0.0";
|
|
118
|
-
const packageName = pkg.name || "qwenproxy-cli";
|
|
119
|
-
const pm = detectPackageManager();
|
|
120
|
-
|
|
121
|
-
console.log(`\n📦 [QwenProxy] Versão local instalada: v${currentVersion}`);
|
|
122
|
-
console.log(`⚙️ [QwenProxy] Gerenciador de pacotes detectado: ${pm}`);
|
|
123
|
-
console.log(`🔍 [QwenProxy] Verificando se há novas versões de ${packageName} no npm registry...`);
|
|
124
|
-
|
|
125
|
-
const latestVersion = await fetchLatestNpmVersion(packageName);
|
|
126
|
-
|
|
127
|
-
if (!latestVersion) {
|
|
128
|
-
console.warn("⚠️ [QwenProxy] Não foi possível consultar o registro online.");
|
|
129
|
-
const manual = getUpdateArgs(pm, packageName);
|
|
130
|
-
console.log(`👉 Você pode forçar a atualização manualmente com:\n ${manual.cmd} ${manual.args.join(" ")}\n`);
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
console.log(`🌐 [QwenProxy] Versão mais recente disponível: v${latestVersion}`);
|
|
135
|
-
|
|
136
|
-
if (!isNewerVersion(currentVersion, latestVersion)) {
|
|
137
|
-
console.log(`\n✨ Você já está utilizando a versão mais recente (v${currentVersion})!\n`);
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
console.log(`\n🚀 Nova versão disponível: v${currentVersion} ➔ v${latestVersion}`);
|
|
142
|
-
const { cmd, args } = getUpdateArgs(pm, packageName, latestVersion);
|
|
143
|
-
console.log(`⏳ Atualizando globalmente via ${pm} (${cmd} ${args.join(" ")})...`);
|
|
144
|
-
const fullUpdateCmd = `${cmd} ${args.join(" ")}`;
|
|
145
|
-
const updateProc = spawnSync(fullUpdateCmd, {
|
|
146
|
-
stdio: "inherit",
|
|
147
|
-
shell: true,
|
|
148
|
-
});
|
|
149
|
-
if (updateProc.status === 0) {
|
|
150
|
-
console.log(`\n✅ [QwenProxy] Atualizado com sucesso para a versão v${latestVersion}!`);
|
|
151
|
-
console.log("👉 Digite 'qpx' para iniciar a nova versão.\n");
|
|
152
|
-
} else {
|
|
153
|
-
console.error(`\n❌ [QwenProxy] Falha ao atualizar automaticamente com ${cmd}.`);
|
|
154
|
-
console.log(`👉 Tente executar manualmente:\n ${cmd} ${args.join(" ")}\n`);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
11
|
+
import { runUpdateCommand } from "../bin/update.js";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
157
14
|
|
|
158
|
-
// Execute if run directly via CLI runner, not when imported in unit tests
|
|
159
15
|
const isDirectRun =
|
|
160
16
|
Boolean(process.argv[1]) &&
|
|
161
17
|
(fileURLToPath(import.meta.url) === path.resolve(process.argv[1]) ||
|