TheSmallHanCat commited on
Commit
a1b7357
·
1 Parent(s): f63c37f

feat: 为无头模式设置独立代理

Browse files
src/api/admin.py CHANGED
@@ -839,14 +839,26 @@ async def update_captcha_config(
839
  token: str = Depends(verify_admin_token)
840
  ):
841
  """Update captcha configuration"""
 
 
842
  captcha_method = request.get("captcha_method")
843
  yescaptcha_api_key = request.get("yescaptcha_api_key")
844
  yescaptcha_base_url = request.get("yescaptcha_base_url")
 
 
 
 
 
 
 
 
845
 
846
  await db.update_captcha_config(
847
  captcha_method=captcha_method,
848
  yescaptcha_api_key=yescaptcha_api_key,
849
- yescaptcha_base_url=yescaptcha_base_url
 
 
850
  )
851
 
852
  # 🔥 Hot reload: sync database config to memory
@@ -862,5 +874,7 @@ async def get_captcha_config(token: str = Depends(verify_admin_token)):
862
  return {
863
  "captcha_method": captcha_config.captcha_method,
864
  "yescaptcha_api_key": captcha_config.yescaptcha_api_key,
865
- "yescaptcha_base_url": captcha_config.yescaptcha_base_url
 
 
866
  }
 
839
  token: str = Depends(verify_admin_token)
840
  ):
841
  """Update captcha configuration"""
842
+ from ..services.browser_captcha import validate_browser_proxy_url
843
+
844
  captcha_method = request.get("captcha_method")
845
  yescaptcha_api_key = request.get("yescaptcha_api_key")
846
  yescaptcha_base_url = request.get("yescaptcha_base_url")
847
+ browser_proxy_enabled = request.get("browser_proxy_enabled", False)
848
+ browser_proxy_url = request.get("browser_proxy_url", "")
849
+
850
+ # 验证浏览器代理URL格式
851
+ if browser_proxy_enabled and browser_proxy_url:
852
+ is_valid, error_msg = validate_browser_proxy_url(browser_proxy_url)
853
+ if not is_valid:
854
+ return {"success": False, "message": error_msg}
855
 
856
  await db.update_captcha_config(
857
  captcha_method=captcha_method,
858
  yescaptcha_api_key=yescaptcha_api_key,
859
+ yescaptcha_base_url=yescaptcha_base_url,
860
+ browser_proxy_enabled=browser_proxy_enabled,
861
+ browser_proxy_url=browser_proxy_url if browser_proxy_enabled else None
862
  )
863
 
864
  # 🔥 Hot reload: sync database config to memory
 
874
  return {
875
  "captcha_method": captcha_config.captcha_method,
876
  "yescaptcha_api_key": captcha_config.yescaptcha_api_key,
877
+ "yescaptcha_base_url": captcha_config.yescaptcha_base_url,
878
+ "browser_proxy_enabled": captcha_config.browser_proxy_enabled,
879
+ "browser_proxy_url": captcha_config.browser_proxy_url or ""
880
  }
src/core/database.py CHANGED
@@ -209,6 +209,8 @@ class Database:
209
  yescaptcha_base_url TEXT DEFAULT 'https://api.yescaptcha.com',
210
  website_key TEXT DEFAULT '6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV',
211
  page_action TEXT DEFAULT 'FLOW_GENERATION',
 
 
212
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
213
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
214
  )
@@ -249,6 +251,21 @@ class Database:
249
  except Exception as e:
250
  print(f" ✗ Failed to add column 'error_ban_threshold': {e}")
251
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
252
  # Check and add missing columns to token_stats table
253
  if await self._table_exists(db, "token_stats"):
254
  stats_columns_to_add = [
@@ -439,6 +456,8 @@ class Database:
439
  yescaptcha_base_url TEXT DEFAULT 'https://api.yescaptcha.com',
440
  website_key TEXT DEFAULT '6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV',
441
  page_action TEXT DEFAULT 'FLOW_GENERATION',
 
 
442
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
443
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
444
  )
@@ -1118,7 +1137,9 @@ class Database:
1118
  self,
