Skip to content

Commit 75b66df

Browse files
committed
fix: preserve valid original rule targets
解决了什么 bug: - 修复 CVR auto-routing 中原始 rules 指向 Proxy B 等现有分组或代理节点时被错误改写到 Proxy A 的问题。 为什么会导致这样: - convertOriginalRules() 以前只保留 DIRECT/REJECT/block,其余 policy 统一降级到默认代理。 - Proxy B 虽然是构建后会创建的预设分组,但旧逻辑没有判断目标是否实际存在,因此把它当作未知旧订阅分组处理。 怎么修复以及影响模块: - 新增 RulePolicyTargets 收集合法 rule policy 目标,包括内置策略、预设分组、代理节点和自定义分组。 - AutoRoutingConfig 与 GlobalRestrictedGroup 现在只对不存在的旧订阅分组降级,保留所有可解析目标。 - 补充 ConfigFactory 单元测试和 CVR 集成回归,覆盖 Proxy B、代理节点和旧 GLOBAL 分组。
1 parent 6bb899c commit 75b66df

5 files changed

Lines changed: 114 additions & 12 deletions

File tree

src/AutoRoutingConfig.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { Clash, Group } from './ClashConfigBuilder';
1010
import type { YAML } from './types/client';
1111
import type { UserCustomRulesConfig } from './types/user-rules';
1212
import { convert3DRulesToMihomoRules, validate3DRules } from './config/UserCustomRulesConverter';
13+
import { collectAvailableRulePolicyTargets } from './config/RulePolicyTargets';
1314

