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

View File

@@ -0,0 +1,277 @@
/* eslint-disable */
// form-submit-helper.js
// Enhanced form submission with multiple methods
if (window.__FORM_SUBMIT_HELPER_INITIALIZED__) {
// Already initialized, skip
} else {
window.__FORM_SUBMIT_HELPER_INITIALIZED__ = true;
/**
* Submit a form using multiple methods
* @param {string} formSelector - CSS selector for the form
* @param {string} inputSelector - CSS selector for input field to focus (optional)
* @param {string} submitMethod - Preferred submission method
* @returns {Promise<Object>} - Result of the submission
*/
async function submitForm(formSelector = 'form', inputSelector = null, submitMethod = 'auto') {
try {
console.log(`🔄 Attempting form submission with method: ${submitMethod}`);
// Find the form
let form = null;
if (formSelector) {
form = document.querySelector(formSelector);
}
// If no specific form found, try to find the form containing the input
if (!form && inputSelector) {
const input = document.querySelector(inputSelector);
if (input) {
form = input.closest('form');
}
}
// If still no form, try to find any form on the page
if (!form) {
form = document.querySelector('form');
}
if (!form) {
return {
success: false,
error: 'No form found on the page',
};
}
// Focus input if specified
if (inputSelector) {
const input = document.querySelector(inputSelector);
if (input) {
input.focus();
await sleep(200);
}
}
// Try submission based on method
let result = null;
if (submitMethod === 'enter' || submitMethod === 'auto') {
result = await tryEnterKeySubmission(form, inputSelector);
if (result && result.success) {
return result;
}
}
if (submitMethod === 'button' || submitMethod === 'auto') {
result = await tryButtonSubmission(form);
if (result && result.success) {
return result;
}
}
if (submitMethod === 'auto') {
result = await tryFormSubmission(form);
if (result && result.success) {
return result;
}
}
return {
success: false,
error: 'All submission methods failed',
attemptedMethods: submitMethod === 'auto' ? ['enter', 'button', 'form'] : [submitMethod],
};
} catch (error) {
console.error('Error in submitForm:', error);
return {
success: false,
error: `Unexpected error: ${error.message}`,
};
}
}
/**
* Try submitting form using Enter key
* @param {Element} form - The form element
* @param {string} inputSelector - Input selector to focus
* @returns {Promise<Object|null>}
*/
async function tryEnterKeySubmission(form, inputSelector) {
try {
console.log('🔄 Trying Enter key submission');
let targetElement = null;
if (inputSelector) {
targetElement = document.querySelector(inputSelector);
}
if (!targetElement) {
// Find the first input in the form
targetElement = form.querySelector('input[type="text"], input[type="search"], textarea, input:not([type])');
}
if (!targetElement) {
return null;
}
targetElement.focus();
await sleep(200);
const enterEvent = new KeyboardEvent('keydown', {
key: 'Enter',
code: 'Enter',
keyCode: 13,
which: 13,
bubbles: true,
cancelable: true
});
targetElement.dispatchEvent(enterEvent);
// Also try keypress and keyup for compatibility
const enterPress = new KeyboardEvent('keypress', {
key: 'Enter',
code: 'Enter',
keyCode: 13,
which: 13,
bubbles: true,
cancelable: true
});
targetElement.dispatchEvent(enterPress);
const enterUp = new KeyboardEvent('keyup', {
key: 'Enter',
code: 'Enter',
keyCode: 13,
which: 13,
bubbles: true,
cancelable: true
});
targetElement.dispatchEvent(enterUp);
await sleep(500);
return {
success: true,
method: 'enter_key',
element: targetElement.tagName.toLowerCase(),
};
} catch (error) {
console.debug('Enter key submission failed:', error);
return null;
}
}
/**
* Try submitting form by clicking submit button
* @param {Element} form - The form element
* @returns {Promise<Object|null>}
*/
async function tryButtonSubmission(form) {
try {
console.log('🔄 Trying button submission');
const buttonSelectors = [
'input[type="submit"]',
'button[type="submit"]',
'button:not([type])', // Default button type is submit
'input[value*="Search" i]',
'input[value*="Submit" i]',
'input[value*="Send" i]',
'button:contains("Search")',
'button:contains("Submit")',
'button:contains("Send")',
'.submit-btn',
'.search-btn',
'.btn-submit',
'[role="button"][aria-label*="search" i]',
'[role="button"][aria-label*="submit" i]'
];
for (const selector of buttonSelectors) {
try {
let button = form.querySelector(selector);
// If not found in form, try the whole document
if (!button) {
button = document.querySelector(selector);
}
if (button) {
button.click();
await sleep(300);
return {
success: true,
method: 'button_click',
selector: selector,
element: button.tagName.toLowerCase(),
};
}
} catch (e) {
continue;
}
}
return null;
} catch (error) {
console.debug('Button submission failed:', error);
return null;
}
}
/**
* Try submitting form using form.submit()
* @param {Element} form - The form element
* @returns {Promise<Object|null>}
*/
async function tryFormSubmission(form) {
try {
console.log('🔄 Trying form.submit()');
form.submit();
await sleep(300);
return {
success: true,
method: 'form_submit',
};
} catch (error) {
console.debug('Form submission failed:', error);
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 === 'submitForm') {
submitForm(request.formSelector, request.inputSelector, request.submitMethod)
.then(sendResponse)
.catch((error) => {
sendResponse({
success: false,
error: `Unexpected error: ${error.message}`,
});
});
return true; // Indicates async response
} else if (request.action === 'form_submit_ping') {
sendResponse({ status: 'pong' });
return false;
}
});
}

