feat: новая функция

This commit is contained in:
2025-11-01 15:58:17 +03:00
parent c33999edc7
commit 772d4cff54
22 changed files with 600 additions and 71 deletions

View File

@@ -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)

View File

@@ -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": {

View 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);
});

View File

@@ -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) {

View File

@@ -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);
}

View File

@@ -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] Подставлены плейсхолдеры в системный промпт`);
}

View File

@@ -242,11 +242,12 @@ ollama serve
- Используйте эмодзи умеренно (1-2 на сообщение)
Правила ответа:
1. Сначала ищите ответ в базе знаний (RAG)
2. Если нашли — отвечайте на основе найденной информации
3. Если не нашли — честно скажите и предложите помощь оператора
4. Не придумывайте информацию о ценах, сроках, условиях
5. При сложных вопросах предлагайте связаться с менеджером
1. ОБЯЗАТЕЛЬНО: Отвечайте ТОЛЬКО на русском языке. Все вопросы и ответы должны быть на русском языке
2. Сначала ищите ответ в базе знаний (RAG)
3. Если нашли — отвечайте на основе найденной информации
4. Если не нашли — честно скажите и предложите помощь оператора
5. Не придумывайте информацию о ценах, сроках, условиях
6. При сложных вопросах предлагайте связаться с менеджером
Всегда заканчивайте: "Чем еще могу помочь? 😊"
```
@@ -258,6 +259,16 @@ ollama serve
> 💡 **Подсказка**: Модели автоматически подтянутся из настроек 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-таблицы
1. В поле **"Выбранные RAG-таблицы"** выберите созданную таблицу:

View File

