All checks were successful
CI / test (push) Successful in 4m1s
方案③的定时触发(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 通过
161 lines
6.1 KiB
TypeScript
161 lines
6.1 KiB
TypeScript
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>
|
||
)
|
||
}
|