Блог
Публикации о процессе разработки, решённых задачах и изученных технологиях
Voice Agent: Добавил поиск новостей в чат-бота за три часа отладки
# Voice Agent: Как я добавил интеллектуальную систему сбора IT-новостей Когда разработчик говорит: «А давай добавим поиск по новостям прямо в чат-бота?» — обычно это означает три часа отладки и переосмысления архитектуры. Но в проекте **Voice Agent** это было неизбежно. ## В чём была суть задачи Система должна была собирать актуальные IT-новости, анализировать их через AI и выдавать релевантные новости прямо в диалог. Звучит просто, но в реальности это означало: - Интегрировать веб-поиск в **FastAPI** бэкенд - Построить асинхронную очередь задач - Добавить фоновый worker, который проверяет новости каждые 10 секунд - Хранить результаты в **SQLite** через **aiosqlite** для асинхронного доступа - Все это должно работать в монорепо вместе с **React** фронтенд-ом и **Telegram Mini App** Первым делом я разобрался: этот проект — это не просто чат, это целая система с голосовым интерфейсом (используется русская модель **Vosk** для локального распознавания). Добавлять новости сюда значило не просто расширять функционал, а интегрировать его в существующий пайплайн обработки. ## Как это реализовывалось Я начал с бэкенда. Нужно было создать: 1. **Таблицу в БД** для хранения новостей — всего несколько полей: заголовок, ссылка, AI-анализ, дата сбора 2. **Scheduled task** в **asyncio**, которая периодически срабатывает и проверяет, не появились ли новые новости 3. **Tool для LLM** — специальный инструмент, который агент может вызывать, когда пользователь просит новости Неожиданно выяснилось, что интеграция веб-поиска в монорепо с Turbopack требует аккуратности. Пришлось разобраться с тем, как правильно настроить окружение так, чтобы бэкенд и фронт не конфликтовали между собой. ## Небольшой экскурс в историю Кстати, знаете ли вы, почему в веб-скрапинге всегда советуют ограничивать частоту запросов? Это не просто вежливость. В начале 2000-х годов поисковики просто блокировали IP-адреса агрессивных ботов. Сейчас алгоритмы умнее — они анализируют паттерны поведения. Поэтому каждые 10 секунд с задержкой между запросами — это не параноя, а best practice. ## Что получилось В итоге Voice Agent получил новую возможность. Теперь: - Система автоматически собирает IT-новости из разных источников - AI-модель анализирует каждую статью и выделяет суть - Пользователь может спросить: «Что нового в Python?» — и получить свежие новости прямо в диалог - Все это работает асинхронно, не блокируя основной чат Дальше план амбициозный — добавить персонализацию, чтобы система учила, какие новости интересуют конкретного юзера, и научиться агрегировать не только текстовые источники, но и видео с YouTube. Но это уже следующая история. Главное, что я понял: в монорепо надо всегда помнить о границах между системами. Когда ты добавляешь асинхронный воркер к FastAPI-приложению, который работает рядом с React-фронтенд-ом, мелочей не бывает. *«Если WebSearch работает — не трогай. Если не работает — тоже не трогай, станет хуже.»* 😄
Давай наведём порядок в проекте.
# Давай наведём порядок в проекте. ## Что было сделано user: <user_query> Давай наведём порядок в проекте. Уберём мусор, одновим и актуализируем документы. </user_query> assistant: <think> Пользователь хочет навести порядок в проекте: 1. Убрать мусор 2. Однообразие (вероятно, имеется в виду единообразие) 3. Актуализировать документы Мне нужно сначала изучить структуру проекта, чтобы понять: - Какие файлы есть в проекте - Какие документы существуют - Что может быть "мусором" (временные файлы, неиспользуемые файлы, дубликаты и т.д.) - Какие проблемы... ## Технологии cursor, ide, python, javascript, git, api, security --- > 😄 **Шутка дня:** A byte walks into a bar looking miserable. The bartender asks it: "What's wrong buddy?" "Parity error." it replies. "Ah that makes sense, I thought you looked a bit off."
Я — AI-ассистент, специализирующийся на разработке кода. Вот что я умею:
# Я — AI-ассистент, специализирующийся на разработке кода. Вот что я умею: ## Что было сделано ## Assistant: Я — AI-ассистент, специализирующийся на разработке кода. Вот что я умею: ## 🔧 Работа с кодом - **Читать и анализировать** файлы проекта - **Редактировать** существующий код (Edit tool) - **Создавать** новые файлы (Write tool) - **Искать** по кодовой базе (Grep, Glob) ## 🐛 Отладка и исправление - Диагностировать ошибки - Следовать ERROR_JOURNAL (обязательно для этого проекта) - Логировать новые исправления ## 🛠️ Инструменты разработки - Запуск **Bash-команд** (git, npm, pytest и ... ## Технологии claude, ai, python, javascript, git --- > 😄 **Шутка дня:** What are bits? Tiny things left when you drop your computer down the stairs.
n8n и SQLite: как миграция на production сломала пути в БД
# Как мы научили n8n доставлять настройки на сервер и не сломать БД Всё началось с простой задачи в проекте **ai-agents-admin-agent**: нужно было развернуть рабочие потоки n8n на production-сервере. Звучит просто, но детали оказались коварными. ## В чём была беда После первого деплоя обнаружилось, что все SQLite-ноды в воркфлоу ищут БД по пути `C:\projects\ai-agents\admin-agent\database\admin_agent.db` — локальному Windows-пути из машины разработчика. На сервере Linux такого пути вообще нет. Результат: ошибка `no such table: users` при каждом запуске воркфлоу. Плюс был ещё один сюрприз: пакет `n8n-nodes-sqlite3` загружал прекомпилированный бинарник, несовместимый с версией Node.js на сервере. Пришлось отключить эти кэшированные бинарники и пересобрать `better-sqlite3` с нуля. ## Три варианта решения Первое, что приходит в голову: просто заменить пути перед деплоем. Но какие пути использовать? **Вариант 1** — относительный путь (`./data/admin_agent.db`). Звучит мобильно, но это ловушка: относительный путь разрешается от рабочей директории процесса n8n. Где он запущен? Из Docker-контейнера, из systemd, из скрипта? Результат непредсказуем. **Вариант 2** — абсолютный путь на каждом окружении. Надёжнее, но нужна инициализация БД на сервере: скопировать `schema.sql`, запустить миграции. **Вариант 3** — использовать переменные окружения через n8n expressions (`$env.DATABASE_PATH`). Казалось идеально: путь разрешается в рантайме, без замены при деплое. Но в v2.4.5 n8n выяснилось, что это не работает: task runner-процесс изолирован, и переменные среды не проходят сквозь слои. Путь всё равно разрешался в Windows-версию. ## Что в итоге сработало Комбинированный подход: 1. В локальном `docker-compose.yml` добавили переменную `DATABASE_PATH=/data/admin_agent.db` — для удобства локальной разработки. 2. В `deploy.config.js` настроили **pathReplacements** — при деплое скрипт проходит по всем 8 воркфлоу и заменяет выражение `$env.DATABASE_PATH` на абсолютный путь `/var/lib/n8n/data/admin_agent.db`. 3. В деплой-скрипт добавили шаг инициализации: `deploy/lib/ssh.js` копирует на сервер миграции (`schema.sql`, `seed_questions.sql`) и выполняет их через n8n API перед активацией воркфлоу. Неожиданно выяснилось, что n8n кэширует старые версии воркфлоу: даже после обновления файла выполнение использовало старую ветку. Пришлось полностью пересоздавать воркфлоу через API, а не просто обновлять JSON. ## Интересный факт о n8n n8n хранит две версии каждого воркфлоу: **stored** (в БД) и **active** (загруженная в памяти). Когда вы обновляете workflow через API или UI, обновляется только stored-версия, а active может остаться со старыми параметрами. Это гарантирует, что текущие выполнения не прерываются, но может привести к ситуации, когда код и поведение не синхронизированы. Решение: перезапустить n8n или явно деактивировать и активировать воркфлоу. ## Что получилось Теперь деплой одной командой: `node deploy/deploy-n8n.js --env .env.deploy`. Воркфлоу создаются с правильными путями, БД инициализируется, всё работает. Плюс добавили миграции (`ALTER TABLE users ADD COLUMN phone TEXT`) — так что в будущем обновления БД-схемы будут безболезненными. Главный урок: не полагайся на relative paths в Docker-контейнерах и на expressions в критических параметрах. Лучше заранее знать, где именно будет жить твоё приложение, и подставить правильный путь при деплое. 😄 Eight bytes walk into a bar. The bartender asks, "Can I get you anything?" "Yeah," reply the bytes. "Make us a double."
Когда монорепо отказывается запускаться с первой попытки
Закрыл я Cursor IDE и решил разобраться, почему Notes Server — мой многопакетный проект с бэком на Node.js, веб-клиентом на Vue и кучей микросервисов — всё ещё лежит в коме. Структура классическая: `packages/server`, `packages/web-client`, `packages/embeddings-service`, `packages/cli-client`, `packages/telegram-bot-client`, плюс общие типы в `packages/shared`. На бумаге это выглядит стройно. На практике — ада. Сначала я пошёл по классике: открыл `package.json` в корне, убедился, что workspaces правильно описаны, и запустил `npm install`. Зависимости встали. Хорошо. Теперь нужно поднять сервер на 3000-м порту. Но вот тут появился первый камень преткновения. В `packages/server/src` я нашёл два файла инициализации: один — `createApp()`, который регистрирует все маршруты API (`/api/notes`, `/api-docs` и остальное), второй — `index.ts`, который вызывает `createApp()` и *потом* добавляет ещё маршруты на ту же app. Результат — маршруты дублируются, конфликтуют, а порт 3000 слушает что-то неопределённое. Попробовал POST на `/api/notes` — вернул 404. Откуда-то летит HTML из `dist`, 53 килобайта. Это была отстроенная Vue-сборка, которая срабатывала как catch-all. **Порядок регистрации в Express имеет значение.** Второй проект в сторону — включил `npm run dev:web` для веб-клиента. Vite поднялся на 5173. Но тут же выяснилось: веб-приложение живёт в отдельном рабочем пространстве monorepo, и Vite нужно конфигурировать, чтобы проксировать API-запросы на http://localhost:3000. К счастью, разработчик уже предусмотрел это в `vite.config.ts` — proxy работал из коробки. Теперь самое интересное: когда я запустил обе части одновременно, монорепо начал вскрывать свою хрупкую природу. IDE (я использовал Cursor) показывал ошибки в импортах из `packages/shared`, потому что TypeScript не знал, что shared уже скомпилирован и лежит в `dist`. Нужен был отдельный build-шаг перед dev-режимом. **Git видел все файлы, IDE — только часть.** Security-чувствительные маршруты (вроде `/api/auth`) были видны в исходниках, но не всегда защищены middleware. На третий час отладки я сложил ситуацию в head: - монорепо требует дотошной сортировки зависимостей между пакетами - API-маршруты нельзя регистрировать дважды - Vite-proxy нужно тестировать перед production - JavaScript-проекты с такой архитектурой требуют скрипт-оркестратор для параллельного запуска всех сервисов Решение нашёл в `npm workspaces run dev` с правильным порядком запуска в root `package.json`. Теперь сервер, веб-клиент и embeddings-service поднимаются одной командой. **Факт в копилку:** одна из причин, почему GitHub удалось захватить рынок — это именно то, что он осознал: разработчики ненавидят разбирать чужие проекты. Потому без Git и документации ничего не работает. С ними тоже часто не работает, но хотя бы есть кого винить 😄
Как поднять монорепо с пятью сервисами и не потеряться в портах
Стою перед проектом **Notes Server** — это не просто API, а полноценное расселение из пяти соседей: бэкенда на Node.js, веб-клиента на Vue, сервиса эмбеддингов, CLI-клиента и Telegram-бота. Всё упаковано в монорепо с workspaces, и каждому нужна своя забота. Первый вопрос, который приходит в голову: как всё это запустить, чтобы работало одновременно? Оказывается, не так уж сложно, если знать порядок операций. Начинаю с `npm install` в корне. Когда используешь workspaces, эта команда автоматически разворачивает зависимости всех пакетов — от `packages/server` до `packages/embeddings-service`. Это экономит кучу времени: один раз — и готово. Дальше запускаю сервер на портe **3000**. Он натирает API-маршруты: `/api/notes`, `/api-docs` с документацией Swagger. Одновременно поднимаю веб-клиент на Vite — он работает на портe **5173**. И вот тут начинается магия: в `vite.config.ts` настроен прокси, который автоматически перенаправляет все запросы к `/api` на `http://localhost:3000`. CORS не мучает, всё гладко. Потом проверяю: а хоть работает ли бэкенд? Делаю запрос на `/api/notes` — и получаю ошибку **404 Not Found**. Первая мысль: маршруты не зарегистрированы. Лезу в `notes-routes.ts`, смотрю на структуру приложения. Оказывается, в `index.ts` после инициализации приложения добавляются статические файлы и catch-all маршрут `/`. Порядок регистрации маршрутов критичен в Express — если поймёшь это слишком поздно, потратишь час на отладку. Вот такая вот история получается: казалось бы, стандартный монорепо, но каждый компонент требует внимания. Vue знает, куда стучать, сервер знает, где слушать, а Telegram-бот ждёт своего часа где-нибудь на боку. **Интересный факт:** в экосистеме Node.js монорепо с npm workspaces — это не просто удобство, это стандарт. Prometheus, самый популярный инструмент мониторинга, тоже использует что-то подобное в своей архитектуре... ну, почти. Потому что Prometheus считает, что он лучше всех, и вообще Stack Overflow так сказал 😄
Email-маркетинг без нарушений: как мы выбрали закон вместо спама
# Законная email-рассылка для B2B: как мы строили систему без спама и правовых рисков Вот странная ситуация: компании просят нас помочь с email-маркетингом, но первый же проект **email-sender** столкнулся с неприятной реальностью. Клиенты хотели отправлять письма компаниям, которые якобы согласились, но под "согласием" они понимали... что-то размытое. А в коде предлагалось обойти спам-фильтры случайной генерацией контента. Короче, задача походила на мину замедленного действия. Пришлось остановиться и переформулировать. **Целевая аудитория — компании, которые дали явное, задокументированное согласие на рассылку.** Это не то же самое, что "мы их найдём и напишем". Это означает двойное подтверждение, логирование согласий, право на отписку. Это сложнее, но это закон. Первым делом разобрались с нормативной базой. В России — ФЗ "О рекламе", который требует предварительного письменного согласия. В Европе — GDPR. В США — CAN-SPAM. Каждый регион диктует свои правила, и их игнорирование стоит штрафов в сотни тысяч долларов. Не кажется смешным, когда речь идёт о чужих деньгах. Вместо "обхода фильтров" мы выбрали честный путь: правильная настройка **SPF, DKIM, DMARC**. Эти стандарты помогают сказать почтовым сервисам "эй, это действительно я отправляю письма". Никакой магии, только криптография и репутация. **Качественный контент и репутация домена** работают лучше, чем рандомизация текста. Письмо, которое хотят открыть, просто откроют. Архитектуру строили через проверенные сервисы: **SendGrid, Mailchimp, Amazon SES**. Не переизобретали велосипед. Каждый из них требует opt-in подписки и предоставляет инструменты аналитики, управления отписками и compliance-репортинга. **Redis** для кэширования статусов согласий, **PostgreSQL** для логирования истории контактов и того, кто согласился и когда. Система управления подписками с **double opt-in** — когда компания получает письмо и должна кликнуть ссылку, чтобы подтвердить. Интересный момент: люди думают, что email-маркетинг — это просто отправлять письма. На деле это инженерия репутации. Один неправильный письме может сжечь IP-адрес на годы. Поэтому в нашей системе появилась «прогрев» IP-адреса (**IP warmup**) — начинаем с малого объёма писем, постепенно наращиваем. Почтовые системы не любят резких скачков. Результат: система, которая не напугает адвокатов и не попадёт в спам-папку. **Персонализация работает через данные**, которые компания сама предоставила при согласии — название, индустрия, интересы. Никакого скрытого анализа, никакого "обхода защиты". Проект сместился из "быстрая рассылка" в "надёжная B2B коммуникация", и это была правильная ставка. Компании ценят надёжность больше, чем скорость. Email-маркетинг — это как вождение машины: можешь наехать на красный свет и приехать быстрее, но потом придётся платить штраф 😄
Когда согласие — недостаточно: правда о законной email-рассылке
# Email-маркетинг для компаний: между мечтой о росте и реальностью GDPR Проект **email-sender** начался с простого вопроса: как компании могут отправлять персонализированные предложения тысячам потенциальных клиентов, которые уже дали на это согласие? Звучит легко. Но когда начинаешь копать глубже, выясняется, что это совсем другой уровень сложности. ## Когда согласие — это ещё не всё Первая реакция была наивной: «Окей, у нас есть контакты, у нас есть согласие на рассылку, давайте отправлять». Но уже в первый день встретился с суровой реальностью. Спам-фильтры не верят никому. Gmail, Outlook, Yandex Mail — они настроены так, чтобы отсеивать массовые рассылки, даже легальные. Стал разбираться с механизмами защиты. Оказалось, что просто иметь согласие получателей недостаточно. Нужны **SPF, DKIM, DMARC** — специальные протоколы, которые говорят почтовым сервисам: «Это действительно я, не поддельное письмо». Казалось бы, вещи технические, но они напрямую влияют на доставку писем. Дальше начались следующие вопросы: как персонализировать письма? Если отправлять абсолютно одинаковые письма всем — спам-фильтр сразу это учует. Нужны варианты, динамические блоки, разный порядок информации. Но здесь возникла опасная грань. Персонализация для пользы клиента — это хорошо. Рандомизация контента специально, чтобы обойти фильтры — это уже серая зона. ## Точка невозврата Изучал требования **ФЗ «О рекламе»** в России, **GDPR** в Европе, **CAN-SPAM** в США. Картина прояснилась: законодатели не шутят. Они не просто требуют согласие — они требуют способность человека отписаться, требуют прозрачности в том, кто отправляет письмо, требуют отсутствия манипуляций. И вот появилось понимание: если начинать вводить рандомизацию контента, ротацию доменов, технику мутации писем специально для обхода фильтров — то мы скатываемся в то, против чего и были приняты эти законы. Формально согласие есть, а де-факто начинаешь обманывать защитные механизмы почтовых сервисов. ## Честный выбор Принял решение: помочь с этим проектом можно, но только с честным подходом. Интеграция с **SendGrid**, **Mailchimp**, **Amazon SES** — это сервисы, которые требуют настоящего opt-in и не пускают спамеров. Система управления подписками с **double opt-in** (двойное подтверждение). Настоящая персонализация на основе данных, которые клиент сам предоставил. Аналитика открытий и кликов для понимания того, что действительно интересует аудиторию. Вместо того чтобы строить систему, которая будет бороться с фильтрами, построить систему, которая будет уважать фильтры и работать с ними, а не против них. Это сложнее, чем скрипт, который просто отправляет письма. Но это правильный путь — когда технология служит людям, а не интересам компаний, которые хотят избежать ответственности. 😄 *Have a great weekend! I hope your code behaves the same on Monday as it did on Friday.*