# md2vk — Архитектура ## Обзор FastAPI-сервис, публикует Markdown на стену VK через `wall.post` с `format_data`. Полный стек: Python 3.12, FastAPI, SQLAlchemy 2.0 async, SQLite, httpx, cryptography (Fernet), Docker. ## Компоненты ``` ┌─────────────────────────────── /opt/md2vk (bigbox) ───────────────────────────────┐ │ │ │ docker (контейнер md2vk, порт 127.0.0.1:8420) │ │ ┌────────────────────────────────────────────────────────────────────┐ │ │ │ app/main.py FastAPI entrypoint, lifespan → init_db │ │ │ │ app/config.py Settings из env (pydantic-settings) │ │ │ │ app/database.py async engine (aiosqlite), get_db │ │ │ │ app/models.py ORM: User, VkAccount, Publication │ │ │ │ app/security.py Fernet encrypt/decrypt, API-key gen/verify │ │ │ │ app/vk_client.py httpx-клиент: wall.post, users.get │ │ │ │ app/converters/markdown_to_vk.py MD → VK format_data + чанки │ │ │ │ app/api/v1.py эндпоинты /api/v1/* │ │ │ │ app/api/deps.py auth по API-ключу (Bearer / тело) │ │ │ └────────────────────────────────────────────────────────────────────┘ │ │ │ │ /data (volume) → SQLite /data/md2vk.db │ │ /logs → access.{date}.log (JSONL аудит) │ │ /run/secrets/token_encryption_key (Docker secret) │ └───────────────────────────────────────────────────────────────────────────────────┘ ``` ## Поток публикации 1. Клиент (Hermes/skill) `POST /api/v1/publish` c `api_key`, `vk_account_id`, `message_md` 2. Auth: hash api_key → User.is_active 3. Load VkAccount → decrypt_token (Fernet, только в памяти) 4. `markdown_to_vk(message_md)` → chunks (VK-лимит ~4096 симв., режет по абзацам; `@` = 2 симв.) 5. Publication (status=draft|scheduled) 6. Если `publish_date` задан → status=scheduled (планировщик — TODO) 7. Иначе `vk.wall_post(message, owner_id, from_group, ..., format_data=items)` 8. Ответ: post_id, owner_id, url `https://vk.com/wall{owner}_{post}`; ошибки → status=error + error_message ## Порт/интерфейсы | Интерфейс | Адрес | Назначение | |-----------|-------|------------| | Локальный | 127.0.0.1:8420 | внутренний (Caddy на vps02 → 10.8.0.2:8420) | | VK API | https://api.vk.com/method | исходящий (wall.post, users.get) | | Swagger | /docs | openapi, за basic auth | ## База данных (SQLite) | Таблица | Ключевые поля | |---------|---------------| | users | id, name, email, api_key_hash, api_key_prefix, is_active, created_at, updated_at | | vk_accounts | id, user_id→users, vk_user_id (owner_id, `-` = сообщество), display_name, access_token_enc (Fernet), token_type (user\|group), is_active, expires_at, last_used_at | | publications | id, vk_account_id→vk_accounts, status (draft\|scheduled\|published\|error), markdown_original, vk_text, vk_format_data, vk_post_id, vk_owner_id, attachments, scheduled_at, published_at, error_message | ## Отложенные посты Модель поддерживает `scheduled_at`, `/publish` с `publish_date` создаёт запись `scheduled`. **Планировщика нет** — открытая задача (worker-процесс/cron, выбирающий `status=scheduled AND scheduled_at<=now`). ## Ошибки VK API Код | Смысл ----|------ `VkApiError` | обёртка: `error_code` + `error_msg` из ответа VK; пишется в `publications.error_message` ## Конвертер Markdown | Markdown | VK format | |----------|-----------| | `**bold**`, `__bold__` | `bold` | | `*italic*`, `_italic_` | `italic` | | `***bold italic***` | `bold` + `italic` | | `` `code` `` | `inline_code` | | `[text](url)` | `link` (+url) | | `# H1..H6` | `bold` (uppercase) | | `> quote` | `italic` | | ```` ```code```` | `code` (без format) | | `---` | `───` | Длинный текст режется на чанки по `\n\n` (абзацы), запас 10% от лимита 4096.