ваше сообщение коммита

This commit is contained in:
2025-09-24 13:05:20 +03:00
parent de0f8aecf2
commit 76cde4b53d
45 changed files with 2167 additions and 2854 deletions

View File

@@ -101,6 +101,80 @@
{{ em.from_email }}
</option>
</select>
<!-- Настройки RAG поиска -->
<div class="rag-search-settings">
<h3>Настройки RAG поиска</h3>
<!-- Метод поиска -->
<label>Метод поиска</label>
<select v-model="ragSettings.searchMethod">
<option value="semantic">Только семантический поиск</option>
<option value="keyword">Только поиск по ключевым словам</option>
<option value="hybrid">Гибридный поиск</option>
</select>
<!-- Количество результатов -->
<label>Максимальное количество результатов поиска</label>
<input type="number" v-model="ragSettings.maxResults" min="1" max="20" />
<!-- Порог релевантности -->
<label>Порог релевантности ({{ ragSettings.relevanceThreshold }})</label>
<input type="range" v-model="ragSettings.relevanceThreshold"
min="0.01" max="1.0" step="0.01" />
<!-- Настройки извлечения ключевых слов -->
<div class="keyword-settings">
<h4>Извлечение ключевых слов</h4>
<label class="checkbox-label">
<input type="checkbox" v-model="ragSettings.keywordExtraction.enabled" />
Включить извлечение ключевых слов
</label>
<label>Минимальная длина слова</label>
<input type="number" v-model="ragSettings.keywordExtraction.minWordLength"
min="2" max="10" />
<label>Максимальное количество ключевых слов</label>
<input type="number" v-model="ragSettings.keywordExtraction.maxKeywords"
min="5" max="20" />
<label class="checkbox-label">
<input type="checkbox" v-model="ragSettings.keywordExtraction.removeStopWords" />
Удалять стоп-слова
</label>
</div>
<!-- Веса для гибридного поиска -->
<div v-if="ragSettings.searchMethod === 'hybrid'" class="search-weights">
<h4>Веса поиска</h4>
<label>Семантический поиск: {{ ragSettings.searchWeights.semantic }}%</label>
<input type="range" v-model="ragSettings.searchWeights.semantic"
min="0" max="100" />
<label>Поиск по ключевым словам: {{ ragSettings.searchWeights.keyword }}%</label>
<input type="range" v-model="ragSettings.searchWeights.keyword"
min="0" max="100" />
</div>
<!-- Дополнительные настройки -->
<div class="advanced-settings">
<h4>Дополнительные настройки</h4>
<label class="checkbox-label">
<input type="checkbox" v-model="ragSettings.advanced.enableFuzzySearch" />
Нечеткий поиск
</label>
<label class="checkbox-label">
<input type="checkbox" v-model="ragSettings.advanced.enableStemming" />
Стемминг слов
</label>
<label class="checkbox-label">
<input type="checkbox" v-model="ragSettings.advanced.enableSynonyms" />
Поиск синонимов
</label>
</div>
</div>
<div class="actions">
<button type="submit">Сохранить</button>
<button type="button" @click="goBack">Отмена</button>
@@ -143,6 +217,29 @@ const placeholders = ref([]);
const editingPlaceholder = ref(null);
const editingPlaceholderValue = ref('');
// Настройки RAG поиска
const ragSettings = ref({
searchMethod: 'hybrid',
maxResults: 5,
relevanceThreshold: 0.1,
keywordExtraction: {
enabled: true,
minWordLength: 3,
maxKeywords: 10,
removeStopWords: true,
language: 'ru'
},
searchWeights: {
semantic: 70,
keyword: 30
},
advanced: {
enableFuzzySearch: true,
enableStemming: true,
enableSynonyms: false
}
});
async function loadUserTables() {
const { data } = await axios.get('/tables');
userTables.value = Array.isArray(data) ? data : [];
@@ -165,7 +262,14 @@ async function loadSettings() {
}
settings.value = settingsData;
// Загружаем настройки RAG, если они есть
if (data.settings.ragSettings) {
ragSettings.value = { ...ragSettings.value, ...data.settings.ragSettings };
}
console.log('[AiAssistantSettings] Loaded settings:', settings.value);
console.log('[AiAssistantSettings] Loaded RAG settings:', ragSettings.value);
}
}
async function loadTelegramBots() {
@@ -225,7 +329,11 @@ async function saveSettings() {
settingsToSave.selected_rag_tables = [settingsToSave.selected_rag_tables];
}
// Добавляем настройки RAG
settingsToSave.ragSettings = ragSettings.value;
console.log('[AiAssistantSettings] Saving settings:', settingsToSave);
console.log('[AiAssistantSettings] Saving RAG settings:', ragSettings.value);
await axios.put('/settings/ai-assistant', settingsToSave);
goBack();
}
@@ -411,4 +519,63 @@ button[type="button"] {
font-size: 1em;
margin: 0.7em 0;
}
/* Стили для настроек RAG поиска */
.rag-search-settings {
margin: 2rem 0;
padding: 1.5rem;
background: #f8f9fa;
border-radius: 8px;
border: 1px solid #e9ecef;
}
.rag-search-settings h3 {
margin-top: 0;
margin-bottom: 1.5rem;
color: #333;
font-size: 1.2rem;
}
.keyword-settings, .search-weights, .advanced-settings {
margin: 1rem 0;
padding: 1rem;
background: #fff;
border-radius: 6px;
border: 1px solid #dee2e6;
}
.keyword-settings h4, .search-weights h4, .advanced-settings h4 {
margin-top: 0;
margin-bottom: 1rem;
color: #555;
font-size: 1rem;
}
.search-weights input[type="range"] {
width: 100%;
margin: 0.5rem 0;
}
.checkbox-label {
display: flex !important;
align-items: center;
gap: 0.5rem;
margin: 0.5rem 0;
font-weight: normal;
}
.checkbox-label input[type="checkbox"] {
width: auto;
margin: 0;
}
.rag-search-settings input[type="range"] {
width: 100%;
margin: 0.5rem 0;
}
.rag-search-settings input[type="number"] {
width: 100px;
margin-right: 1rem;
}
</style>

