GeminiBot
Implement strict sequencer: start requests every 300ms to mimic human flow and avoid IP bans
3727a06 | 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 = 300; // ЖЕСТКИЙ ИНТЕРВАЛ: 3.3 запроса в секунду (безопасный порог для одного IP) | |
| export class DuckAI { | |
| // Функция "Диспетчер" - гарантирует, что запросы стартуют строго по очереди с паузой | |
| private async waitInQueue(reqId: string): Promise<void> { | |
| 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<string> { | |
| try { | |
| const jsScript = Buffer.from(vqdHash, 'base64').toString('utf-8'); | |
| const dom = new JSDOM( | |
| `<iframe id="jsa" sandbox="allow-scripts allow-same-origin" srcdoc="<!DOCTYPE html> | |
| <html> | |
| <head> | |
| <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline'"> | |
| </head> | |
| <body></body> | |
| </html>" style="position: absolute; left: -9999px; top: -9999px;"></iframe>`, | |
| { 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<string> { | |
| 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 = { | |
| "User-Agent": userAgent, | |
| "Accept": "text/event-stream", | |
| "x-vqd-accept": "1", | |
| "x-fe-version": "serp_20250401_100419_ET-19d438eb199b2bf7c300" | |
| }; | |
| try { | |
| console.log(`[${reqId}] [Chat] EXECUTING (Parallel: ${activeRequests})`); | |
| // Получаем токен | |
| 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`); | |
| // Решаем капчу | |
| const solvedVqd = await this.solveChallenge(hashHeader, reqId); | |
| // Сам запрос | |
| const response = await fetch("https://duckduckgo.com/duckchat/v1/chat", { | |
| method: "POST", | |
| headers: { ...headers, "Content-Type": "application/json", "x-vqd-hash-1": solvedVqd }, | |
| body: JSON.stringify(request) | |
| }); | |
| if (!response.ok) { | |
| const body = await response.text(); | |
| throw new Error(`DDG Error ${response.status}: ${body.substring(0, 50)}`); | |
| } | |
| 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 llmResponse.trim() || "Empty response"; | |
| } catch (error: any) { | |
| console.error(`[${reqId}] [Chat] FAILED: ${error.message}`); | |
| throw error; | |
| } finally { | |
| activeRequests--; | |
| } | |
| } | |
| getAvailableModels() { return ["gpt-4o-mini", "gpt-5-mini", "openai/gpt-oss-120b"]; } | |
| } |