commit
This commit is contained in:
@@ -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"成功创建 Profile,ID: {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
|
||||
|
||||
Reference in New Issue
Block a user