Files
zeropost-tool/lib/engine.js
T
Ник (Claude) 8f4dc1a386 system: AI-провайдеры + блок «Расход AI»
* components/SystemSettings.js: добавлен компонент UsageSummary сверху —
  сводка вызовов и стоимости с переключателем периода (Сегодня/Неделя/
  Месяц/Всё) и группировкой (по сервису/провайдеру/модели). Виджеты
  cost_rub/calls/tokens/images + таблица breakdown.

* components/SystemSettings.js: в массив CATEGORIES добавлена категория
  'ai_providers' первой — Aleksei видит все 11 строк (Текст/Картинки
  ключи+URL+модели + AI_USD_RUB_RATE + AI_PROVIDER_MARKUP) сверху.
  Существующая инфраструктура SettingRow (маскировка секретов, save+toast)
  переиспользуется без изменений.

* lib/engine.js: добавлены engine.usageSummary(params) и engine.usageRecent(limit).

* app/api/admin/usage/summary/route.js (новый): прокси-роут к engine
  /api/usage/summary через requireAdmin.

Verify:
* next build прошёл без ошибок.
* /system → 307 redirect на /login (неавторизованный — корректно).
* /api/admin/usage/summary → 403 Forbidden (не-админ — корректно).
2026-06-08 20:21:49 +03:00

103 lines
4.6 KiB
JavaScript

/**
* Engine client — единая точка вызовов к zeropost-engine
*/
const ENGINE_URL = process.env.ENGINE_URL || 'http://127.0.0.1:3040';
const ENGINE_SECRET = process.env.ENGINE_SECRET || 'zeropost_internal_2026';
async function call(path, options = {}) {
const { userId, body, method = 'GET' } = options;
const headers = {
'Content-Type': 'application/json',
'x-internal-secret': ENGINE_SECRET,
};
if (userId) headers['x-user-id'] = String(userId);
const url = `${ENGINE_URL}${path}`;
const res = await fetch(url, {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
cache: 'no-store',
});
if (!res.ok) {
const err = await res.json().catch(() => ({ error: res.statusText }));
const e = new Error(err.error || `Engine ${res.status}`);
e.status = res.status;
e.code = err.code;
throw e;
}
return res.json();
}
export const engine = {
// Channels
listChannels: (userId) => call('/api/channels/', { userId }),
getChannel: (userId, id) => call(`/api/channels/${id}`, { userId }),
createChannel: (userId, data) => call('/api/channels/', { userId, method: 'POST', body: data }),
updateChannel: (userId, id, data) => call(`/api/channels/${id}`, { userId, method: 'PATCH', body: data }),
deleteChannel: (userId, id) => call(`/api/channels/${id}`, { userId, method: 'DELETE' }),
// Generation
generate: (userId, data) => call('/api/generate/', { userId, method: 'POST', body: data }),
getJob: (userId, id) => call(`/api/generate/${id}`, { userId }),
transformPost: (userId, data) => call('/api/generate/transform', { userId, method: 'POST', body: data }),
generatePostImage: (userId, data) => call('/api/generate/post-image', { userId, method: 'POST', body: data }),
topicsIdeas: (userId, data) => call('/api/generate/topics-ideas', { userId, method: 'POST', body: data }),
getImageStyles: () => call('/api/generate/image-styles'),
// User posts (черновики / запланированные / опубликованные)
listUserPosts: (userId, params = {}) => {
const qs = new URLSearchParams(params).toString();
return call(`/api/user-posts${qs ? '?' + qs : ''}`, { userId });
},
savePost: (userId, data) => call('/api/user-posts', { userId, method: 'POST', body: data }),
getPost: (userId, id) => call(`/api/user-posts/${id}`, { userId }),
updatePost: (userId, id, data) => call(`/api/user-posts/${id}`, { userId, method: 'PATCH', body: data }),
deletePost: (userId, id) => call(`/api/user-posts/${id}`, { userId, method: 'DELETE' }),
publishPost: (userId, id) => call(`/api/user-posts/${id}/publish`, { userId, method: 'POST' }),
// Photo search
photoSearchProfiles: () => call('/api/photo-search/profiles'),
photoSearchQuota: () => call('/api/photo-search/quota'),
photoSearchByQuery: (data) => call('/api/photo-search/by-query', { method: 'POST', body: data }),
// Settings (admin)
listSettings: (category) => {
const qs = category ? `?category=${encodeURIComponent(category)}` : '';
return call(`/api/settings/admin${qs}`);
},
updateSetting: (key, value) => call(`/api/settings/admin/${encodeURIComponent(key)}`, { method: 'PUT', body: { value } }),
invalidateSettingsCache: () => call('/api/settings/admin/invalidate', { method: 'POST' }),
// AI usage (admin)
usageSummary: (params = {}) => {
const qs = new URLSearchParams(params).toString();
return call(`/api/usage/summary${qs ? '?' + qs : ''}`);
},
usageRecent: (limit = 20) => call(`/api/usage/recent?limit=${limit}`),
// Calendar
getCalendar: (userId, params = {}) => {
const qs = new URLSearchParams(params).toString();
return call(`/api/calendar${qs ? '?' + qs : ''}`, { userId });
},
// Metrics
getChannelMetrics: (channelId, params = {}) => {
const qs = new URLSearchParams(params).toString();
return call(`/api/metrics/channel/${channelId}${qs ? '?' + qs : ''}`);
},
getBestTime: (channelId, params = {}) => {
const qs = new URLSearchParams(params).toString();
return call(`/api/metrics/best-time/${channelId}${qs ? '?' + qs : ''}`);
},
getUserPostMetrics: (userId, channelId, params = {}) => {
const qs = new URLSearchParams(params).toString();
return call(`/api/metrics/user-posts/${channelId}${qs ? '?' + qs : ''}`, { userId });
},
collectMetrics: () => call('/api/metrics/collect', { method: 'POST' }),
generateFromUrl: (userId, data) => call('/api/generate/from-url', { userId, method: 'POST', body: data }),
updateUserPostSchedule: (userId, id, scheduledAt) =>
call(`/api/user-posts/${id}`, { userId, method: 'PATCH', body: { scheduled_at: scheduledAt } }),
};