From 42e5851a65800cc0dd5b2a82b368a5b3748c664a Mon Sep 17 00:00:00 2001 From: Z User Date: Fri, 7 Aug 2026 10:52:29 +0000 Subject: [PATCH] 9c8a575a-6c74-4b77-8ed9-1836a7f81e09 --- auth-state.json | 4 + db/custom.db | Bin 114688 -> 122880 bytes prisma/schema.prisma | 1 + src/app/api/admin/chatbot/route.ts | 6 +- src/app/api/chat/route.ts | 160 +- src/components/catalog/catalog-hub.tsx | 19 +- src/components/catalog/catalog-page.tsx | 4 +- .../chatbot/chatbot-settings-page.tsx | 66 +- src/components/layout/admin-sidebar.tsx | 7 +- src/components/layout/breadcrumbs.tsx | 82 +- .../read_1786098853590_868a592a6c82.txt | 647 +++++++ .../read_1786098856442_ced0ccbfd651.txt | 647 +++++++ .../read_1786099467704_7b6cbd8dfb2e.txt | 1514 +++++++++++++++++ worklog.md | 30 +- 14 files changed, 3074 insertions(+), 113 deletions(-) create mode 100644 auth-state.json create mode 100644 tool-results/read_1786098853590_868a592a6c82.txt create mode 100644 tool-results/read_1786098856442_ced0ccbfd651.txt create mode 100644 tool-results/read_1786099467704_7b6cbd8dfb2e.txt diff --git a/auth-state.json b/auth-state.json new file mode 100644 index 0000000..f4ec355 --- /dev/null +++ b/auth-state.json @@ -0,0 +1,4 @@ +{ + "cookies": [], + "origins": [] +} \ No newline at end of file diff --git a/db/custom.db b/db/custom.db index a1b4457131e29b64bb4b5ee96e05216c2bc9d994..faf158c816d29f104a16c456d467d6d206c2ecbf 100755 GIT binary patch delta 8817 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 a%eo^dE8Jb1>Ao}lqxautvWgwM&;JK+n{BE9 delta 146 zcmZoTz~0cnK0#Wrl7WFi28dxmW}=QUW97z#h4PX>Ax{1*2L6xy5BbmWALif6zl?tp ze2>{{;3LsjDo&5*{e>_hGR}!ZQ#|jQB_9L4W p1=g`oKeC+h*=DB=I~cbotzr~o+AO*EIn(s`^^8k5vjqI%2LQw>D|7$= diff --git a/prisma/schema.prisma b/prisma/schema.prisma index bf01fe7..6b30afb 100755 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -179,6 +179,7 @@ model ChatSession { telegramId String? @map("telegram_id") leadId Int? @map("lead_id") messages String // JSON array of {role, content, timestamp} + language String @default("en") device String? ip String? country String? diff --git a/src/app/api/admin/chatbot/route.ts b/src/app/api/admin/chatbot/route.ts index 7ac87fc..cc82ae5 100755 --- a/src/app/api/admin/chatbot/route.ts +++ b/src/app/api/admin/chatbot/route.ts @@ -17,6 +17,7 @@ const CHATBOT_KEYS = [ 'chatbot_provider', 'chatbot_api_endpoint', 'chatbot_api_key', + 'chatbot_model', ] as const; const DEFAULTS: Record = { @@ -30,9 +31,10 @@ const DEFAULTS: Record = { chatbot_max_tokens: '1024', chatbot_max_history: '20', chatbot_knowledge_base: '', - chatbot_provider: 'openai', - chatbot_api_endpoint: '', + chatbot_provider: 'ollama', + chatbot_api_endpoint: 'https://ollama.com/v1/chat/completions', chatbot_api_key: '', + chatbot_model: 'deepseek-v4-flash:preview', }; function maskApiKey(value: string): string { diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts index b9db9ce..1140cff 100755 --- a/src/app/api/chat/route.ts +++ b/src/app/api/chat/route.ts @@ -5,14 +5,31 @@ import { getChatbotConfig } from '@/lib/chatbot-config'; const DEFAULTS: Record = { chatbot_enabled: 'false', chatbot_sleep_mode: 'false', - chatbot_sleep_message: 'Извините, мы сейчас не доступны. Напишите позже, пожалуйста.', + chatbot_sleep_message: + 'Извините, мы сейчас не доступны. Напишите позже, пожалуйста.', chatbot_system_prompt: 'Ты — дружелюбный ассистент интернет-магазина. Отвечай на вопросы клиентов о товарах, ценах, доставке. Будь вежливым и полезным.', chatbot_temperature: '0.7', chatbot_max_tokens: '1024', chatbot_max_history: '20', chatbot_knowledge_base: '', + chatbot_provider: 'ollama', + chatbot_api_endpoint: 'https://ollama.com/v1/chat/completions', chatbot_api_key: '', + chatbot_model: 'deepseek-v4-flash:preview', +}; + +const LANGUAGE_INSTRUCTIONS: Record = { + ru: 'ВАЖНО: Отвечай ТОЛЬКО на русском языке. Все ответы должны быть на русском.', + en: 'IMPORTANT: Respond ONLY in English. All responses must be in English.', + es: 'IMPORTANTE: Responde SOLO en español. Todas las respuestas deben estar en español.', + ar: 'مهم: أجب فقط باللغة العربية. جميع الردود يجب أن تكون بالعربية.', + fr: 'IMPORTANT: Répondez UNIQUEMENT en français.', + de: 'WICHTIG: Antworte AUSSCHLIESSLICH auf Deutsch.', + zh: '重要:只用中文回答。所有回复必须使用中文。', + pt: 'IMPORTANTE: Responda APENAS em português.', + tr: 'ÖNEMLİ: Sadece Türkçe cevap ver.', + hi: 'महत्वपूर्ण: कृपया केवल हिंदी में उत्तर दें।', }; interface ChatMessage { @@ -25,7 +42,6 @@ function getConfig(config: Record, key: string, fallback: string return config[key] || fallback; } -// Extract lead data from message history using regex function extractLeadData(messages: ChatMessage[]): { name?: string; phone?: string; @@ -35,7 +51,6 @@ function extractLeadData(messages: ChatMessage[]): { const result: { name?: string; phone?: string; email?: string; telegram?: string } = {}; const allText = messages.map((m) => m.content).join(' '); - // Phone: various formats const phoneMatch = allText.match( /(?:\+?\d[\s\-\(]?){7,}\d|\+?\d{1,3}[\s\-]?\(?\d{2,4}\)?[\s\-]?\d{2,4}[\s\-]?\d{2,4}/, ); @@ -43,19 +58,16 @@ function extractLeadData(messages: ChatMessage[]): { result.phone = phoneMatch[0].replace(/\s+/g, ' ').trim(); } - // Email const emailMatch = allText.match(/[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}/); if (emailMatch) { result.email = emailMatch[0]; } - // Telegram handle const tgMatch = allText.match(/@(?:[a-zA-Z][a-zA-Z0-9_]{3,30})/); if (tgMatch) { result.telegram = tgMatch[0]; } - // Name patterns — "меня зовут X", "я X", "это X" const namePatterns = [ /(?:меня зовут|зовут меня|я\s+—?\s*|это\s+)([А-ЯЁA-Z][а-яёa-z]+(?:\s+[А-ЯЁA-Z][а-яёa-z]+){0,2})/, /(?:my name is|i am|i\'m)\s+([A-Z][a-z]+(?:\s+[A-Z][a-z]+){0,2})/i, @@ -71,14 +83,12 @@ function extractLeadData(messages: ChatMessage[]): { return result; } -// Simple customer profile generation function generateCustomerProfile(messages: ChatMessage[]): string { const totalMessages = messages.length; const userMessages = messages.filter((m) => m.role === 'user'); const lastFew = userMessages.slice(-5).map((m) => m.content); const allText = lastFew.join(' ').toLowerCase(); - // Intent detection let intent = 'general_inquiry'; if (/цен[аыуе]|стоимость|price|how much|сколько/.test(allText)) intent = 'price_inquiry'; else if (/доставк|shipping|достав/.test(allText)) intent = 'delivery_inquiry'; @@ -86,7 +96,6 @@ function generateCustomerProfile(messages: ChatMessage[]): string { else if (/помощь|help|поддержк|support/.test(allText)) intent = 'support_request'; else if (/отзыв|review|проблем|баг|не работ/.test(allText)) intent = 'complaint'; - // Interest detection const interests: string[] = []; if (/биткоин|bitcoin|btc/.test(allText)) interests.push('Bitcoin'); if (/ethereum|eth/.test(allText)) interests.push('Ethereum'); @@ -94,7 +103,6 @@ function generateCustomerProfile(messages: ChatMessage[]): string { if (/usdt|tether/.test(allText)) interests.push('USDT'); if (/кошел[ьеьк]|wallet/.test(allText)) interests.push('Wallets'); - // Sentiment (very simple) const positiveWords = /спасибо|thanks|отлично|хорошо|great|good|класс|круто/; const negativeWords = /плох|бед|ужас|термин|проблем|ошибк|не работает|bad|awful/; let sentiment: string; @@ -102,7 +110,6 @@ function generateCustomerProfile(messages: ChatMessage[]): string { else if (positiveWords.test(allText)) sentiment = 'positive'; else sentiment = 'neutral'; - // Readiness let readiness: string; if (intent === 'purchase_intent') readiness = 'hot'; else if (intent === 'price_inquiry') readiness = 'warm'; @@ -120,20 +127,74 @@ function generateCustomerProfile(messages: ChatMessage[]): string { return JSON.stringify(profile); } +// ── Ollama Cloud API call (OpenAI-compatible) ── +async function callOllama( + messages: { role: string; content: string }[], + endpoint: string, + apiKey: string, + model: string, + temperature: number, + maxTokens: number, +): Promise { + const res = await fetch(endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${apiKey}`, + }, + body: JSON.stringify({ + model, + messages, + temperature, + max_tokens: maxTokens, + stream: false, + }), + }); + + if (!res.ok) { + const text = await res.text().catch(() => ''); + throw new Error(`Ollama API ${res.status}: ${text}`); + } + + const data = await res.json(); + return data?.choices?.[0]?.message?.content || 'Извините, не удалось получить ответ.'; +} + +// ── z-ai-web-dev-sdk fallback ── +async function callZaiSdk( + messages: { role: string; content: string }[], + temperature: number, + maxTokens: number, +): Promise { + const { ZAI } = await import('z-ai-web-dev-sdk'); + const zai = await ZAI.create(); + const result = await zai.chat.completions.create({ + messages, + temperature, + max_tokens: maxTokens, + thinking: { type: 'disabled' as const }, + }); + return result?.choices?.[0]?.message?.content || 'Извините, не удалось получить ответ.'; +} + export async function POST(request: NextRequest) { try { const body = await request.json(); - const { sessionId, message, telegramId } = body as { + const { sessionId, message, telegramId, language: userLang } = body as { sessionId: string; message: string; telegramId?: string; + language?: string; }; if (!sessionId || !message) { return NextResponse.json({ error: 'Missing sessionId or message' }, { status: 400 }); } - // Load chatbot config with cache + // Normalise language + const language = userLang?.toLowerCase()?.slice(0, 2) || 'en'; + + // Load chatbot config const config = await getChatbotConfig(); const enabled = getConfig(config, 'chatbot_enabled', 'false'); if (enabled !== 'true') { @@ -153,11 +214,19 @@ export async function POST(request: NextRequest) { } catch { existingMessages = []; } + // Update language if changed + if (session.language !== language) { + await db.chatSession.update({ + where: { id: session.id }, + data: { language }, + }); + } } else { session = await db.chatSession.create({ data: { sessionId, telegramId: telegramId || null, + language, messages: JSON.stringify([]), isActive: true, }, @@ -182,7 +251,7 @@ export async function POST(request: NextRequest) { }; existingMessages.push(userMsg); - // Build system prompt + // ── Build system prompt ── const systemPrompt = getConfig(config, 'chatbot_system_prompt', DEFAULTS.chatbot_system_prompt); const knowledgeBase = getConfig(config, 'chatbot_knowledge_base', ''); const sleepMode = getConfig(config, 'chatbot_sleep_mode', 'false'); @@ -190,9 +259,19 @@ export async function POST(request: NextRequest) { const temperature = parseFloat(getConfig(config, 'chatbot_temperature', '0.7')); const maxTokens = parseInt(getConfig(config, 'chatbot_max_tokens', '1024'), 10); const maxHistory = parseInt(getConfig(config, 'chatbot_max_history', '20'), 10); + const provider = getConfig(config, 'chatbot_provider', 'ollama'); + const apiEndpoint = getConfig(config, 'chatbot_api_endpoint', DEFAULTS.chatbot_api_endpoint); + const apiKey = getConfig(config, 'chatbot_api_key', ''); + const model = getConfig(config, 'chatbot_model', 'llama3.1:8b'); let fullSystemPrompt = systemPrompt; + // Language instruction + const langInstruction = LANGUAGE_INSTRUCTIONS[language]; + if (langInstruction) { + fullSystemPrompt = langInstruction + '\n\n' + fullSystemPrompt; + } + if (knowledgeBase) { fullSystemPrompt += '\n\n--- База знаний ---\n' + knowledgeBase; } @@ -209,11 +288,11 @@ export async function POST(request: NextRequest) { fullSystemPrompt += '\n\n--- Профиль клиента ---\n' + profileStr; } } catch { - // ignore parse error + // ignore } } - // Catalog context — fetch some products for context + // Catalog context try { const products = await db.product.findMany({ where: { quantityInStock: { gt: 0 } }, @@ -227,17 +306,17 @@ export async function POST(request: NextRequest) { fullSystemPrompt += '\n\n--- Каталог товаров ---\n' + catalogStr; } } catch { - // ignore catalog fetch errors + // ignore } - // Sleep mode + // Sleep mode override if (sleepMode === 'true') { fullSystemPrompt = - `ВНИМАНИЕ: Режим сна включён. Тебе нужно отвечать ТОЛЬКО следующим сообщением: "${sleepMessage}". Не отвечай ни на какие вопросы, просто отправляй это сообщение.\n\n` + + `ВНИМАНИЕ: Режим сна включён. Отвечай ТОЛЬКО следующим сообщением: "${sleepMessage}". Не отвечай ни на какие вопросы, просто отправляй это сообщение.\n\n` + fullSystemPrompt; } - // Build history messages (limited) + // Build history messages const historySlice = existingMessages.slice(-(maxHistory * 2)); const historyMessages = historySlice.map((m) => ({ role: m.role, @@ -249,25 +328,23 @@ export async function POST(request: NextRequest) { ...historyMessages, ]; - // Call LLM via z-ai-web-dev-sdk + // ── Call LLM ── let reply: string; try { - const { ZAI } = await import('z-ai-web-dev-sdk'); - const zai = await ZAI.create(); - const result = await zai.functions.invoke('llm_chat', { - messages: apiMessages, - temperature, - max_tokens: maxTokens, - }); - reply = result?.content || result?.reply || result?.text || 'Извините, не удалось получить ответ.'; + if (provider === 'ollama' && apiKey) { + reply = await callOllama(apiMessages, apiEndpoint, apiKey, model, temperature, maxTokens); + } else { + reply = await callZaiSdk(apiMessages, temperature, maxTokens); + } if (typeof reply !== 'string') { reply = JSON.stringify(reply); } } catch (llmError) { console.error('LLM call failed:', llmError); - reply = sleepMode === 'true' - ? sleepMessage - : 'Извините, произошла техническая ошибка. Попробуйте написать позже.'; + reply = + sleepMode === 'true' + ? sleepMessage + : 'Извините, произошла техническая ошибка. Попробуйте написать позже.'; } // Save assistant reply @@ -279,11 +356,10 @@ export async function POST(request: NextRequest) { existingMessages.push(assistantMsg); // Extract lead data - const allMessages = [...existingMessages]; - const leadData = extractLeadData(allMessages); + const leadData = extractLeadData(existingMessages); // Generate customer profile - const profile = generateCustomerProfile(allMessages); + const profile = generateCustomerProfile(existingMessages); // Update session await db.chatSession.update({ @@ -299,17 +375,13 @@ export async function POST(request: NextRequest) { let leadId = session.leadId; if (leadData.name || leadData.phone || leadData.email || leadData.telegram || telegramId) { - // Try to find existing lead by telegramId first - let lead = telegramId - ? await db.lead.findUnique({ where: { telegramId } }) - : null; + let lead = telegramId ? await db.lead.findUnique({ where: { telegramId } }) : null; if (!lead && leadId) { lead = await db.lead.findUnique({ where: { id: leadId } }); } if (lead) { - // Update existing lead const updateData: Record = { updatedAt: new Date() }; if (leadData.name && !lead.name) updateData.name = leadData.name; if (leadData.phone && !lead.phone) updateData.phone = leadData.phone; @@ -317,13 +389,9 @@ export async function POST(request: NextRequest) { if (leadData.telegram && !lead.telegram) updateData.telegram = leadData.telegram; if (telegramId && !lead.telegramId) updateData.telegramId = telegramId; - await db.lead.update({ - where: { id: lead.id }, - data: updateData, - }); + await db.lead.update({ where: { id: lead.id }, data: updateData }); leadId = lead.id; } else { - // Create new lead const newLead = await db.lead.create({ data: { telegramId: telegramId || null, @@ -336,7 +404,6 @@ export async function POST(request: NextRequest) { }); leadId = newLead.id; - // Link session to lead await db.chatSession.update({ where: { id: session.id }, data: { leadId: newLead.id }, @@ -346,7 +413,6 @@ export async function POST(request: NextRequest) { leadId = session.leadId; } - // Parse profile for response let parsedProfile; try { parsedProfile = JSON.parse(profile); diff --git a/src/components/catalog/catalog-hub.tsx b/src/components/catalog/catalog-hub.tsx index 6d79bc5..332884d 100755 --- a/src/components/catalog/catalog-hub.tsx +++ b/src/components/catalog/catalog-hub.tsx @@ -16,14 +16,13 @@ const TABS = [ type TabValue = (typeof TABS)[number]["value"]; function resolveInitialTab(): TabValue { - const hash = window.location.hash.slice(1); // e.g. /catalog?tab=categories + const hash = window.location.hash.slice(1); const params = hash.split("?")[1] || ""; const match = params.match(/tab=(\w+)/); if (match) { const v = match[1]; if (TABS.some((t) => t.value === v)) return v as TabValue; } - // also support /catalog/categories, /catalog/locations const path = hash.split("?")[0]; if (path === "/catalog/categories") return "categories"; if (path === "/catalog/locations") return "locations"; @@ -33,7 +32,6 @@ function resolveInitialTab(): TabValue { export function CatalogHub() { const [activeTab, setActiveTab] = useState("products"); - // Sync from hash on mount / hashchange useEffect(() => { const sync = () => { setActiveTab(resolveInitialTab()); @@ -46,7 +44,6 @@ export function CatalogHub() { const handleTabChange = (value: string) => { const tab = value as TabValue; setActiveTab(tab); - // Update hash without full page re-render const base = "/catalog"; if (tab === "products") { window.location.hash = base; @@ -71,27 +68,27 @@ export function CatalogHub() { - - + + {TABS.map((tab) => ( - + {tab.label} ))} - + - + - + diff --git a/src/components/catalog/catalog-page.tsx b/src/components/catalog/catalog-page.tsx index d5a0de6..97ee317 100755 --- a/src/components/catalog/catalog-page.tsx +++ b/src/components/catalog/catalog-page.tsx @@ -291,8 +291,6 @@ export function CatalogPage() { // ─── Tree grouping ───────────────────────────── const getGroupedTree = () => { - if (!tree) return {}; - const countryMap = new Map< string, { @@ -308,6 +306,8 @@ export function CatalogPage() { } >(); + if (!tree) return countryMap; + for (const loc of tree.locations) { if (!countryMap.has(loc.country)) countryMap.set(loc.country, { cityMap: new Map() }); diff --git a/src/components/chatbot/chatbot-settings-page.tsx b/src/components/chatbot/chatbot-settings-page.tsx index cba6c4f..6b5e391 100755 --- a/src/components/chatbot/chatbot-settings-page.tsx +++ b/src/components/chatbot/chatbot-settings-page.tsx @@ -56,6 +56,7 @@ interface ChatbotSettings { chatbot_provider: string; chatbot_api_endpoint: string; chatbot_api_key: string; + chatbot_model: string; } const DEFAULTS: ChatbotSettings = { @@ -70,9 +71,10 @@ const DEFAULTS: ChatbotSettings = { chatbot_max_tokens: 1000, chatbot_max_history: 20, chatbot_knowledge_base: "", - chatbot_provider: "openai", - chatbot_api_endpoint: "", + chatbot_provider: "ollama", + chatbot_api_endpoint: "https://ollama.com/v1/chat/completions", chatbot_api_key: "", + chatbot_model: "deepseek-v4-flash:preview", }; function SettingsSkeleton() { @@ -99,7 +101,24 @@ export function ChatbotSettingsPage() { const res = await fetch("/api/admin/chatbot"); if (res.ok) { const data = await res.json(); - setSettings({ ...DEFAULTS, ...data }); + const raw = data.settings || data; + const parsed: ChatbotSettings = { + ...DEFAULTS, + chatbot_enabled: raw.chatbot_enabled === "true", + chatbot_sleep_mode: raw.chatbot_sleep_mode === "true", + chatbot_sleep_message: raw.chatbot_sleep_message || DEFAULTS.chatbot_sleep_message, + chatbot_system_prompt: raw.chatbot_system_prompt || DEFAULTS.chatbot_system_prompt, + chatbot_welcome_message: raw.chatbot_welcome_message || "", + chatbot_temperature: parseFloat(raw.chatbot_temperature) || 0.7, + chatbot_max_tokens: parseInt(raw.chatbot_max_tokens, 10) || 1000, + chatbot_max_history: parseInt(raw.chatbot_max_history, 10) || 20, + chatbot_knowledge_base: raw.chatbot_knowledge_base || "", + chatbot_provider: raw.chatbot_provider || "ollama", + chatbot_api_endpoint: raw.chatbot_api_endpoint || "", + chatbot_api_key: raw.chatbot_api_key || "", + chatbot_model: raw.chatbot_model || "deepseek-v4-flash:preview", + }; + setSettings(parsed); } } catch { toast.error("Ошибка загрузки настроек"); @@ -480,10 +499,47 @@ export function ChatbotSettingsPage() { onChange={(e) => update("chatbot_api_endpoint", e.target.value) } - placeholder="https://api.openai.com/v1" + placeholder="https://api.ollama.com/v1/chat/completions" /> +
+ + +

+ Доступные модели Ollama Cloud. Для Custom — введите вручную. +

+
+