Генератор SEO-описаний для товаров на Wildberries с анализом ключевых слов
Size
87.0 KB
Version
2.8.4
Created
Jan 15, 2026
Updated
3 months ago
1// ==UserScript==
2// @name Wildberries Description Generator 2.0
3// @description Генератор SEO-описаний для товаров на Wildberries с анализом ключевых слов
4// @version 2.8.4
5// @match https://*.seller.wildberries.ru/*
6// @icon https://static-basket-02.wbbasket.ru/vol20/root-monorepo/latest/favicon.ico
7// ==/UserScript==
8(function() {
9 'use strict';
10
11 console.log('Wildberries Description Generator: Расширение запущено');
12
13 // Добавляем стили для модального окна
14 TM_addStyle(`
15 .wb-desc-modal {
16 position: fixed;
17 top: 0;
18 left: 0;
19 width: 100%;
20 height: 100%;
21 background: rgba(0, 0, 0, 0.5);
22 display: flex;
23 align-items: center;
24 justify-content: center;
25 z-index: 10000;
26 }
27
28 .wb-desc-modal-content {
29 background: white;
30 border-radius: 12px;
31 padding: 24px;
32 max-width: 600px;
33 width: 90%;
34 max-height: 80vh;
35 overflow-y: auto;
36 box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
37 }
38
39 .wb-desc-modal-header {
40 font-size: 22px;
41 font-weight: 600;
42 margin-bottom: 20px;
43 color: #001a34;
44 }
45
46 .wb-desc-input-group {
47 margin-bottom: 16px;
48 }
49
50 .wb-desc-label {
51 display: block;
52 margin-bottom: 8px;
53 font-weight: 500;
54 color: #001a34;
55 font-size: 16px;
56 }
57
58 .wb-desc-textarea {
59 width: 100%;
60 min-height: 100px;
61 padding: 12px;
62 border: 1px solid #d1d5db;
63 border-radius: 8px;
64 font-size: 16px;
65 font-family: inherit;
66 resize: vertical;
67 }
68
69 .wb-desc-textarea:focus {
70 outline: none;
71 border-color: #9333ea;
72 }
73
74 .wb-desc-result {
75 background: #f3f4f6;
76 padding: 16px;
77 border-radius: 8px;
78 margin-bottom: 16px;
79 max-height: 300px;
80 overflow-y: auto;
81 white-space: pre-wrap;
82 word-wrap: break-word;
83 font-size: 16px;
84 }
85
86 .wb-desc-char-count {
87 text-align: right;
88 font-size: 14px;
89 color: #6b7280;
90 margin-top: 4px;
91 }
92
93 .wb-desc-char-count.warning {
94 color: #f59e0b;
95 }
96
97 .wb-desc-char-count.error {
98 color: #ef4444;
99 }
100
101 .wb-desc-buttons {
102 display: flex;
103 gap: 12px;
104 justify-content: flex-end;
105 margin-top: 20px;
106 }
107
108 .wb-desc-btn {
109 padding: 10px 20px;
110 border: none;
111 border-radius: 8px;
112 font-size: 16px;
113 font-weight: 500;
114 cursor: pointer;
115 transition: all 0.2s;
116 }
117
118 .wb-desc-btn-primary {
119 background: #9333ea;
120 color: white;
121 }
122
123 .wb-desc-btn-primary:hover {
124 background: #7e22ce;
125 }
126
127 .wb-desc-btn-primary:disabled {
128 background: #9ca3af;
129 cursor: not-allowed;
130 }
131
132 .wb-desc-btn-secondary {
133 background: #e5e7eb;
134 color: #374151;
135 }
136
137 .wb-desc-btn-secondary:hover {
138 background: #d1d5db;
139 }
140
141 .wb-desc-btn-success {
142 background: #10b981;
143 color: white;
144 }
145
146 .wb-desc-btn-success:hover {
147 background: #059669;
148 }
149
150 .wb-desc-generator-btn {
151 margin-left: 12px;
152 padding: 10px 20px;
153 background: #9333ea;
154 color: white;
155 border: none;
156 border-radius: 8px;
157 font-size: 16px;
158 font-weight: 500;
159 cursor: pointer;
160 transition: all 0.2s;
161 }
162
163 .wb-desc-generator-btn:hover {
164 background: #7e22ce;
165 }
166
167 .wb-desc-status {
168 margin-top: 12px;
169 padding: 8px 12px;
170 border-radius: 6px;
171 font-size: 15px;
172 }
173
174 .wb-desc-status.info {
175 background: #dbeafe;
176 color: #1e40af;
177 }
178
179 .wb-desc-status.success {
180 background: #d1fae5;
181 color: #065f46;
182 }
183
184 .wb-desc-status.error {
185 background: #fee2e2;
186 color: #991b1b;
187 }
188
189 .wb-desc-suggest-btn {
190 margin-top: 8px;
191 padding: 8px 16px;
192 background: #6366f1;
193 color: white;
194 border: none;
195 border-radius: 6px;
196 font-size: 15px;
197 font-weight: 500;
198 cursor: pointer;
199 transition: all 0.2s;
200 }
201
202 .wb-desc-suggest-btn:hover {
203 background: #4f46e5;
204 }
205
206 .wb-desc-suggest-btn:disabled {
207 background: #9ca3af;
208 cursor: not-allowed;
209 }
210
211 .wb-desc-suggested-keywords {
212 margin-top: 12px;
213 padding: 12px;
214 background: #f9fafb;
215 border-radius: 8px;
216 border: 1px solid #e5e7eb;
217 }
218
219 .wb-desc-suggested-header {
220 font-weight: 500;
221 margin-bottom: 8px;
222 color: #374151;
223 font-size: 15px;
224 }
225
226 .wb-desc-checkbox-group {
227 display: grid;
228 grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
229 gap: 8px;
230 max-height: 200px;
231 overflow-y: auto;
232 }
233
234 .wb-desc-checkbox-item {
235 display: flex;
236 align-items: center;
237 gap: 8px;
238 }
239
240 .wb-desc-checkbox-item input[type="checkbox"] {
241 cursor: pointer;
242 }
243
244 .wb-desc-checkbox-item label {
245 cursor: pointer;
246 font-size: 15px;
247 color: #374151;
248 margin: 0;
249 }
250
251 .wb-desc-usage-link {
252 color: #6366f1;
253 text-decoration: underline;
254 cursor: pointer;
255 font-size: 14px;
256 }
257
258 .wb-desc-usage-link:hover {
259 color: #4f46e5;
260 }
261
262 .wb-desc-analytics-modal {
263 position: fixed;
264 top: 0;
265 left: 0;
266 width: 100%;
267 height: 100%;
268 background: rgba(0, 0, 0, 0.5);
269 display: flex;
270 align-items: center;
271 justify-content: center;
272 z-index: 10001;
273 }
274
275 .wb-desc-analytics-content {
276 background: white;
277 border-radius: 12px;
278 padding: 24px;
279 max-width: 800px;
280 width: 90%;
281 max-height: 80vh;
282 overflow-y: auto;
283 box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
284 }
285
286 .wb-desc-query-item {
287 padding: 8px 12px;
288 margin-bottom: 4px;
289 border-radius: 6px;
290 font-size: 15px;
291 display: flex;
292 justify-content: space-between;
293 align-items: center;
294 }
295
296 .wb-desc-query-item.used {
297 background: #d1fae5;
298 color: #065f46;
299 }
300
301 .wb-desc-query-item.unused {
302 background: #f3f4f6;
303 color: #6b7280;
304 }
305
306 .wb-desc-query-text {
307 flex: 1;
308 }
309
310 .wb-desc-query-popularity {
311 font-weight: 600;
312 margin-left: 12px;
313 }
314 `);
315
316 // Добавьте в начало скрипта:
317 let lastUrl = location.href;
318
319 // Мониторим изменения URL (для SPA)
320 new MutationObserver(() => {
321 const url = location.href;
322 if (url !== lastUrl) {
323 lastUrl = url;
324 console.log('Wildberries Description Generator: URL изменился');
325
326 // Если перешли на страницу товара - инициализируем
327 if (url.includes('seller.wildberries.ru/new-goods/card')) {
328 setTimeout(init, 1000);
329 }
330 }
331 }).observe(document, { subtree: true, childList: true });
332
333 // Также отслеживаем history API
334 const originalPushState = history.pushState;
335 history.pushState = function() {
336 originalPushState.apply(this, arguments);
337 setTimeout(() => {
338 if (location.href.includes('seller.wildberries.ru/new-goods/card')) {
339 console.log('Wildberries Description Generator: History API навигация');
340 setTimeout(init, 500);
341 }
342 }, 100);
343 };
344
345 // Функция для создания кнопки генератора
346 function createGeneratorButton() {
347 const descriptionHeader = document.querySelector('.Description-header__zK-9sKs8RX');
348
349 if (!descriptionHeader) {
350 console.log('Wildberries Description Generator: Заголовок описания не найден');
351 return;
352 }
353
354 // Проверяем, не добавлена ли уже кнопка
355 if (document.querySelector('.wb-desc-generator-btn')) {
356 console.log('Wildberries Description Generator: Кнопка уже добавлена');
357 return;
358 }
359
360 const button = document.createElement('button');
361 button.className = 'wb-desc-generator-btn';
362 button.textContent = 'Генератор описаний';
363 button.type = 'button';
364 button.addEventListener('click', function() {
365 // Автоматически открываем ингредиенты
366 const expandButton = document.querySelector('div.Characteristics__expand__570w3PkC7D button');
367 if (expandButton) expandButton.click();
368
369 // Открываем модальное окно
370 setTimeout(openModal, 500);
371 });
372
373 descriptionHeader.appendChild(button);
374 console.log('Wildberries Description Generator: Кнопка добавлена');
375 }
376
377 // Функция для открытия модального окна
378 function openModal() {
379
380 console.log('Wildberries Description Generator: Открытие модального окна');
381
382 const modal = document.createElement('div');
383 modal.className = 'wb-desc-modal';
384 modal.innerHTML = `
385 <div class="wb-desc-modal-content">
386 <div class="wb-desc-modal-header">Генератор описаний для Wildberries</div>
387
388 <div class="wb-desc-input-group">
389 <label class="wb-desc-label">Введите ключевые слова (каждое с новой строки):</label>
390 <textarea class="wb-desc-textarea" id="wb-keywords-input" placeholder="Например: витамины иммунитет здоровье"></textarea>
391 <button class="wb-desc-suggest-btn" id="wb-suggest-keywords-btn">Предложить дополнительные ключи</button>
392 <div id="wb-suggested-keywords-container" style="display: none;"></div>
393 </div>
394
395 <div class="wb-desc-input-group">
396 <label class="wb-desc-label">Минус-слова (каждое с новой строки):</label>
397 <textarea class="wb-desc-textarea" style="min-height: 80px;" id="wb-minus-words-input" placeholder="Например: mixit nivea магнит"></textarea>
398 </div>
399
400 <div id="wb-desc-result-container" style="display: none;">
401 <div class="wb-desc-label">Сгенерированное описание:</div>
402 <div class="wb-desc-result" id="wb-desc-result"></div>
403 <div class="wb-desc-char-count" id="wb-char-count"></div>
404 </div>
405
406 <div id="wb-desc-status-container"></div>
407
408 <div class="wb-desc-buttons">
409 <button class="wb-desc-btn wb-desc-btn-secondary" id="wb-close-btn">Закрыть</button>
410 <button class="wb-desc-btn wb-desc-btn-primary" id="wb-generate-btn">Сгенерировать</button>
411 <button class="wb-desc-btn wb-desc-btn-primary" id="wb-regenerate-btn" style="display: none;">Перегенерировать</button>
412 <button class="wb-desc-btn wb-desc-btn-success" id="wb-insert-btn" style="display: none;">Вставить в описание</button>
413 </div>
414 </div>
415 `;
416
417 document.body.appendChild(modal);
418
419 // Обработчики событий
420 modal.addEventListener('click', (e) => {
421 if (e.target === modal) {
422 modal.remove();
423 }
424 });
425
426 document.getElementById('wb-close-btn').addEventListener('click', () => {
427 modal.remove();
428 });
429
430 document.getElementById('wb-suggest-keywords-btn').addEventListener('click', () => {
431 suggestKeywords(modal);
432 });
433
434 document.getElementById('wb-generate-btn').addEventListener('click', () => {
435 generateDescription(modal);
436 });
437
438 document.getElementById('wb-regenerate-btn').addEventListener('click', () => {
439 generateDescription(modal);
440 });
441
442 document.getElementById('wb-insert-btn').addEventListener('click', () => {
443 insertDescription(modal);
444 });
445 }
446
447 // Функция для предложения дополнительных ключей
448 async function suggestKeywords(modal) {
449 const keywordsInput = document.getElementById('wb-keywords-input');
450 const suggestBtn = document.getElementById('wb-suggest-keywords-btn');
451 const suggestedContainer = document.getElementById('wb-suggested-keywords-container');
452 const statusContainer = document.getElementById('wb-desc-status-container');
453
454 const keywordsText = keywordsInput.value.trim();
455
456 if (!keywordsText) {
457 showStatus(statusContainer, 'Пожалуйста, сначала введите базовые ключевые слова', 'error');
458 return;
459 }
460
461 const keywords = keywordsText.split('\n').map(k => k.trim()).filter(k => k);
462
463 suggestBtn.disabled = true;
464 showStatus(statusContainer, 'AI анализирует товар и подбирает ключи...', 'info');
465
466 try {
467 // Получаем информацию о товаре
468 const productInfo = getProductInfo();
469
470 // Запрос к AI для предложения ключей
471 const suggestPrompt = `Проанализируй товар и предложи дополнительные ключевые слова для сбора аналитики Wildberries.
472
473ДАННЫЕ О ТОВАРЕ:
474• Название: ${productInfo.title || 'не указано'}
475• Состав: ${productInfo.composition || 'не указан'}
476• Базовые ключи: ${keywords.join(', ')}
477
478ЗАДАЧА:
479Предложи 10-15 дополнительных релевантных ключевых слов для поиска в аналитике Wildberries.
480
481ВАЖНО:
482- Предлагай СИНОНИМЫ и АЛЬТЕРНАТИВНЫЕ названия товара
483- НЕ расширяй базовый ключ (например, если "масло для загара" → НЕ предлагай "сухое масло для загара")
484- Предлагай ДРУГИЕ типы продуктов с тем же назначением
485- Учитывай состав и назначение товара
486- НЕ предлагай бренды или названия конкурентов
487- Ключи должны быть короткими (1-4 слова)
488
489ПРИМЕРЫ ПРАВИЛЬНЫХ предложений:
490- Базовый ключ: "масло для загара" → Предложи: "средство для загара", "лошон для загара", "усилитель загара", "активатор загара", "крем для загара"
491- Базовый ключ: "пенка для умывания" → Предложи: "средство для умывания", "гель для умывания", "для умывания лица", "очищающее средство"
492- Базовый ключ: "тестостерон" → Предложи: "для тестостерона", "повышение тестостерона", "бустер тестостерона", "тестобустер"
493- Базовый ключ: "витамины" → Предложи: "витаминный комплекс", "мультивитамины", "бад", "добавка"
494
495ПРИМЕРЫ НЕПРАВИЛЬНЫХ предложений (НЕ ДЕЛАЙ ТАК):
496- Базовый ключ: "масло для загара" → ❌ "сухое масло для загара", "натуральное масло для загара" (это расширение базового ключа)
497- Базовый ключ: "пенка для умывания" → ❌ "косметика", "уход за кожей", "красота" (слишком общие)
498- Базовый ключ: "тестостерон" → ❌ "здоровье", "бады" (слишком общие)
499
500ВАЖНО:
501- Предлагай ДРУГИЕ способы назвать тот же товар
502- Думай как покупатель: "Как еще можно назвать этот товар?"
503- НЕ добавляй прилагательные к базовому ключу
504
505Верни ТОЛЬКО список ключей в формате JSON:
506{
507 "keywords": ["ключ 1", "ключ 2", "ключ 3", ...]
508}
509
510НЕ ПИШИ ничего кроме JSON. Начни ответ сразу с {`;
511
512 console.log('Wildberries Description Generator: Запрос предложений ключей от AI');
513
514 const suggestResponse = await RM.aiCall(suggestPrompt);
515
516 // Парсим ответ
517 let suggestedKeywords = [];
518 try {
519 const suggestData = JSON.parse(suggestResponse);
520 suggestedKeywords = suggestData.keywords || [];
521 console.log(`Wildberries Description Generator: AI предложил ${suggestedKeywords.length} ключей`);
522 } catch (e) {
523 console.error('Wildberries Description Generator: Ошибка парсинга предложенных ключей:', e);
524 showStatus(statusContainer, 'Ошибка при получении предложений. Попробуйте еще раз.', 'error');
525 return;
526 }
527
528 if (suggestedKeywords.length === 0) {
529 showStatus(statusContainer, 'AI не смог предложить дополнительные ключи', 'error');
530 return;
531 }
532
533 // Показываем предложенные ключи с чекбоксами
534 suggestedContainer.innerHTML = `
535 <div class="wb-desc-suggested-keywords">
536 <div class="wb-desc-suggested-header">
537 Предложенные ключи (выберите нужные):
538 <button class="wb-desc-suggest-btn" id="wb-toggle-all-btn" style="margin-left: 12px; padding: 4px 12px; font-size: 12px;">Выделить все</button>
539 </div>
540 <div class="wb-desc-checkbox-group">
541 ${suggestedKeywords.map((keyword, index) => `
542 <div class="wb-desc-checkbox-item">
543 <input type="checkbox" id="wb-suggested-${index}" value="${keyword}">
544 <label for="wb-suggested-${index}">${keyword}</label>
545 </div>
546 `).join('')}
547 </div>
548 </div>
549 `;
550
551 suggestedContainer.style.display = 'block';
552
553 // Добавляем обработчик для кнопки "Выделить все"
554 const toggleAllBtn = document.getElementById('wb-toggle-all-btn');
555 toggleAllBtn.addEventListener('click', () => {
556 const checkboxes = document.querySelectorAll('#wb-suggested-keywords-container input[type="checkbox"]');
557 const allChecked = Array.from(checkboxes).every(cb => cb.checked);
558
559 checkboxes.forEach(cb => {
560 cb.checked = !allChecked;
561 });
562
563 toggleAllBtn.textContent = allChecked ? 'Выделить все' : 'Снять выделение';
564 });
565
566 showStatus(statusContainer, `AI предложил ${suggestedKeywords.length} дополнительных ключей. Выберите нужные и нажмите "Сгенерировать"`, 'success');
567
568 } catch (error) {
569 console.error('Wildberries Description Generator: Ошибка при предложении ключей:', error);
570 showStatus(statusContainer, 'Ошибка при получении предложений. Попробуйте еще раз.', 'error');
571 } finally {
572 suggestBtn.disabled = false;
573 }
574 }
575
576 // Функция для сбора данных с аналитики
577 async function collectAnalyticsData(keywords, minusWords) {
578 console.log('Wildberries Description Generator: Начало сбора данных с аналитики');
579
580 // Сохраняем данные для доступа из другой вкладки
581 await GM.setValue('wb_keywords_to_process', JSON.stringify(keywords));
582 await GM.setValue('wb_minus_words', JSON.stringify(minusWords));
583 await GM.setValue('wb_analytics_data', JSON.stringify([]));
584 await GM.setValue('wb_collection_status', 'pending');
585
586 // Открываем страницу аналитики в новой вкладке
587 const analyticsUrl = 'https://seller.wildberries.ru/search-analytics/popular-search-queries';
588 await GM.openInTab(analyticsUrl, false);
589
590 console.log('Wildberries Description Generator: Открыта страница аналитики, ожидание сбора данных...');
591
592 // Ждем завершения сбора данных (максимум 5 минут)
593 const maxWaitTime = 300000; // 5 минут
594 const checkInterval = 2000; // проверяем каждые 2 секунды
595 let waitedTime = 0;
596
597 while (waitedTime < maxWaitTime) {
598 await new Promise(resolve => setTimeout(resolve, checkInterval));
599 waitedTime += checkInterval;
600
601 const status = await GM.getValue('wb_collection_status', 'pending');
602
603 if (status === 'completed') {
604 const analyticsDataStr = await GM.getValue('wb_analytics_data', '[]');
605 const analyticsData = JSON.parse(analyticsDataStr);
606 console.log('Wildberries Description Generator: Данные успешно собраны');
607 return analyticsData;
608 } else if (status === 'error') {
609 console.error('Wildberries Description Generator: Ошибка при сборе данных');
610 return [];
611 }
612 }
613
614 console.error('Wildberries Description Generator: Превышено время ожидания сбора данных');
615 return [];
616 }
617
618 // Функция для автоматического сбора данных на странице аналитики
619 async function autoCollectOnAnalyticsPage() {
620 // Проверяем, что мы на странице аналитики
621 if (!window.location.href.includes('seller.wildberries.ru/search-analytics/popular-search-queries')) {
622 return;
623 }
624
625 console.log('Wildberries Description Generator: Обнаружена страница аналитики');
626
627 // Проверяем, есть ли задача на сбор данных
628 const status = await GM.getValue('wb_collection_status', 'none');
629 if (status !== 'pending') {
630 return;
631 }
632
633 console.log('Wildberries Description Generator: Начинаем автоматический сбор данных');
634
635 try {
636 const keywordsStr = await GM.getValue('wb_keywords_to_process', '[]');
637 const minusWordsStr = await GM.getValue('wb_minus_words', '[]');
638 const productAnalysisStr = await GM.getValue('wb_product_analysis', '{}');
639 const keywords = JSON.parse(keywordsStr);
640 const minusWords = JSON.parse(minusWordsStr);
641 const productAnalysis = JSON.parse(productAnalysisStr);
642
643 console.log('Wildberries Description Generator: AI-критерии фильтрации:', productAnalysis);
644
645 const analyticsData = [];
646
647 // Ждем загрузки страницы
648 await new Promise(resolve => setTimeout(resolve, 3000));
649
650 for (const keyword of keywords) {
651 console.log(`Wildberries Description Generator: Обработка ключевого слова: ${keyword}`);
652
653 try {
654 // Находим поле поиска
655 const searchInput = document.querySelector('input[name="searchString"]');
656 if (!searchInput) {
657 console.error('Wildberries Description Generator: Поле поиска не найдено');
658 continue;
659 }
660
661 // Очищаем и вводим ключевое слово
662 searchInput.value = '';
663 searchInput.focus();
664 searchInput.value = keyword;
665 searchInput.dispatchEvent(new Event('input', { bubbles: true }));
666 searchInput.dispatchEvent(new Event('change', { bubbles: true }));
667
668 // Ждем загрузки результатов
669 await new Promise(resolve => setTimeout(resolve, 5000));
670
671 // Собираем данные из таблицы
672 const rows = document.querySelectorAll('table tbody tr');
673 const keywordData = {
674 keyword: keyword,
675 queries: []
676 };
677
678 console.log(`Wildberries Description Generator: Найдено строк в таблице: ${rows.length}`);
679
680 rows.forEach(row => {
681 const cells = row.querySelectorAll('td');
682 if (cells.length >= 2) {
683 const query = cells[0]?.textContent?.trim();
684 const popularity = cells[1]?.textContent?.trim();
685
686 if (query && popularity) {
687 const queryLower = query.toLowerCase();
688
689 // 1. Фильтруем по минус-словам
690 const hasMinusWord = minusWords.some(minusWord =>
691 queryLower.includes(minusWord.toLowerCase())
692 );
693
694 if (hasMinusWord) {
695 console.log(`Wildberries Description Generator: Исключен запрос "${query}" (содержит минус-слово)`);
696 return;
697 }
698
699 // 2. Автоматическая фильтрация брендов и стран
700 const autoBanList = [
701 'mixit', 'axis', 'nivea', 'garnier', 'loreal', 'maybelline', 'vichy', 'bioderma',
702 'эвалар', 'солгар', 'now foods', 'доппельгерц', 'артнео', 'гельтек',
703 'корея', 'корейск', 'япония', 'японск', 'франция', 'французск', 'америк', 'китай', 'китайск',
704 'купить', 'цена', 'отзыв', 'инструкция', 'доставка'
705 ];
706
707 const hasAutoBan = autoBanList.some(banned =>
708 queryLower.includes(banned)
709 );
710
711 if (hasAutoBan) {
712 console.log(`Wildberries Description Generator: Исключен запрос "${query}" (автофильтр: бренд/страна)`);
713 return;
714 }
715
716 // 3. УМНАЯ фильтрация английских слов с использованием AI-критериев
717 const hasEnglish = /[a-z]/i.test(query);
718
719 if (hasEnglish) {
720 // Проверяем, разрешено ли это английское слово по AI-критериям
721 const allowedWords = productAnalysis.allowed_english_words || [];
722 const isAllowedEnglish = allowedWords.some(allowed =>
723 queryLower.includes(allowed.toLowerCase())
724 );
725
726 if (!isAllowedEnglish) {
727 // Проверяем, похож ли запрос на базовый ключ (с учетом замены латиницы)
728 const normalizeText = (text) => {
729 const latinToCyrillic = {
730 'a': 'а', 'A': 'А', 'e': 'е', 'E': 'Е', 'o': 'о', 'O': 'О',
731 'p': 'р', 'P': 'Р', 'c': 'с', 'C': 'С', 'y': 'у', 'Y': 'У',
732 'x': 'х', 'X': 'Х', 'k': 'к', 'K': 'К', 'h': 'н', 'H': 'Н',
733 'b': 'в', 'B': 'В', 'm': 'м', 'M': 'М', 't': 'т', 'T': 'Т'
734 };
735 return text.split('').map(char => latinToCyrillic[char] || char).join('').toLowerCase();
736 };
737
738 const normalizedQuery = normalizeText(query);
739 const isSimilarToUserKeyword = keywords.some(k => {
740 const normalizedKeyword = normalizeText(k);
741 return normalizedQuery === normalizedKeyword;
742 });
743
744 if (!isSimilarToUserKeyword) {
745 console.log(`Wildberries Description Generator: Исключен запрос "${query}" (содержит неразрешенные английские слова)`);
746 return;
747 }
748 } else {
749 console.log(`Wildberries Description Generator: Разрешен запрос "${query}" (содержит разрешенное английское слово)`);
750 }
751 }
752
753 // 4. УМНАЯ фильтрация по назначению с использованием AI-критериев
754 const excludedPurposes = productAnalysis.excluded_purposes || [];
755 const hasExcludedPurpose = excludedPurposes.some(excluded =>
756 queryLower.includes(excluded.toLowerCase())
757 );
758
759 if (hasExcludedPurpose) {
760 console.log(`Wildberries Description Generator: Исключен запрос "${query}" (неподходящее назначение по AI-критериям)`);
761 return;
762 }
763
764 // Если все проверки пройдены - добавляем запрос
765 keywordData.queries.push({
766 query,
767 popularity
768 });
769 }
770 }
771 });
772
773 analyticsData.push(keywordData);
774 console.log(`Wildberries Description Generator: Собрано ${keywordData.queries.length} запросов для "${keyword}"`);
775
776 } catch (error) {
777 console.error(`Wildberries Description Generator: Ошибка при обработке ключевого слова "${keyword}":`, error);
778 }
779 }
780
781 // Сохраняем собранные данные
782 await GM.setValue('wb_analytics_data', JSON.stringify(analyticsData));
783 await GM.setValue('wb_collection_status', 'completed');
784
785 console.log('Wildberries Description Generator: Сбор данных завершен, можно закрыть вкладку');
786
787 // Закрываем вкладку через 2 секунды
788 setTimeout(() => {
789 window.close();
790 }, 2000);
791
792 } catch (error) {
793 console.error('Wildberries Description Generator: Ошибка при автоматическом сборе данных:', error);
794 await GM.setValue('wb_collection_status', 'error');
795 }
796 }
797
798 // Функция для получения информации о товаре со страницы
799 function getProductInfo() {
800 console.log('Wildberries Description Generator: Сбор информации о товаре');
801
802 const productInfo = {
803 title: '',
804 currentDescription: '',
805 composition: '',
806 attributes: []
807 };
808
809 // 1. Получаем текущее описание
810 const descriptionTextarea = document.querySelector('textarea[data-testid="card-form-main-field-description"]');
811 if (descriptionTextarea) {
812 productInfo.currentDescription = descriptionTextarea.value || '';
813 console.log('Wildberries Description Generator: Текущее описание найдено');
814 }
815
816 // 2. Получаем название товара - ИСПРАВЛЕННЫЙ ПОИСК
817 // Способ 1: По id "editable-title" (contenteditable)
818 const editableTitle = document.querySelector('#editable-title');
819 if (editableTitle) {
820 // Для contenteditable элементов берем textContent или innerText
821 productInfo.title = editableTitle.textContent || editableTitle.innerText || '';
822 console.log('Wildberries Description Generator: Название найдено по #editable-title');
823 }
824
825 // Способ 2: Поиск по label "Наименование"
826 if (!productInfo.title) {
827 // Находим label с текстом "Наименование"
828 const labels = document.querySelectorAll('label, span, div');
829 const nameLabel = Array.from(labels).find(el =>
830 el.textContent && el.textContent.trim() === 'Наименование'
831 );
832
833 if (nameLabel) {
834 console.log('Wildberries Description Generator: Найден label "Наименование"');
835 // Ищем поле ввода рядом с label
836 const parent = nameLabel.closest('div, .field-wrapper, .form-group');
837 if (parent) {
838 // Ищем input или contenteditable
839 const input = parent.querySelector('input, textarea, [contenteditable="true"]');
840 if (input) {
841 if (input.tagName === 'INPUT' || input.tagName === 'TEXTAREA') {
842 productInfo.title = input.value || '';
843 } else {
844 productInfo.title = input.textContent || input.innerText || '';
845 }
846 console.log('Wildberries Description Generator: Название найдено рядом с label');
847 }
848 }
849 }
850 }
851
852 // Способ 3: Общий поиск input
853 if (!productInfo.title) {
854 const nameInput = document.querySelector('input[name*="name"], input[placeholder*="Название"], input[placeholder*="наименование"]');
855 if (nameInput) {
856 productInfo.title = nameInput.value || '';
857 console.log('Wildberries Description Generator: Название найдено через общий поиск');
858 }
859 }
860
861 // 3. Получаем состав товара
862 const compositionBlock = document.querySelector('div#Состав');
863 if (compositionBlock) {
864 console.log('Wildberries Description Generator: Найден блок "Состав"');
865
866 let ingredients = [];
867
868 // Способ 1: По data-testid
869 const byTestId = compositionBlock.querySelectorAll('[data-testid^="undefined-select-item-"]');
870 if (byTestId.length > 0) {
871 ingredients = Array.from(byTestId).map(el => el.textContent.trim()).filter(Boolean);
872 console.log(`Wildberries Description Generator: Извлечено ${ingredients.length} ингредиентов по data-testid`);
873 }
874 // Способ 2: По классу
875 else {
876 const byClass = compositionBlock.querySelectorAll('.Selected-item__text__6P8EDRPmWD');
877 if (byClass.length > 0) {
878 ingredients = Array.from(byClass).map(el => el.textContent.trim()).filter(Boolean);
879 console.log(`Wildberries Description Generator: Извлечено ${ingredients.length} ингредиентов по классу`);
880 }
881 }
882
883 // Способ 3: Поиск в чипах
884 if (ingredients.length === 0) {
885 const chips = compositionBlock.querySelectorAll('.Selected-item__fIyMG5Li-v, .New-multi-select-input__selected-item__KOh9hWF-7q');
886 chips.forEach(chip => {
887 const text = chip.textContent.trim();
888 const cleanText = text.replace(/remove$/, '').trim();
889 if (cleanText) {
890 ingredients.push(cleanText);
891 }
892 });
893 console.log(`Wildberries Description Generator: Извлечено ${ingredients.length} ингредиентов по чипам`);
894 }
895
896 // Объединяем ингредиенты
897 if (ingredients.length > 0) {
898 productInfo.composition = ingredients.join(', ');
899 console.log('Wildberries Description Generator: Состав товара извлечен');
900 } else {
901 console.log('Wildberries Description Generator: Ингредиенты не найдены в блоке "Состав"');
902 }
903 } else {
904 console.log('Wildberries Description Generator: Блок "Состав" не найден');
905
906 // Старый способ
907 const compositionTextarea = document.querySelector('textarea[placeholder*="Состав"], textarea[name*="composition"]');
908 if (compositionTextarea && compositionTextarea.value) {
909 productInfo.composition = compositionTextarea.value;
910 console.log('Wildberries Description Generator: Состав найден через textarea');
911 }
912 }
913
914 // 4. Дополнительные атрибуты (опционально)
915 try {
916 const attributes = {};
917
918 // Цвет
919 const colorElement = document.querySelector('div#Цвет');
920 if (colorElement) {
921 const colorText = colorElement.querySelector('[data-testid^="undefined-select-item-"]')?.textContent;
922 if (colorText) attributes.color = colorText.trim();
923 }
924
925 // Бренд
926 const brandInput = document.querySelector('input[placeholder*="Бренд"], input[name*="brand"]');
927 if (brandInput && brandInput.value) {
928 attributes.brand = brandInput.value;
929 }
930
931 if (Object.keys(attributes).length > 0) {
932 productInfo.attributes = Object.entries(attributes).map(([key, value]) => `${key}: ${value}`);
933 }
934 } catch (e) {
935 console.log('Wildberries Description Generator: Ошибка при сборе атрибутов:', e);
936 }
937
938 console.log('Wildberries Description Generator: Информация о товаре собрана', {
939 titleLength: productInfo.title.length,
940 descriptionLength: productInfo.currentDescription.length,
941 compositionLength: productInfo.composition.length,
942 attributesCount: productInfo.attributes.length
943 });
944
945 return productInfo;
946 }
947
948 // Функция для анализа использованных ключей и расчета популярности
949 async function analyzeUsedKeywords(description) {
950 console.log('Wildberries Description Generator: Анализ использованных ключей');
951
952 const analyticsDataStr = await GM.getValue('wb_analytics_data', '[]');
953 const analyticsData = JSON.parse(analyticsDataStr);
954
955 const descriptionLower = description.toLowerCase();
956 const usedQueries = [];
957 const usedQueriesSet = new Set(); // Для отслеживания уникальных запросов
958 let totalPopularity = 0;
959
960 // Проходим по всем собранным запросам
961 analyticsData.forEach(keywordData => {
962 keywordData.queries.forEach(queryData => {
963 const query = queryData.query.toLowerCase();
964
965 // Проверяем, используется ли запрос И не был ли уже добавлен
966 if (descriptionLower.includes(query) && !usedQueriesSet.has(query)) {
967 // Парсим популярность (убираем пробелы)
968 const popularityStr = queryData.popularity.replace(/\s/g, '');
969 const popularity = parseInt(popularityStr) || 0;
970
971 usedQueries.push({
972 query: queryData.query,
973 popularity: popularity
974 });
975
976 usedQueriesSet.add(query); // Добавляем в Set для отслеживания
977 totalPopularity += popularity;
978 }
979 });
980 });
981
982 const totalQueriesAvailable = analyticsData.reduce((sum, kd) => sum + kd.queries.length, 0);
983
984 console.log(`Wildberries Description Generator: Использовано ${usedQueries.length} запросов из ${totalQueriesAvailable} доступных`);
985 console.log(`Wildberries Description Generator: Общая популярность: ${totalPopularity}`);
986
987 return {
988 usedQueries,
989 totalPopularity,
990 totalQueriesAvailable
991 };
992 }
993
994 // Функция для форматирования числа с разделителями
995 function formatNumber(num) {
996 return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ' ');
997 }
998
999 // Функция для показа детальной аналитики использования запросов
1000 async function showUsageAnalytics() {
1001 console.log('Wildberries Description Generator: Открытие детальной аналитики');
1002
1003 const description = await GM.getValue('wb_generated_description', '');
1004 const analyticsDataStr = await GM.getValue('wb_analytics_data', '[]');
1005 const analyticsData = JSON.parse(analyticsDataStr);
1006
1007 if (!description || !analyticsData || analyticsData.length === 0) {
1008 alert('Нет данных для анализа');
1009 return;
1010 }
1011
1012 const descriptionLower = description.toLowerCase();
1013
1014 // Собираем все запросы с информацией об использовании
1015 const allQueriesWithUsage = [];
1016 const uniqueQueriesSet = new Set(); // Для отслеживания уникальных запросов
1017
1018 analyticsData.forEach(keywordData => {
1019 keywordData.queries.forEach(queryData => {
1020 const query = queryData.query;
1021 const queryLower = query.toLowerCase();
1022
1023 // Пропускаем если уже добавили этот запрос
1024 if (uniqueQueriesSet.has(queryLower)) {
1025 return;
1026 }
1027
1028 uniqueQueriesSet.add(queryLower);
1029
1030 const popularityStr = queryData.popularity.replace(/\s/g, '');
1031 const popularity = parseInt(popularityStr) || 0;
1032
1033 const isUsed = descriptionLower.includes(queryLower);
1034
1035 allQueriesWithUsage.push({
1036 query: queryData.query,
1037 popularity: popularity,
1038 isUsed: isUsed,
1039 keyword: keywordData.keyword
1040 });
1041 });
1042 });
1043
1044 // Сортируем: сначала использованные, потом по популярности
1045 allQueriesWithUsage.sort((a, b) => {
1046 if (a.isUsed && !b.isUsed) return -1;
1047 if (!a.isUsed && b.isUsed) return 1;
1048 return b.popularity - a.popularity;
1049 });
1050
1051 const usedCount = allQueriesWithUsage.filter(q => q.isUsed).length;
1052 const unusedCount = allQueriesWithUsage.filter(q => !q.isUsed).length;
1053
1054 // Создаем модальное окно с аналитикой
1055 const analyticsModal = document.createElement('div');
1056 analyticsModal.className = 'wb-desc-analytics-modal';
1057 analyticsModal.innerHTML = `
1058 <div class="wb-desc-analytics-content">
1059 <div class="wb-desc-modal-header">Детальная аналитика использования запросов</div>
1060
1061 <div style="margin-bottom: 16px;">
1062 <input type="text" id="wb-search-queries" placeholder="Поиск по запросам..." style="width: 100%; padding: 10px; border: 1px solid #d1d5db; border-radius: 8px; font-size: 15px;">
1063 </div>
1064
1065 <div style="margin-bottom: 16px; font-size: 15px;">
1066 <div style="color: #065f46; font-weight: 600;">✅ Использовано: <span id="wb-used-count">${usedCount}</span> запросов</div>
1067 <div style="color: #6b7280; font-weight: 600;">❌ Не использовано: <span id="wb-unused-count">${unusedCount}</span> запросов</div>
1068 <div style="margin-top: 12px; color: #6b7280; font-size: 14px;">
1069 Нажмите ❌ чтобы исключить запрос или ➕ чтобы добавить
1070 </div>
1071 </div>
1072
1073 <div id="wb-queries-list" style="max-height: 500px; overflow-y: auto;">
1074 ${allQueriesWithUsage.map((item, index) => `
1075 <div class="wb-desc-query-item ${item.isUsed ? 'used' : 'unused'}" data-query="${item.query}" data-index="${index}" data-used="${item.isUsed}">
1076 <div style="display: flex; align-items: center; gap: 8px; flex: 1;">
1077 <button class="wb-query-toggle-btn" style="background: none; border: none; cursor: pointer; font-size: 18px; padding: 0; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center;">
1078 ${item.isUsed ? '❌' : '➕'}
1079 </button>
1080 <div class="wb-desc-query-text">
1081 ${item.isUsed ? '✅ ' : '❌ '} ${item.query}
1082 </div>
1083 </div>
1084 <div class="wb-desc-query-popularity">${formatNumber(item.popularity)}</div>
1085 </div>
1086 `).join('')}
1087 </div>
1088
1089 <div class="wb-desc-buttons">
1090 <button class="wb-desc-btn wb-desc-btn-secondary" id="wb-close-analytics-btn">Закрыть</button>
1091 <button class="wb-desc-btn wb-desc-btn-primary" id="wb-regenerate-with-changes-btn" style="display: none;">Перегенерировать</button>
1092 </div>
1093 </div>
1094 `;
1095
1096 document.body.appendChild(analyticsModal);
1097
1098 // Отслеживаем изменения
1099 let hasChanges = false;
1100 const originalState = new Map();
1101 allQueriesWithUsage.forEach((item, index) => {
1102 originalState.set(index, item.isUsed);
1103 });
1104
1105 // Обработчик для поиска
1106 const searchInput = document.getElementById('wb-search-queries');
1107 searchInput.addEventListener('input', (e) => {
1108 const searchTerm = e.target.value.toLowerCase();
1109 const queryItems = analyticsModal.querySelectorAll('.wb-desc-query-item');
1110
1111 queryItems.forEach(item => {
1112 const query = item.dataset.query.toLowerCase();
1113 if (query.includes(searchTerm)) {
1114 item.style.display = 'flex';
1115 } else {
1116 item.style.display = 'none';
1117 }
1118 });
1119 });
1120
1121 // Обработчик для кнопок переключения
1122 const regenerateBtn = document.getElementById('wb-regenerate-with-changes-btn');
1123 const usedCountSpan = document.getElementById('wb-used-count');
1124 const unusedCountSpan = document.getElementById('wb-unused-count');
1125
1126 analyticsModal.querySelectorAll('.wb-query-toggle-btn').forEach((btn) => {
1127 btn.addEventListener('click', () => {
1128 const queryItem = btn.closest('.wb-desc-query-item');
1129 const isCurrentlyUsed = queryItem.dataset.used === 'true';
1130 const newUsedState = !isCurrentlyUsed;
1131
1132 // Обновляем состояние
1133 queryItem.dataset.used = newUsedState;
1134
1135 // Обновляем визуал
1136 if (newUsedState) {
1137 queryItem.classList.remove('unused');
1138 queryItem.classList.add('used');
1139 btn.textContent = '❌';
1140 queryItem.querySelector('.wb-desc-query-text').innerHTML =
1141 '✅ ' + queryItem.dataset.query;
1142 } else {
1143 queryItem.classList.remove('used');
1144 queryItem.classList.add('unused');
1145 btn.textContent = '➕';
1146 queryItem.querySelector('.wb-desc-query-text').innerHTML =
1147 '❌ ' + queryItem.dataset.query;
1148 }
1149
1150 // Обновляем счетчики
1151 const currentUsedCount = analyticsModal.querySelectorAll('.wb-desc-query-item[data-used="true"]').length;
1152 const currentUnusedCount = allQueriesWithUsage.length - currentUsedCount;
1153 usedCountSpan.textContent = currentUsedCount;
1154 unusedCountSpan.textContent = currentUnusedCount;
1155
1156 // Проверяем, есть ли изменения
1157 hasChanges = false;
1158 analyticsModal.querySelectorAll('.wb-desc-query-item').forEach((item, idx) => {
1159 const currentState = item.dataset.used === 'true';
1160 const originalStateValue = originalState.get(idx);
1161 if (currentState !== originalStateValue) {
1162 hasChanges = true;
1163 }
1164 });
1165
1166 // Показываем/скрываем кнопку перегенерации
1167 if (hasChanges) {
1168 regenerateBtn.style.display = 'inline-block';
1169 } else {
1170 regenerateBtn.style.display = 'none';
1171 }
1172 });
1173 });
1174
1175 // Обработчик для перегенерации с изменениями
1176 regenerateBtn.addEventListener('click', async () => {
1177 // Собираем список запросов для исключения (те, которые были использованы, но теперь отключены)
1178 const excludedQueries = [];
1179 const includedQueries = [];
1180
1181 analyticsModal.querySelectorAll('.wb-desc-query-item').forEach((item, index) => {
1182 const query = item.dataset.query;
1183 const isCurrentlyUsed = item.dataset.used === 'true';
1184 const wasOriginallyUsed = originalState.get(index);
1185
1186 // Если был использован, но теперь отключен - исключаем
1187 if (wasOriginallyUsed && !isCurrentlyUsed) {
1188 excludedQueries.push(query);
1189 }
1190
1191 // Если не был использован, но теперь включен - добавляем в приоритет
1192 if (!wasOriginallyUsed && isCurrentlyUsed) {
1193 includedQueries.push(query);
1194 }
1195 });
1196
1197 console.log(`Wildberries Description Generator: Исключено ${excludedQueries.length} запросов, добавлено ${includedQueries.length} запросов`);
1198
1199 // Сохраняем изменения
1200 await GM.setValue('wb_excluded_queries', JSON.stringify(excludedQueries));
1201 await GM.setValue('wb_included_queries', JSON.stringify(includedQueries));
1202
1203 // Закрываем аналитику
1204 analyticsModal.remove();
1205
1206 // Запускаем перегенерацию БЕЗ открытия нового модального окна
1207 await regenerateWithExclusions();
1208 });
1209
1210 // Обработчики событий
1211 analyticsModal.addEventListener('click', (e) => {
1212 if (e.target === analyticsModal) {
1213 analyticsModal.remove();
1214 }
1215 });
1216
1217 document.getElementById('wb-close-analytics-btn').addEventListener('click', () => {
1218 analyticsModal.remove();
1219 });
1220 }
1221
1222 // Функция для генерации описания
1223 async function generateDescription(modal) {
1224 const keywordsInput = document.getElementById('wb-keywords-input');
1225 const minusWordsInput = document.getElementById('wb-minus-words-input');
1226 const generateBtn = document.getElementById('wb-generate-btn');
1227 const regenerateBtn = document.getElementById('wb-regenerate-btn');
1228 const insertBtn = document.getElementById('wb-insert-btn');
1229 const resultContainer = document.getElementById('wb-desc-result-container');
1230 const resultDiv = document.getElementById('wb-desc-result');
1231 const charCountDiv = document.getElementById('wb-char-count');
1232 const statusContainer = document.getElementById('wb-desc-status-container');
1233
1234 const keywordsText = keywordsInput.value.trim();
1235 const minusWordsText = minusWordsInput.value.trim();
1236
1237 if (!keywordsText) {
1238 showStatus(statusContainer, 'Пожалуйста, введите ключевые слова', 'error');
1239 return;
1240 }
1241
1242 let keywords = keywordsText.split('\n').map(k => k.trim()).filter(k => k);
1243 const minusWords = minusWordsText.split('\n').map(k => k.trim().toLowerCase()).filter(k => k);
1244
1245 // Добавляем выбранные дополнительные ключи
1246 const suggestedCheckboxes = document.querySelectorAll('#wb-suggested-keywords-container input[type="checkbox"]:checked');
1247 if (suggestedCheckboxes.length > 0) {
1248 const selectedSuggested = Array.from(suggestedCheckboxes).map(cb => cb.value);
1249 keywords = [...keywords, ...selectedSuggested];
1250 console.log(`Wildberries Description Generator: Добавлено ${selectedSuggested.length} дополнительных ключей`);
1251 }
1252
1253 if (keywords.length === 0) {
1254 showStatus(statusContainer, 'Пожалуйста, введите хотя бы одно ключевое слово', 'error');
1255 return;
1256 }
1257
1258 // Показываем загрузку
1259 generateBtn.disabled = true;
1260 regenerateBtn.style.display = 'none';
1261 insertBtn.style.display = 'none';
1262 resultContainer.style.display = 'none';
1263
1264 // ШАГ 1: AI анализирует товар ДО сбора данных
1265 showStatus(statusContainer, 'AI анализирует товар и создает критерии фильтрации...', 'info');
1266
1267 try {
1268 // Получаем информацию о товаре
1269 const productInfo = getProductInfo();
1270
1271 // AI анализирует товар и создает критерии фильтрации
1272 const analysisPrompt = `Проанализируй товар и создай критерии для умной фильтрации поисковых запросов из аналитики Wildberries.
1273
1274ДАННЫЕ О ТОВАРЕ:
1275• Название: ${productInfo.title || 'не указано'}
1276• Состав: ${productInfo.composition || 'не указан'}
1277• Базовые ключи: ${keywords.join(', ')}
1278
1279ЗАДАЧА:
1280Создай критерии фильтрации, чтобы при сборе данных из аналитики мы НЕ ПОТЕРЯЛИ релевантные запросы.
1281
1282ОПРЕДЕЛИ:
1283
12841. КАТЕГОРИЯ ТОВАРА (одна из):
1285 - косметика_лицо (кремы, сыворотки, маски для лица)
1286 - косметика_волосы (шампуни, маски, масла для волос)
1287 - косметика_тело (кремы для тела, скрабы, масла для тела)
1288 - бад_витамины (витамины, минералы, БАДы)
1289 - бад_спорт (спортивное питание, протеины)
1290 - другое
1291
12922. ЦЕЛЕВАЯ АУДИТОРИЯ:
1293 - для_мужчин / для_женщин / для_детей / универсальный
1294
12953. НАЗНАЧЕНИЕ (основное применение):
1296 - Например: "увлажнение кожи лица", "рост волос", "повышение иммунитета"
1297
12984. КЛЮЧЕВЫЕ КОМПОНЕНТЫ (из состава):
1299 - Список главных активных компонентов
1300
13015. РАЗРЕШЕННЫЕ АНГЛИЙСКИЕ СЛОВА/БРЕНДЫ:
1302 - Если в названии есть английские слова (например, "Elementary"), их НУЖНО разрешить
1303 - Список слов, которые можно оставлять в запросах
1304
13056. ИСКЛЮЧАЕМЫЕ НАЗНАЧЕНИЯ:
1306 - Список назначений, которые НЕ подходят для этого товара
1307 - Например, для "сыворотки для лица" исключить: "сыворотка для роста волос", "сыворотка для ресниц", "сыворотка для тела"
1308 - ВАЖНО: Оставляй общие запросы без уточнения назначения (например, "сыворотка", "витамины")
1309
1310ПРИМЕРЫ:
1311
1312Товар: "Elementary Сыворотка для лица с витамином С"
1313Состав: "Аскорбиновая кислота, Ниацинамид, Гиалуроновая кислота"
1314
1315ПРАВИЛЬНЫЙ ОТВЕТ:
1316{
1317 "category": "косметика_лицо",
1318 "target_audience": "универсальный",
1319 "purpose": "увлажнение и осветление кожи лица",
1320 "key_components": ["витамин с", "аскорбиновая кислота", "ниацинамид", "гиалуроновая кислота"],
1321 "allowed_english_words": ["elementary"],
1322 "excluded_purposes": ["сыворотка для роста волос", "сыворотка для ресниц", "сыворотка для тела"]
1323}
1324
1325Верни ТОЛЬКО JSON в формате выше. НЕ ПИШИ ничего кроме JSON. Начни ответ сразу с {`;
1326
1327 console.log('Wildberries Description Generator: AI анализирует товар');
1328
1329 const analysisResponse = await RM.aiCall(analysisPrompt);
1330
1331 // Парсим ответ
1332 let productAnalysis;
1333 try {
1334 productAnalysis = JSON.parse(analysisResponse);
1335 console.log('Wildberries Description Generator: AI-анализ товара:', productAnalysis);
1336 } catch (e) {
1337 console.error('Wildberries Description Generator: Ошибка парсинга анализа товара:', e);
1338 showStatus(statusContainer, 'Ошибка при анализе товара. Попробуйте еще раз.', 'error');
1339 return;
1340 }
1341
1342 // Сохраняем анализ для использования при сборе данных
1343 await GM.setValue('wb_product_analysis', JSON.stringify(productAnalysis));
1344
1345 // ШАГ 2: Собираем данные с аналитики с умной фильтрацией
1346 showStatus(statusContainer, 'Сбор данных с аналитики (с умной фильтрацией)...', 'info');
1347
1348 // Собираем данные с аналитики
1349 const analyticsData = await collectAnalyticsData(keywords, minusWords);
1350
1351 // Собираем все запросы из аналитики
1352 const allQueries = [];
1353 if (analyticsData && analyticsData.length > 0) {
1354 analyticsData.forEach(data => {
1355 data.queries.forEach(q => {
1356 allQueries.push(q.query);
1357 });
1358 });
1359 }
1360
1361 console.log(`Wildberries Description Generator: Собрано ${allQueries.length} запросов из аналитики`);
1362
1363 // Проверяем, что данные собраны
1364 if (allQueries.length === 0) {
1365 showStatus(statusContainer, 'Не удалось собрать данные с аналитики. Возможно, страница не загрузилась или изменилась структура. Попробуйте еще раз.', 'error');
1366 return;
1367 }
1368
1369 // Проверяем, есть ли исключенные запросы из аналитики
1370 const excludedQueriesStr = await GM.getValue('wb_excluded_queries', '[]');
1371 const excludedQueries = JSON.parse(excludedQueriesStr);
1372
1373 let filteredQueries = allQueries;
1374
1375 if (excludedQueries.length > 0) {
1376 console.log(`Wildberries Description Generator: Исключаем ${excludedQueries.length} запросов по выбору пользователя`);
1377 const excludedLower = excludedQueries.map(q => q.toLowerCase());
1378 filteredQueries = allQueries.filter(q => !excludedLower.includes(q.toLowerCase()));
1379 console.log(`Wildberries Description Generator: После исключения осталось ${filteredQueries.length} запросов`);
1380
1381 // Очищаем список исключенных запросов
1382 await GM.setValue('wb_excluded_queries', '[]');
1383 }
1384
1385 // Генерируем описание с отфильтрованными запросами
1386 showStatus(statusContainer, 'Генерация описания с помощью AI...', 'info');
1387
1388 const descriptionPrompt = `Создай SEO-описание товара для Wildberries (внутренняя SEO-оптимизация).
1389
1390ДАННЫЕ:
1391• Название: ${productInfo.title || 'не указано'}
1392• Состав: ${productInfo.composition || 'не указан'}
1393• Ключевые слова: ${keywords.join(', ')}
1394
1395ЗАПРОСЫ ДЛЯ ИСПОЛЬЗОВАНИЯ (используй МАКСИМУУ из этого списка):
1396${filteredQueries.map((q, i) => `${i+1}. "${q}"`).join('\\n')}
1397
1398=== ЖЕСТКИЕ ТРЕБОВАНИЯ ===
1399
14001. ОБЪЕМ И СТРУКТУРА:
1401 • 3800-4200 символов, только текст описания
1402 • ЕСТЕСТВЕННАЯ СТРУКТУРА: Введение → Основная часть → Практическая часть → Заключение
1403 • НЕ допускай резких переходов между темами
1404
14052. ИСПОЛЬЗОВАНИЕ ЗАПРОСОВ - КРИТИЧЕСКИ ВАЖНО:
1406 • ОБЯЗАТЕЛЬНО используй базовые ключевые слова: ${keywords.join(', ')} - минимум 1 раз каждое
1407 • ТВОЯ ГЛАВНАЯ ЗАДАЧА: Использовать МАКСИМУТ запросов из списка выше
1408 • ЦЕЛЬ: Минимум 80-90% запросов из списка (${Math.floor(filteredQueries.length * 0.8)}-${Math.floor(filteredQueries.length * 0.9)} из ${filteredQueries.length})
1409 • ЭТО НЕ РЕКОМЕНДАЦИЯ - ЭТО ОБЯЗАТЕЛЬНОЕ ТРЕБОВАНИЕ!
1410 • Список запросов выше - это РЕАЛЬНЫЕ поисковые запросы пользователей Wildberries
1411 • Каждый запрос из списка - это возможность попасть в поиск
1412 • ИСПОЛЬЗУЙ запросы ТОЧНО как они написаны, можно только склонять
1413 • Примеры ПРАВИЛЬНОГО использования:
1414 - Запрос "сыворотка для лица от прыщей" → "Эта сыворотка для лица от прыщей содержит..."
1415 - Запрос "сыворотка с ниацинамидом 10%" → "Сыворотка с ниацинамидом 10% помогает..."
1416 - Запрос "средство от акне для подростков" → "Средство от акне для подростков разработано..."
1417 • НЕ БОЙСЯ повторять похожие конструкции - это SEO-текст!
1418 • Плотность: минимум 2.5-3 запроса на 100 символов
1419
14203. ЛОГИКА ЗАМЕНЫ ДЛЯ ОТСУТСТВУЮЩИХ КОМПОНЕНТОВ:
1421 • Если в запросе упоминается компонент, которого НЕТ в составе - используй логику ЗАМЕНЫ
1422 • Пример: Состав содержит "коллаген", запрос "сыворотка с ретинолом" (ретинола нет в составе)
1423 • ПРАВИЛЬНО: "Сыворотка с коллагеном - это отличная замена сывороток с ретинолом и других антивозрастных средств"
1424 • ПРАВИЛЬНО: "Помощь магния может стать альтернативой добавкам с мелатонином для улучшения сна"
1425 • ПРАВИЛЬНО: "Средство с гиалуроновой кислотой заменяет кремы с коллагеном для увлажнения кожи"
1426 • Шаблон: "[Товар с компонентом из состава] - это отличная замена [запрос с отсутствующим компонентом]"
1427 • Это позволяет использовать ВСЕ запросы, даже если компонента нет в составе!
1428
14294. ИСПОЛЬЗОВАНИЕ СОСТАВА:
1430 • ОБЯЗАТЕЛЬНО используй ВСЕ компоненты из состава: ${productInfo.composition}
1431 • Каждый компонент используй 2-4 раз в разных контекстах
1432 • Не перечисляй, а вплетай в повествование
1433 • Объединяй схожие компоненты в группы
1434
14355. ПЛОТНОСТЬ И СВЯЗНОСТЬ:
1436 • Каждые 30-40 символов — новый запрос из списка
1437 • Начинай с самых популярных запросов (они в начале списка)
1438 • Проходи по списку ПОСЛЕДОВАТЕЛЬНО, используя запросы один за другим
1439 • Создавай логические переходы между абзацами
1440 • ВАЖНО: Используй ВСЕ запросы, даже если компонента нет - через логику замены!
1441
14426. СТРУКТУРА ПОВЕСТВОВАНИЯ (разделяй на абзацы):
1443
1444 ЧАСТЬ 1: ВВЕДЕНИЕ (10-15% текста)
1445 • Без заголовка, общее описание товара и его назначения, без слов инновационный, революционный
1446 • Основная проблема, которую решает
1447 • Ключевое преимущество
1448 • ИСПОЛЬЗУЙ 30-40 запросов из списка
1449
1450 ЧАСТЬ 2: ОСНОВНАЯ ЧАСТЬ (60-70% текста)
1451 • Подробно о составе и действии компонентов
1452 • Группировка по темам: восстановление → увлажнение → защита
1453 • Как работает продукт (механизм действия)
1454 • Исследования эффективности, но без утверждений. Например: "Исследования показывают, что ...."
1455 • ИСПОЛЬЗУЙ 80-100 запросов из списка
1456
1457 ЧАСТЬ 3: ПРАКТИЧЕСКАЯ ЧАСТЬ (15-20% текста)
1458 • Для кого подходит (естественный переход через "Благодаря...")
1459 • Когда лучше принимать (время, до / после еды, до / во время тренировок или что то другое) и с какими ещё витаминами сочетается. Не пиши сколько капсул принимать.
1460 • Ожидаемые результаты и преимущества, но без обещаний и эффектов. Например: "Наши покупатели отмечают, что ..."
1461 • ИСПОЛЬЗУЙ 30-40 запросов из списка
1462
1463 ЧАСТЬ 4: ЗАКЛЮЧЕНИЕ (5-10% текста)
1464 • Краткое резюме ключевых преимуществ
1465 • Естественный завершающий акцент без рекомендаций про врачей
1466 • ИСПОЛЬЗУЙ 20-30 запросов из списка
1467
14687. ЗАПРЕТЫ:
1469 ✗ ЛЮБЫЕ английские слова (СТРОГО только русский язык, даже для научных терминов)
1470 ✗ Crucial, essential, vital, key, important, testosterone, energy - переводи на русский: ключевой, важный, существенный, тестостерон, энергия
1471 ✗ Бренды, конкуренты, фамилии, названия компаний
1472 ✗ "Вода": "эликсир", "герой", "ритуал", "скажет спасибо", "настоящий", "буквально"
1473 ✗ Повторы одной фразы (используй синонимы и вариации)
1474 ✗ Инструкционный стиль в конце (никаких "Хранить при температуре...")
1475 ✗ Резкие переходы между темами
1476 ✗ Маркированные списки (•) - используй только текст
1477
14788. ПРОВЕРКА (перед ответом):
1479 ✅ ОБЯЗАТЕЛЬНО использованы ВСЕ базовые ключевые слова: ${keywords.join(', ')} - минимум 1 раз каждое (ПРОВЕРЬ!)
1480 ✅ ОБЯЗАТЕЛЬНО использованы ВСЕ компоненты из состава: ${productInfo.composition} (ПРОВЕРЬ!)
1481 ✅ Использовано ли минимум ${Math.floor(filteredQueries.length * 0.8)} запросов из ${filteredQueries.length} (ПРОВЕРЬ!)
1482 ✅ Нет английских слов (ПРОВЕРЬ на английские слова!)
1483 ✅ Объем 3800-4200 символов
1484 ✅ Плотность: минимум 2.5 запроса на 100 символов
1485 ✅ Естественная структура повествования
1486 ✅ Логичные переходы между абзацами
1487 ✅ ВСЕ слова переведены на русский (crucial → ключевой, testosterone → тестостерон, energy → энергия)
1488
1489=== НАЧНИ ОПИСАНИЕ СРАЗУ ===
1490
1491НЕ ПИШИ вступлений вроде "Я готов помочь" или "Мне нужны данные".
1492НЕ ПРОСИ дополнительные данные.
1493НЕ ЗАДАВАЙ вопросы.
1494НЕ ИСПОЛЬЗУЙ:
1495✗ Слова революционный, инновационный, инвестируйте
1496✗ ЛЮБЫЕ английские слова (crucial, essential, vital, key, testosterone, energy → переводи на русский)
1497✗ Описание неактивных компонентов (тальк, целлюлоза, стеариновая кислота)
1498✗ Не используй заголовки, вопросы и эмоджи !!!
1499✗ Маркированные списки (•)
1500
1501ПРОСТО СГЕНЕРИРУЙ SEO-ОПИСАНИЕ на основе предоставленных данных.
1502
1503ВАЖНО:
15041. Твоя задача - вплести в текст МАКСИМУТ запросов из списка!
15052. Проходи по списку последовательно и используй каждый запрос!
15063. Если компонента из запроса нет в составе - используй логику ЗАМЕНЫ!
1507
1508ПЕРЕД ОТПРАВКОЙ ПРОВЕРЬ:
15091. Все ли базовые ключевые слова (${keywords.join(', ')}) присутствуют в тексте?
15102. Все ли компоненты состава (${productInfo.composition}) упомянуты?
15113. Использовано ли минимум ${Math.floor(filteredQueries.length * 0.8)} запросов из ${filteredQueries.length}?
15124. Нет ли английских слов?
1513
1514Сгенерируй описание:`;
1515
1516 console.log('Wildberries Description Generator: Генерация описания');
1517
1518 // Генерируем описание с помощью AI
1519 const description = await RM.aiCall(descriptionPrompt);
1520
1521 console.log('Wildberries Description Generator: Описание сгенерировано');
1522
1523 // ЖЕСТКАЯ ПОСТОБРАБОТКА: Заменяем английские слова на русские
1524 let cleanedDescription = description;
1525
1526 // Словарь замен английских слов
1527 const englishToRussian = {
1528 'crucial': 'ключевой',
1529 'Crucial': 'Ключевой',
1530 'essential': 'важный',
1531 'Essential': 'Важный',
1532 'vital': 'жизненно важный',
1533 'Vital': 'Жизненно важный',
1534 'key': 'ключевой',
1535 'Key': 'Ключевой',
1536 'important': 'важный',
1537 'Important': 'Важный',
1538 'testosterone': 'тестостерон',
1539 'Testosterone': 'Тестостерон',
1540 'energy': 'энергия',
1541 'Energy': 'Энергия'
1542 };
1543
1544 // Заменяем все английские слова
1545 Object.entries(englishToRussian).forEach(([eng, rus]) => {
1546 const regex = new RegExp('\\b' + eng + '\\b', 'g');
1547 cleanedDescription = cleanedDescription.replace(regex, rus);
1548 });
1549
1550 console.log('Wildberries Description Generator: Постобработка завершена');
1551
1552 // Анализируем использованные ключи
1553 const analysis = await analyzeUsedKeywords(cleanedDescription);
1554
1555 // Проверяем длину
1556 const charCount = cleanedDescription.length;
1557
1558 // Сохраняем описание
1559 await GM.setValue('wb_generated_description', cleanedDescription);
1560
1561 // Показываем результат
1562 resultDiv.textContent = cleanedDescription;
1563 resultContainer.style.display = 'block';
1564
1565 // Обновляем счетчик символов с информацией о ключах (делаем кликабельным)
1566 const popularityInfo = `${charCount} / 5000 символов | <span class="wb-desc-usage-link" id="wb-usage-link">Использовано ${analysis.usedQueries.length} из ${analysis.totalQueriesAvailable} запросов</span> | Общая популярность: ${formatNumber(analysis.totalPopularity)}`;
1567 charCountDiv.innerHTML = popularityInfo;
1568
1569 // Добавляем обработчик клика на ссылку
1570 const usageLink = document.getElementById('wb-usage-link');
1571 if (usageLink) {
1572 usageLink.addEventListener('click', showUsageAnalytics);
1573 }
1574
1575 // Показываем кнопки
1576 generateBtn.style.display = 'none';
1577 regenerateBtn.style.display = 'inline-block';
1578 insertBtn.style.display = 'inline-block';
1579
1580 showStatus(statusContainer, `Описание успешно сгенерировано! (${charCount} символов)`, 'success');
1581
1582 } catch (error) {
1583 console.error('Wildberries Description Generator: Ошибка при генерации описания:', error);
1584 showStatus(statusContainer, 'Ошибка при генерации описания. Попробуйте еще раз.', 'error');
1585 } finally {
1586 generateBtn.disabled = false;
1587 }
1588 }
1589
1590 // Функция для показа статуса
1591 function showStatus(container, message, type) {
1592 container.innerHTML = `<div class="wb-desc-status ${type}">${message}</div>`;
1593 }
1594
1595 // Функция для перегенерации с исключенными запросами
1596 async function regenerateWithExclusions() {
1597 console.log('Wildberries Description Generator: Перегенерация с исключениями');
1598
1599 // Проверяем, открыто ли уже модальное окно
1600 const existingModal = document.querySelector('.wb-desc-modal');
1601 if (existingModal) {
1602 console.log('Wildberries Description Generator: Модальное окно уже открыто, запускаем генерацию');
1603 // Если окно уже открыто, просто запускаем генерацию
1604 const generateBtn = document.getElementById('wb-generate-btn');
1605 if (generateBtn) {
1606 generateBtn.click();
1607 }
1608 return;
1609 }
1610
1611 // Открываем модальное окно
1612 openModal();
1613
1614 // Ждем, пока модальное окно откроется
1615 await new Promise(resolve => setTimeout(resolve, 100));
1616
1617 // Автоматически запускаем генерацию
1618 const generateBtn = document.getElementById('wb-generate-btn');
1619 if (generateBtn) {
1620 generateBtn.click();
1621 }
1622 }
1623
1624 // Функция для вставки описания
1625 async function insertDescription(modal) {
1626 console.log('Wildberries Description Generator: Вставка описания');
1627
1628 try {
1629 const description = await GM.getValue('wb_generated_description', '');
1630
1631 if (!description) {
1632 alert('Описание не найдено. Пожалуйста, сгенерируйте описание сначала.');
1633 return;
1634 }
1635
1636 // Находим поле описания
1637 const descriptionTextarea = document.querySelector('textarea[data-testid="card-form-main-field-description"]');
1638
1639 if (!descriptionTextarea) {
1640 alert('Не удалось найти поле описания. Убедитесь, что вы находитесь на странице редактирования товара.');
1641 return;
1642 }
1643
1644 // Вставляем описание
1645 descriptionTextarea.value = description;
1646 descriptionTextarea.dispatchEvent(new Event('input', { bubbles: true }));
1647 descriptionTextarea.dispatchEvent(new Event('change', { bubbles: true }));
1648
1649 console.log('Wildberries Description Generator: Описание успешно вставлено');
1650
1651 // Закрываем модальное окно
1652 modal.remove();
1653
1654 alert('Описание успешно вставлено!');
1655
1656 } catch (error) {
1657 console.error('Wildberries Description Generator: Ошибка при вставке описания:', error);
1658 alert('Ошибка при вставке описания. Попробуйте еще раз.');
1659 }
1660 }
1661
1662 // Функция для инициализации расширения
1663 function init() {
1664 console.log('Wildberries Description Generator: Инициализация');
1665
1666 // Проверяем, что мы на странице редактирования товара
1667 if (window.location.href.includes('seller.wildberries.ru/new-goods/card')) {
1668
1669 // Ждем загрузки страницы и добавляем кнопку
1670 const observer = new MutationObserver((mutations, obs) => {
1671 const descriptionHeader = document.querySelector('.Description-header__zK-9sKs8RX');
1672 if (descriptionHeader) {
1673 createGeneratorButton();
1674 obs.disconnect();
1675 }
1676 });
1677
1678 observer.observe(document.body, {
1679 childList: true,
1680 subtree: true
1681 });
1682
1683 // Также пробуем добавить кнопку сразу
1684 setTimeout(createGeneratorButton, 2000);
1685 }
1686
1687 // Проверяем, что мы на странице аналитики и запускаем автосбор
1688 if (window.location.href.includes('seller.wildberries.ru/search-analytics/popular-search-queries')) {
1689 setTimeout(autoCollectOnAnalyticsPage, 2000);
1690 }
1691 }
1692
1693 // Запускаем инициализацию
1694 if (document.readyState === 'loading') {
1695 document.addEventListener('DOMContentLoaded', init);
1696 } else {
1697 init();
1698 }
1699
1700})();