qwenproxy-cli 1.4.0 → 1.4.2
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/README.pt-BR.md +7 -5
- package/dist/clean-cache.js +167 -68
- package/dist/delete-chats.js +212 -88
- package/dist/index.js +392 -267
- package/dist/login.js +172 -73
- package/dist/reset-cooldowns.js +167 -68
- package/dist/sync-clients.js +136 -126
- package/package.json +8 -6
- package/src/core/config.ts +13 -0
- package/src/core/version.ts +1 -1
- package/src/routes/chat/index.ts +1 -0
- package/src/services/playwright.ts +137 -43
- package/src/services/qwen.ts +47 -19
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qwenproxy-cli",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.2",
|
|
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": {
|
|
@@ -32,6 +32,8 @@
|
|
|
32
32
|
"test:mock": "tsx --test --test-concurrency=1 --env-file=.env.test src/tests/account-concurrency.test.ts src/tests/concurrency-latest-wins.test.ts src/tests/mutex.test.ts src/tests/context-truncation.test.ts src/tests/streaming-thinking-summary.test.ts src/tests/tool-call-guard.test.ts src/tests/utils-logger-full.test.ts src/tests/logger-toolcall-errors.test.ts src/tests/logger-toolcall-disabled.test.ts src/tests/utils-json-full.test.ts src/tests/utils-session-guard-full.test.ts src/tests/core-logic-full.test.ts src/tests/core-logic-accounts.test.ts src/tests/chat-validation-full.test.ts src/tests/chat-logic-full.test.ts src/tests/account-stickiness.test.ts src/tests/accounts-security.test.ts src/tests/advanced.test.ts src/tests/auth-playwright.test.ts src/tests/auth-session-persistence.test.ts src/tests/captcha-solver.test.ts src/tests/cache-scan-regex.test.ts src/tests/chat-cleanup.test.ts src/tests/chat-cleanup-auto.test.ts src/tests/chromium-args.test.ts src/tests/config-current.test.ts src/tests/context-meter.test.ts src/tests/concurrency.test.ts src/tests/delta.test.ts src/tests/fingerprint.test.ts src/tests/human-behavior.test.ts src/tests/index.test.ts src/tests/json-recovery.test.ts src/tests/jsonFix.test.ts src/tests/memory-cache.test.ts src/tests/model-alias.test.ts src/tests/model-registry-thinking.test.ts src/tests/models-endpoint.test.ts src/tests/multimodal.test.ts src/tests/oss-upload.test.ts src/tests/parser.test.ts src/tests/personalization-flow.test.ts src/tests/personalization-required.test.ts src/tests/retry-policy.test.ts src/tests/rotation.test.ts src/tests/server-lifecycle.test.ts src/tests/session-keeper.test.ts src/tests/session-state.test.ts src/tests/single-account-mode.test.ts src/tests/stream-retry.test.ts src/tests/tool-calls-endpoint.test.ts src/tests/tool-instructions-cache.test.ts src/tests/session-id.test.ts src/tests/watchdog-ram.test.ts src/tests/tool-recovery-t1t2.test.ts src/tests/abort-silent-t4.test.ts src/tests/t3-close-tag-escape.test.ts src/tests/t5-tool-cap.test.ts src/tests/logger-redaction.test.ts src/tests/stall-timeout-retryable.test.ts src/tests/t6-missing-quote-array-close.test.ts src/tests/watchdog-rss.test.ts src/tests/logical-thread-batch.test.ts src/tests/chat-pool-style.test.ts src/tests/queue-wait-cap.test.ts src/tests/context-eviction-order.test.ts src/tests/chat-in-progress-settle.test.ts src/tests/stream-lock-leak.test.ts src/tests/stream-semaphore.test.ts src/tests/waf-isolation.test.ts src/tests/legacy-completions.test.ts src/tests/response-format.test.ts src/tests/cors-headers.test.ts src/tests/local-auth.test.ts src/tests/safe-remote-url.test.ts src/tests/stream-lock-terminal-cancel.test.ts src/tests/parser-truncated-tool-call.test.ts src/tests/metrics-prometheus.test.ts src/tests/context-death-cleanup.test.ts src/tests/personalization-direct-fetch.test.ts src/tests/completion-headers-parity.test.ts src/tests/parallel-escape-repro.test.ts src/tests/chat-in-progress-scale-cost.test.ts src/tests/temp-chat-mode.test.ts src/tests/profile-reset.test.ts src/tests/error-classification.test.ts src/tests/quota-cooldown.test.ts src/tests/account-ready-gate.test.ts src/tests/tool-cap-notice.test.ts src/tests/interaction-flows.test.ts src/tests/headers-gate-cooldown-fallback.test.ts src/tests/unwrapped-tool-calls.test.ts src/tests/anthropic.test.ts src/tests/sync-clients.test.ts src/tests/sync-top10-clients.test.ts src/tests/cli.test.ts src/tests/paths.test.ts src/tests/tool-manifest.test.ts src/tests/tui.test.ts src/tests/tui-settings.test.ts src/tests/personalization-deadline.test.ts src/tests/update-cli.test.ts src/tests/batch-accounts.test.ts src/tests/shared-browser.test.ts src/tests/auth-credential-classification.test.ts src/tests/responses-api.test.ts src/tests/media-generation.test.ts src/tests/infer-tool-name.test.ts src/tests/native-session-headers.test.ts src/tests/omni-thinking.test.ts src/tests/protect-stuck-mutex.test.ts src/tests/stream-error-notice.test.ts src/tests/parser-deep-coverage.test.ts src/tests/tui-logs-copy.test.ts src/tests/auth-jwt-and-validation.test.ts src/tests/tui-port-and-key-modal.test.ts",
|
|
33
33
|
"test:mock:fast": "tsx --test --test-concurrency=2 --env-file=.env.test src/tests/account-concurrency.test.ts src/tests/concurrency-latest-wins.test.ts src/tests/mutex.test.ts src/tests/context-truncation.test.ts src/tests/streaming-thinking-summary.test.ts src/tests/tool-call-guard.test.ts src/tests/utils-logger-full.test.ts src/tests/logger-toolcall-errors.test.ts src/tests/logger-toolcall-disabled.test.ts src/tests/utils-json-full.test.ts src/tests/utils-session-guard-full.test.ts src/tests/core-logic-full.test.ts src/tests/core-logic-accounts.test.ts src/tests/chat-validation-full.test.ts src/tests/chat-logic-full.test.ts src/tests/account-stickiness.test.ts src/tests/accounts-security.test.ts src/tests/advanced.test.ts src/tests/auth-playwright.test.ts src/tests/auth-session-persistence.test.ts src/tests/captcha-solver.test.ts src/tests/cache-scan-regex.test.ts src/tests/chat-cleanup.test.ts src/tests/chat-cleanup-auto.test.ts src/tests/chromium-args.test.ts src/tests/config-current.test.ts src/tests/context-meter.test.ts src/tests/concurrency.test.ts src/tests/delta.test.ts src/tests/fingerprint.test.ts src/tests/human-behavior.test.ts src/tests/index.test.ts src/tests/json-recovery.test.ts src/tests/jsonFix.test.ts src/tests/memory-cache.test.ts src/tests/model-alias.test.ts src/tests/model-registry-thinking.test.ts src/tests/models-endpoint.test.ts src/tests/multimodal.test.ts src/tests/oss-upload.test.ts src/tests/parser.test.ts src/tests/personalization-flow.test.ts src/tests/personalization-required.test.ts src/tests/retry-policy.test.ts src/tests/rotation.test.ts src/tests/server-lifecycle.test.ts src/tests/session-keeper.test.ts src/tests/session-state.test.ts src/tests/single-account-mode.test.ts src/tests/stream-retry.test.ts src/tests/tool-calls-endpoint.test.ts src/tests/tool-instructions-cache.test.ts src/tests/session-id.test.ts src/tests/watchdog-ram.test.ts src/tests/tool-recovery-t1t2.test.ts src/tests/abort-silent-t4.test.ts src/tests/t3-close-tag-escape.test.ts src/tests/t5-tool-cap.test.ts src/tests/logger-redaction.test.ts src/tests/stall-timeout-retryable.test.ts src/tests/t6-missing-quote-array-close.test.ts src/tests/watchdog-rss.test.ts src/tests/logical-thread-batch.test.ts src/tests/chat-pool-style.test.ts src/tests/queue-wait-cap.test.ts src/tests/context-eviction-order.test.ts src/tests/chat-in-progress-settle.test.ts src/tests/stream-lock-leak.test.ts src/tests/stream-semaphore.test.ts src/tests/waf-isolation.test.ts src/tests/legacy-completions.test.ts src/tests/response-format.test.ts src/tests/cors-headers.test.ts src/tests/local-auth.test.ts src/tests/safe-remote-url.test.ts src/tests/stream-lock-terminal-cancel.test.ts src/tests/parser-truncated-tool-call.test.ts src/tests/metrics-prometheus.test.ts src/tests/context-death-cleanup.test.ts src/tests/personalization-direct-fetch.test.ts src/tests/completion-headers-parity.test.ts src/tests/parallel-escape-repro.test.ts src/tests/chat-in-progress-scale-cost.test.ts src/tests/temp-chat-mode.test.ts src/tests/profile-reset.test.ts src/tests/error-classification.test.ts src/tests/quota-cooldown.test.ts src/tests/account-ready-gate.test.ts src/tests/tool-cap-notice.test.ts src/tests/interaction-flows.test.ts src/tests/headers-gate-cooldown-fallback.test.ts src/tests/unwrapped-tool-calls.test.ts src/tests/anthropic.test.ts src/tests/sync-clients.test.ts src/tests/sync-top10-clients.test.ts src/tests/cli.test.ts src/tests/paths.test.ts src/tests/tool-manifest.test.ts src/tests/tui.test.ts src/tests/tui-settings.test.ts src/tests/personalization-deadline.test.ts src/tests/update-cli.test.ts src/tests/batch-accounts.test.ts src/tests/shared-browser.test.ts src/tests/auth-credential-classification.test.ts src/tests/responses-api.test.ts src/tests/media-generation.test.ts src/tests/infer-tool-name.test.ts src/tests/native-session-headers.test.ts src/tests/omni-thinking.test.ts src/tests/protect-stuck-mutex.test.ts src/tests/stream-error-notice.test.ts src/tests/parser-deep-coverage.test.ts src/tests/tui-logs-copy.test.ts src/tests/auth-jwt-and-validation.test.ts src/tests/tui-port-and-key-modal.test.ts",
|
|
34
34
|
"test:live": "tsx --test src/tests/concurrentChat.test.ts src/tests/agenticStress.test.ts",
|
|
35
|
+
"test:stress": "npx tsx src/benchmarks/stress-test.ts",
|
|
36
|
+
"test:stress:personalization": "npx tsx src/benchmarks/stress-test-personalization.ts",
|
|
35
37
|
"benchmark:proxy": "npx tsx src/benchmarks/proxy-baseline.ts",
|
|
36
38
|
"typecheck": "tsc --noEmit"
|
|
37
39
|
},
|
|
@@ -62,16 +64,16 @@
|
|
|
62
64
|
"@hono/node-server": "^2.1.1",
|
|
63
65
|
"ali-oss": "^6.23.0",
|
|
64
66
|
"better-sqlite3": "^13.0.3",
|
|
65
|
-
"dotenv": "^
|
|
66
|
-
"hono": "^4.13.
|
|
67
|
+
"dotenv": "^18.0.4",
|
|
68
|
+
"hono": "^4.13.9",
|
|
67
69
|
"patchright": "1.63.0",
|
|
68
|
-
"tsx": "^4.23.
|
|
70
|
+
"tsx": "^4.23.15",
|
|
69
71
|
"uuid": "^14.0.2",
|
|
70
|
-
"zod": "^4.5
|
|
72
|
+
"zod": "^4.6.5"
|
|
71
73
|
},
|
|
72
74
|
"devDependencies": {
|
|
73
75
|
"@types/better-sqlite3": "^9.6.0",
|
|
74
|
-
"@types/node": "^26.
|
|
76
|
+
"@types/node": "^26.6.3",
|
|
75
77
|
"esbuild": "^0.28.2",
|
|
76
78
|
"typescript": "^7.0.2"
|
|
77
79
|
},
|
package/src/core/config.ts
CHANGED
|
@@ -1,4 +1,17 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import dotenv from "dotenv";
|
|
1
3
|
import { z } from "zod";
|
|
4
|
+
import { getEnvFilePath, isRunningUnderNodeTest } from "./paths.ts";
|
|
5
|
+
|
|
6
|
+
// Ensure .env is loaded before parsing schema across CLI, TUI, and server modes
|
|
7
|
+
if (!isRunningUnderNodeTest()) {
|
|
8
|
+
const envPath = getEnvFilePath();
|
|
9
|
+
if (fs.existsSync(envPath)) {
|
|
10
|
+
dotenv.config({ path: envPath, quiet: true });
|
|
11
|
+
} else {
|
|
12
|
+
dotenv.config({ quiet: true });
|
|
13
|
+
}
|
|
14
|
+
}
|
|
2
15
|
|
|
3
16
|
const envSchema = z
|
|
4
17
|
.object({
|
package/src/core/version.ts
CHANGED
package/src/routes/chat/index.ts
CHANGED
|
@@ -279,6 +279,7 @@ export async function chatCompletions(c: Context) {
|
|
|
279
279
|
)) {
|
|
280
280
|
c.header(name, value);
|
|
281
281
|
}
|
|
282
|
+
c.header("X-QwenProxy-Account", streamResult.activeAccountLabel);
|
|
282
283
|
|
|
283
284
|
// A full-context replay (account switch / missing thread parent) hides its
|
|
284
285
|
// real cost behind the thread-native delta numbers: surface it explicitly
|
|
@@ -450,7 +450,7 @@ export async function isPageLoggedIn(
|
|
|
450
450
|
|
|
451
451
|
export async function getOrLaunchSharedBrowser(
|
|
452
452
|
browserType: BrowserType = "chromium",
|
|
453
|
-
headless =
|
|
453
|
+
headless = config.playwright.headless,
|
|
454
454
|
): Promise<Browser> {
|
|
455
455
|
if (sharedBrowser && sharedBrowser.isConnected()) {
|
|
456
456
|
return sharedBrowser;
|
|
@@ -587,15 +587,42 @@ const CHAT_INPUT_ACTION_TIMEOUT_MS = 10_000;
|
|
|
587
587
|
* account for five minutes.
|
|
588
588
|
*/
|
|
589
589
|
async function clearVisibleChallenge(page: Page): Promise<void> {
|
|
590
|
-
if (
|
|
591
|
-
//
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
590
|
+
if (page.isClosed()) return;
|
|
591
|
+
// 1. Solve slider puzzle captcha if present
|
|
592
|
+
if (config.captcha.enabled) {
|
|
593
|
+
await solveBaxiaCaptcha(page, {
|
|
594
|
+
waitForMs: 0,
|
|
595
|
+
maxAttempts: config.captcha.maxAttempts,
|
|
596
|
+
retryDelayMs: config.captcha.retryDelayMs,
|
|
597
|
+
settleMs: config.captcha.settleMs,
|
|
598
|
+
}).catch(() => false);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// 2. Dismiss any overlay modals (Welcome to Qwen, Terms/Consent, Whats New, etc.)
|
|
602
|
+
// that can block the chat textarea or send button
|
|
603
|
+
try {
|
|
604
|
+
const modalCloseSelectors = [
|
|
605
|
+
".ant-modal-close",
|
|
606
|
+
".ant-modal-close-x",
|
|
607
|
+
"button.ant-modal-close",
|
|
608
|
+
"button[aria-label*='Close' i]",
|
|
609
|
+
"button[aria-label*='Fechar' i]",
|
|
610
|
+
".index-module__whats-new-modal-close",
|
|
611
|
+
"button:has-text('Cancel')",
|
|
612
|
+
"button:has-text('Cancelar')",
|
|
613
|
+
"button:has-text('I understand')",
|
|
614
|
+
"button:has-text('Entendi')",
|
|
615
|
+
"button:has-text('OK')",
|
|
616
|
+
];
|
|
617
|
+
for (const sel of modalCloseSelectors) {
|
|
618
|
+
if (page.isClosed()) return;
|
|
619
|
+
const btn = page.locator(sel).first();
|
|
620
|
+
if (await btn.isVisible({ timeout: 150 }).catch(() => false)) {
|
|
621
|
+
await btn.click({ timeout: 500 }).catch(() => {});
|
|
622
|
+
await sleep(200);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
} catch {}
|
|
599
626
|
}
|
|
600
627
|
|
|
601
628
|
function getErrorMessage(error: unknown): string {
|
|
@@ -1051,6 +1078,21 @@ export function getStealthScript(profile: FingerprintProfile): string {
|
|
|
1051
1078
|
spoofedFunctions.add(window.performance.getEntriesByType);
|
|
1052
1079
|
}
|
|
1053
1080
|
} catch(e) {}
|
|
1081
|
+
|
|
1082
|
+
// --- Auto-resolve birthday compliance & eliminate false logout markers ---
|
|
1083
|
+
try {
|
|
1084
|
+
localStorage.removeItem("qwen_token_logged_out_marker");
|
|
1085
|
+
const b = localStorage.getItem("qwen_account_birthday");
|
|
1086
|
+
if (b) {
|
|
1087
|
+
try {
|
|
1088
|
+
const parsed = JSON.parse(b);
|
|
1089
|
+
if (!parsed.birthday) {
|
|
1090
|
+
parsed.birthday = "1995-06-15";
|
|
1091
|
+
localStorage.setItem("qwen_account_birthday", JSON.stringify(parsed));
|
|
1092
|
+
}
|
|
1093
|
+
} catch {}
|
|
1094
|
+
}
|
|
1095
|
+
} catch(e) {}
|
|
1054
1096
|
})();
|
|
1055
1097
|
`;
|
|
1056
1098
|
}
|
|
@@ -1443,7 +1485,7 @@ async function resolveAccountCredentials(account: QwenAccount): Promise<QwenAcco
|
|
|
1443
1485
|
|
|
1444
1486
|
export async function initPlaywrightForAccount(
|
|
1445
1487
|
rawAccount: QwenAccount,
|
|
1446
|
-
headless =
|
|
1488
|
+
headless = config.playwright.headless,
|
|
1447
1489
|
browserType: BrowserType = "chromium",
|
|
1448
1490
|
options: { skipHeaderCapture?: boolean } = {},
|
|
1449
1491
|
): Promise<void> {
|
|
@@ -1695,10 +1737,19 @@ export async function initPlaywrightForAccount(
|
|
|
1695
1737
|
|
|
1696
1738
|
if (restoredFromDb) {
|
|
1697
1739
|
if (!acctPage.isClosed() && (acctPage.url() === "about:blank" || !acctPage.url().startsWith(qwenOrigin()))) {
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1740
|
+
try {
|
|
1741
|
+
await acctPage.goto(qwenUrl("/"), {
|
|
1742
|
+
waitUntil: "domcontentloaded",
|
|
1743
|
+
timeout: config.timeouts.navigation,
|
|
1744
|
+
});
|
|
1745
|
+
await sleep(300);
|
|
1746
|
+
} catch (err: any) {
|
|
1747
|
+
if (!err?.message?.includes("ERR_ABORTED")) {
|
|
1748
|
+
console.warn(
|
|
1749
|
+
`⚠️ [Playwright] Background navigation warning for ${maskEmail(account.email)}: ${err.message}`,
|
|
1750
|
+
);
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1702
1753
|
}
|
|
1703
1754
|
return;
|
|
1704
1755
|
}
|
|
@@ -2022,17 +2073,33 @@ async function loginToQwen(
|
|
|
2022
2073
|
const apiResult = await loginViaApi(page, email, password);
|
|
2023
2074
|
if (apiResult.success) {
|
|
2024
2075
|
cookieCaches.delete(accountId);
|
|
2025
|
-
|
|
2026
|
-
if (cache) {
|
|
2027
|
-
cache.headers = {};
|
|
2028
|
-
cache.lastRefresh = 0;
|
|
2029
|
-
}
|
|
2030
|
-
unmarkAccountHeadersReady(accountId);
|
|
2076
|
+
await saveStorageState(page.context(), accountId);
|
|
2031
2077
|
try {
|
|
2032
|
-
const
|
|
2033
|
-
|
|
2078
|
+
const liveCookies = await page.context().cookies();
|
|
2079
|
+
const tokenCookie = liveCookies.find((c) => c.name === "token");
|
|
2080
|
+
if (tokenCookie) {
|
|
2081
|
+
const cookieStr = liveCookies.map((c) => `${c.name}=${c.value}`).join("; ");
|
|
2082
|
+
const cache = getHeaderCache(accountId);
|
|
2083
|
+
cache.headers.cookie = cookieStr;
|
|
2084
|
+
cache.lastRefresh = Date.now();
|
|
2085
|
+
markAccountHeadersReady(accountId);
|
|
2086
|
+
const { saveAuthSession } = await import("../core/database.ts");
|
|
2087
|
+
const exp = parseJwtExpiry(tokenCookie.value);
|
|
2088
|
+
saveAuthSession(accountId, {
|
|
2089
|
+
cookie: cookieStr,
|
|
2090
|
+
userAgent: cache.headers["user-agent"] || "",
|
|
2091
|
+
bxV: cache.headers["bx-v"] || "2.5.37",
|
|
2092
|
+
bxUa: cache.headers["bx-ua"] || "",
|
|
2093
|
+
bxUmidtoken: cache.headers["bx-umidtoken"] || "",
|
|
2094
|
+
secChUa: cache.headers["sec-ch-ua"] || undefined,
|
|
2095
|
+
secChUaMobile: cache.headers["sec-ch-ua-mobile"] || undefined,
|
|
2096
|
+
secChUaPlatform: cache.headers["sec-ch-ua-platform"] || undefined,
|
|
2097
|
+
version: cache.headers["version"] || undefined,
|
|
2098
|
+
tokenExpiresAt: exp || undefined,
|
|
2099
|
+
capturedAt: Date.now(),
|
|
2100
|
+
});
|
|
2101
|
+
}
|
|
2034
2102
|
} catch {}
|
|
2035
|
-
await saveStorageState(page.context(), accountId);
|
|
2036
2103
|
return true;
|
|
2037
2104
|
}
|
|
2038
2105
|
|
|
@@ -2052,17 +2119,33 @@ async function loginToQwen(
|
|
|
2052
2119
|
const uiResult = await loginViaUi(page, email, password);
|
|
2053
2120
|
if (uiResult.success) {
|
|
2054
2121
|
cookieCaches.delete(accountId);
|
|
2055
|
-
|
|
2056
|
-
if (cache) {
|
|
2057
|
-
cache.headers = {};
|
|
2058
|
-
cache.lastRefresh = 0;
|
|
2059
|
-
}
|
|
2060
|
-
unmarkAccountHeadersReady(accountId);
|
|
2122
|
+
await saveStorageState(page.context(), accountId);
|
|
2061
2123
|
try {
|
|
2062
|
-
const
|
|
2063
|
-
|
|
2124
|
+
const liveCookies = await page.context().cookies();
|
|
2125
|
+
const tokenCookie = liveCookies.find((c) => c.name === "token");
|
|
2126
|
+
if (tokenCookie) {
|
|
2127
|
+
const cookieStr = liveCookies.map((c) => `${c.name}=${c.value}`).join("; ");
|
|
2128
|
+
const cache = getHeaderCache(accountId);
|
|
2129
|
+
cache.headers.cookie = cookieStr;
|
|
2130
|
+
cache.lastRefresh = Date.now();
|
|
2131
|
+
markAccountHeadersReady(accountId);
|
|
2132
|
+
const { saveAuthSession } = await import("../core/database.ts");
|
|
2133
|
+
const exp = parseJwtExpiry(tokenCookie.value);
|
|
2134
|
+
saveAuthSession(accountId, {
|
|
2135
|
+
cookie: cookieStr,
|
|
2136
|
+
userAgent: cache.headers["user-agent"] || "",
|
|
2137
|
+
bxV: cache.headers["bx-v"] || "2.5.37",
|
|
2138
|
+
bxUa: cache.headers["bx-ua"] || "",
|
|
2139
|
+
bxUmidtoken: cache.headers["bx-umidtoken"] || "",
|
|
2140
|
+
secChUa: cache.headers["sec-ch-ua"] || undefined,
|
|
2141
|
+
secChUaMobile: cache.headers["sec-ch-ua-mobile"] || undefined,
|
|
2142
|
+
secChUaPlatform: cache.headers["sec-ch-ua-platform"] || undefined,
|
|
2143
|
+
version: cache.headers["version"] || undefined,
|
|
2144
|
+
tokenExpiresAt: exp || undefined,
|
|
2145
|
+
capturedAt: Date.now(),
|
|
2146
|
+
});
|
|
2147
|
+
}
|
|
2064
2148
|
} catch {}
|
|
2065
|
-
await saveStorageState(page.context(), accountId);
|
|
2066
2149
|
return true;
|
|
2067
2150
|
}
|
|
2068
2151
|
|
|
@@ -2292,11 +2375,22 @@ async function loginViaUi(
|
|
|
2292
2375
|
}
|
|
2293
2376
|
|
|
2294
2377
|
// In Qwen Web, if the page opens on the default email OTP panel, click "Log in with a password"
|
|
2295
|
-
// to reveal the standard email + password form.
|
|
2378
|
+
// (supporting English, Portuguese, Spanish, Chinese) to reveal the standard email + password form.
|
|
2379
|
+
await clearVisibleChallenge(page);
|
|
2380
|
+
|
|
2381
|
+
const findPasswordModeButton = () => {
|
|
2382
|
+
return page
|
|
2383
|
+
.locator('button, [role="button"], a, div')
|
|
2384
|
+
.filter({
|
|
2385
|
+
hasText: /(?:log\s*in\s*with\s*(?:a\s*)?password|fazer\s*login\s*com\s*senha|entrar\s*com\s*(?:uma\s*)?senha|iniciar\s*sesi[oó]n\s*con\s*contrase[nñ]a|密码登录)/i,
|
|
2386
|
+
})
|
|
2387
|
+
.first();
|
|
2388
|
+
};
|
|
2389
|
+
|
|
2296
2390
|
try {
|
|
2297
|
-
const pwdModeBtn =
|
|
2298
|
-
if (await pwdModeBtn.isVisible({ timeout:
|
|
2299
|
-
await pwdModeBtn.click().catch(() => {});
|
|
2391
|
+
const pwdModeBtn = findPasswordModeButton();
|
|
2392
|
+
if (await pwdModeBtn.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|
2393
|
+
await pwdModeBtn.click({ timeout: 2000 }).catch(() => {});
|
|
2300
2394
|
await sleep(500);
|
|
2301
2395
|
}
|
|
2302
2396
|
} catch {}
|
|
@@ -2307,9 +2401,9 @@ async function loginViaUi(
|
|
|
2307
2401
|
|
|
2308
2402
|
// If "Log in with a password" button appeared after typing email, click it
|
|
2309
2403
|
try {
|
|
2310
|
-
const pwdModeBtn =
|
|
2311
|
-
if (await pwdModeBtn.isVisible({ timeout:
|
|
2312
|
-
await pwdModeBtn.click().catch(() => {});
|
|
2404
|
+
const pwdModeBtn = findPasswordModeButton();
|
|
2405
|
+
if (await pwdModeBtn.isVisible({ timeout: 1500 }).catch(() => false)) {
|
|
2406
|
+
await pwdModeBtn.click({ timeout: 2000 }).catch(() => {});
|
|
2313
2407
|
await sleep(500);
|
|
2314
2408
|
}
|
|
2315
2409
|
} catch {}
|
|
@@ -2343,7 +2437,7 @@ async function loginViaUi(
|
|
|
2343
2437
|
// Prefer clicking the submit button; fall back to pressing Enter.
|
|
2344
2438
|
// The button starts disabled and only enables once both fields are filled.
|
|
2345
2439
|
const submitSelector =
|
|
2346
|
-
'button.qwenchat-auth-pc-submit-button, button[type="submit"], button:has-text("Log in"), button:has-text("Sign in")';
|
|
2440
|
+
'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ón"), button:has-text("登录")';
|
|
2347
2441
|
const submitButton = page.locator(submitSelector).first();
|
|
2348
2442
|
try {
|
|
2349
2443
|
await page.waitForSelector('button[type="submit"]:not([disabled])', {
|
|
@@ -3164,9 +3258,9 @@ async function refreshHeadersInternal(
|
|
|
3164
3258
|
}
|
|
3165
3259
|
}
|
|
3166
3260
|
|
|
3167
|
-
// Fast
|
|
3261
|
+
// Fast refresh completion: if required anti-bot tokens are already cached,
|
|
3168
3262
|
// refresh cookies directly without running slow UI typing interception.
|
|
3169
|
-
if (
|
|
3263
|
+
if (hasRequiredQwenHeaders(cache.headers)) {
|
|
3170
3264
|
const liveCookies = await page.context().cookies();
|
|
3171
3265
|
if (liveCookies.some((c) => c.name === "token")) {
|
|
3172
3266
|
const cookieStr = liveCookies.map((c) => `${c.name}=${c.value}`).join("; ");
|
package/src/services/qwen.ts
CHANGED
|
@@ -800,10 +800,19 @@ async function withQwenBrowserPage<T>(
|
|
|
800
800
|
(normalizedTargetPath !== null && currentPath !== normalizedTargetPath);
|
|
801
801
|
|
|
802
802
|
if (needsNavigation) {
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
803
|
+
try {
|
|
804
|
+
await page.goto(targetUrl, {
|
|
805
|
+
waitUntil: "domcontentloaded",
|
|
806
|
+
timeout: Math.min(config.timeouts.navigation, operationTimeoutMs),
|
|
807
|
+
});
|
|
808
|
+
} catch (err: any) {
|
|
809
|
+
// If navigation was aborted because page reached target origin or redirect finished, ignore benign abort
|
|
810
|
+
if (err?.message?.includes("ERR_ABORTED") && page.url().startsWith(targetOrigin)) {
|
|
811
|
+
// benign navigation interruption
|
|
812
|
+
} else {
|
|
813
|
+
throw err;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
807
816
|
}
|
|
808
817
|
|
|
809
818
|
return fn(page);
|
|
@@ -908,7 +917,7 @@ export async function requestQwenTextInBrowser(
|
|
|
908
917
|
const controller = new AbortController();
|
|
909
918
|
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
910
919
|
try {
|
|
911
|
-
|
|
920
|
+
let response = await fetch(url, {
|
|
912
921
|
method,
|
|
913
922
|
credentials: "include",
|
|
914
923
|
headers,
|
|
@@ -916,6 +925,38 @@ export async function requestQwenTextInBrowser(
|
|
|
916
925
|
signal: controller.signal,
|
|
917
926
|
...(referrer ? { referrer } : {}),
|
|
918
927
|
});
|
|
928
|
+
|
|
929
|
+
// If 401 Unauthorized in browser, try silent in-page token refresh before giving up
|
|
930
|
+
if (response.status === 401) {
|
|
931
|
+
try {
|
|
932
|
+
const refreshRes = await fetch("https://auth.qwen.ai/api/v2/auths/refresh", {
|
|
933
|
+
method: "GET",
|
|
934
|
+
credentials: "include",
|
|
935
|
+
signal: AbortSignal.timeout(3000),
|
|
936
|
+
});
|
|
937
|
+
if (refreshRes.status === 200) {
|
|
938
|
+
const refreshJson: any = await refreshRes.json().catch(() => null);
|
|
939
|
+
if (refreshJson && refreshJson.success === true && refreshJson.data?.token) {
|
|
940
|
+
const freshTok = refreshJson.data.token;
|
|
941
|
+
localStorage.setItem("token", freshTok);
|
|
942
|
+
document.cookie = `token=${encodeURIComponent(freshTok)}; path=/; domain=.qwen.ai; max-age=31536000`;
|
|
943
|
+
if (headers["authorization"] || headers["Authorization"]) {
|
|
944
|
+
headers["authorization"] = `Bearer ${freshTok}`;
|
|
945
|
+
headers["Authorization"] = `Bearer ${freshTok}`;
|
|
946
|
+
}
|
|
947
|
+
response = await fetch(url, {
|
|
948
|
+
method,
|
|
949
|
+
credentials: "include",
|
|
950
|
+
headers,
|
|
951
|
+
body,
|
|
952
|
+
signal: controller.signal,
|
|
953
|
+
...(referrer ? { referrer } : {}),
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
} catch {}
|
|
958
|
+
}
|
|
959
|
+
|
|
919
960
|
return {
|
|
920
961
|
status: response.status,
|
|
921
962
|
contentType: response.headers.get("content-type") || "",
|
|
@@ -1059,20 +1100,7 @@ async function requestQwenPersonalizationInBrowser(
|
|
|
1059
1100
|
headers: Record<string, string>,
|
|
1060
1101
|
payload?: Record<string, unknown>,
|
|
1061
1102
|
): Promise<{ status: number; raw: string; json: any }> {
|
|
1062
|
-
//
|
|
1063
|
-
if (!config.qwen.browserOnlyFetch && !isAuthMockEnabled()) {
|
|
1064
|
-
const direct = await requestQwenSettingsDirectFetch(
|
|
1065
|
-
accountId,
|
|
1066
|
-
method,
|
|
1067
|
-
path,
|
|
1068
|
-
headers,
|
|
1069
|
-
payload,
|
|
1070
|
-
);
|
|
1071
|
-
if (direct) {
|
|
1072
|
-
return direct;
|
|
1073
|
-
}
|
|
1074
|
-
}
|
|
1075
|
-
|
|
1103
|
+
// Always route personalization through stealth browser page (user constraint: tudo via browser stealth)
|
|
1076
1104
|
const response = await requestQwenTextInBrowser(
|
|
1077
1105
|
accountId,
|
|
1078
1106
|
method,
|