Files
deerflow-code/frontend-web/src/core/scheduled-tasks/hooks.ts
T
吉鑫升andClaude Opus 4.7 d339db4799 feat: 公开定时任务页面、Word 导出、定时任务实时刷新与技能详情重构
- 新增 /public/scheduled-tasks/:taskId 公开只读访问页与对应的
  /api/public/scheduled-tasks 后端路由(无需登录)
- 新增对话整体 / 单条消息 / Markdown 工件 "另存为 Word (.docx)" 能力
- 定时任务列表与运行列表在任务执行中自动轮询刷新,并新增"正在执行"标记
- 定时任务运行消息接口在事件存储为空时回退到 LangGraph checkpoint,
  让正在执行的任务也能立即看到提示词与流式输出
- 技能详情对话框由折叠面板改为 Tab 切换(Detail / SKILL.md),
  并支持 Rendered / Raw 切换、一键复制
- config.yaml 增加 database (MySQL) 与 checkpointer (SQLite) 配置块

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 17:04:58 +08:00

273 lines
8.1 KiB
TypeScript

import {
keepPreviousData,
useMutation,
useQuery,
useQueryClient,
} from "@tanstack/react-query";
import {
adminDeleteScheduledTask,
adminListScheduledTaskRuns,
adminTriggerScheduledTask,
adminUpdateScheduledTask,
createScheduledTask,
deleteScheduledTask,
deleteScheduledTaskRun,
getSchedulerThread,
getScheduledTaskDeliveryProfile,
getScheduledTaskRun,
getScheduledTaskRunMessages,
listAllScheduledTasks,
listPublishedScheduledTasks,
listScheduledTaskRuns,
listScheduledTasks,
pauseScheduledTask,
publishScheduledTask,
resumeScheduledTask,
subscribeScheduledTask,
triggerScheduledTask,
unpublishScheduledTask,
unsubscribeScheduledTask,
updateScheduledTaskDeliveryProfile,
updateScheduledTask,
updateScheduledTaskSubscription,
} from "./api";
export function useSchedulerThread() {
return useQuery({
queryKey: ["scheduled-tasks", "thread"],
queryFn: getSchedulerThread,
refetchOnWindowFocus: false,
});
}
/** A task is currently executing — used to drive live polling of the task list. */
function taskIsLive(status?: string | null): boolean {
return status === "running" || status === "pending";
}
export function useScheduledTasks(
params: { search?: string; tagIds?: string[] } = {},
) {
const search = params.search ?? "";
const tagIds = params.tagIds ?? [];
return useQuery({
queryKey: ["scheduled-tasks", "list", search, [...tagIds].sort()],
queryFn: () => listScheduledTasks({ search, tagIds }),
refetchOnWindowFocus: false,
placeholderData: keepPreviousData,
// Poll the list while any task is currently executing so its last_status
// and last_run_at refresh without a manual reload.
refetchInterval: (query) =>
(query.state.data ?? []).some((task) => taskIsLive(task.last_status))
? 5000
: false,
});
}
export function useScheduledTaskRuns(taskId?: string | null) {
return useQuery({
queryKey: ["scheduled-tasks", taskId, "runs"],
queryFn: () => listScheduledTaskRuns(taskId!),
enabled: !!taskId,
refetchOnWindowFocus: false,
// Poll the run list while any run is in flight so the user can watch
// status transitions (pending → running → succeeded/failed) without
// refreshing the page.
refetchInterval: (query) =>
(query.state.data ?? []).some((run) => runIsLive(run.status))
? 4000
: false,
});
}
/** A run still in flight — used to drive live polling of the run/messages. */
function runIsLive(status?: string | null): boolean {
return status === "running" || status === "pending";
}
export function useScheduledTaskRun(runId?: string | null) {
return useQuery({
queryKey: ["scheduled-tasks", "runs", runId],
queryFn: () => getScheduledTaskRun(runId!),
enabled: !!runId,
refetchOnWindowFocus: false,
// Keep refreshing while the run is still executing so the detail page
// reflects status / result changes without a manual reload.
refetchInterval: (query) => (runIsLive(query.state.data?.status) ? 5000 : false),
});
}
/** Agent messages for a run; polls every 4s while the run is still executing. */
export function useScheduledTaskRunMessages(
runId?: string | null,
options: { live?: boolean } = {},
) {
return useQuery({
queryKey: ["scheduled-tasks", "runs", runId, "messages"],
queryFn: () => getScheduledTaskRunMessages(runId!),
enabled: !!runId,
refetchOnWindowFocus: false,
refetchInterval: options.live ? 4000 : false,
});
}
export function useScheduledTaskDeliveryProfile() {
return useQuery({
queryKey: ["scheduled-tasks", "delivery-profile"],
queryFn: getScheduledTaskDeliveryProfile,
refetchOnWindowFocus: false,
});
}
/** Admin: every scheduled task across all users. */
export function useAllScheduledTasks(
params: { search?: string; enabled?: boolean } = {},
) {
const search = params.search ?? "";
return useQuery({
queryKey: ["scheduled-tasks", "admin", "list", search],
queryFn: () => listAllScheduledTasks(search),
enabled: params.enabled ?? true,
refetchOnWindowFocus: false,
placeholderData: keepPreviousData,
// Mirror the user list polling so the admin view also refreshes live.
refetchInterval: (query) =>
(query.state.data ?? []).some((task) => taskIsLive(task.last_status))
? 5000
: false,
});
}
/** Admin: runs for any task regardless of owner. */
export function useAdminScheduledTaskRuns(taskId?: string | null) {
return useQuery({
queryKey: ["scheduled-tasks", "admin", taskId, "runs"],
queryFn: () => adminListScheduledTaskRuns(taskId!),
enabled: !!taskId,
refetchOnWindowFocus: false,
// Poll while any run is still executing so admins see live progress.
refetchInterval: (query) =>
(query.state.data ?? []).some((run) => runIsLive(run.status))
? 4000
: false,
});
}
/** Admin mutations: edit / delete / trigger any task. */
export function useAdminScheduledTaskActions() {
const queryClient = useQueryClient();
const invalidate = () =>
queryClient.invalidateQueries({ queryKey: ["scheduled-tasks"] });
return {
update: useMutation({
mutationFn: ({
taskId,
data,
}: {
taskId: string;
data: Parameters<typeof adminUpdateScheduledTask>[1];
}) => adminUpdateScheduledTask(taskId, data),
onSuccess: invalidate,
}),
remove: useMutation({
mutationFn: adminDeleteScheduledTask,
onSuccess: invalidate,
}),
// The DELETE /runs/{id} route is admin-aware, so the same call deletes
// any user's run when the caller is an admin.
removeRun: useMutation({
mutationFn: deleteScheduledTaskRun,
onSuccess: invalidate,
}),
trigger: useMutation({
mutationFn: adminTriggerScheduledTask,
onSuccess: invalidate,
}),
};
}
export function usePublishedScheduledTasks(enabled: boolean = true) {
return useQuery({
queryKey: ["scheduled-tasks", "published"],
queryFn: () => listPublishedScheduledTasks({ limit: 100 }),
enabled,
refetchOnWindowFocus: false,
});
}
export function useScheduledTaskActions() {
const queryClient = useQueryClient();
const invalidate = () =>
queryClient.invalidateQueries({ queryKey: ["scheduled-tasks"] });
const invalidateRuns = () =>
queryClient.invalidateQueries({ queryKey: ["scheduled-tasks"] });
return {
update: useMutation({
mutationFn: ({
taskId,
data,
}: {
taskId: string;
data: Parameters<typeof updateScheduledTask>[1];
}) => updateScheduledTask(taskId, data),
onSuccess: invalidate,
}),
create: useMutation({
mutationFn: createScheduledTask,
onSuccess: invalidate,
}),
remove: useMutation({
mutationFn: deleteScheduledTask,
onSuccess: invalidate,
}),
removeRun: useMutation({
mutationFn: deleteScheduledTaskRun,
onSuccess: invalidate,
}),
pause: useMutation({
mutationFn: pauseScheduledTask,
onSuccess: invalidate,
}),
resume: useMutation({
mutationFn: resumeScheduledTask,
onSuccess: invalidate,
}),
trigger: useMutation({
mutationFn: triggerScheduledTask,
onSuccess: invalidateRuns,
}),
updateDeliveryProfile: useMutation({
mutationFn: updateScheduledTaskDeliveryProfile,
onSuccess: () =>
queryClient.invalidateQueries({
queryKey: ["scheduled-tasks", "delivery-profile"],
}),
}),
publish: useMutation({
mutationFn: publishScheduledTask,
onSuccess: invalidate,
}),
unpublish: useMutation({
mutationFn: unpublishScheduledTask,
onSuccess: invalidate,
}),
subscribe: useMutation({
mutationFn: ({ taskId, notifyElement }: { taskId: string; notifyElement: boolean }) =>
subscribeScheduledTask(taskId, notifyElement),
onSuccess: invalidate,
}),
updateSubscription: useMutation({
mutationFn: ({ taskId, notifyElement }: { taskId: string; notifyElement: boolean }) =>
updateScheduledTaskSubscription(taskId, notifyElement),
onSuccess: invalidate,
}),
unsubscribe: useMutation({
mutationFn: unsubscribeScheduledTask,
onSuccess: invalidate,
}),
};
}