mirror of
https://github.com/ReaJason/MemShellParty.git
synced 2026-09-22 07:00:43 +08:00
feat: support select child packer
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
package com.reajason.javaweb.boot.controller;
|
package com.reajason.javaweb.boot.controller;
|
||||||
|
|
||||||
import com.reajason.javaweb.boot.vo.CommandConfigVO;
|
import com.reajason.javaweb.boot.vo.CommandConfigVO;
|
||||||
|
import com.reajason.javaweb.boot.vo.PackerVO;
|
||||||
import com.reajason.javaweb.memshell.ServerFactory;
|
import com.reajason.javaweb.memshell.ServerFactory;
|
||||||
import com.reajason.javaweb.memshell.config.CommandConfig;
|
import com.reajason.javaweb.memshell.config.CommandConfig;
|
||||||
import com.reajason.javaweb.memshell.server.AbstractServer;
|
import com.reajason.javaweb.memshell.server.AbstractServer;
|
||||||
@@ -40,6 +41,21 @@ public class ConfigController {
|
|||||||
.map(Packers::name).toList();
|
.map(Packers::name).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回父/子 packer 层级结构,供前端在「父模式 / 子模式」之间选择。
|
||||||
|
* 单独新增端点而非修改 {@link #getPackers()},以避免破坏旧版本前端对返回值的依赖。
|
||||||
|
*/
|
||||||
|
@RequestMapping("/packers/tree")
|
||||||
|
public List<PackerVO> getPackerTree() {
|
||||||
|
return Arrays.stream(Packers.values())
|
||||||
|
.filter(packers -> packers.getParentPacker() == null)
|
||||||
|
.map(packers -> new PackerVO(
|
||||||
|
packers.name(),
|
||||||
|
Packers.getPackersWithParent(packers.getInstance().getClass())
|
||||||
|
.stream().map(Packers::name).toList()))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
@RequestMapping
|
@RequestMapping
|
||||||
public Map<String, Map<?, ?>> config() {
|
public Map<String, Map<?, ?>> config() {
|
||||||
Map<String, Map<?, ?>> coreMap = new HashMap<>(16);
|
Map<String, Map<?, ?>> coreMap = new HashMap<>(16);
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.reajason.javaweb.boot.vo;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author ReaJason
|
||||||
|
* @since 2026/6/27
|
||||||
|
*/
|
||||||
|
public record PackerVO(String name, List<String> children) {
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { PackerConfig } from "@/types/memshell";
|
import type { PackerConfig, PackerOption } from "@/types/memshell";
|
||||||
import type { MemShellFormSchema } from "@/types/schema";
|
import type { MemShellFormSchema } from "@/types/schema";
|
||||||
|
|
||||||
import { PackageIcon } from "lucide-react";
|
import { PackageIcon } from "lucide-react";
|
||||||
@@ -6,9 +6,8 @@ import { useMemo } from "react";
|
|||||||
import { Controller, type UseFormReturn, useWatch } from "react-hook-form";
|
import { Controller, type UseFormReturn, useWatch } from "react-hook-form";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import PackerSelector from "@/components/packer-selector";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { FieldLabel, FieldSet } from "@/components/ui/field";
|
|
||||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
|
||||||
import { Spinner } from "@/components/ui/spinner";
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
|
|
||||||
export default function PackageConfigCard({
|
export default function PackageConfigCard({
|
||||||
@@ -30,8 +29,8 @@ export default function PackageConfigCard({
|
|||||||
name: "server",
|
name: "server",
|
||||||
});
|
});
|
||||||
|
|
||||||
const options = useMemo(() => {
|
const parents = useMemo<PackerOption[]>(() => {
|
||||||
const filteredOptions = (packerConfig ?? []).filter((name) => {
|
return (packerConfig ?? []).filter(({ name }) => {
|
||||||
if (!shellType || shellType === " ") {
|
if (!shellType || shellType === " ") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -43,12 +42,7 @@ export default function PackageConfigCard({
|
|||||||
}
|
}
|
||||||
return !name.startsWith("Agent") && !name.toLowerCase().startsWith("xxl");
|
return !name.startsWith("Agent") && !name.toLowerCase().startsWith("xxl");
|
||||||
});
|
});
|
||||||
form.setValue("packingMethod", filteredOptions[0]);
|
}, [packerConfig, shellType, server]);
|
||||||
return filteredOptions.map((name) => ({
|
|
||||||
name: t(name),
|
|
||||||
value: name,
|
|
||||||
}));
|
|
||||||
}, [packerConfig, shellType, server, t, form]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="w-full">
|
<Card className="w-full">
|
||||||
@@ -59,30 +53,12 @@ export default function PackageConfigCard({
|
|||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{options.length > 0 ? (
|
{parents.length > 0 ? (
|
||||||
<Controller
|
<Controller
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="packingMethod"
|
name="packingMethod"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FieldSet>
|
<PackerSelector parents={parents} value={field.value} onChange={field.onChange} />
|
||||||
<FieldLabel>{t("packerMethod")}</FieldLabel>
|
|
||||||
<RadioGroup
|
|
||||||
name={field.name}
|
|
||||||
value={field.value}
|
|
||||||
defaultValue={options[0].value}
|
|
||||||
onValueChange={field.onChange}
|
|
||||||
className="grid grid-cols-2 md:grid-cols-3"
|
|
||||||
>
|
|
||||||
{options.map(({ name, value }) => (
|
|
||||||
<div key={value} className="flex items-center space-x-3">
|
|
||||||
<RadioGroupItem value={value} id={value} />
|
|
||||||
<FieldLabel className="text-xs" htmlFor={value}>
|
|
||||||
{name}
|
|
||||||
</FieldLabel>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</RadioGroup>
|
|
||||||
</FieldSet>
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -27,6 +27,24 @@ export function ResultComponent({
|
|||||||
const isAgent = packMethod.startsWith("Agent");
|
const isAgent = packMethod.startsWith("Agent");
|
||||||
const isJar = packMethod.endsWith("Jar");
|
const isJar = packMethod.endsWith("Jar");
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const shellClassName = generateResult?.shellClassName;
|
||||||
|
|
||||||
|
const handleDownload = useCallback(() => {
|
||||||
|
const fileName = shellClassName?.substring(shellClassName?.lastIndexOf(".") ?? 0) ?? "";
|
||||||
|
if (packMethod.includes("JSP")) {
|
||||||
|
const fileExtension = packMethod.includes("JSPX") ? ".jspx" : ".jsp";
|
||||||
|
const content = new Blob([packResult as string], { type: "text/plain" });
|
||||||
|
return downloadContent(content, fileName, fileExtension);
|
||||||
|
} else if (packMethod.includes("JavaCommons") || packMethod.includes("Hessian")) {
|
||||||
|
const content = new Blob([base64ToBytes(packResult as string)], {
|
||||||
|
type: "application/octet-stream",
|
||||||
|
});
|
||||||
|
return downloadContent(content, fileName, ".data");
|
||||||
|
} else if (packMethod === "Base64") {
|
||||||
|
return downloadBytes(packResult as string, shellClassName);
|
||||||
|
}
|
||||||
|
}, [packMethod, packResult, shellClassName]);
|
||||||
|
|
||||||
if (allPackResults) {
|
if (allPackResults) {
|
||||||
return (
|
return (
|
||||||
<MultiPackResult
|
<MultiPackResult
|
||||||
@@ -55,24 +73,6 @@ export function ResultComponent({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const shellClassName = generateResult?.shellClassName;
|
|
||||||
|
|
||||||
const handleDownload = useCallback(() => {
|
|
||||||
const fileName = shellClassName?.substring(shellClassName?.lastIndexOf(".") ?? 0) ?? "";
|
|
||||||
if (packMethod.includes("JSP")) {
|
|
||||||
const fileExtension = packMethod.includes("JSPX") ? ".jspx" : ".jsp";
|
|
||||||
const content = new Blob([packResult as string], { type: "text/plain" });
|
|
||||||
return downloadContent(content, fileName, fileExtension);
|
|
||||||
} else if (packMethod.includes("JavaCommons") || packMethod.includes("Hessian")) {
|
|
||||||
const content = new Blob([base64ToBytes(packResult as string)], {
|
|
||||||
type: "application/octet-stream",
|
|
||||||
});
|
|
||||||
return downloadContent(content, fileName, ".data");
|
|
||||||
} else if (packMethod === "Base64") {
|
|
||||||
return downloadBytes(packResult as string, shellClassName);
|
|
||||||
}
|
|
||||||
}, [packMethod, packResult, shellClassName]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CodeViewer
|
<CodeViewer
|
||||||
code={packResult ?? ""}
|
code={packResult ?? ""}
|
||||||
|
|||||||
@@ -0,0 +1,194 @@
|
|||||||
|
import type { PackerOption } from "@/types/memshell";
|
||||||
|
|
||||||
|
import { Check, ChevronRight } from "lucide-react";
|
||||||
|
import { useEffect, useMemo } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
/** A leading radio-style indicator shared by every list item for a consistent look. */
|
||||||
|
function RadioDot({ active }: Readonly<{ active: boolean }>) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className={cn(
|
||||||
|
"flex size-3.5 shrink-0 items-center justify-center rounded-full border transition-colors",
|
||||||
|
active ? "border-sidebar-accent-foreground" : "border-muted-foreground/40",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"size-1.5 rounded-full bg-sidebar-accent-foreground transition-transform",
|
||||||
|
active ? "scale-100" : "scale-0",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ListItem({
|
||||||
|
label,
|
||||||
|
active,
|
||||||
|
onClick,
|
||||||
|
trailing,
|
||||||
|
}: Readonly<{
|
||||||
|
label: string;
|
||||||
|
active: boolean;
|
||||||
|
onClick: () => void;
|
||||||
|
trailing?: React.ReactNode;
|
||||||
|
}>) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="radio"
|
||||||
|
aria-checked={active}
|
||||||
|
onClick={onClick}
|
||||||
|
className={cn(
|
||||||
|
"flex min-h-8 items-center gap-2 rounded-md px-2.5 py-1.5 text-left text-xs leading-5 transition-colors",
|
||||||
|
active
|
||||||
|
? "bg-sidebar-accent text-sidebar-accent-foreground"
|
||||||
|
: "text-foreground hover:bg-sidebar-accent/70 hover:text-sidebar-accent-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<RadioDot active={active} />
|
||||||
|
<span className="flex-1 font-medium break-all">{label}</span>
|
||||||
|
{trailing}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* packer 选择器。
|
||||||
|
* - 左侧分类(父)+ 右侧变体(子)的主从布局,选中某个具体子变体。
|
||||||
|
*
|
||||||
|
* 通过受控的 `value` / `onChange` 与表单字段 `packingMethod` 双向同步,
|
||||||
|
* `value` 始终是单一来源;UI 状态(当前激活的父)由 `value` 推导,避免状态不一致。
|
||||||
|
*/
|
||||||
|
export default function PackerSelector({
|
||||||
|
parents,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: Readonly<{
|
||||||
|
parents: PackerOption[];
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
}>) {
|
||||||
|
const { t } = useTranslation("common");
|
||||||
|
|
||||||
|
const values = useMemo(() => {
|
||||||
|
return parents.flatMap((parent) =>
|
||||||
|
parent.children.length > 0 ? parent.children : [parent.name],
|
||||||
|
);
|
||||||
|
}, [parents]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (values.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedParent = parents.find((parent) => parent.name === value);
|
||||||
|
if (selectedParent?.children.length) {
|
||||||
|
onChange(selectedParent.children[0]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!value || !values.includes(value)) {
|
||||||
|
const firstParent = parents[0];
|
||||||
|
onChange(firstParent?.children[0] ?? firstParent?.name ?? values[0]);
|
||||||
|
}
|
||||||
|
}, [parents, values, value, onChange]);
|
||||||
|
|
||||||
|
const activeParent = useMemo(
|
||||||
|
() =>
|
||||||
|
parents.find((parent) => parent.name === value || parent.children.includes(value)) ??
|
||||||
|
parents[0],
|
||||||
|
[parents, value],
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectParent = (parent: PackerOption) => {
|
||||||
|
onChange(parent.children[0] ?? parent.name);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isParentActive = (parent: PackerOption) => activeParent?.name === parent.name;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<p className="text-xs text-muted-foreground">{t("packerMode.advancedDesc")}</p>
|
||||||
|
|
||||||
|
<div className="grid gap-3 md:grid-cols-2">
|
||||||
|
<section className="flex flex-col">
|
||||||
|
<div className="mb-2 text-xs font-medium tracking-wide text-muted-foreground uppercase">
|
||||||
|
{t("packerMode.category")}
|
||||||
|
</div>
|
||||||
|
<ScrollArea className="h-64 rounded-md border">
|
||||||
|
<div
|
||||||
|
role="radiogroup"
|
||||||
|
aria-label={t("packerMethod")}
|
||||||
|
className="flex flex-col gap-0.5 p-1.5"
|
||||||
|
>
|
||||||
|
{parents.map((parent) => {
|
||||||
|
const active = isParentActive(parent);
|
||||||
|
const hasChildren = parent.children.length > 0;
|
||||||
|
return (
|
||||||
|
<ListItem
|
||||||
|
key={parent.name}
|
||||||
|
label={t(parent.name)}
|
||||||
|
active={active}
|
||||||
|
onClick={() => selectParent(parent)}
|
||||||
|
trailing={
|
||||||
|
hasChildren ? (
|
||||||
|
<span className="flex items-center gap-1.5">
|
||||||
|
<Badge
|
||||||
|
variant={active ? "secondary" : "outline"}
|
||||||
|
className="h-5 px-1.5 text-[11px] tabular-nums"
|
||||||
|
>
|
||||||
|
{parent.children.length}
|
||||||
|
</Badge>
|
||||||
|
<ChevronRight className="size-4 shrink-0 opacity-70" aria-hidden="true" />
|
||||||
|
</span>
|
||||||
|
) : undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="flex flex-col">
|
||||||
|
<div className="mb-2 text-xs font-medium tracking-wide text-muted-foreground uppercase">
|
||||||
|
{t("packerMode.variant")}
|
||||||
|
</div>
|
||||||
|
<ScrollArea className="h-64 rounded-md border">
|
||||||
|
{activeParent && activeParent.children.length > 0 ? (
|
||||||
|
<div
|
||||||
|
role="radiogroup"
|
||||||
|
aria-label={t("packerMode.variant")}
|
||||||
|
className="flex flex-col gap-0.5 p-1.5"
|
||||||
|
>
|
||||||
|
{activeParent.children.map((child) => (
|
||||||
|
<ListItem
|
||||||
|
key={child}
|
||||||
|
label={t(child)}
|
||||||
|
active={value === child}
|
||||||
|
onClick={() => onChange(child)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-64 flex-col items-center justify-center gap-2 px-6 text-center text-sm text-muted-foreground">
|
||||||
|
<Check className="size-6 text-primary" aria-hidden="true" />
|
||||||
|
<span>
|
||||||
|
<span className="font-medium text-foreground">{t(activeParent?.name ?? "")}</span>{" "}
|
||||||
|
{t("packerMode.noVariants")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ScrollArea>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,19 +1,13 @@
|
|||||||
import type { PackerConfig } from "@/types/memshell";
|
import type { PackerConfig, PackerOption } from "@/types/memshell";
|
||||||
import type { ProbeShellFormSchema } from "@/types/schema";
|
import type { ProbeShellFormSchema } from "@/types/schema";
|
||||||
|
|
||||||
import { PackageIcon } from "lucide-react";
|
import { PackageIcon } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useMemo } from "react";
|
||||||
import { Controller, type UseFormReturn } from "react-hook-form";
|
import { Controller, type UseFormReturn } from "react-hook-form";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import PackerSelector from "@/components/packer-selector";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { FieldLabel } from "@/components/ui/field";
|
|
||||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
|
||||||
|
|
||||||
type Option = {
|
|
||||||
name: string;
|
|
||||||
value: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function PackageConfigCard({
|
export default function PackageConfigCard({
|
||||||
packerConfig,
|
packerConfig,
|
||||||
@@ -22,31 +16,17 @@ export default function PackageConfigCard({
|
|||||||
packerConfig: PackerConfig | undefined;
|
packerConfig: PackerConfig | undefined;
|
||||||
form: UseFormReturn<ProbeShellFormSchema>;
|
form: UseFormReturn<ProbeShellFormSchema>;
|
||||||
}>) {
|
}>) {
|
||||||
const [options, setOptions] = useState<Array<Option>>([]);
|
|
||||||
const { t } = useTranslation("common");
|
const { t } = useTranslation("common");
|
||||||
|
|
||||||
useEffect(() => {
|
const parents = useMemo<PackerOption[]>(() => {
|
||||||
const filteredOptions = (packerConfig ?? []).filter((name) => {
|
return (packerConfig ?? []).filter(({ name }) => {
|
||||||
return (
|
return (
|
||||||
!name.startsWith("Agent") &&
|
!name.startsWith("Agent") &&
|
||||||
!name.toLowerCase().startsWith("xxl") &&
|
!name.toLowerCase().startsWith("xxl") &&
|
||||||
!name.toLowerCase().endsWith("jar")
|
!name.toLowerCase().endsWith("jar")
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
}, [packerConfig]);
|
||||||
const mappedOptions = filteredOptions.map((name) => {
|
|
||||||
return {
|
|
||||||
name: name,
|
|
||||||
value: name,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
setOptions(mappedOptions);
|
|
||||||
const currentValue = form.getValues("packingMethod");
|
|
||||||
if (filteredOptions.length > 0 && (!currentValue || !filteredOptions.includes(currentValue))) {
|
|
||||||
form.setValue("packingMethod", filteredOptions[0]);
|
|
||||||
}
|
|
||||||
}, [form, packerConfig]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="w-full">
|
<Card className="w-full">
|
||||||
@@ -57,32 +37,12 @@ export default function PackageConfigCard({
|
|||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{options.length > 0 ? (
|
{parents.length > 0 ? (
|
||||||
<Controller
|
<Controller
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="packingMethod"
|
name="packingMethod"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<div className="space-y-3">
|
<PackerSelector parents={parents} value={field.value} onChange={field.onChange} />
|
||||||
<FieldLabel>{t("packerMethod")}</FieldLabel>
|
|
||||||
<div>
|
|
||||||
<RadioGroup
|
|
||||||
onValueChange={field.onChange}
|
|
||||||
value={field.value}
|
|
||||||
className="grid grid-cols-2 md:grid-cols-3"
|
|
||||||
>
|
|
||||||
{options.map(({ name, value }) => (
|
|
||||||
<div key={value} className="flex items-center space-x-3">
|
|
||||||
<div>
|
|
||||||
<RadioGroupItem value={value} id={value} />
|
|
||||||
</div>
|
|
||||||
<FieldLabel className="text-xs" htmlFor={value}>
|
|
||||||
{name}
|
|
||||||
</FieldLabel>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</RadioGroup>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { ScrollArea as ScrollAreaPrimitive } from "@base-ui/react/scroll-area"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function ScrollArea({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: ScrollAreaPrimitive.Root.Props) {
|
||||||
|
return (
|
||||||
|
<ScrollAreaPrimitive.Root
|
||||||
|
data-slot="scroll-area"
|
||||||
|
className={cn("relative", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ScrollAreaPrimitive.Viewport
|
||||||
|
data-slot="scroll-area-viewport"
|
||||||
|
className="size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ScrollAreaPrimitive.Viewport>
|
||||||
|
<ScrollBar />
|
||||||
|
<ScrollAreaPrimitive.Corner />
|
||||||
|
</ScrollAreaPrimitive.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ScrollBar({
|
||||||
|
className,
|
||||||
|
orientation = "vertical",
|
||||||
|
...props
|
||||||
|
}: ScrollAreaPrimitive.Scrollbar.Props) {
|
||||||
|
return (
|
||||||
|
<ScrollAreaPrimitive.Scrollbar
|
||||||
|
data-slot="scroll-area-scrollbar"
|
||||||
|
data-orientation={orientation}
|
||||||
|
orientation={orientation}
|
||||||
|
className={cn(
|
||||||
|
"flex touch-none p-px transition-colors select-none data-horizontal:h-2.5 data-horizontal:flex-col data-horizontal:border-t data-horizontal:border-t-transparent data-vertical:h-full data-vertical:w-2.5 data-vertical:border-l data-vertical:border-l-transparent",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ScrollAreaPrimitive.Thumb
|
||||||
|
data-slot="scroll-area-thumb"
|
||||||
|
className="relative flex-1 rounded-full bg-border"
|
||||||
|
/>
|
||||||
|
</ScrollAreaPrimitive.Scrollbar>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ScrollArea, ScrollBar }
|
||||||
@@ -22,6 +22,13 @@
|
|||||||
"optional": "(Optional)",
|
"optional": "(Optional)",
|
||||||
"packerConfig.title": "Package Config",
|
"packerConfig.title": "Package Config",
|
||||||
"packerMethod": "Package Method",
|
"packerMethod": "Package Method",
|
||||||
|
"packerMode.default": "Default",
|
||||||
|
"packerMode.advanced": "Advanced",
|
||||||
|
"packerMode.defaultDesc": "Pick a packing method; it packs all of its variants at once.",
|
||||||
|
"packerMode.advancedDesc": "Pick a category, then choose a specific variant.",
|
||||||
|
"packerMode.category": "Category",
|
||||||
|
"packerMode.variant": "Variant",
|
||||||
|
"packerMode.noVariants": "has no variants — selection complete.",
|
||||||
"paramName": "ParamName",
|
"paramName": "ParamName",
|
||||||
"paramName.description": "Supports passing values via request parameter (param) or request header (header)",
|
"paramName.description": "Supports passing values via request parameter (param) or request header (header)",
|
||||||
"placeholders.input": "Please input",
|
"placeholders.input": "Please input",
|
||||||
|
|||||||
@@ -22,6 +22,13 @@
|
|||||||
"optional": "(可选)",
|
"optional": "(可选)",
|
||||||
"packerConfig.title": "打包配置",
|
"packerConfig.title": "打包配置",
|
||||||
"packerMethod": "打包方式",
|
"packerMethod": "打包方式",
|
||||||
|
"packerMode.default": "默认模式",
|
||||||
|
"packerMode.advanced": "进阶模式",
|
||||||
|
"packerMode.defaultDesc": "选择打包方式,将一次性生成其所有子变体。",
|
||||||
|
"packerMode.advancedDesc": "先选择左侧分类,再挑选具体的子变体。",
|
||||||
|
"packerMode.category": "分类",
|
||||||
|
"packerMode.variant": "变体",
|
||||||
|
"packerMode.noVariants": "无子变体,已完成选择。",
|
||||||
"paramName": "参数名称",
|
"paramName": "参数名称",
|
||||||
"paramName.description": "支持请求参数 param 或请求头 header 传值",
|
"paramName.description": "支持请求参数 param 或请求头 header 传值",
|
||||||
"placeholders.input": "请输入",
|
"placeholders.input": "请输入",
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ const fetchServerConfig = () => fetchJson<ServerConfig>(`${env.API_URL}/api/conf
|
|||||||
|
|
||||||
const fetchMainConfig = () => fetchJson<MainConfig>(`${env.API_URL}/api/config`);
|
const fetchMainConfig = () => fetchJson<MainConfig>(`${env.API_URL}/api/config`);
|
||||||
|
|
||||||
const fetchPackerConfig = () => fetchJson<PackerConfig>(`${env.API_URL}/api/config/packers`);
|
const fetchPackerConfig = () => fetchJson<PackerConfig>(`${env.API_URL}/api/config/packers/tree`);
|
||||||
|
|
||||||
export default function MemShellPage() {
|
export default function MemShellPage() {
|
||||||
const { data: serverConfig } = useQuery<ServerConfig>({
|
const { data: serverConfig } = useQuery<ServerConfig>({
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export default function ProbeShellGenerator() {
|
|||||||
const { data: packerConfig } = useQuery<PackerConfig>({
|
const { data: packerConfig } = useQuery<PackerConfig>({
|
||||||
queryKey: ["packerConfig"],
|
queryKey: ["packerConfig"],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const response = await fetch(`${env.API_URL}/api/config/packers`);
|
const response = await fetch(`${env.API_URL}/api/config/packers/tree`);
|
||||||
return await response.json();
|
return await response.json();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -98,7 +98,12 @@ export interface MainConfig {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PackerConfig = Array<string>;
|
export interface PackerOption {
|
||||||
|
name: string;
|
||||||
|
children: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PackerConfig = Array<PackerOption>;
|
||||||
|
|
||||||
export interface MemShellGenerateResponse {
|
export interface MemShellGenerateResponse {
|
||||||
memShellResult: MemShellResult;
|
memShellResult: MemShellResult;
|
||||||
|
|||||||
@@ -2,6 +2,38 @@ import type { InjectorConfig, ShellConfig, ShellToolConfig } from "@/types/memsh
|
|||||||
import type { ProbeConfig, ProbeContentConfig } from "@/types/probeshell";
|
import type { ProbeConfig, ProbeContentConfig } from "@/types/probeshell";
|
||||||
import type { MemShellFormSchema, ProbeShellFormSchema } from "@/types/schema";
|
import type { MemShellFormSchema, ProbeShellFormSchema } from "@/types/schema";
|
||||||
|
|
||||||
|
const SPRING_GZIP_JDK17_RELATED_PACKERS = new Set([
|
||||||
|
"SpEL",
|
||||||
|
"SpELSpringGzipJDK17",
|
||||||
|
"OGNL",
|
||||||
|
"OGNLSpringGzipJDK17",
|
||||||
|
"JXPath",
|
||||||
|
"JXPathSpringGzipJDK17",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const UPPERCASE_LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||||
|
const CLASS_NAME_LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
||||||
|
|
||||||
|
function getRandomIndex(max: number) {
|
||||||
|
if (globalThis.crypto?.getRandomValues) {
|
||||||
|
return globalThis.crypto.getRandomValues(new Uint32Array(1))[0] % max;
|
||||||
|
}
|
||||||
|
return Math.floor(Math.random() * max);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRandomChar(chars: string) {
|
||||||
|
return chars[getRandomIndex(chars.length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateSpringExpressionInjectorClassName() {
|
||||||
|
const randomName = Array.from({ length: 5 }, () => getRandomChar(CLASS_NAME_LETTERS)).join("");
|
||||||
|
return `org.springframework.expression.${getRandomChar(UPPERCASE_LETTERS)}${randomName}Util`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSpringGzipJdk17RelatedPacker(packer: string) {
|
||||||
|
return SPRING_GZIP_JDK17_RELATED_PACKERS.has(packer);
|
||||||
|
}
|
||||||
|
|
||||||
export function transformToPostData(formValue: MemShellFormSchema) {
|
export function transformToPostData(formValue: MemShellFormSchema) {
|
||||||
const shellConfig: ShellConfig = {
|
const shellConfig: ShellConfig = {
|
||||||
server: formValue.server,
|
server: formValue.server,
|
||||||
@@ -32,7 +64,9 @@ export function transformToPostData(formValue: MemShellFormSchema) {
|
|||||||
|
|
||||||
const injectorConfig: InjectorConfig = {
|
const injectorConfig: InjectorConfig = {
|
||||||
urlPattern: formValue.urlPattern,
|
urlPattern: formValue.urlPattern,
|
||||||
injectorClassName: formValue.injectorClassName,
|
injectorClassName: isSpringGzipJdk17RelatedPacker(formValue.packingMethod)
|
||||||
|
? generateSpringExpressionInjectorClassName()
|
||||||
|
: formValue.injectorClassName,
|
||||||
staticInitialize: formValue.staticInitialize,
|
staticInitialize: formValue.staticInitialize,
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
|
|||||||
+453
-379
File diff suppressed because it is too large
Load Diff
+26
-26
@@ -14,29 +14,29 @@
|
|||||||
"fmt:check": "oxfmt --check"
|
"fmt:check": "oxfmt --check"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@base-ui/react": "^1.5.0",
|
"@base-ui/react": "^1.6.0",
|
||||||
"@hookform/resolvers": "^5.4.0",
|
"@hookform/resolvers": "^5.4.0",
|
||||||
"@orama/orama": "^3.1.18",
|
"@orama/orama": "^3.1.18",
|
||||||
"@orama/stopwords": "^3.1.18",
|
"@orama/stopwords": "^3.1.18",
|
||||||
"@orama/tokenizers": "^3.1.18",
|
"@orama/tokenizers": "^3.1.18",
|
||||||
"@react-router/node": "^7.15.1",
|
"@react-router/node": "^8.0.1",
|
||||||
"@tanstack/react-query": "^5.100.14",
|
"@tanstack/react-query": "^5.101.1",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"framer-motion": "^12.40.0",
|
"framer-motion": "^12.42.0",
|
||||||
"fumadocs-core": "^16.9.1",
|
"fumadocs-core": "^16.10.5",
|
||||||
"fumadocs-mdx": "15.0.8",
|
"fumadocs-mdx": "15.0.12",
|
||||||
"fumadocs-ui": "16.9.1",
|
"fumadocs-ui": "16.10.5",
|
||||||
"i18next": "^26.2.0",
|
"i18next": "^26.3.3",
|
||||||
"isbot": "^5.1.40",
|
"isbot": "^5.1.44",
|
||||||
"lucide-react": "^1.16.0",
|
"lucide-react": "^1.21.0",
|
||||||
"motion": "^12.40.0",
|
"motion": "^12.42.0",
|
||||||
"react": "^19.2.6",
|
"react": "^19.2.7",
|
||||||
"react-copy-to-clipboard": "^5.1.1",
|
"react-copy-to-clipboard": "^5.1.1",
|
||||||
"react-dom": "^19.2.6",
|
"react-dom": "^19.2.7",
|
||||||
"react-hook-form": "^7.76.1",
|
"react-hook-form": "^7.80.0",
|
||||||
"react-i18next": "^17.0.8",
|
"react-i18next": "^17.0.8",
|
||||||
"react-medium-image-zoom": "^5.4.5",
|
"react-medium-image-zoom": "^5.4.8",
|
||||||
"react-syntax-highlighter": "^16.1.1",
|
"react-syntax-highlighter": "^16.1.1",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.6.0",
|
"tailwind-merge": "^3.6.0",
|
||||||
@@ -44,23 +44,23 @@
|
|||||||
"yup": "^1.7.1"
|
"yup": "^1.7.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@react-router/dev": "^7.15.1",
|
"@react-router/dev": "^8.0.1",
|
||||||
"@tailwindcss/vite": "^4.3.0",
|
"@tailwindcss/vite": "^4.3.1",
|
||||||
"@types/mdx": "^2.0.13",
|
"@types/mdx": "^2.0.14",
|
||||||
"@types/node": "^25.9.1",
|
"@types/node": "^26.0.1",
|
||||||
"@types/react": "^19.2.15",
|
"@types/react": "^19.2.17",
|
||||||
"@types/react-copy-to-clipboard": "^5.0.7",
|
"@types/react-copy-to-clipboard": "^5.0.7",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@types/react-syntax-highlighter": "^15.5.13",
|
"@types/react-syntax-highlighter": "^15.5.13",
|
||||||
"baseline-browser-mapping": "^2.10.32",
|
"baseline-browser-mapping": "^2.10.40",
|
||||||
"oxfmt": "^0.51.0",
|
"oxfmt": "^0.56.0",
|
||||||
"oxlint": "^1.66.0",
|
"oxlint": "^1.71.0",
|
||||||
"react-router-devtools": "^6.2.0",
|
"react-router-devtools": "^6.2.1",
|
||||||
"rimraf": "^6.1.3",
|
"rimraf": "^6.1.3",
|
||||||
"serve": "^14.2.6",
|
"serve": "^14.2.6",
|
||||||
"tailwindcss": "^4.3.0",
|
"tailwindcss": "^4.3.1",
|
||||||
"typescript": "^6.0.3",
|
"typescript": "^6.0.3",
|
||||||
"vite": "^8.0.14",
|
"vite": "^8.1.0",
|
||||||
"vite-plugin-devtools-json": "^1.0.0",
|
"vite-plugin-devtools-json": "^1.0.0",
|
||||||
"vite-tsconfig-paths": "^6.1.1"
|
"vite-tsconfig-paths": "^6.1.1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,9 +9,6 @@ const getUrl = createGetUrl("/docs");
|
|||||||
export default {
|
export default {
|
||||||
basename: env.VITE_APP_BASE_PATH,
|
basename: env.VITE_APP_BASE_PATH,
|
||||||
ssr: false,
|
ssr: false,
|
||||||
future: {
|
|
||||||
v8_middleware: true,
|
|
||||||
},
|
|
||||||
async prerender({ getStaticPaths }) {
|
async prerender({ getStaticPaths }) {
|
||||||
const paths: string[] = [];
|
const paths: string[] = [];
|
||||||
const excluded: string[] = [];
|
const excluded: string[] = [];
|
||||||
|
|||||||
Reference in New Issue
Block a user