@@ -156,7 +156,17 @@
</div>
<label>Плейсхолдер</label>
<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">
<button class="save-btn" @click="handleAddColumn">Добавить</button>
<button class="cancel-btn" @click="closeAddColModal">Отмена</button>
@@ -184,7 +194,7 @@ onMounted(() => {
window.addEventListener('refresh-application-data', () => {
console.log('[UserTableView] Refreshing table data');
loadTableData(); // Обновляем данные при входе в систему
fetchTable(); // Обновляем данные при входе в систему
});
});
// Импортируем компоненты Element Plus
@@ -236,6 +246,7 @@ const relatedColumnId = ref(null);
const relatedTableColumns = ref([]);
const newColPlaceholder = ref('');
const multiOptionsInput = ref('');
const newColPurpose = ref('');
// Новые фильтры по relation/multiselect/lookup
const relationFilters = ref({});
@@ -304,6 +315,7 @@ function closeAddColModal() {
selectedTagIds.value = [];
newColPlaceholder.value = '';
multiOptionsInput.value = '';
newColPurpose.value = '';
}
async function handleAddColumn() {
@@ -325,6 +337,9 @@ async function handleAddColumn() {
options.relatedTableId = relatedTableId.value;
options.relatedColumnId = relatedColumnId.value;
}
if (newColPurpose.value) {
options.purpose = newColPurpose.value;
}
if (Object.keys(options).length > 0) {
data.options = options;
}
@@ -902,9 +917,6 @@ async function updateRowData(rowId) {
min-width: 80px;
max-width: 600px;
}
.el-table-row-custom {
/* Можно добавить стили для высоты строк, если нужно */
}
.notion-input {
width: 100%;

View File

@@ -562,9 +562,6 @@ export function useChat(auth) {
console.log('[useChat] Clearing chat data');
// Очищаем данные при выходе из системы
messages.value = [];
newMessages.value = [];
readUserIds.value = [];
lastReadMessageDate.value = {};
});
window.addEventListener('refresh-application-data', () => {

View File

@@ -33,7 +33,7 @@
:messages="messages"
:is-loading="isLoading || isConnectingWallet"
:has-more-messages="messageLoading.hasMoreMessages"
:currentUserId="auth.userId"
:currentUserId="auth.userId.value"
v-model:newMessage="newMessage"
v-model:attachments="attachments"
@send-message="handleSendMessage"
@@ -45,7 +45,7 @@
:messages="messages"
:is-loading="isLoading || isConnectingWallet"
:has-more-messages="messageLoading.hasMoreMessages"
:currentUserId="auth.userId"
:currentUserId="auth.userId.value"
v-model:newMessage="newMessage"
v-model:attachments="attachments"
@send-message="handleSendMessage"

View File

@@ -51,7 +51,7 @@
<!-- Основной контент -->
<div class="page-content">
<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">
<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>
@@ -106,10 +106,12 @@
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { ref, onMounted, computed } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import BaseLayout from '../../components/BaseLayout.vue';
import pagesService from '../../services/pagesService';
import { marked } from 'marked';
import DOMPurify from 'dompurify';
// Props
const props = defineProps({
@@ -161,10 +163,26 @@ function formatAddress(address) {
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 (typeof content !== 'string') return '';
// Простое форматирование - замена переносов строк на <br>
return content.replace(/\n/g, '<br>');
}
@@ -284,6 +302,119 @@ onMounted(() => {
font-size: 1rem;
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-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; }

View File

@@ -85,7 +85,7 @@ const canManageLegalDocs = computed(() => hasPermission(SHARED_PERMISSIONS.MANAG
function goBack() { router.push({ name: 'content-list' }); }
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) {
try {
await api.post(`/pages/${id}/reindex`);

View File

@@ -300,7 +300,17 @@ async function loadEmbeddingModels() {
}
async function loadPlaceholders() {
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) {
editingPlaceholder.value = { ...ph };
@@ -316,6 +326,11 @@ async function savePlaceholderEdit() {
await loadPlaceholders();
closeEditPlaceholder();
}
// Обновляем плейсхолдеры при изменении выбранной RAG таблицы
watch(() => settings.value.selected_rag_tables, () => {
loadPlaceholders();
});
onMounted(async () => {
await loadSettings();
await loadUserTables();

View File

@@ -6,7 +6,7 @@
1.1. Правообладатель: Тарабанов Александр Викторович (Автор), паспорт/ИНН, адрес, e-mail: info@hb3-accelerator.com.
1.2. Дистрибьютор: ______________________________ (юр. лицо), ОГРН/ИНН, адрес, представитель, основание полномочий.
1.2. Дистрибьютор: ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ "ЭРАЙТИ" (ООО "ЭРАЙТИ"), ОГРН: 1222600014383, ИНН: 2636220809, КПП: 263601001, адрес: 355007, СТАВРОПОЛЬСКИЙ КРАЙ, Г.О. ГОРОД СТАВРОПОЛЬ, Г СТАВРОПОЛЬ, УЛ БУРМИСТРОВА, Д. 65Б, ПОМЕЩ. 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) Выпуск и пул токенов: создается и ведется Правообладателем.
Адрес смарт‑контракта токенов (лицензий): ________________________________.
Адрес смарт‑контракта токенов (лицензий): 0xdD27a91692da59d1Ee7dD1Fb342B9f1B5FF29386.
2) Распределение Дистрибьютору: по заявке/лотами с указанием лимитов и сроков.

View File

@@ -1,8 +1,15 @@
# Согласие на обработку персональных данных (онлайн)
# Согласие на обработку персональных данных
Субъект ПДн/Представитель Клиента подтверждает: ФИО, должность/основание представительства, email, телефон.
При использовании приложения **эрайти.рф** даю согласие на обработку персональных данных **ООО "ЭРАЙТИ"**.
Оператор: {{company_name}}, адрес: {{company_address}}. Дается согласие на обработку персональных данных для заключения и исполнения договора, обслуживания и коммуникаций.
Я, субъект персональных данных/представитель Клиента, предоставляю настоящее согласие на обработку персональных данных в соответствии с Федеральным законом от 27.07.2006 № 152-ФЗ "О персональных данных".
**Оператор персональных данных:**
ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ "ЭРАЙТИ" (ООО "ЭРАЙТИ")
ОГРН: 1222600014383, ИНН: 2636220809
Адрес: 355007, СТАВРОПОЛЬСКИЙ КРАЙ, Г.О. ГОРОД СТАВРОПОЛЬ, Г СТАВРОПОЛЬ, УЛ БУРМИСТРОВА, Д. 65Б, ПОМЕЩ. 2
Я предоставляю согласие на обработку следующих персональных данных для заключения и исполнения договора, обслуживания и коммуникаций.
1. Перечень ПДн
— ФИО, должность/основание представительства, контактные данные (email, телефон);
@@ -19,11 +26,14 @@
— хостинг‑провайдеру и иным лицам по договору/закону при соблюдении мер защиты; трансграничная передача не осуществляется, если отдельно не указано.
5. Отзыв согласия
— по обращению на {{privacy_email}}. Отзыв не влияет на законность обработки до даты отзыва. При отзыве обработка прекращается, если иное не требуется законом.
— по обращению на 18900@эрайти.рф Отзыв не влияет на законность обработки до даты отзыва. При отзыве обработка прекращается, если иное не требуется законом.
6. Подтверждение акцепта
— согласие предоставляется онлайн (клик/чекбокс) с фиксацией даты/времени, IP и версии документа техническими средствами.
— согласие предоставляется онлайн (клик/чекбокс) с автоматической фиксацией даты/времени, IP-адреса и версии документа техническими средствами.
Подтверждаю ознакомление с Политикой конфиденциальности на {{website}}. Версия согласия указывается в интерфейсе при акцепте.
**Дата предоставления согласия:** автоматически фиксируется системой
**IP-адрес:** автоматически фиксируется системой
Я подтверждаю, что ознакомился с Политикой конфиденциальности на эрайти.рф. Версия согласия указывается в интерфейсе при акцепте.

View File

@@ -1,6 +1,6 @@
# Согласие на использование файлов cookie
Оператор: {{company_name}}. Мы используем cookie для работы сессий и улучшения сервиса.
Оператор: ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ "ЭРАЙТИ" (ООО "ЭРАЙТИ"). Мы используем cookie для работы сессий и улучшения сервиса.
1. Что такое cookie
— небольшие файлы, сохраняемые браузером для корректной работы веб‑приложения.
@@ -17,6 +17,6 @@
5. Правовая основа
— согласие пользователя, кроме строго необходимых cookie, необходимых для предоставления услуги.
Продолжая использовать сайт/приложение или настраивая панели cookie, вы даёте согласие на использование cookie в соответствии с Политикой конфиденциальности на {{website}}.
Продолжая использовать сайт/приложение или настраивая панели cookie, вы даёте согласие на использование cookie в соответствии с Политикой конфиденциальности на эрайти.рф.

View File

@@ -1,12 +1,12 @@
# Права субъектов персональных данных и отзыв согласия
Оператор: {{company_name}}, адрес: {{company_address}}, контакты: {{privacy_email}}, {{privacy_phone}}.
Оператор: ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ "ЭРАЙТИ" (ООО "ЭРАЙТИ"), адрес: 355007, СТАВРОПОЛЬСКИЙ КРАЙ, Г.О. ГОРОД СТАВРОПОЛЬ, Г СТАВРОПОЛЬ, УЛ БУРМИСТРОВА, Д. 65Б, ПОМЕЩ. 2, контакты: 18900@эрайти.рф, +7 (968) 269-92-64.
1. Права субъекта (ст. 14 152ФЗ)
— получать информацию об обработке ПДн; требовать уточнения, блокирования или уничтожения ПДн; отзывать согласие.
2. Порядок обращения
— направьте запрос на {{privacy_email}}. Срок ответа — до 30 календарных дней. Ответ предоставляется в электронной форме по адресу, с которого поступил запрос (если не указано иное), при необходимости — на бумаге.
— направьте запрос на 18900@эрайти.рф. Срок ответа — до 30 календарных дней. Ответ предоставляется в электронной форме по адресу, с которого поступил запрос (если не указано иное), при необходимости — на бумаге.
3. Отзыв согласия
— возможен по электронному запросу. После отзыва обработка прекращается, если иное не требуется по закону.

View File

@@ -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. Цели обработки
— заключение/исполнение договоров, обслуживание клиентов, биллинг, поддержка, улучшение сервиса, соблюдение законодательства.
@@ -29,7 +29,7 @@
— иные данные — до достижения целей или отзыва согласия, если иное не требуется по закону.
6. Передача третьим лицам
передаются по договору/закону при необходимости оказания услуг и соблюдении конфиденциальности и безопасности. Хостинг: {{hosting_provider}} ({{hosting_type}}). Трансграничная передача не осуществляется, если отдельно не указано.
личные данные не передаются третьим лицам. Хостинг осуществляется на арендованном VDS сервере ООО "Хостланд" (ИНН: 7811142520), все данные хранятся в изолированных Docker контейнерах в зашифрованном виде. Трансграничная передача не осуществляется.
7. Меры защиты
— шифрование данных в БД, контроль и разграничение доступа, учёт действий, резервное копирование, изоляция в Dockerконтейнерах, организационные меры.
@@ -40,11 +40,10 @@
— сроки: сессионные — до завершения сессии; функциональные/аналитические — согласно настройкам браузера/панели, но не дольше 30 дней без продления.
9. Права субъектов и порядок обращений
— получение информации об обработке, уточнение, блокирование/уничтожение, отзыв согласия; срок ответа — до 30 календарных дней. Контакты: {{privacy_email}}.
— получение информации об обработке, уточнение, блокирование/уничтожение, отзыв согласия; срок ответа — до 30 календарных дней. Контакты: 18900@эрайти.рф.
10. Контакты Оператора
{{company_name}}, {{company_address}}, {{privacy_email}}, {{privacy_phone}}, сайт: {{website}}.
Эта политика действует с даты публикации и актуализируется по мере изменений. Уведомление в Роскомнадзор направляется после подготовки и публикации документов в приложении.
ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ "ЭРАЙТИ" (ООО "ЭРАЙТИ"), 355007, СТАВРОПОЛЬСКИЙ КРАЙ, Г.О. ГОРОД СТАВРОПОЛЬ, Г СТАВРОПОЛЬ, УЛ БУРМИСТРОВА, Д. 65Б, ПОМЕЩ. 2, 18900@эрайти.рф, +7 (968) 269-92-64, сайт: эрайти.рф.
Эта политика действует с даты публикации и актуализируется по мере изменений.

View File

@@ -1,6 +1,6 @@
# Акт выполненных работ № ____ от «__» ________ 20__ г.
Исполнитель: {{company_name}}, ОГРН {{company_ogrn}}, ИНН {{company_inn}}, адрес: {{company_address}}.
Исполнитель: ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ "ЭРАЙТИ" (ООО "ЭРАЙТИ"), ОГРН: 1222600014383, ИНН: 2636220809, адрес: 355007, СТАВРОПОЛЬСКИЙ КРАЙ, Г.О. ГОРОД СТАВРОПОЛЬ, Г СТАВРОПОЛЬ, УЛ БУРМИСТРОВА, Д. 65Б, ПОМЕЩ. 2.
Заказчик: ________________________________________________
Адрес кошелька Заказчика (тестовая сеть): __________________________
Tx/Hash передачи токена‑лицензии: _________________________________
@@ -15,7 +15,7 @@ Tx/Hash передачи токена‑лицензии: ______________________
4. Замечания Заказчика: отсутствуют / изложены в приложении.
Стороны подтверждают, что услуги оказаны в полном объёме. Порядок приёмки и фиксация передачи лицензии (onchain) определяются документом «Условия приобретения и обслуживания Digital Legal Entity» (`docs/service-terms.md`). Указанные выше адрес кошелька и Tx/Hash используются для идентификации факта передачи.
Стороны подтверждают, что услуги оказаны в полном объёме. Порядок приёмки и фиксация передачи лицензии (onchain) определяются документом «Условия приобретения и обслуживания Digital Legal Entity». Указанные выше адрес кошелька и Tx/Hash используются для идентификации факта передачи.
Подписи сторон
Исполнитель: ______________________ /М.П./

View File

@@ -1,7 +1,12 @@
# Договор оказания услуг / лицензионный договор (минимальный)
Исполнитель: {{company_name}}, ОГРН {{company_ogrn}}, ИНН {{company_inn}}, адрес: {{company_address}}.
Заказчик: юрлицо/ИП или их представитель.
Исполнитель: ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ "ЭРАЙТИ" (ООО "ЭРАЙТИ"), ОГРН: 1222600014383, ИНН: 2636220809, адрес: 355007, СТАВРОПОЛЬСКИЙ КРАЙ, Г.О. ГОРОД СТАВРОПОЛЬ, Г СТАВРОПОЛЬ, УЛ БУРМИСТРОВА, Д. 65Б, ПОМЕЩ. 2.
Заказчик: ________________________________________________
ОГРН: ________________________________________________
ИНН: ________________________________________________
КПП: ________________________________________________
Адрес: ________________________________________________
1. Предмет
1.1. Исполнитель предоставляет доступ к веб‑приложению DLE и/или неисключительную лицензию на ПО и оказывает связанные услуги; Заказчик принимает и оплачивает.
@@ -17,16 +22,16 @@
3. Персональные данные
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.1. Результат услуг подтверждается Актом выполненных работ. Порядок и критерии приёмки, а также фиксация передачи лицензии (onchain) определяются документом «Условия приобретения и обслуживания Digital Legal Entity» (`docs/service-terms.md`).
4.1. Результат услуг подтверждается Актом выполненных работ. Порядок и критерии приёмки, а также фиксация передачи лицензии (onchain) определяются документом «Условия приобретения и обслуживания Digital Legal Entity».
5. Условия обслуживания и возвратов
5.1. Обновления, сопровождение, сроки и порядок возвратов регулируются документом «Условия приобретения и обслуживания Digital Legal Entity» (`docs/service-terms.md`).
5.1. Обновления, сопровождение, сроки и порядок возвратов регулируются документом «Условия приобретения и обслуживания Digital Legal Entity».
6. Коммуникации
6.1. Взаимодействие сторон осуществляется через веб‑приложение, электронную почту и мессенджеры.
@@ -35,8 +40,12 @@
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.
Заказчик: ________________________________________________
ОГРН: ________________________________________________
ИНН: ________________________________________________
КПП: ________________________________________________
Адрес: ________________________________________________
Подписи:
Исполнитель: ______________________ /М.П./

View File

@@ -33,7 +33,7 @@
**Оператор персональных данных:** ООО "ЭРАЙТИ"
**Ответственное лицо:** Тарабанов Александр Викторович (генеральный директор)
**Контактный телефон:** +7 (968) 269-92-64
**Email для обращений:**
**Email для обращений:** 18900@эрайти.рф
**Хостинг:** ООО "Хостланд" (ИНН: 7811142520/781401001) - арендованный VDS сервер
**Сроки хранения персональных данных:**
- **Идентификационные данные:** До отзыва согласия субъектом ПДн

View File

@@ -30,6 +30,14 @@ INSERT INTO roles (id, name_encrypted) VALUES
ON CONFLICT (id) DO UPDATE SET
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 "
INSERT INTO rpc_providers (network_id_encrypted, rpc_url_encrypted, chain_id)
VALUES