diff --git a/.gitignore b/.gitignore index 0c7925e..74785d5 100644 --- a/.gitignore +++ b/.gitignore @@ -52,4 +52,5 @@ id_rsa.pub # Файлы базы данных *.db *.sqlite -*.sqlite3 \ No newline at end of file +*.sqlite3 +docker-compose.local.yml \ No newline at end of file diff --git a/README.md b/README.md index 7ea48b7..0f3c9c1 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# DApp-for-Business +# Digital_Legal_Entity(DLE) Веб3 приложение для бизнеса с ИИ ассистентом @@ -14,7 +14,7 @@ 1. Клонируйте репозиторий: ```bash git clone https://github.com/DAO-HB3-Accelerator/DLE.git -cd DApp-for-Business +cd Digital_Legal_Entity(DLE) ``` 2. Запустите скрипт установки: @@ -36,12 +36,9 @@ docker exec -e NODE_ENV=migration dapp-backend yarn migrate ## Доступные сервисы -После успешного запуска вы получите доступ к следующим сервисам: +После успешного запуска вы получите доступ: - Frontend: http://localhost:5173 -- Backend API: http://localhost:8000 -- Ollama API: http://localhost:11434 -- PostgreSQL: localhost:5432 (по умолчанию dapp_db/dapp_user/dapp_password) ## Ручной запуск diff --git a/backend/.env.example b/backend/.env.example deleted file mode 100644 index 809176e..0000000 --- a/backend/.env.example +++ /dev/null @@ -1,41 +0,0 @@ -PORT=8000 -NODE_ENV=development -SESSION_SECRET=your_session_secret - -# RPC URLs -RPC_URL_ETH=https://your-ethereum-rpc-url -RPC_URL_POLYGON=https://your-polygon-rpc-url -RPC_URL_BSC=https://your-bsc-rpc-url -RPC_URL_ARBITRUM=https://your-arbitrum-rpc-url -RPC_URL=https://your-default-rpc-url -ETHEREUM_NETWORK_URL=https://your-ethereum-network-url -PRIVATE_KEY=your_private_key_here -ETHERSCAN_API_KEY=your_etherscan_api_key - -# Database -DATABASE_URL=postgresql://dapp_user:dapp_password@postgres:5432/dapp_db -DB_HOST=postgres -DB_PORT=5432 -DB_NAME=dapp_db -DB_USER=dapp_user -DB_PASSWORD=dapp_password - -# Email Configuration -EMAIL_USER=your_email@example.com -EMAIL_PASSWORD=your_email_password -EMAIL_SMTP_HOST=smtp.example.com -EMAIL_SMTP_PORT=465 -EMAIL_IMAP_HOST=imap.example.com -EMAIL_IMAP_PORT=993 - -# Ollama AI Configuration -OLLAMA_BASE_URL=http://ollama:11434 -OLLAMA_EMBEDDINGS_MODEL=qwen2.5:7b -OLLAMA_MODEL=qwen2.5:7b - -# Telegram Bot -TELEGRAM_BOT_TOKEN=your_telegram_bot_token -TELEGRAM_BOT_USERNAME=your_bot_username - -# Frontend URL -FRONTEND_URL=http://localhost:5173 \ No newline at end of file diff --git a/backend/app.js b/backend/app.js index 4bf7056..4bb6f64 100644 --- a/backend/app.js +++ b/backend/app.js @@ -14,6 +14,7 @@ const path = require('path'); const messagesRoutes = require('./routes/messages'); const ragRoutes = require('./routes/rag'); // Новый роут для RAG-ассистента const monitoringRoutes = require('./routes/monitoring'); +const pagesRoutes = require('./routes/pages'); // Добавляем импорт роутера страниц // Проверка и создание директорий для хранения данных контрактов const ensureDirectoriesExist = () => { @@ -186,6 +187,7 @@ app.use('/api/messages', messagesRoutes); app.use('/api/identities', identitiesRoutes); app.use('/api/rag', ragRoutes); // Подключаем роут app.use('/api/monitoring', monitoringRoutes); +app.use('/api/pages', pagesRoutes); // Подключаем роутер страниц const nonceStore = new Map(); // или любая другая реализация хранилища nonce diff --git a/backend/db.js b/backend/db.js index dbba042..1889604 100644 --- a/backend/db.js +++ b/backend/db.js @@ -1,5 +1,6 @@ const { Pool } = require('pg'); require('dotenv').config(); +const axios = require('axios'); // Выводим настройки подключения (без пароля) console.log('Настройки подключения к базе данных:'); @@ -105,5 +106,44 @@ async function saveGuestMessageToDatabase(message, language, guestId) { } } +async function waitForOllamaModel(modelName) { + const ollamaUrl = process.env.OLLAMA_BASE_URL || 'http://ollama:11434'; + while (true) { + try { + const res = await axios.get(`${ollamaUrl}/api/tags`); + const models = res.data.models.map(m => m.name); + if (models.includes(modelName)) { + return true; + } + console.log(`[seedAIAssistantSettings] Ожидание загрузки модели ${modelName}...`); + } catch (e) { + console.log('[seedAIAssistantSettings] Ollama недоступна, ожидание...'); + } + await new Promise(r => setTimeout(r, 5000)); + } +} + +async function seedAIAssistantSettings() { + const modelName = process.env.OLLAMA_MODEL || 'qwen2.5:7b'; + await waitForOllamaModel(modelName); + const res = await pool.query('SELECT COUNT(*) FROM ai_assistant_settings'); + if (parseInt(res.rows[0].count, 10) === 0) { + await pool.query(` + INSERT INTO ai_assistant_settings (system_prompt, selected_rag_tables, languages, model, rules, updated_by) + VALUES ($1, $2, $3, $4, $5, $6) + `, [ + 'Ты — ИИ-ассистент для бизнеса. Отвечай кратко и по делу.', + [], + ['ru'], + modelName, + JSON.stringify({}), + 1 + ]); + console.log('[seedAIAssistantSettings] ai_assistant_settings: инициализировано дефолтными значениями'); + } else { + console.log('[seedAIAssistantSettings] ai_assistant_settings: уже инициализировано, пропускаю'); + } +} + // Экспортируем функции для работы с базой данных -module.exports = { query, getQuery, pool, getPool, setPoolChangeCallback, initDbPool }; +module.exports = { query, getQuery, pool, getPool, setPoolChangeCallback, initDbPool, seedAIAssistantSettings }; diff --git a/backend/db/migrations/022_create_db_settings.sql b/backend/db/migrations/022_create_db_settings.sql index 3a3016f..39b35b7 100644 --- a/backend/db/migrations/022_create_db_settings.sql +++ b/backend/db/migrations/022_create_db_settings.sql @@ -11,5 +11,5 @@ CREATE TABLE IF NOT EXISTS db_settings ( -- Для простоты предполагаем, что настройки всегда одни (id=1) INSERT INTO db_settings (db_host, db_port, db_name, db_user, db_password) -VALUES ('localhost', 5432, 'dapp_db', 'dapp_user', 'dapp_password') +VALUES ('postgres', 5432, 'dapp_db', 'dapp_user', 'dapp_password') ON CONFLICT DO NOTHING; \ No newline at end of file diff --git a/backend/db/migrations/029_create_ai_assistant_settings.sql b/backend/db/migrations/029_create_ai_assistant_settings.sql index f0c9a01..e4db098 100644 --- a/backend/db/migrations/029_create_ai_assistant_settings.sql +++ b/backend/db/migrations/029_create_ai_assistant_settings.sql @@ -6,7 +6,7 @@ CREATE TABLE IF NOT EXISTS ai_assistant_settings ( model TEXT, rules JSONB, updated_at TIMESTAMP DEFAULT NOW(), - updated_by INTEGER, + updated_by INTEGER ); -- Вставить дефолтную строку (глобальные настройки) diff --git a/backend/routes/chat.js b/backend/routes/chat.js index affc986..a904057 100644 --- a/backend/routes/chat.js +++ b/backend/routes/chat.js @@ -9,6 +9,7 @@ const crypto = require('crypto'); const aiAssistantSettingsService = require('../services/aiAssistantSettingsService'); const aiAssistantRulesService = require('../services/aiAssistantRulesService'); const { isUserBlocked } = require('../utils/userUtils'); +const { broadcastChatMessage } = require('../wsHub'); // Настройка multer для обработки файлов в памяти const storage = multer.memoryStorage(); @@ -460,7 +461,7 @@ router.post('/message', requireAuth, upload.array('attachments'), async (req, re logger.info(`[RAG] Запуск поиска по RAG: tableId=${ragTableId}, вопрос="${messageContent}", threshold=${threshold}`); const ragResult = await ragAnswer({ tableId: ragTableId, userQuestion: messageContent, threshold }); logger.info(`[RAG] Результат поиска по RAG:`, ragResult); - if (ragResult && ragResult.answer && ragResult.score && ragResult.score > threshold) { + if (ragResult && ragResult.answer && typeof ragResult.score === 'number' && Math.abs(ragResult.score) <= threshold) { logger.info(`[RAG] Найден confident-ответ (score=${ragResult.score}), отправляем ответ из базы.`); // Прямой ответ из RAG const aiMessageResult = await db.getQuery()( @@ -471,6 +472,8 @@ router.post('/message', requireAuth, upload.array('attachments'), async (req, re [conversationId, userId, ragResult.answer] ); aiMessage = aiMessageResult.rows[0]; + // Пушим новое сообщение через WebSocket + broadcastChatMessage(aiMessage); } else if (ragResult) { logger.info(`[RAG] Нет confident-ответа (score=${ragResult.score}), переходим к генерации через LLM.`); // Генерация через LLM с подстановкой значений из RAG @@ -502,6 +505,8 @@ router.post('/message', requireAuth, upload.array('attachments'), async (req, re [conversationId, userId, llmResponse] ); aiMessage = aiMessageResult.rows[0]; + // Пушим новое сообщение через WebSocket + broadcastChatMessage(aiMessage); } else { logger.info(`[RAG] Нет ни одного результата, прошедшего порог (${threshold}).`); } diff --git a/backend/routes/pages.js b/backend/routes/pages.js new file mode 100644 index 0000000..13fe0a9 --- /dev/null +++ b/backend/routes/pages.js @@ -0,0 +1,136 @@ +const express = require('express'); +const router = express.Router(); +const db = require('../db'); + +const FIELDS_TO_EXCLUDE = ['image', 'tags']; + +// Проверка и создание таблицы для пользователя-админа +async function ensureUserPagesTable(userId, fields) { + fields = fields.filter(f => !FIELDS_TO_EXCLUDE.includes(f)); + const tableName = `pages_user_${userId}`; + // Проверяем, есть ли таблица + const existsRes = await db.getQuery()( + `SELECT to_regclass($1) as exists`, [tableName] + ); + if (!existsRes.rows[0].exists) { + // Формируем SQL для создания таблицы с нужными полями + let columns = [ + 'id SERIAL PRIMARY KEY', + 'created_at TIMESTAMP DEFAULT NOW()', + 'updated_at TIMESTAMP DEFAULT NOW()' + ]; + for (const field of fields) { + columns.push(`"${field}" TEXT`); + } + const sql = `CREATE TABLE ${tableName} (${columns.join(', ')})`; + await db.getQuery()(sql); + } else { + // Проверяем, есть ли все нужные столбцы, и добавляем недостающие + const colRes = await db.getQuery()( + `SELECT column_name FROM information_schema.columns WHERE table_name = $1`, [tableName] + ); + const existingCols = colRes.rows.map(r => r.column_name); + for (const field of fields) { + if (!existingCols.includes(field)) { + await db.getQuery()( + `ALTER TABLE ${tableName} ADD COLUMN "${field}" TEXT` + ); + } + } + } + return tableName; +} + +// Создать страницу (только для админа) +router.post('/', async (req, res) => { + if (!req.user || !req.user.isAdmin) { + return res.status(403).json({ error: 'Only admin can create pages' }); + } + const userId = req.user.id; + const fields = Object.keys(req.body).filter(f => !FIELDS_TO_EXCLUDE.includes(f)); + const filteredBody = {}; + fields.forEach(f => { filteredBody[f] = req.body[f]; }); + const tableName = await ensureUserPagesTable(userId, fields); + + // Формируем SQL для вставки данных + const colNames = fields.map(f => `"${f}"`).join(', '); + const values = Object.values(filteredBody); + const placeholders = values.map((_, i) => `$${i + 1}`).join(', '); + const sql = `INSERT INTO ${tableName} (${colNames}) VALUES (${placeholders}) RETURNING *`; + const { rows } = await db.getQuery()(sql, values); + res.json(rows[0]); +}); + +// Получить все страницы пользователя-админа +router.get('/', async (req, res) => { + if (!req.user || !req.user.isAdmin) { + return res.status(403).json({ error: 'Only admin can view pages' }); + } + const userId = req.user.id; + const tableName = `pages_user_${userId}`; + // Проверяем, есть ли таблица + const existsRes = await db.getQuery()( + `SELECT to_regclass($1) as exists`, [tableName] + ); + if (!existsRes.rows[0].exists) return res.json([]); + const { rows } = await db.getQuery()(`SELECT * FROM ${tableName} ORDER BY created_at DESC`); + res.json(rows); +}); + +// Получить одну страницу по id +router.get('/:id', async (req, res) => { + if (!req.user || !req.user.isAdmin) { + return res.status(403).json({ error: 'Only admin can view pages' }); + } + const userId = req.user.id; + const tableName = `pages_user_${userId}`; + const existsRes = await db.getQuery()( + `SELECT to_regclass($1) as exists`, [tableName] + ); + if (!existsRes.rows[0].exists) return res.status(404).json({ error: 'Page table not found' }); + const { rows } = await db.getQuery()(`SELECT * FROM ${tableName} WHERE id = $1`, [req.params.id]); + if (!rows.length) return res.status(404).json({ error: 'Page not found' }); + res.json(rows[0]); +}); + +// Редактировать страницу по id +router.patch('/:id', async (req, res) => { + if (!req.user || !req.user.isAdmin) { + return res.status(403).json({ error: 'Only admin can edit pages' }); + } + const userId = req.user.id; + const tableName = `pages_user_${userId}`; + const existsRes = await db.getQuery()( + `SELECT to_regclass($1) as exists`, [tableName] + ); + if (!existsRes.rows[0].exists) return res.status(404).json({ error: 'Page table not found' }); + const fields = Object.keys(req.body).filter(f => !FIELDS_TO_EXCLUDE.includes(f)); + if (!fields.length) return res.status(400).json({ error: 'No fields to update' }); + const filteredBody = {}; + fields.forEach(f => { filteredBody[f] = req.body[f]; }); + const setClause = fields.map((f, i) => `"${f}" = $${i + 1}`).join(', '); + const values = Object.values(filteredBody); + values.push(req.params.id); + const sql = `UPDATE ${tableName} SET ${setClause}, updated_at = NOW() WHERE id = $${fields.length + 1} RETURNING *`; + const { rows } = await db.getQuery()(sql, values); + if (!rows.length) return res.status(404).json({ error: 'Page not found' }); + res.json(rows[0]); +}); + +// Удалить страницу по id +router.delete('/:id', async (req, res) => { + if (!req.user || !req.user.isAdmin) { + return res.status(403).json({ error: 'Only admin can delete pages' }); + } + const userId = req.user.id; + const tableName = `pages_user_${userId}`; + const existsRes = await db.getQuery()( + `SELECT to_regclass($1) as exists`, [tableName] + ); + if (!existsRes.rows[0].exists) return res.status(404).json({ error: 'Page table not found' }); + const { rows } = await db.getQuery()(`DELETE FROM ${tableName} WHERE id = $1 RETURNING *`, [req.params.id]); + if (!rows.length) return res.status(404).json({ error: 'Page not found' }); + res.json(rows[0]); +}); + +module.exports = router; \ No newline at end of file diff --git a/backend/server.js b/backend/server.js index 413f7cc..8c56c6f 100644 --- a/backend/server.js +++ b/backend/server.js @@ -5,7 +5,7 @@ const { initWSS } = require('./wsHub'); const logger = require('./utils/logger'); const { getBot } = require('./services/telegramBot'); const EmailBotService = require('./services/emailBot'); -const { initDbPool } = require('./db'); +const { initDbPool, seedAIAssistantSettings } = require('./db'); const PORT = process.env.PORT || 8000; @@ -47,6 +47,7 @@ initWSS(server); async function startServer() { await initDbPool(); // Дождаться пересоздания пула! + await seedAIAssistantSettings(); // Инициализация ассистента после загрузки модели Ollama await initServices(); // Только теперь запускать сервисы console.log(`Server is running on port ${PORT}`); } diff --git a/backend/services/emailBot.js b/backend/services/emailBot.js index 34841fa..feb8f15 100644 --- a/backend/services/emailBot.js +++ b/backend/services/emailBot.js @@ -198,7 +198,7 @@ class EmailBotService { if (ragTableId) { // Сначала ищем ответ через RAG const ragResult = await ragAnswer({ tableId: ragTableId, userQuestion: text }); - if (ragResult && ragResult.answer) { + if (ragResult && ragResult.answer && typeof ragResult.score === 'number' && Math.abs(ragResult.score) <= 0.3) { aiResponse = ragResult.answer; } else { aiResponse = await generateLLMResponse({ diff --git a/backend/services/telegramBot.js b/backend/services/telegramBot.js index a147f75..6ea0f96 100644 --- a/backend/services/telegramBot.js +++ b/backend/services/telegramBot.js @@ -359,7 +359,7 @@ async function getBot() { if (ragTableId) { // Сначала ищем ответ через RAG const ragResult = await ragAnswer({ tableId: ragTableId, userQuestion: content }); - if (ragResult && ragResult.answer) { + if (ragResult && ragResult.answer && typeof ragResult.score === 'number' && Math.abs(ragResult.score) <= 0.3) { aiResponse = ragResult.answer; } else { aiResponse = await generateLLMResponse({ diff --git a/backend/wsHub.js b/backend/wsHub.js index 3964907..4d30264 100644 --- a/backend/wsHub.js +++ b/backend/wsHub.js @@ -27,4 +27,12 @@ function broadcastMessagesUpdate() { } } -module.exports = { initWSS, broadcastContactsUpdate, broadcastMessagesUpdate }; \ No newline at end of file +function broadcastChatMessage(message) { + for (const ws of wsClients) { + if (ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ type: 'chat-message', message })); + } + } +} + +module.exports = { initWSS, broadcastContactsUpdate, broadcastMessagesUpdate, broadcastChatMessage }; \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 7051c0a..03e48b2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -139,6 +139,8 @@ services: done echo 'Ollama is ready, pulling qwen2.5-7b model...' curl -X POST http://ollama:11434/api/pull -d '{\"name\":\"${OLLAMA_MODEL:-qwen2.5:7b}\"}' -H 'Content-Type: application/json' + echo 'Pulling embeddings model...' + curl -X POST http://ollama:11434/api/pull -d '{\"name\":\"${OLLAMA_EMBEDDINGS_MODEL:-mxbai-embed-large:latest}\"}' -H 'Content-Type: application/json' echo 'Done!' " ssh-tunnel-frontend: @@ -175,8 +177,8 @@ services: - backend volumes: - postgres_data: null - ollama_data: null - backend_node_modules: null - frontend_node_modules: null - vector_search_data: null \ No newline at end of file + postgres_data: + ollama_data: + vector_search_data: + frontend_node_modules: + backend_node_modules: \ No newline at end of file diff --git a/frontend/.env.example b/frontend/.env.example deleted file mode 100644 index 60163d6..0000000 --- a/frontend/.env.example +++ /dev/null @@ -1 +0,0 @@ -VITE_API_URL=http://localhost:8000 \ No newline at end of file diff --git a/frontend/src/composables/useChat.js b/frontend/src/composables/useChat.js index 3cf463a..20cc9ed 100644 --- a/frontend/src/composables/useChat.js +++ b/frontend/src/composables/useChat.js @@ -1,4 +1,4 @@ -import { ref, computed, watch, onMounted } from 'vue'; +import { ref, computed, watch, onMounted, onUnmounted } from 'vue'; import api from '../api/axios'; import { getFromStorage, setToStorage, removeFromStorage } from '../utils/storage'; import { generateUniqueId } from '../utils/helpers'; @@ -400,9 +400,30 @@ export function useChat(auth) { // window.addEventListener('load-chat-history', () => loadMessages({ initial: true })); }); - // onUnmounted(() => { - // window.removeEventListener('load-chat-history', () => loadMessages({ initial: true })); - // }); + // --- WebSocket для real-time сообщений --- + let ws = null; + function setupChatWebSocket() { + const wsProtocol = window.location.protocol === 'https:' ? 'wss' : 'ws'; + ws = new WebSocket(`${wsProtocol}://${window.location.host}/ws`); + ws.onmessage = (event) => { + try { + const data = JSON.parse(event.data); + if (data.type === 'chat-message' && data.message) { + // Проверяем, что сообщение для текущего пользователя/диалога + // (можно доработать фильтрацию по conversation_id, user_id и т.д.) + messages.value.push(data.message); + } + } catch (e) { + console.error('[useChat] Ошибка обработки chat-message по WebSocket:', e); + } + }; + } + onMounted(() => { + setupChatWebSocket(); + }); + onUnmounted(() => { + if (ws) ws.close(); + }); return { messages, diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index 056f06d..8fc585b 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -166,9 +166,29 @@ const routes = [ }, { path: '/content', - name: 'content-page', + name: 'content-list', + component: () => import('../views/content/ContentListView.vue'), + }, + { + path: '/content/create', + name: 'content-create', component: () => import('../views/ContentPageView.vue'), }, + { + path: '/content/settings', + name: 'content-settings', + component: () => import('../views/content/ContentSettingsView.vue'), + }, + { + path: '/content/page/:id', + name: 'page-view', + component: () => import('../views/content/PageView.vue'), + }, + { + path: '/content/page/:id/edit', + name: 'page-edit', + component: () => import('../views/content/PageEditView.vue'), + }, ]; const router = createRouter({ diff --git a/frontend/src/services/pagesService.js b/frontend/src/services/pagesService.js new file mode 100644 index 0000000..a447a75 --- /dev/null +++ b/frontend/src/services/pagesService.js @@ -0,0 +1,24 @@ +import api from '../api/axios'; + +export default { + async getPages() { + const res = await api.get('/pages'); + return res.data; + }, + async createPage(data) { + const res = await api.post('/pages', data); + return res.data; + }, + async getPage(id) { + const res = await api.get(`/pages/${id}`); + return res.data; + }, + async updatePage(id, data) { + const res = await api.patch(`/pages/${id}`, data); + return res.data; + }, + async deletePage(id) { + const res = await api.delete(`/pages/${id}`); + return res.data; + }, +}; \ No newline at end of file diff --git a/frontend/src/views/ContentPageView.vue b/frontend/src/views/ContentPageView.vue index afb9bac..d4a8d62 100644 --- a/frontend/src/views/ContentPageView.vue +++ b/frontend/src/views/ContentPageView.vue @@ -1,8 +1,13 @@