@pedrofariasx/qwenproxy 1.5.0 → 1.6.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 +4 -4
- package/src/services/playwright.ts +480 -14
- package/src/services/qwen.ts +294 -140
package/package.json
CHANGED
package/src/api/models.ts
CHANGED
|
@@ -41,9 +41,9 @@ app.get('/v1/models', async (c) => {
|
|
|
41
41
|
'X-Request-Id': crypto.randomUUID(),
|
|
42
42
|
'source': 'web',
|
|
43
43
|
'bx-v': bxV,
|
|
44
|
-
'sec-ch-ua': '"Chromium";v="
|
|
44
|
+
'sec-ch-ua': '"Chromium";v="137", "Google Chrome";v="137", "Not/A)Brand";v="99"',
|
|
45
45
|
'sec-ch-ua-mobile': '?0',
|
|
46
|
-
'sec-ch-ua-platform': '"
|
|
46
|
+
'sec-ch-ua-platform': '"Windows"',
|
|
47
47
|
'Timezone': new Date().toString(),
|
|
48
48
|
'Cookie': cookie,
|
|
49
49
|
},
|
|
@@ -125,9 +125,9 @@ app.get('/v1/models/:model', async (c) => {
|
|
|
125
125
|
'X-Request-Id': crypto.randomUUID(),
|
|
126
126
|
'source': 'web',
|
|
127
127
|
'bx-v': bxV,
|
|
128
|
-
'sec-ch-ua': '"Chromium";v="
|
|
128
|
+
'sec-ch-ua': '"Chromium";v="137", "Google Chrome";v="137", "Not/A)Brand";v="99"',
|
|
129
129
|
'sec-ch-ua-mobile': '?0',
|
|
130
|
-
'sec-ch-ua-platform': '"
|
|
130
|
+
'sec-ch-ua-platform': '"Windows"',
|
|
131
131
|
'Timezone': new Date().toString(),
|
|
132
132
|
'Cookie': cookie,
|
|
133
133
|
},
|
|
@@ -61,19 +61,22 @@ function getAccountHeaderCache(accountId: string): AccountHeaderCache {
|
|
|
61
61
|
return cache;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
const HEADERS_TTL =
|
|
64
|
+
const HEADERS_TTL = 5 * 60 * 1000;
|
|
65
65
|
const COOKIE_CACHE_TTL = 5 * 60 * 1000;
|
|
66
66
|
const cookieCaches = new Map<string, { cookie: string, timestamp: number }>();
|
|
67
67
|
const REFRESH_THRESHOLD = 0.7;
|
|
68
68
|
|
|
69
69
|
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
|
70
70
|
|
|
71
|
+
export const CHROME_UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36';
|
|
72
|
+
export const CHROME_CLIENT_HINTS = '"Chromium";v="137", "Google Chrome";v="137", "Not/A)Brand";v="99"';
|
|
73
|
+
|
|
71
74
|
function getStealthScript(): string {
|
|
72
75
|
return `
|
|
76
|
+
try {
|
|
77
|
+
delete navigator.__proto__.webdriver;
|
|
78
|
+
} catch(e) {}
|
|
73
79
|
Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
|
|
74
|
-
Object.defineProperty(navigator, 'plugins', {
|
|
75
|
-
get: () => [1, 2, 3, 4, 5],
|
|
76
|
-
});
|
|
77
80
|
Object.defineProperty(navigator, 'languages', {
|
|
78
81
|
get: () => ['pt-BR', 'pt', 'en-US', 'en'],
|
|
79
82
|
});
|
|
@@ -82,32 +85,153 @@ function getStealthScript(): string {
|
|
|
82
85
|
Object.defineProperty(navigator, 'platform', { get: () => 'Win32' });
|
|
83
86
|
Object.defineProperty(screen, 'colorDepth', { get: () => 24 });
|
|
84
87
|
Object.defineProperty(screen, 'pixelDepth', { get: () => 24 });
|
|
88
|
+
|
|
85
89
|
window.chrome = {
|
|
86
|
-
runtime: {},
|
|
87
|
-
loadTimes: function() {},
|
|
88
|
-
csi: function() {},
|
|
89
|
-
app: {},
|
|
90
|
+
runtime: { onConnect: {}, onMessage: {} },
|
|
91
|
+
loadTimes: function() { return {}; },
|
|
92
|
+
csi: function() { return {}; },
|
|
93
|
+
app: { isInstalled: false, InstallState: { DISABLED: 'disabled', INSTALLED: 'installed', NOT_INSTALLED: 'not_installed' }, RunningState: { CANNOT_RUN: 'cannot_run', READY_TO_RUN: 'ready_to_run', RUNNING: 'running' } },
|
|
90
94
|
};
|
|
95
|
+
|
|
91
96
|
const originalQuery = window.navigator.permissions.query;
|
|
92
97
|
window.navigator.permissions.query = (parameters) =>
|
|
93
98
|
parameters.name === 'notifications'
|
|
94
|
-
? Promise.resolve({ state: Notification.permission })
|
|
99
|
+
? Promise.resolve({ state: (typeof Notification !== 'undefined' ? Notification.permission : 'default'), onchange: null })
|
|
95
100
|
: originalQuery(parameters);
|
|
101
|
+
|
|
96
102
|
const getParameter = WebGLRenderingContext.prototype.getParameter;
|
|
97
103
|
WebGLRenderingContext.prototype.getParameter = function(parameter) {
|
|
98
104
|
if (parameter === 37445) return 'Intel Inc.';
|
|
99
105
|
if (parameter === 37446) return 'Intel Iris OpenGL Engine';
|
|
100
106
|
return getParameter.apply(this, arguments);
|
|
101
107
|
};
|
|
108
|
+
if (typeof WebGL2RenderingContext !== 'undefined') {
|
|
109
|
+
const getParameter2 = WebGL2RenderingContext.prototype.getParameter;
|
|
110
|
+
WebGL2RenderingContext.prototype.getParameter = function(parameter) {
|
|
111
|
+
if (parameter === 37445) return 'Intel Inc.';
|
|
112
|
+
if (parameter === 37446) return 'Intel Iris OpenGL Engine';
|
|
113
|
+
return getParameter2.apply(this, arguments);
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
102
117
|
Object.defineProperty(navigator, 'connection', {
|
|
103
118
|
get: () => ({
|
|
104
119
|
effectiveType: '4g',
|
|
105
120
|
rtt: 50,
|
|
106
121
|
downlink: 10,
|
|
107
122
|
saveData: false,
|
|
123
|
+
addEventListener: () => {},
|
|
124
|
+
removeEventListener: () => {},
|
|
108
125
|
}),
|
|
109
126
|
});
|
|
110
|
-
|
|
127
|
+
|
|
128
|
+
(function() {
|
|
129
|
+
function makeMime(desc, suffixes, type) {
|
|
130
|
+
const m = { description: desc, suffixes: suffixes, type: type };
|
|
131
|
+
return m;
|
|
132
|
+
}
|
|
133
|
+
const pdfMime = makeMime('Portable Document Format', 'pdf', 'application/pdf');
|
|
134
|
+
const pdfxMime = makeMime('Portable Document Format', 'pdf', 'text/pdf');
|
|
135
|
+
const pdfPlugin = {
|
|
136
|
+
name: 'PDF Viewer',
|
|
137
|
+
description: 'Portable Document Format',
|
|
138
|
+
filename: 'internal-pdf-viewer',
|
|
139
|
+
length: 2,
|
|
140
|
+
0: pdfMime,
|
|
141
|
+
1: pdfxMime,
|
|
142
|
+
};
|
|
143
|
+
pdfMime.enabledPlugin = pdfPlugin;
|
|
144
|
+
pdfxMime.enabledPlugin = pdfPlugin;
|
|
145
|
+
|
|
146
|
+
const chromePdfMime = makeMime('Portable Document Format', 'pdf', 'application/pdf');
|
|
147
|
+
const chromePdfMime2 = makeMime('Portable Document Format', 'pdf', 'text/pdf');
|
|
148
|
+
const chromePdfPlugin = {
|
|
149
|
+
name: 'Chrome PDF Viewer',
|
|
150
|
+
description: 'Portable Document Format',
|
|
151
|
+
filename: 'internal-pdf-viewer',
|
|
152
|
+
length: 2,
|
|
153
|
+
0: chromePdfMime,
|
|
154
|
+
1: chromePdfMime2,
|
|
155
|
+
};
|
|
156
|
+
chromePdfMime.enabledPlugin = chromePdfPlugin;
|
|
157
|
+
chromePdfMime2.enabledPlugin = chromePdfPlugin;
|
|
158
|
+
|
|
159
|
+
const nativePlugin = {
|
|
160
|
+
name: 'Native Client',
|
|
161
|
+
description: '',
|
|
162
|
+
filename: 'internal-nacl-plugin',
|
|
163
|
+
length: 2,
|
|
164
|
+
0: makeMime('Native Client Executable', '', 'application/x-nacl'),
|
|
165
|
+
1: makeMime('Portable Native Client Executable', '', 'application/x-pnacl'),
|
|
166
|
+
};
|
|
167
|
+
nativePlugin[0].enabledPlugin = nativePlugin;
|
|
168
|
+
nativePlugin[1].enabledPlugin = nativePlugin;
|
|
169
|
+
|
|
170
|
+
const pluginsList = [pdfPlugin, chromePdfPlugin, nativePlugin];
|
|
171
|
+
const mimeList = [pdfMime, pdfxMime, chromePdfMime, chromePdfMime2, nativePlugin[0], nativePlugin[1]];
|
|
172
|
+
|
|
173
|
+
function makeNamedNodeMap(items, namedEntries) {
|
|
174
|
+
const arr = [...items];
|
|
175
|
+
for (const [k, v] of namedEntries) arr[k] = v;
|
|
176
|
+
arr.item = function(i) { return this[i] || null; };
|
|
177
|
+
arr.namedItem = function(name) { return this[name] || null; };
|
|
178
|
+
arr.refresh = function() {};
|
|
179
|
+
return arr;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const pluginEntries = pluginsList.map((p, i) => [p.name, p]);
|
|
183
|
+
const mimeEntries = mimeList.map((m) => [m.type, m]);
|
|
184
|
+
|
|
185
|
+
const pluginsArr = makeNamedNodeMap(pluginsList, pluginEntries);
|
|
186
|
+
const mimeArr = makeNamedNodeMap(mimeList, mimeEntries);
|
|
187
|
+
|
|
188
|
+
Object.defineProperty(navigator, 'plugins', { get: () => pluginsArr });
|
|
189
|
+
Object.defineProperty(navigator, 'mimeTypes', { get: () => mimeArr });
|
|
190
|
+
})();
|
|
191
|
+
|
|
192
|
+
(function() {
|
|
193
|
+
const _toDataURL = HTMLCanvasElement.prototype.toDataURL;
|
|
194
|
+
const _toBlob = HTMLCanvasElement.prototype.toBlob;
|
|
195
|
+
const _getImageData = CanvasRenderingContext2D.prototype.getImageData;
|
|
196
|
+
|
|
197
|
+
function addNoise(canvas) {
|
|
198
|
+
try {
|
|
199
|
+
const ctx = canvas.getContext('2d');
|
|
200
|
+
if (!ctx) return;
|
|
201
|
+
const style = ctx.fillStyle;
|
|
202
|
+
ctx.fillStyle = 'rgba(255,255,255,0.01)';
|
|
203
|
+
ctx.fillRect(0, 0, 1, 1);
|
|
204
|
+
ctx.fillStyle = style;
|
|
205
|
+
} catch(e) {}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
HTMLCanvasElement.prototype.toDataURL = function(...args) {
|
|
209
|
+
addNoise(this);
|
|
210
|
+
return _toDataURL.apply(this, args);
|
|
211
|
+
};
|
|
212
|
+
HTMLCanvasElement.prototype.toBlob = function(...args) {
|
|
213
|
+
addNoise(this);
|
|
214
|
+
return _toBlob.apply(this, args);
|
|
215
|
+
};
|
|
216
|
+
})();
|
|
217
|
+
|
|
218
|
+
(function() {
|
|
219
|
+
if (typeof OfflineAudioContext === 'undefined') return;
|
|
220
|
+
const _startRendering = OfflineAudioContext.prototype.startRendering;
|
|
221
|
+
OfflineAudioContext.prototype.startRendering = function() {
|
|
222
|
+
return _startRendering.apply(this, arguments).then(buffer => {
|
|
223
|
+
try {
|
|
224
|
+
for (let ch = 0; ch < buffer.numberOfChannels; ch++) {
|
|
225
|
+
const data = buffer.getChannelData(ch);
|
|
226
|
+
for (let i = 0; i < Math.min(data.length, 100); i++) {
|
|
227
|
+
data[i] += (Math.random() - 0.5) * 1e-7;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
} catch(e) {}
|
|
231
|
+
return buffer;
|
|
232
|
+
});
|
|
233
|
+
};
|
|
234
|
+
})();
|
|
111
235
|
`;
|
|
112
236
|
}
|
|
113
237
|
|
|
@@ -228,7 +352,7 @@ export async function initPlaywright(headless = true, browserType: BrowserType =
|
|
|
228
352
|
context = await engine.launchPersistentContext(profilePath, {
|
|
229
353
|
headless,
|
|
230
354
|
channel,
|
|
231
|
-
userAgent:
|
|
355
|
+
userAgent: CHROME_UA,
|
|
232
356
|
ignoreDefaultArgs: ['--enable-automation'],
|
|
233
357
|
args: [
|
|
234
358
|
'--disable-blink-features=AutomationControlled',
|
|
@@ -355,7 +479,118 @@ async function loginToQwenUI(email: string, password: string): Promise<boolean>
|
|
|
355
479
|
return false;
|
|
356
480
|
}
|
|
357
481
|
|
|
482
|
+
let guestContext: BrowserContext | null = null;
|
|
483
|
+
let guestPage: Page | null = null;
|
|
484
|
+
let guestHeadersCache: { headers: Record<string, string>, timestamp: number } | null = null;
|
|
485
|
+
const GUEST_HEADERS_TTL = 30 * 60 * 1000;
|
|
486
|
+
|
|
487
|
+
export async function getGuestHeaders(): Promise<Record<string, string>> {
|
|
488
|
+
if (guestHeadersCache && (Date.now() - guestHeadersCache.timestamp) < GUEST_HEADERS_TTL) {
|
|
489
|
+
return guestHeadersCache.headers;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (!guestPage) {
|
|
493
|
+
const profilePath = path.resolve('qwen_profiles', '_guest');
|
|
494
|
+
const { engine, channel } = resolveBrowserEngine('chromium');
|
|
495
|
+
guestContext = await engine.launchPersistentContext(profilePath, {
|
|
496
|
+
headless: config.browser.headless,
|
|
497
|
+
channel,
|
|
498
|
+
userAgent: CHROME_UA,
|
|
499
|
+
ignoreDefaultArgs: ['--enable-automation'],
|
|
500
|
+
args: ['--disable-blink-features=AutomationControlled', '--disable-features=IsolateOrigins,site-per-process', '--disable-infobars', '--no-first-run', '--no-default-browser-check']
|
|
501
|
+
});
|
|
502
|
+
await guestContext.addInitScript(getStealthScript());
|
|
503
|
+
guestPage = await guestContext.newPage();
|
|
504
|
+
|
|
505
|
+
await guestPage.goto('https://chat.qwen.ai/c/guest', { waitUntil: 'domcontentloaded' });
|
|
506
|
+
|
|
507
|
+
try {
|
|
508
|
+
const keepSessionBtn = await guestPage.$('button:has-text("Manter sessão terminada"), button:has-text("Keep session ended"), button:has-text("Manter sessão encerrada")');
|
|
509
|
+
if (keepSessionBtn) {
|
|
510
|
+
await keepSessionBtn.click();
|
|
511
|
+
console.log('[Playwright] Guest: Clicked "Manter sessão terminada"');
|
|
512
|
+
await sleep(1000);
|
|
513
|
+
}
|
|
514
|
+
} catch (e) {
|
|
515
|
+
// Modal might not be there
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
return new Promise((resolve, reject) => {
|
|
520
|
+
const timeout = setTimeout(() => reject(new Error('Timeout getting guest headers')), 30000);
|
|
521
|
+
|
|
522
|
+
const routeHandler = async (route: any, request: any) => {
|
|
523
|
+
clearTimeout(timeout);
|
|
524
|
+
const reqHeaders = request.headers();
|
|
525
|
+
console.log('[Playwright] Guest intercepted request:', request.url());
|
|
526
|
+
|
|
527
|
+
const extractedHeaders = {
|
|
528
|
+
'cookie': reqHeaders['cookie'] || '',
|
|
529
|
+
'bx-ua': reqHeaders['bx-ua'] || '',
|
|
530
|
+
'bx-umidtoken': reqHeaders['bx-umidtoken'] || '',
|
|
531
|
+
'bx-v': reqHeaders['bx-v'] || '2.5.36',
|
|
532
|
+
'user-agent': reqHeaders['user-agent'] || CHROME_UA,
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
if (extractedHeaders['bx-ua']) {
|
|
536
|
+
console.log('[Playwright] Guest: Successfully captured bx-ua');
|
|
537
|
+
guestHeadersCache = { headers: extractedHeaders, timestamp: Date.now() };
|
|
538
|
+
await route.abort('aborted');
|
|
539
|
+
await guestPage!.unroute('**/api/v2/chat/completions*', routeHandler);
|
|
540
|
+
|
|
541
|
+
import('./qwen.js').then(m => m.disableNativeTools('guest').catch(() => {}));
|
|
542
|
+
|
|
543
|
+
resolve(extractedHeaders);
|
|
544
|
+
} else {
|
|
545
|
+
console.log('[Playwright] Guest: Request missing bx-ua, continuing route. Headers:', Object.keys(reqHeaders));
|
|
546
|
+
await route.continue();
|
|
547
|
+
// If it's the completions request and we still don't have bx-ua, we might need to resolve anyway
|
|
548
|
+
// or the UI interaction failed to trigger the SDK.
|
|
549
|
+
if (request.url().includes('/api/v2/chat/completions')) {
|
|
550
|
+
console.warn('[Playwright] Guest: Completions request made without bx-ua. Resolving with available headers.');
|
|
551
|
+
guestHeadersCache = { headers: extractedHeaders, timestamp: Date.now() };
|
|
552
|
+
await guestPage!.unroute('**/api/v2/chat/completions*', routeHandler);
|
|
553
|
+
resolve(extractedHeaders);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
guestPage!.route('**/api/v2/chat/completions*', routeHandler).then(async () => {
|
|
559
|
+
const inputSelector = 'textarea:visible, [contenteditable="true"]:visible';
|
|
560
|
+
try {
|
|
561
|
+
await guestPage!.waitForSelector(inputSelector, { timeout: 10000 });
|
|
562
|
+
await guestPage!.focus(inputSelector);
|
|
563
|
+
await guestPage!.fill(inputSelector, '');
|
|
564
|
+
await guestPage!.type(inputSelector, 'a', { delay: 50 });
|
|
565
|
+
await sleep(1000);
|
|
566
|
+
|
|
567
|
+
const selectors = ['.message-input-right-button-send .send-button', '.chat-prompt-send-button', 'button.send-button'];
|
|
568
|
+
let clicked = false;
|
|
569
|
+
for (const selector of selectors) {
|
|
570
|
+
const btn = await guestPage!.$(selector);
|
|
571
|
+
if (btn && await btn.isVisible()) {
|
|
572
|
+
await btn.click({ force: true, delay: 50 }).catch(() => {});
|
|
573
|
+
clicked = true;
|
|
574
|
+
break;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
if (!clicked) {
|
|
578
|
+
await guestPage!.keyboard.press('Enter');
|
|
579
|
+
}
|
|
580
|
+
} catch (e) {
|
|
581
|
+
clearTimeout(timeout);
|
|
582
|
+
reject(e);
|
|
583
|
+
}
|
|
584
|
+
});
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
|
|
358
588
|
export async function getQwenHeaders(forceNew = false, accountId?: string): Promise<{ headers: Record<string, string>, chatSessionId: string, parentMessageId: string | null }> {
|
|
589
|
+
if (accountId === 'guest') {
|
|
590
|
+
const headers = await getGuestHeaders();
|
|
591
|
+
return { headers, chatSessionId: 'guest-session', parentMessageId: null };
|
|
592
|
+
}
|
|
593
|
+
|
|
359
594
|
const cacheKey = accountId || 'global';
|
|
360
595
|
const cache = getAccountHeaderCache(cacheKey);
|
|
361
596
|
|
|
@@ -476,7 +711,7 @@ async function _getQwenHeadersInternal(forceNew = false, accountId?: string): Pr
|
|
|
476
711
|
const isOnQwen = currentUrl.includes('chat.qwen.ai');
|
|
477
712
|
const isOnSpecificChat = isOnQwen && /\/c\//.test(currentUrl);
|
|
478
713
|
|
|
479
|
-
if (!isOnQwen || forceNew
|
|
714
|
+
if (!isOnQwen || forceNew) {
|
|
480
715
|
console.log(`[Playwright] Navigating to Qwen home for ${cacheKey}... (Current: ${currentUrl})`);
|
|
481
716
|
await page.goto('https://chat.qwen.ai/', { waitUntil: 'domcontentloaded' });
|
|
482
717
|
}
|
|
@@ -645,7 +880,7 @@ export async function initPlaywrightForAccount(account: QwenAccount, headless =
|
|
|
645
880
|
const acctContext = await engine.launchPersistentContext(profilePath, {
|
|
646
881
|
headless,
|
|
647
882
|
channel,
|
|
648
|
-
userAgent:
|
|
883
|
+
userAgent: CHROME_UA,
|
|
649
884
|
ignoreDefaultArgs: ['--enable-automation'],
|
|
650
885
|
args: [
|
|
651
886
|
'--disable-blink-features=AutomationControlled',
|
|
@@ -696,7 +931,7 @@ export async function launchManualLoginAccount(accountId: string, browserType: B
|
|
|
696
931
|
const acctContext = await engine.launchPersistentContext(profilePath, {
|
|
697
932
|
headless: false,
|
|
698
933
|
channel,
|
|
699
|
-
userAgent:
|
|
934
|
+
userAgent: CHROME_UA,
|
|
700
935
|
ignoreDefaultArgs: ['--enable-automation'],
|
|
701
936
|
args: [
|
|
702
937
|
'--disable-blink-features=AutomationControlled',
|
|
@@ -779,3 +1014,234 @@ async function loginToQwenWithContext(acctContext: BrowserContext, acctPage: Pag
|
|
|
779
1014
|
console.error(`[Playwright] Login failed for ${email}:`, result.data || result.error);
|
|
780
1015
|
return false;
|
|
781
1016
|
}
|
|
1017
|
+
|
|
1018
|
+
export function getPageForAccount(accountId?: string): Page | null {
|
|
1019
|
+
if (accountId === 'guest') return guestPage;
|
|
1020
|
+
if (accountId) return accountPages.get(accountId) || null;
|
|
1021
|
+
return activePage;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
const streamCallbacks = new Map<string, {
|
|
1025
|
+
onChunk: (chunk: string) => void;
|
|
1026
|
+
onEnd: () => void;
|
|
1027
|
+
onError: (msg: string) => void;
|
|
1028
|
+
onMeta: (meta: { status: number; statusText: string; contentType: string; headers: Record<string, string> }) => void;
|
|
1029
|
+
onBody: (body: string) => void;
|
|
1030
|
+
}>();
|
|
1031
|
+
|
|
1032
|
+
const abortControllers = new Map<string, () => void>();
|
|
1033
|
+
|
|
1034
|
+
const pagesWithExposed = new WeakSet<Page>();
|
|
1035
|
+
|
|
1036
|
+
async function ensureStreamBridge(page: Page): Promise<void> {
|
|
1037
|
+
if (pagesWithExposed.has(page)) return;
|
|
1038
|
+
pagesWithExposed.add(page);
|
|
1039
|
+
await page.exposeFunction('__streamRelay', (reqId: string, type: string, data: any) => {
|
|
1040
|
+
const cb = streamCallbacks.get(reqId);
|
|
1041
|
+
if (!cb) return;
|
|
1042
|
+
switch (type) {
|
|
1043
|
+
case 'meta': cb.onMeta(data); break;
|
|
1044
|
+
case 'chunk': cb.onChunk(data); break;
|
|
1045
|
+
case 'end': cb.onEnd(); streamCallbacks.delete(reqId); abortControllers.delete(reqId); break;
|
|
1046
|
+
case 'error': cb.onError(data); streamCallbacks.delete(reqId); abortControllers.delete(reqId); break;
|
|
1047
|
+
case 'body': cb.onBody(data); streamCallbacks.delete(reqId); abortControllers.delete(reqId); break;
|
|
1048
|
+
}
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
export async function browserFetch(
|
|
1053
|
+
page: Page,
|
|
1054
|
+
url: string,
|
|
1055
|
+
options: {
|
|
1056
|
+
method?: string;
|
|
1057
|
+
headers?: Record<string, string>;
|
|
1058
|
+
body?: string;
|
|
1059
|
+
timeoutMs?: number;
|
|
1060
|
+
} = {},
|
|
1061
|
+
): Promise<{ status: number; statusText: string; contentType: string; body: string; headers: Record<string, string> }> {
|
|
1062
|
+
await ensureStreamBridge(page);
|
|
1063
|
+
const reqId = crypto.randomUUID();
|
|
1064
|
+
|
|
1065
|
+
return page.evaluate(async ({ url, options, reqId }: any) => {
|
|
1066
|
+
const controller = new AbortController();
|
|
1067
|
+
const timeoutId = setTimeout(() => controller.abort(), options.timeoutMs || 30000);
|
|
1068
|
+
try {
|
|
1069
|
+
const resp = await fetch(url, {
|
|
1070
|
+
method: options.method || 'POST',
|
|
1071
|
+
headers: options.headers || {},
|
|
1072
|
+
body: options.body || undefined,
|
|
1073
|
+
signal: controller.signal,
|
|
1074
|
+
});
|
|
1075
|
+
clearTimeout(timeoutId);
|
|
1076
|
+
const respHeaders: Record<string, string> = {};
|
|
1077
|
+
resp.headers.forEach((v: string, k: string) => { respHeaders[k] = v; });
|
|
1078
|
+
const body = await resp.text();
|
|
1079
|
+
return {
|
|
1080
|
+
status: resp.status,
|
|
1081
|
+
statusText: resp.statusText,
|
|
1082
|
+
contentType: resp.headers.get('content-type') || '',
|
|
1083
|
+
body,
|
|
1084
|
+
headers: respHeaders,
|
|
1085
|
+
};
|
|
1086
|
+
} catch (e: any) {
|
|
1087
|
+
clearTimeout(timeoutId);
|
|
1088
|
+
throw new Error(`browserFetch failed: ${e.message}`);
|
|
1089
|
+
}
|
|
1090
|
+
}, { url, options, reqId });
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
export async function browserStreamFetch(
|
|
1094
|
+
page: Page,
|
|
1095
|
+
url: string,
|
|
1096
|
+
options: {
|
|
1097
|
+
method?: string;
|
|
1098
|
+
headers?: Record<string, string>;
|
|
1099
|
+
body?: string;
|
|
1100
|
+
timeoutMs?: number;
|
|
1101
|
+
} = {},
|
|
1102
|
+
): Promise<{
|
|
1103
|
+
status: number;
|
|
1104
|
+
statusText: string;
|
|
1105
|
+
contentType: string;
|
|
1106
|
+
headers: Record<string, string>;
|
|
1107
|
+
stream: ReadableStream<Uint8Array>;
|
|
1108
|
+
body: string;
|
|
1109
|
+
reqId: string;
|
|
1110
|
+
abort: () => void;
|
|
1111
|
+
}> {
|
|
1112
|
+
await ensureStreamBridge(page);
|
|
1113
|
+
const reqId = crypto.randomUUID();
|
|
1114
|
+
const enc = new TextEncoder();
|
|
1115
|
+
|
|
1116
|
+
let metaResolve!: (value: { status: number; statusText: string; contentType: string; headers: Record<string, string> }) => void;
|
|
1117
|
+
const metaPromise = new Promise<{ status: number; statusText: string; contentType: string; headers: Record<string, string> }>((resolve) => {
|
|
1118
|
+
metaResolve = resolve;
|
|
1119
|
+
});
|
|
1120
|
+
|
|
1121
|
+
const metaTimeout = setTimeout(() => {
|
|
1122
|
+
streamCallbacks.delete(reqId);
|
|
1123
|
+
abortControllers.delete(reqId);
|
|
1124
|
+
metaResolve({ status: 0, statusText: 'Timeout', contentType: '', headers: {} });
|
|
1125
|
+
}, options.timeoutMs || 130000);
|
|
1126
|
+
|
|
1127
|
+
streamCallbacks.set(reqId, {
|
|
1128
|
+
onMeta: (meta) => {
|
|
1129
|
+
clearTimeout(metaTimeout);
|
|
1130
|
+
metaResolve(meta);
|
|
1131
|
+
},
|
|
1132
|
+
onChunk: () => {},
|
|
1133
|
+
onEnd: () => {},
|
|
1134
|
+
onError: () => {},
|
|
1135
|
+
onBody: () => {},
|
|
1136
|
+
});
|
|
1137
|
+
|
|
1138
|
+
let abortFn = () => {};
|
|
1139
|
+
let bodyResolve!: (value: string) => void;
|
|
1140
|
+
const bodyPromise = new Promise<string>((resolve) => { bodyResolve = resolve; });
|
|
1141
|
+
|
|
1142
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
1143
|
+
start(controller) {
|
|
1144
|
+
const cb = streamCallbacks.get(reqId);
|
|
1145
|
+
if (!cb) return;
|
|
1146
|
+
cb.onChunk = (chunk: string) => {
|
|
1147
|
+
try { controller.enqueue(enc.encode(chunk)); } catch {}
|
|
1148
|
+
};
|
|
1149
|
+
cb.onEnd = () => {
|
|
1150
|
+
try { controller.close(); } catch {}
|
|
1151
|
+
streamCallbacks.delete(reqId);
|
|
1152
|
+
abortControllers.delete(reqId);
|
|
1153
|
+
};
|
|
1154
|
+
cb.onError = (msg: string) => {
|
|
1155
|
+
try { controller.error(new Error(msg)); } catch {}
|
|
1156
|
+
streamCallbacks.delete(reqId);
|
|
1157
|
+
abortControllers.delete(reqId);
|
|
1158
|
+
};
|
|
1159
|
+
cb.onBody = (text: string) => {
|
|
1160
|
+
bodyResolve(text);
|
|
1161
|
+
streamCallbacks.delete(reqId);
|
|
1162
|
+
abortControllers.delete(reqId);
|
|
1163
|
+
};
|
|
1164
|
+
|
|
1165
|
+
page.evaluate(async ({ url, options, reqId }: any) => {
|
|
1166
|
+
const controller = new AbortController();
|
|
1167
|
+
(window as any).__abortControllers = (window as any).__abortControllers || {};
|
|
1168
|
+
(window as any).__abortControllers[reqId] = controller;
|
|
1169
|
+
const timeoutId = setTimeout(() => controller.abort(), options.timeoutMs || 130000);
|
|
1170
|
+
try {
|
|
1171
|
+
const resp = await fetch(url, {
|
|
1172
|
+
method: options.method || 'POST',
|
|
1173
|
+
headers: options.headers || {},
|
|
1174
|
+
body: options.body || undefined,
|
|
1175
|
+
signal: controller.signal,
|
|
1176
|
+
});
|
|
1177
|
+
clearTimeout(timeoutId);
|
|
1178
|
+
const respHeaders: Record<string, string> = {};
|
|
1179
|
+
resp.headers.forEach((v: string, k: string) => { respHeaders[k] = v; });
|
|
1180
|
+
(window as any).__streamRelay(reqId, 'meta', {
|
|
1181
|
+
status: resp.status,
|
|
1182
|
+
statusText: resp.statusText,
|
|
1183
|
+
contentType: resp.headers.get('content-type') || '',
|
|
1184
|
+
headers: respHeaders,
|
|
1185
|
+
});
|
|
1186
|
+
|
|
1187
|
+
if (!resp.ok || !resp.body) {
|
|
1188
|
+
const bodyText = await resp.text();
|
|
1189
|
+
(window as any).__streamRelay(reqId, 'body', bodyText);
|
|
1190
|
+
delete (window as any).__abortControllers[reqId];
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
const reader = resp.body.getReader();
|
|
1195
|
+
const decoder = new TextDecoder();
|
|
1196
|
+
while (true) {
|
|
1197
|
+
const { done, value } = await reader.read();
|
|
1198
|
+
if (done) {
|
|
1199
|
+
(window as any).__streamRelay(reqId, 'end', null);
|
|
1200
|
+
break;
|
|
1201
|
+
}
|
|
1202
|
+
(window as any).__streamRelay(reqId, 'chunk', decoder.decode(value, { stream: true }));
|
|
1203
|
+
}
|
|
1204
|
+
delete (window as any).__abortControllers[reqId];
|
|
1205
|
+
} catch (e: any) {
|
|
1206
|
+
clearTimeout(timeoutId);
|
|
1207
|
+
(window as any).__streamRelay(reqId, 'error', e.message);
|
|
1208
|
+
delete (window as any).__abortControllers[reqId];
|
|
1209
|
+
}
|
|
1210
|
+
}, { url, options, reqId }).catch((e: any) => {
|
|
1211
|
+
const cb = streamCallbacks.get(reqId);
|
|
1212
|
+
if (cb) {
|
|
1213
|
+
cb.onError(e.message);
|
|
1214
|
+
}
|
|
1215
|
+
});
|
|
1216
|
+
},
|
|
1217
|
+
cancel() {
|
|
1218
|
+
page.evaluate((reqId: string) => {
|
|
1219
|
+
const c = (window as any).__abortControllers?.[reqId];
|
|
1220
|
+
if (c) { c.abort(); delete (window as any).__abortControllers[reqId]; }
|
|
1221
|
+
}, reqId).catch(() => {});
|
|
1222
|
+
streamCallbacks.delete(reqId);
|
|
1223
|
+
abortControllers.delete(reqId);
|
|
1224
|
+
},
|
|
1225
|
+
});
|
|
1226
|
+
|
|
1227
|
+
const meta = await metaPromise;
|
|
1228
|
+
|
|
1229
|
+
abortFn = () => {
|
|
1230
|
+
page.evaluate((reqId: string) => {
|
|
1231
|
+
const c = (window as any).__abortControllers?.[reqId];
|
|
1232
|
+
if (c) { c.abort(); delete (window as any).__abortControllers[reqId]; }
|
|
1233
|
+
}, reqId).catch(() => {});
|
|
1234
|
+
streamCallbacks.delete(reqId);
|
|
1235
|
+
abortControllers.delete(reqId);
|
|
1236
|
+
};
|
|
1237
|
+
|
|
1238
|
+
abortControllers.set(reqId, abortFn);
|
|
1239
|
+
|
|
1240
|
+
return {
|
|
1241
|
+
...meta,
|
|
1242
|
+
stream,
|
|
1243
|
+
body: meta.contentType.includes('text/event-stream') ? '' : await bodyPromise,
|
|
1244
|
+
reqId,
|
|
1245
|
+
abort: abortFn,
|
|
1246
|
+
};
|
|
1247
|
+
}
|
package/src/services/qwen.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getQwenHeaders, getBasicHeaders } from './playwright.js';
|
|
1
|
+
import { getQwenHeaders, getBasicHeaders, getGuestHeaders, getPageForAccount, browserFetch, browserStreamFetch, CHROME_CLIENT_HINTS, CHROME_UA } from './playwright.js';
|
|
2
2
|
import { MAX_PAYLOAD_SIZE } from '../core/model-registry.js';
|
|
3
3
|
import { markAccountRateLimited } from '../core/account-manager.js';
|
|
4
4
|
import crypto from 'crypto';
|
|
@@ -11,7 +11,7 @@ const sleep = (ms: number) => new Promise(r => setTimeout(r, ms));
|
|
|
11
11
|
|
|
12
12
|
function getClientHintsHeaders(): Record<string, string> {
|
|
13
13
|
return {
|
|
14
|
-
'sec-ch-ua':
|
|
14
|
+
'sec-ch-ua': CHROME_CLIENT_HINTS,
|
|
15
15
|
'sec-ch-ua-mobile': '?0',
|
|
16
16
|
'sec-ch-ua-platform': '"Windows"',
|
|
17
17
|
};
|
|
@@ -106,7 +106,58 @@ async function getBasicQwenHeaders(accountId?: string): Promise<Record<string, s
|
|
|
106
106
|
};
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
async function createRealQwenChat(header: Record<string, string
|
|
109
|
+
async function createRealQwenChat(header: Record<string, string>, accountId?: string): Promise<string> {
|
|
110
|
+
const page = getPageForAccount(accountId);
|
|
111
|
+
const body = JSON.stringify({
|
|
112
|
+
title: 'Nova Conversa',
|
|
113
|
+
models: ['qwen3.7-plus'],
|
|
114
|
+
chat_mode: 'normal',
|
|
115
|
+
chat_type: 't2t',
|
|
116
|
+
timestamp: Date.now(),
|
|
117
|
+
project_id: '',
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const pageUrl = page?.url() || '';
|
|
121
|
+
const isOnQwenOrigin = pageUrl.includes('chat.qwen.ai');
|
|
122
|
+
|
|
123
|
+
if (page && !page.isClosed() && isOnQwenOrigin) {
|
|
124
|
+
try {
|
|
125
|
+
const result = await browserFetch(page, 'https://chat.qwen.ai/api/v2/chats/new', {
|
|
126
|
+
method: 'POST',
|
|
127
|
+
headers: {
|
|
128
|
+
'accept': 'application/json, text/plain, */*',
|
|
129
|
+
'content-type': 'application/json',
|
|
130
|
+
'x-request-id': crypto.randomUUID(),
|
|
131
|
+
'timezone': CACHED_TIMEZONE,
|
|
132
|
+
},
|
|
133
|
+
body,
|
|
134
|
+
timeoutMs: 30000,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
if (result.status === 429) {
|
|
138
|
+
throw new QwenUpstreamError('Qwen upstream error: RateLimited: Too many requests.', 'RateLimited', 429);
|
|
139
|
+
}
|
|
140
|
+
if (!result.status || result.status >= 400) {
|
|
141
|
+
throw new Error(`Failed to create chat: ${result.status} - ${result.body}`);
|
|
142
|
+
}
|
|
143
|
+
const json = JSON.parse(result.body);
|
|
144
|
+
if (json && json.success === false) {
|
|
145
|
+
const code = json.data?.code || json.code || 'UpstreamError';
|
|
146
|
+
const details = json.data?.details || json.message || 'Qwen returned an error';
|
|
147
|
+
const wait = json.data?.num !== undefined ? ` Wait about ${json.data.num} hour(s) before trying again.` : '';
|
|
148
|
+
let status = 502;
|
|
149
|
+
if (code === 'RateLimited') status = 429;
|
|
150
|
+
throw new QwenUpstreamError(`Qwen upstream error: ${code}: ${details}.${wait}`, code, status);
|
|
151
|
+
}
|
|
152
|
+
const chatId = json.chat_id || json.id || json.data?.chat_id || json.data?.id;
|
|
153
|
+
if (!chatId) throw new Error(`Unexpected chat response: ${JSON.stringify(json).slice(0, 200)}`);
|
|
154
|
+
return chatId;
|
|
155
|
+
} catch (err: any) {
|
|
156
|
+
if (err instanceof QwenUpstreamError) throw err;
|
|
157
|
+
console.warn('[WarmPool] browserFetch failed for chat creation, falling back to Node.js fetch:', err.message);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
110
161
|
const response = await fetch('https://chat.qwen.ai/api/v2/chats/new', {
|
|
111
162
|
method: 'POST',
|
|
112
163
|
headers: {
|
|
@@ -123,25 +174,14 @@ async function createRealQwenChat(header: Record<string, string>): Promise<strin
|
|
|
123
174
|
'bx-umidtoken': header['bx-umidtoken'] || '',
|
|
124
175
|
...getClientHintsHeaders(),
|
|
125
176
|
},
|
|
126
|
-
body
|
|
127
|
-
title: 'Nova Conversa',
|
|
128
|
-
models: ['qwen3.7-plus'],
|
|
129
|
-
chat_mode: 'normal',
|
|
130
|
-
chat_type: 't2t',
|
|
131
|
-
timestamp: Date.now(),
|
|
132
|
-
project_id: '',
|
|
133
|
-
}),
|
|
177
|
+
body,
|
|
134
178
|
signal: AbortSignal.timeout(30000),
|
|
135
179
|
});
|
|
136
180
|
|
|
137
181
|
if (!response.ok) {
|
|
138
182
|
const errText = await response.text().catch(() => '');
|
|
139
183
|
if (response.status === 429) {
|
|
140
|
-
throw new QwenUpstreamError(
|
|
141
|
-
'Qwen upstream error: RateLimited: Too many requests.',
|
|
142
|
-
'RateLimited',
|
|
143
|
-
429
|
|
144
|
-
);
|
|
184
|
+
throw new QwenUpstreamError('Qwen upstream error: RateLimited: Too many requests.', 'RateLimited', 429);
|
|
145
185
|
}
|
|
146
186
|
throw new Error(`Failed to create chat: ${response.status} - ${errText}`);
|
|
147
187
|
}
|
|
@@ -149,16 +189,10 @@ async function createRealQwenChat(header: Record<string, string>): Promise<strin
|
|
|
149
189
|
if (json && json.success === false) {
|
|
150
190
|
const code = json.data?.code || json.code || 'UpstreamError';
|
|
151
191
|
const details = json.data?.details || json.message || 'Qwen returned an error';
|
|
152
|
-
const wait = json.data?.num !== undefined
|
|
153
|
-
? ` Wait about ${json.data.num} hour(s) before trying again.`
|
|
154
|
-
: '';
|
|
192
|
+
const wait = json.data?.num !== undefined ? ` Wait about ${json.data.num} hour(s) before trying again.` : '';
|
|
155
193
|
let status = 502;
|
|
156
194
|
if (code === 'RateLimited') status = 429;
|
|
157
|
-
throw new QwenUpstreamError(
|
|
158
|
-
`Qwen upstream error: ${code}: ${details}.${wait}`,
|
|
159
|
-
code,
|
|
160
|
-
status
|
|
161
|
-
);
|
|
195
|
+
throw new QwenUpstreamError(`Qwen upstream error: ${code}: ${details}.${wait}`, code, status);
|
|
162
196
|
}
|
|
163
197
|
const chatId = json.chat_id || json.id || json.data?.chat_id || json.data?.id;
|
|
164
198
|
if (!chatId) throw new Error(`Unexpected chat response: ${JSON.stringify(json).slice(0, 200)}`);
|
|
@@ -181,10 +215,14 @@ async function refillPoolForAccount(accountId: string) {
|
|
|
181
215
|
return;
|
|
182
216
|
}
|
|
183
217
|
|
|
184
|
-
const
|
|
218
|
+
const acctId = accountId === 'global' ? undefined : accountId;
|
|
219
|
+
for (let i = 0; i < need; i++) {
|
|
220
|
+
if (i > 0) {
|
|
221
|
+
await sleep(800 + Math.floor(Math.random() * 2200));
|
|
222
|
+
}
|
|
185
223
|
try {
|
|
186
|
-
const chatId = await createRealQwenChat(headers);
|
|
187
|
-
|
|
224
|
+
const chatId = await createRealQwenChat(headers, acctId);
|
|
225
|
+
pool.push({ chatId, headers, accountId, timestamp: Date.now() });
|
|
188
226
|
} catch (err: any) {
|
|
189
227
|
if (err instanceof QwenUpstreamError) {
|
|
190
228
|
if (err.upstreamCode === 'RateLimited' || err.upstreamStatus === 429) {
|
|
@@ -192,16 +230,11 @@ async function refillPoolForAccount(accountId: string) {
|
|
|
192
230
|
const cooldownMs = hourHint ? parseInt(hourHint[1]) * 60 * 60 * 1000 : undefined;
|
|
193
231
|
markAccountRateLimited(accountId, cooldownMs, 'RateLimited');
|
|
194
232
|
console.warn(`[WarmPool] Account ${accountId} rate-limited during chat creation. Marked for cooldown.`);
|
|
233
|
+
break;
|
|
195
234
|
}
|
|
196
235
|
}
|
|
197
236
|
console.error(`[WarmPool] chat creation failed for ${accountId}:`, (err as Error).message);
|
|
198
|
-
return null;
|
|
199
237
|
}
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
const results = await Promise.all(creationPromises);
|
|
203
|
-
for (const entry of results) {
|
|
204
|
-
if (entry) pool.push(entry);
|
|
205
238
|
}
|
|
206
239
|
}
|
|
207
240
|
|
|
@@ -304,6 +337,32 @@ export async function disableNativeTools(accountId?: string): Promise<void> {
|
|
|
304
337
|
};
|
|
305
338
|
|
|
306
339
|
console.log(`[Qwen] Disabling native tools for ${cacheKey}...`);
|
|
340
|
+
const page = getPageForAccount(accountId);
|
|
341
|
+
if (page && !page.isClosed() && page.url().includes('chat.qwen.ai')) {
|
|
342
|
+
try {
|
|
343
|
+
const result = await browserFetch(page, 'https://chat.qwen.ai/api/v2/users/user/settings/update', {
|
|
344
|
+
method: 'POST',
|
|
345
|
+
headers: {
|
|
346
|
+
'accept': 'application/json, text/plain, */*',
|
|
347
|
+
'content-type': 'application/json',
|
|
348
|
+
'x-request-id': crypto.randomUUID(),
|
|
349
|
+
'timezone': CACHED_TIMEZONE,
|
|
350
|
+
},
|
|
351
|
+
body: JSON.stringify(payload),
|
|
352
|
+
timeoutMs: 30000,
|
|
353
|
+
});
|
|
354
|
+
if (result.status && result.status < 400) {
|
|
355
|
+
console.log(`[Qwen] Native tools disabled successfully for ${cacheKey}.`);
|
|
356
|
+
nativeToolsDisabled.add(cacheKey);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
console.error(`[Qwen] Failed to disable native tools for ${cacheKey}: ${result.status} - ${result.body}`);
|
|
360
|
+
return;
|
|
361
|
+
} catch (err: any) {
|
|
362
|
+
console.warn('[Qwen] browserFetch failed for disableNativeTools, falling back:', err.message);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
307
366
|
const controller = new AbortController();
|
|
308
367
|
const timeoutId = setTimeout(() => controller.abort(), 30000);
|
|
309
368
|
const response = await fetch('https://chat.qwen.ai/api/v2/users/user/settings/update', {
|
|
@@ -346,6 +405,27 @@ export async function fetchQwenModels(accountId?: string): Promise<any[]> {
|
|
|
346
405
|
return cachedModels;
|
|
347
406
|
}
|
|
348
407
|
|
|
408
|
+
const page = getPageForAccount(accountId);
|
|
409
|
+
if (page && !page.isClosed() && page.url().includes('chat.qwen.ai')) {
|
|
410
|
+
try {
|
|
411
|
+
const result = await browserFetch(page, 'https://chat.qwen.ai/api/models', {
|
|
412
|
+
method: 'GET',
|
|
413
|
+
headers: {
|
|
414
|
+
'accept': 'application/json, text/plain, */*',
|
|
415
|
+
'x-request-id': crypto.randomUUID(),
|
|
416
|
+
'timezone': CACHED_TIMEZONE,
|
|
417
|
+
'source': 'web',
|
|
418
|
+
},
|
|
419
|
+
timeoutMs: 30000,
|
|
420
|
+
});
|
|
421
|
+
if (result.status && result.status < 400) {
|
|
422
|
+
return processModelsJson(JSON.parse(result.body));
|
|
423
|
+
}
|
|
424
|
+
} catch (err: any) {
|
|
425
|
+
console.warn('[Qwen] browserFetch failed for models, falling back:', err.message);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
349
429
|
const { cookie, userAgent, bxV, bxUa, bxUmidtoken } = await getBasicHeaders(accountId);
|
|
350
430
|
|
|
351
431
|
const response = await fetch('https://chat.qwen.ai/api/models', {
|
|
@@ -370,6 +450,10 @@ export async function fetchQwenModels(accountId?: string): Promise<any[]> {
|
|
|
370
450
|
}
|
|
371
451
|
|
|
372
452
|
const json = await response.json();
|
|
453
|
+
return processModelsJson(json);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function processModelsJson(json: any): any[] {
|
|
373
457
|
if (json.data && Array.isArray(json.data)) {
|
|
374
458
|
const models = json.data.map((m: any) => ({
|
|
375
459
|
id: m.id,
|
|
@@ -390,7 +474,7 @@ export async function fetchQwenModels(accountId?: string): Promise<any[]> {
|
|
|
390
474
|
];
|
|
391
475
|
|
|
392
476
|
cachedModels = extendedModels;
|
|
393
|
-
lastModelsFetch = now;
|
|
477
|
+
lastModelsFetch = Date.now();
|
|
394
478
|
return extendedModels;
|
|
395
479
|
}
|
|
396
480
|
|
|
@@ -415,19 +499,73 @@ export async function createQwenStream(
|
|
|
415
499
|
files?: QwenFileEntry[],
|
|
416
500
|
pendingMultimodal?: Array<Array<{ type: string; text?: string; image_url?: { url: string }; video_url?: { url: string }; audio_url?: { url: string }; file_url?: { url: string } }>>
|
|
417
501
|
): Promise<{ stream: ReadableStream, headers: Record<string, string>, uiSessionId: string, controller: AbortController, accountId: string }> {
|
|
418
|
-
let
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
502
|
+
let chatId: string;
|
|
503
|
+
let chatHeaders: Record<string, string>;
|
|
504
|
+
|
|
505
|
+
if (accountId === 'guest') {
|
|
506
|
+
chatHeaders = await getGuestHeaders();
|
|
507
|
+
const guestPage = getPageForAccount('guest');
|
|
508
|
+
const guestBody = JSON.stringify({
|
|
509
|
+
title: 'Guest Chat',
|
|
510
|
+
models: [modelId.replace('-no-thinking', '')],
|
|
511
|
+
chat_mode: 'guest',
|
|
512
|
+
chat_type: 't2t',
|
|
513
|
+
timestamp: Date.now(),
|
|
514
|
+
project_id: '',
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
if (guestPage && !guestPage.isClosed()) {
|
|
518
|
+
try {
|
|
519
|
+
const result = await browserFetch(guestPage, 'https://chat.qwen.ai/api/v2/chats/new', {
|
|
520
|
+
method: 'POST',
|
|
521
|
+
headers: { 'accept': 'application/json, text/plain, */*', 'content-type': 'application/json', 'x-request-id': crypto.randomUUID(), 'timezone': CACHED_TIMEZONE },
|
|
522
|
+
body: guestBody,
|
|
523
|
+
timeoutMs: 30000,
|
|
524
|
+
});
|
|
525
|
+
if (!result.status || result.status >= 400) throw new Error(`Failed to create guest chat: ${result.status}`);
|
|
526
|
+
const json = JSON.parse(result.body);
|
|
527
|
+
chatId = json.chat_id || json.id || json.data?.chat_id || json.data?.id;
|
|
528
|
+
if (!chatId) throw new Error(`Unexpected guest chat response: ${JSON.stringify(json).slice(0, 200)}`);
|
|
529
|
+
} catch (err: any) {
|
|
530
|
+
console.warn('[Qwen] browserFetch guest chat failed, falling back:', err.message);
|
|
531
|
+
const response = await fetch('https://chat.qwen.ai/api/v2/chats/new', {
|
|
532
|
+
method: 'POST',
|
|
533
|
+
headers: { 'accept': 'application/json, text/plain, */*', 'content-type': 'application/json', cookie: chatHeaders['cookie'], origin: 'https://chat.qwen.ai', referer: 'https://chat.qwen.ai/c/guest', 'user-agent': chatHeaders['user-agent'], 'x-request-id': crypto.randomUUID(), 'bx-v': chatHeaders['bx-v'], 'bx-ua': chatHeaders['bx-ua'], 'bx-umidtoken': chatHeaders['bx-umidtoken'], ...getClientHintsHeaders() },
|
|
534
|
+
body: guestBody,
|
|
535
|
+
signal: AbortSignal.timeout(30000),
|
|
536
|
+
});
|
|
537
|
+
if (!response.ok) throw new Error(`Failed to create guest chat: ${response.status}`);
|
|
538
|
+
const json = await response.json();
|
|
539
|
+
chatId = json.chat_id || json.id || json.data?.chat_id || json.data?.id;
|
|
540
|
+
if (!chatId) throw new Error(`Unexpected guest chat response: ${JSON.stringify(json).slice(0, 200)}`);
|
|
541
|
+
}
|
|
542
|
+
} else {
|
|
543
|
+
const response = await fetch('https://chat.qwen.ai/api/v2/chats/new', {
|
|
544
|
+
method: 'POST',
|
|
545
|
+
headers: { 'accept': 'application/json, text/plain, */*', 'content-type': 'application/json', cookie: chatHeaders['cookie'], origin: 'https://chat.qwen.ai', referer: 'https://chat.qwen.ai/c/guest', 'user-agent': chatHeaders['user-agent'], 'x-request-id': crypto.randomUUID(), 'bx-v': chatHeaders['bx-v'], 'bx-ua': chatHeaders['bx-ua'], 'bx-umidtoken': chatHeaders['bx-umidtoken'], ...getClientHintsHeaders() },
|
|
546
|
+
body: guestBody,
|
|
547
|
+
signal: AbortSignal.timeout(30000),
|
|
548
|
+
});
|
|
549
|
+
if (!response.ok) throw new Error(`Failed to create guest chat: ${response.status}`);
|
|
550
|
+
const json = await response.json();
|
|
551
|
+
chatId = json.chat_id || json.id || json.data?.chat_id || json.data?.id;
|
|
552
|
+
if (!chatId) throw new Error(`Unexpected guest chat response: ${JSON.stringify(json).slice(0, 200)}`);
|
|
425
553
|
}
|
|
426
|
-
|
|
554
|
+
} else {
|
|
555
|
+
let chatEntry: WarmPoolEntry;
|
|
556
|
+
try {
|
|
557
|
+
chatEntry = await getWarmedChat(accountId);
|
|
558
|
+
} catch (err: any) {
|
|
559
|
+
if (err.message?.includes('chat is in progress') || err.message?.includes('The chat is in progress')) {
|
|
560
|
+
const retryAfterMs = 2000 + Math.floor(Math.random() * 2000);
|
|
561
|
+
throw new RetryableQwenStreamError(`Qwen: ${err.message}`, retryAfterMs);
|
|
562
|
+
}
|
|
563
|
+
throw err;
|
|
564
|
+
}
|
|
565
|
+
chatId = chatEntry.chatId;
|
|
566
|
+
chatHeaders = chatEntry.headers;
|
|
427
567
|
}
|
|
428
568
|
|
|
429
|
-
const chatId = chatEntry.chatId;
|
|
430
|
-
const chatHeaders = chatEntry.headers;
|
|
431
569
|
const actualParentId: string | null = null;
|
|
432
570
|
|
|
433
571
|
// Process pending multimodal uploads — requires full headers with bx-ua/bx-umidtoken
|
|
@@ -473,7 +611,7 @@ export async function createQwenStream(
|
|
|
473
611
|
version: '2.1',
|
|
474
612
|
incremental_output: true,
|
|
475
613
|
chat_id: chatId,
|
|
476
|
-
chat_mode: 'normal',
|
|
614
|
+
chat_mode: accountId === 'guest' ? 'guest' : 'normal',
|
|
477
615
|
model: model,
|
|
478
616
|
parent_id: actualParentId,
|
|
479
617
|
messages: [
|
|
@@ -518,6 +656,66 @@ export async function createQwenStream(
|
|
|
518
656
|
const timeoutMs = BASE_TIMEOUT_MS + Math.ceil(payloadMB * TIMEOUT_PER_MB);
|
|
519
657
|
|
|
520
658
|
const url = `https://chat.qwen.ai/api/v2/chat/completions?chat_id=${chatId}`;
|
|
659
|
+
|
|
660
|
+
const completionHeaders: Record<string, string> = {
|
|
661
|
+
'accept': 'text/event-stream',
|
|
662
|
+
'content-type': 'application/json',
|
|
663
|
+
'x-request-id': crypto.randomUUID(),
|
|
664
|
+
'timezone': CACHED_TIMEZONE,
|
|
665
|
+
};
|
|
666
|
+
|
|
667
|
+
const page = getPageForAccount(accountId);
|
|
668
|
+
if (page && !page.isClosed() && page.url().includes('chat.qwen.ai')) {
|
|
669
|
+
try {
|
|
670
|
+
const browserResult = await browserStreamFetch(page, url, {
|
|
671
|
+
method: 'POST',
|
|
672
|
+
headers: completionHeaders,
|
|
673
|
+
body: payloadJson,
|
|
674
|
+
timeoutMs,
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
if (browserResult.contentType.includes('text/event-stream') && browserResult.status < 400) {
|
|
678
|
+
const controller = new AbortController();
|
|
679
|
+
return { stream: browserResult.stream, headers: chatHeaders, uiSessionId: chatId, controller, accountId: accountId || 'guest' };
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
if (browserResult.body) {
|
|
683
|
+
const peekText = browserResult.body;
|
|
684
|
+
if (peekText.includes('FAIL_SYS_USER_VALIDATE') || peekText.includes('_____tmd_____') || peekText.includes('RGV587_ERROR')) {
|
|
685
|
+
console.warn('[Qwen] TMD challenge detected via browser, refreshing headers and retrying...');
|
|
686
|
+
try {
|
|
687
|
+
const { headers: freshHeaders } = await getQwenHeaders(true, accountId);
|
|
688
|
+
await sleep(500 + Math.floor(Math.random() * 1000));
|
|
689
|
+
const retryResult = await browserStreamFetch(page, url, {
|
|
690
|
+
method: 'POST',
|
|
691
|
+
headers: completionHeaders,
|
|
692
|
+
body: payloadJson,
|
|
693
|
+
timeoutMs,
|
|
694
|
+
});
|
|
695
|
+
if (retryResult.contentType.includes('text/event-stream') && retryResult.status < 400) {
|
|
696
|
+
const controller = new AbortController();
|
|
697
|
+
return { stream: retryResult.stream, headers: freshHeaders, uiSessionId: chatId, controller, accountId: accountId || 'guest' };
|
|
698
|
+
}
|
|
699
|
+
if (retryResult.body && (retryResult.body.includes('FAIL_SYS_USER_VALIDATE') || retryResult.body.includes('_____tmd_____'))) {
|
|
700
|
+
throw new QwenUpstreamError('Qwen TMD challenge persists after header refresh.', 'FAIL_SYS_USER_VALIDATE', 403);
|
|
701
|
+
}
|
|
702
|
+
if (retryResult.body) {
|
|
703
|
+
handleErrorBody(retryResult.body, retryResult.status);
|
|
704
|
+
}
|
|
705
|
+
} catch (retryErr) {
|
|
706
|
+
if (retryErr instanceof QwenUpstreamError) throw retryErr;
|
|
707
|
+
console.error('[Qwen] Browser TMD retry failed:', (retryErr as Error).message);
|
|
708
|
+
}
|
|
709
|
+
throw new QwenUpstreamError('Qwen TMD anti-bot challenge detected. Headers were refreshed but the challenge persists.', 'FAIL_SYS_USER_VALIDATE', 403);
|
|
710
|
+
}
|
|
711
|
+
handleErrorBody(peekText, browserResult.status);
|
|
712
|
+
}
|
|
713
|
+
} catch (browserErr: any) {
|
|
714
|
+
if (browserErr instanceof QwenUpstreamError || browserErr instanceof RetryableQwenStreamError) throw browserErr;
|
|
715
|
+
console.warn('[Qwen] Browser stream fetch failed, falling back to Node.js:', browserErr.message);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
521
719
|
const controller = new AbortController();
|
|
522
720
|
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
523
721
|
const response = await fetch(url, {
|
|
@@ -528,7 +726,7 @@ export async function createQwenStream(
|
|
|
528
726
|
'content-type': 'application/json',
|
|
529
727
|
'cookie': chatHeaders['cookie'],
|
|
530
728
|
'origin': 'https://chat.qwen.ai',
|
|
531
|
-
'referer': `https://chat.qwen.ai/c/${chatId}`,
|
|
729
|
+
'referer': accountId === 'guest' ? 'https://chat.qwen.ai/c/guest' : `https://chat.qwen.ai/c/${chatId}`,
|
|
532
730
|
'sec-fetch-dest': 'empty',
|
|
533
731
|
'sec-fetch-mode': 'cors',
|
|
534
732
|
'sec-fetch-site': 'same-origin',
|
|
@@ -584,72 +782,25 @@ export async function createQwenStream(
|
|
|
584
782
|
|
|
585
783
|
const retryContentType = retryResponse.headers.get('content-type') || '';
|
|
586
784
|
if (retryResponse.ok && retryContentType.includes('text/event-stream') && retryResponse.body) {
|
|
587
|
-
return { stream: retryResponse.body, headers: freshHeaders, uiSessionId: chatId, controller: retryController, accountId:
|
|
785
|
+
return { stream: retryResponse.body, headers: freshHeaders, uiSessionId: chatId, controller: retryController, accountId: accountId || 'guest' };
|
|
588
786
|
}
|
|
589
787
|
|
|
590
788
|
const retryPeek = await retryResponse.clone().text().catch(() => '');
|
|
591
789
|
if (retryPeek.includes('FAIL_SYS_USER_VALIDATE') || retryPeek.includes('_____tmd_____')) {
|
|
592
|
-
throw new QwenUpstreamError(
|
|
593
|
-
'Qwen TMD challenge persists after header refresh. The account may need manual captcha resolution.',
|
|
594
|
-
'FAIL_SYS_USER_VALIDATE',
|
|
595
|
-
403,
|
|
596
|
-
);
|
|
790
|
+
throw new QwenUpstreamError('Qwen TMD challenge persists after header refresh. The account may need manual captcha resolution.', 'FAIL_SYS_USER_VALIDATE', 403);
|
|
597
791
|
}
|
|
598
792
|
|
|
599
793
|
if (retryResponse.ok && retryResponse.body) {
|
|
600
|
-
|
|
601
|
-
const errorJson = JSON.parse(retryPeek);
|
|
602
|
-
if (errorJson && (errorJson.success === false || errorJson.error)) {
|
|
603
|
-
const code = errorJson.data?.code || errorJson.code || 'UpstreamError';
|
|
604
|
-
const details = errorJson.data?.details || errorJson.message || errorJson.error?.message || 'Qwen returned an error';
|
|
605
|
-
const wait = errorJson.data?.num !== undefined
|
|
606
|
-
? ` Wait about ${errorJson.data.num} hour(s) before trying again.`
|
|
607
|
-
: '';
|
|
608
|
-
let status = 502;
|
|
609
|
-
if (code === 'RateLimited') status = 429;
|
|
610
|
-
|
|
611
|
-
throw new QwenUpstreamError(
|
|
612
|
-
`Qwen upstream error: ${code}: ${details}.${wait}`,
|
|
613
|
-
code,
|
|
614
|
-
status,
|
|
615
|
-
);
|
|
616
|
-
}
|
|
617
|
-
} catch (e) {
|
|
618
|
-
if (e instanceof QwenUpstreamError) throw e;
|
|
619
|
-
}
|
|
620
|
-
return { stream: retryResponse.body, headers: freshHeaders, uiSessionId: chatId, controller: retryController, accountId: chatEntry.accountId };
|
|
794
|
+
return { stream: retryResponse.body, headers: freshHeaders, uiSessionId: chatId, controller: retryController, accountId: accountId || 'guest' };
|
|
621
795
|
}
|
|
622
796
|
} catch (retryErr) {
|
|
623
797
|
if (retryErr instanceof QwenUpstreamError) throw retryErr;
|
|
624
798
|
console.error('[Qwen] TMD retry failed:', (retryErr as Error).message);
|
|
625
799
|
}
|
|
626
800
|
|
|
627
|
-
throw new QwenUpstreamError(
|
|
628
|
-
'Qwen TMD anti-bot challenge detected. Headers were refreshed but the challenge persists.',
|
|
629
|
-
'FAIL_SYS_USER_VALIDATE',
|
|
630
|
-
403,
|
|
631
|
-
);
|
|
801
|
+
throw new QwenUpstreamError('Qwen TMD anti-bot challenge detected. Headers were refreshed but the challenge persists.', 'FAIL_SYS_USER_VALIDATE', 403);
|
|
632
802
|
} else {
|
|
633
|
-
|
|
634
|
-
const errorJson = JSON.parse(peekText);
|
|
635
|
-
if (errorJson && (errorJson.success === false || errorJson.error)) {
|
|
636
|
-
const code = errorJson.data?.code || errorJson.code || 'UpstreamError';
|
|
637
|
-
const details = errorJson.data?.details || errorJson.message || errorJson.error?.message || 'Qwen returned an error';
|
|
638
|
-
const wait = errorJson.data?.num !== undefined
|
|
639
|
-
? ` Wait about ${errorJson.data.num} hour(s) before trying again.`
|
|
640
|
-
: '';
|
|
641
|
-
let status = 502;
|
|
642
|
-
if (code === 'RateLimited') status = 429;
|
|
643
|
-
|
|
644
|
-
throw new QwenUpstreamError(
|
|
645
|
-
`Qwen upstream error: ${code}: ${details}.${wait}`,
|
|
646
|
-
code,
|
|
647
|
-
status,
|
|
648
|
-
);
|
|
649
|
-
}
|
|
650
|
-
} catch (e) {
|
|
651
|
-
if (e instanceof QwenUpstreamError) throw e;
|
|
652
|
-
}
|
|
803
|
+
handleErrorBody(peekText, response.status);
|
|
653
804
|
}
|
|
654
805
|
}
|
|
655
806
|
|
|
@@ -658,50 +809,53 @@ export async function createQwenStream(
|
|
|
658
809
|
const contentType = response.headers.get('content-type') || '';
|
|
659
810
|
|
|
660
811
|
if (contentType.includes('application/json')) {
|
|
661
|
-
|
|
662
|
-
const errorJson = JSON.parse(errText);
|
|
663
|
-
if (errorJson?.data?.details?.includes('chat is in progress') ||
|
|
664
|
-
errorJson?.data?.details?.includes('The chat is in progress')) {
|
|
665
|
-
const retryAfterMs = 2000 + Math.floor(Math.random() * 2000);
|
|
666
|
-
throw new RetryableQwenStreamError(
|
|
667
|
-
`Qwen: ${errorJson.data.details}`,
|
|
668
|
-
retryAfterMs,
|
|
669
|
-
);
|
|
670
|
-
}
|
|
671
|
-
if (errorJson?.success === false) {
|
|
672
|
-
const code = errorJson.data?.code || errorJson.code || 'UpstreamError';
|
|
673
|
-
const details = errorJson.data?.details || errorJson.message || 'Qwen returned an error';
|
|
674
|
-
const wait = errorJson.data?.num !== undefined
|
|
675
|
-
? ` Wait about ${errorJson.data.num} hour(s) before trying again.`
|
|
676
|
-
: '';
|
|
677
|
-
let status: number;
|
|
678
|
-
if (code === 'RateLimited') status = 429;
|
|
679
|
-
else if (code === 'Not_Found') status = 404;
|
|
680
|
-
else if (code === 'UpstreamError') status = 502;
|
|
681
|
-
else status = 502;
|
|
682
|
-
throw new QwenUpstreamError(
|
|
683
|
-
`Qwen upstream error: ${code}: ${details}.${wait}`,
|
|
684
|
-
code,
|
|
685
|
-
status,
|
|
686
|
-
);
|
|
687
|
-
}
|
|
688
|
-
if (errorJson?.data?.details?.includes('is not exist') ||
|
|
689
|
-
errorJson?.data?.details?.includes('not exist') ||
|
|
690
|
-
errorJson.data?.details?.includes('does not exist')) {
|
|
691
|
-
throw new RetryableQwenStreamError(
|
|
692
|
-
`Qwen: ${errorJson.data.details}`,
|
|
693
|
-
0,
|
|
694
|
-
);
|
|
695
|
-
}
|
|
696
|
-
} catch (parseOrRetryError) {
|
|
697
|
-
if (parseOrRetryError instanceof RetryableQwenStreamError ||
|
|
698
|
-
parseOrRetryError instanceof QwenUpstreamError) {
|
|
699
|
-
throw parseOrRetryError;
|
|
700
|
-
}
|
|
701
|
-
}
|
|
812
|
+
handleJsonErrorBody(errText);
|
|
702
813
|
}
|
|
703
814
|
throw new Error(`Failed to fetch from Qwen: ${response.status} ${response.statusText} - ${errText}`);
|
|
704
815
|
}
|
|
705
816
|
|
|
706
|
-
return { stream: response.body, headers: chatHeaders, uiSessionId: chatId, controller, accountId:
|
|
817
|
+
return { stream: response.body, headers: chatHeaders, uiSessionId: chatId, controller, accountId: accountId || 'guest' };
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
function handleErrorBody(peekText: string, status: number): never {
|
|
821
|
+
try {
|
|
822
|
+
const errorJson = JSON.parse(peekText);
|
|
823
|
+
if (errorJson && (errorJson.success === false || errorJson.error)) {
|
|
824
|
+
const code = errorJson.data?.code || errorJson.code || 'UpstreamError';
|
|
825
|
+
const details = errorJson.data?.details || errorJson.message || errorJson.error?.message || 'Qwen returned an error';
|
|
826
|
+
const wait = errorJson.data?.num !== undefined ? ` Wait about ${errorJson.data.num} hour(s) before trying again.` : '';
|
|
827
|
+
let errStatus = 502;
|
|
828
|
+
if (code === 'RateLimited') errStatus = 429;
|
|
829
|
+
throw new QwenUpstreamError(`Qwen upstream error: ${code}: ${details}.${wait}`, code, errStatus);
|
|
830
|
+
}
|
|
831
|
+
} catch (e) {
|
|
832
|
+
if (e instanceof QwenUpstreamError) throw e;
|
|
833
|
+
}
|
|
834
|
+
throw new Error(`Qwen returned status ${status}: ${peekText.slice(0, 500)}`);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function handleJsonErrorBody(errText: string): never {
|
|
838
|
+
try {
|
|
839
|
+
const errorJson = JSON.parse(errText);
|
|
840
|
+
if (errorJson?.data?.details?.includes('chat is in progress') || errorJson?.data?.details?.includes('The chat is in progress')) {
|
|
841
|
+
const retryAfterMs = 2000 + Math.floor(Math.random() * 2000);
|
|
842
|
+
throw new RetryableQwenStreamError(`Qwen: ${errorJson.data.details}`, retryAfterMs);
|
|
843
|
+
}
|
|
844
|
+
if (errorJson?.success === false) {
|
|
845
|
+
const code = errorJson.data?.code || errorJson.code || 'UpstreamError';
|
|
846
|
+
const details = errorJson.data?.details || errorJson.message || 'Qwen returned an error';
|
|
847
|
+
const wait = errorJson.data?.num !== undefined ? ` Wait about ${errorJson.data.num} hour(s) before trying again.` : '';
|
|
848
|
+
let status: number;
|
|
849
|
+
if (code === 'RateLimited') status = 429;
|
|
850
|
+
else if (code === 'Not_Found') status = 404;
|
|
851
|
+
else status = 502;
|
|
852
|
+
throw new QwenUpstreamError(`Qwen upstream error: ${code}: ${details}.${wait}`, code, status);
|
|
853
|
+
}
|
|
854
|
+
if (errorJson?.data?.details?.includes('is not exist') || errorJson?.data?.details?.includes('not exist') || errorJson.data?.details?.includes('does not exist')) {
|
|
855
|
+
throw new RetryableQwenStreamError(`Qwen: ${errorJson.data.details}`, 0);
|
|
856
|
+
}
|
|
857
|
+
} catch (e) {
|
|
858
|
+
if (e instanceof RetryableQwenStreamError || e instanceof QwenUpstreamError) throw e;
|
|
859
|
+
}
|
|
860
|
+
throw new Error(`Qwen JSON error: ${errText.slice(0, 500)}`);
|
|
707
861
|
}
|