Merge pull request #786 from wumode/clashruleprovider

This commit is contained in:
jxxghp
2025-05-29 19:12:38 +08:00
committed by GitHub
19 changed files with 99043 additions and 10 deletions

BIN
icons/Mihomo_Meta_A.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View File

@@ -425,15 +425,28 @@
"name": "IMDb源",
"description": "让探索支持IMDb数据源。",
"labels": "探索",
"version": "1.3",
"version": "1.3.1",
"icon": "IMDb_IOS-OSX_App.png",
"author": "wumode",
"level": 1,
"history": {
"v1.3.1": "修复按日期排序错误",
"v1.3": "优化网络连接",
"v1.2": "推荐热门纪录片",
"v1.1": "推荐支持IMDB数据源; 优化海报尺寸,减少卡顿",
"v1.0": "探索支持IMDb数据源"
}
},
"ClashRuleProvider": {
"name": "Clash Rule Provider",
"description": "随时为Clash添加一些额外的规则。",
"labels": "工具",
"version": "0.1.0",
"icon": "Mihomo_Meta_A.png",
"author": "wumode",
"level": 1,
"history": {
"v0.1.0": "新增ClashRuleProvider"
}
}
}

View File

@@ -0,0 +1,625 @@
import requests
import re
from typing import Any, Optional, List, Dict, Tuple, Union
import time
import yaml
import hashlib
from fastapi import Body, Response
from datetime import datetime, timedelta
import pytz
from apscheduler.schedulers.background import BackgroundScheduler
from cachetools import cached, TTLCache
from apscheduler.triggers.cron import CronTrigger
from app.core.config import settings
from app.core.event import eventmanager, Event
from app.log import logger
from app.plugins import _PluginBase
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
class ClashRuleProvider(_PluginBase):
# 插件名称
plugin_name = "Clash Rule Provider"
# 插件描述
plugin_desc = "随时为Clash添加一些额外的规则。"
# 插件图标
plugin_icon = ("https://raw.githubusercontent.com/wumode/MoviePilot-Plugins/"
"refs/heads/imdbsource_assets/icons/Mihomo_Meta_A.png")
# 插件版本
plugin_version = "0.1.0"
# 插件作者
plugin_author = "wumode"
# 作者主页
author_url = "https://github.com/wumode"
# 插件配置项ID前缀
plugin_config_prefix = "clashruleprovider_"
# 加载顺序
plugin_order = 99
# 可使用的用户级别
auth_level = 1
# 插件配置
# 启用插件
_enabled = False
_proxy = False
_notify = False
# 订阅链接
_sub_links = []
# Clash 面板 URL
_clash_dashboard_url = None
# Clash 面板密钥
_clash_dashboard_secret = None
# MoviePilot URL
_movie_pilot_url = None
_cron = ''
_timeout = 10
_retry_times = 3
_filter_keywords = []
_auto_update_subscriptions = True
_ruleset_prefix = '📂<-'
# 插件数据
_clash_config = None
_top_rules: List[str] = []
_ruleset_rules: List[str] = []
_rule_provider: Dict[str, Any] = {}
_subscription_info = {}
_ruleset_names: Dict[str, str] = {}
# protected variables
_clash_rule_parser = None
_ruleset_rule_parser = None
_custom_rule_sets = None
_scheduler: Optional[BackgroundScheduler] = None
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._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._ruleset_names = self.get_data("ruleset_names") or {}
if config:
self._enabled = config.get("enabled")
self._proxy = config.get("proxy")
self._notify = config.get("notify"),
self._sub_links = config.get("sub_links")
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] == '/':
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._filter_keywords = config.get("filter_keywords")
self._ruleset_prefix = config.get("ruleset_prefix", "Custom_")
self._auto_update_subscriptions = config.get("auto_update_subscriptions")
self._clash_rule_parser = ClashRuleParser()
self._ruleset_rule_parser = ClashRuleParser()
if self._enabled:
self.__parse_config()
self._scheduler = BackgroundScheduler(timezone=settings.TZ)
self._scheduler.start()
def get_state(self) -> bool:
return self._enabled
@staticmethod
def get_command() -> List[Dict[str, Any]]:
pass
def get_api(self) -> List[Dict[str, Any]]:
return [
{
"path": "/connectivity",
"endpoint": self.test_connectivity,
"methods": ["POST"],
"auth": "bear",
"summary": "测试连接",
"description": "测试连接"
},
{
"path": "/clash_outbound",
"endpoint": self.get_clash_outbound,
"methods": ["GET"],
"auth": "bear",
"summary": "clash outbound",
"description": "clash outbound"
},
{
"path": "/status",
"endpoint": self.get_status,
"methods": ["GET"],
"auth": "bear",
"summary": "stated",
"description": "state"
},
{
"path": "/rules",
"endpoint": self.get_rules,
"methods": ["GET"],
"auth": "bear",
"summary": "clash rules",
"description": "clash rules"
},
{
"path": "/rules",
"endpoint": self.update_rules,
"methods": ["PUT"],
"auth": "bear",
"summary": "clash rules",
"description": "clash rules"
},
{
"path": "/reorder-rules",
"endpoint": self.reorder_rules,
"methods": ["PUT"],
"auth": "bear",
"summary": "clash rules",
"description": "clash rules"
},
{
"path": "/rule",
"endpoint": self.update_rule,
"methods": ["PUT"],
"auth": "bear",
"summary": "clash rules",
"description": "clash rules"
},
{
"path": "/rule",
"endpoint": self.add_rule,
"methods": ["POSt"],
"auth": "bear",
"summary": "clash rules",
"description": "clash rules"
},
{
"path": "/rule",
"endpoint": self.delete_rule,
"methods": ["DELETE"],
"auth": "bear",
"summary": "clash rules",
"description": "clash rules"
},
{
"path": "/subscription",
"endpoint": self.get_subscription,
"methods": ["GET"],
"auth": "bear",
"summary": "clash rules",
"description": "clash rules"
},
{
"path": "/subscription",
"endpoint": self.update_subscription,
"methods": ["PUT"],
"auth": "bear",
"summary": "update clash rules",
"description": "update clash rules"
},
{
"path": "/rule_providers",
"endpoint": self.get_rule_providers,
"methods": ["GET"],
"auth": "bear",
"summary": "update rule providers",
"description": "update rule providers"
},
{
"path": "/ruleset",
"endpoint": self.get_ruleset,
"methods": ["GET"],
"summary": "update rule providers",
"description": "update rule providers"
},
{
"path": "/config",
"endpoint": self.get_clash_config,
"methods": ["GET"],
"summary": "update rule providers",
"description": "update rule providers"
}
]
def get_render_mode(self) -> Tuple[str, str]:
"""
获取插件渲染模式
:return: 1、渲染模式支持vue/vuetify默认vuetify
:return: 2、组件路径默认 dist/assets
"""
return "vue", "dist/assets"
def get_form(self) -> Tuple[List[dict], Dict[str, Any]]:
"""
拼装插件配置页面需要返回两块数据1、页面配置2、数据结构
"""
return [], {}
def get_page(self) -> List[dict]:
return []
def stop_service(self):
"""
退出插件
"""
pass
def get_service(self) -> List[Dict[str, Any]]:
if self.get_state() and self._auto_update_subscriptions:
return [{
"id": "ClashRuleProvider",
"name": "Clash Rule Provider 服务",
"trigger": CronTrigger.from_crontab(self._cron),
"func": self.update_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.save_data('clash_config', self._clash_config)
self.save_data('ruleset_rules', self._ruleset_rules)
self.save_data('top_rules', self._top_rules)
self.save_data('subscription_info', self._subscription_info)
self.save_data('ruleset_names', self._ruleset_names)
self.save_data('rule_provider', self._rule_provider)
def __parse_config(self):
if not self._top_rules:
return
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]:
if not self._enabled:
return {"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"}
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"}
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": "测试连接成功"}
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):
outbound = self.clash_outbound(self._clash_config)
return {"success": True, "message": None, "data": {"outbound": outbound}}
def get_status(self):
rule_size = len(self._clash_config.get("rules", [])) if self._clash_config else 0
return {"success": True, "message": "",
"data": {"state": self._enabled,
"ruleset_prefix": self._ruleset_prefix,
"clash": {"rule_size": rule_size},
"subscription_info": self._subscription_info,
"sub_url": f"{self._movie_pilot_url}/api/v1/plugin/ClashRuleProvider/config?"
f"apikey={settings.API_TOKEN}"}}
def get_clash_config(self):
config = self.clash_config()
if not config:
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"]}; '
f'total={self._subscription_info["total"]}; '
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]:
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()}}
def delete_rule(self, params: dict = Body(...)):
if not self._enabled:
return {"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}
def reorder_rules(self, params: Dict[str, Any]):
if not self._enabled:
return {"success": False, "message": ""}
moved_priority = params.get('moved_priority')
target_priority = params.get('target_priority')
try:
if params.get('type') == 'ruleset':
self.__reorder_rules(self._ruleset_rule_parser, moved_priority, target_priority)
self.__add_notification_job(f"{self._ruleset_prefix}{params.get('rule_data').get('action')}")
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}
def update_rules(self, params: Dict[str, Any]):
if not self._enabled:
return {"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}
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)
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)
return {"success": bool(res), "message": None}
def add_rule(self, params: Dict[str, Any]) -> Dict[str, Any]:
if not self._enabled:
return {"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}
def get_subscription(self):
if not self._sub_links:
return None
return {"success": True, "message": None, "data": {"url": self._sub_links[0]}}
def update_subscription(self, params: Dict[str, Any]):
if not self._enabled:
return {"success": False, "message": ""}
url = params.get('url')
if not url:
return {"success": False, "message": "missing params"}
res = self.update_subscription_service()
if not res:
return {"success": True, "message": f"订阅链接 {self._sub_links[0]} 更新失败"}
return {"success": True, "message": "订阅更新成功"}
def get_rule_providers(self):
return {"success": True, "message": None, "data": self.rule_providers()}
@staticmethod
def clash_outbound(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")])
return outbound
def rule_providers(self) -> Optional[Dict[str, Any]]:
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
rule_providers[key] = value
return rule_providers
def __update_rules(self, rules: List[Dict[str, Any]], rule_parser: ClashRuleParser):
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"))
self.__save_data()
def __reorder_rules(self, rule_parser: ClashRuleParser, moved_priority, target_priority):
rule_parser.reorder_rules(moved_priority, target_priority)
self.__save_data()
def __get_ruleset(self, ruleset: str) -> List[str]:
if ruleset.startswith(self._ruleset_prefix):
action = ruleset[len(self._ruleset_prefix):]
else:
return []
try:
action_enum = Action(action.upper())
final_action = action_enum
except ValueError:
final_action = action
rules = self._ruleset_rule_parser.filter_rules_by_action(final_action)
res = []
for rule in rules:
res.append(rule.condition_string())
return res
def __insert_ruleset(self):
outbounds = []
for rule in self._ruleset_rule_parser.rules:
action_str = f"{rule.action.value}" if isinstance(rule.action, Action) else rule.action
if action_str not in outbounds:
outbounds.append(action_str)
self._clash_rule_parser.remove_rules(lambda r: r.rule_type == RuleType.RULE_SET and
r.payload.startswith(self._ruleset_prefix))
for outbound in outbounds:
clash_rule = ClashRuleParser.parse_rule_line(f"RULE-SET,{self._ruleset_prefix}{outbound},{outbound}")
if not self._clash_rule_parser.has_rule(clash_rule):
self._clash_rule_parser.insert_rule_at_priority(clash_rule, 0)
def update_rule_by_priority(self, rule: Dict[str, Any], rule_parser: ClashRuleParser) -> bool:
if not isinstance(rule.get("priority"), int):
return False
clash_rule = ClashRuleParser.parse_rule_dict(rule)
if not clash_rule:
return False
res = rule_parser.update_rule_at_priority(clash_rule, rule.get("priority"))
self.__save_data()
return res
def add_rule_by_priority(self, rule: Dict[str, Any], rule_parser: ClashRuleParser) -> bool:
if not isinstance(rule.get("priority"), int):
return False
try:
clash_rule = self._clash_rule_parser.parse_rule_dict(rule)
except ValueError:
logger.warn(f"无效的输入规则: {rule}")
return False
if not clash_rule:
return False
rule_parser.insert_rule_at_priority(clash_rule, rule.get("priority"))
self.__save_data()
return True
def delete_rule_by_priority(self, priority: int, rule_parser: ClashRuleParser
) -> Optional[Union[ClashRule, LogicRule, MatchRule]]:
if not isinstance(priority, int):
return None
res = rule_parser.remove_rule_at_priority(priority)
self.__save_data()
return res
@eventmanager.register(EventType.PluginAction)
def update_subscription_service(self) -> bool:
if not self._sub_links:
return False
url = self._sub_links[0]
ret = RequestUtils(accept_type="text/html",
proxies=settings.PROXY if self._proxy else None
).get_res(url)
if not ret:
return False
try:
rs = yaml.load(ret.content, Loader=yaml.FullLoader)
self._clash_config = self.__remove_nodes_by_keywords(rs)
except Exception as e:
logger.error(f"解析配置出错: {e}")
return False
if 'Subscription-Userinfo' in ret.headers:
matches = re.findall(r'(\w+)=(\d+)', ret.headers['Subscription-Userinfo'])
variables = {key: int(value) for key, value in matches}
self._subscription_info['download'] = variables['download']
self._subscription_info['upload'] = variables['upload']
self._subscription_info['total'] = variables['total']
self._subscription_info['expire'] = variables['expire']
self._subscription_info["last_update"] = int(time.time())
self.save_data('subscription_info', self._subscription_info)
self.save_data('clash_config', self._clash_config)
return True
def notify_clash(self, ruleset: str):
url = f'{self._clash_dashboard_url}/providers/rules/{ruleset}'
RequestUtils(content_type="application/json",
headers={"authorization": f"Bearer {self._clash_dashboard_secret}"}
).put(url)
def __add_notification_job(self, ruleset: str):
if ruleset in self._rule_provider:
self._scheduler.add_job(self.notify_clash, "date",
run_date=datetime.now(tz=pytz.timezone(settings.TZ)) + timedelta(seconds=30),
args=[ruleset],
id='CRP-notify-clash',
replace_existing=True
)
def __remove_nodes_by_keywords(self, clash_config: Dict[str, Any]) -> Dict[str, Any]:
removed_proxies = []
proxies = []
for proxy in clash_config.get("proxies", []):
has_keywords = bool(len([x for x in self._filter_keywords if x in proxy.get("name", '')]))
if has_keywords:
removed_proxies.append(proxy.get("name"))
else:
proxies.append(proxy)
if proxies:
clash_config["proxies"] = proxies
else:
logger.warn(f"关键词过滤后无可用节点,跳过过滤")
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]]:
if not self._clash_config:
return
self.__insert_ruleset()
self._top_rules = self._clash_rule_parser.to_string()
clash_config = self._clash_config.copy()
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", [])
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
path_name = hashlib.sha256(action_str.encode('utf-8')).hexdigest()[:10]
self._ruleset_names[path_name] = r.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"):
clash_config['rule-providers'].update(self._rule_provider)
else:
clash_config['rule-providers'] = self._rule_provider
for key, item in self._ruleset_names.items():
if item not in clash_config['rule-providers']:
del self._ruleset_names[key]
self.save_data('ruleset_names', self._ruleset_names)
self.save_data('rule_provider', self._rule_provider)
return clash_config

