Files

231 lines
7.6 KiB
Python
Raw Permalink Normal View History

#!/usr/bin/env python3
2026-06-26 17:24:23 +08:00
# app/cdp.py —— 共享 CDP 底座:连接 Chrome、定位/打开商品 tab、执行 JS、模拟拖拽。
# 被正式模块和 prototypes/demo.py 复用。
#
# 依赖:pip install websocket-client requests
import os
# CDP 局域网直连,绝不走代理(WSL 里 *_proxy 指向 :1080 会导致超时)。
for _k in ("ALL_PROXY", "all_proxy", "HTTP_PROXY", "http_proxy", "HTTPS_PROXY", "https_proxy"):
os.environ.pop(_k, None)
import json
import time
import threading
2026-06-26 17:24:23 +08:00
CDP_HOST = os.environ.get("CDP_HOST", "127.0.0.1:9222")
BASE = f"http://{CDP_HOST}"
2026-06-26 18:02:57 +08:00
def _base(host=None):
return f"http://{host or CDP_HOST}"
def http_get(path, host=None, timeout=10):
2026-06-26 17:24:23 +08:00
import requests
s = requests.Session()
s.trust_env = False # 忽略环境代理
return s.get(f"{_base(host)}{path}", timeout=timeout).json()
2026-06-27 16:39:12 +08:00
def close_tab(target_id, host=None):
"""Close a Chrome target/page by target id."""
if not target_id:
return False
import requests
s = requests.Session()
s.trust_env = False
response = s.get(f"{_base(host)}/json/close/{target_id}", timeout=10)
return response.ok
def wait_target_closed(target_id, host=None, timeout=2.0, poll_interval=0.1):
"""Wait briefly for a closed target to disappear from Chrome's target list."""
if not target_id:
return True
timeout = max(0.0, float(timeout))
poll_interval = max(0.01, float(poll_interval))
deadline = time.monotonic() + timeout
while True:
remaining = max(0.0, deadline - time.monotonic())
request_timeout = max(0.05, min(1.0, remaining))
try:
targets = http_get("/json", host=host, timeout=request_timeout)
except Exception:
targets = None
if targets is not None and not any(
str(target.get("id") or "") == str(target_id)
for target in targets
):
return True
remaining = deadline - time.monotonic()
if remaining <= 0:
return False
time.sleep(min(poll_interval, remaining))
def close_tab_and_wait(target_id, host=None, timeout=2.0, poll_interval=0.1):
"""Request target closure and confirm disappearance within a bounded budget."""
if not close_tab(target_id, host=host):
return False
return wait_target_closed(
target_id,
host=host,
timeout=timeout,
poll_interval=poll_interval,
)
2026-07-06 15:01:50 +08:00
def activate_tab(target_id, host=None):
"""Bring an existing Chrome target/page to the foreground."""
if not target_id:
return False
ver = http_get("/json/version", host=host)
b = CDP(ver["webSocketDebuggerUrl"])
try:
b.send("Target.activateTarget", {"targetId": target_id})
finally:
b.close()
return True
class CDP:
"""单个 target 的 CDP 客户端:命令同步、事件回调异步。"""
def __init__(self, ws_url):
2026-06-26 17:24:23 +08:00
from websocket import create_connection
# suppress_origin=True:不发 Origin 头,避免 Chrome 未开 --remote-allow-origins 时 403。
self.ws = create_connection(ws_url, max_size=None, suppress_origin=True)
self._id = 0
self._res = {}
self._cond = threading.Condition()
self._handlers = []
self._stop = False
threading.Thread(target=self._loop, daemon=True).start()
def _loop(self):
while not self._stop:
try:
raw = self.ws.recv()
except Exception:
break
if not raw:
continue
m = json.loads(raw)
if "id" in m:
with self._cond:
self._res[m["id"]] = m
self._cond.notify_all()
elif "method" in m:
for h in self._handlers:
try:
h(m)
except Exception:
pass
def on(self, fn):
self._handlers.append(fn)
def send(self, method, params=None, timeout=30):
self._id += 1
mid = self._id
self.ws.send(json.dumps({"id": mid, "method": method, "params": params or {}}))
end = time.time() + timeout
with self._cond:
while mid not in self._res:
left = end - time.time()
if left <= 0:
raise TimeoutError(f"CDP {method} 超时")
self._cond.wait(left)
m = self._res.pop(mid)
if "error" in m:
raise RuntimeError(f"{method}: {m['error'].get('message')}")
return m.get("result", {})
def ev(self, expr, by_value=True):
r = self.send("Runtime.evaluate", {"expression": expr, "returnByValue": by_value})
if "exceptionDetails" in r:
raise RuntimeError("JS 异常: " + r["exceptionDetails"].get("text", ""))
return r.get("result", {})
def val(self, expr):
return self.ev(expr).get("value")
def object_id(self, expr):
return self.ev(expr, by_value=False).get("objectId")
def drag(self, x0, y0, x1, y1, steps=34):
"""按住源点 → 多步移动到目标点 → 释放,驱动自定义拖拽排序库。"""
me = "Input.dispatchMouseEvent"
self.send(me, {"type": "mouseMoved", "x": x0, "y": y0})
self.send(me, {"type": "mousePressed", "x": x0, "y": y0, "button": "left", "buttons": 1, "clickCount": 1})
for dx in (3, 6, 10): # 小幅抖动,越过拖拽启动阈值
self.send(me, {"type": "mouseMoved", "x": x0 + dx, "y": y0, "button": "left", "buttons": 1})
time.sleep(0.03)
for i in range(1, steps + 1):
x = x0 + (x1 - x0) * i / steps
y = y0 + (y1 - y0) * i / steps
self.send(me, {"type": "mouseMoved", "x": x, "y": y, "button": "left", "buttons": 1})
time.sleep(0.025)
for _ in range(10): # 在目标点停留,让排序库吸附
self.send(me, {"type": "mouseMoved", "x": x1, "y": y1, "button": "left", "buttons": 1})
time.sleep(0.05)
self.send(me, {"type": "mouseReleased", "x": x1, "y": y1, "button": "left", "buttons": 0, "clickCount": 1})
def close(self):
self._stop = True
try:
self.ws.close()
except Exception:
pass
2026-06-26 18:02:57 +08:00
def find_product_tab(item_id, host=None):
"""在已打开的 tab 里找 URL 同时含 item_id 和 shopee.tw 的页面。"""
2026-06-26 18:02:57 +08:00
for t in http_get("/json", host=host):
if t.get("type") != "page":
continue
url = t.get("url") or ""
if item_id in url and "shopee.tw" in url:
return t
return None
def create_tab_info(url, host=None, background=False):
2026-06-27 16:39:12 +08:00
"""用 browser 级 Target.createTarget 新建 tab,返回其 page target 信息。"""
2026-06-26 18:02:57 +08:00
ver = http_get("/json/version", host=host)
b = CDP(ver["webSocketDebuggerUrl"])
try:
params = {"url": url}
if background:
params["background"] = True
try:
tid = b.send("Target.createTarget", params)["targetId"]
except RuntimeError:
if not background:
raise
tid = b.send("Target.createTarget", {"url": url})["targetId"]
finally:
b.close()
end = time.time() + 15
while time.time() < end:
2026-06-26 18:02:57 +08:00
for t in http_get("/json", host=host):
if t.get("id") == tid and t.get("webSocketDebuggerUrl"):
2026-06-27 16:39:12 +08:00
return t
time.sleep(0.5)
raise TimeoutError("等待新 tab websocket 超时")
2026-06-27 16:39:12 +08:00
def create_tab(url, host=None):
"""用 browser 级 Target.createTarget 新建 tab,返回其 page websocket。"""
return create_tab_info(url, host=host)["webSocketDebuggerUrl"]