Size
89.7 KB
Version
3.0.17
Created
Apr 21, 2026
Updated
8 days ago
Парсер с кнопками стоп/пауза, убран тип, бренд только конкурентов
1// ==UserScript==
2// @name Ozon Product Parser v3.0 — Stop/Pause + Fixes
3// @namespace http://tampermonkey.net/
4// @version 3.0.17
5// @description Парсер с кнопками стоп/пауза, убран тип, бренд только конкурентов
6// @author You
7// @match https://ozon.ru/*
8// @match https://www.ozon.ru/*
9// @grant GM.getValue
10// @grant GM.setValue
11// @grant GM.deleteValue
12// @icon https://www.google.com/s2/favicons?sz=64&domain=ozon.ru
13// ==/UserScript==
14(function() {
15 'use strict';
16
17 console.log('🚀 Ozon Parser v3.0: Starting... URL:', window.location.href);
18
19 const PAGE_STATE = {
20 isNavigatingAway: false,
21 panelCreated: false
22 };
23
24 function navigateTo(url) {
25 console.log('🧭 Navigating to:', url);
26 PAGE_STATE.isNavigatingAway = true;
27 GM.setValue('ozon_parser_navigating_to', url).then(() => {
28 window.location.href = url;
29 });
30 }
31
32 function addStyles() {
33 if (document.getElementById('ozon-parser-styles')) return;
34
35 const styles = `
36 .ozon-parser-container {
37 position: fixed !important;
38 top: 20px !important;
39 right: 20px !important;
40 z-index: 2147483647 !important;
41 display: flex !important;
42 gap: 10px !important;
43 cursor: move !important;
44 user-select: none !important;
45 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important;
46 }
47 .ozon-parser-container.collapsed { gap: 0 !important; }
48 .ozon-parser-toggle-btn {
49 background: linear-gradient(135deg, #005bff 0%, #0043c7 100%) !important;
50 color: white !important;
51 border: none !important;
52 padding: 12px !important;
53 border-radius: 8px !important;
54 cursor: pointer !important;
55 font-size: 18px !important;
56 font-weight: 600 !important;
57 box-shadow: 0 4px 12px rgba(0, 91, 255, 0.3) !important;
58 transition: all 0.3s ease !important;
59 width: 44px !important;
60 height: 44px !important;
61 display: flex !important;
62 align-items: center !important;
63 justify-content: center !important;
64 }
65 .ozon-parser-toggle-btn:hover {
66 background: linear-gradient(135deg, #0043c7 0%, #002f8f 100%) !important;
67 transform: translateY(-2px) !important;
68 }
69 .ozon-parser-buttons-wrapper {
70 display: flex !important;
71 gap: 10px !important;
72 transition: all 0.3s ease !important;
73 }
74 .ozon-parser-buttons-wrapper.hidden { display: none !important; }
75 .ozon-parser-btn {
76 background: linear-gradient(135deg, #005bff 0%, #0043c7 100%) !important;
77 color: white !important;
78 border: none !important;
79 padding: 12px 20px !important;
80 border-radius: 8px !important;
81 cursor: pointer !important;
82 font-size: 13px !important;
83 font-weight: 600 !important;
84 box-shadow: 0 4px 12px rgba(0, 91, 255, 0.3) !important;
85 transition: all 0.3s ease !important;
86 white-space: nowrap !important;
87 }
88 .ozon-parser-btn:hover {
89 background: linear-gradient(135deg, #0043c7 0%, #002f8f 100%) !important;
90 transform: translateY(-2px) !important;
91 }
92 .ozon-parser-btn.secondary {
93 background: linear-gradient(135deg, #28a745 0%, #1e7e34 100%) !important;
94 box-shadow: 0 4px 12px rgba(40, 167, 69, 0.3) !important;
95 }
96 .ozon-parser-btn.danger {
97 background: linear-gradient(135deg, #dc3545 0%, #c82333 100%) !important;
98 box-shadow: 0 4px 12px rgba(220, 53, 69, 0.3) !important;
99 }
100 .ozon-parser-btn.warning {
101 background: linear-gradient(135deg, #ffc107 0%, #e0a800 100%) !important;
102 color: #000 !important;
103 box-shadow: 0 4px 12px rgba(255, 193, 7, 0.3) !important;
104 }
105 .ozon-parser-btn:disabled {
106 opacity: 0.6 !important;
107 cursor: not-allowed !important;
108 transform: none !important;
109 }
110 .ozon-parser-modal {
111 position: fixed !important;
112 top: 0 !important;
113 left: 0 !important;
114 width: 100% !important;
115 height: 100% !important;
116 background: rgba(0, 0, 0, 0.7) !important;
117 display: flex !important;
118 justify-content: center !important;
119 align-items: center !important;
120 z-index: 2147483646 !important;
121 }
122 .ozon-parser-modal-content {
123 background: white !important;
124 padding: 30px !important;
125 border-radius: 12px !important;
126 max-width: 98vw !important;
127 width: 98vw !important;
128 max-height: 90vh !important;
129 overflow-y: auto !important;
130 box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3) !important;
131 }
132 .ozon-parser-modal-header {
133 font-size: 24px !important;
134 font-weight: 700 !important;
135 margin-bottom: 20px !important;
136 color: #333 !important;
137 }
138 .ozon-parser-modal-body { margin-bottom: 20px !important; }
139 .ozon-parser-textarea {
140 width: 100% !important;
141 min-height: 200px !important;
142 padding: 12px !important;
143 border: 2px solid #e0e0e0 !important;
144 border-radius: 8px !important;
145 font-size: 14px !important;
146 font-family: inherit !important;
147 resize: vertical !important;
148 box-sizing: border-box !important;
149 }
150 .ozon-parser-textarea:focus {
151 outline: none !important;
152 border-color: #005bff !important;
153 }
154 .ozon-parser-modal-footer {
155 display: flex !important;
156 gap: 10px !important;
157 justify-content: flex-end !important;
158 }
159 .ozon-parser-results-table {
160 width: 100% !important;
161 border-collapse: collapse !important;
162 margin-top: 20px !important;
163 font-size: 12px !important;
164 }
165 .ozon-parser-results-table th,
166 .ozon-parser-results-table td {
167 padding: 8px !important;
168 text-align: left !important;
169 border-bottom: 1px solid #e0e0e0 !important;
170 white-space: nowrap !important;
171 vertical-align: top !important;
172 }
173 .ozon-parser-results-table th {
174 background: #f8f9fa !important;
175 font-weight: 600 !important;
176 color: #333 !important;
177 position: sticky !important;
178 top: 0 !important;
179 font-size: 11px !important;
180 }
181 .ozon-parser-results-table td:nth-child(1),
182 .ozon-parser-results-table th:nth-child(1) { width: 100px !important; } /* Наш SKU */
183 .ozon-parser-results-table td:nth-child(2),
184 .ozon-parser-results-table th:nth-child(2),
185 .ozon-parser-results-table td:nth-child(6),
186 .ozon-parser-results-table th:nth-child(6) {
187 max-width: 150px !important;
188 width: 150px !important;
189 white-space: normal !important;
190 word-wrap: break-word !important;
191 font-size: 11px !important;
192 line-height: 1.3 !important;
193 } /* Названия */
194 .ozon-parser-results-table td:nth-child(3),
195 .ozon-parser-results-table th:nth-child(3),
196 .ozon-parser-results-table td:nth-child(7),
197 .ozon-parser-results-table th:nth-child(7) { width: 80px !important; } /* Цены */
198 .ozon-parser-results-table td:nth-child(4),
199 .ozon-parser-results-table th:nth-child(4),
200 .ozon-parser-results-table td:nth-child(8),
201 .ozon-parser-results-table th:nth-child(8) { width: 100px !important; } /* Выручка */
202 .ozon-parser-results-table td:nth-child(5),
203 .ozon-parser-results-table th:nth-child(5) { width: 100px !important; } /* Бренд конк */
204 .ozon-parser-results-table td:nth-child(9),
205 .ozon-parser-results-table th:nth-child(9) { width: 100px !important; } /* Конк SKU */
206 .ozon-parser-results-table td:nth-child(10),
207 .ozon-parser-results-table th:nth-child(10) { width: 100px !important; } /* Запрос */
208
209 .ozon-parser-results-table tr:hover { background: #f8f9fa !important; }
210 .ozon-parser-highlight { background: #e7f3ff !important; font-weight: 600 !important; }
211 .ozon-parser-sku-link {
212 color: #005bff !important;
213 text-decoration: none !important;
214 font-weight: 600 !important;
215 font-size: 11px !important;
216 }
217 .ozon-parser-sku-link:hover { text-decoration: underline !important; }
218 .ozon-parser-info {
219 padding: 15px !important;
220 background: #e7f3ff !important;
221 border-left: 4px solid #005bff !important;
222 border-radius: 4px !important;
223 margin-bottom: 15px !important;
224 font-size: 13px !important;
225 color: #333 !important;
226 line-height: 1.6 !important;
227 }
228 .ozon-parser-file-input {
229 padding: 10px !important;
230 border: 2px dashed #e0e0e0 !important;
231 border-radius: 8px !important;
232 width: 100% !important;
233 box-sizing: border-box !important;
234 cursor: pointer !important;
235 }
236 .ozon-parser-file-input:hover { border-color: #005bff !important; }
237 .ozon-parser-badge {
238 display: inline-block !important;
239 padding: 3px 6px !important;
240 border-radius: 4px !important;
241 font-size: 10px !important;
242 font-weight: 600 !important;
243 }
244 .ozon-parser-badge.paused { background: #ffc107 !important; color: #000 !important; }
245 .ozon-parser-stats {
246 display: grid !important;
247 grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)) !important;
248 gap: 15px !important;
249 margin-bottom: 20px !important;
250 }
251 .ozon-parser-stat-card {
252 background: #f8f9fa !important;
253 padding: 15px !important;
254 border-radius: 8px !important;
255 text-align: center !important;
256 }
257 .ozon-parser-stat-value {
258 font-size: 28px !important;
259 font-weight: 700 !important;
260 color: #005bff !important;
261 }
262 .ozon-parser-stat-label {
263 font-size: 12px !important;
264 color: #666 !important;
265 margin-top: 5px !important;
266 }
267 .ozon-parser-brand {
268 color: #666 !important;
269 font-size: 10px !important;
270 font-weight: 500 !important;
271 }
272 .ozon-parser-status {
273 position: fixed !important;
274 bottom: 20px !important;
275 left: 20px !important;
276 background: #1a1a2e !important;
277 color: #fff !important;
278 padding: 12px 20px !important;
279 border-radius: 8px !important;
280 font-size: 13px !important;
281 z-index: 2147483647 !important;
282 box-shadow: 0 4px 12px rgba(0,0,0,0.3) !important;
283 max-width: 400px !important;
284 }
285 .ozon-parser-status.error { background: #dc3545 !important; }
286 .ozon-parser-status.success { background: #28a745 !important; }
287 .ozon-parser-status.paused { background: #ffc107 !important; color: #000 !important; }
288 .ozon-parser-progress {
289 position: fixed !important;
290 top: 80px !important;
291 right: 20px !important;
292 background: #1a1a2e !important;
293 color: #fff !important;
294 padding: 10px 15px !important;
295 border-radius: 8px !important;
296 font-size: 12px !important;
297 z-index: 2147483647 !important;
298 min-width: 200px !important;
299 }
300 .ozon-parser-progress.paused {
301 background: #ffc107 !important;
302 color: #000 !important;
303 }
304 .ozon-parser-progress-bar {
305 width: 100% !important;
306 height: 4px !important;
307 background: #333 !important;
308 border-radius: 2px !important;
309 margin-top: 8px !important;
310 overflow: hidden !important;
311 }
312 .ozon-parser-progress-fill {
313 height: 100% !important;
314 background: #005bff !important;
315 transition: width 0.3s ease !important;
316 }
317 .ozon-parser-ai-cell {
318 max-width: 300px !important;
319 width: 300px !important;
320 white-space: normal !important;
321 word-wrap: break-word !important;
322 overflow-wrap: break-word !important;
323 font-size: 11px !important;
324 line-height: 1.5 !important;
325 vertical-align: top !important;
326 overflow: hidden !important;
327 }
328 .ozon-parser-composition-cell {
329 max-width: 200px !important;
330 width: 200px !important;
331 white-space: normal !important;
332 word-wrap: break-word !important;
333 overflow-wrap: break-word !important;
334 font-size: 11px !important;
335 line-height: 1.4 !important;
336 vertical-align: top !important;
337 overflow: hidden !important;
338 }
339 .ozon-parser-name-cell {
340 max-width: 160px !important;
341 width: 160px !important;
342 white-space: normal !important;
343 word-wrap: break-word !important;
344 overflow-wrap: break-word !important;
345 font-size: 11px !important;
346 line-height: 1.3 !important;
347 vertical-align: top !important;
348 overflow: hidden !important;
349 }
350 .ozon-parser-dosage-cell {
351 max-width: 120px !important;
352 width: 120px !important;
353 white-space: normal !important;
354 word-wrap: break-word !important;
355 overflow-wrap: break-word !important;
356 font-size: 11px !important;
357 line-height: 1.3 !important;
358 vertical-align: top !important;
359 overflow: hidden !important;
360 }
361 .ozon-parser-brand-cell {
362 max-width: 100px !important;
363 width: 100px !important;
364 white-space: normal !important;
365 word-wrap: break-word !important;
366 overflow-wrap: break-word !important;
367 font-size: 11px !important;
368 color: #666 !important;
369 vertical-align: top !important;
370 overflow: hidden !important;
371 }
372 .ozon-parser-ai-cell-preview {
373 display: block !important;
374 max-height: 100px !important;
375 overflow: hidden !important;
376 white-space: pre-wrap !important;
377 word-wrap: break-word !important;
378 font-size: 11px !important;
379 line-height: 1.4 !important;
380 color: #333 !important;
381 }
382 .ozon-parser-ai-cell-more-btn {
383 display: inline-block !important;
384 margin-top: 6px !important;
385 padding: 3px 10px !important;
386 background: #005bff !important;
387 color: #fff !important;
388 border: none !important;
389 border-radius: 4px !important;
390 cursor: pointer !important;
391 font-size: 11px !important;
392 font-weight: 600 !important;
393 }
394 .ozon-parser-ai-cell-more-btn:hover {
395 background: #0043c7 !important;
396 }
397 `;
398 const styleElement = document.createElement('style');
399 styleElement.id = 'ozon-parser-styles';
400 styleElement.textContent = styles;
401 document.head.appendChild(styleElement);
402 console.log('✅ Styles added');
403 }
404
405 async function createProgressPanel() {
406 const existing = document.getElementById('ozon-parser-progress');
407 if (existing) existing.remove();
408
409 const tasksJson = await GM.getValue('ozon_parser_tasks', '[]');
410 const tasks = JSON.parse(tasksJson);
411 const currentIndex = parseInt(await GM.getValue('ozon_parser_current_task', 0));
412 const isActive = await GM.getValue('ozon_parser_active', 'false');
413 const isPaused = await GM.getValue('ozon_parser_paused', 'false');
414
415 if (isActive !== 'true' && isPaused !== 'true') return;
416
417 const panel = document.createElement('div');
418 panel.id = 'ozon-parser-progress';
419 panel.className = `ozon-parser-progress ${isPaused === 'true' ? 'paused' : ''}`;
420
421 const percent = Math.round((currentIndex / tasks.length) * 100);
422 const statusText = isPaused === 'true' ? '⏸️ ПАУЗА' : '📊 Парсинг';
423
424 panel.innerHTML = `
425 <div>${statusText}: ${currentIndex + 1} / ${tasks.length}</div>
426 <div style="font-size: 10px; color: #aaa; margin-top: 4px;">${tasks[currentIndex]?.ourSku || ''}</div>
427 <div class="ozon-parser-progress-bar">
428 <div class="ozon-parser-progress-fill" style="width: ${percent}%"></div>
429 </div>
430 `;
431
432 document.body.appendChild(panel);
433 console.log('✅ Progress panel created');
434 }
435
436 async function createUI() {
437 if (document.getElementById('ozon-parser-container')) {
438 console.log('UI already exists');
439 return;
440 }
441
442 console.log('Creating UI...');
443
444 const container = document.createElement('div');
445 container.className = 'ozon-parser-container';
446 container.id = 'ozon-parser-container';
447
448 const toggleBtn = document.createElement('button');
449 toggleBtn.className = 'ozon-parser-toggle-btn';
450 toggleBtn.innerHTML = '📊';
451 toggleBtn.title = 'Свернуть/Развернуть';
452
453 const buttonsWrapper = document.createElement('div');
454 buttonsWrapper.className = 'ozon-parser-buttons-wrapper';
455 buttonsWrapper.id = 'ozon-parser-buttons';
456
457 const parseBtn = document.createElement('button');
458 parseBtn.className = 'ozon-parser-btn';
459 parseBtn.textContent = '📁 Загрузить';
460 parseBtn.onclick = (e) => {
461 e.stopPropagation();
462 showUploadModal();
463 };
464
465 const resultsBtn = document.createElement('button');
466 resultsBtn.className = 'ozon-parser-btn secondary';
467 resultsBtn.textContent = '📋 Результаты';
468 resultsBtn.onclick = (e) => {
469 e.stopPropagation();
470 showResultsModal();
471 };
472
473 // Кнопка Пауза/Продолжить
474 const isActive = await GM.getValue('ozon_parser_active', 'false');
475 const isPaused = await GM.getValue('ozon_parser_paused', 'false');
476
477 const pauseBtn = document.createElement('button');
478 pauseBtn.className = 'ozon-parser-btn warning';
479 pauseBtn.id = 'ozon-parser-pause-btn';
480 pauseBtn.textContent = isPaused === 'true' ? '▶️ Продолжить' : '⏸️ Пауза';
481 pauseBtn.title = 'Приостановить/продолжить парсинг';
482 pauseBtn.disabled = isActive !== 'true' && isPaused !== 'true';
483 pauseBtn.onclick = async (e) => {
484 e.stopPropagation();
485 const currentlyPaused = await GM.getValue('ozon_parser_paused', 'false');
486
487 if (currentlyPaused === 'true') {
488 // Снимаем с паузы
489 await GM.setValue('ozon_parser_paused', 'false');
490 await GM.setValue('ozon_parser_active', 'true');
491 pauseBtn.textContent = '⏸️ Пауза';
492 showStatus('▶️ Парсинг продолжен', 'success', 2000);
493 createProgressPanel();
494 continueParsing();
495 } else {
496 // Ставим на паузу
497 await GM.setValue('ozon_parser_paused', 'true');
498 await GM.setValue('ozon_parser_active', 'false');
499 pauseBtn.textContent = '▶️ Продолжить';
500 showStatus('⏸️ Парсинг приостановлен', 'paused', 3000);
501 createProgressPanel();
502 }
503 };
504
505 // Кнопка Стоп
506 const stopBtn = document.createElement('button');
507 stopBtn.className = 'ozon-parser-btn danger';
508 stopBtn.textContent = '⏹️ Стоп';
509 stopBtn.title = 'Остановить парсинг полностью';
510 stopBtn.disabled = isActive !== 'true' && isPaused !== 'true';
511 stopBtn.onclick = async (e) => {
512 e.stopPropagation();
513 if (confirm('Остановить парсинг? Текущий прогресс будет сохранён.')) {
514 await GM.setValue('ozon_parser_active', 'false');
515 await GM.setValue('ozon_parser_paused', 'false');
516 await GM.setValue('ozon_parser_navigating_to', '');
517 showStatus('⏹️ Парсинг остановлен', 'error', 3000);
518 createProgressPanel();
519 updateControlButtons();
520 }
521 };
522
523 const clearBtn = document.createElement('button');
524 clearBtn.className = 'ozon-parser-btn danger';
525 clearBtn.textContent = '🗑️';
526 clearBtn.title = 'Очистить все данные';
527 clearBtn.onclick = async (e) => {
528 e.stopPropagation();
529 if (confirm('Очистить все сохраненные данные?')) {
530 await GM.setValue('ozon_parser_tasks', '[]');
531 await GM.setValue('ozon_parser_results', '[]');
532 await GM.setValue('ozon_parser_active', 'false');
533 await GM.setValue('ozon_parser_paused', 'false');
534 await GM.setValue('ozon_parser_current_task', '0');
535 await GM.setValue('ozon_parser_navigating_to', '');
536 alert('Данные очищены');
537 location.reload();
538 }
539 };
540
541 buttonsWrapper.appendChild(parseBtn);
542 buttonsWrapper.appendChild(resultsBtn);
543 buttonsWrapper.appendChild(pauseBtn);
544 buttonsWrapper.appendChild(stopBtn);
545 buttonsWrapper.appendChild(clearBtn);
546
547 container.appendChild(toggleBtn);
548 container.appendChild(buttonsWrapper);
549
550 // Позиция и drag-and-drop
551 try {
552 const posJson = await GM.getValue('ozon_parser_position', '{"top":20,"right":20}');
553 const pos = JSON.parse(posJson);
554 container.style.top = pos.top + 'px';
555 container.style.right = pos.right + 'px';
556 } catch {
557 container.style.top = '20px';
558 container.style.right = '20px';
559 }
560
561 try {
562 const collapsed = await GM.getValue('ozon_parser_collapsed', 'false');
563 if (collapsed === 'true') {
564 container.classList.add('collapsed');
565 buttonsWrapper.classList.add('hidden');
566 }
567 } catch {}
568
569 let isDragging = false, initialX, initialY;
570 container.addEventListener('mousedown', (e) => {
571 if (e.target.tagName === 'BUTTON') return;
572 isDragging = true;
573 initialX = e.clientX - container.offsetLeft;
574 initialY = e.clientY - container.offsetTop;
575 container.style.cursor = 'grabbing';
576 });
577
578 document.addEventListener('mousemove', (e) => {
579 if (!isDragging) return;
580 e.preventDefault();
581 let currentX = e.clientX - initialX;
582 let currentY = e.clientY - initialY;
583 currentX = Math.max(0, Math.min(currentX, window.innerWidth - container.offsetWidth));
584 currentY = Math.max(0, Math.min(currentY, window.innerHeight - container.offsetHeight));
585 container.style.left = currentX + 'px';
586 container.style.top = currentY + 'px';
587 container.style.right = 'auto';
588 });
589
590 document.addEventListener('mouseup', async () => {
591 if (isDragging) {
592 isDragging = false;
593 container.style.cursor = 'move';
594 const rect = container.getBoundingClientRect();
595 await GM.setValue('ozon_parser_position', JSON.stringify({
596 top: rect.top,
597 right: window.innerWidth - rect.right
598 }));
599 }
600 });
601
602 toggleBtn.addEventListener('click', async (e) => {
603 e.stopPropagation();
604 const isCollapsed = container.classList.toggle('collapsed');
605 buttonsWrapper.classList.toggle('hidden');
606 await GM.setValue('ozon_parser_collapsed', isCollapsed ? 'true' : 'false');
607 });
608
609 document.body.appendChild(container);
610 PAGE_STATE.panelCreated = true;
611 console.log('✅ UI created');
612 }
613
614 async function updateControlButtons() {
615 const pauseBtn = document.getElementById('ozon-parser-pause-btn');
616 if (!pauseBtn) return;
617
618 const isActive = await GM.getValue('ozon_parser_active', 'false');
619 const isPaused = await GM.getValue('ozon_parser_paused', 'false');
620
621 pauseBtn.textContent = isPaused === 'true' ? '▶️ Продолжить' : '⏸️ Пауза';
622 pauseBtn.disabled = isActive !== 'true' && isPaused !== 'true';
623 }
624
625 function showStatus(message, type = 'info', duration = 0) {
626 const existing = document.querySelector('.ozon-parser-status');
627 if (existing) existing.remove();
628
629 const status = document.createElement('div');
630 status.className = `ozon-parser-status ${type}`;
631 status.textContent = message;
632 document.body.appendChild(status);
633
634 if (duration > 0) {
635 setTimeout(() => status.remove(), duration);
636 }
637 return status;
638 }
639
640 function showUploadModal() {
641 const modal = document.createElement('div');
642 modal.className = 'ozon-parser-modal';
643
644 const content = document.createElement('div');
645 content.className = 'ozon-parser-modal-content';
646 content.style.maxWidth = '600px';
647
648 content.innerHTML = `
649 <div class="ozon-parser-modal-header">📁 Загрузка списка SKU</div>
650 <div class="ozon-parser-modal-body">
651 <div class="ozon-parser-info">
652 <strong>Поддерживаемые форматы:</strong><br><br>
653 <strong>Формат А (SKU + запрос):</strong><br>
654 CSV: <code>SKU,ключевой_запрос</code><br>
655 Пример: <code>12345678,витамин д3</code><br><br>
656 <strong>Формат Б (SKU + 3 конкурента):</strong><br>
657 CSV: <code>SKU_наш,SKU_конк1,SKU_конк2,SKU_конк3</code><br>
658 Пример: <code>12345678,87654321,11111111,22222222</code>
659 </div>
660 <input type="file" id="csv-file" class="ozon-parser-file-input" accept=".csv,.txt">
661 <div style="margin-top: 15px;">
662 <label style="font-size: 14px; font-weight: 600;">Или вставьте текст:</label>
663 <textarea id="manual-input" class="ozon-parser-textarea" placeholder="SKU,запрос 12345678,витамин д3"></textarea>
664 </div>
665 </div>
666 <div class="ozon-parser-modal-footer">
667 <button class="ozon-parser-btn" id="parse-file-btn">📊 Начать парсинг</button>
668 <button class="ozon-parser-btn secondary" id="close-modal-btn">Закрыть</button>
669 </div>
670 `;
671
672 modal.appendChild(content);
673 document.body.appendChild(modal);
674
675 content.querySelector('#close-modal-btn').addEventListener('click', () => modal.remove());
676
677 content.querySelector('#parse-file-btn').addEventListener('click', async () => {
678 const fileInput = content.querySelector('#csv-file');
679 const manualInput = content.querySelector('#manual-input');
680
681 let text = '';
682 if (fileInput.files.length > 0) {
683 text = await fileInput.files[0].text();
684 } else if (manualInput.value.trim()) {
685 text = manualInput.value.trim();
686 } else {
687 alert('Загрузите файл или вставьте текст');
688 return;
689 }
690
691 const tasks = parseInput(text);
692 if (tasks.length === 0) {
693 alert('Не удалось распознать данные');
694 return;
695 }
696
697 await GM.setValue('ozon_parser_tasks', JSON.stringify(tasks));
698 await GM.setValue('ozon_parser_current_task', '0');
699 await GM.setValue('ozon_parser_active', 'true');
700 await GM.setValue('ozon_parser_paused', 'false');
701 await GM.setValue('ozon_parser_results', '[]');
702 await GM.setValue('ozon_parser_navigating_to', '');
703
704 modal.remove();
705 updateControlButtons();
706 startParsing();
707 });
708
709 modal.addEventListener('click', (e) => {
710 if (e.target === modal) modal.remove();
711 });
712 }
713
714 function parseInput(text) {
715 const lines = text.trim().split('\n');
716 const tasks = [];
717
718 for (const line of lines) {
719 const trimmed = line.trim();
720 if (!trimmed || trimmed.toLowerCase().includes('sku')) continue;
721
722 const delimiter = trimmed.includes(',') ? ',' : (trimmed.includes(';') ? ';' : '\t');
723 const parts = trimmed.split(delimiter).map(p => p.trim()).filter(p => p);
724
725 if (parts.length === 2) {
726 tasks.push({
727 type: 'query',
728 ourSku: parts[0],
729 query: parts[1],
730 competitors: []
731 });
732 } else if (parts.length >= 4) {
733 tasks.push({
734 type: 'direct',
735 ourSku: parts[0],
736 query: '',
737 competitors: parts.slice(1, 4)
738 });
739 }
740 }
741 console.log('Parsed tasks:', tasks.length);
742 return tasks;
743 }
744
745 async function startParsing() {
746 const isPaused = await GM.getValue('ozon_parser_paused', 'false');
747 if (isPaused === 'true') {
748 console.log('Parsing is paused, waiting...');
749 showStatus('⏸️ Парсинг на паузе. Нажмите "Продолжить"', 'paused', 0);
750 return;
751 }
752
753 const tasksJson = await GM.getValue('ozon_parser_tasks', '[]');
754 const tasks = JSON.parse(tasksJson);
755 const currentIndex = parseInt(await GM.getValue('ozon_parser_current_task', 0));
756
757 console.log(`Starting parsing: task ${currentIndex + 1}/${tasks.length}`);
758
759 if (currentIndex >= tasks.length) {
760 console.log('All tasks completed');
761 await GM.setValue('ozon_parser_active', 'false');
762 showStatus('✅ Все задачи завершены', 'success', 3000);
763 showResultsModal();
764 updateControlButtons();
765 return;
766 }
767
768 const task = tasks[currentIndex];
769 showStatus(`Задача ${currentIndex + 1}/${tasks.length}: ${task.ourSku}`);
770 await createProgressPanel();
771 updateControlButtons();
772
773 if (task.type === 'query') {
774 const searchUrl = `https://www.ozon.ru/search/?text=${encodeURIComponent(task.query)}&from_global=true`;
775 navigateTo(searchUrl);
776 } else {
777 const url = `https://www.ozon.ru/product/${task.ourSku}`;
778 navigateTo(url);
779 }
780 }
781
782 async function continueParsing() {
783 console.log('continueParsing called');
784
785 const isActive = await GM.getValue('ozon_parser_active', 'false');
786 const isPaused = await GM.getValue('ozon_parser_paused', 'false');
787
788 if (isActive !== 'true') {
789 if (isPaused === 'true') {
790 console.log('Parsing is paused');
791 showStatus('⏸️ Парсинг на паузе', 'paused', 0);
792 await createProgressPanel();
793 }
794 return;
795 }
796
797 const navigatingTo = await GM.getValue('ozon_parser_navigating_to', '');
798 const currentUrl = window.location.href;
799
800 console.log('Expected:', navigatingTo);
801 console.log('Current:', currentUrl);
802
803 if (navigatingTo && currentUrl === 'about:blank') {
804 console.log('Page loading, waiting...');
805 setTimeout(continueParsing, 500);
806 return;
807 }
808
809 if (navigatingTo) {
810 const expectedQuery = new URL(navigatingTo).searchParams.get('text');
811 const currentQuery = new URL(currentUrl).searchParams.get('text');
812
813 const wasSearch = navigatingTo.includes('/search/') || navigatingTo.includes('text=');
814 const isSearchNow = currentUrl.includes('text=');
815
816 const isCorrectPage = wasSearch && isSearchNow && expectedQuery && currentQuery === expectedQuery;
817 const exactMatch = currentUrl.includes(navigatingTo.replace('https://www.ozon.ru', '').split('?')[0]);
818
819 if (!isCorrectPage && !exactMatch) {
820 console.log('Not on target page yet, waiting...');
821 setTimeout(continueParsing, 500);
822 return;
823 }
824
825 await GM.setValue('ozon_parser_navigating_to', '');
826 }
827
828 const tasksJson = await GM.getValue('ozon_parser_tasks', '[]');
829 const tasks = JSON.parse(tasksJson);
830 const currentIndex = parseInt(await GM.getValue('ozon_parser_current_task', 0));
831
832 if (currentIndex >= tasks.length) {
833 await GM.setValue('ozon_parser_active', 'false');
834 showResultsModal();
835 updateControlButtons();
836 return;
837 }
838
839 const task = tasks[currentIndex];
840
841 const hasTextParam = new URL(currentUrl).searchParams.has('text');
842 const isSearchPage = currentUrl.includes('/search/') || (hasTextParam && task.type === 'query');
843 const isProductPage = currentUrl.includes('/product/') || currentUrl.includes('/category/');
844
845 console.log('Task type:', task.type, 'isSearch:', isSearchPage, 'isProduct:', isProductPage);
846
847 try {
848 if (task.type === 'query' && hasTextParam) {
849 await parseSearchResults(task, tasks, currentIndex);
850 } else if (task.type === 'direct' && isProductPage) {
851 await parseProductPage(task, tasks, currentIndex);
852 } else {
853 console.warn('Page type mismatch, retrying...');
854 setTimeout(continueParsing, 1000);
855 }
856 } catch (error) {
857 console.error('Error:', error);
858 showStatus(`Ошибка: ${error.message}`, 'error');
859
860 const resultsJson = await GM.getValue('ozon_parser_results', '[]');
861 const allResults = JSON.parse(resultsJson);
862 allResults.push({
863 ourSku: task.ourSku,
864 query: task.query,
865 type: task.type,
866 error: error.message,
867 parsedAt: new Date().toISOString()
868 });
869 await GM.setValue('ozon_parser_results', JSON.stringify(allResults));
870 await GM.setValue('ozon_parser_current_task', (currentIndex + 1).toString());
871
872 setTimeout(startParsing, 2000);
873 }
874 }
875
876 async function parseSearchResults(task, tasks, currentIndex) {
877 console.log('Parsing search:', task.query);
878 showStatus(`Парсинг: ${task.query}...`);
879
880 await new Promise(resolve => setTimeout(resolve, 3000));
881
882 let productCards = document.querySelectorAll('.tile-root');
883
884 if (productCards.length === 0) {
885 productCards = document.querySelectorAll('[data-widget="searchResultsV2"] .tile-root');
886 }
887 if (productCards.length === 0) {
888 productCards = document.querySelectorAll('[class*="tile"]');
889 }
890
891 console.log('Cards found:', productCards.length);
892
893 if (productCards.length === 0) {
894 showStatus('Товаров не найдено', 'error', 3000);
895
896 const resultsJson = await GM.getValue('ozon_parser_results', '[]');
897 const allResults = JSON.parse(resultsJson);
898 allResults.push({
899 ourSku: task.ourSku,
900 query: task.query,
901 type: 'query',
902 ourProduct: { sku: task.ourSku, name: 'Не найдено', price: 0, revenue: 0 },
903 competitors: [],
904 parsedAt: new Date().toISOString()
905 });
906 await GM.setValue('ozon_parser_results', JSON.stringify(allResults));
907 await GM.setValue('ozon_parser_current_task', (currentIndex + 1).toString());
908 setTimeout(() => startParsing(), 2000);
909 return;
910 }
911
912 let ourProduct = null;
913 const competitors = [];
914 const mpstatsData = {};
915
916 const mpstatsTable = document.querySelector('table._table_ztt2p_1') ||
917 document.querySelector('table[class*="_table_"]');
918
919 if (mpstatsTable) {
920 const rows = mpstatsTable.querySelectorAll('tbody tr');
921 rows.forEach(row => {
922 const cells = row.querySelectorAll('td');
923 if (cells.length >= 7) {
924 const skuCell = cells[2]?.textContent;
925 const sku = skuCell ? skuCell.match(/(\d+)/)?.[1] : null;
926 const brand = cells[3]?.textContent?.trim() || '';
927 const priceText = cells[4]?.textContent || '';
928 const price = priceText ? parseFloat(priceText.replace(/[^\d]/g, '')) : 0;
929 const revenueText = cells[6]?.textContent || '';
930 let revenue = 0;
931 if (revenueText) {
932 const match = revenueText.match(/([\d\s]+)/);
933 if (match) revenue = parseFloat(match[1].replace(/\s/g, ''));
934 }
935 const name = cells[1]?.querySelector('img')?.getAttribute('alt') || '';
936
937 if (sku) mpstatsData[sku] = { sku, name, price, revenue, brand };
938 }
939 });
940 }
941
942 for (const card of productCards) {
943 const link = card.querySelector('a[href*="/product/"]');
944 if (!link) continue;
945
946 const href = link.getAttribute('href');
947 const skuMatch = href.match(/\/product\/[^/]+-(\d+)/);
948 if (!skuMatch) continue;
949
950 const sku = skuMatch[1];
951
952 let name = '';
953 const nameEl = card.querySelector('.tsBody500Medium') ||
954 card.querySelector('[class*="tsBody"]') ||
955 card.querySelector('span[class*="500"]');
956 if (nameEl) name = nameEl.textContent.trim();
957
958 let price = 0;
959 const priceEl = Array.from(card.querySelectorAll('span, div')).find(el =>
960 el.textContent.includes('₽') && el.textContent.match(/\d/)
961 );
962 if (priceEl) {
963 const match = priceEl.textContent.match(/[\d\s]+/);
964 if (match) price = parseFloat(match[0].replace(/\s/g, ''));
965 }
966
967 let brand = mpstatsData[sku]?.brand || '';
968 let revenue = mpstatsData[sku]?.revenue || 0;
969
970 const product = { sku, name, price, revenue, brand };
971
972 if (sku === task.ourSku) {
973 ourProduct = product;
974 } else if (competitors.length < 3 && price > 0) {
975 competitors.push(product);
976 }
977
978 if (ourProduct && competitors.length >= 3) break;
979 }
980
981 const resultsJson = await GM.getValue('ozon_parser_results', '[]');
982 const allResults = JSON.parse(resultsJson);
983 allResults.push({
984 ourSku: task.ourSku,
985 query: task.query,
986 type: 'query',
987 ourProduct: ourProduct || { sku: task.ourSku, name: 'Не найден', price: 0, revenue: 0 },
988 competitors: competitors,
989 parsedAt: new Date().toISOString()
990 });
991
992 await GM.setValue('ozon_parser_results', JSON.stringify(allResults));
993 await GM.setValue('ozon_parser_current_task', (currentIndex + 1).toString());
994
995 showStatus(`✅ ${currentIndex + 1}/${tasks.length} готово`, 'success', 2000);
996
997 if (currentIndex + 1 < tasks.length) {
998 setTimeout(() => startParsing(), 2000);
999 } else {
1000 await GM.setValue('ozon_parser_active', 'false');
1001 showResultsModal();
1002 updateControlButtons();
1003 }
1004 }
1005
1006 async function parseProductPage(task, tasks, currentIndex) {
1007 console.log('Parsing product:', task.ourSku);
1008 showStatus(`Парсинг: ${task.ourSku}...`);
1009
1010 await new Promise(resolve => setTimeout(resolve, 2000));
1011
1012 let urlSku = window.location.pathname.match(/\/product\/[^/]+-(\d+)/)?.[1];
1013
1014 if (!urlSku) {
1015 const skuEl = document.querySelector('[data-widget="webProductHeading"]') ||
1016 document.querySelector('h1');
1017 if (skuEl) {
1018 const match = skuEl.textContent.match(/(\d{8,})/);
1019 if (match) urlSku = match[1];
1020 }
1021 }
1022
1023 if (!urlSku) urlSku = task.ourSku;
1024
1025 const isOurProduct = urlSku === task.ourSku;
1026 const competitorIndex = task.competitors ? task.competitors.indexOf(urlSku) : -1;
1027
1028 let name = '';
1029 const nameSelectors = ['h1', '[data-widget="webProductHeading"] h1', '[class*="title"] h1'];
1030 for (const sel of nameSelectors) {
1031 const el = document.querySelector(sel);
1032 if (el && el.textContent.trim().length > 5) {
1033 name = el.textContent.trim();
1034 break;
1035 }
1036 }
1037
1038 // БРЕНД ТОЛЬКО ДЛЯ КОНКУРЕНТОВ
1039 let brand = '';
1040 if (!isOurProduct) {
1041 const brandSelectors = ['[data-widget="webProductHeading"] a', 'a[href*="/brand/"]', '[class*="brand"]'];
1042 for (const sel of brandSelectors) {
1043 const el = document.querySelector(sel);
1044 if (el && el.textContent.trim().length > 1 && el.textContent.trim().length < 50) {
1045 brand = el.textContent.trim();
1046 break;
1047 }
1048 }
1049 }
1050
1051 let price = 0;
1052 const allElements = document.querySelectorAll('span, div, p');
1053 for (const el of allElements) {
1054 const text = el.textContent;
1055 if (text.includes('₽') && text.match(/\d/)) {
1056 const match = text.match(/[\d\s]+/);
1057 if (match) {
1058 price = parseFloat(match[0].replace(/\s/g, ''));
1059 if (price > 100) break;
1060 }
1061 }
1062 }
1063
1064 let revenue = 0;
1065 const pageText = document.body.textContent;
1066 const patterns = [
1067 /Выручка\s+за\s+\d+\s+дней?\s+([\d\s]+)\s*₽/,
1068 /Выручка\s+([\d\s]+)\s*₽/
1069 ];
1070 for (const pattern of patterns) {
1071 const match = pageText.match(pattern);
1072 if (match) {
1073 revenue = parseFloat(match[1].replace(/\s/g, ''));
1074 break;
1075 }
1076 }
1077
1078 const resultsJson = await GM.getValue('ozon_parser_results', '[]');
1079 const allResults = JSON.parse(resultsJson);
1080
1081 let result = allResults.find(r => r.ourSku === task.ourSku);
1082 if (!result) {
1083 result = {
1084 ourSku: task.ourSku,
1085 query: '',
1086 type: 'direct',
1087 ourProduct: null,
1088 competitors: [],
1089 parsedAt: new Date().toISOString()
1090 };
1091 allResults.push(result);
1092 }
1093
1094 const productData = { sku: urlSku, name, price, revenue, brand };
1095
1096 if (isOurProduct) {
1097 // Наш товар — без бренда
1098 result.ourProduct = { sku: urlSku, name, price, revenue };
1099 } else if (competitorIndex !== -1) {
1100 // Конкурент — с брендом
1101 result.competitors[competitorIndex] = productData;
1102 } else {
1103 if (result.competitors.length < 3) {
1104 result.competitors.push(productData);
1105 }
1106 }
1107
1108 await GM.setValue('ozon_parser_results', JSON.stringify(allResults));
1109
1110 const collectedCompetitors = result.competitors.filter(c => c && c.sku).length;
1111 const totalCompetitors = task.competitors ? task.competitors.length : 0;
1112
1113 console.log('Collected competitors:', collectedCompetitors, 'Total:', totalCompetitors);
1114
1115 if (isOurProduct && collectedCompetitors < totalCompetitors) {
1116 const nextComp = task.competitors.find(c => !result.competitors.find(rc => rc && rc.sku === c));
1117 if (nextComp) {
1118 navigateTo(`https://www.ozon.ru/product/${nextComp}`);
1119 return;
1120 }
1121 }
1122
1123 if (collectedCompetitors >= totalCompetitors ||
1124 (competitorIndex !== -1 && competitorIndex === totalCompetitors - 1)) {
1125
1126 await GM.setValue('ozon_parser_current_task', (currentIndex + 1).toString());
1127 showStatus(`✅ ${currentIndex + 1}/${tasks.length} готово`, 'success', 2000);
1128
1129 if (currentIndex + 1 < tasks.length) {
1130 setTimeout(() => startParsing(), 2000);
1131 } else {
1132 await GM.setValue('ozon_parser_active', 'false');
1133 showResultsModal();
1134 updateControlButtons();
1135 }
1136 } else if (competitorIndex !== -1) {
1137 const nextIndex = competitorIndex + 1;
1138 if (nextIndex < task.competitors.length) {
1139 navigateTo(`https://www.ozon.ru/product/${task.competitors[nextIndex]}`);
1140 }
1141 }
1142 }
1143
1144 // ─── COMPARE HELPERS ────────────────────────────────────────────────────────
1145
1146 function parseQaSection() {
1147 const result = {};
1148 // Find container by class name containing pdp_qa1
1149 const allDivs = document.querySelectorAll('div');
1150 let qaContainer = null;
1151 allDivs.forEach(d => {
1152 if (d.className && typeof d.className === 'string' && d.className.includes('pdp_qa1')) {
1153 qaContainer = d;
1154 }
1155 });
1156 if (!qaContainer) {
1157 console.log('parseQaSection: pdp_qa1 not found');
1158 return result;
1159 }
1160 const h3s = qaContainer.querySelectorAll('h3');
1161 h3s.forEach(h3 => {
1162 const title = h3.textContent.trim();
1163 const p = h3.nextElementSibling;
1164 result[title] = p ? p.textContent.trim() : '';
1165 });
1166 console.log('parseQaSection result:', Object.keys(result));
1167 return result;
1168 }
1169
1170 async function startCompare(results) {
1171 console.log('startCompare: building SKU list');
1172 // Build flat list of {sku, resultIndex, role: 'our'|'competitor', competitorIndex}
1173 const skuList = [];
1174 results.forEach((r, ri) => {
1175 const ourSku = r.ourProduct ? r.ourProduct.sku : r.ourSku;
1176 skuList.push({ sku: ourSku, resultIndex: ri, role: 'our', competitorIndex: -1 });
1177 (r.competitors || []).forEach((c, ci) => {
1178 if (c && c.sku) skuList.push({ sku: c.sku, resultIndex: ri, role: 'competitor', competitorIndex: ci });
1179 });
1180 });
1181
1182 await GM.setValue('ozon_compare_list', JSON.stringify(skuList));
1183 await GM.setValue('ozon_compare_index', '0');
1184 await GM.setValue('ozon_compare_active', 'true');
1185 await GM.setValue('ozon_compare_navigating_to', '');
1186
1187 console.log('startCompare: total SKUs to visit:', skuList.length);
1188 showStatus(`🔍 Сравнение: 0 / ${skuList.length}`, 'info', 0);
1189
1190 const firstSku = skuList[0];
1191 navigateTo(`https://www.ozon.ru/product/${firstSku.sku}`);
1192 }
1193
1194 async function continueCompare() {
1195 const isActive = await GM.getValue('ozon_compare_active', 'false');
1196 if (isActive !== 'true') return;
1197
1198 const navigatingTo = await GM.getValue('ozon_compare_navigating_to', '');
1199 const currentUrl = window.location.href;
1200
1201 if (navigatingTo) {
1202 // Extract SKU (last numeric segment) from navigatingTo URL
1203 const expectedSkuMatch = navigatingTo.match(/\/product\/(?:[^/]+-)?(\d+)\/?$/);
1204 const expectedSku = expectedSkuMatch ? expectedSkuMatch[1] : null;
1205 const currentSkuMatch = currentUrl.match(/\/product\/(?:[^/]+-)?(\d+)\/?/);
1206 const currentSku = currentSkuMatch ? currentSkuMatch[1] : null;
1207
1208 console.log('continueCompare: expectedSku:', expectedSku, 'currentSku:', currentSku);
1209
1210 if (!expectedSku || expectedSku !== currentSku) {
1211 console.log('continueCompare: not on target page yet, waiting...');
1212 setTimeout(continueCompare, 500);
1213 return;
1214 }
1215 await GM.setValue('ozon_compare_navigating_to', '');
1216 }
1217
1218 const skuListJson = await GM.getValue('ozon_compare_list', '[]');
1219 const skuList = JSON.parse(skuListJson);
1220 const currentIndex = parseInt(await GM.getValue('ozon_compare_index', '0'));
1221
1222 if (currentIndex >= skuList.length) {
1223 await GM.setValue('ozon_compare_active', 'false');
1224 showStatus('✅ Сравнение завершено! Запускаю ИИ анализ...', 'info', 0);
1225 setTimeout(() => runAiAnalysisForAll(), 500);
1226 return;
1227 }
1228
1229 const item = skuList[currentIndex];
1230 showStatus(`🔍 Сравнение: ${currentIndex + 1} / ${skuList.length} (SKU: ${item.sku})`, 'info', 0);
1231
1232 // Wait for page to load
1233 await new Promise(resolve => setTimeout(resolve, 3000));
1234
1235 const qaData = parseQaSection();
1236 const dosage = qaData['Дозировка'] || '';
1237 const composition = qaData['Состав'] || '';
1238
1239 console.log(`continueCompare: SKU ${item.sku}, Дозировка: ${dosage.slice(0,50)}, Состав: ${composition.slice(0,50)}`);
1240
1241 // Save to results
1242 const resultsJson = await GM.getValue('ozon_parser_results', '[]');
1243 const allResults = JSON.parse(resultsJson);
1244 const r = allResults[item.resultIndex];
1245
1246 if (r) {
1247 if (item.role === 'our') {
1248 if (!r.ourProduct) r.ourProduct = { sku: item.sku };
1249 r.ourProduct.dosage = dosage;
1250 r.ourProduct.composition = composition;
1251 } else {
1252 const comp = r.competitors[item.competitorIndex];
1253 if (comp) {
1254 comp.dosage = dosage;
1255 comp.composition = composition;
1256 }
1257 }
1258 }
1259
1260 await GM.setValue('ozon_parser_results', JSON.stringify(allResults));
1261 await GM.setValue('ozon_compare_index', (currentIndex + 1).toString());
1262
1263 if (currentIndex + 1 < skuList.length) {
1264 const nextItem = skuList[currentIndex + 1];
1265 await GM.setValue('ozon_compare_navigating_to', `https://www.ozon.ru/product/${nextItem.sku}`);
1266 navigateTo(`https://www.ozon.ru/product/${nextItem.sku}`);
1267 } else {
1268 await GM.setValue('ozon_compare_active', 'false');
1269 showStatus('✅ Сравнение завершено! Запускаю ИИ анализ...', 'info', 0);
1270 setTimeout(() => runAiAnalysisForAll(), 500);
1271 }
1272 }
1273
1274 // ─── AI ANALYSIS ─────────────────────────────────────────────────────────────
1275
1276 const DEFAULT_COMPARISON_PROMPT = `Ты эксперт по БАДам РФ. Сравни наш товар со всеми конкурентами.
1277
1278{{OUR_BLOCK}}
1279{{COMPS_BLOCK}}
1280
1281Правила:
12821. Сравнивай со всеми конкурентами.
12832. Дозировки сверяй с ВРД/СУТ в РФ.
12843. Количество капсул оценивай в связке с составом и суточной дозой.
12854. Только факты и цифры. Минимум текста.
1286
1287Формат:
1288
1289ПОЗИЦИЯ
1290[2–3 предложения: место на рынке, главный разрыв, точка роста.]
1291
1292СИЛЬНЫЕ СТОРОНЫ
12931. [факт с цифрой]
12942. [факт с цифрой]
12953. [факт с цифрой]
1296
1297СЛАБЫЕ СТОРОНЫ
1298Количество: мы N шт / бренд N шт, бренд N шт / [в чём проблема]
1299Дозировка: [вещество] — мы X мг / бренд Y мг, бренд Z мг / ВРД V мг / [вывод]
1300Состав: нет [компонента] / есть у [бренд, бренд] / [зачем нужен, 3–5 слов]
1301
1302ЧТО СДЕЛАТЬ (от важного к менее важному)
1303→ Состав: [что добавить/убрать] — [причина, 3–5 слов]
1304→ Дозировка: [что изменить] — [причина, 3–5 слов]
1305→ Капсулы: [до скольки штук] — [причина, 3–5 слов]
1306→ Цена: [поднять/опустить/оставить] — наша [₽], конкуренты: [бренд ₽, бренд ₽], потолок [₽]
1307
1308СОСТАВ КОНКУРЕНТОВ
1309[Бренд, цена ₽:]
1310Активные: ...
1311Вспомогательные: ...
1312Качество: [прозрачность дозировок, стандартизация, вспомогательные — коротко]
1313
1314Запрещено: заключение, регуляторика, абзацы длиннее 2 предложений, блок качества нашего состава, упоминание выручки.`;
1315
1316 const DEFAULT_RECOMMENDATIONS_PROMPT = `Ты эксперт по разработке БАДов РФ.
1317
1318{{OUR_BLOCK}}
1319
1320{{COMPS_BLOCK}}
1321
1322Правила:
13231. Каждую рекомендацию объясняй подробно.
13242. Если добавляешь компонент — указывай, что он есть у конкурентов.
13253. Без регуляторики и конкурентных преимуществ.
1326
1327Формат:
1328
1329
1330Ингредиенты
1331Добавить [название], [доза]. Почему: [обоснование]. У конкурентов [название]: [доза].
1332Убрать [название]. Почему: [обоснование].
1333
1334Дозировки
1335Увеличить [вещество] с [X] до [Y]. Почему: [обоснование].
1336Уменьшить [вещество] с [X] до [Y]. Почему: [обоснование].
1337
1338Состав
1339[Что изменить чтобы быть лучше конкурентов. С обоснованием.]
1340
1341Запрещено: рекомендации без объяснений, общие фразы без цифр, упоминание выручки.`;
1342
1343 async function showPromptsModal() {
1344 const savedComparison = await GM.getValue('ozon_prompt_comparison', DEFAULT_COMPARISON_PROMPT);
1345 const savedRecommendations = await GM.getValue('ozon_prompt_recommendations', DEFAULT_RECOMMENDATIONS_PROMPT);
1346
1347 const modal = document.createElement('div');
1348 modal.className = 'ozon-parser-modal';
1349 modal.style.zIndex = '2147483648';
1350
1351 const content = document.createElement('div');
1352 content.className = 'ozon-parser-modal-content';
1353 content.style.maxWidth = '800px';
1354 content.style.width = '800px';
1355
1356 content.innerHTML = `
1357 <div class="ozon-parser-modal-header">⚙️ Настройки промптов ИИ</div>
1358 <div class="ozon-parser-modal-body">
1359 <div class="ozon-parser-info" style="margin-bottom:15px;">
1360 Используйте <strong>{{OUR_BLOCK}}</strong> и <strong>{{COMPS_BLOCK}}</strong> как плейсхолдеры — они будут заменены данными о товарах при запуске анализа.
1361 </div>
1362 <div style="margin-bottom: 20px;">
1363 <label style="font-size: 14px; font-weight: 600; display:block; margin-bottom:8px;">📊 Промпт для Сравнения:</label>
1364 <textarea id="prompt-comparison" class="ozon-parser-textarea" style="min-height:200px;">${savedComparison.replace(/</g,'<').replace(/>/g,'>')}</textarea>
1365 </div>
1366 <div style="margin-bottom: 10px;">
1367 <label style="font-size: 14px; font-weight: 600; display:block; margin-bottom:8px;">💡 Промпт для Рекомендаций:</label>
1368 <textarea id="prompt-recommendations" class="ozon-parser-textarea" style="min-height:200px;">${savedRecommendations.replace(/</g,'<').replace(/>/g,'>')}</textarea>
1369 </div>
1370 </div>
1371 <div class="ozon-parser-modal-footer">
1372 <button class="ozon-parser-btn secondary" id="save-prompts-btn">💾 Сохранить</button>
1373 <button class="ozon-parser-btn danger" id="reset-prompts-btn">🔄 Сбросить</button>
1374 <button class="ozon-parser-btn" id="close-prompts-btn">Закрыть</button>
1375 </div>
1376 `;
1377
1378 modal.appendChild(content);
1379 document.body.appendChild(modal);
1380
1381 content.querySelector('#save-prompts-btn').addEventListener('click', async () => {
1382 const compVal = content.querySelector('#prompt-comparison').value.trim();
1383 const recVal = content.querySelector('#prompt-recommendations').value.trim();
1384 if (!compVal || !recVal) { alert('Промпты не могут быть пустыми'); return; }
1385 await GM.setValue('ozon_prompt_comparison', compVal);
1386 await GM.setValue('ozon_prompt_recommendations', recVal);
1387 modal.remove();
1388 showStatus('✅ Промпты сохранены', 'success', 2000);
1389 });
1390
1391 content.querySelector('#reset-prompts-btn').addEventListener('click', async () => {
1392 if (confirm('Сбросить промпты к значениям по умолчанию?')) {
1393 await GM.setValue('ozon_prompt_comparison', DEFAULT_COMPARISON_PROMPT);
1394 await GM.setValue('ozon_prompt_recommendations', DEFAULT_RECOMMENDATIONS_PROMPT);
1395 content.querySelector('#prompt-comparison').value = DEFAULT_COMPARISON_PROMPT;
1396 content.querySelector('#prompt-recommendations').value = DEFAULT_RECOMMENDATIONS_PROMPT;
1397 showStatus('✅ Промпты сброшены', 'success', 2000);
1398 }
1399 });
1400
1401 content.querySelector('#close-prompts-btn').addEventListener('click', () => modal.remove());
1402 modal.addEventListener('click', (e) => { if (e.target === modal) modal.remove(); });
1403 }
1404
1405 // ─── OPENROUTER ──────────────────────────────────────────────────────────────
1406
1407 const DEFAULT_OPENROUTER_MODELS = [
1408 'openai/gpt-4o',
1409 'anthropic/claude-3.5-sonnet',
1410 'google/gemini-2.0-flash-001'
1411 ];
1412
1413 async function showOpenRouterModal() {
1414 const savedKey = await GM.getValue('ozon_openrouter_key', '');
1415 const savedModels = JSON.parse(await GM.getValue('ozon_openrouter_models', JSON.stringify(DEFAULT_OPENROUTER_MODELS)));
1416 const savedActive = await GM.getValue('ozon_openrouter_active_model', savedModels[0] || '');
1417
1418 const modal = document.createElement('div');
1419 modal.className = 'ozon-parser-modal';
1420 modal.style.zIndex = '2147483648';
1421
1422 const content = document.createElement('div');
1423 content.className = 'ozon-parser-modal-content';
1424 content.style.maxWidth = '600px';
1425 content.style.width = '600px';
1426
1427 const renderModelsList = (models, activeModel) => models.map((m, i) => `
1428 <div style="display:flex;align-items:center;gap:8px;padding:6px 0;border-bottom:1px solid #f0f0f0;" data-model-row="${i}">
1429 <input type="radio" name="active-model" value="${m}" ${m === activeModel ? 'checked' : ''} style="cursor:pointer;">
1430 <span style="flex:1;font-size:13px;font-family:monospace;color:#333;">${m}</span>
1431 <button class="ozon-parser-btn danger" data-remove-model="${i}" style="padding:4px 10px;font-size:11px;">✕</button>
1432 <button class="ozon-parser-btn" data-test-model="${m}" style="padding:4px 10px;font-size:11px;background:linear-gradient(135deg,#6c757d,#495057)!important;">🧪</button>
1433 </div>
1434 `).join('');
1435
1436 content.innerHTML = `
1437 <div class="ozon-parser-modal-header">🔗 Настройки OpenRouter</div>
1438 <div class="ozon-parser-modal-body">
1439 <div class="ozon-parser-info" style="margin-bottom:15px;">
1440 Если API ключ не указан — используется встроенный ИИ. Активная модель (отмечена) используется для анализа.
1441 </div>
1442 <div style="margin-bottom:18px;">
1443 <label style="font-size:13px;font-weight:600;display:block;margin-bottom:6px;">🔑 API ключ OpenRouter:</label>
1444 <div style="display:flex;gap:8px;">
1445 <input type="password" id="or-api-key" value="${savedKey}" placeholder="sk-or-v1-..." style="flex:1;padding:10px 12px;border:2px solid #e0e0e0;border-radius:8px;font-size:13px;font-family:monospace;">
1446 <button class="ozon-parser-btn" id="or-toggle-key" style="padding:10px 14px;font-size:13px;">👁</button>
1447 </div>
1448 </div>
1449 <div style="margin-bottom:12px;">
1450 <label style="font-size:13px;font-weight:600;display:block;margin-bottom:6px;">🤖 Модели (выберите активную):</label>
1451 <div id="or-models-list" style="margin-bottom:10px;">${renderModelsList(savedModels, savedActive)}</div>
1452 <div style="display:flex;gap:8px;">
1453 <input type="text" id="or-new-model" placeholder="openai/gpt-4o-mini" style="flex:1;padding:8px 12px;border:2px solid #e0e0e0;border-radius:8px;font-size:13px;font-family:monospace;">
1454 <button class="ozon-parser-btn secondary" id="or-add-model" style="padding:8px 14px;font-size:13px;">+ Добавить</button>
1455 </div>
1456 </div>
1457 <div id="or-test-result" style="display:none;padding:10px 14px;border-radius:8px;font-size:13px;margin-top:10px;"></div>
1458 </div>
1459 <div class="ozon-parser-modal-footer">
1460 <button class="ozon-parser-btn secondary" id="or-save-btn">💾 Сохранить</button>
1461 <button class="ozon-parser-btn danger" id="or-clear-btn">🗑 Очистить ключ</button>
1462 <button class="ozon-parser-btn" id="or-close-btn">Закрыть</button>
1463 </div>
1464 `;
1465
1466 modal.appendChild(content);
1467 document.body.appendChild(modal);
1468
1469 let currentModels = [...savedModels];
1470
1471 const rerender = () => {
1472 const activeRadio = content.querySelector('input[name="active-model"]:checked');
1473 const currentActive = activeRadio ? activeRadio.value : (currentModels[0] || '');
1474 content.querySelector('#or-models-list').innerHTML = renderModelsList(currentModels, currentActive);
1475 bindModelEvents();
1476 };
1477
1478 const bindModelEvents = () => {
1479 content.querySelectorAll('[data-remove-model]').forEach(btn => {
1480 btn.addEventListener('click', () => {
1481 const idx = parseInt(btn.getAttribute('data-remove-model'));
1482 currentModels.splice(idx, 1);
1483 rerender();
1484 });
1485 });
1486
1487 content.querySelectorAll('[data-test-model]').forEach(btn => {
1488 btn.addEventListener('click', async () => {
1489 const model = btn.getAttribute('data-test-model');
1490 const key = content.querySelector('#or-api-key').value.trim();
1491 const testDiv = content.querySelector('#or-test-result');
1492 if (!key) {
1493 testDiv.style.display = 'block';
1494 testDiv.style.background = '#fff3cd';
1495 testDiv.style.color = '#856404';
1496 testDiv.textContent = '⚠️ Введите API ключ для проверки';
1497 return;
1498 }
1499 btn.textContent = '⏳';
1500 btn.disabled = true;
1501 testDiv.style.display = 'block';
1502 testDiv.style.background = '#e7f3ff';
1503 testDiv.style.color = '#004085';
1504 testDiv.textContent = `🧪 Проверяю модель ${model}...`;
1505 try {
1506 const resp = await GM.xmlhttpRequest({
1507 method: 'POST',
1508 url: 'https://openrouter.ai/api/v1/chat/completions',
1509 headers: {
1510 'Authorization': `Bearer ${key}`,
1511 'Content-Type': 'application/json',
1512 'HTTP-Referer': 'https://ozon.ru',
1513 'X-Title': 'Ozon Parser'
1514 },
1515 data: JSON.stringify({
1516 model: model,
1517 messages: [{ role: 'user', content: 'Ответь одним словом: работает' }],
1518 max_tokens: 10
1519 })
1520 });
1521 const data = JSON.parse(resp.responseText);
1522 if (data.choices && data.choices[0]) {
1523 testDiv.style.background = '#d4edda';
1524 testDiv.style.color = '#155724';
1525 testDiv.textContent = `✅ Модель ${model} работает! Ответ: "${data.choices[0].message.content.trim()}"`;
1526 } else if (data.error) {
1527 testDiv.style.background = '#f8d7da';
1528 testDiv.style.color = '#721c24';
1529 testDiv.textContent = `❌ Ошибка: ${data.error.message}`;
1530 }
1531 } catch (err) {
1532 testDiv.style.background = '#f8d7da';
1533 testDiv.style.color = '#721c24';
1534 testDiv.textContent = `❌ Ошибка запроса: ${err.message}`;
1535 }
1536 btn.textContent = '🧪';
1537 btn.disabled = false;
1538 });
1539 });
1540 };
1541
1542 rerender();
1543
1544 content.querySelector('#or-toggle-key').addEventListener('click', () => {
1545 const inp = content.querySelector('#or-api-key');
1546 inp.type = inp.type === 'password' ? 'text' : 'password';
1547 });
1548
1549 content.querySelector('#or-add-model').addEventListener('click', () => {
1550 const val = content.querySelector('#or-new-model').value.trim();
1551 if (!val) return;
1552 if (currentModels.includes(val)) { alert('Модель уже добавлена'); return; }
1553 currentModels.push(val);
1554 content.querySelector('#or-new-model').value = '';
1555 rerender();
1556 });
1557
1558 content.querySelector('#or-save-btn').addEventListener('click', async () => {
1559 const key = content.querySelector('#or-api-key').value.trim();
1560 const activeRadio = content.querySelector('input[name="active-model"]:checked');
1561 const activeModel = activeRadio ? activeRadio.value : (currentModels[0] || '');
1562 await GM.setValue('ozon_openrouter_key', key);
1563 await GM.setValue('ozon_openrouter_models', JSON.stringify(currentModels));
1564 await GM.setValue('ozon_openrouter_active_model', activeModel);
1565 modal.remove();
1566 showStatus('✅ Настройки OpenRouter сохранены', 'success', 2000);
1567 });
1568
1569 content.querySelector('#or-clear-btn').addEventListener('click', async () => {
1570 if (confirm('Очистить API ключ OpenRouter?')) {
1571 await GM.setValue('ozon_openrouter_key', '');
1572 content.querySelector('#or-api-key').value = '';
1573 showStatus('🗑 Ключ очищен', 'info', 2000);
1574 }
1575 });
1576
1577 content.querySelector('#or-close-btn').addEventListener('click', () => modal.remove());
1578 modal.addEventListener('click', (e) => { if (e.target === modal) modal.remove(); });
1579 }
1580
1581 async function openRouterAiCall(prompt, apiKey, model) {
1582 const resp = await GM.xmlhttpRequest({
1583 method: 'POST',
1584 url: 'https://openrouter.ai/api/v1/chat/completions',
1585 headers: {
1586 'Authorization': `Bearer ${apiKey}`,
1587 'Content-Type': 'application/json',
1588 'HTTP-Referer': 'https://ozon.ru',
1589 'X-Title': 'Ozon Parser'
1590 },
1591 data: JSON.stringify({
1592 model: model,
1593 messages: [{ role: 'user', content: prompt }]
1594 })
1595 });
1596 const data = JSON.parse(resp.responseText);
1597 if (data.error) throw new Error(data.error.message || JSON.stringify(data.error));
1598 if (!data.choices || !data.choices[0]) throw new Error('Пустой ответ от OpenRouter');
1599 return data.choices[0].message.content;
1600 }
1601
1602 async function openRouterAiCallWithRetry(prompt, apiKey, model, maxRetries = 3) {
1603 for (let attempt = 1; attempt <= maxRetries; attempt++) {
1604 try {
1605 return await openRouterAiCall(prompt, apiKey, model);
1606 } catch (err) {
1607 const errMsg = err.message || '';
1608 const isRetryable = errMsg.includes('provider') || errMsg.includes('rate') || errMsg.includes('overload') || errMsg.includes('timeout') || errMsg.includes('502') || errMsg.includes('503');
1609 console.warn(`openRouterAiCallWithRetry: attempt ${attempt}/${maxRetries} failed: ${errMsg}`);
1610 if (attempt < maxRetries && isRetryable) {
1611 const delay = attempt * 5000;
1612 showStatus(`⏳ ИИ: ошибка провайдера, повтор через ${delay/1000}с (попытка ${attempt}/${maxRetries})...`, 'info', 0);
1613 await new Promise(resolve => setTimeout(resolve, delay));
1614 } else {
1615 throw err;
1616 }
1617 }
1618 }
1619 }
1620
1621 // ─────────────────────────────────────────────────────────────────────────────
1622
1623 async function runAiAnalysisForAll() {
1624 const resultsJson = await GM.getValue('ozon_parser_results', '[]');
1625 const allResults = JSON.parse(resultsJson);
1626
1627 let anyNeedsAnalysis = false;
1628 for (const r of allResults) {
1629 if (!r.aiComparison || !r.aiRecommendations) { anyNeedsAnalysis = true; break; }
1630 }
1631 if (!anyNeedsAnalysis) {
1632 console.log('runAiAnalysisForAll: all results already have AI analysis');
1633 showResultsModal();
1634 return;
1635 }
1636
1637 const comparisonPromptTemplate = await GM.getValue('ozon_prompt_comparison', DEFAULT_COMPARISON_PROMPT);
1638 const recommendationsPromptTemplate = await GM.getValue('ozon_prompt_recommendations', DEFAULT_RECOMMENDATIONS_PROMPT);
1639
1640 const orKey = await GM.getValue('ozon_openrouter_key', '');
1641 const orModel = await GM.getValue('ozon_openrouter_active_model', '');
1642 const useOpenRouter = !!(orKey && orModel);
1643
1644 console.log(`runAiAnalysisForAll: using ${useOpenRouter ? 'OpenRouter model: ' + orModel : 'built-in RM.aiCall'}`);
1645 showStatus(`🤖 ИИ анализирует... (${useOpenRouter ? orModel : 'встроенный'})`, 'info', 0);
1646
1647 const aiCall = useOpenRouter
1648 ? (prompt) => openRouterAiCallWithRetry(prompt, orKey, orModel)
1649 : (prompt) => RM.aiCall(prompt);
1650
1651 for (let i = 0; i < allResults.length; i++) {
1652 const r = allResults[i];
1653 if (r.aiComparison && r.aiRecommendations) continue;
1654
1655 const our = r.ourProduct || { sku: r.ourSku, name: '—', dosage: '', composition: '' };
1656 const comps = (r.competitors || []).filter(c => c && c.sku);
1657 const aiComparison = (r.aiComparison || '—').replace(/</g, '<').replace(/>/g, '>');
1658 const aiRecommendations = (r.aiRecommendations || '—').replace(/</g, '<').replace(/>/g, '>');
1659 const ourBlock = `НАШ ТОВАР:\nНазвание: ${our.name || '—'}\nДозировка: ${our.dosage || 'не указана'}\nСостав: ${our.composition || 'не указан'}`;
1660
1661 const compsBlock = comps.map((c, ci) => `КОНКУРЕНТ ${ci + 1}:\nНазвание: ${c.name || '—'}\nБренд: ${c.brand || '—'}\nЦена: ${c.price || '—'} ₽\nДозировка: ${c.dosage || 'не указана'}\nСостав: ${c.composition || 'не указан'}`).join('\n\n');
1662
1663 const comparisonPrompt = comparisonPromptTemplate
1664 .replace('{{OUR_BLOCK}}', ourBlock)
1665 .replace('{{COMPS_BLOCK}}', compsBlock);
1666
1667 const recommendationsPrompt = recommendationsPromptTemplate
1668 .replace('{{OUR_BLOCK}}', ourBlock)
1669 .replace('{{COMPS_BLOCK}}', compsBlock);
1670
1671 try {
1672 const [comparison, recommendations] = await Promise.all([
1673 aiCall(comparisonPrompt),
1674 aiCall(recommendationsPrompt)
1675 ]);
1676 r.aiComparison = comparison;
1677 r.aiRecommendations = recommendations;
1678 console.log(`AI analysis done for SKU ${r.ourSku}`);
1679 } catch (err) {
1680 console.error(`AI analysis failed for SKU ${r.ourSku}:`, err);
1681 r.aiComparison = 'Ошибка ИИ: ' + err.message;
1682 r.aiRecommendations = 'Ошибка ИИ: ' + err.message;
1683 }
1684
1685 await GM.setValue('ozon_parser_results', JSON.stringify(allResults));
1686 }
1687
1688 showStatus('✅ ИИ анализ завершён!', 'success', 3000);
1689 setTimeout(() => showResultsModal(), 500);
1690 }
1691
1692 // ─────────────────────────────────────────────────────────────────────────────
1693
1694 function showAiTextModal(title, text) {
1695 const modal = document.createElement('div');
1696 modal.className = 'ozon-parser-modal';
1697 modal.style.zIndex = '2147483649';
1698
1699 const content = document.createElement('div');
1700 content.className = 'ozon-parser-modal-content';
1701 content.style.maxWidth = '700px';
1702 content.style.width = '700px';
1703
1704 content.innerHTML = `
1705 <div class="ozon-parser-modal-header">${title}</div>
1706 <div class="ozon-parser-modal-body">
1707 <div style="white-space:pre-wrap;font-size:14px;line-height:1.7;color:#333;word-wrap:break-word;">${text.replace(/</g,'<').replace(/>/g,'>')}</div>
1708 </div>
1709 <div class="ozon-parser-modal-footer">
1710 <button class="ozon-parser-btn" id="ai-text-close-btn">Закрыть</button>
1711 </div>
1712 `;
1713
1714 modal.appendChild(content);
1715 document.body.appendChild(modal);
1716
1717 content.querySelector('#ai-text-close-btn').addEventListener('click', () => modal.remove());
1718 modal.addEventListener('click', (e) => { if (e.target === modal) modal.remove(); });
1719 }
1720
1721 async function showResultsModal() {
1722 const resultsJson = await GM.getValue('ozon_parser_results', '[]');
1723 const results = JSON.parse(resultsJson);
1724
1725 if (results.length === 0) {
1726 alert('Нет результатов');
1727 return;
1728 }
1729
1730 const modal = document.createElement('div');
1731 modal.className = 'ozon-parser-modal';
1732
1733 const content = document.createElement('div');
1734 content.className = 'ozon-parser-modal-content';
1735
1736 // Store AI texts by index to avoid attribute escaping issues
1737 const aiTextsStore = [];
1738
1739 let html = `
1740 <div class="ozon-parser-modal-header">📊 Результаты (${results.length})</div>
1741 <div class="ozon-parser-modal-body">
1742 <table class="ozon-parser-results-table">
1743 <thead>
1744 <tr>
1745 <th>Наш SKU</th>
1746 <th>Название</th>
1747 <th>Цена</th>
1748 <th>Выручка</th>
1749 <th>Дозировка</th>
1750 <th>Состав</th>
1751 <th>Сравнение (ИИ)</th>
1752 <th>Рекомендации (ИИ)</th>
1753 <th>Конкурент SKU</th>
1754 <th>Название конк.</th>
1755 <th>Цена конк.</th>
1756 <th>Выручка конк.</th>
1757 <th>Бренд конк.</th>
1758 <th>Дозировка конк.</th>
1759 <th>Состав конк.</th>
1760 <th>Запрос</th>
1761 </tr>
1762 </thead>
1763 <tbody>
1764 `;
1765
1766 for (const r of results) {
1767 const our = r.ourProduct || { sku: r.ourSku, name: '—', price: 0, revenue: 0 };
1768 const comps = (r.competitors || []).filter(c => c && c.sku);
1769 const aiComparison = r.aiComparison || '—';
1770 const aiRecommendations = r.aiRecommendations || '—';
1771 const ourName = (our.name || '—').replace(/</g, '<').replace(/>/g, '>');
1772 const ourDosage = (our.dosage || '—').replace(/</g, '<').replace(/>/g, '>');
1773 const ourComposition = (our.composition || '—').replace(/</g, '<').replace(/>/g, '>');
1774 const aiComparisonEsc = aiComparison.replace(/</g, '<').replace(/>/g, '>');
1775 const aiRecommendationsEsc = aiRecommendations.replace(/</g, '<').replace(/>/g, '>');
1776
1777 const compIdx = aiTextsStore.push({ title: '📊 Сравнение (ИИ)', text: aiComparison }) - 1;
1778 const recIdx = aiTextsStore.push({ title: '💡 Рекомендации (ИИ)', text: aiRecommendations }) - 1;
1779
1780 const renderAiCell = (textEsc, storeIdx) => `
1781 <td class="ozon-parser-ai-cell">
1782 <span class="ozon-parser-ai-cell-preview">${textEsc}</span>
1783 <button class="ozon-parser-ai-cell-more-btn" data-ai-idx="${storeIdx}">Подробнее</button>
1784 </td>`;
1785
1786 if (comps.length === 0) {
1787 html += `<tr>
1788 <td><a href="https://www.ozon.ru/product/${our.sku}" target="_blank" class="ozon-parser-sku-link">${our.sku}</a></td>
1789 <td class="ozon-parser-name-cell">${ourName}</td>
1790 <td>${our.price || '—'}</td>
1791 <td>${our.revenue || '—'}</td>
1792 <td class="ozon-parser-dosage-cell">${ourDosage}</td>
1793 <td class="ozon-parser-composition-cell">${ourComposition}</td>
1794 ${renderAiCell(aiComparisonEsc, compIdx)}
1795 ${renderAiCell(aiRecommendationsEsc, recIdx)}
1796 <td colspan="7" style="text-align:center;color:#999;white-space:nowrap;">Конкуренты не найдены</td>
1797 <td>${r.query || '—'}</td>
1798 </tr>`;
1799 } else {
1800 comps.forEach((c, i) => {
1801 const cName = (c.name || '—').replace(/</g, '<').replace(/>/g, '>');
1802 const cDosage = (c.dosage || '—').replace(/</g, '<').replace(/>/g, '>');
1803 const cComposition = (c.composition || '—').replace(/</g, '<').replace(/>/g, '>');
1804 html += `<tr ${i === 0 ? 'class="ozon-parser-highlight"' : ''}>
1805 ${i === 0 ? `
1806 <td rowspan="${comps.length}"><a href="https://www.ozon.ru/product/${our.sku}" target="_blank" class="ozon-parser-sku-link">${our.sku}</a></td>
1807 <td rowspan="${comps.length}" class="ozon-parser-name-cell">${ourName}</td>
1808 <td rowspan="${comps.length}">${our.price || '—'}</td>
1809 <td rowspan="${comps.length}">${our.revenue || '—'}</td>
1810 <td rowspan="${comps.length}" class="ozon-parser-dosage-cell">${ourDosage}</td>
1811 <td rowspan="${comps.length}" class="ozon-parser-composition-cell">${ourComposition}</td>
1812 <td rowspan="${comps.length}" class="ozon-parser-ai-cell">
1813 <span class="ozon-parser-ai-cell-preview">${aiComparisonEsc}</span>
1814 <button class="ozon-parser-ai-cell-more-btn" data-ai-idx="${compIdx}">Подробнее</button>
1815 </td>
1816 <td rowspan="${comps.length}" class="ozon-parser-ai-cell">
1817 <span class="ozon-parser-ai-cell-preview">${aiRecommendationsEsc}</span>
1818 <button class="ozon-parser-ai-cell-more-btn" data-ai-idx="${recIdx}">Подробнее</button>
1819 </td>
1820 ` : ''}
1821 <td><a href="https://www.ozon.ru/product/${c.sku}" target="_blank" class="ozon-parser-sku-link">${c.sku}</a></td>
1822 <td class="ozon-parser-name-cell">${cName}</td>
1823 <td>${c.price || '—'}</td>
1824 <td>${c.revenue || '—'}</td>
1825 <td class="ozon-parser-brand-cell">${c.brand || '—'}</td>
1826 <td class="ozon-parser-dosage-cell">${cDosage}</td>
1827 <td class="ozon-parser-composition-cell">${cComposition}</td>
1828 ${i === 0 ? `<td rowspan="${comps.length}">${r.query || '—'}</td>` : ''}
1829 </tr>`;
1830 });
1831 }
1832 }
1833
1834 html += `</tbody></table></div>
1835 <div class="ozon-parser-modal-footer">
1836 <button class="ozon-parser-btn warning" id="compare-btn">🔍 Сравнить</button>
1837 <button class="ozon-parser-btn" id="run-ai-btn">🤖 Запустить ИИ</button>
1838 <button class="ozon-parser-btn" id="prompts-btn">⚙️ Промпты</button>
1839 <button class="ozon-parser-btn" id="openrouter-btn">🔗 OpenRouter</button>
1840 <button class="ozon-parser-btn secondary" id="export-btn">📥 CSV</button>
1841 <button class="ozon-parser-btn" id="close-btn">Закрыть</button>
1842 </div>`;
1843
1844 content.innerHTML = html;
1845 modal.appendChild(content);
1846 document.body.appendChild(modal);
1847
1848 // Delegate click for "Подробнее" buttons — use aiTextsStore to avoid attribute escaping issues
1849 content.addEventListener('click', (e) => {
1850 const btn = e.target.closest('[data-ai-idx]');
1851 if (btn) {
1852 const idx = parseInt(btn.getAttribute('data-ai-idx'));
1853 const entry = aiTextsStore[idx];
1854 if (entry) showAiTextModal(entry.title, entry.text);
1855 }
1856 });
1857
1858 content.querySelector('#compare-btn').addEventListener('click', async () => {
1859 if (confirm(`Запустить сравнение? Будут открыты страницы ${results.reduce((acc, r) => acc + 1 + (r.competitors||[]).filter(c=>c&&c.sku).length, 0)} товаров для парсинга Дозировки и Состава.`)) {
1860 modal.remove();
1861 await startCompare(results);
1862 }
1863 });
1864 content.querySelector('#run-ai-btn').addEventListener('click', async () => {
1865 // Disable all footer buttons and show loading state
1866 const footerBtns = content.querySelectorAll('.ozon-parser-modal-footer button');
1867 footerBtns.forEach(b => { b.disabled = true; });
1868 const runAiBtn = content.querySelector('#run-ai-btn');
1869 runAiBtn.textContent = '⏳ ИИ анализирует...';
1870
1871 // Clear existing AI results so runAiAnalysisForAll will process them
1872 const rJson = await GM.getValue('ozon_parser_results', '[]');
1873 const rAll = JSON.parse(rJson);
1874 rAll.forEach(r => { delete r.aiComparison; delete r.aiRecommendations; });
1875 await GM.setValue('ozon_parser_results', JSON.stringify(rAll));
1876
1877 console.log('run-ai-btn: cleared AI results, starting analysis...');
1878 await runAiAnalysisForAll();
1879
1880 modal.remove();
1881 });
1882 content.querySelector('#prompts-btn').addEventListener('click', () => showPromptsModal());
1883 content.querySelector('#openrouter-btn').addEventListener('click', () => showOpenRouterModal());
1884 content.querySelector('#export-btn').addEventListener('click', () => exportToCSV(results));
1885 content.querySelector('#close-btn').addEventListener('click', () => modal.remove());
1886 modal.addEventListener('click', (e) => { if (e.target === modal) modal.remove(); });
1887 }
1888
1889 function exportToCSV(results) {
1890 let csv = 'Наш SKU;Название;Цена;Выручка;Дозировка;Состав;Сравнение (ИИ);Рекомендации (ИИ);Конкурент SKU;Название конк.;Цена конк.;Выручка конк.;Бренд конк.;Дозировка конк.;Состав конк.;Запрос\n';
1891
1892 for (const r of results) {
1893 const our = r.ourProduct || {};
1894 const comps = (r.competitors || []).filter(c => c && c.sku);
1895 const aiComparison = (r.aiComparison || '').replace(/"/g, '""').replace(/\n/g, ' ');
1896 const aiRecommendations = (r.aiRecommendations || '').replace(/"/g, '""').replace(/\n/g, ' ');
1897
1898 if (comps.length === 0) {
1899 csv += `${r.ourSku};"${(our.name||'').replace(/"/g,'""')}";${our.price||''};${our.revenue||''};"${(our.dosage||'').replace(/"/g,'""')}";"${(our.composition||'').replace(/"/g,'""')}";"${aiComparison}";"${aiRecommendations}";;;;;;;;;"${r.query||''}"\n`;
1900 } else {
1901 comps.forEach((c, i) => {
1902 const ourAi = i === 0 ? `"${aiComparison}";"${aiRecommendations}"` : '"";""';
1903 csv += `${r.ourSku};"${(our.name||'').replace(/"/g,'""')}";${our.price||''};${our.revenue||''};"${(our.dosage||'').replace(/"/g,'""')}";"${(our.composition||'').replace(/"/g,'""')}";${ourAi};${c.sku};"${(c.name||'').replace(/"/g,'""')}";${c.price||''};${c.revenue||''};"${(c.brand||'').replace(/"/g,'""')}";"${(c.dosage||'').replace(/"/g,'""')}";"${(c.composition||'').replace(/"/g,'""')}";"${r.query||''}"\n`;
1904 });
1905 }
1906 }
1907
1908 const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
1909 const link = document.createElement('a');
1910 link.href = URL.createObjectURL(blob);
1911 link.download = `ozon_results_${new Date().toISOString().slice(0,10)}.csv`;
1912 link.click();
1913 }
1914
1915 async function init() {
1916 console.log('🚀 Init v3.0');
1917
1918 addStyles();
1919 await createUI();
1920 await createProgressPanel();
1921
1922 const compareActive = await GM.getValue('ozon_compare_active', 'false');
1923 if (compareActive === 'true') {
1924 console.log('Resuming compare...');
1925 setTimeout(continueCompare, 1000);
1926 } else {
1927 await GM.setValue('ozon_parser_navigating_to', '');
1928 setTimeout(continueParsing, 1000);
1929 }
1930 }
1931
1932 if (document.readyState === 'loading') {
1933 document.addEventListener('DOMContentLoaded', init);
1934 } else {
1935 init();
1936 }
1937})();