Skip to content
Closed
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
34 changes: 34 additions & 0 deletions packages/core/src/generator/chain.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,40 @@ function dialerGroup(patch: Partial<DialerProxyGroup> = {}): DialerProxyGroup {
}

describe("dialer proxy chain helpers", () => {
it("applies global url-test overrides without changing legacy defaults", () => {
const groups = [
dialerGroup({
id: "auto-relay",
name: "Auto Relay",
type: "url-test",
relayNodes: ["Relay A"],
}),
];

expect(
generateDialerProxyGroups(
groups,
"https://probe.example.com/204",
120,
[],
{ urlTestLazy: false, urlTestTolerance: 50 },
),
).toEqual([
{
name: "Auto Relay",
type: "url-test",
proxies: ["Relay A"],
url: "https://probe.example.com/204",
interval: 120,
lazy: false,
tolerance: 50,
},
]);

expect(generateDialerProxyGroups(groups)[0]).not.toHaveProperty("tolerance");
expect(generateDialerProxyGroups(groups)[0]).toMatchObject({ lazy: true });
});

it("generates typed dialer groups with shared proxy group fields", () => {
expect(
generateDialerProxyGroups(
Expand Down
6 changes: 4 additions & 2 deletions packages/core/src/generator/chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ export function generateDialerProxyGroups(
groups: DialerProxyGroup[],
testUrl: string = DEFAULT_SUBBOOST_CONFIG.testUrl,
testInterval: number = DEFAULT_SUBBOOST_CONFIG.testInterval,
proxyProviderNames: string[] = []
proxyProviderNames: string[] = [],
urlTestOptions: { urlTestLazy?: boolean; urlTestTolerance?: number } = {},
): Array<Record<string, unknown>> {
const providerUse = proxyProviderNames.length > 0 ? { use: proxyProviderNames } : {};
return groups
Expand All @@ -48,7 +49,8 @@ export function generateDialerProxyGroups(
testInterval,
strategy: group.group.strategy ?? DEFAULT_LOAD_BALANCE_STRATEGY,
extraFields: providerUse,
urlTestLazy: true,
urlTestLazy: urlTestOptions.urlTestLazy ?? true,
urlTestTolerance: urlTestOptions.urlTestTolerance,
});
});
}
Expand Down
33 changes: 33 additions & 0 deletions packages/core/src/generator/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,39 @@ function ssNode(patch: Partial<ParsedNode> = {}): ParsedNode {
const REALITY_PUBLIC_KEY = "A".repeat(43);

describe("generateClashConfig", () => {
it("applies global url-test overrides to regular and dialer groups", () => {
const config = generateClashConfig({
nodes: [
ssNode({ name: "Relay" }),
ssNode({ name: "Target", server: "target.example.com" }),
],
userConfig: {
dnsYaml: "",
enabledGroups: ["auto"],
urlTestLazy: true,
urlTestTolerance: 50,
},
dialerProxyGroups: [
{
id: "relay",
name: "Relay Auto",
type: "url-test",
relayNodes: ["Relay"],
targetNodes: ["Target"],
},
],
});

const urlTestGroups = (config["proxy-groups"] ?? []).filter((group) => group.type === "url-test");
expect(urlTestGroups.length).toBeGreaterThanOrEqual(2);
expect(urlTestGroups).toEqual(
expect.arrayContaining([
expect.objectContaining({ name: "⚡ 自动选择", lazy: true, tolerance: 50 }),
expect.objectContaining({ name: "Relay Auto", lazy: true, tolerance: 50 }),
]),
);
});

it("treats explicit empty base YAML as a strict patch instead of re-adding defaults", () => {
const config = generateClashConfig({
nodes: [ssNode()],
Expand Down
8 changes: 7 additions & 1 deletion packages/core/src/generator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,11 @@ export function generateClashConfig(options: GenerateOptions): ClashConfig {
sanitizedDialerProxyGroups,
config.testUrl,
config.testInterval,
proxyProviderNames
proxyProviderNames,
{
urlTestLazy: config.urlTestLazy,
urlTestTolerance: config.urlTestTolerance,
},
) as unknown as ProxyGroup[];

// 生成选项
Expand All @@ -354,6 +358,8 @@ export function generateClashConfig(options: GenerateOptions): ClashConfig {
ruleProviderBaseUrl: config.ruleProviderBaseUrl,
testUrl: config.testUrl,
testInterval: config.testInterval,
urlTestLazy: config.urlTestLazy,
urlTestTolerance: config.urlTestTolerance,
customProxyGroups,
customRuleSets,
proxyGroupAdvanced,
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/generator/proxy-group-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type BuildTypedProxyGroupOptions = {
strategy?: LoadBalanceStrategy;
extraFields?: Record<string, unknown>;
urlTestLazy?: boolean;
urlTestTolerance?: number;
};

export function uniqueProxyNames(values: string[]): string[] {
Expand All @@ -36,6 +37,7 @@ export function buildTypedProxyGroup({
strategy,
extraFields = {},
urlTestLazy = false,
urlTestTolerance,
}: BuildTypedProxyGroupOptions): ProxyGroup {
const base: ProxyGroup = {
name,
Expand All @@ -52,6 +54,7 @@ export function buildTypedProxyGroup({
url: testUrl,
interval: testInterval,
lazy: urlTestLazy,
...(urlTestTolerance !== undefined ? { tolerance: urlTestTolerance } : {}),
};
case "fallback":
return {
Expand Down
18 changes: 18 additions & 0 deletions packages/core/src/generator/proxy-groups.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,24 @@ function customGroup(id: string, groupType: CustomProxyGroup["groupType"]): Cust
}

describe("proxy group generator", () => {
it("applies global url-test overrides when configured", () => {
const groups = generateProxyGroups({
nodes: [node("Node A")],
enabledModules: ["auto"],
ruleProviderBaseUrl: "https://rules.example.com",
testUrl: "https://probe.example.com/204",
testInterval: 120,
urlTestLazy: true,
urlTestTolerance: 50,
});

expect(groups.find((group) => group.name.includes("自动选择"))).toMatchObject({
type: "url-test",
lazy: true,
tolerance: 50,
});
});

it("generates module, custom, advanced-filtered, and provider-backed groups", () => {
const groups = generateProxyGroups({
nodes: [node("Node A"), node("Node B")],
Expand Down
7 changes: 6 additions & 1 deletion packages/core/src/generator/proxy-groups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ export interface GenerateOptions {
ruleProviderBaseUrl: string;
testUrl: string;
testInterval: number;
urlTestLazy?: boolean;
urlTestTolerance?: number;
customProxyGroups?: CustomProxyGroup[];
customRuleSets?: CustomRuleSet[];
proxyGroupAdvanced?: Record<string, ProxyGroupAdvancedConfig>;
Expand Down Expand Up @@ -137,6 +139,8 @@ export function generateProxyGroups(options: GenerateOptions): ProxyGroup[] {
enabledModules,
testUrl,
testInterval,
urlTestLazy,
urlTestTolerance,
customProxyGroups = [],
proxyGroupAdvanced = {},
proxyGroupNameOverrides,
Expand Down Expand Up @@ -237,7 +241,8 @@ export function generateProxyGroups(options: GenerateOptions): ProxyGroup[] {
testInterval,
strategy,
extraFields,
urlTestLazy: false,
urlTestLazy: urlTestLazy ?? false,
urlTestTolerance,
});

// 辅助函数:生成单个代理组
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/subscription/config-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ describe("subscription config utils", () => {
ruleProviderBaseUrl: " https://rules.example.com ",
testUrl: "https://cp.cloudflare.com",
testInterval: 180,
urlTestLazy: true,
urlTestTolerance: 50,
},
{ nodes: [node()] }
);
Expand All @@ -117,6 +119,8 @@ describe("subscription config utils", () => {
experimentalCnUseCnRuleSet: true,
testUrl: "https://cp.cloudflare.com",
testInterval: 180,
urlTestLazy: true,
urlTestTolerance: 50,
listenerPorts: { Node: 12000 },
});
expect(userConfig.customRules?.[0]).toMatchObject({
Expand Down
9 changes: 9 additions & 0 deletions packages/core/src/subscription/config-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,13 @@ export function buildGenerateOptionsFromConfig(
const cnIpNoResolve = typeof config.cnIpNoResolve === "boolean" ? config.cnIpNoResolve : undefined;
const experimentalCnUseCnRuleSet =
typeof config.experimentalCnUseCnRuleSet === "boolean" ? config.experimentalCnUseCnRuleSet : undefined;
const urlTestLazy = typeof config.urlTestLazy === "boolean" ? config.urlTestLazy : undefined;
const urlTestTolerance =
typeof config.urlTestTolerance === "number" &&
Number.isInteger(config.urlTestTolerance) &&
config.urlTestTolerance >= 0
? config.urlTestTolerance
: undefined;
const proxyGroupNameOverrides = normalizeProxyGroupNameOverrides(config.proxyGroupNameOverrides);
const listenerPorts = normalizeListenerPorts(config.listenerPorts);
const ruleOrder = normalizePersistedRuleOrder({
Expand All @@ -290,6 +297,8 @@ export function buildGenerateOptionsFromConfig(
...(autoSelectStrategy ? { autoSelectStrategy } : {}),
testUrl,
testInterval,
...(urlTestLazy !== undefined ? { urlTestLazy } : {}),
...(urlTestTolerance !== undefined ? { urlTestTolerance } : {}),
...(cnIpNoResolve !== undefined ? { cnIpNoResolve } : {}),
...(experimentalCnUseCnRuleSet !== undefined ? { experimentalCnUseCnRuleSet } : {}),
...(normalizePort(config.mixedPort) !== undefined ? { mixedPort: normalizePort(config.mixedPort) as number } : {}),
Expand Down
14 changes: 14 additions & 0 deletions packages/core/src/templates/config-template.fields.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,20 @@ import { validateSubBoostTemplateConfig } from "./config-template";
import { expectInvalid, validConfig } from "./config-template.test-helpers";

describe("validateSubBoostTemplateConfig field validation", () => {
it("accepts optional global url-test overrides and validates tolerance", () => {
const result = validateSubBoostTemplateConfig(
validConfig({ urlTestLazy: true, urlTestTolerance: 50 }),
);

expect(result.ok && result.config).toMatchObject({
urlTestLazy: true,
urlTestTolerance: 50,
});
expectInvalid({ urlTestLazy: "yes" as never }, "urlTestLazy 必须是布尔值");
expectInvalid({ urlTestTolerance: -1 }, "urlTestTolerance 必须是非负整数");
expectInvalid({ urlTestTolerance: 1.5 }, "urlTestTolerance 必须是非负整数");
});

it("rejects invalid dialer, module rule, scalar, and URL fields", () => {
expectInvalid({ dialerProxyGroups: "bad" as never }, "dialerProxyGroups 必须是数组");
expectInvalid({ dialerProxyGroups: [1 as never] }, "dialerProxyGroups 只能包含对象");
Expand Down
17 changes: 17 additions & 0 deletions packages/core/src/templates/config-template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ export function validateSubBoostTemplateConfig(value: unknown): ValidationResult
if (!testUrl.ok) return testUrl;
const testInterval = parsePositiveInteger(value.testInterval, "testInterval");
if (!testInterval.ok) return testInterval;
const urlTestLazy = parseOptionalBoolean(value.urlTestLazy, "urlTestLazy");
if (!urlTestLazy.ok) return urlTestLazy;
const urlTestTolerance = parseOptionalNonNegativeInteger(value.urlTestTolerance, "urlTestTolerance");
if (!urlTestTolerance.ok) return urlTestTolerance;
const ruleProviderBaseUrl = parseHttpUrlString(value.ruleProviderBaseUrl, "ruleProviderBaseUrl");
if (!ruleProviderBaseUrl.ok) return ruleProviderBaseUrl;
const cnIpNoResolve = parseOptionalBoolean(value.cnIpNoResolve, "cnIpNoResolve");
Expand Down Expand Up @@ -142,6 +146,8 @@ export function validateSubBoostTemplateConfig(value: unknown): ValidationResult
allowLan: allowLan.value,
testUrl: testUrl.value,
testInterval: testInterval.value,
...(urlTestLazy.value !== undefined ? { urlTestLazy: urlTestLazy.value } : {}),
...(urlTestTolerance.value !== undefined ? { urlTestTolerance: urlTestTolerance.value } : {}),
ruleProviderBaseUrl: ruleProviderBaseUrl.value,
},
};
Expand Down Expand Up @@ -209,6 +215,17 @@ function parsePositiveInteger(
return { ok: true, value };
}

function parseOptionalNonNegativeInteger(
value: unknown,
field: string
): { ok: true; value?: number } | { ok: false; error: string } {
if (value === undefined) return { ok: true, value: undefined };
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
return invalid(`${field} 必须是非负整数`);
}
return { ok: true, value };
}

function parsePort(value: unknown, field: string): { ok: true; value: number } | { ok: false; error: string } {
const parsed = parsePositiveInteger(value, field);
if (!parsed.ok) return parsed;
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,8 @@ export interface UserConfig {
autoSelectStrategy: "url-test" | "fallback" | "load-balance";
testUrl: string;
testInterval: number;
urlTestLazy?: boolean;
urlTestTolerance?: number;

// 规则设置
ruleProviderBaseUrl: string;
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/types/template-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,7 @@ export type SubBoostTemplateConfig = {
allowLan: boolean;
testUrl: string;
testInterval: number;
urlTestLazy?: boolean;
urlTestTolerance?: number;
ruleProviderBaseUrl: string;
};
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ 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 { ProxyGroupsUrlTestSettings } from "./proxy-groups-url-test-settings";

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";
Expand Down Expand Up @@ -315,6 +316,8 @@ export function ProxyGroupsCategories() {
</div>
</div>

<ProxyGroupsUrlTestSettings />

<div className="space-y-1">
<div className={PROXY_GROUP_SECTION_LABEL_ROW_CLASS}>
<label className={PROXY_GROUP_SECTION_LABEL_CLASS}>分流规则组</label>
Expand Down
Loading