知识库创建

This commit is contained in:
2025-11-06 19:08:20 +08:00
parent 0bb4853d54
commit d9947e273c
23 changed files with 2563 additions and 1018 deletions

View File

@@ -0,0 +1,556 @@
<template>
<div class="knowledge-basic-container">
<!-- 查看模式 -->
<div v-if="type === 'view'" class="view-mode">
<div class="info-section">
<div class="avatar-section">
<img v-if="knowledge?.avatar" :src="FILE_DOWNLOAD_URL + knowledge.avatar" alt="知识库头像" />
<div v-else class="default-avatar">📚</div>
</div>
<div class="info-details">
<h2 class="knowledge-name">{{ knowledge?.title || '未命名' }}</h2>
<p class="knowledge-description">{{ knowledge?.description || '暂无描述' }}</p>
<div class="meta-info">
<div class="meta-item">
<span class="label">状态</span>
<el-tag :type="knowledge?.status === 1 ? 'success' : 'info'" size="small">
{{ knowledge?.status === 1 ? '已启用' : '已禁用' }}
</el-tag>
</div>
<div class="meta-item" v-if="knowledge?.difyDatasetId">
<span class="label">Dify数据集ID</span>
<span class="value">{{ knowledge.difyDatasetId }}</span>
</div>
<div class="meta-item">
<span class="label">索引方式</span>
<span class="value">{{ getIndexingText(knowledge?.difyIndexingTechnique) }}</span>
</div>
<div class="meta-item" v-if="knowledge?.difyIndexingTechnique == 'high_quality'">
<span class="label">Embedding模型</span>
<span class="value">{{ knowledge?.embeddingModel || '-' }}</span>
</div>
<div class="meta-item">
<span class="label">文档数量</span>
<span class="value">{{ knowledge?.documentCount || 0 }}</span>
</div>
<div class="meta-item">
<span class="label">创建时间</span>
<span class="value">{{ formatDate(knowledge?.createTime) }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- 新增/编辑模式 -->
<div v-else class="edit-mode">
<el-form
ref="formRef"
:model="formData"
:rules="rules"
label-position="top"
class="knowledge-form"
>
<el-form-item label="知识库标题" prop="title" required>
<el-input
v-model="formData.title"
placeholder="请输入知识库标题"
maxlength="100"
show-word-limit
:disabled="props.type === 'view'"
/>
</el-form-item>
<el-form-item label="知识库头像" prop="avatar">
<FileUpload
:cover-url="formData.avatar"
@update:cover-url="handleAvatarUpdate"
:as-dialog="false"
list-type="cover"
accept="image/*"
:max-size="2"
module="ai-knowledge"
tip="点击上传知识库头像"
/>
</el-form-item>
<el-form-item label="知识库描述" prop="description">
<el-input
v-model="formData.description"
type="textarea"
:rows="4"
placeholder="请输入知识库描述,介绍知识库的内容、用途等..."
maxlength="500"
show-word-limit
/>
</el-form-item>
<el-form-item label="索引方式" prop="difyIndexingTechnique" required>
<el-radio-group v-model="formData.difyIndexingTechnique">
<el-radio value="high_quality">
<span class="radio-label">高质量</span>
<span class="radio-desc">更精确的检索结果消耗更多tokens</span>
</el-radio>
<el-radio
value="economy"
:disabled="type === 'edit' && knowledge?.difyIndexingTechnique === 'high_quality'"
>
<span class="radio-label">经济</span>
<span class="radio-desc">快速检索消耗较少tokens</span>
</el-radio>
</el-radio-group>
<div v-if="type === 'edit' && knowledge?.difyIndexingTechnique === 'high_quality'" class="form-tip">
高质量模式不能降级为经济模式
</div>
</el-form-item>
<el-form-item v-if="formData.difyIndexingTechnique == 'high_quality'" label="Embedding模型" prop="embeddingModel">
<el-select
v-model="formData.embeddingModel"
placeholder="请选择Embedding模型可选"
clearable
filterable
:loading="modelsLoading"
@change="handleModelChange"
>
<el-option-group
v-for="provider in embeddingModels"
:key="provider.provider"
:label="provider.label || provider.provider"
>
<el-option
v-for="model in provider.models"
:key="model.model"
:label="getModelLabel(model)"
:value="model.model"
:data-provider="model.provider"
>
<span>{{ getModelLabel(model) }}</span>
<span v-if="model.contextSize" style="float: right; color: var(--el-text-color-secondary); font-size: 13px">
上下文: {{ model.contextSize }}
</span>
</el-option>
</el-option-group>
</el-select>
<div v-if="type === 'edit'" class="form-tip form-tip-success">
可以切换Embedding模型以优化检索效果
</div>
</el-form-item>
<el-form-item label="Dify数据集ID" prop="difyDatasetId">
<el-input
v-model="formData.difyDatasetId"
placeholder="可选如需关联现有Dify数据集请填写"
:disabled="type === 'edit'"
/>
<div v-if="type === 'edit'" class="form-tip">
Dify数据集ID创建后不可修改
</div>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-switch
v-model="formData.status"
:active-value="1"
:inactive-value="0"
active-text="启用"
inactive-text="禁用"
/>
</el-form-item>
<div class="form-actions">
<el-button type="primary" @click="handleSubmit" :loading="submitting">
{{ type === 'add' ? '创建知识库' : '保存修改' }}
</el-button>
<el-button @click="handleCancel">
取消
</el-button>
</div>
</el-form>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, watch } from 'vue';
import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
import { FileUpload } from '@/components/file';
import type { AiKnowledge } from '@/types/ai';
import { knowledgeApi } from '@/apis/ai';
import { FILE_DOWNLOAD_URL } from '@/config';
defineOptions({
name: 'KnowledgeBasic'
});
interface KnowledgeBasicProps {
type: 'view' | 'add' | 'edit';
knowledge?: AiKnowledge | null;
}
const props = defineProps<KnowledgeBasicProps>();
const emit = defineEmits<{
success: [knowledge: AiKnowledge];
cancel: [];
}>();
// 表单引用
const formRef = ref<FormInstance>();
const submitting = ref(false);
const modelsLoading = ref(false);
const embeddingModels = ref<any[]>([]);
// 表单数据
const formData = reactive<Partial<AiKnowledge>>({
title: '',
avatar: '',
description: '',
difyIndexingTechnique: 'high_quality',
embeddingModel: '',
embeddingModelProvider: '',
difyDatasetId: '',
status: 1
});
// 表单验证规则
const rules: FormRules = {
title: [
{ required: true, message: '请输入知识库标题', trigger: 'blur' },
{ min: 2, max: 100, message: '标题长度在2-100个字符之间', trigger: 'blur' }
],
difyIndexingTechnique: [
{ required: true, message: '请选择索引方式', trigger: 'change' }
]
};
// 监听知识库数据变化
watch(() => props.knowledge, (newVal) => {
if (newVal && (props.type === 'view' || props.type === 'edit')) {
Object.assign(formData, {
id: newVal.id,
title: newVal.title,
avatar: newVal.avatar,
description: newVal.description,
difyIndexingTechnique: newVal.difyIndexingTechnique || 'high_quality',
embeddingModel: newVal.embeddingModel,
embeddingModelProvider: newVal.embeddingModelProvider,
difyDatasetId: newVal.difyDatasetId,
status: newVal.status ?? 1
});
}
}, { immediate: true });
// 处理头像更新
function handleAvatarUpdate(val: string) {
formData.avatar = val;
}
// 加载嵌入模型列表
async function loadEmbeddingModels() {
try {
modelsLoading.value = true;
const result = await knowledgeApi.getAvailableEmbeddingModels();
if (result.success && result.data) {
// 按提供商分组
const providers = result.data.providers || [];
embeddingModels.value = providers.map((provider: any) => ({
provider: provider.provider,
label: provider.label?.zh_Hans || provider.label?.en_US || provider.provider,
models: (provider.models || []).map((model: any) => ({
model: model.model,
provider: model.provider || provider.provider, // 添加 provider 到 model 对象
label: model.label?.zh_Hans || model.label?.en_US || model.model,
contextSize: model.model_properties?.context_size,
status: model.status
}))
}));
} else {
ElMessage.warning('获取嵌入模型列表失败,使用默认选项');
// 设置默认模型列表
embeddingModels.value = [{
provider: 'openai',
label: 'OpenAI',
models: [
{ model: 'text-embedding-ada-002', label: 'text-embedding-ada-002' },
{ model: 'text-embedding-3-small', label: 'text-embedding-3-small' },
{ model: 'text-embedding-3-large', label: 'text-embedding-3-large' }
]
}];
}
} catch (error: any) {
console.error('加载嵌入模型列表失败:', error);
// 设置默认模型列表
embeddingModels.value = [{
provider: 'openai',
label: 'OpenAI',
models: [
{ model: 'text-embedding-ada-002', label: 'text-embedding-ada-002' },
{ model: 'text-embedding-3-small', label: 'text-embedding-3-small' },
{ model: 'text-embedding-3-large', label: 'text-embedding-3-large' }
]
}];
} finally {
modelsLoading.value = false;
}
}
// 获取模型显示标签
function getModelLabel(model: any): string {
return model.label || model.model;
}
// 处理模型变化
function handleModelChange(modelName: string) {
if (!modelName) {
formData.embeddingModelProvider = '';
return;
}
// 查找选中模型的提供商
for (const providerGroup of embeddingModels.value) {
const foundModel = providerGroup.models.find((m: any) => m.model === modelName);
if (foundModel) {
formData.embeddingModelProvider = foundModel.provider;
console.log('选择模型:', modelName, '提供商:', foundModel.provider);
break;
}
}
}
// 组件挂载时加载模型列表
loadEmbeddingModels();
// 提交表单
async function handleSubmit() {
if (!formRef.value) return;
try {
const valid = await formRef.value.validate();
if (!valid) return;
submitting.value = true;
if (props.type === 'add') {
// 创建知识库
const result = await knowledgeApi.createKnowledge(formData as AiKnowledge);
if (result.success && result.data) {
ElMessage.success('创建成功');
emit('success', result.data);
} else {
ElMessage.error(result.message || '创建失败');
}
} else if (props.type === 'edit') {
// 更新知识库
const result = await knowledgeApi.updateKnowledge(formData as AiKnowledge);
if (result.success && result.data) {
ElMessage.success('保存成功');
emit('success', result.data);
} else {
ElMessage.error(result.message || '保存失败');
}
}
} catch (error: any) {
console.error('提交失败:', error);
ElMessage.error(props.type === 'add' ? '创建失败' : '保存失败');
} finally {
submitting.value = false;
}
}
// 取消操作
function handleCancel() {
emit('cancel');
}
// 获取索引方式文本
function getIndexingText(technique: string | undefined): string {
switch (technique) {
case 'high_quality':
return '高质量';
case 'economy':
return '经济';
default:
return '-';
}
}
// 格式化日期
function formatDate(date: string | Date | undefined): string {
if (!date) return '-';
const d = new Date(date);
if (isNaN(d.getTime())) return '-';
return d.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
}
</script>
<style scoped lang="scss">
.knowledge-basic-container {
padding: 24px;
background: #FFFFFF;
border-radius: 14px;
min-height: 400px;
}
// 查看模式样式
.view-mode {
.info-section {
display: flex;
gap: 24px;
}
.avatar-section {
width: 120px;
height: 120px;
border-radius: 12px;
overflow: hidden;
background: #F8F9FA;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
img {
width: 100%;
height: 100%;
object-fit: cover;
}
.default-avatar {
font-size: 48px;
}
}
.info-details {
flex: 1;
min-width: 0;
.knowledge-name {
font-size: 24px;
font-weight: 600;
color: #101828;
margin: 0 0 12px 0;
letter-spacing: -0.02em;
}
.knowledge-description {
font-size: 14px;
color: #667085;
line-height: 1.6;
margin: 0 0 24px 0;
}
.meta-info {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 16px;
.meta-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
.label {
color: #667085;
font-weight: 500;
}
.value {
color: #101828;
}
}
}
}
}
// 编辑模式样式
.edit-mode {
.knowledge-form {
max-width: 600px;
:deep(.el-form-item__label) {
font-weight: 500;
color: #101828;
margin-bottom: 8px;
}
:deep(.el-radio) {
display: flex;
align-items: flex-start;
margin-bottom: 12px;
height: auto;
.el-radio__label {
display: flex;
flex-direction: column;
gap: 4px;
white-space: normal;
}
.radio-label {
font-weight: 500;
color: #101828;
}
.radio-desc {
font-size: 13px;
color: #667085;
line-height: 1.4;
}
}
.form-tip {
margin-top: 8px;
font-size: 13px;
color: #F59E0B;
line-height: 1.4;
}
.form-tip-success {
color: #67C23A;
}
.form-actions {
display: flex;
gap: 12px;
margin-top: 32px;
padding-top: 24px;
border-top: 1px solid #F3F3F5;
.el-button {
border-radius: 8px;
font-weight: 500;
padding: 12px 24px;
&.el-button--primary {
background: #E7000B;
border-color: #E7000B;
&:hover {
background: #C90009;
}
}
}
}
}
}
:deep(.el-switch) {
.el-switch__label {
font-size: 14px;
color: #667085;
&.is-active {
color: #E7000B;
}
}
}
</style>