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