Импорт консьержа с сайта отеля¶
Для кого: суперадмин (лиды) и отельер (онбординг)
Парсер обходит сайт отеля (главная + внутренние страницы до 2-го уровня, максимум 12 страниц) и собирает черновик консьержа: название, услуги, контакты и (опционально) фото. Для обычных сайтов черновик строится по каркасу базового шаблона (template=hotbot): информационные страницы, сервисы, транспорт, сценарии и т.д. Черновик ревьюится в админке и применяется одним из двух способов:
- К компании — создаёт QR-табличку и
company_cards(как раньше). - В шаблон консьержа (суперадмин) — услуги становятся собственными карточками шаблона (
concierge_template_custom_cards), страницы «Об отеле» / «Контакты» — static pages шаблона. Отельер потом выбирает шаблон из галереи.
Hotbot Shop (дополнение)¶
Если в то же поле URL вставить витрину вида https://shop.hotbot.ai/{alias} (пример: Grand Chalet Altay), срабатывает отдельная ветка без Firecrawl и без LLM:
- Читается SSR
window.HotbotConfigсо страницы магазина. - Подтягиваются категории (
GET /v1/shop/category/{alias}) и статичные страницы (GET /v1/shop/static-page/{id}). - Собирается черновик с
template=hotbot: секции главной, static pages, каталог (подкатегория Hotbot →company_card, товары → options; цены в копейках делятся на 100). Плитка сrootCategoryAliasбезcategoryAliasраскрывается в список подкатегорий (Завтраки / Супы / …);categoryAliasведёт на конкретную карточку.
Обычные URL сайтов отелей по-прежнему идут через Firecrawl/LLM → каркас hotel-base (hotbot). В админке Hotbot-job помечается бейджем.
Не переносится 1:1: вложенное меню глубже одного уровня idea-категорий, мультиязычность, слоты SPA/расписание, формы обратной связи Hotbot (гость пишет в наш чат), эквайринг/loyalty, туры как модуль Tourmakers.
GoodStay (дополнение)¶
Если в то же поле URL вставить витрину вида https://{alias}.goodstay.io (пример: Cosmos Прибалтийская), срабатывает отдельная ветка без Firecrawl и без LLM:
- Читается SSR
window.__PUBLIC_DATA__.hotelEntity(название, брендmainColor, логотип, фон). - Через GraphQL
https://api.goodstay.io/graphql(hotel scope через заголовокOrigin, локаль черезAccept-Language: ru) подтягиваются: - группы главной (
getHotelGroups+ sequenceSHOP/SERVICE); - список витрин (
getShops— в т.ч.main, которых может не быть в groups); - страницы (
getPageById) и контакты (getHotelContacts); - магазины/меню (
getShopById,getShopSections); - баннер (
getBanner). - Собирается черновик с
template=hotbot: группы → секции, pages → static pages,shopSection→company_card, позиции секции → options (как у Hotbot subcategory→card), плитки главной →guest_orderна карточку категории; main-shop вне groups → отдельная секция с категориями; баннер →external_url, мессенджеры → ссылки илиopen_chat.
В админке GoodStay-job помечается бейджем; в discovered_urls.parser будет "goodstay".
Не переносится 1:1: виджеты погоды и курсов валют, stories, квизы/опросы (гость пишет в наш чат), заказ/эквайринг/socket TeamJet, мультиязычный draft (берём RU).
Как это устроено¶
- Firecrawl (self-hosted) — краулит сайт через
POST /v2/crawl:maxDiscoveryDepth=2,limit=12,sitemap=skip. Отдаёт markdown, ссылки и URL картинок с каждой страницы. - site_parser (Playwright) — fallback, если Firecrawl недоступен или
SITE_PARSE_CRAWLER=playwright. В этом режиме парсится только главная. - app (Celery) — склеивает markdown всех страниц, вызывает LLM через HydraAI (
deepseek-v4-flashдля текста, vision-модель для фото), собираетdraft_payloadв формате экспорта консьержа. Для Hotbot-URL —HotbotClient+HotbotDraftBuilder; для GoodStay-URL —GoodstayClient+GoodstayDraftBuilder. - Админка — ревью черновика и:
- Применить к компании →
ConciergeExportService.import_company; - Перенести в шаблон →
ConciergeTemplateImportService(только суперадмин).
Список реально обойдённых URL сохраняется в discovered_urls.crawled_pages. Ссылки на номера/услуги/контакты — в rooms_urls / services_urls / contacts_urls. Для Hotbot в discovered_urls.parser будет "hotbot", для GoodStay — "goodstay".
Structured JSON-формат Firecrawl не используется: у self-hosted он требует OpenAI Responses API и ломается на OpenAI-совместимых прокси. Извлечение структуры остаётся на HydraAI (кроме веток Hotbot и GoodStay).
Запуск парсинга¶
Суперадмин¶
Меню → Парсинг сайтов отелей (/superadmin/site-parse):
- Введите URL главной страницы.
- При необходимости отметьте «Даю согласие на импорт фото».
- Дождитесь статуса
review. - Поправьте услуги/фото при необходимости.
- Либо Применить к компании → выберите отельера → Прикрепить.
- Либо Перенести в шаблон → создайте новый шаблон или допишите карточки в существующий.
Отельер¶
Пока у компании нет QR-табличек, на экране онбординга доступна кнопка Импортировать с сайта рядом с галереей шаблонов. После применения открывается редактор созданной QR-таблички. Перенос в глобальный шаблон отельеру недоступен.
Переменные окружения¶
В app/.env (см. также app/.env.site_parse.example):
HYDRA_API_BASE_URL=https://api.hydraai.ru/v1
HYDRA_API_KEY=sk-hydra-ai-...
HYDRA_TEXT_MODEL=deepseek-v4-flash
HYDRA_VISION_MODEL=gpt-4.1-nano
SITE_PARSE_CRAWLER=firecrawl
SITE_PARSE_CRAWL_MAX_DEPTH=2
SITE_PARSE_CRAWL_MAX_PAGES=12
SITE_PARSE_CRAWL_TIMEOUT_SEC=300
FIRECRAWL_BASE_URL=http://firecrawl-api:3002
FIRECRAWL_API_KEY=
SITE_PARSER_URL=http://site_parser:8090
PARSER_SERVICE_TOKEN=тот-же-токен-что-у-site_parser
В site_parser/.env (fallback):
PARSER_SERVICE_TOKEN=тот-же-токен
PARSER_PROXY_URL= # опционально
RENDER_MAX_RETRIES=2
Контейнер site_parser на проде: shm_size: 256mb, лимит ~768M. При Page crashed рендер повторяется до RENDER_MAX_RETRIES раз с перезапуском браузера.
Docker¶
Сервисы Firecrawl добавлены в docker-compose.yml / docker-compose.prod.yml (только app-network, порты наружу не публикуются):
| Сервис | Локально | Прод (Selectel) | Роль |
|---|---|---|---|
firecrawl-api |
ghcr.io/firecrawl/firecrawl |
cr.selcloud.ru/tourmakers/firecrawl-api |
API + workers |
firecrawl-playwright |
ghcr.io/firecrawl/playwright-service |
cr.selcloud.ru/tourmakers/firecrawl-playwright |
Рендер страниц |
firecrawl-postgres |
ghcr.io/firecrawl/nuq-postgres |
cr.selcloud.ru/tourmakers/firecrawl-postgres |
Очередь NuQ |
firecrawl-redis |
redis:alpine |
redis:alpine |
Кэш/рейтлимит Firecrawl |
firecrawl-rabbitmq |
rabbitmq:3-management |
rabbitmq:3-alpine |
Очередь сообщений |
В CI job mirror-firecrawl тянет образы с ghcr.io и пушит в Selectel; деплой ждёт и build-and-push, и mirror-firecrawl.
backend и celery_worker зависят от firecrawl-api и site_parser.
Прод (≈8G RAM + Supabase): лимиты Firecrawl-стека суммарно ≈2G (api 1G / 1 CPU, playwright 768M, redis/rabbit/pg с потолками), concurrency = 1, BLOCK_MEDIA=true. Без этого стек (api 3G + playwright 2G + site_parser 2G) уводит хост в thrashing (load 50+, iowait, avail Mem → 0). На машине с ≥16G можно поднять лимиты ближе к локальным.
API¶
| Метод | Путь | Описание |
|---|---|---|
| POST | /site-parse/jobs |
Создать задачу |
| GET | /site-parse/jobs |
Список |
| GET | /site-parse/jobs/{id} |
Статус и черновик |
| PATCH | /site-parse/jobs/{id}/draft |
Правки перед применением |
| POST | /site-parse/jobs/{id}/apply?company_id= |
Применить к компании |
| POST | /site-parse/jobs/{id}/apply-template |
Перенести в шаблон (суперадмин) |
Тело apply-template:
{
"template_id": null,
"title": "Название нового шаблона",
"slug": null,
"guest_template": "classic",
"is_active": true,
"append_cards": true
}
Если template_id задан — карточки дописываются в существующий шаблон (append_cards=true) или заменяют собственные (append_cards=false).
Статусы: queued → rendering → analyzing → photos → review → applied (или failed).
После переноса в шаблон в job пишется applied_template_id.