ваше сообщение коммита
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -52,4 +52,5 @@ id_rsa.pub
|
|||||||
# Файлы базы данных
|
# Файлы базы данных
|
||||||
*.db
|
*.db
|
||||||
*.sqlite
|
*.sqlite
|
||||||
*.sqlite3
|
*.sqlite3
|
||||||
|
docker-compose.local.yml
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# DApp-for-Business
|
# Digital_Legal_Entity(DLE)
|
||||||
|
|
||||||
Веб3 приложение для бизнеса с ИИ ассистентом
|
Веб3 приложение для бизнеса с ИИ ассистентом
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
1. Клонируйте репозиторий:
|
1. Клонируйте репозиторий:
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/DAO-HB3-Accelerator/DLE.git
|
git clone https://github.com/DAO-HB3-Accelerator/DLE.git
|
||||||
cd DApp-for-Business
|
cd Digital_Legal_Entity(DLE)
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Запустите скрипт установки:
|
2. Запустите скрипт установки:
|
||||||
@@ -36,12 +36,9 @@ docker exec -e NODE_ENV=migration dapp-backend yarn migrate
|
|||||||
|
|
||||||
## Доступные сервисы
|
## Доступные сервисы
|
||||||
|
|
||||||
После успешного запуска вы получите доступ к следующим сервисам:
|
После успешного запуска вы получите доступ:
|
||||||
|
|
||||||
- Frontend: http://localhost:5173
|
- Frontend: http://localhost:5173
|
||||||
- Backend API: http://localhost:8000
|
|
||||||
- Ollama API: http://localhost:11434
|
|
||||||
- PostgreSQL: localhost:5432 (по умолчанию dapp_db/dapp_user/dapp_password)
|
|
||||||
|
|
||||||
## Ручной запуск
|
## Ручной запуск
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -14,6 +14,7 @@ const path = require('path');
|
|||||||
const messagesRoutes = require('./routes/messages');
|
const messagesRoutes = require('./routes/messages');
|
||||||
const ragRoutes = require('./routes/rag'); // Новый роут для RAG-ассистента
|
const ragRoutes = require('./routes/rag'); // Новый роут для RAG-ассистента
|
||||||
const monitoringRoutes = require('./routes/monitoring');
|
const monitoringRoutes = require('./routes/monitoring');
|
||||||
|
const pagesRoutes = require('./routes/pages'); // Добавляем импорт роутера страниц
|
||||||
|
|
||||||
// Проверка и создание директорий для хранения данных контрактов
|
// Проверка и создание директорий для хранения данных контрактов
|
||||||
const ensureDirectoriesExist = () => {
|
const ensureDirectoriesExist = () => {
|
||||||
@@ -186,6 +187,7 @@ app.use('/api/messages', messagesRoutes);
|
|||||||
app.use('/api/identities', identitiesRoutes);
|
app.use('/api/identities', identitiesRoutes);
|
||||||
app.use('/api/rag', ragRoutes); // Подключаем роут
|
app.use('/api/rag', ragRoutes); // Подключаем роут
|
||||||
app.use('/api/monitoring', monitoringRoutes);
|
app.use('/api/monitoring', monitoringRoutes);
|
||||||
|
app.use('/api/pages', pagesRoutes); // Подключаем роутер страниц
|
||||||
|
|
||||||
const nonceStore = new Map(); // или любая другая реализация хранилища nonce
|
const nonceStore = new Map(); // или любая другая реализация хранилища nonce
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
const { Pool } = require('pg');
|
const { Pool } = require('pg');
|
||||||
require('dotenv').config();
|
require('dotenv').config();
|
||||||
|
const axios = require('axios');
|
||||||
|
|
||||||
// Выводим настройки подключения (без пароля)
|
// Выводим настройки подключения (без пароля)
|
||||||
console.log('Настройки подключения к базе данных:');
|
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 };
|
||||||
|
|||||||
@@ -11,5 +11,5 @@ CREATE TABLE IF NOT EXISTS db_settings (
|
|||||||
|
|
||||||
-- Для простоты предполагаем, что настройки всегда одни (id=1)
|
-- Для простоты предполагаем, что настройки всегда одни (id=1)
|
||||||
INSERT INTO db_settings (db_host, db_port, db_name, db_user, db_password)
|
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;
|
ON CONFLICT DO NOTHING;
|
||||||
@@ -6,7 +6,7 @@ CREATE TABLE IF NOT EXISTS ai_assistant_settings (
|
|||||||
model TEXT,
|
model TEXT,
|
||||||
rules JSONB,
|
rules JSONB,
|
||||||
updated_at TIMESTAMP DEFAULT NOW(),
|
updated_at TIMESTAMP DEFAULT NOW(),
|
||||||
updated_by INTEGER,
|
updated_by INTEGER
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Вставить дефолтную строку (глобальные настройки)
|
-- Вставить дефолтную строку (глобальные настройки)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const crypto = require('crypto');
|
|||||||
const aiAssistantSettingsService = require('../services/aiAssistantSettingsService');
|
const aiAssistantSettingsService = require('../services/aiAssistantSettingsService');
|
||||||
const aiAssistantRulesService = require('../services/aiAssistantRulesService');
|
const aiAssistantRulesService = require('../services/aiAssistantRulesService');
|
||||||
const { isUserBlocked } = require('../utils/userUtils');
|
const { isUserBlocked } = require('../utils/userUtils');
|
||||||
|
const { broadcastChatMessage } = require('../wsHub');
|
||||||
|
|
||||||
// Настройка multer для обработки файлов в памяти
|
// Настройка multer для обработки файлов в памяти
|
||||||
const storage = multer.memoryStorage();
|
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}`);
|
logger.info(`[RAG] Запуск поиска по RAG: tableId=${ragTableId}, вопрос="${messageContent}", threshold=${threshold}`);
|
||||||
const ragResult = await ragAnswer({ tableId: ragTableId, userQuestion: messageContent, threshold });
|
const ragResult = await ragAnswer({ tableId: ragTableId, userQuestion: messageContent, threshold });
|
||||||
logger.info(`[RAG] Результат поиска по RAG:`, ragResult);
|
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}), отправляем ответ из базы.`);
|
logger.info(`[RAG] Найден confident-ответ (score=${ragResult.score}), отправляем ответ из базы.`);
|
||||||
// Прямой ответ из RAG
|
// Прямой ответ из RAG
|
||||||
const aiMessageResult = await db.getQuery()(
|
const aiMessageResult = await db.getQuery()(
|
||||||
@@ -471,6 +472,8 @@ router.post('/message', requireAuth, upload.array('attachments'), async (req, re
|
|||||||
[conversationId, userId, ragResult.answer]
|
[conversationId, userId, ragResult.answer]
|
||||||
);
|
);
|
||||||
aiMessage = aiMessageResult.rows[0];
|
aiMessage = aiMessageResult.rows[0];
|
||||||
|
// Пушим новое сообщение через WebSocket
|
||||||
|
broadcastChatMessage(aiMessage);
|
||||||
} else if (ragResult) {
|
} else if (ragResult) {
|
||||||
logger.info(`[RAG] Нет confident-ответа (score=${ragResult.score}), переходим к генерации через LLM.`);
|
logger.info(`[RAG] Нет confident-ответа (score=${ragResult.score}), переходим к генерации через LLM.`);
|
||||||
// Генерация через LLM с подстановкой значений из RAG
|
// Генерация через LLM с подстановкой значений из RAG
|
||||||
@@ -502,6 +505,8 @@ router.post('/message', requireAuth, upload.array('attachments'), async (req, re
|
|||||||
[conversationId, userId, llmResponse]
|
[conversationId, userId, llmResponse]
|
||||||
);
|
);
|
||||||
aiMessage = aiMessageResult.rows[0];
|
aiMessage = aiMessageResult.rows[0];
|
||||||
|
// Пушим новое сообщение через WebSocket
|
||||||
|
broadcastChatMessage(aiMessage);
|
||||||
} else {
|
} else {
|
||||||
logger.info(`[RAG] Нет ни одного результата, прошедшего порог (${threshold}).`);
|
logger.info(`[RAG] Нет ни одного результата, прошедшего порог (${threshold}).`);
|
||||||
}
|
}
|
||||||
|
|||||||
136
backend/routes/pages.js
Normal file
136
backend/routes/pages.js
Normal file
@@ -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;
|
||||||
@@ -5,7 +5,7 @@ const { initWSS } = require('./wsHub');
|
|||||||
const logger = require('./utils/logger');
|
const logger = require('./utils/logger');
|
||||||
const { getBot } = require('./services/telegramBot');
|
const { getBot } = require('./services/telegramBot');
|
||||||
const EmailBotService = require('./services/emailBot');
|
const EmailBotService = require('./services/emailBot');
|
||||||
const { initDbPool } = require('./db');
|
const { initDbPool, seedAIAssistantSettings } = require('./db');
|
||||||
|
|
||||||
const PORT = process.env.PORT || 8000;
|
const PORT = process.env.PORT || 8000;
|
||||||
|
|
||||||
@@ -47,6 +47,7 @@ initWSS(server);
|
|||||||
|
|
||||||
async function startServer() {
|
async function startServer() {
|
||||||
await initDbPool(); // Дождаться пересоздания пула!
|
await initDbPool(); // Дождаться пересоздания пула!
|
||||||
|
await seedAIAssistantSettings(); // Инициализация ассистента после загрузки модели Ollama
|
||||||
await initServices(); // Только теперь запускать сервисы
|
await initServices(); // Только теперь запускать сервисы
|
||||||
console.log(`Server is running on port ${PORT}`);
|
console.log(`Server is running on port ${PORT}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ class EmailBotService {
|
|||||||
if (ragTableId) {
|
if (ragTableId) {
|
||||||
// Сначала ищем ответ через RAG
|
// Сначала ищем ответ через RAG
|
||||||
const ragResult = await ragAnswer({ tableId: ragTableId, userQuestion: text });
|
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;
|
aiResponse = ragResult.answer;
|
||||||
} else {
|
} else {
|
||||||
aiResponse = await generateLLMResponse({
|
aiResponse = await generateLLMResponse({
|
||||||
|
|||||||
@@ -359,7 +359,7 @@ async function getBot() {
|
|||||||
if (ragTableId) {
|
if (ragTableId) {
|
||||||
// Сначала ищем ответ через RAG
|
// Сначала ищем ответ через RAG
|
||||||
const ragResult = await ragAnswer({ tableId: ragTableId, userQuestion: content });
|
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;
|
aiResponse = ragResult.answer;
|
||||||
} else {
|
} else {
|
||||||
aiResponse = await generateLLMResponse({
|
aiResponse = await generateLLMResponse({
|
||||||
|
|||||||
@@ -27,4 +27,12 @@ function broadcastMessagesUpdate() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { initWSS, broadcastContactsUpdate, broadcastMessagesUpdate };
|
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 };
|
||||||
@@ -139,6 +139,8 @@ services:
|
|||||||
done
|
done
|
||||||
echo 'Ollama is ready, pulling qwen2.5-7b model...'
|
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'
|
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!'
|
echo 'Done!'
|
||||||
"
|
"
|
||||||
ssh-tunnel-frontend:
|
ssh-tunnel-frontend:
|
||||||
@@ -175,8 +177,8 @@ services:
|
|||||||
- backend
|
- backend
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres_data: null
|
postgres_data:
|
||||||
ollama_data: null
|
ollama_data:
|
||||||
backend_node_modules: null
|
vector_search_data:
|
||||||
frontend_node_modules: null
|
frontend_node_modules:
|
||||||
vector_search_data: null
|
backend_node_modules:
|
||||||
@@ -1 +0,0 @@
|
|||||||
VITE_API_URL=http://localhost:8000
|
|
||||||
@@ -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 api from '../api/axios';
|
||||||
import { getFromStorage, setToStorage, removeFromStorage } from '../utils/storage';
|
import { getFromStorage, setToStorage, removeFromStorage } from '../utils/storage';
|
||||||
import { generateUniqueId } from '../utils/helpers';
|
import { generateUniqueId } from '../utils/helpers';
|
||||||
@@ -400,9 +400,30 @@ export function useChat(auth) {
|
|||||||
// window.addEventListener('load-chat-history', () => loadMessages({ initial: true }));
|
// window.addEventListener('load-chat-history', () => loadMessages({ initial: true }));
|
||||||
});
|
});
|
||||||
|
|
||||||
// onUnmounted(() => {
|
// --- WebSocket для real-time сообщений ---
|
||||||
// window.removeEventListener('load-chat-history', () => loadMessages({ initial: true }));
|
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 {
|
return {
|
||||||
messages,
|
messages,
|
||||||
|
|||||||
@@ -166,9 +166,29 @@ const routes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/content',
|
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'),
|
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({
|
const router = createRouter({
|
||||||
|
|||||||
24
frontend/src/services/pagesService.js
Normal file
24
frontend/src/services/pagesService.js
Normal file
@@ -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;
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,8 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<BaseLayout>
|
<BaseLayout>
|
||||||
<div class="content-page-block">
|
<div class="content-page-block">
|
||||||
<h2>Контент</h2>
|
<div class="content-header-nav">
|
||||||
<form class="content-form" @submit.prevent>
|
<button class="nav-btn" @click="goToCreate">Создать</button>
|
||||||
|
<button class="nav-btn" @click="goToList">Список страниц</button>
|
||||||
|
<button class="nav-btn" @click="goToSettings">Настройки</button>
|
||||||
|
</div>
|
||||||
|
<router-view />
|
||||||
|
<form class="content-form" @submit.prevent="handleSubmit">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="title">Заголовок страницы *</label>
|
<label for="title">Заголовок страницы *</label>
|
||||||
<input v-model="form.title" id="title" type="text" required />
|
<input v-model="form.title" id="title" type="text" required />
|
||||||
@@ -15,51 +20,6 @@
|
|||||||
<label for="content">Основной контент *</label>
|
<label for="content">Основной контент *</label>
|
||||||
<textarea v-model="form.content" id="content" required rows="6" />
|
<textarea v-model="form.content" id="content" required rows="6" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
|
||||||
<label for="image">Изображение/обложка</label>
|
|
||||||
<input v-model="form.image" id="image" type="text" placeholder="URL или имя файла" />
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="tags">Теги</label>
|
|
||||||
<div class="tags-input">
|
|
||||||
<input
|
|
||||||
v-model="tagInput"
|
|
||||||
@keydown.enter.prevent="addTag"
|
|
||||||
@blur="addTag"
|
|
||||||
placeholder="Введите тег и нажмите Enter"
|
|
||||||
/>
|
|
||||||
<div class="tags-list">
|
|
||||||
<span v-for="(tag, idx) in form.tags" :key="tag" class="tag">
|
|
||||||
{{ tag }}
|
|
||||||
<button type="button" @click="removeTag(idx)">×</button>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="category">Категория</label>
|
|
||||||
<select v-model="form.category" id="category">
|
|
||||||
<option value="">Не выбрано</option>
|
|
||||||
<option value="О компании">О компании</option>
|
|
||||||
<option value="Продукты">Продукты</option>
|
|
||||||
<option value="Блог">Блог</option>
|
|
||||||
<option value="FAQ">FAQ</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="addToChat">Добавить в чат</label>
|
|
||||||
<select v-model="form.addToChat" id="addToChat">
|
|
||||||
<option value="yes">Да</option>
|
|
||||||
<option value="no">Нет</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="rag">Интегрировать с RAG</label>
|
|
||||||
<select v-model="form.rag" id="rag">
|
|
||||||
<option value="yes">Да</option>
|
|
||||||
<option value="no">Нет</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<button class="submit-btn" type="submit">Сохранить</button>
|
<button class="submit-btn" type="submit">Сохранить</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -68,31 +28,44 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
import BaseLayout from '../components/BaseLayout.vue';
|
import BaseLayout from '../components/BaseLayout.vue';
|
||||||
|
import pagesService from '../services/pagesService';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
function goToCreate() { router.push({ name: 'content-create' }); }
|
||||||
|
function goToList() { router.push({ name: 'content-list' }); }
|
||||||
|
function goToSettings() { router.push({ name: 'content-settings' }); }
|
||||||
|
|
||||||
const form = ref({
|
const form = ref({
|
||||||
title: '',
|
title: '',
|
||||||
summary: '',
|
summary: '',
|
||||||
content: '',
|
content: ''
|
||||||
image: '',
|
|
||||||
tags: [],
|
|
||||||
category: '',
|
|
||||||
addToChat: 'yes',
|
|
||||||
rag: 'yes',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const tagInput = ref('');
|
async function handleSubmit() {
|
||||||
|
console.log('handleSubmit called', form.value);
|
||||||
function addTag() {
|
try {
|
||||||
const tag = tagInput.value.trim();
|
if (!form.value.title) {
|
||||||
if (tag && !form.value.tags.includes(tag)) {
|
alert('Заполните заголовок страницы!');
|
||||||
form.value.tags.push(tag);
|
return;
|
||||||
|
}
|
||||||
|
// Создаём страницу через pagesService
|
||||||
|
const page = await pagesService.createPage({
|
||||||
|
title: form.value.title,
|
||||||
|
summary: form.value.summary,
|
||||||
|
content: form.value.content
|
||||||
|
});
|
||||||
|
console.log('createPage result:', page);
|
||||||
|
if (!page || !page.id) {
|
||||||
|
alert('Ошибка: страница не создана!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
router.push({ name: 'content-list' });
|
||||||
|
} catch (e) {
|
||||||
|
alert('Ошибка при создании страницы: ' + (e?.message || e));
|
||||||
|
console.error('Ошибка при создании страницы:', e);
|
||||||
}
|
}
|
||||||
tagInput.value = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeTag(idx) {
|
|
||||||
form.value.tags.splice(idx, 1);
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -165,4 +138,21 @@ input[type="text"], textarea, select {
|
|||||||
.submit-btn:hover {
|
.submit-btn:hover {
|
||||||
background: #1a4e96;
|
background: #1a4e96;
|
||||||
}
|
}
|
||||||
|
.content-header-nav {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
.nav-btn {
|
||||||
|
background: #f5f5f5;
|
||||||
|
border: 1px solid #d0d0d0;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 7px 18px;
|
||||||
|
font-size: 1em;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
.nav-btn:hover {
|
||||||
|
background: #e0e0e0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -204,7 +204,7 @@ function goToContactsList() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function goToContent() {
|
function goToContent() {
|
||||||
router.push({ name: 'content-page' });
|
router.push({ name: 'content-list' });
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
82
frontend/src/views/content/ContentListView.vue
Normal file
82
frontend/src/views/content/ContentListView.vue
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
<template>
|
||||||
|
<BaseLayout>
|
||||||
|
<div class="content-list-block">
|
||||||
|
<div class="content-header-nav">
|
||||||
|
<button class="nav-btn" @click="goToCreate">Создать</button>
|
||||||
|
<button class="nav-btn" @click="goToList">Список страниц</button>
|
||||||
|
<button class="nav-btn" @click="goToSettings">Настройки</button>
|
||||||
|
</div>
|
||||||
|
<h2>Список страниц</h2>
|
||||||
|
<ul v-if="pages.length" class="pages-list">
|
||||||
|
<li v-for="page in pages" :key="page.id">
|
||||||
|
<router-link :to="{ name: 'page-view', params: { id: page.id } }">{{ page.title }}</router-link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div v-else class="empty-list-placeholder">Нет созданных страниц.</div>
|
||||||
|
</div>
|
||||||
|
</BaseLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import BaseLayout from '../../components/BaseLayout.vue';
|
||||||
|
import pagesService from '../../services/pagesService';
|
||||||
|
const router = useRouter();
|
||||||
|
function goToCreate() { router.push({ name: 'content-create' }); }
|
||||||
|
function goToList() { router.push({ name: 'content-list' }); }
|
||||||
|
function goToSettings() { router.push({ name: 'content-settings' }); }
|
||||||
|
|
||||||
|
const pages = ref([]);
|
||||||
|
onMounted(async () => {
|
||||||
|
pages.value = await pagesService.getPages();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.content-list-block {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 4px 32px rgba(0,0,0,0.12);
|
||||||
|
padding: 32px 24px 24px 24px;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 40px;
|
||||||
|
position: relative;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
.content-header-nav {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
.nav-btn {
|
||||||
|
background: #f5f5f5;
|
||||||
|
border: 1px solid #d0d0d0;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 7px 18px;
|
||||||
|
font-size: 1em;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
.nav-btn:hover {
|
||||||
|
background: #e0e0e0;
|
||||||
|
}
|
||||||
|
.empty-list-placeholder {
|
||||||
|
color: #888;
|
||||||
|
font-size: 1.1em;
|
||||||
|
margin-top: 2em;
|
||||||
|
}
|
||||||
|
.pages-list {
|
||||||
|
margin-top: 1.5em;
|
||||||
|
padding-left: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
.pages-list li {
|
||||||
|
padding: 0.5em 0;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
font-size: 1.08em;
|
||||||
|
}
|
||||||
|
.pages-list li:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
30
frontend/src/views/content/ContentSettingsView.vue
Normal file
30
frontend/src/views/content/ContentSettingsView.vue
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<template>
|
||||||
|
<BaseLayout>
|
||||||
|
<div class="content-settings-block">
|
||||||
|
<h2>Настройки контента</h2>
|
||||||
|
<div class="empty-settings-placeholder">Здесь будут настройки для управления страницами.</div>
|
||||||
|
</div>
|
||||||
|
</BaseLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import BaseLayout from '../../components/BaseLayout.vue';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.content-settings-block {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 4px 32px rgba(0,0,0,0.12);
|
||||||
|
padding: 32px 24px 24px 24px;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 40px;
|
||||||
|
position: relative;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
.empty-settings-placeholder {
|
||||||
|
color: #888;
|
||||||
|
font-size: 1.1em;
|
||||||
|
margin-top: 2em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
46
frontend/src/views/content/PageEditView.vue
Normal file
46
frontend/src/views/content/PageEditView.vue
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<template>
|
||||||
|
<BaseLayout>
|
||||||
|
<div v-if="page" class="page-edit-block">
|
||||||
|
<h2>Редактировать страницу</h2>
|
||||||
|
<form @submit.prevent="save">
|
||||||
|
<label>Заголовок</label>
|
||||||
|
<input v-model="page.title" required />
|
||||||
|
<label>Описание</label>
|
||||||
|
<textarea v-model="page.summary" />
|
||||||
|
<label>Контент</label>
|
||||||
|
<textarea v-model="page.content" />
|
||||||
|
<button type="submit">Сохранить</button>
|
||||||
|
<button type="button" @click="goBack">Отмена</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div v-else>Загрузка...</div>
|
||||||
|
</BaseLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, computed } from 'vue';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
import BaseLayout from '../../components/BaseLayout.vue';
|
||||||
|
import pagesService from '../../services/pagesService';
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const page = ref(null);
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
page.value = await pagesService.getPage(route.params.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function save() {
|
||||||
|
await pagesService.updatePage(route.params.id, {
|
||||||
|
title: page.value.title,
|
||||||
|
summary: page.value.summary,
|
||||||
|
content: page.value.content
|
||||||
|
});
|
||||||
|
router.push({ name: 'page-view', params: { id: route.params.id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
function goBack() {
|
||||||
|
router.push({ name: 'page-view', params: { id: route.params.id } });
|
||||||
|
}
|
||||||
|
</script>
|
||||||
38
frontend/src/views/content/PageView.vue
Normal file
38
frontend/src/views/content/PageView.vue
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<template>
|
||||||
|
<BaseLayout>
|
||||||
|
<div v-if="page" class="page-view-block">
|
||||||
|
<h2>{{ page.title }}</h2>
|
||||||
|
<p><b>Описание:</b> {{ page.summary }}</p>
|
||||||
|
<div><b>Контент:</b> {{ page.content }}</div>
|
||||||
|
<button @click="goToEdit">Редактировать</button>
|
||||||
|
<button @click="deletePage" style="color:red">Удалить</button>
|
||||||
|
</div>
|
||||||
|
<div v-else>Загрузка...</div>
|
||||||
|
</BaseLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
import BaseLayout from '../../components/BaseLayout.vue';
|
||||||
|
import pagesService from '../../services/pagesService';
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const page = ref(null);
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
page.value = await pagesService.getPage(route.params.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
function goToEdit() {
|
||||||
|
router.push({ name: 'page-edit', params: { id: route.params.id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deletePage() {
|
||||||
|
if (confirm('Удалить страницу?')) {
|
||||||
|
await pagesService.deletePage(route.params.id);
|
||||||
|
router.push({ name: 'content-list' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -19,8 +19,8 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
Затем в открывшемся терминале WSL выполните:
|
Затем в открывшемся терминале WSL выполните:
|
||||||
<div class="copy-block" @click="copyToClipboard('cd ~/DApp-for-Business\nsudo bash webssh-agent/install.sh')">
|
<div class="copy-block" @click="copyToClipboard('cd ~/Digital_Legal_Entity(DLE)\nsudo bash webssh-agent/install.sh')">
|
||||||
<pre><code>cd ~/DApp-for-Business
|
<pre><code>cd ~/Digital_Legal_Entity(DLE)
|
||||||
sudo bash webssh-agent/install.sh</code></pre>
|
sudo bash webssh-agent/install.sh</code></pre>
|
||||||
<span class="copy-icon">
|
<span class="copy-icon">
|
||||||
<svg v-if="!copied" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none"><rect x="5" y="7" width="9" height="9" rx="2" stroke="#888" stroke-width="1.5"/><rect x="7" y="4" width="9" height="9" rx="2" stroke="#888" stroke-width="1.5"/></svg>
|
<svg v-if="!copied" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none"><rect x="5" y="7" width="9" height="9" rx="2" stroke="#888" stroke-width="1.5"/><rect x="7" y="4" width="9" height="9" rx="2" stroke="#888" stroke-width="1.5"/></svg>
|
||||||
@@ -31,8 +31,8 @@ sudo bash webssh-agent/install.sh</code></pre>
|
|||||||
<li>
|
<li>
|
||||||
<b>Linux:</b><br>
|
<b>Linux:</b><br>
|
||||||
Откройте терминал и выполните:
|
Откройте терминал и выполните:
|
||||||
<div class="copy-block" @click="copyToClipboard('cd ~/DApp-for-Business\nsudo bash webssh-agent/install.sh')">
|
<div class="copy-block" @click="copyToClipboard('cd ~/Digital_Legal_Entity(DLE)\nsudo bash webssh-agent/install.sh')">
|
||||||
<pre><code>cd ~/DApp-for-Business
|
<pre><code>cd ~/Digital_Legal_Entity(DLE)
|
||||||
sudo bash webssh-agent/install.sh</code></pre>
|
sudo bash webssh-agent/install.sh</code></pre>
|
||||||
<span class="copy-icon">
|
<span class="copy-icon">
|
||||||
<svg v-if="!copied" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none"><rect x="5" y="7" width="9" height="9" rx="2" stroke="#888" stroke-width="1.5"/><rect x="7" y="4" width="9" height="9" rx="2" stroke="#888" stroke-width="1.5"/></svg>
|
<svg v-if="!copied" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none"><rect x="5" y="7" width="9" height="9" rx="2" stroke="#888" stroke-width="1.5"/><rect x="7" y="4" width="9" height="9" rx="2" stroke="#888" stroke-width="1.5"/></svg>
|
||||||
|
|||||||
4
setup.sh
4
setup.sh
@@ -105,7 +105,7 @@ start_project() {
|
|||||||
if [ $? -eq 0 ]; then
|
if [ $? -eq 0 ]; then
|
||||||
print_green "Сервисы успешно запущены!"
|
print_green "Сервисы успешно запущены!"
|
||||||
print_green "----------------------------------------"
|
print_green "----------------------------------------"
|
||||||
print_green "Проект DApp-for-Business доступен по адресам:"
|
print_green "Проект Digital_Legal_Entity(DLE) доступен по адресам:"
|
||||||
print_green "Frontend: http://localhost:5173"
|
print_green "Frontend: http://localhost:5173"
|
||||||
print_green "Backend API: http://localhost:8000"
|
print_green "Backend API: http://localhost:8000"
|
||||||
print_green "Ollama API: http://localhost:11434"
|
print_green "Ollama API: http://localhost:11434"
|
||||||
@@ -126,7 +126,7 @@ start_project() {
|
|||||||
# Основная функция
|
# Основная функция
|
||||||
main() {
|
main() {
|
||||||
print_blue "==============================================="
|
print_blue "==============================================="
|
||||||
print_blue " Установка и запуск DApp-for-Business"
|
print_blue " Установка и запуск Digital_Legal_Entity(DLE)"
|
||||||
print_blue "==============================================="
|
print_blue "==============================================="
|
||||||
|
|
||||||
check_docker
|
check_docker
|
||||||
|
|||||||
Reference in New Issue
Block a user