Major refactor: Multi-user Chrome MCP extension with remote server architecture

This commit is contained in:
nasir@endelospay.com
2025-08-21 20:09:57 +05:00
parent d97cad1736
commit 5d869f6a7c
125 changed files with 16249 additions and 11906 deletions

View 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;
}
});
}