From a258122e22a1e680a2823605637104df70195b21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=89=E9=91=AB=E5=8D=87?= Date: Fri, 22 May 2026 10:55:59 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9A=E6=97=B6=E4=BB=BB=E5=8A=A1=EF=BC=9A?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=99=BA=E8=83=BD=E4=BD=93=E6=89=A7=E8=A1=8C?= =?UTF-8?q?=20422=E3=80=81=E6=96=B0=E5=A2=9E=E6=8F=8F=E8=BF=B0=E5=AD=97?= =?UTF-8?q?=E6=AE=B5=E3=80=81=E5=85=AC=E5=BC=80=E9=A1=B5=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E6=8C=89=E9=92=AE=E7=AD=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 定时任务执行时按 agent.id 解析智能体,老数据中存的中文显示名会回退按 name 查找,避免 422 - scheduled_tasks 表新增 description 字段(迁移 20260522_01),创建/编辑表单支持,公开页标题下显示 - 公开页(/public/scheduled-tasks/{id})新增删除单次执行记录入口,使用样式化 Dialog 确认 - 公开页左侧标题区精简:去掉 schedule/timezone/最近状态两行;执行列表标题剥离尾部时间戳,"计划"改为"执行时间" - 工作区定时任务列表:超长标题改为按字符换行不再截断;按 created_at 倒序(新建任务在前) - 启动 Alembic 升级加超时与单独线程,MySQL 缺库时自动 CREATE DATABASE - vite build 增加 8GB 内存上限 Co-Authored-By: Claude Opus 4.7 --- frontend-web/package.json | 2 +- frontend-web/src/core/scheduled-tasks/api.ts | 3 + .../src/core/scheduled-tasks/types.ts | 3 + .../src/pages/PublicScheduledTaskPage.tsx | 172 +++++++++++++++--- frontend-web/src/pages/ScheduledTasksPage.tsx | 34 +++- .../backend/app/gateway/app.py | 72 ++++++-- .../gateway/routers/public_scheduled_tasks.py | 15 ++ .../app/gateway/routers/scheduled_tasks.py | 3 + .../harness/deerflow/persistence/engine.py | 105 ++++++++--- ...21_02_merge_agent_pins_and_memory_heads.py | 24 +++ .../20260522_01_scheduled_task_description.py | 36 ++++ .../persistence/scheduled_tasks/model.py | 1 + .../persistence/scheduled_tasks/sql.py | 1 + .../deerflow/runtime/scheduler/service.py | 55 +++++- 14 files changed, 451 insertions(+), 75 deletions(-) create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/persistence/migrations/versions/20260521_02_merge_agent_pins_and_memory_heads.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/persistence/migrations/versions/20260522_01_scheduled_task_description.py diff --git a/frontend-web/package.json b/frontend-web/package.json index 8c9b705a..77e59056 100644 --- a/frontend-web/package.json +++ b/frontend-web/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "dev": "vite", - "build": "vite build", + "build": "node --max-old-space-size=8192 ./node_modules/vite/bin/vite.js build", "preview": "vite preview", "typecheck": "tsc --noEmit" }, diff --git a/frontend-web/src/core/scheduled-tasks/api.ts b/frontend-web/src/core/scheduled-tasks/api.ts index 5324b83f..fcc0fb23 100644 --- a/frontend-web/src/core/scheduled-tasks/api.ts +++ b/frontend-web/src/core/scheduled-tasks/api.ts @@ -46,6 +46,7 @@ export async function listScheduledTasks(params: ListScheduledTasksParams = {}) export async function createScheduledTask(data: { name: string; + description?: string | null; prompt: string; schedule: string; timezone?: string | null; @@ -203,6 +204,7 @@ export async function updateScheduledTask( taskId: string, data: Partial<{ name: string; + description: string | null; prompt: string; schedule: string; timezone: string; @@ -329,6 +331,7 @@ export async function adminUpdateScheduledTask( taskId: string, data: Partial<{ name: string; + description: string | null; prompt: string; schedule: string; timezone: string; diff --git a/frontend-web/src/core/scheduled-tasks/types.ts b/frontend-web/src/core/scheduled-tasks/types.ts index fe2f30ba..4efce4e6 100644 --- a/frontend-web/src/core/scheduled-tasks/types.ts +++ b/frontend-web/src/core/scheduled-tasks/types.ts @@ -14,6 +14,7 @@ export type ScheduledTaskSubscription = { export type ScheduledTask = { task_id: string; name: string; + description?: string | null; prompt: string; schedule_text: string; cron_expr: string; @@ -27,6 +28,8 @@ export type ScheduledTask = { scheduler_thread_id: string; published?: boolean; published_at?: string | null; + created_at?: string | null; + updated_at?: string | null; role?: ScheduledTaskRole; _subscription?: ScheduledTaskSubscription | null; creator_user_id?: string | null; diff --git a/frontend-web/src/pages/PublicScheduledTaskPage.tsx b/frontend-web/src/pages/PublicScheduledTaskPage.tsx index c2f78303..dd5570f5 100644 --- a/frontend-web/src/pages/PublicScheduledTaskPage.tsx +++ b/frontend-web/src/pages/PublicScheduledTaskPage.tsx @@ -7,6 +7,7 @@ import { FileText, Loader2, Pencil, + Trash2, X, } from "lucide-react"; import { useCallback, useEffect, useMemo, useState } from "react"; @@ -16,6 +17,14 @@ import { toast } from "sonner"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Textarea } from "@/components/ui/textarea"; @@ -34,6 +43,7 @@ import { cn } from "@/lib/utils"; type PublicTask = { task_id: string; name: string; + description?: string | null; schedule_text: string; cron_expr?: string; timezone: string; @@ -142,6 +152,17 @@ async function updatePublicRunBody(runId: string, content: string): Promise { + const response = await fetch( + `${publicBaseUrl()}/runs/${encodeURIComponent(runId)}`, + { method: "DELETE" }, + ); + if (!response.ok && response.status !== 204) { + const body = await response.json().catch(() => ({ detail: "Request failed" })); + throw new Error(typeof body.detail === "string" ? body.detail : "Request failed"); + } +} + function publicRunFileDownloadUrl(runId: string, index: number): string { return `${publicBaseUrl()}/runs/${encodeURIComponent(runId)}/files/${index}/download`; } @@ -196,6 +217,20 @@ function stripExtension(name: string): string { return name.slice(0, dot); } +/** + * Strip a trailing ` - YYYY-MM-DD HH:MM[:SS]` (or `_YYYY-MM-DD ...`) suffix + * that the agent appends to run titles. The scheduled-for column is already + * displayed separately below the title, so the duplicated date looks like noise. + */ +function stripTimestampSuffix(title: string): string { + if (!title) return title; + const cleaned = title.replace( + /[\s_]*[-_]?\s*\d{4}-\d{1,2}-\d{1,2}[\sT]+\d{1,2}:\d{2}(?::\d{2})?\s*$/, + "", + ); + return cleaned.trim() || title; +} + const BODY_TAB_VALUE = "__body__"; /** @@ -212,6 +247,8 @@ const BODY_TAB_VALUE = "__body__"; export default function PublicScheduledTaskPage() { const { taskId } = useParams<{ taskId: string }>(); const [selectedRunId, setSelectedRunId] = useState(null); + const [pendingDeleteRun, setPendingDeleteRun] = useState<{ id: string; title: string } | null>(null); + const queryClient = useQueryClient(); const { data, isLoading, isError, error } = useQuery({ queryKey: ["public-scheduled-task", taskId], @@ -222,6 +259,21 @@ export default function PublicScheduledTaskPage() { refetchInterval: 8000, }); + const deleteRunMutation = useMutation({ + mutationFn: (runId: string) => deletePublicRun(runId), + onSuccess: async (_data, runId) => { + toast.success("已删除"); + if (selectedRunId === runId) { + setSelectedRunId(null); + } + setPendingDeleteRun(null); + await queryClient.invalidateQueries({ queryKey: ["public-scheduled-task", taskId] }); + }, + onError: (err) => { + toast.error(err instanceof Error ? err.message : "删除失败"); + }, + }); + // Public viewer only surfaces runs that have actual content to show: // pending/running/failed are hidden, and succeeded runs with neither a body // nor a Markdown attachment fall out too (nothing to render). @@ -301,12 +353,11 @@ export default function PublicScheduledTaskPage() { {task?.enabled ? "启用" : "暂停"} -

- {task?.schedule_text} · {task?.timezone} -

-

- 最近状态:{statusLabel(task?.last_status)} · 上次:{formatDate(task?.last_run_at)} -

+ {task?.description && task.description.trim() && ( +

+ {task.description.trim()} +

+ )} @@ -319,15 +370,27 @@ export default function PublicScheduledTaskPage() { {runs.map((run) => { const active = run.id === selectedRunId; const runRunning = run.status === "running" || run.status === "pending"; - const runTitle = run.result?.title || formatDate(run.scheduled_for); + const runTitle = stripTimestampSuffix( + run.result?.title || formatDate(run.scheduled_for), + ); + const isDeletingThis = + deleteRunMutation.isPending && deleteRunMutation.variables === run.id; return ( - + - + ); })} @@ -377,6 +459,54 @@ export default function PublicScheduledTaskPage() { )} + + { + if (!open && !deleteRunMutation.isPending) { + setPendingDeleteRun(null); + } + }} + > + + + 删除执行记录 + + 确认删除这条执行记录吗?此操作不可恢复。 + + + {pendingDeleteRun && ( +
+ {pendingDeleteRun.title} +
+ )} + + + + +
+
); } diff --git a/frontend-web/src/pages/ScheduledTasksPage.tsx b/frontend-web/src/pages/ScheduledTasksPage.tsx index fda257b1..e7cfa30a 100644 --- a/frontend-web/src/pages/ScheduledTasksPage.tsx +++ b/frontend-web/src/pages/ScheduledTasksPage.tsx @@ -78,6 +78,7 @@ function displayUsername(value?: string | null): string { const emptyForm = { name: "", + description: "", schedule: "", timezone: "Asia/Shanghai", prompt: "", @@ -116,9 +117,13 @@ export default function ScheduledTasksPage() { const sortedTasks = useMemo( () => - [...tasks].sort((a, b) => - String(a.next_run_at ?? "").localeCompare(String(b.next_run_at ?? "")), - ), + [...tasks].sort((a, b) => { + // Newest task first. Fall back to next_run_at so tasks that lack a + // created_at (older rows / subscribed tasks) still order consistently. + const aKey = a.created_at ?? a.next_run_at ?? ""; + const bKey = b.created_at ?? b.next_run_at ?? ""; + return String(bKey).localeCompare(String(aKey)); + }), [tasks], ); const selectedTask = sortedTasks.find((task) => task.task_id === selectedTaskId) ?? null; @@ -162,6 +167,7 @@ export default function ScheduledTasksPage() { setEditing(task); setForm({ name: task.name, + description: task.description ?? "", schedule: task.schedule_text, timezone: task.timezone, prompt: task.prompt, @@ -190,6 +196,7 @@ export default function ScheduledTasksPage() { if (creating) { const task = await actions.create.mutateAsync({ name: form.name || "定时任务", + description: form.description.trim() || null, schedule: cronSchedule, timezone: form.timezone || "Asia/Shanghai", prompt: form.prompt, @@ -202,6 +209,7 @@ export default function ScheduledTasksPage() { taskId: editing.task_id, data: { name: form.name, + description: form.description.trim() || null, schedule: cronSchedule, timezone: form.timezone, prompt: form.prompt, @@ -324,9 +332,11 @@ export default function ScheduledTasksPage() { onClick={() => setSelectedTaskId(task.task_id)} >
-
-
-
{task.name}
+
+
+ {task.name} +
+
{isRunningNow && (
- + {task.enabled ? "启用" : "暂停"}
@@ -760,6 +770,14 @@ export default function ScheduledTasksPage() { } placeholder="任务名称" /> +