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,