feat: 监控面板 / 登录鉴权 / 多平台切换 / MySQL
在上游 MediaCrawler 之上新增一层: - 监控层 api/monitor/ —— 多博主/多笔记的定时采集、指标快照差分、报表、 企业微信通知。每轮采集写入独立目录,差分才成立。 - WebUI 登录鉴权 api/auth.py —— PBKDF2 口令 + 服务端会话,/api 全接口防护。 WebSocket 单独加依赖:BaseHTTPMiddleware 对 ws 作用域直接放行,覆盖不到。 - 全局平台切换 + 能力矩阵 —— 如实区分「爬虫模块支持」与「监控层已接线」, 未接通的平台直接拒绝建任务,而不是静默跑空。 - 监控库改用 MySQL 5.7(可回退 SQLite 供测试):逐表强制 utf8mb4 (服务端与库默认都是 latin1),启动校验所连 schema 以防写错库, 连接池 recycle + pre_ping 应对 MySQL 的 8 小时空闲断连。 修复上游缺陷: - xhs/core.py: 主页抓取失败会跳掉整个博主,导致一条作品都抓不到, 而那份资料只喂给一个空函数。改为尽力而为,失败不中断。 - xhs/login.py: cookie 登录只注入 web_session,冷启动签名会失败。 新增 INJECT_ALL_COOKIES 开关(默认关闭,原有行为不变)。 - requirements.txt: 补上 websockets。它在上游 pyproject.toml 里有声明、 这里漏了,导致 uvicorn 没有 WebSocket 能力,实时日志流从未工作。 改动过的上游文件清单及合并方式见 UPSTREAM.md。 测试:492 passed(另有 1 个既有的 Windows/gbk 上游测试失败,与本改动无关)
This commit is contained in:
@@ -0,0 +1,286 @@
|
||||
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<TaskCreatePayload> }) =>
|
||||
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'] })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// --- 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<string, unknown>) => 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}`),
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user