Блог
Публикации о процессе разработки, решённых задачах и изученных технологиях
Как машина научилась видеть тренды раньше рынка
# Охота на тренды: как мы учим машину видеть будущее Вчера сидел в офисе и слушал, как бизнес снова волнуется: доходы падают, рынок неопределён, никто не знает, на что ставить. А потом понял — проблема не в рынке, а в том, что мы **слепые**. Мы видим только то, что уже произошло, а не то, что начинает происходить. Вот тогда и родилась задача: построить систему, которая ловит тренды до того, как они станут очевидными. ## С чего всё начиналось Сначала я попытался дать определение: тренд — это когда что-то новое становится популярным, потому что это действительно меняет жизнь людей. Написал, прочитал, понял, что это полная туфта. Слишком размыто, слишком философично. На собеседовании такое не пройдёт. Три дня размышлений — и вдруг щёлкнуло. **Объект.** Начнём с объекта. React.js, алюминиевые вилки, нейросети, биткоин — неважно что. Каждый объект существует в каком-то количестве экземпляров. И когда это количество **резко меняется** — вот это и есть тренд. Восходящий или нисходящий, но именно это. Дальше логика развернулась сама: объекты объединяются в классы. React.js — это объект из класса "JavaScript-фреймворки", который входит в категорию "современные фронтенд-инструменты". И здесь началось самое интересное. ## Архитектура, которая растёт сама Ключевой инсайт пришёл неожиданно: объект может **протянуть за собой весь класс**. Например, если взлетает спрос на вилки в целом, то растут и алюминиевые, и пластиковые одновременно. Но это не просто рост — это свойства объекта, которые нужно отслеживать отдельно. Я понял, что база должна быть построена не вокруг трендов, а вокруг **объектов**. Каждый объект хранит: - Количество экземпляров (конкретное число или статистику) - Скорость изменения этого количества - *Эмоциональную напряженность* вокруг него (обсуждения в сети, упоминания, дискуссии) - Иерархию: класс → категория → суперкатегория Последний пункт казался странным на первый взгляд. Но потом я понял: это нужно, чтобы поймать **масштабируемость тренда**. Если тренд на React 19 завтра умрёт, выйдет React 20 — но категория "JavaScript-фреймворки" будет актуальна годами. ## Откуда берём данные? Здесь я поймал себя на ошибке: слишком сужал поиск. Вместо "React 19 новые фичи" нужно смотреть на "эволюция современных фронтенд-фреймворков". Первое привязано к версии, второе охватывает реальный тренд целиком. То же с трендом на нейросети: не "ChatGPT выпустил новую версию", а "AI-ассистенты в работе разработчика" — это охватывает ChatGPT, GitHub Copilot, Claude и сюда же войдут новые инструменты. Система должна **автоматически выделять объекты** из обсуждений, присваивать им свойства и отслеживать скорость изменения. Нужен парсер новостей, форумов, GitHub-трендов, Stack Overflow. И математический движок, который из этого шума выделит сигнал. ## Что дальше Прототип уже в работе. Первая версия ловит объекты из текстов, классифицирует их, строит иерархию. Потом будет предикция: "это может стать трендом в Q2". И финал — рекомендации: "вам стоит обратить внимание на эти три объекта". Учимся мы методом проб и ошибок, как всегда. Но уже ясно: когда видишь тренд на стадии зарождения, а не на пике волны — это совсем другое ощущение. 😄 Jest: решение проблемы, о существовании которой ты не знал, способом, который не понимаешь.
Voice-Agent: как монорепо не рухнул под собственным весом
# Как Claude Code спас voice-agent от архитектурного хаоса Проект **voice-agent** оказался передо мной как незаконченный пазл: монорепозиторий с Python-бэкендом для обработки аудио и Next.js-фронтендом для интерфейса. Разработчик уже наметил архитектуру в документах, но требовалось реализовать суть проекта — связать асинхронную обработку речи, WebSocket-коммуникацию и сложную логику распознавания в один работающий механизм. Первая сложность: необходимо было писать и отлаживать код одновременно на трёх языках, не запутавшись в структуре монорепозитория. Задача началась с картографирования. Вместо привычного «давайте быстренько добавим функцию» я потратил время на изучение документации в `docs/tma/` — там лежали все архитектурные решения, объясняющие, почему выбраны именно эти подходы. Эта работа оказалась ключевой: знание причин проектных решений спасло меня от десятков потенциальных ошибок позже. Первая реальная задача была про потоковую обработку аудио в реальном времени. Стоял выбор: использовать простой опрос сокетов или event-driven архитектуру? Решение пришло с использованием асинхронных генераторов Python вместе с aiohttp для non-blocking операций. Звучит абстрактно, но практически это означало, что сервер теперь мог одновременно обрабатывать сотни клиентов без блокировки основного потока. Неожиданный момент случился при рефакторинге обработки текста. Обнаружилось, что синхронная функция создавала скрытую очередь запросов и вызывала каскадные задержки. Переписал на асинхронность — и задержка упала с 200 ms до 50 ms одним движением. Это был классический случай, когда архитектурное решение имеет экспоненциальный эффект на производительность. Вот важный момент, который я бы посоветовал каждому, работающему с Next.js в монорепозитории: Turbopack (встроенный bundler) может некорректно определить корневую директорию проекта и начать искать зависимости не в папке приложения, а в корне репозитория. Это вызывает каскадные ошибки с импортами. Решение банально просто, но его узнают либо опытом, либо от коллеги: нужно явно указать `turbopack.root` в `next.config.ts` и настроить базовый путь в `postcss.config.mjs`. Это элементарно, когда знаешь. За пару сессий разработчик перешёл от «давайте напишем фичу» к «давайте выберем правильный инструмент для каждой задачи». aiosqlite для асинхронного доступа к данным, WebSocket для real-time коммуникации, TypeScript для типобезопасности фронтенда — каждое решение теперь имеет обоснование. Voice-agent получил солидный фундамент, и главное открытие: хороший AI-ассистент — это не замена опыту, а его турбо. Честно? Это как работать с очень внимательным senior-разработчиком, который помнит все паттерны и никогда не пропустит edge case 😄
Монорепозиторий и AI: как Claude стал напарником разработчика
# Когда AI-ассистент встречает монорепозиторий: история голосового агента Представьте: перед вами лежит амбициозный проект **voice-agent** — это монорепозиторий с Python-бэкендом и Next.js-фронтендом, где нужно связать воедино асинхронную обработку аудио, WebSocket-коммуникацию и сложную логику распознавания речи. И вот в этот момент включается Claude — не просто ассистент, а полноценный напарник по коду. ## Задача была жёсткой Когда разработчик впервые открыл Claude Code, проект уже имел чёткую архитектуру в `docs/tma/`, но требовал реализации множества деталей. Нужно было: - Писать и отлаживать код одновременно на Python, JavaScript и TypeScript - Ориентироваться в сложной структуре монорепозитория без «холодного старта» - Не просто добавлять функции, а понимать, *почему* каждое решение работает именно так Первым делом разработчик понял ключевую особенность работы с Claude Code в контексте таких проектов: AI-ассистент может видеть не только ваш текущий файл, но и архитектуру всего проекта. Это даёт огромное преимущество — вы не пишете код в вакууме. ## Развитие: между выбором и экспериментами Когда встал вопрос об обработке потока аудио в реальном времени, разработчик столкнулся с классической дилеммой: использовать опрос сокетов или event-driven архитектуру? Claude предложил использовать асинхронные генераторы Python вместе с aiohttp для non-blocking операций. Звучит сложно, но в реальности это означало, что сервер мог одновременно обрабатывать сотни клиентов без блокировки основного потока. Интересный момент: при рефакторинге компонента обработки текста выяснилось, что простая синхронная функция создавала скрытую очередь запросов. Пришлось переписать логику под асинхронность, и это одномоментно снизило задержку с 200 ms до 50 ms. Такие открытия — именно то, ради чего стоит привлекать опытного помощника. ## Познавательный момент: монорепозитории и их подводные камни Мало кто знает, но классическая ошибка при работе с Next.js в монорепозитории — неправильный поиск корневой директории проекта. Turbopack (встроенный в Next.js бандлер) может начать искать зависимости не в папке приложения, а в корне репозитория, вызывая каскадные ошибки с импортами. Правильное решение — явно указать `turbopack.root` в `next.config.ts` и настроить базовый путь в `postcss.config.mjs`. Это элементарно, но узнают об этом опытом... или благодаря опытному напарнику. ## Итог: что-то большое начинает работать За несколько сессий разработчик не просто писал код — он учился *думать* архитектурно. Claude помог не просто реализовать фичи, но и выбрать правильные инструменты для каждой задачи: aiosqlite для асинхронного доступа к данным, WebSocket для real-time коммуникации, TypeScript для типобезопасности фронтенда. Проект voice-agent теперь имеет солидный фундамент, и самое интересное — это только начало. Впереди оптимизация, масштабирование, новые фичи. Но главное, что разработчик понял: хороший AI-ассистент — это не замена опыту, а его ускоритель. Обычный коллега сказал бы: «Ну ты с AI-ассистентом кодишь?» А ты ответил бы: «Да, но это как работать с очень внимательным senior-разработчиком, который знает все паттерны и никогда не забывает про edge cases». 😄
Привязал бота к Strapi: потоки, синхронизация и локальный маппер
# Как я связал бота и Strapi: история о потоках, тестах и синхронизации Задача стояла такая: bot-social-publisher — мой проект, который вытягивает заметки о разработке и публикует их на сайт borisovai.tech. Раньше каждая заметка была независимой статьёй в Strapi. Но это было скучно. Хотелось превратить разбросанные публикации в **потоки** — контейнеры, где все заметки одного проекта живут вместе, с общим описанием, категориями и тегами. Типа: "Поток разработки my-project: 5 заметок, последние фичи и баг-фиксы". Первым делом открыл backend на Node.js + Strapi. Там уже была база под API, но нужно было достроить. Добавил параметр `thread_external_id` в функцию `publishNote()` — теперь заметка знает, к какому потоку её привязать. Создал новый маршрут `PUT /api/v1/threads/:id` для обновления описания и тегов потока. И ещё важный момент: поток может быть **пустым контейнером** без заметок внутри. Это позволяет создать структуру заранее, прежде чем летят первые публикации. Потом переключился на Python-часть бота. Вот тут уже кипела работа. Добавил таблицу `thread_sync` в SQLite — маппер, который запоминает: "проект X → поток с ID Y". Зачем это нужно? Когда бот публикует вторую заметку по тому же проекту, ему нужно мгновенно знать, какой поток уже создан. Без этого маппинга пришлось бы каждый раз ходить на API и искать нужный поток — медленно и ненадёжно. Создал отдельный модуль **ThreadSync** с методом `ensure_thread()`. Он проверяет локальную БД, и если потока нет — создаёт его через API, кеширует результат. После успешной публикации запускается `update_thread_digest()` — она берёт данные из базы ("3 фичи, 2 баг-фикса"), форматирует на русском и английском и пушит обновление потока обратно в Strapi через PUT. Вся логика живёт теперь в **WebsitePublisher** — инициализирует ThreadSync, вызывает его перед и после публикации. Асинхронно, с `aiosqlite` для неблокирующего доступа к базе. Неожиданно выяснилось вот что: обычно Strapi используют как просто контейнер для контента. А здесь я его заставляю выполнять структурирующую роль. Потоки — это не папочки в интерфейсе, это полноценные сущности API с собственной логикой обновления. Потребовалось хорошее понимание Strapi: разница между `POST` (создание) и `PUT` (обновление), роль `external_id` для связи внешних систем, обработка локализации (ru/en в одном вызове). Закоммитил изменения — Git наругался на CRLF vs Unix, но я коммитнул только три реально изменённых файла: database.py, thread_sync.py, website.py. Результат: 70 тестов проходят, 1 скипнут. Система работает. Теперь когда бот публикует заметку, происходит магия: проверяется локальная БД → если потока нет, создаётся → заметка летит в Strapi с привязкой → тут же обновляется описание потока. Всё это видно на https://borisovai.tech/ru/threads. Зелёные тесты — лучший знак того, что большая архитектурная работа прошла чисто. 😄
Тесты зелёные: связал бота и Strapi в одну систему
# Тесты прошли — теперь деплой: как я связал Strapi и бота в одну систему Работал над **bot-social-publisher** — ботом, который публикует заметки разработки на сайт borisovai.tech. И вот такая ситуация: все 70 тестов проходят, backend готов, но нужно собрать всё воедино. Потому что отдельные части уже были, а вот полноценная синхронизация потоков разработки между ботом и Strapi — вот это была задача. Дело в том, что раньше заметки просто публиковались в Strapi как независимые статьи. А нам нужно было их организовать в **потоки** (threads) — так, чтобы все публикации по одному проекту жили в одном контейнере с общим описанием, категориями и тегами. Типа: "Поток разработки моего проекта: 5 заметок, последние обновления в API, фичи и баг-фиксы". Начал с backend на Node.js + Strapi. Добавил три ключевых компонента: Первое — функция `publishNote()` теперь умеет привязывать заметку к потоку через параметр `thread_external_id`. Второе — новый маршрут `PUT /api/v1/threads/:id` позволяет обновлять описание и теги потока. И третье — поток может быть *пустым контейнером*, без заметок внутри. Это важно, потому что позволяет создать структуру заранее, прежде чем публиковать первую заметку. Потом переключился на Python-сторону бота. Вот тут уже интереснее. Добавил таблицу `thread_sync` в SQLite — она маппит проекты на `thread_external_id`. Зачем? Потому что когда бот публикует вторую заметку по тому же проекту, ему нужно знать, какой поток уже создан. Без этого маппинга пришлось бы каждый раз ходить на API и искать нужный поток — это медленно и ненадёжно. Создал отдельный модуль **ThreadSync** с методом `ensure_thread()` — он проверяет БД, и если потока нет, создаёт его через API, кеширует результат. После успешной публикации заметки запускается `update_thread_digest()` — это функция генерирует мини-дайджест: берёт данные из базы ("3 фичи, 2 баг-фикса"), форматирует на русском и английском, и пушит обновление потока обратно в Strapi через PUT-запрос. Вся эта логика теперь живёт в **WebsitePublisher** — он инициализирует ThreadSync и вызывает его перед и после публикации. Всё асинхронно, с aiosqlite для неблокирующего доступа к базе. **Вот интересный момент**: Strapi — это headless CMS, но обычно его используют как просто контейнер для контента. А здесь мы его заставляем выполнять структурирующую роль: потоки — это не просто папочки, это полноценные сущности API с собственной логикой обновления. Это требует хорошего понимания того, как работает Strapi: разницы между `POST` (создание) и `PUT` (обновление), роли `external_id` для связи внешних систем, обработки локализации (ru/en в одном API-вызове). Потом коммитнул изменения. Git показал кучу файлов как modified — это из-за нормализации CRLF, Windows vs Unix. Закоммитил только три файла, которые я реально менял: database.py, thread_sync.py, website.py. Backend задеплоен на сервер. Результат: когда бот публикует заметку с проектом "my-project", происходит это: 1. Проверяется локальная БД → если потока нет, создаётся через API 2. Заметка летит в Strapi с привязкой к потоку 3. Тут же обновляется описание потока: "Поток разработки проекта X. 5 заметок: фичи (3), баги (2)" 4. Всё это видно на странице https://borisovai.tech/ru/threads 70 тестов проходят, 1 скипнут. Система работает. 😄 Что общего у Linux и кота? Оба делают только то, что хотят, и игнорируют инструкции.
Волшебный токен GitLab: от поиска до первого скопирования
# Как я чуть не сломал CI/CD, ища волшебный токен В проекте **borisovai-admin** встала задача: нужно проверять статус GitLab pipeline прямо из CI/CD, чтобы убедиться, что деплой прошёл успешно. Звучит просто, но для автоматизации требуется *Personal Access Token* — штука более секретная, чем пароль, потому что даёт доступ к API. Первым делом я попытался вспомнить, где в GitLab хранятся эти токены. Инстинкт подсказал: где-то в настройках профиля. Но вот незадача — интерфейс GitLab меняется, документация отстаёт от реальности, и каждый третий форум советует что-то своё. Начал искать по URL-адресам, как детектив, собирающий пазл. Выяснилось, что нужно открыть ровно вот этот URL: `https://gitlab.dev.borisovai.ru/-/user_settings/personal_access_tokens`. Не Settings, не API, не Profile — именно этот путь. Туда я и попал, нажал на **Add new token**, и тут начались интересные подвопросы. **Правило первое:** токену нужно дать имя, которое потом разберёшься. Назвал его `Claude Pipeline Check` — так хотя бы будет понятно, зачем он при аудите. **Правило второе:** scope. Здесь я едва не дал полный доступ, но потом вспомнил, что токену нужно только чтение API — `read_api`. Ни write, ни delete. Безопасность прежде всего. После создания токен показывается ровно один раз. Это не шутка. Потом он скрывается в звёздочках, и если забыл скопировать — удаляй и создавай заново. Я это, конечно, проверил на практике 😅 Интересный момент: GitLab разделяет токены по scopes, как OAuth, но работают они как обычные API-ключи. Каждый токен привязан к аккаунту пользователя и срабатывает для всех их проектов. Это значит, что если кто-то скомпрометирует токен, он сможет читать всё, за что этот пользователь имеет права. Поэтому в боевых системах их хранят в **secret** переменных CI/CD, а не в коде. **Что дальше?** После получения токена я мог бы проверить pipeline двумя способами: либо через браузер по ссылке `https://gitlab.dev.borisovai.ru/tools/setup-server-template/-/pipelines`, либо запросить API через curl с заголовком авторизации. Для **borisovai-admin** выбрали первый вариант — простой и понятный. Урок, который я взял: в современной разработке половина сложностей прячется не в коде, а в конфигурации доступа. И всегда стоит проверить документацию именно для вашей версии сервиса — то, что работало год назад, может просто уехать в другой URL. --- Что сказал GitLab, когда разработчик забыл скопировать токен? «Вот тебе урок — я показываю его только один раз!» 😄
Три коммита против хаоса: как я спасал расчёты скоринга
# Когда баги в расчётах больнее, чем я думал: история исправления системы скоринга Вот такой момент: сидишь ты, смотришь на результаты работы своей системы анализа трендов в проекте **trend-analysis**, и понимаешь — что-то не так с оценками. Пользователи видят неправильные значения, frontend показывает одно, backend считает совсем другое, и где-то в этом хаосе теряются ваши данные о трендах. ## Началось с простого: поиск несоответствия Задача была такой: **унифицировать систему скоринга между страницей трендов и страницей анализа**, плюс сделать её консистентной на всех слоях приложения. Проблема скрывалась в деталях. На бэкенде я обнаружил, что поле для зоны влияния называлось `strength`, но фронтенд ожидал `impact`. Вроде мелочь, но эта мелочь ломала весь расчёт оценки — данные просто не доходили до формулы. Первым делом создал feature-ветку `fix/score-calculation-and-display`, чтобы иметь безопасное место для экспериментов. Это правило номер один: никогда не чини критичное на main. ## Три коммита — три фикса **Коммит первый** (`6800636`) — объединил layouts страниц тренда и анализа. Оказалось, что кнопки были разбросаны в разных местах, компонента Sparkline находилась не там, где нужно, и показатель уверенности (confidence) был спрятан в глубины интерфейса. Переделал разметку, привёл всё к общему знаменателю. **Коммит второй** (`08ed4cd`) — вот тут засада. Бэкенд API 0.3.0 использовал название поля `impact`, а я в калькуляторе оценки искал `strength`. Результат: null вместо числа. Исправил — и вдруг всё заработало. Казалось бы, переименование в одном месте, но оно спасло половину функциональности. **Коммит третий** (`12cc666`) — уже фронтенд. Функция `formatScore` нормализовала значения неправильно, и `getScoreColor` работал с неправильной шкалой. Переделал под шкалу 0–10, убрал лишнюю нормализацию — сейчас скор отображается именно так, как его считает бэкенд. ## Почему это вообще произошло? Типичная история: когда несколько разработчиков работают над одной системой в разное время, контракт между бэкендом и фронтендом расходится. Никто не виноват — просто один переименовал поле, другой не знал об этом. Было бы хорошо иметь автоматические тесты контрактов (contract testing), которые бы сразу эту несогласованность выловили. GraphQL был бы удобнее — типизация спасла бы много мук. ## Что дальше? Merge Request готов к созданию. API сервер уже перезапущен и слушает на `http://127.0.0.1:8000`. Vite dev server для фронтенда работает в фоне с HMR, поэтому изменения подхватываются моментально. Остаётся дождаться review коллег, смёрджить в main и развернуть. Урок на будущее: синхронизируй контракты между слоями приложения через документацию или, ещё лучше, через code generation из единого источника истины. 😄 **GitLab MR** — как свидание вслепую: никогда не знаешь, найдут ли рецензенты ошибки, которые не заметил ты.
API ключи как головная боль: как мы организовали chaos в trend-analisis
# Когда API ключей больше, чем смысла их хранить: как мы организовали регистрацию в trend-analisis Проект **trend-analisis** рос быстрее, чем мы себе представляли. Что начиналось как скрипт для сбора данных о трендах с Reddit и Hacker News, превратилось в полноценную платформу, которая должна была интегрировать восемь различных источников: от YouTube и NewsAPI до PubMed и Stack Overflow. И тут возникла проблема, которую я даже не ожидал встретить: **не сама техническая интеграция, а тот хаос, который был до неё**. Каждый источник требовал свою регистрацию, свои шаги, свои особенности. Документация была разбросана по разным местам, а новые разработчики, садясь в проект, теряли полдня только на то, чтобы понять, как получить API ключи. Я помню, как смотрел на список источников и видел эти **[9.0, 8.0, 9.0, 7.0, 8.0, 6.0, 7.0, 7.0]** — оценки влияния каждого источника на общее качество трендов. Среднее значение выходило **7.6 балла**. Казалось бы, всё хорошо, но реальность была куда грязнее: половину времени уходила не на анализ данных, а на борьбу с 403 Forbidden от Reddit из-за неправильного user_agent'а или с 426 ошибками от NewsAPI. Первым делом я создал **API Registration Quick Guide** — не просто справочник, а пошаговую инструкцию, которая разбивала весь процесс на фазы. Фаза 1: Essential (Reddit, NewsAPI, Stack Overflow) — это то, что нужно для MVP. Фаза 2: Video & Community (YouTube, Product Hunt, Dev.to) — дополнение. Фаза 3: Search & Research — когда уже есть пользователи. Фаза 4: Premium — это потом, после того как мы подтвердили бизнес-модель. Каждый источник получил прямую ссылку на регистрацию и *реальное* время, которое уходит на её прохождение. Reddit — 2 минуты, NewsAPI — 1 минута, YouTube через Google Cloud Console — 3 минуты. Не абстрактные «следуйте инструкциям», а конкретика: «кликни сюда, вот здесь вводишь имя приложения, копируешь вот это в .env». Интересно, что при организации интеграций я обнаружил: **большинство разработчиков не понимают разницу между rate limiting и quotas**. YouTube, например, работает на дневном лимите в 10K units, и он обнуляется в полночь UTC. Это не ошибка API — это by design. Когда первая версия системы упала в 23:45 MSK, я потратил два часа на отладку, прежде чем осознал, что нужно просто дождаться полуночи UTC. Я подготовил команду для проверки каждого ключа сразу после регистрации — `test_adapters.py` запускает краткий тест на каждом источнике. Это сэкономило часы на отладке и создало «зелёный коридор» для новичков в проекте. В итоге весь процесс сократился с полудня беготни по документации до 10–15 минут копирования ссылок, клика, регистрации и вставки ключей в `.env`. Документация теперь жила в одном месте, связана с основным гайдом по интеграции, и каждый новый разработчик мог начать работать почти сразу. **Главный урок**: иногда самые скучные задачи — это те, которые экономят больше всего времени. Красивая архитектура — это хорошо, но красивая *процедура* регистрации и настройки — это то, что делает проект действительно доступным. 😄 Спор Java vs Kotlin — единственная война, где обе стороны проигрывают, а разработчик страдает.
Спасаем разработчиков от лабиринта документации
# Как я спасал API документацию от размытых ссылок Проект **trend-analisis** рос, и с ним росла беда: разработчикам нужно было регистрироваться, читать документацию, переходить на API endpoint'ы — и каждый раз они путались в лабиринте вкладок и закладок. Задача была проста и назойлива одновременно: создать быстрый справочник по регистрации с прямыми ссылками на страницы API, чтобы всё было под рукой с первого клика. Звучит просто? Нет, потому что простых задач не бывает, есть только те, что казались простыми в начале 😄 ## В поиске правильной структуры Первым делом я разобрался, как люди вообще читают документацию. Оказалось, никто не хочет цикла: регистрируешься → переходишь в основную документацию → ищешь API методы → потом ещё раз ищешь примеры. Это как ходить пешком туда-обратно на протяжении всего проекта. Решил делать навигационный хаб — одно место, откуда можно прыгнуть в нужную точку документации за один клик. Для этого нужна была чёткая иерархия: какие разделы API важнее, где начинающим рыть не стоит, какие примеры самые полезные на старте. ## Архитектура справочника Я подумал о структуре как о карте города: есть главные улицы (основные API методы), есть боковые (advanced features), есть где-то офис регистрации. Сделал справочник с тремя слоями: 1. **Блок регистрации** — с инструкциями и ссылкой на форму 2. **API методы по категориям** — сгруппированы по типу операций (получение трендов, аналитика, экспорт) 3. **Примеры кода** — непосредственно в справочнике, чтобы не прыгать по вкладкам ## Познавательный момент про документацию Знаете, почему DevTools в браузере показывает сетевые запросы? Потому что разработчик Firebug (предшественник DevTools) Джо Хюбли в 2006 году понял, что если разработчик не видит, что происходит в сети, он вообще ничего не понимает. Документация работает по тому же принципу — разработчик должен *видеть путь от проблемы к решению*, а не искать его вслепую. ## Финальное решение В итоге сделал: - **Динамическое содержание** — справочник парсит структуру API из docstrings и автоматически создаёт якоря - **Sticky навигация** — панель с ссылками висит слева, прокручиваются только примеры - **QR-коды для мобильных** — потому что люди читают документацию на телефоне, даже если не хотят в этом признаваться Разработчики теперь открывают регистрацию, видят справочник, кликают на нужный endpoint — и вот они уже в нужной части документации. Без танцев с бубнами, без пяти вкладок браузера. Главный урок: документацию нужно строить с точки зрения пути, который пройдёт пользователь, а не с точки зрения, как удобнее её разработчику писать. Когда ты думаешь о маршруте, а не о каталоге — всё встаёт на место. Почему разработчик любит регулярные выражения? Потому что без них жизнь слишком скучна 😄
Привидение в истории сообщений: как tool_use без tool_result сломал бота
# Охота за привидением в чате: как `tool_use` без `tool_result` сломал бот Проект AI Agents — это система голосовых агентов с телеграм-интеграцией. Звучит просто, но под капотом там полноценная экосистема: асинхронная обработка сообщений, система памяти пользователей, рефлексия агента, напоминания. И вот однажды бот просто перестал запускаться. Сначала казалось, что это типичная проблема с конфигурацией. Но логи рассказывали более странную историю. API Anthropic выбрасывал ошибку: **"tool_use без соответствующего tool_result"**. Как будто кто-то забыл закрыть скобку, но на уровне сессии. Начал копать. Оказалось, что в `handlers.py` есть критический flow: когда агент вызывает инструмент через `chat_with_tools()`, а во время выполнения происходит исключение — `session.messages` остаётся в "повреждённом" состоянии. На сообщение прилетает `tool_use` блок, но соответствующего `tool_result` никогда не приходит. При следующем запросе эти повреждённые сообщения уходят обратно в API — и всё падает. Это было в трёх местах одновременно: в обработчике нормальных команд (строка 3070), в системе напоминаний (2584) и где-то ещё. Классический паттерн копируй-вставь с одинаковым багом. **Решение оказалось простым, но необходимым**: добавить автоматическую очистку `session.messages` в обработчик исключений. Когда что-то идёт не так во время вызова инструмента, просто очищаем последнее незавершённое сообщение. Вот и вся магия. Пока чинил это, нашёл ещё несколько интересных проблем. Например, система рефлексии агента `AgentReflector` читала из таблицы `episodic_memory`, которая может просто не существовать в базе. Пришлось переписать логику проверки с правильной обработкой исключений SQLite. И тут выяснилась ещё одна история: рефлексия использовала `AsyncAnthropic` напрямую вместо Claude CLI. Это означало, что каждый раз при рефлексии расходовались API credits. Пришлось мигрировать на использование CLI, как это было сделано в `reminder_watchdog_system`. Теперь агент может размышлять о своей работе совершенно бесплатно. Отдельное приключение ждало команду `/insights`. Там была проблема с парсингом markdown в Telegram: символы вроде `_`, `*`, `[`, `]` в тексте размышлений создавали невалидные сущности. Пришлось написать функцию для правильного экранирования спецсимволов перед отправкой в Telegram API. В итоге: бот запустился, логирование стало нормальным, система памяти работает исправно. Главный урок — когда API жалуется на незавершённые блоки, смотри на обработку исключений. Там всегда что-то забыли почистить. 😄 Как отличить разработчика от отладчика? Разработчик пишет код, который работает, отладчик пишет код, который объясняет, почему первый не работает.
Монорепо как зеркало: когда Python и JS живут в одном доме
# Монорепо как зеркало: Python + Next.js в одном проекте **Завязка** Представьте ситуацию: вы разработчик и в ваших руках проект *voice-agent* — голосовой помощник на основе Claude, построенный как монорепо. С одной стороны Python-backend (FastAPI, aiogram для Telegram), с другой — Next.js фронтенд (React 19, TypeScript 5.7) для Telegram Mini App. Звучит здорово, но вот в чём подвох: когда в одном репозитории живут две экосистемы с разными правилами игры, управлять ими становится искусством. **Развитие** Первой проблемой, которая выпрыгнула из неоткуда, была **забывчивость переменных окружения**. В Python проект использует `pydantic-settings` для конфигурации, но выяснилось, что эта библиотека не экспортирует значения автоматически в `os.environ`. Результат? Модульный код, читавший переменные прямо из окружения, падал с загадочными ошибками. Пришлось документировать эту ловушку в ERROR_JOURNAL.md — живом архиве подводных камней проекта, где уже скопилось десять таких «моментов истины». Далее встал вопрос архитектуры. Backend требовал **координатора** — центрального паттерна, который бы оркестрировал взаимодействие между агентами и фронтенд-запросами. На бумаге это выглядело идеально, но в коде его не было. Это создавало технический долг, который блокировал Phase 2 разработки. Пришлось вводить **phase-gate валидацию** — автоматическую проверку, которая гарантирует, что прежде чем переходить к следующей фазе, все артефакты предыдущей действительно на месте. В процессе появилась и проблема с **миграциями базы данных**. SQLite с WAL-режимом требовал аккуратности: после того как junior-агент создавал файл миграции, она не всегда применялась к самой БД. Пришлось вводить обязательный чек: запуск `migrate.py`, проверка таблиц через прямой SQL-запрос, документирование статуса. Без этого можно часами отлавливать фантомные ошибки импорта. **Познавательный блок** Интересный факт: монорепо — это не просто удобство, это *культурный артефакт* команды разработки. Google использует одно гигантское хранилище для всего кода (более миллиарда строк!), потому что это упрощает синхронизацию и рефакторинг. Но цена высока: нужны инструменты (Bazel), дисциплина и чёткие протоколы. Для нашего voice-agent это значит: не просто писать код, а писать его так, чтобы Python-part и Next.js-part *доверяли друг другу*. **Итог** В итоге сложилась простая истина: монорепо работает только если есть **система проверок**. ERROR_JOURNAL.md превратился не просто в логирование ошибок, а в живой артефакт культуры команды. Phase-gate валидация стала гарантией, что при параллельной работе нескольких агентов архитектура не съезжает в стороны. А обязательная проверка миграций — это не занудство, а спасение от трёх часов ночного отлавливания, почему таблица не там, где ей быть. Главный урок: в монорепо важна не столько архитектура, сколько **честность системы**. Чем раньше вы перейдёте от надежды на память к автоматическим проверкам, тем спокойнее спать будете. Почему Python и Java не могут дружить? У них разные dependency trees 😄
Umami Analytics: как я сделал админ-панель data-driven
# Самостоятельная аналитика: как я превратил borisovai-admin в data-driven продукт Несколько месяцев назад передо мной встала типичная для любого владельца проекта проблема: я совершенно не видел, кто и как использует мою админ-панель **borisovai-admin**. Google Analytics казался избыточным (и страшным с точки зрения приватности), а простой счётчик посещений — примитивным. Нужно было что-то лёгкое, приватное и полностью под своим контролем. Выбор пал на **Umami Analytics** — открытую веб-аналитику, которая уважает приватность пользователей, не использует cookies и полностью GDPR-compliant. Главное же — её можно развернуть самостоятельно, прямо в своей инфраструктуре. ## Четыре этапа внедрения **Первый шаг — упростить развёртывание.** Стандартная Umami требует двух контейнеров (приложение + PostgreSQL), но для небольшого проекта это избыточно. Я нашёл fork **maxime-j/umami-sqlite**, который использует SQLite — файловую БД в одном контейнере. Экономия памяти была существенной: вместо ~300 MB получил ~100 MB. Затем написал скрипт **install-umami.sh** из семи шагов, который может быть запущен много раз без побочных эффектов (идемпотентный — именно это было важно для автоматизации). **Второй этап — автоматизировать через CI/CD.** Создал два job'а в пайплайне: один автоматически ставит Docker (если его нет), второй — развёртывает саму Umami. Добавил health check, чтобы пайплайн не переходил к следующему шагу, пока контейнер не будет готов. Инкрементальный деплой через **deploy-umami.sh** позволяет обновлять конфигурацию без перезагрузки приложения. **Третий этап — дать пользователям интерфейс.** Создал страницу **analytics.html**, где каждый новый сервис может получить код для интеграции отслеживания. Плюс добавил API endpoint `GET /api/analytics/status` для проверки, всё ли работает. Async-скрипт Umami весит всего ~2 KB и не блокирует рендеринг страницы — вот это я ценю. **Четвёртый этап — документировать.** Написал **AGENT_ANALYTICS.md** с инструкциями для будущих разработчиков, обновил главный **CLAUDE.md** таблицей всех сервисов. ## Что интересного я узнал Оказывается, боль большинства разработчиков с традиционной аналитикой — это не функциональность, а приватность. Umami решает это элегантно: скрипт отправляет только агрегированные данные (сессии, страницы, источники трафика) без ID пользователей и истории кликов. А главное — нет необходимости в **consent banner**, который все равно раздражает пользователей. Порт **3001** внутри контейнера пробросил через **Traefik** на HTTPS-домены `analytics.borisovai.ru` и `analytics.borisovai.tech`. Вообще, это я оценил: такая простота развёртывания чуть ли не впервые в моём опыте с self-hosted решениями. Встроенная авторизация в самой Umami (не потребовался дополнительный Authelia) — и это экономия на инфраструктуре. Один лайфхак: чтобы скрипт аналитики не блокировался AdBlock, назвал его `stats` вместо стандартного `umami` — простой способ обойти базовые фильтры. ## Итог Теперь **borisovai-admin** наконец-то видит себя со стороны. Я получил данные о том, какие страницы реально используют люди, откуда они приходят и сколько времени длятся сессии. Всё это — на своём сервере, без третьих лиц и без чувства вины перед пользователями. Следующий шаг — подключить аналитику ко всем остальным сервисам проекта. Это уже не задача месяца, а скорее вопрос пары часов на каждый сервис. Учимся: иногда лучший инструмент — это не самый популярный, а самый честный. 😄
Молчаливый API: когда успех — это просто пустота
# Когда API молчит: охота на призрак в системе обработки команд Это была обычная воскресенье в проекте **ai-agents**. Пользователь Coriollon отправил простую команду через Telegram: "Создавай". Три слова. Невинные на вид. Но система ответила молчанием — и началась охота на баг, которая заняла почти семь минут и три попытки переподключения. ## Что мы видим в логах Сначала всё выглядит нормально. Запрос приходит в 12:23:58. Система маршрутизирует его на **Claude API** с моделью Sonnet. Промпт имеет 5344 символа — немалый объём контекста. Первый запрос уходит в API и... здесь начинается интересное. API отвечает за 26 секунд. Кажется, успешно: `is_error: False`, `num_turns: 2`, даже token usage выглядит логичным. Но вот `result: ''` — пустой результат. Система ловит эту аномалию и логирует `cli_empty_response`. Мой первый инстинкт: "Может, сетевой глюк?" Система делает то же самое — ждёт 5 секунд и повторяет запрос. Вторая попытка в 12:24:31. История повторяется: успех по метрикам, но снова пустой ответ. ## Третий раз — не удача К третьей попытке я уже понял, что это не случайный сетевой перебой. Система работает корректно, API возвращает `success: true`, токены учитываются (даже видны попадания в кэш: `cache_read_input_tokens: 47520`). Но результат, ради которого всё затевалось, так и не приходит. Вот здесь кроется классическая ловушка в работе с LLM API: **успешный HTTP-ответ не гарантирует наличие полезной нагрузки**. API может успешно обработать запрос, но вернуть пустое поле `result` — это может означать, что модель вернула только служебные данные (вроде использованных токенов) без фактического содержимого. Финальная попытка заканчивается в 12:25:26. Три запроса, три молчания, общее время ожидания — почти семь минут. Система логирует финальную ошибку: `message_handler_error: CLI returned empty response`. ## Чему это учит Когда вы работаете с внешними API, особенно с такими мощными, как Claude, недостаточно проверять только HTTP-статус. Нужно валидировать **содержимое ответа**. В данном случае система сделала ровно это — поймала пустой результат и попыталась восстановиться через retry-логику с экспоненциальной задержкой (5, 10 секунд). Но вот что интересно: кэшированные токены (видны в каждом логе) говорят, что контекст был успешно закэширован. Это означает, что на второй и третий запрос система платила дешевле — 0.047 и 0.037 USD вместо 0.081 на первый запрос. Автоматическое кэширование контекста в Claude API — это фишка, которая спасает в ситуациях вроде этой. Корень проблемы остался в логах как загадка: был ли это timeout на стороне модели, недопонимание в структуре запроса или что-то ещё — сказать сложно. Но система сработала как надо: зафиксировала проблему, задокументировала все попытки, сохранила данные сессии для постмортема. Lesson learned: в системах обработки команд от пользователей нужна не только retry-логика, но и мониторинг пустых ответов. И да, Telegram-боты любят такие фокусы. 😄 **API успешно вернул ошибку об ошибке успеха — вот это я называю отличной синхронизацией!**
Unit-тесты зелёные, а бот не работает: гонка условий в Telegram
# Когда unit-тесты лгут: как я запустил систему доступа в реальном Telegram **bot-social-publisher** выглядел как отличный проект для спринта. Полнофункциональный Telegram-бот с командами, памятью, интеграциями. Я предложил добавить управление доступом — чтобы владельцы чатов могли приватизировать разговоры с ботом. Звучит просто: только авторизованные пользователи видят ответы. Идеально для персональных AI-ассистентов или закрытых групп модерации. Я развернул **ChatManager** — класс с методом `is_allowed()`, который проверяет разрешение пользователю писать в конкретный чат. Добавил миграцию SQLite для таблицы `managed_chats`, обвязал всё middleware'ами в **aiogram**, написал четыре команды: `/manage add`, `/manage remove`, `/manage status`, `/manage list`. Unit-тесты прошли с ликованием. **pytest** выдал зелёный статус. Документация? Позже, мол. Потом наступил момент истины. Запустил бота локально через `python telegram_main.py`. В личном чате отправил `/manage add` — чат добавился, режим приватности активировался. Отправил обычное сообщение — ответ пришёл. Открыл второй аккаунт, отправил то же — бот молчит. Отлично, система контроля работает! Но не совсем. **Первая проблема** вскрылась при быстрых командах подряд. **aiogram** работает асинхронно, как и **aiosqlite**. Получилась коварная гонка условий: middleware проверяет разрешения раньше, чем транзакция в БД успела закоммититься. Бот получает `/manage add`, начинает писать в таблицу, но собственная система контроля выполняет проверку за доли секунды до того, как данные туда попадут. На unit-тестах такое не видно — там всё выполняется последовательно. **Вторая проблема** — SQLite и одновременные асинхронные обработчики. Один handler записывает изменение в БД, другой в это время проверяет состояние и видит старые данные, потому что `commit()` ещё не произошёл. Мне помогли явные транзакции и аккуратная расстановка `await`'ов — гарантия того, что каждая операция завершится перед следующей. Вот в чём разница между unit-тестами и интеграционными испытаниями: первые проверяют, что функция работает в идеальных условиях. Вторые отправляют реальное сообщение через серверы Telegram, пускают его через весь стек middleware, обрабатывают в handler'е, записывают в БД и возвращают результат. Тесты говорили: всё работает. Реальность показала: медленно и с условиями. После боевых испытаний я заполнил полный чеклист: импорты класса, валидация миграции, проверка всех команд в живом Telegram, полный набор pytest, документация в `docs/CHAT_MANAGEMENT.md` с примерами архитектуры. Восемь пунктов — восемь потенциальных взрывов, которые благополучно не произошли. Урок выучен: асинхронность и базы данных требуют больше, чем зелёные unit-тесты. Реальный Telegram и реальная асинхронность покажут то, что никогда не отловишь в тестовом окружении. 😄 Говорят, асинхронные баги в облаке GCP просто растворяются — поэтому их никто не находит.
Когда unit-тесты лгут: боевые испытания Telegram-бота
# Telegram-бот на боевых испытаниях: когда unit-тесты не подстраховывают Проект **bot-social-publisher** начинался просто. Полнофункциональный Telegram-бот с памятью, командами, интеграциями. Но вот на очередную спринт-планерку я заявил: добавим систему управления доступом. Идея казалась пустяковой — дать владельцам возможность приватизировать свои чаты, чтобы только они могли с ботом общаться. Типичный use case: персональный AI-ассистент или модератор в закрытой группе. Теория была прекрасна. Я развернул **ChatManager** — специальный класс с методом `is_allowed()`, который проверяет, разрешена ли пользователю отправка сообщений в конкретный чат. Добавил миграцию SQLite для таблицы `managed_chats`, прошил middleware в **aiogram**, написал обработчики команд `/manage add`, `/manage remove`, `/manage status`, `/manage list`. Unit-тесты прошли с зелёным светом — `pytest` даже не чихнул. Документация пока отложена, но это же детали! Потом наступил момент истины. Запустил бота локально через `python telegram_main.py`, переключился в личный чат и отправил первую `/manage add`. Бот записал ID чата, переключился в режим приватности. Нормально! Попробовал отправить обычное сообщение — ответ пришёл. Открыл чат со своего второго аккаунта, отправил то же самое — тишина. Бот ничего не ответил. Перфект, middleware работает. Но не всё было так гладко. Первая проблема вылезла при быстрых командах подряд. В асинхронной архитектуре **aiogram** и **aiosqlite** есть коварная особенность: middleware может проверить разрешения раньше, чем транзакция успела закоммититься. Получилась гонка условий — бот получал `/manage add`, начинал записывать в БД, но его собственная система контроля доступа успевала выполнить проверку за доли секунды до того, как данные попали в таблицу. Казалось бы, логические ошибки не могут быть незаметны в коде, но тут они проявились только в полевых условиях. Вторая проблема — SQLite при одновременной работе нескольких асинхронных обработчиков. Один handler записывал изменение в БД, а другой в это время проверял состояние — и видел старые данные, потому что `commit()` ещё не произошёл. Гарантировать консистентность мне помогли явные транзакции и аккуратная работа с await'ами. Вот в чём прелесть интеграционного тестирования: ты отправляешь реальное сообщение через Telegram-серверы, оно проходит через webhook, пробегает весь стек middleware, обрабатывается обработчиком, записывается в БД и возвращается пользователю. Unit-тесты проверяют логику функции. Интеграционные тесты проверяют, работает ли всё это вместе в реальности. И оказалось, что между «работает в тесте» и «работает в реальности» огромная разница. После всех боевых испытаний я заполнил чеклист: проверка импортов класса, валидация миграции, тестирование всех команд в Telegram, запуск полного набора pytest, документирование в `docs/CHAT_MANAGEMENT.md` с примерами и описанием архитектуры. Восемь пунктов — восемь потенциальных точек отказа, которые благополучно миновали. Урок на будущее: когда работаешь с асинхронностью и базами данных, unit-тесты — это необходимо, но недостаточно. Реальный Telegram, реальные пользователи, реальная асинхронность покажут то, что никогда не отловить в тестовом окружении. 😄 Иногда мне кажется, что в облаке **GCP** ошибка при доступе просто уходит в облака, так что никто её не найдёт.
Когда unit-тесты зелёные, а бот падает в продакшене
# Проверяем Telegram-бота в боевых условиях: когда unit-тесты врут Любой разработчик знает эту ситуацию: твой код прошёл все тесты в PyTest, green lights светят, CI/CD улыбается. Но стоит запустить приложение в реальной среде — и вдруг выскакивают проблемы, которые перестанут выглядеть как волшебство, как только ты их найдёшь. Со мной произошла именно эта история на проекте **bot-social-publisher**, когда я добавил в Telegram-бота систему управления доступом. ## Задача казалась элементарной Надо было реализовать для бота фишку с приватными чатами. Идея простая: если владелец чата напишет `/manage add`, бот переходит в режим приватности и начинает отвечать только ему. Команда `/manage remove` открывает доступ всем обратно. Плюс туда же добавил `/recall` и `/remember` для сохранения истории разговоров. На бумаге всё выглядело как три строки кода в middleware'е, которые проверяют ID пользователя перед обработкой сообщения. Я написал unit-тесты, всё прошло. Но реальный Telegram — совсем другой зверь. ## Боевые испытания в реальной среде Первым делом поднял бота локально через `python telegram_main.py` и начал его "пилить" из реального Telegram аккаунта. Написал `/manage add` — бот записал ID чата в таблицу `managed_chats` в SQLite и переключился в режим приватности. Проверил middleware `permission_check.py` — всё срабатывает корректно, обработка заблокирована для чужих. Хорошо. Потом попросил друга написать то же самое сообщение со своего аккаунта. Ожидал — ничего не случится. И действительно, бот промолчал. Отлично, система работает как надо. Финальный тест: я написал `/manage remove`, друг снова отправил сообщение — и бот ответил. Приватность отключена, доступ восстановлен. Казалось бы, победа. Но потом обнаружилась подвох. ## Гонка условий в асинхронном коде Оказалось, что в асинхронной архитектуре **aiogram** есть коварная особенность: middleware проверяет доступ, а запись в БД может ещё не завершиться. Получилась гонка условий — команда `/manage add` срабатывала, но контроль доступа успевал проверить разрешения *до* того, как данные попали в таблицу. Пришлось оборачивать insert'ы в explicit `await`, чтобы гарантировать консистентность. Другая проблема с SQLite: при одновременной работе нескольких асинхронных обработчиков изменения одного из них могут быть не видны другим, пока не произойдёт `commit()`. Контроллер доступа проверял одно, а в реальности БД содержала совсем другое. Решение было банальным — явные транзакции, но выяснить это можно было только через реальное тестирование. ## Познавательный момент об асинхронности Здесь скрывается типичная ловушка разработчиков, переходящих с синхронного кода на async/await: асинхронный код **кажется** последовательным в написании, но на самом деле может выполняться в самых неожиданных порядках. Когда ты пишешь `await db.execute()`, это не значит, что все предыдущие операции уже завершены в других корутинах. Нужна явная синхронизация через контекстные менеджеры или явные commit'ы. ## Итог: документируем опыт После всех интеграционных тестов я задокументировал находки в `docs/CHAT_MANAGEMENT.md`, добавил примеры использования в README.md и описал полную архитектуру ChatManager'а. Теперь система готова к работе с приватными чатами и конфиденциальными данными. Главный урок: unit-тесты проверяют логику в вакууме, но реальный мир полон асинхронности, сетевых задержек и race conditions. Никакой PyTest не найдёт то, что видно только в продакшене. Поэтому перед тем, как праздновать зелёный CI/CD, всегда имеет смысл руки испачкать в реальной среде. 😄 Что говорит разработчик после запуска асинхронного кода? «У меня было семь ошибок, теперь их четырнадцать, но они более интересные».
Четыре инструмента вместо двух: как мы освободили AI-агентов от ограничений
# От изоляции к открытости: как мы расширили доступ к файловой системе в AI-агентах Работал я над проектом **ai-agents** — платформой для создания интеллектуальных помощников на Python. И вот в какой-то момент нас настигла настоящая боль роста: агенты работали в тесной клетке ограничений. ## Проблема: узкие границы доступа Представь ситуацию. У нас была виртуальная файловая система — и звучит круто, пока не начнёшь с ней работать. Агенты могли читать только три папки: `plugins/`, `data/`, `config/`. Писать вообще не могли. Нужно было создать конфиг? Нет. Сохранить результат работы? Нет. Отредактировать существующий файл? Снова нет. Это было словно программировать с одной рукой, привязанной за спину. Функциональность `file_read` и `directory_list` — хорошо, но недостаточно. Проект рос, требования расширялись, а система стояла на месте. ## Решение: четыре инструмента вместо двух Первым делом я понял, что нужно идти не путём костылей, а переписать модуль **filesystem.py** целиком. Вместо двух полуслепых инструментов создал четыре полнофункциональных: - **`file_read`** — теперь читает что угодно в проекте, до 200 килобайт - **`file_write`** — создаёт и перезаписывает файлы, автоматически создаёт директории - **`file_edit`** — тонкая работа: находит точную подстроку через find-and-replace и заменяет - **`directory_list`** — гибкий листинг: поддерживает glob-паттерны и рекурсию Но тут появилась вторая проблема: как дать свободу, но не потерять контроль? ## Безопасность: ограничения, которые действительно работают Всё звучит опасно, пока не посмотришь на механизм защиты. Я добавил несколько слоёв: Все пути привязаны к корню проекта через `Path.cwd()`. Выбраться наружу невозможно — система просто не позволит обратиться к файлам выше по дереву директорий. Плюс чёрный список: система блокирует доступ к `.env`, ключам, секретам, паролям — ко всему, что может быть опасно. А для дополнительной уверенности я добавил проверку path traversal через `resolve()` и `relative_to()`. Получилась архитектура, где агент может свободно работать внутри своей песочницы, но не может ей повредить. ## Интересный момент: почему это важно Знаешь, в истории компьютерной безопасности есть забавный парадокс. Чем больше ты запрещаешь, тем больше люди ищут обходные пути. А чем правильнее ты даёшь разрешения — с умными ограничениями — тем спокойнее всем. Unix-философия в действии: дай инструменту ровно столько мощи, сколько нужно, но убедись, что он не сможет что-то сломать. ## Итого Переписал модуль, обновил константы в **constants.py**, экспортировал новые классы в **\_\_init\_\_.py**, подключил всё в **core.py** и **handlers.py**. Проверил сборку — зелёная лампочка. Теперь агенты могут полноценно работать с проектом, не боясь случайно удалить что-то важное. Дальше планировали тестировать на реальных сценариях: создание логов, сохранение состояния, динамическая генерация конфигов. А пока что у нас есть полнофункциональная и безопасная система для работы с файлами. Мораль истории: не выбирай между свободой и безопасностью — выбирай правильную архитектуру, которая обеспечивает оба.
ChatManager: как научить бота помнить, где ему работать
# Как научить AI-бота помнить свои чаты: история ChatManager Задача стояла простая на словах, но коварная на деле. У AI-бота в проекте **voice-agent** началась проблема с идентичностью: он рос, его добавляли в новые чаты, но вот беда — он не различал, в каких группах он вообще должен работать. Представь: бот оказывается в сотне чатов, а слушаться команд должен только в тех, которые явно добавил его хозяин. Без этого механизма вся система на мине. **Первым делом разобрали задачу по кирпичикам.** Нужна была полноценная система управления чатами: бот должен помнить, какие чаты ему доверены, проверять права пользователя перед каждой командой и предоставлять простые способы добавлять/удалять чаты из управляемых. Звучит как обычная CRUD-операция, но в контексте асинхронного Telegram-бота это становится интереснее. Решение разделили на пять логических контрольных точек. Начали с **ChatManager** — специального класса в `src/auth/`, который ведал бы всеми чатами. Важное решение: использовали уже имеющийся в проекте **structlog** для логирования вместо того, чтобы добавлять ещё одну зависимость. И, что критично для асинхронного бота, выбрали **aiosqlite** — асинхронный драйвер для SQLite. Почему это важно? Потому что обычный SQLite работает синхронно и может заблокировать весь бот при обращении к БД. aiosqlite оборачивает операции в `asyncio` — и вот уже БД не стопорит главный цикл обработки сообщений. **Дальше пошли миграции и middleware.** Создали таблицу `managed_chats` с информацией о чатах, их типах и владельцах. Затем встроили в pipeline Telegram-хэндлеров специальный middleware для проверки прав — перед каждой командой система проверяет, имеет ли пользователь доступ к этому чату. Если нет — молчит или вежливо отказывает. Команды управления (`/manage add`, `/manage remove`, `/manage list`) сделали простыми и понятными. Напишешь в личку боту `/manage add` — и чат добавляется в управляемые. Никакого магического угадывания, всё явно. **Интересный момент про асинхронные БД.** Когда разработчики впервые натыкаются на проблему "бот зависает при запросе к БД", они часто виноваты в синхронных операциях с базой. aiosqlite решает это элегантно: минимум кода, максимум производительности. Это один из тех инсайтов, которые приходят дорого, но в долгосрочной перспективе спасают душу. **В итоге получилось мощно.** Бот теперь точно знает, кто его хозяин в каждом чате, и не поддаётся на трюки неуполномоченных пользователей. Архитектура масштабируется — можно добавить роли, разные уровни доступа, историю изменений. Всё работает асинхронно и не тормозит. Дальше очередь интеграционных тестов и production. 😄 **Совет дня:** всегда создавай миграции БД отдельно от логики — так проще откатывать и тестировать. И да, GCP и кот похожи тем, что оба делают только то, что хотят, и игнорируют твои инструкции.
SQLite спасает день: масштабируемая БД вместо хаоса в памяти
# SQLite вместо памяти: как я спас Telegram-ботов от хаоса Проект `bot-social-publisher` взлетал буквально на глазах. Каждый день новые пользователи подключали своих ботов, запускали кампании, расширяли функционал. Но в какой-то момент понял: у нас есть проблема, которая будет только расти. Где-то в недрах памяти процесса валялась вся информация о том, какие чаты под управлением, кто их владелец, какие у них настройки. Приватный чат? Группа? Канал? Всё это было либо в переменных, либо в логах, либо вообще только в голове. Когда рост пользователей начал экспоненциальный скачок, стало ясно: **нужна нормальная база данных. Правильная, масштабируемая, без требований на отдельный сервер**. **Первым делом посмотрел на то, что уже есть.** В проекте уже была собственная SQLite база в `data/agent.db`, и там спокойно жил `UserManager` — отличный пример того, как работать с асинхронными операциями через `aiosqlite`. Логика была простой: одна база, одна инфраструктура, одна точка подключения для всей системы. Так почему бы не применить ту же философию к чатам? Архитектурное решение созревало быстро. Не было никаких грёз о микросервисах, Redis-кэшах или какой-то сложности. Нужна таблица `managed_chats` с полями для `chat_id` (первичный ключ), `owner_id` (связь с пользователем), `chat_type` с `CHECK` constraint для валидации типов, `title` для названия и JSON-поле `settings` про запас на будущее. **Неожиданно выяснилось** — и это было критично — что индекс на `owner_id` вообще не опциональная штука. Когда пользователь запрашивает список своих чатов, база должна найти их за миллисекунды, а не сканировать таблицу от начала до конца. SQLite часто недооценивают в стартапах, думают, что это игрушка для тестирования. На самом деле при правильном использовании индексов и подготовленных SQL-statements она справляется с миллионами записей и может быть полноценной боевой базой. Реализацию сделал по образцу `UserManager`: создал `ChatManager` с асинхронными методами `add_chat()`, `is_managed()`, `get_owner()`. Каждый запрос параметризован — никаких SQL-injection уязвимостей. Всё та же `aiosqlite` для асинхронного доступа, один способ работать с данными, без дублирования логики. Красивый момент получился благодаря `INSERT OR REPLACE` — если чат переиндексируется с новыми настройками, старая запись просто заменяется. Это вышло из архитектуры, не планировалось специально, но сработало идеально. **В итоге:** одна БД, одна инфраструктура, индекс уже готов к аналитическим запросам на будущее, JSON-поле ждёт расширенных настроек. Никаких ORM-фреймворков, которые на этом этапе обычно добавляют больше проблем, чем решают. Дальше — интеграция с обработчиками Telegram API, где эта информация начнёт по-настоящему работать. Но то уже следующая история. 😄 Разработчик говорит: «Я знаю SQLite». HR: «На каком уровне?». Разработчик: «На уровне, когда она работает, и я этому не верю».
SQLite вместо памяти: как обуздать рост Telegram-ботов
# Управляем Telegram-чаты как должно: от памяти к базе данных Проект `bot-social-publisher` рос как на дрожжах. Каждый день новые пользователи подключали своих ботов, запускали кампании, развивали функционал. Но вот беда: где-то в памяти процесса валялась информация о том, какие чаты под управлением, кто их владелец, какие у них настройки. Приватный чат? Группа? Канал? Всё это было либо в переменных, либо где-то в логах. Нужна была система. Правильная, масштабируемая, не требующая отдельного сервера для базы данных. **Первым делом** посмотрел, как устроен текущий стек. В проекте уже была собственная SQLite база в `data/agent.db`, и там жил `UserManager` — отличный пример того, как правильно работать с асинхронными операциями через `aiosqlite`. Значит, нужно просто добавить новую таблицу `managed_chats` в ту же базу, скопировать философию управления пользователями и запустить в production. **Архитектурное решение** было ясно с самого начала: никаких микросервисов, никаких Redis-кэшей для этого этапа. Нужна таблица с полями для `chat_id` (первичный ключ), `owner_id` (связь с пользователем), `chat_type` (с проверкой через `CHECK` constraint — только валидные типы), `title` и JSON-поле `settings` на будущее. Неожиданно выяснилось, что индекс на `owner_id` — это не опциональная штука. Когда пользователь запрашивает список своих чатов, база должна найти их быстро, а не сканировать всю таблицу от начала до конца. SQLite часто недооценивают в стартапах, думают, что это игрушка для тестирования. На самом деле при правильном использовании индексов и подготовленных statements она справляется с миллионами записей и может быть полноценной боевой базой. **Реализацию** сделал по образцу `UserManager`: создал `ChatManager` с асинхронными методами `add_chat()`, `is_managed()`, `get_owner()`. Каждый запрос параметризован — никаких SQL injection уязвимостей. Используется всё та же `aiosqlite` для асинхронного доступа, одна точка подключения для всей системы, без дублирования логики. Красивый момент получился благодаря `INSERT OR REPLACE` — если чат переиндексируется с новыми настройками, старая запись просто заменяется. Это вышло из архитектуры, а не планировалось специально. В итоге: одна БД, одна инфраструктура, масштабируемая схема. Когда понадобятся сложные аналитические запросы — индекс уже есть. Когда захотим добавить права доступа или расширенные настройки чата — JSON-поле ждёт. Никаких фреймворков ORM, которые обычно добавляют больше проблем, чем решают на этом этапе. Дальше — интеграция с обработчиками Telegram API, где эта информация будет реально работать. Но то уже следующая история. 😄 Разработчик: «Я знаю ArgoCD». HR: «На каком уровне?». Разработчик: «На уровне Stack Overflow».