update(ClashRuleProvider): 支持: 规则分页; 导入规则; 代理组

This commit is contained in:
wumode
2025-06-17 15:19:39 +08:00
parent dcd8b24637
commit 6f7f72f784
10 changed files with 2927 additions and 42584 deletions

View File

@@ -446,12 +446,13 @@
"name": "Clash Rule Provider",
"description": "随时为Clash添加一些额外的规则。",
"labels": "工具",
"version": "0.1.0",
"version": "1.0.0",
"icon": "Mihomo_Meta_A.png",
"author": "wumode",
"level": 1,
"history": {
"v0.1.0": "新增ClashRuleProvider"
"v0.1.0": "新增ClashRuleProvider",
"v1.0.0": "支持: 规则分页; 导入规则; 代理组; 附加出站代理; 按区域分组"
}
},
"LexiAnnot": {

View File

@@ -1,23 +1,27 @@
import hashlib
import json
import re
import time
from datetime import datetime, timedelta
from typing import Any, Optional, List, Dict, Tuple, Union
import pytz
import time
import yaml
import hashlib
from fastapi import Body, Response
from datetime import datetime, timedelta
import pytz
import copy
import math
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.cron import CronTrigger
from fastapi import Body, Response
from app import schemas
from app.core.config import settings
from app.core.event import eventmanager
from app.log import logger
from app.plugins import _PluginBase
from app.plugins.clashruleprovider.clash_rule_parser import Action, RuleType, ClashRule, MatchRule, LogicRule
from app.plugins.clashruleprovider.clash_rule_parser import ClashRuleParser
from app.schemas.types import EventType
from app.schemas.types import EventType, 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
class ClashRuleProvider(_PluginBase):
@@ -26,10 +30,9 @@ class ClashRuleProvider(_PluginBase):
# 插件描述
plugin_desc = "随时为Clash添加一些额外的规则。"
# 插件图标
plugin_icon = ("https://raw.githubusercontent.com/wumode/MoviePilot-Plugins/"
"refs/heads/imdbsource_assets/icons/Mihomo_Meta_A.png")
plugin_icon = "Mihomo_Meta_A.png"
# 插件版本
plugin_version = "0.1.0"
plugin_version = "1.0.0"
# 插件作者
plugin_author = "wumode"
# 作者主页
@@ -53,13 +56,14 @@ class ClashRuleProvider(_PluginBase):
# Clash 面板密钥
_clash_dashboard_secret = None
# MoviePilot URL
_movie_pilot_url = None
_movie_pilot_url = ''
_cron = ''
_timeout = 10
_retry_times = 3
_filter_keywords = []
_auto_update_subscriptions = True
_ruleset_prefix = '📂<-'
_group_by_region = False
# 插件数据
_clash_config = None
@@ -68,17 +72,23 @@ class ClashRuleProvider(_PluginBase):
_rule_provider: Dict[str, Any] = {}
_subscription_info = {}
_ruleset_names: Dict[str, str] = {}
_proxy_groups = []
_extra_proxies = []
# protected variables
_clash_rule_parser = None
_ruleset_rule_parser = None
_custom_rule_sets = None
_scheduler: Optional[BackgroundScheduler] = None
_countries: Optional[List[Dict[str, str]]] = None
_proxy_groups_by_region: List[Dict[str, Any]] = []
def init_plugin(self, config: dict = None):
self._clash_config = self.get_data("clash_config")
self._ruleset_rules = self.get_data("ruleset_rules")
self._top_rules = self.get_data("top_rules")
self._proxy_groups = self.get_data("proxy_groups") or []
self._extra_proxies = self.get_data("extra_proxies") or []
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 {}
@@ -91,7 +101,7 @@ class ClashRuleProvider(_PluginBase):
self._clash_dashboard_url = config.get("clash_dashboard_url")
self._clash_dashboard_secret = config.get("clash_dashboard_secret")
self._movie_pilot_url = config.get("movie_pilot_url")
if self._movie_pilot_url[-1] == '/':
if self._movie_pilot_url and self._movie_pilot_url[-1] == '/':
self._movie_pilot_url = self._movie_pilot_url[:-1]
self._cron = config.get("cron_string")
self._timeout = config.get("timeout")
@@ -99,9 +109,15 @@ class ClashRuleProvider(_PluginBase):
self._filter_keywords = config.get("filter_keywords")
self._ruleset_prefix = config.get("ruleset_prefix", "Custom_")
self._auto_update_subscriptions = config.get("auto_update_subscriptions")
self._group_by_region = config.get("group_by_region")
self._clash_rule_parser = ClashRuleParser()
self._ruleset_rule_parser = ClashRuleParser()
if self._enabled:
if self._group_by_region:
self._countries = ClashRuleProvider.__load_countries(
f"{settings.ROOT_PATH}/app/plugins/clashruleprovider/countries.json")
self._proxy_groups_by_region = ClashRuleProvider.__group_by_region(self._countries,
self._clash_config.get('proxies'))
self.__parse_config()
self._scheduler = BackgroundScheduler(timezone=settings.TZ)
self._scheduler.start()
@@ -124,7 +140,7 @@ class ClashRuleProvider(_PluginBase):
"description": "测试连接"
},
{
"path": "/clash_outbound",
"path": "/clash-outbound",
"endpoint": self.get_clash_outbound,
"methods": ["GET"],
"auth": "bear",
@@ -204,13 +220,61 @@ class ClashRuleProvider(_PluginBase):
"description": "update clash rules"
},
{
"path": "/rule_providers",
"path": "/rule-providers",
"endpoint": self.get_rule_providers,
"methods": ["GET"],
"auth": "bear",
"summary": "update rule providers",
"description": "update rule providers"
},
{
"path": "/extra-proxies",
"endpoint": self.get_extra_proxies,
"methods": ["GET"],
"auth": "bear",
"summary": "extra proxies",
"description": "extra proxies"
},
{
"path": "/extra-proxies",
"endpoint": self.delete_extra_proxy,
"methods": ["DELETE"],
"auth": "bear",
"summary": "delete a extra proxy",
"description": "delete a extra proxy"
},
{
"path": "/extra-proxies",
"endpoint": self.add_extra_proxies,
"methods": ["POST"],
"auth": "bear",
"summary": "add extra proxies",
"description": "add extra proxies"
},
{
"path": "/proxy-groups",
"endpoint": self.get_proxy_groups,
"methods": ["GET"],
"auth": "bear",
"summary": "proxy groups",
"description": "proxy groups"
},
{
"path": "/proxy-group",
"endpoint": self.delete_proxy_group,
"methods": ["DELETE"],
"auth": "bear",
"summary": "delete a proxy group",
"description": "delete a proxy group"
},
{
"path": "/proxy-group",
"endpoint": self.add_proxy_group,
"methods": ["POST"],
"auth": "bear",
"summary": "add a proxy group",
"description": "add a proxy group"
},
{
"path": "/ruleset",
"endpoint": self.get_ruleset,
@@ -218,6 +282,14 @@ class ClashRuleProvider(_PluginBase):
"summary": "update rule providers",
"description": "update rule providers"
},
{
"path": "/import",
"endpoint": self.import_rules,
"methods": ["POST"],
"auth": "bear",
"summary": "import top rules",
"description": "import top rules"
},
{
"path": "/config",
"endpoint": self.get_clash_config,
@@ -254,7 +326,7 @@ class ClashRuleProvider(_PluginBase):
if self.get_state() and self._auto_update_subscriptions:
return [{
"id": "ClashRuleProvider",
"name": "Clash Rule Provider 服务",
"name": "定时更新订阅",
"trigger": CronTrigger.from_crontab(self._cron),
"func": self.update_subscription_service,
"kwargs": {}
@@ -287,47 +359,47 @@ class ClashRuleProvider(_PluginBase):
self.save_data('subscription_info', self._subscription_info)
self.save_data('ruleset_names', self._ruleset_names)
self.save_data('rule_provider', self._rule_provider)
self.save_data('proxy_groups', self._proxy_groups)
self.save_data('extra_proxies', self._extra_proxies)
def __parse_config(self):
if not self._top_rules:
return
if self._top_rules is None:
self._top_rules = []
if self._ruleset_rules is None:
self._ruleset_rules = []
self._clash_rule_parser.parse_rules_from_list(self._top_rules)
if not self._ruleset_rules:
return
self._ruleset_rule_parser.parse_rules_from_list(self._ruleset_rules)
def test_connectivity(self, params: Dict[str, Any]) -> Dict[str, Any]:
def test_connectivity(self, params: Dict[str, Any]) -> schemas.Response:
if not self._enabled:
return {"success": False, "message": ""}
return schemas.Response(success=False, message="")
if not params.get('clash_dashboard_url') or not params.get('clash_dashboard_secret') \
or not params.get('sub_link'):
return {"success": False, "message": "missing params"}
return schemas.Response(success=True, message="missing params")
clash_version_url = f"{params.get('clash_dashboard_url')}/version"
ret = RequestUtils(accept_type="application/json",
headers={"authorization": f"Bearer {params.get('clash_dashboard_secret')}"}
).get(clash_version_url)
if not ret:
return {"success": False, "message": "无法连接到Clash"}
return schemas.Response(success=False, message="无法连接到Clash")
ret = RequestUtils(accept_type="text/html",
proxies=settings.PROXY if self._proxy else None
).get(params.get('sub_link'))
if not ret:
return {"success": False, "message": f"Unable to get {params.get('sub_link')}"}
return {"success": True, "message": "测试连接成功"}
return schemas.Response(success=False, message=f"Unable to get {params.get('sub_link')}")
return schemas.Response(success=True, message="测试连接成功")
def get_ruleset(self, name):
if not self._ruleset_names.get(name):
return None
name = self._ruleset_names.get(name)
rules = self.__get_ruleset(name)
# if rules or ruleset in self._rule_provider:
# self._rule_provider[ruleset] = rules
res = yaml.dump({"payload": rules}, allow_unicode=True)
return Response(content=res, media_type="text/yaml")
def get_clash_outbound(self):
def get_clash_outbound(self) -> schemas.Response:
outbound = self.clash_outbound(self._clash_config)
return {"success": True, "message": None, "data": {"outbound": outbound}}
return schemas.Response(success=True, message="", data={"outbound": outbound})
def get_status(self):
rule_size = len(self._clash_config.get("rules", [])) if self._clash_config else 0
@@ -342,7 +414,7 @@ class ClashRuleProvider(_PluginBase):
def get_clash_config(self):
config = self.clash_config()
if not config:
return {"success": False, "message": ""}
return {'success': False, "message": ''}
res = yaml.dump(config, allow_unicode=True)
headers = {'Subscription-Userinfo': f'upload={self._subscription_info["upload"]}; '
f'download={self._subscription_info["download"]}; '
@@ -350,22 +422,35 @@ class ClashRuleProvider(_PluginBase):
f'expire={self._subscription_info["expire"]}'}
return Response(headers=headers, content=res, media_type="text/yaml")
def get_rules(self, rule_type: str) -> Dict[str, Any]:
def get_rules(self, rule_type: str) -> schemas.Response:
if rule_type == 'ruleset':
return {"success": True, "message": None, "data": {"rules": self._ruleset_rule_parser.to_dict()}}
return {"success": True, "message": None, "data": {"rules": self._clash_rule_parser.to_dict()}}
return schemas.Response(success=True, message='', data={'rules': self._ruleset_rule_parser.to_dict()})
return schemas.Response(success=True, message='', data={'rules': self._clash_rule_parser.to_dict()})
def delete_rule(self, params: dict = Body(...)):
def delete_rule(self, params: dict = Body(...)) -> schemas.Response:
if not self._enabled:
return {"success": False, "message": ""}
return schemas.Response(success=False, message='')
if params.get('type') == 'ruleset':
res = self.delete_rule_by_priority(params.get('priority'), self._ruleset_rule_parser)
if res:
self.__add_notification_job(
f"{self._ruleset_prefix}{res.action.value if isinstance(res.action, Action) else res.action}")
else:
res = self.delete_rule_by_priority(params.get('priority'), self._clash_rule_parser)
return {"success": res, "message": None}
self.delete_rule_by_priority(params.get('priority'), self._clash_rule_parser)
return schemas.Response(success=True, message='')
def import_rules(self, params: Dict[str, Any]) -> schemas.Response:
if not self._enabled:
return schemas.Response(success=False, message='')
rules: List[str] = []
if params.get('type') == 'YAML':
try:
imported_rules = yaml.load(params["payload"], Loader=yaml.SafeLoader)
rules = imported_rules.get("rules", [])
except yaml.YAMLError as err:
return schemas.Response(success=False, message=f'YAML error: {err}')
self.append_top_rules(rules)
return schemas.Response(success=True)
def reorder_rules(self, params: Dict[str, Any]):
if not self._enabled:
@@ -413,31 +498,105 @@ class ClashRuleProvider(_PluginBase):
res = self.add_rule_by_priority(params.get('rule_data'), self._clash_rule_parser)
return {"success": bool(res), "message": None}
def get_subscription(self):
def get_subscription(self) -> schemas.Response:
if not self._sub_links:
return None
return {"success": True, "message": None, "data": {"url": self._sub_links[0]}}
return schemas.Response(success=False, message=f"Invalid subscription links: {self._sub_links}")
return schemas.Response(success=True, data={"url": self._sub_links[0]})
def update_subscription(self, params: Dict[str, Any]):
if not self._enabled:
return {"success": False, "message": ""}
return schemas.Response(success=False, message="")
url = params.get('url')
if not url:
return {"success": False, "message": "missing params"}
res = self.update_subscription_service()
return schemas.Response(success=False, message="missing params")
res = self.__update_subscription()
if not res:
return {"success": True, "message": f"订阅链接 {self._sub_links[0]} 更新失败"}
return {"success": True, "message": "订阅更新成功"}
return schemas.Response(success=False, message=f"订阅链接 {self._sub_links[0]} 更新失败")
return schemas.Response(success=True, message='订阅更新成功')
def get_rule_providers(self):
return {"success": True, "message": None, "data": self.rule_providers()}
def get_rule_providers(self) -> schemas.Response:
return schemas.Response(success=True, data=self.rule_providers())
@staticmethod
def clash_outbound(clash_config: Dict[str, Any]) -> Optional[List]:
def get_proxy_groups(self) -> schemas.Response:
return schemas.Response(success=True, data={'proxy_groups': self._proxy_groups})
def get_extra_proxies(self) -> schemas.Response:
return schemas.Response(success=True, data={'extra_proxies': self._extra_proxies})
def add_extra_proxies(self, params: Dict[str, Any]):
if not self._enabled:
return schemas.Response(success=False, message='')
extra_proxies: List = []
if params.get('type') == 'YAML':
try:
imported_proxies = yaml.load(params["payload"], Loader=yaml.SafeLoader)
extra_proxies = imported_proxies.get("proxies", [])
except yaml.YAMLError as err:
return schemas.Response(success=False, message=f'YAML error: {err}')
for proxy in extra_proxies:
name = proxy.get('name')
if not name or any(x.get('name') == name for x in self.clash_outbound(self._clash_config)):
logger.warning(f"The proxy name {proxy['name']} already exists. Skipping...")
continue
required_fields = {'name', 'type', 'server', 'port'}
if not required_fields.issubset(proxy.keys()):
missing = required_fields - proxy.keys()
logger.error(f"Required field is missing: {missing}")
continue
self._extra_proxies.append(proxy)
self.save_data('extra_proxies', self._extra_proxies)
return schemas.Response(success=True)
def delete_extra_proxy(self, params: dict = Body(...)) -> schemas.Response:
if not self._enabled:
return schemas.Response(success=False, message='')
name = params.get('name')
self._extra_proxies = [item for item in self._extra_proxies if item.get('name') != name]
self.save_data('extra_proxies', self._extra_proxies)
return schemas.Response(success=True, message='')
def add_proxy_group(self, params: Dict[str, Any]) -> schemas.Response:
if not self._enabled:
return schemas.Response(success=False, message='')
if 'proxy_group' not in params or params['proxy_group'] is None:
return schemas.Response(success=False, message="Missing params")
item = params['proxy_group']
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)
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:
continue
if v is None:
continue
new_item[k] = v
self._proxy_groups.append(new_item)
self.save_data('proxy_groups', self._proxy_groups)
return schemas.Response(success=True)
def delete_proxy_group(self, params: dict = Body(...)) -> schemas.Response:
if not self._enabled:
return schemas.Response(success=False, message='')
name = params.get('name')
self._proxy_groups = [item for item in self._proxy_groups if item.get('name') != name]
self.save_data('proxy_groups', self._proxy_groups)
return schemas.Response(success=True, message='')
def clash_outbound(self, clash_config: Dict[str, Any]) -> Optional[List]:
if not clash_config:
return []
outbound = [{'name': proxy_group.get("name")} for proxy_group in clash_config.get("proxy-groups")]
outbound.extend([{'name': proxy.get("name")} for proxy in clash_config.get("proxies")])
if self._group_by_region:
outbound.extend([{'name': proxy_group.get("name")} for proxy_group in self._proxy_groups_by_region])
outbound.extend([{'name': proxy.get("name")} for proxy in self._extra_proxies])
outbound.extend([{'name': proxy_group.get("name")} for proxy_group in self._proxy_groups])
return outbound
def rule_providers(self) -> Optional[Dict[str, Any]]:
@@ -490,6 +649,17 @@ class ClashRuleProvider(_PluginBase):
if not self._clash_rule_parser.has_rule(clash_rule):
self._clash_rule_parser.insert_rule_at_priority(clash_rule, 0)
def append_top_rules(self, rules: List[str]) -> None:
clash_rules = []
for rule in rules:
clash_rule = ClashRuleParser.parse_rule_line(rule)
if not clash_rule:
continue
clash_rules.append(clash_rule)
self._clash_rule_parser.append_rules(clash_rules)
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):
return False
@@ -522,8 +692,40 @@ class ClashRuleProvider(_PluginBase):
self.__save_data()
return res
@eventmanager.register(EventType.PluginAction)
def update_subscription_service(self) -> bool:
@staticmethod
def format_bytes(bytes):
if bytes == 0:
return '0 B'
k = 1024
sizes = ['B', 'KB', 'MB', 'GB', 'TB']
i = math.floor(math.log(bytes) / math.log(k))
return f"{bytes / math.pow(k, i):.2f} {sizes[i]}"
@staticmethod
def format_expire_time(timestamp):
seconds_left = timestamp - int(time.time())
days = seconds_left // 86400
return f"{days}天后过期" if days > 0 else "已过期"
def update_subscription_service(self):
res = self.__update_subscription()
if res:
used = self._subscription_info['download'] + self._subscription_info['upload']
remaining = self._subscription_info['total'] - used
message = (f"订阅更新成功\n"
f"已用流量: {ClashRuleProvider.format_bytes(used)}\n"
f"剩余流量: {ClashRuleProvider.format_bytes(remaining)}\n"
f"总量: {ClashRuleProvider.format_bytes(self._subscription_info['total'])}\n"
f"过期时间: {ClashRuleProvider.format_expire_time(self._subscription_info['expire'])}")
else:
message = "订阅更新失败"
if self._notify:
self.post_message(title=f"{self.plugin_name}",
mtype=NotificationType.Plugin,
text=f"{message}"
)
def __update_subscription(self) -> bool:
if not self._sub_links:
return False
url = self._sub_links[0]
@@ -556,6 +758,50 @@ class ClashRuleProvider(_PluginBase):
headers={"authorization": f"Bearer {self._clash_dashboard_secret}"}
).put(url)
@staticmethod
def __load_countries(file_path: str) -> List:
try:
countries = json.load(open(file_path))
except Exception as e:
logger.error(f"插件加载错误:{e}")
return []
return countries
@staticmethod
def __group_by_region(countries: List, proxies) -> List[Dict[str, Any]]:
continents_nodes = {'Asia': [], 'Europe': [], 'SouthAmerica': [], 'NorthAmerica': [], 'Africa': [],
'Oceania': [], 'AsiaExceptChina': []}
proxy_groups = []
for proxy_node in proxies:
continent = ClashRuleProvider.__continent_name_from_node(countries, proxy_node['name'])
if not continent:
continue
continents_nodes[continent].append(proxy_node['name'])
for continent_nodes in continents_nodes:
if len(continents_nodes[continent_nodes]):
proxy_group = {'name': continent_nodes, 'type': 'select', 'proxies': continents_nodes[continent_nodes]}
proxy_groups.append(proxy_group)
for continent_node in continents_nodes['Asia']:
if any(x in continent_node for x in ('中国', '香港', 'CN')):
continue
continents_nodes['AsiaExceptChina'].append(continent_node)
proxy_group = {'name': 'AsiaExceptChina', 'type': 'select', 'proxies': continents_nodes['AsiaExceptChina']}
proxy_groups.append(proxy_group)
return proxy_groups
@staticmethod
def __continent_name_from_node(countries: List[Dict[str, str]], node_name: str) -> Optional[str]:
continents_names = {'欧洲': 'Europe',
'亚洲': 'Asia',
'大洋洲': 'Oceania',
'非洲': 'Africa',
'北美洲': 'NorthAmerica',
'南美洲': 'SouthAmerica'}
for country in countries:
if country['chinese'] in node_name or country['english'].lower() in node_name.lower():
return continents_names[country['continent']]
return None
def __add_notification_job(self, ruleset: str):
if ruleset in self._rule_provider:
self._scheduler.add_job(self.notify_clash, "date",
@@ -581,20 +827,43 @@ 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")]
# 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]]:
if not self._clash_config:
return
return None
self.__insert_ruleset()
self._top_rules = self._clash_rule_parser.to_string()
clash_config = self._clash_config.copy()
clash_config = copy.deepcopy(self._clash_config)
# 添加代理组
proxy_groups = copy.deepcopy(self._proxy_groups)
if proxy_groups:
if clash_config.get("proxy-groups"):
clash_config['proxy-groups'].extend(proxy_groups)
else:
clash_config['proxy-groups'] = proxy_groups
# 添加额外节点
if clash_config.get('proxies'):
clash_config['proxies'].extend(self._extra_proxies)
else:
clash_config['proxies'] = copy.deepcopy(self._extra_proxies)
# 添加按大洲代理组
if self._group_by_region:
if self._proxy_groups_by_region:
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}")
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", [])
@@ -616,9 +885,15 @@ class ClashRuleProvider(_PluginBase):
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']:
del self._ruleset_names[key]
key_to_delete.append(key)
for key in key_to_delete:
del self._ruleset_names[key]
if not clash_config.get("rule-providers"):
del clash_config["rule-providers"]
self.save_data('ruleset_names', self._ruleset_names)
self.save_data('rule_provider', self._rule_provider)
return clash_config

