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.
Files changed (2) hide show
  1. package/dist/index.js +205 -56
  2. 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 shipments = Array.from(document.querySelectorAll('[data-widget="shipmentWidget"]'));
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 IN_PROGRESS_MARKERS = [
405
- "\u043C\u043E\u0436\u043D\u043E \u0437\u0430\u0431\u0438\u0440\u0430\u0442\u044C",
406
- "\u043F\u0435\u0440\u0435\u0434\u0430\u0451\u0442\u0441\u044F \u0432 \u0434\u043E\u0441\u0442\u0430\u0432\u043A\u0443",
407
- "\u043F\u0435\u0440\u0435\u0434\u0430\u0435\u0442\u0441\u044F \u0432 \u0434\u043E\u0441\u0442\u0430\u0432\u043A\u0443",
408
- "\u0432 \u0441\u043B\u0443\u0436\u0431\u0435 \u0434\u043E\u0441\u0442\u0430\u0432\u043A\u0438",
409
- "\u0432 \u0441\u0431\u043E\u0440\u043A\u0435",
410
- "\u0432 \u043F\u0443\u0442\u0438"
411
- ];
412
- var COMPLETED_MARKER = "\u043F\u043E\u043B\u0443\u0447\u0435\u043D";
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: [], isInProgress: false };
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
- const resolvedStartOrder = argv.startOrder ?? (backendConfig ? await getNextOrderId(backendConfig) : void 0);
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 when backend is disabled.");
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
- console.log(`Starting orderId: ${resolvedStartOrder}`);
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
- const items = order.items.map((item) => ({
793
- orderId: order.orderNumber,
794
- userId,
795
- orderDate: order.orderDate,
796
- title: item.title,
797
- price: item.price,
798
- imageUrl: item.imageUrl
799
- }));
800
- if (items.length === 0) {
801
- console.log(`Order ${order.orderId} has no items to submit.`);
802
- return;
803
- }
804
- try {
805
- await submitItems(backendConfig, items);
806
- console.log(
807
- `Submitted order ${order.orderId} (${items.length} items) to backend.`
808
- );
809
- } catch (error) {
810
- const message = error instanceof Error ? error.message : String(error);
811
- console.error(`Failed to submit order ${order.orderId}: ${message}`);
812
- throw error;
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
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ozon-grabber",
3
- "version": "0.1.0",
3
+ "version": "0.1.4",
4
4
  "description": "Ozon order history grabber CLI",
5
5
  "repository": {
6
6
  "type": "git",