Перейти к содержимому

Архитектура ​

В продукте один движок-источник истины и одна оболочка:

  • lv24-seo 0.5.2 владеет провайдерскими клиентами, обходом, анализом, отчётами, хранилищем, MCP-инструментами, CLI-командами и автоматизацией.
  • Приложение 0.2.2 — это React-webview плюс Rust-оболочка процессов. Оно рисует экраны, редактирует конфигурацию и запускает встроенный движок; поведение движка оно не переписывает.

Обе поверхности используют одну историю SQLite, если указывают на одно рабочее пространство.

Слои движка ​

text
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 и таблицами документации. Сгенерированные файлы не редактируют вручную; их перегенерируют:

bash
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.

Контракт провайдера ​

Каждый адаптер возвращает один и тот же конверт — это делает честным объединение нескольких движков:

python
@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
sendsLedger доставки и заявки идемпотентности
runsКаждый запуск обхода, отчёта или операции провайдера
schema_migrationsAppend-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, зависимости идут вниз:

text
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/; оболочка ищет по порядку:

  1. LV24_SEO_BIN (только абсолютный путь);
  2. <resource_dir>/engine/lv24-seo/lv24-seo[.exe] (bundled);
  3. <resource_dir>/lv24-seo/lv24-seo[.exe] (bundled-flat);
  4. дерево Linux AppImage $APPDIR/usr/lib/<product>/engine/lv24-seo/ (appimage);
  5. соседний lv24-seo/ рядом с приложением (sibling);
  6. 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.