Spaces:
Paused
Paused
Upload 2 files
Browse files- launch_camoufox.py +1064 -0
- requirements.txt +28 -0
launch_camoufox.py
ADDED
|
@@ -0,0 +1,1064 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# launch_camoufox.py
|
| 3 |
+
import sys
|
| 4 |
+
import subprocess
|
| 5 |
+
import time
|
| 6 |
+
import re
|
| 7 |
+
import os
|
| 8 |
+
import signal
|
| 9 |
+
import atexit
|
| 10 |
+
import argparse
|
| 11 |
+
import select
|
| 12 |
+
import traceback
|
| 13 |
+
import json
|
| 14 |
+
import threading
|
| 15 |
+
import queue
|
| 16 |
+
import logging
|
| 17 |
+
import logging.handlers
|
| 18 |
+
import socket
|
| 19 |
+
import platform
|
| 20 |
+
import shutil
|
| 21 |
+
|
| 22 |
+
# --- 新的导入 ---
|
| 23 |
+
import uvicorn
|
| 24 |
+
from server import app # 从 server.py 导入 FastAPI app 对象
|
| 25 |
+
from dotenv import load_dotenv
|
| 26 |
+
|
| 27 |
+
# 加载 .env 文件
|
| 28 |
+
load_dotenv()
|
| 29 |
+
# -----------------
|
| 30 |
+
|
| 31 |
+
# 尝试导入 launch_server (用于内部启动模式,模拟 Camoufox 行为)
|
| 32 |
+
try:
|
| 33 |
+
from camoufox.server import launch_server
|
| 34 |
+
from camoufox import DefaultAddons # 假设 DefaultAddons 包含 AntiFingerprint
|
| 35 |
+
except ImportError:
|
| 36 |
+
if '--internal-launch' in sys.argv or any(arg.startswith('--internal-') for arg in sys.argv): # 更广泛地检查内部参数
|
| 37 |
+
print("❌ 致命错误:内部启动模式需要 'camoufox.server.launch_server' 和 'camoufox.DefaultAddons' 但无法导入。", file=sys.stderr)
|
| 38 |
+
print(" 这通常意味着 'camoufox' 包未正确安装或不在 PYTHONPATH 中。", file=sys.stderr)
|
| 39 |
+
sys.exit(1)
|
| 40 |
+
else:
|
| 41 |
+
launch_server = None
|
| 42 |
+
DefaultAddons = None
|
| 43 |
+
|
| 44 |
+
# --- 配置常量 ---
|
| 45 |
+
PYTHON_EXECUTABLE = sys.executable
|
| 46 |
+
ENDPOINT_CAPTURE_TIMEOUT = int(os.environ.get('ENDPOINT_CAPTURE_TIMEOUT', '45')) # 秒 (from dev)
|
| 47 |
+
DEFAULT_SERVER_PORT = int(os.environ.get('DEFAULT_FASTAPI_PORT', '2048')) # FastAPI 服务器端口
|
| 48 |
+
DEFAULT_CAMOUFOX_PORT = int(os.environ.get('DEFAULT_CAMOUFOX_PORT', '9222')) # Camoufox 调试端口 (如果内部启动需要)
|
| 49 |
+
DEFAULT_STREAM_PORT = int(os.environ.get('STREAM_PORT', '3120')) # 流式代理服务器端口
|
| 50 |
+
DEFAULT_HELPER_ENDPOINT = os.environ.get('GUI_DEFAULT_HELPER_ENDPOINT', '') # 外部 Helper 端点
|
| 51 |
+
DEFAULT_AUTH_SAVE_TIMEOUT = int(os.environ.get('AUTH_SAVE_TIMEOUT', '30')) # 认证保存超时时间
|
| 52 |
+
DEFAULT_SERVER_LOG_LEVEL = os.environ.get('SERVER_LOG_LEVEL', 'INFO') # 服务器日志级别
|
| 53 |
+
AUTH_PROFILES_DIR = os.path.join(os.path.dirname(__file__), "auth_profiles")
|
| 54 |
+
ACTIVE_AUTH_DIR = os.path.join(AUTH_PROFILES_DIR, "active")
|
| 55 |
+
SAVED_AUTH_DIR = os.path.join(AUTH_PROFILES_DIR, "saved")
|
| 56 |
+
HTTP_PROXY = os.environ.get('HTTP_PROXY', '')
|
| 57 |
+
HTTPS_PROXY = os.environ.get('HTTPS_PROXY', '')
|
| 58 |
+
LOG_DIR = os.path.join(os.path.dirname(__file__), 'logs')
|
| 59 |
+
LAUNCHER_LOG_FILE_PATH = os.path.join(LOG_DIR, 'launch_app.log')
|
| 60 |
+
|
| 61 |
+
# --- 全局进程句柄 ---
|
| 62 |
+
camoufox_proc = None
|
| 63 |
+
|
| 64 |
+
# --- 日志记录器实例 ---
|
| 65 |
+
logger = logging.getLogger("CamoufoxLauncher")
|
| 66 |
+
|
| 67 |
+
# --- WebSocket 端点正则表达式 ---
|
| 68 |
+
ws_regex = re.compile(r"(ws://\S+)")
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
# --- 线程安全的输出队列处理函数 (_enqueue_output) (from dev - more robust error handling) ---
|
| 72 |
+
def _enqueue_output(stream, stream_name, output_queue, process_pid_for_log="<未知PID>"):
|
| 73 |
+
log_prefix = f"[读取线程-{stream_name}-PID:{process_pid_for_log}]"
|
| 74 |
+
try:
|
| 75 |
+
for line_bytes in iter(stream.readline, b''):
|
| 76 |
+
if not line_bytes:
|
| 77 |
+
break
|
| 78 |
+
try:
|
| 79 |
+
line_str = line_bytes.decode('utf-8', errors='replace')
|
| 80 |
+
output_queue.put((stream_name, line_str))
|
| 81 |
+
except Exception as decode_err:
|
| 82 |
+
logger.warning(f"{log_prefix} 解码错误: {decode_err}。原始数据 (前100字节): {line_bytes[:100]}")
|
| 83 |
+
output_queue.put((stream_name, f"[解码错误: {decode_err}] {line_bytes[:100]}...\n"))
|
| 84 |
+
except ValueError:
|
| 85 |
+
logger.debug(f"{log_prefix} ValueError (流可能已关闭)。")
|
| 86 |
+
except Exception as e:
|
| 87 |
+
logger.error(f"{log_prefix} 读取流时发生意外错误: {e}", exc_info=True)
|
| 88 |
+
finally:
|
| 89 |
+
output_queue.put((stream_name, None))
|
| 90 |
+
if hasattr(stream, 'close') and not stream.closed:
|
| 91 |
+
try:
|
| 92 |
+
stream.close()
|
| 93 |
+
except Exception:
|
| 94 |
+
pass
|
| 95 |
+
logger.debug(f"{log_prefix} 线程退出。")
|
| 96 |
+
|
| 97 |
+
# --- 设置本启动器脚本的日志系统 (setup_launcher_logging) (from dev - clears log on start) ---
|
| 98 |
+
def setup_launcher_logging(log_level=logging.INFO):
|
| 99 |
+
os.makedirs(LOG_DIR, exist_ok=True)
|
| 100 |
+
file_log_formatter = logging.Formatter('%(asctime)s - %(levelname)s - [%(name)s:%(funcName)s:%(lineno)d] - %(message)s')
|
| 101 |
+
console_log_formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
|
| 102 |
+
if logger.hasHandlers():
|
| 103 |
+
logger.handlers.clear()
|
| 104 |
+
logger.setLevel(log_level)
|
| 105 |
+
logger.propagate = False
|
| 106 |
+
if os.path.exists(LAUNCHER_LOG_FILE_PATH):
|
| 107 |
+
try:
|
| 108 |
+
os.remove(LAUNCHER_LOG_FILE_PATH)
|
| 109 |
+
except OSError:
|
| 110 |
+
pass
|
| 111 |
+
file_handler = logging.handlers.RotatingFileHandler(
|
| 112 |
+
LAUNCHER_LOG_FILE_PATH, maxBytes=2*1024*1024, backupCount=3, encoding='utf-8', mode='w'
|
| 113 |
+
)
|
| 114 |
+
file_handler.setFormatter(file_log_formatter)
|
| 115 |
+
logger.addHandler(file_handler)
|
| 116 |
+
stream_handler = logging.StreamHandler(sys.stderr)
|
| 117 |
+
stream_handler.setFormatter(console_log_formatter)
|
| 118 |
+
logger.addHandler(stream_handler)
|
| 119 |
+
logger.info("=" * 30 + " Camoufox启动器日志系统已初始化 " + "=" * 30)
|
| 120 |
+
logger.info(f"日志级别设置为: {logging.getLevelName(logger.getEffectiveLevel())}")
|
| 121 |
+
logger.info(f"日志文件路径: {LAUNCHER_LOG_FILE_PATH}")
|
| 122 |
+
|
| 123 |
+
# --- 确保认证文件目录存在 (ensure_auth_dirs_exist) ---
|
| 124 |
+
def ensure_auth_dirs_exist():
|
| 125 |
+
logger.info("正在检查并确保认证文件目录存在...")
|
| 126 |
+
try:
|
| 127 |
+
os.makedirs(ACTIVE_AUTH_DIR, exist_ok=True)
|
| 128 |
+
logger.info(f" ✓ 活动认证目录就绪: {ACTIVE_AUTH_DIR}")
|
| 129 |
+
os.makedirs(SAVED_AUTH_DIR, exist_ok=True)
|
| 130 |
+
logger.info(f" ✓ 已保存认证目录就绪: {SAVED_AUTH_DIR}")
|
| 131 |
+
except Exception as e:
|
| 132 |
+
logger.error(f" ❌ 创建认证目录失败: {e}", exc_info=True)
|
| 133 |
+
sys.exit(1)
|
| 134 |
+
|
| 135 |
+
# --- 清理函数 (在脚本退出时执行) (from dev - more detailed logging and checks) ---
|
| 136 |
+
def cleanup():
|
| 137 |
+
global camoufox_proc
|
| 138 |
+
logger.info("--- 开始执行清理程序 (launch_camoufox.py) ---")
|
| 139 |
+
if camoufox_proc and camoufox_proc.poll() is None:
|
| 140 |
+
pid = camoufox_proc.pid
|
| 141 |
+
logger.info(f"正在终止 Camoufox 内部子进程 (PID: {pid})...")
|
| 142 |
+
try:
|
| 143 |
+
if sys.platform != "win32" and hasattr(os, 'getpgid') and hasattr(os, 'killpg'):
|
| 144 |
+
try:
|
| 145 |
+
pgid = os.getpgid(pid)
|
| 146 |
+
logger.info(f" 向 Camoufox 进程组 (PGID: {pgid}) 发送 SIGTERM 信号...")
|
| 147 |
+
os.killpg(pgid, signal.SIGTERM)
|
| 148 |
+
except ProcessLookupError:
|
| 149 |
+
logger.info(f" Camoufox 进程组 (PID: {pid}) 未找到,尝试直接终止进程...")
|
| 150 |
+
camoufox_proc.terminate()
|
| 151 |
+
else:
|
| 152 |
+
logger.info(f" 向 Camoufox (PID: {pid}) 发送 SIGTERM 信号...")
|
| 153 |
+
camoufox_proc.terminate()
|
| 154 |
+
camoufox_proc.wait(timeout=5)
|
| 155 |
+
logger.info(f" ✓ Camoufox (PID: {pid}) 已通过 SIGTERM 成功终止。")
|
| 156 |
+
except subprocess.TimeoutExpired:
|
| 157 |
+
logger.warning(f" ⚠️ Camoufox (PID: {pid}) SIGTERM 超时。正在发送 SIGKILL 强制终止...")
|
| 158 |
+
if sys.platform != "win32" and hasattr(os, 'getpgid') and hasattr(os, 'killpg'):
|
| 159 |
+
try:
|
| 160 |
+
pgid = os.getpgid(pid)
|
| 161 |
+
logger.info(f" 向 Camoufox 进程组 (PGID: {pgid}) 发送 SIGKILL 信号...")
|
| 162 |
+
os.killpg(pgid, signal.SIGKILL)
|
| 163 |
+
except ProcessLookupError:
|
| 164 |
+
logger.info(f" Camoufox 进程组 (PID: {pid}) 在 SIGKILL 时未找到,尝试直接强制终止...")
|
| 165 |
+
camoufox_proc.kill()
|
| 166 |
+
else:
|
| 167 |
+
camoufox_proc.kill()
|
| 168 |
+
try:
|
| 169 |
+
camoufox_proc.wait(timeout=2)
|
| 170 |
+
logger.info(f" ✓ Camoufox (PID: {pid}) 已通过 SIGKILL 成功终止。")
|
| 171 |
+
except Exception as e_kill:
|
| 172 |
+
logger.error(f" ❌ 等待 Camoufox (PID: {pid}) SIGKILL 完成时出错: {e_kill}")
|
| 173 |
+
except Exception as e_term:
|
| 174 |
+
logger.error(f" ❌ 终止 Camoufox (PID: {pid}) 时发生错误: {e_term}", exc_info=True)
|
| 175 |
+
finally:
|
| 176 |
+
if hasattr(camoufox_proc, 'stdout') and camoufox_proc.stdout and not camoufox_proc.stdout.closed:
|
| 177 |
+
camoufox_proc.stdout.close()
|
| 178 |
+
if hasattr(camoufox_proc, 'stderr') and camoufox_proc.stderr and not camoufox_proc.stderr.closed:
|
| 179 |
+
camoufox_proc.stderr.close()
|
| 180 |
+
camoufox_proc = None
|
| 181 |
+
elif camoufox_proc:
|
| 182 |
+
logger.info(f"Camoufox 内部子进程 (PID: {camoufox_proc.pid if hasattr(camoufox_proc, 'pid') else 'N/A'}) 先前已自行结束,退出码: {camoufox_proc.poll()}。")
|
| 183 |
+
camoufox_proc = None
|
| 184 |
+
else:
|
| 185 |
+
logger.info("Camoufox 内部子进程未运行或已清理。")
|
| 186 |
+
logger.info("--- 清理程序执行完毕 (launch_camoufox.py) ---")
|
| 187 |
+
|
| 188 |
+
atexit.register(cleanup)
|
| 189 |
+
def signal_handler(sig, frame):
|
| 190 |
+
logger.info(f"接收到信号 {signal.Signals(sig).name} ({sig})。正在启动退出程序...")
|
| 191 |
+
sys.exit(0)
|
| 192 |
+
signal.signal(signal.SIGINT, signal_handler)
|
| 193 |
+
signal.signal(signal.SIGTERM, signal_handler)
|
| 194 |
+
|
| 195 |
+
# --- 检查依赖项 (check_dependencies) (from dev - more comprehensive) ---
|
| 196 |
+
def check_dependencies():
|
| 197 |
+
logger.info("--- 步骤 1: 检查依赖项 ---")
|
| 198 |
+
required_modules = {}
|
| 199 |
+
if launch_server is not None and DefaultAddons is not None:
|
| 200 |
+
required_modules["camoufox"] = "camoufox (for server and addons)"
|
| 201 |
+
elif launch_server is not None:
|
| 202 |
+
required_modules["camoufox_server"] = "camoufox.server"
|
| 203 |
+
logger.warning(" ⚠️ 'camoufox.server' 已导入,但 'camoufox.DefaultAddons' 未导入。排除插件功能可能受限。")
|
| 204 |
+
missing_py_modules = []
|
| 205 |
+
dependencies_ok = True
|
| 206 |
+
if required_modules:
|
| 207 |
+
logger.info("正在检查 Python 模块:")
|
| 208 |
+
for module_name, install_package_name in required_modules.items():
|
| 209 |
+
try:
|
| 210 |
+
__import__(module_name)
|
| 211 |
+
logger.info(f" ✓ 模块 '{module_name}' 已找到。")
|
| 212 |
+
except ImportError:
|
| 213 |
+
logger.error(f" ❌ 模块 '{module_name}' (包: '{install_package_name}') 未找到。")
|
| 214 |
+
missing_py_modules.append(install_package_name)
|
| 215 |
+
dependencies_ok = False
|
| 216 |
+
else:
|
| 217 |
+
# 检查是否是内部启动模式,如果是,则 camoufox 必须可导入
|
| 218 |
+
is_any_internal_arg = any(arg.startswith('--internal-') for arg in sys.argv)
|
| 219 |
+
if is_any_internal_arg and (launch_server is None or DefaultAddons is None):
|
| 220 |
+
logger.error(f" ❌ 内部启动模式 (--internal-*) 需要 'camoufox' 包,但未能导入。")
|
| 221 |
+
dependencies_ok = False
|
| 222 |
+
elif not is_any_internal_arg:
|
| 223 |
+
logger.info("未请求内部启动模式,且未导入 camoufox.server,跳过对 'camoufox' Python 包的检查。")
|
| 224 |
+
|
| 225 |
+
|
| 226 |
+
try:
|
| 227 |
+
from server import app as server_app_check
|
| 228 |
+
if server_app_check:
|
| 229 |
+
logger.info(f" ✓ 成功从 'server.py' 导入 'app' 对象。")
|
| 230 |
+
except ImportError as e_import_server:
|
| 231 |
+
logger.error(f" ❌ 无法从 'server.py' 导入 'app' 对象: {e_import_server}")
|
| 232 |
+
logger.error(f" 请确保 'server.py' 文件存在且没有导入错误。")
|
| 233 |
+
dependencies_ok = False
|
| 234 |
+
|
| 235 |
+
if not dependencies_ok:
|
| 236 |
+
logger.error("-------------------------------------------------")
|
| 237 |
+
logger.error("❌ 依赖项检查失败!")
|
| 238 |
+
if missing_py_modules:
|
| 239 |
+
logger.error(f" 缺少的 Python 库: {', '.join(missing_py_modules)}")
|
| 240 |
+
logger.error(f" 请尝试使用 pip 安装: pip install {' '.join(missing_py_modules)}")
|
| 241 |
+
logger.error("-------------------------------------------------")
|
| 242 |
+
sys.exit(1)
|
| 243 |
+
else:
|
| 244 |
+
logger.info("✅ 所有启动器依赖项检查通过。")
|
| 245 |
+
|
| 246 |
+
# --- 端口检查和清理函数 (from dev - more robust) ---
|
| 247 |
+
def is_port_in_use(port: int, host: str = "0.0.0.0") -> bool:
|
| 248 |
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
| 249 |
+
try:
|
| 250 |
+
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
| 251 |
+
s.bind((host, port))
|
| 252 |
+
return False
|
| 253 |
+
except OSError:
|
| 254 |
+
return True
|
| 255 |
+
except Exception as e:
|
| 256 |
+
logger.warning(f"检查端口 {port} (主机 {host}) 时发生未知错误: {e}")
|
| 257 |
+
return True
|
| 258 |
+
|
| 259 |
+
def find_pids_on_port(port: int) -> list[int]:
|
| 260 |
+
pids = []
|
| 261 |
+
system_platform = platform.system()
|
| 262 |
+
command = ""
|
| 263 |
+
try:
|
| 264 |
+
if system_platform == "Linux" or system_platform == "Darwin":
|
| 265 |
+
command = f"lsof -ti :{port} -sTCP:LISTEN"
|
| 266 |
+
process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, close_fds=True)
|
| 267 |
+
stdout, stderr = process.communicate(timeout=5)
|
| 268 |
+
if process.returncode == 0 and stdout:
|
| 269 |
+
pids = [int(pid) for pid in stdout.strip().split('\n') if pid.isdigit()]
|
| 270 |
+
elif process.returncode != 0 and ("command not found" in stderr.lower() or "未找到命令" in stderr):
|
| 271 |
+
logger.error(f"命令 'lsof' 未找到。请确保已安装。")
|
| 272 |
+
elif process.returncode not in [0, 1]: # lsof 在未找到时返回1
|
| 273 |
+
logger.warning(f"执行 lsof 命令失败 (返回码 {process.returncode}): {stderr.strip()}")
|
| 274 |
+
elif system_platform == "Windows":
|
| 275 |
+
command = f'netstat -ano -p TCP | findstr "LISTENING" | findstr ":{port} "'
|
| 276 |
+
process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
| 277 |
+
stdout, stderr = process.communicate(timeout=10)
|
| 278 |
+
if process.returncode == 0 and stdout:
|
| 279 |
+
for line in stdout.strip().split('\n'):
|
| 280 |
+
parts = line.split()
|
| 281 |
+
if len(parts) >= 4 and parts[0].upper() == 'TCP' and f":{port}" in parts[1]:
|
| 282 |
+
if parts[-1].isdigit(): pids.append(int(parts[-1]))
|
| 283 |
+
pids = list(set(pids)) # 去重
|
| 284 |
+
elif process.returncode not in [0, 1]: # findstr 在未找到时返回1
|
| 285 |
+
logger.warning(f"执行 netstat/findstr 命令失败 (返回码 {process.returncode}): {stderr.strip()}")
|
| 286 |
+
else:
|
| 287 |
+
logger.warning(f"不支持的操作系统 '{system_platform}' 用于查找占用端口的进程。")
|
| 288 |
+
except FileNotFoundError:
|
| 289 |
+
cmd_name = command.split()[0] if command else "相关工具"
|
| 290 |
+
logger.error(f"命令 '{cmd_name}' 未找到。")
|
| 291 |
+
except subprocess.TimeoutExpired:
|
| 292 |
+
logger.error(f"执行命令 '{command}' 超时。")
|
| 293 |
+
except Exception as e:
|
| 294 |
+
logger.error(f"查找占用端口 {port} 的进程时出错: {e}", exc_info=True)
|
| 295 |
+
return pids
|
| 296 |
+
|
| 297 |
+
def kill_process_interactive(pid: int) -> bool:
|
| 298 |
+
system_platform = platform.system()
|
| 299 |
+
success = False
|
| 300 |
+
logger.info(f" 尝试终止进程 PID: {pid}...")
|
| 301 |
+
try:
|
| 302 |
+
if system_platform == "Linux" or system_platform == "Darwin":
|
| 303 |
+
result_term = subprocess.run(f"kill {pid}", shell=True, capture_output=True, text=True, timeout=3, check=False)
|
| 304 |
+
if result_term.returncode == 0:
|
| 305 |
+
logger.info(f" ✓ PID {pid} 已发送 SIGTERM 信号。")
|
| 306 |
+
success = True
|
| 307 |
+
else:
|
| 308 |
+
logger.warning(f" PID {pid} SIGTERM 失败: {result_term.stderr.strip() or result_term.stdout.strip()}. 尝试 SIGKILL...")
|
| 309 |
+
result_kill = subprocess.run(f"kill -9 {pid}", shell=True, capture_output=True, text=True, timeout=3, check=False)
|
| 310 |
+
if result_kill.returncode == 0:
|
| 311 |
+
logger.info(f" ✓ PID {pid} 已发送 SIGKILL 信号。")
|
| 312 |
+
success = True
|
| 313 |
+
else:
|
| 314 |
+
logger.error(f" ✗ PID {pid} SIGKILL 失败: {result_kill.stderr.strip() or result_kill.stdout.strip()}.")
|
| 315 |
+
elif system_platform == "Windows":
|
| 316 |
+
command_desc = f"taskkill /PID {pid} /T /F"
|
| 317 |
+
result = subprocess.run(command_desc, shell=True, capture_output=True, text=True, timeout=5, check=False)
|
| 318 |
+
output = result.stdout.strip()
|
| 319 |
+
error_output = result.stderr.strip()
|
| 320 |
+
if result.returncode == 0 and ("SUCCESS" in output.upper() or "成功" in output):
|
| 321 |
+
logger.info(f" ✓ PID {pid} 已通过 taskkill /F 终止。")
|
| 322 |
+
success = True
|
| 323 |
+
elif "could not find process" in error_output.lower() or "找不到" in error_output: # 进程可能已自行退出
|
| 324 |
+
logger.info(f" PID {pid} 执行 taskkill 时未找到 (可能已退出)。")
|
| 325 |
+
success = True # 视为成功,因为目标是端口可用
|
| 326 |
+
else:
|
| 327 |
+
logger.error(f" ✗ PID {pid} taskkill /F 失败: {(error_output + ' ' + output).strip()}.")
|
| 328 |
+
else:
|
| 329 |
+
logger.warning(f" 不支持的操作系统 '{system_platform}' 用于终止进程。")
|
| 330 |
+
except Exception as e:
|
| 331 |
+
logger.error(f" 终止 PID {pid} 时发生意外错误: {e}", exc_info=True)
|
| 332 |
+
return success
|
| 333 |
+
|
| 334 |
+
# --- 带超时的用户输入函数 (from dev - more robust Windows implementation) ---
|
| 335 |
+
def input_with_timeout(prompt_message: str, timeout_seconds: int = 30) -> str:
|
| 336 |
+
print(prompt_message, end='', flush=True)
|
| 337 |
+
if sys.platform == "win32":
|
| 338 |
+
user_input_container = [None]
|
| 339 |
+
def get_input_in_thread():
|
| 340 |
+
try:
|
| 341 |
+
user_input_container[0] = sys.stdin.readline().strip()
|
| 342 |
+
except Exception:
|
| 343 |
+
user_input_container[0] = "" # 出错时返回空字符串
|
| 344 |
+
input_thread = threading.Thread(target=get_input_in_thread, daemon=True)
|
| 345 |
+
input_thread.start()
|
| 346 |
+
input_thread.join(timeout=timeout_seconds)
|
| 347 |
+
if input_thread.is_alive():
|
| 348 |
+
print("\n输入超时。将使用默认值。", flush=True)
|
| 349 |
+
return ""
|
| 350 |
+
return user_input_container[0] if user_input_container[0] is not None else ""
|
| 351 |
+
else: # Linux/macOS
|
| 352 |
+
readable_fds, _, _ = select.select([sys.stdin], [], [], timeout_seconds)
|
| 353 |
+
if readable_fds:
|
| 354 |
+
return sys.stdin.readline().strip()
|
| 355 |
+
else:
|
| 356 |
+
print("\n输入超时。将使用默认值。", flush=True)
|
| 357 |
+
return ""
|
| 358 |
+
|
| 359 |
+
def get_proxy_from_gsettings():
|
| 360 |
+
"""
|
| 361 |
+
Retrieves the proxy settings from GSettings on Linux systems.
|
| 362 |
+
Returns a proxy string like "http://host:port" or None.
|
| 363 |
+
"""
|
| 364 |
+
def _run_gsettings_command(command_parts: list[str]) -> str | None:
|
| 365 |
+
"""Helper function to run gsettings command and return cleaned string output."""
|
| 366 |
+
try:
|
| 367 |
+
process_result = subprocess.run(
|
| 368 |
+
command_parts,
|
| 369 |
+
capture_output=True,
|
| 370 |
+
text=True,
|
| 371 |
+
check=False, # Do not raise CalledProcessError for non-zero exit codes
|
| 372 |
+
timeout=1 # Timeout for the subprocess call
|
| 373 |
+
)
|
| 374 |
+
if process_result.returncode == 0:
|
| 375 |
+
value = process_result.stdout.strip()
|
| 376 |
+
if value.startswith("'") and value.endswith("'"): # Remove surrounding single quotes
|
| 377 |
+
value = value[1:-1]
|
| 378 |
+
|
| 379 |
+
# If after stripping quotes, value is empty, or it's a gsettings "empty" representation
|
| 380 |
+
if not value or value == "''" or value == "@as []" or value == "[]":
|
| 381 |
+
return None
|
| 382 |
+
return value
|
| 383 |
+
else:
|
| 384 |
+
return None
|
| 385 |
+
except subprocess.TimeoutExpired:
|
| 386 |
+
return None
|
| 387 |
+
except Exception: # Broad exception as per pseudocode
|
| 388 |
+
return None
|
| 389 |
+
|
| 390 |
+
proxy_mode = _run_gsettings_command(["gsettings", "get", "org.gnome.system.proxy", "mode"])
|
| 391 |
+
|
| 392 |
+
if proxy_mode == "manual":
|
| 393 |
+
# Try HTTP proxy first
|
| 394 |
+
http_host = _run_gsettings_command(["gsettings", "get", "org.gnome.system.proxy.http", "host"])
|
| 395 |
+
http_port_str = _run_gsettings_command(["gsettings", "get", "org.gnome.system.proxy.http", "port"])
|
| 396 |
+
|
| 397 |
+
if http_host and http_port_str:
|
| 398 |
+
try:
|
| 399 |
+
http_port = int(http_port_str)
|
| 400 |
+
if http_port > 0:
|
| 401 |
+
return f"http://{http_host}:{http_port}"
|
| 402 |
+
except ValueError:
|
| 403 |
+
pass # Continue to HTTPS
|
| 404 |
+
|
| 405 |
+
# Try HTTPS proxy if HTTP not found or invalid
|
| 406 |
+
https_host = _run_gsettings_command(["gsettings", "get", "org.gnome.system.proxy.https", "host"])
|
| 407 |
+
https_port_str = _run_gsettings_command(["gsettings", "get", "org.gnome.system.proxy.https", "port"])
|
| 408 |
+
|
| 409 |
+
if https_host and https_port_str:
|
| 410 |
+
try:
|
| 411 |
+
https_port = int(https_port_str)
|
| 412 |
+
if https_port > 0:
|
| 413 |
+
# Note: Even for HTTPS proxy settings, the scheme for Playwright/requests is usually http://
|
| 414 |
+
return f"http://{https_host}:{https_port}"
|
| 415 |
+
except ValueError:
|
| 416 |
+
pass
|
| 417 |
+
|
| 418 |
+
return None
|
| 419 |
+
|
| 420 |
+
|
| 421 |
+
def determine_proxy_configuration(internal_camoufox_proxy_arg=None):
|
| 422 |
+
"""
|
| 423 |
+
统一的代理配置确定函数
|
| 424 |
+
按优先级顺序:命令行参数 > 环境变量 > 系统设置
|
| 425 |
+
|
| 426 |
+
Args:
|
| 427 |
+
internal_camoufox_proxy_arg: --internal-camoufox-proxy 命令行参数值
|
| 428 |
+
|
| 429 |
+
Returns:
|
| 430 |
+
dict: 包含代理配置信息的字典
|
| 431 |
+
{
|
| 432 |
+
'camoufox_proxy': str or None, # Camoufox浏览器使用的代理
|
| 433 |
+
'stream_proxy': str or None, # 流式代理服务使用的上游代理
|
| 434 |
+
'source': str # 代理来源说明
|
| 435 |
+
}
|
| 436 |
+
"""
|
| 437 |
+
result = {
|
| 438 |
+
'camoufox_proxy': None,
|
| 439 |
+
'stream_proxy': None,
|
| 440 |
+
'source': '无代理'
|
| 441 |
+
}
|
| 442 |
+
|
| 443 |
+
# 1. 优先使用命令行参数
|
| 444 |
+
if internal_camoufox_proxy_arg is not None:
|
| 445 |
+
if internal_camoufox_proxy_arg.strip(): # 非空字符串
|
| 446 |
+
result['camoufox_proxy'] = internal_camoufox_proxy_arg.strip()
|
| 447 |
+
result['stream_proxy'] = internal_camoufox_proxy_arg.strip()
|
| 448 |
+
result['source'] = f"命令行参数 --internal-camoufox-proxy: {internal_camoufox_proxy_arg.strip()}"
|
| 449 |
+
else: # 空字符串,明确禁用代理
|
| 450 |
+
result['source'] = "命令行参数 --internal-camoufox-proxy='' (明确禁用代理)"
|
| 451 |
+
return result
|
| 452 |
+
|
| 453 |
+
# 2. 尝试环境变量 UNIFIED_PROXY_CONFIG (优先级高于 HTTP_PROXY/HTTPS_PROXY)
|
| 454 |
+
unified_proxy = os.environ.get("UNIFIED_PROXY_CONFIG")
|
| 455 |
+
if unified_proxy:
|
| 456 |
+
result['camoufox_proxy'] = unified_proxy
|
| 457 |
+
result['stream_proxy'] = unified_proxy
|
| 458 |
+
result['source'] = f"环境变量 UNIFIED_PROXY_CONFIG: {unified_proxy}"
|
| 459 |
+
return result
|
| 460 |
+
|
| 461 |
+
# 3. 尝试环境变量 HTTP_PROXY
|
| 462 |
+
http_proxy = os.environ.get("HTTP_PROXY")
|
| 463 |
+
if http_proxy:
|
| 464 |
+
result['camoufox_proxy'] = http_proxy
|
| 465 |
+
result['stream_proxy'] = http_proxy
|
| 466 |
+
result['source'] = f"环境变量 HTTP_PROXY: {http_proxy}"
|
| 467 |
+
return result
|
| 468 |
+
|
| 469 |
+
# 4. 尝试环境变量 HTTPS_PROXY
|
| 470 |
+
https_proxy = os.environ.get("HTTPS_PROXY")
|
| 471 |
+
if https_proxy:
|
| 472 |
+
result['camoufox_proxy'] = https_proxy
|
| 473 |
+
result['stream_proxy'] = https_proxy
|
| 474 |
+
result['source'] = f"环境变量 HTTPS_PROXY: {https_proxy}"
|
| 475 |
+
return result
|
| 476 |
+
|
| 477 |
+
# 5. 尝试系统代理设置 (仅限 Linux)
|
| 478 |
+
if sys.platform.startswith('linux'):
|
| 479 |
+
gsettings_proxy = get_proxy_from_gsettings()
|
| 480 |
+
if gsettings_proxy:
|
| 481 |
+
result['camoufox_proxy'] = gsettings_proxy
|
| 482 |
+
result['stream_proxy'] = gsettings_proxy
|
| 483 |
+
result['source'] = f"gsettings 系统代理: {gsettings_proxy}"
|
| 484 |
+
return result
|
| 485 |
+
|
| 486 |
+
return result
|
| 487 |
+
|
| 488 |
+
|
| 489 |
+
# --- 主执行逻辑 ---
|
| 490 |
+
if __name__ == "__main__":
|
| 491 |
+
# 检查是否是内部启动调用,如果是,则不配置 launcher 的日志
|
| 492 |
+
is_internal_call = any(arg.startswith('--internal-') for arg in sys.argv)
|
| 493 |
+
if not is_internal_call:
|
| 494 |
+
setup_launcher_logging(log_level=logging.INFO)
|
| 495 |
+
|
| 496 |
+
parser = argparse.ArgumentParser(
|
| 497 |
+
description="Camoufox 浏览器模拟与 FastAPI 代理服务器的启动器。",
|
| 498 |
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter
|
| 499 |
+
)
|
| 500 |
+
# 内部参数 (from dev)
|
| 501 |
+
parser.add_argument('--internal-launch-mode', type=str, choices=['debug', 'headless', 'virtual_headless'], help=argparse.SUPPRESS)
|
| 502 |
+
parser.add_argument('--internal-auth-file', type=str, default=None, help=argparse.SUPPRESS)
|
| 503 |
+
parser.add_argument('--internal-camoufox-port', type=int, default=DEFAULT_CAMOUFOX_PORT, help=argparse.SUPPRESS)
|
| 504 |
+
parser.add_argument('--internal-camoufox-proxy', type=str, default=None, help=argparse.SUPPRESS)
|
| 505 |
+
parser.add_argument('--internal-camoufox-os', type=str, default="random", help=argparse.SUPPRESS)
|
| 506 |
+
|
| 507 |
+
|
| 508 |
+
# 用户可见参数 (merged from dev and helper)
|
| 509 |
+
parser.add_argument("--server-port", type=int, default=DEFAULT_SERVER_PORT, help=f"FastAPI 服务器监听的端口号 (默认: {DEFAULT_SERVER_PORT})")
|
| 510 |
+
parser.add_argument(
|
| 511 |
+
"--stream-port",
|
| 512 |
+
type=int,
|
| 513 |
+
default=DEFAULT_STREAM_PORT, # 从 .env 文件读取默认值
|
| 514 |
+
help=(
|
| 515 |
+
f"流式代理服务器使用端口"
|
| 516 |
+
f"提供来禁用此功能 --stream-port=0 . 默认: {DEFAULT_STREAM_PORT}"
|
| 517 |
+
)
|
| 518 |
+
)
|
| 519 |
+
parser.add_argument(
|
| 520 |
+
"--helper",
|
| 521 |
+
type=str,
|
| 522 |
+
default=DEFAULT_HELPER_ENDPOINT, # 使用默认值
|
| 523 |
+
help=(
|
| 524 |
+
f"Helper 服务器的 getStreamResponse 端点地址 (例如: http://127.0.0.1:3121/getStreamResponse). "
|
| 525 |
+
f"提供空字符串 (例如: --helper='') 来禁用此功能. 默认: {DEFAULT_HELPER_ENDPOINT}"
|
| 526 |
+
)
|
| 527 |
+
)
|
| 528 |
+
parser.add_argument(
|
| 529 |
+
"--camoufox-debug-port", # from dev
|
| 530 |
+
type=int,
|
| 531 |
+
default=DEFAULT_CAMOUFOX_PORT,
|
| 532 |
+
help=f"内部 Camoufox 实例监听的调试端口号 (默认: {DEFAULT_CAMOUFOX_PORT})"
|
| 533 |
+
)
|
| 534 |
+
mode_selection_group = parser.add_mutually_exclusive_group() # from dev (more options)
|
| 535 |
+
mode_selection_group.add_argument("--debug", action="store_true", help="启动调试模式 (浏览器界面可见,允许交互式认证)")
|
| 536 |
+
mode_selection_group.add_argument("--headless", action="store_true", help="启动无头模式 (浏览器无界面,需要预先保存的认证文件)")
|
| 537 |
+
mode_selection_group.add_argument("--virtual-display", action="store_true", help="启动无头模式并使用虚拟显示 (Xvfb, 仅限 Linux)") # from dev
|
| 538 |
+
|
| 539 |
+
# --camoufox-os 参数已移除,将由脚本内部自动检测系统并设置
|
| 540 |
+
parser.add_argument( # from dev
|
| 541 |
+
"--active-auth-json", type=str, default=None,
|
| 542 |
+
help="[无头模式/调试模式可选] 指定要使用的活动认证JSON文件的路径 (在 auth_profiles/active/ 或 auth_profiles/saved/ 中,或绝对路径)。"
|
| 543 |
+
"如果未提供,无头模式将使用 active/ 目录中最新的JSON文件,调试模式将提示选择或不使用。"
|
| 544 |
+
)
|
| 545 |
+
parser.add_argument( # from dev
|
| 546 |
+
"--auto-save-auth", action='store_true',
|
| 547 |
+
help="[调试模式] 在登录成功后,如果之前未加载认证文件,则自动提示并保存新的认证状态。"
|
| 548 |
+
)
|
| 549 |
+
parser.add_argument( # from dev
|
| 550 |
+
"--auth-save-timeout", type=int, default=DEFAULT_AUTH_SAVE_TIMEOUT,
|
| 551 |
+
help=f"[调试模式] 自动保存认证或输入认证文件名的等待超时时间 (秒)。默认: {DEFAULT_AUTH_SAVE_TIMEOUT}"
|
| 552 |
+
)
|
| 553 |
+
# 日志相关参数 (from dev)
|
| 554 |
+
parser.add_argument(
|
| 555 |
+
"--server-log-level", type=str, default=DEFAULT_SERVER_LOG_LEVEL, choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
|
| 556 |
+
help=f"server.py 的日志级别。默认: {DEFAULT_SERVER_LOG_LEVEL}"
|
| 557 |
+
)
|
| 558 |
+
parser.add_argument(
|
| 559 |
+
"--server-redirect-print", action='store_true',
|
| 560 |
+
help="将 server.py 中的 print 输出重定向到其日志系统。默认不重定向以便调试模式下的 input() 提示可见。"
|
| 561 |
+
)
|
| 562 |
+
parser.add_argument("--debug-logs", action='store_true', help="启用 server.py 内部的 DEBUG 级别详细日志 (环境变量 DEBUG_LOGS_ENABLED)。")
|
| 563 |
+
parser.add_argument("--trace-logs", action='store_true', help="启用 server.py 内部的 TRACE 级别更详细日志 (环境变量 TRACE_LOGS_ENABLED)。")
|
| 564 |
+
|
| 565 |
+
args = parser.parse_args()
|
| 566 |
+
|
| 567 |
+
# --- 自动检测当前系统并设置 Camoufox OS 模拟 ---
|
| 568 |
+
# 这个变量将用于后续的 Camoufox 内部启动和 HOST_OS_FOR_SHORTCUT 设置
|
| 569 |
+
current_system_for_camoufox = platform.system()
|
| 570 |
+
if current_system_for_camoufox == "Linux":
|
| 571 |
+
simulated_os_for_camoufox = "linux"
|
| 572 |
+
elif current_system_for_camoufox == "Windows":
|
| 573 |
+
simulated_os_for_camoufox = "windows"
|
| 574 |
+
elif current_system_for_camoufox == "Darwin": # macOS
|
| 575 |
+
simulated_os_for_camoufox = "macos"
|
| 576 |
+
else:
|
| 577 |
+
simulated_os_for_camoufox = "linux" # 未知系统的默认回退值
|
| 578 |
+
logger.warning(f"无法识别当前系统 '{current_system_for_camoufox}'。Camoufox OS 模拟将默认设置为: {simulated_os_for_camoufox}")
|
| 579 |
+
logger.info(f"根据当前系统 '{current_system_for_camoufox}',Camoufox OS 模拟已自动设置为: {simulated_os_for_camoufox}")
|
| 580 |
+
|
| 581 |
+
# --- 处理内部 Camoufox 启动逻辑 (如果脚本被自身作为子进程调用) (from dev) ---
|
| 582 |
+
if args.internal_launch_mode:
|
| 583 |
+
if not launch_server or not DefaultAddons:
|
| 584 |
+
print("❌ 致命错误 (--internal-launch-mode): camoufox.server.launch_server 或 camoufox.DefaultAddons 不可用。脚本无法继续。", file=sys.stderr)
|
| 585 |
+
sys.exit(1)
|
| 586 |
+
|
| 587 |
+
internal_mode_arg = args.internal_launch_mode
|
| 588 |
+
auth_file = args.internal_auth_file
|
| 589 |
+
camoufox_port_internal = args.internal_camoufox_port
|
| 590 |
+
# 使用统一的代理配置确定逻辑
|
| 591 |
+
proxy_config = determine_proxy_configuration(args.internal_camoufox_proxy)
|
| 592 |
+
actual_proxy_to_use = proxy_config['camoufox_proxy']
|
| 593 |
+
print(f"--- [内部Camoufox启动] 代理配置: {proxy_config['source']} ---", flush=True)
|
| 594 |
+
|
| 595 |
+
camoufox_proxy_internal = actual_proxy_to_use # 更新此变量以供后续使用
|
| 596 |
+
camoufox_os_internal = args.internal_camoufox_os
|
| 597 |
+
|
| 598 |
+
|
| 599 |
+
print(f"--- [内部Camoufox启动] 模式: {internal_mode_arg}, 认证文件: {os.path.basename(auth_file) if auth_file else '无'}, "
|
| 600 |
+
f"Camoufox端口: {camoufox_port_internal}, 代理: {camoufox_proxy_internal or '无'}, 模拟OS: {camoufox_os_internal} ---", flush=True)
|
| 601 |
+
print(f"--- [内部Camoufox启动] 正在调用 camoufox.server.launch_server ... ---", flush=True)
|
| 602 |
+
|
| 603 |
+
try:
|
| 604 |
+
launch_args_for_internal_camoufox = {
|
| 605 |
+
"port": camoufox_port_internal,
|
| 606 |
+
"addons": [],
|
| 607 |
+
# "proxy": camoufox_proxy_internal, # 已移除
|
| 608 |
+
"exclude_addons": [DefaultAddons.UBO], # Assuming DefaultAddons.UBO exists
|
| 609 |
+
}
|
| 610 |
+
|
| 611 |
+
# 正确添加代理的方式
|
| 612 |
+
if camoufox_proxy_internal: # 如果代理字符串存在且不为空
|
| 613 |
+
launch_args_for_internal_camoufox["proxy"] = {"server": camoufox_proxy_internal}
|
| 614 |
+
# 如果 camoufox_proxy_internal 是 None 或空字符串,"proxy" 键就不会被添加。
|
| 615 |
+
if auth_file:
|
| 616 |
+
launch_args_for_internal_camoufox["storage_state"] = auth_file
|
| 617 |
+
|
| 618 |
+
if "," in camoufox_os_internal:
|
| 619 |
+
camoufox_os_list_internal = [s.strip().lower() for s in camoufox_os_internal.split(',')]
|
| 620 |
+
valid_os_values = ["windows", "macos", "linux"]
|
| 621 |
+
if not all(val in valid_os_values for val in camoufox_os_list_internal):
|
| 622 |
+
print(f"❌ 内部Camoufox启动错误: camoufox_os_internal 列表中包含无效值: {camoufox_os_list_internal}", file=sys.stderr)
|
| 623 |
+
sys.exit(1)
|
| 624 |
+
launch_args_for_internal_camoufox['os'] = camoufox_os_list_internal
|
| 625 |
+
elif camoufox_os_internal.lower() in ["windows", "macos", "linux"]:
|
| 626 |
+
launch_args_for_internal_camoufox['os'] = camoufox_os_internal.lower()
|
| 627 |
+
elif camoufox_os_internal.lower() != "random":
|
| 628 |
+
print(f"❌ 内部Camoufox启动错误: camoufox_os_internal 值无效: '{camoufox_os_internal}'", file=sys.stderr)
|
| 629 |
+
sys.exit(1)
|
| 630 |
+
|
| 631 |
+
print(f" 传递给 launch_server 的参数: {launch_args_for_internal_camoufox}", flush=True)
|
| 632 |
+
|
| 633 |
+
if internal_mode_arg == 'headless':
|
| 634 |
+
launch_server(headless=True, **launch_args_for_internal_camoufox)
|
| 635 |
+
elif internal_mode_arg == 'virtual_headless':
|
| 636 |
+
launch_server(headless="virtual", **launch_args_for_internal_camoufox)
|
| 637 |
+
elif internal_mode_arg == 'debug':
|
| 638 |
+
launch_server(headless=False, **launch_args_for_internal_camoufox)
|
| 639 |
+
|
| 640 |
+
print(f"--- [内部Camoufox启动] camoufox.server.launch_server ({internal_mode_arg}模式) 调用已完成/阻塞。脚本将等待其结束。 ---", flush=True)
|
| 641 |
+
except Exception as e_internal_launch_final:
|
| 642 |
+
print(f"❌ 错误 (--internal-launch-mode): 执行 camoufox.server.launch_server 时发生异常: {e_internal_launch_final}", file=sys.stderr, flush=True)
|
| 643 |
+
traceback.print_exc(file=sys.stderr)
|
| 644 |
+
sys.exit(1)
|
| 645 |
+
sys.exit(0)
|
| 646 |
+
|
| 647 |
+
# --- 主启动器逻辑 ---
|
| 648 |
+
logger.info("🚀 Camoufox 启动器开始运行 🚀")
|
| 649 |
+
logger.info("=================================================")
|
| 650 |
+
ensure_auth_dirs_exist()
|
| 651 |
+
check_dependencies()
|
| 652 |
+
logger.info("=================================================")
|
| 653 |
+
|
| 654 |
+
deprecated_auth_state_path = os.path.join(os.path.dirname(__file__), "auth_state.json")
|
| 655 |
+
if os.path.exists(deprecated_auth_state_path):
|
| 656 |
+
logger.warning(f"检测到已弃用的认证文件: {deprecated_auth_state_path}。此文件不再被直接使用。")
|
| 657 |
+
logger.warning("请使用调试模式生成新的认证文件,并按需管理 'auth_profiles' 目录中的文件。")
|
| 658 |
+
|
| 659 |
+
final_launch_mode = None # from dev
|
| 660 |
+
if args.debug:
|
| 661 |
+
final_launch_mode = 'debug'
|
| 662 |
+
elif args.headless:
|
| 663 |
+
final_launch_mode = 'headless'
|
| 664 |
+
elif args.virtual_display: # from dev
|
| 665 |
+
final_launch_mode = 'virtual_headless'
|
| 666 |
+
if platform.system() != "Linux":
|
| 667 |
+
logger.warning("⚠️ --virtual-display 模式主要为 Linux 设计。在非 Linux 系统上,其行为可能与标准无头模式相同或导致 Camoufox 内部错误。")
|
| 668 |
+
else:
|
| 669 |
+
# 读取 .env 文件中的 LAUNCH_MODE 配置作为默认值
|
| 670 |
+
env_launch_mode = os.environ.get('LAUNCH_MODE', '').lower()
|
| 671 |
+
default_mode_from_env = None
|
| 672 |
+
default_interactive_choice = '1' # 默认选择无头模式
|
| 673 |
+
|
| 674 |
+
# 将 .env 中的 LAUNCH_MODE 映射到交互式选择
|
| 675 |
+
if env_launch_mode == 'headless':
|
| 676 |
+
default_mode_from_env = 'headless'
|
| 677 |
+
default_interactive_choice = '1'
|
| 678 |
+
elif env_launch_mode == 'debug' or env_launch_mode == 'normal':
|
| 679 |
+
default_mode_from_env = 'debug'
|
| 680 |
+
default_interactive_choice = '2'
|
| 681 |
+
elif env_launch_mode == 'virtual_display' or env_launch_mode == 'virtual_headless':
|
| 682 |
+
default_mode_from_env = 'virtual_headless'
|
| 683 |
+
default_interactive_choice = '3' if platform.system() == "Linux" else '1'
|
| 684 |
+
|
| 685 |
+
logger.info("--- 请选择启动模式 (未通过命令行参数指定) ---")
|
| 686 |
+
if env_launch_mode and default_mode_from_env:
|
| 687 |
+
logger.info(f" 从 .env 文件读取到默认启动模式: {env_launch_mode} -> {default_mode_from_env}")
|
| 688 |
+
|
| 689 |
+
prompt_options_text = "[1] 无头模式, [2] 调试模式"
|
| 690 |
+
valid_choices = {'1': 'headless', '2': 'debug'}
|
| 691 |
+
|
| 692 |
+
if platform.system() == "Linux": # from dev
|
| 693 |
+
prompt_options_text += ", [3] 无头模式 (虚拟显示 Xvfb)"
|
| 694 |
+
valid_choices['3'] = 'virtual_headless'
|
| 695 |
+
|
| 696 |
+
# 构建提示信息,显示当前默认选择
|
| 697 |
+
default_mode_name = valid_choices.get(default_interactive_choice, 'headless')
|
| 698 |
+
user_mode_choice = input_with_timeout(
|
| 699 |
+
f" 请输入启动模式 ({prompt_options_text}; 默认: {default_interactive_choice} {default_mode_name}模式,{15}秒超时): ", 15
|
| 700 |
+
) or default_interactive_choice
|
| 701 |
+
|
| 702 |
+
if user_mode_choice in valid_choices:
|
| 703 |
+
final_launch_mode = valid_choices[user_mode_choice]
|
| 704 |
+
else:
|
| 705 |
+
final_launch_mode = default_mode_from_env or 'headless' # 使用 .env 默认值或回退到无头模式
|
| 706 |
+
logger.info(f"无效输入 '{user_mode_choice}' 或超时,使用默认启动模式: {final_launch_mode}模式")
|
| 707 |
+
logger.info(f"最终选择的启动模式: {final_launch_mode.replace('_', ' ')}模式")
|
| 708 |
+
logger.info("-------------------------------------------------")
|
| 709 |
+
|
| 710 |
+
if final_launch_mode == 'virtual_headless' and platform.system() == "Linux": # from dev
|
| 711 |
+
logger.info("--- 检查 Xvfb (虚拟显示) 依赖 ---")
|
| 712 |
+
if not shutil.which("Xvfb"):
|
| 713 |
+
logger.error(" ❌ Xvfb 未找到。虚拟显示模式需要 Xvfb。请安装 (例如: sudo apt-get install xvfb) 后重试。")
|
| 714 |
+
sys.exit(1)
|
| 715 |
+
logger.info(" ✓ Xvfb 已找到。")
|
| 716 |
+
|
| 717 |
+
server_target_port = args.server_port
|
| 718 |
+
logger.info(f"--- 步骤 2: 检查 FastAPI 服务器目标端口 ({server_target_port}) 是否被占用 ---")
|
| 719 |
+
port_is_available = False
|
| 720 |
+
uvicorn_bind_host = "0.0.0.0" # from dev (was 127.0.0.1 in helper)
|
| 721 |
+
if is_port_in_use(server_target_port, host=uvicorn_bind_host):
|
| 722 |
+
logger.warning(f" ❌ 端口 {server_target_port} (主机 {uvicorn_bind_host}) 当前被占用。")
|
| 723 |
+
pids_on_port = find_pids_on_port(server_target_port)
|
| 724 |
+
if pids_on_port:
|
| 725 |
+
logger.warning(f" 识别到以下进程 PID 可能占用了端口 {server_target_port}: {pids_on_port}")
|
| 726 |
+
if final_launch_mode == 'debug':
|
| 727 |
+
sys.stderr.flush()
|
| 728 |
+
# Using input_with_timeout for consistency, though timeout might not be strictly needed here
|
| 729 |
+
choice = input_with_timeout(f" 是否尝试终止这些进程? (y/n, 输入 n 将继续并可能导致启动失败, 15s超时): ", 15).strip().lower()
|
| 730 |
+
if choice == 'y':
|
| 731 |
+
logger.info(" 用户选择尝试终止进程...")
|
| 732 |
+
all_killed = all(kill_process_interactive(pid) for pid in pids_on_port)
|
| 733 |
+
time.sleep(2)
|
| 734 |
+
if not is_port_in_use(server_target_port, host=uvicorn_bind_host):
|
| 735 |
+
logger.info(f" ✅ 端口 {server_target_port} (主机 {uvicorn_bind_host}) 现在可用。")
|
| 736 |
+
port_is_available = True
|
| 737 |
+
else:
|
| 738 |
+
logger.error(f" ❌ 尝试终止后,端口 {server_target_port} (主机 {uvicorn_bind_host}) 仍然被占用。")
|
| 739 |
+
else:
|
| 740 |
+
logger.info(" 用户选择不自动终止或超时。将继续尝试启动服务器。")
|
| 741 |
+
else:
|
| 742 |
+
logger.error(f" 无头模式下,不会尝试自动终止占用端口的进程。服务器启动可能会失败。")
|
| 743 |
+
else:
|
| 744 |
+
logger.warning(f" 未能自动识别占用端口 {server_target_port} 的进程。服务器启动可能会失败。")
|
| 745 |
+
|
| 746 |
+
if not port_is_available:
|
| 747 |
+
logger.warning(f"--- 端口 {server_target_port} 仍可能被占用。继续启动服务器,它将自行处理端口绑定。 ---")
|
| 748 |
+
else:
|
| 749 |
+
logger.info(f" ✅ 端口 {server_target_port} (主机 {uvicorn_bind_host}) 当前可用。")
|
| 750 |
+
port_is_available = True
|
| 751 |
+
|
| 752 |
+
|
| 753 |
+
logger.info("--- 步骤 3: 准备并启动 Camoufox 内部进程 ---")
|
| 754 |
+
captured_ws_endpoint = None
|
| 755 |
+
effective_active_auth_json_path = None # from dev
|
| 756 |
+
|
| 757 |
+
if args.active_auth_json:
|
| 758 |
+
logger.info(f" 尝试使用 --active-auth-json 参数提供的路径: '{args.active_auth_json}'")
|
| 759 |
+
candidate_path = os.path.expanduser(args.active_auth_json)
|
| 760 |
+
|
| 761 |
+
# 尝试解析路径:
|
| 762 |
+
# 1. 作为绝对路径
|
| 763 |
+
if os.path.isabs(candidate_path) and os.path.exists(candidate_path) and os.path.isfile(candidate_path):
|
| 764 |
+
effective_active_auth_json_path = candidate_path
|
| 765 |
+
else:
|
| 766 |
+
# 2. 作为相对于当前工作目录的路径
|
| 767 |
+
path_rel_to_cwd = os.path.abspath(candidate_path)
|
| 768 |
+
if os.path.exists(path_rel_to_cwd) and os.path.isfile(path_rel_to_cwd):
|
| 769 |
+
effective_active_auth_json_path = path_rel_to_cwd
|
| 770 |
+
else:
|
| 771 |
+
# 3. 作为相对于脚本目录的路径
|
| 772 |
+
path_rel_to_script = os.path.join(os.path.dirname(__file__), candidate_path)
|
| 773 |
+
if os.path.exists(path_rel_to_script) and os.path.isfile(path_rel_to_script):
|
| 774 |
+
effective_active_auth_json_path = path_rel_to_script
|
| 775 |
+
# 4. 如果它只是一个文件名,则在 ACTIVE_AUTH_DIR 然后 SAVED_AUTH_DIR 中检查
|
| 776 |
+
elif not os.path.sep in candidate_path: # 这是一个简单的文件名
|
| 777 |
+
path_in_active = os.path.join(ACTIVE_AUTH_DIR, candidate_path)
|
| 778 |
+
if os.path.exists(path_in_active) and os.path.isfile(path_in_active):
|
| 779 |
+
effective_active_auth_json_path = path_in_active
|
| 780 |
+
else:
|
| 781 |
+
path_in_saved = os.path.join(SAVED_AUTH_DIR, candidate_path)
|
| 782 |
+
if os.path.exists(path_in_saved) and os.path.isfile(path_in_saved):
|
| 783 |
+
effective_active_auth_json_path = path_in_saved
|
| 784 |
+
|
| 785 |
+
if effective_active_auth_json_path:
|
| 786 |
+
logger.info(f" 将使用通过 --active-auth-json 解析的认证文件: {effective_active_auth_json_path}")
|
| 787 |
+
else:
|
| 788 |
+
logger.error(f"❌ 指定的认证文件 (--active-auth-json='{args.active_auth_json}') 未找到或不是一个文件。")
|
| 789 |
+
sys.exit(1)
|
| 790 |
+
else:
|
| 791 |
+
# --active-auth-json 未提供。
|
| 792 |
+
if final_launch_mode == 'debug':
|
| 793 |
+
# 对于调试模式,一律扫描全目录并提示用户选择,不自动使用任何文件
|
| 794 |
+
logger.info(f" 调试模式: 扫描全目录并提示用户从可用认证文件中选择...")
|
| 795 |
+
else:
|
| 796 |
+
# 对于无头模式,检查 active/ 目录中的默认认证文件
|
| 797 |
+
logger.info(f" --active-auth-json 未提供。检查 '{ACTIVE_AUTH_DIR}' 中的默认认证文件...")
|
| 798 |
+
try:
|
| 799 |
+
if os.path.exists(ACTIVE_AUTH_DIR):
|
| 800 |
+
active_json_files = sorted([
|
| 801 |
+
f for f in os.listdir(ACTIVE_AUTH_DIR)
|
| 802 |
+
if f.lower().endswith('.json') and os.path.isfile(os.path.join(ACTIVE_AUTH_DIR, f))
|
| 803 |
+
])
|
| 804 |
+
if active_json_files:
|
| 805 |
+
effective_active_auth_json_path = os.path.join(ACTIVE_AUTH_DIR, active_json_files[0])
|
| 806 |
+
logger.info(f" 将使用 '{ACTIVE_AUTH_DIR}' 中按名称排序的第一个JSON文件: {os.path.basename(effective_active_auth_json_path)}")
|
| 807 |
+
else:
|
| 808 |
+
logger.info(f" 目录 '{ACTIVE_AUTH_DIR}' 为空或不包含JSON文件。")
|
| 809 |
+
else:
|
| 810 |
+
logger.info(f" 目录 '{ACTIVE_AUTH_DIR}' 不存在。")
|
| 811 |
+
except Exception as e_scan_active:
|
| 812 |
+
logger.warning(f" 扫描 '{ACTIVE_AUTH_DIR}' 时发生错误: {e_scan_active}", exc_info=True)
|
| 813 |
+
|
| 814 |
+
# 处理 debug 模式的用户选择逻辑
|
| 815 |
+
if final_launch_mode == 'debug':
|
| 816 |
+
# 对于调试模式,一律扫描全目录并提示用户选择
|
| 817 |
+
available_profiles = []
|
| 818 |
+
# 首先扫描 ACTIVE_AUTH_DIR,然后是 SAVED_AUTH_DIR
|
| 819 |
+
for profile_dir_path_str, dir_label in [(ACTIVE_AUTH_DIR, "active"), (SAVED_AUTH_DIR, "saved")]:
|
| 820 |
+
if os.path.exists(profile_dir_path_str):
|
| 821 |
+
try:
|
| 822 |
+
# 在每个目录中对文件名进行排序
|
| 823 |
+
filenames = sorted([
|
| 824 |
+
f for f in os.listdir(profile_dir_path_str)
|
| 825 |
+
if f.lower().endswith(".json") and os.path.isfile(os.path.join(profile_dir_path_str, f))
|
| 826 |
+
])
|
| 827 |
+
for filename in filenames:
|
| 828 |
+
full_path = os.path.join(profile_dir_path_str, filename)
|
| 829 |
+
available_profiles.append({"name": f"{dir_label}/{filename}", "path": full_path})
|
| 830 |
+
except OSError as e:
|
| 831 |
+
logger.warning(f" ⚠️ 警告: 无法读取目录 '{profile_dir_path_str}': {e}")
|
| 832 |
+
|
| 833 |
+
if available_profiles:
|
| 834 |
+
# 对可用配置文件列表进行排序,以确保一致的显示顺序
|
| 835 |
+
available_profiles.sort(key=lambda x: x['name'])
|
| 836 |
+
print('-'*60 + "\n 找到以下可用的认证文件:", flush=True)
|
| 837 |
+
for i, profile in enumerate(available_profiles): print(f" {i+1}: {profile['name']}", flush=True)
|
| 838 |
+
print(" N: 不加载任何文件 (使用浏览器当前状态)\n" + '-'*60, flush=True)
|
| 839 |
+
choice = input_with_timeout(f" 请选择要加载的认证文件编号 (输入 N 或直接回车则不加载, {args.auth_save_timeout}s超时): ", args.auth_save_timeout)
|
| 840 |
+
if choice.strip().lower() not in ['n', '']:
|
| 841 |
+
try:
|
| 842 |
+
choice_index = int(choice.strip()) - 1
|
| 843 |
+
if 0 <= choice_index < len(available_profiles):
|
| 844 |
+
selected_profile = available_profiles[choice_index]
|
| 845 |
+
effective_active_auth_json_path = selected_profile["path"]
|
| 846 |
+
logger.info(f" 已选择加载认证文件: {selected_profile['name']}")
|
| 847 |
+
print(f" 已选择加载: {selected_profile['name']}", flush=True)
|
| 848 |
+
else:
|
| 849 |
+
logger.info(" 无效的选择编号或超时。将不加载认证文件。")
|
| 850 |
+
print(" 无效的选择编号或超时。将不加载认证文件。", flush=True)
|
| 851 |
+
except ValueError:
|
| 852 |
+
logger.info(" 无效的输入。将不加载认证文件。")
|
| 853 |
+
print(" 无效的输入。将不加载认证文件。", flush=True)
|
| 854 |
+
else:
|
| 855 |
+
logger.info(" 好的,不加载认证文件或超时。")
|
| 856 |
+
print(" 好的,不加载认证文件或超时。", flush=True)
|
| 857 |
+
print('-'*60, flush=True)
|
| 858 |
+
else:
|
| 859 |
+
logger.info(" 未找到认证文件。将使用浏览器当前状态。")
|
| 860 |
+
print(" 未找到认证文件。将使用浏览器当前状态。", flush=True)
|
| 861 |
+
elif not effective_active_auth_json_path:
|
| 862 |
+
# 对于无头模式,如果 --active-auth-json 未提供且 active/ 为空,则报错
|
| 863 |
+
logger.error(f" ❌ {final_launch_mode} 模式错误: --active-auth-json 未提供,且活动认证目录 '{ACTIVE_AUTH_DIR}' 中未找到任何 '.json' 认证文件。请先在调试模式下保存一个或通过参数指定。")
|
| 864 |
+
sys.exit(1)
|
| 865 |
+
|
| 866 |
+
# 构建 Camoufox 内部启动命令 (from dev)
|
| 867 |
+
camoufox_internal_cmd_args = [
|
| 868 |
+
PYTHON_EXECUTABLE, '-u', __file__,
|
| 869 |
+
'--internal-launch-mode', final_launch_mode
|
| 870 |
+
]
|
| 871 |
+
if effective_active_auth_json_path:
|
| 872 |
+
camoufox_internal_cmd_args.extend(['--internal-auth-file', effective_active_auth_json_path])
|
| 873 |
+
|
| 874 |
+
camoufox_internal_cmd_args.extend(['--internal-camoufox-os', simulated_os_for_camoufox])
|
| 875 |
+
camoufox_internal_cmd_args.extend(['--internal-camoufox-port', str(args.camoufox_debug_port)])
|
| 876 |
+
|
| 877 |
+
# 修复:传递代理参数到内部Camoufox进程
|
| 878 |
+
if args.internal_camoufox_proxy is not None:
|
| 879 |
+
camoufox_internal_cmd_args.extend(['--internal-camoufox-proxy', args.internal_camoufox_proxy])
|
| 880 |
+
|
| 881 |
+
camoufox_popen_kwargs = {'stdout': subprocess.PIPE, 'stderr': subprocess.PIPE, 'env': os.environ.copy()}
|
| 882 |
+
camoufox_popen_kwargs['env']['PYTHONIOENCODING'] = 'utf-8'
|
| 883 |
+
if sys.platform != "win32" and final_launch_mode != 'debug':
|
| 884 |
+
camoufox_popen_kwargs['start_new_session'] = True
|
| 885 |
+
elif sys.platform == "win32" and (final_launch_mode == 'headless' or final_launch_mode == 'virtual_headless'):
|
| 886 |
+
camoufox_popen_kwargs['creationflags'] = subprocess.CREATE_NO_WINDOW
|
| 887 |
+
|
| 888 |
+
|
| 889 |
+
try:
|
| 890 |
+
logger.info(f" 将执行 Camoufox 内部启动命令: {' '.join(camoufox_internal_cmd_args)}")
|
| 891 |
+
camoufox_proc = subprocess.Popen(camoufox_internal_cmd_args, **camoufox_popen_kwargs)
|
| 892 |
+
logger.info(f" Camoufox 内部进程已启动 (PID: {camoufox_proc.pid})。正在等待 WebSocket 端点输出 (最长 {ENDPOINT_CAPTURE_TIMEOUT} 秒)...")
|
| 893 |
+
|
| 894 |
+
camoufox_output_q = queue.Queue()
|
| 895 |
+
camoufox_stdout_reader = threading.Thread(target=_enqueue_output, args=(camoufox_proc.stdout, "stdout", camoufox_output_q, camoufox_proc.pid), daemon=True)
|
| 896 |
+
camoufox_stderr_reader = threading.Thread(target=_enqueue_output, args=(camoufox_proc.stderr, "stderr", camoufox_output_q, camoufox_proc.pid), daemon=True)
|
| 897 |
+
camoufox_stdout_reader.start()
|
| 898 |
+
camoufox_stderr_reader.start()
|
| 899 |
+
|
| 900 |
+
ws_capture_start_time = time.time()
|
| 901 |
+
camoufox_ended_streams_count = 0
|
| 902 |
+
while time.time() - ws_capture_start_time < ENDPOINT_CAPTURE_TIMEOUT:
|
| 903 |
+
if camoufox_proc.poll() is not None:
|
| 904 |
+
logger.error(f" Camoufox 内部进程 (PID: {camoufox_proc.pid}) 在等待 WebSocket 端点期间已意外退出,退出码: {camoufox_proc.poll()}。")
|
| 905 |
+
break
|
| 906 |
+
try:
|
| 907 |
+
stream_name, line_from_camoufox = camoufox_output_q.get(timeout=0.2)
|
| 908 |
+
if line_from_camoufox is None:
|
| 909 |
+
camoufox_ended_streams_count += 1
|
| 910 |
+
logger.debug(f" [InternalCamoufox-{stream_name}-PID:{camoufox_proc.pid}] 输出流已关闭 (EOF)。")
|
| 911 |
+
if camoufox_ended_streams_count >= 2:
|
| 912 |
+
logger.info(f" Camoufox 内部进程 (PID: {camoufox_proc.pid}) 的所有输出流均已关闭。")
|
| 913 |
+
break
|
| 914 |
+
continue
|
| 915 |
+
|
| 916 |
+
log_line_content = f"[InternalCamoufox-{stream_name}-PID:{camoufox_proc.pid}]: {line_from_camoufox.rstrip()}"
|
| 917 |
+
if stream_name == "stderr" or "ERROR" in line_from_camoufox.upper() or "❌" in line_from_camoufox:
|
| 918 |
+
logger.warning(log_line_content)
|
| 919 |
+
else:
|
| 920 |
+
logger.info(log_line_content)
|
| 921 |
+
|
| 922 |
+
ws_match = ws_regex.search(line_from_camoufox)
|
| 923 |
+
if ws_match:
|
| 924 |
+
captured_ws_endpoint = ws_match.group(1)
|
| 925 |
+
logger.info(f" ✅ 成功从 Camoufox 内部进程捕获到 WebSocket 端点: {captured_ws_endpoint[:40]}...")
|
| 926 |
+
break
|
| 927 |
+
except queue.Empty:
|
| 928 |
+
continue
|
| 929 |
+
|
| 930 |
+
if camoufox_stdout_reader.is_alive(): camoufox_stdout_reader.join(timeout=1.0)
|
| 931 |
+
if camoufox_stderr_reader.is_alive(): camoufox_stderr_reader.join(timeout=1.0)
|
| 932 |
+
|
| 933 |
+
if not captured_ws_endpoint and (camoufox_proc and camoufox_proc.poll() is None):
|
| 934 |
+
logger.error(f" ❌ 未能在 {ENDPOINT_CAPTURE_TIMEOUT} 秒内从 Camoufox 内部进程 (PID: {camoufox_proc.pid}) 捕获到 WebSocket 端点。")
|
| 935 |
+
logger.error(" Camoufox 内部进程仍在运行,但未输出预期的 WebSocket 端点。请检查其日志或行为。")
|
| 936 |
+
cleanup()
|
| 937 |
+
sys.exit(1)
|
| 938 |
+
elif not captured_ws_endpoint and (camoufox_proc and camoufox_proc.poll() is not None):
|
| 939 |
+
logger.error(f" ❌ Camoufox 内部进程已退出,且未能捕获到 WebSocket 端点。")
|
| 940 |
+
sys.exit(1)
|
| 941 |
+
elif not captured_ws_endpoint:
|
| 942 |
+
logger.error(f" ❌ 未能捕获到 WebSocket 端点。")
|
| 943 |
+
sys.exit(1)
|
| 944 |
+
|
| 945 |
+
except Exception as e_launch_camoufox_internal:
|
| 946 |
+
logger.critical(f" ❌ 在内部启动 Camoufox 或捕获其 WebSocket 端点时发生致命错误: {e_launch_camoufox_internal}", exc_info=True)
|
| 947 |
+
cleanup()
|
| 948 |
+
sys.exit(1)
|
| 949 |
+
|
| 950 |
+
# --- Helper mode logic (New implementation) ---
|
| 951 |
+
if args.helper: # 如果 args.helper 不是空字符串 (即 helper 功能已通过默认值或用户指定启用)
|
| 952 |
+
logger.info(f" Helper 模式已启用,端点: {args.helper}")
|
| 953 |
+
os.environ['HELPER_ENDPOINT'] = args.helper # 设置端点环境变量
|
| 954 |
+
|
| 955 |
+
if effective_active_auth_json_path:
|
| 956 |
+
logger.info(f" 尝试从认证文件 '{os.path.basename(effective_active_auth_json_path)}' 提取 SAPISID...")
|
| 957 |
+
sapisid = ""
|
| 958 |
+
try:
|
| 959 |
+
with open(effective_active_auth_json_path, 'r', encoding='utf-8') as file:
|
| 960 |
+
auth_file_data = json.load(file)
|
| 961 |
+
if "cookies" in auth_file_data and isinstance(auth_file_data["cookies"], list):
|
| 962 |
+
for cookie in auth_file_data["cookies"]:
|
| 963 |
+
if isinstance(cookie, dict) and cookie.get("name") == "SAPISID" and cookie.get("domain") == ".google.com":
|
| 964 |
+
sapisid = cookie.get("value", "")
|
| 965 |
+
break
|
| 966 |
+
except (FileNotFoundError, json.JSONDecodeError, KeyError, TypeError) as e:
|
| 967 |
+
logger.warning(f" ⚠️ 无法从认证文件 '{os.path.basename(effective_active_auth_json_path)}' 加载或解析SAPISID: {e}")
|
| 968 |
+
except Exception as e_sapisid_extraction:
|
| 969 |
+
logger.warning(f" ⚠️ 提取SAPISID时发生未知错误: {e_sapisid_extraction}")
|
| 970 |
+
|
| 971 |
+
if sapisid:
|
| 972 |
+
logger.info(f" ✅ 成功加载 SAPISID。将设置 HELPER_SAPISID 环境变量。")
|
| 973 |
+
os.environ['HELPER_SAPISID'] = sapisid
|
| 974 |
+
else:
|
| 975 |
+
logger.warning(f" ⚠️ 未能从认证文件 '{os.path.basename(effective_active_auth_json_path)}' 中找到有效的 SAPISID。HELPER_SAPISID 将不会被设置。")
|
| 976 |
+
if 'HELPER_SAPISID' in os.environ: # 清理,以防万一
|
| 977 |
+
del os.environ['HELPER_SAPISID']
|
| 978 |
+
else: # args.helper 有值 (Helper 模式启用), 但没有认证文件
|
| 979 |
+
logger.warning(f" ⚠️ Helper 模式已启用,但没有有效的认证文件来提取 SAPISID。HELPER_SAPISID 将不会被设置。")
|
| 980 |
+
if 'HELPER_SAPISID' in os.environ: # 清理
|
| 981 |
+
del os.environ['HELPER_SAPISID']
|
| 982 |
+
else: # args.helper 是空字符串 (用户通过 --helper='' 禁用了 helper)
|
| 983 |
+
logger.info(" Helper 模式已通过 --helper='' 禁用。")
|
| 984 |
+
# 清理相关的环境变量
|
| 985 |
+
if 'HELPER_ENDPOINT' in os.environ:
|
| 986 |
+
del os.environ['HELPER_ENDPOINT']
|
| 987 |
+
if 'HELPER_SAPISID' in os.environ:
|
| 988 |
+
del os.environ['HELPER_SAPISID']
|
| 989 |
+
|
| 990 |
+
# --- 步骤 4: 设置环境变量并准备启动 FastAPI/Uvicorn 服务器 (from dev) ---
|
| 991 |
+
logger.info("--- 步骤 4: 设置环境变量并准备启动 FastAPI/Uvicorn 服务器 ---")
|
| 992 |
+
|
| 993 |
+
if captured_ws_endpoint:
|
| 994 |
+
os.environ['CAMOUFOX_WS_ENDPOINT'] = captured_ws_endpoint
|
| 995 |
+
else:
|
| 996 |
+
logger.error(" 严重逻辑错误: WebSocket 端点未捕获,但程序仍在继续。")
|
| 997 |
+
sys.exit(1)
|
| 998 |
+
|
| 999 |
+
os.environ['LAUNCH_MODE'] = final_launch_mode
|
| 1000 |
+
os.environ['SERVER_LOG_LEVEL'] = args.server_log_level.upper()
|
| 1001 |
+
os.environ['SERVER_REDIRECT_PRINT'] = str(args.server_redirect_print).lower()
|
| 1002 |
+
os.environ['DEBUG_LOGS_ENABLED'] = str(args.debug_logs).lower()
|
| 1003 |
+
os.environ['TRACE_LOGS_ENABLED'] = str(args.trace_logs).lower()
|
| 1004 |
+
if effective_active_auth_json_path:
|
| 1005 |
+
os.environ['ACTIVE_AUTH_JSON_PATH'] = effective_active_auth_json_path
|
| 1006 |
+
os.environ['AUTO_SAVE_AUTH'] = str(args.auto_save_auth).lower()
|
| 1007 |
+
os.environ['AUTH_SAVE_TIMEOUT'] = str(args.auth_save_timeout)
|
| 1008 |
+
os.environ['SERVER_PORT_INFO'] = str(args.server_port)
|
| 1009 |
+
os.environ['STREAM_PORT'] = str(args.stream_port)
|
| 1010 |
+
|
| 1011 |
+
# 设置统一的代理配置环境变量
|
| 1012 |
+
proxy_config = determine_proxy_configuration(args.internal_camoufox_proxy)
|
| 1013 |
+
if proxy_config['stream_proxy']:
|
| 1014 |
+
os.environ['UNIFIED_PROXY_CONFIG'] = proxy_config['stream_proxy']
|
| 1015 |
+
logger.info(f" 设置统一代理配置: {proxy_config['source']}")
|
| 1016 |
+
elif 'UNIFIED_PROXY_CONFIG' in os.environ:
|
| 1017 |
+
del os.environ['UNIFIED_PROXY_CONFIG']
|
| 1018 |
+
|
| 1019 |
+
host_os_for_shortcut_env = None
|
| 1020 |
+
camoufox_os_param_lower = simulated_os_for_camoufox.lower()
|
| 1021 |
+
if camoufox_os_param_lower == "macos": host_os_for_shortcut_env = "Darwin"
|
| 1022 |
+
elif camoufox_os_param_lower == "windows": host_os_for_shortcut_env = "Windows"
|
| 1023 |
+
elif camoufox_os_param_lower == "linux": host_os_for_shortcut_env = "Linux"
|
| 1024 |
+
if host_os_for_shortcut_env:
|
| 1025 |
+
os.environ['HOST_OS_FOR_SHORTCUT'] = host_os_for_shortcut_env
|
| 1026 |
+
elif 'HOST_OS_FOR_SHORTCUT' in os.environ:
|
| 1027 |
+
del os.environ['HOST_OS_FOR_SHORTCUT']
|
| 1028 |
+
|
| 1029 |
+
logger.info(f" 为 server.app 设置的环境变量:")
|
| 1030 |
+
env_keys_to_log = [
|
| 1031 |
+
'CAMOUFOX_WS_ENDPOINT', 'LAUNCH_MODE', 'SERVER_LOG_LEVEL',
|
| 1032 |
+
'SERVER_REDIRECT_PRINT', 'DEBUG_LOGS_ENABLED', 'TRACE_LOGS_ENABLED',
|
| 1033 |
+
'ACTIVE_AUTH_JSON_PATH', 'AUTO_SAVE_AUTH', 'AUTH_SAVE_TIMEOUT',
|
| 1034 |
+
'SERVER_PORT_INFO', 'HOST_OS_FOR_SHORTCUT',
|
| 1035 |
+
'HELPER_ENDPOINT', 'HELPER_SAPISID', 'STREAM_PORT',
|
| 1036 |
+
'UNIFIED_PROXY_CONFIG' # 新增统一代理配置
|
| 1037 |
+
]
|
| 1038 |
+
for key in env_keys_to_log:
|
| 1039 |
+
if key in os.environ:
|
| 1040 |
+
val_to_log = os.environ[key]
|
| 1041 |
+
if key == 'CAMOUFOX_WS_ENDPOINT' and len(val_to_log) > 40: val_to_log = val_to_log[:40] + "..."
|
| 1042 |
+
if key == 'ACTIVE_AUTH_JSON_PATH': val_to_log = os.path.basename(val_to_log)
|
| 1043 |
+
logger.info(f" {key}={val_to_log}")
|
| 1044 |
+
else:
|
| 1045 |
+
logger.info(f" {key}= (未设置)")
|
| 1046 |
+
|
| 1047 |
+
|
| 1048 |
+
# --- 步骤 5: 启动 FastAPI/Uvicorn 服务器 (from dev) ---
|
| 1049 |
+
logger.info(f"--- 步骤 5: 启动集成的 FastAPI 服务器 (监听端口: {args.server_port}) ---")
|
| 1050 |
+
try:
|
| 1051 |
+
uvicorn.run(
|
| 1052 |
+
app,
|
| 1053 |
+
host="0.0.0.0", # Bind to all interfaces
|
| 1054 |
+
port=args.server_port,
|
| 1055 |
+
log_config=None # server.py will handle its own logging based on env vars
|
| 1056 |
+
)
|
| 1057 |
+
logger.info("Uvicorn 服务器已停止。")
|
| 1058 |
+
except SystemExit as e_sysexit:
|
| 1059 |
+
logger.info(f"Uvicorn 或其子系统通过 sys.exit({e_sysexit.code}) 退出。")
|
| 1060 |
+
except Exception as e_uvicorn:
|
| 1061 |
+
logger.critical(f"❌ 运行 Uvicorn 时发生致命错误: {e_uvicorn}", exc_info=True)
|
| 1062 |
+
sys.exit(1) # Ensure launcher exits if Uvicorn fails critically
|
| 1063 |
+
|
| 1064 |
+
logger.info("🚀 Camoufox 启动器主逻辑执行完毕 🚀")
|
requirements.txt
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# FastAPI and related
|
| 2 |
+
fastapi==0.115.12
|
| 3 |
+
pydantic>=2.7.1,<3.0.0
|
| 4 |
+
|
| 5 |
+
# Uvicorn and standard extras
|
| 6 |
+
uvicorn==0.29.0
|
| 7 |
+
python-dotenv==1.0.1
|
| 8 |
+
websockets==12.0
|
| 9 |
+
# httptools==0.6.1
|
| 10 |
+
uvloop ; sys_platform != "win32" # For Uvicorn performance on Linux/macOS
|
| 11 |
+
|
| 12 |
+
# Core launch_camoufox dependencies
|
| 13 |
+
playwright
|
| 14 |
+
camoufox[geoip]
|
| 15 |
+
|
| 16 |
+
# Security and Cryptography
|
| 17 |
+
cryptography==42.0.5
|
| 18 |
+
|
| 19 |
+
# HTTP client and other utilities
|
| 20 |
+
aiohttp~=3.9.5 # CRITICAL: Using the newer version from the root project.
|
| 21 |
+
# The helper's middleware.py usage of aiohttp is likely compatible.
|
| 22 |
+
requests==2.31.0
|
| 23 |
+
pyjwt==2.8.0
|
| 24 |
+
Flask==3.0.3 # Used in llm.py
|
| 25 |
+
|
| 26 |
+
# Stream Proxy
|
| 27 |
+
aiosocks~=0.2.6
|
| 28 |
+
python-socks~=2.7.1
|