AgentEvalTool/frontend/web/src/components/intelligent_eval/TaskQueueMonitor.tsx
sinohqb 4b66c94969
All checks were successful
CI / test (push) Successful in 3m58s
fix(intelligent-eval): align stat chips so all == sum of states
评估列表:STAT_CHIPS 补 cancelled(已取消 4 条),使 全部18 = 各状态和;
页头 title '智能评估' 统一为 '评估列表'(与菜单一致)。
任务队列:统计条去掉派生的 unresolved(待处理)chip——它与 pending/assigned
重叠重复计数,保留真实四态后 全部8 = 待认领0+执行中1+已完成7+失败0。
tsc 0 错误 vitest 16 passed
2026-08-18 11:03:26 +08:00

169 lines
6.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useEffect, useState } from 'react'
import {
Button, Empty, Space, Table, Tag, Tooltip, message,
} from 'antd'
import type { ColumnsType } from 'antd/es/table'
import { ReloadOutlined } from '@ant-design/icons'
import { intelligentEvalsApi, type TaskQueueItem, type TaskQueueStatus } from '../../api'
import { usePolling } from '../../hooks/usePolling'
import { colors, statusColors } from '../../tokens'
import { formatDateTime } from '../../utils/date'
import { EVAL_STATUS } from './status'
const STATUS_META: Record<TaskQueueStatus, { label: string; color: string }> = {
pending: { label: '待认领', color: 'orange' },
assigned: { label: '执行中', color: 'blue' },
completed: { label: '已完成', color: 'green' },
failed: { label: '失败', color: 'red' },
}
type FilterKey = 'all' | 'unresolved' | TaskQueueStatus
/**
* 任务队列监控(方案③可视化,与评估列表页统一 stat-chip + 单行表格)。
*
* 平台每 60s 扫描 executing 评估入队 + 触发 OpenClaw worker 执行。本组件
* 展示任务明细与状态分布5s 轮询;统计条点击筛选。
*/
export default function TaskQueueMonitor() {
const [tasks, setTasks] = useState<TaskQueueItem[] | null>(null)
const [stats, setStats] = useState<{
pending: number; assigned: number; completed: number; failed: number; unresolved: number
} | null>(null)
const [filter, setFilter] = useState<FilterKey>('all')
const [page, setPage] = useState(1)
const [loading, setLoading] = useState(false)
// 一次拉取全部任务筛选在前端完成数据量小unresolved 是 pending+assigned 并集,
// 后端 status 筛选无法一次表达,且 stats 含派生的 unresolved不能直接求和当总数
const loadData = async () => {
setLoading(true)
try {
const res = await intelligentEvalsApi.listTasks()
setTasks(res.data.tasks)
setStats(res.data.stats)
} catch {
message.error('加载任务队列失败')
} finally {
setLoading(false)
}
}
// Initial fetch (usePolling owns the 5s interval). 筛选/分页为前端状态。
useEffect(() => {
void loadData()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
// 筛选变化时回到第 1 页
useEffect(() => {
setPage(1)
}, [filter])
usePolling(() => { void loadData() }, 5000, true)
const allTasks = tasks ?? []
const visibleTasks = filter === 'all'
? allTasks
: filter === 'unresolved'
? allTasks.filter((t) => t.status === 'pending' || t.status === 'assigned')
: allTasks.filter((t) => t.status === filter)
// 全部任务 = 四态之和(不含派生的 unresolved与后端 stats 对齐
const totalAll = stats ? stats.pending + stats.assigned + stats.completed + stats.failed : allTasks.length
// 统计条只展示真实四态unresolved 是派生字段,与 pending/assigned 重叠会重复计数)
const chipMeta: { key: FilterKey; label: string; color?: string; get: () => number }[] = [
{ key: 'all', label: '全部任务', get: () => totalAll },
{ key: 'pending', label: '待认领', get: () => stats?.pending ?? 0 },
{ key: 'assigned', label: '执行中', color: colors.primary, get: () => stats?.assigned ?? 0 },
{ key: 'completed', label: '已完成', color: statusColors.completed, get: () => stats?.completed ?? 0 },
{ key: 'failed', label: '失败', color: statusColors.failed, get: () => stats?.failed ?? 0 },
]
const columns: ColumnsType<TaskQueueItem> = [
{
title: '入队时间', dataIndex: 'created_at', key: 'created_at', width: 170,
render: (v: string | null) => (v ? formatDateTime(v) : '—'),
},
{
title: '评估', key: 'eval', width: 300, ellipsis: true,
render: (_, t) => (
<Space size={6}>
<span style={{ fontWeight: 500 }}>{t.eval_name ?? t.eval_id.slice(0, 8)}</span>
{t.eval_status && EVAL_STATUS[t.eval_status as keyof typeof EVAL_STATUS] && (
<Tag color={EVAL_STATUS[t.eval_status as keyof typeof EVAL_STATUS].color} style={{ margin: 0, fontSize: 11 }}>
{EVAL_STATUS[t.eval_status as keyof typeof EVAL_STATUS].label}
</Tag>
)}
</Space>
),
},
{
title: '队列状态', dataIndex: 'status', key: 'status', width: 100,
render: (s: TaskQueueStatus) => {
const meta = STATUS_META[s]
return <Tag color={meta.color}>{meta.label}</Tag>
},
},
{
title: '优先级', dataIndex: 'priority', key: 'priority', width: 80,
render: (p: number) => <Tag>{p}</Tag>,
},
{
title: '原因', dataIndex: 'reason', key: 'reason', width: 240, ellipsis: true,
render: (r: string) => <Tooltip title={r}><span>{r}</span></Tooltip>,
},
{
title: '完成时间', dataIndex: 'completed_at', key: 'completed_at', width: 170,
render: (v: string | null, t) => {
if (t.status === 'failed' && t.error) {
return <Tooltip title={t.error}><span style={{ color: statusColors.failed }}></span></Tooltip>
}
return v ? formatDateTime(v) : <span style={{ color: colors.textMuted }}></span>
},
},
]
return (
<div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
<div className="stat-bar">
{chipMeta.map((c) => (
<div
key={c.key}
className={`stat-chip ${filter === c.key ? 'active' : ''}`}
onClick={() => setFilter(c.key)}
>
<div className="n" style={{ color: c.color ?? colors.text }}>{c.get()}</div>
<div className="l">{c.label}</div>
</div>
))}
</div>
<div style={{ background: colors.bgContainer, borderRadius: 8, padding: 16, flex: 1, minHeight: 0, overflow: 'hidden' }}>
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 12 }}>
<Button icon={<ReloadOutlined />} onClick={() => void loadData()} />
</div>
<div style={{ height: 'calc(100% - 44px)', overflowY: 'auto' }}>
<Table
rowKey="id"
loading={loading}
dataSource={visibleTasks}
columns={columns}
pagination={{
current: page,
pageSize: 10,
total: visibleTasks.length,
showSizeChanger: true,
pageSizeOptions: [10, 20, 50],
showTotal: (t) => `${t}`,
onChange: (p) => setPage(p),
}}
locale={{ emptyText: <Empty description="暂无任务" /> }}
/>
</div>
</div>
</div>
)
}