Sfoglia il codice sorgente

feat: rules editor

MystiPanda 11 mesi fa
parent
commit
901a983150

+ 1 - 1
package.json

@@ -24,6 +24,7 @@
     "@emotion/react": "^11.11.4",
     "@emotion/styled": "^11.11.5",
     "@juggle/resize-observer": "^3.4.0",
+    "@monaco-editor/react": "^4.6.0",
     "@mui/icons-material": "^5.15.20",
     "@mui/lab": "5.0.0-alpha.149",
     "@mui/material": "^5.15.20",
@@ -47,7 +48,6 @@
     "react-hook-form": "^7.52.0",
     "react-i18next": "^13.5.0",
     "react-markdown": "^9.0.1",
-    "react-monaco-editor": "^0.55.0",
     "react-router-dom": "^6.23.1",
     "react-transition-group": "^4.4.5",
     "react-virtuoso": "^4.7.11",

+ 41 - 20
pnpm-lock.yaml

@@ -25,6 +25,9 @@ importers:
       "@juggle/resize-observer":
         specifier: ^3.4.0
         version: 3.4.0
+      "@monaco-editor/react":
+        specifier: ^4.6.0
+        version: 4.6.0(monaco-editor@0.49.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
       "@mui/icons-material":
         specifier: ^5.15.20
         version: 5.15.20(@mui/material@5.15.20(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.3)(react@18.3.1)
@@ -94,9 +97,6 @@ importers:
       react-markdown:
         specifier: ^9.0.1
         version: 9.0.1(@types/react@18.3.3)(react@18.3.1)
-      react-monaco-editor:
-        specifier: ^0.55.0
-        version: 0.55.0(@types/react@18.3.3)(monaco-editor@0.49.0)(react@18.3.1)
       react-router-dom:
         specifier: ^6.23.1
         version: 6.23.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -1546,6 +1546,24 @@ packages:
         integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==,
       }
 
+  "@monaco-editor/loader@1.4.0":
+    resolution:
+      {
+        integrity: sha512-00ioBig0x642hytVspPl7DbQyaSWRaolYie/UFNjoTdvoKPzo6xrXLhTk9ixgIKcLH5b5vDOjVNiGyY+uDCUlg==,
+      }
+    peerDependencies:
+      monaco-editor: ">= 0.21.0 < 1"
+
+  "@monaco-editor/react@4.6.0":
+    resolution:
+      {
+        integrity: sha512-RFkU9/i7cN2bsq/iTkurMWOEErmYcY6JiQI3Jn+WeR/FGISH8JbHERjpS9oRuSOPvDMJI0Z8nJeKkbOs9sBYQw==,
+      }
+    peerDependencies:
+      monaco-editor: ">= 0.25.0 < 1"
+      react: ^16.8.0 || ^17.0.0 || ^18.0.0
+      react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
+
   "@mui/base@5.0.0-beta.20":
     resolution:
       {
@@ -3845,16 +3863,6 @@ packages:
       "@types/react": ">=18"
       react: ">=18"
 
-  react-monaco-editor@0.55.0:
-    resolution:
-      {
-        integrity: sha512-GdEP0Q3Rn1dczfKEEyY08Nes5plWwIYU4sWRBQO0+jsQWQsKMHKCC6+hPRwR7G/4aA3V/iU9jSmWPzVJYMVFSQ==,
-      }
-    peerDependencies:
-      "@types/react": ">=16 <= 18"
-      monaco-editor: ^0.44.0
-      react: ">=16 <= 18"
-
   react-refresh@0.14.2:
     resolution:
       {
@@ -4098,6 +4106,12 @@ packages:
         integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==,
       }
 
+  state-local@1.0.7:
+    resolution:
+      {
+        integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==,
+      }
+
   stringify-entities@4.0.4:
     resolution:
       {
@@ -5525,6 +5539,18 @@ snapshots:
 
   "@juggle/resize-observer@3.4.0": {}
 
+  "@monaco-editor/loader@1.4.0(monaco-editor@0.49.0)":
+    dependencies:
+      monaco-editor: 0.49.0
+      state-local: 1.0.7
+
+  "@monaco-editor/react@4.6.0(monaco-editor@0.49.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)":
+    dependencies:
+      "@monaco-editor/loader": 1.4.0(monaco-editor@0.49.0)
+      monaco-editor: 0.49.0
+      react: 18.3.1
+      react-dom: 18.3.1(react@18.3.1)
+
   "@mui/base@5.0.0-beta.20(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)":
     dependencies:
       "@babel/runtime": 7.24.7
@@ -7000,13 +7026,6 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  react-monaco-editor@0.55.0(@types/react@18.3.3)(monaco-editor@0.49.0)(react@18.3.1):
-    dependencies:
-      "@types/react": 18.3.3
-      monaco-editor: 0.49.0
-      prop-types: 15.8.1
-      react: 18.3.1
-
   react-refresh@0.14.2: {}
 
   react-router-dom@6.23.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
@@ -7163,6 +7182,8 @@ snapshots:
 
   space-separated-tokens@2.0.2: {}
 
+  state-local@1.0.7: {}
+
   stringify-entities@4.0.4:
     dependencies:
       character-entities-html4: 2.1.0

+ 1 - 0
src/components/profile/profile-item.tsx

@@ -483,6 +483,7 @@ export const ProfileItem = (props: Props) => {
         onClose={() => setFileOpen(false)}
       />
       <RulesEditorViewer
+        profileUid={uid}
         property={option?.rules ?? ""}
         open={rulesOpen}
         onChange={onChange}

+ 186 - 92
src/components/profile/rules-editor-viewer.tsx

@@ -1,7 +1,8 @@
-import { ReactNode, useEffect, useState, useRef, useCallback } from "react";
+import { ReactNode, useEffect, useState } from "react";
 import { useLockFn } from "ahooks";
 import yaml from "js-yaml";
 import { useTranslation } from "react-i18next";
+
 import {
   Autocomplete,
   Button,
@@ -12,21 +13,18 @@ import {
   List,
   ListItem,
   ListItemText,
-  MenuItem,
-  Select,
   TextField,
   styled,
 } from "@mui/material";
 import { useThemeMode } from "@/services/states";
 import { readProfileFile, saveProfileFile } from "@/services/cmds";
-import { Notice } from "@/components/base";
+import { Notice, Switch } from "@/components/base";
 import getSystem from "@/utils/get-system";
 
-import MonacoEditor from "react-monaco-editor";
-import * as monaco from "monaco-editor";
-import { nanoid } from "nanoid";
+import Editor from "@monaco-editor/react";
 
 interface Props {
+  profileUid: string;
   title?: string | ReactNode;
   property: string;
   open: boolean;
@@ -63,37 +61,95 @@ const RuleTypeList = [
   "DSCP",
   "RULE-SET",
   "SUB-RULE",
+  "AND",
+  "OR",
+  "NOT",
   "MATCH",
+] as const;
+
+const NoResolveList = [
+  "GEOIP",
+  "IP-ASN",
+  "IP-CIDR",
+  "IP-CIDR6",
+  "IP-SUFFIX",
+  "RULE-SET",
 ];
+const ExampleMap = {
+  DOMAIN: "example.com",
+  "DOMAIN-SUFFIX": "example.com",
+  "DOMAIN-KEYWORD": "example",
+  "DOMAIN-REGEX": "example.*",
+  GEOSITE: "youtube",
+  "IP-CIDR": "127.0.0.0/8",
+  "IP-SUFFIX": "8.8.8.8/24",
+  "IP-ASN": "13335",
+  GEOIP: "CN",
+  "SRC-GEOIP": "cn",
+  "SRC-IP-ASN": "9808",
+  "SRC-IP-CIDR": "192.168.1.201/32",
+  "SRC-IP-SUFFIX": "192.168.1.201/8",
+  "DST-PORT": "80",
+  "SRC-PORT": "7777",
+  "IN-PORT": "7890",
+  "IN-TYPE": "SOCKS/HTTP",
+  "IN-USER": "mihomo",
+  "IN-NAME": "ss",
+  "PROCESS-PATH":
+    getSystem() === "windows"
+      ? "C:Program FilesGoogleChromeApplicationchrome.exe"
+      : "/usr/bin/wget",
+  "PROCESS-PATH-REGEX":
+    getSystem() === "windows" ? "(?i).*Application\\chrome.*" : ".*bin/wget",
+  "PROCESS-NAME": getSystem() === "windows" ? "chrome.exe" : "curl",
+  "PROCESS-NAME-REGEX": ".*telegram.*",
+  UID: "1001",
+  NETWORK: "udp",
+  DSCP: "4",
+  "RULE-SET": "providername",
+  "SUB-RULE": "",
+  AND: "((DOMAIN,baidu.com),(NETWORK,UDP))",
+  OR: "((NETWORK,UDP),(DOMAIN,baidu.com))",
+  NOT: "((DOMAIN,baidu.com))",
+  MATCH: "",
+};
+
+const BuiltinProxyPolicyList = ["DIRECT", "REJECT", "REJECT-DROP", "PASS"];
 
 export const RulesEditorViewer = (props: Props) => {
-  const { title, property, open, onClose, onChange } = props;
+  const { title, profileUid, property, open, onClose, onChange } = props;
   const { t } = useTranslation();
 
-  const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>(); // 编辑器实例
-  const monacoRef = useRef<typeof monaco>(); // monaco 实例
-  const monacoHoverProviderRef = useRef<monaco.IDisposable>(); // monaco 注册缓存
-  const monacoCompletionItemProviderRef = useRef<monaco.IDisposable>(); // monaco 注册缓存
-
-  // 获取编辑器实例
-  const editorDidMountHandle = useCallback(
-    (editor: monaco.editor.IStandaloneCodeEditor, monacoIns: typeof monaco) => {
-      editorRef.current = editor;
-      monacoRef.current = monacoIns;
-    },
-    []
-  );
-
   const themeMode = useThemeMode();
   const [prevData, setPrevData] = useState("");
   const [currData, setCurrData] = useState("");
-  const [method, setMethod] = useState("append");
-  const [ruleType, setRuleType] = useState("DOMAIN");
+  const [rule, setRule] = useState("");
+  const [ruleType, setRuleType] =
+    useState<(typeof RuleTypeList)[number]>("DOMAIN");
   const [ruleContent, setRuleContent] = useState("");
-  const [proxyPolicy, setProxyPolicy] = useState("");
+  const [noResolve, setNoResolve] = useState(false);
+  const [proxyPolicy, setProxyPolicy] = useState("DIRECT");
+  const [proxyPolicyList, setProxyPolicyList] = useState<string[]>([]);
+  const [ruleList, setRuleList] = useState<string[]>([]);
 
-  const uri = monaco.Uri.parse(`${nanoid()}`);
-  const model = monaco.editor.createModel(prevData, "yaml", uri);
+  const editorOptions = {
+    tabSize: 2,
+    minimap: { enabled: false },
+    mouseWheelZoom: true,
+    quickSuggestions: {
+      strings: true,
+      comments: true,
+      other: true,
+    },
+    padding: {
+      top: 33,
+    },
+    fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${
+      getSystem() === "windows" ? ", twemoji mozilla" : ""
+    }`,
+    fontLigatures: true,
+    smoothScrolling: true,
+  };
 
   const fetchContent = async () => {
     let data = await readProfileFile(property);
@@ -101,43 +157,59 @@ export const RulesEditorViewer = (props: Props) => {
     setPrevData(data);
   };
 
-  const addSeq = async () => {
+  const fetchProfile = async () => {
+    let data = await readProfileFile(profileUid);
+    let obj = yaml.load(data) as { "proxy-groups": []; proxies: []; rules: [] };
+    if (!obj["proxy-groups"]) {
+      obj = { "proxy-groups": [], proxies: [], rules: [] };
+    }
+    setProxyPolicyList(
+      BuiltinProxyPolicyList.concat(
+        obj["proxy-groups"].map((item: any) => item.name)
+      )
+    );
+    setRuleList(obj.rules);
+  };
+
+  const addSeq = async (method: "prepend" | "append" | "delete") => {
     let obj = yaml.load(currData) as ISeqProfileConfig;
     if (!obj.prepend) {
       obj = { prepend: [], append: [], delete: [] };
     }
     switch (method) {
       case "append": {
-        obj.append.push(`${ruleType},${ruleContent},${proxyPolicy}`);
+        obj.append.push(
+          `${ruleType}${
+            ruleType === "MATCH" ? "" : "," + ruleContent
+          },${proxyPolicy}${
+            NoResolveList.includes(ruleType) && noResolve ? ",no-resolve" : ""
+          }`
+        );
         break;
       }
       case "prepend": {
-        obj.prepend.push(`${ruleType},${ruleContent},${proxyPolicy}`);
+        obj.prepend.push(
+          `${ruleType}${
+            ruleType === "MATCH" ? "" : "," + ruleContent
+          },${proxyPolicy}${
+            NoResolveList.includes(ruleType) && noResolve ? ",no-resolve" : ""
+          }`
+        );
         break;
       }
       case "delete": {
-        obj.delete.push(`${ruleType},${ruleContent},${proxyPolicy}`);
+        obj.delete.push(rule);
         break;
       }
     }
     let raw = yaml.dump(obj);
 
-    await saveProfileFile(property, raw);
     setCurrData(raw);
   };
 
   useEffect(() => {
     fetchContent();
-  }, []);
-
-  useEffect(() => {
-    return () => {
-      if (editorRef.current) {
-        editorRef.current.dispose();
-      }
-      monacoCompletionItemProviderRef.current?.dispose();
-      monacoHoverProviderRef.current?.dispose();
-    };
+    fetchProfile();
   }, [open]);
 
   const onSave = useLockFn(async () => {
@@ -152,7 +224,7 @@ export const RulesEditorViewer = (props: Props) => {
 
   return (
     <Dialog open={open} onClose={onClose} maxWidth="xl" fullWidth>
-      <DialogTitle>{title ?? t("Edit File")}</DialogTitle>
+      <DialogTitle>{title ?? t("Edit Rules")}</DialogTitle>
 
       <DialogContent sx={{ display: "flex", width: "auto", height: "100vh" }}>
         <div
@@ -162,32 +234,11 @@ export const RulesEditorViewer = (props: Props) => {
           }}
         >
           <List>
-            <Item>
-              <ListItemText primary={t("Add Method")} />
-              <Select
-                size="small"
-                sx={{ width: "100px" }}
-                value={method}
-                onChange={(e) => {
-                  setMethod(e.target.value);
-                }}
-              >
-                <MenuItem key="prepend" value="prepend">
-                  <span style={{ fontSize: 14 }}>Prepend</span>
-                </MenuItem>
-                <MenuItem key="append" value="append">
-                  <span style={{ fontSize: 14 }}>Append</span>
-                </MenuItem>
-                <MenuItem key="delete" value="delete">
-                  <span style={{ fontSize: 14 }}>Delete</span>
-                </MenuItem>
-              </Select>
-            </Item>
             <Item>
               <ListItemText primary={t("Rule Type")} />
               <Autocomplete
                 size="small"
-                sx={{ width: "300px" }}
+                sx={{ minWidth: "240px" }}
                 value={ruleType}
                 options={RuleTypeList}
                 onChange={(_, v) => {
@@ -200,7 +251,9 @@ export const RulesEditorViewer = (props: Props) => {
               <ListItemText primary={t("Rule Content")} />
               <TextField
                 size="small"
+                sx={{ minWidth: "240px" }}
                 value={ruleContent}
+                placeholder={ExampleMap[ruleType]}
                 onChange={(e) => {
                   setRuleContent(e.target.value);
                 }}
@@ -208,52 +261,93 @@ export const RulesEditorViewer = (props: Props) => {
             </Item>
             <Item>
               <ListItemText primary={t("Proxy Policy")} />
-              <TextField
+              <Autocomplete
                 size="small"
+                sx={{ minWidth: "240px" }}
                 value={proxyPolicy}
-                onChange={(e) => {
-                  setProxyPolicy(e.target.value);
+                options={proxyPolicyList}
+                onChange={(_, v) => {
+                  if (v) setProxyPolicy(v);
                 }}
+                renderInput={(params) => <TextField {...params} />}
               />
             </Item>
+            {NoResolveList.includes(ruleType) && (
+              <Item>
+                <ListItemText primary={t("No Resolve")} />
+                <Switch
+                  checked={noResolve}
+                  onChange={() => {
+                    setNoResolve(!noResolve);
+                  }}
+                />
+              </Item>
+            )}
           </List>
-          <Button fullWidth variant="contained" onClick={addSeq}>
-            Add
-          </Button>
+          <Item>
+            <Button
+              fullWidth
+              variant="contained"
+              onClick={() => {
+                addSeq("prepend");
+              }}
+            >
+              {t("Add Prepend Rule")}
+            </Button>
+          </Item>
+          <Item>
+            <Button
+              fullWidth
+              variant="contained"
+              onClick={() => {
+                addSeq("append");
+              }}
+            >
+              {t("Add Append Rule")}
+            </Button>
+          </Item>
+          <Item>
+            <Autocomplete
+              fullWidth
+              size="small"
+              sx={{ minWidth: "240px" }}
+              value={rule}
+              options={ruleList}
+              onChange={(_, v) => {
+                if (v) setRule(v);
+              }}
+              renderInput={(params) => <TextField {...params} />}
+            />
+          </Item>
+          <Item>
+            <Button
+              fullWidth
+              variant="contained"
+              onClick={() => {
+                addSeq("delete");
+              }}
+            >
+              {t("Delete Rule")}
+            </Button>
+          </Item>
         </div>
         <div
           style={{
             display: "inline-block",
             width: "50%",
             height: "100%",
+            marginLeft: "10px",
           }}
         >
-          <MonacoEditor
+          <Editor
             language="yaml"
             theme={themeMode === "light" ? "vs" : "vs-dark"}
             height="100%"
             value={currData}
-            onChange={setCurrData}
-            options={{
-              model,
-              tabSize: 2,
-              minimap: { enabled: false }, // 超过一定宽度显示minimap滚动条
-              mouseWheelZoom: true, // 按住Ctrl滚轮调节缩放比例
-              quickSuggestions: {
-                strings: true, // 字符串类型的建议
-                comments: true, // 注释类型的建议
-                other: true, // 其他类型的建议
-              },
-              padding: {
-                top: 33, // 顶部padding防止遮挡snippets
-              },
-              fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${
-                getSystem() === "windows" ? ", twemoji mozilla" : ""
-              }`,
-              fontLigatures: true, // 连字符
-              smoothScrolling: true, // 平滑滚动
+            onChange={(value, _) => {
+              if (value) setCurrData(value);
             }}
-            editorDidMount={editorDidMountHandle}
+            options={editorOptions}
           />
         </div>
       </DialogContent>

+ 7 - 0
src/locales/en.json

@@ -52,6 +52,13 @@
   "Edit Profile": "Edit Profile",
   "Edit Proxies": "Edit Proxies",
   "Edit Rules": "Edit Rules",
+  "Rule Type": "Rule Type",
+  "Rule Content": "Rule Content",
+  "Proxy Policy": "roxy Policy",
+  "No Resolve": "No Resolve",
+  "Add Prepend Rule": "Add Prepend Rule",
+  "Add Append Rule": "Add Append Rule",
+  "Delete Rule": "Delete Rule",
   "Edit Groups": "Edit Proxy Groups",
   "Edit Merge": "Edit Merge",
   "Edit Script": "Edit Script",

+ 7 - 0
src/locales/zh.json

@@ -52,6 +52,13 @@
   "Edit Profile": "编辑配置",
   "Edit Proxies": "添加/删除 节点",
   "Edit Rules": "添加/删除 规则",
+  "Rule Type": "规则类型",
+  "Rule Content": "规则内容",
+  "Proxy Policy": "代理策略",
+  "No Resolve": "跳过DNS解析",
+  "Add Prepend Rule": "添加前置规则",
+  "Add Append Rule": "添加后置规则",
+  "Delete Rule": "删除规则",
   "Edit Groups": "添加/删除 代理组",
   "Edit Merge": "微调配置 (yaml)",
   "Edit Script": "微调配置 (js)",