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 class AdsPowerClient: """AdsPower API客户端 - 集成 Playwright CDP 参考官方示例: localAPI-main/py-examples/example-start-profile.py """ # 大麦IP代理配置 DAMAI_API_URL = "https://api2.damaiip.com/index.php?s=/front/user/getIPlist&xsn=2912cb2b22d3b7ae724f045012790479&osn=TC_NO176707424165606223&tiqu=1" DAMAI_USER = "69538fdef04e1" DAMAI_PASSWORD = "63v0kQBr2yJXnjf" def __init__(self, api_url: str = None, user_id: str = None, api_key: str = None): self.api_url = api_url or Config.ADSPOWER_API_URL self.user_id = user_id or Config.ADSPOWER_USER_ID self.api_key = api_key or Config.ADSPOWER_API_KEY self.playwright = None self.browser = None # 调试信息 if self.api_key: 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: """ 更新 Profile 的代理配置 使用 AdsPower API v2 Args: profile_id: Profile ID proxy_id: 代理ID Returns: 是否成功 """ try: url = f"{self.api_url}/api/v2/browser-profile/update" # 准备请求头 headers = { 'Content-Type': 'application/json' } if self.api_key: headers['Authorization'] = f'Bearer {self.api_key}' # 准备请求体 payload = { "profile_id": profile_id, "proxy_id": proxy_id } logger.info("\n" + "="*70) logger.info("更新 Profile 代理") logger.info("="*70) logger.info(f"URL: {url}") logger.info(f"Method: POST") logger.info(f"Payload:") logger.info(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("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: logger.success(f"成功更新 Profile 代理: {profile_id} -> Proxy {proxy_id}") # 验证代理是否真正更新 import time time.sleep(1) # 等待配置生效 verify_result = self.get_profile_info(profile_id) if verify_result: actual_proxy_id = verify_result.get('data', {}).get('proxy_id', '') logger.info(f"验证代理配置 - 期望: {proxy_id}, 实际: {actual_proxy_id}") if actual_proxy_id == proxy_id: logger.success("✅ 代理配置验证通过") return True else: logger.warning(f"❌ 代理配置验证失败 - Profile中的proxy_id不匹配") return False else: logger.warning("无法验证代理配置,但API返回成功") return True else: logger.error(f"更新 Profile 代理失败: {result.get('msg')}") return False except Exception as e: logger.error(f"更新 Profile 代理异常: {str(e)}") return False def get_profile_info(self, profile_id: str) -> Optional[Dict]: """ 查询 Profile 详细信息 使用 AdsPower API v2 Args: profile_id: Profile ID Returns: Profile 信息 """ try: url = f"{self.api_url}/api/v2/browser-profile/list" headers = { 'Content-Type': 'application/json' } if self.api_key: headers['Authorization'] = f'Bearer {self.api_key}' payload = { "profile_id": profile_id } response = requests.post(url, json=payload, headers=headers, timeout=30) result = response.json() if result.get('code') == 0: profiles = result.get('data', {}).get('list', []) if profiles: return {'code': 0, 'data': profiles[0]} return None except Exception as e: logger.error(f"查询 Profile 信息异常: {str(e)}") return None def update_profile_proxy_v1(self, profile_id: str, proxy_config: Dict) -> bool: """ 使用 API v1 方式直接更新 Profile 的代理配置 传入完整的 user_proxy_config 对象 Args: profile_id: Profile ID (user_id) proxy_config: 代理配置字典,格式: { "proxy_type": "http", "proxy_host": "112.83.100.233", "proxy_port": "11059", "proxy_user": "69538fdef04e1", # 可选 "proxy_password": "63v0kQBr2yJXnjf", # 可选 "proxy_soft": "other" } Returns: 是否成功 """ try: url = f"{self.api_url}/api/v1/user/update" # 准备请求头 headers = { 'Content-Type': 'application/json' } if self.api_key: headers['Authorization'] = f'Bearer {self.api_key}' # 构建 user_proxy_config proxy_port = proxy_config.get("proxy_port") if isinstance(proxy_port, str): proxy_port = int(proxy_port) user_proxy_config = { "proxy_soft": proxy_config.get("proxy_soft", "other"), "proxy_type": proxy_config.get("proxy_type", "http"), "proxy_host": proxy_config["proxy_host"], "proxy_port": proxy_port # 使用整数类型 } # 如果有认证信息,添加到配置中 if proxy_config.get("proxy_user") and proxy_config.get("proxy_password"): user_proxy_config["proxy_user"] = proxy_config["proxy_user"] user_proxy_config["proxy_password"] = proxy_config["proxy_password"] logger.info("使用认证代理模式") else: logger.info("使用白名单代理模式(无认证)") # 准备请求体 payload = { "user_id": profile_id, "user_proxy_config": user_proxy_config } logger.info("\n" + "="*70) logger.info("更新 Profile 代理 (API v1)") logger.info("="*70) logger.info(f"URL: {url}") logger.info(f"Method: POST") logger.info(f"Headers:") for k, v in headers.items(): if k == 'Authorization' and v: logger.info(f" {k}: Bearer {v.split()[-1][:8]}...") else: logger.info(f" {k}: {v}") logger.info(f"Payload:") logger.info(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("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: logger.success(f"成功更新 Profile 代理 (API v1): {profile_id}") # 验证代理是否真正更新 import time time.sleep(1) # 等待配置生效 verify_result = self.get_profile_info(profile_id) if verify_result: actual_proxy = verify_result.get('data', {}).get('user_proxy_config', {}) actual_host = actual_proxy.get('proxy_host', '') actual_port = str(actual_proxy.get('proxy_port', '')) expected_host = proxy_config['proxy_host'] expected_port = str(proxy_port) logger.info(f"验证代理配置:") logger.info(f" 期望: {expected_host}:{expected_port}") logger.info(f" 实际: {actual_host}:{actual_port}") if actual_host == expected_host and actual_port == expected_port: logger.success("✅ 代理配置验证通过") return True else: logger.warning(f"❌ 代理配置验证失败 - 地址不匹配") return False else: logger.warning("无法验证代理配置,但API返回成功") return True else: logger.error(f"更新 Profile 代理失败 (API v1): {result.get('msg')}") return False except Exception as e: logger.error(f"更新 Profile 代理异常 (API v1): {str(e)}") return False def update_profile_proxy_v2_direct(self, profile_id: str, proxy_config: Dict) -> bool: """ 使用 API v2 方式直接传入 user_proxy_config 更新代理 根据官方文档,API v2 支持 proxy_id 和 user_proxy_config 两种方式 Args: profile_id: Profile ID proxy_config: 代理配置字典,格式: { "proxy_type": "http", "proxy_host": "112.83.100.233", "proxy_port": "11059", # 可以是字符串或整数 "proxy_user": "69538fdef04e1", # 可选 "proxy_password": "63v0kQBr2yJXnjf", # 可选 "proxy_soft": "other" } Returns: 是否成功 """ try: url = f"{self.api_url}/api/v2/browser-profile/update" # 准备请求头 headers = { 'Content-Type': 'application/json' } if self.api_key: headers['Authorization'] = f'Bearer {self.api_key}' # 解析端口(确保是整数类型) proxy_port = proxy_config.get("proxy_port") if isinstance(proxy_port, str): proxy_port = int(proxy_port) # 构建 user_proxy_config user_proxy_config = { "proxy_soft": proxy_config.get("proxy_soft", "other"), "proxy_type": proxy_config.get("proxy_type", "http"), "proxy_host": proxy_config["proxy_host"], "proxy_port": proxy_port # 使用整数类型! } # 如果有认证信息,添加到配置中 if proxy_config.get("proxy_user") and proxy_config.get("proxy_password"): user_proxy_config["proxy_user"] = proxy_config["proxy_user"] user_proxy_config["proxy_password"] = proxy_config["proxy_password"] logger.info("使用认证代理模式") else: logger.info("使用白名单代理模式(无认证)") # 准备请求体(注意:API v2 使用 profile_id 而不是 user_id) payload = { "profile_id": profile_id, "user_proxy_config": user_proxy_config } logger.info("\n" + "="*70) logger.info("更新 Profile 代理 (API v2 - Direct user_proxy_config)") logger.info("="*70) logger.info(f"URL: {url}") logger.info(f"Method: POST") logger.info(f"Headers:") for k, v in headers.items(): if k == 'Authorization' and v: logger.info(f" {k}: Bearer {v.split()[-1][:8]}...") else: logger.info(f" {k}: {v}") logger.info(f"Payload:") logger.info(json.dumps(payload, indent=2, ensure_ascii=False)) logger.info(f"注意: proxy_port类型为 {type(user_proxy_config['proxy_port']).__name__}") 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("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: logger.success(f"成功更新 Profile 代理 (API v2 Direct): {profile_id}") # 验证代理是否真正更新 import time time.sleep(1) verify_result = self.get_profile_info(profile_id) if verify_result: actual_proxy = verify_result.get('data', {}).get('user_proxy_config', {}) actual_host = actual_proxy.get('proxy_host', '') actual_port = str(actual_proxy.get('proxy_port', '')) expected_host = proxy_config['proxy_host'] expected_port = str(proxy_port) logger.info(f"验证代理配置:") logger.info(f" 期望: {expected_host}:{expected_port}") logger.info(f" 实际: {actual_host}:{actual_port}") if actual_host == expected_host and actual_port == expected_port: logger.success("✅ 代理配置验证通过") return True else: logger.warning(f"❌ 代理配置验证失败 - 地址不匹配") return False else: logger.warning("无法验证代理配置,但API返回成功") return True else: logger.error(f"更新 Profile 代理失败 (API v2 Direct): {result.get('msg')}") return False except Exception as e: logger.error(f"更新 Profile 代理异常 (API v2 Direct): {str(e)}") return False def setup_proxy_and_start(self, user_id: str = None, use_proxy: bool = True) -> Optional[Dict]: """ 设置代理并启动浏览器 Args: user_id: AdsPower用户ID use_proxy: 是否使用代理 Returns: 浏览器信息 """ target_user_id = user_id or self.user_id if use_proxy: # 1. 获取大麦IP代理 proxy_info = self.get_damai_proxy() if not proxy_info: logger.warning("获取代理失败,将不使用代理启动浏览器") return self.start_browser(user_id=target_user_id) # 2. 创建 AdsPower 代理 proxy_config = { "type": "http", "host": proxy_info["host"], "port": proxy_info["port"], "user": self.DAMAI_USER, "password": self.DAMAI_PASSWORD, "ipchecker": "ip2location", "remark": "Damai Auto Proxy" } proxy_id = self.create_proxy(proxy_config) if not proxy_id: logger.warning("创建代理失败,将不使用代理启动浏览器") return self.start_browser(user_id=target_user_id) # 3. 更新 Profile 使用新代理 if not self.update_profile_proxy(target_user_id, proxy_id): logger.warning("更新 Profile 代理失败,将不使用代理启动浏览器") return self.start_browser(user_id=target_user_id) # 4. 启动浏览器 return self.start_browser(user_id=target_user_id) def start_browser(self, user_id: str = None) -> Optional[Dict]: """ 启动浏览器 使用 AdsPower API v2 Args: user_id: AdsPower用户ID(Profile ID) Returns: 包含浏览器连接信息的字典 """ target_user_id = user_id or self.user_id if not target_user_id: logger.error("未设置 AdsPower User ID") return None try: # 使用 AdsPower API v2 url = f"{self.api_url}/api/v2/browser-profile/start" # 准备请求头 headers = { 'Content-Type': 'application/json' } if self.api_key: headers['Authorization'] = f'Bearer {self.api_key}' # 准备请求体 payload = { "profile_id": target_user_id, "launch_args": [], # 可以根据需要添加启动参数 "headless": "0", "last_opened_tabs": "1", "proxy_detection": "1", "password_filling": "0", "password_saving": "0", "cdp_mask": "1", "delete_cache": "0", "device_scale": "1" } # 打印请求信息 logger.info("\n" + "="*70) logger.info("📤 启动浏览器请求 (API v2)") logger.info("="*70) logger.info(f"URL: {url}") logger.info(f"Method: POST") logger.info(f"Headers:") for key, value in headers.items(): if key == 'Authorization' and 'Bearer' in value: display_value = f"Bearer {value.split(' ')[1][:8]}..." else: display_value = value logger.info(f" {key}: {display_value}") logger.info(f"Payload:") logger.info(f" {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}") logger.info(f"Headers:") for key, value in response.headers.items(): logger.info(f" {key}: {value}") # 格式化 JSON 响应 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: logger.info(f"成功启动浏览器,User ID: {target_user_id}") # v2 API 的响应结构可能不同,需要根据实际返回调整 if 'data' in result and 'ws' in result['data']: logger.info(f"Selenium: {result['data']['ws'].get('selenium', 'N/A')}") logger.info(f"Puppeteer: {result['data']['ws'].get('puppeteer', 'N/A')}") return result else: logger.error(f"启动浏览器失败: {result.get('msg')}") return None except Exception as e: logger.error(f"启动浏览器异常: {str(e)}") return None def connect_browser(self, browser_info: Dict) -> Optional[Browser]: """ 通过 CDP 连接到 AdsPower 浏览器 Args: browser_info: start_browser 返回的浏览器信息 Returns: 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 实例 playwright = sync_playwright().start() # 通过 CDP 连接到浏览器 browser = playwright.chromium.connect_over_cdp(ws_endpoint) logger.info("成功通过 CDP 连接到 AdsPower 浏览器") # 保存引用 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]: """ 获取或创建浏览器页面 Args: browser: Browser 实例 Returns: Page 实例 """ try: # 获取默认上下文 if browser.contexts: context = browser.contexts[0] else: context = browser.new_context() # 获取或创建页面 if context.pages: page = context.pages[0] else: page = context.new_page() logger.info("成功获取浏览器页面") return page except Exception as e: logger.error(f"获取页面失败: {str(e)}") return None def stop_browser(self, user_id: str = None) -> bool: """ 停止浏览器 使用 AdsPower API v2 Args: user_id: AdsPower用户ID Returns: 是否成功停止 """ target_user_id = user_id or self.user_id if not target_user_id: logger.error("未设置 AdsPower User ID") return False # 清理 Playwright 资源 try: if self.browser: self.browser.close() self.browser = None if self.playwright: self.playwright.stop() self.playwright = None except Exception as e: logger.warning(f"清理 Playwright 资源异常: {str(e)}") try: # 使用 AdsPower API v2 url = f"{self.api_url}/api/v2/browser-profile/stop" # 准备请求头 headers = { 'Content-Type': 'application/json' } if self.api_key: headers['Authorization'] = f'Bearer {self.api_key}' # 准备请求体 payload = { "profile_id": target_user_id } logger.info("\n" + "="*70) logger.info("🛑 停止浏览器请求 (API v2)") logger.info("="*70) logger.info(f"URL: {url}") logger.info(f"Method: POST") logger.info(f"Headers:") for key, value in headers.items(): if key == 'Authorization' and 'Bearer' in value: display_value = f"Bearer {value.split(' ')[1][:8]}..." else: display_value = value logger.info(f" {key}: {display_value}") logger.info(f"Payload:") logger.info(f" {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}") # 格式化 JSON 响应 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: logger.info(f"成功停止浏览器,User ID: {target_user_id}") return True else: logger.error(f"停止浏览器失败: {result.get('msg')}") return False except Exception as e: logger.error(f"停止浏览器异常: {str(e)}") return False def get_damai_proxy(self) -> Optional[Dict]: """ 从大麦IP代理池获取代理 Returns: 包含host和port的字典,失败返回None """ try: logger.info("\n" + "="*70) logger.info("获取大麦IP代理") logger.info("="*70) logger.info(f"URL: {self.DAMAI_API_URL}") # 禁用代理,直接连接大麦API proxies = { 'http': None, 'https': None } response = requests.get(self.DAMAI_API_URL, proxies=proxies, timeout=10) logger.info(f"Status Code: {response.status_code}") logger.info(f"Response: {response.text}") if response.status_code == 200 and response.text: # 解析返回的IP:端口格式 proxy_str = response.text.strip() if ':' in proxy_str: host, port = proxy_str.split(':', 1) logger.success(f"成功获取代理: {host}:{port}") logger.info("="*70 + "\n") return { "host": host, "port": port } else: logger.error(f"代理格式错误: {proxy_str}") return None else: logger.error(f"获取代理失败: {response.text}") return None except Exception as e: logger.error(f"获取大麦代理异常: {str(e)}") return None def create_proxy(self, proxy_config: Dict) -> Optional[str]: """ 在 AdsPower 中创建代理 使用 AdsPower API v2 Args: proxy_config: 代理配置字典 Returns: 代理ID,失败返回None """ try: url = f"{self.api_url}/api/v2/proxy-list/create" # 准备请求头 headers = { 'Content-Type': 'application/json' } if self.api_key: headers['Authorization'] = f'Bearer {self.api_key}' # 准备请求体(数组格式) payload = [proxy_config] logger.info("\n" + "="*70) logger.info("创建 AdsPower 代理") logger.info("="*70) logger.info(f"URL: {url}") logger.info(f"Method: POST") logger.info(f"Payload:") logger.info(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("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: # 获取创建的代理ID proxy_data = result.get('data', {}) proxy_ids = proxy_data.get('proxy_id', []) if proxy_ids and len(proxy_ids) > 0: proxy_id = proxy_ids[0] logger.success(f"成功创建代理,ID: {proxy_id}") return proxy_id else: logger.error("创建代理成功但未返回ID") return None else: logger.error(f"创建代理失败: {result.get('msg')}") return None except Exception as e: logger.error(f"创建代理异常: {str(e)}") return None def list_proxies(self, proxy_ids: list = None, page: int = 1, limit: int = 100) -> Optional[Dict]: """ 查询代理列表 使用 AdsPower API v2 Args: proxy_ids: 代理ID列表,可选 page: 页码,默认1 limit: 每页数量,默认100 Returns: 代理列表数据 """ try: url = f"{self.api_url}/api/v2/proxy-list/list" # 准备请求头 headers = { 'Content-Type': 'application/json' } if self.api_key: headers['Authorization'] = f'Bearer {self.api_key}' # 准备请求体 payload = { "page": str(page), "limit": str(limit) } if proxy_ids: payload["proxy_id"] = proxy_ids logger.info("\n" + "="*70) logger.info("查询代理列表 (API v2)") logger.info("="*70) logger.info(f"URL: {url}") logger.info(f"Method: POST") logger.info(f"Headers:") for k, v in headers.items(): if k == 'Authorization' and v: logger.info(f" {k}: Bearer {v.split()[-1][:8]}...") else: logger.info(f" {k}: {v}") logger.info(f"Payload:") logger.info(f" {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}") logger.info("Response Body:") try: response_json = response.json() logger.info(json.dumps(response_json, indent=2, ensure_ascii=False)) except: logger.info(response.text) logger.info("="*70 + "\n") result = response_json if 'response_json' in locals() else response.json() if result.get('code') == 0: proxy_list = result.get('data', {}).get('list', []) logger.success(f"查询成功,找到 {len(proxy_list)} 个代理") return result else: logger.error(f"查询代理列表失败: {result.get('msg')}") return None except Exception as e: logger.error(f"查询代理列表异常: {str(e)}") return None def check_browser_status(self, user_id: str = None) -> Optional[Dict]: """ 检查浏览器状态 Args: user_id: AdsPower用户ID Returns: 浏览器状态信息 """ target_user_id = user_id or self.user_id if not target_user_id: logger.error("未设置 AdsPower User ID") return None try: url = f"{self.api_url}/api/v1/browser/active?user_id={target_user_id}" # 准备请求头 headers = {} if self.api_key: headers['Authorization'] = f'Bearer {self.api_key}' response = requests.get(url, headers=headers, timeout=10) result = response.json() return result except Exception as e: logger.error(f"检查浏览器状态异常: {str(e)}") return None 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 Args: group_id: 组ID(可选) page: 页码 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 列表信息 """ try: url = f"{self.api_url}/api/v2/browser-profile/list" # 准备请求头 headers = { 'Content-Type': 'application/json' } if self.api_key: headers['Authorization'] = f'Bearer {self.api_key}' # 准备请求体 payload = { "page": page, "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) logger.info("📋 查询 Profile 列表 (API v2)") logger.info("="*70) logger.info(f"URL: {url}") logger.info(f"Method: POST") logger.info(f"Headers:") for key, value in headers.items(): if key == 'Authorization' and 'Bearer' in value: display_value = f"Bearer {value.split(' ')[1][:8]}..." else: display_value = value logger.info(f" {key}: {display_value}") logger.info(f"Payload:") logger.info(f" {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}") # 格式化 JSON 响应(限制长度) try: response_json = response.json() # 如果响应太长,只显示关键部分 if len(str(response_json)) > 2000: logger.info(f"Response Body (摘要):") summary = { "code": response_json.get("code"), "msg": response_json.get("msg"), "data": { "total": response_json.get("data", {}).get("total"), "list_count": len(response_json.get("data", {}).get("list", [])) } } logger.info(json.dumps(summary, indent=2, ensure_ascii=False)) else: 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[:500]) logger.info("="*70 + "\n") result = response_json if 'response_json' in locals() else response.json() if result.get('code') == 0: profiles = result.get('data', {}).get('list', []) logger.success(f"查询成功,找到 {len(profiles)} 个 Profile") # 显示 Profile 信息 if profiles: logger.info("\nProfile 列表:") for idx, profile in enumerate(profiles[:10], 1): # 只显示前10个 profile_id = profile.get('profile_id', 'N/A') profile_name = profile.get('name', 'N/A') group_name = profile.get('group_name', 'N/A') logger.info(f" {idx}. ID: {profile_id} | 名称: {profile_name} | 组: {group_name}") if len(profiles) > 10: logger.info(f" ...还有 {len(profiles) - 10} 个 Profile") return result 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 使用 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: if self.browser: self.browser.close() if self.playwright: self.playwright.stop() except: pass if __name__ == "__main__": """快速测试 AdsPower 连接""" logger.info("开始测试 AdsPower API v2 连接") logger.info("="*50) # 创建客户端 client = AdsPowerClient() # 步骤 1: 先查询 Profile 列表 logger.info("\n步骤 1: 查询 Profile 列表") result = client.list_profiles() if not result: logger.error("❌ 查询 Profile 失败,测试终止") exit(1) profiles = result.get('data', {}).get('list', []) if not profiles: logger.error("❌ 没有可用的 Profile,请先在 AdsPower 中创建 Profile") exit(1) # 使用第一个 Profile first_profile = profiles[0] profile_id = first_profile.get('profile_id') profile_name = first_profile.get('name', 'N/A') logger.success(f"\n将使用 Profile: {profile_name} (ID: {profile_id})") # 提示用户是否继续 choice = input(f"\n是否启动此 Profile? (y/n): ").strip().lower() if choice != 'y': logger.info("用户取消操作") exit(0) # 问是否使用代理 use_proxy_choice = input(f"\n是否使用大麦IP代理? (y/n): ").strip().lower() use_proxy = use_proxy_choice == 'y' # 步骤 2: 设置代理并启动浏览器 logger.info(f"\n步骤 2: {'[使用代理] ' if use_proxy else ''}启动浏览器 {profile_id}") result = client.setup_proxy_and_start(user_id=profile_id, use_proxy=use_proxy) if result: logger.success("✅ 浏览器启动成功!") # 等待用户确认 input("\n按 Enter 键停止浏览器...") # 步骤 3: 停止浏览器 logger.info(f"\n步骤 3: 停止浏览器 {profile_id}") if client.stop_browser(user_id=profile_id): logger.success("✅ 浏览器停止成功!") else: logger.error("❌ 浏览器停止失败") else: logger.error("❌ 浏览器启动失败") logger.info("="*50) logger.info("测试完成")