feat: complete aihr mvp mobile workflow

This commit is contained in:
2026-07-03 16:43:37 +08:00
parent 0126b0c661
commit f869d66c90
46 changed files with 5879 additions and 219 deletions
+35
View File
@@ -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 }
});
}
+25 -3
View File
@@ -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;
+97 -5
View File
@@ -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;
}