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