Массовая рассылка сообщений покупателям в Ozon Messenger
Size
169.1 KB
Version
1.1.199
Created
Feb 8, 2026
Updated
29 days ago
1// ==UserScript==
2// @name Ozon BASE Messenger Bulk Sender
3// @description Массовая рассылка сообщений покупателям в Ozon Messenger
4// @version 1.1.199
5// @match *://seller.ozon.ru/*
6// @grant GM.getValue
7// @grant GM.setValue
8// @grant GM.openInTab
9// ==/UserScript==
10(function() {
11 'use strict';
12
13 console.log('Ozon Messenger Bulk Sender загружен - ВЕРСИЯ 1.1.6');
14
15 // Глобальные переменные для управления процессом
16 let isPaused = false;
17 let shouldStop = false;
18
19 // ============= ИНИЦИАЛИЗАЦИЯ INDEXEDDB =============
20
21 let db = null;
22 const DB_NAME = 'OzonMessengerDB';
23 const DB_VERSION = 2;
24 const STORE_NAME = 'storage';
25
26 // Инициализация IndexedDB
27 async function initIndexedDB() {
28 return new Promise((resolve, reject) => {
29 try {
30 console.log('Попытка открытия IndexedDB:', DB_NAME);
31
32 // Проверяем доступность IndexedDB
33 if (!window.indexedDB) {
34 const error = new Error('IndexedDB не поддерживается в этом браузере');
35 console.error(error.message);
36 reject(error);
37 return;
38 }
39
40 const request = indexedDB.open(DB_NAME, DB_VERSION);
41
42 request.onerror = () => {
43 const errorMsg = request.error ?
44 `${request.error.name}: ${request.error.message}` :
45 'Неизвестная ошибка IndexedDB';
46 console.error('Ошибка открытия IndexedDB:', errorMsg);
47 console.error('Полный объект ошибки:', request.error);
48 reject(new Error(errorMsg));
49 };
50
51 request.onsuccess = () => {
52 db = request.result;
53 console.log('✅ IndexedDB успешно инициализирована');
54
55 // Обработчик ошибок базы данных
56 db.onerror = (event) => {
57 console.error('Ошибка базы данных:', event);
58 };
59
60 // Обработчик закрытия базы данных
61 db.onclose = () => {
62 console.warn('База данных была закрыта');
63 db = null;
64 };
65
66 resolve(db);
67 };
68
69 request.onupgradeneeded = (event) => {
70 console.log('Обновление структуры IndexedDB...');
71 const database = event.target.result;
72
73 // Создаем хранилище, если его нет
74 if (!database.objectStoreNames.contains(STORE_NAME)) {
75 database.createObjectStore(STORE_NAME, { keyPath: 'key' });
76 console.log('✅ Создано хранилище IndexedDB:', STORE_NAME);
77 }
78 };
79
80 request.onblocked = () => {
81 console.warn('⚠️ IndexedDB заблокирована. Закройте другие вкладки с этим сайтом.');
82 reject(new Error('IndexedDB заблокирована другой вкладкой'));
83 };
84 } catch (error) {
85 console.error('Критическая ошибка при инициализации IndexedDB:', error);
86 console.error('error.name:', error.name);
87 console.error('error.message:', error.message);
88 console.error('error.stack:', error.stack);
89 reject(error);
90 }
91 });
92 }
93
94 // Сохранение значения в IndexedDB
95 async function setDBValue(key, value) {
96 if (!db) {
97 console.log('База данных не инициализирована, инициализируем...');
98 await initIndexedDB();
99 }
100
101 return new Promise((resolve, reject) => {
102 try {
103 console.log('Сохранение в IndexedDB, ключ:', key, 'размер данных:', JSON.stringify(value).length, 'байт');
104 const transaction = db.transaction([STORE_NAME], 'readwrite');
105
106 transaction.onerror = (event) => {
107 console.error('Ошибка транзакции при сохранении:', event);
108 console.error('transaction.error:', transaction.error);
109 reject(new Error(`Transaction error: ${transaction.error?.name} - ${transaction.error?.message}`));
110 };
111
112 transaction.oncomplete = () => {
113 console.log('Транзакция сохранения завершена успешно');
114 };
115
116 const objectStore = transaction.objectStore(STORE_NAME);
117 const request = objectStore.put({ key: key, value: value });
118
119 request.onsuccess = () => {
120 console.log('✅ Значение сохранено в IndexedDB:', key);
121 resolve();
122 };
123
124 request.onerror = (event) => {
125 console.error('Ошибка сохранения в IndexedDB:', event);
126 console.error('request.error:', request.error);
127 console.error('request.error.name:', request.error?.name);
128 console.error('request.error.message:', request.error?.message);
129 reject(new Error(`Put error: ${request.error?.name} - ${request.error?.message}`));
130 };
131 } catch (error) {
132 console.error('Ошибка при создании транзакции IndexedDB:', error);
133 console.error('error.name:', error.name);
134 console.error('error.message:', error.message);
135 console.error('error.stack:', error.stack);
136 reject(error);
137 }
138 });
139 }
140
141 // Получение значения из IndexedDB
142 async function getDBValue(key, defaultValue = null) {
143 if (!db) {
144 console.log('База данных не инициализирована, инициализируем...');
145 await initIndexedDB();
146 }
147
148 return new Promise((resolve, reject) => {
149 try {
150 console.log('Чтение из IndexedDB, ключ:', key);
151 const transaction = db.transaction([STORE_NAME], 'readonly');
152
153 transaction.onerror = (event) => {
154 console.error('Ошибка транзакции при чтении:', event);
155 console.error('transaction.error:', transaction.error);
156 reject(new Error(`Transaction error: ${transaction.error?.name} - ${transaction.error?.message}`));
157 };
158
159 const objectStore = transaction.objectStore(STORE_NAME);
160 const request = objectStore.get(key);
161
162 request.onsuccess = () => {
163 if (request.result) {
164 console.log('✅ Значение получено из IndexedDB:', key);
165 resolve(request.result.value);
166 } else {
167 console.log('⚠️ Значение не найдено в IndexedDB, возвращаем default:', key);
168 resolve(defaultValue);
169 }
170 };
171
172 request.onerror = (event) => {
173 console.error('Ошибка чтения из IndexedDB:', event);
174 console.error('request.error:', request.error);
175 console.error('request.error.name:', request.error?.name);
176 console.error('request.error.message:', request.error?.message);
177 reject(new Error(`Get error: ${request.error?.name} - ${request.error?.message}`));
178 };
179 } catch (error) {
180 console.error('Ошибка при создании транзакции IndexedDB:', error);
181 console.error('error.name:', error.name);
182 console.error('error.message:', error.message);
183 console.error('error.stack:', error.stack);
184 reject(error);
185 }
186 });
187 }
188
189 // Удаление значения из IndexedDB
190 async function deleteDBValue(key) {
191 if (!db) {
192 console.log('База данных не инициализирована, инициализируем...');
193 await initIndexedDB();
194 }
195
196 return new Promise((resolve, reject) => {
197 try {
198 console.log('Удаление из IndexedDB, ключ:', key);
199 const transaction = db.transaction([STORE_NAME], 'readwrite');
200
201 transaction.onerror = (event) => {
202 console.error('Ошибка транзакции при удалении:', event);
203 console.error('transaction.error:', transaction.error);
204 reject(new Error(`Transaction error: ${transaction.error?.name} - ${transaction.error?.message}`));
205 };
206
207 const objectStore = transaction.objectStore(STORE_NAME);
208 const request = objectStore.delete(key);
209
210 request.onsuccess = () => {
211 console.log('✅ Значение удалено из IndexedDB:', key);
212 resolve();
213 };
214
215 request.onerror = (event) => {
216 console.error('Ошибка удаления из IndexedDB:', event);
217 console.error('request.error:', request.error);
218 console.error('request.error.name:', request.error?.name);
219 console.error('request.error.message:', request.error?.message);
220 reject(new Error(`Delete error: ${request.error?.name} - ${request.error?.message}`));
221 };
222 } catch (error) {
223 console.error('Ошибка при создании транзакции IndexedDB:', error);
224 console.error('error.name:', error.name);
225 console.error('error.message:', error.message);
226 console.error('error.stack:', error.stack);
227 reject(error);
228 }
229 });
230 }
231
232 // ============= ФУНКЦИИ ДЛЯ ИМПОРТА/ЭКСПОРТА БАЗЫ =============
233
234 // Экспорт базы данных в JSON файл
235 async function exportDatabase() {
236 try {
237 const db = await loadContactsDatabase();
238 const currentAccount = getCurrentAccount();
239
240 // Формируем данные для экспорта
241 const exportData = {
242 account: currentAccount,
243 exportDate: new Date().toISOString(),
244 version: '1.0',
245 database: db
246 };
247
248 // Конвертируем в JSON
249 const jsonString = JSON.stringify(exportData, null, 2);
250 const blob = new Blob([jsonString], { type: 'application/json' });
251
252 // Создаем ссылку для скачивания
253 const url = URL.createObjectURL(blob);
254 const a = document.createElement('a');
255 a.href = url;
256
257 // Формируем имя файла с датой и аккаунтом
258 const dateStr = new Date().toISOString().split('T')[0];
259 const accountSlug = currentAccount.replace(/[^a-zA-Z0-9]/g, '_').substring(0, 30);
260 a.download = `ozon_contacts_${accountSlug}_${dateStr}.json`;
261
262 document.body.appendChild(a);
263 a.click();
264 document.body.removeChild(a);
265 URL.revokeObjectURL(url);
266
267 console.log('✅ База данных экспортирована');
268 alert(`✅ База данных экспортирована!\n\nАккаунт: ${currentAccount}\nКонтактов: ${Object.keys(db.contacts).length}\nФайл: ${a.download}`);
269
270 return true;
271 } catch (error) {
272 console.error('❌ Ошибка экспорта базы данных:', error);
273 alert('❌ Ошибка экспорта базы данных: ' + error.message);
274 return false;
275 }
276 }
277
278 // Импорт базы данных из JSON файла
279 async function importDatabase() {
280 return new Promise((resolve) => {
281 // Создаем input для выбора файла
282 const input = document.createElement('input');
283 input.type = 'file';
284 input.accept = '.json';
285
286 input.onchange = async (e) => {
287 try {
288 const file = e.target.files[0];
289 if (!file) {
290 resolve(false);
291 return;
292 }
293
294 // Читаем файл
295 const reader = new FileReader();
296 reader.onload = async (event) => {
297 try {
298 const importData = JSON.parse(event.target.result);
299
300 // Валидация структуры
301 if (!importData.database || !importData.database.contacts) {
302 throw new Error('Неверный формат файла базы данных');
303 }
304
305 const currentAccount = getCurrentAccount();
306 const importedAccount = importData.account || 'Неизвестный аккаунт';
307 const contactsCount = Object.keys(importData.database.contacts).length;
308
309 // Загружаем текущую базу
310 const currentDb = await loadContactsDatabase();
311 const existingContactsCount = Object.keys(currentDb.contacts).length;
312
313 // Подсчитываем новые контакты
314 let newContactsCount = 0;
315 for (const chatId in importData.database.contacts) {
316 if (!currentDb.contacts[chatId]) {
317 newContactsCount++;
318 }
319 }
320
321 // Подтверждение импорта
322 const confirmMessage = 'Импортировать базу данных?\n\n' +
323 `Импортируемый аккаунт: ${importedAccount}\n` +
324 `Текущий аккаунт: ${currentAccount}\n` +
325 `Контактов в файле: ${contactsCount}\n` +
326 `Контактов в текущей базе: ${existingContactsCount}\n` +
327 `Новых контактов для добавления: ${newContactsCount}\n` +
328 `Дата экспорта: ${importData.exportDate ? new Date(importData.exportDate).toLocaleString('ru-RU') : 'Неизвестно'}\n\n` +
329 '✅ Существующие контакты будут сохранены!\n' +
330 '➕ Будут добавлены только новые контакты.';
331
332 if (!confirm(confirmMessage)) {
333 resolve(false);
334 return;
335 }
336
337 // Загружаем всю структуру с аккаунтами
338 const allDatabases = await getDBValue('contactsDatabaseByAccount', {});
339
340 // Получаем текущую базу для аккаунта
341 if (!allDatabases[currentAccount]) {
342 allDatabases[currentAccount] = {
343 contacts: {},
344 lastUpdate: null
345 };
346 }
347
348 // ИЗМЕНЕНО: Объединяем контакты вместо замены
349 // Добавляем только новые контакты из импортируемой базы
350 let actuallyAdded = 0;
351 for (const chatId in importData.database.contacts) {
352 if (!allDatabases[currentAccount].contacts[chatId]) {
353 allDatabases[currentAccount].contacts[chatId] = importData.database.contacts[chatId];
354 actuallyAdded++;
355 }
356 }
357
358 // Обновляем дату последнего обновления
359 allDatabases[currentAccount].lastUpdate = new Date().toISOString();
360
361 // Сохраняем
362 await setDBValue('contactsDatabaseByAccount', allDatabases);
363
364 console.log('✅ База данных импортирована (добавлено новых контактов)');
365 alert(`✅ База данных успешно импортирована!\n\nДобавлено новых контактов: ${actuallyAdded}\nВсего контактов в базе: ${Object.keys(allDatabases[currentAccount].contacts).length}\nАккаунт: ${currentAccount}`);
366
367 // Обновляем статистику в интерфейсе
368 await updateDatabaseStats();
369
370 resolve(true);
371 } catch (error) {
372 console.error('❌ Ошибка парсинга файла:', error);
373 alert('❌ Ошибка чтения файла: ' + error.message);
374 resolve(false);
375 }
376 };
377
378 reader.onerror = () => {
379 alert('❌ Ошибка чтения файла');
380 resolve(false);
381 };
382
383 reader.readAsText(file);
384 } catch (error) {
385 console.error('❌ Ошибка импорта:', error);
386 alert('❌ Ошибка импорта: ' + error.message);
387 resolve(false);
388 }
389 };
390
391 // Открываем диалог выбора файла
392 input.click();
393 });
394 }
395
396 // ============= ФУНКЦИИ ДЛЯ РАБОТЫ С БАЗОЙ КОНТАКТОВ =============
397
398 // Функция для определения текущего аккаунта
399 function getCurrentAccount() {
400 try {
401 // Ищем элемент с названием аккаунта
402 const accountElement = document.querySelector('.index_companyItem_wgEhc.index_hasSelect_GanwO');
403 if (accountElement) {
404 const accountName = accountElement.textContent.trim();
405 console.log('Определен текущий аккаунт:', accountName);
406 return accountName;
407 }
408
409 console.warn('Элемент с названием аккаунта не найден, используем "Неизвестный аккаунт"');
410 return 'Неизвестный аккаунт';
411 } catch (error) {
412 console.error('Ошибка при определении аккаунта:', error);
413 return 'Неизвестный аккаунт';
414 }
415 }
416
417 // Загрузка базы контактов
418 async function loadContactsDatabase() {
419 try {
420 // Загружаем всю структуру с аккаунтами
421 const allDatabases = await getDBValue('contactsDatabaseByAccount', {});
422
423 // Определяем текущий аккаунт
424 const currentAccount = getCurrentAccount();
425
426 // Если для текущего аккаунта нет базы - создаем пустую
427 if (!allDatabases[currentAccount]) {
428 allDatabases[currentAccount] = {
429 contacts: {},
430 lastUpdate: null
431 };
432 }
433
434 const database = allDatabases[currentAccount];
435 console.log('База контактов загружена для аккаунта:', currentAccount, '|', Object.keys(database.contacts).length, 'контактов');
436 return database;
437 } catch (error) {
438 console.error('Ошибка загрузки базы контактов:', error);
439 return {
440 contacts: {},
441 lastUpdate: null
442 };
443 }
444 }
445
446 // Сохранение базы контактов
447 async function saveContactsDatabase(database) {
448 try {
449 // Загружаем всю структуру
450 const allDatabases = await getDBValue('contactsDatabaseByAccount', {});
451
452 // Определяем текущий аккаунт
453 const currentAccount = getCurrentAccount();
454
455 // Обновляем дату последнего обновления
456 database.lastUpdate = new Date().toISOString();
457
458 // Сохраняем базу для текущего аккаунта
459 allDatabases[currentAccount] = database;
460
461 // Сохраняем всю структуру
462 await setDBValue('contactsDatabaseByAccount', allDatabases);
463
464 console.log('✅ База контактов сохранена для аккаунта:', currentAccount, '|', Object.keys(database.contacts).length, 'контактов');
465 return true;
466 } catch (error) {
467 console.error('❌ Ошибка сохранения базы контактов:', error);
468 return false;
469 }
470 }
471
472 // Добавление контакта в базу
473 function addContactToDatabase(database, chatId, contactData) {
474 database.contacts[chatId] = {
475 id: chatId,
476 name: contactData.name,
477 lastMessageDate: contactData.lastMessageDate,
478 lastSentDate: contactData.lastSentDate,
479 messageCount: contactData.messageCount || 0
480 };
481 }
482
483 // Получение статистики базы данных
484 function getDatabaseStats(database) {
485 const contacts = Object.values(database.contacts);
486 const total = contacts.length;
487 const withDates = contacts.filter(c => c.lastMessageDate).length;
488 const sent = contacts.filter(c => c.lastSentDate).length;
489 const notSent = total - sent;
490
491 return {
492 total,
493 withDates,
494 sent,
495 notSent,
496 lastUpdate: database.lastUpdate
497 };
498 }
499
500 // Очистка базы контактов
501 async function clearContactsDatabase() {
502 const currentAccount = getCurrentAccount();
503
504 if (confirm(`Вы уверены, что хотите очистить базу контактов для аккаунта "${currentAccount}"?\n\nЭто действие нельзя отменить.`)) {
505 try {
506 // Загружаем всю структуру
507 const allDatabases = await getDBValue('contactsDatabaseByAccount', {});
508
509 // Очищаем базу для текущего аккаунта
510 allDatabases[currentAccount] = {
511 contacts: {},
512 lastUpdate: null
513 };
514
515 // Сохраняем обновленную структуру
516 await setDBValue('contactsDatabaseByAccount', allDatabases);
517
518 console.log('✅ База контактов очищена для аккаунта:', currentAccount);
519 return true;
520 } catch (error) {
521 console.error('❌ Ошибка очистки базы контактов:', error);
522 return false;
523 }
524 }
525 return false;
526 }
527
528 // Удаление дубликатов из базы
529 async function removeDuplicatesFromDatabase() {
530 const database = await loadContactsDatabase();
531 const contacts = Object.values(database.contacts);
532
533 // Группируем по имени
534 const grouped = {};
535 contacts.forEach(contact => {
536 if (!grouped[contact.name]) {
537 grouped[contact.name] = [];
538 }
539 grouped[contact.name].push(contact);
540 });
541
542 // Находим дубликаты
543 let duplicatesCount = 0;
544 Object.keys(grouped).forEach(name => {
545 const group = grouped[name];
546 if (group.length > 1) {
547 // Оставляем только первый контакт
548 for (let i = 1; i < group.length; i++) {
549 delete database.contacts[group[i].id];
550 duplicatesCount++;
551 }
552 }
553 });
554
555 if (duplicatesCount > 0) {
556 await saveContactsDatabase(database);
557 alert(`Удалено дубликатов: ${duplicatesCount}`);
558 } else {
559 alert('Дубликаты не найдены');
560 }
561 }
562
563 // ============= ФУНКЦИИ ДЛЯ РАБОТЫ С СОСТОЯНИЕМ РАССЫЛКИ =============
564
565 // Загрузка состояния рассылки
566 async function loadSendingState() {
567 try {
568 const state = await getDBValue('sendingState', null);
569 if (state) {
570 console.log('Состояние рассылки загружено:', state);
571 }
572 return state;
573 } catch (error) {
574 console.error('Ошибка загрузки состояния рассылки:', error);
575 return null;
576 }
577 }
578
579 // Сохранение состояния рассылки
580 async function saveSendingState(state) {
581 try {
582 await setDBValue('sendingState', state);
583 console.log('✅ Состояние рассылки сохранено');
584 return true;
585 } catch (error) {
586 console.error('❌ Ошибка сохранения состояния рассылки:', error);
587 return false;
588 }
589 }
590
591 // Очистка состояния рассылки
592 async function clearSendingState() {
593 try {
594 await deleteDBValue('sendingState');
595 console.log('✅ Состояние рассылки очищено');
596 return true;
597 } catch (error) {
598 console.error('❌ Ошибка очистки состояния рассылки:', error);
599 return false;
600 }
601 }
602
603 // ============= ФУНКЦИИ ДЛЯ РАБОТЫ С ЧЕРНЫМ СПИСКОМ =============
604
605 // Загрузка черного списка
606 async function loadBlacklist() {
607 try {
608 // Загружаем всю структуру с аккаунтами
609 const allBlacklists = await getDBValue('blacklistByAccount', {});
610
611 // Определяем текущий аккаунт
612 const currentAccount = getCurrentAccount();
613
614 // Если для текущего аккаунта нет черного списка - создаем пустой
615 if (!allBlacklists[currentAccount]) {
616 allBlacklists[currentAccount] = [];
617 }
618
619 const blacklist = allBlacklists[currentAccount];
620 console.log('Черный список загружен для аккаунта:', currentAccount, '|', blacklist.length, 'ID');
621 return blacklist;
622 } catch (error) {
623 console.error('Ошибка загрузки черного списка:', error);
624 return [];
625 }
626 }
627
628 // Сохранение черного списка
629 async function saveBlacklist(blacklist) {
630 try {
631 // Загружаем всю структуру
632 const allBlacklists = await getDBValue('blacklistByAccount', {});
633
634 // Определяем текущий аккаунт
635 const currentAccount = getCurrentAccount();
636
637 // Сохраняем черный список для текущего аккаунта
638 allBlacklists[currentAccount] = blacklist;
639
640 // Сохраняем всю структуру
641 await setDBValue('blacklistByAccount', allBlacklists);
642
643 console.log('✅ Черный список сохранен для аккаунта:', currentAccount, '|', blacklist.length, 'ID');
644 return true;
645 } catch (error) {
646 console.error('❌ Ошибка сохранения черного списка:', error);
647 return false;
648 }
649 }
650
651 // Добавление ID в черный список
652 async function addToBlacklist(chatIds) {
653 const blacklist = await loadBlacklist();
654 const db = await loadContactsDatabase();
655
656 let addedCount = 0;
657 let removedFromDbCount = 0;
658
659 for (const chatId of chatIds) {
660 // Проверяем, нет ли уже в черном списке
661 if (!blacklist.includes(chatId)) {
662 blacklist.push(chatId);
663 addedCount++;
664 console.log('Добавлен в ЧС:', chatId);
665 }
666
667 // Удаляем из базы контактов, если есть
668 if (db.contacts[chatId]) {
669 delete db.contacts[chatId];
670 removedFromDbCount++;
671 console.log('Удален из базы:', chatId);
672 }
673 }
674
675 await saveBlacklist(blacklist);
676 await saveContactsDatabase(db);
677
678 return { addedCount, removedFromDbCount };
679 }
680
681 // Удаление ID из черного списка
682 async function removeFromBlacklist(chatIds) {
683 const blacklist = await loadBlacklist();
684 const db = await loadContactsDatabase();
685
686 let removedCount = 0;
687 let addedToDbCount = 0;
688
689 for (const chatId of chatIds) {
690 const index = blacklist.indexOf(chatId);
691 if (index > -1) {
692 blacklist.splice(index, 1);
693 removedCount++;
694 console.log('Удален из ЧС:', chatId);
695
696 // Возвращаем в базу контактов
697 if (!db.contacts[chatId]) {
698 addContactToDatabase(db, chatId, {
699 name: 'Восстановлен из ЧС',
700 lastMessageDate: null,
701 lastSentDate: null,
702 messageCount: 0
703 });
704 addedToDbCount++;
705 console.log('Возвращен в базу:', chatId);
706 }
707 }
708 }
709
710 await saveBlacklist(blacklist);
711 await saveContactsDatabase(db);
712
713 return { removedCount, addedToDbCount };
714 }
715
716 // Проверка, находится ли ID в черном списке
717 async function isInBlacklist(chatId) {
718 const blacklist = await loadBlacklist();
719 return blacklist.includes(chatId);
720 }
721
722 // Очистка черного списка
723 async function clearBlacklist() {
724 const blacklist = await loadBlacklist();
725 const count = blacklist.length;
726
727 if (count === 0) {
728 alert('Черный список уже пуст');
729 return { count: 0 };
730 }
731
732 if (confirm(`Вы уверены, что хотите очистить весь черный список? (${count} ID)\n\nВнимание: ID будут возвращены в базу контактов.`)) {
733 // Возвращаем все ID в базу
734 const result = await removeFromBlacklist(blacklist);
735 console.log('Черный список очищен');
736 return result;
737 }
738
739 return { count: 0 };
740 }
741
742 // Массовое добавление видимых чатов в черный список
743 async function collectChatsToBlacklist() {
744 console.log('=== НАЧАЛО МАССОВОГО ДОБАВЛЕНИЯ В ЧЕРНЫЙ СПИСОК ===');
745
746 const chatIdsToAdd = [];
747 let scrollAttempts = 0;
748 let noNewChatsCount = 0;
749 const maxNoNewChatsAttempts = 3;
750
751 // Трекер обработанных ID в этой сессии
752 const processedInSession = new Set();
753
754 while (!shouldStop) {
755 // Проверка на паузу
756 while (isPaused && !shouldStop) {
757 updateStatus('Пауза сбора ЧС', `Собрано: ${chatIdsToAdd.length} ID`);
758 await sleep(500);
759 }
760
761 if (shouldStop) {
762 console.log('Сбор ЧС остановлен пользователем');
763 break;
764 }
765
766 // Получаем текущие видимые чаты
767 const currentChats = getAllChats();
768 console.log(`Попытка ${scrollAttempts + 1}: найдено ${currentChats.length} чатов в DOM`);
769
770 // Фильтруем только те чаты, которые еще не обработали
771 const newChats = currentChats.filter(chat => {
772 const chatId = extractChatId(chat);
773 return chatId && !processedInSession.has(chatId);
774 });
775
776 console.log('Новых необработанных чатов:', newChats.length);
777
778 // Если нет новых чатов - увеличиваем счетчик
779 if (newChats.length === 0) {
780 noNewChatsCount++;
781 console.log(`Новые чаты не появились (${noNewChatsCount}/${maxNoNewChatsAttempts})`);
782
783 if (noNewChatsCount >= maxNoNewChatsAttempts) {
784 console.log('Достигнут конец списка чатов');
785 break;
786 }
787
788 // Скроллим для подгрузки новых
789 console.log('Скроллим для подгрузки новых чатов...');
790 await scrollChatList();
791 await sleep(5000);
792 continue;
793 } else {
794 noNewChatsCount = 0;
795 }
796
797 // Обрабатываем новые чаты
798 for (const chat of newChats) {
799 // Проверка на паузу
800 while (isPaused && !shouldStop) {
801 updateStatus('Пауза сбора базы', `Новых: ${newContactsSet.size}, Обновлено: ${updatedContactsSet.size}, Пропущено (ЧС): ${skippedBlacklist}`);
802 await sleep(500);
803 }
804
805 if (shouldStop) break;
806
807 const chatId = extractChatId(chat);
808 const chatName = chat.querySelector('.index_chatTitle_TiXTq')?.textContent ||
809 chat.querySelector('.index_chatTitle_x9txX')?.textContent ||
810 'Неизвестно';
811
812 // Добавляем ID в кольцевой буфер
813 const isNew = addToRecentIds(chatId);
814 if (!isNew) {
815 console.log('Чат уже обработан (в буфере):', chatName);
816 continue;
817 }
818
819 // Проверяем черный список
820 if (blacklist.has(chatId)) {
821 console.log(`⛔ Чат в черном списке, пропускаем: ${chatName} (${chatId})`);
822 skippedBlacklist++;
823 continue;
824 }
825
826 // Получаем данные чата из элемента списка
827 const chatData = getChatDataFromElement(chat);
828
829 // Проверяем, есть ли уже в базе
830 const isExisting = db.contacts[chatId];
831
832 if (isExisting) {
833 // Обновляем существующий контакт
834 db.contacts[chatId].lastMessageDate = chatData.lastMessageDate || db.contacts[chatId].lastMessageDate;
835 updatedContactsSet.add(chatId);
836 console.log(`🔄 Обновлен контакт: ${chatName} (${chatId})`);
837 } else {
838 // Добавляем новый контакт
839 addContactToDatabase(db, chatId, chatData);
840 newContactsSet.add(chatId);
841 console.log(`➕ Добавлен новый контакт: ${chatName} (${chatId})`);
842 }
843
844 updateStatus(
845 `Сбор базы: ${chatName}`,
846 `Обработано: ${newContactsSet.size + updatedContactsSet.size}, Новых: ${newContactsSet.size}, Обновлено: ${updatedContactsSet.size}, Пропущено (ЧС): ${skippedBlacklist}`
847 );
848
849 // Периодическое сохранение
850 contactsSinceLastSave++;
851 if (contactsSinceLastSave >= saveEveryNContacts) {
852 console.log(`Промежуточное сохранение базы (${newContactsSet.size + updatedContactsSet.size} контактов)...`);
853 await saveContactsDatabase(db);
854 contactsSinceLastSave = 0;
855 }
856 }
857
858 // Скроллим для подгрузки следующей порции
859 if (!shouldStop && newChats.length > 0) {
860 await scrollChatList();
861 await sleep(3000);
862 }
863 }
864
865 // Финальное сохранение базы
866 console.log('Финальное сохранение базы данных...');
867 await saveContactsDatabase(db);
868
869 // ОПТИМИЗАЦИЯ: Финальная очистка памяти
870 console.log('🧹 Выполняем агрессивную очистку памяти...');
871
872 // Очищаем кольцевой буфер до минимума
873 while (recentIdsQueue.length > 100) {
874 const oldestId = recentIdsQueue.shift();
875 recentlyProcessedIds.delete(oldestId);
876 }
877 console.log('Кольцевой буфер очищен до 100 ID');
878
879 // АГРЕССИВНАЯ ОЧИСТКА DOM: Удаляем ВСЕ чаты кроме последних 50
880 const allChatsInDom = document.querySelectorAll('.index_chat_4fr82, .index_chat_EHlBq');
881 const totalChatsInDom = allChatsInDom.length;
882 console.log(`📊 Всего чатов в DOM перед очисткой: ${totalChatsInDom}`);
883
884 if (totalChatsInDom > 50) {
885 // Удаляем все чаты кроме последних 50
886 const chatsToRemove = totalChatsInDom - 50;
887 let removedCount = 0;
888
889 for (let i = 0; i < chatsToRemove; i++) {
890 if (allChatsInDom[i]) {
891 allChatsInDom[i].remove();
892 removedCount++;
893 }
894 }
895
896 console.log(`🗑️ АГРЕССИВНАЯ ОЧИСТКА: Удалено ${removedCount} чатов из DOM`);
897 console.log('✅ Оставлено последних 50 чатов для продолжения работы');
898
899 // ОБНОВЛЯЕМ СТАТУС ПОСЛЕ ОЧИСТКИ
900 updateStatus(
901 `Сбор базы: очистка памяти (удалено ${removedCount} чатов из DOM)`,
902 `Обработано: ${newContactsSet.size + updatedContactsSet.size}, Новых: ${newContactsSet.size}, Обновлено: ${updatedContactsSet.size}, Пропущено (ЧС): ${skippedBlacklist}`
903 );
904
905 // Проверяем результат
906 const remainingChats = document.querySelectorAll('.index_chat_4fr82, .index_chat_EHlBq').length;
907
908 if (remainingChats > 200) {
909 console.warn(`⚠️ ПРОБЛЕМА: После очистки осталось ${remainingChats} чатов!`);
910 console.warn('Рекомендация: Остановите сбор, перезагрузите страницу и продолжите.');
911
912 // Показываем предупреждение пользователю
913 const shouldContinue = confirm(
914 '⚠️ ВНИМАНИЕ: Проблема с памятью не решена!\n\n' +
915 `После очистки осталось ${remainingChats} чатов в DOM.\n` +
916 'Виртуализация Ozon не работает корректно.\n\n' +
917 'Рекомендуется:\n' +
918 '1. Остановить сбор (нажмите "Отмена")\n' +
919 '2. Перезагрузить страницу\n' +
920 '3. Продолжить сбор\n\n' +
921 'Продолжить сбор сейчас? (не рекомендуется)'
922 );
923
924 if (!shouldContinue) {
925 shouldStop = true;
926 // Прерываем цикл через флаг shouldStop
927 }
928 }
929 } else {
930 console.log(`✅ В DOM ${totalChatsInDom} чатов - очистка не требуется`);
931 }
932
933 iterationsSinceCleanup = 0;
934 }
935
936 // Функция для создания модального окна черного списка
937 function createBlacklistModal() {
938 // Проверяем, не создано ли уже окно
939 if (document.getElementById('blacklistModal')) {
940 return;
941 }
942
943 const modalHTML = `
944 <div id="blacklistModal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 10002; justify-content: center; align-items: center;">
945 <div style="background: white; padding: 30px; border-radius: 12px; width: 700px; max-width: 95%; max-height: 90vh; overflow-y: auto; box-shadow: 0 4px 20px rgba(0,0,0,0.3);">
946 <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
947 <h2 style="margin: 0; color: #333; font-size: 24px;">🚫 Черный список</h2>
948 <button id="closeBlacklistButton" style="padding: 8px 16px; background: #999; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 600;">Закрыть</button>
949 </div>
950
951 <div id="blacklistStatsBlock" style="margin-bottom: 20px; padding: 15px; background: #fff3f3; border-radius: 6px; border-left: 4px solid #f44336;">
952 <div style="font-weight: 600; color: #f44336; margin-bottom: 10px;">Статистика черного списка:</div>
953 <div id="blacklistStatsContent" style="color: #333; font-size: 14px;">Загрузка...</div>
954 </div>
955
956 <div style="display: flex; gap: 10px; margin-bottom: 20px; flex-wrap: wrap;">
957 <button id="massAddBlacklistButton" style="padding: 10px 18px; background: #ff5722; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 600;">📥 Массовое добавление</button>
958 <button id="clearAllBlacklistButton" style="padding: 10px 18px; background: #f44336; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 600;">🗑️ Очистить все</button>
959 <button id="pauseBlacklistButton" style="display: none; padding: 10px 18px; background: #ff9800; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 600;">⏸️ Пауза</button>
960 <button id="stopBlacklistButton" style="display: none; padding: 10px 18px; background: #f44336; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 600;">⏹️ Стоп</button>
961 </div>
962
963 <div style="margin-bottom: 15px;">
964 <input type="text" id="searchBlacklistInput" placeholder="Поиск по ID..." style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px;">
965 </div>
966
967 <div id="blacklistContainer" style="max-height: 400px; overflow-y: auto; border: 1px solid #ddd; border-radius: 6px;">
968 <div id="blacklistContent" style="padding: 10px;"></div>
969 </div>
970 </div>
971 </div>
972 `;
973
974 document.body.insertAdjacentHTML('beforeend', modalHTML);
975 console.log('Модальное окно черного списка создано');
976
977 // Обработчики событий
978 document.getElementById('closeBlacklistButton').addEventListener('click', closeBlacklistModal);
979
980 document.getElementById('addManualBlacklistButton').addEventListener('click', async () => {
981 const input = document.getElementById('manualBlacklistId');
982 const chatId = input.value.trim();
983
984 if (!chatId) {
985 alert('Введите ID чата');
986 return;
987 }
988
989 const result = await addToBlacklist([chatId]);
990 alert(`✅ ID добавлен в черный список!\n\nДобавлено в ЧС: ${result.addedCount}\nУдалено из базы: ${result.removedFromDbCount}`);
991
992 input.value = '';
993 await loadAndDisplayBlacklist();
994 await updateDatabaseStats();
995 });
996
997 document.getElementById('massAddBlacklistButton').addEventListener('click', async () => {
998 if (confirm('Начать массовое добавление видимых чатов в черный список?\n\nВсе видимые чаты будут добавлены в ЧС и удалены из базы контактов.')) {
999 // Показываем кнопки паузы и остановки
1000 document.getElementById('massAddBlacklistButton').style.display = 'none';
1001 document.getElementById('pauseBlacklistButton').style.display = 'inline-block';
1002 document.getElementById('stopBlacklistButton').style.display = 'inline-block';
1003
1004 shouldStop = false;
1005 isPaused = false;
1006
1007 await collectChatsToBlacklist();
1008
1009 // Скрываем кнопки паузы и остановки
1010 document.getElementById('massAddBlacklistButton').style.display = 'inline-block';
1011 document.getElementById('pauseBlacklistButton').style.display = 'none';
1012 document.getElementById('stopBlacklistButton').style.display = 'none';
1013
1014 await loadAndDisplayBlacklist();
1015 await updateDatabaseStats();
1016 }
1017 });
1018
1019 document.getElementById('clearAllBlacklistButton').addEventListener('click', async () => {
1020 const result = await clearBlacklist();
1021 if (result.count > 0 || result.removedCount > 0) {
1022 alert(`✅ Черный список очищен!\n\nУдалено из ЧС: ${result.removedCount}\nВозвращено в базу: ${result.addedToDbCount}`);
1023 await loadAndDisplayBlacklist();
1024 await updateDatabaseStats();
1025 }
1026 });
1027
1028 // Обработчик паузы
1029 document.getElementById('pauseBlacklistButton').addEventListener('click', () => {
1030 isPaused = !isPaused;
1031 const pauseButton = document.getElementById('pauseBlacklistButton');
1032
1033 if (isPaused) {
1034 pauseButton.textContent = '▶️ Продолжить';
1035 pauseButton.style.background = '#4caf50';
1036 console.log('Сбор ЧС приостановлен');
1037 } else {
1038 pauseButton.textContent = '⏸️ Пауза';
1039 pauseButton.style.background = '#ff9800';
1040 console.log('Сбор ЧС возобновлен');
1041 }
1042 });
1043
1044 // Обработчик остановки
1045 document.getElementById('stopBlacklistButton').addEventListener('click', () => {
1046 if (confirm('Вы уверены, что хотите остановить сбор?')) {
1047 shouldStop = true;
1048 isPaused = false;
1049 console.log('Запрошена остановка сбора ЧС');
1050 }
1051 });
1052
1053 // Поиск по черному списку
1054 document.getElementById('searchBlacklistInput').addEventListener('input', (e) => {
1055 filterBlacklist(e.target.value);
1056 });
1057 }
1058
1059 // Функция для открытия модального окна черного списка
1060 async function openBlacklistModal() {
1061 createBlacklistModal();
1062 const modal = document.getElementById('blacklistModal');
1063 if (modal) {
1064 modal.style.display = 'flex';
1065 await loadAndDisplayBlacklist();
1066 console.log('Модальное окно черного списка открыто');
1067 }
1068 }
1069
1070 // Функция для закрытия модального окна черного списка
1071 function closeBlacklistModal() {
1072 const modal = document.getElementById('blacklistModal');
1073 if (modal) {
1074 modal.style.display = 'none';
1075 console.log('Модальное окно черного списка закрыто');
1076 }
1077 }
1078
1079 // Функция для загрузки и отображения черного списка
1080 async function loadAndDisplayBlacklist(searchQuery = '') {
1081 const blacklist = await loadBlacklist();
1082
1083 // Обновляем статистику
1084 const statsContent = document.getElementById('blacklistStatsContent');
1085 if (statsContent) {
1086 statsContent.innerHTML = `
1087 <div><strong>Всего ID в черном списке:</strong> ${blacklist.length}</div>
1088 `;
1089 }
1090
1091 // Фильтруем по поисковому запросу
1092 const filteredBlacklist = searchQuery
1093 ? blacklist.filter(id => id.toLowerCase().includes(searchQuery.toLowerCase()))
1094 : blacklist;
1095
1096 // Отображаем список
1097 const blacklistContent = document.getElementById('blacklistContent');
1098 if (blacklistContent) {
1099 if (filteredBlacklist.length === 0) {
1100 blacklistContent.innerHTML = '<div style="text-align: center; padding: 40px; color: #999;">Черный список пуст</div>';
1101 } else {
1102 blacklistContent.innerHTML = filteredBlacklist.map(chatId => {
1103 return `
1104 <div style="padding: 12px; margin-bottom: 8px; background: #fff8f8; border: 1px solid #ffcdd2; border-radius: 6px; font-size: 13px; display: flex; justify-content: space-between; align-items: center;">
1105 <div style="font-family: monospace; color: #333;">${chatId}</div>
1106 <button class="removeFromBlacklistBtn" data-chat-id="${chatId}" style="padding: 6px 12px; background: #4caf50; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; font-weight: 600;">✓ Вернуть</button>
1107 </div>
1108 `;
1109 }).join('');
1110
1111 // Добавляем обработчики для кнопок удаления
1112 document.querySelectorAll('.removeFromBlacklistBtn').forEach(btn => {
1113 btn.addEventListener('click', async (e) => {
1114 const chatId = e.target.getAttribute('data-chat-id');
1115 if (confirm(`Удалить ID из черного списка?\n\n${chatId}\n\nID будет возвращен в базу контактов.`)) {
1116 const result = await removeFromBlacklist([chatId]);
1117 alert(`✅ ID удален из черного списка!\n\nУдалено из ЧС: ${result.removedCount}\nВозвращено в базу: ${result.addedToDbCount}`);
1118 await loadAndDisplayBlacklist();
1119 await updateDatabaseStats();
1120 }
1121 });
1122 });
1123 }
1124 }
1125 }
1126
1127 // Функция для фильтрации черного списка
1128 function filterBlacklist(searchQuery) {
1129 loadAndDisplayBlacklist(searchQuery);
1130 }
1131
1132 // Функция для создания модального окна просмотра базы
1133 function createViewDatabaseModal() {
1134 // Проверяем, не создано ли уже окно
1135 if (document.getElementById('viewDbModal')) {
1136 return;
1137 }
1138
1139 const modalHTML = `
1140 <div id="viewDbModal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 10001; justify-content: center; align-items: center;">
1141 <div style="background: white; padding: 30px; border-radius: 12px; width: 800px; max-width: 95%; max-height: 90vh; overflow-y: auto; box-shadow: 0 4px 20px rgba(0,0,0,0.3);">
1142 <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
1143 <h2 style="margin: 0; color: #333; font-size: 24px;">👁️ Просмотр базы контактов</h2>
1144 <button id="closeViewDbButton" style="padding: 8px 16px; background: #999; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 600;">Закрыть</button>
1145 </div>
1146
1147 <div id="viewDbStatsBlock" style="margin-bottom: 20px; padding: 15px; background: #f0f8ff; border-radius: 6px; border-left: 4px solid #0066cc;">
1148 <div style="font-weight: 600; color: #0066cc; margin-bottom: 10px;">Статистика базы:</div>
1149 <div id="viewDbStatsContent" style="color: #333; font-size: 14px;"></div>
1150 </div>
1151
1152 <div style="display: flex; gap: 10px; align-items: center;">
1153 <input type="text" id="searchContactInput" placeholder="Поиск по имени..." style="flex: 1; padding: 10px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px;">
1154 <button id="removeDuplicatesButton" style="padding: 10px 18px; background: #ff9800; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 600;">🗑️ Удалить дубликаты</button>
1155 </div>
1156
1157 <div id="contactsListContainer" style="max-height: 500px; overflow-y: auto; border: 1px solid #ddd; border-radius: 6px;">
1158 <div id="contactsList" style="padding: 10px;"></div>
1159 </div>
1160 </div>
1161 </div>
1162 `;
1163
1164 document.body.insertAdjacentHTML('beforeend', modalHTML);
1165 console.log('Модальное окно просмотра базы создано');
1166
1167 // Обработчики событий
1168 document.getElementById('closeViewDbButton').addEventListener('click', closeViewDatabaseModal);
1169 document.getElementById('removeDuplicatesButton').addEventListener('click', async () => {
1170 await removeDuplicatesFromDatabase();
1171 await loadAndDisplayContacts();
1172 });
1173
1174 // Поиск по контактам
1175 document.getElementById('searchContactInput').addEventListener('input', (e) => {
1176 filterContactsList(e.target.value);
1177 });
1178 }
1179
1180 // Функция для открытия модального окна просмотра базы
1181 async function openViewDatabaseModal() {
1182 createViewDatabaseModal();
1183 const modal = document.getElementById('viewDbModal');
1184 if (modal) {
1185 modal.style.display = 'flex';
1186 await loadAndDisplayContacts();
1187 console.log('Модальное окно просмотра базы открыто');
1188 }
1189 }
1190
1191 // Функция для закрытия модального окна просмотра базы
1192 function closeViewDatabaseModal() {
1193 const modal = document.getElementById('viewDbModal');
1194 if (modal) {
1195 modal.style.display = 'none';
1196 console.log('Модальное окно просмотра базы закрыто');
1197 }
1198 }
1199
1200 // Функция для загрузки и отображения контактов
1201 async function loadAndDisplayContacts(searchQuery = '') {
1202 const db = await loadContactsDatabase();
1203 const stats = getDatabaseStats(db);
1204 const contacts = Object.values(db.contacts);
1205
1206 // Обновляем статистику
1207 const statsContent = document.getElementById('viewDbStatsContent');
1208 if (statsContent) {
1209 statsContent.innerHTML = `
1210 <div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px;">
1211 <div><strong>Всего контактов:</strong> ${stats.total}</div>
1212 <div><strong>С датами:</strong> ${stats.withDates}</div>
1213 <div><strong>Отправлено:</strong> ${stats.sent}</div>
1214 <div><strong>Не отправлено:</strong> ${stats.notSent}</div>
1215 </div>
1216 `;
1217 }
1218
1219 // Фильтруем контакты по поисковому запросу
1220 const filteredContacts = searchQuery
1221 ? contacts.filter(c => c.name.toLowerCase().includes(searchQuery.toLowerCase()))
1222 : contacts;
1223
1224 // Отображаем список контактов
1225 const contactsList = document.getElementById('contactsList');
1226 if (contactsList) {
1227 if (filteredContacts.length === 0) {
1228 contactsList.innerHTML = '<div style="text-align: center; padding: 40px; color: #999;">Контакты не найдены</div>';
1229 } else {
1230 contactsList.innerHTML = filteredContacts.map(contact => {
1231 const lastMsgDate = contact.lastMessageDate
1232 ? new Date(contact.lastMessageDate).toLocaleDateString('ru-RU')
1233 : 'Нет данных';
1234 const lastSentDate = contact.lastSentDate
1235 ? new Date(contact.lastSentDate).toLocaleDateString('ru-RU')
1236 : 'Не отправлялось';
1237 const sentStatus = contact.lastSentDate ? '✅' : '⏳';
1238
1239 return `
1240 <div style="padding: 12px; margin-bottom: 8px; background: ${contact.lastSentDate ? '#f0f8ff' : '#fff8f0'}; border: 1px solid #ddd; border-radius: 6px; font-size: 13px;">
1241 <div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 8px;">
1242 <div style="font-weight: 600; color: #333; font-size: 14px;">${sentStatus} ${contact.name}</div>
1243 <div style="font-size: 11px; color: #999;">${contact.id.substring(0, 8)}...</div>
1244 </div>
1245 <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; color: #666;">
1246 <div><strong>Последнее сообщение:</strong> ${lastMsgDate}</div>
1247 <div><strong>Отправлено:</strong> ${lastSentDate}</div>
1248 </div>
1249 ${contact.messageCount > 0 ? `<div style="margin-top: 5px; color: #666;"><strong>Отправлено сообщений:</strong> ${contact.messageCount}</div>` : ''}
1250 </div>
1251 `;
1252 }).join('');
1253 }
1254 }
1255 }
1256
1257 // Функция для фильтрации списка контактов
1258 function filterContactsList(searchQuery) {
1259 loadAndDisplayContacts(searchQuery);
1260 }
1261
1262 // Функция для получения всех чатов
1263 function getAllChats() {
1264 // Сначала проверяем, есть ли результаты поиска (класс index_chat_EHlBq)
1265 const searchChats = document.querySelectorAll('.index_chat_EHlBq');
1266
1267 if (searchChats.length > 0) {
1268 // Если есть результаты поиска, используем их
1269 console.log('Найдены результаты поиска:', searchChats.length);
1270 return Array.from(searchChats);
1271 }
1272
1273 // Если поиска нет, используем обычные чаты
1274 const chats = document.querySelectorAll('.index_chat_4fr82');
1275 // Фильтруем только видимые чаты (учитываем фильтр поиска)
1276 const visibleChats = Array.from(chats).filter(chat => {
1277 const style = window.getComputedStyle(chat);
1278 // Проверяем, что чат не скрыт (убрали проверку высоты, т.к. чаты виртуализированы)
1279 return style.display !== 'none' &&
1280 style.visibility !== 'hidden' &&
1281 style.opacity !== '0' &&
1282 chat.offsetParent !== null; // Проверяем offsetParent для фильтрации
1283 });
1284 console.log('Найдено чатов:', visibleChats.length, '(всего в DOM:', chats.length, ')');
1285 return visibleChats;
1286 }
1287
1288 // Функция для клика по чату
1289 async function clickChat(chat) {
1290 // Пробуем оба селектора для имени чата (обычный и поисковый)
1291 const chatName = chat.querySelector('.index_chatTitle_TiXTq')?.textContent?.trim() ||
1292 chat.querySelector('.index_chatTitle_x9txX')?.textContent ||
1293 'Неизвестно';
1294 console.log('Клик по чату:', chatName);
1295 chat.click();
1296 await sleep(1000); // Сокращено с 2000 до 1000
1297 }
1298
1299 // Функция для открытия чата по ID (через прямую ссылку в новой вкладке)
1300 async function openChatById(chatId) {
1301 console.log('Открытие чата по ID в новой вкладке:', chatId);
1302
1303 // Формируем прямую ссылку на чат
1304 const chatUrl = `https://seller.ozon.ru/app/messenger/?group=customers_v2&id=${chatId}`;
1305 console.log('URL чата:', chatUrl);
1306
1307 // Открываем чат в новой вкладке
1308 await GM.openInTab(chatUrl, false); // false = открыть в активной вкладке (на переднем плане)
1309
1310 // Убираем задержку - вкладка откроется сама
1311
1312 return true;
1313 }
1314
1315 // Функция для получения даты последнего сообщения в открытом чате
1316 function getLastMessageDate() {
1317 const dateElements = document.querySelectorAll('.om_1_n4');
1318 if (dateElements.length > 0) {
1319 // Берем последнюю дату (последнее сообщение)
1320 const lastDateElement = dateElements[dateElements.length - 1];
1321 const dateText = lastDateElement.textContent.trim();
1322 console.log('Дата последнего сообщения:', dateText);
1323 return parseDate(dateText);
1324 }
1325 console.log('Элемент даты не найден');
1326 return null;
1327 }
1328
1329 // Функция для отправки сообщения
1330 async function sendMessage(messageText) {
1331 console.log('Отправка сообщения:', messageText);
1332
1333 // Находим текстовое поле
1334 const textarea = document.querySelector('.om_17_a4');
1335 if (!textarea) {
1336 console.error('Текстовое поле не найдено');
1337 return false;
1338 }
1339
1340 // Вставляем текст
1341 textarea.value = messageText;
1342 textarea.dispatchEvent(new Event('input', { bubbles: true }));
1343 console.log('Текст вставлен в поле');
1344
1345 // Сокращено ожидание с 1000 до 500
1346 await sleep(500);
1347
1348 // Находим кнопку отправки (вторая кнопка с классом om_17_a8)
1349 const sendButtons = document.querySelectorAll('.om_17_a8');
1350 if (sendButtons.length < 2) {
1351 console.error('Кнопка отправки не найдена');
1352 return false;
1353 }
1354
1355 // Кликаем на вторую кнопку (первая - это прикрепление файла)
1356 const sendButton = sendButtons[1];
1357 sendButton.click();
1358 console.log('Сообщение отправлено (клик выполнен)');
1359
1360 // Сокращено ожидание с 1000 до 500
1361 await sleep(500);
1362
1363 return true;
1364 }
1365
1366 // Функция задержки
1367 function sleep(ms) {
1368 return new Promise(resolve => setTimeout(resolve, ms));
1369 }
1370
1371 // Функция для парсинга даты из текста Ozon
1372 function parseDate(dateText) {
1373 if (!dateText) return null;
1374
1375 const today = new Date();
1376 today.setHours(0, 0, 0, 0);
1377
1378 // Если только время (например "11:01", "10:39") - значит сегодня
1379 if (/^\d{1,2}:\d{2}$/.test(dateText)) {
1380 console.log('Формат времени обнаружен:', dateText, '- считаем сегодняшней датой');
1381 return today;
1382 }
1383
1384 // Если "Сегодня" или "сегодня"
1385 if (dateText.toLowerCase().includes('сегодня')) {
1386 return today;
1387 }
1388
1389 // Если "Вчера" или "вчера"
1390 if (dateText.toLowerCase().includes('вчера')) {
1391 const yesterday = new Date(today);
1392 yesterday.setDate(yesterday.getDate() - 1);
1393 return yesterday;
1394 }
1395
1396 // Если формат "28.01" или "21.12" (день.месяц без года)
1397 const shortDateMatch = dateText.match(/^(\d{1,2})\.(\d{1,2})$/);
1398 if (shortDateMatch) {
1399 const day = parseInt(shortDateMatch[1]);
1400 const month = parseInt(shortDateMatch[2]) - 1;
1401 const date = new Date(today.getFullYear(), month, day);
1402 // Если дата в будущем, значит это прошлый год
1403 if (date > today) {
1404 date.setFullYear(date.getFullYear() - 1);
1405 }
1406 console.log('Формат день.месяц обнаружен:', dateText, '- распознано как', date.toLocaleDateString('ru-RU'));
1407 return date;
1408 }
1409
1410 // Если формат "21 янв" или "21 января"
1411 const monthMatch = dateText.match(/(\d{1,2})\s+(янв|фев|мар|апр|мая|июн|июл|авг|сен|окт|ноя|дек)/i);
1412 if (monthMatch) {
1413 const day = parseInt(monthMatch[1]);
1414 const monthStr = monthMatch[2].toLowerCase();
1415
1416 const months = {
1417 'янв': 0, 'фев': 1, 'мар': 2, 'апр': 3, 'мая': 4, 'май': 4,
1418 'июн': 5, 'июл': 6, 'авг': 7, 'сен': 8, 'окт': 9, 'ноя': 10, 'дек': 11
1419 };
1420
1421 const month = months[monthStr.substring(0, 3)];
1422 if (month !== undefined) {
1423 const date = new Date(today.getFullYear(), month, day);
1424 // Если дата в будущем, значит это прошлый год
1425 if (date > today) {
1426 date.setFullYear(date.getFullYear() - 1);
1427 }
1428 return date;
1429 }
1430 }
1431
1432 // Если формат "21.01.2024" или "21/01/2024" или "18.06.2025"
1433 const dateMatch = dateText.match(/(\d{1,2})[./](\d{1,2})[./](\d{4})/);
1434 if (dateMatch) {
1435 const day = parseInt(dateMatch[1]);
1436 const month = parseInt(dateMatch[2]) - 1;
1437 const year = parseInt(dateMatch[3]);
1438 return new Date(year, month, day);
1439 }
1440
1441 console.log('Не удалось распознать дату:', dateText);
1442 return null;
1443 }
1444
1445 // Функция для обновления статуса в интерфейсе
1446 function updateStatus(statusText, progressText = '') {
1447 const statusBlock = document.getElementById('statusBlock');
1448 const statusTextElement = document.getElementById('statusText');
1449 const progressTextElement = document.getElementById('progressText');
1450
1451 if (statusBlock && statusTextElement) {
1452 statusBlock.style.display = 'block';
1453 statusTextElement.textContent = statusText;
1454 if (progressTextElement && progressText) {
1455 progressTextElement.textContent = progressText;
1456 }
1457 }
1458 }
1459
1460 // Функция для открытия модального окна
1461 function openModal() {
1462 const modal = document.getElementById('bulkSenderModal');
1463 if (modal) {
1464 modal.style.display = 'flex';
1465 console.log('Модальное окно открыто');
1466 // Обновляем статистику при открытии
1467 updateDatabaseStats();
1468 }
1469 }
1470
1471 // Функция для закрытия модального окна
1472 function closeModal() {
1473 const modal = document.getElementById('bulkSenderModal');
1474 if (modal) {
1475 modal.style.display = 'none';
1476 console.log('Модальное окно закрыто');
1477 }
1478 }
1479
1480 // Функция переключения паузы
1481 function togglePause() {
1482 isPaused = !isPaused;
1483 const pauseButton = document.getElementById('pauseButton');
1484
1485 if (isPaused) {
1486 pauseButton.textContent = '▶️ Продолжить';
1487 pauseButton.style.background = '#4caf50';
1488 console.log('Рассылка приостановлена');
1489 } else {
1490 pauseButton.textContent = '⏸️ Пауза';
1491 pauseButton.style.background = '#ff9800';
1492 console.log('Рассылка возобновлена');
1493 }
1494 }
1495
1496 // Функция остановки рассылки
1497 function stopBulkSending() {
1498 if (confirm('Вы уверены, что хотите остановить рассылку?')) {
1499 shouldStop = true;
1500 isPaused = false;
1501 console.log('Запрошена остановка рассылки');
1502 }
1503 }
1504
1505 // Основная функция рассылки
1506 async function startBulkSending() {
1507 console.log('=== ФУНКЦИЯ startBulkSending ВЫЗВАНА ===');
1508
1509 // Сразу меняем цвет кнопки для визуальной обратной связи
1510 const startButton = document.getElementById('startButton');
1511 if (startButton) {
1512 startButton.style.background = '#ff9800';
1513 startButton.textContent = 'Запуск...';
1514 startButton.disabled = true;
1515 }
1516
1517 const messageText = document.getElementById('messageText').value.trim();
1518 const filterDateInput = document.getElementById('filterDate').value;
1519 const testModeCheckbox = document.getElementById('testMode');
1520 const isTestMode = testModeCheckbox ? testModeCheckbox.checked : false;
1521
1522 // Получаем выбранный режим рассылки
1523 const sendingMode = document.querySelector('input[name="sendingMode"]:checked')?.value || 'database';
1524
1525 console.log('Текст сообщения:', messageText);
1526 console.log('Дата фильтра:', filterDateInput);
1527 console.log('Тестовый режим:', isTestMode);
1528 console.log('Режим рассылки:', sendingMode);
1529
1530 if (!messageText) {
1531 alert('Введите текст сообщения');
1532 console.log('Остановка: нет текста сообщения');
1533 // Возвращаем кнопку в исходное состояние
1534 if (startButton) {
1535 startButton.style.background = '#0066cc';
1536 startButton.textContent = 'Запустить';
1537 startButton.disabled = false;
1538 }
1539 return;
1540 }
1541
1542 if (!filterDateInput) {
1543 alert('Выберите дату фильтра');
1544 console.log('Остановка: нет даты фильтра');
1545 // Возвращаем кнопку в исходное состояние
1546 if (startButton) {
1547 startButton.style.background = '#0066cc';
1548 startButton.textContent = 'Запустить';
1549 startButton.disabled = false;
1550 }
1551 return;
1552 }
1553
1554 // Выбираем функцию в зависимости от режима
1555 if (sendingMode === 'visible') {
1556 await startBulkSendingByVisibleChats(messageText, filterDateInput, isTestMode);
1557 } else {
1558 await startBulkSendingByDatabase(messageText, filterDateInput, isTestMode);
1559 }
1560 }
1561
1562 // Функция рассылки по базе данных
1563 async function startBulkSendingByDatabase(messageText, filterDateInput, isTestMode) {
1564 console.log('=== РАССЫЛКА ПО БАЗЕ ДАННЫХ ===');
1565
1566 // Загружаем базу данных
1567 const db = await loadContactsDatabase();
1568 const totalContacts = Object.keys(db.contacts).length;
1569
1570 if (totalContacts === 0) {
1571 alert('База контактов пуста! Сначала соберите базу контактов с помощью кнопки "Собрать базу".');
1572 // Возвращаем кнопку в исходное состояние
1573 const startButton = document.getElementById('startButton');
1574 if (startButton) {
1575 startButton.style.background = '#0066cc';
1576 startButton.textContent = 'Запустить';
1577 startButton.disabled = false;
1578 }
1579 return;
1580 }
1581
1582 const filterDate = new Date(filterDateInput);
1583 filterDate.setHours(23, 59, 59, 999); // Устанавливаем конец дня для корректного сравнения
1584
1585 console.log('Всего контактов в базе:', totalContacts);
1586 console.log('Фильтр по дате:', filterDate.toLocaleDateString('ru-RU'));
1587 console.log('Тестовый режим:', isTestMode ? 'ДА (только 1 сообщение)' : 'НЕТ');
1588
1589 // Фильтруем контакты по дате
1590 const contactsToSend = Object.values(db.contacts).filter(contact => {
1591 if (!contact.lastMessageDate) {
1592 console.log('❌ Контакт без даты:', contact.name);
1593 return false;
1594 }
1595 const contactDate = new Date(contact.lastMessageDate);
1596 const matches = contactDate <= filterDate;
1597 console.log(`${matches ? '✅' : '❌'} ${contact.name}: ${contactDate.toLocaleDateString('ru-RU')} ${matches ? '<=' : '>'} ${filterDate.toLocaleDateString('ru-RU')}`);
1598 return matches;
1599 });
1600
1601 console.log('Контактов подходящих по дате:', contactsToSend.length);
1602
1603 if (contactsToSend.length === 0) {
1604 alert(`Нет контактов, подходящих по указанной дате.\n\nВсего в базе: ${totalContacts}\nС датами: ${Object.values(db.contacts).filter(c => c.lastMessageDate).length}\nПодходящих по фильтру (дата <= ${filterDate.toLocaleDateString('ru-RU')}): 0`);
1605 // Возвращаем кнопку в исходное состояние
1606 const startButton = document.getElementById('startButton');
1607 if (startButton) {
1608 startButton.style.background = '#0066cc';
1609 startButton.textContent = 'Запустить';
1610 startButton.disabled = false;
1611 }
1612 return;
1613 }
1614
1615 // Показываем статистику фильтрации
1616 const filterStats = `Всего в базе: ${totalContacts} | Подходящих по фильтру: ${contactsToSend.length}`;
1617 console.log(filterStats);
1618
1619 // Показываем пользователю статистику перед запуском
1620 if (!confirm(`Готово к запуску!\n\nВсего контактов в базе: ${totalContacts}\nПодходящих по фильтру (дата <= ${filterDate.toLocaleDateString('ru-RU')}): ${contactsToSend.length}\n\nТестовый режим: ${isTestMode ? 'ДА (1 сообщение)' : 'НЕТ'}\n\nНачать рассылку?`)) {
1621 console.log('Рассылка отменена пользователем');
1622 // Возвращаем кнопку в исходное состояние
1623 const startButton = document.getElementById('startButton');
1624 if (startButton) {
1625 startButton.style.background = '#0066cc';
1626 startButton.textContent = 'Запустить';
1627 startButton.disabled = false;
1628 }
1629 return;
1630 }
1631
1632 // Сохраняем состояние рассылки
1633 const sendingState = {
1634 isActive: true,
1635 messageText: messageText,
1636 filterDate: filterDate.toISOString(), // Сохраняем дату фильтра
1637 testMode: isTestMode,
1638 contactsList: contactsToSend,
1639 totalContacts: contactsToSend.length,
1640 processed: 0,
1641 sent: 0,
1642 skipped: 0,
1643 currentContactId: contactsToSend[0].id,
1644 firstChatOpened: false // Флаг, что первый чат еще не открыт
1645 };
1646
1647 await saveSendingState(sendingState);
1648 console.log('Состояние рассылки сохранено, запускаем мониторинг');
1649
1650 // Запускаем мониторинг прогресса (он откроет первый чат)
1651 console.log('Запускаем мониторинг прогресса рассылки');
1652 monitorSendingProgress();
1653 }
1654
1655 // Функция рассылки по видимым чатам
1656 async function startBulkSendingByVisibleChats(messageText, filterDateInput, isTestMode) {
1657 console.log('=== РАССЫЛКА ПО ВИДИМЫМ ЧАТАМ ===');
1658
1659 const filterDate = new Date(filterDateInput);
1660 filterDate.setHours(23, 59, 59, 999);
1661
1662 console.log('Фильтр по дате:', filterDate);
1663 console.log('Тестовый режим:', isTestMode ? 'ДА (только 1 сообщение)' : 'НЕТ');
1664
1665 // Загружаем черный список
1666 const blacklist = await loadBlacklist();
1667 console.log('Загружен черный список:', blacklist.length, 'ID');
1668
1669 // Меняем интерфейс
1670 isPaused = false;
1671 shouldStop = false;
1672
1673 const testModeCheckbox = document.getElementById('testMode');
1674 document.getElementById('startButton').style.display = 'none';
1675 document.getElementById('pauseButton').style.display = 'inline-block';
1676 document.getElementById('stopButton').style.display = 'inline-block';
1677 document.getElementById('messageText').disabled = true;
1678 document.getElementById('filterDate').disabled = true;
1679 if (testModeCheckbox) testModeCheckbox.disabled = true;
1680
1681 // Блокируем радио-кнопки режима
1682 document.querySelectorAll('input[name="sendingMode"]').forEach(radio => radio.disabled = true);
1683
1684 updateStatus(
1685 'Запуск рассылки по видимым чатам...',
1686 'Подготовка к рассылке...'
1687 );
1688
1689 let processed = 0;
1690 let sent = 0;
1691 let skipped = 0;
1692 let skippedBlacklist = 0;
1693
1694 // Трекер обработанных ID в этой сессии
1695 const processedChatIds = new Set();
1696
1697 // Счетчик попыток без новых чатов
1698 let noNewChatsCount = 0;
1699 const maxNoNewChatsAttempts = 3;
1700
1701 // Счетчик обработанных чатов с момента последнего скролла
1702 let chatsProcessedSinceScroll = 0;
1703 const scrollAfterChats = 10; // Скроллим каждые 10 обработанных чатов
1704
1705 while (!shouldStop) {
1706 // Проверка на паузу
1707 while (isPaused && !shouldStop) {
1708 updateStatus('Пауза', `Обработано: ${processed}, Отправлено: ${sent}, Пропущено: ${skipped}, ЧС: ${skippedBlacklist}`);
1709 await sleep(500);
1710 }
1711
1712 if (shouldStop) {
1713 updateStatus('Остановлено пользователем', `Обработано: ${processed}, Отправлено: ${sent}, Пропущено: ${skipped}, ЧС: ${skippedBlacklist}`);
1714 break;
1715 }
1716
1717 // Проверка тестового режима
1718 if (isTestMode && sent >= 1) {
1719 console.log('Тестовый режим: собран 1 контакт, останавливаем рассылку');
1720 shouldStop = true;
1721 break;
1722 }
1723
1724 // Проверка режима обновления
1725 if (updateExisting && existingContactIds) {
1726 const remainingToUpdate = Array.from(existingContactIds).filter(id => !recentlyProcessedIds.has(id));
1727 console.log('Осталось обновить контактов:', remainingToUpdate.length, 'из', existingContactIds.size);
1728
1729 if (remainingToUpdate.length === 0) {
1730 console.log('Все контакты из базы обновлены, завершаем');
1731 break;
1732 }
1733 }
1734
1735 // Получаем текущие видимые чаты
1736 const currentChats = getAllChats();
1737 console.log('Найдено видимых чатов в DOM:', currentChats.length);
1738
1739 // Фильтруем только те чаты, которые еще не обработали
1740 const newChats = currentChats.filter(chat => {
1741 const chatId = extractChatId(chat);
1742 if (!chatId) return false;
1743
1744 // Проверяем кольцевой буфер последних ID
1745 if (recentlyProcessedIds.has(chatId)) return false;
1746
1747 return true;
1748 });
1749
1750 console.log('Новых необработанных чатов:', newChats.length);
1751 console.log('В кольцевом буфере ID:', recentlyProcessedIds.size);
1752
1753 // Если нет новых чатов - увеличиваем счетчик
1754 if (newChats.length === 0) {
1755 noNewChatsCount++;
1756 console.log(`Новые чаты не появились (${noNewChatsCount}/${maxNoNewChatsAttempts})`);
1757
1758 if (noNewChatsCount >= maxNoNewChatsAttempts) {
1759 console.log('Достигнут конец списка чатов (новые чаты не подгружаются после', maxNoNewChatsAttempts, 'попыток)');
1760 break;
1761 }
1762
1763 // Скроллим список чатов для подгрузки новых
1764 console.log('Нет новых чатов, скроллим для подгрузки...');
1765
1766 // ОБНОВЛЯЕМ СТАТУС ПЕРЕД СКРОЛЛОМ
1767 updateStatus(
1768 'Сбор видимых чатов: прокрутка списка...',
1769 `Обработано: ${processed}, Отправлено: ${sent}, Пропущено: ${skipped}, ЧС: ${skippedBlacklist}`
1770 );
1771
1772 await scrollChatList();
1773 await sleep(3000); // УМЕНЬШЕНО с 5000 до 3000
1774 scrollAttempts++;
1775 continue;
1776 } else {
1777 noNewChatsCount = 0;
1778 }
1779
1780 // Обрабатываем новые чаты
1781 for (const chat of newChats) {
1782 // Проверка на паузу
1783 while (isPaused && !shouldStop) {
1784 updateStatus('Пауза', `Обработано: ${processed}, Отправлено: ${sent}, Пропущено: ${skipped}, ЧС: ${skippedBlacklist}`);
1785 await sleep(500);
1786 }
1787
1788 // Проверка на остановку
1789 if (shouldStop) {
1790 break;
1791 }
1792
1793 // Проверка тестового режима
1794 if (isTestMode && sent >= 1) {
1795 console.log('Тестовый режим: собран 1 контакт, останавливаем рассылку');
1796 shouldStop = true;
1797 break;
1798 }
1799
1800 const chatId = extractChatId(chat);
1801 const chatName = chat.querySelector('.index_chatTitle_TiXTq')?.textContent ||
1802 chat.querySelector('.index_chatTitle_x9txX')?.textContent ||
1803 'Неизвестно';
1804
1805 // Добавляем ID в кольцевой буфер
1806 const isNew = addToRecentIds(chatId);
1807 if (!isNew) {
1808 console.log('Чат уже обработан (в буфере):', chatName);
1809 continue;
1810 }
1811
1812 // Проверяем черный список
1813 if (blacklist.includes(chatId)) {
1814 console.log(`⛔ Чат в черном списке, пропускаем: ${chatName} (${chatId})`);
1815 skippedBlacklist++;
1816 continue;
1817 }
1818
1819 updateStatus(
1820 `Обработка чата: ${chatName}`,
1821 `Обработано: ${processed}, Отправлено: ${sent}, Пропущено: ${skipped}, ЧС: ${skippedBlacklist}`
1822 );
1823
1824 console.log(`[${processed + 1}] Обрабатываем чат: ${chatName} (${chatId})`);
1825
1826 // Кликаем по чату
1827 await clickChat(chat);
1828
1829 // Получаем дату последнего сообщения
1830 const lastMessageDate = getLastMessageDate();
1831
1832 if (!lastMessageDate) {
1833 console.log('Не удалось получить дату, пропускаем чат');
1834 skipped++;
1835 processed++;
1836 chatsProcessedSinceScroll++;
1837 continue;
1838 }
1839
1840 // Проверяем дату перед отправкой - сравниваем реальную дату из чата с фильтром
1841 let shouldSend = false;
1842 if (lastMessageDate && filterDate) {
1843 console.log('Сравнение дат:');
1844 console.log(' Дата из чата:', lastMessageDate.toLocaleDateString('ru-RU'));
1845 console.log(' Дата фильтра:', filterDate.toLocaleDateString('ru-RU'));
1846 console.log(' Дата из чата <= Дата фильтра?', lastMessageDate <= filterDate);
1847
1848 // Отправляем только если дата последнего сообщения <= дате фильтра
1849 shouldSend = lastMessageDate <= filterDate;
1850 } else {
1851 console.log('Не удалось получить дату или фильтр, пропускаем отправку');
1852 shouldSend = false;
1853 }
1854
1855 // Отправляем сообщение только если дата подходит
1856 let success = false;
1857 if (shouldSend) {
1858 console.log('✅ Дата подходит, отправляем сообщение');
1859 success = await sendMessage(messageText);
1860 } else {
1861 console.log('⏭️ Пропускаем отправку (дата не подходит)');
1862 success = false;
1863 }
1864
1865 if (success) {
1866 sent++;
1867 console.log(`✅ Сообщение отправлено в чат: ${chatName}`);
1868
1869 // Обновляем базу данных
1870 const db = await loadContactsDatabase();
1871 if (db.contacts[chatId]) {
1872 db.contacts[chatId].lastSentDate = new Date().toISOString();
1873 db.contacts[chatId].messageCount = (db.contacts[chatId].messageCount || 0) + 1;
1874 await saveContactsDatabase(db);
1875 }
1876 } else {
1877 console.error('❌ Ошибка отправки или дата не подходит');
1878 skipped++;
1879 }
1880
1881 processed++;
1882 chatsProcessedSinceScroll++;
1883
1884 // Скроллим список чатов каждые N обработанных чатов для подгрузки новых
1885 if (chatsProcessedSinceScroll >= scrollAfterChats) {
1886 console.log(`Обработано ${chatsProcessedSinceScroll} чатов, скроллим для подгрузки новых...`);
1887 await scrollChatList();
1888 chatsProcessedSinceScroll = 0;
1889 }
1890
1891 // Пауза между чатами - сокращено с 1000 до 500
1892 await sleep(500);
1893 }
1894
1895 // Скроллим для подгрузки следующей порции
1896 if (!shouldStop && newChats.length > 0) {
1897 console.log('Обработана порция чатов, скроллим для подгрузки следующей...');
1898 await scrollChatList();
1899 // ВАЖНО: Ждем дольше, чтобы новые чаты успели отрендериться в DOM
1900 console.log('Ждем 5 секунд для рендеринга новых чатов...');
1901 await sleep(5000);
1902 }
1903 }
1904
1905 // Завершение
1906 isPaused = false;
1907 shouldStop = false;
1908
1909 document.getElementById('startButton').style.display = 'inline-block';
1910 document.getElementById('pauseButton').style.display = 'none';
1911 document.getElementById('stopButton').style.display = 'none';
1912 document.getElementById('messageText').disabled = false;
1913 document.getElementById('filterDate').disabled = false;
1914 if (testModeCheckbox) testModeCheckbox.disabled = false;
1915 document.querySelectorAll('input[name="sendingMode"]').forEach(radio => radio.disabled = false);
1916
1917 updateStatus(
1918 'Рассылка завершена!',
1919 `Всего обработано: ${processed} | Отправлено: ${sent} | Пропущено: ${skipped} | Черный список: ${skippedBlacklist}`
1920 );
1921
1922 console.log('=== РАССЫЛКА ЗАВЕРШЕНА ===');
1923 console.log('Обработано:', processed);
1924 console.log('Отправлено:', sent);
1925 console.log('Пропущено:', skipped);
1926 console.log('Пропущено (ЧС):', skippedBlacklist);
1927 }
1928
1929 // Функция для создания плавающей кнопки
1930 function createFloatingButton() {
1931 // Проверяем, не создана ли уже кнопка
1932 if (document.getElementById('bulkSenderFloatingBtn')) {
1933 return;
1934 }
1935
1936 const button = document.createElement('button');
1937 button.id = 'bulkSenderFloatingBtn';
1938 button.textContent = '📧 Рассылка';
1939 button.style.cssText = `
1940 position: fixed;
1941 bottom: 20px;
1942 right: 20px;
1943 padding: 15px 25px;
1944 background: #0066cc;
1945 color: white;
1946 border: none;
1947 border-radius: 50px;
1948 cursor: pointer;
1949 font-size: 16px;
1950 font-weight: 600;
1951 box-shadow: 0 4px 12px rgba(0, 102, 204, 0.4);
1952 z-index: 9999;
1953 transition: all 0.3s;
1954 `;
1955
1956 button.addEventListener('mouseenter', () => {
1957 button.style.background = '#0052a3';
1958 button.style.transform = 'scale(1.05)';
1959 button.style.boxShadow = '0 6px 16px rgba(0, 102, 204, 0.6)';
1960 });
1961
1962 button.addEventListener('mouseleave', () => {
1963 button.style.background = '#0066cc';
1964 button.style.transform = 'scale(1)';
1965 button.style.boxShadow = '0 4px 12px rgba(0, 102, 204, 0.4)';
1966 });
1967
1968 button.addEventListener('click', openModal);
1969
1970 document.body.appendChild(button);
1971 console.log('Плавающая кнопка создана');
1972 }
1973
1974 // Функция мониторинга прогресса рассылки в главной вкладке
1975 async function monitorSendingProgress() {
1976 console.log('🚀 Запуск мониторинга прогресса рассылки');
1977
1978 let lastProcessed = -1;
1979 let lastOpenedIndex = -1; // Индекс последнего открытого чата
1980 const maxParallelChats = 3; // Максимум одновременно открытых чатов
1981
1982 const monitorInterval = setInterval(async () => {
1983 const currentState = await loadSendingState();
1984
1985 if (!currentState || !currentState.isActive) {
1986 console.log('Рассылка завершена или остановлена, останавливаем мониторинг');
1987 clearInterval(monitorInterval);
1988
1989 // Обновляем интерфейс
1990 const finalStats = currentState ?
1991 `Обработано: ${currentState.processed}/${currentState.totalContacts} | Отправлено: ${currentState.sent} | Пропущено: ${currentState.skipped}` :
1992 'Проверьте результаты';
1993 updateStatus('Рассылка завершена!', finalStats);
1994 await updateDatabaseStats();
1995
1996 // Разблокируем интерфейс
1997 isPaused = false;
1998 document.getElementById('startButton').style.display = 'inline-block';
1999 document.getElementById('startButton').disabled = false;
2000 document.getElementById('startButton').style.background = '#0066cc';
2001 document.getElementById('startButton').textContent = 'Запустить';
2002 document.getElementById('pauseButton').style.display = 'none';
2003 document.getElementById('stopButton').style.display = 'none';
2004 document.getElementById('messageText').disabled = false;
2005 document.getElementById('filterDate').disabled = false;
2006 const testModeCheckbox = document.getElementById('testMode');
2007 if (testModeCheckbox) testModeCheckbox.disabled = false;
2008 document.querySelectorAll('input[name="sendingMode"]').forEach(radio => radio.disabled = false);
2009
2010 return;
2011 }
2012
2013 console.log('🔍 Проверка состояния:', {
2014 processed: currentState.processed,
2015 totalContacts: currentState.totalContacts,
2016 lastOpenedIndex: lastOpenedIndex,
2017 maxParallel: maxParallelChats
2018 });
2019
2020 // Обновляем статус только если изменился прогресс
2021 if (currentState.processed !== lastProcessed) {
2022 lastProcessed = currentState.processed;
2023
2024 const currentContact = currentState.contactsList[currentState.processed - 1];
2025 const contactName = currentContact ? currentContact.name : 'Неизвестно';
2026
2027 updateStatus(
2028 `Обработка: ${contactName}`,
2029 `Обработано: ${currentState.processed}/${currentState.totalContacts} | Отправлено: ${currentState.sent} | Пропущено: ${currentState.skipped}`
2030 );
2031
2032 console.log('📊 Обновлен прогресс:', currentState.processed, '/', currentState.totalContacts);
2033 }
2034
2035 // Открываем чаты параллельно
2036 // Логика: открываем до maxParallelChats чатов вперед от текущего processed
2037 const remainingContacts = currentState.totalContacts - currentState.processed;
2038 const nextIndexToOpen = lastOpenedIndex + 1;
2039
2040 // ИСПРАВЛЕНО: Правильный расчет - открываем чаты так, чтобы всегда было maxParallelChats впереди
2041 // Сколько чатов должно быть открыто впереди текущего processed
2042 const targetOpenAhead = Math.min(maxParallelChats, remainingContacts);
2043 // Сколько чатов уже открыто впереди (от processed до lastOpenedIndex включительно)
2044 const currentlyOpenAhead = Math.max(0, lastOpenedIndex - currentState.processed + 1);
2045 // Сколько еще нужно открыть
2046 const needToOpen = targetOpenAhead - currentlyOpenAhead;
2047 // Сколько осталось до конца списка
2048 const chatsUntilEnd = currentState.totalContacts - nextIndexToOpen;
2049
2050 const chatsToOpen = Math.max(0, Math.min(needToOpen, chatsUntilEnd));
2051
2052 console.log('Расчет открытия чатов:', {
2053 processed: currentState.processed,
2054 lastOpened: lastOpenedIndex,
2055 nextToOpen: nextIndexToOpen,
2056 targetOpenAhead: targetOpenAhead,
2057 currentlyOpenAhead: currentlyOpenAhead,
2058 needToOpen: needToOpen,
2059 chatsUntilEnd: chatsUntilEnd,
2060 willOpen: chatsToOpen
2061 });
2062
2063 if (chatsToOpen > 0) {
2064 for (let i = 0; i < chatsToOpen; i++) {
2065 const contactIndex = nextIndexToOpen + i;
2066 const contact = currentState.contactsList[contactIndex];
2067
2068 if (contact) {
2069 console.log('📂 Открываем чат [', contactIndex, ']:', contact.name, '(', contact.id, ')');
2070 lastOpenedIndex = contactIndex;
2071 await openChatById(contact.id);
2072 await sleep(200); // Небольшая задержка между открытием вкладок
2073 }
2074 }
2075 }
2076 }, 1000); // Проверяем каждую секунду
2077 }
2078
2079 // Функция для создания фильтра по дате в списке чатов
2080 function createDateFilter() {
2081 // Проверяем, не создан ли уже фильтр
2082 if (document.getElementById('dateFilterContainer')) {
2083 return;
2084 }
2085
2086 // Ищем контейнер с поиском
2087 const searchContainer = document.querySelector('.om_1_f0')?.parentElement;
2088 if (!searchContainer) {
2089 console.log('Контейнер для фильтра не найден');
2090 return;
2091 }
2092
2093 // Создаем контейнер для фильтра
2094 const filterContainer = document.createElement('div');
2095 filterContainer.id = 'dateFilterContainer';
2096 filterContainer.style.cssText = `
2097 padding: 15px;
2098 background: #f0f8ff;
2099 border-bottom: 1px solid #ddd;
2100 display: flex;
2101 gap: 10px;
2102 align-items: center;
2103 flex-wrap: wrap;
2104 `;
2105
2106 filterContainer.innerHTML = `
2107 <label style="font-size: 13px; font-weight: 600; color: #555;">Фильтр по дате последнего сообщения:</label>
2108 <input type="date" id="dateFilterFrom" style="width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px;">
2109 <span style="color: #555;">—</span>
2110 <input type="date" id="dateFilterTo" style="width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px;">
2111 <button id="applyDateFilter" style="padding: 8px 16px; background: #0066cc; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 13px; font-weight: 600;">Применить</button>
2112 <button id="clearDateFilter" style="padding: 8px 16px; background: #999; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 13px; font-weight: 600; display: none;">Сбросить</button>
2113 `;
2114
2115 // Вставляем фильтр перед списком чатов
2116 const chatList = document.querySelector('.om_1_f0');
2117 if (chatList && chatList.parentElement) {
2118 chatList.parentElement.insertBefore(filterContainer, chatList);
2119 console.log('Фильтр по дате создан');
2120
2121 // Обработчики событий
2122 document.getElementById('applyDateFilter').addEventListener('click', applyDateFilter);
2123 document.getElementById('clearDateFilter').addEventListener('click', clearDateFilter);
2124 }
2125 }
2126
2127 // Функция применения фильтра по дате
2128 async function applyDateFilter() {
2129 const dateFrom = document.getElementById('dateFilterFrom').value;
2130 const dateTo = document.getElementById('dateFilterTo').value;
2131
2132 if (!dateFrom || !dateTo) {
2133 alert('Выберите обе даты для фильтрации');
2134 return;
2135 }
2136
2137 const filterFrom = new Date(dateFrom);
2138 const filterTo = new Date(dateTo);
2139
2140 console.log('Применение фильтра по дате:', filterFrom, '-', filterTo);
2141
2142 // Показываем индикатор загрузки
2143 const applyButton = document.getElementById('applyDateFilter');
2144 const originalText = applyButton.textContent;
2145 applyButton.textContent = 'Фильтрация...';
2146 applyButton.disabled = true;
2147
2148 // Получаем все чаты
2149 const allChats = document.querySelectorAll('.index_chat_4fr82');
2150 let visibleCount = 0;
2151 let hiddenCount = 0;
2152
2153 allChats.forEach(chat => {
2154 // Ищем дату в самом элементе чата (не открывая его)
2155 const dateElement = chat.querySelector('.index_chatDate_z4mNc');
2156 const dateText = dateElement?.textContent?.trim();
2157
2158 const chatDate = parseDate(dateText);
2159
2160 if (chatDate) {
2161 // Проверяем, попадает ли дата в диапазон
2162 if (chatDate >= filterFrom && chatDate <= filterTo) {
2163 // Дата подходит - оставляем чат видимым
2164 chat.style.display = 'grid';
2165 visibleCount++;
2166 console.log('Чат подходит:', chat.querySelector('.index_chatTitle_TiXTq')?.textContent, chatDate);
2167 } else {
2168 // Дата не подходит - скрываем чат
2169 chat.style.display = 'none';
2170 hiddenCount++;
2171 }
2172 } else {
2173 // Не удалось распознать дату - оставляем видимым
2174 chat.style.display = 'grid';
2175 visibleCount++;
2176 }
2177 });
2178
2179 // Восстанавливаем кнопку
2180 applyButton.textContent = originalText;
2181 applyButton.disabled = false;
2182
2183 // Показываем кнопку сброса
2184 document.getElementById('clearDateFilter').style.display = 'inline-block';
2185
2186 console.log(`Фильтр применен. Показано: ${visibleCount}, Скрыто: ${hiddenCount}`);
2187 alert(`Фильтр применен!\nПоказано чатов: ${visibleCount}\nСкрыто чатов: ${hiddenCount}`);
2188 }
2189
2190 // Функция сброса фильтра
2191 function clearDateFilter() {
2192 // Показываем все чаты
2193 const allChats = document.querySelectorAll('.index_chat_4fr82');
2194 allChats.forEach(chat => {
2195 chat.style.display = 'grid';
2196 });
2197
2198 // Очищаем поля
2199 document.getElementById('dateFilterFrom').value = '';
2200 document.getElementById('dateFilterTo').value = '';
2201
2202 // Скрываем кнопку сброса
2203 document.getElementById('clearDateFilter').style.display = 'none';
2204
2205 console.log('Фильтр сброшен');
2206 }
2207
2208 // Функция для обновления статистики базы данных в интерфейсе
2209 async function updateDatabaseStats() {
2210 const db = await loadContactsDatabase();
2211 const stats = getDatabaseStats(db);
2212
2213 console.log('Обновление статистики базы данных:', stats);
2214
2215 // Ждем, пока элемент появится в DOM (максимум 5 секунд)
2216 let attempts = 0;
2217 const maxAttempts = 50;
2218
2219 while (attempts < maxAttempts) {
2220 const statsBlock = document.getElementById('dbStatsBlock');
2221
2222 if (statsBlock) {
2223 const lastUpdateText = stats.lastUpdate
2224 ? new Date(stats.lastUpdate).toLocaleString('ru-RU')
2225 : 'Никогда';
2226
2227 statsBlock.innerHTML = `
2228 <div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px;">
2229 <div><strong>Всего контактов:</strong> ${stats.total}</div>
2230 <div><strong>С датами:</strong> ${stats.withDates}</div>
2231 <div><strong>Отправлено:</strong> ${stats.sent}</div>
2232 <div><strong>Не отправлено:</strong> ${stats.notSent}</div>
2233 </div>
2234 <div style="margin-top: 10px; padding-top: 10px; border-top: 1px solid #e0e0e0; font-size: 12px; color: #999;">
2235 Последнее обновление: ${lastUpdateText}
2236 </div>
2237 `;
2238 console.log('Статистика обновлена в интерфейсе');
2239 return true;
2240 }
2241
2242 // Ждем 100мс перед следующей попыткой
2243 await sleep(100);
2244 attempts++;
2245 }
2246
2247 console.error('Элемент dbStatsBlock не найден после', maxAttempts, 'попыток!');
2248 return false;
2249 }
2250
2251 // Функция для фильтрации списка контактов
2252 function filterContactsList(searchQuery) {
2253 loadAndDisplayContacts(searchQuery);
2254 }
2255
2256 // ============= ФУНКЦИИ ДЛЯ СБОРА КОНТАКТОВ =============
2257
2258 // Функция для извлечения ID из URL чата
2259 function extractChatId(chatElement) {
2260 // Ищем атрибут deeplink в элементе чата
2261 const deeplink = chatElement.getAttribute('deeplink');
2262 if (deeplink) {
2263 // Пробуем оба формата:
2264 // 1. Обычный: ?group=customers_v2&id=ec4ee215-e8f6-49bd-968a-8334a98d8aa7
2265 // 2. Поиск: /communications/chats/chat?id=3d7d80d4-5da1-4cb1-8221-2e332a048d39&context=dialog_list
2266 const match = deeplink.match(/[?&]id=([a-f0-9-]+)/);
2267 if (match) {
2268 console.log('Извлечен ID из deeplink:', match[1]);
2269 return match[1];
2270 }
2271 }
2272
2273 // Запасной вариант - ищем ссылку внутри элемента
2274 const link = chatElement.querySelector('a[href*="id="]');
2275 if (link) {
2276 const href = link.getAttribute('href');
2277 const match = href.match(/[?&]id=([a-f0-9-]+)/);
2278 if (match) {
2279 console.log('Извлечен ID из ссылки:', match[1]);
2280 return match[1];
2281 }
2282 }
2283
2284 console.log('Не удалось извлечь ID чата');
2285 return null;
2286 }
2287
2288 // Функция для получения данных чата из элемента списка
2289 function getChatDataFromElement(chatElement) {
2290 const chatName = chatElement.querySelector('.index_chatTitle_TiXTq')?.textContent?.trim() ||
2291 chatElement.querySelector('.index_chatTitle_x9txX')?.textContent?.trim() ||
2292 'Неизвестно';
2293
2294 // Ищем элемент с датой - используем правильный селектор
2295 // Для обычных чатов: .index_chatDate_z4mNc
2296 // Для результатов поиска: .index_chatDate_WJ-+mb
2297 const dateElement = chatElement.querySelector('.index_chatDate_z4mNc, .index_chatDate_WJ-+mb');
2298 const dateText = dateElement?.textContent?.trim();
2299
2300 console.log('📋 Получение данных чата:', chatName);
2301 console.log(' Элемент даты найден:', !!dateElement);
2302 console.log(' Текст даты:', dateText || 'НЕТ');
2303
2304 const lastMessageDate = dateText ? parseDate(dateText) : null;
2305
2306 if (lastMessageDate) {
2307 console.log(' ✅ Дата распознана:', lastMessageDate.toLocaleDateString('ru-RU'));
2308 } else {
2309 console.log(' ❌ Дата НЕ распознана');
2310 }
2311
2312 return {
2313 name: chatName,
2314 lastMessageDate: lastMessageDate ? lastMessageDate.toISOString() : null,
2315 lastSentDate: null,
2316 messageCount: 0
2317 };
2318 }
2319
2320 // Функция для скролла списка чатов
2321 async function scrollChatList() {
2322 console.log('=== НАЧАЛО СКРОЛЛА СПИСКА ЧАТОВ ===');
2323
2324 // Проверяем, применен ли фильтр Ozon
2325 const clearFilterBtn = document.querySelector('.c8s110-a2, .c8s110-c0, .c8s110-a4, .c8s110-a5');
2326 const hasOzonFilter = !!clearFilterBtn;
2327
2328 console.log('Фильтр Ozon активен:', hasOzonFilter);
2329
2330 // ВАЖНО: Находим ПРАВИЛЬНЫЙ элемент списка чатов (тот, который реально скроллится)
2331 // Может быть несколько элементов с классом .om_1_f0, нам нужен тот, у которого scrollHeight > clientHeight
2332 const allChatLists = document.querySelectorAll('.om_1_f0');
2333 console.log('Найдено элементов .om_1_f0:', allChatLists.length);
2334
2335 const chatList = Array.from(allChatLists).find(el => el.scrollHeight > el.clientHeight);
2336
2337 if (!chatList) {
2338 console.error('Скроллируемый список чатов не найден');
2339 // Пробуем скроллить window как запасной вариант
2340 console.log('Пробуем скроллить window...');
2341 window.scrollBy({ top: 500, behavior: 'smooth' });
2342 await sleep(5000);
2343 return true;
2344 }
2345
2346 console.log('Найден скроллируемый список чатов:', {
2347 scrollTop: chatList.scrollTop,
2348 scrollHeight: chatList.scrollHeight,
2349 clientHeight: chatList.clientHeight
2350 });
2351
2352 // Сохраняем текущую позицию скролла
2353 const oldScrollTop = chatList.scrollTop;
2354 console.log('Текущий scrollTop:', oldScrollTop);
2355
2356 // АГРЕССИВНЫЙ СКРОЛЛ: Скроллим сразу к концу списка
2357 console.log('Выполняем агрессивный скролл к концу списка...');
2358 const targetScroll = chatList.scrollHeight - chatList.clientHeight;
2359 console.log('Целевой scrollTop:', targetScroll);
2360
2361 // Скроллим большими шагами к концу
2362 const steps = 5;
2363 const stepSize = Math.ceil((targetScroll - oldScrollTop) / steps);
2364
2365 for (let i = 0; i < steps; i++) {
2366 chatList.scrollTop += stepSize;
2367 console.log(`Агрессивный скролл ${i + 1}/${steps}: scrollTop =`, chatList.scrollTop);
2368 await sleep(300); // Небольшая задержка между шагами
2369 }
2370
2371 // Финальный скролл точно в конец
2372 chatList.scrollTop = targetScroll;
2373 console.log('Финальный scrollTop (в конец):', chatList.scrollTop);
2374
2375 console.log('scrollTop изменен с', oldScrollTop, 'на', chatList.scrollTop);
2376
2377 // УВЕЛИЧЕНО: Ждем 5 секунд для подгрузки и рендеринга новых чатов
2378 console.log('Ждем 5 секунд для подгрузки и рендеринга новых чатов...');
2379 await sleep(5000);
2380
2381 console.log('Финальный scrollTop после ожидания:', chatList.scrollTop);
2382 console.log('=== СКРОЛЛ ЗАВЕРШЕН ===');
2383 return true;
2384 }
2385
2386 // Основная функция сбора базы контактов
2387 async function collectContactsDatabase(updateExisting = false, testMode = false) {
2388 console.log('=== НАЧАЛО СБОРА БАЗЫ КОНТАКТОВ ===');
2389 console.log('Режим:', updateExisting ? 'Обновление существующей базы' : 'Полный сбор');
2390 console.log('Тестовый режим:', testMode ? 'ДА (только 1 контакт)' : 'НЕТ');
2391
2392 // Загружаем существующую базу
2393 const db = await loadContactsDatabase();
2394 const initialCount = Object.keys(db.contacts).length;
2395 console.log('Начальное количество контактов в базе:', initialCount);
2396
2397 // Загружаем черный список и конвертируем в Set для быстрого поиска O(1)
2398 const blacklistArray = await loadBlacklist();
2399 const blacklist = new Set(blacklistArray);
2400 console.log('Загружен черный список:', blacklist.size, 'ID');
2401
2402 // В режиме обновления - создаем Set из ID контактов для быстрой проверки
2403 const existingContactIds = updateExisting ? new Set(Object.keys(db.contacts)) : null;
2404
2405 // ОПТИМИЗАЦИЯ: Используем только Set для ID (без WeakSet для DOM элементов)
2406 // Set с ID занимает меньше памяти чем WeakSet с DOM элементами
2407 const recentlyProcessedIds = new Set();
2408 const MAX_RECENT_IDS = 500; // УМЕНЬШЕНО с 1000 до 500
2409 const recentIdsQueue = [];
2410
2411 function addToRecentIds(chatId) {
2412 if (recentlyProcessedIds.has(chatId)) return false;
2413
2414 recentlyProcessedIds.add(chatId);
2415 recentIdsQueue.push(chatId);
2416
2417 // Если превысили лимит - удаляем самый старый ID
2418 if (recentIdsQueue.length > MAX_RECENT_IDS) {
2419 const oldestId = recentIdsQueue.shift();
2420 recentlyProcessedIds.delete(oldestId);
2421 }
2422
2423 return true;
2424 }
2425
2426 // ИСПРАВЛЕНИЕ: Трекеры для уникальных операций
2427 const newContactsSet = new Set(); // ID новых контактов
2428 const updatedContactsSet = new Set(); // ID обновленных контактов
2429 let skippedBlacklist = 0;
2430 let scrollAttempts = 0;
2431 let noNewChatsCount = 0;
2432 const maxNoNewChatsAttempts = 7;
2433
2434 // ОПТИМИЗАЦИЯ: Сохраняем реже - каждые 1000 контактов
2435 let contactsSinceLastSave = 0;
2436 const saveEveryNContacts = 1000;
2437
2438 // НОВАЯ ОПТИМИЗАЦИЯ: Периодическая очистка скрытых чатов из DOM
2439 let contactsSinceLastCleanup = 0;
2440 const cleanupEveryNContacts = 300; // Очищаем каждые 300 обработанных чатов
2441 const maxHiddenChatsToRemove = 200; // Удаляем максимум 200 скрытых чатов за раз
2442
2443 // Функция для очистки скрытых чатов из DOM
2444 function cleanupHiddenChats() {
2445 console.log('🧹 ПЕРИОДИЧЕСКАЯ ОЧИСТКА: Поиск скрытых чатов в DOM...');
2446
2447 const allChatsInDom = document.querySelectorAll('.index_chat_4fr82, .index_chat_EHlBq');
2448 console.log(`📊 Всего чатов в DOM: ${allChatsInDom.length}`);
2449
2450 // Находим скрытые чаты (не видимые на экране)
2451 const hiddenChats = Array.from(allChatsInDom).filter(chat => {
2452 const style = window.getComputedStyle(chat);
2453 const rect = chat.getBoundingClientRect();
2454
2455 // Чат считается скрытым если:
2456 // 1. display: none или visibility: hidden
2457 // 2. Находится далеко за пределами viewport (более чем на 1000px)
2458 const isStyleHidden = style.display === 'none' ||
2459 style.visibility === 'hidden' ||
2460 style.opacity === '0';
2461 const isFarOffscreen = rect.bottom < -1000 || rect.top > window.innerHeight + 1000;
2462
2463 return isStyleHidden || isFarOffscreen;
2464 });
2465
2466 console.log(`🔍 Найдено скрытых чатов: ${hiddenChats.length}`);
2467
2468 if (hiddenChats.length === 0) {
2469 console.log('✅ Нет скрытых чатов для удаления');
2470 return 0;
2471 }
2472
2473 // Удаляем максимум maxHiddenChatsToRemove скрытых чатов
2474 const chatsToRemove = Math.min(hiddenChats.length, maxHiddenChatsToRemove);
2475 let removedCount = 0;
2476
2477 for (let i = 0; i < chatsToRemove; i++) {
2478 if (hiddenChats[i]) {
2479 hiddenChats[i].remove();
2480 removedCount++;
2481 }
2482 }
2483
2484 console.log(`🗑️ ПЕРИОДИЧЕСКАЯ ОЧИСТКА: Удалено ${removedCount} скрытых чатов из DOM`);
2485
2486 // ХАКИ ДЛЯ СБРОСА REACT-ВИРТУАЛИЗАЦИИ
2487 if (removedCount > 0) {
2488 console.log('🔄 Применяем хаки для сброса React-виртуализации...');
2489
2490 try {
2491 // 1. Эмулируем событие resize - заставляет react-window/react-virtualized пересчитать размеры
2492 window.dispatchEvent(new Event('resize'));
2493 console.log('✅ Отправлено событие resize');
2494
2495 // 2. Эмулируем событие scroll - триггерит пересчет видимых элементов
2496 const chatListContainer = document.querySelector('.om_1_f0');
2497 if (chatListContainer) {
2498 chatListContainer.dispatchEvent(new Event('scroll', { bubbles: true }));
2499 console.log('✅ Отправлено событие scroll на контейнер чатов');
2500 }
2501
2502 // 3. Принудительный скролл на 1px вверх и обратно - форсирует перерисовку
2503 if (chatListContainer) {
2504 const originalScrollTop = chatListContainer.scrollTop;
2505 chatListContainer.scrollTop = Math.max(0, originalScrollTop - 1);
2506 setTimeout(() => {
2507 chatListContainer.scrollTop = originalScrollTop;
2508 console.log('✅ Выполнен принудительный микро-скролл');
2509 }, 50);
2510 }
2511
2512 // 4. Эмулируем изменение размера окна (для react-virtualized)
2513 if (typeof window.ResizeObserver !== 'undefined') {
2514 // Триггерим ResizeObserver если он используется
2515 const resizeEvent = new CustomEvent('resize');
2516 document.body.dispatchEvent(resizeEvent);
2517 console.log('✅ Отправлено кастомное событие resize на body');
2518 }
2519
2520 } catch (error) {
2521 console.warn('⚠️ Ошибка при применении хаков виртуализации:', error);
2522 }
2523 }
2524
2525 // Проверяем результат
2526 const remainingChats = document.querySelectorAll('.index_chat_4fr82, .index_chat_EHlBq').length;
2527 console.log(`📊 Осталось чатов в DOM: ${remainingChats}`);
2528
2529 return removedCount;
2530 }
2531
2532 while (!shouldStop) {
2533 // Проверка на паузу
2534 while (isPaused && !shouldStop) {
2535 updateStatus('Пауза сбора базы', `Новых: ${newContactsSet.size}, Обновлено: ${updatedContactsSet.size}, Пропущено (ЧС): ${skippedBlacklist}`);
2536 await sleep(500);
2537 }
2538
2539 if (shouldStop) {
2540 console.log('Сбор базы остановлен пользователем');
2541 break;
2542 }
2543
2544 // Проверка тестового режима
2545 if (testMode && (newContactsSet.size >= 1 || updatedContactsSet.size >= 1)) {
2546 console.log('Тестовый режим: достигнут лимит, завершаем');
2547 shouldStop = true;
2548 break;
2549 }
2550
2551 // Проверка режима обновления
2552 if (updateExisting && existingContactIds) {
2553 const remainingToUpdate = Array.from(existingContactIds).filter(id => !recentlyProcessedIds.has(id));
2554 console.log('Осталось обновить контактов:', remainingToUpdate.length, 'из', existingContactIds.size);
2555
2556 if (remainingToUpdate.length === 0) {
2557 console.log('Все контакты из базы обновлены, завершаем');
2558 break;
2559 }
2560 }
2561
2562 // Получаем текущие видимые чаты
2563 const currentChats = getAllChats();
2564 console.log(`Попытка ${scrollAttempts + 1}: найдено ${currentChats.length} чатов в DOM`);
2565
2566 // Фильтруем только новые чаты
2567 const newChats = currentChats.filter(chat => {
2568 const chatId = extractChatId(chat);
2569 if (!chatId) return false;
2570
2571 // Проверяем кольцевой буфер последних ID
2572 if (recentlyProcessedIds.has(chatId)) return false;
2573
2574 return true;
2575 });
2576
2577 console.log('Новых необработанных чатов:', newChats.length);
2578 console.log('В кольцевом буфере ID:', recentlyProcessedIds.size);
2579
2580 // Если нет новых чатов - увеличиваем счетчик
2581 if (newChats.length === 0) {
2582 noNewChatsCount++;
2583 console.log(`Новые чаты не появились (${noNewChatsCount}/${maxNoNewChatsAttempts})`);
2584
2585 if (noNewChatsCount >= maxNoNewChatsAttempts) {
2586 console.log('Достигнут конец списка чатов (новые чаты не подгружаются после', maxNoNewChatsAttempts, 'попыток)');
2587 break;
2588 }
2589
2590 // Скроллим список чатов для подгрузки новых
2591 console.log('Нет новых чатов, скроллим для подгрузки...');
2592
2593 // ОБНОВЛЯЕМ СТАТУС ПЕРЕД СКРОЛЛОМ
2594 updateStatus(
2595 'Сбор базы: прокрутка списка...',
2596 `Обработано: ${newContactsSet.size + updatedContactsSet.size}, Новых: ${newContactsSet.size}, Обновлено: ${updatedContactsSet.size}, Пропущено (ЧС): ${skippedBlacklist}`
2597 );
2598
2599 await scrollChatList();
2600 await sleep(3000); // УМЕНЬШЕНО с 5000 до 3000
2601 scrollAttempts++;
2602 continue;
2603 } else {
2604 noNewChatsCount = 0;
2605 }
2606
2607 // Обрабатываем новые чаты
2608 for (const chat of newChats) {
2609 // Проверка на паузу
2610 while (isPaused && !shouldStop) {
2611 updateStatus('Пауза сбора базы', `Новых: ${newContactsSet.size}, Обновлено: ${updatedContactsSet.size}, Пропущено (ЧС): ${skippedBlacklist}`);
2612 await sleep(500);
2613 }
2614
2615 if (shouldStop) break;
2616
2617 const chatId = extractChatId(chat);
2618 const chatName = chat.querySelector('.index_chatTitle_TiXTq')?.textContent ||
2619 chat.querySelector('.index_chatTitle_x9txX')?.textContent ||
2620 'Неизвестно';
2621
2622 // Добавляем ID в кольцевой буфер
2623 const isNew = addToRecentIds(chatId);
2624 if (!isNew) {
2625 console.log('Чат уже обработан (в буфере):', chatName);
2626 continue;
2627 }
2628
2629 // Проверяем черный список
2630 if (blacklist.has(chatId)) {
2631 console.log(`⛔ Чат в черном списке, пропускаем: ${chatName} (${chatId})`);
2632 skippedBlacklist++;
2633 continue;
2634 }
2635
2636 // Получаем данные чата из элемента списка
2637 const chatData = getChatDataFromElement(chat);
2638
2639 // Проверяем, есть ли уже в базе
2640 const isExisting = db.contacts[chatId];
2641
2642 if (isExisting) {
2643 // Обновляем существующий контакт
2644 db.contacts[chatId].lastMessageDate = chatData.lastMessageDate || db.contacts[chatId].lastMessageDate;
2645 updatedContactsSet.add(chatId);
2646 console.log(`🔄 Обновлен контакт: ${chatName} (${chatId})`);
2647 } else {
2648 // Добавляем новый контакт
2649 addContactToDatabase(db, chatId, chatData);
2650 newContactsSet.add(chatId);
2651 console.log(`➕ Добавлен новый контакт: ${chatName} (${chatId})`);
2652 }
2653
2654 updateStatus(
2655 `Сбор базы: ${chatName}`,
2656 `Обработано: ${newContactsSet.size + updatedContactsSet.size}, Новых: ${newContactsSet.size}, Обновлено: ${updatedContactsSet.size}, Пропущено (ЧС): ${skippedBlacklist}`
2657 );
2658
2659 // Периодическое сохранение
2660 contactsSinceLastSave++;
2661 if (contactsSinceLastSave >= saveEveryNContacts) {
2662 console.log(`Промежуточное сохранение базы (${newContactsSet.size + updatedContactsSet.size} контактов)...`);
2663 await saveContactsDatabase(db);
2664 contactsSinceLastSave = 0;
2665 }
2666
2667 // НОВАЯ ЛОГИКА: Периодическая очистка скрытых чатов
2668 contactsSinceLastCleanup++;
2669 if (contactsSinceLastCleanup >= cleanupEveryNContacts) {
2670 const removedCount = cleanupHiddenChats();
2671
2672 // ОБНОВЛЯЕМ СТАТУС ПОСЛЕ ОЧИСТКИ
2673 if (removedCount > 0) {
2674 updateStatus(
2675 `Сбор базы: очистка памяти (удалено ${removedCount} скрытых чатов)`,
2676 `Обработано: ${newContactsSet.size + updatedContactsSet.size}, Новых: ${newContactsSet.size}, Обновлено: ${updatedContactsSet.size}, Пропущено (ЧС): ${skippedBlacklist}`
2677 );
2678
2679 // Небольшая пауза после очистки для стабильности
2680 await sleep(500);
2681 }
2682
2683 contactsSinceLastCleanup = 0;
2684 }
2685 }
2686
2687 // Скроллим для подгрузки следующей порции
2688 if (!shouldStop && newChats.length > 0) {
2689 await scrollChatList();
2690 await sleep(3000);
2691 }
2692 }
2693
2694 // Финальное сохранение базы
2695 console.log('Финальное сохранение базы данных...');
2696 await saveContactsDatabase(db);
2697
2698 // ОПТИМИЗАЦИЯ: Финальная очистка памяти
2699 console.log('🧹 Выполняем финальную очистку памяти...');
2700
2701 // Очищаем кольцевой буфер до минимума
2702 while (recentIdsQueue.length > 100) {
2703 const oldestId = recentIdsQueue.shift();
2704 recentlyProcessedIds.delete(oldestId);
2705 }
2706 console.log('Кольцевой буфер очищен до 100 ID');
2707
2708 // ФИНАЛЬНАЯ ОЧИСТКА: Удаляем скрытые чаты
2709 console.log('🧹 ФИНАЛЬНАЯ ОЧИСТКА: Удаление скрытых чатов из DOM...');
2710 const finalRemovedCount = cleanupHiddenChats();
2711
2712 // Проверяем результат
2713 const remainingChats = document.querySelectorAll('.index_chat_4fr82, .index_chat_EHlBq').length;
2714 console.log(`📊 Финальный результат: осталось ${remainingChats} чатов в DOM`);
2715
2716 if (remainingChats > 200) {
2717 console.warn(`⚠️ ВНИМАНИЕ: После очистки осталось ${remainingChats} чатов в DOM`);
2718 console.warn('Это может указывать на проблемы с виртуализацией Ozon');
2719
2720 // Показываем предупреждение пользователю
2721 const shouldContinue = confirm(
2722 '⚠️ ВНИМАНИЕ: В DOM осталось много чатов!\n\n' +
2723 `После очистки осталось ${remainingChats} чатов в DOM.\n` +
2724 'Виртуализация Ozon работает некорректно.\n\n' +
2725 'Рекомендуется:\n' +
2726 '1. Перезагрузить страницу\n' +
2727 '2. Продолжить сбор\n\n' +
2728 'Нажмите OK для подтверждения.'
2729 );
2730 } else {
2731 console.log(`✅ Очистка успешна: ${remainingChats} чатов в DOM - в пределах нормы`);
2732 }
2733
2734 // ОБНОВЛЯЕМ СТАТУС ПОСЛЕ ФИНАЛЬНОЙ ОЧИСТКИ
2735 updateStatus(
2736 `Сбор базы завершен! (удалено ${finalRemovedCount} скрытых чатов)`,
2737 `Обработано: ${newContactsSet.size + updatedContactsSet.size}, Новых: ${newContactsSet.size}, Обновлено: ${updatedContactsSet.size}, Пропущено (ЧС): ${skippedBlacklist}`
2738 );
2739 }
2740
2741 // Функция для создания модального окна
2742 function createModal() {
2743 const modalHTML = `
2744 <div id="bulkSenderModal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 10000; justify-content: center; align-items: center;">
2745 <div style="background: white; padding: 30px; border-radius: 12px; width: 600px; max-width: 90%; max-height: 90vh; overflow-y: auto; box-shadow: 0 4px 20px rgba(0,0,0,0.3);">
2746 <h2 style="margin: 0 0 20px 0; color: #333; font-size: 24px;">Массовая рассылка</h2>
2747
2748 <!-- Блок управления базой данных -->
2749 <div style="margin-bottom: 25px; padding: 20px; background: #f9f9f9; border-radius: 8px; border: 1px solid #e0e0e0;">
2750 <h3 style="margin: 0 0 15px 0; color: #555; font-size: 18px;">📊 База данных контактов</h3>
2751
2752 <div id="dbStatsBlock" style="margin-bottom: 15px; padding: 15px; background: #f0f8ff; border-radius: 6px; border-left: 4px solid #0066cc;">
2753 <div style="font-weight: 600; color: #0066cc; margin-bottom: 10px;">Статистика базы:</div>
2754 <div style="color: #333; font-size: 14px;">Загрузка...</div>
2755 </div>
2756
2757 <div style="margin-bottom: 15px;">
2758 <label style="display: flex; align-items: center; cursor: pointer;">
2759 <input type="checkbox" id="testModeCollect" style="width: 18px; height: 18px; margin-right: 10px; cursor: pointer;">
2760 <span style="color: #555; font-weight: 600; font-size: 14px;">Тестовый режим сбора (только 1 контакт)</span>
2761 </label>
2762 </div>
2763
2764 <div style="display: flex; flex-wrap: wrap; gap: 10px;">
2765 <button id="collectDbButton" style="padding: 10px 18px; background: #4caf50; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 600;">📥 Собрать базу</button>
2766 <button id="updateDbButton" style="padding: 10px 18px; background: #2196f3; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 600;">🔄 Обновить базу</button>
2767 <button id="viewDbButton" style="padding: 10px 18px; background: #9c27b0; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 600;">👁️ Просмотр</button>
2768 <button id="blacklistButton" style="padding: 10px 18px; background: #f44336; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 600;">🚫 Черный список</button>
2769 <button id="exportDbButton" style="padding: 10px 18px; background: #00bcd4; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 600;">📤 Экспорт базы</button>
2770 <button id="importDbButton" style="padding: 10px 18px; background: #009688; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 600;">📥 Импорт базы</button>
2771 <button id="refreshStatsButton" style="padding: 10px 18px; background: #ff9800; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 600;">📊 Обновить статистику</button>
2772 <button id="clearDbButton" style="padding: 10px 18px; background: #f44336; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 600;">🗑️ Очистить</button>
2773 <button id="pauseDbButton" style="display: none; padding: 10px 18px; background: #ff9800; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 600;">⏸️ Пауза</button>
2774 <button id="stopDbButton" style="display: none; padding: 10px 18px; background: #f44336; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 600;">⏹️ Стоп</button>
2775 </div>
2776 </div>
2777
2778 <!-- Выбор режима рассылки -->
2779 <div style="margin-bottom: 25px; padding: 20px; background: #f9f9f9; border-radius: 8px; border: 1px solid #e0e0e0;">
2780 <h3 style="margin: 0 0 15px 0; color: #555; font-size: 18px;">🎯 Режим рассылки</h3>
2781 <div style="display: flex; flex-direction: column; gap: 12px;">
2782 <label style="display: flex; align-items: center; cursor: pointer; padding: 12px; background: white; border-radius: 6px; border: 2px solid #ddd; transition: all 0.2s;">
2783 <input type="radio" name="sendingMode" value="database" checked style="width: 18px; height: 18px; margin-right: 12px; cursor: pointer;">
2784 <div>
2785 <div style="color: #333; font-weight: 600; font-size: 14px;">📊 По базе данных</div>
2786 <div style="color: #777; font-size: 12px; margin-top: 4px;">Отправка по собранной базе контактов с фильтром по дате</div>
2787 </div>
2788 </label>
2789 <label style="display: flex; align-items: center; cursor: pointer; padding: 12px; background: white; border-radius: 6px; border: 2px solid #ddd; transition: all 0.2s;">
2790 <input type="radio" name="sendingMode" value="visible" style="width: 18px; height: 18px; margin-right: 12px; cursor: pointer;">
2791 <div>
2792 <div style="color: #333; font-weight: 600; font-size: 14px;">👁️ По видимым чатам</div>
2793 <div style="color: #777; font-size: 12px; margin-top: 4px;">Отправка только по чатам, видимым в списке (с учетом фильтров)</div>
2794 </div>
2795 </label>
2796 </div>
2797 </div>
2798
2799 <div style="margin-bottom: 20px;">
2800 <label style="display: block; margin-bottom: 8px; color: #555; font-weight: 600;">Текст сообщения:</label>
2801 <textarea id="messageText" style="width: 100%; height: 120px; padding: 12px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; font-family: inherit; resize: vertical;" placeholder="Введите текст сообщения..."></textarea>
2802 </div>
2803
2804 <div style="margin-bottom: 25px;">
2805 <label style="display: block; margin-bottom: 8px; color: #555; font-weight: 600;">Дата последнего сообщения (до какой даты отправляли):</label>
2806 <input type="date" id="filterDate" style="width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px;">
2807 <small style="color: #777; display: block; margin-top: 5px; margin-left: 28px;">Будут обработаны чаты с датой последнего сообщения до указанной включительно</small>
2808 </div>
2809
2810 <div style="margin-bottom: 20px;">
2811 <label style="display: flex; align-items: center; cursor: pointer;">
2812 <input type="checkbox" id="testMode" checked style="width: 18px; height: 18px; margin-right: 10px; cursor: pointer;">
2813 <span style="color: #555; font-weight: 600;">Тестовый режим (отправить только 1 сообщение)</span>
2814 </label>
2815 <small style="color: #ff6600; display: block; margin-top: 5px; margin-left: 28px;">⚠️ Рекомендуется для первого запуска</small>
2816 </div>
2817
2818 <div id="statusBlock" style="display: none; margin-bottom: 20px; padding: 15px; background: #f0f8ff; border-radius: 6px; border-left: 4px solid #0066cc;">
2819 <div style="font-weight: 600; color: #0066cc; margin-bottom: 5px;">Статус:</div>
2820 <div id="statusText" style="color: #333;">Готов к запуску</div>
2821 <div id="progressText" style="color: #666; margin-top: 5px; font-size: 13px;"></div>
2822 </div>
2823
2824 <div style="display: flex; gap: 10px; justify-content: flex-end; flex-wrap: wrap;">
2825 <button id="startButton" style="padding: 12px 24px; background: #0066cc; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 600;">Запустить</button>
2826 <button id="pauseButton" style="display: none; padding: 12px 24px; background: #ff9800; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 600;">Пауза</button>
2827 <button id="stopButton" style="display: none; padding: 12px 24px; background: #f44336; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 600;">Стоп</button>
2828 <button id="emergencyStopButton" style="padding: 12px 24px; background: #d32f2f; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 600;">🚨 Экстренная остановка</button>
2829 <button id="closeButton" style="padding: 12px 24px; background: #999; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 600;">Закрыть</button>
2830 </div>
2831 </div>
2832 </div>
2833 `;
2834
2835 document.body.insertAdjacentHTML('beforeend', modalHTML);
2836 console.log('Модальное окно создано');
2837
2838 // Обработчики событий
2839 document.getElementById('closeButton').addEventListener('click', closeModal);
2840 document.getElementById('startButton').addEventListener('click', () => {
2841 console.log('!!! КЛИК ПО КНОПКЕ ЗАПУСТИТЬ !!!');
2842 startBulkSending();
2843 });
2844 document.getElementById('pauseButton').addEventListener('click', togglePause);
2845 document.getElementById('stopButton').addEventListener('click', stopBulkSending);
2846
2847 // Обработчик для кнопки черного списка
2848 document.getElementById('blacklistButton').addEventListener('click', openBlacklistModal);
2849
2850 // Обработчики для кнопок управления базой
2851 document.getElementById('viewDbButton').addEventListener('click', openViewDatabaseModal);
2852 document.getElementById('exportDbButton').addEventListener('click', exportDatabase);
2853 document.getElementById('importDbButton').addEventListener('click', importDatabase);
2854 document.getElementById('clearDbButton').addEventListener('click', async () => {
2855 const cleared = await clearContactsDatabase();
2856 if (cleared) {
2857 alert('✅ База данных очищена!');
2858 await updateDatabaseStats();
2859 }
2860 });
2861
2862 document.getElementById('collectDbButton').addEventListener('click', async () => {
2863 const testModeCollect = document.getElementById('testModeCollect').checked;
2864 const confirmMessage = testModeCollect
2865 ? 'Начать тестовый сбор базы контактов (только 1 контакт)?'
2866 : 'Начать полный сбор базы контактов? Это может занять некоторое время.';
2867
2868 if (confirm(confirmMessage)) {
2869 try {
2870 // Показываем кнопки паузы и остановки
2871 document.getElementById('collectDbButton').style.display = 'none';
2872 document.getElementById('updateDbButton').style.display = 'none';
2873 document.getElementById('pauseDbButton').style.display = 'inline-block';
2874 document.getElementById('stopDbButton').style.display = 'inline-block';
2875
2876 shouldStop = false;
2877 isPaused = false;
2878 await collectContactsDatabase(false, testModeCollect);
2879 } catch (error) {
2880 console.error('❌ Ошибка при сборе базы:', error);
2881 alert('❌ Ошибка при сборе базы: ' + error.message);
2882 } finally {
2883 // ВСЕГДА возвращаем кнопки в исходное состояние
2884 document.getElementById('collectDbButton').style.display = 'inline-block';
2885 document.getElementById('updateDbButton').style.display = 'inline-block';
2886 document.getElementById('pauseDbButton').style.display = 'none';
2887 document.getElementById('stopDbButton').style.display = 'none';
2888
2889 await updateDatabaseStats();
2890 }
2891 }
2892 });
2893
2894 document.getElementById('updateDbButton').addEventListener('click', async () => {
2895 const testModeCollect = document.getElementById('testModeCollect').checked;
2896 const confirmMessage = testModeCollect
2897 ? 'Обновить базу контактов в тестовом режиме (только 1 контакт)?'
2898 : 'Обновить существующую базу контактов?';
2899
2900 if (confirm(confirmMessage)) {
2901 try {
2902 // Показываем кнопки паузы и остановки
2903 document.getElementById('collectDbButton').style.display = 'none';
2904 document.getElementById('updateDbButton').style.display = 'none';
2905 document.getElementById('pauseDbButton').style.display = 'inline-block';
2906 document.getElementById('stopDbButton').style.display = 'inline-block';
2907
2908 shouldStop = false;
2909 isPaused = false;
2910 await collectContactsDatabase(true, testModeCollect);
2911 } catch (error) {
2912 console.error('❌ Ошибка при обновлении базы:', error);
2913 alert('❌ Ошибка при обновлении базы: ' + error.message);
2914 } finally {
2915 // ВСЕГДА возвращаем кнопки в исходное состояние
2916 document.getElementById('collectDbButton').style.display = 'inline-block';
2917 document.getElementById('updateDbButton').style.display = 'inline-block';
2918 document.getElementById('pauseDbButton').style.display = 'none';
2919 document.getElementById('stopDbButton').style.display = 'none';
2920
2921 await updateDatabaseStats();
2922 }
2923 }
2924 });
2925
2926 // Обработчик паузы
2927 document.getElementById('pauseDbButton').addEventListener('click', () => {
2928 isPaused = !isPaused;
2929 const pauseDbButton = document.getElementById('pauseDbButton');
2930
2931 if (isPaused) {
2932 pauseDbButton.textContent = '▶️ Продолжить';
2933 pauseDbButton.style.background = '#4caf50';
2934 console.log('Сбор базы приостановлен');
2935 } else {
2936 pauseDbButton.textContent = '⏸️ Пауза';
2937 pauseDbButton.style.background = '#ff9800';
2938 console.log('Сбор базы возобновлен');
2939 }
2940 });
2941
2942 // Обработчик остановки
2943 document.getElementById('stopDbButton').addEventListener('click', () => {
2944 if (confirm('Вы уверены, что хотите остановить сбор базы?')) {
2945 shouldStop = true;
2946 isPaused = false;
2947 console.log('Запрошена остановка сбора базы');
2948 }
2949 });
2950
2951 // Обработчик для кнопки обновления статистики
2952 document.getElementById('refreshStatsButton').addEventListener('click', async () => {
2953 console.log('Принудительное обновление статистики');
2954 await updateDatabaseStats();
2955
2956 // Также выводим в консоль для диагностики
2957 const db = await loadContactsDatabase();
2958 const dbString = JSON.stringify(db);
2959 const sizeBytes = dbString.length;
2960 const sizeKB = (sizeBytes / 1024).toFixed(2);
2961 const sizeMB = (sizeBytes / (1024 * 1024)).toFixed(4);
2962 const totalContacts = Object.keys(db.contacts).length;
2963 const avgContactSize = totalContacts > 0 ? (sizeBytes / totalContacts).toFixed(0) : 0;
2964
2965 // Расчет максимального количества контактов (при лимите 50 MB)
2966 const estimatedMaxContacts = totalContacts > 0 ? Math.floor((50 * 1024 * 1024) / (sizeBytes / totalContacts)) : 0;
2967
2968 console.log('=== ДИАГНОСТИКА БАЗЫ ДАННЫХ ===');
2969 console.log('Всего контактов в базе:', totalContacts);
2970 console.log('Размер базы:', sizeKB, 'KB (', sizeMB, 'MB )');
2971 console.log('Средний размер контакта:', avgContactSize, 'байт');
2972 console.log('Примерно поместится контактов (при лимите 50 MB):', estimatedMaxContacts);
2973 console.log('Первые 3 контакта:', Object.values(db.contacts).slice(0, 3));
2974 console.log('');
2975 console.log('📊 ЛИМИТЫ IndexedDB ПО БРАУЗЕРАМ:');
2976 console.log('Chrome/Edge: ~60% свободного места на диске (обычно несколько GB)');
2977 console.log('Firefox: ~50% свободного места на диске (обычно несколько GB)');
2978 console.log('Safari: ~1 GB');
2979 console.log('');
2980 console.log('✅ Ваша база легко поместится! IndexedDB поддерживает гораздо больше данных.');
2981
2982 alert(`📊 Статистика базы данных:\n\nКонтактов: ${totalContacts}\nРазмер: ${sizeKB} KB (${sizeMB} MB)\nСредний размер контакта: ${avgContactSize} байт\n\nПримерно поместится: ~${estimatedMaxContacts.toLocaleString()} контактов\n(при консервативном лимите 50 MB)\n\n✅ IndexedDB поддерживает гораздо больше!\nChrome/Firefox: несколько GB\nSafari: ~1 GB`);
2983 });
2984
2985 // Обработчик для кнопки экстренной остановки
2986 document.getElementById('emergencyStopButton').addEventListener('click', async () => {
2987 if (confirm('Экстренная остановка очистит все активные процессы рассылки. Продолжить?')) {
2988 await clearSendingState();
2989 shouldStop = true;
2990 isPaused = false;
2991 alert('Процесс остановлен! Все активные рассылки прекращены.');
2992 console.log('Экстренная остановка выполнена');
2993
2994 // Обновляем интерфейс
2995 updateStatus('Остановлено', 'Процесс прерван экстренной остановкой');
2996 }
2997 });
2998
2999 // Загружаем статистику при создании модального окна
3000 updateDatabaseStats();
3001 }
3002
3003 // Функция для фильтрации списка контактов
3004 function filterContactsList(searchQuery) {
3005 loadAndDisplayContacts(searchQuery);
3006 }
3007
3008 // Инициализация
3009 async function init() {
3010 console.log('Инициализация расширения');
3011
3012 // Проверяем, что мы на странице мессенджера с покупателями
3013 if (!window.location.href.includes('group=customers_v2')) {
3014 console.log('Не на странице чатов с покупателями, расширение не активно');
3015 return;
3016 }
3017
3018 console.log('Страница подходит, проверяем состояние');
3019
3020 // Проверяем, есть ли сохраненное состояние рассылки
3021 const savedState = await loadSendingState();
3022
3023 if (savedState && savedState.isActive) {
3024 console.log('Обнаружено активное состояние рассылки');
3025
3026 // Проверяем, это рабочая вкладка или вкладка для отправки
3027 const urlParams = new URLSearchParams(window.location.search);
3028 const chatId = urlParams.get('id');
3029
3030 if (chatId) {
3031 // Это вкладка с чатом - проверяем, есть ли этот ID в списке контактов
3032 const isInContactsList = savedState.contactsList.some(contact => contact.id === chatId);
3033
3034 if (isInContactsList) {
3035 console.log('🎯 Это рабочая вкладка для чата:', chatId);
3036 console.log('⚡ СРАЗУ начинаем обработку без ожидания очереди');
3037
3038 // Ждем загрузки страницы
3039 console.log('Ждем загрузки страницы 3 секунды...');
3040 await sleep(3000);
3041 console.log('Ожидание завершено, начинаем обработку');
3042
3043 // Получаем дату последнего сообщения в этом чате
3044 const lastMessageDate = getLastMessageDate();
3045 console.log('Дата последнего сообщения в чате:', lastMessageDate);
3046
3047 // Получаем дату фильтра из состояния
3048 const filterDate = savedState.filterDate ? new Date(savedState.filterDate) : null;
3049 console.log('Дата фильтра из состояния:', filterDate);
3050
3051 // Проверяем дату перед отправкой
3052 let shouldSend = false;
3053 if (lastMessageDate && filterDate) {
3054 console.log('Сравнение дат:');
3055 console.log(' Дата из чата:', lastMessageDate.toLocaleDateString('ru-RU'));
3056 console.log(' Дата фильтра:', filterDate.toLocaleDateString('ru-RU'));
3057 console.log(' Дата из чата <= Дата фильтра?', lastMessageDate <= filterDate);
3058
3059 shouldSend = lastMessageDate <= filterDate;
3060 } else {
3061 console.log('Не удалось получить дату или фильтр, пропускаем отправку');
3062 shouldSend = false;
3063 }
3064
3065 // Отправляем сообщение только если дата подходит
3066 let success = false;
3067 if (shouldSend) {
3068 console.log('✅ Дата подходит, отправляем сообщение');
3069 success = await sendMessage(savedState.messageText);
3070 } else {
3071 console.log('⏭️ Пропускаем отправку (дата не подходит)');
3072 success = false;
3073 }
3074
3075 // Обновляем состояние АТОМАРНО
3076 const currentState = await loadSendingState();
3077
3078 if (!currentState || !currentState.isActive) {
3079 console.log('Рассылка завершена, закрываем вкладку');
3080 await sleep(300);
3081 window.close();
3082 return;
3083 }
3084
3085 if (success) {
3086 console.log('✅ Сообщение отправлено');
3087
3088 // Обновляем базу данных
3089 const db = await loadContactsDatabase();
3090 if (db.contacts[chatId]) {
3091 db.contacts[chatId].lastSentDate = new Date().toISOString();
3092 db.contacts[chatId].messageCount = (db.contacts[chatId].messageCount || 0) + 1;
3093 await saveContactsDatabase(db);
3094 }
3095
3096 currentState.sent++;
3097 } else {
3098 console.error('❌ Ошибка отправки или дата не подходит');
3099 currentState.skipped++;
3100 }
3101
3102 currentState.processed++;
3103
3104 // Проверяем, нужно ли завершать
3105 if (currentState.testMode && currentState.sent >= 1) {
3106 console.log('Тестовый режим: достигнут лимит, завершаем');
3107 await clearSendingState();
3108 await sleep(300);
3109 window.close();
3110 return;
3111 }
3112
3113 if (currentState.processed >= currentState.totalContacts) {
3114 console.log('Все контакты обработаны, завершаем');
3115 await clearSendingState();
3116 await sleep(300);
3117 window.close();
3118 return;
3119 }
3120
3121 // Обновляем currentContactId на следующий контакт
3122 const nextContact = currentState.contactsList[currentState.processed];
3123 if (nextContact) {
3124 const oldContactId = currentState.currentContactId;
3125 currentState.currentContactId = nextContact.id;
3126 console.log('🔄 Обновлен currentContactId:', oldContactId, '->', nextContact.id);
3127 console.log(' Следующий контакт:', nextContact.name);
3128 }
3129
3130 // Сохраняем обновленное состояние
3131 await saveSendingState(currentState);
3132 console.log('✅ Состояние сохранено');
3133
3134 // Закрываем текущую вкладку
3135 console.log('Закрываем текущую вкладку');
3136 await sleep(300);
3137 window.close();
3138
3139 return; // Не создаем интерфейс
3140 }
3141
3142 console.log('Это главная вкладка (ID чата не в списке контактов)');
3143 // Продолжаем создание интерфейса ниже
3144 } else {
3145 console.log('Это главная вкладка, ожидаем результатов от рабочих вкладок');
3146
3147 // Создаем интерфейс и показываем прогресс
3148 createFloatingButton();
3149 createModal();
3150 openModal();
3151
3152 // Запускаем мониторинг состояния
3153 monitorSendingProgress();
3154
3155 return;
3156 }
3157 }
3158
3159 // Создаем плавающую кнопку сразу
3160 createFloatingButton();
3161 createModal();
3162
3163 console.log('Интерфейс создан');
3164 }
3165
3166 // Запуск при загрузке страницы
3167 if (document.readyState === 'loading') {
3168 document.addEventListener('DOMContentLoaded', init);
3169 } else {
3170 init();
3171 }
3172})();