qwenproxy-cli 1.0.1 → 1.0.3
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 +1 -2
- package/src/api/server.ts +1 -0
- package/src/core/config.ts +2 -2
- package/src/services/playwright.ts +131 -53
- package/src/tui/app.ts +13 -6
- package/src/tui/proxy-client.ts +11 -1
- package/src/tui/theme.ts +1 -1
- package/src/tui/types.ts +1 -0
- package/src/tui/views/accounts-view.ts +6 -2
- package/src/tui/views/status-view.ts +3 -1
- package/src/update-cli.ts +121 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qwenproxy-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "High-performance OpenAI & Anthropic compatible API gateway for Qwen with multi-account rotation, interactive TUI, and resilient tool calling.",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"bin": {
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
"LICENSE"
|
|
17
17
|
],
|
|
18
18
|
"scripts": {
|
|
19
|
-
"postinstall": "patchright install chromium",
|
|
20
19
|
"start": "tsx src/index.ts",
|
|
21
20
|
"tui": "npx tsx src/tui/index.ts",
|
|
22
21
|
"login": "npx tsx src/login.ts",
|
package/src/api/server.ts
CHANGED
|
@@ -269,6 +269,7 @@ app.get("/health", async (c) => {
|
|
|
269
269
|
: undefined,
|
|
270
270
|
timestamp: Date.now(),
|
|
271
271
|
readyAccounts: (await import("../core/account-manager.js")).getHeadersReadyAccountIds(),
|
|
272
|
+
activeAccounts: (await import("../services/playwright.js")).getActivePlaywrightAccountIds(),
|
|
272
273
|
metrics: {
|
|
273
274
|
cache: await cache?.getStats(),
|
|
274
275
|
},
|
package/src/core/config.ts
CHANGED
|
@@ -52,7 +52,7 @@ const envSchema = z
|
|
|
52
52
|
// own context while serving. Accounts in cooldown (rate-limited) sit idle,
|
|
53
53
|
// drop out of the warm set and get evicted.
|
|
54
54
|
PLAYWRIGHT_MAX_ACTIVE_CONTEXTS: z.string().default("2"),
|
|
55
|
-
PLAYWRIGHT_PREPARE_ALL_ON_STARTUP: z.string().default("
|
|
55
|
+
PLAYWRIGHT_PREPARE_ALL_ON_STARTUP: z.string().default("false"),
|
|
56
56
|
CAPTCHA_SOLVER_ENABLED: z.string().default("true"),
|
|
57
57
|
CAPTCHA_SOLVER_MAX_ATTEMPTS: z.string().default("3"),
|
|
58
58
|
CAPTCHA_SOLVER_TIMEOUT_MS: z.string().default("15000"),
|
|
@@ -166,7 +166,7 @@ const envSchema = z
|
|
|
166
166
|
// trust score and gets TMD-challenged on the next request. On by default;
|
|
167
167
|
// the keeper skips accounts that are mid-stream or mutex-busy.
|
|
168
168
|
SESSION_KEEP_ALIVE_ENABLED: z.string().default("true"),
|
|
169
|
-
SESSION_KEEP_ALIVE_INTERVAL_MS: z.string().default("
|
|
169
|
+
SESSION_KEEP_ALIVE_INTERVAL_MS: z.string().default("180000"),
|
|
170
170
|
SESSION_KEEP_ALIVE_IDLE_MS: z.string().default("120000"),
|
|
171
171
|
SESSION_KEEP_ALIVE_NAVIGATION_INTERVAL_MS: z.string().default("480000"),
|
|
172
172
|
API_KEY: z.string().default(""),
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Captures real browser headers (bx-ua, bx-umidtoken) per account.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { chromium, type BrowserContext, type Page } from "patchright";
|
|
6
|
+
import { chromium, type Browser, type BrowserContext, type Page } from "patchright";
|
|
7
7
|
import path from "path";
|
|
8
8
|
import fs from "fs";
|
|
9
9
|
import crypto from "crypto";
|
|
@@ -183,6 +183,112 @@ const accountContexts = new Map<string, BrowserContext>();
|
|
|
183
183
|
const accountPages = new Map<string, Page>();
|
|
184
184
|
const cachedUserAgents = new Map<string, string>();
|
|
185
185
|
|
|
186
|
+
let sharedBrowser: Browser | null = null;
|
|
187
|
+
let sharedBrowserPromise: Promise<Browser> | null = null;
|
|
188
|
+
|
|
189
|
+
export function getSharedBrowser(): Browser | null {
|
|
190
|
+
return sharedBrowser;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function getStorageStatePath(accountId: string): string {
|
|
194
|
+
const profileDir = getAccountProfilePath(accountId);
|
|
195
|
+
return path.join(profileDir, "storage_state.json");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function loadStorageState(accountId: string): string | undefined {
|
|
199
|
+
const p1 = getStorageStatePath(accountId);
|
|
200
|
+
const p2 = path.join(path.dirname(p1), `${accountId}_state.json`);
|
|
201
|
+
const chosenPath = fs.existsSync(p1) ? p1 : fs.existsSync(p2) ? p2 : undefined;
|
|
202
|
+
if (!chosenPath) return undefined;
|
|
203
|
+
try {
|
|
204
|
+
const raw = fs.readFileSync(chosenPath, "utf8");
|
|
205
|
+
const state = JSON.parse(raw);
|
|
206
|
+
if (!state || typeof state !== "object" || !Array.isArray(state.cookies)) {
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|
|
209
|
+
return chosenPath;
|
|
210
|
+
} catch {
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export async function saveStorageState(
|
|
216
|
+
context: BrowserContext,
|
|
217
|
+
accountId: string,
|
|
218
|
+
): Promise<void> {
|
|
219
|
+
try {
|
|
220
|
+
const stateFile = getStorageStatePath(accountId);
|
|
221
|
+
const dir = path.dirname(stateFile);
|
|
222
|
+
if (!fs.existsSync(dir)) {
|
|
223
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
224
|
+
}
|
|
225
|
+
await context.storageState({ path: stateFile });
|
|
226
|
+
} catch (error) {
|
|
227
|
+
console.warn(
|
|
228
|
+
`[Playwright] Failed to save storage state for ${accountId}: ${getErrorMessage(error)}`,
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function hasValidAuthCookie(context: BrowserContext): Promise<boolean> {
|
|
234
|
+
try {
|
|
235
|
+
const cookies = await context.cookies();
|
|
236
|
+
return cookies.some(
|
|
237
|
+
(c) =>
|
|
238
|
+
(c.name.toLowerCase().includes("token") || c.name.toLowerCase().includes("session")) &&
|
|
239
|
+
(c.expires === undefined || c.expires === -1 || c.expires * 1000 > Date.now()),
|
|
240
|
+
);
|
|
241
|
+
} catch {
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export async function getOrLaunchSharedBrowser(
|
|
247
|
+
browserType: BrowserType = "chromium",
|
|
248
|
+
headless = true,
|
|
249
|
+
): Promise<Browser> {
|
|
250
|
+
if (sharedBrowser && sharedBrowser.isConnected()) {
|
|
251
|
+
return sharedBrowser;
|
|
252
|
+
}
|
|
253
|
+
if (sharedBrowserPromise) {
|
|
254
|
+
return sharedBrowserPromise;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
sharedBrowserPromise = (async () => {
|
|
258
|
+
const { engine, channel } = resolveBrowserEngine(browserType);
|
|
259
|
+
const defaultViewport = { width: 1280, height: 800 };
|
|
260
|
+
const launchArgs = buildChromiumLaunchArgs(defaultViewport);
|
|
261
|
+
|
|
262
|
+
console.log(
|
|
263
|
+
`[Playwright] Launching single shared ${browserType} browser...`,
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
const browser = await engine.launch({
|
|
267
|
+
headless,
|
|
268
|
+
channel,
|
|
269
|
+
ignoreDefaultArgs: ["--enable-automation", "--enable-blink-features"],
|
|
270
|
+
args: launchArgs,
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
browser.on("disconnected", () => {
|
|
274
|
+
console.warn("[Playwright] Shared browser disconnected");
|
|
275
|
+
sharedBrowser = null;
|
|
276
|
+
for (const accountId of Array.from(accountPages.keys())) {
|
|
277
|
+
cleanupPlaywrightAccountState(accountId);
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
sharedBrowser = browser;
|
|
282
|
+
return browser;
|
|
283
|
+
})();
|
|
284
|
+
|
|
285
|
+
try {
|
|
286
|
+
return await sharedBrowserPromise;
|
|
287
|
+
} finally {
|
|
288
|
+
sharedBrowserPromise = null;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
186
292
|
// Header cache per account
|
|
187
293
|
interface AccountHeaderCache {
|
|
188
294
|
headers: Record<string, string>;
|
|
@@ -1041,13 +1147,12 @@ export async function initPlaywrightForAccount(
|
|
|
1041
1147
|
// If a context limit is configured, make room by closing idle contexts.
|
|
1042
1148
|
await evictIdlePlaywrightContextsToLimit().catch(() => {});
|
|
1043
1149
|
|
|
1044
|
-
const profilePath = getAccountProfilePath(account.id);
|
|
1045
1150
|
const fingerprint = getFingerprintProfile(account.id);
|
|
1046
|
-
const
|
|
1151
|
+
const sharedBrowser = await getOrLaunchSharedBrowser(browserType, headless);
|
|
1152
|
+
const storageState = loadStorageState(account.id);
|
|
1047
1153
|
|
|
1048
|
-
const acctContext = await
|
|
1049
|
-
|
|
1050
|
-
channel,
|
|
1154
|
+
const acctContext = await sharedBrowser.newContext({
|
|
1155
|
+
...(storageState ? { storageState } : {}),
|
|
1051
1156
|
userAgent: fingerprint.userAgent,
|
|
1052
1157
|
locale: fingerprint.locale,
|
|
1053
1158
|
timezoneId: fingerprint.timezoneId,
|
|
@@ -1062,8 +1167,6 @@ export async function initPlaywrightForAccount(
|
|
|
1062
1167
|
"sec-ch-ua-mobile": "?0",
|
|
1063
1168
|
"sec-ch-ua-platform": '"Windows"',
|
|
1064
1169
|
},
|
|
1065
|
-
ignoreDefaultArgs: ["--enable-automation", "--enable-blink-features"],
|
|
1066
|
-
args: buildChromiumLaunchArgs(fingerprint.viewport),
|
|
1067
1170
|
});
|
|
1068
1171
|
|
|
1069
1172
|
try {
|
|
@@ -1073,23 +1176,7 @@ export async function initPlaywrightForAccount(
|
|
|
1073
1176
|
await hook(acctContext);
|
|
1074
1177
|
}
|
|
1075
1178
|
|
|
1076
|
-
|
|
1077
|
-
// Reuse it instead of creating a second tab. Prefer a tab already on the
|
|
1078
|
-
// Qwen origin when one exists.
|
|
1079
|
-
const existingPages = acctContext.pages().filter((p) => !p.isClosed());
|
|
1080
|
-
const acctPage =
|
|
1081
|
-
existingPages.find((p) => p.url().startsWith(qwenOrigin())) ??
|
|
1082
|
-
existingPages[0] ??
|
|
1083
|
-
(await acctContext.newPage());
|
|
1084
|
-
|
|
1085
|
-
// Close any extra blank tabs that may have been created by the browser
|
|
1086
|
-
// profile/startup, but keep the primary page selected above.
|
|
1087
|
-
for (const extraPage of existingPages.slice(1)) {
|
|
1088
|
-
if (extraPage !== acctPage && extraPage.url() === "about:blank") {
|
|
1089
|
-
await extraPage.close({ runBeforeUnload: false }).catch(() => {});
|
|
1090
|
-
}
|
|
1091
|
-
}
|
|
1092
|
-
|
|
1179
|
+
const acctPage = await acctContext.newPage();
|
|
1093
1180
|
acctPage.setDefaultTimeout(config.timeouts.page);
|
|
1094
1181
|
acctPage.setDefaultNavigationTimeout(config.timeouts.navigation);
|
|
1095
1182
|
accountContexts.set(account.id, acctContext);
|
|
@@ -1205,12 +1292,12 @@ export async function validateAccountLogin(
|
|
|
1205
1292
|
try {
|
|
1206
1293
|
if (accountPages.has(account.id)) return true;
|
|
1207
1294
|
|
|
1208
|
-
const profilePath = getAccountProfilePath(account.id);
|
|
1209
1295
|
const fingerprint = getFingerprintProfile(account.id);
|
|
1210
|
-
const
|
|
1211
|
-
const
|
|
1212
|
-
|
|
1213
|
-
|
|
1296
|
+
const sharedBrowser = await getOrLaunchSharedBrowser(browserType, headless);
|
|
1297
|
+
const storageState = loadStorageState(account.id);
|
|
1298
|
+
|
|
1299
|
+
const acctContext = await sharedBrowser.newContext({
|
|
1300
|
+
...(storageState ? { storageState } : {}),
|
|
1214
1301
|
userAgent: fingerprint.userAgent,
|
|
1215
1302
|
locale: fingerprint.locale,
|
|
1216
1303
|
timezoneId: fingerprint.timezoneId,
|
|
@@ -1225,18 +1312,11 @@ export async function validateAccountLogin(
|
|
|
1225
1312
|
"sec-ch-ua-mobile": "?0",
|
|
1226
1313
|
"sec-ch-ua-platform": '"Windows"',
|
|
1227
1314
|
},
|
|
1228
|
-
ignoreDefaultArgs: ["--enable-automation", "--enable-blink-features"],
|
|
1229
|
-
args: buildChromiumLaunchArgs(fingerprint.viewport),
|
|
1230
1315
|
});
|
|
1231
1316
|
|
|
1232
1317
|
try {
|
|
1233
1318
|
await acctContext.addInitScript(getStealthScript(fingerprint));
|
|
1234
|
-
|
|
1235
|
-
const existingPages = acctContext.pages().filter((p) => !p.isClosed());
|
|
1236
|
-
const acctPage =
|
|
1237
|
-
existingPages.find((p) => p.url().startsWith(qwenOrigin())) ??
|
|
1238
|
-
existingPages[0] ??
|
|
1239
|
-
(await acctContext.newPage());
|
|
1319
|
+
const acctPage = await acctContext.newPage();
|
|
1240
1320
|
|
|
1241
1321
|
// Check if already logged in via cookies
|
|
1242
1322
|
const cookies = await acctContext.cookies();
|
|
@@ -1306,12 +1386,14 @@ async function loginToQwen(
|
|
|
1306
1386
|
// Try API login first
|
|
1307
1387
|
const apiResult = await loginViaApi(page, email, password);
|
|
1308
1388
|
if (apiResult) {
|
|
1389
|
+
await saveStorageState(page.context(), accountId);
|
|
1309
1390
|
return true;
|
|
1310
1391
|
}
|
|
1311
1392
|
|
|
1312
1393
|
// Fallback to UI login
|
|
1313
1394
|
const uiResult = await loginViaUi(page, email, password);
|
|
1314
1395
|
if (uiResult) {
|
|
1396
|
+
await saveStorageState(page.context(), accountId);
|
|
1315
1397
|
return true;
|
|
1316
1398
|
}
|
|
1317
1399
|
|
|
@@ -2107,6 +2189,8 @@ async function resetPlaywrightProfileLocked(accountId: string): Promise<void> {
|
|
|
2107
2189
|
await closePlaywrightForAccountLocked(accountId);
|
|
2108
2190
|
const profilePath = getAccountProfilePath(accountId);
|
|
2109
2191
|
removePlaywrightProfile(profilePath);
|
|
2192
|
+
const stateFile2 = path.join(path.dirname(profilePath), `${accountId}_state.json`);
|
|
2193
|
+
try { fs.rmSync(stateFile2, { force: true }); } catch {}
|
|
2110
2194
|
}
|
|
2111
2195
|
|
|
2112
2196
|
/**
|
|
@@ -2533,7 +2617,11 @@ async function closePlaywrightContextBestEffort(
|
|
|
2533
2617
|
accountId: string,
|
|
2534
2618
|
context: BrowserContext,
|
|
2535
2619
|
): Promise<void> {
|
|
2536
|
-
|
|
2620
|
+
try {
|
|
2621
|
+
if (await hasValidAuthCookie(context)) {
|
|
2622
|
+
await saveStorageState(context, accountId);
|
|
2623
|
+
}
|
|
2624
|
+
} catch {}
|
|
2537
2625
|
|
|
2538
2626
|
try {
|
|
2539
2627
|
const pages = context.pages();
|
|
@@ -2558,19 +2646,6 @@ async function closePlaywrightContextBestEffort(
|
|
|
2558
2646
|
`[Playwright] Failed to close context for ${accountId}: ${getErrorMessage(error)}`,
|
|
2559
2647
|
);
|
|
2560
2648
|
}
|
|
2561
|
-
|
|
2562
|
-
if (browserProcess && !browserProcess.killed) {
|
|
2563
|
-
try {
|
|
2564
|
-
browserProcess.kill("SIGKILL");
|
|
2565
|
-
console.warn(
|
|
2566
|
-
`[Playwright] Killed lingering browser process for ${accountId}`,
|
|
2567
|
-
);
|
|
2568
|
-
} catch (killError) {
|
|
2569
|
-
console.warn(
|
|
2570
|
-
`[Playwright] Failed to kill browser process for ${accountId}: ${getErrorMessage(killError)}`,
|
|
2571
|
-
);
|
|
2572
|
-
}
|
|
2573
|
-
}
|
|
2574
2649
|
}
|
|
2575
2650
|
}
|
|
2576
2651
|
|
|
@@ -2651,6 +2726,10 @@ export async function closeAllPlaywright(): Promise<void> {
|
|
|
2651
2726
|
for (const accountId of accountIds) {
|
|
2652
2727
|
await closePlaywrightForAccount(accountId);
|
|
2653
2728
|
}
|
|
2729
|
+
if (sharedBrowser && sharedBrowser.isConnected()) {
|
|
2730
|
+
await sharedBrowser.close().catch(() => {});
|
|
2731
|
+
sharedBrowser = null;
|
|
2732
|
+
}
|
|
2654
2733
|
} finally {
|
|
2655
2734
|
closingAllPlaywright = false;
|
|
2656
2735
|
}
|
|
@@ -2726,7 +2805,6 @@ export async function getTokenDiagnostics(
|
|
|
2726
2805
|
const targetAccounts = accountId
|
|
2727
2806
|
? [accountId]
|
|
2728
2807
|
: Array.from(accountContexts.keys());
|
|
2729
|
-
|
|
2730
2808
|
const allCookies: CookieDiagnostic[] = [];
|
|
2731
2809
|
const headerDiags: HeaderDiagnostic[] = [];
|
|
2732
2810
|
|
package/src/tui/app.ts
CHANGED
|
@@ -7,7 +7,19 @@ import type { TuiView, ProxyStatusSnapshot } from "./types.ts";
|
|
|
7
7
|
import { theme, glyphs, drawBox, stringWidth } from "./theme.ts";
|
|
8
8
|
import { fetchProxyStatus } from "./proxy-client.ts";
|
|
9
9
|
import { ServerManager } from "./server-manager.ts";
|
|
10
|
+
import fs from "node:fs";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
10
13
|
|
|
14
|
+
let cachedAppVersion = "v1.0.2";
|
|
15
|
+
try {
|
|
16
|
+
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const pkgPath = path.resolve(currentDir, "../../package.json");
|
|
18
|
+
if (fs.existsSync(pkgPath)) {
|
|
19
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
20
|
+
if (pkg.version) cachedAppVersion = `v${pkg.version}`;
|
|
21
|
+
}
|
|
22
|
+
} catch {}
|
|
11
23
|
import { StatusView } from "./views/status-view.ts";
|
|
12
24
|
import { ChatView } from "./views/chat-view.ts";
|
|
13
25
|
import { SyncView } from "./views/sync-view.ts";
|
|
@@ -91,11 +103,6 @@ export class TuiApp {
|
|
|
91
103
|
if (!this.isRunning) return;
|
|
92
104
|
try {
|
|
93
105
|
this.statusSnapshot = await fetchProxyStatus();
|
|
94
|
-
// If the active view has a model refresher, keep it synchronized
|
|
95
|
-
const activeView = this.views[this.activeViewIndex];
|
|
96
|
-
if ("refreshModels" in activeView && typeof (activeView as any).refreshModels === "function") {
|
|
97
|
-
void (activeView as any).refreshModels();
|
|
98
|
-
}
|
|
99
106
|
this.requestRender();
|
|
100
107
|
} catch {}
|
|
101
108
|
}, 1000);
|
|
@@ -245,7 +252,7 @@ export class TuiApp {
|
|
|
245
252
|
|
|
246
253
|
const headerContent = [` ${tabsBar}`];
|
|
247
254
|
const headerBox = drawBox({
|
|
248
|
-
title: `QwenProxy
|
|
255
|
+
title: `QwenProxy ${cachedAppVersion} ${statusChip}`,
|
|
249
256
|
width: cols,
|
|
250
257
|
height: 3,
|
|
251
258
|
borderColor: theme.borderActive,
|
package/src/tui/proxy-client.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
clearAccountCooldown,
|
|
11
11
|
isAccountHeadersReady,
|
|
12
12
|
} from "../core/account-manager.ts";
|
|
13
|
+
import { isPlaywrightInitialized } from "../services/playwright.ts";
|
|
13
14
|
import { getAccountConcurrencySnapshot } from "../core/account-concurrency.ts";
|
|
14
15
|
import { getRssUsageSnapshot } from "../core/memory-usage.ts";
|
|
15
16
|
import type { ProxyStatusSnapshot } from "./types.ts";
|
|
@@ -46,12 +47,14 @@ let cachedAccounts: Array<{
|
|
|
46
47
|
onCooldown: boolean;
|
|
47
48
|
remainingCooldownMs: number;
|
|
48
49
|
headersReady: boolean;
|
|
50
|
+
isInitialized: boolean;
|
|
49
51
|
}> = [];
|
|
50
52
|
let lastAccountsFetch = 0;
|
|
51
53
|
let isHealthCheckPending = false;
|
|
52
54
|
let lastOnlineState = false;
|
|
53
55
|
let lastOverallStatus = "offline";
|
|
54
56
|
let lastServerReadyAccounts: Set<string> | null = null;
|
|
57
|
+
let lastServerActiveAccounts: Set<string> | null = null;
|
|
55
58
|
export async function fetchProxyStatus(): Promise<ProxyStatusSnapshot> {
|
|
56
59
|
const port = config.server?.port || 7936;
|
|
57
60
|
const configuredHost = config.server?.host;
|
|
@@ -73,9 +76,13 @@ export async function fetchProxyStatus(): Promise<ProxyStatusSnapshot> {
|
|
|
73
76
|
if (Array.isArray(data.readyAccounts)) {
|
|
74
77
|
lastServerReadyAccounts = new Set(data.readyAccounts);
|
|
75
78
|
}
|
|
79
|
+
if (Array.isArray(data.activeAccounts)) {
|
|
80
|
+
lastServerActiveAccounts = new Set(data.activeAccounts);
|
|
81
|
+
}
|
|
76
82
|
} else {
|
|
77
83
|
lastOnlineState = false;
|
|
78
84
|
lastServerReadyAccounts = null;
|
|
85
|
+
lastServerActiveAccounts = null;
|
|
79
86
|
}
|
|
80
87
|
})
|
|
81
88
|
.catch(() => {
|
|
@@ -104,6 +111,9 @@ export async function fetchProxyStatus(): Promise<ProxyStatusSnapshot> {
|
|
|
104
111
|
const headersReady = lastServerReadyAccounts !== null
|
|
105
112
|
? lastServerReadyAccounts.has(acc.id)
|
|
106
113
|
: isAccountHeadersReady(acc.id);
|
|
114
|
+
const isInitialized = lastServerActiveAccounts !== null
|
|
115
|
+
? lastServerActiveAccounts.has(acc.id)
|
|
116
|
+
: isPlaywrightInitialized(acc.id);
|
|
107
117
|
return {
|
|
108
118
|
id: acc.id,
|
|
109
119
|
emailOrName: maskAccountIdentifier(acc.email || acc.id),
|
|
@@ -112,10 +122,10 @@ export async function fetchProxyStatus(): Promise<ProxyStatusSnapshot> {
|
|
|
112
122
|
onCooldown,
|
|
113
123
|
remainingCooldownMs,
|
|
114
124
|
headersReady,
|
|
125
|
+
isInitialized,
|
|
115
126
|
};
|
|
116
127
|
});
|
|
117
128
|
}
|
|
118
|
-
|
|
119
129
|
const accounts = cachedAccounts;
|
|
120
130
|
const online = lastOnlineState;
|
|
121
131
|
const overallStatus = lastOverallStatus;
|
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[?
|
|
19
|
+
enableMouse: "\x1b[?1000h\x1b[?1002h\x1b[?1006h",
|
|
20
20
|
disableMouse: "\x1b[?1006l\x1b[?1005l\x1b[?1004l\x1b[?1003l\x1b[?1002l\x1b[?1000l\x1b[?1015l",
|
|
21
21
|
};
|
|
22
22
|
|
package/src/tui/types.ts
CHANGED
|
@@ -497,7 +497,9 @@ export class AccountsView implements TuiView {
|
|
|
497
497
|
const mins = Math.max(1, Math.round(acc.remainingCooldownMs / 60000));
|
|
498
498
|
status = theme.yellow(`⚠ ${mins}m cd `);
|
|
499
499
|
} else if (!acc.headersReady) {
|
|
500
|
-
status =
|
|
500
|
+
status = acc.isInitialized
|
|
501
|
+
? theme.yellow(`◐ Aquecendo...`)
|
|
502
|
+
: theme.muted(`○ Standby `);
|
|
501
503
|
}
|
|
502
504
|
|
|
503
505
|
const line = `${pointer}${num}${name}${status}`;
|
|
@@ -550,7 +552,9 @@ export class AccountsView implements TuiView {
|
|
|
550
552
|
|
|
551
553
|
const hStatus = selected.headersReady
|
|
552
554
|
? theme.green(`${glyphs.check} Capturados`)
|
|
553
|
-
:
|
|
555
|
+
: selected.isInitialized
|
|
556
|
+
? theme.yellow(`◐ Aquecendo...`)
|
|
557
|
+
: theme.muted(`${glyphs.circle} Standby (Sob Demanda)`);
|
|
554
558
|
rightContent.push(` ${theme.bold("Headers:")} ${hStatus}`);
|
|
555
559
|
rightContent.push("");
|
|
556
560
|
rightContent.push(` ${theme.dim("─────────────────────────────────")}`);
|
|
@@ -175,7 +175,9 @@ export class StatusView implements TuiView {
|
|
|
175
175
|
const mins = Math.max(1, Math.round(acc.remainingCooldownMs / 60000));
|
|
176
176
|
status = theme.yellow(`⚠ Cooldown ${mins}m`);
|
|
177
177
|
} else if (!acc.headersReady) {
|
|
178
|
-
status =
|
|
178
|
+
status = acc.isInitialized
|
|
179
|
+
? theme.yellow(`◐ Aquecendo...`)
|
|
180
|
+
: theme.muted(`○ Standby`);
|
|
179
181
|
}
|
|
180
182
|
rightContent.push(` ${num} ${name} ${status}`);
|
|
181
183
|
});
|
|
@@ -0,0 +1,121 @@
|
|
|
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
|
+
export type PackageManager = "bun" | "pnpm" | "yarn" | "npm";
|
|
11
|
+
|
|
12
|
+
export function detectPackageManager(): PackageManager {
|
|
13
|
+
const userAgent = process.env.npm_config_user_agent || "";
|
|
14
|
+
if (userAgent.startsWith("bun")) return "bun";
|
|
15
|
+
if (userAgent.startsWith("pnpm")) return "pnpm";
|
|
16
|
+
if (userAgent.startsWith("yarn")) return "yarn";
|
|
17
|
+
|
|
18
|
+
// Check which executable is actually running the script or installed in PATH
|
|
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(pm: PackageManager, packageName: string): { cmd: string; args: string[] } {
|
|
31
|
+
switch (pm) {
|
|
32
|
+
case "bun":
|
|
33
|
+
return { cmd: "bun", args: ["add", "-g", `${packageName}@latest`] };
|
|
34
|
+
case "pnpm":
|
|
35
|
+
return { cmd: "pnpm", args: ["update", "-g", packageName] };
|
|
36
|
+
case "yarn":
|
|
37
|
+
return { cmd: "yarn", args: ["global", "upgrade", packageName] };
|
|
38
|
+
case "npm":
|
|
39
|
+
default:
|
|
40
|
+
return { cmd: "npm", args: ["install", "-g", `${packageName}@latest`] };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function isNewerVersion(current: string, latest: string): boolean {
|
|
45
|
+
const c = current.replace(/^v/i, "").split(".").map((n) => parseInt(n, 10));
|
|
46
|
+
const l = latest.replace(/^v/i, "").split(".").map((n) => parseInt(n, 10));
|
|
47
|
+
for (let i = 0; i < 3; i++) {
|
|
48
|
+
const cv = c[i] || 0;
|
|
49
|
+
const lv = l[i] || 0;
|
|
50
|
+
if (lv > cv) return true;
|
|
51
|
+
if (lv < cv) return false;
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function runUpdateCommand(): Promise<void> {
|
|
57
|
+
let pkg: any = { name: "qwenproxy-cli", version: "1.0.0" };
|
|
58
|
+
try {
|
|
59
|
+
pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
60
|
+
} catch {}
|
|
61
|
+
|
|
62
|
+
const currentVersion = pkg.version || "1.0.0";
|
|
63
|
+
const packageName = pkg.name || "qwenproxy-cli";
|
|
64
|
+
const pm = detectPackageManager();
|
|
65
|
+
|
|
66
|
+
console.log(`\n📦 [QwenProxy] Versão local instalada: v${currentVersion}`);
|
|
67
|
+
console.log(`⚙️ [QwenProxy] Gerenciador de pacotes detectado: ${pm}`);
|
|
68
|
+
console.log(`🔍 [QwenProxy] Verificando se há novas versões de ${packageName} no npm registry...`);
|
|
69
|
+
|
|
70
|
+
let latestVersion = "";
|
|
71
|
+
try {
|
|
72
|
+
const fullCmd = `npm view ${packageName} version`;
|
|
73
|
+
const res = spawnSync(fullCmd, {
|
|
74
|
+
encoding: "utf-8",
|
|
75
|
+
shell: true,
|
|
76
|
+
timeout: 10000,
|
|
77
|
+
});
|
|
78
|
+
latestVersion = res.stdout ? res.stdout.trim() : "";
|
|
79
|
+
} catch {}
|
|
80
|
+
|
|
81
|
+
if (!latestVersion) {
|
|
82
|
+
console.warn("⚠️ [QwenProxy] Não foi possível consultar o registro online.");
|
|
83
|
+
const manual = getUpdateArgs(pm, packageName);
|
|
84
|
+
console.log(`👉 Você pode forçar a atualização manualmente com:\n ${manual.cmd} ${manual.args.join(" ")}\n`);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
console.log(`🌐 [QwenProxy] Versão mais recente disponível: v${latestVersion}`);
|
|
89
|
+
|
|
90
|
+
if (!isNewerVersion(currentVersion, latestVersion)) {
|
|
91
|
+
console.log(`\n✨ Você já está utilizando a versão mais recente (v${currentVersion})!\n`);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
console.log(`\n🚀 Nova versão disponível: v${currentVersion} ➔ v${latestVersion}`);
|
|
96
|
+
const { cmd, args } = getUpdateArgs(pm, packageName);
|
|
97
|
+
console.log(`⏳ Atualizando globalmente via ${pm} (${cmd} ${args.join(" ")})...`);
|
|
98
|
+
const fullUpdateCmd = `${cmd} ${args.join(" ")}`;
|
|
99
|
+
const updateProc = spawnSync(fullUpdateCmd, {
|
|
100
|
+
stdio: "inherit",
|
|
101
|
+
shell: true,
|
|
102
|
+
});
|
|
103
|
+
if (updateProc.status === 0) {
|
|
104
|
+
console.log(`\n✅ [QwenProxy] Atualizado com sucesso para a versão v${latestVersion}!`);
|
|
105
|
+
console.log("👉 Digite 'qpx' para iniciar a nova versão.\n");
|
|
106
|
+
} else {
|
|
107
|
+
console.error(`\n❌ [QwenProxy] Falha ao atualizar automaticamente com ${cmd}.`);
|
|
108
|
+
console.log(`👉 Tente executar manualmente:\n ${cmd} ${args.join(" ")}\n`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Execute if run directly via CLI runner, not when imported in unit tests
|
|
113
|
+
const isDirectRun =
|
|
114
|
+
Boolean(process.argv[1]) &&
|
|
115
|
+
(fileURLToPath(import.meta.url) === path.resolve(process.argv[1]) ||
|
|
116
|
+
process.argv[1].endsWith("update-cli.ts") ||
|
|
117
|
+
process.argv[1].endsWith("update-cli.js"));
|
|
118
|
+
|
|
119
|
+
if (isDirectRun) {
|
|
120
|
+
void runUpdateCommand();
|
|
121
|
+
}
|