fix(production-readiness): 3-batch production readiness cleanup — 12 tasks
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
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)
This commit is contained in:
@@ -19,6 +19,7 @@ import {
|
||||
BookOutlined,
|
||||
CrownOutlined,
|
||||
SafetyOutlined,
|
||||
FieldTimeOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import { Avatar, Dropdown, Tooltip, Drawer } from 'antd'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
@@ -46,6 +47,7 @@ const navItems: NavItem[] = [
|
||||
{ path: '/api-keys', name: 'API 密钥', icon: <ApiOutlined />, permission: 'provider:manage', group: '资源管理' },
|
||||
{ path: '/usage', name: '用量统计', icon: <BarChartOutlined />, permission: 'admin:full', group: '运维' },
|
||||
{ path: '/relay', name: '中转任务', icon: <SwapOutlined />, permission: 'relay:use', group: '运维' },
|
||||
{ path: '/scheduled-tasks', name: '定时任务', icon: <FieldTimeOutlined />, permission: 'scheduler:read', group: '运维' },
|
||||
{ path: '/knowledge', name: '知识库', icon: <BookOutlined />, permission: 'knowledge:read', group: '资源管理' },
|
||||
{ path: '/billing', name: '计费管理', icon: <CrownOutlined />, permission: 'billing:read', group: '核心' },
|
||||
{ path: '/logs', name: '操作日志', icon: <FileTextOutlined />, permission: 'admin:full', group: '运维' },
|
||||
@@ -211,6 +213,7 @@ const breadcrumbMap: Record<string, string> = {
|
||||
'/agent-templates': 'Agent 模板',
|
||||
'/usage': '用量统计',
|
||||
'/relay': '中转任务',
|
||||
'/scheduled-tasks': '定时任务',
|
||||
'/knowledge': '知识库',
|
||||
'/billing': '计费管理',
|
||||
'/config': '系统配置',
|
||||
|
||||
397
admin-v2/src/pages/ScheduledTasks.tsx
Normal file
397
admin-v2/src/pages/ScheduledTasks.tsx
Normal file
@@ -0,0 +1,397 @@
|
||||
// ============================================================
|
||||
// 定时任务 — 管理页面
|
||||
// ============================================================
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -30,6 +30,7 @@ export const router = createBrowserRouter([
|
||||
{ path: 'usage', lazy: () => import('@/pages/Usage').then((m) => ({ Component: m.default })) },
|
||||
{ path: 'billing', lazy: () => import('@/pages/Billing').then((m) => ({ Component: m.default })) },
|
||||
{ path: 'relay', lazy: () => import('@/pages/Relay').then((m) => ({ Component: m.default })) },
|
||||
{ path: 'scheduled-tasks', lazy: () => import('@/pages/ScheduledTasks').then((m) => ({ Component: m.default })) },
|
||||
{ path: 'knowledge', lazy: () => import('@/pages/Knowledge').then((m) => ({ Component: m.default })) },
|
||||
{ path: 'config', lazy: () => import('@/pages/Config').then((m) => ({ Component: m.default })) },
|
||||
{ path: 'prompts', lazy: () => import('@/pages/Prompts').then((m) => ({ Component: m.default })) },
|
||||
|
||||
71
admin-v2/src/services/scheduled-tasks.ts
Normal file
71
admin-v2/src/services/scheduled-tasks.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
// ============================================================
|
||||
// 定时任务 — Service
|
||||
// ============================================================
|
||||
|
||||
import request, { withSignal } from './request'
|
||||
|
||||
// === Types ===
|
||||
|
||||
export interface TaskTarget {
|
||||
type: string // "agent" | "hand" | "workflow"
|
||||
id: string
|
||||
}
|
||||
|
||||
export interface ScheduledTask {
|
||||
id: string
|
||||
name: string
|
||||
schedule: string
|
||||
schedule_type: string // "cron" | "interval" | "once"
|
||||
target: TaskTarget
|
||||
enabled: boolean
|
||||
description: string | null
|
||||
last_run: string | null
|
||||
next_run: string | null
|
||||
run_count: number
|
||||
last_result: string | null
|
||||
last_error: string | null
|
||||
last_duration_ms: number | null
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface CreateScheduledTaskRequest {
|
||||
name: string
|
||||
schedule: string
|
||||
schedule_type?: string
|
||||
target: TaskTarget
|
||||
description?: string
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
export interface UpdateScheduledTaskRequest {
|
||||
name?: string
|
||||
schedule?: string
|
||||
schedule_type?: string
|
||||
target?: TaskTarget
|
||||
description?: string
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
// === Service ===
|
||||
|
||||
export const scheduledTaskService = {
|
||||
list: (signal?: AbortSignal) =>
|
||||
request.get<ScheduledTask[]>('/scheduler/tasks', withSignal({}, signal))
|
||||
.then((r) => r.data),
|
||||
|
||||
get: (id: string, signal?: AbortSignal) =>
|
||||
request.get<ScheduledTask>(`/scheduler/tasks/${id}`, withSignal({}, signal))
|
||||
.then((r) => r.data),
|
||||
|
||||
create: (data: CreateScheduledTaskRequest) =>
|
||||
request.post<ScheduledTask>('/scheduler/tasks', data)
|
||||
.then((r) => r.data),
|
||||
|
||||
update: (id: string, data: UpdateScheduledTaskRequest) =>
|
||||
request.patch<ScheduledTask>(`/scheduler/tasks/${id}`, data)
|
||||
.then((r) => r.data),
|
||||
|
||||
delete: (id: string) =>
|
||||
request.delete(`/scheduler/tasks/${id}`)
|
||||
.then((r) => r.data),
|
||||
}
|
||||
Reference in New Issue
Block a user