Commit ·
a1b7357
1
Parent(s): f63c37f
feat: 为无头模式设置独立代理
Browse files- src/api/admin.py +16 -2
- src/core/database.py +32 -6
- src/core/models.py +2 -0
- src/main.py +1 -1
- src/services/browser_captcha.py +48 -7
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 = ?,
|
|
|
|
| 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(
|
| 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,
|
| 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.
|
| 52 |
|
| 53 |
@classmethod
|
| 54 |
-
async def get_instance(cls,
|
| 55 |
"""获取单例实例"""
|
| 56 |
if cls._instance is None:
|
| 57 |
async with cls._lock:
|
| 58 |
if cls._instance is None:
|
| 59 |
-
cls._instance = cls(
|
| 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.
|
| 72 |
-
|
|
|
|
|
|
|
| 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()
|