feat: complete aihr mvp mobile workflow
This commit is contained in:
@@ -64,6 +64,26 @@ export type ModelChatResponse = {
|
||||
hints: string[];
|
||||
};
|
||||
|
||||
export type VectorIndexStatus = {
|
||||
modelName: string;
|
||||
modelDimension: number;
|
||||
collection: string;
|
||||
qdrantDimension?: number;
|
||||
qdrantPoints?: number;
|
||||
fragments: number;
|
||||
embeddedFragments: number;
|
||||
embeddingModel?: string;
|
||||
embeddingDimension?: number;
|
||||
matched: boolean;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export type VectorizeResponse = {
|
||||
documents: number;
|
||||
fragments: number;
|
||||
model: string;
|
||||
};
|
||||
|
||||
export function getModelProviders(): Promise<ApiResult<ModelProvider[]>> {
|
||||
return request({
|
||||
url: '/api/aihr/model/providers',
|
||||
@@ -140,3 +160,18 @@ export function testModelChat(data: { prompt: string; model?: string }): Promise
|
||||
headers: { repeatSubmit: false }
|
||||
});
|
||||
}
|
||||
|
||||
export function getVectorIndexStatus(): Promise<ApiResult<VectorIndexStatus>> {
|
||||
return request({
|
||||
url: '/api/knowledge/doc/vector-index-status',
|
||||
method: 'get'
|
||||
});
|
||||
}
|
||||
|
||||
export function rebuildVectorIndex(): Promise<ApiResult<VectorizeResponse>> {
|
||||
return request({
|
||||
url: '/api/knowledge/doc/rebuild-vector-index',
|
||||
method: 'post',
|
||||
headers: { repeatSubmit: false }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -143,9 +143,12 @@
|
||||
<span>PDF/Word/Excel/PPT 解析</span>
|
||||
<el-switch :model-value="displayOverview.rules.parserEnabled" disabled active-color="#e60012" />
|
||||
</label>
|
||||
<label>
|
||||
<span>图片 OCR</span>
|
||||
<el-switch :model-value="displayOverview.rules.imageOcrEnabled" disabled active-color="#e60012" />
|
||||
<label :class="displayOverview.rules.imageOcrEnabled ? '' : 'rule-item-disabled'">
|
||||
<span :class="displayOverview.rules.imageOcrEnabled ? '' : 'rule-label-muted'">
|
||||
图片 OCR
|
||||
<el-tag v-if="!displayOverview.rules.imageOcrEnabled" size="small" type="info" class="coming-soon-tag">即将支持</el-tag>
|
||||
</span>
|
||||
<el-switch :model-value="displayOverview.rules.imageOcrEnabled" disabled :active-color="displayOverview.rules.imageOcrEnabled ? '#e60012' : undefined" />
|
||||
</label>
|
||||
<label>
|
||||
<span>低置信回答不编造</span>
|
||||
@@ -988,6 +991,25 @@ function fileBadgeClass(type: string) {
|
||||
}
|
||||
}
|
||||
|
||||
.rule-item-disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.rule-label-muted {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.coming-soon-tag {
|
||||
font-size: 11px;
|
||||
padding: 0 5px;
|
||||
height: 18px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.risk-tip {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
@@ -98,6 +98,45 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel vector-panel" v-loading="vectorLoading">
|
||||
<div class="panel-title with-actions">
|
||||
<h2>向量库状态</h2>
|
||||
<div>
|
||||
<el-button :icon="Refresh" @click="loadVectorStatus">刷新</el-button>
|
||||
<el-button type="danger" plain :icon="Refresh" :loading="rebuilding" @click="runVectorRebuild">重建索引</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="vector-grid">
|
||||
<div class="field-row">
|
||||
<span>启用模型</span>
|
||||
<strong>{{ vectorStatus?.modelName || '-' }}</strong>
|
||||
</div>
|
||||
<div class="field-row">
|
||||
<span>模型维度</span>
|
||||
<strong>{{ vectorStatus?.modelDimension || '-' }}</strong>
|
||||
</div>
|
||||
<div class="field-row">
|
||||
<span>Qdrant 维度</span>
|
||||
<strong>{{ vectorStatus?.qdrantDimension || '-' }}</strong>
|
||||
</div>
|
||||
<div class="field-row">
|
||||
<span>点数</span>
|
||||
<strong>{{ vectorStatus?.qdrantPoints ?? '-' }}</strong>
|
||||
</div>
|
||||
<div class="field-row">
|
||||
<span>片段向量</span>
|
||||
<strong>{{ vectorStatus ? `${vectorStatus.embeddedFragments} / ${vectorStatus.fragments}` : '-' }}</strong>
|
||||
</div>
|
||||
<div class="field-row">
|
||||
<span>当前状态</span>
|
||||
<strong>
|
||||
<el-tag :type="vectorStatus?.matched ? 'success' : 'warning'">{{ vectorStatusLabel }}</el-tag>
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel model-panel">
|
||||
<div class="panel-title">
|
||||
<h2>模型列表</h2>
|
||||
@@ -203,14 +242,16 @@
|
||||
</template>
|
||||
|
||||
<script setup name="SystemModelConfig" lang="ts">
|
||||
import { Connection, EditPen, Hide, InfoFilled, Plus, Search, View } from '@element-plus/icons-vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { Connection, EditPen, Hide, InfoFilled, Plus, Refresh, Search, View } from '@element-plus/icons-vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
import {
|
||||
createModelConfig,
|
||||
createModelProvider,
|
||||
getModelConfigs,
|
||||
getModelProviders,
|
||||
getVectorIndexStatus,
|
||||
rebuildVectorIndex,
|
||||
testModelChat,
|
||||
updateModelConfig,
|
||||
updateModelConfigEnabled,
|
||||
@@ -219,7 +260,8 @@ import {
|
||||
type ModelConfig,
|
||||
type ModelConfigPayload,
|
||||
type ModelProvider,
|
||||
type ModelProviderPayload
|
||||
type ModelProviderPayload,
|
||||
type VectorIndexStatus
|
||||
} from '@/api/aihr/model';
|
||||
|
||||
type ProviderView = ModelProvider & { logo: string };
|
||||
@@ -385,10 +427,13 @@ const keyword = ref('');
|
||||
const loading = ref(false);
|
||||
const testing = ref(false);
|
||||
const saving = ref(false);
|
||||
const vectorLoading = ref(false);
|
||||
const rebuilding = ref(false);
|
||||
const showKey = ref(false);
|
||||
const providerDialogVisible = ref(false);
|
||||
const modelDialogVisible = ref(false);
|
||||
const lastTestAt = ref('未测试');
|
||||
const vectorStatus = ref<VectorIndexStatus>();
|
||||
|
||||
const providerForm = reactive<ProviderFormState>({
|
||||
id: 0,
|
||||
@@ -435,6 +480,7 @@ const providerEnabled = computed({
|
||||
providerForm.status = value ? '0' : '1';
|
||||
}
|
||||
});
|
||||
const vectorStatusLabel = computed(() => (vectorStatus.value?.matched ? '一致' : vectorStatus.value?.message || '需重建'));
|
||||
|
||||
const providerTag = (provider: ProviderView) => {
|
||||
if (provider.providerCode.includes('ollama')) return { label: '本地', className: 'neutral' };
|
||||
@@ -483,6 +529,38 @@ const loadData = async (preferredProviderCode = selectedProviderCode.value) => {
|
||||
}
|
||||
};
|
||||
|
||||
const loadVectorStatus = async () => {
|
||||
vectorLoading.value = true;
|
||||
try {
|
||||
const result = await getVectorIndexStatus();
|
||||
vectorStatus.value = result.data;
|
||||
} catch {
|
||||
vectorStatus.value = undefined;
|
||||
} finally {
|
||||
vectorLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const runVectorRebuild = async () => {
|
||||
try {
|
||||
await ElMessageBox.confirm('将清空旧片段向量并重建 Qdrant 索引,继续吗?', '重建向量索引', {
|
||||
confirmButtonText: '重建',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
});
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
rebuilding.value = true;
|
||||
try {
|
||||
const result = await rebuildVectorIndex();
|
||||
await loadVectorStatus();
|
||||
ElMessage.success(`已重建 ${result.data.documents} 份资料 / ${result.data.fragments} 个片段`);
|
||||
} finally {
|
||||
rebuilding.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const runConnectionTest = async () => {
|
||||
testing.value = true;
|
||||
try {
|
||||
@@ -623,6 +701,7 @@ const submitModel = async () => {
|
||||
}
|
||||
modelDialogVisible.value = false;
|
||||
await loadData(payload.providerCode);
|
||||
await loadVectorStatus();
|
||||
ElMessage.success('模型配置已保存');
|
||||
} finally {
|
||||
saving.value = false;
|
||||
@@ -640,13 +719,17 @@ const toggleModelEnabled = async (row: ModelConfig, enabled: boolean) => {
|
||||
try {
|
||||
await updateModelConfigEnabled(row.id, enabled);
|
||||
row.enabled = enabled;
|
||||
await loadVectorStatus();
|
||||
ElMessage.success(enabled ? '模型已启用' : '模型已停用');
|
||||
} catch {
|
||||
row.enabled = !enabled;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(loadData);
|
||||
onMounted(() => {
|
||||
loadData();
|
||||
loadVectorStatus();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -849,6 +932,7 @@ onMounted(loadData);
|
||||
}
|
||||
|
||||
.detail-panel,
|
||||
.vector-panel,
|
||||
.model-panel {
|
||||
padding-bottom: 18px;
|
||||
}
|
||||
@@ -870,6 +954,13 @@ onMounted(loadData);
|
||||
padding: 22px 24px 6px;
|
||||
}
|
||||
|
||||
.vector-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(220px, 1fr));
|
||||
gap: 18px 24px;
|
||||
padding: 22px 24px 6px;
|
||||
}
|
||||
|
||||
.field-list {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
@@ -943,7 +1034,8 @@ onMounted(loadData);
|
||||
}
|
||||
|
||||
.model-layout,
|
||||
.detail-grid {
|
||||
.detail-grid,
|
||||
.vector-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user