tap
Back

google/search

googleRead-only

Google 搜索

www.google.com
Last 7 days
0
Last 30 days
0
All time
0
google/search.js
/* @meta
{
  "name": "google/search",
  "description": "Google 搜索",
  "domain": "www.google.com",
  "args": {
    "query": {"required": true, "description": "Search query"},
    "count": {"required": false, "description": "Number of results (default 10)"}
  },
  "readOnly": true,
  "example": "bb-browser site google/search \"bb-browser\""
}
*/

async function(args) {
  if (!args.query) return {error: 'Missing argument: query', hint: 'Provide a search query string'};
  const num = args.count || 10;

  // Navigate to search URL and parse rendered DOM instead of fetching HTML
  // (Google returns different HTML for programmatic fetch vs browser navigation)
  const url = '/search?q=' + encodeURIComponent(args.query) + '&num=' + num;
  const resp = await fetch(url, {credentials: 'include'});
  if (!resp.ok) return {error: 'HTTP ' + resp.status, hint: 'Make sure a google.com tab is open'};
  const html = await resp.text();
  const parser = new DOMParser();
  const doc = parser.parseFromString(html, 'text/html');

  // Try multiple selectors — Google changes DOM structure frequently
  const results = [];

  // Strategy 1: h3 elements with parent anchor (most reliable)
  const headings = doc.querySelectorAll('h3');
  headings.forEach(h3 => {
    const anchor = h3.closest('a') || h3.parentElement?.querySelector('a[href]');
    if (!anchor) return;
    const link = anchor.getAttribute('href');
    if (!link || link.startsWith('/search') || link.startsWith('#')) return;

    // Find snippet: walk up to the result container and grab text
    let snippet = '';
    // Go up to find the result container (typically 3-5 levels up from h3)
    let container = h3;
    for (let i = 0; i < 5; i++) {
      container = container.parentElement;
      if (!container) break;
      // Check if this container has enough text content beyond the heading
      const allText = container.textContent || '';
      if (allText.length > h3.textContent.length + 50) {
        // Extract text excluding the heading
        const clone = container.cloneNode(true);
        const cloneH3 = clone.querySelector('h3');
        if (cloneH3) cloneH3.remove();
        // Remove cite/url elements
        clone.querySelectorAll('cite').forEach(c => c.remove());
        const remaining = clone.textContent.trim();
        if (remaining.length > 30) {
          snippet = remaining.substring(0, 300);
          break;
        }
      }
    }

    // Fallback: look for spans with substantial text in nearby elements
    if (!snippet) {
      const parent = h3.closest('[data-ved]') || h3.parentElement?.parentElement?.parentElement;
      if (parent) {
        const spans = parent.querySelectorAll('span');
        for (const sp of spans) {
          const txt = sp.textContent.trim();
          if (txt.length > 40 && txt !== h3.textContent.trim()) {
            snippet = txt;
            break;
          }
        }
      }
    }

    results.push({
      title: h3.textContent.trim(),
      url: link.startsWith('/url?q=') ? decodeURIComponent(link.split('/url?q=')[1].split('&')[0]) : link,
      snippet: snippet
    });
  });

  // Deduplicate by URL
  const seen = new Set();
  const unique = results.filter(r => {
    if (seen.has(r.url)) return false;
    seen.add(r.url);
    return true;
  });

  return {query: args.query, count: unique.length, results: unique};
}
Updated Apr 14, 2026Created Apr 14, 2026SHA-256: ba4465d27e4b