feat: support ws proxy

This commit is contained in:
ReaJason
2026-01-18 23:49:41 +08:00
parent 54be0712d5
commit 4ec1f06362
22 changed files with 577 additions and 281 deletions
+109 -98
View File
@@ -1,16 +1,5 @@
import {
ArrowUpRightIcon,
AxeIcon,
CommandIcon,
InfoIcon,
NetworkIcon,
ServerIcon,
ShieldOffIcon,
SwordIcon,
WaypointsIcon,
ZapIcon,
} from "lucide-react";
import { type JSX, useCallback, useRef, useState } from "react";
import { ArrowUpRightIcon, InfoIcon, ServerIcon } from "lucide-react";
import { useCallback, useEffect, useMemo } from "react";
import { Controller, type UseFormReturn, useWatch } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { AntSwordTabContent } from "@/components/memshell/tabs/antsword-tab";
@@ -51,17 +40,7 @@ import type { MemShellFormSchema } from "@/types/schema";
import { Spinner } from "../ui/spinner";
import { JREVersionFormField } from "./jreversion-field";
import { ServerVersionFormField } from "./serverversion-field";
const shellToolIcons: Record<ShellToolType, JSX.Element> = {
[ShellToolType.Behinder]: <ShieldOffIcon className="h-4 w-4" />,
[ShellToolType.Godzilla]: <AxeIcon className="h-4 w-4" />,
[ShellToolType.Command]: <CommandIcon className="h-4 w-4" />,
[ShellToolType.AntSword]: <SwordIcon className="h-4 w-4" />,
[ShellToolType.Suo5]: <WaypointsIcon className="h-4 w-4" />,
[ShellToolType.Suo5v2]: <WaypointsIcon className="h-4 w-4" />,
[ShellToolType.NeoreGeorg]: <NetworkIcon className="h-4 w-4" />,
[ShellToolType.Custom]: <ZapIcon className="h-4 w-4" />,
};
import { ProxyTabContent } from "./tabs/proxy-tab";
export default function MainConfigCard({
mainConfig,
@@ -74,62 +53,108 @@ export default function MainConfigCard({
}>) {
const { t } = useTranslation(["common", "memshell"]);
const [shellToolMap, setShellToolMap] = useState<{
[toolName: string]: string[];
}>();
const [shellTools, setShellTools] = useState<ShellToolType[]>([]);
const [shellTypes, setShellTypes] = useState<string[]>([]);
const server = useWatch({
control: form.control,
name: "server",
});
const shellTool = useWatch({
control: form.control,
name: "shellTool",
});
const handleServerChange = useCallback(
(value: string) => {
if (mainConfig) {
const newShellToolMap = mainConfig[value];
setShellToolMap(newShellToolMap);
const serverToolMap = useMemo(() => {
if (!mainConfig || !server) {
return undefined;
}
return mainConfig[server];
}, [mainConfig, server]);
const newShellTools = Object.keys(newShellToolMap);
setShellTools([
...newShellTools.map((tool) => tool as ShellToolType),
ShellToolType.Custom,
]);
const serverOptions = useMemo(() => Object.keys(servers ?? {}), [servers]);
const currentShellTool = form.getValues("shellTool");
const shellTools = useMemo(() => {
if (!serverToolMap) {
return [];
}
const tools = Object.keys(serverToolMap).map(
(tool) => tool as ShellToolType,
);
return Array.from(new Set([...tools, ShellToolType.Custom]));
}, [serverToolMap]);
const firstTool = newShellTools[0];
let currentShellTypes = null;
const customShellTypes = useMemo(() => {
if (!server) {
return [];
}
return servers?.[server] ?? [];
}, [server, servers]);
if (!newShellToolMap[currentShellTool]) {
form.setValue("shellTool", firstTool);
currentShellTypes = newShellToolMap[firstTool];
} else {
currentShellTypes = newShellToolMap[currentShellTool];
const shellTypes = useMemo(() => {
if (!serverToolMap || !server) {
return [];
}
if (shellTool === ShellToolType.Custom) {
return customShellTypes;
}
return serverToolMap[shellTool] ?? [];
}, [customShellTypes, server, serverToolMap, shellTool]);
useEffect(() => {
if (!mainConfig || !server) {
return;
}
const toolMap = mainConfig[server];
if (!toolMap) {
return;
}
const toolKeys = Object.keys(toolMap);
if (toolKeys.length === 0) {
return;
}
const currentShellTool = form.getValues("shellTool") as ShellToolType;
const nextShellTool = toolMap[currentShellTool]
? currentShellTool
: (toolKeys[0] as ShellToolType);
if (nextShellTool !== currentShellTool) {
form.setValue("shellTool", nextShellTool);
}
if (nextShellTool !== ShellToolType.Custom) {
const nextShellTypes = toolMap[nextShellTool] ?? [];
if (nextShellTypes.length > 0) {
const currentShellType = form.getValues("shellType");
if (currentShellType !== nextShellTypes[0]) {
form.setValue("shellType", nextShellTypes[0]);
}
setShellTypes(currentShellTypes);
if (currentShellTypes && currentShellTypes.length > 0) {
form.setValue("shellType", currentShellTypes[0]);
}
if (
(value === "SpringWebFlux" || value === "XXLJOB") &&
Number.parseInt(form.getValues("targetJdkVersion") as string, 10) < 52
) {
form.setValue("targetJdkVersion", "52");
} else {
form.setValue("targetJdkVersion", "50");
}
form.resetField("serverVersion");
form.resetField("byPassJavaModule");
form.resetField("urlPattern");
}
},
[form, mainConfig],
);
}
const currentTargetJdk = form.getValues("targetJdkVersion") as string;
const currentJdkVersion = Number.parseInt(currentTargetJdk, 10);
const shouldRaiseJdkVersion =
(server === "SpringWebFlux" || server === "XXLJOB") &&
currentJdkVersion < 52;
const nextJdkVersion = shouldRaiseJdkVersion ? "52" : "50";
if (currentTargetJdk !== nextJdkVersion) {
form.setValue("targetJdkVersion", nextJdkVersion);
}
form.resetField("serverVersion");
form.resetField("byPassJavaModule");
form.resetField("urlPattern");
}, [form, mainConfig, server]);
useEffect(() => {
if (shellTool !== ShellToolType.Custom || customShellTypes.length === 0) {
return;
}
const currentShellType = form.getValues("shellType");
if (currentShellType !== customShellTypes[0]) {
form.setValue("shellType", customShellTypes[0]);
}
}, [customShellTypes, form, shellTool]);
const handleShellToolChange = useCallback(
(value: string) => {
@@ -172,16 +197,15 @@ export default function MainConfigCard({
form.resetField("shellClassBase64");
};
if (shellToolMap) {
if (serverToolMap) {
let currentShellTypes = null;
if (value === ShellToolType.Custom) {
currentShellTypes = servers?.[form.getValues("server")] as string[];
currentShellTypes = customShellTypes;
} else {
currentShellTypes = shellToolMap[value];
currentShellTypes = serverToolMap[value];
}
setShellTypes(currentShellTypes);
// 直接设置 shellType 而不是依赖 useEffect
// Set shellType directly instead of relying on useEffect.
if (currentShellTypes && currentShellTypes.length > 0) {
form.setValue("shellType", currentShellTypes[0]);
}
@@ -207,18 +231,9 @@ export default function MainConfigCard({
}
form.setValue("shellTool", value);
},
[form, servers, shellToolMap],
[customShellTypes, form, serverToolMap],
);
const initializedRef = useRef(false);
if (!initializedRef.current && mainConfig) {
const initialServer = form.getValues("server");
if (initialServer && mainConfig[initialServer]) {
handleServerChange(initialServer);
initializedRef.current = true;
}
}
return (
<>
<Card>
@@ -249,10 +264,7 @@ export default function MainConfigCard({
{t("common:server")}
</FieldLabel>
<Select
onValueChange={(v) => {
field.onChange(v);
handleServerChange(v as string);
}}
onValueChange={field.onChange}
value={field.value}
>
<SelectTrigger id="server">
@@ -261,13 +273,14 @@ export default function MainConfigCard({
/>
</SelectTrigger>
<SelectContent>
{Object.keys(servers ?? {}).map(
(server: string) => (
<SelectItem key={server} value={server}>
{server}
</SelectItem>
),
)}
{serverOptions.map((serverOption) => (
<SelectItem
key={serverOption}
value={serverOption}
>
{serverOption}
</SelectItem>
))}
</SelectContent>
</Select>
<FieldDescription className="flex items-center">
@@ -312,10 +325,7 @@ export default function MainConfigCard({
<SelectContent>
{shellTools.map((tool) => (
<SelectItem key={tool} value={tool}>
<span className="flex items-center gap-2">
{shellToolIcons[tool]}
{tool}
</span>
{tool}
</SelectItem>
))}
</SelectContent>
@@ -484,6 +494,7 @@ export default function MainConfigCard({
/>
<NeoRegTabContent form={form} shellTypes={shellTypes} />
<CustomTabContent form={form} shellTypes={shellTypes} />
<ProxyTabContent form={form} shellTypes={shellTypes} />
</Tabs>
)}
</>
@@ -11,6 +11,7 @@ import {
type GodzillaShellToolConfig,
type MemShellResult,
type NeoreGeorgShellToolConfig,
type ProxyShellToolConfig,
ShellToolType,
type Suo5ShellToolConfig,
} from "@/types/memshell";
@@ -163,6 +164,13 @@ export function BasicInfo({
value={`${(generateResult?.shellToolConfig as Suo5ShellToolConfig).headerName}: ${(generateResult?.shellToolConfig as Suo5ShellToolConfig).headerValue}`}
/>
)}
{generateResult?.shellConfig.shellTool === ShellToolType.Proxy && (
<CopyableField
label={t("shellToolConfig.httpHeader")}
text={`${(generateResult?.shellToolConfig as ProxyShellToolConfig).headerName}: ${(generateResult?.shellToolConfig as ProxyShellToolConfig).headerValue}`}
value={`${(generateResult?.shellToolConfig as ProxyShellToolConfig).headerName}: ${(generateResult?.shellToolConfig as ProxyShellToolConfig).headerValue}`}
/>
)}
{generateResult?.shellConfig.shellTool === ShellToolType.AntSword && (
<>
<CopyableField
@@ -17,7 +17,7 @@ export function FeedbackAlert() {
const { t } = useTranslation("memshell");
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<AlertDialogTrigger>
<Button variant="outline" type="button">
<CircleHelpIcon /> {t("shellNotWork.title")}
</Button>
@@ -1,5 +1,5 @@
import { DownloadIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import CodeViewer from "@/components/code-viewer";
import { Button } from "@/components/ui/button";
@@ -25,30 +25,36 @@ export function MultiPackResult({
}>) {
const showCode = packMethod === "JSP";
const { t } = useTranslation();
const packMethods = Object.keys(allPackResults ?? {});
const [selectedMethod, setSelectedMethod] = useState(packMethods[0]);
const [packResult, setPackResult] = useState(
allPackResults?.[selectedMethod as keyof typeof allPackResults] ?? "",
const packResults = allPackResults as Record<string, string> | undefined;
const packMethods = useMemo(
() => Object.keys(packResults ?? {}),
[packResults],
);
useEffect(() => {
const newPackMethods = Object.keys(allPackResults ?? {});
if (!newPackMethods.includes(selectedMethod)) {
const newSelectedMethod = newPackMethods[0];
setSelectedMethod(newSelectedMethod);
setPackResult(
allPackResults?.[newSelectedMethod as keyof typeof allPackResults] ??
"",
);
} else {
setPackResult(
allPackResults?.[selectedMethod as keyof typeof allPackResults] ?? "",
);
}
}, [allPackResults, selectedMethod]);
const [selectedMethod, setSelectedMethod] = useState(
() => packMethods[0] ?? "",
);
const handleDownload = () => {
const packResult = useMemo(() => {
if (!selectedMethod) {
return "";
}
return packResults?.[selectedMethod] ?? "";
}, [packResults, selectedMethod]);
useEffect(() => {
if (packMethods.length === 0) {
if (selectedMethod !== "") {
setSelectedMethod("");
}
return;
}
if (!packMethods.includes(selectedMethod)) {
setSelectedMethod(packMethods[0]);
}
}, [packMethods, selectedMethod]);
const handleDownload = useCallback(() => {
const fileName =
shellClassName?.substring(shellClassName?.lastIndexOf(".") ?? 0) ?? "";
if (packMethod === "JSP") {
@@ -64,13 +70,17 @@ export function MultiPackResult({
});
return downloadContent(content, fileName, ".data");
} else if (packMethod === "Base64") {
const base64Content =
allPackResults?.[
Object.keys(allPackResults)[0] as keyof typeof allPackResults
] ?? "";
const base64Content = packResults?.[packMethods[0]] ?? "";
return downloadBytes(base64Content, shellClassName);
}
};
}, [
packMethod,
packMethods,
packResult,
packResults,
selectedMethod,
shellClassName,
]);
return (
<CodeViewer
@@ -80,9 +90,6 @@ export function MultiPackResult({
<Select
onValueChange={(value) => {
setSelectedMethod(value as string);
setPackResult(
allPackResults?.[value as keyof typeof allPackResults] ?? "",
);
}}
value={selectedMethod}
>
@@ -0,0 +1,69 @@
import { Controller, type UseFormReturn, useWatch } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { Card, CardContent } from "@/components/ui/card";
import { Field, FieldLabel } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { TabsContent } from "@/components/ui/tabs";
import type { MemShellFormSchema } from "@/types/schema";
import { OptionalClassFormField } from "./classname-field";
import { ShellTypeFormField } from "./shelltype-field";
export function ProxyTabContent({
form,
shellTypes,
}: Readonly<{
form: UseFormReturn<MemShellFormSchema>;
shellTypes: Array<string>;
}>) {
const shellType = useWatch({
name: "shellType",
control: form.control,
});
const { t } = useTranslation(["memshell", "common"]);
return (
<TabsContent value="Proxy">
<Card>
<CardContent className="space-y-2 mt-4">
<ShellTypeFormField form={form} shellTypes={shellTypes} />
<div
className="grid grid-cols-1 md:grid-cols-2 gap-2"
hidden={
shellType !== "BypassNginxWebSocket" &&
shellType !== "BypassNginxJakartaWebSocket"
}
>
<Controller
control={form.control}
name="headerName"
render={({ field }) => (
<Field className="gap-1">
<FieldLabel>{t("common:headerName")}</FieldLabel>
<Input
{...field}
placeholder={t("common:placeholders.input")}
/>
</Field>
)}
/>
<Controller
control={form.control}
name="headerValue"
render={({ field }) => (
<Field className="gap-1">
<FieldLabel>
{t("common:headerValue")} {t("common:optional")}
</FieldLabel>
<Input
{...field}
placeholder={t("common:placeholders.input")}
/>
</Field>
)}
/>
</div>
<OptionalClassFormField form={form} />
</CardContent>
</Card>
</TabsContent>
);
}