From 36c02a9a0a40cc2622d36804ab0dc81734e4651c Mon Sep 17 00:00:00 2001 From: Alexey Pavlov Date: Sat, 30 May 2026 21:46:28 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=BF=D0=B5=D1=80=D0=B5=D1=85=D0=BE?= =?UTF-8?q?=D0=B4=20=D0=BD=D0=B0=20OpenAI-=D1=81=D0=BE=D0=B2=D0=BC=D0=B5?= =?UTF-8?q?=D1=81=D1=82=D0=B8=D0=BC=D1=8B=D0=B9=20=D1=88=D0=BB=D1=8E=D0=B7?= =?UTF-8?q?=20aiprimetech.io?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ai.js: chat/completions вместо /v1/messages, разделены ключи (Claude/GPT) - config: AI_BASE_URL, AI_API_KEY, AI_IMAGE_API_KEY, per-task модели в env - модели по умолчанию: Haiku 4.5 для постов и идей, Sonnet 4.6 для статей, gpt-image-1 для картинок - добавлена функция image() для генерации изображений --- src/config/index.js | 18 +++++++++++-- src/services/ai.js | 65 ++++++++++++++++++++++++++++++--------------- 2 files changed, 59 insertions(+), 24 deletions(-) diff --git a/src/config/index.js b/src/config/index.js index 24494ea..0c0e94e 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -1,8 +1,22 @@ require('dotenv').config(); module.exports = { - port: process.env.PORT || 3030, - anthropicApiKey: process.env.ANTHROPIC_API_KEY, + port: parseInt(process.env.ZEROPOST_PORT || 3030), + + // AI gateway (OpenAI-compatible: aiprimetech.io) + ai: { + baseUrl: process.env.AI_BASE_URL || 'https://aiprimetech.io/v1', + apiKey: process.env.AI_API_KEY, + imageApiKey: process.env.AI_IMAGE_API_KEY || process.env.AI_API_KEY, + // Per-task model selection — tune cost vs quality here + models: { + post: process.env.AI_MODEL_POST || 'claude-haiku-4-5-20251001', + article: process.env.AI_MODEL_ARTICLE || 'claude-sonnet-4-6', + topics: process.env.AI_MODEL_TOPICS || 'claude-haiku-4-5-20251001', + image: process.env.AI_MODEL_IMAGE || 'gpt-image-1', + }, + }, + db: { host: process.env.DB_HOST || 'localhost', port: process.env.DB_PORT || 5432, diff --git a/src/services/ai.js b/src/services/ai.js index 43d16cd..dd466c6 100644 --- a/src/services/ai.js +++ b/src/services/ai.js @@ -1,34 +1,55 @@ const axios = require('axios'); const config = require('../config'); -const ANTHROPIC_URL = 'https://api.anthropic.com/v1/messages'; -const MODEL = 'claude-sonnet-4-6'; +const client = axios.create({ + baseURL: config.ai.baseUrl, + headers: { 'Content-Type': 'application/json' }, + timeout: 120000, +}); /** - * Generate text via Anthropic API + * Low-level chat completion call (OpenAI-compatible gateway) + * @param {string} model * @param {string} systemPrompt * @param {string} userPrompt - * @param {object} options - { maxTokens, temperature } + * @param {object} options - { maxTokens } */ -const generate = async (systemPrompt, userPrompt, options = {}) => { +const chat = async (model, systemPrompt, userPrompt, options = {}) => { const { maxTokens = 2000 } = options; - const res = await axios.post(ANTHROPIC_URL, { - model: MODEL, + const res = await client.post('/chat/completions', { + model, max_tokens: maxTokens, - system: systemPrompt, - messages: [{ role: 'user', content: userPrompt }], + messages: [ + { role: 'system', content: systemPrompt }, + { role: 'user', content: userPrompt }, + ], }, { - headers: { - 'x-api-key': config.anthropicApiKey, - 'anthropic-version': '2023-06-01', - 'Content-Type': 'application/json', - }, + headers: { Authorization: `Bearer ${config.ai.apiKey}` }, }); - const text = res.data.content?.[0]?.text; - if (!text) throw new Error('Empty response from Anthropic'); - return text; + const text = res.data.choices?.[0]?.message?.content; + if (!text) throw new Error('Empty response from AI gateway'); + return text.trim(); +}; + +/** + * Generate an image (GPT/DALL-E via gateway, separate key) + * @param {string} prompt + * @param {object} options - { size } + * @returns {string} image URL or b64 + */ +const image = async (prompt, options = {}) => { + const { size = '1024x1024' } = options; + const res = await client.post('/images/generations', { + model: config.ai.models.image, + prompt, + n: 1, + size, + }, { + headers: { Authorization: `Bearer ${config.ai.imageApiKey}` }, + }); + return res.data.data?.[0]?.url || res.data.data?.[0]?.b64_json; }; /** @@ -45,7 +66,7 @@ ${channelContext ? `Контекст канала: ${channelContext}` : ''} - В конце добавь 2-4 релевантных хэштега`; const user = `Напиши пост на тему: "${topic}"`; - return generate(system, user, { maxTokens: 1000 }); + return chat(config.ai.models.post, system, user, { maxTokens: 1000 }); }; /** @@ -60,7 +81,7 @@ const generateArticle = async ({ topic, language = 'ru', keywords = [] }) => { - Простой и понятный язык`; const user = `Напиши статью на тему: "${topic}"${keywords.length ? `\nКлючевые слова: ${keywords.join(', ')}` : ''}`; - return generate(system, user, { maxTokens: 3000 }); + return chat(config.ai.models.article, system, user, { maxTokens: 3000 }); }; /** @@ -69,12 +90,12 @@ const generateArticle = async ({ topic, language = 'ru', keywords = [] }) => { const generateTopics = async ({ channelContext, count = 10, language = 'ru' }) => { const system = `Генерируй идеи для постов в Telegram-канале. Отвечай только JSON массивом строк, без пояснений.`; const user = `Придумай ${count} идей для постов. Контекст канала: "${channelContext}". Язык: ${language}. Формат: ["тема1","тема2",...]`; - const raw = await generate(system, user, { maxTokens: 800 }); + const raw = await chat(config.ai.models.topics, system, user, { maxTokens: 800 }); try { return JSON.parse(raw.replace(/```json|```/g, '').trim()); } catch { - return raw.split('\n').filter(Boolean).slice(0, count); + return raw.split('\n').map(s => s.replace(/^[-*\d.\s]+/, '').trim()).filter(Boolean).slice(0, count); } }; -module.exports = { generate, generatePost, generateArticle, generateTopics }; +module.exports = { chat, image, generatePost, generateArticle, generateTopics };