import { ArrowLeftOutlined, ReloadOutlined, StopOutlined, ApiOutlined } from '@ant-design/icons' import { Alert, Button, Card, Descriptions, Space, Tag, Tabs, Typography, Popconfirm, message } from 'antd' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useNavigate, useParams } from 'react-router-dom' import type { ApiError } from '../lib/api' import JsonViewer from '../components/JsonViewer' import { getTask, getTaskResult, retryTask, cancelTask, markTaskRpa, type TaskRecord, type TaskResultResponse } from '../lib/tasks' export default function TaskDetailPage() { const navigate = useNavigate() const params = useParams<{ id: string }>() const id = useMemo(() => params.id ?? '', [params.id]) const [task, setTask] = useState(null) const [result, setResult] = useState(null) const [notReadyBody, setNotReadyBody] = useState(null) const [loading, setLoading] = useState(false) const [polling, setPolling] = useState(false) const [error, setError] = useState(null) const controllerRef = useRef(null) const [actionLoading, setActionLoading] = useState(false) const refresh = useCallback(async () => { if (!id) return setLoading(true) setError(null) try { const taskRes = await getTask(id) setTask(taskRes) } catch (e) { setError(e as ApiError) } finally { setLoading(false) } }, [id]) const handleRetry = async () => { if (!id) return setActionLoading(true) try { await retryTask(id) message.success('任务已重新排队') await refresh() startPolling() } catch (e) { message.error(`重试失败: ${(e as ApiError).message}`) } finally { setActionLoading(false) } } const handleCancel = async () => { if (!id) return setActionLoading(true) try { await cancelTask(id) message.success('任务已取消') await refresh() stopPolling() } catch (e) { message.error(`取消失败: ${(e as ApiError).message}`) } finally { setActionLoading(false) } } const handleMarkRpa = async () => { if (!id) return setActionLoading(true) try { await markTaskRpa(id) message.success('已转为等待 RPA') await refresh() stopPolling() } catch (e) { message.error(`转 RPA 失败: ${(e as ApiError).message}`) } finally { setActionLoading(false) } } useEffect(() => { void refresh() }, [refresh]) function stopPolling() { controllerRef.current?.abort() controllerRef.current = null setPolling(false) } async function sleep(ms: number, signal?: AbortSignal) { await new Promise((resolve, reject) => { const timer = window.setTimeout(() => { signal?.removeEventListener('abort', onAbort) resolve() }, ms) function onAbort() { window.clearTimeout(timer) reject(new DOMException('Aborted', 'AbortError')) } if (signal) { if (signal.aborted) return onAbort() signal.addEventListener('abort', onAbort, { once: true }) } }) } const startPolling = useCallback(async () => { if (!id) return stopPolling() setPolling(true) setError(null) setResult(null) setNotReadyBody(null) const controller = new AbortController() controllerRef.current = controller const signal = controller.signal try { while (true) { const res = await getTaskResult(id, signal) if (res.ready) { setResult(res.data) setNotReadyBody(null) setPolling(false) controllerRef.current = null return } setNotReadyBody(res.body) await sleep(1500, signal) } } catch (e) { if (e instanceof DOMException && e.name === 'AbortError') return setError(e as ApiError) setPolling(false) controllerRef.current = null } }, [id]) useEffect(() => { if (!id) return undefined void startPolling() return () => stopPolling() }, [id, startPolling]) function formatTs(value: number | null | undefined) { if (!value) return '-' const d = new Date(value * 1000) return Number.isFinite(d.getTime()) ? d.toLocaleString() : '-' } return (
Task: {id} {error ? ( ) : null} {task?.status === 'failed' && ( )} {task?.status === 'failed' && ( )} {(task?.status === 'queued' || task?.status === 'running' || task?.status === 'retrying' || task?.status === 'fallback_running') && ( )} } > {task ? (
{task.id} {task.status} {task.task_type} {task.engine || '-'} {task.target || '-'} {formatTs(task.created)} {formatTs(task.started)} {formatTs(task.finished)} {task.retry_count}
payload
error
) : ( 无数据 )}
{polling ? 轮询中 : null} {!polling && result ? 已就绪 : null} {!polling && !result && notReadyBody ? 未就绪 : null} } > {notReadyBody ? (
) : null} {result ? ( , }, { key: 'normalized', label: 'normalized', children: , }, { key: 'meta', label: 'meta', children: , }, ]} /> ) : null} {!result && !notReadyBody ? 暂无 : null}
) }