@pedrofariasx/qwenproxy 1.2.2 → 1.3.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pedrofariasx/qwenproxy",
3
- "version": "1.2.2",
3
+ "version": "1.3.0",
4
4
  "description": "Local OpenAI-compatible proxy API that routes requests to Qwen (chat.qwen.ai) via Playwright browser automation.",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/src/api/models.ts CHANGED
@@ -4,6 +4,7 @@ import { getBasicHeaders } from '../services/playwright.js'
4
4
  import { loadAccounts } from '../core/accounts.js'
5
5
  import { getAccountCooldownInfo } from '../core/account-manager.js'
6
6
  import { cache } from '../cache/memory-cache.js'
7
+ import { syncModelContextWindows } from '../core/model-registry.js'
7
8
 
8
9
  const app = new Hono()
9
10
 
@@ -80,7 +81,7 @@ app.get('/v1/models', async (c) => {
80
81
  ],
81
82
  }
82
83
 
83
- // Cache the formatted models list for 5 minutes (300 seconds)
84
+ syncModelContextWindows(formatted.data)
84
85
  await cache.set(cacheKey, formatted, 300)
85
86
 
86
87
  return c.json(formatted)
@@ -163,6 +164,7 @@ app.get('/v1/models/:model', async (c) => {
163
164
  ],
164
165
  }
165
166
 
167
+ syncModelContextWindows(formatted.data)
166
168
  await cache.set(cacheKey, formatted, 300)
167
169
  models = formatted.data
168
170
  }
package/src/api/server.ts CHANGED
@@ -70,10 +70,10 @@ app.notFound((c) => c.json({ error: 'Not found' }, 404))
70
70
  export async function startServer(): Promise<void> {
71
71
  await cache.connect()
72
72
 
73
- const { loadAccounts } = await import('../core/accounts.ts')
73
+ const { loadAccounts } = await import('../core/accounts.js')
74
74
  const accounts = loadAccounts()
75
75
 
76
- const { initPlaywright, initPlaywrightForAccount, getQwenHeaders } = await import('../services/playwright.ts')
76
+ const { initPlaywright, initPlaywrightForAccount, getQwenHeaders } = await import('../services/playwright.js')
77
77
 
78
78
  await initPlaywright(config.browser.headless)
79
79
 
@@ -87,7 +87,7 @@ export async function startServer(): Promise<void> {
87
87
  )
88
88
  )
89
89
  console.log('[Server] Pre-fetching headers for all accounts in background...')
90
- const { warmAllPools } = await import('../services/qwen.ts')
90
+ const { warmAllPools } = await import('../services/qwen.js')
91
91
  warmAllPools(accounts.map(a => a.id)).catch(() => {})
92
92
  }
93
93
 
