@pedrofariasx/qwenproxy 1.5.1 → 1.6.1
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/core/accounts.ts +2 -3
- package/src/routes/chat.ts +2 -2
- package/src/services/playwright.ts +373 -18
- package/src/services/qwen.ts +277 -154
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
|
},
|
package/src/core/accounts.ts
CHANGED
|
@@ -74,9 +74,8 @@ export function listAccounts(): QwenAccount[] {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
export function getAccountCredentials(id: string): QwenAccount | undefined {
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
return row as QwenAccount | undefined
|
|
77
|
+
const cached = getCachedAccounts()
|
|
78
|
+
return cached.find(a => a.id === id)
|
|
80
79
|
}
|
|
81
80
|
|
|
82
81
|
export function updateAccountCooldown(id: string, cooldownUntil: number, reason: string | null): void {
|
package/src/routes/chat.ts
CHANGED
|
@@ -566,12 +566,12 @@ export async function chatCompletions(c: Context) {
|
|
|
566
566
|
const createdTimestamp = Math.floor(Date.now() / 1000);
|
|
567
567
|
|
|
568
568
|
const fastWriteContent = (content: string) => {
|
|
569
|
-
const escaped =
|
|
569
|
+
const escaped = JSON.stringify(content).slice(1, -1);
|
|
570
570
|
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`);
|
|
571
571
|
};
|
|
572
572
|
|
|
573
573
|
const fastWriteReasoning = (content: string) => {
|
|
574
|
-
const escaped =
|
|
574
|
+
const escaped = JSON.stringify(content).slice(1, -1);
|
|
575
575
|
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`);
|
|
576
576
|
};
|
|
577
577
|
|
|
@@ -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',
|
|
@@ -371,7 +495,7 @@ export async function getGuestHeaders(): Promise<Record<string, string>> {
|
|
|
371
495
|
guestContext = await engine.launchPersistentContext(profilePath, {
|
|
372
496
|
headless: config.browser.headless,
|
|
373
497
|
channel,
|
|
374
|
-
userAgent:
|
|
498
|
+
userAgent: CHROME_UA,
|
|
375
499
|
ignoreDefaultArgs: ['--enable-automation'],
|
|
376
500
|
args: ['--disable-blink-features=AutomationControlled', '--disable-features=IsolateOrigins,site-per-process', '--disable-infobars', '--no-first-run', '--no-default-browser-check']
|
|
377
501
|
});
|
|
@@ -405,7 +529,7 @@ export async function getGuestHeaders(): Promise<Record<string, string>> {
|
|
|
405
529
|
'bx-ua': reqHeaders['bx-ua'] || '',
|
|
406
530
|
'bx-umidtoken': reqHeaders['bx-umidtoken'] || '',
|
|
407
531
|
'bx-v': reqHeaders['bx-v'] || '2.5.36',
|
|
408
|
-
'user-agent': reqHeaders['user-agent'] ||
|
|
532
|
+
'user-agent': reqHeaders['user-agent'] || CHROME_UA,
|
|
409
533
|
};
|
|
410
534
|
|
|
411
535
|
if (extractedHeaders['bx-ua']) {
|
|
@@ -587,7 +711,7 @@ async function _getQwenHeadersInternal(forceNew = false, accountId?: string): Pr
|
|
|
587
711
|
const isOnQwen = currentUrl.includes('chat.qwen.ai');
|
|
588
712
|
const isOnSpecificChat = isOnQwen && /\/c\//.test(currentUrl);
|
|
589
713
|
|
|
590
|
-
if (!isOnQwen || forceNew
|
|
714
|
+
if (!isOnQwen || forceNew) {
|
|
591
715
|
console.log(`[Playwright] Navigating to Qwen home for ${cacheKey}... (Current: ${currentUrl})`);
|
|
592
716
|
await page.goto('https://chat.qwen.ai/', { waitUntil: 'domcontentloaded' });
|
|
593
717
|
}
|
|
@@ -703,9 +827,9 @@ async function _getQwenHeadersInternal(forceNew = false, accountId?: string): Pr
|
|
|
703
827
|
|
|
704
828
|
await page.focus(inputSelector);
|
|
705
829
|
await page.fill(inputSelector, '');
|
|
706
|
-
await page.type(inputSelector, 'a', { delay:
|
|
830
|
+
await page.type(inputSelector, 'a', { delay: 20 });
|
|
707
831
|
console.log(`[Playwright] Typed char for ${cacheKey}, waiting for UI to update...`);
|
|
708
|
-
await sleep(
|
|
832
|
+
await sleep(800);
|
|
709
833
|
|
|
710
834
|
const selectors = [
|
|
711
835
|
'.message-input-right-button-send .send-button',
|
|
@@ -756,7 +880,7 @@ export async function initPlaywrightForAccount(account: QwenAccount, headless =
|
|
|
756
880
|
const acctContext = await engine.launchPersistentContext(profilePath, {
|
|
757
881
|
headless,
|
|
758
882
|
channel,
|
|
759
|
-
userAgent:
|
|
883
|
+
userAgent: CHROME_UA,
|
|
760
884
|
ignoreDefaultArgs: ['--enable-automation'],
|
|
761
885
|
args: [
|
|
762
886
|
'--disable-blink-features=AutomationControlled',
|
|
@@ -807,7 +931,7 @@ export async function launchManualLoginAccount(accountId: string, browserType: B
|
|
|
807
931
|
const acctContext = await engine.launchPersistentContext(profilePath, {
|
|
808
932
|
headless: false,
|
|
809
933
|
channel,
|
|
810
|
-
userAgent:
|
|
934
|
+
userAgent: CHROME_UA,
|
|
811
935
|
ignoreDefaultArgs: ['--enable-automation'],
|
|
812
936
|
args: [
|
|
813
937
|
'--disable-blink-features=AutomationControlled',
|
|
@@ -890,3 +1014,234 @@ async function loginToQwenWithContext(acctContext: BrowserContext, acctPage: Pag
|
|
|
890
1014
|
console.error(`[Playwright] Login failed for ${email}:`, result.data || result.error);
|
|
891
1015
|
return false;
|
|
892
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, getGuestHeaders } 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
|
};
|
|
@@ -86,6 +86,7 @@ const refillPromises: Map<string, Promise<void>> = new Map();
|
|
|
86
86
|
|
|
87
87
|
const WARM_POOL_SIZE = 10;
|
|
88
88
|
const WARM_POOL_TTL_MS = 10 * 60 * 1000;
|
|
89
|
+
const WARM_POOL_LOW_WATER = 3;
|
|
89
90
|
|
|
90
91
|
function cleanupStalePool(accountId: string) {
|
|
91
92
|
const pool = warmPool.get(accountId);
|
|
@@ -106,7 +107,58 @@ async function getBasicQwenHeaders(accountId?: string): Promise<Record<string, s
|
|
|
106
107
|
};
|
|
107
108
|
}
|
|
108
109
|
|
|
109
|
-
async function createRealQwenChat(header: Record<string, string
|
|
110
|
+
async function createRealQwenChat(header: Record<string, string>, accountId?: string): Promise<string> {
|
|
111
|
+
const page = getPageForAccount(accountId);
|
|
112
|
+
const body = JSON.stringify({
|
|
113
|
+
title: 'Nova Conversa',
|
|
114
|
+
models: ['qwen3.7-plus'],
|
|
115
|
+
chat_mode: 'normal',
|
|
116
|
+
chat_type: 't2t',
|
|
117
|
+
timestamp: Date.now(),
|
|
118
|
+
project_id: '',
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const pageUrl = page?.url() || '';
|
|
122
|
+
const isOnQwenOrigin = pageUrl.includes('chat.qwen.ai');
|
|
123
|
+
|
|
124
|
+
if (page && !page.isClosed() && isOnQwenOrigin) {
|
|
125
|
+
try {
|
|
126
|
+
const result = await browserFetch(page, 'https://chat.qwen.ai/api/v2/chats/new', {
|
|
127
|
+
method: 'POST',
|
|
128
|
+
headers: {
|
|
129
|
+
'accept': 'application/json, text/plain, */*',
|
|
130
|
+
'content-type': 'application/json',
|
|
131
|
+
'x-request-id': crypto.randomUUID(),
|
|
132
|
+
'timezone': CACHED_TIMEZONE,
|
|
133
|
+
},
|
|
134
|
+
body,
|
|
135
|
+
timeoutMs: 30000,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
if (result.status === 429) {
|
|
139
|
+
throw new QwenUpstreamError('Qwen upstream error: RateLimited: Too many requests.', 'RateLimited', 429);
|
|
140
|
+
}
|
|
141
|
+
if (!result.status || result.status >= 400) {
|
|
142
|
+
throw new Error(`Failed to create chat: ${result.status} - ${result.body}`);
|
|
143
|
+
}
|
|
144
|
+
const json = JSON.parse(result.body);
|
|
145
|
+
if (json && json.success === false) {
|
|
146
|
+
const code = json.data?.code || json.code || 'UpstreamError';
|
|
147
|
+
const details = json.data?.details || json.message || 'Qwen returned an error';
|
|
148
|
+
const wait = json.data?.num !== undefined ? ` Wait about ${json.data.num} hour(s) before trying again.` : '';
|
|
149
|
+
let status = 502;
|
|
150
|
+
if (code === 'RateLimited') status = 429;
|
|
151
|
+
throw new QwenUpstreamError(`Qwen upstream error: ${code}: ${details}.${wait}`, code, status);
|
|
152
|
+
}
|
|
153
|
+
const chatId = json.chat_id || json.id || json.data?.chat_id || json.data?.id;
|
|
154
|
+
if (!chatId) throw new Error(`Unexpected chat response: ${JSON.stringify(json).slice(0, 200)}`);
|
|
155
|
+
return chatId;
|
|
156
|
+
} catch (err: any) {
|
|
157
|
+
if (err instanceof QwenUpstreamError) throw err;
|
|
158
|
+
console.warn('[WarmPool] browserFetch failed for chat creation, falling back to Node.js fetch:', err.message);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
110
162
|
const response = await fetch('https://chat.qwen.ai/api/v2/chats/new', {
|
|
111
163
|
method: 'POST',
|
|
112
164
|
headers: {
|
|
@@ -123,25 +175,14 @@ async function createRealQwenChat(header: Record<string, string>): Promise<strin
|
|
|
123
175
|
'bx-umidtoken': header['bx-umidtoken'] || '',
|
|
124
176
|
...getClientHintsHeaders(),
|
|
125
177
|
},
|
|
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
|
-
}),
|
|
178
|
+
body,
|
|
134
179
|
signal: AbortSignal.timeout(30000),
|
|
135
180
|
});
|
|
136
181
|
|
|
137
182
|
if (!response.ok) {
|
|
138
183
|
const errText = await response.text().catch(() => '');
|
|
139
184
|
if (response.status === 429) {
|
|
140
|
-
throw new QwenUpstreamError(
|
|
141
|
-
'Qwen upstream error: RateLimited: Too many requests.',
|
|
142
|
-
'RateLimited',
|
|
143
|
-
429
|
|
144
|
-
);
|
|
185
|
+
throw new QwenUpstreamError('Qwen upstream error: RateLimited: Too many requests.', 'RateLimited', 429);
|
|
145
186
|
}
|
|
146
187
|
throw new Error(`Failed to create chat: ${response.status} - ${errText}`);
|
|
147
188
|
}
|
|
@@ -149,16 +190,10 @@ async function createRealQwenChat(header: Record<string, string>): Promise<strin
|
|
|
149
190
|
if (json && json.success === false) {
|
|
150
191
|
const code = json.data?.code || json.code || 'UpstreamError';
|
|
151
192
|
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
|
-
: '';
|
|
193
|
+
const wait = json.data?.num !== undefined ? ` Wait about ${json.data.num} hour(s) before trying again.` : '';
|
|
155
194
|
let status = 502;
|
|
156
195
|
if (code === 'RateLimited') status = 429;
|
|
157
|
-
throw new QwenUpstreamError(
|
|
158
|
-
`Qwen upstream error: ${code}: ${details}.${wait}`,
|
|
159
|
-
code,
|
|
160
|
-
status
|
|
161
|
-
);
|
|
196
|
+
throw new QwenUpstreamError(`Qwen upstream error: ${code}: ${details}.${wait}`, code, status);
|
|
162
197
|
}
|
|
163
198
|
const chatId = json.chat_id || json.id || json.data?.chat_id || json.data?.id;
|
|
164
199
|
if (!chatId) throw new Error(`Unexpected chat response: ${JSON.stringify(json).slice(0, 200)}`);
|
|
@@ -181,10 +216,14 @@ async function refillPoolForAccount(accountId: string) {
|
|
|
181
216
|
return;
|
|
182
217
|
}
|
|
183
218
|
|
|
184
|
-
const
|
|
219
|
+
const acctId = accountId === 'global' ? undefined : accountId;
|
|
220
|
+
for (let i = 0; i < need; i++) {
|
|
221
|
+
if (i > 0) {
|
|
222
|
+
await sleep(800 + Math.floor(Math.random() * 2200));
|
|
223
|
+
}
|
|
185
224
|
try {
|
|
186
|
-
const chatId = await createRealQwenChat(headers);
|
|
187
|
-
|
|
225
|
+
const chatId = await createRealQwenChat(headers, acctId);
|
|
226
|
+
pool.push({ chatId, headers, accountId, timestamp: Date.now() });
|
|
188
227
|
} catch (err: any) {
|
|
189
228
|
if (err instanceof QwenUpstreamError) {
|
|
190
229
|
if (err.upstreamCode === 'RateLimited' || err.upstreamStatus === 429) {
|
|
@@ -192,16 +231,11 @@ async function refillPoolForAccount(accountId: string) {
|
|
|
192
231
|
const cooldownMs = hourHint ? parseInt(hourHint[1]) * 60 * 60 * 1000 : undefined;
|
|
193
232
|
markAccountRateLimited(accountId, cooldownMs, 'RateLimited');
|
|
194
233
|
console.warn(`[WarmPool] Account ${accountId} rate-limited during chat creation. Marked for cooldown.`);
|
|
234
|
+
break;
|
|
195
235
|
}
|
|
196
236
|
}
|
|
197
237
|
console.error(`[WarmPool] chat creation failed for ${accountId}:`, (err as Error).message);
|
|
198
|
-
return null;
|
|
199
238
|
}
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
const results = await Promise.all(creationPromises);
|
|
203
|
-
for (const entry of results) {
|
|
204
|
-
if (entry) pool.push(entry);
|
|
205
239
|
}
|
|
206
240
|
}
|
|
207
241
|
|
|
@@ -210,6 +244,11 @@ export async function getWarmedChat(accountId?: string) {
|
|
|
210
244
|
let pool = warmPool.get(key);
|
|
211
245
|
if (!pool) { pool = []; warmPool.set(key, pool); }
|
|
212
246
|
cleanupStalePool(key);
|
|
247
|
+
|
|
248
|
+
if (pool.length < WARM_POOL_LOW_WATER && !refillPromises.has(key)) {
|
|
249
|
+
refillPromises.set(key, refillPoolForAccount(key).finally(() => refillPromises.delete(key)));
|
|
250
|
+
}
|
|
251
|
+
|
|
213
252
|
if (pool.length === 0) {
|
|
214
253
|
if (!refillPromises.has(key)) {
|
|
215
254
|
refillPromises.set(key, refillPoolForAccount(key).finally(() => refillPromises.delete(key)));
|
|
@@ -304,6 +343,32 @@ export async function disableNativeTools(accountId?: string): Promise<void> {
|
|
|
304
343
|
};
|
|
305
344
|
|
|
306
345
|
console.log(`[Qwen] Disabling native tools for ${cacheKey}...`);
|
|
346
|
+
const page = getPageForAccount(accountId);
|
|
347
|
+
if (page && !page.isClosed() && page.url().includes('chat.qwen.ai')) {
|
|
348
|
+
try {
|
|
349
|
+
const result = await browserFetch(page, 'https://chat.qwen.ai/api/v2/users/user/settings/update', {
|
|
350
|
+
method: 'POST',
|
|
351
|
+
headers: {
|
|
352
|
+
'accept': 'application/json, text/plain, */*',
|
|
353
|
+
'content-type': 'application/json',
|
|
354
|
+
'x-request-id': crypto.randomUUID(),
|
|
355
|
+
'timezone': CACHED_TIMEZONE,
|
|
356
|
+
},
|
|
357
|
+
body: JSON.stringify(payload),
|
|
358
|
+
timeoutMs: 30000,
|
|
359
|
+
});
|
|
360
|
+
if (result.status && result.status < 400) {
|
|
361
|
+
console.log(`[Qwen] Native tools disabled successfully for ${cacheKey}.`);
|
|
362
|
+
nativeToolsDisabled.add(cacheKey);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
console.error(`[Qwen] Failed to disable native tools for ${cacheKey}: ${result.status} - ${result.body}`);
|
|
366
|
+
return;
|
|
367
|
+
} catch (err: any) {
|
|
368
|
+
console.warn('[Qwen] browserFetch failed for disableNativeTools, falling back:', err.message);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
307
372
|
const controller = new AbortController();
|
|
308
373
|
const timeoutId = setTimeout(() => controller.abort(), 30000);
|
|
309
374
|
const response = await fetch('https://chat.qwen.ai/api/v2/users/user/settings/update', {
|
|
@@ -346,6 +411,27 @@ export async function fetchQwenModels(accountId?: string): Promise<any[]> {
|
|
|
346
411
|
return cachedModels;
|
|
347
412
|
}
|
|
348
413
|
|
|
414
|
+
const page = getPageForAccount(accountId);
|
|
415
|
+
if (page && !page.isClosed() && page.url().includes('chat.qwen.ai')) {
|
|
416
|
+
try {
|
|
417
|
+
const result = await browserFetch(page, 'https://chat.qwen.ai/api/models', {
|
|
418
|
+
method: 'GET',
|
|
419
|
+
headers: {
|
|
420
|
+
'accept': 'application/json, text/plain, */*',
|
|
421
|
+
'x-request-id': crypto.randomUUID(),
|
|
422
|
+
'timezone': CACHED_TIMEZONE,
|
|
423
|
+
'source': 'web',
|
|
424
|
+
},
|
|
425
|
+
timeoutMs: 30000,
|
|
426
|
+
});
|
|
427
|
+
if (result.status && result.status < 400) {
|
|
428
|
+
return processModelsJson(JSON.parse(result.body));
|
|
429
|
+
}
|
|
430
|
+
} catch (err: any) {
|
|
431
|
+
console.warn('[Qwen] browserFetch failed for models, falling back:', err.message);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
349
435
|
const { cookie, userAgent, bxV, bxUa, bxUmidtoken } = await getBasicHeaders(accountId);
|
|
350
436
|
|
|
351
437
|
const response = await fetch('https://chat.qwen.ai/api/models', {
|
|
@@ -370,6 +456,10 @@ export async function fetchQwenModels(accountId?: string): Promise<any[]> {
|
|
|
370
456
|
}
|
|
371
457
|
|
|
372
458
|
const json = await response.json();
|
|
459
|
+
return processModelsJson(json);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function processModelsJson(json: any): any[] {
|
|
373
463
|
if (json.data && Array.isArray(json.data)) {
|
|
374
464
|
const models = json.data.map((m: any) => ({
|
|
375
465
|
id: m.id,
|
|
@@ -390,7 +480,7 @@ export async function fetchQwenModels(accountId?: string): Promise<any[]> {
|
|
|
390
480
|
];
|
|
391
481
|
|
|
392
482
|
cachedModels = extendedModels;
|
|
393
|
-
lastModelsFetch = now;
|
|
483
|
+
lastModelsFetch = Date.now();
|
|
394
484
|
return extendedModels;
|
|
395
485
|
}
|
|
396
486
|
|
|
@@ -420,36 +510,53 @@ export async function createQwenStream(
|
|
|
420
510
|
|
|
421
511
|
if (accountId === 'guest') {
|
|
422
512
|
chatHeaders = await getGuestHeaders();
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
referer: 'https://chat.qwen.ai/c/guest',
|
|
432
|
-
'user-agent': chatHeaders['user-agent'],
|
|
433
|
-
'x-request-id': crypto.randomUUID(),
|
|
434
|
-
'bx-v': chatHeaders['bx-v'],
|
|
435
|
-
'bx-ua': chatHeaders['bx-ua'],
|
|
436
|
-
'bx-umidtoken': chatHeaders['bx-umidtoken'],
|
|
437
|
-
...getClientHintsHeaders(),
|
|
438
|
-
},
|
|
439
|
-
body: JSON.stringify({
|
|
440
|
-
title: 'Guest Chat',
|
|
441
|
-
models: [modelId.replace('-no-thinking', '')],
|
|
442
|
-
chat_mode: 'guest',
|
|
443
|
-
chat_type: 't2t',
|
|
444
|
-
timestamp: Date.now(),
|
|
445
|
-
project_id: '',
|
|
446
|
-
}),
|
|
447
|
-
signal: AbortSignal.timeout(30000),
|
|
513
|
+
const guestPage = getPageForAccount('guest');
|
|
514
|
+
const guestBody = JSON.stringify({
|
|
515
|
+
title: 'Guest Chat',
|
|
516
|
+
models: [modelId.replace('-no-thinking', '')],
|
|
517
|
+
chat_mode: 'guest',
|
|
518
|
+
chat_type: 't2t',
|
|
519
|
+
timestamp: Date.now(),
|
|
520
|
+
project_id: '',
|
|
448
521
|
});
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
522
|
+
|
|
523
|
+
if (guestPage && !guestPage.isClosed()) {
|
|
524
|
+
try {
|
|
525
|
+
const result = await browserFetch(guestPage, 'https://chat.qwen.ai/api/v2/chats/new', {
|
|
526
|
+
method: 'POST',
|
|
527
|
+
headers: { 'accept': 'application/json, text/plain, */*', 'content-type': 'application/json', 'x-request-id': crypto.randomUUID(), 'timezone': CACHED_TIMEZONE },
|
|
528
|
+
body: guestBody,
|
|
529
|
+
timeoutMs: 30000,
|
|
530
|
+
});
|
|
531
|
+
if (!result.status || result.status >= 400) throw new Error(`Failed to create guest chat: ${result.status}`);
|
|
532
|
+
const json = JSON.parse(result.body);
|
|
533
|
+
chatId = json.chat_id || json.id || json.data?.chat_id || json.data?.id;
|
|
534
|
+
if (!chatId) throw new Error(`Unexpected guest chat response: ${JSON.stringify(json).slice(0, 200)}`);
|
|
535
|
+
} catch (err: any) {
|
|
536
|
+
console.warn('[Qwen] browserFetch guest chat failed, falling back:', err.message);
|
|
537
|
+
const response = await fetch('https://chat.qwen.ai/api/v2/chats/new', {
|
|
538
|
+
method: 'POST',
|
|
539
|
+
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() },
|
|
540
|
+
body: guestBody,
|
|
541
|
+
signal: AbortSignal.timeout(30000),
|
|
542
|
+
});
|
|
543
|
+
if (!response.ok) throw new Error(`Failed to create guest chat: ${response.status}`);
|
|
544
|
+
const json = await response.json();
|
|
545
|
+
chatId = json.chat_id || json.id || json.data?.chat_id || json.data?.id;
|
|
546
|
+
if (!chatId) throw new Error(`Unexpected guest chat response: ${JSON.stringify(json).slice(0, 200)}`);
|
|
547
|
+
}
|
|
548
|
+
} else {
|
|
549
|
+
const response = await fetch('https://chat.qwen.ai/api/v2/chats/new', {
|
|
550
|
+
method: 'POST',
|
|
551
|
+
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() },
|
|
552
|
+
body: guestBody,
|
|
553
|
+
signal: AbortSignal.timeout(30000),
|
|
554
|
+
});
|
|
555
|
+
if (!response.ok) throw new Error(`Failed to create guest chat: ${response.status}`);
|
|
556
|
+
const json = await response.json();
|
|
557
|
+
chatId = json.chat_id || json.id || json.data?.chat_id || json.data?.id;
|
|
558
|
+
if (!chatId) throw new Error(`Unexpected guest chat response: ${JSON.stringify(json).slice(0, 200)}`);
|
|
559
|
+
}
|
|
453
560
|
} else {
|
|
454
561
|
let chatEntry: WarmPoolEntry;
|
|
455
562
|
try {
|
|
@@ -555,6 +662,66 @@ export async function createQwenStream(
|
|
|
555
662
|
const timeoutMs = BASE_TIMEOUT_MS + Math.ceil(payloadMB * TIMEOUT_PER_MB);
|
|
556
663
|
|
|
557
664
|
const url = `https://chat.qwen.ai/api/v2/chat/completions?chat_id=${chatId}`;
|
|
665
|
+
|
|
666
|
+
const completionHeaders: Record<string, string> = {
|
|
667
|
+
'accept': 'text/event-stream',
|
|
668
|
+
'content-type': 'application/json',
|
|
669
|
+
'x-request-id': crypto.randomUUID(),
|
|
670
|
+
'timezone': CACHED_TIMEZONE,
|
|
671
|
+
};
|
|
672
|
+
|
|
673
|
+
const page = getPageForAccount(accountId);
|
|
674
|
+
if (page && !page.isClosed() && page.url().includes('chat.qwen.ai')) {
|
|
675
|
+
try {
|
|
676
|
+
const browserResult = await browserStreamFetch(page, url, {
|
|
677
|
+
method: 'POST',
|
|
678
|
+
headers: completionHeaders,
|
|
679
|
+
body: payloadJson,
|
|
680
|
+
timeoutMs,
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
if (browserResult.contentType.includes('text/event-stream') && browserResult.status < 400) {
|
|
684
|
+
const controller = new AbortController();
|
|
685
|
+
return { stream: browserResult.stream, headers: chatHeaders, uiSessionId: chatId, controller, accountId: accountId || 'guest' };
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
if (browserResult.body) {
|
|
689
|
+
const peekText = browserResult.body;
|
|
690
|
+
if (peekText.includes('FAIL_SYS_USER_VALIDATE') || peekText.includes('_____tmd_____') || peekText.includes('RGV587_ERROR')) {
|
|
691
|
+
console.warn('[Qwen] TMD challenge detected via browser, refreshing headers and retrying...');
|
|
692
|
+
try {
|
|
693
|
+
const { headers: freshHeaders } = await getQwenHeaders(true, accountId);
|
|
694
|
+
await sleep(500 + Math.floor(Math.random() * 1000));
|
|
695
|
+
const retryResult = await browserStreamFetch(page, url, {
|
|
696
|
+
method: 'POST',
|
|
697
|
+
headers: completionHeaders,
|
|
698
|
+
body: payloadJson,
|
|
699
|
+
timeoutMs,
|
|
700
|
+
});
|
|
701
|
+
if (retryResult.contentType.includes('text/event-stream') && retryResult.status < 400) {
|
|
702
|
+
const controller = new AbortController();
|
|
703
|
+
return { stream: retryResult.stream, headers: freshHeaders, uiSessionId: chatId, controller, accountId: accountId || 'guest' };
|
|
704
|
+
}
|
|
705
|
+
if (retryResult.body && (retryResult.body.includes('FAIL_SYS_USER_VALIDATE') || retryResult.body.includes('_____tmd_____'))) {
|
|
706
|
+
throw new QwenUpstreamError('Qwen TMD challenge persists after header refresh.', 'FAIL_SYS_USER_VALIDATE', 403);
|
|
707
|
+
}
|
|
708
|
+
if (retryResult.body) {
|
|
709
|
+
handleErrorBody(retryResult.body, retryResult.status);
|
|
710
|
+
}
|
|
711
|
+
} catch (retryErr) {
|
|
712
|
+
if (retryErr instanceof QwenUpstreamError) throw retryErr;
|
|
713
|
+
console.error('[Qwen] Browser TMD retry failed:', (retryErr as Error).message);
|
|
714
|
+
}
|
|
715
|
+
throw new QwenUpstreamError('Qwen TMD anti-bot challenge detected. Headers were refreshed but the challenge persists.', 'FAIL_SYS_USER_VALIDATE', 403);
|
|
716
|
+
}
|
|
717
|
+
handleErrorBody(peekText, browserResult.status);
|
|
718
|
+
}
|
|
719
|
+
} catch (browserErr: any) {
|
|
720
|
+
if (browserErr instanceof QwenUpstreamError || browserErr instanceof RetryableQwenStreamError) throw browserErr;
|
|
721
|
+
console.warn('[Qwen] Browser stream fetch failed, falling back to Node.js:', browserErr.message);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
558
725
|
const controller = new AbortController();
|
|
559
726
|
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
560
727
|
const response = await fetch(url, {
|
|
@@ -626,34 +793,10 @@ export async function createQwenStream(
|
|
|
626
793
|
|
|
627
794
|
const retryPeek = await retryResponse.clone().text().catch(() => '');
|
|
628
795
|
if (retryPeek.includes('FAIL_SYS_USER_VALIDATE') || retryPeek.includes('_____tmd_____')) {
|
|
629
|
-
throw new QwenUpstreamError(
|
|
630
|
-
'Qwen TMD challenge persists after header refresh. The account may need manual captcha resolution.',
|
|
631
|
-
'FAIL_SYS_USER_VALIDATE',
|
|
632
|
-
403,
|
|
633
|
-
);
|
|
796
|
+
throw new QwenUpstreamError('Qwen TMD challenge persists after header refresh. The account may need manual captcha resolution.', 'FAIL_SYS_USER_VALIDATE', 403);
|
|
634
797
|
}
|
|
635
798
|
|
|
636
799
|
if (retryResponse.ok && retryResponse.body) {
|
|
637
|
-
try {
|
|
638
|
-
const errorJson = JSON.parse(retryPeek);
|
|
639
|
-
if (errorJson && (errorJson.success === false || errorJson.error)) {
|
|
640
|
-
const code = errorJson.data?.code || errorJson.code || 'UpstreamError';
|
|
641
|
-
const details = errorJson.data?.details || errorJson.message || errorJson.error?.message || 'Qwen returned an error';
|
|
642
|
-
const wait = errorJson.data?.num !== undefined
|
|
643
|
-
? ` Wait about ${errorJson.data.num} hour(s) before trying again.`
|
|
644
|
-
: '';
|
|
645
|
-
let status = 502;
|
|
646
|
-
if (code === 'RateLimited') status = 429;
|
|
647
|
-
|
|
648
|
-
throw new QwenUpstreamError(
|
|
649
|
-
`Qwen upstream error: ${code}: ${details}.${wait}`,
|
|
650
|
-
code,
|
|
651
|
-
status,
|
|
652
|
-
);
|
|
653
|
-
}
|
|
654
|
-
} catch (e) {
|
|
655
|
-
if (e instanceof QwenUpstreamError) throw e;
|
|
656
|
-
}
|
|
657
800
|
return { stream: retryResponse.body, headers: freshHeaders, uiSessionId: chatId, controller: retryController, accountId: accountId || 'guest' };
|
|
658
801
|
}
|
|
659
802
|
} catch (retryErr) {
|
|
@@ -661,32 +804,9 @@ export async function createQwenStream(
|
|
|
661
804
|
console.error('[Qwen] TMD retry failed:', (retryErr as Error).message);
|
|
662
805
|
}
|
|
663
806
|
|
|
664
|
-
throw new QwenUpstreamError(
|
|
665
|
-
'Qwen TMD anti-bot challenge detected. Headers were refreshed but the challenge persists.',
|
|
666
|
-
'FAIL_SYS_USER_VALIDATE',
|
|
667
|
-
403,
|
|
668
|
-
);
|
|
807
|
+
throw new QwenUpstreamError('Qwen TMD anti-bot challenge detected. Headers were refreshed but the challenge persists.', 'FAIL_SYS_USER_VALIDATE', 403);
|
|
669
808
|
} else {
|
|
670
|
-
|
|
671
|
-
const errorJson = JSON.parse(peekText);
|
|
672
|
-
if (errorJson && (errorJson.success === false || errorJson.error)) {
|
|
673
|
-
const code = errorJson.data?.code || errorJson.code || 'UpstreamError';
|
|
674
|
-
const details = errorJson.data?.details || errorJson.message || errorJson.error?.message || 'Qwen returned an error';
|
|
675
|
-
const wait = errorJson.data?.num !== undefined
|
|
676
|
-
? ` Wait about ${errorJson.data.num} hour(s) before trying again.`
|
|
677
|
-
: '';
|
|
678
|
-
let status = 502;
|
|
679
|
-
if (code === 'RateLimited') status = 429;
|
|
680
|
-
|
|
681
|
-
throw new QwenUpstreamError(
|
|
682
|
-
`Qwen upstream error: ${code}: ${details}.${wait}`,
|
|
683
|
-
code,
|
|
684
|
-
status,
|
|
685
|
-
);
|
|
686
|
-
}
|
|
687
|
-
} catch (e) {
|
|
688
|
-
if (e instanceof QwenUpstreamError) throw e;
|
|
689
|
-
}
|
|
809
|
+
handleErrorBody(peekText, response.status);
|
|
690
810
|
}
|
|
691
811
|
}
|
|
692
812
|
|
|
@@ -695,50 +815,53 @@ export async function createQwenStream(
|
|
|
695
815
|
const contentType = response.headers.get('content-type') || '';
|
|
696
816
|
|
|
697
817
|
if (contentType.includes('application/json')) {
|
|
698
|
-
|
|
699
|
-
const errorJson = JSON.parse(errText);
|
|
700
|
-
if (errorJson?.data?.details?.includes('chat is in progress') ||
|
|
701
|
-
errorJson?.data?.details?.includes('The chat is in progress')) {
|
|
702
|
-
const retryAfterMs = 2000 + Math.floor(Math.random() * 2000);
|
|
703
|
-
throw new RetryableQwenStreamError(
|
|
704
|
-
`Qwen: ${errorJson.data.details}`,
|
|
705
|
-
retryAfterMs,
|
|
706
|
-
);
|
|
707
|
-
}
|
|
708
|
-
if (errorJson?.success === false) {
|
|
709
|
-
const code = errorJson.data?.code || errorJson.code || 'UpstreamError';
|
|
710
|
-
const details = errorJson.data?.details || errorJson.message || 'Qwen returned an error';
|
|
711
|
-
const wait = errorJson.data?.num !== undefined
|
|
712
|
-
? ` Wait about ${errorJson.data.num} hour(s) before trying again.`
|
|
713
|
-
: '';
|
|
714
|
-
let status: number;
|
|
715
|
-
if (code === 'RateLimited') status = 429;
|
|
716
|
-
else if (code === 'Not_Found') status = 404;
|
|
717
|
-
else if (code === 'UpstreamError') status = 502;
|
|
718
|
-
else status = 502;
|
|
719
|
-
throw new QwenUpstreamError(
|
|
720
|
-
`Qwen upstream error: ${code}: ${details}.${wait}`,
|
|
721
|
-
code,
|
|
722
|
-
status,
|
|
723
|
-
);
|
|
724
|
-
}
|
|
725
|
-
if (errorJson?.data?.details?.includes('is not exist') ||
|
|
726
|
-
errorJson?.data?.details?.includes('not exist') ||
|
|
727
|
-
errorJson.data?.details?.includes('does not exist')) {
|
|
728
|
-
throw new RetryableQwenStreamError(
|
|
729
|
-
`Qwen: ${errorJson.data.details}`,
|
|
730
|
-
0,
|
|
731
|
-
);
|
|
732
|
-
}
|
|
733
|
-
} catch (parseOrRetryError) {
|
|
734
|
-
if (parseOrRetryError instanceof RetryableQwenStreamError ||
|
|
735
|
-
parseOrRetryError instanceof QwenUpstreamError) {
|
|
736
|
-
throw parseOrRetryError;
|
|
737
|
-
}
|
|
738
|
-
}
|
|
818
|
+
handleJsonErrorBody(errText);
|
|
739
819
|
}
|
|
740
820
|
throw new Error(`Failed to fetch from Qwen: ${response.status} ${response.statusText} - ${errText}`);
|
|
741
821
|
}
|
|
742
822
|
|
|
743
823
|
return { stream: response.body, headers: chatHeaders, uiSessionId: chatId, controller, accountId: accountId || 'guest' };
|
|
744
824
|
}
|
|
825
|
+
|
|
826
|
+
function handleErrorBody(peekText: string, status: number): never {
|
|
827
|
+
try {
|
|
828
|
+
const errorJson = JSON.parse(peekText);
|
|
829
|
+
if (errorJson && (errorJson.success === false || errorJson.error)) {
|
|
830
|
+
const code = errorJson.data?.code || errorJson.code || 'UpstreamError';
|
|
831
|
+
const details = errorJson.data?.details || errorJson.message || errorJson.error?.message || 'Qwen returned an error';
|
|
832
|
+
const wait = errorJson.data?.num !== undefined ? ` Wait about ${errorJson.data.num} hour(s) before trying again.` : '';
|
|
833
|
+
let errStatus = 502;
|
|
834
|
+
if (code === 'RateLimited') errStatus = 429;
|
|
835
|
+
throw new QwenUpstreamError(`Qwen upstream error: ${code}: ${details}.${wait}`, code, errStatus);
|
|
836
|
+
}
|
|
837
|
+
} catch (e) {
|
|
838
|
+
if (e instanceof QwenUpstreamError) throw e;
|
|
839
|
+
}
|
|
840
|
+
throw new Error(`Qwen returned status ${status}: ${peekText.slice(0, 500)}`);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function handleJsonErrorBody(errText: string): never {
|
|
844
|
+
try {
|
|
845
|
+
const errorJson = JSON.parse(errText);
|
|
846
|
+
if (errorJson?.data?.details?.includes('chat is in progress') || errorJson?.data?.details?.includes('The chat is in progress')) {
|
|
847
|
+
const retryAfterMs = 2000 + Math.floor(Math.random() * 2000);
|
|
848
|
+
throw new RetryableQwenStreamError(`Qwen: ${errorJson.data.details}`, retryAfterMs);
|
|
849
|
+
}
|
|
850
|
+
if (errorJson?.success === false) {
|
|
851
|
+
const code = errorJson.data?.code || errorJson.code || 'UpstreamError';
|
|
852
|
+
const details = errorJson.data?.details || errorJson.message || 'Qwen returned an error';
|
|
853
|
+
const wait = errorJson.data?.num !== undefined ? ` Wait about ${errorJson.data.num} hour(s) before trying again.` : '';
|
|
854
|
+
let status: number;
|
|
855
|
+
if (code === 'RateLimited') status = 429;
|
|
856
|
+
else if (code === 'Not_Found') status = 404;
|
|
857
|
+
else status = 502;
|
|
858
|
+
throw new QwenUpstreamError(`Qwen upstream error: ${code}: ${details}.${wait}`, code, status);
|
|
859
|
+
}
|
|
860
|
+
if (errorJson?.data?.details?.includes('is not exist') || errorJson?.data?.details?.includes('not exist') || errorJson.data?.details?.includes('does not exist')) {
|
|
861
|
+
throw new RetryableQwenStreamError(`Qwen: ${errorJson.data.details}`, 0);
|
|
862
|
+
}
|
|
863
|
+
} catch (e) {
|
|
864
|
+
if (e instanceof RetryableQwenStreamError || e instanceof QwenUpstreamError) throw e;
|
|
865
|
+
}
|
|
866
|
+
throw new Error(`Qwen JSON error: ${errText.slice(0, 500)}`);
|
|
867
|
+
}
|