143 lines
9.8 KiB
Markdown
143 lines
9.8 KiB
Markdown
# MIEM Employees Server
|
||
|
||
Сервис собирает сотрудников МИЭМ с сайта ВШЭ, хранит карточки и историю обновлений в Postgres, показывает минимальную админку и отдает read-only MCP endpoint для ИИ-агентов.
|
||
|
||
## Архитектура
|
||
|
||
- `api`: FastAPI, REST API, HTML-админка, healthcheck.
|
||
- `worker`: weekly scheduler, который запускает парсинг по `CRAWL_CRON`.
|
||
- `mcp`: открытый HTTP MCP endpoint для ИИ-агентов.
|
||
- `postgres`: основная БД.
|
||
|
||
Парсер использует фиксированный источник сотрудников, по умолчанию `https://miem.hse.ru/persons`. Для каждой карточки сохраняются ФИО, должности, год начала работы, контакты, идентификаторы, вкладки профиля, секции, публикации, курсы, ВКР, новости, JSON-снапшот и сжатый HTML-снапшот. Детальные публикации дополнительно нормализуются в отдельную таблицу `employee_publications`, а новости из блока «В новостях» — в `employee_news_links`. Ссылки обходятся только из меню профиля самого сотрудника (`person-menu`), например `#sci`, `#teaching`, `#main`.
|
||
|
||
## Переменные окружения
|
||
|
||
Скопируйте `.env.example` в `.env` и поменяйте секреты:
|
||
|
||
```bash
|
||
cp .env.example .env
|
||
```
|
||
|
||
Основные настройки:
|
||
|
||
- `DATABASE_URL`: строка подключения SQLAlchemy.
|
||
- `SOURCE_URL`: список сотрудников МИЭМ.
|
||
- `CRAWL_CRON`: расписание в формате crontab, по умолчанию `0 3 * * 1`.
|
||
- `CRAWL_LIMIT`: опциональный лимит профилей для тестового запуска.
|
||
- `ADMIN_USERNAME`, `ADMIN_PASSWORD`: логин и пароль админки.
|
||
- `SESSION_SECRET`: секрет подписи cookie.
|
||
- `PARSER_USE_PLAYWRIGHT`: включение Playwright-рендера динамических вкладок.
|
||
|
||
## Локальный запуск
|
||
|
||
```bash
|
||
python -m venv .venv
|
||
.venv\Scripts\activate
|
||
pip install -r requirements.txt
|
||
uvicorn app.main:app --reload
|
||
```
|
||
|
||
Админка: `http://localhost:8000/admin`.
|
||
|
||
В админке доступны:
|
||
|
||
- `Dashboard`: общая статистика, последний добавленный сотрудник, прогресс текущего/последнего парсинга и ручной запуск.
|
||
- `Directory`: настраиваемая таблица сотрудников с фильтрами, сортировкой, пагинацией и выбором колонок.
|
||
- `Runs`: история запусков, ошибки и progress bar.
|
||
|
||
## Docker Compose
|
||
|
||
```bash
|
||
docker compose up --build
|
||
```
|
||
|
||
По умолчанию:
|
||
|
||
- API и админка: `http://localhost:8000`
|
||
- MCP: `http://localhost:8001/mcp`
|
||
- Postgres: `localhost:5432`
|
||
|
||
Таблицы создаются приложением при старте. При обновлении существующей базы приложение также добавляет недостающие runtime-колонки, например `crawl_runs.skipped_count`. SQL-миграции для ручного применения лежат в `migrations/`.
|
||
|
||
## Наполнение БД
|
||
|
||
Основная карточка сотрудника хранится в `employees`: профиль, статус, даты обнаружения/увольнения, текущий JSON `current_data`, checksum и версия парсера. История успешных изменений сохраняется в `employee_snapshots` вместе с JSON-снимком и сжатым HTML профиля.
|
||
|
||
Публикации теперь хранятся в двух видах:
|
||
|
||
- краткий список остается внутри `employees.current_data.sections[].publications` для обратной совместимости;
|
||
- детальные записи сохраняются в `employee_publications` и связываются с сотрудником через `employee_id`.
|
||
|
||
`employee_publications` содержит `publication_id`, название, год, тип публикации, язык, статус, ссылку на карточку HSE Publications, DOI, внешние/document-ссылки, citation text, аннотацию, описание, авторов, raw JSON ответа `searchPubs` и `source_hash` для безопасного повторного upsert. Уникальность поддерживается по `(employee_id, publication_id)` и `(employee_id, source_hash)`, поэтому повторный crawl не должен создавать дубликаты.
|
||
|
||
`list_employee_publications` сначала читает `employee_publications`; если детальных строк еще нет, возвращает старые публикации из `current_data`.
|
||
|
||
Новости сотрудников также хранятся в двух видах:
|
||
|
||
- краткий список остается внутри `employees.current_data.sections[].news_links`;
|
||
- нормализованные карточки из вкладки «В новостях» сохраняются в `employee_news_links`.
|
||
|
||
`employee_news_links` содержит название новости, ссылку, краткое описание, дату публикации, год публикации, raw JSON карточки и `source_hash`. Уникальность поддерживается по `(employee_id, url)` и `(employee_id, source_hash)`, поэтому повторный crawl не создает дубликаты.
|
||
|
||
## Парсинг
|
||
|
||
Weekly worker запускается по `CRAWL_CRON`. Ручной запуск доступен в админке на `Dashboard` и странице `Runs` или через REST:
|
||
|
||
```bash
|
||
curl -X POST http://localhost:8000/api/crawl-runs --cookie "miem_admin_session=..."
|
||
```
|
||
|
||
Алгоритм обновления:
|
||
|
||
- найденные сотрудники получают статус `active` и обновленный `last_seen_at`;
|
||
- новые сотрудники добавляются в `employees`;
|
||
- количество новых сотрудников за запуск сохраняется в `crawl_runs.new_count`;
|
||
- публикации из HSE Publications записываются в `employee_publications`, а краткий список остается в JSON профиля;
|
||
- новости из блока «В новостях» записываются в `employee_news_links`, а краткий список остается в JSON профиля;
|
||
- активные сотрудники, исчезнувшие из текущего списка источника, получают статус `dismissed` и `dismissed_at`;
|
||
- каждый успешный новый или измененный разбор сохраняет запись в `employee_snapshots`;
|
||
- неизмененные профили учитываются в `crawl_runs.skipped_count` и не получают новый snapshot.
|
||
|
||
Во время выполнения парсинга `found_count`, `parsed_count`, `skipped_count` и `error_count` обновляются в базе. Админка опрашивает `/api/crawl-runs/latest` и показывает прогресс как `(parsed_count + skipped_count + error_count) / found_count`.
|
||
|
||
## MCP
|
||
|
||
Endpoint: `POST /mcp`, без авторизации на уровне приложения.
|
||
|
||
Поддерживаемые tools:
|
||
|
||
- `get_service_info()`
|
||
- `sync_employees(client_hash?, include_data?)`
|
||
- `search_employees(query, status?, limit?)`
|
||
- `get_employee(profile_id_or_url)`
|
||
- `list_employee_publications(profile_id_or_url)` — публикации сотрудника; при наличии данных из `employee_publications` возвращает авторов, DOI, аннотацию, описание, citation text, год, тип, язык, статус и ссылку HSE Publications.
|
||
- `list_employee_courses(profile_id_or_url)`
|
||
- `get_crawl_status()`
|
||
- `get_crawl_run_details(run_id)`
|
||
|
||
`get_service_info` возвращает метаданные сервиса, список tools и текущую версию набора сотрудников. `sync_employees` отдает полный snapshot или delta по `client_hash`; checksum набора строится по сотрудникам, их статусам и текущим checksums. Ответы tools возвращаются как JSON-строка внутри MCP `content[0].text`.
|
||
|
||
Новости сотрудника отдельной MCP tool не имеют: они доступны в `get_employee(...).data.sections` и `sync_employees(include_data=true)` как секция `type = "news"` с массивом `news_links`.
|
||
|
||
Пример локального запроса списка tools:
|
||
|
||
```bash
|
||
curl http://localhost:8001/mcp \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
|
||
```
|
||
|
||
Если MCP нужно ограничить, делайте это на сетевом уровне: localhost binding, VPN, firewall, reverse proxy или другой внешний контур доступа.
|
||
|
||
## Обслуживание
|
||
|
||
```bash
|
||
docker compose logs -f api
|
||
docker compose logs -f worker
|
||
docker compose exec postgres pg_dump -U miem miem_workers > backup.sql
|
||
docker compose down
|
||
```
|
||
|
||
Версия сервиса: `0.7.0`. Админка всегда показывает версии backend и frontend в footer.
|