1119
  captcha_method: str = None,
1120
  yescaptcha_api_key: str = None,
1121
- yescaptcha_base_url: str = None
 
 
1122
  ):
1123
  """Update captcha configuration"""
1124
  async with aiosqlite.connect(self.db_path) as db:
@@ -1131,20 +1152,25 @@ class Database:
1131
  new_method = captcha_method if captcha_method is not None else current.get("captcha_method", "yescaptcha")
1132
  new_api_key = yescaptcha_api_key if yescaptcha_api_key is not None else current.get("yescaptcha_api_key", "")
1133
  new_base_url = yescaptcha_base_url if yescaptcha_base_url is not None else current.get("yescaptcha_base_url", "https://api.yescaptcha.com")
 
 
1134
 
1135
  await db.execute("""
1136
  UPDATE captcha_config
1137
- SET captcha_method = ?, yescaptcha_api_key = ?, yescaptcha_base_url = ?, updated_at = CURRENT_TIMESTAMP
 
1138
  WHERE id = 1
1139
- """, (new_method, new_api_key, new_base_url))
1140
  else:
1141
  new_method = captcha_method if captcha_method is not None else "yescaptcha"
1142
  new_api_key = yescaptcha_api_key if yescaptcha_api_key is not None else ""
1143
  new_base_url = yescaptcha_base_url if yescaptcha_base_url is not None else "https://api.yescaptcha.com"
 
 
1144
 
1145
  await db.execute("""
1146
- INSERT INTO captcha_config (id, captcha_method, yescaptcha_api_key, yescaptcha_base_url)
1147
- VALUES (1, ?, ?, ?)
1148
- """, (new_method, new_api_key, new_base_url))
1149
 
1150
  await db.commit()
 
209
  yescaptcha_base_url TEXT DEFAULT 'https://api.yescaptcha.com',
210
  website_key TEXT DEFAULT '6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV',
211
  page_action TEXT DEFAULT 'FLOW_GENERATION',
212
+ browser_proxy_enabled BOOLEAN DEFAULT 0,
213
+ browser_proxy_url TEXT,
214
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
215
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
216
  )
 
251
  except Exception as e:
252
  print(f" ✗ Failed to add column 'error_ban_threshold': {e}")
253
 
254
+ # Check and add missing columns to captcha_config table
255
+ if await self._table_exists(db, "captcha_config"):
256
+ captcha_columns_to_add = [
257
+ ("browser_proxy_enabled", "BOOLEAN DEFAULT 0"),
258
+ ("browser_proxy_url", "TEXT"),
259
+ ]
260
+
261
+ for col_name, col_type in captcha_columns_to_add:
262
+ if not await self._column_exists(db, "captcha_config", col_name):
263
+ try:
264
+ await db.execute(f"ALTER TABLE captcha_config ADD COLUMN {col_name} {col_type}")
265
+ print(f" ✓ Added column '{col_name}' to captcha_config table")
266
+ except Exception as e:
267
+ print(f" ✗ Failed to add column '{col_name}': {e}")
268
+
269
  # Check and add missing columns to token_stats table
270
  if await self._table_exists(db, "token_stats"):
271
  stats_columns_to_add = [
 
456
  yescaptcha_base_url TEXT DEFAULT 'https://api.yescaptcha.com',
457
  website_key TEXT DEFAULT '6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV',
458
  page_action TEXT DEFAULT 'FLOW_GENERATION',
459
+ browser_proxy_enabled BOOLEAN DEFAULT 0,
460
+ browser_proxy_url TEXT,
461
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
462
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
463
  )
 
1137
  self,
1138
  captcha_method: str = None,
1139
  yescaptcha_api_key: str = None,
1140
+ yescaptcha_base_url: str = None,
1141
+ browser_proxy_enabled: bool = None,
1142
+ browser_proxy_url: str = None
1143
  ):
1144
  """Update captcha configuration"""
1145
  async with aiosqlite.connect(self.db_path) as db:
 
1152
  new_method = captcha_method if captcha_method is not None else current.get("captcha_method", "yescaptcha")
