Карточка фактов (баланс/ступень/проекты/заявки) считается кодом и всегда
кладётся боту, когда клиент вошёл. Сторож режет числа не из карточки.
Переписка вошедшего закрывается от посторонних. Данные — строго под RLS
тенанта. Бот только рассказывает, ничего не меняет.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Прогон 46 диалогами (250 реплик) через новый чат: 250/250 ответов,
медиана 2,58 с, вранья 0 — переезд чистый. Одна находка (бот выдумал срок
«перезвонят в течение рабочего дня») починена правилом по смыслу.
Отдельно: счётчики от накрутки счёта сидели на файловом кэше, который считает
не атомарно (300 прибавок -> досчиталось 77). Переведены на Redis, 300/300.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Тот же скрипт, что в кабинете: /chat-widget.js отдаётся приложением
(на liderra.ru лендингу принадлежит только «/», остальные пути — приложению).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
8 задач по TDD: журнал разговоров -> свой приём сообщений -> опрос ответов ->
счётчики от накрутки счёта -> письмо владельцу со всей перепиской -> окошко
на портале и лендинге -> вычистка Jivo из настроек -> прогон 46 диалогами.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Владелец 13.07.2026: уходим от Jivo (3142 руб/мес корпоративный тариф ради
Bot API, ответа их поддержки так и нет), пишем своё окошко чата.
Ключевое: в чужом виджете бот не знает, кто с ним говорит -> личные ответы
невозможны. Привязка к Jivo в коде - 94 строки; мозг бота, 49 статей и
130 тестов переезжают нетронутыми.
Согласовано по частям: чат без постоянного соединения (очередь + опрос),
кабинет + лендинг, эскалация письмом владельцу (живого оператора нет),
три уровня счётчиков от накрутки счёта (потолок 1500 ответов/сутки + тревога).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Где что лежит, как поднять стенд, что держится кодом (не ослаблять),
что осталось открытым, порядок выката.
Главный вывод доводки: сторож на регулярках проигрывает гонку формулировок —
работает только правило по смыслу + запрет в промпте.
Код: ветка worktree-jivo-bot-core (004a0b17), тесты 126/126. Бот НЕ на проде.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Проверка advertises лезет на сайт фирмы; загрузка флакует (таймаут, TLS
на Windows), не открылся -> advertises=не проверено -> фирма вылетала из
списка (drop_not_advertising). Из-за этого улов по городам плясал (Хабаровск
13->8 между прогонами). Новый salesfinder/sitefetch.fetch_site_html: повтор
requests (https/http) attempts раз, затем xfetch-рендер. web/app._fetch_site
переведён на него. Тесты +6, всего 191 зелёный.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Что вскрылось (одиночные вопросы этого не видели): клиент оставил телефон →
бот молчал; бот забывал, что сам сказал; враньё «на паузе списаний нет»;
выдуманный телефон поддержки и юр-гарантии в новых оборотах.
Урок метода: машинный счётчик по запрещённым фразам показал 0 нарушений,
хотя враньё осталось другими словами — считать своими же регулярками нельзя.
Код и починка — в ветке worktree-jivo-bot-core (b037a216). Бот НЕ на проде.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Пример формата в ContactCapture.php остался в истории коммита 6f72367b
и блокировал push. Не ПДн — образец для распознавания; в рабочем коде уже
замаскирован. Настоящие номера сторож ловит по-прежнему.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Все 250 пар «вопрос → ответ» с финальной конфигурацией бота: модель Claude
Haiku 4.5 через AITunnel, маска ПДн, чистка markdown, сторож ответа.
Итог: 239 ответил сам, 11 увёл к специалисту, 0 «не знаю», 0 запрещённых фраз.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Симптом: кластер ДФО дал Владивосток 11 фирм вместо реальных 60.
Причина: xfetch.render повторял только на ПУСТОМ ответе, а 2ГИС-антибот
отдаёт непустую SPA-оболочку без карточек — она проходила как успех,
и вся страница фирм терялась. Фикс: render(ok=...) повторяет пока ok(html)
ложно; fetcher требует наличие /firm/ (не просто непустоту), retries=4.
Живой контроль боевым fetcher: Владивосток 11 -> 60 (x5.5). 185 тестов зелёных.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
HTTP Basic Auth через middleware; логин/пароль из secrets/portal_auth.txt
(gitignore, в код не зашиты); нет файла = без пароля (локально). Тестовые
клиенты отключают пароль явно. 182 теста зелёных.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
По личному ИНН директора: шаг 1 POST /2.0/inn -> ФИО+дата рождения
(юрлицо ООО отсекаем), шаг 2 POST /2.0/name_standart -> ВСЕ телефоны из
телефонных полей + ВСЕ почты, в исходном порядке, без сортировки/обрезки
(паспорт/СНИЛС/счета телефоном не считаем). Фильтр ИНН-эхо для fallback.
Проверено живьём: АСТРЕЯ дир. по личному ИНН -> 37 телефонов + личная почта;
по ИНН фирмы возвращается ООО -> 0 (вход = личный ИНН директора).
2 платных запроса на фирму, только по отмеченным галочкам. Тесты вычищены
от реальных ПДн. 179 тестов зелёных.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Выяснено живьём: сервис принимает POST с ключом+ИНН в теле, эндпоинт /2.0/inn
не inn_fl, ищет по 10-значному ИНН фирмы. GET с ключом в адресе давал 401,
inn_fl давал uncorrect format. Переделал contacts.py на POST с телом,
двухстрочный секрет адрес+тело, parse_config сам определяет POST/GET.
Поиск теперь по firm.inn, галочка активна при наличии ИНН фирмы.
Проверено сквозняком: Илатан 7709918259 -> реальные телефоны и почты в колонках.
167 тестов зелёных.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Галочки в списке на прозвон -> кнопка Собрать контакты -> по director_inn
тянем телефон и e-mail через сторонний inn_fl API, 2 новые колонки + Excel.
Домен/ключ API живут только в secrets/inn_fl_url.txt со шаблоном {inn} -
в коде не зашиты. Ответ парсим по образцу regex, ИНН/ОГРН телефоном не считаем.
ПДн: сбор только по явной отметке, последовательно, ошибка одной фирмы не роняет пакет.
165 тестов зелёных.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Владелец спросил «что значит ключ?» — «ключи» это жаргон Keys.so. По-человечески
это поисковые запросы, по которым фирма показывает объявления: чем их больше, тем
плотнее она сидит в рекламе. Переименовано в портале и в Excel, добавлено пояснение.
HANDOFF: актуальное состояние (портал, каталог 2ГИС, кластер, 7 починенных багов,
границы и запреты) + RESUME PROMPT для новой сессии.
146 тестов зелёных.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Корень сразу двух проблем, на которые указал владелец.
Проверено живьём (Дантистика, Красноярск):
база msk (как спрашивали) → 1 ключ, 332 ₽/мес ← мусор в списке
база kry (правильная) → 408 ключей, 1 213 837 ₽/мес ← правда
Разница в 3600 раз.
Второе следствие: канал «кто крутит Директ в нише» тоже спрашивался по Москве,
поэтому местные рекламодатели к нам не попадали — так потерялись СК Корона
(korona24.ru, 9 ключей) и ЛидерДент (liderdent24.ru), которых 2ГИС не отдал.
🔴 КОВАРНО: на НЕИЗВЕСТНЫЙ код базы Keys.so не ругается, а молча отдаёт МОСКОВСКИЕ
цифры (проверил irk/khb/vld/bar/kem — все вернули московские). Поэтому список баз
ЗАКРЫТЫЙ (19 проверенных, взяты из рабочего робота ad-scan), а города вне него
получают base=None → Keys.so не спрашиваем вовсе и бюджет НЕ показываем.
Лучше прочерк, чем московские цифры под видом местных.
Также: семафор MAX_CONCURRENT=2 — Keys.so быстро отвечает 429 «слишком много
запросов» (напоролся при проверке).
146 тестов зелёных.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Нашёл владелец: у АРТ-Денталь (Красноярск) в списке стоял ИНН 7801451618 —
это ООО «Бегет», ПИТЕРСКИЙ ХОСТИНГ. А на странице «Контакты» у клиники два своих
юрлица: ООО «СТАРТ» 2466195937 и ООО «АРТ» 2462216093.
Причины:
1) «Политику конфиденциальности» обходили РАНЬШЕ «Контактов» — а там сплошь и рядом
прописаны реквизиты хостера/разработчика сайта. Брали первый ИНН и останавливались.
2) regex спотыкался о «ИНН/КПП: 123/456» (после ИНН идёт кириллица).
Фикс:
- приоритет страниц: Реквизиты/Контакты → О компании → Политика (последняя);
- NOT_A_CLIENT_INN: чёрный список хостеров/конструкторов (Бегет, Селектел, Рег.ру,
Таймвеб, Тильда, 1С, Яндекс, Мэйл) — такие ИНН не берём никогда;
- extract_inns: берём ВСЕ ИНН страницы (у фирмы может быть несколько юрлиц),
поддержан формат «ИНН/КПП»;
- dadata.pick_inn_for_city: из кандидатов выбираем фирму В НАШЕМ ГОРОДЕ —
чужой питерский хостер не пройдёт даже если просочится.
140 тестов зелёных.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Владелец сравнил с выгрузкой старого робота: по Красноярску тот нашёл 36 фирм,
мы — 13. В логе видно почему: «собрано 49, без сайта 25» — половина отсеивалась.
Причина: сайт брали ТОЛЬКО со страницы результатов поиска 2ГИС, а там ссылка на
сайт есть не у всех — у многих она только в КАРТОЧКЕ фирмы. Проверено:
Реновацио → renovacio-med.ru, Дентал Эксперт → dental-expert24.ru,
Дентоша → dentosha.ru — в листинге сайтов нет, в карточке есть.
Фикс: twogis.site_from_card + шаг resolve_site в пайплайне — для фирм без сайта
открываем карточку (xfetch, параллельно, потолок 3) ДО отсева «без сайта».
Попутно: otello.ru (виджет, висит в каждой карточке 2ГИС) — в чёрный список.
134 теста зелёных.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Владелец сравнил с выгрузкой старого робота: там есть ОГРН и адрес, у нас нет.
Причина глупая: ДаДата их отдаёт, парсер их читает — а в модели Firm полей не было,
и данные молча терялись.
Firm.ogrn + Firm.address; заполняются в обоих путях ДаДаты (по ИНН и по названию).
В Excel добавлены столбцы «ОГРН» и «Юр. адрес».
129 тестов зелёных.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Симптом (нашёл владелец): у 7 из 15 фирм ИНН фирмы есть, а директора нет.
Причина 1: ходили к ФНС в 8 потоков — она отбивается и отдаёт пустоту.
Проверено: те же ИНН по одному отдаются нормально (АСТРЕЯ → Коржова Т.С.,
ИНН 246403602419; Клиника практ. стом. → Джаватханов Д.И., ИНН 246508334975).
Причина 2 (хуже): пустой ответ КЭШИРОВАЛСЯ — фирма навсегда оставалась без
директора даже при пересборе.
Фикс:
- семафор MAX_CONCURRENT=1 + пауза 0.7с: к ФНС ходим по одному, вежливо;
- кэшируем ТОЛЬКО удачу; осевшие пустышки выбрасываем при чтении;
- из кэша вычищено 16 отравленных записей.
129 тестов зелёных.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Нашёл владелец: у Crystal dent сайт значился max.ru (мессенджер Max), у «Алекуса» —
api.whatsapp.com. В московском списке так же криво: medlineservice.infoclinica.ru,
dentol.infoclinica.ru, n2317362.yclients.com — это виджеты онлайн-записи, а не сайты.
Причина: в чёрном списке не было Max, WhatsApp, платформ записи (yclients, infoclinica,
medflex, dikidi) и он не учитывал ПОДДОМЕНЫ (api.whatsapp.com ≠ whatsapp.com).
Фикс: список расширен (мессенджеры, соцсети, виджеты записи, агрегаторы) + проверка
по суффиксу домена. Если первая ссылка — мусор, берём следующую (настоящий сайт).
126 тестов зелёных.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Симптом: «Не удалось собрать список» без единой подсказки почему.
Причина: консоль Windows в cp1251; print с символом «→» кидал UnicodeEncodeError,
исключение вылетало из run_pipeline и убивало сбор. Сам сбор при этом работал
(прямой запуск с PYTHONIOENCODING=utf-8 отрабатывал нормально) — поэтому и не
воспроизводилось.
Фикс:
- app: sys.stdout/stderr переводим в utf-8 (errors=replace);
- pipeline: вызов deps['log'] обёрнут в try/except — лог НИКОГДА не роняет прогон;
- стрелка «→» в сообщении заменена на ASCII '->';
- _do_run больше не глотает исключение молча — печатает traceback.
Регрессионный тест: падающий лог не мешает списку собраться.
Проверено вживую: прогон «Частные стоматологии / Красноярск» — 18 фирм, DONE.
121 тест зелёный.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
НИШИ — реальный рубрикатор 2ГИС (снят 12.07.2026 через xfetch), не выдуманный:
19 разделов (Медицина, Красота, Ремонт-стройка, B2B-услуги, Автосервис...),
100 ниш. У денежных ниш — синонимы-запросы (стоматология / стоматологическая
клиника). NB: 2ГИС на странице раздела показывает только верхние подрубрики,
глубже — за «Ещё».
ГОРОДА — столицы регионов по макрорегионам: Москва | Подмосковье | Питер |
Ленобласть | ЦФО | СЗФО | ЮФО | СКФО | ПФО | Урал | Сибирь | ДФО.
Москва отделена от Подмосковья, Питер от Ленобласти (иначе кластер разрастается).
🔑 Слаги СВЕРЕНЫ с реальным списком проектов 2ГИС — 21 из них были неверны:
khabarovsk (не habarovsk), n_novgorod, ulanude, novokuznetsk, p_kamchatskiy,
kh_mansiysk, yoshkarola, lyubertsy, khimki... Майкоп в 2ГИС не найден.
КЛАСТЕР: пункт «★ ВЕСЬ РЕГИОН» гоняет прогон по всем городам группы, у каждой
фирмы свой город (колонка «Город»). В интерфейсе честно показано, сколько
городов и примерное время.
120 тестов зелёных.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Firm.city — город сбора по-русски (из cities.py по слагу 2ГИС). Проставляется
в пайплайне всем фирмам прогона. Видно в таблице портала и в выгрузке —
чтобы при сводной выгрузке нескольких городов было понятно, кто откуда.
107 тестов зелёных.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Главный ключ сортировки теперь «полностью опознан»: есть ФИО директора И его
личный ИНН. Такие идут выше всех, даже если холоднее и беднее — по ним звонок
можно делать прямо сейчас.
Порядок: готов к звонку → горячесть → есть хотя бы ФИО → каналы → бюджет.
Эффект на живом списке: 25 «готовых» занимают позиции 1-25; Ройтберг
(3,8 млн ₽/мес) и К+31 (8,5 млн) уходят за границу — по ним звонить некому.
106 тестов зелёных.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Сортировка в 4 ключа (как задал владелец):
1) горячесть
2) есть ли директор — кому звонить важнее, чем сколько тратит
3) в скольких каналах рекламируется (шире реклама — выше)
4) бюджет Директа
Эффект: К+31 с самым большим бюджетом (до 8,5 млн ₽/мес) уходит вниз,
потому что директор не найден — звонить некому.
104 теста зелёных.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Было: всё строго последовательно, и тяжёлое обогащение (обход страниц
реквизитов, ДаДата, выписка ЕГРЮЛ) тратилось даже на фирмы без сайта и на
тех, кто нигде не рекламируется.
Стало — этапы с отсевом:
1. сбор (2ГИС параллельно по страницам ∥ Keys.so) → свод
2. ОТСЕВ: нет сайта → выкидываем (проверять и обогащать нечего)
3. ПАРАЛЛЕЛЬНО дёшево: бюджет Директа + скан главной → «рекламируется?»
4. ОТСЕВ: нигде не рекламируется → не наш клиент
5. ПАРАЛЛЕЛЬНО дорого: реквизиты→ИНН, ДаДата, ЕГРЮЛ — только по оставшимся
6. ранг
parallel.py: pmap (потоки, падение задачи → None, прогон не рушится).
xfetch: глобальный семафор MAX_CONCURRENT=3 — любой параллельный код упирается
в потолок сервиса (тест проверяет пик ≤ 3).
twogis: collect_two_gis_parallel — страницы рубрик тянем разом.
pipeline: deps['log'] сообщает, сколько отсеяно (без тихих обрезаний).
Слой A переехал из scan_ad в enrich; главная страница кэшируется и
переиспользуется (не качаем дважды).
100 тестов зелёных.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ДаДата личный ИНН директора НЕ отдаёт: поле managers в ответе есть, но
приходит null (нужен другой платный тариф). В management только ФИО+должность.
Источник — официальная выписка ЕГРЮЛ с egrul.nalog.ru: бесплатно, без капчи
(captchaRequired=false), раздел «Сведения о лице, имеющем право без доверенности
действовать от имени юрлица» содержит ФИО, ЛИЧНЫЙ ИНН и должность.
Проверено: Илатан 7709918259 → Костышев Артур Вадимович, ИНН 561506859802.
egrul.py: parse_director (чистая, тестируется на тексте), fetch_vypiska (PDF,
кэш по ИНН), enrich_director. У ИП ИНН фирмы = личный ИНН человека (12 цифр) —
в ФНС не ходим. Капчу НЕ обходим: если ФНС просит — возвращаем пусто.
ФИО/должность из ЕГРЮЛ официальные — уточняем ими данные ДаДаты.
Колонки: «ИНН директора» и «Должность» (в портале — под ФИО, в Excel — столбцы).
ПДн: ИНН физлица — персональные данные (152-ФЗ), строже ИНН фирмы.
Инструмент внутренний; нужна ПДн-проверка перед реальной эксплуатацией.
91 тест зелёный. Добавлена зависимость pypdf (чтение выписки).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
git add -f по каталогу переопределил .gitignore и затянул скомпилированный мусор.
Впредь форсить только конкретные файлы.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Портал (web/ui.py, палитра Forest): экран выбора ниши+города, экран списка
с таблицей, крутилка на время сбора, «Последние списки», кнопка Excel.
Бюджет Директа — ТЯНЕМ (раньше не тянули вовсе): keyso.fetch_domain_context
через domain_dashboard (порт adscan/keyso_api.py): adkeyscnt/adscnt/adcost →
вилка ₽/мес (average…bdg100). Забираем ДО решения «рекламируется?», иначе
фирма с бюджетом, но без следов на сайте, попадала в «нет» (так терялся К+31
с бюджетом до 8,5 млн ₽/мес).
Колонка «Где рекламируется» — adscan.channels_from: Директ / коллтрекинг /
пиксели. Колонка «Бюджет Директа, ₽/мес» — вилка + число ключей.
Ранкер: деньги стали главным весом (до +50 за бюджет). Ложная метка
two_gis_promoted УБРАНА из оценки — парсер ставил её на всю страницу, а не
на фирму, и все получали одинаковые 90. Вместо голой цифры — подпись
горячая/тёплая/холодная.
Проверено живьём: НоваДент 3,2–7,3 млн; К+31 3,6–8,5 млн; Илатан 0,57–1,3 млн;
Дантистофф 0. 82 теста зелёных.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Слой A (requisites.py): если ИНН нет на главной сайта — ищем страницы
реквизитов/политики конфиденциальности/лицензии/контактов и достаём ИНН
оттуда (по 152-ФЗ ИНН обязан быть на «Политике»). Надёжно, точно.
Слой B (dadata.enrich_by_name): если ИНН так и нет — ищем в ДаДате по
названию+городу с защитой best_match (только действующие с совпадением
города). Директор по названию ВСЕГДА помечается «проверьте» (совпадение
может быть чужой фирмой — напр. Чудо Доктор→Аптека Чудо Доктор).
cities.py: слаг города (латиница, для 2ГИС) → русское имя (для ДаДаты).
Реквизит-страницы тянем только быстрым requests (без медленного xfetch).
Слой C (бесплатные порталы по телефону) отклонён — проверен через xfetch,
контроль провалился, отдаётся нестабильно.
Проверено живьём: слой A дал ИНН+ФИО Доброму доктору, Дентал Фэнтези,
Белгравии и др. со своих сайтов; слой B — Дантистофф/Медлайн/СМ-Стоматология
с пометкой. 61 тест зелёный.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Живая проверка 12.07: xf4.ru/fetch рендерит 2ГИС (обход антибота) → парсер листинга
(порт ChannelA) даёт фирмы+сайты. Телефон и ИНН снимаем структурно с сайта (tel:/schema),
директор — ДаДата по ИНН. Сквозной прогон 'стоматология москва': 12 фирм, телефон 10/12,
директор 4/12, за ~43с. 42 теста зелёные. allowlist gitleaks для тест-телефонов sales-finder.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Живая проверка 12.07: keyword_dashboard отдаёт ads[] (список блоков объявлений
с domain/header/bid), а не data[]. Парсер переписан на рекламодателей ниши.
Пайплайн объединяет Директ-сигнал Keys.so с сайтовым (не затирает). 32 теста зелёные.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>