hins111 commited on
Commit
6178b04
·
verified ·
1 Parent(s): 1d0244e

Upload 2 files

Browse files
Files changed (2) hide show
  1. launch_camoufox.py +1064 -0
  2. 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