@semantest/chrome-extension
Version:
Browser extension for ChatGPT-buddy - AI automation extension built on Web-Buddy framework
760 lines (759 loc) โข 28.9 kB
JavaScript
/**
* 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();