From c85f2829840c46f46173e6332d36a9ffbf3bd1d5 Mon Sep 17 00:00:00 2001 From: AAEE86 Date: Mon, 6 Jul 2026 12:30:33 +0800 Subject: [PATCH] feat: refine proxy group member controls Split advanced proxy group members into node and rule group sections, with separate clear actions for enabled nodes and enabled rule groups. Update proxy group summaries to show routing rules, nodes, and rule sets as separate counts. --- ...-group-advanced-panel-interactions.test.ts | 81 +++++- .../proxy-group-advanced-panel.test.ts | 11 +- .../sections/proxy-group-advanced-panel.tsx | 231 +++++++++++++----- .../sections/proxy-groups-categories.test.ts | 4 +- .../sections/proxy-groups-categories.tsx | 27 +- ...ups-custom-groups-panel-card-props.test.ts | 4 +- .../proxy-groups-custom-groups-panel.tsx | 10 +- .../sections/proxy-groups-module-card.test.ts | 11 +- .../sections/proxy-groups-module-card.tsx | 16 +- 9 files changed, 299 insertions(+), 96 deletions(-) diff --git a/packages/ui/src/product/converter/advanced-mode/sections/proxy-group-advanced-panel-interactions.test.ts b/packages/ui/src/product/converter/advanced-mode/sections/proxy-group-advanced-panel-interactions.test.ts index 5559699..0d6f41a 100644 --- a/packages/ui/src/product/converter/advanced-mode/sections/proxy-group-advanced-panel-interactions.test.ts +++ b/packages/ui/src/product/converter/advanced-mode/sections/proxy-group-advanced-panel-interactions.test.ts @@ -173,7 +173,7 @@ describe("ProxyGroupAdvancedPanel interactions", () => { expect(onChange).toHaveBeenCalledWith(expect.objectContaining({ memberOrder: expect.any(Array) })); expect(onChange).toHaveBeenCalledWith( expect.objectContaining({ - excludedMembers: expect.arrayContaining([expect.objectContaining({ kind: "direct" })]), + excludedMembers: expect.arrayContaining([expect.objectContaining({ name: "US Source" })]), }), ); expect(onChange).toHaveBeenCalledWith( @@ -181,10 +181,85 @@ describe("ProxyGroupAdvancedPanel interactions", () => { extraMembers: expect.arrayContaining([expect.objectContaining({ kind: "reject" })]), }), ); - expect(mocks.stateSetters[0]).toHaveBeenCalledWith("direct:DIRECT"); + expect(mocks.stateSetters[0]).toHaveBeenCalledWith("node:US Source"); expect(mocks.stateSetters[0]).toHaveBeenCalledWith(null); }); + it("clears enabled nodes from advanced member config", () => { + const onChange = vi.fn(); + const tree = ProxyGroupAdvancedPanel({ + target: { kind: "custom", id: "media", name: "Media" }, + advanced: { + extraMembers: [ + { kind: "node", name: "Japan Source" }, + { kind: "node", name: "Extra Node" }, + ], + excludedMembers: [{ kind: "reject" }], + memberOrder: [ + { kind: "direct" }, + { kind: "node", name: "Extra Node" }, + { kind: "node", name: "Japan Source" }, + ], + }, + onChange, + rulesCount: 0, + rulesContent: null, + }); + const clearButton = flattenElements(tree).find( + (element) => element.type === "button" && element.props.title === "清空节点", + ); + + expect(clearButton?.props.disabled).toBe(false); + clearButton?.props.onClick(); + + expect(onChange).toHaveBeenCalledWith({ + extraMembers: [{ kind: "node", name: "Extra Node" }], + excludedMembers: [ + { kind: "reject" }, + { kind: "node", name: "US Source" }, + { kind: "node", name: "Japan Source" }, + ], + memberOrder: [{ kind: "direct" }, { kind: "node", name: "Extra Node" }], + }); + }); + + it("clears enabled rule groups from advanced member config", () => { + const onChange = vi.fn(); + const tree = ProxyGroupAdvancedPanel({ + target: { kind: "custom", id: "media", name: "Media" }, + advanced: { + extraMembers: [ + { kind: "direct" }, + { kind: "node", name: "Extra Node" }, + ], + excludedMembers: [{ kind: "reject" }], + memberOrder: [ + { kind: "direct" }, + { kind: "node", name: "US Source" }, + { kind: "node", name: "Extra Node" }, + ], + }, + onChange, + rulesCount: 0, + rulesContent: null, + }); + const clearButton = flattenElements(tree).find( + (element) => element.type === "button" && element.props.title === "清空规则组", + ); + + expect(clearButton?.props.disabled).toBe(false); + clearButton?.props.onClick(); + + expect(onChange).toHaveBeenCalledWith({ + extraMembers: [{ kind: "node", name: "Extra Node" }], + excludedMembers: [{ kind: "reject" }, { kind: "direct" }], + memberOrder: [ + { kind: "node", name: "US Source" }, + { kind: "node", name: "Extra Node" }, + ], + }); + }); + it("ignores member drops without a real move target", () => { const onChange = vi.fn(); mocks.draggingKey = null; @@ -203,7 +278,7 @@ describe("ProxyGroupAdvancedPanel interactions", () => { vi.clearAllMocks(); mocks.stateSetters = []; - mocks.draggingKey = "direct:DIRECT"; + mocks.draggingKey = "node:US Source"; tree = ProxyGroupAdvancedPanel({ target: { kind: "custom", id: "media", name: "Media" }, advanced: {}, diff --git a/packages/ui/src/product/converter/advanced-mode/sections/proxy-group-advanced-panel.test.ts b/packages/ui/src/product/converter/advanced-mode/sections/proxy-group-advanced-panel.test.ts index 8c31f28..4983048 100644 --- a/packages/ui/src/product/converter/advanced-mode/sections/proxy-group-advanced-panel.test.ts +++ b/packages/ui/src/product/converter/advanced-mode/sections/proxy-group-advanced-panel.test.ts @@ -120,6 +120,8 @@ describe("ProxyGroupAdvancedPanel", () => { expect(html).not.toContain("剩余流量"); expect(html).toContain("US Source"); expect(html).toContain("DIRECT"); + expect(html).toContain("已启用规则组"); + expect(html).toContain("未启用规则组"); expect(html).toContain("rules-content"); expect(html).toContain("还没有分流规则"); expect(html).toContain("max-h-52 space-y-1.5 overflow-y-auto pr-1 custom-scrollbar"); @@ -159,7 +161,8 @@ describe("ProxyGroupAdvancedPanel", () => { ); expect(html).toContain("暂无可匹配的导入源"); - expect(html).toContain("暂无已启用的节点或代理组"); + expect(html).toContain("暂无已启用的节点"); + expect(html).toContain("暂无已启用的规则组"); expect(html).toContain("DIRECT"); expect(html).toContain("REJECT"); expect(html).toContain("existing-rules"); @@ -196,7 +199,7 @@ describe("ProxyGroupAdvancedPanel", () => { expect(html).toContain("#1 订阅链接"); expect(html).toContain("#2 YAML 配置"); expect(html).toContain("#3 节点链接"); - expect(html).toContain("暂无已启用的节点或代理组"); + expect(html).toContain("暂无已启用的节点"); }); it("previews enabled members for a disabled built-in proxy group", () => { @@ -220,7 +223,7 @@ describe("ProxyGroupAdvancedPanel", () => { expect(html).toContain("US Source"); expect(html).toContain("Japan Source"); expect(html).toContain("max-h-52 overflow-y-auto pr-1 custom-scrollbar flex flex-wrap gap-1.5"); - expect(html).not.toContain("暂无已启用的节点或代理组"); + expect(html).not.toContain("暂无已启用的节点"); }); it("previews enabled members for a disabled custom proxy group", () => { @@ -245,7 +248,7 @@ describe("ProxyGroupAdvancedPanel", () => { expect(html).toContain("US Source"); expect(html).toContain("Japan Source"); - expect(html).not.toContain("暂无已启用的节点或代理组"); + expect(html).not.toContain("暂无已启用的节点"); }); it("normalizes advanced member helpers without rendering the panel", () => { diff --git a/packages/ui/src/product/converter/advanced-mode/sections/proxy-group-advanced-panel.tsx b/packages/ui/src/product/converter/advanced-mode/sections/proxy-group-advanced-panel.tsx index a635214..603365a 100644 --- a/packages/ui/src/product/converter/advanced-mode/sections/proxy-group-advanced-panel.tsx +++ b/packages/ui/src/product/converter/advanced-mode/sections/proxy-group-advanced-panel.tsx @@ -106,6 +106,15 @@ export function withMember(list: readonly ProxyGroupMemberRef[] | undefined, mem return [...withoutMember(list, key), member]; } +function isNodeMember(member: ResolvedMember): boolean { + return member.kind === "node"; +} + +function isRuleGroupMember(member: ResolvedMember): boolean { + // 规则组区域承载所有非节点成员,包括内置/自定义组以及 DIRECT/REJECT。 + return member.kind !== "node"; +} + const PROTECTED_INSERT_KEYS = new Set([ "direct:DIRECT", "reject:REJECT", @@ -275,6 +284,17 @@ export function ProxyGroupAdvancedPanel({ return candidateMembers.filter((member) => !included.has(member.key)); }, [candidateMembers, includedMembers]); + const includedNodeMembers = React.useMemo(() => includedMembers.filter(isNodeMember), [includedMembers]); + const includedRuleGroupMembers = React.useMemo( + () => includedMembers.filter(isRuleGroupMember), + [includedMembers], + ); + const excludedNodeMembers = React.useMemo(() => excludedMembers.filter(isNodeMember), [excludedMembers]); + const excludedRuleGroupMembers = React.useMemo( + () => excludedMembers.filter(isRuleGroupMember), + [excludedMembers], + ); + const sourceOptions = React.useMemo(() => { const sourceIdsInNodes = new Set(); for (const node of activeNodes) { @@ -330,6 +350,110 @@ export function ProxyGroupAdvancedPanel({ [excludedRefs, extraRefs, includedMembers, onChange], ); + const clearMembers = React.useCallback((members: ResolvedMember[]) => { + if (members.length === 0) return; + + const includedKeys = new Set(members.map((member) => member.key)); + const nextExcludedMembers = [...excludedRefs]; + const excludedKeys = new Set(excludedRefs.map(getProxyGroupMemberKey)); + + for (const member of members) { + if (excludedKeys.has(member.key)) continue; + excludedKeys.add(member.key); + nextExcludedMembers.push(member.ref); + } + + onChange({ + // 清空当前列表时同步清掉手动加入和排序,避免旧配置把成员重新带回。 + extraMembers: extraRefs.filter( + (member) => !includedKeys.has(getProxyGroupMemberKey(member)), + ), + excludedMembers: nextExcludedMembers, + memberOrder: normalizeList(advanced.memberOrder).filter( + (member) => !includedKeys.has(getProxyGroupMemberKey(member)), + ), + }); + }, [advanced.memberOrder, excludedRefs, extraRefs, onChange]); + + const clearIncludedNodeMembers = React.useCallback(() => { + clearMembers(includedNodeMembers); + }, [clearMembers, includedNodeMembers]); + + const clearIncludedRuleGroupMembers = React.useCallback(() => { + clearMembers(includedRuleGroupMembers); + }, [clearMembers, includedRuleGroupMembers]); + + const renderIncludedMembers = (members: ResolvedMember[], emptyMessage: string) => ( + members.length === 0 ? ( +
+ {emptyMessage} +
+ ) : ( +
+ {members.map((member) => ( +
setDraggingKey(member.key)} + onDragOver={(event) => event.preventDefault()} + onDrop={() => { + if (draggingKey) moveMember(draggingKey, member.key); + setDraggingKey(null); + }} + onDragEnd={() => setDraggingKey(null)} + className={cn( + "grid grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-2 rounded border border-white/10 bg-white/[0.04] px-2 py-1.5 text-xs", + draggingKey === member.key && "opacity-50", + )} + > + + + +
+
+ {memberLabel(member)} +
+
{memberKindLabel(member)}
+
+ +
+ ))} +
+ ) + ); + + const renderExcludedMembers = (members: ResolvedMember[], emptyMessage: string) => ( + members.length === 0 ? ( +
{emptyMessage}
+ ) : ( +
+ {members.map((member) => { + return ( + + ); + })} +
+ ) + ); + return (
@@ -407,79 +531,54 @@ export function ProxyGroupAdvancedPanel({
已启用节点
- {includedMembers.length} 个 + + {includedNodeMembers.length} 个
- {includedMembers.length === 0 ? ( -
- 暂无已启用的节点或代理组 + {renderIncludedMembers(includedNodeMembers, "暂无已启用的节点")} + +
+
+
未启用节点
+ {excludedNodeMembers.length} 个
- ) : ( -
- {includedMembers.map((member) => ( -
setDraggingKey(member.key)} - onDragOver={(event) => event.preventDefault()} - onDrop={() => { - if (draggingKey) moveMember(draggingKey, member.key); - setDraggingKey(null); - }} - onDragEnd={() => setDraggingKey(null)} - className={cn( - "grid grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-2 rounded border border-white/10 bg-white/[0.04] px-2 py-1.5 text-xs", - draggingKey === member.key && "opacity-50", - )} - > - - - -
-
- {memberLabel(member)} -
-
{memberKindLabel(member)}
-
- -
- ))} + {renderExcludedMembers(excludedNodeMembers, "暂无未启用的节点")} +
+ +
+
+
已启用规则组
+ + {includedRuleGroupMembers.length} 个
- )} + {renderIncludedMembers(includedRuleGroupMembers, "暂无已启用的规则组")} +
-
未启用节点
- {excludedMembers.length} 个 +
未启用规则组
+ {excludedRuleGroupMembers.length} 个
- {excludedMembers.length === 0 ? ( -
暂无未启用的节点或代理组
- ) : ( -
- {excludedMembers.map((member) => { - return ( - - ); - })} -
- )} + {renderExcludedMembers(excludedRuleGroupMembers, "暂无未启用的规则组")}
diff --git a/packages/ui/src/product/converter/advanced-mode/sections/proxy-groups-categories.test.ts b/packages/ui/src/product/converter/advanced-mode/sections/proxy-groups-categories.test.ts index 6d23bb1..1fa767b 100644 --- a/packages/ui/src/product/converter/advanced-mode/sections/proxy-groups-categories.test.ts +++ b/packages/ui/src/product/converter/advanced-mode/sections/proxy-groups-categories.test.ts @@ -408,7 +408,7 @@ describe("ProxyGroupsCategories", () => { renderCategories({ 0: new Set(["core"]) }); const card = mocks.captures.moduleCards[0]; - expect(card.nodeCount).toBe(2); + expect(card.memberStats).toEqual({ nodeCount: 1, ruleSetCount: 1 }); expect(card.extraRules).toEqual([ { id: "set-1", name: "Set 1", behavior: "domain", path: "geosite/set-1.mrs", noResolve: true }, ]); @@ -470,7 +470,7 @@ describe("ProxyGroupsCategories", () => { renderCategories({ 0: new Set(["core"]) }); - expect(mocks.captures.moduleCards[0].nodeCount).toBe(0); + expect(mocks.captures.moduleCards[0].memberStats).toEqual({ nodeCount: 0, ruleSetCount: 0 }); expect(mocks.captures.moduleCards[0].extraRules).toEqual([]); }); diff --git a/packages/ui/src/product/converter/advanced-mode/sections/proxy-groups-categories.tsx b/packages/ui/src/product/converter/advanced-mode/sections/proxy-groups-categories.tsx index 96b8c29..d7655d4 100644 --- a/packages/ui/src/product/converter/advanced-mode/sections/proxy-groups-categories.tsx +++ b/packages/ui/src/product/converter/advanced-mode/sections/proxy-groups-categories.tsx @@ -33,12 +33,26 @@ import { import { ProxyGroupsCustomGroupsPanel } from "./proxy-groups-custom-groups-panel"; import { ProxyGroupsCustomRoutingRules } from "./proxy-groups-custom-routing-rules"; import { ProxyGroupAdvancedPanel } from "./proxy-group-advanced-panel"; -import { ProxyGroupsModuleCard } from "./proxy-groups-module-card"; +import { ProxyGroupsModuleCard, type ProxyGroupMemberStats } from "./proxy-groups-module-card"; const PROXY_GROUP_SECTION_LABEL_ROW_CLASS = "flex min-h-7 items-center gap-2"; const PROXY_GROUP_SECTION_LABEL_CLASS = "text-xs text-white/50"; const CUSTOM_CATEGORY_ID = "custom"; +function countProxyGroupMembers(proxies: unknown, nodeNames: Set): ProxyGroupMemberStats { + if (!Array.isArray(proxies)) return { nodeCount: 0, ruleSetCount: 0 }; + + let nodeCount = 0; + let ruleSetCount = 0; + for (const proxyName of proxies) { + if (typeof proxyName !== "string" || !proxyName.trim()) continue; + // 生成后的 proxies 中既可能是真实节点,也可能是内置/自定义规则组;卡片摘要需要拆开显示。 + if (nodeNames.has(proxyName)) nodeCount += 1; + else ruleSetCount += 1; + } + return { nodeCount, ruleSetCount }; +} + export function ProxyGroupsCategories() { const { ruleProviderBaseUrl, @@ -157,8 +171,9 @@ export function ProxyGroupsCategories() { } return grouped; }, [hiddenProxyGroups]); - const generatedProxyGroupNodeCounts = React.useMemo(() => { - if (nodes.length === 0) return new Map(); + const generatedProxyGroupMemberStats = React.useMemo(() => { + if (nodes.length === 0) return new Map(); + const nodeNames = new Set(nodes.map((node) => node.name)); const generated = generateProxyGroups({ nodes, enabledModules: enabledProxyGroups, @@ -174,7 +189,7 @@ export function ProxyGroupsCategories() { return new Map( generated.map((group) => [ group.name, - Array.isArray(group.proxies) ? group.proxies.length : 0, + countProxyGroupMembers(group.proxies, nodeNames), ]), ); }, [ @@ -603,7 +618,7 @@ export function ProxyGroupsCategories() { }) } advancedMode={proxyGroupAdvancedModeEnabled} - nodeCount={generatedProxyGroupNodeCounts.get(display.full) ?? 0} + memberStats={generatedProxyGroupMemberStats.get(display.full)} renderAdvancedContent={(rulesContent, rulesCount) => ( )}
diff --git a/packages/ui/src/product/converter/advanced-mode/sections/proxy-groups-custom-groups-panel-card-props.test.ts b/packages/ui/src/product/converter/advanced-mode/sections/proxy-groups-custom-groups-panel-card-props.test.ts index 62bae9f..c3566b3 100644 --- a/packages/ui/src/product/converter/advanced-mode/sections/proxy-groups-custom-groups-panel-card-props.test.ts +++ b/packages/ui/src/product/converter/advanced-mode/sections/proxy-groups-custom-groups-panel-card-props.test.ts @@ -146,7 +146,7 @@ describe("ProxyGroupsCustomGroupsPanel card props", () => { renderToStaticMarkup( React.createElement(ProxyGroupsCustomGroupsPanel, { advancedMode: true, - nodeCounts: new Map([["C Custom", 3]]), + memberStats: new Map([["C Custom", { nodeCount: 2, ruleSetCount: 1 }]]), }), ); @@ -154,7 +154,7 @@ describe("ProxyGroupsCustomGroupsPanel card props", () => { expect(card).toMatchObject({ advancedMode: true, isEnabled: false, - nodeCount: 3, + memberStats: { nodeCount: 2, ruleSetCount: 1 }, rulesCountOverride: 2, }); diff --git a/packages/ui/src/product/converter/advanced-mode/sections/proxy-groups-custom-groups-panel.tsx b/packages/ui/src/product/converter/advanced-mode/sections/proxy-groups-custom-groups-panel.tsx index 8c7b384..a3d8da3 100644 --- a/packages/ui/src/product/converter/advanced-mode/sections/proxy-groups-custom-groups-panel.tsx +++ b/packages/ui/src/product/converter/advanced-mode/sections/proxy-groups-custom-groups-panel.tsx @@ -36,14 +36,14 @@ import { toProxyGroupNameDraft, type ProxyGroupNameDraft, } from "./proxy-group-name-editor"; -import { ProxyGroupsModuleCard } from "./proxy-groups-module-card"; +import { ProxyGroupsModuleCard, type ProxyGroupMemberStats } from "./proxy-groups-module-card"; export function ProxyGroupsCustomGroupsPanel({ advancedMode = false, - nodeCounts, + memberStats, }: { advancedMode?: boolean; - nodeCounts?: Map; + memberStats?: Map; }) { const { enabledProxyGroups, @@ -253,7 +253,7 @@ export function ProxyGroupsCustomGroupsPanel({ ); const totalRules = groupRuleSets.length + manualRules.length; const description = group.description?.trim() || "自定义代理组"; - const nodeCount = nodeCounts?.get(group.name) ?? 0; + const groupMemberStats = memberStats?.get(group.name); const toggleExpand = () => { setExpandedCustomGroups((prev) => { @@ -424,7 +424,7 @@ export function ProxyGroupsCustomGroupsPanel({ } rulesCountOverride={totalRules} advancedMode={advancedMode} - nodeCount={nodeCount} + memberStats={groupMemberStats} renderAdvancedContent={(content, count) => ( { expect(html).toContain("Gemini Display"); expect(html).toContain("AI description"); - expect(html).toContain("3 规则"); + expect(html).toContain("3 分流规则"); expect(html).toContain("Gemini 分流说明"); expect(html).toContain("rules-panel"); expect(mocks.panels[0]).toEqual(expect.objectContaining({ module: baseModule, cnIpNoResolve: true })); @@ -271,15 +271,18 @@ describe("ProxyGroupsModuleCard", () => { props({ module: { ...baseModule, id: "manual", description: "手动选择代理节点" }, display: { full: "手动选择" }, + memberStats: { nodeCount: 1, ruleSetCount: 1 }, }) ) ); expect(html).toContain("手动选择代理节点"); expect(html).toContain('text-indigo-300">手动选择代理节点'); - expect(html).toContain('text-emerald-300">3 规则'); + expect(html).toContain('text-emerald-300">3 分流规则'); + expect(html).toContain("1 节点"); + expect(html).toContain("1 规则集"); expect(html).not.toContain('title="手动选择代理节点'); - expect(html).not.toContain('title="3 规则'); + expect(html).not.toContain('title="3 分流规则'); const overrideHtml = renderToStaticMarkup( React.createElement( @@ -305,7 +308,7 @@ describe("ProxyGroupsModuleCard", () => { }) ) ); - expect(emptyDescriptionHtml).toContain("0 规则"); + expect(emptyDescriptionHtml).toContain("0 分流规则"); expect(emptyDescriptionHtml).not.toContain("AI description"); }); }); diff --git a/packages/ui/src/product/converter/advanced-mode/sections/proxy-groups-module-card.tsx b/packages/ui/src/product/converter/advanced-mode/sections/proxy-groups-module-card.tsx index cc08d63..828bfdd 100644 --- a/packages/ui/src/product/converter/advanced-mode/sections/proxy-groups-module-card.tsx +++ b/packages/ui/src/product/converter/advanced-mode/sections/proxy-groups-module-card.tsx @@ -33,6 +33,11 @@ import { type ProxyGroupTypeMenuValue, } from "./proxy-group-type-menu"; +export type ProxyGroupMemberStats = { + nodeCount: number; + ruleSetCount: number; +}; + function ModuleHintPopover({ moduleId }: { moduleId: string }) { const isGemini = moduleId === "gemini"; const label = isGemini ? "Gemini 分流说明" : "谷歌学术分流说明"; @@ -141,7 +146,7 @@ export function ProxyGroupsModuleCard({ rulesContentOverride, rulesCountOverride, advancedMode = false, - nodeCount = 0, + memberStats = { nodeCount: 0, ruleSetCount: 0 }, renderAdvancedContent, }: { module: ProxyGroupModule; @@ -191,7 +196,7 @@ export function ProxyGroupsModuleCard({ rulesContentOverride?: React.ReactNode; rulesCountOverride?: number; advancedMode?: boolean; - nodeCount?: number; + memberStats?: ProxyGroupMemberStats; renderAdvancedContent?: (rulesContent: React.ReactNode, rulesCount: number) => React.ReactNode; }) { const effectiveRules = getEffectiveModuleRuleItems(module, ruleSetsByTarget, hiddenPresetRuleIds); @@ -213,8 +218,11 @@ export function ProxyGroupsModuleCard({ : getProxyGroupTypeLabel(effectiveGroupType); const summaryItems = [ { label: description ?? module.description ?? "", tone: "accent" as const }, - { label: `${totalRules} 规则`, tone: "success" as const }, - { label: `${nodeCount} 节点`, tone: "muted" as const }, + { label: `${totalRules} 分流规则`, tone: "success" as const }, + { label: `${memberStats.nodeCount} 节点`, tone: "muted" as const }, + ...(memberStats.ruleSetCount > 0 + ? [{ label: `${memberStats.ruleSetCount} 规则集`, tone: "muted" as const }] + : []), ]; const rulesContent = rulesContentOverride !== undefined ? rulesContentOverride : hasRuleManagement ? (