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