update(ClashRuleProvider): 支持配置 Hosts

This commit is contained in:
wumode
2025-08-01 18:36:04 +08:00
parent 969ed23d4c
commit d0ddbfde5f
9 changed files with 902 additions and 294 deletions

View File

@@ -461,11 +461,12 @@
"name": "Clash Rule Provider",
"description": "随时为Clash添加一些额外的规则。",
"labels": "工具",
"version": "1.2.8",
"version": "1.3.1",
"icon": "Mihomo_Meta_A.png",
"author": "wumode",
"level": 1,
"history": {
"v1.3.1": "支持配置 Hosts",
"v1.2.8": "改进导入界面",
"v1.2.7": "修复分享链接解析错误",
"v1.2.6": "修复代理组修改丢失问题",

View File

@@ -4,7 +4,6 @@ import urllib
from typing import Any, Optional, List, Dict, Tuple, Union
import time
from urllib.parse import urlparse
import yaml
import hashlib
from datetime import datetime, timedelta
@@ -14,7 +13,6 @@ import math
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.cron import CronTrigger
import httpx
import asyncio
from fastapi import HTTPException, Request, status, Body, Response
import websockets
@@ -23,9 +21,10 @@ from sse_starlette.sse import EventSourceResponse
from app import schemas
from app.core.config import settings
from app.log import logger
from app.plugins import _PluginBase
from app.schemas.types import NotificationType
from app.utils.http import RequestUtils
from app.utils.ip import IpUtils
from app.utils.http import RequestUtils, AsyncRequestUtils
from app.plugins import _PluginBase
from app.plugins.clashruleprovider.clashruleparser import ClashRuleParser, Converter
from app.plugins.clashruleprovider.clashruleparser import Action, RuleType, ClashRule, MatchRule, LogicRule
from app.plugins.clashruleprovider.clashruleparser import ProxyGroup, RuleProvider
@@ -39,7 +38,7 @@ class ClashRuleProvider(_PluginBase):
# 插件图标
plugin_icon = "Mihomo_Meta_A.png"
# 插件版本
plugin_version = "1.2.8"
plugin_version = "1.3.1"
# 插件作者
plugin_author = "wumode"
# 作者主页
@@ -77,6 +76,8 @@ class ClashRuleProvider(_PluginBase):
_dashboard_components: List[str] = []
_clash_template_yaml: str = ''
_hint_geo_dat: bool = False
# Cloudflare 优选 IPs 可通过外部设置
_best_cf_ip: List[str] = []
# 插件数据
_top_rules: List[str] = []
@@ -91,6 +92,7 @@ class ClashRuleProvider(_PluginBase):
_acl4ssr_prefix: str = '🗂️=>'
# 保存每个订阅文件的原始内容
_clash_configs: Dict[str, Any] = {}
_hosts: List[Dict[str, Any]] = []
# protected variables
_clash_rule_parser = None
@@ -111,6 +113,7 @@ class ClashRuleProvider(_PluginBase):
self._ruleset_names = self.get_data("ruleset_names") or {}
self._acl4ssr_providers = self.get_data("acl4ssr_providers") or {}
self._clash_configs = self.get_data("clash_configs") or {}
self._hosts = self.get_data("hosts") or []
if config:
self._enabled = config.get("enabled")
self._proxy = config.get("proxy")
@@ -126,7 +129,7 @@ class ClashRuleProvider(_PluginBase):
self._movie_pilot_url = config.get("movie_pilot_url")
if self._movie_pilot_url and self._movie_pilot_url[-1] == '/':
self._movie_pilot_url = self._movie_pilot_url[:-1]
self._cron = config.get("cron_string")
self._cron = config.get("cron_string") or '30 12 * * *'
self._timeout = config.get("timeout")
self._retry_times = config.get("retry_times") or 3
self._filter_keywords = config.get("filter_keywords")
@@ -140,6 +143,7 @@ class ClashRuleProvider(_PluginBase):
self._dashboard_components = config.get("dashboard_components") or []
self._clash_template_yaml = config.get("clash_template") or ''
self._hint_geo_dat = config.get("hint_geo_dat", False)
self._best_cf_ip = config.get("best_cf_ip") or []
self._clash_rule_parser = ClashRuleParser()
self._ruleset_rule_parser = ClashRuleParser()
self._clash_template = {}
@@ -367,6 +371,30 @@ class ClashRuleProvider(_PluginBase):
"summary": "导入规则",
"description": "导入规则"
},
{
"path": "/hosts",
"endpoint": self.get_hosts,
"methods": ["GET"],
"auth": "bear",
"summary": "获取 Hosts",
"description": "获取 Hosts"
},
{
"path": "/host",
"endpoint": self.update_hosts,
"methods": ["POST"],
"auth": "bear",
"summary": "更新 Host",
"description": "更新 Host"
},
{
"path": "/host",
"endpoint": self.delete_host,
"methods": ["DELETE"],
"auth": "bear",
"summary": "删除一条 Host",
"description": "删除一条 Host"
},
{
"path": "/config",
"endpoint": self.get_clash_config,
@@ -471,6 +499,16 @@ class ClashRuleProvider(_PluginBase):
}]
return []
def update_best_cf_ip(self, ips: List[str]):
"""
通过深拷贝更新 Cloudflare 优选 IPs
:param ips: Best Cloudflare IPs
"""
self._best_cf_ip = [*ips]
config = self.get_config()
config['best_cf_ip'] = self._best_cf_ip
self.update_config(config)
def __save_data(self):
self.__insert_ruleset()
self._top_rules = self._clash_rule_parser.to_list()
@@ -485,6 +523,7 @@ class ClashRuleProvider(_PluginBase):
self.save_data('extra_rule_providers', self._extra_rule_providers)
self.save_data('acl4ssr_providers', self._acl4ssr_providers)
self.save_data('clash_configs', self._clash_configs)
self.save_data('hosts', self._hosts)
def __parse_config(self):
if self._top_rules is None:
@@ -505,6 +544,7 @@ class ClashRuleProvider(_PluginBase):
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:
@@ -536,34 +576,32 @@ class ClashRuleProvider(_PluginBase):
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)}")
response = await AsyncRequestUtils().get_res(url, headers=clash_headers, timeout=10)
if response is None:
raise HTTPException(status_code=502, detail=f"Failed to fetch {endpoint}")
return response.json()
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:
async def test_connectivity(self, params: Dict[str, Any]) -> schemas.Response:
if not self._enabled:
return schemas.Response(success=False, message="")
if not params.get('clash_dashboard_url') or not params.get('clash_dashboard_secret') \
or not params.get('sub_link'):
or not params.get('sub_links'):
return schemas.Response(success=True, message="missing params")
clash_version_url = f"{params.get('clash_dashboard_url')}/version"
ret = RequestUtils(accept_type="application/json",
ret = await AsyncRequestUtils(accept_type="application/json",
headers={"authorization": f"Bearer {params.get('clash_dashboard_secret')}"}
).get(clash_version_url)
if not ret:
if ret is None:
return schemas.Response(success=False, message="无法连接到Clash")
ret = RequestUtils(accept_type="text/html",
proxies=settings.PROXY if self._proxy else None
).get(params.get('sub_link'))
if not ret:
return schemas.Response(success=False, message=f"Unable to get {params.get('sub_link')}")
for sub_link in (params.get('sub_links') or []):
ret = await AsyncRequestUtils(accept_type="text/html",
proxies=settings.PROXY if self._proxy else None
).get(sub_link)
if ret is None:
return schemas.Response(success=False, message=f"Unable to fetch {sub_link}")
return schemas.Response(success=True, message="测试连接成功")
def get_ruleset(self, name):
@@ -585,6 +623,7 @@ class ClashRuleProvider(_PluginBase):
"data": {"state": self._enabled,
"ruleset_prefix": self._ruleset_prefix,
"clash": {"rule_size": rule_size},
"best_cf_ip": self._best_cf_ip,
"geoRules": self._geo_rules,
"subscription_info": self._subscription_info,
"sub_url": f"{self._movie_pilot_url}/api/v1/plugin/ClashRuleProvider/config?"
@@ -608,6 +647,47 @@ class ClashRuleProvider(_PluginBase):
f'expire={sub_info.get("expire", 0)}'}
return Response(headers=headers, content=res, media_type="text/yaml")
def get_hosts(self) -> schemas.Response:
if not self._enabled:
schemas.Response(success=True, message='', data={'hosts': []})
return schemas.Response(success=True, message='', data={'hosts': self._hosts})
def update_hosts(self, params: dict = Body(...)) -> schemas.Response:
if not self._enabled:
return schemas.Response(success=False, message='')
domain = params.get('domain')
if not domain:
return schemas.Response(success=False, message=f"Invalid param: domain={domain}")
# Search for the host with the same domain
for i, host in enumerate(self._hosts):
if host['domain'] == domain:
# Update the existing host
self._hosts[i] = {**host, **params.get('value', {})}
self.save_data('hosts', self._hosts)
return schemas.Response(success=True, message=f'Host for domain {domain} updated successfully.')
self._hosts.append(params.get('value', {}))
self.save_data('hosts', self._hosts)
return schemas.Response(success=True, message=f"New host for domain {domain} added successfully.")
def delete_host(self, params: dict = Body(...)) -> schemas.Response:
if not self._enabled:
return schemas.Response(success=False, message='Host deletion is disabled.')
domain = params.get('domain')
if not domain:
return schemas.Response(success=False, message=f"Invalid param: domain={domain}")
original_hosts_length = len(self._hosts)
self._hosts = [host for host in self._hosts if host.get('domain') != domain]
self.save_data('hosts', self._hosts)
if len(self._hosts) < original_hosts_length:
return schemas.Response(success=True, message=f'Host for domain {domain} deleted successfully.')
else:
return schemas.Response(success=False, message=f'Host for domain {domain} not found.')
def get_rules(self, rule_type: str) -> schemas.Response:
if rule_type == 'ruleset':
return schemas.Response(success=True, message='', data={'rules': self._ruleset_rule_parser.to_dict()})
@@ -664,9 +744,9 @@ class ClashRuleProvider(_PluginBase):
self.__update_rules(params.get('rules'), self._clash_rule_parser)
return schemas.Response(success=True)
def update_rule(self, params: Dict[str, Any]) -> Dict[str, Any]:
def update_rule(self, params: Dict[str, Any]) -> schemas.Response:
if not self._enabled:
return {"success": False, "message": ""}
return schemas.Response(success=False, message='')
if params.get('type') == 'ruleset':
original_rule = self._ruleset_rule_parser.get_rule_at_priority(params.get('priority'))
res = self.update_rule_by_priority(params.get('rule_data'),
@@ -679,7 +759,7 @@ class ClashRuleProvider(_PluginBase):
self.__add_notification_job(ruleset_to_notify)
else:
res = self.update_rule_by_priority(params.get('rule_data'), params.get('priority'), self._clash_rule_parser)
return {"success": bool(res), "message": None}
return schemas.Response(success=bool(res), message='')
def add_rule(self, params: Dict[str, Any]) -> schemas.Response:
if not self._enabled:
@@ -697,7 +777,7 @@ class ClashRuleProvider(_PluginBase):
return schemas.Response(success=False, message="")
url = params.get('url')
if not url:
return schemas.Response(success=False, message="missing params")
return schemas.Response(success=False, message="Missing params")
config, info = self.__get_subscription(url)
if not config:
return schemas.Response(success=False, message=f"订阅链接 {url} 更新失败")
@@ -1051,12 +1131,14 @@ class ClashRuleProvider(_PluginBase):
def refresh_subscription_service(self):
res = self.refresh_subscriptions()
messages = []
index = 1
for url, result in res.items():
try:
host_name = urlparse(url).hostname
except ValueError:
host_name = url
message = f"1. 「 {host_name}\n"
message = f"{index}. 「 {host_name}\n"
index += 1
if result:
sub_info = self._subscription_info.get(url, {})
if sub_info.get('total') is not None:
@@ -1389,6 +1471,16 @@ class ClashRuleProvider(_PluginBase):
continue
top_rules.append(rule.raw_rule)
clash_config["rules"] = top_rules
# 添加 Hosts
if self._hosts:
clash_config.setdefault('hosts', {})
new_hosts = {
item['domain']: item.get('value', []) if not item.get('using_cloudflare') else self._best_cf_ip
for item in self._hosts if item.get('domain')
}
clash_config["hosts"] = {**clash_config["hosts"], **new_hosts}
if self._rule_provider:
clash_config['rule-providers'] = clash_config.get('rule-providers') or {}
clash_config['rule-providers'].update(self._rule_provider)
@@ -1404,3 +1496,13 @@ class ClashRuleProvider(_PluginBase):
self.save_data('ruleset_names', self._ruleset_names)
self.save_data('rule_provider', self._rule_provider)
return clash_config
@property
def best_cf_ipv4(self) -> List[str]:
v4 = [ip for ip in self._best_cf_ip if IpUtils.is_ipv4(ip)]
return v4
@property
def best_cf_ipv6(self) -> List[str]:
v6 = [ip for ip in self._best_cf_ip if IpUtils.is_ipv6(ip)]
return v6

View File

@@ -0,0 +1,4 @@
.plugin-config[data-v-929102b8] {
margin: 0 auto;
}

View File

@@ -1,4 +0,0 @@
.plugin-config[data-v-c67dff26] {
margin: 0 auto;
}

View File

@@ -89,6 +89,7 @@ const defaultConfig = {
dashboard_components: [],
clash_template: '',
hint_geo_dat: false,
best_cf_ip: []
};
// 响应式配置对象
@@ -128,6 +129,25 @@ function validateSubLinks(links) {
return true
}
function isValidIP(ip) {
// IPv4 正则四段数字0255用点隔开
const ipv4Regex = /^(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}$/;
// IPv6 正则:八组 14 位 16 进制数,用冒号隔开,支持简写 ::(不严格支持所有极端情况,但能覆盖大多数合法 IPv6
const ipv6Regex = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|(([0-9a-fA-F]{1,4}:){1,7}|:):([0-9a-fA-F]{1,4}:){0,6}[0-9a-fA-F]{1,4})$/;
return ipv4Regex.test(ip) || ipv6Regex.test(ip);
}
function validateIPs(ips) {
for (const ip of ips) {
if (!isValidIP(ip)) {
return `无效的 IP 地址: ${ip}`
}
}
return true
}
// 测试连接
async function testConnection() {
testing.value = true;
@@ -152,7 +172,7 @@ async function testConnection() {
const testParams = {
clash_dashboard_url: config.clash_dashboard_url,
clash_dashboard_secret: config.clash_dashboard_secret,
sub_link: config.sub_links[0] // 使用第一个订阅链接进行测试
sub_links: config.sub_links // 使用第一个订阅链接进行测试
};
// 调用API进行连接测试
@@ -284,7 +304,7 @@ return (_ctx, _cache) => {
}, {
default: _withCtx(() => [
_createVNode(_component_v_icon, { left: "" }, {
default: _withCtx(() => _cache[28] || (_cache[28] = [
default: _withCtx(() => _cache[29] || (_cache[29] = [
_createTextVNode("mdi-close")
])),
_: 1
@@ -295,7 +315,7 @@ return (_ctx, _cache) => {
]),
default: _withCtx(() => [
_createVNode(_component_v_card_title, null, {
default: _withCtx(() => _cache[27] || (_cache[27] = [
default: _withCtx(() => _cache[28] || (_cache[28] = [
_createTextVNode("Clash Rule Provider 插件配置")
])),
_: 1
@@ -321,11 +341,11 @@ return (_ctx, _cache) => {
ref_key: "form",
ref: form,
modelValue: isFormValid.value,
"onUpdate:modelValue": _cache[21] || (_cache[21] = $event => ((isFormValid).value = $event)),
"onUpdate:modelValue": _cache[22] || (_cache[22] = $event => ((isFormValid).value = $event)),
onSubmit: _withModifiers(saveConfig, ["prevent"])
}, {
default: _withCtx(() => [
_cache[42] || (_cache[42] = _createElementVNode("div", { class: "text-subtitle-1 font-weight-bold mt-4 mb-2" }, "基本设置", -1)),
_cache[43] || (_cache[43] = _createElementVNode("div", { class: "text-subtitle-1 font-weight-bold mb-2" }, "基本设置", -1)),
_createVNode(_component_v_row, null, {
default: _withCtx(() => [
_createVNode(_component_v_col, {
@@ -399,7 +419,7 @@ return (_ctx, _cache) => {
]),
_: 1
}),
_cache[43] || (_cache[43] = _createElementVNode("div", { class: "text-subtitle-1 font-weight-bold mt-4 mb-2" }, "订阅配置", -1)),
_cache[44] || (_cache[44] = _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" }, {
@@ -441,6 +461,7 @@ return (_ctx, _cache) => {
multiple: "",
chips: "",
"closable-chips": "",
clearable: "",
hint: "添加用于过滤节点的关键词"
}, {
chip: _withCtx(({ props, item }) => [
@@ -463,7 +484,7 @@ return (_ctx, _cache) => {
]),
_: 1
}),
_cache[44] || (_cache[44] = _createElementVNode("div", { class: "text-subtitle-1 font-weight-bold mt-4 mb-2" }, "Clash 面板设置", -1)),
_cache[45] || (_cache[45] = _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" }, {
@@ -479,7 +500,7 @@ return (_ctx, _cache) => {
}, {
"prepend-inner": _withCtx(() => [
_createVNode(_component_v_icon, { color: "primary" }, {
default: _withCtx(() => _cache[29] || (_cache[29] = [
default: _withCtx(() => _cache[30] || (_cache[30] = [
_createTextVNode("mdi-web")
])),
_: 1
@@ -508,7 +529,7 @@ return (_ctx, _cache) => {
}, {
"prepend-inner": _withCtx(() => [
_createVNode(_component_v_icon, { color: "warning" }, {
default: _withCtx(() => _cache[30] || (_cache[30] = [
default: _withCtx(() => _cache[31] || (_cache[31] = [
_createTextVNode("mdi-key")
])),
_: 1
@@ -538,7 +559,7 @@ return (_ctx, _cache) => {
}, {
"prepend-inner": _withCtx(() => [
_createVNode(_component_v_icon, { color: "info" }, {
default: _withCtx(() => _cache[31] || (_cache[31] = [
default: _withCtx(() => _cache[32] || (_cache[32] = [
_createTextVNode("mdi-view-dashboard")
])),
_: 1
@@ -552,7 +573,7 @@ return (_ctx, _cache) => {
]),
_: 1
}),
_cache[45] || (_cache[45] = _createElementVNode("div", { class: "text-subtitle-1 font-weight-bold mt-4 mb-2" }, "MoviePilot 设置", -1)),
_cache[46] || (_cache[46] = _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" }, {
@@ -568,7 +589,7 @@ return (_ctx, _cache) => {
}, {
"prepend-inner": _withCtx(() => [
_createVNode(_component_v_icon, { color: "success" }, {
default: _withCtx(() => _cache[32] || (_cache[32] = [
default: _withCtx(() => _cache[33] || (_cache[33] = [
_createTextVNode("mdi-movie")
])),
_: 1
@@ -582,10 +603,13 @@ return (_ctx, _cache) => {
]),
_: 1
}),
_cache[46] || (_cache[46] = _createElementVNode("div", { class: "text-subtitle-1 font-weight-bold mt-4 mb-2" }, "执行设置", -1)),
_cache[47] || (_cache[47] = _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" }, {
_createVNode(_component_v_col, {
cols: "12",
md: "4"
}, {
default: _withCtx(() => [
_createVNode(_component_v_cron_field, {
modelValue: config.cron_string,
@@ -596,7 +620,7 @@ return (_ctx, _cache) => {
}, {
"prepend-inner": _withCtx(() => [
_createVNode(_component_v_icon, { color: "info" }, {
default: _withCtx(() => _cache[33] || (_cache[33] = [
default: _withCtx(() => _cache[34] || (_cache[34] = [
_createTextVNode("mdi-clock-time-four-outline")
])),
_: 1
@@ -609,7 +633,7 @@ return (_ctx, _cache) => {
}),
_createVNode(_component_v_col, {
cols: "12",
md: "6"
md: "4"
}, {
default: _withCtx(() => [
_createVNode(_component_v_text_field, {
@@ -630,7 +654,7 @@ return (_ctx, _cache) => {
}),
_createVNode(_component_v_col, {
cols: "12",
md: "6"
md: "4"
}, {
default: _withCtx(() => [
_createVNode(_component_v_text_field, {
@@ -647,7 +671,7 @@ return (_ctx, _cache) => {
}, {
"prepend-inner": _withCtx(() => [
_createVNode(_component_v_icon, { color: "info" }, {
default: _withCtx(() => _cache[34] || (_cache[34] = [
default: _withCtx(() => _cache[35] || (_cache[35] = [
_createTextVNode("mdi-refresh")
])),
_: 1
@@ -671,12 +695,12 @@ return (_ctx, _cache) => {
_createVNode(_component_v_expansion_panel_title, null, {
default: _withCtx(() => [
_createVNode(_component_v_icon, { class: "mr-2" }, {
default: _withCtx(() => _cache[35] || (_cache[35] = [
default: _withCtx(() => _cache[36] || (_cache[36] = [
_createTextVNode("mdi-cog")
])),
_: 1
}),
_cache[36] || (_cache[36] = _createTextVNode(" 高级选项 "))
_cache[37] || (_cache[37] = _createTextVNode(" 高级选项 "))
]),
_: 1
}),
@@ -769,7 +793,7 @@ return (_ctx, _cache) => {
}, {
"prepend-inner": _withCtx(() => [
_createVNode(_component_v_icon, { color: "info" }, {
default: _withCtx(() => _cache[37] || (_cache[37] = [
default: _withCtx(() => _cache[38] || (_cache[38] = [
_createTextVNode("mdi-palette")
])),
_: 1
@@ -796,7 +820,7 @@ return (_ctx, _cache) => {
}, {
"prepend-inner": _withCtx(() => [
_createVNode(_component_v_icon, { color: "primary" }, {
default: _withCtx(() => _cache[38] || (_cache[38] = [
default: _withCtx(() => _cache[39] || (_cache[39] = [
_createTextVNode("mdi-palette")
])),
_: 1
@@ -827,7 +851,7 @@ return (_ctx, _cache) => {
}, {
"prepend-inner": _withCtx(() => [
_createVNode(_component_v_icon, { color: "info" }, {
default: _withCtx(() => _cache[39] || (_cache[39] = [
default: _withCtx(() => _cache[40] || (_cache[40] = [
_createTextVNode("mdi-clock-outline")
])),
_: 1
@@ -851,12 +875,12 @@ return (_ctx, _cache) => {
}, {
default: _withCtx(() => [
_createVNode(_component_v_icon, { left: "" }, {
default: _withCtx(() => _cache[40] || (_cache[40] = [
default: _withCtx(() => _cache[41] || (_cache[41] = [
_createTextVNode("mdi-import")
])),
_: 1
}),
_cache[41] || (_cache[41] = _createTextVNode(" Clash 配置模板 "))
_cache[42] || (_cache[42] = _createTextVNode(" Clash 配置模板 "))
]),
_: 1
})
@@ -865,6 +889,44 @@ return (_ctx, _cache) => {
})
]),
_: 1
}),
_createVNode(_component_v_row, null, {
default: _withCtx(() => [
_createVNode(_component_v_col, {
cols: "12",
md: "12"
}, {
default: _withCtx(() => [
_createVNode(_component_v_combobox, {
modelValue: config.best_cf_ip,
"onUpdate:modelValue": _cache[21] || (_cache[21] = $event => ((config.best_cf_ip) = $event)),
label: "Cloudflare CDN 优选 IPs",
variant: "outlined",
multiple: "",
chips: "",
"closable-chips": "",
clearable: "",
hint: "用于设置 Hosts 中的 Cloudflare 域名",
rules: [validateIPs]
}, {
chip: _withCtx(({ props, item }) => [
_createVNode(_component_v_chip, _mergeProps(props, {
closable: "",
size: "small"
}), {
default: _withCtx(() => [
_createTextVNode(_toDisplayString(item.value), 1)
]),
_: 2
}, 1040)
]),
_: 1
}, 8, ["modelValue", "rules"])
]),
_: 1
})
]),
_: 1
})
]),
_: 1
@@ -884,10 +946,9 @@ return (_ctx, _cache) => {
_createVNode(_component_v_alert, {
type: "info",
text: "",
class: "mb-6",
variant: "tonal"
}, {
default: _withCtx(() => _cache[47] || (_cache[47] = [
default: _withCtx(() => _cache[48] || (_cache[48] = [
_createTextVNode(" 配置说明参考: "),
_createElementVNode("a", {
href: "https://github.com/wumode/MoviePilot-Plugins/tree/main/plugins.v2/clashruleprovider/README.md",
@@ -904,12 +965,12 @@ return (_ctx, _cache) => {
}, {
default: _withCtx(() => [
_createVNode(_component_v_icon, { left: "" }, {
default: _withCtx(() => _cache[48] || (_cache[48] = [
default: _withCtx(() => _cache[49] || (_cache[49] = [
_createTextVNode("mdi-view-dashboard-edit")
])),
_: 1
}),
_cache[49] || (_cache[49] = _createTextVNode(" 规则 "))
_cache[50] || (_cache[50] = _createTextVNode(" 规则 "))
]),
_: 1
}),
@@ -917,7 +978,7 @@ return (_ctx, _cache) => {
color: "secondary",
onClick: resetForm
}, {
default: _withCtx(() => _cache[50] || (_cache[50] = [
default: _withCtx(() => _cache[51] || (_cache[51] = [
_createTextVNode("重置")
])),
_: 1
@@ -927,7 +988,7 @@ return (_ctx, _cache) => {
onClick: testConnection,
loading: testing.value
}, {
default: _withCtx(() => _cache[51] || (_cache[51] = [
default: _withCtx(() => _cache[52] || (_cache[52] = [
_createTextVNode("测试连接")
])),
_: 1
@@ -939,7 +1000,7 @@ return (_ctx, _cache) => {
onClick: saveConfig,
loading: saving.value
}, {
default: _withCtx(() => _cache[52] || (_cache[52] = [
default: _withCtx(() => _cache[53] || (_cache[53] = [
_createTextVNode(" 保存配置 ")
])),
_: 1
@@ -954,7 +1015,7 @@ return (_ctx, _cache) => {
variant: "tonal",
closable: "",
class: "ma-4 mt-0",
"onClick:close": _cache[22] || (_cache[22] = $event => (testResult.show = false))
"onClick:close": _cache[23] || (_cache[23] = $event => (testResult.show = false))
}, {
default: _withCtx(() => [
_createElementVNode("div", _hoisted_2, [
@@ -979,14 +1040,14 @@ return (_ctx, _cache) => {
]),
_createVNode(_component_v_dialog, {
modelValue: clashTemplateDialog.value,
"onUpdate:modelValue": _cache[26] || (_cache[26] = $event => ((clashTemplateDialog).value = $event)),
"onUpdate:modelValue": _cache[27] || (_cache[27] = $event => ((clashTemplateDialog).value = $event)),
"max-width": "600"
}, {
default: _withCtx(() => [
_createVNode(_component_v_card, null, {
default: _withCtx(() => [
_createVNode(_component_v_card_title, null, {
default: _withCtx(() => _cache[53] || (_cache[53] = [
default: _withCtx(() => _cache[54] || (_cache[54] = [
_createTextVNode("Clash 配置模板")
])),
_: 1
@@ -995,14 +1056,14 @@ return (_ctx, _cache) => {
default: _withCtx(() => [
_createVNode(_component_v_select, {
modelValue: clashTemplateType.value,
"onUpdate:modelValue": _cache[23] || (_cache[23] = $event => ((clashTemplateType).value = $event)),
"onUpdate:modelValue": _cache[24] || (_cache[24] = $event => ((clashTemplateType).value = $event)),
items: ['YAML'],
label: "配置类型",
class: "mb-4"
}, null, 8, ["modelValue"]),
_createVNode(_unref(VAceEditor), {
value: clashTemplateContent.value,
"onUpdate:value": _cache[24] || (_cache[24] = $event => ((clashTemplateContent).value = $event)),
"onUpdate:value": _cache[25] || (_cache[25] = $event => ((clashTemplateContent).value = $event)),
lang: "yaml",
theme: "monokai",
hint: "",
@@ -1017,7 +1078,7 @@ return (_ctx, _cache) => {
class: "mb-4",
variant: "tonal"
}, {
default: _withCtx(() => _cache[54] || (_cache[54] = [
default: _withCtx(() => _cache[55] || (_cache[55] = [
_createTextVNode("规则和出站代理会被添加在配置模板上 ")
])),
_: 1
@@ -1030,9 +1091,9 @@ return (_ctx, _cache) => {
_createVNode(_component_v_spacer),
_createVNode(_component_v_btn, {
text: "",
onClick: _cache[25] || (_cache[25] = $event => (clashTemplateDialog.value = false))
onClick: _cache[26] || (_cache[26] = $event => (clashTemplateDialog.value = false))
}, {
default: _withCtx(() => _cache[55] || (_cache[55] = [
default: _withCtx(() => _cache[56] || (_cache[56] = [
_createTextVNode("取消")
])),
_: 1
@@ -1041,7 +1102,7 @@ return (_ctx, _cache) => {
color: "primary",
onClick: saveClashTemplate
}, {
default: _withCtx(() => _cache[56] || (_cache[56] = [
default: _withCtx(() => _cache[57] || (_cache[57] = [
_createTextVNode("确定")
])),
_: 1
@@ -1060,6 +1121,6 @@ return (_ctx, _cache) => {
}
};
const ConfigComponent = /*#__PURE__*/_export_sfc(_sfc_main, [['__scopeId',"data-v-c67dff26"]]);
const ConfigComponent = /*#__PURE__*/_export_sfc(_sfc_main, [['__scopeId',"data-v-929102b8"]]);
export { ConfigComponent as default };

View File

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

View File

@@ -2,11 +2,11 @@ const currentImports = {};
const exportSet = new Set(['Module', '__esModule', 'default', '_export_sfc']);
let moduleMap = {
"./Page":()=>{
dynamicLoadingCss(["__federation_expose_Page-BYQOdJxf.css"], false, './Page');
return __federation_import('./__federation_expose_Page-D07z4AMB.js').then(module =>Object.keys(module).every(item => exportSet.has(item)) ? () => module.default : () => module)},
dynamicLoadingCss(["__federation_expose_Page-BOym_1fV.css"], false, './Page');
return __federation_import('./__federation_expose_Page-D5l2MyNA.js').then(module =>Object.keys(module).every(item => exportSet.has(item)) ? () => module.default : () => module)},
"./Config":()=>{
dynamicLoadingCss(["__federation_expose_Config-CibZbOMJ.css"], false, './Config');
return __federation_import('./__federation_expose_Config-D6fcnbk5.js').then(module =>Object.keys(module).every(item => exportSet.has(item)) ? () => module.default : () => module)},
dynamicLoadingCss(["__federation_expose_Config-BrXQaadr.css"], false, './Config');
return __federation_import('./__federation_expose_Config-NH09p1Am.js').then(module =>Object.keys(module).every(item => exportSet.has(item)) ? () => module.default : () => module)},
"./Dashboard":()=>{
dynamicLoadingCss(["__federation_expose_Dashboard-vS9Qm2ZB.css"], false, './Dashboard');
return __federation_import('./__federation_expose_Dashboard-BDSt5WaH.js').then(module =>Object.keys(module).every(item => exportSet.has(item)) ? () => module.default : () => module)},};

View File

@@ -1,3 +1,2 @@
websockets
httpx~=0.28.1
sse_starlette~=2.3.6