This commit is contained in:
本间白猫 2025-04-01 15:03:42 +08:00
parent d207a3bb8f
commit 086d37aa73
12 changed files with 830 additions and 354 deletions

View File

@ -1,5 +1,11 @@
# ProxyCat 使用手册
## 重要事项
- Python版本最好为Python 3.11
- Releases中为较为稳定的打包版本不一定是最新
- API 接口所获取的代理地址必须为 IP:PORT 格式且只提供一条地址
## 源码使用及 Docker 部署
### 源码手册
@ -52,6 +58,7 @@ getip_url = 获取代理地址的 API 接口
```
python ProxyCat.py
python app.py (Web控制管理-推荐方式)
```
![Run](./Operation%20Manual.assets/Run.png)
@ -90,6 +97,7 @@ docker logs proxycat
# 0: 仅显示代理切换和错误信息
# 1: 显示代理切换、倒计时和错误信息
# 2: 显示所有详细信息
# 仅终端管理时生效
display_level = 1
# 本地服务器监听端口(默认为:1080)
@ -99,8 +107,8 @@ port = 1080
# Web 管理页面端口(默认为:5000)
web_port = 5000
# 代理地址轮换模式cycle 表示循环使用,custom 表示使用自定义模式,load_balance 表示负载均衡(默认为:cycle)
# Proxy rotation mode: cycle means cyclic use, custom means custom mode, load_balance means load balancing (default:cycle)
# 代理地址轮换模式cycle 表示循环使用loadbalance 表示负载均衡(默认为:cycle)
# Proxy rotation mode: cycle means cyclic use, loadbalance means load balancing (default:cycle)
mode = cycle
# 代理地址更换时间(秒),设置为 0 时每次请求都更换 IP(默认为:300)

View File

@ -1,3 +1,20 @@
### 2025/03/23
- 修复负载均衡模式无法调用的BUG
- 修复socks5连接错误
- 修复http、socks5监听下的目标网站错误和代理地址失效情况一致导致无法正常触发代理切换
- 修改代理有效性校验,配置为可控检测,关闭后将不会进行有效性检查避免特殊情况一直切换
- 修复并发下导致大规模触发更换和提示的问题,锁定操作的原子性
- 修复大量细节逻辑、描述错误
- 当前代理切换触发条件为时间间隔到期切换、代理失效自动切换、Web手动切换、API下首次请求自动获取
### 2025/03/17
- 修复目标站点本身错误时会触发代理切换的错误逻辑
- 修改连接关闭方式
- 优化监听服务器性能
- 修复多处错误BUG
### 2025/03/14
- 修复'_last_used'报错问题,连接关闭方式修正

View File

@ -70,7 +70,7 @@ def update_status(server):
time.sleep(1)
continue
if server.mode == 'load_balance':
if server.mode == 'loadbalance':
if display_level >= 1:
print_proxy_info()
time.sleep(5)
@ -155,7 +155,7 @@ async def run_server(server):
async def run_proxy_check(server):
if server.config.get('check_proxies', 'False').lower() == 'true':
logging.info(get_message('proxy_check_start', server.language))
valid_proxies = await check_proxies(server.proxies)
valid_proxies = await check_proxies(server.proxies, server.test_url)
if valid_proxies:
server.proxies = valid_proxies
server.proxy_cycle = cycle(valid_proxies)
@ -187,6 +187,11 @@ class ProxyCat:
if hasattr(socket, 'SO_KEEPALIVE'):
socket.SO_KEEPALIVE = True
if hasattr(socket, 'SO_REUSEADDR'):
socket.SO_REUSEADDR = True
if hasattr(socket, 'SO_REUSEPORT') and os.name != 'nt':
socket.SO_REUSEPORT = True
if os.name != 'nt':
import resource
soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE)
@ -225,11 +230,17 @@ class ProxyCat:
async def start_server(self):
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
if hasattr(socket, 'SO_REUSEPORT') and os.name != 'nt':
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
sock.bind((self.config.get('SERVER', 'host'), int(self.config.get('SERVER', 'port'))))
server = await asyncio.start_server(
self.handle_client,
self.config.get('SERVER', 'host'),
self.config.get('SERVER', 'port')
sock=sock
)
logging.info(get_message('server_running', self.language,
self.config.get('SERVER', 'host'),
self.config.get('SERVER', 'port')))
@ -396,6 +407,16 @@ class ProxyCat:
except Exception as e:
logging.error(get_message('data_transfer_error', self.language, e))
def monitor_resources(self):
import psutil
process = psutil.Process(os.getpid())
while self.running:
mem_info = process.memory_info()
logging.debug(f"Memory usage: {mem_info.rss / 1024 / 1024:.2f} MB, "
f"Connections: {len(self.tasks)}")
time.sleep(60)
if __name__ == '__main__':
setup_logging()
parser = argparse.ArgumentParser(description=logos())
@ -413,6 +434,9 @@ if __name__ == '__main__':
status_thread = threading.Thread(target=update_status, args=(server,), daemon=True)
status_thread.start()
cleanup_thread = threading.Thread(target=lambda: asyncio.run(server.cleanup_clients()), daemon=True)
cleanup_thread.start()
try:
asyncio.run(run_server(server))
except KeyboardInterrupt:

87
app.py
View File

