vantuz 3.2.7 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,220 @@
1
+ // core/eia-monitor.js
2
+ import { getScheduler } from './scheduler.js';
3
+ import { TrendyolScraper } from './scrapers/TrendyolScraper.js'; // Example scraper
4
+ import { getEIABrain } from './eia-brain.js';
5
+ import { getDatabase } from './database.js'; // Import database
6
+ import { getVectorDB } from './vector-db.js'; // Import vector DB
7
+ import { log } from './ai-provider.js';
8
+
9
+ class EIAMonitor {
10
+ constructor(config, env) {
11
+ this.scheduler = getScheduler();
12
+ this.eiaBrain = getEIABrain(config, env);
13
+ this.db = getDatabase(); // Initialize database
14
+ this.vectorDb = getVectorDB(); // Initialize vector DB
15
+ this.config = config;
16
+ this.env = env;
17
+ this.productUrls = this._parseUrls(this.env.EIA_PRODUCT_URLS);
18
+ this.competitorUrls = this._parseUrls(this.env.EIA_COMPETITOR_URLS);
19
+ this.targetProfitMargin = parseFloat(this.env.EIA_TARGET_PROFIT_MARGIN || '15'); // Default 15%
20
+ this.scrapers = {
21
+ trendyol: new TrendyolScraper() // Initialize other scrapers here
22
+ };
23
+ log('INFO', 'EIA Monitor initialized');
24
+ }
25
+
26
+ _parseUrls(urlsString) {
27
+ return urlsString ? urlsString.split(',').map(url => url.trim()).filter(Boolean) : [];
28
+ }
29
+
30
+ async initMonitoringTasks() {
31
+ // Ensure DB is initialized before registering tasks
32
+ await this.db.init();
33
+
34
+ const cronSchedule = '*/30 * * * *'; // Every 30 minutes, for example
35
+
36
+ // Monitor product URLs
37
+ for (const productUrl of this.productUrls) {
38
+ // Assuming a way to extract productId and platform from URL
39
+ const { productId, platform } = this._extractProductInfoFromUrl(productUrl);
40
+ if (productId && platform) {
41
+ // Register monitoring for various data types
42
+ this.startMonitoring(productId, platform, productUrl, 'price_changes', cronSchedule);
43
+ this.startMonitoring(productId, platform, productUrl, 'buybox_status', cronSchedule);
44
+ this.startMonitoring(productId, platform, productUrl, 'stock_movements', cronSchedule);
45
+ this.startMonitoring(productId, platform, productUrl, 'product_reviews', cronSchedule);
46
+ } else {
47
+ log('WARN', `Could not extract product info from URL: ${productUrl}`);
48
+ }
49
+ }
50
+
51
+ // Monitor competitor URLs (for price changes and buybox status)
52
+ for (const competitorUrl of this.competitorUrls) {
53
+ const { productId, platform } = this._extractProductInfoFromUrl(competitorUrl);
54
+ if (productId && platform) {
55
+ this.startMonitoring(`comp-${productId}`, platform, competitorUrl, 'price_changes', cronSchedule);
56
+ this.startMonitoring(`comp-${productId}`, platform, competitorUrl, 'buybox_status', cronSchedule);
57
+ } else {
58
+ log('WARN', `Could not extract product info from competitor URL: ${competitorUrl}`);
59
+ }
60
+ }
61
+
62
+ // Load company rules into vector DB (placeholder)
63
+ await this._loadCompanyRules();
64
+ }
65
+
66
+ _extractProductInfoFromUrl(url) {
67
+ // Placeholder: Implement logic to extract product ID and platform from various marketplace URLs
68
+ // This will be crucial for mapping URLs to specific scraper methods and DB entries
69
+ // For Trendyol example: trendyol.com/s/product-id -> platform: 'trendyol', productId: 'product-id'
70
+ if (url.includes('trendyol.com')) {
71
+ const match = url.match(/\/s\/(.*?)(?:\?|$)/);
72
+ return { platform: 'trendyol', productId: match ? match[1] : url };
73
+ }
74
+ // Add logic for Hepsiburada, Amazon, etc.
75
+ return { platform: 'unknown', productId: url };
76
+ }
77
+
78
+ async _loadCompanyRules() {
79
+ // Placeholder: Load company rules from a file or config and embed them into the vector DB
80
+ const rules = [
81
+ 'Asla %10 marjın altına düşme.',
82
+ 'Müşteri şikayetlerine 24 saat içinde yanıt ver.',
83
+ 'Rakip fiyatı %5 düşürürse kendi fiyatını %2 düşür.'
84
+ ];
85
+ for (const rule of rules) {
86
+ // This would require an embedding model to convert text to vector
87
+ // For now, use a dummy vector
88
+ const dummyVector = Array.from({ length: 1536 }, () => Math.random()); // Example: OpenAI embedding size
89
+ await this.vectorDb.add('company_rules', dummyVector, { type: 'rule', text: rule });
90
+ }
91
+ log('INFO', `${rules.length} company rules loaded into vector DB.`);
92
+ }
93
+
94
+ /**
95
+ * Start monitoring a product for a specific data type.
96
+ * @param {string} productId Unique identifier for the product.
97
+ * @param {string} platform Platform name (e.g., 'trendyol').
98
+ * @param {string} productUrl URL of the product page.
99
+ * @param {string} dataType Type of data to monitor (e.g., 'price_changes', 'buybox_status').
100
+ * @param {string} cronSchedule Cron schedule for monitoring (e.g., '0 * * * *').
101
+ */
102
+ async startMonitoring(productId, platform, productUrl, dataType, cronSchedule) {
103
+ const jobName = `monitor-${platform}-${productId}-${dataType}`;
104
+ const scraper = this.scrapers[platform];
105
+
106
+ if (!scraper) {
107
+ log('ERROR', `No scraper found for platform: ${platform}`);
108
+ return;
109
+ }
110
+
111
+ const task = async () => {
112
+ log('INFO', `Monitoring ${dataType} for product ${productId} on ${platform}`);
113
+ let rawData;
114
+ try {
115
+ // Scrape Data
116
+ switch (dataType) {
117
+ case 'price_changes':
118
+ rawData = await scraper.getPriceChanges(productUrl);
119
+ break;
120
+ case 'buybox_status':
121
+ rawData = await scraper.getBuyboxStatus(productUrl);
122
+ break;
123
+ case 'stock_movements':
124
+ rawData = await scraper.getStockMovements(productUrl);
125
+ break;
126
+ case 'product_reviews':
127
+ rawData = await scraper.getProductReviewsAndRatings(productUrl);
128
+ break;
129
+ default:
130
+ log('WARN', `Unknown data type for monitoring: ${dataType}`);
131
+ return;
132
+ }
133
+
134
+ // Process Raw Data & Compare with Baseline (Placeholder)
135
+ const insights = await this.eiaBrain.processRawData(rawData, dataType); // Use EIA Brain for processing
136
+ const anomaly = this._detectAnomaly(productId, dataType, insights); // Implement anomaly detection
137
+
138
+ if (anomaly) {
139
+ log('WARN', `Anomaly detected for ${dataType} on product ${productId}: ${JSON.stringify(anomaly)}`);
140
+ await this._alertUserViaLLM(productId, platform, dataType, anomaly, insights);
141
+ } else {
142
+ log('INFO', `No anomaly detected for ${dataType} on product ${productId}.`);
143
+ }
144
+
145
+ } catch (error) {
146
+ log('ERROR', `Error during monitoring job ${jobName}: ${error.message}`, { error });
147
+ }
148
+ };
149
+
150
+ this.scheduler.addJob(jobName, cronSchedule, task);
151
+ log('INFO', `Started monitoring job "${jobName}" for ${productUrl} every ${cronSchedule}`);
152
+ }
153
+
154
+ /**
155
+ * Placeholder for anomaly detection logic.
156
+ * This will involve comparing current data with historical baseline data.
157
+ */
158
+ _detectAnomaly(productId, dataType, currentInsights) {
159
+ // For demonstration, let's say any new data is an "anomaly" if no baseline exists
160
+ // In a real system, this would be a sophisticated comparison
161
+ if (!this.baselineData[dataType]?.[productId]) {
162
+ this.baselineData[dataType] = this.baselineData[dataType] || {};
163
+ this.baselineData[dataType][productId] = currentInsights; // Store current as baseline
164
+ return null; // No anomaly, first data point
165
+ }
166
+
167
+ // Simple example: if a price changes from baseline
168
+ if (dataType === 'price_changes' && currentInsights.currentPrice !== this.baselineData.prices[productId].currentPrice) {
169
+ return {
170
+ type: 'price_change',
171
+ oldPrice: this.baselineData.prices[productId].currentPrice,
172
+ newPrice: currentInsights.currentPrice
173
+ };
174
+ }
175
+ return null;
176
+ }
177
+
178
+ /**
179
+ * Placeholder for alerting the user via LLM.
180
+ */
181
+ async _alertUserViaLLM(productId, platform, dataType, anomaly, insights) {
182
+ let currentSituationDescription = `🚨 Anomali Tespiti!
183
+ Ürün ID: ${productId}
184
+ Platform: ${platform}
185
+ Veri Tipi: ${dataType}`;
186
+
187
+ if (dataType === 'price_changes' && anomaly.type === 'price_change') {
188
+ currentSituationDescription += `\nFiyat Değişikliği Tespit Edildi: Ürünün eski fiyatı ${anomaly.oldPrice} iken, yeni fiyatı ${anomaly.newPrice} oldu.`;
189
+ currentSituationDescription += `\nİçgörü Özeti: ${insights.summary}`;
190
+ currentSituationDescription += `\nİçgörü Detayları: ${insights.details.join('; ')}`;
191
+ } else if (dataType === 'buybox_status') {
192
+ currentSituationDescription += `\nBuybox Durumu: ${anomaly.status}.`;
193
+ currentSituationDescription += `\nİçgörü Özeti: ${insights.summary}`;
194
+ }
195
+ // Add more specific descriptions for other anomaly types as needed
196
+
197
+ // Fetch relevant company rules from vector DB (placeholder for actual embedding and search)
198
+ const relevantRules = await this.vectorDb.search('company_rules', [/* dummy embedding */], 2); // Assume some query vector
199
+ const companyRulesContext = relevantRules.map(r => r.metadata.text).join('\n- ');
200
+
201
+ // Fetch past strategic decisions (placeholder)
202
+ const pastDecisionsContext = "Daha önce benzer bir durumda fiyat düşürme kararı alınmıştı."; // Placeholder
203
+
204
+ const llmResponse = await this.eiaBrain.analyzeAndDecide({
205
+ currentSituation: currentSituationDescription,
206
+ companyRules: companyRulesContext || 'Şirket kuralları bulunamadı.',
207
+ pastDecisions: pastDecisionsContext
208
+ });
209
+ log('INFO', 'Alert sent via LLM', { llmResponse: llmResponse.slice(0, 100) });
210
+ }
211
+ }
212
+
213
+ let eiaMonitorInstance = null;
214
+
215
+ export function getEIAMonitor(config, env) {
216
+ if (!eiaMonitorInstance) {
217
+ eiaMonitorInstance = new EIAMonitor(config, env);
218
+ }
219
+ return eiaMonitorInstance;
220
+ }
package/core/engine.js CHANGED
@@ -17,6 +17,7 @@ import platformHub from '../plugins/vantuz/platforms/index.js';
17
17
  import { getChannelManager } from './channels.js';
