#!/usr/bin/env node /** * Norda Biznes - Company Data Collector Agent * * Automatycznie zbiera dane o firmach członkowskich z różnych źródeł: * - ALEO.com (NIP, REGON, KRS, dane urzędowe) * - Google Search (znajdowanie stron WWW) * - Strony firmowe (kontakt, usługi, kompetencje) * * Autor: Norda Biznes Partner Development * Data: 2025-11-23 */ const fs = require('fs'); const path = require('path'); // Konfiguracja const CONFIG = { dataDir: path.join(__dirname, '..', 'data'), companiesBasicPath: path.join(__dirname, '..', 'data', 'companies-basic.json'), companiesFullPath: path.join(__dirname, '..', 'data', 'companies-full.json'), dataSourcesPath: path.join(__dirname, '..', 'data', 'data-sources.md'), collectionLogPath: path.join(__dirname, '..', 'data', 'collection-log.md'), delayBetweenRequests: 2000, // 2 sekundy między zapytaniami maxRetries: 3, timeout: 30000 }; // Kolory dla konsoli const colors = { reset: '\x1b[0m', green: '\x1b[32m', yellow: '\x1b[33m', red: '\x1b[31m', blue: '\x1b[34m', cyan: '\x1b[36m' }; /** * Company Data Collector Agent */ class CompanyDataCollector { constructor() { this.stats = { total: 0, processed: 0, complete: 0, partial: 0, failed: 0, startTime: null, endTime: null }; this.log = []; } /** * Główna funkcja uruchamiająca agenta */ async run(options = {}) { console.log(colors.cyan + '\n🤖 Norda Biznes - Company Data Collector Agent' + colors.reset); console.log(colors.cyan + '================================================\n' + colors.reset); this.stats.startTime = new Date(); try { // Wczytaj firmy do zbadania const companies = this.loadCompanies(); this.stats.total = companies.length; console.log(colors.blue + `📊 Załadowano ${companies.length} firm do zbadania\n` + colors.reset); // Filtruj firmy jeśli podano opcje let companiesToProcess = companies; if (options.limit) { companiesToProcess = companies.slice(0, options.limit); console.log(colors.yellow + `⚠️ Limit: przetwarzanie ${options.limit} firm\n` + colors.reset); } if (options.skipProcessed) { const fullData = this.loadFullData(); const processedIds = new Set(fullData.companies.map(c => c.id)); companiesToProcess = companiesToProcess.filter(c => !processedIds.has(c.id)); console.log(colors.yellow + `⚠️ Pomijanie już przetworzonych: pozostało ${companiesToProcess.length} firm\n` + colors.reset); } // Przetwarzaj każdą firmę for (const company of companiesToProcess) { await this.processCompany(company); this.stats.processed++; // Opóźnienie między firmami if (this.stats.processed < companiesToProcess.length) { await this.delay(CONFIG.delayBetweenRequests); } } // Zakończ i zapisz wyniki this.stats.endTime = new Date(); this.generateReport(); this.saveLog(); this.updateDataSources(); console.log(colors.green + '\n✅ Agent zakończył pracę!\n' + colors.reset); } catch (error) { console.error(colors.red + '\n❌ Błąd agenta: ' + error.message + colors.reset); throw error; } } /** * Przetwarzanie pojedynczej firmy */ async processCompany(company) { console.log(colors.cyan + `\n[${this.stats.processed + 1}/${this.stats.total}] Przetwarzanie: ${company.name}` + colors.reset); console.log(colors.blue + '─'.repeat(60) + colors.reset); const companyData = { id: company.id, name: company.name, category: company.category, description: { short: company.description, full: null }, ...this.createEmptyCompanyStructure() }; const logEntry = { timestamp: new Date().toISOString(), company: company.name, steps: [], success: false }; try { // KROK 1: Szukaj w ALEO.com (NIP, REGON, KRS) console.log(colors.yellow + '📡 KROK 1: Wyszukiwanie w ALEO.com...' + colors.reset); const aleoData = await this.searchAleo(company.name); if (aleoData) { Object.assign(companyData, aleoData); logEntry.steps.push({ step: 'ALEO', status: 'success', data: 'NIP, REGON, KRS' }); console.log(colors.green + ` ✅ Znaleziono dane urzędowe` + colors.reset); } else { logEntry.steps.push({ step: 'ALEO', status: 'not_found' }); console.log(colors.yellow + ` ⚠️ Nie znaleziono w ALEO` + colors.reset); } // KROK 2: Znajdź stronę WWW przez Google console.log(colors.yellow + '🔍 KROK 2: Szukanie strony WWW...' + colors.reset); const websiteUrl = await this.findWebsite(company.name); if (websiteUrl) { companyData.website = websiteUrl; logEntry.steps.push({ step: 'Google', status: 'success', data: websiteUrl }); console.log(colors.green + ` ✅ Znaleziono stronę: ${websiteUrl}` + colors.reset); // KROK 3: Ekstraktuj dane ze strony WWW console.log(colors.yellow + '🌐 KROK 3: Pobieranie danych ze strony...' + colors.reset); const websiteData = await this.scrapeWebsite(websiteUrl); if (websiteData) { this.mergeWebsiteData(companyData, websiteData); logEntry.steps.push({ step: 'Website', status: 'success', data: 'contact, services' }); console.log(colors.green + ` ✅ Pobrano dane ze strony` + colors.reset); } else { logEntry.steps.push({ step: 'Website', status: 'failed' }); console.log(colors.yellow + ` ⚠️ Nie udało się pobrać danych` + colors.reset); } } else { logEntry.steps.push({ step: 'Google', status: 'not_found' }); console.log(colors.yellow + ` ⚠️ Nie znaleziono strony WWW` + colors.reset); } // Oceń jakość danych companyData.dataQuality = this.assessDataQuality(companyData); companyData.lastUpdated = new Date().toISOString().split('T')[0]; companyData.status = companyData.dataQuality === 'complete' ? 'complete' : 'partial'; // Zapisz dane firmy this.saveCompanyData(companyData); // Aktualizuj statystyki if (companyData.dataQuality === 'complete') { this.stats.complete++; console.log(colors.green + `✅ Pełne dane zebrane` + colors.reset); } else { this.stats.partial++; console.log(colors.yellow + `⚠️ Częściowe dane (${companyData.dataQuality})` + colors.reset); } logEntry.success = true; } catch (error) { this.stats.failed++; logEntry.error = error.message; console.error(colors.red + `❌ Błąd: ${error.message}` + colors.reset); } this.log.push(logEntry); } /** * Wyszukiwanie firmy w ALEO.com * UWAGA: To jest symulacja - wymaga rzeczywistego web scrapingu lub API */ async searchAleo(companyName) { // TODO: Implementacja rzeczywistego wyszukiwania w ALEO.com // Możliwe podejścia: // 1. Web scraping z użyciem puppeteer/playwright // 2. API jeśli dostępne // 3. Ręczne wyszukiwanie i import danych console.log(colors.yellow + ` ℹ️ ALEO search: "${companyName}" (symulacja)` + colors.reset); // Symulacja - zwróć null (nie znaleziono) // W rzeczywistej implementacji tutaj byłby kod scrapingu return null; } /** * Znajdowanie strony WWW firmy przez Google * UWAGA: To jest symulacja - wymaga Google Custom Search API lub scraping */ async findWebsite(companyName) { // TODO: Implementacja Google Custom Search API lub scraping // https://developers.google.com/custom-search/v1/overview console.log(colors.yellow + ` ℹ️ Google search: "${companyName} Wejherowo" (symulacja)` + colors.reset); // Symulacja - zwróć null return null; } /** * Scraping danych ze strony WWW firmy * UWAGA: To jest symulacja - wymaga puppeteer/playwright lub cheerio */ async scrapeWebsite(url) { // TODO: Implementacja web scrapingu // Możliwe narzędzia: // 1. puppeteer - dla stron z JavaScriptem // 2. cheerio - dla statycznych stron HTML // 3. playwright - bardziej zaawansowane console.log(colors.yellow + ` ℹ️ Scraping: ${url} (symulacja)` + colors.reset); // Symulacja - zwróć null return null; } /** * Merge danych ze strony WWW do obiektu firmy */ mergeWebsiteData(companyData, websiteData) { if (websiteData.email) companyData.email = websiteData.email; if (websiteData.phone) companyData.phone = websiteData.phone; if (websiteData.address) companyData.address = websiteData.address; if (websiteData.services) companyData.services = websiteData.services; if (websiteData.competencies) companyData.competencies = websiteData.competencies; if (websiteData.description) companyData.description.full = websiteData.description; if (websiteData.social) companyData.social = { ...companyData.social, ...websiteData.social }; } /** * Oceń jakość zebranych danych */ assessDataQuality(company) { const requiredFields = ['nip', 'website', 'email', 'phone']; const highPriorityFields = ['regon', 'krs', 'address', 'services']; const hasRequired = requiredFields.every(field => company[field]); const hasHighPriority = highPriorityFields.filter(field => { if (field === 'address') return company.address && company.address.full; if (field === 'services') return company.services && company.services.length > 0; return company[field]; }).length; if (hasRequired && hasHighPriority >= 3) return 'complete'; if (hasRequired || hasHighPriority >= 2) return 'partial'; return 'basic'; } /** * Utwórz pustą strukturę danych firmy */ createEmptyCompanyStructure() { return { legalName: null, nip: null, regon: null, krs: null, website: null, email: null, phone: null, address: { street: null, city: null, postal: null, full: null }, services: [], competencies: [], social: { facebook: null, linkedin: null, instagram: null }, financialData: {}, yearEstablished: null, certifications: [], awards: [], status: 'pending', dataQuality: 'basic', lastUpdated: null, sources: [] }; } /** * Wczytaj listę firm z companies-basic.json */ loadCompanies() { try { const data = fs.readFileSync(CONFIG.companiesBasicPath, 'utf8'); const json = JSON.parse(data); return json.companies; } catch (error) { console.error(colors.red + '❌ Nie można wczytać companies-basic.json: ' + error.message + colors.reset); throw error; } } /** * Wczytaj pełne dane firm */ loadFullData() { try { const data = fs.readFileSync(CONFIG.companiesFullPath, 'utf8'); return JSON.parse(data); } catch (error) { // Jeśli plik nie istnieje, utwórz nową strukturę return { meta: { total: 0, researched: 0, complete: 0, partial: 0, pending: 0, lastUpdated: new Date().toISOString().split('T')[0], version: '2.0' }, companies: [] }; } } /** * Zapisz dane firmy do companies-full.json */ saveCompanyData(companyData) { try { const fullData = this.loadFullData(); // Sprawdź czy firma już istnieje const existingIndex = fullData.companies.findIndex(c => c.id === companyData.id); if (existingIndex >= 0) { fullData.companies[existingIndex] = companyData; } else { fullData.companies.push(companyData); } // Aktualizuj metadane fullData.meta.total = fullData.companies.length; fullData.meta.researched = fullData.companies.length; fullData.meta.complete = fullData.companies.filter(c => c.dataQuality === 'complete').length; fullData.meta.partial = fullData.companies.filter(c => c.dataQuality === 'partial').length; fullData.meta.pending = this.stats.total - fullData.meta.researched; fullData.meta.lastUpdated = new Date().toISOString().split('T')[0]; // Zapisz do pliku fs.writeFileSync(CONFIG.companiesFullPath, JSON.stringify(fullData, null, 2)); } catch (error) { console.error(colors.red + '❌ Błąd zapisu danych: ' + error.message + colors.reset); } } /** * Wygeneruj raport końcowy */ generateReport() { const duration = (this.stats.endTime - this.stats.startTime) / 1000 / 60; // minuty console.log(colors.cyan + '\n\n' + '='.repeat(60) + colors.reset); console.log(colors.cyan + '📊 RAPORT KOŃCOWY' + colors.reset); console.log(colors.cyan + '='.repeat(60) + '\n' + colors.reset); console.log(colors.blue + `Przetworzono: ${this.stats.processed}/${this.stats.total} firm` + colors.reset); console.log(colors.green + `Pełne dane: ${this.stats.complete} (${Math.round(this.stats.complete/this.stats.processed*100)}%)` + colors.reset); console.log(colors.yellow + `Częściowe dane: ${this.stats.partial} (${Math.round(this.stats.partial/this.stats.processed*100)}%)` + colors.reset); console.log(colors.red + `Niepowodzenia: ${this.stats.failed} (${Math.round(this.stats.failed/this.stats.processed*100)}%)` + colors.reset); console.log(colors.blue + `\nCzas działania: ${duration.toFixed(2)} minut` + colors.reset); console.log(colors.blue + `Średnio na firmę: ${(duration/this.stats.processed).toFixed(2)} min` + colors.reset); console.log(colors.cyan + '\n' + '='.repeat(60) + '\n' + colors.reset); } /** * Zapisz log działania agenta */ saveLog() { try { const logContent = this.generateLogContent(); fs.writeFileSync(CONFIG.collectionLogPath, logContent); console.log(colors.green + `✅ Log zapisany: ${CONFIG.collectionLogPath}` + colors.reset); } catch (error) { console.error(colors.red + '❌ Błąd zapisu logu: ' + error.message + colors.reset); } } /** * Wygeneruj zawartość logu */ generateLogContent() { let content = `# Collection Log - Norda Biznes Companies\n\n`; content += `**Data:** ${new Date().toISOString()}\n`; content += `**Przetworzono:** ${this.stats.processed}/${this.stats.total} firm\n`; content += `**Pełne dane:** ${this.stats.complete}\n`; content += `**Częściowe dane:** ${this.stats.partial}\n`; content += `**Niepowodzenia:** ${this.stats.failed}\n\n`; content += `---\n\n`; content += `## Szczegóły przetwarzania\n\n`; for (const entry of this.log) { content += `### ${entry.company}\n`; content += `- **Czas:** ${entry.timestamp}\n`; content += `- **Sukces:** ${entry.success ? '✅' : '❌'}\n`; content += `- **Kroki:**\n`; for (const step of entry.steps) { const status = step.status === 'success' ? '✅' : step.status === 'not_found' ? '⚠️' : '❌'; content += ` - ${status} ${step.step}: ${step.data || step.status}\n`; } if (entry.error) { content += `- **Błąd:** ${entry.error}\n`; } content += `\n`; } return content; } /** * Aktualizuj data-sources.md */ updateDataSources() { // TODO: Implementacja aktualizacji data-sources.md z nowymi statystykami console.log(colors.yellow + '⚠️ data-sources.md: aktualizacja ręczna wymagana' + colors.reset); } /** * Opóźnienie (delay) */ delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } } // CLI Interface if (require.main === module) { const args = process.argv.slice(2); const options = { limit: null, skipProcessed: false }; // Parse argumentów for (let i = 0; i < args.length; i++) { if (args[i] === '--limit' && args[i + 1]) { options.limit = parseInt(args[i + 1]); i++; } else if (args[i] === '--skip-processed') { options.skipProcessed = true; } else if (args[i] === '--help') { console.log(` Norda Biznes - Company Data Collector Agent Usage: node company-data-collector.js [options] Options: --limit N Przetwórz tylko pierwsze N firm --skip-processed Pomiń firmy już przetworzone --help Pokaż tę pomoc Examples: node company-data-collector.js node company-data-collector.js --limit 5 node company-data-collector.js --skip-processed --limit 10 `); process.exit(0); } } // Uruchom agenta const agent = new CompanyDataCollector(); agent.run(options).catch(error => { console.error('Fatal error:', error); process.exit(1); }); } module.exports = CompanyDataCollector;