diff --git a/server/routers/evaluation_router.py b/server/routers/evaluation_router.py
index 5fee1ff2..ee635ce2 100644
--- a/server/routers/evaluation_router.py
+++ b/server/routers/evaluation_router.py
@@ -1,6 +1,7 @@
import traceback
from fastapi import APIRouter, HTTPException, Depends, File, Form, Body, UploadFile
+from fastapi.responses import FileResponse
from src.storage.postgres.models_business import User
from server.utils.auth_middleware import get_admin_user
from src.utils import logger
@@ -53,6 +54,29 @@ async def delete_evaluation_benchmark(benchmark_id: str, current_user: User = De
raise HTTPException(status_code=500, detail=f"删除评估基准失败: {str(e)}")
+@evaluation.get("/benchmarks/{benchmark_id}/download")
+async def download_evaluation_benchmark(benchmark_id: str, current_user: User = Depends(get_admin_user)):
+ """下载评估基准文件"""
+ from src.services.evaluation_service import EvaluationService
+
+ try:
+ service = EvaluationService()
+ download_info = await service.get_benchmark_download_info(benchmark_id)
+ return FileResponse(
+ path=download_info["file_path"],
+ filename=download_info["filename"],
+ media_type="application/x-ndjson",
+ )
+ except ValueError as e:
+ if "not found" in str(e).lower():
+ raise HTTPException(status_code=404, detail=str(e))
+ logger.error(f"下载评估基准失败: {e}, {traceback.format_exc()}")
+ raise HTTPException(status_code=500, detail=f"下载评估基准失败: {str(e)}")
+ except Exception as e:
+ logger.error(f"下载评估基准失败: {e}, {traceback.format_exc()}")
+ raise HTTPException(status_code=500, detail=f"下载评估基准失败: {str(e)}")
+
+
@evaluation.get("/databases/{db_id}/results/{task_id}")
async def get_evaluation_results_by_db(
db_id: str,
diff --git a/src/services/evaluation_service.py b/src/services/evaluation_service.py
index e1b6968b..1c4b42b9 100644
--- a/src/services/evaluation_service.py
+++ b/src/services/evaluation_service.py
@@ -231,6 +231,29 @@ class EvaluationService:
logger.error(f"获取评估基准详情失败: {e}")
raise
+ async def get_benchmark_download_info(self, benchmark_id: str) -> dict[str, str]:
+ """获取评估基准下载信息"""
+ row = await self.eval_repo.get_benchmark(benchmark_id)
+ if row is None:
+ raise ValueError("Benchmark not found")
+
+ data_file_path = row.data_file_path or ""
+ if not data_file_path or not os.path.exists(data_file_path):
+ raise ValueError("Benchmark file not found")
+
+ filename_base = (row.name or "").strip()
+ if not filename_base:
+ filename_base = row.benchmark_id
+
+ filename_base = re.sub(r"[\\/:*?\"<>|]+", "_", filename_base).strip()
+ if not filename_base or filename_base in {".", ".."}:
+ filename_base = row.benchmark_id
+
+ if not filename_base.endswith(".jsonl"):
+ filename_base = f"{filename_base}.jsonl"
+
+ return {"file_path": data_file_path, "filename": filename_base}
+
async def delete_benchmark(self, benchmark_id: str) -> None:
"""删除评估基准"""
try:
diff --git a/test/api/test_evaluation_router.py b/test/api/test_evaluation_router.py
new file mode 100644
index 00000000..c90e6492
--- /dev/null
+++ b/test/api/test_evaluation_router.py
@@ -0,0 +1,62 @@
+"""
+Integration tests for evaluation router endpoints.
+"""
+
+from __future__ import annotations
+
+import uuid
+
+import pytest
+
+pytestmark = [pytest.mark.asyncio, pytest.mark.integration]
+
+
+async def _upload_test_benchmark(test_client, admin_headers: dict[str, str], db_id: str) -> tuple[str, str]:
+ benchmark_name = f"pytest_benchmark_{uuid.uuid4().hex[:8]}"
+ line = '{"query":"什么是单元测试?","gold_answer":"用于验证代码行为的自动化测试"}\n'
+
+ response = await test_client.post(
+ f"/api/evaluation/databases/{db_id}/benchmarks/upload",
+ data={"name": benchmark_name, "description": "pytest benchmark for download"},
+ files={"file": ("pytest_benchmark.jsonl", line.encode("utf-8"), "application/x-ndjson")},
+ headers=admin_headers,
+ )
+ assert response.status_code == 200, response.text
+
+ payload = response.json()
+ assert payload.get("message") == "success"
+ benchmark_id = payload.get("data", {}).get("benchmark_id")
+ assert benchmark_id
+ return benchmark_id, line
+
+
+async def test_download_benchmark_requires_admin(test_client, standard_user):
+ response = await test_client.get(
+ "/api/evaluation/benchmarks/benchmark_fake/download",
+ headers=standard_user["headers"],
+ )
+ assert response.status_code == 403
+
+
+async def test_admin_can_download_benchmark(test_client, admin_headers, knowledge_database):
+ benchmark_id, expected_line = await _upload_test_benchmark(test_client, admin_headers, knowledge_database["db_id"])
+
+ response = await test_client.get(
+ f"/api/evaluation/benchmarks/{benchmark_id}/download",
+ headers=admin_headers,
+ )
+ assert response.status_code == 200, response.text
+ assert "application/x-ndjson" in response.headers.get("content-type", "")
+ assert "attachment" in response.headers.get("content-disposition", "").lower()
+
+ content = response.content.decode("utf-8")
+ assert expected_line.strip() in content
+
+
+async def test_download_benchmark_not_found(test_client, admin_headers):
+ response = await test_client.get(
+ f"/api/evaluation/benchmarks/benchmark_not_found_{uuid.uuid4().hex[:8]}/download",
+ headers=admin_headers,
+ )
+ assert response.status_code == 404, response.text
+
diff --git a/web/src/apis/knowledge_api.js b/web/src/apis/knowledge_api.js
index 8c23230b..7e78794f 100644
--- a/web/src/apis/knowledge_api.js
+++ b/web/src/apis/knowledge_api.js
@@ -453,6 +453,15 @@ export const evaluationApi = {
return apiAdminDelete(`/api/evaluation/benchmarks/${benchmarkId}`)
},
+ /**
+ * 下载评估基准
+ * @param {string} benchmarkId - 基准ID
+ * @returns {Promise} - Response对象
+ */
+ downloadBenchmark: async (benchmarkId) => {
+ return apiAdminGet(`/api/evaluation/benchmarks/${benchmarkId}/download`, {}, 'blob')
+ },
+
/**
* 自动生成评估基准
* @param {string} dbId - 知识库ID
diff --git a/web/src/components/EvaluationBenchmarks.vue b/web/src/components/EvaluationBenchmarks.vue
index 667d9c80..5edf108b 100644
--- a/web/src/components/EvaluationBenchmarks.vue
+++ b/web/src/components/EvaluationBenchmarks.vue
@@ -48,6 +48,14 @@
+
+
+
@@ -196,6 +204,7 @@ import {
UploadOutlined,
RobotOutlined,
EyeOutlined,
+ DownloadOutlined,
DeleteOutlined,
CheckCircleOutlined,
CloseCircleOutlined,
@@ -225,6 +234,7 @@ const generateModalVisible = ref(false)
const previewModalVisible = ref(false)
const previewData = ref(null)
const previewQuestions = ref([])
+const downloadingBenchmarkMap = reactive({})
const previewPagination = ref({
current: 1,
pageSize: 10,
@@ -407,6 +417,60 @@ const previewBenchmark = async (benchmark) => {
}
}
+const parseDownloadFilename = (contentDisposition) => {
+ if (!contentDisposition) return ''
+
+ const utf8Match = contentDisposition.match(/filename\*=UTF-8''([^;]+)/i)
+ if (utf8Match && utf8Match[1]) {
+ try {
+ return decodeURIComponent(utf8Match[1])
+ } catch (error) {
+ console.warn('解析 UTF-8 文件名失败:', error)
+ }
+ }
+
+ const asciiMatch = contentDisposition.match(/filename=\"?([^\";]+)\"?/i)
+ if (asciiMatch && asciiMatch[1]) {
+ return asciiMatch[1]
+ }
+
+ return ''
+}
+
+// 下载基准
+const downloadBenchmark = async (benchmark) => {
+ const benchmarkId = benchmark?.benchmark_id
+ if (!benchmarkId) return
+ if (downloadingBenchmarkMap[benchmarkId]) return
+
+ downloadingBenchmarkMap[benchmarkId] = true
+ try {
+ const response = await evaluationApi.downloadBenchmark(benchmarkId)
+ const blob = await response.blob()
+ const contentDisposition =
+ response.headers.get('Content-Disposition') || response.headers.get('content-disposition')
+ const headerFilename = parseDownloadFilename(contentDisposition)
+ const fallbackFilename = `${benchmark.name || benchmarkId}.jsonl`
+ const filename = headerFilename || fallbackFilename
+
+ const url = window.URL.createObjectURL(blob)
+ const link = document.createElement('a')
+ link.href = url
+ link.download = filename
+ document.body.appendChild(link)
+ link.click()
+ document.body.removeChild(link)
+ window.URL.revokeObjectURL(url)
+
+ message.success('下载成功')
+ } catch (error) {
+ console.error('下载基准失败:', error)
+ message.error(`下载失败: ${error.message || '未知错误'}`)
+ } finally {
+ delete downloadingBenchmarkMap[benchmarkId]
+ }
+}
+
// 删除基准
const deleteBenchmark = (benchmark) => {
Modal.confirm({