1153
  new_api_key = yescaptcha_api_key if yescaptcha_api_key is not None else current.get("yescaptcha_api_key", "")
1154
  new_base_url = yescaptcha_base_url if yescaptcha_base_url is not None else current.get("yescaptcha_base_url", "https://api.yescaptcha.com")
1155
+ new_proxy_enabled = browser_proxy_enabled if browser_proxy_enabled is not None else current.get("browser_proxy_enabled", False)
1156
+ new_proxy_url = browser_proxy_url if browser_proxy_url is not None else current.get("browser_proxy_url")
1157
 
1158
  await db.execute("""
1159
  UPDATE captcha_config
1160
+ SET captcha_method = ?, yescaptcha_api_key = ?, yescaptcha_base_url = ?,
1161
+ browser_proxy_enabled = ?, browser_proxy_url = ?, updated_at = CURRENT_TIMESTAMP
1162
  WHERE id = 1
1163
+ """, (new_method, new_api_key, new_base_url, new_proxy_enabled, new_proxy_url))
1164
  else:
1165
  new_method = captcha_method if captcha_method is not None else "yescaptcha"
1166
  new_api_key = yescaptcha_api_key if yescaptcha_api_key is not None else ""
1167
  new_base_url = yescaptcha_base_url if yescaptcha_base_url is not None else "https://api.yescaptcha.com"
1168
+ new_proxy_enabled = browser_proxy_enabled if browser_proxy_enabled is not None else False
1169
+ new_proxy_url = browser_proxy_url
1170
 
1171
  await db.execute("""
1172
+ INSERT INTO captcha_config (id, captcha_method, yescaptcha_api_key, yescaptcha_base_url, browser_proxy_enabled, browser_proxy_url)
1173
+ VALUES (1, ?, ?, ?, ?, ?)
1174
+ """, (new_method, new_api_key, new_base_url, new_proxy_enabled, new_proxy_url))
1175
 
1176
  await db.commit()
src/core/models.py CHANGED
@@ -152,6 +152,8 @@ class CaptchaConfig(BaseModel):
152
  yescaptcha_base_url: str = "https://api.yescaptcha.com"
153
  website_key: str = "6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV"
154
  page_action: str = "FLOW_GENERATION"
 
 
155
  created_at: Optional[datetime] = None
156
  updated_at: Optional[datetime] = None
157
 
 
152
  yescaptcha_base_url: str = "https://api.yescaptcha.com"
153
  website_key: str = "6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV"
154
  page_action: str = "FLOW_GENERATION"
155
+ browser_proxy_enabled: bool = False # 浏览器打码是否启用代理
156
+ browser_proxy_url: Optional[str] = None # 浏览器打码代理URL
157
  created_at: Optional[datetime] = None
158
  updated_at: Optional[datetime] = None
159
 
src/main.py CHANGED
@@ -76,7 +76,7 @@ async def lifespan(app: FastAPI):
76
  browser_service = None
77
  if captcha_config.captcha_method == "browser":
78
  from .services.browser_captcha import BrowserCaptchaService
79
- browser_service = await BrowserCaptchaService.get_instance(proxy_manager)
80
  print("✓ Browser captcha service initialized (headless mode)")
81
 
82
  # Initialize concurrency manager
 
76
  browser_service = None
77
  if captcha_config.captcha_method == "browser":
78
  from .services.browser_captcha import BrowserCaptchaService
79
+ browser_service = await BrowserCaptchaService.get_instance(db)
80
  print("✓ Browser captcha service initialized (headless mode)")
81
 
82
  # Initialize concurrency manager
src/services/browser_captcha.py CHANGED
@@ -35,28 +35,67 @@ def parse_proxy_url(proxy_url: str) -> Optional[Dict[str, str]]:
35
  return None
36
 
37
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  class BrowserCaptchaService:
39
  """浏览器自动化获取 reCAPTCHA token(单例模式)"""
40
 
41
  _instance: Optional['BrowserCaptchaService'] = None
42
  _lock = asyncio.Lock()
43
 
44
- def __init__(self, proxy_manager=None):
45
  """初始化服务(始终使用无头模式)"""
