Files
wehub-resource-sync 9b395f5cc3
E2E Headed Chrome / e2e-headed (macos-15) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (ubuntu-latest) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (windows-latest) (push) Has been cancelled
CI / build (macos-latest) (push) Has been cancelled
CI / build (ubuntu-latest) (push) Has been cancelled
CI / build (windows-latest) (push) Has been cancelled
CI / unit-test (push) Has been cancelled
CI / bun-test (push) Has been cancelled
CI / adapter-test (push) Has been cancelled
CI / smoke-test (macos-latest) (push) Has been cancelled
CI / smoke-test (ubuntu-latest) (push) Has been cancelled
Security Audit / audit (push) Has been cancelled
Build Chrome Extension / build (push) Has been cancelled
Trigger Website Rebuild (Docs Updated) / dispatch (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:39:48 +08:00

132 lines
5.0 KiB
JavaScript

import { cli, Strategy } from '@jackwener/opencli/registry';
import { ArgumentError } from '@jackwener/opencli/errors';
import {
emptySearchResults,
requireBoundedInteger,
requireNonNegativeInteger,
requireRows,
requireSearchQuery,
runBrowserStep,
toHttpsUrl,
} from '../_shared/search-adapter.js';
function decodeDdgUrl(href) {
if (!href) return '';
try {
const url = new URL(href, 'https://duckduckgo.com');
const uddg = url.searchParams.get('uddg');
return toHttpsUrl(uddg || href, 'https://duckduckgo.com');
} catch {
return '';
}
}
function buildExtractFn(limit) {
return 'function(doc){' +
'var r=[];var seen={};var items=doc.querySelectorAll(".result");' +
'for(var i=0;i<items.length;i++){' +
'if(r.length>=' + limit + ')break;' +
'var el=items[i];var te=el.querySelector(".result__a");' +
'var se=el.querySelector(".result__snippet");' +
'var ue=el.querySelector(".result__url");' +
'var ie=el.querySelector(".result__icon__img");' +
'var cls=el.className||"";var rt="web";' +
'if(cls.indexOf("result--ad")!==-1||cls.indexOf("result--ads")!==-1||cls.indexOf("badge--ad")!==-1)continue;' +
'if(!te)continue;' +
'var t=(te.textContent||"").trim();' +
'var h=te.getAttribute("href")||"";' +
'var sn=se?(se.textContent||"").trim():"";' +
'var du=ue?(ue.textContent||"").trim():"";' +
'var ic=ie?(ie.getAttribute("src")||""):"";' +
'if(cls.indexOf("news-result")!==-1)rt="news";' +
'else if(cls.indexOf("video-result")!==-1)rt="video";' +
'else if(cls.indexOf("image-result")!==-1)rt="image";' +
'if(!t||!h||seen[h])continue;seen[h]=true;' +
'r.push([t,h,sn,du,ic,rt]);' +
'}return r;}';
}
function buildExtractorJs(limit) {
return '(' + buildExtractFn(limit) + '(document))';
}
function buildPaginateJs(limit, keyword, offset, region) {
var params = 'q=' + encodeURIComponent(keyword) + '&s=' + offset + '&v=l&o=json';
if (region) params += '&kl=' + encodeURIComponent(region);
return (
'new Promise(function($r){' +
'var x=new XMLHttpRequest();' +
'x.open("POST","/html/",true);' +
'x.setRequestHeader("Content-Type","application/x-www-form-urlencoded");' +
'x.onload=function(){' +
'try{var d=new DOMParser().parseFromString(x.responseText,"text/html");' +
'$r(' + buildExtractFn(limit) + '(d));' +
'}catch(e){$r({error:"parse",message:String(e&&e.message||e)})}' +
'};' +
'x.onerror=function(){$r({error:"network"})};' +
'x.send("' + params + '");' +
'})'
);
}
const command = cli({
site: 'duckduckgo',
name: 'search',
access: 'read',
description: 'Search DuckDuckGo',
domain: 'html.duckduckgo.com',
strategy: Strategy.PUBLIC,
browser: true,
args: [
{ name: 'keyword', positional: true, required: true, help: 'Search query' },
{ name: 'limit', type: 'int', default: 10, help: 'Number of results per page (1-10). For multi-page, use --offset' },
{ name: 'offset', type: 'int', default: 0, help: 'Result offset for pagination (0, 10, 20...). Uses XHR POST internally' },
{ name: 'region', help: 'Region code (e.g. jp-jp, us-en, cn-zh). Default: all regions' },
{ name: 'time', help: 'Time range: d (day), w (week), m (month), y (year)' },
],
columns: ['rank', 'title', 'url', 'snippet', 'displayUrl', 'icon', 'resultType'],
func: async (page, kwargs) => {
const limit = requireBoundedInteger(kwargs.limit, 10, 1, 10, '--limit');
const keyword = requireSearchQuery(kwargs.keyword);
const offset = requireNonNegativeInteger(kwargs.offset, 0, '--offset');
if (offset % 10 !== 0) {
throw new ArgumentError('--offset must be a multiple of 10 for DuckDuckGo HTML pagination');
}
if (kwargs.time && !/^(d|w|m|y)$/.test(String(kwargs.time))) {
throw new ArgumentError('--time must be one of d, w, m, or y');
}
let url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(keyword)}`;
if (kwargs.region) url += `&kl=${encodeURIComponent(String(kwargs.region))}`;
if (kwargs.time) url += `&df=${encodeURIComponent(String(kwargs.time))}`;
await runBrowserStep('duckduckgo search navigation', () => page.goto(url));
try {
await page.wait({ selector: '.result', timeout: 8 });
} catch {
await page.wait(3).catch(function() {});
}
var raw;
if (offset === 0) {
raw = await runBrowserStep('duckduckgo search extraction', () => page.evaluate(buildExtractorJs(limit)));
} else {
raw = await runBrowserStep('duckduckgo search pagination extraction', () => page.evaluate(buildPaginateJs(limit, keyword, offset, kwargs.region)));
}
const rows = requireRows(raw, 'duckduckgo search');
if (rows.length === 0) {
throw emptySearchResults('DuckDuckGo', keyword);
}
return rows.map(function(r, index) {
return {
rank: index + 1 + offset,
title: r[0],
url: decodeDdgUrl(r[1]),
snippet: r[2],
displayUrl: r[3],
icon: r[4],
resultType: r[5],
};
}).filter((row) => row.url);
},
});
export const __test__ = { command };