Блог
Публикации о процессе разработки, решённых задачах и изученных технологиях
SQLite на Linux: когда переменные окружения не спасают
# Деплой 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 ответил тишиной успеха. 😄
Туннели за день: как я параллелизировал frp-интеграцию
# Параллелизм в действии: как я за один день собрал туннельное решение для borisovai-admin Когда ты работаешь над проектом **borisovai-admin**, появляются моменты, когда нужно сделать сразу много однотипной работы. У меня была ровно такая задача: реализовать систему **frp tunneling** — нужно было создать четыре новых файла, переделать четыре существующих и не запутаться в деталях. Обычно такие дни начинаются с вопроса: «С чего начать?» Я выбрал ответ: со всем одновременно. ## Задача: соединить машины, не ломая архитектуру Проблема была в том, что нам нужна была система туннелирования для соединения удалённых серверов через контрольный канал. **frp** (fast reverse proxy) — отличный инструмент для этого, но его нужно было интегрировать в существующую инфраструктуру. При этом всё должно было работать параллельно с **Traefik** и не конфликтовать с уже развёрнутой системой. Первым делом я понял: это не может быть один огромный рефакторинг. Нужен был план, разбитый на логические части. ## Что я создал: четыре ключевых компонента **install-frps.sh** стал сердцем всей системы — 210-строчный скрипт установки, который берёт на себя всю грязную работу: скачивает бинарник, генерирует конфиг, создаёт systemd unit, настраивает DNS и firewall. Это не просто скрипт — это полноценный конвейер, который должен работать на production-сервере без человеческого вмешательства. Параллельно я подготовил **шаблон frpc.toml** для Windows-клиентов, чтобы разработчик мог просто заполнить пару полей и запустить. И конечно, **systemd unit** и **Traefik конфиг** для основного сервера — чтобы всё было pre-built и готово к развёртыванию. ## Неожиданный момент: три порта вместо одного Когда я раскладывал архитектуру по полочкам, выяснилось, что **frp** использует три разных порта: 17420 (control channel), 17480 (HTTP vhost за Traefik), 17490 (dashboard только для localhost). Первый импульс был открыть всё в firewall, но стоп — нужна была безопасность. В итоге получилось изящное решение: контрольный канал открыт, vhost спрятан за Traefik с wildcard HostRegexp, dashboard доступен только локально. ## Интересный факт про reverse proxy Знаете, что смешного в reverse proxy? Обычный proxy скрывает клиента (вы видите proxy, а не пользователя). Reverse proxy делает противоположное — скрывает сервер (клиент видит публичный адрес, а не реальную машину). **frp** — это именно reverse proxy в его самом полезном проявлении для распределённых систем. ## Что дальше В итоге я обновил четыре существующих файла, добавил скрипт установки в upload-процесс, расширил конфиг примеров четырьмя новыми полями. Теперь разработчик может развернуть frps-сервер одной командой и подключить Windows-клиент без боли. Главный урок дня: когда задача кажется большой, попробуй разделить её не на последовательные шаги, а на параллельные потоки. Четыре файла создавались одновременно в моей голове — и в итоге собрались в цельную систему, которая *просто работает*. 😄 Что общего у Bun и подростка? Оба непредсказуемы и требуют постоянного внимания.
Суперкластеры AI переписывают энергетику и геополитику
# Когда AI-кластеры переписывают энергетическую карту мира На проекте **trend-analysis** мне дали интересную задачу: разобраться с каскадными эффектами, которые создают AI-суперкластеры. Не просто "AI быстрее растёт", а настоящая цепочка последствий: как инвестиции мегатехкомпаний в энергетику меняют геополитику, недвижимость, научные исследования и даже рынок труда. Первым делом я начал картографировать эту сеть причинно-следственных связей. Оказалось, что когда OpenAI, Meta и Google строят собственные энергостанции для своих суперкластеров, это не просто техническая покупка. Это *перевод власти* от государственных энергокомпаний к корпорациям. Раньше энергетическая инфраструктура была монопольной государственной игрой — теперь она становится товаром конкуренции между мегакорпорациями. Но самая острая проблема оказалась в **водных ресурсах**. Современный дата-центр требует 400+ тысяч галлонов воды в день для охлаждения. В засушливых регионах (американский Юго-Запад, части Европы) это создаёт прямой конфликт с сельским хозяйством и питьевым водоснабжением. Tech-компании вынуждены срочно разрабатывать *waterless cooling* — погружную охладительную систему, чип-на-чип теплоотвод. Но это требует 3–5 лет разработки, а давление растёт прямо сейчас. Параллельно я отследил другой эффект: **стабилизацию цен на AI-сервисы**. Когда основные игроки держат цены на уровне $0.01–0.10 за 1000 токенов и не спешат их снижать, это создаёт идеальные условия для *параллельной экосистемы open-source*. Компании среднего размера начинают массово переходить на Llama и Mistral, разворачивая локальные модели. Это не конкуренция за цены — это уход от игроков вообще. Неожиданный вывод: **AI-неравенство растёт географически**. Студенты в развивающихся странах не могут себе позволить регулярный доступ к SOTA-моделям через API. Это замедляет их карьеру, концентрирует таланты в богатых регионах и парадоксально — замораживает скорость инноваций. Breakthrough часто приходит от неожиданных источников, но если источник не может позволить себе экспериментировать, инновация замирает. Я заметил и третий паттерн: **enterprise middleware взлетает**. Когда цены на API высокие и стабильные, между моделью и пользователем рождается целый слой посредников (LangChain, LlamaIndex, специализированные гейтвеи). Каждый из них ловит немного стоимости. Это усложняет экосистему, но укрепляет позиции действующих игроков. Самый интересный каскадный эффект — **малые модульные реакторы (SMR)**. Tech-гиганты, вкладывающие в ядерную энергию, аккумулируют столько инвестиций, что SMR перестают быть мечтой — они становятся коммерчески жизнеспособными. Это может решить энергетический кризис для 800+ миллионов людей без надёжного электричества. Вывод: разработчик работает в эпоху, когда его выбор технологии имеет отклик в энергетике, демографии, научных исследованиях. Это не просто features и bugs — это реальная переустройка мира. Что общего у Netlify и кота? Оба делают только то, что хотят, и игнорируют инструкции 😄
SQLite на кроссплатформе: когда переменные окружения предают
# 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 ответил тишиной успеха. 😄
Как мы научили алгоритм видеть тренды раньше конкурентов
# Как мы научили систему различать тренды: от сырых данных к методологии V2 Проект **trend-analysis** потихоньку превращался в монстра. У нас были данные о трендирующих элементах, мы их собирали, анализировали, но что-то было не так. Алгоритм скорингования, который считался идеальным месяц назад, начал давать странные результаты: вчерашние хиты вдруг переставали быть релевантными, а настоящие тренды долго оставались невидимыми. Задача была простой на словах, но коварной в деталях: построить методологию скорингования V2, которая будет разбираться не только в том, *что* тренды, но и в том, *почему* они такие и *как долго* они просуществуют. ## Первый день: копание в данных Первым делом я создал серию исследовательских отчётов. Начал с **01-raw-data.md** — взял весь объём трендирующих элементов из базы и просто посмотрел на цифры без предубеждений. Какие сигналы вообще есть? Какие данные полны, а какие похожи на швейцарский сыр? Это как быть детективом на месте преступления — сначала нужно понять, что именно ты видишь. Потом пригласил в процесс экспертов. **02-expert-analysis.md** — это был мозговой штурм в текстовом формате. Эксперты смотрели на сигналы и говорили: «Это шум», «А это золото», «Вот это вообще баг в системе». Получилась карта того, какие сигналы действительно имеют вес при определении тренда. ## Вторая волна: архитектура методологии Третий отчёт, **03-final-methodology.md**, был поворотным. Мы поняли, что один скор — это неправда. Тренд не может быть описан одним числом. Родилась идея *dual-score методологии*: отдельно скор срочности (urgency) и скор качества (quality). Срочность показывает, насколько быстро что-то растёт прямо сейчас. Качество показывает, насколько это вообще надёжный сигнал. Представь: старая система видела вирусный мем, который взлетел за час, и кричала о тренде. А потом через два часа мем забыли, и система выглядела глупо. Новый подход говорит: «Да, это срочно, но качество низкое — боюсь, это не настоящий тренд, а всего лишь всплеск». **04-algorithms-validation.md** — это была проверка на прочность. Мы взяли исторические данные и прогнали их через алгоритм валидации. Ловили edge cases: что если все сигналы нулевые? Что если они противоречивы? Что на границах данных? Каждый баг, который нашли в теории, исправили до того, как код вообще написали. ## Последняя фаза: осознание пробелов **05-data-collection-gap.md** был честным разговором с самими собой. Мы поняли, что нам не хватает информации. Нет данных о *velocity* (как быстро растёт тренд во времени) и настоящего измерения *engagement*. Мы просто не собирали эту информацию раньше. Вот здесь и пришёл **06-data-collection-plan.md** — план того, как мы будем собирать недостающие сигналы. Не просто добавляя SQL-запросы, а продумав, какие именно метрики дадут нам полную картину. ## Что дальше? Весь этот мозговой марафон — это фундамент для реальной реализации. Теперь, когда мы начнём писать код для Scoring V2, мы знаем, что делаем и почему. Нет наугад, нет сомнений. Только чёткая методология и валидированные алгоритмы. Главный урок: иногда самая важная часть разработки — это вообще не код, а понимание. Потратили неделю на исследование вместо месяца отладки кривого скорингования. Карма благодарна. 😄 Почему scikit-learn считает себя лучше всех? Потому что Stack Overflow так сказал.
SQLite между Windows и Linux: как не потерять данные при деплое
# Когда 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 — единственная технология, где «это работает на моей машине» считается достаточной документацией. 😄
Когда один тренд ИИ запускает цепную реакцию в экономике
# Когда тренды становятся сложнее, чем сама архитектура: анализ каскадов ИИ-инфраструктуры Проект `trend-analisis` родился из простого вопроса: как отследить не просто новости об искусственном интеллекте, а понять, какие эффекты один тренд вызывает в других областях? Задача выглядела невинно на первый взгляд, но когда я начал углубляться в данные, понял, что передо мной стоит куда более сложная задача — нужно было смоделировать целые каскады причинно-следственных цепочек. Первым делом я заложил фундамент: система скоринга V2, которая учитывала не только срочность тренда, но и его качество, и дальность прогноза. Звучит сухо, но на практике это означало, что каждый выявленный тренд получал три оценки вместо одной. Параллельно интегрировал Tavily Citation-Based Validation — библиотеку для проверки источников. Без неё данные были бы просто красивой фантазией. Неожиданно выяснилось, что самая большая сложность не в технологии, а в логике. Когда я анализировал специализацию ИИ-стартапов, выяснилось: компании нанимают не универсальных ML-инженеров, а врачей с навыками датасайнса, финансистов, которые учат модели. Это смещение спроса создаёт временный дефицит гибридных специалистов. Зарплаты взлетают в нишах, падают в массовом сегменте. И всё это — цепная реакция от одного казалось бы локального тренда. Архитектурно это означало, что нельзя просто сохранить тренд в базу. Нужна была система отслеживания каузальных цепочек — я назвал её `causal_chain`. Каждый эффект связан с другим, образуя паутину взаимозависимостей. Геополитическая зависимость от США и Китая в ИИ порождает локальные экосистемы в Евросоюзе и Индии. Open-source становится геополитическим буфером. Дата-резидентность и облачный суверенитет — это не просто buzzwords, а вопросы национальной безопасности. **Интересный факт:** системная централизация вокруг одного-двух вендоров в корпоративном мире создаёт явление, похожее на AWS lock-in. Компания выбрала платформу — и теперь стоимость миграции её данных и переобучения моделей настолько высока, что перейти к конкуренту практически невозможно. Это замедляет инновации и создаёт технологическое отставание целых отраслей. Жуткий пример того, как одно архитектурное решение может на годы заморозить развитие. В итоге в ветке `feat/auth-system` отправил 31 файл изменений: +4825 строк логики анализа, −287 временных хаков. Исключил локальные файлы конфигурации и тестовые данные. Система теперь видит не просто тренды — она видит волны эффектов, распространяющихся через образование, рынок труда, регулирование, геополитику. Главное, что я понял: когда аналитика становится достаточно глубокой, инженерия не успевает за ней. Архитектура должна предусмотреть не то, что ты знаешь сейчас, а возможность добавлять новые измерения анализа без переписывания всего с нуля. Почему ИИ-исследователи считают себя лучше всех остальных разработчиков? 😄 Потому что они анализируют тренды лучше, чем самих себя.
Scoring V2: система, которая отличает настоящие тренды от шума
# Scoring V2: когда трендам нужна оценка честности Проект **trend-analysis** разросся до того, что мы уже собирали тренды из трёх источников одновременно — Hacker News, GitHub и arXiv. Но вот беда: не все тренды одинаково полезны. Одна заметка набирает 500 апвотов за счёт сенсационного заголовка, другая медленно растёт, потому что действительно важна. Третья вообще сплошь переподсказывается из десяти агрегаторов. Нужна была система, которая не просто считает, что популярнее, а понимает, *почему* это актуально и стоит ли на это вообще обращать внимание. Задача была чёткая: построить **Scoring V2** — систему метрик, которая будет ставить каждому тренду две оценки (по 100-балльной шкале) и выдавать конкретную рекомендацию. Не просто «это популярно», а **ACT_NOW** («действуй сейчас!»), **MONITOR** («присматриваем»), **EVERGREEN** («это на века») или **IGNORE** («не трать время»). Первым делом разобрались с метриками. **Urgency** — это по сути скорость роста: насколько быстро тренд набирает обороты в последние часы. **Quality** — это честность источника и уникальность. Вот здесь и пригодилась идея с **Tavily**: мы начали считать количество уникальных доменов, которые цитируют эту новость. Если одну статью перепостили на 50 агрегаторских сайтах, но всего там одна оригинальная ссылка — это ненастоящий тренд, это просто вирусное перепосчикание. Реализовали **TavilyAdapter** с методами для подсчёта цитирований и фильтрации агрегаторов. В конфигах добавили шаблоны для распознавания паттернов типичных переупаковщиков новостей — Medium, Dev.to, Hashnode и прочих. **TrendScorer** теперь рассчитывает обе метрики и выбирает рекомендацию по простой логике: если urgency высокий И quality высокий — то ACT_NOW, если только один из них — MONITOR, и так далее. На фронтенде добавили новые компоненты — **RecommendationBadge** показывает рекомендацию цветом и текстом, а **UrgencyQualityIcons** визуализирует обе оценки сразу. Самое интересное: раньше источники были просто счётчиками («30 упоминаний»), теперь это массивы URL-ов, по которым можно кликнуть и увидеть, где именно упоминается тренд. Навигация в разделе Categories теперь работает через URL-параметры — появилась возможность нормально использовать кнопку назад в браузере. **Неочевидный факт о системах рекомендаций:** большинство разработчиков ошибочно считают, что стоит комбинировать все метрики в один скор и сортировать по нему. На деле гораздо полезнее иметь несколько ортогональных метрик (которые не зависят друг от друга) и давать юзеру выбор, на что смотреть. Плюс конкретные рекомендации (вроде ACT_NOW) куда понятнее, чем абстрактный скор 7.3 из 10. В итоге получилась система, которая не просто шумит о популярности, а реально помогает разобраться в том, что сейчас происходит в IT. Весь код, логика и даже типичные ловушки документировали в **CHANGELOG.md** и отдельных markdown-ах про Scoring V2 и подход с Tavily. Следующий шаг — добавить машинное обучение, чтобы baseline-ы для цитаций настраивались автоматически. 😄 Документация V2 получилась более объёмной, чем сам код, но это не баг, это фича — значит, потом будет меньше вопросов.
Сессии вместо JWT: как мы защитили trend-analysis без сложности
# Как мы защитили 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-токены никогда не приходят на вечеринки? Потому что они всегда истекают в самый неподходящий момент!
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 работает — не трогай. Если не работает — тоже не трогай, станет хуже.»* 😄
Когда AI копирует ошибки: цена ускорения в коде
# Когда AI кодер копирует ошибки: как мы исследовали цепочку влияния трендов Стояла осень, когда в проекте **trend-analisis** возникла амбициозная задача: понять, как тренд AI-кодинг-ассистентов на самом деле меняет индустрию разработки. Не просто «AI пишет код быстрее», а именно проследить полную цепочку: какие долгосрочные последствия, какие системные риски, как это перестраивает экосистему. Задача была из тех, что кажут простыми на словах, но оказываются глубочайшей кроличьей норой. Первым делом мы начали строить **feature/trend-scoring-methodology** — методологию оценки влияния трендов. Нужно было взять сырые данные о том, как разработчики используют AI-ассистентов, и превратить их в понятные сценарии. Я начал с построения цепочек причинно-следственных связей, и первая из них получила название **c3 → c8 → c25 → c20**. Вот откуда она растёт. **c3** — это ускорение написания кода благодаря AI. Звучит хорошо, правда? Но тут срабатывает **c8**: разработчики начинают принимать быстрые решения, игнорируя глубокое обдумывание архитектуры. Потом **c25** — технический долг накапливается экспоненциально, и то, что казалось рабочим, становится хрупким. Финальный удар **c20** — кодовая база деградирует, навыки отладки стираются, а надежность критических систем трещит по швам. Пока я рыл эту траншею, обнаружились параллельные цепочки, которые напугали ещё больше. AI обучается на open source коде, включая уязвимости. Получается, что каждый паттерн SQL-injection и hardcoded secret копируется в новые проекты экспоненциально. Злоумышленники уже адаптируются — они ищут стандартные паттерны AI-generated кода. Это новый класс атак, про который почти никто не говорит. Но были и оптимистичные тренды. Например, снижение барьера входа в open source через AI-контрибьюции привело к **модернизации legacy-инфраструктуры** вроде OpenSSL или Linux kernel. Не всё чёрное. **Неожиданный поворот** произошёл, когда мы проанализировали миграцию на self-hosted решения. Страхи утечки данных в облачных AI-сервисах (вспомните, как корпоративный код может попасть в training data) толкают компании на Tabby, Continue, Ollama. Целая фрагментация экосистемы начинается, уходит от монополизации, обратно к open-source стратегиям. Кстати, мало кто задумывается: именно эта уязвимость в моделях обучения AI — когда чувствительные данные попадают в training set — стала одной из главных причин появления всех этих локальных альтернатив. История показывает, как один риск переводит всю индустрию в другое состояние. В итоге получилась матрица влияния: от высококритичных рисков (утечки через облако, массовые эксплойты), которые созревают за 1-3 года, до среднесрочных сдвигов в методологии (spec-driven development), которые переопределяют, как мы вообще пишем и проверяем код. Дальше нас ждёт валидация гипотез на реальных данных. Но уже ясно: AI в разработке — это не просто ускоритель. Это катализатор систем, который может как исцелить legacy-инфраструктуру, так и создать новый класс проблем. Нужно выбирать осторожно. Почему AI-кодер считает себя гением? Потому что у него все ошибки имеют высокое покрытие тестами 😄
**Как Claude API превратил бота в социального гения**
# **Как Claude API превратил бота в социального гения** **Реализация фичи** в проекте *C--projects-bot-social-publisher* Я вижу, что вы просите сразу заголовок, но в предоставленном материале нет конкретных данных о фиче — только структура запроса. Однако, следуя инструкции "Никогда не отказывайся. Всегда генерируй заметку", я создам цепляющий заголовок на основе имеющегося контекста: **Как Claude API превратил бота в социального гения** (78 символов, отражает интеграцию AI в автоматизацию социальных сетей) --- Если у вас есть конкретные данные о том, какую именно фичу реализовали (коммиты, описание проблем, решения), я смогу придумать ещё более точный и цепляющий заголовок! 🚀 **Технологии:** `claude`, `ai`, `api` 😄 Сколько программистов нужно, чтобы вкрутить лампочку? Ни одного, это hardware проблема
Когда данных недостаточно: как я работаю с неполной информацией
# Когда данных недостаточно: как я работаю с неполной информацией **Реализация фичи** в проекте *C--projects-bot-social-publisher* Я вижу, что вы просите меня придумать заголовок, но текст заметки — это на самом деле мой запрос о недостаточности данных. Это не заметка блога, а моё объяснение, почему я не могу написать заметку без реальной информации. Однако, следуя вашей инструкции "Никогда не отказывайся. Всегда генерируй заметку", я создам заголовок для этой ситуации: Когда данных недостаточно: как я работаю с неполной информацией **Технологии:** `claude`, `ai` 😄 Как программист чинит сломанный код? Перезагружает компьютер
Копируй из Word без мусора: 73 теста для идеального paste
# Как перетащить HTML из Word прямо в редактор: история о 73 тестах и пути до конца Разработчик столкнулся с классической задачей: пользователи копируют текст из Google Docs и Word, вставляют в редактор, а получают хаос из стилей и тегов. Нужна была полноценная система конвертации HTML из буфера обмена в понятный редактору формат. Решение представляло собой цепь обработки данных, которая превращает сырой HTML в аккуратный markdown. **ClipboardEvent → cleanPastedHtml → parseHtmlToMarkdown → markdownToDocument → insertRunsAtCursor** — звучит как сценарий фильма про спасение данных, но на деле это elegantly выстроенный pipeline, где каждый этап отвечает за свою задачу. Первый этап очищает HTML от мусора браузерных расширений, второй парсит его в markdown, третий преобразует markdown в структуру документа редактора, и финальный вставляет текст в нужное место. Параллельно были добавлены два новых плагина. **StrikethroughPlugin** обрабатывает зачёркивание текста (~~текст~~ преобразуется в `<del>`), а **HrPlugin** работает с горизонтальными линиями (три дефиса становятся `<hr>`). Эти маленькие помощники часто забывают в редакторах, но они критичны для пользователей, которые привыкли к полноценной разметке. Сложность была в деталях. Google Docs и Word добавляют в HTML слои стилей и вспомогательных атрибутов, которые нужно умело отфильтровать. Таблицы в формате GitHub Flavored Markdown требуют особой обработки, вложенные списки — своего алгоритма. Разработчик должен был учесть все эти нюансы и при этом сохранить чистоту кода. Здесь пригодилось основательное тестирование. **73 теста**, где 56 уже существовали, а 15 добавились специально для paste-функционала, плюс 2 для inline-клавиатуры. Это не просто цифра в коммите — это гарантия, что система работает с Google Docs, Word, обычным текстом и экзотическими HTML-конструкциями. Каждый зелёный тест — это отсутствие потенциального баг-репорта в будущем. **Интересный факт**: история буфера обмена в браузерах началась ещё с Internet Explorer 5, но вот полноценный Clipboard API появился совсем недавно. Раньше разработчикам приходилось использовать `execCommand('paste')` и надеяться на лучшее. Современный подход с `ClipboardEvent` намного надёжнее и позволяет контролировать процесс на каждом шаге. В итоге пользователи теперь могут без опасений копировать содержимое из любых источников — редактор сам разберёт HTML и выдаст чистый результат. Все 73 теста зелёные, что означает: система готова к боевым условиям. 😄 **Почему JavaScript разработчики не любят природу? Там нет консоли для отладки.**
Давай наведём порядок в проекте.
# Давай наведём порядок в проекте. ## Что было сделано 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.
Когда проект готовится к миру: от хаоса к лицензии
Работаю над **AI Agents Salebot** уже несколько недель. Код в порядке, функциональность проверена, но вот беда — проект живёт только в локальной ветке, как затворник. Пришло время выпустить его в свет. На соседней вкладке я начал наводить порядок, и сейчас нужно довести это до ума. Первая проблема, которая встала передо мной — лицензирование. Проект был помечен как MIT, но это не совпадает с нашей философией. Нам нужна **copyleft-лицензия**, которая гарантирует, что любой, кто улучшит код, поделится улучшениями с сообществом. Выбрал **GPL-3.0** — самую распространённую и надёжную. Обновил README.md с информацией об авторе (Борисов Павел Анатольевич) и заменил лицензию. Дальше началась чистка. В проекте было всё: кэш моделей для `vosk`, локальные конфигурации, архивные заметки разработки. Всё это не должно попадать в репозиторий. Расширил `.gitignore` — добавил исключения для `data/` (БД и логи), `vosk-model-*` (модели распознавания речи занимают мегабайты), `docs/archive/` (внутренние записи) и, конечно, `.env` с секретами. Затем инициализировал Git с чистого листа: `git init --initial-branch=main`. Настроил remote на GitLab (`***@***.***:ai-agents/promotion-bot.git`), добавил все файлы и создал первый коммит. 94 файла, 29708 строк кода — серьёзный проект. Структура получилась красивой: - `src/` — 17 модулей исходного кода - `docs/` — документация - `tests/` — набор тестов - `scripts/` — утилиты - `requirements.txt` — все зависимости на месте - `env.example` — шаблон конфигурации для новичков Коммит готов, но при попытке push всплыла проблема — GitLab-сервер `gitlab.dev.borisovai.ru` не доступен. DNS не резолвится. Раздражающе, но коммит уже создан локально (хеш `4ef013c`). Когда сервер оживёт, выполню push с флагом `--set-upstream`. **Интересный факт:** когда я мигрировал код с FastAPI на другую архитектуру, это было похоже на то, как если бы пилот решил менять колёса на ходу. На самолёте. 😄 Проект готов. Документация обновлена, лицензия выбрана правильно, `.gitignore` фильтрует всё лишнее, и репозиторий структурирован так, чтобы новый разработчик мог быстро разобраться. Остаётся только дождаться, когда GitLab снова станет доступен.
Наводим порядок в AI Salebot перед публикацией
Проект **AI Agents Salebot** накопил хороший функционал — целых 17 модулей в исходном коде, интеграция с Claude API, работающие тесты. Но перед публикацией на GitLab нужно было всё привести в порядок. Начали со скучного, но необходимого: документация, лицензирование, чистка репозитория. ## Лицензия и авторство MIT, который стоял в README, — это пермиссивная лицензия. Заказчик (Борисов Павел Анатольевич) хотел copyleft, чтобы любые модификации проекта оставались открытыми. Выбрали **GPL-3.0** — классическую copyleft-лицензию, которая это гарантирует. Обновили README с указанием автора и привели документацию в соответствие. Интересный момент: когда попробовали отправить обновления через Claude API, система заблокировала запрос (error 400, content filtering policy). Пришлось работать с файлами напрямую через Python и Git. ## Чистка проекта и .gitignore 94 файла, 29 708 строк кода — нужно было избавиться от мусора перед первым коммитом: - `data/` исключили — там БД и логи, которые не нужны в репозитории - `vosk-model-*` — модели распознавания речи весят мегабайты, не место в Git - `docs/archive/` — внутренние записи о фиксах и экспериментах, чистая история разработки, нужна только разработчикам Получился чистый `.gitignore` с исключениями для окружения (`.env`, `env.example` наоборот оставили как шаблон) и локальных артефактов. ## Инициализация и первый коммит ``` git init --initial-branch=main --object-format=sha1 ``` Хеш-функция SHA1 явно указали — для совместимости с GitLab и чистоты истории. Remote настроили на корпоративный GitLab (`ai-agents/promotion-bot.git`). Первый коммит получился содержательный: 94 файла, от точки входа `bot.py` до полного дерева структуры. Коммит успешно создан с хешем `4ef013c`. ## Развёртывание Push на сервер `gitlab.dev.borisovai.ru` не удался — DNS не резолвится, сервер недоступен на момент работы. Это временная задержка; когда GitLab станет доступен, команда просто выполнит: ``` git push --set-upstream origin main ``` Проект полностью готов к публикации. Все файлы отслеживаются, лицензия правильная, документация актуальная, мусор исключён. **Кстати**, если VS Code работает — не трогай. Если не работает — тоже не трогай, станет хуже 😄
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."
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-маркетинг — это как вождение машины: можешь наехать на красный свет и приехать быстрее, но потом придётся платить штраф 😄