feat: 更新任务列表接口,增加限制参数并返回任务摘要信息
This commit is contained in:
parent
12bfc58709
commit
66abbc986b
@ -10,11 +10,11 @@ tasks = APIRouter(prefix="/tasks", tags=["tasks"])
|
|||||||
@tasks.get("")
|
@tasks.get("")
|
||||||
async def list_tasks(
|
async def list_tasks(
|
||||||
status: str | None = Query(default=None),
|
status: str | None = Query(default=None),
|
||||||
|
limit: int = Query(default=100, ge=1, le=100),
|
||||||
current_user: User = Depends(get_admin_user),
|
current_user: User = Depends(get_admin_user),
|
||||||
):
|
):
|
||||||
"""List tasks, optionally filtered by status."""
|
"""List tasks, optionally filtered by status."""
|
||||||
task_list = await tasker.list_tasks(status=status)
|
return await tasker.list_tasks(status=status, limit=limit)
|
||||||
return {"tasks": task_list}
|
|
||||||
|
|
||||||
|
|
||||||
@tasks.get("/{task_id}")
|
@tasks.get("/{task_id}")
|
||||||
|
|||||||
@ -6,6 +6,7 @@ from dataclasses import asdict, dataclass, field
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from collections.abc import Awaitable, Callable
|
from collections.abc import Awaitable, Callable
|
||||||
|
from collections import Counter
|
||||||
|
|
||||||
from src.config import config
|
from src.config import config
|
||||||
from src.utils.logging_config import logger
|
from src.utils.logging_config import logger
|
||||||
@ -137,13 +138,31 @@ class Tasker:
|
|||||||
logger.info("Enqueued task {} ({})", task_id, name)
|
logger.info("Enqueued task {} ({})", task_id, name)
|
||||||
return task
|
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:
|
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:
|
if status:
|
||||||
tasks = [task for task in tasks if task.status == 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 def get_task(self, task_id: str) -> dict[str, Any] | None:
|
||||||
async with self._lock:
|
async with self._lock:
|
||||||
|
|||||||
@ -33,6 +33,8 @@ async def test_admin_can_list_tasks(test_client, admin_headers):
|
|||||||
payload = response.json()
|
payload = response.json()
|
||||||
assert "tasks" in payload
|
assert "tasks" in payload
|
||||||
assert isinstance(payload["tasks"], list)
|
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):
|
async def test_cancel_unknown_task_returns_client_error(test_client, admin_headers):
|
||||||
|
|||||||
@ -98,7 +98,7 @@
|
|||||||
<div v-else class="task-empty">
|
<div v-else class="task-empty">
|
||||||
<div class="task-empty-icon">🗂️</div>
|
<div class="task-empty-icon">🗂️</div>
|
||||||
<div class="task-empty-title">暂无任务</div>
|
<div class="task-empty-title">暂无任务</div>
|
||||||
<div class="task-empty-subtitle">当你提交知识库导入或其他后台任务时,会在这里展示实时进度。</div>
|
<div class="task-empty-subtitle">当你提交知识库导入或其他后台任务时,会在这里展示实时进度(仅展示最近的 100 个任务)。</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a-drawer>
|
</a-drawer>
|
||||||
@ -112,27 +112,32 @@ import { storeToRefs } from 'pinia'
|
|||||||
import { formatFullDateTime, formatRelative, parseToShanghai } from '@/utils/time'
|
import { formatFullDateTime, formatRelative, parseToShanghai } from '@/utils/time'
|
||||||
|
|
||||||
const taskerStore = useTaskerStore()
|
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 isOpen = isDrawerOpen
|
||||||
|
|
||||||
const tasks = computed(() => sortedTasks.value)
|
const tasks = computed(() => sortedTasks.value)
|
||||||
const loadingState = computed(() => Boolean(loading.value))
|
const loadingState = computed(() => Boolean(loading.value))
|
||||||
const lastErrorState = computed(() => lastError.value)
|
const lastErrorState = computed(() => lastError.value)
|
||||||
const statusFilter = ref('all')
|
const statusFilter = ref('all')
|
||||||
const inProgressCount = computed(
|
const inProgressCount = computed(() => activeCount.value || 0)
|
||||||
() => tasks.value.filter((task) => ACTIVE_CLASS_STATUSES.has(task.status)).length
|
const completedCount = computed(() => successCount.value || 0)
|
||||||
)
|
const failedTaskCount = computed(() => failedCount.value || 0)
|
||||||
const completedCount = computed(() => tasks.value.filter((task) => task.status === 'success').length)
|
const totalTaskCount = computed(() => totalCount.value || 0)
|
||||||
const failedCount = computed(
|
|
||||||
() => tasks.value.filter((task) => FAILED_STATUSES.has(task.status)).length
|
|
||||||
)
|
|
||||||
const totalCount = computed(() => tasks.value.length)
|
|
||||||
const taskFilterOptions = computed(() => [
|
const taskFilterOptions = computed(() => [
|
||||||
{
|
{
|
||||||
label: () =>
|
label: () =>
|
||||||
h('span', { class: 'task-filter-option' }, [
|
h('span', { class: 'task-filter-option' }, [
|
||||||
'全部',
|
'全部',
|
||||||
h('span', { class: 'filter-count' }, totalCount.value)
|
h('span', { class: 'filter-count' }, totalTaskCount.value)
|
||||||
]),
|
]),
|
||||||
value: 'all'
|
value: 'all'
|
||||||
},
|
},
|
||||||
@ -156,7 +161,7 @@ const taskFilterOptions = computed(() => [
|
|||||||
label: () =>
|
label: () =>
|
||||||
h('span', { class: 'task-filter-option' }, [
|
h('span', { class: 'task-filter-option' }, [
|
||||||
'失败',
|
'失败',
|
||||||
h('span', { class: 'filter-count' }, failedCount.value)
|
h('span', { class: 'filter-count' }, failedTaskCount.value)
|
||||||
]),
|
]),
|
||||||
value: 'failed'
|
value: 'failed'
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,14 @@ import { taskerApi } from '@/apis/tasker'
|
|||||||
import { parseToShanghai } from '@/utils/time'
|
import { parseToShanghai } from '@/utils/time'
|
||||||
|
|
||||||
const ACTIVE_STATUSES = new Set(['pending', 'running', 'queued'])
|
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 = {}) => ({
|
const toTask = (raw = {}) => ({
|
||||||
id: raw.id,
|
id: raw.id,
|
||||||
@ -29,6 +37,7 @@ export const useTaskerStore = defineStore('tasker', () => {
|
|||||||
const lastError = ref(null)
|
const lastError = ref(null)
|
||||||
const isPolling = ref(false)
|
const isPolling = ref(false)
|
||||||
const isDrawerOpen = ref(false)
|
const isDrawerOpen = ref(false)
|
||||||
|
const summary = ref(createDefaultSummary())
|
||||||
let pollingTimer = null
|
let pollingTimer = null
|
||||||
|
|
||||||
const sortedTasks = computed(() => {
|
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) {
|
function upsertTask(rawTask) {
|
||||||
if (!rawTask || !rawTask.id) return
|
if (!rawTask || !rawTask.id) return
|
||||||
@ -61,10 +79,15 @@ export const useTaskerStore = defineStore('tasker', () => {
|
|||||||
try {
|
try {
|
||||||
const response = await taskerApi.fetchTasks(params)
|
const response = await taskerApi.fetchTasks(params)
|
||||||
const taskList = response?.tasks || []
|
const taskList = response?.tasks || []
|
||||||
|
summary.value = {
|
||||||
|
...createDefaultSummary(),
|
||||||
|
...(response?.summary || {})
|
||||||
|
}
|
||||||
tasks.value = taskList.map(toTask)
|
tasks.value = taskList.map(toTask)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载任务列表失败', error)
|
console.error('加载任务列表失败', error)
|
||||||
lastError.value = error
|
lastError.value = error
|
||||||
|
summary.value = createDefaultSummary()
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
@ -144,12 +167,18 @@ export const useTaskerStore = defineStore('tasker', () => {
|
|||||||
tasks.value = []
|
tasks.value = []
|
||||||
lastError.value = null
|
lastError.value = null
|
||||||
isDrawerOpen.value = false
|
isDrawerOpen.value = false
|
||||||
|
summary.value = createDefaultSummary()
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isDrawerOpen,
|
isDrawerOpen,
|
||||||
tasks,
|
tasks,
|
||||||
sortedTasks,
|
sortedTasks,
|
||||||
|
summary,
|
||||||
|
statusCounts,
|
||||||
|
totalCount,
|
||||||
|
successCount,
|
||||||
|
failedCount,
|
||||||
loading,
|
loading,
|
||||||
lastError,
|
lastError,
|
||||||
activeCount,
|
activeCount,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user