vantuz 3.4.2 → 3.5.1
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/.env.example +21 -0
- package/.openclaw/completions/openclaw.bash +227 -0
- package/.openclaw/completions/openclaw.fish +1552 -0
- package/.openclaw/completions/openclaw.ps1 +1966 -0
- package/.openclaw/completions/openclaw.zsh +3571 -0
- package/.openclaw/gateway.cmd +10 -0
- package/.openclaw/identity/device.json +7 -0
- package/.openclaw/openclaw.json +40 -0
- package/.windsurf/workflows/vantuz-dev.md +31 -0
- package/DOCS_TR.md +80 -0
- package/LICENSE +45 -45
- package/README.md +52 -21
- package/cli.js +685 -585
- package/config.js +733 -733
- package/core/agent-loop.js +190 -190
- package/core/ai-provider.js +298 -261
- package/core/automation.js +523 -523
- package/core/brand-analyst.js +101 -0
- package/core/channels.js +167 -167
- package/core/dashboard.js +230 -230
- package/core/database.js +135 -37
- package/core/eia-monitor.js +3 -1
- package/core/engine.js +648 -636
- package/core/gateway.js +447 -447
- package/core/learning.js +214 -214
- package/core/license.js +113 -0
- package/core/marketplace-adapter.js +168 -168
- package/core/memory.js +190 -190
- package/core/migrations/001-initial-schema.sql +1 -1
- package/core/queue.js +120 -120
- package/core/self-healer.js +314 -314
- package/core/unified-product.js +214 -214
- package/core/vision-service.js +113 -113
- package/index.js +217 -174
- package/modules/crm/sentiment-crm.js +231 -231
- package/modules/healer/listing-healer.js +201 -201
- package/modules/oracle/predictor.js +214 -214
- package/modules/researcher/agent.js +169 -169
- package/modules/team/agents/base.js +92 -92
- package/modules/team/agents/dev.js +33 -33
- package/modules/team/agents/josh.js +40 -40
- package/modules/team/agents/marketing.js +33 -33
- package/modules/team/agents/milo.js +36 -36
- package/modules/team/index.js +78 -78
- package/modules/team/shared-memory.js +87 -87
- package/modules/war-room/competitor-tracker.js +250 -250
- package/modules/war-room/pricing-engine.js +308 -308
- package/n11docs.md +1680 -0
- package/nodes/warehouse.js +238 -238
- package/onboard.js +1 -1
- package/openclawdocs.md +3 -0
- package/package.json +7 -5
- package/platforms/pttavm.js +14 -14
- package/plugins/vantuz/index.js +528 -528
- package/plugins/vantuz/memory/hippocampus.js +465 -465
- package/plugins/vantuz/package.json +20 -20
- package/plugins/vantuz/platforms/_template.js +118 -118
- package/plugins/vantuz/platforms/amazon.js +236 -236
- package/plugins/vantuz/platforms/ciceksepeti.js +166 -166
- package/plugins/vantuz/platforms/hepsiburada.js +180 -180
- package/plugins/vantuz/platforms/index.js +165 -165
- package/plugins/vantuz/platforms/n11.js +229 -229
- package/plugins/vantuz/platforms/pazarama.js +154 -154
- package/plugins/vantuz/platforms/pttavm.js +127 -127
- package/plugins/vantuz/platforms/trendyol.js +326 -326
- package/plugins/vantuz/services/alerts.js +253 -253
- package/plugins/vantuz/services/license.js +34 -34
- package/plugins/vantuz/services/scheduler.js +232 -232
- package/plugins/vantuz/tools/analytics.js +152 -152
- package/plugins/vantuz/tools/crossborder.js +187 -187
- package/plugins/vantuz/tools/nl-parser.js +211 -211
- package/plugins/vantuz/tools/product.js +110 -110
- package/plugins/vantuz/tools/quick-report.js +175 -175
- package/plugins/vantuz/tools/repricer.js +314 -314
- package/plugins/vantuz/tools/sentiment.js +115 -115
- package/plugins/vantuz/tools/vision.js +257 -257
- package/public.pem +9 -0
- package/server/app.js +260 -260
- package/server/public/index.html +514 -514
- package/start.bat +33 -33
- package/vantuz.sqlite +0 -0
- package/workspace/AGENTS.md +73 -0
- package/workspace/BRAND.md +29 -0
- package/workspace/SOUL.md +72 -0
- package/workspace/team/DECISIONS.md +3 -0
- package/workspace/team/GOALS.md +3 -0
- package/workspace/team/PROJECT_STATUS.md +3 -0
- package/workspace/team/agents/dev/SOUL.md +12 -0
- package/workspace/team/agents/josh/SOUL.md +12 -0
- package/workspace/team/agents/marketing/SOUL.md +12 -0
- package/workspace/team/agents/milo/SOUL.md +12 -0
- package/vantuz-3.3.4.tgz +0 -0
package/core/unified-product.js
CHANGED
|
@@ -1,214 +1,214 @@
|
|
|
1
|
-
// core/unified-product.js
|
|
2
|
-
// Unified Product Model for Vantuz OS V2
|
|
3
|
-
// Normalizes product data from ALL platforms into one Vantuz format.
|
|
4
|
-
|
|
5
|
-
import { log } from './ai-provider.js';
|
|
6
|
-
|
|
7
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
8
|
-
// UNIFIED PRODUCT SCHEMA
|
|
9
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Creates a Unified Product from raw platform data.
|
|
13
|
-
* @param {object} raw - Raw product data from any platform API.
|
|
14
|
-
* @param {string} platform - Source platform name.
|
|
15
|
-
* @returns {object} Unified Product.
|
|
16
|
-
*/
|
|
17
|
-
export function normalizeProduct(raw, platform) {
|
|
18
|
-
const normalized = {
|
|
19
|
-
// Identity
|
|
20
|
-
sku: raw.sku || raw.stockCode || raw.merchantSku || raw.productCode || null,
|
|
21
|
-
barcode: raw.barcode || raw.barcodes?.[0] || raw.gtin || null,
|
|
22
|
-
title: raw.title || raw.productName || raw.name || '',
|
|
23
|
-
brand: raw.brand || raw.brandName || '',
|
|
24
|
-
category: raw.category || raw.categoryName || '',
|
|
25
|
-
|
|
26
|
-
// Pricing
|
|
27
|
-
price: parseFloat(raw.salePrice || raw.price || raw.listingPrice || 0),
|
|
28
|
-
listPrice: parseFloat(raw.listPrice || raw.marketPrice || raw.originalPrice || 0),
|
|
29
|
-
cost: parseFloat(raw.cost || raw.costPrice || 0),
|
|
30
|
-
currency: raw.currency || 'TRY',
|
|
31
|
-
|
|
32
|
-
// Stock
|
|
33
|
-
stock: parseInt(raw.quantity || raw.stock || raw.stockQuantity || 0, 10),
|
|
34
|
-
|
|
35
|
-
// Images
|
|
36
|
-
images: raw.images || raw.imageUrls || (raw.imageUrl ? [raw.imageUrl] : []),
|
|
37
|
-
|
|
38
|
-
// Status
|
|
39
|
-
onSale: raw.onSale ?? raw.approved ?? raw.active ?? true,
|
|
40
|
-
|
|
41
|
-
// Platform-specific data
|
|
42
|
-
platforms: {
|
|
43
|
-
[platform]: {
|
|
44
|
-
id: raw.id || raw.productId || raw.contentId || null,
|
|
45
|
-
url: raw.url || raw.productUrl || null,
|
|
46
|
-
lastSync: new Date().toISOString(),
|
|
47
|
-
raw: raw // Keep original for debugging
|
|
48
|
-
}
|
|
49
|
-
},
|
|
50
|
-
|
|
51
|
-
// Analytics (populated by other modules)
|
|
52
|
-
competitors: [],
|
|
53
|
-
salesVelocity: 0, // units/day — filled by Oracle
|
|
54
|
-
margin: 0, // calculated below
|
|
55
|
-
stockoutDate: null, // filled by Oracle
|
|
56
|
-
sentimentScore: null, // filled by CRM
|
|
57
|
-
|
|
58
|
-
// Meta
|
|
59
|
-
_source: platform,
|
|
60
|
-
_normalizedAt: new Date().toISOString()
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
// Calculate margin
|
|
64
|
-
if (normalized.cost > 0 && normalized.price > 0) {
|
|
65
|
-
normalized.margin = ((normalized.price - normalized.cost) / normalized.price) * 100;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return normalized;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Merge a new platform occurrence into an existing unified product.
|
|
73
|
-
* (Same barcode, different platform.)
|
|
74
|
-
* @param {object} existing - Existing unified product.
|
|
75
|
-
* @param {object} raw - New raw data.
|
|
76
|
-
* @param {string} platform - Source platform.
|
|
77
|
-
* @returns {object} Merged product.
|
|
78
|
-
*/
|
|
79
|
-
export function mergeProduct(existing, raw, platform) {
|
|
80
|
-
const incoming = normalizeProduct(raw, platform);
|
|
81
|
-
|
|
82
|
-
// Add to platforms map
|
|
83
|
-
existing.platforms[platform] = incoming.platforms[platform];
|
|
84
|
-
|
|
85
|
-
// Use the lowest price as the "effective" price
|
|
86
|
-
if (incoming.price > 0 && (incoming.price < existing.price || existing.price === 0)) {
|
|
87
|
-
existing.price = incoming.price;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Sum stock across platforms
|
|
91
|
-
existing.stock = Object.values(existing.platforms).reduce((sum, p) => {
|
|
92
|
-
const pStock = parseInt(p.raw?.quantity || p.raw?.stock || 0, 10);
|
|
93
|
-
return sum + pStock;
|
|
94
|
-
}, 0);
|
|
95
|
-
|
|
96
|
-
// Use best title (longest, usually most SEO-friendly)
|
|
97
|
-
if (incoming.title.length > existing.title.length) {
|
|
98
|
-
existing.title = incoming.title;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Merge images (unique)
|
|
102
|
-
const allImages = [...(existing.images || []), ...(incoming.images || [])];
|
|
103
|
-
existing.images = [...new Set(allImages)];
|
|
104
|
-
|
|
105
|
-
// Recalculate margin
|
|
106
|
-
if (existing.cost > 0 && existing.price > 0) {
|
|
107
|
-
existing.margin = ((existing.price - existing.cost) / existing.price) * 100;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
return existing;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
114
|
-
// PRODUCT CATALOG (In-Memory + Disk)
|
|
115
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
116
|
-
|
|
117
|
-
import fs from 'fs';
|
|
118
|
-
import path from 'path';
|
|
119
|
-
import os from 'os';
|
|
120
|
-
|
|
121
|
-
const CATALOG_FILE = path.join(os.homedir(), '.vantuz', 'memory', 'catalog.json');
|
|
122
|
-
|
|
123
|
-
class ProductCatalog {
|
|
124
|
-
constructor() {
|
|
125
|
-
this.products = new Map(); // barcode -> UnifiedProduct
|
|
126
|
-
this._load();
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
_load() {
|
|
130
|
-
try {
|
|
131
|
-
if (fs.existsSync(CATALOG_FILE)) {
|
|
132
|
-
const data = JSON.parse(fs.readFileSync(CATALOG_FILE, 'utf-8'));
|
|
133
|
-
for (const [barcode, product] of Object.entries(data)) {
|
|
134
|
-
this.products.set(barcode, product);
|
|
135
|
-
}
|
|
136
|
-
log('INFO', `Product catalog loaded`, { count: this.products.size });
|
|
137
|
-
}
|
|
138
|
-
} catch (e) {
|
|
139
|
-
log('WARN', 'Catalog load failed', { error: e.message });
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
_save() {
|
|
144
|
-
const dir = path.dirname(CATALOG_FILE);
|
|
145
|
-
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
146
|
-
const obj = Object.fromEntries(this.products);
|
|
147
|
-
const tmp = CATALOG_FILE + '.tmp';
|
|
148
|
-
fs.writeFileSync(tmp, JSON.stringify(obj, null, 2), 'utf-8');
|
|
149
|
-
fs.renameSync(tmp, CATALOG_FILE);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Ingest raw product data from a platform.
|
|
154
|
-
*/
|
|
155
|
-
ingest(rawProducts, platform) {
|
|
156
|
-
let added = 0, updated = 0;
|
|
157
|
-
|
|
158
|
-
for (const raw of rawProducts) {
|
|
159
|
-
const barcode = raw.barcode || raw.barcodes?.[0] || raw.gtin;
|
|
160
|
-
if (!barcode) continue;
|
|
161
|
-
|
|
162
|
-
if (this.products.has(barcode)) {
|
|
163
|
-
mergeProduct(this.products.get(barcode), raw, platform);
|
|
164
|
-
updated++;
|
|
165
|
-
} else {
|
|
166
|
-
this.products.set(barcode, normalizeProduct(raw, platform));
|
|
167
|
-
added++;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
this._save();
|
|
172
|
-
log('INFO', `Catalog ingested from ${platform}`, { added, updated, total: this.products.size });
|
|
173
|
-
return { added, updated, total: this.products.size };
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
get(barcode) {
|
|
177
|
-
return this.products.get(barcode) || null;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
getAll() {
|
|
181
|
-
return [...this.products.values()];
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Find products that are low on stock.
|
|
186
|
-
* @param {number} threshold - Min stock threshold (default: 5).
|
|
187
|
-
*/
|
|
188
|
-
getLowStock(threshold = 5) {
|
|
189
|
-
return this.getAll().filter(p => p.stock > 0 && p.stock <= threshold);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Find products below target margin.
|
|
194
|
-
* @param {number} minMargin - Minimum margin % (default: 15).
|
|
195
|
-
*/
|
|
196
|
-
getLowMargin(minMargin = 15) {
|
|
197
|
-
return this.getAll().filter(p => p.margin > 0 && p.margin < minMargin);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
get size() {
|
|
201
|
-
return this.products.size;
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
let catalogInstance = null;
|
|
206
|
-
|
|
207
|
-
export function getCatalog() {
|
|
208
|
-
if (!catalogInstance) {
|
|
209
|
-
catalogInstance = new ProductCatalog();
|
|
210
|
-
}
|
|
211
|
-
return catalogInstance;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
export default ProductCatalog;
|
|
1
|
+
// core/unified-product.js
|
|
2
|
+
// Unified Product Model for Vantuz OS V2
|
|
3
|
+
// Normalizes product data from ALL platforms into one Vantuz format.
|
|
4
|
+
|
|
5
|
+
import { log } from './ai-provider.js';
|
|
6
|
+
|
|
7
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
8
|
+
// UNIFIED PRODUCT SCHEMA
|
|
9
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Creates a Unified Product from raw platform data.
|
|
13
|
+
* @param {object} raw - Raw product data from any platform API.
|
|
14
|
+
* @param {string} platform - Source platform name.
|
|
15
|
+
* @returns {object} Unified Product.
|
|
16
|
+
*/
|
|
17
|
+
export function normalizeProduct(raw, platform) {
|
|
18
|
+
const normalized = {
|
|
19
|
+
// Identity
|
|
20
|
+
sku: raw.sku || raw.stockCode || raw.merchantSku || raw.productCode || null,
|
|
21
|
+
barcode: raw.barcode || raw.barcodes?.[0] || raw.gtin || null,
|
|
22
|
+
title: raw.title || raw.productName || raw.name || '',
|
|
23
|
+
brand: raw.brand || raw.brandName || '',
|
|
24
|
+
category: raw.category || raw.categoryName || '',
|
|
25
|
+
|
|
26
|
+
// Pricing
|
|
27
|
+
price: parseFloat(raw.salePrice || raw.price || raw.listingPrice || 0),
|
|
28
|
+
listPrice: parseFloat(raw.listPrice || raw.marketPrice || raw.originalPrice || 0),
|
|
29
|
+
cost: parseFloat(raw.cost || raw.costPrice || 0),
|
|
30
|
+
currency: raw.currency || 'TRY',
|
|
31
|
+
|
|
32
|
+
// Stock
|
|
33
|
+
stock: parseInt(raw.quantity || raw.stock || raw.stockQuantity || 0, 10),
|
|
34
|
+
|
|
35
|
+
// Images
|
|
36
|
+
images: raw.images || raw.imageUrls || (raw.imageUrl ? [raw.imageUrl] : []),
|
|
37
|
+
|
|
38
|
+
// Status
|
|
39
|
+
onSale: raw.onSale ?? raw.approved ?? raw.active ?? true,
|
|
40
|
+
|
|
41
|
+
// Platform-specific data
|
|
42
|
+
platforms: {
|
|
43
|
+
[platform]: {
|
|
44
|
+
id: raw.id || raw.productId || raw.contentId || null,
|
|
45
|
+
url: raw.url || raw.productUrl || null,
|
|
46
|
+
lastSync: new Date().toISOString(),
|
|
47
|
+
raw: raw // Keep original for debugging
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
// Analytics (populated by other modules)
|
|
52
|
+
competitors: [],
|
|
53
|
+
salesVelocity: 0, // units/day — filled by Oracle
|
|
54
|
+
margin: 0, // calculated below
|
|
55
|
+
stockoutDate: null, // filled by Oracle
|
|
56
|
+
sentimentScore: null, // filled by CRM
|
|
57
|
+
|
|
58
|
+
// Meta
|
|
59
|
+
_source: platform,
|
|
60
|
+
_normalizedAt: new Date().toISOString()
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// Calculate margin
|
|
64
|
+
if (normalized.cost > 0 && normalized.price > 0) {
|
|
65
|
+
normalized.margin = ((normalized.price - normalized.cost) / normalized.price) * 100;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return normalized;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Merge a new platform occurrence into an existing unified product.
|
|
73
|
+
* (Same barcode, different platform.)
|
|
74
|
+
* @param {object} existing - Existing unified product.
|
|
75
|
+
* @param {object} raw - New raw data.
|
|
76
|
+
* @param {string} platform - Source platform.
|
|
77
|
+
* @returns {object} Merged product.
|
|
78
|
+
*/
|
|
79
|
+
export function mergeProduct(existing, raw, platform) {
|
|
80
|
+
const incoming = normalizeProduct(raw, platform);
|
|
81
|
+
|
|
82
|
+
// Add to platforms map
|
|
83
|
+
existing.platforms[platform] = incoming.platforms[platform];
|
|
84
|
+
|
|
85
|
+
// Use the lowest price as the "effective" price
|
|
86
|
+
if (incoming.price > 0 && (incoming.price < existing.price || existing.price === 0)) {
|
|
87
|
+
existing.price = incoming.price;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Sum stock across platforms
|
|
91
|
+
existing.stock = Object.values(existing.platforms).reduce((sum, p) => {
|
|
92
|
+
const pStock = parseInt(p.raw?.quantity || p.raw?.stock || 0, 10);
|
|
93
|
+
return sum + pStock;
|
|
94
|
+
}, 0);
|
|
95
|
+
|
|
96
|
+
// Use best title (longest, usually most SEO-friendly)
|
|
97
|
+
if (incoming.title.length > existing.title.length) {
|
|
98
|
+
existing.title = incoming.title;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Merge images (unique)
|
|
102
|
+
const allImages = [...(existing.images || []), ...(incoming.images || [])];
|
|
103
|
+
existing.images = [...new Set(allImages)];
|
|
104
|
+
|
|
105
|
+
// Recalculate margin
|
|
106
|
+
if (existing.cost > 0 && existing.price > 0) {
|
|
107
|
+
existing.margin = ((existing.price - existing.cost) / existing.price) * 100;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return existing;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
114
|
+
// PRODUCT CATALOG (In-Memory + Disk)
|
|
115
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
116
|
+
|
|
117
|
+
import fs from 'fs';
|
|
118
|
+
import path from 'path';
|
|
119
|
+
import os from 'os';
|
|
120
|
+
|
|
121
|
+
const CATALOG_FILE = path.join(os.homedir(), '.vantuz', 'memory', 'catalog.json');
|
|
122
|
+
|
|
123
|
+
class ProductCatalog {
|
|
124
|
+
constructor() {
|
|
125
|
+
this.products = new Map(); // barcode -> UnifiedProduct
|
|
126
|
+
this._load();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
_load() {
|
|
130
|
+
try {
|
|
131
|
+
if (fs.existsSync(CATALOG_FILE)) {
|
|
132
|
+
const data = JSON.parse(fs.readFileSync(CATALOG_FILE, 'utf-8'));
|
|
133
|
+
for (const [barcode, product] of Object.entries(data)) {
|
|
134
|
+
this.products.set(barcode, product);
|
|
135
|
+
}
|
|
136
|
+
log('INFO', `Product catalog loaded`, { count: this.products.size });
|
|
137
|
+
}
|
|
138
|
+
} catch (e) {
|
|
139
|
+
log('WARN', 'Catalog load failed', { error: e.message });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
_save() {
|
|
144
|
+
const dir = path.dirname(CATALOG_FILE);
|
|
145
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
146
|
+
const obj = Object.fromEntries(this.products);
|
|
147
|
+
const tmp = CATALOG_FILE + '.tmp';
|
|
148
|
+
fs.writeFileSync(tmp, JSON.stringify(obj, null, 2), 'utf-8');
|
|
149
|
+
fs.renameSync(tmp, CATALOG_FILE);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Ingest raw product data from a platform.
|
|
154
|
+
*/
|
|
155
|
+
ingest(rawProducts, platform) {
|
|
156
|
+
let added = 0, updated = 0;
|
|
157
|
+
|
|
158
|
+
for (const raw of rawProducts) {
|
|
159
|
+
const barcode = raw.barcode || raw.barcodes?.[0] || raw.gtin;
|
|
160
|
+
if (!barcode) continue;
|
|
161
|
+
|
|
162
|
+
if (this.products.has(barcode)) {
|
|
163
|
+
mergeProduct(this.products.get(barcode), raw, platform);
|
|
164
|
+
updated++;
|
|
165
|
+
} else {
|
|
166
|
+
this.products.set(barcode, normalizeProduct(raw, platform));
|
|
167
|
+
added++;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
this._save();
|
|
172
|
+
log('INFO', `Catalog ingested from ${platform}`, { added, updated, total: this.products.size });
|
|
173
|
+
return { added, updated, total: this.products.size };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
get(barcode) {
|
|
177
|
+
return this.products.get(barcode) || null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
getAll() {
|
|
181
|
+
return [...this.products.values()];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Find products that are low on stock.
|
|
186
|
+
* @param {number} threshold - Min stock threshold (default: 5).
|
|
187
|
+
*/
|
|
188
|
+
getLowStock(threshold = 5) {
|
|
189
|
+
return this.getAll().filter(p => p.stock > 0 && p.stock <= threshold);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Find products below target margin.
|
|
194
|
+
* @param {number} minMargin - Minimum margin % (default: 15).
|
|
195
|
+
*/
|
|
196
|
+
getLowMargin(minMargin = 15) {
|
|
197
|
+
return this.getAll().filter(p => p.margin > 0 && p.margin < minMargin);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
get size() {
|
|
201
|
+
return this.products.size;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
let catalogInstance = null;
|
|
206
|
+
|
|
207
|
+
export function getCatalog() {
|
|
208
|
+
if (!catalogInstance) {
|
|
209
|
+
catalogInstance = new ProductCatalog();
|
|
210
|
+
}
|
|
211
|
+
return catalogInstance;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export default ProductCatalog;
|
package/core/vision-service.js
CHANGED
|
@@ -1,113 +1,113 @@
|
|
|
1
|
-
// core/vision-service.js
|
|
2
|
-
// Decoupled Vision AI Service
|
|
3
|
-
// Extracted from plugins/vantuz/tools/vision.js for API/Gateway access.
|
|
4
|
-
// Can be called from CLI, API endpoint, or Warehouse Node.
|
|
5
|
-
|
|
6
|
-
import axios from 'axios';
|
|
7
|
-
import fs from 'fs';
|
|
8
|
-
import { log } from './ai-provider.js';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Analyze a product image and return structured data.
|
|
12
|
-
* Accepts: file path, URL, or raw Base64 string.
|
|
13
|
-
*
|
|
14
|
-
* @param {string} imageInput - File path, URL, or base64 data URI.
|
|
15
|
-
* @param {object} aiConfig - { apiKey, baseUrl?, model? }
|
|
16
|
-
* @returns {object} { detected, confidence, attributes, suggestedPrice, seo_keywords }
|
|
17
|
-
*/
|
|
18
|
-
export async function analyzeProductImage(imageInput, aiConfig) {
|
|
19
|
-
if (!aiConfig?.apiKey) {
|
|
20
|
-
throw new Error('AI API anahtarı gerekli (aiConfig.apiKey)');
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// Normalize image to base64 data URI
|
|
24
|
-
let imageData = imageInput;
|
|
25
|
-
|
|
26
|
-
if (imageInput.startsWith('http://') || imageInput.startsWith('https://')) {
|
|
27
|
-
log('INFO', 'Vision: Downloading image from URL...');
|
|
28
|
-
const response = await axios.get(imageInput, { responseType: 'arraybuffer', timeout: 15000 });
|
|
29
|
-
imageData = `data:image/jpeg;base64,${Buffer.from(response.data).toString('base64')}`;
|
|
30
|
-
} else if (imageInput.startsWith('data:')) {
|
|
31
|
-
// Already a data URI
|
|
32
|
-
imageData = imageInput;
|
|
33
|
-
} else if (fs.existsSync(imageInput)) {
|
|
34
|
-
log('INFO', 'Vision: Reading local file...');
|
|
35
|
-
const buffer = fs.readFileSync(imageInput);
|
|
36
|
-
imageData = `data:image/jpeg;base64,${buffer.toString('base64')}`;
|
|
37
|
-
} else {
|
|
38
|
-
// Assume raw base64 string
|
|
39
|
-
imageData = `data:image/jpeg;base64,${imageInput}`;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const model = aiConfig.model || 'gpt-4o';
|
|
43
|
-
const baseUrl = aiConfig.baseUrl || 'https://api.openai.com/v1';
|
|
44
|
-
|
|
45
|
-
log('INFO', `Vision: Analyzing with ${model}...`);
|
|
46
|
-
|
|
47
|
-
const response = await axios.post(`${baseUrl}/chat/completions`, {
|
|
48
|
-
model,
|
|
49
|
-
messages: [
|
|
50
|
-
{
|
|
51
|
-
role: 'user',
|
|
52
|
-
content: [
|
|
53
|
-
{
|
|
54
|
-
type: 'text',
|
|
55
|
-
text: `Bu ürün fotoğrafını analiz et. JSON formatında yanıt ver:
|
|
56
|
-
{
|
|
57
|
-
"detected": "Ürün tipi",
|
|
58
|
-
"confidence": 0.95,
|
|
59
|
-
"attributes": {
|
|
60
|
-
"color": "Renk",
|
|
61
|
-
"material": "Malzeme",
|
|
62
|
-
"style": "Stil",
|
|
63
|
-
"pattern": "Desen",
|
|
64
|
-
"condition": "Durum (yeni/kullanılmış/hasarlı)"
|
|
65
|
-
},
|
|
66
|
-
"suggestedPrice": { "min": 100, "max": 200, "optimal": 149 },
|
|
67
|
-
"seo_keywords": ["anahtar1", "anahtar2"],
|
|
68
|
-
"defects": "Görünür hasar/kusur varsa açıkla, yoksa null"
|
|
69
|
-
}`
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
type: 'image_url',
|
|
73
|
-
image_url: { url: imageData }
|
|
74
|
-
}
|
|
75
|
-
]
|
|
76
|
-
}
|
|
77
|
-
],
|
|
78
|
-
max_tokens: 1000
|
|
79
|
-
}, {
|
|
80
|
-
headers: {
|
|
81
|
-
'Authorization': `Bearer ${aiConfig.apiKey}`,
|
|
82
|
-
'Content-Type': 'application/json'
|
|
83
|
-
},
|
|
84
|
-
timeout: 30000
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
const content = response.data.choices[0].message.content;
|
|
88
|
-
const jsonMatch = content.match(/\{[\s\S]*\}/);
|
|
89
|
-
if (jsonMatch) {
|
|
90
|
-
const result = JSON.parse(jsonMatch[0]);
|
|
91
|
-
log('INFO', 'Vision: Analysis complete', { detected: result.detected });
|
|
92
|
-
return result;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
throw new Error('AI Vision yanıtı parse edilemedi');
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Quick damage check for returns.
|
|
100
|
-
* @param {string} imageInput - Image path/URL/base64.
|
|
101
|
-
* @param {object} aiConfig - AI config.
|
|
102
|
-
* @returns {object} { isDamaged, severity, description }
|
|
103
|
-
*/
|
|
104
|
-
export async function checkReturnDamage(imageInput, aiConfig) {
|
|
105
|
-
const analysis = await analyzeProductImage(imageInput, aiConfig);
|
|
106
|
-
return {
|
|
107
|
-
isDamaged: !!analysis.defects,
|
|
108
|
-
severity: analysis.defects ? 'check_required' : 'ok',
|
|
109
|
-
condition: analysis.attributes?.condition || 'unknown',
|
|
110
|
-
description: analysis.defects || 'Görünür hasar tespit edilmedi.',
|
|
111
|
-
fullAnalysis: analysis
|
|
112
|
-
};
|
|
113
|
-
}
|
|
1
|
+
// core/vision-service.js
|
|
2
|
+
// Decoupled Vision AI Service
|
|
3
|
+
// Extracted from plugins/vantuz/tools/vision.js for API/Gateway access.
|
|
4
|
+
// Can be called from CLI, API endpoint, or Warehouse Node.
|
|
5
|
+
|
|
6
|
+
import axios from 'axios';
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import { log } from './ai-provider.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Analyze a product image and return structured data.
|
|
12
|
+
* Accepts: file path, URL, or raw Base64 string.
|
|
13
|
+
*
|
|
14
|
+
* @param {string} imageInput - File path, URL, or base64 data URI.
|
|
15
|
+
* @param {object} aiConfig - { apiKey, baseUrl?, model? }
|
|
16
|
+
* @returns {object} { detected, confidence, attributes, suggestedPrice, seo_keywords }
|
|
17
|
+
*/
|
|
18
|
+
export async function analyzeProductImage(imageInput, aiConfig) {
|
|
19
|
+
if (!aiConfig?.apiKey) {
|
|
20
|
+
throw new Error('AI API anahtarı gerekli (aiConfig.apiKey)');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Normalize image to base64 data URI
|
|
24
|
+
let imageData = imageInput;
|
|
25
|
+
|
|
26
|
+
if (imageInput.startsWith('http://') || imageInput.startsWith('https://')) {
|
|
27
|
+
log('INFO', 'Vision: Downloading image from URL...');
|
|
28
|
+
const response = await axios.get(imageInput, { responseType: 'arraybuffer', timeout: 15000 });
|
|
29
|
+
imageData = `data:image/jpeg;base64,${Buffer.from(response.data).toString('base64')}`;
|
|
30
|
+
} else if (imageInput.startsWith('data:')) {
|
|
31
|
+
// Already a data URI
|
|
32
|
+
imageData = imageInput;
|
|
33
|
+
} else if (fs.existsSync(imageInput)) {
|
|
34
|
+
log('INFO', 'Vision: Reading local file...');
|
|
35
|
+
const buffer = fs.readFileSync(imageInput);
|
|
36
|
+
imageData = `data:image/jpeg;base64,${buffer.toString('base64')}`;
|
|
37
|
+
} else {
|
|
38
|
+
// Assume raw base64 string
|
|
39
|
+
imageData = `data:image/jpeg;base64,${imageInput}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const model = aiConfig.model || 'gpt-4o';
|
|
43
|
+
const baseUrl = aiConfig.baseUrl || 'https://api.openai.com/v1';
|
|
44
|
+
|
|
45
|
+
log('INFO', `Vision: Analyzing with ${model}...`);
|
|
46
|
+
|
|
47
|
+
const response = await axios.post(`${baseUrl}/chat/completions`, {
|
|
48
|
+
model,
|
|
49
|
+
messages: [
|
|
50
|
+
{
|
|
51
|
+
role: 'user',
|
|
52
|
+
content: [
|
|
53
|
+
{
|
|
54
|
+
type: 'text',
|
|
55
|
+
text: `Bu ürün fotoğrafını analiz et. JSON formatında yanıt ver:
|
|
56
|
+
{
|
|
57
|
+
"detected": "Ürün tipi",
|
|
58
|
+
"confidence": 0.95,
|
|
59
|
+
"attributes": {
|
|
60
|
+
"color": "Renk",
|
|
61
|
+
"material": "Malzeme",
|
|
62
|
+
"style": "Stil",
|
|
63
|
+
"pattern": "Desen",
|
|
64
|
+
"condition": "Durum (yeni/kullanılmış/hasarlı)"
|
|
65
|
+
},
|
|
66
|
+
"suggestedPrice": { "min": 100, "max": 200, "optimal": 149 },
|
|
67
|
+
"seo_keywords": ["anahtar1", "anahtar2"],
|
|
68
|
+
"defects": "Görünür hasar/kusur varsa açıkla, yoksa null"
|
|
69
|
+
}`
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
type: 'image_url',
|
|
73
|
+
image_url: { url: imageData }
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
],
|
|
78
|
+
max_tokens: 1000
|
|
79
|
+
}, {
|
|
80
|
+
headers: {
|
|
81
|
+
'Authorization': `Bearer ${aiConfig.apiKey}`,
|
|
82
|
+
'Content-Type': 'application/json'
|
|
83
|
+
},
|
|
84
|
+
timeout: 30000
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const content = response.data.choices[0].message.content;
|
|
88
|
+
const jsonMatch = content.match(/\{[\s\S]*\}/);
|
|
89
|
+
if (jsonMatch) {
|
|
90
|
+
const result = JSON.parse(jsonMatch[0]);
|
|
91
|
+
log('INFO', 'Vision: Analysis complete', { detected: result.detected });
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
throw new Error('AI Vision yanıtı parse edilemedi');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Quick damage check for returns.
|
|
100
|
+
* @param {string} imageInput - Image path/URL/base64.
|
|
101
|
+
* @param {object} aiConfig - AI config.
|
|
102
|
+
* @returns {object} { isDamaged, severity, description }
|
|
103
|
+
*/
|
|
104
|
+
export async function checkReturnDamage(imageInput, aiConfig) {
|
|
105
|
+
const analysis = await analyzeProductImage(imageInput, aiConfig);
|
|
106
|
+
return {
|
|
107
|
+
isDamaged: !!analysis.defects,
|
|
108
|
+
severity: analysis.defects ? 'check_required' : 'ok',
|
|
109
|
+
condition: analysis.attributes?.condition || 'unknown',
|
|
110
|
+
description: analysis.defects || 'Görünür hasar tespit edilmedi.',
|
|
111
|
+
fullAnalysis: analysis
|
|
112
|
+
};
|
|
113
|
+
}
|