mirror of
https://github.com/d0zingcat/MoviePilot-Plugins.git
synced 2026-05-13 23:16:47 +00:00
Merge pull request #819 from wumode/clashruleprovider
This commit is contained in:
@@ -450,11 +450,13 @@
|
||||
"name": "Clash Rule Provider",
|
||||
"description": "随时为Clash添加一些额外的规则。",
|
||||
"labels": "工具",
|
||||
"version": "1.1.0",
|
||||
"version": "1.1.3",
|
||||
"icon": "Mihomo_Meta_A.png",
|
||||
"author": "wumode",
|
||||
"level": 1,
|
||||
"history": {
|
||||
"v1.1.3": "添加仪表盘组件",
|
||||
"v1.1.1": "支持解析 V2ray 订阅",
|
||||
"v1.1.0": "支持规则集合; 添加ACL4SSR规则集; 配置说明",
|
||||
"v1.0.1": "支持规则搜索, 优化细节",
|
||||
"v1.0.0": "支持: 规则分页; 导入规则; 代理组; 附加出站代理; 按区域分组",
|
||||
|
||||
@@ -4,7 +4,6 @@ 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
|
||||
import copy
|
||||
@@ -12,6 +11,12 @@ import math
|
||||
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from apscheduler.triggers.cron import CronTrigger
|
||||
import httpx
|
||||
import asyncio
|
||||
import json
|
||||
from fastapi import HTTPException, Request, status, Body, Response
|
||||
import websockets
|
||||
from sse_starlette.sse import EventSourceResponse
|
||||
|
||||
from app import schemas
|
||||
from app.core.config import settings
|
||||
@@ -19,7 +24,7 @@ from app.log import logger
|
||||
from app.plugins import _PluginBase
|
||||
from app.schemas.types import NotificationType
|
||||
from app.utils.http import RequestUtils
|
||||
from app.plugins.clashruleprovider.clash_rule_parser import ClashRuleParser
|
||||
from app.plugins.clashruleprovider.clash_rule_parser import ClashRuleParser, Converter
|
||||
from app.plugins.clashruleprovider.clash_rule_parser import Action, RuleType, ClashRule, MatchRule, LogicRule
|
||||
from app.plugins.clashruleprovider.clash_rule_parser import ProxyGroup, RuleProvider
|
||||
|
||||
@@ -32,7 +37,7 @@ class ClashRuleProvider(_PluginBase):
|
||||
# 插件图标
|
||||
plugin_icon = "Mihomo_Meta_A.png"
|
||||
# 插件版本
|
||||
plugin_version = "1.1.0"
|
||||
plugin_version = "1.1.3"
|
||||
# 插件作者
|
||||
plugin_author = "wumode"
|
||||
# 作者主页
|
||||
@@ -67,6 +72,7 @@ class ClashRuleProvider(_PluginBase):
|
||||
_refresh_delay: int = 5
|
||||
_discard_rules: bool = False
|
||||
_enable_acl4ssr: bool = False
|
||||
_dashboard_components: List[str] = []
|
||||
|
||||
# 插件数据
|
||||
_clash_config: Optional[Dict[str, Any]] = None
|
||||
@@ -106,7 +112,12 @@ class ClashRuleProvider(_PluginBase):
|
||||
self._proxy = config.get("proxy")
|
||||
self._notify = config.get("notify"),
|
||||
self._sub_links = config.get("sub_links") or []
|
||||
self._clash_dashboard_url = config.get("clash_dashboard_url")
|
||||
self._clash_dashboard_url = config.get("clash_dashboard_url") or ''
|
||||
if self._clash_dashboard_url and self._clash_dashboard_url[-1] == '/':
|
||||
self._clash_dashboard_url = self._clash_dashboard_url[:-1]
|
||||
if not (self._clash_dashboard_url.startswith('http://') or
|
||||
self._clash_dashboard_url.startswith('https://')):
|
||||
self._clash_dashboard_url = 'http://' + self._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 and self._movie_pilot_url[-1] == '/':
|
||||
@@ -122,6 +133,7 @@ class ClashRuleProvider(_PluginBase):
|
||||
self._refresh_delay = config.get("refresh_delay") or 5
|
||||
self._discard_rules = config.get("discard_rules") or False
|
||||
self._enable_acl4ssr = config.get("enable_acl4ssr") or False
|
||||
self._dashboard_components = config.get("dashboard_components") or []
|
||||
self._clash_rule_parser = ClashRuleParser()
|
||||
self._ruleset_rule_parser = ClashRuleParser()
|
||||
if self._enabled:
|
||||
@@ -165,182 +177,198 @@ class ClashRuleProvider(_PluginBase):
|
||||
"endpoint": self.get_clash_outbound,
|
||||
"methods": ["GET"],
|
||||
"auth": "bear",
|
||||
"summary": "clash outbound",
|
||||
"description": "clash outbound"
|
||||
"summary": "获取所有出站",
|
||||
"description": "获取所有出站"
|
||||
},
|
||||
{
|
||||
"path": "/status",
|
||||
"endpoint": self.get_status,
|
||||
"methods": ["GET"],
|
||||
"auth": "bear",
|
||||
"summary": "stated",
|
||||
"description": "state"
|
||||
"summary": "插件状态",
|
||||
"description": "插件状态"
|
||||
},
|
||||
{
|
||||
"path": "/rules",
|
||||
"endpoint": self.get_rules,
|
||||
"methods": ["GET"],
|
||||
"auth": "bear",
|
||||
"summary": "clash rules",
|
||||
"description": "clash rules"
|
||||
"summary": "获取指定集合中的规则",
|
||||
"description": "获取指定集合中的规则"
|
||||
},
|
||||
{
|
||||
"path": "/rules",
|
||||
"endpoint": self.update_rules,
|
||||
"methods": ["PUT"],
|
||||
"auth": "bear",
|
||||
"summary": "clash rules",
|
||||
"description": "clash rules"
|
||||
"summary": "更新 Clash 规则",
|
||||
"description": "更新 Clash 规则"
|
||||
},
|
||||
{
|
||||
"path": "/reorder-rules",
|
||||
"endpoint": self.reorder_rules,
|
||||
"methods": ["PUT"],
|
||||
"auth": "bear",
|
||||
"summary": "clash rules",
|
||||
"description": "clash rules"
|
||||
"summary": "重新排序两条规则",
|
||||
"description": "重新排序两条规则"
|
||||
},
|
||||
{
|
||||
"path": "/rule",
|
||||
"endpoint": self.update_rule,
|
||||
"methods": ["PUT"],
|
||||
"auth": "bear",
|
||||
"summary": "clash rules",
|
||||
"description": "clash rules"
|
||||
"summary": "更新一条规则",
|
||||
"description": "更新一条规则"
|
||||
},
|
||||
{
|
||||
"path": "/rule",
|
||||
"endpoint": self.add_rule,
|
||||
"methods": ["POSt"],
|
||||
"auth": "bear",
|
||||
"summary": "clash rules",
|
||||
"description": "clash rules"
|
||||
"summary": "添加一条规则",
|
||||
"description": "添加一条规则"
|
||||
},
|
||||
{
|
||||
"path": "/rule",
|
||||
"endpoint": self.delete_rule,
|
||||
"methods": ["DELETE"],
|
||||
"auth": "bear",
|
||||
"summary": "clash rules",
|
||||
"description": "clash rules"
|
||||
"summary": "删除一条规则",
|
||||
"description": "删除一条规则"
|
||||
},
|
||||
{
|
||||
"path": "/subscription",
|
||||
"endpoint": self.get_subscription,
|
||||
"methods": ["GET"],
|
||||
"auth": "bear",
|
||||
"summary": "clash rules",
|
||||
"description": "clash rules"
|
||||
"summary": "获取原订阅链接",
|
||||
"description": "获取原订阅链接"
|
||||
},
|
||||
{
|
||||
"path": "/subscription",
|
||||
"endpoint": self.refresh_subscription,
|
||||
"methods": ["PUT"],
|
||||
"auth": "bear",
|
||||
"summary": "refresh clash configuration",
|
||||
"description": "refresh clash configuration"
|
||||
"summary": "更新订阅",
|
||||
"description": "更新订阅"
|
||||
},
|
||||
{
|
||||
"path": "/rule-providers",
|
||||
"endpoint": self.get_rule_providers,
|
||||
"methods": ["GET"],
|
||||
"auth": "bear",
|
||||
"summary": "rule providers",
|
||||
"description": "rule providers"
|
||||
"summary": "获取规则集合",
|
||||
"description": "获取规则集合"
|
||||
},
|
||||
{
|
||||
"path": "/extra-rule-providers",
|
||||
"endpoint": self.get_extra_rule_providers,
|
||||
"methods": ["GET"],
|
||||
"auth": "bear",
|
||||
"summary": "extra rule providers",
|
||||
"description": "extra rule providers"
|
||||
"summary": "添加规则集合",
|
||||
"description": "添加规则集合"
|
||||
},
|
||||
{
|
||||
"path": "/extra-rule-provider",
|
||||
"endpoint": self.update_extra_rule_provider,
|
||||
"methods": ["POST"],
|
||||
"auth": "bear",
|
||||
"summary": "update an extra rule provider",
|
||||
"description": "update an rule provider"
|
||||
"summary": "更新一个规则集合",
|
||||
"description": "更新一个规则集合"
|
||||
},
|
||||
{
|
||||
"path": "/extra-rule-provider",
|
||||
"endpoint": self.delete_extra_rule_provider,
|
||||
"methods": ["DELETE"],
|
||||
"auth": "bear",
|
||||
"summary": "add an extra rule provider",
|
||||
"description": "add an rule provider"
|
||||
"summary": "删除一个规则集合",
|
||||
"description": "删除一个规则集合"
|
||||
},
|
||||
{
|
||||
"path": "/extra-proxies",
|
||||
"endpoint": self.get_extra_proxies,
|
||||
"methods": ["GET"],
|
||||
"auth": "bear",
|
||||
"summary": "extra proxies",
|
||||
"description": "extra proxies"
|
||||
"summary": "获取附加出站代理",
|
||||
"description": "获取附加出站代理"
|
||||
},
|
||||
{
|
||||
"path": "/extra-proxies",
|
||||
"endpoint": self.delete_extra_proxy,
|
||||
"methods": ["DELETE"],
|
||||
"auth": "bear",
|
||||
"summary": "delete an extra proxy",
|
||||
"description": "delete an extra proxy"
|
||||
"summary": "删除一条出站代理",
|
||||
"description": "删除一条出站代理"
|
||||
},
|
||||
{
|
||||
"path": "/extra-proxies",
|
||||
"endpoint": self.add_extra_proxies,
|
||||
"methods": ["POST"],
|
||||
"auth": "bear",
|
||||
"summary": "add extra proxies",
|
||||
"description": "add extra proxies"
|
||||
"summary": "添加一条出站代理",
|
||||
"description": "添加一条出站代理"
|
||||
},
|
||||
{
|
||||
"path": "/proxy-groups",
|
||||
"endpoint": self.get_proxy_groups,
|
||||
"methods": ["GET"],
|
||||
"auth": "bear",
|
||||
"summary": "proxy groups",
|
||||
"description": "proxy groups"
|
||||
"summary": "获取代理组",
|
||||
"description": "获取代理组"
|
||||
},
|
||||
{
|
||||
"path": "/proxy-group",
|
||||
"endpoint": self.delete_proxy_group,
|
||||
"methods": ["DELETE"],
|
||||
"auth": "bear",
|
||||
"summary": "delete a proxy group",
|
||||
"description": "delete a proxy group"
|
||||
"summary": "删除一个代理组",
|
||||
"description": "删除一个代理组"
|
||||
},
|
||||
{
|
||||
"path": "/proxy-group",
|
||||
"endpoint": self.add_proxy_group,
|
||||
"methods": ["POST"],
|
||||
"auth": "bear",
|
||||
"summary": "add a proxy group",
|
||||
"description": "add a proxy group"
|
||||
"summary": "添加一个代理组",
|
||||
"description": "添加一个代理组"
|
||||
},
|
||||
{
|
||||
"path": "/ruleset",
|
||||
"endpoint": self.get_ruleset,
|
||||
"methods": ["GET"],
|
||||
"summary": "update rule providers",
|
||||
"description": "update rule providers"
|
||||
"summary": "获取规则集规则",
|
||||
"description": "获取规则集规则"
|
||||
},
|
||||
{
|
||||
"path": "/import",
|
||||
"endpoint": self.import_rules,
|
||||
"methods": ["POST"],
|
||||
"auth": "bear",
|
||||
"summary": "import top rules",
|
||||
"description": "import top rules"
|
||||
"summary": "导入规则",
|
||||
"description": "导入规则"
|
||||
},
|
||||
{
|
||||
"path": "/config",
|
||||
"endpoint": self.get_clash_config,
|
||||
"methods": ["GET"],
|
||||
"summary": "update rule providers",
|
||||
"description": "update rule providers"
|
||||
"summary": "获取 Clash 配置",
|
||||
"description": "获取 Clash 配置"
|
||||
},
|
||||
{
|
||||
"path": "/clash/proxy/{path:path}",
|
||||
"auth": "bear",
|
||||
"endpoint": self.clash_proxy,
|
||||
"methods": ["GET"],
|
||||
"summary": "转发 Clash API 请求",
|
||||
"description": "转发 Clash API 请求"
|
||||
},
|
||||
{
|
||||
"path": "/clash/ws/{endpoint}",
|
||||
"endpoint": self.clash_websocket,
|
||||
"methods": ["GET"],
|
||||
"summary": "转发 Clash API Websocket 请求",
|
||||
"description": "转发 Clash API Websocket 请求",
|
||||
"allow_anonymous": True
|
||||
}
|
||||
]
|
||||
|
||||
@@ -358,6 +386,51 @@ class ClashRuleProvider(_PluginBase):
|
||||
"""
|
||||
return [], {}
|
||||
|
||||
def get_dashboard_meta(self) -> Optional[List[Dict[str, str]]]:
|
||||
components = [
|
||||
{
|
||||
"key": "clash_info",
|
||||
"name": "Clash Info"
|
||||
},
|
||||
{
|
||||
"key": "traffic_stats",
|
||||
"name": "Traffic Stats"
|
||||
}
|
||||
]
|
||||
return [component for component in components if component.get("name") in self._dashboard_components]
|
||||
|
||||
def get_dashboard(self, key: str, **kwargs) -> Optional[Tuple[Dict[str, Any], Dict[str, Any], List[dict]]]:
|
||||
"""
|
||||
获取插件仪表盘页面,需要返回:1、仪表板col配置字典;2、全局配置(自动刷新等);3、仪表板页面元素配置json(含数据)
|
||||
1、col配置参考:
|
||||
{
|
||||
"cols": 12, "md": 6
|
||||
}
|
||||
2、全局配置参考:
|
||||
{
|
||||
"refresh": 10, // 自动刷新时间,单位秒
|
||||
"border": True, // 是否显示边框,默认True,为False时取消组件边框和边距,由插件自行控制
|
||||
"title": "组件标题", // 组件标题,如有将显示该标题,否则显示插件名称
|
||||
"subtitle": "组件子标题", // 组件子标题,缺省时不展示子标题
|
||||
}
|
||||
3、页面配置使用Vuetify组件拼装,参考:https://vuetifyjs.com/
|
||||
|
||||
kwargs参数可获取的值:1、user_agent:浏览器UA
|
||||
|
||||
:param key: 仪表盘key,根据指定的key返回相应的仪表盘数据,缺省时返回一个固定的仪表盘数据(兼容旧版)
|
||||
"""
|
||||
clash_available = bool(self._clash_dashboard_url and self._clash_dashboard_secret)
|
||||
components = {'clash_info': {'title': 'Clash Info', 'md': 4},
|
||||
'traffic_stats': {'title': 'Traffic Stats', 'md': 8}}
|
||||
col_config = {'cols': 12, 'md': components.get(key, {}).get('md', 4)}
|
||||
global_config = {
|
||||
'title': components.get(key, {}).get('title', 'Clash Info'),
|
||||
'border': True,
|
||||
'clash_available': clash_available,
|
||||
'secret': self._clash_dashboard_secret,
|
||||
}
|
||||
return col_config, global_config, []
|
||||
|
||||
def get_page(self) -> List[dict]:
|
||||
return []
|
||||
|
||||
@@ -401,6 +474,59 @@ class ClashRuleProvider(_PluginBase):
|
||||
self._clash_rule_parser.parse_rules_from_list(self._top_rules)
|
||||
self._ruleset_rule_parser.parse_rules_from_list(self._ruleset_rules)
|
||||
|
||||
async def clash_websocket(self, request: Request, endpoint: str, secret: str):
|
||||
if secret != self._clash_dashboard_secret:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=f"Secret 校验不通过"
|
||||
)
|
||||
if endpoint not in ['traffic', 'connections', 'memory']:
|
||||
raise HTTPException(status_code=400, detail="Invalid endpoint")
|
||||
queue = asyncio.Queue()
|
||||
ws_base = self._clash_dashboard_url.replace('http://', 'ws://').replace('https://', 'wss://')
|
||||
url = f"{ws_base}/{endpoint}?token={self._clash_dashboard_secret}"
|
||||
async def clash_ws_listener():
|
||||
try:
|
||||
async with websockets.connect(url, ping_interval=None) as ws:
|
||||
async for message in ws:
|
||||
data = json.loads(message)
|
||||
await queue.put(data)
|
||||
except Exception as e:
|
||||
await queue.put({"error": str(e)})
|
||||
|
||||
listener_task = asyncio.create_task(clash_ws_listener())
|
||||
|
||||
async def event_generator():
|
||||
try:
|
||||
while True:
|
||||
if await request.is_disconnected():
|
||||
break
|
||||
try:
|
||||
data = await queue.get()
|
||||
yield {
|
||||
'event': endpoint,
|
||||
'data': json.dumps(data)
|
||||
}
|
||||
except asyncio.CancelledError:
|
||||
break
|
||||
finally:
|
||||
listener_task.cancel() # 停止与 Clash 的连接
|
||||
return EventSourceResponse(event_generator())
|
||||
|
||||
async def fetch_clash_data(self, endpoint: str) -> Dict:
|
||||
clash_headers = {"Authorization": f"Bearer {self._clash_dashboard_secret}"}
|
||||
url = f"{self._clash_dashboard_url}/{endpoint}"
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
response = await client.get(url, headers=clash_headers, timeout=5.0)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except httpx.HTTPError as e:
|
||||
raise HTTPException(status_code=502, detail=f"Failed to fetch {endpoint}: {str(e)}")
|
||||
|
||||
async def clash_proxy(self, path: str) -> Dict:
|
||||
return await self.fetch_clash_data(path)
|
||||
|
||||
def test_connectivity(self, params: Dict[str, Any]) -> schemas.Response:
|
||||
if not self._enabled:
|
||||
return schemas.Response(success=False, message="")
|
||||
@@ -442,6 +568,7 @@ class ClashRuleProvider(_PluginBase):
|
||||
"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:
|
||||
@@ -840,7 +967,7 @@ class ClashRuleProvider(_PluginBase):
|
||||
logger.error(f"Invalid links: {self._sub_links}")
|
||||
return False
|
||||
url = self._sub_links[0]
|
||||
logger.info(f"Refreshing: {url}")
|
||||
logger.info(f"正在更新: {url}")
|
||||
ret = None
|
||||
for i in range(0, self._retry_times):
|
||||
ret = RequestUtils(accept_type="text/html",
|
||||
@@ -852,7 +979,13 @@ class ClashRuleProvider(_PluginBase):
|
||||
return False
|
||||
try:
|
||||
rs = yaml.load(ret.content, Loader=yaml.FullLoader)
|
||||
logger.debug(f"{type(rs)} => {rs}")
|
||||
if type(rs) is str:
|
||||
all_proxies = {'name': "All Proxies", 'type': 'select', 'include-all-proxies': True}
|
||||
proxies = Converter.convert_v2ray(ret.content)
|
||||
if not proxies:
|
||||
raise ValueError(f"Unknown content: {rs}")
|
||||
rs = {'proxies': proxies, 'proxy-groups': [all_proxies, ]}
|
||||
logger.info(f"已更新: {url}. 节点数量: {len(rs['proxies'])}")
|
||||
if rs.get('rules') is None:
|
||||
rs['rules'] = []
|
||||
if self._discard_rules:
|
||||
@@ -950,7 +1083,7 @@ class ClashRuleProvider(_PluginBase):
|
||||
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]
|
||||
proxy_group['proxies'] = [x for x in proxy_group.get('proxies', []) if x not in removed_proxies]
|
||||
return clash_config
|
||||
|
||||
def clash_config(self) -> Optional[Dict[str, Any]]:
|
||||
|
||||
@@ -2,6 +2,10 @@ import re
|
||||
from typing import List, Dict, Any, Optional, Union, Callable, Literal
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from urllib.parse import urlparse, parse_qs, unquote, parse_qsl
|
||||
import json
|
||||
import base64
|
||||
import binascii
|
||||
|
||||
from pydantic import BaseModel, Field, validator, HttpUrl
|
||||
|
||||
@@ -635,3 +639,503 @@ class ClashRuleParser:
|
||||
for i in range(target_index, moved_index):
|
||||
self.rules[i].priority += 1
|
||||
self.rules.sort(key=lambda x: x.priority)
|
||||
|
||||
|
||||
class Converter:
|
||||
"""
|
||||
Converter for V2Ray Subscription
|
||||
|
||||
Reference:
|
||||
https://github.com/MetaCubeX/mihomo/blob/Alpha/common/convert/converter.go
|
||||
https://github.com/SubConv/SubConv/blob/main/modules/convert/converter.py
|
||||
"""
|
||||
@staticmethod
|
||||
def decode_base64(data):
|
||||
# 添加适配不同 padding 的容错机制
|
||||
data = data.strip()
|
||||
missing_padding = len(data) % 4
|
||||
if missing_padding:
|
||||
data += '=' * (4 - missing_padding)
|
||||
return base64.b64decode(data)
|
||||
|
||||
@staticmethod
|
||||
def try_decode_base64_json(data):
|
||||
try:
|
||||
return json.loads(Converter.decode_base64(data).decode('utf-8'))
|
||||
except (binascii.Error, UnicodeDecodeError, json.JSONDecodeError, TypeError):
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def unique_name(name_map, name):
|
||||
index = name_map.get(name, 0)
|
||||
name_map[name] = index + 1
|
||||
if index > 0:
|
||||
return f"{name}-{index:02d}"
|
||||
return name
|
||||
|
||||
@staticmethod
|
||||
def strtobool(val):
|
||||
val = val.lower()
|
||||
if val in ("y", "yes", "t", "true", "on", "1"):
|
||||
return True
|
||||
elif val in ("n", "no", "f", "false", "off", "0"):
|
||||
return False
|
||||
else:
|
||||
raise ValueError(f"invalid truth value {val!r}")
|
||||
|
||||
@staticmethod
|
||||
def convert_v2ray(buf: bytes):
|
||||
decoded = Converter.decode_base64(buf).decode("utf-8")
|
||||
lines = decoded.strip().splitlines()
|
||||
proxies = []
|
||||
names = {}
|
||||
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
if "://" not in line:
|
||||
continue
|
||||
|
||||
scheme, body = line.split("://", 1)
|
||||
scheme = scheme.lower()
|
||||
|
||||
if scheme == "vmess":
|
||||
vmess_data = Converter.try_decode_base64_json(body)
|
||||
if not vmess_data:
|
||||
continue
|
||||
|
||||
name = Converter.unique_name(names, vmess_data.get("ps", "vmess"))
|
||||
net = str(vmess_data.get("net", "")).lower()
|
||||
fake_type = str(vmess_data.get("type", "")).lower()
|
||||
tls_mode = str(vmess_data.get("tls", "")).lower()
|
||||
cipher = vmess_data.get("scy", "auto") or "auto"
|
||||
alter_id = vmess_data.get("aid", 0)
|
||||
|
||||
# 调整 network 类型
|
||||
if fake_type == "http":
|
||||
net = "http"
|
||||
elif net == "http":
|
||||
net = "h2"
|
||||
|
||||
proxy = {
|
||||
"name": name,
|
||||
"type": "vmess",
|
||||
"server": vmess_data.get("add"),
|
||||
"port": vmess_data.get("port"),
|
||||
"uuid": vmess_data.get("id"),
|
||||
"alterId": alter_id,
|
||||
"cipher": cipher,
|
||||
"tls": tls_mode.endswith("tls") or tls_mode == "reality",
|
||||
"udp": True,
|
||||
"xudp": True,
|
||||
"skip-cert-verify": False,
|
||||
"network": net
|
||||
}
|
||||
|
||||
# TLS、Reality 扩展
|
||||
if proxy["tls"]:
|
||||
proxy["client-fingerprint"] = vmess_data.get("fp", "chrome") or "chrome"
|
||||
alpn = vmess_data.get("alpn")
|
||||
if alpn:
|
||||
proxy["alpn"] = alpn.split(",") if isinstance(alpn, str) else alpn
|
||||
sni = vmess_data.get("sni")
|
||||
if sni:
|
||||
proxy["servername"] = sni
|
||||
|
||||
if tls_mode == "reality":
|
||||
proxy["reality-opts"] = {
|
||||
"public-key": vmess_data.get("pbk", ""),
|
||||
"short-id": vmess_data.get("sid", "")
|
||||
}
|
||||
|
||||
path = vmess_data.get("path", "/")
|
||||
host = vmess_data.get("host")
|
||||
|
||||
# 不同 network 的扩展字段处理
|
||||
if net == "tcp":
|
||||
if fake_type == "http":
|
||||
proxy["http-opts"] = {
|
||||
"path": path,
|
||||
"headers": {"Host": host} if host else {}
|
||||
}
|
||||
elif net == "http":
|
||||
proxy["network"] = "http"
|
||||
proxy["http-opts"] = {
|
||||
"path": path,
|
||||
"headers": {"Host": host} if host else {}
|
||||
}
|
||||
elif net == "h2":
|
||||
proxy["h2-opts"] = {
|
||||
"path": path,
|
||||
"host": [host] if host else []
|
||||
}
|
||||
|
||||
elif net == "ws":
|
||||
ws_headers = {"Host": host} if host else {}
|
||||
ws_headers["User-Agent"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64)" # 可选伪装
|
||||
ws_opts = {
|
||||
"path": path,
|
||||
"headers": ws_headers
|
||||
}
|
||||
# 补充 early-data 配置
|
||||
early_data = vmess_data.get("ed")
|
||||
if early_data:
|
||||
try:
|
||||
ws_opts["max-early-data"] = int(early_data)
|
||||
except ValueError:
|
||||
pass
|
||||
early_data_header = vmess_data.get("edh")
|
||||
if early_data_header:
|
||||
ws_opts["early-data-header-name"] = early_data_header
|
||||
proxy["ws-opts"] = ws_opts
|
||||
|
||||
elif net == "grpc":
|
||||
proxy["grpc-opts"] = {
|
||||
"grpc-service-name": path
|
||||
}
|
||||
proxies.append(proxy)
|
||||
elif scheme == "vless":
|
||||
try:
|
||||
parsed = urlparse(line)
|
||||
query = parse_qs(parsed.query)
|
||||
|
||||
uuid = parsed.username or ""
|
||||
server = parsed.hostname or ""
|
||||
port = parsed.port or 443
|
||||
tls_mode = query.get("security", [""])[0].lower()
|
||||
tls = tls_mode == "tls" or tls_mode == "reality"
|
||||
sni = query.get("sni", [""])[0]
|
||||
flow = query.get("flow", [""])[0]
|
||||
network = query.get("type", [""])[0]
|
||||
path = query.get("path", [""])[0]
|
||||
host = query.get("host", [""])[0]
|
||||
|
||||
name = Converter.unique_name(names, unquote(parsed.fragment or f"{server}:{port}"))
|
||||
|
||||
proxy = {
|
||||
"name": name,
|
||||
"type": "vless",
|
||||
"server": server,
|
||||
"port": str(port),
|
||||
"uuid": uuid,
|
||||
"tls": tls,
|
||||
"udp": True
|
||||
}
|
||||
|
||||
if sni:
|
||||
proxy["sni"] = sni
|
||||
if flow:
|
||||
proxy["flow"] = flow
|
||||
|
||||
if network:
|
||||
proxy["network"] = network
|
||||
if network == "ws":
|
||||
proxy["ws-opts"] = {
|
||||
"path": path,
|
||||
"headers": {"Host": host}
|
||||
}
|
||||
elif network == "grpc":
|
||||
proxy["grpc-opts"] = {
|
||||
"grpc-service-name": path
|
||||
}
|
||||
|
||||
if tls_mode == "reality":
|
||||
proxy["reality-opts"] = {
|
||||
"public-key": query.get("pbk", [""])[0],
|
||||
"short-id": query.get("sid", [""])[0]
|
||||
}
|
||||
proxy["client-fingerprint"] = query.get("fp", ["chrome"])[0]
|
||||
alpn = query.get("alpn", [""])[0]
|
||||
if alpn:
|
||||
proxy["alpn"] = alpn.split(",")
|
||||
|
||||
return proxy
|
||||
|
||||
except Exception as e:
|
||||
raise ValueError(f"VLESS parse error: {e}") from e
|
||||
elif scheme == "trojan":
|
||||
try:
|
||||
parsed = urlparse(line)
|
||||
query = dict(parse_qsl(parsed.query))
|
||||
|
||||
name = Converter.unique_name(names, unquote(parsed.fragment or f"{parsed.hostname}:{parsed.port}"))
|
||||
|
||||
trojan = {
|
||||
"name": name,
|
||||
"type": "trojan",
|
||||
"server": parsed.hostname,
|
||||
"port": parsed.port or 443,
|
||||
"password": parsed.username or "",
|
||||
"udp": True,
|
||||
}
|
||||
|
||||
# skip-cert-verify
|
||||
try:
|
||||
trojan["skip-cert-verify"] = Converter.strtobool(query.get("allowInsecure", "0"))
|
||||
except ValueError:
|
||||
trojan["skip-cert-verify"] = False
|
||||
|
||||
# optional fields
|
||||
if "sni" in query:
|
||||
trojan["sni"] = query["sni"]
|
||||
|
||||
alpn = query.get("alpn", "")
|
||||
if alpn:
|
||||
trojan["alpn"] = alpn.split(",")
|
||||
|
||||
network = query.get("type", "").lower()
|
||||
if network:
|
||||
trojan["network"] = network
|
||||
|
||||
if network == "ws":
|
||||
headers = {"User-Agent": "clash"} # 或 RandUserAgent()
|
||||
trojan["ws-opts"] = {
|
||||
"path": query.get("path", "/"),
|
||||
"headers": headers
|
||||
}
|
||||
|
||||
elif network == "grpc":
|
||||
trojan["grpc-opts"] = {
|
||||
"grpc-service-name": query.get("serviceName", "")
|
||||
}
|
||||
|
||||
fp = query.get("fp", "")
|
||||
trojan["client-fingerprint"] = fp if fp else "chrome"
|
||||
|
||||
proxies.append(trojan)
|
||||
|
||||
except Exception as e:
|
||||
raise ValueError(f"Error parsing trojan:// link: {e}") from e
|
||||
elif scheme == "hysteria":
|
||||
try:
|
||||
parsed = urlparse(line)
|
||||
query = dict(parse_qsl(parsed.query))
|
||||
|
||||
name = Converter.unique_name(names, unquote(parsed.fragment or f"{parsed.hostname}:{parsed.port}"))
|
||||
hysteria = {
|
||||
"name": name,
|
||||
"type": "hysteria",
|
||||
"server": parsed.hostname,
|
||||
"port": parsed.port,
|
||||
"auth_str": parsed.username or query.get("auth", ""),
|
||||
"obfs": query.get("obfs", ""),
|
||||
"sni": query.get("peer", ""),
|
||||
"protocol": query.get("protocol", "")
|
||||
}
|
||||
|
||||
up = query.get("up", "")
|
||||
down = query.get("down", "")
|
||||
if not up:
|
||||
up = query.get("upmbps", "")
|
||||
if not down:
|
||||
down = query.get("downmbps", "")
|
||||
hysteria["up"] = up
|
||||
hysteria["down"] = down
|
||||
|
||||
# alpn split
|
||||
alpn = query.get("alpn", "")
|
||||
if alpn:
|
||||
hysteria["alpn"] = alpn.split(",")
|
||||
|
||||
# skip-cert-verify
|
||||
try:
|
||||
hysteria["skip-cert-verify"] = Converter.strtobool(query.get("insecure", "false"))
|
||||
except ValueError:
|
||||
hysteria["skip-cert-verify"] = False
|
||||
|
||||
proxies.append(hysteria)
|
||||
except Exception as e:
|
||||
raise ValueError(f"Hysteria parse error: {e}") from e
|
||||
elif scheme in ("socks", "socks5", "socks5h"):
|
||||
try:
|
||||
parsed = urlparse(line)
|
||||
server = parsed.hostname
|
||||
port = parsed.port
|
||||
username = parsed.username or ""
|
||||
password = parsed.password or ""
|
||||
name = Converter.unique_name(names, unquote(parsed.fragment or f"{server}:{port}"))
|
||||
|
||||
proxy = {
|
||||
"name": name,
|
||||
"type": "socks5",
|
||||
"server": server,
|
||||
"port": str(port),
|
||||
"username": username,
|
||||
"password": password,
|
||||
"udp": True
|
||||
}
|
||||
proxies.append(proxy)
|
||||
except Exception as e:
|
||||
raise ValueError(f"SOCKS5 parse error: {e}") from e
|
||||
elif scheme == "ss":
|
||||
try:
|
||||
# 兼容 ss://base64 或 ss://base64#name
|
||||
if "#" in body:
|
||||
body, fragment = body.split("#", 1)
|
||||
name = Converter.unique_name(names, unquote(fragment))
|
||||
else:
|
||||
name = Converter.unique_name(names, "ss")
|
||||
|
||||
if "@" in body:
|
||||
userinfo, server = body.split("@", 1)
|
||||
else:
|
||||
decoded = Converter.decode_base64(body).decode()
|
||||
userinfo, server = decoded.rsplit("@", 1)
|
||||
|
||||
cipher, password = userinfo.split(":")
|
||||
server_host, server_port = server.split(":")
|
||||
|
||||
proxy = {
|
||||
"name": name,
|
||||
"type": "ss",
|
||||
"server": server_host,
|
||||
"port": server_port,
|
||||
"cipher": cipher,
|
||||
"password": password,
|
||||
"udp": True
|
||||
}
|
||||
proxies.append(proxy)
|
||||
except Exception as e:
|
||||
raise ValueError(f"SS parse error: {e}") from e
|
||||
elif scheme == "ssr":
|
||||
try:
|
||||
decoded = Converter.decode_base64(body).decode()
|
||||
parts, _, params_str = decoded.partition("/?")
|
||||
host, port, protocol, method, obfs, password_enc = parts.split(":")
|
||||
|
||||
password = Converter.decode_base64(password_enc).decode()
|
||||
params = parse_qs(params_str)
|
||||
|
||||
remarks = Converter.decode_base64(params.get("remarks", [""])[0]).decode()
|
||||
obfsparam = Converter.decode_base64(params.get("obfsparam", [""])[0]).decode()
|
||||
protoparam = Converter.decode_base64(params.get("protoparam", [""])[0]).decode()
|
||||
|
||||
name = Converter.unique_name(names, remarks or f"{host}:{port}")
|
||||
|
||||
proxy = {
|
||||
"name": name,
|
||||
"type": "ssr",
|
||||
"server": host,
|
||||
"port": port,
|
||||
"cipher": method,
|
||||
"password": password,
|
||||
"obfs": obfs,
|
||||
"protocol": protocol,
|
||||
"udp": True
|
||||
}
|
||||
|
||||
if obfsparam:
|
||||
proxy["obfs-param"] = obfsparam
|
||||
if protoparam:
|
||||
proxy["protocol-param"] = protoparam
|
||||
|
||||
proxies.append(proxy)
|
||||
except Exception as e:
|
||||
raise ValueError(f"SSR parse error: {e}") from e
|
||||
elif scheme == "tuic":
|
||||
try:
|
||||
parsed = urlparse(line)
|
||||
query = parse_qs(parsed.query)
|
||||
|
||||
user = parsed.username or ""
|
||||
password = parsed.password or ""
|
||||
server = parsed.hostname
|
||||
port = parsed.port or 443
|
||||
|
||||
name = Converter.unique_name(names, unquote(parsed.fragment or f"{server}:{port}"))
|
||||
proxy = {
|
||||
"name": name,
|
||||
"type": "tuic",
|
||||
"server": server,
|
||||
"port": str(port),
|
||||
"udp": True
|
||||
}
|
||||
|
||||
if password:
|
||||
proxy["uuid"] = user
|
||||
proxy["password"] = password
|
||||
else:
|
||||
proxy["token"] = user
|
||||
|
||||
if "congestion_control" in query:
|
||||
proxy["congestion-controller"] = query["congestion_control"][0]
|
||||
if "alpn" in query:
|
||||
proxy["alpn"] = query["alpn"][0].split(",")
|
||||
if "sni" in query:
|
||||
proxy["sni"] = query["sni"][0]
|
||||
if query.get("disable_sni", ["0"])[0] == "1":
|
||||
proxy["disable-sni"] = True
|
||||
if "udp_relay_mode" in query:
|
||||
proxy["udp-relay-mode"] = query["udp_relay_mode"][0]
|
||||
|
||||
proxies.append(proxy)
|
||||
except Exception as e:
|
||||
raise ValueError(f"TUIC parse error: {e}") from e
|
||||
elif scheme == "anytls":
|
||||
try:
|
||||
parsed = urlparse(line)
|
||||
query = parse_qs(parsed.query)
|
||||
|
||||
username = parsed.username or ""
|
||||
password = parsed.password or username
|
||||
server = parsed.hostname
|
||||
port = parsed.port
|
||||
insecure = query.get("insecure", ["0"])[0] == "1"
|
||||
sni = query.get("sni", [""])[0]
|
||||
fingerprint = query.get("hpkp", [""])[0]
|
||||
|
||||
name = Converter.unique_name(names, unquote(parsed.fragment or f"{server}:{port}"))
|
||||
proxy = {
|
||||
"name": name,
|
||||
"type": "anytls",
|
||||
"server": server,
|
||||
"port": str(port),
|
||||
"username": username,
|
||||
"password": password,
|
||||
"sni": sni,
|
||||
"fingerprint": fingerprint,
|
||||
"skip-cert-verify": insecure,
|
||||
"udp": True
|
||||
}
|
||||
|
||||
proxies.append(proxy)
|
||||
except Exception as e:
|
||||
raise ValueError(f"AnyTLS parse error: {e}") from e
|
||||
elif scheme in ("hysteria2", "hy2"):
|
||||
try:
|
||||
parsed = urlparse(line)
|
||||
query = parse_qs(parsed.query)
|
||||
|
||||
password = parsed.username or ""
|
||||
server = parsed.hostname
|
||||
port = parsed.port or 443
|
||||
|
||||
name = Converter.unique_name(names, unquote(parsed.fragment or f"{server}:{port}"))
|
||||
proxy = {
|
||||
"name": name,
|
||||
"type": "hysteria2",
|
||||
"server": server,
|
||||
"port": str(port),
|
||||
"password": password,
|
||||
"obfs": query.get("obfs", [""])[0],
|
||||
"obfs-password": query.get("obfs-password", [""])[0],
|
||||
"sni": query.get("sni", [""])[0],
|
||||
"skip-cert-verify": query.get("insecure", ["false"])[0] == "true",
|
||||
"down": query.get("down", [""])[0],
|
||||
"up": query.get("up", [""])[0],
|
||||
"fingerprint": query.get("pinSHA256", [""])[0]
|
||||
}
|
||||
|
||||
if "alpn" in query:
|
||||
proxy["alpn"] = query["alpn"][0].split(",")
|
||||
|
||||
proxies.append(proxy)
|
||||
except Exception as e:
|
||||
raise ValueError(f"Hysteria2 parse error: {e}") from e
|
||||
|
||||
if not proxies:
|
||||
raise ValueError("convert v2ray subscribe error: format invalid")
|
||||
|
||||
return proxies
|
||||
@@ -40,6 +40,7 @@ const saving = ref(false);
|
||||
const testing = ref(false);
|
||||
const showClashSecret = ref(false);
|
||||
const selectedCronOption = ref('6hours');
|
||||
const dashboardComponents = ['Clash Info', 'Traffic Stats'];
|
||||
|
||||
// Test result state
|
||||
const testResult = reactive({
|
||||
@@ -70,6 +71,7 @@ const defaultConfig = {
|
||||
refresh_delay: 5,
|
||||
discard_rules: false,
|
||||
enable_acl4ssr: false,
|
||||
dashboard_components: [],
|
||||
};
|
||||
|
||||
// 响应式配置对象
|
||||
@@ -90,14 +92,15 @@ onMounted(() => {
|
||||
});
|
||||
|
||||
// 验证函数
|
||||
function isValidUrl(url) {
|
||||
const isValidUrl = (urlString) => {
|
||||
if (!urlString) return false;
|
||||
try {
|
||||
new URL(url);
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
const url = new URL(urlString);
|
||||
return url.protocol === 'http:' || url.protocol === 'https:';
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function validateSubLinks(links) {
|
||||
if (!links || links.length === 0) {
|
||||
@@ -232,6 +235,7 @@ return (_ctx, _cache) => {
|
||||
const _component_v_chip = _resolveComponent("v-chip");
|
||||
const _component_v_combobox = _resolveComponent("v-combobox");
|
||||
const _component_v_text_field = _resolveComponent("v-text-field");
|
||||
const _component_v_select = _resolveComponent("v-select");
|
||||
const _component_v_cron_field = _resolveComponent("v-cron-field");
|
||||
const _component_v_expansion_panel_title = _resolveComponent("v-expansion-panel-title");
|
||||
const _component_v_expansion_panel_text = _resolveComponent("v-expansion-panel-text");
|
||||
@@ -256,7 +260,7 @@ return (_ctx, _cache) => {
|
||||
}, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_v_icon, { left: "" }, {
|
||||
default: _withCtx(() => _cache[22] || (_cache[22] = [
|
||||
default: _withCtx(() => _cache[23] || (_cache[23] = [
|
||||
_createTextVNode("mdi-close")
|
||||
])),
|
||||
_: 1
|
||||
@@ -267,7 +271,7 @@ return (_ctx, _cache) => {
|
||||
]),
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_v_card_title, null, {
|
||||
default: _withCtx(() => _cache[21] || (_cache[21] = [
|
||||
default: _withCtx(() => _cache[22] || (_cache[22] = [
|
||||
_createTextVNode("Clash Rule Provider 插件配置")
|
||||
])),
|
||||
_: 1
|
||||
@@ -293,11 +297,11 @@ return (_ctx, _cache) => {
|
||||
ref_key: "form",
|
||||
ref: form,
|
||||
modelValue: isFormValid.value,
|
||||
"onUpdate:modelValue": _cache[19] || (_cache[19] = $event => ((isFormValid).value = $event)),
|
||||
"onUpdate:modelValue": _cache[20] || (_cache[20] = $event => ((isFormValid).value = $event)),
|
||||
onSubmit: _withModifiers(saveConfig, ["prevent"])
|
||||
}, {
|
||||
default: _withCtx(() => [
|
||||
_cache[33] || (_cache[33] = _createElementVNode("div", { class: "text-subtitle-1 font-weight-bold mt-4 mb-2" }, "基本设置", -1)),
|
||||
_cache[35] || (_cache[35] = _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, {
|
||||
@@ -311,7 +315,8 @@ return (_ctx, _cache) => {
|
||||
label: "启用插件",
|
||||
color: "primary",
|
||||
inset: "",
|
||||
hint: "启用后插件将开始监控和同步"
|
||||
hint: "启用后插件将开始监控和同步",
|
||||
density: "compact"
|
||||
}, null, 8, ["modelValue"])
|
||||
]),
|
||||
_: 1
|
||||
@@ -327,7 +332,8 @@ return (_ctx, _cache) => {
|
||||
label: "启用代理",
|
||||
color: "primary",
|
||||
inset: "",
|
||||
hint: "是否使用系统代理进行网络请求"
|
||||
hint: "是否使用系统代理进行网络请求",
|
||||
density: "compact"
|
||||
}, null, 8, ["modelValue"])
|
||||
]),
|
||||
_: 1
|
||||
@@ -343,7 +349,8 @@ return (_ctx, _cache) => {
|
||||
label: "启用通知",
|
||||
color: "primary",
|
||||
inset: "",
|
||||
hint: "执行完成后发送通知消息"
|
||||
hint: "执行完成后发送通知消息",
|
||||
density: "compact"
|
||||
}, null, 8, ["modelValue"])
|
||||
]),
|
||||
_: 1
|
||||
@@ -359,7 +366,8 @@ return (_ctx, _cache) => {
|
||||
label: "自动更新订阅",
|
||||
color: "primary",
|
||||
inset: "",
|
||||
hint: "定期自动更新Clash订阅配置"
|
||||
hint: "定期自动更新Clash订阅配置",
|
||||
density: "compact"
|
||||
}, null, 8, ["modelValue"])
|
||||
]),
|
||||
_: 1
|
||||
@@ -367,7 +375,7 @@ return (_ctx, _cache) => {
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_cache[34] || (_cache[34] = _createElementVNode("div", { class: "text-subtitle-1 font-weight-bold mt-4 mb-2" }, "订阅配置", -1)),
|
||||
_cache[36] || (_cache[36] = _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" }, {
|
||||
@@ -431,7 +439,7 @@ return (_ctx, _cache) => {
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_cache[35] || (_cache[35] = _createElementVNode("div", { class: "text-subtitle-1 font-weight-bold mt-4 mb-2" }, "Clash 面板设置", -1)),
|
||||
_cache[37] || (_cache[37] = _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" }, {
|
||||
@@ -447,7 +455,7 @@ return (_ctx, _cache) => {
|
||||
}, {
|
||||
"prepend-inner": _withCtx(() => [
|
||||
_createVNode(_component_v_icon, { color: "primary" }, {
|
||||
default: _withCtx(() => _cache[23] || (_cache[23] = [
|
||||
default: _withCtx(() => _cache[24] || (_cache[24] = [
|
||||
_createTextVNode("mdi-web")
|
||||
])),
|
||||
_: 1
|
||||
@@ -458,7 +466,10 @@ return (_ctx, _cache) => {
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_createVNode(_component_v_col, { cols: "12" }, {
|
||||
_createVNode(_component_v_col, {
|
||||
cols: "12",
|
||||
md: "6"
|
||||
}, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_v_text_field, {
|
||||
modelValue: config.clash_dashboard_secret,
|
||||
@@ -473,7 +484,7 @@ return (_ctx, _cache) => {
|
||||
}, {
|
||||
"prepend-inner": _withCtx(() => [
|
||||
_createVNode(_component_v_icon, { color: "warning" }, {
|
||||
default: _withCtx(() => _cache[24] || (_cache[24] = [
|
||||
default: _withCtx(() => _cache[25] || (_cache[25] = [
|
||||
_createTextVNode("mdi-key")
|
||||
])),
|
||||
_: 1
|
||||
@@ -483,18 +494,48 @@ return (_ctx, _cache) => {
|
||||
}, 8, ["modelValue", "append-inner-icon", "type"])
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_createVNode(_component_v_col, {
|
||||
cols: "12",
|
||||
md: "6"
|
||||
}, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_v_select, {
|
||||
modelValue: config.dashboard_components,
|
||||
"onUpdate:modelValue": _cache[9] || (_cache[9] = $event => ((config.dashboard_components) = $event)),
|
||||
items: dashboardComponents,
|
||||
label: "仪表盘组件",
|
||||
"hide-details": "",
|
||||
variant: "outlined",
|
||||
multiple: "",
|
||||
chips: "",
|
||||
class: "mb-4",
|
||||
hint: "添加仪表盘组件"
|
||||
}, {
|
||||
"prepend-inner": _withCtx(() => [
|
||||
_createVNode(_component_v_icon, { color: "info" }, {
|
||||
default: _withCtx(() => _cache[26] || (_cache[26] = [
|
||||
_createTextVNode("mdi-view-dashboard")
|
||||
])),
|
||||
_: 1
|
||||
})
|
||||
]),
|
||||
_: 1
|
||||
}, 8, ["modelValue"])
|
||||
]),
|
||||
_: 1
|
||||
})
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_cache[36] || (_cache[36] = _createElementVNode("div", { class: "text-subtitle-1 font-weight-bold mt-4 mb-2" }, "MoviePilot 设置", -1)),
|
||||
_cache[38] || (_cache[38] = _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[9] || (_cache[9] = $event => ((config.movie_pilot_url) = $event)),
|
||||
"onUpdate:modelValue": _cache[10] || (_cache[10] = $event => ((config.movie_pilot_url) = $event)),
|
||||
label: "MoviePilot URL",
|
||||
variant: "outlined",
|
||||
placeholder: "http://localhost:3001",
|
||||
@@ -503,7 +544,7 @@ return (_ctx, _cache) => {
|
||||
}, {
|
||||
"prepend-inner": _withCtx(() => [
|
||||
_createVNode(_component_v_icon, { color: "success" }, {
|
||||
default: _withCtx(() => _cache[25] || (_cache[25] = [
|
||||
default: _withCtx(() => _cache[27] || (_cache[27] = [
|
||||
_createTextVNode("mdi-movie")
|
||||
])),
|
||||
_: 1
|
||||
@@ -517,18 +558,28 @@ return (_ctx, _cache) => {
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_cache[37] || (_cache[37] = _createElementVNode("div", { class: "text-subtitle-1 font-weight-bold mt-4 mb-2" }, "执行设置", -1)),
|
||||
_cache[39] || (_cache[39] = _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_cron_field, {
|
||||
modelValue: config.cron_string,
|
||||
"onUpdate:modelValue": _cache[10] || (_cache[10] = $event => ((config.cron_string) = $event)),
|
||||
"onUpdate:modelValue": _cache[11] || (_cache[11] = $event => ((config.cron_string) = $event)),
|
||||
label: "执行周期",
|
||||
placeholder: "0 4 * * *",
|
||||
hint: "使用标准Cron表达式格式 (分 时 日 月 周)"
|
||||
}, null, 8, ["modelValue"])
|
||||
}, {
|
||||
"prepend-inner": _withCtx(() => [
|
||||
_createVNode(_component_v_icon, { color: "info" }, {
|
||||
default: _withCtx(() => _cache[28] || (_cache[28] = [
|
||||
_createTextVNode("mdi-clock-time-four-outline")
|
||||
])),
|
||||
_: 1
|
||||
})
|
||||
]),
|
||||
_: 1
|
||||
}, 8, ["modelValue"])
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
@@ -539,7 +590,7 @@ return (_ctx, _cache) => {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_v_text_field, {
|
||||
modelValue: config.timeout,
|
||||
"onUpdate:modelValue": _cache[11] || (_cache[11] = $event => ((config.timeout) = $event)),
|
||||
"onUpdate:modelValue": _cache[12] || (_cache[12] = $event => ((config.timeout) = $event)),
|
||||
modelModifiers: { number: true },
|
||||
label: "超时时间 (秒)",
|
||||
variant: "outlined",
|
||||
@@ -548,17 +599,7 @@ return (_ctx, _cache) => {
|
||||
max: "300",
|
||||
hint: "请求的超时时间",
|
||||
rules: [v => v > 0 || '超时时间必须大于0']
|
||||
}, {
|
||||
"prepend-inner": _withCtx(() => [
|
||||
_createVNode(_component_v_icon, { color: "warning" }, {
|
||||
default: _withCtx(() => _cache[26] || (_cache[26] = [
|
||||
_createTextVNode("mdi-timer")
|
||||
])),
|
||||
_: 1
|
||||
})
|
||||
]),
|
||||
_: 1
|
||||
}, 8, ["modelValue", "rules"])
|
||||
}, null, 8, ["modelValue", "rules"])
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
@@ -569,7 +610,7 @@ return (_ctx, _cache) => {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_v_text_field, {
|
||||
modelValue: config.retry_times,
|
||||
"onUpdate:modelValue": _cache[12] || (_cache[12] = $event => ((config.retry_times) = $event)),
|
||||
"onUpdate:modelValue": _cache[13] || (_cache[13] = $event => ((config.retry_times) = $event)),
|
||||
modelModifiers: { number: true },
|
||||
label: "重试次数",
|
||||
variant: "outlined",
|
||||
@@ -581,7 +622,7 @@ return (_ctx, _cache) => {
|
||||
}, {
|
||||
"prepend-inner": _withCtx(() => [
|
||||
_createVNode(_component_v_icon, { color: "info" }, {
|
||||
default: _withCtx(() => _cache[27] || (_cache[27] = [
|
||||
default: _withCtx(() => _cache[29] || (_cache[29] = [
|
||||
_createTextVNode("mdi-refresh")
|
||||
])),
|
||||
_: 1
|
||||
@@ -605,12 +646,12 @@ return (_ctx, _cache) => {
|
||||
_createVNode(_component_v_expansion_panel_title, null, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_v_icon, { class: "mr-2" }, {
|
||||
default: _withCtx(() => _cache[28] || (_cache[28] = [
|
||||
default: _withCtx(() => _cache[30] || (_cache[30] = [
|
||||
_createTextVNode("mdi-cog")
|
||||
])),
|
||||
_: 1
|
||||
}),
|
||||
_cache[29] || (_cache[29] = _createTextVNode(" 高级选项 "))
|
||||
_cache[31] || (_cache[31] = _createTextVNode(" 高级选项 "))
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
@@ -625,7 +666,7 @@ return (_ctx, _cache) => {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_v_switch, {
|
||||
modelValue: config.discard_rules,
|
||||
"onUpdate:modelValue": _cache[13] || (_cache[13] = $event => ((config.discard_rules) = $event)),
|
||||
"onUpdate:modelValue": _cache[14] || (_cache[14] = $event => ((config.discard_rules) = $event)),
|
||||
label: "丢弃订阅规则",
|
||||
color: "primary",
|
||||
inset: "",
|
||||
@@ -641,7 +682,7 @@ return (_ctx, _cache) => {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_v_switch, {
|
||||
modelValue: config.enable_acl4ssr,
|
||||
"onUpdate:modelValue": _cache[14] || (_cache[14] = $event => ((config.enable_acl4ssr) = $event)),
|
||||
"onUpdate:modelValue": _cache[15] || (_cache[15] = $event => ((config.enable_acl4ssr) = $event)),
|
||||
label: "ACL4SSR规则集",
|
||||
color: "primary",
|
||||
inset: "",
|
||||
@@ -657,7 +698,7 @@ return (_ctx, _cache) => {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_v_switch, {
|
||||
modelValue: config.group_by_region,
|
||||
"onUpdate:modelValue": _cache[15] || (_cache[15] = $event => ((config.group_by_region) = $event)),
|
||||
"onUpdate:modelValue": _cache[16] || (_cache[16] = $event => ((config.group_by_region) = $event)),
|
||||
label: "按大洲分组节点",
|
||||
color: "primary",
|
||||
inset: "",
|
||||
@@ -678,7 +719,7 @@ return (_ctx, _cache) => {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_v_text_field, {
|
||||
modelValue: config.ruleset_prefix,
|
||||
"onUpdate:modelValue": _cache[16] || (_cache[16] = $event => ((config.ruleset_prefix) = $event)),
|
||||
"onUpdate:modelValue": _cache[17] || (_cache[17] = $event => ((config.ruleset_prefix) = $event)),
|
||||
label: "规则集前缀",
|
||||
variant: "outlined",
|
||||
placeholder: "📂<=",
|
||||
@@ -687,7 +728,7 @@ return (_ctx, _cache) => {
|
||||
}, {
|
||||
"prepend-inner": _withCtx(() => [
|
||||
_createVNode(_component_v_icon, { color: "info" }, {
|
||||
default: _withCtx(() => _cache[30] || (_cache[30] = [
|
||||
default: _withCtx(() => _cache[32] || (_cache[32] = [
|
||||
_createTextVNode("mdi-palette")
|
||||
])),
|
||||
_: 1
|
||||
@@ -705,7 +746,7 @@ return (_ctx, _cache) => {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_v_text_field, {
|
||||
modelValue: config.acl4ssr_prefix,
|
||||
"onUpdate:modelValue": _cache[17] || (_cache[17] = $event => ((config.acl4ssr_prefix) = $event)),
|
||||
"onUpdate:modelValue": _cache[18] || (_cache[18] = $event => ((config.acl4ssr_prefix) = $event)),
|
||||
label: "ACL4SSR 规则集前缀",
|
||||
variant: "outlined",
|
||||
placeholder: "🗂️=>",
|
||||
@@ -714,7 +755,7 @@ return (_ctx, _cache) => {
|
||||
}, {
|
||||
"prepend-inner": _withCtx(() => [
|
||||
_createVNode(_component_v_icon, { color: "primary" }, {
|
||||
default: _withCtx(() => _cache[31] || (_cache[31] = [
|
||||
default: _withCtx(() => _cache[33] || (_cache[33] = [
|
||||
_createTextVNode("mdi-palette")
|
||||
])),
|
||||
_: 1
|
||||
@@ -732,7 +773,7 @@ return (_ctx, _cache) => {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_v_text_field, {
|
||||
modelValue: config.refresh_delay,
|
||||
"onUpdate:modelValue": _cache[18] || (_cache[18] = $event => ((config.refresh_delay) = $event)),
|
||||
"onUpdate:modelValue": _cache[19] || (_cache[19] = $event => ((config.refresh_delay) = $event)),
|
||||
modelModifiers: { number: true },
|
||||
label: "刷新延迟",
|
||||
variant: "outlined",
|
||||
@@ -745,7 +786,7 @@ return (_ctx, _cache) => {
|
||||
}, {
|
||||
"prepend-inner": _withCtx(() => [
|
||||
_createVNode(_component_v_icon, { color: "info" }, {
|
||||
default: _withCtx(() => _cache[32] || (_cache[32] = [
|
||||
default: _withCtx(() => _cache[34] || (_cache[34] = [
|
||||
_createTextVNode("mdi-clock-outline")
|
||||
])),
|
||||
_: 1
|
||||
@@ -780,7 +821,7 @@ return (_ctx, _cache) => {
|
||||
class: "mb-6",
|
||||
variant: "tonal"
|
||||
}, {
|
||||
default: _withCtx(() => _cache[38] || (_cache[38] = [
|
||||
default: _withCtx(() => _cache[40] || (_cache[40] = [
|
||||
_createTextVNode(" 配置说明参考: "),
|
||||
_createElementVNode("a", {
|
||||
href: "https://github.com/wumode/MoviePilot-Plugins/tree/main/plugins.v2/clashruleprovider/README.md",
|
||||
@@ -797,12 +838,12 @@ return (_ctx, _cache) => {
|
||||
}, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_v_icon, { left: "" }, {
|
||||
default: _withCtx(() => _cache[39] || (_cache[39] = [
|
||||
default: _withCtx(() => _cache[41] || (_cache[41] = [
|
||||
_createTextVNode("mdi-view-dashboard-edit")
|
||||
])),
|
||||
_: 1
|
||||
}),
|
||||
_cache[40] || (_cache[40] = _createTextVNode(" 规则 "))
|
||||
_cache[42] || (_cache[42] = _createTextVNode(" 规则 "))
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
@@ -810,7 +851,7 @@ return (_ctx, _cache) => {
|
||||
color: "secondary",
|
||||
onClick: resetForm
|
||||
}, {
|
||||
default: _withCtx(() => _cache[41] || (_cache[41] = [
|
||||
default: _withCtx(() => _cache[43] || (_cache[43] = [
|
||||
_createTextVNode("重置")
|
||||
])),
|
||||
_: 1
|
||||
@@ -820,7 +861,7 @@ return (_ctx, _cache) => {
|
||||
onClick: testConnection,
|
||||
loading: testing.value
|
||||
}, {
|
||||
default: _withCtx(() => _cache[42] || (_cache[42] = [
|
||||
default: _withCtx(() => _cache[44] || (_cache[44] = [
|
||||
_createTextVNode("测试连接")
|
||||
])),
|
||||
_: 1
|
||||
@@ -832,7 +873,7 @@ return (_ctx, _cache) => {
|
||||
onClick: saveConfig,
|
||||
loading: saving.value
|
||||
}, {
|
||||
default: _withCtx(() => _cache[43] || (_cache[43] = [
|
||||
default: _withCtx(() => _cache[45] || (_cache[45] = [
|
||||
_createTextVNode(" 保存配置 ")
|
||||
])),
|
||||
_: 1
|
||||
@@ -847,7 +888,7 @@ return (_ctx, _cache) => {
|
||||
variant: "tonal",
|
||||
closable: "",
|
||||
class: "ma-4 mt-0",
|
||||
"onClick:close": _cache[20] || (_cache[20] = $event => (testResult.show = false))
|
||||
"onClick:close": _cache[21] || (_cache[21] = $event => (testResult.show = false))
|
||||
}, {
|
||||
default: _withCtx(() => [
|
||||
_createElementVNode("div", _hoisted_2, [
|
||||
@@ -874,6 +915,6 @@ return (_ctx, _cache) => {
|
||||
}
|
||||
|
||||
};
|
||||
const ConfigComponent = /*#__PURE__*/_export_sfc(_sfc_main, [['__scopeId',"data-v-c50374dc"]]);
|
||||
const ConfigComponent = /*#__PURE__*/_export_sfc(_sfc_main, [['__scopeId',"data-v-bd2044a8"]]);
|
||||
|
||||
export { ConfigComponent as default };
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
.plugin-config[data-v-c50374dc] {
|
||||
.plugin-config[data-v-bd2044a8] {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
15503
plugins.v2/clashruleprovider/dist/assets/__federation_expose_Dashboard-DEFRy9WP.js
vendored
Normal file
15503
plugins.v2/clashruleprovider/dist/assets/__federation_expose_Dashboard-DEFRy9WP.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,15 +0,0 @@
|
||||
import { importShared } from './__federation_fn_import-JrT3xvdd.js';
|
||||
import { _ as _export_sfc } from './_plugin-vue_export-helper-pcqpp-6-.js';
|
||||
|
||||
const _sfc_main = {};
|
||||
const {openBlock:_openBlock,createElementBlock:_createElementBlock} = await importShared('vue');
|
||||
|
||||
|
||||
const _hoisted_1 = { class: "dashboard-widget" };
|
||||
|
||||
function _sfc_render(_ctx, _cache) {
|
||||
return (_openBlock(), _createElementBlock("div", _hoisted_1))
|
||||
}
|
||||
const DashboardComponent = /*#__PURE__*/_export_sfc(_sfc_main, [['render',_sfc_render]]);
|
||||
|
||||
export { DashboardComponent as default };
|
||||
4
plugins.v2/clashruleprovider/dist/assets/__federation_expose_Dashboard-IipjE6HA.css
vendored
Normal file
4
plugins.v2/clashruleprovider/dist/assets/__federation_expose_Dashboard-IipjE6HA.css
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
|
||||
.dashboard-widget[data-v-6c8bed46] {
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,14 +1,14 @@
|
||||
|
||||
.plugin-page[data-v-455476d4] {
|
||||
.plugin-page[data-v-eae2a1d4] {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* 使卡片等宽并适应移动端 */
|
||||
.d-flex.flex-wrap[data-v-455476d4] {
|
||||
.d-flex.flex-wrap[data-v-eae2a1d4] {
|
||||
gap: 16px;
|
||||
}
|
||||
.url-display[data-v-455476d4] {
|
||||
.url-display[data-v-eae2a1d4] {
|
||||
word-break: break-all;
|
||||
padding: 8px;
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
@@ -17,19 +17,19 @@
|
||||
|
||||
/* 移动端堆叠布局 */
|
||||
@media (max-width: 768px) {
|
||||
.d-flex.flex-wrap[data-v-455476d4] {
|
||||
.d-flex.flex-wrap[data-v-eae2a1d4] {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
/* Add visual distinction between sections */
|
||||
.ruleset-section[data-v-455476d4] {
|
||||
.ruleset-section[data-v-eae2a1d4] {
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
padding: 16px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.top-section[data-v-455476d4] {
|
||||
.top-section[data-v-eae2a1d4] {
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
padding: 16px;
|
||||
@@ -37,12 +37,12 @@
|
||||
}
|
||||
|
||||
/* Optional: Add different border colors to further distinguish */
|
||||
.ruleset-section[data-v-455476d4] {
|
||||
.ruleset-section[data-v-eae2a1d4] {
|
||||
border-left: 4px solid #2196F3; /* Blue accent */
|
||||
}
|
||||
.top-section[data-v-455476d4] {
|
||||
.top-section[data-v-eae2a1d4] {
|
||||
border-left: 4px solid #4CAF50; /* Green accent */
|
||||
}
|
||||
.drag-handle[data-v-455476d4] {
|
||||
.drag-handle[data-v-eae2a1d4] {
|
||||
cursor: move;
|
||||
}
|
||||
@@ -2,14 +2,14 @@ const currentImports = {};
|
||||
const exportSet = new Set(['Module', '__esModule', 'default', '_export_sfc']);
|
||||
let moduleMap = {
|
||||
"./Page":()=>{
|
||||
dynamicLoadingCss(["__federation_expose_Page-Djrrbsow.css"], false, './Page');
|
||||
return __federation_import('./__federation_expose_Page-D_nruYha.js').then(module =>Object.keys(module).every(item => exportSet.has(item)) ? () => module.default : () => module)},
|
||||
dynamicLoadingCss(["__federation_expose_Page-DQjiFgWw.css"], false, './Page');
|
||||
return __federation_import('./__federation_expose_Page--ZdI8TQS.js').then(module =>Object.keys(module).every(item => exportSet.has(item)) ? () => module.default : () => module)},
|
||||
"./Config":()=>{
|
||||
dynamicLoadingCss(["__federation_expose_Config-BJvXq0hj.css"], false, './Config');
|
||||
return __federation_import('./__federation_expose_Config-Btg4HYx3.js').then(module =>Object.keys(module).every(item => exportSet.has(item)) ? () => module.default : () => module)},
|
||||
dynamicLoadingCss(["__federation_expose_Config-xrxN2F1l.css"], false, './Config');
|
||||
return __federation_import('./__federation_expose_Config-sTz3W3yr.js').then(module =>Object.keys(module).every(item => exportSet.has(item)) ? () => module.default : () => module)},
|
||||
"./Dashboard":()=>{
|
||||
dynamicLoadingCss([], false, './Dashboard');
|
||||
return __federation_import('./__federation_expose_Dashboard-DKtydfsT.js').then(module =>Object.keys(module).every(item => exportSet.has(item)) ? () => module.default : () => module)},};
|
||||
dynamicLoadingCss(["__federation_expose_Dashboard-IipjE6HA.css"], false, './Dashboard');
|
||||
return __federation_import('./__federation_expose_Dashboard-DEFRy9WP.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;
|
||||
|
||||
3
plugins.v2/clashruleprovider/requirements.txt
Normal file
3
plugins.v2/clashruleprovider/requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
websockets
|
||||
httpx~=0.28.1
|
||||
sse_starlette~=2.3.6
|
||||
Reference in New Issue
Block a user