Files
zeropost-engine/index.js
T
Ник (Claude) 449d1fa728 AI config migration to app_settings + ai_usage logging
* config/index.js: добавлен reloadAi() — асинхронно подтягивает значения
  из app_settings (категория ai_providers), мутирует config.ai in-place.
  Сервисы продолжают использовать config.ai.* синхронно. 3-уровневый fallback:
  app_settings.value → process.env (новое имя) → process.env (старое имя) → дефолт.

* index.js: добавлен await config.reloadAi() в startup после migrate().
  Добавлен middleware AsyncLocalStorage для проброса service/userId в
  AI-сервисы. Сервис определяется по URL-префиксу (zeropost-blog vs
  zeropost-tool). Подмонтирован роут /api/usage.

* routes/settings.js: PUT и invalidate вызывают config.reloadAi() после
  изменения настройки категории ai_providers — горячая перезагрузка
  без рестарта PM2.

* routes/usage.js (новый): GET /api/usage/summary?range&group_by — сводка
  расходов с разбивкой по сервису/провайдеру/модели. GET /api/usage/recent
  — последние вызовы.

* lib/aiContext.js (новый): обёртка над AsyncLocalStorage для service/userId.

* services/aiUsage.js (новый): log() с расчётом cost_rub по справочнику цен
  Anthropic/OpenAI (USD/1M токенов или USD/картинку) × markup × usd_rub.
  Никогда не бросает наружу — ошибки логирования не валят генерацию.

* services/ai.js: chat() и image() обёрнуты в try/catch с aiUsage.log().
  Все вышестоящие функции (generatePost, transformPost, generateTopics,
  generateArticle) идут через chat() — покрытие 100%.

* services/covers.js: 3 call sites хукнуты (generateCoverViaResponses,
  generateCoverViaImagesEndpoint, generateCoverViaImageGenerations). Заодно
  поправлен process.env.AI_MODEL_IMAGE_VIA_RESPONSES → config.ai.imageModelViaResponses.
  Удалён мёртвый код после throw.

* services/postImages.js: 1 call site (/responses + image_generation tool).
  Тот же fix env-ref.

* services/articleAutoSeries.js: 1 call site (/messages, Anthropic-формат).
  usage.input_tokens / output_tokens корректно парсится.

Verify:
* Все 11 строк app_settings ai_providers заполнены значениями из .env.
* Горячая перезагрузка: PUT /api/settings/admin/AI_TEXT_MODEL_POST -> runtime
  обновился без рестарта PM2.
* Live test: ai.chat() через aiprimetech, 118+49 токенов, 0.0414 ₽ — запись
  в ai_usage с правильным service/provider/model.
* GET /api/usage/summary возвращает корректные totals + breakdown.
2026-06-08 20:21:04 +03:00

103 lines
4.2 KiB
JavaScript

const express = require('express');
const config = require('./src/config');
const { migrate } = require('./src/config/db');
// Routes
const generateRoutes = require('./src/routes/generate');
const channelsRoutes = require('./src/routes/channels');
const postsRoutes = require('./src/routes/posts');
const articlesRoutes = require('./src/routes/articles');
const statsRoutes = require('./src/routes/stats');
const notesRoutes = require('./src/routes/notes');
const seriesRoutes = require('./src/routes/series');
const categoriesRoutes = require('./src/routes/categories');
const autogenRoutes = require('./src/routes/autogen');
const userPostsRoutes = require('./src/routes/userPosts');
const settingsRoutes = require('./src/routes/settings');
const photoSearchRoutes = require('./src/routes/photo-search');
const scheduledPostsRoutes = require('./src/routes/scheduledPosts');
const channelStatsRoutes = require('./src/routes/channelStats');
const calendarRoutes = require('./src/routes/calendar');
const metricsRoutes = require('./src/routes/metrics');
const usageRoutes = require('./src/routes/usage');
// Start queue worker
require('./src/workers/generation');
// Metrics collector
require('./src/services/metricsCollector').startAutoCollect();
const app = express();
app.use(express.json());
// Раздача загруженных файлов (обложки статей и т.п.)
const path = require('path');
const UPLOADS_DIR = process.env.UPLOADS_DIR || '/var/www/zeropost-uploads';
require('fs').mkdirSync(UPLOADS_DIR, { recursive: true });
// Public uploads — ДО auth-middleware, без секрета
app.use('/uploads', express.static(UPLOADS_DIR, { maxAge: '7d', immutable: true }));
// Simple internal auth middleware
app.use((req, res, next) => {
const secret = req.headers['x-internal-secret'];
if (secret !== config.internalSecret) {
return res.status(401).json({ error: 'Unauthorized' });
}
next();
});
// AI usage context — приклеивает к каждому запросу service + user_id,
// чтобы сервисы (ai.js, covers.js, postImages.js, articleAutoSeries.js)
// логировали расход без явного проброса параметров.
const aiContext = require('./src/lib/aiContext');
app.use((req, res, next) => {
let service = 'zeropost-other';
// Блог-сторона zeropost.ru: статьи, серии, авто-публикация, темы.
if (/^\/api\/(articles|autogen|series|notes|categories|stats|posts|scheduled-posts|generate)/.test(req.path)) {
service = 'zeropost-blog';
// SaaS-сторона app.zeropost.ru: пользовательские посты, каналы, календарь, аналитика.
} else if (/^\/api\/(user-posts|calendar|channels|channel-stats|metrics|photo-search)/.test(req.path)) {
service = 'zeropost-tool';
}
const userIdRaw = req.headers['x-user-id'];
const userId = userIdRaw ? parseInt(userIdRaw, 10) || null : null;
aiContext.run({ service, userId }, () => next());
});
app.use('/api/generate', generateRoutes);
app.use('/api/channels', channelsRoutes);
app.use('/api/posts', postsRoutes);
app.use('/api/articles', articlesRoutes);
app.use('/api/stats', statsRoutes);
app.use('/api/notes', notesRoutes);
app.use('/api/series', seriesRoutes);
app.use('/api/categories', categoriesRoutes);
app.use('/api/autogen', autogenRoutes);
app.use('/api/user-posts', userPostsRoutes);
app.use('/api/settings', settingsRoutes);
app.use('/api/photo-search', photoSearchRoutes);
app.use('/api/scheduled-posts', scheduledPostsRoutes);
app.use('/api/channel-stats', channelStatsRoutes);
app.use('/api/calendar', calendarRoutes);
app.use('/api/metrics', metricsRoutes);
app.use('/api/usage', usageRoutes);
app.get('/health', (req, res) => {
res.json({ ok: true, service: 'zeropost-engine', time: new Date() });
});
const start = async () => {
await migrate();
await config.reloadAi();
console.log('[Engine] AI config loaded from app_settings: text=' + config.ai.baseUrl + ', images=' + config.ai.imageBaseUrl);
app.listen(config.port, () => {
console.log(`[Engine] Running on port ${config.port}`);
});
};
start().catch(err => {
console.error('[Engine] Failed to start:', err);
process.exit(1);
});