Генератор SEO-описаний для товаров на Wildberries с анализом ключевых слов
Size
59.6 KB
Version
2.4.6
Created
Jan 14, 2026
Updated
3 months ago
1// ==UserScript==
2// @name Wildberries Description Generator 2.0
3// @description Генератор SEO-описаний для товаров на Wildberries с анализом ключевых слов
4// @version 2.4.6
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
252 // Добавьте в начало скрипта:
253 let lastUrl = location.href;
254
255 // Мониторим изменения URL (для SPA)
256 new MutationObserver(() => {
257 const url = location.href;
258 if (url !== lastUrl) {
259 lastUrl = url;
260 console.log('Wildberries Description Generator: URL изменился');
261
262 // Если перешли на страницу товара - инициализируем
263 if (url.includes('seller.wildberries.ru/new-goods/card')) {
264 setTimeout(init, 1000);
265 }
266 }
267 }).observe(document, { subtree: true, childList: true });
268
269 // Также отслеживаем history API
270 const originalPushState = history.pushState;
271 history.pushState = function() {
272 originalPushState.apply(this, arguments);
273 setTimeout(() => {
274 if (location.href.includes('seller.wildberries.ru/new-goods/card')) {
275 console.log('Wildberries Description Generator: History API навигация');
276 setTimeout(init, 500);
277 }
278 }, 100);
279 };
280
281 // Функция для создания кнопки генератора
282 function createGeneratorButton() {
283 const descriptionHeader = document.querySelector('.Description-header__zK-9sKs8RX');
284
285 if (!descriptionHeader) {
286 console.log('Wildberries Description Generator: Заголовок описания не найден');
287 return;
288 }
289
290 // Проверяем, не добавлена ли уже кнопка
291 if (document.querySelector('.wb-desc-generator-btn')) {
292 console.log('Wildberries Description Generator: Кнопка уже добавлена');
293 return;
294 }
295
296 const button = document.createElement('button');
297 button.className = 'wb-desc-generator-btn';
298 button.textContent = 'Генератор описаний';
299 button.type = 'button';
300 button.addEventListener('click', function() {
301 // Автоматически открываем ингредиенты
302 const expandButton = document.querySelector('div.Characteristics__expand__570w3PkC7D button');
303 if (expandButton) expandButton.click();
304
305 // Открываем модальное окно
306 setTimeout(openModal, 500);
307 });
308
309 descriptionHeader.appendChild(button);
310 console.log('Wildberries Description Generator: Кнопка добавлена');
311 }
312
313 // Функция для открытия модального окна
314 function openModal() {
315
316 console.log('Wildberries Description Generator: Открытие модального окна');
317
318 const modal = document.createElement('div');
319 modal.className = 'wb-desc-modal';
320 modal.innerHTML = `
321 <div class="wb-desc-modal-content">
322 <div class="wb-desc-modal-header">Генератор описаний для Wildberries</div>
323
324 <div class="wb-desc-input-group">
325 <label class="wb-desc-label">Введите ключевые слова (каждое с новой строки):</label>
326 <textarea class="wb-desc-textarea" id="wb-keywords-input" placeholder="Например: витамины иммунитет здоровье"></textarea>
327 <button class="wb-desc-suggest-btn" id="wb-suggest-keywords-btn">Предложить дополнительные ключи</button>
328 <div id="wb-suggested-keywords-container" style="display: none;"></div>
329 </div>
330
331 <div class="wb-desc-input-group">
332 <label class="wb-desc-label">Минус-слова (каждое с новой строки):</label>
333 <textarea class="wb-desc-textarea" style="min-height: 80px;" id="wb-minus-words-input" placeholder="Например: mixit nivea магнит"></textarea>
334 </div>
335
336 <div id="wb-desc-result-container" style="display: none;">
337 <div class="wb-desc-label">Сгенерированное описание:</div>
338 <div class="wb-desc-result" id="wb-desc-result"></div>
339 <div class="wb-desc-char-count" id="wb-char-count"></div>
340 </div>
341
342 <div id="wb-desc-status-container"></div>
343
344 <div class="wb-desc-buttons">
345 <button class="wb-desc-btn wb-desc-btn-secondary" id="wb-close-btn">Закрыть</button>
346 <button class="wb-desc-btn wb-desc-btn-primary" id="wb-generate-btn">Сгенерировать</button>
347 <button class="wb-desc-btn wb-desc-btn-primary" id="wb-regenerate-btn" style="display: none;">Перегенерировать</button>
348 <button class="wb-desc-btn wb-desc-btn-success" id="wb-insert-btn" style="display: none;">Вставить в описание</button>
349 </div>
350 </div>
351 `;
352
353 document.body.appendChild(modal);
354
355 // Обработчики событий
356 modal.addEventListener('click', (e) => {
357 if (e.target === modal) {
358 modal.remove();
359 }
360 });
361
362 document.getElementById('wb-close-btn').addEventListener('click', () => {
363 modal.remove();
364 });
365
366 document.getElementById('wb-suggest-keywords-btn').addEventListener('click', () => {
367 suggestKeywords(modal);
368 });
369
370 document.getElementById('wb-generate-btn').addEventListener('click', () => {
371 generateDescription(modal);
372 });
373
374 document.getElementById('wb-regenerate-btn').addEventListener('click', () => {
375 generateDescription(modal);
376 });
377
378 document.getElementById('wb-insert-btn').addEventListener('click', () => {
379 insertDescription(modal);
380 });
381 }
382
383 // Функция для предложения дополнительных ключей
384 async function suggestKeywords(modal) {
385 const keywordsInput = document.getElementById('wb-keywords-input');
386 const suggestBtn = document.getElementById('wb-suggest-keywords-btn');
387 const suggestedContainer = document.getElementById('wb-suggested-keywords-container');
388 const statusContainer = document.getElementById('wb-desc-status-container');
389
390 const keywordsText = keywordsInput.value.trim();
391
392 if (!keywordsText) {
393 showStatus(statusContainer, 'Пожалуйста, сначала введите базовые ключевые слова', 'error');
394 return;
395 }
396
397 const keywords = keywordsText.split('\n').map(k => k.trim()).filter(k => k);
398
399 suggestBtn.disabled = true;
400 showStatus(statusContainer, 'AI анализирует товар и подбирает ключи...', 'info');
401
402 try {
403 // Получаем информацию о товаре
404 const productInfo = getProductInfo();
405
406 // Запрос к AI для предложения ключей
407 const suggestPrompt = `Проанализируй товар и предложи дополнительные ключевые слова для сбора аналитики Wildberries.
408
409ДАННЫЕ О ТОВАРЕ:
410• Название: ${productInfo.title || 'не указано'}
411• Состав: ${productInfo.composition || 'не указан'}
412• Базовые ключи: ${keywords.join(', ')}
413
414ЗАДАЧА:
415Предложи 10-15 дополнительных релевантных ключевых слов для поиска в аналитике Wildberries.
416
417ВАЖНО:
418- Предлагай СИНОНИМЫ и АЛЬТЕРНАТИВНЫЕ названия товара
419- НЕ расширяй базовый ключ (например, если "масло для загара" → НЕ предлагай "сухое масло для загара")
420- Предлагай ДРУГИЕ типы продуктов с тем же назначением
421- Учитывай состав и назначение товара
422- НЕ предлагай бренды или названия конкурентов
423- Ключи должны быть короткими (1-4 слова)
424
425ПРИМЕРЫ ПРАВИЛЬНЫХ предложений:
426- Базовый ключ: "масло для загара" → Предложи: "средство для загара", "лошон для загара", "усилитель загара", "активатор загара", "крем для загара"
427- Базовый ключ: "пенка для умывания" → Предложи: "средство для умывания", "гель для умывания", "для умывания лица", "очищающее средство"
428- Базовый ключ: "тестостерон" → Предложи: "для тестостерона", "повышение тестостерона", "бустер тестостерона", "тестобустер"
429- Базовый ключ: "витамины" → Предложи: "витаминный комплекс", "мультивитамины", "бад", "добавка"
430
431ПРИМЕРЫ НЕПРАВИЛЬНЫХ предложений (НЕ ДЕЛАЙ ТАК):
432- Базовый ключ: "масло для загара" → ❌ "сухое масло для загара", "натуральное масло для загара" (это расширение базового ключа)
433- Базовый ключ: "пенка для умывания" → ❌ "косметика", "уход за кожей", "красота" (слишком общие)
434- Базовый ключ: "тестостерон" → ❌ "здоровье", "бады" (слишком общие)
435
436ВАЖНО:
437- Предлагай ДРУГИЕ способы назвать тот же товар
438- Думай как покупатель: "Как еще можно назвать этот товар?"
439- НЕ добавляй прилагательные к базовому ключу
440
441Верни ТОЛЬКО список ключей в формате JSON:
442{
443 "keywords": ["ключ 1", "ключ 2", "ключ 3", ...]
444}
445
446НЕ ПИШИ ничего кроме JSON. Начни ответ сразу с {`;
447
448 console.log('Wildberries Description Generator: Запрос предложений ключей от AI');
449
450 const suggestResponse = await RM.aiCall(suggestPrompt);
451
452 // Парсим ответ
453 let suggestedKeywords = [];
454 try {
455 const suggestData = JSON.parse(suggestResponse);
456 suggestedKeywords = suggestData.keywords || [];
457 console.log(`Wildberries Description Generator: AI предложил ${suggestedKeywords.length} ключей`);
458 } catch (e) {
459 console.error('Wildberries Description Generator: Ошибка парсинга предложенных ключей:', e);
460 showStatus(statusContainer, 'Ошибка при получении предложений. Попробуйте еще раз.', 'error');
461 return;
462 }
463
464 if (suggestedKeywords.length === 0) {
465 showStatus(statusContainer, 'AI не смог предложить дополнительные ключи', 'error');
466 return;
467 }
468
469 // Показываем предложенные ключи с чекбоксами
470 suggestedContainer.innerHTML = `
471 <div class="wb-desc-suggested-keywords">
472 <div class="wb-desc-suggested-header">
473 Предложенные ключи (выберите нужные):
474 <button class="wb-desc-suggest-btn" id="wb-toggle-all-btn" style="margin-left: 12px; padding: 4px 12px; font-size: 12px;">Выделить все</button>
475 </div>
476 <div class="wb-desc-checkbox-group">
477 ${suggestedKeywords.map((keyword, index) => `
478 <div class="wb-desc-checkbox-item">
479 <input type="checkbox" id="wb-suggested-${index}" value="${keyword}">
480 <label for="wb-suggested-${index}">${keyword}</label>
481 </div>
482 `).join('')}
483 </div>
484 </div>
485 `;
486
487 suggestedContainer.style.display = 'block';
488
489 // Добавляем обработчик для кнопки "Выделить все"
490 const toggleAllBtn = document.getElementById('wb-toggle-all-btn');
491 toggleAllBtn.addEventListener('click', () => {
492 const checkboxes = document.querySelectorAll('#wb-suggested-keywords-container input[type="checkbox"]');
493 const allChecked = Array.from(checkboxes).every(cb => cb.checked);
494
495 checkboxes.forEach(cb => {
496 cb.checked = !allChecked;
497 });
498
499 toggleAllBtn.textContent = allChecked ? 'Выделить все' : 'Снять выделение';
500 });
501
502 showStatus(statusContainer, `AI предложил ${suggestedKeywords.length} дополнительных ключей. Выберите нужные и нажмите "Сгенерировать"`, 'success');
503
504 } catch (error) {
505 console.error('Wildberries Description Generator: Ошибка при предложении ключей:', error);
506 showStatus(statusContainer, 'Ошибка при получении предложений. Попробуйте еще раз.', 'error');
507 } finally {
508 suggestBtn.disabled = false;
509 }
510 }
511
512 // Функция для сбора данных с аналитики
513 async function collectAnalyticsData(keywords, minusWords) {
514 console.log('Wildberries Description Generator: Начало сбора данных с аналитики');
515
516 // Сохраняем данные для доступа из другой вкладки
517 await GM.setValue('wb_keywords_to_process', JSON.stringify(keywords));
518 await GM.setValue('wb_minus_words', JSON.stringify(minusWords));
519 await GM.setValue('wb_analytics_data', JSON.stringify([]));
520 await GM.setValue('wb_collection_status', 'pending');
521
522 // Открываем страницу аналитики в новой вкладке
523 const analyticsUrl = 'https://seller.wildberries.ru/search-analytics/popular-search-queries';
524 await GM.openInTab(analyticsUrl, false);
525
526 console.log('Wildberries Description Generator: Открыта страница аналитики, ожидание сбора данных...');
527
528 // Ждем завершения сбора данных (максимум 5 минут)
529 const maxWaitTime = 300000; // 5 минут
530 const checkInterval = 2000; // проверяем каждые 2 секунды
531 let waitedTime = 0;
532
533 while (waitedTime < maxWaitTime) {
534 await new Promise(resolve => setTimeout(resolve, checkInterval));
535 waitedTime += checkInterval;
536
537 const status = await GM.getValue('wb_collection_status', 'pending');
538
539 if (status === 'completed') {
540 const analyticsDataStr = await GM.getValue('wb_analytics_data', '[]');
541 const analyticsData = JSON.parse(analyticsDataStr);
542 console.log('Wildberries Description Generator: Данные успешно собраны');
543 return analyticsData;
544 } else if (status === 'error') {
545 console.error('Wildberries Description Generator: Ошибка при сборе данных');
546 return [];
547 }
548 }
549
550 console.error('Wildberries Description Generator: Превышено время ожидания сбора данных');
551 return [];
552 }
553
554 // Функция для автоматического сбора данных на странице аналитики
555 async function autoCollectOnAnalyticsPage() {
556 // Проверяем, что мы на странице аналитики
557 if (!window.location.href.includes('seller.wildberries.ru/search-analytics/popular-search-queries')) {
558 return;
559 }
560
561 console.log('Wildberries Description Generator: Обнаружена страница аналитики');
562
563 // Проверяем, есть ли задача на сбор данных
564 const status = await GM.getValue('wb_collection_status', 'none');
565 if (status !== 'pending') {
566 return;
567 }
568
569 console.log('Wildberries Description Generator: Начинаем автоматический сбор данных');
570
571 try {
572 const keywordsStr = await GM.getValue('wb_keywords_to_process', '[]');
573 const minusWordsStr = await GM.getValue('wb_minus_words', '[]');
574 const keywords = JSON.parse(keywordsStr);
575 const minusWords = JSON.parse(minusWordsStr);
576
577 const analyticsData = [];
578
579 // Ждем загрузки страницы
580 await new Promise(resolve => setTimeout(resolve, 3000));
581
582 for (const keyword of keywords) {
583 console.log(`Wildberries Description Generator: Обработка ключевого слова: ${keyword}`);
584
585 try {
586 // Находим поле поиска
587 const searchInput = document.querySelector('input[name="searchString"]');
588 if (!searchInput) {
589 console.error('Wildberries Description Generator: Поле поиска не найдено');
590 continue;
591 }
592
593 // Очищаем и вводим ключевое слово
594 searchInput.value = '';
595 searchInput.focus();
596 searchInput.value = keyword;
597 searchInput.dispatchEvent(new Event('input', { bubbles: true }));
598 searchInput.dispatchEvent(new Event('change', { bubbles: true }));
599
600 // Ждем загрузки результатов
601 await new Promise(resolve => setTimeout(resolve, 5000));
602
603 // Собираем данные из таблицы
604 const rows = document.querySelectorAll('table tbody tr');
605 const keywordData = {
606 keyword: keyword,
607 queries: []
608 };
609
610 console.log(`Wildberries Description Generator: Найдено строк в таблице: ${rows.length}`);
611
612 rows.forEach(row => {
613 const cells = row.querySelectorAll('td');
614 if (cells.length >= 2) {
615 const query = cells[0]?.textContent?.trim();
616 const popularity = cells[1]?.textContent?.trim();
617
618 if (query && popularity) {
619 // Фильтруем по минус-словам
620 const queryLower = query.toLowerCase();
621 const hasMinusWord = minusWords.some(minusWord =>
622 queryLower.includes(minusWord.toLowerCase())
623 );
624
625 if (!hasMinusWord) {
626 keywordData.queries.push({
627 query,
628 popularity
629 });
630 } else {
631 console.log(`Wildberries Description Generator: Исключен запрос "${query}" (содержит минус-слово)`);
632 }
633 }
634 }
635 });
636
637 analyticsData.push(keywordData);
638 console.log(`Wildberries Description Generator: Собрано ${keywordData.queries.length} запросов для "${keyword}"`);
639
640 } catch (error) {
641 console.error(`Wildberries Description Generator: Ошибка при обработке ключевого слова "${keyword}":`, error);
642 }
643 }
644
645 // Сохраняем собранные данные
646 await GM.setValue('wb_analytics_data', JSON.stringify(analyticsData));
647 await GM.setValue('wb_collection_status', 'completed');
648
649 console.log('Wildberries Description Generator: Сбор данных завершен, можно закрыть вкладку');
650
651 // Закрываем вкладку через 2 секунды
652 setTimeout(() => {
653 window.close();
654 }, 2000);
655
656 } catch (error) {
657 console.error('Wildberries Description Generator: Ошибка при автоматическом сборе данных:', error);
658 await GM.setValue('wb_collection_status', 'error');
659 }
660 }
661
662 // Функция для получения информации о товаре со страницы
663 function getProductInfo() {
664 console.log('Wildberries Description Generator: Сбор информации о товаре');
665
666 const productInfo = {
667 title: '',
668 currentDescription: '',
669 composition: '',
670 attributes: []
671 };
672
673 // 1. Получаем текущее описание
674 const descriptionTextarea = document.querySelector('textarea[data-testid="card-form-main-field-description"]');
675 if (descriptionTextarea) {
676 productInfo.currentDescription = descriptionTextarea.value || '';
677 console.log('Wildberries Description Generator: Текущее описание найдено');
678 }
679
680 // 2. Получаем название товара - ИСПРАВЛЕННЫЙ ПОИСК
681 // Способ 1: По id "editable-title" (contenteditable)
682 const editableTitle = document.querySelector('#editable-title');
683 if (editableTitle) {
684 // Для contenteditable элементов берем textContent или innerText
685 productInfo.title = editableTitle.textContent || editableTitle.innerText || '';
686 console.log('Wildberries Description Generator: Название найдено по #editable-title');
687 }
688
689 // Способ 2: Поиск по label "Наименование"
690 if (!productInfo.title) {
691 // Находим label с текстом "Наименование"
692 const labels = document.querySelectorAll('label, span, div');
693 const nameLabel = Array.from(labels).find(el =>
694 el.textContent && el.textContent.trim() === 'Наименование'
695 );
696
697 if (nameLabel) {
698 console.log('Wildberries Description Generator: Найден label "Наименование"');
699 // Ищем поле ввода рядом с label
700 const parent = nameLabel.closest('div, .field-wrapper, .form-group');
701 if (parent) {
702 // Ищем input или contenteditable
703 const input = parent.querySelector('input, textarea, [contenteditable="true"]');
704 if (input) {
705 if (input.tagName === 'INPUT' || input.tagName === 'TEXTAREA') {
706 productInfo.title = input.value || '';
707 } else {
708 productInfo.title = input.textContent || input.innerText || '';
709 }
710 console.log('Wildberries Description Generator: Название найдено рядом с label');
711 }
712 }
713 }
714 }
715
716 // Способ 3: Общий поиск input
717 if (!productInfo.title) {
718 const nameInput = document.querySelector('input[name*="name"], input[placeholder*="Название"], input[placeholder*="наименование"]');
719 if (nameInput) {
720 productInfo.title = nameInput.value || '';
721 console.log('Wildberries Description Generator: Название найдено через общий поиск');
722 }
723 }
724
725 // 3. Получаем состав товара
726 const compositionBlock = document.querySelector('div#Состав');
727 if (compositionBlock) {
728 console.log('Wildberries Description Generator: Найден блок "Состав"');
729
730 let ingredients = [];
731
732 // Способ 1: По data-testid
733 const byTestId = compositionBlock.querySelectorAll('[data-testid^="undefined-select-item-"]');
734 if (byTestId.length > 0) {
735 ingredients = Array.from(byTestId).map(el => el.textContent.trim()).filter(Boolean);
736 console.log(`Wildberries Description Generator: Извлечено ${ingredients.length} ингредиентов по data-testid`);
737 }
738 // Способ 2: По классу
739 else {
740 const byClass = compositionBlock.querySelectorAll('.Selected-item__text__6P8EDRPmWD');
741 if (byClass.length > 0) {
742 ingredients = Array.from(byClass).map(el => el.textContent.trim()).filter(Boolean);
743 console.log(`Wildberries Description Generator: Извлечено ${ingredients.length} ингредиентов по классу`);
744 }
745 }
746
747 // Способ 3: Поиск в чипах
748 if (ingredients.length === 0) {
749 const chips = compositionBlock.querySelectorAll('.Selected-item__fIyMG5Li-v, .New-multi-select-input__selected-item__KOh9hWF-7q');
750 chips.forEach(chip => {
751 const text = chip.textContent.trim();
752 const cleanText = text.replace(/remove$/, '').trim();
753 if (cleanText) {
754 ingredients.push(cleanText);
755 }
756 });
757 console.log(`Wildberries Description Generator: Извлечено ${ingredients.length} ингредиентов по чипам`);
758 }
759
760 // Объединяем ингредиенты
761 if (ingredients.length > 0) {
762 productInfo.composition = ingredients.join(', ');
763 console.log('Wildberries Description Generator: Состав товара извлечен');
764 } else {
765 console.log('Wildberries Description Generator: Ингредиенты не найдены в блоке "Состав"');
766 }
767 } else {
768 console.log('Wildberries Description Generator: Блок "Состав" не найден');
769
770 // Старый способ
771 const compositionTextarea = document.querySelector('textarea[placeholder*="Состав"], textarea[name*="composition"]');
772 if (compositionTextarea && compositionTextarea.value) {
773 productInfo.composition = compositionTextarea.value;
774 console.log('Wildberries Description Generator: Состав найден через textarea');
775 }
776 }
777
778 // 4. Дополнительные атрибуты (опционально)
779 try {
780 const attributes = {};
781
782 // Цвет
783 const colorElement = document.querySelector('div#Цвет');
784 if (colorElement) {
785 const colorText = colorElement.querySelector('[data-testid^="undefined-select-item-"]')?.textContent;
786 if (colorText) attributes.color = colorText.trim();
787 }
788
789 // Бренд
790 const brandInput = document.querySelector('input[placeholder*="Бренд"], input[name*="brand"]');
791 if (brandInput && brandInput.value) {
792 attributes.brand = brandInput.value;
793 }
794
795 if (Object.keys(attributes).length > 0) {
796 productInfo.attributes = Object.entries(attributes).map(([key, value]) => `${key}: ${value}`);
797 }
798 } catch (e) {
799 console.log('Wildberries Description Generator: Ошибка при сборе атрибутов:', e);
800 }
801
802 console.log('Wildberries Description Generator: Информация о товаре собрана', {
803 titleLength: productInfo.title.length,
804 descriptionLength: productInfo.currentDescription.length,
805 compositionLength: productInfo.composition.length,
806 attributesCount: productInfo.attributes.length
807 });
808
809 return productInfo;
810 }
811
812 // Функция для анализа использованных ключей и расчета популярности
813 async function analyzeUsedKeywords(description) {
814 console.log('Wildberries Description Generator: Анализ использованных ключей');
815
816 const analyticsDataStr = await GM.getValue('wb_analytics_data', '[]');
817 const analyticsData = JSON.parse(analyticsDataStr);
818
819 const descriptionLower = description.toLowerCase();
820 const usedQueries = [];
821 let totalPopularity = 0;
822
823 // Проходим по всем собранным запросам
824 analyticsData.forEach(keywordData => {
825 keywordData.queries.forEach(queryData => {
826 const query = queryData.query.toLowerCase();
827
828 // Проверяем, используется ли запрос в описании
829 if (descriptionLower.includes(query)) {
830 // Парсим популярность (убираем пробелы)
831 const popularityStr = queryData.popularity.replace(/\s/g, '');
832 const popularity = parseInt(popularityStr) || 0;
833
834 usedQueries.push({
835 query: queryData.query,
836 popularity: popularity
837 });
838
839 totalPopularity += popularity;
840 }
841 });
842 });
843
844 console.log(`Wildberries Description Generator: Использовано ${usedQueries.length} запросов`);
845 console.log(`Wildberries Description Generator: Общая популярность: ${totalPopularity}`);
846
847 return {
848 usedQueries,
849 totalPopularity,
850 totalQueriesAvailable: analyticsData.reduce((sum, kd) => sum + kd.queries.length, 0)
851 };
852 }
853
854 // Функция для форматирования числа с разделителями
855 function formatNumber(num) {
856 return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ' ');
857 }
858
859 // Функция для генерации описания
860 async function generateDescription(modal) {
861 const keywordsInput = document.getElementById('wb-keywords-input');
862 const minusWordsInput = document.getElementById('wb-minus-words-input');
863 const generateBtn = document.getElementById('wb-generate-btn');
864 const regenerateBtn = document.getElementById('wb-regenerate-btn');
865 const insertBtn = document.getElementById('wb-insert-btn');
866 const resultContainer = document.getElementById('wb-desc-result-container');
867 const resultDiv = document.getElementById('wb-desc-result');
868 const charCountDiv = document.getElementById('wb-char-count');
869 const statusContainer = document.getElementById('wb-desc-status-container');
870
871 const keywordsText = keywordsInput.value.trim();
872 const minusWordsText = minusWordsInput.value.trim();
873
874 if (!keywordsText) {
875 showStatus(statusContainer, 'Пожалуйста, введите ключевые слова', 'error');
876 return;
877 }
878
879 let keywords = keywordsText.split('\n').map(k => k.trim()).filter(k => k);
880 const minusWords = minusWordsText.split('\n').map(k => k.trim().toLowerCase()).filter(k => k);
881
882 // Добавляем выбранные дополнительные ключи
883 const suggestedCheckboxes = document.querySelectorAll('#wb-suggested-keywords-container input[type="checkbox"]:checked');
884 if (suggestedCheckboxes.length > 0) {
885 const selectedSuggested = Array.from(suggestedCheckboxes).map(cb => cb.value);
886 keywords = [...keywords, ...selectedSuggested];
887 console.log(`Wildberries Description Generator: Добавлено ${selectedSuggested.length} дополнительных ключей`);
888 }
889
890 if (keywords.length === 0) {
891 showStatus(statusContainer, 'Пожалуйста, введите хотя бы одно ключевое слово', 'error');
892 return;
893 }
894
895 // Показываем загрузку
896 generateBtn.disabled = true;
897 regenerateBtn.style.display = 'none';
898 insertBtn.style.display = 'none';
899 resultContainer.style.display = 'none';
900
901 showStatus(statusContainer, 'Сбор данных с аналитики...', 'info');
902
903 try {
904 // Собираем данные с аналитики
905 const analyticsData = await collectAnalyticsData(keywords, minusWords);
906
907 // Получаем информацию о товаре
908 const productInfo = getProductInfo();
909
910 // ЭТАП 1: Фильтруем и планируем использование запросов
911 showStatus(statusContainer, 'Анализ и планирование запросов...', 'info');
912
913 const planningPrompt = `Проанализируй данные аналитики и создай план использования запросов для SEO-описания товара.
914
915ДАННЫЕ:
916• Название: ${productInfo.title || 'не указано'}
917• Состав: ${productInfo.composition || 'не указан'}
918• Ключевые слова: ${keywords.join(', ')}
919• Минус-слова: ${minusWords.join(', ') || 'нет'}
920
921${
922 analyticsData && analyticsData.length > 0
923 ? `ДАННЫЕ АНАЛИТИКИ:
924${analyticsData.map(data => {
925 return `\nКлючевое слово: "${data.keyword}"\nЗапросы:\n${data.queries.map((q, i) => `${i+1}. "${q.query}" (популярность: ${q.popularity})`).join('\n')}`;
926 }).join('\n')}`
927 : 'ДАННЫЕ АНАЛИТИКИ: Запросы не предоставлены.'
928 }
929
930ЗАДАЧА:
9311. Отфильтруй запросы, удалив ТОЛЬКО:
932 - Бренды: эвалар, артнео, солгар, now foods, доппельгерц, афобазол, новопассит, фенибут, нормотим, ашваганда
933 - Фамилии: зубарева, агапкин, малышева, лихи, роберт
934 - Минус-слова: ${minusWords.join(', ') || 'нет'}
935 - Нерелевантное: купить, цена, отзывы, инструкция, доставка, книга, раскраска, игрушка, подарок, кольцо
936 - Животных: для кошек, для собак, кот баюн
937
9382. ВАЖНО: НЕ удаляй запросы, которые являются расширением базовых ключей!
939 - Например, если базовый ключ "средство от прыщей", то "средство от прыщей на спине" - ОСТАВЬ
940 - Если базовый ключ "масло для загара", то "сухое масло для загара" - ОСТАВЬ
941 - Расширенные запросы тоже важны для SEO!
942
9433. Отсортируй чистые запросы по популярности
944
9454. Выбери ТОП-70 самых популярных чистых запросов для использования в описании
946
9475. Верни ТОЛЬКО список выбранных запросов в формате JSON:
948{
949 "queries": ["запрос 1", "запрос 2", "запрос 3", ...]
950}
951
952НЕ ПИШИ ничего кроме JSON. Начни ответ сразу с {`;
953
954 console.log('Wildberries Description Generator: Этап 1 - Планирование запросов');
955
956 const planResponse = await RM.aiCall(planningPrompt);
957
958 // Парсим план
959 let selectedQueries = [];
960 try {
961 const planData = JSON.parse(planResponse);
962 selectedQueries = planData.queries || [];
963 console.log(`Wildberries Description Generator: Выбрано ${selectedQueries.length} запросов для использования`);
964 } catch (e) {
965 console.error('Wildberries Description Generator: Ошибка парсинга плана:', e);
966 // Если не удалось распарсить, используем все запросы
967 selectedQueries = analyticsData.flatMap(data => data.queries.map(q => q.query));
968 }
969
970 // ЭТАП 2: Генерируем описание с использованием выбранных запросов
971 showStatus(statusContainer, 'Генерация описания с помощью AI...', 'info');
972
973 const descriptionPrompt = `Создай SEO-описание товара для Wildberries (внутренняя SEO-оптимизация).
974
975ДАННЫЕ:
976• Название: ${productInfo.title || 'не указано'}
977• Состав: ${productInfo.composition || 'не указан'}
978• Ключевые слова: ${keywords.join(', ')}
979
980ЗАПРОСЫ ДЛЯ ИСПОЛЬЗОВАНИЯ (используй МАКСИМУМ из этого списка):
981${selectedQueries.map((q, i) => `${i+1}. "${q}"`).join('\n')}
982
983=== ЖЕСТКИЕ ТРЕБОВАНИЯ ===
984
9851. ОБЪЕМ И СТРУКТУРА:
986 • 3800-4200 символов, только текст описания
987 • ЕСТЕСТВЕННАЯ СТРУКТУРА: Введение → Основная часть → Практическая часть → Заключение
988 • НЕ допускай резких переходов между темами
989
9902. ИСПОЛЬЗОВАНИЕ ЗАПРОСОВ:
991 • ОБЯЗАТЕЛЬНО используй базовые ключевые слова: ${keywords.join(', ')} - минимум 1 раз каждое
992 • ОБЯЗАТЕЛЬНО проверь перед отправкой, что ВСЕ базовые ключи присутствуют в тексте
993 • ИСПОЛЬЗУЙ МАКСИМУМ запросов из списка выше (80-90%)
994 • Запросы можно СКЛОНЯТЬ и адаптировать для естественности текста
995 • Пример ПРАВИЛЬНО: "добавка для увеличения тестостерона для мужчин" (вместо "тестостерон для мужчин в этом комплексе")
996 • Пример ПРАВИЛЬНО: "витамины для спокойствия с магнием" (вместо "витамины для спокойствия - это магний")
997 • Главное - сохранить СМЫСЛ и КЛЮЧЕВЫЕ СЛОВА из запроса, но сделать текст естественным
998 • Шаблон: "[Запрос из списка, можно склонять] с [компонент из состава] [действие] [эффект]"
999 • **Распределение**: 30% введение/основная, 40% практическая, 30% заключение
1000
10013. ИСПОЛЬЗОВАНИЕ СОСТАВА:
1002 • ОБЯЗАТЕЛЬНО используй ВСЕ компоненты из состава: ${productInfo.composition}
1003 • Каждый компонент используй 2-4 раз в разных контекстах
1004 • Не перечисляй, а вплетай в повествование
1005 • Объединяй схожие компоненты в группы
1006
10074. ПЛОТНОСТЬ И СВЯЗНОСТЬ:
1008 • Каждые 50-70 символов — новый запрос из списка
1009 • Начинай с самых популярных запросов (они в начале списка)
1010 • Чередуй: 1 SEO-предложение → 1-2 естественных предложения
1011 • Создавай логические переходы между абзацами
1012 • ВАЖНО: Текст должен быть естественным и логичным, не жертвуй смыслом ради точного совпадения
1013
10145. СТРУКТУРА ПОВЕСТВОВАНИЯ (разделяй на абзацы):
1015
1016 ЧАСТЬ 1: ВВЕДЕНИЕ (10-15% текста)
1017 • Без заголовка, общее описание товара и его назначение, без слов инновационный, революционный
1018 • Основная проблема, которую решает
1019 • Ключевое преимущество
1020
1021 ЧАСТЬ 2: ОСНОВНАЯ ЧАСТЬ (60-70% текста)
1022 • Подробно о составе и действии компонентов
1023 • Группировка по темам: восстановление → увлажнение → защита
1024 • Как работает продукт (механизм действия)
1025 • Исследования эффективности, но без утверждений. Например: "Исследования показывают, что ...."
1026
1027 ЧАСТЬ 3: ПРАКТИЧЕСКАЯ ЧАСТЬ (15-20% текста)
1028 • Для кого подходит (естественный переход через "Благодаря...")
1029 • Когда лучше принимать (время, до / после еды, до / во время тренировок или что то другое) и с какими ещё витаминами сочетается. Не пиши сколько капсул принимать.
1030 • Ожидаемые результаты и преимущества, но без обещаний и эффектов. Например: "Наши покупатели отмечают, что ...
1031
1032 ЧАСТЬ 4: ЗАКЛЮЧЕНИЕ (5-10% текста)
1033 • Краткое резюме ключевых преимуществ
1034 • Естественный завершающий акцент без рекомендаций про врачей
1035
10366. ЗАПРЕТЫ:
1037 ✗ ЛЮБЫЕ английские слова (СТРОГО только русский язык, даже для научных терминов)
1038 ✗ Crucial, essential, vital, key, important, testosterone, energy - переводи на русский: ключевой, важный, существенный, тестостерон, энергия
1039 ✗ Бренды, конкуренты, фамилии, названия компаний
1040 ✗ "Вода": "эликсир", "герой", "ритуал", "скажет спасибо", "настоящий", "буквально"
1041 ✗ Повторы одной фразы (используй синонимы и вариации)
1042 ✗ Инструкционный стиль в конце (никаких "Хранить при температуре...")
1043 ✗ Резкие переходы между темами
1044 ✗ Маркированные списки (•) - используй только текст
1045 ✗ Нелогичные конструкции типа "тестостерон для мужчин в этом комплексе - это не просто добавка"
1046
10477. ПРОВЕРКА (перед ответом):
1048 ✅ ОБЯЗАТЕЛЬНО использованы ВСЕ базовые ключевые слова: ${keywords.join(', ')} - минимум 1 раз каждое (ПРОВЕРЬ!)
1049 ✅ ОБЯЗАТЕЛЬНО использованы ВСЕ компоненты из состава: ${productInfo.composition} (ПРОВЕРЬ!)
1050 ✅ Использовано 50+ разных запросов из списка
1051 ✅ Нет английских слов (ПРОВЕРЬ на английские слова!)
1052 ✅ Объем 3800-4200 символов
1053 ✅ Плотность: 1.5-2.0 запроса на 100 символов
1054 ✅ Естественная структура повествования
1055 ✅ Логичные переходы между абзацами
1056 ✅ ВСЕ слова переведены на русский (crucial → ключевой, testosterone → тестостерон, energy → энергия)
1057 ✅ Текст естественный и логичный, запросы склонены правильно
1058
1059=== НАЧНИ ОПИСАНИЕ СРАЗУ ===
1060
1061НЕ ПИШИ вступлений вроде "Я готов помочь" или "Мне нужны данные".
1062НЕ ПРОСИ дополнительные данные.
1063НЕ ЗАДАВАЙ вопросы.
1064НЕ ИСПОЛЬЗУЙ:
1065✗ Слова революционный, инновационный, инвестируйте
1066✗ ЛЮБЫЕ английские слова (crucial, essential, vital, key, testosterone, energy → переводи на русский)
1067✗ Описание неактивных компонентов (тальк, целлюлоза, стеариновая кислота)
1068✗ Не используй заголовки, вопросы и эмоджи !!!
1069✗ Маркированные списки (•)
1070
1071ПРОСТО СГЕНЕРИРУЙ SEO-ОПИСАНИЕ на основе предоставленных данных.
1072
1073ВАЖНО: Склоняй запросы для естественности текста, но сохраняй все ключевые слова из них!
1074
1075ПЕРЕД ОТПРАВКОЙ ПРОВЕРЬ:
10761. Все ли базовые ключевые слова (${keywords.join(', ')}) присутствуют в тексте?
10772. Все ли компоненты состава (${productInfo.composition}) упомянуты?
10783. Нет ли английских слов?
1079
1080Сгенерируй описание:`;
1081
1082 console.log('Wildberries Description Generator: Этап 2 - Генерация описания');
1083
1084 // Генерируем описание с помощью AI
1085 const description = await RM.aiCall(descriptionPrompt);
1086
1087 console.log('Wildberries Description Generator: Описание сгенерировано');
1088
1089 // Анализируем использованные ключи
1090 const analysis = await analyzeUsedKeywords(description);
1091
1092 // Проверяем длину
1093 const charCount = description.length;
1094
1095 // Сохраняем описание
1096 await GM.setValue('wb_generated_description', description);
1097
1098 // Показываем результат
1099 resultDiv.textContent = description;
1100 resultContainer.style.display = 'block';
1101
1102 // Обновляем счетчик символов с информацией о ключах
1103 const popularityInfo = `${charCount} / 5000 символов | Использовано ${analysis.usedQueries.length} из ${analysis.totalQueriesAvailable} запросов | Общая популярность: ${formatNumber(analysis.totalPopularity)}`;
1104 charCountDiv.textContent = popularityInfo;
1105
1106 // Показываем кнопки
1107 generateBtn.style.display = 'none';
1108 regenerateBtn.style.display = 'inline-block';
1109 insertBtn.style.display = 'inline-block';
1110
1111 showStatus(statusContainer, `Описание успешно сгенерировано! (${charCount} символов)`, 'success');
1112
1113 } catch (error) {
1114 console.error('Wildberries Description Generator: Ошибка при генерации описания:', error);
1115 showStatus(statusContainer, 'Ошибка при генерации описания. Попробуйте еще раз.', 'error');
1116 } finally {
1117 generateBtn.disabled = false;
1118 }
1119 }
1120
1121 // Функция для показа статуса
1122 function showStatus(container, message, type) {
1123 container.innerHTML = `<div class="wb-desc-status ${type}">${message}</div>`;
1124 }
1125
1126 // Функция для вставки описания
1127 async function insertDescription(modal) {
1128 console.log('Wildberries Description Generator: Вставка описания');
1129
1130 try {
1131 const description = await GM.getValue('wb_generated_description', '');
1132
1133 if (!description) {
1134 alert('Описание не найдено. Пожалуйста, сгенерируйте описание сначала.');
1135 return;
1136 }
1137
1138 // Находим поле описания
1139 const descriptionTextarea = document.querySelector('textarea[data-testid="card-form-main-field-description"]');
1140
1141 if (!descriptionTextarea) {
1142 alert('Не удалось найти поле описания. Убедитесь, что вы находитесь на странице редактирования товара.');
1143 return;
1144 }
1145
1146 // Вставляем описание
1147 descriptionTextarea.value = description;
1148 descriptionTextarea.dispatchEvent(new Event('input', { bubbles: true }));
1149 descriptionTextarea.dispatchEvent(new Event('change', { bubbles: true }));
1150
1151 console.log('Wildberries Description Generator: Описание успешно вставлено');
1152
1153 // Закрываем модальное окно
1154 modal.remove();
1155
1156 alert('Описание успешно вставлено!');
1157
1158 } catch (error) {
1159 console.error('Wildberries Description Generator: Ошибка при вставке описания:', error);
1160 alert('Ошибка при вставке описания. Попробуйте еще раз.');
1161 }
1162 }
1163
1164 // Функция для инициализации расширения
1165 function init() {
1166 console.log('Wildberries Description Generator: Инициализация');
1167
1168 // Проверяем, что мы на странице редактирования товара
1169 if (window.location.href.includes('seller.wildberries.ru/new-goods/card')) {
1170
1171 // Ждем загрузки страницы и добавляем кнопку
1172 const observer = new MutationObserver((mutations, obs) => {
1173 const descriptionHeader = document.querySelector('.Description-header__zK-9sKs8RX');
1174 if (descriptionHeader) {
1175 createGeneratorButton();
1176 obs.disconnect();
1177 }
1178 });
1179
1180 observer.observe(document.body, {
1181 childList: true,
1182 subtree: true
1183 });
1184
1185 // Также пробуем добавить кнопку сразу
1186 setTimeout(createGeneratorButton, 2000);
1187 }
1188
1189 // Проверяем, что мы на странице аналитики и запускаем автосбор
1190 if (window.location.href.includes('seller.wildberries.ru/search-analytics/popular-search-queries')) {
1191 setTimeout(autoCollectOnAnalyticsPage, 2000);
1192 }
1193 }
1194
1195 // Запускаем инициализацию
1196 if (document.readyState === 'loading') {
1197 document.addEventListener('DOMContentLoaded', init);
1198 } else {
1199 init();
1200 }
1201
1202})();