Update 2.0.1
Before Width: | Height: | Size: 222 KiB |
Before Width: | Height: | Size: 106 KiB |
Before Width: | Height: | Size: 76 KiB |
Before Width: | Height: | Size: 151 KiB |
Before Width: | Height: | Size: 691 KiB |
After Width: | Height: | Size: 2.3 MiB |
After Width: | Height: | Size: 1.5 MiB |
After Width: | Height: | Size: 1.2 MiB |
@ -165,15 +165,11 @@ neko=123456
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## 问题Q&A
|
## 问题Q&A
|
||||||
|
|
||||||
|
@ -1,3 +1,9 @@
|
|||||||
|
### 2025/03/03
|
||||||
|
|
||||||
|
- 美化 Web管理界面
|
||||||
|
- 修复大量 BUG
|
||||||
|
- 添加更多处理小脚本辅助使用
|
||||||
|
|
||||||
### 2025/02/21
|
### 2025/02/21
|
||||||
|
|
||||||
- 增加 Web 管理界面
|
- 增加 Web 管理界面
|
||||||
|
235
ProxyCat.py
@ -11,13 +11,12 @@ from configparser import ConfigParser
|
|||||||
|
|
||||||
init(autoreset=True)
|
init(autoreset=True)
|
||||||
|
|
||||||
log_format = '%(asctime)s - %(levelname)s - %(message)s'
|
def setup_logging():
|
||||||
formatter = ColoredFormatter(log_format)
|
log_format = '%(asctime)s - %(levelname)s - %(message)s'
|
||||||
|
formatter = ColoredFormatter(log_format)
|
||||||
console_handler = logging.StreamHandler()
|
console_handler = logging.StreamHandler()
|
||||||
console_handler.setFormatter(formatter)
|
console_handler.setFormatter(formatter)
|
||||||
|
logging.basicConfig(level=logging.INFO, handlers=[console_handler])
|
||||||
logging.basicConfig(level=logging.INFO, handlers=[console_handler])
|
|
||||||
|
|
||||||
def update_status(server):
|
def update_status(server):
|
||||||
def print_proxy_info():
|
def print_proxy_info():
|
||||||
@ -30,69 +29,20 @@ def update_status(server):
|
|||||||
old_port = int(server.config.get('port', '1080'))
|
old_port = int(server.config.get('port', '1080'))
|
||||||
|
|
||||||
server.config.update(new_config)
|
server.config.update(new_config)
|
||||||
|
server._update_config_values(new_config)
|
||||||
server.port = int(new_config.get('port', '1080'))
|
|
||||||
server.mode = new_config.get('mode', 'cycle')
|
|
||||||
server.interval = int(new_config.get('interval', '300'))
|
|
||||||
server.language = new_config.get('language', 'cn')
|
|
||||||
server.use_getip = new_config.get('use_getip', 'False').lower() == 'true'
|
|
||||||
server.check_proxies = new_config.get('check_proxies', 'True').lower() == 'true'
|
|
||||||
|
|
||||||
server.username = new_config.get('username', '')
|
|
||||||
server.password = new_config.get('password', '')
|
|
||||||
server.proxy_username = new_config.get('proxy_username', '')
|
|
||||||
server.proxy_password = new_config.get('proxy_password', '')
|
|
||||||
server.auth_required = bool(server.username and server.password)
|
|
||||||
|
|
||||||
server.proxy_file = new_config.get('proxy_file', 'ip.txt')
|
|
||||||
server.whitelist_file = new_config.get('whitelist_file', '')
|
|
||||||
server.blacklist_file = new_config.get('blacklist_file', '')
|
|
||||||
server.ip_auth_priority = new_config.get('ip_auth_priority', 'whitelist')
|
|
||||||
|
|
||||||
server.whitelist = load_ip_list(new_config.get('whitelist_file', ''))
|
|
||||||
server.blacklist = load_ip_list(new_config.get('blacklist_file', ''))
|
|
||||||
|
|
||||||
if old_use_getip != server.use_getip or old_mode != server.mode:
|
if old_use_getip != server.use_getip or old_mode != server.mode:
|
||||||
if server.use_getip:
|
server._handle_mode_change()
|
||||||
server.proxies = []
|
|
||||||
server.proxy_cycle = None
|
|
||||||
server.current_proxy = None
|
|
||||||
logging.info(get_message('api_mode_notice', server.language))
|
|
||||||
else:
|
|
||||||
server.proxies = server._load_file_proxies()
|
|
||||||
if server.proxies:
|
|
||||||
server.proxy_cycle = cycle(server.proxies)
|
|
||||||
server.current_proxy = next(server.proxy_cycle)
|
|
||||||
if server.check_proxies:
|
|
||||||
asyncio.run(run_proxy_check(server))
|
|
||||||
|
|
||||||
if server.use_getip:
|
|
||||||
server.getip_url = new_config.get('getip_url', '')
|
|
||||||
|
|
||||||
server.last_switch_time = time.time()
|
|
||||||
|
|
||||||
nonlocal display_level
|
|
||||||
display_level = int(new_config.get('display_level', '1'))
|
|
||||||
|
|
||||||
if hasattr(server, 'progress_bar'):
|
|
||||||
if not is_docker:
|
|
||||||
server.progress_bar.close()
|
|
||||||
delattr(server, 'progress_bar')
|
|
||||||
if hasattr(server, 'last_update_time'):
|
|
||||||
delattr(server, 'last_update_time')
|
|
||||||
|
|
||||||
if old_port != server.port:
|
if old_port != server.port:
|
||||||
logging.info(get_message('port_changed', server.language, old_port, server.port))
|
logging.info(get_message('port_changed', server.language, old_port, server.port))
|
||||||
|
|
||||||
logging.info(get_message('config_updated', server.language))
|
|
||||||
|
|
||||||
display_level = int(server.config.get('display_level', '1'))
|
|
||||||
is_docker = os.path.exists('/.dockerenv')
|
|
||||||
|
|
||||||
config_file = 'config/config.ini'
|
config_file = 'config/config.ini'
|
||||||
ip_file = server.proxy_file
|
ip_file = server.proxy_file
|
||||||
last_config_modified_time = os.path.getmtime(config_file) if os.path.exists(config_file) else 0
|
last_config_modified_time = os.path.getmtime(config_file) if os.path.exists(config_file) else 0
|
||||||
last_ip_modified_time = os.path.getmtime(ip_file) if os.path.exists(ip_file) else 0
|
last_ip_modified_time = os.path.getmtime(ip_file) if os.path.exists(ip_file) else 0
|
||||||
|
display_level = int(server.config.get('display_level', '1'))
|
||||||
|
is_docker = os.path.exists('/.dockerenv')
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
@ -109,12 +59,7 @@ def update_status(server):
|
|||||||
current_ip_modified_time = os.path.getmtime(ip_file)
|
current_ip_modified_time = os.path.getmtime(ip_file)
|
||||||
if current_ip_modified_time > last_ip_modified_time:
|
if current_ip_modified_time > last_ip_modified_time:
|
||||||
logging.info(get_message('proxy_file_changed', server.language))
|
logging.info(get_message('proxy_file_changed', server.language))
|
||||||
server.proxies = server._load_file_proxies()
|
server._reload_proxies()
|
||||||
if server.proxies:
|
|
||||||
server.proxy_cycle = cycle(server.proxies)
|
|
||||||
server.current_proxy = next(server.proxy_cycle)
|
|
||||||
if server.check_proxies:
|
|
||||||
asyncio.run(run_proxy_check(server))
|
|
||||||
last_ip_modified_time = current_ip_modified_time
|
last_ip_modified_time = current_ip_modified_time
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -141,10 +86,6 @@ def update_status(server):
|
|||||||
if not hasattr(server, 'last_proxy') or server.last_proxy != server.current_proxy:
|
if not hasattr(server, 'last_proxy') or server.last_proxy != server.current_proxy:
|
||||||
print_proxy_info()
|
print_proxy_info()
|
||||||
server.last_proxy = server.current_proxy
|
server.last_proxy = server.current_proxy
|
||||||
if display_level >= 2:
|
|
||||||
logging.info(get_message('proxy_switch_detail', server.language,
|
|
||||||
getattr(server, 'previous_proxy', 'None'),
|
|
||||||
server.current_proxy))
|
|
||||||
server.previous_proxy = server.current_proxy
|
server.previous_proxy = server.current_proxy
|
||||||
|
|
||||||
total_time = int(server.interval)
|
total_time = int(server.interval)
|
||||||
@ -227,24 +168,44 @@ async def run_proxy_check(server):
|
|||||||
|
|
||||||
class ProxyCat:
|
class ProxyCat:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
cpu_count = os.cpu_count() or 1
|
||||||
self.executor = ThreadPoolExecutor(
|
self.executor = ThreadPoolExecutor(
|
||||||
max_workers=min(32, (os.cpu_count() or 1) * 4),
|
max_workers=min(32, cpu_count + 4),
|
||||||
thread_name_prefix="proxy_worker"
|
thread_name_prefix="proxy_worker",
|
||||||
|
thread_name_format="proxy_worker_%d"
|
||||||
)
|
)
|
||||||
|
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
loop.set_default_executor(self.executor)
|
loop.set_default_executor(self.executor)
|
||||||
|
|
||||||
if hasattr(asyncio, 'WindowsSelectorEventLoopPolicy'):
|
if hasattr(loop, 'set_task_factory'):
|
||||||
if os.name == 'nt':
|
loop.set_task_factory(None)
|
||||||
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
|
||||||
|
|
||||||
socket.setdefaulttimeout(30)
|
socket.setdefaulttimeout(30)
|
||||||
if hasattr(socket, 'TCP_NODELAY'):
|
if hasattr(socket, 'TCP_NODELAY'):
|
||||||
socket.TCP_NODELAY = True
|
socket.TCP_NODELAY = True
|
||||||
|
if hasattr(socket, 'SO_KEEPALIVE'):
|
||||||
|
socket.SO_KEEPALIVE = True
|
||||||
|
|
||||||
self.running = True
|
if os.name != 'nt':
|
||||||
|
import resource
|
||||||
|
soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE)
|
||||||
|
try:
|
||||||
|
resource.setrlimit(resource.RLIMIT_NOFILE, (hard, hard))
|
||||||
|
except ValueError:
|
||||||
|
resource.setrlimit(resource.RLIMIT_NOFILE, (soft, soft))
|
||||||
|
|
||||||
|
soft, hard = resource.getrlimit(resource.RLIMIT_NPROC)
|
||||||
|
try:
|
||||||
|
resource.setrlimit(resource.RLIMIT_NPROC, (hard, hard))
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
self.running = True
|
||||||
|
self.tasks = set()
|
||||||
|
self.max_tasks = 20000
|
||||||
|
self.task_semaphore = asyncio.Semaphore(self.max_tasks)
|
||||||
|
|
||||||
signal.signal(signal.SIGINT, self.handle_shutdown)
|
signal.signal(signal.SIGINT, self.handle_shutdown)
|
||||||
signal.signal(signal.SIGTERM, self.handle_shutdown)
|
signal.signal(signal.SIGTERM, self.handle_shutdown)
|
||||||
self.config = load_config('config/config.ini')
|
self.config = load_config('config/config.ini')
|
||||||
@ -256,6 +217,11 @@ class ProxyCat:
|
|||||||
if config.has_section('Users'):
|
if config.has_section('Users'):
|
||||||
self.users = dict(config.items('Users'))
|
self.users = dict(config.items('Users'))
|
||||||
self.auth_required = bool(self.users)
|
self.auth_required = bool(self.users)
|
||||||
|
|
||||||
|
self.proxy_pool = {}
|
||||||
|
self.max_connections = 1000
|
||||||
|
self.connection_timeout = 30
|
||||||
|
self.read_timeout = 60
|
||||||
|
|
||||||
async def start_server(self):
|
async def start_server(self):
|
||||||
try:
|
try:
|
||||||
@ -292,9 +258,9 @@ class ProxyCat:
|
|||||||
return
|
return
|
||||||
|
|
||||||
await asyncio.get_event_loop().run_in_executor(
|
await asyncio.get_event_loop().run_in_executor(
|
||||||
self.executor,
|
self.executor,
|
||||||
self.process_client_request,
|
self._handle_proxy_request,
|
||||||
reader,
|
reader,
|
||||||
writer
|
writer
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -305,6 +271,7 @@ class ProxyCat:
|
|||||||
await writer.wait_closed()
|
await writer.wait_closed()
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
self.tasks.remove(task)
|
||||||
|
|
||||||
def _authenticate(self, auth_header):
|
def _authenticate(self, auth_header):
|
||||||
if not self.users:
|
if not self.users:
|
||||||
@ -322,7 +289,115 @@ class ProxyCat:
|
|||||||
except:
|
except:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
async def _handle_proxy_request(self, reader, writer):
|
||||||
|
try:
|
||||||
|
request_line = await reader.readline()
|
||||||
|
if not request_line:
|
||||||
|
return
|
||||||
|
|
||||||
|
method, target, version = request_line.decode().strip().split(' ')
|
||||||
|
|
||||||
|
if method == 'CONNECT':
|
||||||
|
await self._handle_connect(target, reader, writer)
|
||||||
|
else:
|
||||||
|
await self._handle_http(method, target, version, reader, writer)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(get_message('request_handling_error', self.language, e))
|
||||||
|
try:
|
||||||
|
writer.close()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def _handle_connect(self, target, reader, writer):
|
||||||
|
host, port = target.split(':')
|
||||||
|
port = int(port)
|
||||||
|
|
||||||
|
try:
|
||||||
|
remote_reader, remote_writer = await asyncio.wait_for(
|
||||||
|
asyncio.open_connection(host, port),
|
||||||
|
timeout=self.connection_timeout
|
||||||
|
)
|
||||||
|
|
||||||
|
writer.write(b'HTTP/1.1 200 Connection Established\r\n\r\n')
|
||||||
|
await writer.drain()
|
||||||
|
|
||||||
|
await self._create_pipe(reader, writer, remote_reader, remote_writer)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(get_message('proxy_forward_error', self.language, e))
|
||||||
|
writer.write(b'HTTP/1.1 502 Bad Gateway\r\n\r\n')
|
||||||
|
await writer.drain()
|
||||||
|
|
||||||
|
async def _handle_http(self, method, target, version, reader, writer):
|
||||||
|
try:
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
url = urlparse(target)
|
||||||
|
host = url.hostname
|
||||||
|
port = url.port or 80
|
||||||
|
|
||||||
|
remote_reader, remote_writer = await asyncio.wait_for(
|
||||||
|
asyncio.open_connection(host, port),
|
||||||
|
timeout=self.connection_timeout
|
||||||
|
)
|
||||||
|
|
||||||
|
path = url.path + ('?' + url.query if url.query else '')
|
||||||
|
request = f'{method} {path} {version}\r\n'
|
||||||
|
remote_writer.write(request.encode())
|
||||||
|
|
||||||
|
while True:
|
||||||
|
line = await reader.readline()
|
||||||
|
if line == b'\r\n':
|
||||||
|
break
|
||||||
|
remote_writer.write(line)
|
||||||
|
remote_writer.write(b'\r\n')
|
||||||
|
|
||||||
|
await self._create_pipe(reader, writer, remote_reader, remote_writer)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(get_message('proxy_forward_error', self.language, e))
|
||||||
|
writer.write(b'HTTP/1.1 502 Bad Gateway\r\n\r\n')
|
||||||
|
await writer.drain()
|
||||||
|
|
||||||
|
async def _create_pipe(self, client_reader, client_writer, remote_reader, remote_writer):
|
||||||
|
try:
|
||||||
|
pipe1 = asyncio.create_task(self._pipe(client_reader, remote_writer))
|
||||||
|
pipe2 = asyncio.create_task(self._pipe(remote_reader, client_writer))
|
||||||
|
|
||||||
|
done, pending = await asyncio.wait(
|
||||||
|
[pipe1, pipe2],
|
||||||
|
return_when=asyncio.FIRST_COMPLETED
|
||||||
|
)
|
||||||
|
|
||||||
|
for task in pending:
|
||||||
|
task.cancel()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(get_message('data_transfer_error', self.language, e))
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
remote_writer.close()
|
||||||
|
await remote_writer.wait_closed()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def _pipe(self, reader, writer):
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
data = await reader.read(8192)
|
||||||
|
if not data:
|
||||||
|
break
|
||||||
|
|
||||||
|
writer.write(data)
|
||||||
|
await writer.drain()
|
||||||
|
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
pass
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(get_message('data_transfer_error', self.language, e))
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
setup_logging()
|
||||||
parser = argparse.ArgumentParser(description=logos())
|
parser = argparse.ArgumentParser(description=logos())
|
||||||
parser.add_argument('-c', '--config', default='config/config.ini', help='配置文件路径')
|
parser.add_argument('-c', '--config', default='config/config.ini', help='配置文件路径')
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
312
README-EN.md
@ -11,18 +11,11 @@
|
|||||||
- [Development Background](#development-background)
|
- [Development Background](#development-background)
|
||||||
- [Features](#features)
|
- [Features](#features)
|
||||||
- [Installation and Usage](#installation-and-usage)
|
- [Installation and Usage](#installation-and-usage)
|
||||||
- [Dependencies Installation](#dependencies-installation)
|
|
||||||
- [Running the Tool](#running-the-tool)
|
|
||||||
- [Manual Proxy Entry](#iptxt-manual-proxy-entry)
|
|
||||||
- [Configuration File](#configuration-file)
|
|
||||||
- [Demo Effect](#demo-effect)
|
|
||||||
- [Using API for Automatic Proxy Retrieval](#using-api-for-automatic-proxy-retrieval)
|
|
||||||
- [Docker Deployment](#docker-deployment)
|
|
||||||
- [Performance](#performance)
|
|
||||||
- [Disclaimer](#disclaimer)
|
- [Disclaimer](#disclaimer)
|
||||||
- [Change Log](#change-log)
|
- [Changelog](#changelog)
|
||||||
- [Development Plan](#development-plan)
|
- [Development Plan](#development-plan)
|
||||||
- [Acknowledgments](#acknowledgments)
|
- [Special Thanks](#special-thanks)
|
||||||
|
- [Sponsor](#sponsor)
|
||||||
- [Proxy Recommendations](#proxy-recommendations)
|
- [Proxy Recommendations](#proxy-recommendations)
|
||||||
|
|
||||||
## Development Background
|
## Development Background
|
||||||
@ -35,269 +28,82 @@ Therefore, **ProxyCat** was born! This tool aims to transform short-term IPs (la
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Dual Protocol Support**: Supports both SOCKS5 and HTTP protocol listening, compatible with more tools.
|
- **Dual Protocol Listening**: Supports HTTP/SOCKS5 protocol listening, compatible with more tools.
|
||||||
- **Multiple Proxy Protocols**: Supports HTTP/HTTPS/SOCKS5 proxy servers to meet various application needs.
|
- **Triple Proxy Types**: Supports HTTP/HTTPS/SOCKS5 proxy servers with authentication.
|
||||||
- **Multiple Switching Modes**: Cycles through each proxy in the list sequentially; randomly selects available proxies to distribute traffic load and improve performance. Allows users to customize proxy selection logic for specific needs.
|
- **Flexible Switching Modes**: Supports sequential, random, and custom proxy selection for optimized traffic distribution.
|
||||||
- **Function-based Proxy Retrieval**: Supports dynamic proxy retrieval through GetIP function for real-time availability.
|
- **Dynamic Proxy Acquisition**: Get available proxies in real-time through GetIP function, supports API interface calls.
|
||||||
- **Automatic Validity Detection**: Automatically detects proxy availability at startup to filter invalid proxies.
|
- **Proxy Protection**: When using GetIP method, proxies are only fetched upon receiving requests, not at initial startup.
|
||||||
- **Switch Only During Proxy Forwarding**: Changes to new proxy server only when timer reaches zero and new requests arrive.
|
- **Automatic Proxy Detection**: Automatically checks proxy validity at startup, removing invalid ones.
|
||||||
- **Proxy Failure Switching**: Automatically switches to new proxy if current proxy fails during traffic forwarding.
|
- **Smart Proxy Switching**: Only obtains new proxies during request execution, reducing resource consumption.
|
||||||
- **Proxy Pool Authentication**: Supports username/password-based authentication and blacklist/whitelist mechanisms.
|
- **Invalid Proxy Handling**: Automatically validates and switches to new proxies when current ones fail.
|
||||||
- **Real-time Status Updates**: Displays current proxy status and next switch time.
|
- **Authentication Support**: Supports username/password authentication and IP blacklist/whitelist management.
|
||||||
- **Configurable File**: Easily adjust port, mode, authentication info via config.ini.
|
- **Real-time Status Display**: Shows proxy status and switching times for dynamic monitoring.
|
||||||
- **Version Detection**: Built-in version checking for automatic updates.
|
- **Dynamic Configuration**: Updates configuration without service restart.
|
||||||
|
- **Web UI Interface**: Provides web management interface for convenient operation.
|
||||||
|
- **Docker Deployment**: One-click Docker deployment with unified web management.
|
||||||
|
- **Bilingual Support**: Supports Chinese and English language switching.
|
||||||
|
- **Flexible Configuration**: Customize ports, modes, and authentication through config.ini.
|
||||||
|
- **Version Check**: Automatic software update checking.
|
||||||
|
|
||||||
## Installation and Usage
|
## Tool Usage
|
||||||
|
|
||||||
### Dependencies Installation
|
[ProxyCat Operation Manual](../main/ProxyCat-Manual/Operation%20Manual.md)
|
||||||
|
|
||||||
The tool is based on Python, recommended version **Python 3.8** or above. Install dependencies using:
|
## Error Troubleshooting
|
||||||
|
|
||||||
```bash
|
[ProxyCat Investigation Manual](../main/ProxyCat-Manual/Investigation%20Manual.md)
|
||||||
pip install -r requirements.txt
|
|
||||||
# Or using Chinese mirror:
|
|
||||||
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple/
|
|
||||||
```
|
|
||||||
|
|
||||||
### Running the Tool
|
|
||||||
|
|
||||||
Run the following command in the project directory to view help information:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python3 ProxyCat.py -h
|
|
||||||
```
|
|
||||||
|
|
||||||
Success is indicated by this response:
|
|
||||||
|
|
||||||
```
|
|
||||||
|\ _,,,---,,_ by honmashironeko
|
|
||||||
ZZZzz /,`.-'`' -. ;-;;,_
|
|
||||||
|,4- ) )-,_. ,\ ( `'-'
|
|
||||||
'---''(_/--' `-'\_) ProxyCat
|
|
||||||
|
|
||||||
Usage: ProxyCat.py [-h] [-c]
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
-h, --help Show this help message and exit
|
|
||||||
-c C Specify config file name (default: config.ini)
|
|
||||||
```
|
|
||||||
|
|
||||||
### ip.txt Manual Proxy Entry
|
|
||||||
|
|
||||||
Add proxies to `ip.txt` in the following format (`socks5://127.0.0.1:7890` or `http://127.0.0.1:7890`), one per line:
|
|
||||||
|
|
||||||
```txt
|
|
||||||
socks5://127.0.0.1:7890
|
|
||||||
https://127.0.0.1:7890
|
|
||||||
http://127.0.0.1:7890
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
### Configuration File
|
|
||||||
|
|
||||||
Configure parameters in `config.ini` (or custom config file):
|
|
||||||
|
|
||||||
```ini
|
|
||||||
[SETTINGS]
|
|
||||||
# Local server listening port (default: 1080)
|
|
||||||
port = 1080
|
|
||||||
|
|
||||||
# Proxy rotation mode: cycle, custom, or load_balance (default: cycle)
|
|
||||||
mode = cycle
|
|
||||||
|
|
||||||
# Proxy change interval in seconds, 0 means change every request (default: 300)
|
|
||||||
interval = 300
|
|
||||||
|
|
||||||
# Local server authentication username (default: neko) empty means no auth
|
|
||||||
username = neko
|
|
||||||
|
|
||||||
# Local server authentication password (default: 123456) empty means no auth
|
|
||||||
password = 123456
|
|
||||||
|
|
||||||
# Whether to use getip module for proxy retrieval True/False (default: False)
|
|
||||||
use_getip = False
|
|
||||||
|
|
||||||
# Proxy list file (default: ip.txt)
|
|
||||||
proxy_file = ip.txt
|
|
||||||
|
|
||||||
# Enable proxy checking True/False (default: True)
|
|
||||||
check_proxies = True
|
|
||||||
|
|
||||||
# Language setting (cn/en)
|
|
||||||
language = en
|
|
||||||
|
|
||||||
# IP whitelist file path (empty to disable)
|
|
||||||
whitelist_file = whitelist.txt
|
|
||||||
|
|
||||||
# IP blacklist file path (empty to disable)
|
|
||||||
blacklist_file = blacklist.txt
|
|
||||||
|
|
||||||
# IP authentication priority (whitelist/blacklist)
|
|
||||||
# whitelist: Check whitelist first, allow if in whitelist
|
|
||||||
# blacklist: Check blacklist first, deny if in blacklist
|
|
||||||
ip_auth_priority = whitelist
|
|
||||||
```
|
|
||||||
|
|
||||||
After configuration, run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python3 ProxyCat.py
|
|
||||||
```
|
|
||||||
|
|
||||||
### Demo Effect
|
|
||||||
|
|
||||||
**Fixed proxy address (default)**:
|
|
||||||
|
|
||||||
```
|
|
||||||
http://neko:123456@127.0.0.1:1080
|
|
||||||
http://127.0.0.1:1080
|
|
||||||
socks5://neko:123456@127.0.0.1:1080
|
|
||||||
socks5://127.0.0.1:1080
|
|
||||||
```
|
|
||||||
|
|
||||||
If you're deploying on a public network, simply replace `127.0.0.1` with your public IP.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### Using API for Automatic Proxy Retrieval
|
|
||||||
|
|
||||||
The tool supports direct API calls to obtain proxy addresses. When you configure `use_getip = True`, the tool will no longer read from local `ip.txt` but instead execute **getip.py** script to obtain new proxy addresses (ensure your IP is whitelisted, and the format should be IP:port, only one proxy address can be used each time).
|
|
||||||
|
|
||||||
In this case, you need to modify the content of **getip.py** to your own API, with format `IP:PORT`. Default protocol is `socks5`, manually change to `http` if needed.
|
|
||||||
|
|
||||||
### Docker Deployment
|
|
||||||
|
|
||||||
Please install Docker and Docker-compose in advance. You can search for installation methods online.
|
|
||||||
|
|
||||||
```
|
|
||||||
# Clone the project source code locally
|
|
||||||
git clone https://github.com/honmashironeko/ProxyCat.git
|
|
||||||
|
|
||||||
# Modify the content in the config.ini file in the config folder
|
|
||||||
|
|
||||||
# Enter the ProxyCat folder, build the image and start the container
|
|
||||||
docker-compose up -d --build
|
|
||||||
|
|
||||||
# Stop and start the service (you need to restart the service after modifying the configuration each time)
|
|
||||||
docker-compose down | docker-compose up -d
|
|
||||||
|
|
||||||
# View log information
|
|
||||||
docker logs proxycat
|
|
||||||
```
|
|
||||||
|
|
||||||
## Performance
|
|
||||||
|
|
||||||
Through actual testing, when proxy server performance is sufficient, ProxyCat can handle **1000** concurrent connections without packet loss, covering most scanning and penetration testing needs.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## Disclaimer
|
## Disclaimer
|
||||||
|
|
||||||
- By downloading, installing, using, or modifying this tool and related code, you indicate your trust in this tool.
|
- By downloading, installing, using, or modifying this tool and related code, you indicate your trust in this tool.
|
||||||
- We take no responsibility for any form of loss or damage caused to yourself or others while using this tool.
|
- We are not responsible for any form of loss or damage caused to yourself or others while using this tool.
|
||||||
- You are solely responsible for any illegal activities during your use of this tool, and we bear no legal or associated liability.
|
- You are solely responsible for any illegal activities conducted while using this tool.
|
||||||
- Please carefully read and fully understand all terms, especially liability exemption or limitation clauses, and choose to accept or not.
|
- Please carefully read and fully understand all terms, especially liability exemption clauses.
|
||||||
- Unless you have read and accepted all terms of this agreement, you have no right to download, install, or use this tool.
|
- You have no right to download, install, or use this tool unless you have read and accepted all terms.
|
||||||
- Your download, installation, and usage actions indicate you have read and agreed to be bound by the above agreement.
|
- Your download, installation, and usage actions indicate your acceptance of this agreement.
|
||||||
|
|
||||||
## Change Log
|
## Changelog
|
||||||
|
|
||||||
### 2025/01/07
|
[Changelog Records](../main/ProxyCat-Manual/logs.md)
|
||||||
|
|
||||||
- Introduced connection pool mechanism to improve performance.
|
|
||||||
- Optimized error handling and logging.
|
|
||||||
- Enhanced proxy switching mechanism.
|
|
||||||
|
|
||||||
### 2025/01/03
|
|
||||||
|
|
||||||
- Centralized configuration parameters management into config files for better maintenance
|
|
||||||
- Fixed known bugs and improved stability and concurrency capabilities
|
|
||||||
|
|
||||||
### 2025/01/02
|
|
||||||
|
|
||||||
- Restructured software architecture for better usability
|
|
||||||
- Added blacklist/whitelist mechanism for authentication
|
|
||||||
- GetIP method now only requests proxy after receiving first request to prevent resource waste
|
|
||||||
- Changed language configuration logic, now controlled via config.ini parameter
|
|
||||||
- Updated configuration panel, addresses can be copied without username/password
|
|
||||||
- Added docker deployment support
|
|
||||||
|
|
||||||
### 2024/10/23
|
|
||||||
|
|
||||||
- Restructured code, split into separate files
|
|
||||||
- Added automatic proxy switching when current proxy fails during forwarding
|
|
||||||
|
|
||||||
### 2024/09/29
|
|
||||||
|
|
||||||
- Removed less-used single cycle mode, replaced with custom mode for customizable proxy switching logic
|
|
||||||
- Changed proxy validity checking to asynchronous for better speed
|
|
||||||
- Removed problematic SOCKS4 protocol support
|
|
||||||
- Beautified logging system
|
|
||||||
- Improved exception handling logic
|
|
||||||
- Added proxy format validation
|
|
||||||
|
|
||||||
### 2024/09/10
|
|
||||||
|
|
||||||
- Optimized concurrency efficiency, supporting next request before receiving response
|
|
||||||
- Added load balancing mode for random proxy selection and concurrent proxy usage
|
|
||||||
- Changed proxy validity checking to asynchronous for better efficiency
|
|
||||||
|
|
||||||
### 2024/09/09
|
|
||||||
|
|
||||||
- Added option to validate proxies in ip.txt at startup
|
|
||||||
- Function downgrade to support lower Python versions
|
|
||||||
|
|
||||||
### 2024/09/03
|
|
||||||
|
|
||||||
- Added local SOCKS5 listening for wider software compatibility
|
|
||||||
- Changed some functions to support lower Python versions
|
|
||||||
- Beautified output display
|
|
||||||
|
|
||||||
### 2024/08/31
|
|
||||||
|
|
||||||
- Major project structure adjustment
|
|
||||||
- Beautified display with continuous proxy switch time indication
|
|
||||||
- Added Ctrl+C support for stopping
|
|
||||||
- Major adjustment to async requests, improved concurrency efficiency
|
|
||||||
- Changed from runtime parameters to local ini config file
|
|
||||||
- Added support for local authentication-free mode
|
|
||||||
- Added version detection
|
|
||||||
- Added proxy server authentication
|
|
||||||
- Added GetIP update only on request feature
|
|
||||||
- Added proxy protocol auto-detection
|
|
||||||
- Added HTTPS protocol support
|
|
||||||
- Changed asyncio.timeout() to asyncio.wait_for() for lower Python version support
|
|
||||||
|
|
||||||
## Development Plan
|
## Development Plan
|
||||||
|
|
||||||
- [x] Added local server authentication
|
- [x] Add detailed logging to record all IP identities connecting to ProxyCat, supporting multiple users.
|
||||||
- [x] Added IP change per request feature
|
- [x] Add Web UI for a more powerful and user-friendly interface.
|
||||||
- [x] Added static proxy auto-update module
|
- [ ] Develop babycat module that can run on any server or host to turn it into a proxy server.
|
||||||
- [x] Added load balancing mode
|
- [ ] Add request blacklist/whitelist to specify URLs, IPs, or domains to be forcibly dropped or bypassed.
|
||||||
- [x] Added version detection
|
- [ ] Package to PyPi for easier installation and use.
|
||||||
- [x] Added proxy authentication support
|
|
||||||
- [x] Added request-triggered getip updates
|
|
||||||
- [x] Added initial proxy validity check
|
|
||||||
- [x] Added SOCKS protocol support
|
|
||||||
- [ ] Add detailed logging with multi-user support
|
|
||||||
- [ ] Add Web UI interface
|
|
||||||
- [x] Add docker deployment
|
|
||||||
- [ ] Develop babycat module
|
|
||||||
|
|
||||||
For feedback or suggestions, please contact via WeChat Official Account: **樱花庄的本间白猫**
|
If you have good ideas or encounter bugs during use, please contact the author through:
|
||||||
|
|
||||||
## Acknowledgments
|
WeChat Official Account: **樱花庄的本间白猫**
|
||||||
|
|
||||||
In no particular order, thanks to:
|
## Special Thanks
|
||||||
|
|
||||||
- [AabyssZG](https://github.com/AabyssZG)
|
In no particular order, thanks to all contributors who helped with this project:
|
||||||
- [ProbiusOfficial](https://github.com/ProbiusOfficial)
|
|
||||||
- [gh0stkey](https://github.com/gh0stkey)
|
- [AabyssZG (曾哥)](https://github.com/AabyssZG)
|
||||||
|
- [ProbiusOfficial (探姬)](https://github.com/ProbiusOfficial)
|
||||||
|
- [gh0stkey (EvilChen)](https://github.com/gh0stkey)
|
||||||
|
- [huangzheng2016(HydrogenE7)](https://github.com/huangzheng2016)
|
||||||
- chars6
|
- chars6
|
||||||
- qianzai
|
- qianzai(千载)
|
||||||
- ziwindlu
|
- ziwindlu
|
||||||
|
|
||||||

|
## Sponsor
|
||||||
|
|
||||||
|
Open source development isn't easy. If you find this tool helpful, consider sponsoring the author's development!
|
||||||
|
|
||||||
|
---
|
||||||
|
| Rank | ID | Amount (CNY) |
|
||||||
|
| :--: | :-----------------: | :----------: |
|
||||||
|
| 1 | **陆沉** | 1266.62 |
|
||||||
|
| 2 | **柯林斯.民间新秀** | 696 |
|
||||||
|
| 3 | **taffy** | 150 |
|
||||||
|
| [Sponsor List](https://github.com/honmashironeko/Thanks-for-sponsorship) | Every sponsorship is a motivation for the author! | (´∀`)♡ |
|
||||||
|
|
||||||
|
---
|
||||||
|

|
||||||
|
|
||||||
## Proxy Recommendations
|
## Proxy Recommendations
|
||||||
|
|
||||||
|
264
app.py
@ -1,9 +1,8 @@
|
|||||||
from flask import Flask, render_template, jsonify, request, redirect, url_for
|
from flask import Flask, render_template, jsonify, request, redirect, url_for, send_from_directory
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from enum import Enum
|
|
||||||
import json
|
import json
|
||||||
from configparser import ConfigParser
|
from configparser import ConfigParser
|
||||||
from itertools import cycle
|
from itertools import cycle
|
||||||
@ -20,18 +19,20 @@ import threading
|
|||||||
import time
|
import time
|
||||||
|
|
||||||
app = Flask(__name__,
|
app = Flask(__name__,
|
||||||
template_folder='web/templates')
|
template_folder='web/templates',
|
||||||
|
static_folder='web/static')
|
||||||
|
|
||||||
werkzeug.serving.WSGIRequestHandler.log = lambda self, type, message, *args: None
|
werkzeug.serving.WSGIRequestHandler.log = lambda self, type, message, *args: None
|
||||||
|
|
||||||
logging.getLogger('werkzeug').setLevel(logging.ERROR)
|
logging.getLogger('werkzeug').setLevel(logging.ERROR)
|
||||||
|
|
||||||
config = load_config('config/config.ini')
|
config = load_config('config/config.ini')
|
||||||
server = AsyncProxyServer(config)
|
server = AsyncProxyServer(config)
|
||||||
|
|
||||||
|
def get_config_path(filename):
|
||||||
|
return os.path.join('config', filename)
|
||||||
|
|
||||||
log_file = 'logs/proxycat.log'
|
log_file = 'logs/proxycat.log'
|
||||||
os.makedirs('logs', exist_ok=True)
|
os.makedirs('logs', exist_ok=True)
|
||||||
|
|
||||||
log_messages = []
|
log_messages = []
|
||||||
max_log_messages = 10000
|
max_log_messages = 10000
|
||||||
|
|
||||||
@ -39,9 +40,25 @@ class CustomFormatter(logging.Formatter):
|
|||||||
def formatTime(self, record, datefmt=None):
|
def formatTime(self, record, datefmt=None):
|
||||||
return datetime.fromtimestamp(record.created).strftime('%Y-%m-%d %H:%M:%S')
|
return datetime.fromtimestamp(record.created).strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
file_formatter = CustomFormatter('%(asctime)s - %(levelname)s - %(message)s')
|
def setup_logging():
|
||||||
file_handler = logging.FileHandler(log_file, encoding='utf-8')
|
file_formatter = CustomFormatter('%(asctime)s - %(levelname)s - %(message)s')
|
||||||
file_handler.setFormatter(file_formatter)
|
file_handler = logging.FileHandler(log_file, encoding='utf-8')
|
||||||
|
file_handler.setFormatter(file_formatter)
|
||||||
|
|
||||||
|
console_handler = logging.StreamHandler()
|
||||||
|
console_formatter = CustomFormatter('%(asctime)s - %(levelname)s - %(message)s')
|
||||||
|
console_handler.setFormatter(console_formatter)
|
||||||
|
|
||||||
|
memory_handler = MemoryHandler()
|
||||||
|
memory_handler.setFormatter(CustomFormatter('%(message)s'))
|
||||||
|
|
||||||
|
root_logger = logging.getLogger()
|
||||||
|
root_logger.setLevel(logging.INFO)
|
||||||
|
for handler in root_logger.handlers[:]:
|
||||||
|
root_logger.removeHandler(handler)
|
||||||
|
root_logger.addHandler(file_handler)
|
||||||
|
root_logger.addHandler(console_handler)
|
||||||
|
root_logger.addHandler(memory_handler)
|
||||||
|
|
||||||
class MemoryHandler(logging.Handler):
|
class MemoryHandler(logging.Handler):
|
||||||
def emit(self, record):
|
def emit(self, record):
|
||||||
@ -54,21 +71,6 @@ class MemoryHandler(logging.Handler):
|
|||||||
if len(log_messages) > max_log_messages:
|
if len(log_messages) > max_log_messages:
|
||||||
log_messages = log_messages[-max_log_messages:]
|
log_messages = log_messages[-max_log_messages:]
|
||||||
|
|
||||||
console_handler = logging.StreamHandler()
|
|
||||||
console_formatter = CustomFormatter('%(asctime)s - %(levelname)s - %(message)s')
|
|
||||||
console_handler.setFormatter(console_formatter)
|
|
||||||
|
|
||||||
memory_handler = MemoryHandler()
|
|
||||||
memory_handler.setFormatter(CustomFormatter('%(message)s'))
|
|
||||||
|
|
||||||
root_logger = logging.getLogger()
|
|
||||||
root_logger.setLevel(logging.INFO)
|
|
||||||
for handler in root_logger.handlers[:]:
|
|
||||||
root_logger.removeHandler(handler)
|
|
||||||
root_logger.addHandler(file_handler)
|
|
||||||
root_logger.addHandler(console_handler)
|
|
||||||
root_logger.addHandler(memory_handler)
|
|
||||||
|
|
||||||
def require_token(f):
|
def require_token(f):
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def decorated_function(*args, **kwargs):
|
def decorated_function(*args, **kwargs):
|
||||||
@ -110,8 +112,15 @@ def get_status():
|
|||||||
|
|
||||||
server_config = dict(config.items('Server')) if config.has_section('Server') else {}
|
server_config = dict(config.items('Server')) if config.has_section('Server') else {}
|
||||||
|
|
||||||
|
current_proxy = server.current_proxy
|
||||||
|
if not current_proxy and not server.use_getip:
|
||||||
|
if hasattr(server, 'proxies') and server.proxies:
|
||||||
|
current_proxy = server.proxies[0]
|
||||||
|
else:
|
||||||
|
current_proxy = get_message('no_proxy', server.language)
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'current_proxy': server.current_proxy,
|
'current_proxy': current_proxy,
|
||||||
'mode': server.mode,
|
'mode': server.mode,
|
||||||
'port': int(server_config.get('port', '1080')),
|
'port': int(server_config.get('port', '1080')),
|
||||||
'interval': server.interval,
|
'interval': server.interval,
|
||||||
@ -121,6 +130,7 @@ def get_status():
|
|||||||
'getip_url': getattr(server, 'getip_url', '') if getattr(server, 'use_getip', False) else '',
|
'getip_url': getattr(server, 'getip_url', '') if getattr(server, 'use_getip', False) else '',
|
||||||
'auth_required': server.auth_required,
|
'auth_required': server.auth_required,
|
||||||
'display_level': int(config.get('DEFAULT', 'display_level', fallback='1')),
|
'display_level': int(config.get('DEFAULT', 'display_level', fallback='1')),
|
||||||
|
'service_status': 'running' if server.running else 'stopped',
|
||||||
'config': {
|
'config': {
|
||||||
'port': server_config.get('port', ''),
|
'port': server_config.get('port', ''),
|
||||||
'mode': server_config.get('mode', 'cycle'),
|
'mode': server_config.get('mode', 'cycle'),
|
||||||
@ -142,90 +152,48 @@ def get_status():
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@app.route('/api/config', methods=['GET', 'POST'])
|
@app.route('/api/config', methods=['POST'])
|
||||||
def handle_config():
|
def save_config():
|
||||||
if request.method == 'POST':
|
try:
|
||||||
new_config = request.json
|
new_config = request.get_json()
|
||||||
try:
|
current_config = load_config('config/config.ini')
|
||||||
with open('config/config.ini', 'r', encoding='utf-8') as f:
|
port_changed = str(new_config.get('port', '')) != str(current_config.get('port', ''))
|
||||||
lines = f.readlines()
|
|
||||||
|
config_parser = ConfigParser()
|
||||||
|
config_parser.read('config/config.ini', encoding='utf-8')
|
||||||
|
|
||||||
|
if not config_parser.has_section('Server'):
|
||||||
|
config_parser.add_section('Server')
|
||||||
|
|
||||||
current_section = None
|
for key, value in new_config.items():
|
||||||
updated_lines = []
|
if key != 'users':
|
||||||
i = 0
|
config_parser.set('Server', key, str(value))
|
||||||
while i < len(lines):
|
|
||||||
line = lines[i].strip()
|
with open('config/config.ini', 'w', encoding='utf-8') as f:
|
||||||
|
config_parser.write(f)
|
||||||
if line.startswith('['):
|
|
||||||
current_section = line[1:-1]
|
|
||||||
updated_lines.append(lines[i])
|
|
||||||
i += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
if line.startswith('#') or not line:
|
|
||||||
updated_lines.append(lines[i])
|
|
||||||
i += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
if '=' in line:
|
|
||||||
key = line.split('=')[0].strip()
|
|
||||||
if key in new_config:
|
|
||||||
updated_lines.append(f"{key} = {new_config[key]}\n")
|
|
||||||
else:
|
|
||||||
updated_lines.append(lines[i])
|
|
||||||
i += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
updated_lines.append(lines[i])
|
|
||||||
i += 1
|
|
||||||
|
|
||||||
with open('config/config.ini', 'w', encoding='utf-8') as f:
|
server.config = load_config('config/config.ini')
|
||||||
f.writelines(updated_lines)
|
server._update_config_values(server.config)
|
||||||
|
|
||||||
config = load_config('config/config.ini')
|
return jsonify({
|
||||||
server.config = config
|
'status': 'success',
|
||||||
|
'port_changed': port_changed
|
||||||
server.mode = config.get('mode', 'cycle')
|
})
|
||||||
server.interval = int(config.get('interval', '300'))
|
|
||||||
server.language = config.get('language', 'cn')
|
except Exception as e:
|
||||||
server.use_getip = config.get('use_getip', 'False').lower() == 'true'
|
logging.error(f"Error saving config: {e}")
|
||||||
server.check_proxies = config.get('check_proxies', 'True').lower() == 'true'
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
server.username = config.get('username', '')
|
'message': str(e)
|
||||||
server.password = config.get('password', '')
|
})
|
||||||
server.proxy_username = config.get('proxy_username', '')
|
|
||||||
server.proxy_password = config.get('proxy_password', '')
|
|
||||||
server.auth_required = bool(server.username and server.password)
|
|
||||||
|
|
||||||
server.proxy_file = config.get('proxy_file')
|
|
||||||
server.whitelist_file = config.get('whitelist_file', '')
|
|
||||||
server.blacklist_file = config.get('blacklist_file', '')
|
|
||||||
|
|
||||||
if server.use_getip:
|
|
||||||
server.getip_url = config.get('getip_url', '')
|
|
||||||
|
|
||||||
old_port = int(server.config.get('port', '1080'))
|
|
||||||
new_port = int(new_config.get('port', '1080'))
|
|
||||||
needs_restart = old_port != new_port
|
|
||||||
|
|
||||||
return jsonify({
|
|
||||||
'status': 'success',
|
|
||||||
'needs_restart': needs_restart,
|
|
||||||
'message': '配置已更新,需要重启服务器' if needs_restart else '配置已更新'
|
|
||||||
})
|
|
||||||
except Exception as e:
|
|
||||||
return jsonify({'status': 'error', 'message': str(e)})
|
|
||||||
else:
|
|
||||||
with open('config/config.ini', 'r', encoding='utf-8') as f:
|
|
||||||
config_content = f.read()
|
|
||||||
return jsonify({'config': config_content})
|
|
||||||
|
|
||||||
@app.route('/api/proxies', methods=['GET', 'POST'])
|
@app.route('/api/proxies', methods=['GET', 'POST'])
|
||||||
def handle_proxies():
|
def handle_proxies():
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
try:
|
try:
|
||||||
proxies = request.json.get('proxies', [])
|
proxies = request.json.get('proxies', [])
|
||||||
with open(server.proxy_file, 'w', encoding='utf-8') as f:
|
proxy_file = get_config_path(os.path.basename(server.proxy_file))
|
||||||
|
with open(proxy_file, 'w', encoding='utf-8') as f:
|
||||||
f.write('\n'.join(proxies))
|
f.write('\n'.join(proxies))
|
||||||
server.proxies = server._load_file_proxies()
|
server.proxies = server._load_file_proxies()
|
||||||
if server.proxies:
|
if server.proxies:
|
||||||
@ -242,10 +210,11 @@ def handle_proxies():
|
|||||||
})
|
})
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
with open(server.proxy_file, 'r', encoding='utf-8') as f:
|
proxy_file = get_config_path(os.path.basename(server.proxy_file))
|
||||||
|
with open(proxy_file, 'r', encoding='utf-8') as f:
|
||||||
proxies = f.read().splitlines()
|
proxies = f.read().splitlines()
|
||||||
return jsonify({'proxies': proxies})
|
return jsonify({'proxies': proxies})
|
||||||
except Exception as e:
|
except Exception:
|
||||||
return jsonify({'proxies': []})
|
return jsonify({'proxies': []})
|
||||||
|
|
||||||
@app.route('/api/check_proxies')
|
@app.route('/api/check_proxies')
|
||||||
@ -272,7 +241,8 @@ def handle_ip_lists():
|
|||||||
try:
|
try:
|
||||||
list_type = request.json.get('type')
|
list_type = request.json.get('type')
|
||||||
ip_list = request.json.get('list', [])
|
ip_list = request.json.get('list', [])
|
||||||
filename = server.whitelist_file if list_type == 'whitelist' else server.blacklist_file
|
base_filename = os.path.basename(server.whitelist_file if list_type == 'whitelist' else server.blacklist_file)
|
||||||
|
filename = get_config_path(base_filename)
|
||||||
|
|
||||||
with open(filename, 'w', encoding='utf-8') as f:
|
with open(filename, 'w', encoding='utf-8') as f:
|
||||||
f.write('\n'.join(ip_list))
|
f.write('\n'.join(ip_list))
|
||||||
@ -292,9 +262,11 @@ def handle_ip_lists():
|
|||||||
'message': get_message('ip_list_save_failed', server.language, str(e))
|
'message': get_message('ip_list_save_failed', server.language, str(e))
|
||||||
})
|
})
|
||||||
else:
|
else:
|
||||||
|
whitelist_file = get_config_path(os.path.basename(server.whitelist_file))
|
||||||
|
blacklist_file = get_config_path(os.path.basename(server.blacklist_file))
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'whitelist': list(load_ip_list(server.whitelist_file)),
|
'whitelist': list(load_ip_list(whitelist_file)),
|
||||||
'blacklist': list(load_ip_list(server.blacklist_file))
|
'blacklist': list(load_ip_list(blacklist_file))
|
||||||
})
|
})
|
||||||
|
|
||||||
@app.route('/api/logs')
|
@app.route('/api/logs')
|
||||||
@ -359,7 +331,7 @@ def switch_proxy():
|
|||||||
new_proxy = newip()
|
new_proxy = newip()
|
||||||
server.current_proxy = new_proxy
|
server.current_proxy = new_proxy
|
||||||
server.last_switch_time = time.time()
|
server.last_switch_time = time.time()
|
||||||
logging.info(get_message('manual_switch', server.language, old_proxy, new_proxy))
|
server._log_proxy_switch(old_proxy, new_proxy)
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'status': 'success',
|
'status': 'success',
|
||||||
'current_proxy': server.current_proxy,
|
'current_proxy': server.current_proxy,
|
||||||
@ -380,7 +352,7 @@ def switch_proxy():
|
|||||||
old_proxy = server.current_proxy
|
old_proxy = server.current_proxy
|
||||||
server.current_proxy = next(server.proxy_cycle)
|
server.current_proxy = next(server.proxy_cycle)
|
||||||
server.last_switch_time = time.time()
|
server.last_switch_time = time.time()
|
||||||
logging.info(get_message('manual_switch', server.language, old_proxy, server.current_proxy))
|
server._log_proxy_switch(old_proxy, server.current_proxy)
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'status': 'success',
|
'status': 'success',
|
||||||
'current_proxy': server.current_proxy,
|
'current_proxy': server.current_proxy,
|
||||||
@ -418,41 +390,46 @@ def control_service():
|
|||||||
if server.running:
|
if server.running:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'status': 'success',
|
'status': 'success',
|
||||||
'message': get_message('service_start_success', server.language)
|
'message': get_message('service_start_success', server.language),
|
||||||
|
'service_status': 'running'
|
||||||
})
|
})
|
||||||
else:
|
else:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'status': 'error',
|
'status': 'error',
|
||||||
'message': get_message('service_start_failed', server.language)
|
'message': get_message('service_start_failed', server.language),
|
||||||
|
'service_status': 'stopped'
|
||||||
})
|
})
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'status': 'success',
|
'status': 'success',
|
||||||
'message': get_message('service_already_running', server.language)
|
'message': get_message('service_already_running', server.language),
|
||||||
|
'service_status': 'running'
|
||||||
})
|
})
|
||||||
|
|
||||||
elif action == 'stop':
|
elif action == 'stop':
|
||||||
if server.running:
|
if server.running:
|
||||||
server.stop_server = True
|
server.stop_server = True
|
||||||
|
server.running = False
|
||||||
|
|
||||||
if server.server_instance:
|
if server.server_instance:
|
||||||
server.server_instance.close()
|
server.server_instance.close()
|
||||||
|
|
||||||
for _ in range(10):
|
if hasattr(server, 'proxy_thread') and server.proxy_thread:
|
||||||
if not server.running:
|
server.proxy_thread = None
|
||||||
break
|
|
||||||
time.sleep(0.5)
|
|
||||||
|
|
||||||
if server.running:
|
for _ in range(5):
|
||||||
if hasattr(server, 'proxy_thread') and server.proxy_thread:
|
if server.server_instance is None:
|
||||||
server.proxy_thread = None
|
break
|
||||||
server.running = False
|
time.sleep(0.2)
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'status': 'success',
|
'status': 'success',
|
||||||
'message': get_message('service_stop_success', server.language)
|
'message': get_message('service_stop_success', server.language),
|
||||||
|
'service_status': 'stopped'
|
||||||
})
|
})
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'status': 'success',
|
'status': 'success',
|
||||||
'message': get_message('service_not_running', server.language)
|
'message': get_message('service_not_running', server.language),
|
||||||
|
'service_status': 'stopped'
|
||||||
})
|
})
|
||||||
|
|
||||||
elif action == 'restart':
|
elif action == 'restart':
|
||||||
@ -546,32 +523,33 @@ def check_version():
|
|||||||
original_level = httpx_logger.level
|
original_level = httpx_logger.level
|
||||||
httpx_logger.setLevel(logging.WARNING)
|
httpx_logger.setLevel(logging.WARNING)
|
||||||
|
|
||||||
CURRENT_VERSION = "ProxyCat-V2.0.0"
|
CURRENT_VERSION = "ProxyCat-V2.0.1"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
client = httpx.Client(transport=httpx.HTTPTransport(retries=3))
|
client = httpx.Client(transport=httpx.HTTPTransport(retries=3))
|
||||||
response = client.get("https://y.shironekosan.cn/1.html", timeout=10)
|
response = client.get("https://y.shironekosan.cn/1.html", timeout=10)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
content = response.text
|
content = response.text
|
||||||
|
|
||||||
|
match = re.search(r'<p>(ProxyCat-V\d+\.\d+\.\d+)</p>', content)
|
||||||
|
if match:
|
||||||
|
latest_version = match.group(1)
|
||||||
|
is_latest = version.parse(latest_version.split('-V')[1]) <= version.parse(CURRENT_VERSION.split('-V')[1])
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'status': 'success',
|
||||||
|
'is_latest': is_latest,
|
||||||
|
'current_version': CURRENT_VERSION,
|
||||||
|
'latest_version': latest_version
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'message': get_message('version_info_not_found', server.language)
|
||||||
|
})
|
||||||
finally:
|
finally:
|
||||||
httpx_logger.setLevel(original_level)
|
httpx_logger.setLevel(original_level)
|
||||||
|
|
||||||
match = re.search(r'<p>(ProxyCat-V\d+\.\d+\.\d+)</p>', content)
|
|
||||||
if match:
|
|
||||||
latest_version = match.group(1)
|
|
||||||
is_latest = version.parse(latest_version.split('-V')[1]) <= version.parse(CURRENT_VERSION.split('-V')[1])
|
|
||||||
|
|
||||||
return jsonify({
|
|
||||||
'status': 'success',
|
|
||||||
'is_latest': is_latest,
|
|
||||||
'current_version': CURRENT_VERSION,
|
|
||||||
'latest_version': latest_version
|
|
||||||
})
|
|
||||||
else:
|
|
||||||
return jsonify({
|
|
||||||
'status': 'error',
|
|
||||||
'message': get_message('version_info_not_found', server.language)
|
|
||||||
})
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'status': 'error',
|
'status': 'error',
|
||||||
@ -635,10 +613,19 @@ def handle_users():
|
|||||||
logging.error(f"Error getting users: {e}")
|
logging.error(f"Error getting users: {e}")
|
||||||
return jsonify({'users': {}})
|
return jsonify({'users': {}})
|
||||||
|
|
||||||
|
@app.route('/static/<path:path>')
|
||||||
|
def send_static(path):
|
||||||
|
return send_from_directory('web/static', path)
|
||||||
|
|
||||||
def run_proxy_server():
|
def run_proxy_server():
|
||||||
asyncio.run(run_server(server))
|
asyncio.run(run_server(server))
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
logging.info(get_message('user_interrupt', server.language))
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Proxy server error: {e}")
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
setup_logging()
|
||||||
web_port = int(config.get('web_port', '5000'))
|
web_port = int(config.get('web_port', '5000'))
|
||||||
web_url = f"http://127.0.0.1:{web_port}"
|
web_url = f"http://127.0.0.1:{web_port}"
|
||||||
if config.get('token'):
|
if config.get('token'):
|
||||||
@ -649,4 +636,5 @@ if __name__ == '__main__':
|
|||||||
|
|
||||||
proxy_thread = threading.Thread(target=run_proxy_server, daemon=True)
|
proxy_thread = threading.Thread(target=run_proxy_server, daemon=True)
|
||||||
proxy_thread.start()
|
proxy_thread.start()
|
||||||
|
|
||||||
app.run(host='0.0.0.0', port=web_port)
|
app.run(host='0.0.0.0', port=web_port)
|
@ -2,10 +2,12 @@ version: '3'
|
|||||||
services:
|
services:
|
||||||
proxycat:
|
proxycat:
|
||||||
build: .
|
build: .
|
||||||
|
environment:
|
||||||
|
- TZ=Asia/Shanghai
|
||||||
ports:
|
ports:
|
||||||
- "1080:1080"
|
- "1080:1080"
|
||||||
- "5000:5000"
|
- "5000:5000"
|
||||||
volumes:
|
volumes:
|
||||||
- ./config:/app/config
|
- ./config:/app/config
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
network_mode: "bridge"
|
network_mode: "bridge"
|
@ -25,12 +25,10 @@ MESSAGES = {
|
|||||||
'valid_proxies': '有效代理地址: {}',
|
'valid_proxies': '有效代理地址: {}',
|
||||||
'no_valid_proxies': '没有有效的代理地址',
|
'no_valid_proxies': '没有有效的代理地址',
|
||||||
'proxy_check_failed': '{}代理 {} 检测失败: {}',
|
'proxy_check_failed': '{}代理 {} 检测失败: {}',
|
||||||
'proxy_switch': '切换到新的代理: {}',
|
'proxy_switch': '切换代理: {} -> {}',
|
||||||
'proxy_switch_detail': '已切换代理: {} -> {}',
|
|
||||||
'proxy_consecutive_fails': '代理 {} 连续失败 {} 次,正在切换新代理',
|
'proxy_consecutive_fails': '代理 {} 连续失败 {} 次,正在切换新代理',
|
||||||
'proxy_invalid': '代理 {} 已失效,立即切换新代理',
|
'proxy_invalid': '代理 {} 无效,立即切换代理',
|
||||||
'connection_timeout': '连接超时',
|
'connection_timeout': '连接超时',
|
||||||
'proxy_invalid_switching': '代理地址失效,切换代理地址',
|
|
||||||
'data_transfer_timeout': '数据传输超时,正在重试...',
|
'data_transfer_timeout': '数据传输超时,正在重试...',
|
||||||
'connection_reset': '连接被重置',
|
'connection_reset': '连接被重置',
|
||||||
'transfer_cancelled': '传输被取消',
|
'transfer_cancelled': '传输被取消',
|
||||||
@ -81,7 +79,6 @@ MESSAGES = {
|
|||||||
'response_write_error': '写入响应时出错: {}',
|
'response_write_error': '写入响应时出错: {}',
|
||||||
'consecutive_failures': '检测到连续代理失败: {}',
|
'consecutive_failures': '检测到连续代理失败: {}',
|
||||||
'invalid_proxy': '当前代理无效: {}',
|
'invalid_proxy': '当前代理无效: {}',
|
||||||
'proxy_switched': '已从代理 {} 切换到 {}',
|
|
||||||
'whitelist_error': '添加白名单失败: {}',
|
'whitelist_error': '添加白名单失败: {}',
|
||||||
'api_mode_notice': '当前为API模式,收到请求将自动获取代理地址',
|
'api_mode_notice': '当前为API模式,收到请求将自动获取代理地址',
|
||||||
'server_running': '代理服务器运行在 {}:{}',
|
'server_running': '代理服务器运行在 {}:{}',
|
||||||
@ -100,14 +97,11 @@ MESSAGES = {
|
|||||||
2: 显示所有详细信息''',
|
2: 显示所有详细信息''',
|
||||||
'new_client_connect': '新客户端连接 - IP: {}, 用户: {}',
|
'new_client_connect': '新客户端连接 - IP: {}, 用户: {}',
|
||||||
'no_auth': '无认证',
|
'no_auth': '无认证',
|
||||||
'proxy_changed': '代理变更: {} -> {}',
|
|
||||||
'connection_error': '连接处理错误: {}',
|
'connection_error': '连接处理错误: {}',
|
||||||
'cleanup_error': '清理IP错误: {}',
|
'cleanup_error': '清理IP错误: {}',
|
||||||
'port_changed': '端口已更改: {} -> {},需要重启服务器生效',
|
'port_changed': '端口已更改: {} -> {},需要重启服务器生效',
|
||||||
'config_updated': '服务器配置已更新',
|
'config_updated': '服务器配置已更新',
|
||||||
'load_proxy_file_error': '加载代理文件失败: {}',
|
'load_proxy_file_error': '加载代理文件失败: {}',
|
||||||
'manual_switch': '手动切换代理: {} -> {}',
|
|
||||||
'auto_switch': '自动切换代理: {} -> {}',
|
|
||||||
'proxy_check_result': '代理检查完成,有效代理:{}个',
|
'proxy_check_result': '代理检查完成,有效代理:{}个',
|
||||||
'no_proxy': '无代理',
|
'no_proxy': '无代理',
|
||||||
'cycle_mode': '循环模式',
|
'cycle_mode': '循环模式',
|
||||||
@ -196,12 +190,10 @@ MESSAGES = {
|
|||||||
'valid_proxies': 'Valid proxies: {}',
|
'valid_proxies': 'Valid proxies: {}',
|
||||||
'no_valid_proxies': 'No valid proxies found',
|
'no_valid_proxies': 'No valid proxies found',
|
||||||
'proxy_check_failed': '{} proxy {} check failed: {}',
|
'proxy_check_failed': '{} proxy {} check failed: {}',
|
||||||
'proxy_switch': 'Switching to new proxy: {}',
|
'proxy_switch': 'Switch proxy: {} -> {}',
|
||||||
'proxy_switch_detail': 'Switched proxy: {} -> {}',
|
|
||||||
'proxy_consecutive_fails': 'Proxy {} failed {} times consecutively, switching to new proxy',
|
'proxy_consecutive_fails': 'Proxy {} failed {} times consecutively, switching to new proxy',
|
||||||
'proxy_invalid': 'Proxy {} is invalid, switching immediately',
|
'proxy_invalid': 'Proxy {} is invalid, switching proxy immediately',
|
||||||
'connection_timeout': 'Connection timeout',
|
'connection_timeout': 'Connection timeout',
|
||||||
'proxy_invalid_switching': 'Proxy invalid, switching to new proxy',
|
|
||||||
'data_transfer_timeout': 'Data transfer timeout, retrying...',
|
'data_transfer_timeout': 'Data transfer timeout, retrying...',
|
||||||
'connection_reset': 'Connection reset',
|
'connection_reset': 'Connection reset',
|
||||||
'transfer_cancelled': 'Transfer cancelled',
|
'transfer_cancelled': 'Transfer cancelled',
|
||||||
@ -254,7 +246,6 @@ MESSAGES = {
|
|||||||
'response_write_error': 'Error writing response: {}',
|
'response_write_error': 'Error writing response: {}',
|
||||||
'consecutive_failures': 'Consecutive proxy failures detected for {}',
|
'consecutive_failures': 'Consecutive proxy failures detected for {}',
|
||||||
'invalid_proxy': 'Current proxy is invalid: {}',
|
'invalid_proxy': 'Current proxy is invalid: {}',
|
||||||
'proxy_switched': 'Switched from proxy {} to {}',
|
|
||||||
'whitelist_error': 'Failed to add whitelist: {}',
|
'whitelist_error': 'Failed to add whitelist: {}',
|
||||||
'api_mode_notice': 'Currently in API mode, proxy address will be automatically obtained upon request',
|
'api_mode_notice': 'Currently in API mode, proxy address will be automatically obtained upon request',
|
||||||
'server_running': 'Proxy server running at {}:{}',
|
'server_running': 'Proxy server running at {}:{}',
|
||||||
@ -273,14 +264,11 @@ MESSAGES = {
|
|||||||
2: Show all detailed information''',
|
2: Show all detailed information''',
|
||||||
'new_client_connect': 'New client connection - IP: {}, User: {}',
|
'new_client_connect': 'New client connection - IP: {}, User: {}',
|
||||||
'no_auth': 'No authentication',
|
'no_auth': 'No authentication',
|
||||||
'proxy_changed': 'Proxy changed: {} -> {}',
|
|
||||||
'connection_error': 'Connection handling error: {}',
|
'connection_error': 'Connection handling error: {}',
|
||||||
'cleanup_error': 'IP cleanup error: {}',
|
'cleanup_error': 'IP cleanup error: {}',
|
||||||
'port_changed': 'Port changed: {} -> {}, server restart required',
|
'port_changed': 'Port changed: {} -> {}, server restart required',
|
||||||
'config_updated': 'Server configuration updated',
|
'config_updated': 'Server configuration updated',
|
||||||
'load_proxy_file_error': 'Failed to load proxy file: {}',
|
'load_proxy_file_error': 'Failed to load proxy file: {}',
|
||||||
'manual_switch': 'Manual proxy switch: {} -> {}',
|
|
||||||
'auto_switch': 'Auto switch proxy: {} -> {}',
|
|
||||||
'proxy_check_result': 'Proxy check completed, valid proxies: {}',
|
'proxy_check_result': 'Proxy check completed, valid proxies: {}',
|
||||||
'no_proxy': 'No proxy',
|
'no_proxy': 'No proxy',
|
||||||
'cycle_mode': 'Cycle Mode',
|
'cycle_mode': 'Cycle Mode',
|
||||||
@ -479,29 +467,37 @@ DEFAULT_CONFIG = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def load_config(config_file='config/config.ini'):
|
def load_config(config_file='config/config.ini'):
|
||||||
config = ConfigParser()
|
try:
|
||||||
config.read(config_file, encoding='utf-8')
|
config = ConfigParser()
|
||||||
|
config.read(config_file, encoding='utf-8')
|
||||||
settings = {}
|
|
||||||
if config.has_section('Server'):
|
|
||||||
settings.update(dict(config.items('Server')))
|
|
||||||
|
|
||||||
config_dir = os.path.dirname(config_file)
|
if not config.has_section('Server'):
|
||||||
for key in ['proxy_file', 'whitelist_file', 'blacklist_file']:
|
config.add_section('Server')
|
||||||
if key in settings and settings[key]:
|
for key, value in DEFAULT_CONFIG.items():
|
||||||
settings[key] = os.path.join(config_dir, settings[key])
|
config.set('Server', key, str(value))
|
||||||
|
with open(config_file, 'w', encoding='utf-8') as f:
|
||||||
if config.has_section('DEFAULT'):
|
config.write(f)
|
||||||
settings.update(dict(config.items('DEFAULT')))
|
|
||||||
|
result = dict(config.items('Server'))
|
||||||
return {**DEFAULT_CONFIG, **settings}
|
|
||||||
|
# 添加用户配置
|
||||||
|
if config.has_section('Users'):
|
||||||
|
result['Users'] = dict(config.items('Users'))
|
||||||
|
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error loading config: {e}")
|
||||||
|
return DEFAULT_CONFIG.copy()
|
||||||
|
|
||||||
def load_ip_list(file_path):
|
def load_ip_list(file_path):
|
||||||
if not file_path or not os.path.exists(file_path):
|
try:
|
||||||
return set()
|
config_path = os.path.join('config', os.path.basename(file_path))
|
||||||
|
if os.path.exists(config_path):
|
||||||
with open(file_path, 'r') as f:
|
with open(config_path, 'r', encoding='utf-8') as f:
|
||||||
return {line.strip() for line in f if line.strip()}
|
return set(line.strip() for line in f if line.strip())
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error loading IP list: {e}")
|
||||||
|
return set()
|
||||||
|
|
||||||
_proxy_check_cache = {}
|
_proxy_check_cache = {}
|
||||||
_proxy_check_ttl = 10
|
_proxy_check_ttl = 10
|
||||||
@ -640,7 +636,7 @@ async def check_for_updates(language='cn'):
|
|||||||
match = re.search(r'<p>(ProxyCat-V\d+\.\d+\.\d+)</p>', content)
|
match = re.search(r'<p>(ProxyCat-V\d+\.\d+\.\d+)</p>', content)
|
||||||
if match:
|
if match:
|
||||||
latest_version = match.group(1)
|
latest_version = match.group(1)
|
||||||
CURRENT_VERSION = "ProxyCat-V2.0.0"
|
CURRENT_VERSION = "ProxyCat-V2.0.1"
|
||||||
if version.parse(latest_version.split('-V')[1]) > version.parse(CURRENT_VERSION.split('-V')[1]):
|
if version.parse(latest_version.split('-V')[1]) > version.parse(CURRENT_VERSION.split('-V')[1]):
|
||||||
print(f"{Fore.YELLOW}{get_message('new_version_found', language)} 当前版本: {CURRENT_VERSION}, 最新版本: {latest_version}{Style.RESET_ALL}")
|
print(f"{Fore.YELLOW}{get_message('new_version_found', language)} 当前版本: {CURRENT_VERSION}, 最新版本: {latest_version}{Style.RESET_ALL}")
|
||||||
print(f"{Fore.YELLOW}{get_message('visit_quark', language)}{Style.RESET_ALL}")
|
print(f"{Fore.YELLOW}{get_message('visit_quark', language)}{Style.RESET_ALL}")
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
colorama==0.4.6
|
colorama==0.4.6
|
||||||
httpx==0.27.2
|
httpx==0.27.2
|
||||||
|
httpx[http2,socks]==0.27.2
|
||||||
packaging==24.1
|
packaging==24.1
|
||||||
Requests==2.32.3
|
Requests==2.32.3
|
||||||
tqdm>=4.65.0
|
tqdm>=4.65.0
|
||||||
flask>=2.0.1
|
flask>=2.0.1
|
||||||
|
werkzeug>=2.0.0
|
25
web/static/css/animations.css
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
@keyframes slideIn {
|
||||||
|
from { transform: translateY(-20px); opacity: 0; }
|
||||||
|
to { transform: translateY(0); opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% { transform: scale(1); }
|
||||||
|
50% { transform: scale(1.05); }
|
||||||
|
100% { transform: scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-in {
|
||||||
|
animation: fadeInUp 0.3s ease;
|
||||||
|
}
|
48
web/static/css/base.css
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
:root {
|
||||||
|
--primary-color: #3498db;
|
||||||
|
--secondary-color: #2ecc71;
|
||||||
|
--warning-color: #f1c40f;
|
||||||
|
--danger-color: #e74c3c;
|
||||||
|
--background-color: #f5f6fa;
|
||||||
|
--text-color: #2c3e50;
|
||||||
|
--border-color: #dcdde1;
|
||||||
|
--card-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: var(--background-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: var(--card-shadow);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 20px;
|
||||||
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
color: var(--text-color);
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
border-bottom: 2px solid var(--border-color);
|
||||||
|
}
|
45
web/static/css/buttons.css
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
.btn {
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn i {
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success {
|
||||||
|
background-color: var(--secondary-color);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-warning {
|
||||||
|
background-color: var(--warning-color);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background-color: var(--danger-color);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
filter: brightness(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:active {
|
||||||
|
transform: translateY(1px);
|
||||||
|
}
|
61
web/static/css/dark-mode.css
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--background-color: #1a1a1a;
|
||||||
|
--text-color: #ffffff;
|
||||||
|
--border-color: #2d2d2d;
|
||||||
|
--card-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: var(--background-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: #2d2d2d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-card {
|
||||||
|
background: linear-gradient(145deg, #2d2d2d, #252525);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-card .card-title {
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-card .card-value {
|
||||||
|
color: #3498db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-card .card-footer {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
input, select, textarea {
|
||||||
|
background-color: #2d2d2d;
|
||||||
|
border-color: #3d3d3d;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus, select:focus, textarea:focus {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
background: linear-gradient(45deg, #64b5f6, #4a90e2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
}
|
210
web/static/css/forms.css
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
.form-control, .form-select {
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 2px solid #e9ecef;
|
||||||
|
padding: 10px 15px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:focus, .form-select:focus {
|
||||||
|
border-color: #4a90e2;
|
||||||
|
box-shadow: 0 0 0 0.2rem rgba(74, 144, 226, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
background: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table thead th {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-bottom: 2px solid #dee2e6;
|
||||||
|
color: #2c3e50;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table tbody td {
|
||||||
|
padding: 12px 15px;
|
||||||
|
vertical-align: middle;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.form-control, .form-select {
|
||||||
|
background-color: #2d2d2d;
|
||||||
|
color: #ffffff;
|
||||||
|
border-color: #3d3d3d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
background-color: #2d2d2d;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table thead th {
|
||||||
|
background-color: #3d3d3d;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 浮动保存按钮样式 */
|
||||||
|
.floating-save-container {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 30px;
|
||||||
|
right: 30px;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-save-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 30px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
background: linear-gradient(135deg, #4a90e2, #357abd);
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-save-btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-save-btn:active {
|
||||||
|
transform: translateY(1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-save-btn i {
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 暗色模式适配 */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.floating-save-btn {
|
||||||
|
background: linear-gradient(135deg, #2980b9, #2c3e50);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端适配 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.floating-save-container {
|
||||||
|
bottom: 20px;
|
||||||
|
right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-save-btn {
|
||||||
|
padding: 10px 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toast 样式优化 */
|
||||||
|
.toast {
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
min-width: 300px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
color: white;
|
||||||
|
transform: translateX(120%);
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
z-index: 1100;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.show {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 优化不同类型的 Toast 样式 */
|
||||||
|
.toast-success {
|
||||||
|
background: linear-gradient(135deg, #00b09b, #96c93d);
|
||||||
|
border-left: 4px solid #96c93d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-error {
|
||||||
|
background: linear-gradient(135deg, #ff5f6d, #ffc371);
|
||||||
|
border-left: 4px solid #ff5f6d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-info {
|
||||||
|
background: linear-gradient(135deg, #2193b0, #6dd5ed);
|
||||||
|
border-left: 4px solid #2193b0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-warning {
|
||||||
|
background: linear-gradient(135deg, #f2994a, #f2c94c);
|
||||||
|
border-left: 4px solid #f2994a;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toast 图标样式 */
|
||||||
|
.toast i {
|
||||||
|
font-size: 1.4em;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toast 堆叠效果 */
|
||||||
|
.toast + .toast {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toast 文本样式 */
|
||||||
|
.toast span {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 暗色模式适配 */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.toast {
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-success {
|
||||||
|
background: linear-gradient(135deg, #004d40, #1b5e20);
|
||||||
|
border-left: 4px solid #00c853;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-error {
|
||||||
|
background: linear-gradient(135deg, #b71c1c, #c62828);
|
||||||
|
border-left: 4px solid #ff1744;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-info {
|
||||||
|
background: linear-gradient(135deg, #0d47a1, #1976d2);
|
||||||
|
border-left: 4px solid #2196f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-warning {
|
||||||
|
background: linear-gradient(135deg, #e65100, #f57c00);
|
||||||
|
border-left: 4px solid #ffd600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端适配 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.toast {
|
||||||
|
min-width: auto;
|
||||||
|
width: calc(100% - 32px);
|
||||||
|
margin: 0 16px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast + .toast {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
}
|
107
web/static/css/logs.css
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
.log-container {
|
||||||
|
height: 500px;
|
||||||
|
position: relative;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-INFO {
|
||||||
|
background-color: #e8f5e9;
|
||||||
|
color: #1b5e20;
|
||||||
|
border-left: 6px solid #2e7d32;
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-WARNING {
|
||||||
|
background-color: #fff3e0;
|
||||||
|
color: #e65100;
|
||||||
|
border-left: 6px solid #f57c00;
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-ERROR {
|
||||||
|
background-color: #ffebee;
|
||||||
|
color: #b71c1c;
|
||||||
|
border-left: 6px solid #c62828;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-CRITICAL {
|
||||||
|
background-color: #ffebee;
|
||||||
|
color: #7f0000;
|
||||||
|
border-left: 6px solid #b71c1c;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 900;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-message {
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 搜索高亮样式 */
|
||||||
|
.log-highlight {
|
||||||
|
background-color: #fff176;
|
||||||
|
color: #000000;
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 暗色模式下的高亮样式 */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.log-highlight {
|
||||||
|
background-color: #ffd600;
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-INFO {
|
||||||
|
background-color: rgba(232, 245, 233, 0.1);
|
||||||
|
color: #81c784;
|
||||||
|
border-left-color: #2e7d32;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-WARNING {
|
||||||
|
background-color: rgba(255, 243, 224, 0.1);
|
||||||
|
color: #ffb74d;
|
||||||
|
border-left-color: #f57c00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-ERROR {
|
||||||
|
background-color: rgba(255, 235, 238, 0.1);
|
||||||
|
color: #e57373;
|
||||||
|
border-left-color: #c62828;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-CRITICAL {
|
||||||
|
background-color: rgba(255, 235, 238, 0.1);
|
||||||
|
color: #ef5350;
|
||||||
|
border-left-color: #b71c1c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 日志条目样式 */
|
||||||
|
.log-entry {
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entry:hover {
|
||||||
|
filter: brightness(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-time {
|
||||||
|
font-family: monospace;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-level {
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0 4px;
|
||||||
|
}
|
33
web/static/css/nav-tabs.css
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
.nav-tabs {
|
||||||
|
border-bottom: 2px solid #dee2e6;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tabs .nav-link {
|
||||||
|
border: none;
|
||||||
|
color: #6c757d;
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tabs .nav-link:hover {
|
||||||
|
border: none;
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tabs .nav-link.active {
|
||||||
|
border: none;
|
||||||
|
color: var(--primary-color);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tabs .nav-link.active:after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: -2px;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
}
|
44
web/static/css/progress.css
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
.progress-container {
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress {
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: rgba(0, 0, 0, 0.05);
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
background: linear-gradient(45deg, #4a90e2, #357abd);
|
||||||
|
box-shadow: 0 0 10px rgba(74, 144, 226, 0.5);
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
rgba(255, 255, 255, 0) 0%,
|
||||||
|
rgba(255, 255, 255, 0.3) 50%,
|
||||||
|
rgba(255, 255, 255, 0) 100%
|
||||||
|
);
|
||||||
|
animation: shimmer 1.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
}
|
41
web/static/css/responsive.css
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
@media (max-width: 768px) {
|
||||||
|
.status-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-responsive {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-container {
|
||||||
|
height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-search input {
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 10px 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-switch {
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
z-index: 1000;
|
||||||
|
background: white;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: var(--card-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.language-switch {
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
}
|
||||||
|
}
|
44
web/static/css/search.css
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
.log-search {
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-search input {
|
||||||
|
padding: 12px 45px;
|
||||||
|
border-radius: 25px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
border: 2px solid #e9ecef;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-search input:focus {
|
||||||
|
border-color: #4a90e2;
|
||||||
|
box-shadow: 0 0 0 3px rgba(74, 144, 226, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-search .clear-search {
|
||||||
|
position: absolute;
|
||||||
|
right: 15px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #6c757d;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 5px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-search .clear-search:hover {
|
||||||
|
color: #dc3545;
|
||||||
|
transform: translateY(-50%) scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-search i {
|
||||||
|
position: absolute;
|
||||||
|
left: 15px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
80
web/static/css/service-control.css
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
.service-control {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-control .card-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-title {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-color);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-description {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-actions {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-btn {
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 8px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-btn .btn-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-btn:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
filter: brightness(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-btn:active {
|
||||||
|
transform: translateY(1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.service-actions {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-btn {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 暗色模式适配 */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.service-title {
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-description {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
}
|
827
web/static/css/status-card.css
Normal file
@ -0,0 +1,827 @@
|
|||||||
|
.status-cards {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 25px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 统一卡片基础样式 */
|
||||||
|
.card.status-card, .card.service-control {
|
||||||
|
height: 150px;
|
||||||
|
background: linear-gradient(165deg, #ffffff, #f8f9fa);
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.8);
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card.status-card:hover, .card.service-control:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 统一卡片标题样式 */
|
||||||
|
.card-title, .service-title {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-color);
|
||||||
|
margin-bottom: 0.7rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title i, .service-title i {
|
||||||
|
font-size: 1.1em;
|
||||||
|
color: var(--primary-color);
|
||||||
|
background: rgba(52, 152, 219, 0.1);
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 统一卡片内容样式 */
|
||||||
|
.card-text {
|
||||||
|
color: var(--text-color);
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 0.7rem;
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 统一按钮容器样式 */
|
||||||
|
.button-container, .service-actions, .progress-container {
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 统一按钮样式 */
|
||||||
|
.btn-sm, .service-btn {
|
||||||
|
padding: 6px 10px;
|
||||||
|
height: 32px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 4px 6px rgba(50, 50, 93, 0.11);
|
||||||
|
border: none;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
color: white;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm i, .service-btn i {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按钮颜色统一为蓝色 */
|
||||||
|
.btn-primary,
|
||||||
|
.service-btn.start-btn,
|
||||||
|
.service-btn.stop-btn,
|
||||||
|
.service-btn.restart-btn {
|
||||||
|
background: linear-gradient(135deg, #3498db, #2980b9);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按钮悬停效果 */
|
||||||
|
.btn-primary:hover,
|
||||||
|
.service-btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 7px 14px rgba(50, 50, 93, 0.18);
|
||||||
|
filter: brightness(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按钮点击效果 */
|
||||||
|
.btn-primary:active,
|
||||||
|
.service-btn:active {
|
||||||
|
transform: translateY(1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 服务控制按钮组样式 */
|
||||||
|
.service-actions {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 进度条样式 */
|
||||||
|
.progress {
|
||||||
|
height: 5px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: rgba(0, 0, 0, 0.05);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
background: linear-gradient(90deg, var(--primary-color), #5dade2);
|
||||||
|
box-shadow: 0 0 10px rgba(52, 152, 219, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡片图标样式 */
|
||||||
|
.card-icon {
|
||||||
|
position: absolute;
|
||||||
|
top: 15px;
|
||||||
|
right: 15px;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: var(--primary-color);
|
||||||
|
opacity: 0.15;
|
||||||
|
transform: scale(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 暗色模式适配 */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.card.status-card, .card.service-control {
|
||||||
|
background: linear-gradient(165deg, #2d2d2d, #252525);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title, .service-title {
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title i, .service-title i {
|
||||||
|
background: rgba(52, 152, 219, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-text {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-icon {
|
||||||
|
color: var(--primary-color);
|
||||||
|
opacity: 0.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm, .service-btn {
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm:hover, .service-btn:hover {
|
||||||
|
box-shadow: 0 7px 14px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式布局 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.status-cards {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card.status-card, .card.service-control {
|
||||||
|
height: auto;
|
||||||
|
min-height: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-actions {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm, .service-btn {
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 服务控制卡片特殊样式 */
|
||||||
|
.service-control .card-body {
|
||||||
|
padding: 0;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-control .service-title {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-actions {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 服务控制按钮样式优化 */
|
||||||
|
.service-btn {
|
||||||
|
padding: 6px 10px;
|
||||||
|
min-height: 32px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-btn .btn-content {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-btn i {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 添加信息卡片样式 */
|
||||||
|
.info-card {
|
||||||
|
background: linear-gradient(165deg, #ffffff, #f8f9fa);
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.8);
|
||||||
|
padding: 10px;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 地址容器样式 */
|
||||||
|
.address-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
background: rgba(255, 255, 255, 0.5);
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
|
min-height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-color);
|
||||||
|
min-width: 80px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-label i {
|
||||||
|
color: var(--primary-color);
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-value {
|
||||||
|
flex-grow: 1;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-color);
|
||||||
|
padding: 2px 4px;
|
||||||
|
background: rgba(0, 0, 0, 0.03);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--primary-color);
|
||||||
|
padding: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-btn:hover {
|
||||||
|
background: rgba(52, 152, 219, 0.1);
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-btn.copy-success {
|
||||||
|
color: #2ecc71;
|
||||||
|
background: rgba(46, 204, 113, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 关于信息样式 */
|
||||||
|
.about-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
background: rgba(255, 255, 255, 0.5);
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
|
min-height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-item i {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
width: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-content {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.github-link {
|
||||||
|
color: var(--primary-color);
|
||||||
|
text-decoration: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.github-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wechat-name {
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 暗色模式适配 */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.info-card {
|
||||||
|
background: linear-gradient(165deg, #2d2d2d, #252525);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-item, .about-item {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-value {
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-btn:hover {
|
||||||
|
background: rgba(52, 152, 219, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wechat-name {
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式布局 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.info-card {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
min-height: auto;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-item, .about-item {
|
||||||
|
padding: 6px 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡片标题样式优化 */
|
||||||
|
.info-card .card-title {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
margin-bottom: 0.6rem;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card .card-title i {
|
||||||
|
font-size: 1rem;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 背景图标优化 */
|
||||||
|
.info-card .card-icon {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
transform: scale(1.8);
|
||||||
|
top: 12px;
|
||||||
|
right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 标签页卡片统一样式 */
|
||||||
|
.tab-pane .card {
|
||||||
|
background: linear-gradient(165deg, #ffffff, #f8f9fa);
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.8);
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-pane .card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 标签页标题样式 */
|
||||||
|
.tab-pane .card-title {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-color);
|
||||||
|
margin-bottom: 1.2rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-pane .card-title i {
|
||||||
|
font-size: 1.2em;
|
||||||
|
color: var(--primary-color);
|
||||||
|
background: rgba(52, 152, 219, 0.1);
|
||||||
|
padding: 6px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 标签页背景图标 */
|
||||||
|
.tab-pane .card-icon {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: var(--primary-color);
|
||||||
|
opacity: 0.15;
|
||||||
|
transform: scale(2.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按钮统一样式 */
|
||||||
|
.tab-pane .btn {
|
||||||
|
padding: 8px 16px;
|
||||||
|
height: 36px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 4px 6px rgba(50, 50, 93, 0.11);
|
||||||
|
border: none;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
color: white;
|
||||||
|
white-space: nowrap;
|
||||||
|
background: linear-gradient(135deg, #3498db, #2980b9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-pane .btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 7px 14px rgba(50, 50, 93, 0.18);
|
||||||
|
filter: brightness(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 暗色模式适配 */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.tab-pane .card {
|
||||||
|
background: linear-gradient(165deg, #2d2d2d, #252525);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-pane .card-title {
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-pane .card-title i {
|
||||||
|
background: rgba(52, 152, 219, 0.15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 代理管理标签页样式 */
|
||||||
|
.proxy-management {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.proxy-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.proxy-actions .btn {
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 120px;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.proxy-actions .btn i {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* IP名单标签页样式 */
|
||||||
|
.ip-lists-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ip-lists-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ip-list-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ip-list-title {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-color);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ip-auth-settings {
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding-top: 1rem;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ip-lists-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ip-lists-actions .btn {
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 120px;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按钮悬停效果 */
|
||||||
|
.proxy-actions .btn:hover,
|
||||||
|
.ip-lists-actions .btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 暗色模式适配 */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.ip-list-title {
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ip-auth-settings {
|
||||||
|
border-top-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 代理列表区域样式 */
|
||||||
|
.proxy-lists {
|
||||||
|
background: rgba(255, 255, 255, 0.5);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 15px;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.proxy-list-section {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.proxy-list-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.proxy-list-header i {
|
||||||
|
color: var(--primary-color);
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.proxy-list-help {
|
||||||
|
margin-left: auto;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #666;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.proxy-list-section textarea {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
resize: vertical;
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.proxy-list-section textarea:focus {
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 暗色模式适配 */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.proxy-lists {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.proxy-list-header {
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.proxy-list-help {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.proxy-list-section textarea {
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.proxy-list-section textarea:focus {
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 地址显示样式 */
|
||||||
|
.address-value {
|
||||||
|
font-family: monospace;
|
||||||
|
background: rgba(0, 0, 0, 0.05);
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--text-color);
|
||||||
|
flex-grow: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 暗色模式适配 */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.address-value {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-btn:hover {
|
||||||
|
background: rgba(52, 152, 219, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-btn.copy-success {
|
||||||
|
background: rgba(46, 204, 113, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 服务状态样式 */
|
||||||
|
.service-status {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot.running {
|
||||||
|
background-color: #2ecc71;
|
||||||
|
box-shadow: 0 0 10px rgba(46, 204, 113, 0.5);
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot.stopped {
|
||||||
|
background-color: #e74c3c;
|
||||||
|
box-shadow: 0 0 10px rgba(231, 76, 60, 0.5);
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-color);
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 添加脉冲动画 */
|
||||||
|
@keyframes pulse {
|
||||||
|
0% { transform: scale(1); opacity: 1; }
|
||||||
|
50% { transform: scale(1.2); opacity: 0.8; }
|
||||||
|
100% { transform: scale(1); opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 服务控制卡片的按钮容器 */
|
||||||
|
.status-card .service-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 服务控制按钮基础样式 */
|
||||||
|
.status-card .service-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: white;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
min-width: 70px;
|
||||||
|
height: 32px;
|
||||||
|
background: linear-gradient(135deg, #3498db, #2980b9);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 服务控制按钮交互效果 */
|
||||||
|
.status-card .service-btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
filter: brightness(1.1);
|
||||||
|
box-shadow: 0 4px 12px rgba(52, 152, 219, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-card .service-btn:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
background: linear-gradient(135deg, #95a5a6, #7f8c8d);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式布局 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.status-card .service-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-card .service-btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 添加过渡效果 */
|
||||||
|
.status-text {
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 优化按钮状态过渡 */
|
||||||
|
.service-btn {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-btn:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
BIN
web/static/images/logo.png
Normal file
After Width: | Height: | Size: 363 KiB |