mooncat-browser-client 0.4.7__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of mooncat-browser-client might be problematic. Click here for more details.

@@ -0,0 +1,70 @@
1
+ Metadata-Version: 2.4
2
+ Name: mooncat-browser-client
3
+ Version: 0.4.7
4
+ Summary: Python client for the local MoonCat browser daemon
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://github.com/NINTING/mooncat-browser
7
+ Project-URL: Repository, https://github.com/NINTING/mooncat-browser
8
+ Project-URL: Issues, https://github.com/NINTING/mooncat-browser/issues
9
+ Keywords: browser,automation,mooncat,browserd
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3 :: Only
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Requires-Python: >=3.11
18
+ Description-Content-Type: text/markdown
19
+ Requires-Dist: httpx<1,>=0.27.0
20
+ Requires-Dist: markdownify<2,>=0.13.0
21
+
22
+ # MoonCat Browser Python Client
23
+
24
+ 这是 MoonCat 本地浏览器服务 `browserd` 的 Python 客户端。复杂的浏览器逻辑运行在
25
+ 统一后端中,Python 包只负责调用服务接口。
26
+
27
+ ## 安装
28
+
29
+ ```powershell
30
+ pip install mooncat-browser-client
31
+ ```
32
+
33
+ 先启动 MoonCat 本地浏览器服务,再运行 Python:
34
+
35
+ ```python
36
+ from browser import Browser
37
+
38
+ browser = Browser()
39
+ browser.open()
40
+ tab = browser.new_tab("https://example.com")
41
+ print(browser.inner_text(tab, "body"))
42
+ ```
43
+
44
+ `new_tab()` 返回包含 `pageHandle` 的标签信息。为方便使用,`operate()`、`click()`、
45
+ `fill()` 等方法既接受完整标签信息,也接受其中的 `pageHandle`。
46
+
47
+ ## 服务地址
48
+
49
+ 默认连接 `http://127.0.0.1:17322`。可直接指定外部服务端口:
50
+
51
+ ```python
52
+ browser = Browser(host="192.168.1.20", port=17322)
53
+ ```
54
+
55
+ 也可使用完整地址:
56
+
57
+ ```python
58
+ browser = Browser(base_url="http://192.168.1.20:17322")
59
+ ```
60
+
61
+ 支持环境变量:
62
+
63
+ - `MOONCAT_BROWSERD_BASE_URL`
64
+ - `MOONCAT_BROWSERD_HOST`
65
+ - `MOONCAT_BROWSERD_PORT`
66
+
67
+ ## 后端接口
68
+
69
+ 客户端提供浏览器打开/关闭、标签页、通用页面操作、Cookie、下载任务和自动下载设置接口。
70
+ 页面动作统一通过 `operate()` 发送给 browserd,Python 侧不重复实现浏览器逻辑。
@@ -0,0 +1,49 @@
1
+ # MoonCat Browser Python Client
2
+
3
+ 这是 MoonCat 本地浏览器服务 `browserd` 的 Python 客户端。复杂的浏览器逻辑运行在
4
+ 统一后端中,Python 包只负责调用服务接口。
5
+
6
+ ## 安装
7
+
8
+ ```powershell
9
+ pip install mooncat-browser-client
10
+ ```
11
+
12
+ 先启动 MoonCat 本地浏览器服务,再运行 Python:
13
+
14
+ ```python
15
+ from browser import Browser
16
+
17
+ browser = Browser()
18
+ browser.open()
19
+ tab = browser.new_tab("https://example.com")
20
+ print(browser.inner_text(tab, "body"))
21
+ ```
22
+
23
+ `new_tab()` 返回包含 `pageHandle` 的标签信息。为方便使用,`operate()`、`click()`、
24
+ `fill()` 等方法既接受完整标签信息,也接受其中的 `pageHandle`。
25
+
26
+ ## 服务地址
27
+
28
+ 默认连接 `http://127.0.0.1:17322`。可直接指定外部服务端口:
29
+
30
+ ```python
31
+ browser = Browser(host="192.168.1.20", port=17322)
32
+ ```
33
+
34
+ 也可使用完整地址:
35
+
36
+ ```python
37
+ browser = Browser(base_url="http://192.168.1.20:17322")
38
+ ```
39
+
40
+ 支持环境变量:
41
+
42
+ - `MOONCAT_BROWSERD_BASE_URL`
43
+ - `MOONCAT_BROWSERD_HOST`
44
+ - `MOONCAT_BROWSERD_PORT`
45
+
46
+ ## 后端接口
47
+
48
+ 客户端提供浏览器打开/关闭、标签页、通用页面操作、Cookie、下载任务和自动下载设置接口。
49
+ 页面动作统一通过 `operate()` 发送给 browserd,Python 侧不重复实现浏览器逻辑。
@@ -0,0 +1,138 @@
1
+ """web/behavior — 人类行为模拟 (路由无关, 全走 operate).
2
+
3
+ 忠实复刻 lib/web/behavior.js。
4
+ 贝塞尔鼠标轨迹 / 随机点击偏移 / 打字节奏 / 平滑滚动。
5
+ 所有页面动作统一走 operate(pageHandle, {action}),本模块只编排动作序列。
6
+
7
+ 依赖: browser (operate)
8
+ """
9
+ from __future__ import annotations
10
+ import math
11
+ import random
12
+ import time
13
+ from typing import Any
14
+
15
+ import sys
16
+ sys.path.insert(0, __file__.rsplit("\\", 1)[0] if "\\" in __file__ else __file__.rsplit("/", 1)[0])
17
+ from browser import operate # noqa: E402
18
+
19
+
20
+ def _bezier(p0: float, p1: float, p2: float, p3: float, t: float) -> float:
21
+ u = 1 - t
22
+ return u * u * u * p0 + 3 * u * u * t * p1 + 3 * u * t * t * p2 + t * t * t * p3
23
+
24
+
25
+ def _trajectory(from_pt: dict, to_pt: dict, steps: int) -> list[dict]:
26
+ """生成贝塞尔轨迹点 (含随机抖动)."""
27
+ pts = []
28
+ mx = (from_pt["x"] + to_pt["x"]) / 2 + (random.random() - 0.5) * abs(to_pt["x"] - from_pt["x"]) * 0.6
29
+ my = (from_pt["y"] + to_pt["y"]) / 2 + (random.random() - 0.5) * abs(to_pt["y"] - from_pt["y"]) * 0.6
30
+ c1 = {"x": from_pt["x"] + (mx - from_pt["x"]) * 0.3, "y": from_pt["y"] + (my - from_pt["y"]) * 0.3}
31
+ c2 = {"x": to_pt["x"] + (mx - to_pt["x"]) * 0.3, "y": to_pt["y"] + (my - to_pt["y"]) * 0.3}
32
+ for i in range(steps + 1):
33
+ t = i / steps
34
+ pts.append({
35
+ "x": _bezier(from_pt["x"], c1["x"], c2["x"], to_pt["x"], t) + (random.random() - 0.5) * 2,
36
+ "y": _bezier(from_pt["y"], c1["y"], c2["y"], to_pt["y"], t) + (random.random() - 0.5) * 2,
37
+ })
38
+ return pts
39
+
40
+
41
+ def _random_point_in_rect(rect: dict) -> dict:
42
+ return {
43
+ "x": rect["x"] + rect["width"] * (0.3 + random.random() * 0.4),
44
+ "y": rect["y"] + rect["height"] * (0.3 + random.random() * 0.4),
45
+ }
46
+
47
+
48
+ def _move_trajectory(page_handle: dict, from_pt: dict, to_pt: dict) -> None:
49
+ """沿贝塞尔轨迹多点 mouseMove 注入."""
50
+ pts = _trajectory(from_pt, to_pt, 10 + random.randint(0, 5))
51
+ for pt in pts:
52
+ operate(page_handle, {"action": "mouseMove", "x": pt["x"], "y": pt["y"]})
53
+ time.sleep(0.008 + random.random() * 0.016)
54
+
55
+
56
+ def create_behavior(page_handle: dict) -> dict:
57
+ """创建绑定到 pageHandle 的行为模拟器.
58
+
59
+ 返回: move_to, click, click_element, type_text, type_in_element, scroll_down, scroll_to_bottom, scroll_to_element
60
+ """
61
+ if not page_handle:
62
+ raise ValueError("create_behavior: page_handle required")
63
+ last_pos = {"x": 100, "y": 100}
64
+
65
+ def move_to(x: float, y: float) -> None:
66
+ nonlocal last_pos
67
+ _move_trajectory(page_handle, last_pos, {"x": x, "y": y})
68
+ last_pos = {"x": x, "y": y}
69
+
70
+ def click(x: float, y: float, options: dict | None = None) -> None:
71
+ nonlocal last_pos
72
+ _move_trajectory(page_handle, last_pos, {"x": x, "y": y})
73
+ last_pos = {"x": x, "y": y}
74
+ time.sleep(0.05 + random.random() * 0.1)
75
+ operate(page_handle, {"action": "click", "x": x, "y": y})
76
+ delay = (options or {}).get("delay", 0.05 + random.random() * 0.1)
77
+ time.sleep(delay)
78
+
79
+ def click_element(selector: str, timeout: int = 10000) -> None:
80
+ nonlocal last_pos
81
+ operate(page_handle, {"action": "waitForSelector", "selector": selector, "timeout": timeout})
82
+ box_r = operate(page_handle, {"action": "boundingBox", "selector": selector})
83
+ box = None
84
+ if isinstance(box_r, dict) and box_r.get("width") is not None:
85
+ box = box_r
86
+ if not box:
87
+ operate(page_handle, {"action": "click", "selector": selector})
88
+ time.sleep(0.05 + random.random() * 0.1)
89
+ return
90
+ pt = _random_point_in_rect(box)
91
+ move_to(pt["x"], pt["y"])
92
+ time.sleep(0.05 + random.random() * 0.1)
93
+ operate(page_handle, {"action": "click", "x": pt["x"], "y": pt["y"]})
94
+ time.sleep(0.05 + random.random() * 0.1)
95
+
96
+ def type_text(text: str, delay: float = 0.1) -> None:
97
+ for ch in text:
98
+ operate(page_handle, {"action": "press", "selector": "body", "key": ch})
99
+ d = delay * (0.5 + random.random())
100
+ if random.random() < 0.1:
101
+ d += 0.2 + random.random() * 0.4
102
+ time.sleep(d)
103
+
104
+ def type_in_element(selector: str, text: str, options: dict | None = None) -> None:
105
+ opts = options or {}
106
+ delay = opts.get("delay", 0.1)
107
+ timeout = opts.get("timeout", 10000)
108
+ operate(page_handle, {"action": "waitForSelector", "selector": selector, "timeout": timeout})
109
+ operate(page_handle, {"action": "click", "selector": selector})
110
+ operate(page_handle, {"action": "type", "selector": selector, "value": text, "delay": delay})
111
+
112
+ def scroll_down(pixels: int = 400) -> None:
113
+ step = 30 + random.randint(0, 29)
114
+ operate(page_handle, {
115
+ "action": "evaluate",
116
+ "source": f"(p) => new Promise((r) => {{ let done = 0; const t = setInterval(() => {{ window.scrollBy(0, {step}); done += {step}; if (done >= p) {{ clearInterval(t); r() }} }}, 30 + Math.random() * 30) }})",
117
+ "args": pixels
118
+ })
119
+
120
+ def scroll_to_bottom() -> None:
121
+ operate(page_handle, {
122
+ "action": "evaluate",
123
+ "source": "() => new Promise((resolve) => { let last=-1; const t=setInterval(()=>{ window.scrollBy(0,100+Math.random()*100); const h=document.body.scrollHeight; if(h===last){clearInterval(t);resolve()} last=h }, 150) })"
124
+ })
125
+
126
+ def scroll_to_element(selector: str, timeout: int = 10000) -> None:
127
+ operate(page_handle, {
128
+ "action": "evaluate",
129
+ "source": f'(s) => {{ const el = document.querySelector(s); if (el) el.scrollIntoView({{ behavior: "smooth", block: "center" }}) }}',
130
+ "args": selector
131
+ })
132
+
133
+ return {
134
+ "move_to": move_to, "click": click, "click_element": click_element,
135
+ "type_text": type_text, "type_in_element": type_in_element,
136
+ "scroll_down": scroll_down, "scroll_to_bottom": scroll_to_bottom,
137
+ "scroll_to_element": scroll_to_element,
138
+ }