UNPKG

@semantest/chrome-extension

Version:

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

760 lines (759 loc) โ€ข 28.9 kB
/** * Pattern Manager - Advanced Pattern Sharing and Management * * Provides comprehensive pattern management including export/import, * sharing, validation, and collaboration features. */ import { globalMessageStore } from './message-store'; export class PatternManager { constructor() { this.patterns = new Map(); this.shares = new Map(); this.categories = new Set(['web-automation', 'data-extraction', 'testing', 'workflow', 'utility']); this.STORAGE_KEY = 'chatgpt-buddy-patterns'; this.SHARES_KEY = 'chatgpt-buddy-pattern-shares'; this.MAX_PATTERN_SIZE = 1024 * 1024; // 1MB per pattern this.EXPORT_VERSION = '1.0.0'; this.loadPatterns(); this.loadShares(); this.startPatternCleanup(); } /** * Create a new automation pattern */ async createPattern(name, description, steps, options = {}) { const pattern = { id: this.generatePatternId(), name, description, version: '1.0.0', author: options.author || 'Anonymous', created: new Date().toISOString(), lastModified: new Date().toISOString(), tags: options.tags || [], category: options.category || 'web-automation', steps, conditions: options.conditions || [], variables: options.variables || [], metadata: { compatibility: ['chrome', 'firefox', 'edge'], requirements: [], permissions: [], language: 'en', difficulty: 'beginner', estimatedDuration: this.estimatePatternDuration(steps), successRate: 1.0, popularity: 0, domains: [], ...options.metadata }, statistics: { executions: 0, successes: 0, failures: 0, averageExecutionTime: 0, errorTypes: {}, performanceMetrics: { fastestExecution: 0, slowestExecution: 0, memoryUsage: [] }, ...options.statistics } }; // Validate pattern const validation = this.validatePattern(pattern); if (!validation.isValid) { throw new Error(`Invalid pattern: ${validation.errors.join(', ')}`); } this.patterns.set(pattern.id, pattern); await this.savePatterns(); // Track pattern creation globalMessageStore.addInboundMessage('PATTERN_CREATED', { patternId: pattern.id, name, category: pattern.category }, `pattern-created-${Date.now()}`, { extensionId: chrome.runtime.id, userAgent: navigator.userAgent }); console.log(`๐Ÿ“ Created pattern: ${name} (${pattern.id})`); return pattern; } /** * Get a pattern by ID */ getPattern(id) { return this.patterns.get(id) || null; } /** * Get all patterns with optional filtering */ getPatterns(filter) { let patterns = Array.from(this.patterns.values()); if (filter) { if (filter.category) { patterns = patterns.filter(p => p.category === filter.category); } if (filter.tags && filter.tags.length > 0) { patterns = patterns.filter(p => filter.tags.some(tag => p.tags.includes(tag))); } if (filter.author) { patterns = patterns.filter(p => p.author === filter.author); } if (filter.difficulty) { patterns = patterns.filter(p => p.metadata.difficulty === filter.difficulty); } if (filter.searchText) { const searchLower = filter.searchText.toLowerCase(); patterns = patterns.filter(p => p.name.toLowerCase().includes(searchLower) || p.description.toLowerCase().includes(searchLower) || p.tags.some(tag => tag.toLowerCase().includes(searchLower))); } } return patterns.sort((a, b) => new Date(b.lastModified).getTime() - new Date(a.lastModified).getTime()); } /** * Update an existing pattern */ async updatePattern(id, updates) { const pattern = this.patterns.get(id); if (!pattern) { throw new Error(`Pattern not found: ${id}`); } const updatedPattern = { ...pattern, ...updates, id, // Ensure ID doesn't change lastModified: new Date().toISOString(), version: this.incrementVersion(pattern.version) }; // Validate updated pattern const validation = this.validatePattern(updatedPattern); if (!validation.isValid) { throw new Error(`Invalid pattern update: ${validation.errors.join(', ')}`); } this.patterns.set(id, updatedPattern); await this.savePatterns(); // Track pattern update globalMessageStore.addInboundMessage('PATTERN_UPDATED', { patternId: id, version: updatedPattern.version }, `pattern-updated-${Date.now()}`, { extensionId: chrome.runtime.id, userAgent: navigator.userAgent }); console.log(`๐Ÿ“ Updated pattern: ${updatedPattern.name} (${id})`); return updatedPattern; } /** * Delete a pattern */ async deletePattern(id) { const pattern = this.patterns.get(id); if (!pattern) { return false; } this.patterns.delete(id); // Remove related shares Array.from(this.shares.values()) .filter(share => share.patternId === id) .forEach(share => this.shares.delete(share.shareId)); await this.savePatterns(); await this.saveShares(); // Track pattern deletion globalMessageStore.addInboundMessage('PATTERN_DELETED', { patternId: id, name: pattern.name }, `pattern-deleted-${Date.now()}`, { extensionId: chrome.runtime.id, userAgent: navigator.userAgent }); console.log(`๐Ÿ—‘๏ธ Deleted pattern: ${pattern.name} (${id})`); return true; } /** * Export patterns to various formats */ async exportPatterns(patternIds, format = 'json', options = {}) { const patterns = patternIds .map(id => this.patterns.get(id)) .filter(pattern => pattern !== undefined); if (patterns.length === 0) { throw new Error('No valid patterns found for export'); } // Prepare export data const exportData = { version: this.EXPORT_VERSION, exportedAt: new Date().toISOString(), exportedBy: 'ChatGPT-buddy Extension', format, compression: options.compression || false, encryption: options.encryption || false, patterns: options.includeStatistics ? patterns : patterns.map(p => ({ ...p, statistics: { executions: 0, successes: 0, failures: 0, averageExecutionTime: 0, errorTypes: {}, performanceMetrics: { fastestExecution: 0, slowestExecution: 0, memoryUsage: [] } } })), dependencies: options.includeDependencies ? this.extractDependencies(patterns) : [], checksum: '' }; // Generate checksum exportData.checksum = await this.generateChecksum(JSON.stringify(exportData.patterns)); // Convert to requested format let serialized = ''; switch (format) { case 'json': serialized = JSON.stringify(exportData, null, 2); break; case 'yaml': serialized = this.convertToYaml(exportData); break; case 'xml': serialized = this.convertToXml(exportData); break; default: throw new Error(`Unsupported export format: ${format}`); } // Apply compression if requested if (options.compression) { serialized = await this.compressData(serialized); } // Apply encryption if requested if (options.encryption) { serialized = await this.encryptData(serialized); } // Track export globalMessageStore.addInboundMessage('PATTERNS_EXPORTED', { count: patterns.length, format, size: serialized.length, compression: options.compression, encryption: options.encryption }, `patterns-exported-${Date.now()}`, { extensionId: chrome.runtime.id, userAgent: navigator.userAgent }); console.log(`๐Ÿ“ค Exported ${patterns.length} patterns as ${format.toUpperCase()}`); return serialized; } /** * Import patterns from various formats */ async importPatterns(data, options = {}) { const result = { imported: [], skipped: [], errors: [] }; try { // Decrypt if needed let processedData = data; if (this.isEncrypted(data)) { processedData = await this.decryptData(data); } // Decompress if needed if (this.isCompressed(processedData)) { processedData = await this.decompressData(processedData); } // Parse data let exportData; try { if (processedData.trim().startsWith('<')) { exportData = this.parseXml(processedData); } else if (processedData.includes('version:') && !processedData.trim().startsWith('{')) { exportData = this.parseYaml(processedData); } else { exportData = JSON.parse(processedData); } } catch (parseError) { result.errors.push(`Failed to parse import data: ${parseError}`); return result; } // Validate checksum if (exportData.checksum) { const calculatedChecksum = await this.generateChecksum(JSON.stringify(exportData.patterns)); if (calculatedChecksum !== exportData.checksum) { result.errors.push('Checksum validation failed - data may be corrupted'); if (!options.validateOnly) { return result; } } } // Process each pattern for (const pattern of exportData.patterns) { try { // Validate pattern const validation = this.validatePattern(pattern); if (!validation.isValid) { result.errors.push(`Invalid pattern ${pattern.name}: ${validation.errors.join(', ')}`); continue; } // Check for existing pattern const existingPattern = this.patterns.get(pattern.id); if (existingPattern) { if (!options.overwrite && !options.mergeDuplicates) { result.skipped.push(`Pattern already exists: ${pattern.name} (${pattern.id})`); continue; } if (options.mergeDuplicates) { // Merge patterns const mergedPattern = this.mergePatterns(existingPattern, pattern); if (!options.validateOnly) { this.patterns.set(pattern.id, mergedPattern); } result.imported.push(mergedPattern); } else if (options.overwrite) { // Overwrite existing if (!options.validateOnly) { this.patterns.set(pattern.id, pattern); } result.imported.push(pattern); } } else { // New pattern if (!options.validateOnly) { this.patterns.set(pattern.id, pattern); } result.imported.push(pattern); } } catch (error) { result.errors.push(`Error processing pattern ${pattern.name}: ${error}`); } } // Save patterns if not validation-only if (!options.validateOnly && result.imported.length > 0) { await this.savePatterns(); } // Track import globalMessageStore.addInboundMessage('PATTERNS_IMPORTED', { imported: result.imported.length, skipped: result.skipped.length, errors: result.errors.length, validateOnly: options.validateOnly }, `patterns-imported-${Date.now()}`, { extensionId: chrome.runtime.id, userAgent: navigator.userAgent }); console.log(`๐Ÿ“ฅ Import completed: ${result.imported.length} imported, ${result.skipped.length} skipped, ${result.errors.length} errors`); } catch (error) { result.errors.push(`Import failed: ${error}`); } return result; } /** * Create a shareable link for patterns */ async sharePatterns(patternIds, shareType = 'private', options = {}) { if (patternIds.length === 0) { throw new Error('No patterns specified for sharing'); } // Validate patterns exist const invalidPatterns = patternIds.filter(id => !this.patterns.has(id)); if (invalidPatterns.length > 0) { throw new Error(`Invalid pattern IDs: ${invalidPatterns.join(', ')}`); } const shareId = this.generateShareId(); const share = { patternId: patternIds[0], // For now, support single pattern sharing shareId, shareType, expiresAt: options.expiresIn ? new Date(Date.now() + options.expiresIn).toISOString() : undefined, permissions: options.permissions || [], accessCount: 0, downloadCount: 0, created: new Date().toISOString(), createdBy: 'current-user' // TODO: Implement user management }; this.shares.set(shareId, share); await this.saveShares(); // Track sharing globalMessageStore.addInboundMessage('PATTERNS_SHARED', { shareId, patternCount: patternIds.length, shareType }, `patterns-shared-${Date.now()}`, { extensionId: chrome.runtime.id, userAgent: navigator.userAgent }); console.log(`๐Ÿ”— Created share: ${shareId} for ${patternIds.length} pattern(s)`); return share; } /** * Access shared patterns */ async accessSharedPatterns(shareId) { const share = this.shares.get(shareId); if (!share) { throw new Error(`Share not found: ${shareId}`); } // Check expiration if (share.expiresAt && new Date() > new Date(share.expiresAt)) { this.shares.delete(shareId); await this.saveShares(); throw new Error('Share has expired'); } // Update access count share.accessCount++; this.shares.set(shareId, share); await this.saveShares(); // Get pattern(s) const pattern = this.patterns.get(share.patternId); if (!pattern) { throw new Error('Shared pattern no longer exists'); } // Track access globalMessageStore.addInboundMessage('SHARED_PATTERNS_ACCESSED', { shareId, patternId: share.patternId }, `shared-patterns-accessed-${Date.now()}`, { extensionId: chrome.runtime.id, userAgent: navigator.userAgent }); return [pattern]; } /** * Get pattern statistics and analytics */ getPatternAnalytics() { const patterns = Array.from(this.patterns.values()); const analytics = { totalPatterns: patterns.length, categoriesBreakdown: {}, difficultiesBreakdown: {}, averageSuccessRate: 0, totalExecutions: 0, mostPopular: [], recentlyCreated: [] }; if (patterns.length === 0) { return analytics; } // Calculate breakdowns patterns.forEach(pattern => { analytics.categoriesBreakdown[pattern.category] = (analytics.categoriesBreakdown[pattern.category] || 0) + 1; analytics.difficultiesBreakdown[pattern.metadata.difficulty] = (analytics.difficultiesBreakdown[pattern.metadata.difficulty] || 0) + 1; analytics.totalExecutions += pattern.statistics.executions; }); // Calculate average success rate const successRates = patterns .filter(p => p.statistics.executions > 0) .map(p => p.statistics.successes / p.statistics.executions); analytics.averageSuccessRate = successRates.length > 0 ? successRates.reduce((sum, rate) => sum + rate, 0) / successRates.length : 0; // Get most popular patterns analytics.mostPopular = patterns .filter(p => p.statistics.executions > 0) .sort((a, b) => b.metadata.popularity - a.metadata.popularity) .slice(0, 5); // Get recently created patterns analytics.recentlyCreated = patterns .sort((a, b) => new Date(b.created).getTime() - new Date(a.created).getTime()) .slice(0, 5); return analytics; } /** * Validate a pattern for correctness and completeness */ validatePattern(pattern) { const errors = []; const warnings = []; // Basic validation if (!pattern.id || pattern.id.trim() === '') { errors.push('Pattern ID is required'); } if (!pattern.name || pattern.name.trim() === '') { errors.push('Pattern name is required'); } if (!pattern.steps || pattern.steps.length === 0) { errors.push('Pattern must have at least one step'); } // Validate steps pattern.steps?.forEach((step, index) => { if (!step.id) { errors.push(`Step ${index + 1}: ID is required`); } if (!step.type) { errors.push(`Step ${index + 1}: Type is required`); } if (!step.selector && ['click', 'type', 'extract'].includes(step.type)) { errors.push(`Step ${index + 1}: Selector is required for ${step.type} steps`); } if (step.type === 'type' && !step.value) { warnings.push(`Step ${index + 1}: Type step has no value specified`); } if (step.timeout < 0) { errors.push(`Step ${index + 1}: Timeout cannot be negative`); } }); // Validate conditions pattern.conditions?.forEach((condition, index) => { if (!condition.id) { errors.push(`Condition ${index + 1}: ID is required`); } if (!condition.type) { errors.push(`Condition ${index + 1}: Type is required`); } if (condition.action === 'goto_step' && !condition.targetStep) { errors.push(`Condition ${index + 1}: Target step is required for goto_step action`); } }); // Validate variables pattern.variables?.forEach((variable, index) => { if (!variable.id) { errors.push(`Variable ${index + 1}: ID is required`); } if (!variable.name) { errors.push(`Variable ${index + 1}: Name is required`); } if (!variable.type) { errors.push(`Variable ${index + 1}: Type is required`); } }); // Check pattern size const patternSize = JSON.stringify(pattern).length; if (patternSize > this.MAX_PATTERN_SIZE) { errors.push(`Pattern size (${patternSize} bytes) exceeds maximum (${this.MAX_PATTERN_SIZE} bytes)`); } return { isValid: errors.length === 0, errors, warnings }; } /** * Generate unique pattern ID */ generatePatternId() { return `pattern-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; } /** * Generate unique share ID */ generateShareId() { return `share-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; } /** * Estimate pattern execution duration based on steps */ estimatePatternDuration(steps) { const baseTimes = { click: 1000, // 1 second type: 2000, // 2 seconds wait: 3000, // 3 seconds navigate: 5000, // 5 seconds extract: 1500, // 1.5 seconds condition: 500, // 0.5 seconds loop: 1000 // 1 second base }; return steps.reduce((total, step) => { const baseTime = baseTimes[step.type] || 1000; const timeout = step.timeout || 0; return total + Math.max(baseTime, timeout); }, 0); } /** * Increment semantic version */ incrementVersion(version) { const parts = version.split('.').map(Number); parts[2] = (parts[2] || 0) + 1; return parts.join('.'); } /** * Extract dependencies from patterns */ extractDependencies(patterns) { const dependencies = new Set(); patterns.forEach(pattern => { pattern.metadata.requirements.forEach(req => dependencies.add(req)); pattern.metadata.permissions.forEach(perm => dependencies.add(perm)); }); return Array.from(dependencies); } /** * Generate SHA-256 checksum */ async generateChecksum(data) { const encoder = new TextEncoder(); const buffer = encoder.encode(data); const hashBuffer = await crypto.subtle.digest('SHA-256', buffer); const hashArray = Array.from(new Uint8Array(hashBuffer)); return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); } /** * Convert to YAML format (simplified) */ convertToYaml(data) { // Simplified YAML conversion - in production, use a proper YAML library return `version: ${data.version}\nexportedAt: ${data.exportedAt}\npatterns:\n${JSON.stringify(data.patterns, null, 2)}`; } /** * Convert to XML format (simplified) */ convertToXml(data) { // Simplified XML conversion - in production, use a proper XML library return `<?xml version="1.0" encoding="UTF-8"?>\n<export version="${data.version}" exportedAt="${data.exportedAt}">\n<patterns>${JSON.stringify(data.patterns)}</patterns>\n</export>`; } /** * Parse YAML data (simplified) */ parseYaml(data) { // Simplified YAML parsing - in production, use a proper YAML library throw new Error('YAML parsing not implemented'); } /** * Parse XML data (simplified) */ parseXml(data) { // Simplified XML parsing - in production, use a proper XML library throw new Error('XML parsing not implemented'); } /** * Data compression (simplified) */ async compressData(data) { // In production, use proper compression library like pako return btoa(data); } /** * Data decompression (simplified) */ async decompressData(data) { try { return atob(data); } catch { throw new Error('Failed to decompress data'); } } /** * Data encryption (simplified) */ async encryptData(data) { // In production, use proper encryption return btoa(data); } /** * Data decryption (simplified) */ async decryptData(data) { try { return atob(data); } catch { throw new Error('Failed to decrypt data'); } } /** * Check if data is encrypted */ isEncrypted(data) { // Simple heuristic - in production, use proper markers return data.startsWith('encrypted:'); } /** * Check if data is compressed */ isCompressed(data) { // Simple heuristic - in production, use proper markers return data.startsWith('compressed:'); } /** * Merge two patterns (for duplicate handling) */ mergePatterns(existing, incoming) { return { ...existing, ...incoming, id: existing.id, // Keep original ID created: existing.created, // Keep original creation date lastModified: new Date().toISOString(), version: this.incrementVersion(existing.version), statistics: { ...existing.statistics, // Don't merge statistics from import } }; } /** * Load patterns from storage */ async loadPatterns() { try { const result = await chrome.storage.local.get(this.STORAGE_KEY); const stored = result[this.STORAGE_KEY]; if (stored && stored.patterns) { stored.patterns.forEach((pattern) => { this.patterns.set(pattern.id, pattern); }); console.log(`๐Ÿ“š Loaded ${this.patterns.size} patterns from storage`); } } catch (error) { console.error('โŒ Failed to load patterns:', error); } } /** * Save patterns to storage */ async savePatterns() { try { const patterns = Array.from(this.patterns.values()); await chrome.storage.local.set({ [this.STORAGE_KEY]: { patterns, lastSaved: new Date().toISOString(), version: this.EXPORT_VERSION } }); } catch (error) { console.error('โŒ Failed to save patterns:', error); } } /** * Load shares from storage */ async loadShares() { try { const result = await chrome.storage.local.get(this.SHARES_KEY); const stored = result[this.SHARES_KEY]; if (stored && stored.shares) { stored.shares.forEach((share) => { this.shares.set(share.shareId, share); }); console.log(`๐Ÿ”— Loaded ${this.shares.size} shares from storage`); } } catch (error) { console.error('โŒ Failed to load shares:', error); } } /** * Save shares to storage */ async saveShares() { try { const shares = Array.from(this.shares.values()); await chrome.storage.local.set({ [this.SHARES_KEY]: { shares, lastSaved: new Date().toISOString() } }); } catch (error) { console.error('โŒ Failed to save shares:', error); } } /** * Start pattern cleanup (remove expired shares, etc.) */ startPatternCleanup() { setInterval(() => { this.cleanupExpiredShares(); }, 60 * 60 * 1000); // Run every hour } /** * Clean up expired shares */ async cleanupExpiredShares() { const now = new Date(); let removedCount = 0; for (const [shareId, share] of this.shares.entries()) { if (share.expiresAt && now > new Date(share.expiresAt)) { this.shares.delete(shareId); removedCount++; } } if (removedCount > 0) { await this.saveShares(); console.log(`๐Ÿงน Cleaned up ${removedCount} expired shares`); } } } // Global pattern manager instance export const globalPatternManager = new PatternManager();