From 3ecf4bc238b5f728a10c1b55a9559572427354a2 Mon Sep 17 00:00:00 2001 From: wonfen Date: Sun, 2 Mar 2025 04:20:38 +0800 Subject: [PATCH] feat: refactor logging system into a global service --- src/hooks/use-log-data.ts | 62 +++-------- src/pages/_layout.tsx | 15 ++- src/pages/logs.tsx | 34 +++++- src/services/global-log-service.ts | 168 +++++++++++++++++++++++++++++ 4 files changed, 223 insertions(+), 56 deletions(-) create mode 100644 src/services/global-log-service.ts diff --git a/src/hooks/use-log-data.ts b/src/hooks/use-log-data.ts index 3db2cf9d..a8a8e3ee 100644 --- a/src/hooks/use-log-data.ts +++ b/src/hooks/use-log-data.ts @@ -5,18 +5,19 @@ import { useClashInfo } from "./use-clash"; import dayjs from "dayjs"; import { create } from "zustand"; import { useVisibility } from "./use-visibility"; +import { + useGlobalLogData, + clearGlobalLogs, + LogLevel, + ILogItem, +} from "@/services/global-log-service"; + +// 为了向后兼容,导出相同的类型 +export type { LogLevel }; +export type { ILogItem }; const MAX_LOG_NUM = 1000; -export type LogLevel = "warning" | "info" | "debug" | "error" | "all"; - -interface ILogItem { - time?: string; - type: string; - payload: string; - [key: string]: any; -} - const buildWSUrl = (server: string, secret: string, logLevel: LogLevel) => { const baseUrl = `ws://${server}/logs`; const params = new URLSearchParams(); @@ -57,45 +58,6 @@ const useLogStore = create( }), ); -export const useLogData = (logLevel: LogLevel) => { - const { clashInfo } = useClashInfo(); - const [enableLog] = useEnableLog(); - const { logs, appendLog } = useLogStore(); - const pageVisible = useVisibility(); +export const useLogData = useGlobalLogData; - useEffect(() => { - if (!enableLog || !clashInfo || !pageVisible) return; - - const { server = "", secret = "" } = clashInfo; - const wsUrl = buildWSUrl(server, secret, logLevel); - - let isActive = true; - const socket = createSockette(wsUrl, { - onmessage(event) { - if (!isActive) return; - const data = JSON.parse(event.data) as ILogItem; - const time = dayjs().format("MM-DD HH:mm:ss"); - appendLog({ ...data, time }); - }, - onerror() { - if (!isActive) return; - socket.close(); - }, - }); - - return () => { - isActive = false; - socket.close(); - }; - }, [clashInfo, enableLog, logLevel]); - - // 根据当前选择的日志等级过滤日志 - return logLevel === "all" - ? logs - : logs.filter((log) => log.type.toLowerCase() === logLevel); -}; - -// 导出清空日志的方法 -export const clearLogs = () => { - useLogStore.getState().clearLogs(); -}; +export const clearLogs = clearGlobalLogs; diff --git a/src/pages/_layout.tsx b/src/pages/_layout.tsx index c5d6b048..d5b93573 100644 --- a/src/pages/_layout.tsx +++ b/src/pages/_layout.tsx @@ -13,7 +13,7 @@ import { useVerge } from "@/hooks/use-verge"; import LogoSvg from "@/assets/image/logo.svg?react"; import iconLight from "@/assets/image/icon_light.svg?react"; import iconDark from "@/assets/image/icon_dark.svg?react"; -import { useThemeMode } from "@/services/states"; +import { useThemeMode, useEnableLog } from "@/services/states"; import { Notice } from "@/components/base"; import { LayoutItem } from "@/components/layout/layout-item"; import { LayoutControl } from "@/components/layout/layout-control"; @@ -28,6 +28,8 @@ import React from "react"; import { TransitionGroup, CSSTransition } from "react-transition-group"; import { useListen } from "@/hooks/use-listen"; import { listen } from "@tauri-apps/api/event"; +import { useClashInfo } from "@/hooks/use-clash"; +import { initGlobalLogService } from "@/services/global-log-service"; const appWindow = getCurrentWebviewWindow(); export let portableFlag = false; @@ -126,6 +128,8 @@ const Layout = () => { const { t } = useTranslation(); const { theme } = useCustomTheme(); const { verge } = useVerge(); + const { clashInfo } = useClashInfo(); + const [enableLog] = useEnableLog(); const { language, start_page } = verge ?? {}; const navigate = useNavigate(); const location = useLocation(); @@ -140,6 +144,15 @@ const Layout = () => { [t, navigate], ); + // 初始化全局日志服务 + useEffect(() => { + if (clashInfo) { + const { server = "", secret = "" } = clashInfo; + // 使用本地存储中的enableLog值初始化全局日志服务 + initGlobalLogService(server, secret, enableLog, "info"); + } + }, [clashInfo, enableLog]); + // 设置监听器 useEffect(() => { const listeners = [ diff --git a/src/pages/logs.tsx b/src/pages/logs.tsx index 0784f3d4..755efd64 100644 --- a/src/pages/logs.tsx +++ b/src/pages/logs.tsx @@ -8,7 +8,8 @@ import { PlayCircleOutlineRounded, PauseCircleOutlineRounded, } from "@mui/icons-material"; -import { useLogData, LogLevel, clearLogs } from "@/hooks/use-log-data"; +import { LogLevel, clearLogs } from "@/hooks/use-log-data"; +import { useClashInfo } from "@/hooks/use-clash"; import { useEnableLog } from "@/services/states"; import { BaseEmpty, BasePage } from "@/components/base"; import LogItem from "@/components/log/log-item"; @@ -16,10 +17,17 @@ import { useTheme } from "@mui/material/styles"; import { BaseSearchBox } from "@/components/base/base-search-box"; import { BaseStyledSelect } from "@/components/base/base-styled-select"; import { SearchState } from "@/components/base/base-search-box"; +import { + useGlobalLogData, + clearGlobalLogs, + changeLogLevel, + toggleLogEnabled, +} from "@/services/global-log-service"; const LogPage = () => { const { t } = useTranslation(); const [enableLog, setEnableLog] = useEnableLog(); + const { clashInfo } = useClashInfo(); const theme = useTheme(); const isDark = theme.palette.mode === "dark"; const [logLevel, setLogLevel] = useLocalStorage( @@ -27,7 +35,7 @@ const LogPage = () => { "info", ); const [match, setMatch] = useState(() => (_: string) => true); - const logData = useLogData(logLevel); + const logData = useGlobalLogData(logLevel); const [searchState, setSearchState] = useState(); const filterLogs = useMemo(() => { @@ -44,6 +52,22 @@ const LogPage = () => { : []; }, [logData, logLevel, match]); + const handleLogLevelChange = (newLevel: LogLevel) => { + setLogLevel(newLevel); + if (clashInfo) { + const { server = "", secret = "" } = clashInfo; + changeLogLevel(newLevel, server, secret); + } + }; + + const handleToggleLog = () => { + if (clashInfo) { + const { server = "", secret = "" } = clashInfo; + toggleLogEnabled(server, secret); + setEnableLog(!enableLog); + } + }; + return ( { title={t("Pause")} size="small" color="inherit" - onClick={() => setEnableLog((e) => !e)} + onClick={handleToggleLog} > {enableLog ? ( @@ -74,7 +98,7 @@ const LogPage = () => { size="small" variant="contained" onClick={() => { - clearLogs(); + clearGlobalLogs(); }} > {t("Clear")} @@ -95,7 +119,7 @@ const LogPage = () => { > setLogLevel(e.target.value as LogLevel)} + onChange={(e) => handleLogLevelChange(e.target.value as LogLevel)} > ALL INFO diff --git a/src/services/global-log-service.ts b/src/services/global-log-service.ts new file mode 100644 index 00000000..a547a6ad --- /dev/null +++ b/src/services/global-log-service.ts @@ -0,0 +1,168 @@ +// 全局日志服务,使应用在任何页面都能收集日志 +import { create } from "zustand"; +import { createSockette } from "../utils/websocket"; +import dayjs from "dayjs"; +import { useState, useEffect } from "react"; + +// 最大日志数量 +const MAX_LOG_NUM = 1000; + +export type LogLevel = "warning" | "info" | "debug" | "error" | "all"; + +export interface ILogItem { + time?: string; + type: string; + payload: string; + [key: string]: any; +} + +interface GlobalLogStore { + logs: ILogItem[]; + enabled: boolean; + isConnected: boolean; + currentLevel: LogLevel; + setEnabled: (enabled: boolean) => void; + setCurrentLevel: (level: LogLevel) => void; + clearLogs: () => void; + appendLog: (log: ILogItem) => void; +} + +// 创建全局状态存储 +export const useGlobalLogStore = create((set) => ({ + logs: [], + enabled: false, + isConnected: false, + currentLevel: "info", + setEnabled: (enabled) => set({ enabled }), + setCurrentLevel: (currentLevel) => set({ currentLevel }), + clearLogs: () => set({ logs: [] }), + appendLog: (log: ILogItem) => + set((state) => { + const newLogs = + state.logs.length >= MAX_LOG_NUM + ? [...state.logs.slice(1), log] + : [...state.logs, log]; + return { logs: newLogs }; + }), +})); + +// 构建WebSocket URL +const buildWSUrl = (server: string, secret: string, logLevel: LogLevel) => { + const baseUrl = `ws://${server}/logs`; + const params = new URLSearchParams(); + + if (secret) { + params.append("token", secret); + } + if (logLevel === "all") { + params.append("level", "debug"); + } else { + params.append("level", logLevel); + } + const queryString = params.toString(); + return queryString ? `${baseUrl}?${queryString}` : baseUrl; +}; + +// 初始化全局日志服务 +let globalLogSocket: any = null; + +export const initGlobalLogService = ( + server: string, + secret: string, + enabled: boolean = false, + logLevel: LogLevel = "info", +) => { + const { appendLog, setEnabled } = useGlobalLogStore.getState(); + + // 更新启用状态 + setEnabled(enabled); + + // 如果不启用或没有服务器信息,则不初始化 + if (!enabled || !server) { + closeGlobalLogConnection(); + return; + } + + // 关闭现有连接 + closeGlobalLogConnection(); + + // 创建新的WebSocket连接 + const wsUrl = buildWSUrl(server, secret, logLevel); + globalLogSocket = createSockette(wsUrl, { + onmessage(event) { + try { + const data = JSON.parse(event.data) as ILogItem; + const time = dayjs().format("MM-DD HH:mm:ss"); + appendLog({ ...data, time }); + } catch (error) { + console.error("Failed to parse log data:", error); + } + }, + onerror() { + console.error("Log WebSocket connection error"); + closeGlobalLogConnection(); + }, + onclose() { + console.log("Log WebSocket connection closed"); + useGlobalLogStore.setState({ isConnected: false }); + }, + onopen() { + console.log("Log WebSocket connection opened"); + useGlobalLogStore.setState({ isConnected: true }); + }, + }); +}; + +// 关闭全局日志连接 +export const closeGlobalLogConnection = () => { + if (globalLogSocket) { + globalLogSocket.close(); + globalLogSocket = null; + useGlobalLogStore.setState({ isConnected: false }); + } +}; + +// 切换日志级别 +export const changeLogLevel = ( + level: LogLevel, + server: string, + secret: string, +) => { + const { enabled } = useGlobalLogStore.getState(); + useGlobalLogStore.setState({ currentLevel: level }); + + if (enabled && server) { + initGlobalLogService(server, secret, enabled, level); + } +}; + +// 切换启用状态 +export const toggleLogEnabled = (server: string, secret: string) => { + const { enabled, currentLevel } = useGlobalLogStore.getState(); + const newEnabled = !enabled; + + useGlobalLogStore.setState({ enabled: newEnabled }); + + if (newEnabled && server) { + initGlobalLogService(server, secret, newEnabled, currentLevel); + } else { + closeGlobalLogConnection(); + } +}; + +// 获取日志清理函数 +export const clearGlobalLogs = () => { + useGlobalLogStore.getState().clearLogs(); +}; + +// 自定义钩子,用于获取过滤后的日志数据 +export const useGlobalLogData = (logLevel: LogLevel = "all") => { + const logs = useGlobalLogStore((state) => state.logs); + + // 根据当前选择的日志等级过滤日志 + if (logLevel === "all") { + return logs; + } else { + return logs.filter((log) => log.type.toLowerCase() === logLevel); + } +};