feat(frontend): 新增模型平台管理页面并接入RAG模型配置

This commit is contained in:
2026-05-27 22:14:34 +08:00
parent 5d7ca5b31f
commit 21c9eaa44d
12 changed files with 1489 additions and 17 deletions

View File

@@ -3,6 +3,14 @@ import { CirclePlus, Delete, Edit, FolderAdd, Refresh, Search, UploadFilled } fr
import { ElMessage, ElMessageBox } from 'element-plus';
import { computed, onMounted, reactive, ref } from 'vue';
import { useRouter } from 'vue-router';
import { loadModelProviderEnumOptions, type EnumOption } from '@/api/modelEnums';
import {
getRagStoreModelConfig,
queryModelConfigs,
rebuildRagStoreIndex,
saveRagStoreModelConfig,
type ModelConfig,
} from '@/api/modelProvider';
import {
deleteRagStore,
@@ -28,6 +36,20 @@ const activeStoreId = ref<string | null>(null);
const activeStore = ref<RagStore | null>(null);
const pageOverview = ref<RagStoreOverview | null>(null);
const activeStoreDocumentOverview = ref<RagStoreDocumentOverview | null>(null);
const embeddingModels = ref<ModelConfig[]>([]);
const ragConfigLoading = ref(false);
const ragConfigSaving = ref(false);
const ragConfig = reactive({
id: '',
embeddingModelId: '',
embeddingDimension: 1024,
chunkStrategy: null as number | null,
chunkSize: null as number | null,
chunkOverlap: null as number | null,
delimiter: '',
remark: '',
});
const chunkStrategyOptions = ref<EnumOption[]>([]);
const queryForm = reactive({
storeName: '',
@@ -91,6 +113,8 @@ async function loadStores(preferredStoreId?: string | null) {
activeStoreId.value = null;
activeStore.value = null;
activeStoreDocumentOverview.value = null;
ragConfig.id = '';
ragConfig.embeddingModelId = '';
return;
}
@@ -123,6 +147,7 @@ async function selectStore(storeId: string) {
]);
activeStore.value = storeResponse.data ?? null;
activeStoreDocumentOverview.value = documentOverviewResponse.data ?? null;
await loadRagModelConfig(storeId);
} finally {
detailLoading.value = false;
}
@@ -223,10 +248,6 @@ async function removeStore() {
await loadStores();
}
function showFutureMessage(actionName: string) {
ElMessage.info(`${actionName} 会在下一批接口里补齐`);
}
function openBatchUploadDialog() {
if (!activeStore.value?.id) {
ElMessage.warning('请选择知识库');
@@ -252,13 +273,85 @@ async function refreshAfterUpload() {
]);
}
async function loadRagModelConfig(storeId: string) {
ragConfigLoading.value = true;
try {
const response = await getRagStoreModelConfig(storeId);
const data = response.data;
ragConfig.id = data?.id ?? '';
ragConfig.embeddingModelId = data?.embeddingModelId ?? '';
ragConfig.embeddingDimension = data?.embeddingDimension ?? 1024;
ragConfig.chunkStrategy = data?.chunkStrategy ?? null;
ragConfig.chunkSize = data?.chunkSize ?? null;
ragConfig.chunkOverlap = data?.chunkOverlap ?? null;
ragConfig.delimiter = data?.delimiter ?? '';
ragConfig.remark = data?.remark ?? '';
} finally {
ragConfigLoading.value = false;
}
}
async function saveStoreModelConfig() {
if (!activeStoreId.value) {
return;
}
if (!ragConfig.embeddingModelId) {
ElMessage.warning('请选择 Embedding 模型');
return;
}
ragConfigSaving.value = true;
try {
await saveRagStoreModelConfig({
id: ragConfig.id || undefined,
storeId: activeStoreId.value,
embeddingModelId: ragConfig.embeddingModelId,
embeddingDimension: ragConfig.embeddingDimension,
chunkStrategy: ragConfig.chunkStrategy ?? undefined,
chunkSize: ragConfig.chunkSize ?? undefined,
chunkOverlap: ragConfig.chunkOverlap ?? undefined,
delimiter: ragConfig.delimiter || undefined,
remark: ragConfig.remark || undefined,
});
ElMessage.success('知识库模型配置已保存');
await loadRagModelConfig(activeStoreId.value);
} finally {
ragConfigSaving.value = false;
}
}
async function triggerRebuildIndex() {
if (!activeStoreId.value) {
return;
}
await rebuildRagStoreIndex(activeStoreId.value);
ElMessage.success('已触发重建索引');
}
function syncEmbeddingDimensionFromModel() {
const model = embeddingModels.value.find((item) => item.id === ragConfig.embeddingModelId);
if (!model) {
return;
}
if (model.embeddingDimension) {
ragConfig.embeddingDimension = model.embeddingDimension;
}
}
function getStatusTagType(status?: string | null) {
return status === '启用' ? 'success' : 'info';
}
onMounted(() => {
loadOverview();
loadStores();
Promise.all([
loadOverview(),
loadStores(),
queryModelConfigs().then((response) => {
embeddingModels.value = (response.data ?? []).filter((item) => item.modelType === 'EMBEDDING');
}),
loadModelProviderEnumOptions().then((enums) => {
chunkStrategyOptions.value = enums.chunk_strategy ?? [];
}),
]);
});
</script>
@@ -351,7 +444,7 @@ onMounted(() => {
>
批量导入文件
</el-button>
<el-button :icon="FolderAdd" @click="showFutureMessage('重建索引')">重建索引</el-button>
<el-button :icon="FolderAdd" @click="triggerRebuildIndex">重建索引</el-button>
<el-button type="danger" :icon="Delete" @click="removeStore">删除</el-button>
</div>
</div>
@@ -415,12 +508,56 @@ onMounted(() => {
</el-descriptions>
</article>
<article class="detail-card detail-card--placeholder">
<article class="detail-card">
<div class="detail-card__header">
<h4>检索配置</h4>
<span>下一批接口补充</span>
<el-button type="primary" link :loading="ragConfigSaving" @click="saveStoreModelConfig">
保存配置
</el-button>
</div>
<div v-loading="ragConfigLoading" class="rag-config-form">
<el-form :model="ragConfig" label-width="112px">
<el-form-item label="Embedding 模型" required>
<el-select
v-model="ragConfig.embeddingModelId"
placeholder="请选择模型"
@change="syncEmbeddingDimensionFromModel"
>
<el-option
v-for="item in embeddingModels"
:key="item.id"
:label="`${item.modelName}(${item.modelCode})`"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="向量维度">
<el-input-number v-model="ragConfig.embeddingDimension" :min="1" controls-position="right" />
</el-form-item>
<el-form-item label="切片策略">
<el-select v-model="ragConfig.chunkStrategy" clearable placeholder="可选">
<el-option
v-for="item in chunkStrategyOptions"
:key="item.value"
:label="item.label"
:value="Number(item.value)"
/>
</el-select>
</el-form-item>
<el-form-item label="切片大小">
<el-input-number v-model="ragConfig.chunkSize" :min="1" controls-position="right" />
</el-form-item>
<el-form-item label="切片重叠">
<el-input-number v-model="ragConfig.chunkOverlap" :min="0" controls-position="right" />
</el-form-item>
<el-form-item label="分隔符">
<el-input v-model="ragConfig.delimiter" placeholder="可选,如 \\n\\n" />
</el-form-item>
<el-form-item label="备注">
<el-input v-model="ragConfig.remark" type="textarea" :rows="2" />
</el-form-item>
</el-form>
</div>
<el-empty description="检索模式、Embedding 模型、Chunk 参数待后端补充" />
</article>
<article class="detail-card detail-card--placeholder">
@@ -721,6 +858,11 @@ onMounted(() => {
padding: 12px 0;
}
.rag-config-form :deep(.el-select),
.rag-config-form :deep(.el-input-number) {
width: 100%;
}
@media (max-width: 1280px) {
.overview-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));