qwenproxy-cli 1.0.19 → 1.0.21
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qwenproxy-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.21",
|
|
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": {
|
|
@@ -956,18 +956,9 @@ function parseJsonIfPossible(raw: string): unknown {
|
|
|
956
956
|
}
|
|
957
957
|
|
|
958
958
|
/**
|
|
959
|
-
* Cooldown for a RateLimited error.
|
|
960
|
-
*
|
|
961
|
-
* back to the account manager's default cooldown.
|
|
959
|
+
* Cooldown for a RateLimited error. Per Invariant 4, daily quotas reset strictly
|
|
960
|
+
* at 00:00 UTC, so we delegate to markAccountRateLimited's default midnight calculation.
|
|
962
961
|
*/
|
|
963
|
-
function rateLimitCooldownMs(err: UpstreamRateLimit): number | undefined {
|
|
964
|
-
const hourMatch = err.message?.match(/(\d+)\s*hours?/i);
|
|
965
|
-
if (hourMatch) {
|
|
966
|
-
const hours = Math.max(1, parseInt(hourMatch[1], 10));
|
|
967
|
-
return hours * 60 * 60 * 1000;
|
|
968
|
-
}
|
|
969
|
-
return undefined;
|
|
970
|
-
}
|
|
971
962
|
|
|
972
963
|
/**
|
|
973
964
|
* Detects a Qwen daily usage limit response. The upstream answers HTTP 200
|
|
@@ -1290,11 +1281,7 @@ export async function generateImage(params: {
|
|
|
1290
1281
|
}
|
|
1291
1282
|
|
|
1292
1283
|
if (lastError instanceof UpstreamRateLimit) {
|
|
1293
|
-
markAccountRateLimited(
|
|
1294
|
-
account.id,
|
|
1295
|
-
rateLimitCooldownMs(lastError),
|
|
1296
|
-
"RateLimited",
|
|
1297
|
-
);
|
|
1284
|
+
markAccountRateLimited(account.id, undefined, "RateLimited");
|
|
1298
1285
|
} else {
|
|
1299
1286
|
markAccountRateLimited(account.id, ACCOUNT_COOLDOWN_MS, "MediaGenFailed");
|
|
1300
1287
|
}
|
|
@@ -1533,11 +1520,7 @@ export async function generateVideo(params: {
|
|
|
1533
1520
|
}
|
|
1534
1521
|
|
|
1535
1522
|
if (lastError instanceof UpstreamRateLimit) {
|
|
1536
|
-
markAccountRateLimited(
|
|
1537
|
-
account.id,
|
|
1538
|
-
rateLimitCooldownMs(lastError),
|
|
1539
|
-
"RateLimited",
|
|
1540
|
-
);
|
|
1523
|
+
markAccountRateLimited(account.id, undefined, "RateLimited");
|
|
1541
1524
|
} else {
|
|
1542
1525
|
markAccountRateLimited(account.id, ACCOUNT_COOLDOWN_MS, "MediaGenFailed");
|
|
1543
1526
|
}
|
|
@@ -189,7 +189,7 @@ async function recoverStuckAccountMutex(
|
|
|
189
189
|
);
|
|
190
190
|
const context = accountContexts.get(accountId);
|
|
191
191
|
if (context) {
|
|
192
|
-
await closePlaywrightContextBestEffort(accountId, context);
|
|
192
|
+
await closePlaywrightContextBestEffort(accountId, context, { skipStorageSave: true });
|
|
193
193
|
}
|
|
194
194
|
cleanupPlaywrightAccountState(accountId);
|
|
195
195
|
if (accountMutexes.get(accountId) === mutex) {
|
|
@@ -308,8 +308,17 @@ async function hasValidAuthCookie(context: BrowserContext, timeoutMs = 3_000): P
|
|
|
308
308
|
* Detect whether the page is authenticated.
|
|
309
309
|
* Probes the authoritative /api/v1/auths/ endpoint from the page context (verified via HAR forensics)
|
|
310
310
|
* and falls back to inspecting the presence of visible "Log in" / "Sign up" buttons.
|
|
311
|
+
*
|
|
312
|
+
* The probe is bounded: page.evaluate ignores Playwright's default timeouts, so a
|
|
313
|
+
* WAF-blocked page whose in-page fetch never settles would hang the caller with no
|
|
314
|
+
* error at all. A probe that cannot answer is treated as "not logged in" — the
|
|
315
|
+
* caller re-authenticates or reloads, which is strictly better than burning the
|
|
316
|
+
* whole header budget waiting on a frozen page.
|
|
311
317
|
*/
|
|
312
|
-
export async function isPageLoggedIn(
|
|
318
|
+
export async function isPageLoggedIn(
|
|
319
|
+
page: Page,
|
|
320
|
+
timeoutMs = SESSION_PROBE_NAVIGATION_TIMEOUT_MS,
|
|
321
|
+
): Promise<boolean> {
|
|
313
322
|
if (!page) return false;
|
|
314
323
|
if (typeof page.isClosed === "function" && page.isClosed()) return false;
|
|
315
324
|
try {
|
|
@@ -317,7 +326,7 @@ export async function isPageLoggedIn(page: Page): Promise<boolean> {
|
|
|
317
326
|
if (url.includes("/auth") || url.includes("/login")) return false;
|
|
318
327
|
if (typeof page.evaluate !== "function") return true;
|
|
319
328
|
|
|
320
|
-
|
|
329
|
+
const probe = page
|
|
321
330
|
.evaluate(async () => {
|
|
322
331
|
try {
|
|
323
332
|
const res = await fetch("/api/v1/auths/", { method: "GET" });
|
|
@@ -330,6 +339,12 @@ export async function isPageLoggedIn(page: Page): Promise<boolean> {
|
|
|
330
339
|
}
|
|
331
340
|
})
|
|
332
341
|
.catch(() => false);
|
|
342
|
+
|
|
343
|
+
return await withTimeout(
|
|
344
|
+
probe,
|
|
345
|
+
Math.max(1_000, timeoutMs),
|
|
346
|
+
`session probe timed out after ${timeoutMs}ms`,
|
|
347
|
+
);
|
|
333
348
|
} catch {
|
|
334
349
|
return false;
|
|
335
350
|
}
|
|
@@ -456,6 +471,16 @@ const FIRST_TRIGGER_GRACE_MS = 3_000;
|
|
|
456
471
|
* sends cover it while still failing a page that never produces them.
|
|
457
472
|
*/
|
|
458
473
|
const HEADER_CAPTURE_TRIGGER_ATTEMPTS = 3;
|
|
474
|
+
/**
|
|
475
|
+
* A healthy Qwen chat page renders its input within a couple of seconds. When
|
|
476
|
+
* it never does, the page is blocked (WAF interstitial, punish document, or a
|
|
477
|
+
* cold SPA that failed to hydrate) and page.focus would wait out the 60s page
|
|
478
|
+
* default, freezing the whole capture. Bound the wait well under the header
|
|
479
|
+
* budget so a stuck page reloads and retries instead of hanging.
|
|
480
|
+
*/
|
|
481
|
+
const CHAT_INPUT_APPEAR_TIMEOUT_MS = 15_000;
|
|
482
|
+
/** Per-action bound for focus/fill/type once the input is already visible. */
|
|
483
|
+
const CHAT_INPUT_ACTION_TIMEOUT_MS = 10_000;
|
|
459
484
|
|
|
460
485
|
/**
|
|
461
486
|
* A challenge blocking the chat page makes the send button inert, so header
|
|
@@ -1992,6 +2017,7 @@ export async function captureQwenHeaders(
|
|
|
1992
2017
|
let headersCaptured = false;
|
|
1993
2018
|
let retriggerRequested = false;
|
|
1994
2019
|
let lastAttemptGraceTimedOut = false;
|
|
2020
|
+
let lastAttemptInputMissing = false;
|
|
1995
2021
|
let graceTimeoutCount = 0;
|
|
1996
2022
|
let wakeTriggerLoop: (() => void) | undefined;
|
|
1997
2023
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -2199,7 +2225,10 @@ export async function captureQwenHeaders(
|
|
|
2199
2225
|
// burn every trigger attempt on a textarea that does not exist. Re-login
|
|
2200
2226
|
// immediately when credentials are available; otherwise fail fast with a
|
|
2201
2227
|
// clear diagnosis instead of 3 pointless grace timeouts.
|
|
2202
|
-
const loggedIn = await isPageLoggedIn(
|
|
2228
|
+
const loggedIn = await isPageLoggedIn(
|
|
2229
|
+
page,
|
|
2230
|
+
Math.max(1_000, Math.min(remainingBudgetMs(), SESSION_PROBE_NAVIGATION_TIMEOUT_MS)),
|
|
2231
|
+
);
|
|
2203
2232
|
if (!loggedIn) {
|
|
2204
2233
|
const { getAccountCredentials } = await import("../core/accounts.ts");
|
|
2205
2234
|
const creds = getAccountCredentials(accountId);
|
|
@@ -2237,11 +2266,59 @@ export async function captureQwenHeaders(
|
|
|
2237
2266
|
// Mirrors upstream 5b3fd3e (robust account header capture).
|
|
2238
2267
|
const inputSelector =
|
|
2239
2268
|
'textarea.message-input-textarea:visible, textarea:visible, [contenteditable="true"]:visible';
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2269
|
+
// Bound the appearance wait: a page that never renders the chat input is
|
|
2270
|
+
// blocked (WAF interstitial, punish document, or failed SPA hydration).
|
|
2271
|
+
// Unbounded, page.focus would burn its 60s default timeout on every
|
|
2272
|
+
// attempt, freezing the whole capture and cooling a healthy account with
|
|
2273
|
+
// AuthInitFailed. A miss marks the attempt for a reload instead.
|
|
2274
|
+
try {
|
|
2275
|
+
await page
|
|
2276
|
+
.locator(inputSelector)
|
|
2277
|
+
.first()
|
|
2278
|
+
.waitFor({
|
|
2279
|
+
state: "visible",
|
|
2280
|
+
timeout: Math.max(
|
|
2281
|
+
1,
|
|
2282
|
+
Math.min(CHAT_INPUT_APPEAR_TIMEOUT_MS, remainingBudgetMs()),
|
|
2283
|
+
),
|
|
2284
|
+
});
|
|
2285
|
+
} catch {
|
|
2286
|
+
if (settled || page.isClosed()) return;
|
|
2287
|
+
console.warn(
|
|
2288
|
+
`⏱️ [Playwright] Chat input never appeared for ${accountId} (attempt ${attempt}); reloading`,
|
|
2289
|
+
);
|
|
2290
|
+
lastAttemptInputMissing = true;
|
|
2291
|
+
retriggerRequested = true;
|
|
2292
|
+
wakeTrigger();
|
|
2293
|
+
return;
|
|
2294
|
+
}
|
|
2243
2295
|
if (settled || page.isClosed()) return;
|
|
2244
|
-
|
|
2296
|
+
const inputActionTimeoutMs = Math.max(
|
|
2297
|
+
1,
|
|
2298
|
+
Math.min(CHAT_INPUT_ACTION_TIMEOUT_MS, remainingBudgetMs()),
|
|
2299
|
+
);
|
|
2300
|
+
try {
|
|
2301
|
+
await page.focus(inputSelector, { timeout: inputActionTimeoutMs });
|
|
2302
|
+
if (settled || page.isClosed()) return;
|
|
2303
|
+
await page.fill(inputSelector, "", { timeout: inputActionTimeoutMs });
|
|
2304
|
+
if (settled || page.isClosed()) return;
|
|
2305
|
+
await page.type(inputSelector, "a", {
|
|
2306
|
+
delay: 100,
|
|
2307
|
+
timeout: inputActionTimeoutMs,
|
|
2308
|
+
});
|
|
2309
|
+
} catch {
|
|
2310
|
+
// The input detached mid-interaction (challenge overlay, SPA
|
|
2311
|
+
// re-render): same diagnosis as never appearing — only a fresh load
|
|
2312
|
+
// recovers it.
|
|
2313
|
+
if (settled || page.isClosed()) return;
|
|
2314
|
+
console.warn(
|
|
2315
|
+
`⏱️ [Playwright] Chat input interaction failed for ${accountId} (attempt ${attempt}); reloading`,
|
|
2316
|
+
);
|
|
2317
|
+
lastAttemptInputMissing = true;
|
|
2318
|
+
retriggerRequested = true;
|
|
2319
|
+
wakeTrigger();
|
|
2320
|
+
return;
|
|
2321
|
+
}
|
|
2245
2322
|
if (settled || page.isClosed()) return;
|
|
2246
2323
|
await sleep(2000);
|
|
2247
2324
|
if (settled || page.isClosed()) return;
|
|
@@ -2309,8 +2386,16 @@ export async function captureQwenHeaders(
|
|
|
2309
2386
|
// no request (indicating a stuck page/challenge that needs a fresh load).
|
|
2310
2387
|
// Attempt 2 preserves the page from attempt 1 so the bx SDK that just
|
|
2311
2388
|
// finished initializing in the background is not thrown away.
|
|
2312
|
-
|
|
2389
|
+
// A missing chat input is the exception: the page never rendered the
|
|
2390
|
+
// chat UI, so there is no warm SDK state to protect and only a fresh
|
|
2391
|
+
// load can recover it.
|
|
2392
|
+
if (
|
|
2393
|
+
attempt === 1 ||
|
|
2394
|
+
lastAttemptInputMissing ||
|
|
2395
|
+
(lastAttemptGraceTimedOut && attempt >= 3)
|
|
2396
|
+
) {
|
|
2313
2397
|
lastAttemptGraceTimedOut = false;
|
|
2398
|
+
lastAttemptInputMissing = false;
|
|
2314
2399
|
await openChatPage();
|
|
2315
2400
|
}
|
|
2316
2401
|
if (settled) return;
|
|
@@ -2559,7 +2644,7 @@ export async function withAccountPage<T>(
|
|
|
2559
2644
|
);
|
|
2560
2645
|
const context = accountContexts.get(accountId);
|
|
2561
2646
|
if (context) {
|
|
2562
|
-
await closePlaywrightContextBestEffort(accountId, context);
|
|
2647
|
+
await closePlaywrightContextBestEffort(accountId, context, { skipStorageSave: true });
|
|
2563
2648
|
}
|
|
2564
2649
|
cleanupPlaywrightAccountState(accountId);
|
|
2565
2650
|
}
|
|
@@ -3047,7 +3132,7 @@ export function installContextDeathHandlers(
|
|
|
3047
3132
|
): void {
|
|
3048
3133
|
const onDeath = (): void => {
|
|
3049
3134
|
cleanupPlaywrightAccountState(accountId);
|
|
3050
|
-
void closePlaywrightContextBestEffort(accountId, context).catch(() => {});
|
|
3135
|
+
void closePlaywrightContextBestEffort(accountId, context, { skipStorageSave: true }).catch(() => {});
|
|
3051
3136
|
};
|
|
3052
3137
|
context.on("close", onDeath);
|
|
3053
3138
|
page.on("crash", onDeath);
|