| | // duckai_cli.js | | | // Author : Omegatech | | | // WhatsApp : https://whatsapp.com/channel/0029Vb785rSBlHpWSitPY61i | | | // Name : Duck.ai Scraper (CLI) | | | // Date : 15 October 2026 | | | // | | | // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | | | // DISCLAIMER | | | // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | | | // This is an unofficial scraper for https://duck.ai/ | | | // Duck.ai has no public API. This tool interacts with the | | | // private chat endpoint by driving a real browser (puppeteer-real-browser). | | | // | | | // β’ Use at your own risk. | | | // β’ Do not abuse rate limits or run high-volume automation. | | | // β’ DuckDuckGo may change the frontend / challenge at any time. | | | // β’ This project is for educational / personal use only. | | | // β’ The author is not responsible for any account blocks, IP bans, | | | // or legal consequences arising from misuse. | | | // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | | | // | | | // SCRAPE INFO | | | // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ |
| | // Target : https://duck.ai/ |
| | // Method : Browser automation (puppeteer-real-browser) |
| | // Endpoint : POST /duckchat/v1/chat (SSE stream) |
| | // Challenge : Solved automatically by the real browser session | | | // Features : | | | // - Text chat (single or multi-turn) | | | // - Model selection | | | // - Reasoning / sources / tool-calls extraction | | | // - Image generation capture (base64 β saved files) | | | // - Debug mode | | | // Dependencies: | | | // npm i puppeteer-real-browser | | | // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ |
| | const { connect } = require("puppeteer-real-browser"); |
| | const fs = require("fs"); |
| | const path = require("path"); |
| | // ββ Config ββ | | | const DUCK_URL = "https://duck.ai/"; | | | const INPUT_SELECTOR = |
| | 'textarea, div[contenteditable="true"][role="textbox"], div[contenteditable="true"]'; |
| | const IMAGE_DIR = path.join(process.cwd(), "duckai-images"); |
| | const DEFAULT_TIMEOUT = 240000; |
| | // ββ Fetch hook injected into the page ββ |
| | const HOOK = () => { |
| | window.__duckaiCaptures = []; |
| | const origFetch = window.fetch.bind(window); |
| | window.fetch = async function (...args) { |
| | let url = ""; |
| | let reqBody = null; |
| | try { |
| | if (typeof args[0] === "string") url = args[0]; |
| | else if (args[0] && args[0].url) url = args[0].url; |
| | else if (args[0] && args[0].toString) url = args[0].toString(); |
| | if (args[1] && args[1].body) { |
| | reqBody = | | | typeof args[1].body === "string" ? args[1].body : "[non-string]"; | | | } |
| | } catch (_) {} |
| | const response = await origFetch(...args); |
| | if (url && url.indexOf("/duckchat/v1/chat") !== -1) { |
| | const cap = { |
| | url, | | | reqBody, | | | ts: Date.now(), | | | body: "", | | | done: false, | | | error: null, |
| | }; |
| | window.__duckaiCaptures.push(cap); |
| | try { |
| | const clone = response.clone(); |
| | const reader = clone.body.getReader(); |
| | const decoder = new TextDecoder(); |
| | (async () => { |
| | try { |
| | while (true) { |
| | const { done, value } = await reader.read(); |
| | if (done) break; |
| | cap.body += decoder.decode(value, { stream: true }); |
| | } |
| | cap.done = true; |
| | } catch (err) { |
| | cap.error = err && err.message ? err.message : String(err); |
| | cap.done = true; |
| | } |
| | })(); |
| | } catch (err) { |
| | cap.error = err && err.message ? err.message : String(err); |
| | cap.done = true; |
| | } | | | } | | | return response; |
| | }; |
| | }; |
| | // ββ Browser helpers ββ |
| | async function openDuck(options = {}) { |
| | const connection = await connect({ |
| | headless: options.headless === true, |
| | turnstile: true, | | | fingerprint: true, |
| | args: [ |
| | "--no-sandbox", |
| | "--disable-setuid-sandbox", |
| | "--disable-dev-shm-usage", |
| | "--disable-blink-features=AutomationControlled", |
| | ], |
| | customConfig: {}, |
| | }); |
| | const { browser, page } = connection; |
| | await page.evaluateOnNewDocument(HOOK); |
| | await page.setViewport({ width: 1366, height: 900 }); |
| | await page.setExtraHTTPHeaders({ |
| | "Accept-Language": "id-ID,id;q=0.9,en-US;q=0.8,en;q=0.7", |
| | }); |
| | await page.goto(DUCK_URL, { waitUntil: "domcontentloaded", timeout: 90000 }); |
| | await page.waitForSelector(INPUT_SELECTOR, { timeout: 60000 }); |
| | await new Promise((r) => setTimeout(r, 1500)); |
| | return { browser, page }; |
| | } | | | // ββ SSE parsing ββ |
| | function parseSSE(body) { |
| | const events = []; |
| | if (!body) return events; |
| | const lines = body.split("\n"); |
| | for (const raw of lines) { |
| | const line = raw.trim(); |
| | if (!line.startsWith("data:")) continue; |
| | const payload = line.slice(5).trim(); |
| | if (!payload \|\| payload === "[DONE]" \|\| payload === "[PING]") continue; |
| | try { |
| | events.push(JSON.parse(payload)); |
| | } catch (_) { |
| | events.push({ _raw: payload }); |
| | } | | | } | | | return events; | | | } |
| | function assembleResponse(events) { |
| | const textParts = []; |
| | const reasoningParts = []; |
| | const sources = []; |
| | const toolCalls = []; |
| | const toolResults = []; |
| | const images = []; |
| | let model = null; |
| | let responseId = null; |
| | let summaryText = null; |
| | for (const e of events) { |
| | if (!model && e.model) model = e.model; |
| | if (!responseId && e.id) responseId = e.id; |
| | if (e.role === "assistant" && typeof e.message === "string") { |
| | textParts.push(e.message); |
| | } |
| | if (e.role === "reasoning" && e.state === "text-delta" && typeof e.text === "string") { |
| | reasoningParts.push(e.text); |
| | } |
| | if (e.role === "reasoning" && e.state === "done" && Array.isArray(e.summaryText)) { |
| | summaryText = e.summaryText.join("\n"); |
| | } |
| | if (e.role === "source" && e.source) { |
| | sources.push(e.source); |
| | } |
| | if (e.role === "tool-invocation" && e.state === "call") { |
| | let args = e.toolArguments; |
| | try { |
| | args = typeof args === "string" ? JSON.parse(args) : args; |
| | } catch (_) {} |
| | toolCalls.push({ name: e.toolName, args, callId: e.toolCallId }); |
| | } |
| | if (e.role === "tool-invocation" && e.state === "result") { |
| | toolResults.push({ |
| | callId: e.toolCallId, | | | result: e.result, | | | data: e.data, | | | }); | | | } | | | if ( |
| | e.role === "ui-component" && |
| | e.name === "generate-image" && |
| | e.state === "data" && |
| | e.data && |
| | typeof e.data.b64Image === "string" && |
| | e.data.status === "success" |
| | ) { |
| | images.push({ |
| | id: e.id, | | | toolCallId: e.toolCallId, | | | format: e.data.format || "jpeg", | | | width: e.data.width || null, | | | height: e.data.height || null, | | | generationDurationMs: e.data.generationDurationMs || null, | | | moderationToken: e.data.moderationToken || null, | | | b64: e.data.b64Image, | | | }); | | | } | | | } | | | return { | | | model, | | | responseId, |
| | text: textParts.join(""), |
| | reasoning: reasoningParts.join(""), |
| | reasoningSummary: summaryText, | | | sources, | | | toolCalls, | | | toolResults, | | | images, | | | }; | | | } | | | // ββ Image saver ββ |
| | function saveImages(result, dir) { |
| | if (!result.images \|\| !result.images.length) return []; |
| | const targetDir = dir || IMAGE_DIR; |
| | if (!fs.existsSync(targetDir)) { |
| | fs.mkdirSync(targetDir, { recursive: true }); |
| | } |
| | const saved = []; |
| | const stamp = Date.now(); |
| | for (let i = 0; i < result.images.length; i++) { |
| | const img = result.images[i]; |
| | const ext = img.format === "png" ? "png" : "jpg"; |
| | const name = `duckai-${stamp}-${i}.${ext}`; |
| | const full = path.join(targetDir, name); |
| | fs.writeFileSync(full, Buffer.from(img.b64, "base64")); |
| | const toolMatch = result.toolCalls.find( |
| | (c) => c.name === "GenerateImage" && c.callId === img.toolCallId |
| | ); |
| | saved.push({ |
| | path: full, | | | format: img.format, | | | width: img.width, | | | height: img.height, | | | bytes: Buffer.byteLength(img.b64, "base64"), | | | prompt: toolMatch && toolMatch.args ? toolMatch.args.imageGenPrompt : null, | | | }); | | | } | | | return saved; | | | } | | | // ββ Send single message ββ |
| | async function sendChat(page, message, options = {}) { |
| | const prevCount = await page.evaluate(() => window.__duckaiCaptures.length); |
| | const input = await page.waitForSelector(INPUT_SELECTOR, { timeout: 30000 }); |
| | await input.click(); |
| | await new Promise((r) => setTimeout(r, 300)); |
| | // Clear existing content |
| | await page.evaluate((sel) => { |
| | const el = document.querySelector(sel); |
| | if (!el) return; |
| | el.focus(); |
| | if (el.tagName === "TEXTAREA") { |
| | el.value = ""; |
| | } else if (el.isContentEditable) { |
| | el.innerHTML = ""; |
| | } |
| | }, INPUT_SELECTOR); |
| | await new Promise((r) => setTimeout(r, 200)); |
| | // Type via CDP (more reliable) |
| | const client = await page.target().createCDPSession(); |
| | await client.send("Input.insertText", { text: message }); |
| | await client.detach().catch(() => {}); |
| | await new Promise((r) => setTimeout(r, 700)); |
| | const typed = await page.evaluate((sel) => { |
| | const el = document.querySelector(sel); |
| | if (!el) return ""; |
| | return el.tagName === "TEXTAREA" |
| | ? el.value || "" |
| | : el.textContent \|\| ""; |
| | }, INPUT_SELECTOR); |
| | if (options.debug) { |
| | console.log("[debug] typed:", JSON.stringify(typed)); |
| | console.log("[debug] want :", JSON.stringify(message)); |
| | } |
| | if (typed.trim().length === 0) { |
| | throw new Error("composer empty after typing"); |
| | } | | | // Click send button or press Enter |
| | const clicked = await page.evaluate(() => { |
| | const buttons = Array.from(document.querySelectorAll("button")); |
| | const match = buttons.find((b) => { |
| | const label = ( |
| | b.getAttribute("aria-label") \|\| |
| | b.getAttribute("title") \|\| |
| | b.textContent || | | | "" | | | ).toLowerCase(); | | | const disabled = |
| | b.disabled \|\| b.getAttribute("aria-disabled") === "true"; |
| | if (disabled) return false; |
| | return ( |
| | label.includes("send") \|\| |
| | label.includes("kirim") \|\| |
| | label.includes("submit") \|\| |
| | label.includes("ask") |
| | ); |
| | }); |
| | if (match) { |
| | match.click(); |
| | return true; | | | } |
| | const submit = buttons.find( |
| | (b) => |
| | b.type === "submit" && |
| | !b.disabled && |
| | b.getAttribute("aria-disabled") !== "true" |
| | ); |
| | if (submit) { |
| | submit.click(); |
| | return true; | | | } | | | return false; |
| | }); |
| | if (!clicked) { |
| | await page.keyboard.press("Enter"); |
| | } | | | // Wait for capture |
| | const deadline = Date.now() + (options.timeout \|\| DEFAULT_TIMEOUT); |
| | let capture = null; |
| | while (Date.now() < deadline) { |
| | const state = await page.evaluate((prev) => { |
| | const caps = window.__duckaiCaptures; |
| | if (caps.length <= prev) return null; |
| | const c = caps[caps.length - 1]; |
| | return { | | | done: c.done, | | | error: c.error, | | | body: c.body, | | | reqBody: c.reqBody, |
| | }; |
| | }, prevCount); |
| | if (state && state.done) { |
| | capture = state; |
| | break; | | | } | | | await new Promise((r) => setTimeout(r, 400)); | | | } |
| | if (!capture) throw new Error("capture timeout"); |
| | if (options.debug && capture.reqBody) { |
| | const m = capture.reqBody.match(/"text":"((?:[^"\\]\|\\.){0,300})/); |
| | if (m) console.log("[debug] request text:", JSON.stringify(m[1])); |
| | } |
| | const events = parseSSE(capture.body); |
| | const result = { events, ...assembleResponse(events) }; |
| | if (result.images.length && options.saveImages !== false) { |
| | result.savedImages = saveImages(result, options.imageDir); |
| | } | | | return result; | | | } | | | // ββ Public API ββ |
| | async function ask(message, options = {}) { |
| | const { browser, page } = await openDuck(options); |
| | try { |
| | return await sendChat(page, message, options); |
| | } finally { |
| | if (!options.keepOpen) { |
| | try { |
| | await browser.close(); |
| | } catch (_) {} |
| | } | | | } | | | } |
| | async function askMany(messages, options = {}) { |
| | const { browser, page } = await openDuck(options); |
| | const results = []; |
| | try { |
| | for (const msg of messages) { |
| | const r = await sendChat(page, msg, options); |
| | results.push({ message: msg, ...r }); |
| | await new Promise((res) => |
| | setTimeout(res, options.gapMs || 1800) | | | ); | | | } | | | return results; |
| | } finally { |
| | if (!options.keepOpen) { |
| | try { |
| | await browser.close(); |
| | } catch (_) {} |
| | } | | | } | | | } | | | // ββ CLI ββ |
| | function printResult(r) { |
| | console.log("MODEL:", r.model \|\| "unknown"); |
| | console.log("β".repeat(50)); |
| | console.log(r.text \|\| "(empty)"); |
| | if (r.reasoning) { |
| | console.log("\nββ reasoning ββ"); |
| | console.log(r.reasoning); |
| | } |
| | if (r.sources && r.sources.length) { |
| | console.log("\nββ sources ββ"); |
| | for (const s of r.sources) { |
| | console.log(s.url \|\| s.title \|\| JSON.stringify(s)); |
| | } | | | } |
| | if (r.toolCalls && r.toolCalls.length) { |
| | console.log("\nββ tool calls ββ"); |
| | for (const t of r.toolCalls) { |
| | console.log(t.name, JSON.stringify(t.args)); |
| | } | | | } |
| | if (r.savedImages && r.savedImages.length) { |
| | console.log("\nββ images ββ"); |
| | for (const img of r.savedImages) { |
| | console.log("saved:", img.path); |
| | console.log( |
| | ` format=${img.format} size=${img.width}x${img.height} bytes=${img.bytes}` |
| | ); |
| | if (img.prompt) console.log(" prompt:", img.prompt); |
| | } | | | } | | | } |
| | async function main() { |
| | const args = process.argv.slice(2); |
| | const mode = args[0] \|\| "single"; |
| | const payload = args.slice(1).join(" ") \|\| "halo"; |
| | const headless = args.includes("--headless"); |
| | const debug = args.includes("--debug"); |
| | console.log("Duck.ai Scraper (Omegatech)"); |
| | console.log("β".repeat(50)); |
| | try { | | | if (mode === "multi") { | | | const list = payload |
| | .split("\|") |
| | .map((s) => s.trim()) |
| | .filter(Boolean); |
| | const results = await askMany(list, { headless, debug }); |
| | for (const r of results) { |
| | console.log("\n=== USER:", r.message); |
| | printResult(r); |
| | } | | | return; | | | } | | | // default: single |
| | const message = mode === "single" ? payload : args.join(" ") \|\| "halo"; |
| | const r = await ask(message, { headless, debug }); |
| | printResult(r); |
| | } catch (err) { |
| | console.error("\n[error]", err && err.message ? err.message : err); |
| | process.exit(1); |
| | } | | | } |
| | // Export for require() |
| | module.exports = { |
| | openDuck, | | | sendChat, | | | parseSSE, | | | assembleResponse, | | | saveImages, | | | ask, | | | askMany, | | | }; | | | // Run as CLI |
| | if (require.main === module) { |
| | main(); |
| | } |