qwenproxy-cli 1.5.3 β 1.6.1
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 +252 -26
- package/dist/delete-chats.js +206 -37
- package/dist/index.js +548 -74
- package/dist/login.js +144 -28
- package/dist/reset-cooldowns.js +252 -26
- package/dist/sync-clients.js +2 -2
- package/package.json +3 -3
- package/src/api/server.ts +1 -0
- package/src/clean-cache.ts +3 -0
- package/src/core/config.ts +2 -2
- package/src/core/daily-usage.ts +276 -0
- package/src/core/database.ts +27 -1
- package/src/core/process-guard.ts +77 -0
- package/src/core/version.ts +1 -1
- package/src/delete-chats.ts +3 -0
- package/src/index.ts +3 -29
- package/src/login.ts +2 -0
- package/src/routes/chat/account.ts +33 -5
- package/src/routes/chat/index.ts +1 -1
- package/src/routes/chat/retry-policy.ts +30 -0
- package/src/routes/chat/streaming.ts +72 -4
- package/src/services/captcha-coordinator.ts +12 -7
- package/src/services/chat-cleanup.ts +5 -0
- package/src/services/playwright.ts +82 -26
- package/src/services/qwen.ts +65 -9
- 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
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
setToolCapNotice,
|
|
19
19
|
} from "../../services/qwen.ts";
|
|
20
20
|
import { acquireUpstreamStream } from "./account.ts";
|
|
21
|
+
import { loadAccounts } from "../../core/accounts.ts";
|
|
21
22
|
import { markAccountRateLimited, computeQuotaCooldownMs } from "../../core/account-manager.ts";
|
|
22
23
|
import {
|
|
23
24
|
clearTemporaryBusy,
|
|
@@ -60,6 +61,7 @@ import {
|
|
|
60
61
|
logTokenEstimationSample,
|
|
61
62
|
type TokenEstimationContext,
|
|
62
63
|
} from "../../services/token-estimation-metrics.ts";
|
|
64
|
+
import { recordTurnUsage } from "../../core/daily-usage.ts";
|
|
63
65
|
import {
|
|
64
66
|
enrichUsageWithContextMeter,
|
|
65
67
|
getContextMeterHeaders,
|
|
@@ -781,6 +783,10 @@ export async function processNonStreamingResponse(
|
|
|
781
783
|
context: currentTokenEstimationContext,
|
|
782
784
|
});
|
|
783
785
|
|
|
786
|
+
if (activeAccountId && usage) {
|
|
787
|
+
recordTurnUsage(activeAccountId, usage, body.model);
|
|
788
|
+
}
|
|
789
|
+
|
|
784
790
|
// The response was fully processed: persist the next-turn parent.
|
|
785
791
|
if (pendingParentId) {
|
|
786
792
|
rememberParent(pendingParentId);
|
|
@@ -900,6 +906,9 @@ export async function processStreamingResponse(
|
|
|
900
906
|
let activeReader: ReadableStreamDefaultReader<Uint8Array> | null = null;
|
|
901
907
|
let invalidInputSameAccountRetries = 0;
|
|
902
908
|
let chatInProgressSameAccountRetries = 0;
|
|
909
|
+
let antiBotSameAccountRetries = 0;
|
|
910
|
+
const totalConfiguredAccounts = loadAccounts().length;
|
|
911
|
+
let quotaRotationsLeft = Math.min(Math.max(1, totalConfiguredAccounts - 1), 6);
|
|
903
912
|
// Set when the terminal [DONE] reached the client. The `| recovered`
|
|
904
913
|
// suffix on Stream done must only appear for attempts that COMPLETED after
|
|
905
914
|
// a mid-stream retry, not for failed attempts that consumed retries.
|
|
@@ -1372,7 +1381,6 @@ export async function processStreamingResponse(
|
|
|
1372
1381
|
!normalizedError ||
|
|
1373
1382
|
(clientDisconnected && !gracePending) ||
|
|
1374
1383
|
c.req.raw.signal.aborted ||
|
|
1375
|
-
retryContext.retriesLeft <= 0 ||
|
|
1376
1384
|
!midStreamRetry ||
|
|
1377
1385
|
emittedModelOutput
|
|
1378
1386
|
) {
|
|
@@ -1384,6 +1392,19 @@ export async function processStreamingResponse(
|
|
|
1384
1392
|
});
|
|
1385
1393
|
if (!policy.retryable) return false;
|
|
1386
1394
|
|
|
1395
|
+
const isRealQuotaError =
|
|
1396
|
+
policy.reason === "quota_or_rate_limit" &&
|
|
1397
|
+
(policy.accountCooldownReason === "RateLimited" ||
|
|
1398
|
+
policy.accountCooldownReason === "QuotaExceeded");
|
|
1399
|
+
|
|
1400
|
+
const hasBudget = isRealQuotaError
|
|
1401
|
+
? quotaRotationsLeft > 0
|
|
1402
|
+
: retryContext.retriesLeft > 0;
|
|
1403
|
+
|
|
1404
|
+
if (!hasBudget) {
|
|
1405
|
+
return false;
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1387
1408
|
// Full recovery decision β same rationale as the create-path policy
|
|
1388
1409
|
// log: the failure line shows the error, this line shows WHY the
|
|
1389
1410
|
// chosen action (retry same / switch / new chat / cooldown) was taken.
|
|
@@ -1397,9 +1418,13 @@ export async function processStreamingResponse(
|
|
|
1397
1418
|
invalidateLogicalThreadParent(midStreamRetry.sessionId);
|
|
1398
1419
|
}
|
|
1399
1420
|
|
|
1400
|
-
|
|
1421
|
+
if (isRealQuotaError) {
|
|
1422
|
+
quotaRotationsLeft--;
|
|
1423
|
+
} else {
|
|
1424
|
+
retryContext.retriesLeft--;
|
|
1425
|
+
}
|
|
1401
1426
|
console.warn(
|
|
1402
|
-
`π [Chat] Stream recovery | account=${currentAccountId} | reason=${policy.reason} | error=${normalizedError.message.substring(0, 150)} | retries_left=${retryContext.retriesLeft}`,
|
|
1427
|
+
`π [Chat] Stream recovery | account=${currentAccountId} | reason=${policy.reason} | error=${normalizedError.message.substring(0, 150)} | retries_left=${isRealQuotaError ? quotaRotationsLeft : retryContext.retriesLeft}`,
|
|
1403
1428
|
);
|
|
1404
1429
|
|
|
1405
1430
|
const retryInvalidInputOnSameAccount =
|
|
@@ -1432,9 +1457,19 @@ export async function processStreamingResponse(
|
|
|
1432
1457
|
);
|
|
1433
1458
|
return false;
|
|
1434
1459
|
}
|
|
1435
|
-
|
|
1460
|
+
let switchAccount =
|
|
1436
1461
|
policy.switchAccount && !retryInvalidInputOnSameAccount;
|
|
1437
1462
|
|
|
1463
|
+
if (policy.reason === "anti_bot") {
|
|
1464
|
+
if (antiBotSameAccountRetries === 0) {
|
|
1465
|
+
antiBotSameAccountRetries++;
|
|
1466
|
+
switchAccount = false;
|
|
1467
|
+
} else {
|
|
1468
|
+
switchAccount = true;
|
|
1469
|
+
markAccountRateLimited(currentAccountId, 60_000, "AntiBotWafChallenge");
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1438
1473
|
if (
|
|
1439
1474
|
switchAccount &&
|
|
1440
1475
|
(policy.accountCooldownMs ?? 0) > 0
|
|
@@ -1452,6 +1487,35 @@ export async function processStreamingResponse(
|
|
|
1452
1487
|
.catch(() => {});
|
|
1453
1488
|
}
|
|
1454
1489
|
|
|
1490
|
+
// Preventative stop: signal Qwen to stop generating on the backend before switching
|
|
1491
|
+
if (currentUiSessionId && targetResponseId) {
|
|
1492
|
+
const streamData = getStream(completionId);
|
|
1493
|
+
const stopHeaders = streamData?.headers;
|
|
1494
|
+
if (stopHeaders?.cookie && stopHeaders["user-agent"]) {
|
|
1495
|
+
void requestQwenTextInBrowser(
|
|
1496
|
+
currentAccountId,
|
|
1497
|
+
"POST",
|
|
1498
|
+
`/api/v2/chat/completions/stop?chat_id=${encodeURIComponent(currentUiSessionId)}`,
|
|
1499
|
+
buildQwenRequestHeaders({
|
|
1500
|
+
cookie: stopHeaders.cookie,
|
|
1501
|
+
userAgent: stopHeaders["user-agent"],
|
|
1502
|
+
bxUa: stopHeaders["bx-ua"],
|
|
1503
|
+
bxUmidtoken: stopHeaders["bx-umidtoken"],
|
|
1504
|
+
bxV: stopHeaders["bx-v"],
|
|
1505
|
+
chatSessionId: currentUiSessionId,
|
|
1506
|
+
}),
|
|
1507
|
+
JSON.stringify({
|
|
1508
|
+
chat_id: currentUiSessionId,
|
|
1509
|
+
response_id: targetResponseId,
|
|
1510
|
+
}),
|
|
1511
|
+
{
|
|
1512
|
+
referrer: qwenUrl(`/c/${encodeURIComponent(currentUiSessionId)}`),
|
|
1513
|
+
noMutexRecovery: true,
|
|
1514
|
+
},
|
|
1515
|
+
).catch(() => undefined);
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1455
1519
|
retryContext.releaseAccountLease?.();
|
|
1456
1520
|
retryContext.releaseAccountLease = null;
|
|
1457
1521
|
await reader.cancel().catch(() => undefined);
|
|
@@ -2566,6 +2630,10 @@ export async function processStreamingResponse(
|
|
|
2566
2630
|
mode: "stream",
|
|
2567
2631
|
context: currentTokenEstimationContext,
|
|
2568
2632
|
});
|
|
2633
|
+
|
|
2634
|
+
if (activeAccountId && usage) {
|
|
2635
|
+
recordTurnUsage(activeAccountId, usage, body.model);
|
|
2636
|
+
}
|
|
2569
2637
|
} else {
|
|
2570
2638
|
if (isToolcallDebugEnabled()) {
|
|
2571
2639
|
logger.debug(
|
|
@@ -21,6 +21,11 @@ const CHALLENGE_PATH_MARKER = "_____tmd_____";
|
|
|
21
21
|
*/
|
|
22
22
|
const FAILED_RECOVERY_BACKOFF_MS = 30_000;
|
|
23
23
|
const lastFailedRecoveryAt = new Map<string, number>();
|
|
24
|
+
const activeCaptchaRecoveries = new Set<string>();
|
|
25
|
+
|
|
26
|
+
export function isCaptchaRecoveryActive(accountId: string): boolean {
|
|
27
|
+
return activeCaptchaRecoveries.has(accountId);
|
|
28
|
+
}
|
|
24
29
|
|
|
25
30
|
async function gotoBestEffort(page: Page, url: string): Promise<void> {
|
|
26
31
|
// A WAF-blocked navigation can time out while still having rendered the
|
|
@@ -105,13 +110,6 @@ export async function recoverBaxiaCaptcha(
|
|
|
105
110
|
|
|
106
111
|
metrics.increment("captcha.challenges.detected", 1, { solver });
|
|
107
112
|
|
|
108
|
-
const challengeUrl = options.challengeBody
|
|
109
|
-
? extractBaxiaChallengeUrl(options.challengeBody, config.qwen.baseUrl)
|
|
110
|
-
: null;
|
|
111
|
-
|
|
112
|
-
// The slider itself waits up to 5s for each attempt. Keep the page
|
|
113
|
-
// operation alive for the full bounded solver budget so a slow challenge
|
|
114
|
-
// cannot be mistaken for a stuck browser. Do not clamp to timeouts.page.
|
|
115
113
|
// Two navigations (open the challenge, return to the chat page) are part of
|
|
116
114
|
// the recovery, so their budget belongs in the same total.
|
|
117
115
|
const solverOperationTimeoutMs = Math.max(
|
|
@@ -122,6 +120,11 @@ export async function recoverBaxiaCaptcha(
|
|
|
122
120
|
2 * CHALLENGE_NAVIGATION_TIMEOUT_MS,
|
|
123
121
|
);
|
|
124
122
|
|
|
123
|
+
const challengeUrl = options.challengeBody
|
|
124
|
+
? extractBaxiaChallengeUrl(options.challengeBody, config.qwen.baseUrl)
|
|
125
|
+
: null;
|
|
126
|
+
|
|
127
|
+
activeCaptchaRecoveries.add(accountId);
|
|
125
128
|
try {
|
|
126
129
|
// recoverOnTimeout: false ensures that if the captcha solve times out,
|
|
127
130
|
// withAccountPage does NOT destructively kill the browser context, which
|
|
@@ -160,5 +163,7 @@ export async function recoverBaxiaCaptcha(
|
|
|
160
163
|
detail,
|
|
161
164
|
});
|
|
162
165
|
return false;
|
|
166
|
+
} finally {
|
|
167
|
+
activeCaptchaRecoveries.delete(accountId);
|
|
163
168
|
}
|
|
164
169
|
}
|
|
@@ -108,6 +108,11 @@ export async function deleteChatsForConfiguredAccounts(keepBrowserOpen = false):
|
|
|
108
108
|
`β [DeleteChats] [${currentIdx}/${totalCount}] Falha ao apagar conversas de ${maskEmail(account.email)}:`,
|
|
109
109
|
error instanceof Error ? error.message : String(error),
|
|
110
110
|
);
|
|
111
|
+
} finally {
|
|
112
|
+
if (!keepBrowserOpen) {
|
|
113
|
+
const { closePlaywrightForAccount } = await import("./playwright.ts");
|
|
114
|
+
await closePlaywrightForAccount(account.id).catch(() => {});
|
|
115
|
+
}
|
|
111
116
|
}
|
|
112
117
|
}
|
|
113
118
|
} finally {
|
|
@@ -287,7 +287,7 @@ export function loadStorageState(accountId: string): string | undefined {
|
|
|
287
287
|
export async function saveStorageState(
|
|
288
288
|
context: BrowserContext,
|
|
289
289
|
accountId: string,
|
|
290
|
-
timeoutMs =
|
|
290
|
+
timeoutMs = 10_000,
|
|
291
291
|
): Promise<void> {
|
|
292
292
|
try {
|
|
293
293
|
const stateFile = getStorageStatePath(accountId);
|
|
@@ -295,8 +295,12 @@ export async function saveStorageState(
|
|
|
295
295
|
if (!fs.existsSync(dir)) {
|
|
296
296
|
fs.mkdirSync(dir, { recursive: true });
|
|
297
297
|
}
|
|
298
|
+
const rawPromise = context.storageState({ path: stateFile }).catch(() => {
|
|
299
|
+
// Swallowed on context teardown to avoid unhandled rejection if withTimeout already fired
|
|
300
|
+
return undefined as any;
|
|
301
|
+
});
|
|
298
302
|
await withTimeout(
|
|
299
|
-
|
|
303
|
+
rawPromise,
|
|
300
304
|
timeoutMs,
|
|
301
305
|
`storageState timed out after ${timeoutMs}ms`,
|
|
302
306
|
);
|
|
@@ -512,6 +516,7 @@ const COOKIE_CACHE_TTL = 15 * 60 * 1000; // 15 minutes
|
|
|
512
516
|
const cookieCaches = new Map<string, { cookie: string; timestamp: number }>();
|
|
513
517
|
const lastAccountActivity = new Map<string, number>();
|
|
514
518
|
const lastKeepAliveNavigation = new Map<string, number>();
|
|
519
|
+
const lastProactiveTokenRefresh = new Map<string, number>();
|
|
515
520
|
const profileResetQueue = new Map<string, Promise<void>>();
|
|
516
521
|
let profileResetChain: Promise<void> = Promise.resolve();
|
|
517
522
|
let closingAllPlaywright = false;
|
|
@@ -1228,6 +1233,11 @@ export async function getBasicHeaders(accountId: string): Promise<{
|
|
|
1228
1233
|
throw new Error(`Playwright not initialized for account: ${accountId}`);
|
|
1229
1234
|
}
|
|
1230
1235
|
|
|
1236
|
+
const inFlightInit = inFlightAccountInits.get(accountId);
|
|
1237
|
+
if (inFlightInit) {
|
|
1238
|
+
await inFlightInit.catch(() => {});
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1231
1241
|
// Acquire mutex to prevent concurrent browser access
|
|
1232
1242
|
const release = await acquireAccountMutex(
|
|
1233
1243
|
accountId,
|
|
@@ -3068,6 +3078,12 @@ export async function captureQwenHeaders(
|
|
|
3068
3078
|
const { saveAuthSession } = await import("../core/database.ts");
|
|
3069
3079
|
const { parseJwtExpiry } = await import("../utils/jwt.ts");
|
|
3070
3080
|
const tokenExpiry = parseJwtExpiry(capturedHeaders.cookie);
|
|
3081
|
+
let rTok: string | undefined;
|
|
3082
|
+
try {
|
|
3083
|
+
rTok =
|
|
3084
|
+
(await page.evaluate(() => localStorage.getItem("refresh_token") || "")).trim() ||
|
|
3085
|
+
undefined;
|
|
3086
|
+
} catch {}
|
|
3071
3087
|
saveAuthSession(accountId, {
|
|
3072
3088
|
cookie: capturedHeaders.cookie,
|
|
3073
3089
|
userAgent: capturedHeaders["user-agent"],
|
|
@@ -3079,6 +3095,7 @@ export async function captureQwenHeaders(
|
|
|
3079
3095
|
secChUaPlatform: capturedHeaders["sec-ch-ua-platform"],
|
|
3080
3096
|
version: capturedHeaders["version"],
|
|
3081
3097
|
tokenExpiresAt: tokenExpiry || undefined,
|
|
3098
|
+
refreshToken: rTok,
|
|
3082
3099
|
capturedAt: Date.now(),
|
|
3083
3100
|
});
|
|
3084
3101
|
} catch {}
|
|
@@ -3121,40 +3138,61 @@ export async function captureQwenHeaders(
|
|
|
3121
3138
|
const isAuthUrl = currentUrl.includes("/auth") || currentUrl.includes("/login");
|
|
3122
3139
|
const isSuspectedGuest = isAuthUrl || (attempt >= 2 && !(await isPageLoggedIn(page, 5000)));
|
|
3123
3140
|
if (isSuspectedGuest) {
|
|
3124
|
-
|
|
3125
|
-
const
|
|
3126
|
-
if (
|
|
3127
|
-
console.
|
|
3128
|
-
|
|
3141
|
+
// Try fast silent token refresh first using the 30-day refresh_token in-page (~200ms)
|
|
3142
|
+
const silentRefresh = await refreshAccountToken(accountId);
|
|
3143
|
+
if (silentRefresh.success) {
|
|
3144
|
+
console.log(
|
|
3145
|
+
`β‘ [Playwright] Token refreshed silently during header capture for ${accountId}`,
|
|
3129
3146
|
);
|
|
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.
|
|
3147
|
+
if (isAuthUrl) {
|
|
3135
3148
|
await page.goto(qwenUrl("/"), {
|
|
3136
3149
|
waitUntil: "domcontentloaded",
|
|
3137
3150
|
timeout: Math.min(config.timeouts.navigation, timeoutMs),
|
|
3138
3151
|
});
|
|
3139
|
-
await sleep(
|
|
3152
|
+
await sleep(1500);
|
|
3153
|
+
}
|
|
3154
|
+
if (await isPageLoggedIn(page, 5000)) {
|
|
3155
|
+
armOverallDeadline();
|
|
3140
3156
|
}
|
|
3141
|
-
|
|
3157
|
+
}
|
|
3158
|
+
|
|
3159
|
+
const stillSuspected = !silentRefresh.success || !(await isPageLoggedIn(page, 2000));
|
|
3160
|
+
if (stillSuspected) {
|
|
3161
|
+
const { getAccountCredentials } = await import("../core/accounts.ts");
|
|
3162
|
+
const creds = getAccountCredentials(accountId);
|
|
3163
|
+
if (creds && creds.email && creds.password) {
|
|
3164
|
+
console.warn(
|
|
3165
|
+
`β οΈ [Playwright] Session expired or guest state detected during header capture for ${accountId}; re-authenticating...`,
|
|
3166
|
+
);
|
|
3167
|
+
armOverallDeadline();
|
|
3168
|
+
const ok = await loginToQwen(accountId, creds.email, creds.password);
|
|
3169
|
+
if (ok) {
|
|
3170
|
+
// Re-login navigated; load the chat page and wait for hydration so
|
|
3171
|
+
// the check below probes a live authenticated chat page.
|
|
3172
|
+
await page.goto(qwenUrl("/"), {
|
|
3173
|
+
waitUntil: "domcontentloaded",
|
|
3174
|
+
timeout: Math.min(config.timeouts.navigation, timeoutMs),
|
|
3175
|
+
});
|
|
3176
|
+
await sleep(2000);
|
|
3177
|
+
}
|
|
3178
|
+
if (!ok || !(await isPageLoggedIn(page, 6000))) {
|
|
3179
|
+
settle(
|
|
3180
|
+
new Error(
|
|
3181
|
+
`Header capture failed for ${accountId}: re-login after session expiry did not succeed`,
|
|
3182
|
+
),
|
|
3183
|
+
);
|
|
3184
|
+
return;
|
|
3185
|
+
}
|
|
3186
|
+
armOverallDeadline();
|
|
3187
|
+
if (settled || page.isClosed()) return;
|
|
3188
|
+
} else {
|
|
3142
3189
|
settle(
|
|
3143
3190
|
new Error(
|
|
3144
|
-
`Header capture failed for ${accountId}: re-login
|
|
3191
|
+
`Header capture failed for ${accountId}: session expired and no credentials available for re-login (run 'qpx login')`,
|
|
3145
3192
|
),
|
|
3146
3193
|
);
|
|
3147
3194
|
return;
|
|
3148
3195
|
}
|
|
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
3196
|
}
|
|
3159
3197
|
}
|
|
3160
3198
|
|
|
@@ -3593,7 +3631,15 @@ async function refreshHeadersInternal(
|
|
|
3593
3631
|
`[Playwright] Navigation check failed during refresh for ${accountId}:`,
|
|
3594
3632
|
(navErr as Error).message,
|
|
3595
3633
|
);
|
|
3596
|
-
await
|
|
3634
|
+
if (await isPageLoggedIn(page, 3000)) {
|
|
3635
|
+
// Still authenticated despite navigation timeout; do not escalate to slow form re-auth
|
|
3636
|
+
} else {
|
|
3637
|
+
// Try silent in-page token refresh before executing full form login
|
|
3638
|
+
const refreshed = await refreshAccountToken(accountId);
|
|
3639
|
+
if (!refreshed.success) {
|
|
3640
|
+
await executeReauth();
|
|
3641
|
+
}
|
|
3642
|
+
}
|
|
3597
3643
|
}
|
|
3598
3644
|
}
|
|
3599
3645
|
|
|
@@ -3610,6 +3656,12 @@ async function refreshHeadersInternal(
|
|
|
3610
3656
|
const { saveAuthSession } = await import("../core/database.ts");
|
|
3611
3657
|
const tokenCookie = liveCookies.find((c) => c.name === "token");
|
|
3612
3658
|
const exp = tokenCookie ? parseJwtExpiry(tokenCookie.value) : undefined;
|
|
3659
|
+
let rTok: string | undefined;
|
|
3660
|
+
try {
|
|
3661
|
+
rTok =
|
|
3662
|
+
(await page.evaluate(() => localStorage.getItem("refresh_token") || "")).trim() ||
|
|
3663
|
+
undefined;
|
|
3664
|
+
} catch {}
|
|
3613
3665
|
saveAuthSession(accountId, {
|
|
3614
3666
|
cookie: cookieStr,
|
|
3615
3667
|
userAgent: cache.headers["user-agent"] || "",
|
|
@@ -3621,6 +3673,7 @@ async function refreshHeadersInternal(
|
|
|
3621
3673
|
secChUaPlatform: cache.headers["sec-ch-ua-platform"] || undefined,
|
|
3622
3674
|
version: cache.headers["version"] || undefined,
|
|
3623
3675
|
tokenExpiresAt: exp || undefined,
|
|
3676
|
+
refreshToken: rTok,
|
|
3624
3677
|
capturedAt: Date.now(),
|
|
3625
3678
|
});
|
|
3626
3679
|
} catch {}
|
|
@@ -4169,8 +4222,10 @@ export async function keepAlivePlaywrightAccount(
|
|
|
4169
4222
|
const currentUrl = page.url();
|
|
4170
4223
|
|
|
4171
4224
|
// Proactive session check: if token expires within 5 minutes, refresh in-page silently using refreshAccountToken
|
|
4225
|
+
const lastRefreshAttempt = lastProactiveTokenRefresh.get(accountId) ?? 0;
|
|
4172
4226
|
const cookie = await getCookies(accountId);
|
|
4173
|
-
if (cookie && isTokenExpiringSoon(cookie, 5)) {
|
|
4227
|
+
if (cookie && isTokenExpiringSoon(cookie, 5) && now - lastRefreshAttempt > 60_000) {
|
|
4228
|
+
lastProactiveTokenRefresh.set(accountId, now);
|
|
4174
4229
|
console.log(
|
|
4175
4230
|
`π [SessionKeeper] Account ${accountId} access token expiring soon (<5m); silently refreshing...`,
|
|
4176
4231
|
);
|
|
@@ -4233,6 +4288,7 @@ function cleanupPlaywrightAccountState(accountId: string): void {
|
|
|
4233
4288
|
cookieCaches.delete(accountId);
|
|
4234
4289
|
lastAccountActivity.delete(accountId);
|
|
4235
4290
|
lastKeepAliveNavigation.delete(accountId);
|
|
4291
|
+
lastProactiveTokenRefresh.delete(accountId);
|
|
4236
4292
|
clearFingerprintCache(accountId);
|
|
4237
4293
|
// The account's context died/closed β its captured headers are stale or the
|
|
4238
4294
|
// 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)
|
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`);
|