View File

@@ -0,0 +1,486 @@
import re
from typing import List, Dict, Any, Optional, Union, Callable
from dataclasses import dataclass
from enum import Enum
class RuleType(Enum):
"""Enumeration of all supported Clash rule types"""
DOMAIN = "DOMAIN"
DOMAIN_SUFFIX = "DOMAIN-SUFFIX"
DOMAIN_KEYWORD = "DOMAIN-KEYWORD"
DOMAIN_REGEX = "DOMAIN-REGEX"
GEOSITE = "GEOSITE"
IP_CIDR = "IP-CIDR"
IP_CIDR6 = "IP-CIDR6"
IP_SUFFIX = "IP-SUFFIX"
IP_ASN = "IP-ASN"
GEOIP = "GEOIP"
SRC_GEOIP = "SRC-GEOIP"
SRC_IP_ASN = "SRC-IP-ASN"
SRC_IP_CIDR = "SRC-IP-CIDR"
SRC_IP_SUFFIX = "SRC-IP-SUFFIX"
DST_PORT = "DST-PORT"
SRC_PORT = "SRC-PORT"
IN_PORT = "IN-PORT"
IN_TYPE = "IN-TYPE"
IN_USER = "IN-USER"
IN_NAME = "IN-NAME"
PROCESS_PATH = "PROCESS-PATH"
PROCESS_PATH_REGEX = "PROCESS-PATH-REGEX"
PROCESS_NAME = "PROCESS-NAME"
PROCESS_NAME_REGEX = "PROCESS-NAME-REGEX"
UID = "UID"
NETWORK = "NETWORK"
DSCP = "DSCP"
RULE_SET = "RULE-SET"
AND = "AND"
OR = "OR"
NOT = "NOT"
SUB_RULE = "SUB-RULE"
MATCH = "MATCH"
class Action(Enum):
"""Enumeration of rule actions"""
DIRECT = "DIRECT"
REJECT = "REJECT"
REJECT_DROP = "REJECT-DROP"
PASS = "PASS"
COMPATIBLE = "COMPATIBLE"
@dataclass
class ClashRule:
"""Represents a parsed Clash routing rule"""
rule_type: RuleType
payload: str
action: Union[Action, str] # Can be Action enum or custom proxy group name
additional_params: Optional[List[str]] = None
raw_rule: str = ""
priority: int = 0
def __post_init__(self):
if self.additional_params is None:
self.additional_params = []
def condition_string(self) -> str:
return f"{self.rule_type.value},{self.payload}"
@dataclass
class LogicRule:
"""Represents a logic rule (AND, OR, NOT)"""
logic_type: RuleType
conditions: List[Union[ClashRule, 'LogicRule']]
action: Union[Action, str]
raw_rule: str = ""
priority: int = 0
def condition_string(self) -> str:
conditions_str = ','.join([f"({c.condition_string()})" for c in self.conditions])
return f"{self.logic_type.value},({conditions_str})"
@dataclass
class MatchRule:
"""Represents a match rule"""
action: Union[Action, str]
raw_rule: str = ""
priority: int = 0
rule_type: RuleType = RuleType.MATCH
@staticmethod
def condition_string() -> str:
return "MATCH"
class ClashRuleParser:
"""Parser for Clash routing rules"""
def __init__(self):
self.rules: List[Union[ClashRule, LogicRule, MatchRule]] = []
@staticmethod
def parse_rule_line(line: str) -> Optional[Union[ClashRule, LogicRule, MatchRule]]:
"""Parse a single rule line"""
line = line.strip()
try:
# Handle logic rules (AND, OR, NOT)
if line.startswith(('AND,', 'OR,', 'NOT,')):
return ClashRuleParser._parse_logic_rule(line)
elif line.startswith('MATCH'):
return ClashRuleParser._parse_match_rule(line)
# Handle regular rules
return ClashRuleParser._parse_regular_rule(line)
except Exception as e:
print(f"Error parsing rule '{line}': {e}")
return None
@staticmethod
def parse_rule_dict(clash_rule: Dict[str, Any]) -> Optional[Union[ClashRule, LogicRule, MatchRule]]:
if not clash_rule:
return None
if clash_rule.get("type") in ('AND', 'OR', 'NOT'):
conditions = clash_rule.get("conditions")
if not conditions:
return None
conditions_str = ''
for condition in conditions:
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)
elif clash_rule.get("type") == 'MATCH':
raw_rule = f"{clash_rule.get('type')},{clash_rule.get('action')}"
return 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)
@staticmethod
def _parse_match_rule(line: str) -> MatchRule:
parts = line.split(',')
if len(parts) < 2:
raise ValueError(f"Invalid rule format: {line}")
action = parts[1]
# Validate rule type
try:
action_enum = Action(action.upper())
final_action = action_enum
except ValueError:
final_action = action
return MatchRule(
action=final_action,
raw_rule=line
)
@staticmethod
def _parse_regular_rule(line: str) -> ClashRule:
"""Parse a regular (non-logic) rule"""
parts = line.split(',')
if len(parts) < 3:
raise ValueError(f"Invalid rule format: {line}")
rule_type_str = parts[0].upper()
payload = parts[1]
action = parts[2]
if not payload or not rule_type_str:
raise ValueError(f"Invalid rule format: {line}")
additional_params = parts[3:] if len(parts) > 3 else []
# Validate rule type
try:
rule_type = RuleType(rule_type_str)
except ValueError:
raise ValueError(f"Unknown rule type: {rule_type_str}")
# Try to convert action to enum, otherwise keep as string (custom proxy group)
try:
action_enum = Action(action.upper())
final_action = action_enum
except ValueError:
final_action = action
return ClashRule(
rule_type=rule_type,
payload=payload,
action=final_action,
additional_params=additional_params,
raw_rule=line
)
@staticmethod
def _parse_logic_rule(line: str) -> LogicRule:
"""Parse a logic rule (AND, OR, NOT)"""
# Extract logic type
logic_rule_match = re.match(r'^(AND|OR|NOT),\((.+)\),([^,]+)$', line)
if not logic_rule_match:
raise ValueError(f"Cannot extract action from logic rule: {line}")
logic_type_str = logic_rule_match.group(1).upper()
logic_type = RuleType(logic_type_str)
action = logic_rule_match.group(3)
# Try to convert action to enum
try:
action_enum = Action(action.upper())
final_action = action_enum
except ValueError:
final_action = action
conditions_str = logic_rule_match.group(2)
conditions = ClashRuleParser._parse_logic_conditions(conditions_str)
return LogicRule(
logic_type=logic_type,
conditions=conditions,
action=final_action,
raw_rule=line
)
@staticmethod
def _parse_logic_conditions(conditions_str: str) -> List[ClashRule]:
"""Parse conditions within logic rules"""
conditions = []
# Simple parser for conditions like (DOMAIN,baidu.com),(NETWORK,UDP)
# This is a basic implementation - more complex nested logic would need a proper parser
condition_pattern = r'\(([^,]+),([^)]+)\)'
matches = re.findall(condition_pattern, conditions_str)
for rule_type_str, payload in matches:
try:
rule_type = RuleType(rule_type_str.upper())
condition = ClashRule(
rule_type=rule_type,
payload=payload,
action="", # Logic conditions don't have actions
raw_rule=f"{rule_type_str},{payload}"
)
conditions.append(condition)
except ValueError:
print(f"Unknown rule type in logic condition: {rule_type_str}")
return conditions
def parse_rules(self, rules_text: str) -> List[Union[ClashRule, LogicRule, MatchRule]]:
"""Parse multiple rules from text, preserving order and priority"""
self.rules = []
lines = rules_text.strip().split('\n')
priority = 0
for line in lines:
rule = self.parse_rule_line(line)
if rule:
rule.priority = priority # Assign priority based on position
self.rules.append(rule)
priority += 1
return self.rules
def parse_rules_from_list(self, rules_list: List[str]) -> List[Union[ClashRule, LogicRule, MatchRule]]:
"""Parse rules from a list of rule strings, preserving order and priority"""
self.rules = []
for priority, rule_str in enumerate(rules_list):
rule = self.parse_rule_line(rule_str)
if rule:
rule.priority = priority # Assign priority based on list position
self.rules.append(rule)
return self.rules
def validate_rule(self, rule: ClashRule) -> bool:
"""Validate a parsed rule"""
try:
# Basic validation based on rule type
if rule.rule_type in [RuleType.IP_CIDR, RuleType.IP_CIDR6]:
# Validate CIDR format
return '/' in rule.payload
elif rule.rule_type == RuleType.DST_PORT or rule.rule_type == RuleType.SRC_PORT:
# Validate port number/range
return rule.payload.isdigit() or '-' in rule.payload
elif rule.rule_type == RuleType.NETWORK:
# Validate network type
return rule.payload.lower() in ['tcp', 'udp']
elif rule.rule_type == RuleType.DOMAIN_REGEX or rule.rule_type == RuleType.PROCESS_PATH_REGEX:
# Try to compile regex
re.compile(rule.payload)
return True
return True
except Exception:
return False
def to_string(self) -> List[str]:
result = []
for rule in self.rules:
result.append(rule.raw_rule)
return result
def to_dict(self) -> List[Dict[str, Any]]:
"""Convert parsed rules to dictionary format"""
result = []
for rule in self.rules:
if isinstance(rule, ClashRule):
rule_dict = {
'type': rule.rule_type.value,
'payload': rule.payload,
'action': rule.action.value if isinstance(rule.action, Action) else rule.action,
'additional_params': rule.additional_params,
'priority': rule.priority,
'raw': rule.raw_rule
}
result.append(rule_dict)
elif isinstance(rule, LogicRule):
conditions_dict = []
for condition in rule.conditions:
if isinstance(condition, ClashRule):
conditions_dict.append({
'type': condition.rule_type.value,
'payload': condition.payload
})
rule_dict = {
'type': rule.logic_type.value,
'conditions': conditions_dict,
'action': rule.action.value if isinstance(rule.action, Action) else rule.action,
'priority': rule.priority,
'raw': rule.raw_rule
}
result.append(rule_dict)
elif isinstance(rule, MatchRule):
rule_dict = {
'type': 'MATCH',
'action': rule.action.value if isinstance(rule.action, Action) else rule.action,
'priority': rule.priority,
'raw': rule.raw_rule
}
result.append(rule_dict)
return result
def get_rules_by_priority(self) -> List[Union[ClashRule, LogicRule, MatchRule]]:
"""Get rules sorted by priority (highest priority first)"""
return sorted(self.rules, key=lambda rule: rule.priority)
def append_rule(self, rule: Union[ClashRule, LogicRule, MatchRule]) -> None:
max_priority = max(rule.priority for rule in self.rules) if len(self.rules) else 0
rule.priority = max_priority + 1
self.rules.append(rule)
# Re-sort rules to maintain order
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
for existing_rule in self.rules:
if existing_rule.priority >= priority:
existing_rule.priority += 1
rule.priority = priority
self.rules.append(rule)
# Re-sort rules to maintain order
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
def remove_rule_at_priority(self, priority: int) -> Optional[Union[ClashRule, LogicRule, MatchRule]]:
"""Remove rule at specific priority and adjust remaining priorities"""
rule_to_remove = None
for rule in self.rules:
if rule.priority == priority:
rule_to_remove = rule
break
if rule_to_remove:
self.rules.remove(rule_to_remove)
# Adjust priorities of remaining rules
for rule in self.rules:
if rule.priority > priority:
rule.priority -= 1
return rule_to_remove
return None
def remove_rules(self, condition: Callable[[Union[ClashRule, LogicRule, MatchRule]], bool]):
"""Remove rules by lambda"""
i = 0
while i < len(self.rules):
if condition(self.rules[i]):
priority = self.rules[i].priority
for rule in self.rules:
if rule.priority > priority:
rule.priority -= 1
del self.rules[i]
else:
i += 1
def move_rule_priority(self, from_priority: int, to_priority: int) -> bool:
"""Move a rule from one priority position to another"""
rule_to_move = None
for rule in self.rules:
if rule.priority == from_priority:
rule_to_move = rule
break
if not rule_to_move:
return False
# Remove rule temporarily
self.remove_rule_at_priority(from_priority)
# Insert at new priority
self.insert_rule_at_priority(rule_to_move, to_priority)
return True
def filter_rules_by_type(self, rule_type: RuleType) -> List[ClashRule]:
"""Filter rules by type"""
return [rule for rule in self.rules
if isinstance(rule, ClashRule) and rule.rule_type == rule_type]
def filter_rules_by_action(self, action: Union[Action, str]) -> List[Union[ClashRule, LogicRule, MatchRule]]:
"""Filter rules by action"""
return [rule for rule in self.rules if rule.action == action]
def has_rule(self, clash_rule: Union[ClashRule, LogicRule, MatchRule]) -> bool:
for rule in self.rules:
if rule.rule_type == clash_rule.rule_type and rule.action == clash_rule.action \
and rule.payload == clash_rule.payload:
return True
return False
def reorder_rules(
self,
moved_rule_priority: int,
target_priority: int,
):
"""
Reorder rules
:param moved_rule_priority: 被移动规则的原始优先级
:param target_priority: 目标位置的优先级
"""
moved_index = next(i for i, r in enumerate(self.rules) if r.priority == moved_rule_priority)
target_index = next(
(i for i, r in enumerate(self.rules) if r.priority == target_priority),
len(self.rules)
)
# 直接修改被移动规则的优先级
moved_rule = self.rules[moved_index]
moved_rule.priority = target_priority
if moved_index < target_index:
# 向后移动:原位置到目标位置之间的规则优先级 -1
for i in range(moved_index + 1, target_index + 1):
self.rules[i].priority -= 1
elif moved_index > target_index:
# 向前移动:目标位置到原位置之间的规则优先级 +1
for i in range(target_index, moved_index):
self.rules[i].priority += 1
self.rules.sort(key=lambda x: x.priority)