View File

@@ -1,8 +1,96 @@
import re
from typing import List, Dict, Any, Optional, Union, Callable
from typing import List, Dict, Any, Optional, Union, Callable, Literal
from dataclasses import dataclass
from enum import Enum
from pydantic import BaseModel, Field, validator
class ProxyGroupBase(BaseModel):
"""
包含所有代理组类型共有的通用字段。
"""
# Required field
name: str = Field(..., description="The name of the proxy group.")
# Proxy and provider references
proxies: Optional[List[str]] = Field(None, description="References to outbound proxies or other proxy groups.")
use: Optional[List[str]] = Field(None, description="References to proxy provider sets.")
# Health check fields
url: Optional[str] = Field(None, description="Health check test address.")
interval: Optional[int] = Field(None, description="Health check interval in seconds.")
lazy: bool = Field(True, description="If not selected, no health checks are performed.")
timeout: Optional[int] = Field(5000, description="Health check timeout in milliseconds.")
max_failed_times: Optional[int] = Field(5, description="Maximum number of failures before a forced health check.")
expected_status: Optional[str] = Field(None, description="Expected HTTP response status code for health checks.")
# Network and routing fields
disable_udp: Optional[bool] = Field(False, description="Disables UDP for this proxy group.")
interface_name: Optional[str] = Field(None, description="DEPRECATED. Specifies the outbound interface.")
routing_mark: Optional[int] = Field(None, description="DEPRECATED. The routing mark for outbound connections.")
# Dynamic proxy inclusion
include_all: Optional[bool] = Field(False, description="Includes all outbound proxies and proxy sets.")
include_all_proxies: Optional[bool] = Field(False, description="Includes all outbound proxies.")
include_all_providers: Optional[bool] = Field(False, description="Includes all proxy provider sets.")
# Filtering
filter: Optional[str] = Field(None, description="Regex to filter nodes from providers.")
exclude_filter: Optional[str] = Field(None, description="Regex to exclude nodes.")
exclude_type: Optional[str] = Field(None, description="Exclude nodes by adapter type, separated by '|'.")
# UI fields
hidden: Optional[bool] = Field(False, description="Hides the proxy group in the API.")
icon: Optional[str] = Field(None, description="Icon string for the proxy group, for UI use.")
@validator('expected_status')
def validate_expected_status(cls, v: Optional[str]) -> Optional[str]:
if v is None or v == '*':
return v
pattern = re.compile(r'^\d{3}([-/]\d{3})*$')
if not pattern.match(v):
raise ValueError("Invalid format for expected-status.")
parts = re.split(r'[/]', v)
for part in parts:
if '-' in part:
start, end = part.split('-')
if not (start.isdigit() and end.isdigit() and 100 <= int(start) < 600 and 100 <= int(end) < 600 and int(start) <= int(end)):
raise ValueError(f"Invalid status code range: {part}")
elif not (part.isdigit() and 100 <= int(part) < 600):
raise ValueError(f"Invalid status code: {part}")
return v
class SelectGroup(ProxyGroupBase):
type: Literal['select']
class RelayGroup(ProxyGroupBase):
type: Literal['relay']
class FallbackGroup(ProxyGroupBase):
type: Literal['fallback']
class UrlTestGroup(ProxyGroupBase):
type: Literal['url-test']
tolerance: Optional[int] = Field(None, description="proxies switch tolerance, measured in milliseconds (ms).")
class LoadBalanceGroup(ProxyGroupBase):
type: Literal['load-balance']
strategy: Optional[Literal['round-robin', 'consistent-hashing', 'sticky-sessions']] = Field(
'round-robin',
description="Load balancing strategy."
)
# --- Discriminated Union ---
ProxyGroupUnion = Union[SelectGroup, RelayGroup, FallbackGroup, UrlTestGroup, LoadBalanceGroup]
class ProxyGroupValidator(BaseModel):
"""
这是Pydantic V1的验证器。
它使用 __root__ 字段来处理可辨识联合。
"""
__root__: ProxyGroupUnion
class RuleType(Enum):
"""Enumeration of all supported Clash rule types"""
@@ -282,8 +370,7 @@ class ClashRuleParser:
return self.rules
@staticmethod
def validate_rule(rule: ClashRule) -> bool:
def validate_rule(self, rule: ClashRule) -> bool:
"""Validate a parsed rule"""
try:
# Basic validation based on rule type
@@ -306,8 +393,7 @@ class ClashRuleParser:
return True
except Exception as e:
print(f"Invalid rule '{rule.raw_rule}': {e}")
except Exception:
return False
def to_string(self) -> List[str]:
@@ -370,6 +456,15 @@ class ClashRuleParser:
# Re-sort rules to maintain order
self.rules.sort(key=lambda r: r.priority)
def append_rules(self, rules: List[Union[ClashRule, LogicRule, MatchRule]]) -> None:
max_priority = max(rule.priority for rule in self.rules) if len(self.rules) else 0
priority = max_priority + 1
for rule in rules:
rule.priority = priority
self.rules.append(rule)
priority += 1
self.rules.sort(key=lambda r: r.priority)
def insert_rule_at_priority(self, rule: Union[ClashRule, LogicRule, MatchRule], priority: int):
"""Insert a rule at a specific priority position, adjusting other rules"""
# Adjust priorities of existing rules

