GPTWEB: чиним блог-генератор и проектируем админку v3.0
GPTWEB — мой AI-агрегатор с биллингом для российского рынка (ask.gptweb.ru). Под капотом — форк LobeChat с YooKassa-интеграцией, Better Auth и кастомной локализацией, плюс standalone-админка на Next.js 16 за Caddy, Telegram-бот и лендинг. На прошлой неделе всё это разом подкинуло мне три параллельные задачи: после переезда сервера сломались автоматический блог-генератор и динамические тарифы на лендинге, а сверху упал запрос на новую версию админки — v3.0 с детальной аналитикой по каждому пользователю. В этой статье собираю всё в одну историю: как я диагностировал обе регрессии, какие архитектурные решения принял для админки и что вынес из исследования AI-провайдеров для wrapper-сервисов.
Контекст: что такое GPTWEB и из чего он состоит
GPTWEB — это classic wrapper-сервис: пользователь платит рублями через YooKassa, получает баланс в токенах и доступ к десяткам моделей (Claude, GPT, Llama, Mistral и так далее) через единый интерфейс. Ниже капота — четыре репозитория:
- aggregator — основа на форке LobeChat. Next.js 16 + Drizzle ORM + tRPC. База — ParadeDB поверх PostgreSQL 17 в Docker-контейнере.
- admin — отдельная админка по пути
/admin. Standalone-сборка Next.js, контейнер на 3400 порту, проксируется через Caddy. - bot — Telegram-уведомления (баланс, оплаты, ошибки).
- landing — статический промо-сайт, читает тарифы через REST.
Ключ в том, что тарифы и контент блога админка отдаёт наружу: лендинг динамически подтягивает планы из таблицы billing_plans, а блог — из ai_aggregator.blog_posts. Сломалась админка — слетел и публичный сайт.
Проблема №1: тарифы пропали из админки и сломались на лендинге
Симптом: на лендинге вместо тарифов — заглушка, в админке нет страницы редактирования планов. Пошёл по follow-the-data: API-ручки на месте, данные в БД на месте, проблема где-то в UI.
Проверил /api/billing/plans (GET и PUT) — рабочие, возвращают free/basic/pro корректно. Глянул components/sidebar.tsx — секции «Биллинг» нет. Полез в историю app/(admin)/billing/page.tsx и обнаружил находку: в Phase 3 рефакторинга админки эту страницу превратили в банальный redirect("/finance/payments"). То есть страница CRUD тарифов была физически удалена, остался только редирект — и его никто не заметил, потому что кнопки в сайдбаре тоже не было.
// app/(admin)/billing/page.tsx (было после Phase 3)
import { redirect } from "next/navigation";
export default function BillingPage() {
redirect("/finance/payments");
}Классический случай неполного рефакторинга: финансовую секцию переделали и красиво сгруппировали (date-range picker, KPI-карточки, графики на Recharts, детализация расходов по каналам), но управление тарифами при этом просто потеряли. Не критическое — billing_plans всё это время были в БД. Критическое — лендинг читает из этой таблицы, и никто не может править цены без прямого SQL.
Фикс — восстановить /admin/billing/plans как полноценную CRUD-страницу: список планов, редактор лимитов и цены в рублях, кнопка «активировать/деактивировать», ссылка обратно в Финансы. Сайдбар возвращает группу «Тарифы» с тремя пунктами: Планы, Платежи, Себестоимость. Логически это правильнее, чем размазывать биллинг по двум разным разделам.
Проблема №2: блог перестал писать черновики
Второй симптом: автогенератор блога не создаёт новые статьи. У GPTWEB настроен пайплайн «cron → API-ручка → генерация черновиков → ручное ревью → публикация». Ходить за тёмным углом и спрашивать, что сломалось, бесполезно — нужны логи.
Запросил статусы systemd-таймеров на VPS:
systemctl status blog-generate.timer blog-keywords.timer blog-sync.timer
systemctl status blog-generate.serviceКартинка получилась грустная: все три таймера срабатывают, все три сервиса в состоянии failed с кодом status=203/EXEC. Это очень специфическая ошибка — systemd просто не может найти исполняемый файл, на который ссылается ExecStart.
Открыл unit-файлы:
# /etc/systemd/system/blog-generate.service
[Service]
WorkingDirectory=/home/deploy/projects/ai-aggregator
ExecStart=/home/deploy/projects/ai-aggregator/scripts/blog-generate.shИ всё стало ясно. После переезда на новый сервер и реорганизации проектов директория ai-aggregator была переименована в ai-aggregator-lobechat. Скрипты вместе с проектом «уехали» — а юнит-файлы systemd остались с прежними путями. Таймеры сработали, нашли несуществующий путь, упали, повторили. Тихая регрессия: ни ошибок в основном приложении, ни падений Next.js, ни 500-ок в API. Просто черновики магически перестают создаваться по ночам.
Решение в три действия:
- Восстановить три скрипта (
blog-generate.sh,blog-keywords.sh,blog-sync.sh) из приватного репоbugle-c/ai-aggregator— на сервере их физически нет, потому что после переименования папки они туда так и не попали. - Поменять в unit-файлах
WorkingDirectoryиExecStartна новый путьai-aggregator-lobechat. - Сделать
systemctl daemon-reloadи руками тригернуть все три таймера, чтобы убедиться, что фикс рабочий.
Первым прогнал blog-keywords (он самый лёгкий — просто пинг к API подбора SEO-ключей). Зелёный exit code, статья получила ключевики. Дальше blog-sync — отработал и подтянул одну статью из текущей очереди в боевую таблицу. Третий, тяжёлый blog-generate, запустил в фоне: он стучится в Claude API, генерит структурированный черновик и пишет в blog_posts со статусом draft. Через пару минут проверил БД — черновик появился. Чиним.
Урок: переезд сервера — это не только rsync файлов
Прежде чем перейти к админке v3.0, выпишу для себя главное правило по этой регрессии. Когда мигрируешь рабочий сервер, недостаточно скопировать файлы и поднять Docker — есть скрытый слой состояния:
- systemd-юниты в
/etc/systemd/system/ - cron-таблицы в
/var/spool/cron/ - переменные окружения в
.envфайлах сервисов - права на файлы и владельцы (UID/GID могут не совпадать между серверами)
- самописные скрипты где-нибудь в
/home/deploy/scripts/
Ни один из этих артефактов не лежит в Git и не входит в стандартный backup приложения. После любой миграции нужно делать обход: systemctl list-timers, crontab -l для всех пользователей, ls /etc/cron.d/, find /etc/systemd/system -name '*.service'. Сейчас завожу этот чек-лист как часть dokploy-deploy скилла и в KNOWLEDGE.md проекта.
Админка v3.0: проектирование
Пока чинил блог, параллельно стартовал бренсторм по третьей задаче — админке версии 3.0. Цель пользователя сформулирована широко: «глубокая аналитика пользователей, финансы, моя себестоимость на каждого пользователя». Это не один экран — это переосмысление всей админки от dashboard'а до per-user страницы.
Первый принципиальный вопрос — что считать единицей анализа. Я предложил три фокуса:
- A. Юнит-экономика: для каждого пользователя выручка минус моя себестоимость = маржа. Кто прибыльный, кто сливает деньги в минус.
- B. Поведение: какие модели использует, когда активен, какие промпты, кто power-user.
- C. Финансовый pulse: aggregate-метрики бизнеса по дням/месяцам.
Выбран путь «все три равнозначны». Это означает, что dashboard будет иметь три верхнеуровневых блока, и каждый из них раскрывается в отдельный раздел.
Ключевая проблема: я не знаю свою себестоимость
Самое интересное вылезло на втором вопросе. Чтобы посчитать «моя себестоимость на пользователя X в апреле», нужно знать: сколько он сделал запросов, к каким моделям, сколько input/output токенов и какая у каждой модели цена в $/1M токенов на тот момент.
Проверил, что есть в текущей БД:
| Что есть | Чего не хватает |
|---|---|
billingPayments — все платежи с YooKassa ID и суммой | Нет per-request лога с model, input_tokens, output_tokens, user_id, ts |
userBilling.tokensUsedMonth — агрегат месяца | Нет истории по дням/моделям |
model-rates.ts — прайс моделей в $/1M | — |
messages.model — модель сообщения | Без счётчиков токенов |
recordTokenUsage() — функция есть и вызывается | Но пишет только агрегат, не лог |
Итог: текущая схема позволяет узнать, сколько пользователь потратил в общем, но не позволяет разложить это по моделям и посчитать реальную себестоимость. То есть ключевая фича v3.0 — невозможна без миграции схемы. Решено: стартуем с чистого листа, заводим таблицу usage_logs с полным контекстом каждого запроса.
По масштабу — выбрали baseline на текущий объём (менее 100 активных пользователей, до 1000 запросов/день, обычная таблица + индексы по user_id и created_at), но с прицелом на рост: индексы сразу под будущие выборки, готовность отделить таблицу-rollup для дашбордов и schema-ready под партиционирование, если придётся. Это применение паттерна YAGNI с одним «но» — закладываемся не на функциональность, а на структуру, которую больно менять задним числом.
Параллельный ресёрч: какой провайдер LLM использовать
В ту же сессию пользователь попросил параллельно исследовать рынок AI-провайдеров для wrapper-сервиса. Запустил двух subagent'ов: один на текстовые модели, второй на image/video/audio. Оба упали с ошибкой — у subagent'ов в политике не было доступа к WebSearch и WebFetch. Полез в ~/.claude/settings.json, добавил оба инструмента в user-global allowlist через скилл update-config (это правильное место — глобальные пермишены применяются и к subagent'ам), перезапустил.
По текстовым моделям картина такая (апрель 2026, цены $/1M input/output):
- OpenRouter — нулевая наценка относительно direct-цены провайдеров, но мультибрендинг и единый ключ. Для wrapper'а это идеал.
- AI/ML API — 30% наценка, зато прямое пополнение криптой (актуально из РФ).
- ProxyAPI — ~3x наценка относительно direct, но рублёвый эквайринг.
- Together, Fireworks, DeepInfra — для open-source моделей (Llama, Mixtral) дешевле, чем у direct-провайдеров.
Вывод по тексту: для основного потока — OpenRouter, для open-source-моделей с приоритетом на цену — DeepInfra или Together как fallback.
По картинкам/видео/аудио: Fal.ai и Replicate — два кита, поверх них WaveSpeed (быстрый inference на коротких задачах) и Runware. Для специализированных моделей (Suno для музыки, ElevenLabs для голоса) — direct API. Mix-and-match выгоднее, чем один универсальный провайдер.
Выводы
Первый урок — рефакторинг без чек-листа теряет фичи. Phase 3 админки переделала финансовую секцию красиво, но управление тарифами просто исчезло. Никто не заметил, потому что лендинг продолжал работать (БД-то не трогали), а в админке для редактирования цен ходят раз в полгода. Я завёл правило: при любом нетривиальном рефакторинге админ-страниц делать diff app/(admin)/** до и после, и для каждой страницы, которую заменили на redirect(), явно подтверждать в плане, куда переехала функциональность. Если функциональность нигде — это красный флаг, не молчаливый «потом добавим».
Второй урок — миграция сервера ломает то, чего не видно. systemd-таймеры, cron-job'ы, самописные скрипты в произвольных папках — всё это не часть Docker-образа и не часть Git. После переезда (особенно если попутно реорганизовал директории) все эти зависимости нужно перепрописывать. У меня в результате этого инцидента появился пункт в дokploy-чек-листе: после миграции пройтись по systemctl list-timers --all, crontab -l для каждого пользователя и ls /etc/cron.d/ — и для каждой найденной единицы убедиться, что пути и переменные актуальны. Дёшево и предотвращает «тихие» регрессии.
Третий урок — данные собирай заранее, считать «по агрегату» — тупик. История с себестоимостью v3.0 показательная: я знаю, сколько пользователь потратил, и знаю прайс моделей, но не знаю, какой моделью он пользовался — потому что в БД нет per-request лога. Любая wrapper-аналитика рано или поздно упирается в этот узкий тренинг: если ты не пишешь usage-лог с момента запуска, переход на детальную метрику = миграция данных назад во времени, что обычно невозможно. Завожу правило: для любого сервиса с pay-per-use всегда сразу есть таблица usage_logs с user_id, ресурс, quantity, cost, ts. Пишется при каждом тарифицируемом действии. Без этого нет ни диагностики, ни оптимизации, ни честной юнит-экономики.
Четвёртый урок — multi-provider стратегия для AI выгоднее single-provider. Это контринтуитивно (хочется один SDK, один ключ, одна биллинг-точка), но реальность: разные модели имеют разную экономику у разных хостов. OpenRouter решает 80% случаев бесплатным агрегированием, но для open-source моделей DeepInfra даёт 30-50% дешевле. Для картинок Fal быстрый, Replicate универсальный, WaveSpeed — самый дешёвый на конкретных диффузионных моделях. Wrapper-сервис выигрывает, когда умеет роутить запросы к самому дешёвому из доступных провайдеров для каждой конкретной модели и держит fallback'и на остальных. Это сложнее в коде, но окупается на масштабе.
Дальше план такой: добиваю восстановление /admin/billing/plans, прохожу по systemd-юнитам и переписываю пути, после этого начинаю проектирование схемы usage_logs и каркаса dashboard'а v3.0. Параллельно — выбор провайдер-стратегии (вероятно, OpenRouter как default + DeepInfra для open-source) и интеграция её в логику биллинга, чтобы себестоимость считалась честно по реальному провайдеру, а не по прайсу OpenAI.

AI-инженер, предприниматель, маркетолог. Основатель feberra.com и x10seo.ru. 13 лет в перфоманс-маркетинге, 3 года в системной интеграции AI в бизнес.