- 新增 /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>
273 lines
8.1 KiB
TypeScript
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,
|
|
}),
|
|
};
|
|
}
|