项目文件夹

文件
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

139 行
5.5 KiB
JavaScript

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
/**
* 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;
},
});