ozon-grabber 0.1.0 → 0.1.4
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/dist/index.js +205 -56
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -103,6 +103,22 @@ var submitItems = async (config, items) => {
|
|
|
103
103
|
body: items
|
|
104
104
|
});
|
|
105
105
|
};
|
|
106
|
+
var getSetting = async (config, key) => {
|
|
107
|
+
const payload = await requestJson(config, `/settings/${key}`);
|
|
108
|
+
return payload.value;
|
|
109
|
+
};
|
|
110
|
+
var updateSetting = async (config, key, value) => {
|
|
111
|
+
await requestJson(config, `/settings/${key}`, {
|
|
112
|
+
method: "POST",
|
|
113
|
+
body: { value }
|
|
114
|
+
});
|
|
115
|
+
};
|
|
116
|
+
var markItemAsReturn = async (config, title, price) => {
|
|
117
|
+
return requestJson(config, "/items/mark-return", {
|
|
118
|
+
method: "POST",
|
|
119
|
+
body: { title, price }
|
|
120
|
+
});
|
|
121
|
+
};
|
|
106
122
|
|
|
107
123
|
// src/mcp.ts
|
|
108
124
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
@@ -203,7 +219,13 @@ var extractOrderItems = async (client, tools) => {
|
|
|
203
219
|
const isPriceText = (text) => /\u20BD|\\b\u0440\u0443\u0431\\.?\\b/i.test(text);
|
|
204
220
|
const isShowMoreText = (text) => /\u041F\u043E\u043A\u0430\u0437\u0430\u0442\u044C\\s+\u0435\u0449[\u0435\u0451]/i.test(text);
|
|
205
221
|
|
|
206
|
-
const
|
|
222
|
+
const isCancelled = (text) => {
|
|
223
|
+
const normalized = text.toLowerCase().replace(/\u0451/g, '\u0435');
|
|
224
|
+
return normalized.includes('\u043E\u0442\u043C\u0435\u043D\u0435\u043D');
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const shipments = Array.from(document.querySelectorAll('[data-widget="shipmentWidget"]'))
|
|
228
|
+
.filter((shipment) => !isCancelled(textOf(shipment)));
|
|
207
229
|
if (shipments.length === 0) return [];
|
|
208
230
|
|
|
209
231
|
const clickShowMore = async () => {
|
|
@@ -298,6 +320,73 @@ var extractOrderItems = async (client, tools) => {
|
|
|
298
320
|
};
|
|
299
321
|
}).filter((item) => item.title.length > 0);
|
|
300
322
|
};
|
|
323
|
+
var extractReturnItems = async (client, tools) => {
|
|
324
|
+
const evaluateTool = resolveToolName(tools, "evaluate_script");
|
|
325
|
+
const result = await client.callTool({
|
|
326
|
+
name: evaluateTool,
|
|
327
|
+
arguments: {
|
|
328
|
+
function: `() => {
|
|
329
|
+
const normalize = (value) => String(value ?? '').replace(/\\s+/g, ' ').trim();
|
|
330
|
+
const widget = document.querySelector('[data-widget="returnDetails"]');
|
|
331
|
+
if (!widget) return [];
|
|
332
|
+
|
|
333
|
+
const productLinks = Array.from(widget.querySelectorAll('a[href*="/product/"]'));
|
|
334
|
+
const items = [];
|
|
335
|
+
const seenTitles = new Set();
|
|
336
|
+
|
|
337
|
+
productLinks.forEach(link => {
|
|
338
|
+
const title = normalize(link.textContent);
|
|
339
|
+
if (!title || seenTitles.has(title)) return;
|
|
340
|
+
seenTitles.add(title);
|
|
341
|
+
|
|
342
|
+
// Find price nearby. Price is usually in a span containing \u20BD.
|
|
343
|
+
let root = link.parentElement;
|
|
344
|
+
let price = null;
|
|
345
|
+
for (let i = 0; i < 8; i++) {
|
|
346
|
+
if (!root) break;
|
|
347
|
+
const spans = Array.from(root.querySelectorAll('span'));
|
|
348
|
+
const priceSpan = spans.find(s => s.textContent.includes('\u20BD'));
|
|
349
|
+
if (priceSpan) {
|
|
350
|
+
price = normalize(priceSpan.textContent);
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
353
|
+
root = root.parentElement;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Image
|
|
357
|
+
let imageUrl = null;
|
|
358
|
+
root = link.parentElement;
|
|
359
|
+
for (let i = 0; i < 8; i++) {
|
|
360
|
+
if (!root) break;
|
|
361
|
+
const img = root.querySelector('img');
|
|
362
|
+
if (img) {
|
|
363
|
+
imageUrl = img.currentSrc || img.getAttribute('src');
|
|
364
|
+
break;
|
|
365
|
+
}
|
|
366
|
+
root = root.parentElement;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
items.push({ title, price, imageUrl });
|
|
370
|
+
});
|
|
371
|
+
return items;
|
|
372
|
+
}`
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
const payload = unwrapToolContent(result.content);
|
|
376
|
+
if (!Array.isArray(payload)) {
|
|
377
|
+
return [];
|
|
378
|
+
}
|
|
379
|
+
return payload.filter(isRecord2).map((item) => {
|
|
380
|
+
const title = typeof item.title === "string" ? item.title : "";
|
|
381
|
+
const price = typeof item.price === "string" ? item.price : null;
|
|
382
|
+
const imageUrl = typeof item.imageUrl === "string" ? item.imageUrl : void 0;
|
|
383
|
+
return {
|
|
384
|
+
title,
|
|
385
|
+
price,
|
|
386
|
+
imageUrl
|
|
387
|
+
};
|
|
388
|
+
}).filter((item) => item.title.length > 0);
|
|
389
|
+
};
|
|
301
390
|
|
|
302
391
|
// src/order-loop.ts
|
|
303
392
|
var DEFAULT_ORDER_URL = "https://www.ozon.ru/my/orderdetails/";
|
|
@@ -401,15 +490,28 @@ var parseStartOrder = (startOrder) => {
|
|
|
401
490
|
};
|
|
402
491
|
var formatOrderNumber = (orderNumber, width) => String(orderNumber).padStart(width, "0");
|
|
403
492
|
var buildOrderUrl = (orderId) => `${DEFAULT_ORDER_URL}?order=${encodeURIComponent(orderId)}&selectedTab=archive`;
|
|
404
|
-
var
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
];
|
|
412
|
-
|
|
493
|
+
var buildReturnUrl = (returnNumber) => `https://www.ozon.ru/my/returnDetails?returnNumber=${encodeURIComponent(returnNumber)}`;
|
|
494
|
+
var getReturnPageState = async (client, tools) => {
|
|
495
|
+
const evaluateTool = resolveToolName(tools, "evaluate_script");
|
|
496
|
+
const result = await client.callTool({
|
|
497
|
+
name: evaluateTool,
|
|
498
|
+
arguments: {
|
|
499
|
+
function: `() => {
|
|
500
|
+
const widget = document.querySelector('[data-widget="returnDetails"]');
|
|
501
|
+
return {
|
|
502
|
+
hasReturnWidget: Boolean(widget)
|
|
503
|
+
};
|
|
504
|
+
}`
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
const payload = unwrapToolContent2(result.content);
|
|
508
|
+
if (!isRecord3(payload)) {
|
|
509
|
+
return { hasReturnWidget: false };
|
|
510
|
+
}
|
|
511
|
+
return {
|
|
512
|
+
hasReturnWidget: Boolean(payload.hasReturnWidget)
|
|
513
|
+
};
|
|
514
|
+
};
|
|
413
515
|
var getOrderPageState = async (client, tools) => {
|
|
414
516
|
const evaluateTool = resolveToolName(tools, "evaluate_script");
|
|
415
517
|
const result = await client.callTool({
|
|
@@ -429,21 +531,14 @@ var getOrderPageState = async (client, tools) => {
|
|
|
429
531
|
});
|
|
430
532
|
const payload = unwrapToolContent2(result.content);
|
|
431
533
|
if (!isRecord3(payload)) {
|
|
432
|
-
return { hasItemsBlock: false, shipmentTexts: []
|
|
534
|
+
return { hasItemsBlock: false, shipmentTexts: [] };
|
|
433
535
|
}
|
|
434
536
|
const hasItemsBlock = Boolean(payload.hasItemsBlock);
|
|
435
537
|
const shipmentTextsRaw = Array.isArray(payload.shipmentTexts) ? payload.shipmentTexts.filter((entry) => typeof entry === "string") : [];
|
|
436
538
|
const shipmentTexts = shipmentTextsRaw.map((text) => normalizeText(text).toLowerCase());
|
|
437
|
-
const isInProgress = shipmentTexts.some((text) => {
|
|
438
|
-
if (text.includes(COMPLETED_MARKER)) {
|
|
439
|
-
return false;
|
|
440
|
-
}
|
|
441
|
-
return IN_PROGRESS_MARKERS.some((marker) => text.includes(marker));
|
|
442
|
-
});
|
|
443
539
|
return {
|
|
444
540
|
hasItemsBlock,
|
|
445
|
-
shipmentTexts
|
|
446
|
-
isInProgress
|
|
541
|
+
shipmentTexts
|
|
447
542
|
};
|
|
448
543
|
};
|
|
449
544
|
var getOrderTitleText = async (client, tools) => {
|
|
@@ -558,11 +653,32 @@ var scanOrders = async (client, tools, userId, startOrder, options = {}) => {
|
|
|
558
653
|
stopOrderNumber
|
|
559
654
|
};
|
|
560
655
|
}
|
|
561
|
-
const orderNumber = formatOrderNumber(numeric + offset, width);
|
|
656
|
+
const orderNumber = options.isReturns ? `R${numeric + offset}` : formatOrderNumber(numeric + offset, width);
|
|
562
657
|
const orderId = `${userId}-${orderNumber}`;
|
|
563
|
-
const url = buildOrderUrl(orderId);
|
|
564
|
-
logMessage(options, `Opening order ${orderId}`);
|
|
658
|
+
const url = options.isReturns ? buildReturnUrl(orderId) : buildOrderUrl(orderId);
|
|
659
|
+
logMessage(options, `Opening ${options.isReturns ? "return" : "order"} ${orderId}`);
|
|
565
660
|
await openPage(client, tools, url);
|
|
661
|
+
if (options.isReturns) {
|
|
662
|
+
const { hasReturnWidget } = await getReturnPageState(client, tools);
|
|
663
|
+
if (!hasReturnWidget) {
|
|
664
|
+
logMessage(options, `Return widget missing for ${orderId}, stopping.`);
|
|
665
|
+
return {
|
|
666
|
+
orders,
|
|
667
|
+
scannedOrders: orders.length,
|
|
668
|
+
stopReason: "missing",
|
|
669
|
+
stopOrderNumber: orderNumber
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
const items2 = await extractReturnItems(client, tools);
|
|
673
|
+
const orderDate2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
674
|
+
const order2 = { orderId, orderNumber, orderDate: orderDate2, items: items2 };
|
|
675
|
+
if (options.onOrder) {
|
|
676
|
+
await options.onOrder(order2);
|
|
677
|
+
}
|
|
678
|
+
orders.push(order2);
|
|
679
|
+
offset += 1;
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
566
682
|
await waitForOrderPageWidgets(
|
|
567
683
|
client,
|
|
568
684
|
tools,
|
|
@@ -605,15 +721,6 @@ var scanOrders = async (client, tools, userId, startOrder, options = {}) => {
|
|
|
605
721
|
stopOrderNumber: orderNumber
|
|
606
722
|
};
|
|
607
723
|
}
|
|
608
|
-
if (pageState.isInProgress) {
|
|
609
|
-
logMessage(options, `Order ${orderId} is in progress, stopping scan.`);
|
|
610
|
-
return {
|
|
611
|
-
orders,
|
|
612
|
-
scannedOrders: orders.length,
|
|
613
|
-
stopReason: "in-progress",
|
|
614
|
-
stopOrderNumber: orderNumber
|
|
615
|
-
};
|
|
616
|
-
}
|
|
617
724
|
if (!titleText) {
|
|
618
725
|
titleText = await getOrderTitleText(client, tools);
|
|
619
726
|
}
|
|
@@ -756,6 +863,10 @@ var cli = yargs(hideBin(process.argv)).scriptName("ozon-grabber").command(
|
|
|
756
863
|
type: "boolean",
|
|
757
864
|
default: false,
|
|
758
865
|
describe: "Enable backend upload and next-order resolution"
|
|
866
|
+
}).option("returns", {
|
|
867
|
+
type: "boolean",
|
|
868
|
+
default: false,
|
|
869
|
+
describe: "Scan for returns instead of orders"
|
|
759
870
|
});
|
|
760
871
|
return addBackendOptions(configured);
|
|
761
872
|
},
|
|
@@ -767,49 +878,87 @@ var cli = yargs(hideBin(process.argv)).scriptName("ozon-grabber").command(
|
|
|
767
878
|
const outputPath = argv.output;
|
|
768
879
|
const verbose = argv.verbose;
|
|
769
880
|
const useBackend = Boolean(argv.backend);
|
|
881
|
+
const isReturns = Boolean(argv.returns);
|
|
770
882
|
const backendConfig = useBackend ? resolveBackendConfig({
|
|
771
883
|
backendUrl: argv.backendUrl,
|
|
772
884
|
backendApiKey: argv.backendApiKey
|
|
773
885
|
}) : null;
|
|
774
|
-
|
|
886
|
+
let resolvedStartOrder = argv.startOrder;
|
|
887
|
+
if (isReturns) {
|
|
888
|
+
if (!resolvedStartOrder && backendConfig) {
|
|
889
|
+
const lastReturnId = await getSetting(backendConfig, "last_return_id");
|
|
890
|
+
if (lastReturnId) {
|
|
891
|
+
const match = lastReturnId.match(/^R(\d+)$/);
|
|
892
|
+
if (match) {
|
|
893
|
+
resolvedStartOrder = String(Number.parseInt(match[1], 10) + 1);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
if (!resolvedStartOrder) {
|
|
898
|
+
resolvedStartOrder = "1";
|
|
899
|
+
}
|
|
900
|
+
} else {
|
|
901
|
+
if (!resolvedStartOrder && backendConfig) {
|
|
902
|
+
resolvedStartOrder = await getNextOrderId(backendConfig);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
775
905
|
if (!resolvedStartOrder) {
|
|
776
|
-
throw new Error("Starting order number is required
|
|
906
|
+
throw new Error("Starting order number is required.");
|
|
777
907
|
}
|
|
778
|
-
if (backendConfig && !/^\d{4}$/.test(resolvedStartOrder.trim())) {
|
|
908
|
+
if (!isReturns && backendConfig && !/^\d{4}$/.test(resolvedStartOrder.trim())) {
|
|
779
909
|
throw new Error(
|
|
780
910
|
`Starting order must be a 4-digit string when using backend: ${resolvedStartOrder}.`
|
|
781
911
|
);
|
|
782
912
|
}
|
|
783
|
-
|
|
913
|
+
if (isReturns) {
|
|
914
|
+
console.log(`Starting returns scan for user: ${userId}, from: R${resolvedStartOrder}`);
|
|
915
|
+
} else {
|
|
916
|
+
console.log(`Starting orderId: ${resolvedStartOrder}`);
|
|
917
|
+
}
|
|
784
918
|
const { client, tools } = await connectChromeDevtoolsClient();
|
|
785
919
|
try {
|
|
786
920
|
const summary = await scanOrders(client, tools, userId, resolvedStartOrder, {
|
|
787
921
|
maxOrders,
|
|
788
922
|
pageLoadTimeoutMs,
|
|
789
923
|
verbose,
|
|
924
|
+
isReturns,
|
|
790
925
|
logger: (message) => console.log(message),
|
|
791
926
|
onOrder: backendConfig ? async (order) => {
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
927
|
+
if (isReturns) {
|
|
928
|
+
for (const item of order.items) {
|
|
929
|
+
try {
|
|
930
|
+
await markItemAsReturn(backendConfig, item.title, item.price);
|
|
931
|
+
console.log(`Marked item as return: ${item.title} (${item.price})`);
|
|
932
|
+
} catch (error) {
|
|
933
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
934
|
+
console.error(`Failed to mark item as return: ${message}`);
|
|
935
|
+
throw error;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
await updateSetting(backendConfig, "last_return_id", order.orderNumber);
|
|
939
|
+
} else {
|
|
940
|
+
const items = order.items.map((item) => ({
|
|
941
|
+
orderId: order.orderNumber,
|
|
942
|
+
userId,
|
|
943
|
+
orderDate: order.orderDate,
|
|
944
|
+
title: item.title,
|
|
945
|
+
price: item.price,
|
|
946
|
+
imageUrl: item.imageUrl
|
|
947
|
+
}));
|
|
948
|
+
if (items.length === 0) {
|
|
949
|
+
console.log(`Order ${order.orderId} has no items to submit.`);
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
try {
|
|
953
|
+
await submitItems(backendConfig, items);
|
|
954
|
+
console.log(
|
|
955
|
+
`Submitted order ${order.orderId} (${items.length} items) to backend.`
|
|
956
|
+
);
|
|
957
|
+
} catch (error) {
|
|
958
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
959
|
+
console.error(`Failed to submit order ${order.orderId}: ${message}`);
|
|
960
|
+
throw error;
|
|
961
|
+
}
|
|
813
962
|
}
|
|
814
963
|
} : void 0
|
|
815
964
|
});
|