mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-01-28 07:14:40 +08:00
chore(eslint): add eslint-plugin-react-compiler (#5918)
* chore(eslint): add `eslint-plugin-react-compiler` * refactor: fix eslint warnings and avoid in-render mutations --------- Co-authored-by: Slinetrac <realakayuki@gmail.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import configPrettier from "eslint-config-prettier";
|
|||||||
import { createTypeScriptImportResolver } from "eslint-import-resolver-typescript";
|
import { createTypeScriptImportResolver } from "eslint-import-resolver-typescript";
|
||||||
import pluginImportX from "eslint-plugin-import-x";
|
import pluginImportX from "eslint-plugin-import-x";
|
||||||
import pluginPrettier from "eslint-plugin-prettier";
|
import pluginPrettier from "eslint-plugin-prettier";
|
||||||
|
import pluginReactCompiler from "eslint-plugin-react-compiler";
|
||||||
import pluginReactHooks from "eslint-plugin-react-hooks";
|
import pluginReactHooks from "eslint-plugin-react-hooks";
|
||||||
import pluginReactRefresh from "eslint-plugin-react-refresh";
|
import pluginReactRefresh from "eslint-plugin-react-refresh";
|
||||||
import pluginUnusedImports from "eslint-plugin-unused-imports";
|
import pluginUnusedImports from "eslint-plugin-unused-imports";
|
||||||
@@ -19,6 +20,7 @@ export default defineConfig([
|
|||||||
js: eslintJS,
|
js: eslintJS,
|
||||||
// @ts-expect-error -- https://github.com/typescript-eslint/typescript-eslint/issues/11543
|
// @ts-expect-error -- https://github.com/typescript-eslint/typescript-eslint/issues/11543
|
||||||
"react-hooks": pluginReactHooks,
|
"react-hooks": pluginReactHooks,
|
||||||
|
"react-compiler": pluginReactCompiler,
|
||||||
// @ts-expect-error -- https://github.com/un-ts/eslint-plugin-import-x/issues/421
|
// @ts-expect-error -- https://github.com/un-ts/eslint-plugin-import-x/issues/421
|
||||||
"import-x": pluginImportX,
|
"import-x": pluginImportX,
|
||||||
"react-refresh": pluginReactRefresh,
|
"react-refresh": pluginReactRefresh,
|
||||||
@@ -52,6 +54,7 @@ export default defineConfig([
|
|||||||
// React
|
// React
|
||||||
"react-hooks/rules-of-hooks": "error",
|
"react-hooks/rules-of-hooks": "error",
|
||||||
"react-hooks/exhaustive-deps": "error",
|
"react-hooks/exhaustive-deps": "error",
|
||||||
|
"react-compiler/react-compiler": "error",
|
||||||
"react-refresh/only-export-components": [
|
"react-refresh/only-export-components": [
|
||||||
"warn",
|
"warn",
|
||||||
{ allowConstantExport: true },
|
{ allowConstantExport: true },
|
||||||
|
|||||||
@@ -97,6 +97,7 @@
|
|||||||
"eslint-plugin-import-x": "^4.16.1",
|
"eslint-plugin-import-x": "^4.16.1",
|
||||||
"eslint-plugin-prettier": "^5.5.4",
|
"eslint-plugin-prettier": "^5.5.4",
|
||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
|
"eslint-plugin-react-compiler": "^19.1.0-rc.2",
|
||||||
"eslint-plugin-react-refresh": "^0.4.26",
|
"eslint-plugin-react-refresh": "^0.4.26",
|
||||||
"eslint-plugin-unused-imports": "^4.3.0",
|
"eslint-plugin-unused-imports": "^4.3.0",
|
||||||
"glob": "^13.0.0",
|
"glob": "^13.0.0",
|
||||||
|
|||||||
51
pnpm-lock.yaml
generated
51
pnpm-lock.yaml
generated
@@ -192,6 +192,9 @@ importers:
|
|||||||
eslint-plugin-prettier:
|
eslint-plugin-prettier:
|
||||||
specifier: ^5.5.4
|
specifier: ^5.5.4
|
||||||
version: 5.5.4(eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))(prettier@3.7.4)
|
version: 5.5.4(eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))(prettier@3.7.4)
|
||||||
|
eslint-plugin-react-compiler:
|
||||||
|
specifier: ^19.1.0-rc.2
|
||||||
|
version: 19.1.0-rc.2(eslint@9.39.2(jiti@2.6.1))
|
||||||
eslint-plugin-react-hooks:
|
eslint-plugin-react-hooks:
|
||||||
specifier: ^7.0.1
|
specifier: ^7.0.1
|
||||||
version: 7.0.1(eslint@9.39.2(jiti@2.6.1))
|
version: 7.0.1(eslint@9.39.2(jiti@2.6.1))
|
||||||
@@ -393,6 +396,13 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@babel/core': ^7.0.0
|
'@babel/core': ^7.0.0
|
||||||
|
|
||||||
|
'@babel/plugin-proposal-private-methods@7.18.6':
|
||||||
|
resolution: {integrity: sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==}
|
||||||
|
engines: {node: '>=6.9.0'}
|
||||||
|
deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-methods instead.
|
||||||
|
peerDependencies:
|
||||||
|
'@babel/core': ^7.0.0-0
|
||||||
|
|
||||||
'@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2':
|
'@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2':
|
||||||
resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==}
|
resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
@@ -2549,6 +2559,12 @@ packages:
|
|||||||
eslint-config-prettier:
|
eslint-config-prettier:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
eslint-plugin-react-compiler@19.1.0-rc.2:
|
||||||
|
resolution: {integrity: sha512-oKalwDGcD+RX9mf3NEO4zOoUMeLvjSvcbbEOpquzmzqEEM2MQdp7/FY/Hx9NzmUwFzH1W9SKTz5fihfMldpEYw==}
|
||||||
|
engines: {node: ^14.17.0 || ^16.0.0 || >= 18.0.0}
|
||||||
|
peerDependencies:
|
||||||
|
eslint: '>=7'
|
||||||
|
|
||||||
eslint-plugin-react-dom@2.3.13:
|
eslint-plugin-react-dom@2.3.13:
|
||||||
resolution: {integrity: sha512-O9jglTOnnuyfJcSxjeVc8lqIp5kuS9/0MLLCHlOTH8ZjIifHHxUr6GZ2fd4la9y0FsoEYXEO7DBIMjWx2vCwjg==}
|
resolution: {integrity: sha512-O9jglTOnnuyfJcSxjeVc8lqIp5kuS9/0MLLCHlOTH8ZjIifHHxUr6GZ2fd4la9y0FsoEYXEO7DBIMjWx2vCwjg==}
|
||||||
engines: {node: '>=20.19.0'}
|
engines: {node: '>=20.19.0'}
|
||||||
@@ -4109,12 +4125,21 @@ packages:
|
|||||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
zod-validation-error@3.5.4:
|
||||||
|
resolution: {integrity: sha512-+hEiRIiPobgyuFlEojnqjJnhFvg4r/i3cqgcm67eehZf/WBaK3g6cD02YU9mtdVxZjv8CzCA9n/Rhrs3yAAvAw==}
|
||||||
|
engines: {node: '>=18.0.0'}
|
||||||
|
peerDependencies:
|
||||||
|
zod: ^3.24.4
|
||||||
|
|
||||||
zod-validation-error@4.0.2:
|
zod-validation-error@4.0.2:
|
||||||
resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==}
|
resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
zod: ^3.25.0 || ^4.0.0
|
zod: ^3.25.0 || ^4.0.0
|
||||||
|
|
||||||
|
zod@3.25.76:
|
||||||
|
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
|
||||||
|
|
||||||
zod@4.2.1:
|
zod@4.2.1:
|
||||||
resolution: {integrity: sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==}
|
resolution: {integrity: sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==}
|
||||||
|
|
||||||
@@ -4331,6 +4356,14 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
'@babel/plugin-proposal-private-methods@7.18.6(@babel/core@7.28.5)':
|
||||||
|
dependencies:
|
||||||
|
'@babel/core': 7.28.5
|
||||||
|
'@babel/helper-create-class-features-plugin': 7.28.5(@babel/core@7.28.5)
|
||||||
|
'@babel/helper-plugin-utils': 7.27.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
'@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.28.5)':
|
'@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.28.5)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.28.5
|
'@babel/core': 7.28.5
|
||||||
@@ -6626,6 +6659,18 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
eslint-config-prettier: 10.1.8(eslint@9.39.2(jiti@2.6.1))
|
eslint-config-prettier: 10.1.8(eslint@9.39.2(jiti@2.6.1))
|
||||||
|
|
||||||
|
eslint-plugin-react-compiler@19.1.0-rc.2(eslint@9.39.2(jiti@2.6.1)):
|
||||||
|
dependencies:
|
||||||
|
'@babel/core': 7.28.5
|
||||||
|
'@babel/parser': 7.28.5
|
||||||
|
'@babel/plugin-proposal-private-methods': 7.18.6(@babel/core@7.28.5)
|
||||||
|
eslint: 9.39.2(jiti@2.6.1)
|
||||||
|
hermes-parser: 0.25.1
|
||||||
|
zod: 3.25.76
|
||||||
|
zod-validation-error: 3.5.4(zod@3.25.76)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
eslint-plugin-react-dom@2.3.13(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3):
|
eslint-plugin-react-dom@2.3.13(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint-react/ast': 2.3.13(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
'@eslint-react/ast': 2.3.13(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||||
@@ -8583,10 +8628,16 @@ snapshots:
|
|||||||
|
|
||||||
yocto-queue@0.1.0: {}
|
yocto-queue@0.1.0: {}
|
||||||
|
|
||||||
|
zod-validation-error@3.5.4(zod@3.25.76):
|
||||||
|
dependencies:
|
||||||
|
zod: 3.25.76
|
||||||
|
|
||||||
zod-validation-error@4.0.2(zod@4.2.1):
|
zod-validation-error@4.0.2(zod@4.2.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
zod: 4.2.1
|
zod: 4.2.1
|
||||||
|
|
||||||
|
zod@3.25.76: {}
|
||||||
|
|
||||||
zod@4.2.1: {}
|
zod@4.2.1: {}
|
||||||
|
|
||||||
zwitch@2.0.4: {}
|
zwitch@2.0.4: {}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
|
|
||||||
import matchCaseIcon from "@/assets/image/component/match_case.svg?react";
|
import matchCaseIcon from "@/assets/image/component/match_case.svg?react";
|
||||||
import matchWholeWordIcon from "@/assets/image/component/match_whole_word.svg?react";
|
import matchWholeWordIcon from "@/assets/image/component/match_whole_word.svg?react";
|
||||||
import useRegularExpressionIcon from "@/assets/image/component/use_regular_expression.svg?react";
|
import UseRegularExpressionIcon from "@/assets/image/component/use_regular_expression.svg?react";
|
||||||
import { buildRegex, compileStringMatcher } from "@/utils/search-matcher";
|
import { buildRegex, compileStringMatcher } from "@/utils/search-matcher";
|
||||||
|
|
||||||
export type SearchState = {
|
export type SearchState = {
|
||||||
@@ -223,7 +223,7 @@ export const BaseSearchBox = ({
|
|||||||
<Tooltip title={t("shared.placeholders.useRegex")}>
|
<Tooltip title={t("shared.placeholders.useRegex")}>
|
||||||
<div>
|
<div>
|
||||||
<SvgIcon
|
<SvgIcon
|
||||||
component={useRegularExpressionIcon}
|
component={UseRegularExpressionIcon}
|
||||||
aria-label={useRegularExpression ? "active" : "inactive"}
|
aria-label={useRegularExpression ? "active" : "inactive"}
|
||||||
{...iconStyle}
|
{...iconStyle}
|
||||||
onClick={handleToggleUseRegularExpression}
|
onClick={handleToggleUseRegularExpression}
|
||||||
|
|||||||
@@ -123,24 +123,6 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initialDataRef.current = initialData;
|
initialDataRef.current = initialData;
|
||||||
}, [initialData]);
|
}, [initialData]);
|
||||||
// Background refresh: when the dialog/model is ready and the underlying resource key changes,
|
|
||||||
// try to refresh content (only if user hasn't typed). Do NOT depend on `initialData` function
|
|
||||||
// identity because callers often pass inline lambdas that change every render.
|
|
||||||
useEffect(() => {
|
|
||||||
if (!open) return;
|
|
||||||
// Only attempt after initial model is ready to avoid racing the initial load
|
|
||||||
if (!modelPath) return;
|
|
||||||
// Avoid immediate double-load on open: the initial load has just completed.
|
|
||||||
if (skipNextRefreshRef.current) {
|
|
||||||
skipNextRefreshRef.current = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Only meaningful when a callable loader is provided (plain Promise cannot be "recalled")
|
|
||||||
if (typeof initialDataRef.current === "function") {
|
|
||||||
void reloadLatest();
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [open, modelPath, dataKey]);
|
|
||||||
// Helper to (soft) reload latest source and apply only if the user hasn't typed yet
|
// Helper to (soft) reload latest source and apply only if the user hasn't typed yet
|
||||||
const reloadLatest = useLockFn(async () => {
|
const reloadLatest = useLockFn(async () => {
|
||||||
// Snapshot the model/doc identity and bump a token so older calls can't win
|
// Snapshot the model/doc identity and bump a token so older calls can't win
|
||||||
@@ -201,6 +183,23 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
// Background refresh: when the dialog/model is ready and the underlying resource key changes,
|
||||||
|
// try to refresh content (only if user hasn't typed). Do NOT depend on `initialData` function
|
||||||
|
// identity because callers often pass inline lambdas that change every render.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return;
|
||||||
|
// Only attempt after initial model is ready to avoid racing the initial load
|
||||||
|
if (!modelPath) return;
|
||||||
|
// Avoid immediate double-load on open: the initial load has just completed.
|
||||||
|
if (skipNextRefreshRef.current) {
|
||||||
|
skipNextRefreshRef.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Only meaningful when a callable loader is provided (plain Promise cannot be "recalled")
|
||||||
|
if (typeof initialDataRef.current === "function") {
|
||||||
|
void reloadLatest();
|
||||||
|
}
|
||||||
|
}, [open, modelPath, dataKey, reloadLatest]);
|
||||||
|
|
||||||
const beforeMount = () => {
|
const beforeMount = () => {
|
||||||
monacoInitialization();
|
monacoInitialization();
|
||||||
|
|||||||
@@ -88,10 +88,6 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
|||||||
|
|
||||||
const handleOk = useLockFn(
|
const handleOk = useLockFn(
|
||||||
handleSubmit(async (form) => {
|
handleSubmit(async (form) => {
|
||||||
if (form.option?.timeout_seconds) {
|
|
||||||
form.option.timeout_seconds = +form.option.timeout_seconds;
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
// 基本验证
|
// 基本验证
|
||||||
@@ -101,17 +97,21 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 处理表单数据
|
// 处理表单数据
|
||||||
if (form.option?.update_interval) {
|
const option = form.option ? { ...form.option } : undefined;
|
||||||
form.option.update_interval = +form.option.update_interval;
|
if (option?.timeout_seconds) {
|
||||||
} else {
|
option.timeout_seconds = +option.timeout_seconds;
|
||||||
delete form.option?.update_interval;
|
|
||||||
}
|
}
|
||||||
if (form.option?.user_agent === "") {
|
if (option?.update_interval) {
|
||||||
delete form.option.user_agent;
|
option.update_interval = +option.update_interval;
|
||||||
|
} else if (option) {
|
||||||
|
option.update_interval = undefined;
|
||||||
|
}
|
||||||
|
if (option?.user_agent === "") {
|
||||||
|
option.user_agent = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const name = form.name || `${form.type} file`;
|
const name = form.name || `${form.type} file`;
|
||||||
const item = { ...form, name };
|
const item = { ...form, name, option };
|
||||||
const isRemote = form.type === "remote";
|
const isRemote = form.type === "remote";
|
||||||
const isUpdate = openType === "edit";
|
const isUpdate = openType === "edit";
|
||||||
|
|
||||||
|
|||||||
@@ -88,25 +88,19 @@ export const WebUIItem = (props: Props) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const placeholderCounts: Record<string, number> = {};
|
const renderedParts = highlightedParts.map((part, index) => {
|
||||||
let textCounter = 0;
|
|
||||||
const renderedParts = highlightedParts.map((part) => {
|
|
||||||
const isPlaceholder =
|
const isPlaceholder =
|
||||||
part === "%host" || part === "%port" || part === "%secret";
|
part === "%host" || part === "%port" || part === "%secret";
|
||||||
|
const repeatIndex = highlightedParts
|
||||||
|
.slice(0, index)
|
||||||
|
.filter((prev) => prev === part).length;
|
||||||
|
const key = `${part || "empty"}-${repeatIndex}`;
|
||||||
|
|
||||||
if (isPlaceholder) {
|
return (
|
||||||
const count = placeholderCounts[part] ?? 0;
|
<span key={key} className={isPlaceholder ? "placeholder" : undefined}>
|
||||||
placeholderCounts[part] = count + 1;
|
{part}
|
||||||
return (
|
</span>
|
||||||
<span key={`placeholder-${part}-${count}`} className="placeholder">
|
);
|
||||||
{part}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const key = `text-${textCounter}-${part || "empty"}`;
|
|
||||||
textCounter += 1;
|
|
||||||
return <span key={key}>{part}</span>;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -62,18 +62,15 @@ export const useProxySelection = (options: ProxySelectionOptions = {}) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (current && !skipConfigSave) {
|
if (current && !skipConfigSave) {
|
||||||
if (!current.selected) current.selected = [];
|
const selected = current.selected ? [...current.selected] : [];
|
||||||
|
const index = selected.findIndex((item) => item.name === groupName);
|
||||||
const index = current.selected.findIndex(
|
|
||||||
(item) => item.name === groupName,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (index < 0) {
|
if (index < 0) {
|
||||||
current.selected.push({ name: groupName, now: proxyName });
|
selected.push({ name: groupName, now: proxyName });
|
||||||
} else {
|
} else {
|
||||||
current.selected[index] = { name: groupName, now: proxyName };
|
selected[index] = { name: groupName, now: proxyName };
|
||||||
}
|
}
|
||||||
await patchCurrent({ selected: current.selected });
|
await patchCurrent({ selected });
|
||||||
}
|
}
|
||||||
|
|
||||||
await selectNodeForGroup(groupName, proxyName);
|
await selectNodeForGroup(groupName, proxyName);
|
||||||
|
|||||||
Reference in New Issue
Block a user