From bfa21fc1560d6a81eedf8dbc182672d4a8a2a093 Mon Sep 17 00:00:00 2001 From: NW Date: Sat, 8 Aug 2026 15:31:19 +0100 Subject: [PATCH] =?UTF-8?q?fix(ai):=20issue=20#141=20=E2=80=94=20custom=20?= =?UTF-8?q?OpenAI-compatible=20providers=20work=20+=20language=20selection?= =?UTF-8?q?=20on=20first=20start=20in=20sleep=20mode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - callOllama: reasoning_effort only for provider=ollama (others returned 400 -> stub reply) - endpoint normalization: base URL (Groq /openai/v1) -> /chat/completions appended - reasoning block stripping: / tags + thinking/response markers (Groq qwen) - handleStart: sleep mode + language_set=0 -> show language selection keyboard first - handleSetLanguage: sleep mode -> AI dialog in chosen language after selection --- admin-next/src/app/api/chat/route.ts | 59 ++++++++++++++++++------ src/handlers/userHandlers/userHandler.js | 31 ++++++++++++- 2 files changed, 76 insertions(+), 14 deletions(-) diff --git a/admin-next/src/app/api/chat/route.ts b/admin-next/src/app/api/chat/route.ts index fc89d19..753abf2 100755 --- a/admin-next/src/app/api/chat/route.ts +++ b/admin-next/src/app/api/chat/route.ts @@ -140,7 +140,7 @@ function generateCustomerProfile(messages: ChatMessage[]): string { return JSON.stringify(profile); } -// ── Ollama Cloud API call (OpenAI-compatible) ── +// ── LLM API call (OpenAI-compatible) ── async function callOllama( messages: { role: string; content: string }[], endpoint: string, @@ -148,22 +148,34 @@ async function callOllama( model: string, temperature: number, maxTokens: number, + provider: string, ): Promise { - const res = await fetch(endpoint, { + const body: Record = { + model, + messages, + temperature, + max_tokens: maxTokens, + stream: false, + }; + // reasoning_effort — параметр только Ollama Cloud; другие OpenAI-совместимые API возвращают 400 + if (provider === 'ollama') { + body.reasoning_effort = 'none'; + } + + // Нормализация endpoint: если указан базовый URL (без /chat/completions) — добавляем + // (Groq: https://api.groq.com/openai/v1 → /openai/v1/chat/completions) + let url = endpoint; + if (!/\/chat\/completions$/i.test(url)) { + url = url.replace(/\/+$/, '') + '/chat/completions'; + } + + const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}`, }, - body: JSON.stringify({ - model, - messages, - temperature, - max_tokens: maxTokens, - stream: false, - // Отключаем reasoning-токены — возвращаем только content - reasoning_effort: 'none', - }), + body: JSON.stringify(body), }); if (!res.ok) { @@ -172,7 +184,28 @@ async function callOllama( } const data = await res.json(); - return data?.choices?.[0]?.message?.content || 'Извините, не удалось получить ответ.'; + let content = data?.choices?.[0]?.message?.content || 'Извините, не удалось получить ответ.'; + // ── Убираем блоки рассуждений (reasoning) ── + // Форматы от разных провайдеров: + // Groq qwen: "...рассуждения...\n response\n<ответ>" + // Ollama: "\n thinking\n<рассуждения>\n response\n<ответ>" + // 1) Полные теги ... / ... + content = content.replace(/[\s\S]*?<\/think(?:ing)?>/gi, ''); + // 2) Незакрытый — вырезаем до конца (или до маркера response) + content = content.replace(/[\s\S]*$/gi, ''); + // 3) Строка-маркер "response" — берём всё после неё + const lines = content.split('\n'); + const respIdx = lines.findIndex((l) => l.trim().toLowerCase() === 'response'); + if (respIdx !== -1) { + content = lines.slice(respIdx + 1).join('\n'); + } else { + // 4) Нет маркера response — строка "thinking": отрезаем рассуждения + const thinkIdx = lines.findIndex((l) => l.trim().toLowerCase() === 'thinking'); + if (thinkIdx !== -1) { + content = lines.slice(thinkIdx + 1).join('\n'); + } + } + return content.trim(); } export async function POST(request: NextRequest) { @@ -429,7 +462,7 @@ export async function POST(request: NextRequest) { if (!apiKey) { throw new Error('Ollama API key is not configured'); } - reply = await callOllama(apiMessages, apiEndpoint, apiKey, model, temperature, maxTokens); + reply = await callOllama(apiMessages, apiEndpoint, apiKey, model, temperature, maxTokens, provider); if (typeof reply !== 'string') { reply = JSON.stringify(reply); } diff --git a/src/handlers/userHandlers/userHandler.js b/src/handlers/userHandlers/userHandler.js index f114ba9..184e91c 100644 --- a/src/handlers/userHandlers/userHandler.js +++ b/src/handlers/userHandlers/userHandler.js @@ -128,7 +128,19 @@ ${t('profile.member_since')}: ${new Date(userStats.created_at).toLocaleDateStrin // Sleep mode: если магазин на паузе — живой ИИ-диалог вместо каталога if (await chatbotService.isSleepMode()) { - const lang = (await UserService.getUserByTelegramId(telegramId))?.language || 'en'; + const user = await UserService.getUserByTelegramId(telegramId); + // Если язык ещё не выбран (первый старт) — сначала выбор языка + if (!user?.language_set) { + const keyboard = { + inline_keyboard: AVAILABLE_LANGUAGES.map(code => [{ + text: LANGUAGE_NAMES[code], + callback_data: `set_language_${code}` + }]) + }; + await bot.sendMessage(chatId, tForUser('en')('bot.language_select'), { reply_markup: keyboard }); + return; + } + const lang = user?.language || 'en'; // Приветствие от ИИ-бота (живой диалог, не заглушка) const welcome = await chatbotService.sendToChatbot({ sessionId: telegramId, @@ -176,6 +188,23 @@ ${t('profile.member_since')}: ${new Date(userStats.created_at).toLocaleDateStrin await bot.answerCallbackQuery(callbackQuery.id); + // Если магазин на паузе — после выбора языка сразу ИИ-диалог на выбранном языке + if (await chatbotService.isSleepMode()) { + try { await bot.deleteMessage(chatId, callbackQuery.message.message_id); } catch {} + const welcome = await chatbotService.sendToChatbot({ + sessionId: telegramId, + message: '/start', + telegramId, + language: lang, + username: callbackQuery.from?.username, + name: callbackQuery.from?.first_name, + }); + await bot.sendMessage(chatId, welcome.reply || await chatbotService.getWelcomeMessage(), { + reply_markup: { remove_keyboard: true }, + }); + return; + } + const keyboard = { reply_markup: { keyboard: [