feat: новая функция
This commit is contained in:
@@ -32,7 +32,7 @@ CREATE TABLE IF NOT EXISTS user_cell_values (
|
||||
id SERIAL PRIMARY KEY,
|
||||
row_id INTEGER NOT NULL REFERENCES user_rows(id) ON DELETE CASCADE,
|
||||
column_id INTEGER NOT NULL REFERENCES user_columns(id) ON DELETE CASCADE,
|
||||
value TEXT,
|
||||
value_encrypted TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(row_id, column_id)
|
||||
|
||||
@@ -26,7 +26,8 @@
|
||||
"generate:abi": "node scripts/generate-abi.js",
|
||||
"generate:flattened": "node scripts/generate-flattened.js",
|
||||
"compile:full": "npx hardhat compile && npm run generate:abi && npm run generate:flattened",
|
||||
"seed:legal": "node scripts/seed/legalTemplatesSeed.js"
|
||||
"seed:legal": "node scripts/seed/legalTemplatesSeed.js",
|
||||
"import:legal": "node scripts/import-legal-docs.js"
|
||||
},
|
||||
"bin": {},
|
||||
"engines": {
|
||||
|
||||
291
backend/scripts/import-legal-docs.js
Normal file
291
backend/scripts/import-legal-docs.js
Normal file
@@ -0,0 +1,291 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
/**
|
||||
* Импорт правовых документов из папки legal в admin_pages_simple
|
||||
* Конвертирует markdown файлы в HTML и добавляет их как опубликованные страницы
|
||||
*/
|
||||
|
||||
const db = require('../db');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Маппинг файлов на документацию
|
||||
const legalDocsMapping = {
|
||||
'CONSENT_PERSONAL_DATA_RU.md': {
|
||||
title: 'Согласие на обработку персональных данных',
|
||||
summary: 'Шаблон пользовательского согласия на обработку персональных данных'
|
||||
},
|
||||
'COOKIE_CONSENT_RU.md': {
|
||||
title: 'Согласие на использование файлов cookie',
|
||||
summary: 'Шаблон согласия на использование cookie по категориям'
|
||||
},
|
||||
'PDN_RIGHTS_AND_REVOCATION_RU.md': {
|
||||
title: 'Права субъектов персональных данных и отзыв согласия',
|
||||
summary: 'Информация о правах субъектов ПДн и форма отзыва согласия'
|
||||
},
|
||||
'PRIVACY_POLICY_RU.md': {
|
||||
title: 'Политика конфиденциальности',
|
||||
summary: 'Публичная политика конфиденциальности сервиса'
|
||||
},
|
||||
'SERVICE_ACT_TEMPLATE_RU.md': {
|
||||
title: 'Акт выполненных работ',
|
||||
summary: 'Шаблон акта выполненных работ для подтверждения оказанных услуг'
|
||||
},
|
||||
'SERVICE_AGREEMENT_RU.md': {
|
||||
title: 'Договор оказания услуг',
|
||||
summary: 'Минимальный договор оказания услуг / лицензионный договор'
|
||||
},
|
||||
'service-terms.md': {
|
||||
title: 'Условия приобретения и обслуживания Digital Legal Entity',
|
||||
summary: 'Условия приобретения, лицензирования и обслуживания DLE'
|
||||
}
|
||||
};
|
||||
|
||||
async function ensureTable(tableName) {
|
||||
const existsRes = await db.getQuery()(
|
||||
`SELECT to_regclass($1) as exists`,
|
||||
[tableName]
|
||||
);
|
||||
if (!existsRes.rows[0].exists) {
|
||||
await db.getQuery()(`
|
||||
CREATE TABLE ${tableName} (
|
||||
id SERIAL PRIMARY KEY,
|
||||
author_address TEXT NULL,
|
||||
title TEXT,
|
||||
summary TEXT,
|
||||
content TEXT,
|
||||
seo JSONB,
|
||||
status TEXT,
|
||||
visibility TEXT,
|
||||
required_permission TEXT,
|
||||
format TEXT,
|
||||
mime_type TEXT,
|
||||
storage_type TEXT,
|
||||
file_path TEXT,
|
||||
size_bytes BIGINT,
|
||||
checksum TEXT,
|
||||
is_system_template BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
)
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureColumns(tableName) {
|
||||
const needed = {
|
||||
author_address: 'TEXT',
|
||||
title: 'TEXT',
|
||||
summary: 'TEXT',
|
||||
content: 'TEXT',
|
||||
seo: 'JSONB',
|
||||
status: 'TEXT',
|
||||
visibility: 'TEXT',
|
||||
required_permission: 'TEXT',
|
||||
format: 'TEXT',
|
||||
mime_type: 'TEXT',
|
||||
storage_type: 'TEXT',
|
||||
file_path: 'TEXT',
|
||||
size_bytes: 'BIGINT',
|
||||
checksum: 'TEXT',
|
||||
is_system_template: 'BOOLEAN DEFAULT FALSE',
|
||||
created_at: 'TIMESTAMP DEFAULT NOW()',
|
||||
updated_at: 'TIMESTAMP DEFAULT NOW()'
|
||||
};
|
||||
|
||||
const existingRes = await db.getQuery()(
|
||||
`SELECT column_name FROM information_schema.columns WHERE table_name = $1`,
|
||||
[tableName]
|
||||
);
|
||||
const existing = existingRes.rows.map(r => r.column_name);
|
||||
|
||||
for (const [col, type] of Object.entries(needed)) {
|
||||
if (!existing.includes(col)) {
|
||||
await db.getQuery()(`ALTER TABLE ${tableName} ADD COLUMN ${col} ${type}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Простая функция конвертации markdown в HTML
|
||||
function markdownToHtml(markdown) {
|
||||
let html = markdown;
|
||||
|
||||
// Заголовки
|
||||
html = html.replace(/^### (.*$)/gim, '<h3>$1</h3>');
|
||||
html = html.replace(/^## (.*$)/gim, '<h2>$1</h2>');
|
||||
html = html.replace(/^# (.*$)/gim, '<h1>$1</h1>');
|
||||
|
||||
// Списки
|
||||
html = html.replace(/^\- (.*$)/gim, '<li>$1</li>');
|
||||
html = html.replace(/^(\d+)\. (.*$)/gim, '<li>$2</li>');
|
||||
html = html.replace(/(<li>.*<\/li>\n)+/g, '<ul>$&</ul>');
|
||||
|
||||
// Жирный текст
|
||||
html = html.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
|
||||
html = html.replace(/__(.*?)__/g, '<strong>$1</strong>');
|
||||
|
||||
// Курсив
|
||||
html = html.replace(/\*(.*?)\*/g, '<em>$1</em>');
|
||||
html = html.replace(/_(.*?)_/g, '<em>$1</em>');
|
||||
|
||||
// Ссылки
|
||||
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
|
||||
|
||||
// Изображения
|
||||
html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1">');
|
||||
|
||||
// Code blocks
|
||||
html = html.replace(/```([\s\S]*?)```/g, '<pre><code>$1</code></pre>');
|
||||
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
|
||||
|
||||
// Blockquotes
|
||||
html = html.replace(/^> (.*$)/gim, '<blockquote>$1</blockquote>');
|
||||
|
||||
// Горизонтальная линия
|
||||
html = html.replace(/^---$/gim, '<hr>');
|
||||
|
||||
// Параграфы
|
||||
html = html.split('\n\n').map(p => {
|
||||
p = p.trim();
|
||||
if (p && !p.match(/^<[hul]/) && !p.match(/<\/[hul]/) && !p.match(/^<pre>/) && !p.match(/<\/pre>/) && !p.match(/^<blockquote>/) && !p.match(/<\/blockquote>/)) {
|
||||
return `<p>${p}</p>`;
|
||||
}
|
||||
return p;
|
||||
}).join('\n\n');
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
async function importLegalDocument(filename, metadata) {
|
||||
// Пробуем разные пути в зависимости от окружения
|
||||
const possiblePaths = [
|
||||
path.join('/legal', filename), // Docker окружение
|
||||
path.join(__dirname, '..', '..', 'legal', filename), // Локальное окружение
|
||||
path.join(process.cwd(), '..', 'legal', filename) // Альтернативный путь
|
||||
];
|
||||
|
||||
let filePath = null;
|
||||
for (const possiblePath of possiblePaths) {
|
||||
if (fs.existsSync(possiblePath)) {
|
||||
filePath = possiblePath;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!filePath) {
|
||||
console.error(`Файл не найден: ${filename}`);
|
||||
console.error(`Проверенные пути: ${possiblePaths.join(', ')}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log(`Чтение файла: ${filename}`);
|
||||
const markdownContent = fs.readFileSync(filePath, 'utf-8');
|
||||
|
||||
// Конвертируем markdown в HTML
|
||||
const htmlContent = markdownToHtml(markdownContent);
|
||||
|
||||
// Проверяем, существует ли уже документ с таким названием
|
||||
const existing = await db.getQuery()(
|
||||
`SELECT id FROM admin_pages_simple WHERE title = $1 LIMIT 1`,
|
||||
[metadata.title]
|
||||
);
|
||||
|
||||
const pageData = {
|
||||
title: metadata.title,
|
||||
summary: metadata.summary,
|
||||
content: htmlContent,
|
||||
seo: {
|
||||
title: metadata.title,
|
||||
description: metadata.summary,
|
||||
keywords: 'ПДн, политика, согласие, правовые документы, DLE'
|
||||
},
|
||||
status: 'published',
|
||||
visibility: 'public',
|
||||
required_permission: null,
|
||||
format: 'html',
|
||||
mime_type: 'text/html',
|
||||
storage_type: 'embedded'
|
||||
};
|
||||
|
||||
if (existing.rows.length > 0) {
|
||||
// Обновляем существующий документ
|
||||
const sql = `UPDATE admin_pages_simple
|
||||
SET summary = $2, content = $3, seo = $4, status = $5, visibility = $6,
|
||||
format = $7, mime_type = $8, storage_type = $9, updated_at = NOW()
|
||||
WHERE id = $1`;
|
||||
await db.getQuery()(sql, [
|
||||
existing.rows[0].id,
|
||||
pageData.summary,
|
||||
pageData.content,
|
||||
JSON.stringify(pageData.seo),
|
||||
pageData.status,
|
||||
pageData.visibility,
|
||||
pageData.format,
|
||||
pageData.mime_type,
|
||||
pageData.storage_type
|
||||
]);
|
||||
console.log(`✓ Обновлен: ${metadata.title}`);
|
||||
return { updated: 1, inserted: 0 };
|
||||
} else {
|
||||
// Вставляем новый документ
|
||||
const sql = `INSERT INTO admin_pages_simple
|
||||
(author_address, title, summary, content, seo, status, visibility, required_permission, format, mime_type, storage_type)
|
||||
VALUES (NULL, $1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`;
|
||||
await db.getQuery()(sql, [
|
||||
pageData.title,
|
||||
pageData.summary,
|
||||
pageData.content,
|
||||
JSON.stringify(pageData.seo),
|
||||
pageData.status,
|
||||
pageData.visibility,
|
||||
pageData.required_permission,
|
||||
pageData.format,
|
||||
pageData.mime_type,
|
||||
pageData.storage_type
|
||||
]);
|
||||
console.log(`✓ Создан: ${metadata.title}`);
|
||||
return { updated: 0, inserted: 1 };
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
console.log('Начало импорта правовых документов...');
|
||||
|
||||
const tableName = 'admin_pages_simple';
|
||||
await ensureTable(tableName);
|
||||
await ensureColumns(tableName);
|
||||
|
||||
let totalInserted = 0;
|
||||
let totalUpdated = 0;
|
||||
|
||||
// Импортируем все документы из маппинга
|
||||
for (const [filename, metadata] of Object.entries(legalDocsMapping)) {
|
||||
const result = await importLegalDocument(filename, metadata);
|
||||
if (result) {
|
||||
totalInserted += result.inserted;
|
||||
totalUpdated += result.updated;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n✓ Импорт завершен: создано=${totalInserted}, обновлено=${totalUpdated}`);
|
||||
} catch (error) {
|
||||
console.error('Ошибка импорта правовых документов:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main().then(() => process.exit(0)).catch(err => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -137,7 +137,8 @@ class AIAssistant {
|
||||
systemPrompt: aiSettings ? aiSettings.system_prompt : '',
|
||||
history: conversationHistory,
|
||||
model: aiSettings ? aiSettings.model : undefined,
|
||||
rules: rules ? rules.rules : null
|
||||
rules: rules ? rules.rules : null,
|
||||
selectedRagTables: aiSettings ? aiSettings.selected_rag_tables : []
|
||||
});
|
||||
|
||||
if (!aiResponse) {
|
||||
|
||||
@@ -216,8 +216,18 @@ class EncryptedDataService {
|
||||
continue;
|
||||
}
|
||||
const currentParamIndex = paramIndex++;
|
||||
filteredData[key] = value; // Добавляем в отфильтрованные данные
|
||||
console.log(`✅ Добавили зашифрованное поле ${key} = "${value}" в filteredData`);
|
||||
|
||||
// Преобразуем значение в строку для шифрования
|
||||
let valueToEncrypt;
|
||||
if (typeof value === 'object') {
|
||||
// Если это объект/массив, преобразуем в JSON
|
||||
valueToEncrypt = JSON.stringify(value);
|
||||
} else {
|
||||
valueToEncrypt = value;
|
||||
}
|
||||
|
||||
filteredData[key] = valueToEncrypt; // Добавляем в отфильтрованные данные
|
||||
console.log(`✅ Добавили зашифрованное поле ${key} = "${valueToEncrypt}" в filteredData`);
|
||||
if (encryptedColumn.data_type === 'jsonb') {
|
||||
encryptedData[`${key}_encrypted`] = `encrypt_json($${currentParamIndex}, ${hasEncryptedFields ? '$1::text' : 'NULL'})`;
|
||||
} else {
|
||||
@@ -289,7 +299,27 @@ class EncryptedDataService {
|
||||
const placeholders = Object.keys(allData).map(key => allData[key]).join(', ');
|
||||
|
||||
const query = `INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (${placeholders}) RETURNING *`;
|
||||
const params = hasEncryptedFields ? [this.encryptionKey, ...Object.values(filteredData)] : [...Object.values(filteredData)];
|
||||
|
||||
// Собираем параметры в правильном порядке: сначала для encrypted, потом для unencrypted
|
||||
const paramsArray = [];
|
||||
if (hasEncryptedFields) paramsArray.push(this.encryptionKey);
|
||||
|
||||
// Добавляем параметры для encrypted колонок
|
||||
for (const key of Object.keys(encryptedData)) {
|
||||
const originalKey = key.replace('_encrypted', '');
|
||||
if (filteredData[originalKey] !== undefined) {
|
||||
paramsArray.push(filteredData[originalKey]);
|
||||
} else if (filteredData[originalKey + '_unencrypted'] !== undefined) {
|
||||
paramsArray.push(filteredData[originalKey + '_unencrypted']);
|
||||
}
|
||||
}
|
||||
|
||||
// Добавляем параметры для unencrypted колонок
|
||||
for (const key of Object.keys(unencryptedData)) {
|
||||
paramsArray.push(filteredData[key + '_unencrypted'] || filteredData[key]);
|
||||
}
|
||||
|
||||
const params = paramsArray;
|
||||
|
||||
console.log(`🔍 Выполняем INSERT запрос:`, query);
|
||||
console.log(`🔍 Параметры:`, params);
|
||||
@@ -359,9 +389,13 @@ class EncryptedDataService {
|
||||
params.push(...paramsToAdd);
|
||||
}
|
||||
|
||||
// Добавляем ключ шифрования в начало, если есть зашифрованные поля
|
||||
const hasEncryptedFields = columns.some(col => col.column_name.endsWith('_encrypted'));
|
||||
if (hasEncryptedFields) {
|
||||
// Определяем, нужно ли добавлять ключ шифрования
|
||||
// Проверяем, есть ли в WHERE условиях зашифрованные колонки
|
||||
const hasEncryptedFieldsInConditions = Object.keys(conditions).some(key => {
|
||||
return columns.find(col => col.column_name === `${key}_encrypted`);
|
||||
});
|
||||
|
||||
if (hasEncryptedFieldsInConditions) {
|
||||
params.unshift(this.encryptionKey);
|
||||
}
|
||||
|
||||
|
||||
@@ -201,13 +201,20 @@ async function ragAnswer({ tableId, userQuestion, product = null, threshold = 30
|
||||
/**
|
||||
* Загрузка всех плейсхолдеров и их значений из пользовательских таблиц
|
||||
* Возвращает объект: { placeholder1: value1, placeholder2: value2, ... }
|
||||
* @param {Array} selectedRagTables - Массив ID выбранных RAG таблиц для фильтрации
|
||||
*/
|
||||
async function getAllPlaceholdersWithValues() {
|
||||
async function getAllPlaceholdersWithValues(selectedRagTables = []) {
|
||||
try {
|
||||
console.log('[RAG] Начинаем загрузку плейсхолдеров...');
|
||||
|
||||
// Получаем все колонки с плейсхолдерами
|
||||
const columns = await encryptedDb.getData('user_columns', {});
|
||||
// Получаем колонки с плейсхолдерами
|
||||
let columns = await encryptedDb.getData('user_columns', {});
|
||||
|
||||
// Фильтруем по выбранным RAG таблицам, если они указаны
|
||||
if (selectedRagTables && selectedRagTables.length > 0) {
|
||||
columns = columns.filter(col => selectedRagTables.includes(col.table_id));
|
||||
console.log(`[RAG] Фильтруем по RAG таблицам: ${selectedRagTables.join(', ')}`);
|
||||
}
|
||||
console.log(`[RAG] Получено колонок: ${columns.length}`);
|
||||
|
||||
const columnsWithPlaceholders = columns.filter(col => col.placeholder && col.placeholder.trim() !== '');
|
||||
@@ -281,7 +288,8 @@ async function generateLLMResponse({
|
||||
date,
|
||||
rules,
|
||||
history,
|
||||
model
|
||||
model,
|
||||
selectedRagTables
|
||||
}) {
|
||||
console.log(`[RAG] generateLLMResponse called with:`, {
|
||||
userQuestion,
|
||||
@@ -338,7 +346,7 @@ async function generateLLMResponse({
|
||||
// --- ДОБАВЛЕНО: подстановка плейсхолдеров ---
|
||||
let finalSystemPrompt = systemPrompt;
|
||||
if (systemPrompt && systemPrompt.includes('{')) {
|
||||
const placeholders = await getAllPlaceholdersWithValues();
|
||||
const placeholders = await getAllPlaceholdersWithValues(selectedRagTables);
|
||||
finalSystemPrompt = replacePlaceholders(systemPrompt, placeholders);
|
||||
console.log(`[RAG] Подставлены плейсхолдеры в системный промпт`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user