@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 +1 -1
- package/src/api/models.ts +3 -1
- package/src/api/server.ts +4 -4
- package/src/cache/memory-cache.ts +6 -5
- package/src/core/account-manager.ts +1 -1
- package/src/core/accounts.ts +1 -1
- package/src/core/model-registry.ts +56 -9
- package/src/login.ts +2 -2
- package/src/routes/chat.ts +20 -49
- package/src/routes/upload.ts +1 -1
- package/src/services/playwright.ts +42 -121
- package/src/services/qwen.ts +30 -17
- package/src/tests/concurrency.test.ts +1 -1
- package/src/tests/concurrentChat.test.ts +1 -1
- package/src/tests/contextTruncation.test.ts +142 -0
- package/src/tests/delta.test.ts +80 -10
- package/src/tests/jsonFix.test.ts +127 -98
- package/src/tests/multimodal.test.ts +1 -1
- package/src/tests/parser.test.ts +104 -24
- package/src/tools/parser.ts +94 -23
- package/src/utils/context-truncation.ts +13 -11
- package/src/utils/json.ts +130 -10
- package/src/utils/types.ts +1 -1
package/package.json
CHANGED
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
|
-
|
|
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.
|
|
73
|
+
const { loadAccounts } = await import('../core/accounts.js')
|
|
74
74
|
const accounts = loadAccounts()
|
|
75
75
|
|
|
76
|
-
const { initPlaywright, initPlaywrightForAccount, getQwenHeaders } = await import('../services/playwright.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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 =
|
|
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',
|
|
89
|
+
metrics.histogram('cache.value.size', entrySize)
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
async get<T>(key: CacheKey): Promise<T | null> {
|
package/src/core/accounts.ts
CHANGED
|
@@ -1,16 +1,58 @@
|
|
|
1
1
|
const modelContextWindows: Record<string, number> = {
|
|
2
|
-
'
|
|
3
|
-
'
|
|
4
|
-
'
|
|
5
|
-
'
|
|
6
|
-
'
|
|
7
|
-
'
|
|
8
|
-
'
|
|
9
|
-
'
|
|
10
|
-
'
|
|
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.
|
|
2
|
-
import { initPlaywrightForAccount, closePlaywrightForAccount, BrowserType, launchManualLoginAccount, extractAccountInfoFromContext } from './services/playwright.
|
|
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
|
|
package/src/routes/chat.ts
CHANGED
|
@@ -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.
|
|
15
|
-
import { OpenAIRequest, ChoiceDelta, Message } from '../utils/types.
|
|
16
|
-
import { registry } from '../tools/registry.
|
|
17
|
-
import type { FunctionToolDefinition } from '../tools/types.
|
|
18
|
-
import { robustParseJSON } from '../utils/json.
|
|
19
|
-
import { StreamingToolParser } from '../tools/parser.
|
|
20
|
-
import { QwenStreamParser, ParsedChunkResult } from '../utils/qwen-stream-parser.
|
|
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.
|
|
24
|
-
import { getNextAccount, getNextAvailableAccount, markAccountRateLimited, getAccountCooldownInfo } from '../core/account-manager.
|
|
25
|
-
import { registerStream, removeStream, getStream } from '../core/stream-registry.
|
|
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
|
-
|
|
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
|
|
528
|
-
|
|
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
|
|
539
|
-
|
|
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
|
-
|
|
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
|
|
package/src/routes/upload.ts
CHANGED
|
@@ -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.
|
|
15
|
-
import { config } from '../core/config.
|
|
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
|
|
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).
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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',
|