From 66abbc986b9a4d21713751ab378281c7e79792c8 Mon Sep 17 00:00:00 2001 From: Wenjie Zhang Date: Sat, 25 Oct 2025 14:55:06 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E6=8E=A5=E5=8F=A3=EF=BC=8C=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E9=99=90=E5=88=B6=E5=8F=82=E6=95=B0=E5=B9=B6=E8=BF=94=E5=9B=9E?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E6=91=98=E8=A6=81=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/routers/task_router.py | 4 ++-- server/services/tasker.py | 27 +++++++++++++++++---- test/api/test_task_router.py | 2 ++ web/src/components/TaskCenterDrawer.vue | 29 +++++++++++++---------- web/src/stores/tasker.js | 31 ++++++++++++++++++++++++- 5 files changed, 74 insertions(+), 19 deletions(-) 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,