feat: новая функция
This commit is contained in:
@@ -32,7 +32,7 @@ CREATE TABLE IF NOT EXISTS user_cell_values (
|
|||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
row_id INTEGER NOT NULL REFERENCES user_rows(id) ON DELETE CASCADE,
|
row_id INTEGER NOT NULL REFERENCES user_rows(id) ON DELETE CASCADE,
|
||||||
column_id INTEGER NOT NULL REFERENCES user_columns(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,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
UNIQUE(row_id, column_id)
|
UNIQUE(row_id, column_id)
|
||||||
|
|||||||
@@ -26,7 +26,8 @@
|
|||||||
"generate:abi": "node scripts/generate-abi.js",
|
"generate:abi": "node scripts/generate-abi.js",
|
||||||
"generate:flattened": "node scripts/generate-flattened.js",
|
"generate:flattened": "node scripts/generate-flattened.js",
|
||||||
"compile:full": "npx hardhat compile && npm run generate:abi && npm run generate:flattened",
|
"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": {},
|
"bin": {},
|
||||||
"engines": {
|
"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 : '',
|
systemPrompt: aiSettings ? aiSettings.system_prompt : '',
|
||||||
history: conversationHistory,
|
history: conversationHistory,
|
||||||
model: aiSettings ? aiSettings.model : undefined,
|
model: aiSettings ? aiSettings.model : undefined,
|
||||||
rules: rules ? rules.rules : null
|
rules: rules ? rules.rules : null,
|
||||||
|
selectedRagTables: aiSettings ? aiSettings.selected_rag_tables : []
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!aiResponse) {
|
if (!aiResponse) {
|
||||||
|
|||||||
@@ -216,8 +216,18 @@ class EncryptedDataService {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const currentParamIndex = paramIndex++;
|
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') {
|
if (encryptedColumn.data_type === 'jsonb') {
|
||||||
encryptedData[`${key}_encrypted`] = `encrypt_json($${currentParamIndex}, ${hasEncryptedFields ? '$1::text' : 'NULL'})`;
|
encryptedData[`${key}_encrypted`] = `encrypt_json($${currentParamIndex}, ${hasEncryptedFields ? '$1::text' : 'NULL'})`;
|
||||||
} else {
|
} else {
|
||||||
@@ -289,7 +299,27 @@ class EncryptedDataService {
|
|||||||
const placeholders = Object.keys(allData).map(key => allData[key]).join(', ');
|
const placeholders = Object.keys(allData).map(key => allData[key]).join(', ');
|
||||||
|
|
||||||
const query = `INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (${placeholders}) RETURNING *`;
|
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(`🔍 Выполняем INSERT запрос:`, query);
|
||||||
console.log(`🔍 Параметры:`, params);
|
console.log(`🔍 Параметры:`, params);
|
||||||
@@ -359,9 +389,13 @@ class EncryptedDataService {
|
|||||||
params.push(...paramsToAdd);
|
params.push(...paramsToAdd);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Добавляем ключ шифрования в начало, если есть зашифрованные поля
|
// Определяем, нужно ли добавлять ключ шифрования
|
||||||
const hasEncryptedFields = columns.some(col => col.column_name.endsWith('_encrypted'));
|
// Проверяем, есть ли в WHERE условиях зашифрованные колонки
|
||||||
if (hasEncryptedFields) {
|
const hasEncryptedFieldsInConditions = Object.keys(conditions).some(key => {
|
||||||
|
return columns.find(col => col.column_name === `${key}_encrypted`);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasEncryptedFieldsInConditions) {
|
||||||
params.unshift(this.encryptionKey);
|
params.unshift(this.encryptionKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -201,13 +201,20 @@ async function ragAnswer({ tableId, userQuestion, product = null, threshold = 30
|
|||||||
/**
|
/**
|
||||||
* Загрузка всех плейсхолдеров и их значений из пользовательских таблиц
|
* Загрузка всех плейсхолдеров и их значений из пользовательских таблиц
|
||||||
* Возвращает объект: { placeholder1: value1, placeholder2: value2, ... }
|
* Возвращает объект: { placeholder1: value1, placeholder2: value2, ... }
|
||||||
|
* @param {Array} selectedRagTables - Массив ID выбранных RAG таблиц для фильтрации
|
||||||
*/
|
*/
|
||||||
async function getAllPlaceholdersWithValues() {
|
async function getAllPlaceholdersWithValues(selectedRagTables = []) {
|
||||||
try {
|
try {
|
||||||
console.log('[RAG] Начинаем загрузку плейсхолдеров...');
|
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}`);
|
console.log(`[RAG] Получено колонок: ${columns.length}`);
|
||||||
|
|
||||||
const columnsWithPlaceholders = columns.filter(col => col.placeholder && col.placeholder.trim() !== '');
|
const columnsWithPlaceholders = columns.filter(col => col.placeholder && col.placeholder.trim() !== '');
|
||||||
@@ -281,7 +288,8 @@ async function generateLLMResponse({
|
|||||||
date,
|
date,
|
||||||
rules,
|
rules,
|
||||||
history,
|
history,
|
||||||
model
|
model,
|
||||||
|
selectedRagTables
|
||||||
}) {
|
}) {
|
||||||
console.log(`[RAG] generateLLMResponse called with:`, {
|
console.log(`[RAG] generateLLMResponse called with:`, {
|
||||||
userQuestion,
|
userQuestion,
|
||||||
@@ -338,7 +346,7 @@ async function generateLLMResponse({
|
|||||||
// --- ДОБАВЛЕНО: подстановка плейсхолдеров ---
|
// --- ДОБАВЛЕНО: подстановка плейсхолдеров ---
|
||||||
let finalSystemPrompt = systemPrompt;
|
let finalSystemPrompt = systemPrompt;
|
||||||
if (systemPrompt && systemPrompt.includes('{')) {
|
if (systemPrompt && systemPrompt.includes('{')) {
|
||||||
const placeholders = await getAllPlaceholdersWithValues();
|
const placeholders = await getAllPlaceholdersWithValues(selectedRagTables);
|
||||||
finalSystemPrompt = replacePlaceholders(systemPrompt, placeholders);
|
finalSystemPrompt = replacePlaceholders(systemPrompt, placeholders);
|
||||||
console.log(`[RAG] Подставлены плейсхолдеры в системный промпт`);
|
console.log(`[RAG] Подставлены плейсхолдеры в системный промпт`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -242,11 +242,12 @@ ollama serve
|
|||||||
- Используйте эмодзи умеренно (1-2 на сообщение)
|
- Используйте эмодзи умеренно (1-2 на сообщение)
|
||||||
|
|
||||||
Правила ответа:
|
Правила ответа:
|
||||||
1. Сначала ищите ответ в базе знаний (RAG)
|
1. ОБЯЗАТЕЛЬНО: Отвечайте ТОЛЬКО на русском языке. Все вопросы и ответы должны быть на русском языке
|
||||||
2. Если нашли — отвечайте на основе найденной информации
|
2. Сначала ищите ответ в базе знаний (RAG)
|
||||||
3. Если не нашли — честно скажите и предложите помощь оператора
|
3. Если нашли — отвечайте на основе найденной информации
|
||||||
4. Не придумывайте информацию о ценах, сроках, условиях
|
4. Если не нашли — честно скажите и предложите помощь оператора
|
||||||
5. При сложных вопросах предлагайте связаться с менеджером
|
5. Не придумывайте информацию о ценах, сроках, условиях
|
||||||
|
6. При сложных вопросах предлагайте связаться с менеджером
|
||||||
|
|
||||||
Всегда заканчивайте: "Чем еще могу помочь? 😊"
|
Всегда заканчивайте: "Чем еще могу помочь? 😊"
|
||||||
```
|
```
|
||||||
@@ -258,6 +259,16 @@ ollama serve
|
|||||||
|
|
||||||
> 💡 **Подсказка**: Модели автоматически подтянутся из настроек Ollama
|
> 💡 **Подсказка**: Модели автоматически подтянутся из настроек Ollama
|
||||||
|
|
||||||
|
> 📊 **Размер контекстного окна**:
|
||||||
|
> - **Qwen2.5:7b**: Базовый контекст = **32,768 токенов** (~24,000 русских слов)
|
||||||
|
> - Всего данных, отправляемых в модель:
|
||||||
|
> - Системный промпт: ~500-2000 символов (~300-1200 токенов)
|
||||||
|
> - История диалога: до 20 сообщений (~100-500 токенов на сообщение = ~2000-10000 токенов)
|
||||||
|
> - RAG контекст: ~500-2000 токенов (из найденных данных)
|
||||||
|
> - Текущий вопрос: ~50-200 токенов
|
||||||
|
> - **Итого**: примерно 3,000-15,000 токенов (запас достаточен)
|
||||||
|
> - Если нужен больший контекст → используйте Qwen3 с YaRN (до 131K токенов)
|
||||||
|
|
||||||
### 4.4 Выбор RAG-таблицы
|
### 4.4 Выбор RAG-таблицы
|
||||||
|
|
||||||
1. В поле **"Выбранные RAG-таблицы"** выберите созданную таблицу:
|
1. В поле **"Выбранные RAG-таблицы"** выберите созданную таблицу:
|
||||||
|
|||||||
@@ -156,7 +156,17 @@
|
|||||||
</div>
|
</div>
|
||||||
<label>Плейсхолдер</label>
|
<label>Плейсхолдер</label>
|
||||||
<input v-model="newColPlaceholder" class="notion-input" placeholder="Плейсхолдер (авто)" />
|
<input v-model="newColPlaceholder" class="notion-input" placeholder="Плейсхолдер (авто)" />
|
||||||
<!-- Удаляю блок назначения столбца -->
|
<label>Назначение (для RAG)</label>
|
||||||
|
<select v-model="newColPurpose" class="notion-input">
|
||||||
|
<option value="">Без назначения</option>
|
||||||
|
<option value="question">Вопрос для AI</option>
|
||||||
|
<option value="answer">Ответ AI</option>
|
||||||
|
<option value="product">Продукт</option>
|
||||||
|
<option value="userTags">Теги пользователя</option>
|
||||||
|
<option value="context">Контекст</option>
|
||||||
|
<option value="priority">Приоритет</option>
|
||||||
|
<option value="date">Дата</option>
|
||||||
|
</select>
|
||||||
<div class="modal-actions">
|
<div class="modal-actions">
|
||||||
<button class="save-btn" @click="handleAddColumn">Добавить</button>
|
<button class="save-btn" @click="handleAddColumn">Добавить</button>
|
||||||
<button class="cancel-btn" @click="closeAddColModal">Отмена</button>
|
<button class="cancel-btn" @click="closeAddColModal">Отмена</button>
|
||||||
@@ -184,7 +194,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
window.addEventListener('refresh-application-data', () => {
|
window.addEventListener('refresh-application-data', () => {
|
||||||
console.log('[UserTableView] Refreshing table data');
|
console.log('[UserTableView] Refreshing table data');
|
||||||
loadTableData(); // Обновляем данные при входе в систему
|
fetchTable(); // Обновляем данные при входе в систему
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
// Импортируем компоненты Element Plus
|
// Импортируем компоненты Element Plus
|
||||||
@@ -236,6 +246,7 @@ const relatedColumnId = ref(null);
|
|||||||
const relatedTableColumns = ref([]);
|
const relatedTableColumns = ref([]);
|
||||||
const newColPlaceholder = ref('');
|
const newColPlaceholder = ref('');
|
||||||
const multiOptionsInput = ref('');
|
const multiOptionsInput = ref('');
|
||||||
|
const newColPurpose = ref('');
|
||||||
|
|
||||||
// Новые фильтры по relation/multiselect/lookup
|
// Новые фильтры по relation/multiselect/lookup
|
||||||
const relationFilters = ref({});
|
const relationFilters = ref({});
|
||||||
@@ -304,6 +315,7 @@ function closeAddColModal() {
|
|||||||
selectedTagIds.value = [];
|
selectedTagIds.value = [];
|
||||||
newColPlaceholder.value = '';
|
newColPlaceholder.value = '';
|
||||||
multiOptionsInput.value = '';
|
multiOptionsInput.value = '';
|
||||||
|
newColPurpose.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleAddColumn() {
|
async function handleAddColumn() {
|
||||||
@@ -325,6 +337,9 @@ async function handleAddColumn() {
|
|||||||
options.relatedTableId = relatedTableId.value;
|
options.relatedTableId = relatedTableId.value;
|
||||||
options.relatedColumnId = relatedColumnId.value;
|
options.relatedColumnId = relatedColumnId.value;
|
||||||
}
|
}
|
||||||
|
if (newColPurpose.value) {
|
||||||
|
options.purpose = newColPurpose.value;
|
||||||
|
}
|
||||||
if (Object.keys(options).length > 0) {
|
if (Object.keys(options).length > 0) {
|
||||||
data.options = options;
|
data.options = options;
|
||||||
}
|
}
|
||||||
@@ -902,9 +917,6 @@ async function updateRowData(rowId) {
|
|||||||
min-width: 80px;
|
min-width: 80px;
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
}
|
}
|
||||||
.el-table-row-custom {
|
|
||||||
/* Можно добавить стили для высоты строк, если нужно */
|
|
||||||
}
|
|
||||||
|
|
||||||
.notion-input {
|
.notion-input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -562,9 +562,6 @@ export function useChat(auth) {
|
|||||||
console.log('[useChat] Clearing chat data');
|
console.log('[useChat] Clearing chat data');
|
||||||
// Очищаем данные при выходе из системы
|
// Очищаем данные при выходе из системы
|
||||||
messages.value = [];
|
messages.value = [];
|
||||||
newMessages.value = [];
|
|
||||||
readUserIds.value = [];
|
|
||||||
lastReadMessageDate.value = {};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener('refresh-application-data', () => {
|
window.addEventListener('refresh-application-data', () => {
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
:messages="messages"
|
:messages="messages"
|
||||||
:is-loading="isLoading || isConnectingWallet"
|
:is-loading="isLoading || isConnectingWallet"
|
||||||
:has-more-messages="messageLoading.hasMoreMessages"
|
:has-more-messages="messageLoading.hasMoreMessages"
|
||||||
:currentUserId="auth.userId"
|
:currentUserId="auth.userId.value"
|
||||||
v-model:newMessage="newMessage"
|
v-model:newMessage="newMessage"
|
||||||
v-model:attachments="attachments"
|
v-model:attachments="attachments"
|
||||||
@send-message="handleSendMessage"
|
@send-message="handleSendMessage"
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
:messages="messages"
|
:messages="messages"
|
||||||
:is-loading="isLoading || isConnectingWallet"
|
:is-loading="isLoading || isConnectingWallet"
|
||||||
:has-more-messages="messageLoading.hasMoreMessages"
|
:has-more-messages="messageLoading.hasMoreMessages"
|
||||||
:currentUserId="auth.userId"
|
:currentUserId="auth.userId.value"
|
||||||
v-model:newMessage="newMessage"
|
v-model:newMessage="newMessage"
|
||||||
v-model:attachments="attachments"
|
v-model:attachments="attachments"
|
||||||
@send-message="handleSendMessage"
|
@send-message="handleSendMessage"
|
||||||
|
|||||||
@@ -51,7 +51,7 @@
|
|||||||
<!-- Основной контент -->
|
<!-- Основной контент -->
|
||||||
<div class="page-content">
|
<div class="page-content">
|
||||||
<h2>Содержание</h2>
|
<h2>Содержание</h2>
|
||||||
<div class="content-text" v-if="page.format === 'html'" v-html="formatContent(page.content)"></div>
|
<div class="content-text" v-if="page.format === 'html'" v-html="formatContent"></div>
|
||||||
<div v-else-if="page.format === 'pdf' && page.file_path" class="file-preview">
|
<div v-else-if="page.format === 'pdf' && page.file_path" class="file-preview">
|
||||||
<embed :src="page.file_path" type="application/pdf" class="pdf-embed" />
|
<embed :src="page.file_path" type="application/pdf" class="pdf-embed" />
|
||||||
<a class="btn btn-outline" :href="page.file_path" target="_blank" download>Скачать PDF</a>
|
<a class="btn btn-outline" :href="page.file_path" target="_blank" download>Скачать PDF</a>
|
||||||
@@ -106,10 +106,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue';
|
import { ref, onMounted, computed } from 'vue';
|
||||||
import { useRouter, useRoute } from 'vue-router';
|
import { useRouter, useRoute } from 'vue-router';
|
||||||
import BaseLayout from '../../components/BaseLayout.vue';
|
import BaseLayout from '../../components/BaseLayout.vue';
|
||||||
import pagesService from '../../services/pagesService';
|
import pagesService from '../../services/pagesService';
|
||||||
|
import { marked } from 'marked';
|
||||||
|
import DOMPurify from 'dompurify';
|
||||||
|
|
||||||
// Props
|
// Props
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -161,10 +163,26 @@ function formatAddress(address) {
|
|||||||
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatContent(content) {
|
const formatContent = computed(() => {
|
||||||
|
if (!page.value || !page.value.content) return '';
|
||||||
|
const content = page.value.content;
|
||||||
|
|
||||||
|
// Проверяем, является ли контент markdown (содержит markdown синтаксис)
|
||||||
|
const isMarkdown = /^#{1,6}\s|^\*\s|^\-\s|^\d+\.\s|```|\[.+\]\(.+\)|!\[.+\]\(.+\)/m.test(content);
|
||||||
|
|
||||||
|
if (isMarkdown) {
|
||||||
|
// Конвертируем markdown в HTML
|
||||||
|
const rawHtml = marked.parse(content);
|
||||||
|
return DOMPurify.sanitize(rawHtml);
|
||||||
|
} else {
|
||||||
|
// Простое форматирование - замена переносов строк на <br>
|
||||||
|
return content.replace(/\n/g, '<br>');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function formatContentAsFunc(content) {
|
||||||
if (!content) return '';
|
if (!content) return '';
|
||||||
if (typeof content !== 'string') return '';
|
if (typeof content !== 'string') return '';
|
||||||
// Простое форматирование - замена переносов строк на <br>
|
|
||||||
return content.replace(/\n/g, '<br>');
|
return content.replace(/\n/g, '<br>');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,6 +302,119 @@ onMounted(() => {
|
|||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
line-height: 1.7;
|
line-height: 1.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Markdown стили */
|
||||||
|
.content-text h1 {
|
||||||
|
color: var(--color-primary);
|
||||||
|
font-size: 2rem;
|
||||||
|
margin: 1.5rem 0 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-text h2 {
|
||||||
|
color: var(--color-primary);
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin: 1.25rem 0 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-text h3 {
|
||||||
|
color: var(--color-primary);
|
||||||
|
font-size: 1.25rem;
|
||||||
|
margin: 1rem 0 0.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-text h4 {
|
||||||
|
color: var(--color-primary);
|
||||||
|
font-size: 1.1rem;
|
||||||
|
margin: 0.75rem 0 0.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-text p {
|
||||||
|
margin: 0.75rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-text ul,
|
||||||
|
.content-text ol {
|
||||||
|
margin: 1rem 0;
|
||||||
|
padding-left: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-text li {
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-text code {
|
||||||
|
background: #f4f4f4;
|
||||||
|
padding: 0.2rem 0.4rem;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-text pre {
|
||||||
|
background: #f4f4f4;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-text pre code {
|
||||||
|
background: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-text blockquote {
|
||||||
|
border-left: 4px solid var(--color-primary);
|
||||||
|
padding-left: 1rem;
|
||||||
|
margin: 1rem 0;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-text table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-text table th,
|
||||||
|
.content-text table td {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 0.5rem;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-text table th {
|
||||||
|
background: #f8f9fa;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-text a {
|
||||||
|
color: var(--color-primary);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-text a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-text hr {
|
||||||
|
border: none;
|
||||||
|
border-top: 2px solid #f0f0f0;
|
||||||
|
margin: 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-text strong {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-text em {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
.seo-info { display: grid; gap: 12px; }
|
.seo-info { display: grid; gap: 12px; }
|
||||||
.seo-item { display: flex; justify-content: space-between; align-items: flex-start; padding: 8px 0; border-bottom: 1px solid #f0f0f0; }
|
.seo-item { display: flex; justify-content: space-between; align-items: flex-start; padding: 8px 0; border-bottom: 1px solid #f0f0f0; }
|
||||||
.seo-item:last-child { border-bottom: none; }
|
.seo-item:last-child { border-bottom: none; }
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ const canManageLegalDocs = computed(() => hasPermission(SHARED_PERMISSIONS.MANAG
|
|||||||
|
|
||||||
function goBack() { router.push({ name: 'content-list' }); }
|
function goBack() { router.push({ name: 'content-list' }); }
|
||||||
function openPublic(id) { router.push({ name: 'public-page-view', params: { id } }); }
|
function openPublic(id) { router.push({ name: 'public-page-view', params: { id } }); }
|
||||||
function goEdit(id) { router.push({ name: 'page-edit', params: { id } }); }
|
function goEdit(id) { router.push({ name: 'content-create', query: { edit: id } }); }
|
||||||
async function reindex(id) {
|
async function reindex(id) {
|
||||||
try {
|
try {
|
||||||
await api.post(`/pages/${id}/reindex`);
|
await api.post(`/pages/${id}/reindex`);
|
||||||
|
|||||||
@@ -300,7 +300,17 @@ async function loadEmbeddingModels() {
|
|||||||
}
|
}
|
||||||
async function loadPlaceholders() {
|
async function loadPlaceholders() {
|
||||||
const { data } = await axios.get('/tables/placeholders/all');
|
const { data } = await axios.get('/tables/placeholders/all');
|
||||||
placeholders.value = Array.isArray(data) ? data : [];
|
const allPlaceholders = Array.isArray(data) ? data : [];
|
||||||
|
|
||||||
|
// Фильтруем только плейсхолдеры из выбранных RAG таблиц
|
||||||
|
if (settings.value.selected_rag_tables) {
|
||||||
|
const selectedTableId = typeof settings.value.selected_rag_tables === 'object'
|
||||||
|
? settings.value.selected_rag_tables[0]
|
||||||
|
: settings.value.selected_rag_tables;
|
||||||
|
placeholders.value = allPlaceholders.filter(ph => ph.table_id === Number(selectedTableId));
|
||||||
|
} else {
|
||||||
|
placeholders.value = [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
function openEditPlaceholder(ph) {
|
function openEditPlaceholder(ph) {
|
||||||
editingPlaceholder.value = { ...ph };
|
editingPlaceholder.value = { ...ph };
|
||||||
@@ -316,6 +326,11 @@ async function savePlaceholderEdit() {
|
|||||||
await loadPlaceholders();
|
await loadPlaceholders();
|
||||||
closeEditPlaceholder();
|
closeEditPlaceholder();
|
||||||
}
|
}
|
||||||
|
// Обновляем плейсхолдеры при изменении выбранной RAG таблицы
|
||||||
|
watch(() => settings.value.selected_rag_tables, () => {
|
||||||
|
loadPlaceholders();
|
||||||
|
});
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadSettings();
|
await loadSettings();
|
||||||
await loadUserTables();
|
await loadUserTables();
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
1.1. Правообладатель: Тарабанов Александр Викторович (Автор), паспорт/ИНН, адрес, e-mail: info@hb3-accelerator.com.
|
1.1. Правообладатель: Тарабанов Александр Викторович (Автор), паспорт/ИНН, адрес, e-mail: info@hb3-accelerator.com.
|
||||||
|
|
||||||
1.2. Дистрибьютор: ______________________________ (юр. лицо), ОГРН/ИНН, адрес, представитель, основание полномочий.
|
1.2. Дистрибьютор: ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ "ЭРАЙТИ" (ООО "ЭРАЙТИ"), ОГРН: 1222600014383, ИНН: 2636220809, КПП: 263601001, адрес: 355007, СТАВРОПОЛЬСКИЙ КРАЙ, Г.О. ГОРОД СТАВРОПОЛЬ, Г СТАВРОПОЛЬ, УЛ БУРМИСТРОВА, Д. 65Б, ПОМЕЩ. 2, представитель: Тарабанов Александр Викторович, генеральный директор, действующий на основании Устава.
|
||||||
|
|
||||||
## 2. Термины
|
## 2. Термины
|
||||||
|
|
||||||
@@ -153,16 +153,18 @@
|
|||||||
|
|
||||||
Реквизиты и подписи
|
Реквизиты и подписи
|
||||||
|
|
||||||
Правообладатель: _____________________________ Дата: __.__.20__
|
Правообладатель: Тарабанов Александр Викторович (Автор), e-mail: info@hb3-accelerator.com.
|
||||||
|
Дата: __.__.20__
|
||||||
|
|
||||||
Дистрибьютор: _____________________________ Дата: __.__.20__
|
Дистрибьютор: ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ "ЭРАЙТИ" (ООО "ЭРАЙТИ"), 355007, СТАВРОПОЛЬСКИЙ КРАЙ, Г.О. ГОРОД СТАВРОПОЛЬ, Г СТАВРОПОЛЬ, УЛ БУРМИСТРОВА, Д. 65Б, ПОМЕЩ. 2, ИНН: 2636220809, ОГРН: 1222600014383, 18900@эрайти.рф, +7 (968) 269-92-64.
|
||||||
|
Дата: __.__.20__
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Приложение 1. Порядок выдачи и учета токенов (лицензий)
|
### Приложение 1. Порядок выдачи и учета токенов (лицензий)
|
||||||
|
|
||||||
1) Выпуск и пул токенов: создается и ведется Правообладателем.
|
1) Выпуск и пул токенов: создается и ведется Правообладателем.
|
||||||
Адрес смарт‑контракта токенов (лицензий): ________________________________.
|
Адрес смарт‑контракта токенов (лицензий): 0xdD27a91692da59d1Ee7dD1Fb342B9f1B5FF29386.
|
||||||
|
|
||||||
2) Распределение Дистрибьютору: по заявке/лотами с указанием лимитов и сроков.
|
2) Распределение Дистрибьютору: по заявке/лотами с указанием лимитов и сроков.
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
# Согласие на обработку персональных данных (онлайн)
|
# Согласие на обработку персональных данных
|
||||||
|
|
||||||
Субъект ПДн/Представитель Клиента подтверждает: ФИО, должность/основание представительства, e‑mail, телефон.
|
При использовании приложения **эрайти.рф** даю согласие на обработку персональных данных **ООО "ЭРАЙТИ"**.
|
||||||
|
|
||||||
Оператор: {{company_name}}, адрес: {{company_address}}. Дается согласие на обработку персональных данных для заключения и исполнения договора, обслуживания и коммуникаций.
|
Я, субъект персональных данных/представитель Клиента, предоставляю настоящее согласие на обработку персональных данных в соответствии с Федеральным законом от 27.07.2006 № 152-ФЗ "О персональных данных".
|
||||||
|
|
||||||
|
**Оператор персональных данных:**
|
||||||
|
ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ "ЭРАЙТИ" (ООО "ЭРАЙТИ")
|
||||||
|
ОГРН: 1222600014383, ИНН: 2636220809
|
||||||
|
Адрес: 355007, СТАВРОПОЛЬСКИЙ КРАЙ, Г.О. ГОРОД СТАВРОПОЛЬ, Г СТАВРОПОЛЬ, УЛ БУРМИСТРОВА, Д. 65Б, ПОМЕЩ. 2
|
||||||
|
|
||||||
|
Я предоставляю согласие на обработку следующих персональных данных для заключения и исполнения договора, обслуживания и коммуникаций.
|
||||||
|
|
||||||
1. Перечень ПДн
|
1. Перечень ПДн
|
||||||
— ФИО, должность/основание представительства, контактные данные (e‑mail, телефон);
|
— ФИО, должность/основание представительства, контактные данные (e‑mail, телефон);
|
||||||
@@ -19,11 +26,14 @@
|
|||||||
— хостинг‑провайдеру и иным лицам по договору/закону при соблюдении мер защиты; трансграничная передача не осуществляется, если отдельно не указано.
|
— хостинг‑провайдеру и иным лицам по договору/закону при соблюдении мер защиты; трансграничная передача не осуществляется, если отдельно не указано.
|
||||||
|
|
||||||
5. Отзыв согласия
|
5. Отзыв согласия
|
||||||
— по обращению на {{privacy_email}}. Отзыв не влияет на законность обработки до даты отзыва. При отзыве обработка прекращается, если иное не требуется законом.
|
— по обращению на 18900@эрайти.рф Отзыв не влияет на законность обработки до даты отзыва. При отзыве обработка прекращается, если иное не требуется законом.
|
||||||
|
|
||||||
6. Подтверждение акцепта
|
6. Подтверждение акцепта
|
||||||
— согласие предоставляется онлайн (клик/чекбокс) с фиксацией даты/времени, IP и версии документа техническими средствами.
|
— согласие предоставляется онлайн (клик/чекбокс) с автоматической фиксацией даты/времени, IP-адреса и версии документа техническими средствами.
|
||||||
|
|
||||||
Подтверждаю ознакомление с Политикой конфиденциальности на {{website}}. Версия согласия указывается в интерфейсе при акцепте.
|
**Дата предоставления согласия:** автоматически фиксируется системой
|
||||||
|
**IP-адрес:** автоматически фиксируется системой
|
||||||
|
|
||||||
|
Я подтверждаю, что ознакомился с Политикой конфиденциальности на эрайти.рф. Версия согласия указывается в интерфейсе при акцепте.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Согласие на использование файлов cookie
|
# Согласие на использование файлов cookie
|
||||||
|
|
||||||
Оператор: {{company_name}}. Мы используем cookie для работы сессий и улучшения сервиса.
|
Оператор: ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ "ЭРАЙТИ" (ООО "ЭРАЙТИ"). Мы используем cookie для работы сессий и улучшения сервиса.
|
||||||
|
|
||||||
1. Что такое cookie
|
1. Что такое cookie
|
||||||
— небольшие файлы, сохраняемые браузером для корректной работы веб‑приложения.
|
— небольшие файлы, сохраняемые браузером для корректной работы веб‑приложения.
|
||||||
@@ -17,6 +17,6 @@
|
|||||||
5. Правовая основа
|
5. Правовая основа
|
||||||
— согласие пользователя, кроме строго необходимых cookie, необходимых для предоставления услуги.
|
— согласие пользователя, кроме строго необходимых cookie, необходимых для предоставления услуги.
|
||||||
|
|
||||||
Продолжая использовать сайт/приложение или настраивая панели cookie, вы даёте согласие на использование cookie в соответствии с Политикой конфиденциальности на {{website}}.
|
Продолжая использовать сайт/приложение или настраивая панели cookie, вы даёте согласие на использование cookie в соответствии с Политикой конфиденциальности на эрайти.рф.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
# Права субъектов персональных данных и отзыв согласия
|
# Права субъектов персональных данных и отзыв согласия
|
||||||
|
|
||||||
Оператор: {{company_name}}, адрес: {{company_address}}, контакты: {{privacy_email}}, {{privacy_phone}}.
|
Оператор: ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ "ЭРАЙТИ" (ООО "ЭРАЙТИ"), адрес: 355007, СТАВРОПОЛЬСКИЙ КРАЙ, Г.О. ГОРОД СТАВРОПОЛЬ, Г СТАВРОПОЛЬ, УЛ БУРМИСТРОВА, Д. 65Б, ПОМЕЩ. 2, контакты: 18900@эрайти.рф, +7 (968) 269-92-64.
|
||||||
|
|
||||||
1. Права субъекта (ст. 14 152‑ФЗ)
|
1. Права субъекта (ст. 14 152‑ФЗ)
|
||||||
— получать информацию об обработке ПДн; требовать уточнения, блокирования или уничтожения ПДн; отзывать согласие.
|
— получать информацию об обработке ПДн; требовать уточнения, блокирования или уничтожения ПДн; отзывать согласие.
|
||||||
|
|
||||||
2. Порядок обращения
|
2. Порядок обращения
|
||||||
— направьте запрос на {{privacy_email}}. Срок ответа — до 30 календарных дней. Ответ предоставляется в электронной форме по адресу, с которого поступил запрос (если не указано иное), при необходимости — на бумаге.
|
— направьте запрос на 18900@эрайти.рф. Срок ответа — до 30 календарных дней. Ответ предоставляется в электронной форме по адресу, с которого поступил запрос (если не указано иное), при необходимости — на бумаге.
|
||||||
|
|
||||||
3. Отзыв согласия
|
3. Отзыв согласия
|
||||||
— возможен по электронному запросу. После отзыва обработка прекращается, если иное не требуется по закону.
|
— возможен по электронному запросу. После отзыва обработка прекращается, если иное не требуется по закону.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Политика конфиденциальности
|
# Политика конфиденциальности
|
||||||
|
|
||||||
Оператор персональных данных: {{company_name}}, ОГРН {{company_ogrn}}, ИНН {{company_inn}}, адрес: {{company_address}}. Контакты: {{privacy_email}}, {{privacy_phone}}.
|
Оператор персональных данных: ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ "ЭРАЙТИ" (ООО "ЭРАЙТИ"), ОГРН: 1222600014383, ИНН: 2636220809, адрес: 355007, СТАВРОПОЛЬСКИЙ КРАЙ, Г.О. ГОРОД СТАВРОПОЛЬ, Г СТАВРОПОЛЬ, УЛ БУРМИСТРОВА, Д. 65Б, ПОМЕЩ. 2. Контакты: 18900@эрайти.рф, +7 (968) 269-92-64.
|
||||||
|
|
||||||
1. Цели обработки
|
1. Цели обработки
|
||||||
— заключение/исполнение договоров, обслуживание клиентов, биллинг, поддержка, улучшение сервиса, соблюдение законодательства.
|
— заключение/исполнение договоров, обслуживание клиентов, биллинг, поддержка, улучшение сервиса, соблюдение законодательства.
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
— иные данные — до достижения целей или отзыва согласия, если иное не требуется по закону.
|
— иные данные — до достижения целей или отзыва согласия, если иное не требуется по закону.
|
||||||
|
|
||||||
6. Передача третьим лицам
|
6. Передача третьим лицам
|
||||||
— передаются по договору/закону при необходимости оказания услуг и соблюдении конфиденциальности и безопасности. Хостинг: {{hosting_provider}} ({{hosting_type}}). Трансграничная передача не осуществляется, если отдельно не указано.
|
— личные данные не передаются третьим лицам. Хостинг осуществляется на арендованном VDS сервере ООО "Хостланд" (ИНН: 7811142520), все данные хранятся в изолированных Docker контейнерах в зашифрованном виде. Трансграничная передача не осуществляется.
|
||||||
|
|
||||||
7. Меры защиты
|
7. Меры защиты
|
||||||
— шифрование данных в БД, контроль и разграничение доступа, учёт действий, резервное копирование, изоляция в Docker‑контейнерах, организационные меры.
|
— шифрование данных в БД, контроль и разграничение доступа, учёт действий, резервное копирование, изоляция в Docker‑контейнерах, организационные меры.
|
||||||
@@ -40,11 +40,10 @@
|
|||||||
— сроки: сессионные — до завершения сессии; функциональные/аналитические — согласно настройкам браузера/панели, но не дольше 30 дней без продления.
|
— сроки: сессионные — до завершения сессии; функциональные/аналитические — согласно настройкам браузера/панели, но не дольше 30 дней без продления.
|
||||||
|
|
||||||
9. Права субъектов и порядок обращений
|
9. Права субъектов и порядок обращений
|
||||||
— получение информации об обработке, уточнение, блокирование/уничтожение, отзыв согласия; срок ответа — до 30 календарных дней. Контакты: {{privacy_email}}.
|
— получение информации об обработке, уточнение, блокирование/уничтожение, отзыв согласия; срок ответа — до 30 календарных дней. Контакты: 18900@эрайти.рф.
|
||||||
|
|
||||||
10. Контакты Оператора
|
10. Контакты Оператора
|
||||||
— {{company_name}}, {{company_address}}, {{privacy_email}}, {{privacy_phone}}, сайт: {{website}}.
|
— ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ "ЭРАЙТИ" (ООО "ЭРАЙТИ"), 355007, СТАВРОПОЛЬСКИЙ КРАЙ, Г.О. ГОРОД СТАВРОПОЛЬ, Г СТАВРОПОЛЬ, УЛ БУРМИСТРОВА, Д. 65Б, ПОМЕЩ. 2, 18900@эрайти.рф, +7 (968) 269-92-64, сайт: эрайти.рф.
|
||||||
|
|
||||||
Эта политика действует с даты публикации и актуализируется по мере изменений. Уведомление в Роскомнадзор направляется после подготовки и публикации документов в приложении.
|
|
||||||
|
|
||||||
|
Эта политика действует с даты публикации и актуализируется по мере изменений.
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Акт выполненных работ № ____ от «__» ________ 20__ г.
|
# Акт выполненных работ № ____ от «__» ________ 20__ г.
|
||||||
|
|
||||||
Исполнитель: {{company_name}}, ОГРН {{company_ogrn}}, ИНН {{company_inn}}, адрес: {{company_address}}.
|
Исполнитель: ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ "ЭРАЙТИ" (ООО "ЭРАЙТИ"), ОГРН: 1222600014383, ИНН: 2636220809, адрес: 355007, СТАВРОПОЛЬСКИЙ КРАЙ, Г.О. ГОРОД СТАВРОПОЛЬ, Г СТАВРОПОЛЬ, УЛ БУРМИСТРОВА, Д. 65Б, ПОМЕЩ. 2.
|
||||||
Заказчик: ________________________________________________
|
Заказчик: ________________________________________________
|
||||||
Адрес кошелька Заказчика (тестовая сеть): __________________________
|
Адрес кошелька Заказчика (тестовая сеть): __________________________
|
||||||
Tx/Hash передачи токена‑лицензии: _________________________________
|
Tx/Hash передачи токена‑лицензии: _________________________________
|
||||||
@@ -15,7 +15,7 @@ Tx/Hash передачи токена‑лицензии: ______________________
|
|||||||
|
|
||||||
4. Замечания Заказчика: отсутствуют / изложены в приложении.
|
4. Замечания Заказчика: отсутствуют / изложены в приложении.
|
||||||
|
|
||||||
Стороны подтверждают, что услуги оказаны в полном объёме. Порядок приёмки и фиксация передачи лицензии (on‑chain) определяются документом «Условия приобретения и обслуживания Digital Legal Entity» (`docs/service-terms.md`). Указанные выше адрес кошелька и Tx/Hash используются для идентификации факта передачи.
|
Стороны подтверждают, что услуги оказаны в полном объёме. Порядок приёмки и фиксация передачи лицензии (on‑chain) определяются документом «Условия приобретения и обслуживания Digital Legal Entity». Указанные выше адрес кошелька и Tx/Hash используются для идентификации факта передачи.
|
||||||
|
|
||||||
Подписи сторон
|
Подписи сторон
|
||||||
Исполнитель: ______________________ /М.П./
|
Исполнитель: ______________________ /М.П./
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
# Договор оказания услуг / лицензионный договор (минимальный)
|
# Договор оказания услуг / лицензионный договор (минимальный)
|
||||||
|
|
||||||
Исполнитель: {{company_name}}, ОГРН {{company_ogrn}}, ИНН {{company_inn}}, адрес: {{company_address}}.
|
Исполнитель: ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ "ЭРАЙТИ" (ООО "ЭРАЙТИ"), ОГРН: 1222600014383, ИНН: 2636220809, адрес: 355007, СТАВРОПОЛЬСКИЙ КРАЙ, Г.О. ГОРОД СТАВРОПОЛЬ, Г СТАВРОПОЛЬ, УЛ БУРМИСТРОВА, Д. 65Б, ПОМЕЩ. 2.
|
||||||
Заказчик: юрлицо/ИП или их представитель.
|
Заказчик: ________________________________________________
|
||||||
|
ОГРН: ________________________________________________
|
||||||
|
ИНН: ________________________________________________
|
||||||
|
КПП: ________________________________________________
|
||||||
|
Адрес: ________________________________________________
|
||||||
|
|
||||||
|
|
||||||
1. Предмет
|
1. Предмет
|
||||||
1.1. Исполнитель предоставляет доступ к веб‑приложению DLE и/или неисключительную лицензию на ПО и оказывает связанные услуги; Заказчик принимает и оплачивает.
|
1.1. Исполнитель предоставляет доступ к веб‑приложению DLE и/или неисключительную лицензию на ПО и оказывает связанные услуги; Заказчик принимает и оплачивает.
|
||||||
@@ -17,16 +22,16 @@
|
|||||||
|
|
||||||
3. Персональные данные
|
3. Персональные данные
|
||||||
3.1. Обработка ПДн осуществляется в соответствии с действующими документами Исполнителя:
|
3.1. Обработка ПДн осуществляется в соответствии с действующими документами Исполнителя:
|
||||||
— Политика конфиденциальности (`PRIVACY_POLICY_RU.md`),
|
— Политика конфиденциальности,
|
||||||
— Согласие на обработку ПДн (`CONSENT_PERSONAL_DATA_RU.md`),
|
— Согласие на обработку ПДн,
|
||||||
— Права субъектов и отзыв (`PDN_RIGHTS_AND_REVOCATION_RU.md`),
|
— Права субъектов и отзыв,
|
||||||
— Согласие на cookie (`COOKIE_CONSENT_RU.md`).
|
— Согласие на cookie.
|
||||||
|
|
||||||
4. Приёмка
|
4. Приёмка
|
||||||
4.1. Результат услуг подтверждается Актом выполненных работ. Порядок и критерии приёмки, а также фиксация передачи лицензии (on‑chain) определяются документом «Условия приобретения и обслуживания Digital Legal Entity» (`docs/service-terms.md`).
|
4.1. Результат услуг подтверждается Актом выполненных работ. Порядок и критерии приёмки, а также фиксация передачи лицензии (on‑chain) определяются документом «Условия приобретения и обслуживания Digital Legal Entity».
|
||||||
|
|
||||||
5. Условия обслуживания и возвратов
|
5. Условия обслуживания и возвратов
|
||||||
5.1. Обновления, сопровождение, сроки и порядок возвратов регулируются документом «Условия приобретения и обслуживания Digital Legal Entity» (`docs/service-terms.md`).
|
5.1. Обновления, сопровождение, сроки и порядок возвратов регулируются документом «Условия приобретения и обслуживания Digital Legal Entity».
|
||||||
|
|
||||||
6. Коммуникации
|
6. Коммуникации
|
||||||
6.1. Взаимодействие сторон осуществляется через веб‑приложение, электронную почту и мессенджеры.
|
6.1. Взаимодействие сторон осуществляется через веб‑приложение, электронную почту и мессенджеры.
|
||||||
@@ -34,10 +39,14 @@
|
|||||||
7. Прочие условия
|
7. Прочие условия
|
||||||
7.1. Применяется право РФ. Споры рассматриваются в суде по месту нахождения Исполнителя.
|
7.1. Применяется право РФ. Споры рассматриваются в суде по месту нахождения Исполнителя.
|
||||||
|
|
||||||
Реквизиты и подписи
|
Реквизиты и подписи
|
||||||
Исполнитель: {{company_name}}, {{company_address}}, ИНН {{company_inn}}, ОГРН {{company_ogrn}}, {{privacy_email}}, {{privacy_phone}}.
|
Исполнитель: ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ "ЭРАЙТИ" (ООО "ЭРАЙТИ"), 355007, СТАВРОПОЛЬСКИЙ КРАЙ, Г.О. ГОРОД СТАВРОПОЛЬ, Г СТАВРОПОЛЬ, УЛ БУРМИСТРОВА, Д. 65Б, ПОМЕЩ. 2, ИНН: 2636220809, ОГРН: 1222600014383, 18900@эрайти.рф, +7 (968) 269-92-64.
|
||||||
Заказчик: ____________________________________________
|
Заказчик: ________________________________________________
|
||||||
|
ОГРН: ________________________________________________
|
||||||
|
ИНН: ________________________________________________
|
||||||
|
КПП: ________________________________________________
|
||||||
|
Адрес: ________________________________________________
|
||||||
|
|
||||||
Подписи:
|
Подписи:
|
||||||
Исполнитель: ______________________ /М.П./
|
Исполнитель: ______________________ /М.П./
|
||||||
Заказчик: _________________________ /М.П./
|
Заказчик: _________________________ /М.П./
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
**Оператор персональных данных:** ООО "ЭРАЙТИ"
|
**Оператор персональных данных:** ООО "ЭРАЙТИ"
|
||||||
**Ответственное лицо:** Тарабанов Александр Викторович (генеральный директор)
|
**Ответственное лицо:** Тарабанов Александр Викторович (генеральный директор)
|
||||||
**Контактный телефон:** +7 (968) 269-92-64
|
**Контактный телефон:** +7 (968) 269-92-64
|
||||||
**Email для обращений:**
|
**Email для обращений:** 18900@эрайти.рф
|
||||||
**Хостинг:** ООО "Хостланд" (ИНН: 7811142520/781401001) - арендованный VDS сервер
|
**Хостинг:** ООО "Хостланд" (ИНН: 7811142520/781401001) - арендованный VDS сервер
|
||||||
**Сроки хранения персональных данных:**
|
**Сроки хранения персональных данных:**
|
||||||
- **Идентификационные данные:** До отзыва согласия субъектом ПДн
|
- **Идентификационные данные:** До отзыва согласия субъектом ПДн
|
||||||
|
|||||||
@@ -30,6 +30,14 @@ INSERT INTO roles (id, name_encrypted) VALUES
|
|||||||
ON CONFLICT (id) DO UPDATE SET
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
name_encrypted = EXCLUDED.name_encrypted;"
|
name_encrypted = EXCLUDED.name_encrypted;"
|
||||||
|
|
||||||
|
# Заполняем справочную таблицу is_rag_source
|
||||||
|
docker exec dapp-postgres psql -U dapp_user -d dapp_db -c "
|
||||||
|
INSERT INTO is_rag_source (id, name_encrypted) VALUES
|
||||||
|
(1, encrypt_text('Да', '$ENCRYPTION_KEY')),
|
||||||
|
(2, encrypt_text('Нет', '$ENCRYPTION_KEY'))
|
||||||
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
|
name_encrypted = EXCLUDED.name_encrypted;"
|
||||||
|
|
||||||
docker exec dapp-postgres psql -U dapp_user -d dapp_db -c "
|
docker exec dapp-postgres psql -U dapp_user -d dapp_db -c "
|
||||||
INSERT INTO rpc_providers (network_id_encrypted, rpc_url_encrypted, chain_id)
|
INSERT INTO rpc_providers (network_id_encrypted, rpc_url_encrypted, chain_id)
|
||||||
VALUES
|
VALUES
|
||||||
|
|||||||
Reference in New Issue
Block a user