diff --git a/package.v2.json b/package.v2.json index 550a94b..a939e52 100644 --- a/package.v2.json +++ b/package.v2.json @@ -485,12 +485,13 @@ "name": "Clash Rule Provider", "description": "随时为Clash添加一些额外的规则。", "labels": "工具", - "version": "2.0.6", + "version": "2.0.7", "icon": "Mihomo_Meta_A.png", "author": "wumode", "level": 1, "release": true, "history": { + "v2.0.7": "修复子规则比较错误", "v2.0.6": "修复已知问题; 改进对代理组的配置和验证", "v2.0.5": "完善了对嵌套逻辑规则和子规则的配置和验证", "v2.0.4": "修复已知问题; 使用异步调度器; 显示规则更改日期", diff --git a/plugins.v2/clashruleprovider/__init__.py b/plugins.v2/clashruleprovider/__init__.py index baa0724..726dd09 100644 --- a/plugins.v2/clashruleprovider/__init__.py +++ b/plugins.v2/clashruleprovider/__init__.py @@ -30,7 +30,7 @@ class ClashRuleProvider(_ClashRuleProviderBase): # 插件图标 plugin_icon = "Mihomo_Meta_A.png" # 插件版本 - plugin_version = "2.0.6" + plugin_version = "2.0.7" # 插件作者 plugin_author = "wumode" # 作者主页 diff --git a/plugins.v2/clashruleprovider/helper/clashrulemanager.py b/plugins.v2/clashruleprovider/helper/clashrulemanager.py index 8353a99..ec7aabe 100644 --- a/plugins.v2/clashruleprovider/helper/clashrulemanager.py +++ b/plugins.v2/clashruleprovider/helper/clashrulemanager.py @@ -32,7 +32,7 @@ class ClashRuleManager: def export_rules(self) -> List[Dict[str, str]]: rules_list = [] for rule in self.rules: - rules_list.append({'rule': rule.rule.raw_rule, 'remark': rule.remark, 'time_modified': rule.time_modified}) + rules_list.append({'rule': str(rule.rule), 'remark': rule.remark, 'time_modified': rule.time_modified}) return rules_list def append_rules(self, clash_rules: List[RuleItem]): @@ -97,30 +97,10 @@ class ClashRuleManager: def has_rule(self, clash_rule: Union[ClashRule, LogicRule, MatchRule]) -> bool: """Check if there is an identical rule""" - for r in self.rules: - rule = r.rule - if rule.rule_type != RoutingRuleType.MATCH: - if rule.rule_type == clash_rule.rule_type and rule.action == clash_rule.action \ - and rule.payload == clash_rule.payload: - return True - else: - if rule.rule_type == clash_rule.rule_type and rule.action == clash_rule.action: - return True - return False + return any(r.rule == clash_rule for r in self.rules) def has_rule_item(self, clash_rule: RuleItem) -> bool: - for r in self.rules: - if clash_rule.remark != r.remark: - continue - rule = r.rule - if rule.rule_type != RoutingRuleType.MATCH: - if rule.rule_type == clash_rule.rule.rule_type and rule.action == clash_rule.rule.action \ - and rule.payload == clash_rule.rule.payload: - return True - else: - if rule.rule_type == clash_rule.rule.rule_type and rule.action == clash_rule.rule.action: - return True - return False + return any(clash_rule.remark == r.remark and r.rule == clash_rule.rule for r in self.rules) def reorder_rules(self, moved_priority: int, target_priority: int) -> RuleItem: """Reorder the rules""" diff --git a/plugins.v2/clashruleprovider/helper/clashruleparser.py b/plugins.v2/clashruleprovider/helper/clashruleparser.py index 79dbbcf..136a801 100644 --- a/plugins.v2/clashruleprovider/helper/clashruleparser.py +++ b/plugins.v2/clashruleprovider/helper/clashruleparser.py @@ -24,7 +24,7 @@ class ClashRuleParser: # Handle regular rules return ClashRuleParser._parse_regular_rule(line) - except (ValidationError, TypeError, ValueError): + except (ValidationError, TypeError, ValueError, RecursionError): return None @staticmethod @@ -120,7 +120,7 @@ class ClashRuleParser: ) @staticmethod - def _parenthesis_balance(s: str) -> int: + def _parenthesis_balance(s: str) -> Optional[int]: """Calculate balance of parenthesis""" balance = 0 for i, char in enumerate(s): @@ -128,6 +128,8 @@ class ClashRuleParser: balance += 1 elif char == ')': balance -= 1 + if balance < 0: + return None return balance @staticmethod @@ -281,7 +283,7 @@ class ClashRuleParser: ) conditions.append(condition) except ValueError: - continue + raise ValueError(f"Invalid rule format: {content}") return conditions diff --git a/plugins.v2/clashruleprovider/models/rule/__init__.py b/plugins.v2/clashruleprovider/models/rule/__init__.py index 36ef042..7e37a5a 100644 --- a/plugins.v2/clashruleprovider/models/rule/__init__.py +++ b/plugins.v2/clashruleprovider/models/rule/__init__.py @@ -65,6 +65,9 @@ class Action(Enum): PASS = "PASS" COMPATIBLE = "COMPATIBLE" + def __str__(self) -> str: + return self.value + class RuleBase(BaseModel): rule_type: RoutingRuleType @@ -74,6 +77,14 @@ class RuleBase(BaseModel): def to_dict(self) -> Dict[str, Any]: pass + def __str__(self) -> str: + pass + + def __eq__(self, other: 'RuleBase') -> bool: + if not isinstance(other, RuleBase): + return NotImplemented + return self.__str__() == other.__str__() + class ClashRule(RuleBase): """Represents a parsed Clash routing rule""" @@ -93,6 +104,18 @@ class ClashRule(RuleBase): 'raw': self.raw_rule } + def __str__(self) -> str: + rule_str = f"{self.condition_string()},{self.action}" + if self.additional_params: + rule_str += f",{self.additional_params.value}" + return rule_str + + @validator('payload', allow_reuse=True) + def validate_payload(cls, v: str, values: Dict[str, Any]) -> Optional[str]: + if values.get('rule_type') == RoutingRuleType.NETWORK and v.upper() not in ('TCP', 'UDP'): + raise ValueError('Payload must be TCP or UDP') + return v + class LogicRule(RuleBase): """Represents a logic rule (AND, OR, NOT)""" @@ -121,6 +144,9 @@ class LogicRule(RuleBase): raise ValueError('A condition list must be provided') return v + def __str__(self) -> str: + return f"{self.condition_string()},{self.action}" + class SubRule(RuleBase): rule_type: Literal[RoutingRuleType.SUB_RULE] = RoutingRuleType.SUB_RULE @@ -128,7 +154,7 @@ class SubRule(RuleBase): action: str def condition_string(self) -> str: - return f"{self.rule_type.value},{self.condition.condition_string()}" + return f"{self.rule_type.value},({self.condition.condition_string()})" def to_dict(self) -> Dict[str, Any]: return { @@ -138,6 +164,9 @@ class SubRule(RuleBase): 'raw': self.raw_rule } + def __str__(self) -> str: + return f"{self.condition_string()},{self.action}" + class MatchRule(RuleBase): """Represents a match rule""" @@ -154,5 +183,8 @@ class MatchRule(RuleBase): 'raw': self.raw_rule } + def __str__(self) -> str: + return f"{self.condition_string()},{self.action}" + RuleType = Union[ClashRule, LogicRule, SubRule, MatchRule] diff --git a/plugins.v2/clashruleprovider/services.py b/plugins.v2/clashruleprovider/services.py index a34e51b..45480f4 100644 --- a/plugins.v2/clashruleprovider/services.py +++ b/plugins.v2/clashruleprovider/services.py @@ -648,7 +648,7 @@ class ClashRuleProviderService: if rule.payload not in config.get('rule-providers', {}): logger.warn(f"规则集合 {rule.payload!r} 不存在, 跳过 {rule.raw_rule!r}") continue - top_rules.append(rule.raw_rule) + top_rules.append(str(rule)) for raw_rule in config.get("rules", []): rule = ClashRuleParser.parse_rule_line(raw_rule) if not rule: @@ -659,7 +659,7 @@ class ClashRuleProviderService: rule.rule_type != RoutingRuleType.SUB_RULE): logger.warn(f"出站 {rule.action!r} 不存在, 跳过 {rule.raw_rule!r}") continue - top_rules.append(rule.raw_rule) + top_rules.append(str(rule)) config["rules"] = top_rules # 添加 Hosts