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

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:
iven
2026-04-03 21:34:56 +08:00
parent 305984c982
commit 2ceeeaba3d
17 changed files with 1157 additions and 81 deletions

View File

@@ -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': '系统配置',

View 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>
)
}

View File

@@ -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 })) },

View 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),
}