1415
const SUPPORTED_RULE_TYPES = new Set([
1516
'DOMAIN-SUFFIX',
@@ -369,11 +370,16 @@ export class AutoRoutingGroup extends Clash {
369370

370371
/**
371372
* 转换用户原始rules的group名称
372-
* - block/REJECT/DIRECT 保持不变
373-
* - 其他group统一转换为 ProxyA
373+
* - 已存在的内置策略、预设分组、自定义分组、代理节点保持不变
374+
* - 指向已被清空的原始订阅分组时,降级到 ProxyA,避免悬空 policy
374375
*/
375376
convertOriginalRules(originalRules: string[]): string[] {
376377
const proxyAGroup = this.presetGroups[SelectorSymbols.ManualA];
378+
const availablePolicyTargets = collectAvailableRulePolicyTargets(
379+
this.presetGroups,
380+
this.proxiesList,
381+
this.userRules.groups,
382+
);
377383

378384
return originalRules.flatMap(rule => {
379385
const parts = rule.split(',');
@@ -390,13 +396,11 @@ export class AutoRoutingGroup extends Clash {
390396
// 普通规则: TYPE,value,group 或 TYPE,value,group,no-resolve
391397
if (parts.length >= 3) {
392398
const group = parts[2];
393-
// block/REJECT/DIRECT 保持不变
394-
if (group === 'block' || group === 'REJECT' || group === 'DIRECT') {
399+
if (availablePolicyTargets.has(group)) {
395400
return [rule];
396401
}
397-
// 其他group转换为ProxyA
402+
398403
if (parts.length === 4) {
399-
// 带no-resolve的规则
400404
return [`${parts[0]},${parts[1]},${proxyAGroup},${parts[3]}`];
401405
}
402406
return [`${parts[0]},${parts[1]},${proxyAGroup}`];

src/config/ConfigFactory.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,46 @@ describe('ConfigFactory 测试', () => {
159159
'应该包含 Telegram IPv6 CIDR 规则'
160160
);
161161
});
162+
163+
it('auto-routing 应该保留原始 rules 中已存在的预设分组和代理节点', () => {
164+
const config = ConfigFactory.createConfig(
165+
'auto-routing',
166+
{
167+
source: {
168+
proxies: [
169+
{ name: 'Proxy1', type: 'ss', server: 'test.com', port: 443, password: 'test', udp: false }
170+
],
171+
'proxy-groups': [],
172+
rules: [
173+
'DOMAIN-SUFFIX,dola.com,🍀 Proxy B 🍀',
174+
'DOMAIN-SUFFIX,node-target.example,Proxy1',
175+
'DOMAIN-SUFFIX,legacy-group.example,GLOBAL',
176+
'MATCH,DIRECT'
177+
]
178+
},
179+
raw: ''
180+
} as any,
181+
mockDeps as any,
182+
mockParams
183+
) as AutoRoutingGroup;
184+
185+
assert.ok(
186+
config.source.rules.includes('DOMAIN-SUFFIX,dola.com,🍀 Proxy B 🍀'),
187+
'Proxy B 预设分组不应该被改写为 Proxy A'
188+
);
189+
assert.ok(
190+
config.source.rules.includes('DOMAIN-SUFFIX,node-target.example,Proxy1'),
191+
'仍存在的代理节点可以作为规则 policy 目标'
192+
);
193+
assert.ok(
194+
config.source.rules.includes('DOMAIN-SUFFIX,legacy-group.example,🫧 Proxy A 🫧'),
195+
'已被清空的原始订阅分组应该降级到 Proxy A'
196+
);
197+
assert.ok(
198+
!config.source.rules.includes('DOMAIN-SUFFIX,dola.com,🫧 Proxy A 🫧'),
199+
'不应生成被错误改写的 dola.com 规则'
200+
);
201+
});
162202
});
163203

164204
describe('模式特性验证', () => {

src/config/GlobalRestrictedGroup.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import { RoutingConfig } from './RoutingConfig';
88
import { Group } from '../ClashConfigBuilder';
99
import { SelectorSymbols } from '../RuleConverters';
10+
import { collectAvailableRulePolicyTargets } from './RulePolicyTargets';
1011
import type { ClientDependencies, ClientParams, ClientSource } from '../types/client';
1112

1213
export class GlobalRestrictedGroup extends RoutingConfig {
@@ -90,12 +91,16 @@ export class GlobalRestrictedGroup extends RoutingConfig {
9091

9192
/**
9293
* 转换用户原始rules的group名称
93-
* - block/REJECT/DIRECT 保持不变
94-
* - 其他group统一转换为 ProxyA
94+
* - 已存在的内置策略、预设分组、代理节点保持不变
95+
* - 指向已被清空的原始订阅分组时,降级到 Global Proxy,避免悬空 policy
9596
*/
9697
convertOriginalRules(): string[] {
9798
const proxyAGroup = this.presetGroups[SelectorSymbols.ManualA];
9899
const originalRules = this.originalRules || [];
100+
const availablePolicyTargets = collectAvailableRulePolicyTargets(
101+
this.presetGroups,
102+
this.proxiesList,
103+
);
99104

100105
if (originalRules.length === 0) {
101106
return [];
@@ -118,13 +123,11 @@ export class GlobalRestrictedGroup extends RoutingConfig {
118123
// 普通规则: TYPE,value,group 或 TYPE,value,group,no-resolve
119124
if (parts.length >= 3) {
120125
const group = parts[2];
121-
// block/REJECT/DIRECT 保持不变
122-
if (group === 'block' || group === 'REJECT' || group === 'DIRECT') {
126+
if (availablePolicyTargets.has(group)) {
123127
return [rule];
124128
}
125-
// 其他group转换为ProxyA
129+
126130
if (parts.length === 4) {
127-
// 带no-resolve的规则
128131
return [`${parts[0]},${parts[1]},${proxyAGroup},${parts[3]}`];
129132
}
130133
return [`${parts[0]},${parts[1]},${proxyAGroup}`];

src/config/RulePolicyTargets.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/**
2+
* Clash rules 的 policy 目标可以是内置策略、代理组或单个代理节点。
3+
*/
4+
5+
const BUILTIN_RULE_POLICIES = [
6+
'DIRECT',
7+
'REJECT',
8+
'REJECT-DROP',
9+
'REJECT-NO-DROP',
10+
'PASS',
11+
'block',
12+
] as const;
13+
14+
export function collectAvailableRulePolicyTargets(
15+
presetGroups: object,
16+
proxiesList: string[] = [],
17+
customGroups: Array<{ name?: string }> = [],
18+
): Set<string> {
19+
const presetRecord = presetGroups as Record<PropertyKey, unknown>;
20+
21+
// presetGroups 使用 Symbol 作为部分 key,Object.values 会漏掉这些分组。
22+
const presetGroupNames = Reflect.ownKeys(presetRecord)
23+
.map((key) => presetRecord[key])
24+
.filter((value): value is string => typeof value === 'string' && value.length > 0);
25+
26+
const customGroupNames = customGroups
27+
.map((group) => group.name)
28+
.filter((name): name is string => typeof name === 'string' && name.length > 0);
29+
30+
return new Set([
31+
...BUILTIN_RULE_POLICIES,
32+
...presetGroupNames,
33+
...proxiesList,
34+
...customGroupNames,
35+
]);
36+
}

tests/integration.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,25 @@ describe('CVR 客户端业务逻辑测试', () => {
408408
assert.ok(hasGeoIPRule, '应该包含 GEOIP,CN 规则');
409409
});
410410

411+
it('应该保留 CVR 原始 rules 中指向 🍀 Proxy B 🍀 的规则', () => {
412+
const testConfig = createTestConfig();
413+
testConfig.rules.unshift('DOMAIN-SUFFIX,dola.com,🍀 Proxy B 🍀');
414+
415+
const result = testMainFunction('cvr', 'auto-routing', testConfig);
416+
417+
assert.ok(result.success, `执行失败: ${result.error?.message}`);
418+
419+
const rules = result.output.rules || [];
420+
assert.ok(
421+
rules.includes('DOMAIN-SUFFIX,dola.com,🍀 Proxy B 🍀'),
422+
'dola.com 应该保留指向 Proxy B'
423+
);
424+
assert.ok(
425+
!rules.includes('DOMAIN-SUFFIX,dola.com,🫧 Proxy A 🫧'),
426+
'dola.com 不应该被改写到 Proxy A'
427+
);
428+
});
429+
411430
it('应该包含大量 Microsoft 规则', () => {
412431
const testConfig = createTestConfig();
413432
const result = testMainFunction('cvr', 'auto-routing', testConfig);

0 commit comments

Comments
 (0)