This commit is contained in:
sjk
2026-01-16 22:06:46 +08:00
parent 816bf29a2a
commit 3d402639da
114 changed files with 10763 additions and 419 deletions

View File

@@ -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: