Major refactor: Multi-user Chrome MCP extension with remote server architecture
This commit is contained in:
560
app/chrome-extension/inject-scripts/enhanced-search-helper.js
Normal file
560
app/chrome-extension/inject-scripts/enhanced-search-helper.js
Normal file
@@ -0,0 +1,560 @@
|
||||
/* eslint-disable */
|
||||
// enhanced-search-helper.js
|
||||
// Enhanced search automation with multiple submission methods
|
||||
|
||||
if (window.__ENHANCED_SEARCH_HELPER_INITIALIZED__) {
|
||||
// Already initialized, skip
|
||||
} else {
|
||||
window.__ENHANCED_SEARCH_HELPER_INITIALIZED__ = true;
|
||||
|
||||
/**
|
||||
* Perform Google search with enhanced reliability
|
||||
* @param {string} selector - CSS selector for the search box
|
||||
* @param {string} query - Search query
|
||||
* @returns {Promise<Object>} - Result of the search operation
|
||||
*/
|
||||
async function performGoogleSearch(selector, query) {
|
||||
try {
|
||||
console.log(`🔍 Attempting Google search with selector: ${selector}, query: ${query}`);
|
||||
|
||||
// Find the search element
|
||||
const searchElement = document.querySelector(selector);
|
||||
if (!searchElement) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Search element with selector "${selector}" not found`,
|
||||
};
|
||||
}
|
||||
|
||||
// Focus and clear the search box
|
||||
searchElement.focus();
|
||||
await sleep(200);
|
||||
|
||||
// Clear existing content
|
||||
searchElement.select();
|
||||
await sleep(100);
|
||||
|
||||
// Fill the search box
|
||||
searchElement.value = query;
|
||||
|
||||
// Trigger input events to ensure the page recognizes the input
|
||||
searchElement.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
searchElement.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
|
||||
await sleep(500);
|
||||
|
||||
// Try multiple submission methods
|
||||
const submissionSuccess = await submitGoogleSearch(searchElement, query);
|
||||
|
||||
if (submissionSuccess) {
|
||||
console.log(`✅ Google search submitted successfully using selector: ${selector}`);
|
||||
return {
|
||||
success: true,
|
||||
selector,
|
||||
query,
|
||||
method: submissionSuccess.method,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
error: 'All submission methods failed',
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in performGoogleSearch:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: `Unexpected error: ${error.message}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Try multiple methods to submit Google search
|
||||
* @param {Element} searchElement - The search input element
|
||||
* @param {string} query - Search query
|
||||
* @returns {Promise<Object|null>} - Success result or null
|
||||
*/
|
||||
async function submitGoogleSearch(searchElement, query) {
|
||||
const methods = [
|
||||
{
|
||||
name: 'enter_key',
|
||||
action: async () => {
|
||||
console.log('🔄 Method 1: Trying Enter key');
|
||||
searchElement.focus();
|
||||
await sleep(200);
|
||||
|
||||
const enterEvent = new KeyboardEvent('keydown', {
|
||||
key: 'Enter',
|
||||
code: 'Enter',
|
||||
keyCode: 13,
|
||||
which: 13,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
|
||||
searchElement.dispatchEvent(enterEvent);
|
||||
await sleep(1000);
|
||||
|
||||
// Check if search was successful
|
||||
if (await checkSearchResultsLoaded()) {
|
||||
return { method: 'enter_key' };
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'search_button',
|
||||
action: async () => {
|
||||
console.log('🔄 Method 2: Trying search button');
|
||||
|
||||
const buttonSelectors = [
|
||||
'input[value*="Google Search"]',
|
||||
'button[aria-label*="Google Search"]',
|
||||
'input[type="submit"][value*="Google Search"]',
|
||||
'.gNO89b', // Google Search button class
|
||||
'center input[type="submit"]:first-of-type',
|
||||
'button[type="submit"]',
|
||||
'[role="button"][aria-label*="search"]',
|
||||
'.Tg7LZd',
|
||||
];
|
||||
|
||||
for (const buttonSelector of buttonSelectors) {
|
||||
try {
|
||||
const button = document.querySelector(buttonSelector);
|
||||
if (button) {
|
||||
button.click();
|
||||
await sleep(1000);
|
||||
|
||||
if (await checkSearchResultsLoaded()) {
|
||||
return { method: 'search_button', selector: buttonSelector };
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'form_submit',
|
||||
action: async () => {
|
||||
console.log('🔄 Method 3: Trying form submission');
|
||||
|
||||
const form = searchElement.closest('form');
|
||||
if (form) {
|
||||
form.submit();
|
||||
await sleep(1000);
|
||||
|
||||
if (await checkSearchResultsLoaded()) {
|
||||
return { method: 'form_submit' };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'double_enter',
|
||||
action: async () => {
|
||||
console.log('🔄 Method 4: Trying double Enter');
|
||||
searchElement.focus();
|
||||
await sleep(200);
|
||||
|
||||
// First Enter
|
||||
const enterEvent1 = new KeyboardEvent('keydown', {
|
||||
key: 'Enter',
|
||||
code: 'Enter',
|
||||
keyCode: 13,
|
||||
which: 13,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
searchElement.dispatchEvent(enterEvent1);
|
||||
await sleep(300);
|
||||
|
||||
// Second Enter
|
||||
const enterEvent2 = new KeyboardEvent('keydown', {
|
||||
key: 'Enter',
|
||||
code: 'Enter',
|
||||
keyCode: 13,
|
||||
which: 13,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
searchElement.dispatchEvent(enterEvent2);
|
||||
await sleep(1000);
|
||||
|
||||
if (await checkSearchResultsLoaded()) {
|
||||
return { method: 'double_enter' };
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
for (const method of methods) {
|
||||
try {
|
||||
const result = await method.action();
|
||||
if (result) {
|
||||
console.log(`✅ Submission method "${method.name}" successful`);
|
||||
return result;
|
||||
}
|
||||
} catch (error) {
|
||||
console.debug(`Submission method "${method.name}" failed:`, error);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
console.warn('❌ All submission methods failed');
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Google search results have loaded
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async function checkSearchResultsLoaded() {
|
||||
const resultIndicators = [
|
||||
'#search', // Main search results container
|
||||
'#rso', // Results container
|
||||
'.g', // Individual result
|
||||
'.tF2Cxc', // Modern Google result container
|
||||
'#result-stats', // Search statistics
|
||||
'.yuRUbf', // Result link container
|
||||
];
|
||||
|
||||
for (const indicator of resultIndicators) {
|
||||
const element = document.querySelector(indicator);
|
||||
if (element && element.children.length > 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract search results from the current page with intelligent selector discovery
|
||||
* @param {number} maxResults - Maximum number of results to extract
|
||||
* @returns {Promise<Object>} - Extracted results
|
||||
*/
|
||||
async function extractSearchResults(maxResults = 10) {
|
||||
try {
|
||||
console.log('🔍 Starting intelligent search result extraction...');
|
||||
const results = [];
|
||||
|
||||
// Try multiple selectors for Google search results
|
||||
const resultSelectors = [
|
||||
'.tF2Cxc', // Current Google search result container
|
||||
'.g', // Traditional Google search result
|
||||
'#rso .g', // Results container with .g class
|
||||
'.yuRUbf', // Google result link container
|
||||
'.rc', // Another Google result class
|
||||
];
|
||||
|
||||
let resultElements = [];
|
||||
let successfulSelector = null;
|
||||
|
||||
// First try standard selectors
|
||||
for (const selector of resultSelectors) {
|
||||
resultElements = document.querySelectorAll(selector);
|
||||
if (resultElements.length > 0) {
|
||||
successfulSelector = selector;
|
||||
console.log(`✅ Found results with standard selector: ${selector}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If standard selectors fail, try intelligent discovery
|
||||
if (resultElements.length === 0) {
|
||||
console.log('🧠 Standard selectors failed, trying intelligent discovery...');
|
||||
const discoveryResult = await discoverSearchResultElements();
|
||||
resultElements = discoveryResult.elements;
|
||||
successfulSelector = discoveryResult.selector;
|
||||
}
|
||||
|
||||
// Extract results from found elements
|
||||
for (let i = 0; i < Math.min(resultElements.length, maxResults); i++) {
|
||||
const element = resultElements[i];
|
||||
|
||||
try {
|
||||
const extractedResult = extractResultFromElement(element, i + 1);
|
||||
if (extractedResult) {
|
||||
results.push(extractedResult);
|
||||
}
|
||||
} catch (e) {
|
||||
console.debug(`Error extracting result ${i}:`, e);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
results,
|
||||
totalFound: results.length,
|
||||
selectorUsed: successfulSelector,
|
||||
method: resultElements.length > 0 ? 'extraction' : 'none',
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error extracting search results:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
results: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Intelligent discovery of search result elements
|
||||
* @returns {Object} - Object with elements array and successful selector
|
||||
*/
|
||||
async function discoverSearchResultElements() {
|
||||
console.log('🔬 Starting intelligent element discovery...');
|
||||
|
||||
// Intelligent selectors based on common patterns
|
||||
const intelligentSelectors = [
|
||||
// Modern Google patterns (2024+)
|
||||
'[data-ved] h3',
|
||||
'[data-ved]:has(h3)',
|
||||
'[data-ved]:has(a[href*="http"])',
|
||||
'[jscontroller]:has(h3)',
|
||||
'[jscontroller]:has(a[href*="http"])',
|
||||
|
||||
// Generic search result patterns
|
||||
'div[class*="result"]:has(h3)',
|
||||
'div[class*="search"]:has(h3)',
|
||||
'article:has(h3)',
|
||||
'li[class*="result"]:has(h3)',
|
||||
'[role="main"] div:has(h3)',
|
||||
|
||||
// Link-based patterns
|
||||
'a[href*="http"]:has(h3)',
|
||||
'div:has(h3):has(a[href*="http"])',
|
||||
|
||||
// Container patterns
|
||||
'div[class*="container"] > div:has(h3)',
|
||||
'div[id*="result"]:has(h3)',
|
||||
'div[id*="search"]:has(h3)',
|
||||
|
||||
// Semantic patterns
|
||||
'[role="article"]:has(h3)',
|
||||
'[role="listitem"]:has(h3)',
|
||||
'div[aria-label*="result"]:has(h3)',
|
||||
|
||||
// Fallback broad patterns
|
||||
'main div:has(h3)',
|
||||
'#main div:has(h3)',
|
||||
'.main div:has(h3)',
|
||||
'h3:has(+ div)',
|
||||
'div:has(h3)',
|
||||
];
|
||||
|
||||
for (const selector of intelligentSelectors) {
|
||||
try {
|
||||
const elements = document.querySelectorAll(selector);
|
||||
if (elements.length > 0) {
|
||||
// Validate that these look like search results
|
||||
const validElements = Array.from(elements).filter((el) =>
|
||||
validateSearchResultElement(el),
|
||||
);
|
||||
|
||||
if (validElements.length > 0) {
|
||||
console.log(
|
||||
`✅ Found ${validElements.length} results with intelligent selector: ${selector}`,
|
||||
);
|
||||
return {
|
||||
elements: validElements,
|
||||
selector: `intelligent-${selector}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.debug(`Intelligent selector failed: ${selector}`, e);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Final fallback - DOM structure analysis
|
||||
console.log('🔬 Trying DOM structure analysis...');
|
||||
return analyzeDOMForSearchResults();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that an element looks like a search result
|
||||
* @param {Element} element - Element to validate
|
||||
* @returns {boolean} - True if element looks like a search result
|
||||
*/
|
||||
function validateSearchResultElement(element) {
|
||||
try {
|
||||
// Check for common search result indicators
|
||||
const hasHeading = element.querySelector('h1, h2, h3, h4, h5, h6');
|
||||
const hasLink = element.querySelector('a[href*="http"]');
|
||||
const hasText = element.textContent && element.textContent.trim().length > 50;
|
||||
|
||||
// Must have at least heading and link, or substantial text
|
||||
return (hasHeading && hasLink) || hasText;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze DOM structure to find search results using heuristics
|
||||
* @returns {Object} - Object with elements array and successful selector
|
||||
*/
|
||||
function analyzeDOMForSearchResults() {
|
||||
console.log('🔬 Analyzing DOM structure for search results...');
|
||||
|
||||
try {
|
||||
// Look for containers with multiple links (likely search results)
|
||||
const heuristicSelectors = [
|
||||
'div:has(a[href*="http"]):has(h3)',
|
||||
'li:has(a[href*="http"]):has(h3)',
|
||||
'article:has(a[href*="http"])',
|
||||
'main > div:has(h3)',
|
||||
'#main > div:has(h3)',
|
||||
'[role="main"] > div:has(h3)',
|
||||
'div:has(h3):has(a[href*="http"])',
|
||||
'section:has(h3):has(a[href*="http"])',
|
||||
];
|
||||
|
||||
for (const selector of heuristicSelectors) {
|
||||
try {
|
||||
const elements = document.querySelectorAll(selector);
|
||||
if (elements.length > 0) {
|
||||
const validElements = Array.from(elements).filter((el) =>
|
||||
validateSearchResultElement(el),
|
||||
);
|
||||
|
||||
if (validElements.length > 0) {
|
||||
console.log(
|
||||
`✅ Found ${validElements.length} results with DOM analysis: ${selector}`,
|
||||
);
|
||||
return {
|
||||
elements: validElements,
|
||||
selector: `dom-analysis-${selector}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.debug(`DOM analysis selector failed: ${selector}`, e);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Ultimate fallback - any elements with links
|
||||
const fallbackElements = document.querySelectorAll('a[href*="http"]');
|
||||
if (fallbackElements.length > 0) {
|
||||
console.log(`⚠️ Using fallback: found ${fallbackElements.length} link elements`);
|
||||
return {
|
||||
elements: Array.from(fallbackElements).slice(0, 10), // Limit to 10
|
||||
selector: 'fallback-links',
|
||||
};
|
||||
}
|
||||
|
||||
console.warn('❌ DOM analysis failed to find any search results');
|
||||
return {
|
||||
elements: [],
|
||||
selector: null,
|
||||
};
|
||||
} catch (e) {
|
||||
console.error('Error in DOM analysis:', e);
|
||||
return {
|
||||
elements: [],
|
||||
selector: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract result data from a single element
|
||||
* @param {Element} element - Element to extract from
|
||||
* @param {number} index - Result index
|
||||
* @returns {Object|null} - Extracted result or null
|
||||
*/
|
||||
function extractResultFromElement(element, index) {
|
||||
try {
|
||||
// Try multiple patterns for title extraction
|
||||
const titleSelectors = ['h3', 'h2', 'h1', '.LC20lb', '.DKV0Md', 'a[href*="http"]'];
|
||||
let titleElement = null;
|
||||
|
||||
for (const selector of titleSelectors) {
|
||||
titleElement = element.querySelector(selector);
|
||||
if (titleElement) break;
|
||||
}
|
||||
|
||||
// Try multiple patterns for link extraction
|
||||
const linkElement =
|
||||
element.querySelector('a[href*="http"]') || (element.tagName === 'A' ? element : null);
|
||||
|
||||
// Try multiple patterns for snippet extraction
|
||||
const snippetSelectors = ['.VwiC3b', '.s', '.st', 'p', 'div:not(:has(h1,h2,h3,h4,h5,h6))'];
|
||||
let snippetElement = null;
|
||||
|
||||
for (const selector of snippetSelectors) {
|
||||
snippetElement = element.querySelector(selector);
|
||||
if (snippetElement && snippetElement.textContent.trim().length > 20) break;
|
||||
}
|
||||
|
||||
// Extract data
|
||||
const title = titleElement?.textContent?.trim() || 'No title found';
|
||||
const url = linkElement?.href || '';
|
||||
const snippet = snippetElement?.textContent?.trim() || '';
|
||||
|
||||
// Validate we have meaningful data
|
||||
if (title && title !== 'No title found' && url) {
|
||||
return {
|
||||
title,
|
||||
url,
|
||||
snippet,
|
||||
index,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (e) {
|
||||
console.debug(`Error extracting from element:`, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sleep utility function
|
||||
* @param {number} ms - Milliseconds to sleep
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
// Listen for messages from the extension
|
||||
chrome.runtime.onMessage.addListener((request, _sender, sendResponse) => {
|
||||
if (request.action === 'performGoogleSearch') {
|
||||
performGoogleSearch(request.selector, request.query)
|
||||
.then(sendResponse)
|
||||
.catch((error) => {
|
||||
sendResponse({
|
||||
success: false,
|
||||
error: `Unexpected error: ${error.message}`,
|
||||
});
|
||||
});
|
||||
return true; // Indicates async response
|
||||
} else if (request.action === 'extractSearchResults') {
|
||||
extractSearchResults(request.maxResults)
|
||||
.then(sendResponse)
|
||||
.catch((error) => {
|
||||
sendResponse({
|
||||
success: false,
|
||||
error: `Unexpected error: ${error.message}`,
|
||||
results: [],
|
||||
});
|
||||
});
|
||||
return true; // Indicates async response
|
||||
} else if (request.action === 'enhanced_search_ping') {
|
||||
sendResponse({ status: 'pong' });
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
Reference in New Issue
Block a user