Диапазон 999-000 у операторов не существует — заведён специально для
демо-данных и съёмки инструкций, чтобы в кадр не попал настоящий номер.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Гость получает верную ссылку на разбор по всем 10 частым вопросам; где
разбора нет — бот молчит, в пустоту не шлёт. 20 тем, 126 кадров, единый
демо-клиент. Отдельно: найден баг боевой функции — поиск дублей считал
ноль признаком совпадения (на проде он есть, фикс в ветке).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- ★ рядом с городом в списке выбора, если keyso_base(slug) не None (19 регионов, 30 городов)
- сноска под формой поясняет значок
- звезда не трогает сбор, только отображение выбора городов
- 216 тестов зелёных
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Точное разделение робот/браузер/человек (отклик страницы, а не запрос),
сквозная склейка лендинг→портал через куку на .liderra.ru, воронка и каналы
в админке. Метрику и Вебвизор не трогаем.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Playwright-съёмщик (record.mjs) логинится демо-клиентом, проходит сценарий
из topics/<slug>.mjs и рисует поверх каждого шага стрелку+рамку+подпись
(подписи — из app/resources/help/sozdat-proekt.md, без выдумок). Первая
тема — sozdat-proekt, 8 кадров.
Мелкие находки по пути: приветственный тур перехватывал клики (глушим через
localStorage-флаг), первый проект нового клиента открывает шаг «Реквизиты»
(проходим тихо, без кадра), собранный фронт портала был устаревшим и тянул
старый виджет JivoSite поверх экрана (пересобрал npm run build — исходники
уже используют свой чат-виджет), подпись снизу могла перекрыть стрелку на
кнопках у нижнего края диалога (авто-перенос наверх).
- ниша и город показываются по-русски (label_for/city_ru) в таблице списков, шапке результата и экране сбора
- федеральный округ + русское имя города вместо слага
- кнопка «Собрать контакты по отмеченным» перенесена НАД таблицу (форма по-прежнему оборачивает галочки)
- 215 тестов зелёных
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- POST /delete/{rid} + RunStore.delete; кнопка ✕ в таблице «Последние списки» с подтверждением
- _run_one пропускает удалённый из очереди прогон (проверка существования под замком)
- убрана галочка Москвы по умолчанию — города не отмечены заранее
- 212 тестов зелёных
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- cities.federal_district(slug) → ЦФО/СЗФО/ЮФО/СКФО/ПФО/УФО/СФО/ДФО (Москва,Подмосковье→ЦФО; Питер,Ленобласть→СЗФО)
- в таблице последних списков перед городом бейдж округа
- 208 тестов зелёных
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Клипы по сути (сразу с нужного экрана, без входа и хождения по меню)
и тот же показ 2-3 картинками. Четыре темы: заявки, пополнение, создание
проекта, Конкурентное поле. Телефоны в кадре размыты. Страница сравнения —
tours-compare.html. На боевом сайте этого нет.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- STORE.recent(limit=200) на главной вместо дефолтных 10
- очередь из нескольких городов больше не «съедает» ранее собранные готовые списки
- тест: 15 списков — виден и самый старый, и самый свежий
- 205 тестов зелёных
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- города выбираются чекбоксами (несколько сразу), сгруппированы по регионам
- пункт «★ ВЕСЬ РЕГИОН» и режим @регион убраны
- несколько городов собираются ПОСЛЕДОВАТЕЛЬНО под глобальным замком _RUN_LOCK
(одновременно один город на весь портал — не ловим лимиты платных сервисов)
- каждый город — отдельный список; один город ведёт на его экран, несколько — на главную к очереди
- _do_run/_cities_of заменены на _run_one (под замком) + _do_batch (очередь)
- тесты очереди: порядок сохраняется, каждый город свой список; тест-изоляция STORE
- 204 теста зелёных
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Главное: сбор контактов переведён с метода inn (для ЮРлица) на inn_fl (поиск по ИНН
ФИЗЛИЦА, поле inn_fl=<ИНН>). Раньше по личному ИНН человека сервис отвечал not_found —
звали не тот метод. Теперь один запрос отдаёт полное досье, как «Поиск по ИНН» в кабинете.
Живьём: Моисеев not_found→ok 2тел, Орто-Премьер ip→ok 9тел, Медика not_found→ok 7тел.
- collect_contacts_for: один вызов inn_fl вместо двухшагового inn→name_standart; дешевле
- _api_status: ok / not_found / error по ответу сервиса
- убраны обломки двухшагового режима: _looks_like_ip, _name_standart_body, контакт-статус «ip»
- контакт ищем ТОЛЬКО по личному ИНН директора; по ИНН фирмы не ходим
- метка «ИП» перед ФИО в списке — dadata.check_ip по личному ИНН, поле director_is_ip
- пустая клетка контакта показывает причину: «сервис не знает этого человека» / «ошибка»
- 203 теста зелёных
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
20 личных вопросов на тестовом клиенте — 20 правдивых ответов; границы
держит (паузу не ставит, ПДн не даёт, будущее не прогнозирует). Гостю —
вход в кабинет. Посторонний получает 404. Две находки прогона закрыты:
жёсткий отказ в замене ловится в любой форме, правда про ниши больше
не режется. Тесты 216/216. На проде этого нет.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- сбор контактов идёт ТОЛЬКО по личному ИНН директора; по ИНН фирмы больше не ищем
- директор-ИП распознаётся по ЕГРИП: даты рождения у ИП нет → второй платный запрос не делаем, помечаем status=ip
- collect_contacts_for теперь возвращает status: ok / ip / not_found / error
- в списке галочка активна лишь при личном ИНН директора; пустая клетка контакта показывает причину
- живьём: Евромед→ok 3тел·1почта, Орто-Премьер→ip, Медика→not_found; 196 тестов зелёных
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
9 задач по TDD: ClientFacts (деньги/ступень/проекты/заявки под RLS),
сторож на выдуманные числа, карточка в контексте вошедшего, гостю вход
в кабинет, владелец разговора (404 чужому), ключ разговора на каждый вход,
живой прогон с судьями.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Карточка фактов (баланс/ступень/проекты/заявки) считается кодом и всегда
кладётся боту, когда клиент вошёл. Сторож режет числа не из карточки.
Переписка вошедшего закрывается от посторонних. Данные — строго под 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>