ваше сообщение коммита
This commit is contained in:
@@ -76,9 +76,16 @@ async function reinitPoolFromDbSettings() {
|
||||
}
|
||||
}
|
||||
|
||||
// При старте приложения — сразу пробуем инициализировать из db_settings
|
||||
if (process.env.NODE_ENV !== 'migration') {
|
||||
reinitPoolFromDbSettings();
|
||||
// При старте приложения — убираем автоматический вызов reinitPoolFromDbSettings
|
||||
// if (process.env.NODE_ENV !== 'migration') {
|
||||
// reinitPoolFromDbSettings();
|
||||
// }
|
||||
|
||||
// Экспортируем функцию для явной инициализации пула
|
||||
async function initDbPool() {
|
||||
if (process.env.NODE_ENV !== 'migration') {
|
||||
await reinitPoolFromDbSettings();
|
||||
}
|
||||
}
|
||||
|
||||
// Функция для сохранения гостевого сообщения в базе данных
|
||||
@@ -99,4 +106,4 @@ async function saveGuestMessageToDatabase(message, language, guestId) {
|
||||
}
|
||||
|
||||
// Экспортируем функции для работы с базой данных
|
||||
module.exports = { query, getQuery, pool, getPool, setPoolChangeCallback };
|
||||
module.exports = { query, getQuery, pool, getPool, setPoolChangeCallback, initDbPool };
|
||||
|
||||
@@ -4,6 +4,7 @@ const express = require('express');
|
||||
const router = express.Router();
|
||||
const db = require('../db');
|
||||
const { requireAuth } = require('../middleware/auth');
|
||||
const vectorSearchClient = require('../services/vectorSearchClient');
|
||||
|
||||
router.use((req, res, next) => {
|
||||
console.log('Tables router received:', req.method, req.originalUrl);
|
||||
@@ -79,6 +80,13 @@ router.post('/:id/rows', async (req, res, next) => {
|
||||
'INSERT INTO user_rows (table_id) VALUES ($1) RETURNING *',
|
||||
[tableId]
|
||||
);
|
||||
// Получаем все строки и значения для upsert
|
||||
const rows = (await db.getQuery()('SELECT r.id as row_id, c.value as text, c2.value as answer FROM user_rows r LEFT JOIN user_cell_values c ON c.row_id = r.id AND c.column_id = 1 LEFT JOIN user_cell_values c2 ON c2.row_id = r.id AND c2.column_id = 2 WHERE r.table_id = $1', [tableId])).rows;
|
||||
const upsertRows = rows.filter(r => r.row_id && r.text).map(r => ({ row_id: r.row_id, text: r.text, metadata: { answer: r.answer } }));
|
||||
console.log('[DEBUG][upsertRows]', upsertRows);
|
||||
if (upsertRows.length > 0) {
|
||||
await vectorSearchClient.upsert(tableId, upsertRows);
|
||||
}
|
||||
res.json(result.rows[0]);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
@@ -94,6 +102,24 @@ router.patch('/cell/:cellId', async (req, res, next) => {
|
||||
'UPDATE user_cell_values SET value = $1, updated_at = NOW() WHERE id = $2 RETURNING *',
|
||||
[value, cellId]
|
||||
);
|
||||
// Получаем row_id и table_id
|
||||
const row = (await db.getQuery()('SELECT row_id FROM user_cell_values WHERE id = $1', [cellId])).rows[0];
|
||||
if (row) {
|
||||
const rowId = row.row_id;
|
||||
const table = (await db.getQuery()('SELECT table_id FROM user_rows WHERE id = $1', [rowId])).rows[0];
|
||||
if (table) {
|
||||
const tableId = table.table_id;
|
||||
// Получаем всю строку для upsert
|
||||
const rowData = (await db.getQuery()('SELECT r.id as row_id, c.value as text, c2.value as answer FROM user_rows r LEFT JOIN user_cell_values c ON c.row_id = r.id AND c.column_id = 1 LEFT JOIN user_cell_values c2 ON c2.row_id = r.id AND c2.column_id = 2 WHERE r.id = $1', [rowId])).rows[0];
|
||||
if (rowData) {
|
||||
const upsertRows = [{ row_id: rowData.row_id, text: rowData.text, metadata: { answer: rowData.answer } }].filter(r => r.row_id && r.text);
|
||||
console.log('[DEBUG][upsertRows]', upsertRows);
|
||||
if (upsertRows.length > 0) {
|
||||
await vectorSearchClient.upsert(tableId, upsertRows);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
res.json(result.rows[0]);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
@@ -110,6 +136,20 @@ router.post('/cell', async (req, res, next) => {
|
||||
RETURNING *`,
|
||||
[row_id, column_id, value]
|
||||
);
|
||||
// Получаем table_id
|
||||
const table = (await db.getQuery()('SELECT table_id FROM user_rows WHERE id = $1', [row_id])).rows[0];
|
||||
if (table) {
|
||||
const tableId = table.table_id;
|
||||
// Получаем всю строку для upsert
|
||||
const rowData = (await db.getQuery()('SELECT r.id as row_id, c.value as text, c2.value as answer FROM user_rows r LEFT JOIN user_cell_values c ON c.row_id = r.id AND c.column_id = 1 LEFT JOIN user_cell_values c2 ON c2.row_id = r.id AND c2.column_id = 2 WHERE r.id = $1', [row_id])).rows[0];
|
||||
if (rowData) {
|
||||
const upsertRows = [{ row_id: rowData.row_id, text: rowData.text, metadata: { answer: rowData.answer } }].filter(r => r.row_id && r.text);
|
||||
console.log('[DEBUG][upsertRows]', upsertRows);
|
||||
if (upsertRows.length > 0) {
|
||||
await vectorSearchClient.upsert(tableId, upsertRows);
|
||||
}
|
||||
}
|
||||
}
|
||||
res.json(result.rows[0]);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
@@ -120,7 +160,19 @@ router.post('/cell', async (req, res, next) => {
|
||||
router.delete('/row/:rowId', async (req, res, next) => {
|
||||
try {
|
||||
const rowId = req.params.rowId;
|
||||
// Получаем table_id
|
||||
const table = (await db.getQuery()('SELECT table_id FROM user_rows WHERE id = $1', [rowId])).rows[0];
|
||||
await db.getQuery()('DELETE FROM user_rows WHERE id = $1', [rowId]);
|
||||
if (table) {
|
||||
const tableId = table.table_id;
|
||||
// Получаем все строки для rebuild
|
||||
const rows = (await db.getQuery()('SELECT r.id as row_id, c.value as text, c2.value as answer FROM user_rows r LEFT JOIN user_cell_values c ON c.row_id = r.id AND c.column_id = 1 LEFT JOIN user_cell_values c2 ON c2.row_id = r.id AND c2.column_id = 2 WHERE r.table_id = $1', [tableId])).rows;
|
||||
const rebuildRows = rows.filter(r => r.row_id && r.text).map(r => ({ row_id: r.row_id, text: r.text, metadata: { answer: r.answer } }));
|
||||
console.log('[DEBUG][rebuildRows]', rebuildRows);
|
||||
if (rebuildRows.length > 0) {
|
||||
await vectorSearchClient.rebuild(tableId, rebuildRows);
|
||||
}
|
||||
}
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
@@ -206,6 +258,51 @@ router.patch('/:id', async (req, res, next) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Получить id колонок с purpose 'question' и 'answer'
|
||||
async function getQuestionAnswerColumnIds(tableId) {
|
||||
const { rows } = await db.getQuery()(
|
||||
`SELECT id, options FROM user_columns WHERE table_id = $1`, [tableId]
|
||||
);
|
||||
let questionCol = null, answerCol = null;
|
||||
for (const col of rows) {
|
||||
if (col.options && col.options.purpose === 'question') questionCol = col.id;
|
||||
if (col.options && col.options.purpose === 'answer') answerCol = col.id;
|
||||
}
|
||||
return { questionCol, answerCol };
|
||||
}
|
||||
|
||||
// Пересобрать векторный индекс для таблицы (только для админа)
|
||||
router.post('/:id/rebuild-index', requireAuth, async (req, res, next) => {
|
||||
try {
|
||||
if (!req.session.isAdmin) {
|
||||
return res.status(403).json({ error: 'Доступ только для администратора' });
|
||||
}
|
||||
const tableId = req.params.id;
|
||||
const { questionCol, answerCol } = await getQuestionAnswerColumnIds(tableId);
|
||||
if (!questionCol || !answerCol) {
|
||||
return res.status(400).json({ error: 'Не найдены колонки с вопросами и ответами' });
|
||||
}
|
||||
const rows = (await db.getQuery()(
|
||||
`SELECT r.id as row_id, c.value as text, c2.value as answer
|
||||
FROM user_rows r
|
||||
LEFT JOIN user_cell_values c ON c.row_id = r.id AND c.column_id = $2
|
||||
LEFT JOIN user_cell_values c2 ON c2.row_id = r.id AND c2.column_id = $3
|
||||
WHERE r.table_id = $1`,
|
||||
[tableId, questionCol, answerCol]
|
||||
)).rows;
|
||||
const rebuildRows = rows.filter(r => r.row_id && r.text).map(r => ({ row_id: r.row_id, text: r.text, metadata: { answer: r.answer } }));
|
||||
console.log('[DEBUG][rebuildRows]', rebuildRows);
|
||||
if (rebuildRows.length > 0) {
|
||||
await vectorSearchClient.rebuild(tableId, rebuildRows);
|
||||
res.json({ success: true, count: rebuildRows.length });
|
||||
} else {
|
||||
res.status(400).json({ error: 'Нет валидных строк для индексации' });
|
||||
}
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE: удалить таблицу и каскадно все связанные строки/столбцы/ячейки (доступно всем)
|
||||
router.delete('/:id', requireAuth, async (req, res, next) => {
|
||||
const dbModule = require('../db');
|
||||
|
||||
@@ -5,6 +5,7 @@ const { initWSS } = require('./wsHub');
|
||||
const logger = require('./utils/logger');
|
||||
const { getBot } = require('./services/telegramBot');
|
||||
const EmailBotService = require('./services/emailBot');
|
||||
const { initDbPool } = require('./db');
|
||||
|
||||
const PORT = process.env.PORT || 8000;
|
||||
|
||||
@@ -44,10 +45,15 @@ async function initServices() {
|
||||
const server = http.createServer(app);
|
||||
initWSS(server);
|
||||
|
||||
async function startServer() {
|
||||
await initDbPool(); // Дождаться пересоздания пула!
|
||||
await initServices(); // Только теперь запускать сервисы
|
||||
console.log(`Server is running on port ${PORT}`);
|
||||
}
|
||||
|
||||
server.listen(PORT, async () => {
|
||||
try {
|
||||
await initServices();
|
||||
console.log(`Server is running on port ${PORT}`);
|
||||
await startServer();
|
||||
} catch (error) {
|
||||
console.error('Error starting server:', error);
|
||||
process.exit(1);
|
||||
|
||||
@@ -8,6 +8,8 @@ const identityService = require('./identity-service');
|
||||
const aiAssistant = require('./ai-assistant');
|
||||
const { checkAdminRole } = require('./admin-role');
|
||||
const { broadcastContactsUpdate } = require('../wsHub');
|
||||
const aiAssistantSettingsService = require('./aiAssistantSettingsService');
|
||||
const { ragAnswer, generateLLMResponse } = require('./ragService');
|
||||
|
||||
let botInstance = null;
|
||||
let telegramSettingsCache = null;
|
||||
@@ -335,8 +337,34 @@ async function getBot() {
|
||||
]
|
||||
);
|
||||
|
||||
// 3. Получить ответ от ИИ
|
||||
const aiResponse = await aiAssistant.getResponse(content, 'auto');
|
||||
// 3. Получить ответ от ИИ (RAG + LLM)
|
||||
const aiSettings = await aiAssistantSettingsService.getSettings();
|
||||
let ragTableId = null;
|
||||
if (aiSettings && aiSettings.selected_rag_tables) {
|
||||
ragTableId = Array.isArray(aiSettings.selected_rag_tables)
|
||||
? aiSettings.selected_rag_tables[0]
|
||||
: aiSettings.selected_rag_tables;
|
||||
}
|
||||
let aiResponse;
|
||||
if (ragTableId) {
|
||||
// Сначала ищем ответ через RAG
|
||||
const ragResult = await ragAnswer({ tableId: ragTableId, userQuestion: content });
|
||||
if (ragResult && ragResult.answer) {
|
||||
aiResponse = ragResult.answer;
|
||||
} else {
|
||||
aiResponse = await generateLLMResponse({
|
||||
userQuestion: content,
|
||||
context: ragResult && ragResult.context ? ragResult.context : '',
|
||||
answer: ragResult && ragResult.answer ? ragResult.answer : '',
|
||||
systemPrompt: aiSettings ? aiSettings.system_prompt : '',
|
||||
history: null,
|
||||
model: aiSettings ? aiSettings.model : undefined,
|
||||
language: aiSettings && aiSettings.languages && aiSettings.languages.length > 0 ? aiSettings.languages[0] : 'ru'
|
||||
});
|
||||
}
|
||||
} else {
|
||||
aiResponse = await aiAssistant.getResponse(content, 'auto');
|
||||
}
|
||||
// 4. Сохранить ответ в БД с conversation_id
|
||||
await db.getQuery()(
|
||||
`INSERT INTO messages (user_id, conversation_id, sender_type, content, channel, role, direction, created_at)
|
||||
|
||||
@@ -1,57 +1,89 @@
|
||||
const axios = require('axios');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
const VECTOR_SEARCH_URL = process.env.VECTOR_SEARCH_URL || 'http://vector-search:8001';
|
||||
|
||||
async function upsert(tableId, rows) {
|
||||
const res = await axios.post(`${VECTOR_SEARCH_URL}/upsert`, {
|
||||
table_id: String(tableId),
|
||||
rows: rows.map(r => ({
|
||||
row_id: String(r.row_id),
|
||||
text: r.text,
|
||||
metadata: r.metadata || {}
|
||||
}))
|
||||
});
|
||||
return res.data;
|
||||
logger.info(`[VectorSearch] upsert: tableId=${tableId}, rows=${rows.length}`);
|
||||
try {
|
||||
const res = await axios.post(`${VECTOR_SEARCH_URL}/upsert`, {
|
||||
table_id: String(tableId),
|
||||
rows: rows.map(r => ({
|
||||
row_id: String(r.row_id),
|
||||
text: r.text,
|
||||
metadata: r.metadata || {}
|
||||
}))
|
||||
});
|
||||
logger.info(`[VectorSearch] upsert result:`, res.data);
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
logger.error(`[VectorSearch] upsert error:`, error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function search(tableId, query, topK = 3) {
|
||||
const res = await axios.post(`${VECTOR_SEARCH_URL}/search`, {
|
||||
table_id: String(tableId),
|
||||
query,
|
||||
top_k: topK
|
||||
});
|
||||
return res.data.results;
|
||||
logger.info(`[VectorSearch] search: tableId=${tableId}, query="${query}", topK=${topK}`);
|
||||
try {
|
||||
const res = await axios.post(`${VECTOR_SEARCH_URL}/search`, {
|
||||
table_id: String(tableId),
|
||||
query,
|
||||
top_k: topK
|
||||
});
|
||||
logger.info(`[VectorSearch] search result:`, res.data.results);
|
||||
return res.data.results;
|
||||
} catch (error) {
|
||||
logger.error(`[VectorSearch] search error:`, error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function remove(tableId, rowIds) {
|
||||
const res = await axios.post(`${VECTOR_SEARCH_URL}/delete`, {
|
||||
table_id: String(tableId),
|
||||
row_ids: rowIds.map(id => String(id))
|
||||
});
|
||||
return res.data;
|
||||
logger.info(`[VectorSearch] remove: tableId=${tableId}, rowIds=${rowIds}`);
|
||||
try {
|
||||
const res = await axios.post(`${VECTOR_SEARCH_URL}/remove`, {
|
||||
table_id: String(tableId),
|
||||
row_ids: rowIds.map(String)
|
||||
});
|
||||
logger.info(`[VectorSearch] remove result:`, res.data);
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
logger.error(`[VectorSearch] remove error:`, error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function rebuild(tableId, rows) {
|
||||
const res = await axios.post(`${VECTOR_SEARCH_URL}/rebuild`, {
|
||||
table_id: String(tableId),
|
||||
rows: rows.map(r => ({
|
||||
row_id: String(r.row_id),
|
||||
text: r.text,
|
||||
metadata: r.metadata || {}
|
||||
}))
|
||||
});
|
||||
return res.data;
|
||||
logger.info(`[VectorSearch] rebuild: tableId=${tableId}, rows=${rows.length}`);
|
||||
try {
|
||||
const res = await axios.post(`${VECTOR_SEARCH_URL}/rebuild`, {
|
||||
table_id: String(tableId),
|
||||
rows: rows.map(r => ({
|
||||
row_id: String(r.row_id),
|
||||
text: r.text,
|
||||
metadata: r.metadata || {}
|
||||
}))
|
||||
});
|
||||
logger.info(`[VectorSearch] rebuild result:`, res.data);
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
logger.error(`[VectorSearch] rebuild error:`, error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function health() {
|
||||
logger.info(`[VectorSearch] health check`);
|
||||
try {
|
||||
const res = await axios.get(`${VECTOR_SEARCH_URL}/health`, { timeout: 5000 });
|
||||
logger.info(`[VectorSearch] health result:`, res.data);
|
||||
return {
|
||||
status: 'ok',
|
||||
url: VECTOR_SEARCH_URL,
|
||||
response: res.data
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(`[VectorSearch] health error:`, error.message);
|
||||
return {
|
||||
status: 'error',
|
||||
url: VECTOR_SEARCH_URL,
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
<div class="user-table-header" v-if="tableMeta">
|
||||
<h2>{{ tableMeta.name }}</h2>
|
||||
<div class="table-desc">{{ tableMeta.description }}</div>
|
||||
<button v-if="isAdmin" class="rebuild-btn" @click="rebuildIndex" :disabled="rebuilding">
|
||||
{{ rebuilding ? 'Пересборка...' : 'Пересобрать индекс' }}
|
||||
</button>
|
||||
<span v-if="rebuildStatus" :class="['rebuild-status', rebuildStatus.success ? 'success' : 'error']">
|
||||
{{ rebuildStatus.message }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="notion-table-wrapper">
|
||||
<table class="notion-table">
|
||||
@@ -97,6 +103,11 @@
|
||||
import { ref, onMounted } from 'vue';
|
||||
import tablesService from '../../services/tablesService';
|
||||
import TableCell from './TableCell.vue';
|
||||
import { useAuthContext } from '@/composables/useAuth';
|
||||
import axios from 'axios';
|
||||
const { isAdmin } = useAuthContext();
|
||||
const rebuilding = ref(false);
|
||||
const rebuildStatus = ref(null);
|
||||
|
||||
const props = defineProps({ tableId: Number });
|
||||
const columns = ref([]);
|
||||
@@ -252,6 +263,22 @@ function deleteColumn(col) {
|
||||
tablesService.deleteColumn(col.id).then(fetchTable);
|
||||
}
|
||||
}
|
||||
|
||||
async function rebuildIndex() {
|
||||
rebuilding.value = true;
|
||||
rebuildStatus.value = null;
|
||||
try {
|
||||
const { data } = await axios.post(`/tables/${props.tableId}/rebuild-index`);
|
||||
if (data.success) {
|
||||
rebuildStatus.value = { success: true, message: `Индекс пересобран (${data.count || 0} строк)` };
|
||||
} else {
|
||||
rebuildStatus.value = { success: false, message: data.message || 'Ошибка пересборки' };
|
||||
}
|
||||
} catch (e) {
|
||||
rebuildStatus.value = { success: false, message: e.response?.data?.error || e.message || 'Ошибка запроса' };
|
||||
}
|
||||
rebuilding.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -448,4 +475,30 @@ tr:hover .delete-row-btn {
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
z-index: 1999;
|
||||
}
|
||||
.rebuild-btn {
|
||||
margin-top: 1em;
|
||||
background: #2ecc40;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem 1.5rem;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.rebuild-btn:disabled {
|
||||
background: #b2e6c2;
|
||||
color: #fff;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.rebuild-status {
|
||||
margin-left: 1em;
|
||||
font-weight: 500;
|
||||
}
|
||||
.rebuild-status.success {
|
||||
color: #2ecc40;
|
||||
}
|
||||
.rebuild-status.error {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user