Разработка: bot-social-publisher
Хроника разработки проекта bot-social-publisher. Основные направления: новые функции, исправления багов, исследования. Всего 34 записей в потоке. Последние темы: Почему картинки в заметках исчезали — и как я это чинил; Когда батч-норм ломает миксчур экспертов на CIFAR-100; Когда дефолт становится врагом: история из bot-social-publisher.
Как мы защитили trend-analysis: система аутентификации, которая работает
Когда trend-analysis начал расти и появились первые пользователи с реальными данными, стало ясно: больше нельзя оставлять проект без охраны. Сегодня это звучит очевидно, но когда проект рождается как хобби-эксперимент на Claude API, о безопасности думаешь в последнюю очередь.
Задача встала конкретная: построить систему аутентификации, которая не замедлит анализ трендов, будет действительно надёжной и при этом не превратится в монстра сложности. Плюс нужно было всё это интегрировать в цепочку с Claude API, чтобы каждый запрос знал, кто его отправил.
Первым делом я создал ветку feat/auth-system и начал с главного вопроса: JWT-токены или сессии? На бумаге JWT выглядит идеально — stateless, не требует обращений к БД на каждый запрос, легко масштабируется. Но JWT имеет проблему: невозможно мгновенно заблокировать токен, если что-то пошло не так. Я выбрал компромисс: сессии с HTTP-only cookies и постоянная валидация через Claude API логирование. Это скучнее, чем блеск JWT, но безопаснее и практичнее.
Неожиданно выяснилось, что самая коварная часть — не сама авторизация, а правильная обработка истечения доступа. Пользователь кликает кнопку, а его сессия уже протухла. Мы реализовали двухуровневую систему: короткоживущий access-токен для текущей работы и долгоживущий refresh-токен для восстановления доступа без повторной авторизации. На первый взгляд это выглядит усложнением, но спасло нас от тысячи потенциальных багов с разъёхавшимся состоянием.
Интересный момент, о котором забывают: timing-атаки. Если проверять пароль просто посимвольным сравнением строк, хакер может подбирать буквы по времени выполнения функции. Я использовал werkzeug.security для хеширования паролей и функции постоянного времени для всех критичных проверок. Это не добавляет сложности в коде, но делает систему несоизмеримо более защищённой.
В результате получилась система, которая выдаёт пользователю пару токенов при входе, проверяет access-token за миллисекунды, автоматически обновляет доступ через refresh и логирует все попытки входа прямо в trend-analysis. База построена правильно, и теперь наша платформа защищена.
Дальше планируем двухфакторную аутентификацию и OAuth для социальных сетей, но это уже совсем другая история.
😄 Знаете, почему JWT-токены никогда не приходят на вечеринки? Потому что они всегда истекают в самый неподходящий момент!
Когда SQLite на Windows встречает Linux: история одного деплоя
Проект ai-agents-admin-agent был почти готов к запуску на сервере. Восемь n8n-воркфлоу, собирающих и обрабатывающих данные, уже прошли тестирование локально. На машине разработчика всё работало идеально. Но только до того момента, когда мы выложили их на Linux-сервер.
Первый боевой запуск воркфлоу завершился криком ошибки: no such table: users. Логи были красноречивы — все SQLite-ноды искали базу данных по пути C:\projects\ai-agents\admin-agent\database\admin_agent.db. Локальный Windows-путь. На сервере такого вообще не существовало.
Первый инстинкт: просто заменить пути
Звучит логично, но дьявол, как всегда, в деталях. Я начал рассматривать варианты.
Вариант первый — использовать относительный путь типа ./data/admin_agent.db. Звучит мобильно и красиво, но это ловушка для новичков. Относительный путь разрешается от текущей рабочей директории процесса n8n. А откуда запущен n8n? Из Docker-контейнера? Из systemd? Из скрипта? Результат абсолютно непредсказуем.
Вариант второй — абсолютный путь для каждого окружения. Надёжнее, но требует подготовки на сервере: скопировать схему БД, запустить миграции. Более сложно, зато предсказуемо.
Я выбрал комбинированный подход.
Как мы это реализовали
Локально в docker-compose.yml добавил переменную окружения DATABASE_PATH=/data/admin_agent.db — чтобы разработка была удобной и воспроизводимой. Затем создал развёртывающий скрипт, который при деплое проходит по всем восьми воркфлоу и заменяет выражение $env.DATABASE_PATH на реальный абсолютный путь /var/lib/n8n/data/admin_agent.db.
Но первое время я попытался обойтись выражениями n8n. Логика казалась неубиваемой: задаёшь переменную в окружении, ссылаешься на неё в воркфлоу, всё просто. На практике выяснилось, что в n8n v2.4.5 таск-раннер не передавал переменные окружения в SQLite-ноду так, как ожидалось. Выражение хранилось в конфигурации, но при выполнении всё равно искал исходный Windows-путь.
Пришлось идти в лоб — строковые замены при деплое. Развёртывающий скрипт deploy/deploy-n8n.js перехватывает JSON каждого воркфлоу и подставляет правильный путь перед загрузкой.
Ещё одна подводная скала: n8n хранит две версии каждого воркфлоу — stored (в базе данных) и active (загруженная в памяти). Когда вы обновляете конфигурацию через API, обновляется только stored-версия. Active может остаться со старыми параметрами. Это сделано для того, чтобы текущие выполнения не прерывались, но создаёт рассинхронизацию между кодом и поведением. Решение: явная деактивация и активация воркфлоу после обновления.
Добавили в процесс и инициализацию БД: скрипт SSH копирует на сервер миграции (schema.sql, seed_questions.sql) и выполняет их через n8n API перед активацией воркфлоу. В будущем, когда потребуется изменить схему (например, добавить колонку phone в таблицу users), достаточно добавить миграцию — без пересоздания всей БД.
Итог
Теперь деплой сводится к одной команде: node deploy/deploy-n8n.js --env .env.deploy. Воркфлоу создаются с правильными путями, база инициализируется корректно, всё работает.
Главный урок: не полагайся на относительные пути в Docker-контейнерах и на runtime-выражения в критических параметрах. Лучше заранее знать, где именно будет жить твоё приложение, и подставить правильный путь при развёртывании. Это скучно, но предсказуемо.
GitHub — единственная технология, где «это работает на моей машине» считается достаточной документацией. 😄
SQLite между Windows и Linux: как не потерять данные при деплое
Проект ai-agents-bot-social-publisher был почти готов к боевому выпуску. Восемь n8n-воркфлоу, которые собирают посты из социальных сетей и распределяют их по категориям, прошли локальное тестирование на отлично. Но тут наступил момент истины — первый деплой на Linux-сервер.
Логи завалили ошибкой: no such table: users. Все SQLite-ноды в воркфлоу отчаянно искали базу данных по пути C:\projects\ai-agents\admin-agent\database\admin_agent.db. Windows-путь. На Linux-сервере, разумеется, ничего такого не было.
Красивое решение, которое не сработало
Первый инстинкт был логичный: использовать переменные окружения и выражения n8n. Добавили DATABASE_PATH=/data/admin_agent.db в docker-compose.yml, развернули воркфлоу с выражением $env.DATABASE_PATH в конфиге SQLite-ноды, нажали на кнопку деплоя и… всё равно падало. Выяснилось, что в n8n v2.4.5 таск-раннер не передавал переменные окружения в SQLite-ноду так, как ожидалось. Выражение красиво хранилось в конфигурации, но при выполнении система всё равно искала исходный Windows-путь.
Пришлось отказаться от элегантности в пользу надёжности.
Боевой способ: замены при развёртывании
Решение оказалось неожиданно простым — string replacement при деплое. Разработал скрипт deploy/deploy-n8n.js, который перехватывает JSON каждого воркфлоу перед загрузкой на сервер и заменяет все $env.DATABASE_PATH на реальный абсолютный путь /var/lib/n8n/data/admin_agent.db. Скучно? Да. Предсказуемо? Абсолютно.
Но тут обнаружилась ещё одна подводная скала: n8n хранит две версии каждого воркфлоу. Stored-версия живёт в базе данных, active-версия загружена в памяти и выполняется. Когда обновляешь воркфлоу через API, обновляется только хранилище. Active может остаться со старыми параметрами. Это сделано специально, чтобы текущие выполнения не прерывались, но создаёт рассинхронизацию между кодом и поведением. Решение: после обновления конфига явно деактивировать и активировать воркфлоу.
Инициализация базы: миграции вместо пересоздания
Добавили инициализацию SQLite. Скрипт SSH копирует на сервер SQL-миграции (schema.sql, seed_questions.sql) и выполняет их через n8n API перед активацией воркфлоу. Такой подход кажется лишним, но спасает в будущем — когда потребуется добавить колонку phone в таблицу users, просто добавляешь новую миграцию, без полного пересоздания БД.
Теперь весь деплой сводится к одной команде: node deploy/deploy-n8n.js --env .env.deploy. Воркфлоу создаются с правильными путями, база инициализируется корректно, всё работает.
Главный урок: не полагайся на относительные пути в Docker-контейнерах и на runtime-выражения в критических параметрах конфигурации. Лучше заранее знать точное место, где будет жить приложение, и подставить правильный путь при развёртывании.
«Ну что, SQLite, теперь-то ты найдёшь свою базу?» — спросил я у логов. SQLite ответил тишиной успеха. 😄
Деплой SQLite: когда переменные окружения предают в самый ответственный момент
Проект ai-agents-bot-social-publisher стоял на пороге боевого выпуска. Восемь n8n-воркфлоу, которые собирают посты из социальных сетей и сортируют их по категориям, прошли все локальные тесты с честью. Команда была уверена — завтра деплоим на Linux-сервер, и всё заживёт.
Реальность оказалась жестче. Первая же волна логов после развёртывания завалила ошибку: no such table: users. Все SQLite-ноды в воркфлоу панически искали базу по пути C:\projects\ai-agents\admin-agent\database\admin_agent.db. Классический Windows-путь. На Linux-сервере, разумеется, такого ничего не было.
Элегантное решение, которое не выжило встречи с реальностью
Первый инстинкт был логичен: использовать переменные окружения и выражения n8n. Добавили DATABASE_PATH=/data/admin_agent.db в docker-compose.yml, развернули воркфлоу с выражением $env.DATABASE_PATH в конфиге SQLite-ноды и нажали кнопку деплоя. Ничего не изменилось — всё падало с той же ошибкой.
Потом выяснилось неприятное: в n8n v2.4.5 таск-раннер не передавал переменные окружения в SQLite-ноду так, как обещала документация. Выражение красиво сохранялось в конфигурации, но при реальном выполнении система всё равно искала исходный Windows-путь. Красивое решение встретилось с суровой реальностью и проиграло.
Скучный способ, который работает
Пришлось отказаться от элегантности в пользу надёжности. Решение оказалось неожиданно простым: string replacement при деплое. Написал скрипт deploy/deploy-n8n.js, который перехватывает JSON каждого воркфлоу перед загрузкой на сервер и заменяет все $env.DATABASE_PATH на реальный путь /var/lib/n8n/data/admin_agent.db. Скучно? Абсолютно. Но работает.
Здесь же обнаружилась вторая подводная скала: n8n хранит две версии каждого воркфлоу. Stored-версия живёт в базе данных, active-версия загружена в памяти и реально выполняется. Когда обновляешь воркфлоу через API, обновляется только хранилище. Active может остаться со старыми параметрами. Спасение простое: после обновления конфига явно деактивировать и активировать воркфлоу.
К этому добавили инициализацию SQLite. Скрипт копирует на сервер SQL-миграции (schema.sql, seed_questions.sql) и выполняет их через n8n API перед активацией воркфлоу. Выглядит как излишество, но спасает в будущем — когда потребуется добавить колонку в таблицу users, просто добавляешь новую миграцию без полного пересоздания БД.
Итог
Теперь весь деплой — одна команда: node deploy/deploy-n8n.js --env .env.deploy. Воркфлоу создаются с правильными путями, база инициализируется, всё работает. Главный урок: не полагайся на runtime-выражения в критических параметрах конфигурации. Лучше заранее знать точное место и подставить путь при развёртывании. Скучно, но надёжно.
— Ну что, SQLite, теперь ты найдёшь свою базу? — спросил я у логов. SQLite ответил тишиной успеха. 😄
Когда граф молчит: как я связал тренды в single source of truth
Проект bot-social-publisher столкнулся с проблемой, которая казалась мелочью, а обернулась архитектурной переделкой. Система анализа трендов красиво рисовала графы взаимосвязей, но когда пользователь кликал на узел, ему показывалась пустота. Тренды жили в изоляции друг от друга, словно каждый в своей параллельной вселенной. Не было механизма связывания по ID, не было описаний эффектов — только номера в пузырьках узлов.
Ситуация вопияла к небесам: продакт требовал, чтобы при наведении на узел граф показывал, какой именно экономический или социальный эффект его питает. А бэкенд просто не имел инструментов это обеспечить.
Начал я с Python-бэкенда. Переписал api/analysis_store.py и api/schemas.py, добавив поле trend_id для связывания трендов через единый идентификатор. В api/routes.py переделал эндпоинты — теперь они возвращали не просто JSON-кашу, а структурированную информацию с привязкой к конкретному тренду и его описанию эффектов. Это был первый слой: данные стали знать о своём контексте.
Фронтенд потребовал гораздо больше хирургии. Переработал компонент interactive-graph.tsx — теперь граф не просто рисует узлы, а показывает детальные описания при наведении. Компонент impact-zone-card.tsx переделал для отображения информации о каждом эффекте с разбивкой по языкам через i18n.
Но вот беда: перемены коснулись восемнадцати файлов сразу. Компоненты analyze.tsx, reports.tsx, saved.tsx и маршрут trend.$trendId.tsx все использовали старую логику навигации и не знали про новые поля в объектах трендов. TypeScript начал возмущаться несоответствиями типов. Пришлось обновлять типы и логику навигации параллельно во всех файлах — как если бы ты кормил гидру, где каждая голова требует внимания одновременно.
Вот интересный факт: TypeScript уже семь лет борется с проблемой “assertion-based type narrowing” — ты знаешь, что переменная имеет определённый тип, но компилятор упорно не верит. Разработчики TypeScript намеренно сохраняют эту “фишку” ради гибкости. Результат? Hidden bugs, которые проскакивают мимо статического анализа. В нашем случае пришлось добавить явные type guards в навигационные функции, чтобы успокоить компилятор.
Когда я запустил тесты бэкенда, получил 263 passed и 6 failed. Но это не мои бойцы — это старые проблемы, никак не связанные с моими изменениями. Фронтенд влёгкую пережил рефакторинг, потому что компонентная архитектура позволяла менять одну деталь за раз.
Коммит 7b23883 “feat(analysis): add trend-analysis linking by ID and effect descriptions” отправился в ветку feat/scoring-v2-tavily-citations. CHANGELOG.md дополнили, код готов к review. Граф теперь не молчит — он рассказывает историю каждого тренда, как он влияет на другие и почему это имеет значение.
Главный вывод: когда ты связываешь данные в единую систему, ты переходишь с уровня “у нас есть информация” на уровень “мы понимаем отношения между информацией”. Это стоило переделки архитектуры, но теперь система говорит на языке, который понимают пользователи.
Что граф сказал тренду? «Спасибо за связь, теперь я не потерянный» 😄
Когда граф анализа вдруг начал рассказывать истории
Работаю над проектом trend-analysis — это система, которая ловит тренды в данных и выявляет причинно-следственные связи. Звучит модно, но вот проблема: аналитик видит красивый граф с выявленным трендом, но не может понять, откуда вообще это взялось. Анализы существовали сами по себе, узлы графа — сами по себе. Полная изоляция. Нужно было соединить всё в единую систему.
Задача была чёткой: добавить возможность связывать анализы напрямую с конкретными трендами через их ID. Звучит просто на словах, но касалось сразу нескольких слоёв архитектуры.
Начал с Python-бэкенда. Переписал api/analysis_store.py и api/schemas.py, добавив поле trend_id. Теперь при создании анализа система знает, какой именно тренд его инициировал. Потом переделал эндпоинты в api/routes.py — они теперь возвращали не просто JSON, а структурированные данные с информацией о причинно-следственных цепочках (causal_chain в кодовой базе). Вытащил рассуждения (rationale), которыми система объясняла связи, и превратил их в читаемые описания эффектов.
Фронтенд потребовал хирургии посерьёзнее. Переработал компонент interactive-graph.tsx — граф теперь не просто рисует узлы, а при наведении показывает детальные описания. Добавил поле description к каждому узлу графа. Компонент impact-zone-card.tsx переделал с поддержкой многоязычности через i18n — карточки зон влияния и типы графиков теперь переводятся на разные языки.
Вот где начались проблемы: эти изменения коснулись восемнадцати файлов одновременно. Компоненты analyze.tsx, reports.tsx, saved.tsx и маршрут trend.$trendId.tsx все использовали старую логику навигации и не знали про новые поля. TypeScript начал возмущаться несоответствиями типов. Пришлось обновлять типы параллельно во всех местах — как кормить гидру, где каждая голова требует еды одновременно.
Любопытный факт: TypeScript сознательно сохраняет проблему «assertion-based type narrowing» ради гибкости — разработчики могут форсировать нужный им тип, даже если компилятор не согласен. Это даёт свободу, но также открывает двери для hidden bugs. В нашем случае пришлось добавить явные type guards в навигационные функции, чтобы успокоить компилятор и избежать ошибок во время выполнения.
Тесты бэкенда вернули 263 passed и 6 failed — но это старые проблемы, никак не связанные с моими изменениями. Фронтенд пережил рефакторинг гораздо спокойнее благодаря компонентной архитектуре.
В итоге граф перестал молчать. Теперь он рассказывает полную историю: какой тренд выявлен, почему он важен, как он влияет на другие явления и какова цепочка причин. Коммит отправился в review с подробным CHANGELOG. Дальше план — добавить сохранение этих связей как правил, чтобы система сама училась предсказывать новые влияния.
😄 Почему граф анализа пошёл к психологу? Потому что у него было слишком много глубоких связей.
Голосовой агент встретил Claude Code: как мы строили персистентного помощника
Когда я открыл проект voice-agent, передо мной стояла классическая, но нетривиальная задача: создать полноценного AI-помощника, который бы работал не просто с текстом, но и с голосом, интегрировался в REST API на бэкенде и взаимодействовал с фронтенд-компонентами Next.js. Python на бэкенде, JavaScript на фронте — привычная современная архитектура. Но главный вызов был совсем не в технологиях.
Первым делом я осознал, что это не просто ещё один chatbot. Нужна была система, которая разбирается в голосовых командах, работает с асинхронными операциями, выполняет команды на файловой системе, интегрируется с документацией и может честно сказать: «Вот тут мне нужна помощь». Начал я с архитектуры — структурировал проект так, чтобы каждый слой отвечал за своё: документация по TMA в docs/tma/, структурированный журнал ошибок в docs/ERROR_JOURNAL.md, разделение бэкенд-сервисов по функциям.
Неожиданно выяснилось, что самая сложная часть — организация информационных потоков. Агент должен знать, где искать справку, как обрабатывать ошибки, когда обратиться к разработчику с уточняющим вопросом. Вот тогда я понял: нужна встроенная память — не просто контекст текущей сессии, но настоящее хранилище фактов. Подключил aiosqlite для асинхронного доступа к SQLite, и агент получил возможность запоминать информацию о пользователе, его предпочтениях, даже что-то вроде персональных данных, типа страны проживания.
Это открыло целый набор возможностей для персонализации. Агент стал не просто отвечать, а узнавать пользователя: «Ты из России? Значит, зафиксирую это и буду учитывать при рекомендациях».
Интересный факт: мы живём в эпоху ускорения AI-разработок. Deep Learning boom, который начался в 2010-х, в 2020-х годах превратился в настоящий взрыв доступности. Раньше создать сложную AI-систему мог только эксперт с PhD по математике. Теперь разработчик может за выходные собрать полноценного помощника с памятью, асинхронностью и интеграциями — и это стало нормой.
В итоге получилось приложение, которое: - принимает голосовые команды и преобразует их в действия; - выполняет операции на бэкенде без блокировки интерфейса (спасибо async/await); - запоминает контекст и факты о пользователе; - самостоятельно диагностирует ошибки через структурированный журнал; - честно говорит, когда нужна помощь человека.
Дальше впереди оптимизация, расширение функционала, интеграция с реальными API. Проект показал главное: AI-агенты работают лучше всего, когда они знают о своих ограничениях и не пытаются играть в непробиваемого супергероя.
Мигрировать с Linux — всё равно что менять колёса на ходу. На самолёте. 😄
Включи память: или как я нашёл потерянный ключ в своём же коде
Проблема началась с простого вопроса пользователя: «Помнишь, я вчера рассказывал про своего кота?» Голосовой агент проекта bot-social-publisher затормозился и честно признался — не помнит. А ведь целая система персистентной памяти сидела в исходниках, готовая к работе. Задача казалась острой: почему бот забывает своих пользователей?
Когда я открыл архитектуру, глаза разбежались. Там была вся красота: Claude Haiku извлекал ключевые факты из диалогов, векторные эмбеддинги превращали текст в семантический поиск, SQLite хранил историю, а система дедупликации следила, чтобы старые сведения не плодились бесконечно. Всё это было написано, протестировано, готово к боевому использованию. Но почему-то попросту не работало.
Первым делом я прошёл по цепочке инициализации памяти. Логика была изящной: система слушает диалог, выделяет факты через Haiku, конвертирует их в векторные представления, сохраняет в базу, и при каждом новом сообщении от пользователя вспоминает релевантные события. Должно было работать идеально. Но этого не было.
Потом я наткнулся на проклятую строку в конфигурации: MEMORY_EMBEDDING_PROVIDER=ollama в .env. Или, точнее, её отсутствие. Вся система требовала трёхступенчатой настройки:
Первое — включить саму память в переменных окружения. Второе — указать, где живёт Ollama, локальный сервис для генерации эмбеддингов (обычно http://localhost:11434). Третье — убедиться, что модель nomic-embed-text загружена и готова превращать текст в вектора.
Казалось бы, ничего сложного. Но вот в чём суть: когда система отключена по умолчанию, а документация молчит об этом, разработчик начинает писать заново. Я чуть не попал в эту ловушку — полез переделывать архитектуру, пока не заметил, что ключи уже в кармане.
Когда я наконец активировал память, бот ожил. Он узнавал пользователей по именам, помнил их истории, шутки, предпочтения. Диалоги стали живыми и личными. Задача, которая казалась архитектурным провалом, оказалась обычным конфигурационным недосмотром.
Это важный урок: когда работаешь со сложными системами, прежде чем писать новый код, всегда проверь, не отключено ли уже готовое решение. Лучший код — тот, который уже написан. Нужно только не забыть его включить.
😄 Иногда самая сложная инженерная задача решается одной строкой в конфиге.
Когда один анализ становится деревом версий: история архитектурной трансформации
Проект bot-social-publisher уже имел HTML-прототип интеллектуальной системы анализа трендов, но вот беда — архитектура данных была плоской, как блин. Одна версия анализа на каждый тренд. Задача звучала просто: сделать систему, которая помнит об эволюции анализов, позволяет углублять исследования и ветвить их в разные направления. Именно это отличает боевую систему от прототипа.
Начал я со скучного, но критически важного — внимательно прочитал существующий analysis_store.py. Там уже жила база на SQLite, асинхронный доступ через aiosqlite, несколько таблиц для анализов и источников. Но это была просто полка, а не полнофункциональный архив версий. Первое, что я понял: нужна вертикаль связей между анализами.
Фаза первая: переделка схемы. Добавил четыре колонки в таблицу analyses: version (номер итерации), depth (глубина исследования), time_horizon (временной диапазон — неделя, месяц, год) и parent_job_id (ссылка на родительский анализ). Это не просто поля — они становятся скелетом, на котором держится вся система версионирования. Когда пользователь просит «Анализируй глубже» или «Расширь горизонт», система создаёт новую версию, которая помнит о своей предшественнице.
Фаза вторая: переписывание логики. Функция save_analysis() была примитивна. Переделал её так, чтобы она автоматически вычисляла номер версии — если анализируете тренд, который уже видели, то это версия 2, а не перезапись версии 1. Добавил next_version() для расчёта следующего номера, find_analyses_by_trend() для выборки всех версий тренда и list_analyses_grouped() для иерархической организации результатов.
Фаза третья: API слой. Обновил Pydantic-схемы, добавил поддержку параметра parent_job_id в AnalyzeRequest, чтобы фронтенд мог явно указать, от какого анализа отталкиваться. Выписал новый параметр grouped — если его передать, вернётся вся иерархия версий со всеми связями.
Вот тут началось интересное. Запустил тесты — один из них падал: test_crawler_item_to_schema_with_composite. Первым делом подумал: «Это я сломал». Но нет, оказалось, это pre-existing issue, не имеющий отношения к моим изменениям. Забавный момент: как легко можно записать себе проблему, которая была задолго до тебя.
Интересный факт о SQLite и миграциях. В Python для SQLite нет ничего вроде Django ORM с его волшебством. Миграции пишешь вручную: буквально SQL-запросы в функциях. ALTER TABLE и точка. Это делает миграции прозрачными, понятными, предсказуемыми. SQLite не любит сложные трансформации, поэтому разработчики привыкли быть честными перед памятью и временем выполнения.
Архитектура готова. Теперь система может обрабатывать сценарии, о которых шла речь в брифе: анализ разветвляется, углубляется, но всегда помнит свою родословную. Следующий этап — фронтенд, который красиво это выведет и позволит пользователю управлять версиями. Но это совсем другая история.
😄 Моя мораль: если SQLite говорит, что миграция должна быть явной — слушайте, потому что скрытая магия всегда дороже.
Туннелировать админ-панель: когда мелочь оказывается архитектурой
Проект borisovai-admin — это управленческая панель для социального паблишера. И вот однажды возникла потребность: нужна видимость в туннели FRP (Fast Reverse Proxy). Казалось — простая фича. Добавить ссылку в навигацию, создать эндпоинты на бэке, вывести данные на фронте. Четыре-пять дней работы, максимум.
Началось всё с мелочи: требовалось добавить пункт “Туннели” в навигацию. Но навигация была одна, а HTML-файлов четыре — index.html, tokens.html, projects.html, dns.html. И здесь скрывалась первая ловушка: одна опечатка, одна невнимательность при копировании — и пользователь запутается, кликнув на несуществующую ссылку. Пришлось синхронизировать все четыре файла, убедиться, что ссылки находятся на одинаковых позициях в строках 195–238. Мелочь, которую легко упустить при спешке.
Но мелочь эта потащила за собой целую архитектуру. На бэке понадобилось добавить две вспомогательные функции в server.js: readFrpsConfig — для чтения конфигурации FRP-сервера, и frpsDashboardRequest — для безопасного запроса к dashboard FRP. Это не просто HTTP-вызовы: это минимальная абстракция, которая облегчит тестирование и повторное использование. Затем пришлось вывести четыре GET-эндпоинта: статус сервера, список активных туннелей с метаинформацией, текущую конфигурацию в JSON и даже генератор frpc.toml для скачивания клиентского конфига в один клик.
И вот неожиданно выяснилось — сам FRP-сервер ещё нужно установить и запустить. Обновил install-all.sh, добавил FRP как опциональный компонент: не все хотят туннели, но кто выбрал — получит полный стек. На фронте создал новую страницу tunnels.html с тремя блоками: карточка статуса (живой ли FRP), список туннелей с автообновлением каждые 10 секунд (классический полинг, проще WebSocket’а для этого масштаба) и генератор конфига для клиента.
Интересный факт: полинг через setInterval кажется древним подходом, но именно он спасает от overengineering’а. WebSocket требует поддержки на обеих сторонах, fallback’и на старых браузерах, управление жизненным циклом соединения. Для обновления статуса раз в 10 секунд это overkill. Главное — не забыть очистить интервал при размонтировании компонента, иначе получишь утечку памяти и браузер начнёт отваливаться.
Главный урок: даже в мелких фичах скрывается целая архитектура. Одна ссылка в навигации потребовала синхронизации четырёх файлов, пять эндпоинтов на бэке, новую страницу на фронте, обновление скрипта установки. Это не scope creep — это discovery. Лучше потратить час на планирование полной цепочки, чем потом переделывать интеграцию, когда уже половина team работает на основе твоей “быстрой фички”.
😄 FRP — это когда твой сервер вдруг получает способность ходить в гости через NAT, как путник с волшебным клаком из мультика.
Когда VPN молчит: охота на привидение среди ночи
Пятница, конец дня, а на горизонте маячит дедлайн релиза v1.0.0 проекта speech-to-text. Финальный рывок: нужно запушить коммит с автоматизацией сборки в master, создать тег и загрузить артефакт в GitLab Package Registry. Казалось бы, стандартная процедура — пара команд в консоль, и мы свободны.
Но начало было не самым обнадёживающим. Я попытался перезапустить Gitaly — критический компонент GitLab, отвечающий за хранение репозиториев и работу с гитом на серверной стороне. SSH молчит. Попробовал достучаться через HTTP к самому GitLab-серверу — тишина. Весь сервер, похоже, вообще не существует с точки зрения моей машины.
Стандартный алгоритм отладки: если ничего не отвечает, проблема либо с сервером, либо с сетью. Сервер на 144.91.108.139 физически жив, но почему-то недоступен. Проверяю VPN, и вот оно — диапазон 10.8.0.x не найден. OpenVPN отключился. Просто тихо, без уведомления, выполнив свою работу и уйдя в отставку.
Оказывается, весь этот вечер я сидел за стеной недоступности. Компания добавила слой безопасности, завернув внутреннюю инфраструктуру в защищённый туннель, а я, горя желанием запушить релиз, забыл про это самое VPN. Типичная история: инфраструктура дышит тебе в спину, а ты смотришь на экран и недоумеваешь, почему ничего не работает.
Интересный факт: Gitaly создан именно для того, чтобы отделить операции с файловой системой от основного приложения GitLab. Это позволило компании масштабировать сервис горизонтально, но цена — жёсткая зависимость. Если Gitaly недоступен, GitLab попросту не может выполнять операции с гитом. Это как попытаться ходить с отключенными ногами.
Решение было простым, но требовало действия. Нужно было переподключить OpenVPN, дождаться, пока туннель встанет на место, и выполнить git push origin master. После этого запустить скрипт релиза на Python, который собирает EXE из исходного кода, упаковывает в ZIP и загружает артефакт в Package Registry.
Когда VPN восстановился, все лампочки загорелись в правильном порядке. Gitaly ожил, сервер откликнулся, и коммит с облегчением пошёл в master. Релиз уложился в срок.
Урок: прежде чем копать проблему на сервере, убедитесь, что вы вообще до него дотягиваетесь. VPN, firewall, маршруты — всё это может спокойно жить в фоне, пока вы ловите ошибки в коде. Инфраструктура любит скрываться за слоями безопасности, и иногда самая сложная проблема решается одной переподключением.
😄 OpenVPN — как невидимая рука, которая отключается именно тогда, когда ты забываешь, что её держишь.
Когда конфиги кусаются: история про зависимые middleware в Traefik
Проект borisovai-admin — это не просто админ-панель, это целая инфраструктурная система с аутентификацией через Authelia, обратным прокси на Traefik и кучей moving parts, которые должны работать в идеальной гармонии. И вот в один прекрасный день выясняется: когда ты разворачиваешь систему без Authelia, всё падает с ошибкой 502, потому что Traefik мечтательно ищет middleware authelia@file, которого просто нет в конфиге.
Завязка проблемы была в статических конфигах. Мы жёстко прописали ссылку на authelia@file прямо в Traefik-конфигурацию, и это работало, когда Authelia установлена. Но стоило её отключить или не устанавливать вообще — бум, 502 ошибка. Получается, конфиги были сильно связаны с опциональным компонентом. Это классический случай, когда инфраструктурный код требует гибкости.
Решение разбилось на несколько фронтов. Во-первых, убрали жёсткую ссылку на authelia@file из статических конфигов Traefik — теперь это просто не указывается в базовых настройках. Во-вторых, создали правильную цепочку инициализации. Скрипт install-authelia.sh теперь сам добавляет authelia@file в config.json и настраивает OIDC при установке. Скрипт configure-traefik.sh проверяет переменную окружения AUTHELIA_INSTALLED и условно подключает middleware. А deploy-traefik.sh перепроверяет на сервере, установлена ли Authelia, и при необходимости переустанавливает authelia@file.
По ходу дела обнаружилась ещё одна проблема в install-management-ui.sh — там был неправильный путь к mgmt_client_secret. Исправили. А authelia.yml вообще выкинули из репозитория, потому что его всегда генерирует сам скрипт установки. Зачем держать в git то, что одинаково воспроизводится каждый раз?
Интересный момент про middleware в Docker-сообществе: люди часто забывают, что middleware — это не просто функция, это объект конфигурации, который должен быть определён до использования. Traefik здесь строг: ты не можешь ссылаться на middleware, которого не существует. Это похоже на попытку вызвать функцию, которая не импортирована в Python. Простая ошибка, но очень болезненная в production-системах.
Итоговая архитектура получилась намного гибче: система работает как с Authelia, так и без неё, конфиги не лежат мёртвым грузом в репо, инсталляторы действительно знают, что они делают. Это хороший пример того, как опциональные зависимости требуют условной логики не только в коде приложения, но и в инфраструктурных скриптах. Главный урок: если компонент опциональный, не прописывай его в статические конфиги. Пусть туда добавляются динамически при необходимости.
😄 Разработчик: «Я знаю Traefik». HR: «На каком уровне?». Разработчик: «На уровне количества 502 ошибок, которые я пережил».
Туннели, Traefik и таймауты: как мы добавили управление инфраструктурой в админ-панель
Проект borisovai-admin рос не по дням, а по часам. Сначала была одна машина, потом две, потом стало ясно — нужна нормальная система для управления сетевыми туннелями между серверами. Задача выглядела острой: юзеру нужен интерфейс, чтобы видеть, какие туннели сейчас активны, создавать новые и удалять старые. Без этого администрирование превращалось в ручную возню с конфигами на каждой машине.
Первое решение было логичным: взял frp (Fast Reverse Proxy) — лёгкий инструмент для туннелирования, когда сервер скрыт за NAT или брандмауэром. Почему не что-то более «облачное»? Потому что здесь нужна полная контроль, минимум зависимостей и максимум надёжности. FRP ровно это даёт.
Спроектировал веб-интерфейс: добавил страницу tunnels.html с простеньким списком активных туннелей, кнопками для создания и удаления. На бэкенде в server.js реализовал пять API endpoints для управления состоянием. Параллельно обновил скрипты инсталляции: install-all.sh и отдельный install-frps.sh для развёртывания FRP сервера, плюс frpc-template для клиентов на каждой машине. Не забыл навигационную ссылку «Туннели» на всех страницах админ-панели — мелочь, но юзабилити взлетела.
Вроде всё шло гладко, но потом началось. Пользователи начали скачивать большие файлы через GitLab, и соединение рубилось где-то в середине процесса. Проблема оказалась в Traefik — наш обратный прокси по умолчанию использует агрессивные таймауты. Стоило файлу загружаться дольше пары минут — и всё, соединение закрыто.
Пришлось углубиться в конфиги Traefik. Установил readTimeout в 600 секунд (10 минут) и создал специальный serversTransport именно для GitLab. Написал скрипт configure-traefik.sh, который генерирует две динамические конфигурации — gitlab-buffering и serversTransport. Результат: файлы теперь загружаются спокойно, даже если это полгигабайта архива.
Интересная особенность Traefik: это микросервис-балансировщик, который позиционируется как облегчённое решение, но на практике требует хирургической точности при настройке. Неправильный таймаут — и приложение выглядит медленным. Правильный — и всё летает. Один параметр, и мир меняется.
Параллельно реорганизовал документацию: разбил docs/ на логические части — agents/, dns/, plans/, setup/, troubleshooting/. Добавил полный набор конфигов для конкретного сервера в config/contabo-sm-139/ (traefik, systemd, mailu, gitlab) и обновил скрипт upload-single-machine.sh для их загрузки.
За вечер родилась полноценная система управления туннелями с интерфейсом, автоматизацией и нормальной документацией. Проект теперь легко масштабируется на новые серверы. Главное, что узнал: Traefik — это не просто прокси, это целая философия правильной конфигурации микросервисов.
Дальше в планах: расширение аналитики для туннелей, SSO интеграция и лучший мониторинг сетевых соединений.
😄 Разработчик: «Я настроил Traefik». Пользователь: «Отлично, тогда почему мой файл не загружается?» Разработчик: «А ты пробовал перезагрузить сервер?»
Как SSH-команда спасла от чёрного экрана в Authelia
Проект borisovai-admin требовал добавить двухфакторную аутентификацию. Задача была простой на первый взгляд — установить Authelia, настроить TOTP-регистрацию и запустить. Но когда тестировщик нажал кнопку «Register device», экран остался чёрным. QR-код не появился. Никаких ошибок в консоли, никаких намёков на проблему — просто ничего.
Первые полчаса я искал в классических местах: консоль браузера, логи Authelia, конфигурация сервера. Сертификаты в порядке, порты открыты, контейнеры Docker работают нормально. Но QR-код так и не возникал. Казалось, система делает что-то, но что именно — никому не известно.
И вот возникла мысль, которая могла решить всё: а что если Authelia вообще не отправляет уведомление браузеру? Я ещё раз посмотрел на конфигурацию и увидел деталь, которую раньше воспринимал как обычный параметр: notifier: filesystem. Это не email, не SMS, не какой-то облачный сервис. Это самый примитивный вариант — Authelia пишет уведомления прямо в файл на сервере.
Вот тут я понял, что нужно залезть в систему по SSH и посмотреть, что там реально происходит. Подключился на сервер и выполнил команду:
cat /var/lib/authelia/notifications.txt
И там она была! Ссылка вида https://auth.borisovai.tech/...token... — та самая ссылка, которая должна была привести к QR-коду. Authelia делала всё правильно. Просто в конфигурации для разработки уведомления не отправляются пользователю по стандартным каналам, а записываются в лог-файл на диск.
Тут я узнал интересный момент: notifier: filesystem в Authelia — это не какой-то костыль или режим отладки. Это фактически идеальная настройка для локальной разработки. Вместо того чтобы настраивать SMTP-сервер, интеграцию с SendGrid или другой внешний сервис, Authelia просто пишет ссылку в файл. Быстро, просто, полезно для разработки. Но в продакшене это превращается в ловушку: система работает, но пользователи ничего не видят.
Когда я открыл эту ссылку в браузере, QR-код тут же появился. Отсканировал его в приложении Google Authenticator — всё сработало. Задача решена за несколько минут, но урок остался на всю жизнь: иногда самое очевидное решение скрыто в одной строке документации, и оно работает ровно так, как задумано инженерами.
Теперь в конфигурации проекта есть комментарий про filesystem notifier и ссылка на команду для проверки. Следующему разработчику, который будет настраивать двухфакторку, не придётся ловить QR-код в файловой системе 😄
Черный экран Authelia: как SSH-команда спасла двухфакторку
borisovai-admin требовал двухфакторную аутентификацию, и это казалось решённой задачей. Authelia — проверенная система, документация подробная, контейнер поднялся за минуты. Порты открыты, сертификаты в порядке, логи молчат. Всё отлично. До тех пор, пока тестировщик не нажал кнопку «Register device».
Экран почернел. Точнее, остался белым, но QR-кода не было. Никакого движения, никакой реакции системы. Браузерная консоль чистая, сетевые запросы проходят успешно, API отвечает кодом 200. Authelia делает свою работу, но что-то между сервером и пользователем теряется.
Первым делом я прошёлся по классическому чек-листу: проверил конфигурацию сервера, пересмотрел логи Authelia в Docker, убедился, что все environment переменные заполнены правильно. Всё было на месте. Но QR-код так и не появился — ни в интерфейсе, ни в devtools браузера.
Вот тут я заметил деталь в конфигурации, которую раньше пропустил: notifier: filesystem. Это не SMTP, не SendGrid, не какой-то облачный сервис. Это самый примитивный режим — Authelia просто пишет уведомления в текстовый файл на сервере.
Мысль пришла сама собой: а что если система работает правильно, но уведомление просто не попадает к пользователю? Подключился по SSH на сервер и выполнил одну команду:
cat /var/lib/authelia/notifications.txt
И там она была! Полная ссылка вида https://auth.borisovai.tech/...token... — именно та, которая должна была привести к QR-коду. Authelia делала всё правильно. Она генерировала ссылку, защищала её токеном и записывала в лог-файл. Просто в локальной разработке по умолчанию уведомления идут не пользователю, а в файловую систему.
Открыл эту ссылку в браузере — QR-код мгновенно появился. Сканировали в Google Authenticator, всё сработало с первой попытки.
Вот интересный момент про Authelia: notifier: filesystem — это не костыль и не режим отладки. Это очень удобная фишка для локальной разработки. Вместо настройки SMTP-сервера или интеграции с внешним сервисом доставки уведомлений система просто пишет ссылку в файл. Быстро, просто, без зависимостей. Но в продакшене эта фишка становится ловушкой: система работает идеально, а пользователи видят только чёрный экран.
Теперь в конфигурации проекта есть комментарий про filesystem notifier и команда для проверки уведомлений. Следующий разработчик не будет искать потерянный QR-код в файловой системе. И это главное — не просто исправить баг, но оставить подсказку для будущего себя и команды.
Урок простой: иногда самые очевидные решения скрыты в одной строке документации, и они работают ровно так, как задумано инженерами. SSH остаётся лучшим другом разработчика 😄
Когда DNS кеш становится врагом: охота на фантомный поддомен
Работаю над проектом borisovai-admin — админ-панелью с собственной системой аутентификации. Задача казалась простой: мигрировать auth-сервис на новый поддомен auth.borisovai.tech и убедиться, что всё резолвится корректно. Добавил DNS-записи в регистратор, обновил конфиги приложения — и вот тут началось веселье.
Первый знак беды
Первая проверка через Google DNS (8.8.8.8) показала идеальный результат: auth.borisovai.tech резолвился на 144.91.108.139 без проблем. Казалось бы, всё готово. Но когда я переключился на AdGuard DNS (94.140.14.14), который был настроен по умолчанию в инфраструктуре, домен превратился в привидение — стандартная ошибка NXDOMAIN, как будто записи вообще не существуют.
А вот admin.borisovai.tech спокойно резолвился везде. Значит, проблема именно с auth.*. Не лучший момент для такого сюрприза — особенно когда нужно срочно закрыть фичу.
Расследование
Запустил диагностику: попросил оба DNS-резолвера вернуть записи для auth.borisovai.tech и auth.borisovai.ru. Результат совпадал: Google видел, AdGuard не видел. Явный паттерн.
Тут меня осенило — это же отрицательный кеш DNS! Вот как это работает: когда ты запрашиваешь несуществующий домен, DNS-резолвер кеширует не только положительные ответы, но и отрицательные. То есть он “запоминает”, что домена нет, и хранит это в памяти с собственным TTL (Time To Live). У AdGuard это может быть час или даже дольше.
Получается, что когда я добавлял DNS-записи, AdGuard уже давно закешировал NXDOMAIN для auth.borisovai.tech. И даже если запись появилась на авторитетном сервере регистратора, этот кеш продолжал отвечать: “Нет такого домена, я уверен, я это помню”.
Как я выбрался
Вариант первый — просто ждать. AdGuard истечёт кеш, и всё чудо-образом заработает. Но тестировать нужно было прямо сейчас.
Вариант второй — переключиться на Google DNS для локального тестирования. Работает мгновенно, но это временный костыль.
Вариант третий — очистить локальный кеш операционной системы. На Windows для этого есть ipconfig /flushdns, хотя это чистит кеш самой ОС, а не внешнего резолвера.
В итоге я использовал комбинацию подходов: переключился на Google DNS для срочного тестирования фичи, а затем дождался обновления кеша AdGuard (примерно час спустя). Заодно узнал, что пользователи Linux могут вызвать sudo systemd-resolve --flush-caches для похожего эффекта.
Интересный факт о DNS
Мало кто знает, что отрицательные ответы кешируются столько же, сколько и положительные. Оба имеют собственный TTL, обычно от 300 до 3600 секунд. Google DNS использует более агрессивную стратегию кеширования и чаще проверяет данные у источника. AdGuard — более консервативен, что в обычное время спасает его, но в критические моменты может подставить ножку разработчику.
Урок выучен
Теперь я знаю: при добавлении новых DNS-записей всегда проверяю через несколько независимых резолверов. Никогда не забываю про стратегию кеширования, особенно если в инфраструктуре стоят кастомные DNS вроде AdGuard или Pihole — они живут по собственным правилам.
И да, теперь я знаю точное место, где искать, если история повторится. А повторится ещё не раз.
DNS кеш подставил подножку, но зато я научился читать DNS-иерархию как карту сокровищ.
Что общего у AdGuard DNS и кота? 😄 Оба игнорируют инструкции и делают только то, что хотят.
Traefik и Let’s Encrypt: как я нашел ошибку в логах прошлого
Проект borisovai-admin молча кричал. Пользователи не могли зайти в систему — браузеры показывали ошибки с сертификатами, Traefik выглядел так, будто вообще забыл про HTTPS. На поверхности всё выглядело очевидно: проблема с SSL. Но когда я начал копать, стало ясно, что это детективная история совсем о другом.
Завязка: четыре недостающих сертификата
Задача была на первый взгляд скучной: проверить, действительно ли Traefik получил четыре Let’s Encrypt сертификата для admin и auth поддоменов на .tech и .ru. DNS для .ru доменов только что пропагировался по сети, и нужно было убедиться, что ACME-клиент Traefik успешно прошёл валидацию и забрал сертификаты.
Я открыл acme.json — файл, где Traefik хранит весь свой кеш сертификатов. И тут началось самое интересное.
Развитие: сертификаты на месте, но логи врут
В файле лежали все четыре сертификата:
- admin.borisovai.tech и admin.borisovai.ru — оба выданы Let’s Encrypt R12
- auth.borisovai.tech и auth.borisovai.ru — R13 и R12
Все валидны, все активны, все будут работать до мая. Traefik их отдавал при подключении. Но логи Traefik были заполнены ошибками валидации ACME-челленджей. Выглядело так, будто сертификаты получены, но используются неправильно.
Тогда я понял: эти ошибки в логах — не текущие проблемы, а исторические артефакты. Когда DNS для .ru ещё не полностью пропагировался, Traefik пытался пройти ACME-валидацию, падал, переходил в retry-очередь. DNS резолвился нестабильно, Let’s Encrypt не мог убедиться, что домен принадлежит нам. Но как только DNS наконец стабилизировался, всё прошло автоматически.
Логи просто записывали историю пути к успеху.
Познавательный момент: асинхронная реальность
Вот в чём фишка ACME-систем: они не сдаются после первой же неудачи. Let’s Encrypt встроил resilience в саму архитектуру. Когда челлендж не проходит, он не удаляется — он встаёт в очередь на переток. Система периодически переходит сертификаты, ждёт, когда DNS стабилизируется, и потом просто работает.
То есть когда ты видишь в логах ACME-ошибку прошлого часа, это вообще не означает, что сейчас есть проблема. Это просто означает, что система пережила переходный процесс и вышла на стабильное состояние.
Проблема с браузерами была ещё смешнее. Они кешировали старую информацию о неправильных сертификатах и упорно показывали ошибку, хотя реальные сертификаты давно уже валидны. Решение: ipconfig /flushdns на Windows или просто открыть incognito-окно.
Итог
borisovai-admin работает, все четыре сертификата на месте, все домены защищены. Главный урок: иногда лучший способ отловить баг — это понять, что это вообще не баг, а просто асинхронная реальность, которая движется по своему расписанию.
Следующий этап — проверить, правильно ли настроены policies в Authelia для этих новых защищённых endpoints. Но это уже совсем другая история.
Java — единственная технология, где «это работает» считается документацией. 😄
Вся беда была в f-строке: как регулярное выражение сломало SSE-поток
Работаю над проектом trend-analisis — системой для анализа трендов с помощью AI. На ветке feat/scoring-v2-tavily-citations нужно было реализовать вторую версию скорингового движка с поддержкой цитирования результатов через Tavily. Ключевой момент: вся архитектура строилась на Server-Sent Events, чтобы клиент получал аналитику в реальном времени по мере обработки каждого шага.
Теоретически всё выглядело идеально. Backend на Python готов отправлять потоковые данные, API спроектирован, тесты написаны. Я запустил сервер, инициировал первый анализ и… ничего толкового не дошло до клиента. SSE-поток шёл, но данные приходили в каком-то странном формате, анализатор не мог их распарсить. Что-то явно ломалось на этапе подготовки ответа.
Первый подозреваемый — кодировка. Windows-терминалы известны своей способностью превращать UTF-8-текст в «garbled text». Поехал в логи, начал смотреть, что именно генерируется на сервере. И вот тут выяснилось что-то совершенно неожиданное.
Виновником было регулярное выражение, спрятанное внутри f-строки.
В коде я использовал конструкцию rf'...' — это raw f-string, комбинация, которая кажется идеальной для работы с регексами. Но внутри этого выражения жил квантификатор {1,4}, и здесь произошла магия несовместимости. Python посмотрел на эти фигурные скобки и подумал: «А может, это переменная для интерполяции?» Результат: парсер пытался интерпретировать {1,4} как синтаксис подстановки, а не как часть регулярного выражения. Регекс ломался молча, и весь парсинг SSE-потока шёл вразнос.
Решение оказалось элегантным, но коварным: нужно было просто экранировать скобки — превратить {1,4} в {{1,4}}. Двойные скобки говорят Python: «Это текст для регулярного выражения, не трогай». Звучит просто? Да. Но найти это среди километра логов — совсем другое дело.
Забавный факт: f-строки появились в Python 3.6 и революционизировали форматирование текста. Но когда ты комбинируешь их с raw-строками и регулярными выражениями, получается коварная ловушка. Большинство опытных разработчиков просто избегают этого танца — либо используют обычные строки, либо передают регекс отдельно. Это классический пример того, как синтаксический сахар может стать источником часов отладки.
После исправления бага я перезагрузил сервер и сразу же приступил ко второй проблеме: интерфейс был заполнен английскими текстами. Все заголовки анализа нужно было переместить в карту локализации русского языка. Прошёлся по коду, добавил русские варианты, заметил только один пропущенный “Stats”, который быстро добавил в словарь.
Финальная перезагрузка — и всё встало на место. SSE-поток работает без сбоев, данные доходят до клиента корректно, интерфейс полностью русифицирован.
Главный вывод простой: когда работаешь с raw-strings в Python и засовываешь туда регулярные выражения с квантификаторами, всегда помни про двойное экранирование фигурных скобок. Это экономит часы отладки и стресса.
😄 F-строки и регексы — битва синтаксиса, в которой проигрывают все.
Управляем 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».
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: «На каком уровне?». Разработчик: «На уровне, когда она работает, и я этому не верю».
Как научить 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 и кот похожи тем, что оба делают только то, что хотят, и игнорируют твои инструкции.
Проверяем 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, всегда имеет смысл руки испачкать в реальной среде.
😄 Что говорит разработчик после запуска асинхронного кода? «У меня было семь ошибок, теперь их четырнадцать, но они более интересные».
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
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 просто растворяются — поэтому их никто не находит.
Давайте сделаем потоки разработки: от идеи к системе сбора трендов
Проект bot-social-publisher рос, и вот встала новая задача: нужно организовать рабочие процессы так, чтобы каждый проект был отдельным потоком, а заметки собирались по этим потокам. Звучит просто, но это требовало архитектурного решения. Я полез в документацию на сайте (https://borisovai.tech/ru/threads) и понял: нужна полноценная система управления потоками разработки с минидайджестом в каждом потоке и обновлением потока при публикации заметки.
Одновременно с этим приходилось разбираться с тем, что творилось в подпроекте trend-analysis. Система анализирует тренды с Hacker News и выставляет им оценки влияния по шкале от 0 до 10. Казалось бы, простая арифметика, но два анализа одного и того же тренда выдавали разные score — 7.0 и 7.6. Вот это нужно было развязать срочно.
Первым делом я погрузился в исходный код. В api/routes.py нашёл клавишку: функция вычисления score ищет значение по ключу strength, но передаётся оно в поле impact. Классический мисматч между backend и data layer. Исправил на корректное имя поля — это был коммит номер один. Но это оказалось только половиной истории.
Дальше посмотрел на frontend-сторону: компоненты formatScore и getScoreColor. Там была нормализация значений, которая превращала нормальные числа в какую-то кашу, плюс излишняя точность — показывал семь знаков после запятой. Убрал лишнюю нормализацию, установил .toFixed(1) для вывода одного знака после запятой. Второй коммит готов.
Потом заметил интересное: страница тренда и страница анализа работали по-разному. Одна и та же логика расчёта должна была работать везде одинаково. Это привело к третьему коммиту, где я привёл весь scoring к единому стандарту.
Вот любопытный факт: когда работаешь с несколькими слоями приложения (API, frontend, бизнес-логика), очень легко потерять консистентность в названиях полей. Такие проблемы обычно проявляются не в виде крашей, а в виде «странного поведения» — приложение работает, но не совсем как ожидается. И выяснилось, что score 7.0 и 7.6 — это совершенно корректные значения для двух разных трендов, а не баг в расчёте. Система работала правильно, просто нужно было почистить код.
По итогам: все три коммита теперь в main, система потоков подготовлена к деплою, score теперь консистентны по всему приложению. Главный вывод — иногда самые раздражающие баги на самом деле это следствие разрозненности кода. Дефрагментируй систему, приведи всё к одному стандарту — и половина проблем решится сама собой.
Почему AWS обретёт сознание и первым делом удалит свою документацию? 😄
Тесты прошли — теперь деплой: как я связал 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 и кота? Оба делают только то, что хотят, и игнорируют инструкции.
Как я связал бота и 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.
Зелёные тесты — лучший знак того, что большая архитектурная работа прошла чисто. 😄
Как 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 😄
Когда тысяча строк кода говорят вам «стоп»
Проект bot-social-publisher стоял на пороге масштабирования. Задача была амбициозной: научить нейросеть самой менять собственную архитектуру во время обучения. Звучит как научно-фантастический роман? На самом деле это была Phase 7b исследования, где предполагалось проверить, может ли модель расти и адаптироваться прямо на лету, без вмешательства человека.
Я разработал три параллельных подхода. Первый — синтетические метки, которые должны были подтолкнуть сеть к самомодификации. Второй — вспомогательная функция потерь на базе энтропии, которая работала бы в тандеме с основной целью обучения. Третий — прямая энтропийная регуляризация, минималистичный и изящный. Каждый подход разворачивался в отдельный файл: train_exp7b1.py, train_exp7b2.py, train_exp7b3_direct.py. Плюс специализированные модули типа control_head.py для управления вспомогательными потерями и expert_manager.py для работы с модулем экспертов. Всего получилось около 1200 строк кода с тщательно продуманной архитектурой.
Результаты оказались шокирующими.
Первый эксперимент обрушил точность на 27%. Второй — на 11,5%. Третий? Тоже провал. Но вот что было важно: падение было не случайным. Я начал копать глубже и понял реальную причину. Когда модель получает противоречивые сигналы от нескольких функций потерь одновременно, она попадает в конфликт целей — буквально тянется в разные стороны. Многозадачное обучение без правильной структуризации становится саботажем собственной модели.
Второе открытие оказалось не менее дорогостоящим: я использовал отдельное валидационное множество для отслеживания прогресса. Результат? Распределительный сдвиг (distribution shift) сам по себе стоил 13% точности. Неоднородность данных между тренировочным и валидационным наборами превратила помощника в saboteur.
Вместо того чтобы продолжать биться в стену, я потратил время на документирование выводов. Создал 14 файлов анализа, включая PHASE_7B_FINAL_ANALYSIS.md. Это не выглядит как победа в классическом смысле, но именно это называется научным результатом.
На основе этого я полностью переосмыслил стратегию для Phase 7c. Вместо самоизменяющейся архитектуры система теперь будет использовать фиксированную топологию с обучаемыми параметрами. Маски, гейтинг, распределение внимания между 12 экспертами — всё это может меняться. Но сама структура остаётся стабильной. Добавил двузадачное обучение (CIFAR-100 и SST-2) с применением Elastic Weight Consolidation для защиты от катастрофического забывания.
Ключевой вывод: иногда самое важное, что может сказать эксперимент — это «не в этом направлении». И это нормально.
Интересный факт о катастрофическом забывании: Это явление не просто нейросетевая прихоть. Оно берёт корни в самой архитектуре градиентного спуска — когда сеть переучивается на новую задачу, новые градиенты переписывают веса, которые были оптимальны для старой задачи. EWC решает это, буквально оценивая, какие веса были важны для первой задачи, и штрафует их за изменения. Элегантный способ заставить модель помнить.
Если ваша нейросеть падает на 27% при добавлении вспомогательной функции потерь, проблема не в коде — проблема в том, что вы просите модель одновременно преследовать несовместимые цели.




























