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.
This commit is contained in:
Ник (Claude)
2026-06-08 20:21:04 +03:00
parent 594cc01fe6
commit 449d1fa728
10 changed files with 643 additions and 74 deletions
+25 -1
View File
@@ -13,6 +13,7 @@
const axios = require('axios');
const { query } = require('../config/db');
const config = require('../config');
const aiUsage = require('./aiUsage');
const SERIES_DESCRIPTIONS = [
{
@@ -59,11 +60,13 @@ ${seriesList}
Отвечай ТОЛЬКО одним словом — slug серии (prompts / mcp-agents / cases) или "none" если статья ни к одной не подходит достаточно хорошо.
Выбирай серию только если уверен на 80%+. Лучше "none" чем неточное попадание.`;
const model = config.ai.models?.post || 'claude-haiku-4-5-20251001';
const started = Date.now();
try {
const res = await axios.post(
`${config.ai.baseUrl}/messages`,
{
model: config.ai.models?.post || 'claude-haiku-4-5-20251001',
model,
max_tokens: 10,
messages: [{ role: 'user', content: prompt }],
},
@@ -73,12 +76,33 @@ ${seriesList}
}
);
const usage = res.data?.usage || {};
aiUsage.log({
provider: aiUsage.providerFromBaseUrl(config.ai.baseUrl),
requestType: 'chat',
model,
promptTokens: usage.input_tokens,
completionTokens: usage.output_tokens,
requestId: res.data?.id,
meta: { purpose: 'auto-series-detection' },
durationMs: Date.now() - started,
succeeded: true,
}).catch(() => {});
const raw = res.data?.content?.[0]?.text?.trim().toLowerCase() || 'none';
// Извлекаем только slug без лишнего текста
const valid = SERIES_DESCRIPTIONS.map(s => s.slug);
const found = valid.find(s => raw.includes(s));
return found || null;
} catch (err) {
aiUsage.log({
provider: aiUsage.providerFromBaseUrl(config.ai.baseUrl),
requestType: 'chat', model,
meta: { purpose: 'auto-series-detection' },
durationMs: Date.now() - started,
succeeded: false,
errorMessage: (err.response?.data?.error?.message || err.message || '').slice(0, 500),
}).catch(() => {});
console.warn('[AutoSeries] Claude detection failed:', err.message.slice(0, 100));
return null;
}