import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { toast } from 'sonner' import { monitorApi, settingsApi } from '@/lib/api' import { usePlatformStore } from '@/store/platformStore' import type { TaskCreatePayload } from '@/types/monitor' /** * The platform every monitor hook is scoped to. * * Read from the store rather than threaded through props, so switching the * global platform switcher re-scopes every view at once and no component can * forget to pass it along. */ function usePlatformParam(): string { return usePlatformStore((state) => state.platform) } /** Polling cadence for the dashboard. Runs take minutes, so a few seconds of * lag is fine and keeps the number of requests low. */ const POLL_MS = 5000 export function useMonitorOverview() { const platform = usePlatformParam() return useQuery({ queryKey: ['monitorOverview', platform], queryFn: async () => (await monitorApi.getOverview(platform)).data, refetchInterval: POLL_MS, }) } export function useMonitorTasks() { const platform = usePlatformParam() return useQuery({ queryKey: ['monitorTasks', platform], queryFn: async () => (await monitorApi.getTasks(platform)).data.tasks, refetchInterval: POLL_MS, }) } export function useCreateTask() { const queryClient = useQueryClient() return useMutation({ mutationFn: (payload: TaskCreatePayload) => monitorApi.createTask(payload), onSuccess: () => { toast.success('监控任务已创建') queryClient.invalidateQueries({ queryKey: ['monitorTasks'] }) queryClient.invalidateQueries({ queryKey: ['monitorOverview'] }) }, onError: (error: Error) => toast.error(`创建失败:${error.message}`), }) } export function useUpdateTask() { const queryClient = useQueryClient() return useMutation({ mutationFn: ({ id, payload }: { id: number; payload: Partial }) => monitorApi.updateTask(id, payload), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['monitorTasks'] }) }, onError: (error: Error) => toast.error(`更新失败:${error.message}`), }) } export function useDeleteTask() { const queryClient = useQueryClient() return useMutation({ mutationFn: (id: number) => monitorApi.deleteTask(id), onSuccess: () => { toast.success('监控任务已删除') queryClient.invalidateQueries({ queryKey: ['monitorTasks'] }) queryClient.invalidateQueries({ queryKey: ['monitorOverview'] }) }, onError: (error: Error) => toast.error(`删除失败:${error.message}`), }) } export function useRunTaskNow() { const queryClient = useQueryClient() return useMutation({ mutationFn: (id: number) => monitorApi.runTaskNow(id), onSuccess: () => { // The crawl itself runs in the background; the run list picks it up on poll. toast.success('已排队执行,可在下方运行历史查看进度') queryClient.invalidateQueries({ queryKey: ['monitorRuns'] }) }, onError: (error: Error) => toast.error(`启动失败:${error.message}`), }) } export function useTaskRuns(taskId: number | null) { return useQuery({ queryKey: ['monitorRuns', taskId], queryFn: async () => (await monitorApi.getRuns(taskId as number)).data.runs, enabled: taskId !== null, refetchInterval: POLL_MS, }) } export function useMonitorNotes(taskId: number | null, onlyNew: boolean) { const platform = usePlatformParam() return useQuery({ queryKey: ['monitorNotes', platform, taskId, onlyNew], queryFn: async () => (await monitorApi.getNotes(taskId ?? undefined, onlyNew, 200, platform)).data.notes, refetchInterval: POLL_MS, }) } export function useNoteSeries(noteId: string | null, taskId: number | null) { return useQuery({ queryKey: ['monitorNoteSeries', noteId, taskId], queryFn: async () => (await monitorApi.getNoteSeries(noteId as string, taskId ?? undefined)).data.series, enabled: noteId !== null, staleTime: 30_000, }) } export function useMonitorComments(taskId: number | null, noteId?: string | null) { const platform = usePlatformParam() return useQuery({ queryKey: ['monitorComments', platform, taskId, noteId ?? null], queryFn: async () => (await monitorApi.getComments(taskId ?? undefined, noteId ?? undefined, 200, platform)).data .comments, refetchInterval: POLL_MS, }) } export function useMonitorCommentsGrouped(taskId: number | null, noteId?: string | null) { const platform = usePlatformParam() return useQuery({ queryKey: ['monitorCommentsGrouped', platform, taskId, noteId ?? null], queryFn: async () => (await monitorApi.getCommentsGrouped(taskId ?? undefined, noteId ?? undefined, 200, platform)) .data.groups, refetchInterval: POLL_MS, }) } export function useCommentNoteOptions(taskId: number | null) { const platform = usePlatformParam() return useQuery({ queryKey: ['monitorCommentNotes', platform, taskId], queryFn: async () => (await monitorApi.getCommentNotes(taskId ?? undefined, platform)).data.notes, refetchInterval: POLL_MS, }) } export function useMonitorEvents(taskId: number | null) { const platform = usePlatformParam() return useQuery({ queryKey: ['monitorEvents', platform, taskId], queryFn: async () => (await monitorApi.getEvents(taskId ?? undefined, 200, platform)).data.events, refetchInterval: POLL_MS, }) } export function useMarkEventsRead() { const queryClient = useQueryClient() return useMutation({ mutationFn: (taskId?: number) => monitorApi.markEventsRead(taskId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['monitorEvents'] }) queryClient.invalidateQueries({ queryKey: ['monitorTasks'] }) queryClient.invalidateQueries({ queryKey: ['monitorOverview'] }) }, }) } export function useCookieStatus() { const platform = usePlatformParam() return useQuery({ queryKey: ['monitorCookie', platform], queryFn: async () => (await monitorApi.getCookieStatus(platform)).data, refetchInterval: 15000, }) } export function useSetCookie() { const queryClient = useQueryClient() const platform = usePlatformParam() return useMutation({ mutationFn: (cookie: string) => monitorApi.setCookie(cookie, platform), onSuccess: () => { toast.success('Cookie 已保存') queryClient.invalidateQueries({ queryKey: ['monitorCookie'] }) }, onError: (error: Error) => toast.error(`保存失败:${error.message}`), }) } export function useClearCookie() { const queryClient = useQueryClient() const platform = usePlatformParam() return useMutation({ mutationFn: () => monitorApi.clearCookie(platform), onSuccess: () => { toast.success('Cookie 已清除') queryClient.invalidateQueries({ queryKey: ['monitorCookie'] }) }, }) } // --- QR login (over CDP) --------------------------------------------------- /** Polls only while a code is on screen; an idle session has nothing to watch. */ export function useQrLoginStatus(polling: boolean) { return useQuery({ queryKey: ['monitorQrLogin'], queryFn: async () => (await monitorApi.getQrLogin()).data, refetchInterval: polling ? 2000 : false, }) } export function useStartQrLogin() { const queryClient = useQueryClient() const platform = usePlatformParam() return useMutation({ mutationFn: () => monitorApi.startQrLogin(platform), onSuccess: (response) => { queryClient.setQueryData(['monitorQrLogin'], response.data) }, onError: (error: Error) => { // The backend's reason -- "Chrome is not reachable on ...", "no QR on the // page" -- is the entire value here, and axios would flatten it to // "Request failed with status code 502". const detail = (error as { response?: { data?: { detail?: string } } })?.response ?.data?.detail toast.error(`获取二维码失败:${detail ?? error.message}`) }, }) } export function useCancelQrLogin() { const queryClient = useQueryClient() return useMutation({ mutationFn: () => monitorApi.cancelQrLogin(), onSuccess: (response) => { queryClient.setQueryData(['monitorQrLogin'], response.data) }, }) } /** * Whether the browser is actually signed in. * * Tracked separately from the QR session above: that session lives in the * server's memory and a restart erases it, while the profile it wrote to does * not. Polled slowly when idle and briskly while a code is on screen. */ export function useLoginState(polling: boolean) { return useQuery({ queryKey: ['monitorLoginState'], queryFn: async () => (await monitorApi.getLoginState()).data, refetchInterval: polling ? 4000 : 30000, refetchOnWindowFocus: false, }) } /** Re-ask after reloading the page, for when the state looks stale. */ export function useRecheckLogin() { const queryClient = useQueryClient() return useMutation({ mutationFn: () => monitorApi.getLoginState(true), onSuccess: (response) => { queryClient.setQueryData(['monitorLoginState'], response.data) }, onError: (error: Error) => toast.error(`检测失败:${error.message}`), }) } // --- Settings ------------------------------------------------------------- export function useSettings() { const platform = usePlatformParam() return useQuery({ queryKey: ['settings', platform], queryFn: async () => (await settingsApi.getAll(platform)).data, staleTime: 30_000, }) } export function useUpdateSettings() { const queryClient = useQueryClient() const platform = usePlatformParam() return useMutation({ mutationFn: (values: Record) => settingsApi.update(values, platform), onSuccess: (response) => { toast.success(`已保存 ${response.data.changed.length} 项设置`) queryClient.invalidateQueries({ queryKey: ['settings'] }) // Cookie / webhook panels read their own keys. queryClient.invalidateQueries({ queryKey: ['monitorCookie'] }) queryClient.invalidateQueries({ queryKey: ['monitorWebhook'] }) }, onError: (error: Error) => toast.error(`保存失败:${error.message}`), }) } // --- Report --------------------------------------------------------------- export function useReport(taskIds: number[], startDate: string, endDate: string) { const platform = usePlatformParam() return useQuery({ queryKey: ['monitorReport', platform, taskIds, startDate, endDate], queryFn: async () => (await monitorApi.getReport(taskIds, startDate, endDate, platform)).data, // Reports are read-only aggregates; no need to poll them every few seconds. staleTime: 30_000, }) } // --- WeCom webhook -------------------------------------------------------- export function useWebhookStatus() { return useQuery({ queryKey: ['monitorWebhook'], queryFn: async () => (await monitorApi.getWebhook()).data, refetchInterval: 30_000, }) } export function useSetWebhook() { const queryClient = useQueryClient() return useMutation({ mutationFn: (url: string) => monitorApi.setWebhook(url), onSuccess: () => { toast.success('Webhook 已保存') queryClient.invalidateQueries({ queryKey: ['monitorWebhook'] }) }, onError: (error: Error) => toast.error(`保存失败:${error.message}`), }) } export function useClearWebhook() { const queryClient = useQueryClient() return useMutation({ mutationFn: () => monitorApi.clearWebhook(), onSuccess: () => { toast.success('Webhook 已删除') queryClient.invalidateQueries({ queryKey: ['monitorWebhook'] }) }, }) } export function useTestWebhook() { return useMutation({ mutationFn: (url?: string) => monitorApi.testWebhook(url), onSuccess: () => toast.success('测试消息已发送,请查看企业微信群'), onError: (error: Error) => toast.error(`发送失败:${error.message}`), }) } // --- 上游更新检查 ---------------------------------------------------------- export function useUpstreamStatus() { return useQuery({ queryKey: ['monitorUpstream'], queryFn: async () => (await monitorApi.getUpstream()).data, // 检查本身是每天一次的量级,页面开着时慢点刷就够了。 refetchInterval: 60_000, }) } export function useCheckUpstream() { const queryClient = useQueryClient() return useMutation({ mutationFn: () => monitorApi.checkUpstream(), onSuccess: (response) => { const data = response.data if (!data.ok) { toast.error(`检查失败:${data.error ?? '未知错误'}`) } else if ((data.behind ?? 0) > 0) { toast.success(`上游有 ${data.behind} 个新提交`) } else { toast.success('已是最新,上游没有新提交') } queryClient.invalidateQueries({ queryKey: ['monitorUpstream'] }) }, onError: (error: Error) => toast.error(`检查失败:${error.message}`), }) }