import { JSDOM } from "jsdom"; import { createHash } from "node:crypto"; import { Buffer } from "node:buffer"; import UserAgent from "user-agents"; // Глобальное управление потоком let activeRequests = 0; const MAX_CONCURRENT = 50; // Сколько всего запросов может "висеть" одновременно let lastRequestStartTime = 0; const MIN_GAP_MS = 500; // ЖЕСТКИЙ ИНТЕРВАЛ: 2 запроса в секунду (МАКСИМАЛЬНАЯ БЕЗОПАСНОСТЬ) export class DuckAI { // Функция "Диспетчер" - гарантирует, что запросы стартуют строго по очереди с паузой private async waitInQueue(reqId: string): Promise { const now = Date.now(); // Вычисляем, когда этому запросу разрешено стартовать const targetStartTime = Math.max(now, lastRequestStartTime + MIN_GAP_MS); lastRequestStartTime = targetStartTime; const waitTime = targetStartTime - now; if (waitTime > 0) { // console.log(`[${reqId}] [Queue] Waiting ${waitTime}ms to maintain pace...`); await new Promise(r => setTimeout(r, waitTime)); } } private async solveChallenge(vqdHash: string, reqId: string): Promise { try { const jsScript = Buffer.from(vqdHash, 'base64').toString('utf-8'); const dom = new JSDOM( ``, { runScripts: 'dangerously', resources: "usable", url: "https://duckduckgo.com/" } ); const window = dom.window as any; window.screen = { width: 1920, height: 1080, availWidth: 1920, availHeight: 1080 }; window.chrome = { runtime: {} }; window.top.__DDG_BE_VERSION__ = 1; window.top.__DDG_FE_CHAT_HASH__ = 1; const jsa = window.document.querySelector('#jsa'); const contentDoc = jsa.contentDocument || jsa.contentWindow.document; const meta = contentDoc.createElement('meta'); meta.setAttribute('http-equiv', 'Content-Security-Policy'); meta.setAttribute('content', "default-src 'none'; script-src 'unsafe-inline';"); contentDoc.head.appendChild(meta); const result = await window.eval(jsScript) as any; if (!result) throw new Error("Challenge script returned nothing"); result.client_hashes[0] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36'; const solved = btoa(JSON.stringify({ ...result, client_hashes: result.client_hashes.map((t: string) => { const hash = createHash('sha256'); hash.update(t); return hash.digest('base64'); }) })); dom.window.close(); return solved; } catch (e: any) { throw new Error(`Challenge Failed: ${e.message}`); } } async chat(request: any): Promise<{ message: string, vqd: string | null }> { const reqId = Math.random().toString(36).substring(7).toUpperCase(); // 1. Становимся в очередь на старт await this.waitInQueue(reqId); activeRequests++; const startTime = Date.now(); const userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36'; const headers: any = { "User-Agent": userAgent, "Accept": "text/event-stream", "x-vqd-accept": "1", "x-fe-version": "serp_20250401_100419_ET-19d438eb199b2bf7c300" }; if (request.vqd) { headers["x-vqd-4"] = request.vqd; } try { console.log(`[${reqId}] [Chat] EXECUTING (Parallel: ${activeRequests}, HasVQD: ${!!request.vqd})`); let solvedVqd = ""; // Если VQD нет, получаем хэш и решаем челлендж if (!request.vqd) { const statusRes = await fetch("https://duckduckgo.com/duckchat/v1/status?q=1", { headers }); const hashHeader = statusRes.headers.get("x-vqd-hash-1"); if (!hashHeader) throw new Error(`Status ${statusRes.status}: No VQD Hash`); solvedVqd = await this.solveChallenge(hashHeader, reqId); } // Сам запрос const chatHeaders: any = { ...headers, "Content-Type": "application/json" }; if (solvedVqd) chatHeaders["x-vqd-hash-1"] = solvedVqd; const response = await fetch("https://duckduckgo.com/duckchat/v1/chat", { method: "POST", headers: chatHeaders, body: JSON.stringify({ model: request.model, messages: request.messages }) }); if (!response.ok) { const body = await response.text(); throw new Error(`DDG Error ${response.status}: ${body.substring(0, 50)}`); } const newVqd = response.headers.get("x-vqd-4"); const text = await response.text(); let llmResponse = ""; const lines = text.split("\n"); for (const line of lines) { if (line.startsWith("data: ")) { try { const chunk = line.slice(6); if (chunk === "[DONE]") break; const json = JSON.parse(chunk); if (json.message) llmResponse += json.message; } catch (e) {} } } console.log(`[${reqId}] [Chat] SUCCESS (${Date.now() - startTime}ms)`); return { message: llmResponse.trim() || "Empty response", vqd: newVqd }; } catch (error: any) { console.error(`[${reqId}] [Chat] FAILED: ${error.message}`); throw error; } finally { activeRequests--; } } async chatStream(request: any): Promise<{ stream: ReadableStream, vqd: string | null }> { const reqId = Math.random().toString(36).substring(7).toUpperCase(); await this.waitInQueue(reqId); activeRequests++; const userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36'; const headers: any = { "User-Agent": userAgent, "Accept": "text/event-stream", "x-vqd-accept": "1", "x-fe-version": "serp_20250401_100419_ET-19d438eb199b2bf7c300" }; if (request.vqd) { headers["x-vqd-4"] = request.vqd; } try { let solvedVqd = ""; if (!request.vqd) { const statusRes = await fetch("https://duckduckgo.com/duckchat/v1/status?q=1", { headers }); const hashHeader = statusRes.headers.get("x-vqd-hash-1"); if (!hashHeader) throw new Error(`Status ${statusRes.status}: No VQD Hash`); solvedVqd = await this.solveChallenge(hashHeader, reqId); } const chatHeaders: any = { ...headers, "Content-Type": "application/json" }; if (solvedVqd) chatHeaders["x-vqd-hash-1"] = solvedVqd; const response = await fetch("https://duckduckgo.com/duckchat/v1/chat", { method: "POST", headers: chatHeaders, body: JSON.stringify({ model: request.model, messages: request.messages }) }); if (!response.ok) { const body = await response.text(); throw new Error(`DDG Error ${response.status}: ${body.substring(0, 50)}`); } const newVqd = response.headers.get("x-vqd-4"); const stream = new ReadableStream({ async start(controller) { const reader = response.body?.getReader(); if (!reader) { controller.close(); return; } const decoder = new TextDecoder(); try { while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value, { stream: true }); const lines = chunk.split("\n"); for (const line of lines) { if (line.startsWith("data: ")) { try { const data = line.slice(6); if (data === "[DONE]") continue; const json = JSON.parse(data); if (json.message) controller.enqueue(json.message); } catch (e) {} } } } } catch (e) { controller.error(e); } finally { controller.close(); activeRequests--; } } }); return { stream, vqd: newVqd }; } catch (error: any) { activeRequests--; throw error; } } getAvailableModels() { return ["gpt-4o-mini", "gpt-5-mini", "openai/gpt-oss-120b"]; } }