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/dist/sync-clients.js
CHANGED
|
@@ -119,7 +119,7 @@ var envSchema = z.object({
|
|
|
119
119
|
// Version header on Qwen API requests = the deployed web bundle version
|
|
120
120
|
// (fallback when dynamic DOM detection is not yet completed).
|
|
121
121
|
// Override via env when Qwen ships a new bundle.
|
|
122
|
-
QWEN_WEB_VERSION: z.string().default("0.3.
|
|
122
|
+
QWEN_WEB_VERSION: z.string().default("0.3.43"),
|
|
123
123
|
// Controls bx-ua/bx-umidtoken injection on the GENERAL API paths
|
|
124
124
|
// (chats/new, settings): those work without them (live-probed). The
|
|
125
125
|
// completions path is the exception — the 0.2.86 HAR shows the real
|
|
@@ -162,6 +162,7 @@ var envSchema = z.object({
|
|
|
162
162
|
CAPTCHA_SOLVER_ENABLED: z.string().default("true"),
|
|
163
163
|
CAPTCHA_SOLVER_MAX_ATTEMPTS: z.string().default("3"),
|
|
164
164
|
CAPTCHA_SOLVER_TIMEOUT_MS: z.string().default("15000"),
|
|
165
|
+
CAPTCHA_SLIDER_TIMEOUT_MS: z.string().default("8000"),
|
|
165
166
|
CAPTCHA_SOLVER_RETRY_DELAY_MS: z.string().default("1000"),
|
|
166
167
|
CAPTCHA_SOLVER_SETTLE_MS: z.string().default("2000"),
|
|
167
168
|
CAPTCHA_ACCOUNT_COOLDOWN_MS: z.string().default("120000"),
|
|
@@ -313,6 +314,8 @@ var config = {
|
|
|
313
314
|
enabled: env.CAPTCHA_SOLVER_ENABLED === "true",
|
|
314
315
|
maxAttempts: Math.max(1, Math.min(5, parseInt(env.CAPTCHA_SOLVER_MAX_ATTEMPTS))),
|
|
315
316
|
timeoutMs: Math.max(0, parseInt(env.CAPTCHA_SOLVER_TIMEOUT_MS)),
|
|
317
|
+
/** Max wait for a challenge slider to become visible (slow WAF renders). */
|
|
318
|
+
sliderTimeoutMs: Math.max(500, parseInt(env.CAPTCHA_SLIDER_TIMEOUT_MS)),
|
|
316
319
|
retryDelayMs: Math.max(0, parseInt(env.CAPTCHA_SOLVER_RETRY_DELAY_MS)),
|
|
317
320
|
settleMs: Math.max(0, parseInt(env.CAPTCHA_SOLVER_SETTLE_MS)),
|
|
318
321
|
/** Rest an account whose challenge could not be cleared before reusing it. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qwenproxy-cli",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.12",
|
|
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": {
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"bugs": {
|
|
59
59
|
"url": "https://github.com/johngbl/qwenproxy/issues"
|
|
60
60
|
},
|
|
61
|
-
"author": "johngbl
|
|
61
|
+
"author": "johngbl",
|
|
62
62
|
"license": "ISC",
|
|
63
63
|
"dependencies": {
|
|
64
64
|
"@hono/node-server": "^2.1.1",
|
package/src/api/server.ts
CHANGED
|
@@ -883,10 +883,14 @@ export async function startServer(options?: {
|
|
|
883
883
|
void (async () => {
|
|
884
884
|
const { ensureAccountInPriority } = await import("../core/account-priority.ts");
|
|
885
885
|
|
|
886
|
-
const
|
|
887
|
-
config.playwright.
|
|
888
|
-
|
|
889
|
-
|
|
886
|
+
const maxWarmCap =
|
|
887
|
+
config.playwright.maxActiveContexts > 0
|
|
888
|
+
? Math.min(
|
|
889
|
+
config.playwright.startupWarmAccounts,
|
|
890
|
+
config.playwright.maxActiveContexts,
|
|
891
|
+
)
|
|
892
|
+
: config.playwright.startupWarmAccounts;
|
|
893
|
+
const targetWarmTotal = Math.min(maxWarmCap, totalAccounts);
|
|
890
894
|
let currentReadyCount = readyAccountIds.size;
|
|
891
895
|
|
|
892
896
|
if (currentReadyCount < targetWarmTotal && remainingAccounts.length > 0) {
|
package/src/core/config.ts
CHANGED
|
@@ -34,7 +34,7 @@ const envSchema = z
|
|
|
34
34
|
// Version header on Qwen API requests = the deployed web bundle version
|
|
35
35
|
// (fallback when dynamic DOM detection is not yet completed).
|
|
36
36
|
// Override via env when Qwen ships a new bundle.
|
|
37
|
-
QWEN_WEB_VERSION: z.string().default("0.3.
|
|
37
|
+
QWEN_WEB_VERSION: z.string().default("0.3.43"),
|
|
38
38
|
// Controls bx-ua/bx-umidtoken injection on the GENERAL API paths
|
|
39
39
|
// (chats/new, settings): those work without them (live-probed). The
|
|
40
40
|
// completions path is the exception — the 0.2.86 HAR shows the real
|
|
@@ -81,6 +81,7 @@ const envSchema = z
|
|
|
81
81
|
CAPTCHA_SOLVER_ENABLED: z.string().default("true"),
|
|
82
82
|
CAPTCHA_SOLVER_MAX_ATTEMPTS: z.string().default("3"),
|
|
83
83
|
CAPTCHA_SOLVER_TIMEOUT_MS: z.string().default("15000"),
|
|
84
|
+
CAPTCHA_SLIDER_TIMEOUT_MS: z.string().default("8000"),
|
|
84
85
|
CAPTCHA_SOLVER_RETRY_DELAY_MS: z.string().default("1000"),
|
|
85
86
|
CAPTCHA_SOLVER_SETTLE_MS: z.string().default("2000"),
|
|
86
87
|
CAPTCHA_ACCOUNT_COOLDOWN_MS: z.string().default("120000"),
|
|
@@ -243,6 +244,8 @@ export const config = {
|
|
|
243
244
|
enabled: env.CAPTCHA_SOLVER_ENABLED === "true",
|
|
244
245
|
maxAttempts: Math.max(1, Math.min(5, parseInt(env.CAPTCHA_SOLVER_MAX_ATTEMPTS))),
|
|
245
246
|
timeoutMs: Math.max(0, parseInt(env.CAPTCHA_SOLVER_TIMEOUT_MS)),
|
|
247
|
+
/** Max wait for a challenge slider to become visible (slow WAF renders). */
|
|
248
|
+
sliderTimeoutMs: Math.max(500, parseInt(env.CAPTCHA_SLIDER_TIMEOUT_MS)),
|
|
246
249
|
retryDelayMs: Math.max(0, parseInt(env.CAPTCHA_SOLVER_RETRY_DELAY_MS)),
|
|
247
250
|
settleMs: Math.max(0, parseInt(env.CAPTCHA_SOLVER_SETTLE_MS)),
|
|
248
251
|
/** Rest an account whose challenge could not be cleared before reusing it. */
|
package/src/core/model-alias.ts
CHANGED
package/src/core/version.ts
CHANGED
package/src/core/watchdog.ts
CHANGED
|
@@ -148,14 +148,15 @@ export class Watchdog extends EventEmitter {
|
|
|
148
148
|
// (idle mutex, no active stream, preserves the max-active-context minimum)
|
|
149
149
|
// so the next health check sees the freed RSS.
|
|
150
150
|
try {
|
|
151
|
-
const { closeIdlePlaywrightAccounts } = await import(
|
|
151
|
+
const { closeIdlePlaywrightAccounts, evictIdlePlaywrightContextsToLimit } = await import(
|
|
152
152
|
"../services/playwright.js"
|
|
153
153
|
);
|
|
154
|
+
const evicted = await evictIdlePlaywrightContextsToLimit(1);
|
|
154
155
|
const closed = await closeIdlePlaywrightAccounts(
|
|
155
156
|
config.sessionKeeper.idleMs,
|
|
156
157
|
);
|
|
157
|
-
if (closed > 0) {
|
|
158
|
-
this.emit("recovery:ram:contexts-closed", closed);
|
|
158
|
+
if (evicted + closed > 0) {
|
|
159
|
+
this.emit("recovery:ram:contexts-closed", evicted + closed);
|
|
159
160
|
}
|
|
160
161
|
} catch {
|
|
161
162
|
// Playwright may not be initialized (mock mode / pre-start); RSS relief
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
} from "../../core/account-manager.ts";
|
|
9
9
|
import { markAccountSuccessful, markAccountFailed, getAccountsByPriority } from "../../core/account-priority.ts";
|
|
10
10
|
import { recordWafHardBlock, noteWafRecovery } from "../../core/waf-isolation.ts";
|
|
11
|
+
import { isCaptchaRecoveryInBackoff } from "../../services/captcha-coordinator.ts";
|
|
11
12
|
import { loadAccounts, type QwenAccount } from "../../core/accounts.ts";
|
|
12
13
|
import { config, type ChatMode } from "../../core/config.ts";
|
|
13
14
|
import { ClientAbortedError, UpstreamRateLimit, ValidationError } from "../../core/errors.ts";
|
|
@@ -1615,6 +1616,17 @@ async function tryCreateStreamWithRetry(
|
|
|
1615
1616
|
invalidateLogicalThreadParent(params.sessionId);
|
|
1616
1617
|
}
|
|
1617
1618
|
|
|
1619
|
+
// If captcha recovery on this account already failed and is in its 30s
|
|
1620
|
+
// backoff window, same-account retries will only hit `recovery_skipped`
|
|
1621
|
+
// and waste time/RAM re-sending large payloads. Exit immediately so the
|
|
1622
|
+
// outer loop quarantines this account and switches to a fresh one.
|
|
1623
|
+
if (
|
|
1624
|
+
policy.reason === "anti_bot" &&
|
|
1625
|
+
isCaptchaRecoveryInBackoff(currentAccountId)
|
|
1626
|
+
) {
|
|
1627
|
+
return { success: false, error: err };
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1618
1630
|
// A generic invalid_input is often a stale/corrupted upstream chat rather
|
|
1619
1631
|
// than an account failure. Rebuild it once on the same account first. If the
|
|
1620
1632
|
// fresh chat fails again, the normal policy is allowed to rotate.
|
|
@@ -1259,8 +1259,8 @@ export async function processStreamingResponse(
|
|
|
1259
1259
|
};
|
|
1260
1260
|
|
|
1261
1261
|
const emitAnswerText = async (textChunk: string) => {
|
|
1262
|
-
if (textChunk) emittedModelOutput = true;
|
|
1263
1262
|
if (!toolParser) {
|
|
1263
|
+
if (textChunk) emittedModelOutput = true;
|
|
1264
1264
|
finalContent += textChunk;
|
|
1265
1265
|
writeDeltaEvent({ content: textChunk });
|
|
1266
1266
|
return;
|
|
@@ -1777,6 +1777,23 @@ export async function processStreamingResponse(
|
|
|
1777
1777
|
.replace(/\\\\/g, "\\");
|
|
1778
1778
|
|
|
1779
1779
|
if (unescaped) {
|
|
1780
|
+
if (!emittedModelOutput && isOverloadMessage(unescaped)) {
|
|
1781
|
+
const overloadErr = toRetryableStreamError(
|
|
1782
|
+
"server_overloaded",
|
|
1783
|
+
unescaped,
|
|
1784
|
+
{
|
|
1785
|
+
switchAccount: true,
|
|
1786
|
+
forceNewChat: true,
|
|
1787
|
+
reason: "server_busy",
|
|
1788
|
+
accountCooldownMs: OVERLOAD_COOLDOWN_MS,
|
|
1789
|
+
accountCooldownReason: "ServerOverloaded",
|
|
1790
|
+
},
|
|
1791
|
+
);
|
|
1792
|
+
if (await recoverFromStreamError(overloadErr)) {
|
|
1793
|
+
break;
|
|
1794
|
+
}
|
|
1795
|
+
throw overloadErr;
|
|
1796
|
+
}
|
|
1780
1797
|
const result = getIncrementalDelta(
|
|
1781
1798
|
lastRawContent,
|
|
1782
1799
|
unescaped,
|
|
@@ -1942,7 +1959,6 @@ export async function processStreamingResponse(
|
|
|
1942
1959
|
if (vStr === "FINISHED") continue;
|
|
1943
1960
|
|
|
1944
1961
|
if (isThinkingChunk) {
|
|
1945
|
-
emittedModelOutput = true;
|
|
1946
1962
|
reasoningBuffer += vStr;
|
|
1947
1963
|
writeDeltaEvent({ reasoning_content: vStr });
|
|
1948
1964
|
} else {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AuthError } from "../core/errors.ts";
|
|
2
2
|
import { getAccountCredentials, loadAccounts } from "../core/accounts.ts";
|
|
3
3
|
import { config } from "../core/config.ts";
|
|
4
|
+
import { getQwenBxVersion, getQwenWebVersion } from "./qwen-headers.ts";
|
|
4
5
|
import {
|
|
5
6
|
getBasicHeaders as getPlaywrightBasicHeaders,
|
|
6
7
|
initPlaywrightForAccount,
|
|
@@ -78,13 +79,13 @@ export async function getBasicHeaders(accountId?: string): Promise<{
|
|
|
78
79
|
return {
|
|
79
80
|
cookie: "token=mock",
|
|
80
81
|
userAgent: "mock",
|
|
81
|
-
bxV:
|
|
82
|
+
bxV: getQwenBxVersion(),
|
|
82
83
|
bxUa: "mock-bx-ua",
|
|
83
84
|
bxUmidtoken: "mock-bx-umidtoken",
|
|
84
85
|
secChUa: "",
|
|
85
86
|
secChUaMobile: "?0",
|
|
86
87
|
secChUaPlatform: "",
|
|
87
|
-
version:
|
|
88
|
+
version: getQwenWebVersion(),
|
|
88
89
|
};
|
|
89
90
|
}
|
|
90
91
|
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
extractBaxiaChallengeUrl,
|
|
8
8
|
logBaxiaCaptcha,
|
|
9
9
|
sanitizeCaptchaErrorDetail,
|
|
10
|
-
|
|
10
|
+
solveVisibleCaptcha,
|
|
11
11
|
} from "./captcha-solver.ts";
|
|
12
12
|
|
|
13
13
|
const CHALLENGE_NAVIGATION_TIMEOUT_MS = 8_000;
|
|
@@ -55,12 +55,13 @@ export async function solveChallengeOnPage(
|
|
|
55
55
|
maxAttempts: config.captcha.maxAttempts,
|
|
56
56
|
retryDelayMs: config.captcha.retryDelayMs,
|
|
57
57
|
settleMs: config.captcha.settleMs,
|
|
58
|
+
sliderTimeoutMs: config.captcha.sliderTimeoutMs,
|
|
58
59
|
};
|
|
59
60
|
|
|
60
61
|
// Qwen's own Baxia SDK sometimes renders the dialog for the background
|
|
61
62
|
// fetch. When it did, solve it in place: navigating away would discard the
|
|
62
63
|
// challenge the SDK is waiting on.
|
|
63
|
-
if (await
|
|
64
|
+
if (await solveVisibleCaptcha(page, { ...solverOptions, waitForMs: 0 })) {
|
|
64
65
|
return true;
|
|
65
66
|
}
|
|
66
67
|
|
|
@@ -72,7 +73,7 @@ export async function solveChallengeOnPage(
|
|
|
72
73
|
await gotoBestEffort(page, challengeUrl ?? qwenUrl("/"));
|
|
73
74
|
|
|
74
75
|
try {
|
|
75
|
-
return await
|
|
76
|
+
return await solveVisibleCaptcha(page, { ...solverOptions, waitForMs });
|
|
76
77
|
} finally {
|
|
77
78
|
// Never leave the account page parked on the punish document: a stale
|
|
78
79
|
// challenge page makes the next detection pass find itself.
|
|
@@ -92,6 +93,12 @@ export async function solveChallengeOnPage(
|
|
|
92
93
|
* a punish document that is never rendered; opening that document in the page
|
|
93
94
|
* is what gives the solver a slider to drive.
|
|
94
95
|
*/
|
|
96
|
+
export function isCaptchaRecoveryInBackoff(accountId: string | undefined): boolean {
|
|
97
|
+
if (!accountId) return false;
|
|
98
|
+
const lastFailure = lastFailedRecoveryAt.get(accountId) ?? 0;
|
|
99
|
+
return Date.now() - lastFailure < FAILED_RECOVERY_BACKOFF_MS;
|
|
100
|
+
}
|
|
101
|
+
|
|
95
102
|
export async function recoverBaxiaCaptcha(
|
|
96
103
|
accountId: string | undefined,
|
|
97
104
|
label: string,
|
|
@@ -126,19 +133,42 @@ export async function recoverBaxiaCaptcha(
|
|
|
126
133
|
|
|
127
134
|
activeCaptchaRecoveries.add(accountId);
|
|
128
135
|
try {
|
|
129
|
-
const {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
"Account stream interrupted by captcha challenge recovery",
|
|
133
|
-
);
|
|
134
|
-
} catch {}
|
|
135
|
-
try {
|
|
136
|
+
const { hasActiveBrowserStreamsForAccount } = await import("./qwen.ts");
|
|
137
|
+
const hasConcurrentStream = hasActiveBrowserStreamsForAccount(accountId);
|
|
138
|
+
|
|
136
139
|
// recoverOnTimeout: false ensures that if the captcha solve times out,
|
|
137
140
|
// withAccountPage does NOT destructively kill the browser context, which
|
|
138
141
|
// previously caused "Target page, context or browser has been closed" mid-slider.
|
|
139
142
|
const solved = await withAccountPage(
|
|
140
143
|
accountId,
|
|
141
|
-
(page) =>
|
|
144
|
+
async (page) => {
|
|
145
|
+
// If another stream is actively running on this account's primary page,
|
|
146
|
+
// NEVER navigate `page` away (which would unload its DOM and kill the stream).
|
|
147
|
+
// Instead, try in-place modal solve first, or open an ephemeral tab in the
|
|
148
|
+
// same BrowserContext so cookies/x5sec are solved without touching `page`.
|
|
149
|
+
if (hasConcurrentStream) {
|
|
150
|
+
const solverOptions = {
|
|
151
|
+
maxAttempts: config.captcha.maxAttempts,
|
|
152
|
+
retryDelayMs: config.captcha.retryDelayMs,
|
|
153
|
+
settleMs: config.captcha.settleMs,
|
|
154
|
+
sliderTimeoutMs: config.captcha.sliderTimeoutMs,
|
|
155
|
+
};
|
|
156
|
+
if (await solveVisibleCaptcha(page, { ...solverOptions, waitForMs: 0 })) {
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
const ctx = typeof page.context === "function" ? page.context() : null;
|
|
160
|
+
if (ctx && typeof ctx.newPage === "function") {
|
|
161
|
+
const tempPage = await ctx.newPage();
|
|
162
|
+
try {
|
|
163
|
+
return await solveChallengeOnPage(tempPage, challengeUrl);
|
|
164
|
+
} finally {
|
|
165
|
+
await tempPage.close({ runBeforeUnload: false }).catch(() => {});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
return await solveChallengeOnPage(page, challengeUrl);
|
|
171
|
+
},
|
|
142
172
|
solverOperationTimeoutMs,
|
|
143
173
|
solverOperationTimeoutMs,
|
|
144
174
|
false,
|