18
18
  import { chat as aiChat, log } from './ai-provider.js';
19
19
  import { getGateway } from './gateway.js';
20
+ import { getEIAMonitor } from './eia-monitor.js'; // New import
20
21
 
21
22
  // Tools
22
23
  import { repricerTool } from '../plugins/vantuz/tools/repricer.js';
@@ -27,6 +28,44 @@ import { productTool } from '../plugins/vantuz/tools/product.js';
27
28
  import { analyticsTool } from '../plugins/vantuz/tools/analytics.js';
28
29
  import { quickReportTool } from '../plugins/vantuz/tools/quick-report.js';
29
30
 
31
+ const PLATFORM_CONFIG_MAP = {
32
+ trendyol: {
33
+ envPrefix: 'TRENDYOL',
34
+ keys: ['supplierId', 'apiKey', 'apiSecret']
35
+ },
36
+ hepsiburada: {
37
+ envPrefix: 'HEPSIBURADA',
38
+ keys: ['merchantId', 'username', 'password']
39
+ },
40
+ n11: {
41
+ envPrefix: 'N11',
42
+ keys: ['apiKey', 'apiSecret']
43
+ },
44
+ amazon: {
45
+ envPrefix: 'AMAZON',
46
+ nested: {
47
+ eu: {
48
+ keys: ['sellerId', 'clientId', 'refreshToken']
49
+ },
50
+ us: {
51
+ keys: ['sellerId', 'clientId', 'refreshToken']
52
+ }
53
+ }
54
+ },
55
+ ciceksepeti: {
56
+ envPrefix: 'CICEKSEPETI',
57
+ keys: ['apiKey', 'apiSecret']
58
+ },
59
+ pttavm: {
60
+ envPrefix: 'PTTAVM',
61
+ keys: ['apiKey', 'apiSecret']
62
+ },
63
+ pazarama: {
64
+ envPrefix: 'PAZARAMA',
65
+ keys: ['apiKey', 'apiSecret']
66
+ }
67
+ };
68
+
30
69
  // ═══════════════════════════════════════════════════════════════════════════
