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 })); // Публичные роуты (без auth) app.get('/api/billing/plans', async (req, res) => { const { query: q } = require('./src/config/db'); const { rows: plans } = await q('SELECT * FROM plans WHERE is_active=true ORDER BY sort_order'); const { rows: costs } = await q('SELECT * FROM credit_costs ORDER BY operation'); res.json({ plans, costs }); }); // ЮKassa webhook — публичный, без internal secret app.post('/api/billing/webhook', express.json({ type: '*/*' }), require('./src/routes/billing').handle || ((req, res, next) => { require('./src/services/yukassa').handleWebhook(req.body) .then(r => res.json({ ok: true, ...r })) .catch(err => res.status(500).json({ error: err.message })); }) ); // TG webhook — публичный (TG не шлёт internal secret) const inboxRoutes = require('./src/routes/inbox'); app.use('/api', inboxRoutes); // включает /api/tg-webhook/:channelId // 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.use('/api/billing', require('./src/routes/billing')); app.use('/api/channels', require('./src/routes/polls')); app.use('/api', inboxRoutes); // /inbox/:id, /inbox/:id/reply, /tg-webhook/:channelId 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=routerai.ru (' + (config.ai.routeraiModel || 'gpt-5-image-mini') + ')'); // Автоматический ретрай SVG-заглушек require('./src/services/coverRetry').start(); 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); });