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="任务名称" /> +