| import { JSDOM } from "jsdom"; |
| import { createHash } from "node:crypto"; |
| import UserAgent from "user-agents"; |
|
|
| export class DuckAI { |
| private async solveChallenge(vqdHash: string, ua: string): Promise<string> { |
| try { |
| const jsScript = Buffer.from(vqdHash, 'base64').toString('utf-8'); |
| |
| const dom = new JSDOM(`<!DOCTYPE html><html><body></body></html>`, { |
| url: "https://duckduckgo.com/", |
| referrer: "https://duckduckgo.com/", |
| runScripts: "dangerously", |
| resources: "usable" |
| }); |
|
|
| const mockWindow = dom.window as any; |
|
|
| |
| mockWindow.self = mockWindow; |
| mockWindow.parent = mockWindow; |
| mockWindow.top = mockWindow; |
|
|
| |
| const originalGetElementById = mockWindow.document.getElementById; |
| mockWindow.document.getElementById = function(id: string) { |
| const el = originalGetElementById.call(this, id); |
| if (el) return el; |
| return mockWindow.document.createElement('div'); |
| }; |
|
|
| const originalQuerySelector = mockWindow.document.querySelector; |
| mockWindow.document.querySelector = function(selector: string) { |
| try { |
| const el = originalQuerySelector.call(this, selector); |
| if (el) return el; |
| } catch(e) {} |
| return mockWindow.document.createElement('div'); |
| }; |
|
|
| |
| Object.defineProperties(mockWindow.navigator, { |
| userAgent: { value: ua }, |
| platform: { value: 'Win32' }, |
| webdriver: { value: false }, |
| languages: { value: ['en-US', 'en'] } |
| }); |
|
|
| mockWindow.screen = { width: 1920, height: 1080, availWidth: 1920, availHeight: 1080 }; |
| mockWindow.chrome = { runtime: {} }; |
| |
| |
| Object.defineProperty(mockWindow.HTMLIFrameElement.prototype, 'contentDocument', { |
| get: function() { return mockWindow.document; }, |
| configurable: true |
| }); |
| |
| Object.defineProperty(mockWindow.HTMLIFrameElement.prototype, 'contentWindow', { |
| get: function() { return mockWindow; }, |
| configurable: true |
| }); |
|
|
| |
| const originalCreateElement = mockWindow.document.createElement; |
| mockWindow.document.createElement = function(tagName: string) { |
| const element = originalCreateElement.call(this, tagName); |
| if (tagName.toLowerCase() === 'iframe') { |
| try { |
| Object.defineProperty(element, 'contentDocument', { |
| get: () => mockWindow.document, |
| configurable: true |
| }); |
| Object.defineProperty(element, 'contentWindow', { |
| get: () => mockWindow, |
| configurable: true |
| }); |
| } catch (e) {} |
| } |
| return element; |
| }; |
| |
| |
| const originalGetContext = mockWindow.HTMLCanvasElement.prototype.getContext; |
| mockWindow.HTMLCanvasElement.prototype.getContext = function (type: string, options?: any) { |
| try { |
| const ctx = originalGetContext.call(this, type, options); |
| if (ctx) return ctx; |
| } catch (e) {} |
|
|
| return { |
| canvas: this, |
| fillRect: () => {}, |
| clearRect: () => {}, |
| getImageData: (x: number, y: number, w: number, h: number) => ({ |
| data: new Uint8ClampedArray(w * h * 4), |
| width: w, |
| height: h |
| }), |
| putImageData: () => {}, |
| createImageData: () => ({ data: new Uint8ClampedArray(4) }), |
| setTransform: () => {}, |
| drawImage: () => {}, |
| save: () => {}, |
| restore: () => {}, |
| beginPath: () => {}, |
| moveTo: () => {}, |
| lineTo: () => {}, |
| closePath: () => {}, |
| stroke: () => {}, |
| translate: () => {}, |
| scale: () => {}, |
| rotate: () => {}, |
| arc: () => {}, |
| fill: () => {}, |
| measureText: () => ({ width: 0, actualBoundingBoxAscent: 0, actualBoundingBoxDescent: 0 }), |
| transform: () => {}, |
| rect: () => {}, |
| clip: () => {}, |
| createLinearGradient: () => ({ addColorStop: () => {} }), |
| createRadialGradient: () => ({ addColorStop: () => {} }), |
| createPattern: () => ({}), |
| bezierCurveTo: () => {}, |
| quadraticCurveTo: () => {}, |
| fillText: () => {}, |
| strokeText: () => {}, |
| globalAlpha: 1, |
| globalCompositeOperation: 'source-over', |
| fillStyle: '#000000', |
| strokeStyle: '#000000', |
| lineWidth: 1, |
| lineCap: 'butt', |
| lineJoin: 'miter', |
| miterLimit: 10, |
| shadowOffsetX: 0, |
| shadowOffsetY: 0, |
| shadowBlur: 0, |
| shadowColor: 'rgba(0, 0, 0, 0)', |
| font: '10px sans-serif', |
| textAlign: 'start', |
| textBaseline: 'alphabetic' |
| }; |
| } as any; |
|
|
| const originalToDataURL = mockWindow.HTMLCanvasElement.prototype.toDataURL; |
| mockWindow.HTMLCanvasElement.prototype.toDataURL = function(type?: string, quality?: any) { |
| try { |
| const result = originalToDataURL.call(this, type, quality); |
| if (result && result !== "data:,") return result; |
| } catch(e) {} |
| return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="; |
| }; |
|
|
| |
| |
| mockWindow.document = mockWindow.document; |
| mockWindow.window = mockWindow; |
| mockWindow.self = mockWindow; |
| mockWindow.parent = mockWindow; |
| mockWindow.top = mockWindow; |
| |
| |
| |
| |
| |
| const result = mockWindow.eval(` |
| try { |
| ${jsScript} |
| } catch(e) { |
| console.error("Script Eval Error:", e); |
| null; |
| } |
| `); |
| |
| if (!result || !result.client_hashes) { |
| |
| |
| |
| throw new Error("Script executed but returned invalid result (no client_hashes)"); |
| } |
|
|
| result.client_hashes[0] = ua; |
| result.client_hashes = result.client_hashes.map((t: string) => { |
| const hash = createHash('sha256'); |
| hash.update(t); |
| return hash.digest('base64'); |
| }); |
| |
| return btoa(JSON.stringify(result)); |
| } catch (e: any) { |
| console.error(`[DuckAI] Challenge Solver Failed: ${e.message}`); |
| console.log("[DuckAI] Attempting fallback with original VQD..."); |
| return vqdHash; |
| } |
| } |
|
|
| getAvailableModels(): string[] { |
| return [ |
| "gpt-4o-mini", |
| "gpt-5-mini", |
| "openai/gpt-oss-120b" |
| ]; |
| } |
|
|
| getRateLimitStatus() { |
| return { status: "unknown" }; |
| } |
|
|
| async chat(request: any, retries = 1): Promise<string> { |
| const ua = new UserAgent({ deviceCategory: 'desktop' }).toString(); |
| const headers: any = { |
| "User-Agent": ua, |
| "Accept": "text/event-stream", |
| "x-vqd-accept": "1" |
| }; |
|
|
| try { |
| console.log("[DuckAI] Fetching status..."); |
| const statusRes = await fetch("https://duckduckgo.com/duckchat/v1/status?q=1", { headers }); |
| console.log(`[DuckAI] Status response: ${statusRes.status}`); |
| |
| const hashHeader = statusRes.headers.get("x-vqd-hash-1"); |
| console.log(`[DuckAI] x-vqd-hash-1: ${hashHeader ? "FOUND" : "MISSING"}`); |
| |
| if (!hashHeader) throw new Error("Missing x-vqd-hash-1 - DuckDuckGo might be blocking this IP or Challenge failed."); |
|
|
| console.log("[DuckAI] Solving challenge..."); |
| const solvedVqd = await this.solveChallenge(hashHeader, ua); |
| console.log("[DuckAI] Challenge solved (or fallback used)."); |
|
|
| const response = await fetch("https://duckduckgo.com/duckchat/v1/chat", { |
| method: "POST", |
| headers: { ...headers, "x-vqd-hash-1": solvedVqd, "Content-Type": "application/json" }, |
| body: JSON.stringify(request) |
| }); |
|
|
| console.log(`[DuckAI] Chat response status: ${response.status}`); |
|
|
| if (!response.ok) { |
| const errorText = await response.text(); |
| |
| |
| if (response.status === 418 && retries > 0) { |
| console.log(`[DuckAI] Got 418 Error. Retrying... (${retries} attempts left)`); |
| return this.chat(request, retries - 1); |
| } |
|
|
| console.log(`[DuckAI] Error body: ${errorText}`); |
| throw new Error(`DuckDuckGo API Error (${response.status}): ${errorText.substring(0, 100)}`); |
| } |
|
|
| 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) {} |
| } |
| } |
| |
| if (!llmResponse) { |
| console.log("[DuckAI] Warning: Empty LLM response extracted."); |
| throw new Error("Empty response from DuckDuckGo"); |
| } |
| |
| return llmResponse.trim(); |
| } catch (error: any) { |
| |
| if (retries > 0) { |
| console.log(`[DuckAI] Error: ${error.message}. Retrying... (${retries} attempts left)`); |
| return this.chat(request, retries - 1); |
| } |
| throw error; |
| } |
| } |
| } |