Files
DLE/backend/routes/api.js

855 lines
27 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const express = require('express');
const router = express.Router();
const { ChatOllama } = require('@langchain/ollama');
const { PGVectorStore } = require('@langchain/community/vectorstores/pgvector');
const { OllamaEmbeddings } = require('@langchain/ollama');
const { RunnableSequence } = require('@langchain/core/runnables');
const { StringOutputParser } = require('@langchain/core/output_parsers');
const { PromptTemplate } = require('@langchain/core/prompts');
const { Pool } = require('pg');
const { ethers } = require('ethers');
const contractABI = require('../artifacts/contracts/MyContract.sol/MyContract.json').abi;
const crypto = require('crypto');
const TelegramBotService = require('../services/telegramBot');
require('dotenv').config();
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false
});
const chat = new ChatOllama({
model: 'mistral',
baseUrl: 'http://localhost:11434',
temperature: 0.7,
format: 'json'
});
const embeddings = new OllamaEmbeddings({
model: 'mistral',
baseUrl: 'http://localhost:11434',
requestOptions: {
headers: {
'Content-Type': 'application/json'
}
},
dimensions: 4096,
stripNewLines: true,
maxConcurrency: 1,
maxRetries: 3,
timeout: 10000
});
let vectorStore;
let contract;
async function initVectorStore() {
try {
console.log('Начинаем инициализацию векторного хранилища...');
vectorStore = await PGVectorStore.initialize(
embeddings,
{
postgresConnectionOptions: {
connectionString: process.env.DATABASE_URL
},
tableName: 'documents',
columns: {
idColumnName: 'id',
vectorColumnName: 'embedding',
contentColumnName: 'content',
metadataColumnName: 'metadata',
}
}
);
console.log('Векторное хранилище инициализировано:', {
tableName: 'documents',
columns: {
structure: (await pool.query(`
SELECT
column_name,
data_type,
is_nullable,
column_default
FROM information_schema.columns
WHERE table_name = 'documents'
ORDER BY ordinal_position
`)).rows.map(row => ({
name: row.column_name,
type: row.data_type,
nullable: row.is_nullable,
default: row.column_default
}))
},
config: {
tableName: vectorStore.tableName,
columns: vectorStore.columns,
client: vectorStore.client ? 'Connected' : 'Not connected',
embeddings: vectorStore.embeddings ? 'Initialized' : 'Not initialized'
}
});
} catch (error) {
console.error('Ошибка инициализации векторного хранилища:', error);
throw error;
}
}
async function initContract() {
try {
const provider = new ethers.JsonRpcProvider(process.env.ETHEREUM_NETWORK_URL);
// Проверяем подключение к сети
const network = await provider.getNetwork();
console.log('Подключены к сети:', network.chainId);
contract = new ethers.Contract(
process.env.CONTRACT_ADDRESS,
contractABI,
provider
);
// Проверяем что контракт существует
const code = await provider.getCode(process.env.CONTRACT_ADDRESS);
if (code === '0x') {
throw new Error('Contract not deployed at this address');
}
// Проверяем подключение
const owner = await contract.owner();
console.log('Владелец контракта:', owner);
console.log('Контракт инициализирован:', process.env.CONTRACT_ADDRESS);
} catch (error) {
console.error('Ошибка инициализации контракта:', error);
// Если контракт не найден, не пытаемся переподключиться
if (error.message.includes('not deployed')) {
console.error('Контракт не найден по указанному адресу');
return;
}
// Пробуем переподключиться через 5 секунд
setTimeout(initContract, 5000);
}
}
// Инициализируем при старте
initVectorStore().catch(console.error);
initContract().catch(console.error);
// Проверяем подключение к БД при старте
pool.connect((err, client, release) => {
if (err) {
console.error('Ошибка подключения к PostgreSQL:', err);
} else {
console.log('Успешное подключение к PostgreSQL');
release();
}
});
// Middleware для проверки аутентификации
function requireAuth(req, res, next) {
if (!req.session?.siwe?.address) {
return res.status(401).json({ error: 'Not authenticated' });
}
next();
}
// Генерация случайного nonce
function generateNonce() {
return crypto.randomBytes(16).toString('base64').replace(/[^a-zA-Z0-9]/g, '');
}
// Получение nonce для подписи
router.get('/nonce', (req, res) => {
try {
setCorsHeaders(res);
const nonce = generateNonce();
console.log('Сгенерирован новый nonce:', nonce);
res.json({ nonce });
} catch (error) {
console.error('Ошибка генерации nonce:', error);
res.status(500).json({ error: 'Server error' });
}
});
// Верификация подписи
router.post('/verify', async (req, res) => {
try {
const { message, signature } = req.body;
// Обновляем данные сессии
Object.assign(req.session, {
authenticated: true,
siwe: message,
userAddress: message.address,
cookie: {
maxAge: 7 * 24 * 60 * 60 * 1000
}
});
// Ждем сохранения
await new Promise((resolve) => {
req.session.save(resolve);
});
console.log('Session saved:', {
id: req.sessionID,
authenticated: req.session.authenticated,
address: req.session.userAddress
});
// Проверяем права админа сразу после входа
const contractOwner = await contract.owner();
const isAdmin = message.address.toLowerCase() === contractOwner.toLowerCase();
console.log('Проверка прав после входа:', {
userAddress: message.address,
contractOwner,
isAdmin
});
res.json({
ok: true,
isAdmin
});
} catch (error) {
console.error('Verify error:', error);
res.status(400).json({ error: error.message });
}
});
// Создаем шаблон промпта для RAG
const TEMPLATE = `Вы - ассистент в DApp приложении. Используйте следующий контекст для ответа:
Контекст: {context}
Вопрос пользователя: {question}
Отвечайте кратко и по существу, основываясь на предоставленном контексте. Если контекст пустой или не релевантный,
используйте свои базовые знания о DApp и блокчейне.`;
const prompt = PromptTemplate.fromTemplate(TEMPLATE);
// Создаем RAG цепочку
const chain = RunnableSequence.from([
{
context: async (input) => {
try {
const results = await vectorStore.similaritySearch(
input.question,
1,
{ type: 'approved_chat' }
);
if (!results.length) return '';
return results
.filter(doc => doc.pageContent)
.map(doc => doc.pageContent)
.join('\n\n');
} catch (error) {
console.error('Ошибка поиска контекста:', error);
return '';
}
},
question: (input) => input.message
},
prompt,
chat,
new StringOutputParser()
]);
// Функция проверки работоспособности эмбеддингов
async function checkEmbeddings() {
try {
const testEmbed = await embeddings.embedQuery('test');
console.log('Эмбеддинги работают, размерность:', testEmbed.length);
if (testEmbed.length !== 4096) {
throw new Error(`Неверная размерность: ${testEmbed.length}, ожидалось: 4096`);
}
return true;
} catch (error) {
console.error('Ошибка эмбеддингов:', error);
return false;
}
}
router.post('/chat', requireAuth, async (req, res) => {
try {
const { message } = req.body;
const userAddress = req.session.siwe.address;
// Получаем или создаем пользователя
let userResult = await pool.query(
'SELECT id FROM users WHERE LOWER(address) = LOWER($1)',
[userAddress]
);
if (userResult.rows.length === 0) {
userResult = await pool.query(
'INSERT INTO users (address) VALUES (LOWER($1)) RETURNING id',
[userAddress]
);
}
const userId = userResult.rows[0].id;
// Создаем входные данные для chain
const input = {
message: message,
question: message
};
// Проверяем эмбеддинги перед использованием
if (!await checkEmbeddings()) {
console.warn('Embeddings service unavailable, continuing without context');
try {
const response = await chain.invoke(input);
// Сохраняем в базу без контекста
await pool.query(
'INSERT INTO chat_history (user_id, message, response) VALUES ($1, $2, $3)',
[userId, message, response]
);
return res.json({ response });
} catch (error) {
console.error('Ошибка генерации ответа:', error);
throw error;
}
}
const response = await chain.invoke(input);
// Сохраняем в базу с обработкой ошибок
try {
// Получаем похожие документы
const similarDocs = await vectorStore.similaritySearch(
message,
1,
{ type: 'approved_chat' }
);
// Извлекаем ID чатов из метаданных
const contextIds = similarDocs
.map(doc => doc.metadata?.chatId)
.filter(id => typeof id === 'number');
await pool.query(
'INSERT INTO chat_history (user_id, message, response, context_docs) VALUES ($1, $2, $3, $4::integer[])',
[userId, message, response, contextIds]
);
} catch (dbError) {
console.error('Ошибка сохранения в БД:', dbError);
// Продолжаем выполнение даже при ошибке сохранения
}
res.json({ response });
} catch (error) {
console.error('Ошибка чата:', error);
res.status(500).json({
error: error.message,
details: error.stack
});
}
});
// Получение истории чата
router.get('/chat/history', requireAuth, async (req, res) => {
try {
setCorsHeaders(res);
const userAddress = req.session.siwe.address;
// Получаем историю чата пользователя
const result = await pool.query(
`SELECT ch.*
FROM chat_history ch
JOIN users u ON ch.user_id = u.id
WHERE LOWER(u.address) = LOWER($1)
ORDER BY ch.created_at DESC`,
[userAddress]
);
res.json({ history: result.rows });
} catch (error) {
console.error('Ошибка получения истории:', error);
res.status(500).json({ error: 'Server error' });
}
});
// Получение списка пользователей
router.get('/users', requireAuth, async (req, res) => {
try {
console.log('Запрос списка пользователей');
const users = await pool.query(
'SELECT id, LOWER(address) as address, created_at FROM users ORDER BY created_at DESC'
);
console.log('Найдено пользователей:', users.rows);
res.json({ users: users.rows });
} catch (error) {
console.error('Ошибка получения пользователей:', error);
res.status(500).json({ error: 'Ошибка сервера' });
}
});
// Проверка на админа
router.get('/admin/check', requireAuth, async (req, res) => {
try {
if (!contract) {
await initContract();
if (!contract) {
throw new Error('Contract not initialized');
}
}
// Получаем адрес из сессии
const userAddress = req.session.siwe.address;
console.log('Проверка админа, адрес из сессии:', userAddress);
const contractOwner = await contract.owner();
console.log('Проверка админа:', {
userAddress,
contractOwner
});
const isAdmin = userAddress.toLowerCase() === contractOwner.toLowerCase();
console.log('Результат проверки админа:', isAdmin);
res.json({ isAdmin });
} catch (error) {
console.error('Ошибка проверки админа:', error);
res.status(500).json({
error: 'Server error',
details: error.message,
code: error.code
});
}
});
// Общая функция для установки CORS заголовков
function setCorsHeaders(res) {
res.header('Access-Control-Allow-Origin', 'http://127.0.0.1:5173');
res.header('Access-Control-Allow-Credentials', 'true');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.header('Access-Control-Allow-Headers', 'Content-Type, Accept');
}
// Получение всех чатов для админа
router.get('/admin/chats', requireAdmin, async (req, res) => {
try {
setCorsHeaders(res);
const chats = await pool.query(`
SELECT
ch.id,
LOWER(u.address) as address,
ch.message,
ch.response,
ch.created_at,
ch.context_docs,
EXISTS (
SELECT 1 FROM documents d
WHERE d.metadata->>'chatId' = ch.id::text
AND d.metadata->>'type' = 'approved_chat'
) as is_approved
FROM chat_history ch
JOIN users u ON ch.user_id = u.id
ORDER BY ch.created_at DESC
`);
console.log('Получено чатов:', chats.rows.length);
if (chats.rows.length > 0) {
console.log('Пример чата:', {
id: chats.rows[0].id,
address: chats.rows[0].address,
is_approved: chats.rows[0].is_approved
});
}
res.json({ chats: chats.rows });
} catch (error) {
console.error('Ошибка получения чатов:', error);
res.status(500).json({
error: 'Server error',
details: error.message
});
}
});
// Одобрение чата для обучения
router.post('/admin/approve', requireAuth, async (req, res) => {
try {
const userAddress = req.session.siwe.address;
const contractOwner = await contract.owner();
if (userAddress.toLowerCase() !== contractOwner.toLowerCase()) {
return res.status(403).json({ error: 'Not authorized' });
}
const { chatId } = req.body;
// Обновляем статус в базе
await pool.query(
'UPDATE chat_history SET is_approved = true WHERE id = $1',
[chatId]
);
// Добавляем в векторное хранилище для обучения
const chat = await pool.query(
`SELECT message, response FROM chat_history WHERE id = $1`,
[chatId]
);
if (chat.rows.length > 0) {
const { message, response } = chat.rows[0];
console.log('Добавляем в векторное хранилище:', {
message: message.substring(0, 50) + '...',
response: response.substring(0, 50) + '...',
chatId
});
const document = {
pageContent: `Q: ${message}\nA: ${response}`,
metadata: {
type: 'approved_chat',
approvedBy: userAddress,
chatId: chatId
}
};
// Проверяем работу эмбеддингов
try {
const testEmbedding = await embeddings.embedQuery('test');
console.log('Эмбеддинги работают, размерность:', testEmbedding.length);
} catch (error) {
console.error('Ошибка проверки эмбеддингов:', error);
throw new Error('Embeddings error: ' + error.message);
}
console.log('Документ для добавления:', {
pageContent: document.pageContent.substring(0, 100) + '...',
metadata: document.metadata,
vectorStore: {
tableName: vectorStore.tableName,
columns: vectorStore.columns
}
});
// Проверяем существование таблицы и её структуру
const tableInfo = await pool.query(`
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_name = 'documents'
);
`);
console.log('Таблица documents существует:', tableInfo.rows[0].exists);
if (tableInfo.rows[0].exists) {
const columns = await pool.query(`
SELECT column_name, data_type
FROM information_schema.columns
WHERE table_name = 'documents'
ORDER BY ordinal_position;
`);
console.log('Структура таблицы documents:',
columns.rows.map(row => `${row.column_name} (${row.data_type})`)
);
}
await vectorStore.addDocuments([
document
]);
// Проверяем, что документ добавлен
const added = await vectorStore.similaritySearch(
document.pageContent,
1,
{ chatId: chatId }
);
console.log('Проверка добавления документа:', {
found: added.length > 0,
document: added[0]?.pageContent.substring(0, 100) + '...'
});
console.log('Успешно добавлено в векторное хранилище');
}
res.json({ success: true });
} catch (error) {
console.error('Ошибка одобрения:', error);
res.status(500).json({
error: 'Server error',
details: error.message,
code: error.code
});
}
});
// Улучшаем проверку авторизации админа
async function requireAdmin(req, res, next) {
if (!req.session?.siwe?.address) {
return res.status(401).json({
error: 'Not authenticated',
details: 'Please sign in first'
});
}
try {
// Получаем адреса
const userAddress = req.session.siwe.address;
const contractOwner = await contract.owner();
console.log('Проверка админа:', {
userAddress: userAddress,
contractOwner: contractOwner
});
if (userAddress.toLowerCase() !== contractOwner.toLowerCase()) {
return res.status(403).json({
error: 'Not authorized',
details: 'Only contract owner can access this endpoint'
});
}
next();
} catch (error) {
console.error('Ошибка проверки админа:', error);
return res.status(500).json({
error: 'Server error',
details: error.message
});
}
}
// Получение векторного хранилища для админа
router.get('/admin/vectors', requireAdmin, async (req, res) => {
try {
setCorsHeaders(res);
// Добавляем колонку created_at если её нет
await pool.query(`
ALTER TABLE documents
ADD COLUMN IF NOT EXISTS created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
`);
console.log('Проверена/добавлена колонка created_at');
// Проверяем структуру таблицы
const tableInfo = await pool.query(`
SELECT column_name, data_type
FROM information_schema.columns
WHERE table_name = 'documents'
`);
console.log('Структура таблицы documents:', tableInfo.rows);
// Получаем все документы из векторного хранилища
const documents = await pool.query(`
SELECT
d.id,
d.content,
d.metadata,
length(d.embedding::text) as embedding_size,
COALESCE(d.created_at, CURRENT_TIMESTAMP) as created_at,
CASE
WHEN d.metadata->>'type' = 'approved_chat' THEN true
ELSE false
END as is_approved
FROM documents d
ORDER BY d.created_at DESC NULLS LAST
`);
// Форматируем ответ
const vectors = documents.rows.map(doc => ({
id: doc.id,
content: doc.content,
metadata: doc.metadata,
embedding_size: doc.embedding ? 4096 : 0, // Фиксированный размер для mistral
created: doc.created_at,
is_approved: doc.is_approved
}));
console.log('Получено векторов:', vectors.length);
console.log('Пример вектора:', vectors[0]);
res.json({ vectors });
} catch (error) {
console.error('Ошибка получения векторов:', error);
console.error('Детали ошибки:', {
code: error.code,
detail: error.detail,
hint: error.hint,
position: error.position
});
res.status(500).json({
error: 'Server error',
details: error.message,
code: error.code
});
}
});
// Обработка CORS preflight запросов для админских роутов
router.options('/admin/*', (req, res) => {
setCorsHeaders(res);
res.sendStatus(200);
});
// Очистка кэша и данных
router.post('/admin/clear-cache', requireAdmin, async (req, res) => {
try {
setCorsHeaders(res);
// Очищаем таблицы
await pool.query('TRUNCATE TABLE documents CASCADE');
await pool.query('TRUNCATE TABLE chat_history CASCADE');
await pool.query('TRUNCATE TABLE users CASCADE');
// Сбрасываем автоинкремент
await pool.query('ALTER SEQUENCE documents_id_seq RESTART WITH 1');
await pool.query('ALTER SEQUENCE chat_history_id_seq RESTART WITH 1');
await pool.query('ALTER SEQUENCE users_id_seq RESTART WITH 1');
// Реинициализируем векторное хранилище
await initVectorStore();
console.log('Кэш и данные очищены');
res.json({ success: true });
} catch (error) {
console.error('Ошибка очистки кэша:', error);
res.status(500).json({
error: 'Server error',
details: error.message
});
}
});
// Выход из системы
router.post('/signout', requireAuth, async (req, res) => {
try {
setCorsHeaders(res);
// Уничтожаем сессию
req.session.destroy((err) => {
if (err) {
console.error('Ошибка при удалении сессии:', err);
return res.status(500).json({ error: 'Failed to destroy session' });
}
console.log('Сессия успешно завершена');
res.json({ success: true });
});
} catch (error) {
console.error('Ошибка выхода:', error);
res.status(500).json({ error: 'Server error' });
}
});
// Проверка сессии
router.get('/session', (req, res) => {
try {
setCorsHeaders(res);
if (req.session?.authenticated && req.session?.siwe?.address) {
res.json({
authenticated: true,
address: req.session.siwe.address
});
} else {
res.json({
authenticated: false
});
}
} catch (error) {
console.error('Ошибка проверки сессии:', error);
res.status(500).json({ error: 'Server error' });
}
});
// Создание нового пользователя
router.post('/users', async (req, res) => {
try {
setCorsHeaders(res);
const { address } = req.body;
// Проверяем существование пользователя
const existingUser = await pool.query(
'SELECT * FROM users WHERE address = $1',
[address.toLowerCase()]
);
if (existingUser.rows.length > 0) {
return res.json({ user: existingUser.rows[0] });
}
// Создаем нового пользователя
const result = await pool.query(
'INSERT INTO users (address) VALUES ($1) RETURNING *',
[address.toLowerCase()]
);
res.json({ user: result.rows[0] });
} catch (error) {
console.error('Ошибка создания пользователя:', error);
res.status(500).json({ error: 'Server error' });
}
});
// Создание необходимых таблиц при старте
async function initializeTables() {
try {
await pool.query(`
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
address VARCHAR(42) NOT NULL UNIQUE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS chat_history (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id),
message TEXT,
response TEXT,
is_user BOOLEAN DEFAULT true,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
`);
console.log('Таблицы успешно инициализированы');
// Инициализируем vectorStore
vectorStore = await PGVectorStore.initialize(
embeddings,
{
postgresConnectionOptions: {
connectionString: process.env.DATABASE_URL
},
tableName: 'documents',
columns: {
idColumnName: 'id',
vectorColumnName: 'embedding',
contentColumnName: 'content',
metadataColumnName: 'metadata'
}
}
);
console.log('Векторное хранилище инициализировано:', {
tableName: 'documents',
columns: vectorStore.columns,
config: {
tableName: vectorStore.tableName,
columns: vectorStore.columns,
client: vectorStore.client ? 'Connected' : 'Not Connected',
embeddings: vectorStore.embeddings ? 'Initialized' : 'Not Initialized'
}
});
// Создаем экземпляр TelegramBotService только после инициализации vectorStore
if (vectorStore) {
const telegramBot = new TelegramBotService(
process.env.TELEGRAM_BOT_TOKEN,
vectorStore
);
}
} catch (error) {
console.error('Ошибка при инициализации:', error);
}
}
// Вызываем инициализацию при старте
initializeTables();
module.exports = router;