@pedrofariasx/qwenproxy 1.2.2 → 1.2.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.
- package/package.json +1 -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/login.ts +2 -2
- package/src/routes/chat.ts +15 -30
- package/src/routes/upload.ts +1 -1
- package/src/services/playwright.ts +39 -120
- package/src/services/qwen.ts +7 -14
- 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 +110 -98
- package/src/tests/multimodal.test.ts +1 -1
- package/src/tests/parser.test.ts +40 -2
- package/src/tools/parser.ts +88 -20
- package/src/utils/context-truncation.ts +1 -1
- package/src/utils/json.ts +9 -8
- package/src/utils/types.ts +1 -1
package/package.json
CHANGED
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
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 {
|
|
@@ -289,7 +288,7 @@ export async function chatCompletions(c: Context) {
|
|
|
289
288
|
|
|
290
289
|
// Account selection with fallback on rate-limit/failure
|
|
291
290
|
let account = getNextAccount();
|
|
292
|
-
|
|
291
|
+
const triedAccountIds = new Set<string>();
|
|
293
292
|
let lastError: any = null;
|
|
294
293
|
|
|
295
294
|
let stream: ReadableStream | undefined;
|
|
@@ -395,7 +394,6 @@ export async function chatCompletions(c: Context) {
|
|
|
395
394
|
|
|
396
395
|
const toolCallsOut: any[] = [];
|
|
397
396
|
let buffer = '';
|
|
398
|
-
const hasTools = Array.isArray(bodyAny.tools) && bodyAny.tools.length > 0;
|
|
399
397
|
|
|
400
398
|
const qwenParser = new QwenStreamParser(uiSessionId, {
|
|
401
399
|
tools: hasTools ? bodyAny.tools : [],
|
|
@@ -524,25 +522,13 @@ export async function chatCompletions(c: Context) {
|
|
|
524
522
|
const createdTimestamp = Math.floor(Date.now() / 1000);
|
|
525
523
|
|
|
526
524
|
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`);
|
|
525
|
+
const escaped = content.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n').replace(/\r/g, '\\r').replace(/\t/g, '\\t');
|
|
526
|
+
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
527
|
};
|
|
536
528
|
|
|
537
529
|
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`);
|
|
530
|
+
const escaped = content.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n').replace(/\r/g, '\\r').replace(/\t/g, '\\t');
|
|
531
|
+
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
532
|
};
|
|
547
533
|
|
|
548
534
|
writeEvent({
|
|
@@ -563,7 +549,6 @@ export async function chatCompletions(c: Context) {
|
|
|
563
549
|
let targetResponseId: string | null = null;
|
|
564
550
|
let targetResponseIdSet = false;
|
|
565
551
|
let currentThoughtIndex = 0;
|
|
566
|
-
const hasTools = Array.isArray(bodyAny.tools) && bodyAny.tools.length > 0;
|
|
567
552
|
const toolParser = hasTools ? new StreamingToolParser(bodyAny.tools) : null;
|
|
568
553
|
|
|
569
554
|
let buffer = '';
|
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> {
|
|
@@ -374,7 +335,11 @@ async function tryLightweightCookieRefresh(accountId?: string): Promise<{ header
|
|
|
374
335
|
try {
|
|
375
336
|
const cookies = await page.context().cookies();
|
|
376
337
|
const cookieStr = cookies.map(c => `${c.name}=${c.value}`).join('; ');
|
|
377
|
-
|
|
338
|
+
let userAgent = cachedUserAgents.get(cacheKey);
|
|
339
|
+
if (!userAgent) {
|
|
340
|
+
userAgent = await page.evaluate(() => navigator.userAgent);
|
|
341
|
+
cachedUserAgents.set(cacheKey, userAgent);
|
|
342
|
+
}
|
|
378
343
|
|
|
379
344
|
const now = Date.now();
|
|
380
345
|
cookieCaches.set(cacheKey, { cookie: cookieStr, timestamp: now });
|
|
@@ -473,7 +438,7 @@ async function _getQwenHeadersInternal(forceNew = false, accountId?: string): Pr
|
|
|
473
438
|
console.warn('[Playwright] Detected login page but QWEN_EMAIL/PASSWORD not provided in .env');
|
|
474
439
|
}
|
|
475
440
|
} else {
|
|
476
|
-
const { getAccountCredentials } = await import('../core/accounts.
|
|
441
|
+
const { getAccountCredentials } = await import('../core/accounts.js');
|
|
477
442
|
const creds = getAccountCredentials(accountId);
|
|
478
443
|
if (creds && creds.email && creds.password) {
|
|
479
444
|
console.log(`[Playwright] Detected login page for account ${creds.email}. Attempting login...`);
|
|
@@ -548,7 +513,7 @@ async function _getQwenHeadersInternal(forceNew = false, accountId?: string): Pr
|
|
|
548
513
|
cache.lastHeadersTime = Date.now();
|
|
549
514
|
cache.refreshInProgress = false;
|
|
550
515
|
|
|
551
|
-
import('./qwen.
|
|
516
|
+
import('./qwen.js').then(m => m.disableNativeTools(accountId).catch(() => {}));
|
|
552
517
|
|
|
553
518
|
await route.abort('aborted');
|
|
554
519
|
|
|
@@ -609,34 +574,11 @@ async function _getQwenHeadersInternal(forceNew = false, accountId?: string): Pr
|
|
|
609
574
|
|
|
610
575
|
export async function initPlaywrightForAccount(account: QwenAccount, headless = true, browserType: BrowserType = 'chromium') {
|
|
611
576
|
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
|
-
}
|
|
577
|
+
const { engine, channel } = resolveBrowserEngine(browserType);
|
|
636
578
|
|
|
637
579
|
console.log(`[Playwright] Launching ${browserType} for account ${account.email}...`);
|
|
638
580
|
|
|
639
|
-
const acctContext = await
|
|
581
|
+
const acctContext = await engine.launchPersistentContext(profilePath, {
|
|
640
582
|
headless,
|
|
641
583
|
channel,
|
|
642
584
|
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 +608,9 @@ export async function initPlaywrightForAccount(account: QwenAccount, headless =
|
|
|
666
608
|
|
|
667
609
|
export async function launchManualLoginAccount(accountId: string, browserType: BrowserType = 'chromium'): Promise<{ context: BrowserContext, page: Page }> {
|
|
668
610
|
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
|
-
}
|
|
611
|
+
const { engine, channel } = resolveBrowserEngine(browserType);
|
|
693
612
|
|
|
694
|
-
const acctContext = await
|
|
613
|
+
const acctContext = await engine.launchPersistentContext(profilePath, {
|
|
695
614
|
headless: false,
|
|
696
615
|
channel,
|
|
697
616
|
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36',
|
package/src/services/qwen.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getQwenHeaders, getBasicHeaders } from './playwright.
|
|
1
|
+
import { getQwenHeaders, getBasicHeaders } from './playwright.js';
|
|
2
2
|
import crypto from 'crypto';
|
|
3
3
|
|
|
4
4
|
const CACHED_TIMEZONE = new Date().toString().split(' (')[0];
|
|
@@ -28,8 +28,7 @@ interface SessionEntry {
|
|
|
28
28
|
timestamp: number;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
const sessionStates: Map<string, SessionEntry> =
|
|
32
|
-
(globalThis as any)._sessionStates = sessionStates;
|
|
31
|
+
const sessionStates: Map<string, SessionEntry> = new Map();
|
|
33
32
|
const SESSION_TTL_MS = 24 * 60 * 60 * 1000;
|
|
34
33
|
|
|
35
34
|
function cleanupStaleSessions() {
|
|
@@ -65,11 +64,9 @@ interface WarmPoolEntry {
|
|
|
65
64
|
timestamp: number;
|
|
66
65
|
}
|
|
67
66
|
|
|
68
|
-
const warmPool: Map<string, WarmPoolEntry[]> =
|
|
69
|
-
(globalThis as any)._warmPool = warmPool;
|
|
67
|
+
const warmPool: Map<string, WarmPoolEntry[]> = new Map();
|
|
70
68
|
|
|
71
|
-
const refillPromises: Map<string, Promise<void>> =
|
|
72
|
-
(globalThis as any)._refillPromises = refillPromises;
|
|
69
|
+
const refillPromises: Map<string, Promise<void>> = new Map();
|
|
73
70
|
|
|
74
71
|
const WARM_POOL_SIZE = 5;
|
|
75
72
|
const WARM_POOL_TTL_MS = 10 * 60 * 1000;
|
|
@@ -78,9 +75,8 @@ function cleanupStalePool(accountId: string) {
|
|
|
78
75
|
const pool = warmPool.get(accountId);
|
|
79
76
|
if (!pool) return;
|
|
80
77
|
const now = Date.now();
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
78
|
+
const filtered = pool.filter(e => now - e.timestamp <= WARM_POOL_TTL_MS);
|
|
79
|
+
if (filtered.length !== pool.length) warmPool.set(accountId, filtered);
|
|
84
80
|
}
|
|
85
81
|
|
|
86
82
|
async function getBasicQwenHeaders(accountId?: string): Promise<Record<string, string>> {
|
|
@@ -93,8 +89,6 @@ async function getBasicQwenHeaders(accountId?: string): Promise<Record<string, s
|
|
|
93
89
|
}
|
|
94
90
|
|
|
95
91
|
async function createRealQwenChat(header: Record<string, string>): Promise<string> {
|
|
96
|
-
const controller = new AbortController();
|
|
97
|
-
const timeoutId = setTimeout(() => controller.abort(), 30000);
|
|
98
92
|
const response = await fetch('https://chat.qwen.ai/api/v2/chats/new', {
|
|
99
93
|
method: 'POST',
|
|
100
94
|
headers: {
|
|
@@ -116,9 +110,8 @@ async function createRealQwenChat(header: Record<string, string>): Promise<strin
|
|
|
116
110
|
timestamp: Date.now(),
|
|
117
111
|
project_id: '',
|
|
118
112
|
}),
|
|
119
|
-
signal:
|
|
113
|
+
signal: AbortSignal.timeout(30000),
|
|
120
114
|
});
|
|
121
|
-
clearTimeout(timeoutId);
|
|
122
115
|
|
|
123
116
|
if (!response.ok) throw new Error(`Failed to create chat: ${response.status}`);
|
|
124
117
|
const json = await response.json();
|
|
@@ -4,7 +4,7 @@ import assert from 'node:assert';
|
|
|
4
4
|
process.env.TEST_MOCK_PLAYWRIGHT = 'true';
|
|
5
5
|
|
|
6
6
|
import { app } from '../api/server.js';
|
|
7
|
-
import { initPlaywright, closePlaywright } from '../services/playwright.
|
|
7
|
+
import { initPlaywright, closePlaywright } from '../services/playwright.js';
|
|
8
8
|
|
|
9
9
|
test('Concurrent requests are serialized by mutex', async () => {
|
|
10
10
|
const originalFetch = globalThis.fetch;
|
|
@@ -3,7 +3,7 @@ import assert from 'node:assert';
|
|
|
3
3
|
import net from 'node:net';
|
|
4
4
|
import { serve } from '@hono/node-server';
|
|
5
5
|
import { app } from '../api/server.js';
|
|
6
|
-
import { initPlaywright, closePlaywright } from '../services/playwright.
|
|
6
|
+
import { initPlaywright, closePlaywright } from '../services/playwright.js';
|
|
7
7
|
|
|
8
8
|
function isPortAvailable(port: number): Promise<boolean> {
|
|
9
9
|
return new Promise((resolve) => {
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { estimateTokenCount, truncateMessages } from '../utils/context-truncation.js';
|
|
4
|
+
|
|
5
|
+
test('estimateTokenCount: returns 0 for empty string', () => {
|
|
6
|
+
assert.strictEqual(estimateTokenCount(''), 0);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
test('estimateTokenCount: estimates tokens conservatively using 2.5 divisor', () => {
|
|
10
|
+
assert.strictEqual(estimateTokenCount('hello'), 2);
|
|
11
|
+
assert.strictEqual(estimateTokenCount('a'.repeat(100)), 40);
|
|
12
|
+
assert.strictEqual(estimateTokenCount('a'.repeat(250)), 100);
|
|
13
|
+
assert.strictEqual(estimateTokenCount('a'.repeat(2500)), 1000);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('estimateTokenCount: handles single character', () => {
|
|
17
|
+
assert.strictEqual(estimateTokenCount('x'), 1);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('estimateTokenCount: rounds up for non-multiples of 2.5', () => {
|
|
21
|
+
assert.strictEqual(estimateTokenCount('ab'), 1);
|
|
22
|
+
assert.strictEqual(estimateTokenCount('abc'), 2);
|
|
23
|
+
assert.strictEqual(estimateTokenCount('abcd'), 2);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('truncateMessages: returns all messages when within context window', () => {
|
|
27
|
+
const messages = [
|
|
28
|
+
{ role: 'system', content: 'You are helpful.' },
|
|
29
|
+
{ role: 'user', content: 'Hello' },
|
|
30
|
+
{ role: 'assistant', content: 'Hi there!' },
|
|
31
|
+
];
|
|
32
|
+
const result = truncateMessages(messages, 100000);
|
|
33
|
+
assert.strictEqual(result.length, 3);
|
|
34
|
+
assert.strictEqual(result[0].content, 'You are helpful.');
|
|
35
|
+
assert.strictEqual(result[1].content, 'Hello');
|
|
36
|
+
assert.strictEqual(result[2].content, 'Hi there!');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('truncateMessages: preserves chronological order', () => {
|
|
40
|
+
const messages = [
|
|
41
|
+
{ role: 'user', content: 'first' },
|
|
42
|
+
{ role: 'assistant', content: 'second' },
|
|
43
|
+
{ role: 'user', content: 'third' },
|
|
44
|
+
];
|
|
45
|
+
const result = truncateMessages(messages, 100000);
|
|
46
|
+
assert.strictEqual(result[0].role, 'user');
|
|
47
|
+
assert.strictEqual(result[0].content, 'first');
|
|
48
|
+
assert.strictEqual(result[1].role, 'assistant');
|
|
49
|
+
assert.strictEqual(result[2].role, 'user');
|
|
50
|
+
assert.strictEqual(result[2].content, 'third');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('truncateMessages: drops oldest messages first when exceeding context', () => {
|
|
54
|
+
const largeContent = 'x'.repeat(5000);
|
|
55
|
+
const messages = [
|
|
56
|
+
{ role: 'user', content: largeContent },
|
|
57
|
+
{ role: 'assistant', content: largeContent },
|
|
58
|
+
{ role: 'user', content: 'latest message' },
|
|
59
|
+
];
|
|
60
|
+
const result = truncateMessages(messages, 2000);
|
|
61
|
+
const lastMsg = result[result.length - 1];
|
|
62
|
+
assert.ok(lastMsg.content.includes('latest message') || lastMsg.content.includes('[Truncated]'));
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('truncateMessages: returns system prompt as fallback when context is extremely small', () => {
|
|
66
|
+
const messages = [
|
|
67
|
+
{ role: 'user', content: 'some content' },
|
|
68
|
+
];
|
|
69
|
+
const systemPrompt = 'system instructions';
|
|
70
|
+
const result = truncateMessages(messages, 10, systemPrompt);
|
|
71
|
+
assert.strictEqual(result.length, 1);
|
|
72
|
+
assert.strictEqual(result[0].role, 'user');
|
|
73
|
+
assert.strictEqual(result[0].content, systemPrompt);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('truncateMessages: handles array content in messages', () => {
|
|
77
|
+
const messages = [
|
|
78
|
+
{
|
|
79
|
+
role: 'user',
|
|
80
|
+
content: [
|
|
81
|
+
{ type: 'text', text: 'hello' },
|
|
82
|
+
{ type: 'image_url', image_url: { url: 'data:image/png;base64,...' } },
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
];
|
|
86
|
+
const result = truncateMessages(messages, 100000);
|
|
87
|
+
assert.strictEqual(result.length, 1);
|
|
88
|
+
assert.ok(result[0].content.includes('hello'));
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('truncateMessages: handles null content', () => {
|
|
92
|
+
const messages = [
|
|
93
|
+
{ role: 'user', content: null },
|
|
94
|
+
{ role: 'assistant', content: 'response' },
|
|
95
|
+
];
|
|
96
|
+
const result = truncateMessages(messages, 100000);
|
|
97
|
+
assert.strictEqual(result.length, 2);
|
|
98
|
+
assert.strictEqual(result[0].content, '');
|
|
99
|
+
assert.strictEqual(result[1].content, 'response');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('truncateMessages: handles object content', () => {
|
|
103
|
+
const messages = [
|
|
104
|
+
{ role: 'user', content: { structured: 'data', value: 42 } },
|
|
105
|
+
];
|
|
106
|
+
const result = truncateMessages(messages, 100000);
|
|
107
|
+
assert.strictEqual(result.length, 1);
|
|
108
|
+
assert.ok(result[0].content.includes('structured'));
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('truncateMessages: truncates partially fitting message with marker', () => {
|
|
112
|
+
const messages = [
|
|
113
|
+
{ role: 'user', content: 'a'.repeat(10000) },
|
|
114
|
+
];
|
|
115
|
+
const result = truncateMessages(messages, 1000);
|
|
116
|
+
assert.strictEqual(result.length, 1);
|
|
117
|
+
assert.ok(
|
|
118
|
+
result[0].content.includes('[Truncated]') || result[0].content.length < 10000,
|
|
119
|
+
'Should truncate or mark as truncated'
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('truncateMessages: accounts for system prompt in available tokens', () => {
|
|
124
|
+
const systemPrompt = 'x'.repeat(2000);
|
|
125
|
+
const messages = [
|
|
126
|
+
{ role: 'user', content: 'short' },
|
|
127
|
+
];
|
|
128
|
+
const withSystem = truncateMessages(messages, 2000, systemPrompt);
|
|
129
|
+
const withoutSystem = truncateMessages(messages, 2000);
|
|
130
|
+
assert.ok(withSystem.length <= withoutSystem.length);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('truncateMessages: handles empty messages array', () => {
|
|
134
|
+
const result = truncateMessages([], 100000);
|
|
135
|
+
assert.strictEqual(result.length, 0);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('truncateMessages: handles empty messages with system prompt fallback', () => {
|
|
139
|
+
const result = truncateMessages([], 5, 'fallback');
|
|
140
|
+
assert.strictEqual(result.length, 1);
|
|
141
|
+
assert.strictEqual(result[0].content, 'fallback');
|
|
142
|
+
});
|