Files
wehub-resource-sync 9b395f5cc3
Build Chrome Extension / build (push) Waiting to run
Trigger Website Rebuild (Docs Updated) / dispatch (push) Waiting to run
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
chore: import upstream snapshot with attribution
2026-07-13 12:39:48 +08:00

139 lines
5.5 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Google Web Search via browser DOM extraction.
* Uses browser mode to navigate google.com and extract results from the DOM.
*
* Extraction strategy (2026-03): Google no longer uses `.g` class containers.
* Instead, we find all `a` tags containing `h3` within `#rso`, then walk up
* to the result container (`div.tF2Cxc` or closest `div[data-hveid]`) to find
* snippets. This approach is resilient to class name changes.
*/
import { cli, Strategy } from '@jackwener/opencli/registry';
import { CliError } from '@jackwener/opencli/errors';
cli({
site: 'google',
name: 'search',
access: 'read',
description: 'Search Google',
domain: 'google.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 (1-100)' },
{ name: 'lang', default: 'en', help: 'Language short code (e.g. en, zh)' },
],
columns: ['type', 'title', 'url', 'snippet'],
func: async (page, args) => {
const limit = Math.max(1, Math.min(Number(args.limit), 100));
const keyword = encodeURIComponent(args.keyword);
const lang = encodeURIComponent(args.lang);
const url = `https://www.google.com/search?q=${keyword}&hl=${lang}&num=${limit}`;
await page.goto(url);
// Wait until at least one SERP title link is present. On Chrome 148 /
// Linux Wayland, DOM stability can be reached before #rso anchors are
// populated, making browser execution look visually correct while the
// adapter extracts an empty array.
try {
await page.wait({ selector: '#rso a h3', timeout: 5 });
}
catch {
await page.wait(2);
}
const wrapper = await page.evaluate(`
(function() {
var results = [];
var seenUrls = {};
var rso = document.querySelector('#rso');
if (!rso) return {items: results};
// -- Featured snippet (scoped to #rso to avoid matching unrelated elements) --
var featuredEl = rso.querySelector('.xpdopen .hgKElc')
|| rso.querySelector('.IZ6rdc');
if (featuredEl) {
var parentBlock = featuredEl.closest('[data-hveid]') || featuredEl.parentElement;
var fLink = parentBlock ? parentBlock.querySelector('a[href]') : null;
var fUrl = fLink ? fLink.href : '';
if (fUrl) seenUrls[fUrl] = true;
results.push({
type: 'snippet',
title: featuredEl.textContent.trim().slice(0, 200),
url: fUrl,
snippet: '',
});
}
// -- Standard search results --
// Strategy: find all links containing h3 within #rso
var allLinks = rso.querySelectorAll('a');
for (var i = 0; i < allLinks.length; i++) {
var link = allLinks[i];
var h3 = link.querySelector('h3');
if (!h3) continue;
var href = link.href || '';
// Skip non-http, Google internal links, and duplicates
if (!(href.startsWith('http://') || href.startsWith('https://'))) continue;
if (href.indexOf('google.com/search') !== -1) continue;
if (seenUrls[href]) continue;
seenUrls[href] = true;
// Walk up to find result container for snippet extraction
var container = link;
for (var j = 0; j < 6; j++) {
if (container.parentElement && container.parentElement !== rso) {
container = container.parentElement;
}
// Stop at a known result boundary
if (container.getAttribute && container.getAttribute('data-hveid')) break;
}
// Find snippet: look for descriptive text, skip breadcrumbs and metadata
var snippetText = '';
var titleText = h3.textContent.trim();
var candidates = container.querySelectorAll('span, div');
for (var k = 0; k < candidates.length; k++) {
var el = candidates[k];
if (el.querySelector('h3') || el.querySelector('a[href]')) continue;
var text = el.textContent.trim();
if (text.length < 40 || text.length > 500) continue;
if (text === titleText) continue;
// Skip URL breadcrumbs (e.g. "https://example.com path..." or "Site Namehttps://...")
if (text.indexOf('\u203A') !== -1) continue;
if (new RegExp('https?://').test(text.slice(0, 60))) continue;
snippetText = text;
break;
}
results.push({
type: 'result',
title: h3.textContent.trim(),
url: href,
snippet: snippetText.slice(0, 300),
});
}
// -- People Also Ask --
var paaContainers = document.querySelectorAll('[data-sgrd="true"]');
for (var i = 0; i < paaContainers.length; i++) {
var questionEl = paaContainers[i].querySelector('span.CSkcDe');
if (questionEl) {
results.push({
type: 'paa',
title: questionEl.textContent.trim(),
url: '',
snippet: '',
});
}
}
return {items: results};
})()
`);
const results = (wrapper && wrapper.items) || [];
if (results.length === 0) {
throw new CliError('NOT_FOUND', 'No search results found', 'Try a different keyword or check for CAPTCHA');
}
return results;
},
});