Telegram-бот для персональных заметок: принимает текст, голос, видео, документы,
форварды; транскрибирует медиа, извлекает текст из файлов (txt/md/csv/pdf/docx),
классифицирует через GPT-4o, сохраняет готовые страницы в
Notion (по умолчанию) или Buildin —
переключатель NOTES_PROVIDER=notion|buildin.
Бот заточен под личный workflow автора: набор workspace'ов, типов заметок и
DB-properties — это реф-пример, не «единственный правильный дефолт».
Если форкаешь под себя — отредактируй bot/domain/workspaces.py и
bot/domain/note_types.py (см. раздел «Под себя» ниже).
Эволюция: отдельные ветки и Conventional-Commits-PR в
main(см.AGENTS.md).
- Принимает текст, голосовые, аудио, видео, видео-кружочки, документы, фото (одиночные и альбомы), форварды.
- Голос/аудио/видео транскрибируется через OpenAI Speech-to-Text
(
gpt-4o-mini-transcribeпо умолчанию, ~$0.003/мин и ~6% WER на русском; для шумных записей —gpt-4o-transcribe, ~$0.006/мин и ~5% WER, переключается черезOPENAI_TRANSCRIBE_MODEL). Сырой транскрипт остаётся в БД (промежуточный шаг), в провайдер уходит только готовая заметка после LLM. БезOPENAI_API_KEY— голосовые отключены, бот отвечает «транскрибация выключена». - Документы (
txt,md,csv,pdf,docx) — прямой upload или форвард. Текст извлекается локально (pypdfдля PDF,python-docxдля DOCX, stdlib для остальных) и идёт в тот же LLM-pipeline. Лимит — 20 МБ (Telegram bot API). Длинные файлы (>40k символов после извлечения) проходят map-reduce: бьются на куски, каждый кусок суммаризуется отдельным LLM-вызовом, итоговая склейка классифицируется как обычно. Зашифрованные/сканированные PDF без текстового слоя помечают draftfailedс понятным сообщением. - Для форвардов извлекаются метаданные (автор, канал, дата, подпись,
оригинальный message_id) и подаются в LLM как контекст-prefix
[Forwarded] От: …; Когда: …. Draft помечаетсяkind=forward. - Фото и альбомы: форвард поста с одной или несколькими картинками
превращается в заметку, фото вставляется image-блоком перед body в
Notion / Buildin. Caption уходит в LLM как обычный текст (с тем же
forward-prefix, что и для текстовых форвардов). Альбом
агрегируется через
AlbumMiddleware(debounce 1 c поmedia_group_id) — один draft на весь альбом. URL фото — Telegram file-URL, живёт ~1 час: успей открыть страницу в Notion/Buildin чтобы провайдер закэшировал превью. Photo без caption → draft с пустым body, title =Photo from <author/chat>, LLM не вызывается. - Создаёт черновик в SQLite до любой обработки (durability-контракт).
- Один GPT-4o-вызов: классифицирует тип заметки и workspace
(наборы заданы в реестрах
bot/domain/note_types.pyиbot/domain/workspaces.py— в текущем реф-сетапе 9 типов: note / task / idea / meeting / 1on1 / work / personal / book / film, и 5 workspace'ов: personal / work / family / growth / ai_path), извлекает properties (Status, Priority, DueDate, Tags, Attendees, …) и форматирует тело как markdown под template типа. Дляmeetingотдельно различает регулярные синки команды черезextras.kind="sync": для sync LLM кладёт всю иерархию (по людям/зонам/темам) прямо вmarkdown_bodyчерез### subheading+ bullets — шаблон passthrough, без дублирующих секций. Для классическогоmeeting(kind=meeting) — секции Agenda / Decisions / Action items. Дляtaskописание задачи (контекст, мотивация) пишется вmarkdown_bodyперед## Чек-лист. Дляmeeting/1on1LLM проставляет дату вproperties.Date(ISO-форматYYYY-MM-DD, требование Notion API) и в title в скобках в человеческом формате(DD.MM.YYYY). БезOPENAI_API_KEY— stub-fallback (типnote, workspacepersonal, тело = исходник). - Превью с кнопками:
💾 Save/✖ Cancelсверху,🔁 Type/📁 Workspaceниже, ряд✏️ Edit. Для type=meeting добавляется ряд🔄 Sync ↔ Meetingдля ручного переключения подвида; рядом с типом показывается метка(sync)если kind=sync. Тап по тоглу запускает повторный LLM-проход с подсказкойforce_meeting_kind, чтобы тело переразметить под нужный шаблон (Agenda/Decisions для meeting или иерархический passthrough для sync). Workspace и тип можно переопределить вручную. ✏️ Editоткрывает Telegram Web App — встроенную форму с предзаполненными title и body (полный текст из БД, без обрезки превью). Юзер правит оба поля сразу, тапает «Сохранить» — то же самое preview-сообщение в чате обновляется in-place, без новых сообщений и без перенабора. ТребуетWEBAPP_BASE_URL(поддомен с HTTPS) и/setdomainв BotFather; без env-переменной кнопка скрыта.- На Save — кладёт в outbox-очередь, фоновой воркер вызывает API провайдера
(Buildin или Notion в зависимости от
NOTES_PROVIDER). - Routing: per-(workspace × type) для Buildin (
BUILDIN_DB_<WS>_<TYPE>→BUILDIN_DB_<TYPE>→BUILDIN_DB_DEFAULT); для Notion та же цепочка (NOTION_DB_<WS>_<TYPE>→NOTION_DB_<TYPE>→NOTION_DATABASE_ID) + lazy one-step auto-create подNOTION_PARENT_PAGE_<WS>если ничего не задано — бот создаёт full-page DB прямо в parent-page при первом hit и кэширует database_id вnotion_dbs. Notion рендерит full-page DB как страницу-раздел (📝 Заметки/✅ Задачи/ …) с full-width таблицей, отдельная wrapper-page не нужна. Без токена провайдера — stub-режим. - Markdown тела заметки конвертируется в provider-specific blocks (параграфы,
заголовки
#/##/###, списки-/1., цитаты>, code-fences```). Для Buildin длинные заметки (>100 блоков) дописываются чанками по 100 черезPATCH /v1/blocks/{page_id}/children. - На успех — атомарно
idempotency.insert + drafts.delete + outbox.delete. Превью редактируется в «✅ Открыть в Buildin/Notion» с прямой ссылкой на созданную страницу (если провайдер вернулurl); иначе — fallback на<code>page_id</code>. - На ошибку — экспоненциальный backoff. После
MAX_ATTEMPTSчерновик помечаетсяfailed, остаётся видимым через/list. - При
NOTES_PROVIDER=buildinбот на старте делает health-check (GET /v1/users/me) и предупреждает о незаданныхBUILDIN_SPACE_<WS>env'ах. - Команды:
/start,/help,/list,/retry. - На старте: восстановление черновиков, застрявших в
savingдольше 5 минут.
В Buildin: settings → integrations → создать новую integration. Скопировать
токен → BUILDIN_TOKEN в .env.
В реф-сетапе настроены 5 workspace'ов: personal, work, family,
growth, ai_path (см. bot/domain/workspaces.py — там же поля
label, description, space_env). Если форкаешь под себя — поправь
реестр и env-vars из таблицы ниже соответственно. Под каждый workspace
нужен отдельный Buildin space. Создайте space'ы руками, разрешите для
них integration, скопируйте UUID:
| Workspace | Env var |
|---|---|
personal (Личное) |
BUILDIN_SPACE_PERSONAL |
work (Работа) |
BUILDIN_SPACE_WORK |
family (Семья) |
BUILDIN_SPACE_FAMILY |
growth (Куда расти?) |
BUILDIN_SPACE_GROWTH |
ai_path (Путь ИИ) |
BUILDIN_SPACE_AI_PATH |
blog (Блог) |
BUILDIN_SPACE_BLOG |
Внутри каждого space скрипт создаёт full-page DB на каждый тип заметки.
Title берётся из plural-формы реестра типов (в реф-сетапе: 📝 Заметки,
✅ Задачи, 💡 Идеи, 🤝 Митинги, 👥 1:1, 💼 Рабочее, 🌱 Личное —
см. _PLURAL_TITLES в скрипте) — без префикса воркспейса, так как имя
space'а уже задаёт контекст. Buildin рендерит full-page DB как страницу-раздел
с full-width таблицей. Структура в Buildin:
space «Работа»
├── DB ✅ Задачи ← full-page DB (создаст скрипт)
├── DB 📝 Заметки
└── …
Можно создать руками (тогда скопируйте UUID DB из URL в BUILDIN_DB_<WS>_<TYPE>),
либо запустить автосоздание:
source .venv/bin/activate
python -m scripts.setup_buildin_dbs >> .envСкрипт уважает уже заданные env'ы — заполняет только пустые slot'ы.
Доп. флаги: --dry-run (без сетевых вызовов), --workspace=<key>,
--type=<key> (только конкретный workspace или тип).
Кроме перечисленных свойств бот всегда дописывает CreatedAt (date) — оно
создаётся скриптом автоматически.
NOTES_PROVIDER=buildin включает Buildin sink. Дефолт notion — для
переключения на Buildin поставьте NOTES_PROVIDER=buildin.
Рекомендуемый путь — per-workspace токены. В каждом Notion workspace,
где лежит NOTION_PARENT_PAGE_<WS>, создать свой Internal Integration на
https://www.notion.so/my-integrations и положить токен в
NOTION_TOKEN_<WS>. Internal Integration привязан к одному Notion
workspace — единый токен не сможет ходить между ними.
NOTION_TOKEN_PERSONAL=...
NOTION_TOKEN_WORK=...
NOTION_TOKEN_FAMILY=...
NOTION_TOKEN_GROWTH=...
NOTION_TOKEN_AI_PATH=...
Если все NOTION_PARENT_PAGE_<WS> лежат в одном Notion workspace —
достаточно одного Integration и legacy NOTION_TOKEN (fallback, когда
per-WS не задан):
NOTION_TOKEN=... # legacy fallback
Per-WS токен имеет приоритет над NOTION_TOKEN.
Бот ищет database для пары (workspace, type) по цепочке:
notion_dbs (SQLite cache)
→ NOTION_DB_<WS>_<TYPE> (env, например NOTION_DB_WORK_TASK)
→ NOTION_DB_<TYPE> (per-type, без workspace-разреза)
→ NOTION_DATABASE_ID (глобальный fallback)
→ one-step auto-create под NOTION_PARENT_PAGE_<WS>:
databases.create (full-page DB прямо в parent-page,
title = plural-форма типа).
database_id кэшируется в notion_dbs.
Любой первый результат — финальный. Notion рендерит full-page DB как страницу-раздел с full-width таблицей — отдельная wrapper-page не нужна.
Минимальный setup без ручного создания 35 баз. Создаёшь по одной пустой top-level странице в каждом Notion workspace — она будет parent для воркспейса бота. Расшарить страницу с integration — все вложенные DB наследуют доступ.
(Notion workspace «Личное»)
└── Notes-bot ← parent-page (расшарена с integration)
├── DB 📝 Заметки ← full-page DB (создаст бот при первом hit)
├── DB ✅ Задачи
└── …
ID каждой parent-страницы (32-hex из URL) → в .env:
NOTION_PARENT_PAGE_PERSONAL=...
NOTION_PARENT_PAGE_WORK=...
NOTION_PARENT_PAGE_FAMILY=...
NOTION_PARENT_PAGE_GROWTH=...
NOTION_PARENT_PAGE_AI_PATH=...
При первом сохранении в (workspace × type) бот создаст full-page DB с
plural-title типа (📝 Заметки и т.п.) под parent-страницей, со схемой
типа из реестра (note_types.py), и закэширует database_id в SQLite.
Структура растёт по факту использования — пустые комбинации не плодятся.
Workspace, для которого NOTION_PARENT_PAGE_<WS> не задан, фолбэкнется
на NOTION_DB_<TYPE> или NOTION_DATABASE_ID.
Если хочется контролировать схему — создавай руками. Schema ниже — реф-пример
из текущего bot/domain/note_types.py. Если редактируешь реестр, эта
таблица перестанет соответствовать твоей конфигурации:
| Тип | Env var (per-type) | Required properties (имя — kind) |
|---|---|---|
note |
NOTION_DB_NOTE |
Name (title), Tags (multi_select) |
task |
NOTION_DB_TASK |
Name (title), Status (select), Priority (select), DueDate (date) |
idea |
NOTION_DB_IDEA |
Name (title), Tags (multi_select) |
meeting |
NOTION_DB_MEETING |
Name (title), Attendees (multi_select), Date (date) |
1on1 |
NOTION_DB_1ON1 |
Name (title), With (rich_text), Date (date) |
work |
NOTION_DB_WORK |
Name (title), Tags (multi_select) |
personal |
NOTION_DB_PERSONAL |
Name (title), Tags (multi_select) |
book |
NOTION_DB_BOOK |
Name (title), Author (rich_text), Rating (select ⭐…⭐×10) |
film |
NOTION_DB_FILM |
Name (title), Director (rich_text), Year (rich_text), Genre (multi_select), Rating (select ⭐…⭐×10) |
Per-(workspace × type): NOTION_DB_<WS>_<TYPE> (например
NOTION_DB_WORK_TASK). Имеет высший приоритет среди env. Бот всегда
дописывает CreatedAt (date) — auto-create добавляет это поле сам.
Для каждой базы / родительской страницы: … (меню в правом верхнем углу)
→ Connections → выбрать integration. Auto-created DB наследуют доступ
от parent-page. Top-level page расшарена один раз — все вложенные DB и
страницы наследуют.
Скопировать database / page id из URL (32-символьный hex после workspace)
в соответствующую переменную .env. Без любого NOTION_TOKEN_<WS> /
NOTION_TOKEN бот работает в stub-режиме (логирует payload).
OPENAI_API_KEY в .env — включает реальный GPT-4o. Без ключа — stub
(тип note, body = raw input). Модель меняется через OPENAI_MODEL
(по умолчанию gpt-4o).
TMDB_API_KEY в .env — для заметок типа film бот ходит в TMDb
(themoviedb.org) и дозаполняет пустые Director/Year/Genre. Покрывает
knowledge cutoff модели (gpt-4o знает только релизы до ~окт 2023). TMDb
бесплатен, лимиты для личного бота избыточны. Регистрация:
https://www.themoviedb.org/settings/api → API Key (v3 auth). Пустой ключ —
дозаполнение отключено, поля заполняются только из LLM-extraction.
Реестры в bot/domain/ — это setup автора. Бот рассчитан на то, что
форкер их отредактирует под свой workflow. Файлы захардкожены не из-за
архитектурного ограничения, а потому что Python-dataclass'ы — самая
понятная форма для одного человека, который сам себе админ.
Шаг 1. bot/domain/workspaces.py — добавить/удалить/переименовать
записи в WORKSPACES. Поля Workspace:
key— ASCII-friendly id (попадает в env-имена и LLM-ответ).label— UI-строка для TG-кнопок (RU/EN/любой).description— подсказка для LLM, когда выбрать этот workspace.space_env— имя env-переменной для UUID Buildin space'а.
Шаг 2. bot/domain/note_types.py — записи в TYPES. Поля NoteType:
key,label,description(как у workspace).db_env— имя env-переменной для per-type Notion DB id (напримерNOTION_DB_TASK). Для Buildin берётся суффикс (NOTION_DB_TASK→BUILDIN_DB_<WS>_TASK).template_id— ключ ветки вdomain/templates.py.properties— список колонок DB. КаждаяNotionProperty:name(case-sensitive имя колонки),kind(title|rich_text|select|multi_select|date|checkbox),llm_hint(что LLM кладёт),select_options(для select/multi_select — фиксированный список значений; нужен setup-скрипту).
Шаг 3. Если меняется template_id — добавить ветку в
bot/domain/templates.py (рендер markdown под этот тип).
Шаг 4. Адаптировать тесты: tests/test_workspaces.py и
tests/test_note_types.py (а при правке templates — частично
tests/test_templates.py) проверяют конкретный набор ключей. После
кастомизации обновить тесты под новый набор — это разрешённое исключение
(см. CLAUDE.md, «исключения, когда тест разрешено менять»).
Шаг 5. Перегенерировать DB: удалить устаревшие
BUILDIN_DB_<WS>_<TYPE> из .env, удалить старые DB в Buildin UI,
запустить:
source .venv/bin/activate
python -m scripts.setup_buildin_dbs >> .envСкрипт читает реестры — создаст DB под обновлённый набор.
Шаг 6. Прогнать pytest -v — всё должно быть зелёным.
Бот всегда дописывает в каждую DB колонку CreatedAt (date) автоматически —
объявлять её в properties не нужно.
- Python 3.11+
- aiogram 3.x — Telegram bot framework
- pydantic-settings + python-dotenv — config через
.env - aiosqlite — async-драйвер SQLite
- httpx — async HTTP-клиент для Buildin API (тонкий клиент по openapi)
- notion-client — Python SDK Notion API (legacy fallback провайдер)
- openai — GPT-4o classify+format + Speech-to-Text транскрибация
(
gpt-4o-mini-transcribe/gpt-4o-transcribe) - pypdf, python-docx — извлечение текста из PDF/DOCX
- pytest + pytest-asyncio — тесты (
reportlab— dev-only, для генерации тестовых PDF вtest_doc_extractor.py) - Docker + docker-compose
bot/
├── main.py # entry: init_db → buildin_health_check → recover_stuck → outbox worker + webapp server → polling
├── config.py # pydantic settings
├── handlers/
│ ├── inputs.py # text → draft → LLM → preview
│ ├── voice.py # voice/audio/video/video_note → download → transcribe → LLM → preview
│ ├── documents.py # document (txt/md/csv/pdf/docx) → download → extract → LLM → preview
│ ├── photo.py # photo (single + альбом) → get_file URL → caption→LLM → image-блок в Notion/Buildin
│ ├── callbacks.py # Save / Type / Cancel + клавиатура выбора типа
│ └── commands.py # /start /help /list /retry
├── middlewares/
│ ├── auth.py # whitelist Telegram user IDs
│ └── album.py # агрегатор фото-альбома (debounce 1s по media_group_id)
├── services/
│ ├── llm_processor.py # GPT-4o classify+format (один вызов) + map-reduce process_long + stub fallback
│ ├── doc_extractor.py # txt/md/csv/pdf/docx → plain text (pypdf, python-docx, stdlib)
│ ├── notion_client.py # compat-shim → sinks/notion.py
│ ├── preview.py # refresh_preview helper (in-place edit_message_text)
│ ├── transcriber.py # OpenAI Speech-to-Text (gpt-4o-mini-transcribe default)
│ ├── tmdb.py # TMDb lookup для film: дозаполнение Director/Year/Genre
│ └── sinks/ # провайдеры хранилища заметок
│ ├── __init__.py # Sink Protocol
│ ├── _properties.py # общий property builder (shape="notion"|"buildin")
│ ├── notion.py # NotionSink — pages.create через notion-client (default)
│ ├── buildin.py # BuildinSink — httpx + Buildin API
│ └── factory.py # get_sink() по NOTES_PROVIDER
├── web/ # Telegram Web App (Mini App) для правки draft
│ ├── auth.py # validate_init_data — HMAC по схеме Telegram
│ ├── server.py # aiohttp: GET /edit, POST /edit/submit, GET /healthz
│ └── static/editor.html # одностраничник: title + body, нативная MainButton
├── domain/
│ ├── note_types.py # 8 типов с properties + db_env + LLM-hints + select_options
│ ├── workspaces.py # реестр workspaces (personal/work/family/growth/ai_path)
│ └── templates.py # per-type markdown шаблоны
├── storage/
│ ├── db.py # aiosqlite-соединение, WAL, миграции
│ ├── drafts.py # CRUD черновиков
│ ├── outbox.py # очередь с экспоненциальным backoff
│ ├── idempotency.py # dedup draft_id → page_id
│ └── save_tx.py # атомарный commit_save
├── workers/
│ └── outbox_worker.py # asyncio task: claim_due → save → commit
├── fsm/ # FSM-стейты (пока пусто, нужен в Increment 3)
└── utils/
├── progress.py # ProgressReporter
├── text_chunking.py
├── md_blocks.py # parse_markdown + markdown_to_blocks (Notion) + markdown_to_blocks_buildin + image_block_*
├── forward.py # извлечение метаданных forward + prefix для LLM
└── errors.py
scripts/
├── setup_buildin_dbs.py # авто-создание Buildin DB по реестрам
└── backup_db.sh # online-backup SQLite + ротация (для cron на VPS)
tests/ # pytest + pytest-asyncio, in-memory SQLite фикстура
data/ # SQLite БД (volume в compose)
python3.12 -m venv .venv # python3.11+ тоже подходит
source .venv/bin/activate
pip install -r requirements-dev.txt
cp .env.example .env
# заполнить BOT_TOKEN и ALLOWED_USER_IDS
python -m bot.maincp .env.example .env
docker compose up --builddocker-compose.yml ставит restart: unless-stopped, лимит памяти 512m
(хватает для документов, проходящих map-reduce), persistent volume
./data → /app/data (SQLite-файл живёт между рестартами), healthcheck
SELECT 1 к БД и ротацию логов (10 МБ × 5 файлов).
Кнопка ✏️ Edit в превью открывает встроенную HTML-форму прямо в
Telegram-клиенте: title и body предзаполнены полным текстом из БД (без
обрезки превью), сабмит обновляет то же preview-сообщение in-place. Без
этой настройки кнопка скрыта, всё остальное работает как раньше.
Что нужно сделать:
-
HTTPS-поддомен, который проксирует на порт
WEBAPP_PORTконтейнера бота (default8080). Reverse-proxy любой — nginx, Caddy, Traefik: важно только TLS-termination иproxy_pass http://<bot-host>:8080. Telegram WebApp-кнопки требуют HTTPS; порт у поддомена может быть нестандартным (Telegram-клиенты принимают URL видаhttps://host:port). -
.envбота:WEBAPP_BASE_URL=https://<твой-поддомен>[:port] WEBAPP_BIND_HOST=0.0.0.0 WEBAPP_PORT=8080Бот при старте увидит
WEBAPP_BASE_URL, поднимет aiohttp-сервер рядом с polling-ом и начнёт показывать кнопку✏️ Editв превью. -
Сетевой доступ от reverse-proxy к контейнеру бота. Если оба в Docker — подключить контейнер бота к сети reverse-proxy (через
docker-compose.override.ymlсnetworks: { caddy_net: { external: true } }или аналогом). Если reverse-proxy на хосте — оставитьports: ["8080:8080"]вdocker-compose.ymlиproxy_passна127.0.0.1:8080. -
BotFather:
/setdomain→ выбрать бота → ввести поддомен (без порта). -
Liveness-check:
GET https://<поддомен>/healthz→ok.
Под капотом: Telegram-клиент открывает /edit?draft_id=X, передаёт
initData (HMAC-подпись бот-токеном); aiohttp валидирует подпись + владельца
draft, отдаёт editor.html с инжектнутыми title/body. На «Сохранить» фронт
шлёт POST /edit/submit (initData в заголовке X-Telegram-Init-Data),
бэкенд обновляет draft и перерисовывает preview-сообщение через
bot.edit_message_text по сохранённому preview_msg_id — без новых
сообщений в чате.
sudo apt update && sudo apt upgrade -y
sudo apt install -y git sqlite3 curl ca-certificates
# Docker Engine + compose plugin
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
-o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \
https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" \
| sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io \
docker-buildx-plugin docker-compose-plugin
sudo usermod -aG docker $USER # нужен relogin / `newgrp docker`Минимум железа: 1 vCPU, 1 ГБ RAM, 10 ГБ SSD. Комфортно: 2 vCPU, 2 ГБ.
sudo mkdir -p /opt/notes-bot && sudo chown $USER:$USER /opt/notes-bot
cd /opt/notes-bot
git clone <repo-url> .
mkdir -p data
# .env заполнить локально (см. разделы «Подключение Buildin / Notion»
# и «OpenAI» выше) и залить:
# scp .env user@vps:/opt/notes-bot/.env
chmod 600 .env
docker compose up -d --build
docker compose logs -f botrestart: unless-stopped поднимает контейнер при ребуте VPS автоматически —
ничего настраивать в systemd не нужно.
scripts/backup_db.sh делает sqlite3 .backup (online, безопасно при
работающем боте через WAL), сжимает gzip и удаляет старше KEEP_DAYS.
sudo mkdir -p /var/backups/notes-bot
sudo chown $USER:$USER /var/backups/notes-bot
crontab -e
# добавить строкой:
17 3 * * * /opt/notes-bot/scripts/backup_db.sh >> /var/log/notes-bot-backup.log 2>&1Восстановление: остановить контейнер, распаковать дамп на место БД, запустить заново.
docker compose stop bot
gunzip -c /var/backups/notes-bot/notes-YYYYMMDD-HHMMSS.db.gz > data/notes.db
docker compose start botСоздать на healthchecks.io check (free до 20),
скопировать ping URL и в crontab -e:
*/5 * * * * docker inspect --format='{{.State.Health.Status}}' \
$(docker compose -f /opt/notes-bot/docker-compose.yml ps -q bot) 2>/dev/null \
| grep -q healthy && curl -fsS --retry 3 https://hc-ping.com/<uuid> > /dev/nullКонтейнер падает / Docker daemon упал / VPS не поднялся — пинги прекращаются и через 10 минут healthchecks.io шлёт алерт.
cd /opt/notes-bot
git pull
docker compose up -d --build
docker compose logs --tail=100 -f botОткат при несовместимой миграции БД: git checkout <prev-tag>,
восстановить БД из бэкапа, docker compose up -d --build.
| Симптом | Причина / фикс |
|---|---|
| Бот молчит на сообщения | ALLOWED_USER_IDS не содержит твой TG id (AuthMiddleware режет). |
Run polling for bot не появляется |
Неверный BOT_TOKEN или нет outbound https. |
На старте лог BUILDIN_SPACE_<X>= пуст |
Не заданы UUID space'ов — заполнить или ограничить роутинг через BUILDIN_DB_DEFAULT. |
| Документ >40k символов падает по OOM | Поднять deploy.resources.limits.memory в docker-compose.yml. |
sqlite3: not found в backup-скрипте |
sudo apt install sqlite3 на хост. |
source .venv/bin/activate
pytest -v179 тестов на момент 0.11.0 (23 файла):
test_config.py,test_auth.py— конфиг и middleware.test_drafts.py,test_outbox.py,test_idempotency.py,test_save_tx.py— storage-слой (in-memory SQLite через фикстуруfresh_db).test_outbox_worker.py— happy path, retry, idempotency после крэша, max_attempts.test_note_types.py,test_templates.py,test_workspaces.py— реестры.⚠️ Эти тесты завязаны на конкретный реф-сетап и требуют обновления при кастомизации (см. раздел Под себя).test_llm_processor.py— stub + real path с моком OpenAI (включая workspace classification + fallback на ошибке и неизвестный тип/workspace).test_handlers_inputs.py,test_handlers_callbacks.py— UX-логика (выбор типа + workspace).test_md_blocks.py,test_md_blocks_buildin.py— markdown → blocks для Notion и Buildin shapes.test_notion_client.py— Notion sink: stub/real mode, per-type DB routing, multi_select, fallback title, failure-injector.test_buildin_sink.py— Buildin sink (httpx MockTransport): wire shape, workspace × type routing, chunking длинных заметок, error mapping, health-check/v1/users/me.test_workspace_routing.py—settings.database_id_for()для обоих провайдеров.test_sink_factory.py—get_sink()переключение поNOTES_PROVIDER.test_sinks_properties.py— общий property builder для обоих shape'ов (shape="notion"|"buildin"): title/select/multi_select/date/checkbox, пустые значения, неизвестные kind.test_transcriber.py— happy-path с моком OpenAI Speech-to-Text, передачаFORCE_LANGUAGE_CODE, disabled-flag.test_handlers_voice.py— happy path с моками download/transcribe/LLM, durability при ошибках.test_forward.py— извлечение метаданных всех типов forward_origin (User/HiddenUser/Chat/Channel) + format_prefix + enrich.test_doc_extractor.py— txt/md/csv/pdf/docx happy-paths, пустой PDF →EmptyDocumentError, бинарь без mime →UnsupportedDocumentError, расширение приоритетнее mime.test_handlers_document.py— happy path с моками download/extract/LLM, oversized/audio/unsupported reject, durability при ошибках, forwarded →kind=forward+ enrich-prefix.
| Переменная | Назначение |
|---|---|
BOT_TOKEN |
Токен Telegram-бота от @BotFather |
ALLOWED_USER_IDS |
Разрешённые TG user IDs через запятую |
DATABASE_PATH |
Путь к файлу SQLite (по умолчанию data/notes.db) |
NOTES_PROVIDER |
notion (default) или buildin |
BUILDIN_TOKEN |
Токен Buildin integration. Пусто → stub-режим |
BUILDIN_SPACE_<WS> |
UUID space'а workspace'а (PERSONAL, WORK, FAMILY, GROWTH, AI_PATH, BLOG). Нужны для setup-script |
BUILDIN_DB_<WS>_<TYPE> |
Per-(workspace × type) DB id. Заполняется setup-script'ом или руками |
BUILDIN_DB_<TYPE> |
Per-type fallback DB id (без workspace-разреза) |
BUILDIN_DB_DEFAULT |
Финальный fallback DB id |
NOTION_TOKEN_<WS> |
Per-workspace токен Internal Integration (рекомендуемый). Заполнять для каждого NOTION_PARENT_PAGE_<WS>. Имеет приоритет над NOTION_TOKEN |
NOTION_TOKEN |
Legacy fallback: один токен, когда все parent-страницы в одном Notion workspace. Пусто + нет ни одного NOTION_TOKEN_<WS> → stub-режим |
NOTION_DATABASE_ID |
Default DB id (финальный fallback для всех типов и workspace'ов) |
NOTION_DB_<TYPE> |
Per-type DB id: NOTE, TASK, IDEA, MEETING, 1ON1, WORK, PERSONAL. Все optional |
NOTION_DB_<WS>_<TYPE> |
Per-(workspace × type) DB id, например NOTION_DB_WORK_TASK. Высший приоритет среди env. Optional |
NOTION_PARENT_PAGE_<WS> |
Parent-page id для lazy one-step auto-create (full-page DB прямо в parent-page) конкретного workspace. Если задан — бот создаст DB при первом сохранении в (<ws> × type) и закэширует database_id. Optional |
OPENAI_API_KEY |
Ключ OpenAI для GPT-4o classify+format и Speech-to-Text транскрибации. Пусто → stub LLM + voice/audio/video отключены |
OPENAI_MODEL |
Имя LLM-модели для classify+format (по умолчанию gpt-4o) |
OPENAI_TRANSCRIBE_MODEL |
STT-модель: gpt-4o-mini-transcribe (default, ~$0.003/мин, gpt-4o-transcribe ( |
FORCE_LANGUAGE_CODE |
ru, en, … Пусто → autodetect |
TEMP_DIR |
Временные файлы для скачанных медиа (по умолчанию /tmp/notes-bot) |