qwenproxy-cli 1.6.10 → 1.6.12
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 +390 -30
- package/dist/delete-chats.js +946 -445
- package/dist/index.js +612 -100
- package/dist/login.js +417 -51
- package/dist/reset-cooldowns.js +769 -407
- package/dist/sync-clients.js +4 -1
- package/package.json +2 -2
- package/src/api/server.ts +8 -4
- package/src/core/config.ts +4 -1
- package/src/core/model-alias.ts +1 -0
- package/src/core/version.ts +1 -1
- package/src/core/watchdog.ts +4 -3
- package/src/routes/chat/account.ts +12 -0
- package/src/routes/chat/streaming.ts +18 -2
- package/src/services/auth-playwright.ts +3 -2
- package/src/services/captcha-coordinator.ts +41 -11
- package/src/services/captcha-solver.ts +400 -2
- package/src/services/chat-cleanup.ts +11 -2
- package/src/services/playwright.ts +135 -50
- package/src/services/qwen-chat-pool.ts +52 -9
- package/src/services/qwen-headers.ts +18 -1
- package/src/services/qwen.ts +35 -7
- package/src/services/session-keeper.ts +70 -8
package/src/services/qwen.ts
CHANGED
|
@@ -129,6 +129,20 @@ const browserStreamStates = new Map<string, BrowserStreamState>();
|
|
|
129
129
|
const browserStreamBindingPages = new WeakSet<object>();
|
|
130
130
|
const browserStreamBindingContexts = new WeakSet<object>();
|
|
131
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Whether the account currently has an in-flight browser stream. Captcha
|
|
134
|
+
* recovery uses this to avoid navigating the page that is feeding a live
|
|
135
|
+
* stream (it opens an ephemeral tab instead).
|
|
136
|
+
*/
|
|
137
|
+
export function hasActiveBrowserStreamsForAccount(accountId: string): boolean {
|
|
138
|
+
for (const state of browserStreamStates.values()) {
|
|
139
|
+
if (state.accountId === accountId && !state.done) {
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
|
|
132
146
|
/**
|
|
133
147
|
* Abort all active browser streams for an account immediately.
|
|
134
148
|
* Called when an account page is navigated by captcha recovery, closed, or reset,
|
|
@@ -992,7 +1006,7 @@ export async function requestQwenTextInBrowser(
|
|
|
992
1006
|
|
|
993
1007
|
const evaluateRequest = (page: Page) =>
|
|
994
1008
|
page.evaluate(
|
|
995
|
-
async ({ url, method, headers, body, referrer, timeoutMs, tz }: {
|
|
1009
|
+
async ({ url, method, headers, body, referrer, timeoutMs, tz, version }: {
|
|
996
1010
|
url: string;
|
|
997
1011
|
method: "GET" | "POST" | "DELETE";
|
|
998
1012
|
headers: Record<string, string>;
|
|
@@ -1000,6 +1014,7 @@ export async function requestQwenTextInBrowser(
|
|
|
1000
1014
|
referrer?: string;
|
|
1001
1015
|
timeoutMs: number;
|
|
1002
1016
|
tz: string;
|
|
1017
|
+
version: string;
|
|
1003
1018
|
}): Promise<BrowserTextResponse> => {
|
|
1004
1019
|
const controller = new AbortController();
|
|
1005
1020
|
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
@@ -1024,7 +1039,7 @@ export async function requestQwenTextInBrowser(
|
|
|
1024
1039
|
headers: {
|
|
1025
1040
|
accept: "application/json, text/plain, */*",
|
|
1026
1041
|
source: "web",
|
|
1027
|
-
version
|
|
1042
|
+
version,
|
|
1028
1043
|
"x-request-origin": "https://chat.qwen.ai",
|
|
1029
1044
|
timezone: tz,
|
|
1030
1045
|
authorization: `Bearer ${rTok}`,
|
|
@@ -1073,6 +1088,7 @@ export async function requestQwenTextInBrowser(
|
|
|
1073
1088
|
referrer: options.referrer,
|
|
1074
1089
|
timeoutMs: options.timeoutMs ?? Math.min(config.timeouts.page, 20_000),
|
|
1075
1090
|
tz: formatAsciiTimezone(),
|
|
1091
|
+
version: getQwenWebVersion(),
|
|
1076
1092
|
},
|
|
1077
1093
|
);
|
|
1078
1094
|
const recoverOnTimeout = !options.noMutexRecovery;
|
|
@@ -1364,12 +1380,22 @@ async function createQwenBrowserResponse(
|
|
|
1364
1380
|
await ensureBrowserStreamBinding(page);
|
|
1365
1381
|
if (typeof (page as any).on === "function") {
|
|
1366
1382
|
const onFrameNavigated = (frame: any) => {
|
|
1367
|
-
if (
|
|
1368
|
-
|
|
1369
|
-
|
|
1383
|
+
if (
|
|
1384
|
+
!settled &&
|
|
1385
|
+
typeof (page as any).mainFrame === "function" &&
|
|
1386
|
+
frame === (page as any).mainFrame()
|
|
1387
|
+
) {
|
|
1388
|
+
const url =
|
|
1389
|
+
typeof frame.url === "function"
|
|
1390
|
+
? frame.url()
|
|
1391
|
+
: String(frame.url || "");
|
|
1392
|
+
// Same-origin SPA routing within Qwen Web (e.g. /c/<id>, /#) keeps
|
|
1393
|
+
// the live DOM and MUST NOT abort the stream. Only a genuine
|
|
1394
|
+
// unload (auth, Baxia punish, or another origin) kills the fetch.
|
|
1370
1395
|
if (
|
|
1371
1396
|
url.startsWith(qwenOrigin()) &&
|
|
1372
1397
|
!url.includes("/auth") &&
|
|
1398
|
+
!url.includes("/login") &&
|
|
1373
1399
|
!url.includes("_____tmd_____") &&
|
|
1374
1400
|
!url.includes("/punish")
|
|
1375
1401
|
) {
|
|
@@ -1393,10 +1419,12 @@ async function createQwenBrowserResponse(
|
|
|
1393
1419
|
};
|
|
1394
1420
|
(page as any).on("framenavigated", onFrameNavigated);
|
|
1395
1421
|
(page as any).on("close", onPageClosed);
|
|
1422
|
+
(page as any).on("crash", onPageClosed);
|
|
1396
1423
|
pageCleanup = () => {
|
|
1397
1424
|
try {
|
|
1398
1425
|
(page as any).off?.("framenavigated", onFrameNavigated);
|
|
1399
1426
|
(page as any).off?.("close", onPageClosed);
|
|
1427
|
+
(page as any).off?.("crash", onPageClosed);
|
|
1400
1428
|
} catch {}
|
|
1401
1429
|
};
|
|
1402
1430
|
}
|
|
@@ -2369,7 +2397,7 @@ export async function deleteSingleQwenChat(
|
|
|
2369
2397
|
|
|
2370
2398
|
const requestHeaders: Record<string, string> = {
|
|
2371
2399
|
source: "web",
|
|
2372
|
-
version: getQwenWebVersion()
|
|
2400
|
+
version: getQwenWebVersion(),
|
|
2373
2401
|
timezone: formatAsciiTimezone(),
|
|
2374
2402
|
"x-request-id": crypto.randomUUID(),
|
|
2375
2403
|
Referer: qwenUrl(`/c/${encodeURIComponent(chatId)}`),
|
|
@@ -2422,7 +2450,7 @@ export async function fetchRemoteQwenChats(
|
|
|
2422
2450
|
}
|
|
2423
2451
|
|
|
2424
2452
|
const requestHeaders: Record<string, string> = {
|
|
2425
|
-
version: getQwenWebVersion()
|
|
2453
|
+
version: getQwenWebVersion(),
|
|
2426
2454
|
timezone: formatAsciiTimezone(),
|
|
2427
2455
|
"x-request-id": crypto.randomUUID(),
|
|
2428
2456
|
Referer: qwenUrl("/settings/chats"),
|
|
@@ -13,6 +13,55 @@ let running = false;
|
|
|
13
13
|
let intervalId: ReturnType<typeof setInterval> | null = null;
|
|
14
14
|
let cycleInProgress = false;
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* A keep-alive failure (navigation timeout, transient WAF, closed context
|
|
18
|
+
* that is not yet reported as closed) must not retry on every cycle: it would
|
|
19
|
+
* hammer the same broken page and bury the logs. Back off per account with the
|
|
20
|
+
* usual exponential schedule, capped at five minutes.
|
|
21
|
+
*/
|
|
22
|
+
const KEEP_ALIVE_BACKOFF_BASE_MS = 2_000;
|
|
23
|
+
const KEEP_ALIVE_BACKOFF_MAX_MS = 5 * 60_000;
|
|
24
|
+
|
|
25
|
+
interface KeepAliveRetryState {
|
|
26
|
+
failures: number;
|
|
27
|
+
nextAttemptAt: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const keepAliveRetries = new Map<string, KeepAliveRetryState>();
|
|
31
|
+
|
|
32
|
+
export function computeSessionKeeperBackoffMs(failures: number): number {
|
|
33
|
+
const exponent = Math.max(0, Math.min(failures - 1, 8));
|
|
34
|
+
return Math.min(
|
|
35
|
+
KEEP_ALIVE_BACKOFF_MAX_MS,
|
|
36
|
+
KEEP_ALIVE_BACKOFF_BASE_MS * 2 ** exponent,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isKeepAliveBackedOff(accountId: string): boolean {
|
|
41
|
+
const state = keepAliveRetries.get(accountId);
|
|
42
|
+
return Boolean(state && Date.now() < state.nextAttemptAt);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function recordKeepAliveFailure(accountId: string): number {
|
|
46
|
+
const state = keepAliveRetries.get(accountId) ?? {
|
|
47
|
+
failures: 0,
|
|
48
|
+
nextAttemptAt: 0,
|
|
49
|
+
};
|
|
50
|
+
state.failures += 1;
|
|
51
|
+
const backoffMs = computeSessionKeeperBackoffMs(state.failures);
|
|
52
|
+
state.nextAttemptAt = Date.now() + backoffMs;
|
|
53
|
+
keepAliveRetries.set(accountId, state);
|
|
54
|
+
return backoffMs;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function resetSessionKeeperRetryStateForTests(): void {
|
|
58
|
+
keepAliveRetries.clear();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function isSessionKeeperAccountBackedOff(accountId: string): boolean {
|
|
62
|
+
return isKeepAliveBackedOff(accountId);
|
|
63
|
+
}
|
|
64
|
+
|
|
16
65
|
export function isSessionKeeperRunning(): boolean {
|
|
17
66
|
return running;
|
|
18
67
|
}
|
|
@@ -24,19 +73,31 @@ async function runKeepAliveCycle(): Promise<void> {
|
|
|
24
73
|
if (config.sessionKeeper.enabled) {
|
|
25
74
|
const accountIds = getActivePlaywrightAccountIds();
|
|
26
75
|
for (const accountId of accountIds) {
|
|
27
|
-
|
|
76
|
+
if (isKeepAliveBackedOff(accountId)) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
const keptAlive = await keepAlivePlaywrightAccount(accountId);
|
|
81
|
+
if (keptAlive) {
|
|
82
|
+
keepAliveRetries.delete(accountId);
|
|
83
|
+
}
|
|
84
|
+
} catch (error) {
|
|
28
85
|
// Shutdown/eviction closes contexts while a cycle is in flight; the
|
|
29
86
|
// resulting "already closed" rejection is benign. The old substring
|
|
30
87
|
// filter ("Target closed"/"Page is closed") missed Playwright's real
|
|
31
88
|
// message ("Target page, context or browser has been closed") and
|
|
32
89
|
// leaked a warning on every Ctrl+C.
|
|
33
|
-
if (isPlaywrightAlreadyClosedError(error))
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
90
|
+
if (isPlaywrightAlreadyClosedError(error)) {
|
|
91
|
+
keepAliveRetries.delete(accountId);
|
|
92
|
+
} else {
|
|
93
|
+
const message =
|
|
94
|
+
error instanceof Error ? error.message : String(error);
|
|
95
|
+
const backoffMs = recordKeepAliveFailure(accountId);
|
|
96
|
+
console.warn(
|
|
97
|
+
`[SessionKeeper] Keep-alive failed for ${accountId}: ${message} (retry in ${Math.round(backoffMs / 1000)}s)`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
40
101
|
await sleep(humanDelay(250, 900));
|
|
41
102
|
}
|
|
42
103
|
}
|
|
@@ -81,6 +142,7 @@ export function stopSessionKeeper(): void {
|
|
|
81
142
|
intervalId = null;
|
|
82
143
|
}
|
|
83
144
|
cycleInProgress = false;
|
|
145
|
+
keepAliveRetries.clear();
|
|
84
146
|
}
|
|
85
147
|
|
|
86
148
|
export async function runSessionKeeperOnceForTesting(): Promise<void> {
|