Авторская колонка

LMS и CRM на 205 000 строк кода силами ИИ-агентов: цена эксплуатации, три отметки лида и один символ \r

Две связанные боевые системы — образовательная платформа LMS (121 853 строки TS/TSX, 47 роутеров, 288 процедур) и CRM с единым реестром из 334 инструментов (83 251 строка) — были написаны без команды разработчиков с помощью ИИ-агентов поверх единого PostgreSQL. Написание кода заняло вечера, а главную цену выставила доводка до продакшна: 14 критических дефектов на этапе адверсарного ревью, ещё 5 при проверке собственных фиксов и невидимый символ возврата каретки \r, тихо отключивший интерактивную практику на 138 уроках из 146. Надёжная интеграция ИИ-агентов с живым бизнесом потребовала разделить работу с лидом на три независимых состояния под блокировкой FOR UPDATE, реконструировать историю диалога на сервере и сохранять свою запись встречи до вызова внешних API Zoom и Google Calendar.

LMS и CRM на 205 000 строк кода силами ИИ-агентов: цена эксплуатации, три отметки лида и один символ \r
Hamidun News.
Слушать статью
Паспорт материала и цифры
ПараметрДанные
Главный редактор
Ключевой факт #0Объём кодовой базы без команды разработчиков: 205 104 строки TypeScript/TSX (121 853 в LMS на 532 файлах и 83 251 в CRM на 367 файлах)
Поверхность API и агентного фасада47 роутеров и 288 процедур tRPC в LMS (93 инструмента) + 334 инструмента в реестре CRM (257 открыты в фасаде)
Масштаб одного невидимого бага CRLF138 из 146 практических уроков с нулём успешных отправок из-за хвостового символа `\r` при разборе на сервере
Урожай адверсарного ревью перед запуском14 опасных уязвимостей в первой волне + 5 дефектов, внесённых самими фиксами
  • Объём кодовой базы без команды разработчиков: 205 104 строки TypeScript/TSX (121 853 в LMS на 532 файлах и 83 251 в CRM на 367 файлах)
  • Поверхность API и агентного фасада: 47 роутеров и 288 процедур tRPC в LMS (93 инструмента) + 334 инструмента в реестре CRM (257 открыты в фасаде)
  • Масштаб одного невидимого бага CRLF: 138 из 146 практических уроков с нулём успешных отправок из-за хвостового символа `\r` при разборе на сервере
  • Урожай адверсарного ревью перед запуском: 14 опасных уязвимостей в первой волне + 5 дефектов, внесённых самими фиксами

Написать крупную систему силами ИИ-агентов без штата программистов сегодня стало дешёвой задачей. Дорого стоит всё, что начинается на следующее утро после демо: защита от подделки истории клиентом, блокировки в базе при двойном клике менеджеров и невидимые спецсимволы в секретах.

Обе мои системы — обучающая платформа LMS (121 853 строки TS/TSX, Next 16.2.6, React 19, Drizzle ORM, 58 миграций, 16 треков, 211 уроков и 55 симуляторов) и стоящая рядом CRM (83 251 строка, Prisma 7.6, 48 моделей БД, BullMQ и четыре подключённых ИИ-агента) — живут на одном инстансе PostgreSQL. Свой стек мы строили по двум причинам: в коробочных LMS нет примитива, где нейросеть сначала отыгрывает слабый промпт студента и лишь затем коучит по скрытой преамбуле, а CRM должна была без дублирования баз лечь поверх работающего в Telegram бота воронки. Кстати, перекраска 289 цветовых переменных при форке CRM заняла вечер, а вычищение унаследованных демо-данных (205 тестовых заявок, фолбэк «маржа 38%» и фальшивые бейджи «Задачи 7») растянулось на два месяца.

Как один невидимый символ возврата каретки \r сломал 138 уроков,

дедупликацию и OAuth?

Самый коварный баг в LMS не оставлял ошибок в логах. Студенты видели нормально отрисованный блок `:::practice`, нажимали «Отправить» и получали «Практический блок не найден». Статистика по базе показала, что на 138 уроках из 146 было ровно ноль успешных прохождений. Контент создавался на Windows с переносами `CRLF` (`\r\n`): фронтенд перед рендером нормализовал текст, а серверный парсер резал по `\n`, оставляя невидимый `\r` на конце строки:

```typescript // Без нормализации хвостовой \r ломает якорь $ в JS, // так как точка (.) не захватывает \r, и поле task парсится пустым: md = md.replace(/\r\n?/g, "\n"); const match = line.match(/^(task|starter|goal|model|system)\s:\s?(.)$/i); ```

Тот же `\r` менял контрольный хеш текста при нарезке чанков, заставляя систему заново оплачивать генерацию эмбеддингов, а в третий раз приехал в конец секрета в `.env`, из-за чего Google OAuth возвращал `invalid_client` (поймано проверкой длины строки `awk -F= '/^GOOGLE_/ {print $1, length($2)}' .env` — 36 байт вместо 35).

Почему нельзя верить данным из браузера и как устроен реестр из 334

инструментов CRM?

