qwenproxy-cli 1.6.1 → 1.6.3

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.
@@ -538,14 +538,14 @@ const ACCOUNT_PAGE_OPERATION_TIMEOUT_MS = config.timeouts.page;
538
538
  */
539
539
  const SESSION_PROBE_NAVIGATION_TIMEOUT_MS = 15_000;
540
540
  /** Grace period for the intercepted completion request after the send is triggered. */
541
- const HEADER_CAPTURE_TRIGGER_GRACE_MS = 15_000;
541
+ const HEADER_CAPTURE_TRIGGER_GRACE_MS = 20_000;
542
542
  /**
543
543
  * First-send grace is short: the page is cold and the bx SDK has not computed
544
544
  * its tokens yet, so a cold page almost never produces a request from the first
545
545
  * send. Fail it fast and let the retry loop reload + re-send against the warm
546
546
  * SDK instead of stalling the boot for the full 15s.
547
547
  */
548
- const FIRST_TRIGGER_GRACE_MS = 8_000;
548
+ const FIRST_TRIGGER_GRACE_MS = 12_000;
549
549
  /**
550
550
  * Sends (the initial one plus re-triggers) header capture may spend on getting a
551
551
  * completion request that actually carries the bx headers. The in-page SDK can
@@ -593,6 +593,10 @@ async function clearVisibleChallenge(page: Page): Promise<void> {
593
593
  "button[aria-label*='Close' i]",
594
594
  "button[aria-label*='Fechar' i]",
595
595
  ".index-module__whats-new-modal-close",
596
+ ".age-confirmation-modal button",
597
+ ".age-confirmation-modal .qwen-chat-v2-btn-black",
598
+ "button:has-text('Continuar')",
599
+ "button:has-text('Continue')",
596
600
  "button:has-text('Cancel')",
597
601
  "button:has-text('Cancelar')",
598
602
  "button:has-text('I understand')",
@@ -1989,7 +1993,376 @@ export async function validateAccountLogin(
1989
1993
  release();
1990
1994
  }
1991
1995
  }
1992
- // ─── Login ────────────────────────────────────────────────────────────────────
1996
+
1997
+ // ─── Passwordless Email OTP Login ──────────────────────────────────────────
1998
+
1999
+ export interface OtpStartResult {
2000
+ success: boolean;
2001
+ sessionId?: string;
2002
+ error?: string;
2003
+ }
2004
+
2005
+ export interface OtpVerifyResult {
2006
+ success: boolean;
2007
+ error?: string;
2008
+ account?: QwenAccount;
2009
+ }
2010
+
2011
+ interface ActiveOtpSession {
2012
+ sessionId: string;
2013
+ accountId: string;
2014
+ email: string;
2015
+ context: BrowserContext;
2016
+ page: Page;
2017
+ fingerprint: FingerprintProfile;
2018
+ createdAt: number;
2019
+ timer: NodeJS.Timeout;
2020
+ }
2021
+
2022
+ const activeOtpSessions = new Map<string, ActiveOtpSession>();
2023
+ const mockOtpSessions = new Map<string, string>();
2024
+
2025
+ /**
2026
+ * Initiates passwordless email OTP login:
2027
+ * Launches an account Playwright context, visits /auth?mode=register,
2028
+ * and requests a 6-digit verification code from auth.qwen.ai.
2029
+ */
2030
+ export async function startEmailOtpLogin(
2031
+ email: string,
2032
+ options: {
2033
+ headless?: boolean;
2034
+ browserType?: BrowserType;
2035
+ } = {},
2036
+ ): Promise<OtpStartResult> {
2037
+ const normalizedEmail = email.trim().toLowerCase();
2038
+ if (!normalizedEmail || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalizedEmail)) {
2039
+ return { success: false, error: "Formato de e-mail inválido" };
2040
+ }
2041
+
2042
+ const { listAccounts } = await import("../core/accounts.ts");
2043
+ if (listAccounts().some((a) => a.email.toLowerCase() === normalizedEmail)) {
2044
+ return {
2045
+ success: false,
2046
+ error: `Uma conta com o e-mail "${normalizedEmail}" já está cadastrada.`,
2047
+ };
2048
+ }
2049
+
2050
+ if (process.env.TEST_MOCK_QWEN_AUTH === "true" && process.env.NODE_ENV !== "production") {
2051
+ const mockSessionId = "mock-otp-" + Date.now();
2052
+ mockOtpSessions.set(mockSessionId, normalizedEmail);
2053
+ return { success: true, sessionId: mockSessionId };
2054
+ }
2055
+
2056
+ const accountId = crypto.randomUUID();
2057
+ const sessionId = accountId;
2058
+
2059
+ const profilePath = getAccountProfilePath(accountId);
2060
+ const fingerprint = getFingerprintProfile(accountId);
2061
+ const browserType = options.browserType ?? config.playwright.browser;
2062
+ const { engine, channel } = resolveBrowserEngine(browserType);
2063
+ const headless = options.headless ?? config.playwright.headless;
2064
+
2065
+ const launchOptions = {
2066
+ headless,
2067
+ channel,
2068
+ userAgent: fingerprint.userAgent,
2069
+ locale: fingerprint.locale,
2070
+ timezoneId: fingerprint.timezoneId,
2071
+ viewport: fingerprint.viewport,
2072
+ screen: fingerprint.viewport,
2073
+ deviceScaleFactor: 1,
2074
+ isMobile: false,
2075
+ hasTouch: false,
2076
+ colorScheme: "light" as const,
2077
+ extraHTTPHeaders: {
2078
+ "sec-ch-ua": fingerprint.secChUa,
2079
+ "sec-ch-ua-mobile": "?0",
2080
+ "sec-ch-ua-platform": '"Windows"',
2081
+ },
2082
+ ignoreDefaultArgs: ["--enable-automation", "--enable-blink-features"],
2083
+ args: buildChromiumLaunchArgs(fingerprint.viewport),
2084
+ };
2085
+
2086
+ let acctContext: BrowserContext;
2087
+ try {
2088
+ acctContext = await withTimeout(
2089
+ engine.launchPersistentContext(profilePath, launchOptions),
2090
+ 30_000,
2091
+ `O navegador não iniciou em 30s para a conta ${maskEmail(normalizedEmail)}.`,
2092
+ );
2093
+ } catch (launchErr: any) {
2094
+ if (launchErr?.message?.includes("Executable doesn't exist")) {
2095
+ autoInstallPlaywrightChromium();
2096
+ acctContext = await withTimeout(
2097
+ engine.launchPersistentContext(profilePath, launchOptions),
2098
+ 30_000,
2099
+ `O navegador não iniciou em 30s para a conta ${maskEmail(normalizedEmail)}.`,
2100
+ );
2101
+ } else {
2102
+ return { success: false, error: launchErr?.message || String(launchErr) };
2103
+ }
2104
+ }
2105
+
2106
+ try {
2107
+ await acctContext.addInitScript(getStealthScript(fingerprint));
2108
+
2109
+ const existingPages = acctContext.pages().filter((p) => !p.isClosed());
2110
+ const acctPage =
2111
+ existingPages.find((p) => p.url().startsWith(qwenOrigin())) ??
2112
+ existingPages[0] ??
2113
+ (await acctContext.newPage());
2114
+
2115
+ await acctPage.goto(qwenUrl("/auth?mode=register"), {
2116
+ waitUntil: "domcontentloaded",
2117
+ timeout: Math.min(config.timeouts.navigation, 20_000),
2118
+ }).catch(() => {});
2119
+ await sleep(1000);
2120
+
2121
+ await clearVisibleChallenge(acctPage);
2122
+
2123
+ const requestId = crypto.randomUUID();
2124
+ const evalRes: any = await acctPage.evaluate(
2125
+ async ({ email, requestId, version, timezone }) => {
2126
+ try {
2127
+ const response = await fetch("https://auth.qwen.ai/api/v2/auths/otp/email/request", {
2128
+ method: "POST",
2129
+ credentials: "include",
2130
+ headers: {
2131
+ accept: "application/json, text/plain, */*",
2132
+ "content-type": "application/json",
2133
+ source: "web",
2134
+ version,
2135
+ "x-request-origin": "https://chat.qwen.ai",
2136
+ timezone,
2137
+ "x-request-id": requestId,
2138
+ },
2139
+ body: JSON.stringify({
2140
+ identifier: email,
2141
+ accept_terms: true,
2142
+ language: "pt-BR",
2143
+ }),
2144
+ });
2145
+ const json = await response.json().catch(() => null);
2146
+ return { ok: response.ok, status: response.status, data: json };
2147
+ } catch (e: any) {
2148
+ return { ok: false, error: e.message };
2149
+ }
2150
+ },
2151
+ {
2152
+ email: normalizedEmail,
2153
+ requestId,
2154
+ version: getQwenWebVersion() || "0.3.12",
2155
+ timezone: new Date().toString().split(" (")[0],
2156
+ },
2157
+ ).catch((err: any) => ({ ok: false, error: err?.message || String(err) }));
2158
+
2159
+ if (!evalRes.ok || evalRes.data?.success !== true) {
2160
+ const errorMsg =
2161
+ evalRes.data?.data?.details ||
2162
+ evalRes.data?.data?.message ||
2163
+ evalRes.data?.message ||
2164
+ evalRes.error ||
2165
+ "Falha ao solicitar código de verificação";
2166
+ await closePlaywrightContextBestEffort(accountId, acctContext).catch(() => {});
2167
+ cleanupPlaywrightAccountState(accountId);
2168
+ return { success: false, error: errorMsg };
2169
+ }
2170
+
2171
+ const timer = setTimeout(async () => {
2172
+ activeOtpSessions.delete(sessionId);
2173
+ await closePlaywrightContextBestEffort(accountId, acctContext).catch(() => {});
2174
+ cleanupPlaywrightAccountState(accountId);
2175
+ }, 5 * 60 * 1000);
2176
+ timer.unref?.();
2177
+
2178
+ activeOtpSessions.set(sessionId, {
2179
+ sessionId,
2180
+ accountId,
2181
+ email: normalizedEmail,
2182
+ context: acctContext,
2183
+ page: acctPage,
2184
+ fingerprint,
2185
+ createdAt: Date.now(),
2186
+ timer,
2187
+ });
2188
+
2189
+ return { success: true, sessionId };
2190
+ } catch (err: any) {
2191
+ await closePlaywrightContextBestEffort(accountId, acctContext).catch(() => {});
2192
+ cleanupPlaywrightAccountState(accountId);
2193
+ return { success: false, error: err?.message || String(err) };
2194
+ }
2195
+ }
2196
+
2197
+ /**
2198
+ * Verifies email OTP code, captures access/refresh tokens, stores the authenticated
2199
+ * session in SQLite, and registers the account.
2200
+ */
2201
+ export async function verifyEmailOtpLogin(
2202
+ sessionId: string,
2203
+ code: string,
2204
+ ): Promise<OtpVerifyResult> {
2205
+ const cleanCode = code.trim();
2206
+ if (!cleanCode) {
2207
+ return { success: false, error: "Código de verificação é obrigatório" };
2208
+ }
2209
+
2210
+ if (process.env.TEST_MOCK_QWEN_AUTH === "true" && process.env.NODE_ENV !== "production") {
2211
+ const mockEmail = mockOtpSessions.get(sessionId);
2212
+ if (!mockEmail) {
2213
+ return { success: false, error: "Sessão OTP inválida ou expirada" };
2214
+ }
2215
+ if (cleanCode === "000000") {
2216
+ return { success: false, error: "Código de verificação incorreto ou expirado" };
2217
+ }
2218
+ const { addAccount } = await import("../core/accounts.ts");
2219
+ const mockAccId = "mock-otp-acc-" + Date.now();
2220
+ const newAccount = addAccount(mockEmail, "", mockAccId);
2221
+ mockOtpSessions.delete(sessionId);
2222
+ return { success: true, account: newAccount };
2223
+ }
2224
+
2225
+ const session = activeOtpSessions.get(sessionId);
2226
+ if (!session) {
2227
+ return { success: false, error: "Sessão OTP expirada ou não encontrada. Solicite um novo código." };
2228
+ }
2229
+ if (session.page.isClosed()) {
2230
+ activeOtpSessions.delete(sessionId);
2231
+ return { success: false, error: "Navegador foi fechado. Solicite um novo código." };
2232
+ }
2233
+
2234
+ const requestId = crypto.randomUUID();
2235
+ const evalRes: any = await session.page.evaluate(
2236
+ async ({ email, code, requestId, version, timezone }) => {
2237
+ try {
2238
+ const response = await fetch("https://auth.qwen.ai/api/v2/auths/otp/email/verify", {
2239
+ method: "POST",
2240
+ credentials: "include",
2241
+ headers: {
2242
+ accept: "application/json, text/plain, */*",
2243
+ "content-type": "application/json",
2244
+ source: "web",
2245
+ version,
2246
+ "x-request-origin": "https://chat.qwen.ai",
2247
+ timezone,
2248
+ "x-request-id": requestId,
2249
+ },
2250
+ body: JSON.stringify({
2251
+ identifier: email,
2252
+ code,
2253
+ name: null,
2254
+ }),
2255
+ });
2256
+ const json = await response.json().catch(() => null);
2257
+ return { ok: response.ok, status: response.status, data: json };
2258
+ } catch (e: any) {
2259
+ return { ok: false, error: e.message };
2260
+ }
2261
+ },
2262
+ {
2263
+ email: session.email,
2264
+ code: cleanCode,
2265
+ requestId,
2266
+ version: getQwenWebVersion() || "0.3.12",
2267
+ timezone: new Date().toString().split(" (")[0],
2268
+ },
2269
+ ).catch((err: any) => ({ ok: false, error: err?.message || String(err) }));
2270
+
2271
+ if (!evalRes.ok || evalRes.data?.success !== true || !evalRes.data?.data) {
2272
+ const errorMsg =
2273
+ evalRes.data?.data?.details ||
2274
+ evalRes.data?.data?.message ||
2275
+ evalRes.data?.message ||
2276
+ evalRes.error ||
2277
+ "Código de verificação incorreto ou expirado";
2278
+ return { success: false, error: errorMsg };
2279
+ }
2280
+
2281
+ const resData = evalRes.data.data;
2282
+ const accessToken = resData.access_token || resData.token;
2283
+ const refreshToken = resData.refresh_token;
2284
+ const userId = resData.id;
2285
+
2286
+ try {
2287
+ await session.page.evaluate(({ token, rTok }) => {
2288
+ try {
2289
+ localStorage.setItem("token", token);
2290
+ localStorage.setItem("access_token", token);
2291
+ if (rTok) localStorage.setItem("refresh_token", rTok);
2292
+ document.cookie = `token=${encodeURIComponent(token)}; path=/; domain=.qwen.ai; max-age=31536000`;
2293
+ } catch {}
2294
+ }, { token: accessToken, rTok: refreshToken });
2295
+
2296
+ await session.context.addCookies([
2297
+ {
2298
+ name: "token",
2299
+ value: accessToken,
2300
+ domain: ".qwen.ai",
2301
+ path: "/",
2302
+ expires: Math.floor(Date.now() / 1000) + 31536000,
2303
+ httpOnly: false,
2304
+ secure: true,
2305
+ sameSite: "Lax",
2306
+ },
2307
+ ]);
2308
+
2309
+ await session.page.goto(qwenUrl("/"), {
2310
+ waitUntil: "domcontentloaded",
2311
+ timeout: Math.min(config.timeouts.navigation, 15_000),
2312
+ }).catch(() => {});
2313
+ await sleep(1000);
2314
+
2315
+ await saveStorageState(session.context, session.accountId);
2316
+
2317
+ const { saveAuthSession } = await import("../core/database.ts");
2318
+ const { parseJwtExpiry } = await import("../utils/jwt.ts");
2319
+ const exp = parseJwtExpiry(accessToken);
2320
+ saveAuthSession(session.accountId, {
2321
+ cookie: `token=${accessToken}`,
2322
+ userAgent: session.fingerprint.userAgent,
2323
+ bxV: "2.5.37",
2324
+ version: getQwenWebVersion() || "0.3.12",
2325
+ userId: userId || undefined,
2326
+ tokenExpiresAt: exp || undefined,
2327
+ refreshToken: refreshToken || undefined,
2328
+ capturedAt: Date.now(),
2329
+ });
2330
+
2331
+ const { addAccount } = await import("../core/accounts.ts");
2332
+ const newAccount = addAccount(session.email, "", session.accountId);
2333
+
2334
+ clearTimeout(session.timer);
2335
+ activeOtpSessions.delete(sessionId);
2336
+ await closePlaywrightContextBestEffort(session.accountId, session.context).catch(() => {});
2337
+ cleanupPlaywrightAccountState(session.accountId);
2338
+
2339
+ return { success: true, account: newAccount };
2340
+ } catch (err: any) {
2341
+ clearTimeout(session.timer);
2342
+ activeOtpSessions.delete(sessionId);
2343
+ await closePlaywrightContextBestEffort(session.accountId, session.context).catch(() => {});
2344
+ cleanupPlaywrightAccountState(session.accountId);
2345
+ return { success: false, error: err?.message || String(err) };
2346
+ }
2347
+ }
2348
+
2349
+ /**
2350
+ * Cancels an in-progress email OTP session and cleans up resources.
2351
+ */
2352
+ export async function cancelEmailOtpLogin(sessionId: string): Promise<void> {
2353
+ if (process.env.TEST_MOCK_QWEN_AUTH === "true" && process.env.NODE_ENV !== "production") {
2354
+ mockOtpSessions.delete(sessionId);
2355
+ return;
2356
+ }
2357
+ const session = activeOtpSessions.get(sessionId);
2358
+ if (!session) return;
2359
+ clearTimeout(session.timer);
2360
+ activeOtpSessions.delete(sessionId);
2361
+ await closePlaywrightContextBestEffort(session.accountId, session.context).catch(() => {});
2362
+ cleanupPlaywrightAccountState(session.accountId);
2363
+ const { wipeAccountSessionFiles } = await import("../core/accounts.ts");
2364
+ wipeAccountSessionFiles(session.accountId);
2365
+ }
1993
2366
 
