rootbrowse 0.2.0__py3-none-any.whl

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.
rootbrowse/__init__.py ADDED
@@ -0,0 +1,39 @@
1
+ """RootBrowse - Python 浏览器自动化 MCP 框架"""
2
+
3
+ from ._version import __version__
4
+ from .browser import Browser
5
+ from .tab_manager import TabManager
6
+ from .element_operator import ElementOperator
7
+ from .page_scanner import PageScanner
8
+ from .types import Region, Element, RegionSummary, ElementPreview, OperationResult
9
+ from .exceptions import (
10
+ RootBrowseError,
11
+ BrowserError,
12
+ ElementNotFoundError,
13
+ RegionNotFoundError,
14
+ TabNotFoundError,
15
+ OperationError,
16
+ StateFileError,
17
+ PageLoadError,
18
+ )
19
+
20
+ __all__ = [
21
+ "__version__",
22
+ "Browser",
23
+ "TabManager",
24
+ "ElementOperator",
25
+ "PageScanner",
26
+ "Region",
27
+ "Element",
28
+ "RegionSummary",
29
+ "ElementPreview",
30
+ "OperationResult",
31
+ "RootBrowseError",
32
+ "BrowserError",
33
+ "ElementNotFoundError",
34
+ "RegionNotFoundError",
35
+ "TabNotFoundError",
36
+ "OperationError",
37
+ "StateFileError",
38
+ "PageLoadError",
39
+ ]
rootbrowse/_version.py ADDED
@@ -0,0 +1,3 @@
1
+ """版本信息"""
2
+
3
+ __version__ = "0.1.0"
rootbrowse/browser.py ADDED
@@ -0,0 +1,181 @@
1
+ """浏览器主入口"""
2
+
3
+ import json
4
+ from typing import Any
5
+
6
+ from .constants import DEFAULT_URL
7
+ from .tab_manager import TabManager
8
+ from .element_operator import ElementOperator
9
+ from .page_scanner import PageScanner
10
+ from .types import Region, RegionSummary, Element, ElementPreview, OperationResult
11
+ from .exceptions import BrowserError, StateFileError, PageLoadError
12
+
13
+
14
+ class Browser:
15
+ """
16
+ 浏览器主入口,组合 TabManager、ElementOperator、PageScanner
17
+
18
+ AI 调用方式:
19
+ browser = Browser() # 默认打开 bing.com
20
+ browser = Browser('https://example.com') # 指定 URL
21
+ browser.view.get_regions()
22
+ browser.act.click('r1')
23
+ """
24
+
25
+ def __init__(self, page: Any | None = None, url: str | None = None, headless: bool = True):
26
+ """
27
+ 初始化 Browser
28
+
29
+ Args:
30
+ page: DrissionPage ChromiumPage 实例,None 则自动创建
31
+ url: 默认打开的 URL,配合 page 为 None 时使用
32
+ headless: 是否使用无头模式,默认 Ture(无头浏览器)
33
+ """
34
+ page_2 = page
35
+ if page is None:
36
+ from DrissionPage import ChromiumPage, ChromiumOptions
37
+ opts = ChromiumOptions()
38
+ # 有头还是无头浏览器
39
+ if headless:
40
+ opts.headless(True)
41
+ else:
42
+ opts.headless(False)
43
+
44
+ page = ChromiumPage(addr_or_opts=opts)
45
+
46
+ self._page = page
47
+ self._closed = False
48
+
49
+ # 初始化子模块
50
+ self._page_scanner = PageScanner(self._page)
51
+ self._tab_manager = TabManager(self._page)
52
+ self._element_operator = ElementOperator(self._page, self._page_scanner._element_map)
53
+
54
+ # 打开默认 URL(只有当 page 和 url 都没提供时才自动打开)
55
+ if page_2 is None and url is None:
56
+ self.get(DEFAULT_URL)
57
+
58
+ @classmethod
59
+ def create(cls, url: str | None = None) -> "Browser":
60
+ """
61
+ 工厂方法:创建 Browser 并打开指定 URL
62
+
63
+ Args:
64
+ url: 目标 URL,None 则打开 DEFAULT_URL (bing.com)
65
+
66
+ Returns:
67
+ Browser 实例
68
+ """
69
+ if url is None:
70
+ url = DEFAULT_URL
71
+ return cls(url=url)
72
+
73
+ @property
74
+ def tabs(self) -> TabManager:
75
+ """标签页管理器"""
76
+ return self._tab_manager
77
+
78
+ @property
79
+ def view(self) -> PageScanner:
80
+ """页面扫描器"""
81
+ return self._page_scanner
82
+
83
+ @property
84
+ def act(self) -> ElementOperator:
85
+ """元素操作器"""
86
+ return self._element_operator
87
+
88
+ def get(self, url: str, timeout: float = 30) -> dict:
89
+ """
90
+ 打开 URL
91
+
92
+ Args:
93
+ url: 目标 URL
94
+ timeout: 超时时间(秒)
95
+
96
+ Returns:
97
+ dict: {url, title}
98
+
99
+ Raises:
100
+ PageLoadError: 页面加载失败
101
+ """
102
+ try:
103
+ self._page.get(url, timeout=timeout)
104
+ title = self._page.title or ""
105
+ return {"url": url, "title": title}
106
+ except Exception as e:
107
+ raise PageLoadError(f"Failed to load page: {url}, error: {e}")
108
+
109
+ def screenshot(
110
+ self,
111
+ path: str | None = None,
112
+ annotate: list[list[int]] | None = None
113
+ ) -> str:
114
+ """
115
+ 页面截图
116
+
117
+ Args:
118
+ path: 保存路径,None 则返回 base64 字符串
119
+ annotate: 标注区域列表 [[x1,y1,x2,y2], ...]
120
+
121
+ Returns:
122
+ str: 文件路径或 base64 字符串
123
+ """
124
+ if path:
125
+ self._page.get_screenshot(path=path)
126
+ return path
127
+ else:
128
+ return self._page.screenshot()
129
+
130
+ def save_state(self, path: str) -> None:
131
+ """
132
+ 保存浏览器状态(cookies)
133
+
134
+ Args:
135
+ path: 保存路径
136
+
137
+ Raises:
138
+ StateFileError: 保存失败
139
+ """
140
+ try:
141
+ cookies = self._page.cookies()
142
+ data = {
143
+ 'version': '0.1.0',
144
+ 'cookies': cookies if isinstance(cookies, list) else list(cookies),
145
+ 'saved_at': self._get_timestamp(),
146
+ }
147
+ with open(path, 'w', encoding='utf-8') as f:
148
+ json.dump(data, f, ensure_ascii=False, indent=2)
149
+ except Exception as e:
150
+ raise StateFileError(f"Failed to save state: {e}")
151
+
152
+ def load_state(self, path: str) -> None:
153
+ """
154
+ 恢复浏览器状态(cookies)
155
+
156
+ Args:
157
+ path: 状态文件路径
158
+
159
+ Raises:
160
+ StateFileError: 恢复失败
161
+ """
162
+ try:
163
+ with open(path, 'r', encoding='utf-8') as f:
164
+ data = json.load(f)
165
+ cookies = data.get('cookies', [])
166
+ if cookies:
167
+ self._page.set.cookies(cookies)
168
+ except Exception as e:
169
+ raise StateFileError(f"Failed to load state: {e}")
170
+
171
+ @staticmethod
172
+ def _get_timestamp() -> str:
173
+ """返回当前时间戳 ISO 格式"""
174
+ from datetime import datetime, timezone
175
+ return datetime.now(timezone.utc).isoformat()
176
+
177
+ def close(self) -> None:
178
+ """关闭浏览器"""
179
+ if not self._closed:
180
+ self._page.quit()
181
+ self._closed = True
@@ -0,0 +1,62 @@
1
+ """常量配置"""
2
+
3
+ # 默认 URL
4
+ DEFAULT_URL = "https://www.bing.com"
5
+
6
+ # 可交互的 HTML 标签(可被点击、输入、选择的元素)
7
+ INTERACTIVE_TAGS = [
8
+ 'a', # 链接
9
+ 'button', # 按钮
10
+ 'input', # 输入框
11
+ 'select', # 下拉框
12
+ 'textarea', # 文本域
13
+ 'details', # 可折叠
14
+ 'summary', # 可折叠标题
15
+ 'menuitem', # 菜单项
16
+ ]
17
+
18
+ # 可输入的 HTML 标签
19
+ INPUT_TAGS = [
20
+ 'input',
21
+ 'textarea',
22
+ 'select',
23
+ ]
24
+
25
+ # 可点击的 HTML 标签
26
+ CLICKABLE_TAGS = [
27
+ 'a',
28
+ 'button',
29
+ 'input',
30
+ 'details',
31
+ 'summary',
32
+ 'menuitem',
33
+ ]
34
+
35
+ # ARIA role 映射
36
+ ROLE_TAG_MAP = {
37
+ 'link': 'a',
38
+ 'button': 'button',
39
+ 'textbox': 'input',
40
+ 'searchbox': 'input',
41
+ 'checkbox': 'input',
42
+ 'radio': 'input',
43
+ 'menuitem': 'menuitem',
44
+ }
45
+
46
+ # 区域 ID 命名
47
+ REGION_ID_HEADER = 'header'
48
+ REGION_ID_MAIN = 'main'
49
+ REGION_ID_SIDEBAR = 'sidebar'
50
+ REGION_ID_FOOTER = 'footer'
51
+ REGION_ID_NAV = 'nav'
52
+
53
+ # 默认配置
54
+ DEFAULT_LIMIT = 20
55
+ DEFAULT_OFFSET = 0
56
+ DEFAULT_TIMEOUT = 10
57
+
58
+ # ref 前缀
59
+ REF_PREFIX = 'r'
60
+
61
+ # 状态文件扩展名
62
+ STATE_FILE_EXTENSION = '.state.json'
@@ -0,0 +1,219 @@
1
+ """元素操作器"""
2
+
3
+ from typing import Any
4
+
5
+ from .types import Element, OperationResult
6
+ from .exceptions import ElementNotFoundError
7
+
8
+
9
+ class ElementOperator:
10
+ """元素操作器,执行点击、输入、悬停等操作"""
11
+
12
+ def __init__(self, page: Any, element_map: dict[str, Element]):
13
+ """
14
+ 初始化 ElementOperator
15
+
16
+ Args:
17
+ page: DrissionPage ChromiumPage 实例
18
+ element_map: ref -> Element 映射表
19
+ """
20
+ self._page = page
21
+ self._element_map = element_map
22
+
23
+ def click(self, locator: str, locator_type: str | None = None) -> OperationResult:
24
+ """
25
+ 点击元素
26
+
27
+ Args:
28
+ locator: 定位符(ref / xpath / role / text)
29
+ locator_type: 定位类型,不指定则自动推断
30
+
31
+ Returns:
32
+ OperationResult: {success, new_url?, error?}
33
+ """
34
+ try:
35
+ xpath = self._resolve_locator(locator, locator_type)
36
+ self._page.ele(f'xpath={xpath}').click()
37
+ return OperationResult(success=True)
38
+ except Exception as e:
39
+ return OperationResult(success=False, error=str(e))
40
+
41
+ def input_by_ref(
42
+ self, locator: str, text: str, clear: bool = False, locator_type: str | None = None
43
+ ) -> OperationResult:
44
+ """
45
+ 向输入框写入文字
46
+
47
+ Args:
48
+ locator: 定位符(ref / xpath / role / text)
49
+ text: 要输入的文字
50
+ clear: 是否先清空输入框
51
+ locator_type: 定位类型
52
+
53
+ Returns:
54
+ OperationResult: {success, error?}
55
+ """
56
+ try:
57
+ xpath = self._resolve_locator(locator, locator_type)
58
+ ele = self._page.ele(f'xpath={xpath}')
59
+ if clear:
60
+ ele.clear()
61
+ ele.input(text)
62
+ return OperationResult(success=True)
63
+ except Exception as e:
64
+ return OperationResult(success=False, error=str(e))
65
+
66
+ def hover(self, locator: str, locator_type: str | None = None) -> OperationResult:
67
+ """
68
+ 悬停在元素上
69
+
70
+ Args:
71
+ locator: 定位符(ref / xpath / role / text)
72
+ locator_type: 定位类型
73
+
74
+ Returns:
75
+ OperationResult: {success, error?}
76
+ """
77
+ try:
78
+ xpath = self._resolve_locator(locator, locator_type)
79
+ self._page.ele(f'xpath={xpath}').hover()
80
+ return OperationResult(success=True)
81
+ except Exception as e:
82
+ return OperationResult(success=False, error=str(e))
83
+
84
+ def double_click(self, locator: str, locator_type: str | None = None) -> OperationResult:
85
+ """
86
+ 双击元素
87
+
88
+ Args:
89
+ locator: 定位符(ref / xpath / role / text)
90
+ locator_type: 定位类型
91
+
92
+ Returns:
93
+ OperationResult: {success, error?}
94
+ """
95
+ try:
96
+ xpath = self._resolve_locator(locator, locator_type)
97
+ self._page.ele(f'xpath={xpath}').double_click()
98
+ return OperationResult(success=True)
99
+ except Exception as e:
100
+ return OperationResult(success=False, error=str(e))
101
+
102
+ def right_click(self, locator: str, locator_type: str | None = None) -> OperationResult:
103
+ """
104
+ 右键点击元素
105
+
106
+ Args:
107
+ locator: 定位符(ref / xpath / role / text)
108
+ locator_type: 定位类型
109
+
110
+ Returns:
111
+ OperationResult: {success, error?}
112
+ """
113
+ try:
114
+ xpath = self._resolve_locator(locator, locator_type)
115
+ self._page.ele(f'xpath={xpath}').right_click()
116
+ return OperationResult(success=True)
117
+ except Exception as e:
118
+ return OperationResult(success=False, error=str(e))
119
+
120
+ def submit(self, locator: str, locator_type: str | None = None) -> OperationResult:
121
+ """
122
+ 提交表单
123
+
124
+ Args:
125
+ locator: 定位符(ref / xpath / role / text)
126
+ locator_type: 定位类型
127
+
128
+ Returns:
129
+ OperationResult: {success, error?}
130
+ """
131
+ try:
132
+ xpath = self._resolve_locator(locator, locator_type)
133
+ self._page.ele(f'xpath={xpath}').submit()
134
+ return OperationResult(success=True)
135
+ except Exception as e:
136
+ return OperationResult(success=False, error=str(e))
137
+
138
+ def clear(self, locator: str, locator_type: str | None = None) -> OperationResult:
139
+ """
140
+ 清空输入框
141
+
142
+ Args:
143
+ locator: 定位符(ref / xpath / role / text)
144
+ locator_type: 定位类型
145
+
146
+ Returns:
147
+ OperationResult: {success, error?}
148
+ """
149
+ try:
150
+ xpath = self._resolve_locator(locator, locator_type)
151
+ self._page.ele(f'xpath={xpath}').clear()
152
+ return OperationResult(success=True)
153
+ except Exception as e:
154
+ return OperationResult(success=False, error=str(e))
155
+
156
+ def send_enter(self) -> None:
157
+ """发送回车键"""
158
+ self._page.run_js(
159
+ "document.activeElement.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter', code: 'Enter', keyCode: 13}));"
160
+ )
161
+ self._page.wait(0.1)
162
+
163
+ def ref_to_xpath(self, ref: str) -> str:
164
+ """
165
+ 通过 ref 获取 xpath(内部方法,供外部调用)
166
+
167
+ Args:
168
+ ref: 元素引用 ID
169
+
170
+ Returns:
171
+ xpath 路径
172
+
173
+ Raises:
174
+ ElementNotFoundError: 元素不存在
175
+ """
176
+ if ref not in self._element_map:
177
+ raise ElementNotFoundError(f"Element not found: {ref}")
178
+ return self._element_map[ref].xpath
179
+
180
+ def _resolve_locator(self, locator: str, locator_type: str | None = None) -> str:
181
+ """
182
+ 解析定位符,ref 需要查表转 xpath,其他直接返回
183
+
184
+ Args:
185
+ locator: 定位符
186
+ locator_type: 定位类型
187
+
188
+ Returns:
189
+ xpath 字符串
190
+
191
+ Raises:
192
+ ElementNotFoundError: ref 不存在
193
+ """
194
+ # 指定了类型
195
+ if locator_type == "ref":
196
+ return self.ref_to_xpath(locator)
197
+ elif locator_type:
198
+ return locator
199
+
200
+ # 自动推断:ref 查表,其他直接返回
201
+ if self._is_ref(locator):
202
+ return self.ref_to_xpath(locator)
203
+ return locator
204
+
205
+ def _is_ref(self, locator: str) -> bool:
206
+ """
207
+ 判断是否为 ref(格式:r + 数字)
208
+
209
+ Args:
210
+ locator: 定位符
211
+
212
+ Returns:
213
+ 是否为 ref
214
+ """
215
+ if not locator:
216
+ return False
217
+ if locator.startswith("r") and locator[1:].isdigit():
218
+ return True
219
+ return False
@@ -0,0 +1,41 @@
1
+ """自定义异常"""
2
+
3
+
4
+ class RootBrowseError(Exception):
5
+ """RootBrowse 基异常"""
6
+ pass
7
+
8
+
9
+ class BrowserError(RootBrowseError):
10
+ """浏览器相关错误"""
11
+ pass
12
+
13
+
14
+ class ElementNotFoundError(RootBrowseError):
15
+ """元素未找到"""
16
+ pass
17
+
18
+
19
+ class RegionNotFoundError(RootBrowseError):
20
+ """区域未找到"""
21
+ pass
22
+
23
+
24
+ class TabNotFoundError(RootBrowseError):
25
+ """标签页未找到"""
26
+ pass
27
+
28
+
29
+ class OperationError(RootBrowseError):
30
+ """操作失败"""
31
+ pass
32
+
33
+
34
+ class StateFileError(RootBrowseError):
35
+ """状态文件相关错误"""
36
+ pass
37
+
38
+
39
+ class PageLoadError(RootBrowseError):
40
+ """页面加载失败"""
41
+ pass