Files
QuestionProject/ai_knowledge_base_v2/apps/admin-web/src/components/AgentBatchTestPanel.vue

243 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { ElMessage, ElMessageBox } from "element-plus";
import { computed, onBeforeUnmount, onMounted, reactive, ref } from "vue";
import { api } from "../services/api";
import type { AgentBatchItem, AgentBatchJob, AgentBatchJobDetail } from "../types/api";
import AdminPagination from "./AdminPagination.vue";
import { formatDateTime } from "../utils/dateTime";
const props = defineProps<{
config: Record<string, unknown>;
modelName: string;
knowledgeSummary: string;
}>();
const fileInput = ref<HTMLInputElement | null>(null);
const selectedFile = ref<File | null>(null);
const taskName = ref("");
const executionMode = ref<"sequential" | "concurrent">("sequential");
const concurrencyLimit = ref(5);
const creating = ref(false);
const loading = ref(false);
const exportingId = ref<number | null>(null);
const jobs = ref<AgentBatchJob[]>([]);
const pager = reactive({ page: 1, pageSize: 10, total: 0 });
const detailOpen = ref(false);
const detailLoading = ref(false);
const selectedJob = ref<AgentBatchJobDetail | null>(null);
const detailPager = reactive({ page: 1, pageSize: 20, total: 0 });
let pollTimer: number | null = null;
const hasRunningJob = computed(() => jobs.value.some((job) => ["pending", "running"].includes(job.status)));
onMounted(() => void loadJobs());
onBeforeUnmount(stopPolling);
async function loadJobs(page = pager.page, pageSize = pager.pageSize, quiet = false) {
if (!quiet) loading.value = true;
try {
const result = await api.agentBatchJobs({ page, pageSize });
jobs.value = result.items;
Object.assign(pager, { page: result.page, pageSize: result.pageSize, total: result.total });
schedulePolling();
if (selectedJob.value && detailOpen.value) await loadDetail(selectedJob.value.id, detailPager.page, detailPager.pageSize, true);
} catch (error) {
if (!quiet) ElMessage.error(errorMessage(error, "批量测试任务加载失败"));
} finally {
if (!quiet) loading.value = false;
}
}
function schedulePolling() {
stopPolling();
if (!hasRunningJob.value) return;
pollTimer = window.setTimeout(() => void loadJobs(pager.page, pager.pageSize, true), 2500);
}
function stopPolling() {
if (pollTimer !== null) window.clearTimeout(pollTimer);
pollTimer = null;
}
function selectFile(event: Event) {
const input = event.target as HTMLInputElement;
selectedFile.value = input.files?.[0] || null;
if (selectedFile.value && !taskName.value.trim()) taskName.value = selectedFile.value.name.replace(/\.xlsx$/i, "");
}
function clearFile() {
selectedFile.value = null;
if (fileInput.value) fileInput.value.value = "";
}
async function downloadTemplate() {
try {
await api.downloadAgentBatchTemplate();
} catch (error) {
ElMessage.error(errorMessage(error, "模板下载失败"));
}
}
async function createJob() {
if (!selectedFile.value) return ElMessage.warning("请先选择填写好的 Excel 文件");
if (!String(props.config.modelId || "")) return ElMessage.warning("请先在调试配置中选择模型");
creating.value = true;
try {
const created = await api.createAgentBatch(selectedFile.value, {
...props.config,
name: taskName.value.trim(),
executionMode: executionMode.value,
concurrencyLimit: executionMode.value === "sequential" ? 1 : concurrencyLimit.value,
});
ElMessage.success(`已创建批量测试任务,共 ${created.totalCount} 个问题`);
clearFile();
taskName.value = "";
await loadJobs(1, pager.pageSize);
await openDetail(created);
} catch (error) {
ElMessage.error(errorMessage(error, "批量测试任务创建失败"));
} finally {
creating.value = false;
}
}
async function openDetail(job: AgentBatchJob) {
detailOpen.value = true;
detailPager.page = 1;
await loadDetail(job.id, 1, detailPager.pageSize);
}
async function loadDetail(jobId: number, page = detailPager.page, pageSize = detailPager.pageSize, quiet = false) {
if (!quiet) detailLoading.value = true;
try {
const result = await api.agentBatchJob(jobId, { page, pageSize });
selectedJob.value = result;
Object.assign(detailPager, { page: result.items.page, pageSize: result.items.pageSize, total: result.items.total });
} catch (error) {
if (!quiet) ElMessage.error(errorMessage(error, "任务详情加载失败"));
} finally {
if (!quiet) detailLoading.value = false;
}
}
async function exportJob(job: AgentBatchJob) {
exportingId.value = job.id;
try {
await api.exportAgentBatch(job.id);
ElMessage.success("测试结果已导出");
} catch (error) {
ElMessage.error(errorMessage(error, "结果导出失败"));
} finally {
exportingId.value = null;
}
}
async function cancelJob(job: AgentBatchJob) {
try {
await ElMessageBox.confirm("取消后尚未开始的问题不会继续生成,已完成结果仍可导出。", "取消批量测试", {
type: "warning", confirmButtonText: "确认取消", cancelButtonText: "继续执行",
});
} catch {
return;
}
try {
await api.cancelAgentBatch(job.id);
ElMessage.success("批量测试已取消");
await loadJobs(pager.page, pager.pageSize);
} catch (error) {
ElMessage.error(errorMessage(error, "取消失败"));
}
}
function statusLabel(status: string) {
return ({ pending: "等待执行", running: "生成中", completed: "已完成", completed_with_errors: "部分失败", cancelled: "已取消", success: "成功", failed: "失败" } as Record<string, string>)[status] || status;
}
function statusType(status: string) {
if (["completed", "success"].includes(status)) return "success";
if (["completed_with_errors", "failed"].includes(status)) return "danger";
if (status === "running") return "primary";
if (status === "cancelled") return "info";
return "warning";
}
function executionLabel(job: AgentBatchJob) {
return job.executionMode === "concurrent" ? `并发 · ${job.concurrencyLimit}` : "逐条串行";
}
function itemDuration(item: AgentBatchItem) {
return item.durationMs == null ? "—" : `${(item.durationMs / 1000).toFixed(1)}`;
}
function errorMessage(error: unknown, fallback: string) {
return error instanceof Error ? error.message : fallback;
}
</script>
<template>
<div class="agent-batch-panel">
<div class="agent-section-intro">
<div><h3>Excel 批量测试</h3><p>普通管理员仅看到自己的任务超级管理员可统一审计创建时会固化当前提示词模型知识库和生成参数</p></div>
<el-button @click="downloadTemplate">下载导入模板</el-button>
</div>
<section class="agent-batch-create-card">
<div class="agent-batch-config-summary">
<span><small>模型</small><strong>{{ modelName }}</strong></span>
<span><small>知识库</small><strong>{{ knowledgeSummary }}</strong></span>
<span><small>单次上限</small><strong>200 个问题</strong></span>
</div>
<div class="agent-batch-mode-setting">
<div class="agent-batch-mode-copy"><strong>执行方式</strong><span>串行适合日常回归并发适合压测吞吐与稳定性并发数会被系统安全上限约束</span></div>
<el-radio-group v-model="executionMode" class="agent-batch-mode-options">
<el-radio-button value="sequential">逐条串行</el-radio-button>
<el-radio-button value="concurrent">并发测试</el-radio-button>
</el-radio-group>
<label v-if="executionMode === 'concurrent'" class="agent-batch-concurrency"><span>并发数</span><el-input-number v-model="concurrencyLimit" :min="2" :max="10" controls-position="right" /></label>
</div>
<div class="agent-batch-upload-row">
<label class="agent-batch-name"><span>任务名称</span><input v-model="taskName" maxlength="160" placeholder="例如8月主提示词回归测试" /></label>
<label class="agent-batch-file" :class="{ selected: selectedFile }">
<input ref="fileInput" type="file" accept=".xlsx" @change="selectFile" />
<span>{{ selectedFile?.name || "选择填写好的 Excel 文件" }}</span>
<small>{{ selectedFile ? `${(selectedFile.size / 1024).toFixed(1)} KB` : "仅支持 .xlsx最大 5MB" }}</small>
</label>
<el-button v-if="selectedFile" @click="clearFile">移除</el-button>
<el-button type="primary" :loading="creating" :disabled="!selectedFile" @click="createJob">开始批量测试</el-button>
</div>
</section>
<div class="agent-batch-list-head"><div><h4>测试记录</h4><p>任务在后台执行离开页面不会中断</p></div><el-button :loading="loading" @click="loadJobs()">刷新</el-button></div>
<el-table v-loading="loading" :data="jobs" stripe>
<el-table-column label="任务" min-width="190"><template #default="{ row }"><strong>{{ row.name }}</strong><small class="agent-batch-cell-note">#{{ row.id }} · {{ formatDateTime(row.createdAt, "—") }}</small></template></el-table-column>
<el-table-column label="配置快照" min-width="180"><template #default="{ row }"><span>{{ row.modelName }}</span><small class="agent-batch-cell-note">{{ row.knowledgeNames.join('、') }}</small></template></el-table-column>
<el-table-column label="进度" min-width="190"><template #default="{ row }"><el-progress :percentage="row.progress" :status="row.status === 'completed' ? 'success' : undefined" /><small class="agent-batch-cell-note">{{ row.processedCount }}/{{ row.totalCount }} · 成功 {{ row.successCount }} · 失败 {{ row.failedCount }}</small></template></el-table-column>
<el-table-column label="执行方式" width="118"><template #default="{ row }"><el-tag type="info">{{ executionLabel(row) }}</el-tag></template></el-table-column>
<el-table-column label="状态" width="108"><template #default="{ row }"><el-tag :type="statusType(row.status)">{{ statusLabel(row.status) }}</el-tag></template></el-table-column>
<el-table-column label="操作" width="190" fixed="right"><template #default="{ row }"><el-button link type="primary" @click="openDetail(row)">查看</el-button><el-button link type="primary" :loading="exportingId === row.id" @click="exportJob(row)">导出</el-button><el-button v-if="['pending','running'].includes(row.status)" link type="danger" @click="cancelJob(row)">取消</el-button></template></el-table-column>
</el-table>
<AdminPagination :page="pager.page" :page-size="pager.pageSize" :total="pager.total" @change="loadJobs" />
</div>
<el-dialog v-model="detailOpen" width="min(1100px, 94vw)" title="批量测试详情" destroy-on-close>
<div v-loading="detailLoading" class="agent-batch-detail">
<template v-if="selectedJob">
<div class="agent-batch-detail-head">
<div><strong>{{ selectedJob.name }}</strong><span>{{ selectedJob.modelName }} · {{ selectedJob.knowledgeNames.join('、') }} · {{ executionLabel(selectedJob) }}</span></div>
<el-tag :type="statusType(selectedJob.status)">{{ statusLabel(selectedJob.status) }}</el-tag>
</div>
<el-table :data="selectedJob.items.items" stripe max-height="520">
<el-table-column prop="externalNo" label="序号" width="76" />
<el-table-column prop="question" label="问题" min-width="230" show-overflow-tooltip />
<el-table-column label="答案" min-width="360"><template #default="{ row }"><div class="agent-batch-answer">{{ row.answer || row.errorMessage || statusLabel(row.status) }}</div></template></el-table-column>
<el-table-column label="状态" width="90"><template #default="{ row }"><el-tag size="small" :type="statusType(row.status)">{{ statusLabel(row.status) }}</el-tag></template></el-table-column>
<el-table-column label="耗时" width="92"><template #default="{ row }">{{ itemDuration(row) }}</template></el-table-column>
</el-table>
<AdminPagination :page="detailPager.page" :page-size="detailPager.pageSize" :total="detailPager.total" @change="(page, pageSize) => loadDetail(selectedJob!.id, page, pageSize)" />
</template>
</div>
<template #footer><el-button @click="detailOpen = false">关闭</el-button><el-button v-if="selectedJob" type="primary" :loading="exportingId === selectedJob.id" @click="exportJob(selectedJob)">导出 Excel</el-button></template>
</el-dialog>
</template>