1994
2367
  export interface LoginAttemptResult {
1995
2368
  success: boolean;
@@ -2869,7 +3242,24 @@ export async function captureQwenHeaders(
2869
3242
  timeoutMs = config.timeouts.headers,
2870
3243
  triggerGraceMs = HEADER_CAPTURE_TRIGGER_GRACE_MS,
2871
3244
  ): Promise<void> {
2872
- const page = pageOverride ?? accountPages.get(accountId);
3245
+ let page = pageOverride ?? accountPages.get(accountId);
3246
+ if (!page || page.isClosed()) {
3247
+ const { isRunningUnderNodeTest } = await import("../core/paths.ts");
3248
+ if (!isRunningUnderNodeTest()) {
3249
+ const { getAccountCredentials } = await import("../core/accounts.ts");
3250
+ const creds = getAccountCredentials(accountId);
3251
+ if (creds) {
3252
+ await initPlaywrightForAccount(
3253
+ creds,
3254
+ config.playwright.headless,
3255
+ config.playwright.browser,
3256
+ { skipHeaderCapture: true },
3257
+ ).catch(() => {});
3258
+ page = accountPages.get(accountId);
3259
+ }
3260
+ }
3261
+ }
3262
+
2873
3263
  if (!page || page.isClosed()) {
2874
3264
  throw new Error(`Playwright page unavailable for header capture: ${accountId}`);
2875
3265
  }
@@ -3727,22 +4117,51 @@ export async function refreshHeaders(
3727
4117
  * Run work against the account Playwright page under the per-account mutex.
3728
4118
  * Used by captcha recovery so it cannot race header capture / login.
3729
4119
  */
3730
- export async function withAccountPage<T>(
3731
- accountId: string,
3732
- fn: (page: Page) => Promise<T>,
3733
- timeoutMs = ACCOUNT_PAGE_OPERATION_TIMEOUT_MS,
3734
- mutexTimeoutMs = PLAYWRIGHT_MUTEX_WAIT_MS,
3735
- recoverOnTimeout = true,
3736
- ): Promise<T> {
4120
+ async function ensureAccountPageAvailable(accountId: string): Promise<Page> {
3737
4121
  const inFlightInit = inFlightAccountInits.get(accountId);
3738
4122
  if (inFlightInit) {
3739
4123
  await inFlightInit.catch(() => {});
3740
4124
  }
3741
4125
 
3742
- const page = accountPages.get(accountId);
4126
+ let page = accountPages.get(accountId);
4127
+ if (page && !page.isClosed()) {
4128
+ return page;
4129
+ }
4130
+
4131
+ // Under Node.js test suites, preserve strict mock failure contract
4132
+ const { isRunningUnderNodeTest } = await import("../core/paths.ts");
4133
+ if (isRunningUnderNodeTest()) {
4134
+ throw new Error(`Playwright page unavailable for account: ${accountId}`);
4135
+ }
4136
+
4137
+ // Lazy boot / recovery: if the account was evicted or in standby, initialize it transparently on demand
4138
+ const { getAccountCredentials } = await import("../core/accounts.ts");
4139
+ const creds = getAccountCredentials(accountId);
4140
+ if (!creds) {
4141
+ throw new Error(`Playwright page unavailable for account: ${accountId}`);
4142
+ }
4143
+
4144
+ await initPlaywrightForAccount(
4145
+ creds,
4146
+ config.playwright.headless,
4147
+ config.playwright.browser,
4148
+ );
4149
+
4150
+ page = accountPages.get(accountId);
3743
4151
  if (!page || page.isClosed()) {
3744
4152
  throw new Error(`Playwright page unavailable for account: ${accountId}`);
3745
4153
  }
4154
+ return page;
4155
+ }
4156
+
4157
+ export async function withAccountPage<T>(
4158
+ accountId: string,
4159
+ fn: (page: Page) => Promise<T>,
4160
+ timeoutMs = ACCOUNT_PAGE_OPERATION_TIMEOUT_MS,
4161
+ mutexTimeoutMs = PLAYWRIGHT_MUTEX_WAIT_MS,
4162
+ recoverOnTimeout = true,
4163
+ ): Promise<T> {
4164
+ const page = await ensureAccountPageAvailable(accountId);
3746
4165
  const release = await acquireAccountMutex(
3747
4166
  accountId,
3748
4167
  `page:${accountId.substring(0, 12)}`,
@@ -3763,7 +4182,8 @@ export async function withAccountPage<T>(
3763
4182
  const message = getErrorMessage(error);
3764
4183
  if (
3765
4184
  recoverOnTimeout &&
3766
- message.includes("Playwright page operation timed out")
4185
+ message.includes("Playwright page operation timed out") &&
4186
+ (!page || page.isClosed())
3767
4187
  ) {
3768
4188
  console.warn(
3769
4189
  `⏱️ [Playwright] Resetting account context after a stuck page operation: ${accountId}`,
@@ -4168,8 +4588,10 @@ export async function evictIdlePlaywrightContextsToLimit(): Promise<number> {
4168
4588
 
4169
4589
  const candidates = priorityOrderForEviction(
4170
4590
  Array.from(accountPages.keys()).filter((accountId) => {
4591
+ if (isAccountServingStream(accountId)) return false;
4592
+ if (isAccountRecentlyActive(accountId, 30_000)) return false;
4171
4593
  const mutex = accountMutexes.get(accountId);
4172
- return mutex?.isIdle() && !isAccountServingStream(accountId);
4594
+ return mutex?.isIdle();
4173
4595
  }),
4174
4596
  ).map((accountId) => ({
4175
4597
  accountId,
@@ -27,7 +27,7 @@ import {
27
27
  syncModelMetadata,
28
28
  } from "../core/model-registry.ts";
29
29
  import { type Page, type BrowserContext } from "patchright";
30
- import { withAccountPage, assertAntiBotHeaders, onBrowserContextCreated, isPlaywrightInitializing } from "./playwright.ts";
30
+ import { withAccountPage, assertAntiBotHeaders, onBrowserContextCreated, isPlaywrightInitializing, isPlaywrightInitialized } from "./playwright.ts";
31
31
  import { recoverBaxiaCaptcha } from "./captcha-coordinator.ts";
32
32
  import { startBaxiaCaptchaWatcher } from "./captcha-solver.ts";
33
33
  import { isAccountBusy } from "../core/account-concurrency.ts";
@@ -1139,6 +1139,7 @@ async function requestQwenPersonalizationInBrowser(
1139
1139
  {
1140
1140
  settingsPage: true,
1141
1141
  referrer: qwenUrl("/settings/personalization"),
1142
+ noMutexRecovery: true,
1142
1143
  },
1143
1144
  );
1144
1145
  const { raw, json } = await readJsonTextResponse(response);
@@ -1831,8 +1832,13 @@ async function syncQwenRequestPersonalizationInternal(
1831
1832
  typeof textSize
1832
1833
  >;
1833
1834
 
1835
+ const matchReturned =
1836
+ (isEmptyInstruction && (!returnedInstruction || returned.chars === 0)) ||
1837
+ (returned.hash !== null && returned.hash === sent.hash);
1838
+
1834
1839
  let verifyData: any = null;
1835
- if (config.qwen.personalizationVerifyGet) {
1840
+ // If the update response already confirmed the exact instruction, skip the redundant GET round-trip
1841
+ if (config.qwen.personalizationVerifyGet && !matchReturned) {
1836
1842
  const { json: verifyJson } =
1837
1843
  await requestQwenPersonalizationInBrowser(
1838
1844
  accountId,
@@ -1843,10 +1849,6 @@ async function syncQwenRequestPersonalizationInternal(
1843
1849
  verifyData = verifyJson?.data?.personalization;
1844
1850
  stored = textSize(verifyData?.instruction);
1845
1851
  }
1846
-
1847
- const matchReturned =
1848
- (isEmptyInstruction && (!returnedInstruction || returned.chars === 0)) ||
1849
- (returned.hash !== null && returned.hash === sent.hash);
1850
1852
  const matchStored =
1851
1853
  stored.hash === null
1852
1854
  ? null
@@ -2331,6 +2333,20 @@ export async function fetchQwenModels(
2331
2333
  return cached.models;
2332
2334
  }
2333
2335
 
2336
+ // For accounts that are not initialized yet, avoid cold-booting Playwright just for model metadata.
2337
+ // Reuse existing models from any active cache entry (models catalog is identical across accounts).
2338
+ if (accountId && !isAuthMockEnabled() && !isPlaywrightInitialized(accountId)) {
2339
+ for (const [, entry] of modelsCache.entries()) {
2340
+ if (entry && now - entry.fetchedAt < MODEL_CACHE_TTL_MS * 4) {
2341
+ syncModelMetadata(
2342
+ entry.models as unknown as Array<Record<string, unknown> & { id: string }>,
2343
+ accountId,
2344
+ );
2345
+ return entry.models;
2346
+ }
2347
+ }
2348
+ }
2349
+
2334
2350
  // Use an isolated page only when the main page is actively serving a stream.
2335
2351
  // Startup/idle operations should not open a visible extra tab.
2336
2352
  if (accountId && !isAuthMockEnabled() && isAccountBusy(accountId)) {