Генератор SEO-описаний для товаров на Ozon с анализом ключевых слов
Size
139.1 KB
Version
3.2.67
Created
Jan 22, 2026
Updated
26 days ago
1// ==UserScript==
2// @name Ozon Description Generator 3.0
3// @description Генератор SEO-описаний для товаров на Ozon с анализом ключевых слов
4// @version 3.2.67
5// @match https://*.seller.ozon.ru/*
6// @icon https://cdn.ozon.ru/s3/cms/branding/favicon/favicon-32x32-9e4e5b2c9b.png
7// ==/UserScript==
8(function() {
9 'use strict';
10
11 // ============================================
12 // УТИЛИТЫ
13 // ============================================
14
15 function debounce(func, wait) {
16 let timeout;
17 return function executedFunction(...args) {
18 clearTimeout(timeout);
19 timeout = setTimeout(() => func(...args), wait);
20 };
21 }
22
23 function formatNumber(num) {
24 if (num >= 1000000) {
25 return (num / 1000000).toFixed(1) + 'M';
26 } else if (num >= 1000) {
27 return (num / 1000).toFixed(1) + 'K';
28 }
29 return num.toString();
30 }
31
32 // ============================================
33 // AI PROMPT GENERATORS
34 // ============================================
35
36 function generateMasksPrompt(productInfo) {
37 return `Ты — эксперт по SEO на OZON. Предложи поисковые маски и ключевые слова ДЛЯ КОНКРЕТНОГО ТОВАРА.
38
39ДАННЫЕ О ТОВАРЕ:
40• Название: ${productInfo.title || 'не указано'}
41
42ВАЖНОЕ ПРАВИЛО:
431. АНАЛИЗИРУЙ название товара и используй ТОЧНО ТОТ ТИП ТОВАРА, который указан в названии
442. НЕ заменяй тип товара на другой, даже если похоже
453. Если в названии "маска для лица" — ВСЕ ключи должны быть про МАСКУ, не про сыворотку, крем или другие товары
464. Если в названии "сыворотка" — все ключи про сыворотку
475. Если в названии "крем" — все ключи про крем
48
49Предложи 2 типа запросов:
50
511. МАСКИ (15 штук) — короткие слова:
52 - Используй ТОЧНО тип товара из названия
53 - Пример для "маска для лица": ["маска", "маска для", "для лица", ...]
54 - НЕЛЬЗЯ: ["сыворотка", "крем", "гель"] если это не указано в названии
55
562. КЛЮЧЕВЫЕ СЛОВА (15 штук) — точные запросы:
57 - Начинай с типа товара из названия
58 - Пример для "маска для лица": ["маска для лица", "тканевая маска для лица", ...]
59 - НЕЛЬЗЯ: ["сыворотка для лица", "крем для лица", ...] если это не маска
60
61КАТЕГОРИИ ДЛЯ МАСОК:
62- ТИП ТОВАРА ИЗ НАЗВАНИЯ (3 маски): основное название
63- ТИП ТОВАРА + предлог (4 маски)
64- Синонимы ТОГО ЖЕ ТИПА (2 маски)
65- Назначение (2 маски)
66- Ингредиенты из названия (3 маски)
67- Эффект (1 маска)
68
69КАТЕГОРИИ ДЛЯ КЛЮЧЕВЫХ СЛОВ:
70- ТИП ТОВАРА + назначение (6 ключей)
71- ТИП ТОВАРА + аудитория (2 ключа)
72- ТИП ТОВАРА + ингредиент (5 ключей)
73- ТИП ТОВАРА + эффект (2 ключей)
74
75ПРАВИЛА:
76- Используй ТОЧНО тип товара из названия, не меняй его
77- Все слова на русском (кроме названий компонентов типа "hyaluronic", "niacinamide")
78- Без брендов
79
80ПРИМЕР ДЛЯ "Маска для лица с гиалуроновой кислотой":
81{
82 "masks": ["маска", "маска для", "для лица", "тканевая", "патчи", "гиалуроновая", "алоэ", "увлажнение", "освежение", "уход", "косметика", "средство", "средство для", "уходовая", "гидратация"],
83 "keywords": ["маска для лица", "тканевая маска для лица", "маска для лица увлажняющая", "маска с гиалуроновой кислотой", "гиалуроновая маска для лица", "маска для лица с алоэ", "увлажняющая маска для лица", "маска для лица тканевая", "освежающая маска для лица", "маска для лица гиалуроновая", "маска для лица для увлажнения", "маска для лица ежедневного ухода", "маска для лица против сухости", "маска для лица уходовая", "маска для лица с алоэ вера"]
84}
85
86ПРИМЕР ДЛЯ "Сыворотка для лица с витамином С":
87{
88 "masks": ["сыворотка", "сыворотка для", "серум", "витамин с", "ниацинамид", "антиоксидант", "осветление", "для лица", "уход", "косметика", "средство", "средство для", "антивозрастная", "витаминная", "активная"],
89 "keywords": ["сыворотка для лица", "сыворотка с витамином с", "витамин с для лица", "осветляющая сыворотка", "сыворотка для лица с ниацинамидом", "антиоксидантная сыворотка", "сыворотка против пигментации", "антивозрастная сыворотка", "сыворотка для сияния кожи", "сыворотка для лица витаминная", "сыворотка для лица активная", "сыворотка для ровного тона", "сыворотка с антиоксидантами", "сыворотка для осветления", "сыворотка для ухода за лицом"]
90}
91
92Верни ТОЛЬКО JSON:
93{
94 "masks": ["маска1", "маска2", ...],
95 "keywords": ["ключ1", "ключ2", ...]
96}
97
98Начни ответ сразу с {`;
99 }
100
101 function generateDescriptionPrompt(productInfo, keywords, filteredQueries, queryPopularity = {}, customPrompt = '') {
102 const sortedQueries = filteredQueries
103 .map(q => ({ query: q, pop: queryPopularity[q.toLowerCase()] || 0 }))
104 .sort((a, b) => b.pop - a.pop);
105
106 const highPriority = sortedQueries.slice(0, 25).map(q => q.query);
107 const mediumPriority = sortedQueries.slice(25, 70).map(q => q.query);
108 const lowPriority = sortedQueries.slice(70, 120).map(q => q.query);
109
110 const basePrompt = `Создай SEO-описание товара для Ozon.
111
112ДАННЫЕ:
113• Название: ${productInfo.title || 'не указано'}
114• Базовые ключи: ${keywords.join(', ')}
115
116ЗАПРОСЫ ПО ПРИОРИТЕТУ:
117
118🔴 ВЫСОКИЙ ПРИОРИТЕТ (использовать ВСЕ 100%, минимум 1 раз каждый):
119${highPriority.map((q, i) => `${i+1}. "${q}"`).join('\n')}
120
121🟡 СРЕДНИЙ ПРИОРИТЕТ (использовать 80-90%):
122${mediumPriority.length > 0 ? mediumPriority.map((q, i) => `${i+1}. "${q}"`).join('\n') : 'нет запросов'}
123
124🟢 НИЗКИЙ ПРИОРИТЕТ (использовать 50-70%):
125${lowPriority.length > 0 ? lowPriority.map((q, i) => `${i+1}. "${q}"`).join('\n') : 'нет запросов'}
126
127ТРЕБОВАНИЯ:
1281) Объем 3500–4000 символов, только текст.
1292) Структура (каждая часть с нового абзаца. между абзацами пустая строка):
130 - Введение (400-500 симв.) — суть товара, что это, зачем, для кого, что делает кратко.
131 - Проблема и решение (600-800 симв.) — основная проблема, которую решает это средство, к чему приводит проблема. Кратко как этот продукт решает проблему.
132 - Состав и действие (800-1000 симв.) — ингредиенты и их польза. Подробная информация о составе и компонентах которые усиливают друг друга. как они работают, зачем они нужны.
133 - Применение (600-800 симв.) — для кого, как работает
134 - Отзывы покупателей (300-400 симв.) — отдельный абзац, который начинается фразой "Наши покупатели отмечают ... через ...", где описан эффект и сроки появления результата
135 - Заключение (400-500 симв.) — результат для покупателя. что получит покупатель при использовании средства.
1363) Плавные переходы между частями, без заголовков.
1374) Все базовые ключи — минимум 1 раз.
1385) Каждый запрос использовать не более 2 раз.
1396) Если компонента из запроса нет в составе — используй через «альтернативу» (засчитывается).
1407) Размер текста минимум 3000 символов - ПРОВЕРЬ!
141
142ГРУППИРОВКА ЗАПРОСОВ:
1438) Сгруппируй запросы по смыслу:
144 • Тип/категория товара
145 • Проблемы/назначение
146 • Состав/ингредиенты
147 • Аудитория
148 • Эффект/результат
1499) В каждом абзаце используй запросы из 2-3 групп одновременно.
15010) Комбинируй разные типы запросов в одном предложении естественным образом.
151
152ПРАВИЛА ПЛОТНОСТИ КЛЮЧЕЙ:
15311) Плотность ключей: 4-5 запросов на 100 слов (это нормально для SEO).
15412) В каждом абзаце минимум 3-4 ключевых запроса.
15513) Встраивай ключи естественно в середину и начало предложений.
15614) Используй синонимы и вариации ключей для естественности.
15715) Одно и то же ключевое словосочетание — максимум 2 раза в тексте.
15816) Чередуй короткие (2-3 слова) и длинные (4-5 слов) ключи.
159
160СТИЛЬ:
16117) Информативно, конкретные факты, без воды.
16218) Между абзацами пустая строка.
16319) Чередуй длину предложений: 30% короткие (8-12 слов), 70% средние (13-20 слов).
16420) Используй разные конструкции предложений для естественности.
165
166ЗАПРЕТЫ:
16721) Только русский язык. Без брендов, компаний, фамилий.
16822) Запрещены слова: «лекарство», «препарат», «революционный», «инновационный», «уникальный».
16923) Без заголовков, списков, вопросов, эмоджи, инструкций хранения и описания неактивных компонентов.
170
171ПРИМЕР ХОРОШЕГО АБЗАЦА С ВЫСОКОЙ ПЛОТНОСТЬЮ КЛЮЧЕЙ:
172Натуральный гель для интимной гигиены обеспечивает деликатный уход и поддерживает естественный pH баланс. Средство для интимной гигиены с мягкой формулой подходит для ежедневного применения и помогает предотвратить дискомфорт. Гель с пантенолом и экстрактом лаванды увлажняет кожу деликатных зон, а водная основа делает текстуру легкой и комфортной. Интимный гель для женщин не содержит агрессивных компонентов и подходит даже для чувствительной кожи.
173
174ПРИМЕР ПЛОХОГО АБЗАЦА (НЕ ДЕЛАЙ ТАК):
175Гель для интимной гигиены очищает. Интимный гель увлажняет. Гель для женщин помогает.
176
177ВАЖНО:
178- Используй МАКСИМУУ запросов из списка
179- Вплетай их естественно, но плотно
180- Не бойся использовать 4-5 ключей на 100 слов
181- Главное - сохранять читаемость и естественность
182
183${customPrompt ? `\n\nДОПОЛНИТЕЛЬНЫЕ ТРЕБОВАНИЯ:\n${customPrompt}\n` : ''}
184
185ВЫВОД:
186Начни сразу с описания. Никаких вступлений, вопросов и пояснений.`;
187
188 return basePrompt;
189 }
190
191 // ============================================
192 // СТИЛИ
193 // ============================================
194
195 TM_addStyle(`
196 .ozon-desc-modal {
197 position: fixed;
198 top: 0;
199 left: 0;
200 width: 100%;
201 height: 100%;
202 background: rgba(0, 0, 0, 0.5);
203 display: flex;
204 align-items: center;
205 justify-content: center;
206 z-index: 10000;
207 }
208
209 .ozon-desc-modal-content {
210 background: white;
211 border-radius: 12px;
212 padding: 24px;
213 max-width: 700px;
214 width: 90%;
215 max-height: 85vh;
216 overflow-y: auto;
217 box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
218 }
219
220 .ozon-desc-modal-header {
221 font-size: 22px;
222 font-weight: 600;
223 margin-bottom: 20px;
224 color: #001a34;
225 }
226
227 .ozon-desc-input-group {
228 margin-bottom: 16px;
229 }
230
231 .ozon-desc-label {
232 display: block;
233 margin-bottom: 8px;
234 font-weight: 500;
235 color: #001a34;
236 font-size: 16px;
237 }
238
239 .ozon-desc-textarea {
240 width: 100%;
241 min-height: 100px;
242 padding: 12px;
243 border: 1px solid #d1d5db;
244 border-radius: 8px;
245 font-size: 16px;
246 font-family: inherit;
247 resize: vertical;
248 box-sizing: border-box;
249 }
250
251 .ozon-desc-result {
252 background: #f3f4f6;
253 padding: 16px;
254 border-radius: 8px;
255 margin-bottom: 16px;
256 max-height: 300px;
257 overflow-y: auto;
258 white-space: pre-wrap;
259 word-wrap: break-word;
260 font-size: 15px;
261 line-height: 1.6;
262 }
263
264 .ozon-desc-char-count {
265 text-align: right;
266 font-size: 14px;
267 color: #6b7280;
268 margin-top: 4px;
269 }
270
271 .ozon-desc-char-count.success {
272 color: #10b981;
273 }
274
275 .ozon-desc-buttons {
276 display: flex;
277 gap: 12px;
278 justify-content: flex-end;
279 margin-top: 20px;
280 flex-wrap: wrap;
281 }
282
283 .ozon-desc-btn {
284 padding: 10px 20px;
285 border: none;
286 border-radius: 8px;
287 font-size: 16px;
288 font-weight: 500;
289 cursor: pointer;
290 transition: all 0.2s;
291 }
292
293 .ozon-desc-btn-primary {
294 background: linear-gradient(135deg, #005bff, #0041cc);
295 color: white;
296 }
297
298 .ozon-desc-btn-primary:hover {
299 background: linear-gradient(135deg, #0041cc, #0033a0);
300 transform: translateY(-1px);
301 }
302
303 .ozon-desc-btn-primary:disabled {
304 background: #9ca3af;
305 cursor: not-allowed;
306 transform: none;
307 }
308
309 .ozon-desc-btn-secondary {
310 background: #e5e7eb;
311 color: #374151;
312 }
313
314 .ozon-desc-btn-secondary:hover {
315 background: #d1d5db;
316 }
317
318 .ozon-desc-btn-success {
319 background: linear-gradient(135deg, #10b981, #059669);
320 color: white;
321 }
322
323 .ozon-desc-btn-success:hover {
324 background: linear-gradient(135deg, #059669, #047857);
325 transform: translateY(-1px);
326 }
327
328 .ozon-desc-generator-btn {
329 margin-top: 12px;
330 padding: 10px 20px;
331 background: linear-gradient(135deg, #005bff, #0041cc);
332 color: white;
333 border: none;
334 border-radius: 8px;
335 font-size: 16px;
336 font-weight: 500;
337 cursor: pointer;
338 transition: all 0.2s;
339 width: 100%;
340 }
341
342 .ozon-desc-generator-btn:hover {
343 background: linear-gradient(135deg, #0041cc, #0033a0);
344 transform: translateY(-2px);
345 }
346
347 .ozon-desc-status {
348 margin-top: 12px;
349 padding: 12px 16px;
350 border-radius: 8px;
351 font-size: 15px;
352 }
353
354 .ozon-desc-status.info {
355 background: #dbeafe;
356 color: #1e40af;
357 border-left: 4px solid #3b82f6;
358 }
359
360 .ozon-desc-status.success {
361 background: #d1fae5;
362 color: #065f46;
363 border-left: 4px solid #10b981;
364 }
365
366 .ozon-desc-status.error {
367 background: #fee2e2;
368 color: #991b1b;
369 border-left: 4px solid #ef4444;
370 }
371
372 .ozon-desc-suggest-btn {
373 margin-top: 8px;
374 padding: 8px 16px;
375 background: linear-gradient(135deg, #6366f1, #8b5cf6);
376 color: white;
377 border: none;
378 border-radius: 6px;
379 font-size: 15px;
380 font-weight: 500;
381 cursor: pointer;
382 transition: all 0.2s;
383 }
384
385 .ozon-desc-suggest-btn:hover {
386 background: linear-gradient(135deg, #4f46e5, #7c3aed);
387 transform: translateY(-1px);
388 }
389
390 .ozon-desc-suggest-btn:disabled {
391 background: #9ca3af;
392 cursor: not-allowed;
393 transform: none;
394 }
395
396 .ozon-desc-masks-container {
397 margin-top: 12px;
398 padding: 16px;
399 background: #f9fafb;
400 border-radius: 8px;
401 border: 1px solid #e5e7eb;
402 }
403
404 .ozon-desc-masks-header {
405 font-weight: 600;
406 margin-bottom: 12px;
407 color: #374151;
408 font-size: 15px;
409 display: flex;
410 justify-content: space-between;
411 align-items: center;
412 flex-wrap: wrap;
413 gap: 8px;
414 }
415
416 .ozon-desc-masks-group {
417 margin-bottom: 12px;
418 }
419
420 .ozon-desc-masks-group:last-child {
421 margin-bottom: 0;
422 }
423
424 .ozon-desc-masks-group-title {
425 font-size: 12px;
426 color: #6b7280;
427 margin-bottom: 8px;
428 text-transform: uppercase;
429 letter-spacing: 0.5px;
430 }
431
432 .ozon-desc-masks-grid {
433 display: flex;
434 flex-wrap: wrap;
435 gap: 8px;
436 }
437
438 .ozon-mask-chip {
439 display: inline-flex;
440 align-items: center;
441 gap: 6px;
442 padding: 8px 14px;
443 border-radius: 20px;
444 font-size: 14px;
445 cursor: pointer;
446 transition: all 0.2s;
447 border: 2px solid transparent;
448 user-select: none;
449 }
450
451 .ozon-mask-chip:hover {
452 transform: translateY(-2px);
453 box-shadow: 0 4px 12px rgba(0,0,0,0.1);
454 }
455
456 .ozon-mask-chip.selected {
457 border-color: #10b981;
458 box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2);
459 }
460
461 .ozon-mask-chip[data-type="маска"] { background: #dbeafe; color: #1e40af; }
462 .ozon-mask-chip[data-type="ключ"] { background: #fef3c7; color: #92400e; }
463
464 .ozon-desc-stats {
465 margin-top: 12px;
466 padding: 14px;
467 background: linear-gradient(135deg, #f3f4f6, #e5e7eb);
468 border-radius: 8px;
469 font-size: 14px;
470 }
471
472 .ozon-desc-stats-row {
473 display: flex;
474 justify-content: space-between;
475 margin-bottom: 6px;
476 }
477
478 .ozon-desc-stats-row:last-child {
479 margin-bottom: 0;
480 }
481
482 .ozon-desc-usage-link {
483 color: #6366f1;
484 text-decoration: underline;
485 cursor: pointer;
486 font-size: 14px;
487 }
488
489 .ozon-desc-usage-link:hover {
490 color: #4f46e5;
491 }
492
493 .ozon-desc-analytics-modal {
494 position: fixed;
495 top: 0;
496 left: 0;
497 width: 100%;
498 height: 100%;
499 background: rgba(0, 0, 0, 0.5);
500 display: flex;
501 align-items: center;
502 justify-content: center;
503 z-index: 10001;
504 }
505
506 .ozon-desc-analytics-content {
507 background: white;
508 border-radius: 12px;
509 padding: 24px;
510 max-width: 900px;
511 width: 95%;
512 max-height: 85vh;
513 overflow-y: auto;
514 box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
515 }
516
517 .ozon-desc-query-item {
518 padding: 10px 14px;
519 margin-bottom: 6px;
520 border-radius: 8px;
521 font-size: 14px;
522 display: flex;
523 justify-content: space-between;
524 align-items: center;
525 transition: all 0.2s;
526 }
527
528 .ozon-desc-query-item.used {
529 background: #d1fae5;
530 color: #065f46;
531 }
532
533 .ozon-desc-query-item.unused {
534 background: #f3f4f6;
535 color: #6b7280;
536 }
537
538 .ozon-desc-query-text {
539 flex: 1;
540 }
541
542 .ozon-desc-query-popularity {
543 font-weight: 600;
544 margin-left: 12px;
545 min-width: 50px;
546 text-align: right;
547 }
548
549 .ozon-desc-minus-words-section {
550 margin-bottom: 16px;
551 padding: 14px;
552 background: linear-gradient(135deg, #fef3c7, #fde68a);
553 border-radius: 8px;
554 border: 1px solid #fbbf24;
555 }
556
557 .ozon-desc-minus-words-header {
558 font-weight: 600;
559 margin-bottom: 10px;
560 color: #92400e;
561 font-size: 15px;
562 }
563
564 .ozon-desc-minus-words-list {
565 display: flex;
566 flex-wrap: wrap;
567 gap: 8px;
568 }
569
570 .ozon-desc-minus-word-chip {
571 background: #fbbf24;
572 color: #78350f;
573 padding: 6px 12px;
574 border-radius: 16px;
575 font-size: 14px;
576 display: flex;
577 align-items: center;
578 gap: 6px;
579 cursor: pointer;
580 transition: all 0.2s;
581 }
582
583 .ozon-desc-minus-word-chip:hover {
584 background: #f59e0b;
585 transform: translateY(-1px);
586 }
587
588 .ozon-desc-minus-word-remove {
589 font-weight: bold;
590 font-size: 16px;
591 }
592
593 .ozon-desc-query-word {
594 cursor: pointer;
595 padding: 2px 4px;
596 border-radius: 3px;
597 transition: background 0.2s;
598 }
599
600 .ozon-desc-query-word:hover {
601 background: #fef3c7;
602 }
603
604 .ozon-desc-search-input {
605 width: 100%;
606 padding: 12px 14px;
607 border: 1px solid #d1d5db;
608 border-radius: 8px;
609 font-size: 15px;
610 margin-bottom: 16px;
611 box-sizing: border-box;
612 }
613
614 .ozon-desc-search-input:focus {
615 outline: none;
616 border-color: #005bff;
617 box-shadow: 0 0 0 3px rgba(0, 91, 255, 0.1);
618 }
619
620 /* Стили для автогенерации */
621 .ozon-autogen-btn {
622 padding: 12px 24px;
623 background: linear-gradient(135deg, #f59e0b, #d97706);
624 color: white;
625 border: none;
626 border-radius: 8px;
627 font-size: 16px;
628 font-weight: 600;
629 cursor: pointer;
630 transition: all 0.2s;
631 box-shadow: 0 2px 8px rgba(245, 158, 11, 0.3);
632 }
633
634 .ozon-autogen-btn:hover {
635 background: linear-gradient(135deg, #d97706, #b45309);
636 transform: translateY(-2px);
637 box-shadow: 0 4px 12px rgba(245, 158, 11, 0.4);
638 }
639
640 .ozon-autogen-progress {
641 position: fixed;
642 top: 20px;
643 right: 20px;
644 background: white;
645 border-radius: 12px;
646 padding: 20px;
647 box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
648 z-index: 10002;
649 min-width: 350px;
650 max-width: 400px;
651 }
652
653 .ozon-autogen-progress-header {
654 font-size: 18px;
655 font-weight: 600;
656 margin-bottom: 16px;
657 color: #001a34;
658 display: flex;
659 justify-content: space-between;
660 align-items: center;
661 }
662
663 .ozon-autogen-progress-close {
664 cursor: pointer;
665 font-size: 24px;
666 color: #6b7280;
667 line-height: 1;
668 }
669
670 .ozon-autogen-progress-close:hover {
671 color: #374151;
672 }
673
674 .ozon-autogen-progress-info {
675 margin-bottom: 12px;
676 padding: 12px;
677 background: #f3f4f6;
678 border-radius: 8px;
679 font-size: 14px;
680 }
681
682 .ozon-autogen-progress-stats {
683 display: flex;
684 gap: 16px;
685 margin-bottom: 16px;
686 flex-wrap: wrap;
687 }
688
689 .ozon-autogen-progress-stat {
690 flex: 1;
691 min-width: 100px;
692 }
693
694 .ozon-autogen-progress-stat-label {
695 font-size: 12px;
696 color: #6b7280;
697 margin-bottom: 4px;
698 }
699
700 .ozon-autogen-progress-stat-value {
701 font-size: 20px;
702 font-weight: 600;
703 color: #001a34;
704 }
705
706 .ozon-autogen-progress-stat-value.success {
707 color: #10b981;
708 }
709
710 .ozon-autogen-progress-stat-value.error {
711 color: #ef4444;
712 }
713
714 .ozon-autogen-progress-controls {
715 display: flex;
716 gap: 8px;
717 }
718
719 .ozon-autogen-progress-btn {
720 flex: 1;
721 padding: 8px 16px;
722 border: none;
723 border-radius: 6px;
724 font-size: 14px;
725 font-weight: 500;
726 cursor: pointer;
727 transition: all 0.2s;
728 }
729
730 .ozon-autogen-progress-btn.pause {
731 background: #fbbf24;
732 color: #78350f;
733 }
734
735 .ozon-autogen-progress-btn.pause:hover {
736 background: #f59e0b;
737 }
738
739 .ozon-autogen-progress-btn.stop {
740 background: #ef4444;
741 color: white;
742 }
743
744 .ozon-autogen-progress-btn.stop:hover {
745 background: #dc2626;
746 }
747
748 .ozon-autogen-progress-btn.resume {
749 background: #10b981;
750 color: white;
751 }
752
753 .ozon-autogen-progress-btn.resume:hover {
754 background: #059669;
755 }
756
757 .ozon-product-progress {
758 position: fixed;
759 top: 20px;
760 right: 20px;
761 background: white;
762 border-radius: 12px;
763 padding: 20px;
764 box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
765 z-index: 10002;
766 min-width: 300px;
767 }
768
769 .ozon-product-progress-header {
770 font-size: 16px;
771 font-weight: 600;
772 margin-bottom: 12px;
773 color: #001a34;
774 }
775
776 .ozon-product-progress-stage {
777 padding: 8px 12px;
778 margin-bottom: 8px;
779 border-radius: 6px;
780 font-size: 14px;
781 display: flex;
782 align-items: center;
783 gap: 8px;
784 }
785
786 .ozon-product-progress-stage.pending {
787 background: #f3f4f6;
788 color: #6b7280;
789 }
790
791 .ozon-product-progress-stage.active {
792 background: #dbeafe;
793 color: #1e40af;
794 font-weight: 500;
795 }
796
797 .ozon-product-progress-stage.completed {
798 background: #d1fae5;
799 color: #065f46;
800 }
801
802 .ozon-product-progress-stage.error {
803 background: #fee2e2;
804 color: #991b1b;
805 }
806
807 .ozon-checkbox-container {
808 display: flex;
809 align-items: center;
810 gap: 8px;
811 margin-top: 12px;
812 }
813
814 .ozon-checkbox {
815 width: 18px;
816 height: 18px;
817 cursor: pointer;
818 }
819
820 .ozon-checkbox-label {
821 font-size: 15px;
822 color: #374151;
823 cursor: pointer;
824 user-select: none;
825 }
826 `);
827
828 // ============================================
829 // ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ
830 // ============================================
831
832 let lastUrl = location.href;
833 let isProcessingProduct = false; // Флаг для предотвращения параллельной обработки
834
835 // ============================================
836 // МОНИТОРИНГ URL (SPA)
837 // ============================================
838
839 const debouncedUrlCheck = debounce(() => {
840 const url = location.href;
841 if (url !== lastUrl) {
842 lastUrl = url;
843
844 if (url.includes('seller.ozon.ru/app/products/') && url.includes('/edit/all-attrs')) {
845 setTimeout(init, 1000);
846 }
847 }
848 }, 300);
849
850 new MutationObserver(debouncedUrlCheck).observe(document, { subtree: true, childList: true });
851
852 // ============================================
853 // ПОЛУЧЕНИЕ ИНФОРМАЦИИ О ТОВАРЕ
854 // ============================================
855
856 async function getProductInfo() {
857 // Получаем SKU из URL
858 const urlMatch = window.location.href.match(/\/products\/(\d+)\//);
859 const sku = urlMatch ? urlMatch[1] : null;
860
861 // СТРОГАЯ ПРОВЕРКА SKU
862 if (!sku || !/^\d+$/.test(sku)) {
863 console.error('Ozon Description Generator: Некорректный SKU в URL:', sku);
864 return { title: '', sku: null };
865 }
866
867 // Получаем название товара
868 let title = '';
869 const titleInput = document.querySelector('input[name="name"]');
870 if (titleInput) {
871 title = titleInput.value;
872 }
873
874 return { title, sku };
875 }
876
877 // ============================================
878 // СОЗДАНИЕ КНОПКИ ГЕНЕРАТОРА
879 // ============================================
880
881 function createGeneratorButton() {
882 // Ищем контейнер с аннотацией (описанием)
883 const annotationContainer = document.querySelector('[id="attribute#4191"]')?.closest('.index_formField_P3SmU');
884
885 if (!annotationContainer) {
886 return false;
887 }
888
889 if (document.querySelector('.ozon-desc-generator-btn')) {
890 return true;
891 }
892
893 const buttonsContainer = document.createElement('div');
894 buttonsContainer.style.cssText = 'display: flex; gap: 8px; margin-top: 12px;';
895
896 const generatorButton = document.createElement('button');
897 generatorButton.className = 'ozon-desc-generator-btn';
898 generatorButton.textContent = '✨ Генератор описаний';
899 generatorButton.type = 'button';
900 generatorButton.style.width = 'auto';
901 generatorButton.style.flex = '1';
902 generatorButton.addEventListener('click', function(e) {
903 e.preventDefault();
904 openModal();
905 });
906
907 const richContentButton = document.createElement('button');
908 richContentButton.className = 'ozon-desc-generator-btn';
909 richContentButton.textContent = '📄 Отправить в Rich-контент';
910 richContentButton.type = 'button';
911 richContentButton.style.width = 'auto';
912 richContentButton.style.flex = '1';
913 richContentButton.style.background = 'linear-gradient(135deg, #10b981, #059669)';
914 richContentButton.addEventListener('click', function(e) {
915 e.preventDefault();
916 sendToRichContent();
917 });
918 richContentButton.addEventListener('mouseenter', function() {
919 this.style.background = 'linear-gradient(135deg, #059669, #047857)';
920 });
921 richContentButton.addEventListener('mouseleave', function() {
922 this.style.background = 'linear-gradient(135deg, #10b981, #059669)';
923 });
924
925 buttonsContainer.appendChild(generatorButton);
926 buttonsContainer.appendChild(richContentButton);
927
928 annotationContainer.appendChild(buttonsContainer);
929 return true;
930 }
931
932 // ============================================
933 // ПОКАЗ СТАТУСА
934 // ============================================
935
936 function showStatus(container, message, type) {
937 container.innerHTML = `<div class="ozon-desc-status ${type}">${message}</div>`;
938 }
939
940 // ============================================
941 // МОДАЛЬНОЕ ОКНО
942 // ============================================
943
944 async function openModal() {
945 const productInfo = await getProductInfo();
946 const currentSKU = productInfo.sku;
947
948 let savedKeywords = '';
949 let savedMinusWords = '';
950 let savedPrompt = '';
951
952 if (currentSKU) {
953 savedKeywords = await GM.getValue(`ozon_product_${currentSKU}_keywords`, '');
954 savedMinusWords = await GM.getValue(`ozon_product_${currentSKU}_minus_words`, '');
955 savedPrompt = await GM.getValue(`ozon_product_${currentSKU}_custom_prompt`, '');
956 }
957
958 // Предустановленные промпты
959 const presetPrompts = [
960 { name: 'Без дополнительных требований', value: '' },
961 { name: 'Акцент на натуральность', value: 'Сделай акцент на натуральности состава, экологичности и безопасности для здоровья.' },
962 { name: 'Премиум-сегмент', value: 'Используй стиль премиум-сегмента: подчеркни эксклюзивность, высокое качество и статусность продукта.' },
963 { name: 'Для чувствительной кожи', value: 'Особое внимание уделить гипоаллергенности, мягкости формулы и подходу для чувствительной кожи.' },
964 { name: 'Антивозрастной уход', value: 'Акцентируй внимание на антивозрастных свойствах, омоложении и борьбе с признаками старения.' },
965 { name: 'Быстрый результат', value: 'Подчеркни быстроту достижения видимого результата и эффективность средства.' }
966 ];
967
968 const modal = document.createElement('div');
969 modal.className = 'ozon-desc-modal';
970 modal.innerHTML = `
971 <div class="ozon-desc-modal-content">
972 <div class="ozon-desc-modal-header">✨ Генератор описаний для Ozon</div>
973
974 <div class="ozon-desc-input-group">
975 <label class="ozon-desc-label">Введите ключевые слова (каждое с новой строки):</label>
976 <textarea class="ozon-desc-textarea" id="ozon-keywords-input" placeholder="Например: сыворотка для лица витамин с увлажнение">${savedKeywords}</textarea>
977 <button class="ozon-desc-suggest-btn" id="ozon-suggest-keywords-btn">🔍 Предложить поисковые маски</button>
978 <div id="ozon-suggested-keywords-container" style="display: none;"></div>
979 </div>
980
981 <div class="ozon-desc-input-group">
982 <label class="ozon-desc-label">Минус-слова (каждое с новой строки):</label>
983 <textarea class="ozon-desc-textarea" style="min-height: 80px;" id="ozon-minus-words-input" placeholder="Например: mixit nivea корея">${savedMinusWords}</textarea>
984 </div>
985
986 <div class="ozon-desc-input-group">
987 <label class="ozon-desc-label">Дополнительные требования к описанию:</label>
988 <select class="ozon-desc-textarea" id="ozon-prompt-preset-select" style="min-height: auto; padding: 10px; margin-bottom: 8px;">
989 ${presetPrompts.map(preset => `<option value="${preset.value}">${preset.name}</option>`).join('')}
990 </select>
991 <textarea class="ozon-desc-textarea" style="min-height: 80px;" id="ozon-custom-prompt-input" placeholder="Или напишите свои требования к стилю и содержанию описания...">${savedPrompt}</textarea>
992 </div>
993
994 <div id="ozon-desc-result-container" style="display: none;">
995 <div class="ozon-desc-label">Сгенерированное описание:</div>
996 <div class="ozon-desc-result" id="ozon-desc-result"></div>
997 <div class="ozon-desc-char-count" id="ozon-char-count"></div>
998 <div id="ozon-desc-stats-container"></div>
999 </div>
1000
1001 <div id="ozon-desc-status-container"></div>
1002
1003 <div class="ozon-desc-buttons">
1004 <button class="ozon-desc-btn ozon-desc-btn-secondary" id="ozon-close-btn">Закрыть</button>
1005 <button class="ozon-desc-btn ozon-desc-btn-primary" id="ozon-generate-btn">🚀 Сгенерировать</button>
1006 <button class="ozon-desc-btn ozon-desc-btn-primary" id="ozon-regenerate-btn" style="display: none;">🔄 Перегенерировать</button>
1007 <button class="ozon-desc-btn ozon-desc-btn-success" id="ozon-insert-btn" style="display: none;">✅ Вставить в описание</button>
1008 </div>
1009 </div>
1010 `;
1011
1012 document.body.appendChild(modal);
1013
1014 // Обработчик выбора пресета промпта
1015 document.getElementById('ozon-prompt-preset-select').addEventListener('change', (e) => {
1016 const customPromptInput = document.getElementById('ozon-custom-prompt-input');
1017 if (e.target.value) {
1018 customPromptInput.value = e.target.value;
1019 }
1020 });
1021
1022 modal.addEventListener('click', (e) => {
1023 if (e.target === modal) {
1024 modal.remove();
1025 }
1026 });
1027
1028 document.getElementById('ozon-close-btn').addEventListener('click', () => {
1029 modal.remove();
1030 });
1031
1032 document.getElementById('ozon-suggest-keywords-btn').addEventListener('click', () => {
1033 suggestKeywords();
1034 });
1035
1036 document.getElementById('ozon-generate-btn').addEventListener('click', () => {
1037 generateDescription(modal);
1038 });
1039
1040 document.getElementById('ozon-regenerate-btn').addEventListener('click', () => {
1041 generateDescription(modal, true);
1042 });
1043
1044 document.getElementById('ozon-insert-btn').addEventListener('click', () => {
1045 insertDescription(modal);
1046 });
1047 }
1048
1049 // ============================================
1050 // ПРЕДЛОЖЕНИЕ КЛЮЧЕВЫХ СЛОВ / МАСОК
1051 // ============================================
1052
1053 async function suggestKeywords() {
1054 const keywordsInput = document.getElementById('ozon-keywords-input');
1055 const suggestBtn = document.getElementById('ozon-suggest-keywords-btn');
1056 const suggestedContainer = document.getElementById('ozon-suggested-keywords-container');
1057 const statusContainer = document.getElementById('ozon-desc-status-container');
1058
1059 const keywordsText = keywordsInput.value.trim();
1060
1061 if (!keywordsText) {
1062 showStatus(statusContainer, 'Пожалуйста, сначала введите базовые ключевые слова', 'error');
1063 return;
1064 }
1065
1066 suggestBtn.disabled = true;
1067 suggestBtn.textContent = '⏳ AI анализирует...';
1068 showStatus(statusContainer, 'AI анализирует товар и подбирает поисковые маски и ключевые слова...', 'info');
1069
1070 try {
1071 const productInfo = await getProductInfo();
1072
1073 const suggestPrompt = generateMasksPrompt(productInfo);
1074 console.log('Ozon Description Generator: Запрос масок от AI');
1075
1076 const suggestResponse = await RM.aiCall(suggestPrompt);
1077
1078 let masks = [];
1079 let aiKeywords = [];
1080 try {
1081 const suggestData = JSON.parse(suggestResponse);
1082 masks = Array.isArray(suggestData.masks) ? suggestData.masks.filter(Boolean) : [];
1083 aiKeywords = Array.isArray(suggestData.keywords) ? suggestData.keywords.filter(Boolean) : [];
1084 console.log(`Ozon Description Generator: AI предложил ${masks.length} масок и ${aiKeywords.length} ключей`);
1085 } catch (e) {
1086 console.error('Ozon Description Generator: Ошибка парсинга масок:', e);
1087 showStatus(statusContainer, 'Ошибка при получении предложений. Попробуйте еще раз.', 'error');
1088 return;
1089 }
1090
1091 if (masks.length === 0 && aiKeywords.length === 0) {
1092 showStatus(statusContainer, 'AI не смог предложить маски и ключевые слова', 'error');
1093 return;
1094 }
1095
1096 const hasChips = masks.length + aiKeywords.length > 0;
1097
1098 suggestedContainer.innerHTML = `
1099 <div class="ozon-desc-masks-container">
1100 <div class="ozon-desc-masks-header">
1101 <span>Поисковые маски и ключевые слова (кликните для выбора):</span>
1102 ${hasChips ? `<button class="ozon-desc-suggest-btn" id="ozon-toggle-all-btn" style="padding: 4px 12px; font-size: 13px;">
1103 Выбрать все
1104 </button>` : ''}
1105 </div>
1106 ${masks.length ? `
1107 <div class="ozon-desc-masks-group">
1108 <div class="ozon-desc-masks-group-title">МАСКИ</div>
1109 <div class="ozon-desc-masks-grid">
1110 ${masks.map(mask => `
1111 <div class="ozon-mask-chip" data-type="маска" data-mask="${mask}">
1112 <span>${mask}</span>
1113 </div>
1114 `).join('')}
1115 </div>
1116 </div>
1117 ` : ''}
1118 ${aiKeywords.length ? `
1119 <div class="ozon-desc-masks-group">
1120 <div class="ozon-desc-masks-group-title">КЛЮЧЕВЫЕ СЛОВА</div>
1121 <div class="ozon-desc-masks-grid">
1122 ${aiKeywords.map(keyword => `
1123 <div class="ozon-mask-chip" data-type="ключ" data-mask="${keyword}">
1124 <span>${keyword}</span>
1125 </div>
1126 `).join('')}
1127 </div>
1128 </div>
1129 ` : ''}
1130 </div>
1131 `;
1132
1133 suggestedContainer.style.display = 'block';
1134
1135 suggestedContainer.querySelectorAll('.ozon-mask-chip').forEach(chip => {
1136 chip.addEventListener('click', () => {
1137 chip.classList.toggle('selected');
1138 updateToggleButtonText();
1139 });
1140 });
1141
1142 function updateToggleButtonText() {
1143 const chips = suggestedContainer.querySelectorAll('.ozon-mask-chip');
1144 const allSelected = Array.from(chips).every(c => c.classList.contains('selected'));
1145 const toggleBtn = document.getElementById('ozon-toggle-all-btn');
1146 if (toggleBtn) {
1147 toggleBtn.textContent = allSelected ? 'Снять все' : 'Выбрать все';
1148 }
1149 }
1150
1151 const toggleBtn = document.getElementById('ozon-toggle-all-btn');
1152 if (toggleBtn) {
1153 toggleBtn.addEventListener('click', () => {
1154 const chips = suggestedContainer.querySelectorAll('.ozon-mask-chip');
1155 const allSelected = Array.from(chips).every(c => c.classList.contains('selected'));
1156
1157 chips.forEach(c => {
1158 if (allSelected) {
1159 c.classList.remove('selected');
1160 } else {
1161 c.classList.add('selected');
1162 }
1163 });
1164
1165 updateToggleButtonText();
1166 });
1167 }
1168
1169 showStatus(statusContainer, `AI предложил ${masks.length} масок и ${aiKeywords.length} ключевых слов. Выберите нужные и нажмите "Сгенерировать"`, 'success');
1170
1171 } catch (error) {
1172 console.error('Ozon Description Generator: Ошибка при предложении масок:', error);
1173 showStatus(statusContainer, 'Ошибка при получении предложений: ' + error.message, 'error');
1174 } finally {
1175 suggestBtn.disabled = false;
1176 suggestBtn.textContent = '🔍 Предложить поисковые маски';
1177 }
1178 }
1179
1180 // ============================================
1181 // СБОР ДАННЫХ С АНАЛИТИКИ
1182 // ============================================
1183
1184 async function collectAnalyticsData(keywords, minusWords) {
1185 console.log('Ozon Description Generator: Начало сбора данных с аналитики');
1186 console.log('Ozon Description Generator: Минус-слова для фильтрации:', minusWords);
1187
1188 // Проверяем, не идет ли уже сбор данных
1189 const currentStatus = await GM.getValue('ozon_collection_status', 'none');
1190 if (currentStatus === 'pending') {
1191 console.log('Ozon Description Generator: Обнаружен статус pending, сбрасываем и начинаем заново');
1192 await GM.setValue('ozon_collection_status', 'none');
1193 }
1194
1195 await GM.setValue('ozon_keywords_to_process', JSON.stringify(keywords));
1196 await GM.setValue('ozon_minus_words', JSON.stringify(minusWords));
1197 await GM.setValue('ozon_analytics_data', JSON.stringify([]));
1198 await GM.setValue('ozon_collection_status', 'pending');
1199
1200 const analyticsUrl = 'https://seller.ozon.ru/app/analytics/what-to-sell/all-queries';
1201 console.log('Ozon Description Generator: Открываем страницу аналитики');
1202 await GM.openInTab(analyticsUrl, false);
1203
1204 console.log('Ozon Description Generator: Открыта страница аналитики, ожидание сбора данных...');
1205
1206 const maxWaitTime = 300000;
1207 const checkInterval = 2000;
1208 let waitedTime = 0;
1209
1210 while (waitedTime < maxWaitTime) {
1211 await new Promise(resolve => setTimeout(resolve, checkInterval));
1212 waitedTime += checkInterval;
1213
1214 const status = await GM.getValue('ozon_collection_status', 'pending');
1215
1216 if (status === 'completed') {
1217 const analyticsDataStr = await GM.getValue('ozon_analytics_data', '[]');
1218 const analyticsData = JSON.parse(analyticsDataStr);
1219 console.log('Ozon Description Generator: Данные успешно собраны');
1220 return analyticsData;
1221 } else if (status === 'error') {
1222 console.error('Ozon Description Generator: Ошибка при сборе данных');
1223 return [];
1224 }
1225 }
1226
1227 console.error('Ozon Description Generator: Превышено время ожидания сбора данных');
1228 return [];
1229 }
1230
1231 // ============================================
1232 // АВТОМАТИЧЕСКИЙ СБОР НА СТРАНИЦЕ АНАЛИТИКИ
1233 // ============================================
1234
1235 async function autoCollectOnAnalyticsPage() {
1236 if (!window.location.href.includes('seller.ozon.ru/app/analytics/what-to-sell/all-queries')) {
1237 return;
1238 }
1239
1240 console.log('Ozon Description Generator: Обнаружена страница аналитики');
1241
1242 const status = await GM.getValue('ozon_collection_status', 'none');
1243 if (status !== 'pending') {
1244 return;
1245 }
1246
1247 console.log('Ozon Description Generator: Начинаем автоматический сбор данных');
1248
1249 try {
1250 const keywordsStr = await GM.getValue('ozon_keywords_to_process', '[]');
1251 const minusWordsStr = await GM.getValue('ozon_minus_words', '[]');
1252 const keywords = JSON.parse(keywordsStr);
1253 const minusWords = JSON.parse(minusWordsStr);
1254
1255 const analyticsData = [];
1256
1257 await new Promise(resolve => setTimeout(resolve, 3000));
1258
1259 // Выбираем период 28 дней
1260 try {
1261 const periodButton = document.querySelector('button[data-active="true"]');
1262 if (periodButton && periodButton.textContent.includes('7 дней')) {
1263 console.log('Ozon Description Generator: Меняем период на 28 дней');
1264 periodButton.click();
1265 await new Promise(resolve => setTimeout(resolve, 1000));
1266
1267 // Ищем кнопку 28 дней в выпадающем меню
1268 const buttons = document.querySelectorAll('button');
1269 const days28Button = Array.from(buttons).find(btn =>
1270 btn.textContent && btn.textContent.trim().includes('28 дней')
1271 );
1272
1273 if (days28Button) {
1274 days28Button.click();
1275 await new Promise(resolve => setTimeout(resolve, 2000));
1276 console.log('Ozon Description Generator: Период "28 дней" выбран');
1277 }
1278 }
1279 } catch (e) {
1280 console.error('Ozon Description Generator: Ошибка при выборе периода:', e);
1281 }
1282
1283 for (const keyword of keywords) {
1284 console.log(`Ozon Description Generator: Обработка ключевого слова: ${keyword}`);
1285
1286 try {
1287 const searchInput = document.querySelector('input[placeholder="Поисковый запрос"]');
1288 if (!searchInput) {
1289 console.error('Ozon Description Generator: Поле поиска не найдено');
1290 continue;
1291 }
1292
1293 searchInput.value = '';
1294 searchInput.focus();
1295 searchInput.value = keyword;
1296 searchInput.dispatchEvent(new Event('input', { bubbles: true }));
1297 searchInput.dispatchEvent(new Event('change', { bubbles: true }));
1298
1299 await new Promise(resolve => setTimeout(resolve, 5000));
1300
1301 const rows = document.querySelectorAll('table tbody tr');
1302 const keywordData = {
1303 keyword: keyword,
1304 queries: []
1305 };
1306
1307 console.log(`Ozon Description Generator: Найдено строк в таблице: ${rows.length}`);
1308
1309 rows.forEach(row => {
1310 const cells = row.querySelectorAll('td');
1311 if (cells.length >= 2) {
1312 const query = cells[0]?.textContent?.trim();
1313 const popularityText = cells[1]?.textContent?.trim();
1314
1315 if (query && popularityText) {
1316 const popularity = parseInt(popularityText.replace(/\s+/g, ''));
1317 const queryLower = query.toLowerCase();
1318
1319 const hasMinusWord = minusWords.some(minusWord =>
1320 queryLower.includes(minusWord.toLowerCase())
1321 );
1322
1323 if (hasMinusWord) {
1324 console.log(`Ozon Description Generator: Исключен запрос "${query}" (содержит минус-слово)`);
1325 return;
1326 }
1327
1328 keywordData.queries.push({
1329 query,
1330 popularity
1331 });
1332 }
1333 }
1334 });
1335
1336 analyticsData.push(keywordData);
1337 console.log(`Ozon Description Generator: Собрано ${keywordData.queries.length} запросов для "${keyword}"`);
1338
1339 } catch (error) {
1340 console.error(`Ozon Description Generator: Ошибка при обработке ключевого слова "${keyword}":`, error);
1341 }
1342 }
1343
1344 await GM.setValue('ozon_analytics_data', JSON.stringify(analyticsData));
1345 await GM.setValue('ozon_collection_status', 'completed');
1346
1347 console.log('Ozon Description Generator: Сбор данных завершен, закрываем вкладку через 1 секунду');
1348
1349 setTimeout(() => {
1350 console.log('Ozon Description Generator: Закрываем вкладку аналитики');
1351 window.close();
1352 }, 1000);
1353
1354 } catch (error) {
1355 console.error('Ozon Description Generator: Ошибка при автоматическом сборе данных:', error);
1356 await GM.setValue('ozon_collection_status', 'error');
1357
1358 // Закрываем вкладку даже при ошибке
1359 setTimeout(() => {
1360 window.close();
1361 }, 2000);
1362 }
1363 }
1364
1365 // ============================================
1366 // ГЕНЕРАЦИЯ ОПИСАНИЯ
1367 // ============================================
1368
1369 async function generateDescription(modal, skipDataCollection = false) {
1370 console.log('Ozon Description Generator: Генерация описания');
1371
1372 const keywordsInput = document.getElementById('ozon-keywords-input');
1373 const minusWordsInput = document.getElementById('ozon-minus-words-input');
1374 const customPromptInput = document.getElementById('ozon-custom-prompt-input');
1375 const generateBtn = document.getElementById('ozon-generate-btn');
1376 const regenerateBtn = document.getElementById('ozon-regenerate-btn');
1377 const insertBtn = document.getElementById('ozon-insert-btn');
1378 const resultContainer = document.getElementById('ozon-desc-result-container');
1379 const resultDiv = document.getElementById('ozon-desc-result');
1380 const charCountDiv = document.getElementById('ozon-char-count');
1381 const statusContainer = document.getElementById('ozon-desc-status-container');
1382 const statsContainer = document.getElementById('ozon-desc-stats-container');
1383
1384 let keywordsText = keywordsInput.value.trim();
1385
1386 const selectedSuggestions = Array.from(document.querySelectorAll('.ozon-mask-chip.selected'))
1387 .map(chip => chip.dataset.mask);
1388
1389 if (selectedSuggestions.length > 0) {
1390 const existingKeywords = keywordsText.split('\n').map(k => k.trim()).filter(k => k);
1391 const allKeywords = [...new Set([...existingKeywords, ...selectedSuggestions])];
1392 keywordsText = allKeywords.join('\n');
1393 console.log('Ozon Description Generator: Добавлены маски/ключи:', selectedSuggestions);
1394 }
1395
1396 const keywords = keywordsText.split('\n').map(k => k.trim()).filter(k => k);
1397 const minusWords = minusWordsInput.value.split('\n').map(k => k.trim()).filter(k => k);
1398 const customPrompt = customPromptInput.value.trim();
1399
1400 if (keywords.length === 0) {
1401 showStatus(statusContainer, 'Пожалуйста, введите хотя бы одно ключевое слово', 'error');
1402 return;
1403 }
1404
1405 const productInfo = await getProductInfo();
1406 const currentSKU = productInfo.sku;
1407
1408 if (currentSKU) {
1409 await GM.setValue(`ozon_product_${currentSKU}_keywords`, keywords.join('\n'));
1410 await GM.setValue(`ozon_product_${currentSKU}_minus_words`, minusWords.join('\n'));
1411 await GM.setValue(`ozon_product_${currentSKU}_custom_prompt`, customPrompt);
1412 console.log('Ozon Description Generator: Сохранены ключевые слова, минус-слова и промпт для товара', currentSKU);
1413 }
1414
1415 generateBtn.disabled = true;
1416 regenerateBtn.disabled = true;
1417
1418 try {
1419 let analyticsData = [];
1420 let queryPopularity = {};
1421
1422 if (!skipDataCollection) {
1423 showStatus(statusContainer, 'Сбор данных из аналитики...', 'info');
1424
1425 analyticsData = await collectAnalyticsData(keywords, minusWords);
1426
1427 if (analyticsData.length === 0) {
1428 showStatus(statusContainer, 'Не удалось собрать данные из аналитики', 'error');
1429 return;
1430 }
1431 } else {
1432 const analyticsDataStr = await GM.getValue('ozon_analytics_data', '[]');
1433 analyticsData = JSON.parse(analyticsDataStr);
1434
1435 if (analyticsData.length === 0) {
1436 showStatus(statusContainer, 'Нет сохраненных данных. Пожалуйста, сначала соберите данные.', 'error');
1437 return;
1438 }
1439 }
1440
1441 const allQueries = [];
1442 analyticsData.forEach(data => {
1443 data.queries.forEach(q => {
1444 allQueries.push(q.query);
1445 queryPopularity[q.query.toLowerCase()] = q.popularity;
1446 });
1447 });
1448
1449 console.log(`Ozon Description Generator: Всего запросов для генерации: ${allQueries.length}`);
1450
1451 if (allQueries.length === 0) {
1452 showStatus(statusContainer, 'Не найдено подходящих запросов для генерации', 'error');
1453 return;
1454 }
1455
1456 console.log('Ozon Description Generator: Применяем минус-слова перед генерацией');
1457 console.log('Ozon Description Generator: Минус-слова:', minusWords);
1458 console.log('Ozon Description Generator: Запросов до фильтрации:', allQueries.length);
1459
1460 const filteredQueries = allQueries.filter(query => {
1461 const queryLower = query.toLowerCase();
1462 const hasMinusWord = minusWords.some(minusWord =>
1463 queryLower.includes(minusWord.toLowerCase())
1464 );
1465
1466 if (hasMinusWord) {
1467 console.log(`Ozon Description Generator: Исключен запрос "${query}" (содержит минус-слово)`);
1468 }
1469
1470 return !hasMinusWord;
1471 });
1472
1473 console.log('Ozon Description Generator: Запросов после фильтрации:', filteredQueries.length);
1474
1475 if (filteredQueries.length === 0) {
1476 showStatus(statusContainer, 'Все запросы отфильтрованы минус-словами. Попробуйте уменьшить количество минус-слов.', 'error');
1477 return;
1478 }
1479
1480 showStatus(statusContainer, 'AI генерирует описание...', 'info');
1481
1482 const descriptionPrompt = generateDescriptionPrompt(productInfo, keywords, filteredQueries, queryPopularity, customPrompt);
1483 const description = await RM.aiCall(descriptionPrompt);
1484
1485 await GM.setValue('ozon_generated_description', description);
1486 await GM.setValue('ozon_query_popularity', JSON.stringify(queryPopularity));
1487
1488 resultDiv.textContent = description;
1489 resultContainer.style.display = 'block';
1490
1491 const charCount = description.length;
1492 charCountDiv.textContent = `Символов: ${charCount}`;
1493 charCountDiv.className = 'ozon-desc-char-count success';
1494
1495 const analysis = await analyzeUsedKeywords(description, queryPopularity, minusWords);
1496 const usagePercent = Math.round(analysis.usedQueries.length / analysis.totalQueriesAvailable * 100);
1497
1498 if (!statsContainer) {
1499 const newStatsContainer = document.createElement('div');
1500 newStatsContainer.id = 'ozon-desc-stats-container';
1501 charCountDiv.parentElement.insertBefore(newStatsContainer, charCountDiv.nextSibling);
1502 }
1503
1504 document.getElementById('ozon-desc-stats-container').innerHTML = `
1505 <div class="ozon-desc-stats">
1506 <div class="ozon-desc-stats-row">
1507 <span><strong>Использовано запросов:</strong></span>
1508 <span>${analysis.usedQueries.length} из ${analysis.totalQueriesAvailable} (${usagePercent}%)</span>
1509 </div>
1510 <div class="ozon-desc-stats-row">
1511 <span><strong>Общая частотность:</strong></span>
1512 <span>${formatNumber(analysis.totalPopularity)}</span>
1513 </div>
1514 </div>
1515 `;
1516
1517 generateBtn.style.display = 'none';
1518 regenerateBtn.style.display = 'inline-block';
1519 insertBtn.style.display = 'inline-block';
1520
1521 showStatus(statusContainer, '✅ Описание успешно сгенерировано! <span class="ozon-desc-usage-link" id="ozon-show-analytics-link">Показать аналитику использования запросов</span>', 'success');
1522
1523 setTimeout(() => {
1524 const analyticsLink = document.getElementById('ozon-show-analytics-link');
1525 if (analyticsLink) {
1526 analyticsLink.addEventListener('click', () => {
1527 showUsageAnalytics();
1528 });
1529 }
1530 }, 100);
1531
1532 } catch (error) {
1533 console.error('Ozon Description Generator: Ошибка при генерации описания:', error);
1534 showStatus(statusContainer, 'Ошибка при генерации: ' + error.message, 'error');
1535 } finally {
1536 generateBtn.disabled = false;
1537 regenerateBtn.disabled = false;
1538 }
1539 }
1540
1541 // ============================================
1542 // АНАЛИЗ ИСПОЛЬЗОВАННЫХ КЛЮЧЕВЫХ СЛОВ
1543 // ============================================
1544
1545 async function analyzeUsedKeywords(description, queryPopularityParam = null, minusWords = []) {
1546 console.log('Ozon Description Generator: Анализ использованных ключевых слов');
1547
1548 const analyticsDataStr = await GM.getValue('ozon_analytics_data', '[]');
1549 const analyticsData = JSON.parse(analyticsDataStr);
1550
1551 const allQueries = [];
1552 let queryPopularity = queryPopularityParam || {};
1553
1554 if (!queryPopularityParam) {
1555 const savedPopularity = await GM.getValue('ozon_query_popularity', '{}');
1556 queryPopularity = JSON.parse(savedPopularity);
1557 }
1558
1559 analyticsData.forEach(data => {
1560 data.queries.forEach(q => {
1561 allQueries.push(q.query);
1562 if (!queryPopularity[q.query.toLowerCase()]) {
1563 queryPopularity[q.query.toLowerCase()] = q.popularity;
1564 }
1565 });
1566 });
1567
1568 // Фильтруем запросы с учетом минус-слов
1569 const filteredQueries = allQueries.filter(query => {
1570 const queryLower = query.toLowerCase();
1571 const hasMinusWord = minusWords.some(minusWord =>
1572 queryLower.includes(minusWord.toLowerCase())
1573 );
1574 return !hasMinusWord;
1575 });
1576
1577 const descriptionLower = description.toLowerCase();
1578 const usedQueries = [];
1579 const unusedQueries = [];
1580 let totalPopularity = 0;
1581
1582 filteredQueries.forEach(query => {
1583 if (descriptionLower.includes(query.toLowerCase())) {
1584 usedQueries.push(query);
1585 totalPopularity += queryPopularity[query.toLowerCase()] || 0;
1586 } else {
1587 unusedQueries.push(query);
1588 }
1589 });
1590
1591 console.log(`Ozon Description Generator: Использовано ${usedQueries.length} из ${filteredQueries.length} запросов (после фильтрации минус-слов)`);
1592
1593 return {
1594 usedQueries,
1595 unusedQueries,
1596 totalQueriesAvailable: filteredQueries.length,
1597 totalPopularity,
1598 queryPopularity
1599 };
1600 }
1601
1602 // ============================================
1603 // ПОКАЗ АНАЛИТИКИ ИСПОЛЬЗОВАНИЯ ЗАПРОСОВ
1604 // ============================================
1605
1606 async function showUsageAnalytics() {
1607 console.log('Ozon Description Generator: Показ аналитики использования');
1608
1609 const description = await GM.getValue('ozon_generated_description', '');
1610 if (!description) {
1611 alert('Описание не найдено');
1612 return;
1613 }
1614
1615 const analysis = await analyzeUsedKeywords(description);
1616
1617 const minusWordsStr = await GM.getValue('ozon_analytics_minus_words', '[]');
1618 const minusWords = JSON.parse(minusWordsStr);
1619
1620 let usedQueries = [...analysis.usedQueries];
1621 let unusedQueries = [...analysis.unusedQueries];
1622 let currentMinusWords = [...minusWords];
1623 let searchQuery = '';
1624
1625 function renderModal() {
1626 const usedContainer = document.getElementById('ozon-used-queries-container');
1627 const unusedContainer = document.getElementById('ozon-unused-queries-container');
1628 const usedScrollTop = usedContainer ? usedContainer.scrollTop : 0;
1629 const unusedScrollTop = unusedContainer ? unusedContainer.scrollTop : 0;
1630
1631 const filteredUsed = usedQueries.filter(q =>
1632 q.toLowerCase().includes(searchQuery.toLowerCase())
1633 );
1634 const filteredUnused = unusedQueries.filter(q =>
1635 q.toLowerCase().includes(searchQuery.toLowerCase())
1636 );
1637
1638 const analyticsModal = document.querySelector('.ozon-desc-analytics-modal');
1639 if (!analyticsModal) return;
1640
1641 analyticsModal.innerHTML = `
1642 <div class="ozon-desc-analytics-content">
1643 <div class="ozon-desc-modal-header">📊 Аналитика использования запросов</div>
1644
1645 ${currentMinusWords.length > 0 ? `
1646 <div class="ozon-desc-minus-words-section">
1647 <div class="ozon-desc-minus-words-header">Минус-слова (клик для удаления):</div>
1648 <div class="ozon-desc-minus-words-list">
1649 ${currentMinusWords.map(word => `
1650 <div class="ozon-desc-minus-word-chip" data-word="${word}">
1651 ${word}
1652 <span class="ozon-desc-minus-word-remove">×</span>
1653 </div>
1654 `).join('')}
1655 </div>
1656 </div>
1657 ` : ''}
1658
1659 <input type="text" class="ozon-desc-search-input" id="ozon-analytics-search" placeholder="🔍 Поиск по запросам..." value="${searchQuery}">
1660
1661 <div style="margin-bottom: 16px; display: flex; gap: 20px; flex-wrap: wrap;">
1662 <div><strong>Использовано:</strong> ${analysis.usedQueries.length} из ${analysis.totalQueriesAvailable} (${Math.round(analysis.usedQueries.length / analysis.totalQueriesAvailable * 100)}%)</div>
1663 <div><strong>Общая частотность:</strong> ${formatNumber(analysis.totalPopularity)}</div>
1664 ${searchQuery ? `<div><strong>Найдено:</strong> ${filteredUsed.length + filteredUnused.length}</div>` : ''}
1665 </div>
1666
1667 <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px;">
1668 <div>
1669 <div style="margin-bottom: 12px; font-weight: 600; color: #065f46;">✅ Использованные (${filteredUsed.length}):</div>
1670 <div style="max-height: 350px; overflow-y: auto;" id="ozon-used-queries-container">
1671 ${filteredUsed.length > 0 ? filteredUsed.map(query => `
1672 <div class="ozon-desc-query-item used">
1673 <span class="ozon-desc-query-text">${highlightWords(query, currentMinusWords, true, 'used')}</span>
1674 <div style="display: flex; align-items: center; gap: 8px;">
1675 <span class="ozon-desc-query-popularity">${formatNumber(analysis.queryPopularity[query.toLowerCase()] || 0)}</span>
1676 <span class="ozon-desc-query-exclude" data-query="${query}" style="cursor: pointer; font-size: 18px; color: #991b1b; font-weight: bold;" title="Исключить запрос">×</span>
1677 </div>
1678 </div>
1679 `).join('') : '<div style="padding: 12px; color: #6b7280; text-align: center;">Нет результатов</div>'}
1680 </div>
1681 </div>
1682
1683 <div>
1684 <div style="margin-bottom: 12px; font-weight: 600; color: #6b7280;">⬜ Неиспользованные (${filteredUnused.length}):</div>
1685 <div style="max-height: 350px; overflow-y: auto;" id="ozon-unused-queries-container">
1686 ${filteredUnused.length > 0 ? filteredUnused.map(query => `
1687 <div class="ozon-desc-query-item unused">
1688 <span class="ozon-desc-query-text">${highlightWords(query, currentMinusWords, true, 'unused')}</span>
1689 <div style="display: flex; align-items: center; gap: 8px;">
1690 <span class="ozon-desc-query-popularity">${formatNumber(analysis.queryPopularity[query.toLowerCase()] || 0)}</span>
1691 <span class="ozon-desc-query-include" data-query="${query}" style="cursor: pointer; font-size: 18px; color: #059669; font-weight: bold;" title="Включить запрос">+</span>
1692 </div>
1693 </div>
1694 `).join('') : '<div style="padding: 12px; color: #6b7280; text-align: center;">Нет результатов</div>'}
1695 </div>
1696 </div>
1697 </div>
1698
1699 <div class="ozon-desc-buttons">
1700 <button class="ozon-desc-btn ozon-desc-btn-secondary" id="ozon-close-analytics-btn">Закрыть</button>
1701 <button class="ozon-desc-btn ozon-desc-btn-primary" id="ozon-regenerate-with-exclusions-btn">🔄 Перегенерировать с изменениями</button>
1702 </div>
1703 </div>
1704 `;
1705
1706 setTimeout(() => {
1707 const newUsedContainer = document.getElementById('ozon-used-queries-container');
1708 const newUnusedContainer = document.getElementById('ozon-unused-queries-container');
1709 if (newUsedContainer) newUsedContainer.scrollTop = usedScrollTop;
1710 if (newUnusedContainer) newUnusedContainer.scrollTop = unusedScrollTop;
1711 }, 0);
1712
1713 attachEventHandlers();
1714 }
1715
1716 function highlightWords(text, words, clickable = false, type = 'unused') {
1717 if (!clickable) return text;
1718
1719 const textWords = text.split(/\s+/);
1720 return textWords.map(word => {
1721 const cleanWord = word.toLowerCase().replace(/[.,!?;:]/g, '');
1722 return `<span class="ozon-desc-query-word" data-word="${cleanWord}" data-type="${type}">${word}</span>`;
1723 }).join(' ');
1724 }
1725
1726 function removeQueriesWithMinusWord(minusWord) {
1727 const minusWordLower = minusWord.toLowerCase();
1728
1729 usedQueries = usedQueries.filter(query =>
1730 !query.toLowerCase().includes(minusWordLower)
1731 );
1732
1733 unusedQueries = unusedQueries.filter(query =>
1734 !query.toLowerCase().includes(minusWordLower)
1735 );
1736 }
1737
1738 function attachEventHandlers() {
1739 const analyticsModal = document.querySelector('.ozon-desc-analytics-modal');
1740 if (!analyticsModal) return;
1741
1742 const searchInput = document.getElementById('ozon-analytics-search');
1743 if (searchInput) {
1744 searchInput.addEventListener('input', (e) => {
1745 searchQuery = e.target.value;
1746 const cursorPosition = e.target.selectionStart;
1747 renderModal();
1748 setTimeout(() => {
1749 const newSearchInput = document.getElementById('ozon-analytics-search');
1750 if (newSearchInput) {
1751 newSearchInput.focus();
1752 newSearchInput.setSelectionRange(cursorPosition, cursorPosition);
1753 }
1754 }, 0);
1755 });
1756 }
1757
1758 analyticsModal.addEventListener('click', (e) => {
1759 if (e.target === analyticsModal) {
1760 analyticsModal.remove();
1761 }
1762 });
1763
1764 const closeBtn = document.getElementById('ozon-close-analytics-btn');
1765 if (closeBtn) {
1766 closeBtn.addEventListener('click', () => {
1767 analyticsModal.remove();
1768 });
1769 }
1770
1771 const regenerateBtn = document.getElementById('ozon-regenerate-with-exclusions-btn');
1772 if (regenerateBtn) {
1773 regenerateBtn.addEventListener('click', async () => {
1774 await GM.setValue('ozon_analytics_minus_words', JSON.stringify(currentMinusWords));
1775
1776 const analyticsDataStr = await GM.getValue('ozon_analytics_data', '[]');
1777 const analyticsData = JSON.parse(analyticsDataStr);
1778
1779 const updatedAnalyticsData = analyticsData.map(data => {
1780 return {
1781 keyword: data.keyword,
1782 queries: data.queries.filter(q => usedQueries.includes(q.query))
1783 };
1784 });
1785
1786 await GM.setValue('ozon_analytics_data', JSON.stringify(updatedAnalyticsData));
1787
1788 console.log('Ozon Description Generator: Обновлены данные аналитики для перегенерации');
1789 console.log('Использованные запросы:', usedQueries.length);
1790 console.log('Минус-слова:', currentMinusWords);
1791
1792 analyticsModal.remove();
1793 await regenerateWithExclusions();
1794 });
1795 }
1796
1797 analyticsModal.querySelectorAll('.ozon-desc-minus-word-chip').forEach(chip => {
1798 chip.addEventListener('click', () => {
1799 const word = chip.dataset.word;
1800 currentMinusWords = currentMinusWords.filter(w => w !== word);
1801 console.log(`Ozon Description Generator: Минус-слово "${word}" удалено`);
1802 renderModal();
1803 });
1804 });
1805
1806 analyticsModal.querySelectorAll('.ozon-desc-query-exclude').forEach(excludeBtn => {
1807 excludeBtn.addEventListener('click', () => {
1808 const query = excludeBtn.dataset.query;
1809 console.log(`Ozon Description Generator: Перемещаем запрос "${query}" в неиспользованные`);
1810
1811 usedQueries = usedQueries.filter(q => q !== query);
1812 if (!unusedQueries.includes(query)) {
1813 unusedQueries.push(query);
1814 }
1815
1816 renderModal();
1817 });
1818 });
1819
1820 analyticsModal.querySelectorAll('.ozon-desc-query-include').forEach(includeBtn => {
1821 includeBtn.addEventListener('click', () => {
1822 const query = includeBtn.dataset.query;
1823 console.log(`Ozon Description Generator: Перемещаем запрос "${query}" в использованные`);
1824
1825 unusedQueries = unusedQueries.filter(q => q !== query);
1826 if (!usedQueries.includes(query)) {
1827 usedQueries.push(query);
1828 }
1829
1830 const queryLower = query.toLowerCase();
1831 currentMinusWords = currentMinusWords.filter(w => w !== queryLower);
1832
1833 renderModal();
1834 });
1835 });
1836
1837 analyticsModal.querySelectorAll('.ozon-desc-query-word').forEach(wordSpan => {
1838 wordSpan.addEventListener('click', () => {
1839 const word = wordSpan.dataset.word;
1840
1841 console.log(`Ozon Description Generator: Добавляем минус-слово "${word}"`);
1842
1843 if (!currentMinusWords.includes(word)) {
1844 currentMinusWords.push(word);
1845 }
1846
1847 removeQueriesWithMinusWord(word);
1848
1849 renderModal();
1850 });
1851 });
1852 }
1853
1854 const analyticsModal = document.createElement('div');
1855 analyticsModal.className = 'ozon-desc-analytics-modal';
1856 document.body.appendChild(analyticsModal);
1857
1858 renderModal();
1859 }
1860
1861 // ============================================
1862 // ПЕРЕГЕНЕРАЦИЯ С ИСКЛЮЧЕНИЯМИ
1863 // ============================================
1864
1865 async function regenerateWithExclusions() {
1866 console.log('Ozon Description Generator: Перегенерация с исключениями');
1867
1868 const analyticsMinusWordsStr = await GM.getValue('ozon_analytics_minus_words', '[]');
1869 const analyticsMinusWords = JSON.parse(analyticsMinusWordsStr);
1870
1871 console.log('Ozon Description Generator: Минус-слова из аналитики для перегенерации:', analyticsMinusWords);
1872
1873 const existingModal = document.querySelector('.ozon-desc-modal');
1874 if (existingModal) {
1875 console.log('Ozon Description Generator: Модальное окно уже открыто');
1876
1877 const minusWordsInput = document.getElementById('ozon-minus-words-input');
1878 if (minusWordsInput) {
1879 const currentMinusWords = minusWordsInput.value.split('\n').map(k => k.trim()).filter(k => k);
1880 const allMinusWords = [...new Set([...currentMinusWords, ...analyticsMinusWords])];
1881 minusWordsInput.value = allMinusWords.join('\n');
1882
1883 console.log('Ozon Description Generator: Обновлены минус-слова в модальном окне:', allMinusWords);
1884 }
1885
1886 generateDescription(existingModal, true);
1887 return;
1888 }
1889
1890 await openModal();
1891 await new Promise(resolve => setTimeout(resolve, 100));
1892
1893 const minusWordsInput = document.getElementById('ozon-minus-words-input');
1894 if (minusWordsInput) {
1895 const currentMinusWords = minusWordsInput.value.split('\n').map(k => k.trim()).filter(k => k);
1896 const allMinusWords = [...new Set([...currentMinusWords, ...analyticsMinusWords])];
1897 minusWordsInput.value = allMinusWords.join('\n');
1898
1899 console.log('Ozon Description Generator: Обновлены минус-слова в новом модальном окне:', allMinusWords);
1900 }
1901
1902 const newModal = document.querySelector('.ozon-desc-modal');
1903 if (newModal) {
1904 generateDescription(newModal, true);
1905 }
1906 }
1907
1908 // ============================================
1909 // ОТПРАВКА В RICH-КОНТЕНТ
1910 // ============================================
1911
1912 async function insertDescription(modal) {
1913 console.log('Ozon Description Generator: Вставка описания в поле');
1914
1915 const description = await GM.getValue('ozon_generated_description', '');
1916
1917 if (!description) {
1918 alert('Описание не найдено. Пожалуйста, сначала сгенерируйте описание.');
1919 return;
1920 }
1921
1922 const proseMirrorDiv = document.querySelector('[id="attribute#4191"] .ProseMirror');
1923
1924 if (!proseMirrorDiv) {
1925 alert('Не удалось найти поле описания. Убедитесь, что вы находитесь на странице редактирования товара.');
1926 return;
1927 }
1928
1929 // Вставляем описание с сохранением пустых строк между абзацами
1930 const paragraphs = description.split('\n\n').filter(p => p.trim());
1931 const htmlContent = paragraphs.map(p => `<p>${p.replace(/\n/g, '<br>')}</p>`).join('<p><br></p>');
1932
1933 proseMirrorDiv.innerHTML = htmlContent;
1934 proseMirrorDiv.dispatchEvent(new Event('input', { bubbles: true }));
1935 proseMirrorDiv.dispatchEvent(new Event('change', { bubbles: true }));
1936
1937 console.log('Ozon Description Generator: Описание вставлено');
1938
1939 modal.remove();
1940
1941 alert('✅ Описание успешно вставлено!');
1942 }
1943
1944 async function sendToRichContent() {
1945 console.log('Ozon Description Generator: Отправка в rich-контент');
1946
1947 try {
1948 // Получаем описание из ProseMirror редактора
1949 const proseMirrorDiv = document.querySelector('[id="attribute#4191"] .ProseMirror');
1950
1951 if (!proseMirrorDiv) {
1952 alert('Не удалось найти поле описания. Убедитесь, что вы на странице редактирования товара.');
1953 return;
1954 }
1955
1956 const description = proseMirrorDiv.textContent.trim();
1957
1958 if (!description) {
1959 alert('Описание пустое. Пожалуйста, сначала сгенерируйте и вставьте описание.');
1960 return;
1961 }
1962
1963 console.log('Ozon Description Generator: Описание скопировано, переходим на страницу медиа');
1964
1965 // Сохраняем описание для использования на странице медиа
1966 await GM.setValue('ozon_description_for_rich', description);
1967
1968 // Находим кнопку перехода на вкладку "Медиа"
1969 const mediaTabButton = Array.from(document.querySelectorAll('button.c9s110-a3.c9s110-a0.c9s110-a.c9s110-a4.c9s110-a6')).find(btn =>
1970 btn.textContent && btn.textContent.includes('Медиа')
1971 );
1972
1973 if (!mediaTabButton) {
1974 alert('Не удалось найти кнопку перехода на вкладку "Медиа"');
1975 return;
1976 }
1977
1978 console.log('Ozon Description Generator: Нажимаем кнопку "Медиа"');
1979 mediaTabButton.click();
1980
1981 // Ждем загрузки страницы медиа и вставляем rich-контент
1982 setTimeout(async () => {
1983 await insertRichContent();
1984 }, 1000);
1985
1986 } catch (error) {
1987 console.error('Ozon Description Generator: Ошибка при отправке в rich-контент:', error);
1988 alert('Ошибка при отправке в rich-контент: ' + error.message);
1989 }
1990 }
1991
1992 async function insertRichContent() {
1993 console.log('Ozon Description Generator: Вставка rich-контента');
1994
1995 try {
1996 const description = await GM.getValue('ozon_description_for_rich', '');
1997
1998 if (!description) {
1999 console.error('Ozon Description Generator: Описание не найдено в хранилище');
2000 return;
2001 }
2002
2003 // Ищем поле Rich-контента JSON
2004 const richContentInput = document.querySelector('textarea[id^="baseInput___"]');
2005
2006 if (!richContentInput) {
2007 alert('Не удалось найти поле Rich-контента. Убедитесь, что вы на странице редактирования товара.');
2008 return;
2009 }
2010
2011 // Разбиваем описание на абзацы с сохранением пустых строк
2012 const paragraphs = description.split('\n\n').filter(p => p.trim());
2013
2014 // Создаем JSON для rich-контента с абзацами
2015 const richContentJSON = {
2016 'content': [
2017 {
2018 'widgetName': 'raTextBlock',
2019 'title': {
2020 'content': [],
2021 'size': 'size5',
2022 'color': 'color1'
2023 },
2024 'theme': 'default',
2025 'padding': 'type2',
2026 'gapSize': 'm',
2027 'text': {
2028 'size': 'size2',
2029 'align': 'left',
2030 'color': 'color1',
2031 'content': paragraphs
2032 }
2033 }
2034 ],
2035 'version': 0.3
2036 };
2037
2038 // Вставляем JSON в поле
2039 richContentInput.value = JSON.stringify(richContentJSON, null, 2);
2040 richContentInput.dispatchEvent(new Event('input', { bubbles: true }));
2041 richContentInput.dispatchEvent(new Event('change', { bubbles: true }));
2042
2043 console.log('Ozon Description Generator: Rich-контент успешно вставлен');
2044
2045 // Очищаем сохраненное описание
2046 await GM.setValue('ozon_description_for_rich', '');
2047
2048 // Проверяем, идет ли автогенерация
2049 const expectedKeys = Object.keys(localStorage).filter(key => key.startsWith('wbAutoExpected_'));
2050 const isAutogen = expectedKeys.length > 0;
2051
2052 // Показываем alert только если это НЕ автогенерация
2053 if (!isAutogen) {
2054 alert('✅ Описание успешно отправлено в Rich-контент!');
2055 }
2056
2057 } catch (error) {
2058 console.error('Ozon Description Generator: Ошибка при вставке rich-контента:', error);
2059 alert('Ошибка при вставке rich-контента: ' + error.message);
2060 }
2061 }
2062
2063 // ============================================
2064 // АВТОГЕНЕРАЦИЯ ПО ВСЕМ ТОВАРАМ
2065 // ============================================
2066
2067 // Вспомогательные функции для работы с localStorage
2068 function generateCheckId() {
2069 return Date.now() + '_' + Math.random().toString(36).substr(2, 9);
2070 }
2071
2072 function setAutoCheck(nmID, title, checkId) {
2073 const data = {
2074 nmID,
2075 title,
2076 checkId,
2077 timestamp: Date.now()
2078 };
2079 localStorage.setItem('wbAutoCheck', JSON.stringify(data));
2080 localStorage.setItem(`wbAutoExpected_${checkId}`, 'true');
2081 console.log('Ozon Description Generator: Сохранен autoCheck:', data);
2082 }
2083
2084 function getAutoCheck() {
2085 const data = localStorage.getItem('wbAutoCheck');
2086 return data ? JSON.parse(data) : null;
2087 }
2088
2089 function clearAutoCheck(checkId) {
2090 localStorage.removeItem('wbAutoCheck');
2091 if (checkId) {
2092 localStorage.removeItem(`wbAutoExpected_${checkId}`);
2093 localStorage.removeItem('wbAutoResult');
2094 }
2095 console.log('Ozon Description Generator: Очищен autoCheck');
2096 }
2097
2098 function setAutoResult(checkId, success, error = null) {
2099 const result = {
2100 checkId,
2101 success,
2102 error,
2103 timestamp: Date.now()
2104 };
2105 localStorage.setItem('wbAutoResult', JSON.stringify(result));
2106 console.log('Ozon Description Generator: Сохранен результат:', result);
2107 }
2108
2109 function getAutoResult() {
2110 const data = localStorage.getItem('wbAutoResult');
2111 return data ? JSON.parse(data) : null;
2112 }
2113
2114 function isCheckIdExpected(checkId) {
2115 return localStorage.getItem(`wbAutoExpected_${checkId}`) === 'true';
2116 }
2117
2118 function isTimestampFresh(timestamp, maxAgeMs = 180000) { // 3 минуты
2119 return (Date.now() - timestamp) < maxAgeMs;
2120 }
2121
2122 // Создание кнопки автогенерации на странице списка товаров
2123 function createAutogenButton() {
2124 if (document.querySelector('.ozon-autogen-btn')) {
2125 return;
2126 }
2127
2128 // Ищем контейнер с кнопками действий
2129 const actionsContainer = document.querySelector('.cs5110-a5 .cs5110-b0 div');
2130
2131 if (!actionsContainer) {
2132 return;
2133 }
2134
2135 const autogenButton = document.createElement('button');
2136 autogenButton.className = 'ozon-autogen-btn';
2137 autogenButton.textContent = '🤖 Автогенерация';
2138 autogenButton.type = 'button';
2139 autogenButton.setAttribute('data-ozon-autogen', 'true');
2140 autogenButton.addEventListener('click', openAutogenModal);
2141
2142 actionsContainer.insertBefore(autogenButton, actionsContainer.firstChild);
2143 console.log('Ozon Description Generator: Кнопка автогенерации добавлена');
2144 }
2145
2146 // Модальное окно настроек автогенерации
2147 async function openAutogenModal() {
2148 console.log('Ozon Description Generator: Открытие модального окна автогенерации');
2149
2150 // Предустановленные промпты
2151 const presetPrompts = [
2152 { name: 'Без дополнительных требований', value: '' },
2153 { name: 'Акцент на натуральность', value: 'Сделай акцент на натуральности состава, экологичности и безопасности для здоровья.' },
2154 { name: 'Премиум-сегмент', value: 'Используй стиль премиум-сегмента: подчеркни эксклюзивность, высокое качество и статусность продукта.' },
2155 { name: 'Для чувствительной кожи', value: 'Особое внимание уделить гипоаллергенности, мягкости формулы и подходу для чувствительной кожи.' },
2156 { name: 'Антивозрастной уход', value: 'Акцентируй внимание на антивозрастных свойствах, омоложении и борьбе с признаками старения.' },
2157 { name: 'Быстрый результат', value: 'Подчеркни быстроту достижения видимого результата и эффективность средства.' }
2158 ];
2159
2160 const modal = document.createElement('div');
2161 modal.className = 'ozon-desc-modal';
2162 modal.innerHTML = `
2163 <div class="ozon-desc-modal-content">
2164 <div class="ozon-desc-modal-header">🤖 Автогенерация описаний</div>
2165
2166 <div class="ozon-desc-input-group">
2167 <label class="ozon-desc-label">Ключевые слова (необязательно, каждое с новой строки):</label>
2168 <textarea class="ozon-desc-textarea" id="ozon-autogen-keywords-input" placeholder="Если не заполнено, AI сам подберет ключевые слова для каждого товара"></textarea>
2169 </div>
2170
2171 <div class="ozon-desc-input-group">
2172 <label class="ozon-desc-label">Дополнительные требования к описанию:</label>
2173 <select class="ozon-desc-textarea" id="ozon-autogen-prompt-preset-select" style="min-height: auto; padding: 10px; margin-bottom: 8px;">
2174 ${presetPrompts.map(preset => `<option value="${preset.value}">${preset.name}</option>`).join('')}
2175 </select>
2176 <textarea class="ozon-desc-textarea" style="min-height: 80px;" id="ozon-autogen-custom-prompt-input" placeholder="Или напишите свои требования к стилю и содержанию описания..."></textarea>
2177 </div>
2178
2179 <div class="ozon-checkbox-container">
2180 <input type="checkbox" class="ozon-checkbox" id="ozon-autogen-rich-checkbox" checked>
2181 <label class="ozon-checkbox-label" for="ozon-autogen-rich-checkbox">Отправить в Rich-контент</label>
2182 </div>
2183
2184 <div class="ozon-checkbox-container">
2185 <input type="checkbox" class="ozon-checkbox" id="ozon-autogen-test-mode-checkbox">
2186 <label class="ozon-checkbox-label" for="ozon-autogen-test-mode-checkbox">🧪 Тестовый режим (без AI, вставка "тест")</label>
2187 </div>
2188
2189 <div id="ozon-autogen-status-container"></div>
2190
2191 <div class="ozon-desc-buttons">
2192 <button class="ozon-desc-btn ozon-desc-btn-secondary" id="ozon-autogen-close-btn">Закрыть</button>
2193 <button class="ozon-desc-btn ozon-desc-btn-primary" id="ozon-autogen-start-btn">🚀 Начать автогенерацию</button>
2194 </div>
2195 </div>
2196 `;
2197
2198 document.body.appendChild(modal);
2199
2200 // Обработчик выбора пресета промпта
2201 document.getElementById('ozon-autogen-prompt-preset-select').addEventListener('change', (e) => {
2202 const customPromptInput = document.getElementById('ozon-autogen-custom-prompt-input');
2203 if (e.target.value) {
2204 customPromptInput.value = e.target.value;
2205 }
2206 });
2207
2208 modal.addEventListener('click', (e) => {
2209 if (e.target === modal) {
2210 modal.remove();
2211 }
2212 });
2213
2214 document.getElementById('ozon-autogen-close-btn').addEventListener('click', () => {
2215 modal.remove();
2216 });
2217
2218 document.getElementById('ozon-autogen-start-btn').addEventListener('click', () => {
2219 startAutogeneration(modal);
2220 });
2221 }
2222
2223 // Начало автогенерации
2224 async function startAutogeneration(modal) {
2225 const keywordsInput = document.getElementById('ozon-autogen-keywords-input');
2226 const customPromptInput = document.getElementById('ozon-autogen-custom-prompt-input');
2227 const richCheckbox = document.getElementById('ozon-autogen-rich-checkbox');
2228 const testModeCheckbox = document.getElementById('ozon-autogen-test-mode-checkbox');
2229
2230 const globalKeywords = keywordsInput.value.trim();
2231 const customPrompt = customPromptInput.value.trim();
2232 const sendToRich = richCheckbox.checked;
2233 const testMode = testModeCheckbox.checked;
2234
2235 console.log('Ozon Description Generator: Начало автогенерации');
2236 console.log('Глобальные ключевые слова:', globalKeywords);
2237 console.log('Дополнительные требования:', customPrompt);
2238 console.log('Отправить в Rich-контент:', sendToRich);
2239 console.log('Тестовый режим:', testMode);
2240
2241 // Сохраняем настройки в localStorage
2242 localStorage.setItem('ozon_autogen_global_keywords', globalKeywords);
2243 localStorage.setItem('ozon_autogen_custom_prompt', customPrompt);
2244 localStorage.setItem('ozon_autogen_send_to_rich', sendToRich);
2245 localStorage.setItem('ozon_autogen_test_mode', testMode);
2246 localStorage.setItem('ozon_autogen_processed', '0');
2247 localStorage.setItem('ozon_autogen_errors', '0');
2248
2249 // Очищаем список обработанных товаров при новом запуске
2250 localStorage.setItem('ozon_autogen_processed_skus', '[]');
2251 console.log('Ozon Description Generator: Список обработанных товаров очищен');
2252
2253 modal.remove();
2254
2255 // Показываем окно прогресса
2256 showAutogenProgress();
2257
2258 // Начинаем обработку товаров
2259 processNextProduct();
2260 }
2261
2262 // Обработка следующего товара
2263 async function processNextProduct() {
2264 console.log('Ozon Description Generator: processNextProduct вызван');
2265
2266 // Прокручиваем страницу вниз, чтобы загрузить товары
2267 window.scrollBy(0, 500);
2268 await new Promise(resolve => setTimeout(resolve, 1000));
2269
2270 // Получаем список уже обработанных товаров
2271 const processedProducts = JSON.parse(localStorage.getItem('ozon_autogen_processed_skus') || '[]');
2272 console.log('Ozon Description Generator: Уже обработано товаров:', processedProducts.length);
2273
2274 // Ищем ссылки на карточки товаров - пробуем разные варианты
2275 let productLinks = Array.from(document.querySelectorAll('a[href*="/app/products/"]')).filter(link => {
2276 const href = link.getAttribute('href');
2277 return href && href.includes('/app/products/') && /\/\d+\//.test(href);
2278 });
2279
2280 // Если не нашли ссылки, ищем кнопки "Редактировать" по классам и title
2281 if (productLinks.length === 0) {
2282 console.log('Ozon Description Generator: Ссылки не найдены, ищем кнопки редактирования по классам');
2283
2284 // Ищем кнопки с классом c9r110-a и title "редактировать товар"
2285 const editButtons = Array.from(document.querySelectorAll('button.c9r110-a[title*="едактировать"]'));
2286
2287 if (editButtons.length > 0) {
2288 console.log(`Ozon Description Generator: Найдено кнопок редактирования: ${editButtons.length}`);
2289
2290 // Ищем первую необработанную кнопку
2291 let editButton = null;
2292 let nmID = '';
2293 let productName = 'Товар';
2294
2295 for (const btn of editButtons) {
2296 const row = btn.closest('tr');
2297 const skuElement = row?.querySelector('.index_skuText_61dv5');
2298
2299 if (skuElement) {
2300 const sku = skuElement.textContent.trim();
2301
2302 // Проверяем, не обработан ли уже этот товар
2303 if (!processedProducts.includes(sku)) {
2304 editButton = btn;
2305 nmID = sku;
2306 const nameCell = row?.querySelector('td:nth-child(2)');
2307 if (nameCell) {
2308 productName = nameCell.textContent.trim();
2309 }
2310 console.log('Ozon Description Generator: Найден необработанный товар с SKU:', nmID);
2311 break;
2312 } else {
2313 console.log('Ozon Description Generator: Товар с SKU', sku, 'уже обработан, пропускаем');
2314 }
2315 }
2316 }
2317
2318 if (!editButton) {
2319 console.log('Ozon Description Generator: Все товары на странице уже обработаны');
2320 await stopAutogeneration();
2321 return;
2322 }
2323
2324 // Если не нашли SKU, генерируем временный
2325 if (!nmID) {
2326 nmID = Date.now().toString();
2327 console.log('Ozon Description Generator: SKU не найден, используем временный:', nmID);
2328 }
2329
2330 console.log(`Ozon Description Generator: Обработка товара: ${productName} (SKU: ${nmID})`);
2331
2332 // Добавляем товар в список обработанных
2333 processedProducts.push(nmID);
2334 localStorage.setItem('ozon_autogen_processed_skus', JSON.stringify(processedProducts));
2335
2336 // Генерируем checkId и сохраняем данные
2337 const checkId = generateCheckId();
2338 setAutoCheck(nmID, productName, checkId);
2339
2340 // Обновляем текущий товар в прогрессе
2341 localStorage.setItem('ozon_autogen_current_product', productName);
2342
2343 // Кликаем на кнопку
2344 console.log('Ozon Description Generator: Кликаем на кнопку редактирования');
2345 editButton.click();
2346
2347 // Ждем результата через polling
2348 await waitForAutoResult(checkId);
2349 return;
2350 }
2351 }
2352
2353 console.log(`Ozon Description Generator: Найдено ссылок на товары: ${productLinks.length}`);
2354
2355 if (productLinks.length === 0) {
2356 console.log('Ozon Description Generator: Ссылки и кнопки не найдены, завершаем автогенерацию');
2357 await stopAutogeneration();
2358 return;
2359 }
2360
2361 // Берем первую ссылку
2362 const productLink = productLinks[0];
2363 const href = productLink.getAttribute('href');
2364 const nmIDMatch = href.match(/\/products\/(\d+)\//);
2365 const nmID = nmIDMatch ? nmIDMatch[1] : null;
2366
2367 if (!nmID) {
2368 console.log('Ozon Description Generator: Не удалось извлечь nmID из ссылки');
2369 await stopAutogeneration();
2370 return;
2371 }
2372
2373 // Получаем название товара из строки таблицы
2374 const row = productLink.closest('tr');
2375 const productName = row?.querySelector('td:nth-child(2)')?.textContent?.trim() || `Товар ${nmID}`;
2376
2377 console.log(`Ozon Description Generator: Обработка товара: ${productName} (nmID: ${nmID})`);
2378
2379 // Добавляем товар в список обработанных
2380 processedProducts.push(nmID);
2381 localStorage.setItem('ozon_autogen_processed_skus', JSON.stringify(processedProducts));
2382
2383 // Генерируем checkId и сохраняем данные
2384 const checkId = generateCheckId();
2385 setAutoCheck(nmID, productName, checkId);
2386
2387 // Обновляем текущий товар в прогрессе
2388 localStorage.setItem('ozon_autogen_current_product', productName);
2389
2390 // Кликаем на ссылку
2391 productLink.click();
2392
2393 // Ждем результата через polling
2394 await waitForAutoResult(checkId);
2395 }
2396
2397 // Ожидание результата через polling
2398 async function waitForAutoResult(checkId) {
2399 console.log('Ozon Description Generator: Ожидание результата для checkId:', checkId);
2400
2401 const maxWaitTime = 300000; // 5 минут
2402 const pollInterval = 500;
2403 let waitedTime = 0;
2404
2405 while (waitedTime < maxWaitTime) {
2406 await new Promise(resolve => setTimeout(resolve, pollInterval));
2407 waitedTime += pollInterval;
2408
2409 const result = getAutoResult();
2410
2411 if (result && result.checkId === checkId) {
2412 console.log('Ozon Description Generator: Получен результат:', result);
2413
2414 // Обновляем счетчики
2415 if (result.success) {
2416 const processed = parseInt(localStorage.getItem('ozon_autogen_processed') || '0');
2417 localStorage.setItem('ozon_autogen_processed', (processed + 1).toString());
2418 } else {
2419 const errors = parseInt(localStorage.getItem('ozon_autogen_errors') || '0');
2420 localStorage.setItem('ozon_autogen_errors', (errors + 1).toString());
2421 }
2422
2423 // Очищаем временные данные
2424 clearAutoCheck(checkId);
2425
2426 // Обрабатываем следующий товар
2427 setTimeout(() => processNextProduct(), 2000);
2428 return;
2429 }
2430 }
2431
2432 console.error('Ozon Description Generator: Превышено время ожидания результата');
2433 const errors = parseInt(localStorage.getItem('ozon_autogen_errors') || '0');
2434 localStorage.setItem('ozon_autogen_errors', (errors + 1).toString());
2435 clearAutoCheck(checkId);
2436
2437 // Обрабатываем следующий товар
2438 setTimeout(() => processNextProduct(), 2000);
2439 }
2440
2441 // ============================================
2442 // СКРЫТИЕ ОКНА ПРОГРЕССА ОБРАБОТКИ ТОВАРА
2443 // ============================================
2444
2445 // Скрытие окна прогресса обработки товара
2446 function hideProductProgress() {
2447 const progressDiv = document.querySelector('.ozon-product-progress');
2448 if (progressDiv) {
2449 progressDiv.remove();
2450 }
2451 }
2452
2453 // ============================================
2454 // СОЗДАНИЕ ОКНА ПРОГРЕССА ОБРАБОТКИ ТОВАРА
2455 // ============================================
2456
2457 // Показ окна прогресса обработки товара
2458 function showProductProgress() {
2459 if (document.querySelector('.ozon-product-progress')) {
2460 return;
2461 }
2462
2463 const progressDiv = document.createElement('div');
2464 progressDiv.className = 'ozon-product-progress';
2465 progressDiv.innerHTML = `
2466 <div class="ozon-product-progress-header">Обработка товара</div>
2467 <div class="ozon-product-progress-stage pending" data-stage="info">Получение информации о товаре</div>
2468 <div class="ozon-product-progress-stage pending" data-stage="keywords">Подбор ключевых слов</div>
2469 <div class="ozon-product-progress-stage pending" data-stage="analytics">Сбор данных из аналитики</div>
2470 <div class="ozon-product-progress-stage pending" data-stage="generation">Генерация описания</div>
2471 <div class="ozon-product-progress-stage pending" data-stage="insert">Вставка описания</div>
2472 <div class="ozon-product-progress-stage pending" data-stage="rich">Отправка в Rich-контент</div>
2473 <div class="ozon-product-progress-stage pending" data-stage="save">Сохранение товара</div>
2474 `;
2475
2476 document.body.appendChild(progressDiv);
2477 }
2478
2479 // ============================================
2480 // ПРОКРУТИВКА СТРАНИЦЫ
2481 // ============================================
2482
2483 // Обновление прогресса обработки товара
2484 function updateProductProgress(stageName, status) {
2485 const stageMap = {
2486 'Получение информации о товаре': 'info',
2487 'Подбор ключевых слов': 'keywords',
2488 'Сбор данных из аналитики': 'analytics',
2489 'Генерация описания': 'generation',
2490 'Вставка описания': 'insert',
2491 'Отправка в Rich-контент': 'rich',
2492 'Сохранение товара': 'save',
2493 'Ошибка': 'error'
2494 };
2495
2496 const stageId = stageMap[stageName];
2497 if (!stageId) return;
2498
2499 const stageEl = document.querySelector(`.ozon-product-progress-stage[data-stage="${stageId}"]`);
2500 if (stageEl) {
2501 stageEl.className = `ozon-product-progress-stage ${status}`;
2502 }
2503 }
2504
2505 // Обработка товара на странице редактирования при автогенерации
2506 async function handleProductPageAutogen() {
2507 console.log('Ozon Description Generator: Проверка автогенерации на странице товара');
2508
2509 // Ищем все ключи wbAutoExpected_*
2510 const expectedKeys = Object.keys(localStorage).filter(key => key.startsWith('wbAutoExpected_'));
2511
2512 if (expectedKeys.length === 0) {
2513 console.log('Ozon Description Generator: Нет ожидаемых задач автогенерации');
2514 return;
2515 }
2516
2517 const checkId = expectedKeys[0].replace('wbAutoExpected_', '');
2518 console.log('Ozon Description Generator: Найден checkId:', checkId);
2519
2520 // Проверяем, что задача свежая
2521 const autoCheck = getAutoCheck();
2522 if (!autoCheck || autoCheck.checkId !== checkId) {
2523 console.log('Ozon Description Generator: autoCheck не найден или не совпадает');
2524 clearAutoCheck(checkId);
2525 return;
2526 }
2527
2528 console.log('Ozon Description Generator: Начинаем обработку товара:', autoCheck.title);
2529
2530 // Показываем окно прогресса обработки товара
2531 showProductProgress();
2532
2533 try {
2534 // Проверяем, на какой странице мы находимся
2535 const currentUrl = window.location.href;
2536
2537 if (currentUrl.includes('/edit/general-info')) {
2538 // Шаг 1: Получаем название товара
2539 updateProductProgress('Получение информации о товаре', 'active');
2540 console.log('Ozon Description Generator: Ожидаем загрузки страницы товара');
2541
2542 await new Promise(resolve => setTimeout(resolve, 5000));
2543
2544 let title = '';
2545 let attempts = 0;
2546 const maxAttempts = 20;
2547
2548 while (attempts < maxAttempts) {
2549 const titleInput = document.querySelector('input[name="name"]');
2550 if (titleInput && titleInput.value.trim()) {
2551 title = titleInput.value;
2552 console.log('Ozon Description Generator: Название товара найдено:', title);
2553 break;
2554 }
2555 console.log('Ozon Description Generator: Попытка', attempts + 1, '- название еще не загружено');
2556 await new Promise(resolve => setTimeout(resolve, 500));
2557 attempts++;
2558 }
2559
2560 if (!title) {
2561 throw new Error('Не удалось загрузить название товара');
2562 }
2563
2564 // Сохраняем название
2565 localStorage.setItem(`ozon_autogen_title_${checkId}`, title);
2566
2567 // Переходим на all-attrs
2568 console.log('Ozon Description Generator: Название загружено, переходим на all-attrs');
2569 const newUrl = currentUrl.replace('/edit/general-info', '/edit/all-attrs');
2570 window.location.href = newUrl;
2571 return;
2572 }
2573
2574 // Если мы на странице all-attrs - продолжаем обработку
2575 if (currentUrl.includes('/edit/all-attrs')) {
2576 // Получаем сохраненное название
2577 const title = localStorage.getItem(`ozon_autogen_title_${checkId}`) || autoCheck.title;
2578 console.log('Ozon Description Generator: Используем название:', title);
2579
2580 updateProductProgress('Получение информации о товаре', 'active');
2581 await new Promise(resolve => setTimeout(resolve, 5000));
2582
2583 const productInfo = { title };
2584 updateProductProgress('Получение информации о товаре', 'completed');
2585
2586 // Очищаем временное сохранение названия
2587 localStorage.removeItem(`ozon_autogen_title_${checkId}`);
2588
2589 // Проверяем тестовый режим
2590 const testMode = localStorage.getItem('ozon_autogen_test_mode') === 'true';
2591 const sendToRich = localStorage.getItem('ozon_autogen_send_to_rich') === 'true';
2592
2593 if (testMode) {
2594 console.log('Ozon Description Generator: 🧪 ТЕСТОВЫЙ РЕЖИМ - пропускаем AI, вставляем "тест"');
2595
2596 updateProductProgress('Подбор ключевых слов', 'completed');
2597 updateProductProgress('Генерация описания', 'completed');
2598
2599 updateProductProgress('Вставка описания', 'active');
2600 const proseMirrorDiv = document.querySelector('[id="attribute#4191"] .ProseMirror');
2601 if (proseMirrorDiv) {
2602 proseMirrorDiv.innerHTML = '<p>тест</p>';
2603 proseMirrorDiv.dispatchEvent(new Event('input', { bubbles: true }));
2604 proseMirrorDiv.dispatchEvent(new Event('change', { bubbles: true }));
2605 console.log('Ozon Description Generator: 🧪 Тестовое описание "тест" вставлено');
2606 updateProductProgress('Вставка описания', 'completed');
2607 }
2608
2609 if (sendToRich) {
2610 updateProductProgress('Отправка в Rich-контент', 'active');
2611 await GM.setValue('ozon_description_for_rich', 'тест');
2612
2613 const mediaTabButton = Array.from(document.querySelectorAll('button.c9s110-a3.c9s110-a0.c9s110-a.c9s110-a4.c9s110-a6')).find(btn =>
2614 btn.textContent && btn.textContent.includes('Медиа')
2615 );
2616
2617 if (mediaTabButton) {
2618 console.log('Ozon Description Generator: Переходим на вкладку Медиа');
2619 mediaTabButton.click();
2620 await new Promise(resolve => setTimeout(resolve, 1000));
2621 await insertRichContent();
2622 }
2623 updateProductProgress('Отправка в Rich-контент', 'completed');
2624 }
2625
2626 } else {
2627 // ОБЫЧНЫЙ РЕЖИМ - весь код с AI
2628
2629 updateProductProgress('Подбор ключевых слов', 'active');
2630 const globalKeywords = localStorage.getItem('ozon_autogen_global_keywords') || '';
2631 const customPrompt = localStorage.getItem('ozon_autogen_custom_prompt') || '';
2632 let keywords = [];
2633
2634 if (globalKeywords) {
2635 keywords = globalKeywords.split('\n').map(k => k.trim()).filter(k => k);
2636 console.log('Ozon Description Generator: Используем глобальные ключевые слова:', keywords);
2637 updateProductProgress('Подбор ключевых слов', 'completed');
2638 } else {
2639 console.log('Ozon Description Generator: AI подбирает ключевые слова');
2640 const suggestPrompt = generateMasksPrompt(productInfo);
2641 const suggestResponse = await RM.aiCall(suggestPrompt);
2642 const suggestData = JSON.parse(suggestResponse);
2643 keywords = [...(suggestData.masks || []), ...(suggestData.keywords || [])];
2644 console.log('Ozon Description Generator: AI подобрал', keywords.length, 'ключевых слов');
2645 updateProductProgress('Подбор ключевых слов', 'completed');
2646 }
2647
2648 updateProductProgress('Сбор данных из аналитики', 'active');
2649 const analyticsData = await collectAnalyticsData(keywords, []);
2650
2651 if (analyticsData.length === 0) {
2652 throw new Error('Не удалось собрать данные из аналитики');
2653 }
2654 console.log('Ozon Description Generator: Данные из аналитики собраны');
2655 updateProductProgress('Сбор данных из аналитики', 'completed');
2656
2657 updateProductProgress('Генерация описания', 'active');
2658 const allQueries = [];
2659 const queryPopularity = {};
2660 analyticsData.forEach(data => {
2661 data.queries.forEach(q => {
2662 allQueries.push(q.query);
2663 queryPopularity[q.query.toLowerCase()] = q.popularity;
2664 });
2665 });
2666
2667 console.log('Ozon Description Generator: Генерируем описание с', allQueries.length, 'запросами');
2668 const descriptionPrompt = generateDescriptionPrompt(productInfo, keywords, allQueries, queryPopularity, customPrompt);
2669 const description = await RM.aiCall(descriptionPrompt);
2670 console.log('Ozon Description Generator: Описание сгенерировано, длина:', description.length);
2671 updateProductProgress('Генерация описания', 'completed');
2672
2673 updateProductProgress('Вставка описания', 'active');
2674 const proseMirrorDiv = document.querySelector('[id="attribute#4191"] .ProseMirror');
2675 if (proseMirrorDiv) {
2676 proseMirrorDiv.innerHTML = `<p>${description.replace(/\n\n/g, '</p><p>').replace(/\n/g, '<br>')}</p>`;
2677 proseMirrorDiv.dispatchEvent(new Event('input', { bubbles: true }));
2678 proseMirrorDiv.dispatchEvent(new Event('change', { bubbles: true }));
2679 console.log('Ozon Description Generator: Описание вставлено');
2680 updateProductProgress('Вставка описания', 'completed');
2681 }
2682
2683 if (sendToRich) {
2684 updateProductProgress('Отправка в Rich-контент', 'active');
2685 await GM.setValue('ozon_description_for_rich', description);
2686
2687 const mediaTabButton = Array.from(document.querySelectorAll('button.c9s110-a3.c9s110-a0.c9s110-a.c9s110-a4.c9s110-a6')).find(btn =>
2688 btn.textContent && btn.textContent.includes('Медиа')
2689 );
2690
2691 if (mediaTabButton) {
2692 console.log('Ozon Description Generator: Переходим на вкладку Медиа');
2693 mediaTabButton.click();
2694 await new Promise(resolve => setTimeout(resolve, 1000));
2695 await insertRichContent();
2696 }
2697 updateProductProgress('Отправка в Rich-контент', 'completed');
2698 }
2699 }
2700
2701 // Ждем 3 секунды перед сохранением
2702 console.log('Ozon Description Generator: Ожидание 3 секунды перед сохранением');
2703 await new Promise(resolve => setTimeout(resolve, 3000));
2704
2705 // Сохраняем товар
2706 updateProductProgress('Сохранение товара', 'active');
2707 await new Promise(resolve => setTimeout(resolve, 3000));
2708
2709 const buttons = Array.from(document.querySelectorAll('button'));
2710 const moderationButton = buttons.find(btn =>
2711 btn.textContent && btn.textContent.includes('Отправить на модерацию')
2712 );
2713
2714 if (moderationButton) {
2715 console.log('Ozon Description Generator: Кнопка "Отправить на модерацию" найдена');
2716
2717 const clickEvent = new MouseEvent('click', {
2718 view: window,
2719 bubbles: true,
2720 cancelable: true
2721 });
2722 moderationButton.dispatchEvent(clickEvent);
2723
2724 console.log('Ozon Description Generator: Клик выполнен, товар отправлен на модерацию');
2725 updateProductProgress('Сохранение товара', 'completed');
2726
2727 // Сохраняем успешный результат
2728 setAutoResult(checkId, true);
2729
2730 hideProductProgress();
2731
2732 // Ждем полсекунды перед закрытием вкладки
2733 console.log('Ozon Description Generator: Ожидание 0.5 секунды перед закрытием вкладки');
2734 await new Promise(resolve => setTimeout(resolve, 500));
2735
2736 console.log('Ozon Description Generator: Закрываем вкладку товара');
2737 window.close();
2738 } else {
2739 throw new Error('Кнопка "Отправить на модерацию" не найдена');
2740 }
2741 }
2742
2743 } catch (error) {
2744 console.error('Ozon Description Generator: Ошибка при обработке товара:', error);
2745 updateProductProgress('Ошибка', 'error');
2746
2747 // Очищаем временное сохранение названия
2748 localStorage.removeItem(`ozon_autogen_title_${checkId}`);
2749
2750 // Сохраняем результат с ошибкой
2751 setAutoResult(checkId, false, error.message);
2752
2753 console.log('Ozon Description Generator: Ошибка, закрываем вкладку через 3 секунды');
2754
2755 setTimeout(() => {
2756 hideProductProgress();
2757 window.close();
2758 }, 3000);
2759 }
2760 }
2761
2762 // ============================================
2763 // ИНИЦИАЛИЗАЦИЯ
2764 // ============================================
2765
2766 function init() {
2767 console.log('Ozon Description Generator: Инициализация');
2768
2769 // Страница списка товаров
2770 if (window.location.href === 'https://seller.ozon.ru/app/products' ||
2771 (window.location.href.includes('seller.ozon.ru/app/products') && !window.location.href.includes('/edit/'))) {
2772 console.log('Ozon Description Generator: Страница списка товаров');
2773
2774 // Постоянно следим за кнопкой автогенерации
2775 const debouncedCreateButton = debounce(createAutogenButton, 500);
2776
2777 const observer = new MutationObserver(() => {
2778 debouncedCreateButton();
2779 });
2780
2781 observer.observe(document.body, {
2782 childList: true,
2783 subtree: true
2784 });
2785
2786 setTimeout(createAutogenButton, 2000);
2787
2788 // Проверяем, нужно ли продолжить автогенерацию - СРАЗУ и через интервалы
2789 checkAndContinueAutogen();
2790
2791 // Проверяем каждые 2 секунды, нужно ли продолжить
2792 const autogenCheckInterval = setInterval(async () => {
2793 const autogenStatus = await GM.getValue('ozon_autogen_status', 'stopped');
2794 if (autogenStatus === 'running') {
2795 console.log('Ozon Description Generator: Обнаружена активная автогенерация, продолжаем');
2796 clearInterval(autogenCheckInterval);
2797 showAutogenProgress();
2798 setTimeout(processNextProduct, 3000);
2799 }
2800 }, 2000);
2801
2802 // Останавливаем проверку через 30 секунд
2803 setTimeout(() => clearInterval(autogenCheckInterval), 30000);
2804 }
2805
2806 // Страница редактирования товара
2807 if (window.location.href.includes('seller.ozon.ru/app/products/') &&
2808 (window.location.href.includes('/edit/all-attrs') || window.location.href.includes('/edit/general-info'))) {
2809 console.log('Ozon Description Generator: Страница редактирования товара');
2810
2811 // Проверяем, идет ли автогенерация
2812 checkAndStartAutogen();
2813
2814 const observer = new MutationObserver((mutations, obs) => {
2815 const annotationContainer = document.querySelector('[id="attribute#4191"]');
2816 if (annotationContainer) {
2817 createGeneratorButton();
2818 obs.disconnect();
2819 }
2820 });
2821
2822 observer.observe(document.body, {
2823 childList: true,
2824 subtree: true
2825 });
2826
2827 setTimeout(createGeneratorButton, 2000);
2828 }
2829
2830 if (window.location.href.includes('seller.ozon.ru/app/products/') && window.location.href.includes('/edit/media')) {
2831 console.log('Ozon Description Generator: Страница медиа');
2832 setTimeout(insertRichContent, 2000);
2833 }
2834
2835 if (window.location.href.includes('seller.ozon.ru/app/analytics/what-to-sell/all-queries')) {
2836 setTimeout(autoCollectOnAnalyticsPage, 2000);
2837 }
2838 }
2839
2840 // Проверка и продолжение автогенерации на странице списка
2841 async function checkAndContinueAutogen() {
2842 console.log('Ozon Description Generator: Проверяем необходимость продолжения автогенерации');
2843
2844 // Проверяем, есть ли активная задача
2845 const autoCheck = getAutoCheck();
2846
2847 if (autoCheck && isTimestampFresh(autoCheck.timestamp)) {
2848 console.log('Ozon Description Generator: Найдена активная задача автогенерации');
2849 showAutogenProgress();
2850
2851 // Ждем результата
2852 await waitForAutoResult(autoCheck.checkId);
2853 }
2854 }
2855
2856 // Проверка и запуск автогенерации
2857 async function checkAndStartAutogen() {
2858 // Проверяем, есть ли ожидаемая задача для этой вкладки
2859 const expectedKeys = Object.keys(localStorage).filter(key => key.startsWith('wbAutoExpected_'));
2860
2861 if (expectedKeys.length > 0) {
2862 console.log('Ozon Description Generator: Обнаружена задача автогенерации, запускаем обработку');
2863 await handleProductPageAutogen();
2864 }
2865 }
2866
2867 // Показ окна прогресса автогенерации
2868 async function showAutogenProgress() {
2869 if (document.querySelector('.ozon-autogen-progress')) {
2870 return;
2871 }
2872
2873 const progressDiv = document.createElement('div');
2874 progressDiv.className = 'ozon-autogen-progress';
2875 progressDiv.innerHTML = `
2876 <div class="ozon-autogen-progress-header">
2877 <span>🤖 Автогенерация</span>
2878 <span class="ozon-autogen-progress-close" id="ozon-autogen-progress-close">×</span>
2879 </div>
2880 <div class="ozon-autogen-progress-info" id="ozon-autogen-progress-info">
2881 Ожидание...
2882 </div>
2883 <div class="ozon-autogen-progress-stats">
2884 <div class="ozon-autogen-progress-stat">
2885 <div class="ozon-autogen-progress-stat-label">Обработано</div>
2886 <div class="ozon-autogen-progress-stat-value success" id="ozon-autogen-processed">0</div>
2887 </div>
2888 <div class="ozon-autogen-progress-stat">
2889 <div class="ozon-autogen-progress-stat-label">Ошибок</div>
2890 <div class="ozon-autogen-progress-stat-value error" id="ozon-autogen-errors">0</div>
2891 </div>
2892 </div>
2893 <div class="ozon-autogen-progress-controls">
2894 <button class="ozon-autogen-progress-btn pause" id="ozon-autogen-pause-btn">⏸ Пауза</button>
2895 <button class="ozon-autogen-progress-btn stop" id="ozon-autogen-stop-btn">⏹ Остановить</button>
2896 </div>
2897 `;
2898
2899 document.body.appendChild(progressDiv);
2900
2901 document.getElementById('ozon-autogen-progress-close').addEventListener('click', () => {
2902 progressDiv.remove();
2903 });
2904
2905 document.getElementById('ozon-autogen-pause-btn').addEventListener('click', toggleAutogenPause);
2906 document.getElementById('ozon-autogen-stop-btn').addEventListener('click', stopAutogeneration);
2907
2908 // Обновляем прогресс каждую секунду
2909 setInterval(updateAutogenProgress, 1000);
2910 }
2911
2912 // Обновление прогресса автогенерации
2913 async function updateAutogenProgress() {
2914 const progressDiv = document.querySelector('.ozon-autogen-progress');
2915 if (!progressDiv) return;
2916
2917 const processed = parseInt(localStorage.getItem('ozon_autogen_processed') || '0');
2918 const errors = parseInt(localStorage.getItem('ozon_autogen_errors') || '0');
2919 const currentProduct = localStorage.getItem('ozon_autogen_current_product') || '';
2920
2921 const processedEl = document.getElementById('ozon-autogen-processed');
2922 const errorsEl = document.getElementById('ozon-autogen-errors');
2923 const infoEl = document.getElementById('ozon-autogen-progress-info');
2924
2925 if (processedEl) processedEl.textContent = processed;
2926 if (errorsEl) errorsEl.textContent = errors;
2927 if (infoEl) {
2928 if (currentProduct) {
2929 infoEl.textContent = `Обработка: ${currentProduct}`;
2930 } else {
2931 infoEl.textContent = 'Ожидание...';
2932 }
2933 }
2934 }
2935
2936 // Переключение паузы автогенерации
2937 async function toggleAutogenPause() {
2938 // Пауза больше не нужна в новой логике
2939 console.log('Ozon Description Generator: Пауза не поддерживается в новой версии');
2940 }
2941
2942 // Остановка автогенерации
2943 async function stopAutogeneration() {
2944 console.log('Ozon Description Generator: Автогенерация остановлена');
2945
2946 // Очищаем все данные автогенерации
2947 const autoCheck = getAutoCheck();
2948 if (autoCheck) {
2949 clearAutoCheck(autoCheck.checkId);
2950 }
2951
2952 localStorage.removeItem('ozon_autogen_global_keywords');
2953 localStorage.removeItem('ozon_autogen_send_to_rich');
2954 localStorage.removeItem('ozon_autogen_test_mode');
2955 localStorage.removeItem('ozon_autogen_current_product');
2956
2957 const progressDiv = document.querySelector('.ozon-autogen-progress');
2958 if (progressDiv) {
2959 setTimeout(() => progressDiv.remove(), 2000);
2960 }
2961 }
2962
2963 if (document.readyState === 'loading') {
2964 document.addEventListener('DOMContentLoaded', init);
2965 } else {
2966 init();
2967 }
2968
2969})();