iflow-mcp_janspoerer-mcp_browser_use 0.1.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.
- iflow_mcp_janspoerer_mcp_browser_use-0.1.0.dist-info/METADATA +26 -0
- iflow_mcp_janspoerer_mcp_browser_use-0.1.0.dist-info/RECORD +50 -0
- iflow_mcp_janspoerer_mcp_browser_use-0.1.0.dist-info/WHEEL +5 -0
- iflow_mcp_janspoerer_mcp_browser_use-0.1.0.dist-info/entry_points.txt +2 -0
- iflow_mcp_janspoerer_mcp_browser_use-0.1.0.dist-info/licenses/LICENSE +201 -0
- iflow_mcp_janspoerer_mcp_browser_use-0.1.0.dist-info/top_level.txt +1 -0
- mcp_browser_use/__init__.py +2 -0
- mcp_browser_use/__main__.py +1347 -0
- mcp_browser_use/actions/__init__.py +1 -0
- mcp_browser_use/actions/elements.py +173 -0
- mcp_browser_use/actions/extraction.py +864 -0
- mcp_browser_use/actions/keyboard.py +43 -0
- mcp_browser_use/actions/navigation.py +73 -0
- mcp_browser_use/actions/screenshots.py +85 -0
- mcp_browser_use/browser/__init__.py +1 -0
- mcp_browser_use/browser/chrome.py +150 -0
- mcp_browser_use/browser/chrome_executable.py +204 -0
- mcp_browser_use/browser/chrome_launcher.py +330 -0
- mcp_browser_use/browser/chrome_process.py +104 -0
- mcp_browser_use/browser/devtools.py +230 -0
- mcp_browser_use/browser/driver.py +322 -0
- mcp_browser_use/browser/process.py +133 -0
- mcp_browser_use/cleaners.py +530 -0
- mcp_browser_use/config/__init__.py +30 -0
- mcp_browser_use/config/environment.py +155 -0
- mcp_browser_use/config/paths.py +97 -0
- mcp_browser_use/constants.py +68 -0
- mcp_browser_use/context.py +150 -0
- mcp_browser_use/context_pack.py +85 -0
- mcp_browser_use/decorators/__init__.py +13 -0
- mcp_browser_use/decorators/ensure.py +84 -0
- mcp_browser_use/decorators/envelope.py +83 -0
- mcp_browser_use/decorators/locking.py +172 -0
- mcp_browser_use/helpers.py +173 -0
- mcp_browser_use/helpers_context.py +261 -0
- mcp_browser_use/locking/__init__.py +1 -0
- mcp_browser_use/locking/action_lock.py +190 -0
- mcp_browser_use/locking/file_mutex.py +139 -0
- mcp_browser_use/locking/window_registry.py +178 -0
- mcp_browser_use/tools/__init__.py +59 -0
- mcp_browser_use/tools/browser_management.py +260 -0
- mcp_browser_use/tools/debugging.py +195 -0
- mcp_browser_use/tools/extraction.py +58 -0
- mcp_browser_use/tools/interaction.py +323 -0
- mcp_browser_use/tools/navigation.py +84 -0
- mcp_browser_use/tools/screenshots.py +116 -0
- mcp_browser_use/utils/__init__.py +1 -0
- mcp_browser_use/utils/diagnostics.py +85 -0
- mcp_browser_use/utils/html_utils.py +118 -0
- mcp_browser_use/utils/retry.py +57 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"""Browser lifecycle management tool implementations."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import psutil
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from ..context import get_context, reset_context
|
|
7
|
+
from ..config import get_env_config, profile_key
|
|
8
|
+
from ..constants import ACTION_LOCK_TTL_SECS
|
|
9
|
+
from ..utils.diagnostics import collect_diagnostics
|
|
10
|
+
|
|
11
|
+
# Import specific functions we need
|
|
12
|
+
from ..browser.driver import (
|
|
13
|
+
_ensure_driver_and_window,
|
|
14
|
+
close_singleton_window,
|
|
15
|
+
_close_extra_blank_windows_safe,
|
|
16
|
+
ensure_process_tag,
|
|
17
|
+
)
|
|
18
|
+
from ..actions.navigation import _wait_document_ready
|
|
19
|
+
from ..actions.screenshots import _make_page_snapshot
|
|
20
|
+
from ..locking.action_lock import _release_action_lock
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
async def start_browser():
|
|
24
|
+
"""
|
|
25
|
+
Start browser session or open new window in existing session.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
JSON string with session info and snapshot
|
|
29
|
+
"""
|
|
30
|
+
ctx = get_context()
|
|
31
|
+
|
|
32
|
+
# Ensure process tag
|
|
33
|
+
if ctx.process_tag is None:
|
|
34
|
+
ctx.process_tag = ensure_process_tag()
|
|
35
|
+
|
|
36
|
+
owner = ctx.process_tag
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
# Initialize driver and window
|
|
40
|
+
_ensure_driver_and_window()
|
|
41
|
+
|
|
42
|
+
# Check if initialization succeeded
|
|
43
|
+
if not ctx.is_driver_initialized():
|
|
44
|
+
diag = collect_diagnostics(None, None, ctx.config)
|
|
45
|
+
if isinstance(diag, str):
|
|
46
|
+
diag = {"summary": diag}
|
|
47
|
+
|
|
48
|
+
return json.dumps({
|
|
49
|
+
"ok": False,
|
|
50
|
+
"error": "driver_not_initialized",
|
|
51
|
+
"driver_initialized": False,
|
|
52
|
+
"debugger": ctx.get_debugger_address(),
|
|
53
|
+
"diagnostics": diag,
|
|
54
|
+
"message": "Failed to attach/launch a debuggable Chrome session."
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
# Clean up extra blank windows
|
|
58
|
+
handle = getattr(ctx.driver, "current_window_handle", None)
|
|
59
|
+
try:
|
|
60
|
+
_close_extra_blank_windows_safe(
|
|
61
|
+
ctx.driver,
|
|
62
|
+
exclude_handles={handle} if handle else None
|
|
63
|
+
)
|
|
64
|
+
except Exception:
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
# Wait for page ready and get snapshot
|
|
68
|
+
_wait_document_ready(timeout=5.0)
|
|
69
|
+
try:
|
|
70
|
+
snapshot = _make_page_snapshot()
|
|
71
|
+
except Exception:
|
|
72
|
+
snapshot = None
|
|
73
|
+
|
|
74
|
+
snapshot = snapshot or {
|
|
75
|
+
"url": "about:blank",
|
|
76
|
+
"title": "",
|
|
77
|
+
"html": "",
|
|
78
|
+
"truncated": False,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
msg = (
|
|
82
|
+
f"Browser session created successfully. "
|
|
83
|
+
f"Session ID: {owner}. "
|
|
84
|
+
f"Current URL: {snapshot.get('url') or 'about:blank'}"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
payload = {
|
|
88
|
+
"ok": True,
|
|
89
|
+
"session_id": owner,
|
|
90
|
+
"debugger": ctx.get_debugger_address(),
|
|
91
|
+
"lock_ttl_seconds": ACTION_LOCK_TTL_SECS,
|
|
92
|
+
"snapshot": snapshot,
|
|
93
|
+
"message": msg,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return json.dumps(payload)
|
|
97
|
+
|
|
98
|
+
except Exception as e:
|
|
99
|
+
diag = collect_diagnostics(ctx.driver, e, ctx.config)
|
|
100
|
+
snapshot = _make_page_snapshot() or {
|
|
101
|
+
"url": "about:blank",
|
|
102
|
+
"title": "",
|
|
103
|
+
"html": "",
|
|
104
|
+
"truncated": False,
|
|
105
|
+
}
|
|
106
|
+
return json.dumps({
|
|
107
|
+
"ok": False,
|
|
108
|
+
"error": str(e),
|
|
109
|
+
"diagnostics": diag,
|
|
110
|
+
"snapshot": snapshot
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
async def unlock_browser():
|
|
115
|
+
"""Release the action lock for this process."""
|
|
116
|
+
ctx = get_context()
|
|
117
|
+
|
|
118
|
+
if ctx.process_tag is None:
|
|
119
|
+
ctx.process_tag = ensure_process_tag()
|
|
120
|
+
|
|
121
|
+
owner = ctx.process_tag
|
|
122
|
+
released = _release_action_lock(owner)
|
|
123
|
+
|
|
124
|
+
return json.dumps({
|
|
125
|
+
"ok": True,
|
|
126
|
+
"released": bool(released)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
async def close_browser() -> str:
|
|
130
|
+
"""Close the browser window for this session."""
|
|
131
|
+
ctx = get_context()
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
closed = close_singleton_window()
|
|
135
|
+
msg = "Browser window closed successfully" if closed else "No window to close"
|
|
136
|
+
|
|
137
|
+
return json.dumps({
|
|
138
|
+
"ok": True,
|
|
139
|
+
"closed": bool(closed),
|
|
140
|
+
"message": msg
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
except Exception as e:
|
|
144
|
+
diag = collect_diagnostics(ctx.driver, e, ctx.config)
|
|
145
|
+
return json.dumps({
|
|
146
|
+
"ok": False,
|
|
147
|
+
"error": str(e),
|
|
148
|
+
"diagnostics": diag
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
async def force_close_all_chrome() -> str:
|
|
152
|
+
"""
|
|
153
|
+
Force close all Chrome processes, quit driver, and clean up all state.
|
|
154
|
+
Use this to recover from stuck Chrome instances.
|
|
155
|
+
"""
|
|
156
|
+
ctx = get_context()
|
|
157
|
+
killed_processes = []
|
|
158
|
+
errors = []
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
# 1. Try to quit the Selenium driver gracefully
|
|
162
|
+
if ctx.driver is not None:
|
|
163
|
+
try:
|
|
164
|
+
ctx.driver.quit()
|
|
165
|
+
except Exception as e:
|
|
166
|
+
errors.append(f"Driver quit failed: {e}")
|
|
167
|
+
|
|
168
|
+
ctx.driver = None
|
|
169
|
+
|
|
170
|
+
# 2. Get config to find which Chrome processes to kill
|
|
171
|
+
user_data_dir = ctx.config.get("user_data_dir", "")
|
|
172
|
+
if not user_data_dir:
|
|
173
|
+
try:
|
|
174
|
+
cfg = get_env_config()
|
|
175
|
+
user_data_dir = cfg.get("user_data_dir", "")
|
|
176
|
+
except Exception as e:
|
|
177
|
+
errors.append(f"Could not get config: {e}")
|
|
178
|
+
|
|
179
|
+
# 3. Kill all Chrome processes using the MCP profile
|
|
180
|
+
chrome_processes_found = []
|
|
181
|
+
for p in psutil.process_iter(["name", "cmdline", "pid"]):
|
|
182
|
+
try:
|
|
183
|
+
if not p.info.get("name"):
|
|
184
|
+
continue
|
|
185
|
+
if "chrome" not in p.info["name"].lower():
|
|
186
|
+
continue
|
|
187
|
+
|
|
188
|
+
chrome_processes_found.append(p)
|
|
189
|
+
|
|
190
|
+
# If we have a user_data_dir, check if this process matches
|
|
191
|
+
if user_data_dir:
|
|
192
|
+
cmd = p.info.get("cmdline")
|
|
193
|
+
if cmd:
|
|
194
|
+
user_data_normalized = user_data_dir.replace("\\", "/").lower()
|
|
195
|
+
for arg in cmd:
|
|
196
|
+
if arg and "--user-data-dir" in arg:
|
|
197
|
+
arg_normalized = arg.replace("\\", "/").lower()
|
|
198
|
+
if user_data_normalized in arg_normalized:
|
|
199
|
+
p.kill()
|
|
200
|
+
killed_processes.append(p.info["pid"])
|
|
201
|
+
break
|
|
202
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied) as e:
|
|
203
|
+
errors.append(f"Could not access process: {e}")
|
|
204
|
+
|
|
205
|
+
# 4. Fallback: If no processes killed but some found, kill them all
|
|
206
|
+
if not killed_processes and chrome_processes_found:
|
|
207
|
+
for p in chrome_processes_found:
|
|
208
|
+
try:
|
|
209
|
+
p.kill()
|
|
210
|
+
killed_processes.append(p.info["pid"])
|
|
211
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied) as e:
|
|
212
|
+
errors.append(f"Could not kill process in fallback: {e}")
|
|
213
|
+
|
|
214
|
+
# 5. Clean up context state
|
|
215
|
+
ctx.debugger_host = None
|
|
216
|
+
ctx.debugger_port = None
|
|
217
|
+
ctx.reset_window_state()
|
|
218
|
+
|
|
219
|
+
# 6. Release locks
|
|
220
|
+
try:
|
|
221
|
+
if ctx.process_tag:
|
|
222
|
+
_release_action_lock(ctx.process_tag)
|
|
223
|
+
except Exception as e:
|
|
224
|
+
errors.append(f"Lock release failed: {e}")
|
|
225
|
+
|
|
226
|
+
# 7. Clean up lock files
|
|
227
|
+
try:
|
|
228
|
+
if user_data_dir:
|
|
229
|
+
lock_dir = Path(ctx.lock_dir)
|
|
230
|
+
if lock_dir.exists():
|
|
231
|
+
profile_key_val = profile_key(ctx.config) if ctx.config else ""
|
|
232
|
+
for lock_file in lock_dir.glob(f"*{profile_key_val}*"):
|
|
233
|
+
try:
|
|
234
|
+
lock_file.unlink()
|
|
235
|
+
except Exception:
|
|
236
|
+
pass
|
|
237
|
+
except Exception as e:
|
|
238
|
+
errors.append(f"Lock file cleanup failed: {e}")
|
|
239
|
+
|
|
240
|
+
msg = f"Force closed Chrome. Killed {len(killed_processes)} processes."
|
|
241
|
+
if errors:
|
|
242
|
+
msg += f" Errors: {'; '.join(errors)}"
|
|
243
|
+
|
|
244
|
+
return json.dumps({
|
|
245
|
+
"ok": True,
|
|
246
|
+
"killed_processes": killed_processes,
|
|
247
|
+
"errors": errors,
|
|
248
|
+
"message": msg
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
except Exception as e:
|
|
252
|
+
return json.dumps({
|
|
253
|
+
"ok": False,
|
|
254
|
+
"error": str(e),
|
|
255
|
+
"killed_processes": killed_processes,
|
|
256
|
+
"errors": errors
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
__all__ = ['start_browser', 'unlock_browser', 'close_browser', 'force_close_all_chrome']
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""Debugging and diagnostic tool implementations."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Dict, Any
|
|
6
|
+
from selenium.common.exceptions import TimeoutException
|
|
7
|
+
from ..context import get_context
|
|
8
|
+
from ..utils.diagnostics import collect_diagnostics
|
|
9
|
+
from ..actions.elements import find_element, _wait_clickable_element
|
|
10
|
+
from ..actions.screenshots import _make_page_snapshot
|
|
11
|
+
from ..utils.retry import retry_op
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
async def get_debug_diagnostics_info() -> str:
|
|
15
|
+
"""Get debug diagnostics using context."""
|
|
16
|
+
ctx = get_context()
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
cfg = ctx.config
|
|
20
|
+
udir = cfg.get("user_data_dir")
|
|
21
|
+
port_file = str(Path(udir) / "DevToolsActivePort") if udir else None
|
|
22
|
+
|
|
23
|
+
# Read DevToolsActivePort
|
|
24
|
+
port_val = None
|
|
25
|
+
if udir:
|
|
26
|
+
p = Path(udir) / "DevToolsActivePort"
|
|
27
|
+
if p.exists():
|
|
28
|
+
try:
|
|
29
|
+
port_val = int(p.read_text().splitlines()[0].strip())
|
|
30
|
+
except Exception:
|
|
31
|
+
port_val = None
|
|
32
|
+
|
|
33
|
+
devtools_http = None
|
|
34
|
+
if port_val:
|
|
35
|
+
import urllib.request, json as _json
|
|
36
|
+
try:
|
|
37
|
+
with urllib.request.urlopen(f"http://127.0.0.1:{port_val}/json/version", timeout=1.0) as r:
|
|
38
|
+
devtools_http = _json.loads(r.read().decode("utf-8"))
|
|
39
|
+
except Exception:
|
|
40
|
+
devtools_http = {"ok": False}
|
|
41
|
+
|
|
42
|
+
diag_summary = collect_diagnostics(driver=ctx.driver, exc=None, config=cfg)
|
|
43
|
+
diagnostics = {
|
|
44
|
+
"summary": diag_summary,
|
|
45
|
+
"driver_initialized": ctx.is_driver_initialized(),
|
|
46
|
+
"debugger": ctx.get_debugger_address(),
|
|
47
|
+
"devtools_active_port_file": {"path": port_file, "port": port_val, "exists": port_val is not None},
|
|
48
|
+
"devtools_http_version": devtools_http,
|
|
49
|
+
"context_state": {
|
|
50
|
+
"driver_initialized": ctx.is_driver_initialized(),
|
|
51
|
+
"window_ready": ctx.is_window_ready(),
|
|
52
|
+
"debugger_address": ctx.get_debugger_address(),
|
|
53
|
+
"process_tag": ctx.process_tag,
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
snapshot = (_make_page_snapshot()
|
|
58
|
+
if ctx.is_driver_initialized()
|
|
59
|
+
else {"url": None, "title": None, "html": "", "truncated": False})
|
|
60
|
+
return json.dumps({"ok": True, "diagnostics": diagnostics, "snapshot": snapshot})
|
|
61
|
+
|
|
62
|
+
except Exception as e:
|
|
63
|
+
diag = collect_diagnostics(driver=ctx.driver, exc=e, config=ctx.config)
|
|
64
|
+
return json.dumps({"ok": False, "error": str(e), "diagnostics": {"summary": diag}})
|
|
65
|
+
|
|
66
|
+
async def debug_element(
|
|
67
|
+
selector,
|
|
68
|
+
selector_type,
|
|
69
|
+
timeout,
|
|
70
|
+
iframe_selector,
|
|
71
|
+
iframe_selector_type,
|
|
72
|
+
shadow_root_selector,
|
|
73
|
+
shadow_root_selector_type,
|
|
74
|
+
max_html_length=5000,
|
|
75
|
+
include_html=True,
|
|
76
|
+
):
|
|
77
|
+
"""
|
|
78
|
+
Debug an element on the page.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
selector: CSS selector, XPath, or ID of the element
|
|
82
|
+
selector_type: Type of selector (css, xpath, id)
|
|
83
|
+
timeout: Maximum time to wait for element
|
|
84
|
+
iframe_selector: Optional iframe selector
|
|
85
|
+
iframe_selector_type: Iframe selector type
|
|
86
|
+
shadow_root_selector: Optional shadow root selector
|
|
87
|
+
shadow_root_selector_type: Shadow root selector type
|
|
88
|
+
max_html_length: Maximum length of outerHTML to return (default: 5000 chars)
|
|
89
|
+
include_html: Whether to include HTML in response (default: True)
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
JSON string with debug information
|
|
93
|
+
"""
|
|
94
|
+
ctx = get_context()
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
info: Dict[str, Any] = {
|
|
98
|
+
"selector": selector,
|
|
99
|
+
"selector_type": selector_type,
|
|
100
|
+
"exists": False,
|
|
101
|
+
"displayed": None,
|
|
102
|
+
"enabled": None,
|
|
103
|
+
"clickable": None,
|
|
104
|
+
"rect": None,
|
|
105
|
+
"outerHTML": None,
|
|
106
|
+
"truncated": False,
|
|
107
|
+
"notes": [],
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
el = retry_op(fn=lambda: find_element(
|
|
112
|
+
driver=ctx.driver,
|
|
113
|
+
selector=selector,
|
|
114
|
+
selector_type=selector_type,
|
|
115
|
+
timeout=int(timeout),
|
|
116
|
+
visible_only=False,
|
|
117
|
+
iframe_selector=iframe_selector,
|
|
118
|
+
iframe_selector_type=iframe_selector_type,
|
|
119
|
+
shadow_root_selector=shadow_root_selector,
|
|
120
|
+
shadow_root_selector_type=shadow_root_selector_type,
|
|
121
|
+
stay_in_context=True,
|
|
122
|
+
))
|
|
123
|
+
info["exists"] = True
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
info["displayed"] = bool(el.is_displayed())
|
|
127
|
+
except Exception:
|
|
128
|
+
info["displayed"] = None
|
|
129
|
+
try:
|
|
130
|
+
info["enabled"] = bool(el.is_enabled())
|
|
131
|
+
except Exception:
|
|
132
|
+
info["enabled"] = None
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
_wait_clickable_element(el=el, driver=ctx.driver, timeout=timeout)
|
|
136
|
+
info["clickable"] = True
|
|
137
|
+
except Exception:
|
|
138
|
+
info["clickable"] = False
|
|
139
|
+
|
|
140
|
+
try:
|
|
141
|
+
r = el.rect
|
|
142
|
+
info["rect"] = {
|
|
143
|
+
"x": r.get("x"),
|
|
144
|
+
"y": r.get("y"),
|
|
145
|
+
"width": r.get("width"),
|
|
146
|
+
"height": r.get("height"),
|
|
147
|
+
}
|
|
148
|
+
except Exception:
|
|
149
|
+
info["rect"] = None
|
|
150
|
+
|
|
151
|
+
# Get HTML if requested
|
|
152
|
+
if include_html:
|
|
153
|
+
try:
|
|
154
|
+
html = ctx.driver.execute_script("return arguments[0].outerHTML;", el)
|
|
155
|
+
# Clean invalid characters
|
|
156
|
+
html = html.replace('\x00', '').encode('utf-8', errors='ignore').decode('utf-8')
|
|
157
|
+
|
|
158
|
+
# Truncate if too large
|
|
159
|
+
full_length = len(html)
|
|
160
|
+
if max_html_length and len(html) > max_html_length:
|
|
161
|
+
info["outerHTML"] = html[:max_html_length]
|
|
162
|
+
info["truncated"] = True
|
|
163
|
+
info["full_html_length"] = full_length
|
|
164
|
+
info["notes"].append(f"HTML truncated from {full_length} to {max_html_length} chars")
|
|
165
|
+
else:
|
|
166
|
+
info["outerHTML"] = html
|
|
167
|
+
info["truncated"] = False
|
|
168
|
+
except Exception as e:
|
|
169
|
+
info["outerHTML"] = None
|
|
170
|
+
info["notes"].append(f"Could not get HTML: {str(e)}")
|
|
171
|
+
else:
|
|
172
|
+
info["notes"].append("HTML omitted (include_html=False)")
|
|
173
|
+
|
|
174
|
+
except TimeoutException:
|
|
175
|
+
info["notes"].append("Element not found within timeout")
|
|
176
|
+
except Exception as e:
|
|
177
|
+
info["notes"].append(f"Error while probing element: {repr(e)}")
|
|
178
|
+
|
|
179
|
+
snapshot = _make_page_snapshot()
|
|
180
|
+
return json.dumps({"ok": True, "debug": info, "snapshot": snapshot})
|
|
181
|
+
|
|
182
|
+
except Exception as e:
|
|
183
|
+
diag = collect_diagnostics(driver=ctx.driver, exc=e, config=ctx.config)
|
|
184
|
+
snapshot = _make_page_snapshot()
|
|
185
|
+
return json.dumps({"ok": False, "error": str(e), "diagnostics": diag, "snapshot": snapshot})
|
|
186
|
+
|
|
187
|
+
finally:
|
|
188
|
+
try:
|
|
189
|
+
if ctx.is_driver_initialized():
|
|
190
|
+
ctx.driver.switch_to.default_content()
|
|
191
|
+
except Exception:
|
|
192
|
+
pass
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
__all__ = ['get_debug_diagnostics_info', 'debug_element']
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Element extraction tool implementations."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional, List, Dict
|
|
4
|
+
from ..actions.extraction import extract_elements as _extract_elements_action
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
async def extract_elements(
|
|
8
|
+
selectors: Optional[List[Dict[str, str]]] = None,
|
|
9
|
+
container_selector: Optional[str] = None,
|
|
10
|
+
fields: Optional[List[Dict[str, str]]] = None,
|
|
11
|
+
selector_type: Optional[str] = None,
|
|
12
|
+
wait_for_visible: bool = False,
|
|
13
|
+
timeout: int = 10,
|
|
14
|
+
max_items: Optional[int] = None,
|
|
15
|
+
offset: Optional[int] = None,
|
|
16
|
+
discover_containers: bool = False,
|
|
17
|
+
wait_for_content_loaded: Optional[Dict[str, any]] = None,
|
|
18
|
+
) -> str:
|
|
19
|
+
"""
|
|
20
|
+
Extract content from specific elements on the current page.
|
|
21
|
+
|
|
22
|
+
This is a wrapper around the extraction action that provides the tool interface.
|
|
23
|
+
|
|
24
|
+
Supports two modes:
|
|
25
|
+
- Simple extraction: Use 'selectors' parameter
|
|
26
|
+
- Structured extraction: Use 'container_selector' + 'fields' parameters
|
|
27
|
+
- Discovery mode: Use 'container_selector' + 'discover_containers=True'
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
selectors: [MODE 1] Optional list of selector specifications
|
|
31
|
+
container_selector: [MODE 2] CSS or XPath selector for containers
|
|
32
|
+
fields: [MODE 2] List of field extractors with field_name, selector, etc.
|
|
33
|
+
selector_type: [MODE 2] Type of container_selector (auto-detects if None)
|
|
34
|
+
wait_for_visible: [MODE 2] Wait for containers to be visible
|
|
35
|
+
timeout: [MODE 2] Timeout in seconds
|
|
36
|
+
max_items: [MODE 2] Limit number of containers to extract
|
|
37
|
+
offset: [MODE 2] Skip first N containers before extracting (useful for pagination)
|
|
38
|
+
discover_containers: [MODE 2] Return container analysis instead of extraction
|
|
39
|
+
wait_for_content_loaded: [MODE 2] Smart wait config for lazy-loaded content
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
JSON string with extraction results and page snapshot.
|
|
43
|
+
"""
|
|
44
|
+
return await _extract_elements_action(
|
|
45
|
+
selectors=selectors,
|
|
46
|
+
container_selector=container_selector,
|
|
47
|
+
fields=fields,
|
|
48
|
+
selector_type=selector_type,
|
|
49
|
+
wait_for_visible=wait_for_visible,
|
|
50
|
+
timeout=timeout,
|
|
51
|
+
max_items=max_items,
|
|
52
|
+
offset=offset,
|
|
53
|
+
discover_containers=discover_containers,
|
|
54
|
+
wait_for_content_loaded=wait_for_content_loaded
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
__all__ = ['extract_elements']
|