46
  self.headless = True # 始终无头
47
  self.playwright = None
48
  self.browser: Optional[Browser] = None
49
  self._initialized = False
50
  self.website_key = "6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV"
51
- self.proxy_manager = proxy_manager
52
 
53
  @classmethod
54
- async def get_instance(cls, proxy_manager=None) -> 'BrowserCaptchaService':
55
  """获取单例实例"""
56
  if cls._instance is None:
57
  async with cls._lock:
58
  if cls._instance is None:
59
- cls._instance = cls(proxy_manager)
60
  await cls._instance.initialize()
61
  return cls._instance
62
 
@@ -66,10 +105,12 @@ class BrowserCaptchaService:
66
  return
67
 
68
  try:
69
- # 获取代理配置
70
  proxy_url = None
71
- if self.proxy_manager:
72
- proxy_url = await self.proxy_manager.get_proxy_url()
 
 
73
 
74
  debug_logger.log_info(f"[BrowserCaptcha] 正在启动浏览器... (proxy={proxy_url or 'None'})")
75
  self.playwright = await async_playwright().start()
 
35
  return None
36
 
37
 
38
+ def validate_browser_proxy_url(proxy_url: str) -> tuple[bool, str]:
39
+ """验证浏览器代理URL格式(仅支持HTTP和无认证SOCKS5)
40
+
41
+ Args:
42
+ proxy_url: 代理URL
43
+
44
+ Returns:
45
+ (是否有效, 错误信息)
46
+ """
47
+ if not proxy_url or not proxy_url.strip():
48
+ return True, "" # 空URL视为有效(不使用代理)
49
+
50
+ proxy_url = proxy_url.strip()
51
+ parsed = parse_proxy_url(proxy_url)
52
+
53
+ if not parsed:
54
+ return False, "代理URL格式错误,正确格式:http://host:port 或 socks5://host:port"
55
+
56
+ # 检查是否有认证信息
57
+ has_auth = 'username' in parsed
58
+
59
+ # 获取协议
60
+ protocol = parsed['server'].split('://')[0]
61
+
62
+ # SOCKS5不支持认证
63
+ if protocol == 'socks5' and has_auth:
64
+ return False, "浏览器不支持带认证的SOCKS5代理,请使用HTTP代理或移除SOCKS5认证"
65
+
66
+ # HTTP/HTTPS支持认证
67
+ if protocol in ['http', 'https']:
68
+ return True, ""
69
+
70
+ # SOCKS5无认证支持
71
+ if protocol == 'socks5' and not has_auth:
72
+ return True, ""
73
+
74
+ return False, f"不支持的代理协议:{protocol}"
75
+
76
+
77
  class BrowserCaptchaService:
78
  """浏览器自动化获取 reCAPTCHA token(单例模式)"""
79
 
80
  _instance: Optional['BrowserCaptchaService'] = None
81
  _lock = asyncio.Lock()
82
 
83
+ def __init__(self, db=None):
84
  """初始化服务(始终使用无头模式)"""
85
  self.headless = True # 始终无头
86
  self.playwright = None
87
  self.browser: Optional[Browser] = None
88
  self._initialized = False
89
  self.website_key = "6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV"
90
+ self.db = db
91
 
92
  @classmethod
93
+ async def get_instance(cls, db=None) -> 'BrowserCaptchaService':
94
  """获取单例实例"""
95
  if cls._instance is None:
96
  async with cls._lock:
97
  if cls._instance is None:
98
+ cls._instance = cls(db)
99
  await cls._instance.initialize()
100
  return cls._instance
101
 
 
105
  return
106
 
107
  try:
108
+ # 获取浏览器专用代理配置
109
  proxy_url = None
110
+ if self.db:
111
+ captcha_config = await self.db.get_captcha_config()
112
+ if captcha_config.browser_proxy_enabled and captcha_config.browser_proxy_url:
113
+ proxy_url = captcha_config.browser_proxy_url
114
 
115
  debug_logger.log_info(f"[BrowserCaptcha] 正在启动浏览器... (proxy={proxy_url or 'None'})")
116
  self.playwright = await async_playwright().start()