@@ -111,7 +111,7 @@ export async function startServer(): Promise<void> {
111
111
  await cache.close()
112
112
  const { closePlaywright } = await import('../services/playwright.js')
113
113
  await closePlaywright()
114
- const { closeDatabase } = await import('../core/database.ts')
114
+ const { closeDatabase } = await import('../core/database.js')
115
115
  closeDatabase()
116
116
  server?.close()
117
117
  process.exit(0)
@@ -21,6 +21,7 @@ export class MemoryCache {
21
21
  private cleanupInterval: NodeJS.Timeout | null
22
22
  private maxEntries: number
23
23
  private totalBytes: number
24
+ private scanRegexCache: Map<string, RegExp>
24
25
 
25
26
  constructor(options?: { prefix?: string; defaultTTL?: number; maxEntries?: number }) {
26
27
  this.prefix = options?.prefix || 'qwenproxy:'
@@ -29,12 +30,14 @@ export class MemoryCache {
29
30
  this.store = new Map()
30
31
  this.totalBytes = 0
31
32
  this.cleanupInterval = null
33
+ this.scanRegexCache = new Map()
32
34
 
33
35
  this.startCleanup()
34
36
  }
35
37
 
36
38
  private entryByteSize(key: string, value: any): number {
37
- return Buffer.byteLength(key) + Buffer.byteLength(JSON.stringify(value))
39
+ const valueStr = typeof value === 'string' ? value : JSON.stringify(value)
40
+ return Buffer.byteLength(key) + Buffer.byteLength(valueStr || '')
38
41
  }
39
42
 
40
43
  private evictLRU(): void {
@@ -63,11 +66,9 @@ export class MemoryCache {
63
66
  }
64
67
 
65
68
  async set<T>(key: CacheKey, value: T, ttl?: number): Promise<void> {
66
- const serialized = JSON.stringify(value)
67
- const valueBytes = Buffer.byteLength(serialized)
68
69
  const effectiveTTL = ttl || this.defaultTTL
69
70
  const fullKey = this.prefix + key
70
- const entrySize = Buffer.byteLength(fullKey) + valueBytes
71
+ const entrySize = this.entryByteSize(fullKey, value)
71
72
 
72
73
  if (this.store.has(fullKey)) {
73
74
  const oldEntry = this.store.get(fullKey)
@@ -85,7 +86,7 @@ export class MemoryCache {
85
86
  this.totalBytes += entrySize
86
87
 
87
88
  metrics.increment('cache.set')
88
- metrics.histogram('cache.value.size', valueBytes)
89
+ metrics.histogram('cache.value.size', entrySize)
89
90
  }
90
91
 
91
92
  async get<T>(key: CacheKey): Promise<T | null> {
@@ -1,4 +1,4 @@
1
- import { QwenAccount, loadAccounts } from './accounts.ts'
1
+ import { QwenAccount, loadAccounts } from './accounts.js'
2
2
  import { config } from './config.js'
3
3
 
4
4
  let currentIndex = 0
@@ -1,5 +1,5 @@
1
1
  import crypto from 'crypto'
2
- import { getDatabase } from './database.ts'
2
+ import { getDatabase } from './database.js'
3
3
  import { config } from './config.js'
4
4
 
5
5
  export interface QwenAccount {
@@ -1,16 +1,58 @@
1
1
  const modelContextWindows: Record<string, number> = {
2
- 'qwen-max': 32768,
3
- 'qwen-max-latest': 32768,
4
- 'qwen-plus': 131072,
5
- 'qwen-plus-latest': 131072,
6
- 'qwen-turbo': 131072,
7
- 'qwen-turbo-latest': 131072,
8
- 'qwen-long': 1000000,
9
- 'qwen-coder': 131072,
10
- 'qwen-coder-plus': 131072,
2
+ 'qwen3.7-plus': 1000000,
3
+ 'qwen3.7-max': 1000000,
4
+ 'qwen3.6-plus': 1000000,
5
+ 'qwen3.6-plus-preview': 1000000,
6
+ 'qwen3.6-max-preview': 262144,
7
+ 'qwen3.6-27b': 262144,
8
+ 'qwen3.6-35b-a3b': 262144,
9
+ 'qwen3.5-plus': 1000000,
10
+ 'qwen3.5-flash': 1000000,
11
+ 'qwen3.5-omni-plus': 262144,
12
+ 'qwen3.5-omni-flash': 262144,
13
+ 'qwen3.5-max-2026-03-08': 262144,
14
+ 'qwen3.5-397b-a17b': 262144,
15
+ 'qwen3.5-122b-a10b': 262144,
16
+ 'qwen3.5-27b': 262144,
17
+ 'qwen3.5-35b-a3b': 262144,
18
+ 'qwen3-max-2026-01-23': 262144,
19
+ 'qwen3-coder-plus': 1048576,
20
+ 'qwen3-vl-plus': 262144,
21
+ 'qwen3-omni-flash-2025-12-01': 65536,
22
+ 'qwen-plus-2025-07-28': 131072,
23
+ 'qwen-latest-series-invite-beta-v24': 262144,
24
+ 'qwen-latest-series-invite-beta-v16': 1000000,
25
+ }
26
+
27
+ const modelTokenDivisors: Record<string, number> = {
28
+ 'qwen3.7-max': 2.2,
29
+ 'qwen3.6-max-preview': 2.2,
30
+ 'qwen3.5-max-2026-03-08': 2.2,
31
+ 'qwen3-max-2026-01-23': 2.2,
32
+ 'qwen-latest-series-invite-beta-v24': 2.2,
33
+ 'qwen3.7-plus': 2.0,
34
+ 'qwen3.6-plus': 2.0,
35
+ 'qwen3.6-plus-preview': 2.0,
36
+ 'qwen3.5-plus': 2.0,
37
+ 'qwen-plus-2025-07-28': 2.0,
38
+ 'qwen-latest-series-invite-beta-v16': 2.0,
39
+ 'qwen3.5-flash': 1.8,
40
+ 'qwen3.5-omni-plus': 1.8,
41
+ 'qwen3.5-omni-flash': 1.7,
42
+ 'qwen3-omni-flash-2025-12-01': 1.7,
43
+ 'qwen3.5-397b-a17b': 1.9,
44
+ 'qwen3.5-122b-a10b': 1.9,
45
+ 'qwen3.6-35b-a3b': 1.9,
46
+ 'qwen3.5-35b-a3b': 1.9,
47
+ 'qwen3.6-27b': 1.9,
48
+ 'qwen3.5-27b': 1.9,
49
+ 'qwen3-coder-plus': 2.3,
50
+ 'qwen3-vl-plus': 2.1,
11
51
  }
12
52
 
13
53
  const defaultContextWindow = 131072
54
+ const defaultTokenDivisor = 2.0
55
+ export const MAX_PAYLOAD_SIZE = 10 * 1024 * 1024
14
56
 
15
57
  export function setModelContextWindow(modelId: string, contextWindow: number): void {
16
58
  modelContextWindows[modelId] = contextWindow
@@ -21,6 +63,11 @@ export function getModelContextWindow(modelId: string): number {
21
63
  return modelContextWindows[baseId] ?? defaultContextWindow
22
64
  }
23
65
 
66
+ export function getModelTokenDivisor(modelId: string): number {
67
+ const baseId = modelId.replace('-no-thinking', '')
68
+ return modelTokenDivisors[baseId] ?? defaultTokenDivisor
69
+ }
70
+
24
71
  export function syncModelContextWindows(models: Array<{ id: string; context_window?: number }>): void {
25
72
  for (const m of models) {
26
73
  if (m.context_window) {
package/src/login.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { addAccount, removeAccount, listAccounts, getAccountCredentials, QwenAccount } from './core/accounts.ts'
2
- import { initPlaywrightForAccount, closePlaywrightForAccount, BrowserType, launchManualLoginAccount, extractAccountInfoFromContext } from './services/playwright.ts'
1
+ import { addAccount, removeAccount, listAccounts, getAccountCredentials, QwenAccount } from './core/accounts.js'
2
+ import { initPlaywrightForAccount, closePlaywrightForAccount, BrowserType, launchManualLoginAccount, extractAccountInfoFromContext } from './services/playwright.js'
3
3
  import * as readline from 'readline'
4
4
  import * as dotenv from 'dotenv'
5
5
 
@@ -11,18 +11,17 @@
11
11
  import { Context } from 'hono';
12
12
  import { stream as honoStream } from 'hono/streaming';
13
13
  import crypto from 'crypto';
14
- import { createQwenStream, updateSessionParent } from '../services/qwen.ts';
15
- import { OpenAIRequest, ChoiceDelta, Message } from '../utils/types.ts';
16
- import { registry } from '../tools/registry.ts';
17
- import type { FunctionToolDefinition } from '../tools/types.ts';
18
- import { robustParseJSON } from '../utils/json.ts';
19
- import { StreamingToolParser } from '../tools/parser.ts';
20
- import { QwenStreamParser, ParsedChunkResult } from '../utils/qwen-stream-parser.ts';
21
- import { RetryableQwenStreamError } from '../services/qwen.ts';
14
+ import { createQwenStream, updateSessionParent, RetryableQwenStreamError } from '../services/qwen.js';
15
+ import { OpenAIRequest, ChoiceDelta, Message } from '../utils/types.js';
16
+ import { registry } from '../tools/registry.js';
17
+ import type { FunctionToolDefinition } from '../tools/types.js';
18
+ import { robustParseJSON } from '../utils/json.js';
19
+ import { StreamingToolParser } from '../tools/parser.js';
20
+ import { QwenStreamParser, ParsedChunkResult } from '../utils/qwen-stream-parser.js';
22
21
  import { getModelContextWindow } from '../core/model-registry.js'
23
- import { truncateMessages, estimateTokenCount } from '../utils/context-truncation.ts';
24
- import { getNextAccount, getNextAvailableAccount, markAccountRateLimited, getAccountCooldownInfo } from '../core/account-manager.ts';
25
- import { registerStream, removeStream, getStream } from '../core/stream-registry.ts';
22
+ import { truncateMessages, estimateTokenCount } from '../utils/context-truncation.js';
23
+ import { getNextAccount, getNextAvailableAccount, markAccountRateLimited, getAccountCooldownInfo } from '../core/account-manager.js';
24
+ import { registerStream, removeStream, getStream } from '../core/stream-registry.js';
26
25
  import { metrics } from '../core/metrics.js'
27
26
 
28
27
  export interface DeltaResult {
@@ -53,14 +52,6 @@ export function getIncrementalDelta(oldStr: string, newStr: string, prevLength:
53
52
  const actualSuffix = newStr.slice(prevLength - checkLen, prevLength);
54
53
 
55
54
  if (expectedSuffix === actualSuffix) {
56
- if (delta.length <= 4 && oldStr.length > 2000) {
57
- return {
58
- delta: newStr,
59
- matchedContent: oldStr + newStr,
60
- contentLength: newStr.length,
61
- contentSuffix: newStr.slice(-64)
62
- };
63
- }
64
55
  return {
65
56
  delta,
66
57
  matchedContent: newStr,
@@ -73,14 +64,6 @@ export function getIncrementalDelta(oldStr: string, newStr: string, prevLength:
73
64
  // Fallback: startsWith check for edge cases
74
65
  if (newStr.startsWith(oldStr)) {
75
66
  const delta = newStr.slice(oldStr.length);
76
- if (delta.length <= 4 && oldStr.length > 2000) {
77
- return {
78
- delta: newStr,
79
- matchedContent: oldStr + newStr,
80
- contentLength: newStr.length,
81
- contentSuffix: newStr.slice(-64)
82
- };
83
- }
84
67
  return {
85
68
  delta,
86
69
  matchedContent: newStr,
@@ -264,12 +247,12 @@ export async function chatCompletions(c: Context) {
264
247
 
265
248
  const modelId = body.model.replace('-no-thinking', '');
266
249
  const modelContextWindow = getModelContextWindow(modelId)
267
- const estimatedTokens = estimateTokenCount(systemPrompt + prompt);
250
+ const estimatedTokens = estimateTokenCount(systemPrompt + prompt, modelId);
268
251
  const hasTools = Array.isArray(bodyAny.tools) && bodyAny.tools.length > 0;
269
252
 
270
253
  let finalPrompt: string;
271
254
  if (estimatedTokens > modelContextWindow - 1000) {
272
- const truncated = truncateMessages(messages, modelContextWindow, systemPrompt);
255
+ const truncated = truncateMessages(messages, modelContextWindow, systemPrompt, modelId);
273
256
  const truncatedBody = truncated.map(m => `${m.role === 'user' ? 'User' : m.role === 'assistant' ? 'Assistant' : m.role}: ${m.content}`).join('\n\n');
274
257
  finalPrompt = systemPrompt ? `${systemPrompt}\n\n${truncatedBody}` : truncatedBody;
275
258
  } else {
@@ -289,7 +272,7 @@ export async function chatCompletions(c: Context) {
289
272
 
290
273
  // Account selection with fallback on rate-limit/failure
291
274
  let account = getNextAccount();
292
- let triedAccountIds = new Set<string>();
275
+ const triedAccountIds = new Set<string>();
293
276
  let lastError: any = null;
294
277
 
295
278
  let stream: ReadableStream | undefined;
@@ -395,7 +378,6 @@ export async function chatCompletions(c: Context) {
395
378
 
396
379
  const toolCallsOut: any[] = [];
397
380
  let buffer = '';
398
- const hasTools = Array.isArray(bodyAny.tools) && bodyAny.tools.length > 0;
399
381
 
400
382
  const qwenParser = new QwenStreamParser(uiSessionId, {
401
383
  tools: hasTools ? bodyAny.tools : [],
@@ -524,25 +506,13 @@ export async function chatCompletions(c: Context) {
524
506
  const createdTimestamp = Math.floor(Date.now() / 1000);
525
507
 
526
508
  const fastWriteContent = (content: string) => {
527
- const chunk = JSON.stringify({
528
- id: completionId,
529
- object: 'chat.completion.chunk',
530
- created: createdTimestamp,
531
- model: body.model,
532
- choices: [makeChoice({ content })]
533
- });
534
- streamWriter.write(`data: ${chunk}\n\n`);
509
+ const escaped = content.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n').replace(/\r/g, '\\r').replace(/\t/g, '\\t');
510
+ streamWriter.write(`data: {"id":"${completionId}","object":"chat.completion.chunk","created":${createdTimestamp},"model":"${body.model}","choices":[{"index":0,"delta":{"content":"${escaped}"},"logprobs":null,"finish_reason":null}]}\n\n`);
535
511
  };
536
512
 
537
513
  const fastWriteReasoning = (content: string) => {
538
- const chunk = JSON.stringify({
539
- id: completionId,
540
- object: 'chat.completion.chunk',
541
- created: createdTimestamp,
542
- model: body.model,
543
- choices: [makeChoice({ reasoning_content: content })]
544
- });
545
- streamWriter.write(`data: ${chunk}\n\n`);
514
+ const escaped = content.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n').replace(/\r/g, '\\r').replace(/\t/g, '\\t');
515
+ streamWriter.write(`data: {"id":"${completionId}","object":"chat.completion.chunk","created":${createdTimestamp},"model":"${body.model}","choices":[{"index":0,"delta":{"reasoning_content":"${escaped}"},"logprobs":null,"finish_reason":null}]}\n\n`);
546
516
  };
547
517
 
548
518
  writeEvent({
@@ -563,7 +533,6 @@ export async function chatCompletions(c: Context) {
563
533
  let targetResponseId: string | null = null;
564
534
  let targetResponseIdSet = false;
565
535
  let currentThoughtIndex = 0;
566
- const hasTools = Array.isArray(bodyAny.tools) && bodyAny.tools.length > 0;
567
536
  const toolParser = hasTools ? new StreamingToolParser(bodyAny.tools) : null;
568
537
 
569
538
  let buffer = '';
@@ -687,7 +656,9 @@ export async function chatCompletions(c: Context) {
687
656
  }
688
657
  }
689
658
  } catch (e) {
690
- // parse error, ignore partial chunk
659
+ if (dataStr.length > 10) {
660
+ console.warn(`[Chat] SSE parse error for chunk (${dataStr.length} chars):`, (e as Error).message);
661
+ }
691
662
  }
692
663
  }
693
664
 
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import { Context } from "hono";
8
- import { getQwenHeaders } from "../services/playwright.ts";
8
+ import { getQwenHeaders } from "../services/playwright.js";
9
9
  import crypto from "crypto";
10
10
 
11
11
  interface STSResponse {
@@ -11,11 +11,27 @@
11
11
  import { chromium, firefox, webkit, BrowserContext, Page } from 'playwright';
12
12
  import path from 'path';
13
13
  import crypto from 'crypto';
14
- import { QwenAccount } from '../core/accounts.ts';
15
- import { config } from '../core/config.ts';
14
+ import { QwenAccount } from '../core/accounts.js';
15
+ import { config } from '../core/config.js';
16
16
 
17
17
  export type BrowserType = 'chromium' | 'firefox' | 'webkit' | 'chrome' | 'edge';
18
18
 
19
+ interface BrowserEngineConfig {
20
+ engine: typeof chromium | typeof firefox | typeof webkit;
21
+ channel?: string;
22
+ }
23
+
24
+ function resolveBrowserEngine(browserType: BrowserType): BrowserEngineConfig {
25
+ switch (browserType) {
26
+ case 'firefox': return { engine: firefox };
27
+ case 'webkit': return { engine: webkit };
28
+ case 'chrome': return { engine: chromium, channel: 'chrome' };
29
+ case 'edge': return { engine: chromium, channel: 'msedge' };
30
+ case 'chromium':
31
+ default: return { engine: chromium };
32
+ }
33
+ }
34
+
19
35
  let context: BrowserContext | null = null;
20
36
  export let activePage: Page | null = null;
21
37
  const accountContexts = new Map<string, BrowserContext>();
@@ -120,9 +136,14 @@ export async function getBasicHeaders(accountId?: string): Promise<{ cookie: str
120
136
  if (!page) throw new Error('Playwright not initialized');
121
137
 
122
138
  const cookie = await getCookies(accountId);
123
- const userAgent = await page.evaluate(() => navigator.userAgent);
124
-
125
139
  const cacheKey = accountId || 'global';
140
+
141
+ let userAgent = cachedUserAgents.get(cacheKey);
142
+ if (!userAgent) {
143
+ userAgent = await page.evaluate(() => navigator.userAgent);
144
+ cachedUserAgents.set(cacheKey, userAgent);
145
+ }
146
+
126
147
  const cache = getAccountHeaderCache(cacheKey);
127
148
  const bxV = cache.currentHeaders['bx-v'] || '2.5.36';
128
149
  const bxUa = cache.currentHeaders['bx-ua'];
@@ -138,34 +159,11 @@ export async function initPlaywright(headless = true, browserType: BrowserType =
138
159
  }
139
160
 
140
161
  const profilePath = path.resolve('qwen_profiles', '_default');
141
-
142
- let browserEngine;
143
- let channel: string | undefined;
144
-
145
- switch (browserType) {
146
- case 'firefox':
147
- browserEngine = firefox;
148
- break;
149
- case 'webkit':
150
- browserEngine = webkit;
151
- break;
152
- case 'chrome':
153
- browserEngine = chromium;
154
- channel = 'chrome';
155
- break;
156
- case 'edge':
157
- browserEngine = chromium;
158
- channel = 'msedge';
159
- break;
160
- case 'chromium':
161
- default:
162
- browserEngine = chromium;
163
- break;
164
- }
162
+ const { engine, channel } = resolveBrowserEngine(browserType);
165
163
 
166
164
  console.log(`[Playwright] Launching ${browserType}...`);
167
165
 
168
- context = await browserEngine.launchPersistentContext(profilePath, {
166
+ context = await engine.launchPersistentContext(profilePath, {
169
167
  headless,
170
168
  channel,
171
169
  userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36',
@@ -249,45 +247,8 @@ export async function closePlaywright() {
249
247
 
250
248
  export async function loginToQwen(email: string, password: string): Promise<boolean> {
251
249
  if (!activePage) throw new Error('Playwright not initialized');
252
-
253
250
  console.log(`[Playwright] Attempting API login for ${email}...`);
254
-
255
- await activePage.goto('https://chat.qwen.ai/auth', { waitUntil: 'domcontentloaded' });
256
-
257
- const hashedPassword = crypto.createHash('sha256').update(password).digest('hex');
258
-
259
- const result = await activePage.evaluate(async ({ email, password }) => {
260
- try {
261
- const response = await fetch("https://chat.qwen.ai/api/v2/auths/signin", {
262
- method: "POST",
263
- headers: {
264
- "accept": "application/json, text/plain, */*",
265
- "content-type": "application/json",
266
- "source": "web",
267
- "timezone": new Date().toString().split(' (')[0],
268
- "x-request-id": crypto.randomUUID()
269
- },
270
- body: JSON.stringify({ email, password, login_type: "email" })
271
- });
272
- const data = await response.json();
273
- return { ok: response.ok, data };
274
- } catch (e: any) {
275
- return { ok: false, error: e.message };
276
- }
277
- }, { email, password: hashedPassword });
278
-
279
- if (result.ok) {
280
- console.log('[Playwright] API login request successful.');
281
- await activePage.goto('https://chat.qwen.ai/', { waitUntil: 'domcontentloaded' });
282
- const isLogged = !(activePage.url().includes('auth') || activePage.url().includes('login'));
283
- if (isLogged) {
284
- console.log('[Playwright] Login confirmed.');
285
- return true;
286
- }
287
- }
288
-
289
- console.error('[Playwright] Login failed:', result.data || result.error);
290
- return false;
251
+ return loginToQwenWithContext(activePage.context(), activePage, email, password);
291
252
  }
292
253
 
293
254
  async function loginToQwenUI(email: string, password: string): Promise<boolean> {
@@ -341,7 +302,9 @@ export async function getQwenHeaders(forceNew = false, accountId?: string): Prom
341
302
  if (age < HEADERS_TTL) {
342
303
  if (age > HEADERS_TTL * REFRESH_THRESHOLD && !cache.refreshInProgress) {
343
304
  cache.refreshInProgress = true;
344
- getQwenHeaders(true, accountId).finally(() => {
305
+ getQwenHeaders(true, accountId).catch((err) => {
306
+ console.warn(`[Playwright] Background header refresh failed for ${cacheKey}:`, (err as Error).message);
307
+ }).finally(() => {
345
308
  cache.refreshInProgress = false;
346
309
  });
347
310
  }
@@ -374,7 +337,11 @@ async function tryLightweightCookieRefresh(accountId?: string): Promise<{ header
374
337
  try {
375
338
  const cookies = await page.context().cookies();
376
339
  const cookieStr = cookies.map(c => `${c.name}=${c.value}`).join('; ');
377
- const userAgent = await page.evaluate(() => navigator.userAgent);
340
+ let userAgent = cachedUserAgents.get(cacheKey);
341
+ if (!userAgent) {
342
+ userAgent = await page.evaluate(() => navigator.userAgent);
343
+ cachedUserAgents.set(cacheKey, userAgent);
344
+ }
378
345
 
379
346
  const now = Date.now();
380
347
  cookieCaches.set(cacheKey, { cookie: cookieStr, timestamp: now });
@@ -473,7 +440,7 @@ async function _getQwenHeadersInternal(forceNew = false, accountId?: string): Pr
473
440
  console.warn('[Playwright] Detected login page but QWEN_EMAIL/PASSWORD not provided in .env');
474
441
  }
475
442
  } else {
476
- const { getAccountCredentials } = await import('../core/accounts.ts');
443
+ const { getAccountCredentials } = await import('../core/accounts.js');
477
444
  const creds = getAccountCredentials(accountId);
478
445
  if (creds && creds.email && creds.password) {
479
446
  console.log(`[Playwright] Detected login page for account ${creds.email}. Attempting login...`);
@@ -548,7 +515,7 @@ async function _getQwenHeadersInternal(forceNew = false, accountId?: string): Pr
548
515
  cache.lastHeadersTime = Date.now();
549
516
  cache.refreshInProgress = false;
550
517
 
551
- import('./qwen.ts').then(m => m.disableNativeTools(accountId).catch(() => {}));
518
+ import('./qwen.js').then(m => m.disableNativeTools(accountId).catch(() => {}));
552
519
 
553
520
  await route.abort('aborted');
554
521
 
@@ -609,34 +576,11 @@ async function _getQwenHeadersInternal(forceNew = false, accountId?: string): Pr
609
576
 
610
577
  export async function initPlaywrightForAccount(account: QwenAccount, headless = true, browserType: BrowserType = 'chromium') {
611
578
  const profilePath = path.resolve('qwen_profiles', account.id);
612
-
613
- let browserEngine;
614
- let channel: string | undefined;
615
-
616
- switch (browserType) {
617
- case 'firefox':
618
- browserEngine = firefox;
619
- break;
620
- case 'webkit':
621
- browserEngine = webkit;
622
- break;
623
- case 'chrome':
624
- browserEngine = chromium;
625
- channel = 'chrome';
626
- break;
627
- case 'edge':
628
- browserEngine = chromium;
629
- channel = 'msedge';
630
- break;
631
- case 'chromium':
632
- default:
633
- browserEngine = chromium;
634
- break;
635
- }
579
+ const { engine, channel } = resolveBrowserEngine(browserType);
636
580
 
637
581
  console.log(`[Playwright] Launching ${browserType} for account ${account.email}...`);
638
582
 
639
- const acctContext = await browserEngine.launchPersistentContext(profilePath, {
583
+ const acctContext = await engine.launchPersistentContext(profilePath, {
640
584
  headless,
641
585
  channel,
642
586
  userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36',
@@ -666,32 +610,9 @@ export async function initPlaywrightForAccount(account: QwenAccount, headless =
666
610
 
667
611
  export async function launchManualLoginAccount(accountId: string, browserType: BrowserType = 'chromium'): Promise<{ context: BrowserContext, page: Page }> {
668
612
  const profilePath = path.resolve('qwen_profiles', accountId);
669
-
670
- let browserEngine;
671
- let channel: string | undefined;
672
-
673
- switch (browserType) {
674
- case 'firefox':
675
- browserEngine = firefox;
676
- break;
677
- case 'webkit':
678
- browserEngine = webkit;
679
- break;
680
- case 'chrome':
681
- browserEngine = chromium;
682
- channel = 'chrome';
683
- break;
684
- case 'edge':
685
- browserEngine = chromium;
686
- channel = 'msedge';
687
- break;
688
- case 'chromium':
689
- default:
690
- browserEngine = chromium;
691
- break;
692
- }
613
+ const { engine, channel } = resolveBrowserEngine(browserType);
693
614
 
694
- const acctContext = await browserEngine.launchPersistentContext(profilePath, {
615
+ const acctContext = await engine.launchPersistentContext(profilePath, {
695
616
  headless: false,
696
617
  channel,
697
618
  userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36',