View File

@@ -70,6 +70,7 @@
import { ref } from 'vue';
import AIProviderSettings from './AIProviderSettings.vue';
import { useAuthContext } from '@/composables/useAuth';
import { usePermissions } from '@/composables/usePermissions';
import NoAccessModal from '@/components/NoAccessModal.vue';
const showProvider = ref(null);
@@ -80,6 +81,7 @@ const showAiAssistantSettings = ref(false);
const showNoAccessModal = ref(false);
const { isAdmin } = useAuthContext();
const { canManageSettings } = usePermissions();
const providerLabels = {
openai: {
@@ -117,7 +119,7 @@ const providerLabels = {
};
function goTo(path) {
if (!isAdmin.value) {
if (!canManageSettings.value) {
showNoAccessModal.value = true;
return;
}

View File

@@ -13,17 +13,31 @@
<template>
<div class="auth-tokens-settings">
<h4>Токены аутентификации</h4>
<!-- Отображение текущего уровня доступа -->
<div v-if="userAccessLevel && userAccessLevel.hasAccess" class="access-level-info">
<div class="access-level-badge" :class="getLevelClass(userAccessLevel.level)">
<i class="fas fa-shield-alt"></i>
<span>{{ getLevelDescription(userAccessLevel.level) }}</span>
<span class="token-count">({{ userAccessLevel.tokenCount }} токен{{ userAccessLevel.tokenCount === 1 ? '' : userAccessLevel.tokenCount < 5 ? 'а' : 'ов' }})</span>
</div>
<div class="access-level-description">
{{ getAccessLevelDescription(userAccessLevel.level) }}
</div>
</div>
<div v-if="authTokens.length > 0" class="tokens-list">
<div v-for="(token, index) in authTokens" :key="token.address + token.network" class="token-entry">
<span><strong>Название:</strong> {{ token.name }}</span>
<span><strong>Адрес:</strong> {{ token.address }}</span>
<span><strong>Сеть:</strong> {{ getNetworkLabel(token.network) }}</span>
<span><strong>Мин. баланс:</strong> {{ token.minBalance }}</span>
<span><strong>Read-Only:</strong> {{ token.readonlyThreshold || 1 }} токен{{ token.readonlyThreshold === 1 ? '' : 'а' }}</span>
<span><strong>Editor:</strong> {{ token.editorThreshold || 2 }} токен{{ token.editorThreshold === 1 ? '' : token.editorThreshold < 5 ? 'а' : 'ов' }}</span>
<button
class="btn btn-sm"
:class="isAdmin ? 'btn-danger' : 'btn-secondary'"
@click="isAdmin ? removeToken(index) : null"
:disabled="!isAdmin"
:class="canEdit ? 'btn-danger' : 'btn-secondary'"
@click="canEdit ? removeToken(index) : null"
:disabled="!canEdit"
>
Удалить
</button>
@@ -39,7 +53,7 @@
v-model="newToken.name"
class="form-control"
placeholder="test2"
:disabled="!isAdmin"
:disabled="!canEdit"
>
</div>
<div class="form-group">
@@ -49,12 +63,12 @@
v-model="newToken.address"
class="form-control"
placeholder="0x..."
:disabled="!isAdmin"
:disabled="!canEdit"
>
</div>
<div class="form-group">
<label>Сеть:</label>
<select v-model="newToken.network" class="form-control" :disabled="!isAdmin">
<select v-model="newToken.network" class="form-control" :disabled="!canEdit">
<option value="">-- Выберите сеть --</option>
<optgroup v-for="(group, groupIndex) in networkGroups" :key="groupIndex" :label="group.label">
<option v-for="option in group.options" :key="option.value" :value="option.value">
@@ -70,14 +84,43 @@
v-model.number="newToken.minBalance"
class="form-control"
placeholder="0"
:disabled="!isAdmin"
:disabled="!canEdit"
>
</div>
<!-- Настройки прав доступа -->
<div class="access-settings">
<h6>Настройки прав доступа</h6>
<div class="form-group">
<label>Минимум токенов для Read-Only доступа:</label>
<input
type="number"
v-model="newToken.readonlyThreshold"
class="form-control"
placeholder="1"
min="1"
:disabled="!canEdit"
>
<small class="form-text">Количество токенов для получения прав только на чтение</small>
</div>
<div class="form-group">
<label>Минимум токенов для Editor доступа:</label>
<input
type="number"
v-model="newToken.editorThreshold"
class="form-control"
placeholder="2"
min="2"
:disabled="!canEdit"
>
<small class="form-text">Количество токенов для получения прав на редактирование и удаление</small>
</div>
</div>
<button
class="btn"
:class="isAdmin ? 'btn-secondary' : 'btn-secondary'"
@click="isAdmin ? addToken() : null"
:disabled="!isAdmin"
:class="canEdit ? 'btn-primary' : 'btn-secondary'"
@click="canEdit ? addToken() : null"
:disabled="!canEdit"
>
Добавить токен
</button>
@@ -86,36 +129,58 @@
</template>
<script setup>
import { reactive } from 'vue';
import { reactive, computed } from 'vue';
import useBlockchainNetworks from '@/composables/useBlockchainNetworks';
import api from '@/api/axios';
import { useAuthContext } from '@/composables/useAuth';
import { usePermissions } from '@/composables/usePermissions';
import eventBus from '@/utils/eventBus';
const props = defineProps({
authTokens: { type: Array, required: true }
});
const emit = defineEmits(['update']);
const newToken = reactive({ name: '', address: '', network: '', minBalance: 0 });
const newToken = reactive({
name: '',
address: '',
network: '',
minBalance: 0,
readonlyThreshold: 1,
editorThreshold: 2
});
const { networkGroups, networks } = useBlockchainNetworks();
const { isAdmin, checkTokenBalances, address, checkAuth } = useAuthContext();
const { isAdmin, checkTokenBalances, address, checkAuth, userAccessLevel, checkUserAccessLevel } = useAuthContext();
const { canEdit, getLevelClass, getLevelDescription } = usePermissions();
async function addToken() {
if (!newToken.name || !newToken.address || !newToken.network) {
alert('Все поля обязательны');
return;
}
const tokenData = {
name: newToken.name,
address: newToken.address,
network: newToken.network,
minBalance: Number(newToken.minBalance) || 0,
readonlyThreshold: newToken.readonlyThreshold !== null && newToken.readonlyThreshold !== undefined && newToken.readonlyThreshold !== '' ? Number(newToken.readonlyThreshold) : 1,
editorThreshold: newToken.editorThreshold !== null && newToken.editorThreshold !== undefined && newToken.editorThreshold !== '' ? Number(newToken.editorThreshold) : 2
};
console.log('[AuthTokensSettings] Отправляем данные токена:', tokenData);
console.log('[AuthTokensSettings] newToken объект:', newToken);
console.log('[AuthTokensSettings] newToken.readonlyThreshold:', newToken.readonlyThreshold, 'тип:', typeof newToken.readonlyThreshold);
console.log('[AuthTokensSettings] newToken.editorThreshold:', newToken.editorThreshold, 'тип:', typeof newToken.editorThreshold);
try {
await api.post('/settings/auth-token', {
...newToken,
minBalance: Number(newToken.minBalance) || 0
});
await api.post('/settings/auth-token', tokenData);
// После добавления токена перепроверяем баланс пользователя и обновляем состояние аутентификации
try {
if (address.value) {
await checkTokenBalances(address.value);
console.log('[AuthTokensSettings] Баланс токенов перепроверен после добавления');
await checkUserAccessLevel(address.value);
console.log('[AuthTokensSettings] Баланс токенов и уровень доступа перепроверены после добавления');
}
// Обновляем состояние аутентификации чтобы отразить изменения роли
@@ -138,6 +203,8 @@ async function addToken() {
newToken.address = '';
newToken.network = '';
newToken.minBalance = 0;
newToken.readonlyThreshold = 1;
newToken.editorThreshold = 2;
} catch (e) {
alert('Ошибка при добавлении токена: ' + (e.response?.data?.error || e.message));
}
@@ -159,7 +226,8 @@ async function removeToken(index) {
try {
if (address.value) {
await checkTokenBalances(address.value);
console.log('[AuthTokensSettings] Баланс токенов перепроверен после удаления');
await checkUserAccessLevel(address.value);
console.log('[AuthTokensSettings] Баланс токенов и уровень доступа перепроверены после удаления');
}
// Обновляем состояние аутентификации чтобы отразить изменения роли
@@ -188,13 +256,118 @@ function getNetworkLabel(networkId) {
const found = networks.value.find(n => n.value === networkId);
return found ? found.label : networkId;
}
function getAccessLevelDescription(level) {
switch (level) {
case 'readonly':
return 'Можете просматривать данные, но не можете редактировать или удалять';
case 'editor':
return 'Можете просматривать, редактировать и удалять данные';
case 'user':
default:
return 'Базовые права пользователя';
}
}
</script>
<style scoped>
.tokens-list { margin-bottom: 1rem; }
.token-entry { display: flex; gap: 1rem; align-items: center; margin-bottom: 0.5rem; }
.token-entry {
display: flex;
gap: 1rem;
align-items: center;
margin-bottom: 0.5rem;
flex-wrap: wrap;
padding: 0.75rem;
background-color: #f8f9fa;
border-radius: 6px;
border: 1px solid #e9ecef;
}
.token-entry span {
font-size: 0.875rem;
white-space: nowrap;
}
.add-token-form { margin-top: 1rem; }
/* Стили для секции настроек прав доступа */
.access-settings {
margin-top: 1.5rem;
padding: 1rem;
background-color: #f8f9fa;
border-radius: 8px;
border: 1px solid #e9ecef;
}
.access-settings h6 {
margin-bottom: 1rem;
color: #495057;
font-weight: 600;
border-bottom: 1px solid #dee2e6;
padding-bottom: 0.5rem;
}
.form-text {
display: block;
margin-top: 0.25rem;
font-size: 0.875rem;
color: #6c757d;
}
/* Стили для отображения уровня доступа */
.access-level-info {
margin-bottom: 1.5rem;
padding: 1rem;
background-color: #f8f9fa;
border-radius: 8px;
border-left: 4px solid #007bff;
}
.access-level-badge {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
border-radius: 20px;
font-weight: 600;
font-size: 0.9rem;
margin-bottom: 0.5rem;
}
.access-level-badge i {
font-size: 1rem;
}
.access-readonly {
background-color: #fff3cd;
color: #856404;
border: 1px solid #ffeaa7;
}
.access-editor {
background-color: #d1ecf1;
color: #0c5460;
border: 1px solid #bee5eb;
}
.access-user {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.token-count {
font-weight: 400;
opacity: 0.8;
}
.access-level-description {
font-size: 0.85rem;
color: #6c757d;
margin-top: 0.25rem;
}
/* Стили для неактивных кнопок */
.btn[disabled], .btn:disabled {
background: #e0e0e0 !important;

View File

@@ -858,7 +858,7 @@
@click="deploySmartContracts"
type="button"
class="btn btn-primary btn-lg deploy-btn"
:disabled="!isFormValid || !adminTokenCheck.isAdmin || adminTokenCheck.isLoading || showDeployProgress"
:disabled="!isFormValid || !canEdit || adminTokenCheck.isLoading || showDeployProgress"
:title="`isFormValid: ${isFormValid}, isAdmin: ${adminTokenCheck.isAdmin}, isLoading: ${adminTokenCheck.isLoading}, showDeployProgress: ${showDeployProgress}`"
>
<i class="fas fa-cogs"></i>
@@ -941,6 +941,7 @@
import { reactive, ref, computed, onMounted, onUnmounted, watch } from 'vue';
import { useRouter } from 'vue-router';
import { useAuthContext } from '@/composables/useAuth';
import { usePermissions } from '@/composables/usePermissions';
import api from '@/api/axios';
import DeploymentWizard from '@/components/deployment/DeploymentWizard.vue';
@@ -959,6 +960,7 @@ function normalizePrivateKey(raw) {
// Получаем контекст авторизации для адреса кошелька
const { address, isAdmin } = useAuthContext();
const { canEdit } = usePermissions();
// Состояние для проверки админских токенов
const adminTokenCheck = ref({

View File

@@ -33,8 +33,8 @@
</div>
<button
class="btn-primary"
@click="isAdmin ? goToAkashDetails() : null"
:disabled="!isAdmin"
@click="canManageSettings ? goToAkashDetails() : null"
:disabled="!canManageSettings"
>
Подробнее
</button>
@@ -54,8 +54,8 @@
</div>
<button
class="btn-primary"
@click="isAdmin ? goToFluxDetails() : null"
:disabled="!isAdmin"
@click="canManageSettings ? goToFluxDetails() : null"
:disabled="!canManageSettings"
>
Подробнее
</button>
@@ -91,10 +91,12 @@
<script setup>
import { useRouter } from 'vue-router';
import { useAuthContext } from '@/composables/useAuth';
import { usePermissions } from '@/composables/usePermissions';
import NoAccessModal from '@/components/NoAccessModal.vue';
import { ref } from 'vue';
const router = useRouter();
const { isAdmin } = useAuthContext();
const { canManageSettings } = usePermissions();
const goBack = () => router.push('/settings');

View File

@@ -76,6 +76,7 @@ import RpcProvidersSettings from './RpcProvidersSettings.vue';
import AuthTokensSettings from './AuthTokensSettings.vue';
import { useRouter } from 'vue-router';
import { useAuthContext } from '@/composables/useAuth';
import { usePermissions } from '@/composables/usePermissions';
import NoAccessModal from '@/components/NoAccessModal.vue';
import wsClient from '@/utils/websocket';
@@ -88,6 +89,7 @@ const showNoAccessModal = ref(false);
// Получаем контекст авторизации
const { isAdmin } = useAuthContext();
const { canManageSettings } = usePermissions();
// Настройки безопасности
const securitySettings = reactive({
@@ -168,7 +170,9 @@ const loadSettings = async () => {
name: token.name,
address: token.address,
network: token.network,
minBalance: token.min_balance
minBalance: token.min_balance,
readonlyThreshold: token.readonly_threshold || 1,
editorThreshold: token.editor_threshold || 2
}));
}
@@ -331,11 +335,11 @@ provide('networks', networks);
// Функция для обработки клика по кнопке "Подробнее" для RPC провайдеров
const handleRpcDetailsClick = () => {
if (isAdmin.value) {
// Если администратор - показываем детали RPC
if (canManageSettings.value) {
// Если есть права на управление настройками - показываем детали RPC
showRpcSettings.value = !showRpcSettings.value;
} else {
// Если обычный пользователь - показываем модальное окно с ограничением доступа
// Если нет прав - показываем модальное окно с ограничением доступа
showNoAccessModal.value = true;
}
};