commit
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
import requests
|
||||
import json
|
||||
from typing import Dict, Optional
|
||||
from loguru import logger
|
||||
from playwright.sync_api import sync_playwright, Browser, Page
|
||||
from config import Config
|
||||
import json
|
||||
|
||||
|
||||
class AdsPowerClient:
|
||||
@@ -28,6 +28,72 @@ class AdsPowerClient:
|
||||
logger.debug(f"AdsPowerClient 初始化 - API Key: {self.api_key[:8]}... (len={len(self.api_key)})")
|
||||
else:
|
||||
logger.debug("AdsPowerClient 初始化 - 未配置 API Key")
|
||||
|
||||
def _make_request(self, method: str, endpoint: str, json: Dict = None, params: Dict = None) -> Optional[Dict]:
|
||||
"""
|
||||
通用 API 请求方法
|
||||
|
||||
Args:
|
||||
method: HTTP方法 (GET/POST/PUT/DELETE)
|
||||
endpoint: API端点,如 '/api/v2/browser-profile/create'
|
||||
json: JSON请求体
|
||||
params: URL查询参数
|
||||
|
||||
Returns:
|
||||
响应JSON或None
|
||||
"""
|
||||
try:
|
||||
import json as json_lib # 避免参数名冲突
|
||||
|
||||
url = f"{self.api_url}{endpoint}"
|
||||
|
||||
headers = {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
if self.api_key:
|
||||
headers['Authorization'] = f'Bearer {self.api_key}'
|
||||
|
||||
# 记录请求
|
||||
logger.info("\n" + "="*70)
|
||||
logger.info(f"API请求: {method} {endpoint}")
|
||||
logger.info("="*70)
|
||||
logger.info(f"URL: {url}")
|
||||
if params:
|
||||
logger.info(f"Params:\n{json_lib.dumps(params, indent=2, ensure_ascii=False)}")
|
||||
if json:
|
||||
logger.info(f"Body:\n{json_lib.dumps(json, indent=2, ensure_ascii=False)}")
|
||||
|
||||
response = requests.request(
|
||||
method=method,
|
||||
url=url,
|
||||
json=json,
|
||||
params=params,
|
||||
headers=headers,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
# 记录响应
|
||||
logger.info("\n" + "-"*70)
|
||||
logger.info("API响应")
|
||||
logger.info("-"*70)
|
||||
logger.info(f"Status: {response.status_code}")
|
||||
|
||||
try:
|
||||
response_data = response.json()
|
||||
logger.info(f"Body:\n{json_lib.dumps(response_data, indent=2, ensure_ascii=False)}")
|
||||
except:
|
||||
logger.info(f"Body (Raw):\n{response.text}")
|
||||
response_data = None
|
||||
|
||||
logger.info("="*70 + "\n")
|
||||
|
||||
return response_data
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"API请求异常 [{method} {endpoint}]: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return None
|
||||
|
||||
def update_profile_proxy(self, profile_id: str, proxy_id: str) -> bool:
|
||||
"""
|
||||
@@ -553,21 +619,70 @@ 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']
|
||||
|
||||
# 初始化 Playwright
|
||||
if not self.playwright:
|
||||
self.playwright = sync_playwright().start()
|
||||
# 创建新的 Playwright 实例
|
||||
playwright = sync_playwright().start()
|
||||
|
||||
# 通过 CDP 连接到浏览器
|
||||
self.browser = self.playwright.chromium.connect_over_cdp(ws_endpoint)
|
||||
browser = playwright.chromium.connect_over_cdp(ws_endpoint)
|
||||
logger.info("成功通过 CDP 连接到 AdsPower 浏览器")
|
||||
|
||||
return self.browser
|
||||
# 保存引用
|
||||
self.playwright = playwright
|
||||
self.browser = browser
|
||||
|
||||
return browser
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"CDP 连接失败: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return None
|
||||
|
||||
def get_page(self, browser: Browser) -> Optional[Page]:
|
||||
@@ -912,7 +1027,119 @@ class AdsPowerClient:
|
||||
logger.error(f"检查浏览器状态异常: {str(e)}")
|
||||
return None
|
||||
|
||||
def list_profiles(self, group_id: str = None, page: int = 1, page_size: int = 100) -> Optional[Dict]:
|
||||
def list_groups(self, group_name: str = None, page: int = 1, page_size: int = 2000) -> Optional[Dict]:
|
||||
"""
|
||||
查询分组列表
|
||||
使用 AdsPower API v1
|
||||
|
||||
Args:
|
||||
group_name: 分组名称(可选)
|
||||
page: 页码
|
||||
page_size: 每页数量(范围 1 ~ 2000)
|
||||
|
||||
Returns:
|
||||
分组列表信息
|
||||
"""
|
||||
try:
|
||||
url = f"{self.api_url}/api/v1/group/list"
|
||||
|
||||
# 准备请求头
|
||||
headers = {}
|
||||
if self.api_key:
|
||||
headers['Authorization'] = f'Bearer {self.api_key}'
|
||||
|
||||
# 准备请求参数
|
||||
params = {
|
||||
"page": page,
|
||||
"page_size": page_size
|
||||
}
|
||||
if group_name:
|
||||
params["group_name"] = group_name
|
||||
|
||||
logger.info("\n" + "="*70)
|
||||
logger.info("📂 查询分组列表")
|
||||
logger.info("="*70)
|
||||
logger.info(f"URL: {url}")
|
||||
logger.info(f"Method: GET")
|
||||
logger.info(f"Params: {params}")
|
||||
|
||||
response = requests.get(url, params=params, 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:")
|
||||
logger.info(json.dumps(response_json, indent=2, ensure_ascii=False))
|
||||
except:
|
||||
logger.info(f"Response Body (Raw):")
|
||||
logger.info(response.text)
|
||||
|
||||
logger.info("="*70 + "\n")
|
||||
|
||||
result = response_json if 'response_json' in locals() else response.json()
|
||||
|
||||
if result.get('code') == 0:
|
||||
groups = result.get('data', {}).get('list', [])
|
||||
logger.success(f"查询成功,找到 {len(groups)} 个分组")
|
||||
|
||||
if groups:
|
||||
logger.info("\n分组列表:")
|
||||
for idx, group in enumerate(groups, 1):
|
||||
group_id = group.get('group_id', 'N/A')
|
||||
group_name = group.get('group_name', 'N/A')
|
||||
remark = group.get('remark', '')
|
||||
logger.info(f" {idx}. ID: {group_id} | 名称: {group_name} | 备注: {remark}")
|
||||
|
||||
return result
|
||||
else:
|
||||
logger.error(f"查询分组失败: {result.get('msg')}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"查询分组异常: {str(e)}")
|
||||
return None
|
||||
|
||||
def get_group_by_env(self) -> Optional[str]:
|
||||
"""
|
||||
根据当前运行环境获取对应的分组ID
|
||||
dev环境查询 group_name=dev
|
||||
生产环境查询 group_name=prod
|
||||
|
||||
Returns:
|
||||
分组ID,失败返回None
|
||||
"""
|
||||
# 根据环境决定分组名
|
||||
group_name = 'dev' if Config.ENV == 'development' else 'prod'
|
||||
|
||||
logger.info(f"当前运行环境: {Config.ENV},查询分组: {group_name}")
|
||||
|
||||
# 等待一下避免请求过于频繁
|
||||
import time
|
||||
time.sleep(1.0)
|
||||
|
||||
# 查询分组
|
||||
result = self.list_groups(group_name=group_name)
|
||||
|
||||
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
|
||||
else:
|
||||
logger.warning(f"未找到名为 '{group_name}' 的分组")
|
||||
return None
|
||||
else:
|
||||
logger.error(f"查询分组失败")
|
||||
return None
|
||||
|
||||
def list_profiles(self, group_id: str = None, page: int = 1, page_size: int = 100,
|
||||
profile_id: list = None, profile_no: list = None,
|
||||
limit: int = None, sort_type: str = None, sort_order: str = None) -> Optional[Dict]:
|
||||
"""
|
||||
查询 Profile 列表
|
||||
使用 AdsPower API v2
|
||||
@@ -920,7 +1147,12 @@ class AdsPowerClient:
|
||||
Args:
|
||||
group_id: 组ID(可选)
|
||||
page: 页码
|
||||
page_size: 每页数量
|
||||
page_size: 每页数量(当limit未指定时使用)
|
||||
profile_id: 环境ID数组(可选)
|
||||
profile_no: 环境编号数组(可选)
|
||||
limit: 每页大小(范围 1 ~ 200)
|
||||
sort_type: 排序类型 (profile_no/last_open_time/created_time)
|
||||
sort_order: 排序顺序 (asc/desc)
|
||||
|
||||
Returns:
|
||||
Profile 列表信息
|
||||
@@ -938,10 +1170,18 @@ class AdsPowerClient:
|
||||
# 准备请求体
|
||||
payload = {
|
||||
"page": page,
|
||||
"page_size": page_size
|
||||
"page_size": limit if limit else page_size
|
||||
}
|
||||
if group_id:
|
||||
payload["group_id"] = group_id
|
||||
if profile_id:
|
||||
payload["profile_id"] = profile_id
|
||||
if profile_no:
|
||||
payload["profile_no"] = profile_no
|
||||
if sort_type:
|
||||
payload["sort_type"] = sort_type
|
||||
if sort_order:
|
||||
payload["sort_order"] = sort_order
|
||||
|
||||
# 打印请求信息
|
||||
logger.info("\n" + "="*70)
|
||||
@@ -1018,6 +1258,35 @@ class AdsPowerClient:
|
||||
logger.error(f"查询 Profile 异常: {str(e)}")
|
||||
return None
|
||||
|
||||
def delete_profile(self, profile_id: str) -> bool:
|
||||
"""
|
||||
删除 Profile
|
||||
使用 AdsPower API v2
|
||||
|
||||
Args:
|
||||
profile_id: Profile ID
|
||||
|
||||
Returns:
|
||||
是否成功删除
|
||||
"""
|
||||
try:
|
||||
result = self._make_request(
|
||||
'POST',
|
||||
'/api/v2/browser-profile/delete',
|
||||
json={"profile_id": [profile_id]}
|
||||
)
|
||||
|
||||
if result and result.get('code') == 0:
|
||||
logger.success(f"✅ 成功删除 Profile: {profile_id}")
|
||||
return True
|
||||
else:
|
||||
logger.error(f"删除 Profile 失败: {result.get('msg') if result else '请求失败'}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"删除 Profile 异常: {str(e)}")
|
||||
return False
|
||||
|
||||
def __del__(self):
|
||||
"""析构函数,确保资源清理"""
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user