View File

@@ -0,0 +1,828 @@
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

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

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,48 @@
.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;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,418 @@
const buildIdentifier = "[0-9A-Za-z-]+";
const build = `(?:\\+(${buildIdentifier}(?:\\.${buildIdentifier})*))`;
const numericIdentifier = "0|[1-9]\\d*";
const numericIdentifierLoose = "[0-9]+";
const nonNumericIdentifier = "\\d*[a-zA-Z-][a-zA-Z0-9-]*";
const preReleaseIdentifierLoose = `(?:${numericIdentifierLoose}|${nonNumericIdentifier})`;
const preReleaseLoose = `(?:-?(${preReleaseIdentifierLoose}(?:\\.${preReleaseIdentifierLoose})*))`;
const preReleaseIdentifier = `(?:${numericIdentifier}|${nonNumericIdentifier})`;
const preRelease = `(?:-(${preReleaseIdentifier}(?:\\.${preReleaseIdentifier})*))`;
const xRangeIdentifier = `${numericIdentifier}|x|X|\\*`;
const xRangePlain = `[v=\\s]*(${xRangeIdentifier})(?:\\.(${xRangeIdentifier})(?:\\.(${xRangeIdentifier})(?:${preRelease})?${build}?)?)?`;
const hyphenRange = `^\\s*(${xRangePlain})\\s+-\\s+(${xRangePlain})\\s*$`;
const mainVersionLoose = `(${numericIdentifierLoose})\\.(${numericIdentifierLoose})\\.(${numericIdentifierLoose})`;
const loosePlain = `[v=\\s]*${mainVersionLoose}${preReleaseLoose}?${build}?`;
const gtlt = "((?:<|>)?=?)";
const comparatorTrim = `(\\s*)${gtlt}\\s*(${loosePlain}|${xRangePlain})`;
const loneTilde = "(?:~>?)";
const tildeTrim = `(\\s*)${loneTilde}\\s+`;
const loneCaret = "(?:\\^)";
const caretTrim = `(\\s*)${loneCaret}\\s+`;
const star = "(<|>)?=?\\s*\\*";
const caret = `^${loneCaret}${xRangePlain}$`;
const mainVersion = `(${numericIdentifier})\\.(${numericIdentifier})\\.(${numericIdentifier})`;
const fullPlain = `v?${mainVersion}${preRelease}?${build}?`;
const tilde = `^${loneTilde}${xRangePlain}$`;
const xRange = `^${gtlt}\\s*${xRangePlain}$`;
const comparator = `^${gtlt}\\s*(${fullPlain})$|^$`;
const gte0 = "^\\s*>=\\s*0.0.0\\s*$";
function parseRegex(source) {
return new RegExp(source);
}
function isXVersion(version) {
return !version || version.toLowerCase() === "x" || version === "*";
}
function pipe(...fns) {
return (x) => {
return fns.reduce((v, f) => f(v), x);
};
}
function extractComparator(comparatorString) {
return comparatorString.match(parseRegex(comparator));
}
function combineVersion(major, minor, patch, preRelease2) {
const mainVersion2 = `${major}.${minor}.${patch}`;
if (preRelease2) {
return `${mainVersion2}-${preRelease2}`;
}
return mainVersion2;
}
function parseHyphen(range) {
return range.replace(
parseRegex(hyphenRange),
(_range, from, fromMajor, fromMinor, fromPatch, _fromPreRelease, _fromBuild, to, toMajor, toMinor, toPatch, toPreRelease) => {
if (isXVersion(fromMajor)) {
from = "";
} else if (isXVersion(fromMinor)) {
from = `>=${fromMajor}.0.0`;
} else if (isXVersion(fromPatch)) {
from = `>=${fromMajor}.${fromMinor}.0`;
} else {
from = `>=${from}`;
}
if (isXVersion(toMajor)) {
to = "";
} else if (isXVersion(toMinor)) {
to = `<${+toMajor + 1}.0.0-0`;
} else if (isXVersion(toPatch)) {
to = `<${toMajor}.${+toMinor + 1}.0-0`;
} else if (toPreRelease) {
to = `<=${toMajor}.${toMinor}.${toPatch}-${toPreRelease}`;
} else {
to = `<=${to}`;
}
return `${from} ${to}`.trim();
}
);
}
function parseComparatorTrim(range) {
return range.replace(parseRegex(comparatorTrim), "$1$2$3");
}
function parseTildeTrim(range) {
return range.replace(parseRegex(tildeTrim), "$1~");
}
function parseCaretTrim(range) {
return range.replace(parseRegex(caretTrim), "$1^");
}
function parseCarets(range) {
return range.trim().split(/\s+/).map((rangeVersion) => {
return rangeVersion.replace(
parseRegex(caret),
(_, major, minor, patch, preRelease2) => {
if (isXVersion(major)) {
return "";
} else if (isXVersion(minor)) {
return `>=${major}.0.0 <${+major + 1}.0.0-0`;
} else if (isXVersion(patch)) {
if (major === "0") {
return `>=${major}.${minor}.0 <${major}.${+minor + 1}.0-0`;
} else {
return `>=${major}.${minor}.0 <${+major + 1}.0.0-0`;
}
} else if (preRelease2) {
if (major === "0") {
if (minor === "0") {
return `>=${major}.${minor}.${patch}-${preRelease2} <${major}.${minor}.${+patch + 1}-0`;
} else {
return `>=${major}.${minor}.${patch}-${preRelease2} <${major}.${+minor + 1}.0-0`;
}
} else {
return `>=${major}.${minor}.${patch}-${preRelease2} <${+major + 1}.0.0-0`;
}
} else {
if (major === "0") {
if (minor === "0") {
return `>=${major}.${minor}.${patch} <${major}.${minor}.${+patch + 1}-0`;
} else {
return `>=${major}.${minor}.${patch} <${major}.${+minor + 1}.0-0`;
}
}
return `>=${major}.${minor}.${patch} <${+major + 1}.0.0-0`;
}
}
);
}).join(" ");
}
function parseTildes(range) {
return range.trim().split(/\s+/).map((rangeVersion) => {
return rangeVersion.replace(
parseRegex(tilde),
(_, major, minor, patch, preRelease2) => {
if (isXVersion(major)) {
return "";
} else if (isXVersion(minor)) {
return `>=${major}.0.0 <${+major + 1}.0.0-0`;
} else if (isXVersion(patch)) {
return `>=${major}.${minor}.0 <${major}.${+minor + 1}.0-0`;
} else if (preRelease2) {
return `>=${major}.${minor}.${patch}-${preRelease2} <${major}.${+minor + 1}.0-0`;
}
return `>=${major}.${minor}.${patch} <${major}.${+minor + 1}.0-0`;
}
);
}).join(" ");
}
function parseXRanges(range) {
return range.split(/\s+/).map((rangeVersion) => {
return rangeVersion.trim().replace(
parseRegex(xRange),
(ret, gtlt2, major, minor, patch, preRelease2) => {
const isXMajor = isXVersion(major);
const isXMinor = isXMajor || isXVersion(minor);
const isXPatch = isXMinor || isXVersion(patch);
if (gtlt2 === "=" && isXPatch) {
gtlt2 = "";
}
preRelease2 = "";
if (isXMajor) {
if (gtlt2 === ">" || gtlt2 === "<") {
return "<0.0.0-0";
} else {
return "*";
}
} else if (gtlt2 && isXPatch) {
if (isXMinor) {
minor = 0;
}
patch = 0;
if (gtlt2 === ">") {
gtlt2 = ">=";
if (isXMinor) {
major = +major + 1;
minor = 0;
patch = 0;
} else {
minor = +minor + 1;
patch = 0;
}
} else if (gtlt2 === "<=") {
gtlt2 = "<";
if (isXMinor) {
major = +major + 1;
} else {
minor = +minor + 1;
}
}
if (gtlt2 === "<") {
preRelease2 = "-0";
}
return `${gtlt2 + major}.${minor}.${patch}${preRelease2}`;
} else if (isXMinor) {
return `>=${major}.0.0${preRelease2} <${+major + 1}.0.0-0`;
} else if (isXPatch) {
return `>=${major}.${minor}.0${preRelease2} <${major}.${+minor + 1}.0-0`;
}
return ret;
}
);
}).join(" ");
}
function parseStar(range) {
return range.trim().replace(parseRegex(star), "");
}
function parseGTE0(comparatorString) {
return comparatorString.trim().replace(parseRegex(gte0), "");
}
function compareAtom(rangeAtom, versionAtom) {
rangeAtom = +rangeAtom || rangeAtom;
versionAtom = +versionAtom || versionAtom;
if (rangeAtom > versionAtom) {
return 1;
}
if (rangeAtom === versionAtom) {
return 0;
}
return -1;
}
function comparePreRelease(rangeAtom, versionAtom) {
const { preRelease: rangePreRelease } = rangeAtom;
const { preRelease: versionPreRelease } = versionAtom;
if (rangePreRelease === void 0 && !!versionPreRelease) {
return 1;
}
if (!!rangePreRelease && versionPreRelease === void 0) {
return -1;
}
if (rangePreRelease === void 0 && versionPreRelease === void 0) {
return 0;
}
for (let i = 0, n = rangePreRelease.length; i <= n; i++) {
const rangeElement = rangePreRelease[i];
const versionElement = versionPreRelease[i];
if (rangeElement === versionElement) {
continue;
}
if (rangeElement === void 0 && versionElement === void 0) {
return 0;
}
if (!rangeElement) {
return 1;
}
if (!versionElement) {
return -1;
}
return compareAtom(rangeElement, versionElement);
}
return 0;
}
function compareVersion(rangeAtom, versionAtom) {
return compareAtom(rangeAtom.major, versionAtom.major) || compareAtom(rangeAtom.minor, versionAtom.minor) || compareAtom(rangeAtom.patch, versionAtom.patch) || comparePreRelease(rangeAtom, versionAtom);
}
function eq(rangeAtom, versionAtom) {
return rangeAtom.version === versionAtom.version;
}
function compare(rangeAtom, versionAtom) {
switch (rangeAtom.operator) {
case "":
case "=":
return eq(rangeAtom, versionAtom);
case ">":
return compareVersion(rangeAtom, versionAtom) < 0;
case ">=":
return eq(rangeAtom, versionAtom) || compareVersion(rangeAtom, versionAtom) < 0;
case "<":
return compareVersion(rangeAtom, versionAtom) > 0;
case "<=":
return eq(rangeAtom, versionAtom) || compareVersion(rangeAtom, versionAtom) > 0;
case void 0: {
return true;
}
default:
return false;
}
}
function parseComparatorString(range) {
return pipe(
parseCarets,
parseTildes,
parseXRanges,
parseStar
)(range);
}
function parseRange(range) {
return pipe(
parseHyphen,
parseComparatorTrim,
parseTildeTrim,
parseCaretTrim
)(range.trim()).split(/\s+/).join(" ");
}
function satisfy(version, range) {
if (!version) {
return false;
}
const parsedRange = parseRange(range);
const parsedComparator = parsedRange.split(" ").map((rangeVersion) => parseComparatorString(rangeVersion)).join(" ");
const comparators = parsedComparator.split(/\s+/).map((comparator2) => parseGTE0(comparator2));
const extractedVersion = extractComparator(version);
if (!extractedVersion) {
return false;
}
const [
,
versionOperator,
,
versionMajor,
versionMinor,
versionPatch,
versionPreRelease
] = extractedVersion;
const versionAtom = {
version: combineVersion(
versionMajor,
versionMinor,
versionPatch,
versionPreRelease
),
major: versionMajor,
minor: versionMinor,
patch: versionPatch,
preRelease: versionPreRelease == null ? void 0 : versionPreRelease.split(".")
};
for (const comparator2 of comparators) {
const extractedComparator = extractComparator(comparator2);
if (!extractedComparator) {
return false;
}
const [
,
rangeOperator,
,
rangeMajor,
rangeMinor,
rangePatch,
rangePreRelease
] = extractedComparator;
const rangeAtom = {
operator: rangeOperator,
version: combineVersion(
rangeMajor,
rangeMinor,
rangePatch,
rangePreRelease
),
major: rangeMajor,
minor: rangeMinor,
patch: rangePatch,
preRelease: rangePreRelease == null ? void 0 : rangePreRelease.split(".")
};
if (!compare(rangeAtom, versionAtom)) {
return false;
}
}
return true;
}
// eslint-disable-next-line no-undef
const moduleMap = {};
const moduleCache = Object.create(null);
async function importShared(name, shareScope = 'default') {
return moduleCache[name]
? new Promise((r) => r(moduleCache[name]))
: (await getSharedFromRuntime(name, shareScope)) || getSharedFromLocal(name)
}
async function getSharedFromRuntime(name, shareScope) {
let module = null;
if (globalThis?.__federation_shared__?.[shareScope]?.[name]) {
const versionObj = globalThis.__federation_shared__[shareScope][name];
const requiredVersion = moduleMap[name]?.requiredVersion;
const hasRequiredVersion = !!requiredVersion;
if (hasRequiredVersion) {
const versionKey = Object.keys(versionObj).find((version) =>
satisfy(version, requiredVersion)
);
if (versionKey) {
const versionValue = versionObj[versionKey];
module = await (await versionValue.get())();
} else {
console.log(
`provider support ${name}(${versionKey}) is not satisfied requiredVersion(\${moduleMap[name].requiredVersion})`
);
}
} else {
const versionKey = Object.keys(versionObj)[0];
const versionValue = versionObj[versionKey];
module = await (await versionValue.get())();
}
}
if (module) {
return flattenModule(module, name)
}
}
async function getSharedFromLocal(name) {
if (moduleMap[name]?.import) {
let module = await (await moduleMap[name].get())();
return flattenModule(module, name)
} else {
console.error(
`consumer config import=false,so cant use callback shared module`
);
}
}
function flattenModule(module, name) {
// use a shared module which export default a function will getting error 'TypeError: xxx is not a function'
if (typeof module.default === 'function') {
Object.keys(module).forEach((key) => {
if (key !== 'default') {
module.default[key] = module[key];
}
});
moduleCache[name] = module.default;
return module.default
}
if (module.default) module = Object.assign({}, module.default, module);
moduleCache[name] = module;
return module
}
export { importShared, getSharedFromLocal as importSharedLocal, getSharedFromRuntime as importSharedRuntime };

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,9 @@
const _export_sfc = (sfc, props) => {
const target = sfc.__vccOpts || sfc;
for (const [key, val] of props) {
target[key] = val;
}
return target;
};
export { _export_sfc as _ };

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,458 @@
/* 为了使测试应用更美观 */
.app-container[data-v-422baab7] {
block-size: 100vh;
inline-size: 100vw;
}
.component-preview[data-v-422baab7] {
overflow: hidden;
border: 1px solid #e0e0e0;
border-radius: 8px;
}
@supports not selector(:focus-visible) {
}
@supports not selector(:focus-visible) {
}
@supports selector(:focus-visible) {
}@supports not selector(:focus-visible) {
}@keyframes progress-circular-dash {
0% {
stroke-dasharray: 1, 200;
stroke-dashoffset: 0px;
}
50% {
stroke-dasharray: 100, 200;
stroke-dashoffset: -15px;
}
100% {
stroke-dasharray: 100, 200;
stroke-dashoffset: -124px;
}
}
@keyframes progress-circular-rotate {
100% {
transform: rotate(270deg);
}
}@media (forced-colors: active) {
}
@media (forced-colors: active) {
}
@media (forced-colors: active) {
}
@keyframes indeterminate-ltr {
0% {
left: -90%;
right: 100%;
}
60% {
left: -90%;
right: 100%;
}
100% {
left: 100%;
right: -35%;
}
}
@keyframes indeterminate-rtl {
0% {
left: 100%;
right: -90%;
}
60% {
left: 100%;
right: -90%;
}
100% {
left: -35%;
right: 100%;
}
}
@keyframes indeterminate-short-ltr {
0% {
left: -200%;
right: 100%;
}
60% {
left: 107%;
right: -8%;
}
100% {
left: 107%;
right: -8%;
}
}
@keyframes indeterminate-short-rtl {
0% {
left: 100%;
right: -200%;
}
60% {
left: -8%;
right: 107%;
}
100% {
left: -8%;
right: 107%;
}
}
@keyframes stream {
to {
transform: translateX(var(--v-progress-linear-stream-to));
}
}
@keyframes progress-linear-stripes {
0% {
background-position-x: var(--v-progress-linear-height);
}
}@supports not selector(:focus-visible) {
}
@supports not selector(:focus-visible) {
}@supports not selector(:focus-visible) {
}
@supports not selector(:focus-visible) {
}
@supports selector(:focus-visible) {
}/* region BLOCK */
/* endregion */
/* region ELEMENTS */
/* endregion *//* region INPUT */
/* endregion */
/* region MODIFIERS */
/* endregion */
/* region ELEMENTS */
/* endregion */
/* region AFFIXES */
@media (hover: hover) {
}
@media (hover: none) {
}
/* endregion */
/* region LABEL */
/* endregion */
/* region OUTLINE */
@media (hover: hover) {
}
/* endregion */
/* region LOADER */
/* endregion */
/* region OVERLAY */
@media (hover: hover) {
}
@media (hover: hover) {
}
@media (hover: hover) {
}
/* endregion */
/* region MODIFIERS */
/* endregion */.bottom-sheet-transition-enter-from {
transform: translateY(100%);
}
.bottom-sheet-transition-leave-to {
transform: translateY(100%);
}
@media (min-width: 600px) {
}@supports not selector(:focus-visible) {
}
@supports not selector(:focus-visible) {
}@media (forced-colors: active) {
}
@media (hover: hover) {
}@media (forced-colors: active) {
}
@media (forced-colors: active) {
}
@media (forced-colors: active) {
}@media (min-width: 960px) {
}
@media (min-width: 1280px) {
}
@media (min-width: 1920px) {
}
@media (min-width: 2560px) {
}
.offset-1 {
margin-inline-start: 8.3333333333%;
}
.offset-2 {
margin-inline-start: 16.6666666667%;
}
.offset-3 {
margin-inline-start: 25%;
}
.offset-4 {
margin-inline-start: 33.3333333333%;
}
.offset-5 {
margin-inline-start: 41.6666666667%;
}
.offset-6 {
margin-inline-start: 50%;
}
.offset-7 {
margin-inline-start: 58.3333333333%;
}
.offset-8 {
margin-inline-start: 66.6666666667%;
}
.offset-9 {
margin-inline-start: 75%;
}
.offset-10 {
margin-inline-start: 83.3333333333%;
}
.offset-11 {
margin-inline-start: 91.6666666667%;
}
@media (min-width: 600px) {
.offset-sm-0 {
margin-inline-start: 0;
}
.offset-sm-1 {
margin-inline-start: 8.3333333333%;
}
.offset-sm-2 {
margin-inline-start: 16.6666666667%;
}
.offset-sm-3 {
margin-inline-start: 25%;
}
.offset-sm-4 {
margin-inline-start: 33.3333333333%;
}
.offset-sm-5 {
margin-inline-start: 41.6666666667%;
}
.offset-sm-6 {
margin-inline-start: 50%;
}
.offset-sm-7 {
margin-inline-start: 58.3333333333%;
}
.offset-sm-8 {
margin-inline-start: 66.6666666667%;
}
.offset-sm-9 {
margin-inline-start: 75%;
}
.offset-sm-10 {
margin-inline-start: 83.3333333333%;
}
.offset-sm-11 {
margin-inline-start: 91.6666666667%;
}
}
@media (min-width: 960px) {
.offset-md-0 {
margin-inline-start: 0;
}
.offset-md-1 {
margin-inline-start: 8.3333333333%;
}
.offset-md-2 {
margin-inline-start: 16.6666666667%;
}
.offset-md-3 {
margin-inline-start: 25%;
}
.offset-md-4 {
margin-inline-start: 33.3333333333%;
}
.offset-md-5 {
margin-inline-start: 41.6666666667%;
}
.offset-md-6 {
margin-inline-start: 50%;
}
.offset-md-7 {
margin-inline-start: 58.3333333333%;
}
.offset-md-8 {
margin-inline-start: 66.6666666667%;
}
.offset-md-9 {
margin-inline-start: 75%;
}
.offset-md-10 {
margin-inline-start: 83.3333333333%;
}
.offset-md-11 {
margin-inline-start: 91.6666666667%;
}
}
@media (min-width: 1280px) {
.offset-lg-0 {
margin-inline-start: 0;
}
.offset-lg-1 {
margin-inline-start: 8.3333333333%;
}
.offset-lg-2 {
margin-inline-start: 16.6666666667%;
}
.offset-lg-3 {
margin-inline-start: 25%;
}
.offset-lg-4 {
margin-inline-start: 33.3333333333%;
}
.offset-lg-5 {
margin-inline-start: 41.6666666667%;
}
.offset-lg-6 {
margin-inline-start: 50%;
}
.offset-lg-7 {
margin-inline-start: 58.3333333333%;
}
.offset-lg-8 {
margin-inline-start: 66.6666666667%;
}
.offset-lg-9 {
margin-inline-start: 75%;
}
.offset-lg-10 {
margin-inline-start: 83.3333333333%;
}
.offset-lg-11 {
margin-inline-start: 91.6666666667%;
}
}
@media (min-width: 1920px) {
.offset-xl-0 {
margin-inline-start: 0;
}
.offset-xl-1 {
margin-inline-start: 8.3333333333%;
}
.offset-xl-2 {
margin-inline-start: 16.6666666667%;
}
.offset-xl-3 {
margin-inline-start: 25%;
}
.offset-xl-4 {
margin-inline-start: 33.3333333333%;
}
.offset-xl-5 {
margin-inline-start: 41.6666666667%;
}
.offset-xl-6 {
margin-inline-start: 50%;
}
.offset-xl-7 {
margin-inline-start: 58.3333333333%;
}
.offset-xl-8 {
margin-inline-start: 66.6666666667%;
}
.offset-xl-9 {
margin-inline-start: 75%;
}
.offset-xl-10 {
margin-inline-start: 83.3333333333%;
}
.offset-xl-11 {
margin-inline-start: 91.6666666667%;
}
}
@media (min-width: 2560px) {
.offset-xxl-0 {
margin-inline-start: 0;
}
.offset-xxl-1 {
margin-inline-start: 8.3333333333%;
}
.offset-xxl-2 {
margin-inline-start: 16.6666666667%;
}
.offset-xxl-3 {
margin-inline-start: 25%;
}
.offset-xxl-4 {
margin-inline-start: 33.3333333333%;
}
.offset-xxl-5 {
margin-inline-start: 41.6666666667%;
}
.offset-xxl-6 {
margin-inline-start: 50%;
}
.offset-xxl-7 {
margin-inline-start: 58.3333333333%;
}
.offset-xxl-8 {
margin-inline-start: 66.6666666667%;
}
.offset-xxl-9 {
margin-inline-start: 75%;
}
.offset-xxl-10 {
margin-inline-start: 83.3333333333%;
}
.offset-xxl-11 {
margin-inline-start: 91.6666666667%;
}
}.date-picker-header-transition-enter-active,
.date-picker-header-reverse-transition-enter-active {
transition-duration: 0.3s;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
.date-picker-header-transition-leave-active,
.date-picker-header-reverse-transition-leave-active {
transition-duration: 0.3s;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
.date-picker-header-transition-enter-from {
transform: translate(0, 100%);
}
.date-picker-header-transition-leave-to {
opacity: 0;
transform: translate(0, -100%);
}
.date-picker-header-reverse-transition-enter-from {
transform: translate(0, -100%);
}
.date-picker-header-reverse-transition-leave-to {
opacity: 0;
transform: translate(0, 100%);
}@supports not selector(:focus-visible) {
}
@supports not selector(:focus-visible) {
}@keyframes loading {
100% {
transform: translateX(100%);
}
}@supports not selector(:focus-visible) {
}
@supports not selector(:focus-visible) {
}@media (forced-colors: active) {
}@media (max-width: 1279.98px) {
}/** Modifiers **/

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,87 @@
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)},
"./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)},
"./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)},};
const seen = {};
const dynamicLoadingCss = (cssFilePaths, dontAppendStylesToHead, exposeItemName) => {
const metaUrl = import.meta.url;
if (typeof metaUrl === 'undefined') {
console.warn('The remote style takes effect only when the build.target option in the vite.config.ts file is higher than that of "es2020".');
return;
}
const curUrl = metaUrl.substring(0, metaUrl.lastIndexOf('remoteEntry.js'));
const base = '/';
'assets';
cssFilePaths.forEach(cssPath => {
let href = '';
const baseUrl = base || curUrl;
if (baseUrl) {
const trimmer = {
trailing: (path) => (path.endsWith('/') ? path.slice(0, -1) : path),
leading: (path) => (path.startsWith('/') ? path.slice(1) : path)
};
const isAbsoluteUrl = (url) => url.startsWith('http') || url.startsWith('//');
const cleanBaseUrl = trimmer.trailing(baseUrl);
const cleanCssPath = trimmer.leading(cssPath);
const cleanCurUrl = trimmer.trailing(curUrl);
if (isAbsoluteUrl(baseUrl)) {
href = [cleanBaseUrl, cleanCssPath].filter(Boolean).join('/');
} else {
if (cleanCurUrl.includes(cleanBaseUrl)) {
href = [cleanCurUrl, cleanCssPath].filter(Boolean).join('/');
} else {
href = [cleanCurUrl + cleanBaseUrl, cleanCssPath].filter(Boolean).join('/');
}
}
} else {
href = cssPath;
}
if (dontAppendStylesToHead) {
const key = 'css__ClashRuleProvider__' + exposeItemName;
window[key] = window[key] || [];
window[key].push(href);
return;
}
if (href in seen) return;
seen[href] = true;
const element = document.createElement('link');
element.rel = 'stylesheet';
element.href = href;
document.head.appendChild(element);
});
};
async function __federation_import(name) {
currentImports[name] ??= import(name);
return currentImports[name]
} const get =(module) => {
if(!moduleMap[module]) throw new Error('Can not find remote module ' + module)
return moduleMap[module]();
};
const init =(shareScope) => {
globalThis.__federation_shared__= globalThis.__federation_shared__|| {};
Object.entries(shareScope).forEach(([key, value]) => {
for (const [versionKey, versionValue] of Object.entries(value)) {
const scope = versionValue.scope || 'default';
globalThis.__federation_shared__[scope] = globalThis.__federation_shared__[scope] || {};
const shared= globalThis.__federation_shared__[scope];
(shared[key] = shared[key]||{})[versionKey] = versionValue;
}
});
};
export { dynamicLoadingCss, get, init };

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MoviePilot插件组件示例</title>
<link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900" rel="stylesheet" />
<link href="https://cdn.jsdelivr.net/npm/@mdi/font@6.x/css/materialdesignicons.min.css" rel="stylesheet" />
<style>
body {
margin: 0;
padding: 0;
font-family: 'Roboto', sans-serif;
}
</style>
<script type="module" crossorigin src="/assets/index-Bff29tuV.js"></script>
<link rel="modulepreload" crossorigin href="/assets/__federation_fn_import-JrT3xvdd.js">
<link rel="modulepreload" crossorigin href="/assets/_plugin-vue_export-helper-pcqpp-6-.js">
<link rel="modulepreload" crossorigin href="/assets/__federation_expose_Page-DlQgf7u6.js">
<link rel="modulepreload" crossorigin href="/assets/__federation_expose_Config-C3BpNVeC.js">
<link rel="modulepreload" crossorigin href="/assets/runtime-core.esm-bundler-BQhfUSSX.js">
<link rel="modulepreload" crossorigin href="/assets/__federation_expose_Dashboard-BkyO-3pr.js">
<link rel="modulepreload" crossorigin href="/assets/date--mM7W7--.js">
<link rel="stylesheet" crossorigin href="/assets/__federation_expose_Page-Bl7XNZ7k.css">
<link rel="stylesheet" crossorigin href="/assets/__federation_expose_Config-DXzIavcD.css">
<link rel="stylesheet" crossorigin href="/assets/index-B8APBpoy.css">
</head>
<body>
<div id="app"></div>
</body>
</html>

