diff --git a/server/routers/task_router.py b/server/routers/task_router.py index c4c4246d..fc692b6b 100644 --- a/server/routers/task_router.py +++ b/server/routers/task_router.py @@ -10,11 +10,11 @@ tasks = APIRouter(prefix="/tasks", tags=["tasks"]) @tasks.get("") async def list_tasks( status: str | None = Query(default=None), + limit: int = Query(default=100, ge=1, le=100), current_user: User = Depends(get_admin_user), ): """List tasks, optionally filtered by status.""" - task_list = await tasker.list_tasks(status=status) - return {"tasks": task_list} + return await tasker.list_tasks(status=status, limit=limit) @tasks.get("/{task_id}") diff --git a/server/services/tasker.py b/server/services/tasker.py index 0e500ceb..657d1fa2 100644 --- a/server/services/tasker.py +++ b/server/services/tasker.py @@ -6,6 +6,7 @@ from dataclasses import asdict, dataclass, field from pathlib import Path from typing import Any from collections.abc import Awaitable, Callable +from collections import Counter from src.config import config from src.utils.logging_config import logger @@ -137,13 +138,31 @@ class Tasker: logger.info("Enqueued task {} ({})", task_id, name) return task - async def list_tasks(self, status: str | None = None) -> list[dict[str, Any]]: + async def list_tasks(self, status: str | None = None, limit: int = 100) -> dict[str, Any]: async with self._lock: - tasks = list(self._tasks.values()) + all_tasks = list(self._tasks.values()) + + status_counter = Counter(task.status for task in all_tasks) + type_counter = Counter(task.type for task in all_tasks) + all_tasks.sort(key=lambda item: item.created_at or utc_isoformat(), reverse=True) + + tasks = all_tasks if status: tasks = [task for task in tasks if task.status == status] - tasks.sort(key=lambda item: item.created_at or utc_isoformat(), reverse=True) - return [task.to_dict() for task in tasks] + + limited_tasks = tasks[: max(limit, 0)] + + summary: dict[str, Any] = { + "total": len(all_tasks), + "filtered_total": len(tasks), + "status_counts": dict(status_counter), + "type_counts": dict(type_counter), + } + + return { + "tasks": [task.to_dict() for task in limited_tasks], + "summary": summary, + } async def get_task(self, task_id: str) -> dict[str, Any] | None: async with self._lock: diff --git a/test/api/test_task_router.py b/test/api/test_task_router.py index 8080f25d..250ba1b7 100644 --- a/test/api/test_task_router.py +++ b/test/api/test_task_router.py @@ -33,6 +33,8 @@ async def test_admin_can_list_tasks(test_client, admin_headers): payload = response.json() assert "tasks" in payload assert isinstance(payload["tasks"], list) + assert "summary" in payload + assert isinstance(payload["summary"], dict) async def test_cancel_unknown_task_returns_client_error(test_client, admin_headers): diff --git a/web/src/components/TaskCenterDrawer.vue b/web/src/components/TaskCenterDrawer.vue index 1bffdd66..eaa6bdd3 100644 --- a/web/src/components/TaskCenterDrawer.vue +++ b/web/src/components/TaskCenterDrawer.vue @@ -98,7 +98,7 @@
🗂️
暂无任务
-
当你提交知识库导入或其他后台任务时,会在这里展示实时进度。
+
当你提交知识库导入或其他后台任务时,会在这里展示实时进度(仅展示最近的 100 个任务)。
@@ -112,27 +112,32 @@ import { storeToRefs } from 'pinia' import { formatFullDateTime, formatRelative, parseToShanghai } from '@/utils/time' const taskerStore = useTaskerStore() -const { isDrawerOpen, sortedTasks, loading, lastError } = storeToRefs(taskerStore) +const { + isDrawerOpen, + sortedTasks, + loading, + lastError, + activeCount, + totalCount, + successCount, + failedCount +} = storeToRefs(taskerStore) const isOpen = isDrawerOpen const tasks = computed(() => sortedTasks.value) const loadingState = computed(() => Boolean(loading.value)) const lastErrorState = computed(() => lastError.value) const statusFilter = ref('all') -const inProgressCount = computed( - () => tasks.value.filter((task) => ACTIVE_CLASS_STATUSES.has(task.status)).length -) -const completedCount = computed(() => tasks.value.filter((task) => task.status === 'success').length) -const failedCount = computed( - () => tasks.value.filter((task) => FAILED_STATUSES.has(task.status)).length -) -const totalCount = computed(() => tasks.value.length) +const inProgressCount = computed(() => activeCount.value || 0) +const completedCount = computed(() => successCount.value || 0) +const failedTaskCount = computed(() => failedCount.value || 0) +const totalTaskCount = computed(() => totalCount.value || 0) const taskFilterOptions = computed(() => [ { label: () => h('span', { class: 'task-filter-option' }, [ '全部', - h('span', { class: 'filter-count' }, totalCount.value) + h('span', { class: 'filter-count' }, totalTaskCount.value) ]), value: 'all' }, @@ -156,7 +161,7 @@ const taskFilterOptions = computed(() => [ label: () => h('span', { class: 'task-filter-option' }, [ '失败', - h('span', { class: 'filter-count' }, failedCount.value) + h('span', { class: 'filter-count' }, failedTaskCount.value) ]), value: 'failed' } diff --git a/web/src/stores/tasker.js b/web/src/stores/tasker.js index 4618e25c..e1f7b9c5 100644 --- a/web/src/stores/tasker.js +++ b/web/src/stores/tasker.js @@ -5,6 +5,14 @@ import { taskerApi } from '@/apis/tasker' import { parseToShanghai } from '@/utils/time' const ACTIVE_STATUSES = new Set(['pending', 'running', 'queued']) +const FAILED_STATUSES = new Set(['failed', 'cancelled']) + +const createDefaultSummary = () => ({ + total: 0, + filtered_total: 0, + status_counts: {}, + type_counts: {} +}) const toTask = (raw = {}) => ({ id: raw.id, @@ -29,6 +37,7 @@ export const useTaskerStore = defineStore('tasker', () => { const lastError = ref(null) const isPolling = ref(false) const isDrawerOpen = ref(false) + const summary = ref(createDefaultSummary()) let pollingTimer = null const sortedTasks = computed(() => { @@ -42,7 +51,16 @@ export const useTaskerStore = defineStore('tasker', () => { }) }) - const activeCount = computed(() => sortedTasks.value.filter(task => ACTIVE_STATUSES.has(task.status)).length) + const statusCounts = computed(() => summary.value?.status_counts || {}) + + const activeCount = computed(() => + Array.from(ACTIVE_STATUSES).reduce((count, status) => count + (statusCounts.value?.[status] || 0), 0) + ) + const failedCount = computed(() => + Array.from(FAILED_STATUSES).reduce((count, status) => count + (statusCounts.value?.[status] || 0), 0) + ) + const successCount = computed(() => statusCounts.value?.success || 0) + const totalCount = computed(() => summary.value?.total || 0) function upsertTask(rawTask) { if (!rawTask || !rawTask.id) return @@ -61,10 +79,15 @@ export const useTaskerStore = defineStore('tasker', () => { try { const response = await taskerApi.fetchTasks(params) const taskList = response?.tasks || [] + summary.value = { + ...createDefaultSummary(), + ...(response?.summary || {}) + } tasks.value = taskList.map(toTask) } catch (error) { console.error('加载任务列表失败', error) lastError.value = error + summary.value = createDefaultSummary() } finally { loading.value = false } @@ -144,12 +167,18 @@ export const useTaskerStore = defineStore('tasker', () => { tasks.value = [] lastError.value = null isDrawerOpen.value = false + summary.value = createDefaultSummary() } return { isDrawerOpen, tasks, sortedTasks, + summary, + statusCounts, + totalCount, + successCount, + failedCount, loading, lastError, activeCount,