Архитектура
В продукте один движок-источник истины и одна оболочка:
lv24-seo 0.5.2владеет провайдерскими клиентами, обходом, анализом, отчётами, хранилищем, MCP-инструментами, CLI-командами и автоматизацией.- Приложение
0.2.2— это React-webview плюс Rust-оболочка процессов. Оно рисует экраны, редактирует конфигурацию и запускает встроенный движок; поведение движка оно не переписывает.
Обе поверхности используют одну историю SQLite, если указывают на одно рабочее пространство.
Слои движка
surfaces CLI (Typer) · MCP server · skills · automation
|
analysis/report deltas · rules · digests · render · delivery
|
store SQLite (WAL) · snapshots · sends ledger
|
providers/crawl provider adapters · fetcher · sitemap · log ingest
|
core config · http · errors · time · write gateЗависимости идут только вниз. providers и crawl никогда не импортируют report или cli. Анализ зависит от моделей хранилища, а не от классов провайдерских клиентов. Адаптеры не меняют базу; сохранение делает вызывающий код.
Структура пакетов движка
| Пакет | Ответственность |
|---|---|
core/ | Слоистая конфигурация, HTTP retry/rate limiting, типизированные ошибки, конверты, время, хранилище секретов, редактирование логов, write gate |
providers/ | Адаптеры GSC, Yandex Webmaster, Yandex Metrika, Bing, CrUX, PSI, опционально GA4 |
crawl/ | Fetcher, HTML-парсеры, robots и sitemap, модули правил, граф ссылок, приём логов |
analysis/ | Delta, возможности, indexing triage, fusion и правила обхода |
store/ | Схема SQLite, миграции, репозитории, ledger снимков и отправок |
report/ | Builder'ы, каталоги локалей, рендереры, доставка, diff |
mcp/ | Сервер, канонический реестр, ресурсы, промпты |
cli/ | Команды Typer и человекочитаемый вывод |
automation/ | Генераторы планировщиков, рецепты доставки, рецепты GitHub Actions |
src/lv24_seo/mcp/registry.py — единственный источник истины для метаданных инструментов. Он управляет JSON-схемами MCP, справкой CLI, schemas/tools.json и таблицами документации. Сгенерированные файлы не редактируют вручную; их перегенерируют:
python -m lv24_seo.mcp.registry --write
python -m lv24_seo.mcp.registry --checkИнструментов 11: девять читающих и два гейтированных write-инструмента. seo_crawl_site и seo_weekly_report — читающие инструменты, которые сохраняют локальные снимки, поэтому сгенерированная схема добавляет writes_local_state и помечает их readOnlyHint: false.
Контракт провайдера
Каждый адаптер возвращает один и тот же конверт — это делает честным объединение нескольких движков:
@dataclass(frozen=True)
class ProviderResult[T]:
source: str # gsc, yandex_webmaster, yandex_metrika, bing, crux, psi
property_id: str # provider property id
fetched_at: datetime # UTC
data_age_days: int # provider lag at fetch time
rows: list[T]
total: int | None # total available, when the provider reports it
truncated: bool # True when a cap was hit
limitations: list[str] # machine codes such as rows_cap_50000
warnings: list[str] # human hints such as host_not_loadedПравила:
truncatedвычисляется по собственному лимиту провайдера и признакам наличия данных сверх него, а не угадывается. GSC помечает последнюю страницу, ровно заполнившую лимит, какtruncated: true; полная страница не доказывает отсутствие следующей строки.- Коды
limitationsпохожи на enum и переводятся только в рендерере отчёта. - Деградировавший хост — это данные, а не исключение: пустые строки, предупреждение
host_not_loadedи ограничениеdegraded_host.
Конверты и модель сбоев
CLI --json и структурированный вывод MCP используют один конверт:
| Поле | Значение |
|---|---|
ok | Успех или типизированный сбой |
data | Полезная нагрузка при успехе |
error.code | Стабильный машинный код при сбое |
error.message | Человеческое сообщение без тел запросов и токенов |
error.next_action | Предлагаемая следующая команда |
error.retryable | Поможет ли повтор |
meta | Как минимум version; может добавить truncated, data_age_days, limitations, sources |
warnings | Отсутствующие токены, свойства, деградировавшие хосты |
Ветвитесь по error.code, а не по коду выхода: разные сбои делят exit 2. У PROPERTY_NOT_FOUND нет отдельного кода выхода. Exit 5 зарезервирован для успешной нагрузки с meta.truncated и meta.strict; сейчас strict не выставляет ни одна команда.
Частичный сбой — норма. Недельный отчёт с упавшими источниками всё равно возвращает доступные секции плюс sources_failed и complete: false. Он не отменяет весь запуск из-за 401/403/429/timeout одного движка. Полный простой даёт явный отчёт без данных.
Хранилище и миграции
SQLite работает в режиме WAL. data/seo.sqlite3 игнорируется git. Относительный storage.data_dir из seo.toml привязывается к каталогу конфига; встроенное значение по умолчанию и LV24_SEO_DATA_DIR остаются process-scoped. doctor --json сообщает meta.data_dir и meta.data_dir_source, а config show --json показывает эффективный путь. Основные таблицы:
| Таблица | Назначение |
|---|---|
properties | Настроенные сайты и id провайдеров |
metric_daily | Только настоящие посуточные строки, с ключом по дате |
metric_windows | Агрегаты окон с реальными [start, end] |
pages | Инвентарь URL и состояние индексации |
issues | Находки обхода и провайдеров с жизненным циклом |
snapshots | База для diff отчётов: канонический JSON в gzip |
sends | Ledger доставки и заявки идемпотентности |
runs | Каждый запуск обхода, отчёта или операции провайдера |
schema_migrations | Append-only ledger миграций |
Посуточные строки и агрегаты окон лежат в разных таблицах по построению, поэтому пересекающиеся окна не перезаписывают друг друга. Одномерный ключ хранится как есть; многомерные ключи кодируются в JSON, поэтому значение с | не конфликтует.
Изменения схемы — нумерованные миграции в store/db.py, записываемые в schema_migrations. Пользовательские строки при обновлении не удаляются. Миграция 2 помечает посуточные строки GSC, созданные до миграции, как accuracy=legacy_unknown_granularity; исходное окно восстановить нельзя. Обычное чтение их исключает, а вызывающий код может явно запросить include_legacy=True.
Сырых архивов провайдеров на диске нет. Снимки — это gzip-канонический JSON в таблице snapshots. raw_retention_days — зарезервированная настройка; pruning пишет в лог, что она ни на что не влияет.
Снимки отчётов и доставка
report/builders.py создаёт dataclass Report, сериализуемый в JSON и канонически английский. У Report.to_dict() нет поля locale, поэтому снимки, report diff и нагрузки MCP не меняются от языка отображения. report/i18n.py — только презентация: каталоги локалей (en, ru, zh), правила множественного числа и форматирование чисел, процентов, позиций и дат. Неизвестная локаль откатывается к английской; некорректное число или дата рендерятся пустым текстом, а не 0.
Недельное письмо — шаблон 600px с inline-CSS, KPI-карточками, топ-возможностями, серьёзностью проблем, объединёнными источниками, заметками о данных и футером. В нём нет внешних изображений, стилей, скриптов и tracking pixels. Доставка использует проверяющий TLS-контекст, нормализованный результат транспорта и семантический хеш содержимого, включающий локаль рендера. Смена локали переотправляет тот же период один раз; пересборка на том же языке — нет.
Report diff читает два сохранённых недельных снимка, по умолчанию второй с конца и последний. Метрики, присутствующие только в одном снимке, неизвестны, а не равны нулю. report diff только читает и не вызывает провайдеров.
Frontend приложения
Feature-Sliced Design, зависимости идут вниз:
app/ root store, providers, router, layout, styles
pages/ dashboard, reports, settings, configuration, recommendations, not-found
features/ save-config, secret-set, update, agent-task
entities/ config, engine, report, stats, recommendation, update
shared/ CLI transport, desktop settings/files, i18n, UI primitives, formattingВ 0.2.2 нет слоя widgets/. Заголовок, навигация и бейдж обновления живут в app/layout и features/update; слой виджетов добавляется только когда срез действительно переиспользуется между страницами.
Каждый срез предоставляет один публичный API и не импортирует внутренности другого среза. Парсер конверта и маппинг ошибок живут в shared; экраны не разбирают сырой вывод процесса. Локаль UI (en, ru, zh) независима от report.locale. Машинные коды не переводятся; неизвестные коды показываются как есть.
Состояние привязано к текущей рабочей области: смена workspace или потеря движка очищает состояние отчёта, статистики, doctor и секретов, а не показывает данные предыдущего проекта. Действия, которые не могут выполниться, отключаются, а не падают по клику. Каталоги en/ru/zh согласованы; отсутствующий ключ — ошибка теста.
MobX-хранилища держат корневое состояние, статус движка, черновик конфигурации, данные отчётов, рекомендации и состояние updater. UpdateStore делает тихую проверку при старте с таймаутом 30 секунд, ручную проверку из Settings и установку только после явного нажатия. Для portable, deb, rpm, MSI и неизвестных типов пакетов он отключает встроенную установку.
Rust-оболочка и sidecar
| Модуль | Ответственность |
|---|---|
commands.rs | Фиксированные команды Tauri, статус движка, MIN_ENGINE_VERSION, prepare_update |
cli.rs | Построение argv, stdin-мост, фильтр окружения, таймауты, ограниченный вывод |
config.rs | Патчи toml_edit, валидация, надёжная атомарная замена, резервные копии 0600, ограниченное чтение |
paths.rs | Корни config/data/log/export, allow-list путей, поиск sidecar |
settings.rs | Локаль UI, размеры окна, выбранный путь конфигурации |
error.rs | Типизированные ошибки на границе IPC |
Релизный sidecar — дерево PyInstaller onedir. Onefile отклонён: он распаковывается при каждом вызове, медленно стартует и плохо подходит для подписи macOS. Дерево отображается в ресурсы Tauri как engine/lv24-seo/; оболочка ищет по порядку:
LV24_SEO_BIN(только абсолютный путь);<resource_dir>/engine/lv24-seo/lv24-seo[.exe](bundled);<resource_dir>/lv24-seo/lv24-seo[.exe](bundled-flat);- дерево Linux AppImage
$APPDIR/usr/lib/<product>/engine/lv24-seo/(appimage); - соседний
lv24-seo/рядом с приложением (sibling); PATH, только если заданLV24_SEO_ALLOW_PATH_ENGINE=1.
Ничего не скачивается автоматически. Оболочка запускает lv24-seo version --json, записывает путь, источник и версию и сравнивает с MIN_ENGINE_VERSION (0.5.2 в текущем исходнике). Старый движок помечается compatible: false; UI считает его offline и показывает уведомление. Новее гейта проходит, но не является протестированным пином релиза.
Поток данных
- CLI: разбор аргументов → один
composition.Workspace(конфигурация плюс общий SQLite) → одна операция сервиса → закрытие хранилища. Apply записи с--confirmподключает SQLite-журнал идемпотентности; без хранилища конверт предупреждает, что защита от повтора отключена на пять минут жизни токена. - MCP: инструменты вызывают те же сервисы через тот же composition root. SDK выполняет обработчики в worker-потоках, но соединение
sqlite3привязано к создавшему его потоку, поэтому каждый инструмент регистрируется черезasync-адаптер. Вызовы идут в потоке event loop по одному; конкурентныеtools/callсериализуются. - Приложение: webview вызывает фиксированную команду Tauri; Rust проверяет аргументы, запускает sidecar с массивом argv и
--json, разбирает один конверт и возвращает типизированный результат. Одновременно активно одно рабочее пространство. Локального HTTP API нет.
Без init CLI и MCP работают без хранилища с явным предупреждением. Приложению всё равно нужен выбранный seo.toml; оно не придумывает каталог данных и не переопределяет LV24_SEO_DATA_DIR молча.
Переносимость
Поддерживаются Windows, macOS и Linux:
- Windows требует runtime WebView2; NSIS-установщик может его скачать, portable ZIP — нет. Оболочка запускается с
CREATE_NO_WINDOWи выставляетPYTHONUTF8=1иPYTHONIOENCODING=utf-8, чтобы не-ASCII пути переживали кодовые страницы. - macOS требует macOS 13 или новее. Сборки подписаны ad-hoc, без notarization.
- Linux-пакеты собраны на Ubuntu 22.04 с базой glibc 2.35. AppImage может требовать FUSE 2; deb и rpm — manual-only.
- Поставляемые цели: Windows x64, Linux x64, macOS arm64 и macOS x64. Сборок Windows arm64 и Linux arm64 нет.
См. Безопасность и приватность для контроля и Релизы и обновление для упаковки.
Source: _source/engine-architecture.md, _source/desktop-architecture.md, _source/engine-releases-0.5.2.md, _source/desktop-releases-0.2.2.md, _source/desktop-release.md.