Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Batch 1 — User-facing fixes: - B1-1: Pipeline verified end-to-end (14 Rust commands, 8 frontend invoke, fully connected) - B1-2: MessageSearch restored to ChatArea with search button in DeerFlow header - B1-3: Viking cleanup — removed 5 orphan invokes (no Rust impl), added addWithMetadata + storeWithSummaries methods + summary generation UI - B1-4: api-fallbacks transparency — added _isFallback markers + console.warn to all 6 fallback functions Batch 2 — System health: - B2-1: Document drift calibration — TRUTH.md/README.md numbers verified and updated - B2-2: @reserved annotations on 15 SaaS handler functions with no frontend callers - B2-3: Scheduled Task Admin V2 — new service + page + route + sidebar navigation - B2-4: TRUTH.md Pipeline/Viking/ScheduledTask records corrected Batch 3 — Long-term quality: - B3-1: hand_run_status/hand_run_list verified as fully implemented (not stubs) - B3-2: Identity snapshot rollback UI added to RightPanel - B3-3: P2 code quality — 4 fixes (TODO comments, fire-and-forget notes, design notes, table name validation), 2 verified N/A, 1 upstream - B3-4: Config PATCH→PUT alignment (admin-v2 config.ts matched to SaaS backend)
398 lines
11 KiB
TypeScript
398 lines
11 KiB
TypeScript
// ============================================================
|
|
// 定时任务 — 管理页面
|
|
// ============================================================
|
|
|
|
import { useState } from 'react'
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
|
import { Button, message, Tag, Modal, Form, Input, Select, Switch, Popconfirm, Space } from 'antd'
|
|
import type { ProColumns } from '@ant-design/pro-components'
|
|
import { ProTable } from '@ant-design/pro-components'
|
|
import { PlusOutlined } from '@ant-design/icons'
|
|
import { scheduledTaskService } from '@/services/scheduled-tasks'
|
|
import type { ScheduledTask, CreateScheduledTaskRequest, UpdateScheduledTaskRequest } from '@/services/scheduled-tasks'
|
|
import { PageHeader } from '@/components/PageHeader'
|
|
import { ErrorState } from '@/components/ErrorState'
|
|
|
|
const scheduleTypeLabels: Record<string, string> = {
|
|
cron: 'Cron',
|
|
interval: '间隔',
|
|
once: '一次性',
|
|
}
|
|
|
|
const scheduleTypeColors: Record<string, string> = {
|
|
cron: 'blue',
|
|
interval: 'green',
|
|
once: 'orange',
|
|
}
|
|
|
|
const targetTypeLabels: Record<string, string> = {
|
|
agent: 'Agent',
|
|
hand: 'Hand',
|
|
workflow: 'Workflow',
|
|
}
|
|
|
|
const targetTypeColors: Record<string, string> = {
|
|
agent: 'purple',
|
|
hand: 'cyan',
|
|
workflow: 'geekblue',
|
|
}
|
|
|
|
function formatDateTime(value: string | null): string {
|
|
if (!value) return '-'
|
|
return new Date(value).toLocaleString('zh-CN')
|
|
}
|
|
|
|
function formatDuration(ms: number | null): string {
|
|
if (ms === null) return '-'
|
|
if (ms < 1000) return `${ms}ms`
|
|
return `${(ms / 1000).toFixed(2)}s`
|
|
}
|
|
|
|
export default function ScheduledTasks() {
|
|
const queryClient = useQueryClient()
|
|
const [form] = Form.useForm()
|
|
const [modalOpen, setModalOpen] = useState(false)
|
|
const [editingId, setEditingId] = useState<string | null>(null)
|
|
|
|
const { data, isLoading, error, refetch } = useQuery({
|
|
queryKey: ['scheduled-tasks'],
|
|
queryFn: ({ signal }) => scheduledTaskService.list(signal),
|
|
})
|
|
|
|
const createMutation = useMutation({
|
|
mutationFn: (data: CreateScheduledTaskRequest) => scheduledTaskService.create(data),
|
|
onSuccess: () => {
|
|
message.success('任务创建成功')
|
|
queryClient.invalidateQueries({ queryKey: ['scheduled-tasks'] })
|
|
closeModal()
|
|
},
|
|
onError: (err: Error) => message.error(err.message || '创建失败'),
|
|
})
|
|
|
|
const updateMutation = useMutation({
|
|
mutationFn: ({ id, data }: { id: string; data: UpdateScheduledTaskRequest }) =>
|
|
scheduledTaskService.update(id, data),
|
|
onSuccess: () => {
|
|
message.success('任务更新成功')
|
|
queryClient.invalidateQueries({ queryKey: ['scheduled-tasks'] })
|
|
closeModal()
|
|
},
|
|
onError: (err: Error) => message.error(err.message || '更新失败'),
|
|
})
|
|
|
|
const deleteMutation = useMutation({
|
|
mutationFn: (id: string) => scheduledTaskService.delete(id),
|
|
onSuccess: () => {
|
|
message.success('任务已删除')
|
|
queryClient.invalidateQueries({ queryKey: ['scheduled-tasks'] })
|
|
},
|
|
onError: (err: Error) => message.error(err.message || '删除失败'),
|
|
})
|
|
|
|
const toggleMutation = useMutation({
|
|
mutationFn: ({ id, enabled }: { id: string; enabled: boolean }) =>
|
|
scheduledTaskService.update(id, { enabled }),
|
|
onSuccess: () => {
|
|
message.success('状态已更新')
|
|
queryClient.invalidateQueries({ queryKey: ['scheduled-tasks'] })
|
|
},
|
|
onError: (err: Error) => message.error(err.message || '状态更新失败'),
|
|
})
|
|
|
|
const columns: ProColumns<ScheduledTask>[] = [
|
|
{
|
|
title: '任务名称',
|
|
dataIndex: 'name',
|
|
width: 160,
|
|
ellipsis: true,
|
|
},
|
|
{
|
|
title: '调度规则',
|
|
dataIndex: 'schedule',
|
|
width: 140,
|
|
ellipsis: true,
|
|
hideInSearch: true,
|
|
},
|
|
{
|
|
title: '调度类型',
|
|
dataIndex: 'schedule_type',
|
|
width: 100,
|
|
valueType: 'select',
|
|
valueEnum: {
|
|
cron: { text: 'Cron' },
|
|
interval: { text: '间隔' },
|
|
once: { text: '一次性' },
|
|
},
|
|
render: (_, record) => (
|
|
<Tag color={scheduleTypeColors[record.schedule_type]}>
|
|
{scheduleTypeLabels[record.schedule_type] || record.schedule_type}
|
|
</Tag>
|
|
),
|
|
},
|
|
{
|
|
title: '目标',
|
|
dataIndex: ['target', 'type'],
|
|
width: 140,
|
|
hideInSearch: true,
|
|
render: (_, record) => (
|
|
<Space size={4}>
|
|
<Tag color={targetTypeColors[record.target.type]}>
|
|
{targetTypeLabels[record.target.type] || record.target.type}
|
|
</Tag>
|
|
<span className="text-xs text-neutral-500 dark:text-neutral-400">{record.target.id}</span>
|
|
</Space>
|
|
),
|
|
},
|
|
{
|
|
title: '启用',
|
|
dataIndex: 'enabled',
|
|
width: 80,
|
|
hideInSearch: true,
|
|
render: (_, record) => (
|
|
<Switch
|
|
size="small"
|
|
checked={record.enabled}
|
|
onChange={(checked) => toggleMutation.mutate({ id: record.id, enabled: checked })}
|
|
/>
|
|
),
|
|
},
|
|
{
|
|
title: '执行次数',
|
|
dataIndex: 'run_count',
|
|
width: 90,
|
|
hideInSearch: true,
|
|
render: (_, record) => (
|
|
<span className="tabular-nums">{record.run_count}</span>
|
|
),
|
|
},
|
|
{
|
|
title: '上次执行',
|
|
dataIndex: 'last_run',
|
|
width: 170,
|
|
hideInSearch: true,
|
|
render: (_, record) => formatDateTime(record.last_run),
|
|
},
|
|
{
|
|
title: '下次执行',
|
|
dataIndex: 'next_run',
|
|
width: 170,
|
|
hideInSearch: true,
|
|
render: (_, record) => formatDateTime(record.next_run),
|
|
},
|
|
{
|
|
title: '上次耗时',
|
|
dataIndex: 'last_duration_ms',
|
|
width: 100,
|
|
hideInSearch: true,
|
|
render: (_, record) => formatDuration(record.last_duration_ms),
|
|
},
|
|
{
|
|
title: '上次错误',
|
|
dataIndex: 'last_error',
|
|
width: 160,
|
|
ellipsis: true,
|
|
hideInSearch: true,
|
|
render: (_, record) =>
|
|
record.last_error ? (
|
|
<span className="text-red-500 text-xs">{record.last_error}</span>
|
|
) : (
|
|
<span className="text-neutral-400">-</span>
|
|
),
|
|
},
|
|
{
|
|
title: '操作',
|
|
width: 140,
|
|
hideInSearch: true,
|
|
render: (_, record) => (
|
|
<Space>
|
|
<Button
|
|
size="small"
|
|
onClick={() => openEditModal(record)}
|
|
>
|
|
编辑
|
|
</Button>
|
|
<Popconfirm
|
|
title="确定删除此任务?"
|
|
description="删除后无法恢复"
|
|
onConfirm={() => deleteMutation.mutate(record.id)}
|
|
>
|
|
<Button size="small" danger>删除</Button>
|
|
</Popconfirm>
|
|
</Space>
|
|
),
|
|
},
|
|
]
|
|
|
|
const openCreateModal = () => {
|
|
setEditingId(null)
|
|
form.resetFields()
|
|
form.setFieldsValue({ schedule_type: 'cron', enabled: true })
|
|
setModalOpen(true)
|
|
}
|
|
|
|
const openEditModal = (record: ScheduledTask) => {
|
|
setEditingId(record.id)
|
|
form.setFieldsValue({
|
|
name: record.name,
|
|
schedule: record.schedule,
|
|
schedule_type: record.schedule_type,
|
|
target_type: record.target.type,
|
|
target_id: record.target.id,
|
|
description: record.description ?? '',
|
|
enabled: record.enabled,
|
|
})
|
|
setModalOpen(true)
|
|
}
|
|
|
|
const closeModal = () => {
|
|
setModalOpen(false)
|
|
setEditingId(null)
|
|
form.resetFields()
|
|
}
|
|
|
|
const handleSave = async () => {
|
|
const values = await form.validateFields()
|
|
const payload: CreateScheduledTaskRequest | UpdateScheduledTaskRequest = {
|
|
name: values.name,
|
|
schedule: values.schedule,
|
|
schedule_type: values.schedule_type,
|
|
target: {
|
|
type: values.target_type,
|
|
id: values.target_id,
|
|
},
|
|
description: values.description || undefined,
|
|
enabled: values.enabled,
|
|
}
|
|
|
|
if (editingId) {
|
|
updateMutation.mutate({ id: editingId, data: payload })
|
|
} else {
|
|
createMutation.mutate(payload as CreateScheduledTaskRequest)
|
|
}
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<>
|
|
<PageHeader title="定时任务" description="管理系统定时任务的创建、调度与执行" />
|
|
<ErrorState message={(error as Error).message} onRetry={() => refetch()} />
|
|
</>
|
|
)
|
|
}
|
|
|
|
const tasks = Array.isArray(data) ? data : []
|
|
|
|
return (
|
|
<div>
|
|
<PageHeader
|
|
title="定时任务"
|
|
description="管理系统定时任务的创建、调度与执行"
|
|
actions={
|
|
<Button
|
|
type="primary"
|
|
icon={<PlusOutlined />}
|
|
onClick={openCreateModal}
|
|
>
|
|
新建任务
|
|
</Button>
|
|
}
|
|
/>
|
|
|
|
<ProTable<ScheduledTask>
|
|
columns={columns}
|
|
dataSource={tasks}
|
|
loading={isLoading}
|
|
rowKey="id"
|
|
search={false}
|
|
toolBarRender={() => []}
|
|
pagination={{
|
|
showSizeChanger: true,
|
|
defaultPageSize: 20,
|
|
}}
|
|
options={{
|
|
density: false,
|
|
fullScreen: false,
|
|
reload: () => refetch(),
|
|
}}
|
|
/>
|
|
|
|
<Modal
|
|
title={
|
|
<span className="text-base font-semibold">
|
|
{editingId ? '编辑任务' : '新建任务'}
|
|
</span>
|
|
}
|
|
open={modalOpen}
|
|
onOk={handleSave}
|
|
onCancel={closeModal}
|
|
confirmLoading={createMutation.isPending || updateMutation.isPending}
|
|
width={520}
|
|
destroyOnClose
|
|
>
|
|
<Form form={form} layout="vertical" className="mt-4">
|
|
<Form.Item
|
|
name="name"
|
|
label="任务名称"
|
|
rules={[{ required: true, message: '请输入任务名称' }]}
|
|
>
|
|
<Input placeholder="例如:每日数据汇总" />
|
|
</Form.Item>
|
|
|
|
<Form.Item
|
|
name="schedule_type"
|
|
label="调度类型"
|
|
rules={[{ required: true, message: '请选择调度类型' }]}
|
|
>
|
|
<Select
|
|
options={[
|
|
{ value: 'cron', label: 'Cron 表达式' },
|
|
{ value: 'interval', label: '固定间隔' },
|
|
{ value: 'once', label: '一次性执行' },
|
|
]}
|
|
/>
|
|
</Form.Item>
|
|
|
|
<Form.Item
|
|
name="schedule"
|
|
label="调度规则"
|
|
rules={[{ required: true, message: '请输入调度规则' }]}
|
|
extra="Cron: 0 8 * * * 间隔: 30m / 1h / 24h 一次性: 2025-12-31T00:00:00Z"
|
|
>
|
|
<Input placeholder="0 8 * * *" />
|
|
</Form.Item>
|
|
|
|
<Form.Item
|
|
name="target_type"
|
|
label="目标类型"
|
|
rules={[{ required: true, message: '请选择目标类型' }]}
|
|
>
|
|
<Select
|
|
options={[
|
|
{ value: 'agent', label: 'Agent' },
|
|
{ value: 'hand', label: 'Hand' },
|
|
{ value: 'workflow', label: 'Workflow' },
|
|
]}
|
|
/>
|
|
</Form.Item>
|
|
|
|
<Form.Item
|
|
name="target_id"
|
|
label="目标 ID"
|
|
rules={[{ required: true, message: '请输入目标 ID' }]}
|
|
>
|
|
<Input placeholder="目标唯一标识符" />
|
|
</Form.Item>
|
|
|
|
<Form.Item name="description" label="描述">
|
|
<Input.TextArea rows={3} placeholder="可选的任务描述" />
|
|
</Form.Item>
|
|
|
|
<Form.Item name="enabled" label="启用" valuePropName="checked">
|
|
<Switch />
|
|
</Form.Item>
|
|
</Form>
|
|
</Modal>
|
|
</div>
|
|
)
|
|
}
|