| 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; |
|
|
| 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) { |
| |
| 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<{ message: string, vqd: string | null }> { |
| const reqId = Math.random().toString(36).substring(7).toUpperCase(); |
| |
| |
| 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 = ""; |
| |
| 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<string>, 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"]; } |
| } |