AgentEvalTool/frontend/web/src/components/intelligent_eval/TaskQueueMonitor.tsx
sinohqb b4f9c887f4
All checks were successful
CI / test (push) Successful in 4m1s
feat(intelligent-eval): task queue monitor (方案③可视化)
方案③的定时触发(scan loop 每 60s 入队 + 触发 worker)此前只有 Worker
消费端 API,无可查看的列表。新增:
- GET /api/intelligent-evals/tasks:任务明细(含评估名/状态)+ 状态分布统计
  (注册在 /{eval_id} 之前避免被捕获为 eval_id="tasks")
- 前端 TaskQueueMonitor 组件 + 智能评估页任务队列入口:5s 轮询
  (usePolling),状态卡 + 状态筛选 + 明细表
测试:+3(列表/筛选/不被 {eval_id} 遮蔽),892 passed,tsc 通过
2026-08-17 13:57:00 +08:00

161 lines
6.1 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 {
Alert, Button, Empty, Select, Space, Statistic, 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'
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' },
}
const STATUS_OPTIONS = (Object.keys(STATUS_META) as TaskQueueStatus[]).map((s) => ({
value: s,
label: STATUS_META[s].label,
}))
/**
* 任务队列监控(方案③可视化)。
*
* 方案③的"定时触发"scan loop 每 60s 扫描入队 + 触发 OpenClaw worker
* 此前只有 Worker 消费端 API无可查看的列表。这里展示任务队列明细与
* 状态分布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 [statusFilter, setStatusFilter] = useState<TaskQueueStatus | 'all'>('all')
const [loading, setLoading] = useState(false)
const loadData = async () => {
setLoading(true)
try {
const res = await intelligentEvalsApi.listTasks(
statusFilter === 'all' ? undefined : { status: statusFilter },
)
setTasks(res.data.tasks)
setStats(res.data.stats)
} catch {
message.error('加载任务队列失败')
} finally {
setLoading(false)
}
}
// Initial fetch (usePolling owns the 5s interval + visibility pause).
useEffect(() => {
void loadData()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [statusFilter])
usePolling(() => { void loadData() }, 5000, true)
const columns: ColumnsType<TaskQueueItem> = [
{
title: '入队时间', dataIndex: 'created_at', key: 'created_at', width: 170,
render: (v: string | null) => (v ? formatDateTime(v) : '—'),
},
{
title: '评估', dataIndex: 'eval_name', key: 'eval_name',
render: (name: string | null, t) => (
<Space size={6} direction="vertical" style={{ gap: 2 }}>
<span style={{ fontWeight: 500 }}>{name ?? t.eval_id.slice(0, 8)}</span>
{t.eval_status && (
<span style={{ fontSize: 12, color: colors.textSecondary }}>{t.eval_status}</span>
)}
</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',
render: (r: string) => (
<Tooltip title={r}>
<span style={{ display: 'inline-block', maxWidth: 220, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{r}
</span>
</Tooltip>
),
},
{
title: '认领方', dataIndex: 'assigned_cron_id', key: 'assigned_cron_id', width: 140,
render: (v: string | null, t) => {
if (!v) return <span style={{ color: colors.textSecondary }}></span>
return (
<Tooltip title={t.assigned_at ? `认领于 ${formatDateTime(t.assigned_at)}` : undefined}>
<span style={{ fontFamily: 'monospace', fontSize: 12 }}>{v}</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) : '—'
},
},
]
return (
<div style={{ height: '100%', display: 'flex', flexDirection: 'column', gap: 16 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 8 }}>
<Space size={16} wrap>
<Statistic title="待处理" value={stats?.unresolved ?? 0} valueStyle={{ color: colors.warning }} />
<Statistic title="待认领" value={stats?.pending ?? 0} />
<Statistic title="执行中" value={stats?.assigned ?? 0} valueStyle={{ color: colors.primary }} />
<Statistic title="已完成" value={stats?.completed ?? 0} valueStyle={{ color: statusColors.completed }} />
<Statistic title="失败" value={stats?.failed ?? 0} valueStyle={{ color: statusColors.failed }} />
</Space>
<Space>
<Select
value={statusFilter}
onChange={(v) => setStatusFilter(v)}
style={{ width: 110 }}
options={[{ value: 'all', label: '全部状态' }, ...STATUS_OPTIONS]}
/>
<Button icon={<ReloadOutlined />} onClick={() => void loadData()} />
</Space>
</div>
<Alert
type="info"
showIcon
message="平台每 60 秒扫描 executing 评估并入队,有任务时通过 docker exec 触发 OpenClaw worker 执行(取代外部 Channel 的方案③)。"
style={{ fontSize: 12 }}
/>
<div style={{ flex: 1, overflowY: 'auto' }}>
<Table
rowKey="id"
size="small"
loading={loading}
dataSource={tasks ?? []}
columns={columns}
pagination={(tasks?.length ?? 0) > 50 ? { pageSize: 50, showTotal: (t) => `${t}` } : false}
locale={{ emptyText: <Empty description="暂无任务" /> }}
/>
</div>
</div>
)
}