ads_power_client.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. """
  2. AdsPower 指纹浏览器连接客户端
  3. 通过 AdsPower 本地 API 连接指纹浏览器,并返回 Playwright browser 实例;默认永不关闭用户浏览器
  4. """
  5. import time
  6. import requests
  7. from typing import Optional
  8. from playwright.sync_api import Browser, sync_playwright, Page
  9. class AdsPowerClient:
  10. """AdsPower 客户端"""
  11. def __init__(self, base_url: str = "http://127.0.0.1:50325", api_key: str = ""):
  12. self.base_url = base_url.rstrip("/")
  13. self.api_key = api_key
  14. self.profile_id: Optional[str] = None
  15. self.browser: Optional[Browser] = None
  16. self.playwright = None
  17. self._ws_endpoint: Optional[str] = None
  18. def _headers(self) -> dict:
  19. # AdsPower 本地 API 使用 query param 'apikey' 认证,不使用 header
  20. return {}
  21. def start_browser(self, profile_id: str, headless: bool = False) -> Browser:
  22. """
  23. 启动 AdsPower 浏览器并返回 Playwright Browser 实例
  24. """
  25. self.profile_id = profile_id
  26. url = f"{self.base_url}/api/v1/browser/start"
  27. params = {
  28. "user_id": profile_id,
  29. "headless": int(headless),
  30. }
  31. if self.api_key:
  32. params["apikey"] = self.api_key
  33. try:
  34. resp = requests.get(url, params=params, headers=self._headers(), timeout=30)
  35. resp.raise_for_status()
  36. except requests.RequestException as e:
  37. raise ConnectionError(f"无法连接 AdsPower API: {e}") from e
  38. data = resp.json()
  39. if data.get("code") != 0:
  40. raise RuntimeError(f"AdsPower 启动浏览器失败: {data}")
  41. ws_data = data.get("data", {}).get("ws", {})
  42. # AdsPower 返回的 ws endpoint,优先 puppeteer
  43. ws_endpoint = ws_data.get("puppeteer") or ws_data.get("selenium")
  44. if not ws_endpoint:
  45. raise RuntimeError(f"AdsPower 未返回 ws endpoint: {data}")
  46. self._ws_endpoint = ws_endpoint
  47. # 等待浏览器启动
  48. time.sleep(2)
  49. self.playwright = sync_playwright().start()
  50. # 连接 CDP
  51. try:
  52. self.browser = self.playwright.chromium.connect_over_cdp(ws_endpoint)
  53. except Exception as e:
  54. raise ConnectionError(f"Playwright 连接 AdsPower 浏览器失败: {e}") from e
  55. return self.browser
  56. def new_page(self) -> Page:
  57. """新建一个页面"""
  58. if not self.browser:
  59. raise RuntimeError("浏览器未启动,请先调用 start_browser()")
  60. context = self.browser.contexts[0] if self.browser.contexts else self.browser.new_context()
  61. return context.new_page()
  62. def get_open_page(self) -> Optional[Page]:
  63. """获取 AdsPower 启动时已打开的页面"""
  64. if not self.browser:
  65. return None
  66. if self.browser.contexts and self.browser.contexts[0].pages:
  67. return self.browser.contexts[0].pages[0]
  68. return None
  69. def close_browser(self) -> bool:
  70. """
  71. Compatibility method: detach Playwright only, never stop or close AdsPower.
  72. v4.1 hard rule: scripts must not close the user's logged-in fingerprint
  73. browser. Keep this method name for older callers, but make it safe.
  74. """
  75. self.browser = None
  76. if self.playwright:
  77. try:
  78. self.playwright.stop()
  79. except Exception:
  80. pass
  81. self.playwright = None
  82. print("AdsPower browser left open; Playwright connection stopped only.")
  83. return True
  84. def __enter__(self):
  85. return self
  86. def __exit__(self, exc_type, exc_val, exc_tb):
  87. self.close_browser()
  88. def test_connection(base_url: str = "http://127.0.0.1:50325", profile_id: str = ""):
  89. """测试 AdsPower 连接"""
  90. if not profile_id:
  91. print("请提供 profile_id")
  92. return
  93. client = AdsPowerClient(base_url)
  94. try:
  95. browser = client.start_browser(profile_id)
  96. page = client.get_open_page() or client.new_page()
  97. page.goto("https://www.facebook.com")
  98. print(f"页面标题: {page.title()}")
  99. time.sleep(3)
  100. finally:
  101. client.close_browser()
  102. if __name__ == "__main__":
  103. import sys
  104. if len(sys.argv) >= 3:
  105. test_connection(sys.argv[1], sys.argv[2])
  106. else:
  107. print("用法: python ads_power_client.py <base_url> <profile_id>")
  108. print("示例: python ads_power_client.py http://127.0.0.1:50325 abc123")