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.
- package/core/ai-provider.js +95 -141
- package/core/database.js +70 -60
- package/core/eia-brain.js +102 -0
- package/core/eia-monitor.js +220 -0
- package/core/engine.js +99 -39
- package/core/gateway.js +5 -21
- package/core/migrations/001-initial-schema.js +28 -0
- package/core/scheduler.js +80 -0
- package/core/scrapers/Scraper.js +61 -0
- package/core/scrapers/TrendyolScraper.js +76 -0
- package/core/vector-db.js +97 -0
- package/onboard.js +27 -1
- package/package.json +7 -5
- package/plugins/vantuz/platforms/index.js +13 -9
- package/server/app.js +2 -2
|
@@ -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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
//
|
|
244
|
-
if (lower.
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
|
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.
|
|
65
|
-
this.
|
|
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
|
+
}
|