qwenproxy-cli 1.6.9 → 1.6.11
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 +199 -55
- package/dist/delete-chats.js +269 -78
- package/dist/index.js +290 -88
- package/dist/login.js +203 -67
- package/dist/reset-cooldowns.js +199 -55
- package/package.json +2 -2
- package/src/api/server.ts +8 -4
- 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/captcha-coordinator.ts +36 -8
- package/src/services/chat-cleanup.ts +11 -2
- package/src/services/playwright.ts +244 -74
- package/src/services/qwen.ts +29 -3
package/dist/reset-cooldowns.js
CHANGED
|
@@ -1236,6 +1236,26 @@ function clearAllAccountCooldowns() {
|
|
|
1236
1236
|
cooldowns.clear();
|
|
1237
1237
|
return count;
|
|
1238
1238
|
}
|
|
1239
|
+
function getAccountCooldownInfo(accountId) {
|
|
1240
|
+
const entry = cooldowns.get(accountId);
|
|
1241
|
+
if (!entry) return null;
|
|
1242
|
+
const remaining = entry.until - Date.now();
|
|
1243
|
+
if (remaining <= 0) {
|
|
1244
|
+
cooldowns.delete(accountId);
|
|
1245
|
+
if (accountId !== "global") {
|
|
1246
|
+
try {
|
|
1247
|
+
updateAccountCooldown(accountId, 0, null);
|
|
1248
|
+
} catch (err) {
|
|
1249
|
+
console.error(
|
|
1250
|
+
`\u274C [AccountManager] Failed to clear expired cooldown in DB:`,
|
|
1251
|
+
err.message
|
|
1252
|
+
);
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
return null;
|
|
1256
|
+
}
|
|
1257
|
+
return { onCooldown: true, remainingMs: remaining, reason: entry.reason };
|
|
1258
|
+
}
|
|
1239
1259
|
function markAccountHeadersReady(accountId) {
|
|
1240
1260
|
if (!accountId || accountId === "global") return;
|
|
1241
1261
|
headersReadyAccounts.add(accountId);
|
|
@@ -3256,7 +3276,19 @@ async function isPageLoggedIn(page, timeoutMs = SESSION_PROBE_NAVIGATION_TIMEOUT
|
|
|
3256
3276
|
if (localStorage.getItem("qwen_token_logged_out_marker")) {
|
|
3257
3277
|
return false;
|
|
3258
3278
|
}
|
|
3259
|
-
const
|
|
3279
|
+
const lsToken = (localStorage.getItem("token") || "").trim();
|
|
3280
|
+
if (lsToken && !/(?:^|;\s*)token=/.test(document.cookie || "")) {
|
|
3281
|
+
try {
|
|
3282
|
+
document.cookie = `token=${encodeURIComponent(lsToken)}; path=/; domain=.qwen.ai; max-age=31536000`;
|
|
3283
|
+
} catch {
|
|
3284
|
+
}
|
|
3285
|
+
}
|
|
3286
|
+
const authHeaders = lsToken ? { Authorization: `Bearer ${lsToken}` } : {};
|
|
3287
|
+
const res = await fetch("/api/v1/auths/", {
|
|
3288
|
+
method: "GET",
|
|
3289
|
+
headers: authHeaders,
|
|
3290
|
+
credentials: "include"
|
|
3291
|
+
});
|
|
3260
3292
|
if (res.status !== 200) return false;
|
|
3261
3293
|
const json = await res.json().catch(() => null);
|
|
3262
3294
|
if (!json) return false;
|
|
@@ -3274,6 +3306,7 @@ async function isPageLoggedIn(page, timeoutMs = SESSION_PROBE_NAVIGATION_TIMEOUT
|
|
|
3274
3306
|
try {
|
|
3275
3307
|
const settingsRes = await fetch("/api/v2/users/user/settings", {
|
|
3276
3308
|
method: "GET",
|
|
3309
|
+
headers: authHeaders,
|
|
3277
3310
|
credentials: "include",
|
|
3278
3311
|
signal: AbortSignal.timeout(3e3)
|
|
3279
3312
|
});
|
|
@@ -3876,8 +3909,12 @@ async function initPlaywrightForAccount(rawAccount, headless = config.playwright
|
|
|
3876
3909
|
if (accountPages.has(account.id)) {
|
|
3877
3910
|
return;
|
|
3878
3911
|
}
|
|
3879
|
-
|
|
3880
|
-
|
|
3912
|
+
if (config.playwright.maxActiveContexts > 0) {
|
|
3913
|
+
await evictIdlePlaywrightContextsToLimit(
|
|
3914
|
+
Math.max(1, config.playwright.maxActiveContexts - 1)
|
|
3915
|
+
).catch(() => {
|
|
3916
|
+
});
|
|
3917
|
+
}
|
|
3881
3918
|
const profilePath = getAccountProfilePath(account.id);
|
|
3882
3919
|
const fingerprint = getFingerprintProfile(account.id);
|
|
3883
3920
|
const { engine, channel } = resolveBrowserEngine(browserType);
|
|
@@ -4102,8 +4139,15 @@ async function initPlaywrightForAccount(rawAccount, headless = config.playwright
|
|
|
4102
4139
|
const hasAuthCookie = cookies.some(
|
|
4103
4140
|
(c) => c.name.toLowerCase().includes("token") || c.name.toLowerCase().includes("session")
|
|
4104
4141
|
);
|
|
4142
|
+
let initialLoginAttempted = false;
|
|
4105
4143
|
if (!hasAuthCookie && account.email && account.password) {
|
|
4106
|
-
|
|
4144
|
+
initialLoginAttempted = true;
|
|
4145
|
+
const loggedIn = await loginToQwen(account.id, account.email, account.password);
|
|
4146
|
+
if (!loggedIn) {
|
|
4147
|
+
throw new Error(
|
|
4148
|
+
`Authentication failed for account ${maskEmail(account.email)}`
|
|
4149
|
+
);
|
|
4150
|
+
}
|
|
4107
4151
|
}
|
|
4108
4152
|
try {
|
|
4109
4153
|
await acctPage.goto(qwenUrl("/"), {
|
|
@@ -4113,13 +4157,21 @@ async function initPlaywrightForAccount(rawAccount, headless = config.playwright
|
|
|
4113
4157
|
await sleep2(1500);
|
|
4114
4158
|
const currentUrl = acctPage.url();
|
|
4115
4159
|
const isAuthUrl = currentUrl.includes("/auth") || currentUrl.includes("/login");
|
|
4116
|
-
if (isAuthUrl && account.email && account.password) {
|
|
4160
|
+
if (isAuthUrl && !initialLoginAttempted && account.email && account.password) {
|
|
4117
4161
|
console.log(
|
|
4118
4162
|
`[Playwright] Initial login needed for ${maskEmail(account.email)}, authenticating...`
|
|
4119
4163
|
);
|
|
4120
|
-
await loginToQwen(account.id, account.email, account.password);
|
|
4164
|
+
const loggedIn = await loginToQwen(account.id, account.email, account.password);
|
|
4165
|
+
if (!loggedIn) {
|
|
4166
|
+
throw new Error(
|
|
4167
|
+
`Authentication failed for account ${maskEmail(account.email)}`
|
|
4168
|
+
);
|
|
4169
|
+
}
|
|
4121
4170
|
}
|
|
4122
4171
|
} catch (err) {
|
|
4172
|
+
if (err?.message?.includes("Authentication failed for account")) {
|
|
4173
|
+
throw err;
|
|
4174
|
+
}
|
|
4123
4175
|
console.warn(
|
|
4124
4176
|
`\u26A0\uFE0F [Playwright] Initial navigation check for ${maskEmail(account.email)}: ${err.message}`
|
|
4125
4177
|
);
|
|
@@ -4131,7 +4183,7 @@ async function initPlaywrightForAccount(rawAccount, headless = config.playwright
|
|
|
4131
4183
|
if (!acctPage.isClosed()) {
|
|
4132
4184
|
try {
|
|
4133
4185
|
const currentUrl = new URL(acctPage.url());
|
|
4134
|
-
if (currentUrl.origin !== qwenOrigin() || currentUrl.pathname
|
|
4186
|
+
if (currentUrl.origin !== qwenOrigin() || currentUrl.pathname.includes("/auth") || currentUrl.pathname.includes("/login") || currentUrl.pathname.includes("_____tmd_____")) {
|
|
4135
4187
|
await acctPage.goto(qwenUrl("/"), {
|
|
4136
4188
|
waitUntil: "domcontentloaded",
|
|
4137
4189
|
timeout: config.timeouts.navigation
|
|
@@ -4149,6 +4201,8 @@ async function initPlaywrightForAccount(rawAccount, headless = config.playwright
|
|
|
4149
4201
|
} finally {
|
|
4150
4202
|
release();
|
|
4151
4203
|
}
|
|
4204
|
+
await evictIdlePlaywrightContextsToLimit().catch(() => {
|
|
4205
|
+
});
|
|
4152
4206
|
})();
|
|
4153
4207
|
try {
|
|
4154
4208
|
await initPromise;
|
|
@@ -4262,10 +4316,30 @@ async function loginToQwen(accountId, email, password) {
|
|
|
4262
4316
|
const uiResult = await loginViaUi(page, email, password);
|
|
4263
4317
|
if (uiResult.success) {
|
|
4264
4318
|
cookieCaches.delete(accountId);
|
|
4265
|
-
await saveStorageState(page.context(), accountId);
|
|
4266
4319
|
try {
|
|
4267
|
-
|
|
4268
|
-
|
|
4320
|
+
let liveCookies = await page.context().cookies();
|
|
4321
|
+
let tokenCookie = liveCookies.find((c) => c.name === "token");
|
|
4322
|
+
if (!tokenCookie) {
|
|
4323
|
+
const lsTok = uiResult.token || await page.evaluate(() => (localStorage.getItem("token") || "").trim()).catch(() => "");
|
|
4324
|
+
if (lsTok) {
|
|
4325
|
+
await page.context().addCookies([
|
|
4326
|
+
{
|
|
4327
|
+
name: "token",
|
|
4328
|
+
value: lsTok,
|
|
4329
|
+
domain: ".qwen.ai",
|
|
4330
|
+
path: "/",
|
|
4331
|
+
expires: Math.floor(Date.now() / 1e3) + 31536e3,
|
|
4332
|
+
httpOnly: false,
|
|
4333
|
+
secure: true,
|
|
4334
|
+
sameSite: "Lax"
|
|
4335
|
+
}
|
|
4336
|
+
]).catch(() => {
|
|
4337
|
+
});
|
|
4338
|
+
liveCookies = await page.context().cookies();
|
|
4339
|
+
tokenCookie = liveCookies.find((c) => c.name === "token");
|
|
4340
|
+
}
|
|
4341
|
+
}
|
|
4342
|
+
await saveStorageState(page.context(), accountId);
|
|
4269
4343
|
if (tokenCookie) {
|
|
4270
4344
|
const cookieStr = liveCookies.map((c) => `${c.name}=${c.value}`).join("; ");
|
|
4271
4345
|
const cache = getHeaderCache(accountId);
|
|
@@ -4274,7 +4348,7 @@ async function loginToQwen(accountId, email, password) {
|
|
|
4274
4348
|
markAccountHeadersReady(accountId);
|
|
4275
4349
|
const { saveAuthSession: saveAuthSession2 } = await Promise.resolve().then(() => (init_database(), database_exports));
|
|
4276
4350
|
const exp = parseJwtExpiry(tokenCookie.value);
|
|
4277
|
-
let rTok = uiResult.refreshToken;
|
|
4351
|
+
let rTok = uiResult.refreshToken || liveCookies.find((c) => c.name === "refresh_token")?.value;
|
|
4278
4352
|
if (!rTok) {
|
|
4279
4353
|
try {
|
|
4280
4354
|
rTok = await page.evaluate(() => localStorage.getItem("refresh_token") || "") || void 0;
|
|
@@ -4355,8 +4429,9 @@ async function loginViaApi(page, email, password) {
|
|
|
4355
4429
|
}
|
|
4356
4430
|
const signinUrl = "https://auth.qwen.ai/api/v2/auths/signin";
|
|
4357
4431
|
const requestId = crypto3.randomUUID();
|
|
4432
|
+
const webVersion = getQwenWebVersion() || "0.3.26";
|
|
4358
4433
|
const evalRes = await page.evaluate(
|
|
4359
|
-
async ({ email: email2, password: password2, signinUrl: signinUrl2, requestId: requestId2, tz }) => {
|
|
4434
|
+
async ({ email: email2, password: password2, signinUrl: signinUrl2, requestId: requestId2, tz, version }) => {
|
|
4360
4435
|
try {
|
|
4361
4436
|
const response = await fetch(signinUrl2, {
|
|
4362
4437
|
method: "POST",
|
|
@@ -4366,7 +4441,7 @@ async function loginViaApi(page, email, password) {
|
|
|
4366
4441
|
accept: "application/json, text/plain, */*",
|
|
4367
4442
|
"content-type": "application/json",
|
|
4368
4443
|
source: "web",
|
|
4369
|
-
version
|
|
4444
|
+
version,
|
|
4370
4445
|
"x-request-origin": "https://chat.qwen.ai",
|
|
4371
4446
|
timezone: tz,
|
|
4372
4447
|
"x-request-id": requestId2
|
|
@@ -4379,7 +4454,14 @@ async function loginViaApi(page, email, password) {
|
|
|
4379
4454
|
return { ok: false, error: e.message };
|
|
4380
4455
|
}
|
|
4381
4456
|
},
|
|
4382
|
-
{
|
|
4457
|
+
{
|
|
4458
|
+
email,
|
|
4459
|
+
password: hashedPassword,
|
|
4460
|
+
signinUrl,
|
|
4461
|
+
requestId,
|
|
4462
|
+
tz: formatAsciiTimezone(),
|
|
4463
|
+
version: webVersion
|
|
4464
|
+
}
|
|
4383
4465
|
).catch(() => null);
|
|
4384
4466
|
let data = evalRes?.data;
|
|
4385
4467
|
let signinSuccess = Boolean(
|
|
@@ -4388,7 +4470,7 @@ async function loginViaApi(page, email, password) {
|
|
|
4388
4470
|
if (!signinSuccess && !data?.data?.code) {
|
|
4389
4471
|
const fallbackUrl = qwenUrl("/api/v2/auths/signin");
|
|
4390
4472
|
const fallbackRes = await page.evaluate(
|
|
4391
|
-
async ({ email: email2, password: password2, fallbackUrl: fallbackUrl2, requestId: requestId2, tz }) => {
|
|
4473
|
+
async ({ email: email2, password: password2, fallbackUrl: fallbackUrl2, requestId: requestId2, tz, version }) => {
|
|
4392
4474
|
try {
|
|
4393
4475
|
const response = await fetch(fallbackUrl2, {
|
|
4394
4476
|
method: "POST",
|
|
@@ -4398,7 +4480,7 @@ async function loginViaApi(page, email, password) {
|
|
|
4398
4480
|
accept: "application/json, text/plain, */*",
|
|
4399
4481
|
"content-type": "application/json",
|
|
4400
4482
|
source: "web",
|
|
4401
|
-
version
|
|
4483
|
+
version,
|
|
4402
4484
|
timezone: tz,
|
|
4403
4485
|
"x-request-id": requestId2
|
|
4404
4486
|
},
|
|
@@ -4410,7 +4492,14 @@ async function loginViaApi(page, email, password) {
|
|
|
4410
4492
|
return { ok: false, error: e.message };
|
|
4411
4493
|
}
|
|
4412
4494
|
},
|
|
4413
|
-
{
|
|
4495
|
+
{
|
|
4496
|
+
email,
|
|
4497
|
+
password: hashedPassword,
|
|
4498
|
+
fallbackUrl,
|
|
4499
|
+
requestId,
|
|
4500
|
+
tz: formatAsciiTimezone(),
|
|
4501
|
+
version: webVersion
|
|
4502
|
+
}
|
|
4414
4503
|
).catch(() => null);
|
|
4415
4504
|
if (fallbackRes?.data) {
|
|
4416
4505
|
data = fallbackRes.data;
|
|
@@ -4635,28 +4724,34 @@ async function loginViaUi(page, email, password) {
|
|
|
4635
4724
|
await sleep2(500);
|
|
4636
4725
|
const submitSelector = 'button.qwenchat-auth-pc-submit-button, button[type="submit"], button:has-text("Log in"), button:has-text("Sign in"), button:has-text("Fazer login"), button:has-text("Entrar"), button:has-text("Iniciar sesi\xF3n"), button:has-text("\u767B\u5F55")';
|
|
4637
4726
|
const submitButton = page.locator(submitSelector).first();
|
|
4638
|
-
|
|
4639
|
-
try {
|
|
4640
|
-
const btn = document.querySelector(sel);
|
|
4641
|
-
if (btn) {
|
|
4642
|
-
btn.removeAttribute("disabled");
|
|
4643
|
-
btn.classList.remove("ant-btn-disabled");
|
|
4644
|
-
btn.click();
|
|
4645
|
-
}
|
|
4646
|
-
} catch {
|
|
4647
|
-
}
|
|
4648
|
-
}, submitSelector).catch(() => {
|
|
4649
|
-
});
|
|
4727
|
+
let submitted = false;
|
|
4650
4728
|
try {
|
|
4651
4729
|
if (await submitButton.isVisible({ timeout: 2e3 }).catch(() => false)) {
|
|
4652
|
-
await submitButton.click({
|
|
4730
|
+
await submitButton.click({ timeout: 2e3 }).catch(() => {
|
|
4653
4731
|
});
|
|
4732
|
+
submitted = true;
|
|
4654
4733
|
}
|
|
4655
4734
|
} catch {
|
|
4656
4735
|
}
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
4736
|
+
if (!submitted) {
|
|
4737
|
+
await page.evaluate(() => {
|
|
4738
|
+
try {
|
|
4739
|
+
const btn = document.querySelector(
|
|
4740
|
+
'button.qwenchat-auth-pc-submit-button, button[type="submit"]'
|
|
4741
|
+
);
|
|
4742
|
+
if (btn) {
|
|
4743
|
+
btn.removeAttribute("disabled");
|
|
4744
|
+
btn.classList.remove("ant-btn-disabled");
|
|
4745
|
+
btn.click();
|
|
4746
|
+
}
|
|
4747
|
+
} catch {
|
|
4748
|
+
}
|
|
4749
|
+
}).catch(() => {
|
|
4750
|
+
});
|
|
4751
|
+
const passInput = page.locator(passwordSelector).first();
|
|
4752
|
+
await passInput.press("Enter", { timeout: 2e3 }).catch(() => {
|
|
4753
|
+
});
|
|
4754
|
+
}
|
|
4660
4755
|
const postSubmitDeadline = Date.now() + 15e3;
|
|
4661
4756
|
while (Date.now() < postSubmitDeadline) {
|
|
4662
4757
|
if (page.isClosed()) break;
|
|
@@ -4674,9 +4769,12 @@ async function loginViaUi(page, email, password) {
|
|
|
4674
4769
|
".ant-form-item-explain-error",
|
|
4675
4770
|
".ant-message-error",
|
|
4676
4771
|
".ant-message-notice",
|
|
4772
|
+
".ant-message-custom-content",
|
|
4773
|
+
".ant-notification-notice",
|
|
4677
4774
|
'[role="alert"]',
|
|
4678
4775
|
".qwenchat-auth-error",
|
|
4679
|
-
".auth-error-message"
|
|
4776
|
+
".auth-error-message",
|
|
4777
|
+
".qwenchat-toast"
|
|
4680
4778
|
].join(", ");
|
|
4681
4779
|
const errorEl = page.locator(errorSelector).first();
|
|
4682
4780
|
if (await errorEl.isVisible().catch(() => false)) {
|
|
@@ -4690,7 +4788,15 @@ async function loginViaUi(page, email, password) {
|
|
|
4690
4788
|
};
|
|
4691
4789
|
}
|
|
4692
4790
|
}
|
|
4693
|
-
const codeSelector =
|
|
4791
|
+
const codeSelector = [
|
|
4792
|
+
'input[placeholder*="code" i]',
|
|
4793
|
+
'input[placeholder*="c\xF3digo" i]',
|
|
4794
|
+
'input[name*="code" i]',
|
|
4795
|
+
".qwenchat-verification-code-input",
|
|
4796
|
+
".qwenchat-verification-code-input-cell",
|
|
4797
|
+
'[aria-label*="Verification code" i]',
|
|
4798
|
+
'[aria-label*="c\xF3digo de verifica\xE7\xE3o" i]'
|
|
4799
|
+
].join(", ");
|
|
4694
4800
|
const codeInput = page.locator(codeSelector).first();
|
|
4695
4801
|
if (await codeInput.isVisible().catch(() => false)) {
|
|
4696
4802
|
return {
|
|
@@ -4700,16 +4806,44 @@ async function loginViaUi(page, email, password) {
|
|
|
4700
4806
|
}
|
|
4701
4807
|
try {
|
|
4702
4808
|
const liveCookies = await page.context().cookies();
|
|
4703
|
-
|
|
4704
|
-
|
|
4809
|
+
const rTokCookie = liveCookies.find((c) => c.name === "refresh_token")?.value;
|
|
4810
|
+
const tokCookie = liveCookies.find((c) => c.name === "token")?.value;
|
|
4811
|
+
if (tokCookie) {
|
|
4812
|
+
return { success: true, token: tokCookie, refreshToken: rTokCookie };
|
|
4705
4813
|
}
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4814
|
+
const currentUrl = page.url();
|
|
4815
|
+
if (!currentUrl.includes("/auth") && !currentUrl.includes("/login")) {
|
|
4816
|
+
const lsTok = await page.evaluate(() => {
|
|
4817
|
+
const t = (localStorage.getItem("token") || "").trim();
|
|
4818
|
+
if (t) {
|
|
4819
|
+
try {
|
|
4820
|
+
document.cookie = `token=${encodeURIComponent(t)}; path=/; domain=.qwen.ai; max-age=31536000`;
|
|
4821
|
+
} catch {
|
|
4822
|
+
}
|
|
4823
|
+
}
|
|
4824
|
+
return t;
|
|
4825
|
+
}).catch(() => "");
|
|
4826
|
+
if (lsTok) {
|
|
4827
|
+
await page.context().addCookies([
|
|
4828
|
+
{
|
|
4829
|
+
name: "token",
|
|
4830
|
+
value: lsTok,
|
|
4831
|
+
domain: ".qwen.ai",
|
|
4832
|
+
path: "/",
|
|
4833
|
+
expires: Math.floor(Date.now() / 1e3) + 31536e3,
|
|
4834
|
+
httpOnly: false,
|
|
4835
|
+
secure: true,
|
|
4836
|
+
sameSite: "Lax"
|
|
4837
|
+
}
|
|
4838
|
+
]).catch(() => {
|
|
4839
|
+
});
|
|
4840
|
+
return { success: true, token: lsTok, refreshToken: rTokCookie };
|
|
4841
|
+
}
|
|
4842
|
+
if (await isPageLoggedIn(page, 3e3)) {
|
|
4843
|
+
return { success: true, refreshToken: rTokCookie };
|
|
4844
|
+
}
|
|
4712
4845
|
}
|
|
4846
|
+
} catch {
|
|
4713
4847
|
}
|
|
4714
4848
|
await sleep2(1e3);
|
|
4715
4849
|
}
|
|
@@ -4739,10 +4873,18 @@ async function refreshAccountToken(accountId) {
|
|
|
4739
4873
|
} catch {
|
|
4740
4874
|
}
|
|
4741
4875
|
}
|
|
4876
|
+
if (!refreshToken) {
|
|
4877
|
+
try {
|
|
4878
|
+
const liveCookies = await page.context().cookies();
|
|
4879
|
+
refreshToken = liveCookies.find((c) => c.name === "refresh_token")?.value;
|
|
4880
|
+
} catch {
|
|
4881
|
+
}
|
|
4882
|
+
}
|
|
4742
4883
|
if (refreshToken) {
|
|
4743
4884
|
try {
|
|
4885
|
+
const webVersion = getQwenWebVersion() || "0.3.26";
|
|
4744
4886
|
const refreshResult = await page.evaluate(
|
|
4745
|
-
async ({ rTok, tz }) => {
|
|
4887
|
+
async ({ rTok, tz, version }) => {
|
|
4746
4888
|
try {
|
|
4747
4889
|
const response = await fetch("https://auth.qwen.ai/api/v2/auths/refresh", {
|
|
4748
4890
|
method: "GET",
|
|
@@ -4750,7 +4892,7 @@ async function refreshAccountToken(accountId) {
|
|
|
4750
4892
|
headers: {
|
|
4751
4893
|
accept: "application/json, text/plain, */*",
|
|
4752
4894
|
source: "web",
|
|
4753
|
-
version
|
|
4895
|
+
version,
|
|
4754
4896
|
"x-request-origin": "https://chat.qwen.ai",
|
|
4755
4897
|
timezone: tz,
|
|
4756
4898
|
authorization: `Bearer ${rTok}`
|
|
@@ -4762,7 +4904,7 @@ async function refreshAccountToken(accountId) {
|
|
|
4762
4904
|
return { ok: false, error: e.message };
|
|
4763
4905
|
}
|
|
4764
4906
|
},
|
|
4765
|
-
{ rTok: refreshToken, tz: formatAsciiTimezone() }
|
|
4907
|
+
{ rTok: refreshToken, tz: formatAsciiTimezone(), version: webVersion }
|
|
4766
4908
|
);
|
|
4767
4909
|
const json = refreshResult?.data;
|
|
4768
4910
|
if (json && json.success === true && json.data) {
|
|
@@ -5563,7 +5705,11 @@ function priorityOrderForEviction(accountIds) {
|
|
|
5563
5705
|
const ranked = getAccountsByPriority(accountIds.map((id) => ({ id })));
|
|
5564
5706
|
const rank = /* @__PURE__ */ new Map();
|
|
5565
5707
|
for (const [index, account] of ranked.entries()) {
|
|
5566
|
-
|
|
5708
|
+
if (getAccountCooldownInfo(account.id)) {
|
|
5709
|
+
rank.set(account.id, Number.MAX_SAFE_INTEGER);
|
|
5710
|
+
} else {
|
|
5711
|
+
rank.set(account.id, index);
|
|
5712
|
+
}
|
|
5567
5713
|
}
|
|
5568
5714
|
return orderContextsForEviction(
|
|
5569
5715
|
accountIds,
|
|
@@ -5571,14 +5717,15 @@ function priorityOrderForEviction(accountIds) {
|
|
|
5571
5717
|
(id) => lastAccountActivity.get(id) ?? 0
|
|
5572
5718
|
);
|
|
5573
5719
|
}
|
|
5574
|
-
async function evictIdlePlaywrightContextsToLimit() {
|
|
5575
|
-
const
|
|
5576
|
-
if (
|
|
5720
|
+
async function evictIdlePlaywrightContextsToLimit(targetLimit) {
|
|
5721
|
+
const configuredMax = config.playwright.maxActiveContexts;
|
|
5722
|
+
if (configuredMax <= 0) return 0;
|
|
5723
|
+
const max = targetLimit !== void 0 ? Math.max(0, targetLimit) : configuredMax;
|
|
5577
5724
|
if (accountPages.size <= max) return 0;
|
|
5578
5725
|
const candidates = priorityOrderForEviction(
|
|
5579
5726
|
Array.from(accountPages.keys()).filter((accountId) => {
|
|
5580
5727
|
if (isAccountServingStream(accountId)) return false;
|
|
5581
|
-
if (
|
|
5728
|
+
if (hasActiveAccountLease(accountId)) return false;
|
|
5582
5729
|
const mutex = accountMutexes.get(accountId);
|
|
5583
5730
|
return mutex?.isIdle();
|
|
5584
5731
|
})
|
|
@@ -5591,6 +5738,7 @@ async function evictIdlePlaywrightContextsToLimit() {
|
|
|
5591
5738
|
for (const candidate of candidates) {
|
|
5592
5739
|
if (accountPages.size <= max) break;
|
|
5593
5740
|
if (isAccountServingStream(candidate.accountId)) continue;
|
|
5741
|
+
if (hasActiveAccountLease(candidate.accountId)) continue;
|
|
5594
5742
|
const mutex = accountMutexes.get(candidate.accountId);
|
|
5595
5743
|
if (!mutex?.isIdle()) continue;
|
|
5596
5744
|
await closePlaywrightForAccount(candidate.accountId).catch((error) => {
|
|
@@ -5693,10 +5841,6 @@ async function closePlaywrightForAccount(accountId) {
|
|
|
5693
5841
|
release();
|
|
5694
5842
|
}
|
|
5695
5843
|
}
|
|
5696
|
-
function isAccountRecentlyActive(accountId, maxIdleMs = 3e5) {
|
|
5697
|
-
const last = lastAccountActivity.get(accountId) ?? 0;
|
|
5698
|
-
return last > 0 && Date.now() - last < maxIdleMs;
|
|
5699
|
-
}
|
|
5700
5844
|
var requireLocal, contextInitHooks, ACCOUNT_MUTEX_MAX_HOLD_MS, accountMutexes, accountContexts, accountPages, inFlightAccountInits, headerCaches, HEADER_CACHE_TTL, COOKIE_CACHE_TTL, cookieCaches, lastAccountActivity, lastKeepAliveNavigation, lastProactiveTokenRefresh, profileResetQueue, profileResetChain, closingAllPlaywright, sleep2, HEADER_CAPTURE_SETTLE_MS, PLAYWRIGHT_MUTEX_WAIT_MS, ACCOUNT_PAGE_OPERATION_TIMEOUT_MS, SESSION_PROBE_NAVIGATION_TIMEOUT_MS, HEADER_CAPTURE_TRIGGER_GRACE_MS, FIRST_TRIGGER_GRACE_MS, HEADER_CAPTURE_TRIGGER_ATTEMPTS, CHAT_INPUT_APPEAR_TIMEOUT_MS, CHAT_INPUT_ACTION_TIMEOUT_MS, PROFILE_RESET_TIMEOUT_MS;
|
|
5701
5845
|
var init_playwright = __esm({
|
|
5702
5846
|
"src/services/playwright.ts"() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qwenproxy-cli",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.11",
|
|
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/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 {
|
|
@@ -92,6 +92,12 @@ export async function solveChallengeOnPage(
|
|
|
92
92
|
* a punish document that is never rendered; opening that document in the page
|
|
93
93
|
* is what gives the solver a slider to drive.
|
|
94
94
|
*/
|
|
95
|
+
export function isCaptchaRecoveryInBackoff(accountId: string | undefined): boolean {
|
|
96
|
+
if (!accountId) return false;
|
|
97
|
+
const lastFailure = lastFailedRecoveryAt.get(accountId) ?? 0;
|
|
98
|
+
return Date.now() - lastFailure < FAILED_RECOVERY_BACKOFF_MS;
|
|
99
|
+
}
|
|
100
|
+
|
|
95
101
|
export async function recoverBaxiaCaptcha(
|
|
96
102
|
accountId: string | undefined,
|
|
97
103
|
label: string,
|
|
@@ -126,19 +132,41 @@ export async function recoverBaxiaCaptcha(
|
|
|
126
132
|
|
|
127
133
|
activeCaptchaRecoveries.add(accountId);
|
|
128
134
|
try {
|
|
129
|
-
const {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
"Account stream interrupted by captcha challenge recovery",
|
|
133
|
-
);
|
|
134
|
-
} catch {}
|
|
135
|
-
try {
|
|
135
|
+
const { hasActiveBrowserStreamsForAccount } = await import("./qwen.ts");
|
|
136
|
+
const hasConcurrentStream = hasActiveBrowserStreamsForAccount(accountId);
|
|
137
|
+
|
|
136
138
|
// recoverOnTimeout: false ensures that if the captcha solve times out,
|
|
137
139
|
// withAccountPage does NOT destructively kill the browser context, which
|
|
138
140
|
// previously caused "Target page, context or browser has been closed" mid-slider.
|
|
139
141
|
const solved = await withAccountPage(
|
|
140
142
|
accountId,
|
|
141
|
-
(page) =>
|
|
143
|
+
async (page) => {
|
|
144
|
+
// If another stream is actively running on this account's primary page,
|
|
145
|
+
// NEVER navigate `page` away (which would unload its DOM and kill the stream).
|
|
146
|
+
// Instead, try in-place modal solve first, or open an ephemeral tab in the
|
|
147
|
+
// same BrowserContext so cookies/x5sec are solved without touching `page`.
|
|
148
|
+
if (hasConcurrentStream) {
|
|
149
|
+
const solverOptions = {
|
|
150
|
+
maxAttempts: config.captcha.maxAttempts,
|
|
151
|
+
retryDelayMs: config.captcha.retryDelayMs,
|
|
152
|
+
settleMs: config.captcha.settleMs,
|
|
153
|
+
};
|
|
154
|
+
if (await solveBaxiaCaptcha(page, { ...solverOptions, waitForMs: 0 })) {
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
const ctx = typeof page.context === "function" ? page.context() : null;
|
|
158
|
+
if (ctx && typeof ctx.newPage === "function") {
|
|
159
|
+
const tempPage = await ctx.newPage();
|
|
160
|
+
try {
|
|
161
|
+
return await solveChallengeOnPage(tempPage, challengeUrl);
|
|
162
|
+
} finally {
|
|
163
|
+
await tempPage.close({ runBeforeUnload: false }).catch(() => {});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
return await solveChallengeOnPage(page, challengeUrl);
|
|
169
|
+
},
|
|
142
170
|
solverOperationTimeoutMs,
|
|
143
171
|
solverOperationTimeoutMs,
|
|
144
172
|
false,
|