vantuz 3.3.0 → 3.3.2
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/cli.js +185 -70
- package/core/agent.js +82 -0
- package/core/ai-provider.js +26 -6
- package/core/automation.js +504 -0
- package/core/channels.js +2 -2
- package/core/eia-monitor.js +14 -6
- package/core/engine.js +319 -90
- package/core/gateway.js +68 -16
- package/core/migrations/001-initial-schema.sql +20 -0
- package/core/openclaw-bridge.js +191 -0
- package/core/scrapers/TrendyolScraper.js +74 -19
- package/package.json +85 -91
- package/platforms/amazon.js +5 -3
- package/platforms/ciceksepeti.js +4 -2
- package/platforms/hepsiburada.js +6 -4
- package/platforms/n11.js +5 -3
- package/platforms/pazarama.js +4 -2
- package/platforms/trendyol.js +3 -3
- package/plugins/vantuz/package.json +2 -2
- package/plugins/vantuz/platforms/_request.js +34 -0
- package/plugins/vantuz/platforms/amazon.js +59 -35
- package/plugins/vantuz/platforms/ciceksepeti.js +14 -9
- package/plugins/vantuz/platforms/hepsiburada.js +14 -9
- package/plugins/vantuz/platforms/index.js +101 -54
- package/plugins/vantuz/platforms/n11.js +91 -34
- package/plugins/vantuz/platforms/pazarama.js +30 -17
- package/plugins/vantuz/platforms/pttavm.js +14 -9
- package/plugins/vantuz/platforms/trendyol.js +16 -11
- package/server/app.js +29 -12
- package/server/public/index.html +2 -2
- package/.openclaw/completions/openclaw.bash +0 -227
- package/.openclaw/completions/openclaw.fish +0 -1552
- package/.openclaw/completions/openclaw.ps1 +0 -1966
- package/.openclaw/completions/openclaw.zsh +0 -3571
- package/.openclaw/gateway.cmd +0 -10
- package/.openclaw/identity/device.json +0 -7
- package/.openclaw/openclaw.json +0 -32
- package/DOCS_TR.md +0 -298
- package/onboard.js +0 -322
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
// core/automation.js
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import crypto from 'crypto';
|
|
6
|
+
import { log, chat as aiChat } from './ai-provider.js';
|
|
7
|
+
import { getScheduler } from './scheduler.js';
|
|
8
|
+
|
|
9
|
+
const VANTUZ_HOME = path.join(os.homedir(), '.vantuz');
|
|
10
|
+
const CONFIG_JSON = path.join(VANTUZ_HOME, 'config.json');
|
|
11
|
+
|
|
12
|
+
const RISKY_KEYWORDS = [
|
|
13
|
+
'fiyat', 'zam', 'indirim', 'stok', 'g?ncelle', 'update',
|
|
14
|
+
'yay?nla', 'yay?ndan kald?r', 'unpublish', 'publish',
|
|
15
|
+
'kampanya', 'kupon', 'ip',
|
|
16
|
+
'sil', 'kald?r', 'de?i?tir', 'price', 'stock'
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const APPROVE_KEYWORDS = ['onay', 'onayla', 'kabul', 'evet', 'tamam'];
|
|
20
|
+
const REJECT_KEYWORDS = ['hay?r', 'iptal', 'vazge?', 'reddet'];
|
|
21
|
+
|
|
22
|
+
function loadConfig() {
|
|
23
|
+
try {
|
|
24
|
+
if (fs.existsSync(CONFIG_JSON)) {
|
|
25
|
+
return JSON.parse(fs.readFileSync(CONFIG_JSON, 'utf-8'));
|
|
26
|
+
}
|
|
27
|
+
} catch (e) {
|
|
28
|
+
log('WARN', 'Config okunamad?', { error: e.message });
|
|
29
|
+
}
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function saveConfig(config) {
|
|
34
|
+
try {
|
|
35
|
+
fs.writeFileSync(CONFIG_JSON, JSON.stringify(config, null, 2));
|
|
36
|
+
return true;
|
|
37
|
+
} catch (e) {
|
|
38
|
+
log('WARN', 'Config yaz?lamad?', { error: e.message });
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function normalizePhone(input) {
|
|
44
|
+
if (!input) return '';
|
|
45
|
+
const cleaned = String(input).replace(/[\s-]/g, '');
|
|
46
|
+
return cleaned.startsWith('+') ? cleaned : `+${cleaned}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function messageHasAny(message, keywords) {
|
|
50
|
+
const lower = message.toLowerCase();
|
|
51
|
+
return keywords.some(k => lower.includes(k));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function normalizeTr(input) {
|
|
55
|
+
return String(input || '')
|
|
56
|
+
.toLowerCase()
|
|
57
|
+
.replace(/[??]/g, 'c')
|
|
58
|
+
.replace(/[??]/g, 'g')
|
|
59
|
+
.replace(/[??]/g, 'i')
|
|
60
|
+
.replace(/[??]/g, 'o')
|
|
61
|
+
.replace(/[??]/g, 's')
|
|
62
|
+
.replace(/[??]/g, 'u');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function summarizeOrdersByStatus(orders = []) {
|
|
66
|
+
if (!Array.isArray(orders) || orders.length === 0) return '';
|
|
67
|
+
const counts = {};
|
|
68
|
+
orders.forEach(o => {
|
|
69
|
+
const s = (o.status || o.shipmentPackageStatus || o.orderStatus || 'UNKNOWN').toString();
|
|
70
|
+
counts[s] = (counts[s] || 0) + 1;
|
|
71
|
+
});
|
|
72
|
+
return Object.entries(counts)
|
|
73
|
+
.sort((a, b) => b[1] - a[1])
|
|
74
|
+
.map(([s, n]) => `${s}:${n}`)
|
|
75
|
+
.join(', ');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function isActiveStatus(status) {
|
|
79
|
+
const s = String(status || '').toLowerCase();
|
|
80
|
+
return s === 'created' || s === 'picking' || s === 'unpacked';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function extractProductNames(order) {
|
|
84
|
+
const lines = Array.isArray(order?.lines) ? order.lines : [];
|
|
85
|
+
const names = lines
|
|
86
|
+
.map(l => l?.productName || l?.name)
|
|
87
|
+
.filter(Boolean);
|
|
88
|
+
return names;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function getOrderTimestamp(order) {
|
|
92
|
+
const candidates = [
|
|
93
|
+
order?.lastModifiedDate,
|
|
94
|
+
order?.agreedDeliveryDate,
|
|
95
|
+
order?.createdDate,
|
|
96
|
+
order?.orderDate
|
|
97
|
+
];
|
|
98
|
+
for (const value of candidates) {
|
|
99
|
+
if (!value) continue;
|
|
100
|
+
const num = Number(value);
|
|
101
|
+
if (Number.isFinite(num) && num > 0) return num;
|
|
102
|
+
const parsed = Date.parse(value);
|
|
103
|
+
if (!Number.isNaN(parsed)) return parsed;
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function formatOrderLine(order) {
|
|
109
|
+
const number = order.orderNumber || order.id || 'N/A';
|
|
110
|
+
const name = order.customerName || order.customerfullName || order.customerFullName || 'M??teri';
|
|
111
|
+
const total = order.totalPrice ?? order.totalAmount ?? order.total ?? '?';
|
|
112
|
+
const status = order.status || order.shipmentPackageStatus || order.orderStatus || 'UNKNOWN';
|
|
113
|
+
return `#${number} - ${name} - ${total} TL - ${status}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function getTenantId(channel, from) {
|
|
117
|
+
if (channel === 'whatsapp' && from) return normalizePhone(from);
|
|
118
|
+
return 'default';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function ensureConfigShape(config) {
|
|
122
|
+
if (!config.tenants) config.tenants = {};
|
|
123
|
+
if (!config.automation) config.automation = {};
|
|
124
|
+
if (!config.automation.approvals) config.automation.approvals = [];
|
|
125
|
+
if (!config.automation.cronJobs) config.automation.cronJobs = [];
|
|
126
|
+
return config;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function ensureTenantState(config, tenantId) {
|
|
130
|
+
if (!config.tenants[tenantId]) {
|
|
131
|
+
config.tenants[tenantId] = { riskAccepted: false };
|
|
132
|
+
}
|
|
133
|
+
if (!config.tenants[tenantId].session) {
|
|
134
|
+
config.tenants[tenantId].session = {};
|
|
135
|
+
}
|
|
136
|
+
return config.tenants[tenantId];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function toSessionOrder(order) {
|
|
140
|
+
return {
|
|
141
|
+
orderNumber: order.orderNumber || order.id || 'N/A',
|
|
142
|
+
status: order.status || order.shipmentPackageStatus || order.orderStatus || 'UNKNOWN',
|
|
143
|
+
productNames: extractProductNames(order)
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function storeLastOrders(config, tenantId, orders) {
|
|
148
|
+
const tenant = ensureTenantState(config, tenantId);
|
|
149
|
+
const list = Array.isArray(orders) ? orders.map(toSessionOrder) : [];
|
|
150
|
+
tenant.session.lastOrders = list.slice(0, 20);
|
|
151
|
+
tenant.session.lastOrderAt = new Date().toISOString();
|
|
152
|
+
saveConfig(config);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function newApproval(message, meta, kind = 'action') {
|
|
156
|
+
return {
|
|
157
|
+
id: crypto.randomUUID(),
|
|
158
|
+
channel: meta.channel,
|
|
159
|
+
from: meta.from || 'unknown',
|
|
160
|
+
message,
|
|
161
|
+
kind,
|
|
162
|
+
createdAt: new Date().toISOString()
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function findPendingApproval(config, meta) {
|
|
167
|
+
const approvals = config.automation?.approvals || [];
|
|
168
|
+
const from = meta.from || 'unknown';
|
|
169
|
+
return approvals.find(a => a.from === from && a.channel === meta.channel);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function removeApproval(config, approvalId) {
|
|
173
|
+
config.automation.approvals = (config.automation.approvals || []).filter(a => a.id !== approvalId);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function planWithAI(message, engine) {
|
|
177
|
+
const systemPrompt = [
|
|
178
|
+
'Sen Vantuz otomasyon planlay?c?s?s?n.',
|
|
179
|
+
'Kullan?c? mesaj?n? tek bir otomasyon plan?na ?evir ve sadece JSON d?nd?r.',
|
|
180
|
+
'?ema:',
|
|
181
|
+
'{ "intent": "report|analysis|change|schedule|other", "risk": "low|high", "schedule": "", "action": "" }',
|
|
182
|
+
'risk = pazaryeri de?i?ikli?i varsa "high" olmal?.',
|
|
183
|
+
'schedule = cron ifadesi (bo? olabilir).',
|
|
184
|
+
'action = yap?lacak i?i k?sa T?rk?e ?zetle.'
|
|
185
|
+
].join('\n');
|
|
186
|
+
|
|
187
|
+
const response = await aiChat(message, {
|
|
188
|
+
aiProvider: engine.config.aiProvider || 'gemini',
|
|
189
|
+
systemContext: systemPrompt
|
|
190
|
+
}, engine.env);
|
|
191
|
+
|
|
192
|
+
const jsonMatch = response.match(/\{[\s\S]*\}/);
|
|
193
|
+
if (!jsonMatch) return null;
|
|
194
|
+
try {
|
|
195
|
+
return JSON.parse(jsonMatch[0]);
|
|
196
|
+
} catch {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function fallbackPlan(message) {
|
|
202
|
+
const risky = messageHasAny(message, RISKY_KEYWORDS);
|
|
203
|
+
const schedule = message.includes('cron ') ? message.split('cron ')[1].trim() : '';
|
|
204
|
+
return {
|
|
205
|
+
intent: schedule ? 'schedule' : (risky ? 'change' : 'analysis'),
|
|
206
|
+
risk: risky ? 'high' : 'low',
|
|
207
|
+
schedule,
|
|
208
|
+
action: message
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export class AutomationManager {
|
|
213
|
+
constructor(engine) {
|
|
214
|
+
this.engine = engine;
|
|
215
|
+
this.scheduler = getScheduler();
|
|
216
|
+
this.config = ensureConfigShape(loadConfig());
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
init() {
|
|
220
|
+
this._restoreCronJobs();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
_restoreCronJobs() {
|
|
224
|
+
const jobs = this.config.automation?.cronJobs || [];
|
|
225
|
+
for (const job of jobs) {
|
|
226
|
+
this._scheduleJob(job, false);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
_scheduleJob(job, persist = true) {
|
|
231
|
+
if (!job?.name || !job?.cron || !job?.message) return;
|
|
232
|
+
|
|
233
|
+
this.scheduler.addJob(job.name, job.cron, async () => {
|
|
234
|
+
await this.engine.chat(job.message);
|
|
235
|
+
}, true);
|
|
236
|
+
|
|
237
|
+
if (persist) {
|
|
238
|
+
this.config.automation.cronJobs.push(job);
|
|
239
|
+
saveConfig(this.config);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async handleMessage(message, meta = { channel: 'local', from: 'local' }) {
|
|
244
|
+
this.config = ensureConfigShape(loadConfig());
|
|
245
|
+
|
|
246
|
+
const lower = String(message || '').toLowerCase();
|
|
247
|
+
const normalized = normalizeTr(message);
|
|
248
|
+
const hasOrderKeyword = lower.includes('sipari?') || normalized.includes('siparis');
|
|
249
|
+
const asksApproved = lower.includes('onaylanan') || normalized.includes('onaylanan');
|
|
250
|
+
const asksUnshipped = lower.includes('kargoya verilmemi?') || lower.includes('hen?z kargoya') || lower.includes('kargoya verilmedi') || normalized.includes('kargoya verilmemis');
|
|
251
|
+
const asksPickingWhich = lower.includes('picking') || normalized.includes('picking');
|
|
252
|
+
const asksOrderNames = lower.includes('sipari?lerin ad?') || normalized.includes('siparislerin adi') || lower.includes('?r?n ad?') || normalized.includes('urun adi') || lower.includes('?r?n?n ad?') || normalized.includes('urunun adi');
|
|
253
|
+
const asksToday = lower.includes('bug?n') || normalized.includes('bugun');
|
|
254
|
+
const asksLast24h = lower.includes('son 24') || normalized.includes('son 24');
|
|
255
|
+
const asksYesterday = lower.includes('d?n') || normalized.includes('dun');
|
|
256
|
+
const asksNeedShipment = lower.includes('kargoya ??kmas?') || lower.includes('kargoya cikmasi') || normalized.includes('kargoya cikmasi');
|
|
257
|
+
const asksWhichProduct = lower.includes('hangi ?r?n') || normalized.includes('hangi urun');
|
|
258
|
+
|
|
259
|
+
const tenantId = getTenantId(meta.channel, meta.from);
|
|
260
|
+
ensureTenantState(this.config, tenantId);
|
|
261
|
+
|
|
262
|
+
if (asksApproved) {
|
|
263
|
+
const orders = await this.engine.getOrders({ size: 100, status: 'Picking' });
|
|
264
|
+
storeLastOrders(this.config, tenantId, orders);
|
|
265
|
+
return {
|
|
266
|
+
handled: true,
|
|
267
|
+
response: `?u an onaylanan (Picking) ${orders.length} sipari? var.`
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (asksUnshipped) {
|
|
272
|
+
const orders = await this.engine.getOrders({ size: 100, status: 'Created' });
|
|
273
|
+
storeLastOrders(this.config, tenantId, orders);
|
|
274
|
+
return {
|
|
275
|
+
handled: true,
|
|
276
|
+
response: `?u an kargoya verilmemi? (Created) ${orders.length} sipari? var.`
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (asksPickingWhich) {
|
|
281
|
+
const orders = await this.engine.getOrders({ size: 100, status: 'Picking' });
|
|
282
|
+
if (!orders || orders.length === 0) {
|
|
283
|
+
return { handled: true, response: 'Picking stat?s?nde sipari? yok.' };
|
|
284
|
+
}
|
|
285
|
+
storeLastOrders(this.config, tenantId, orders);
|
|
286
|
+
const lines = orders.slice(0, 5).map(formatOrderLine).join('\n');
|
|
287
|
+
return { handled: true, response: `Picking sipari?leri:\n${lines}` };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (asksNeedShipment) {
|
|
291
|
+
const orders = await this.engine.getOrders({ size: 100, status: 'Created' });
|
|
292
|
+
if (!orders || orders.length === 0) {
|
|
293
|
+
return { handled: true, response: 'Kargoya ??kmas? gereken (Created) sipari? yok.' };
|
|
294
|
+
}
|
|
295
|
+
storeLastOrders(this.config, tenantId, orders);
|
|
296
|
+
const lines = orders.slice(0, 5).map(formatOrderLine).join('\n');
|
|
297
|
+
return { handled: true, response: `Kargoya ??kmas? gereken sipari?ler:\n${lines}` };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (asksWhichProduct) {
|
|
301
|
+
const tenant = ensureTenantState(this.config, tenantId);
|
|
302
|
+
const last = tenant.session?.lastOrders || [];
|
|
303
|
+
if (last.length === 1 && last[0].productNames?.length) {
|
|
304
|
+
return { handled: true, response: `?r?n: ${last[0].productNames.join(', ')}` };
|
|
305
|
+
}
|
|
306
|
+
if (last.length > 1) {
|
|
307
|
+
const lines = last.slice(0, 5).map(o => `#${o.orderNumber} - ${o.productNames?.join(', ') || '?r?n ad? yok'}`).join('\n');
|
|
308
|
+
return { handled: true, response: `Hangi sipari?i kastediyorsun?\n${lines}` };
|
|
309
|
+
}
|
|
310
|
+
return { handled: true, response: '?nce sipari?leri g?rmem gerekiyor. "/siparis" yazabilir misin?' };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (asksOrderNames) {
|
|
314
|
+
const orders = await this.engine.getOrders({ size: 50, allStatuses: true });
|
|
315
|
+
if (!orders || orders.length === 0) {
|
|
316
|
+
return { handled: true, response: 'Sipari? verisine ula??lamad?.' };
|
|
317
|
+
}
|
|
318
|
+
const active = orders.filter(o => isActiveStatus(o.status || o.shipmentPackageStatus || o.orderStatus));
|
|
319
|
+
const target = active.length > 0 ? active : orders;
|
|
320
|
+
storeLastOrders(this.config, tenantId, target);
|
|
321
|
+
if (target.length === 1) {
|
|
322
|
+
const only = target[0];
|
|
323
|
+
const names = extractProductNames(only);
|
|
324
|
+
if (names.length > 0) {
|
|
325
|
+
return { handled: true, response: `?r?n adlar?: ${names.join(', ')}` };
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
const productLines = target
|
|
329
|
+
.map(o => {
|
|
330
|
+
const names = extractProductNames(o);
|
|
331
|
+
const number = o.orderNumber || o.id || 'N/A';
|
|
332
|
+
if (names.length === 0) return null;
|
|
333
|
+
return `#${number} - ${names.join(', ')}`;
|
|
334
|
+
})
|
|
335
|
+
.filter(Boolean)
|
|
336
|
+
.slice(0, 10)
|
|
337
|
+
.join('\n');
|
|
338
|
+
if (!productLines) {
|
|
339
|
+
const fallback = target.slice(0, 10).map(formatOrderLine).join('\n');
|
|
340
|
+
return { handled: true, response: `Sipari?ler:\n${fallback}` };
|
|
341
|
+
}
|
|
342
|
+
return { handled: true, response: `?r?n adlar?:\n${productLines}` };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (asksYesterday) {
|
|
346
|
+
const orders = await this.engine.getOrders({ size: 200, allStatuses: true });
|
|
347
|
+
if (!orders || orders.length === 0) {
|
|
348
|
+
return { handled: true, response: 'Sipari? verisine ula??lamad?.' };
|
|
349
|
+
}
|
|
350
|
+
const now = Date.now();
|
|
351
|
+
const end = new Date(new Date().setHours(0, 0, 0, 0)).getTime();
|
|
352
|
+
const start = end - 24 * 60 * 60 * 1000;
|
|
353
|
+
const filtered = orders.filter(o => {
|
|
354
|
+
const ts = getOrderTimestamp(o);
|
|
355
|
+
return ts && ts >= start && ts < end;
|
|
356
|
+
});
|
|
357
|
+
if (filtered.length === 0) {
|
|
358
|
+
return { handled: true, response: 'D?n sipari? yok.' };
|
|
359
|
+
}
|
|
360
|
+
const wantsShipped = lower.includes('g?nder') || normalized.includes('gonder') || lower.includes('kargoya') || normalized.includes('kargoya');
|
|
361
|
+
const shippedOnly = wantsShipped ? filtered.filter(o => {
|
|
362
|
+
const s = String(o.status || o.shipmentPackageStatus || o.orderStatus || '').toLowerCase();
|
|
363
|
+
return s === 'shipped' || s === 'delivered';
|
|
364
|
+
}) : filtered;
|
|
365
|
+
const target = shippedOnly.length > 0 ? shippedOnly : filtered;
|
|
366
|
+
storeLastOrders(this.config, tenantId, target);
|
|
367
|
+
const productLines = target
|
|
368
|
+
.map(o => {
|
|
369
|
+
const names = extractProductNames(o);
|
|
370
|
+
const number = o.orderNumber || o.id || 'N/A';
|
|
371
|
+
if (names.length === 0) return null;
|
|
372
|
+
return `#${number} - ${names.join(', ')}`;
|
|
373
|
+
})
|
|
374
|
+
.filter(Boolean)
|
|
375
|
+
.slice(0, 10)
|
|
376
|
+
.join('\n');
|
|
377
|
+
if (productLines) {
|
|
378
|
+
return { handled: true, response: `D?nk? ?r?nler:\n${productLines}` };
|
|
379
|
+
}
|
|
380
|
+
const lines = target.slice(0, 10).map(formatOrderLine).join('\n');
|
|
381
|
+
return { handled: true, response: `D?nk? sipari?ler:\n${lines}` };
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (asksToday || asksLast24h) {
|
|
385
|
+
const orders = await this.engine.getOrders({ size: 200, allStatuses: true });
|
|
386
|
+
if (!orders || orders.length === 0) {
|
|
387
|
+
return { handled: true, response: 'Sipari? verisine ula??lamad?.' };
|
|
388
|
+
}
|
|
389
|
+
const now = Date.now();
|
|
390
|
+
const start = asksToday
|
|
391
|
+
? new Date(new Date().setHours(0, 0, 0, 0)).getTime()
|
|
392
|
+
: now - 24 * 60 * 60 * 1000;
|
|
393
|
+
const filtered = orders.filter(o => {
|
|
394
|
+
const ts = getOrderTimestamp(o);
|
|
395
|
+
return ts && ts >= start && ts <= now;
|
|
396
|
+
});
|
|
397
|
+
if (filtered.length === 0) {
|
|
398
|
+
return { handled: true, response: 'Belirtilen aral?kta sipari? yok.' };
|
|
399
|
+
}
|
|
400
|
+
storeLastOrders(this.config, tenantId, filtered);
|
|
401
|
+
const lines = filtered.slice(0, 10).map(formatOrderLine).join('\n');
|
|
402
|
+
return { handled: true, response: `Sipari?ler:\n${lines}` };
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (lower.includes('sipari? var m?') || normalized.includes('siparis var mi') || lower.includes('sipari? varmi') || normalized.includes('siparis varmi')) {
|
|
406
|
+
const orders = await this.engine.getOrders({ size: 100, allStatuses: true });
|
|
407
|
+
if (!orders || orders.length === 0) {
|
|
408
|
+
return { handled: true, response: 'Sipari? verisine ula??lamad?. L?tfen platform API ba?lant?lar?n? kontrol edin.' };
|
|
409
|
+
}
|
|
410
|
+
const active = orders.filter(o => isActiveStatus(o.status || o.shipmentPackageStatus || o.orderStatus));
|
|
411
|
+
if (active.length === 0) {
|
|
412
|
+
return { handled: true, response: '?u an aktif (Created/Picking/UnPacked) sipari? yok.' };
|
|
413
|
+
}
|
|
414
|
+
const summary = summarizeOrdersByStatus(active);
|
|
415
|
+
storeLastOrders(this.config, tenantId, active);
|
|
416
|
+
return { handled: true, response: `?u an ba?l? platformlardan gelen toplam ${active.length} aktif sipari? var.${summary ? ` Durum k?r?l?m?: ${summary}` : ''}` };
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (hasOrderKeyword) {
|
|
420
|
+
const orders = await this.engine.getOrders({ size: 100, allStatuses: true });
|
|
421
|
+
if (!orders || orders.length === 0) {
|
|
422
|
+
return {
|
|
423
|
+
handled: true,
|
|
424
|
+
response: 'Sipari? verisine ula??lamad?. L?tfen platform API ba?lant?lar?n? kontrol edin.'
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
const active = orders.filter(o => isActiveStatus(o.status || o.shipmentPackageStatus || o.orderStatus));
|
|
428
|
+
const summary = summarizeOrdersByStatus(active);
|
|
429
|
+
storeLastOrders(this.config, tenantId, active);
|
|
430
|
+
return {
|
|
431
|
+
handled: true,
|
|
432
|
+
response: `?u an ba?l? platformlardan gelen toplam ${active.length} sipari? var.${summary ? ` Durum k?r?l?m?: ${summary}` : ''}`
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const tenant = ensureTenantState(this.config, tenantId);
|
|
437
|
+
|
|
438
|
+
const pending = findPendingApproval(this.config, meta);
|
|
439
|
+
if (pending) {
|
|
440
|
+
if (messageHasAny(message, APPROVE_KEYWORDS)) {
|
|
441
|
+
removeApproval(this.config, pending.id);
|
|
442
|
+
if (pending.kind === 'risk-accept') {
|
|
443
|
+
tenant.riskAccepted = true;
|
|
444
|
+
saveConfig(this.config);
|
|
445
|
+
const approval = newApproval(pending.message, meta, 'action');
|
|
446
|
+
this.config.automation.approvals.push(approval);
|
|
447
|
+
saveConfig(this.config);
|
|
448
|
+
return {
|
|
449
|
+
handled: true,
|
|
450
|
+
response: 'Risk kabul edildi. ??lemi onaylamak i?in "Evet" yaz.'
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
saveConfig(this.config);
|
|
454
|
+
const result = await this.engine.chat(pending.message);
|
|
455
|
+
return { handled: true, response: `Onay al?nd?.\n\n${result}` };
|
|
456
|
+
}
|
|
457
|
+
if (messageHasAny(message, REJECT_KEYWORDS)) {
|
|
458
|
+
removeApproval(this.config, pending.id);
|
|
459
|
+
saveConfig(this.config);
|
|
460
|
+
return { handled: true, response: '??lem iptal edildi.' };
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const plan = (await planWithAI(message, this.engine)) || fallbackPlan(message);
|
|
465
|
+
const risky = plan.risk === 'high';
|
|
466
|
+
|
|
467
|
+
if (risky && !tenant.riskAccepted) {
|
|
468
|
+
const approval = newApproval(message, meta, 'risk-accept');
|
|
469
|
+
this.config.automation.approvals.push(approval);
|
|
470
|
+
saveConfig(this.config);
|
|
471
|
+
return {
|
|
472
|
+
handled: true,
|
|
473
|
+
response: 'Bu i?lem pazaryerlerinde de?i?iklik yapabilir ve risklidir. Riskleri kabul ediyor musun? "Evet" yaz.'
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if (risky) {
|
|
478
|
+
const approval = newApproval(message, meta, 'action');
|
|
479
|
+
this.config.automation.approvals.push(approval);
|
|
480
|
+
saveConfig(this.config);
|
|
481
|
+
return {
|
|
482
|
+
handled: true,
|
|
483
|
+
response: `Bu i?lem riskli g?r?n?yor.\nOnaylamak i?in "Evet" yaz.`
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (plan.intent === 'schedule' && plan.schedule) {
|
|
488
|
+
const job = {
|
|
489
|
+
name: `auto-${crypto.randomUUID().slice(0, 8)}`,
|
|
490
|
+
cron: plan.schedule,
|
|
491
|
+
message: plan.action || message
|
|
492
|
+
};
|
|
493
|
+
this._scheduleJob(job, true);
|
|
494
|
+
return {
|
|
495
|
+
handled: true,
|
|
496
|
+
response: `Zamanl? g?rev olu?turuldu: ${job.name}\nCron: ${job.cron}`
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return { handled: false };
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
export default AutomationManager;
|
package/core/channels.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 📱 CHANNEL MANAGER v3.2
|
|
3
|
-
*
|
|
3
|
+
* Gateway üzerinden kanal yönetimi
|
|
4
4
|
*
|
|
5
5
|
* Desteklenen kanallar:
|
|
6
6
|
* - WhatsApp (Meta Business API / Gateway)
|
|
@@ -31,7 +31,7 @@ export class ChannelManager {
|
|
|
31
31
|
async initAll() {
|
|
32
32
|
const env = this._loadEnv();
|
|
33
33
|
|
|
34
|
-
//
|
|
34
|
+
// Gateway üzerinden kanal durumu kontrol et
|
|
35
35
|
try {
|
|
36
36
|
this.gateway = await getGateway();
|
|
37
37
|
|
package/core/eia-monitor.js
CHANGED
|
@@ -17,8 +17,9 @@ class EIAMonitor {
|
|
|
17
17
|
this.productUrls = this._parseUrls(this.env.EIA_PRODUCT_URLS);
|
|
18
18
|
this.competitorUrls = this._parseUrls(this.env.EIA_COMPETITOR_URLS);
|
|
19
19
|
this.targetProfitMargin = parseFloat(this.env.EIA_TARGET_PROFIT_MARGIN || '15'); // Default 15%
|
|
20
|
-
this.
|
|
21
|
-
|
|
20
|
+
this.baselineData = {};
|
|
21
|
+
this.scraperFactories = {
|
|
22
|
+
trendyol: () => new TrendyolScraper() // Initialize other scrapers here
|
|
22
23
|
};
|
|
23
24
|
log('INFO', 'EIA Monitor initialized');
|
|
24
25
|
}
|
|
@@ -101,9 +102,9 @@ class EIAMonitor {
|
|
|
101
102
|
*/
|
|
102
103
|
async startMonitoring(productId, platform, productUrl, dataType, cronSchedule) {
|
|
103
104
|
const jobName = `monitor-${platform}-${productId}-${dataType}`;
|
|
104
|
-
const
|
|
105
|
+
const createScraper = this.scraperFactories[platform];
|
|
105
106
|
|
|
106
|
-
if (!
|
|
107
|
+
if (!createScraper) {
|
|
107
108
|
log('ERROR', `No scraper found for platform: ${platform}`);
|
|
108
109
|
return;
|
|
109
110
|
}
|
|
@@ -111,6 +112,7 @@ class EIAMonitor {
|
|
|
111
112
|
const task = async () => {
|
|
112
113
|
log('INFO', `Monitoring ${dataType} for product ${productId} on ${platform}`);
|
|
113
114
|
let rawData;
|
|
115
|
+
const scraper = createScraper();
|
|
114
116
|
try {
|
|
115
117
|
// Scrape Data
|
|
116
118
|
switch (dataType) {
|
|
@@ -144,6 +146,12 @@ class EIAMonitor {
|
|
|
144
146
|
|
|
145
147
|
} catch (error) {
|
|
146
148
|
log('ERROR', `Error during monitoring job ${jobName}: ${error.message}`, { error });
|
|
149
|
+
} finally {
|
|
150
|
+
try {
|
|
151
|
+
await scraper.close();
|
|
152
|
+
} catch (e) {
|
|
153
|
+
log('WARN', `Failed to close scraper for ${jobName}: ${e.message}`);
|
|
154
|
+
}
|
|
147
155
|
}
|
|
148
156
|
};
|
|
149
157
|
|
|
@@ -165,10 +173,10 @@ class EIAMonitor {
|
|
|
165
173
|
}
|
|
166
174
|
|
|
167
175
|
// Simple example: if a price changes from baseline
|
|
168
|
-
if (dataType === 'price_changes' && currentInsights.currentPrice !== this.baselineData
|
|
176
|
+
if (dataType === 'price_changes' && currentInsights.currentPrice !== this.baselineData[dataType][productId].currentPrice) {
|
|
169
177
|
return {
|
|
170
178
|
type: 'price_change',
|
|
171
|
-
oldPrice: this.baselineData
|
|
179
|
+
oldPrice: this.baselineData[dataType][productId].currentPrice,
|
|
172
180
|
newPrice: currentInsights.currentPrice
|
|
173
181
|
};
|
|
174
182
|
}
|