ваше сообщение коммита
This commit is contained in:
@@ -10,10 +10,30 @@
|
||||
* GitHub: https://github.com/VC-HB3-Accelerator
|
||||
*/
|
||||
|
||||
const { Pool } = require('pg');
|
||||
const { Pool, types } = require('pg');
|
||||
require('dotenv').config();
|
||||
const axios = require('axios');
|
||||
|
||||
// Настройка парсера для BYTEA - возвращаем Buffer напрямую без конвертации в строку
|
||||
// OID для BYTEA в PostgreSQL: 17
|
||||
types.setTypeParser(17, (value) => {
|
||||
// value уже является Buffer при использовании binary формата
|
||||
// Но если это строка, конвертируем её в Buffer
|
||||
if (Buffer.isBuffer(value)) {
|
||||
return value;
|
||||
}
|
||||
// Если это строка (hex или base64), конвертируем
|
||||
if (typeof value === 'string') {
|
||||
// Проверяем, является ли это hex строка
|
||||
if (/^[0-9a-fA-F]+$/.test(value)) {
|
||||
return Buffer.from(value, 'hex');
|
||||
}
|
||||
// Иначе считаем binary строкой
|
||||
return Buffer.from(value, 'binary');
|
||||
}
|
||||
return value;
|
||||
});
|
||||
|
||||
// Убираем избыточное логирование настроек подключения
|
||||
// console.log('Настройки подключения к базе данных:');
|
||||
// console.log('DATABASE_URL:', process.env.DATABASE_URL?.replace(/:([^:@]+)@/, ':***@'));
|
||||
|
||||
@@ -65,6 +65,7 @@
|
||||
"nodemailer": "^6.10.0",
|
||||
"openai": "^4.102.0",
|
||||
"pg": "^8.10.0",
|
||||
"pg-large-object": "^2.0.0",
|
||||
"semver": "^7.7.1",
|
||||
"session-file-store": "^1.5.0",
|
||||
"siwe": "^2.1.4",
|
||||
|
||||
@@ -14,7 +14,13 @@ const express = require('express');
|
||||
const router = express.Router();
|
||||
const { ethers } = require('ethers');
|
||||
const { Interface, AbiCoder } = ethers;
|
||||
const hre = require('hardhat');
|
||||
// Hardhat опционален - используется только для компиляции в dev режиме
|
||||
let hre = null;
|
||||
try {
|
||||
hre = require('hardhat');
|
||||
} catch (e) {
|
||||
// Hardhat не установлен в production - это нормально
|
||||
}
|
||||
const rpcProviderService = require('../services/rpcProviderService');
|
||||
const { spawn } = require('child_process');
|
||||
const path = require('path');
|
||||
@@ -2181,6 +2187,9 @@ router.post('/deploy-module-all-networks', async (req, res) => {
|
||||
console.log(`[DLE Modules] Текущий nonce для сети ${network.chainId}: ${currentNonce}`);
|
||||
|
||||
// Получаем фабрику контракта
|
||||
if (!hre) {
|
||||
throw new Error('Hardhat не установлен. Установите hardhat для компиляции контрактов.');
|
||||
}
|
||||
const ContractFactory = await hre.ethers.getContractFactory(moduleConfig.contractName);
|
||||
|
||||
// Подготавливаем аргументы конструктора
|
||||
|
||||
@@ -93,17 +93,39 @@ router.post('/media', auth.requireAuth, async (req, res) => {
|
||||
// Используем middleware для загрузки файла
|
||||
mediaUpload.single('media')(req, res, async (err) => {
|
||||
if (err) {
|
||||
console.error('[uploads/media] Ошибка multer:', err);
|
||||
return res.status(400).json({ success: false, message: err.message });
|
||||
}
|
||||
|
||||
try {
|
||||
if (!req.file || !req.file.buffer) return res.status(400).json({ success: false, message: 'Файл не получен' });
|
||||
if (!req.file) {
|
||||
console.error('[uploads/media] Файл не получен в req.file');
|
||||
return res.status(400).json({ success: false, message: 'Файл не получен' });
|
||||
}
|
||||
|
||||
if (!req.file.buffer) {
|
||||
console.error('[uploads/media] Буфер файла отсутствует');
|
||||
return res.status(400).json({ success: false, message: 'Буфер файла отсутствует' });
|
||||
}
|
||||
|
||||
if (!req.file.mimetype) {
|
||||
console.error('[uploads/media] MIME тип отсутствует');
|
||||
return res.status(400).json({ success: false, message: 'MIME тип файла не определен' });
|
||||
}
|
||||
|
||||
const db = require('../db');
|
||||
const mediaType = req.file.mimetype.startsWith('image/') ? 'image' : 'video';
|
||||
|
||||
console.log('[uploads/media] Начало обработки файла:', {
|
||||
originalname: req.file.originalname,
|
||||
mimetype: req.file.mimetype,
|
||||
size: req.file.size,
|
||||
mediaType: mediaType
|
||||
});
|
||||
|
||||
// Вычисляем SHA-256 хеш файла для дедупликации
|
||||
const fileHash = crypto.createHash('sha256').update(req.file.buffer).digest('hex');
|
||||
console.log('[uploads/media] Хеш файла вычислен:', fileHash.substring(0, 16) + '...');
|
||||
|
||||
// Проверяем, не загружен ли уже такой файл
|
||||
const existingFile = await db.getQuery()(
|
||||
@@ -116,10 +138,22 @@ router.post('/media', auth.requireAuth, async (req, res) => {
|
||||
|
||||
if (existingFile.rows.length > 0) {
|
||||
// Файл уже существует - возвращаем существующую запись
|
||||
console.log('[uploads/media] Файл уже существует, используем существующую запись:', existingFile.rows[0].id);
|
||||
mediaId = existingFile.rows[0].id;
|
||||
fileName = existingFile.rows[0].file_name;
|
||||
} else {
|
||||
// Сохраняем новый файл в базу данных
|
||||
console.log('[uploads/media] Сохранение нового файла в БД...');
|
||||
|
||||
// Нормализуем page_id: преобразуем в число или null
|
||||
let pageId = null;
|
||||
if (req.body.page_id) {
|
||||
const parsedPageId = parseInt(req.body.page_id);
|
||||
if (!isNaN(parsedPageId) && parsedPageId > 0) {
|
||||
pageId = parsedPageId;
|
||||
}
|
||||
}
|
||||
|
||||
const { rows } = await db.getQuery()(`
|
||||
INSERT INTO content_media (
|
||||
file_data,
|
||||
@@ -140,11 +174,12 @@ router.post('/media', auth.requireAuth, async (req, res) => {
|
||||
fileHash,
|
||||
mediaType,
|
||||
req.session.address,
|
||||
req.body.page_id || null
|
||||
pageId
|
||||
]);
|
||||
|
||||
mediaId = rows[0].id;
|
||||
fileName = rows[0].file_name;
|
||||
console.log('[uploads/media] Файл успешно сохранен в БД, ID:', mediaId);
|
||||
}
|
||||
|
||||
// URL для доступа к файлу через API
|
||||
@@ -153,6 +188,7 @@ router.post('/media', auth.requireAuth, async (req, res) => {
|
||||
// Это позволяет работать с разными портами (frontend на 9000, backend на 8000)
|
||||
const fullUrl = fileUrl;
|
||||
|
||||
console.log('[uploads/media] Успешная загрузка, возвращаем ответ');
|
||||
return res.json({
|
||||
success: true,
|
||||
data: {
|
||||
@@ -168,39 +204,212 @@ router.post('/media', auth.requireAuth, async (req, res) => {
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Ошибка сохранения медиа в БД:', e);
|
||||
return res.status(500).json({ success: false, message: e.message });
|
||||
console.error('[uploads/media] Ошибка сохранения медиа в БД:', {
|
||||
message: e.message,
|
||||
stack: e.stack,
|
||||
name: e.name,
|
||||
code: e.code,
|
||||
detail: e.detail,
|
||||
constraint: e.constraint,
|
||||
table: e.table,
|
||||
column: e.column
|
||||
});
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: e.message || 'Внутренняя ошибка сервера',
|
||||
error: process.env.NODE_ENV === 'development' ? {
|
||||
name: e.name,
|
||||
code: e.code,
|
||||
detail: e.detail,
|
||||
constraint: e.constraint
|
||||
} : undefined
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// GET /api/uploads/media/:id/file - получить файл по ID
|
||||
// GET /api/uploads/media/:id/file - получить файл по ID с поддержкой Range requests
|
||||
router.get('/media/:id/file', async (req, res) => {
|
||||
let client = null;
|
||||
const mediaId = parseInt(req.params.id);
|
||||
// Увеличиваем chunk size до 1MB для больших файлов - меньше запросов к БД
|
||||
const chunkSize = 1048576; // 1MB chunks для оптимальной производительности стриминга
|
||||
|
||||
try {
|
||||
const db = require('../db');
|
||||
const mediaId = parseInt(req.params.id);
|
||||
const pool = db.getPool();
|
||||
client = await pool.connect();
|
||||
|
||||
const { rows } = await db.getQuery()(
|
||||
'SELECT file_data, file_name, mime_type, file_size FROM content_media WHERE id = $1',
|
||||
// Сначала получаем метаданные без file_data
|
||||
const metaResult = await client.query(
|
||||
'SELECT file_name, mime_type, file_size FROM content_media WHERE id = $1',
|
||||
[mediaId]
|
||||
);
|
||||
|
||||
if (rows.length === 0) {
|
||||
if (metaResult.rows.length === 0) {
|
||||
client.release();
|
||||
return res.status(404).json({ success: false, message: 'Медиа-файл не найден' });
|
||||
}
|
||||
|
||||
const media = rows[0];
|
||||
const media = metaResult.rows[0];
|
||||
const fileSize = parseInt(media.file_size) || 0;
|
||||
|
||||
// Устанавливаем заголовки для правильной отдачи файла
|
||||
// Поддержка HTTP Range requests для стриминга (как на YouTube/Vimeo)
|
||||
const range = req.headers.range;
|
||||
let start = 0;
|
||||
let end = fileSize - 1;
|
||||
let statusCode = 200;
|
||||
|
||||
if (range) {
|
||||
// Парсим Range заголовок (например: "bytes=0-1023" или "bytes=1024-")
|
||||
const parts = range.replace(/bytes=/, '').split('-');
|
||||
start = parseInt(parts[0], 10);
|
||||
end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1;
|
||||
|
||||
// Валидация диапазона
|
||||
if (start >= fileSize || end >= fileSize) {
|
||||
res.setHeader('Content-Range', `bytes */${fileSize}`);
|
||||
client.release();
|
||||
return res.status(416).end(); // Range Not Satisfiable
|
||||
}
|
||||
|
||||
statusCode = 206; // Partial Content
|
||||
}
|
||||
|
||||
const contentLength = end - start + 1;
|
||||
|
||||
// Устанавливаем заголовки для правильной отдачи файла с поддержкой Range
|
||||
res.setHeader('Content-Type', media.mime_type);
|
||||
res.setHeader('Content-Length', media.file_size);
|
||||
res.setHeader('Content-Disposition', `inline; filename="${media.file_name}"`);
|
||||
res.setHeader('Accept-Ranges', 'bytes'); // Указываем, что поддерживаем Range requests
|
||||
res.setHeader('Content-Length', contentLength);
|
||||
res.setHeader('Cache-Control', 'public, max-age=31536000'); // Кеширование на 1 год
|
||||
|
||||
// Отправляем бинарные данные
|
||||
res.send(media.file_data);
|
||||
if (range) {
|
||||
res.setHeader('Content-Range', `bytes ${start}-${end}/${fileSize}`);
|
||||
res.status(statusCode);
|
||||
} else {
|
||||
res.setHeader('Content-Disposition', `inline; filename="${media.file_name}"`);
|
||||
}
|
||||
|
||||
// Используем прямой стриминг BYTEA данных частями через SQL substring
|
||||
// Начинаем с нужной позиции (для Range requests)
|
||||
let offset = start + 1; // PostgreSQL substring использует 1-based индексацию
|
||||
const endOffset = end + 1;
|
||||
|
||||
const streamChunk = async () => {
|
||||
try {
|
||||
// Проверяем, не достигли ли мы конца запрошенного диапазона
|
||||
if (offset > endOffset) {
|
||||
// Достигнут конец запрошенного диапазона
|
||||
client.release();
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
// Вычисляем размер текущего chunk (может быть меньше chunkSize для последнего chunk)
|
||||
const currentChunkSize = Math.min(chunkSize, endOffset - offset + 1);
|
||||
|
||||
// Читаем следующий chunk данных, используя encode для получения hex-строки
|
||||
const chunkResult = await client.query(
|
||||
`SELECT encode(substring(file_data FROM $1 FOR $2), 'hex') as chunk_hex FROM content_media WHERE id = $3`,
|
||||
[offset, currentChunkSize, mediaId]
|
||||
);
|
||||
|
||||
if (chunkResult.rows.length === 0 || !chunkResult.rows[0] || !chunkResult.rows[0].chunk_hex) {
|
||||
// Достигнут конец файла или данные отсутствуют
|
||||
client.release();
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
const chunkHex = chunkResult.rows[0].chunk_hex;
|
||||
|
||||
// Если chunk пустой, значит достигнут конец
|
||||
if (!chunkHex || chunkHex.length === 0) {
|
||||
client.release();
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
// Преобразуем hex-строку в Buffer
|
||||
const buffer = Buffer.from(chunkHex, 'hex');
|
||||
|
||||
// Отправляем chunk клиенту
|
||||
if (!res.write(buffer)) {
|
||||
// Буфер переполнен, ждем события 'drain'
|
||||
res.once('drain', () => {
|
||||
offset += currentChunkSize;
|
||||
streamChunk();
|
||||
});
|
||||
} else {
|
||||
// Продолжаем отправку следующего chunk
|
||||
offset += currentChunkSize;
|
||||
streamChunk();
|
||||
}
|
||||
} catch (chunkErr) {
|
||||
console.error('[uploads/media/:id/file] Ошибка чтения chunk:', {
|
||||
message: chunkErr.message,
|
||||
stack: chunkErr.stack,
|
||||
offset: offset,
|
||||
endOffset: endOffset,
|
||||
fileSize: fileSize
|
||||
});
|
||||
if (client) {
|
||||
client.release();
|
||||
}
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({ success: false, message: 'Ошибка чтения файла' });
|
||||
} else {
|
||||
res.end();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Начинаем стриминг
|
||||
streamChunk();
|
||||
|
||||
// Обработка ошибок HTTP ответа
|
||||
res.on('error', (resErr) => {
|
||||
console.error('[uploads/media/:id/file] Ошибка HTTP ответа:', resErr);
|
||||
if (client) {
|
||||
client.release();
|
||||
}
|
||||
});
|
||||
|
||||
// Обработка закрытия соединения клиентом
|
||||
res.on('close', () => {
|
||||
if (client) {
|
||||
client.release();
|
||||
}
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
return res.status(500).json({ success: false, message: e.message });
|
||||
if (client) {
|
||||
client.release();
|
||||
}
|
||||
|
||||
console.error('[uploads/media/:id/file] Ошибка получения файла:', {
|
||||
message: e.message,
|
||||
stack: e.stack,
|
||||
name: e.name,
|
||||
code: e.code,
|
||||
detail: e.detail,
|
||||
constraint: e.constraint,
|
||||
table: e.table,
|
||||
column: e.column
|
||||
});
|
||||
if (!res.headersSent) {
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: e.message || 'Внутренняя ошибка сервера',
|
||||
error: process.env.NODE_ENV === 'development' ? {
|
||||
name: e.name,
|
||||
code: e.code,
|
||||
detail: e.detail,
|
||||
constraint: e.constraint
|
||||
} : undefined
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1575,16 +1575,27 @@ router.get('/ssl/status', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETT
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`[VDS] SSL статус проверен: найдено сертификатов: ${allCertificates.length}, домен: ${domain}`);
|
||||
res.json({
|
||||
success: true,
|
||||
certificates: checkResult.stdout,
|
||||
allCertificates: allCertificates,
|
||||
domain: domain,
|
||||
certInfo: certInfo
|
||||
certInfo: certInfo,
|
||||
hasCertificates: allCertificates.length > 0
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[VDS] Ошибка проверки SSL сертификата:', error);
|
||||
res.status(500).json({ success: false, error: error.message });
|
||||
logger.error('[VDS] Детали ошибки:', {
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
code: error.code
|
||||
});
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message,
|
||||
details: process.env.NODE_ENV === 'development' ? error.stack : undefined
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -5262,6 +5262,11 @@ pg-int8@1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c"
|
||||
integrity sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==
|
||||
|
||||
pg-large-object@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/pg-large-object/-/pg-large-object-2.0.0.tgz#19e863aa5aee64ee3735238b740b2cb250eb5b7e"
|
||||
integrity sha512-SiqyK3G4Qv4WJLsSCh7N59om9Ga3t6ysAqyguTIojYG/SIU+YO1wQIRp7SM4Bov3sikGtRP8GZ9iqDJv8h/xXA==
|
||||
|
||||
pg-pool@^3.10.1:
|
||||
version "3.10.1"
|
||||
resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.10.1.tgz#481047c720be2d624792100cac1816f8850d31b2"
|
||||
|
||||
884
docs/application-overview.md
Normal file
884
docs/application-overview.md
Normal file
@@ -0,0 +1,884 @@
|
||||
<!--
|
||||
Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||
All rights reserved.
|
||||
|
||||
This software is proprietary and confidential.
|
||||
Unauthorized copying, modification, or distribution is prohibited.
|
||||
|
||||
For licensing inquiries: info@hb3-accelerator.com
|
||||
Website: https://hb3-accelerator.com
|
||||
GitHub: https://github.com/VC-HB3-Accelerator
|
||||
-->
|
||||
|
||||
# Обзор приложения Digital Legal Entity (DLE)
|
||||
|
||||
## 📋 Содержание
|
||||
|
||||
1. [Установка приложения](#установка-приложения)
|
||||
2. [Настройка безопасности](#настройка-безопасности)
|
||||
3. [Модули и микросервисная архитектура](#модули-и-микросервисная-архитектура)
|
||||
4. [Корпоративный мессенджер с ИИ ассистентом](#корпоративный-мессенджер-с-ии-ассистентом)
|
||||
5. [Настройки приложения для работы с интернет пользователями](#настройки-приложения-для-работы-с-интернет-пользователями)
|
||||
6. [Требования регулятора](#требования-регулятора)
|
||||
7. [Обновления софта](#обновления-софта)
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Установка приложения
|
||||
|
||||
### Быстрый старт
|
||||
|
||||
Digital Legal Entity (DLE) разворачивается с помощью Docker Compose, что обеспечивает простую и надежную установку на любой платформе.
|
||||
|
||||
### Системные требования
|
||||
|
||||
**Минимальные требования**:
|
||||
- **CPU**: 4 ядра
|
||||
- **RAM**: 12 GB (4 GB приложение + 6 GB AI + 2 GB Vector Search)
|
||||
- **Хранилище**: 100 GB SSD
|
||||
- **ОС**: Ubuntu 20.04+, Debian 11+, CentOS 8+, любая Linux с Docker
|
||||
- **Docker**: версия 20.10+
|
||||
- **Docker Compose**: версия 2.0+
|
||||
|
||||
### Процесс установки
|
||||
|
||||
Для Linux/macOS/WSL:
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/VC-HB3-Accelerator/DLE/main/setup.sh | bash
|
||||
```
|
||||
|
||||
Скрипт автоматически:
|
||||
- Скачивает последние артефакты из релиза
|
||||
- Разворачивает `docker-data`
|
||||
- Настраивает необходимые конфигурации
|
||||
|
||||
|
||||
### Компоненты системы
|
||||
|
||||
После установки запускаются следующие сервисы:
|
||||
|
||||
| Сервис | Описание | Порт |
|
||||
|--------|----------|------|
|
||||
| **PostgreSQL** | База данных с расширением pgvector | Внутренний |
|
||||
| **Ollama** | Локальный AI сервер | Внутренний (11434) |
|
||||
| **Vector Search** | Сервис векторного поиска (RAG) | Внутренний (8001) |
|
||||
| **Backend** | Node.js API сервер | Внутренний (8000) |
|
||||
| **Frontend (Nginx)** | Веб-интерфейс | 9000 (HTTP)
|
||||
|
||||
### Доступ к приложению
|
||||
|
||||
После успешного запуска приложение доступно по адресу:
|
||||
- **Production**: `http://localhost:9000` (HTTP)
|
||||
|
||||
### Первоначальная настройка
|
||||
|
||||
1. Откройте приложение в браузере
|
||||
2. Подключите крипто-кошелек (MetaMask, WalletConnect)
|
||||
3. Настройте RPC провайдеры в разделе **Настройки → Безопасность**
|
||||
4. Настройте смарт-контракты в разделе **Настройки → Блокчейн**
|
||||
|
||||
> 💡 **Подробная инструкция**: См. [Инструкция по установке](./setup-instruction.md) для пошаговой настройки всех компонентов.
|
||||
|
||||
---
|
||||
|
||||
## 🔒 Настройка безопасности
|
||||
|
||||
### Многоуровневая модель безопасности
|
||||
|
||||
DLE использует комплексный подход к безопасности на всех уровнях архитектуры:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Уровни защиты DLE │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Уровень 1: Блокчейн (Неизменяемая база) │
|
||||
│ • Смарт-контракт DLE (проверен, иммутабельный) │
|
||||
│ • Токены управления (ERC20Votes) │
|
||||
│ • История всех операций на блокчейне │
|
||||
│ • Невозможность изменения правил без голосования │
|
||||
│ │
|
||||
│ Уровень 2: Веб-приложение (Backend) │
|
||||
│ • Проверка токенов в реальном времени │
|
||||
│ • Аутентификация через кошелек (SIWE) │
|
||||
│ • Шифрование данных (AES-256) │
|
||||
│ • Rate limiting и защита от DDoS │
|
||||
│ │
|
||||
│ Уровень 3: Frontend (Vue.js) │
|
||||
│ • Подключение к кошельку │
|
||||
│ • Подпись транзакций │
|
||||
│ • XSS защита (DOMPurify) │
|
||||
│ • CSRF токены │
|
||||
│ │
|
||||
│ Уровень 4: Пользователь │
|
||||
│ • Приватный ключ кошелька (MetaMask, WalletConnect) │
|
||||
│ • Подтверждение каждой операции │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Настройка безопасности
|
||||
|
||||
#### 1. Настройка RPC провайдеров
|
||||
|
||||
**Путь**: Настройки → Безопасность → RPC провайдеры
|
||||
|
||||
Для каждой блокчейн-сети необходимо добавить RPC провайдера:
|
||||
- **Network Name**: Название сети (Ethereum, Polygon, BSC и т.д.)
|
||||
- **RPC URL**: URL подключения (например: `https://eth-mainnet.g.alchemy.com/v2/YOUR-API-KEY`)
|
||||
- **Network ID**: Chain ID сети
|
||||
|
||||
> ⚠️ **Важно**: Получите API ключи от провайдеров (Alchemy, Infura, Quicknode и т.д.) перед добавлением.
|
||||
|
||||
#### 2. Настройка смарт-контрактов
|
||||
|
||||
**Путь**: Настройки → Блокчейн
|
||||
|
||||
Настройте адреса смарт-контрактов для каждой сети:
|
||||
- **Factory Address**: Адрес фабрики контрактов
|
||||
- **Core Contract Address**: Адрес основного контракта DLE
|
||||
|
||||
#### 3. Настройка шифрования
|
||||
|
||||
Все чувствительные данные шифруются с использованием AES-256:
|
||||
- Персональные данные пользователей
|
||||
- Идентификаторы гостей
|
||||
- Приватные сообщения
|
||||
- Ключи шифрования хранятся в защищенном хранилище
|
||||
|
||||
#### 4. Управление доступом
|
||||
|
||||
Система ролей и разрешений:
|
||||
- **Guest**: Базовый доступ, чат с AI
|
||||
- **User**: Полный доступ к коммуникациям
|
||||
- **ReadOnly**: Просмотр данных без редактирования
|
||||
- **Editor**: Полный доступ ко всем функциям
|
||||
|
||||
#### 5. Аудит и мониторинг
|
||||
|
||||
- Все действия логируются
|
||||
- Audit trail для критичных операций
|
||||
- Мониторинг подозрительной активности
|
||||
- Уведомления о важных событиях
|
||||
|
||||
> 💡 **Подробная информация**: См. [Безопасность DLE](./security.md) для детального описания всех уровней защиты, сценариев атак и рекомендаций.
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Модули и микросервисная архитектура
|
||||
|
||||
### Архитектура системы
|
||||
|
||||
DLE построен на принципах микросервисной архитектуры, что обеспечивает:
|
||||
- ✅ Легкое масштабирование отдельных компонентов
|
||||
- ✅ Независимое развертывание сервисов
|
||||
- ✅ Высокую отказоустойчивость
|
||||
- ✅ Гибкость в разработке и поддержке
|
||||
|
||||
### Основные сервисы
|
||||
|
||||
#### 1. PostgreSQL (База данных)
|
||||
- **Назначение**: Хранение всех данных приложения
|
||||
- **Особенности**: Расширение pgvector для векторного поиска
|
||||
- **Масштабирование**: Репликация, шардирование
|
||||
|
||||
#### 2. Ollama (AI сервер)
|
||||
- **Назначение**: Локальный AI для генерации ответов
|
||||
- **Модель**: qwen2.5:7b (настраивается)
|
||||
- **Особенности**: 100% конфиденциальность, работа offline
|
||||
- **Ресурсы**: 6 GB RAM, 2 CPU cores
|
||||
|
||||
#### 3. Vector Search (RAG сервис)
|
||||
- **Назначение**: Векторный поиск для контекстных ответов AI
|
||||
- **Технология**: FAISS, pgvector
|
||||
- **Модель эмбеддингов**: mxbai-embed-large:latest
|
||||
- **Особенности**: Семантический поиск по документам
|
||||
|
||||
#### 4. Backend (Node.js API)
|
||||
- **Назначение**: Основной API сервер
|
||||
- **Технологии**: Express.js, WebSocket
|
||||
- **Функции**:
|
||||
- Обработка запросов
|
||||
- Управление сообщениями
|
||||
- Интеграция с блокчейном
|
||||
- Управление пользователями
|
||||
|
||||
#### 5. Frontend (Vue.js)
|
||||
- **Назначение**: Пользовательский интерфейс
|
||||
- **Технологии**: Vue 3, Vite, Element Plus
|
||||
- **Особенности**: SPA, адаптивный дизайн
|
||||
|
||||
### Модульная система
|
||||
|
||||
DLE поддерживает расширение функциональности через модули:
|
||||
|
||||
#### Доступные модули
|
||||
|
||||
1. **HierarchicalVotingModule**
|
||||
- Иерархическое голосование
|
||||
- Делегирование голосов
|
||||
- Сложные сценарии управления
|
||||
|
||||
2. **Custom Modules**
|
||||
- Возможность добавления собственных модулей
|
||||
- Интеграция через смарт-контракты
|
||||
- Управление через голосование токен-холдеров
|
||||
|
||||
#### Добавление модулей
|
||||
|
||||
Модули добавляются через:
|
||||
1. Голосование токен-холдеров
|
||||
2. Деплой смарт-контракта модуля
|
||||
3. Регистрация в основном контракте DLE
|
||||
|
||||
### Коммуникация между сервисами
|
||||
|
||||
```
|
||||
┌─────────────┐
|
||||
│ Frontend │
|
||||
│ (Vue.js) │
|
||||
└──────┬──────┘
|
||||
│ HTTP/WebSocket
|
||||
▼
|
||||
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
|
||||
│ Backend │──────│ PostgreSQL │ │ Ollama │
|
||||
│ (Node.js) │ │ (pgvector) │ │ (AI Server)│
|
||||
└──────┬──────┘ └──────────────┘ └─────────────┘
|
||||
│
|
||||
│ HTTP
|
||||
▼
|
||||
┌─────────────┐
|
||||
│Vector Search│
|
||||
│ (RAG) │
|
||||
└─────────────┘
|
||||
```
|
||||
|
||||
### Масштабирование
|
||||
|
||||
Каждый сервис может масштабироваться независимо:
|
||||
- **Горизонтальное масштабирование**: Добавление инстансов
|
||||
- **Вертикальное масштабирование**: Увеличение ресурсов
|
||||
- **Балансировка нагрузки**: Nginx, HAProxy
|
||||
|
||||
> 💡 **Подробная информация**: См. [Техническая документация по блокчейну](./blockchain-integration-technical.md) для информации о модульной системе смарт-контрактов.
|
||||
|
||||
---
|
||||
|
||||
## 💬 Корпоративный мессенджер с ИИ ассистентом
|
||||
|
||||
### Обзор функциональности
|
||||
|
||||
DLE включает полнофункциональный корпоративный мессенджер с интегрированным AI ассистентом, который обеспечивает:
|
||||
- Единый интерфейс для всех каналов коммуникации
|
||||
- Автоматические ответы с помощью AI
|
||||
- Контекстное понимание истории общения
|
||||
- Многоканальную поддержку клиентов
|
||||
|
||||
### Каналы коммуникации
|
||||
|
||||
#### 1. Веб-чат
|
||||
- Публичный чат на сайте
|
||||
- Приватные сообщения между пользователями
|
||||
- Административный чат для связи с клиентами
|
||||
|
||||
#### 2. Telegram бот
|
||||
- Интеграция с Telegram
|
||||
- Автоматические ответы через AI
|
||||
- Синхронизация с веб-интерфейсом
|
||||
|
||||
#### 3. Email
|
||||
- Обработка входящих писем
|
||||
- Автоматические ответы
|
||||
- Интеграция с CRM
|
||||
|
||||
|
||||
### ИИ ассистент
|
||||
|
||||
#### Возможности AI ассистента
|
||||
|
||||
**Локальный AI на вашем сервере**:
|
||||
- Модель: qwen2.5:7b (настраивается)
|
||||
- Технология: Ollama + Vector Search (RAG)
|
||||
- Конфиденциальность: 100% (данные не покидают сервер)
|
||||
- Стоимость: $0 (без лимитов на запросы)
|
||||
|
||||
#### Функции AI ассистента
|
||||
|
||||
1. **Автоматические ответы**
|
||||
- Генерация ответов на основе контекста
|
||||
- Обучение на ваших документах
|
||||
- Персонализация под ваш бизнес
|
||||
|
||||
2. **Анализ настроения**
|
||||
- Определение эмоционального тона сообщений
|
||||
- Приоритизация запросов
|
||||
- Эскалация критичных ситуаций
|
||||
|
||||
3. **Контекстный поиск**
|
||||
- Поиск информации в базе знаний
|
||||
- Использование истории общения
|
||||
- Ссылки на релевантные документы
|
||||
|
||||
4. **Многозадачность**
|
||||
- Обработка нескольких запросов одновременно
|
||||
- Очередь запросов
|
||||
- Приоритизация важных сообщений
|
||||
|
||||
#### Настройка AI ассистента
|
||||
|
||||
**Путь**: Настройки → AI Ассистент
|
||||
|
||||
1. **Базовая настройка**
|
||||
- Выбор модели AI
|
||||
- Настройка температуры генерации
|
||||
- Лимиты токенов
|
||||
|
||||
2. **Правила и контекст**
|
||||
- Добавление правил поведения
|
||||
- Загрузка документов для обучения
|
||||
- Настройка тона общения
|
||||
|
||||
3. **Векторный поиск**
|
||||
- Индексация документов
|
||||
- Настройка релевантности
|
||||
- Обновление базы знаний
|
||||
|
||||
### Типы чатов
|
||||
|
||||
#### 1. Публичный чат
|
||||
- Доступен всем пользователям
|
||||
- AI может отвечать автоматически
|
||||
- Модерация сообщений
|
||||
|
||||
#### 2. Приватный чат
|
||||
- Личные сообщения между пользователями
|
||||
- Шифрование контента
|
||||
- История переписки
|
||||
|
||||
#### 3. Административный чат
|
||||
- Связь администраторов с клиентами
|
||||
- Управление через веб-интерфейс
|
||||
- Интеграция с CRM
|
||||
|
||||
#### 4. Гостевой чат
|
||||
- Для неавторизованных пользователей
|
||||
- Ограниченный функционал
|
||||
- Требуется согласие на обработку данных
|
||||
|
||||
### Управление сообщениями
|
||||
|
||||
- **История**: Полная история всех сообщений
|
||||
- **Поиск**: Поиск по содержимому, отправителю, дате
|
||||
- **Фильтры**: По каналам, типам, статусам
|
||||
- **Экспорт**: Выгрузка переписки в различных форматах
|
||||
|
||||
> 💡 **Подробная информация**: См. [AI Ассистент](./ai-assistant.md) для полного описания возможностей и [Настройка AI ассистента](./setup-ai-assistant.md) для инструкций по настройке.
|
||||
|
||||
---
|
||||
|
||||
## 🌐 Настройки приложения для работы с интернет пользователями
|
||||
|
||||
### Работа с гостями (неавторизованными пользователями)
|
||||
|
||||
DLE поддерживает работу с интернет-пользователями без обязательной регистрации, обеспечивая при этом соответствие требованиям регуляторов.
|
||||
|
||||
#### Роли и права доступа
|
||||
|
||||
**Guest (Гость)**:
|
||||
- ✅ Просмотр главной страницы
|
||||
- ✅ Чат с AI ассистентом
|
||||
- ❌ Отправка сообщений пользователям
|
||||
- ❌ Доступ к CRM и данным
|
||||
|
||||
**User (Пользователь)**:
|
||||
- ✅ Все права гостя
|
||||
- ✅ Получение сообщений
|
||||
- ✅ Отправка сообщений другим пользователям
|
||||
- ✅ Просмотр контактов
|
||||
- ✅ Приватный чат с администраторами
|
||||
- ✅ Просмотр базовых документов
|
||||
|
||||
#### Настройка гостевого доступа
|
||||
|
||||
**Путь**: Настройки → Пользователи → Гостевой доступ
|
||||
|
||||
1. **Включение гостевого режима**
|
||||
- Разрешить доступ без регистрации
|
||||
- Настройка ограничений
|
||||
- Лимиты на использование
|
||||
|
||||
2. **Сбор согласий**
|
||||
- Автоматический запрос согласия на обработку ПД
|
||||
- Политика конфиденциальности
|
||||
- Пользовательское соглашение
|
||||
- Политика использования cookies
|
||||
|
||||
3. **Идентификация гостей**
|
||||
- Уникальные идентификаторы
|
||||
- Шифрование данных
|
||||
- Анонимизация при необходимости
|
||||
|
||||
### Обработка персональных данных
|
||||
|
||||
#### Согласие на обработку ПД
|
||||
|
||||
При первом обращении гостя система:
|
||||
1. Показывает политику конфиденциальности
|
||||
2. Запрашивает согласие на обработку ПД
|
||||
3. Сохраняет факт согласия с временной меткой
|
||||
4. Предоставляет доступ к функциям
|
||||
|
||||
#### Управление согласиями
|
||||
|
||||
**Путь**: Настройки → Контент → Юридические документы
|
||||
|
||||
- Просмотр всех согласий
|
||||
- Экспорт данных по запросу
|
||||
- Удаление данных при отзыве согласия
|
||||
- История изменений согласий
|
||||
|
||||
### Публичные документы
|
||||
|
||||
#### Обязательные документы для публичного доступа
|
||||
|
||||
1. **Политика конфиденциальности**
|
||||
- Описание обработки ПД
|
||||
- Права пользователей
|
||||
- Контакты для обращений
|
||||
|
||||
2. **Пользовательское соглашение**
|
||||
- Условия использования сервиса
|
||||
- Права и обязанности
|
||||
- Ограничения ответственности
|
||||
|
||||
3. **Согласие на обработку персональных данных**
|
||||
- Форма согласия
|
||||
- Информация о целях обработки
|
||||
- Возможность отзыва
|
||||
|
||||
4. **Политика использования cookies**
|
||||
- Типы используемых cookies
|
||||
- Цели использования
|
||||
- Управление настройками
|
||||
|
||||
#### Публикация документов
|
||||
|
||||
**Путь**: Контент → Шаблоны
|
||||
|
||||
1. Выберите необходимые шаблоны
|
||||
2. Предварительный просмотр с автозаполнением
|
||||
3. Редактирование специфичных параметров
|
||||
4. Публикация:
|
||||
- **Публичное использование**: Доступно на сайте
|
||||
- **Внутреннее использование**: Только в CRM
|
||||
- **Печать**: Экспорт в PDF
|
||||
|
||||
### Настройки веб-интерфейса
|
||||
|
||||
#### Публичные страницы
|
||||
|
||||
- **Главная страница**: Информация о сервисе
|
||||
- **Чат**: Публичный чат с AI
|
||||
- **Контакты**: Форма обратной связи
|
||||
- **Документы**: Публичные юридические документы
|
||||
|
||||
#### Интеграция с сайтом
|
||||
|
||||
DLE может быть интегрирован на ваш сайт:
|
||||
- Виджет чата
|
||||
- Формы обратной связи
|
||||
- API для кастомных решений
|
||||
|
||||
### Управление контактами
|
||||
|
||||
#### Автоматическое создание контактов
|
||||
|
||||
При обращении гостя система:
|
||||
1. Создает контакт в CRM
|
||||
2. Присваивает уникальный идентификатор
|
||||
3. Сохраняет канал коммуникации
|
||||
4. Записывает историю взаимодействий
|
||||
|
||||
#### Объединение контактов
|
||||
|
||||
Если один пользователь обращается через разные каналы:
|
||||
- Автоматическое определение дубликатов
|
||||
- Объединение в единый контакт
|
||||
- Единая история коммуникаций
|
||||
|
||||
> 💡 **Важно**: Все настройки для работы с интернет-пользователями должны соответствовать требованиям регуляторов (см. раздел "Требования регулятора").
|
||||
|
||||
---
|
||||
|
||||
## ⚖️ Требования регулятора
|
||||
|
||||
### Соответствие законодательству
|
||||
|
||||
DLE разработан с учетом требований основных регуляторов по защите персональных данных и обеспечению прозрачности работы с пользователями.
|
||||
|
||||
### GDPR (General Data Protection Regulation)
|
||||
|
||||
**Европейское законодательство о защите данных**
|
||||
|
||||
#### Основные требования и их реализация:
|
||||
|
||||
1. **Право на информацию** ✅
|
||||
- Политика конфиденциальности доступна на сайте
|
||||
- Прозрачная информация о целях обработки данных
|
||||
- Контакты контролера данных
|
||||
|
||||
2. **Право на доступ** ✅
|
||||
- Пользователи могут запросить копию своих данных
|
||||
- Экспорт данных в структурированном формате
|
||||
- История обработки данных
|
||||
|
||||
3. **Право на исправление** ✅
|
||||
- Редактирование персональных данных
|
||||
- Обновление информации в профиле
|
||||
- Корректировка неточных данных
|
||||
|
||||
4. **Право на удаление ("право быть забытым")** ✅
|
||||
- Удаление данных по запросу
|
||||
- Отзыв согласия на обработку
|
||||
- Полное удаление из системы
|
||||
|
||||
5. **Право на ограничение обработки** ✅
|
||||
- Временная блокировка обработки
|
||||
- Сохранение данных без использования
|
||||
- Уведомление о снятии ограничений
|
||||
|
||||
6. **Право на переносимость данных** ✅
|
||||
- Экспорт данных в машиночитаемом формате
|
||||
- Передача данных другому контролеру
|
||||
- Структурированные форматы (JSON, CSV)
|
||||
|
||||
7. **Право на возражение** ✅
|
||||
- Отказ от обработки данных
|
||||
- Отзыв согласия
|
||||
- Остановка маркетинговых рассылок
|
||||
|
||||
8. **Автоматизированное принятие решений** ✅
|
||||
- Прозрачность использования AI
|
||||
- Возможность человеческого вмешательства
|
||||
- Объяснение логики решений
|
||||
|
||||
### CCPA (California Consumer Privacy Act)
|
||||
|
||||
**Калифорнийский закон о защите конфиденциальности**
|
||||
|
||||
#### Основные требования:
|
||||
|
||||
1. **Право знать** ✅
|
||||
- Информация о собираемых данных
|
||||
- Цели использования данных
|
||||
- Категории третьих сторон
|
||||
|
||||
2. **Право на удаление** ✅
|
||||
- Удаление персональных данных
|
||||
- Исключения для бизнес-нужд
|
||||
- Подтверждение удаления
|
||||
|
||||
3. **Право на отказ от продажи** ✅
|
||||
- DLE не продает данные пользователей
|
||||
- Прозрачная политика
|
||||
- Механизм отказа
|
||||
|
||||
4. **Недискриминация** ✅
|
||||
- Равный доступ к сервису
|
||||
- Отсутствие штрафов за использование прав
|
||||
- Справедливое ценообразование
|
||||
|
||||
### 152-ФЗ (Российское законодательство)
|
||||
|
||||
**Федеральный закон "О персональных данных"**
|
||||
|
||||
#### Основные требования:
|
||||
|
||||
1. **Согласие на обработку** ✅
|
||||
- Явное согласие субъекта ПД
|
||||
- Информированное согласие
|
||||
- Возможность отзыва
|
||||
|
||||
2. **Уведомление Роскомнадзора** ✅
|
||||
- Документация для уведомления
|
||||
- Описание целей обработки
|
||||
- Меры безопасности
|
||||
|
||||
3. **Локализация данных** ✅
|
||||
- Хранение на серверах в РФ (опционально)
|
||||
- Контроль местоположения данных
|
||||
- Соответствие требованиям
|
||||
|
||||
4. **Права субъектов ПД** ✅
|
||||
- Доступ к данным
|
||||
- Исправление данных
|
||||
- Удаление данных
|
||||
- Отзыв согласия
|
||||
|
||||
5. **Меры безопасности** ✅
|
||||
- Шифрование данных
|
||||
- Контроль доступа
|
||||
- Аудит действий
|
||||
- Резервное копирование
|
||||
|
||||
### Готовые документы для регулятора
|
||||
|
||||
DLE включает готовый пакет документов, необходимых для соответствия требованиям:
|
||||
|
||||
#### 1. Политика конфиденциальности
|
||||
- Описание обработки ПД
|
||||
- Права пользователей
|
||||
- Контакты контролера
|
||||
- Механизмы реализации прав
|
||||
|
||||
#### 2. Пользовательское соглашение
|
||||
- Условия использования
|
||||
- Права и обязанности сторон
|
||||
- Ограничения ответственности
|
||||
- Разрешение споров
|
||||
|
||||
#### 3. Согласие на обработку персональных данных
|
||||
- Форма согласия
|
||||
- Цели обработки
|
||||
- Сроки хранения
|
||||
- Право отзыва
|
||||
|
||||
#### 4. Политика использования cookies
|
||||
- Типы cookies
|
||||
- Цели использования
|
||||
- Управление настройками
|
||||
- Отключение cookies
|
||||
|
||||
#### 5. Документация для Роскомнадзора
|
||||
- Описание системы обработки ПД
|
||||
- Меры безопасности
|
||||
- Технические характеристики
|
||||
- Процедуры обработки запросов
|
||||
|
||||
### Аудит и соответствие
|
||||
|
||||
#### Регулярные проверки
|
||||
|
||||
- Проверка актуальности документов
|
||||
- Обновление политик при изменении законодательства
|
||||
- Аудит обработки данных
|
||||
- Тестирование механизмов реализации прав
|
||||
|
||||
#### Рекомендации
|
||||
|
||||
1. **Регулярное обновление**
|
||||
- Следите за изменениями в законодательстве
|
||||
- Обновляйте документы при необходимости
|
||||
- Проводите аудит соответствия
|
||||
|
||||
2. **Консультации**
|
||||
- Рекомендуется консультация с юристом
|
||||
- Проверка соответствия локальному законодательству
|
||||
- Адаптация под специфику вашего региона
|
||||
|
||||
3. **Документирование**
|
||||
- Ведите журнал обработки данных
|
||||
- Сохраняйте историю согласий
|
||||
- Документируйте запросы пользователей
|
||||
|
||||
> 💡 **Подробная информация**: См. [Безопасность DLE](./security.md) для детального описания мер безопасности и соответствия требованиям регуляторов.
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Обновления софта
|
||||
|
||||
### Политика обновлений
|
||||
|
||||
DLE предоставляет бесплатные обновления в течение 5 лет для держателей лицензионных токенов. Это включает:
|
||||
- ✅ Исправления ошибок (bug fixes)
|
||||
- ✅ Новые функции (features)
|
||||
- ✅ Улучшения безопасности (security updates)
|
||||
- ✅ Обновления зависимостей (dependencies)
|
||||
- ✅ Оптимизация производительности
|
||||
|
||||
### Типы обновлений
|
||||
|
||||
#### 1. Патч-обновления (Patch)
|
||||
- **Формат версии**: X.Y.**Z** (например, 1.0.1 → 1.0.2)
|
||||
- **Содержание**: Исправления ошибок, мелкие улучшения
|
||||
- **Частота**: По мере необходимости
|
||||
- **Критичность**: Обычно низкая-средняя
|
||||
|
||||
#### 2. Минорные обновления (Minor)
|
||||
- **Формат версии**: X.**Y**.Z (например, 1.0.2 → 1.1.0)
|
||||
- **Содержание**: Новые функции, улучшения
|
||||
- **Частота**: Ежеквартально или по мере готовности
|
||||
- **Критичность**: Средняя
|
||||
|
||||
#### 3. Мажорные обновления (Major)
|
||||
- **Формат версии**: **X**.Y.Z (например, 1.1.0 → 2.0.0)
|
||||
- **Содержание**: Крупные изменения, breaking changes
|
||||
- **Частота**: Раз в год или реже
|
||||
- **Критичность**: Высокая (требуется миграция)
|
||||
|
||||
### Процесс обновления
|
||||
|
||||
#### Автоматическое обновление (рекомендуется)
|
||||
|
||||
```bash
|
||||
# 1. Остановка текущей версии
|
||||
docker-compose down
|
||||
|
||||
# 2. Резервное копирование данных
|
||||
docker run --rm -v dle_postgres_data:/data -v $(pwd):/backup \
|
||||
alpine tar czf /backup/postgres_backup.tar.gz -C /data .
|
||||
|
||||
# 3. Обновление кода
|
||||
git pull origin main
|
||||
|
||||
# 4. Обновление образов
|
||||
docker-compose pull
|
||||
|
||||
# 5. Пересборка (если необходимо)
|
||||
docker-compose build
|
||||
|
||||
# 6. Запуск обновленной версии
|
||||
docker-compose up -d
|
||||
|
||||
# 7. Проверка статуса
|
||||
docker-compose ps
|
||||
docker-compose logs -f
|
||||
```
|
||||
|
||||
#### Ручное обновление
|
||||
|
||||
1. **Подготовка**
|
||||
- Проверьте текущую версию
|
||||
- Изучите changelog обновления
|
||||
- Создайте резервную копию
|
||||
|
||||
2. **Резервное копирование**
|
||||
```bash
|
||||
# База данных
|
||||
docker-compose exec postgres pg_dump -U dapp_user dapp_db > backup.sql
|
||||
|
||||
# Файлы загрузок
|
||||
tar -czf uploads_backup.tar.gz backend/uploads/
|
||||
|
||||
# Конфигурации
|
||||
cp .env .env.backup
|
||||
```
|
||||
|
||||
3. **Обновление**
|
||||
- Скачайте новую версию
|
||||
- Обновите зависимости
|
||||
- Примените миграции БД (если есть)
|
||||
|
||||
4. **Проверка**
|
||||
- Проверьте работоспособность
|
||||
- Протестируйте ключевые функции
|
||||
- Проверьте логи на ошибки
|
||||
|
||||
### Миграции базы данных
|
||||
|
||||
При обновлении могут потребоваться миграции БД:
|
||||
|
||||
```bash
|
||||
# Автоматическое применение миграций
|
||||
docker-compose exec backend npm run migrate
|
||||
|
||||
# Или вручную через SQL
|
||||
docker-compose exec postgres psql -U dapp_user -d dapp_db -f migrations/version_X.X.X.sql
|
||||
```
|
||||
|
||||
### Откат обновления
|
||||
|
||||
В случае проблем можно откатиться к предыдущей версии:
|
||||
|
||||
```bash
|
||||
# 1. Остановка текущей версии
|
||||
docker-compose down
|
||||
|
||||
# 2. Восстановление из резервной копии
|
||||
docker run --rm -v dle_postgres_data:/data -v $(pwd):/backup \
|
||||
alpine tar xzf /backup/postgres_backup.tar.gz -C /data
|
||||
|
||||
# 3. Переключение на предыдущую версию
|
||||
git checkout <previous-version-tag>
|
||||
|
||||
# 4. Запуск предыдущей версии
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### Уведомления об обновлениях
|
||||
|
||||
#### Каналы уведомлений
|
||||
|
||||
1. **GitHub Releases**
|
||||
- Официальные релизы
|
||||
- Changelog изменений
|
||||
- Инструкции по обновлению
|
||||
|
||||
2. **Email рассылка**
|
||||
- Уведомления о важных обновлениях
|
||||
- Информация о breaking changes
|
||||
- Рекомендации по обновлению
|
||||
|
||||
3. **В приложении**
|
||||
- Уведомления о доступных обновлениях
|
||||
- Информация о новых функциях
|
||||
- Ссылки на документацию
|
||||
|
||||
### Рекомендации по обновлениям
|
||||
|
||||
1. **Регулярность**
|
||||
- Обновляйтесь регулярно для получения исправлений безопасности
|
||||
- Не откладывайте критичные обновления
|
||||
- Следите за changelog
|
||||
|
||||
2. **Тестирование**
|
||||
- Тестируйте обновления на тестовой среде
|
||||
- Проверяйте совместимость с вашими данными
|
||||
- Убедитесь в работоспособности интеграций
|
||||
|
||||
3. **Резервное копирование**
|
||||
- Всегда создавайте резервные копии перед обновлением
|
||||
- Храните несколько версий бэкапов
|
||||
- Тестируйте восстановление из бэкапа
|
||||
|
||||
4. **Документирование**
|
||||
- Ведите журнал обновлений
|
||||
- Записывайте возникшие проблемы
|
||||
- Документируйте кастомные изменения
|
||||
|
||||
### Долгосрочная поддержка (LTS)
|
||||
|
||||
Для стабильных версий может быть доступна долгосрочная поддержка:
|
||||
- Расширенный период поддержки
|
||||
- Приоритетные исправления безопасности
|
||||
- Гарантированная совместимость
|
||||
|
||||
> 💡 **Важно**: Обновления предоставляются бесплатно в течение 5 лет для держателей лицензионных токенов. После этого периода обновления могут быть платными или доступны через сообщество.
|
||||
|
||||
---
|
||||
|
||||
## 📚 Дополнительные ресурсы
|
||||
|
||||
### Документация
|
||||
|
||||
- [Описание приложения](./application-description.md) - Полный обзор возможностей и преимуществ
|
||||
- [Инструкция по установке](./setup-instruction.md) - Пошаговая настройка
|
||||
- [Настройка AI ассистента](./setup-ai-assistant.md) - Конфигурация AI
|
||||
- [Безопасность DLE](./security.md) - Детальная информация о безопасности
|
||||
- [FAQ](./FAQ.md) - Ответы на частые вопросы
|
||||
|
||||
### Поддержка
|
||||
|
||||
- **Email**: info@hb3-accelerator.com
|
||||
- **Сайт**: https://hb3-accelerator.com
|
||||
- **GitHub**: https://github.com/VC-HB3-Accelerator
|
||||
|
||||
---
|
||||
|
||||
**© 2024-2025 Тарабанов Александр Викторович. Все права защищены.**
|
||||
|
||||
**Digital Legal Entity (DLE)** - комплексное решение для управления бизнесом с блокчейн и AI.
|
||||
|
||||
**Версия документа**: 1.0.0
|
||||
**Последнее обновление**: January 2025
|
||||
|
||||
@@ -452,6 +452,7 @@ onMounted(() => {
|
||||
margin: 0 auto;
|
||||
padding: 40px;
|
||||
min-height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
@@ -713,7 +714,7 @@ onMounted(() => {
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
min-height: 300px;
|
||||
min-height: 400px;
|
||||
border-radius: 8px;
|
||||
margin: 1.5rem 0;
|
||||
display: block;
|
||||
@@ -721,6 +722,12 @@ onMounted(() => {
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.content-text :deep(video.ql-video) {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.content-text :deep(video:focus) {
|
||||
outline: 2px solid var(--color-primary);
|
||||
outline-offset: 2px;
|
||||
|
||||
@@ -22,13 +22,23 @@ import Quill from 'quill';
|
||||
import 'quill/dist/quill.snow.css';
|
||||
import api from '../../api/axios';
|
||||
|
||||
// Импортируем и регистрируем модуль изменения размера изображений
|
||||
let ImageResize;
|
||||
try {
|
||||
ImageResize = require('quill-image-resize-module').default || require('quill-image-resize-module');
|
||||
Quill.register('modules/imageResize', ImageResize);
|
||||
} catch (error) {
|
||||
console.warn('[RichTextEditor] Не удалось загрузить модуль изменения размера изображений:', error);
|
||||
// Функция для загрузки и регистрации модуля изменения размера изображений
|
||||
async function loadImageResizeModule() {
|
||||
try {
|
||||
// Используем динамический импорт для совместимости с Vite
|
||||
const module = await import('quill-image-resize-module');
|
||||
const ImageResize = module.default || module.ImageResize || module;
|
||||
if (ImageResize && typeof ImageResize === 'function') {
|
||||
Quill.register('modules/imageResize', ImageResize);
|
||||
return true;
|
||||
} else if (ImageResize && ImageResize.default && typeof ImageResize.default === 'function') {
|
||||
Quill.register('modules/imageResize', ImageResize.default);
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[RichTextEditor] Не удалось загрузить модуль изменения размера изображений:', error);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
@@ -63,26 +73,36 @@ const toolbarOptions = [
|
||||
['clean']
|
||||
];
|
||||
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
if (!editorContainer.value) return;
|
||||
|
||||
// Загружаем модуль изменения размера изображений перед инициализацией
|
||||
const imageResizeLoaded = await loadImageResizeModule();
|
||||
|
||||
// Конфигурация модулей
|
||||
const modulesConfig = {
|
||||
toolbar: {
|
||||
container: toolbarOptions,
|
||||
handlers: {
|
||||
'image': handleImageClick,
|
||||
'video': handleVideoClick
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Добавляем imageResize только если модуль загружен
|
||||
if (imageResizeLoaded) {
|
||||
modulesConfig.imageResize = {
|
||||
parchment: Quill.import('parchment'),
|
||||
modules: ['Resize', 'DisplaySize', 'Toolbar']
|
||||
};
|
||||
}
|
||||
|
||||
// Инициализация Quill
|
||||
quill = new Quill(editorContainer.value, {
|
||||
theme: 'snow',
|
||||
placeholder: props.placeholder,
|
||||
modules: {
|
||||
toolbar: {
|
||||
container: toolbarOptions,
|
||||
handlers: {
|
||||
'image': handleImageClick,
|
||||
'video': handleVideoClick
|
||||
}
|
||||
},
|
||||
imageResize: {
|
||||
parchment: Quill.import('parchment'),
|
||||
modules: ['Resize', 'DisplaySize', 'Toolbar']
|
||||
}
|
||||
}
|
||||
modules: modulesConfig
|
||||
});
|
||||
|
||||
// Устанавливаем начальное значение
|
||||
@@ -368,14 +388,55 @@ defineExpose({
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
:deep(.ql-snow img),
|
||||
:deep(.ql-snow video) {
|
||||
:deep(.ql-snow img) {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 4px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
/* Стили для видео в редакторе - на всю ширину */
|
||||
:deep(.ql-snow video),
|
||||
:deep(.ql-editor video) {
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
min-height: 400px;
|
||||
border-radius: 8px;
|
||||
margin: 1.5rem 0;
|
||||
display: block;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
background: #000;
|
||||
}
|
||||
|
||||
:deep(.ql-snow video.ql-video),
|
||||
:deep(.ql-editor video.ql-video) {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
/* Стили для iframe в редакторе */
|
||||
:deep(.ql-snow iframe),
|
||||
:deep(.ql-editor iframe) {
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
min-height: 400px;
|
||||
border-radius: 8px;
|
||||
margin: 1.5rem 0;
|
||||
display: block;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
background: #000;
|
||||
border: none;
|
||||
}
|
||||
|
||||
:deep(.ql-snow iframe.ql-video),
|
||||
:deep(.ql-editor iframe.ql-video) {
|
||||
min-height: 400px;
|
||||
aspect-ratio: 16 / 9;
|
||||
}
|
||||
|
||||
/* Стили для изменения размера изображений */
|
||||
:deep(.ql-image-resize) {
|
||||
display: inline-block;
|
||||
@@ -394,13 +455,5 @@ defineExpose({
|
||||
cursor: nwse-resize;
|
||||
box-shadow: 0 0 2px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
:deep(.ql-snow img),
|
||||
:deep(.ql-snow video) {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 4px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -569,6 +569,7 @@ onMounted(async () => {
|
||||
border: 1px solid #e9ecef;
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
@@ -635,6 +636,64 @@ onMounted(async () => {
|
||||
color: var(--color-grey-dark);
|
||||
}
|
||||
|
||||
/* Стили для видео в редакторе */
|
||||
.content-form :deep(video) {
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
min-height: 400px;
|
||||
border-radius: 8px;
|
||||
margin: 1.5rem 0;
|
||||
display: block;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.content-form :deep(video.ql-video) {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.content-form :deep(video:focus) {
|
||||
outline: 2px solid var(--color-primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Стили для iframe в редакторе (для внешних видео) */
|
||||
.content-form :deep(iframe) {
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
min-height: 400px;
|
||||
border-radius: 8px;
|
||||
margin: 1.5rem 0;
|
||||
display: block;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
background: #000;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.content-form :deep(iframe.ql-video) {
|
||||
min-height: 400px;
|
||||
aspect-ratio: 16 / 9;
|
||||
}
|
||||
|
||||
.content-form :deep(iframe:focus) {
|
||||
outline: 2px solid var(--color-primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Стили для изображений в редакторе */
|
||||
.content-form :deep(img) {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
margin: 1.5rem 0;
|
||||
display: block;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.category-select-wrapper {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
|
||||
@@ -396,10 +396,16 @@
|
||||
<div class="ssl-section">
|
||||
<div class="section-header">
|
||||
<h2>SSL сертификат</h2>
|
||||
<div v-if="isDevelopment" style="font-size: 12px; color: #666; margin-top: 5px;">
|
||||
Debug: isEditor={{ isEditor }}, currentRole={{ currentRole }}, isLoadingSsl={{ isLoadingSsl }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!isEditor" class="access-denied-message">
|
||||
<p>⚠️ Управление SSL доступно только пользователям с ролью "Редактор"</p>
|
||||
<p v-if="isDevelopment" style="font-size: 12px; color: #666;">
|
||||
Текущая роль: {{ currentRole }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
@@ -407,7 +413,7 @@
|
||||
<div v-if="isLoadingSsl">
|
||||
Загрузка статуса SSL...
|
||||
</div>
|
||||
<div v-else>
|
||||
<div v-else>
|
||||
<div v-if="sslStatus && sslStatus.success && sslStatus.allCertificates && sslStatus.allCertificates.length">
|
||||
<div class="ssl-info">
|
||||
<div
|
||||
@@ -415,15 +421,24 @@
|
||||
:key="cert.name"
|
||||
class="ssl-info-item"
|
||||
>
|
||||
<label>{{ cert.name }}</label>
|
||||
<label>{{ cert.name || 'Без имени' }}</label>
|
||||
<span :class="{ 'expiring-soon': isCertExpiringSoon(cert.expiryDate) }">
|
||||
{{ cert.expiryDate || 'Без данных' }}
|
||||
{{ cert.expiryDate ? formatDate(cert.expiryDate) : 'Без данных' }}
|
||||
</span>
|
||||
<div v-if="cert.domains && cert.domains.length" class="ssl-domains">
|
||||
Домены: {{ cert.domains.join(', ') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="ssl-no-cert">
|
||||
SSL сертификат не найден для текущего домена.
|
||||
<p>SSL сертификат не найден для текущего домена.</p>
|
||||
<p v-if="sslStatus && sslStatus.domain" class="ssl-domain-info">
|
||||
Домен: {{ sslStatus.domain }}
|
||||
</p>
|
||||
<p v-if="sslStatus && !sslStatus.success" class="ssl-error-info">
|
||||
Ошибка: {{ sslStatus.error || 'Неизвестная ошибка' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -441,9 +456,13 @@
|
||||
class="action-btn ssl-btn renew"
|
||||
:disabled="isLoading"
|
||||
@click="renewSslCertificate"
|
||||
:title="isLoading ? 'Выполняется...' : 'Получить или обновить SSL сертификат'"
|
||||
>
|
||||
🔐 Получить / обновить SSL
|
||||
</button>
|
||||
<div v-if="!isEditor && isDevelopment" style="font-size: 12px; color: #f00; margin-top: 5px;">
|
||||
Кнопка скрыта: isEditor=false, currentRole={{ currentRole }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -550,6 +569,9 @@ const router = useRouter();
|
||||
const { currentRole, canManageSettings } = usePermissions();
|
||||
const isEditor = computed(() => currentRole.value === ROLES.EDITOR);
|
||||
|
||||
// Отладочная информация (только для разработки)
|
||||
const isDevelopment = computed(() => import.meta.env.DEV || import.meta.env.MODE === 'development');
|
||||
|
||||
// Состояние
|
||||
const domain = ref(null);
|
||||
const isOnline = ref(false);
|
||||
@@ -1223,13 +1245,21 @@ const sendBackup = async () => {
|
||||
const loadSslStatus = async () => {
|
||||
if (!isEditor.value) {
|
||||
// Не показываем ошибку, если пользователь не редактор - просто не загружаем статус
|
||||
console.log('[VDS] Пользователь не является редактором, пропускаем загрузку SSL статуса');
|
||||
return;
|
||||
}
|
||||
console.log('[VDS] Загрузка SSL статуса...');
|
||||
isLoadingSsl.value = true;
|
||||
try {
|
||||
const response = await axios.get('/vds/ssl/status');
|
||||
console.log('[VDS] Ответ от /vds/ssl/status:', response.data);
|
||||
if (response.data.success) {
|
||||
sslStatus.value = response.data;
|
||||
console.log('[VDS] SSL статус загружен:', {
|
||||
hasCertificates: response.data.allCertificates?.length > 0,
|
||||
certificatesCount: response.data.allCertificates?.length || 0,
|
||||
domain: response.data.domain
|
||||
});
|
||||
} else {
|
||||
console.warn('[VDS] Получение статуса SSL не успешно:', response.data);
|
||||
sslStatus.value = null;
|
||||
@@ -1238,16 +1268,24 @@ const loadSslStatus = async () => {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка получения статуса SSL:', error);
|
||||
console.error('Детали ошибки:', {
|
||||
status: error.response?.status,
|
||||
statusText: error.response?.statusText,
|
||||
data: error.response?.data,
|
||||
message: error.message
|
||||
});
|
||||
const errorMessage = error.response?.data?.error || error.message || 'Неизвестная ошибка';
|
||||
|
||||
// Если VDS не настроена, это нормальная ситуация - не показываем ошибку
|
||||
if (errorMessage.includes('VDS не настроена') || error.response?.status === 400) {
|
||||
console.log('[VDS] VDS не настроена, это нормально');
|
||||
sslStatus.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// Если ошибка аутентификации (401), это нормальная ситуация - пользователь не авторизован
|
||||
if (error.response?.status === 401 || errorMessage.includes('Требуется аутентификация') || errorMessage.includes('аутентификация')) {
|
||||
console.log('[VDS] Ошибка аутентификации, это нормально');
|
||||
sslStatus.value = null;
|
||||
return;
|
||||
}
|
||||
@@ -1293,26 +1331,38 @@ const checkSslStatus = async () => {
|
||||
};
|
||||
|
||||
const renewSslCertificate = async () => {
|
||||
console.log('[VDS] renewSslCertificate вызвана, isEditor:', isEditor.value);
|
||||
if (!isEditor.value) {
|
||||
console.warn('[VDS] Пользователь не является редактором, доступ запрещен');
|
||||
alert('Только пользователи с ролью "Редактор" могут получать SSL сертификаты');
|
||||
return;
|
||||
}
|
||||
if (!confirm('Получить/обновить SSL сертификат от Let\'s Encrypt? Это может занять некоторое время.')) {
|
||||
console.log('[VDS] Пользователь отменил получение SSL сертификата');
|
||||
return;
|
||||
}
|
||||
console.log('[VDS] Начинаем получение SSL сертификата...');
|
||||
isLoading.value = true;
|
||||
try {
|
||||
const response = await axios.post('/vds/ssl/renew', {
|
||||
sslProvider: 'letsencrypt'
|
||||
});
|
||||
console.log('[VDS] Ответ от /vds/ssl/renew:', response.data);
|
||||
if (response.data.success) {
|
||||
alert('SSL сертификат успешно получен/обновлен');
|
||||
await loadSslStatus();
|
||||
} else {
|
||||
console.error('[VDS] Ошибка получения SSL сертификата:', response.data);
|
||||
alert('Ошибка получения SSL сертификата: ' + (response.data.error || 'Неизвестная ошибка'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка получения SSL сертификата:', error);
|
||||
console.error('Детали ошибки:', {
|
||||
status: error.response?.status,
|
||||
statusText: error.response?.statusText,
|
||||
data: error.response?.data,
|
||||
message: error.message
|
||||
});
|
||||
const errorMessage = error.response?.data?.error || error.message || 'Неизвестная ошибка';
|
||||
const errorDetails = error.response?.data?.details || '';
|
||||
|
||||
@@ -1519,6 +1569,7 @@ const updateCharts = () => {
|
||||
|
||||
// Жизненный цикл
|
||||
onMounted(async () => {
|
||||
console.log('[VDS] Компонент монтирован, isEditor:', isEditor.value, 'currentRole:', currentRole.value);
|
||||
await loadSettings();
|
||||
await loadContainers();
|
||||
await initCharts();
|
||||
@@ -1526,8 +1577,11 @@ onMounted(async () => {
|
||||
|
||||
// Загружаем пользователей только для редакторов
|
||||
if (isEditor.value) {
|
||||
console.log('[VDS] Пользователь является редактором, загружаем пользователей и SSL статус');
|
||||
await loadUsers();
|
||||
await loadSslStatus();
|
||||
} else {
|
||||
console.log('[VDS] Пользователь НЕ является редактором, пропускаем загрузку пользователей и SSL');
|
||||
}
|
||||
|
||||
// Обновляем статистику каждые 5 секунд
|
||||
|
||||
@@ -239,10 +239,27 @@ function getStatusText(status) {
|
||||
}
|
||||
|
||||
function formatContent(content) {
|
||||
// Простое форматирование контента
|
||||
// Форматирование контента
|
||||
if (!content) return '';
|
||||
|
||||
// Заменяем переносы строк на <br>
|
||||
// Если контент уже содержит HTML теги (например, из RichTextEditor), обрабатываем его
|
||||
if (/<[a-z][\s\S]*>/i.test(content)) {
|
||||
// Преобразуем iframe с локальными видео-файлами обратно в тег video
|
||||
// Quill может преобразовывать video в iframe, но для локальных файлов нужен тег video
|
||||
content = content.replace(/<iframe([^>]*?)src=["']([^"']+)["']([^>]*?)><\/iframe>/gi, (match, attrs1, url, attrs2) => {
|
||||
// Проверяем, является ли это видео-файл из нашей системы
|
||||
if (url.includes('/api/uploads/media/') && url.includes('/file')) {
|
||||
// Преобразуем в тег video для локальных видео-файлов
|
||||
return `<video controls class="ql-video" style="max-width: 100%; width: 100%; height: auto; min-height: 400px; border-radius: 8px; margin: 1.5rem 0; display: block;" src="${url}"></video>`;
|
||||
}
|
||||
// Оставляем iframe для внешних видео (YouTube, Vimeo и т.д.)
|
||||
return match;
|
||||
});
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
// Иначе заменяем переносы строк на <br>
|
||||
return content.replace(/\n/g, '<br>');
|
||||
}
|
||||
|
||||
@@ -349,6 +366,8 @@ onMounted(() => {
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 25px;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08);
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.page-content {
|
||||
@@ -356,6 +375,8 @@ onMounted(() => {
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 25px;
|
||||
border: 1px solid #e9ecef;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.content-section {
|
||||
@@ -364,6 +385,8 @@ onMounted(() => {
|
||||
padding: 25px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid #e9ecef;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.content-section:last-child {
|
||||
@@ -387,6 +410,64 @@ onMounted(() => {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* Стили для видео в контенте */
|
||||
.main-content :deep(video) {
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
min-height: 400px;
|
||||
border-radius: 8px;
|
||||
margin: 1.5rem 0;
|
||||
display: block;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.main-content :deep(video.ql-video) {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.main-content :deep(video:focus) {
|
||||
outline: 2px solid var(--color-primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Стили для iframe в контенте (для внешних видео) */
|
||||
.main-content :deep(iframe) {
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
min-height: 400px;
|
||||
border-radius: 8px;
|
||||
margin: 1.5rem 0;
|
||||
display: block;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
background: #000;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.main-content :deep(iframe.ql-video) {
|
||||
min-height: 400px;
|
||||
aspect-ratio: 16 / 9;
|
||||
}
|
||||
|
||||
.main-content :deep(iframe:focus) {
|
||||
outline: 2px solid var(--color-primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Стили для изображений в контенте */
|
||||
.main-content :deep(img) {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
margin: 1.5rem 0;
|
||||
display: block;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.file-preview { display: flex; flex-direction: column; gap: 12px; }
|
||||
.pdf-embed { width: 100%; height: 70vh; border: 1px solid #e9ecef; border-radius: var(--radius-sm); }
|
||||
.image-preview { max-width: 100%; border: 1px solid #e9ecef; border-radius: var(--radius-sm); }
|
||||
|
||||
@@ -126,6 +126,7 @@ services:
|
||||
vector-search:
|
||||
condition: service_started
|
||||
volumes:
|
||||
- ./backend:/app
|
||||
- backend_node_modules:/app/node_modules
|
||||
- ./ssl:/app/ssl:ro
|
||||
# Доступ к Docker socket для управления контейнерами на VDS
|
||||
@@ -160,6 +161,8 @@ services:
|
||||
- OLLAMA_EMBEDDINGS_MODEL=${OLLAMA_EMBEDDINGS_MODEL:-mxbai-embed-large:latest}
|
||||
# 🆕 Исправленный URL для Vector Search
|
||||
- VECTOR_SEARCH_URL=http://dapp-vector-search:8001
|
||||
# Команда запуска для production
|
||||
command: ["yarn", "run", "start"]
|
||||
# НЕ открываем порт 8000 наружу - только nginx подключается
|
||||
healthcheck:
|
||||
test: ["CMD", "node", "-e", "require('http').get('http://localhost:8000/api/health', (res) => { process.exit(res.statusCode === 200 ? 0 : 1) })"]
|
||||
|
||||
Reference in New Issue
Block a user