ab4e340db9
/onboarding: 3-шаговый вайзард (платформа → название/ниша → готово)
login/page.js: новый пользователь → /onboarding, существующий → /
TopicBank.js: просмотр/пополнение/добавление/удаление тем
ChannelEdit AI-стиль: TopicBank компонент внизу вкладки
channels/new: при 402 CHANNEL_LIMIT_REACHED → ошибка + redirect /plans
lib/engine.js: ENGINE_URL дефолт 3040 → 3030
API routes: /api/topics-bank/[channelId]/{refill,add}, /item/[id]
120 lines
5.5 KiB
JavaScript
120 lines
5.5 KiB
JavaScript
/**
|
|
* Engine client — единая точка вызовов к zeropost-engine
|
|
*/
|
|
const ENGINE_URL = process.env.ENGINE_URL || 'http://127.0.0.1:3030';
|
|
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}`),
|
|
|
|
// Billing
|
|
getBillingBalance: (userId) => call('/api/billing/balance', { userId }),
|
|
getBillingPlans: () => fetch('/api/billing/plans', { cache: 'no-store' }).then(r => r.json()),
|
|
getTransactions: (params = {}) => {
|
|
const qs = new URLSearchParams(params).toString();
|
|
return call(`/api/billing/transactions?${qs}`);
|
|
},
|
|
adminCreditUser: (data) => call('/api/billing/admin/credit', { method: 'POST', body: data }),
|
|
adminGetBalances: () => call('/api/billing/admin/users'),
|
|
|
|
// Editor notes
|
|
listNotes: () => call('/api/notes?limit=100'),
|
|
createNote: (data) => call('/api/notes', { method: 'POST', body: data }),
|
|
updateNote: (id, data) => call(`/api/notes/${id}`, { method: 'PATCH', body: data }),
|
|
deleteNote: (id) => call(`/api/notes/${id}`, { method: 'DELETE' }),
|
|
|
|
// 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 } }),
|
|
};
|
|
// Добавляем в конец файла перед module.exports или в общий объект
|