31
70
  // CONFIG
32
71
  // ═══════════════════════════════════════════════════════════════════════════
@@ -51,6 +90,7 @@ export class VantuzEngine {
51
90
  products: [],
52
91
  connectedPlatforms: []
53
92
  };
93
+ this.eiaMonitor = null; // New property
54
94
 
55
95
  // Tool Registry
56
96
  this.tools = {
@@ -93,6 +133,10 @@ export class VantuzEngine {
93
133
  // Context oluştur (bağlı platformlardan veri çek)
94
134
  await this._buildContext();
95
135
 
136
+ // Initialize and start EIA Monitor
137
+ this.eiaMonitor = getEIAMonitor(this.config, this.env);
138
+ await this.eiaMonitor.initMonitoringTasks(); // New line
139
+
96
140
  this.initialized = true;
97
141
  log('INFO', 'Vantuz Engine hazır', {
98
142
  platforms: this.context.connectedPlatforms.length,
@@ -140,35 +184,39 @@ export class VantuzEngine {
140
184
  async _initPlatforms() {
141
185
  const platformConfig = {};
142
186
 
143
- // Otomatik config eşleme
144
- if (this.env.TRENDYOL_API_KEY) {
145
- platformConfig.trendyol = {
146
- supplierId: this.env.TRENDYOL_SUPPLIER_ID,
147
- apiKey: this.env.TRENDYOL_API_KEY,
148
- apiSecret: this.env.TRENDYOL_API_SECRET
149
- };
150
- }
151
- if (this.env.HEPSIBURADA_MERCHANT_ID) {
152
- platformConfig.hepsiburada = {
153
- merchantId: this.env.HEPSIBURADA_MERCHANT_ID,
154
- username: this.env.HEPSIBURADA_USERNAME,
155
- password: this.env.HEPSIBURADA_PASSWORD
156
- };
157
- }
158
- if (this.env.N11_API_KEY) {
159
- platformConfig.n11 = {
160
- apiKey: this.env.N11_API_KEY,
161
- apiSecret: this.env.N11_API_SECRET
162
- };
163
- }
164
- if (this.env.AMAZON_SELLER_ID) {
165
- platformConfig.amazon = {
166
- eu: {
167
- sellerId: this.env.AMAZON_SELLER_ID,
168
- clientId: this.env.AMAZON_CLIENT_ID,
169
- refreshToken: this.env.AMAZON_REFRESH_TOKEN
187
+ for (const platformName in PLATFORM_CONFIG_MAP) {
188
+ const platformMap = PLATFORM_CONFIG_MAP[platformName];
189
+ if (platformMap.envPrefix) {
190
+ const config = {};
191
+ let hasRequiredEnv = false;
192
+ for (const key of platformMap.keys) {
193
+ const envKey = `${platformMap.envPrefix}_${key.toUpperCase()}`;
194
+ if (this.env[envKey]) {
195
+ config[key] = this.env[envKey];
196
+ hasRequiredEnv = true;
197
+ }
170
198
  }
171
- };
199
+ if (hasRequiredEnv) {
200
+ platformConfig[platformName] = config;
201
+ }
202
+ } else if (platformName === 'amazon' && this.env.AMAZON_SELLER_ID) { // Specific handling for Amazon's nested structure
203
+ platformConfig.amazon = {};
204
+ // Handle EU configuration
205
+ const euConfig = {};
206
+ let hasEuEnv = false;
207
+ for (const key of PLATFORM_CONFIG_MAP.amazon.nested.eu.keys) {
208
+ const envKey = `AMAZON_${key.toUpperCase()}`;
209
+ if (this.env[envKey]) {
210
+ euConfig[key] = this.env[envKey];
211
+ hasEuEnv = true;
212
+ }
213
+ }
214
+ if (hasEuEnv) {
215
+ platformConfig.amazon.eu = euConfig;
216
+ }
217
+
218
+ // Handle US configuration if needed (add similar logic here)
219
+ }
172
220
  }
173
221
 
174
222
  // Platform Hub'ı başlat
@@ -238,17 +286,29 @@ export class VantuzEngine {
238
286
  * Mesajdan Tool tespiti (Basit NLP)
239
287
  */
240
288
  async _tryExecuteToolFromMessage(message) {
241
- const lower = message.toLowerCase();
242
-
243
- // Repricer
244
- if (lower.includes('fiyat analizi') || lower.includes('rakip analizi')) {
245
- return "Repricer aracı çalıştırılıyor... (Detaylı parametreler için /rakip komutunu kullanın)";
246
- }
247
-
248
- // Stok
249
- if (lower.includes('stok durumu')) {
250
- const stocks = await this.getStock();
251
- return JSON.stringify(stocks.map(s => ({ platform: s.platform, items: s.products.length })), null, 2);
289
+ const lower = message.toLowerCase().trim();
290
+
291
+ // Check for explicit commands
292
+ if (lower.startsWith('/')) {
293
+ const parts = lower.substring(1).split(' ');
294
+ const command = parts[0];
295
+ const args = parts.slice(1);
296
+
297
+ switch (command) {
298
+ case 'repricer':
299
+ case 'rakip':
300
+ case 'fiyat-analizi':
301
+ // In a real scenario, you'd parse args and pass them to repricerTool
302
+ return "Repricer aracı çalıştırılıyor... (Detaylı parametreler için /rakip komutunu kullanın)";
303
+ case 'stock':
304
+ case 'stok-durumu':
305
+ const stocks = await this.getStock();
306
+ return JSON.stringify(stocks.map(s => ({ platform: s.platform, items: s.products.length })), null, 2);
307
+ case 'help':
308
+ return "Kullanabileceğin komutlar: /rakip, /stok-durumu";
309
+ default:
310
+ return `Bilinmeyen komut: /${command}. Yardım için /help yazabilirsin.`;
311
+ }
252
312
  }
253
313
 
254
314
  return null;
package/core/gateway.js CHANGED
@@ -34,24 +34,7 @@ function loadGatewayConfig() {
34
34
  return null;
35
35
  }
36
36
 
37
- function getGatewayUrl() {
38
- // Gateway cmd dosyasından port oku
39
- const gatewayCmd = path.join(OPENCLAW_HOME, 'gateway.cmd');
40
- let port = 18789; // Varsayılan
41
-
42
- try {
43
- if (fs.existsSync(gatewayCmd)) {
44
- const content = fs.readFileSync(gatewayCmd, 'utf-8');
45
- const portMatch = content.match(/OPENCLAW_GATEWAY_PORT=(\d+)/);
46
- if (portMatch) port = parseInt(portMatch[1]);
47
- }
48
- } catch (e) { }
49
-
50
- return `http://localhost:${port}`;
51
- }
52
-
53
- function getGatewayToken() {
54
- const config = loadGatewayConfig();
37
+ function getGatewayToken(config) {
55
38
  return config?.gateway?.auth?.token || null;
56
39
  }
57
40
 
@@ -61,11 +44,12 @@ function getGatewayToken() {
61
44
 
62
45
  export class VantuzGateway {
63
46
  constructor() {
64
- this.baseUrl = getGatewayUrl();
65
- this.token = getGatewayToken();
47
+ this.config = loadGatewayConfig();
48
+ const port = this.config?.gateway?.port || 18789;
49
+ this.baseUrl = `http://localhost:${port}`;
50
+ this.token = getGatewayToken(this.config);
66
51
  this.connected = false;
67
52
  this.version = null;
68
- this.config = loadGatewayConfig();
69
53
  }
70
54
 
71
55
  /**
@@ -0,0 +1,28 @@
1
+ // core/migrations/001-initial-schema.js
2
+ export async function up(db) {
3
+ await db.exec(`
4
+ CREATE TABLE products (
5
+ id TEXT PRIMARY KEY,
6
+ name TEXT NOT NULL,
7
+ price REAL,
8
+ cost REAL,
9
+ platform TEXT NOT NULL,
10
+ lastUpdated TEXT
11
+ );
12
+
13
+ CREATE TABLE historical_prices (
14
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
15
+ productId TEXT NOT NULL,
16
+ price REAL NOT NULL,
17
+ timestamp TEXT NOT NULL,
18
+ FOREIGN KEY (productId) REFERENCES products(id) ON DELETE CASCADE
19
+ );
20
+ `);
21
+ }
22
+
23
+ export async function down(db) {
24
+ await db.exec(`
25
+ DROP TABLE IF EXISTS historical_prices;
26
+ DROP TABLE IF EXISTS products;
27
+ `);
28
+ }
@@ -0,0 +1,80 @@
1
+ // core/scheduler.js
2
+ import { CronJob } from 'cron';
3
+ import { log } from './ai-provider.js';
4
+
5
+ class Scheduler {
6
+ constructor() {
7
+ this.jobs = new Map();
8
+ log('INFO', 'Scheduler initialized');
9
+ }
10
+
11
+ /**
12
+ * Bir görevi belirli bir cron zamanlamasına göre kaydeder ve başlatır.
13
+ * @param {string} name Görevin benzersiz adı.
14
+ * @param {string} cronTime Cron zamanlama dizesi (örn. '0 * * * *' her saat başı).
15
+ * @param {function} task Çalıştırılacak asenkron fonksiyon.
16
+ * @param {boolean} startImmediately Hemen başlatılsın mı? (Varsayılan: true)
17
+ */
18
+ addJob(name, cronTime, task, startImmediately = true) {
19
+ if (this.jobs.has(name)) {
20
+ log('WARN', `Job with name "${name}" already exists. Skipping.`);
21
+ return;
22
+ }
23
+
24
+ const job = new CronJob(cronTime, async () => {
25
+ log('INFO', `Running scheduled job: ${name}`);
26
+ try {
27
+ await task();
28
+ log('INFO', `Scheduled job "${name}" completed successfully.`);
29
+ } catch (error) {
30
+ log('ERROR', `Scheduled job "${name}" failed: ${error.message}`, { error });
31
+ }
32
+ }, null, startImmediately, 'UTC'); // UTC timezone for consistency
33
+
34
+ this.jobs.set(name, job);
35
+ log('INFO', `Scheduled job "${name}" added and started with cron: "${cronTime}"`);
36
+ }
37
+
38
+ /**
39
+ * Kayıtlı bir görevi durdurur.
40
+ * @param {string} name Durdurulacak görevin adı.
41
+ */
42
+ stopJob(name) {
43
+ const job = this.jobs.get(name);
44
+ if (job) {
45
+ job.stop();
46
+ this.jobs.delete(name);
47
+ log('INFO', `Scheduled job "${name}" stopped and removed.`);
48
+ } else {
49
+ log('WARN', `Job with name "${name}" not found. Cannot stop.`);
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Tüm kayıtlı görevleri durdurur.
55
+ */
56
+ stopAllJobs() {
57
+ for (const [name, job] of this.jobs) {
58
+ job.stop();
59
+ log('INFO', `Scheduled job "${name}" stopped.`);
60
+ }
61
+ this.jobs.clear();
62
+ log('INFO', 'All scheduled jobs stopped.');
63
+ }
64
+
65
+ /**
66
+ * Tüm aktif görevlerin listesini döner.
67
+ */
68
+ listJobs() {
69
+ return Array.from(this.jobs.keys());
70
+ }
71
+ }
72
+
73
+ let schedulerInstance = null;
74
+
75
+ export function getScheduler() {
76
+ if (!schedulerInstance) {
77
+ schedulerInstance = new Scheduler();
78
+ }
79
+ return schedulerInstance;
80
+ }
@@ -0,0 +1,61 @@
1
+ // core/scrapers/Scraper.js
2
+ import { chromium } from 'playwright';
3
+
4
+ export class Scraper {
5
+ constructor() {
6
+ this.browser = null;
7
+ this.page = null;
8
+ }
9
+
10
+ async init() {
11
+ if (!this.browser) {
12
+ this.browser = await chromium.launch({ headless: true }); // headless: true for production
13
+ }
14
+ this.page = await this.browser.newPage();
15
+ }
16
+
17
+ async close() {
18
+ if (this.page) {
19
+ await this.page.close();
20
+ this.page = null;
21
+ }
22
+ if (this.browser) {
23
+ await this.browser.close();
24
+ this.browser = null;
25
+ }
26
+ }
27
+
28
+ async goTo(url) {
29
+ if (!this.page) {
30
+ throw new Error('Scraper not initialized. Call init() first.');
31
+ }
32
+ await this.page.goto(url);
33
+ }
34
+
35
+ async extractText(selector) {
36
+ if (!this.page) return null;
37
+ return this.page.locator(selector).textContent();
38
+ }
39
+
40
+ async extractAttribute(selector, attribute) {
41
+ if (!this.page) return null;
42
+ return this.page.locator(selector).getAttribute(attribute);
43
+ }
44
+
45
+ async extractAllText(selector) {
46
+ if (!this.page) return [];
47
+ return this.page.locator(selector).allTextContents();
48
+ }
49
+
50
+ async type(selector, text) {
51
+ if (!this.page) return;
52
+ await this.page.locator(selector).type(text);
53
+ }
54
+
55
+ async click(selector) {
56
+ if (!this.page) return;
57
+ await this.page.locator(selector).click();
58
+ }
59
+
60
+ // You can add more generic scraping methods here
61
+ }