* add external `cors` control panel

* optimize format

* fix-script.rs

* fix-service.rs

* fix-rs

async_proxy_query.rs

event_driven_proxy.rs

service_ipc.rs

service.rs

sysopt.rs

* lower the prettier version number to 3.5.3

* Revert "lower the prettier version number to 3.5.3"

This reverts commit 0f1c3dfa8abad9f451f32d2da6211e86341bda84.

* fix: prttier erros

* add developer environment detection and controlled the display of development environment URL

* submit required

* fix-external-controller-cors

* use the custom component ToggleButton to ensure a uniform button style

* fix-tsx

hotkey-viewer.tsx

external-controller-cors.tsx

* fix-bug_report.yml

* remove the annoying title

* fix-write overload problem

* Individual button settings

* fix-setting-clash.tsx

---------

Co-authored-by: Tunglies <77394545+Tunglies@users.noreply.github.com>
Co-authored-by: Tunglies <tunglies.dev@outlook.com>
This commit is contained in:
Dyna
2025-06-30 20:48:20 +08:00
committed by GitHub
parent fe13dad06f
commit 7b5afb7afe
14 changed files with 472 additions and 57 deletions

View File

@@ -0,0 +1,354 @@
import { BaseDialog } from "@/components/base";
import { useClash } from "@/hooks/use-clash";
import { showNotice } from "@/services/noticeService";
import { Delete as DeleteIcon } from "@mui/icons-material";
import {
Box,
Button,
Divider,
List,
ListItem,
styled,
TextField,
} from "@mui/material";
import { useLockFn, useRequest } from "ahooks";
import { forwardRef, useImperativeHandle, useState } from "react";
import { useTranslation } from "react-i18next";
// 自定义开关按钮样式
const ToggleButton = styled("label")`
position: relative;
display: inline-block;
width: 48px;
height: 24px;
input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #e0e0e0;
transition: 0.4s;
border-radius: 34px;
&:before {
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 4px;
bottom: 4px;
background-color: white;
transition: 0.4s;
border-radius: 50%;
}
}
input:checked + .slider {
background-color: #2196f3;
}
input:focus + .slider {
box-shadow: 0 0 1px #2196f3;
}
input:checked + .slider:before {
transform: translateX(24px);
}
`;
// 定义开发环境的URL列表
// 这些URL在开发模式下会被自动包含在允许的来源中
// 在生产环境中这些URL会被过滤掉
// 这样可以确保在生产环境中不会意外暴露开发环境的URL
const DEV_URLS = [
"tauri://localhost",
"http://tauri.localhost",
"http://localhost:3000",
];
// 判断是否处于开发模式
const isDevMode = import.meta.env.MODE === "development";
// 过滤开发环境URL
const filterDevOrigins = (origins: string[]) => {
if (isDevMode) {
return origins;
}
return origins.filter((origin: string) => !DEV_URLS.includes(origin.trim()));
};
// 获取完整的源列表包括开发URL
const getFullOrigins = (origins: string[]) => {
if (!isDevMode) {
return origins;
}
// 合并现有源和开发URL并去重
const allOrigins = [...origins, ...DEV_URLS];
const uniqueOrigins = [...new Set(allOrigins)];
return uniqueOrigins;
};
// 统一使用的按钮样式
const buttonStyle = {
borderRadius: "8px",
textTransform: "none",
boxShadow: "0 2px 4px rgba(0,0,0,0.1)",
transition: "all 0.3s ease",
"&:hover": {
boxShadow: "0 4px 8px rgba(0,0,0,0.15)",
transform: "translateY(-1px)",
},
"&:active": {
transform: "translateY(0)",
},
};
// 保存按钮样式
const saveButtonStyle = {
...buttonStyle,
backgroundColor: "#165DFF",
color: "white",
"&:hover": {
backgroundColor: "#0E42D2",
},
};
// 添加按钮样式
const addButtonStyle = {
...buttonStyle,
backgroundColor: "#4CAF50",
color: "white",
"&:hover": {
backgroundColor: "#388E3C",
},
};
// 删除按钮样式
const deleteButtonStyle = {
...buttonStyle,
backgroundColor: "#FF5252",
color: "white",
"&:hover": {
backgroundColor: "#D32F2F",
},
};
interface ClashHeaderConfigingRef {
open: () => void;
close: () => void;
}
export const HeaderConfiguration = forwardRef<ClashHeaderConfigingRef>(
(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"];
const origins = cors?.["allow-origins"] ?? ["*"];
return {
allowPrivateNetwork: cors?.["allow-private-network"] ?? true,
allowOrigins: filterDevOrigins(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 () => {
// 保存时使用完整的源列表包括开发URL
const fullOrigins = getFullOrigins(corsConfig.allowOrigins);
await patchClash({
"external-controller-cors": {
"allow-private-network": corsConfig.allowPrivateNetwork,
"allow-origins": fullOrigins.filter(
(origin: string) => 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"];
const origins = cors?.["allow-origins"] ?? ["*"];
setCorsConfig({
allowPrivateNetwork: cors?.["allow-private-network"] ?? true,
allowOrigins: filterDevOrigins(origins),
});
setOpen(true);
},
close: () => setOpen(false),
}));
const handleSave = useLockFn(async () => {
await saveConfig();
});
return (
<BaseDialog
open={open}
title={t("External Cors Configuration")}
contentSx={{ width: 500 }}
okBtn={loading ? t("Saving...") : t("Save")}
cancelBtn={t("Cancel")}
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
onOk={handleSave}
>
<List sx={{ width: "90%", padding: 2 }}>
<ListItem sx={{ padding: "8px 0" }}>
<Box
display="flex"
justifyContent="space-between"
alignItems="center"
width="100%"
>
<span style={{ fontWeight: "normal" }}>
{t("Allow private network access")}
</span>
<ToggleButton>
<input
type="checkbox"
checked={corsConfig.allowPrivateNetwork}
onChange={(e) =>
handleCorsConfigChange(
"allowPrivateNetwork",
e.target.checked,
)
}
id="private-network-toggle"
/>
<span className="slider"></span>
</ToggleButton>
</Box>
</ListItem>
<Divider sx={{ my: 2 }} />
<ListItem sx={{ padding: "8px 0" }}>
<div style={{ width: "100%" }}>
<div style={{ marginBottom: 8, fontWeight: "bold" }}>
{t("Allowed Origins")}
</div>
{corsConfig.allowOrigins.map((origin, index) => (
<div
key={index}
style={{
display: "flex",
alignItems: "center",
marginBottom: 8,
}}
>
<TextField
fullWidth
size="small"
sx={{ fontSize: 14, marginRight: 2 }}
value={origin}
onChange={(e) => handleUpdateOrigin(index, e.target.value)}
placeholder={t("Please enter a valid url")}
inputProps={{ style: { fontSize: 14 } }}
/>
<Button
variant="contained"
color="error"
size="small"
onClick={() => handleDeleteOrigin(index)}
disabled={corsConfig.allowOrigins.length <= 1}
sx={deleteButtonStyle}
>
<DeleteIcon fontSize="small" />
</Button>
</div>
))}
<Button
variant="contained"
size="small"
onClick={handleAddOrigin}
sx={addButtonStyle}
>
{t("Add")}
</Button>
{isDevMode && (
<div
style={{
marginTop: 12,
padding: 8,
backgroundColor: "#f5f5f5",
borderRadius: 4,
}}
>
<div
style={{ color: "#666", fontSize: 12, fontStyle: "italic" }}
>
{t(
"Development mode: Automatically includes Tauri and localhost origins",
)}
</div>
</div>
)}
</div>
</ListItem>
</List>
</BaseDialog>
);
},
);

View File

@@ -1,12 +1,62 @@
import { forwardRef, useImperativeHandle, useState } from "react";
import { useTranslation } from "react-i18next";
import { useLockFn } from "ahooks";
import { styled, Typography, Switch } from "@mui/material";
import { styled, Typography } from "@mui/material";
import { useVerge } from "@/hooks/use-verge";
import { BaseDialog, DialogRef } from "@/components/base";
import { HotkeyInput } from "./hotkey-input";
import { showNotice } from "@/services/noticeService";
// 修复后的自定义开关组件
const ToggleButton = styled("label")`
position: relative;
display: inline-block;
width: 48px;
height: 24px;
input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #e0e0e0;
transition: 0.4s;
border-radius: 34px;
&:before {
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 4px;
bottom: 4px;
background-color: white;
transition: 0.4s;
border-radius: 50%;
}
}
input:checked + .slider {
background-color: #2196f3;
}
input:focus + .slider {
box-shadow: 0 0 1px #2196f3;
}
input:checked + .slider:before {
transform: translateX(24px);
}
`;
const ItemWrapper = styled("div")`
display: flex;
align-items: center;
@@ -97,11 +147,15 @@ export const HotkeyViewer = forwardRef<DialogRef>((props, ref) => {
>
<ItemWrapper style={{ marginBottom: 16 }}>
<Typography>{t("Enable Global Hotkey")}</Typography>
<Switch
edge="end"
checked={enableGlobalHotkey}
onChange={(e) => setEnableHotkey(e.target.checked)}
/>
<ToggleButton>
<input
type="checkbox"
checked={enableGlobalHotkey}
onChange={(e) => setEnableHotkey(e.target.checked)}
id="global-hotkey-toggle"
/>
<span className="slider"></span>
</ToggleButton>
</ItemWrapper>
{HOTKEY_FUNC.map((func) => (

View File

@@ -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<DialogRef>(null);
const networkRef = useRef<DialogRef>(null);
const dnsRef = useRef<DialogRef>(null);
const corsRef = useRef<DialogRef>(null);
const onSwitchFormat = (_e: any, value: boolean) => value;
const onChangeData = (patch: Partial<IConfigData>) => {
@@ -101,6 +103,7 @@ const SettingClash = ({ onError }: Props) => {
<ClashCoreViewer ref={coreRef} />
<NetworkInterfaceViewer ref={networkRef} />
<DnsViewer ref={dnsRef} />
<HeaderConfiguration ref={corsRef} />
<SettingItem
label={t("Allow Lan")}
@@ -215,18 +218,20 @@ const SettingClash = ({ onError }: Props) => {
</SettingItem>
<SettingItem
onClick={() => ctrlRef.current?.open()}
label={
<>
{t("External")}
<TooltipIcon
title={t(
"Enable one-click random API port and key. Click to randomize the port and key",
)}
sx={{ opacity: "0.7" }}
/>
</>
label={<>{t("External")}</>}
extra={
<TooltipIcon
title={t("External Cors Settings")}
icon={SettingsRounded}
onClick={(e) => {
e.stopPropagation();
corsRef.current?.open();
}}
/>
}
onClick={() => {
ctrlRef.current?.open();
}}
/>
<SettingItem onClick={() => webRef.current?.open()} label={t("Web UI")} />