feat: 支持 Agent 并发批测与洞察导出
This commit is contained in:
@@ -15,6 +15,8 @@ const props = defineProps<{
|
||||
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);
|
||||
@@ -81,7 +83,12 @@ async function createJob() {
|
||||
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() });
|
||||
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 = "";
|
||||
@@ -154,6 +161,10 @@ function statusType(status: string) {
|
||||
return "warning";
|
||||
}
|
||||
|
||||
function executionLabel(job: AgentBatchJob) {
|
||||
return job.executionMode === "concurrent" ? `并发 · ${job.concurrencyLimit} 路` : "逐条串行";
|
||||
}
|
||||
|
||||
function formatDate(value?: string | null) {
|
||||
return value ? new Date(value).toLocaleString("zh-CN", { hour12: false }) : "—";
|
||||
}
|
||||
@@ -170,7 +181,7 @@ function errorMessage(error: unknown, fallback: string) {
|
||||
<template>
|
||||
<div class="agent-batch-panel">
|
||||
<div class="agent-section-intro">
|
||||
<div><h3>Excel 批量测试</h3><p>使用当前编辑框中的提示词、调试模型、知识库和生成参数创建配置快照,后台逐条生成答案。</p></div>
|
||||
<div><h3>Excel 批量测试</h3><p>普通管理员仅看到自己的任务,超级管理员可统一审计;创建时会固化当前提示词、模型、知识库和生成参数。</p></div>
|
||||
<el-button @click="downloadTemplate">下载导入模板</el-button>
|
||||
</div>
|
||||
|
||||
@@ -180,6 +191,14 @@ function errorMessage(error: unknown, fallback: string) {
|
||||
<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 }">
|
||||
@@ -197,6 +216,7 @@ function errorMessage(error: unknown, fallback: string) {
|
||||
<el-table-column label="任务" min-width="190"><template #default="{ row }"><strong>{{ row.name }}</strong><small class="agent-batch-cell-note">#{{ row.id }} · {{ formatDate(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>
|
||||
@@ -207,7 +227,7 @@ function errorMessage(error: unknown, fallback: string) {
|
||||
<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('、') }}</span></div>
|
||||
<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">
|
||||
|
||||
@@ -122,7 +122,7 @@ async function load() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const [promptResult, modelRows, knowledgeRows, formalConfig] = await Promise.all([
|
||||
api.prompt(), api.models(), api.knowledgeOptions(), api.agentRuntimeConfig(),
|
||||
api.prompt(), api.agentModelOptions(), api.agentKnowledgeOptions(), api.agentRuntimeConfig(),
|
||||
]);
|
||||
applyPrompt(promptResult);
|
||||
models.value = modelRows;
|
||||
|
||||
@@ -14,6 +14,7 @@ import ChatDetailDrawer from "./ChatDetailDrawer.vue";
|
||||
|
||||
type RecordTab = "chats" | "questionInsights" | "aiLogs" | "operationLogs";
|
||||
const loading = ref(false);
|
||||
const insightExporting = ref(false);
|
||||
const activeTab = ref<RecordTab>("chats");
|
||||
const chats = ref<ChatRecord[]>([]);
|
||||
const ssoClients = ref<SsoClientItem[]>([]);
|
||||
@@ -175,6 +176,32 @@ async function resetInsightFilters() {
|
||||
});
|
||||
await refreshInsights();
|
||||
}
|
||||
async function exportInsights() {
|
||||
const dateFrom = formatDateTime(insightFilters.dateFrom, "start");
|
||||
const dateTo = formatDateTime(insightFilters.dateTo, "end");
|
||||
if (dateFrom && dateTo && new Date(dateFrom.replace(" ", "T")) > new Date(dateTo.replace(" ", "T"))) {
|
||||
return ElMessage.warning("开始时间不能晚于结束时间");
|
||||
}
|
||||
insightExporting.value = true;
|
||||
try {
|
||||
const fromLabel = insightFilters.dateFrom.slice(0, 10).replaceAll("-", "") || "全部";
|
||||
const toLabel = insightFilters.dateTo.slice(0, 10).replaceAll("-", "") || "全部";
|
||||
await api.exportQuestionInsights(
|
||||
{
|
||||
dateFrom,
|
||||
dateTo,
|
||||
minCount: insightFilters.minCount,
|
||||
maxMessages: insightFilters.maxMessages,
|
||||
},
|
||||
`问题洞察_${fromLabel}_${toLabel}.xlsx`,
|
||||
);
|
||||
ElMessage.success("问题洞察已导出");
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : "问题洞察导出失败");
|
||||
} finally {
|
||||
insightExporting.value = false;
|
||||
}
|
||||
}
|
||||
function openChat(sessionId: number) {
|
||||
detailSessionId.value = sessionId;
|
||||
chatDetailOpen.value = true;
|
||||
@@ -355,6 +382,7 @@ function formatMoney(value?: number | null, currency = "CNY") {
|
||||
@click="refreshInsights"
|
||||
>刷新并统计</el-button
|
||||
><el-button @click="resetInsightFilters">重置</el-button>
|
||||
<el-button :loading="insightExporting" @click="exportInsights">导出洞察</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="question-insight-help">
|
||||
|
||||
@@ -20,6 +20,8 @@ import {
|
||||
ElOption,
|
||||
ElPagination,
|
||||
ElProgress,
|
||||
ElRadioButton,
|
||||
ElRadioGroup,
|
||||
ElSelect,
|
||||
ElSlider,
|
||||
ElSwitch,
|
||||
@@ -54,6 +56,8 @@ import "element-plus/theme-chalk/el-option.css";
|
||||
import "element-plus/theme-chalk/el-overlay.css";
|
||||
import "element-plus/theme-chalk/el-pagination.css";
|
||||
import "element-plus/theme-chalk/el-progress.css";
|
||||
import "element-plus/theme-chalk/el-radio-button.css";
|
||||
import "element-plus/theme-chalk/el-radio-group.css";
|
||||
import "element-plus/theme-chalk/el-popper.css";
|
||||
import "element-plus/theme-chalk/el-scrollbar.css";
|
||||
import "element-plus/theme-chalk/el-select.css";
|
||||
@@ -95,6 +99,8 @@ app.config.globalProperties.$ELEMENT = { locale: zhCn };
|
||||
ElOption,
|
||||
ElPagination,
|
||||
ElProgress,
|
||||
ElRadioButton,
|
||||
ElRadioGroup,
|
||||
ElSelect,
|
||||
ElSlider,
|
||||
ElSwitch,
|
||||
@@ -108,6 +114,9 @@ app.config.globalProperties.$ELEMENT = { locale: zhCn };
|
||||
].forEach((component) => {
|
||||
app.use(component);
|
||||
});
|
||||
// Element Plus 2.11 exposes radio group/button without an installer; register them explicitly.
|
||||
app.component("ElRadioButton", ElRadioButton);
|
||||
app.component("ElRadioGroup", ElRadioGroup);
|
||||
|
||||
app.directive("loading", vLoading);
|
||||
app.mount("#app");
|
||||
|
||||
@@ -261,6 +261,8 @@ export const api = {
|
||||
agentRuntimeConfig: () => request<AgentRuntimeConfig>("/admin/agent/runtime-config"),
|
||||
saveAgentRuntimeConfig: (payload: AgentGenerationConfig) =>
|
||||
request<AgentRuntimeConfig>("/admin/agent/runtime-config", { method: "PUT", body: JSON.stringify(payload) }),
|
||||
agentKnowledgeOptions: () => request<KnowledgeItem[]>("/admin/agent/knowledge-options"),
|
||||
agentModelOptions: () => request<ModelItem[]>("/admin/agent/model-options"),
|
||||
downloadAgentBatchTemplate: () => download("/admin/agent/batch/template", "Agent批量测试导入模板.xlsx"),
|
||||
createAgentBatch: (file: File, config: Record<string, unknown>) => {
|
||||
const formData = new FormData();
|
||||
@@ -334,6 +336,8 @@ export const api = {
|
||||
request<QuestionInsightRefreshResult>(`/admin/question-insights/refresh${queryString(query)}`, {
|
||||
method: "POST",
|
||||
}),
|
||||
exportQuestionInsights: (query: { dateFrom?: string; dateTo?: string; minCount?: number; maxMessages?: number } = {}, filename = "问题洞察.xlsx") =>
|
||||
download(`/admin/question-insights/export${queryString(query)}`, filename),
|
||||
retrievalLogs: (query: { page?: number; pageSize?: number } = {}) => request<PageResult<RetrievalLogItem>>(`/admin/retrieval-log/list${queryString(query)}`),
|
||||
estimateRetrievalCleanup: (before: string) => request<{ before: string; estimatedCount: number }>("/admin/retrieval-log/cleanup/estimate", { method: "POST", body: JSON.stringify({ before }) }),
|
||||
cleanupRetrievalLogs: (before: string) => request<{ before: string; deleted: number }>("/admin/retrieval-log/cleanup", { method: "POST", body: JSON.stringify({ before }) }),
|
||||
|
||||
@@ -1061,6 +1061,14 @@ textarea {
|
||||
.agent-batch-config-summary small { margin-bottom: 4px; color: #7a8c85; font-size: 11px; }
|
||||
.agent-batch-config-summary strong { overflow: hidden; color: #2b4038; font-size: 13px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.agent-batch-upload-row { display: flex; align-items: flex-end; gap: 10px; flex-wrap: wrap; }
|
||||
.agent-batch-mode-setting { display: flex; align-items: center; gap: 14px; padding: 12px; border: 1px solid #dfe8e5; border-radius: 9px; background: #fff; }
|
||||
.agent-batch-mode-copy { display: grid; min-width: 240px; flex: 1; gap: 3px; }
|
||||
.agent-batch-mode-copy strong { color: #2b4038; font-size: 13px; }
|
||||
.agent-batch-mode-copy span { color: #71827c; font-size: 11px; line-height: 1.5; }
|
||||
.agent-batch-mode-options { flex: none; }
|
||||
.agent-batch-concurrency { display: flex; align-items: center; gap: 8px; color: #536860; font-size: 12px; }
|
||||
.agent-batch-concurrency > span { white-space: nowrap; }
|
||||
.agent-batch-concurrency .el-input-number { width: 112px; }
|
||||
.agent-batch-name { display: grid; gap: 6px; width: 220px; color: #536860; font-size: 12px; }
|
||||
.agent-batch-name input { height: 40px; padding: 0 12px; border: 1px solid #d8e2df; border-radius: 6px; outline: none; background: #fff; color: #263832; }
|
||||
.agent-batch-name input:focus { border-color: #2f9479; box-shadow: 0 0 0 2px rgba(47, 148, 121, .12); }
|
||||
@@ -2349,6 +2357,11 @@ textarea {
|
||||
|
||||
.agent-batch-config-summary { grid-template-columns: 1fr; }
|
||||
.agent-batch-name, .agent-batch-file { width: 100%; min-width: 0; }
|
||||
.agent-batch-mode-setting { align-items: stretch; flex-direction: column; }
|
||||
.agent-batch-mode-copy { min-width: 0; }
|
||||
.agent-batch-mode-options { display: flex; }
|
||||
.agent-batch-mode-options .el-radio-button { flex: 1; }
|
||||
.agent-batch-mode-options .el-radio-button__inner { width: 100%; }
|
||||
.agent-batch-upload-row > .el-button { flex: 1; }
|
||||
}
|
||||
|
||||
|
||||
@@ -404,6 +404,8 @@ export interface AgentBatchJob {
|
||||
successCount: number;
|
||||
failedCount: number;
|
||||
progress: number;
|
||||
executionMode: "sequential" | "concurrent";
|
||||
concurrencyLimit: number;
|
||||
modelId: number;
|
||||
modelName: string;
|
||||
knowledgeIds: number[];
|
||||
|
||||
Reference in New Issue
Block a user