feat(sales-finder): «ключи» → «запросов в рекламе» + HANDOFF с промтом продолжения

Владелец спросил «что значит ключ?» — «ключи» это жаргон Keys.so. По-человечески
это поисковые запросы, по которым фирма показывает объявления: чем их больше, тем
плотнее она сидит в рекламе. Переименовано в портале и в Excel, добавлено пояснение.

HANDOFF: актуальное состояние (портал, каталог 2ГИС, кластер, 7 починенных багов,
границы и запреты) + RESUME PROMPT для новой сессии.

146 тестов зелёных.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-07-12 14:06:44 +03:00
parent 4d937feb76
commit fe08b5afcd
4 changed files with 51 additions and 43 deletions
@@ -174,56 +174,60 @@ python -m uvicorn web.app:app --host 127.0.0.1 --port 8200
- Запрос, дающий ОДНУ фирму, 2ГИС показывает карточкой, а не списком — парсер вернёт 0.
- **ПДн** (личный ИНН директора — персональные данные) — владелец сказал: **отдельная задача**.
## 7. Что осталось / следующие шаги (по приоритету)
## 7. Что осталось (пп.1-6 прежнего списка — СДЕЛАНЫ, см. §6.2)
1. **Приёмка в браузере** — поднять uvicorn, владелец прокликает нишу+город, скачает Excel.
2. **Поднять покрытие ДИРЕКТОРА** (сейчас 4/12): открывать карточку 2ГИС через xfetch и/или
страницу реквизитов сайта (`/kontakty`,`/o-klinike`,`/license`) → вытащить ИНН → ДаДата.
Альтернатива директору без ИНН: ДаДата по названию+городу (fuzzy) — проверить качество.
3. **Поднять покрытие ТЕЛЕФОНА** (10/12): структурный сбор с карточки 2ГИС (как боевой
`HtmlPhoneScanner` — брать из tel:/schema/microdata, не из тела), + xfetch-рендер сайтов,
что блокируют requests.
4. **Fix Important #1 из ревью:** `twogis.parse_listing` ставит `two_gis_promoted` НА ВСЮ
страницу (глобально), а не на карточку. Теперь есть реальный HTML 2ГИС (через xfetch) —
привязать метку «Реклама»/приоритет к конкретной фирме по её card-разметке. Иначе ранг врёт.
5. **Пагинация/объём:** `_real_pipeline` зовёт `collect_two_gis` с дефолтным max_pages=5 —
может быть медленно/дорого (xfetch-кредиты + фетч сайтов). Ограничить (напр. max_pages=12
и/или cap N фирм) + прогресс в UI.
6. **Красота портала** — перенести стиль `ad-scan-v2/build_portal.py` (палитра Forest) в
экран результата (сейчас минимальный HTML).
7. **ПДн:** перед реальным использованием — короткий проход `pdn-152fz-audit` (ФИО директора,
телефоны — внутренний инструмент, не публикуем).
**Ничего срочного. Инструмент готов к использованию.** Открытые пункты:
1. **ПДн-аудит** — личный ИНН директора = персональные данные (152-ФЗ). Владелец: **отдельная
задача**, не сейчас.
2. **Глубина 2ГИС** — берём верхушку (~50 фирм/город). Владелец: **«не надо, достаточно»**.
Если передумает: больше страниц + больше синонимов-запросов + не терять сорванные рендеры.
3. **Приёмка** — владелец смотрел списки, правил на ходу; финальной формальной приёмки не было.
## 8. Границы (НЕ нарушать)
- Боевой автоподбор (`.claude/worktrees/avtopodbor`, `worktree-avtopodbor`) и боевой
`моя/ad-scan/` — НЕ трогать, только читать/копировать приёмы. Прод liderra.ru — не трогать.
- Боевой автоподбор (`.claude/worktrees/avtopodbor`) и боевой `моя/ad-scan/`**только читать**
(оттуда взяты: контракт xfetch, `keyso_api.domain_dashboard`, карта баз `regions.py`).
Прод liderra.ru — не трогать.
- Ключи — только в `secrets/` (gitignore), НИКОГДА в код/git.
- **Keys.so: НЕ придумывать коды баз.** На неизвестный код он молча отдаёт московские цифры.
Список закрыт (`cities.KEYSO_BASE`, 19 шт.).
- **ФНС (ЕГРЮЛ): не долбить параллельно и НЕ обходить капчу.** Семафор=1. Если просит капчу —
честно возвращаем пусто.
---
## 9. RESUME PROMPT (вставить в новую сессию после компакта)
```
Продолжаем портал поиска клиентов для отдела продаж (sales-finder). Прочитай сначала:
docs/superpowers/2026-07-12-HANDOFF-sales-finder-STATE.md (полное состояние),
затем при необходимости design/plan/brainstorm-notes от 2026-07-12.
Продолжаем портал поиска клиентов для отдела продаж (sales-finder).
Прочитай docs/superpowers/2026-07-12-HANDOFF-sales-finder-STATE.md — там полное состояние.
СОСТОЯНИЕ: v1 РАБОТАЕТ живьём end-to-end. Ветка feat/sales-finder, проект моя/sales-finder/,
42 теста зелёные (cd моя/sales-finder && python -m pytest -q). 2ГИС берётся через xfetch
(xf4.ru), Keys.so/ДаДата/телефон-с-сайта — работают. Секреты в secrets/. Границы: боевое
(автоподбор/ad-scan/прод) НЕ трогать, только копии; ключи только в secrets/. Коммит — сам,
у владельца уже есть общее «добро» на эту сборку (git add -f, т.к. моя/ в gitignore;
docs/observer/STATUS.md если пре-стейджен — git restore --staged перед коммитом).
СОСТОЯНИЕ: РАБОТАЕТ, готов к использованию. Ветка feat/sales-finder, проект моя/sales-finder/,
146 тестов зелёных (cd моя/sales-finder && python -m pytest -q).
Портал: cd моя/sales-finder && python -m uvicorn web.app:app --port 8200 → http://127.0.0.1:8200
Живой прогон 12.07 (стоматология moscow): 12 фирм, телефон 10/12, директор 4/12.
ЧТО УМЕЕТ: менеджер выбирает нишу (реальный рубрикатор 2ГИС: 19 разделов, 100 ниш) и город
(столицы регионов по 12 макрорегионам, Москва≠Подмосковье, Питер≠Ленобласть; есть пункт
«★ ВЕСЬ РЕГИОН» — кластер по всем городам группы). Получает список на прозвон: телефон, сайт,
ГДЕ рекламируется (Директ/коллтрекинг/пиксели), БЮДЖЕТ Директа (вилка ₽/мес + «запросов
в рекламе»), ИНН, ОГРН, юр.адрес, директор + должность + ЕГО ЛИЧНЫЙ ИНН. Выгрузка в Excel.
Сортировка: готов к звонку (директор+его ИНН) → горячесть → есть ФИО → каналы → бюджет.
СЛЕДУЮЩИЙ ШАГ (спроси владельца, что первым — §7 HANDOFF):
1) приёмка в браузере (uvicorn web.app:app :8200);
2) поднять покрытие директора (карточка 2ГИС/страница реквизитов → ИНН → ДаДата);
3) поднять покрытие телефона (структурный сбор с карточки 2ГИС);
4) fix two_gis_promoted per-firm (ревью Important #1) — теперь есть реальный HTML 2ГИС;
5) пагинация/объём + прогресс; 6) красота портала (стиль build_portal.py); 7) pdn-152fz-audit.
ИСТОЧНИКИ (все проверены живьём): 2ГИС через xfetch(xf4.ru) — листинг + КАРТОЧКА фирмы
(в листинге сайт есть не у всех!); Keys.so domain_dashboard — бюджет, ОБЯЗАТЕЛЬНО с региональной
базой (cities.KEYSO_BASE, 19 кодов; на неизвестный код молча отдаёт МОСКОВСКИЕ цифры!);
ДаДата — ИНН/ОГРН/директор/адрес; ЕГРЮЛ ФНС (egrul.nalog.ru, бесплатно, без капчи) — ЛИЧНЫЙ
ИНН директора; сайт фирмы — телефон и ИНН (Контакты/Реквизиты смотреть РАНЬШЕ Политики —
там реквизиты хостера!). Секреты в secrets/.
Работай по TDD, субагентами (Sonnet+) как раньше. Ничего боевого не трогай.
ГРАНИЦЫ: боевое (автоподбор/ad-scan/прод liderra.ru) НЕ трогать, только читать. Ключи только
в secrets/. ФНС не долбить параллельно (семафор=1), капчу НЕ обходить. Коды баз Keys.so
не выдумывать. Коммит — сам, у владельца есть общее «добро» (git add -f — моя/ в gitignore;
git restore --staged docs/observer/STATUS.md перед коммитом; форсить КОНКРЕТНЫЕ файлы, не каталог).
ОТКРЫТО: (1) ПДн-аудит личного ИНН директора — владелец сказал «отдельная задача»;
(2) глубина 2ГИС (берём верхушку ~50 фирм/город) — владелец сказал «не надо, достаточно».
Работай по TDD. Спроси владельца, что делать дальше.
```
+2 -2
View File
@@ -2,7 +2,7 @@
import csv, openpyxl
COLUMNS = ["Фирма", "Город", "Телефон", "Сайт", "Рекламируется", "Где рекламируется",
"Бюджет Директ от, ₽/мес", "Бюджет Директ до, ₽/мес", "Ключей Директ",
"Бюджет Директ от, ₽/мес", "Бюджет Директ до, ₽/мес", "Запросов в рекламе",
"Уверенность", "Доказательства", "ИНН", "ОГРН", "Директор", "ИНН директора",
"Должность", "Юрлицо", "Статус", "Юр. адрес", "Горячесть"]
@@ -16,7 +16,7 @@ def to_rows(firms: list) -> list:
"Где рекламируется": ", ".join(f.channels or []),
"Бюджет Директ от, ₽/мес": f.direct_budget_min or "",
"Бюджет Директ до, ₽/мес": f.direct_budget or "",
"Ключей Директ": f.direct_keys or "",
"Запросов в рекламе": f.direct_keys or "",
"Уверенность": f.confidence or "",
"Доказательства": "; ".join(f.evidence or []),
"ИНН": f.inn or "",
+1 -1
View File
@@ -50,7 +50,7 @@ def test_results_shows_channels_and_budget():
assert "Яндекс.Директ" in html
assert "Calltouch (коллтрекинг)" in html
assert "77 922" in html and "177 570" in html # вилка бюджета с разрядами
assert "812 ключей" in html
assert "812 запросов в рекламе" in html # по-человечески, а не жаргон «ключи»
assert "горячая" in html # понятная подпись вместо цифры
+7 -3
View File
@@ -177,7 +177,10 @@ def _budget_cell(f):
return '<span class="dim">—</span>'
rng = f"{_money(lo)} {_money(hi)}" if lo and lo != hi else _money(hi)
keys = f.get("direct_keys") or 0
sub = f'<small>{keys} ключей</small>' if keys else ""
# «ключи» — жаргон Keys.so. По-человечески это поисковые запросы, по которым
# фирма показывает рекламу: чем их больше, тем плотнее она сидит в Директе.
sub = (f'<small title="по стольким поисковым запросам фирма показывает рекламу">'
f'{keys} запросов в рекламе</small>') if keys else ""
return f'<span class="money">{rng}</span>{sub}'
@@ -261,8 +264,9 @@ def results_html(run):
'<th>Где рекламируется</th><th>Бюджет Директа, ₽/мес</th>'
'<th>ИНН</th><th>Директор</th><th>Оценка</th></tr>'
+ "".join(rows) + '</table>')
note = ('<div class=note><b>Бюджет</b> — оценка Keys.so, сколько фирма льёт в Яндекс.Директ '
'(вилка «в среднем – максимум» ₽/мес). Кто тратит больше — тот выше в списке. '
note = ('<div class=note><b>Бюджет</b> — оценка, сколько фирма льёт в Яндекс.Директ '
'(вилка «в среднем – максимум» ₽/мес). <b>«Запросов в рекламе»</b> — по скольким '
'поисковым фразам она показывает объявления: чем больше, тем плотнее сидит в рекламе. '
'<b>«Проверьте»</b> у директора — фамилия найдена по названию фирмы, а не по ИНН '
'с её сайта; сверьте перед звонком.</div>')
return page("Список на прозвон — Лидерра", head + table + note)