mirror of
https://github.com/Tencent/WeKnora.git
synced 2026-09-24 16:29:01 +08:00
feat: Add Graph Settings component for managing graph extraction configurations
This commit is contained in:
@@ -271,6 +271,56 @@ export default {
|
||||
saveFailed: 'Failed to save configuration: {message}'
|
||||
}
|
||||
},
|
||||
graphSettings: {
|
||||
title: 'Knowledge Graph Configuration',
|
||||
description: 'Configure entity-relationship extraction to automatically build knowledge graphs from text',
|
||||
enableLabel: 'Enable Entity-Relationship Extraction',
|
||||
enableDescription: 'Automatically extract entities and relationships from text when enabled',
|
||||
tagsLabel: 'Relationship Types',
|
||||
tagsDescription: 'Define relationship type tags to extract, separated by commas',
|
||||
tagsPlaceholder: 'Enter relationship types, e.g., works_at, colleague, friend',
|
||||
generateRandomTags: 'Generate Random Tags',
|
||||
sampleTextLabel: 'Sample Text',
|
||||
sampleTextDescription: 'Sample text for testing entity-relationship extraction',
|
||||
sampleTextPlaceholder: 'Enter text containing entities and relationships...',
|
||||
generateRandomText: 'Generate Random Text',
|
||||
entityListLabel: 'Entity List',
|
||||
entityListDescription: 'Entities and their attributes extracted from text',
|
||||
nodeNamePlaceholder: 'Enter entity name',
|
||||
attributePlaceholder: 'Enter attribute value',
|
||||
addAttribute: 'Add Attribute',
|
||||
manageEntitiesLabel: 'Manage Entities',
|
||||
manageEntitiesDescription: 'Add or remove entity nodes',
|
||||
addEntity: 'Add Entity',
|
||||
relationListLabel: 'Relationship List',
|
||||
relationListDescription: 'Define relationship connections between entities',
|
||||
selectEntity: 'Select Entity',
|
||||
selectRelationType: 'Select Relationship Type',
|
||||
manageRelationsLabel: 'Manage Relationships',
|
||||
manageRelationsDescription: 'Add or remove relationships between entities',
|
||||
addRelation: 'Add Relationship',
|
||||
extractActionsLabel: 'Extraction Actions',
|
||||
extractActionsDescription: 'Perform entity-relationship extraction or manage sample data',
|
||||
startExtraction: 'Start Extraction',
|
||||
extracting: 'Extracting...',
|
||||
defaultExample: 'Default Example',
|
||||
clearExample: 'Clear Example',
|
||||
completeModelConfig: 'Please complete model configuration first',
|
||||
tagsGenerated: 'Tags generated successfully',
|
||||
tagsGenerateFailed: 'Failed to generate tags',
|
||||
textGenerated: 'Text generated successfully',
|
||||
textGenerateFailed: 'Failed to generate text',
|
||||
pleaseInputText: 'Please enter sample text first',
|
||||
extractSuccess: 'Entity-relationship extraction successful',
|
||||
extractFailed: 'Entity-relationship extraction failed',
|
||||
exampleLoaded: 'Example loaded',
|
||||
exampleCleared: 'Example cleared',
|
||||
saveSuccess: 'Graph configuration saved',
|
||||
saveFailed: 'Failed to save configuration: {message}',
|
||||
errors: {
|
||||
unknown: 'Unknown error',
|
||||
},
|
||||
},
|
||||
initialization: {
|
||||
title: 'Initialization',
|
||||
welcome: 'Welcome to WeKnora',
|
||||
@@ -770,7 +820,8 @@ export default {
|
||||
models: 'Model Configuration',
|
||||
chunking: 'Chunking Settings',
|
||||
advanced: 'Advanced Settings',
|
||||
faq: 'FAQ Settings'
|
||||
faq: 'FAQ Settings',
|
||||
graph: 'Knowledge Graph'
|
||||
},
|
||||
basic: {
|
||||
title: 'Basic Information',
|
||||
@@ -936,7 +987,7 @@ export default {
|
||||
},
|
||||
advanced: {
|
||||
title: 'Advanced Settings',
|
||||
description: 'Configure multimodal and knowledge graph features',
|
||||
description: 'Configure multimodal features',
|
||||
multimodal: {
|
||||
label: 'Multimodal Feature',
|
||||
description: 'Enable understanding of multimodal content such as images and videos',
|
||||
@@ -981,40 +1032,6 @@ export default {
|
||||
pathPrefixDescription: 'Optional prefix for stored file paths',
|
||||
pathPrefixPlaceholder: 'Enter path prefix'
|
||||
}
|
||||
},
|
||||
graph: {
|
||||
label: 'Knowledge Graph Extraction',
|
||||
description: 'Automatically extract entities and relationships to build a knowledge graph',
|
||||
configTitle: 'Graph Configuration',
|
||||
promptLabel: 'Prompt Text',
|
||||
promptDescription: 'Prompt used to guide the model when extracting entities and relationships',
|
||||
promptPlaceholder: 'Enter prompt text',
|
||||
tagsLabel: 'Tags',
|
||||
tagsDescription: 'Predefined entity tags (separate multiple tags with commas)',
|
||||
tagsPlaceholder: 'Enter a tag and press Enter',
|
||||
nodesLabel: 'Node Configuration',
|
||||
nodesDescription: 'Define entity node types and attributes to extract',
|
||||
addNode: 'Add Node',
|
||||
nodeNameLabel: 'Node Name',
|
||||
nodeNamePlaceholder: 'e.g., Person, Organization',
|
||||
nodeChunksLabel: 'Associated Chunks',
|
||||
nodeChunksPlaceholder: 'Enter chunk ID and press Enter',
|
||||
nodeAttributesLabel: 'Node Attributes',
|
||||
nodeAttributesPlaceholder: 'Enter attribute name and press Enter',
|
||||
relationsLabel: 'Relation Configuration',
|
||||
relationsDescription: 'Define relationship types between nodes',
|
||||
addRelation: 'Add Relation',
|
||||
relationNode1Label: 'Source Node',
|
||||
relationNode1Placeholder: 'e.g., Person',
|
||||
relationNode2Label: 'Target Node',
|
||||
relationNode2Placeholder: 'e.g., Organization',
|
||||
relationTypeLabel: 'Relation Type',
|
||||
relationTypePlaceholder: 'e.g., WORKS_FOR, BELONGS_TO',
|
||||
deleteNode: 'Delete Node',
|
||||
deleteRelation: 'Delete Relation',
|
||||
disabledWarning:
|
||||
'Graph database is not enabled. Please follow the "Enable Knowledge Graph" guide before using this feature.',
|
||||
howToEnable: 'View guide'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -270,6 +270,56 @@ export default {
|
||||
saveFailed: 'Не удалось сохранить настройки: {message}'
|
||||
}
|
||||
},
|
||||
graphSettings: {
|
||||
title: 'Настройки графа знаний',
|
||||
description: 'Настройте извлечение сущностей и отношений для автоматического построения графа знаний из текста',
|
||||
enableLabel: 'Включить извлечение сущностей и отношений',
|
||||
enableDescription: 'Автоматически извлекать сущности и отношения из текста при включении',
|
||||
tagsLabel: 'Типы отношений',
|
||||
tagsDescription: 'Определите теги типов отношений для извлечения, разделённые запятыми',
|
||||
tagsPlaceholder: 'Введите типы отношений, например: работает_в, коллега, друг',
|
||||
generateRandomTags: 'Сгенерировать случайные теги',
|
||||
sampleTextLabel: 'Образец текста',
|
||||
sampleTextDescription: 'Образец текста для тестирования извлечения сущностей и отношений',
|
||||
sampleTextPlaceholder: 'Введите текст, содержащий сущности и отношения...',
|
||||
generateRandomText: 'Сгенерировать случайный текст',
|
||||
entityListLabel: 'Список сущностей',
|
||||
entityListDescription: 'Сущности и их атрибуты, извлечённые из текста',
|
||||
nodeNamePlaceholder: 'Введите имя сущности',
|
||||
attributePlaceholder: 'Введите значение атрибута',
|
||||
addAttribute: 'Добавить атрибут',
|
||||
manageEntitiesLabel: 'Управление сущностями',
|
||||
manageEntitiesDescription: 'Добавить или удалить узлы сущностей',
|
||||
addEntity: 'Добавить сущность',
|
||||
relationListLabel: 'Список отношений',
|
||||
relationListDescription: 'Определите связи отношений между сущностями',
|
||||
selectEntity: 'Выберите сущность',
|
||||
selectRelationType: 'Выберите тип отношения',
|
||||
manageRelationsLabel: 'Управление отношениями',
|
||||
manageRelationsDescription: 'Добавить или удалить отношения между сущностями',
|
||||
addRelation: 'Добавить отношение',
|
||||
extractActionsLabel: 'Действия извлечения',
|
||||
extractActionsDescription: 'Выполнить извлечение сущностей и отношений или управлять образцами данных',
|
||||
startExtraction: 'Начать извлечение',
|
||||
extracting: 'Извлечение...',
|
||||
defaultExample: 'Пример по умолчанию',
|
||||
clearExample: 'Очистить пример',
|
||||
completeModelConfig: 'Пожалуйста, сначала завершите настройку модели',
|
||||
tagsGenerated: 'Теги успешно сгенерированы',
|
||||
tagsGenerateFailed: 'Не удалось сгенерировать теги',
|
||||
textGenerated: 'Текст успешно сгенерирован',
|
||||
textGenerateFailed: 'Не удалось сгенерировать текст',
|
||||
pleaseInputText: 'Пожалуйста, сначала введите образец текста',
|
||||
extractSuccess: 'Извлечение сущностей и отношений выполнено успешно',
|
||||
extractFailed: 'Не удалось извлечь сущности и отношения',
|
||||
exampleLoaded: 'Пример загружен',
|
||||
exampleCleared: 'Пример очищен',
|
||||
saveSuccess: 'Настройки графа сохранены',
|
||||
saveFailed: 'Не удалось сохранить настройки: {message}',
|
||||
errors: {
|
||||
unknown: 'Неизвестная ошибка',
|
||||
},
|
||||
},
|
||||
initialization: {
|
||||
title: 'Инициализация',
|
||||
welcome: 'Добро пожаловать в WeKnora',
|
||||
@@ -858,7 +908,8 @@ export default {
|
||||
models: 'Конфигурация моделей',
|
||||
chunking: 'Настройки разбиения',
|
||||
advanced: 'Дополнительные настройки',
|
||||
faq: 'FAQ настройки'
|
||||
faq: 'FAQ настройки',
|
||||
graph: 'Граф знаний'
|
||||
},
|
||||
basic: {
|
||||
title: 'Основная информация',
|
||||
@@ -1021,7 +1072,7 @@ export default {
|
||||
},
|
||||
advanced: {
|
||||
title: 'Расширенные настройки',
|
||||
description: 'Настройте мультимодальные возможности и извлечение знаний',
|
||||
description: 'Настройте мультимодальные возможности',
|
||||
multimodal: {
|
||||
label: 'Мультимодальная функция',
|
||||
description: 'Включите понимание мультимедийного контента, такого как изображения и видео',
|
||||
@@ -1066,19 +1117,6 @@ export default {
|
||||
pathPrefixDescription: 'Необязательный префикс для путей хранения файлов',
|
||||
pathPrefixPlaceholder: 'Введите префикс пути'
|
||||
}
|
||||
},
|
||||
graph: {
|
||||
label: 'Извлечение знаний',
|
||||
description: 'Автоматически извлекайте сущности и связи для построения графа знаний',
|
||||
configTitle: 'Настройки графа',
|
||||
promptLabel: 'Текст подсказки',
|
||||
promptDescription: 'Текст подсказки, который помогает модели извлекать сущности и связи',
|
||||
promptPlaceholder: 'Введите текст подсказки',
|
||||
tagsLabel: 'Теги',
|
||||
tagsDescription: 'Предопределённые теги сущностей (несколько тегов разделяйте запятыми)',
|
||||
tagsPlaceholder: 'Введите тег и нажмите Enter',
|
||||
disabledWarning: 'Графовая база данных не включена. Пожалуйста, сначала включите графовую базу данных, чтобы использовать эту функцию.',
|
||||
howToEnable: 'Как включить'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -373,6 +373,56 @@ export default {
|
||||
saveFailed: "保存配置失败: {message}",
|
||||
},
|
||||
},
|
||||
graphSettings: {
|
||||
title: "知识图谱配置",
|
||||
description: "配置实体关系提取功能,自动从文本中提取实体和关系构建知识图谱",
|
||||
enableLabel: "启用实体关系提取",
|
||||
enableDescription: "开启后将自动从文本中提取实体和关系",
|
||||
tagsLabel: "关系类型",
|
||||
tagsDescription: "定义要提取的关系类型标签,多个标签用逗号分隔",
|
||||
tagsPlaceholder: "输入关系类型,如:工作于、同事、朋友等",
|
||||
generateRandomTags: "生成随机标签",
|
||||
sampleTextLabel: "示例文本",
|
||||
sampleTextDescription: "用于测试实体关系提取的示例文本",
|
||||
sampleTextPlaceholder: "输入一段包含实体和关系的文本...",
|
||||
generateRandomText: "生成随机文本",
|
||||
entityListLabel: "实体列表",
|
||||
entityListDescription: "从文本中提取的实体及其属性",
|
||||
nodeNamePlaceholder: "输入实体名称",
|
||||
attributePlaceholder: "输入属性值",
|
||||
addAttribute: "添加属性",
|
||||
manageEntitiesLabel: "管理实体",
|
||||
manageEntitiesDescription: "添加或删除实体节点",
|
||||
addEntity: "添加实体",
|
||||
relationListLabel: "关系列表",
|
||||
relationListDescription: "定义实体之间的关系连接",
|
||||
selectEntity: "选择实体",
|
||||
selectRelationType: "选择关系类型",
|
||||
manageRelationsLabel: "管理关系",
|
||||
manageRelationsDescription: "添加或删除实体间的关系",
|
||||
addRelation: "添加关系",
|
||||
extractActionsLabel: "提取操作",
|
||||
extractActionsDescription: "执行实体关系提取或管理示例数据",
|
||||
startExtraction: "开始提取",
|
||||
extracting: "提取中...",
|
||||
defaultExample: "默认示例",
|
||||
clearExample: "清除示例",
|
||||
completeModelConfig: "请先完成模型配置",
|
||||
tagsGenerated: "标签生成成功",
|
||||
tagsGenerateFailed: "标签生成失败",
|
||||
textGenerated: "文本生成成功",
|
||||
textGenerateFailed: "文本生成失败",
|
||||
pleaseInputText: "请先输入示例文本",
|
||||
extractSuccess: "实体关系提取成功",
|
||||
extractFailed: "实体关系提取失败",
|
||||
exampleLoaded: "示例已加载",
|
||||
exampleCleared: "示例已清除",
|
||||
saveSuccess: "图谱配置已保存",
|
||||
saveFailed: "保存配置失败: {message}",
|
||||
errors: {
|
||||
unknown: "未知错误",
|
||||
},
|
||||
},
|
||||
initialization: {
|
||||
title: "初始化",
|
||||
welcome: "欢迎使用WeKnora",
|
||||
@@ -1094,6 +1144,7 @@ export default {
|
||||
chunking: "分块设置",
|
||||
advanced: "高级设置",
|
||||
faq: "FAQ 设置",
|
||||
graph: "知识图谱",
|
||||
},
|
||||
basic: {
|
||||
title: "基本信息",
|
||||
@@ -1260,7 +1311,7 @@ export default {
|
||||
},
|
||||
advanced: {
|
||||
title: "高级设置",
|
||||
description: "配置多模态、知识图谱等高级功能",
|
||||
description: "配置多模态等高级功能",
|
||||
multimodal: {
|
||||
label: "多模态功能",
|
||||
description: "启用图片、视频等多模态内容的理解能力",
|
||||
@@ -1307,40 +1358,6 @@ export default {
|
||||
pathPrefixPlaceholder: "请输入路径前缀",
|
||||
},
|
||||
},
|
||||
graph: {
|
||||
label: "知识图谱提取",
|
||||
description: "从文档中自动提取实体和关系,构建知识图谱",
|
||||
configTitle: "图谱配置",
|
||||
promptLabel: "提示文本",
|
||||
promptDescription: "用于引导模型提取实体和关系的提示文本",
|
||||
promptPlaceholder: "请输入提示文本",
|
||||
tagsLabel: "标签",
|
||||
tagsDescription: "预定义的实体标签(多个标签用逗号分隔)",
|
||||
tagsPlaceholder: "输入标签后按回车",
|
||||
nodesLabel: "节点配置",
|
||||
nodesDescription: "定义需要提取的实体节点类型及其属性",
|
||||
addNode: "添加节点",
|
||||
nodeNameLabel: "节点名称",
|
||||
nodeNamePlaceholder: "如:Person、Organization",
|
||||
nodeChunksLabel: "关联文档块",
|
||||
nodeChunksPlaceholder: "输入 chunk ID 后按回车",
|
||||
nodeAttributesLabel: "节点属性",
|
||||
nodeAttributesPlaceholder: "输入属性名后按回车",
|
||||
relationsLabel: "关系配置",
|
||||
relationsDescription: "定义节点之间的关系类型",
|
||||
addRelation: "添加关系",
|
||||
relationNode1Label: "起始节点",
|
||||
relationNode1Placeholder: "如:Person",
|
||||
relationNode2Label: "目标节点",
|
||||
relationNode2Placeholder: "如:Organization",
|
||||
relationTypeLabel: "关系类型",
|
||||
relationTypePlaceholder: "如:WORKS_FOR、BELONGS_TO",
|
||||
deleteNode: "删除节点",
|
||||
deleteRelation: "删除关系",
|
||||
disabledWarning:
|
||||
"图数据库未启用,无法使用知识图谱功能。请先根据《开启知识图谱功能指南》完成配置。",
|
||||
howToEnable: "查看启用说明",
|
||||
},
|
||||
},
|
||||
},
|
||||
input: {
|
||||
|
||||
@@ -128,16 +128,24 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 图谱设置 -->
|
||||
<div v-if="!isFAQ" v-show="currentSection === 'graph'" class="section">
|
||||
<GraphSettings
|
||||
v-if="formData"
|
||||
:graph-extract="formData.extractConfig"
|
||||
:all-models="allModels"
|
||||
@update:graphExtract="handleNodeExtractUpdate"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 高级设置 -->
|
||||
<div v-if="!isFAQ" v-show="currentSection === 'advanced'" class="section">
|
||||
<KBAdvancedSettings
|
||||
ref="advancedSettingsRef"
|
||||
v-if="formData"
|
||||
:multimodal="formData.multimodalConfig"
|
||||
:node-extract="formData.nodeExtractConfig"
|
||||
:all-models="allModels"
|
||||
@update:multimodal="handleMultimodalUpdate"
|
||||
@update:nodeExtract="handleNodeExtractUpdate"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -169,6 +177,7 @@ import { useUIStore } from '@/stores/ui'
|
||||
import KBModelConfig from './settings/KBModelConfig.vue'
|
||||
import KBChunkingSettings from './settings/KBChunkingSettings.vue'
|
||||
import KBAdvancedSettings from './settings/KBAdvancedSettings.vue'
|
||||
import GraphSettings from './settings/GraphSettings.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const uiStore = useUIStore()
|
||||
@@ -204,6 +213,7 @@ const navItems = computed(() => {
|
||||
} else {
|
||||
items.push(
|
||||
{ key: 'chunking', icon: 'file-copy', label: t('knowledgeEditor.sidebar.chunking') },
|
||||
{ key: 'graph', icon: 'chart-bubble', label: t('knowledgeEditor.sidebar.graph') },
|
||||
{ key: 'advanced', icon: 'setting', label: t('knowledgeEditor.sidebar.advanced') }
|
||||
)
|
||||
}
|
||||
@@ -272,13 +282,12 @@ const initFormData = (type: 'document' | 'faq' = 'document') => {
|
||||
pathPrefix: ''
|
||||
}
|
||||
},
|
||||
nodeExtractConfig: {
|
||||
extractConfig: {
|
||||
enabled: false,
|
||||
text: '',
|
||||
tags: [] as string[],
|
||||
nodes: [] as Array<{
|
||||
name: string
|
||||
chunks: string[]
|
||||
attributes: string[]
|
||||
}>,
|
||||
relations: [] as Array<{
|
||||
@@ -286,7 +295,7 @@ const initFormData = (type: 'document' | 'faq' = 'document') => {
|
||||
node2: string
|
||||
type: string
|
||||
}>
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -358,13 +367,16 @@ const loadKBData = async () => {
|
||||
pathPrefix: kb.cos_config?.path_prefix || ''
|
||||
}
|
||||
},
|
||||
nodeExtractConfig: {
|
||||
extractConfig: {
|
||||
enabled: kb.extract_config?.enabled || false,
|
||||
text: kb.extract_config?.text || '',
|
||||
tags: kb.extract_config?.tags || [],
|
||||
nodes: kb.extract_config?.nodes || [],
|
||||
nodes: (kb.extract_config?.nodes || []).map((node: any) => ({
|
||||
name: node.name,
|
||||
attributes: node.attributes || []
|
||||
})),
|
||||
relations: kb.extract_config?.relations || []
|
||||
}
|
||||
},
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load knowledge base data:', error)
|
||||
@@ -396,7 +408,7 @@ const handleMultimodalUpdate = (config: any) => {
|
||||
|
||||
const handleNodeExtractUpdate = (config: any) => {
|
||||
if (formData.value) {
|
||||
formData.value.nodeExtractConfig = { ...config }
|
||||
formData.value.extractConfig = { ...config }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -492,13 +504,13 @@ const buildSubmitData = () => {
|
||||
}
|
||||
|
||||
// 添加知识图谱配置
|
||||
if (formData.value.nodeExtractConfig.enabled) {
|
||||
if (formData.value.extractConfig.enabled) {
|
||||
data.extract_config = {
|
||||
enabled: true,
|
||||
text: formData.value.nodeExtractConfig.text,
|
||||
tags: formData.value.nodeExtractConfig.tags,
|
||||
nodes: formData.value.nodeExtractConfig.nodes,
|
||||
relations: formData.value.nodeExtractConfig.relations
|
||||
text: formData.value.extractConfig.text,
|
||||
tags: formData.value.extractConfig.tags,
|
||||
nodes: formData.value.extractConfig.nodes,
|
||||
relations: formData.value.extractConfig.relations
|
||||
}
|
||||
}
|
||||
|
||||
@@ -584,8 +596,8 @@ const handleSubmit = async () => {
|
||||
enabled: data.extract_config?.enabled || false,
|
||||
text: data.extract_config?.text || '',
|
||||
tags: data.extract_config?.tags || [],
|
||||
nodes: [],
|
||||
relations: []
|
||||
nodes: data.extract_config?.nodes || [],
|
||||
relations: data.extract_config?.relations || []
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,746 @@
|
||||
<template>
|
||||
<div class="graph-settings">
|
||||
<div class="section-header">
|
||||
<h2>{{ t('graphSettings.title') }}</h2>
|
||||
<p class="section-description">{{ t('graphSettings.description') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="settings-group">
|
||||
<!-- 启用实体关系提取 -->
|
||||
<div class="setting-row">
|
||||
<div class="setting-info">
|
||||
<label>{{ t('graphSettings.enableLabel') }}</label>
|
||||
<p class="desc">{{ t('graphSettings.enableDescription') }}</p>
|
||||
</div>
|
||||
<div class="setting-control">
|
||||
<t-switch
|
||||
v-model="localGraphExtract.enabled"
|
||||
@change="handleEnabledChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 关系类型配置 -->
|
||||
<div v-if="localGraphExtract.enabled" class="setting-row vertical">
|
||||
<div class="setting-info">
|
||||
<label>{{ t('graphSettings.tagsLabel') }}</label>
|
||||
<p class="desc">{{ t('graphSettings.tagsDescription') }}</p>
|
||||
</div>
|
||||
<div class="setting-control full-width">
|
||||
<div class="tags-control-group">
|
||||
<t-button
|
||||
theme="default"
|
||||
size="medium"
|
||||
:disabled="!modelStatus.llm.available"
|
||||
:loading="tagFabring"
|
||||
@click="handleFabriTag"
|
||||
class="gen-tags-btn"
|
||||
>
|
||||
{{ t('graphSettings.generateRandomTags') }}
|
||||
</t-button>
|
||||
<t-select
|
||||
v-model="localGraphExtract.tags"
|
||||
multiple
|
||||
:placeholder="t('graphSettings.tagsPlaceholder')"
|
||||
clearable
|
||||
creatable
|
||||
filterable
|
||||
@change="handleTagsChange"
|
||||
style="flex: 1; min-width: 400px;"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="!modelStatus.llm.available" class="control-tip">
|
||||
<t-icon name="info-circle" class="tip-icon" />
|
||||
<span>{{ t('graphSettings.completeModelConfig') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 示例文本 -->
|
||||
<div v-if="localGraphExtract.enabled" class="setting-row vertical">
|
||||
<div class="setting-info">
|
||||
<label>{{ t('graphSettings.sampleTextLabel') }}</label>
|
||||
<p class="desc">{{ t('graphSettings.sampleTextDescription') }}</p>
|
||||
</div>
|
||||
<div class="setting-control full-width">
|
||||
<div class="text-control-group">
|
||||
<t-button
|
||||
theme="default"
|
||||
size="medium"
|
||||
:disabled="!modelStatus.llm.available"
|
||||
:loading="textFabring"
|
||||
@click="handleFabriText"
|
||||
class="gen-text-btn"
|
||||
>
|
||||
{{ t('graphSettings.generateRandomText') }}
|
||||
</t-button>
|
||||
<t-textarea
|
||||
v-model="localGraphExtract.text"
|
||||
:placeholder="t('graphSettings.sampleTextPlaceholder')"
|
||||
:autosize="{ minRows: 6, maxRows: 12 }"
|
||||
show-word-limit
|
||||
maxlength="5000"
|
||||
@change="handleTextChange"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="!modelStatus.llm.available" class="control-tip">
|
||||
<t-icon name="info-circle" class="tip-icon" />
|
||||
<span>{{ t('graphSettings.completeModelConfig') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 实体列表 -->
|
||||
<div v-if="localGraphExtract.enabled && localGraphExtract.nodes.length > 0" class="setting-row vertical">
|
||||
<div class="setting-info">
|
||||
<label>{{ t('graphSettings.entityListLabel') }}</label>
|
||||
<p class="desc">{{ t('graphSettings.entityListDescription') }}</p>
|
||||
</div>
|
||||
<div class="setting-control full-width">
|
||||
<div class="node-list">
|
||||
<div v-for="(node, nodeIndex) in localGraphExtract.nodes" :key="nodeIndex" class="node-item">
|
||||
<div class="node-header">
|
||||
<t-icon name="user" class="node-icon" />
|
||||
<t-input
|
||||
v-model="node.name"
|
||||
:placeholder="t('graphSettings.nodeNamePlaceholder')"
|
||||
@change="handleNodesChange"
|
||||
class="node-name-input"
|
||||
/>
|
||||
<t-button
|
||||
theme="default"
|
||||
size="small"
|
||||
@click="removeNode(nodeIndex)"
|
||||
>
|
||||
<t-icon name="delete" />
|
||||
</t-button>
|
||||
</div>
|
||||
<div class="node-attributes">
|
||||
<div v-for="(attribute, attrIndex) in node.attributes" :key="attrIndex" class="attribute-item">
|
||||
<t-input
|
||||
v-model="node.attributes[attrIndex]"
|
||||
:placeholder="t('graphSettings.attributePlaceholder')"
|
||||
@change="handleNodesChange"
|
||||
class="attribute-input"
|
||||
/>
|
||||
<t-button
|
||||
theme="default"
|
||||
size="small"
|
||||
@click="removeAttribute(nodeIndex, attrIndex)"
|
||||
>
|
||||
<t-icon name="close" />
|
||||
</t-button>
|
||||
</div>
|
||||
<t-button
|
||||
theme="default"
|
||||
size="small"
|
||||
@click="addAttribute(nodeIndex)"
|
||||
class="add-attr-btn"
|
||||
>
|
||||
{{ t('graphSettings.addAttribute') }}
|
||||
</t-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 添加实体按钮 -->
|
||||
<div v-if="localGraphExtract.enabled" class="setting-row">
|
||||
<div class="setting-info">
|
||||
<label>{{ t('graphSettings.manageEntitiesLabel') }}</label>
|
||||
<p class="desc">{{ t('graphSettings.manageEntitiesDescription') }}</p>
|
||||
</div>
|
||||
<div class="setting-control">
|
||||
<t-button
|
||||
theme="primary"
|
||||
@click="addNode"
|
||||
>
|
||||
{{ t('graphSettings.addEntity') }}
|
||||
</t-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 关系列表 -->
|
||||
<div v-if="localGraphExtract.enabled && localGraphExtract.relations.length > 0" class="setting-row vertical">
|
||||
<div class="setting-info">
|
||||
<label>{{ t('graphSettings.relationListLabel') }}</label>
|
||||
<p class="desc">{{ t('graphSettings.relationListDescription') }}</p>
|
||||
</div>
|
||||
<div class="setting-control full-width">
|
||||
<div class="relation-list">
|
||||
<div v-for="(relation, index) in localGraphExtract.relations" :key="index" class="relation-item">
|
||||
<t-select
|
||||
v-model="relation.node1"
|
||||
:placeholder="t('graphSettings.selectEntity')"
|
||||
@change="handleRelationsChange"
|
||||
class="relation-select"
|
||||
>
|
||||
<t-option
|
||||
v-for="node in localGraphExtract.nodes"
|
||||
:key="node.name"
|
||||
:value="node.name"
|
||||
:label="node.name"
|
||||
/>
|
||||
</t-select>
|
||||
<t-icon name="arrow-right" class="relation-arrow" />
|
||||
<t-select
|
||||
v-model="relation.type"
|
||||
:placeholder="t('graphSettings.selectRelationType')"
|
||||
clearable
|
||||
creatable
|
||||
filterable
|
||||
@change="handleRelationsChange"
|
||||
class="relation-select"
|
||||
>
|
||||
<t-option
|
||||
v-for="tag in localGraphExtract.tags"
|
||||
:key="tag"
|
||||
:value="tag"
|
||||
:label="tag"
|
||||
/>
|
||||
</t-select>
|
||||
<t-icon name="arrow-right" class="relation-arrow" />
|
||||
<t-select
|
||||
v-model="relation.node2"
|
||||
:placeholder="t('graphSettings.selectEntity')"
|
||||
@change="handleRelationsChange"
|
||||
class="relation-select"
|
||||
>
|
||||
<t-option
|
||||
v-for="node in localGraphExtract.nodes"
|
||||
:key="node.name"
|
||||
:value="node.name"
|
||||
:label="node.name"
|
||||
/>
|
||||
</t-select>
|
||||
<t-button
|
||||
theme="default"
|
||||
size="small"
|
||||
@click="removeRelation(index)"
|
||||
>
|
||||
<t-icon name="delete" />
|
||||
</t-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 添加关系按钮 -->
|
||||
<div v-if="localGraphExtract.enabled" class="setting-row">
|
||||
<div class="setting-info">
|
||||
<label>{{ t('graphSettings.manageRelationsLabel') }}</label>
|
||||
<p class="desc">{{ t('graphSettings.manageRelationsDescription') }}</p>
|
||||
</div>
|
||||
<div class="setting-control">
|
||||
<t-button
|
||||
theme="primary"
|
||||
@click="addRelation"
|
||||
>
|
||||
{{ t('graphSettings.addRelation') }}
|
||||
</t-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 提取操作按钮 -->
|
||||
<div v-if="localGraphExtract.enabled" class="setting-row">
|
||||
<div class="setting-info">
|
||||
<label>{{ t('graphSettings.extractActionsLabel') }}</label>
|
||||
<p class="desc">{{ t('graphSettings.extractActionsDescription') }}</p>
|
||||
</div>
|
||||
<div class="setting-control">
|
||||
<div class="action-buttons">
|
||||
<t-button
|
||||
theme="primary"
|
||||
:disabled="!modelStatus.llm.available || !localGraphExtract.text"
|
||||
:loading="extracting"
|
||||
@click="handleExtract"
|
||||
>
|
||||
{{ extracting ? t('graphSettings.extracting') : t('graphSettings.startExtraction') }}
|
||||
</t-button>
|
||||
<t-button
|
||||
theme="default"
|
||||
@click="defaultExtractExample"
|
||||
>
|
||||
{{ t('graphSettings.defaultExample') }}
|
||||
</t-button>
|
||||
<t-button
|
||||
theme="default"
|
||||
@click="clearExtractExample"
|
||||
>
|
||||
{{ t('graphSettings.clearExample') }}
|
||||
</t-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted } from 'vue'
|
||||
import { MessagePlugin } from 'tdesign-vue-next'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { extractTextRelations, fabriText, fabriTag, type Node, type Relation, type LLMConfig } from '@/api/initialization'
|
||||
import { listModels } from '@/api/model'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
interface GraphExtractConfig {
|
||||
enabled: boolean
|
||||
text: string
|
||||
tags: string[]
|
||||
nodes: Node[]
|
||||
relations: Relation[]
|
||||
}
|
||||
|
||||
interface Props {
|
||||
graphExtract: GraphExtractConfig
|
||||
allModels?: any[]
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:graphExtract': [value: GraphExtractConfig]
|
||||
}>()
|
||||
|
||||
// 本地状态
|
||||
const localGraphExtract = ref<GraphExtractConfig>({
|
||||
...props.graphExtract,
|
||||
nodes: props.graphExtract.nodes || [],
|
||||
relations: props.graphExtract.relations || []
|
||||
})
|
||||
|
||||
// 加载状态
|
||||
const tagFabring = ref(false)
|
||||
const textFabring = ref(false)
|
||||
const extracting = ref(false)
|
||||
|
||||
// 模型状态
|
||||
const modelStatus = ref({
|
||||
llm: {
|
||||
available: false,
|
||||
config: null as LLMConfig | null
|
||||
}
|
||||
})
|
||||
|
||||
// Watch for prop changes
|
||||
watch(() => props.graphExtract, (newVal) => {
|
||||
localGraphExtract.value = {
|
||||
...newVal,
|
||||
nodes: newVal.nodes || [],
|
||||
relations: newVal.relations || []
|
||||
}
|
||||
}, { deep: true })
|
||||
|
||||
// 处理配置变更
|
||||
const handleConfigChange = () => {
|
||||
emit('update:graphExtract', localGraphExtract.value)
|
||||
}
|
||||
|
||||
// 处理启用/禁用切换
|
||||
const handleEnabledChange = () => {
|
||||
// 当关闭提取功能时,清空所有数据
|
||||
if (!localGraphExtract.value.enabled) {
|
||||
localGraphExtract.value.text = ''
|
||||
localGraphExtract.value.tags = []
|
||||
localGraphExtract.value.nodes = []
|
||||
localGraphExtract.value.relations = []
|
||||
}
|
||||
handleConfigChange()
|
||||
}
|
||||
|
||||
const handleTagsChange = () => {
|
||||
handleConfigChange()
|
||||
}
|
||||
|
||||
const handleTextChange = () => {
|
||||
handleConfigChange()
|
||||
}
|
||||
|
||||
const handleNodesChange = () => {
|
||||
handleConfigChange()
|
||||
}
|
||||
|
||||
const handleRelationsChange = () => {
|
||||
handleConfigChange()
|
||||
}
|
||||
|
||||
// 节点操作
|
||||
const addNode = () => {
|
||||
if (!localGraphExtract.value.nodes) {
|
||||
localGraphExtract.value.nodes = []
|
||||
}
|
||||
localGraphExtract.value.nodes.push({
|
||||
name: '',
|
||||
attributes: []
|
||||
})
|
||||
handleNodesChange()
|
||||
}
|
||||
|
||||
const removeNode = (index: number) => {
|
||||
localGraphExtract.value.nodes.splice(index, 1)
|
||||
handleNodesChange()
|
||||
}
|
||||
|
||||
const addAttribute = (nodeIndex: number) => {
|
||||
localGraphExtract.value.nodes[nodeIndex].attributes.push('')
|
||||
handleNodesChange()
|
||||
}
|
||||
|
||||
const removeAttribute = (nodeIndex: number, attrIndex: number) => {
|
||||
localGraphExtract.value.nodes[nodeIndex].attributes.splice(attrIndex, 1)
|
||||
handleNodesChange()
|
||||
}
|
||||
|
||||
// 关系操作
|
||||
const addRelation = () => {
|
||||
if (!localGraphExtract.value.relations) {
|
||||
localGraphExtract.value.relations = []
|
||||
}
|
||||
localGraphExtract.value.relations.push({
|
||||
node1: '',
|
||||
node2: '',
|
||||
type: ''
|
||||
})
|
||||
handleRelationsChange()
|
||||
}
|
||||
|
||||
const removeRelation = (index: number) => {
|
||||
localGraphExtract.value.relations.splice(index, 1)
|
||||
handleRelationsChange()
|
||||
}
|
||||
|
||||
// 生成随机标签
|
||||
const handleFabriTag = async () => {
|
||||
if (!modelStatus.value.llm.available || !modelStatus.value.llm.config) {
|
||||
MessagePlugin.warning(t('graphSettings.completeModelConfig'))
|
||||
return
|
||||
}
|
||||
|
||||
tagFabring.value = true
|
||||
try {
|
||||
const response = await fabriTag({
|
||||
llmConfig: modelStatus.value.llm.config
|
||||
})
|
||||
localGraphExtract.value.tags = response.tags || []
|
||||
handleTagsChange()
|
||||
MessagePlugin.success(t('graphSettings.tagsGenerated'))
|
||||
} catch (error: any) {
|
||||
console.error('Failed to generate tags:', error)
|
||||
MessagePlugin.error(t('graphSettings.tagsGenerateFailed'))
|
||||
} finally {
|
||||
tagFabring.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 生成随机文本
|
||||
const handleFabriText = async () => {
|
||||
if (!modelStatus.value.llm.available || !modelStatus.value.llm.config) {
|
||||
MessagePlugin.warning(t('graphSettings.completeModelConfig'))
|
||||
return
|
||||
}
|
||||
|
||||
textFabring.value = true
|
||||
try {
|
||||
const response = await fabriText({
|
||||
tags: localGraphExtract.value.tags,
|
||||
llmConfig: modelStatus.value.llm.config
|
||||
})
|
||||
localGraphExtract.value.text = response.text || ''
|
||||
handleTextChange()
|
||||
MessagePlugin.success(t('graphSettings.textGenerated'))
|
||||
} catch (error: any) {
|
||||
console.error('Failed to generate text:', error)
|
||||
MessagePlugin.error(t('graphSettings.textGenerateFailed'))
|
||||
} finally {
|
||||
textFabring.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 提取实体关系
|
||||
const handleExtract = async () => {
|
||||
if (!modelStatus.value.llm.available || !modelStatus.value.llm.config) {
|
||||
MessagePlugin.warning(t('graphSettings.completeModelConfig'))
|
||||
return
|
||||
}
|
||||
|
||||
if (!localGraphExtract.value.text) {
|
||||
MessagePlugin.warning(t('graphSettings.pleaseInputText'))
|
||||
return
|
||||
}
|
||||
|
||||
extracting.value = true
|
||||
try {
|
||||
const response = await extractTextRelations({
|
||||
text: localGraphExtract.value.text,
|
||||
tags: localGraphExtract.value.tags,
|
||||
llmConfig: modelStatus.value.llm.config
|
||||
})
|
||||
localGraphExtract.value.nodes = response.nodes || []
|
||||
localGraphExtract.value.relations = response.relations || []
|
||||
handleNodesChange()
|
||||
MessagePlugin.success(t('graphSettings.extractSuccess'))
|
||||
} catch (error: any) {
|
||||
console.error('Failed to extract relations:', error)
|
||||
MessagePlugin.error(t('graphSettings.extractFailed'))
|
||||
} finally {
|
||||
extracting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 默认示例
|
||||
const defaultExtractExample = () => {
|
||||
localGraphExtract.value.text = `《红楼梦》,又名《石头记》,是清代作家曹雪芹创作的中国古典四大名著之一,被誉为中国封建社会的百科全书。该书前80回由曹雪芹所著,后40回一般认为是高鹗所续。小说以贾、史、王、薛四大家族的兴衰为背景,以贾宝玉、林黛玉和薛宝钗的爱情悲剧为主线,刻画了以贾宝玉和金陵十二钗为中心的正邪两赋、贤愚并出的高度复杂的人物群像。成书于乾隆年间(1743年前后),是中国文学史上现实主义的高峰,对后世影响深远。`
|
||||
localGraphExtract.value.tags = ['作者', '别名']
|
||||
localGraphExtract.value.nodes = [
|
||||
{name: '红楼梦', attributes: ['中国古典四大名著之一', '又名《石头记》', '被誉为中国封建社会的百科全书']},
|
||||
{name: '石头记', attributes: ['《红楼梦》的别名']},
|
||||
{name: '曹雪芹', attributes: ['清代作家', '《红楼梦》前 80 回的作者']},
|
||||
{name: '高鹗', attributes: ['一般认为是《红楼梦》后 40 回的续写者']}
|
||||
]
|
||||
localGraphExtract.value.relations = [
|
||||
{node1: '红楼梦', node2: '石头记', type: '别名'},
|
||||
{node1: '红楼梦', node2: '曹雪芹', type: '作者'},
|
||||
{node1: '红楼梦', node2: '高鹗', type: '作者'}
|
||||
]
|
||||
handleNodesChange()
|
||||
MessagePlugin.success(t('graphSettings.exampleLoaded'))
|
||||
}
|
||||
|
||||
// 清除示例
|
||||
const clearExtractExample = () => {
|
||||
localGraphExtract.value.text = ''
|
||||
localGraphExtract.value.tags = []
|
||||
localGraphExtract.value.nodes = []
|
||||
localGraphExtract.value.relations = []
|
||||
handleNodesChange()
|
||||
MessagePlugin.success(t('graphSettings.exampleCleared'))
|
||||
}
|
||||
|
||||
// 加载模型状态
|
||||
const loadModelStatus = async () => {
|
||||
try {
|
||||
const models = await listModels()
|
||||
|
||||
// 查找可用的KnowledgeQA模型(对话模型)
|
||||
const llmModels = models.filter((m: any) => m.type === 'KnowledgeQA')
|
||||
if (llmModels.length > 0) {
|
||||
const llmModel = llmModels[0]
|
||||
modelStatus.value.llm.available = true
|
||||
modelStatus.value.llm.config = {
|
||||
source: llmModel.source,
|
||||
modelName: llmModel.name,
|
||||
baseUrl: llmModel.parameters?.base_url || '',
|
||||
apiKey: llmModel.parameters?.api_key || ''
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Failed to load model status:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(async () => {
|
||||
await loadModelStatus()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.graph-settings {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
margin-bottom: 32px;
|
||||
|
||||
h2 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #333333;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.section-description {
|
||||
font-size: 14px;
|
||||
color: #666666;
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
.settings-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.setting-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
padding: 20px 0;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&.vertical {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
.setting-control {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.setting-info {
|
||||
flex: 1;
|
||||
max-width: 65%;
|
||||
padding-right: 24px;
|
||||
|
||||
label {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: #333333;
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.desc {
|
||||
font-size: 13px;
|
||||
color: #666666;
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
.setting-control {
|
||||
flex-shrink: 0;
|
||||
min-width: 280px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
|
||||
&.full-width {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.tags-control-group,
|
||||
.text-control-group {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.text-control-group {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.control-tip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
color: #666666;
|
||||
|
||||
.tip-icon {
|
||||
color: #0052d9;
|
||||
}
|
||||
}
|
||||
|
||||
.node-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.node-item {
|
||||
background: #f8fafb;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.node-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.node-icon {
|
||||
font-size: 20px;
|
||||
color: #0052d9;
|
||||
}
|
||||
|
||||
.node-name-input {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.node-attributes {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding-left: 32px;
|
||||
}
|
||||
|
||||
.attribute-item {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
|
||||
.attribute-input {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.add-attr-btn {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.relation-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.relation-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
background: #f8fafb;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
|
||||
.relation-select {
|
||||
flex: 1;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.relation-arrow {
|
||||
color: #666666;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
</style>
|
||||
@@ -210,198 +210,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Knowledge graph extraction -->
|
||||
<div class="setting-row">
|
||||
<div class="setting-info">
|
||||
<label>{{ $t('knowledgeEditor.advanced.graph.label') }}</label>
|
||||
<p class="desc">{{ $t('knowledgeEditor.advanced.graph.description') }}</p>
|
||||
<!-- Warning message when graph database is not enabled -->
|
||||
<t-alert
|
||||
v-if="!isGraphDatabaseEnabled"
|
||||
theme="warning"
|
||||
style="margin-top: 8px;"
|
||||
>
|
||||
<template #message>
|
||||
<div>{{ $t('knowledgeEditor.advanced.graph.disabledWarning') }}</div>
|
||||
<t-link class="graph-guide-link" theme="primary" @click="handleOpenGraphGuide">
|
||||
{{ $t('knowledgeEditor.advanced.graph.howToEnable') }}
|
||||
</t-link>
|
||||
</template>
|
||||
</t-alert>
|
||||
</div>
|
||||
<div class="setting-control">
|
||||
<t-switch
|
||||
v-model="localNodeExtract.enabled"
|
||||
@change="handleNodeExtractToggle"
|
||||
:disabled="!isGraphDatabaseEnabled"
|
||||
size="large"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Knowledge graph configuration -->
|
||||
<div v-if="localNodeExtract.enabled && isGraphDatabaseEnabled" class="subsection">
|
||||
<div class="subsection-header">
|
||||
<h4>{{ $t('knowledgeEditor.advanced.graph.configTitle') }}</h4>
|
||||
</div>
|
||||
|
||||
<div class="setting-row">
|
||||
<div class="setting-info">
|
||||
<label>{{ $t('knowledgeEditor.advanced.graph.promptLabel') }}</label>
|
||||
<p class="desc">{{ $t('knowledgeEditor.advanced.graph.promptDescription') }}</p>
|
||||
</div>
|
||||
<div class="setting-control">
|
||||
<t-textarea
|
||||
v-model="localNodeExtract.text"
|
||||
:placeholder="$t('knowledgeEditor.advanced.graph.promptPlaceholder')"
|
||||
:autosize="{ minRows: 3, maxRows: 6 }"
|
||||
@change="handleConfigChange"
|
||||
style="width: 280px;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-row">
|
||||
<div class="setting-info">
|
||||
<label>{{ $t('knowledgeEditor.advanced.graph.tagsLabel') }}</label>
|
||||
<p class="desc">{{ $t('knowledgeEditor.advanced.graph.tagsDescription') }}</p>
|
||||
</div>
|
||||
<div class="setting-control">
|
||||
<t-tag-input
|
||||
v-model="localNodeExtract.tags"
|
||||
:placeholder="$t('knowledgeEditor.advanced.graph.tagsPlaceholder')"
|
||||
@change="handleConfigChange"
|
||||
style="width: 280px;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Nodes Configuration -->
|
||||
<div class="subsection-header">
|
||||
<h4>{{ $t('knowledgeEditor.advanced.graph.nodesLabel') }}</h4>
|
||||
<p class="subsection-desc">{{ $t('knowledgeEditor.advanced.graph.nodesDescription') }}</p>
|
||||
</div>
|
||||
|
||||
<div v-for="(node, index) in localNodeExtract.nodes" :key="index" class="config-item">
|
||||
<div class="config-item-header">
|
||||
<span class="config-item-title">节点 #{{ index + 1 }}</span>
|
||||
<t-button
|
||||
theme="danger"
|
||||
variant="text"
|
||||
size="small"
|
||||
@click="removeNode(index)"
|
||||
>
|
||||
{{ $t('knowledgeEditor.advanced.graph.deleteNode') }}
|
||||
</t-button>
|
||||
</div>
|
||||
|
||||
<div class="config-item-body">
|
||||
<div class="config-field">
|
||||
<label>{{ $t('knowledgeEditor.advanced.graph.nodeNameLabel') }}</label>
|
||||
<t-input
|
||||
v-model="node.name"
|
||||
:placeholder="$t('knowledgeEditor.advanced.graph.nodeNamePlaceholder')"
|
||||
@change="handleConfigChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="config-field">
|
||||
<label>{{ $t('knowledgeEditor.advanced.graph.nodeChunksLabel') }}</label>
|
||||
<t-tag-input
|
||||
v-model="node.chunks"
|
||||
:placeholder="$t('knowledgeEditor.advanced.graph.nodeChunksPlaceholder')"
|
||||
@change="handleConfigChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="config-field">
|
||||
<label>{{ $t('knowledgeEditor.advanced.graph.nodeAttributesLabel') }}</label>
|
||||
<t-tag-input
|
||||
v-model="node.attributes"
|
||||
:placeholder="$t('knowledgeEditor.advanced.graph.nodeAttributesPlaceholder')"
|
||||
@change="handleConfigChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<t-button
|
||||
theme="primary"
|
||||
variant="outline"
|
||||
@click="addNode"
|
||||
style="margin-top: 12px;"
|
||||
>
|
||||
<template #icon>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path d="M8 2a1 1 0 011 1v4h4a1 1 0 110 2H9v4a1 1 0 11-2 0V9H3a1 1 0 110-2h4V3a1 1 0 011-1z"/>
|
||||
</svg>
|
||||
</template>
|
||||
{{ $t('knowledgeEditor.advanced.graph.addNode') }}
|
||||
</t-button>
|
||||
|
||||
<!-- Relations Configuration -->
|
||||
<div class="subsection-header" style="margin-top: 24px;">
|
||||
<h4>{{ $t('knowledgeEditor.advanced.graph.relationsLabel') }}</h4>
|
||||
<p class="subsection-desc">{{ $t('knowledgeEditor.advanced.graph.relationsDescription') }}</p>
|
||||
</div>
|
||||
|
||||
<div v-for="(relation, index) in localNodeExtract.relations" :key="index" class="config-item">
|
||||
<div class="config-item-header">
|
||||
<span class="config-item-title">关系 #{{ index + 1 }}</span>
|
||||
<t-button
|
||||
theme="danger"
|
||||
variant="text"
|
||||
size="small"
|
||||
@click="removeRelation(index)"
|
||||
>
|
||||
{{ $t('knowledgeEditor.advanced.graph.deleteRelation') }}
|
||||
</t-button>
|
||||
</div>
|
||||
|
||||
<div class="config-item-body">
|
||||
<div class="config-field">
|
||||
<label>{{ $t('knowledgeEditor.advanced.graph.relationNode1Label') }}</label>
|
||||
<t-input
|
||||
v-model="relation.node1"
|
||||
:placeholder="$t('knowledgeEditor.advanced.graph.relationNode1Placeholder')"
|
||||
@change="handleConfigChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="config-field">
|
||||
<label>{{ $t('knowledgeEditor.advanced.graph.relationNode2Label') }}</label>
|
||||
<t-input
|
||||
v-model="relation.node2"
|
||||
:placeholder="$t('knowledgeEditor.advanced.graph.relationNode2Placeholder')"
|
||||
@change="handleConfigChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="config-field">
|
||||
<label>{{ $t('knowledgeEditor.advanced.graph.relationTypeLabel') }}</label>
|
||||
<t-input
|
||||
v-model="relation.type"
|
||||
:placeholder="$t('knowledgeEditor.advanced.graph.relationTypePlaceholder')"
|
||||
@change="handleConfigChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<t-button
|
||||
theme="primary"
|
||||
variant="outline"
|
||||
@click="addRelation"
|
||||
style="margin-top: 12px;"
|
||||
>
|
||||
<template #icon>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path d="M8 2a1 1 0 011 1v4h4a1 1 0 110 2H9v4a1 1 0 11-2 0V9H3a1 1 0 110-2h4V3a1 1 0 011-1z"/>
|
||||
</svg>
|
||||
</template>
|
||||
{{ $t('knowledgeEditor.advanced.graph.addRelation') }}
|
||||
</t-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -433,25 +241,8 @@ interface MultimodalConfig {
|
||||
}
|
||||
}
|
||||
|
||||
interface NodeExtractConfig {
|
||||
enabled: boolean
|
||||
text: string
|
||||
tags: string[]
|
||||
nodes: Array<{
|
||||
name: string
|
||||
chunks: string[]
|
||||
attributes: string[]
|
||||
}>
|
||||
relations: Array<{
|
||||
node1: string
|
||||
node2: string
|
||||
type: string
|
||||
}>
|
||||
}
|
||||
|
||||
interface Props {
|
||||
multimodal: MultimodalConfig
|
||||
nodeExtract: NodeExtractConfig
|
||||
allModels?: any[]
|
||||
}
|
||||
|
||||
@@ -459,18 +250,11 @@ const props = defineProps<Props>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:multimodal': [value: MultimodalConfig]
|
||||
'update:nodeExtract': [value: NodeExtractConfig]
|
||||
}>()
|
||||
|
||||
const localMultimodal = ref<MultimodalConfig>({ ...props.multimodal })
|
||||
const localNodeExtract = ref<NodeExtractConfig>({
|
||||
...props.nodeExtract,
|
||||
nodes: props.nodeExtract.nodes || [],
|
||||
relations: props.nodeExtract.relations || []
|
||||
})
|
||||
|
||||
const vllmSelectorRef = ref()
|
||||
const isGraphDatabaseEnabled = ref(false)
|
||||
const isMinioEnabled = ref(false)
|
||||
|
||||
// Check system status on mount
|
||||
@@ -478,28 +262,6 @@ onMounted(async () => {
|
||||
try {
|
||||
const systemInfo = await getSystemInfo()
|
||||
|
||||
// Check graph database status
|
||||
if (systemInfo.data?.graph_database_engine) {
|
||||
// Check if graph database is enabled
|
||||
// Enabled if it's "Neo4j" or any other non-empty value that's not a disabled indicator
|
||||
const engine = systemInfo.data.graph_database_engine.trim()
|
||||
const disabledIndicators = ['未启用', '未配置', 'Unknown', 'Неизвестно', '']
|
||||
isGraphDatabaseEnabled.value = !disabledIndicators.includes(engine) && engine.length > 0
|
||||
|
||||
// If graph database is disabled, also disable node extract
|
||||
if (!isGraphDatabaseEnabled.value && localNodeExtract.value.enabled) {
|
||||
localNodeExtract.value.enabled = false
|
||||
emit('update:nodeExtract', localNodeExtract.value)
|
||||
}
|
||||
} else {
|
||||
// No graph database engine info, assume disabled
|
||||
isGraphDatabaseEnabled.value = false
|
||||
if (localNodeExtract.value.enabled) {
|
||||
localNodeExtract.value.enabled = false
|
||||
emit('update:nodeExtract', localNodeExtract.value)
|
||||
}
|
||||
}
|
||||
|
||||
// Check MinIO status
|
||||
isMinioEnabled.value = systemInfo.data?.minio_enabled === true
|
||||
|
||||
@@ -511,12 +273,7 @@ onMounted(async () => {
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch system info:', error)
|
||||
// Default to disabled if we can't fetch the info
|
||||
isGraphDatabaseEnabled.value = false
|
||||
isMinioEnabled.value = false
|
||||
if (localNodeExtract.value.enabled) {
|
||||
localNodeExtract.value.enabled = false
|
||||
emit('update:nodeExtract', localNodeExtract.value)
|
||||
}
|
||||
// If MinIO status unknown and storage type is minio, switch to cos
|
||||
if (localMultimodal.value.storageType === 'minio') {
|
||||
localMultimodal.value.storageType = 'cos'
|
||||
@@ -530,14 +287,6 @@ watch(() => props.multimodal, (newVal) => {
|
||||
localMultimodal.value = { ...newVal }
|
||||
}, { deep: true })
|
||||
|
||||
watch(() => props.nodeExtract, (newVal) => {
|
||||
localNodeExtract.value = {
|
||||
...newVal,
|
||||
nodes: newVal.nodes || [],
|
||||
relations: newVal.relations || []
|
||||
}
|
||||
}, { deep: true })
|
||||
|
||||
// Handle multimodal toggle
|
||||
const handleMultimodalToggle = () => {
|
||||
// Reset related configuration when multimodal is disabled
|
||||
@@ -580,67 +329,9 @@ const handleAddModel = (subSection: string) => {
|
||||
uiStore.openSettings('models', subSection)
|
||||
}
|
||||
|
||||
const graphGuideUrl =
|
||||
import.meta.env.VITE_KG_GUIDE_URL ||
|
||||
'https://github.com/Tencent/WeKnora/blob/main/docs/%E5%BC%80%E5%90%AF%E7%9F%A5%E8%AF%86%E5%9B%BE%E8%B0%B1%E5%8A%9F%E8%83%BD.md'
|
||||
|
||||
// Handle knowledge graph toggle
|
||||
const handleNodeExtractToggle = () => {
|
||||
// Prevent enabling if graph database is not enabled
|
||||
if (!isGraphDatabaseEnabled.value) {
|
||||
localNodeExtract.value.enabled = false
|
||||
return
|
||||
}
|
||||
emit('update:nodeExtract', localNodeExtract.value)
|
||||
}
|
||||
|
||||
// Open guide documentation to show how to enable graph database
|
||||
const handleOpenGraphGuide = () => {
|
||||
window.open(graphGuideUrl, '_blank', 'noopener')
|
||||
}
|
||||
|
||||
// Handle configuration change
|
||||
const handleConfigChange = () => {
|
||||
emit('update:multimodal', localMultimodal.value)
|
||||
emit('update:nodeExtract', localNodeExtract.value)
|
||||
}
|
||||
|
||||
// Add a new node
|
||||
const addNode = () => {
|
||||
if (!localNodeExtract.value.nodes) {
|
||||
localNodeExtract.value.nodes = []
|
||||
}
|
||||
localNodeExtract.value.nodes.push({
|
||||
name: '',
|
||||
chunks: [],
|
||||
attributes: []
|
||||
})
|
||||
handleConfigChange()
|
||||
}
|
||||
|
||||
// Remove a node
|
||||
const removeNode = (index: number) => {
|
||||
localNodeExtract.value.nodes.splice(index, 1)
|
||||
handleConfigChange()
|
||||
}
|
||||
|
||||
// Add a new relation
|
||||
const addRelation = () => {
|
||||
if (!localNodeExtract.value.relations) {
|
||||
localNodeExtract.value.relations = []
|
||||
}
|
||||
localNodeExtract.value.relations.push({
|
||||
node1: '',
|
||||
node2: '',
|
||||
type: ''
|
||||
})
|
||||
handleConfigChange()
|
||||
}
|
||||
|
||||
// Remove a relation
|
||||
const removeRelation = (index: number) => {
|
||||
localNodeExtract.value.relations.splice(index, 1)
|
||||
handleConfigChange()
|
||||
}
|
||||
|
||||
// The allModels prop keeps model options in sync
|
||||
@@ -716,11 +407,6 @@ const removeRelation = (index: number) => {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.graph-guide-link {
|
||||
display: inline-block;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.subsection {
|
||||
padding: 16px 20px;
|
||||
margin: 12px 0 0 0;
|
||||
|
||||
@@ -319,13 +319,19 @@ func (h *InitializationHandler) UpdateKBConfig(c *gin.Context) {
|
||||
}
|
||||
|
||||
kb.ExtractConfig = &types.ExtractConfig{
|
||||
Enabled: req.NodeExtract.Enabled,
|
||||
Text: req.NodeExtract.Text,
|
||||
Tags: req.NodeExtract.Tags,
|
||||
Nodes: nodes,
|
||||
Relations: relations,
|
||||
}
|
||||
} else {
|
||||
kb.ExtractConfig = nil
|
||||
kb.ExtractConfig = &types.ExtractConfig{Enabled: false}
|
||||
}
|
||||
if err := validateExtractConfig(kb.ExtractConfig); err != nil {
|
||||
logger.Error(ctx, "Invalid extract configuration", err)
|
||||
c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
// 保存更新后的知识库
|
||||
@@ -1811,13 +1817,7 @@ func (h *InitializationHandler) ExtractTextRelations(c *gin.Context) {
|
||||
result, err := h.extractRelationsFromText(ctx, req.Text, req.Tags, req.LLMConfig)
|
||||
if err != nil {
|
||||
logger.Error(ctx, "文本关系提取失败", err)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"data": gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
},
|
||||
})
|
||||
c.Error(errors.NewInternalServerError("文本关系提取失败: " + err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1885,13 +1885,7 @@ func (h *InitializationHandler) FabriText(c *gin.Context) {
|
||||
result, err := h.fabriText(ctx, req.Tags, req.LLMConfig)
|
||||
if err != nil {
|
||||
logger.Error(ctx, "生成示例文本失败", err)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"data": gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
},
|
||||
})
|
||||
c.Error(errors.NewInternalServerError("生成示例文本失败: " + err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package handler
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/Tencent/WeKnora/internal/errors"
|
||||
"github.com/Tencent/WeKnora/internal/logger"
|
||||
@@ -78,6 +79,11 @@ func (h *KnowledgeBaseHandler) CreateKnowledgeBase(c *gin.Context) {
|
||||
c.Error(errors.NewBadRequestError("Invalid request parameters").WithDetails(err.Error()))
|
||||
return
|
||||
}
|
||||
if err := validateExtractConfig(req.ExtractConfig); err != nil {
|
||||
logger.Error(ctx, "Invalid extract configuration", err)
|
||||
c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Infof(ctx, "Creating knowledge base, name: %s", req.Name)
|
||||
// Create knowledge base using the service
|
||||
@@ -267,3 +273,70 @@ func (h *KnowledgeBaseHandler) CopyKnowledgeBase(c *gin.Context) {
|
||||
"message": "Knowledge base copy successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// validateExtractConfig validates the graph configuration parameters
|
||||
func validateExtractConfig(config *types.ExtractConfig) error {
|
||||
logger.Errorf(context.Background(), "Validating extract configuration: %+v", config)
|
||||
if config == nil {
|
||||
return nil
|
||||
}
|
||||
if !config.Enabled {
|
||||
*config = types.ExtractConfig{Enabled: false}
|
||||
return nil
|
||||
}
|
||||
// Validate text field
|
||||
if config.Text == "" {
|
||||
return errors.NewBadRequestError("text cannot be empty")
|
||||
}
|
||||
|
||||
// Validate tags field
|
||||
if len(config.Tags) == 0 {
|
||||
return errors.NewBadRequestError("tags cannot be empty")
|
||||
}
|
||||
for i, tag := range config.Tags {
|
||||
if tag == "" {
|
||||
return errors.NewBadRequestError("tag cannot be empty at index " + strconv.Itoa(i))
|
||||
}
|
||||
}
|
||||
|
||||
// Validate nodes
|
||||
if len(config.Nodes) == 0 {
|
||||
return errors.NewBadRequestError("nodes cannot be empty")
|
||||
}
|
||||
nodeNames := make(map[string]bool)
|
||||
for i, node := range config.Nodes {
|
||||
if node.Name == "" {
|
||||
return errors.NewBadRequestError("node name cannot be empty at index " + strconv.Itoa(i))
|
||||
}
|
||||
// Check for duplicate node names
|
||||
if nodeNames[node.Name] {
|
||||
return errors.NewBadRequestError("duplicate node name: " + node.Name)
|
||||
}
|
||||
nodeNames[node.Name] = true
|
||||
}
|
||||
|
||||
if len(config.Relations) == 0 {
|
||||
return errors.NewBadRequestError("relations cannot be empty")
|
||||
}
|
||||
// Validate relations
|
||||
for i, relation := range config.Relations {
|
||||
if relation.Node1 == "" {
|
||||
return errors.NewBadRequestError("relation node1 cannot be empty at index " + strconv.Itoa(i))
|
||||
}
|
||||
if relation.Node2 == "" {
|
||||
return errors.NewBadRequestError("relation node2 cannot be empty at index " + strconv.Itoa(i))
|
||||
}
|
||||
if relation.Type == "" {
|
||||
return errors.NewBadRequestError("relation type cannot be empty at index " + strconv.Itoa(i))
|
||||
}
|
||||
// Check if referenced nodes exist
|
||||
if !nodeNames[relation.Node1] {
|
||||
return errors.NewBadRequestError("relation references non-existent node1: " + relation.Node1)
|
||||
}
|
||||
if !nodeNames[relation.Node2] {
|
||||
return errors.NewBadRequestError("relation references non-existent node2: " + relation.Node2)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -197,10 +197,10 @@ func (c *VLMConfig) Scan(value interface{}) error {
|
||||
|
||||
type ExtractConfig struct {
|
||||
Enabled bool `yaml:"enabled" json:"enabled"`
|
||||
Text string `yaml:"text" json:"text"`
|
||||
Tags []string `yaml:"tags" json:"tags"`
|
||||
Nodes []*GraphNode `yaml:"nodes" json:"nodes"`
|
||||
Relations []*GraphRelation `yaml:"relations" json:"relations"`
|
||||
Text string `yaml:"text" json:"text,omitempty"`
|
||||
Tags []string `yaml:"tags" json:"tags,omitempty"`
|
||||
Nodes []*GraphNode `yaml:"nodes" json:"nodes,omitempty"`
|
||||
Relations []*GraphRelation `yaml:"relations" json:"relations,omitempty"`
|
||||
}
|
||||
|
||||
// Value implements the driver.Valuer interface, used to convert ExtractConfig to database value
|
||||
|
||||
Reference in New Issue
Block a user