mirror of
https://github.com/jxxghp/MoviePilot-Plugins.git
synced 2026-05-26 23:16:47 +00:00
- 数据模型重构: 全面引入 Pydantic 模型(ClashConfig, Proxy, ProxyGroup 等)替代原有字典结构,提供更严格的数据验证与类型安全。 - 数据迁移机制: 新增 v2.1.0 数据升级脚本,支持将旧版代理、策略组及规则数据自动迁移至新架构。 - 配置补丁系统: 实现基于 JSON Patch 的细粒度配置修补机制,替代旧版覆盖逻辑,提升配置修改的灵活性。 - 服务层优化: 重写 ClashRuleProviderService 以适配新对象模型,增强代码可维护性与扩展性。 - API模型同步: 更新相关 API 数据模型以保持与内部数据结构的一致性。 - 用户界面: 批量规则管理和数据项隐藏支持
193 lines
5.4 KiB
Python
193 lines
5.4 KiB
Python
from enum import Enum, StrEnum
|
|
from typing import Any, List, Optional, Union, Dict, Literal
|
|
|
|
from pydantic import BaseModel, field_validator, ValidationInfo
|
|
|
|
|
|
class AdditionalParam(Enum):
|
|
NO_RESOLVE = 'no-resolve'
|
|
SRC = 'src'
|
|
|
|
|
|
class RoutingRuleType(Enum):
|
|
"""Enumeration of all supported Clash rule types"""
|
|
DOMAIN = "DOMAIN"
|
|
DOMAIN_SUFFIX = "DOMAIN-SUFFIX"
|
|
DOMAIN_KEYWORD = "DOMAIN-KEYWORD"
|
|
DOMAIN_REGEX = "DOMAIN-REGEX"
|
|
DOMAIN_WILDCARD = "DOMAIN-WILDCARD"
|
|
|
|
GEOSITE = "GEOSITE"
|
|
GEOIP = "GEOIP"
|
|
|
|
IP_CIDR = "IP-CIDR"
|
|
IP_CIDR6 = "IP-CIDR6"
|
|
IP_SUFFIX = "IP-SUFFIX"
|
|
IP_ASN = "IP-ASN"
|
|
|
|
|
|
SRC_GEOIP = "SRC-GEOIP"
|
|
SRC_IP_ASN = "SRC-IP-ASN"
|
|
SRC_IP_CIDR = "SRC-IP-CIDR"
|
|
SRC_IP_SUFFIX = "SRC-IP-SUFFIX"
|
|
|
|
DST_PORT = "DST-PORT"
|
|
SRC_PORT = "SRC-PORT"
|
|
|
|
IN_PORT = "IN-PORT"
|
|
IN_TYPE = "IN-TYPE"
|
|
IN_USER = "IN-USER"
|
|
IN_NAME = "IN-NAME"
|
|
|
|
PROCESS_PATH = "PROCESS-PATH"
|
|
PROCESS_PATH_REGEX = "PROCESS-PATH-REGEX"
|
|
PROCESS_NAME = "PROCESS-NAME"
|
|
PROCESS_NAME_REGEX = "PROCESS-NAME-REGEX"
|
|
|
|
UID = "UID"
|
|
NETWORK = "NETWORK"
|
|
DSCP = "DSCP"
|
|
|
|
RULE_SET = "RULE-SET"
|
|
AND = "AND"
|
|
OR = "OR"
|
|
NOT = "NOT"
|
|
SUB_RULE = "SUB-RULE"
|
|
|
|
MATCH = "MATCH"
|
|
|
|
|
|
class Action(StrEnum):
|
|
"""Enumeration of rule actions"""
|
|
DIRECT = "DIRECT"
|
|
REJECT = "REJECT"
|
|
REJECT_DROP = "REJECT-DROP"
|
|
PASS = "PASS"
|
|
COMPATIBLE = "COMPATIBLE"
|
|
|
|
|
|
class RuleBase(BaseModel):
|
|
rule_type: RoutingRuleType
|
|
action: Union[Action, str] # Can be Action enum or custom proxy group name
|
|
raw_rule: str
|
|
|
|
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"""
|
|
rule_type: RoutingRuleType
|
|
payload: str
|
|
additional_params: Optional[AdditionalParam] = None
|
|
|
|
def condition_string(self) -> str:
|
|
return f"{self.rule_type.value},{self.payload}"
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
return {
|
|
'type': self.rule_type.value,
|
|
'payload': self.payload,
|
|
'action': self.action.value if isinstance(self.action, Action) else self.action,
|
|
'additional_params': self.additional_params.value if self.additional_params else None,
|
|
'rule_string': str(self)
|
|
}
|
|
|
|
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
|
|
|
|
@field_validator('payload', mode='after')
|
|
@classmethod
|
|
def validate_payload(cls, v: Optional[str], info: ValidationInfo) -> Optional[str]:
|
|
# 获取其他字段的值
|
|
rule_type = info.data['rule_type']
|
|
|
|
if rule_type == RoutingRuleType.NETWORK and v is not None 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)"""
|
|
rule_type: Literal[RoutingRuleType.AND, RoutingRuleType.OR, RoutingRuleType.NOT]
|
|
conditions: List[Union[ClashRule, 'LogicRule']]
|
|
|
|
def condition_string(self) -> str:
|
|
conditions_str = ','.join([f"({c.condition_string()})" for c in self.conditions])
|
|
return f"{self.rule_type.value},({conditions_str})"
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
conditions: list[str] = []
|
|
for condition in self.conditions:
|
|
conditions.append(condition.condition_string())
|
|
|
|
return {
|
|
'type': self.rule_type.value,
|
|
'conditions': conditions,
|
|
'action': self.action.value if isinstance(self.action, Action) else self.action,
|
|
'rule_string': str(self)
|
|
}
|
|
|
|
@field_validator('conditions', mode='after')
|
|
@classmethod
|
|
def validate_conditions(cls, v: List[Union[ClashRule, 'LogicRule']]) -> List[Union[ClashRule, 'LogicRule']]:
|
|
if not v:
|
|
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
|
|
condition: Union[ClashRule, LogicRule]
|
|
action: str
|
|
|
|
def condition_string(self) -> str:
|
|
return f"{self.rule_type.value},({self.condition.condition_string()})"
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
return {
|
|
'type': self.rule_type.value,
|
|
'condition': f"({self.condition.condition_string()})",
|
|
'action': self.action,
|
|
'rule_string': str(self)
|
|
}
|
|
|
|
def __str__(self) -> str:
|
|
return f"{self.condition_string()},{self.action}"
|
|
|
|
|
|
class MatchRule(RuleBase):
|
|
"""Represents a match rule"""
|
|
rule_type: Literal[RoutingRuleType.MATCH] = RoutingRuleType.MATCH
|
|
|
|
@staticmethod
|
|
def condition_string() -> str:
|
|
return "MATCH"
|
|
|
|
def to_dict(self) -> Dict[str, str]:
|
|
return {
|
|
'type': 'MATCH',
|
|
'action': self.action.value if isinstance(self.action, Action) else self.action,
|
|
'rule_string': str(self)
|
|
}
|
|
|
|
def __str__(self) -> str:
|
|
return f"{self.condition_string()},{self.action}"
|
|
|
|
|
|
RuleType = Union[ClashRule, LogicRule, SubRule, MatchRule]
|