Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions packages/core/src/filtered-proxy-groups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,6 @@ function getNodeSourceIds(node: ParsedNode): string[] {
.map((s) => s.trim());
}

function escapeRegExp(input: string): string {
return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}

function compileRegex(pattern?: string): RegExp | null {
const raw = typeof pattern === "string" ? pattern.trim() : "";
if (!raw) return null;
Expand Down
4 changes: 0 additions & 4 deletions packages/core/src/generator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import type { ClashConfig, UserConfig, TemplateType, ProxyGroup } from "@subboos
import type { CustomProxyGroup } from "@subboost/core/types/config";
import type { DialerProxyGroup, ModuleRuleOverride } from "@subboost/core/types/template-config";
import type { FilteredProxyGroup } from "@subboost/core/types/filtered-proxy-group";
import { TEMPLATES } from "../templates";
import { collectDnsPolicyEntries, configToYaml } from "./yaml";
import type { ModuleRuleExclusions } from "./module-rules";
import { isMihomoSupportedProxyNode, normalizeMihomoVlessForGeneration } from "@subboost/core/mihomo/proxy-sanitizer";
Expand Down Expand Up @@ -182,9 +181,6 @@ export function generateClashConfig(options: GenerateOptions): ClashConfig {
proxyGroupNameOverrides,
} = options;

// 获取模板配置
const templateConfig = TEMPLATES[template];

// 合并用户配置
const config: UserConfig = {
...buildDefaultUserConfig(template),
Expand Down
1 change: 0 additions & 1 deletion packages/ui/src/product/converter/advanced-mode/root.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"use client";

import * as React from "react";
import { useConfigStore } from "@subboost/ui/store/config-store";
import { DialerProxyGroupsSection } from "./sections/dialer-proxy-groups-section";
import { DnsSection } from "./sections/dns-section";
import { FilteredProxyGroupsSection } from "./sections/filtered-proxy-groups-section";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,19 +197,19 @@ describe("NodeManagementBulkEditDialog", () => {
});

it("shows regex errors, skipped nodes, empty names, and no-match previews", () => {
let result = renderDialog({ 0: "[", 2: "(" });
expect(result.html).toContain("无效正则");
const invalidRegexResult = renderDialog({ 0: "[", 2: "(" });
expect(invalidRegexResult.html).toContain("无效正则");
buttonByText("完成").onClick();
expect(result.props.bulkRenameNodes).not.toHaveBeenCalled();
expect(invalidRegexResult.props.bulkRenameNodes).not.toHaveBeenCalled();

result = renderDialog({ 0: "Locked", 2: "Locked", 3: "Open" });
expect(result.html).toContain("跳过:当前节点命名模板无法解析");
const skippedNodesResult = renderDialog({ 0: "Locked", 2: "Locked", 3: "Open" });
expect(skippedNodesResult.html).toContain("跳过:当前节点命名模板无法解析");

result = renderDialog({ 0: "Beta", 2: ".*", 3: "" });
expect(result.html).toContain("跳过:新名称为空");
const emptyNameResult = renderDialog({ 0: "Beta", 2: ".*", 3: "" });
expect(emptyNameResult.html).toContain("跳过:新名称为空");

result = renderDialog({ 0: "Missing" });
expect(result.html).toContain("暂无匹配节点");
const noMatchResult = renderDialog({ 0: "Missing" });
expect(noMatchResult.html).toContain("暂无匹配节点");
});

it("auto-fills listener ports and clears listener-port UI state", () => {
Expand All @@ -227,70 +227,70 @@ describe("NodeManagementBulkEditDialog", () => {
});

it("validates listener-port input and conflicts", () => {
let result = renderDialog({ 0: "Alpha", 6: "70000" });
renderDialog({ 0: "Alpha", 6: "70000" });
buttonByText("自动填充监听端口").onClick();
expect(mocks.toast).toHaveBeenCalledWith(expect.objectContaining({ title: "起始监听端口需为 1-65535 的整数", variant: "warning" }));

result = renderDialog({ 0: "Alpha", 6: "" });
renderDialog({ 0: "Alpha", 6: "" });
buttonByText("自动填充监听端口").onClick();
expect(mocks.toast).toHaveBeenCalledWith(expect.objectContaining({ title: "起始监听端口需为 1-65535 的整数", variant: "warning" }));

result = renderDialog({ 0: "Alpha", 6: "abc" });
renderDialog({ 0: "Alpha", 6: "abc" });
buttonByText("自动填充监听端口").onClick();
expect(mocks.toast).toHaveBeenCalledWith(expect.objectContaining({ title: "起始监听端口需为 1-65535 的整数", variant: "warning" }));

result = renderDialog({ 0: "Alpha|Beta", 6: "65535" });
renderDialog({ 0: "Alpha|Beta", 6: "65535" });
buttonByText("自动填充监听端口").onClick();
expect(mocks.toast).toHaveBeenCalledWith(expect.objectContaining({ title: "监听端口超出范围", variant: "warning" }));

result = renderDialog(
const conflictResult = renderDialog(
{ 0: "Alpha|Beta", 6: "42000" },
{ listenerPorts: { Other: 42001 } },
);
buttonByText("自动填充监听端口").onClick();
expect(mocks.toast).toHaveBeenCalledWith(expect.objectContaining({ title: "监听端口冲突:42001", variant: "destructive" }));
expect(result.props.bulkSetListenerPorts).not.toHaveBeenCalled();
expect(conflictResult.props.bulkSetListenerPorts).not.toHaveBeenCalled();

result = renderDialog({ 0: "Missing", 6: "42000" });
renderDialog({ 0: "Missing", 6: "42000" });
buttonByText("自动填充监听端口").onClick();
expect(mocks.toast).toHaveBeenCalledWith(expect.objectContaining({ title: "暂无匹配节点", variant: "warning" }));
});

it("bulk-removes listener ports and hides listener controls when disabled", () => {
let result = renderDialog({ 0: "Alpha|Beta" });
const enabledResult = renderDialog({ 0: "Alpha|Beta" });
buttonByText("批量删除监听端口").onClick();
expect(result.props.bulkSetListenerPorts).toHaveBeenCalledWith({
expect(enabledResult.props.bulkSetListenerPorts).toHaveBeenCalledWith({
"[HK] Alpha One": null,
"Beta Two": null,
});
expect(result.props.onClearListenerPortUiState).toHaveBeenCalledWith(["[HK] Alpha One", "Beta Two"]);
expect(enabledResult.props.onClearListenerPortUiState).toHaveBeenCalledWith(["[HK] Alpha One", "Beta Two"]);
expect(mocks.toast).toHaveBeenCalledWith(expect.objectContaining({ title: "已批量删除监听端口 2 个节点", variant: "success" }));

result = renderDialog({}, { listenerPortEnabled: false });
expect(result.html).not.toContain("监听端口批量操作");
const disabledResult = renderDialog({}, { listenerPortEnabled: false });
expect(disabledResult.html).not.toContain("监听端口批量操作");
});

it("handles no-op previews, empty node names, disabled actions, and listener-port reservations", () => {
let result = renderDialog(
const noOpResult = renderDialog(
{ 0: "Alpha", 4: false, 5: false, 6: "43000" },
{ nodes: [{ name: "" } as any, nodes[0], nodes[1]], listenerPorts: { "[HK] Alpha One": 42000, Other: "bad" as any } },
);
expect(result.html).toContain("无变更");
expect(noOpResult.html).toContain("无变更");
buttonByText("完成").onClick();
expect(result.props.bulkRenameNodes).not.toHaveBeenCalled();
expect(result.props.onOpenChange).toHaveBeenCalledWith(false);
expect(noOpResult.props.bulkRenameNodes).not.toHaveBeenCalled();
expect(noOpResult.props.onOpenChange).toHaveBeenCalledWith(false);

buttonByText("自动填充监听端口").onClick();
expect(result.props.bulkSetListenerPorts).toHaveBeenCalledWith({ "[HK] Alpha One": 43000 });
expect(noOpResult.props.bulkSetListenerPorts).toHaveBeenCalledWith({ "[HK] Alpha One": 43000 });

result = renderDialog({ 0: "[", 6: "42000" });
const invalidRegexResult = renderDialog({ 0: "[", 6: "42000" });
buttonByText("自动填充监听端口").onClick();
buttonByText("批量删除监听端口").onClick();
buttonByText("完成").onClick();
expect(result.props.bulkSetListenerPorts).not.toHaveBeenCalled();
expect(result.props.bulkRenameNodes).not.toHaveBeenCalled();
expect(invalidRegexResult.props.bulkSetListenerPorts).not.toHaveBeenCalled();
expect(invalidRegexResult.props.bulkRenameNodes).not.toHaveBeenCalled();

result = renderDialog({ 0: "Missing" });
renderDialog({ 0: "Missing" });
buttonByText("批量删除监听端口").onClick();
expect(mocks.toast).toHaveBeenCalledWith(expect.objectContaining({ title: "暂无匹配节点", variant: "warning" }));
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

import * as React from "react";
import { ChevronDown, ChevronRight, Pencil, RotateCcw } from "lucide-react";
import { ChevronDown, ChevronRight, RotateCcw } from "lucide-react";
import { Badge } from "@subboost/ui/components/ui/badge";
import { confirmDialog } from "@subboost/ui/components/ui/confirm-dialog";
import {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ import {

export function ProxyGroupsCustomGroupsPanel() {
const {
ruleProviderBaseUrl,
enabledProxyGroups,
hiddenProxyGroups,
proxyGroupNameOverrides,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ import {
RULE_EDIT_TRAILING_CONTROLS_CLASS,
RULE_HEADER_ACTION_BUTTON_CLASS,
RULE_HEADER_ROW_CLASS,
RULE_TARGET_SELECT_TRIGGER_CLASS,
RULE_TEXT_ACTION_BUTTON_CLASS,
} from "./proxy-groups-rule-editor-layout";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ describe("ProxyGroupsModuleRulesPanel", () => {

it("moves, removes, restores, and adds CN experimental and candidate rule sets", () => {
const props = baseProps();
let result = renderPanel(props, {
renderPanel(props, {
0: [{ id: "candidate-1", name: "Candidate", behavior: "domain", path: "rule-set/candidate.mrs", parentRuleId: "parent" }],
});

Expand All @@ -256,8 +256,8 @@ describe("ProxyGroupsModuleRulesPanel", () => {
]);

const disabledProps = baseProps({ experimentalCnUseCnRuleSet: false });
result = renderPanel(disabledProps);
expect(result.html).toContain("已移除");
const disabledResult = renderPanel(disabledProps);
expect(disabledResult.html).toContain("已移除");
mocks.captures.buttons.find((button) => button["aria-label"] === "恢复 中国业务实验规则 规则集").onClick();
expect(disabledProps.onChangeExperimentalCnUseCnRuleSet).toHaveBeenCalledWith(true);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,21 +147,21 @@ describe("proxy group rules library search", () => {
});

it("loads total rules as helper copy", async () => {
let hook = useRenderedHook();
useRenderedHook();
await flushAsync();
hook = useRenderedHook();
const hook = useRenderedHook();

expect(mocks.bag.rulesApi.getTotalRules).toHaveBeenCalled();
expect(hook.totalRules).toBe(100);
});

it("searches after debounce and records success metadata", async () => {
let hook = useRenderedHook();
hook.setRuleSearchKeyword(" openai ");
hook = useRenderedHook();
const initialHook = useRenderedHook();
initialHook.setRuleSearchKeyword(" openai ");
useRenderedHook();
vi.advanceTimersByTime(300);
await flushAsync();
hook = useRenderedHook();
const hook = useRenderedHook();

expect(mocks.bag.rulesApi.searchRules).toHaveBeenCalledWith(
expect.objectContaining({ keyword: "openai", page: 1, size: 50 })
Expand Down Expand Up @@ -192,17 +192,17 @@ describe("proxy group rules library search", () => {
totalRules: 120,
});

let hook = useRenderedHook();
hook.setRuleSearchKeyword("ai");
hook = useRenderedHook();
const initialHook = useRenderedHook();
initialHook.setRuleSearchKeyword("ai");
useRenderedHook();
vi.advanceTimersByTime(300);
await flushAsync();
hook = useRenderedHook();
const firstPageHook = useRenderedHook();

expect(hook.canLoadMore).toBe(true);
hook.handleLoadMore();
expect(firstPageHook.canLoadMore).toBe(true);
firstPageHook.handleLoadMore();
await flushAsync();
hook = useRenderedHook();
const hook = useRenderedHook();

expect(mocks.bag.rulesApi.searchRules).toHaveBeenLastCalledWith(
expect.objectContaining({ keyword: "ai", page: 2, size: 50 })
Expand All @@ -216,12 +216,12 @@ describe("proxy group rules library search", () => {
getTotalRules: vi.fn(async () => null),
};

let hook = useRenderedHook();
hook.setRuleSearchKeyword("openai");
hook = useRenderedHook();
const initialHook = useRenderedHook();
initialHook.setRuleSearchKeyword("openai");
useRenderedHook();
vi.advanceTimersByTime(300);
await flushAsync();
hook = useRenderedHook();
const hook = useRenderedHook();

expect(hook.searchResults).toEqual([]);
expect(hook.rulesSearchError).toBe("规则库接口暂不可用");
Expand Down Expand Up @@ -254,29 +254,29 @@ describe("proxy group rules library search", () => {
})
.mockRejectedValue("boom");

let hook = useRenderedHook();
hook.setRuleSearchKeyword("missing");
hook = useRenderedHook();
const initialHook = useRenderedHook();
initialHook.setRuleSearchKeyword("missing");
useRenderedHook();
vi.advanceTimersByTime(300);
await flushAsync();
hook = useRenderedHook();
const emptyResultHook = useRenderedHook();

expect(hook.searchResults).toEqual([]);
expect(hook.totalMatched).toBeNull();
expect(hook.rulesSearchSource).toBeNull();
expect(emptyResultHook.searchResults).toEqual([]);
expect(emptyResultHook.totalMatched).toBeNull();
expect(emptyResultHook.rulesSearchSource).toBeNull();
expect(mocks.bag.interactions.rulesSearchCompleted).toHaveBeenCalledWith({
result: "noResult",
resultSource: "unknown",
resultCount: 0,
});

hook.setRuleSearchKeyword("again");
hook = useRenderedHook();
emptyResultHook.setRuleSearchKeyword("again");
useRenderedHook();
vi.advanceTimersByTime(300);
await flushAsync();
hook = useRenderedHook();
const failedHook = useRenderedHook();

expect(hook.rulesSearchError).toBe("搜索失败");
expect(failedHook.rulesSearchError).toBe("搜索失败");
});

it("guards load-more calls while busy or unavailable", () => {
Expand Down Expand Up @@ -315,23 +315,23 @@ describe("proxy group rules library search", () => {
})
.mockRejectedValueOnce("offline");

let hook = useRenderedHook();
hook.setRuleSearchKeyword("ai");
hook = useRenderedHook();
const initialHook = useRenderedHook();
initialHook.setRuleSearchKeyword("ai");
useRenderedHook();
vi.advanceTimersByTime(300);
await flushAsync();
hook = useRenderedHook();
const firstPageHook = useRenderedHook();

hook.handleLoadMore();
firstPageHook.handleLoadMore();
await flushAsync();
hook = useRenderedHook();
expect(hook.searchResults.map((item) => item.id)).toEqual(["a", "b"]);
expect(hook.rulesSearchSource).toBe("unavailable");
expect(hook.totalRules).toBe(101);
const mergedHook = useRenderedHook();
expect(mergedHook.searchResults.map((item) => item.id)).toEqual(["a", "b"]);
expect(mergedHook.rulesSearchSource).toBe("unavailable");
expect(mergedHook.totalRules).toBe(101);

hook.handleLoadMore();
mergedHook.handleLoadMore();
await flushAsync();
hook = useRenderedHook();
expect(hook.rulesSearchError).toBe("加载失败");
const failedHook = useRenderedHook();
expect(failedHook.rulesSearchError).toBe("加载失败");
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,6 @@ export function SourcesSection() {
<div className="flex flex-col gap-2">
{sources.map((source, index) => {
const typeInfo = sourceTypeInfo[source.type];
const TypeIcon = typeInfo.icon;
const sourceDisplayLabel = buildSourceDisplayLabel({
typeLabel: typeInfo.label,
tag: source.tag,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import * as React from "react";
import { ChevronDown, ChevronRight } from "lucide-react";
import { Badge } from "@subboost/ui/components/ui/badge";

export interface SectionHeaderProps {
expanded: boolean;
Expand Down
2 changes: 0 additions & 2 deletions packages/ui/src/product/preview/visual-graph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,6 @@ export function VisualGraph() {
);

// 节点名称列表
const nodeNames = React.useMemo(() => nodes.map((n) => n.name), [nodes]);

// 生成当前配置下的代理组(用于显示“默认选中项”)
const generatedProxyGroups = React.useMemo(() => {
if (nodes.length === 0) return [];
Expand Down
5 changes: 1 addition & 4 deletions packages/ui/src/store/config-store/persistence.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import type { ConfigState } from "./definitions";
import { safeParseJsonObject } from "@subboost/core/json";
import {
CONFIG_DRAFT_GUEST_STORAGE_NAME,
getConfigDraftStorageNameForUser,
} from "./draft-storage";
import { getConfigDraftStorageNameForUser } from "./draft-storage";

export {
CONFIG_DRAFT_GUEST_STORAGE_NAME,
Expand Down
1 change: 0 additions & 1 deletion packages/ui/src/store/config-store/source-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import type { ConfigActions, SubscriptionSource } from "./definitions";
import {
fetchUrlContentInBrowser,
getNodeSourceIds,
makeUniqueName,
ORIGIN_NAME_KEY,
SOURCE_IDS_KEY,
withNodeSourceId,
Expand Down