UNPKG

@semantest/chrome-extension

Version:

Browser extension for ChatGPT-buddy - AI automation extension built on Web-Buddy framework

485 lines (484 loc) 17.4 kB
/** * @fileoverview Contract Discovery Adapter * @description Adapter for discovering and managing web application contracts */ import { webBuddyStorage } from '../storage'; /** * Event emitted when contracts are discovered */ export class ContractsDiscoveredEvent extends CustomEvent { constructor(contracts) { super('web-buddy:contracts-discovered', { detail: contracts, bubbles: true, cancelable: false }); } } /** * Contract discovery and management adapter for Web-Buddy extension */ export class ContractDiscoveryAdapter { constructor() { this.discoveredContracts = new Map(); this.isInitialized = false; } /** * Initialize contract discovery */ async initialize() { if (this.isInitialized) { return; } console.log('🔍 Initializing contract discovery adapter...'); // Set up DOM observer for dynamic contract discovery this.setupDOMObserver(); // Set up message handlers for contract communication this.setupMessageHandlers(); // Perform initial contract discovery await this.discoverContracts(); // Start periodic discovery this.startPeriodicDiscovery(); this.isInitialized = true; console.log('✅ Contract discovery adapter initialized'); } /** * Discover contracts on the current page */ async discoverContracts() { const contracts = []; try { // Method 1: Discover from meta tags contracts.push(...this.discoverFromMetaTags()); // Method 2: Discover from JSON-LD scripts contracts.push(...this.discoverFromJsonLD()); // Method 3: Discover from custom elements contracts.push(...this.discoverFromCustomElements()); // Method 4: Discover from data attributes contracts.push(...this.discoverFromDataAttributes()); // Method 5: Discover from window events (contract library communication) contracts.push(...this.discoverFromWindowEvents()); // Store discovered contracts for (const contract of contracts) { await this.registerContract(contract); } if (contracts.length > 0) { console.log(`🎯 Discovered ${contracts.length} contract(s) on ${window.location.hostname}`); // Emit discovery event window.dispatchEvent(new ContractsDiscoveredEvent(contracts)); // Notify background script chrome.runtime.sendMessage({ type: 'contracts:discovered', contracts: contracts, url: window.location.href, timestamp: new Date().toISOString() }); } return contracts; } catch (error) { console.error('❌ Contract discovery failed:', error); return []; } } /** * Register a contract for use by the extension */ async registerContract(contract) { try { // Validate contract const validation = this.validateContract(contract); if (!validation.valid) { console.warn('⚠️ Contract validation failed:', validation.errors); return; } // Generate contract ID const contractId = this.generateContractId(contract); // Store in memory this.discoveredContracts.set(contractId, contract); // Convert to automation patterns for existing storage system const patterns = this.convertContractToPatterns(contract); for (const pattern of patterns) { await webBuddyStorage.saveAutomationPattern(pattern); } console.log(`✅ Registered contract: ${contract.title} (${contractId})`); } catch (error) { console.error('❌ Failed to register contract:', error); } } /** * Get all discovered contracts */ getDiscoveredContracts() { return Array.from(this.discoveredContracts.values()); } /** * Get contracts by domain */ getContractsByDomain(domain) { return this.getDiscoveredContracts().filter(contract => contract.domain === domain || this.domainMatches(domain, contract.domain)); } /** * Execute capability using contract */ async executeCapability(contractId, capabilityName, parameters = {}) { const contract = this.discoveredContracts.get(contractId); if (!contract) { throw new Error(`Contract not found: ${contractId}`); } const capability = contract.capabilities[capabilityName]; if (!capability) { throw new Error(`Capability not found: ${capabilityName} in contract ${contractId}`); } // Execute capability based on type return this.executeCapabilityAction(capability, parameters); } /** * Validate contract structure */ validateContract(contract) { const errors = []; if (!contract.version) { errors.push('Contract version is required'); } if (!contract.domain) { errors.push('Contract domain is required'); } if (!contract.title) { errors.push('Contract title is required'); } if (!contract.capabilities || Object.keys(contract.capabilities).length === 0) { errors.push('Contract must have at least one capability'); } // Validate capabilities for (const [name, capability] of Object.entries(contract.capabilities || {})) { if (!capability.type || !capability.description || !capability.selector) { errors.push(`Capability ${name} is missing required fields`); } } return { valid: errors.length === 0, errors }; } /** * Convert contract to automation patterns for existing storage */ convertContractToPatterns(contract) { const patterns = []; for (const [name, capability] of Object.entries(contract.capabilities)) { const selector = typeof capability.selector === 'string' ? capability.selector : capability.selector.primary; patterns.push({ url: window.location.href, domain: contract.domain, action: name, selector: selector, parameters: { type: capability.type, description: capability.description, timeout: capability.timeout, retries: capability.retries }, success: true, contextHash: this.generateContextHash(), userConfirmed: true // Auto-confirm contract-based patterns }); } return patterns; } /** * Execute capability action */ async executeCapabilityAction(capability, parameters) { const selector = typeof capability.selector === 'string' ? capability.selector : capability.selector.primary; // Find element const element = await this.findElement(capability.selector, capability.timeout); if (!element) { throw new Error(`Element not found: ${selector}`); } // Execute based on capability type switch (capability.type) { case 'action': return this.executeAction(element, parameters); case 'form': return this.executeFormAction(element, parameters); case 'query': return this.executeQuery(element, parameters); case 'navigation': return this.executeNavigation(element, parameters); default: throw new Error(`Unsupported capability type: ${capability.type}`); } } /** * Find element using selector definition */ async findElement(selectorDef, timeout = 5000) { if (typeof selectorDef === 'string') { return document.querySelector(selectorDef); } // Try primary selector let element = document.querySelector(selectorDef.primary); if (element) { return element; } // Try fallback selectors if (selectorDef.fallback) { for (const fallbackSelector of selectorDef.fallback) { element = document.querySelector(fallbackSelector); if (element) { return element; } } } return null; } /** * Execute action capability */ async executeAction(element, parameters) { if (element instanceof HTMLElement) { element.click(); return { success: true, action: 'click', element: element.tagName }; } throw new Error('Element is not clickable'); } /** * Execute form capability */ async executeFormAction(element, parameters) { if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) { const value = parameters.value || parameters.text || ''; element.value = value; element.dispatchEvent(new Event('input', { bubbles: true })); element.dispatchEvent(new Event('change', { bubbles: true })); return { success: true, action: 'fillInput', value, element: element.tagName }; } throw new Error('Element is not a form input'); } /** * Execute query capability */ async executeQuery(element, parameters) { return { success: true, action: 'query', data: { textContent: element.textContent, innerHTML: element.innerHTML, attributes: Array.from(element.attributes).reduce((acc, attr) => { acc[attr.name] = attr.value; return acc; }, {}) } }; } /** * Execute navigation capability */ async executeNavigation(element, parameters) { if (element instanceof HTMLAnchorElement) { const href = element.href; if (parameters.newTab) { window.open(href, '_blank'); } else { window.location.href = href; } return { success: true, action: 'navigation', href }; } throw new Error('Element is not a navigation link'); } /** * Discover contracts from meta tags */ discoverFromMetaTags() { const contracts = []; const metaTags = document.querySelectorAll('meta[name^="web-buddy-contract"]'); metaTags.forEach(tag => { const content = tag.getAttribute('content'); if (content) { try { const contract = JSON.parse(content); contracts.push(contract); } catch (error) { console.warn('Invalid contract in meta tag:', error); } } }); return contracts; } /** * Discover contracts from JSON-LD scripts */ discoverFromJsonLD() { const contracts = []; const scripts = document.querySelectorAll('script[type="application/ld+json"]'); scripts.forEach(script => { try { const data = JSON.parse(script.textContent || ''); if (data['@type'] === 'WebBuddyContract') { contracts.push(data); } } catch (error) { // Ignore invalid JSON-LD } }); return contracts; } /** * Discover contracts from custom elements */ discoverFromCustomElements() { const contracts = []; const contractElements = document.querySelectorAll('web-buddy-contract'); contractElements.forEach(element => { const contractData = element.textContent; if (contractData) { try { const contract = JSON.parse(contractData); contracts.push(contract); } catch (error) { console.warn('Invalid contract in custom element:', error); } } }); return contracts; } /** * Discover contracts from data attributes */ discoverFromDataAttributes() { const contracts = []; const elements = document.querySelectorAll('[data-automation-contract]'); elements.forEach(element => { const contractData = element.getAttribute('data-automation-contract'); if (contractData) { try { const contract = JSON.parse(contractData); contracts.push(contract); } catch (error) { console.warn('Invalid contract in data attribute:', error); } } }); return contracts; } /** * Discover contracts from window events (contract library communication) */ discoverFromWindowEvents() { // This will be populated by listening to contract registration events return []; } /** * Set up DOM observer for dynamic contract discovery */ setupDOMObserver() { this.domObserver = new MutationObserver((mutations) => { let shouldRediscover = false; mutations.forEach((mutation) => { if (mutation.type === 'childList') { mutation.addedNodes.forEach((node) => { if (node.nodeType === Node.ELEMENT_NODE) { const element = node; // Check for contract-related elements if (element.tagName === 'WEB-BUDDY-CONTRACT' || element.hasAttribute('data-automation-contract') || element.querySelector('web-buddy-contract, [data-automation-contract]')) { shouldRediscover = true; } } }); } }); if (shouldRediscover) { // Debounce rediscovery setTimeout(() => this.discoverContracts(), 100); } }); this.domObserver.observe(document.body, { childList: true, subtree: true }); } /** * Set up message handlers for contract communication */ setupMessageHandlers() { // Listen for contract registration events from the contract library window.addEventListener('web-buddy:contract-registered', (event) => { const contract = event.detail; this.registerContract(contract); }); // Listen for contract discovery requests window.addEventListener('message', (event) => { if (event.data.type === 'web-buddy:request-contracts') { const contracts = this.getDiscoveredContracts(); event.source?.postMessage({ type: 'web-buddy:contracts-response', contracts, correlationId: event.data.correlationId }, event.origin); } }); } /** * Start periodic contract discovery */ startPeriodicDiscovery() { this.discoveryInterval = window.setInterval(() => { this.discoverContracts(); }, 10000); // Every 10 seconds } /** * Stop periodic contract discovery */ stopDiscovery() { if (this.discoveryInterval) { clearInterval(this.discoveryInterval); this.discoveryInterval = undefined; } if (this.domObserver) { this.domObserver.disconnect(); this.domObserver = undefined; } } /** * Generate unique contract ID */ generateContractId(contract) { return `${contract.domain}_${contract.title.replace(/[^a-zA-Z0-9]/g, '_')}_${Date.now()}`; } /** * Check if domain matches pattern */ domainMatches(domain, pattern) { if (pattern.includes('*')) { const regex = new RegExp(pattern.replace(/\*/g, '.*')); return regex.test(domain); } return domain === pattern; } /** * Generate context hash for pattern matching */ generateContextHash() { const context = { domain: window.location.hostname, path: window.location.pathname, title: document.title, bodyClasses: Array.from(document.body.classList).sort().join(' '), elementCount: document.querySelectorAll('*').length }; return btoa(JSON.stringify(context)).slice(0, 16); } } // Global contract discovery adapter instance export const contractDiscovery = new ContractDiscoveryAdapter();