update(ClashRuleProvider): 支持规则集合; 添加ACL4SSR规则集; 配置说明

This commit is contained in:
wumode
2025-06-20 22:49:38 +08:00
parent 0db1640133
commit 5f5e385fda
9 changed files with 1426 additions and 736 deletions

View File

@@ -449,11 +449,12 @@
"name": "Clash Rule Provider",
"description": "随时为Clash添加一些额外的规则。",
"labels": "工具",
"version": "1.0.1",
"version": "1.1.0",
"icon": "Mihomo_Meta_A.png",
"author": "wumode",
"level": 1,
"history": {
"v1.1.0": "支持规则集合; 添加ACL4SSR规则集; 配置说明",
"v1.0.1": "支持规则搜索, 优化细节",
"v1.0.0": "支持: 规则分页; 导入规则; 代理组; 附加出站代理; 按区域分组",
"v0.1.0": "新增ClashRuleProvider"

View File

@@ -0,0 +1,38 @@
# Clash Rule Provider
**Clash Rule Provider** 生成适用于 [Meta Kernel](https://github.com/MetaCubeX/mihomo/tree/Meta) 定制配置,便于增加、修改和删除规则。
- 即时通知 Clash 刷新规则集合
- 基于 Meta 内核丰富的代理组配置,提供灵活的路由功能
- 支持按大洲分组节点
- 支持 [ACL4SSR](https://github.com/ACL4SSR/ACL4SSR) 规则集合
## 配置说明
### 规则集规则
用于添加能够在 Clash 中即时生效的规则Clash Rule Provider 会根据每条规则的**出站**生成相应的**规则集合** `📂<-` + `出站`
### 置顶规则
这是Clash配置文件的`rules`字段中最顶部的规则相比于其它规则它们拥有更高的优先级。Clash Rule Provider 会自动在此处添加**规则集规则**内的**规则集合**。
### 代理组
代理组中配置项的说明请参考 [Mihomo docs](https://wiki.metacubex.one/config/proxy-groups/)
这里以两个例子说明如何定制代理组:
- 访问北邮人的代理组
北邮人拒绝国内以及所有IPv4连接可以添加一个 `type``url-test` 的代理组,在 `url` 中填写北邮人的地址,打开 `include-all-proxies`,其余配置项保持默认。
然后,在**置顶规则**中添加一条规则: `DOMAIN,xxx.pt,PtProxy`
![](https://images2.imgbox.com/c9/37/FhBGLNQw_o.jpg)
- 访问ChatGPT的代理组
在**高级选项**中启用按大洲分组节点。选择Asia以外的代理组设置`url`: `https://chatgpt.com/` , `expected-status`: `200`
![](https://images2.imgbox.com/e2/37/EoITSfRi_o.jpg)

View File

@@ -17,11 +17,11 @@ from app import schemas
from app.core.config import settings
from app.log import logger
from app.plugins import _PluginBase
from app.schemas.types import EventType, NotificationType
from app.schemas.types import NotificationType
from app.utils.http import RequestUtils
from app.plugins.clashruleprovider.clash_rule_parser import ClashRuleParser
from app.plugins.clashruleprovider.clash_rule_parser import Action, RuleType, ClashRule, MatchRule, LogicRule
from app.plugins.clashruleprovider.clash_rule_parser import ProxyGroupValidator
from app.plugins.clashruleprovider.clash_rule_parser import ProxyGroup, RuleProvider
class ClashRuleProvider(_PluginBase):
@@ -32,7 +32,7 @@ class ClashRuleProvider(_PluginBase):
# 插件图标
plugin_icon = "Mihomo_Meta_A.png"
# 插件版本
plugin_version = "1.0.1"
plugin_version = "1.1.0"
# 插件作者
plugin_author = "wumode"
# 作者主页
@@ -62,19 +62,24 @@ class ClashRuleProvider(_PluginBase):
_retry_times = 3
_filter_keywords = []
_auto_update_subscriptions = True
_ruleset_prefix: str = '📂<-'
_ruleset_prefix: str = '📂<='
_group_by_region: bool = False
_refresh_delay: int = 5
_discard_rules: bool = False
_enable_acl4ssr: bool = False
# 插件数据
_clash_config: Optional[Dict[str, Any]] = None
_top_rules: List[str] = []
_ruleset_rules: List[str] = []
_rule_provider: Dict[str, Any] = {}
_extra_rule_providers: Dict[str, Any] = {}
_subscription_info = {}
_ruleset_names: Dict[str, str] = {}
_proxy_groups = []
_extra_proxies = []
_acl4ssr_providers: Dict[str, Any] = {}
_acl4ssr_prefix: str = '🗂️=>'
# protected variables
_clash_rule_parser = None
@@ -93,7 +98,9 @@ class ClashRuleProvider(_PluginBase):
self._subscription_info = self.get_data("subscription_info") or \
{"download": 0, "upload": 0, "total": 0, "expire": 0, "last_update": 0}
self._rule_provider = self.get_data("rule_provider") or {}
self._extra_rule_providers = self.get_data("extra_rule_providers") or {}
self._ruleset_names = self.get_data("ruleset_names") or {}
self._acl4ssr_providers = self.get_data("acl4ssr_providers") or {}
if config:
self._enabled = config.get("enabled")
self._proxy = config.get("proxy")
@@ -106,12 +113,15 @@ class ClashRuleProvider(_PluginBase):
self._movie_pilot_url = self._movie_pilot_url[:-1]
self._cron = config.get("cron_string")
self._timeout = config.get("timeout")
self._retry_times = config.get("retry_times")
self._retry_times = config.get("retry_times") or 3
self._filter_keywords = config.get("filter_keywords")
self._ruleset_prefix = config.get("ruleset_prefix", "Custom_")
self._ruleset_prefix = config.get("ruleset_prefix", "📂<=")
self._acl4ssr_prefix = config.get("acl4ssr_prefix", "🗂️=>")
self._auto_update_subscriptions = config.get("auto_update_subscriptions")
self._group_by_region = config.get("group_by_region")
self._refresh_delay = config.get("refresh_delay") or 5
self._discard_rules = config.get("discard_rules") or False
self._enable_acl4ssr = config.get("enable_acl4ssr") or False
self._clash_rule_parser = ClashRuleParser()
self._ruleset_rule_parser = ClashRuleParser()
if self._enabled:
@@ -125,7 +135,13 @@ class ClashRuleProvider(_PluginBase):
self._scheduler.start()
# 更新订阅
self._scheduler.add_job(self.__refresh_subscription, "date",
run_date=datetime.now(tz=pytz.timezone(settings.TZ)) + timedelta(seconds=5))
run_date=datetime.now(tz=pytz.timezone(settings.TZ)) + timedelta(seconds=2))
# 更新acl4ssr
if self._enable_acl4ssr:
self._scheduler.add_job(self.__refresh_acl4ssr, "date",
run_date=datetime.now(tz=pytz.timezone(settings.TZ)) + timedelta(seconds=5))
else:
self._acl4ssr_providers = {}
def get_state(self) -> bool:
return self._enabled
@@ -229,8 +245,32 @@ class ClashRuleProvider(_PluginBase):
"endpoint": self.get_rule_providers,
"methods": ["GET"],
"auth": "bear",
"summary": "update rule providers",
"description": "update rule providers"
"summary": "rule providers",
"description": "rule providers"
},
{
"path": "/extra-rule-providers",
"endpoint": self.get_extra_rule_providers,
"methods": ["GET"],
"auth": "bear",
"summary": "extra rule providers",
"description": "extra rule providers"
},
{
"path": "/extra-rule-provider",
"endpoint": self.update_extra_rule_provider,
"methods": ["POST"],
"auth": "bear",
"summary": "update an extra rule provider",
"description": "update an rule provider"
},
{
"path": "/extra-rule-provider",
"endpoint": self.delete_extra_rule_provider,
"methods": ["DELETE"],
"auth": "bear",
"summary": "add an extra rule provider",
"description": "add an rule provider"
},
{
"path": "/extra-proxies",
@@ -245,8 +285,8 @@ class ClashRuleProvider(_PluginBase):
"endpoint": self.delete_extra_proxy,
"methods": ["DELETE"],
"auth": "bear",
"summary": "delete a extra proxy",
"description": "delete a extra proxy"
"summary": "delete an extra proxy",
"description": "delete an extra proxy"
},
{
"path": "/extra-proxies",
@@ -333,31 +373,15 @@ class ClashRuleProvider(_PluginBase):
"id": "ClashRuleProvider",
"name": "定时更新订阅",
"trigger": CronTrigger.from_crontab(self._cron),
"func": self.update_subscription_service,
"func": self.refresh_subscription_service,
"kwargs": {}
}]
return []
def __update_config(self):
# 保存配置
self.update_config(
{
"enabled": self._enabled,
"cron": self._cron,
"proxy": self._proxy,
"notify": self._notify,
"sub_links": self._sub_links,
"clash_dashboard_url": self._clash_dashboard_url,
"clash_dashboard_secret": self._clash_dashboard_secret,
"movie_pilot_url": self._movie_pilot_url,
"retry_times": self._retry_times,
"timeout": self._timeout,
})
def __save_data(self):
self.__insert_ruleset()
self._top_rules = self._clash_rule_parser.to_string()
self._ruleset_rules = self._ruleset_rule_parser.to_string()
self._top_rules = self._clash_rule_parser.to_list()
self._ruleset_rules = self._ruleset_rule_parser.to_list()
self.save_data('clash_config', self._clash_config)
self.save_data('ruleset_rules', self._ruleset_rules)
self.save_data('top_rules', self._top_rules)
@@ -366,6 +390,8 @@ class ClashRuleProvider(_PluginBase):
self.save_data('rule_provider', self._rule_provider)
self.save_data('proxy_groups', self._proxy_groups)
self.save_data('extra_proxies', self._extra_proxies)
self.save_data('extra_rule_providers', self._extra_rule_providers)
self.save_data('acl4ssr_providers', self._acl4ssr_providers)
def __parse_config(self):
if self._top_rules is None:
@@ -457,9 +483,9 @@ class ClashRuleProvider(_PluginBase):
self.append_top_rules(rules)
return schemas.Response(success=True)
def reorder_rules(self, params: Dict[str, Any]):
def reorder_rules(self, params: Dict[str, Any]) -> schemas.Response:
if not self._enabled:
return {"success": False, "message": ""}
return schemas.Response(success=False, message='')
moved_priority = params.get('moved_priority')
target_priority = params.get('target_priority')
try:
@@ -469,39 +495,41 @@ class ClashRuleProvider(_PluginBase):
else:
self.__reorder_rules(self._clash_rule_parser, moved_priority, target_priority)
except Exception as e:
return {"success": False, "message": str(e)}
return {"success": True, "message": None}
return schemas.Response(success=False, message=str(e))
return schemas.Response(success=True)
def update_rules(self, params: Dict[str, Any]):
def update_rules(self, params: Dict[str, Any]) -> schemas.Response:
if not self._enabled:
return {"success": False, "message": ""}
return schemas.Response(success=False, message='')
if params.get('type') == 'ruleset':
self.__update_rules(params.get('rules'), self._ruleset_rule_parser)
else:
self.__update_rules(params.get('rules'), self._clash_rule_parser)
return {"success": True, "message": None}
return schemas.Response(success=True)
def update_rule(self, params: Dict[str, Any]) -> Dict[str, Any]:
if not self._enabled:
return {"success": False, "message": ""}
if params.get('type') == 'ruleset':
res = self.update_rule_by_priority(params.get('rule_data'), self._ruleset_rule_parser)
res = self.update_rule_by_priority(params.get('rule_data'),
params.get('priority'),
self._ruleset_rule_parser)
if res:
self.__add_notification_job(f"{self._ruleset_prefix}{params.get('rule_data').get('action')}")
else:
res = self.update_rule_by_priority(params.get('rule_data'), self._clash_rule_parser)
res = self.update_rule_by_priority(params.get('rule_data'), params.get('priority'), self._clash_rule_parser)
return {"success": bool(res), "message": None}
def add_rule(self, params: Dict[str, Any]) -> Dict[str, Any]:
def add_rule(self, params: Dict[str, Any]) -> schemas.Response:
if not self._enabled:
return {"success": False, "message": ""}
return schemas.Response(success=False, message='')
if params.get('type') == 'ruleset':
res = self.add_rule_by_priority(params.get('rule_data'), self._ruleset_rule_parser)
if res:
self.__add_notification_job(f"{self._ruleset_prefix}{params.get('rule_data').get('action')}")
else:
res = self.add_rule_by_priority(params.get('rule_data'), self._clash_rule_parser)
return {"success": bool(res), "message": None}
return schemas.Response(success=bool(res), message='')
def get_subscription(self) -> schemas.Response:
if not self._sub_links:
@@ -517,11 +545,61 @@ class ClashRuleProvider(_PluginBase):
res = self.__refresh_subscription()
if not res:
return schemas.Response(success=False, message=f"订阅链接 {self._sub_links[0]} 更新失败")
return schemas.Response(success=True, message='订阅更新成功')
return schemas.Response(success=True, message='订阅更新成功')
def get_rule_providers(self) -> schemas.Response:
return schemas.Response(success=True, data=self.rule_providers())
def get_extra_rule_providers(self) -> schemas.Response:
extra_rule_providers = []
for name, value in self._extra_rule_providers.items():
item = {'name': name}
item.update(value)
extra_rule_providers.append(item)
return schemas.Response(success=True, data={'rule_providers': extra_rule_providers})
def update_extra_rule_provider(self, params: Dict[str, Any]) -> schemas.Response:
if not self._enabled:
return schemas.Response(success=False, message='')
name = params.get('name')
new_value = params.get('value')
new_name = new_value.get('name')
if not name or not new_name:
return schemas.Response(success=False, message="Missing param: name")
item = {}
for key, value in new_value.items():
if key == 'name' or value is None:
continue
if value == '' or value is None:
continue
item[key] = value
try:
rule_provider = RuleProvider.parse_obj(item)
if rule_provider.type == 'inline' and rule_provider.behavior == 'classical':
for rule in rule_provider.payload:
clash_rule = ClashRuleParser.parse_rule_line(f"{rule},DIRECT")
if not clash_rule:
raise ValueError(f"Invalid clash_rule: {rule}")
except Exception as e:
error_message = f"Failed to parse rule provider: Invalid data={item}, error={repr(e)}"
logger.error(error_message)
return schemas.Response(success=False, message=str(error_message))
if name != new_name:
self._extra_rule_providers.pop(name, None)
self._extra_rule_providers[new_name] = item
self.save_data('extra_rule_providers', self._extra_rule_providers)
return schemas.Response(success=True)
def delete_extra_rule_provider(self, params: Dict[str, Any]) -> schemas.Response:
if not self._enabled:
return schemas.Response(success=False, message='')
name = params.get('name')
if not name:
return schemas.Response(success=False, message="Missing param: name")
self._extra_rule_providers.pop(name, None)
self.save_data('extra_rule_providers', self._extra_rule_providers)
return schemas.Response(success=True)
def get_proxy_groups(self) -> schemas.Response:
return schemas.Response(success=True, data={'proxy_groups': self._proxy_groups})
@@ -569,14 +647,14 @@ class ClashRuleProvider(_PluginBase):
if not item.get('name') or any(x.get('name') == item.get('name') for x in self._proxy_groups):
return schemas.Response(success=False, message=f"The proxy group name {item.get('name')} already exists")
try:
ProxyGroupValidator.parse_obj(item)
ProxyGroup.parse_obj(item)
except Exception as e:
error_message = f"Failed to parse proxy group: Invalid data={item}, error={repr(e)}"
logger.error(error_message)
return schemas.Response(success=False, message=str(error_message))
new_item = {}
for k, v in item.items():
if type(v) is str and len(v) == 0:
if v == '':
continue
if v is None:
continue
@@ -608,9 +686,11 @@ class ClashRuleProvider(_PluginBase):
if not self._clash_config:
return None
rule_providers = {}
for key, value in self._clash_config.get("rule-providers", {}):
if value.get("path", '').startwith("./CRP/"):
continue
for key, value in self._clash_config.get('rule-providers', {}).items():
rule_providers[key] = value
for key, value in self._extra_rule_providers.items():
rule_providers[key] = value
for key, value in self._acl4ssr_providers.items():
rule_providers[key] = value
return rule_providers
@@ -618,7 +698,7 @@ class ClashRuleProvider(_PluginBase):
rule_parser.rules = []
for rule in rules:
clash_rule = ClashRuleParser.parse_rule_dict(rule)
rule_parser.insert_rule_at_priority(clash_rule, rule.get("priority"))
rule_parser.insert_rule_at_priority(clash_rule, rule.get('priority'))
self.__save_data()
def __reorder_rules(self, rule_parser: ClashRuleParser, moved_priority, target_priority):
@@ -665,13 +745,14 @@ class ClashRuleProvider(_PluginBase):
self.__save_data()
return
def update_rule_by_priority(self, rule: Dict[str, Any], rule_parser: ClashRuleParser) -> bool:
if not isinstance(rule.get("priority"), int):
def update_rule_by_priority(self, rule: Dict[str, Any], priority: int, rule_parser: ClashRuleParser) -> bool:
if type(rule.get("priority")) is not int or type(priority) is not int:
return False
clash_rule = ClashRuleParser.parse_rule_dict(rule)
if not clash_rule:
logger.error(f"Failed to update rule at priority {priority}. Invalid clash rule: {rule}")
return False
res = rule_parser.update_rule_at_priority(clash_rule, rule.get("priority"))
res = rule_parser.update_rule_at_priority(clash_rule, priority)
self.__save_data()
return res
@@ -712,7 +793,7 @@ class ClashRuleProvider(_PluginBase):
days = seconds_left // 86400
return f"{days}天后过期" if days > 0 else "已过期"
def update_subscription_service(self):
def refresh_subscription_service(self):
res = self.__refresh_subscription()
if res:
used = self._subscription_info['download'] + self._subscription_info['upload']
@@ -730,19 +811,51 @@ class ClashRuleProvider(_PluginBase):
text=f"{message}"
)
def __refresh_acl4ssr(self):
logger.info(f"Refreshing ACL4SSR")
# 配置参数
owner = 'ACL4SSR'
repo = 'ACL4SSR'
paths = ['Clash/Providers', 'Clash/Providers/Ruleset']
api_url = f"https://api.github.com/repos/{owner}/{repo}/contents/%s"
branch = 'master'
for path in paths:
response = RequestUtils().get_res(api_url % path, headers=settings.GITHUB_HEADERS, params={'ref': branch})
if not response:
return
files = response.json()
yaml_files = [f for f in files if f["type"] == "file" and f["name"].endswith((".yaml", ".yml"))]
self._acl4ssr_providers = {}
for f in yaml_files:
name = f"{self._acl4ssr_prefix}{f['name'][:f['name'].rfind('.')]}"
path = f"./ACL4SSR/{f['name']}"
provider = {'type': 'http', 'path': path, 'url': f["download_url"], 'interval': 600,
'behavior': 'classical', 'format': 'yaml', 'size-limit': 0}
if name not in self._acl4ssr_providers:
self._acl4ssr_providers[name] = provider
self.save_data('acl4ssr_providers', self._acl4ssr_providers)
def __refresh_subscription(self) -> bool:
if not self._sub_links:
logger.error(f"Invalid links: {self._sub_links}")
return False
url = self._sub_links[0]
logger.info(f"Refreshing: {url}")
ret = RequestUtils(accept_type="text/html",
proxies=settings.PROXY if self._proxy else None
).get_res(url)
ret = None
for i in range(0, self._retry_times):
ret = RequestUtils(accept_type="text/html",
proxies=settings.PROXY if self._proxy else None
).get_res(url)
if ret:
break
if not ret:
return False
try:
rs = yaml.load(ret.content, Loader=yaml.FullLoader)
if rs.get('rules') is None:
rs['rules'] = []
if self._discard_rules:
rs['rules'] = []
self._clash_config = self.__remove_nodes_by_keywords(rs)
except Exception as e:
logger.error(f"解析配置出错: {e}")
@@ -837,16 +950,18 @@ class ClashRuleProvider(_PluginBase):
removed_proxies = []
for proxy_group in clash_config.get("proxy-groups", []):
proxy_group['proxies'] = [x for x in proxy_group.get('proxies') if x not in removed_proxies]
# clash_config["proxy-groups"] = [x for x in clash_config.get("proxy-groups", []) if x.get("proxies")]
return clash_config
def clash_config(self) -> Optional[Dict[str, Any]]:
"""
整理clash配置返回配置字典
"""
if not self._clash_config:
return None
self.__insert_ruleset()
self._top_rules = self._clash_rule_parser.to_string()
self._top_rules = self._clash_rule_parser.to_list()
clash_config = copy.deepcopy(self._clash_config)
clash_config['rule-providers'] = clash_config.get('rule-providers') or {}
# 添加代理组
proxy_groups = copy.deepcopy(self._proxy_groups)
if proxy_groups:
@@ -864,41 +979,56 @@ class ClashRuleProvider(_PluginBase):
# 添加按大洲代理组
if self._group_by_region:
if self._proxy_groups_by_region:
if clash_config.get("proxy-groups"):
if clash_config.get('proxy-groups'):
clash_config['proxy-groups'].extend(self._proxy_groups_by_region)
else:
clash_config['proxy-groups'] = copy.deepcopy(self._proxy_groups_by_region)
top_rules = []
for rule in self._clash_rule_parser.rules:
if (not isinstance(rule.action, Action) and
not len([x for x in self.clash_outbound(clash_config) if rule.action == x.get("name", '')])):
logger.warn(f"出站 {rule.action} 不存在, 跳过 {rule.raw_rule}")
continue
top_rules.append(rule.raw_rule)
clash_config["rules"] = self._top_rules + clash_config.get("rules", [])
outbound_names = list(x.get("name") for x in self.clash_outbound(clash_config))
# 添加 extra rule providers
if self._extra_rule_providers:
clash_config['rule-providers'].update(self._extra_rule_providers)
# 通过 ruleset rules 添加 rule-providers
self._rule_provider = {}
for r in self._clash_rule_parser.rules:
if r.rule_type == RuleType.RULE_SET and r.payload.startswith(self._ruleset_prefix):
action_str = f"{r.action.value}" if isinstance(r.action, Action) else r.action
for rule in self._ruleset_rule_parser.rules:
action_str = f"{rule.action.value}" if isinstance(rule.action, Action) else rule.action
rule_provider_name = f'{self._ruleset_prefix}{action_str}'
if rule_provider_name not in self._rule_provider:
path_name = hashlib.sha256(action_str.encode('utf-8')).hexdigest()[:10]
self._ruleset_names[path_name] = r.payload
self._ruleset_names[path_name] = rule.payload
sub_url = (f"{self._movie_pilot_url}/api/v1/plugin/ClashRuleProvider/ruleset?"
f"name={path_name}&apikey={settings.API_TOKEN}")
self._rule_provider[r.payload] = {"behavior": "classical",
"format": "yaml",
"interval": 3600,
"path": f"./CRP/{path_name}.yaml",
"type": "http",
"url": sub_url}
if clash_config.get("rule-providers"):
self._rule_provider[rule_provider_name] = {"behavior": "classical",
"format": "yaml",
"interval": 3600,
"path": f"./CRP/{path_name}.yaml",
"type": "http",
"url": sub_url}
clash_config['rule-providers'].update(self._rule_provider)
# 添加规则
for rule in self._clash_rule_parser.rules:
if not isinstance(rule.action, Action) and rule.action not in outbound_names:
logger.warn(f"出站 {rule.action} 不存在, 跳过 {rule.raw_rule}")
continue
if rule.rule_type == RuleType.RULE_SET:
# 添加ACL4SSR Rules
if rule.payload in self._acl4ssr_providers:
clash_config['rule-providers'][rule.payload] = self._acl4ssr_providers[rule.payload]
if rule.payload not in clash_config.get('rule-providers', {}):
logger.warn(f"规则集合 {rule.payload} 不存在, 跳过 {rule.raw_rule}")
continue
top_rules.append(rule.raw_rule)
clash_config["rules"] = self._top_rules + clash_config.get("rules", [])
if self._rule_provider:
clash_config['rule-providers'] = clash_config.get('rule-providers') or {}
clash_config['rule-providers'].update(self._rule_provider)
else:
clash_config['rule-providers'] = self._rule_provider
key_to_delete = []
for key, item in self._ruleset_names.items():
if item not in clash_config['rule-providers']:
if item not in clash_config.get('rule-providers', {}):
key_to_delete.append(key)
for key in key_to_delete:
del self._ruleset_names[key]

View File

@@ -3,9 +3,58 @@ from typing import List, Dict, Any, Optional, Union, Callable, Literal
from dataclasses import dataclass
from enum import Enum
from pydantic import BaseModel, Field, validator
from pydantic import BaseModel, Field, validator, HttpUrl
class RuleProvider(BaseModel):
type: Literal["http", "file", "inline"] = Field(..., description="Provider type")
url: Optional[HttpUrl] = Field(None, description="Must be configured if the type is http")
path: Optional[str] = Field(None, description="Optional, file path, must be unique.")
interval: Optional[int] = Field(None, ge=0, description="The update interval for the provider, in seconds.")
proxy: Optional[str] = Field(None, description="Download/update through the specified proxy.")
behavior: Optional[Literal["domain", "ipcidr", "classical"]] = Field(None,
description="Behavior of the rule provider")
format: Literal["yaml", "text", "mrs"] = Field("yaml", description="Format of the rule provider file")
size_limit: int = Field(0, ge=0, description="The maximum size of downloadable files in bytes (0 for no limit)")
payload: Optional[List[str]] = Field(None, description="Content, only effective when type is inline")
@validator("url", pre=True, always=True)
def check_url_for_http_type(cls, v, values):
if values.get("type") == "http" and v is None:
raise ValueError("url must be configured if the type is 'http'")
return v
@validator("path", pre=True, always=True)
def check_path_for_file_type(cls, v, values):
if values.get("type") == "file" and v is None:
raise ValueError("path must be configured if the type is 'file'")
return v
@validator("payload", pre=True, always=True)
def handle_payload_for_non_inline_type(cls, v, values):
# If type is not inline, payload should be ignored (set to None)
if values.get("type") != "inline" and v is not None:
return None
return v
@validator("payload")
def check_payload_type_for_inline(cls, v, values):
if values.get("type") == "inline" and v is not None and not isinstance(v, list):
raise ValueError("payload must be a list of strings when type is 'inline'")
if values.get("type") == "inline" and v is None:
raise ValueError("payload must be configured if the type is 'inline'")
return v
@validator("format")
def check_format_with_behavior(cls, v, values):
behavior = values.get("behavior")
if v == "mrs" and behavior not in ["domain", "ipcidr"]:
raise ValueError("mrs format only supports 'domain' or 'ipcidr' behavior")
return v
class RuleProviders(BaseModel):
__root__: dict[str, RuleProvider]
class ProxyGroupBase(BaseModel):
"""
包含所有代理组类型共有的通用字段。
@@ -85,11 +134,7 @@ class LoadBalanceGroup(ProxyGroupBase):
# --- Discriminated Union ---
ProxyGroupUnion = Union[SelectGroup, RelayGroup, FallbackGroup, UrlTestGroup, LoadBalanceGroup]
class ProxyGroupValidator(BaseModel):
"""
这是Pydantic V1的验证器。
它使用 __root__ 字段来处理可辨识联合。
"""
class ProxyGroup(BaseModel):
__root__: ProxyGroupUnion
class RuleType(Enum):
@@ -212,7 +257,6 @@ class ClashRuleParser:
return ClashRuleParser._parse_regular_rule(line)
except Exception as e:
print(f"Error parsing rule '{line}': {e}")
return None
@staticmethod
@@ -228,13 +272,16 @@ class ClashRuleParser:
conditions_str += f'({condition.get("type")},{condition.get("payload")})'
conditions_str = f"({conditions_str})"
raw_rule = f"{clash_rule.get('type')},{conditions_str},{clash_rule.get('action')}"
return ClashRuleParser._parse_logic_rule(raw_rule)
rule = ClashRuleParser._parse_logic_rule(raw_rule)
elif clash_rule.get("type") == 'MATCH':
raw_rule = f"{clash_rule.get('type')},{clash_rule.get('action')}"
return ClashRuleParser._parse_match_rule(raw_rule)
rule = ClashRuleParser._parse_match_rule(raw_rule)
else:
raw_rule = f"{clash_rule.get('type')},{clash_rule.get('payload')},{clash_rule.get('action')}"
return ClashRuleParser._parse_regular_rule(raw_rule)
rule = ClashRuleParser._parse_regular_rule(raw_rule)
if rule and 'priority' in clash_rule:
rule.priority = clash_rule['priority']
return rule
@staticmethod
def _parse_match_rule(line: str) -> MatchRule:
@@ -339,7 +386,7 @@ class ClashRuleParser:
)
conditions.append(condition)
except ValueError:
print(f"Unknown rule type in logic condition: {rule_type_str}")
continue
return conditions
@@ -396,7 +443,7 @@ class ClashRuleParser:
except Exception:
return False
def to_string(self) -> List[str]:
def to_list(self) -> List[str]:
result = []
for rule in self.rules:
result.append(rule.raw_rule)
@@ -478,12 +525,19 @@ class ClashRuleParser:
self.rules.sort(key=lambda r: r.priority)
def update_rule_at_priority(self, clash_rule: Union[ClashRule, LogicRule], priority: int) -> bool:
for index, existing_rule in enumerate(self.rules):
if existing_rule.priority == priority:
self.rules[index] = clash_rule
self.rules[index].priority = priority
return True
return False
if clash_rule.priority == priority:
for index, existing_rule in enumerate(self.rules):
if existing_rule.priority == priority:
self.rules[index] = clash_rule
self.rules[index].priority = priority
return True
return False
else:
removed = self.remove_rule_at_priority(priority)
if not removed:
return False
self.insert_rule_at_priority(clash_rule, clash_rule.priority)
return True
def remove_rule_at_priority(self, priority: int) -> Optional[Union[ClashRule, LogicRule, MatchRule]]:
"""Remove rule at specific priority and adjust remaining priorities"""

View File

@@ -1,5 +1,5 @@
.plugin-config[data-v-52f6e9ed] {
.plugin-config[data-v-c50374dc] {
max-width: 800px;
margin: 0 auto;
}

View File

@@ -49,18 +49,6 @@ const testResult = reactive({
message: ''
});
// Cron 选项
const cronOptions = [
{text: '每5分钟', value: '5min', cron: '*/5 * * * *'},
{text: '每15分钟', value: '15min', cron: '*/15 * * * *'},
{text: '每30分钟', value: '30min', cron: '*/30 * * * *'},
{text: '每小时', value: '1hour', cron: '0 * * * *'},
{text: '每2小时', value: '2hours', cron: '0 */2 * * *'},
{text: '每6小时', value: '6hours', cron: '0 */6 * * *'},
{text: '每12小时', value: '12hours', cron: '0 */12 * * *'},
{text: '每天', value: '1day', cron: '0 0 * * *'},
{text: '自定义', value: 'custom', cron: ''},
];
// 默认配置
const defaultConfig = {
@@ -76,9 +64,12 @@ const defaultConfig = {
proxy: false,
notify: false,
auto_update_subscriptions: true,
ruleset_prefix: '📂<-',
ruleset_prefix: '📂<=',
acl4ssr_prefix: '🗂️=>',
group_by_region: false,
refresh_delay: 5,
discard_rules: false,
enable_acl4ssr: false,
};
// 响应式配置对象
@@ -95,14 +86,6 @@ onMounted(() => {
config[key] = props.initialConfig[key];
}
});
// 设置对应的cron选项
const cronOption = cronOptions.find(option => option.cron === config.cron_string);
if (cronOption) {
selectedCronOption.value = cronOption.value;
} else {
selectedCronOption.value = 'custom';
}
}
});
@@ -129,25 +112,6 @@ function validateSubLinks(links) {
return true
}
function validateCronExpression(cronStr) {
if (!cronStr) return '请输入Cron表达式'
// 简单的cron表达式验证
const parts = cronStr.trim().split(/\s+/);
if (parts.length !== 5) {
return 'Cron表达式应包含5个部分 (分 时 日 月 周)'
}
return true
}
// 更新cron字符串
function updateCronString(optionValue) {
const option = cronOptions.find(opt => opt.value === optionValue);
if (option && option.cron) {
config.cron_string = option.cron;
}
}
// 测试连接
async function testConnection() {
testing.value = true;
@@ -268,7 +232,7 @@ return (_ctx, _cache) => {
const _component_v_chip = _resolveComponent("v-chip");
const _component_v_combobox = _resolveComponent("v-combobox");
const _component_v_text_field = _resolveComponent("v-text-field");
const _component_v_select = _resolveComponent("v-select");
const _component_v_cron_field = _resolveComponent("v-cron-field");
const _component_v_expansion_panel_title = _resolveComponent("v-expansion-panel-title");
const _component_v_expansion_panel_text = _resolveComponent("v-expansion-panel-text");
const _component_v_expansion_panel = _resolveComponent("v-expansion-panel");
@@ -292,7 +256,7 @@ return (_ctx, _cache) => {
}, {
default: _withCtx(() => [
_createVNode(_component_v_icon, { left: "" }, {
default: _withCtx(() => _cache[20] || (_cache[20] = [
default: _withCtx(() => _cache[22] || (_cache[22] = [
_createTextVNode("mdi-close")
])),
_: 1
@@ -303,7 +267,7 @@ return (_ctx, _cache) => {
]),
default: _withCtx(() => [
_createVNode(_component_v_card_title, null, {
default: _withCtx(() => _cache[19] || (_cache[19] = [
default: _withCtx(() => _cache[21] || (_cache[21] = [
_createTextVNode("Clash Rule Provider 插件配置")
])),
_: 1
@@ -329,16 +293,16 @@ return (_ctx, _cache) => {
ref_key: "form",
ref: form,
modelValue: isFormValid.value,
"onUpdate:modelValue": _cache[17] || (_cache[17] = $event => ((isFormValid).value = $event)),
"onUpdate:modelValue": _cache[19] || (_cache[19] = $event => ((isFormValid).value = $event)),
onSubmit: _withModifiers(saveConfig, ["prevent"])
}, {
default: _withCtx(() => [
_cache[31] || (_cache[31] = _createElementVNode("div", { class: "text-subtitle-1 font-weight-bold mt-4 mb-2" }, "基本设置", -1)),
_cache[33] || (_cache[33] = _createElementVNode("div", { class: "text-subtitle-1 font-weight-bold mt-4 mb-2" }, "基本设置", -1)),
_createVNode(_component_v_row, null, {
default: _withCtx(() => [
_createVNode(_component_v_col, {
cols: "12",
md: "4"
md: "3"
}, {
default: _withCtx(() => [
_createVNode(_component_v_switch, {
@@ -347,15 +311,14 @@ return (_ctx, _cache) => {
label: "启用插件",
color: "primary",
inset: "",
hint: "启用后插件将开始监控和同步",
"persistent-hint": ""
hint: "启用后插件将开始监控和同步"
}, null, 8, ["modelValue"])
]),
_: 1
}),
_createVNode(_component_v_col, {
cols: "12",
md: "4"
md: "3"
}, {
default: _withCtx(() => [
_createVNode(_component_v_switch, {
@@ -364,15 +327,14 @@ return (_ctx, _cache) => {
label: "启用代理",
color: "primary",
inset: "",
hint: "是否使用系统代理进行网络请求",
"persistent-hint": ""
hint: "是否使用系统代理进行网络请求"
}, null, 8, ["modelValue"])
]),
_: 1
}),
_createVNode(_component_v_col, {
cols: "12",
md: "4"
md: "3"
}, {
default: _withCtx(() => [
_createVNode(_component_v_switch, {
@@ -381,8 +343,23 @@ return (_ctx, _cache) => {
label: "启用通知",
color: "primary",
inset: "",
hint: "执行完成后发送通知消息",
"persistent-hint": ""
hint: "执行完成后发送通知消息"
}, null, 8, ["modelValue"])
]),
_: 1
}),
_createVNode(_component_v_col, {
cols: "12",
md: "3"
}, {
default: _withCtx(() => [
_createVNode(_component_v_switch, {
modelValue: config.auto_update_subscriptions,
"onUpdate:modelValue": _cache[3] || (_cache[3] = $event => ((config.auto_update_subscriptions) = $event)),
label: "自动更新订阅",
color: "primary",
inset: "",
hint: "定期自动更新Clash订阅配置"
}, null, 8, ["modelValue"])
]),
_: 1
@@ -390,21 +367,20 @@ return (_ctx, _cache) => {
]),
_: 1
}),
_cache[32] || (_cache[32] = _createElementVNode("div", { class: "text-subtitle-1 font-weight-bold mt-4 mb-2" }, "订阅配置", -1)),
_cache[34] || (_cache[34] = _createElementVNode("div", { class: "text-subtitle-1 font-weight-bold mt-4 mb-2" }, "订阅配置", -1)),
_createVNode(_component_v_row, null, {
default: _withCtx(() => [
_createVNode(_component_v_col, { cols: "12" }, {
default: _withCtx(() => [
_createVNode(_component_v_combobox, {
modelValue: config.sub_links,
"onUpdate:modelValue": _cache[3] || (_cache[3] = $event => ((config.sub_links) = $event)),
"onUpdate:modelValue": _cache[4] || (_cache[4] = $event => ((config.sub_links) = $event)),
label: "订阅链接",
variant: "outlined",
multiple: "",
chips: "",
"closable-chips": "",
hint: "添加一个Clash订阅链接, 按回车确认输入",
"persistent-hint": "",
rules: [validateSubLinks]
}, {
chip: _withCtx(({ props, item }) => [
@@ -427,14 +403,13 @@ return (_ctx, _cache) => {
default: _withCtx(() => [
_createVNode(_component_v_combobox, {
modelValue: config.filter_keywords,
"onUpdate:modelValue": _cache[4] || (_cache[4] = $event => ((config.filter_keywords) = $event)),
"onUpdate:modelValue": _cache[5] || (_cache[5] = $event => ((config.filter_keywords) = $event)),
label: "节点过滤关键词",
variant: "outlined",
multiple: "",
chips: "",
"closable-chips": "",
hint: "添加用于过滤节点的关键词",
"persistent-hint": ""
hint: "添加用于过滤节点的关键词"
}, {
chip: _withCtx(({ props, item }) => [
_createVNode(_component_v_chip, _mergeProps(props, {
@@ -456,24 +431,23 @@ return (_ctx, _cache) => {
]),
_: 1
}),
_cache[33] || (_cache[33] = _createElementVNode("div", { class: "text-subtitle-1 font-weight-bold mt-4 mb-2" }, "Clash 面板设置", -1)),
_cache[35] || (_cache[35] = _createElementVNode("div", { class: "text-subtitle-1 font-weight-bold mt-4 mb-2" }, "Clash 面板设置", -1)),
_createVNode(_component_v_row, null, {
default: _withCtx(() => [
_createVNode(_component_v_col, { cols: "12" }, {
default: _withCtx(() => [
_createVNode(_component_v_text_field, {
modelValue: config.clash_dashboard_url,
"onUpdate:modelValue": _cache[5] || (_cache[5] = $event => ((config.clash_dashboard_url) = $event)),
"onUpdate:modelValue": _cache[6] || (_cache[6] = $event => ((config.clash_dashboard_url) = $event)),
label: "Clash 面板 URL",
variant: "outlined",
placeholder: "http://localhost:9090",
hint: "Clash 控制面板的访问地址",
"persistent-hint": "",
rules: [v => !v || isValidUrl(v) || '请输入有效的URL地址']
}, {
"prepend-inner": _withCtx(() => [
_createVNode(_component_v_icon, { color: "primary" }, {
default: _withCtx(() => _cache[21] || (_cache[21] = [
default: _withCtx(() => _cache[23] || (_cache[23] = [
_createTextVNode("mdi-web")
])),
_: 1
@@ -488,19 +462,18 @@ return (_ctx, _cache) => {
default: _withCtx(() => [
_createVNode(_component_v_text_field, {
modelValue: config.clash_dashboard_secret,
"onUpdate:modelValue": _cache[6] || (_cache[6] = $event => ((config.clash_dashboard_secret) = $event)),
"onUpdate:modelValue": _cache[7] || (_cache[7] = $event => ((config.clash_dashboard_secret) = $event)),
label: "Clash 面板密钥",
variant: "outlined",
placeholder: "your-clash-secret",
hint: "用于访问Clash API的密钥",
"persistent-hint": "",
"append-inner-icon": showClashSecret.value ? 'mdi-eye-off' : 'mdi-eye',
type: showClashSecret.value ? 'text' : 'password',
"onClick:appendInner": _cache[7] || (_cache[7] = $event => (showClashSecret.value = !showClashSecret.value))
"onClick:appendInner": _cache[8] || (_cache[8] = $event => (showClashSecret.value = !showClashSecret.value))
}, {
"prepend-inner": _withCtx(() => [
_createVNode(_component_v_icon, { color: "warning" }, {
default: _withCtx(() => _cache[22] || (_cache[22] = [
default: _withCtx(() => _cache[24] || (_cache[24] = [
_createTextVNode("mdi-key")
])),
_: 1
@@ -514,24 +487,23 @@ return (_ctx, _cache) => {
]),
_: 1
}),
_cache[34] || (_cache[34] = _createElementVNode("div", { class: "text-subtitle-1 font-weight-bold mt-4 mb-2" }, "MoviePilot 设置", -1)),
_cache[36] || (_cache[36] = _createElementVNode("div", { class: "text-subtitle-1 font-weight-bold mt-4 mb-2" }, "MoviePilot 设置", -1)),
_createVNode(_component_v_row, null, {
default: _withCtx(() => [
_createVNode(_component_v_col, { cols: "12" }, {
default: _withCtx(() => [
_createVNode(_component_v_text_field, {
modelValue: config.movie_pilot_url,
"onUpdate:modelValue": _cache[8] || (_cache[8] = $event => ((config.movie_pilot_url) = $event)),
"onUpdate:modelValue": _cache[9] || (_cache[9] = $event => ((config.movie_pilot_url) = $event)),
label: "MoviePilot URL",
variant: "outlined",
placeholder: "http://localhost:3001",
hint: "MoviePilot 服务的访问地址",
"persistent-hint": "",
rules: [v => !!v || 'MoviePilot URL不能为空', v => isValidUrl(v) || '请输入有效的URL地址']
}, {
"prepend-inner": _withCtx(() => [
_createVNode(_component_v_icon, { color: "success" }, {
default: _withCtx(() => _cache[23] || (_cache[23] = [
default: _withCtx(() => _cache[25] || (_cache[25] = [
_createTextVNode("mdi-movie")
])),
_: 1
@@ -545,58 +517,21 @@ return (_ctx, _cache) => {
]),
_: 1
}),
_cache[35] || (_cache[35] = _createElementVNode("div", { class: "text-subtitle-1 font-weight-bold mt-4 mb-2" }, "执行设置", -1)),
_cache[37] || (_cache[37] = _createElementVNode("div", { class: "text-subtitle-1 font-weight-bold mt-4 mb-2" }, "执行设置", -1)),
_createVNode(_component_v_row, null, {
default: _withCtx(() => [
_createVNode(_component_v_col, { cols: "12" }, {
default: _withCtx(() => [
_createVNode(_component_v_select, {
modelValue: selectedCronOption.value,
"onUpdate:modelValue": [
_cache[9] || (_cache[9] = $event => ((selectedCronOption).value = $event)),
updateCronString
],
_createVNode(_component_v_cron_field, {
modelValue: config.cron_string,
"onUpdate:modelValue": _cache[10] || (_cache[10] = $event => ((config.cron_string) = $event)),
label: "执行周期",
items: cronOptions,
variant: "outlined",
"item-title": "text",
"item-value": "value",
hint: "选择插件执行的时间间隔",
"persistent-hint": ""
placeholder: "0 4 * * *",
hint: "使用标准Cron表达式格式 (分 时 日 月 周)"
}, null, 8, ["modelValue"])
]),
_: 1
}),
(selectedCronOption.value === 'custom')
? (_openBlock(), _createBlock(_component_v_col, {
key: 0,
cols: "12"
}, {
default: _withCtx(() => [
_createVNode(_component_v_text_field, {
modelValue: config.cron_string,
"onUpdate:modelValue": _cache[10] || (_cache[10] = $event => ((config.cron_string) = $event)),
label: "自定义 Cron 表达式",
variant: "outlined",
placeholder: "0 */6 * * *",
hint: "使用标准Cron表达式格式 (分 时 日 月 周)",
"persistent-hint": "",
rules: [validateCronExpression]
}, {
"prepend-inner": _withCtx(() => [
_createVNode(_component_v_icon, { color: "info" }, {
default: _withCtx(() => _cache[24] || (_cache[24] = [
_createTextVNode("mdi-clock-outline")
])),
_: 1
})
]),
_: 1
}, 8, ["modelValue", "rules"])
]),
_: 1
}))
: _createCommentVNode("", true),
_createVNode(_component_v_col, {
cols: "12",
md: "6"
@@ -612,12 +547,11 @@ return (_ctx, _cache) => {
min: "1",
max: "300",
hint: "请求的超时时间",
"persistent-hint": "",
rules: [v => v > 0 || '超时时间必须大于0']
}, {
"prepend-inner": _withCtx(() => [
_createVNode(_component_v_icon, { color: "warning" }, {
default: _withCtx(() => _cache[25] || (_cache[25] = [
default: _withCtx(() => _cache[26] || (_cache[26] = [
_createTextVNode("mdi-timer")
])),
_: 1
@@ -643,12 +577,11 @@ return (_ctx, _cache) => {
min: "0",
max: "10",
hint: "失败时的重试次数",
"persistent-hint": "",
rules: [v => v >= 0 || '重试次数不能为负数']
}, {
"prepend-inner": _withCtx(() => [
_createVNode(_component_v_icon, { color: "info" }, {
default: _withCtx(() => _cache[26] || (_cache[26] = [
default: _withCtx(() => _cache[27] || (_cache[27] = [
_createTextVNode("mdi-refresh")
])),
_: 1
@@ -672,12 +605,12 @@ return (_ctx, _cache) => {
_createVNode(_component_v_expansion_panel_title, null, {
default: _withCtx(() => [
_createVNode(_component_v_icon, { class: "mr-2" }, {
default: _withCtx(() => _cache[27] || (_cache[27] = [
default: _withCtx(() => _cache[28] || (_cache[28] = [
_createTextVNode("mdi-cog")
])),
_: 1
}),
_cache[28] || (_cache[28] = _createTextVNode(" 高级选项 "))
_cache[29] || (_cache[29] = _createTextVNode(" 高级选项 "))
]),
_: 1
}),
@@ -687,34 +620,48 @@ return (_ctx, _cache) => {
default: _withCtx(() => [
_createVNode(_component_v_col, {
cols: "12",
md: "6"
md: "4"
}, {
default: _withCtx(() => [
_createVNode(_component_v_switch, {
modelValue: config.auto_update_subscriptions,
"onUpdate:modelValue": _cache[13] || (_cache[13] = $event => ((config.auto_update_subscriptions) = $event)),
label: "自动更新订阅",
modelValue: config.discard_rules,
"onUpdate:modelValue": _cache[13] || (_cache[13] = $event => ((config.discard_rules) = $event)),
label: "丢弃订阅规则",
color: "primary",
inset: "",
hint: "定期自动更新Clash订阅配置",
"persistent-hint": ""
hint: "不保留订阅配置文件的rules字段"
}, null, 8, ["modelValue"])
]),
_: 1
}),
_createVNode(_component_v_col, {
cols: "12",
md: "6"
md: "4"
}, {
default: _withCtx(() => [
_createVNode(_component_v_switch, {
modelValue: config.enable_acl4ssr,
"onUpdate:modelValue": _cache[14] || (_cache[14] = $event => ((config.enable_acl4ssr) = $event)),
label: "ACL4SSR规则集",
color: "primary",
inset: "",
hint: "启用ACL4SSR规则集"
}, null, 8, ["modelValue"])
]),
_: 1
}),
_createVNode(_component_v_col, {
cols: "12",
md: "4"
}, {
default: _withCtx(() => [
_createVNode(_component_v_switch, {
modelValue: config.group_by_region,
"onUpdate:modelValue": _cache[14] || (_cache[14] = $event => ((config.group_by_region) = $event)),
"onUpdate:modelValue": _cache[15] || (_cache[15] = $event => ((config.group_by_region) = $event)),
label: "按大洲分组节点",
color: "primary",
inset: "",
hint: "启用后根据名称,将节点添加到代理组",
"persistent-hint": ""
hint: "启用后根据名称,将节点添加到代理组"
}, null, 8, ["modelValue"])
]),
_: 1
@@ -726,23 +673,22 @@ return (_ctx, _cache) => {
default: _withCtx(() => [
_createVNode(_component_v_col, {
cols: "12",
md: "6"
md: "4"
}, {
default: _withCtx(() => [
_createVNode(_component_v_text_field, {
modelValue: config.ruleset_prefix,
"onUpdate:modelValue": _cache[15] || (_cache[15] = $event => ((config.ruleset_prefix) = $event)),
"onUpdate:modelValue": _cache[16] || (_cache[16] = $event => ((config.ruleset_prefix) = $event)),
label: "规则集前缀",
variant: "outlined",
placeholder: "📂<-",
placeholder: "📂<=",
rules: [v => !!v || '规则集前缀不能为空'],
hint: "为生成的规则集添加前缀",
"persistent-hint": ""
hint: "为生成的规则集添加前缀"
}, {
"prepend-inner": _withCtx(() => [
_createVNode(_component_v_icon, { color: "info" }, {
default: _withCtx(() => _cache[29] || (_cache[29] = [
_createTextVNode("mdi-prefix")
default: _withCtx(() => _cache[30] || (_cache[30] = [
_createTextVNode("mdi-palette")
])),
_: 1
})
@@ -754,12 +700,39 @@ return (_ctx, _cache) => {
}),
_createVNode(_component_v_col, {
cols: "12",
md: "6"
md: "4"
}, {
default: _withCtx(() => [
_createVNode(_component_v_text_field, {
modelValue: config.acl4ssr_prefix,
"onUpdate:modelValue": _cache[17] || (_cache[17] = $event => ((config.acl4ssr_prefix) = $event)),
label: "ACL4SSR 规则集前缀",
variant: "outlined",
placeholder: "🗂️=>",
rules: [v => !!v || '规则集前缀不能为空'],
hint: "ACL4SSR 规则集前缀"
}, {
"prepend-inner": _withCtx(() => [
_createVNode(_component_v_icon, { color: "primary" }, {
default: _withCtx(() => _cache[31] || (_cache[31] = [
_createTextVNode("mdi-palette")
])),
_: 1
})
]),
_: 1
}, 8, ["modelValue", "rules"])
]),
_: 1
}),
_createVNode(_component_v_col, {
cols: "12",
md: "4"
}, {
default: _withCtx(() => [
_createVNode(_component_v_text_field, {
modelValue: config.refresh_delay,
"onUpdate:modelValue": _cache[16] || (_cache[16] = $event => ((config.refresh_delay) = $event)),
"onUpdate:modelValue": _cache[18] || (_cache[18] = $event => ((config.refresh_delay) = $event)),
modelModifiers: { number: true },
label: "刷新延迟",
variant: "outlined",
@@ -768,12 +741,11 @@ return (_ctx, _cache) => {
max: "30",
suffix: "秒",
hint: "通知Clash刷新规则集的延迟时间",
"persistent-hint": "",
rules: [v => v >= 0 || '刷新延迟不能为负数']
}, {
"prepend-inner": _withCtx(() => [
_createVNode(_component_v_icon, { color: "info" }, {
default: _withCtx(() => _cache[30] || (_cache[30] = [
default: _withCtx(() => _cache[32] || (_cache[32] = [
_createTextVNode("mdi-clock-outline")
])),
_: 1
@@ -802,6 +774,21 @@ return (_ctx, _cache) => {
]),
_: 1
}),
_createVNode(_component_v_alert, {
type: "info",
text: "",
class: "mb-6",
variant: "tonal"
}, {
default: _withCtx(() => _cache[38] || (_cache[38] = [
_createTextVNode(" 配置说明参考: "),
_createElementVNode("a", {
href: "https://github.com/wumode/MoviePilot-Plugins/tree/main/plugins.v2/clashruleprovider/README.md",
target: "_blank"
}, "README", -1)
])),
_: 1
}),
_createVNode(_component_v_card_actions, null, {
default: _withCtx(() => [
_createVNode(_component_v_btn, {
@@ -810,12 +797,12 @@ return (_ctx, _cache) => {
}, {
default: _withCtx(() => [
_createVNode(_component_v_icon, { left: "" }, {
default: _withCtx(() => _cache[36] || (_cache[36] = [
default: _withCtx(() => _cache[39] || (_cache[39] = [
_createTextVNode("mdi-view-dashboard-edit")
])),
_: 1
}),
_cache[37] || (_cache[37] = _createTextVNode(" 规则 "))
_cache[40] || (_cache[40] = _createTextVNode(" 规则 "))
]),
_: 1
}),
@@ -823,7 +810,7 @@ return (_ctx, _cache) => {
color: "secondary",
onClick: resetForm
}, {
default: _withCtx(() => _cache[38] || (_cache[38] = [
default: _withCtx(() => _cache[41] || (_cache[41] = [
_createTextVNode("重置")
])),
_: 1
@@ -833,7 +820,7 @@ return (_ctx, _cache) => {
onClick: testConnection,
loading: testing.value
}, {
default: _withCtx(() => _cache[39] || (_cache[39] = [
default: _withCtx(() => _cache[42] || (_cache[42] = [
_createTextVNode("测试连接")
])),
_: 1
@@ -845,7 +832,7 @@ return (_ctx, _cache) => {
onClick: saveConfig,
loading: saving.value
}, {
default: _withCtx(() => _cache[40] || (_cache[40] = [
default: _withCtx(() => _cache[43] || (_cache[43] = [
_createTextVNode(" 保存配置 ")
])),
_: 1
@@ -860,7 +847,7 @@ return (_ctx, _cache) => {
variant: "tonal",
closable: "",
class: "ma-4 mt-0",
"onClick:close": _cache[18] || (_cache[18] = $event => (testResult.show = false))
"onClick:close": _cache[20] || (_cache[20] = $event => (testResult.show = false))
}, {
default: _withCtx(() => [
_createElementVNode("div", _hoisted_2, [
@@ -887,6 +874,6 @@ return (_ctx, _cache) => {
}
};
const ConfigComponent = /*#__PURE__*/_export_sfc(_sfc_main, [['__scopeId',"data-v-52f6e9ed"]]);
const ConfigComponent = /*#__PURE__*/_export_sfc(_sfc_main, [['__scopeId',"data-v-c50374dc"]]);
export { ConfigComponent as default };

View File

@@ -1,14 +1,14 @@
.plugin-page[data-v-783fd7ef] {
.plugin-page[data-v-455476d4] {
max-width: 1200px;
margin: 0 auto;
}
/* 使卡片等宽并适应移动端 */
.d-flex.flex-wrap[data-v-783fd7ef] {
.d-flex.flex-wrap[data-v-455476d4] {
gap: 16px;
}
.url-display[data-v-783fd7ef] {
.url-display[data-v-455476d4] {
word-break: break-all;
padding: 8px;
background: rgba(0, 0, 0, 0.05);
@@ -17,19 +17,19 @@
/* 移动端堆叠布局 */
@media (max-width: 768px) {
.d-flex.flex-wrap[data-v-783fd7ef] {
.d-flex.flex-wrap[data-v-455476d4] {
flex-direction: column;
}
}
/* Add visual distinction between sections */
.ruleset-section[data-v-783fd7ef] {
.ruleset-section[data-v-455476d4] {
border: 1px solid #e0e0e0;
border-radius: 4px;
padding: 16px;
background-color: #f5f5f5;
}
.top-section[data-v-783fd7ef] {
.top-section[data-v-455476d4] {
border: 1px solid #e0e0e0;
border-radius: 4px;
padding: 16px;
@@ -37,12 +37,12 @@
}
/* Optional: Add different border colors to further distinguish */
.ruleset-section[data-v-783fd7ef] {
.ruleset-section[data-v-455476d4] {
border-left: 4px solid #2196F3; /* Blue accent */
}
.top-section[data-v-783fd7ef] {
.top-section[data-v-455476d4] {
border-left: 4px solid #4CAF50; /* Green accent */
}
.drag-handle[data-v-783fd7ef] {
.drag-handle[data-v-455476d4] {
cursor: move;
}

View File

@@ -2,11 +2,11 @@ const currentImports = {};
const exportSet = new Set(['Module', '__esModule', 'default', '_export_sfc']);
let moduleMap = {
"./Page":()=>{
dynamicLoadingCss(["__federation_expose_Page-_CExF_DI.css"], false, './Page');
return __federation_import('./__federation_expose_Page-C2_enJ99.js').then(module =>Object.keys(module).every(item => exportSet.has(item)) ? () => module.default : () => module)},
dynamicLoadingCss(["__federation_expose_Page-Djrrbsow.css"], false, './Page');
return __federation_import('./__federation_expose_Page-D_nruYha.js').then(module =>Object.keys(module).every(item => exportSet.has(item)) ? () => module.default : () => module)},
"./Config":()=>{
dynamicLoadingCss(["__federation_expose_Config-nL3Pv4Qs.css"], false, './Config');
return __federation_import('./__federation_expose_Config-DZF0yyTH.js').then(module =>Object.keys(module).every(item => exportSet.has(item)) ? () => module.default : () => module)},
dynamicLoadingCss(["__federation_expose_Config-BJvXq0hj.css"], false, './Config');
return __federation_import('./__federation_expose_Config-Btg4HYx3.js').then(module =>Object.keys(module).every(item => exportSet.has(item)) ? () => module.default : () => module)},
"./Dashboard":()=>{
dynamicLoadingCss([], false, './Dashboard');
return __federation_import('./__federation_expose_Dashboard-DKtydfsT.js').then(module =>Object.keys(module).every(item => exportSet.has(item)) ? () => module.default : () => module)},};