qwenproxy-cli 1.5.2 → 1.6.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/dist/clean-cache.js +205 -22
- package/dist/delete-chats.js +199 -36
- package/dist/index.js +492 -57
- package/dist/login.js +95 -24
- package/dist/reset-cooldowns.js +205 -22
- package/package.json +3 -3
- package/src/api/server.ts +1 -0
- package/src/core/daily-usage.ts +276 -0
- package/src/core/database.ts +27 -1
- package/src/core/version.ts +1 -1
- package/src/routes/chat/account.ts +43 -10
- package/src/routes/chat/index.ts +1 -1
- package/src/routes/chat/streaming.ts +31 -3
- package/src/services/captcha-coordinator.ts +12 -7
- package/src/services/playwright.ts +76 -24
- package/src/services/qwen.ts +130 -16
- package/src/tui/proxy-client.ts +11 -1
- package/src/tui/types.ts +11 -0
- package/src/tui/views/accounts-view.ts +19 -2
- package/src/tui/views/status-view.ts +37 -4
|
@@ -512,6 +512,7 @@ const COOKIE_CACHE_TTL = 15 * 60 * 1000; // 15 minutes
|
|
|
512
512
|
const cookieCaches = new Map<string, { cookie: string; timestamp: number }>();
|
|
513
513
|
const lastAccountActivity = new Map<string, number>();
|
|
514
514
|
const lastKeepAliveNavigation = new Map<string, number>();
|
|
515
|
+
const lastProactiveTokenRefresh = new Map<string, number>();
|
|
515
516
|
const profileResetQueue = new Map<string, Promise<void>>();
|
|
516
517
|
let profileResetChain: Promise<void> = Promise.resolve();
|
|
517
518
|
let closingAllPlaywright = false;
|
|
@@ -1228,6 +1229,11 @@ export async function getBasicHeaders(accountId: string): Promise<{
|
|
|
1228
1229
|
throw new Error(`Playwright not initialized for account: ${accountId}`);
|
|
1229
1230
|
}
|
|
1230
1231
|
|
|
1232
|
+
const inFlightInit = inFlightAccountInits.get(accountId);
|
|
1233
|
+
if (inFlightInit) {
|
|
1234
|
+
await inFlightInit.catch(() => {});
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1231
1237
|
// Acquire mutex to prevent concurrent browser access
|
|
1232
1238
|
const release = await acquireAccountMutex(
|
|
1233
1239
|
accountId,
|
|
@@ -3068,6 +3074,12 @@ export async function captureQwenHeaders(
|
|
|
3068
3074
|
const { saveAuthSession } = await import("../core/database.ts");
|
|
3069
3075
|
const { parseJwtExpiry } = await import("../utils/jwt.ts");
|
|
3070
3076
|
const tokenExpiry = parseJwtExpiry(capturedHeaders.cookie);
|
|
3077
|
+
let rTok: string | undefined;
|
|
3078
|
+
try {
|
|
3079
|
+
rTok =
|
|
3080
|
+
(await page.evaluate(() => localStorage.getItem("refresh_token") || "")).trim() ||
|
|
3081
|
+
undefined;
|
|
3082
|
+
} catch {}
|
|
3071
3083
|
saveAuthSession(accountId, {
|
|
3072
3084
|
cookie: capturedHeaders.cookie,
|
|
3073
3085
|
userAgent: capturedHeaders["user-agent"],
|
|
@@ -3079,6 +3091,7 @@ export async function captureQwenHeaders(
|
|
|
3079
3091
|
secChUaPlatform: capturedHeaders["sec-ch-ua-platform"],
|
|
3080
3092
|
version: capturedHeaders["version"],
|
|
3081
3093
|
tokenExpiresAt: tokenExpiry || undefined,
|
|
3094
|
+
refreshToken: rTok,
|
|
3082
3095
|
capturedAt: Date.now(),
|
|
3083
3096
|
});
|
|
3084
3097
|
} catch {}
|
|
@@ -3121,40 +3134,61 @@ export async function captureQwenHeaders(
|
|
|
3121
3134
|
const isAuthUrl = currentUrl.includes("/auth") || currentUrl.includes("/login");
|
|
3122
3135
|
const isSuspectedGuest = isAuthUrl || (attempt >= 2 && !(await isPageLoggedIn(page, 5000)));
|
|
3123
3136
|
if (isSuspectedGuest) {
|
|
3124
|
-
|
|
3125
|
-
const
|
|
3126
|
-
if (
|
|
3127
|
-
console.
|
|
3128
|
-
|
|
3137
|
+
// Try fast silent token refresh first using the 30-day refresh_token in-page (~200ms)
|
|
3138
|
+
const silentRefresh = await refreshAccountToken(accountId);
|
|
3139
|
+
if (silentRefresh.success) {
|
|
3140
|
+
console.log(
|
|
3141
|
+
`⚡ [Playwright] Token refreshed silently during header capture for ${accountId}`,
|
|
3129
3142
|
);
|
|
3130
|
-
|
|
3131
|
-
const ok = await loginToQwen(accountId, creds.email, creds.password);
|
|
3132
|
-
if (ok) {
|
|
3133
|
-
// Re-login navigated; load the chat page and wait for hydration so
|
|
3134
|
-
// the check below probes a live authenticated chat page.
|
|
3143
|
+
if (isAuthUrl) {
|
|
3135
3144
|
await page.goto(qwenUrl("/"), {
|
|
3136
3145
|
waitUntil: "domcontentloaded",
|
|
3137
3146
|
timeout: Math.min(config.timeouts.navigation, timeoutMs),
|
|
3138
3147
|
});
|
|
3139
|
-
await sleep(
|
|
3148
|
+
await sleep(1500);
|
|
3140
3149
|
}
|
|
3141
|
-
if (
|
|
3150
|
+
if (await isPageLoggedIn(page, 5000)) {
|
|
3151
|
+
armOverallDeadline();
|
|
3152
|
+
}
|
|
3153
|
+
}
|
|
3154
|
+
|
|
3155
|
+
const stillSuspected = !silentRefresh.success || !(await isPageLoggedIn(page, 2000));
|
|
3156
|
+
if (stillSuspected) {
|
|
3157
|
+
const { getAccountCredentials } = await import("../core/accounts.ts");
|
|
3158
|
+
const creds = getAccountCredentials(accountId);
|
|
3159
|
+
if (creds && creds.email && creds.password) {
|
|
3160
|
+
console.warn(
|
|
3161
|
+
`⚠️ [Playwright] Session expired or guest state detected during header capture for ${accountId}; re-authenticating...`,
|
|
3162
|
+
);
|
|
3163
|
+
armOverallDeadline();
|
|
3164
|
+
const ok = await loginToQwen(accountId, creds.email, creds.password);
|
|
3165
|
+
if (ok) {
|
|
3166
|
+
// Re-login navigated; load the chat page and wait for hydration so
|
|
3167
|
+
// the check below probes a live authenticated chat page.
|
|
3168
|
+
await page.goto(qwenUrl("/"), {
|
|
3169
|
+
waitUntil: "domcontentloaded",
|
|
3170
|
+
timeout: Math.min(config.timeouts.navigation, timeoutMs),
|
|
3171
|
+
});
|
|
3172
|
+
await sleep(2000);
|
|
3173
|
+
}
|
|
3174
|
+
if (!ok || !(await isPageLoggedIn(page, 6000))) {
|
|
3175
|
+
settle(
|
|
3176
|
+
new Error(
|
|
3177
|
+
`Header capture failed for ${accountId}: re-login after session expiry did not succeed`,
|
|
3178
|
+
),
|
|
3179
|
+
);
|
|
3180
|
+
return;
|
|
3181
|
+
}
|
|
3182
|
+
armOverallDeadline();
|
|
3183
|
+
if (settled || page.isClosed()) return;
|
|
3184
|
+
} else {
|
|
3142
3185
|
settle(
|
|
3143
3186
|
new Error(
|
|
3144
|
-
`Header capture failed for ${accountId}: re-login
|
|
3187
|
+
`Header capture failed for ${accountId}: session expired and no credentials available for re-login (run 'qpx login')`,
|
|
3145
3188
|
),
|
|
3146
3189
|
);
|
|
3147
3190
|
return;
|
|
3148
3191
|
}
|
|
3149
|
-
armOverallDeadline();
|
|
3150
|
-
if (settled || page.isClosed()) return;
|
|
3151
|
-
} else {
|
|
3152
|
-
settle(
|
|
3153
|
-
new Error(
|
|
3154
|
-
`Header capture failed for ${accountId}: session expired and no credentials available for re-login (run 'qpx login')`,
|
|
3155
|
-
),
|
|
3156
|
-
);
|
|
3157
|
-
return;
|
|
3158
3192
|
}
|
|
3159
3193
|
}
|
|
3160
3194
|
|
|
@@ -3593,7 +3627,15 @@ async function refreshHeadersInternal(
|
|
|
3593
3627
|
`[Playwright] Navigation check failed during refresh for ${accountId}:`,
|
|
3594
3628
|
(navErr as Error).message,
|
|
3595
3629
|
);
|
|
3596
|
-
await
|
|
3630
|
+
if (await isPageLoggedIn(page, 3000)) {
|
|
3631
|
+
// Still authenticated despite navigation timeout; do not escalate to slow form re-auth
|
|
3632
|
+
} else {
|
|
3633
|
+
// Try silent in-page token refresh before executing full form login
|
|
3634
|
+
const refreshed = await refreshAccountToken(accountId);
|
|
3635
|
+
if (!refreshed.success) {
|
|
3636
|
+
await executeReauth();
|
|
3637
|
+
}
|
|
3638
|
+
}
|
|
3597
3639
|
}
|
|
3598
3640
|
}
|
|
3599
3641
|
|
|
@@ -3610,6 +3652,12 @@ async function refreshHeadersInternal(
|
|
|
3610
3652
|
const { saveAuthSession } = await import("../core/database.ts");
|
|
3611
3653
|
const tokenCookie = liveCookies.find((c) => c.name === "token");
|
|
3612
3654
|
const exp = tokenCookie ? parseJwtExpiry(tokenCookie.value) : undefined;
|
|
3655
|
+
let rTok: string | undefined;
|
|
3656
|
+
try {
|
|
3657
|
+
rTok =
|
|
3658
|
+
(await page.evaluate(() => localStorage.getItem("refresh_token") || "")).trim() ||
|
|
3659
|
+
undefined;
|
|
3660
|
+
} catch {}
|
|
3613
3661
|
saveAuthSession(accountId, {
|
|
3614
3662
|
cookie: cookieStr,
|
|
3615
3663
|
userAgent: cache.headers["user-agent"] || "",
|
|
@@ -3621,6 +3669,7 @@ async function refreshHeadersInternal(
|
|
|
3621
3669
|
secChUaPlatform: cache.headers["sec-ch-ua-platform"] || undefined,
|
|
3622
3670
|
version: cache.headers["version"] || undefined,
|
|
3623
3671
|
tokenExpiresAt: exp || undefined,
|
|
3672
|
+
refreshToken: rTok,
|
|
3624
3673
|
capturedAt: Date.now(),
|
|
3625
3674
|
});
|
|
3626
3675
|
} catch {}
|
|
@@ -4169,8 +4218,10 @@ export async function keepAlivePlaywrightAccount(
|
|
|
4169
4218
|
const currentUrl = page.url();
|
|
4170
4219
|
|
|
4171
4220
|
// Proactive session check: if token expires within 5 minutes, refresh in-page silently using refreshAccountToken
|
|
4221
|
+
const lastRefreshAttempt = lastProactiveTokenRefresh.get(accountId) ?? 0;
|
|
4172
4222
|
const cookie = await getCookies(accountId);
|
|
4173
|
-
if (cookie && isTokenExpiringSoon(cookie, 5)) {
|
|
4223
|
+
if (cookie && isTokenExpiringSoon(cookie, 5) && now - lastRefreshAttempt > 60_000) {
|
|
4224
|
+
lastProactiveTokenRefresh.set(accountId, now);
|
|
4174
4225
|
console.log(
|
|
4175
4226
|
`💓 [SessionKeeper] Account ${accountId} access token expiring soon (<5m); silently refreshing...`,
|
|
4176
4227
|
);
|
|
@@ -4233,6 +4284,7 @@ function cleanupPlaywrightAccountState(accountId: string): void {
|
|
|
4233
4284
|
cookieCaches.delete(accountId);
|
|
4234
4285
|
lastAccountActivity.delete(accountId);
|
|
4235
4286
|
lastKeepAliveNavigation.delete(accountId);
|
|
4287
|
+
lastProactiveTokenRefresh.delete(accountId);
|
|
4236
4288
|
clearFingerprintCache(accountId);
|
|
4237
4289
|
// The account's context died/closed — its captured headers are stale or the
|
|
4238
4290
|
// page is gone, so it must not be selected by the rotation gate until a
|
package/src/services/qwen.ts
CHANGED
|
@@ -1540,6 +1540,12 @@ async function createQwenBrowserResponse(
|
|
|
1540
1540
|
}
|
|
1541
1541
|
}
|
|
1542
1542
|
|
|
1543
|
+
const inFlightPersonalizationSyncs = new Map<string, Promise<boolean>>();
|
|
1544
|
+
|
|
1545
|
+
export function getInFlightPersonalizationCount(): number {
|
|
1546
|
+
return inFlightPersonalizationSyncs.size;
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1543
1549
|
export async function syncQwenRequestPersonalization(
|
|
1544
1550
|
instruction: string,
|
|
1545
1551
|
accountId?: string,
|
|
@@ -1552,15 +1558,67 @@ export async function syncQwenRequestPersonalization(
|
|
|
1552
1558
|
forceSync?: boolean;
|
|
1553
1559
|
} = {},
|
|
1554
1560
|
): Promise<boolean> {
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1561
|
+
const cacheKey = accountId || "global";
|
|
1562
|
+
const sent = textSize(instruction);
|
|
1563
|
+
const syncHash = sent.hash ? `${sent.hash}:${QWEN_SAFE_SETTINGS_HASH}` : null;
|
|
1564
|
+
const inFlightKey = `${cacheKey}:${syncHash || "empty"}`;
|
|
1565
|
+
|
|
1566
|
+
const existingInFlight = inFlightPersonalizationSyncs.get(inFlightKey);
|
|
1567
|
+
if (existingInFlight) {
|
|
1568
|
+
logger.debug("[Qwen] Personalization sync already in flight, sharing promise", {
|
|
1569
|
+
accountId: cacheKey,
|
|
1570
|
+
});
|
|
1571
|
+
return existingInFlight;
|
|
1560
1572
|
}
|
|
1561
|
-
// instruction pode ser vazia para limpar personalization
|
|
1562
1573
|
|
|
1563
|
-
|
|
1574
|
+
let syncPromise!: Promise<boolean>;
|
|
1575
|
+
syncPromise = (async () => {
|
|
1576
|
+
await Promise.resolve();
|
|
1577
|
+
try {
|
|
1578
|
+
if (isAuthMockEnabled()) {
|
|
1579
|
+
const delay = parseInt(process.env.TEST_PERSONALIZATION_DELAY_MS || "0", 10);
|
|
1580
|
+
if (delay > 0) {
|
|
1581
|
+
await sleep(delay);
|
|
1582
|
+
}
|
|
1583
|
+
// Test hook: force the sync to report "not applied" so the fail-fast
|
|
1584
|
+
// contract (personalization-required suite) is exercisable in mock mode.
|
|
1585
|
+
if (process.env.TEST_PERSONALIZATION_SYNC_FAIL === "true") return false;
|
|
1586
|
+
return true;
|
|
1587
|
+
}
|
|
1588
|
+
return await syncQwenRequestPersonalizationInternal(
|
|
1589
|
+
instruction,
|
|
1590
|
+
accountId,
|
|
1591
|
+
metadata,
|
|
1592
|
+
cacheKey,
|
|
1593
|
+
sent,
|
|
1594
|
+
syncHash,
|
|
1595
|
+
);
|
|
1596
|
+
} finally {
|
|
1597
|
+
if (inFlightPersonalizationSyncs.get(inFlightKey) === syncPromise) {
|
|
1598
|
+
inFlightPersonalizationSyncs.delete(inFlightKey);
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
})();
|
|
1602
|
+
|
|
1603
|
+
inFlightPersonalizationSyncs.set(inFlightKey, syncPromise);
|
|
1604
|
+
return syncPromise;
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
async function syncQwenRequestPersonalizationInternal(
|
|
1608
|
+
instruction: string,
|
|
1609
|
+
accountId: string | undefined,
|
|
1610
|
+
metadata: {
|
|
1611
|
+
model?: string;
|
|
1612
|
+
toolsCount?: number;
|
|
1613
|
+
sessionId?: string | null;
|
|
1614
|
+
promptChars?: number;
|
|
1615
|
+
forceSync?: boolean;
|
|
1616
|
+
},
|
|
1617
|
+
cacheKey: string,
|
|
1618
|
+
sent: ReturnType<typeof textSize>,
|
|
1619
|
+
syncHash: string | null,
|
|
1620
|
+
): Promise<boolean> {
|
|
1621
|
+
// instruction pode ser vazia para limpar personalization
|
|
1564
1622
|
|
|
1565
1623
|
// Proactive token renewal: refresh BEFORE attempting personalization
|
|
1566
1624
|
// to avoid 401 errors that waste time on retry
|
|
@@ -1584,8 +1642,6 @@ export async function syncQwenRequestPersonalization(
|
|
|
1584
1642
|
let currentSettings: any = null;
|
|
1585
1643
|
let payload = buildQwenSettingsUpdatePayload(currentSettings, instruction);
|
|
1586
1644
|
|
|
1587
|
-
const sent = textSize(instruction);
|
|
1588
|
-
const syncHash = sent.hash ? `${sent.hash}:${QWEN_SAFE_SETTINGS_HASH}` : null;
|
|
1589
1645
|
const bypassCache = metadata.forceSync === true;
|
|
1590
1646
|
|
|
1591
1647
|
// 1. Check memory cache (skipped on forceSync)
|
|
@@ -2502,15 +2558,23 @@ function parseQwenJsonError(
|
|
|
2502
2558
|
|
|
2503
2559
|
if (errorJson?.success === false) {
|
|
2504
2560
|
const code = errorJson.data?.code || errorJson.code || "UpstreamError";
|
|
2561
|
+
const detailsStr = typeof details === "string" ? details : "";
|
|
2562
|
+
const codeStr = String(code).toLowerCase();
|
|
2563
|
+
const detailsLower = detailsStr.toLowerCase();
|
|
2505
2564
|
|
|
2506
|
-
|
|
2565
|
+
const isAuthExpired =
|
|
2507
2566
|
status === 401 ||
|
|
2508
|
-
|
|
2509
|
-
(
|
|
2510
|
-
|
|
2511
|
-
|
|
2567
|
+
codeStr.includes("unauthorized") ||
|
|
2568
|
+
detailsLower.includes("token has expired") ||
|
|
2569
|
+
detailsLower.includes("log in") ||
|
|
2570
|
+
detailsLower.includes("login") ||
|
|
2571
|
+
detailsLower.includes("session") ||
|
|
2572
|
+
detailsLower.includes("não autorizado") ||
|
|
2573
|
+
detailsLower.includes("unauthorized");
|
|
2574
|
+
|
|
2575
|
+
if (isAuthExpired) {
|
|
2512
2576
|
return new QwenSessionExpiredError(
|
|
2513
|
-
`Session expired: ${
|
|
2577
|
+
`Session expired: ${detailsStr || codeStr}`,
|
|
2514
2578
|
accountId || "global",
|
|
2515
2579
|
);
|
|
2516
2580
|
}
|
|
@@ -3301,8 +3365,29 @@ async function createQwenStreamInternal(
|
|
|
3301
3365
|
);
|
|
3302
3366
|
}
|
|
3303
3367
|
|
|
3368
|
+
const parsedJsonErr = parseQwenJsonError(errText, response.status, accountId);
|
|
3369
|
+
if (
|
|
3370
|
+
parsedJsonErr instanceof QwenSessionExpiredError &&
|
|
3371
|
+
accountId &&
|
|
3372
|
+
!retriedNonSseResponse
|
|
3373
|
+
) {
|
|
3374
|
+
retriedNonSseResponse = true;
|
|
3375
|
+
logger.warn(
|
|
3376
|
+
"[Qwen] Token expired on completion fetch (200 JSON); refreshing session and retrying",
|
|
3377
|
+
{ accountId, chatId: chatSessionId ?? "new" },
|
|
3378
|
+
);
|
|
3379
|
+
const { refreshAccountToken } = await import("./playwright.ts");
|
|
3380
|
+
const refreshed = await refreshAccountToken(accountId);
|
|
3381
|
+
if (refreshed.success) {
|
|
3382
|
+
const fresh = await getQwenHeaders(false, accountId);
|
|
3383
|
+
activeHeaders = fresh.headers;
|
|
3384
|
+
response = await fetchCompletion(activeHeaders);
|
|
3385
|
+
continue;
|
|
3386
|
+
}
|
|
3387
|
+
}
|
|
3388
|
+
|
|
3304
3389
|
throw withCreatedChatMetadata(
|
|
3305
|
-
|
|
3390
|
+
parsedJsonErr ??
|
|
3306
3391
|
new QwenUpstreamError(
|
|
3307
3392
|
`Qwen returned non-stream JSON response: ${errText.substring(0, 300)}`,
|
|
3308
3393
|
"NonStreamJsonResponse",
|
|
@@ -3378,6 +3463,35 @@ async function createQwenStreamInternal(
|
|
|
3378
3463
|
response.status,
|
|
3379
3464
|
accountId,
|
|
3380
3465
|
);
|
|
3466
|
+
if (
|
|
3467
|
+
parsedError instanceof QwenSessionExpiredError &&
|
|
3468
|
+
accountId &&
|
|
3469
|
+
!retriedNonSseResponse
|
|
3470
|
+
) {
|
|
3471
|
+
retriedNonSseResponse = true;
|
|
3472
|
+
logger.warn(
|
|
3473
|
+
"[Qwen] Token expired on completion fetch (!response.ok); refreshing session and retrying",
|
|
3474
|
+
{ accountId, chatId: chatSessionId ?? "new", status: response.status },
|
|
3475
|
+
);
|
|
3476
|
+
const { refreshAccountToken } = await import("./playwright.ts");
|
|
3477
|
+
const refreshed = await refreshAccountToken(accountId);
|
|
3478
|
+
if (refreshed.success) {
|
|
3479
|
+
const fresh = await getQwenHeaders(false, accountId);
|
|
3480
|
+
activeHeaders = fresh.headers;
|
|
3481
|
+
response = await fetchCompletion(activeHeaders);
|
|
3482
|
+
if (response.ok && response.body) {
|
|
3483
|
+
return {
|
|
3484
|
+
stream: wrapUpstreamStream(response.body, controller),
|
|
3485
|
+
headers: activeHeaders,
|
|
3486
|
+
uiSessionId: chatSessionId || "",
|
|
3487
|
+
controller,
|
|
3488
|
+
accountId: accountId ?? "global",
|
|
3489
|
+
createdNewChat,
|
|
3490
|
+
tokenEstimationContext,
|
|
3491
|
+
};
|
|
3492
|
+
}
|
|
3493
|
+
}
|
|
3494
|
+
}
|
|
3381
3495
|
if (parsedError) {
|
|
3382
3496
|
throw withCreatedChatMetadata(parsedError);
|
|
3383
3497
|
}
|
package/src/tui/proxy-client.ts
CHANGED
|
@@ -15,6 +15,10 @@ import { isPlaywrightInitialized } from "../services/playwright.ts";
|
|
|
15
15
|
import { getAccountConcurrencySnapshot } from "../core/account-concurrency.ts";
|
|
16
16
|
import { getRssUsageSnapshot } from "../core/memory-usage.ts";
|
|
17
17
|
import { metrics } from "../core/metrics.ts";
|
|
18
|
+
import {
|
|
19
|
+
getAllAccountsDailyUsage,
|
|
20
|
+
getPoolDailyUsageSummary,
|
|
21
|
+
} from "../core/daily-usage.ts";
|
|
18
22
|
import type { ProxyStatusSnapshot } from "./types.ts";
|
|
19
23
|
|
|
20
24
|
export function maskAccountIdentifier(idOrEmail: string): string {
|
|
@@ -153,13 +157,18 @@ export async function fetchProxyStatus(): Promise<ProxyStatusSnapshot> {
|
|
|
153
157
|
}
|
|
154
158
|
} catch {}
|
|
155
159
|
|
|
156
|
-
// Attach concurrency to accounts
|
|
160
|
+
// Attach concurrency and daily usage to accounts
|
|
161
|
+
const dailyUsageMap = getAllAccountsDailyUsage();
|
|
157
162
|
const accounts = cachedAccounts.map((acc) => {
|
|
158
163
|
const concurrency = concurrencyMap.get(acc.id);
|
|
164
|
+
const daily = dailyUsageMap.get(acc.id);
|
|
159
165
|
return {
|
|
160
166
|
...acc,
|
|
161
167
|
activeStreams: concurrency?.active ?? 0,
|
|
162
168
|
streamLimit: concurrency?.limit ?? config.concurrency.maxStreamsPerAccount,
|
|
169
|
+
dailyTokens: daily?.completionTokens ?? 0,
|
|
170
|
+
dailyUsagePercent: daily?.usagePercent ?? 0,
|
|
171
|
+
dailyTurns: daily?.turnsCount ?? 0,
|
|
163
172
|
};
|
|
164
173
|
});
|
|
165
174
|
// RAM usage
|
|
@@ -220,6 +229,7 @@ export async function fetchProxyStatus(): Promise<ProxyStatusSnapshot> {
|
|
|
220
229
|
cacheHitRatio,
|
|
221
230
|
cacheBytesSaved,
|
|
222
231
|
},
|
|
232
|
+
dailyUsage: getPoolDailyUsageSummary(cachedAccounts.map((a) => a.id)),
|
|
223
233
|
accounts,
|
|
224
234
|
};
|
|
225
235
|
}
|
package/src/tui/types.ts
CHANGED
|
@@ -38,6 +38,14 @@ export interface ProxyStatusSnapshot {
|
|
|
38
38
|
cacheHitRatio?: number;
|
|
39
39
|
cacheBytesSaved?: number;
|
|
40
40
|
};
|
|
41
|
+
dailyUsage?: {
|
|
42
|
+
poolUsagePercent: number;
|
|
43
|
+
totalPromptTokens: number;
|
|
44
|
+
totalCompletionTokens: number;
|
|
45
|
+
totalTokens: number;
|
|
46
|
+
totalTurns: number;
|
|
47
|
+
poolCapacityTokens: number;
|
|
48
|
+
};
|
|
41
49
|
accounts: Array<{
|
|
42
50
|
id: string;
|
|
43
51
|
emailOrName: string;
|
|
@@ -50,5 +58,8 @@ export interface ProxyStatusSnapshot {
|
|
|
50
58
|
isInitialized?: boolean;
|
|
51
59
|
activeStreams?: number;
|
|
52
60
|
streamLimit?: number;
|
|
61
|
+
dailyTokens?: number;
|
|
62
|
+
dailyUsagePercent?: number;
|
|
63
|
+
dailyTurns?: number;
|
|
53
64
|
}>;
|
|
54
65
|
}
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
} from "../../core/accounts.ts";
|
|
20
20
|
import { ServerManager } from "../server-manager.ts";
|
|
21
21
|
import { config } from "../../core/config.ts";
|
|
22
|
+
import { renderProgressBar } from "./status-view.ts";
|
|
22
23
|
export function formatCooldownReason(reason?: string | null, maxLen = 28): string {
|
|
23
24
|
if (!reason) return theme.yellow("Cooldown ativo");
|
|
24
25
|
if (
|
|
@@ -938,9 +939,11 @@ export class AccountsView implements TuiView {
|
|
|
938
939
|
}
|
|
939
940
|
|
|
940
941
|
// Left Panel: Accounts List Table
|
|
942
|
+
const showUsageCol = leftW >= 48;
|
|
943
|
+
const headerUsage = showUsageCol ? "Uso% " : "";
|
|
941
944
|
const leftContent: string[] = [
|
|
942
945
|
"",
|
|
943
|
-
` ${theme.dim(
|
|
946
|
+
` ${theme.dim(`# Conta ${headerUsage}Status`)}`,
|
|
944
947
|
` ${theme.dim("───────────────────────────────────────")}`,
|
|
945
948
|
];
|
|
946
949
|
|
|
@@ -997,7 +1000,14 @@ export class AccountsView implements TuiView {
|
|
|
997
1000
|
: theme.muted(`○ Standby `);
|
|
998
1001
|
}
|
|
999
1002
|
|
|
1000
|
-
const
|
|
1003
|
+
const usagePct = acc.dailyUsagePercent ?? 0;
|
|
1004
|
+
const usagePctStr = showUsageCol
|
|
1005
|
+
? (usagePct > 0
|
|
1006
|
+
? (usagePct >= 80 ? theme.red(pad(`${usagePct}%`, 5)) : usagePct >= 40 ? theme.yellow(pad(`${usagePct}%`, 5)) : theme.cyan(pad(`${usagePct}%`, 5)))
|
|
1007
|
+
: theme.dim(pad("0%", 5))) + " "
|
|
1008
|
+
: "";
|
|
1009
|
+
|
|
1010
|
+
const line = `${pointer}${num}${name}${usagePctStr}${status}`;
|
|
1001
1011
|
if (isHovered) {
|
|
1002
1012
|
leftContent.push(theme.bgHover(line));
|
|
1003
1013
|
} else if (isFocused) {
|
|
@@ -1046,6 +1056,13 @@ export class AccountsView implements TuiView {
|
|
|
1046
1056
|
rightContent.push(` ${theme.bold("Sistema ID:")} ${theme.muted(selected.id.slice(0, 14))}`);
|
|
1047
1057
|
rightContent.push(` ${theme.bold("Nível:")} ${selected.priority}`);
|
|
1048
1058
|
|
|
1059
|
+
const usagePct = selected.dailyUsagePercent ?? 0;
|
|
1060
|
+
const usageTokens = selected.dailyTokens ?? 0;
|
|
1061
|
+
const usageTurns = selected.dailyTurns ?? 0;
|
|
1062
|
+
const usageColor = usagePct >= 80 ? theme.red : usagePct >= 40 ? theme.yellow : theme.cyan;
|
|
1063
|
+
const usageBar = renderProgressBar(usagePct, 6, usageColor);
|
|
1064
|
+
rightContent.push(` ${theme.bold("Cota Hoje:")} [${usageBar}] ${usageColor(`${usagePct}%`)} ${theme.dim(`(~${Math.round(usageTokens / 1000)}k / ${usageTurns} reqs)`)}`);
|
|
1065
|
+
|
|
1049
1066
|
const cdStatus = selected.onCooldown
|
|
1050
1067
|
? theme.yellow(`[!] Cooldown ${Math.round(selected.remainingCooldownMs / 60000)}m`)
|
|
1051
1068
|
: theme.green(`${glyphs.check} Disponível`);
|
|
@@ -846,12 +846,29 @@ export class StatusView implements TuiView {
|
|
|
846
846
|
const poolPct = accounts.length > 0 ? Math.round((availableCount / accounts.length) * 100) : 0;
|
|
847
847
|
const poolColor = poolPct >= 70 ? theme.green : poolPct >= 40 ? theme.yellow : theme.red;
|
|
848
848
|
const poolBar = renderProgressBar(poolPct, 8, poolColor);
|
|
849
|
+
const poolDailyUsage = data?.dailyUsage;
|
|
850
|
+
const poolUsagePct = poolDailyUsage?.poolUsagePercent ?? 0;
|
|
851
|
+
const poolUsageColor = poolUsagePct >= 80 ? theme.red : poolUsagePct >= 40 ? theme.yellow : theme.cyan;
|
|
852
|
+
const poolUsageBar = renderProgressBar(poolUsagePct, 8, poolUsageColor);
|
|
849
853
|
const innerRightW = Math.max(30, rightW - 2);
|
|
850
|
-
const
|
|
854
|
+
const showUsageCol = innerRightW >= 52;
|
|
855
|
+
const emailWidth = showUsageCol
|
|
856
|
+
? Math.max(14, Math.min(18, innerRightW - 33))
|
|
857
|
+
: Math.max(16, Math.min(20, innerRightW - 27));
|
|
858
|
+
|
|
851
859
|
const rightContent: string[] = [
|
|
852
860
|
` ${theme.bold("Disponibilidade:")} [${poolBar}] ${poolColor(`${availableCount}/${accounts.length} (${poolPct}%)`)}`,
|
|
861
|
+
...(poolDailyUsage
|
|
862
|
+
? [
|
|
863
|
+
` ${theme.bold("Cota Pool Hoje:")} [${poolUsageBar}] ${poolUsageColor(
|
|
864
|
+
`${poolUsagePct}%`,
|
|
865
|
+
)} ${theme.dim(
|
|
866
|
+
`(~${Math.round(poolDailyUsage.totalTokens / 1000)}k tok)`,
|
|
867
|
+
)}`,
|
|
868
|
+
]
|
|
869
|
+
: []),
|
|
853
870
|
` ${theme.dim("─".repeat(Math.max(32, innerRightW - 2)))}`,
|
|
854
|
-
` ${theme.dim(`# ${pad("Conta", emailWidth)} Carga Status`)}`,
|
|
871
|
+
` ${theme.dim(`# ${pad("Conta", emailWidth)} Carga ${showUsageCol ? "Uso% " : ""}Status`)}`,
|
|
855
872
|
` ${theme.dim("─".repeat(Math.max(32, innerRightW - 2)))}`,
|
|
856
873
|
];
|
|
857
874
|
|
|
@@ -864,7 +881,23 @@ export class StatusView implements TuiView {
|
|
|
864
881
|
const name = pad(truncate(acc.emailOrName, emailWidth - 1), emailWidth);
|
|
865
882
|
const active = acc.activeStreams || 0;
|
|
866
883
|
const limit = acc.streamLimit || 1;
|
|
867
|
-
const
|
|
884
|
+
const usagePct = acc.dailyUsagePercent ?? 0;
|
|
885
|
+
|
|
886
|
+
let loadBadge: string;
|
|
887
|
+
if (active > 0) {
|
|
888
|
+
loadBadge = theme.yellow(`[${active}/${limit}]`);
|
|
889
|
+
} else if (!showUsageCol && usagePct > 0) {
|
|
890
|
+
const pctColor = usagePct >= 80 ? theme.red : usagePct >= 40 ? theme.yellow : theme.cyan;
|
|
891
|
+
loadBadge = pctColor(pad(`[${usagePct}%]`, 5));
|
|
892
|
+
} else {
|
|
893
|
+
loadBadge = theme.dim(`[0/${limit}]`);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
const usageCol = showUsageCol
|
|
897
|
+
? (usagePct > 0
|
|
898
|
+
? (usagePct >= 80 ? theme.red(pad(`${usagePct}%`, 5)) : usagePct >= 40 ? theme.yellow(pad(`${usagePct}%`, 5)) : theme.cyan(pad(`${usagePct}%`, 5)))
|
|
899
|
+
: theme.dim(pad("0%", 5))) + " "
|
|
900
|
+
: "";
|
|
868
901
|
|
|
869
902
|
let status = theme.green(`${glyphs.bullet} Pronto`);
|
|
870
903
|
if (active > 0 && !acc.onCooldown && acc.headersReady) {
|
|
@@ -888,7 +921,7 @@ export class StatusView implements TuiView {
|
|
|
888
921
|
? theme.yellow(`◐ Aquecendo...`)
|
|
889
922
|
: theme.muted(`○ Standby`);
|
|
890
923
|
}
|
|
891
|
-
rightContent.push(` ${num}${name} ${loadBadge} ${status}`);
|
|
924
|
+
rightContent.push(` ${num}${name} ${loadBadge} ${usageCol}${status}`);
|
|
892
925
|
});
|
|
893
926
|
}
|
|
894
927
|
|