From c8a314fb41521432756f47afabb0e2d0e8409196 Mon Sep 17 00:00:00 2001 From: Ahao Date: Fri, 27 Jun 2025 13:29:38 +0800 Subject: [PATCH] add external `cors` control panel --- UPDATELOG.md | 1 + .../setting/mods/external-controller-cors.tsx | 182 ++++++++++++++++++ src/components/setting/setting-clash.tsx | 18 ++ src/locales/en.json | 10 +- src/locales/zh.json | 10 +- src/services/types.d.ts | 4 + 6 files changed, 223 insertions(+), 2 deletions(-) create mode 100644 src/components/setting/mods/external-controller-cors.tsx diff --git a/UPDATELOG.md b/UPDATELOG.md index bb495b10..5b62f5e2 100644 --- a/UPDATELOG.md +++ b/UPDATELOG.md @@ -14,6 +14,7 @@ - `sidecar` 模式下清理多余的内核进程,防止运行出现异常 - 新 macOS 下 TUN 和系统代理模式托盘图标(暂测) - 快捷键事件通过系统通知 +- 添加外部 `cors` 控制面板 ### 🚀 优化改进 diff --git a/src/components/setting/mods/external-controller-cors.tsx b/src/components/setting/mods/external-controller-cors.tsx new file mode 100644 index 00000000..8e48b09e --- /dev/null +++ b/src/components/setting/mods/external-controller-cors.tsx @@ -0,0 +1,182 @@ +import { BaseDialog } from "@/components/base"; +import { useClash } from "@/hooks/use-clash"; +import { showNotice } from "@/services/noticeService"; +import { + Button, + Divider, + FormControlLabel, + List, + ListItem, + Switch, + TextField +} from "@mui/material"; +import { Delete as DeleteIcon } from "@mui/icons-material"; +import { useLockFn, useRequest } from "ahooks"; +import { forwardRef, useImperativeHandle, useState } from "react"; +import { useTranslation } from "react-i18next"; + +interface ClashHeaderConfigingRef { + open: () => void; + close: () => void; +} + +export const HeaderConfiguration = forwardRef( + (props, ref) => { + const { t } = useTranslation(); + const { clash, mutateClash, patchClash } = useClash(); + const [open, setOpen] = useState(false); + + // CORS配置状态管理 + const [corsConfig, setCorsConfig] = useState<{ + allowPrivateNetwork: boolean; + allowOrigins: string[]; + }>(() => { + const cors = clash?.["external-controller-cors"]; + return { + allowPrivateNetwork: cors?.["allow-private-network"] ?? true, + allowOrigins: cors?.["allow-origins"] ?? ["*"] + }; + }); + + // 处理CORS配置变更 + const handleCorsConfigChange = ( + key: "allowPrivateNetwork" | "allowOrigins", + value: boolean | string[] + ) => { + setCorsConfig(prev => ({ + ...prev, + [key]: value + })); + }; + + // 添加新的允许来源 + const handleAddOrigin = () => { + handleCorsConfigChange("allowOrigins", [...corsConfig.allowOrigins, ""]); + }; + + // 更新允许来源列表中的某一项 + const handleUpdateOrigin = (index: number, value: string) => { + const newOrigins = [...corsConfig.allowOrigins]; + newOrigins[index] = value; + handleCorsConfigChange("allowOrigins", newOrigins); + }; + + // 删除允许来源列表中的某一项 + const handleDeleteOrigin = (index: number) => { + const newOrigins = [...corsConfig.allowOrigins]; + newOrigins.splice(index, 1); + handleCorsConfigChange("allowOrigins", newOrigins); + }; + + // 保存配置请求 + const { loading, run: saveConfig } = useRequest( + async () => { + await patchClash({ + "external-controller-cors": { + "allow-private-network": corsConfig.allowPrivateNetwork, + "allow-origins": corsConfig.allowOrigins.filter(origin => origin.trim() !== "") + } + }); + await mutateClash(); + }, + { + manual: true, + onSuccess: () => { + setOpen(false); + showNotice("success", t("Configuration saved successfully")); + }, + onError: () => { + showNotice("error", t("Failed to save configuration")); + } + } + ); + + useImperativeHandle(ref, () => ({ + open: () => { + const cors = clash?.["external-controller-cors"]; + setCorsConfig({ + allowPrivateNetwork: cors?.["allow-private-network"] ?? true, + allowOrigins: cors?.["allow-origins"] ?? ["*"] + }); + setOpen(true); + }, + close: () => setOpen(false), + })); + + const handleSave = useLockFn(async () => { + await saveConfig(); + }); + + return ( + setOpen(false)} + onCancel={() => setOpen(false)} + onOk={handleSave} + > + + + + {t("External Controller CORS Settings")} + + + + handleCorsConfigChange("allowPrivateNetwork", e.target.checked)} + /> + } + label={t("Allow private network access")} + /> + + + + + +
+
+ {t("Allowed Origins")} +
+ {corsConfig.allowOrigins.map((origin, index) => ( +
+ handleUpdateOrigin(index, e.target.value)} + placeholder={t("Please enter a valid url")} + inputProps={{ style: { fontSize: 14 } }} + /> + +
+ ))} + +
+
+
+
+ ); + } +); diff --git a/src/components/setting/setting-clash.tsx b/src/components/setting/setting-clash.tsx index a56bf3ce..4639febc 100644 --- a/src/components/setting/setting-clash.tsx +++ b/src/components/setting/setting-clash.tsx @@ -21,6 +21,7 @@ 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"; +import { HeaderConfiguration } from "./mods/external-controller-cors"; const isWIN = getSystem() === "windows"; @@ -57,6 +58,7 @@ const SettingClash = ({ onError }: Props) => { const coreRef = useRef(null); const networkRef = useRef(null); const dnsRef = useRef(null); + const corsRef = useRef(null); const onSwitchFormat = (_e: any, value: boolean) => value; const onChangeData = (patch: Partial) => { @@ -101,6 +103,7 @@ const SettingClash = ({ onError }: Props) => { + { } /> + corsRef.current?.open()} + label={ + <> + {t("External Cors")} + + + } + /> + webRef.current?.open()} label={t("Web UI")} />