From 7cd1816866bfa67de56e0d1d4be71dcdb6fa5256 Mon Sep 17 00:00:00 2001 From: Ahao <108321411+xuanyuan0408@users.noreply.github.com> Date: Tue, 20 May 2025 18:49:16 +0800 Subject: [PATCH] optimized port settings and added one-click random API port and key/separate refresh button --- UPDATELOG.md | 2 + .../setting/mods/clash-port-viewer.tsx | 518 +++++++++--------- .../setting/mods/controller-viewer.tsx | 240 +++++++- src/components/setting/mods/web-ui-viewer.tsx | 4 +- src/components/setting/setting-clash.tsx | 74 +-- src/locales/en.json | 19 +- src/locales/zh.json | 21 +- 7 files changed, 529 insertions(+), 349 deletions(-) diff --git a/UPDATELOG.md b/UPDATELOG.md index 538fa0c90..a871d3df4 100644 --- a/UPDATELOG.md +++ b/UPDATELOG.md @@ -50,6 +50,7 @@ - 添加了Zashboard的一键跳转URL - 使用操作系统默认的窗口管理器 - 切换、升级、重启内核的状态管理 + - 增加了一键随机API端口和密钥/单独刷新按钮 #### 优化了: - 系统代理 Bypass 设置 @@ -74,6 +75,7 @@ - 优化窗口状态初始化逻辑和添加缺失的权限设置 - 异步化配置:优化端口查找和配置保存逻辑 - 重构事件通知机制到独立线程,避免前端卡死 + - 优化端口设置,每个端口可随机设置端口号 ## v2.2.3 diff --git a/src/components/setting/mods/clash-port-viewer.tsx b/src/components/setting/mods/clash-port-viewer.tsx index 0e36c7432..dca9f02d5 100644 --- a/src/components/setting/mods/clash-port-viewer.tsx +++ b/src/components/setting/mods/clash-port-viewer.tsx @@ -1,286 +1,282 @@ +import { BaseDialog, Switch } from "@/components/base"; +import { useClashInfo } from "@/hooks/use-clash"; +import { useVerge } from "@/hooks/use-verge"; +import { showNotice } from "@/services/noticeService"; +import getSystem from "@/utils/get-system"; +import { Shuffle } from "@mui/icons-material"; +import { IconButton, List, ListItem, ListItemText, TextField } from "@mui/material"; +import { useLockFn } from "ahooks"; import { forwardRef, useImperativeHandle, useState } from "react"; import { useTranslation } from "react-i18next"; -import { useLockFn } from "ahooks"; -import { List, ListItem, ListItemText, TextField } from "@mui/material"; -import { useClashInfo } from "@/hooks/use-clash"; -import { BaseDialog, DialogRef, Switch } from "@/components/base"; -import { useVerge } from "@/hooks/use-verge"; -import getSystem from "@/utils/get-system"; -import { showNotice } from "@/services/noticeService"; + const OS = getSystem(); -export const ClashPortViewer = forwardRef((props, ref) => { - const { t } = useTranslation(); +interface ClashPortViewerProps {} - const { clashInfo, patchInfo } = useClashInfo(); - const { verge, patchVerge } = useVerge(); - const [open, setOpen] = useState(false); - const [redirPort, setRedirPort] = useState( - verge?.verge_redir_port ?? clashInfo?.redir_port ?? 7895 - ); - const [redirEnabled, setRedirEnabled] = useState( - verge?.verge_redir_enabled ?? false - ); - const [tproxyPort, setTproxyPort] = useState( - verge?.verge_tproxy_port ?? clashInfo?.tproxy_port ?? 7896 - ); - const [tproxyEnabled, setTproxyEnabled] = useState( - verge?.verge_tproxy_enabled ?? false - ); - const [mixedPort, setMixedPort] = useState( - verge?.verge_mixed_port ?? clashInfo?.mixed_port ?? 7897 - ); - const [socksPort, setSocksPort] = useState( - verge?.verge_socks_port ?? clashInfo?.socks_port ?? 7898 - ); - const [socksEnabled, setSocksEnabled] = useState( - verge?.verge_socks_enabled ?? false - ); - const [port, setPort] = useState( - verge?.verge_port ?? clashInfo?.port ?? 7899 - ); - const [httpEnabled, setHttpEnabled] = useState( - verge?.verge_http_enabled ?? false - ); +interface ClashPortViewerRef { + open: () => void; + close: () => void; +} - useImperativeHandle(ref, () => ({ - open: () => { - if (verge?.verge_redir_port) setRedirPort(verge?.verge_redir_port); - setRedirEnabled(verge?.verge_redir_enabled ?? false); - if (verge?.verge_tproxy_port) setTproxyPort(verge?.verge_tproxy_port); - setTproxyEnabled(verge?.verge_tproxy_enabled ?? false); - if (verge?.verge_mixed_port) setMixedPort(verge?.verge_mixed_port); - if (verge?.verge_socks_port) setSocksPort(verge?.verge_socks_port); - setSocksEnabled(verge?.verge_socks_enabled ?? false); - if (verge?.verge_port) setPort(verge?.verge_port); - setHttpEnabled(verge?.verge_http_enabled ?? false); - setOpen(true); - }, - close: () => setOpen(false), - })); +const generateRandomPort = () => Math.floor(Math.random() * (65535 - 1025 + 1)) + 1025; - const onSave = useLockFn(async () => { - if ( - redirPort === verge?.verge_redir_port && - tproxyPort === verge?.verge_tproxy_port && - mixedPort === verge?.verge_mixed_port && - socksPort === verge?.verge_socks_port && - port === verge?.verge_port && - redirEnabled === verge?.verge_redir_enabled && - tproxyEnabled === verge?.verge_tproxy_enabled && - socksEnabled === verge?.verge_socks_enabled && - httpEnabled === verge?.verge_http_enabled - ) { - setOpen(false); - return; - } +export const ClashPortViewer = forwardRef( + (props, ref) => { + const { t } = useTranslation(); + const { clashInfo, patchInfo } = useClashInfo(); + const { verge, patchVerge } = useVerge(); + const [open, setOpen] = useState(false); - if ( - OS === "linux" && - new Set([redirPort, tproxyPort, mixedPort, socksPort, port]).size !== 5 - ) { - showNotice('error', t("Port Conflict")); - return; - } - if ( - OS === "macos" && - new Set([redirPort, mixedPort, socksPort, port]).size !== 4 - ) { - showNotice('error', t("Port Conflict")); - return; - } - if (OS === "windows" && new Set([mixedPort, socksPort, port]).size !== 3) { - showNotice('error', t("Port Conflict")); - return; - } - try { - if (OS === "windows") { + // Mixed Port + const [mixedPort, setMixedPort] = useState( + verge?.verge_mixed_port ?? clashInfo?.mixed_port ?? 7897 + ); + + // 其他端口状态 + const [socksPort, setSocksPort] = useState(verge?.verge_socks_port ?? 7898); + const [socksEnabled, setSocksEnabled] = useState(verge?.verge_socks_enabled ?? false); + const [httpPort, setHttpPort] = useState(verge?.verge_port ?? 7899); + const [httpEnabled, setHttpEnabled] = useState(verge?.verge_http_enabled ?? false); + const [redirPort, setRedirPort] = useState(verge?.verge_redir_port ?? 7895); + const [redirEnabled, setRedirEnabled] = useState(verge?.verge_redir_enabled ?? false); + const [tproxyPort, setTproxyPort] = useState(verge?.verge_tproxy_port ?? 7896); + const [tproxyEnabled, setTproxyEnabled] = useState(verge?.verge_tproxy_enabled ?? false); + + useImperativeHandle(ref, () => ({ + open: () => { + setMixedPort(verge?.verge_mixed_port ?? clashInfo?.mixed_port ?? 7897); + setSocksPort(verge?.verge_socks_port ?? 7898); + setSocksEnabled(verge?.verge_socks_enabled ?? false); + setHttpPort(verge?.verge_port ?? 7899); + setHttpEnabled(verge?.verge_http_enabled ?? false); + setRedirPort(verge?.verge_redir_port ?? 7895); + setRedirEnabled(verge?.verge_redir_enabled ?? false); + setTproxyPort(verge?.verge_tproxy_port ?? 7896); + setTproxyEnabled(verge?.verge_tproxy_enabled ?? false); + setOpen(true); + }, + close: () => setOpen(false), + })); + + const onSave = useLockFn(async () => { + // 端口冲突检测 + const portList = [ + mixedPort, + socksEnabled ? socksPort : -1, + httpEnabled ? httpPort : -1, + redirEnabled ? redirPort : -1, + tproxyEnabled ? tproxyPort : -1 + ].filter(p => p !== -1); + + if (new Set(portList).size !== portList.length) { + showNotice("error", t("Port Conflict")); + return; + } + + try { + // 更新Clash配置 await patchInfo({ "mixed-port": mixedPort, "socks-port": socksPort, - port, - }); + port: httpPort, + "redir-port": redirPort, + "tproxy-port": tproxyPort + } as any); + + // 更新Verge配置 await patchVerge({ verge_mixed_port: mixedPort, verge_socks_port: socksPort, verge_socks_enabled: socksEnabled, - verge_port: port, + verge_port: httpPort, verge_http_enabled: httpEnabled, - }); - } - if (OS === "macos") { - await patchInfo({ - "redir-port": redirPort, - "mixed-port": mixedPort, - "socks-port": socksPort, - port, - }); - await patchVerge({ - verge_redir_port: redirPort, - verge_redir_enabled: redirEnabled, - verge_mixed_port: mixedPort, - verge_socks_port: socksPort, - verge_socks_enabled: socksEnabled, - verge_port: port, - verge_http_enabled: httpEnabled, - }); - } - if (OS === "linux") { - await patchInfo({ - "redir-port": redirPort, - "tproxy-port": tproxyPort, - "mixed-port": mixedPort, - "socks-port": socksPort, - port, - }); - await patchVerge({ verge_redir_port: redirPort, verge_redir_enabled: redirEnabled, verge_tproxy_port: tproxyPort, - verge_tproxy_enabled: tproxyEnabled, - verge_mixed_port: mixedPort, - verge_socks_port: socksPort, - verge_socks_enabled: socksEnabled, - verge_port: port, - verge_http_enabled: httpEnabled, + verge_tproxy_enabled: tproxyEnabled }); - } - setOpen(false); - showNotice('success', t("Clash Port Modified")); - } catch (err: any) { - showNotice('error', err.message || err.toString()); - } - }); - return ( - setOpen(false)} - onCancel={() => setOpen(false)} - onOk={onSave} - > - - - - - setMixedPort(+e.target.value?.replace(/\D+/, "").slice(0, 5)) - } - /> - - - - - setSocksPort(+e.target.value?.replace(/\D+/, "").slice(0, 5)) - } - slotProps={{ - input: { - sx: { pr: 1 }, - endAdornment: ( - { - setSocksEnabled(c); - }} - /> - ), - } - }} - /> - - - - - setPort(+e.target.value?.replace(/\D+/, "").slice(0, 5)) - } - slotProps={{ - input: { - sx: { pr: 1 }, - endAdornment: ( - { - setHttpEnabled(c); - }} - /> - ), - } - }} - /> - - {OS !== "windows" && ( - - - - setRedirPort(+e.target.value?.replace(/\D+/, "").slice(0, 5)) - } - slotProps={{ - input: { - sx: { pr: 1 }, - endAdornment: ( - { - setRedirEnabled(c); - }} - /> - ), - } - }} + setOpen(false); + showNotice("success", t("Port settings saved")); + } catch (err) { + showNotice("error", t("Failed to save settings")); + } + }); + + return ( + setOpen(false)} + onOk={onSave} + > + + + +
+ setMixedPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))} + inputProps={{ style: { fontSize: 12 } }} + /> + setMixedPort(generateRandomPort())} + title={t("Random Port")} + sx={{ mr: 0.5 }} + > + + + +
- )} - {OS === "linux" && ( - - - - setTproxyPort(+e.target.value?.replace(/\D+/, "").slice(0, 5)) - } - slotProps={{ - input: { - sx: { pr: 1 }, - endAdornment: ( - { - setTproxyEnabled(c); - }} - /> - ), - } - }} + + + +
+ setSocksPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))} + disabled={!socksEnabled} + inputProps={{ style: { fontSize: 12 } }} + /> + setSocksPort(generateRandomPort())} + title={t("Random Port")} + disabled={!socksEnabled} + sx={{ mr: 0.5 }} + > + + + setSocksEnabled(c)} + sx={{ ml: 0.5 }} + /> +
- )} -
-
- ); -}); + + + +
+ setHttpPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))} + disabled={!httpEnabled} + inputProps={{ style: { fontSize: 12 } }} + /> + setHttpPort(generateRandomPort())} + title={t("Random Port")} + disabled={!httpEnabled} + sx={{ mr: 0.5 }} + > + + + setHttpEnabled(c)} + sx={{ ml: 0.5 }} + /> +
+
+ + {OS !== "windows" && ( + + +
+ setRedirPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))} + disabled={!redirEnabled} + inputProps={{ style: { fontSize: 12 } }} + /> + setRedirPort(generateRandomPort())} + title={t("Random Port")} + disabled={!redirEnabled} + sx={{ mr: 0.5 }} + > + + + setRedirEnabled(c)} + sx={{ ml: 0.5 }} + /> +
+
+ )} + + {OS === "linux" && ( + + +
+ setTproxyPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))} + disabled={!tproxyEnabled} + inputProps={{ style: { fontSize: 12 } }} + /> + setTproxyPort(generateRandomPort())} + title={t("Random Port")} + disabled={!tproxyEnabled} + sx={{ mr: 0.5 }} + > + + + setTproxyEnabled(c)} + sx={{ ml: 0.5 }} + /> +
+
+ )} +
+
+ ); + } +); diff --git a/src/components/setting/mods/controller-viewer.tsx b/src/components/setting/mods/controller-viewer.tsx index 0a0d9b18e..98b83f66a 100644 --- a/src/components/setting/mods/controller-viewer.tsx +++ b/src/components/setting/mods/controller-viewer.tsx @@ -1,20 +1,90 @@ -import { forwardRef, useImperativeHandle, useState } from "react"; -import { useLockFn } from "ahooks"; -import { useTranslation } from "react-i18next"; -import { List, ListItem, ListItemText, TextField } from "@mui/material"; -import { useClashInfo } from "@/hooks/use-clash"; import { BaseDialog, DialogRef } from "@/components/base"; +import { useClashInfo } from "@/hooks/use-clash"; import { showNotice } from "@/services/noticeService"; +import { + ContentCopy, + RefreshRounded +} from "@mui/icons-material"; +import { + Alert, + Box, + FormControlLabel, + IconButton, + List, + ListItem, + ListItemText, + Snackbar, + Switch, + TextField, + Tooltip +} from "@mui/material"; +import { useLocalStorageState, useLockFn } from "ahooks"; +import { forwardRef, useEffect, useImperativeHandle, useState } from "react"; +import { useTranslation } from "react-i18next"; + +// 随机端口和密码生成 +const generateRandomPort = (): number => { + return Math.floor(Math.random() * (65535 - 1024 + 1)) + 1024; +}; + +const generateRandomPassword = (length: number = 32): string => { + const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-_=+[]{}|;:'\",.<>/?"; + let password = ""; + + for (let i = 0; i < length; i++) { + const randomIndex = Math.floor(Math.random() * charset.length); + password += charset.charAt(randomIndex); + } + + return password; +}; + +// 初始化执行一次随机生成 +const useAppInitialization = (autoGenerate: boolean, onGenerate: () => void) => { + const [initialized, setInitialized] = useState(false); + + useEffect(() => { + if (!initialized && autoGenerate) { + onGenerate(); + setInitialized(true); + } + }, [initialized, autoGenerate, onGenerate]); +}; export const ControllerViewer = forwardRef((props, ref) => { const { t } = useTranslation(); const [open, setOpen] = useState(false); + // 防止数值null + const [autoGenerateState, setAutoGenerate] = useLocalStorageState( + 'autoGenerateConfig', + { defaultValue: false as boolean } + ); + const autoGenerate = autoGenerateState!; + + const [copySuccess, setCopySuccess] = useState(null); + const { clashInfo, patchInfo } = useClashInfo(); const [controller, setController] = useState(clashInfo?.server || ""); const [secret, setSecret] = useState(clashInfo?.secret || ""); + // 初始化生成随机配置 + useAppInitialization(autoGenerate, () => { + const port = generateRandomPort(); + const password = generateRandomPassword(); + + const host = controller.split(':')[0] || '127.0.0.1'; + const newController = `${host}:${port}`; + + setController(newController); + setSecret(password); + + patchInfo({ "external-controller": newController, secret: password }); + + showNotice('info', t("Auto generated new config on startup"), 1000); + }); + useImperativeHandle(ref, () => ({ open: () => { setOpen(true); @@ -34,6 +104,38 @@ export const ControllerViewer = forwardRef((props, ref) => { } }); + // 生成随机端口 + const handleGeneratePort = useLockFn(async () => { + if (!autoGenerate) { + const port = generateRandomPort(); + const host = controller.split(':')[0] || '127.0.0.1'; + setController(`${host}:${port}`); + showNotice('success', t("Random port generated"), 1000); + } + return Promise.resolve(); + }); + + // 生成随机 Secret + const handleGenerateSecret = useLockFn(async () => { + if (!autoGenerate) { + const password = generateRandomPassword(); + setSecret(password); + showNotice('success', t("Random secret generated"), 1000); + } + return Promise.resolve(); + }); + + // 复制到剪贴板 + const handleCopyToClipboard = useLockFn(async (text: string, type: string) => { + try { + await navigator.clipboard.writeText(text); + setCopySuccess(type); + setTimeout(() => setCopySuccess(null), 2000); + } catch (err) { + showNotice('error', t("Failed to copy"), 2000); + } + }); + return ( ((props, ref) => { onOk={onSave} > - - - setController(e.target.value)} - /> + + + + + + + + + + + setController(e.target.value)} + disabled={autoGenerate} + /> + {autoGenerate && ( + + + handleCopyToClipboard(controller, "controller")} + color="primary" + > + + + + )} + - - - - setSecret(e.target.value?.replace(/[^\x00-\x7F]/g, "")) + + + + + + + + + + + + setSecret(e.target.value?.replace(/[^\x00-\x7F]/g, "")) + } + disabled={autoGenerate} + /> + {autoGenerate && ( + + handleCopyToClipboard(secret, "secret")} + color="primary" + > + + + + )} + + + + + + setAutoGenerate(!autoGenerate)} + color="primary" + /> } + label={autoGenerate ? t("On") : t("Off")} + labelPlacement="start" /> + + + + {copySuccess === "controller" + ? t("Controller address copied to clipboard") + : t("Secret copied to clipboard") + } + + ); }); diff --git a/src/components/setting/mods/web-ui-viewer.tsx b/src/components/setting/mods/web-ui-viewer.tsx index 569a684f7..34219deaf 100644 --- a/src/components/setting/mods/web-ui-viewer.tsx +++ b/src/components/setting/mods/web-ui-viewer.tsx @@ -67,13 +67,13 @@ export const WebUIViewer = forwardRef((props, ref) => { url = url.replaceAll("%port", port || "9097"); url = url.replaceAll( "%secret", - encodeURIComponent(clashInfo.secret || "") + encodeURIComponent(clashInfo.secret || ""), ); } await openWebUrl(url); } catch (e: any) { - showNotice('error', e.message || e.toString()); + showNotice("error", e.message || e.toString()); } }); diff --git a/src/components/setting/setting-clash.tsx b/src/components/setting/setting-clash.tsx index 4c8db6c74..5eaeec7bd 100644 --- a/src/components/setting/setting-clash.tsx +++ b/src/components/setting/setting-clash.tsx @@ -1,30 +1,29 @@ -import { useRef, useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { TextField, Select, MenuItem, Typography } from "@mui/material"; -import { - SettingsRounded, - ShuffleRounded, - LanRounded, -} from "@mui/icons-material"; import { DialogRef, Switch } from "@/components/base"; +import { TooltipIcon } from "@/components/base/base-tooltip-icon"; import { useClash } from "@/hooks/use-clash"; -import { GuardState } from "./mods/guard-state"; -import { WebUIViewer } from "./mods/web-ui-viewer"; -import { ClashPortViewer } from "./mods/clash-port-viewer"; -import { ControllerViewer } from "./mods/controller-viewer"; -import { SettingList, SettingItem } from "./mods/setting-comp"; -import { ClashCoreViewer } from "./mods/clash-core-viewer"; -import { invoke_uwp_tool } from "@/services/cmds"; -import getSystem from "@/utils/get-system"; +import { useListen } from "@/hooks/use-listen"; import { useVerge } from "@/hooks/use-verge"; import { updateGeoData } from "@/services/api"; -import { TooltipIcon } from "@/components/base/base-tooltip-icon"; -import { NetworkInterfaceViewer } from "./mods/network-interface-viewer"; -import { DnsViewer } from "./mods/dns-viewer"; +import { invoke_uwp_tool } from "@/services/cmds"; +import { showNotice } from "@/services/noticeService"; +import getSystem from "@/utils/get-system"; +import { + LanRounded, + SettingsRounded +} from "@mui/icons-material"; +import { MenuItem, Select, TextField, Typography } from "@mui/material"; import { invoke } from "@tauri-apps/api/core"; import { useLockFn } from "ahooks"; -import { useListen } from "@/hooks/use-listen"; -import { showNotice } from "@/services/noticeService"; +import { useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { ClashCoreViewer } from "./mods/clash-core-viewer"; +import { ClashPortViewer } from "./mods/clash-port-viewer"; +import { ControllerViewer } from "./mods/controller-viewer"; +import { DnsViewer } from "./mods/dns-viewer"; +import { GuardState } from "./mods/guard-state"; +import { NetworkInterfaceViewer } from "./mods/network-interface-viewer"; +import { SettingItem, SettingList } from "./mods/setting-comp"; +import { WebUIViewer } from "./mods/web-ui-viewer"; const isWIN = getSystem() === "windows"; @@ -50,16 +49,9 @@ const SettingClash = ({ onError }: Props) => { // 独立跟踪DNS设置开关状态 const [dnsSettingsEnabled, setDnsSettingsEnabled] = useState(() => { - // 尝试从localStorage获取之前保存的状态 - // 如果重装(或删除数据更新)前开关处于关闭状态,重装后会获取到错误的状态 - // const savedState = localStorage.getItem("dns_settings_enabled"); - // if (savedState !== null) { - // return savedState === "true"; - // } - // 如果没有保存的状态,则从verge配置中获取 return verge?.enable_dns_settings ?? false; }); - + const { addListener } = useListen(); const webRef = useRef(null); @@ -88,24 +80,18 @@ const SettingClash = ({ onError }: Props) => { // 实现DNS设置开关处理函数 const handleDnsToggle = useLockFn(async (enable: boolean) => { try { - // 立即更新UI状态 setDnsSettingsEnabled(enable); - // 保存到localStorage,用于记住用户的选择 localStorage.setItem("dns_settings_enabled", String(enable)); - // 更新verge配置 await patchVerge({ enable_dns_settings: enable }); await invoke("apply_dns_config", { apply: enable }); setTimeout(() => { mutateClash(); - }, 500); // 延迟500ms确保后端完成处理 + }, 500); } catch (err: any) { - // 如果出错,恢复原始状态 setDnsSettingsEnabled(!enable); localStorage.setItem("dns_settings_enabled", String(!enable)); showNotice('error', err.message || err.toString()); - await patchVerge({ enable_dns_settings: !enable }).catch(() => { - // 忽略恢复状态时的错误 - }); + await patchVerge({ enable_dns_settings: !enable }).catch(() => {}); throw err; } }); @@ -219,22 +205,10 @@ const SettingClash = ({ onError }: Props) => { { - showNotice('success', t("Restart Application to Apply Modifications"), 1000); - onChangeVerge({ enable_random_port: !enable_random_port }); - patchVerge({ enable_random_port: !enable_random_port }); - }} - /> - } >