@ -119,37 +119,26 @@ def get_status():
else:
current_proxy = get_message('no_proxy', server.language)
time_left = server.time_until_next_switch()
if server.mode == 'loadbalance':
if time_left == float('inf'):
time_left = -1
return jsonify({
'current_proxy': current_proxy,
'mode': server.mode,
'port': int(server_config.get('port', '1080')),
'interval': server.interval,
'time_left': server.time_until_next_switch(),
'time_left': time_left,
'total_proxies': len(server.proxies) if hasattr(server, 'proxies') else 0,
'use_getip': server.use_getip,
'getip_url': getattr(server, 'getip_url', '') if getattr(server, 'use_getip', False) else '',
'auth_required': server.auth_required,
'display_level': int(config.get('DEFAULT', 'display_level', fallback='1')),
'display_level': int(server_config.get('display_level', '1')),
'service_status': 'running' if server.running else 'stopped',
'config': {
'port': server_config.get('port', ''),
'mode': server_config.get('mode', 'cycle'),
'interval': server_config.get('interval', ''),
'username': server_config.get('username', ''),
'password': server_config.get('password', ''),
'use_getip': server_config.get('use_getip', 'False'),
'getip_url': server_config.get('getip_url', ''),
'proxy_username': server_config.get('proxy_username', ''),
'proxy_password': server_config.get('proxy_password', ''),
'proxy_file': server_config.get('proxy_file', ''),
'check_proxies': server_config.get('check_proxies', 'False'),
'language': server_config.get('language', 'cn'),
'whitelist_file': server_config.get('whitelist_file', ''),
'blacklist_file': server_config.get('blacklist_file', ''),
'ip_auth_priority': server_config.get('ip_auth_priority', 'whitelist'),
'display_level': config.get('DEFAULT', 'display_level', fallback='1'),
'raw_content': config_content
}
'config': server_config
})
@app.route('/api/config', methods=['POST'])
@ -158,6 +147,8 @@ def save_config():
new_config = request.get_json()
current_config = load_config('config/config.ini')
port_changed = str(new_config.get('port', '')) != str(current_config.get('port', ''))
mode_changed = new_config.get('mode', '') != current_config.get('mode', '')
use_getip_changed = (new_config.get('use_getip', 'False').lower() == 'true') != (current_config.get('use_getip', 'False').lower() == 'true')
config_parser = ConfigParser()
config_parser.read('config/config.ini', encoding='utf-8')
@ -172,12 +163,27 @@ def save_config():
with open('config/config.ini', 'w', encoding='utf-8') as f:
config_parser.write(f)
old_mode = server.mode
old_use_getip = server.use_getip
server.config = load_config('config/config.ini')
server._init_config_values(server.config)
if mode_changed or use_getip_changed:
server._handle_mode_change()
if new_config.get('mode') == 'loadbalance':
server.last_switch_time = time.time()
server.last_switch_attempt = 0
return jsonify({
'status': 'success',
'port_changed': port_changed
'port_changed': port_changed,
'service_status': 'running' if server.running else 'stopped'
})
except Exception as e:
@ -324,35 +330,8 @@ def clear_logs():
@require_token
def switch_proxy():
try:
if server.use_getip:
from config.getip import newip
try:
old_proxy = server.current_proxy
new_proxy = newip()
server.current_proxy = new_proxy
server.last_switch_time = time.time()
server._log_proxy_switch(old_proxy, new_proxy)
return jsonify({
'status': 'success',
'current_proxy': server.current_proxy,
'message': get_message('switch_success', server.language)
})
except Exception as e:
return jsonify({
'status': 'error',
'message': get_message('get_proxy_failed', server.language, str(e))
})
else:
if not server.proxies:
server.proxies = server._load_file_proxies()
if server.proxies:
server.proxy_cycle = cycle(server.proxies)
if server.proxy_cycle:
old_proxy = server.current_proxy
server.current_proxy = next(server.proxy_cycle)
server.last_switch_time = time.time()
server._log_proxy_switch(old_proxy, server.current_proxy)
result = asyncio.run(server.switch_proxy())
if result:
return jsonify({
'status': 'success',
'current_proxy': server.current_proxy,
@ -361,7 +340,7 @@ def switch_proxy():
else:
return jsonify({
'status': 'error',
'message': get_message('no_proxies_available', server.language)
'message': get_message('switch_failed', server.language, 'Proxy switch not needed or in cooldown')
})
except Exception as e:
return jsonify({
@ -523,7 +502,7 @@ def check_version():
original_level = httpx_logger.level
httpx_logger.setLevel(logging.WARNING)
CURRENT_VERSION = "ProxyCat-V2.0.2"
CURRENT_VERSION = "ProxyCat-V2.0.4"
try:
client = httpx.Client(transport=httpx.HTTPTransport(retries=3))
@ -630,7 +609,7 @@ if __name__ == '__main__':
web_port = int(config.get('web_port', '5000'))
web_url = f"http://127.0.0.1:{web_port}"
if config.get('token'):
web_url += f"?token={config.get('token')}"
web_url += f"/web?token={config.get('token')}"
logging.info(get_message('web_panel_url', server.language, web_url))
logging.info(get_message('web_panel_notice', server.language))

View File

@ -4,12 +4,12 @@ port = 1080
web_port = 5000
mode = cycle
interval = 300
use_getip = False
use_getip = false
getip_url = http://example.com/getip
proxy_username =
proxy_password =
proxy_file = ip.txt
check_proxies = True
check_proxies = true
test_url =
language = cn
whitelist_file = whitelist.txt
@ -19,4 +19,5 @@ token = honmashironeko
[Users]
neko = 123456
k = 123

View File

@ -1,2 +1,2 @@
socks5://127.0.0.1:7890
http://127.0.0.1:7890
socks5://127.0.0.1:7890

View File

@ -28,6 +28,9 @@ MESSAGES = {
'proxy_switch': '切换代理: {} -> {}',
'proxy_consecutive_fails': '代理 {} 连续失败 {} 次,正在切换新代理',
'proxy_invalid': '代理 {} 无效,立即切换代理',
'proxy_failure': '代理检测失败: {}',
'proxy_failure_threshold': '代理无效,开始切换',
'proxy_check_error': '代理检查时发生错误: {}',
'connection_timeout': '连接超时',
'data_transfer_timeout': '数据传输超时,正在重试...',
'connection_reset': '连接被重置',
@ -49,7 +52,7 @@ MESSAGES = {
'blog': '博客',
'proxy_mode': '代理轮换模式',
'cycle': '循环',
'load_balance': '负载均衡',
'loadbalance': '负载均衡',
'single_round': '单轮',
'proxy_interval': '代理更换时间',
'default_auth': '默认账号密码',
@ -105,7 +108,7 @@ MESSAGES = {
'proxy_check_result': '代理检查完成,有效代理:{}',
'no_proxy': '无代理',
'cycle_mode': '循环模式',
'load_balance_mode': '负载均衡模式',
'loadbalance_mode': '负载均衡模式',
'proxy_check_start': '开始检查代理...',
'proxy_check_complete': '代理检查完成',
'proxy_save_success': '代理保存成功',
@ -181,6 +184,11 @@ MESSAGES = {
'web_panel_url': '网页控制面板地址: {}',
'web_panel_notice': '请使用浏览器访问上述地址来管理代理服务器',
'api_proxy_settings_title': 'API代理设置',
'all_retries_failed': '所有重试均已失败,最后错误: {}',
'proxy_get_failed': '获取代理失败',
'proxy_get_error': '获取代理错误: {}',
'request_error': '请求错误: {}',
'proxy_switch_error': '代理切换错误: {}',
},
'en': {
'getting_new_proxy': 'Getting new proxy IP',
@ -193,6 +201,9 @@ MESSAGES = {
'proxy_switch': 'Switch proxy: {} -> {}',
'proxy_consecutive_fails': 'Proxy {} failed {} times consecutively, switching to new proxy',
'proxy_invalid': 'Proxy {} is invalid, switching proxy immediately',
'proxy_failure': 'Proxy check failed: {}',
'proxy_failure_threshold': 'Proxy invalid, switching now',
'proxy_check_error': 'Error occurred during proxy check: {}',
'connection_timeout': 'Connection timeout',
'data_transfer_timeout': 'Data transfer timeout, retrying...',
'connection_reset': 'Connection reset',
@ -214,7 +225,7 @@ MESSAGES = {
'blog': 'Blog',
'proxy_mode': 'Proxy Rotation Mode',
'cycle': 'Cycle',
'load_balance': 'Load Balance',
'loadbalance': 'Load Balance',
'single_round': 'Single Round',
'proxy_interval': 'Proxy Change Interval',
'default_auth': 'Default Username and Password',
@ -248,37 +259,40 @@ MESSAGES = {
'invalid_proxy': 'Current proxy is invalid: {}',
'whitelist_error': 'Failed to add whitelist: {}',
'api_mode_notice': 'Currently in API mode, proxy address will be automatically obtained upon request',
'all_retries_failed': 'All retries failed, last error: {}',
'proxy_get_failed': 'Failed to get proxy',
'proxy_get_error': 'Error getting proxy: {}',
'proxy_switch_error': 'Error switching proxy: {}',
'server_running': 'Proxy server running at {}:{}',
'server_start_error': 'Server startup error: {}',
'server_shutting_down': 'Shutting down server...',
'client_process_error': 'Error processing client request: {}',
'server_shutting_down': 'Server shutting down...',
'client_process_error': 'Client processing error: {}',
'request_handling_error': 'Request handling error: {}',
'proxy_forward_error': 'Proxy forwarding error: {}',
'data_transfer_timeout': '{} data transfer timeout',
'data_transfer_error': '{} data transfer error: {}',
'data_transfer_timeout': '{}data transfer timeout',
'status_update_error': 'Status update error',
'display_level_notice': 'Current display level: {}',
'display_level_desc': '''Display level description:
0: Show only proxy switches and errors
1: Show proxy switches, countdown and errors
0: Only show proxy switch and error messages
1: Show proxy switch, countdown and error messages
2: Show all detailed information''',
'new_client_connect': 'New client connection - IP: {}, User: {}',
'no_auth': 'No authentication',
'connection_error': 'Connection handling error: {}',
'cleanup_error': 'IP cleanup error: {}',
'port_changed': 'Port changed: {} -> {}, server restart required',
'cleanup_error': 'Cleanup IP error: {}',
'port_changed': 'Port changed: {} -> {}, server restart required to take effect',
'config_updated': 'Server configuration updated',
'load_proxy_file_error': 'Failed to load proxy file: {}',
'proxy_check_result': 'Proxy check completed, valid proxies: {}',
'no_proxy': 'No proxy',
'cycle_mode': 'Cycle Mode',
'load_balance_mode': 'Load Balance Mode',
'cycle_mode': 'Cycle mode',
'loadbalance_mode': 'Load balance mode',
'proxy_check_start': 'Starting proxy check...',
'proxy_check_complete': 'Proxy check completed',
'proxy_save_success': 'Proxies saved successfully',
'proxy_save_failed': 'Failed to save proxies: {}',
'ip_list_save_success': 'IP lists saved successfully',
'ip_list_save_failed': 'Failed to save IP lists: {}',
'proxy_save_success': 'Proxy saved successfully',
'proxy_save_failed': 'Failed to save proxy: {}',
'ip_list_save_success': 'IP list saved successfully',
'ip_list_save_failed': 'Failed to save IP list: {}',
'switch_success': 'Proxy switched successfully',
'switch_failed': 'Failed to switch proxy: {}',
'service_start_success': 'Service started successfully',
@ -305,20 +319,20 @@ MESSAGES = {
'language_label': 'Language',
'chinese': 'Chinese',
'english': 'English',
'manual_switch_btn': 'Manual Switch',
'manual_switch_btn': 'Switch Manually',
'service_control_title': 'Service Control',
'language_switch_success': '',
'language_switch_failed': '',
'language_switch_success': 'Language switched successfully',
'language_switch_failed': 'Failed to switch language',
'refresh_failed': 'Failed to refresh data: {}',
'auth_username_label': 'Auth Username',
'auth_password_label': 'Auth Password',
'proxy_auth_username_label': 'Proxy Auth Username',
'proxy_auth_password_label': 'Proxy Auth Password',
'auth_username_label': 'Authentication Username',
'auth_password_label': 'Authentication Password',
'proxy_auth_username_label': 'Proxy Authentication Username',
'proxy_auth_password_label': 'Proxy Authentication Password',
'progress_bar_label': 'Switch Progress',
'proxy_settings_title': 'Proxy Settings',
'config_save_success': 'Configuration saved successfully',
'config_save_failed': 'Failed to save configuration: {}',
'config_restart_required': 'Configuration changed, server restart required',
'config_restart_required': 'Configuration changed, server restart required to take effect',
'confirm_restart_service': 'Restart server now?',
'service_status': 'Service Status',
'running': 'Running',
@ -329,10 +343,10 @@ MESSAGES = {
'service_stop_failed': 'Failed to stop service: {}',
'service_restart_failed': 'Failed to restart service: {}',
'invalid_token': 'Invalid access token',
'config_file_changed': 'Configuration file change detected, reloading...',
'config_file_changed': 'Config file change detected, reloading...',
'proxy_file_changed': 'Proxy file changed, reloading...',
'test_target_label': 'Test Target URL',
'invalid_test_target': 'Invalid test target URL',
'test_target_label': 'Test Target Address',
'invalid_test_target': 'Invalid test target address',
'users_save_success': 'Users saved successfully',
'users_save_failed': 'Failed to save users: {}',
'user_management_title': 'User Management',
@ -340,13 +354,13 @@ MESSAGES = {
'password_column': 'Password',
'actions_column': 'Actions',
'add_user_btn': 'Add User',
'enter_username': 'Enter username',
'enter_password': 'Enter password',
'enter_username': 'Please enter username',
'enter_password': 'Please enter password',
'confirm_delete_user': 'Are you sure you want to delete this user?',
'no_logs_found': 'No matching logs found',
'clear_search': 'Clear Search',
'web_panel_url': 'Web control panel URL: {}',
'web_panel_notice': 'Please use a browser to visit the above URL to manage the proxy server',
'web_panel_url': 'Web panel URL: {}',
'web_panel_notice': 'Please use a browser to access the above URL to manage the proxy server',
'api_proxy_settings_title': 'API Proxy Settings',
}
}
@ -376,7 +390,7 @@ def print_banner(config):
banner_info = [
(get_message('public_account', language), '樱花庄的本间白猫'),
(get_message('blog', language), 'https://y.shironekosan.cn'),
(get_message('proxy_mode', language), get_message('cycle', language) if config.get('mode') == 'cycle' else get_message('load_balance', language) if config.get('mode') == 'load_balance' else get_message('single_round', language)),
(get_message('proxy_mode', language), get_message('cycle', language) if config.get('mode') == 'cycle' else get_message('loadbalance', language) if config.get('mode') == 'loadbalance' else get_message('single_round', language)),
(get_message('proxy_interval', language), f"{config.get('interval')}{get_message('seconds', language)}"),
(get_message('default_auth', language), auth_info),
(get_message('local_http', language), http_addr),
@ -542,10 +556,9 @@ async def check_http_proxy(proxy, test_url=None):
except:
return False
async def check_https_proxy(proxy, test_url=None):
return await check_http_proxy(proxy, test_url)
async def check_socks_proxy(proxy, test_url=None):
if test_url is None:
test_url = 'https://www.baidu.com'
protocol, auth, host, port = parse_proxy(proxy)
if not all([host, port]):
return False
@ -605,7 +618,7 @@ async def check_proxy(proxy, test_url=None):
proxy_type = proxy.split('://')[0]
check_funcs = {
'http': check_http_proxy,
'https': check_https_proxy,
'https': check_http_proxy,
'socks5': check_socks_proxy
}
@ -637,7 +650,7 @@ async def check_for_updates(language='cn'):
match = re.search(r'<p>(ProxyCat-V\d+\.\d+\.\d+)</p>', content)
if match:
latest_version = match.group(1)
CURRENT_VERSION = "ProxyCat-V2.0.2"
CURRENT_VERSION = "ProxyCat-V2.0.4"
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('visit_quark', language)}{Style.RESET_ALL}")

View File

@ -25,6 +25,7 @@ class AsyncProxyServer:
self._init_config_values(config)
self._init_server_state()
self._init_connection_settings()
self.proxy_failure_lock = asyncio.Lock()
def _init_config_values(self, config):
self.port = int(config.get('port', '1080'))
@ -61,11 +62,11 @@ class AsyncProxyServer:
self.switch_cooldown = 5
self.proxy_check_cache = {}
self.last_check_time = {}
self.proxy_check_ttl = 300
self.proxy_check_ttl = 60
self.check_cooldown = 10
self.max_fail_count = 3
self.proxy_fail_count = 0
self.connected_clients = set()
self.last_proxy_failure_time = 0
self.proxy_failure_cooldown = 3
def _init_server_state(self):
self.running = False
@ -73,7 +74,6 @@ class AsyncProxyServer:
self.server_instance = None
self.tasks = set()
self.last_switch_time = time.time()
self.proxy_failed = False
self.proxy_cycle = None
self.current_proxy = None
self.proxies = []
@ -89,36 +89,82 @@ class AsyncProxyServer:
self.buffer_size = 8192
self.connection_timeout = 30
self.read_timeout = 60
self.max_concurrent_requests = 50
self.max_concurrent_requests = 1000
self.request_semaphore = asyncio.Semaphore(self.max_concurrent_requests)
self.connection_pool = {}
self.max_pool_size = 100
self.max_pool_size = 500
self.client_pool = {}
self.client_pool_lock = asyncio.Lock()
self.proxy_pool = {}
self.active_connections = set()
def _update_config_values(self, new_config):
self._init_config_values(new_config)
self.last_switch_time = time.time()
self.last_switch_attempt = 0
def _handle_mode_change(self):
self.last_switch_attempt = 0
if self.use_getip:
self.proxies = []
self.proxy_cycle = None
self.current_proxy = None
logging.info(get_message('api_mode_notice', self.language))
else:
logging.info(f"切换到{'负载均衡' if self.mode == 'loadbalance' else '循环模式'}模式,从 {self.proxy_file} 加载代理列表")
self.proxies = self._load_file_proxies()
logging.info(f"加载到 {len(self.proxies)} 个代理")
if self.proxies:
self.proxy_cycle = cycle(self.proxies)
self.current_proxy = next(self.proxy_cycle)
if self.check_proxies:
asyncio.run(self._check_proxies())
logging.info(f"当前使用代理: {self.current_proxy}")
if self.check_proxies and self.mode != 'loadbalance':
try:
loop = asyncio.get_event_loop()
if loop.is_running():
asyncio.create_task(self._check_proxies_wrapper())
else:
loop.run_until_complete(self._check_proxies())
except Exception as e:
logging.error(f"检查代理时出错: {str(e)}")
else:
logging.error(f"从文件 {self.proxy_file} 加载代理失败,请检查文件是否存在且包含有效代理")
async def _check_proxies_wrapper(self):
"""包装 _check_proxies 方法,用于在已运行的事件循环中调用"""
await self._check_proxies()
def _reload_proxies(self):
self.last_switch_attempt = 0
logging.info(f"重新加载代理列表文件 {self.proxy_file}")
self.proxies = self._load_file_proxies()
logging.info(f"加载到 {len(self.proxies)} 个代理")
if self.proxies:
self.proxy_cycle = cycle(self.proxies)
self.current_proxy = next(self.proxy_cycle)
logging.info(f"当前使用代理: {self.current_proxy}")
if self.check_proxies:
asyncio.run(self._check_proxies())
try:
loop = asyncio.get_event_loop()
if loop.is_running():
asyncio.create_task(self._check_proxies_wrapper())
else:
loop.run_until_complete(self._check_proxies())
except Exception as e:
logging.error(f"检查代理时出错: {str(e)}")
else:
logging.error(f"从文件 {self.proxy_file} 加载代理失败,请检查文件是否存在且包含有效代理")
async def _check_proxies(self):
from modules.modules import check_proxies
@ -148,15 +194,44 @@ class AsyncProxyServer:
self.running = True
try:
self.server_instance = await asyncio.start_server(
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 1024 * 1024)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 1024 * 1024)
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
sock.bind(('0.0.0.0', self.port))
loop = asyncio.get_event_loop()
if hasattr(loop, 'set_default_executor'):
import concurrent.futures
executor = concurrent.futures.ThreadPoolExecutor(max_workers=max(32, os.cpu_count() * 4))
loop.set_default_executor(executor)
server = await asyncio.start_server(
self.handle_client,
'0.0.0.0',
self.port
sock=sock,
backlog=2048,
limit=32768,
)
self.server_instance = server
logging.info(get_message('server_running', self.language, '0.0.0.0', self.port))
async with self.server_instance:
await self.server_instance.serve_forever()
self.tasks.add(asyncio.create_task(self.cleanup_clients()))
self.tasks.add(asyncio.create_task(self._cleanup_pool()))
self.tasks.add(asyncio.create_task(self.cleanup_disconnected_ips()))
if hasattr(os, 'sched_setaffinity'):
try:
os.sched_setaffinity(0, range(os.cpu_count()))
except:
pass
async with server:
await server.serve_forever()
except Exception as e:
if not self.stop_server:
logging.error(get_message('server_start_error', self.language, str(e)))
@ -185,12 +260,32 @@ class AsyncProxyServer:
try:
current_time = time.time()
if self.interval != 0 and (self.switching_proxy or (current_time - self.last_switch_attempt < self.switch_cooldown)):
if self.mode == 'loadbalance' and self.proxies:
if not self.switching_proxy:
try:
self.switching_proxy = True
self.last_switch_attempt = current_time
if not self.use_getip:
if not self.proxy_cycle:
self.proxy_cycle = cycle(self.proxies)
self.current_proxy = next(self.proxy_cycle)
logging.info(f"负载均衡模式选择代理: {self.current_proxy}")
else:
await self.get_proxy()
finally:
self.switching_proxy = False
return self.current_proxy
if (self.use_getip and (not self.current_proxy or
current_time - self.last_switch_time >= self.interval)) or \
(not self.use_getip and self.interval == 0):
if self.switching_proxy or (current_time - self.last_switch_attempt < self.switch_cooldown):
return self.current_proxy
if self.interval > 0 and current_time - self.last_switch_time >= self.interval or \
(self.use_getip and not self.current_proxy):
try:
self.switching_proxy = True
self.last_switch_attempt = current_time
@ -221,7 +316,7 @@ class AsyncProxyServer:
return valid_proxies[0]
def time_until_next_switch(self):
return float('inf') if self.mode == 'load_balance' else max(0, self.interval - (time.time() - self.last_switch_time))
return float('inf') if self.mode == 'loadbalance' else max(0, self.interval - (time.time() - self.last_switch_time))
def check_ip_auth(self, ip):
try:
@ -270,9 +365,25 @@ class AsyncProxyServer:
return False
async def _close_connection(self, writer):
try:
if writer and not writer.is_closing():
writer.write_eof()
await writer.drain()
writer.close()
try:
await writer.wait_closed()
except Exception:
pass
except Exception:
pass
async def handle_client(self, reader, writer):
task = asyncio.current_task()
self.tasks.add(task)
peername = writer.get_extra_info('peername')
if peername:
self.active_connections.add(peername)
try:
peername = writer.get_extra_info('peername')
if peername:
@ -295,25 +406,37 @@ class AsyncProxyServer:
except Exception as e:
logging.error(get_message('client_handle_error', self.language, e))
finally:
try:
writer.close()
await writer.wait_closed()
except:
pass
if peername:
self.active_connections.discard(peername)
await self._close_connection(writer)
self.tasks.remove(task)
async def _pipe(self, reader, writer):
try:
while True:
try:
data = await reader.read(self.buffer_size)
if not data:
break
try:
writer.write(data)
await writer.drain()
except (ConnectionError, ConnectionResetError):
await self.handle_proxy_failure()
break
except (ConnectionError, ConnectionResetError):
await self.handle_proxy_failure()
break
except asyncio.CancelledError:
pass
except Exception as e:
logging.error(get_message('data_transfer_error', self.language, e))
await self.handle_proxy_failure()
pass
finally:
await self._close_connection(writer)
def _split_proxy_auth(self, proxy_addr):
match = re.match(r'((?P<username>.+?):(?P<password>.+?)@)?(?P<host>.+)', proxy_addr)
@ -338,7 +461,12 @@ class AsyncProxyServer:
else:
proxy_url = f"{proxy_type}://{proxy_addr}"
client = httpx.AsyncClient(
import logging as httpx_logging
httpx_logging.getLogger("httpx").setLevel(logging.WARNING)
httpx_logging.getLogger("hpack").setLevel(logging.WARNING)
httpx_logging.getLogger("h2").setLevel(logging.WARNING)
return httpx.AsyncClient(
proxies={"all://": proxy_url},
limits=httpx.Limits(
max_keepalive_connections=100,
@ -347,10 +475,9 @@ class AsyncProxyServer:
),
timeout=30.0,
http2=True,
verify=False
verify=False,
follow_redirects=True
)
client._last_used = time.time()
return client
async def _cleanup_connections(self):
current_time = time.time()
@ -423,7 +550,7 @@ class AsyncProxyServer:
dst_port = struct.unpack('!H', await reader.readexactly(2))[0]
max_retries = 3
max_retries = 1
retry_count = 0
last_error = None
@ -456,7 +583,6 @@ class AsyncProxyServer:
self._pipe(remote_reader, writer)
)
self.proxy_failed = False
return
except (asyncio.TimeoutError, ConnectionRefusedError, ConnectionResetError) as e:
@ -477,8 +603,8 @@ class AsyncProxyServer:
await asyncio.sleep(1)
continue
if last_error:
logging.error(get_message('all_retries_failed', self.language, str(last_error)))
#if last_error:
#logging.error(get_message('all_retries_failed', self.language, str(last_error)))
writer.write(b'\x05\x01\x00\x01\x00\x00\x00\x00\x00\x00')
await writer.drain()
@ -600,6 +726,8 @@ class AsyncProxyServer:
async def _handle_client_impl(self, reader, writer, first_byte):
try:
peername = writer.get_extra_info('peername')
client_info = f"{peername[0]}:{peername[1]}" if peername else "未知客户端"
if peername:
client_ip = peername[0]
if not self.check_ip_auth(client_ip):
@ -614,7 +742,7 @@ class AsyncProxyServer:
try:
method, path, _ = request_line.decode('utf-8', errors='ignore').split()
except ValueError:
except (ValueError, UnicodeDecodeError) as e:
return
headers = {}
@ -651,10 +779,14 @@ class AsyncProxyServer:
else:
await self._handle_request(method, path, headers, reader, writer)
except (ConnectionError, ConnectionResetError, ConnectionAbortedError):
return
except asyncio.CancelledError:
raise
return
except Exception as e:
logging.error(get_message('client_request_error', self.language, e))
if not isinstance(e, (ConnectionError, ConnectionResetError, ConnectionAbortedError,
asyncio.CancelledError, asyncio.TimeoutError)):
logging.error(get_message('client_request_error', self.language, str(e)))
async def _handle_connect(self, path, reader, writer):
try:
@ -665,6 +797,12 @@ class AsyncProxyServer:
await writer.drain()
return
max_retries = 1
retry_count = 0
last_error = None
while retry_count < max_retries:
try:
proxy = await self.get_next_proxy()
if not proxy:
writer.write(b'HTTP/1.1 503 Service Unavailable\r\n\r\n')
@ -692,6 +830,14 @@ class AsyncProxyServer:
await remote_writer.drain()
response = await remote_reader.readline()
if not response.startswith(b'HTTP/1.1 200'):
await self.handle_proxy_failure()
last_error = f"Bad Gateway: {response.decode('utf-8', errors='ignore')}"
retry_count += 1
if retry_count < max_retries:
logging.warning(get_message('request_retry', self.language, max_retries - retry_count))
await asyncio.sleep(1)
continue
raise Exception("Bad Gateway")
while (await remote_reader.readline()) != b'\r\n':
pass
@ -702,6 +848,14 @@ class AsyncProxyServer:
remote_writer.write(b'\x05\x01\x00\x03' + len(host).to_bytes(1, 'big') + host.encode() + port.to_bytes(2, 'big'))
await remote_writer.drain()
if (await remote_reader.read(10))[1] != 0:
await self.handle_proxy_failure()
last_error = "SOCKS5 connection failed"
retry_count += 1
if retry_count < max_retries:
logging.warning(get_message('request_retry', self.language, max_retries - retry_count))
await asyncio.sleep(1)
continue
raise Exception("Bad Gateway")
else:
raise Exception("Unsupported proxy type")
@ -713,22 +867,58 @@ class AsyncProxyServer:
self._pipe(reader, remote_writer),
self._pipe(remote_reader, writer)
)
return
except asyncio.TimeoutError:
await self.handle_proxy_failure()
last_error = "Connection Timeout"
retry_count += 1
if retry_count < max_retries:
logging.warning(get_message('request_retry', self.language, max_retries - retry_count))
await asyncio.sleep(1)
continue
logging.error(get_message('connect_timeout', self.language))
writer.write(b'HTTP/1.1 504 Gateway Timeout\r\n\r\n')
await writer.drain()
return
except Exception as e:
logging.error(get_message('proxy_invalid_switch', self.language))
await self.handle_proxy_failure()
last_error = str(e)
retry_count += 1
if retry_count < max_retries:
logging.warning(get_message('request_retry', self.language, max_retries - retry_count))
await asyncio.sleep(1)
continue
writer.write(b'HTTP/1.1 502 Bad Gateway\r\n\r\n')
await writer.drain()
if not self.proxy_failed:
self.proxy_failed = True
await self.get_proxy()
else:
self.proxy_failed = False
return
except Exception as e:
last_error = str(e)
retry_count += 1
if retry_count < max_retries:
logging.warning(get_message('request_retry', self.language, max_retries - retry_count))
await asyncio.sleep(1)
continue
writer.write(b'HTTP/1.1 502 Bad Gateway\r\n\r\n')
await writer.drain()
return
#if last_error:
#logging.error(get_message('all_retries_failed', self.language, last_error))
async def _handle_request(self, method, path, headers, reader, writer):
async with self.request_semaphore:
max_retries = 1
retry_count = 0
last_error = None
while retry_count < max_retries:
try:
proxy = await self.get_next_proxy()
if not proxy:
@ -736,7 +926,8 @@ class AsyncProxyServer:
await writer.drain()
return
key = f"{proxy}:{path}"
try:
client = await self._get_client(proxy)
proxy_headers = headers.copy()
proxy_type, proxy_addr = proxy.split('://')
@ -745,79 +936,228 @@ class AsyncProxyServer:
auth_header = f'Basic {base64.b64encode(auth.encode()).decode()}'
proxy_headers['Proxy-Authorization'] = auth_header
if key in self.connection_pool:
client = self.connection_pool[key]
else:
client = await self._create_client(proxy)
self.connection_pool[key] = client
try:
async with client.stream(
method,
path,
headers=proxy_headers,
content=reader,
timeout=30.0
) as response:
writer.write(f'HTTP/1.1 {response.status_code} {response.reason_phrase}\r\n'.encode())
for header_name, header_value in response.headers.items():
if header_name.lower() != 'transfer-encoding':
if header_name.lower() not in ('transfer-encoding', 'connection'):
writer.write(f'{header_name}: {header_value}\r\n'.encode())
writer.write(b'\r\n')
try:
async for chunk in response.aiter_bytes(chunk_size=self.buffer_size):
if not chunk:
break
try:
writer.write(chunk)
if len(chunk) >= self.buffer_size:
await writer.drain()
except (ConnectionError, ConnectionResetError, ConnectionAbortedError):
return
except Exception:
break
await writer.drain()
except (ConnectionError, ConnectionResetError, ConnectionAbortedError):
return
except Exception:
pass
return
except httpx.RequestError:
await self.handle_proxy_failure()
last_error = "Request Error"
retry_count += 1
if retry_count < max_retries:
logging.warning(get_message('request_retry', self.language, max_retries - retry_count))
await asyncio.sleep(1)
continue
return
except Exception as e:
logging.error(get_message('request_handling_error', self.language, str(e)))
if isinstance(e, (ConnectionError, ConnectionResetError, ConnectionAbortedError)):
await self.handle_proxy_failure()
last_error = str(e)
retry_count += 1
if retry_count < max_retries:
logging.warning(get_message('request_retry', self.language, max_retries - retry_count))
await asyncio.sleep(1)
continue
return
writer.write(b'HTTP/1.1 502 Bad Gateway\r\n\r\n')
await writer.drain()
finally:
await self._cleanup_connections()
return
except httpx.HTTPError:
await self.handle_proxy_failure()
last_error = "HTTP Error"
retry_count += 1
if retry_count < max_retries:
logging.warning(get_message('request_retry', self.language, max_retries - retry_count))
await asyncio.sleep(1)
continue
return
except Exception as e:
if isinstance(e, (ConnectionError, ConnectionResetError, ConnectionAbortedError)):
await self.handle_proxy_failure()
last_error = str(e)
retry_count += 1
if retry_count < max_retries:
logging.warning(get_message('request_retry', self.language, max_retries - retry_count))
await asyncio.sleep(1)
continue
return
writer.write(b'HTTP/1.1 502 Bad Gateway\r\n\r\n')
await writer.drain()
return
except Exception as e:
if isinstance(e, (ConnectionError, ConnectionResetError, ConnectionAbortedError)):
await self.handle_proxy_failure()
last_error = str(e)
retry_count += 1
if retry_count < max_retries:
logging.warning(get_message('request_retry', self.language, max_retries - retry_count))
await asyncio.sleep(1)
continue
return
if not isinstance(e, (asyncio.CancelledError,)):
logging.error(f"请求处理错误: {str(e)}")
try:
writer.write(b'HTTP/1.1 502 Bad Gateway\r\n\r\n')
await writer.drain()
except:
pass
return
#if last_error:
logging.error(get_message('all_retries_failed', self.language, last_error))
async def _get_client(self, proxy):
async with self.client_pool_lock:
current_time = time.time()
if proxy in self.client_pool:
client, last_used = self.client_pool[proxy]
if current_time - last_used < 30 and not client.is_closed:
self.client_pool[proxy] = (client, current_time)
return client
else:
await client.aclose()
del self.client_pool[proxy]
try:
client = await self._create_client(proxy)
if len(self.client_pool) >= self.max_pool_size:
oldest_proxy = min(self.client_pool, key=lambda x: self.client_pool[x][1])
old_client, _ = self.client_pool[oldest_proxy]
await old_client.aclose()
del self.client_pool[oldest_proxy]
self.client_pool[proxy] = (client, current_time)
return client
except Exception as e:
logging.error(f"创建客户端失败: {str(e)}")
raise
async def handle_proxy_failure(self):
if not self.check_proxies:
return
current_time = time.time()
if current_time - self.last_proxy_failure_time < self.proxy_failure_cooldown:
return
if self.switching_proxy:
return
try:
if not self.proxy_failure_lock.locked():
async with self.proxy_failure_lock:
if (current_time - self.last_proxy_failure_time < self.proxy_failure_cooldown or
self.switching_proxy):
return
self.last_proxy_failure_time = current_time
try:
is_valid = await self.check_current_proxy()
if not is_valid:
#logging.warning(get_message('proxy_failure', self.language, self.current_proxy))
await self.switch_proxy()
except Exception as e:
logging.error(get_message('proxy_check_error', self.language, str(e)))
except Exception as e:
logging.error(f"代理失败处理出错: {str(e)}")
async def switch_proxy(self):
try:
current_time = time.time()
if self.switching_proxy or (current_time - self.last_switch_attempt < self.switch_cooldown):
return
self.proxy_fail_count += 1
if current_time - self.last_switch_attempt < self.switch_cooldown:
return False
if self.switching_proxy:
return False
if self.proxy_fail_count >= self.max_fail_count:
current_proxy = self.current_proxy if self.current_proxy else get_message('no_proxy', self.language)
logging.warning(get_message('proxy_consecutive_fails', self.language,
current_proxy, self.proxy_fail_count))
try:
self.switching_proxy = True
self.last_switch_attempt = current_time
old_proxy = current_proxy
old_proxy = self.current_proxy
temp_current_proxy = self.current_proxy
await self.get_proxy()
self.proxy_fail_count = 0
self.proxy_failed = False
finally:
self.switching_proxy = False
else:
current_proxy = self.current_proxy if self.current_proxy else get_message('no_proxy', self.language)
logging.warning(get_message('request_retry', self.language,
self.max_fail_count - self.proxy_fail_count))
if temp_current_proxy != self.current_proxy:
self._log_proxy_switch(old_proxy, self.current_proxy)
self.last_proxy_failure_time = current_time
return True
return False
except Exception as e:
current_proxy = self.current_proxy if self.current_proxy else get_message('no_proxy', self.language)
logging.error(get_message('proxy_invalid', self.language, current_proxy))
logging.error(get_message('proxy_switch_error', self.language, str(e)))
return False
finally:
self.switching_proxy = False
async def check_current_proxy(self):
try:
proxy = self.current_proxy
if not proxy:
return False
current_time = time.time()
if not self.check_proxies:
return True
if proxy in self.last_check_time:
if current_time - self.last_check_time[proxy] < self.check_cooldown:
return self.proxy_check_cache.get(proxy, (current_time, True))[1]
@ -828,21 +1168,25 @@ class AsyncProxyServer:
return is_valid
self.last_check_time[proxy] = current_time
test_url = self.config.get('test_url', 'https://www.baidu.com')
proxy_type = proxy.split('://')[0]
async with httpx.AsyncClient(
proxies={f"{proxy_type}://": proxy},
timeout=10,
verify=False
) as client:
response = await client.get(test_url)
is_valid = response.status_code == 200
try:
from modules.modules import check_proxy
is_valid = await check_proxy(proxy, test_url)
logging.warning(f"代理检查结果: {proxy} - {'有效' if is_valid else '无效'}")
except Exception as e:
logging.error(f"代理检测错误: {proxy} - {str(e)}")
is_valid = False
self.proxy_check_cache[proxy] = (current_time, is_valid)
return is_valid
except Exception:
except Exception as e:
logging.error(f"代理检测异常: {str(e)}")
if 'proxy' in locals():
self.proxy_check_cache[proxy] = (current_time, False)
return False
@ -860,39 +1204,40 @@ class AsyncProxyServer:
}
def initialize_proxies(self):
if self.mode == 'cycle':
if hasattr(self, 'proxies') and self.proxies:
self.proxy_cycle = cycle(self.proxies)
elif self.use_getip:
pass
else:
return
if self.use_getip:
logging.info("API模式将在请求时动态获取代理")
return
try:
with open(self.proxy_file, 'r') as f:
self.proxies = [line.strip() for line in f if line.strip()]
logging.info(f"从文件 {self.proxy_file} 加载代理列表")
self.proxies = self._load_file_proxies()
logging.info(f"加载到 {len(self.proxies)} 个代理")
if self.proxies:
self.proxy_cycle = cycle(self.proxies)
self.current_proxy = next(self.proxy_cycle)
logging.info(f"初始代理: {self.current_proxy}")
except Exception as e:
logging.error(get_message('load_proxy_file_error', self.language, str(e)))
logging.error(f"初始化代理列表失败: {str(e)}")
async def cleanup_disconnected_ips(self):
while True:
try:
active_ips = set()
for client_info in self.get_active_connections():
active_ips.add(client_info[0])
active_ips = {addr[0] for addr in self.active_connections}
self.connected_clients = active_ips
except Exception as e:
logging.error(get_message('cleanup_error', self.language, str(e)))
await asyncio.sleep(30)
def is_docker():
return os.path.exists('/.dockerenv')
async def get_proxy_status(self):
if self.mode == 'load_balance':
if self.mode == 'loadbalance':
return f"{get_message('current_proxy', self.language)}: {self.current_proxy}"
else:
time_left = self.time_until_next_switch()
@ -979,8 +1324,17 @@ class AsyncProxyServer:
async def _cleanup_pool(self):
while True:
try:
def is_expired(conn):
return hasattr(conn, 'is_closed') and conn.is_closed
to_remove = []
for proxy, conn in list(self.proxy_pool.items()):
if conn.is_closed:
if is_expired(conn):
to_remove.append(proxy)
for proxy in to_remove:
if proxy in self.proxy_pool:
del self.proxy_pool[proxy]
except Exception as e:
logging.error(f'连接池清理错误: {e}')
@ -990,12 +1344,24 @@ class AsyncProxyServer:
if old_proxy != new_proxy:
old_proxy = old_proxy if old_proxy else get_message('no_proxy', self.language)
new_proxy = new_proxy if new_proxy else get_message('no_proxy', self.language)
current_time = time.time()
if not hasattr(self, '_last_log_time') or \
not hasattr(self, '_last_log_content') or \
current_time - self._last_log_time > 1 or \
self._last_log_content != f"{old_proxy} -> {new_proxy}":
logging.info(get_message('proxy_switch', self.language, old_proxy, new_proxy))
self._last_log_time = current_time
self._last_log_content = f"{old_proxy} -> {new_proxy}"
async def _validate_proxy(self, proxy):
if not proxy:
return False
if not self.check_proxies:
return True
try:
if not validate_proxy(proxy):
logging.warning(get_message('proxy_invalid', self.language, proxy))
@ -1027,6 +1393,7 @@ class AsyncProxyServer:
async def get_proxy(self):
try:
old_proxy = self.current_proxy
temp_current_proxy = self.current_proxy
if not self.use_getip and self.proxies:
if not self.proxy_cycle:
@ -1037,6 +1404,7 @@ class AsyncProxyServer:
if await self._validate_proxy(new_proxy):
self.current_proxy = new_proxy
self.last_switch_time = time.time()
if temp_current_proxy != self.current_proxy:
self._log_proxy_switch(old_proxy, self.current_proxy)
return self.current_proxy
@ -1047,8 +1415,10 @@ class AsyncProxyServer:
try:
new_proxy = await self._load_getip_proxy()
if new_proxy and await self._validate_proxy(new_proxy):
self.current_proxy = new_proxy
self.last_switch_time = time.time()
if temp_current_proxy != self.current_proxy:
self._log_proxy_switch(old_proxy, self.current_proxy)
return self.current_proxy
else:
@ -1061,3 +1431,36 @@ class AsyncProxyServer:
except Exception as e:
logging.error(get_message('proxy_get_error', self.language, str(e)))
return self.current_proxy
async def cleanup_clients(self):
while True:
try:
async with self.client_pool_lock:
current_time = time.time()
expired_proxies = [
proxy for proxy, (_, last_used) in self.client_pool.items()
if current_time - last_used > 30
]
for proxy in expired_proxies:
client, _ = self.client_pool[proxy]
await client.aclose()
del self.client_pool[proxy]
except Exception as e:
logging.error(f"清理客户端池错误: {str(e)}")
await asyncio.sleep(30)
def get_active_connections(self):
active = []
for task in self.tasks:
if not task.done():
try:
coro = task.get_coro()
if coro.__qualname__.startswith('AsyncProxyServer.handle_client'):
writer = coro.cr_frame.f_locals.get('writer')
if writer:
peername = writer.get_extra_info('peername')
if peername:
active.append(peername)
except Exception:
continue
return active

View File

@ -6,3 +6,5 @@ Requests==2.32.3
tqdm>=4.65.0
flask>=2.0.1
werkzeug>=2.0.0
asyncio>=3.4.3
configparser>=5.0.0

7
web/static/js/bootstrap.bundle.min.js vendored Normal file

File diff suppressed because one or more lines are too long

2
web/static/js/jquery.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -1329,7 +1329,7 @@
<label class="form-label" data-i18n="run_mode_label">运行模式</label>
<select class="form-select" name="mode">
<option value="cycle" data-i18n="cycle_mode">循环模式</option>
<option value="load_balance" data-i18n="load_balance_mode">负载均衡</option>
<option value="loadbalance" data-i18n="loadbalance_mode">负载均衡</option>
</select>
</div>
<div class="mb-3">
@ -1375,7 +1375,7 @@
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="check_proxies" id="check-proxies">
<label class="form-check-label" for="check-proxies" data-i18n="enable_proxy_check_label">启用代理检测</label>
<label class="form-check-label" for="check-proxies" data-i18n="enable_proxy_check_label">代理有效性检测</label>
</div>
</div>
</form>
@ -1590,8 +1590,8 @@
<div class="notification-container" id="notification-container"></div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js"></script>
<script src="/static/js/bootstrap.bundle.min.js"></script>
<script src="/static/js/jquery.min.js"></script>
<script>
let lastLogId = 0;
let searchTimeout = null;
@ -1630,7 +1630,13 @@
$.get(appendToken('/api/status'), function(data) {
// 只在值发生变化时更新显示
const currentProxy = $('#current-proxy').text();
const newProxy = data.current_proxy || translations[currentLanguage].no_proxy;
let newProxy = data.current_proxy || translations[currentLanguage].no_proxy;
// 负载均衡模式特殊处理
if (data.mode === 'loadbalance') {
newProxy = translations[currentLanguage].loadbalance_mode || '负载均衡模式';
}
if (currentProxy !== newProxy) {
$('#current-proxy').text(newProxy);
}
@ -1651,7 +1657,11 @@
}
// 更新进度条和其他状态...
if (data.interval && data.time_left !== undefined) {
if (data.mode === 'loadbalance') {
// 负载均衡模式不显示倒计时
$('#next-switch').text(translations[currentLanguage].loadbalance_mode || '负载均衡模式');
$('.progress-bar').css('width', '100%');
} else if (data.interval && data.time_left !== undefined) {
const timeLeft = Math.max(0, Math.ceil(data.time_left));
$('#next-switch').text(`${timeLeft} ${translations[currentLanguage].seconds}`);
@ -1673,13 +1683,20 @@
function updateCountdown() {
$.get(appendToken('/api/status'), function(data) {
if (data.time_left !== undefined && data.interval) {
if (data.time_left !== undefined) {
// 负载均衡模式特殊处理
if (data.time_left === -1 || data.mode === 'loadbalance') {
// 负载均衡模式不显示倒计时
$('.progress-bar').css('width', '100%');
$('#next-switch').text(translations[currentLanguage]['loadbalance_mode'] || '负载均衡模式');
} else if (data.interval) {
const timeLeft = Math.max(0, Math.ceil(data.time_left));
const progress = Math.min(100, ((data.interval - data.time_left) / data.interval) * 100);
$('.progress-bar').css('width', progress + '%');
$('#next-switch').text(timeLeft + ' ' + translations[currentLanguage]['seconds']);
}
}
});
}
@ -1688,31 +1705,34 @@
if (data.config) {
const config = data.config;
// 更新表单值
$('input[name="port"]').val(config.port);
$('select[name="mode"]').val(config.mode);
$('input[name="interval"]').val(config.interval);
$('input[name="username"]').val(config.username);
$('input[name="password"]').val(config.password);
// 更新表单值 - 始终使用配置文件中的值避免空值
$('input[name="port"]').val(config.port || '1080');
$('select[name="mode"]').val(config.mode || 'cycle');
$('input[name="interval"]').val(config.interval || '300');
$('input[name="username"]').val(config.username || '');
$('input[name="password"]').val(config.password || '');
// 修正checkbox的状态设置
$('input[name="use_getip"]').prop('checked', config.use_getip.toLowerCase() === 'true');
$('input[name="getip_url"]').val(config.getip_url);
$('input[name="proxy_username"]').val(config.proxy_username);
$('input[name="proxy_password"]').val(config.proxy_password);
$('input[name="proxy_file"]').val(config.proxy_file);
$('input[name="check_proxies"]').prop('checked', config.check_proxies.toLowerCase() === 'true');
$('select[name="language"]').val(config.language);
$('input[name="whitelist_file"]').val(config.whitelist_file);
$('input[name="blacklist_file"]').val(config.blacklist_file);
$('select[name="ip_auth_priority"]').val(config.ip_auth_priority);
$('select[name="display_level"]').val(config.display_level);
$('input[name="use_getip"]').prop('checked', (config.use_getip || '').toLowerCase() === 'true');
$('input[name="getip_url"]').val(config.getip_url || '');
$('input[name="proxy_username"]').val(config.proxy_username || '');
$('input[name="proxy_password"]').val(config.proxy_password || '');
$('input[name="proxy_file"]').val(config.proxy_file || 'ip.txt');
$('input[name="check_proxies"]').prop('checked', (config.check_proxies || '').toLowerCase() === 'true');
$('select[name="language"]').val(config.language || 'cn');
$('input[name="whitelist_file"]').val(config.whitelist_file || '');
$('input[name="blacklist_file"]').val(config.blacklist_file || '');
$('select[name="ip_auth_priority"]').val(config.ip_auth_priority || 'whitelist');
$('select[name="display_level"]').val(config.display_level || '1');
// 根据 use_getip 的状态更新 getip_url 输入框的禁用状态
const useGetip = config.use_getip.toLowerCase() === 'true';
const useGetip = (config.use_getip || '').toLowerCase() === 'true';
$('input[name="getip_url"]').prop('disabled', !useGetip);
// 更新其他相关UI元素
updateUIState();
// 更新地址显示
updateAddresses(config);
}
});
}
@ -2146,7 +2166,7 @@
'switch_interval': '切换间隔(秒)',
'run_mode_text': '运行模式',
'cycle_mode': '循环模式',
'load_balance_mode': '负载均衡',
'loadbalance_mode': '负载均衡',
'language_text': '语言',
'auth_config': '认证配置',
'username': '用户名',
@ -2196,11 +2216,11 @@
'switch_interval_label': '切换间隔(秒)',
'run_mode_label': '运行模式',
'cycle_mode': '循环模式',
'load_balance_mode': '负载均衡',
'loadbalance_mode': '负载均衡',
'auth_config_title': '认证配置',
'use_api_label': '使用API获取代理',
'api_url_label': 'API地址',
'enable_proxy_check_label': '启用代理检测',
'enable_proxy_check_label': '代理有效性检测',
'proxy_list_label': '代理列表',
'save_proxy_btn': '保存代理',
'check_proxy_btn': '检测代理',
@ -2301,11 +2321,11 @@
'loading': '加载中...',
'manual_switch_btn': '手动切换',
'cycle_mode': '循环模式',
'load_balance_mode': '负载均衡',
'loadbalance_mode': '负载均衡',
'next_switch': '下次切换',
'proxy_mode': {
'cycle': '循环模式',
'load_balance': '负载均衡'
'loadbalance': '负载均衡'
},
'service_action': {
'start_success': '服务启动成功',
@ -2373,7 +2393,7 @@
'switch_interval': 'Switch Interval(s)',
'run_mode_text': 'Run Mode',
'cycle_mode': 'Cycle Mode',
'load_balance_mode': 'Load Balance',
'loadbalance_mode': 'Load Balance',
'language_text': 'Language',
'auth_config': 'Auth Config',
'username': 'Username',
@ -2424,11 +2444,11 @@
'switch_interval_label': 'Switch Interval(s)',
'run_mode_label': 'Run Mode',
'cycle_mode': 'Cycle Mode',
'load_balance_mode': 'Load Balance',
'loadbalance_mode': 'Load Balance',
'auth_config_title': 'Authentication',
'use_api_label': 'Use API for Proxy',
'api_url_label': 'API URL',
'enable_proxy_check_label': 'Enable Proxy Check',
'enable_proxy_check_label': 'Proxy Validity Check',
'proxy_list_label': 'Proxy List',
'save_proxy_btn': 'Save Proxies',
'check_proxy_btn': 'Check Proxies',