View File

@@ -1,6 +1,7 @@
import re
import json
from typing import Optional, Any, List, Dict, Tuple
from datetime import datetime
from app.core.config import settings
from app.core.event import eventmanager, Event
@@ -24,7 +25,7 @@ class ImdbSource(_PluginBase):
plugin_icon = ("https://raw.githubusercontent.com/jxxghp/"
"MoviePilot-Plugins/refs/heads/main/icons/IMDb_IOS-OSX_App.png")
# 插件版本
plugin_version = "1.3"
plugin_version = "1.3.1"
# 插件作者
plugin_author = "wumode"
# 作者主页
@@ -607,6 +608,8 @@ class ImdbSource(_PluginBase):
elif year == "1970s":
release_date_start = "1970-01-01"
release_date_end = "1979-12-31"
if not release_date_end:
release_date_end = datetime.now().date().strftime("%Y-%m-%d")
awards = (award,) if award else None
ranked_lists = (ranked_list,) if ranked_list else None
first_page = False
@@ -644,17 +647,13 @@ class ImdbSource(_PluginBase):
if mtype == "movies":
for movie in results:
movie_info = movie.get('node').get("title")
pub_status = movie_info.get("productionStatus")
if pub_status and pub_status.get("currentProductionStage"):
if pub_status.get("currentProductionStage", {}).get("id") == 'released':
res.append(self.__movie_to_media(movie_info))
res.append(self.__movie_to_media(movie_info))
else:
for tv in results:
tv_info = tv.get('node').get('title')
pub_status = tv_info.get("productionStatus")
if pub_status and pub_status.get("currentProductionStage"):
if pub_status.get("currentProductionStage", {}).get("id") == 'released':
res.append(self.__series_to_media(tv_info))
res.append(self.__series_to_media(tv_info))
return res
def get_api(self) -> List[Dict[str, Any]]: