Генератор SEO-описаний для товаров на Ozon с анализом ключевых слов
Size
134.7 KB
Version
3.2.65
Created
Jan 22, 2026
Updated
3 months ago
1// ==UserScript==
2// @name Ozon Description Generator 3.0
3// @description Генератор SEO-описаний для товаров на Ozon с анализом ключевых слов
4// @version 3.2.65
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• Состав: ${productInfo.composition || 'не указан'}
42
43ВАЖНОЕ ПРАВИЛО:
441. АНАЛИЗИРУЙ название товара и используй ТОЧНО ТОТ ТИП ТОВАРА, который указан в названии
452. НЕ заменяй тип товара на другой, даже если похоже
463. Если в названии "маска для лица" — ВСЕ ключи должны быть про МАСКУ, не про сыворотку, крем или другие товары
474. Если в названии "сыворотка" — все ключи про сыворотку
485. Если в названии "крем" — все ключи про крем
49
50Предложи 2 типа запросов:
51
521. МАСКИ (15 штук) — короткие слова:
53 - Используй ТОЧНО тип товара из названия
54 - Пример для "маска для лица": ["маска", "маска для", "для лица", ...]
55 - НЕЛЬЗЯ: ["сыворотка", "крем", "гель"] если это не указано в названии
56
572. КЛЮЧЕВЫЕ СЛОВА (15 штук) — точные запросы:
58 - Начинай с типа товара из названия
59 - Пример для "маска для лица": ["маска для лица", "тканевая маска для лица", ...]
60 - НЕЛЬЗЯ: ["сыворотка для лица", "крем для лица", ...] если это не маска
61
62КАТЕГОРИИ ДЛЯ МАСОК:
63- ТИП ТОВАРА ИЗ НАЗВАНИЯ (3 маски): основное название
64- ТИП ТОВАРА + предлог (4 маски)
65- Синонимы ТОГО ЖЕ ТИПА (2 маски)
66- Назначение (2 маски)
67- Ингредиенты из состава (3 маски)
68- Эффект (1 маска)
69
70КАТЕГОРИИ ДЛЯ КЛЮЧЕВЫХ СЛОВ:
71- ТИП ТОВАРА + назначение (6 ключей)
72- ТИП ТОВАРА + аудитория (2 ключа)
73- ТИП ТОВАРА + ингредиент (5 ключей)
74- ТИП ТОВАРА + эффект (2 ключей)
75
76ПРАВИЛА:
77- Используй ТОЧНО тип товара из названия, не меняй его
78- Если состав пустой, используй только общие ключи
79- Все слова на русском (кроме названий компонентов типа "hyaluronic", "niacinamide")
80- Без брендов
81
82ПРИМЕР ДЛЯ "Маска для лица с гиалуроновой кислотой" (состав: гиалуроновая кислота, алоэ вера):
83{
84 "masks": ["маска", "маска для", "для лица", "тканевая", "патчи", "гиалуроновая", "алоэ", "увлажнение", "освежение", "уход", "косметика", "средство", "средство для", "уходовая", "гидратация"],
85 "keywords": ["маска для лица", "тканевая маска для лица", "маска для лица увлажняющая", "маска с гиалуроновой кислотой", "гиалуроновая маска для лица", "маска для лица с алоэ", "увлажняющая маска для лица", "маска для лица тканевая", "освежающая маска для лица", "маска для лица гиалуроновая", "маска для лица для увлажнения", "маска для лица ежедневного ухода", "маска для лица против сухости", "маска для лица уходовая", "маска для лица с алоэ вера"]
86}
87
88ПРИМЕР ДЛЯ "Сыворотка для лица с витамином С" (состав: витамин С, ниацинамид):
89{
90 "masks": ["сыворотка", "сыворотка для", "серум", "витамин с", "ниацинамид", "антиоксидант", "осветление", "для лица", "уход", "косметика", "средство", "средство для", "антивозрастная", "витаминная", "активная"],
91 "keywords": ["сыворотка для лица", "сыворотка с витамином с", "витамин с для лица", "осветляющая сыворотка", "сыворотка для лица с ниацинамидом", "антиоксидантная сыворотка", "сыворотка против пигментации", "антивозрастная сыворотка", "сыворотка для сияния кожи", "сыворотка для лица витаминная", "сыворотка для лица активная", "сыворотка для ровного тона", "сыворотка с антиоксидантами", "сыворотка для осветления", "сыворотка для ухода за лицом"]
92}
93
94Верни ТОЛЬКО JSON:
95{
96 "masks": ["маска1", "маска2", ...],
97 "keywords": ["ключ1", "ключ2", ...]
98}
99
100Начни ответ сразу с {`;
101 }
102
103 function generateDescriptionPrompt(productInfo, keywords, filteredQueries, queryPopularity = {}) {
104 const sortedQueries = filteredQueries
105 .map(q => ({ query: q, pop: queryPopularity[q.toLowerCase()] || 0 }))
106 .sort((a, b) => b.pop - a.pop);
107
108 const highPriority = sortedQueries.slice(0, 25).map(q => q.query);
109 const mediumPriority = sortedQueries.slice(25, 70).map(q => q.query);
110 const lowPriority = sortedQueries.slice(70, 120).map(q => q.query);
111
112 return `Создай SEO-описание товара для Ozon.
113
114ДАННЫЕ:
115• Название: ${productInfo.title || 'не указано'}
116• Состав: ${productInfo.composition || 'не указан'}
117• Базовые ключи: ${keywords.join(', ')}
118
119ЗАПРОСЫ ПО ПРИОРИТЕТУ:
120
121🔴 ВЫСОКИЙ ПРИОРИТЕТ (использовать ВСЕ 100%, минимум 1 раз каждый):
122${highPriority.map((q, i) => `${i+1}. "${q}"`).join('\n')}
123
124🟡 СРЕДНИЙ ПРИОРИТЕТ (использовать 80-90%):
125${mediumPriority.length > 0 ? mediumPriority.map((q, i) => `${i+1}. "${q}"`).join('\n') : 'нет запросов'}
126
127🟢 НИЗКИЙ ПРИОРИТЕТ (использовать 50-70%):
128${lowPriority.length > 0 ? lowPriority.map((q, i) => `${i+1}. "${q}"`).join('\n') : 'нет запросов'}
129
130ТРЕБОВАНИЯ:
1311) Объем 3500–4000 символов, только текст.
1322) Структура (каждая часть с нового абзаца. между абзацами пустая строка):
133 - Введение (400-500 симв.) — суть товара, что это, зачем, для кого, что делает кратко.
134 - Проблема и решение (600-800 симв.) — основная проблема, которую решает это средство, к чему приводит проблема. Кратко как этот продукт решает проблему.
135 - Состав и действие (800-1000 симв.) — ингредиенты и их польза. Подробная информация о составе и компонентах которые усиливают друг друга. как они работают, зачем они нужны.
136 - Применение (600-800 симв.) — для кого, как работает
137 - Отзывы покупателей (300-400 симв.) — отдельный абзац, который начинается фразой "Наши покупатели отмечают ... через ...", где описан эффект и сроки появления результата
138 - Заключение (400-500 симв.) — результат для покупателя. что получит покупатель при использовании средства.
1393) Плавные переходы между частями, без заголовков.
1404) Все базовые ключи — минимум 1 раз.
1415) Каждый запрос использовать не более 2 раз.
1426) Если компонента из запроса нет в составе — используй через «альтернативу» (засчитывается).
1437) Все компоненты состава — 1–2 раза, без списков, вплетай в текст.
1448) Размер текста минимум 3000 символов - ПРОВЕРЬ!
145
146ГРУППИРОВКА ЗАПРОСОВ:
1479) Сгруппируй запросы по смыслу:
148 • Тип/категория товара
149 • Проблемы/назначение
150 • Состав/ингредиенты
151 • Аудитория
152 • Эффект/результат
15310) В каждом абзаце используй запросы из 2-3 групп одновременно.
15411) Комбинируй разные типы запросов в одном предложении естественным образом.
155
156ПРАВИЛА ПЛОТНОСТИ КЛЮЧЕЙ:
15712) Плотность ключей: 4-5 запросов на 100 слов (это нормально для SEO).
15813) В каждом абзаце минимум 3-4 ключевых запроса.
15914) Встраивай ключи естественно в середину и начало предложений.
16015) Используй синонимы и вариации ключей для естественности.
16116) Одно и то же ключевое словосочетание — максимум 2 раза в тексте.
16217) Чередуй короткие (2-3 слова) и длинные (4-5 слов) ключи.
163
164СТИЛЬ:
16518) Информативно, конкретные факты, без воды.
16619) Между абзацами пустая строка.
16720) Чередуй длину предложений: 30% короткие (8-12 слов), 70% средние (13-20 слов).
16821) Используй разные конструкции предложений для естественности.
169
170ЗАПРЕТЫ:
17122) Только русский язык. Без брендов, компаний, фамилий.
17223) Запрещены слова: «лекарство», «препарат», «революционный», «инновационный», «уникальный».
17324) Без заголовков, списков, вопросов, эмоджи, инструкций хранения и описания неактивных компонентов.
174
175ПРИМЕР ХОРОШЕГО АБЗАЦА С ВЫСОКОЙ ПЛОТНОСТЬЮ КЛЮЧЕЙ:
176Натуральный гель для интимной гигиены обеспечивает деликатный уход и поддерживает естественный pH баланс. Средство для интимной гигиены с мягкой формулой подходит для ежедневного применения и помогает предотвратить дискомфорт. Гель с пантенолом и экстрактом лаванды увлажняет кожу деликатных зон, а водная основа делает текстуру легкой и комфортной. Интимный гель для женщин не содержит агрессивных компонентов и подходит даже для чувствительной кожи.
177
178ПРИМЕР ПЛОХОГО АБЗАЦА (НЕ ДЕЛАЙ ТАК):
179Гель для интимной гигиены очищает. Интимный гель увлажняет. Гель для женщин помогает.
180
181ВАЖНО:
182- Используй МАКСИМУМ запросов из списка
183- Вплетай их естественно, но плотно
184- Не бойся использовать 4-5 ключей на 100 слов
185- Главное - сохранять читаемость и естественность
186
187ВЫВОД:
188Начни сразу с описания. Никаких вступлений, вопросов и пояснений.`;
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 // Получаем название товара
862 let title = '';
863 const titleInput = document.querySelector('input[name="name"]');
864 if (titleInput) {
865 title = titleInput.value;
866 }
867
868 // Получаем состав
869 let composition = '';
870 const compositionTextarea = document.querySelector('textarea[name="attribute#8050"]');
871 if (compositionTextarea) {
872 composition = compositionTextarea.value;
873 }
874
875 return { title, composition, sku };
876 }
877
878 // ============================================
879 // СОЗДАНИЕ КНОПКИ ГЕНЕРАТОРА
880 // ============================================
881
882 function createGeneratorButton() {
883 // Ищем контейнер с аннотацией (описанием)
884 const annotationContainer = document.querySelector('[id="attribute#4191"]')?.closest('.index_formField_P3SmU');
885
886 if (!annotationContainer) {
887 return false;
888 }
889
890 if (document.querySelector('.ozon-desc-generator-btn')) {
891 return true;
892 }
893
894 const buttonsContainer = document.createElement('div');
895 buttonsContainer.style.cssText = 'display: flex; gap: 8px; margin-top: 12px;';
896
897 const generatorButton = document.createElement('button');
898 generatorButton.className = 'ozon-desc-generator-btn';
899 generatorButton.textContent = '✨ Генератор описаний';
900 generatorButton.type = 'button';
901 generatorButton.style.width = 'auto';
902 generatorButton.style.flex = '1';
903 generatorButton.addEventListener('click', function(e) {
904 e.preventDefault();
905 openModal();
906 });
907
908 const richContentButton = document.createElement('button');
909 richContentButton.className = 'ozon-desc-generator-btn';
910 richContentButton.textContent = '📄 Отправить в Rich-контент';
911 richContentButton.type = 'button';
912 richContentButton.style.width = 'auto';
913 richContentButton.style.flex = '1';
914 richContentButton.style.background = 'linear-gradient(135deg, #10b981, #059669)';
915 richContentButton.addEventListener('click', function(e) {
916 e.preventDefault();
917 sendToRichContent();
918 });
919 richContentButton.addEventListener('mouseenter', function() {
920 this.style.background = 'linear-gradient(135deg, #059669, #047857)';
921 });
922 richContentButton.addEventListener('mouseleave', function() {
923 this.style.background = 'linear-gradient(135deg, #10b981, #059669)';
924 });
925
926 buttonsContainer.appendChild(generatorButton);
927 buttonsContainer.appendChild(richContentButton);
928
929 annotationContainer.appendChild(buttonsContainer);
930 return true;
931 }
932
933 // ============================================
934 // ПОКАЗ СТАТУСА
935 // ============================================
936
937 function showStatus(container, message, type) {
938 container.innerHTML = `<div class="ozon-desc-status ${type}">${message}</div>`;
939 }
940
941 // ============================================
942 // МОДАЛЬНОЕ ОКНО
943 // ============================================
944
945 async function openModal() {
946 const productInfo = await getProductInfo();
947 const currentSKU = productInfo.sku;
948
949 let savedKeywords = '';
950 let savedMinusWords = '';
951
952 if (currentSKU) {
953 savedKeywords = await GM.getValue(`ozon_product_${currentSKU}_keywords`, '');
954 savedMinusWords = await GM.getValue(`ozon_product_${currentSKU}_minus_words`, '');
955 }
956
957 const modal = document.createElement('div');
958 modal.className = 'ozon-desc-modal';
959 modal.innerHTML = `
960 <div class="ozon-desc-modal-content">
961 <div class="ozon-desc-modal-header">✨ Генератор описаний для Ozon</div>
962
963 <div class="ozon-desc-input-group">
964 <label class="ozon-desc-label">Введите ключевые слова (каждое с новой строки):</label>
965 <textarea class="ozon-desc-textarea" id="ozon-keywords-input" placeholder="Например: сыворотка для лица витамин с увлажнение">${savedKeywords}</textarea>
966 <button class="ozon-desc-suggest-btn" id="ozon-suggest-keywords-btn">🔍 Предложить поисковые маски</button>
967 <div id="ozon-suggested-keywords-container" style="display: none;"></div>
968 </div>
969
970 <div class="ozon-desc-input-group">
971 <label class="ozon-desc-label">Минус-слова (каждое с новой строки):</label>
972 <textarea class="ozon-desc-textarea" style="min-height: 80px;" id="ozon-minus-words-input" placeholder="Например: mixit nivea корея">${savedMinusWords}</textarea>
973 </div>
974
975 <div id="ozon-desc-result-container" style="display: none;">
976 <div class="ozon-desc-label">Сгенерированное описание:</div>
977 <div class="ozon-desc-result" id="ozon-desc-result"></div>
978 <div class="ozon-desc-char-count" id="ozon-char-count"></div>
979 <div id="ozon-desc-stats-container"></div>
980 </div>
981
982 <div id="ozon-desc-status-container"></div>
983
984 <div class="ozon-desc-buttons">
985 <button class="ozon-desc-btn ozon-desc-btn-secondary" id="ozon-close-btn">Закрыть</button>
986 <button class="ozon-desc-btn ozon-desc-btn-primary" id="ozon-generate-btn">🚀 Сгенерировать</button>
987 <button class="ozon-desc-btn ozon-desc-btn-primary" id="ozon-regenerate-btn" style="display: none;">🔄 Перегенерировать</button>
988 <button class="ozon-desc-btn ozon-desc-btn-success" id="ozon-insert-btn" style="display: none;">✅ Вставить в описание</button>
989 </div>
990 </div>
991 `;
992
993 document.body.appendChild(modal);
994
995 modal.addEventListener('click', (e) => {
996 if (e.target === modal) {
997 modal.remove();
998 }
999 });
1000
1001 document.getElementById('ozon-close-btn').addEventListener('click', () => {
1002 modal.remove();
1003 });
1004
1005 document.getElementById('ozon-suggest-keywords-btn').addEventListener('click', () => {
1006 suggestKeywords();
1007 });
1008
1009 document.getElementById('ozon-generate-btn').addEventListener('click', () => {
1010 generateDescription(modal);
1011 });
1012
1013 document.getElementById('ozon-regenerate-btn').addEventListener('click', () => {
1014 generateDescription(modal, true);
1015 });
1016
1017 document.getElementById('ozon-insert-btn').addEventListener('click', () => {
1018 insertDescription(modal);
1019 });
1020 }
1021
1022 // ============================================
1023 // ПРЕДЛОЖЕНИЕ КЛЮЧЕВЫХ СЛОВ / МАСОК
1024 // ============================================
1025
1026 async function suggestKeywords() {
1027 const keywordsInput = document.getElementById('ozon-keywords-input');
1028 const suggestBtn = document.getElementById('ozon-suggest-keywords-btn');
1029 const suggestedContainer = document.getElementById('ozon-suggested-keywords-container');
1030 const statusContainer = document.getElementById('ozon-desc-status-container');
1031
1032 const keywordsText = keywordsInput.value.trim();
1033
1034 if (!keywordsText) {
1035 showStatus(statusContainer, 'Пожалуйста, сначала введите базовые ключевые слова', 'error');
1036 return;
1037 }
1038
1039 suggestBtn.disabled = true;
1040 suggestBtn.textContent = '⏳ AI анализирует...';
1041 showStatus(statusContainer, 'AI анализирует товар и подбирает поисковые маски и ключевые слова...', 'info');
1042
1043 try {
1044 const productInfo = await getProductInfo();
1045
1046 const suggestPrompt = generateMasksPrompt(productInfo);
1047 console.log('Ozon Description Generator: Запрос масок от AI');
1048
1049 const suggestResponse = await RM.aiCall(suggestPrompt);
1050
1051 let masks = [];
1052 let aiKeywords = [];
1053 try {
1054 const suggestData = JSON.parse(suggestResponse);
1055 masks = Array.isArray(suggestData.masks) ? suggestData.masks.filter(Boolean) : [];
1056 aiKeywords = Array.isArray(suggestData.keywords) ? suggestData.keywords.filter(Boolean) : [];
1057 console.log(`Ozon Description Generator: AI предложил ${masks.length} масок и ${aiKeywords.length} ключей`);
1058 } catch (e) {
1059 console.error('Ozon Description Generator: Ошибка парсинга масок:', e);
1060 showStatus(statusContainer, 'Ошибка при получении предложений. Попробуйте еще раз.', 'error');
1061 return;
1062 }
1063
1064 if (masks.length === 0 && aiKeywords.length === 0) {
1065 showStatus(statusContainer, 'AI не смог предложить маски и ключевые слова', 'error');
1066 return;
1067 }
1068
1069 const hasChips = masks.length + aiKeywords.length > 0;
1070
1071 suggestedContainer.innerHTML = `
1072 <div class="ozon-desc-masks-container">
1073 <div class="ozon-desc-masks-header">
1074 <span>Поисковые маски и ключевые слова (кликните для выбора):</span>
1075 ${hasChips ? `<button class="ozon-desc-suggest-btn" id="ozon-toggle-all-btn" style="padding: 4px 12px; font-size: 13px;">
1076 Выбрать все
1077 </button>` : ''}
1078 </div>
1079 ${masks.length ? `
1080 <div class="ozon-desc-masks-group">
1081 <div class="ozon-desc-masks-group-title">МАСКИ</div>
1082 <div class="ozon-desc-masks-grid">
1083 ${masks.map(mask => `
1084 <div class="ozon-mask-chip" data-type="маска" data-mask="${mask}">
1085 <span>${mask}</span>
1086 </div>
1087 `).join('')}
1088 </div>
1089 </div>
1090 ` : ''}
1091 ${aiKeywords.length ? `
1092 <div class="ozon-desc-masks-group">
1093 <div class="ozon-desc-masks-group-title">КЛЮЧЕВЫЕ СЛОВА</div>
1094 <div class="ozon-desc-masks-grid">
1095 ${aiKeywords.map(keyword => `
1096 <div class="ozon-mask-chip" data-type="ключ" data-mask="${keyword}">
1097 <span>${keyword}</span>
1098 </div>
1099 `).join('')}
1100 </div>
1101 </div>
1102 ` : ''}
1103 </div>
1104 `;
1105
1106 suggestedContainer.style.display = 'block';
1107
1108 suggestedContainer.querySelectorAll('.ozon-mask-chip').forEach(chip => {
1109 chip.addEventListener('click', () => {
1110 chip.classList.toggle('selected');
1111 updateToggleButtonText();
1112 });
1113 });
1114
1115 function updateToggleButtonText() {
1116 const chips = suggestedContainer.querySelectorAll('.ozon-mask-chip');
1117 const allSelected = Array.from(chips).every(c => c.classList.contains('selected'));
1118 const toggleBtn = document.getElementById('ozon-toggle-all-btn');
1119 if (toggleBtn) {
1120 toggleBtn.textContent = allSelected ? 'Снять все' : 'Выбрать все';
1121 }
1122 }
1123
1124 const toggleBtn = document.getElementById('ozon-toggle-all-btn');
1125 if (toggleBtn) {
1126 toggleBtn.addEventListener('click', () => {
1127 const chips = suggestedContainer.querySelectorAll('.ozon-mask-chip');
1128 const allSelected = Array.from(chips).every(c => c.classList.contains('selected'));
1129
1130 chips.forEach(c => {
1131 if (allSelected) {
1132 c.classList.remove('selected');
1133 } else {
1134 c.classList.add('selected');
1135 }
1136 });
1137
1138 updateToggleButtonText();
1139 });
1140 }
1141
1142 showStatus(statusContainer, `AI предложил ${masks.length} масок и ${aiKeywords.length} ключевых слов. Выберите нужные и нажмите "Сгенерировать"`, 'success');
1143
1144 } catch (error) {
1145 console.error('Ozon Description Generator: Ошибка при предложении масок:', error);
1146 showStatus(statusContainer, 'Ошибка при получении предложений: ' + error.message, 'error');
1147 } finally {
1148 suggestBtn.disabled = false;
1149 suggestBtn.textContent = '🔍 Предложить поисковые маски';
1150 }
1151 }
1152
1153 // ============================================
1154 // СБОР ДАННЫХ С АНАЛИТИКИ
1155 // ============================================
1156
1157 async function collectAnalyticsData(keywords, minusWords) {
1158 console.log('Ozon Description Generator: Начало сбора данных с аналитики');
1159
1160 // Проверяем, не идет ли уже сбор данных
1161 const currentStatus = await GM.getValue('ozon_collection_status', 'none');
1162 if (currentStatus === 'pending') {
1163 console.log('Ozon Description Generator: Обнаружен статус pending, сбрасываем и начинаем заново');
1164 await GM.setValue('ozon_collection_status', 'none');
1165 }
1166
1167 await GM.setValue('ozon_keywords_to_process', JSON.stringify(keywords));
1168 await GM.setValue('ozon_minus_words', JSON.stringify(minusWords));
1169 await GM.setValue('ozon_analytics_data', JSON.stringify([]));
1170 await GM.setValue('ozon_collection_status', 'pending');
1171
1172 const analyticsUrl = 'https://seller.ozon.ru/app/analytics/what-to-sell/all-queries';
1173 console.log('Ozon Description Generator: Открываем страницу аналитики');
1174 await GM.openInTab(analyticsUrl, false);
1175
1176 console.log('Ozon Description Generator: Открыта страница аналитики, ожидание сбора данных...');
1177
1178 const maxWaitTime = 300000;
1179 const checkInterval = 2000;
1180 let waitedTime = 0;
1181
1182 while (waitedTime < maxWaitTime) {
1183 await new Promise(resolve => setTimeout(resolve, checkInterval));
1184 waitedTime += checkInterval;
1185
1186 const status = await GM.getValue('ozon_collection_status', 'pending');
1187
1188 if (status === 'completed') {
1189 const analyticsDataStr = await GM.getValue('ozon_analytics_data', '[]');
1190 const analyticsData = JSON.parse(analyticsDataStr);
1191 console.log('Ozon Description Generator: Данные успешно собраны');
1192 return analyticsData;
1193 } else if (status === 'error') {
1194 console.error('Ozon Description Generator: Ошибка при сборе данных');
1195 return [];
1196 }
1197 }
1198
1199 console.error('Ozon Description Generator: Превышено время ожидания сбора данных');
1200 return [];
1201 }
1202
1203 // ============================================
1204 // АВТОМАТИЧЕСКИЙ СБОР НА СТРАНИЦЕ АНАЛИТИКИ
1205 // ============================================
1206
1207 async function autoCollectOnAnalyticsPage() {
1208 if (!window.location.href.includes('seller.ozon.ru/app/analytics/what-to-sell/all-queries')) {
1209 return;
1210 }
1211
1212 console.log('Ozon Description Generator: Обнаружена страница аналитики');
1213
1214 const status = await GM.getValue('ozon_collection_status', 'none');
1215 if (status !== 'pending') {
1216 return;
1217 }
1218
1219 console.log('Ozon Description Generator: Начинаем автоматический сбор данных');
1220
1221 try {
1222 const keywordsStr = await GM.getValue('ozon_keywords_to_process', '[]');
1223 const minusWordsStr = await GM.getValue('ozon_minus_words', '[]');
1224 const keywords = JSON.parse(keywordsStr);
1225 const minusWords = JSON.parse(minusWordsStr);
1226
1227 const analyticsData = [];
1228
1229 await new Promise(resolve => setTimeout(resolve, 3000));
1230
1231 // Выбираем период 28 дней
1232 try {
1233 const periodButton = document.querySelector('button[data-active="true"]');
1234 if (periodButton && periodButton.textContent.includes('7 дней')) {
1235 console.log('Ozon Description Generator: Меняем период на 28 дней');
1236 periodButton.click();
1237 await new Promise(resolve => setTimeout(resolve, 1000));
1238
1239 // Ищем кнопку 28 дней в выпадающем меню
1240 const buttons = document.querySelectorAll('button');
1241 const days28Button = Array.from(buttons).find(btn =>
1242 btn.textContent && btn.textContent.trim().includes('28 дней')
1243 );
1244
1245 if (days28Button) {
1246 days28Button.click();
1247 await new Promise(resolve => setTimeout(resolve, 2000));
1248 console.log('Ozon Description Generator: Период "28 дней" выбран');
1249 }
1250 }
1251 } catch (e) {
1252 console.error('Ozon Description Generator: Ошибка при выборе периода:', e);
1253 }
1254
1255 for (const keyword of keywords) {
1256 console.log(`Ozon Description Generator: Обработка ключевого слова: ${keyword}`);
1257
1258 try {
1259 const searchInput = document.querySelector('input[placeholder="Поисковый запрос"]');
1260 if (!searchInput) {
1261 console.error('Ozon Description Generator: Поле поиска не найдено');
1262 continue;
1263 }
1264
1265 searchInput.value = '';
1266 searchInput.focus();
1267 searchInput.value = keyword;
1268 searchInput.dispatchEvent(new Event('input', { bubbles: true }));
1269 searchInput.dispatchEvent(new Event('change', { bubbles: true }));
1270
1271 await new Promise(resolve => setTimeout(resolve, 5000));
1272
1273 const rows = document.querySelectorAll('table tbody tr');
1274 const keywordData = {
1275 keyword: keyword,
1276 queries: []
1277 };
1278
1279 console.log(`Ozon Description Generator: Найдено строк в таблице: ${rows.length}`);
1280
1281 rows.forEach(row => {
1282 const cells = row.querySelectorAll('td');
1283 if (cells.length >= 2) {
1284 const query = cells[0]?.textContent?.trim();
1285 const popularityText = cells[1]?.textContent?.trim();
1286
1287 if (query && popularityText) {
1288 const popularity = parseInt(popularityText.replace(/\s+/g, ''));
1289 const queryLower = query.toLowerCase();
1290
1291 const hasMinusWord = minusWords.some(minusWord =>
1292 queryLower.includes(minusWord.toLowerCase())
1293 );
1294
1295 if (hasMinusWord) {
1296 console.log(`Ozon Description Generator: Исключен запрос "${query}" (содержит минус-слово)`);
1297 return;
1298 }
1299
1300 keywordData.queries.push({
1301 query,
1302 popularity
1303 });
1304 }
1305 }
1306 });
1307
1308 analyticsData.push(keywordData);
1309 console.log(`Ozon Description Generator: Собрано ${keywordData.queries.length} запросов для "${keyword}"`);
1310
1311 } catch (error) {
1312 console.error(`Ozon Description Generator: Ошибка при обработке ключевого слова "${keyword}":`, error);
1313 }
1314 }
1315
1316 await GM.setValue('ozon_analytics_data', JSON.stringify(analyticsData));
1317 await GM.setValue('ozon_collection_status', 'completed');
1318
1319 console.log('Ozon Description Generator: Сбор данных завершен, закрываем вкладку через 1 секунду');
1320
1321 setTimeout(() => {
1322 console.log('Ozon Description Generator: Закрываем вкладку аналитики');
1323 window.close();
1324 }, 1000);
1325
1326 } catch (error) {
1327 console.error('Ozon Description Generator: Ошибка при автоматическом сборе данных:', error);
1328 await GM.setValue('ozon_collection_status', 'error');
1329
1330 // Закрываем вкладку даже при ошибке
1331 setTimeout(() => {
1332 window.close();
1333 }, 2000);
1334 }
1335 }
1336
1337 // ============================================
1338 // ГЕНЕРАЦИЯ ОПИСАНИЯ
1339 // ============================================
1340
1341 async function generateDescription(modal, skipDataCollection = false) {
1342 console.log('Ozon Description Generator: Генерация описания');
1343
1344 const keywordsInput = document.getElementById('ozon-keywords-input');
1345 const minusWordsInput = document.getElementById('ozon-minus-words-input');
1346 const generateBtn = document.getElementById('ozon-generate-btn');
1347 const regenerateBtn = document.getElementById('ozon-regenerate-btn');
1348 const insertBtn = document.getElementById('ozon-insert-btn');
1349 const resultContainer = document.getElementById('ozon-desc-result-container');
1350 const resultDiv = document.getElementById('ozon-desc-result');
1351 const charCountDiv = document.getElementById('ozon-char-count');
1352 const statusContainer = document.getElementById('ozon-desc-status-container');
1353 const statsContainer = document.getElementById('ozon-desc-stats-container');
1354
1355 let keywordsText = keywordsInput.value.trim();
1356
1357 const selectedSuggestions = Array.from(document.querySelectorAll('.ozon-mask-chip.selected'))
1358 .map(chip => chip.dataset.mask);
1359
1360 if (selectedSuggestions.length > 0) {
1361 const existingKeywords = keywordsText.split('\n').map(k => k.trim()).filter(k => k);
1362 const allKeywords = [...new Set([...existingKeywords, ...selectedSuggestions])];
1363 keywordsText = allKeywords.join('\n');
1364 console.log('Ozon Description Generator: Добавлены маски/ключи:', selectedSuggestions);
1365 }
1366
1367 const keywords = keywordsText.split('\n').map(k => k.trim()).filter(k => k);
1368 const minusWords = minusWordsInput.value.split('\n').map(k => k.trim()).filter(k => k);
1369
1370 if (keywords.length === 0) {
1371 showStatus(statusContainer, 'Пожалуйста, введите хотя бы одно ключевое слово', 'error');
1372 return;
1373 }
1374
1375 const productInfo = await getProductInfo();
1376 const currentSKU = productInfo.sku;
1377
1378 if (currentSKU) {
1379 await GM.setValue(`ozon_product_${currentSKU}_keywords`, keywords.join('\n'));
1380 await GM.setValue(`ozon_product_${currentSKU}_minus_words`, minusWords.join('\n'));
1381 console.log('Ozon Description Generator: Сохранены ключевые слова и минус-слова для товара', currentSKU);
1382 }
1383
1384 generateBtn.disabled = true;
1385 regenerateBtn.disabled = true;
1386
1387 try {
1388 let analyticsData = [];
1389 let queryPopularity = {};
1390
1391 if (!skipDataCollection) {
1392 showStatus(statusContainer, 'Сбор данных из аналитики...', 'info');
1393
1394 analyticsData = await collectAnalyticsData(keywords, minusWords);
1395
1396 if (analyticsData.length === 0) {
1397 showStatus(statusContainer, 'Не удалось собрать данные из аналитики', 'error');
1398 return;
1399 }
1400 } else {
1401 const analyticsDataStr = await GM.getValue('ozon_analytics_data', '[]');
1402 analyticsData = JSON.parse(analyticsDataStr);
1403
1404 if (analyticsData.length === 0) {
1405 showStatus(statusContainer, 'Нет сохраненных данных. Пожалуйста, сначала соберите данные.', 'error');
1406 return;
1407 }
1408 }
1409
1410 const allQueries = [];
1411 analyticsData.forEach(data => {
1412 data.queries.forEach(q => {
1413 allQueries.push(q.query);
1414 queryPopularity[q.query.toLowerCase()] = q.popularity;
1415 });
1416 });
1417
1418 console.log(`Ozon Description Generator: Всего запросов для генерации: ${allQueries.length}`);
1419
1420 if (allQueries.length === 0) {
1421 showStatus(statusContainer, 'Не найдено подходящих запросов для генерации', 'error');
1422 return;
1423 }
1424
1425 console.log('Ozon Description Generator: Применяем минус-слова перед генерацией');
1426 console.log('Ozon Description Generator: Минус-слова:', minusWords);
1427 console.log('Ozon Description Generator: Запросов до фильтрации:', allQueries.length);
1428
1429 const filteredQueries = allQueries.filter(query => {
1430 const queryLower = query.toLowerCase();
1431 const hasMinusWord = minusWords.some(minusWord =>
1432 queryLower.includes(minusWord.toLowerCase())
1433 );
1434
1435 if (hasMinusWord) {
1436 console.log(`Ozon Description Generator: Исключен запрос "${query}" (содержит минус-слово)`);
1437 }
1438
1439 return !hasMinusWord;
1440 });
1441
1442 console.log('Ozon Description Generator: Запросов после фильтрации:', filteredQueries.length);
1443
1444 if (filteredQueries.length === 0) {
1445 showStatus(statusContainer, 'Все запросы отфильтрованы минус-словами. Попробуйте уменьшить количество минус-слов.', 'error');
1446 return;
1447 }
1448
1449 showStatus(statusContainer, 'AI генерирует описание...', 'info');
1450
1451 const descriptionPrompt = generateDescriptionPrompt(productInfo, keywords, filteredQueries, queryPopularity);
1452 const description = await RM.aiCall(descriptionPrompt);
1453
1454 await GM.setValue('ozon_generated_description', description);
1455 await GM.setValue('ozon_query_popularity', JSON.stringify(queryPopularity));
1456
1457 resultDiv.textContent = description;
1458 resultContainer.style.display = 'block';
1459
1460 const charCount = description.length;
1461 charCountDiv.textContent = `Символов: ${charCount}`;
1462 charCountDiv.className = 'ozon-desc-char-count success';
1463
1464 const analysis = await analyzeUsedKeywords(description, queryPopularity);
1465 const usagePercent = Math.round(analysis.usedQueries.length / analysis.totalQueriesAvailable * 100);
1466
1467 if (!statsContainer) {
1468 const newStatsContainer = document.createElement('div');
1469 newStatsContainer.id = 'ozon-desc-stats-container';
1470 charCountDiv.parentElement.insertBefore(newStatsContainer, charCountDiv.nextSibling);
1471 }
1472
1473 document.getElementById('ozon-desc-stats-container').innerHTML = `
1474 <div class="ozon-desc-stats">
1475 <div class="ozon-desc-stats-row">
1476 <span><strong>Использовано запросов:</strong></span>
1477 <span>${analysis.usedQueries.length} из ${analysis.totalQueriesAvailable} (${usagePercent}%)</span>
1478 </div>
1479 <div class="ozon-desc-stats-row">
1480 <span><strong>Общая частотность:</strong></span>
1481 <span>${formatNumber(analysis.totalPopularity)}</span>
1482 </div>
1483 </div>
1484 `;
1485
1486 generateBtn.style.display = 'none';
1487 regenerateBtn.style.display = 'inline-block';
1488 insertBtn.style.display = 'inline-block';
1489
1490 showStatus(statusContainer, '✅ Описание успешно сгенерировано! <span class="ozon-desc-usage-link" id="ozon-show-analytics-link">Показать аналитику использования запросов</span>', 'success');
1491
1492 setTimeout(() => {
1493 const analyticsLink = document.getElementById('ozon-show-analytics-link');
1494 if (analyticsLink) {
1495 analyticsLink.addEventListener('click', () => {
1496 showUsageAnalytics();
1497 });
1498 }
1499 }, 100);
1500
1501 } catch (error) {
1502 console.error('Ozon Description Generator: Ошибка при генерации описания:', error);
1503 showStatus(statusContainer, 'Ошибка при генерации: ' + error.message, 'error');
1504 } finally {
1505 generateBtn.disabled = false;
1506 regenerateBtn.disabled = false;
1507 }
1508 }
1509
1510 // ============================================
1511 // АНАЛИЗ ИСПОЛЬЗОВАННЫХ КЛЮЧЕВЫХ СЛОВ
1512 // ============================================
1513
1514 async function analyzeUsedKeywords(description, queryPopularityParam = null) {
1515 console.log('Ozon Description Generator: Анализ использованных ключевых слов');
1516
1517 const analyticsDataStr = await GM.getValue('ozon_analytics_data', '[]');
1518 const analyticsData = JSON.parse(analyticsDataStr);
1519
1520 const allQueries = [];
1521 let queryPopularity = queryPopularityParam || {};
1522
1523 if (!queryPopularityParam) {
1524 const savedPopularity = await GM.getValue('ozon_query_popularity', '{}');
1525 queryPopularity = JSON.parse(savedPopularity);
1526 }
1527
1528 analyticsData.forEach(data => {
1529 data.queries.forEach(q => {
1530 allQueries.push(q.query);
1531 if (!queryPopularity[q.query.toLowerCase()]) {
1532 queryPopularity[q.query.toLowerCase()] = q.popularity;
1533 }
1534 });
1535 });
1536
1537 const descriptionLower = description.toLowerCase();
1538 const usedQueries = [];
1539 const unusedQueries = [];
1540 let totalPopularity = 0;
1541
1542 allQueries.forEach(query => {
1543 if (descriptionLower.includes(query.toLowerCase())) {
1544 usedQueries.push(query);
1545 totalPopularity += queryPopularity[query.toLowerCase()] || 0;
1546 } else {
1547 unusedQueries.push(query);
1548 }
1549 });
1550
1551 console.log(`Ozon Description Generator: Использовано ${usedQueries.length} из ${allQueries.length} запросов`);
1552
1553 return {
1554 usedQueries,
1555 unusedQueries,
1556 totalQueriesAvailable: allQueries.length,
1557 totalPopularity,
1558 queryPopularity
1559 };
1560 }
1561
1562 // ============================================
1563 // ПОКАЗ АНАЛИТИКИ ИСПОЛЬЗОВАНИЯ ЗАПРОСОВ
1564 // ============================================
1565
1566 async function showUsageAnalytics() {
1567 console.log('Ozon Description Generator: Показ аналитики использования');
1568
1569 const description = await GM.getValue('ozon_generated_description', '');
1570 if (!description) {
1571 alert('Описание не найдено');
1572 return;
1573 }
1574
1575 const analysis = await analyzeUsedKeywords(description);
1576
1577 const minusWordsStr = await GM.getValue('ozon_analytics_minus_words', '[]');
1578 const minusWords = JSON.parse(minusWordsStr);
1579
1580 let usedQueries = [...analysis.usedQueries];
1581 let unusedQueries = [...analysis.unusedQueries];
1582 let currentMinusWords = [...minusWords];
1583 let searchQuery = '';
1584
1585 function renderModal() {
1586 const usedContainer = document.getElementById('ozon-used-queries-container');
1587 const unusedContainer = document.getElementById('ozon-unused-queries-container');
1588 const usedScrollTop = usedContainer ? usedContainer.scrollTop : 0;
1589 const unusedScrollTop = unusedContainer ? unusedContainer.scrollTop : 0;
1590
1591 const filteredUsed = usedQueries.filter(q =>
1592 q.toLowerCase().includes(searchQuery.toLowerCase())
1593 );
1594 const filteredUnused = unusedQueries.filter(q =>
1595 q.toLowerCase().includes(searchQuery.toLowerCase())
1596 );
1597
1598 const analyticsModal = document.querySelector('.ozon-desc-analytics-modal');
1599 if (!analyticsModal) return;
1600
1601 analyticsModal.innerHTML = `
1602 <div class="ozon-desc-analytics-content">
1603 <div class="ozon-desc-modal-header">📊 Аналитика использования запросов</div>
1604
1605 ${currentMinusWords.length > 0 ? `
1606 <div class="ozon-desc-minus-words-section">
1607 <div class="ozon-desc-minus-words-header">Минус-слова (клик для удаления):</div>
1608 <div class="ozon-desc-minus-words-list">
1609 ${currentMinusWords.map(word => `
1610 <div class="ozon-desc-minus-word-chip" data-word="${word}">
1611 ${word}
1612 <span class="ozon-desc-minus-word-remove">×</span>
1613 </div>
1614 `).join('')}
1615 </div>
1616 </div>
1617 ` : ''}
1618
1619 <input type="text" class="ozon-desc-search-input" id="ozon-analytics-search" placeholder="🔍 Поиск по запросам..." value="${searchQuery}">
1620
1621 <div style="margin-bottom: 16px; display: flex; gap: 20px; flex-wrap: wrap;">
1622 <div><strong>Использовано:</strong> ${filteredUsed.length} из ${analysis.totalQueriesAvailable} (${Math.round(filteredUsed.length / analysis.totalQueriesAvailable * 100)}%)</div>
1623 <div><strong>Общая частотность:</strong> ${formatNumber(analysis.totalPopularity)}</div>
1624 ${searchQuery ? `<div><strong>Найдено:</strong> ${filteredUsed.length + filteredUnused.length}</div>` : ''}
1625 </div>
1626
1627 <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px;">
1628 <div>
1629 <div style="margin-bottom: 12px; font-weight: 600; color: #065f46;">✅ Использованные (${filteredUsed.length}):</div>
1630 <div style="max-height: 350px; overflow-y: auto;" id="ozon-used-queries-container">
1631 ${filteredUsed.length > 0 ? filteredUsed.map(query => `
1632 <div class="ozon-desc-query-item used">
1633 <span class="ozon-desc-query-text">${highlightWords(query, currentMinusWords, true, 'used')}</span>
1634 <div style="display: flex; align-items: center; gap: 8px;">
1635 <span class="ozon-desc-query-popularity">${formatNumber(analysis.queryPopularity[query.toLowerCase()] || 0)}</span>
1636 <span class="ozon-desc-query-exclude" data-query="${query}" style="cursor: pointer; font-size: 18px; color: #991b1b; font-weight: bold;" title="Исключить запрос">×</span>
1637 </div>
1638 </div>
1639 `).join('') : '<div style="padding: 12px; color: #6b7280; text-align: center;">Нет результатов</div>'}
1640 </div>
1641 </div>
1642
1643 <div>
1644 <div style="margin-bottom: 12px; font-weight: 600; color: #6b7280;">⬜ Неиспользованные (${filteredUnused.length}):</div>
1645 <div style="max-height: 350px; overflow-y: auto;" id="ozon-unused-queries-container">
1646 ${filteredUnused.length > 0 ? filteredUnused.map(query => `
1647 <div class="ozon-desc-query-item unused">
1648 <span class="ozon-desc-query-text">${highlightWords(query, currentMinusWords, true, 'unused')}</span>
1649 <div style="display: flex; align-items: center; gap: 8px;">
1650 <span class="ozon-desc-query-popularity">${formatNumber(analysis.queryPopularity[query.toLowerCase()] || 0)}</span>
1651 <span class="ozon-desc-query-include" data-query="${query}" style="cursor: pointer; font-size: 18px; color: #059669; font-weight: bold;" title="Включить запрос">+</span>
1652 </div>
1653 </div>
1654 `).join('') : '<div style="padding: 12px; color: #6b7280; text-align: center;">Нет результатов</div>'}
1655 </div>
1656 </div>
1657 </div>
1658
1659 <div class="ozon-desc-buttons">
1660 <button class="ozon-desc-btn ozon-desc-btn-secondary" id="ozon-close-analytics-btn">Закрыть</button>
1661 <button class="ozon-desc-btn ozon-desc-btn-primary" id="ozon-regenerate-with-exclusions-btn">🔄 Перегенерировать с изменениями</button>
1662 </div>
1663 </div>
1664 `;
1665
1666 setTimeout(() => {
1667 const newUsedContainer = document.getElementById('ozon-used-queries-container');
1668 const newUnusedContainer = document.getElementById('ozon-unused-queries-container');
1669 if (newUsedContainer) newUsedContainer.scrollTop = usedScrollTop;
1670 if (newUnusedContainer) newUnusedContainer.scrollTop = unusedScrollTop;
1671 }, 0);
1672
1673 attachEventHandlers();
1674 }
1675
1676 function highlightWords(text, words, clickable = false, type = 'unused') {
1677 if (!clickable) return text;
1678
1679 const textWords = text.split(/\s+/);
1680 return textWords.map(word => {
1681 const cleanWord = word.toLowerCase().replace(/[.,!?;:]/g, '');
1682 return `<span class="ozon-desc-query-word" data-word="${cleanWord}" data-type="${type}">${word}</span>`;
1683 }).join(' ');
1684 }
1685
1686 function removeQueriesWithMinusWord(minusWord) {
1687 const minusWordLower = minusWord.toLowerCase();
1688
1689 usedQueries = usedQueries.filter(query =>
1690 !query.toLowerCase().includes(minusWordLower)
1691 );
1692
1693 unusedQueries = unusedQueries.filter(query =>
1694 !query.toLowerCase().includes(minusWordLower)
1695 );
1696 }
1697
1698 function attachEventHandlers() {
1699 const analyticsModal = document.querySelector('.ozon-desc-analytics-modal');
1700 if (!analyticsModal) return;
1701
1702 const searchInput = document.getElementById('ozon-analytics-search');
1703 if (searchInput) {
1704 searchInput.addEventListener('input', (e) => {
1705 searchQuery = e.target.value;
1706 const cursorPosition = e.target.selectionStart;
1707 renderModal();
1708 setTimeout(() => {
1709 const newSearchInput = document.getElementById('ozon-analytics-search');
1710 if (newSearchInput) {
1711 newSearchInput.focus();
1712 newSearchInput.setSelectionRange(cursorPosition, cursorPosition);
1713 }
1714 }, 0);
1715 });
1716 }
1717
1718 analyticsModal.addEventListener('click', (e) => {
1719 if (e.target === analyticsModal) {
1720 analyticsModal.remove();
1721 }
1722 });
1723
1724 const closeBtn = document.getElementById('ozon-close-analytics-btn');
1725 if (closeBtn) {
1726 closeBtn.addEventListener('click', () => {
1727 analyticsModal.remove();
1728 });
1729 }
1730
1731 const regenerateBtn = document.getElementById('ozon-regenerate-with-exclusions-btn');
1732 if (regenerateBtn) {
1733 regenerateBtn.addEventListener('click', async () => {
1734 await GM.setValue('ozon_analytics_minus_words', JSON.stringify(currentMinusWords));
1735
1736 const analyticsDataStr = await GM.getValue('ozon_analytics_data', '[]');
1737 const analyticsData = JSON.parse(analyticsDataStr);
1738
1739 const updatedAnalyticsData = analyticsData.map(data => {
1740 return {
1741 keyword: data.keyword,
1742 queries: data.queries.filter(q => usedQueries.includes(q.query))
1743 };
1744 });
1745
1746 await GM.setValue('ozon_analytics_data', JSON.stringify(updatedAnalyticsData));
1747
1748 console.log('Ozon Description Generator: Обновлены данные аналитики для перегенерации');
1749 console.log('Использованные запросы:', usedQueries.length);
1750 console.log('Минус-слова:', currentMinusWords);
1751
1752 analyticsModal.remove();
1753 await regenerateWithExclusions();
1754 });
1755 }
1756
1757 analyticsModal.querySelectorAll('.ozon-desc-minus-word-chip').forEach(chip => {
1758 chip.addEventListener('click', () => {
1759 const word = chip.dataset.word;
1760 currentMinusWords = currentMinusWords.filter(w => w !== word);
1761 console.log(`Ozon Description Generator: Минус-слово "${word}" удалено`);
1762 renderModal();
1763 });
1764 });
1765
1766 analyticsModal.querySelectorAll('.ozon-desc-query-exclude').forEach(excludeBtn => {
1767 excludeBtn.addEventListener('click', () => {
1768 const query = excludeBtn.dataset.query;
1769 console.log(`Ozon Description Generator: Перемещаем запрос "${query}" в неиспользованные`);
1770
1771 usedQueries = usedQueries.filter(q => q !== query);
1772 if (!unusedQueries.includes(query)) {
1773 unusedQueries.push(query);
1774 }
1775
1776 renderModal();
1777 });
1778 });
1779
1780 analyticsModal.querySelectorAll('.ozon-desc-query-include').forEach(includeBtn => {
1781 includeBtn.addEventListener('click', () => {
1782 const query = includeBtn.dataset.query;
1783 console.log(`Ozon Description Generator: Перемещаем запрос "${query}" в использованные`);
1784
1785 unusedQueries = unusedQueries.filter(q => q !== query);
1786 if (!usedQueries.includes(query)) {
1787 usedQueries.push(query);
1788 }
1789
1790 const queryLower = query.toLowerCase();
1791 currentMinusWords = currentMinusWords.filter(w => w !== queryLower);
1792
1793 renderModal();
1794 });
1795 });
1796
1797 analyticsModal.querySelectorAll('.ozon-desc-query-word').forEach(wordSpan => {
1798 wordSpan.addEventListener('click', () => {
1799 const word = wordSpan.dataset.word;
1800
1801 console.log(`Ozon Description Generator: Добавляем минус-слово "${word}"`);
1802
1803 if (!currentMinusWords.includes(word)) {
1804 currentMinusWords.push(word);
1805 }
1806
1807 removeQueriesWithMinusWord(word);
1808
1809 renderModal();
1810 });
1811 });
1812 }
1813
1814 const analyticsModal = document.createElement('div');
1815 analyticsModal.className = 'ozon-desc-analytics-modal';
1816 document.body.appendChild(analyticsModal);
1817
1818 renderModal();
1819 }
1820
1821 // ============================================
1822 // ПЕРЕГЕНЕРАЦИЯ С ИСКЛЮЧЕНИЯМИ
1823 // ============================================
1824
1825 async function regenerateWithExclusions() {
1826 console.log('Ozon Description Generator: Перегенерация с исключениями');
1827
1828 const analyticsMinusWordsStr = await GM.getValue('ozon_analytics_minus_words', '[]');
1829 const analyticsMinusWords = JSON.parse(analyticsMinusWordsStr);
1830
1831 console.log('Ozon Description Generator: Минус-слова из аналитики для перегенерации:', analyticsMinusWords);
1832
1833 const existingModal = document.querySelector('.ozon-desc-modal');
1834 if (existingModal) {
1835 console.log('Ozon Description Generator: Модальное окно уже открыто');
1836
1837 const minusWordsInput = document.getElementById('ozon-minus-words-input');
1838 if (minusWordsInput) {
1839 const currentMinusWords = minusWordsInput.value.split('\n').map(k => k.trim()).filter(k => k);
1840 const allMinusWords = [...new Set([...currentMinusWords, ...analyticsMinusWords])];
1841 minusWordsInput.value = allMinusWords.join('\n');
1842
1843 console.log('Ozon Description Generator: Обновлены минус-слова в модальном окне:', allMinusWords);
1844 }
1845
1846 generateDescription(existingModal, true);
1847 return;
1848 }
1849
1850 await openModal();
1851 await new Promise(resolve => setTimeout(resolve, 100));
1852
1853 const minusWordsInput = document.getElementById('ozon-minus-words-input');
1854 if (minusWordsInput) {
1855 const currentMinusWords = minusWordsInput.value.split('\n').map(k => k.trim()).filter(k => k);
1856 const allMinusWords = [...new Set([...currentMinusWords, ...analyticsMinusWords])];
1857 minusWordsInput.value = allMinusWords.join('\n');
1858
1859 console.log('Ozon Description Generator: Обновлены минус-слова в новом модальном окне:', allMinusWords);
1860 }
1861
1862 const newModal = document.querySelector('.ozon-desc-modal');
1863 if (newModal) {
1864 generateDescription(newModal, true);
1865 }
1866 }
1867
1868 // ============================================
1869 // ОТПРАВКА В RICH-КОНТЕНТ
1870 // ============================================
1871
1872 async function insertDescription(modal) {
1873 console.log('Ozon Description Generator: Вставка описания в поле');
1874
1875 const description = await GM.getValue('ozon_generated_description', '');
1876
1877 if (!description) {
1878 alert('Описание не найдено. Пожалуйста, сначала сгенерируйте описание.');
1879 return;
1880 }
1881
1882 const proseMirrorDiv = document.querySelector('[id="attribute#4191"] .ProseMirror');
1883
1884 if (!proseMirrorDiv) {
1885 alert('Не удалось найти поле описания. Убедитесь, что вы находитесь на странице редактирования товара.');
1886 return;
1887 }
1888
1889 // Вставляем описание с сохранением пустых строк между абзацами
1890 const paragraphs = description.split('\n\n').filter(p => p.trim());
1891 const htmlContent = paragraphs.map(p => `<p>${p.replace(/\n/g, '<br>')}</p>`).join('<p><br></p>');
1892
1893 proseMirrorDiv.innerHTML = htmlContent;
1894 proseMirrorDiv.dispatchEvent(new Event('input', { bubbles: true }));
1895 proseMirrorDiv.dispatchEvent(new Event('change', { bubbles: true }));
1896
1897 console.log('Ozon Description Generator: Описание вставлено');
1898
1899 modal.remove();
1900
1901 alert('✅ Описание успешно вставлено!');
1902 }
1903
1904 async function sendToRichContent() {
1905 console.log('Ozon Description Generator: Отправка в rich-контент');
1906
1907 try {
1908 // Получаем описание из ProseMirror редактора
1909 const proseMirrorDiv = document.querySelector('[id="attribute#4191"] .ProseMirror');
1910
1911 if (!proseMirrorDiv) {
1912 alert('Не удалось найти поле описания. Убедитесь, что вы на странице редактирования товара.');
1913 return;
1914 }
1915
1916 const description = proseMirrorDiv.textContent.trim();
1917
1918 if (!description) {
1919 alert('Описание пустое. Пожалуйста, сначала сгенерируйте и вставьте описание.');
1920 return;
1921 }
1922
1923 console.log('Ozon Description Generator: Описание скопировано, переходим на страницу медиа');
1924
1925 // Сохраняем описание для использования на странице медиа
1926 await GM.setValue('ozon_description_for_rich', description);
1927
1928 // Находим кнопку перехода на вкладку "Медиа"
1929 const mediaTabButton = Array.from(document.querySelectorAll('button.c9s110-a3.c9s110-a0.c9s110-a.c9s110-a4.c9s110-a6')).find(btn =>
1930 btn.textContent && btn.textContent.includes('Медиа')
1931 );
1932
1933 if (!mediaTabButton) {
1934 alert('Не удалось найти кнопку перехода на вкладку "Медиа"');
1935 return;
1936 }
1937
1938 console.log('Ozon Description Generator: Нажимаем кнопку "Медиа"');
1939 mediaTabButton.click();
1940
1941 // Ждем загрузки страницы медиа и вставляем rich-контент
1942 setTimeout(async () => {
1943 await insertRichContent();
1944 }, 1000);
1945
1946 } catch (error) {
1947 console.error('Ozon Description Generator: Ошибка при отправке в rich-контент:', error);
1948 alert('Ошибка при отправке в rich-контент: ' + error.message);
1949 }
1950 }
1951
1952 async function insertRichContent() {
1953 console.log('Ozon Description Generator: Вставка rich-контента');
1954
1955 try {
1956 const description = await GM.getValue('ozon_description_for_rich', '');
1957
1958 if (!description) {
1959 console.error('Ozon Description Generator: Описание не найдено в хранилище');
1960 return;
1961 }
1962
1963 // Ищем поле Rich-контента JSON
1964 const richContentInput = document.querySelector('textarea[id^="baseInput___"]');
1965
1966 if (!richContentInput) {
1967 alert('Не удалось найти поле Rich-контента. Убедитесь, что вы на странице редактирования товара.');
1968 return;
1969 }
1970
1971 // Разбиваем описание на абзацы с сохранением пустых строк
1972 const paragraphs = description.split('\n\n').filter(p => p.trim());
1973
1974 // Создаем JSON для rich-контента с абзацами
1975 const richContentJSON = {
1976 'content': [
1977 {
1978 'widgetName': 'raTextBlock',
1979 'title': {
1980 'content': [],
1981 'size': 'size5',
1982 'color': 'color1'
1983 },
1984 'theme': 'default',
1985 'padding': 'type2',
1986 'gapSize': 'm',
1987 'text': {
1988 'size': 'size2',
1989 'align': 'left',
1990 'color': 'color1',
1991 'content': paragraphs
1992 }
1993 }
1994 ],
1995 'version': 0.3
1996 };
1997
1998 // Вставляем JSON в поле
1999 richContentInput.value = JSON.stringify(richContentJSON, null, 2);
2000 richContentInput.dispatchEvent(new Event('input', { bubbles: true }));
2001 richContentInput.dispatchEvent(new Event('change', { bubbles: true }));
2002
2003 console.log('Ozon Description Generator: Rich-контент успешно вставлен');
2004
2005 // Очищаем сохраненное описание
2006 await GM.setValue('ozon_description_for_rich', '');
2007
2008 // Проверяем, идет ли автогенерация
2009 const expectedKeys = Object.keys(localStorage).filter(key => key.startsWith('wbAutoExpected_'));
2010 const isAutogen = expectedKeys.length > 0;
2011
2012 // Показываем alert только если это НЕ автогенерация
2013 if (!isAutogen) {
2014 alert('✅ Описание успешно отправлено в Rich-контент!');
2015 }
2016
2017 } catch (error) {
2018 console.error('Ozon Description Generator: Ошибка при вставке rich-контента:', error);
2019 alert('Ошибка при вставке rich-контента: ' + error.message);
2020 }
2021 }
2022
2023 // ============================================
2024 // АВТОГЕНЕРАЦИЯ ПО ВСЕМ ТОВАРАМ
2025 // ============================================
2026
2027 // Вспомогательные функции для работы с localStorage
2028 function generateCheckId() {
2029 return Date.now() + '_' + Math.random().toString(36).substr(2, 9);
2030 }
2031
2032 function setAutoCheck(nmID, title, checkId) {
2033 const data = {
2034 nmID,
2035 title,
2036 checkId,
2037 timestamp: Date.now()
2038 };
2039 localStorage.setItem('wbAutoCheck', JSON.stringify(data));
2040 localStorage.setItem(`wbAutoExpected_${checkId}`, 'true');
2041 console.log('Ozon Description Generator: Сохранен autoCheck:', data);
2042 }
2043
2044 function getAutoCheck() {
2045 const data = localStorage.getItem('wbAutoCheck');
2046 return data ? JSON.parse(data) : null;
2047 }
2048
2049 function clearAutoCheck(checkId) {
2050 localStorage.removeItem('wbAutoCheck');
2051 if (checkId) {
2052 localStorage.removeItem(`wbAutoExpected_${checkId}`);
2053 localStorage.removeItem('wbAutoResult');
2054 }
2055 console.log('Ozon Description Generator: Очищен autoCheck');
2056 }
2057
2058 function setAutoResult(checkId, success, error = null) {
2059 const result = {
2060 checkId,
2061 success,
2062 error,
2063 timestamp: Date.now()
2064 };
2065 localStorage.setItem('wbAutoResult', JSON.stringify(result));
2066 console.log('Ozon Description Generator: Сохранен результат:', result);
2067 }
2068
2069 function getAutoResult() {
2070 const data = localStorage.getItem('wbAutoResult');
2071 return data ? JSON.parse(data) : null;
2072 }
2073
2074 function isCheckIdExpected(checkId) {
2075 return localStorage.getItem(`wbAutoExpected_${checkId}`) === 'true';
2076 }
2077
2078 function isTimestampFresh(timestamp, maxAgeMs = 180000) { // 3 минуты
2079 return (Date.now() - timestamp) < maxAgeMs;
2080 }
2081
2082 // Создание кнопки автогенерации на странице списка товаров
2083 function createAutogenButton() {
2084 if (document.querySelector('.ozon-autogen-btn')) {
2085 return;
2086 }
2087
2088 // Ищем контейнер с кнопками действий
2089 const actionsContainer = document.querySelector('.cs5110-a5 .cs5110-b0 div');
2090
2091 if (!actionsContainer) {
2092 return;
2093 }
2094
2095 const autogenButton = document.createElement('button');
2096 autogenButton.className = 'ozon-autogen-btn';
2097 autogenButton.textContent = '🤖 Автогенерация';
2098 autogenButton.type = 'button';
2099 autogenButton.setAttribute('data-ozon-autogen', 'true');
2100 autogenButton.addEventListener('click', openAutogenModal);
2101
2102 actionsContainer.insertBefore(autogenButton, actionsContainer.firstChild);
2103 console.log('Ozon Description Generator: Кнопка автогенерации добавлена');
2104 }
2105
2106 // Модальное окно настроек автогенерации
2107 async function openAutogenModal() {
2108 console.log('Ozon Description Generator: Открытие модального окна автогенерации');
2109
2110 const modal = document.createElement('div');
2111 modal.className = 'ozon-desc-modal';
2112 modal.innerHTML = `
2113 <div class="ozon-desc-modal-content">
2114 <div class="ozon-desc-modal-header">🤖 Автогенерация описаний</div>
2115
2116 <div class="ozon-desc-input-group">
2117 <label class="ozon-desc-label">Ключевые слова (необязательно, каждое с новой строки):</label>
2118 <textarea class="ozon-desc-textarea" id="ozon-autogen-keywords-input" placeholder="Если не заполнено, AI сам подберет ключевые слова для каждого товара"></textarea>
2119 </div>
2120
2121 <div class="ozon-checkbox-container">
2122 <input type="checkbox" class="ozon-checkbox" id="ozon-autogen-rich-checkbox">
2123 <label class="ozon-checkbox-label" for="ozon-autogen-rich-checkbox">Отправить в Rich-контент</label>
2124 </div>
2125
2126 <div class="ozon-checkbox-container">
2127 <input type="checkbox" class="ozon-checkbox" id="ozon-autogen-test-mode-checkbox">
2128 <label class="ozon-checkbox-label" for="ozon-autogen-test-mode-checkbox">🧪 Тестовый режим (без AI, вставка "тест")</label>
2129 </div>
2130
2131 <div id="ozon-autogen-status-container"></div>
2132
2133 <div class="ozon-desc-buttons">
2134 <button class="ozon-desc-btn ozon-desc-btn-secondary" id="ozon-autogen-close-btn">Закрыть</button>
2135 <button class="ozon-desc-btn ozon-desc-btn-primary" id="ozon-autogen-start-btn">🚀 Начать автогенерацию</button>
2136 </div>
2137 </div>
2138 `;
2139
2140 document.body.appendChild(modal);
2141
2142 modal.addEventListener('click', (e) => {
2143 if (e.target === modal) {
2144 modal.remove();
2145 }
2146 });
2147
2148 document.getElementById('ozon-autogen-close-btn').addEventListener('click', () => {
2149 modal.remove();
2150 });
2151
2152 document.getElementById('ozon-autogen-start-btn').addEventListener('click', () => {
2153 startAutogeneration(modal);
2154 });
2155 }
2156
2157 // Начало автогенерации
2158 async function startAutogeneration(modal) {
2159 const keywordsInput = document.getElementById('ozon-autogen-keywords-input');
2160 const richCheckbox = document.getElementById('ozon-autogen-rich-checkbox');
2161 const testModeCheckbox = document.getElementById('ozon-autogen-test-mode-checkbox');
2162
2163 const globalKeywords = keywordsInput.value.trim();
2164 const sendToRich = richCheckbox.checked;
2165 const testMode = testModeCheckbox.checked;
2166
2167 console.log('Ozon Description Generator: Начало автогенерации');
2168 console.log('Глобальные ключевые слова:', globalKeywords);
2169 console.log('Отправить в Rich-контент:', sendToRich);
2170 console.log('Тестовый режим:', testMode);
2171
2172 // Сохраняем настройки в localStorage
2173 localStorage.setItem('ozon_autogen_global_keywords', globalKeywords);
2174 localStorage.setItem('ozon_autogen_send_to_rich', sendToRich);
2175 localStorage.setItem('ozon_autogen_test_mode', testMode);
2176 localStorage.setItem('ozon_autogen_processed', '0');
2177 localStorage.setItem('ozon_autogen_errors', '0');
2178
2179 // Очищаем список обработанных товаров при новом запуске
2180 localStorage.setItem('ozon_autogen_processed_skus', '[]');
2181 console.log('Ozon Description Generator: Список обработанных товаров очищен');
2182
2183 modal.remove();
2184
2185 // Показываем окно прогресса
2186 showAutogenProgress();
2187
2188 // Начинаем обработку товаров
2189 processNextProduct();
2190 }
2191
2192 // Обработка следующего товара
2193 async function processNextProduct() {
2194 console.log('Ozon Description Generator: processNextProduct вызван');
2195
2196 // Прокручиваем страницу вниз, чтобы загрузить товары
2197 window.scrollBy(0, 500);
2198 await new Promise(resolve => setTimeout(resolve, 1000));
2199
2200 // Получаем список уже обработанных товаров
2201 const processedProducts = JSON.parse(localStorage.getItem('ozon_autogen_processed_skus') || '[]');
2202 console.log('Ozon Description Generator: Уже обработано товаров:', processedProducts.length);
2203
2204 // Ищем ссылки на карточки товаров - пробуем разные варианты
2205 let productLinks = Array.from(document.querySelectorAll('a[href*="/app/products/"]')).filter(link => {
2206 const href = link.getAttribute('href');
2207 return href && href.includes('/app/products/') && /\/\d+\//.test(href);
2208 });
2209
2210 // Если не нашли ссылки, ищем кнопки "Редактировать" по классам и title
2211 if (productLinks.length === 0) {
2212 console.log('Ozon Description Generator: Ссылки не найдены, ищем кнопки редактирования по классам');
2213
2214 // Ищем кнопки с классом c9r110-a и title "редактировать товар"
2215 const editButtons = Array.from(document.querySelectorAll('button.c9r110-a[title*="едактировать"]'));
2216
2217 if (editButtons.length > 0) {
2218 console.log(`Ozon Description Generator: Найдено кнопок редактирования: ${editButtons.length}`);
2219
2220 // Ищем первую необработанную кнопку
2221 let editButton = null;
2222 let nmID = '';
2223 let productName = 'Товар';
2224
2225 for (const btn of editButtons) {
2226 const row = btn.closest('tr');
2227 const skuElement = row?.querySelector('.index_skuText_61dv5');
2228
2229 if (skuElement) {
2230 const sku = skuElement.textContent.trim();
2231
2232 // Проверяем, не обработан ли уже этот товар
2233 if (!processedProducts.includes(sku)) {
2234 editButton = btn;
2235 nmID = sku;
2236 const nameCell = row?.querySelector('td:nth-child(2)');
2237 if (nameCell) {
2238 productName = nameCell.textContent.trim();
2239 }
2240 console.log('Ozon Description Generator: Найден необработанный товар с SKU:', nmID);
2241 break;
2242 } else {
2243 console.log('Ozon Description Generator: Товар с SKU', sku, 'уже обработан, пропускаем');
2244 }
2245 }
2246 }
2247
2248 if (!editButton) {
2249 console.log('Ozon Description Generator: Все товары на странице уже обработаны');
2250 await stopAutogeneration();
2251 return;
2252 }
2253
2254 // Если не нашли SKU, генерируем временный
2255 if (!nmID) {
2256 nmID = Date.now().toString();
2257 console.log('Ozon Description Generator: SKU не найден, используем временный:', nmID);
2258 }
2259
2260 console.log(`Ozon Description Generator: Обработка товара: ${productName} (SKU: ${nmID})`);
2261
2262 // Добавляем товар в список обработанных
2263 processedProducts.push(nmID);
2264 localStorage.setItem('ozon_autogen_processed_skus', JSON.stringify(processedProducts));
2265
2266 // Генерируем checkId и сохраняем данные
2267 const checkId = generateCheckId();
2268 setAutoCheck(nmID, productName, checkId);
2269
2270 // Обновляем текущий товар в прогрессе
2271 localStorage.setItem('ozon_autogen_current_product', productName);
2272
2273 // Кликаем на кнопку
2274 console.log('Ozon Description Generator: Кликаем на кнопку редактирования');
2275 editButton.click();
2276
2277 // Ждем результата через polling
2278 await waitForAutoResult(checkId);
2279 return;
2280 }
2281 }
2282
2283 console.log(`Ozon Description Generator: Найдено ссылок на товары: ${productLinks.length}`);
2284
2285 if (productLinks.length === 0) {
2286 console.log('Ozon Description Generator: Ссылки и кнопки не найдены, завершаем автогенерацию');
2287 await stopAutogeneration();
2288 return;
2289 }
2290
2291 // Берем первую ссылку
2292 const productLink = productLinks[0];
2293 const href = productLink.getAttribute('href');
2294 const nmIDMatch = href.match(/\/products\/(\d+)\//);
2295 const nmID = nmIDMatch ? nmIDMatch[1] : null;
2296
2297 if (!nmID) {
2298 console.log('Ozon Description Generator: Не удалось извлечь nmID из ссылки');
2299 await stopAutogeneration();
2300 return;
2301 }
2302
2303 // Получаем название товара из строки таблицы
2304 const row = productLink.closest('tr');
2305 const productName = row?.querySelector('td:nth-child(2)')?.textContent?.trim() || `Товар ${nmID}`;
2306
2307 console.log(`Ozon Description Generator: Обработка товара: ${productName} (nmID: ${nmID})`);
2308
2309 // Добавляем товар в список обработанных
2310 processedProducts.push(nmID);
2311 localStorage.setItem('ozon_autogen_processed_skus', JSON.stringify(processedProducts));
2312
2313 // Генерируем checkId и сохраняем данные
2314 const checkId = generateCheckId();
2315 setAutoCheck(nmID, productName, checkId);
2316
2317 // Обновляем текущий товар в прогрессе
2318 localStorage.setItem('ozon_autogen_current_product', productName);
2319
2320 // Кликаем на ссылку
2321 productLink.click();
2322
2323 // Ждем результата через polling
2324 await waitForAutoResult(checkId);
2325 }
2326
2327 // Ожидание результата через polling
2328 async function waitForAutoResult(checkId) {
2329 console.log('Ozon Description Generator: Ожидание результата для checkId:', checkId);
2330
2331 const maxWaitTime = 300000; // 5 минут
2332 const pollInterval = 500;
2333 let waitedTime = 0;
2334
2335 while (waitedTime < maxWaitTime) {
2336 await new Promise(resolve => setTimeout(resolve, pollInterval));
2337 waitedTime += pollInterval;
2338
2339 const result = getAutoResult();
2340
2341 if (result && result.checkId === checkId) {
2342 console.log('Ozon Description Generator: Получен результат:', result);
2343
2344 // Обновляем счетчики
2345 if (result.success) {
2346 const processed = parseInt(localStorage.getItem('ozon_autogen_processed') || '0');
2347 localStorage.setItem('ozon_autogen_processed', (processed + 1).toString());
2348 } else {
2349 const errors = parseInt(localStorage.getItem('ozon_autogen_errors') || '0');
2350 localStorage.setItem('ozon_autogen_errors', (errors + 1).toString());
2351 }
2352
2353 // Очищаем временные данные
2354 clearAutoCheck(checkId);
2355
2356 // Обрабатываем следующий товар
2357 setTimeout(() => processNextProduct(), 2000);
2358 return;
2359 }
2360 }
2361
2362 console.error('Ozon Description Generator: Превышено время ожидания результата');
2363 const errors = parseInt(localStorage.getItem('ozon_autogen_errors') || '0');
2364 localStorage.setItem('ozon_autogen_errors', (errors + 1).toString());
2365 clearAutoCheck(checkId);
2366
2367 // Обрабатываем следующий товар
2368 setTimeout(() => processNextProduct(), 2000);
2369 }
2370
2371 // ============================================
2372 // СКРЫТИЕ ОКНА ПРОГРЕССА ОБРАБОТКИ ТОВАРА
2373 // ============================================
2374
2375 // Скрытие окна прогресса обработки товара
2376 function hideProductProgress() {
2377 const progressDiv = document.querySelector('.ozon-product-progress');
2378 if (progressDiv) {
2379 progressDiv.remove();
2380 }
2381 }
2382
2383 // ============================================
2384 // СОЗДАНИЕ ОКНА ПРОГРЕССА ОБРАБОТКИ ТОВАРА
2385 // ============================================
2386
2387 // Показ окна прогресса обработки товара
2388 function showProductProgress() {
2389 if (document.querySelector('.ozon-product-progress')) {
2390 return;
2391 }
2392
2393 const progressDiv = document.createElement('div');
2394 progressDiv.className = 'ozon-product-progress';
2395 progressDiv.innerHTML = `
2396 <div class="ozon-product-progress-header">Обработка товара</div>
2397 <div class="ozon-product-progress-stage pending" data-stage="info">Получение информации о товаре</div>
2398 <div class="ozon-product-progress-stage pending" data-stage="keywords">Подбор ключевых слов</div>
2399 <div class="ozon-product-progress-stage pending" data-stage="analytics">Сбор данных из аналитики</div>
2400 <div class="ozon-product-progress-stage pending" data-stage="generation">Генерация описания</div>
2401 <div class="ozon-product-progress-stage pending" data-stage="insert">Вставка описания</div>
2402 <div class="ozon-product-progress-stage pending" data-stage="rich">Отправка в Rich-контент</div>
2403 <div class="ozon-product-progress-stage pending" data-stage="save">Сохранение товара</div>
2404 `;
2405
2406 document.body.appendChild(progressDiv);
2407 }
2408
2409 // ============================================
2410 // ПРОКРУТИВКА СТРАНИЦЫ
2411 // ============================================
2412
2413 // Обновление прогресса обработки товара
2414 function updateProductProgress(stageName, status) {
2415 const stageMap = {
2416 'Получение информации о товаре': 'info',
2417 'Подбор ключевых слов': 'keywords',
2418 'Сбор данных из аналитики': 'analytics',
2419 'Генерация описания': 'generation',
2420 'Вставка описания': 'insert',
2421 'Отправка в Rich-контент': 'rich',
2422 'Сохранение товара': 'save',
2423 'Ошибка': 'error'
2424 };
2425
2426 const stageId = stageMap[stageName];
2427 if (!stageId) return;
2428
2429 const stageEl = document.querySelector(`.ozon-product-progress-stage[data-stage="${stageId}"]`);
2430 if (stageEl) {
2431 stageEl.className = `ozon-product-progress-stage ${status}`;
2432 }
2433 }
2434
2435 // Обработка товара на странице редактирования при автогенерации
2436 async function handleProductPageAutogen() {
2437 console.log('Ozon Description Generator: Проверка автогенерации на странице товара');
2438
2439 // Ищем все ключи wbAutoExpected_*
2440 const expectedKeys = Object.keys(localStorage).filter(key => key.startsWith('wbAutoExpected_'));
2441
2442 if (expectedKeys.length === 0) {
2443 console.log('Ozon Description Generator: Нет ожидаемых задач автогенерации');
2444 return;
2445 }
2446
2447 const checkId = expectedKeys[0].replace('wbAutoExpected_', '');
2448 console.log('Ozon Description Generator: Найден checkId:', checkId);
2449
2450 // Проверяем, что задача свежая
2451 const autoCheck = getAutoCheck();
2452 if (!autoCheck || autoCheck.checkId !== checkId) {
2453 console.log('Ozon Description Generator: autoCheck не найден или не совпадает');
2454 clearAutoCheck(checkId);
2455 return;
2456 }
2457
2458 console.log('Ozon Description Generator: Начинаем обработку товара:', autoCheck.title);
2459
2460 // Показываем окно прогресса обработки товара
2461 showProductProgress();
2462
2463 try {
2464 // Проверяем, на какой странице мы находимся
2465 const currentUrl = window.location.href;
2466
2467 if (currentUrl.includes('/edit/general-info')) {
2468 // Шаг 1: Получаем название товара
2469 updateProductProgress('Получение информации о товаре', 'active');
2470 console.log('Ozon Description Generator: Ожидаем загрузки страницы товара');
2471
2472 await new Promise(resolve => setTimeout(resolve, 5000));
2473
2474 let title = '';
2475 let attempts = 0;
2476 const maxAttempts = 20;
2477
2478 while (attempts < maxAttempts) {
2479 const titleInput = document.querySelector('input[name="name"]');
2480 if (titleInput && titleInput.value.trim()) {
2481 title = titleInput.value;
2482 console.log('Ozon Description Generator: Название товара найдено:', title);
2483 break;
2484 }
2485 console.log('Ozon Description Generator: Попытка', attempts + 1, '- название еще не загружено');
2486 await new Promise(resolve => setTimeout(resolve, 500));
2487 attempts++;
2488 }
2489
2490 if (!title) {
2491 throw new Error('Не удалось загрузить название товара');
2492 }
2493
2494 // Сохраняем название
2495 localStorage.setItem(`ozon_autogen_title_${checkId}`, title);
2496
2497 // Переходим на all-attrs
2498 console.log('Ozon Description Generator: Название загружено, переходим на all-attrs');
2499 const newUrl = currentUrl.replace('/edit/general-info', '/edit/all-attrs');
2500 window.location.href = newUrl;
2501 return;
2502 }
2503
2504 // Если мы на странице all-attrs - продолжаем обработку
2505 if (currentUrl.includes('/edit/all-attrs')) {
2506 // Получаем сохраненное название
2507 const title = localStorage.getItem(`ozon_autogen_title_${checkId}`) || autoCheck.title;
2508 console.log('Ozon Description Generator: Используем название:', title);
2509
2510 updateProductProgress('Получение информации о товаре', 'active');
2511 await new Promise(resolve => setTimeout(resolve, 5000));
2512
2513 // Получаем состав
2514 const compositionTextarea = document.querySelector('textarea[name="attribute#8050"]');
2515 let composition = '';
2516 if (compositionTextarea) {
2517 composition = compositionTextarea.value;
2518 console.log('Ozon Description Generator: Состав найден, длина:', composition.length);
2519 }
2520
2521 const productInfo = { title, composition };
2522 updateProductProgress('Получение информации о товаре', 'completed');
2523
2524 // Очищаем временное сохранение названия
2525 localStorage.removeItem(`ozon_autogen_title_${checkId}`);
2526
2527 // Проверяем тестовый режим
2528 const testMode = localStorage.getItem('ozon_autogen_test_mode') === 'true';
2529
2530 if (testMode) {
2531 console.log('Ozon Description Generator: 🧪 ТЕСТОВЫЙ РЕЖИМ - пропускаем AI, вставляем "тест"');
2532
2533 updateProductProgress('Подбор ключевых слов', 'completed');
2534 updateProductProgress('Генерация описания', 'completed');
2535
2536 updateProductProgress('Вставка описания', 'active');
2537 const proseMirrorDiv = document.querySelector('[id="attribute#4191"] .ProseMirror');
2538 if (proseMirrorDiv) {
2539 proseMirrorDiv.innerHTML = '<p>тест</p>';
2540 proseMirrorDiv.dispatchEvent(new Event('input', { bubbles: true }));
2541 proseMirrorDiv.dispatchEvent(new Event('change', { bubbles: true }));
2542 console.log('Ozon Description Generator: 🧪 Тестовое описание "тест" вставлено');
2543 updateProductProgress('Вставка описания', 'completed');
2544 }
2545
2546 updateProductProgress('Отправка в Rich-контент', 'completed');
2547
2548 } else {
2549 // ОБЫЧНЫЙ РЕЖИМ - весь код с AI
2550
2551 updateProductProgress('Подбор ключевых слов', 'active');
2552 const globalKeywords = localStorage.getItem('ozon_autogen_global_keywords') || '';
2553 let keywords = [];
2554
2555 if (globalKeywords) {
2556 keywords = globalKeywords.split('\n').map(k => k.trim()).filter(k => k);
2557 console.log('Ozon Description Generator: Используем глобальные ключевые слова:', keywords);
2558 updateProductProgress('Подбор ключевых слов', 'completed');
2559 } else {
2560 console.log('Ozon Description Generator: AI подбирает ключевые слова');
2561 const suggestPrompt = generateMasksPrompt(productInfo);
2562 const suggestResponse = await RM.aiCall(suggestPrompt);
2563 const suggestData = JSON.parse(suggestResponse);
2564 keywords = [...(suggestData.masks || []), ...(suggestData.keywords || [])];
2565 console.log('Ozon Description Generator: AI подобрал', keywords.length, 'ключевых слов');
2566 updateProductProgress('Подбор ключевых слов', 'completed');
2567 }
2568
2569 updateProductProgress('Сбор данных из аналитики', 'active');
2570 const analyticsData = await collectAnalyticsData(keywords, []);
2571
2572 if (analyticsData.length === 0) {
2573 throw new Error('Не удалось собрать данные из аналитики');
2574 }
2575 console.log('Ozon Description Generator: Данные из аналитики собраны');
2576 updateProductProgress('Сбор данных из аналитики', 'completed');
2577
2578 updateProductProgress('Генерация описания', 'active');
2579 const allQueries = [];
2580 const queryPopularity = {};
2581 analyticsData.forEach(data => {
2582 data.queries.forEach(q => {
2583 allQueries.push(q.query);
2584 queryPopularity[q.query.toLowerCase()] = q.popularity;
2585 });
2586 });
2587
2588 console.log('Ozon Description Generator: Генерируем описание с', allQueries.length, 'запросами');
2589 const descriptionPrompt = generateDescriptionPrompt(productInfo, keywords, allQueries, queryPopularity);
2590 const description = await RM.aiCall(descriptionPrompt);
2591 console.log('Ozon Description Generator: Описание сгенерировано, длина:', description.length);
2592 updateProductProgress('Генерация описания', 'completed');
2593
2594 updateProductProgress('Вставка описания', 'active');
2595 const proseMirrorDiv = document.querySelector('[id="attribute#4191"] .ProseMirror');
2596 if (proseMirrorDiv) {
2597 proseMirrorDiv.innerHTML = `<p>${description.replace(/\n\n/g, '</p><p>').replace(/\n/g, '<br>')}</p>`;
2598 proseMirrorDiv.dispatchEvent(new Event('input', { bubbles: true }));
2599 proseMirrorDiv.dispatchEvent(new Event('change', { bubbles: true }));
2600 console.log('Ozon Description Generator: Описание вставлено');
2601 updateProductProgress('Вставка описания', 'completed');
2602 }
2603
2604 const sendToRich = localStorage.getItem('ozon_autogen_send_to_rich') === 'true';
2605 if (sendToRich) {
2606 updateProductProgress('Отправка в Rich-контент', 'active');
2607 await GM.setValue('ozon_description_for_rich', description);
2608
2609 const mediaTabButton = Array.from(document.querySelectorAll('button.c9s110-a3.c9s110-a0.c9s110-a.c9s110-a4.c9s110-a6')).find(btn =>
2610 btn.textContent && btn.textContent.includes('Медиа')
2611 );
2612
2613 if (mediaTabButton) {
2614 console.log('Ozon Description Generator: Переходим на вкладку Медиа');
2615 mediaTabButton.click();
2616 await new Promise(resolve => setTimeout(resolve, 1000));
2617 await insertRichContent();
2618 }
2619 updateProductProgress('Отправка в Rich-контент', 'completed');
2620 }
2621
2622 updateProductProgress('Сохранение товара', 'active');
2623 await new Promise(resolve => setTimeout(resolve, 3000));
2624
2625 const buttons = Array.from(document.querySelectorAll('button'));
2626 const moderationButton = buttons.find(btn =>
2627 btn.textContent && btn.textContent.includes('Отправить на модерацию')
2628 );
2629
2630 if (moderationButton) {
2631 console.log('Ozon Description Generator: Кнопка "Отправить на модерацию" найдена');
2632
2633 const clickEvent = new MouseEvent('click', {
2634 view: window,
2635 bubbles: true,
2636 cancelable: true
2637 });
2638 moderationButton.dispatchEvent(clickEvent);
2639
2640 console.log('Ozon Description Generator: Клик выполнен, товар отправлен на модерацию');
2641 updateProductProgress('Сохранение товара', 'completed');
2642
2643 // Сохраняем успешный результат
2644 setAutoResult(checkId, true);
2645
2646 hideProductProgress();
2647
2648 // Ждем полсекунды перед закрытием вкладки
2649 console.log('Ozon Description Generator: Ожидание 0.5 секунды перед закрытием вкладки');
2650 await new Promise(resolve => setTimeout(resolve, 500));
2651
2652 console.log('Ozon Description Generator: Закрываем вкладку товара');
2653 window.close();
2654 } else {
2655 throw new Error('Кнопка "Отправить на модерацию" не найдена');
2656 }
2657 }
2658
2659 // Ждем 3 секунды перед сохранением
2660 console.log('Ozon Description Generator: Ожидание 3 секунды перед сохранением');
2661 await new Promise(resolve => setTimeout(resolve, 3000));
2662
2663 // Сохраняем товар
2664 updateProductProgress('Сохранение товара', 'active');
2665 await new Promise(resolve => setTimeout(resolve, 3000));
2666
2667 const buttons = Array.from(document.querySelectorAll('button'));
2668 const moderationButton = buttons.find(btn =>
2669 btn.textContent && btn.textContent.includes('Отправить на модерацию')
2670 );
2671
2672 if (moderationButton) {
2673 console.log('Ozon Description Generator: Кнопка "Отправить на модерацию" найдена');
2674
2675 const clickEvent = new MouseEvent('click', {
2676 view: window,
2677 bubbles: true,
2678 cancelable: true
2679 });
2680 moderationButton.dispatchEvent(clickEvent);
2681
2682 console.log('Ozon Description Generator: Клик выполнен, товар отправлен на модерацию');
2683 updateProductProgress('Сохранение товара', 'completed');
2684
2685 // Сохраняем успешный результат
2686 setAutoResult(checkId, true);
2687
2688 hideProductProgress();
2689
2690 // Ждем полсекунды перед закрытием вкладки
2691 console.log('Ozon Description Generator: Ожидание 0.5 секунды перед закрытием вкладки');
2692 await new Promise(resolve => setTimeout(resolve, 500));
2693
2694 console.log('Ozon Description Generator: Закрываем вкладку товара');
2695 window.close();
2696 } else {
2697 throw new Error('Кнопка "Отправить на модерацию" не найдена');
2698 }
2699 }
2700
2701 } catch (error) {
2702 console.error('Ozon Description Generator: Ошибка при обработке товара:', error);
2703 updateProductProgress('Ошибка', 'error');
2704
2705 // Очищаем временное сохранение названия
2706 localStorage.removeItem(`ozon_autogen_title_${checkId}`);
2707
2708 // Сохраняем результат с ошибкой
2709 setAutoResult(checkId, false, error.message);
2710
2711 console.log('Ozon Description Generator: Ошибка, закрываем вкладку через 3 секунды');
2712
2713 setTimeout(() => {
2714 hideProductProgress();
2715 window.close();
2716 }, 3000);
2717 }
2718 }
2719
2720 // ============================================
2721 // ИНИЦИАЛИЗАЦИЯ
2722 // ============================================
2723
2724 function init() {
2725 console.log('Ozon Description Generator: Инициализация');
2726
2727 // Страница списка товаров
2728 if (window.location.href === 'https://seller.ozon.ru/app/products' ||
2729 (window.location.href.includes('seller.ozon.ru/app/products') && !window.location.href.includes('/edit/'))) {
2730 console.log('Ozon Description Generator: Страница списка товаров');
2731
2732 // Постоянно следим за кнопкой автогенерации
2733 const debouncedCreateButton = debounce(createAutogenButton, 500);
2734
2735 const observer = new MutationObserver(() => {
2736 debouncedCreateButton();
2737 });
2738
2739 observer.observe(document.body, {
2740 childList: true,
2741 subtree: true
2742 });
2743
2744 setTimeout(createAutogenButton, 2000);
2745
2746 // Проверяем, нужно ли продолжить автогенерацию - СРАЗУ и через интервалы
2747 checkAndContinueAutogen();
2748
2749 // Проверяем каждые 2 секунды, нужно ли продолжить
2750 const autogenCheckInterval = setInterval(async () => {
2751 const autogenStatus = await GM.getValue('ozon_autogen_status', 'stopped');
2752 if (autogenStatus === 'running') {
2753 console.log('Ozon Description Generator: Обнаружена активная автогенерация, продолжаем');
2754 clearInterval(autogenCheckInterval);
2755 showAutogenProgress();
2756 setTimeout(processNextProduct, 3000);
2757 }
2758 }, 2000);
2759
2760 // Останавливаем проверку через 30 секунд
2761 setTimeout(() => clearInterval(autogenCheckInterval), 30000);
2762 }
2763
2764 // Страница редактирования товара
2765 if (window.location.href.includes('seller.ozon.ru/app/products/') &&
2766 (window.location.href.includes('/edit/all-attrs') || window.location.href.includes('/edit/general-info'))) {
2767 console.log('Ozon Description Generator: Страница редактирования товара');
2768
2769 // Проверяем, идет ли автогенерация
2770 checkAndStartAutogen();
2771
2772 const observer = new MutationObserver((mutations, obs) => {
2773 const annotationContainer = document.querySelector('[id="attribute#4191"]');
2774 if (annotationContainer) {
2775 createGeneratorButton();
2776 obs.disconnect();
2777 }
2778 });
2779
2780 observer.observe(document.body, {
2781 childList: true,
2782 subtree: true
2783 });
2784
2785 setTimeout(createGeneratorButton, 2000);
2786 }
2787
2788 if (window.location.href.includes('seller.ozon.ru/app/products/') && window.location.href.includes('/edit/media')) {
2789 console.log('Ozon Description Generator: Страница медиа');
2790 setTimeout(insertRichContent, 2000);
2791 }
2792
2793 if (window.location.href.includes('seller.ozon.ru/app/analytics/what-to-sell/all-queries')) {
2794 setTimeout(autoCollectOnAnalyticsPage, 2000);
2795 }
2796 }
2797
2798 // Проверка и продолжение автогенерации на странице списка
2799 async function checkAndContinueAutogen() {
2800 console.log('Ozon Description Generator: Проверяем необходимость продолжения автогенерации');
2801
2802 // Проверяем, есть ли активная задача
2803 const autoCheck = getAutoCheck();
2804
2805 if (autoCheck && isTimestampFresh(autoCheck.timestamp)) {
2806 console.log('Ozon Description Generator: Найдена активная задача автогенерации');
2807 showAutogenProgress();
2808
2809 // Ждем результата
2810 await waitForAutoResult(autoCheck.checkId);
2811 }
2812 }
2813
2814 // Проверка и запуск автогенерации
2815 async function checkAndStartAutogen() {
2816 // Проверяем, есть ли ожидаемая задача для этой вкладки
2817 const expectedKeys = Object.keys(localStorage).filter(key => key.startsWith('wbAutoExpected_'));
2818
2819 if (expectedKeys.length > 0) {
2820 console.log('Ozon Description Generator: Обнаружена задача автогенерации, запускаем обработку');
2821 await handleProductPageAutogen();
2822 }
2823 }
2824
2825 // Показ окна прогресса автогенерации
2826 async function showAutogenProgress() {
2827 if (document.querySelector('.ozon-autogen-progress')) {
2828 return;
2829 }
2830
2831 const progressDiv = document.createElement('div');
2832 progressDiv.className = 'ozon-autogen-progress';
2833 progressDiv.innerHTML = `
2834 <div class="ozon-autogen-progress-header">
2835 <span>🤖 Автогенерация</span>
2836 <span class="ozon-autogen-progress-close" id="ozon-autogen-progress-close">×</span>
2837 </div>
2838 <div class="ozon-autogen-progress-info" id="ozon-autogen-progress-info">
2839 Ожидание...
2840 </div>
2841 <div class="ozon-autogen-progress-stats">
2842 <div class="ozon-autogen-progress-stat">
2843 <div class="ozon-autogen-progress-stat-label">Обработано</div>
2844 <div class="ozon-autogen-progress-stat-value success" id="ozon-autogen-processed">0</div>
2845 </div>
2846 <div class="ozon-autogen-progress-stat">
2847 <div class="ozon-autogen-progress-stat-label">Ошибок</div>
2848 <div class="ozon-autogen-progress-stat-value error" id="ozon-autogen-errors">0</div>
2849 </div>
2850 </div>
2851 <div class="ozon-autogen-progress-controls">
2852 <button class="ozon-autogen-progress-btn pause" id="ozon-autogen-pause-btn">⏸ Пауза</button>
2853 <button class="ozon-autogen-progress-btn stop" id="ozon-autogen-stop-btn">⏹ Остановить</button>
2854 </div>
2855 `;
2856
2857 document.body.appendChild(progressDiv);
2858
2859 document.getElementById('ozon-autogen-progress-close').addEventListener('click', () => {
2860 progressDiv.remove();
2861 });
2862
2863 document.getElementById('ozon-autogen-pause-btn').addEventListener('click', toggleAutogenPause);
2864 document.getElementById('ozon-autogen-stop-btn').addEventListener('click', stopAutogeneration);
2865
2866 // Обновляем прогресс каждую секунду
2867 setInterval(updateAutogenProgress, 1000);
2868 }
2869
2870 // Обновление прогресса автогенерации
2871 async function updateAutogenProgress() {
2872 const progressDiv = document.querySelector('.ozon-autogen-progress');
2873 if (!progressDiv) return;
2874
2875 const processed = parseInt(localStorage.getItem('ozon_autogen_processed') || '0');
2876 const errors = parseInt(localStorage.getItem('ozon_autogen_errors') || '0');
2877 const currentProduct = localStorage.getItem('ozon_autogen_current_product') || '';
2878
2879 const processedEl = document.getElementById('ozon-autogen-processed');
2880 const errorsEl = document.getElementById('ozon-autogen-errors');
2881 const infoEl = document.getElementById('ozon-autogen-progress-info');
2882
2883 if (processedEl) processedEl.textContent = processed;
2884 if (errorsEl) errorsEl.textContent = errors;
2885 if (infoEl) {
2886 if (currentProduct) {
2887 infoEl.textContent = `Обработка: ${currentProduct}`;
2888 } else {
2889 infoEl.textContent = 'Ожидание...';
2890 }
2891 }
2892 }
2893
2894 // Переключение паузы автогенерации
2895 async function toggleAutogenPause() {
2896 // Пауза больше не нужна в новой логике
2897 console.log('Ozon Description Generator: Пауза не поддерживается в новой версии');
2898 }
2899
2900 // Остановка автогенерации
2901 async function stopAutogeneration() {
2902 console.log('Ozon Description Generator: Автогенерация остановлена');
2903
2904 // Очищаем все данные автогенерации
2905 const autoCheck = getAutoCheck();
2906 if (autoCheck) {
2907 clearAutoCheck(autoCheck.checkId);
2908 }
2909
2910 localStorage.removeItem('ozon_autogen_global_keywords');
2911 localStorage.removeItem('ozon_autogen_send_to_rich');
2912 localStorage.removeItem('ozon_autogen_test_mode');
2913 localStorage.removeItem('ozon_autogen_current_product');
2914
2915 const progressDiv = document.querySelector('.ozon-autogen-progress');
2916 if (progressDiv) {
2917 setTimeout(() => progressDiv.remove(), 2000);
2918 }
2919 }
2920
2921 if (document.readyState === 'loading') {
2922 document.addEventListener('DOMContentLoaded', init);
2923 } else {
2924 init();
2925 }
2926
2927})();