File diff suppressed because it is too large Load Diff

View File

@@ -1,828 +0,0 @@
import { importShared } from './__federation_fn_import-JrT3xvdd.js';
import { _ as _export_sfc } from './_plugin-vue_export-helper-pcqpp-6-.js';
const {createTextVNode:_createTextVNode,resolveComponent:_resolveComponent,withCtx:_withCtx,createVNode:_createVNode,toDisplayString:_toDisplayString,openBlock:_openBlock,createBlock:_createBlock,createCommentVNode:_createCommentVNode,createElementVNode:_createElementVNode,mergeProps:_mergeProps,withModifiers:_withModifiers,createElementBlock:_createElementBlock} = await importShared('vue');
const _hoisted_1 = { class: "plugin-config" };
const _hoisted_2 = { class: "d-flex align-center" };
const _hoisted_3 = { class: "font-weight-medium" };
const _hoisted_4 = { class: "text-body-2" };
const {ref,reactive,onMounted,computed} = await importShared('vue');
// Props
const _sfc_main = {
__name: 'Config',
props: {
initialConfig: {
type: Object,
default: () => ({}),
},
api: {
type: Object,
default: () => {
},
},
},
emits: ['save', 'close'],
setup(__props, { emit: __emit }) {
const props = __props;
// 状态变量
const form = ref(null);
const isFormValid = ref(true);
const error = ref(null);
const saving = ref(false);
const testing = ref(false);
const showClashSecret = ref(false);
const selectedCronOption = ref('6hours');
// Test result state
const testResult = reactive({
show: false,
success: false,
title: '',
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 = {
enabled: false,
sub_links: [],
filter_keywords: ["公益性", "高延迟", "域名", "官网", "重启", "过期时间", "系统代理"],
clash_dashboard_url: '',
clash_dashboard_secret: '',
movie_pilot_url: '',
cron_string: '0 */6 * * *',
timeout: 10,
retry_times: 3,
proxy: false,
notify: false,
auto_update_subscriptions: true,
ruleset_prefix: '📂<-',
};
// 响应式配置对象
const config = reactive({...defaultConfig});
// 自定义事件
const emit = __emit;
// 初始化
onMounted(() => {
if (props.initialConfig) {
Object.keys(props.initialConfig).forEach(key => {
if (key in config) {
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';
}
}
});
// 验证函数
function isValidUrl(url) {
try {
new URL(url);
return true
} catch {
return false
}
}
function validateSubLinks(links) {
if (!links || links.length === 0) {
return '至少需要一个订阅链接'
}
for (const link of links) {
if (!isValidUrl(link)) {
return `无效的订阅链接: ${link}`
}
}
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;
error.value = null;
testResult.show = false;
try {
// 验证必需的参数
if (!config.clash_dashboard_url) {
throw new Error('请先配置 Clash 面板 URL')
}
if (!config.clash_dashboard_secret) {
throw new Error('请先配置 Clash 面板密钥')
}
if (!config.sub_links || config.sub_links.length === 0) {
throw new Error('请先配置至少一个订阅链接')
}
if (!config.movie_pilot_url || config.movie_pilot_url.length === 0) {
throw new Error('请先MoviePilot链接')
}
// 准备API请求参数
const testParams = {
clash_dashboard_url: config.clash_dashboard_url,
clash_dashboard_secret: config.clash_dashboard_secret,
sub_link: config.sub_links[0] // 使用第一个订阅链接进行测试
};
// 调用API进行连接测试
const result = await props.api.post('/plugin/ClashRuleProvider/connectivity', testParams);
// 根据返回结果显示相应消息
if (result.success) {
testResult.success = true;
testResult.title = '连接测试成功!';
testResult.message = 'Clash面板和订阅链接连接正常配置验证通过';
testResult.show = true;
// Auto hide after 5 seconds
setTimeout(() => {
testResult.show = false;
}, 5000);
} else {
throw new Error(result.message || '连接测试失败,请检查配置')
}
} catch (err) {
console.error('连接测试失败:', err);
testResult.success = false;
testResult.title = '连接测试失败';
testResult.message = err.message;
testResult.show = true;
} finally {
testing.value = false;
}
}
// 保存配置
async function saveConfig() {
if (!isFormValid.value) {
error.value = '请修正表单中的错误';
return
}
saving.value = true;
error.value = null;
try {
await new Promise(resolve => setTimeout(resolve, 1000));
emit('save', {...config});
} catch (err) {
console.error('保存配置失败:', err);
error.value = err.message || '保存配置失败';
} finally {
saving.value = false;
}
}
function extractDomain(url) {
try {
const domain = new URL(url).hostname;
return domain.startsWith('www.') ? domain.substring(4) : domain
} catch {
return url // 如果解析失败返回原始URL
}
}
// 重置表单
function resetForm() {
Object.keys(defaultConfig).forEach(key => {
config[key] = defaultConfig[key];
});
selectedCronOption.value = '6hours';
if (form.value) {
form.value.resetValidation();
}
}
// 关闭组件
function notifyClose() {
emit('close');
}
// 通知主应用切换到Page页面
function notifySwitch() {
emit('switch');
}
return (_ctx, _cache) => {
const _component_v_card_title = _resolveComponent("v-card-title");
const _component_v_icon = _resolveComponent("v-icon");
const _component_v_btn = _resolveComponent("v-btn");
const _component_v_card_item = _resolveComponent("v-card-item");
const _component_v_alert = _resolveComponent("v-alert");
const _component_v_switch = _resolveComponent("v-switch");
const _component_v_col = _resolveComponent("v-col");
const _component_v_row = _resolveComponent("v-row");
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_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");
const _component_v_expansion_panels = _resolveComponent("v-expansion-panels");
const _component_v_form = _resolveComponent("v-form");
const _component_v_card_text = _resolveComponent("v-card-text");
const _component_v_spacer = _resolveComponent("v-spacer");
const _component_v_card_actions = _resolveComponent("v-card-actions");
const _component_v_card = _resolveComponent("v-card");
return (_openBlock(), _createElementBlock("div", _hoisted_1, [
_createVNode(_component_v_card, null, {
default: _withCtx(() => [
_createVNode(_component_v_card_item, null, {
append: _withCtx(() => [
_createVNode(_component_v_btn, {
icon: "",
color: "primary",
variant: "text",
onClick: notifyClose
}, {
default: _withCtx(() => [
_createVNode(_component_v_icon, { left: "" }, {
default: _withCtx(() => _cache[18] || (_cache[18] = [
_createTextVNode("mdi-close")
])),
_: 1
})
]),
_: 1
})
]),
default: _withCtx(() => [
_createVNode(_component_v_card_title, null, {
default: _withCtx(() => _cache[17] || (_cache[17] = [
_createTextVNode("Clash Rule Provider 插件配置")
])),
_: 1
})
]),
_: 1
}),
_createVNode(_component_v_card_text, { class: "overflow-y-auto" }, {
default: _withCtx(() => [
(error.value)
? (_openBlock(), _createBlock(_component_v_alert, {
key: 0,
type: "error",
class: "mb-4"
}, {
default: _withCtx(() => [
_createTextVNode(_toDisplayString(error.value), 1)
]),
_: 1
}))
: _createCommentVNode("", true),
_createVNode(_component_v_form, {
ref_key: "form",
ref: form,
modelValue: isFormValid.value,
"onUpdate:modelValue": _cache[15] || (_cache[15] = $event => ((isFormValid).value = $event)),
onSubmit: _withModifiers(saveConfig, ["prevent"])
}, {
default: _withCtx(() => [
_cache[28] || (_cache[28] = _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"
}, {
default: _withCtx(() => [
_createVNode(_component_v_switch, {
modelValue: config.enabled,
"onUpdate:modelValue": _cache[0] || (_cache[0] = $event => ((config.enabled) = $event)),
label: "启用插件",
color: "primary",
inset: "",
hint: "启用后插件将开始监控和同步",
"persistent-hint": ""
}, null, 8, ["modelValue"])
]),
_: 1
}),
_createVNode(_component_v_col, {
cols: "12",
md: "4"
}, {
default: _withCtx(() => [
_createVNode(_component_v_switch, {
modelValue: config.proxy,
"onUpdate:modelValue": _cache[1] || (_cache[1] = $event => ((config.proxy) = $event)),
label: "启用代理",
color: "primary",
inset: "",
hint: "是否使用系统代理进行网络请求",
"persistent-hint": ""
}, null, 8, ["modelValue"])
]),
_: 1
}),
_createVNode(_component_v_col, {
cols: "12",
md: "4"
}, {
default: _withCtx(() => [
_createVNode(_component_v_switch, {
modelValue: config.notify,
"onUpdate:modelValue": _cache[2] || (_cache[2] = $event => ((config.notify) = $event)),
label: "启用通知",
color: "primary",
inset: "",
hint: "执行完成后发送通知消息",
"persistent-hint": ""
}, null, 8, ["modelValue"])
]),
_: 1
})
]),
_: 1
}),
_cache[29] || (_cache[29] = _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)),
label: "订阅链接",
variant: "outlined",
multiple: "",
chips: "",
"closable-chips": "",
hint: "添加一个Clash订阅链接",
"persistent-hint": "",
rules: [validateSubLinks]
}, {
chip: _withCtx(({ props, item }) => [
_createVNode(_component_v_chip, _mergeProps(props, {
closable: "",
size: "small"
}), {
default: _withCtx(() => [
_createTextVNode(_toDisplayString(extractDomain(item.value)), 1)
]),
_: 2
}, 1040)
]),
_: 1
}, 8, ["modelValue", "rules"])
]),
_: 1
}),
_createVNode(_component_v_col, { cols: "12" }, {
default: _withCtx(() => [
_createVNode(_component_v_combobox, {
modelValue: config.filter_keywords,
"onUpdate:modelValue": _cache[4] || (_cache[4] = $event => ((config.filter_keywords) = $event)),
label: "节点过滤关键词",
variant: "outlined",
multiple: "",
chips: "",
"closable-chips": "",
hint: "添加用于过滤节点的关键词",
"persistent-hint": ""
}, {
chip: _withCtx(({ props, item }) => [
_createVNode(_component_v_chip, _mergeProps(props, {
closable: "",
size: "small",
color: "info"
}), {
default: _withCtx(() => [
_createTextVNode(_toDisplayString(item.value), 1)
]),
_: 2
}, 1040)
]),
_: 1
}, 8, ["modelValue"])
]),
_: 1
})
]),
_: 1
}),
_cache[30] || (_cache[30] = _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)),
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[19] || (_cache[19] = [
_createTextVNode("mdi-web")
])),
_: 1
})
]),
_: 1
}, 8, ["modelValue", "rules"])
]),
_: 1
}),
_createVNode(_component_v_col, { cols: "12" }, {
default: _withCtx(() => [
_createVNode(_component_v_text_field, {
modelValue: config.clash_dashboard_secret,
"onUpdate:modelValue": _cache[6] || (_cache[6] = $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))
}, {
"prepend-inner": _withCtx(() => [
_createVNode(_component_v_icon, { color: "warning" }, {
default: _withCtx(() => _cache[20] || (_cache[20] = [
_createTextVNode("mdi-key")
])),
_: 1
})
]),
_: 1
}, 8, ["modelValue", "append-inner-icon", "type"])
]),
_: 1
})
]),
_: 1
}),
_cache[31] || (_cache[31] = _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)),
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[21] || (_cache[21] = [
_createTextVNode("mdi-movie")
])),
_: 1
})
]),
_: 1
}, 8, ["modelValue", "rules"])
]),
_: 1
})
]),
_: 1
}),
_cache[32] || (_cache[32] = _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
],
label: "执行周期",
items: cronOptions,
variant: "outlined",
"item-title": "text",
"item-value": "value",
hint: "选择插件执行的时间间隔",
"persistent-hint": ""
}, 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[22] || (_cache[22] = [
_createTextVNode("mdi-clock-outline")
])),
_: 1
})
]),
_: 1
}, 8, ["modelValue", "rules"])
]),
_: 1
}))
: _createCommentVNode("", true),
_createVNode(_component_v_col, {
cols: "12",
md: "6"
}, {
default: _withCtx(() => [
_createVNode(_component_v_text_field, {
modelValue: config.timeout,
"onUpdate:modelValue": _cache[11] || (_cache[11] = $event => ((config.timeout) = $event)),
modelModifiers: { number: true },
label: "超时时间 (秒)",
variant: "outlined",
type: "number",
min: "1",
max: "300",
hint: "请求的超时时间",
"persistent-hint": "",
rules: [v => v > 0 || '超时时间必须大于0']
}, {
"prepend-inner": _withCtx(() => [
_createVNode(_component_v_icon, { color: "warning" }, {
default: _withCtx(() => _cache[23] || (_cache[23] = [
_createTextVNode("mdi-timer")
])),
_: 1
})
]),
_: 1
}, 8, ["modelValue", "rules"])
]),
_: 1
}),
_createVNode(_component_v_col, {
cols: "12",
md: "6"
}, {
default: _withCtx(() => [
_createVNode(_component_v_text_field, {
modelValue: config.retry_times,
"onUpdate:modelValue": _cache[12] || (_cache[12] = $event => ((config.retry_times) = $event)),
modelModifiers: { number: true },
label: "重试次数",
variant: "outlined",
type: "number",
min: "0",
max: "10",
hint: "失败时的重试次数",
"persistent-hint": "",
rules: [v => v >= 0 || '重试次数不能为负数']
}, {
"prepend-inner": _withCtx(() => [
_createVNode(_component_v_icon, { color: "info" }, {
default: _withCtx(() => _cache[24] || (_cache[24] = [
_createTextVNode("mdi-refresh")
])),
_: 1
})
]),
_: 1
}, 8, ["modelValue", "rules"])
]),
_: 1
})
]),
_: 1
}),
_createVNode(_component_v_expansion_panels, {
variant: "accordion",
class: "mt-4"
}, {
default: _withCtx(() => [
_createVNode(_component_v_expansion_panel, null, {
default: _withCtx(() => [
_createVNode(_component_v_expansion_panel_title, null, {
default: _withCtx(() => [
_createVNode(_component_v_icon, { class: "mr-2" }, {
default: _withCtx(() => _cache[25] || (_cache[25] = [
_createTextVNode("mdi-cog")
])),
_: 1
}),
_cache[26] || (_cache[26] = _createTextVNode(" 高级选项 "))
]),
_: 1
}),
_createVNode(_component_v_expansion_panel_text, null, {
default: _withCtx(() => [
_createVNode(_component_v_row, null, {
default: _withCtx(() => [
_createVNode(_component_v_col, { cols: "12" }, {
default: _withCtx(() => [
_createVNode(_component_v_switch, {
modelValue: config.auto_update_subscriptions,
"onUpdate:modelValue": _cache[13] || (_cache[13] = $event => ((config.auto_update_subscriptions) = $event)),
label: "自动更新订阅",
color: "primary",
inset: "",
hint: "定期自动更新Clash订阅配置"
}, null, 8, ["modelValue"])
]),
_: 1
}),
_createVNode(_component_v_col, { cols: "12" }, {
default: _withCtx(() => [
_createVNode(_component_v_text_field, {
modelValue: config.ruleset_prefix,
"onUpdate:modelValue": _cache[14] || (_cache[14] = $event => ((config.ruleset_prefix) = $event)),
label: "规则集前缀",
variant: "outlined",
placeholder: "📂<-",
hint: "为生成的规则集添加前缀",
"persistent-hint": ""
}, {
"prepend-inner": _withCtx(() => [
_createVNode(_component_v_icon, { color: "info" }, {
default: _withCtx(() => _cache[27] || (_cache[27] = [
_createTextVNode("mdi-prefix")
])),
_: 1
})
]),
_: 1
}, 8, ["modelValue"])
]),
_: 1
})
]),
_: 1
})
]),
_: 1
})
]),
_: 1
})
]),
_: 1
})
]),
_: 1
}, 8, ["modelValue"])
]),
_: 1
}),
_createVNode(_component_v_card_actions, null, {
default: _withCtx(() => [
_createVNode(_component_v_btn, {
color: "primary",
onClick: notifySwitch
}, {
default: _withCtx(() => [
_createVNode(_component_v_icon, { left: "" }, {
default: _withCtx(() => _cache[33] || (_cache[33] = [
_createTextVNode("mdi-view-dashboard-edit")
])),
_: 1
}),
_cache[34] || (_cache[34] = _createTextVNode(" 规则 "))
]),
_: 1
}),
_createVNode(_component_v_btn, {
color: "secondary",
onClick: resetForm
}, {
default: _withCtx(() => _cache[35] || (_cache[35] = [
_createTextVNode("重置")
])),
_: 1
}),
_createVNode(_component_v_btn, {
color: "info",
onClick: testConnection,
loading: testing.value
}, {
default: _withCtx(() => _cache[36] || (_cache[36] = [
_createTextVNode("测试连接")
])),
_: 1
}, 8, ["loading"]),
_createVNode(_component_v_spacer),
_createVNode(_component_v_btn, {
color: "primary",
disabled: !isFormValid.value,
onClick: saveConfig,
loading: saving.value
}, {
default: _withCtx(() => _cache[37] || (_cache[37] = [
_createTextVNode(" 保存配置 ")
])),
_: 1
}, 8, ["disabled", "loading"])
]),
_: 1
}),
(testResult.show)
? (_openBlock(), _createBlock(_component_v_alert, {
key: 0,
type: testResult.success ? 'success' : 'error',
variant: "tonal",
closable: "",
class: "ma-4 mt-0",
"onClick:close": _cache[16] || (_cache[16] = $event => (testResult.show = false))
}, {
default: _withCtx(() => [
_createElementVNode("div", _hoisted_2, [
_createVNode(_component_v_icon, { class: "mr-2" }, {
default: _withCtx(() => [
_createTextVNode(_toDisplayString(testResult.success ? 'mdi-check-circle' : 'mdi-alert-circle'), 1)
]),
_: 1
}),
_createElementVNode("div", null, [
_createElementVNode("div", _hoisted_3, _toDisplayString(testResult.title), 1),
_createElementVNode("div", _hoisted_4, _toDisplayString(testResult.message), 1)
])
])
]),
_: 1
}, 8, ["type"]))
: _createCommentVNode("", true)
]),
_: 1
})
]))
}
}
};
const ConfigComponent = /*#__PURE__*/_export_sfc(_sfc_main, [['__scopeId',"data-v-0e64dae0"]]);
export { ConfigComponent as default };

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -2,14 +2,14 @@ const currentImports = {};
const exportSet = new Set(['Module', '__esModule', 'default', '_export_sfc']);
let moduleMap = {
"./Page":()=>{
dynamicLoadingCss(["__federation_expose_Page-Bl7XNZ7k.css"], false, './Page');
return __federation_import('./__federation_expose_Page-DlQgf7u6.js').then(module =>Object.keys(module).every(item => exportSet.has(item)) ? () => module.default : () => module)},
dynamicLoadingCss(["__federation_expose_Page-BiV11X52.css"], false, './Page');
return __federation_import('./__federation_expose_Page-DSmFC_QV.js').then(module =>Object.keys(module).every(item => exportSet.has(item)) ? () => module.default : () => module)},
"./Config":()=>{
dynamicLoadingCss(["__federation_expose_Config-DXzIavcD.css"], false, './Config');
return __federation_import('./__federation_expose_Config-C3BpNVeC.js').then(module =>Object.keys(module).every(item => exportSet.has(item)) ? () => module.default : () => module)},
dynamicLoadingCss(["__federation_expose_Config-C_eVGIzn.css"], false, './Config');
return __federation_import('./__federation_expose_Config-BK6LRC9E.js').then(module =>Object.keys(module).every(item => exportSet.has(item)) ? () => module.default : () => module)},
"./Dashboard":()=>{
dynamicLoadingCss([], false, './Dashboard');
return __federation_import('./__federation_expose_Dashboard-BkyO-3pr.js').then(module =>Object.keys(module).every(item => exportSet.has(item)) ? () => module.default : () => module)},};
return __federation_import('./__federation_expose_Dashboard-DKtydfsT.js').then(module =>Object.keys(module).every(item => exportSet.has(item)) ? () => module.default : () => module)},};
const seen = {};
const dynamicLoadingCss = (cssFilePaths, dontAppendStylesToHead, exposeItemName) => {
const metaUrl = import.meta.url;