Адверсарное ревью перед запуском вскрыло 14 тяжёлых дефектов, а проверка написанных по ним патчей — ещё 5. В первой версии практики браузер сам присылал на сервер текст задания и массив `history`, позволяя подменить реплику через DevTools, а 8 из 55 симуляторов проходились на 71–90 баллов простым копированием условия в ответ. Мы перевели сборку преамбулы и реконструкцию 6 последних реплик строго на сервер, а таймаут грейдера подняли с 30 до 60 секунд.

В CRM вся архитектура построена вокруг единого реестра из 334 инструментов (257 открыты в агентный фасад, а 77 системных закрыты флагом `facadeExposed: false`). Любой вызов проходит единый конвейер `invoke()`: идентичность `acting_user` извлекается только из сессии (разделяя ошибки `missing` и `invalid`, когда прислали ID сессии `api-...` или отрицательный ID группового чата), а аргументы перед записью в `audit_log` чистятся регуляркой `SECRET_KEY_RE`.

Зачем делить одну кнопку лида на три состояния и как ловить встречи-сироты в

Zoom?

В живых продажах продавец пишет клиенту в личном Telegram, а параллельно того же человека догревает бот. Поэтому одну кнопку «Взять лида» пришлось разделить на три независимых состояния: «Беру в работу» (ответственный), «Связались» (факт первого касания) и «Перехвачен» (бот замолкает и уступает диалог человеку). Чтобы два менеджера не перетёрли карточку при двойном клике, CRM находит самую старую запись по `telegram_id` и берёт её под блокировку `SELECT ... FOR UPDATE`. А при создании встреч в Zoom и Google Calendar первой строкой всегда идёт `INSERT` в свою БД со статусом `creating` и полями `zoom_state` / `gcal_state`, чтобы при сбое внешнего API в облаке не оставались неучтённые встречи-сироты.

Эта механика интерактивных тренажёров и агентных реестров работает внутри [Hamidun Academy](https://academy.hamidun.com) (открыт 72-часовой триал) и в контуре [Nomad AI](https://nomadnet.ai) / [agent.nomadnet.ai](https://agent.nomadnet.ai), а архитектуру наших CRM и LMS мы разбираем в [Vibe Club](https://hamidun.com/club).

Сюжет

Вопросы и ответы по материалу

Как один невидимый символ возврата каретки \r сломал 138 уроков, дедупликацию и OAuth?

Самый коварный баг в LMS не оставлял ошибок в логах. Студенты видели нормально отрисованный блок `:::practice`, нажимали «Отправить» и получали «Практический блок не найден». Статистика по базе показала, что на 138 уроках из 146 было ровно ноль успешных прохождений. Контент создавался на Windows с переносами `CRLF` (`\r\n`): фронтенд перед рендером нормализовал текст, а серверный парсер резал по `\n`, оставляя невидимый `\r` на конце строки: ```typescript // Без нормализации хвостовой \r ломает якорь $ в JS, // так как точка (.) не захватывает \r, и поле task парсится пусты

Почему нельзя верить данным из браузера и как устроен реестр из 334 инструментов CRM?

Адверсарное ревью перед запуском вскрыло 14 тяжёлых дефектов, а проверка написанных по ним патчей — ещё 5. В первой версии практики браузер сам присылал на сервер текст задания и массив `history`, позволяя подменить реплику через DevTools, а 8 из 55 симуляторов проходились на 71–90 баллов простым копированием условия в ответ. Мы перевели сборку преамбулы и реконструкцию 6 последних реплик строго на сервер, а таймаут грейдера подняли с 30 до 60 секунд. В CRM вся архитектура построена вокруг единого реестра из 334 инструментов (257 открыты в агентный фасад, а 77 системных зак

Зачем делить одну кнопку лида на три состояния и как ловить встречи-сироты в Zoom?

В живых продажах продавец пишет клиенту в личном Telegram, а параллельно того же человека догревает бот. Поэтому одну кнопку «Взять лида» пришлось разделить на три независимых состояния: «Беру в работу» (ответственный), «Связались» (факт первого касания) и «Перехвачен» (бот замолкает и уступает диалог человеку). Чтобы два менеджера не перетёрли карточку при двойном клике, CRM находит самую старую запись по `telegram_id` и берёт её под блокировку `SELECT ... FOR UPDATE`. А при создании встреч в Zoom и Google Calendar первой строкой всегда идёт `INSERT` в свою БД со статусом

ЖХ
Жемал Хамидун — CPO Alpina Digital, продакт-менеджер AlpinaGPT. Делает AI-продукты с 2019 года. Hamidun News — pet project, который вырос в production-агрегатор с 6000+ читателей.
✦ Практика · Hamidun Academy

Соберите рабочего ИИ-агента или сервис своими руками без ручного кода

В Академии Жемала Хамидуна (academy.hamidun.com) открыт бесплатный 72-часовой тест-драйв: интерактивные скетч-уроки по Claude Code и Cursor, живой Tech Radar и готовые шаблоны агентов.

Открыть 72ч бесплатный доступ →Интенсив по Вайб-кодингу
Что вы думаете?
Загружаем комментарии…