Files
zclaw_openfang/admin-v2/src/pages/Relay.tsx
iven ba2c6a6105 fix(saas): P1 审计修复 — 连接池断路器 + Worker重试 + XSS防护 + 状态机SQL解析器
P1 修复内容:
- F7: health handler 连接池容量检查 (80%阈值返回503 degraded)
- F9: SSE spawned task 并发限制 (Semaphore 16 permits)
- F10: Key Pool 单次 JOIN 查询优化 (消除 N+1)
- F12: CORS panic → 配置错误
- F14: 连接池使用率计算修正 (ratio = used*100/total)
- F15: SQL 迁移解析器替换为状态机 (支持 $$, DO $body$, 存储过程)
- Worker 重试机制: 失败任务通过 mpsc channel 重新入队
- DOMPurify XSS 防护 (PipelineResultPreview)
- Admin V2: ErrorBoundary + SWR全局配置 + 请求优化
2026-03-30 14:21:39 +08:00

110 lines
3.5 KiB
TypeScript

// ============================================================
// 中转任务
// ============================================================
import { useQuery } from '@tanstack/react-query'
import { Tag, Select, Typography } from 'antd'
import type { ProColumns } from '@ant-design/pro-components'
import { ProTable } from '@ant-design/pro-components'
import { relayService } from '@/services/relay'
import { useState } from 'react'
import type { RelayTask } from '@/types'
const { Title } = Typography
const statusLabels: Record<string, string> = {
queued: '排队中',
running: '运行中',
completed: '已完成',
failed: '失败',
cancelled: '已取消',
}
const statusColors: Record<string, string> = {
queued: 'default',
running: 'processing',
completed: 'green',
failed: 'red',
cancelled: 'default',
}
export default function Relay() {
const [statusFilter, setStatusFilter] = useState<string | undefined>(undefined)
const [page, setPage] = useState(1)
const { data, isLoading } = useQuery({
queryKey: ['relay-tasks', page, statusFilter],
queryFn: ({ signal }) => relayService.list({ page, page_size: 20, status: statusFilter }, signal),
})
const columns: ProColumns<RelayTask>[] = [
{ title: 'ID', dataIndex: 'id', width: 120, render: (_, r) => <code>{r.id.substring(0, 8)}...</code> },
{
title: '状态',
dataIndex: 'status',
width: 100,
render: (_, r) => <Tag color={statusColors[r.status] || 'default'}>{statusLabels[r.status] || r.status}</Tag>,
},
{ title: '模型', dataIndex: 'model_id', width: 160 },
{ title: '优先级', dataIndex: 'priority', width: 70 },
{ title: '尝试次数', dataIndex: 'attempt_count', width: 80 },
{
title: 'Token',
width: 140,
render: (_, r) => `${r.input_tokens.toLocaleString()} / ${r.output_tokens.toLocaleString()}`,
},
{ title: '错误信息', dataIndex: 'error_message', width: 200, ellipsis: true },
{
title: '排队时间',
dataIndex: 'queued_at',
width: 180,
render: (_, r) => new Date(r.queued_at).toLocaleString('zh-CN'),
},
{
title: '完成时间',
dataIndex: 'completed_at',
width: 180,
render: (_, r) => r.completed_at ? new Date(r.completed_at).toLocaleString('zh-CN') : '-',
},
]
return (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}>
<Title level={4} style={{ margin: 0 }}></Title>
<Select
value={statusFilter}
onChange={(v) => { setStatusFilter(v === 'all' ? undefined : v); setPage(1) }}
placeholder="状态筛选"
style={{ width: 140 }}
allowClear
options={[
{ value: 'all', label: '全部' },
{ value: 'queued', label: '排队中' },
{ value: 'running', label: '运行中' },
{ value: 'completed', label: '已完成' },
{ value: 'failed', label: '失败' },
{ value: 'cancelled', label: '已取消' },
]}
/>
</div>
<ProTable<RelayTask>
columns={columns}
dataSource={data?.items ?? []}
loading={isLoading}
rowKey="id"
search={false}
toolBarRender={false}
pagination={{
total: data?.total ?? 0,
pageSize: 20,
current: page,
onChange: setPage,
showSizeChanger: false,
}}
/>
</div>
)
}