7dd6ed47c1
- Introduced chunkingDirty state to track manual adjustments to chunking settings, preventing automatic overrides during strategy changes. - Added WIKI_ONLY_CHUNKING_PRESET for specific configurations when using the wiki-only indexing strategy, ensuring optimal settings in creation mode. - Implemented logic to apply or revert chunking presets based on the current indexing strategy, maintaining user-defined settings in edit mode. - Updated resetState function to clear chunkingDirty state upon modal reset.
1627 lines
54 KiB
Vue
1627 lines
54 KiB
Vue
<template>
|
||
<Teleport to="body">
|
||
<Transition name="modal">
|
||
<div v-if="visible" class="settings-overlay" @click.self="handleClose">
|
||
<div class="settings-modal">
|
||
<!-- 关闭按钮 -->
|
||
<button class="close-btn" @click="handleClose" :aria-label="$t('general.close')">
|
||
<svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
|
||
<path d="M15 5L5 15M5 5L15 15" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||
</svg>
|
||
</button>
|
||
|
||
<div class="settings-container">
|
||
<!-- 左侧导航 -->
|
||
<div class="settings-sidebar">
|
||
<div class="sidebar-header">
|
||
<h2 class="sidebar-title">{{ mode === 'create' ? $t('knowledgeEditor.titleCreate') : $t('knowledgeEditor.titleEdit') }}</h2>
|
||
</div>
|
||
<div class="settings-nav">
|
||
<div
|
||
v-for="(item, index) in navItems"
|
||
:key="index"
|
||
:class="['nav-item', { 'active': currentSection === item.key }]"
|
||
@click="currentSection = item.key"
|
||
>
|
||
<t-icon :name="item.icon" class="nav-icon" />
|
||
<span class="nav-label">{{ item.label }}</span>
|
||
<span v-if="item.badge" class="nav-badge">{{ item.badge }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 右侧内容区域 -->
|
||
<div class="settings-content">
|
||
<div class="content-wrapper">
|
||
<!-- 基本信息 -->
|
||
<div v-show="currentSection === 'basic'" class="section">
|
||
<div v-if="formData" class="section-content">
|
||
<div class="section-header">
|
||
<h3 class="section-title">{{ $t('knowledgeEditor.basic.title') }}</h3>
|
||
<p class="section-desc">{{ $t('knowledgeEditor.basic.description') }}</p>
|
||
</div>
|
||
<div class="section-body">
|
||
<div class="form-item">
|
||
<label class="form-label required">{{ $t('knowledgeEditor.basic.typeLabel') }}</label>
|
||
<t-radio-group
|
||
v-model="formData.type"
|
||
:disabled="mode === 'edit'"
|
||
>
|
||
<t-radio-button value="document">{{ $t('knowledgeEditor.basic.typeDocument') }}</t-radio-button>
|
||
<t-radio-button value="faq">{{ $t('knowledgeEditor.basic.typeFAQ') }}</t-radio-button>
|
||
</t-radio-group>
|
||
<p class="form-tip">{{ $t('knowledgeEditor.basic.typeDescription') }}</p>
|
||
</div>
|
||
|
||
<!-- 索引策略 (紧跟类型选择) -->
|
||
<div v-if="!isFAQ" class="form-item">
|
||
<label class="form-label required">{{ $t('knowledgeEditor.indexing.title') }}</label>
|
||
<p class="form-tip">{{ $t('knowledgeEditor.indexing.description') }}</p>
|
||
<div class="indexing-checks" :class="{ 'is-locked': isIndexingLocked }">
|
||
<div
|
||
class="indexing-check-item"
|
||
:class="{ 'is-checked': formData.indexingStrategy.vectorEnabled, 'is-disabled': isIndexingLocked }"
|
||
@click="toggleVectorIndexing"
|
||
>
|
||
<t-checkbox
|
||
:checked="formData.indexingStrategy.vectorEnabled"
|
||
:disabled="isIndexingLocked"
|
||
class="indexing-check-box"
|
||
>{{ $t('knowledgeEditor.indexing.searchTitle') }}</t-checkbox>
|
||
<p class="indexing-check-desc">{{ $t('knowledgeEditor.indexing.searchDesc') }}</p>
|
||
</div>
|
||
<div
|
||
class="indexing-check-item"
|
||
:class="{ 'is-checked': formData.indexingStrategy.wikiEnabled, 'is-disabled': isIndexingLocked }"
|
||
@click="toggleWikiIndexing"
|
||
>
|
||
<t-checkbox
|
||
:checked="formData.indexingStrategy.wikiEnabled"
|
||
:disabled="isIndexingLocked"
|
||
class="indexing-check-box"
|
||
>
|
||
<span class="indexing-check-title">
|
||
{{ $t('knowledgeEditor.indexing.wikiTitle') }}
|
||
<span class="indexing-new-badge">NEW</span>
|
||
</span>
|
||
</t-checkbox>
|
||
<p class="indexing-check-desc">{{ $t('knowledgeEditor.indexing.wikiDesc') }}</p>
|
||
</div>
|
||
</div>
|
||
<p v-if="isIndexingLocked" class="form-tip locked-tip">
|
||
{{ $t('knowledgeEditor.indexing.lockedTip') }}
|
||
</p>
|
||
</div>
|
||
|
||
<!-- Wiki 提取粒度 (仅当 Wiki 启用时显示) -->
|
||
<div v-if="!isFAQ && formData.indexingStrategy.wikiEnabled" class="form-item">
|
||
<label class="form-label">{{ $t('knowledgeEditor.wiki.extractionGranularityLabel') }}</label>
|
||
<p class="form-tip">{{ $t('knowledgeEditor.wiki.extractionGranularityTip') }}</p>
|
||
<t-radio-group
|
||
:value="resolvedGranularity"
|
||
class="granularity-radio-group"
|
||
@change="handleGranularityChange"
|
||
>
|
||
<t-radio-button value="focused">
|
||
{{ $t('knowledgeEditor.wiki.granularityFocused') }}
|
||
</t-radio-button>
|
||
<t-radio-button value="standard">
|
||
{{ $t('knowledgeEditor.wiki.granularityStandard') }}
|
||
</t-radio-button>
|
||
<t-radio-button value="exhaustive">
|
||
{{ $t('knowledgeEditor.wiki.granularityExhaustive') }}
|
||
</t-radio-button>
|
||
</t-radio-group>
|
||
<p class="form-tip granularity-hint">{{ granularityHint }}</p>
|
||
</div>
|
||
|
||
<div class="form-item">
|
||
<label class="form-label required">{{ $t('knowledgeEditor.basic.nameLabel') }}</label>
|
||
<t-input
|
||
v-model="formData.name"
|
||
:placeholder="$t('knowledgeEditor.basic.namePlaceholder')"
|
||
:maxlength="50"
|
||
/>
|
||
</div>
|
||
<div class="form-item">
|
||
<label class="form-label">{{ $t('knowledgeEditor.basic.descriptionLabel') }}</label>
|
||
<t-textarea
|
||
v-model="formData.description"
|
||
:placeholder="$t('knowledgeEditor.basic.descriptionPlaceholder')"
|
||
:maxlength="200"
|
||
:autosize="{ minRows: 3, maxRows: 6 }"
|
||
/>
|
||
</div>
|
||
|
||
<!-- Wiki 合成模型移至模型配置页 -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 模型配置 -->
|
||
<div v-show="currentSection === 'models'" class="section">
|
||
<KBModelConfig
|
||
ref="modelConfigRef"
|
||
v-if="formData"
|
||
:config="formData.modelConfig"
|
||
:has-files="hasFiles"
|
||
:wiki-enabled="formData.indexingStrategy?.wikiEnabled"
|
||
:rag-enabled="formData.indexingStrategy?.vectorEnabled || formData.indexingStrategy?.keywordEnabled"
|
||
:all-models="allModels"
|
||
@update:config="handleModelConfigUpdate"
|
||
/>
|
||
</div>
|
||
|
||
<!-- FAQ 配置 -->
|
||
<div v-if="isFAQ && formData" v-show="currentSection === 'faq'" class="section">
|
||
<div class="section-content">
|
||
<div class="section-header">
|
||
<h3 class="section-title">{{ $t('knowledgeEditor.faq.title') }}</h3>
|
||
<p class="section-desc">{{ $t('knowledgeEditor.faq.description') }}</p>
|
||
</div>
|
||
<div class="section-body">
|
||
<div class="form-item">
|
||
<label class="form-label required">{{ $t('knowledgeEditor.faq.indexModeLabel') }}</label>
|
||
<t-radio-group
|
||
v-model="formData.faqConfig.indexMode"
|
||
>
|
||
<t-radio-button value="question_only">{{ $t('knowledgeEditor.faq.modes.questionOnly') }}</t-radio-button>
|
||
<t-radio-button value="question_answer">{{ $t('knowledgeEditor.faq.modes.questionAnswer') }}</t-radio-button>
|
||
</t-radio-group>
|
||
<p class="form-tip">{{ $t('knowledgeEditor.faq.indexModeDescription') }}</p>
|
||
</div>
|
||
<div class="form-item">
|
||
<label class="form-label required">{{ $t('knowledgeEditor.faq.questionIndexModeLabel') }}</label>
|
||
<t-radio-group
|
||
v-model="formData.faqConfig.questionIndexMode"
|
||
>
|
||
<t-radio-button value="combined">{{ $t('knowledgeEditor.faq.modes.combined') }}</t-radio-button>
|
||
<t-radio-button value="separate">{{ $t('knowledgeEditor.faq.modes.separate') }}</t-radio-button>
|
||
</t-radio-group>
|
||
<p class="form-tip">{{ $t('knowledgeEditor.faq.questionIndexModeDescription') }}</p>
|
||
</div>
|
||
<div class="faq-guide">
|
||
<p>{{ $t('knowledgeEditor.faq.entryGuide') }}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 解析引擎 -->
|
||
<div v-if="!isFAQ && formData" v-show="currentSection === 'parser'" class="section">
|
||
<KBParserSettings
|
||
:parser-engine-rules="formData.chunkingConfig.parserEngineRules"
|
||
@update:parser-engine-rules="handleParserEngineRulesUpdate"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 存储引擎 -->
|
||
<div v-if="!isFAQ && formData" v-show="currentSection === 'storage'" class="section">
|
||
<KBStorageSettings
|
||
:storage-provider="formData.storageProvider"
|
||
:has-files="mode === 'edit' && hasFiles"
|
||
@update:storage-provider="handleStorageProviderUpdate"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 分块设置 -->
|
||
<div v-if="!isFAQ" v-show="currentSection === 'chunking'" class="section">
|
||
<KBChunkingSettings
|
||
v-if="formData"
|
||
:config="formData.chunkingConfig"
|
||
@update:config="handleChunkingConfigUpdate"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 多模态配置 -->
|
||
<div v-if="!isFAQ" v-show="currentSection === 'multimodal'" class="section">
|
||
<div v-if="formData" class="kb-multimodal-settings">
|
||
<div class="section-header">
|
||
<h2>{{ $t('knowledgeEditor.multimodal.title') }}</h2>
|
||
<p class="section-description">{{ $t('knowledgeEditor.multimodal.description') }}</p>
|
||
</div>
|
||
|
||
<div class="settings-group">
|
||
<!-- 多模态开关 -->
|
||
<div class="setting-row">
|
||
<div class="setting-info">
|
||
<label>{{ $t('knowledgeEditor.advanced.multimodal.label') }}</label>
|
||
<p class="desc">{{ $t('knowledgeEditor.advanced.multimodal.description') }}</p>
|
||
</div>
|
||
<div class="setting-control">
|
||
<t-switch
|
||
v-model="formData.multimodalConfig.enabled"
|
||
@change="handleMultimodalToggle"
|
||
size="medium"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- VLLM 模型选择(多模态启用时) -->
|
||
<div v-if="formData.multimodalConfig.enabled" class="setting-row">
|
||
<div class="setting-info">
|
||
<label>{{ $t('knowledgeEditor.advanced.multimodal.vllmLabel') }} <span class="required">*</span></label>
|
||
<p class="desc">{{ $t('knowledgeEditor.advanced.multimodal.vllmDescription') }}</p>
|
||
</div>
|
||
<div class="setting-control">
|
||
<ModelSelector
|
||
model-type="VLLM"
|
||
:selected-model-id="formData.multimodalConfig.vllmModelId"
|
||
:all-models="allModels"
|
||
@update:selected-model-id="handleMultimodalVLLMChange"
|
||
@add-model="handleAddVLLMModel"
|
||
:placeholder="$t('knowledgeEditor.advanced.multimodal.vllmPlaceholder')"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 音频处理(ASR)设置 -->
|
||
<div v-if="!isFAQ" v-show="currentSection === 'asr'" class="section">
|
||
<div v-if="formData" class="kb-multimodal-settings">
|
||
<div class="section-header">
|
||
<h2>{{ $t('knowledgeEditor.asr.title') }}</h2>
|
||
<p class="section-description">{{ $t('knowledgeEditor.asr.description') }}</p>
|
||
</div>
|
||
|
||
<div class="settings-group">
|
||
<!-- ASR 开关 -->
|
||
<div class="setting-row">
|
||
<div class="setting-info">
|
||
<label>{{ $t('knowledgeEditor.asr.label') }}</label>
|
||
<p class="desc">{{ $t('knowledgeEditor.asr.desc') }}</p>
|
||
</div>
|
||
<div class="setting-control">
|
||
<t-switch
|
||
v-model="formData.asrConfig.enabled"
|
||
size="medium"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ASR 模型选择 -->
|
||
<div v-if="formData.asrConfig.enabled" class="setting-row">
|
||
<div class="setting-info">
|
||
<label>{{ $t('knowledgeEditor.asr.modelLabel') }} <span class="required">*</span></label>
|
||
<p class="desc">{{ $t('knowledgeEditor.asr.modelDescription') }}</p>
|
||
</div>
|
||
<div class="setting-control">
|
||
<ModelSelector
|
||
model-type="ASR"
|
||
:selected-model-id="formData.asrConfig.modelId"
|
||
:all-models="allModels"
|
||
@update:selected-model-id="(val: string) => { if (formData) formData.asrConfig.modelId = val }"
|
||
@add-model="handleAddASRModel"
|
||
:placeholder="$t('knowledgeEditor.asr.modelPlaceholder')"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 知识图谱 -->
|
||
<div v-if="!isFAQ" v-show="currentSection === 'graph'" class="section">
|
||
<GraphSettings
|
||
v-if="formData"
|
||
:graph-extract="formData.nodeExtractConfig"
|
||
:model-id="formData.modelConfig.llmModelId"
|
||
:all-models="allModels"
|
||
@update:graphExtract="handleNodeExtractUpdate"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 高级设置 -->
|
||
<div v-if="!isFAQ" v-show="currentSection === 'advanced'" class="section">
|
||
<KBAdvancedSettings
|
||
ref="advancedSettingsRef"
|
||
v-if="formData"
|
||
:question-generation="formData.questionGenerationConfig"
|
||
:rag-enabled="formData.indexingStrategy?.vectorEnabled || formData.indexingStrategy?.keywordEnabled"
|
||
:all-models="allModels"
|
||
@update:question-generation="handleQuestionGenerationUpdate"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 数据源管理(仅编辑模式) -->
|
||
<div v-if="mode === 'edit' && kbId" v-show="currentSection === 'datasource'" class="section">
|
||
<DataSourceSettings :kb-id="kbId" @count="dsCount = $event" />
|
||
</div>
|
||
|
||
<!-- 共享设置(仅编辑模式) -->
|
||
<div v-if="mode === 'edit' && kbId" v-show="currentSection === 'share'" class="section">
|
||
<KBShareSettings :kb-id="kbId" />
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 保存按钮 -->
|
||
<div class="settings-footer">
|
||
<t-button theme="default" variant="outline" @click="handleClose">
|
||
{{ $t('common.cancel') }}
|
||
</t-button>
|
||
<t-button theme="primary" @click="handleSubmit" :loading="saving">
|
||
{{ mode === 'create' ? $t('knowledgeEditor.buttons.create') : $t('knowledgeEditor.buttons.save') }}
|
||
</t-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Transition>
|
||
</Teleport>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed, watch } from 'vue'
|
||
import { MessagePlugin, DialogPlugin } from 'tdesign-vue-next'
|
||
import { createKnowledgeBase, getKnowledgeBaseById, listKnowledgeFiles, updateKnowledgeBase, rebuildKBIndex } from '@/api/knowledge-base'
|
||
import { updateKBConfig, type KBModelConfigRequest } from '@/api/initialization'
|
||
import { listModels } from '@/api/model'
|
||
import { useUIStore } from '@/stores/ui'
|
||
import { useAuthStore } from '@/stores/auth'
|
||
import KBModelConfig from './settings/KBModelConfig.vue'
|
||
import KBParserSettings from './settings/KBParserSettings.vue'
|
||
import KBStorageSettings from './settings/KBStorageSettings.vue'
|
||
import KBChunkingSettings from './settings/KBChunkingSettings.vue'
|
||
import KBAdvancedSettings from './settings/KBAdvancedSettings.vue'
|
||
import ModelSelector from '@/components/ModelSelector.vue'
|
||
import GraphSettings from './settings/GraphSettings.vue'
|
||
import KBShareSettings from './settings/KBShareSettings.vue'
|
||
import DataSourceSettings from './settings/DataSourceSettings.vue'
|
||
import { useI18n } from 'vue-i18n'
|
||
|
||
const uiStore = useUIStore()
|
||
const authStore = useAuthStore()
|
||
const { t } = useI18n()
|
||
|
||
// Props
|
||
const props = defineProps<{
|
||
visible: boolean
|
||
mode: 'create' | 'edit'
|
||
kbId?: string
|
||
initialType?: 'document' | 'faq'
|
||
}>()
|
||
|
||
// Emits
|
||
const emit = defineEmits<{
|
||
(e: 'update:visible', value: boolean): void
|
||
(e: 'success', kbId: string): void
|
||
}>()
|
||
|
||
const currentSection = ref<string>('basic')
|
||
const saving = ref(false)
|
||
const loading = ref(false)
|
||
const allModels = ref<any[]>([])
|
||
const hasFiles = ref(false)
|
||
const initialStorageProvider = ref<string>('')
|
||
const initialIndexingStrategy = ref<any>(null)
|
||
const dsCount = ref(0)
|
||
// 用户是否在分块设置中手动改过任何值。一旦为 true,就不再根据索引策略自动调整默认分块参数。
|
||
const chunkingDirty = ref(false)
|
||
|
||
// 仅 Wiki 索引模式下的分块预设:更大 chunk、无 overlap、关闭父子分块。
|
||
// 该预设只在「创建模式」下、且用户尚未手动调整分块参数时生效,避免覆盖既有 KB 的配置。
|
||
const WIKI_ONLY_CHUNKING_PRESET = {
|
||
chunkSize: 2048,
|
||
chunkOverlap: 0,
|
||
enableParentChild: false,
|
||
} as const
|
||
|
||
// 非 Wiki-only 场景下回落到的默认值(与 initFormData 保持一致)。
|
||
const DEFAULT_CHUNKING_PRESET = {
|
||
chunkSize: 512,
|
||
chunkOverlap: 100,
|
||
enableParentChild: true,
|
||
} as const
|
||
|
||
const navItems = computed(() => {
|
||
const items: { key: string; icon: string; label: string; badge?: number }[] = [
|
||
{ key: 'basic', icon: 'info-circle', label: t('knowledgeEditor.sidebar.basic') },
|
||
{ key: 'models', icon: 'control-platform', label: t('knowledgeEditor.sidebar.models') }
|
||
]
|
||
if (formData.value?.type === 'faq') {
|
||
items.push({ key: 'faq', icon: 'help-circle', label: t('knowledgeEditor.sidebar.faq') })
|
||
} else {
|
||
items.push(
|
||
{ key: 'parser', icon: 'file-search', label: t('settings.parserEngine') },
|
||
{ key: 'multimodal', icon: 'image', label: t('knowledgeEditor.sidebar.multimodal') },
|
||
{ key: 'asr', icon: 'sound', label: t('knowledgeEditor.sidebar.asr') },
|
||
{ key: 'storage', icon: 'cloud', label: t('knowledgeEditor.sidebar.storage') },
|
||
{ 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') }
|
||
)
|
||
if (props.mode === 'edit' && props.kbId) {
|
||
items.push({ key: 'datasource', icon: 'cloud-download', label: t('knowledgeEditor.sidebar.datasource'), badge: dsCount.value || undefined })
|
||
}
|
||
}
|
||
if (props.mode === 'edit' && props.kbId && !authStore.isLiteMode) {
|
||
items.push({ key: 'share', icon: 'share', label: t('knowledgeEditor.sidebar.share') })
|
||
}
|
||
return items
|
||
})
|
||
|
||
// 模型配置引用
|
||
const modelConfigRef = ref<InstanceType<typeof KBModelConfig>>()
|
||
const advancedSettingsRef = ref<InstanceType<typeof KBAdvancedSettings>>()
|
||
|
||
// 表单数据
|
||
const formData = ref<any>(null)
|
||
const isFAQ = computed(() => formData.value?.type === 'faq')
|
||
|
||
watch(
|
||
() => formData.value?.type,
|
||
(newType, oldType) => {
|
||
if (!formData.value) return
|
||
if (newType === 'faq') {
|
||
if (!formData.value.faqConfig) {
|
||
formData.value.faqConfig = { indexMode: 'question_only', questionIndexMode: 'separate' }
|
||
}
|
||
if (!['basic', 'models', 'faq'].includes(currentSection.value)) {
|
||
currentSection.value = 'faq'
|
||
}
|
||
} else if (oldType === 'faq' && currentSection.value === 'faq') {
|
||
currentSection.value = 'basic'
|
||
}
|
||
}
|
||
)
|
||
|
||
// 初始化表单数据
|
||
const initFormData = (type: 'document' | 'faq' = 'document') => {
|
||
return {
|
||
type,
|
||
name: '',
|
||
description: '',
|
||
faqConfig: {
|
||
indexMode: 'question_only',
|
||
questionIndexMode: 'separate'
|
||
},
|
||
modelConfig: {
|
||
llmModelId: '',
|
||
embeddingModelId: '',
|
||
wikiSynthesisModelId: '',
|
||
},
|
||
chunkingConfig: {
|
||
chunkSize: 512,
|
||
chunkOverlap: 100,
|
||
separators: ['\n\n', '\n', '。', '!', '?', ';', ';'],
|
||
parserEngineRules: undefined as any,
|
||
enableParentChild: true,
|
||
parentChunkSize: 4096,
|
||
childChunkSize: 384
|
||
},
|
||
storageProvider: '' as string,
|
||
multimodalConfig: {
|
||
enabled: false,
|
||
vllmModelId: ''
|
||
},
|
||
asrConfig: {
|
||
enabled: false,
|
||
modelId: '',
|
||
language: ''
|
||
},
|
||
nodeExtractConfig: {
|
||
enabled: false,
|
||
text: '',
|
||
tags: [] as string[],
|
||
nodes: [] as Array<{
|
||
name: string
|
||
attributes: string[]
|
||
}>,
|
||
relations: [] as Array<{
|
||
node1: string
|
||
node2: string
|
||
type: string
|
||
}>
|
||
},
|
||
questionGenerationConfig: {
|
||
enabled: true,
|
||
questionCount: 3
|
||
},
|
||
wikiConfig: {
|
||
synthesisModelId: '',
|
||
maxPagesPerIngest: 0,
|
||
extractionGranularity: 'standard' as 'focused' | 'standard' | 'exhaustive',
|
||
},
|
||
indexingStrategy: {
|
||
vectorEnabled: true,
|
||
keywordEnabled: true,
|
||
wikiEnabled: false,
|
||
graphEnabled: false,
|
||
},
|
||
}
|
||
}
|
||
|
||
// 加载所有模型
|
||
const loadAllModels = async () => {
|
||
try {
|
||
const models = await listModels()
|
||
allModels.value = models || []
|
||
} catch (error) {
|
||
console.error('Failed to load model list:', error)
|
||
MessagePlugin.error(t('knowledgeEditor.messages.loadModelsFailed'))
|
||
allModels.value = []
|
||
}
|
||
}
|
||
|
||
// 加载知识库数据(编辑模式)
|
||
const loadKBData = async () => {
|
||
if (props.mode !== 'edit' || !props.kbId) return
|
||
|
||
loading.value = true
|
||
try {
|
||
const [kbInfo, models, filesResult] = await Promise.all([
|
||
getKnowledgeBaseById(props.kbId),
|
||
loadAllModels(),
|
||
listKnowledgeFiles(props.kbId, { page: 1, page_size: 1 })
|
||
])
|
||
|
||
if (!kbInfo || !kbInfo.data) {
|
||
throw new Error(t('knowledgeEditor.messages.notFound'))
|
||
}
|
||
|
||
const kb = kbInfo.data
|
||
hasFiles.value = (filesResult as any)?.total > 0
|
||
|
||
// 设置表单数据
|
||
const kbType = (kb.type as 'document' | 'faq') || 'document'
|
||
formData.value = {
|
||
type: kbType,
|
||
name: kb.name || '',
|
||
description: kb.description || '',
|
||
faqConfig: {
|
||
indexMode: kb.faq_config?.index_mode || 'question_only',
|
||
questionIndexMode: kb.faq_config?.question_index_mode || 'separate'
|
||
},
|
||
modelConfig: {
|
||
llmModelId: kb.summary_model_id || '',
|
||
embeddingModelId: kb.embedding_model_id || '',
|
||
wikiSynthesisModelId: kb.wiki_config?.synthesis_model_id || ''
|
||
},
|
||
chunkingConfig: {
|
||
chunkSize: kb.chunking_config?.chunk_size || 512,
|
||
chunkOverlap: kb.chunking_config?.chunk_overlap || 100,
|
||
separators: kb.chunking_config?.separators || ['\n\n', '\n', '。', '!', '?', ';', ';'],
|
||
parserEngineRules: kb.chunking_config?.parser_engine_rules || undefined,
|
||
enableParentChild: kb.chunking_config?.enable_parent_child || false,
|
||
parentChunkSize: kb.chunking_config?.parent_chunk_size || 4096,
|
||
childChunkSize: kb.chunking_config?.child_chunk_size || 384
|
||
},
|
||
storageProvider: (kb.storage_provider_config?.provider || kb.storage_config?.provider || 'local') as string,
|
||
multimodalConfig: {
|
||
enabled: !!kb.vlm_config?.enabled,
|
||
vllmModelId: kb.vlm_config?.model_id || ''
|
||
},
|
||
asrConfig: {
|
||
enabled: !!kb.asr_config?.enabled,
|
||
modelId: kb.asr_config?.model_id || '',
|
||
language: kb.asr_config?.language || ''
|
||
},
|
||
nodeExtractConfig: {
|
||
enabled: kb.extract_config?.enabled || false,
|
||
text: kb.extract_config?.text || '',
|
||
tags: kb.extract_config?.tags || [],
|
||
nodes: (kb.extract_config?.nodes || []).map((node: any) => ({
|
||
name: node.name,
|
||
attributes: node.attributes || []
|
||
})),
|
||
relations: kb.extract_config?.relations || []
|
||
},
|
||
questionGenerationConfig: {
|
||
enabled: kb.question_generation_config?.enabled || false,
|
||
questionCount: kb.question_generation_config?.question_count || 3
|
||
},
|
||
wikiConfig: {
|
||
synthesisModelId: kb.wiki_config?.synthesis_model_id || '',
|
||
maxPagesPerIngest: kb.wiki_config?.max_pages_per_ingest || 0,
|
||
extractionGranularity: (
|
||
kb.wiki_config?.extraction_granularity === 'focused' ||
|
||
kb.wiki_config?.extraction_granularity === 'exhaustive'
|
||
? kb.wiki_config.extraction_granularity
|
||
: 'standard'
|
||
) as 'focused' | 'standard' | 'exhaustive',
|
||
},
|
||
indexingStrategy: {
|
||
vectorEnabled: kb.indexing_strategy?.vector_enabled ?? true,
|
||
keywordEnabled: kb.indexing_strategy?.keyword_enabled ?? true,
|
||
wikiEnabled: kb.indexing_strategy?.wiki_enabled ?? false,
|
||
graphEnabled: kb.indexing_strategy?.graph_enabled ?? false,
|
||
},
|
||
}
|
||
initialStorageProvider.value = formData.value.storageProvider
|
||
initialIndexingStrategy.value = { ...formData.value.indexingStrategy }
|
||
} catch (error) {
|
||
console.error('Failed to load knowledge base data:', error)
|
||
MessagePlugin.error(t('knowledgeEditor.messages.loadDataFailed'))
|
||
handleClose()
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
// 处理配置更新
|
||
const handleModelConfigUpdate = (config: any) => {
|
||
if (formData.value) {
|
||
formData.value.modelConfig = { ...config }
|
||
}
|
||
}
|
||
|
||
// 粒度选择器:从 formData.wikiConfig 读出并规范化,未知值回退到 'standard',
|
||
// 与后端 WikiExtractionGranularity.Normalize() 的契约保持一致。
|
||
const resolvedGranularity = computed<'focused' | 'standard' | 'exhaustive'>(() => {
|
||
const g = formData.value?.wikiConfig?.extractionGranularity
|
||
if (g === 'focused' || g === 'standard' || g === 'exhaustive') {
|
||
return g
|
||
}
|
||
return 'standard'
|
||
})
|
||
|
||
const granularityHint = computed<string>(() => {
|
||
switch (resolvedGranularity.value) {
|
||
case 'focused':
|
||
return t('knowledgeEditor.wiki.granularityFocusedHint')
|
||
case 'exhaustive':
|
||
return t('knowledgeEditor.wiki.granularityExhaustiveHint')
|
||
default:
|
||
return t('knowledgeEditor.wiki.granularityStandardHint')
|
||
}
|
||
})
|
||
|
||
const handleGranularityChange = (value: string | number | boolean) => {
|
||
if (!formData.value) return
|
||
const next: 'focused' | 'standard' | 'exhaustive' =
|
||
value === 'focused' || value === 'exhaustive'
|
||
? (value as 'focused' | 'exhaustive')
|
||
: 'standard'
|
||
formData.value.wikiConfig = {
|
||
...formData.value.wikiConfig,
|
||
extractionGranularity: next,
|
||
}
|
||
}
|
||
|
||
const isIndexingLocked = computed(() => props.mode === 'edit' && hasFiles.value)
|
||
|
||
const toggleVectorIndexing = () => {
|
||
if (!formData.value) return
|
||
if (isIndexingLocked.value) return
|
||
const next = !formData.value.indexingStrategy.vectorEnabled
|
||
formData.value.indexingStrategy.vectorEnabled = next
|
||
formData.value.indexingStrategy.keywordEnabled = next
|
||
}
|
||
|
||
const toggleWikiIndexing = () => {
|
||
if (!formData.value) return
|
||
if (isIndexingLocked.value) return
|
||
formData.value.indexingStrategy.wikiEnabled = !formData.value.indexingStrategy.wikiEnabled
|
||
}
|
||
|
||
const handleChunkingConfigUpdate = (config: any) => {
|
||
if (formData.value) {
|
||
formData.value.chunkingConfig = { ...config }
|
||
// 用户已经手动触达分块设置,后续索引策略切换不再覆盖这些值
|
||
chunkingDirty.value = true
|
||
}
|
||
}
|
||
|
||
// 判断当前是否为「仅 Wiki 索引」:只开了 Wiki,关了向量/关键词检索
|
||
const isWikiOnlyStrategy = computed(() => {
|
||
const s = formData.value?.indexingStrategy
|
||
if (!s) return false
|
||
return !!s.wikiEnabled && !s.vectorEnabled && !s.keywordEnabled
|
||
})
|
||
|
||
// 仅在创建模式、用户未改过分块设置时,随索引策略自动应用/撤销 Wiki-only 预设。
|
||
// 编辑模式严格保持后端已有配置不变,避免误改。
|
||
watch(isWikiOnlyStrategy, (wikiOnly) => {
|
||
if (props.mode !== 'create') return
|
||
if (!formData.value) return
|
||
if (chunkingDirty.value) return
|
||
const preset = wikiOnly ? WIKI_ONLY_CHUNKING_PRESET : DEFAULT_CHUNKING_PRESET
|
||
formData.value.chunkingConfig = {
|
||
...formData.value.chunkingConfig,
|
||
...preset,
|
||
}
|
||
})
|
||
|
||
const handleParserEngineRulesUpdate = (rules: any[]) => {
|
||
if (formData.value) {
|
||
formData.value.chunkingConfig.parserEngineRules = rules?.length ? rules : undefined
|
||
}
|
||
}
|
||
|
||
const handleMultimodalToggle = () => {
|
||
if (formData.value && !formData.value.multimodalConfig.enabled) {
|
||
formData.value.multimodalConfig.vllmModelId = ''
|
||
}
|
||
}
|
||
|
||
const handleMultimodalVLLMChange = (modelId: string) => {
|
||
if (formData.value) {
|
||
formData.value.multimodalConfig.vllmModelId = modelId
|
||
}
|
||
}
|
||
|
||
const handleAddVLLMModel = () => {
|
||
uiStore.openSettings('models', 'vllm')
|
||
}
|
||
|
||
const handleAddASRModel = () => {
|
||
uiStore.openSettings('models', 'asr')
|
||
}
|
||
|
||
const handleAddWikiModel = () => {
|
||
uiStore.openSettings('models', 'knowledgeqa')
|
||
}
|
||
|
||
const handleStorageProviderUpdate = (value: string) => {
|
||
if (formData.value) {
|
||
formData.value.storageProvider = value || 'local'
|
||
}
|
||
}
|
||
|
||
const handleQuestionGenerationUpdate = (config: any) => {
|
||
if (formData.value) {
|
||
formData.value.questionGenerationConfig = { ...config }
|
||
}
|
||
}
|
||
|
||
const handleNodeExtractUpdate = (config: any) => {
|
||
if (formData.value) {
|
||
formData.value.nodeExtractConfig = { ...config }
|
||
}
|
||
}
|
||
|
||
// 验证表单
|
||
const validateForm = (): boolean => {
|
||
if (!formData.value) return false
|
||
|
||
// 验证基本信息
|
||
if (!formData.value.name || !formData.value.name.trim()) {
|
||
MessagePlugin.warning(t('knowledgeEditor.messages.nameRequired'))
|
||
currentSection.value = 'basic'
|
||
return false
|
||
}
|
||
|
||
// 验证索引策略 — 文档类型至少需要开启一种
|
||
if (formData.value.type !== 'faq') {
|
||
const s = formData.value.indexingStrategy
|
||
if (s && !s.vectorEnabled && !s.keywordEnabled && !s.wikiEnabled && !s.graphEnabled) {
|
||
MessagePlugin.warning(t('knowledgeEditor.indexing.atLeastOne'))
|
||
currentSection.value = 'basic'
|
||
return false
|
||
}
|
||
}
|
||
|
||
// 验证模型配置 - embedding 模型仅在检索索引启用时必须
|
||
const needsEmbedding = formData.value.indexingStrategy?.vectorEnabled || formData.value.indexingStrategy?.keywordEnabled
|
||
if (needsEmbedding && !formData.value.modelConfig.embeddingModelId) {
|
||
MessagePlugin.warning(t('knowledgeEditor.indexing.embeddingRequired'))
|
||
currentSection.value = 'models'
|
||
return false
|
||
}
|
||
|
||
if (!formData.value.modelConfig.llmModelId) {
|
||
MessagePlugin.warning(t('knowledgeEditor.messages.summaryRequired'))
|
||
currentSection.value = 'models'
|
||
return false
|
||
}
|
||
|
||
// 验证多模态配置(如果启用)
|
||
if (formData.value.multimodalConfig.enabled && !formData.value.multimodalConfig.vllmModelId) {
|
||
MessagePlugin.warning(t('knowledgeEditor.messages.multimodalInvalid'))
|
||
currentSection.value = 'multimodal'
|
||
return false
|
||
}
|
||
|
||
if (formData.value.type === 'faq' && !formData.value.faqConfig?.indexMode) {
|
||
MessagePlugin.warning(t('knowledgeEditor.messages.indexModeRequired'))
|
||
currentSection.value = 'faq'
|
||
return false
|
||
}
|
||
|
||
return true
|
||
}
|
||
|
||
// 构建提交数据
|
||
const buildSubmitData = () => {
|
||
if (!formData.value) return null
|
||
|
||
const data: any = {
|
||
name: formData.value.name,
|
||
description: formData.value.description,
|
||
type: formData.value.type,
|
||
chunking_config: {
|
||
chunk_size: formData.value.chunkingConfig.chunkSize,
|
||
chunk_overlap: formData.value.chunkingConfig.chunkOverlap,
|
||
separators: formData.value.chunkingConfig.separators,
|
||
enable_multimodal: formData.value.multimodalConfig.enabled,
|
||
enable_parent_child: formData.value.chunkingConfig.enableParentChild,
|
||
parent_chunk_size: formData.value.chunkingConfig.parentChunkSize,
|
||
child_chunk_size: formData.value.chunkingConfig.childChunkSize,
|
||
...(formData.value.chunkingConfig.parserEngineRules?.length
|
||
? { parser_engine_rules: formData.value.chunkingConfig.parserEngineRules }
|
||
: {})
|
||
},
|
||
embedding_model_id: formData.value.modelConfig.embeddingModelId,
|
||
summary_model_id: formData.value.modelConfig.llmModelId
|
||
}
|
||
|
||
// 添加多模态配置
|
||
data.vlm_config = {
|
||
enabled: formData.value.multimodalConfig.enabled,
|
||
model_id: formData.value.multimodalConfig.enabled
|
||
? (formData.value.multimodalConfig.vllmModelId || '')
|
||
: ''
|
||
}
|
||
|
||
// 添加ASR语音识别配置
|
||
data.asr_config = {
|
||
enabled: formData.value.asrConfig?.enabled || false,
|
||
model_id: formData.value.asrConfig?.enabled
|
||
? (formData.value.asrConfig?.modelId || '')
|
||
: '',
|
||
language: formData.value.asrConfig?.language || ''
|
||
}
|
||
|
||
// 存储引擎:仅传 provider,参数从全局设置读取
|
||
// Write to storage_provider_config (authoritative) + storage_config (legacy dual-write)
|
||
data.storage_provider_config = {
|
||
provider: formData.value.storageProvider || 'local'
|
||
}
|
||
data.storage_config = {
|
||
provider: formData.value.storageProvider || 'local'
|
||
}
|
||
|
||
// 添加知识图谱配置 — now synced via indexingStrategy.graphEnabled
|
||
// extract_config is sent below along with indexing_strategy
|
||
|
||
// 添加问题生成配置
|
||
if (formData.value.questionGenerationConfig?.enabled) {
|
||
data.question_generation_config = {
|
||
enabled: true,
|
||
question_count: formData.value.questionGenerationConfig.questionCount || 3
|
||
}
|
||
}
|
||
|
||
if (formData.value.type === 'faq') {
|
||
data.faq_config = {
|
||
index_mode: formData.value.faqConfig?.indexMode || 'question_only',
|
||
question_index_mode: formData.value.faqConfig?.questionIndexMode || 'separate'
|
||
}
|
||
}
|
||
|
||
// Wiki enablement is carried solely by indexing_strategy.wiki_enabled.
|
||
// wiki_config only holds wiki-specific tunables.
|
||
if (formData.value.type !== 'faq') {
|
||
data.wiki_config = {
|
||
synthesis_model_id: formData.value.modelConfig?.wikiSynthesisModelId || '',
|
||
max_pages_per_ingest: formData.value.wikiConfig?.maxPagesPerIngest || 0,
|
||
extraction_granularity: formData.value.wikiConfig?.extractionGranularity || 'standard',
|
||
}
|
||
}
|
||
|
||
// Send indexing strategy
|
||
if (formData.value.type !== 'faq') {
|
||
data.indexing_strategy = {
|
||
vector_enabled: formData.value.indexingStrategy?.vectorEnabled ?? true,
|
||
keyword_enabled: formData.value.indexingStrategy?.keywordEnabled ?? true,
|
||
wiki_enabled: formData.value.indexingStrategy?.wikiEnabled ?? false,
|
||
graph_enabled: formData.value.indexingStrategy?.graphEnabled ?? false,
|
||
}
|
||
}
|
||
|
||
// Sync extract_config.enabled from indexingStrategy.graphEnabled
|
||
if (formData.value.indexingStrategy?.graphEnabled && formData.value.nodeExtractConfig?.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
|
||
}
|
||
}
|
||
|
||
return data
|
||
}
|
||
|
||
// 提交表单
|
||
const handleSubmit = async () => {
|
||
if (!validateForm()) {
|
||
return
|
||
}
|
||
|
||
// 编辑模式下,若已有文件且存储引擎发生了变化,弹窗确认
|
||
if (
|
||
props.mode === 'edit' &&
|
||
hasFiles.value &&
|
||
formData.value &&
|
||
initialStorageProvider.value &&
|
||
formData.value.storageProvider !== initialStorageProvider.value
|
||
) {
|
||
const dialog = DialogPlugin.confirm({
|
||
header: t('common.confirm'),
|
||
body: t('knowledgeEditor.messages.storageChangeConfirm'),
|
||
confirmBtn: t('common.confirm'),
|
||
cancelBtn: t('common.cancel'),
|
||
onConfirm: () => {
|
||
dialog.destroy()
|
||
doSubmit()
|
||
},
|
||
onCancel: () => {
|
||
dialog.destroy()
|
||
},
|
||
})
|
||
return
|
||
}
|
||
|
||
doSubmit()
|
||
}
|
||
|
||
const doSubmit = async () => {
|
||
saving.value = true
|
||
try {
|
||
const data = buildSubmitData()
|
||
if (!data) {
|
||
throw new Error(t('knowledgeEditor.messages.buildDataFailed'))
|
||
}
|
||
|
||
if (props.mode === 'create') {
|
||
// 创建模式:一次性创建知识库及所有配置
|
||
const result: any = await createKnowledgeBase(data)
|
||
if (!result.success || !result.data?.id) {
|
||
throw new Error(result.message || t('knowledgeEditor.messages.createFailed'))
|
||
}
|
||
MessagePlugin.success(t('knowledgeEditor.messages.createSuccess'))
|
||
emit('success', result.data.id)
|
||
} else {
|
||
// 编辑模式:分别更新基本信息和配置
|
||
if (!props.kbId) {
|
||
throw new Error(t('knowledgeEditor.messages.missingId'))
|
||
}
|
||
|
||
// 1. 更新基本信息(名称、描述)和 FAQ/Wiki 配置
|
||
const updateConfig: any = {}
|
||
if (formData.value.type === 'faq' && formData.value.faqConfig) {
|
||
updateConfig.faq_config = {
|
||
index_mode: formData.value.faqConfig.indexMode || 'question_only',
|
||
question_index_mode: formData.value.faqConfig.questionIndexMode || 'separate'
|
||
}
|
||
}
|
||
if (formData.value.wikiConfig && formData.value.type !== 'faq') {
|
||
updateConfig.wiki_config = {
|
||
synthesis_model_id: formData.value.modelConfig?.wikiSynthesisModelId || '',
|
||
max_pages_per_ingest: formData.value.wikiConfig.maxPagesPerIngest || 0,
|
||
extraction_granularity: formData.value.wikiConfig.extractionGranularity || 'standard',
|
||
}
|
||
}
|
||
if (formData.value.type !== 'faq') {
|
||
updateConfig.indexing_strategy = {
|
||
vector_enabled: formData.value.indexingStrategy?.vectorEnabled ?? true,
|
||
keyword_enabled: formData.value.indexingStrategy?.keywordEnabled ?? true,
|
||
wiki_enabled: formData.value.indexingStrategy?.wikiEnabled ?? false,
|
||
graph_enabled: formData.value.indexingStrategy?.graphEnabled ?? false,
|
||
}
|
||
}
|
||
await updateKnowledgeBase(props.kbId, {
|
||
name: data.name,
|
||
description: data.description,
|
||
config: updateConfig
|
||
})
|
||
|
||
// 2. 更新完整配置(模型、分块、多模态、存储引擎、知识图谱等)
|
||
const config: KBModelConfigRequest = {
|
||
llmModelId: data.summary_model_id,
|
||
embeddingModelId: data.embedding_model_id,
|
||
vlm_config: data.vlm_config,
|
||
asr_config: data.asr_config,
|
||
documentSplitting: {
|
||
chunkSize: data.chunking_config.chunk_size,
|
||
chunkOverlap: data.chunking_config.chunk_overlap,
|
||
separators: data.chunking_config.separators,
|
||
parserEngineRules: data.chunking_config.parser_engine_rules || undefined,
|
||
enableParentChild: data.chunking_config.enable_parent_child || false,
|
||
parentChunkSize: data.chunking_config.parent_chunk_size || 4096,
|
||
childChunkSize: data.chunking_config.child_chunk_size || 384
|
||
},
|
||
multimodal: {
|
||
enabled: !!data.vlm_config?.enabled
|
||
},
|
||
storageProvider: data.storage_provider_config?.provider || data.storage_config?.provider || 'local',
|
||
nodeExtract: {
|
||
enabled: data.extract_config?.enabled || false,
|
||
text: data.extract_config?.text || '',
|
||
tags: data.extract_config?.tags || [],
|
||
nodes: data.extract_config?.nodes || [],
|
||
relations: data.extract_config?.relations || []
|
||
},
|
||
questionGeneration: {
|
||
enabled: data.question_generation_config?.enabled || false,
|
||
questionCount: data.question_generation_config?.question_count || 3
|
||
}
|
||
}
|
||
|
||
await updateKBConfig(props.kbId, config)
|
||
MessagePlugin.success(t('knowledgeEditor.messages.updateSuccess'))
|
||
|
||
// Check if indexing strategy changed and offer rebuild
|
||
if (hasFiles.value && initialIndexingStrategy.value && formData.value) {
|
||
const curr = formData.value.indexingStrategy
|
||
const prev = initialIndexingStrategy.value
|
||
const strategyChanged = (
|
||
curr.vectorEnabled !== prev.vectorEnabled ||
|
||
curr.keywordEnabled !== prev.keywordEnabled ||
|
||
curr.wikiEnabled !== prev.wikiEnabled ||
|
||
curr.graphEnabled !== prev.graphEnabled
|
||
)
|
||
if (strategyChanged) {
|
||
const dialog = DialogPlugin.confirm({
|
||
header: t('knowledgeEditor.indexing.rebuildConfirmTitle'),
|
||
body: t('knowledgeEditor.indexing.rebuildConfirmBody', { count: '...' }),
|
||
confirmBtn: t('common.confirm'),
|
||
cancelBtn: t('common.cancel'),
|
||
onConfirm: async () => {
|
||
dialog.destroy()
|
||
try {
|
||
const result: any = await rebuildKBIndex(props.kbId!)
|
||
const count = result?.data?.document_count ?? 0
|
||
MessagePlugin.success(t('knowledgeEditor.indexing.rebuildSuccess', { count }))
|
||
} catch (e) {
|
||
console.error('Rebuild index failed:', e)
|
||
}
|
||
},
|
||
onCancel: () => {
|
||
dialog.destroy()
|
||
MessagePlugin.info(t('knowledgeEditor.indexing.rebuildSkip'))
|
||
},
|
||
})
|
||
}
|
||
}
|
||
|
||
emit('success', props.kbId)
|
||
}
|
||
|
||
handleClose()
|
||
} catch (error: any) {
|
||
console.error('Knowledge base operation failed:', error)
|
||
MessagePlugin.error(error?.message || t('common.operationFailed'))
|
||
} finally {
|
||
saving.value = false
|
||
}
|
||
}
|
||
|
||
// 重置所有状态
|
||
const resetState = () => {
|
||
currentSection.value = 'basic'
|
||
formData.value = null
|
||
hasFiles.value = false
|
||
initialStorageProvider.value = ''
|
||
initialIndexingStrategy.value = null
|
||
saving.value = false
|
||
loading.value = false
|
||
chunkingDirty.value = false
|
||
}
|
||
|
||
// 关闭弹窗
|
||
const handleClose = () => {
|
||
emit('update:visible', false)
|
||
setTimeout(() => {
|
||
resetState()
|
||
}, 300)
|
||
}
|
||
|
||
// 监听弹窗打开/关闭
|
||
watch(() => props.visible, async (newVal) => {
|
||
if (newVal) {
|
||
// 打开弹窗时,先重置状态
|
||
resetState()
|
||
|
||
// 检查是否有初始 section,如果有则跳转
|
||
if (uiStore.kbEditorInitialSection) {
|
||
currentSection.value = uiStore.kbEditorInitialSection
|
||
}
|
||
|
||
// 加载模型列表
|
||
await loadAllModels()
|
||
|
||
// 根据模式加载数据
|
||
if (props.mode === 'edit' && props.kbId) {
|
||
await loadKBData()
|
||
} else {
|
||
// 创建模式:初始化空表单
|
||
formData.value = initFormData(props.initialType || 'document')
|
||
hasFiles.value = false
|
||
}
|
||
} else {
|
||
// 关闭弹窗时,延迟重置状态(等待动画结束)
|
||
setTimeout(() => {
|
||
resetState()
|
||
currentSection.value = 'basic' // 重置为默认 section
|
||
}, 300)
|
||
}
|
||
})
|
||
|
||
// 监听全局设置弹窗关闭后刷新模型列表
|
||
watch(
|
||
() => uiStore.showSettingsModal,
|
||
async (visible, previous) => {
|
||
if (!visible && previous && props.visible) {
|
||
await loadAllModels()
|
||
}
|
||
}
|
||
)
|
||
</script>
|
||
|
||
<style scoped lang="less">
|
||
// 复用创建知识库的样式
|
||
.settings-overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 1000;
|
||
backdrop-filter: blur(4px);
|
||
}
|
||
|
||
.settings-modal {
|
||
position: relative;
|
||
width: 90vw;
|
||
max-width: 1000px;
|
||
height: 85vh;
|
||
max-height: 750px;
|
||
background: var(--td-bg-color-container);
|
||
border-radius: 12px;
|
||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.close-btn {
|
||
position: absolute;
|
||
top: 20px;
|
||
right: 20px;
|
||
width: 32px;
|
||
height: 32px;
|
||
border: none;
|
||
background: var(--td-bg-color-secondarycontainer);
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: var(--td-text-color-secondary);
|
||
transition: all 0.2s ease;
|
||
z-index: 10;
|
||
|
||
&:hover {
|
||
background: var(--td-bg-color-secondarycontainer);
|
||
color: var(--td-text-color-primary);
|
||
}
|
||
}
|
||
|
||
.settings-container {
|
||
display: flex;
|
||
height: 100%;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.settings-sidebar {
|
||
width: 200px;
|
||
background: var(--td-bg-color-settings-modal);
|
||
border-right: 1px solid var(--td-component-stroke);
|
||
display: flex;
|
||
flex-direction: column;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.sidebar-header {
|
||
padding: 24px 20px;
|
||
border-bottom: 1px solid var(--td-component-stroke);
|
||
}
|
||
|
||
.sidebar-title {
|
||
margin: 0;
|
||
font-family: "PingFang SC";
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
color: var(--td-text-color-primary);
|
||
}
|
||
|
||
.settings-nav {
|
||
flex: 1;
|
||
padding: 12px 8px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.nav-item {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 10px 12px;
|
||
margin-bottom: 4px;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
font-family: "PingFang SC";
|
||
font-size: 14px;
|
||
color: var(--td-text-color-secondary);
|
||
|
||
&:hover {
|
||
background: var(--td-bg-color-secondarycontainer-hover);
|
||
color: var(--td-text-color-primary);
|
||
}
|
||
|
||
&.active {
|
||
background: var(--td-brand-color-light);
|
||
color: var(--td-brand-color);
|
||
font-weight: 500;
|
||
}
|
||
}
|
||
|
||
.nav-icon {
|
||
margin-right: 8px;
|
||
font-size: 18px;
|
||
flex-shrink: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.nav-label {
|
||
flex: 1;
|
||
}
|
||
|
||
.nav-badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-width: 18px;
|
||
height: 18px;
|
||
padding: 0 5px;
|
||
border-radius: 9px;
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
background: var(--td-bg-color-component);
|
||
color: var(--td-text-color-secondary);
|
||
line-height: 1;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.nav-item.active .nav-badge {
|
||
background: var(--td-brand-color);
|
||
color: #fff;
|
||
}
|
||
|
||
.settings-content {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.content-wrapper {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
padding: 24px 32px;
|
||
}
|
||
|
||
.section {
|
||
margin-bottom: 32px;
|
||
|
||
&:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
}
|
||
|
||
.section-content {
|
||
.section-header {
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.section-title {
|
||
margin: 0 0 8px 0;
|
||
font-family: "PingFang SC";
|
||
font-size: 20px;
|
||
font-weight: 600;
|
||
color: var(--td-text-color-primary);
|
||
}
|
||
|
||
.section-desc {
|
||
margin: 0;
|
||
font-family: "PingFang SC";
|
||
font-size: 14px;
|
||
color: var(--td-text-color-placeholder);
|
||
line-height: 22px;
|
||
}
|
||
|
||
.section-body {
|
||
background: var(--td-bg-color-container);
|
||
}
|
||
}
|
||
|
||
.form-item {
|
||
margin-bottom: 16px;
|
||
|
||
&:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
}
|
||
|
||
.form-label {
|
||
display: block;
|
||
margin-bottom: 8px;
|
||
font-family: "PingFang SC";
|
||
font-size: 15px;
|
||
font-weight: 500;
|
||
color: var(--td-text-color-primary);
|
||
|
||
&.required::after {
|
||
content: '*';
|
||
color: var(--td-error-color);
|
||
margin-left: 4px;
|
||
}
|
||
}
|
||
|
||
.form-tip {
|
||
margin-top: 6px;
|
||
font-size: 12px;
|
||
color: var(--td-text-color-placeholder);
|
||
}
|
||
|
||
.granularity-radio-group {
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.granularity-hint {
|
||
margin-top: 8px;
|
||
line-height: 1.6;
|
||
color: var(--td-text-color-secondary);
|
||
white-space: normal;
|
||
word-break: break-word;
|
||
}
|
||
|
||
.indexing-checks {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||
gap: 12px;
|
||
margin-top: 10px;
|
||
}
|
||
|
||
.indexing-check-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
padding: 12px 14px;
|
||
border: 1px solid var(--td-component-stroke);
|
||
border-radius: 8px;
|
||
background: var(--td-bg-color-container);
|
||
cursor: pointer;
|
||
user-select: none;
|
||
transition: border-color 0.2s ease, background 0.2s ease;
|
||
|
||
&:hover {
|
||
border-color: var(--td-brand-color);
|
||
}
|
||
|
||
&.is-checked {
|
||
border-color: var(--td-brand-color);
|
||
background: var(--td-brand-color-light);
|
||
}
|
||
|
||
&.is-disabled {
|
||
cursor: not-allowed;
|
||
opacity: 0.7;
|
||
|
||
&:hover {
|
||
border-color: var(--td-component-stroke);
|
||
}
|
||
|
||
&.is-checked:hover {
|
||
border-color: var(--td-brand-color);
|
||
}
|
||
}
|
||
|
||
:deep(.t-checkbox__label) {
|
||
font-weight: 500;
|
||
color: var(--td-text-color-primary);
|
||
}
|
||
}
|
||
|
||
.locked-tip {
|
||
color: var(--td-warning-color);
|
||
margin-top: 8px;
|
||
}
|
||
|
||
// 禁用内部 checkbox 自身的点击事件,统一由卡片处理
|
||
.indexing-check-box {
|
||
pointer-events: none;
|
||
}
|
||
|
||
.indexing-check-title {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
|
||
.indexing-new-badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
padding: 0 6px;
|
||
height: 16px;
|
||
border-radius: 3px;
|
||
font-size: 10px;
|
||
font-weight: 600;
|
||
line-height: 1;
|
||
letter-spacing: 0.4px;
|
||
color: var(--td-brand-color);
|
||
background: var(--td-brand-color-light);
|
||
}
|
||
|
||
.indexing-check-desc {
|
||
margin: 0;
|
||
padding-left: 24px;
|
||
font-size: 12px;
|
||
line-height: 18px;
|
||
color: var(--td-text-color-placeholder);
|
||
}
|
||
|
||
.faq-guide {
|
||
margin-top: 20px;
|
||
padding: 12px 16px;
|
||
border-radius: 8px;
|
||
background: var(--td-bg-color-secondarycontainer);
|
||
color: var(--td-text-color-secondary);
|
||
font-size: 13px;
|
||
line-height: 20px;
|
||
}
|
||
|
||
.settings-footer {
|
||
padding: 16px 32px;
|
||
border-top: 1px solid var(--td-component-stroke);
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 12px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
// 过渡动画
|
||
.modal-enter-active,
|
||
.modal-leave-active {
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.modal-enter-from,
|
||
.modal-leave-to {
|
||
opacity: 0;
|
||
|
||
.settings-modal {
|
||
transform: scale(0.95);
|
||
}
|
||
}
|
||
|
||
// Radio-group 样式优化,符合项目主题风格
|
||
:deep(.t-radio-group) {
|
||
.t-radio-group--filled {
|
||
background: var(--td-bg-color-secondarycontainer);
|
||
}
|
||
.t-radio-button {
|
||
border-color: var(--td-component-stroke);
|
||
// color: var(--td-text-color-placeholder);
|
||
|
||
&:hover:not(.t-is-disabled) {
|
||
border-color: var(--td-brand-color);
|
||
color: var(--td-brand-color);
|
||
}
|
||
|
||
&.t-is-checked {
|
||
background: var(--td-brand-color);
|
||
border-color: var(--td-brand-color);
|
||
color: var(--td-text-color-anti);
|
||
|
||
&:hover:not(.t-is-disabled) {
|
||
background: var(--td-brand-color-active);
|
||
border-color: var(--td-brand-color-active);
|
||
color: var(--td-text-color-anti);
|
||
}
|
||
}
|
||
|
||
// 禁用状态样式
|
||
&.t-is-disabled {
|
||
background: var(--td-bg-color-secondarycontainer);
|
||
border-color: var(--td-component-stroke);
|
||
color: var(--td-text-color-disabled);
|
||
cursor: not-allowed;
|
||
opacity: 0.6;
|
||
|
||
&.t-is-checked {
|
||
background: var(--td-bg-color-secondarycontainer);
|
||
border-color: var(--td-component-stroke);
|
||
color: var(--td-text-color-placeholder);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 多模态配置内联样式(与子组件 KBStorageSettings/KBAdvancedSettings 一致)
|
||
.kb-multimodal-settings {
|
||
width: 100%;
|
||
|
||
.section-header {
|
||
margin-bottom: 32px;
|
||
|
||
h2 {
|
||
font-size: 20px;
|
||
font-weight: 600;
|
||
color: var(--td-text-color-primary);
|
||
margin: 0 0 8px 0;
|
||
}
|
||
|
||
.section-description {
|
||
font-size: 14px;
|
||
color: var(--td-text-color-secondary);
|
||
margin: 0;
|
||
line-height: 1.5;
|
||
}
|
||
}
|
||
|
||
.settings-group {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.setting-row {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: space-between;
|
||
padding: 20px 0;
|
||
border-bottom: 1px solid var(--td-component-stroke);
|
||
|
||
&:last-child {
|
||
border-bottom: none;
|
||
}
|
||
}
|
||
|
||
.setting-info {
|
||
flex: 1;
|
||
max-width: 65%;
|
||
padding-right: 24px;
|
||
|
||
label {
|
||
font-size: 15px;
|
||
font-weight: 500;
|
||
color: var(--td-text-color-primary);
|
||
display: block;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.desc {
|
||
font-size: 13px;
|
||
color: var(--td-text-color-secondary);
|
||
margin: 0;
|
||
line-height: 1.5;
|
||
}
|
||
}
|
||
|
||
.setting-control {
|
||
flex-shrink: 0;
|
||
min-width: 280px;
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
align-items: center;
|
||
}
|
||
|
||
.required {
|
||
color: var(--td-error-color);
|
||
margin-left: 2px;
|
||
font-weight: 500;
|
||
}
|
||
}
|
||
</style>
|
||
|