Size
55.8 KB
Version
2.3.17
Created
Feb 19, 2026
Updated
18 days ago
1// ==UserScript==
2// @name Auto Boost для Ozon
3// @description Автоматическое включение бустинга для товаров на Ozon Seller
4// @version 2.3.17
5// @match https://*.seller.ozon.ru/*
6// @icon https://st.ozone.ru/s3/seller-ui-static/icon/favicon32.png
7// @grant GM.getValue
8// @grant GM.setValue
9// @grant GM.openInTab
10// ==/UserScript==
11(function() {
12 'use strict';
13 console.log('Auto Boost для Ozon: расширение запущено (v2.3.2)');
14 // Глобальные переменные
15 let isPaused = false;
16 let currentProductIndex = 0;
17 let boostedProducts = [];
18 let nonBoostedProducts = [];
19 let currentIntervalId = null; // ID текущего интервала
20 let selectedInterval = null; // Выбранный интервал повтора (в минутах) или null для однократного запуска
21 let sessionBoostedCount = 0; // Счетчик товаров с бустингом в текущей сессии
22 let sessionNonBoostedCount = 0; // Счетчик товаров без бустинга в текущей сессии
23 let processedPages = new Set(); // Отслеживаем обработанные страницы
24
25 // Функция для ожидания
26 function wait(ms) {
27 return new Promise(resolve => setTimeout(resolve, ms));
28 }
29
30 // Функция для ожидания элемента
31 function waitForElement(selector, timeout = 10000) {
32 return new Promise((resolve, reject) => {
33 const element = document.querySelector(selector);
34 if (element) {
35 resolve(element);
36 return;
37 }
38
39 const observer = new MutationObserver(() => {
40 const element = document.querySelector(selector);
41 if (element) {
42 resolve(element);
43 observer.disconnect();
44 }
45 });
46
47 observer.observe(document.body, {
48 childList: true,
49 subtree: true
50 });
51
52 setTimeout(() => {
53 observer.disconnect();
54 reject(new Error(`Элемент ${selector} не найден в течение ${timeout} мс`));
55 }, timeout);
56 });
57 }
58
59 // Функция для создания стилей
60 function addStyles() {
61 const styles = `
62.auto-boost-button {
63 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
64 color: white;
65 border: none;
66 padding: 12px 24px;
67 border-radius: 8px;
68 font-size: 14px;
69 font-weight: 600;
70 cursor: pointer;
71 box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
72 transition: all 0.3s ease;
73 margin-bottom: 16px;
74}
75.auto-boost-button:hover {
76 transform: translateY(-2px);
77 box-shadow: 0 6px 16px rgba(102, 126, 234, 0.4);
78}
79.auto-boost-modal {
80 position: fixed;
81 top: 0;
82 left: 0;
83 width: 100%;
84 height: 100%;
85 background: rgba(0, 0, 0, 0.5);
86 display: flex;
87 align-items: center;
88 justify-content: center;
89 z-index: 10000;
90}
91.auto-boost-modal-content {
92 background: white;
93 border-radius: 12px;
94 padding: 24px;
95 width: 90%;
96 max-width: 800px;
97 max-height: 80vh;
98 overflow-y: auto;
99 box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
100}
101.auto-boost-modal-header {
102 display: flex;
103 justify-content: space-between;
104 align-items: center;
105 margin-bottom: 20px;
106 padding-bottom: 16px;
107 border-bottom: 2px solid #f0f0f0;
108}
109.auto-boost-modal-title {
110 font-size: 24px;
111 font-weight: 700;
112 color: #333;
113}
114.auto-boost-close {
115 background: none;
116 border: none;
117 font-size: 28px;
118 cursor: pointer;
119 color: #999;
120 padding: 0;
121 width: 32px;
122 height: 32px;
123 display: flex;
124 align-items: center;
125 justify-content: center;
126 border-radius: 50%;
127 transition: all 0.2s;
128}
129.auto-boost-close:hover {
130 background: #f0f0f0;
131 color: #333;
132}
133.auto-boost-controls {
134 display: flex;
135 flex-direction: column;
136 gap: 12px;
137 margin-bottom: 20px;
138}
139.auto-boost-start-btn, .auto-boost-pause-btn, .auto-boost-stop-btn {
140 padding: 12px 24px;
141 border: none;
142 border-radius: 8px;
143 font-size: 14px;
144 font-weight: 600;
145 cursor: pointer;
146 transition: all 0.3s ease;
147 width: 100%;
148}
149.auto-boost-start-btn {
150 background: #10b981;
151 color: white;
152}
153.auto-boost-start-btn:hover {
154 background: #059669;
155}
156.auto-boost-pause-btn {
157 background: #f59e0b;
158 color: white;
159}
160.auto-boost-pause-btn:hover {
161 background: #d97706;
162}
163.auto-boost-stop-btn {
164 background: #ef4444;
165 color: white;
166}
167.auto-boost-stop-btn:hover {
168 background: #dc2626;
169}
170.auto-boost-stats {
171 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
172 color: white;
173 padding: 16px;
174 border-radius: 8px;
175 margin-bottom: 20px;
176 text-align: center;
177 font-size: 18px;
178 font-weight: 600;
179}
180.auto-boost-products-list {
181 max-height: 400px;
182 overflow-y: auto;
183 border: 1px solid #e5e7eb;
184 border-radius: 8px;
185 padding: 12px;
186}
187.auto-boost-product-item {
188 background: #f9fafb;
189 padding: 12px;
190 margin-bottom: 8px;
191 border-radius: 6px;
192 border-left: 4px solid #10b981;
193}
194.auto-boost-product-sku {
195 font-weight: 600;
196 color: #333;
197 margin-bottom: 4px;
198}
199.auto-boost-product-name {
200 color: #666;
201 font-size: 13px;
202 margin-bottom: 4px;
203}
204.auto-boost-product-link {
205 color: #667eea;
206 text-decoration: none;
207 font-size: 12px;
208}
209.auto-boost-product-link:hover {
210 text-decoration: underline;
211}
212.auto-boost-product-status {
213 display: inline-block;
214 background: #10b981;
215 color: white;
216 padding: 4px 8px;
217 border-radius: 4px;
218 font-size: 11px;
219 margin-top: 4px;
220}
221.auto-boost-counter {
222 background: white;
223 color: #667eea;
224 padding: 8px 16px;
225 border-radius: 6px;
226 font-size: 14px;
227 font-weight: 600;
228 display: inline-block;
229 margin-bottom: 12px;
230 border: 2px solid #667eea;
231}
232.auto-boost-interval-controls {
233 display: flex;
234 flex-direction: column;
235 gap: 8px;
236 margin-top: 10px;
237}
238.auto-boost-interval-label {
239 font-weight: 600;
240 color: #333;
241 margin-bottom: 4px;
242}
243.auto-boost-interval-select {
244 padding: 8px 12px;
245 border: 1px solid #d1d5db;
246 border-radius: 6px;
247 font-size: 14px;
248}
249.auto-boost-interval-info {
250 font-size: 12px;
251 color: #666;
252 margin-top: 4px;
253 text-align: center;
254}
255 `;
256 const styleElement = document.createElement('style');
257 styleElement.textContent = styles;
258 document.head.appendChild(styleElement);
259 }
260
261 // Функция для создания кнопки "Включить буст"
262 function createBoostButton() {
263 const tableWidget = document.querySelector('[data-widget="@products/product-list-table-ods"]');
264 if (!tableWidget) {
265 console.log('Таблица товаров не найдена, повторная попытка через 2 секунды');
266 setTimeout(createBoostButton, 2000);
267 return;
268 }
269 // Проверяем, не создана ли уже кнопка
270 if (document.querySelector('.auto-boost-button')) {
271 return;
272 }
273 const button = document.createElement('button');
274 button.className = 'auto-boost-button';
275 button.textContent = '🚀 Включить буст';
276 button.onclick = openModal;
277 tableWidget.parentElement.insertBefore(button, tableWidget);
278 console.log('Кнопка "Включить буст" создана');
279 }
280
281 // Функция для создания модального окна
282 async function openModal() {
283 // Загружаем сохраненные данные
284 await loadBoostedProducts();
285 await loadNonBoostedProducts();
286
287 // Загружаем результаты последнего буста
288 const lastSessionBoosted = await GM.getValue('lastSessionBoosted', 0);
289 const lastSessionNonBoosted = await GM.getValue('lastSessionNonBoosted', 0);
290
291 // Загружаем время последней проверки
292 const lastCheckTime = await GM.getValue('lastCheckTime', null);
293 let lastCheckText = '';
294 if (lastCheckTime) {
295 const date = new Date(lastCheckTime);
296 const time = date.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' });
297 const dateStr = date.toLocaleDateString('ru-RU');
298 lastCheckText = `<div style="text-align: center; color: #666; font-size: 13px; margin-top: 8px;">Последняя проверка: ${time} / ${dateStr}</div>`;
299 }
300
301 // Загружаем сохраненный интервал
302 const savedInterval = await GM.getValue('selectedInterval', null);
303 if (savedInterval !== null) {
304 selectedInterval = savedInterval;
305 }
306
307 // Проверяем, есть ли активный таймер и время следующего запуска
308 const intervalActive = await GM.getValue('intervalActive', false);
309 const nextRunTime = await GM.getValue('nextRunTime', null);
310 let nextRunText = '';
311 let showStopButton = false;
312
313 if (intervalActive && nextRunTime && selectedInterval && selectedInterval > 0) {
314 const now = Date.now();
315 if (nextRunTime > now) {
316 const nextRunDate = new Date(nextRunTime);
317 const nextRunTimeStr = nextRunDate.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' });
318 nextRunText = `<div style="text-align: center; color: #667eea; font-size: 14px; font-weight: 600; margin-top: 8px; padding: 8px; background: #f0f4ff; border-radius: 6px;">⏰ Следующий запуск в ${nextRunTimeStr} (через ${Math.round((nextRunTime - now) / 1000 / 60)} минут)</div>`;
319 showStopButton = true;
320 }
321 }
322
323 const modal = document.createElement('div');
324 modal.className = 'auto-boost-modal';
325 modal.innerHTML = `
326 <div class="auto-boost-modal-content">
327 <div class="auto-boost-modal-header">
328 <div class="auto-boost-modal-title">🚀 Auto Boost для Ozon</div>
329 <button class="auto-boost-close">×</button>
330 </div>
331 <div style="display: flex; gap: 12px; margin-bottom: 12px;">
332 <div class="auto-boost-counter" style="border-color: #10b981; color: #10b981;">
333 ✓ С бустингом: <span id="boost-counter">${lastSessionBoosted}</span>
334 </div>
335 <div class="auto-boost-counter" style="border-color: #ef4444; color: #ef4444;">
336 ✗ Без бустинга: <span id="non-boost-counter">${lastSessionNonBoosted}</span>
337 </div>
338 </div>
339 <div id="last-check-time">${lastCheckText}</div>
340 <div id="next-run-time">${nextRunText}</div>
341 <div class="auto-boost-controls">
342 <button class="auto-boost-start-btn" id="start-boost-btn">▶ Старт</button>
343 <button class="auto-boost-pause-btn" id="pause-boost-btn" style="display: none;">⏸ Пауза</button>
344 <button class="auto-boost-stop-btn" id="stop-boost-btn" style="display: ${showStopButton ? 'block' : 'none'};">⏹ Остановить</button>
345 </div>
346 <div class="auto-boost-interval-controls">
347 <div class="auto-boost-interval-label">Интервал повтора:</div>
348 <select class="auto-boost-interval-select" id="interval-select">
349 <option value="0" ${selectedInterval === null || selectedInterval === 0 ? 'selected' : ''}>Без повтора (однократно)</option>
350 <option value="5" ${selectedInterval === 5 ? 'selected' : ''}>Каждые 5 минут</option>
351 <option value="15" ${selectedInterval === 15 ? 'selected' : ''}>Каждые 15 минут</option>
352 <option value="30" ${selectedInterval === 30 ? 'selected' : ''}>Каждые 30 минут</option>
353 <option value="60" ${selectedInterval === 60 ? 'selected' : ''}>Каждый час (60 мин)</option>
354 <option value="120" ${selectedInterval === 120 ? 'selected' : ''}>Каждые 2 часа (120 мин)</option>
355 </select>
356 <div class="auto-boost-interval-info" id="interval-info">${selectedInterval && selectedInterval > 0 ? `Выбрано: Каждые ${selectedInterval} минут${selectedInterval === 60 ? ' (1 час)' : selectedInterval === 120 ? ' (2 часа)' : ''}` : 'Выбрано: Без повтора'}</div>
357 </div>
358 <div class="auto-boost-stats" id="boost-stats">
359 Нажмите "Старт" для начала обработки товаров
360 </div>
361 <div class="auto-boost-products-list" id="products-list">
362 ${renderProductsList()}
363 </div>
364 </div>
365 `;
366 document.body.appendChild(modal);
367
368 // Обработчики событий
369 modal.querySelector('.auto-boost-close').onclick = () => {
370 modal.remove();
371 };
372 modal.onclick = (e) => {
373 if (e.target === modal) {
374 modal.remove();
375 }
376 };
377
378 const startBtn = modal.querySelector('#start-boost-btn');
379 const pauseBtn = modal.querySelector('#pause-boost-btn');
380 const stopBtn = modal.querySelector('#stop-boost-btn');
381 const intervalSelect = modal.querySelector('#interval-select');
382 const intervalInfo = modal.querySelector('#interval-info');
383
384 // Обработчик выбора интервала
385 intervalSelect.onchange = async (e) => {
386 const value = parseInt(e.target.value);
387 if (value === 0) {
388 selectedInterval = null;
389 intervalInfo.textContent = 'Выбрано: Без повтора';
390 } else {
391 selectedInterval = value;
392 intervalInfo.textContent = `Выбрано: Каждые ${value} минут${value === 60 ? ' (1 час)' : value === 120 ? ' (2 часа)' : ''}`;
393 }
394 await GM.setValue('selectedInterval', selectedInterval);
395 console.log(`Выбран интервал: ${selectedInterval} минут`);
396 };
397
398 startBtn.onclick = async () => {
399 // Устанавливаем начальное значение интервала, если ещё не установлено
400 if (selectedInterval === undefined) {
401 const value = parseInt(intervalSelect.value);
402 selectedInterval = value === 0 ? null : value;
403 await GM.setValue('selectedInterval', selectedInterval);
404 }
405
406 // Снимаем паузу
407 isPaused = false;
408 localStorage.setItem('autoBoostPaused', 'false');
409
410 // Очищаем списки товаров перед новым запуском
411 boostedProducts = [];
412 nonBoostedProducts = [];
413 await GM.setValue('boostedProducts', JSON.stringify(boostedProducts));
414 await GM.setValue('nonBoostedProducts', JSON.stringify(nonBoostedProducts));
415 console.log('Списки товаров очищены перед новым запуском');
416
417 // Сбрасываем счетчики сессии
418 sessionBoostedCount = 0;
419 sessionNonBoostedCount = 0;
420
421 // Обновляем счетчики в UI сразу
422 updateCounter();
423
424 // Обновляем список товаров в UI
425 updateProductsList();
426
427 // Очищаем список обработанных страниц
428 processedPages.clear();
429 console.log('Список обработанных страниц очищен');
430
431 startBtn.style.display = 'none';
432 pauseBtn.style.display = 'block';
433 stopBtn.style.display = 'block';
434
435 await startProcessing();
436 };
437
438 pauseBtn.onclick = () => {
439 isPaused = true;
440 localStorage.setItem('autoBoostPaused', 'true');
441 pauseBtn.textContent = '▶ Продолжить';
442 pauseBtn.className = 'auto-boost-start-btn';
443 pauseBtn.onclick = () => {
444 isPaused = false;
445 localStorage.setItem('autoBoostPaused', 'false');
446 pauseBtn.textContent = '⏸ Пауза';
447 pauseBtn.className = 'auto-boost-pause-btn';
448 pauseBtn.onclick = () => {
449 isPaused = true;
450 pauseBtn.textContent = '▶ Продолжить';
451 pauseBtn.className = 'auto-boost-start-btn';
452 };
453 startProcessing();
454 };
455 };
456
457 stopBtn.onclick = async () => {
458 isPaused = true;
459 localStorage.setItem('autoBoostPaused', 'true');
460
461 // Полностью останавливаем таймер и очищаем все данные
462 if (currentIntervalId) {
463 clearInterval(currentIntervalId);
464 currentIntervalId = null;
465 }
466
467 // Очищаем все сохраненные данные о таймере
468 await GM.setValue('intervalActive', false);
469 await GM.setValue('nextRunTime', null);
470 console.log('Таймер полностью остановлен и очищен.');
471
472 startBtn.style.display = 'block';
473 pauseBtn.style.display = 'none';
474 stopBtn.style.display = 'none';
475 pauseBtn.textContent = '⏹ Обработка остановлена. Все запланированные бустинги отменены.';
476 pauseBtn.className = 'auto-boost-stop-btn';
477
478 // Обновляем отображение времени следующего запуска
479 const nextRunTimeElement = document.querySelector('#next-run-time');
480 if (nextRunTimeElement) {
481 nextRunTimeElement.innerHTML = '';
482 }
483
484 updateStats('⏹ Обработка остановлена. Все запланированные бустинги отменены.');
485 };
486
487 console.log('Модальное окно открыто');
488 }
489
490 // Функция для отображения списка товаров
491 function renderProductsList() {
492 if (boostedProducts.length === 0) {
493 return '<div style="text-align: center; color: #999; padding: 20px;">Пока нет товаров с включенным бустингом</div>';
494 }
495 return boostedProducts.map(product => `
496 <div class="auto-boost-product-item">
497 <div class="auto-boost-product-sku">SKU: ${product.sku}</div>
498 <div class="auto-boost-product-name">${product.name}</div>
499 <a href="${product.url}" target="_blank" class="auto-boost-product-link">Открыть товар</a>
500 <div class="auto-boost-product-status">✓ Включен бустинг</div>
501 </div>
502 `).join('');
503 }
504
505 // Функция для начала обработки товаров
506 async function startProcessing() {
507 console.log('Начинаем обработку товаров');
508 updateStats('Сбор товаров со страницы...');
509
510 // Получаем общее количество товаров из интерфейса
511 let totalProducts = 0;
512 // Ищем кнопку с текстом "Все" и берем число из нее
513 const allTabButton = document.querySelector('button[data-active="true"][data-widget="@products/tabs/products-list-filter-tab"]');
514 if (allTabButton) {
515 const counterElement = allTabButton.querySelector('.rc8110-a0');
516 if (counterElement) {
517 totalProducts = parseInt(counterElement.textContent.trim());
518 console.log(`Общее количество товаров: ${totalProducts}`);
519 }
520 }
521
522 if (totalProducts === 0) {
523 console.log('Не удалось определить общее количество товаров');
524 }
525
526 // Собираем все строки таблицы
527 const rows = document.querySelectorAll('[data-widget="@products/product-list-table-ods"] tbody tr');
528 console.log(`Найдено строк в таблице: ${rows.length}`);
529
530 if (rows.length === 0) {
531 updateStats('Товары не найдены на странице');
532 return;
533 }
534
535 // Собираем данные всех товаров ДО начала обработки
536 const productsData = [];
537 for (let i = 0; i < rows.length; i++) {
538 const row = rows[i];
539 // Прокручиваем к строке чтобы данные загрузились
540 row.scrollIntoView({ block: 'center', behavior: 'auto' });
541 await wait(100);
542
543 // Получаем SKU - ищем разными способами
544 let sku = 'N/A';
545
546 // Способ 1: по классу index_sku
547 const skuElement = row.querySelector('[class*="index_sku"]');
548 if (skuElement) {
549 sku = skuElement.textContent.trim().replace('SKU', '').trim();
550 }
551
552 // Способ 2: если не нашли, ищем длинное число (обычно SKU - это 8+ цифр)
553 if (sku === 'N/A') {
554 const allDivs = row.querySelectorAll('div');
555 for (const div of allDivs) {
556 const text = div.textContent.trim();
557 // SKU обычно содержит только цифры и длиннее 8 символов
558 if (/^\d{8,}$/.test(text)) {
559 sku = text;
560 console.log(`SKU найден через поиск числа: ${sku}`);
561 break;
562 }
563 }
564 }
565
566 // Получаем название товара
567 const nameLink = row.querySelector('a[href*="ozon.ru/product"]');
568 const name = nameLink ? nameLink.textContent.trim() : 'Без названия';
569
570 console.log(`Товар ${i + 1}: SKU=${sku}, Название=${name.substring(0, 50)}`);
571
572 productsData.push({
573 sku: sku,
574 name: name,
575 rowIndex: i
576 });
577 }
578
579 console.log(`Собрано данных о товарах: ${productsData.length}`);
580 updateStats(`Найдено товаров: ${productsData.length}. Начинаем обработку...`);
581
582 // Обрабатываем товары по очереди
583 for (let i = currentProductIndex; i < productsData.length; i++) {
584 if (isPaused) {
585 console.log('Обработка приостановлена.');
586 return;
587 }
588
589 currentProductIndex = i;
590 const productData = productsData[i];
591
592 // Вычисляем глобальный номер товара
593 const globalProductNumber = sessionBoostedCount + sessionNonBoostedCount + 1;
594
595 updateStats(`Обработка товара ${globalProductNumber} из ${totalProducts > 0 ? totalProducts : '?'}: ${productData.name}`);
596 console.log(`Обрабатываем товар ${globalProductNumber} из ${totalProducts > 0 ? totalProducts : '?'}`);
597
598 await processProduct(productData);
599 await wait(500);
600
601 // ВАЖНО: Проверяем, достигли ли мы общего количества товаров
602 const processedTotal = sessionBoostedCount + sessionNonBoostedCount;
603 if (totalProducts > 0 && processedTotal >= totalProducts) {
604 console.log(`Достигнуто общее количество товаров: ${processedTotal} из ${totalProducts}. Останавливаем обработку.`);
605 updateStats(`Обработка завершена! Обработано ${processedTotal} товаров из ${totalProducts}.`);
606
607 // Переходим к завершению обработки
608 await finishProcessing();
609 return;
610 }
611 }
612
613 updateStats(`Страница обработана! Обработано товаров: ${productsData.length}. Проверяем следующую страницу...`);
614 currentProductIndex = 0;
615
616 // Проверяем, достигли ли мы общего количества товаров
617 const processedTotal = sessionBoostedCount + sessionNonBoostedCount;
618 if (totalProducts > 0 && processedTotal >= totalProducts) {
619 console.log(`Достигнуто общее количество товаров: ${processedTotal} из ${totalProducts}. Останавливаем обработку.`);
620 await finishProcessing();
621 return;
622 }
623
624 // Проверяем, есть ли следующая страница
625 const paginationContainer = document.querySelector('#pagination');
626 if (paginationContainer) {
627 // Ищем все кнопки с классом t0c110-a1 и берем последнюю (это кнопка "Вперед")
628 const paginationButtons = paginationContainer.querySelectorAll('button.t0c110-a1.table-500');
629 const nextPageButton = paginationButtons.length > 0 ? paginationButtons[paginationButtons.length - 1] : null;
630
631 if (nextPageButton && !nextPageButton.disabled) {
632 console.log('Найдена следующая страница, переходим...');
633 updateStats('Переход на следующую страницу...');
634
635 // Кликаем на следующую страницу
636 nextPageButton.click();
637
638 // Ждем загрузки новой страницы
639 await wait(3000);
640
641 // Продолжаем обработку на новой странице
642 if (!isPaused) {
643 await startProcessing();
644 }
645 } else {
646 console.log('Следующая страница не найдена или кнопка disabled, обработка завершена');
647 await finishProcessing();
648 }
649 } else {
650 console.log('Пагинация не найдена, обработка завершена');
651 await finishProcessing();
652 }
653 }
654
655 // Функция для завершения обработки
656 async function finishProcessing() {
657 // Сохраняем время завершения
658 await GM.setValue('lastCheckTime', Date.now());
659 updateLastCheckTime();
660
661 // Сохраняем результаты последнего буста
662 await GM.setValue('lastSessionBoosted', sessionBoostedCount);
663 await GM.setValue('lastSessionNonBoosted', sessionNonBoostedCount);
664
665 // Обновляем счетчики с результатами последнего буста
666 updateCounter();
667
668 // Перезагружаем список товаров из хранилища
669 await reloadProductsList();
670
671 // ВАЖНО: Останавливаем процесс независимо от интервала
672 isPaused = true;
673 localStorage.setItem('autoBoostPaused', 'true');
674
675 if (selectedInterval === null || selectedInterval === 0) {
676 updateStats(`Обработка завершена! С бустингом: ${sessionBoostedCount}, Без бустинга: ${sessionNonBoostedCount}`);
677
678 // Обновляем кнопки
679 const startBtn = document.querySelector('#start-boost-btn');
680 const pauseBtn = document.querySelector('#pause-boost-btn');
681 const stopBtn = document.querySelector('#stop-boost-btn');
682
683 if (startBtn) startBtn.style.display = 'block';
684 if (pauseBtn) pauseBtn.style.display = 'none';
685 if (stopBtn) stopBtn.style.display = 'none';
686 } else {
687 // Вычисляем время следующего запуска
688 const nextRunTime = new Date(Date.now() + selectedInterval * 60 * 1000);
689 const nextRunTimeStr = nextRunTime.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' });
690
691 // ВАЖНО: Сохраняем время следующего запуска и активируем таймер
692 await GM.setValue('nextRunTime', nextRunTime.getTime());
693 await GM.setValue('intervalActive', true);
694
695 updateStats(`Обработка завершена! С бустингом: ${sessionBoostedCount}, Без бустинга: ${sessionNonBoostedCount}. Следующий запуск в ${nextRunTimeStr}`);
696
697 // ВАЖНО: Запускаем таймер проверки прямо сейчас
698 startIntervalChecker();
699
700 // Обновляем отображение времени следующего запуска
701 const nextRunTimeElement = document.querySelector('#next-run-time');
702 if (nextRunTimeElement) {
703 nextRunTimeElement.innerHTML = `<div style="text-align: center; color: #667eea; font-size: 14px; font-weight: 600; margin-top: 8px; padding: 8px; background: #f0f4ff; border-radius: 6px;">⏰ Следующий запуск в ${nextRunTimeStr}</div>`;
704 }
705
706 // Обновляем кнопки
707 const startBtn = document.querySelector('#start-boost-btn');
708 const pauseBtn = document.querySelector('#pause-boost-btn');
709 const stopBtn = document.querySelector('#stop-boost-btn');
710
711 if (startBtn) startBtn.style.display = 'block';
712 if (pauseBtn) pauseBtn.style.display = 'none';
713 if (stopBtn) stopBtn.style.display = 'block';
714 }
715 }
716
717 // Функция для обработки одного товара
718 async function processProduct(productData) {
719 try {
720 // Проверяем паузу перед началом обработки
721 const pausedState = localStorage.getItem('autoBoostPaused');
722 if (pausedState === 'true' || isPaused) {
723 console.log('Процесс на паузе, пропускаем обработку товара');
724 return;
725 }
726
727 console.log(`Открываем товар: ${productData.name} (SKU: ${productData.sku})`);
728
729 // Находим строку товара заново (DOM мог измениться)
730 const rows = document.querySelectorAll('[data-widget="@products/product-list-table-ods"] tbody tr');
731 let targetRow = null;
732
733 // Ищем строку по SKU
734 for (const row of rows) {
735 // Пробуем найти SKU разными способами
736 let sku = '';
737
738 // Способ 1: по классу index_sku
739 const skuElement = row.querySelector('[class*="index_sku"]');
740 if (skuElement) {
741 sku = skuElement.textContent.trim().replace('SKU', '').trim();
742 }
743
744 // Способ 2: если не нашли, ищем длинное число (обычно SKU - это 10+ цифр)
745 if (!sku || sku === 'N/A') {
746 const allDivs = row.querySelectorAll('div');
747 for (const div of allDivs) {
748 const text = div.textContent.trim();
749 // SKU обычно содержит только цифры и длиннее 8 символов
750 if (/^\d{8,}$/.test(text)) {
751 sku = text;
752 break;
753 }
754 }
755 }
756
757 if (sku === productData.sku) {
758 targetRow = row;
759 break;
760 }
761 }
762
763 if (!targetRow) {
764 console.log(`Строка товара ${productData.sku} не найдена в DOM`);
765 return;
766 }
767
768 // Прокручиваем к строке
769 targetRow.scrollIntoView({ block: 'center', behavior: 'auto' });
770 await wait(300);
771
772 // Ищем кнопку с ценой - это первая кнопка в строке, которая содержит символ ₽
773 const allButtons = targetRow.querySelectorAll('button');
774 let priceButton = null;
775
776 for (const btn of allButtons) {
777 if (btn.textContent.includes('₽')) {
778 priceButton = btn;
779 break;
780 }
781 }
782
783 if (!priceButton) {
784 console.log(`Кнопка с ценой не найдена для товара ${productData.sku}`);
785 return;
786 }
787
788 // Генерируем уникальный ID для этой проверки
789 const checkId = Date.now() + Math.random().toString(36).substr(2, 9);
790
791 // Отправляем сообщение для проверки слайдера с уникальным ID
792 const checkData = {
793 action: 'checkBoost',
794 productSku: productData.sku,
795 productName: productData.name,
796 productUrl: '',
797 timestamp: Date.now(),
798 checkId: checkId
799 };
800 localStorage.setItem('autoBoostCheck', JSON.stringify(checkData));
801
802 // Устанавливаем флаг, что вкладка с этим ID ожидается
803 localStorage.setItem('autoBoostExpected_' + checkId, 'true');
804
805 // Кликаем на кнопку с ценой (откроется в новой вкладке)
806 console.log(`Кликаем на кнопку с ценой для товара ${productData.sku}`);
807 priceButton.click();
808 console.log('Клик выполнен, ожидаем результата проверки...');
809
810 // Ждем результата проверки
811 let waitTime = 0;
812 const maxWaitTime = 5000; // 5 секунд
813
814 while (waitTime < maxWaitTime) {
815 await wait(200);
816 waitTime += 200;
817
818 const result = localStorage.getItem('autoBoostResult');
819 if (result) {
820 const resultData = JSON.parse(result);
821 if (resultData.timestamp === checkData.timestamp && resultData.checkId === checkId) {
822 console.log('Получен результат проверки:', resultData);
823
824 if (resultData.boosted) {
825 await addBoostedProduct({
826 sku: productData.sku,
827 name: productData.name,
828 url: resultData.url
829 });
830 sessionBoostedCount++;
831 console.log(`Товар добавлен в список с бустом. Всего с бустом: ${sessionBoostedCount}`);
832 } else {
833 await addNonBoostedProduct({
834 sku: productData.sku,
835 name: productData.name,
836 url: resultData.url
837 });
838 sessionNonBoostedCount++;
839 console.log(`Товар добавлен в список без буста. Всего без буста: ${sessionNonBoostedCount}`);
840 }
841
842 // Обновляем счетчики в UI
843 updateCounter();
844
845 localStorage.removeItem('autoBoostResult');
846 break;
847 }
848 }
849
850 // Проверяем, не остановлен ли основной процесс
851 if (isPaused) {
852 console.log('Основной процесс остановлен, прерываем ожидание результата проверки.');
853 break;
854 }
855 }
856
857 // Удаляем флаг ожидания после завершения ожидания
858 localStorage.removeItem('autoBoostExpected_' + checkId);
859 console.log(`Товар обработан: ${productData.name}`);
860
861 } catch (error) {
862 console.error('Ошибка при обработке товара:', error);
863 }
864 }
865
866 // Функция для добавления товара в список с бустингом
867 async function addBoostedProduct(product) {
868 const exists = boostedProducts.some(p => p.sku === product.sku);
869 if (exists) {
870 console.log(`Товар ${product.sku} уже в списке`);
871 return;
872 }
873
874 boostedProducts.push({
875 sku: product.sku,
876 name: product.name,
877 url: product.url,
878 timestamp: Date.now()
879 });
880
881 await GM.setValue('boostedProducts', JSON.stringify(boostedProducts));
882 updateProductsList();
883 console.log(`Товар добавлен в список с бустингом: ${product.name}`);
884 }
885
886 // Функция для добавления товара в список без бустинга
887 async function addNonBoostedProduct(product) {
888 const exists = nonBoostedProducts.some(p => p.sku === product.sku);
889 if (exists) {
890 console.log(`Товар ${product.sku} уже в списке без бустинга`);
891 return;
892 }
893
894 nonBoostedProducts.push({
895 sku: product.sku,
896 name: product.name,
897 url: product.url,
898 timestamp: Date.now()
899 });
900
901 await GM.setValue('nonBoostedProducts', JSON.stringify(nonBoostedProducts));
902 console.log(`Товар добавлен в список без бустинга: ${product.name}`);
903 }
904
905 // Функция для загрузки сохраненных товаров без бустинга
906 async function loadNonBoostedProducts() {
907 try {
908 const saved = await GM.getValue('nonBoostedProducts', '[]');
909 nonBoostedProducts = JSON.parse(saved);
910 console.log(`Загружено товаров без бустинга: ${nonBoostedProducts.length}`);
911 } catch (error) {
912 console.error('Ошибка при загрузке данных без бустинга:', error);
913 nonBoostedProducts = [];
914 }
915 }
916
917 // Функция для загрузки сохраненных товаров
918 async function loadBoostedProducts() {
919 try {
920 const saved = await GM.getValue('boostedProducts', '[]');
921 boostedProducts = JSON.parse(saved);
922 console.log(`Загружено товаров с бустингом: ${boostedProducts.length}`);
923 } catch (error) {
924 console.error('Ошибка при загрузке данных:', error);
925 boostedProducts = [];
926 }
927 }
928
929 // Функция для обновления списка товаров в UI
930 function updateProductsList() {
931 const listElement = document.querySelector('#products-list');
932 if (listElement) {
933 listElement.innerHTML = renderProductsList();
934 }
935 }
936
937 // Функция для перезагрузки списка товаров из хранилища
938 async function reloadProductsList() {
939 await loadBoostedProducts();
940 updateProductsList();
941 }
942
943 // Функция для обновления счетчика
944 function updateCounter() {
945 const counterElement = document.querySelector('#boost-counter');
946 if (counterElement) {
947 counterElement.textContent = sessionBoostedCount;
948 }
949
950 const nonBoostCounterElement = document.querySelector('#non-boost-counter');
951 if (nonBoostCounterElement) {
952 nonBoostCounterElement.textContent = sessionNonBoostedCount;
953 }
954 }
955
956 // Функция для обновления статуса
957 function updateStats(message) {
958 const statsElement = document.querySelector('#boost-stats');
959 if (statsElement) {
960 statsElement.textContent = message;
961 }
962 console.log('Статус:', message);
963 }
964
965 // Функция для обновления времени последней проверки
966 async function updateLastCheckTime() {
967 const lastCheckTime = await GM.getValue('lastCheckTime', null);
968 const lastCheckElement = document.querySelector('#last-check-time');
969
970 if (lastCheckElement && lastCheckTime) {
971 const date = new Date(lastCheckTime);
972 const time = date.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' });
973 const dateStr = date.toLocaleDateString('ru-RU');
974 lastCheckElement.innerHTML = `<div style="text-align: center; color: #666; font-size: 13px; margin-top: 8px;">Последняя проверка: ${time} / ${dateStr}</div>`;
975 }
976 }
977
978 // Обработчик для страницы товара (проверка слайдера)
979 async function handleProductPage() {
980 // Проверяем, не на паузе ли процесс
981 const pausedState = localStorage.getItem('autoBoostPaused');
982 if (pausedState === 'true') {
983 console.log('Процесс на паузе, вкладка не будет автоматически закрыта');
984 return;
985 }
986
987 // Проверяем, есть ли ожидаемая проверка (по уникальному ID)
988 for (let i = 0; i < localStorage.length; i++) {
989 const key = localStorage.key(i);
990 if (key && key.startsWith('autoBoostExpected_') && localStorage.getItem(key) === 'true') {
991 const checkId = key.replace('autoBoostExpected_', '');
992 const checkDataRaw = localStorage.getItem('autoBoostCheck');
993
994 if (checkDataRaw) {
995 const checkData = JSON.parse(checkDataRaw);
996
997 // Проверяем, совпадает ли checkId в autoBoostCheck с найденным
998 if (checkData.checkId === checkId) {
999 console.log('Найдена ожидаемая проверка с ID:', checkId);
1000 await performCheckAndClose(checkData);
1001 return;
1002 }
1003 }
1004 }
1005 }
1006
1007 // Если не нашли ожидаемой проверки, это вкладка, открытая вручную
1008 console.log('Вкладка открыта вручную, проверка не требуется.');
1009 }
1010
1011 // Выносим логику проверки в отдельную функцию
1012 async function performCheckAndClose(data) {
1013 if (Date.now() - data.timestamp > 60000) {
1014 console.log('Таймаут проверки бустинга истек.');
1015 window.close();
1016 return;
1017 }
1018
1019 console.log('Обнаружен запрос на проверку бустинга для ID:', data.checkId);
1020
1021 try {
1022 // Ждем загрузки страницы
1023 await wait(2000);
1024
1025 // Ищем элементы для сравнения
1026 const boostElement = document.querySelector('.nd-bl5');
1027 const baseElement = document.querySelector('.nd-lb5.nd-bl3');
1028
1029 if (!boostElement || !baseElement) {
1030 console.log('Элементы для проверки буста не найдены');
1031 const result = {
1032 boosted: false,
1033 timestamp: data.timestamp,
1034 checkId: data.checkId,
1035 url: window.location.href
1036 };
1037 localStorage.setItem('autoBoostResult', JSON.stringify(result));
1038 await wait(300);
1039 window.close();
1040 return;
1041 }
1042
1043 // Получаем ширину элементов из style
1044 const boostStyle = boostElement.getAttribute('style') || '';
1045 const baseStyle = baseElement.getAttribute('style') || '';
1046
1047 const boostWidthMatch = boostStyle.match(/width:\s*(\d+(?:\.\d+)?)%/);
1048 const baseWidthMatch = baseStyle.match(/width:\s*(\d+(?:\.\d+)?)%/);
1049
1050 const boostWidth = boostWidthMatch ? parseFloat(boostWidthMatch[1]) : 0;
1051 const baseWidth = baseWidthMatch ? parseFloat(baseWidthMatch[1]) : 0;
1052
1053 console.log(`Ширина буста: ${boostWidth}%, Ширина базы: ${baseWidth}%`);
1054
1055 // Если ширина буста >= ширины базы, значит буст включен
1056 if (boostWidth >= baseWidth && boostWidth > 0) {
1057 console.log('Бустинг включен, ищем кнопку "Сохранить"');
1058 const saveButton = Array.from(document.querySelectorAll('button')).find(btn =>
1059 btn.textContent.includes('Сохранить') || btn.textContent.includes('сохранить')
1060 );
1061
1062 if (saveButton) {
1063 console.log('Нажимаем кнопку "Сохранить"');
1064 saveButton.click();
1065 await wait(1500);
1066 } else {
1067 console.log('Кнопка "Сохранить" не найдена.');
1068 }
1069
1070 const result = {
1071 boosted: true,
1072 timestamp: data.timestamp,
1073 checkId: data.checkId,
1074 url: window.location.href
1075 };
1076 localStorage.setItem('autoBoostResult', JSON.stringify(result));
1077 await wait(300);
1078 window.close();
1079 } else {
1080 console.log('Бустинг не включен');
1081 const result = {
1082 boosted: false,
1083 timestamp: data.timestamp,
1084 checkId: data.checkId,
1085 url: window.location.href
1086 };
1087 localStorage.setItem('autoBoostResult', JSON.stringify(result));
1088 await wait(300);
1089 window.close();
1090 }
1091
1092 } catch (error) {
1093 console.error('Ошибка при проверке бустинга:', error.message);
1094 const result = {
1095 boosted: false,
1096 timestamp: data.timestamp,
1097 checkId: data.checkId,
1098 url: window.location.href
1099 };
1100 localStorage.setItem('autoBoostResult', JSON.stringify(result));
1101 await wait(300);
1102 window.close();
1103 }
1104 }
1105
1106 // Функция для проверки и запуска таймера при загрузке страницы
1107 async function checkAndRestoreInterval() {
1108 const intervalActive = await GM.getValue('intervalActive', false);
1109 const savedInterval = await GM.getValue('selectedInterval', null);
1110 const nextRunTime = await GM.getValue('nextRunTime', null);
1111
1112 console.log('Проверка сохраненного состояния:', { intervalActive, savedInterval, nextRunTime });
1113
1114 if (intervalActive && savedInterval && savedInterval > 0) {
1115 selectedInterval = savedInterval;
1116
1117 if (nextRunTime) {
1118 const now = Date.now();
1119 const timeUntilNextRun = nextRunTime - now;
1120
1121 if (timeUntilNextRun > 0) {
1122 const nextRunDate = new Date(nextRunTime);
1123 const nextRunTimeStr = nextRunDate.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' });
1124 console.log(`Восстановление таймера. Следующий запуск в ${nextRunTimeStr} (через ${Math.round(timeUntilNextRun / 1000 / 60)} минут)`);
1125
1126 // Запускаем таймер проверки
1127 startIntervalChecker();
1128
1129 console.log(`Таймер успешно восстановлен. Интервал: ${selectedInterval} минут`);
1130 } else {
1131 // Время уже прошло, запускаем сразу
1132 console.log('Время следующего запуска уже прошло, запускаем процесс сейчас');
1133
1134 // Вычисляем новое время следующего запуска
1135 const newNextRunTime = new Date(Date.now() + selectedInterval * 60 * 1000);
1136 await GM.setValue('nextRunTime', newNextRunTime.getTime());
1137
1138 sessionBoostedCount = 0;
1139 sessionNonBoostedCount = 0;
1140 isPaused = false;
1141 localStorage.setItem('autoBoostPaused', 'false');
1142
1143 // Очищаем списки товаров перед новым запуском
1144 boostedProducts = [];
1145 nonBoostedProducts = [];
1146 await GM.setValue('boostedProducts', JSON.stringify(boostedProducts));
1147 await GM.setValue('nonBoostedProducts', JSON.stringify(nonBoostedProducts));
1148 console.log('Списки товаров очищены перед автоматическим запуском');
1149
1150 await startProcessing();
1151 }
1152 } else {
1153 // Если nextRunTime не установлено, значит это первый запуск после выбора интервала
1154 console.log('Таймер активен, но время следующего запуска не установлено');
1155 }
1156 } else {
1157 console.log('Нет активного таймера для восстановления');
1158 }
1159 }
1160
1161 // Функция для запуска таймера проверки
1162 function startIntervalChecker() {
1163 // Очищаем предыдущий таймер, если он был
1164 if (currentIntervalId) {
1165 clearInterval(currentIntervalId);
1166 console.log('Предыдущий таймер очищен');
1167 }
1168
1169 // Проверяем каждые 30 секунд, не пора ли запускать
1170 currentIntervalId = setInterval(async () => {
1171 const currentNextRunTime = await GM.getValue('nextRunTime', null);
1172 const currentIntervalActive = await GM.getValue('intervalActive', false);
1173
1174 if (!currentIntervalActive) {
1175 console.log('Таймер был остановлен, прекращаем проверку');
1176 clearInterval(currentIntervalId);
1177 currentIntervalId = null;
1178 return;
1179 }
1180
1181 if (currentNextRunTime && Date.now() >= currentNextRunTime) {
1182 console.log('Время запуска наступило! Запускаем процесс');
1183 clearInterval(currentIntervalId);
1184 currentIntervalId = null;
1185
1186 // Вычисляем новое время следующего запуска
1187 const newNextRunTime = new Date(Date.now() + selectedInterval * 60 * 1000);
1188 await GM.setValue('nextRunTime', newNextRunTime.getTime());
1189
1190 // Сбрасываем счетчики и снимаем паузу
1191 sessionBoostedCount = 0;
1192 sessionNonBoostedCount = 0;
1193 isPaused = false;
1194 localStorage.setItem('autoBoostPaused', 'false');
1195
1196 // Очищаем списки товаров перед новым запуском
1197 boostedProducts = [];
1198 nonBoostedProducts = [];
1199 await GM.setValue('boostedProducts', JSON.stringify(boostedProducts));
1200 await GM.setValue('nonBoostedProducts', JSON.stringify(nonBoostedProducts));
1201 console.log('Списки товаров очищены перед автоматическим запуском');
1202
1203 await startProcessing();
1204 }
1205 }, 30000); // Проверяем каждые 30 секунд
1206
1207 console.log('Таймер проверки запущен (проверка каждые 30 секунд)');
1208 }
1209
1210 // Инициализация
1211 async function init() {
1212 console.log('Инициализация Auto Boost для Ozon (v2.3.2)');
1213 addStyles();
1214
1215 const currentUrl = window.location.href;
1216
1217 if (currentUrl.includes('/app/products')) {
1218 console.log('Обнаружена страница с таблицей товаров');
1219
1220 // Проверяем и восстанавливаем таймер
1221 await checkAndRestoreInterval();
1222
1223 await wait(2000);
1224 createBoostButton();
1225
1226 const observer = new MutationObserver(() => {
1227 if (!document.querySelector('.auto-boost-button')) {
1228 createBoostButton();
1229 }
1230 });
1231
1232 observer.observe(document.body, {
1233 childList: true,
1234 subtree: true
1235 });
1236 } else if (currentUrl.includes('/app/prices/manager/')) {
1237 console.log('Обнаружена страница товара');
1238 handleProductPage();
1239 }
1240 }
1241
1242 if (document.readyState === 'loading') {
1243 document.addEventListener('DOMContentLoaded', init);
1244 } else {
1245 init();
1246 }
1247})();