This commit is contained in:
sjk
2026-02-24 12:46:35 +08:00
parent be0c13e1a6
commit 85224e01e6
116 changed files with 8380 additions and 9642 deletions

View File

@@ -477,9 +477,10 @@ class AdsPowerClient:
use_proxy: 是否使用代理
Returns:
浏览器信息
浏览器信息,如果使用代理会包含 proxy_id 字段
"""
target_user_id = user_id or self.user_id
proxy_id = None
if use_proxy:
# 1. 获取大麦IP代理
@@ -507,10 +508,18 @@ class AdsPowerClient:
# 3. 更新 Profile 使用新代理
if not self.update_profile_proxy(target_user_id, proxy_id):
logger.warning("更新 Profile 代理失败,将不使用代理启动浏览器")
# 删除刚创建的代理
self.delete_proxy(proxy_id)
return self.start_browser(user_id=target_user_id)
# 4. 启动浏览器
return self.start_browser(user_id=target_user_id)
result = self.start_browser(user_id=target_user_id)
# 5. 如果启动成功且有代理,在返回结果中添加 proxy_id
if result and proxy_id:
result['proxy_id'] = proxy_id
return result
def start_browser(self, user_id: str = None) -> Optional[Dict]:
"""
@@ -542,7 +551,11 @@ class AdsPowerClient:
# 准备请求体
payload = {
"profile_id": target_user_id,
"launch_args": [], # 可以根据需要添加启动参数
"launch_args": [
"--no-sandbox", # 禁用沙箱root用户必需
"--disable-setuid-sandbox", # 禁用setuid沙箱
"--disable-dev-shm-usage" # 避免/dev/shm空间不足
],
"headless": "0",
"last_opened_tabs": "1",
"proxy_detection": "1",
@@ -619,53 +632,31 @@ class AdsPowerClient:
Playwright Browser 实例
"""
try:
import asyncio
import sys
# 检测是否在 asyncio 事件循环中
try:
loop = asyncio.get_running_loop()
logger.warning("检测到 asyncio 事件循环,将在新线程中执行 Playwright")
# 在新线程中执行 Playwright 同步 API
import threading
result_container = {'browser': None, 'error': None}
def run_playwright():
try:
# 获取 CDP WebSocket 端点
ws_endpoint = browser_info['data']['ws']['puppeteer']
# 创建新的 Playwright 实例
playwright = sync_playwright().start()
# 通过 CDP 连接到浏览器
browser = playwright.chromium.connect_over_cdp(ws_endpoint)
logger.info("成功通过 CDP 连接到 AdsPower 浏览器")
# 保存引用
self.playwright = playwright
self.browser = browser
result_container['browser'] = browser
except Exception as e:
result_container['error'] = str(e)
thread = threading.Thread(target=run_playwright)
thread.start()
thread.join(timeout=30)
if result_container['error']:
raise Exception(result_container['error'])
return result_container['browser']
except RuntimeError:
# 没有运行中的事件循环,正常执行
pass
# 获取 CDP WebSocket 端点
ws_endpoint = browser_info['data']['ws']['puppeteer']
# 检查是否在 asyncio 事件循环中
import asyncio
try:
loop = asyncio.get_running_loop()
# 如果有运行中的循环,使用线程来执行同步代码
import concurrent.futures
with concurrent.futures.ThreadPoolExecutor() as executor:
future = executor.submit(self._connect_browser_sync, ws_endpoint)
return future.result(timeout=30)
except RuntimeError:
# 没有运行中的循环,直接执行
return self._connect_browser_sync(ws_endpoint)
except Exception as e:
logger.error(f"CDP 连接失败: {str(e)}")
import traceback
traceback.print_exc()
return None
def _connect_browser_sync(self, ws_endpoint: str) -> Optional[Browser]:
"""同步执行浏览器连接"""
try:
# 创建新的 Playwright 实例
playwright = sync_playwright().start()
@@ -678,11 +669,8 @@ class AdsPowerClient:
self.browser = browser
return browser
except Exception as e:
logger.error(f"CDP 连接失败: {str(e)}")
import traceback
traceback.print_exc()
logger.error(f"CDP 连接失败(sync): {str(e)}")
return None
def get_page(self, browser: Browser) -> Optional[Page]:
@@ -804,6 +792,18 @@ class AdsPowerClient:
logger.error(f"停止浏览器异常: {str(e)}")
return False
def close_browser(self, profile_id: str = None) -> bool:
"""
关闭浏览器stop_browser的别名
Args:
profile_id: Profile ID
Returns:
是否成功关闭
"""
return self.stop_browser(user_id=profile_id)
def get_damai_proxy(self) -> Optional[Dict]:
"""
从大麦IP代理池获取代理
@@ -996,6 +996,67 @@ class AdsPowerClient:
logger.error(f"查询代理列表异常: {str(e)}")
return None
def delete_proxy(self, proxy_id: str) -> bool:
"""
删除代理
使用 AdsPower API v2
Args:
proxy_id: 代理ID
Returns:
是否成功删除
"""
try:
url = f"{self.api_url}/api/v2/proxy-list/delete"
# 准备请求头
headers = {
'Content-Type': 'application/json'
}
if self.api_key:
headers['Authorization'] = f'Bearer {self.api_key}'
# 准备请求体(数组格式)
payload = {
"proxy_id": [proxy_id]
}
logger.info("\n" + "="*70)
logger.info("删除 AdsPower 代理")
logger.info("="*70)
logger.info(f"URL: {url}")
logger.info(f"Method: POST")
logger.info(f"Payload: {json.dumps(payload, indent=2, ensure_ascii=False)}")
response = requests.post(url, json=payload, headers=headers, timeout=30)
logger.info("\n" + "-"*70)
logger.info("响应信息")
logger.info("-"*70)
logger.info(f"Status Code: {response.status_code}")
try:
response_json = response.json()
logger.info(f"Response Body: {json.dumps(response_json, indent=2, ensure_ascii=False)}")
except:
logger.info(f"Response Body (Raw): {response.text}")
logger.info("="*70 + "\n")
result = response_json if 'response_json' in locals() else response.json()
if result.get('code') == 0:
logger.success(f"成功删除代理ID: {proxy_id}")
return True
else:
logger.error(f"删除代理失败: {result.get('msg')}")
return False
except Exception as e:
logger.error(f"删除代理异常: {str(e)}")
return False
def check_browser_status(self, user_id: str = None) -> Optional[Dict]:
"""
检查浏览器状态
@@ -1127,9 +1188,16 @@ class AdsPowerClient:
if result and result.get('code') == 0:
groups = result.get('data', {}).get('list', [])
if groups:
group_id = groups[0].get('group_id')
logger.success(f"获取到分组ID: {group_id}")
return group_id
# 精确匹配分组名称API返回的可能包含多个包含关键词的分组
for group in groups:
if group.get('group_name') == group_name:
group_id = group.get('group_id')
logger.success(f"获取到分组ID: {group_id} (名称: {group_name})")
return group_id
# 如果没有精确匹配,记录警告
logger.warning(f"未找到精确匹配的分组 '{group_name}',返回的分组: {[g.get('group_name') for g in groups]}")
return None
else:
logger.warning(f"未找到名为 '{group_name}' 的分组")
return None
@@ -1258,6 +1326,85 @@ class AdsPowerClient:
logger.error(f"查询 Profile 异常: {str(e)}")
return None
def create_profile(self, group_id: str, name: str = None, proxy_id: str = None) -> Optional[str]:
"""
创建新的 Profile
使用 AdsPower API v2
Args:
group_id: 分组ID
name: Profile名称可选不填则自动生成
proxy_id: 代理ID必填
Returns:
创建的 Profile ID失败返回 None
"""
try:
url = f"{self.api_url}/api/v2/browser-profile/create"
# 准备请求头
headers = {
'Content-Type': 'application/json'
}
if self.api_key:
headers['Authorization'] = f'Bearer {self.api_key}'
# 准备请求体
import time
profile_name = name or f"auto_{int(time.time())}"
payload = {
"group_id": group_id,
"name": profile_name,
"platform": "health.baidu.com",
"proxyid": proxy_id,
"fingerprint_config": {
"automatic_timezone": "1",
"language": ["zh-CN", "zh"],
"ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
}
}
logger.info("\n" + "="*70)
logger.info("创建 Profile (API v2)")
logger.info("="*70)
logger.info(f"URL: {url}")
logger.info(f"Method: POST")
logger.info(f"Payload: {json.dumps(payload, indent=2, ensure_ascii=False)}")
response = requests.post(url, json=payload, headers=headers, timeout=30)
logger.info("\n" + "-"*70)
logger.info("响应信息")
logger.info("-"*70)
logger.info(f"Status Code: {response.status_code}")
try:
response_json = response.json()
logger.info(f"Response Body: {json.dumps(response_json, indent=2, ensure_ascii=False)}")
except:
logger.info(f"Response Body (Raw): {response.text}")
logger.info("="*70 + "\n")
result = response_json if 'response_json' in locals() else response.json()
if result.get('code') == 0:
profile_id = result.get('data', {}).get('profile_id')
if profile_id:
logger.success(f"成功创建 ProfileID: {profile_id}")
return profile_id
else:
logger.error("创建 Profile 成功但未返回ID")
return None
else:
logger.error(f"创建 Profile 失败: {result.get('msg')}")
return None
except Exception as e:
logger.error(f"创建 Profile 异常: {str(e)}")
return None
def delete_profile(self, profile_id: str) -> bool:
"""
删除 Profile