mirror of
https://github.com/jxxghp/MoviePilot-Plugins.git
synced 2026-03-27 10:05:57 +00:00
refactor(ClashRuleProvider): 重构后端核心逻辑与数据模型
- 数据模型重构: 全面引入 Pydantic 模型(ClashConfig, Proxy, ProxyGroup 等)替代原有字典结构,提供更严格的数据验证与类型安全。 - 数据迁移机制: 新增 v2.1.0 数据升级脚本,支持将旧版代理、策略组及规则数据自动迁移至新架构。 - 配置补丁系统: 实现基于 JSON Patch 的细粒度配置修补机制,替代旧版覆盖逻辑,提升配置修改的灵活性。 - 服务层优化: 重写 ClashRuleProviderService 以适配新对象模型,增强代码可维护性与扩展性。 - API模型同步: 更新相关 API 数据模型以保持与内部数据结构的一致性。 - 用户界面: 批量规则管理和数据项隐藏支持
This commit is contained in:
@@ -508,12 +508,13 @@
|
||||
"name": "Clash Rule Provider",
|
||||
"description": "随时为Clash添加一些额外的规则。",
|
||||
"labels": "工具",
|
||||
"version": "2.0.10",
|
||||
"version": "2.1.1",
|
||||
"icon": "Mihomo_Meta_A.png",
|
||||
"author": "wumode",
|
||||
"level": 1,
|
||||
"release": true,
|
||||
"history": {
|
||||
"v2.1.1": "增强数据管理功能",
|
||||
"v2.0.10": "适配 MoviePilot 2.8.4",
|
||||
"v2.0.9": "适配 Pydantic V2 (主程序版本需高于 2.8.1-1)",
|
||||
"v2.0.8": "修复已知问题",
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
# Clash Rule Provider
|
||||
|
||||
**Clash Rule Provider** 生成适用于 [Meta Kernel](https://github.com/MetaCubeX/mihomo/tree/Meta) 定制配置,便于增加、修改和删除规则。
|
||||
**Clash Rule Provider** 是一个[MoviePilot](https://github.com/jxxghp/MoviePilot)插件,用于生成适用于 [Meta Kernel](https://github.com/MetaCubeX/mihomo/tree/Meta) 定制配置,便于增加、修改和删除规则,基于 Meta 内核丰富的代理组配置,提供灵活的路由功能。
|
||||
|
||||
- 即时通知 Clash 刷新规则集合
|
||||
- 基于 Meta 内核丰富的代理组配置,提供灵活的路由功能
|
||||
- 支持按大洲和国家分组节点
|
||||
- 支持覆写出站代理
|
||||
- GEO 规则输入提示
|
||||
@@ -13,7 +12,7 @@
|
||||
|
||||
### 规则集规则
|
||||
|
||||
用于添加能够在 Clash 中即时生效的规则,Clash Rule Provider 会根据每条规则的**出站**生成相应的**规则集合** `📂<-` + `出站`。
|
||||
用于添加能够在 Clash 中即时生效的规则,Clash Rule Provider 会根据每条规则的**出站**生成相应的**规则集合**。
|
||||
|
||||
### 置顶规则
|
||||
|
||||
@@ -41,4 +40,28 @@
|
||||
|
||||
### Hosts
|
||||
|
||||
如果需要自动更新此处使用的 Cloudflare IP, 可以通过其它[插件](https://github.com/wumode/MoviePilot-Addons)实现。
|
||||
如果需要自动更新此处使用的 Cloudflare IP, 可以通过其它[插件](https://github.com/wumode/MoviePilot-Addons)实现。
|
||||
|
||||
### 配置隐藏
|
||||
|
||||
如果希望某些代理组、规则或是代理节点仅在特定条件下可见,可以使用可见性限制功能。例如,可以设置某些规则集仅在特定网络环境下可见。
|
||||
自定义表达式是个返回`bool`值的Python表达式,可以使用以下变量:
|
||||
|
||||
```python
|
||||
# 请求 URL
|
||||
url: str
|
||||
# 客户端的IP地址
|
||||
client_host: str
|
||||
# 请求的标识符
|
||||
identifier: str | None = None
|
||||
# User-Agent
|
||||
user_agent : str | None = None
|
||||
```
|
||||
|
||||
表达式示例:
|
||||
- `client_host == '192.168.1.1'`
|
||||
- `identifier == 'office-laptop' and 'Mobile' in user_agent`
|
||||
|
||||
## 远程组件
|
||||
|
||||
[ClashRuleProvider-Remote](https://github.com/wumode/ClashRuleProvider-Remote)
|
||||
@@ -1,5 +1,3 @@
|
||||
import asyncio
|
||||
import copy
|
||||
import pytz
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, Optional, List, Dict, Tuple
|
||||
@@ -9,21 +7,28 @@ from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
from apscheduler.triggers.cron import CronTrigger
|
||||
from pydantic import ValidationError
|
||||
|
||||
from app.api.endpoints.plugin import register_plugin_api
|
||||
from app.core.config import settings, global_vars
|
||||
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.string import StringUtils
|
||||
|
||||
from .api import ClashRuleProviderApi, apis
|
||||
from .base import _ClashRuleProviderBase
|
||||
from .base import Constant
|
||||
from .config import PluginConfig
|
||||
from .helper.utilsprovider import UtilsProvider
|
||||
from .state import PluginState
|
||||
from .models import ProxyGroup, ProxyGroups, RuleProviders, Hosts
|
||||
from .models.api import SubscriptionsInfo
|
||||
from .models.configuration import ClashConfig
|
||||
from .models.datapatch import DataPatch
|
||||
from .models.types import DataKey, DataSource
|
||||
from .state import PluginState, GeoRules
|
||||
from .services import ClashRuleProviderService
|
||||
from .store import PluginStore
|
||||
|
||||
|
||||
class ClashRuleProvider(_ClashRuleProviderBase):
|
||||
class ClashRuleProvider(_PluginBase):
|
||||
# 插件名称
|
||||
plugin_name = "Clash Rule Provider"
|
||||
# 插件描述
|
||||
@@ -31,7 +36,7 @@ class ClashRuleProvider(_ClashRuleProviderBase):
|
||||
# 插件图标
|
||||
plugin_icon = "Mihomo_Meta_A.png"
|
||||
# 插件版本
|
||||
plugin_version = "2.0.10"
|
||||
plugin_version = "2.1.1"
|
||||
# 插件作者
|
||||
plugin_author = "wumode"
|
||||
# 作者主页
|
||||
@@ -42,110 +47,82 @@ class ClashRuleProvider(_ClashRuleProviderBase):
|
||||
plugin_order = 99
|
||||
# 可使用的用户级别
|
||||
auth_level = 1
|
||||
# 主线程事件循环
|
||||
event_loop: Optional[asyncio.AbstractEventLoop] = None
|
||||
|
||||
# Runtime variables
|
||||
services: ClashRuleProviderService
|
||||
api: ClashRuleProviderApi
|
||||
|
||||
def __init__(self):
|
||||
# Configuration attributes
|
||||
super().__init__()
|
||||
state: PluginState
|
||||
scheduler: AsyncIOScheduler | None = None
|
||||
|
||||
def init_plugin(self, conf: dict = None):
|
||||
self.stop_service()
|
||||
self.state = PluginState()
|
||||
self.config = PluginConfig()
|
||||
self.store = PluginStore(self.__class__.__name__)
|
||||
|
||||
# Load persistent data into state
|
||||
self.state.proxy_groups = self.get_data("proxy_groups") or []
|
||||
self.state.extra_proxies = self.get_data("extra_proxies") or []
|
||||
self.state.subscription_info = self.get_data("subscription_info") or {}
|
||||
self.state.rule_provider = self.get_data("rule_provider") or {}
|
||||
self.state.rule_providers = self.get_data("extra_rule_providers") or {}
|
||||
self.state.ruleset_names = self.get_data("ruleset_names") or {}
|
||||
self.state.acl4ssr_providers = self.get_data("acl4ssr_providers") or {}
|
||||
self.state.clash_configs = self.get_data("clash_configs") or {}
|
||||
self.state.hosts = self.get_data("hosts") or []
|
||||
self.state.overwritten_region_groups = self.get_data("overwritten_region_groups") or {}
|
||||
self.state.overwritten_proxies = self.get_data("overwritten_proxies") or {}
|
||||
self.state.geo_rules = self.get_data("geo_rules") or {'geoip': [], 'geosite': []}
|
||||
self.state = PluginState(self.__class__.__name__)
|
||||
self.upgrade_data()
|
||||
|
||||
if conf:
|
||||
try:
|
||||
raw_conf = PluginConfig.upgrade_conf(conf)
|
||||
self.config = PluginConfig.model_validate(raw_conf)
|
||||
self.state.config = PluginConfig.model_validate(conf)
|
||||
except ValidationError as e:
|
||||
logger.error(f"解析配置出错: {e}")
|
||||
return
|
||||
self._update_config()
|
||||
|
||||
if self.config.enabled:
|
||||
if self.state.config.enabled:
|
||||
self._initialize_plugin()
|
||||
|
||||
def upgrade_data(self):
|
||||
data_version = self.get_data(DataKey.DATA_VERSION) or "2.0.10"
|
||||
if StringUtils.compare_version(data_version, '<', "2.1.0"):
|
||||
from .helper.dataupgrader import v_2_1_0
|
||||
v_2_1_0.upgrade(self.__class__.__name__)
|
||||
|
||||
def _initialize_plugin(self):
|
||||
self.state.proxies_manager.clear()
|
||||
self.state.top_rules_manager.clear()
|
||||
self.state.ruleset_rules_manager.clear()
|
||||
|
||||
if ClashRuleProvider.event_loop is None:
|
||||
ClashRuleProvider.event_loop = global_vars.loop
|
||||
self.scheduler = AsyncIOScheduler(timezone=settings.TZ, event_loop=ClashRuleProvider.event_loop)
|
||||
self.services = ClashRuleProviderService(self.__class__.__name__, self.config, self.state, self.store,
|
||||
self.scheduler)
|
||||
self.api = ClashRuleProviderApi(self.services, self.config)
|
||||
self.scheduler = AsyncIOScheduler(timezone=settings.TZ, event_loop=global_vars.loop)
|
||||
self.services = ClashRuleProviderService(self.__class__.__name__, self.state, self.scheduler)
|
||||
self.api = ClashRuleProviderApi(self.services, self.state.config)
|
||||
|
||||
try:
|
||||
self.state.clash_template_dict = yaml.load(self.config.clash_template, Loader=yaml.SafeLoader) or {}
|
||||
if not isinstance(self.state.clash_template_dict, dict):
|
||||
self.state.clash_template_dict = {}
|
||||
clash_template_dict = yaml.load(self.state.config.clash_template, Loader=yaml.SafeLoader) or {}
|
||||
if isinstance(clash_template_dict, dict):
|
||||
self.state.clash_template = ClashConfig.model_validate(clash_template_dict)
|
||||
else:
|
||||
logger.error("Invalid clash template yaml")
|
||||
except yaml.YAMLError as exc:
|
||||
logger.error(f"Error loading clash template yaml: {exc}")
|
||||
self.state.clash_template_dict = {}
|
||||
|
||||
# Normalize template
|
||||
for key, default in self.DEFAULT_CLASH_CONF.items():
|
||||
self.state.clash_template_dict.setdefault(key, copy.deepcopy(default))
|
||||
|
||||
self.services.load_rules()
|
||||
self.services.load_proxies()
|
||||
|
||||
self.state.subscription_info = {url: self.state.subscription_info.get(url) or {}
|
||||
for url in self.config.sub_links}
|
||||
for _, sub_info in self.state.subscription_info.items():
|
||||
sub_info.setdefault('enabled', True)
|
||||
self.state.clash_configs = {url: self.state.clash_configs[url] for url in self.config.sub_links if
|
||||
self.state.clash_configs.get(url)}
|
||||
# Accessing subscription_info property triggers load from DB.
|
||||
sub_info_map = self.state.subscription_info
|
||||
sub_info_map.update(self.state.config.sub_links)
|
||||
self.state.subscription_info = sub_info_map
|
||||
|
||||
for url, conf in self.state.clash_configs.items():
|
||||
self.services.add_proxies_to_manager(conf.get('proxies', []),
|
||||
f"Sub:{UtilsProvider.get_url_domain(url)}-{abs(hash(url))}")
|
||||
self.services.add_proxies_to_manager(self.state.clash_template_dict.get('proxies', []), 'Template')
|
||||
# sub_configs loaded from DB. Filter by current sub_links.
|
||||
sub_configs_map = self.state.sub_configs
|
||||
sub_configs_map = {url: sub_configs_map[url] for url in self.state.config.sub_links if sub_configs_map.get(url)}
|
||||
self.state.sub_configs = sub_configs_map
|
||||
|
||||
self.services.check_proxies_lifetime()
|
||||
self.services.check_patch_lifetime()
|
||||
self._start_scheduler()
|
||||
|
||||
def _start_scheduler(self):
|
||||
self.scheduler.start()
|
||||
now = datetime.now(tz=pytz.timezone(settings.TZ))
|
||||
self.scheduler.add_job(self.services.async_refresh_subscriptions, "date",
|
||||
run_date=now + timedelta(seconds=2), misfire_grace_time=self.MISFIRE_GRACE_TIME)
|
||||
if self.config.hint_geo_dat:
|
||||
run_date=now + timedelta(seconds=2), misfire_grace_time=Constant.MISFIRE_GRACE_TIME)
|
||||
if self.state.config.hint_geo_dat:
|
||||
self.scheduler.add_job(self.services.async_refresh_geo_dat, "date",
|
||||
run_date=now + timedelta(seconds=3), misfire_grace_time=self.MISFIRE_GRACE_TIME)
|
||||
else:
|
||||
self.state.geo_rules = {'geoip': [], 'geosite': []}
|
||||
if self.config.enable_acl4ssr:
|
||||
run_date=now + timedelta(seconds=3), misfire_grace_time=Constant.MISFIRE_GRACE_TIME)
|
||||
|
||||
if self.state.config.enable_acl4ssr:
|
||||
self.scheduler.add_job(self.services.async_refresh_acl4ssr, "date",
|
||||
run_date=now + timedelta(seconds=4), misfire_grace_time=self.MISFIRE_GRACE_TIME)
|
||||
else:
|
||||
self.state.acl4ssr_providers = {}
|
||||
run_date=now + timedelta(seconds=4), misfire_grace_time=Constant.MISFIRE_GRACE_TIME)
|
||||
|
||||
def get_state(self) -> bool:
|
||||
return self.config.enabled
|
||||
return self.state.config.enabled
|
||||
|
||||
@staticmethod
|
||||
def get_command() -> List[Dict[str, Any]]:
|
||||
@@ -165,10 +142,10 @@ class ClashRuleProvider(_ClashRuleProviderBase):
|
||||
{"key": "clash_info", "name": "Clash Info"},
|
||||
{"key": "traffic_stats", "name": "Traffic Stats"}
|
||||
]
|
||||
return [c for c in components if c.get("name") in self.config.dashboard_components]
|
||||
return [c for c in components if c.get("name") in self.state.config.dashboard_components]
|
||||
|
||||
def get_dashboard(self, key: str, **kwargs) -> Optional[Tuple[Dict[str, Any], Dict[str, Any], List[dict]]]:
|
||||
clash_available = bool(self.config.dashboard_url and self.config.dashboard_secret)
|
||||
clash_available = bool(self.state.config.dashboard_url and self.state.config.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)}
|
||||
@@ -176,7 +153,7 @@ class ClashRuleProvider(_ClashRuleProviderBase):
|
||||
'title': components.get(key, {}).get('title', 'Clash Info'),
|
||||
'border': True,
|
||||
'clash_available': clash_available,
|
||||
'secret': self.config.dashboard_secret,
|
||||
'secret': self.state.config.dashboard_secret,
|
||||
}
|
||||
return col_config, global_config, []
|
||||
|
||||
@@ -193,18 +170,18 @@ class ClashRuleProvider(_ClashRuleProviderBase):
|
||||
logger.error(f"退出插件失败:{e}")
|
||||
|
||||
def get_service(self) -> List[Dict[str, Any]]:
|
||||
if self.get_state() and self.config.auto_update_subscriptions and self.config.sub_links:
|
||||
if self.get_state() and self.state.config.auto_update_subscriptions and self.state.config.sub_links:
|
||||
return [{
|
||||
"id": "ClashRuleProvider",
|
||||
"name": "定时更新订阅",
|
||||
"trigger": CronTrigger.from_crontab(self.config.cron_string),
|
||||
"trigger": CronTrigger.from_crontab(self.state.config.cron_string),
|
||||
"func": self.refresh_subscription_service,
|
||||
"kwargs": {}
|
||||
}]
|
||||
return []
|
||||
|
||||
async def refresh_subscription_service(self):
|
||||
if not self.config.sub_links:
|
||||
if not self.state.config.sub_links:
|
||||
return
|
||||
res = await self.services.async_refresh_subscriptions()
|
||||
messages = []
|
||||
@@ -214,37 +191,36 @@ class ClashRuleProvider(_ClashRuleProviderBase):
|
||||
message = f"{index}. 「 {host_name} 」\n"
|
||||
index += 1
|
||||
if result:
|
||||
sub_info = self.state.subscription_info.get(url, {})
|
||||
if sub_info.get('total') is not None:
|
||||
used = sub_info.get('download', 0) + sub_info.get('upload', 0)
|
||||
remaining = sub_info.get('total', 0) - used
|
||||
sub_info = self.state.subscription_info.get(url)
|
||||
if sub_info.total:
|
||||
used = sub_info.download + sub_info.upload
|
||||
remaining = sub_info.total- used
|
||||
info = (
|
||||
f"节点数量: {sub_info.get('proxy_num', 0)}\n"
|
||||
f"节点数量: {sub_info.proxy_num}\n"
|
||||
f"已用流量: {UtilsProvider.format_bytes(used)}\n"
|
||||
f"剩余流量: {UtilsProvider.format_bytes(remaining)}\n"
|
||||
f"总量: {UtilsProvider.format_bytes(sub_info.get('total', 0))}\n"
|
||||
f"过期时间: {UtilsProvider.format_expire_time(sub_info.get('expire', 0))}"
|
||||
f"总量: {UtilsProvider.format_bytes(sub_info.total)}\n"
|
||||
f"过期时间: {UtilsProvider.format_expire_time(sub_info.expire)}"
|
||||
)
|
||||
else:
|
||||
info = f"节点数量: {sub_info.get('proxy_num', 0)}\n"
|
||||
info = f"节点数量: {sub_info.proxy_num}\n"
|
||||
message += f"订阅更新成功\n{info}"
|
||||
else:
|
||||
message += '订阅更新失败'
|
||||
messages.append(message)
|
||||
if self.config.notify:
|
||||
self.post_message(title=f"【{self.plugin_name}】",
|
||||
mtype=NotificationType.Plugin,
|
||||
text='\n'.join(messages)
|
||||
)
|
||||
if self.state.config.notify:
|
||||
self.post_message(
|
||||
title=f"【{self.plugin_name}】", mtype=NotificationType.Plugin, text='\n'.join(messages)
|
||||
)
|
||||
|
||||
def _update_config(self):
|
||||
conf = self.config.model_dump(by_alias=True)
|
||||
conf = self.state.config.model_dump(by_alias=True)
|
||||
self.update_config(conf)
|
||||
|
||||
def update_best_cf_ip(self, ips: List[str]):
|
||||
self.config.best_cf_ip = [*ips]
|
||||
self.state.config.best_cf_ip = [*ips]
|
||||
conf = self.get_config()
|
||||
conf['best_cf_ip'] = self.config.best_cf_ip
|
||||
conf['best_cf_ip'] = self.state.config.best_cf_ip
|
||||
self.update_config(conf)
|
||||
|
||||
@eventmanager.register(EventType.PluginAction)
|
||||
@@ -258,3 +234,13 @@ class ClashRuleProvider(_ClashRuleProviderBase):
|
||||
if isinstance(ips, list):
|
||||
logger.info("更新 Cloudflare 优选 IP ...")
|
||||
self.update_best_cf_ip(ips)
|
||||
|
||||
@eventmanager.register(EventType.PluginReload)
|
||||
def reload(self, event):
|
||||
"""
|
||||
响应插件重载事件
|
||||
"""
|
||||
plugin_id = event.event_data.get("plugin_id")
|
||||
if plugin_id == self.__class__.__name__:
|
||||
logger.info("正在注册 API ...")
|
||||
register_plugin_api(plugin_id=plugin_id)
|
||||
|
||||
@@ -5,7 +5,7 @@ from typing import Any, Dict, List, Callable, Optional, Literal
|
||||
|
||||
import websockets
|
||||
import yaml
|
||||
from fastapi import HTTPException, Request, status, Response
|
||||
from fastapi import HTTPException, Request, status, Response, Body
|
||||
from fastapi.responses import PlainTextResponse
|
||||
from sse_starlette.sse import EventSourceResponse
|
||||
|
||||
@@ -14,8 +14,10 @@ from app.core.config import settings
|
||||
from app.log import logger
|
||||
|
||||
from .config import PluginConfig
|
||||
from .models import ProxyGroup
|
||||
from .models.api import RuleData, Connectivity, Subscription, RuleProviderData, SubscriptionInfo, HostData
|
||||
from .models import ProxyGroup, Proxy, HostData, RuleData, RuleProvider, RuleProviderData
|
||||
from .models.api import Connectivity, SubscriptionSetting, ConfigRequest
|
||||
from .models.metadata import Metadata
|
||||
from .models.types import RuleSet, DataSource
|
||||
from .services import ClashRuleProviderService
|
||||
|
||||
|
||||
@@ -27,14 +29,16 @@ class ApiCollection:
|
||||
methods: List[Literal['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD', 'TRACE']],
|
||||
allow_anonymous: Optional[bool] = None,
|
||||
auth: Optional[str] = None,
|
||||
summary: Optional[str] = ''):
|
||||
summary: Optional[str] = '',
|
||||
**kwargs):
|
||||
|
||||
def decorator(func: Callable):
|
||||
route_meta: Dict[str, Any] = {
|
||||
'path': path,
|
||||
'methods': methods,
|
||||
'summary': summary,
|
||||
'endpoint': func
|
||||
'endpoint': func,
|
||||
**kwargs
|
||||
}
|
||||
if allow_anonymous is not None:
|
||||
route_meta['allow_anonymous'] = allow_anonymous
|
||||
@@ -63,7 +67,7 @@ class ClashRuleProviderApi:
|
||||
self.services: ClashRuleProviderService = services
|
||||
self.config = config
|
||||
|
||||
@apis.register(path='/connectivity', methods=['POST'], auth='bear', summary='测试连接')
|
||||
@apis.register(path="/connectivity", methods=["POST"], auth="bear", summary="测试连接")
|
||||
async def test_connectivity(self, item: Connectivity) -> schemas.Response:
|
||||
success, message = await self.services.test_connectivity(item.clash_apis, item.sub_links)
|
||||
return schemas.Response(success=success, message=message)
|
||||
@@ -71,7 +75,7 @@ class ClashRuleProviderApi:
|
||||
@apis.register(path="/clash-outbound", methods=["GET"], auth="bear", summary="获取所有出站")
|
||||
def get_clash_outbound(self) -> schemas.Response:
|
||||
outbound = self.services.clash_outbound()
|
||||
return schemas.Response(success=True, data={"outbound": outbound})
|
||||
return schemas.Response(success=True, data=outbound)
|
||||
|
||||
@apis.register(path="/status", methods=["GET"], auth="bear", summary="插件状态")
|
||||
def get_status(self) -> schemas.Response:
|
||||
@@ -79,56 +83,80 @@ class ClashRuleProviderApi:
|
||||
return schemas.Response(success=True, data=data)
|
||||
|
||||
@apis.register(path="/rules/{ruleset}", methods=["GET"], auth="bear", summary="获取指定集合中的规则")
|
||||
def get_rules(self, ruleset: Literal['ruleset', 'top']) -> schemas.Response:
|
||||
def get_rules(self, ruleset: RuleSet) -> schemas.Response:
|
||||
data = self.services.get_rules(ruleset)
|
||||
return schemas.Response(success=True, data={'rules': data})
|
||||
return schemas.Response(success=True, data=data)
|
||||
|
||||
@apis.register(path="/reorder-rules/{ruleset}/{target_priority}", methods=["PUT"], auth="bear",
|
||||
summary="重新排序规则")
|
||||
def reorder_rules(self, ruleset: Literal['ruleset', 'top'], target_priority: int,
|
||||
rule_data: RuleData) -> schemas.Response:
|
||||
moved_priority = rule_data.priority
|
||||
success, message = self.services.reorder_rules(ruleset, moved_priority, target_priority)
|
||||
@apis.register(path="/reorder-rules/{ruleset}/{target}", methods=["PUT"], auth="bear", summary="重新排序规则")
|
||||
def reorder_rules(self, ruleset: RuleSet, target: int,
|
||||
moved_priority: int = Body(..., embed=True)) -> schemas.Response:
|
||||
success, message = self.services.reorder_rules(ruleset, moved_priority, target)
|
||||
return schemas.Response(success=success, message=message)
|
||||
|
||||
@apis.register(path="/rules/{ruleset}/{priority}", methods=["PATCH"], auth="bear", summary="更新规则")
|
||||
def update_rule(self, ruleset: Literal['ruleset', 'top'], priority: int, rule_data: RuleData) -> schemas.Response:
|
||||
def update_rule(self, ruleset: RuleSet, priority: int, rule_data: RuleData) -> schemas.Response:
|
||||
success, message = self.services.update_rule(ruleset, priority, rule_data)
|
||||
return schemas.Response(success=success, message=message)
|
||||
|
||||
@apis.register(path="/rules/{ruleset}", methods=["POST"], auth="bear", summary="添加规则")
|
||||
def add_rule(self, ruleset: Literal['ruleset', 'top'], rule_data: RuleData) -> schemas.Response:
|
||||
def add_rule(self, ruleset: RuleSet, rule_data: RuleData = Body(...)) -> schemas.Response:
|
||||
success, message = self.services.add_rule(ruleset, rule_data)
|
||||
return schemas.Response(success=success, message=message)
|
||||
|
||||
@apis.register(path="/rules/{ruleset}/{priority}/meta", methods=["PATCH"], auth="bear", summary="更新规则元数据")
|
||||
def update_rule_meta(self, ruleset: RuleSet, priority: int, meta: Metadata = Body(...)) -> schemas.Response:
|
||||
success, message = self.services.update_rule_meta(ruleset, priority, meta)
|
||||
return schemas.Response(success=success, message=message)
|
||||
|
||||
@apis.register(path="/rules/{ruleset}/metadata/disabled", methods=["POST"], auth="bear", summary="设置规则状态")
|
||||
def set_rules_status(self, ruleset: RuleSet, priorities: dict[int, bool] = Body(...)):
|
||||
self.services.set_rules_status(ruleset, priorities)
|
||||
|
||||
@apis.register(path="/rules/{ruleset}/{priority}", methods=["DELETE"], auth="bear", summary="删除规则")
|
||||
def delete_rule(self, ruleset: Literal['ruleset', 'top'], priority: int) -> schemas.Response:
|
||||
def delete_rule(self, ruleset: RuleSet, priority: int) -> schemas.Response:
|
||||
self.services.delete_rule(ruleset, priority)
|
||||
return schemas.Response(success=True)
|
||||
|
||||
@apis.register(path="/rules/{ruleset}", methods=["DELETE"], auth="bear", summary="批量删除规则")
|
||||
def delete_rules(self, ruleset: RuleSet, priority: list[int] = Body(...)) -> schemas.Response:
|
||||
self.services.delete_rules(ruleset, priority)
|
||||
return schemas.Response(success=True)
|
||||
|
||||
@apis.register(path="/refresh", methods=["PUT"], auth="bear", summary="更新订阅")
|
||||
async def refresh_subscription(self, subscription: Subscription) -> schemas.Response:
|
||||
success, message = await self.services.refresh_subscription(subscription.url)
|
||||
async def refresh_subscription(self, url: str = Body(..., embed=True)) -> schemas.Response:
|
||||
success, message = await self.services.refresh_subscription(url)
|
||||
return schemas.Response(success=success, message=message)
|
||||
|
||||
@apis.register(path="/rule-providers", methods=["GET"], auth="bear", summary="获取规则集合")
|
||||
@apis.register(path="/rule-providers", methods=["GET"], auth="bear", summary="获取规则集合",
|
||||
response_model=schemas.Response, response_model_exclude_none=True)
|
||||
def get_rule_providers(self) -> schemas.Response:
|
||||
return schemas.Response(success=True, data=self.services.rule_providers())
|
||||
return schemas.Response(success=True, data=self.services.state.all_rule_providers)
|
||||
|
||||
@apis.register(path="/rule-providers/{name}", methods=["POST"], auth="bear", summary="更新规则集合")
|
||||
@apis.register(path="/rule-providers/{name}", methods=["POST"], auth="bear", summary="添加代理组")
|
||||
def add_rule_provider(self, name: str, item: RuleProvider):
|
||||
success, message = self.services.add_rule_provider(name, item)
|
||||
return schemas.Response(success=success, message=message)
|
||||
|
||||
@apis.register(path="/rule-providers/{name}", methods=["PATCH"], auth="bear", summary="更新规则集合")
|
||||
def update_rule_provider(self, name: str, item: RuleProviderData):
|
||||
success, message = self.services.update_rule_provider(name, item)
|
||||
return schemas.Response(success=success, message=message)
|
||||
|
||||
@apis.register(path="/rule-providers/{name}/meta", methods=["PATCH"], auth="bear", summary="更新规则集元数据")
|
||||
def update_rule_providers_meta(self, name: str, meta: Metadata):
|
||||
success, message = self.services.update_rule_providers_meta(name, meta)
|
||||
return schemas.Response(success=success, message=message)
|
||||
|
||||
@apis.register(path="/rule-providers/{name}", methods=["DELETE"], auth="bear", summary="删除规则集合")
|
||||
def delete_rule_provider(self, name: str):
|
||||
self.services.delete_rule_provider(name)
|
||||
return schemas.Response(success=True)
|
||||
|
||||
@apis.register(path="/proxies", methods=["GET"], auth="bear", summary="获取出站代理")
|
||||
@apis.register(path="/proxies", methods=["GET"], auth="bear", summary="获取代理",
|
||||
response_model=schemas.Response, response_model_exclude_none=True)
|
||||
def get_proxies(self):
|
||||
proxies = self.services.get_all_proxies_with_details()
|
||||
return schemas.Response(success=True, data={'proxies': proxies})
|
||||
proxies = self.services.get_proxies()
|
||||
return schemas.Response(success=True, data=proxies)
|
||||
|
||||
@apis.register(path="/proxies/{name}", methods=["DELETE"], auth="bear", summary="删除出站代理")
|
||||
def delete_proxy(self, name: str):
|
||||
@@ -136,39 +164,61 @@ class ClashRuleProviderApi:
|
||||
return schemas.Response(success=True)
|
||||
|
||||
@apis.register(path="/proxies", methods=["PUT"], auth="bear", summary="添加出站代理")
|
||||
def import_proxies(self, params: Dict[str, Any]):
|
||||
success, message = self.services.import_proxies(params)
|
||||
def import_proxies(self, vehicle: Literal["YAML", "LINK"] = Body(...), payload: str = Body(...)):
|
||||
success, message = self.services.import_proxies(vehicle, payload)
|
||||
return schemas.Response(success=success, message=message)
|
||||
|
||||
@apis.register(path="/proxies/{name}", methods=["PATCH"], auth="bear", summary="更新出站代理")
|
||||
def update_proxy(self, name: str, param: Dict[str, Any]) -> schemas.Response:
|
||||
success, message = self.services.update_proxy(name, param)
|
||||
def update_proxy(self, name: str, source: DataSource = Body(...), proxy: Proxy = Body(...)) -> schemas.Response:
|
||||
success, message = self.services.update_proxy(name, source, proxy)
|
||||
return schemas.Response(success=success, message=message)
|
||||
|
||||
@apis.register(path="/proxy-groups", methods=["GET"], auth="bear", summary="获取代理组")
|
||||
@apis.register(path="/proxies/{name}/meta", methods=["PATCH"], auth="bear", summary="更新代理组元数据")
|
||||
def update_proxy_meta(self, name: str, meta: Metadata):
|
||||
success, message = self.services.update_proxy_meta(name, meta)
|
||||
return schemas.Response(success=success, message=message)
|
||||
|
||||
@apis.register(path="/proxies/{name}/patch", methods=["DELETE"], auth="bear", summary="删除代理补丁")
|
||||
def delete_proxy_patch(self, name: str):
|
||||
success, message = self.services.delete_proxy_patch(name)
|
||||
return schemas.Response(success=success, message=message)
|
||||
|
||||
@apis.register(path="/proxy-groups", methods=["GET"], auth="bear", summary="获取代理组",
|
||||
response_model=schemas.Response, response_model_exclude_none=True)
|
||||
def get_proxy_groups(self):
|
||||
proxy_groups = self.services.get_all_proxy_groups_with_source()
|
||||
return schemas.Response(success=True, data={'proxy_groups': proxy_groups})
|
||||
proxy_groups = self.services.get_proxy_groups()
|
||||
return schemas.Response(success=True, data=proxy_groups)
|
||||
|
||||
@apis.register(path="/proxy-groups/{name}", methods=["DELETE"], auth="bear", summary="删除代理组")
|
||||
def delete_proxy_group(self, name: str):
|
||||
success, message = self.services.delete_proxy_group(name)
|
||||
return schemas.Response(success=success, message=message)
|
||||
|
||||
@apis.register(path="/proxy-groups/{name}/meta", methods=["PATCH"], auth="bear", summary="更新代理组元数据")
|
||||
def update_proxy_group_meta(self, name: str, meta: Metadata):
|
||||
success, message = self.services.update_proxy_group_meta(name, meta)
|
||||
return schemas.Response(success=success, message=message)
|
||||
|
||||
@apis.register(path="/proxy-groups/{name}/patch", methods=["DELETE"], auth="bear", summary="删除代理组补丁")
|
||||
def delete_proxy_group_patch(self, name: str):
|
||||
success, message = self.services.delete_proxy_group_patch(name)
|
||||
return schemas.Response(success=success, message=message)
|
||||
|
||||
@apis.register(path="/proxy-groups", methods=["POST"], auth="bear", summary="添加代理组")
|
||||
def add_proxy_group(self, item: ProxyGroup):
|
||||
success, message = self.services.add_proxy_group(item)
|
||||
return schemas.Response(success=success, message=message)
|
||||
|
||||
@apis.register(path="/proxy-groups/{previous_name}", methods=["PATCH"], auth="bear", summary="更新代理组")
|
||||
def update_proxy_group(self, previous_name: str, item: ProxyGroup):
|
||||
success, message = self.services.update_proxy_group(previous_name, item)
|
||||
@apis.register(path="/proxy-groups/{name}", methods=["PATCH"], auth="bear", summary="更新代理组")
|
||||
def update_proxy_group(self, name: str, source: DataSource = Body(...), proxy_group: ProxyGroup = Body(...)):
|
||||
success, message = self.services.update_proxy_group(name, source, proxy_group)
|
||||
return schemas.Response(success=success, message=message)
|
||||
|
||||
@apis.register(path="/proxy-providers", methods=["GET"], auth="bear", summary="获取代理集合")
|
||||
@apis.register(path="/proxy-providers", methods=["GET"], auth="bear", summary="获取代理集合",
|
||||
response_model=schemas.Response, response_model_exclude_none=True)
|
||||
def get_proxy_providers(self):
|
||||
proxy_providers = self.services.all_proxy_providers()
|
||||
return schemas.Response(success=True, data={'proxy_providers': proxy_providers})
|
||||
proxy_providers = self.services.state.all_proxy_providers
|
||||
return schemas.Response(success=True, data=proxy_providers)
|
||||
|
||||
@apis.register(path="/ruleset", methods=["GET"], allow_anonymous=True, summary="获取规则集规则")
|
||||
def get_ruleset(self, name: str, apikey: str) -> PlainTextResponse:
|
||||
@@ -181,43 +231,49 @@ class ClashRuleProviderApi:
|
||||
return PlainTextResponse(content=res, media_type="application/x-yaml")
|
||||
|
||||
@apis.register(path="/import", methods=["POST"], auth="bear", summary="导入规则")
|
||||
def import_rules(self, params: Dict[str, Any]):
|
||||
self.services.import_rules(params)
|
||||
def import_rules(self, vehicle: Literal["YAML"] = Body(...), payload: str = Body(...)):
|
||||
self.services.import_rules(vehicle, payload)
|
||||
return schemas.Response(success=True)
|
||||
|
||||
@apis.register(path="/hosts", methods=["GET"], auth="bear", summary="获取 Hosts")
|
||||
def get_hosts(self):
|
||||
return schemas.Response(success=True, data={'hosts': self.services.get_hosts()})
|
||||
return schemas.Response(success=True, data=self.services.state.hosts.model_dump(mode='json'))
|
||||
|
||||
@apis.register(path="/hosts", methods=["POST"], auth="bear", summary="更新 Hosts")
|
||||
def update_hosts(self, host: HostData):
|
||||
success, message = self.services.update_hosts(host)
|
||||
def update_hosts(self, domain: str = Body(..., embed=True), host: HostData = Body(...)):
|
||||
success, message = self.services.update_hosts(domain, host)
|
||||
return schemas.Response(success=success, message=message)
|
||||
|
||||
@apis.register(path="/hosts", methods=["DELETE"], auth="bear", summary="删除 Hosts")
|
||||
def delete_host(self, host: HostData):
|
||||
success, message = self.services.delete_host(host)
|
||||
@apis.register(path="/hosts/{domain}", methods=["DELETE"], auth="bear", summary="删除 Hosts")
|
||||
def delete_host(self, domain: str):
|
||||
success, message = self.services.delete_host(domain)
|
||||
return schemas.Response(success=success, message=message)
|
||||
|
||||
@apis.register(path="/subscription-info", methods=["POST"], auth="bear", summary="更新订阅信息")
|
||||
def update_subscription_info(self, sub_info: SubscriptionInfo):
|
||||
def update_subscription_info(self, sub_info: SubscriptionSetting):
|
||||
self.services.update_subscription_info(sub_info)
|
||||
return schemas.Response(success=True)
|
||||
|
||||
@apis.register(path="/config", methods=["GET"], allow_anonymous=bool(True), summary="获取 Clash 配置")
|
||||
def get_clash_config(self, apikey: str, request: Request):
|
||||
def get_clash_config(self, apikey: str, request: Request, identifier: str | None = None):
|
||||
_apikey = self.config.apikey or settings.API_TOKEN
|
||||
param = ConfigRequest(
|
||||
url=str(request.url),
|
||||
client_host=request.client.host,
|
||||
identifier=identifier,
|
||||
user_agent=request.headers.get("user-agent")
|
||||
)
|
||||
if not secrets.compare_digest(apikey, _apikey):
|
||||
raise HTTPException(status_code=403, detail="Invalid API Key")
|
||||
logger.info(f"{request.client.host} 正在获取配置")
|
||||
config = self.services.clash_config()
|
||||
config = self.services.build_clash_config(param=param)
|
||||
if not config:
|
||||
raise HTTPException(status_code=500, detail="配置不可用")
|
||||
|
||||
res = yaml.dump(config, allow_unicode=True, sort_keys=False)
|
||||
config_dict = config.model_dump(mode="json", by_alias=True, exclude_none=True)
|
||||
res = yaml.dump(config_dict, allow_unicode=True, sort_keys=False)
|
||||
sub_info = self.services.get_subscription_user_info()
|
||||
headers = {'Subscription-Userinfo': f'upload={sub_info["upload"]}; download={sub_info["download"]}; '
|
||||
f'total={sub_info["total"]}; expire={sub_info["expire"]}'}
|
||||
headers = {'Subscription-Userinfo': sub_info.header}
|
||||
return Response(headers=headers, content=res, media_type="text/yaml")
|
||||
|
||||
@apis.register(path="/clash/proxy/{path:path}", methods=["GET"], auth="bear", summary="转发 Clash API 请求")
|
||||
|
||||
@@ -1,34 +1,8 @@
|
||||
from abc import ABC
|
||||
from typing import Final, Literal, Dict
|
||||
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
|
||||
from app.plugins import _PluginBase
|
||||
|
||||
from .config import PluginConfig
|
||||
from .state import PluginState
|
||||
from .store import PluginStore
|
||||
from typing import Final
|
||||
|
||||
|
||||
class _ClashRuleProviderBase(_PluginBase, ABC):
|
||||
# Constants
|
||||
DEFAULT_CLASH_CONF: Final[
|
||||
Dict[Literal['rules', 'rule-providers', 'proxies', 'proxy-groups', 'proxy-providers'], dict | list]] = {
|
||||
'rules': [], 'rule-providers': {},
|
||||
'proxies': [], 'proxy-groups': [], 'proxy-providers': {}
|
||||
}
|
||||
OVERWRITTEN_PROXIES_LIFETIME: Final[int] = 10
|
||||
class Constant:
|
||||
PATCH_LIFESPAN: Final[int] = 10
|
||||
ACL4SSR_API: Final[str] = "https://api.github.com/repos/ACL4SSR/ACL4SSR"
|
||||
METACUBEX_RULE_DAT_API: Final[str] = "https://api.github.com/repos/MetaCubeX/meta-rules-dat"
|
||||
MISFIRE_GRACE_TIME: Final[int] = 120
|
||||
KEY_TOP_RULES: Final[str] = "top_rules"
|
||||
KEY_RULESET_RULES: Final[str] = "ruleset_rules"
|
||||
KEY_PROXIES: Final[str] = "proxies"
|
||||
KEY_PROXY_GROUPS: Final[str] = "proxy-groups"
|
||||
KEY_NAME: Final[str] = "name"
|
||||
|
||||
# Runtime variables
|
||||
state: PluginState
|
||||
config: PluginConfig
|
||||
store: PluginStore
|
||||
scheduler: AsyncIOScheduler = None
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import List, Optional
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
|
||||
from .models.api import ClashApi
|
||||
|
||||
@@ -8,10 +8,10 @@ from .models.api import ClashApi
|
||||
class SubscriptionConfig(BaseModel):
|
||||
url: str
|
||||
rules: Optional[bool] = True
|
||||
rule_providers: Optional[bool] = Field(True, alias='rule-providers')
|
||||
rule_providers: Optional[bool] = Field(default=True, alias='rule-providers')
|
||||
proxies: Optional[bool] = True
|
||||
proxy_groups: Optional[bool] = Field(True, alias='proxy-groups')
|
||||
proxy_providers: Optional[bool] = Field(True, alias='proxy-providers')
|
||||
proxy_groups: Optional[bool] = Field(default=True, alias='proxy-groups')
|
||||
proxy_providers: Optional[bool] = Field(default=True, alias='proxy-providers')
|
||||
|
||||
@field_validator('url')
|
||||
@classmethod
|
||||
@@ -23,10 +23,14 @@ class PluginConfig(BaseModel):
|
||||
"""
|
||||
A dataclass to hold all the configuration of the ClashRuleProvider plugin.
|
||||
"""
|
||||
model_config = ConfigDict(
|
||||
str_strip_whitespace=True,
|
||||
)
|
||||
|
||||
enabled: bool = False
|
||||
proxy: bool = False
|
||||
notify: bool = False
|
||||
subscriptions_config: List[SubscriptionConfig] = Field(default_factory=list)
|
||||
subscriptions_config: list[SubscriptionConfig] = Field(default_factory=list)
|
||||
movie_pilot_url: str = ''
|
||||
cron_string: str = '30 12 * * *'
|
||||
timeout: int = 10
|
||||
@@ -46,6 +50,8 @@ class PluginConfig(BaseModel):
|
||||
apikey: Optional[str] = None
|
||||
clash_dashboards: List[ClashApi] = Field(default_factory=list)
|
||||
active_dashboard: Optional[int] = None
|
||||
identifiers: list[str] = Field(default_factory=list)
|
||||
cache_ttl: int = 3600
|
||||
|
||||
@field_validator('clash_dashboards')
|
||||
@classmethod
|
||||
@@ -62,32 +68,6 @@ class PluginConfig(BaseModel):
|
||||
def validate_movie_pilot_url(cls, v: str):
|
||||
return v.rstrip('/')
|
||||
|
||||
@field_validator('ruleset_prefix')
|
||||
@classmethod
|
||||
def validate_ruleset_prefix(cls, v: str):
|
||||
return v.strip()
|
||||
|
||||
@field_validator('acl4ssr_prefix')
|
||||
@classmethod
|
||||
def validate_acl4ssr_prefix(cls, v: str):
|
||||
return v.strip()
|
||||
|
||||
@staticmethod
|
||||
def upgrade_conf(conf: Dict[str, Any]) -> Dict[str, Any]:
|
||||
if conf.get('sub_links'):
|
||||
subscriptions_config = conf.get('subscriptions_config') or []
|
||||
subscriptions_config.extend(
|
||||
[{'url': url, 'rules': True, 'rule-providers': True, 'proxies': True, 'proxy-groups': True,
|
||||
'proxy-providers': True}
|
||||
for url in conf['sub_links']]
|
||||
)
|
||||
conf['subscriptions_config'] = subscriptions_config
|
||||
if conf.get('clash_dashboard_url') and conf.get('clash_dashboard_secret'):
|
||||
clash_dashboards = conf.get('clash_dashboards') or []
|
||||
clash_dashboards.append({'url': conf.get('clash_dashboard_url'), 'secret': conf.get('clash_dashboard_secret')})
|
||||
conf['clash_dashboards'] = clash_dashboards
|
||||
return conf
|
||||
|
||||
@property
|
||||
def sub_links(self) -> List[str]:
|
||||
return [sub.url for sub in self.subscriptions_config]
|
||||
@@ -105,3 +85,6 @@ class PluginConfig(BaseModel):
|
||||
if self.active_dashboard is not None and self.active_dashboard in range(len(self.clash_dashboards)):
|
||||
dashboard_secret = self.clash_dashboards[self.active_dashboard].secret
|
||||
return dashboard_secret
|
||||
|
||||
def get_sub_conf(self, url: str) -> SubscriptionConfig:
|
||||
return next((conf for conf in self.subscriptions_config if conf.url == url), SubscriptionConfig(url=url))
|
||||
|
||||
3
plugins.v2/clashruleprovider/dist/assets/Meta-1zu2nKV2.js
vendored
Normal file
3
plugins.v2/clashruleprovider/dist/assets/Meta-1zu2nKV2.js
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
const MetaLogo = "/assets/Meta-uqWbsmWL.png";
|
||||
|
||||
export { MetaLogo as M };
|
||||
BIN
plugins.v2/clashruleprovider/dist/assets/Meta-uqWbsmWL.png
vendored
Normal file
BIN
plugins.v2/clashruleprovider/dist/assets/Meta-uqWbsmWL.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 79 KiB |
File diff suppressed because it is too large
Load Diff
1479
plugins.v2/clashruleprovider/dist/assets/__federation_expose_Config-CY46uj5g.js
vendored
Normal file
1479
plugins.v2/clashruleprovider/dist/assets/__federation_expose_Config-CY46uj5g.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
4
plugins.v2/clashruleprovider/dist/assets/__federation_expose_Config-CwbjkOP2.css
vendored
Normal file
4
plugins.v2/clashruleprovider/dist/assets/__federation_expose_Config-CwbjkOP2.css
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
|
||||
.plugin-config[data-v-3fef8398] {
|
||||
margin: 0 auto;
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
|
||||
.plugin-config[data-v-5f383f33] {
|
||||
margin: 0 auto;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
|
||||
.dashboard-widget[data-v-de7a088e] {
|
||||
.dashboard-widget[data-v-318a5020] {
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
84
plugins.v2/clashruleprovider/dist/assets/__federation_expose_Page-BLBLx7jX.css
vendored
Normal file
84
plugins.v2/clashruleprovider/dist/assets/__federation_expose_Page-BLBLx7jX.css
vendored
Normal file
@@ -0,0 +1,84 @@
|
||||
|
||||
.rule-card[data-v-da4a3497]:hover {
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1) !important;
|
||||
}
|
||||
|
||||
.proxy-group-card[data-v-ef6241d5]:hover {
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1) !important;
|
||||
}
|
||||
|
||||
.proxy-card[data-v-ca5a79a2]:hover {
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1) !important;
|
||||
}
|
||||
|
||||
.subscription-card[data-v-97c0f367] {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
.subscription-card[data-v-97c0f367]:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 4px 25px 0 rgba(0, 0, 0, 0.1);
|
||||
border-color: rgb(var(--v-theme-primary));
|
||||
}
|
||||
.card-header[data-v-97c0f367] {
|
||||
background: rgba(var(--v-theme-surface-variant), 0.05);
|
||||
}
|
||||
.bg-surface-variant-lighten[data-v-97c0f367] {
|
||||
background: rgba(var(--v-theme-surface-variant), 0.02);
|
||||
}
|
||||
.stats-grid[data-v-97c0f367] {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.bounce[data-v-6a1d5a83] {
|
||||
animation: bounce-6a1d5a83 2s infinite;
|
||||
}
|
||||
@keyframes bounce-6a1d5a83 {
|
||||
0%,
|
||||
20%,
|
||||
50%,
|
||||
80%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
40% {
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
60% {
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
}
|
||||
|
||||
.rule-provider-card[data-v-24eb2895]:hover {
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1) !important;
|
||||
}
|
||||
|
||||
.host-card[data-v-a5d6e0e6]:hover {
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1) !important;
|
||||
}
|
||||
.search-field {
|
||||
max-width: 25rem;
|
||||
}
|
||||
|
||||
.clash-data-table {
|
||||
max-height: 40rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.dragging-item {
|
||||
opacity: 0.5;
|
||||
background-color: rgb(var(--v-theme-grey-200));
|
||||
}
|
||||
|
||||
.drop-over {
|
||||
background-color: rgba(var(--v-theme-primary), 0.1) !important;
|
||||
}
|
||||
|
||||
.plugin-page[data-v-ab912b83] {
|
||||
margin: 0 auto;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,70 +0,0 @@
|
||||
|
||||
|
||||
.plugin-page[data-v-67d1defe] {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* 使卡片等宽并适应移动端 */
|
||||
.d-flex.flex-wrap[data-v-67d1defe] {
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
/* 移动端堆叠布局 */
|
||||
@media (max-width: 768px) {
|
||||
.d-flex.flex-wrap[data-v-67d1defe] {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
.drag-handle[data-v-67d1defe] {
|
||||
cursor: move;
|
||||
}
|
||||
.toggle-container[data-v-67d1defe] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.625rem;
|
||||
margin-left: 0.75rem;
|
||||
margin-right: 0.75rem;
|
||||
}
|
||||
.subscription-card[data-v-67d1defe] {
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
transition: transform 0.3s, box-shadow 0.3s;
|
||||
background: white;
|
||||
}
|
||||
.subscription-card[data-v-67d1defe]:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
.card-title[data-v-67d1defe] {
|
||||
color: whitesmoke;
|
||||
}
|
||||
.card-header[data-v-67d1defe] {
|
||||
padding: 0.625rem;
|
||||
background: linear-gradient(135deg, rgba(var(--v-theme-primary), 1) 0%, rgba(var(--v-theme-primary), 0.7) 100%);
|
||||
color: white;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.card-refresh-button[data-v-67d1defe] {
|
||||
background-color: rgba(var(--v-theme-primary), 0.9);
|
||||
color: whitesmoke;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.625rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
.search-field[data-v-67d1defe] {
|
||||
max-width: 25rem;
|
||||
}
|
||||
.clash-data-table[data-v-67d1defe] {
|
||||
max-height: 40rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
14224
plugins.v2/clashruleprovider/dist/assets/__federation_expose_Page-KIk8A7rq.js
vendored
Normal file
14224
plugins.v2/clashruleprovider/dist/assets/__federation_expose_Page-KIk8A7rq.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
208
plugins.v2/clashruleprovider/dist/assets/_plugin-vue_export-helper-D32QZFxh.js
vendored
Normal file
208
plugins.v2/clashruleprovider/dist/assets/_plugin-vue_export-helper-D32QZFxh.js
vendored
Normal file
@@ -0,0 +1,208 @@
|
||||
const isValidUrl = (urlString) => {
|
||||
if (!urlString) return false;
|
||||
try {
|
||||
const url = new URL(urlString);
|
||||
return url.protocol === "http:" || url.protocol === "https:";
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
function isValidIP(ip) {
|
||||
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}$/;
|
||||
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) {
|
||||
if (ips.length === 0) {
|
||||
return `至少需要一个 IP 地址`;
|
||||
}
|
||||
for (const ip of ips) {
|
||||
if (!isValidIP(ip)) {
|
||||
return `无效的 IP 地址: ${ip}`;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
function getUsageColor(percentage) {
|
||||
return percentage > 90 ? "error" : percentage > 70 ? "warning" : "success";
|
||||
}
|
||||
function getBehaviorColor(action) {
|
||||
const colors = {
|
||||
classical: "success",
|
||||
domain: "error",
|
||||
ipcidr: "error"
|
||||
};
|
||||
return colors[action] || "primary";
|
||||
}
|
||||
function getFormatColor(action) {
|
||||
const colors = {
|
||||
yaml: "success",
|
||||
text: "warning",
|
||||
mrs: "info"
|
||||
};
|
||||
return colors[action] || "secondary";
|
||||
}
|
||||
function getRuleTypeColor(type) {
|
||||
const colors = {
|
||||
DOMAIN: "primary",
|
||||
"DOMAIN-SUFFIX": "primary",
|
||||
"DOMAIN-KEYWORD": "primary",
|
||||
"DOMAIN-REGEX": "primary",
|
||||
"DOMAIN-WILDCARD": "primary",
|
||||
GEOSITE: "info",
|
||||
GEOIP: "info",
|
||||
"IP-CIDR": "warning",
|
||||
"IP-CIDR6": "warning",
|
||||
"IP-SUFFIX": "warning",
|
||||
"IP-ASN": "warning",
|
||||
"SRC-GEOIP": "info",
|
||||
"SRC-IP-ASN": "warning",
|
||||
"SRC-IP-CIDR": "warning",
|
||||
"SRC-IP-SUFFIX": "warning",
|
||||
"DST-PORT": "success",
|
||||
"SRC-PORT": "success",
|
||||
"IN-PORT": "success",
|
||||
"IN-TYPE": "success",
|
||||
"IN-USER": "success",
|
||||
"IN-NAME": "success",
|
||||
"PROCESS-PATH": "error",
|
||||
"PROCESS-PATH-REGEX": "error",
|
||||
"PROCESS-NAME": "error",
|
||||
"PROCESS-NAME-REGEX": "error",
|
||||
UID: "secondary",
|
||||
NETWORK: "secondary",
|
||||
DSCP: "secondary",
|
||||
"RULE-SET": "deep-purple",
|
||||
AND: "deep-orange",
|
||||
OR: "deep-orange",
|
||||
NOT: "deep-orange",
|
||||
"SUB-RULE": "deep-orange",
|
||||
MATCH: "teal"
|
||||
};
|
||||
return colors[type] || "grey";
|
||||
}
|
||||
function getSourceColor(source) {
|
||||
const colors = {
|
||||
Auto: "success",
|
||||
Manual: "info"
|
||||
};
|
||||
return colors[source] || "primary";
|
||||
}
|
||||
function getActionColor(action) {
|
||||
const colors = {
|
||||
DIRECT: "success",
|
||||
REJECT: "error",
|
||||
"REJECT-DROP": "error",
|
||||
PASS: "warning",
|
||||
COMPATIBLE: "info"
|
||||
};
|
||||
return colors[action] || "primary";
|
||||
}
|
||||
function getProxyGroupTypeColor(action) {
|
||||
const colors = {
|
||||
"url-test": "success",
|
||||
fallback: "error",
|
||||
"load-balance": "primary",
|
||||
select: "info"
|
||||
};
|
||||
return colors[action] || "warning";
|
||||
}
|
||||
function getProxyColor(action) {
|
||||
const colors = {
|
||||
ss: "success",
|
||||
ssr: "success",
|
||||
trojan: "error",
|
||||
vmess: "primary",
|
||||
vless: "primary",
|
||||
hysteria: "info",
|
||||
hysteria2: "info",
|
||||
anytls: "warning"
|
||||
};
|
||||
return colors[action] || "secondary";
|
||||
}
|
||||
function getBoolColor(value) {
|
||||
if (value) {
|
||||
return "primary";
|
||||
}
|
||||
return "success";
|
||||
}
|
||||
function isSystemRule(rule) {
|
||||
return rule.meta.source?.startsWith("Auto");
|
||||
}
|
||||
function isManual(source) {
|
||||
return source === "Manual";
|
||||
}
|
||||
function isInvalid(source) {
|
||||
return source === "Invalid";
|
||||
}
|
||||
function isRegion(source) {
|
||||
return source === "Auto";
|
||||
}
|
||||
function pageTitle(itemPerPageValue) {
|
||||
if (itemPerPageValue < 0) {
|
||||
return "♾️";
|
||||
}
|
||||
return `${itemPerPageValue}`;
|
||||
}
|
||||
function formatBytes(bytes) {
|
||||
if (bytes === 0) return "0 B";
|
||||
const k = 1024;
|
||||
const sizes = ["B", "KB", "MB", "GB", "TB"];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
||||
}
|
||||
function formatTimestamp(timestamp) {
|
||||
if (!timestamp) return "N/A";
|
||||
const date = new Date(timestamp * 1e3);
|
||||
return date.toLocaleDateString("zh-CN");
|
||||
}
|
||||
function timestampToDate(timestamp) {
|
||||
if (!timestamp) return "N/A";
|
||||
const date = new Date(timestamp * 1e3);
|
||||
return date.toLocaleString("zh-CN", {
|
||||
// 'en-GB' 表示使用英国格式(YYYY-MM-DD HH:mm:ss)
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
hour12: false
|
||||
// 24小时制
|
||||
});
|
||||
}
|
||||
function getExpireColor(timestamp) {
|
||||
if (!timestamp) return "grey";
|
||||
const secondsLeft = timestamp - Math.floor(Date.now() / 1e3);
|
||||
const daysLeft = secondsLeft / 86400;
|
||||
return daysLeft < 7 ? "error" : daysLeft < 30 ? "warning" : "success";
|
||||
}
|
||||
function extractDomain(url) {
|
||||
try {
|
||||
const hostname = new URL(url).hostname;
|
||||
const parts = hostname.split(".");
|
||||
if (/^\d+\.\d+\.\d+\.\d+$/.test(hostname) || hostname.includes(":")) {
|
||||
return hostname;
|
||||
}
|
||||
if (parts.length <= 2) {
|
||||
return hostname;
|
||||
}
|
||||
return parts.slice(-2).join(".");
|
||||
} catch {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
function getUsedPercentageFloor(data) {
|
||||
const used = data.upload + data.download;
|
||||
return data.total > 0 ? Math.floor(used / data.total * 100) : 0;
|
||||
}
|
||||
|
||||
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 _, getActionColor as a, isManual as b, isRegion as c, getSourceColor as d, getProxyGroupTypeColor as e, isValidUrl as f, getRuleTypeColor as g, isInvalid as h, isSystemRule as i, getProxyColor as j, extractDomain as k, formatTimestamp as l, getExpireColor as m, formatBytes as n, getUsageColor as o, pageTitle as p, getUsedPercentageFloor as q, getFormatColor as r, getBehaviorColor as s, timestampToDate as t, getBoolColor as u, validateIPs as v };
|
||||
@@ -1,9 +0,0 @@
|
||||
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 _ };
|
||||
@@ -2,14 +2,14 @@ const currentImports = {};
|
||||
const exportSet = new Set(['Module', '__esModule', 'default', '_export_sfc']);
|
||||
let moduleMap = {
|
||||
"./Page":()=>{
|
||||
dynamicLoadingCss(["__federation_expose_Page-Dx-0nC8K.css"], false, './Page');
|
||||
return __federation_import('./__federation_expose_Page-CUYOswsP.js').then(module =>Object.keys(module).every(item => exportSet.has(item)) ? () => module.default : () => module)},
|
||||
dynamicLoadingCss(["__federation_expose_Page-BLBLx7jX.css"], false, './Page');
|
||||
return __federation_import('./__federation_expose_Page-KIk8A7rq.js').then(module =>Object.keys(module).every(item => exportSet.has(item)) ? () => module.default : () => module)},
|
||||
"./Config":()=>{
|
||||
dynamicLoadingCss(["__federation_expose_Config-D7x82s8Y.css"], false, './Config');
|
||||
return __federation_import('./__federation_expose_Config-C8YPPEsk.js').then(module =>Object.keys(module).every(item => exportSet.has(item)) ? () => module.default : () => module)},
|
||||
dynamicLoadingCss(["__federation_expose_Config-CwbjkOP2.css"], false, './Config');
|
||||
return __federation_import('./__federation_expose_Config-CY46uj5g.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)},};
|
||||
dynamicLoadingCss(["__federation_expose_Dashboard-CFBdUa27.css"], false, './Dashboard');
|
||||
return __federation_import('./__federation_expose_Dashboard-CybypqLB.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;
|
||||
|
||||
@@ -713,6 +713,37 @@ var ace$2 = {exports: {}};
|
||||
exports.importCssStylsheet = function (uri, doc) {
|
||||
exports.buildDom(["link", { rel: "stylesheet", href: uri }], exports.getDocumentHead(doc));
|
||||
};
|
||||
exports.$fixPositionBug = function (el) {
|
||||
var rect = el.getBoundingClientRect();
|
||||
if (el.style.left) {
|
||||
var target = parseFloat(el.style.left);
|
||||
var result = +rect.left;
|
||||
if (Math.abs(target - result) > 1) {
|
||||
el.style.left = 2 * target - result + "px";
|
||||
}
|
||||
}
|
||||
if (el.style.right) {
|
||||
var target = parseFloat(el.style.right);
|
||||
var result = window.innerWidth - rect.right;
|
||||
if (Math.abs(target - result) > 1) {
|
||||
el.style.right = 2 * target - result + "px";
|
||||
}
|
||||
}
|
||||
if (el.style.top) {
|
||||
var target = parseFloat(el.style.top);
|
||||
var result = +rect.top;
|
||||
if (Math.abs(target - result) > 1) {
|
||||
el.style.top = 2 * target - result + "px";
|
||||
}
|
||||
}
|
||||
if (el.style.bottom) {
|
||||
var target = parseFloat(el.style.bottom);
|
||||
var result = window.innerHeight - rect.bottom;
|
||||
if (Math.abs(target - result) > 1) {
|
||||
el.style.bottom = 2 * target - result + "px";
|
||||
}
|
||||
}
|
||||
};
|
||||
exports.scrollbarWidth = function (doc) {
|
||||
var inner = exports.createElement("ace_inner");
|
||||
inner.style.width = "100%";
|
||||
@@ -1319,7 +1350,7 @@ var ace$2 = {exports: {}};
|
||||
reportErrorIfPathIsNotConfigured = function () { };
|
||||
}
|
||||
};
|
||||
exports.version = "1.43.2";
|
||||
exports.version = "1.43.5";
|
||||
|
||||
});
|
||||
|
||||
@@ -2072,6 +2103,7 @@ var ace$2 = {exports: {}};
|
||||
this.text = dom.createElement("textarea");
|
||||
this.text.className = "ace_text-input";
|
||||
this.text.setAttribute("wrap", "off");
|
||||
this.text.setAttribute("autocomplete", "off");
|
||||
this.text.setAttribute("autocorrect", "off");
|
||||
this.text.setAttribute("autocapitalize", "off");
|
||||
this.text.setAttribute("spellcheck", "false");
|
||||
@@ -2858,7 +2890,7 @@ var ace$2 = {exports: {}};
|
||||
anchor = this.$clickSelection.start;
|
||||
}
|
||||
else {
|
||||
var orientedRange = calcRangeOrientation(this.$clickSelection, cursor);
|
||||
var orientedRange = calcRangeOrientation(this.$clickSelection, cursor, editor.session);
|
||||
cursor = orientedRange.cursor;
|
||||
anchor = orientedRange.anchor;
|
||||
}
|
||||
@@ -2889,7 +2921,7 @@ var ace$2 = {exports: {}};
|
||||
anchor = range.start;
|
||||
}
|
||||
else {
|
||||
var orientedRange = calcRangeOrientation(this.$clickSelection, cursor);
|
||||
var orientedRange = calcRangeOrientation(this.$clickSelection, cursor, editor.session);
|
||||
cursor = orientedRange.cursor;
|
||||
anchor = orientedRange.anchor;
|
||||
}
|
||||
@@ -3003,11 +3035,11 @@ var ace$2 = {exports: {}};
|
||||
function calcDistance(ax, ay, bx, by) {
|
||||
return Math.sqrt(Math.pow(bx - ax, 2) + Math.pow(by - ay, 2));
|
||||
}
|
||||
function calcRangeOrientation(range, cursor) {
|
||||
function calcRangeOrientation(range, cursor, session) {
|
||||
if (range.start.row == range.end.row)
|
||||
var cmp = 2 * cursor.column - range.start.column - range.end.column;
|
||||
else if (range.start.row == range.end.row - 1 && !range.start.column && !range.end.column)
|
||||
var cmp = cursor.column - 4;
|
||||
var cmp = 3 * cursor.column - 2 * session.getLine(range.start.row).length;
|
||||
else
|
||||
var cmp = 2 * cursor.row - range.start.row - range.end.row;
|
||||
if (cmp < 0)
|
||||
@@ -3018,6 +3050,71 @@ var ace$2 = {exports: {}};
|
||||
|
||||
});
|
||||
|
||||
ace.define("ace/mouse/mouse_event",["require","exports","module","ace/lib/event","ace/lib/useragent"], function(require, exports, module){ var event = require("../lib/event");
|
||||
var useragent = require("../lib/useragent");
|
||||
var MouseEvent = /** @class */ (function () {
|
||||
function MouseEvent(domEvent, editor) { this.speed; this.wheelX; this.wheelY;
|
||||
this.domEvent = domEvent;
|
||||
this.editor = editor;
|
||||
this.x = this.clientX = domEvent.clientX;
|
||||
this.y = this.clientY = domEvent.clientY;
|
||||
this.$pos = null;
|
||||
this.$inSelection = null;
|
||||
this.propagationStopped = false;
|
||||
this.defaultPrevented = false;
|
||||
}
|
||||
MouseEvent.prototype.stopPropagation = function () {
|
||||
event.stopPropagation(this.domEvent);
|
||||
this.propagationStopped = true;
|
||||
};
|
||||
MouseEvent.prototype.preventDefault = function () {
|
||||
event.preventDefault(this.domEvent);
|
||||
this.defaultPrevented = true;
|
||||
};
|
||||
MouseEvent.prototype.stop = function () {
|
||||
this.stopPropagation();
|
||||
this.preventDefault();
|
||||
};
|
||||
MouseEvent.prototype.getDocumentPosition = function () {
|
||||
if (this.$pos)
|
||||
return this.$pos;
|
||||
this.$pos = this.editor.renderer.screenToTextCoordinates(this.clientX, this.clientY);
|
||||
return this.$pos;
|
||||
};
|
||||
MouseEvent.prototype.getGutterRow = function () {
|
||||
var documentRow = this.getDocumentPosition().row;
|
||||
var screenRow = this.editor.session.documentToScreenRow(documentRow, 0);
|
||||
var screenTopRow = this.editor.session.documentToScreenRow(this.editor.renderer.$gutterLayer.$lines.get(0).row, 0);
|
||||
return screenRow - screenTopRow;
|
||||
};
|
||||
MouseEvent.prototype.inSelection = function () {
|
||||
if (this.$inSelection !== null)
|
||||
return this.$inSelection;
|
||||
var editor = this.editor;
|
||||
var selectionRange = editor.getSelectionRange();
|
||||
if (selectionRange.isEmpty())
|
||||
this.$inSelection = false;
|
||||
else {
|
||||
var pos = this.getDocumentPosition();
|
||||
this.$inSelection = selectionRange.contains(pos.row, pos.column);
|
||||
}
|
||||
return this.$inSelection;
|
||||
};
|
||||
MouseEvent.prototype.getButton = function () {
|
||||
return event.getButton(this.domEvent);
|
||||
};
|
||||
MouseEvent.prototype.getShiftKey = function () {
|
||||
return this.domEvent.shiftKey;
|
||||
};
|
||||
MouseEvent.prototype.getAccelKey = function () {
|
||||
return useragent.isMac ? this.domEvent.metaKey : this.domEvent.ctrlKey;
|
||||
};
|
||||
return MouseEvent;
|
||||
}());
|
||||
exports.MouseEvent = MouseEvent;
|
||||
|
||||
});
|
||||
|
||||
ace.define("ace/lib/scroll",["require","exports","module"], function(require, exports, module){exports.preventParentScroll = function preventParentScroll(event) {
|
||||
event.stopPropagation();
|
||||
var target = event.currentTarget;
|
||||
@@ -3090,8 +3187,20 @@ var ace$2 = {exports: {}};
|
||||
dom.addCssClass(this.getElement(), className);
|
||||
};
|
||||
Tooltip.prototype.setTheme = function (theme) {
|
||||
this.$element.className = CLASSNAME + " " +
|
||||
(theme.isDark ? "ace_dark " : "") + (theme.cssClass || "");
|
||||
if (this.theme) {
|
||||
this.theme.isDark && dom.removeCssClass(this.getElement(), "ace_dark");
|
||||
this.theme.cssClass && dom.removeCssClass(this.getElement(), this.theme.cssClass);
|
||||
}
|
||||
if (theme.isDark) {
|
||||
dom.addCssClass(this.getElement(), "ace_dark");
|
||||
}
|
||||
if (theme.cssClass) {
|
||||
dom.addCssClass(this.getElement(), theme.cssClass);
|
||||
}
|
||||
this.theme = {
|
||||
isDark: theme.isDark,
|
||||
cssClass: theme.cssClass
|
||||
};
|
||||
};
|
||||
Tooltip.prototype.show = function (text, x, y) {
|
||||
if (text != null)
|
||||
@@ -3218,12 +3327,18 @@ var ace$2 = {exports: {}};
|
||||
HoverTooltip.prototype.addToEditor = function (editor) {
|
||||
editor.on("mousemove", this.onMouseMove);
|
||||
editor.on("mousedown", this.hide);
|
||||
editor.renderer.getMouseEventTarget().addEventListener("mouseout", this.onMouseOut, true);
|
||||
var target = editor.renderer.getMouseEventTarget();
|
||||
if (target && typeof target.removeEventListener === "function") {
|
||||
target.addEventListener("mouseout", this.onMouseOut, true);
|
||||
}
|
||||
};
|
||||
HoverTooltip.prototype.removeFromEditor = function (editor) {
|
||||
editor.off("mousemove", this.onMouseMove);
|
||||
editor.off("mousedown", this.hide);
|
||||
editor.renderer.getMouseEventTarget().removeEventListener("mouseout", this.onMouseOut, true);
|
||||
var target = editor.renderer.getMouseEventTarget();
|
||||
if (target && typeof target.removeEventListener === "function") {
|
||||
target.removeEventListener("mouseout", this.onMouseOut, true);
|
||||
}
|
||||
if (this.timeout) {
|
||||
clearTimeout(this.timeout);
|
||||
this.timeout = null;
|
||||
@@ -3278,7 +3393,6 @@ var ace$2 = {exports: {}};
|
||||
this.$gatherData = value;
|
||||
};
|
||||
HoverTooltip.prototype.showForRange = function (editor, range, domNode, startingEvent) {
|
||||
var MARGIN = 10;
|
||||
if (startingEvent && startingEvent != this.lastEvent)
|
||||
return;
|
||||
if (this.isOpen && document.activeElement == this.getElement())
|
||||
@@ -3290,7 +3404,6 @@ var ace$2 = {exports: {}};
|
||||
this.setTheme(renderer.theme);
|
||||
}
|
||||
this.isOpen = true;
|
||||
this.addMarker(range, editor.session);
|
||||
this.range = Range.fromPoints(range.start, range.end);
|
||||
var position = renderer.textToScreenCoordinates(range.start.row, range.start.column);
|
||||
var rect = renderer.scroller.getBoundingClientRect();
|
||||
@@ -3301,17 +3414,27 @@ var ace$2 = {exports: {}};
|
||||
element.appendChild(domNode);
|
||||
element.style.maxHeight = "";
|
||||
element.style.display = "block";
|
||||
var labelHeight = element.clientHeight;
|
||||
var labelWidth = element.clientWidth;
|
||||
var spaceBelow = window.innerHeight - position.pageY - renderer.lineHeight;
|
||||
var isAbove = true;
|
||||
if (position.pageY - labelHeight < 0 && position.pageY < spaceBelow) {
|
||||
isAbove = false;
|
||||
}
|
||||
element.style.maxHeight = (isAbove ? position.pageY : spaceBelow) - MARGIN + "px";
|
||||
element.style.top = isAbove ? "" : position.pageY + renderer.lineHeight + "px";
|
||||
element.style.bottom = isAbove ? window.innerHeight - position.pageY + "px" : "";
|
||||
element.style.left = Math.min(position.pageX, window.innerWidth - labelWidth - MARGIN) + "px";
|
||||
this.$setPosition(editor, position, true, range);
|
||||
dom.$fixPositionBug(element);
|
||||
};
|
||||
HoverTooltip.prototype.$setPosition = function (editor, position, withMarker, range) {
|
||||
var MARGIN = 10;
|
||||
withMarker && this.addMarker(range, editor.session);
|
||||
var renderer = editor.renderer;
|
||||
var element = this.getElement();
|
||||
var labelHeight = element.offsetHeight;
|
||||
var labelWidth = element.offsetWidth;
|
||||
var anchorTop = position.pageY;
|
||||
var anchorLeft = position.pageX;
|
||||
var spaceBelow = window.innerHeight - anchorTop - renderer.lineHeight;
|
||||
var isAbove = this.$shouldPlaceAbove(labelHeight, anchorTop, spaceBelow - MARGIN);
|
||||
element.style.maxHeight = (isAbove ? anchorTop : spaceBelow) - MARGIN + "px";
|
||||
element.style.top = isAbove ? "" : anchorTop + renderer.lineHeight + "px";
|
||||
element.style.bottom = isAbove ? window.innerHeight - anchorTop + "px" : "";
|
||||
element.style.left = Math.min(anchorLeft, window.innerWidth - labelWidth - MARGIN) + "px";
|
||||
};
|
||||
HoverTooltip.prototype.$shouldPlaceAbove = function (labelHeight, anchorTop, spaceBelow) {
|
||||
return !(anchorTop - labelHeight < 0 && anchorTop < spaceBelow);
|
||||
};
|
||||
HoverTooltip.prototype.addMarker = function (range, session) {
|
||||
if (this.marker) {
|
||||
@@ -3321,6 +3444,11 @@ var ace$2 = {exports: {}};
|
||||
this.marker = session && session.addMarker(range, "ace_highlight-marker", "text");
|
||||
};
|
||||
HoverTooltip.prototype.hide = function (e) {
|
||||
if (e && this.$fromKeyboard && e.type == "keydown") {
|
||||
if (e.code == "Escape") {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!e && document.activeElement == this.getElement())
|
||||
return;
|
||||
if (e && e.target && (e.type != "keydown" || e.ctrlKey || e.metaKey) && this.$element.contains(e.target))
|
||||
@@ -3331,6 +3459,7 @@ var ace$2 = {exports: {}};
|
||||
this.timeout = null;
|
||||
this.addMarker(null);
|
||||
if (this.isOpen) {
|
||||
this.$fromKeyboard = false;
|
||||
this.$removeCloseEvents();
|
||||
this.getElement().style.display = "none";
|
||||
this.isOpen = false;
|
||||
@@ -3368,7 +3497,7 @@ var ace$2 = {exports: {}};
|
||||
|
||||
});
|
||||
|
||||
ace.define("ace/mouse/default_gutter_handler",["require","exports","module","ace/lib/dom","ace/lib/event","ace/tooltip","ace/config"], function(require, exports, module){ var __extends = (this && this.__extends) || (function () {
|
||||
ace.define("ace/mouse/default_gutter_handler",["require","exports","module","ace/lib/dom","ace/mouse/mouse_event","ace/tooltip","ace/config","ace/range"], function(require, exports, module){ var __extends = (this && this.__extends) || (function () {
|
||||
var extendStatics = function (d, b) {
|
||||
extendStatics = Object.setPrototypeOf ||
|
||||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
|
||||
@@ -3395,17 +3524,19 @@ var ace$2 = {exports: {}};
|
||||
throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
|
||||
};
|
||||
var dom = require("../lib/dom");
|
||||
var event = require("../lib/event");
|
||||
var Tooltip = require("../tooltip").Tooltip;
|
||||
var MouseEvent = require("./mouse_event").MouseEvent;
|
||||
var HoverTooltip = require("../tooltip").HoverTooltip;
|
||||
var nls = require("../config").nls;
|
||||
var GUTTER_TOOLTIP_LEFT_OFFSET = 5;
|
||||
var GUTTER_TOOLTIP_TOP_OFFSET = 3;
|
||||
exports.GUTTER_TOOLTIP_LEFT_OFFSET = GUTTER_TOOLTIP_LEFT_OFFSET;
|
||||
exports.GUTTER_TOOLTIP_TOP_OFFSET = GUTTER_TOOLTIP_TOP_OFFSET;
|
||||
var Range = require("../range").Range;
|
||||
function GutterHandler(mouseHandler) {
|
||||
var editor = mouseHandler.editor;
|
||||
var gutter = editor.renderer.$gutterLayer;
|
||||
var tooltip = new GutterTooltip(editor, true);
|
||||
mouseHandler.$tooltip = new GutterTooltip(editor);
|
||||
mouseHandler.$tooltip.addToEditor(editor);
|
||||
mouseHandler.$tooltip.setDataProvider(function (e, editor) {
|
||||
var row = e.getDocumentPosition().row;
|
||||
mouseHandler.$tooltip.showTooltip(row);
|
||||
});
|
||||
mouseHandler.editor.setDefaultHandler("guttermousedown", function (e) {
|
||||
if (!editor.isFocused() || e.getButton() != 0)
|
||||
return;
|
||||
@@ -3427,87 +3558,11 @@ var ace$2 = {exports: {}};
|
||||
mouseHandler.captureMouse(e);
|
||||
return e.preventDefault();
|
||||
});
|
||||
var tooltipTimeout, mouseEvent;
|
||||
function showTooltip() {
|
||||
var row = mouseEvent.getDocumentPosition().row;
|
||||
var maxRow = editor.session.getLength();
|
||||
if (row == maxRow) {
|
||||
var screenRow = editor.renderer.pixelToScreenCoordinates(0, mouseEvent.y).row;
|
||||
var pos = mouseEvent.$pos;
|
||||
if (screenRow > editor.session.documentToScreenRow(pos.row, pos.column))
|
||||
return hideTooltip();
|
||||
}
|
||||
tooltip.showTooltip(row);
|
||||
if (!tooltip.isOpen)
|
||||
return;
|
||||
editor.on("mousewheel", hideTooltip);
|
||||
editor.on("changeSession", hideTooltip);
|
||||
window.addEventListener("keydown", hideTooltip, true);
|
||||
if (mouseHandler.$tooltipFollowsMouse) {
|
||||
moveTooltip(mouseEvent);
|
||||
}
|
||||
else {
|
||||
var gutterRow = mouseEvent.getGutterRow();
|
||||
var gutterCell = gutter.$lines.get(gutterRow);
|
||||
if (gutterCell) {
|
||||
var gutterElement = gutterCell.element.querySelector(".ace_gutter_annotation");
|
||||
var rect = gutterElement.getBoundingClientRect();
|
||||
var style = tooltip.getElement().style;
|
||||
style.left = (rect.right - GUTTER_TOOLTIP_LEFT_OFFSET) + "px";
|
||||
style.top = (rect.bottom - GUTTER_TOOLTIP_TOP_OFFSET) + "px";
|
||||
}
|
||||
else {
|
||||
moveTooltip(mouseEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
function hideTooltip(e) {
|
||||
if (e && e.type === "keydown" && (e.ctrlKey || e.metaKey))
|
||||
return;
|
||||
if (e && e.type === "mouseout" && (!e.relatedTarget || tooltip.getElement().contains(e.relatedTarget)))
|
||||
return;
|
||||
if (tooltipTimeout)
|
||||
tooltipTimeout = clearTimeout(tooltipTimeout);
|
||||
if (tooltip.isOpen) {
|
||||
tooltip.hideTooltip();
|
||||
editor.off("mousewheel", hideTooltip);
|
||||
editor.off("changeSession", hideTooltip);
|
||||
window.removeEventListener("keydown", hideTooltip, true);
|
||||
}
|
||||
}
|
||||
function moveTooltip(e) {
|
||||
tooltip.setPosition(e.x, e.y);
|
||||
}
|
||||
mouseHandler.editor.setDefaultHandler("guttermousemove", function (e) {
|
||||
var target = e.domEvent.target || e.domEvent.srcElement;
|
||||
if (dom.hasCssClass(target, "ace_fold-widget") || dom.hasCssClass(target, "ace_custom-widget"))
|
||||
return hideTooltip();
|
||||
if (tooltip.isOpen && mouseHandler.$tooltipFollowsMouse)
|
||||
moveTooltip(e);
|
||||
mouseEvent = e;
|
||||
if (tooltipTimeout)
|
||||
return;
|
||||
tooltipTimeout = setTimeout(function () {
|
||||
tooltipTimeout = null;
|
||||
if (mouseEvent && !mouseHandler.isMousePressed)
|
||||
showTooltip();
|
||||
}, 50);
|
||||
});
|
||||
event.addListener(editor.renderer.$gutter, "mouseout", function (e) {
|
||||
mouseEvent = null;
|
||||
if (!tooltip.isOpen)
|
||||
return;
|
||||
tooltipTimeout = setTimeout(function () {
|
||||
tooltipTimeout = null;
|
||||
hideTooltip(e);
|
||||
}, 50);
|
||||
}, editor);
|
||||
}
|
||||
exports.GutterHandler = GutterHandler;
|
||||
var GutterTooltip = /** @class */ (function (_super) {
|
||||
__extends(GutterTooltip, _super);
|
||||
function GutterTooltip(editor, isHover) {
|
||||
if (isHover === void 0) { isHover = false; }
|
||||
function GutterTooltip(editor) {
|
||||
var _this = _super.call(this, editor.container) || this;
|
||||
_this.id = "gt" + (++GutterTooltip.$uid);
|
||||
_this.editor = editor;
|
||||
@@ -3516,35 +3571,37 @@ var ace$2 = {exports: {}};
|
||||
el.setAttribute("role", "tooltip");
|
||||
el.setAttribute("id", _this.id);
|
||||
el.style.pointerEvents = "auto";
|
||||
if (isHover) {
|
||||
_this.onMouseOut = _this.onMouseOut.bind(_this);
|
||||
el.addEventListener("mouseout", _this.onMouseOut);
|
||||
}
|
||||
_this.idleTime = 50;
|
||||
_this.onDomMouseMove = _this.onDomMouseMove.bind(_this);
|
||||
_this.onDomMouseOut = _this.onDomMouseOut.bind(_this);
|
||||
_this.setClassName("ace_gutter-tooltip");
|
||||
return _this;
|
||||
}
|
||||
GutterTooltip.prototype.onMouseOut = function (e) {
|
||||
if (!this.isOpen)
|
||||
return;
|
||||
if (!e.relatedTarget || this.getElement().contains(e.relatedTarget))
|
||||
return;
|
||||
if (e && e.currentTarget.contains(e.relatedTarget))
|
||||
return;
|
||||
this.hideTooltip();
|
||||
GutterTooltip.prototype.onDomMouseMove = function (domEvent) {
|
||||
var aceEvent = new MouseEvent(domEvent, this.editor);
|
||||
this.onMouseMove(aceEvent, this.editor);
|
||||
};
|
||||
GutterTooltip.prototype.setPosition = function (x, y) {
|
||||
var windowWidth = window.innerWidth || document.documentElement.clientWidth;
|
||||
var windowHeight = window.innerHeight || document.documentElement.clientHeight;
|
||||
var width = this.getWidth();
|
||||
var height = this.getHeight();
|
||||
x += 15;
|
||||
y += 15;
|
||||
if (x + width > windowWidth) {
|
||||
x -= (x + width) - windowWidth;
|
||||
GutterTooltip.prototype.onDomMouseOut = function (domEvent) {
|
||||
var aceEvent = new MouseEvent(domEvent, this.editor);
|
||||
this.onMouseOut(aceEvent);
|
||||
};
|
||||
GutterTooltip.prototype.addToEditor = function (editor) {
|
||||
var gutter = editor.renderer.$gutter;
|
||||
gutter.addEventListener("mousemove", this.onDomMouseMove);
|
||||
gutter.addEventListener("mouseout", this.onDomMouseOut);
|
||||
_super.prototype.addToEditor.call(this, editor);
|
||||
};
|
||||
GutterTooltip.prototype.removeFromEditor = function (editor) {
|
||||
var gutter = editor.renderer.$gutter;
|
||||
gutter.removeEventListener("mousemove", this.onDomMouseMove);
|
||||
gutter.removeEventListener("mouseout", this.onDomMouseOut);
|
||||
_super.prototype.removeFromEditor.call(this, editor);
|
||||
};
|
||||
GutterTooltip.prototype.destroy = function () {
|
||||
if (this.editor) {
|
||||
this.removeFromEditor(this.editor);
|
||||
}
|
||||
if (y + height > windowHeight) {
|
||||
y -= 20 + height;
|
||||
}
|
||||
Tooltip.prototype.setPosition.call(this, x, y);
|
||||
_super.prototype.destroy.call(this);
|
||||
};
|
||||
Object.defineProperty(GutterTooltip, "annotationLabels", {
|
||||
get: function () {
|
||||
@@ -3610,7 +3667,7 @@ var ace$2 = {exports: {}};
|
||||
}
|
||||
}
|
||||
if (annotation.displayText.length === 0)
|
||||
return this.hideTooltip();
|
||||
return this.hide();
|
||||
var annotationMessages = { error: [], security: [], warning: [], info: [], hint: [] };
|
||||
var iconClassName = gutter.$useSvgGutterIcons ? "ace_icon_svg" : "ace_icon";
|
||||
for (var i = 0; i < annotation.displayText.length; i++) {
|
||||
@@ -3625,26 +3682,42 @@ var ace$2 = {exports: {}};
|
||||
lineElement.appendChild(dom.createElement("br"));
|
||||
annotationMessages[annotation.type[i].replace("_fold", "")].push(lineElement);
|
||||
}
|
||||
var tooltipElement = this.getElement();
|
||||
dom.removeChildren(tooltipElement);
|
||||
var tooltipElement = dom.createElement("span");
|
||||
annotationMessages.error.forEach(function (el) { return tooltipElement.appendChild(el); });
|
||||
annotationMessages.security.forEach(function (el) { return tooltipElement.appendChild(el); });
|
||||
annotationMessages.warning.forEach(function (el) { return tooltipElement.appendChild(el); });
|
||||
annotationMessages.info.forEach(function (el) { return tooltipElement.appendChild(el); });
|
||||
annotationMessages.hint.forEach(function (el) { return tooltipElement.appendChild(el); });
|
||||
tooltipElement.setAttribute("aria-live", "polite");
|
||||
if (!this.isOpen) {
|
||||
this.setTheme(this.editor.renderer.theme);
|
||||
this.setClassName("ace_gutter-tooltip");
|
||||
}
|
||||
var annotationNode = this.$findLinkedAnnotationNode(row);
|
||||
if (annotationNode) {
|
||||
annotationNode.setAttribute("aria-describedby", this.id);
|
||||
}
|
||||
this.show();
|
||||
var range = Range.fromPoints({ row: row, column: 0 }, { row: row, column: 0 });
|
||||
this.showForRange(this.editor, range, tooltipElement);
|
||||
this.visibleTooltipRow = row;
|
||||
this.editor._signal("showGutterTooltip", this);
|
||||
};
|
||||
GutterTooltip.prototype.$setPosition = function (editor, _ignoredPosition, _withMarker, range) {
|
||||
var gutterCell = this.$findCellByRow(range.start.row);
|
||||
if (!gutterCell)
|
||||
return;
|
||||
var el = gutterCell && gutterCell.element;
|
||||
var anchorEl = el && (el.querySelector(".ace_gutter_annotation"));
|
||||
if (!anchorEl)
|
||||
return;
|
||||
var r = anchorEl.getBoundingClientRect();
|
||||
if (!r)
|
||||
return;
|
||||
var position = {
|
||||
pageX: r.right,
|
||||
pageY: r.top
|
||||
};
|
||||
return _super.prototype.$setPosition.call(this, editor, position, false, range);
|
||||
};
|
||||
GutterTooltip.prototype.$shouldPlaceAbove = function (labelHeight, anchorTop, spaceBelow) {
|
||||
return spaceBelow < labelHeight;
|
||||
};
|
||||
GutterTooltip.prototype.$findLinkedAnnotationNode = function (row) {
|
||||
var cell = this.$findCellByRow(row);
|
||||
if (cell) {
|
||||
@@ -3657,12 +3730,11 @@ var ace$2 = {exports: {}};
|
||||
GutterTooltip.prototype.$findCellByRow = function (row) {
|
||||
return this.editor.renderer.$gutterLayer.$lines.cells.find(function (el) { return el.row === row; });
|
||||
};
|
||||
GutterTooltip.prototype.hideTooltip = function () {
|
||||
GutterTooltip.prototype.hide = function (e) {
|
||||
if (!this.isOpen) {
|
||||
return;
|
||||
}
|
||||
this.$element.removeAttribute("aria-live");
|
||||
this.hide();
|
||||
if (this.visibleTooltipRow != undefined) {
|
||||
var annotationNode = this.$findLinkedAnnotationNode(this.visibleTooltipRow);
|
||||
if (annotationNode) {
|
||||
@@ -3671,6 +3743,7 @@ var ace$2 = {exports: {}};
|
||||
}
|
||||
this.visibleTooltipRow = undefined;
|
||||
this.editor._signal("hideGutterTooltip", this);
|
||||
_super.prototype.hide.call(this, e);
|
||||
};
|
||||
GutterTooltip.annotationsToSummaryString = function (annotations) {
|
||||
var e_1, _a;
|
||||
@@ -3694,78 +3767,19 @@ var ace$2 = {exports: {}};
|
||||
}
|
||||
return summary.join(", ");
|
||||
};
|
||||
GutterTooltip.prototype.isOutsideOfText = function (e) {
|
||||
var editor = e.editor;
|
||||
var rect = editor.renderer.$gutter.getBoundingClientRect();
|
||||
return !(e.clientX >= rect.left && e.clientX <= rect.right &&
|
||||
e.clientY >= rect.top && e.clientY <= rect.bottom);
|
||||
};
|
||||
return GutterTooltip;
|
||||
}(Tooltip));
|
||||
}(HoverTooltip));
|
||||
GutterTooltip.$uid = 0;
|
||||
exports.GutterTooltip = GutterTooltip;
|
||||
|
||||
});
|
||||
|
||||
ace.define("ace/mouse/mouse_event",["require","exports","module","ace/lib/event","ace/lib/useragent"], function(require, exports, module){ var event = require("../lib/event");
|
||||
var useragent = require("../lib/useragent");
|
||||
var MouseEvent = /** @class */ (function () {
|
||||
function MouseEvent(domEvent, editor) { this.speed; this.wheelX; this.wheelY;
|
||||
this.domEvent = domEvent;
|
||||
this.editor = editor;
|
||||
this.x = this.clientX = domEvent.clientX;
|
||||
this.y = this.clientY = domEvent.clientY;
|
||||
this.$pos = null;
|
||||
this.$inSelection = null;
|
||||
this.propagationStopped = false;
|
||||
this.defaultPrevented = false;
|
||||
}
|
||||
MouseEvent.prototype.stopPropagation = function () {
|
||||
event.stopPropagation(this.domEvent);
|
||||
this.propagationStopped = true;
|
||||
};
|
||||
MouseEvent.prototype.preventDefault = function () {
|
||||
event.preventDefault(this.domEvent);
|
||||
this.defaultPrevented = true;
|
||||
};
|
||||
MouseEvent.prototype.stop = function () {
|
||||
this.stopPropagation();
|
||||
this.preventDefault();
|
||||
};
|
||||
MouseEvent.prototype.getDocumentPosition = function () {
|
||||
if (this.$pos)
|
||||
return this.$pos;
|
||||
this.$pos = this.editor.renderer.screenToTextCoordinates(this.clientX, this.clientY);
|
||||
return this.$pos;
|
||||
};
|
||||
MouseEvent.prototype.getGutterRow = function () {
|
||||
var documentRow = this.getDocumentPosition().row;
|
||||
var screenRow = this.editor.session.documentToScreenRow(documentRow, 0);
|
||||
var screenTopRow = this.editor.session.documentToScreenRow(this.editor.renderer.$gutterLayer.$lines.get(0).row, 0);
|
||||
return screenRow - screenTopRow;
|
||||
};
|
||||
MouseEvent.prototype.inSelection = function () {
|
||||
if (this.$inSelection !== null)
|
||||
return this.$inSelection;
|
||||
var editor = this.editor;
|
||||
var selectionRange = editor.getSelectionRange();
|
||||
if (selectionRange.isEmpty())
|
||||
this.$inSelection = false;
|
||||
else {
|
||||
var pos = this.getDocumentPosition();
|
||||
this.$inSelection = selectionRange.contains(pos.row, pos.column);
|
||||
}
|
||||
return this.$inSelection;
|
||||
};
|
||||
MouseEvent.prototype.getButton = function () {
|
||||
return event.getButton(this.domEvent);
|
||||
};
|
||||
MouseEvent.prototype.getShiftKey = function () {
|
||||
return this.domEvent.shiftKey;
|
||||
};
|
||||
MouseEvent.prototype.getAccelKey = function () {
|
||||
return useragent.isMac ? this.domEvent.metaKey : this.domEvent.ctrlKey;
|
||||
};
|
||||
return MouseEvent;
|
||||
}());
|
||||
exports.MouseEvent = MouseEvent;
|
||||
|
||||
});
|
||||
|
||||
ace.define("ace/mouse/dragdrop_handler",["require","exports","module","ace/lib/dom","ace/lib/event","ace/lib/useragent"], function(require, exports, module){ var dom = require("../lib/dom");
|
||||
var event = require("../lib/event");
|
||||
var useragent = require("../lib/useragent");
|
||||
@@ -4574,6 +4588,8 @@ var ace$2 = {exports: {}};
|
||||
MouseHandler.prototype.destroy = function () {
|
||||
if (this.releaseMouse)
|
||||
this.releaseMouse();
|
||||
if (this.$tooltip)
|
||||
this.$tooltip.destroy();
|
||||
};
|
||||
return MouseHandler;
|
||||
}());
|
||||
@@ -4583,7 +4599,6 @@ var ace$2 = {exports: {}};
|
||||
dragDelay: { initialValue: (useragent.isMac ? 150 : 0) },
|
||||
dragEnabled: { initialValue: true },
|
||||
focusTimeout: { initialValue: 0 },
|
||||
tooltipFollowsMouse: { initialValue: true }
|
||||
});
|
||||
exports.MouseHandler = MouseHandler;
|
||||
|
||||
@@ -13724,8 +13739,7 @@ var ace$2 = {exports: {}};
|
||||
|
||||
});
|
||||
|
||||
ace.define("ace/keyboard/gutter_handler",["require","exports","module","ace/lib/keys","ace/mouse/default_gutter_handler"], function(require, exports, module){ var keys = require('../lib/keys');
|
||||
var GutterTooltip = require("../mouse/default_gutter_handler").GutterTooltip;
|
||||
ace.define("ace/keyboard/gutter_handler",["require","exports","module","ace/lib/keys"], function(require, exports, module){ var keys = require('../lib/keys');
|
||||
var GutterKeyboardHandler = /** @class */ (function () {
|
||||
function GutterKeyboardHandler(editor) {
|
||||
this.editor = editor;
|
||||
@@ -13734,7 +13748,7 @@ var ace$2 = {exports: {}};
|
||||
this.lines = editor.renderer.$gutterLayer.$lines;
|
||||
this.activeRowIndex = null;
|
||||
this.activeLane = null;
|
||||
this.annotationTooltip = new GutterTooltip(this.editor);
|
||||
this.annotationTooltip = this.editor.$mouseHandler.$tooltip;
|
||||
}
|
||||
GutterKeyboardHandler.prototype.addListener = function () {
|
||||
this.element.addEventListener("keydown", this.$onGutterKeyDown.bind(this));
|
||||
@@ -13750,7 +13764,7 @@ var ace$2 = {exports: {}};
|
||||
if (this.annotationTooltip.isOpen) {
|
||||
e.preventDefault();
|
||||
if (e.keyCode === keys["escape"])
|
||||
this.annotationTooltip.hideTooltip();
|
||||
this.annotationTooltip.hide();
|
||||
return;
|
||||
}
|
||||
if (e.target === this.element) {
|
||||
@@ -13869,12 +13883,8 @@ var ace$2 = {exports: {}};
|
||||
}
|
||||
return;
|
||||
case "annotation":
|
||||
var gutterElement = this.lines.cells[this.activeRowIndex].element.childNodes[2];
|
||||
var rect = gutterElement.getBoundingClientRect();
|
||||
var style = this.annotationTooltip.getElement().style;
|
||||
style.left = rect.right + "px";
|
||||
style.top = rect.bottom + "px";
|
||||
this.annotationTooltip.showTooltip(this.$rowIndexToRow(this.activeRowIndex));
|
||||
this.annotationTooltip.$fromKeyboard = true;
|
||||
break;
|
||||
}
|
||||
return;
|
||||
@@ -13893,7 +13903,7 @@ var ace$2 = {exports: {}};
|
||||
}
|
||||
}
|
||||
if (this.annotationTooltip.isOpen)
|
||||
this.annotationTooltip.hideTooltip();
|
||||
this.annotationTooltip.hide();
|
||||
return;
|
||||
};
|
||||
GutterKeyboardHandler.prototype.$isFoldWidgetVisible = function (index) {
|
||||
@@ -16178,7 +16188,6 @@ var ace$2 = {exports: {}};
|
||||
dragDelay: "$mouseHandler",
|
||||
dragEnabled: "$mouseHandler",
|
||||
focusTimeout: "$mouseHandler",
|
||||
tooltipFollowsMouse: "$mouseHandler",
|
||||
firstLineNumber: "session",
|
||||
overwrite: "session",
|
||||
newLineMode: "session",
|
||||
@@ -16328,6 +16337,7 @@ var ace$2 = {exports: {}};
|
||||
var nls = require("../config").nls;
|
||||
var Gutter = /** @class */ (function () {
|
||||
function Gutter(parentEl) {
|
||||
this.$showCursorMarker = null;
|
||||
this.element = dom.createElement("div");
|
||||
this.element.className = "ace_layer ace_gutter-layer";
|
||||
parentEl.appendChild(this.element);
|
||||
@@ -16448,6 +16458,8 @@ var ace$2 = {exports: {}};
|
||||
}
|
||||
this._signal("afterRender");
|
||||
this.$updateGutterWidth(config);
|
||||
if (this.$showCursorMarker && this.$highlightGutterLine)
|
||||
this.$updateCursorMarker();
|
||||
};
|
||||
Gutter.prototype.$updateGutterWidth = function (config) {
|
||||
var session = this.session;
|
||||
@@ -16476,6 +16488,8 @@ var ace$2 = {exports: {}};
|
||||
this.$cursorRow = position.row;
|
||||
};
|
||||
Gutter.prototype.updateLineHighlight = function () {
|
||||
if (this.$showCursorMarker)
|
||||
this.$updateCursorMarker();
|
||||
if (!this.$highlightGutterLine)
|
||||
return;
|
||||
var row = this.session.selection.cursor.row;
|
||||
@@ -16502,6 +16516,26 @@ var ace$2 = {exports: {}};
|
||||
}
|
||||
}
|
||||
};
|
||||
Gutter.prototype.$updateCursorMarker = function () {
|
||||
if (!this.session)
|
||||
return;
|
||||
var session = this.session;
|
||||
if (!this.$highlightElement) {
|
||||
this.$highlightElement = dom.createElement("div");
|
||||
this.$highlightElement.className = "ace_gutter-cursor";
|
||||
this.$highlightElement.style.pointerEvents = "none";
|
||||
this.element.appendChild(this.$highlightElement);
|
||||
}
|
||||
var pos = session.selection.cursor;
|
||||
var config = this.config;
|
||||
var lines = this.$lines;
|
||||
var screenTop = config.firstRowScreen * config.lineHeight;
|
||||
var screenPage = Math.floor(screenTop / lines.canvasHeight);
|
||||
var lineTop = session.documentToScreenRow(pos) * config.lineHeight;
|
||||
var top = lineTop - (screenPage * lines.canvasHeight);
|
||||
dom.setStyle(this.$highlightElement.style, "height", config.lineHeight + "px");
|
||||
dom.setStyle(this.$highlightElement.style, "top", top + "px");
|
||||
};
|
||||
Gutter.prototype.scrollLines = function (config) {
|
||||
var oldConfig = this.config;
|
||||
this.config = config;
|
||||
@@ -16745,6 +16779,10 @@ var ace$2 = {exports: {}};
|
||||
};
|
||||
Gutter.prototype.setHighlightGutterLine = function (highlightGutterLine) {
|
||||
this.$highlightGutterLine = highlightGutterLine;
|
||||
if (!highlightGutterLine && this.$highlightElement) {
|
||||
this.$highlightElement.remove();
|
||||
this.$highlightElement = null;
|
||||
}
|
||||
};
|
||||
Gutter.prototype.setShowLineNumbers = function (show) {
|
||||
this.$renderer = !show && {
|
||||
@@ -16786,8 +16824,24 @@ var ace$2 = {exports: {}};
|
||||
};
|
||||
Gutter.prototype.$getGutterCell = function (row) {
|
||||
var cells = this.$lines.cells;
|
||||
var visibileRow = this.session.documentToScreenRow(row, 0);
|
||||
return cells[row - this.config.firstRowScreen - (row - visibileRow)];
|
||||
var min = 0;
|
||||
var max = cells.length - 1;
|
||||
if (row < cells[0].row || row > cells[max].row)
|
||||
return;
|
||||
while (min <= max) {
|
||||
var mid = Math.floor((min + max) / 2);
|
||||
var cell = cells[mid];
|
||||
if (cell.row > row) {
|
||||
max = mid - 1;
|
||||
}
|
||||
else if (cell.row < row) {
|
||||
min = mid + 1;
|
||||
}
|
||||
else {
|
||||
return cell;
|
||||
}
|
||||
}
|
||||
return cell;
|
||||
};
|
||||
Gutter.prototype.$addCustomWidget = function (row, _a, cell) {
|
||||
var className = _a.className, label = _a.label, title = _a.title, callbacks = _a.callbacks;
|
||||
@@ -16850,7 +16904,7 @@ var ace$2 = {exports: {}};
|
||||
}());
|
||||
Gutter.prototype.$fixedWidth = false;
|
||||
Gutter.prototype.$highlightGutterLine = true;
|
||||
Gutter.prototype.$renderer = "";
|
||||
Gutter.prototype.$renderer = undefined;
|
||||
Gutter.prototype.$showLineNumbers = true;
|
||||
Gutter.prototype.$showFoldWidgets = true;
|
||||
oop.implement(Gutter.prototype, EventEmitter);
|
||||
@@ -19856,6 +19910,15 @@ var ace$2 = {exports: {}};
|
||||
: "padding" in (_self.theme || {}) ? 4 : _self.$padding;
|
||||
if (_self.$padding && padding != _self.$padding)
|
||||
_self.setPadding(padding);
|
||||
if (_self.$gutterLayer) {
|
||||
var showGutterCursor = module["$showGutterCursorMarker"];
|
||||
if (showGutterCursor && !_self.$gutterLayer.$showCursorMarker) {
|
||||
_self.$gutterLayer.$showCursorMarker = "theme";
|
||||
}
|
||||
else if (!showGutterCursor && _self.$gutterLayer.$showCursorMarker == "theme") {
|
||||
_self.$gutterLayer.$showCursorMarker = null;
|
||||
}
|
||||
}
|
||||
_self.$theme = module.cssClass;
|
||||
_self.theme = module;
|
||||
dom.addCssClass(_self.container, module.cssClass);
|
||||
@@ -1,18 +1,10 @@
|
||||
import time
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Callable, Dict, List, Optional, Union, Iterator
|
||||
|
||||
from .clashruleparser import ClashRuleParser
|
||||
from ..models.rule import Action, RoutingRuleType, MatchRule, ClashRule, LogicRule, SubRule
|
||||
from pydantic import TypeAdapter, ValidationError
|
||||
|
||||
|
||||
@dataclass
|
||||
class RuleItem:
|
||||
"""Clash rule item"""
|
||||
rule: Union[ClashRule, LogicRule, MatchRule, SubRule]
|
||||
remark: str = field(default="")
|
||||
time_modified: float = field(default=0)
|
||||
from ..models.metadata import Metadata
|
||||
from ..models.rule import Action, RoutingRuleType, MatchRule, ClashRule, LogicRule
|
||||
from ..models.ruleitem import RuleItem, RuleData
|
||||
|
||||
|
||||
class ClashRuleManager:
|
||||
@@ -21,20 +13,17 @@ class ClashRuleManager:
|
||||
self.rules: List[RuleItem] = []
|
||||
|
||||
def import_rules(self, rules_list: List[Dict[str, Any]]):
|
||||
self.rules = []
|
||||
self.rules.clear()
|
||||
for r in rules_list:
|
||||
rule = ClashRuleParser.parse_rule_line(r['rule'])
|
||||
if rule is None:
|
||||
try:
|
||||
rule = RuleItem.model_validate(r)
|
||||
except ValidationError:
|
||||
continue
|
||||
remark = r.get('remark', '')
|
||||
time_modified = r.get('time_modified', time.time())
|
||||
self.rules.append(RuleItem(rule=rule, remark=remark, time_modified=time_modified))
|
||||
self.rules.append(rule)
|
||||
|
||||
def export_rules(self) -> List[Dict[str, str]]:
|
||||
rules_list = []
|
||||
for rule in self.rules:
|
||||
rules_list.append({'rule': str(rule.rule), 'remark': rule.remark, 'time_modified': rule.time_modified})
|
||||
return rules_list
|
||||
adapter = TypeAdapter(list[RuleItem])
|
||||
return adapter.dump_python(self.rules, mode='json')
|
||||
|
||||
def append_rules(self, clash_rules: List[RuleItem]):
|
||||
self.rules.extend(clash_rules)
|
||||
@@ -64,6 +53,15 @@ class ClashRuleManager:
|
||||
return self.rules.pop(priority)
|
||||
return None
|
||||
|
||||
def remove_rules_at_priorities(self, priorities: list[int]) -> list[RuleItem]:
|
||||
"""Remove rules at specific priorities"""
|
||||
removed = []
|
||||
# Sort priorities in descending order to avoid index shift issues during removal
|
||||
for priority in sorted(priorities, reverse=True):
|
||||
if 0 <= priority < len(self.rules):
|
||||
removed.append(self.rules.pop(priority))
|
||||
return removed
|
||||
|
||||
def remove_rules_by_lambda(self, condition: Callable[[RuleItem], bool]):
|
||||
"""Remove rules by lambda"""
|
||||
initial_count = len(self.rules)
|
||||
@@ -101,7 +99,7 @@ class ClashRuleManager:
|
||||
return any(r.rule == clash_rule for r in self.rules)
|
||||
|
||||
def has_rule_item(self, clash_rule: RuleItem) -> bool:
|
||||
return any(clash_rule.remark == r.remark and r.rule == clash_rule.rule for r in self.rules)
|
||||
return any(clash_rule.meta.source == r.meta.source and r.rule == clash_rule.rule for r in self.rules)
|
||||
|
||||
def reorder_rules(self, moved_priority: int, target_priority: int) -> RuleItem:
|
||||
"""Reorder the rules"""
|
||||
@@ -113,13 +111,27 @@ class ClashRuleManager:
|
||||
self.rules.insert(target_priority, rule)
|
||||
return rule
|
||||
|
||||
def to_list(self) -> List[Dict[str, Any]]:
|
||||
def update_rules_at_priorities(self, priorities: dict[int, bool]) -> list[RuleItem]:
|
||||
"""Disable rules"""
|
||||
updated = []
|
||||
for priority, disabled in priorities.items():
|
||||
if 0 <= priority < len(self.rules):
|
||||
self.rules[priority].meta.disabled = disabled
|
||||
updated.append(self.rules[priority])
|
||||
return updated
|
||||
|
||||
def update_rule_meta_at_priority(self, priority: int, meta: Metadata) -> bool:
|
||||
"""Update rule metadata at priority"""
|
||||
if 0 <= priority < len(self.rules):
|
||||
self.rules[priority].meta = meta
|
||||
return True
|
||||
return False
|
||||
|
||||
def to_list(self) -> list[RuleData]:
|
||||
"""Convert parsed rules to a list"""
|
||||
result = []
|
||||
result: list[RuleData] = []
|
||||
for priority, rule_item in enumerate(self.rules):
|
||||
rule_dict = {'remark': rule_item.remark, 'time_modified': rule_item.time_modified,'priority': priority,
|
||||
**rule_item.rule.to_dict()}
|
||||
result.append(rule_dict)
|
||||
result.append(RuleData.from_rule_item(rule_item, priority))
|
||||
return result
|
||||
|
||||
def clear(self):
|
||||
|
||||
@@ -9,21 +9,25 @@ from ..models.rule import RuleType, Action, RoutingRuleType, MatchRule, ClashRul
|
||||
class ClashRuleParser:
|
||||
"""Parser for Clash routing rules"""
|
||||
|
||||
@staticmethod
|
||||
def parse(line: str) -> RuleType:
|
||||
"""Parse a single rule line"""
|
||||
# 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)
|
||||
elif line.startswith('SUB-RULE'):
|
||||
return ClashRuleParser._parse_sub_rule(line)
|
||||
# Handle regular rules
|
||||
return ClashRuleParser._parse_regular_rule(line)
|
||||
|
||||
@staticmethod
|
||||
def parse_rule_line(line: str) -> Optional[RuleType]:
|
||||
"""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)
|
||||
elif line.startswith('SUB-RULE'):
|
||||
return ClashRuleParser._parse_sub_rule(line)
|
||||
# Handle regular rules
|
||||
return ClashRuleParser._parse_regular_rule(line)
|
||||
|
||||
return ClashRuleParser.parse(line)
|
||||
except (ValidationError, TypeError, ValueError, RecursionError):
|
||||
return None
|
||||
|
||||
@@ -221,7 +225,7 @@ class ClashRuleParser:
|
||||
"""
|
||||
Parse conditions within logic rules, supporting nested logic.
|
||||
The examples of conditions_str:
|
||||
- (DOMAIN,baidu.com)`
|
||||
- (DOMAIN,baidu.com)
|
||||
- (AND,(DOMAIN,baidu.com),(NETWORK,TCP))
|
||||
"""
|
||||
|
||||
@@ -288,11 +292,6 @@ class ClashRuleParser:
|
||||
raise ValueError(f"Invalid rule format: {content}")
|
||||
return conditions
|
||||
|
||||
|
||||
@staticmethod
|
||||
def action_string(action: Union[Action, str]) -> str:
|
||||
return action.value if isinstance(action, Action) else action
|
||||
|
||||
@staticmethod
|
||||
def parse_rules(rules_text: str) -> List[Union[ClashRule, LogicRule, MatchRule]]:
|
||||
"""Parse multiple rules from text, preserving order and priority"""
|
||||
|
||||
@@ -2,7 +2,7 @@ import base64
|
||||
import importlib
|
||||
import json
|
||||
import os
|
||||
from typing import List, Dict, Any, Optional, Union
|
||||
from typing import Dict, Any, Optional, Union
|
||||
from urllib.parse import quote
|
||||
|
||||
from .converters import BaseConverter
|
||||
@@ -54,8 +54,8 @@ class Converter:
|
||||
print(f"Could not load converter for {module_name}: {e}")
|
||||
return converters
|
||||
|
||||
def convert_line(self, line: str, names: Optional[Dict[str, int]] = None, skip_exception: bool = True
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
def convert_line(self, line: str, names: dict[str, int] | None = None, skip_exception: bool = True,
|
||||
logger: Any = None) -> dict[str, Any] | None:
|
||||
"""
|
||||
Parses a single subscription link and converts it to a proxy dictionary.
|
||||
"""
|
||||
@@ -73,12 +73,15 @@ class Converter:
|
||||
try:
|
||||
return converter.convert(line, names)
|
||||
except Exception as e:
|
||||
if logger:
|
||||
logger.error(f"Error converting line {line}: {e}")
|
||||
if not skip_exception:
|
||||
raise ValueError(f"{scheme.upper()} parse error: {e}") from e
|
||||
return None
|
||||
return None
|
||||
|
||||
def convert_v2ray(self, v2ray_link: Union[list, bytes], skip_exception: bool = True) -> List[Dict[str, Any]]:
|
||||
def convert_v2ray(self, v2ray_link: Union[list, bytes], skip_exception: bool = True,
|
||||
logger: Any = None) -> dict[str, dict[str, Any]]:
|
||||
"""
|
||||
Converts a base64 encoded V2Ray subscription content or a list of links
|
||||
into a list of proxy dictionaries.
|
||||
@@ -89,15 +92,15 @@ class Converter:
|
||||
else:
|
||||
lines = v2ray_link
|
||||
|
||||
proxies = []
|
||||
proxies: dict[str, dict[str, Any]] = {}
|
||||
names = {}
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
proxy = self.convert_line(line, names, skip_exception=skip_exception)
|
||||
proxy = self.convert_line(line, names, skip_exception=skip_exception, logger=logger)
|
||||
if proxy:
|
||||
proxies.append(proxy)
|
||||
proxies[line] = proxy
|
||||
elif not skip_exception:
|
||||
raise ValueError("Failed to convert one of the links in the subscription.")
|
||||
return proxies
|
||||
|
||||
@@ -17,6 +17,7 @@ class HysteriaConverter(BaseConverter):
|
||||
"type": "hysteria",
|
||||
"server": parsed.hostname,
|
||||
"port": parsed.port,
|
||||
"udp": True
|
||||
}
|
||||
|
||||
auth_str = query.get("auth")
|
||||
|
||||
@@ -34,6 +34,7 @@ class Hysteria2Converter(BaseConverter):
|
||||
"skip-cert-verify": StringUtils.to_bool(query.get("insecure", "false")),
|
||||
"down": query.get("down"),
|
||||
"up": query.get("up"),
|
||||
"udp": True
|
||||
}
|
||||
if "pinSHA256" in query:
|
||||
proxy["fingerprint"] = query.get("pinSHA256")
|
||||
|
||||
114
plugins.v2/clashruleprovider/helper/dataupgrader/v_2_1_0.py
Normal file
114
plugins.v2/clashruleprovider/helper/dataupgrader/v_2_1_0.py
Normal file
@@ -0,0 +1,114 @@
|
||||
import copy
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
import jsonpatch
|
||||
from pydantic import ValidationError
|
||||
|
||||
from app.db.plugindata_oper import PluginDataOper
|
||||
|
||||
from ..configconverter import Converter
|
||||
from ..utilsprovider import UtilsProvider
|
||||
from ...models.proxygroups import ProxyGroupData
|
||||
from ...models.proxy import Proxy, ProxyData
|
||||
from ...models.ruleproviders import RuleProviderData
|
||||
from ...models.types import DataSource, DataKey
|
||||
from ...models.datapatch import PatchItem
|
||||
from ...models.metadata import Metadata
|
||||
|
||||
|
||||
def _overwrite_proxy(proxy: dict[str, Any], overwritten_proxies: dict[str, Any]) -> dict[str, Any]:
|
||||
if proxy["name"] in overwritten_proxies:
|
||||
for key in ['base', 'tls', 'network']:
|
||||
if overlay := overwritten_proxies[proxy["name"]].get(key):
|
||||
proxy.update(copy.deepcopy(overlay))
|
||||
return proxy
|
||||
|
||||
|
||||
def upgrade(plugin_id: str):
|
||||
data_oper = PluginDataOper()
|
||||
|
||||
# Upgrade proxy groups
|
||||
proxy_groups = data_oper.get_data(plugin_id, "proxy_groups") or []
|
||||
new_pg, invalid_pg, names = [], [], set()
|
||||
|
||||
for pg in proxy_groups:
|
||||
try:
|
||||
obj = ProxyGroupData(meta=Metadata(source=DataSource.MANUAL), data=pg, name=pg["name"])
|
||||
if obj.name not in names:
|
||||
new_pg.append(obj.model_dump(by_alias=True, exclude_none=True))
|
||||
names.add(obj.name)
|
||||
except ValidationError:
|
||||
invalid_pg.append(pg)
|
||||
|
||||
data_oper.save(plugin_id, DataKey.PROXY_GROUPS, new_pg)
|
||||
data_oper.save(plugin_id, "proxy_groups", invalid_pg)
|
||||
|
||||
# Upgrade rule providers
|
||||
rule_providers = data_oper.get_data(plugin_id, "extra_rule_providers") or {}
|
||||
new_rp, invalid_rp = [], []
|
||||
|
||||
for name, rp in rule_providers.items():
|
||||
try:
|
||||
obj = RuleProviderData(meta=Metadata(source=DataSource.MANUAL), name=name, data=rp)
|
||||
new_rp.append(obj.model_dump(by_alias=True, exclude_none=True))
|
||||
except ValidationError:
|
||||
invalid_rp.append(rp)
|
||||
|
||||
data_oper.save(plugin_id, DataKey.RULE_PROVIDERS, new_rp)
|
||||
data_oper.save(plugin_id, "extra_rule_providers", invalid_rp)
|
||||
|
||||
# Upgrade proxies
|
||||
proxies = data_oper.get_data(plugin_id, DataKey.PROXIES) or []
|
||||
new_proxies, invalid_proxies = [], []
|
||||
all_proxies = []
|
||||
names = set()
|
||||
converter = Converter()
|
||||
|
||||
for proxy in proxies:
|
||||
try:
|
||||
raw = None
|
||||
if isinstance(proxy, str):
|
||||
proxy_dict, raw = converter.convert_line(proxy), proxy
|
||||
elif isinstance(proxy, dict):
|
||||
proxy_dict = UtilsProvider.filter_empty(proxy, empty=['', None])
|
||||
else:
|
||||
continue
|
||||
|
||||
obj = Proxy.model_validate(proxy_dict)
|
||||
if obj.name in names: continue
|
||||
|
||||
p_data = ProxyData(data=obj, name=obj.name, meta=Metadata(source=DataSource.MANUAL), raw=raw)
|
||||
new_proxies.append(p_data.model_dump(by_alias=True, exclude_none=True))
|
||||
all_proxies.append(p_data.data)
|
||||
names.add(p_data.name)
|
||||
except Exception:
|
||||
invalid_proxies.append(proxy)
|
||||
|
||||
data_oper.save(plugin_id, DataKey.PROXIES, new_proxies)
|
||||
data_oper.save(plugin_id, "extra_proxies", invalid_proxies)
|
||||
|
||||
# Create proxy patches
|
||||
data_patch = {}
|
||||
overwritten = data_oper.get_data(plugin_id, "overwritten_proxies") or {}
|
||||
for name in overwritten:
|
||||
if proxy := next((p for p in all_proxies if p.name == name), None):
|
||||
src = proxy.model_dump(by_alias=True)
|
||||
# Create a deep copy for dst to avoid modifying src in place if _overwrite_proxy mutates
|
||||
dst = _overwrite_proxy(copy.deepcopy(src), overwritten)
|
||||
if patch := jsonpatch.make_patch(src, dst).to_string():
|
||||
data_patch[name] = PatchItem(patch=patch).model_dump(by_alias=True, exclude_none=True)
|
||||
|
||||
data_oper.save(plugin_id, DataKey.PROXY_PATCH, data_patch)
|
||||
data_oper.save(plugin_id, DataKey.ACL4SSR, [])
|
||||
|
||||
# Upgrade rules
|
||||
for key in [DataKey.TOP_RULES, DataKey.RULESET_RULES]:
|
||||
if rules := data_oper.get_data(plugin_id, key):
|
||||
for rule in rules:
|
||||
rule["meta"] = Metadata(
|
||||
source=rule.get("remark") or DataSource.MANUAL,
|
||||
time_modified=rule.get("time_modified") or time.time()
|
||||
).model_dump()
|
||||
data_oper.save(plugin_id, key, rules)
|
||||
data_oper.save(plugin_id, DataKey.DATA_VERSION, "2.1.0")
|
||||
@@ -1,105 +1,110 @@
|
||||
import copy
|
||||
from dataclasses import dataclass
|
||||
from typing import Callable, Dict, List, Optional, Union, Any, Iterator
|
||||
from typing import Callable, Dict, List, Optional, Any, Iterator, Union
|
||||
|
||||
from ..models.proxy import Proxy, ProxyType
|
||||
from ..models.metadata import Metadata
|
||||
from ..models.types import DataSource
|
||||
from ..models.proxy import Proxy, ProxyData
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProxyItem:
|
||||
proxy: ProxyType
|
||||
remark: str = ""
|
||||
raw: Optional[Union[str, Dict[str, Any]]] = None
|
||||
|
||||
class ProxyManager:
|
||||
"""Proxy Manager"""
|
||||
def __init__(self):
|
||||
self.proxies: Dict[str,ProxyItem] = {}
|
||||
self._proxies: Dict[str, ProxyData] = {}
|
||||
|
||||
def add(self, proxy: ProxyType, remark: str = "", raw: Optional[str|Dict[str, Any]] = None):
|
||||
def add(self, proxy: Proxy, source: DataSource, remark: str = "", raw: str | dict[str, Any] | None = None):
|
||||
"""Add a proxy to the proxy manager. """
|
||||
if proxy.name not in self.proxies:
|
||||
self.proxies[proxy.name] = ProxyItem(proxy, remark, raw=copy.deepcopy(raw))
|
||||
if proxy.name not in self._proxies:
|
||||
meta = Metadata(source=source, remark=remark)
|
||||
self._proxies[proxy.name] = ProxyData(data=proxy, name=proxy.name, meta=meta, raw=copy.deepcopy(raw))
|
||||
else:
|
||||
raise ValueError(f"Proxy with name {proxy.name!r} already exists.")
|
||||
|
||||
def add_proxy_dict(self, proxy_dict: Dict[str, Any], remark: str = "", raw: Optional[str] = None):
|
||||
def update(self, name: str, proxy: Proxy):
|
||||
if name not in self._proxies:
|
||||
raise ValueError(f"Key '{name}' not found")
|
||||
src = self._proxies[name]
|
||||
src.data = proxy
|
||||
|
||||
def add_proxy_data(self, proxy_data: dict[str, Any]):
|
||||
pd = ProxyData.model_validate(proxy_data)
|
||||
if pd.data.name not in self._proxies:
|
||||
self._proxies[pd.data.name] = pd
|
||||
else:
|
||||
raise ValueError(f"Proxy with name {pd.data.name!r} already exists.")
|
||||
|
||||
def add_proxy_dict(self, proxy_dict: Dict[str, Any], source: DataSource, remark: str = "", raw: str | None = None):
|
||||
"""
|
||||
Add a proxy to the proxies list.
|
||||
:param proxy_dict: Proxy dict with proxy name as key
|
||||
:param remark: Proxy remark
|
||||
:param source: Proxy source
|
||||
:param remark: Remark
|
||||
:param raw: Proxy raw
|
||||
:raises: ValueError if proxy name already exists
|
||||
"""
|
||||
proxy = Proxy.model_validate(proxy_dict)
|
||||
raw = raw or proxy_dict
|
||||
self.add(proxy.root, remark=remark, raw=raw)
|
||||
|
||||
def add_from_list(self, proxies: List[Dict[str, Any]], remark: str = "", skip_existing: bool = False):
|
||||
"""Add proxies from the proxies list. """
|
||||
proxies_list = []
|
||||
for proxy in proxies:
|
||||
p = Proxy.model_validate(proxy)
|
||||
proxies_list.append(ProxyItem(p.root, remark, raw=proxy))
|
||||
|
||||
for proxy_item in proxies_list:
|
||||
try:
|
||||
self.add(proxy_item.proxy, remark=remark, raw=proxy_item.raw)
|
||||
except ValueError:
|
||||
if skip_existing:
|
||||
continue
|
||||
raise
|
||||
self.add(proxy, source=source, remark=remark, raw=raw)
|
||||
|
||||
def get_all_proxies(self) -> List[Dict[str, Any]]:
|
||||
proxies = []
|
||||
for proxy_item in self.proxies.values():
|
||||
proxy_dict = proxy_item.proxy.model_dump(by_alias=True, exclude_none=True)
|
||||
for proxy_item in self._proxies.values():
|
||||
proxy_dict = proxy_item.data.model_dump(by_alias=True, exclude_none=True)
|
||||
proxies.append(proxy_dict)
|
||||
return proxies
|
||||
|
||||
def remove_proxy(self, name):
|
||||
if name in self.proxies:
|
||||
del self.proxies[name]
|
||||
if name in self._proxies:
|
||||
del self._proxies[name]
|
||||
|
||||
def remove_proxies_by_condition(self, condition: Callable[[ProxyItem], bool]) -> int:
|
||||
def remove_proxies_by_condition(self, condition: Callable[[ProxyData], bool]) -> int:
|
||||
"""
|
||||
Removes proxies from the manager based on a given condition.
|
||||
:param condition: A callable that takes a ProxyItem and returns True if the proxy should be removed.
|
||||
:param condition: A callable that takes a ProxyData and returns True if the proxy should be removed.
|
||||
:return: The number of proxies removed.
|
||||
"""
|
||||
initial_count = len(self.proxies)
|
||||
self.proxies = {
|
||||
initial_count = len(self._proxies)
|
||||
self._proxies = {
|
||||
name: item
|
||||
for name, item in self.proxies.items()
|
||||
for name, item in self._proxies.items()
|
||||
if not condition(item)
|
||||
}
|
||||
return initial_count - len(self.proxies)
|
||||
return initial_count - len(self._proxies)
|
||||
|
||||
def filter_proxies_by_condition(self, condition: Callable[[ProxyItem], bool]) -> List[ProxyItem]:
|
||||
return [proxy for proxy in self.proxies.values() if condition(proxy)]
|
||||
def filter_proxies_by_condition(self, condition: Callable[[ProxyData], bool]) -> List[ProxyData]:
|
||||
return [proxy for proxy in self._proxies.values() if condition(proxy)]
|
||||
|
||||
def clear(self):
|
||||
self.proxies.clear()
|
||||
self._proxies.clear()
|
||||
|
||||
def export_raw(self, condition: Optional[Callable[[ProxyItem], bool]] = None) -> List[str|Dict[str, Any]]:
|
||||
def export_raw(self, condition: Optional[Callable[[ProxyData], bool]] = None) -> List[Union[str, Dict[str, Any]]]:
|
||||
proxies = []
|
||||
for proxy in self.proxies.values():
|
||||
for proxy in self._proxies.values():
|
||||
if condition and not condition(proxy):
|
||||
continue
|
||||
if proxy.raw:
|
||||
proxies.append(copy.deepcopy(proxy.raw))
|
||||
else:
|
||||
proxies.append(proxy.proxy.model_dump(by_alias=True, exclude_none=True))
|
||||
proxies.append(proxy.data.model_dump(by_alias=True, exclude_none=True))
|
||||
return proxies
|
||||
|
||||
def proxy_names(self) -> Iterator[str]:
|
||||
return iter(self.proxies)
|
||||
return iter(self._proxies)
|
||||
|
||||
def set_proxy_meta(self, name: str, meta: Metadata):
|
||||
if name not in self._proxies:
|
||||
raise ValueError(f"Key '{name}' not found")
|
||||
self._proxies[name].meta = meta
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self.proxies)
|
||||
return len(self._proxies)
|
||||
|
||||
def __iter__(self) -> Iterator[ProxyItem]:
|
||||
return iter(self.proxies.values())
|
||||
def __iter__(self) -> Iterator[ProxyData]:
|
||||
return iter(self._proxies.values())
|
||||
|
||||
def __contains__(self, name: str) -> bool:
|
||||
return name in self.proxies
|
||||
return name in self._proxies
|
||||
|
||||
def __getitem__(self, name: str) -> ProxyData:
|
||||
if name not in self._proxies:
|
||||
raise KeyError(f"Key '{name}' not found")
|
||||
return self._proxies[name]
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
from .proxy import *
|
||||
from .hosts import *
|
||||
from .ruleitem import *
|
||||
from .ruleproviders import *
|
||||
from .proxygroups import *
|
||||
from .proxyproviders import *
|
||||
|
||||
@@ -1,48 +1,71 @@
|
||||
from typing import List, Optional, Union, Literal
|
||||
from typing import List
|
||||
|
||||
from pydantic import BaseModel, Field, ConfigDict
|
||||
from pydantic import BaseModel, Field, RootModel
|
||||
from simpleeval import simple_eval
|
||||
|
||||
from .rule import RoutingRuleType, Action, AdditionalParam
|
||||
from .ruleproviders import RuleProvider
|
||||
|
||||
class RuleData(BaseModel):
|
||||
priority: int
|
||||
type: RoutingRuleType
|
||||
payload: Optional[str] = None
|
||||
action: Union['Action', str]
|
||||
additional_params: Optional[AdditionalParam] = None
|
||||
conditions: Optional[List[str]] = None
|
||||
condition: Optional[str] = None
|
||||
|
||||
model_config = ConfigDict(
|
||||
use_enum_values=True
|
||||
)
|
||||
|
||||
class ClashApi(BaseModel):
|
||||
url: str
|
||||
secret: str
|
||||
|
||||
|
||||
class Connectivity(BaseModel):
|
||||
clash_apis: List[ClashApi] = Field(default_factory=list)
|
||||
sub_links: List[str] = Field(default_factory=list)
|
||||
|
||||
class Subscription(BaseModel):
|
||||
|
||||
class SubscriptionSetting(BaseModel):
|
||||
url: str
|
||||
enabled: bool
|
||||
|
||||
class RuleProviderData(BaseModel):
|
||||
name: str
|
||||
rule_provider: RuleProvider
|
||||
|
||||
class SubscriptionInfo(BaseModel):
|
||||
class DataUsage(BaseModel):
|
||||
upload: int = 0
|
||||
download: int = 0
|
||||
total: int = 0
|
||||
expire: int = 0
|
||||
|
||||
@property
|
||||
def header(self) -> str:
|
||||
return f'upload={self.upload}; download={self.download}; total={self.total}; expire={self.expire};'
|
||||
|
||||
|
||||
class SubscriptionInfo(DataUsage):
|
||||
last_update: int = Field(default=0)
|
||||
proxy_num: int = Field(default=0)
|
||||
enabled: bool = True
|
||||
|
||||
def update(self, setting: SubscriptionSetting):
|
||||
self.enabled = setting.enabled
|
||||
|
||||
|
||||
class SubscriptionsInfo(RootModel[dict[str, SubscriptionInfo]]):
|
||||
root: dict[str, SubscriptionInfo] = Field(default_factory=dict)
|
||||
|
||||
def update(self, urls: list[str]):
|
||||
if not urls:
|
||||
return
|
||||
|
||||
self.root.clear()
|
||||
for url in urls:
|
||||
self.root[url] = self.root.get(url, SubscriptionInfo())
|
||||
|
||||
def get(self, url: str) -> SubscriptionInfo:
|
||||
return self.root.get(url, SubscriptionInfo())
|
||||
|
||||
def __setitem__(self, key: str, value: SubscriptionInfo):
|
||||
self.root[key] = value
|
||||
|
||||
def set(self, setting: SubscriptionSetting):
|
||||
if setting.url in self.root:
|
||||
self.root[setting.url].update(setting)
|
||||
|
||||
|
||||
class ConfigRequest(BaseModel):
|
||||
url: str
|
||||
field: Literal['name', 'enabled']
|
||||
value: Union[bool, str]
|
||||
client_host: str
|
||||
identifier: str | None = None
|
||||
user_agent : str | None = None
|
||||
|
||||
class Host(BaseModel):
|
||||
domain: str
|
||||
value: List[str]
|
||||
using_cloudflare: bool
|
||||
|
||||
class HostData(BaseModel):
|
||||
domain: str
|
||||
value: Optional[Host] = None
|
||||
def resolve(self, expr) -> bool:
|
||||
return bool(simple_eval(expr=expr, names=self.model_dump()))
|
||||
|
||||
226
plugins.v2/clashruleprovider/models/configuration.py
Normal file
226
plugins.v2/clashruleprovider/models/configuration.py
Normal file
@@ -0,0 +1,226 @@
|
||||
from typing import Any, Literal
|
||||
|
||||
from pydantic import BaseModel, Field, model_validator, field_validator, field_serializer, PrivateAttr
|
||||
|
||||
from app.log import logger
|
||||
|
||||
from .proxy import Proxy
|
||||
from .proxygroups import ProxyGroup
|
||||
from .proxyproviders import ProxyProvider
|
||||
from .proxy.tlsmixin import ClientFingerprint
|
||||
from .ruleproviders import RuleProvider
|
||||
from .rule import RuleType, Action, RoutingRuleType
|
||||
from ..helper.clashruleparser import ClashRuleParser
|
||||
|
||||
|
||||
class ExternalControllerCors(BaseModel):
|
||||
allow_origins: list[str] = Field(default_factory=lambda: ["*"], alias="allow-origins")
|
||||
allow_credentials: bool = Field(default=True, alias="allow-credentials")
|
||||
|
||||
|
||||
class Profile(BaseModel):
|
||||
store_selected: bool = Field(default=False, alias="store-selected")
|
||||
store_fake_ip: bool = Field(default=False, alias="store-fake-ip")
|
||||
|
||||
|
||||
class NTP(BaseModel):
|
||||
enable: bool = Field(default=False)
|
||||
Server: str = Field(default="time.apple.com")
|
||||
port: int = Field(default=123)
|
||||
write_to_system: bool = Field(default=False, alias="write-to-system")
|
||||
|
||||
|
||||
class Experimental(BaseModel):
|
||||
quic_go_disable_gso: bool = Field(default=False, alias="quic-go-disable-gso")
|
||||
quic_go_disable_ecn: bool = Field(default=True, alias="quic-go-disable-ecn")
|
||||
dialer_ip4p_convert: bool = Field(default=False, alias="dialer-ip4p-convert")
|
||||
|
||||
|
||||
class ClashConfig(BaseModel):
|
||||
_raw_proxies: dict[str, str] = PrivateAttr(default_factory=dict)
|
||||
|
||||
dns: dict[str, Any] | None = Field(default=None)
|
||||
hosts: dict[str, list[str] | str] | None = Field(default=None)
|
||||
allow_lan: bool | None = Field(default=None, alias="allow-lan")
|
||||
bind_address: str = Field(default="*", alias="bind-address")
|
||||
lan_allowed_ips: list[str] = Field(default_factory=lambda: ["0.0.0.0/0", "::/0"], alias="lan-allowed-ips")
|
||||
lan_disallowed_ips: list[str] = Field(default_factory=list, alias="lan-disallowed-ips")
|
||||
authentication: list[str] = Field(default_factory=list)
|
||||
skip_auth_prefixes: list[str] = Field(default_factory=list, alias="skip-auth-prefixes")
|
||||
mode: Literal["rule", "global", "direct"] = Field(default="rule")
|
||||
log_level: Literal["silent", "error", "warning", "info", "debug"] = Field(default="info", alias="log-level")
|
||||
ipv6: bool = Field(default=True)
|
||||
keep_alive_interval: int = Field(default=0, alias="keep-alive-interval")
|
||||
keep_alive_idle: int = Field(default=0, alias="keep-alive-idle")
|
||||
disable_keep_alive: bool = Field(default=False, alias="disable-keep-alive")
|
||||
find_process_mode: Literal["strict", "always", "off"] = Field(default="strict", alias="find-process-mode")
|
||||
external_controller: str | None = Field(default=None, alias="external-controller")
|
||||
external_controller_cors: ExternalControllerCors = Field(default_factory=ExternalControllerCors,
|
||||
alias="external-controller-cors")
|
||||
external_controller_unix: str | None = Field(default=None, alias="external-controller-unix")
|
||||
external_controller_pipe: str | None = Field(default=None, alias="external-controller-pipe")
|
||||
external_controller_tls: str | None = Field(default=None, alias="external-controller-tls")
|
||||
secret: str | None = Field(default=None)
|
||||
external_ui: str | None = Field(default=None, alias="external-ui")
|
||||
external_ui_name: str | None = Field(default=None, alias="external-ui-name")
|
||||
external_ui_url: str | None = Field(default=None, alias="external-ui-url")
|
||||
profile: Profile = Field(default_factory=Profile)
|
||||
unified_delay: bool = Field(default=True, alias="unified-delay")
|
||||
tcp_concurrent: bool = Field(default=True, alias="tcp-concurrent")
|
||||
interface_name: str | None = Field(default=None, alias="interface-name")
|
||||
routing_mark: int | None = Field(default=None, alias="routing-mark")
|
||||
tls: dict[str, Any] | None = Field(default=None, alias="tls")
|
||||
global_client_fingerprint: ClientFingerprint | None = Field(default=ClientFingerprint.chrome,
|
||||
alias="global-client-fingerprint")
|
||||
geodata_mode: bool | None = Field(default=None, alias="geodata-mode")
|
||||
geodata_loader: Literal["memconservative", "standard"] = Field(default="memconservative", alias="geodata-loader")
|
||||
geo_auto_update: bool = Field(default=False, alias="geo-auto-update")
|
||||
geo_update_interval: int = Field(default=24, alias="geo-update-interval")
|
||||
global_ua: str = Field(default="clash.meta", alias="global-ua")
|
||||
etag_support: bool = Field(default=True, alias="etag-support")
|
||||
sniffer: dict[str, Any] | None = None
|
||||
listeners: list[dict[str, Any]] | None = Field(default=None)
|
||||
port: int = Field(default=0, description="HTTP(S) proxy port")
|
||||
socks_port: int = Field(default=0, alias="socks-port")
|
||||
mixed_port: int = Field(default=0, alias="mixed-port")
|
||||
redir_port: int = Field(default=0, alias="redir-port")
|
||||
tproxy_port: int = Field(default=0, alias="tproxy-port")
|
||||
tun: dict[str, Any] | None = Field(default=None)
|
||||
sub_rules: dict[str, Any] | None = Field(default=None, alias="sub-rules")
|
||||
tunnels: list[dict[str, Any] | str] | None = Field(default=None)
|
||||
ntp: NTP | None = Field(default=None)
|
||||
experimental: Experimental | None = Field(default=None)
|
||||
proxies: list[Proxy] = Field(default_factory=list)
|
||||
proxy_providers: dict[str, ProxyProvider] = Field(default_factory=dict, alias="proxy-providers")
|
||||
proxy_groups: list[ProxyGroup] = Field(default_factory=list, alias="proxy-groups")
|
||||
rules: list[RuleType] = Field(default_factory=list)
|
||||
rule_providers: dict[str, RuleProvider] = Field(default_factory=dict, alias="rule-providers")
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def fill_none_with_default(cls, values: dict):
|
||||
fill_none_fields = {"proxies", "proxy_providers", "proxy_groups", "rules", "rule_providers"}
|
||||
for field_name in fill_none_fields:
|
||||
field = cls.model_fields[field_name]
|
||||
factory = field.default_factory
|
||||
if not factory:
|
||||
continue
|
||||
keys = {field_name}
|
||||
if field.alias:
|
||||
keys.add(field.alias)
|
||||
|
||||
for key in keys:
|
||||
if key in values and values[key] is None:
|
||||
values[key] = factory()
|
||||
return values
|
||||
|
||||
@field_serializer("proxies")
|
||||
def serialize_proxies(self, v: list[Proxy], _info):
|
||||
serialized_proxies = []
|
||||
seen_names = set()
|
||||
for proxy in v:
|
||||
if proxy.name in seen_names:
|
||||
logger.warning(f"Skipping duplicate proxy: {proxy.name}")
|
||||
continue
|
||||
seen_names.add(proxy.name)
|
||||
serialized_proxies.append(proxy.model_dump(by_alias=True, exclude_none=True, mode="json"))
|
||||
return serialized_proxies
|
||||
|
||||
@field_serializer("proxy_groups")
|
||||
def serialize_proxy_groups(self, v: list[ProxyGroup], _info):
|
||||
valid_outbounds = {a.value for a in Action}
|
||||
valid_outbounds.add("GLOBAL")
|
||||
if self.proxies:
|
||||
valid_outbounds.update(p.name for p in self.proxies)
|
||||
if v:
|
||||
valid_outbounds.update(pg.name for pg in v)
|
||||
|
||||
serialized_groups = []
|
||||
seen_names = set()
|
||||
for group in v:
|
||||
if group.name in seen_names:
|
||||
logger.warning(f"Skipping duplicate proxy group: {group.name}")
|
||||
continue
|
||||
seen_names.add(group.name)
|
||||
|
||||
group_data = group.model_dump(by_alias=True, exclude_none=True, mode="json")
|
||||
if "proxies" in group_data and group_data["proxies"]:
|
||||
original_proxies = group_data["proxies"]
|
||||
group_data["proxies"] = [
|
||||
p for p in original_proxies if p in valid_outbounds
|
||||
]
|
||||
removed = set(original_proxies) - set(group_data["proxies"])
|
||||
if removed:
|
||||
logger.warning(f"Proxy group {group.name} removed missing proxies: {removed}")
|
||||
serialized_groups.append(group_data)
|
||||
|
||||
return serialized_groups
|
||||
|
||||
@field_validator("rules", mode="before")
|
||||
@classmethod
|
||||
def validate_rules(cls, v):
|
||||
if isinstance(v, list):
|
||||
rules = []
|
||||
for item in v:
|
||||
if isinstance(item, str):
|
||||
rules.append(ClashRuleParser.parse(item))
|
||||
else:
|
||||
rules.append(item)
|
||||
return rules
|
||||
return v
|
||||
|
||||
@field_serializer("rules")
|
||||
def serialize_rules(self, v: list[RuleType], _info):
|
||||
valid_rules = []
|
||||
valid_outbounds = set(self.outbounds)
|
||||
valid_actions = {a.value for a in Action}
|
||||
|
||||
for rule in v:
|
||||
if rule.rule_type == RoutingRuleType.SUB_RULE:
|
||||
if self.sub_rules and rule.action in self.sub_rules:
|
||||
valid_rules.append(rule)
|
||||
else:
|
||||
logger.warning(f"Skipping rule with missing sub-rule action: {rule}")
|
||||
continue
|
||||
|
||||
if rule.rule_type == RoutingRuleType.RULE_SET:
|
||||
if rule.payload not in self.rule_providers:
|
||||
logger.warning(f"Skipping rule with missing rule-provider: {rule}")
|
||||
continue
|
||||
|
||||
action_str = str(rule.action)
|
||||
if action_str in valid_actions or action_str in valid_outbounds:
|
||||
valid_rules.append(rule)
|
||||
else:
|
||||
logger.warning(f"Skipping rule with invalid outbound: {rule}")
|
||||
|
||||
return [str(rule) for rule in valid_rules]
|
||||
|
||||
@property
|
||||
def outbounds(self) -> list[str]:
|
||||
outbounds = []
|
||||
if self.proxies:
|
||||
outbounds.extend(p.name for p in self.proxies)
|
||||
if self.proxy_groups:
|
||||
outbounds.extend(pg.name for pg in self.proxy_groups)
|
||||
return outbounds
|
||||
|
||||
@property
|
||||
def node_num(self) -> int:
|
||||
return len(self.proxies)
|
||||
|
||||
@property
|
||||
def raw_proxies(self) -> dict[str, str]:
|
||||
return self._raw_proxies
|
||||
|
||||
@raw_proxies.setter
|
||||
def raw_proxies(self, value: dict[str, str]):
|
||||
self._raw_proxies = value
|
||||
|
||||
def merge(self, other: 'ClashConfig') -> 'ClashConfig':
|
||||
self.proxies += other.proxies
|
||||
self.proxy_groups += other.proxy_groups
|
||||
self.rules += other.rules
|
||||
self.rule_providers |= other.rule_providers
|
||||
self.proxy_providers |= other.proxy_providers
|
||||
return self
|
||||
31
plugins.v2/clashruleprovider/models/datamodel.py
Normal file
31
plugins.v2/clashruleprovider/models/datamodel.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from .api import SubscriptionsInfo
|
||||
from .configuration import ClashConfig
|
||||
from .datapatch import DataPatch
|
||||
from .hosts import Hosts
|
||||
from .proxy import Proxies
|
||||
from .proxygroups import ProxyGroups
|
||||
from .ruleproviders import RuleProviders, RuleProvider
|
||||
from .types import DataKey
|
||||
|
||||
|
||||
class GeoRules(BaseModel):
|
||||
geoip: list[str] = Field(default_factory=list)
|
||||
geosite: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class PersistState(BaseModel):
|
||||
proxies: Proxies = Field(alias=DataKey.PROXIES, default_factory=Proxies)
|
||||
proxy_groups: ProxyGroups = Field(alias=DataKey.PROXY_GROUPS, default_factory=ProxyGroups)
|
||||
subscription_info: SubscriptionsInfo = Field(alias=DataKey.SUB_INFO, default_factory=SubscriptionsInfo)
|
||||
rule_provider: dict[str, RuleProvider] = Field(alias=DataKey.AUTO_RULE_PROVIDERS, default_factory=dict)
|
||||
rule_providers: RuleProviders = Field(alias=DataKey.RULE_PROVIDERS, default_factory=RuleProviders)
|
||||
ruleset_names: dict[str, str] = Field(alias=DataKey.RULESET_NAMES, default_factory=dict)
|
||||
acl4ssr_providers: RuleProviders = Field(alias=DataKey.ACL4SSR, default_factory=RuleProviders)
|
||||
sub_configs: dict[str, ClashConfig] = Field(alias=DataKey.SUB_CONFIGS, default_factory=dict)
|
||||
hosts: Hosts = Field(alias=DataKey.HOSTS, default_factory=Hosts)
|
||||
proxy_group_patch: DataPatch = Field(alias=DataKey.PROXY_GROUP_PATCH, default_factory=DataPatch)
|
||||
proxy_patch: DataPatch = Field(alias=DataKey.PROXY_PATCH, default_factory=DataPatch)
|
||||
geo_rules: GeoRules = Field(alias=DataKey.GEO_RULES, default_factory=GeoRules)
|
||||
rule_provider_patch: DataPatch = Field(alias=DataKey.RULE_PROVIDER_PATCH, default_factory=DataPatch)
|
||||
32
plugins.v2/clashruleprovider/models/datapatch.py
Normal file
32
plugins.v2/clashruleprovider/models/datapatch.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from pydantic import BaseModel, Field, RootModel
|
||||
|
||||
|
||||
class PatchItem(BaseModel):
|
||||
lifecycle: int = Field(default=3)
|
||||
patch: str
|
||||
|
||||
|
||||
class DataPatch(RootModel[dict[str, PatchItem]]):
|
||||
"""DataPatch model for storing patch items."""
|
||||
root: dict[str, PatchItem] = Field(default_factory=dict, description="Dictionary of patch items.")
|
||||
|
||||
def update_patch(self, alive_keys: list[str] | set[str], lifespan: int = 3):
|
||||
outdated_keys = []
|
||||
for key in list(self.root.keys()):
|
||||
if key not in alive_keys:
|
||||
self.root[key].lifecycle -= 1
|
||||
if self.root[key].lifecycle == 0:
|
||||
outdated_keys.append(key)
|
||||
else:
|
||||
self.root[key].lifecycle = lifespan
|
||||
for key in outdated_keys:
|
||||
del self.root[key]
|
||||
|
||||
def __setitem__(self, key: str, value: PatchItem):
|
||||
self.root[key] = value
|
||||
|
||||
def __contains__(self, key: str) -> bool:
|
||||
return key in self.root
|
||||
|
||||
def __getitem__(self, key: str) -> PatchItem:
|
||||
return self.root[key]
|
||||
93
plugins.v2/clashruleprovider/models/generics.py
Normal file
93
plugins.v2/clashruleprovider/models/generics.py
Normal file
@@ -0,0 +1,93 @@
|
||||
from typing import TypeVar, Generic, Iterator, Any
|
||||
from pydantic import BaseModel, RootModel, Field, model_validator
|
||||
from .metadata import Metadata
|
||||
|
||||
|
||||
# Specific data payload model
|
||||
T = TypeVar("T")
|
||||
|
||||
class ResourceItem(BaseModel, Generic[T]):
|
||||
"""Generic resource item model"""
|
||||
name: str = Field(..., description="Resource name")
|
||||
data: T = Field(..., description="Resource data payload")
|
||||
meta: Metadata = Field(default_factory=Metadata, description="Resource metadata")
|
||||
|
||||
|
||||
# Subclasses of ResourceItem
|
||||
R = TypeVar("R", bound=ResourceItem)
|
||||
|
||||
class ResourceList(RootModel[list[R]], Generic[R]):
|
||||
"""
|
||||
Generic configuration list base class
|
||||
"""
|
||||
root: list[R] = Field(default_factory=list)
|
||||
|
||||
@model_validator(mode='after')
|
||||
def validate_unique_names(self) -> 'ResourceList[R]':
|
||||
names = [item.name for item in self.root]
|
||||
if len(names) != len(set(names)):
|
||||
raise ValueError("names must be unique")
|
||||
return self
|
||||
|
||||
def __iter__(self) -> Iterator[R]:
|
||||
return iter(self.root)
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self.root)
|
||||
|
||||
def __contains__(self, name: str) -> bool:
|
||||
"""Check if a configuration with the specified name exists"""
|
||||
return any(item.name == name for item in self.root)
|
||||
|
||||
def get(self, name: str) -> R | None:
|
||||
"""Get the configuration item by name"""
|
||||
for item in self.root:
|
||||
if item.name == name:
|
||||
return item
|
||||
return None
|
||||
|
||||
def add(self, item: R):
|
||||
"""Add a configuration item, raise an exception if the name is duplicated"""
|
||||
if item.name in self:
|
||||
raise ValueError(f"name {item.name!r} already exists")
|
||||
self.root.insert(0, item)
|
||||
|
||||
def remove(self, name: str):
|
||||
"""Remove the configuration item by name"""
|
||||
self.root = [item for item in self.root if item.name != name]
|
||||
|
||||
def pop(self, name: str) -> R | None:
|
||||
"""Remove and return the configuration item with the specified name"""
|
||||
for i, item in enumerate(self.root) :
|
||||
if item.name == name:
|
||||
return self.root.pop(i)
|
||||
return None
|
||||
|
||||
def update(self, name: str, item: R):
|
||||
"""Update the configuration item with the specified name"""
|
||||
for i, existing_item in enumerate(self.root):
|
||||
if existing_item.name == name:
|
||||
item.meta = self.root[i].meta
|
||||
self.root[i] = item
|
||||
return
|
||||
|
||||
def update_data(self, name: str, data: Any) -> bool:
|
||||
"""Update only the data payload of the configuration item with the specified name"""
|
||||
item = self.get(name)
|
||||
if item:
|
||||
item.data = data
|
||||
return True
|
||||
return False
|
||||
|
||||
def set_meta(self, name: str, meta: Metadata) -> bool:
|
||||
"""Set metadata for the specified configuration item"""
|
||||
item = self.get(name)
|
||||
if item:
|
||||
item.meta = meta
|
||||
return True
|
||||
return False
|
||||
|
||||
@property
|
||||
def names(self) -> list[str]:
|
||||
"""Return a list of names for all configuration items"""
|
||||
return [item.name for item in self.root]
|
||||
33
plugins.v2/clashruleprovider/models/hosts.py
Normal file
33
plugins.v2/clashruleprovider/models/hosts.py
Normal file
@@ -0,0 +1,33 @@
|
||||
from pydantic import Field, RootModel, BaseModel
|
||||
|
||||
from .metadata import Metadata
|
||||
|
||||
|
||||
class HostData(BaseModel):
|
||||
domain: str
|
||||
value: list[str]
|
||||
using_cloudflare: bool
|
||||
meta: Metadata = Field(default_factory=Metadata)
|
||||
|
||||
|
||||
class Hosts(RootModel[list[HostData]]):
|
||||
root: list[HostData] = Field(default_factory=list)
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self.root)
|
||||
|
||||
def update(self, domain: str, data: HostData):
|
||||
self.root = [host for host in self.root if host.domain != domain]
|
||||
self.root.append(data)
|
||||
|
||||
def delete(self, domain: str):
|
||||
self.root = [host for host in self.root if host.domain != domain]
|
||||
|
||||
def to_dict(self, cloudflare: list[str]) -> dict[str, list[str]]:
|
||||
hosts = {}
|
||||
for host in self.root:
|
||||
if host.using_cloudflare:
|
||||
hosts[host.domain] = cloudflare
|
||||
else:
|
||||
hosts[host.domain] = host.value
|
||||
return hosts
|
||||
25
plugins.v2/clashruleprovider/models/metadata.py
Normal file
25
plugins.v2/clashruleprovider/models/metadata.py
Normal file
@@ -0,0 +1,25 @@
|
||||
import time
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from .api import ConfigRequest
|
||||
from .types import DataSource
|
||||
|
||||
|
||||
class Metadata(BaseModel):
|
||||
"""Metadata model for Clash items"""
|
||||
# source of the item
|
||||
source: DataSource = Field(default=DataSource.MANUAL)
|
||||
# whether the item is disabled
|
||||
disabled: bool = Field(default=False)
|
||||
# roles that cannot see the item
|
||||
invisible_to: list[str] = Field(default_factory=list)
|
||||
# additional remarks
|
||||
remark: str = Field(default="")
|
||||
# last modified time
|
||||
time_modified: float = Field(default_factory=lambda: time.time())
|
||||
# whether the item has been patched
|
||||
patched: bool = Field(default=False)
|
||||
|
||||
def available(self, param: ConfigRequest | None = None) -> bool:
|
||||
return not self.disabled and (param is None or not any(param.resolve(expr) for expr in self.invisible_to))
|
||||
@@ -1,6 +1,7 @@
|
||||
from typing import Union
|
||||
import jsonpatch
|
||||
from typing import Union, Any
|
||||
|
||||
from pydantic import Field, RootModel
|
||||
from pydantic import Field, RootModel, model_validator
|
||||
|
||||
from .anytlsproxy import AnyTLSProxy
|
||||
from .directproxy import DirectProxy
|
||||
@@ -22,6 +23,7 @@ from .tuicproxy import TuicProxy
|
||||
from .vlessproxy import VlessProxy
|
||||
from .vmessproxy import VmessProxy
|
||||
from .wireguardproxy import WireGuardProxy
|
||||
from ..generics import ResourceItem, ResourceList
|
||||
|
||||
ProxyType = Union[
|
||||
AnyTLSProxy,
|
||||
@@ -46,3 +48,31 @@ ProxyType = Union[
|
||||
|
||||
class Proxy(RootModel[ProxyType]):
|
||||
root: ProxyType = Field(..., discriminator="type")
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self.root.name
|
||||
|
||||
def __getattr__(self, item):
|
||||
return getattr(self.root, item)
|
||||
|
||||
def patch(self, patch: str) -> 'Proxy':
|
||||
src = self.model_dump(mode='json', by_alias=True)
|
||||
patched = jsonpatch.apply_patch(src, patch=patch, in_place=True)
|
||||
return Proxy.model_validate(patched)
|
||||
|
||||
|
||||
class ProxyData(ResourceItem[Proxy]):
|
||||
raw: Union[str, dict[str, Any], None] = None
|
||||
v2ray_link: str | None = None
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_name_consistency(self):
|
||||
if self.name != self.data.name:
|
||||
raise ValueError(f"name ({self.name}) must equal data.name ({self.data.name})")
|
||||
return self
|
||||
|
||||
|
||||
class Proxies(ResourceList[ProxyData]):
|
||||
"""Proxies Collection"""
|
||||
pass
|
||||
|
||||
@@ -10,8 +10,8 @@ class Hysteria2Proxy(ProxyBase):
|
||||
password: Optional[str] = None
|
||||
obfs: Optional[Literal['salamander']] = None
|
||||
obfs_password: Optional[str] = Field(None, alias='obfs-password')
|
||||
up: Optional[str] = None
|
||||
down: Optional[str] = None
|
||||
up: Optional[int | str] = None
|
||||
down: Optional[int | str] = None
|
||||
hop_interval: Optional[int] = Field(None, alias='hop-interval')
|
||||
ca: Optional[str] = None
|
||||
ca_str: Optional[str] = Field(None, alias='ca-str')
|
||||
|
||||
@@ -11,10 +11,8 @@ class HysteriaProxy(ProxyBase):
|
||||
auth: Optional[str] = None
|
||||
protocol: Optional[Literal['udp','wechat-video', 'faketcp']] = None
|
||||
up: Optional[str] = None
|
||||
down: Optional[str] = None
|
||||
up_speed: Optional[int] = Field(None, alias='up-speed')
|
||||
down_speed: Optional[int] = Field(None, alias='down-speed')
|
||||
obfs: Optional[str] = None
|
||||
down: Optional[int | str] = None
|
||||
obfs: Optional[int | str] = None
|
||||
obfs_protocol: Optional[str] = Field(None, alias='obfs-protocol')
|
||||
recv_window_conn: Optional[int] = Field(None, alias='recv-window-conn')
|
||||
recv_window: Optional[int] = Field(None, alias='recv-window')
|
||||
|
||||
@@ -1,8 +1,21 @@
|
||||
from typing import List, Optional, Literal
|
||||
from enum import StrEnum
|
||||
from typing import List, Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class ClientFingerprint(StrEnum):
|
||||
chrome = 'chrome'
|
||||
firefox = 'firefox'
|
||||
safari = 'safari'
|
||||
ios = 'ios'
|
||||
android = 'android'
|
||||
edge = 'edge'
|
||||
n360 = '360'
|
||||
qq = 'qq'
|
||||
random = 'random'
|
||||
|
||||
|
||||
class RealityOpts(BaseModel):
|
||||
public_key: str = Field(..., alias='public-key')
|
||||
short_id: Optional[str] = Field(None, alias='short-id')
|
||||
@@ -23,6 +36,6 @@ class TLSMixin(BaseModel):
|
||||
fingerprint: Optional[str] = None
|
||||
alpn: Optional[List[str]] = None
|
||||
skip_cert_verify: Optional[bool] = Field(None, alias='skip-cert-verify')
|
||||
client_fingerprint: Optional[Literal['chrome', 'firefox', 'safari', 'ios', 'android', 'edge', '360', 'qq', 'random']] = Field(None, alias='client-fingerprint')
|
||||
client_fingerprint: Optional[ClientFingerprint] = Field(None, alias='client-fingerprint')
|
||||
reality_opts: Optional[RealityOpts] = Field(None, alias='reality-opts')
|
||||
ech_opts: Optional[EchOpts] = Field(None, alias='ech-opts')
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import jsonpatch
|
||||
import re
|
||||
from typing import List, Optional, Union, Literal
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator, RootModel
|
||||
from pydantic import BaseModel, Field, field_validator, RootModel, model_validator
|
||||
|
||||
from .generics import ResourceItem, ResourceList
|
||||
|
||||
|
||||
class ProxyGroupBase(BaseModel):
|
||||
@@ -12,44 +15,45 @@ class ProxyGroupBase(BaseModel):
|
||||
name: str = Field(..., description="The name of the proxy group.")
|
||||
|
||||
# Proxy and provider references
|
||||
proxies: Optional[List[str]] = Field(None, description="References to outbound proxies or other proxy groups.")
|
||||
use: Optional[List[str]] = Field(None, description="References to proxy provider sets.")
|
||||
proxies: Optional[List[str]] = Field(default=None,
|
||||
description="References to outbound proxies or other proxy groups.")
|
||||
use: Optional[List[str]] = Field(default=None, description="References to proxy provider sets.")
|
||||
|
||||
# Health check fields
|
||||
url: Optional[str] = Field(None, description="Health check test address.")
|
||||
interval: Optional[int] = Field(None, description="Health check interval in seconds.")
|
||||
lazy: Optional[bool] = Field(True, description="If not selected, no health checks are performed.")
|
||||
timeout: Optional[int] = Field(None, description="Health check timeout in milliseconds.")
|
||||
max_failed_times: Optional[int] = Field(5, description="Maximum number of failures before a forced health check.",
|
||||
alias="max-failed-times")
|
||||
expected_status: Optional[str] = Field('*',
|
||||
description="Expected HTTP response status code for health checks.",
|
||||
alias="expected-status")
|
||||
url: Optional[str] = Field(default="https://www.gstatic.com/generate_204", description="Health check test address.")
|
||||
interval: Optional[int] = Field(default=300, description="Health check interval in seconds.")
|
||||
lazy: Optional[bool] = Field(default=True, description="If not selected, no health checks are performed.")
|
||||
timeout: Optional[int] = Field(default=5000, description="Health check timeout in milliseconds.")
|
||||
max_failed_times: Optional[int] = Field(default=5, alias="max-failed-times",
|
||||
description="Maximum number of failures before a forced health check.")
|
||||
expected_status: Optional[str] = Field(default='*', alias="expected-status",
|
||||
description="Expected HTTP response status code for health checks.")
|
||||
|
||||
# Network and routing fields
|
||||
disable_udp: Optional[bool] = Field(False, description="Disables UDP for this proxy group.", alias="disable-udp")
|
||||
interface_name: Optional[str] = Field(None, description="DEPRECATED. Specifies the outbound interface.",
|
||||
disable_udp: Optional[bool] = Field(default=False, description="Disables UDP for this proxy group.",
|
||||
alias="disable-udp")
|
||||
interface_name: Optional[str] = Field(default=None, description="DEPRECATED. Specifies the outbound interface.",
|
||||
alias="interface-name")
|
||||
routing_mark: Optional[int] = Field(None, description="DEPRECATED. The routing mark for outbound connections.",
|
||||
alias="routing-mark")
|
||||
routing_mark: Optional[int] = Field(default=None, alias="routing-mark",
|
||||
description="DEPRECATED. The routing mark for outbound connections.")
|
||||
|
||||
# Dynamic proxy inclusion
|
||||
include_all: Optional[bool] = Field(False, description="Includes all outbound proxies and proxy sets.",
|
||||
include_all: Optional[bool] = Field(default=False, description="Includes all outbound proxies and proxy sets.",
|
||||
alias="include-all")
|
||||
include_all_proxies: Optional[bool] = Field(False, description="Includes all outbound proxies.",
|
||||
include_all_proxies: Optional[bool] = Field(default=False, description="Includes all outbound proxies.",
|
||||
alias="include-all-proxies")
|
||||
include_all_providers: Optional[bool] = Field(False, description="Includes all proxy provider sets.",
|
||||
include_all_providers: Optional[bool] = Field(default=False, description="Includes all proxy provider sets.",
|
||||
alias="include-all-providers")
|
||||
|
||||
# Filtering
|
||||
filter: Optional[str] = Field(None, description="Regex to filter nodes from providers.")
|
||||
exclude_filter: Optional[str] = Field(None, description="Regex to exclude nodes.", alias="exclude-filter")
|
||||
exclude_type: Optional[str] = Field(None, description="Exclude nodes by adapter type, separated by '|'.",
|
||||
filter: Optional[str] = Field(default=None, description="Regex to filter nodes from providers.")
|
||||
exclude_filter: Optional[str] = Field(default=None, description="Regex to exclude nodes.", alias="exclude-filter")
|
||||
exclude_type: Optional[str] = Field(default=None, description="Exclude nodes by adapter type, separated by '|'.",
|
||||
alias="exclude-type")
|
||||
|
||||
# UI fields
|
||||
hidden: Optional[bool] = Field(False, description="Hides the proxy group in the API.")
|
||||
icon: Optional[str] = Field(None, description="Icon string for the proxy group, for UI use.")
|
||||
hidden: Optional[bool] = Field(default=False, description="Hides the proxy group in the API.")
|
||||
icon: Optional[str] = Field(default=None, description="Icon string for the proxy group, for UI use.")
|
||||
|
||||
@field_validator('expected_status')
|
||||
@classmethod
|
||||
@@ -72,44 +76,50 @@ class ProxyGroupBase(BaseModel):
|
||||
|
||||
|
||||
class SelectGroup(ProxyGroupBase):
|
||||
type: Literal['select']
|
||||
type: Literal['select'] = "select"
|
||||
|
||||
|
||||
class RelayGroup(ProxyGroupBase):
|
||||
type: Literal['relay']
|
||||
type: Literal['relay'] = "relay"
|
||||
|
||||
|
||||
class FallbackGroup(ProxyGroupBase):
|
||||
type: Literal['fallback']
|
||||
type: Literal['fallback'] = "fallback"
|
||||
|
||||
|
||||
class UrlTestGroup(ProxyGroupBase):
|
||||
type: Literal['url-test']
|
||||
tolerance: Optional[int] = Field(None, description="proxies switch tolerance, measured in milliseconds (ms).")
|
||||
type: Literal['url-test'] = "url-test"
|
||||
tolerance: Optional[int] = Field(default=None, description="proxies switch tolerance, measured in milliseconds (ms).")
|
||||
|
||||
|
||||
class LoadBalanceGroup(ProxyGroupBase):
|
||||
type: Literal['load-balance']
|
||||
type: Literal['load-balance'] = "load-balance"
|
||||
strategy: Optional[Literal['round-robin', 'consistent-hashing', 'sticky-sessions']] = Field(
|
||||
'round-robin',
|
||||
description="Load balancing strategy."
|
||||
default='round-robin', description="Load balancing strategy."
|
||||
)
|
||||
|
||||
|
||||
class SmartGroup(ProxyGroupBase):
|
||||
type: Literal['smart']
|
||||
uselightgbm: bool = Field(..., description="Use LightGBM model predict weight.")
|
||||
collectdata: bool = Field(..., description="Collect datas for model training.")
|
||||
policy_priority: Optional[str] = Field("1",
|
||||
type: Literal['smart'] = "smart"
|
||||
uselightgbm: bool = Field(default=False, description="Use LightGBM model predict weight.")
|
||||
collectdata: bool = Field(default=False, description="Collect datas for model training.")
|
||||
policy_priority: Optional[str] = Field(default=None,
|
||||
description="<1 means lower priority, >1 means higher priority, "
|
||||
"the default is 1, pattern support regex and string.",
|
||||
alias="policy-priority")
|
||||
strategy: Optional[Literal['round-robin', 'sticky-sessions']] = Field(
|
||||
'sticky-sessions',
|
||||
description="Load balancing strategy."
|
||||
default='sticky-sessions', description="Load balancing strategy."
|
||||
)
|
||||
sample_rate: Optional[int] = Field(1, description="Data acquisition rate.", alias="sample-rate")
|
||||
sample_rate: Optional[int] = Field(default=1, description="Data acquisition rate.", alias="sample-rate")
|
||||
|
||||
@field_validator('policy_priority', mode='before')
|
||||
@classmethod
|
||||
def validate_policy_priority(cls, v):
|
||||
if v is None or v == "":
|
||||
return None
|
||||
if not isinstance(v, str):
|
||||
raise ValueError('policy_priority must be a string')
|
||||
return v
|
||||
|
||||
# Discriminated Union
|
||||
ProxyGroupType = Union[SelectGroup, RelayGroup, FallbackGroup, UrlTestGroup, LoadBalanceGroup, SmartGroup]
|
||||
@@ -117,3 +127,37 @@ ProxyGroupType = Union[SelectGroup, RelayGroup, FallbackGroup, UrlTestGroup, Loa
|
||||
|
||||
class ProxyGroup(RootModel[ProxyGroupType]):
|
||||
root: ProxyGroupType = Field(..., discriminator='type')
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self.root.name
|
||||
|
||||
@property
|
||||
def proxies(self) -> list[str]:
|
||||
if self.root.proxies:
|
||||
return self.root.proxies
|
||||
return []
|
||||
|
||||
def __getattr__(self, item):
|
||||
return getattr(self.root, item)
|
||||
|
||||
def patch(self, patch: str) -> 'ProxyGroup':
|
||||
src = self.model_dump(mode="json", by_alias=True)
|
||||
patched = jsonpatch.apply_patch(src, patch=patch, in_place=True)
|
||||
return ProxyGroup.model_validate(patched)
|
||||
|
||||
|
||||
class ProxyGroupData(ResourceItem[ProxyGroup]):
|
||||
"""Proxy Group Data"""
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_name_consistency(self):
|
||||
data_name = self.data.name
|
||||
if self.name != data_name:
|
||||
raise ValueError(f"name ({self.name}) must equal data.name ({data_name})")
|
||||
return self
|
||||
|
||||
|
||||
class ProxyGroups(ResourceList[ProxyGroupData]):
|
||||
"""Proxy Groups Collection"""
|
||||
pass
|
||||
|
||||
130
plugins.v2/clashruleprovider/models/proxyproviders.py
Normal file
130
plugins.v2/clashruleprovider/models/proxyproviders.py
Normal file
@@ -0,0 +1,130 @@
|
||||
from typing import Any, Optional
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator, ConfigDict
|
||||
|
||||
from .generics import ResourceItem, ResourceList
|
||||
from .types import VehicleType
|
||||
|
||||
|
||||
class OverrideProxyName(BaseModel):
|
||||
"""代理名称覆盖配置"""
|
||||
pattern: str | None = Field(None, description="正则表达式模式")
|
||||
target: str = Field(..., description="替换目标")
|
||||
|
||||
|
||||
class Override(BaseModel):
|
||||
"""代理配置覆盖"""
|
||||
tfo: bool | None = Field(None, description="TCP Fast Open")
|
||||
mptcp: bool | None = Field(None, description="Multipath TCP")
|
||||
udp: bool | None = Field(None, description="UDP支持")
|
||||
udp_over_tcp: bool | None = Field(None, alias="udp-over-tcp", description="UDP over TCP")
|
||||
up: str | None = Field(None, description="上传速度限制")
|
||||
dialer_proxy: str | None = Field(None, alias="dialer-proxy", description="拨号代理")
|
||||
skip_cert_verify: bool | None = Field(None, alias="skip-cert-verify", description="跳过证书验证")
|
||||
interface_name: Optional[str] = Field(None, alias="interface-name", description="网络接口名称")
|
||||
routing_mark: int | None = Field(None, alias="routing-mark", description="路由标记")
|
||||
ip_version: str | None = Field(None, alias="ip-version", description="IP版本偏好")
|
||||
additional_prefix: str | None = Field(None, alias="additional-prefix", description="名称前缀")
|
||||
additional_suffix: str | None = Field(None, alias="additional-suffix", description="名称后缀")
|
||||
proxy_name: list[OverrideProxyName] | None = Field(None, alias="proxy-name", description="代理名称替换规则")
|
||||
|
||||
|
||||
class HealthCheck(BaseModel):
|
||||
"""健康检查配置"""
|
||||
enable: bool = Field(..., description="启用健康检查")
|
||||
url: str = Field(..., description="健康检查URL")
|
||||
interval: int = Field(300, description="检查间隔(秒)")
|
||||
timeout: int | None = Field(None, description="超时时间(毫秒)")
|
||||
lazy: bool = Field(True, description="懒加载模式")
|
||||
expected_status: str | None = Field(None, alias="expected-status", description="期望的HTTP状态码")
|
||||
|
||||
@field_validator('interval')
|
||||
@classmethod
|
||||
def validate_interval(cls, v):
|
||||
if v <= 0:
|
||||
raise ValueError("间隔时间必须大于0")
|
||||
return v
|
||||
|
||||
@field_validator('timeout')
|
||||
@classmethod
|
||||
def validate_timeout(cls, v):
|
||||
if v is not None and v <= 0:
|
||||
raise ValueError("超时时间必须大于0")
|
||||
return v
|
||||
|
||||
|
||||
class ProxyProvider(BaseModel):
|
||||
"""Proxy Provider"""
|
||||
model_config = ConfigDict(
|
||||
str_strip_whitespace=True,
|
||||
validate_assignment=True,
|
||||
)
|
||||
|
||||
type: VehicleType = Field(..., description="Provider类型")
|
||||
path: str | None = Field(default=None, description="本地文件路径")
|
||||
url: str | None = Field(default=None, description="远程URL")
|
||||
proxy: str | None = Field(default=None, description="使用的代理")
|
||||
interval: int | None = Field(default=None, description="更新间隔(秒)")
|
||||
filter: str | None = Field(default=None, description="过滤正则表达式")
|
||||
exclude_filter: str | None = Field(default=None, alias="exclude-filter", description="排除过滤正则表达式")
|
||||
exclude_type: str | None = Field(default=None, alias="exclude-type", description="排除的代理类型")
|
||||
dialer_proxy: str | None = Field(default=None, alias="dialer-proxy", description="拨号代理")
|
||||
size_limit: int | None = Field(default=None, alias="size-limit", description="文件大小限制(字节)")
|
||||
payload: list[dict[str, Any]] | None = Field(default=None, description="内联代理配置")
|
||||
health_check: HealthCheck | None = Field(default=None, alias="health-check", description="健康检查配置")
|
||||
override: Override | None = Field(default=None, description="配置覆盖")
|
||||
header: dict[str, list[str]] | None = Field(default=None, description="HTTP请求头")
|
||||
|
||||
@field_validator('interval')
|
||||
@classmethod
|
||||
def validate_interval(cls, v):
|
||||
if v is not None and v <= 0:
|
||||
raise ValueError("间隔时间必须大于0")
|
||||
return v
|
||||
|
||||
@field_validator('size_limit')
|
||||
@classmethod
|
||||
def validate_size_limit(cls, v):
|
||||
if v is not None and v < 0:
|
||||
raise ValueError("文件大小限制不能为负数")
|
||||
return v
|
||||
|
||||
@field_validator('exclude_type')
|
||||
@classmethod
|
||||
def validate_exclude_type(cls, v):
|
||||
if v is not None:
|
||||
types = [t.strip() for t in v.split('|')]
|
||||
if not all(types):
|
||||
raise ValueError("排除类型不能为空")
|
||||
return v
|
||||
|
||||
@field_validator('url')
|
||||
@classmethod
|
||||
def validate_url_dependency(cls, v, info):
|
||||
if info.data.get('type') == VehicleType.HTTP and not v:
|
||||
raise ValueError("HTTP类型的provider必须提供URL")
|
||||
return v
|
||||
|
||||
@field_validator('path')
|
||||
@classmethod
|
||||
def validate_path_dependency(cls, v, info):
|
||||
if info.data.get('type') == VehicleType.FILE and not v:
|
||||
raise ValueError("FILE类型的provider必须提供路径")
|
||||
return v
|
||||
|
||||
@field_validator('payload')
|
||||
@classmethod
|
||||
def validate_payload_dependency(cls, v, info):
|
||||
if info.data.get('type') == VehicleType.INLINE and not v:
|
||||
raise ValueError("INLINE类型的provider必须提供payload")
|
||||
return v
|
||||
|
||||
|
||||
class ProxyProviderData(ResourceItem[ProxyProvider]):
|
||||
"""Proxy Provider Data"""
|
||||
pass
|
||||
|
||||
|
||||
class ProxyProviders(ResourceList[ProxyProviderData]):
|
||||
"""Proxy Provider Collection"""
|
||||
pass
|
||||
@@ -1,4 +1,4 @@
|
||||
from enum import Enum
|
||||
from enum import Enum, StrEnum
|
||||
from typing import Any, List, Optional, Union, Dict, Literal
|
||||
|
||||
from pydantic import BaseModel, field_validator, ValidationInfo
|
||||
@@ -57,7 +57,7 @@ class RoutingRuleType(Enum):
|
||||
MATCH = "MATCH"
|
||||
|
||||
|
||||
class Action(Enum):
|
||||
class Action(StrEnum):
|
||||
"""Enumeration of rule actions"""
|
||||
DIRECT = "DIRECT"
|
||||
REJECT = "REJECT"
|
||||
@@ -65,9 +65,6 @@ class Action(Enum):
|
||||
PASS = "PASS"
|
||||
COMPATIBLE = "COMPATIBLE"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.value
|
||||
|
||||
|
||||
class RuleBase(BaseModel):
|
||||
rule_type: RoutingRuleType
|
||||
@@ -101,7 +98,7 @@ class ClashRule(RuleBase):
|
||||
'payload': self.payload,
|
||||
'action': self.action.value if isinstance(self.action, Action) else self.action,
|
||||
'additional_params': self.additional_params.value if self.additional_params else None,
|
||||
'raw': self.raw_rule
|
||||
'rule_string': str(self)
|
||||
}
|
||||
|
||||
def __str__(self) -> str:
|
||||
@@ -131,7 +128,7 @@ class LogicRule(RuleBase):
|
||||
return f"{self.rule_type.value},({conditions_str})"
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
conditions = []
|
||||
conditions: list[str] = []
|
||||
for condition in self.conditions:
|
||||
conditions.append(condition.condition_string())
|
||||
|
||||
@@ -139,7 +136,7 @@ class LogicRule(RuleBase):
|
||||
'type': self.rule_type.value,
|
||||
'conditions': conditions,
|
||||
'action': self.action.value if isinstance(self.action, Action) else self.action,
|
||||
'raw': self.raw_rule
|
||||
'rule_string': str(self)
|
||||
}
|
||||
|
||||
@field_validator('conditions', mode='after')
|
||||
@@ -166,7 +163,7 @@ class SubRule(RuleBase):
|
||||
'type': self.rule_type.value,
|
||||
'condition': f"({self.condition.condition_string()})",
|
||||
'action': self.action,
|
||||
'raw': self.raw_rule
|
||||
'rule_string': str(self)
|
||||
}
|
||||
|
||||
def __str__(self) -> str:
|
||||
@@ -185,7 +182,7 @@ class MatchRule(RuleBase):
|
||||
return {
|
||||
'type': 'MATCH',
|
||||
'action': self.action.value if isinstance(self.action, Action) else self.action,
|
||||
'raw': self.raw_rule
|
||||
'rule_string': str(self)
|
||||
}
|
||||
|
||||
def __str__(self) -> str:
|
||||
|
||||
40
plugins.v2/clashruleprovider/models/ruleitem.py
Normal file
40
plugins.v2/clashruleprovider/models/ruleitem.py
Normal file
@@ -0,0 +1,40 @@
|
||||
from pydantic import BaseModel, Field, field_validator, field_serializer
|
||||
|
||||
from .metadata import Metadata
|
||||
from .rule import RuleType
|
||||
from .rule import RoutingRuleType, Action, AdditionalParam
|
||||
from ..helper.clashruleparser import ClashRuleParser
|
||||
|
||||
|
||||
class RuleItem(BaseModel):
|
||||
"""Clash rule item"""
|
||||
rule: RuleType
|
||||
meta: Metadata = Field(default_factory=Metadata)
|
||||
|
||||
@field_serializer("rule")
|
||||
def serialize_rule(self, v: RuleType, _info):
|
||||
return str(v)
|
||||
|
||||
@field_validator("rule", mode="before")
|
||||
@classmethod
|
||||
def validate_rule(cls, v):
|
||||
if isinstance(v, str):
|
||||
return ClashRuleParser.parse(v)
|
||||
return v
|
||||
|
||||
|
||||
class RuleData(BaseModel):
|
||||
priority: int
|
||||
rule_string: str
|
||||
type: RoutingRuleType
|
||||
payload: str | None = None
|
||||
action: Action | str
|
||||
additional_params: AdditionalParam | None = None
|
||||
conditions: list[str] | None = None
|
||||
condition: str | None = None
|
||||
meta: Metadata = Field(default_factory=Metadata)
|
||||
|
||||
@classmethod
|
||||
def from_rule_item(cls, item: RuleItem, priority: int) -> 'RuleData':
|
||||
fields = item.rule.to_dict()
|
||||
return cls(priority=priority, meta=item.meta, **fields)
|
||||
@@ -1,20 +1,29 @@
|
||||
from typing import List, Optional, Literal, Dict
|
||||
from typing import List, Optional, Literal
|
||||
|
||||
from pydantic import BaseModel, Field, model_validator, HttpUrl, RootModel
|
||||
from pydantic import BaseModel, ConfigDict, Field, model_validator, HttpUrl
|
||||
|
||||
from .generics import ResourceItem, ResourceList
|
||||
from .types import VehicleType
|
||||
|
||||
|
||||
class RuleProvider(BaseModel):
|
||||
type: Literal["http", "file", "inline"] = Field(..., description="Provider type")
|
||||
url: Optional[HttpUrl] = Field(None, description="Must be configured if the type is http")
|
||||
path: Optional[str] = Field(None, description="Optional, file path, must be unique.")
|
||||
interval: Optional[int] = Field(None, ge=0, description="The update interval for the provider, in seconds.")
|
||||
proxy: Optional[str] = Field(None, description="Download/update through the specified proxy.")
|
||||
"""Rule Provider"""
|
||||
model_config = ConfigDict(
|
||||
str_strip_whitespace=True,
|
||||
validate_assignment=True,
|
||||
)
|
||||
|
||||
type: VehicleType = Field(..., description="Provider type")
|
||||
url: Optional[HttpUrl] = Field(default=None, description="Must be configured if the type is http")
|
||||
path: Optional[str] = Field(default=None, description="Optional, file path, must be unique.")
|
||||
interval: Optional[int] = Field(default=None, ge=0, description="The update interval for the provider, in seconds.")
|
||||
proxy: Optional[str] = Field(default=None, description="Download/update through the specified proxy.")
|
||||
behavior: Optional[Literal["domain", "ipcidr", "classical"]] = Field(None,
|
||||
description="Behavior of the rule provider")
|
||||
format: Literal["yaml", "text", "mrs"] = Field("yaml", description="Format of the rule provider file")
|
||||
size_limit: int = Field(0, ge=0, description="The maximum size of downloadable files in bytes (0 for no limit)",
|
||||
alias="size-limit")
|
||||
payload: Optional[List[str]] = Field(None, description="Content, only effective when type is inline")
|
||||
size_limit: int = Field(default=0, ge=0, alias="size-limit",
|
||||
description="The maximum size of downloadable files in bytes (0 for no limit)")
|
||||
payload: Optional[List[str]] = Field(default=None, description="Content, only effective when type is inline")
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
@@ -55,5 +64,11 @@ class RuleProvider(BaseModel):
|
||||
return values
|
||||
|
||||
|
||||
class RuleProviders(RootModel[Dict[str, RuleProvider]]):
|
||||
root: Dict[str, RuleProvider]
|
||||
class RuleProviderData(ResourceItem[RuleProvider]):
|
||||
"""Rule Provider Data"""
|
||||
pass
|
||||
|
||||
|
||||
class RuleProviders(ResourceList[RuleProviderData]):
|
||||
"""Rule Providers Collection"""
|
||||
pass
|
||||
|
||||
57
plugins.v2/clashruleprovider/models/types.py
Normal file
57
plugins.v2/clashruleprovider/models/types.py
Normal file
@@ -0,0 +1,57 @@
|
||||
from enum import StrEnum
|
||||
from typing import TypeVar, Protocol
|
||||
|
||||
|
||||
class DataSource(StrEnum):
|
||||
MANUAL = "Manual"
|
||||
ACL4SSR = "Acl4SSR"
|
||||
TEMPLATE = "Template"
|
||||
SUB = "Subscription"
|
||||
AUTO = "Auto"
|
||||
|
||||
|
||||
class VehicleType(StrEnum):
|
||||
FILE = "file"
|
||||
HTTP = "http"
|
||||
INLINE = "inline"
|
||||
|
||||
|
||||
class DataKey(StrEnum):
|
||||
"""Plugin data key"""
|
||||
PROXY_PATCH = "proxy_patch"
|
||||
PROXY_GROUPS = "proxy-groups"
|
||||
PROXIES = "proxies"
|
||||
INVALID_PROXIES = "extra_proxies"
|
||||
SUB_INFO = "subscription_info"
|
||||
HOSTS = "hosts"
|
||||
ACL4SSR = "acl4ssr_providers"
|
||||
RULE_PROVIDERS = "rule-providers"
|
||||
DATA_VERSION = "data_version"
|
||||
SUB_CONFIGS = "clash_configs"
|
||||
PROXY_GROUP_PATCH = "proxy_group_patch"
|
||||
RULESET_NAMES = "ruleset_names"
|
||||
AUTO_RULE_PROVIDERS = "rule_provider"
|
||||
GEO_RULES = "geo_rules"
|
||||
TOP_RULES = "top_rules"
|
||||
RULESET_RULES = "ruleset_rules"
|
||||
RULE_PROVIDER_PATCH = "rule_provider_patch"
|
||||
RAW_PROXIES = "raw_proxies"
|
||||
|
||||
|
||||
class RuleSet(StrEnum):
|
||||
TOP = "top"
|
||||
RULESET = "ruleset"
|
||||
|
||||
|
||||
class ClashKey(StrEnum):
|
||||
PROXIES = "proxies"
|
||||
PROXY_GROUPS = "proxy-groups"
|
||||
NAME = "name"
|
||||
RULES = "rules"
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
class SupportsPatch(Protocol[T]):
|
||||
def patch(self, patch: str) -> T:
|
||||
...
|
||||
@@ -1,3 +1,5 @@
|
||||
websockets
|
||||
sse_starlette~=2.3.6
|
||||
PyYAML~=6.0.2
|
||||
sse_starlette~=3.1.1
|
||||
PyYAML~=6.0.2
|
||||
jsonpatch~=1.33
|
||||
simpleeval~=1.0.3
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,33 +1,283 @@
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Dict, List
|
||||
from itertools import chain
|
||||
from typing import Any, Generator, Callable
|
||||
|
||||
from pydantic import TypeAdapter
|
||||
|
||||
from app.core.cache import Cache
|
||||
from app.db.plugindata_oper import PluginDataOper
|
||||
|
||||
from .config import PluginConfig
|
||||
from .helper.clashrulemanager import ClashRuleManager
|
||||
from .helper.proxiesmanager import ProxyManager
|
||||
from .helper.utilsprovider import UtilsProvider
|
||||
from .models import RuleProviderData, ProxyProviderData, ProxyGroupData, Hosts, ProxyGroups, RuleProviders, \
|
||||
RuleProvider, Metadata, Proxies, ProxyData
|
||||
from .models.configuration import ClashConfig
|
||||
from .models.types import DataSource, RuleSet, DataKey
|
||||
from .models.datapatch import DataPatch
|
||||
from .models.api import SubscriptionsInfo
|
||||
from .models.datamodel import GeoRules, PersistState
|
||||
|
||||
|
||||
@dataclass
|
||||
class PluginState:
|
||||
"""
|
||||
A dataclass to hold all the runtime state of the ClashRuleProvider plugin.
|
||||
A DAL to manage the runtime state of ClashRuleProvider.
|
||||
"""
|
||||
# Rule and Proxy Managers
|
||||
top_rules_manager: ClashRuleManager = field(default_factory=ClashRuleManager)
|
||||
ruleset_rules_manager: ClashRuleManager = field(default_factory=ClashRuleManager)
|
||||
proxies_manager: ProxyManager = field(default_factory=ProxyManager)
|
||||
def __init__(self, plugin_id: str, config: PluginConfig = None):
|
||||
self.plugin_id = plugin_id
|
||||
self.config = config or PluginConfig()
|
||||
self.plugin_data = PluginDataOper()
|
||||
self.cache = Cache(maxsize=256, ttl=self.config.cache_ttl)
|
||||
self.cache_region = f"app.plugins.{self.plugin_id.lower()}"
|
||||
|
||||
# Loaded from saved data
|
||||
proxy_groups: List[Dict[str, Any]] = field(default_factory=list)
|
||||
extra_proxies: List[Dict[str, Any]] = field(default_factory=list)
|
||||
subscription_info: Dict[str, Any] = field(default_factory=dict)
|
||||
rule_provider: Dict[str, Any] = field(default_factory=dict)
|
||||
rule_providers: Dict[str, Any] = field(default_factory=dict)
|
||||
ruleset_names: Dict[str, str] = field(default_factory=dict)
|
||||
acl4ssr_providers: Dict[str, Any] = field(default_factory=dict)
|
||||
clash_configs: Dict[str, Any] = field(default_factory=dict)
|
||||
hosts: List[Dict[str, Any]] = field(default_factory=list)
|
||||
overwritten_region_groups: Dict[str, Any] = field(default_factory=dict)
|
||||
overwritten_proxies: Dict[str, Any] = field(default_factory=dict)
|
||||
clash_template_dict: Dict[str, Any] = field(default_factory=dict)
|
||||
# Build schemas from PersistState model
|
||||
self._schemas: dict[str, tuple[TypeAdapter, Callable[[], Any]]] = {}
|
||||
for _, field in PersistState.model_fields.items():
|
||||
alias = field.alias
|
||||
if alias:
|
||||
self._schemas[alias] = (TypeAdapter(field.annotation), field.default_factory)
|
||||
|
||||
# Volatile state (generated at runtime)
|
||||
geo_rules: Dict[str, List[str]] = field(default_factory=lambda: {'geoip': [], 'geosite': []})
|
||||
# Rule and Proxy Managers (Runtime)
|
||||
self.top_rules_manager: ClashRuleManager = ClashRuleManager()
|
||||
self.ruleset_rules_manager: ClashRuleManager = ClashRuleManager()
|
||||
|
||||
# Runtime variables (not persisted directly or persisted via config)
|
||||
self.clash_template: ClashConfig = ClashConfig()
|
||||
|
||||
def _get_val(self, key: str) -> Any:
|
||||
# Check cache
|
||||
if self.cache.exists(key, region=self.cache_region):
|
||||
return self.cache.get(key, region=self.cache_region)
|
||||
|
||||
data = self.plugin_data.get_data(self.plugin_id, key)
|
||||
adapter, default_factory = self._schemas.get(key, (None, None))
|
||||
|
||||
if data is None:
|
||||
if default_factory:
|
||||
val = default_factory()
|
||||
self.cache.set(key, val, region=self.cache_region)
|
||||
return val
|
||||
return None
|
||||
|
||||
if adapter:
|
||||
val = adapter.validate_python(data)
|
||||
else:
|
||||
val = data
|
||||
|
||||
self.cache.set(key, val, region=self.cache_region)
|
||||
return val
|
||||
|
||||
def _set_val(self, key: str, value: Any):
|
||||
adapter, _ = self._schemas.get(key, (None, None))
|
||||
if adapter:
|
||||
data = adapter.dump_python(value, mode="json", by_alias=True, exclude_none=True)
|
||||
else:
|
||||
data = value
|
||||
self.plugin_data.save(self.plugin_id, key, data)
|
||||
self.cache.set(key, value, region=self.cache_region)
|
||||
|
||||
@property
|
||||
def proxies(self) -> Proxies:
|
||||
return self._get_val(DataKey.PROXIES)
|
||||
|
||||
@proxies.setter
|
||||
def proxies(self, value: Proxies):
|
||||
self._set_val(DataKey.PROXIES, value)
|
||||
|
||||
@property
|
||||
def proxy_groups(self) -> ProxyGroups:
|
||||
return self._get_val(DataKey.PROXY_GROUPS)
|
||||
|
||||
@proxy_groups.setter
|
||||
def proxy_groups(self, value: ProxyGroups):
|
||||
self._set_val(DataKey.PROXY_GROUPS, value)
|
||||
|
||||
@property
|
||||
def subscription_info(self) -> SubscriptionsInfo:
|
||||
return self._get_val(DataKey.SUB_INFO)
|
||||
|
||||
@subscription_info.setter
|
||||
def subscription_info(self, value: SubscriptionsInfo):
|
||||
self._set_val(DataKey.SUB_INFO, value)
|
||||
|
||||
@property
|
||||
def rule_provider(self) -> dict[str, RuleProvider]:
|
||||
return self._get_val(DataKey.AUTO_RULE_PROVIDERS)
|
||||
|
||||
@rule_provider.setter
|
||||
def rule_provider(self, value: dict[str, RuleProvider]):
|
||||
self._set_val(DataKey.AUTO_RULE_PROVIDERS, value)
|
||||
|
||||
@property
|
||||
def rule_providers(self) -> RuleProviders:
|
||||
return self._get_val(DataKey.RULE_PROVIDERS)
|
||||
|
||||
@rule_providers.setter
|
||||
def rule_providers(self, value: RuleProviders):
|
||||
self._set_val(DataKey.RULE_PROVIDERS, value)
|
||||
|
||||
@property
|
||||
def ruleset_names(self) -> dict[str, str]:
|
||||
return self._get_val(DataKey.RULESET_NAMES)
|
||||
|
||||
@ruleset_names.setter
|
||||
def ruleset_names(self, value: dict[str, str]):
|
||||
self._set_val(DataKey.RULESET_NAMES, value)
|
||||
|
||||
@property
|
||||
def acl4ssr_providers(self) -> RuleProviders:
|
||||
return self._get_val(DataKey.ACL4SSR)
|
||||
|
||||
@acl4ssr_providers.setter
|
||||
def acl4ssr_providers(self, value: RuleProviders):
|
||||
self._set_val(DataKey.ACL4SSR, value)
|
||||
|
||||
@property
|
||||
def sub_configs(self) -> dict[str, ClashConfig]:
|
||||
sub_conf = self._get_val(DataKey.SUB_CONFIGS)
|
||||
return sub_conf
|
||||
|
||||
@sub_configs.setter
|
||||
def sub_configs(self, value: dict[str, ClashConfig]):
|
||||
self._set_val(DataKey.SUB_CONFIGS, value)
|
||||
|
||||
@property
|
||||
def hosts(self) -> Hosts:
|
||||
return self._get_val(DataKey.HOSTS)
|
||||
|
||||
@hosts.setter
|
||||
def hosts(self, value: Hosts):
|
||||
self._set_val(DataKey.HOSTS, value)
|
||||
|
||||
@property
|
||||
def proxy_group_patch(self) -> DataPatch:
|
||||
return self._get_val(DataKey.PROXY_GROUP_PATCH)
|
||||
|
||||
@proxy_group_patch.setter
|
||||
def proxy_group_patch(self, value: DataPatch):
|
||||
self._set_val(DataKey.PROXY_GROUP_PATCH, value)
|
||||
|
||||
@property
|
||||
def proxy_patch(self) -> DataPatch:
|
||||
return self._get_val(DataKey.PROXY_PATCH)
|
||||
|
||||
@proxy_patch.setter
|
||||
def proxy_patch(self, value: DataPatch):
|
||||
self._set_val(DataKey.PROXY_PATCH, value)
|
||||
|
||||
@property
|
||||
def rule_provider_patch(self) -> DataPatch:
|
||||
return self._get_val(DataKey.RULE_PROVIDER_PATCH)
|
||||
|
||||
@rule_provider_patch.setter
|
||||
def rule_provider_patch(self, value: DataPatch):
|
||||
self._set_val(DataKey.RULE_PROVIDER_PATCH, value)
|
||||
|
||||
@property
|
||||
def geo_rules(self) -> GeoRules:
|
||||
return self._get_val(DataKey.GEO_RULES)
|
||||
|
||||
@geo_rules.setter
|
||||
def geo_rules(self, value: GeoRules):
|
||||
self._set_val(DataKey.GEO_RULES, value)
|
||||
|
||||
def get_data(self, key: str) -> Any:
|
||||
return self.plugin_data.get_data(self.plugin_id, key)
|
||||
|
||||
def save_data(self, key: str, value: Any):
|
||||
self.plugin_data.save(self.plugin_id, key, value)
|
||||
|
||||
def get_rule_manager(self, ruleset: RuleSet) -> ClashRuleManager:
|
||||
if ruleset == RuleSet.RULESET:
|
||||
return self.ruleset_rules_manager
|
||||
return self.top_rules_manager
|
||||
|
||||
def get_sub_config(self, url: str) -> ClashConfig:
|
||||
conf = self.sub_configs.get(url)
|
||||
if conf is None:
|
||||
return ClashConfig()
|
||||
ret = ClashConfig()
|
||||
sub_options = self.config.get_sub_conf(url)
|
||||
for field_name in sub_options.model_fields.keys():
|
||||
if getattr(sub_options, field_name) is True and field_name in ret.model_fields:
|
||||
setattr(ret, field_name, getattr(conf, field_name))
|
||||
return ret
|
||||
|
||||
def set_rule_providers(self, rule_providers: dict[str, dict[str, Any]]):
|
||||
self.rule_provider.clear()
|
||||
for name, rp in rule_providers.items():
|
||||
self.rule_providers[name] = RuleProvider(**rp)
|
||||
|
||||
def rule_providers_from_subs(self) -> Generator[RuleProviderData, None, None]:
|
||||
for url, conf in self.sub_configs.items():
|
||||
if self.config.get_sub_conf(url).rule_providers:
|
||||
for name, rp in conf.rule_providers.items():
|
||||
meta = Metadata(source=DataSource.SUB, remark=UtilsProvider.get_url_domain(url))
|
||||
yield RuleProviderData(name=name, data=rp, meta=meta)
|
||||
|
||||
def rule_providers_from_template(self) -> Generator[RuleProviderData, None, None]:
|
||||
for name, rp in self.clash_template.rule_providers.items():
|
||||
yield RuleProviderData(meta=Metadata(source=DataSource.TEMPLATE), name=name, data=rp)
|
||||
|
||||
def proxy_providers_from_subs(self) -> Generator[ProxyProviderData, None, None]:
|
||||
for url, conf in self.sub_configs.items():
|
||||
if self.config.get_sub_conf(url).proxy_providers:
|
||||
for name, pp in conf.proxy_providers.items():
|
||||
meta = Metadata(source=DataSource.SUB, remark=UtilsProvider.get_url_domain(url))
|
||||
yield ProxyProviderData(meta=meta, name=name, data=pp)
|
||||
|
||||
def proxy_providers_from_template(self) -> Generator[ProxyProviderData, None, None]:
|
||||
for name, pp in self.clash_template.proxy_providers.items():
|
||||
yield ProxyProviderData(meta=Metadata(source=DataSource.TEMPLATE), name=name, data=pp)
|
||||
|
||||
def proxy_groups_from_subs(self) -> Generator[ProxyGroupData, None, None]:
|
||||
for url, conf in self.sub_configs.items():
|
||||
if self.config.get_sub_conf(url).proxy_groups:
|
||||
for pg in conf.proxy_groups:
|
||||
meta = Metadata(source=DataSource.SUB, remark=UtilsProvider.get_url_domain(url))
|
||||
yield ProxyGroupData(meta=meta, data=pg, name=pg.name)
|
||||
|
||||
def proxy_groups_from_template(self) -> Generator[ProxyGroupData, None, None]:
|
||||
for pg in self.clash_template.proxy_groups:
|
||||
yield ProxyGroupData(meta=Metadata(source=DataSource.TEMPLATE), data=pg, name=pg.name)
|
||||
|
||||
def proxies_from_subs(self) -> Generator[ProxyData, None, None]:
|
||||
for url, conf in self.sub_configs.items():
|
||||
for p in conf.proxies:
|
||||
meta = Metadata(source=DataSource.SUB, remark=UtilsProvider.get_url_domain(url))
|
||||
yield ProxyData(meta=meta, data=p, name=p.name, v2ray_link=conf.raw_proxies.get(p.name))
|
||||
|
||||
def proxies_from_template(self) -> Generator[ProxyData, None, None]:
|
||||
for p in self.clash_template.proxies:
|
||||
yield ProxyData(meta=Metadata(source=DataSource.TEMPLATE), data=p, name=p.name)
|
||||
|
||||
@property
|
||||
def all_rule_providers(self) -> list[RuleProviderData]:
|
||||
return list(chain(
|
||||
self.rule_providers,
|
||||
self.rule_providers_from_template(),
|
||||
self.rule_providers_from_subs(),
|
||||
self.acl4ssr_providers
|
||||
))
|
||||
|
||||
@property
|
||||
def all_proxy_providers(self) -> list[ProxyProviderData]:
|
||||
return list(chain(
|
||||
self.proxy_providers_from_subs(),
|
||||
self.proxy_providers_from_template()
|
||||
))
|
||||
|
||||
@property
|
||||
def all_proxy_groups(self) -> list[ProxyGroupData]:
|
||||
return list(chain(
|
||||
self.proxy_groups,
|
||||
self.proxy_groups_from_subs(),
|
||||
self.proxy_groups_from_template()
|
||||
))
|
||||
|
||||
@property
|
||||
def all_proxies(self) -> list[ProxyData]:
|
||||
return list(chain(
|
||||
self.proxies,
|
||||
self.proxies_from_subs(),
|
||||
self.proxies_from_template()
|
||||
))
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
from typing import Any, Optional
|
||||
|
||||
from app.db.plugindata_oper import PluginDataOper
|
||||
|
||||
|
||||
class PluginStore:
|
||||
"""数据持久化"""
|
||||
def __init__(self, plugin_id: str):
|
||||
self.plugin_id = plugin_id
|
||||
self.plugin_data = PluginDataOper()
|
||||
|
||||
def get_data(self, key: Optional[str] = None) -> Any:
|
||||
return self.plugin_data.get_data(self.plugin_id, key)
|
||||
|
||||
def save_data(self, key: str, value: Any):
|
||||
self.plugin_data.save(self.plugin_id, key, value)
|
||||
|
||||
def del_data(self, key: str) -> Any:
|
||||
self.plugin_data.del_data(self.plugin_id, key)
|
||||
Reference in New Issue
Block a user