Size
48.2 KB
Version
2.3.4
Created
Dec 29, 2025
Updated
10 days ago
1// ==UserScript==
2// @name Auto Boost для Ozon
3// @description Автоматическое включение бустинга для товаров на Ozon Seller
4// @version 2.3.4
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 isProcessing = false; // Добавляем флаг процесса
17 let currentProductIndex = 0;
18 let boostedProducts = [];
19 let nonBoostedProducts = [];
20 let currentIntervalId = null; // ID текущего интервала
21 let selectedInterval = null; // Выбранный интервал повтора (в минутах) или null для однократного запуска
22 let sessionBoostedCount = 0; // Счетчик товаров с бустингом в текущей сессии
23 let sessionNonBoostedCount = 0; // Счетчик товаров без бустинга в текущей сессии
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}</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 isProcessing = true;
422 startBtn.style.display = 'none';
423 pauseBtn.style.display = 'block';
424 stopBtn.style.display = 'block';
425
426 await startProcessing();
427 };
428
429 pauseBtn.onclick = () => {
430 isPaused = true;
431 localStorage.setItem('autoBoostPaused', 'true');
432 pauseBtn.textContent = '▶ Продолжить';
433 pauseBtn.className = 'auto-boost-start-btn';
434 pauseBtn.onclick = () => {
435 isPaused = false;
436 localStorage.setItem('autoBoostPaused', 'false');
437 pauseBtn.textContent = '⏸ Пауза';
438 pauseBtn.className = 'auto-boost-pause-btn';
439 pauseBtn.onclick = () => {
440 isPaused = true;
441 pauseBtn.textContent = '▶ Продолжить';
442 pauseBtn.className = 'auto-boost-start-btn';
443 };
444 startProcessing();
445 };
446 };
447
448 stopBtn.onclick = async () => {
449 isPaused = true;
450 localStorage.setItem('autoBoostPaused', 'true');
451
452 // Полностью останавливаем таймер и очищаем все данные
453 if (currentIntervalId) {
454 clearInterval(currentIntervalId);
455 currentIntervalId = null;
456 }
457
458 // Очищаем все сохраненные данные о таймере
459 await GM.setValue('intervalActive', false);
460 await GM.setValue('nextRunTime', null);
461 console.log('Таймер полностью остановлен и очищен.');
462
463 startBtn.style.display = 'block';
464 pauseBtn.style.display = 'none';
465 stopBtn.style.display = 'none';
466 pauseBtn.textContent = '⏸ Пауза';
467 pauseBtn.className = 'auto-boost-pause-btn';
468
469 // Обновляем отображение времени следующего запуска
470 const nextRunTimeElement = document.querySelector('#next-run-time');
471 if (nextRunTimeElement) {
472 nextRunTimeElement.innerHTML = '';
473 }
474
475 updateStats('Процесс остановлен. Все запланированные бустинги отменены.');
476 };
477
478 console.log('Модальное окно открыто');
479 }
480
481 // Функция для отображения списка товаров
482 function renderProductsList() {
483 if (boostedProducts.length === 0) {
484 return '<div style="text-align: center; color: #999; padding: 20px;">Пока нет товаров с включенным бустингом</div>';
485 }
486 return boostedProducts.map(product => `
487 <div class="auto-boost-product-item">
488 <div class="auto-boost-product-sku">SKU: ${product.sku}</div>
489 <div class="auto-boost-product-name">${product.name}</div>
490 <a href="${product.url}" target="_blank" class="auto-boost-product-link">Открыть товар</a>
491 <div class="auto-boost-product-status">✓ Включен бустинг</div>
492 </div>
493 `).join('');
494 }
495
496 // Функция для начала обработки товаров
497 async function startProcessing() {
498 console.log('Начинаем обработку товаров');
499 updateStats('Сбор товаров со страницы...');
500
501 // Собираем все строки таблицы
502 const rows = document.querySelectorAll('[data-widget="@products/product-list-table-ods"] tbody tr');
503 console.log(`Найдено строк в таблице: ${rows.length}`);
504
505 if (rows.length === 0) {
506 updateStats('Товары не найдены на странице');
507 isProcessing = false;
508 return;
509 }
510
511 // Собираем данные всех товаров ДО начала обработки
512 const productsData = [];
513 for (let i = 0; i < rows.length; i++) {
514 const row = rows[i];
515 // Прокручиваем к строке чтобы данные загрузились
516 row.scrollIntoView({ block: 'center', behavior: 'auto' });
517 await wait(100);
518
519 // Получаем SKU
520 const skuElement = row.querySelector('[class*="index_sku"]');
521 const sku = skuElement ? skuElement.textContent.trim().replace('SKU', '').trim() : 'N/A';
522
523 // Получаем название товара
524 const nameLink = row.querySelector('a[href*="ozon.ru/product"]');
525 const name = nameLink ? nameLink.textContent.trim() : 'Без названия';
526
527 productsData.push({
528 sku: sku,
529 name: name,
530 rowIndex: i
531 });
532 }
533
534 console.log(`Собрано данных о товарах: ${productsData.length}`);
535 updateStats(`Найдено товаров: ${productsData.length}. Начинаем обработку...`);
536
537 // Обрабатываем товары по очереди
538 for (let i = currentProductIndex; i < productsData.length; i++) {
539 if (isPaused) {
540 console.log('Обработка приостановлена.');
541 return;
542 }
543
544 currentProductIndex = i;
545 const productData = productsData[i];
546 updateStats(`Обработка товара ${i + 1} из ${productsData.length}: ${productData.name}`);
547 console.log(`Обрабатываем товар ${i + 1} из ${productsData.length}`);
548
549 await processProduct(productData);
550 await wait(500);
551 }
552
553 updateStats(`Страница обработана! Обработано товаров: ${productsData.length}. Проверяем следующую страницу...`);
554 currentProductIndex = 0;
555
556 // Проверяем, есть ли следующая страница
557 const nextPageButton = document.querySelector('#pagination button[type="button"]:not([data-selected="true"])');
558 if (nextPageButton) {
559 console.log('Найдена следующая страница, переходим...');
560 updateStats('Переход на следующую страницу...');
561
562 // Кликаем на следующую страницу
563 nextPageButton.click();
564
565 // Ждем загрузки новой страницы
566 await wait(2000);
567
568 // Продолжаем обработку на новой странице
569 if (!isPaused) {
570 await startProcessing();
571 }
572 } else {
573 console.log('Следующая страница не найдена, обработка завершена');
574
575 // Сохраняем время завершения
576 await GM.setValue('lastCheckTime', Date.now());
577 updateLastCheckTime();
578
579 // Сохраняем результаты последнего буста
580 await GM.setValue('lastSessionBoosted', sessionBoostedCount);
581 await GM.setValue('lastSessionNonBoosted', sessionNonBoostedCount);
582
583 // Обновляем счетчики с результатами последнего буста
584 updateCounter();
585
586 // Перезагружаем список товаров из хранилища
587 await reloadProductsList();
588
589 if (selectedInterval === null || selectedInterval === 0) {
590 updateStats(`Обработка завершена! С бустингом: ${sessionBoostedCount}, Без бустинга: ${sessionNonBoostedCount}`);
591 isProcessing = false;
592
593 // Обновляем кнопки
594 const startBtn = document.querySelector('#start-boost-btn');
595 const pauseBtn = document.querySelector('#pause-boost-btn');
596 const stopBtn = document.querySelector('#stop-boost-btn');
597
598 if (startBtn) startBtn.style.display = 'block';
599 if (pauseBtn) pauseBtn.style.display = 'none';
600 if (stopBtn) stopBtn.style.display = 'none';
601 } else {
602 // Вычисляем время следующего запуска
603 const nextRunTime = new Date(Date.now() + selectedInterval * 60 * 1000);
604 const nextRunTimeStr = nextRunTime.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' });
605
606 // ВАЖНО: Сохраняем время следующего запуска и активируем таймер
607 await GM.setValue('nextRunTime', nextRunTime.getTime());
608 await GM.setValue('intervalActive', true);
609
610 updateStats(`Обработка завершена! С бустингом: ${sessionBoostedCount}, Без бустинга: ${sessionNonBoostedCount}. Следующий запуск в ${nextRunTimeStr}`);
611 isProcessing = false;
612
613 // НЕ запускаем setInterval здесь - таймер будет восстановлен при перезагрузке
614 console.log(`Следующий запуск запланирован на ${nextRunTimeStr}`);
615
616 // ВАЖНО: Запускаем таймер проверки прямо сейчас
617 startIntervalChecker();
618
619 // Обновляем отображение времени следующего запуска
620 const nextRunTimeElement = document.querySelector('#next-run-time');
621 if (nextRunTimeElement) {
622 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>`;
623 }
624
625 // Обновляем кнопки
626 const startBtn = document.querySelector('#start-boost-btn');
627 const pauseBtn = document.querySelector('#pause-boost-btn');
628 const stopBtn = document.querySelector('#stop-boost-btn');
629
630 if (startBtn) startBtn.style.display = 'block';
631 if (pauseBtn) pauseBtn.style.display = 'none';
632 if (stopBtn) stopBtn.style.display = 'block';
633 }
634 }
635 }
636
637 // Функция для обработки одного товара
638 async function processProduct(productData) {
639 try {
640 // Проверяем паузу перед началом обработки
641 const pausedState = localStorage.getItem('autoBoostPaused');
642 if (pausedState === 'true' || isPaused) {
643 console.log('Процесс на паузе, пропускаем обработку товара');
644 return;
645 }
646
647 console.log(`Открываем товар: ${productData.name} (SKU: ${productData.sku})`);
648
649 // Находим строку товара заново (DOM мог измениться)
650 const rows = document.querySelectorAll('[data-widget="@products/product-list-table-ods"] tbody tr');
651 let targetRow = null;
652
653 // Ищем строку по SKU
654 for (const row of rows) {
655 const skuElement = row.querySelector('[class*="index_sku"]');
656 const sku = skuElement ? skuElement.textContent.trim().replace('SKU', '').trim() : '';
657 if (sku === productData.sku) {
658 targetRow = row;
659 break;
660 }
661 }
662
663 if (!targetRow) {
664 console.log(`Строка товара ${productData.sku} не найдена в DOM`);
665 return;
666 }
667
668 // Прокручиваем к строке
669 targetRow.scrollIntoView({ block: 'center', behavior: 'auto' });
670 await wait(300);
671
672 // Ищем кнопку с ценой
673 const priceButton = targetRow.querySelector('button');
674 if (!priceButton) {
675 console.log(`Кнопка с ценой не найдена для товара ${productData.sku}`);
676 return;
677 }
678
679 // Генерируем уникальный ID для этой проверки
680 const checkId = Date.now() + Math.random().toString(36).substr(2, 9);
681
682 // Отправляем сообщение для проверки слайдера с уникальным ID
683 const checkData = {
684 action: 'checkBoost',
685 productSku: productData.sku,
686 productName: productData.name,
687 productUrl: '',
688 timestamp: Date.now(),
689 checkId: checkId
690 };
691 localStorage.setItem('autoBoostCheck', JSON.stringify(checkData));
692
693 // Устанавливаем флаг, что вкладка с этим ID ожидается
694 localStorage.setItem('autoBoostExpected_' + checkId, 'true');
695
696 // Кликаем на кнопку с ценой (откроется в новой вкладке)
697 priceButton.click();
698
699 // Ждем результата проверки
700 let waitTime = 0;
701 const maxWaitTime = 10000; // 10 секунд
702
703 while (waitTime < maxWaitTime) {
704 await wait(300);
705 waitTime += 300;
706
707 const result = localStorage.getItem('autoBoostResult');
708 if (result) {
709 const resultData = JSON.parse(result);
710 if (resultData.timestamp === checkData.timestamp && resultData.checkId === checkId) {
711 console.log('Получен результат проверки:', resultData);
712
713 if (resultData.boosted) {
714 await addBoostedProduct({
715 sku: productData.sku,
716 name: productData.name,
717 url: resultData.url
718 });
719 sessionBoostedCount++;
720 } else {
721 await addNonBoostedProduct({
722 sku: productData.sku,
723 name: productData.name,
724 url: resultData.url
725 });
726 sessionNonBoostedCount++;
727 }
728
729 localStorage.removeItem('autoBoostResult');
730 break;
731 }
732 }
733
734 // Проверяем, не остановлен ли основной процесс
735 if (isPaused) {
736 console.log('Основной процесс остановлен, прерываем ожидание результата проверки.');
737 break;
738 }
739 }
740
741 // Удаляем флаг ожидания после завершения ожидания
742 localStorage.removeItem('autoBoostExpected_' + checkId);
743 console.log(`Товар обработан: ${productData.name}`);
744
745 } catch (error) {
746 console.error('Ошибка при обработке товара:', error);
747 }
748 }
749
750 // Функция для добавления товара в список с бустингом
751 async function addBoostedProduct(product) {
752 const exists = boostedProducts.some(p => p.sku === product.sku);
753 if (exists) {
754 console.log(`Товар ${product.sku} уже в списке`);
755 return;
756 }
757
758 boostedProducts.push({
759 sku: product.sku,
760 name: product.name,
761 url: product.url,
762 timestamp: Date.now()
763 });
764
765 await GM.setValue('boostedProducts', JSON.stringify(boostedProducts));
766 updateProductsList();
767 console.log(`Товар добавлен в список с бустингом: ${product.name}`);
768 }
769
770 // Функция для добавления товара в список без бустинга
771 async function addNonBoostedProduct(product) {
772 const exists = nonBoostedProducts.some(p => p.sku === product.sku);
773 if (exists) {
774 console.log(`Товар ${product.sku} уже в списке без бустинга`);
775 return;
776 }
777
778 nonBoostedProducts.push({
779 sku: product.sku,
780 name: product.name,
781 url: product.url,
782 timestamp: Date.now()
783 });
784
785 await GM.setValue('nonBoostedProducts', JSON.stringify(nonBoostedProducts));
786 console.log(`Товар добавлен в список без бустинга: ${product.name}`);
787 }
788
789 // Функция для загрузки сохраненных товаров без бустинга
790 async function loadNonBoostedProducts() {
791 try {
792 const saved = await GM.getValue('nonBoostedProducts', '[]');
793 nonBoostedProducts = JSON.parse(saved);
794 console.log(`Загружено товаров без бустинга: ${nonBoostedProducts.length}`);
795 } catch (error) {
796 console.error('Ошибка при загрузке данных без бустинга:', error);
797 nonBoostedProducts = [];
798 }
799 }
800
801 // Функция для загрузки сохраненных товаров
802 async function loadBoostedProducts() {
803 try {
804 const saved = await GM.getValue('boostedProducts', '[]');
805 boostedProducts = JSON.parse(saved);
806 console.log(`Загружено товаров с бустингом: ${boostedProducts.length}`);
807 } catch (error) {
808 console.error('Ошибка при загрузке данных:', error);
809 boostedProducts = [];
810 }
811 }
812
813 // Функция для обновления списка товаров в UI
814 function updateProductsList() {
815 const listElement = document.querySelector('#products-list');
816 if (listElement) {
817 listElement.innerHTML = renderProductsList();
818 }
819 }
820
821 // Функция для перезагрузки списка товаров из хранилища
822 async function reloadProductsList() {
823 await loadBoostedProducts();
824 updateProductsList();
825 }
826
827 // Функция для обновления счетчика
828 function updateCounter() {
829 const counterElement = document.querySelector('#boost-counter');
830 if (counterElement) {
831 counterElement.textContent = sessionBoostedCount;
832 }
833
834 const nonBoostCounterElement = document.querySelector('#non-boost-counter');
835 if (nonBoostCounterElement) {
836 nonBoostCounterElement.textContent = sessionNonBoostedCount;
837 }
838 }
839
840 // Функция для обновления статуса
841 function updateStats(message) {
842 const statsElement = document.querySelector('#boost-stats');
843 if (statsElement) {
844 statsElement.textContent = message;
845 }
846 console.log('Статус:', message);
847 }
848
849 // Функция для обновления времени последней проверки
850 async function updateLastCheckTime() {
851 const lastCheckTime = await GM.getValue('lastCheckTime', null);
852 const lastCheckElement = document.querySelector('#last-check-time');
853
854 if (lastCheckElement && lastCheckTime) {
855 const date = new Date(lastCheckTime);
856 const time = date.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' });
857 const dateStr = date.toLocaleDateString('ru-RU');
858 lastCheckElement.innerHTML = `<div style="text-align: center; color: #666; font-size: 13px; margin-top: 8px;">Последняя проверка: ${time} / ${dateStr}</div>`;
859 }
860 }
861
862 // Обработчик для страницы товара (проверка слайдера)
863 async function handleProductPage() {
864 // Проверяем, не на паузе ли процесс
865 const pausedState = localStorage.getItem('autoBoostPaused');
866 if (pausedState === 'true') {
867 console.log('Процесс на паузе, вкладка не будет автоматически закрыта');
868 return;
869 }
870
871 // Проверяем, есть ли ожидаемая проверка (по уникальному ID)
872 for (let i = 0; i < localStorage.length; i++) {
873 const key = localStorage.key(i);
874 if (key && key.startsWith('autoBoostExpected_') && localStorage.getItem(key) === 'true') {
875 const checkId = key.replace('autoBoostExpected_', '');
876 const checkDataRaw = localStorage.getItem('autoBoostCheck');
877
878 if (checkDataRaw) {
879 const checkData = JSON.parse(checkDataRaw);
880
881 // Проверяем, совпадает ли checkId в autoBoostCheck с найденным
882 if (checkData.checkId === checkId) {
883 console.log('Найдена ожидаемая проверка с ID:', checkId);
884 await performCheckAndClose(checkData);
885 return;
886 }
887 }
888 }
889 }
890
891 // Если не нашли ожидаемой проверки, это вкладка, открытая вручную
892 console.log('Вкладка открыта вручную, проверка не требуется.');
893 }
894
895 // Выносим логику проверки в отдельную функцию
896 async function performCheckAndClose(data) {
897 if (Date.now() - data.timestamp > 60000) {
898 console.log('Таймаут проверки бустинга истек.');
899 window.close();
900 return;
901 }
902
903 console.log('Обнаружен запрос на проверку бустинга для ID:', data.checkId);
904
905 try {
906 const slider = await waitForElement('[class*="index_sliderTrack"]', 5000);
907 console.log('Слайдер найден.');
908
909 const style = slider.getAttribute('style') || '';
910 const widthMatch = style.match(/width:\s*(\d+(?:\.\d+)?)%/);
911 const width = widthMatch ? parseFloat(widthMatch[1]) : 0;
912
913 console.log(`Ширина слайдера: ${width}%`);
914
915 if (width === 0) {
916 console.log('Бустинг не включен (width: 0%)');
917 const result = {
918 boosted: false,
919 timestamp: data.timestamp,
920 checkId: data.checkId,
921 url: window.location.href
922 };
923 localStorage.setItem('autoBoostResult', JSON.stringify(result));
924 await wait(300);
925 window.close();
926 return;
927 }
928
929 console.log('Бустинг включен, ищем кнопку "Сохранить"');
930 const saveButton = Array.from(document.querySelectorAll('button')).find(btn =>
931 btn.textContent.includes('Сохранить') || btn.textContent.includes('сохранить')
932 );
933
934 if (saveButton) {
935 console.log('Нажимаем кнопку "Сохранить"');
936 saveButton.click();
937 await wait(1500);
938 } else {
939 console.log('Кнопка "Сохранить" не найдена.');
940 }
941
942 const result = {
943 boosted: true,
944 timestamp: data.timestamp,
945 checkId: data.checkId,
946 url: window.location.href
947 };
948 localStorage.setItem('autoBoostResult', JSON.stringify(result));
949 await wait(300);
950 window.close();
951
952 } catch (error) {
953 console.error('Ошибка при ожидании слайдера:', error.message);
954 const result = {
955 boosted: false,
956 timestamp: data.timestamp,
957 checkId: data.checkId,
958 url: window.location.href
959 };
960 localStorage.setItem('autoBoostResult', JSON.stringify(result));
961 await wait(300);
962 window.close();
963 }
964 }
965
966 // Функция для проверки и запуска таймера при загрузке страницы
967 async function checkAndRestoreInterval() {
968 const intervalActive = await GM.getValue('intervalActive', false);
969 const savedInterval = await GM.getValue('selectedInterval', null);
970 const nextRunTime = await GM.getValue('nextRunTime', null);
971
972 console.log('Проверка сохраненного состояния:', { intervalActive, savedInterval, nextRunTime });
973
974 if (intervalActive && savedInterval && savedInterval > 0) {
975 selectedInterval = savedInterval;
976
977 if (nextRunTime) {
978 const now = Date.now();
979 const timeUntilNextRun = nextRunTime - now;
980
981 if (timeUntilNextRun > 0) {
982 const nextRunDate = new Date(nextRunTime);
983 const nextRunTimeStr = nextRunDate.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' });
984 console.log(`Восстановление таймера. Следующий запуск в ${nextRunTimeStr} (через ${Math.round(timeUntilNextRun / 1000 / 60)} минут)`);
985
986 // Запускаем таймер проверки
987 startIntervalChecker();
988
989 console.log(`Таймер успешно восстановлен. Интервал: ${selectedInterval} минут`);
990 } else {
991 // Время уже прошло, запускаем сразу
992 console.log('Время следующего запуска уже прошло, запускаем процесс сейчас');
993
994 // Вычисляем новое время следующего запуска
995 const newNextRunTime = new Date(Date.now() + selectedInterval * 60 * 1000);
996 await GM.setValue('nextRunTime', newNextRunTime.getTime());
997
998 sessionBoostedCount = 0;
999 sessionNonBoostedCount = 0;
1000 isProcessing = true;
1001 isPaused = false;
1002 localStorage.setItem('autoBoostPaused', 'false');
1003
1004 // Очищаем списки товаров перед новым запуском
1005 boostedProducts = [];
1006 nonBoostedProducts = [];
1007 await GM.setValue('boostedProducts', JSON.stringify(boostedProducts));
1008 await GM.setValue('nonBoostedProducts', JSON.stringify(nonBoostedProducts));
1009 console.log('Списки товаров очищены перед автоматическим запуском');
1010
1011 await startProcessing();
1012 }
1013 } else {
1014 // Если nextRunTime не установлено, значит это первый запуск после выбора интервала
1015 console.log('Таймер активен, но время следующего запуска не установлено');
1016 }
1017 } else {
1018 console.log('Нет активного таймера для восстановления');
1019 }
1020 }
1021
1022 // Функция для запуска таймера проверки
1023 function startIntervalChecker() {
1024 // Очищаем предыдущий таймер, если он был
1025 if (currentIntervalId) {
1026 clearInterval(currentIntervalId);
1027 console.log('Предыдущий таймер очищен');
1028 }
1029
1030 // Проверяем каждые 30 секунд, не пора ли запускать
1031 currentIntervalId = setInterval(async () => {
1032 const currentNextRunTime = await GM.getValue('nextRunTime', null);
1033 const currentIntervalActive = await GM.getValue('intervalActive', false);
1034
1035 if (!currentIntervalActive) {
1036 console.log('Таймер был остановлен, прекращаем проверку');
1037 clearInterval(currentIntervalId);
1038 currentIntervalId = null;
1039 return;
1040 }
1041
1042 if (currentNextRunTime && Date.now() >= currentNextRunTime) {
1043 console.log('Время запуска наступило! Запускаем процесс');
1044 clearInterval(currentIntervalId);
1045 currentIntervalId = null;
1046
1047 // Вычисляем новое время следующего запуска
1048 const newNextRunTime = new Date(Date.now() + selectedInterval * 60 * 1000);
1049 await GM.setValue('nextRunTime', newNextRunTime.getTime());
1050
1051 // Сбрасываем счетчики и снимаем паузу
1052 sessionBoostedCount = 0;
1053 sessionNonBoostedCount = 0;
1054 isProcessing = true;
1055 isPaused = false;
1056 localStorage.setItem('autoBoostPaused', 'false');
1057
1058 // Очищаем списки товаров перед новым запуском
1059 boostedProducts = [];
1060 nonBoostedProducts = [];
1061 await GM.setValue('boostedProducts', JSON.stringify(boostedProducts));
1062 await GM.setValue('nonBoostedProducts', JSON.stringify(nonBoostedProducts));
1063 console.log('Списки товаров очищены перед автоматическим запуском');
1064
1065 await startProcessing();
1066 }
1067 }, 30000); // Проверяем каждые 30 секунд
1068
1069 console.log('Таймер проверки запущен (проверка каждые 30 секунд)');
1070 }
1071
1072 // Инициализация
1073 async function init() {
1074 console.log('Инициализация Auto Boost для Ozon (v2.3.2)');
1075 addStyles();
1076
1077 const currentUrl = window.location.href;
1078
1079 if (currentUrl.includes('/app/products')) {
1080 console.log('Обнаружена страница с таблицей товаров');
1081
1082 // Проверяем и восстанавливаем таймер
1083 await checkAndRestoreInterval();
1084
1085 await wait(2000);
1086 createBoostButton();
1087
1088 const observer = new MutationObserver(() => {
1089 if (!document.querySelector('.auto-boost-button')) {
1090 createBoostButton();
1091 }
1092 });
1093
1094 observer.observe(document.body, {
1095 childList: true,
1096 subtree: true
1097 });
1098 } else if (currentUrl.includes('/app/prices/manager/')) {
1099 console.log('Обнаружена страница товара');
1100 handleProductPage();
1101 }
1102 }
1103
1104 if (document.readyState === 'loading') {
1105 document.addEventListener('DOMContentLoaded', init);
1106 } else {
1107 init();
1108 }
1109})();