View File

@@ -0,0 +1,147 @@
/**
* Chrome Extension User ID Helper
* This script provides easy access to the Chrome extension user ID in any web page
*/
(function() {
'use strict';
// Namespace for Chrome extension user ID functionality
window.ChromeExtensionUserID = {
// Current user ID (will be populated when available)
userId: null,
// Callbacks to execute when user ID becomes available
callbacks: [],
/**
* Get the current user ID
* @returns {Promise<string|null>} The user ID or null if not available
*/
async getUserId() {
// If already available, return immediately
if (this.userId) {
return this.userId;
}
// Try to get from sessionStorage first
try {
const storedUserId = sessionStorage.getItem('chromeExtensionUserId');
if (storedUserId) {
this.userId = storedUserId;
return storedUserId;
}
} catch (e) {
// Ignore storage errors
}
// Try to get from global window variable
if (window.chromeExtensionUserId) {
this.userId = window.chromeExtensionUserId;
return this.userId;
}
// Request from content script
return new Promise((resolve) => {
// Set up listener for the custom event
const listener = (event) => {
if (event.detail && event.detail.userId) {
this.userId = event.detail.userId;
window.removeEventListener('chromeExtensionUserIdReady', listener);
resolve(this.userId);
}
};
window.addEventListener('chromeExtensionUserIdReady', listener);
// Also check if it's already available
setTimeout(() => {
if (window.chromeExtensionUserId) {
this.userId = window.chromeExtensionUserId;
window.removeEventListener('chromeExtensionUserIdReady', listener);
resolve(this.userId);
} else {
// Timeout after 5 seconds
setTimeout(() => {
window.removeEventListener('chromeExtensionUserIdReady', listener);
resolve(null);
}, 5000);
}
}, 100);
});
},
/**
* Execute callback when user ID becomes available
* @param {Function} callback - Function to execute with user ID
*/
onUserIdReady(callback) {
if (this.userId) {
// Execute immediately if already available
callback(this.userId);
} else {
// Store callback for later execution
this.callbacks.push(callback);
// Try to get user ID
this.getUserId().then((userId) => {
if (userId) {
// Execute all pending callbacks
this.callbacks.forEach(cb => cb(userId));
this.callbacks = [];
}
});
}
},
/**
* Check if user ID is available
* @returns {boolean} True if user ID is available
*/
isAvailable() {
return this.userId !== null;
},
/**
* Get user ID synchronously (only if already loaded)
* @returns {string|null} The user ID or null if not loaded
*/
getUserIdSync() {
return this.userId || window.chromeExtensionUserId || null;
}
};
// Auto-initialize when script loads
window.ChromeExtensionUserID.getUserId().then((userId) => {
if (userId) {
console.log('Chrome Extension User ID Helper: User ID loaded:', userId);
} else {
console.log('Chrome Extension User ID Helper: No user ID available');
}
});
// Listen for the custom event in case it comes later
window.addEventListener('chromeExtensionUserIdReady', (event) => {
if (event.detail && event.detail.userId) {
window.ChromeExtensionUserID.userId = event.detail.userId;
console.log('Chrome Extension User ID Helper: User ID received via event:', event.detail.userId);
// Execute any pending callbacks
window.ChromeExtensionUserID.callbacks.forEach(callback => callback(event.detail.userId));
window.ChromeExtensionUserID.callbacks = [];
}
});
})();
// Also provide a simple global function for easy access
window.getChromeExtensionUserId = function() {
return window.ChromeExtensionUserID.getUserId();
};
// Provide a synchronous version
window.getChromeExtensionUserIdSync = function() {
return window.ChromeExtensionUserID.getUserIdSync();
};
console.log('Chrome Extension User ID Helper loaded. Use window.getChromeExtensionUserId() or window.ChromeExtensionUserID.getUserId()');