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

View File

@ -70,7 +70,7 @@ def update_status(server):
time.sleep(1) time.sleep(1)
continue continue
if server.mode == 'load_balance': if server.mode == 'loadbalance':
if display_level >= 1: if display_level >= 1:
print_proxy_info() print_proxy_info()
time.sleep(5) time.sleep(5)
@ -155,7 +155,7 @@ async def run_server(server):
async def run_proxy_check(server): async def run_proxy_check(server):
if server.config.get('check_proxies', 'False').lower() == 'true': if server.config.get('check_proxies', 'False').lower() == 'true':
logging.info(get_message('proxy_check_start', server.language)) 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: if valid_proxies:
server.proxies = valid_proxies server.proxies = valid_proxies
server.proxy_cycle = cycle(valid_proxies) server.proxy_cycle = cycle(valid_proxies)
@ -187,6 +187,11 @@ class ProxyCat:
if hasattr(socket, 'SO_KEEPALIVE'): if hasattr(socket, 'SO_KEEPALIVE'):
socket.SO_KEEPALIVE = True 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': if os.name != 'nt':
import resource import resource
soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE) soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE)
@ -225,11 +230,17 @@ class ProxyCat:
async def start_server(self): async def start_server(self):
try: 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( server = await asyncio.start_server(
self.handle_client, self.handle_client,
self.config.get('SERVER', 'host'), sock=sock
self.config.get('SERVER', 'port')
) )
logging.info(get_message('server_running', self.language, logging.info(get_message('server_running', self.language,
self.config.get('SERVER', 'host'), self.config.get('SERVER', 'host'),
self.config.get('SERVER', 'port'))) self.config.get('SERVER', 'port')))
@ -396,6 +407,16 @@ class ProxyCat:
except Exception as e: except Exception as e:
logging.error(get_message('data_transfer_error', self.language, 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__': if __name__ == '__main__':
setup_logging() setup_logging()
parser = argparse.ArgumentParser(description=logos()) 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 = threading.Thread(target=update_status, args=(server,), daemon=True)
status_thread.start() status_thread.start()
cleanup_thread = threading.Thread(target=lambda: asyncio.run(server.cleanup_clients()), daemon=True)
cleanup_thread.start()
try: try:
asyncio.run(run_server(server)) asyncio.run(run_server(server))
except KeyboardInterrupt: except KeyboardInterrupt:

103
app.py
View File

@ -119,37 +119,26 @@ def get_status():
else: else:
current_proxy = get_message('no_proxy', server.language) 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({ return jsonify({
'current_proxy': 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,
'time_left': server.time_until_next_switch(), 'time_left': time_left,
'total_proxies': len(server.proxies) if hasattr(server, 'proxies') else 0, 'total_proxies': len(server.proxies) if hasattr(server, 'proxies') else 0,
'use_getip': server.use_getip, 'use_getip': server.use_getip,
'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(server_config.get('display_level', '1')),
'service_status': 'running' if server.running else 'stopped', 'service_status': 'running' if server.running else 'stopped',
'config': { 'config': server_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
}
}) })
@app.route('/api/config', methods=['POST']) @app.route('/api/config', methods=['POST'])
@ -158,6 +147,8 @@ def save_config():
new_config = request.get_json() new_config = request.get_json()
current_config = load_config('config/config.ini') current_config = load_config('config/config.ini')
port_changed = str(new_config.get('port', '')) != str(current_config.get('port', '')) 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 = ConfigParser()
config_parser.read('config/config.ini', encoding='utf-8') 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: with open('config/config.ini', 'w', encoding='utf-8') as f:
config_parser.write(f) config_parser.write(f)
old_mode = server.mode
old_use_getip = server.use_getip
server.config = load_config('config/config.ini') server.config = load_config('config/config.ini')
server._init_config_values(server.config) 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({ return jsonify({
'status': 'success', 'status': 'success',
'port_changed': port_changed 'port_changed': port_changed,
'service_status': 'running' if server.running else 'stopped'
}) })
except Exception as e: except Exception as e:
@ -324,45 +330,18 @@ def clear_logs():
@require_token @require_token
def switch_proxy(): def switch_proxy():
try: try:
if server.use_getip: result = asyncio.run(server.switch_proxy())
from config.getip import newip if result:
try: return jsonify({
old_proxy = server.current_proxy 'status': 'success',
new_proxy = newip() 'current_proxy': server.current_proxy,
server.current_proxy = new_proxy 'message': get_message('switch_success', server.language)
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: else:
if not server.proxies: return jsonify({
server.proxies = server._load_file_proxies() 'status': 'error',
if server.proxies: 'message': get_message('switch_failed', server.language, 'Proxy switch not needed or in cooldown')
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)
return jsonify({
'status': 'success',
'current_proxy': server.current_proxy,
'message': get_message('switch_success', server.language)
})
else:
return jsonify({
'status': 'error',
'message': get_message('no_proxies_available', server.language)
})
except Exception as e: except Exception as e:
return jsonify({ return jsonify({
'status': 'error', 'status': 'error',
@ -523,7 +502,7 @@ 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.2" CURRENT_VERSION = "ProxyCat-V2.0.4"
try: try:
client = httpx.Client(transport=httpx.HTTPTransport(retries=3)) client = httpx.Client(transport=httpx.HTTPTransport(retries=3))
@ -630,7 +609,7 @@ if __name__ == '__main__':
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'):
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_url', server.language, web_url))
logging.info(get_message('web_panel_notice', server.language)) logging.info(get_message('web_panel_notice', server.language))

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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