Jarvis-Brain 0.1.11.10__tar.gz → 0.1.13.9__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.
- {jarvis_brain-0.1.11.10 → jarvis_brain-0.1.13.9}/PKG-INFO +1 -1
- {jarvis_brain-0.1.11.10 → jarvis_brain-0.1.13.9}/mcp_tools/dp_tools.py +128 -17
- {jarvis_brain-0.1.11.10 → jarvis_brain-0.1.13.9}/mcp_tools/main.py +2 -0
- {jarvis_brain-0.1.11.10 → jarvis_brain-0.1.13.9}/pyproject.toml +1 -1
- {jarvis_brain-0.1.11.10 → jarvis_brain-0.1.13.9}/tools/tools.py +96 -9
- {jarvis_brain-0.1.11.10 → jarvis_brain-0.1.13.9}/.gitignore +0 -0
- {jarvis_brain-0.1.11.10 → jarvis_brain-0.1.13.9}/README.md +0 -0
- {jarvis_brain-0.1.11.10 → jarvis_brain-0.1.13.9}/mcp_tools/__init__.py +0 -0
- {jarvis_brain-0.1.11.10 → jarvis_brain-0.1.13.9}/tools/__init__.py +0 -0
- {jarvis_brain-0.1.11.10 → jarvis_brain-0.1.13.9}/tools/browser_manager.py +0 -0
- {jarvis_brain-0.1.11.10 → jarvis_brain-0.1.13.9}/tools/browser_proxy.py +0 -0
- {jarvis_brain-0.1.11.10 → jarvis_brain-0.1.13.9}/uv.lock +0 -0
|
@@ -7,7 +7,7 @@ import os
|
|
|
7
7
|
import time
|
|
8
8
|
from typing import Any
|
|
9
9
|
|
|
10
|
-
import
|
|
10
|
+
from DrissionPage.errors import NoRectError
|
|
11
11
|
from fastmcp import FastMCP
|
|
12
12
|
|
|
13
13
|
from tools.browser_manager import BrowserManager
|
|
@@ -248,22 +248,17 @@ def register_click_action(mcp: FastMCP, browser_manager):
|
|
|
248
248
|
if "css:" not in css_selector:
|
|
249
249
|
css_selector = "css:" + css_selector
|
|
250
250
|
target_eles = target_tab.eles(css_selector)
|
|
251
|
-
# click_success = False
|
|
252
|
-
# element_clickable = False
|
|
253
|
-
# if len(target_eles) == 1:
|
|
254
251
|
target_element = target_eles[target_element_index]
|
|
255
252
|
element_clickable = target_element.states.is_clickable
|
|
256
253
|
try:
|
|
257
|
-
target_element.click()
|
|
258
|
-
click_success =
|
|
254
|
+
click_success = target_element.click()
|
|
255
|
+
click_success = click_success is not False
|
|
259
256
|
except Exception as e:
|
|
260
257
|
click_success = False
|
|
261
258
|
if target_element_index > 0:
|
|
262
259
|
message = f"tab页:【{tab_id}】点击【{css_selector}】【index={target_element_index}】的元素 {'成功' if click_success else '失败'} 了"
|
|
263
260
|
else:
|
|
264
261
|
message = f"tab页:【{tab_id}】点击【{css_selector}】 {'成功' if click_success else '失败'} 了"
|
|
265
|
-
# else:
|
|
266
|
-
# message = f"tab页:【{tab_id}】传入的css_selector找到了{len(target_eles)}个元素,请确保传入的css_selector可以找到唯一的一个元素"
|
|
267
262
|
return dp_mcp_message_pack(
|
|
268
263
|
message=message,
|
|
269
264
|
browser_port=browser_port,
|
|
@@ -289,23 +284,28 @@ def register_scroll_action(mcp: FastMCP, browser_manager):
|
|
|
289
284
|
if forward == "down":
|
|
290
285
|
if pixel is None:
|
|
291
286
|
target_tab.scroll.to_half()
|
|
292
|
-
|
|
287
|
+
else:
|
|
288
|
+
target_tab.scroll.down(pixel)
|
|
293
289
|
elif forward == "up":
|
|
294
290
|
if pixel is None:
|
|
295
291
|
target_tab.scroll.to_top()
|
|
296
|
-
|
|
292
|
+
else:
|
|
293
|
+
target_tab.scroll.up(pixel)
|
|
297
294
|
elif forward == "left":
|
|
298
295
|
if pixel is None:
|
|
299
296
|
target_tab.scroll.to_leftmost()
|
|
300
|
-
|
|
297
|
+
else:
|
|
298
|
+
target_tab.scroll.left(pixel)
|
|
301
299
|
elif forward == "right":
|
|
302
300
|
if pixel is None:
|
|
303
301
|
target_tab.scroll.to_rightmost()
|
|
304
|
-
|
|
302
|
+
else:
|
|
303
|
+
target_tab.scroll.right(pixel)
|
|
305
304
|
else:
|
|
306
305
|
if pixel is None:
|
|
307
306
|
target_tab.scroll.to_half()
|
|
308
|
-
|
|
307
|
+
else:
|
|
308
|
+
target_tab.scroll.down()
|
|
309
309
|
message = f"已完成对tab页:【{tab_id}】forward={forward} 的滑动"
|
|
310
310
|
return dp_mcp_message_pack(
|
|
311
311
|
message=message,
|
|
@@ -314,6 +314,117 @@ def register_scroll_action(mcp: FastMCP, browser_manager):
|
|
|
314
314
|
)
|
|
315
315
|
|
|
316
316
|
|
|
317
|
+
def register_mouse_hover(mcp: FastMCP, browser_manager):
|
|
318
|
+
@mcp.tool(name="mouse_hover", description="将鼠标悬停在元素上【这个功能使用了Drissionpage的action行为链功能】")
|
|
319
|
+
async def mouse_hover(browser_port: int, tab_id: str, css_selector: str, target_element_index: int = 0) -> dict[
|
|
320
|
+
str, Any]:
|
|
321
|
+
_browser = browser_manager.get_browser(browser_port)
|
|
322
|
+
target_tab = _browser.get_tab(tab_id)
|
|
323
|
+
css_selector = css_selector
|
|
324
|
+
if "css:" not in css_selector:
|
|
325
|
+
css_selector = "css:" + css_selector
|
|
326
|
+
target_eles = target_tab.eles(css_selector)
|
|
327
|
+
target_element = target_eles[target_element_index]
|
|
328
|
+
try:
|
|
329
|
+
target_tab.actions.move_to(target_element)
|
|
330
|
+
target_element.hover()
|
|
331
|
+
hover_success = True
|
|
332
|
+
except Exception as e:
|
|
333
|
+
hover_success = False
|
|
334
|
+
if target_element_index > 0:
|
|
335
|
+
message = f"tab页:【{tab_id}】hover【{css_selector}】【index={target_element_index}】的元素 {'成功' if hover_success else '失败'} 了"
|
|
336
|
+
else:
|
|
337
|
+
message = f"tab页:【{tab_id}】hover【{css_selector}】 {'成功' if hover_success else '失败'} 了"
|
|
338
|
+
# else:
|
|
339
|
+
# message = f"tab页:【{tab_id}】传入的css_selector找到了{len(target_eles)}个元素,请确保传入的css_selector可以找到唯一的一个元素"
|
|
340
|
+
return dp_mcp_message_pack(
|
|
341
|
+
message=message,
|
|
342
|
+
browser_port=browser_port,
|
|
343
|
+
tab_id=tab_id,
|
|
344
|
+
css_selector=css_selector,
|
|
345
|
+
hoversuccess=hover_success,
|
|
346
|
+
extra_message="hover成功,页面可能有更新,请重新获取页面html,并重新分析页面Selector" if hover_success else ""
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def register_get_ele_info(mcp: FastMCP, browser_manager):
|
|
351
|
+
@mcp.tool(name="get_ele_info", description="返回元素有关一系列的信息。"
|
|
352
|
+
"参数说明:"
|
|
353
|
+
"browser_port:浏览器端口号。"
|
|
354
|
+
"tab_id:tab页的id。"
|
|
355
|
+
"css_selector:目标选择器"
|
|
356
|
+
"element_index:如果选择器定位到的是一个列表【或者可以定位到多个元素】,则该值用于定位这个列表中具体元素,默认值为0【列表中的第一个元素】"
|
|
357
|
+
"返回值说明:"
|
|
358
|
+
"element_tag:此属性返回元素的标签名。"
|
|
359
|
+
"element_attrs_key:此属性以list的形式返回元素所有属性的key。"
|
|
360
|
+
"element_rect_size:此属性以元组形式返回元素的大小【如果元素没有位置及大小,则返回空元组】。"
|
|
361
|
+
"is_in_viewport:此属性以布尔值方式返回元素是否在视口中,以元素可以接受点击的点为判断。"
|
|
362
|
+
"is_whole_in_viewport:此属性以布尔值方式返回元素是否整个在视口中。"
|
|
363
|
+
"is_alive:此属性以布尔值形式返回当前元素是否仍可用。用于判断是否因页面刷新而导致元素失效。"
|
|
364
|
+
"is_checked:此属性以布尔值返回表单单选或多选元素是否选中。"
|
|
365
|
+
"is_selected:此属性以布尔值返回<select>元素中的项是否选中。"
|
|
366
|
+
"is_enabled:此属性以布尔值返回元素是否可用。"
|
|
367
|
+
"is_displayed:此属性以布尔值返回元素是否可见。"
|
|
368
|
+
"is_covered:此属性返回元素是否被其它元素覆盖/遮挡。如被覆盖/遮挡,返回覆盖元素的 id【无障碍树的id,无法用于Selector】,否则返回False。"
|
|
369
|
+
"is_clickable:此属性返回元素是否可被模拟点击,从是否有大小、是否可用、是否显示、是否响应点击判断,不判断是否被遮挡。"
|
|
370
|
+
"has_rect:此属性返回元素是否拥有大小和位置信息,有则返回四个角在页面上的坐标组成的列表,没有则返回False。")
|
|
371
|
+
async def get_ele_info(browser_port: int, tab_id: str, css_selector: str, element_index: int = 0) -> dict[
|
|
372
|
+
str, Any]:
|
|
373
|
+
_browser = browser_manager.get_browser(browser_port)
|
|
374
|
+
target_tab = _browser.get_tab(tab_id)
|
|
375
|
+
css_selector = css_selector
|
|
376
|
+
if "css:" not in css_selector:
|
|
377
|
+
css_selector = "css:" + css_selector
|
|
378
|
+
target_eles = target_tab.eles(css_selector)
|
|
379
|
+
try:
|
|
380
|
+
target_element = target_eles[element_index]
|
|
381
|
+
except IndexError:
|
|
382
|
+
return dp_mcp_message_pack(
|
|
383
|
+
message="报错:IndexError: list index out of range。请检查Selector中是否包含了如:first-child、nth-child等字段,如果有则去掉。必须使用element_index来控制元素的选择",
|
|
384
|
+
browser_port=browser_port,
|
|
385
|
+
tab_id=tab_id,
|
|
386
|
+
css_selector=css_selector,
|
|
387
|
+
element_index=element_index,
|
|
388
|
+
)
|
|
389
|
+
try:
|
|
390
|
+
has_rect = target_element.states.has_rect,
|
|
391
|
+
element_rect_size = tuple()
|
|
392
|
+
if not has_rect:
|
|
393
|
+
element_rect_size = target_element.rect.size,
|
|
394
|
+
try:
|
|
395
|
+
child_count=target_element.child_count
|
|
396
|
+
except Exception:
|
|
397
|
+
child_count=0
|
|
398
|
+
return dp_mcp_message_pack(
|
|
399
|
+
message="元素可以被正常的选择到,以下是元素相关的一系列信息",
|
|
400
|
+
browser_port=browser_port,
|
|
401
|
+
tab_id=tab_id,
|
|
402
|
+
css_selector=css_selector,
|
|
403
|
+
element_index=element_index,
|
|
404
|
+
element_tag=target_element.tag,
|
|
405
|
+
element_attrs_key=list(target_element.attrs.keys()),
|
|
406
|
+
element_child_count=child_count,
|
|
407
|
+
element_rect_size=element_rect_size,
|
|
408
|
+
is_in_viewport=target_element.states.is_in_viewport,
|
|
409
|
+
is_whole_in_viewport=target_element.states.is_whole_in_viewport,
|
|
410
|
+
is_alive=target_element.states.is_alive,
|
|
411
|
+
is_checked=target_element.states.is_checked,
|
|
412
|
+
is_selected=target_element.states.is_selected,
|
|
413
|
+
is_enabled=target_element.states.is_enabled,
|
|
414
|
+
is_displayed=target_element.states.is_displayed,
|
|
415
|
+
is_covered=target_element.states.is_covered,
|
|
416
|
+
is_clickable=target_element.states.is_clickable,
|
|
417
|
+
has_rect=has_rect
|
|
418
|
+
)
|
|
419
|
+
except NoRectError:
|
|
420
|
+
return dp_mcp_message_pack(
|
|
421
|
+
message="报错:NoRectError: 该元素没有位置及大小。你传入的css_selector和element_index选出的元素没有rect,请确认该元素是否可见",
|
|
422
|
+
browser_port=browser_port,
|
|
423
|
+
tab_id=tab_id,
|
|
424
|
+
css_selector=css_selector,
|
|
425
|
+
element_index=element_index,
|
|
426
|
+
)
|
|
427
|
+
|
|
317
428
|
def register_get_screenshot(mcp: FastMCP, browser_manager):
|
|
318
429
|
@mcp.tool(name="get_tab_screenshot",
|
|
319
430
|
description="尝试对传入tab页进行截图,并将截图压缩为1M大小png图片,会返回截图保存路径")
|
|
@@ -324,10 +435,10 @@ def register_get_screenshot(mcp: FastMCP, browser_manager):
|
|
|
324
435
|
if not os.path.exists(html_source_code_local_save_path):
|
|
325
436
|
os.makedirs(html_source_code_local_save_path)
|
|
326
437
|
timestamp = int(time.time() * 1000)
|
|
327
|
-
|
|
328
|
-
origin_png = target_tab.get_screenshot(as_bytes="
|
|
329
|
-
compress_png = compress_image_bytes(origin_png
|
|
330
|
-
image_path = os.path.join(html_source_code_local_save_path, f"{browser_port}_{tab_id}_{timestamp}.
|
|
438
|
+
time.sleep(3)
|
|
439
|
+
origin_png = target_tab.get_screenshot(as_bytes="png")
|
|
440
|
+
compress_png = compress_image_bytes(origin_png)
|
|
441
|
+
image_path = os.path.join(html_source_code_local_save_path, f"{browser_port}_{tab_id}_{timestamp}.png")
|
|
331
442
|
with open(image_path, "wb") as f:
|
|
332
443
|
f.write(compress_png)
|
|
333
444
|
return dp_mcp_message_pack(
|
|
@@ -24,6 +24,8 @@ if "TeamNode-Dp" in enabled_modules:
|
|
|
24
24
|
# 页面交互
|
|
25
25
|
register_click_action(mcp, browser_manager)
|
|
26
26
|
register_scroll_action(mcp, browser_manager)
|
|
27
|
+
register_mouse_hover(mcp, browser_manager)
|
|
28
|
+
register_get_ele_info(mcp, browser_manager)
|
|
27
29
|
|
|
28
30
|
if "JarvisNode" in enabled_modules:
|
|
29
31
|
register_assert_waf(mcp, browser_manager)
|
|
@@ -10,7 +10,7 @@ import base64
|
|
|
10
10
|
from PIL import Image
|
|
11
11
|
import io
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
compress_html_js1 = """
|
|
14
14
|
function getSimplifiedDOM(node) {
|
|
15
15
|
// 1. 处理文本节点
|
|
16
16
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
@@ -102,6 +102,101 @@ function getSimplifiedDOM(node) {
|
|
|
102
102
|
return getSimplifiedDOM(document.body);
|
|
103
103
|
"""
|
|
104
104
|
|
|
105
|
+
# 我自己优化后的版本,逻辑为:删除不可见元素、标签的任何属性value的长度大于20时直接删除这个属性、id和class采用简写方式:id=>#,class=>.
|
|
106
|
+
compress_html_js="""
|
|
107
|
+
function getSimplifiedDOM(node) {
|
|
108
|
+
// 全局配置:最大属性值长度
|
|
109
|
+
const MAX_ATTR_LEN = 40;
|
|
110
|
+
|
|
111
|
+
// 1. 处理文本节点
|
|
112
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
113
|
+
const text = node.textContent.trim();
|
|
114
|
+
return text ? text.slice(0, 100) + (text.length > 100 ? '...' : '') : null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 2. 过滤无用标签
|
|
118
|
+
const ignoreTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'IFRAME', 'LINK', 'META', 'AUDIO', 'VIDEO', 'CANVAS'];
|
|
119
|
+
if (ignoreTags.includes(node.tagName)) return null;
|
|
120
|
+
if (node.nodeType !== Node.ELEMENT_NODE) return null;
|
|
121
|
+
|
|
122
|
+
// 3. 过滤不可见元素
|
|
123
|
+
const style = window.getComputedStyle(node);
|
|
124
|
+
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return null;
|
|
125
|
+
|
|
126
|
+
const rect = node.getBoundingClientRect();
|
|
127
|
+
if ((rect.width === 0 || rect.height === 0) && style.overflow !== 'visible') return null;
|
|
128
|
+
|
|
129
|
+
// --- 开始构建标签字符串 ---
|
|
130
|
+
const tagName = node.tagName.toLowerCase();
|
|
131
|
+
let tagStr = tagName;
|
|
132
|
+
|
|
133
|
+
const id = node.id;
|
|
134
|
+
const className = node.getAttribute('class');
|
|
135
|
+
|
|
136
|
+
// A. 处理 ID 简写 (#id)
|
|
137
|
+
// 限制提高到 40
|
|
138
|
+
if (id && id.length <= MAX_ATTR_LEN) {
|
|
139
|
+
tagStr += `#${id}`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// B. 处理 Class 简写 (.class)
|
|
143
|
+
// 限制提高到 40
|
|
144
|
+
if (className && typeof className === 'string' && className.length <= MAX_ATTR_LEN) {
|
|
145
|
+
const classes = className.trim().split(/\s+/);
|
|
146
|
+
if (classes.length > 0) {
|
|
147
|
+
tagStr += `.${classes.join('.')}`;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
let propsStr = '';
|
|
152
|
+
|
|
153
|
+
// C. 处理属性
|
|
154
|
+
if (node.hasAttributes()) {
|
|
155
|
+
for (const attr of node.attributes) {
|
|
156
|
+
const name = attr.name;
|
|
157
|
+
const value = attr.value;
|
|
158
|
+
|
|
159
|
+
// 1. 跳过 ID 和 Class (已在 tagStr 处理,或因过长被丢弃)
|
|
160
|
+
if (name === 'id' || name === 'class') continue;
|
|
161
|
+
|
|
162
|
+
// 2. 黑名单:直接删除 style 和 aria-label
|
|
163
|
+
if (name === 'style' || name === 'aria-label') continue;
|
|
164
|
+
|
|
165
|
+
// 3. 特殊标签:path 标签删除所有属性
|
|
166
|
+
if (tagName === 'path') continue;
|
|
167
|
+
|
|
168
|
+
// 4. 【长度与白名单逻辑】
|
|
169
|
+
// 如果不是 src 且不是 href,同时长度又超过了 40,则删除
|
|
170
|
+
const isLinkAttr = (name === 'src' || name === 'href');
|
|
171
|
+
|
|
172
|
+
if (!isLinkAttr && value.length > MAX_ATTR_LEN) {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// 5. 拼接保留的属性
|
|
177
|
+
propsStr += ` ${name}="${value.replace(/"/g, '"')}"`;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 4. 递归子节点
|
|
182
|
+
let childNodes = Array.from(node.childNodes);
|
|
183
|
+
if (node.shadowRoot) {
|
|
184
|
+
childNodes = [...childNodes, ...Array.from(node.shadowRoot.childNodes)];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const children = childNodes
|
|
188
|
+
.map(getSimplifiedDOM)
|
|
189
|
+
.filter(n => n !== null);
|
|
190
|
+
|
|
191
|
+
// 5. 组装输出
|
|
192
|
+
if (children.length === 0) {
|
|
193
|
+
return `<${tagStr}${propsStr} />`;
|
|
194
|
+
}
|
|
195
|
+
return `<${tagStr}${propsStr}>${children.join('')}</${tagName}>`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return getSimplifiedDOM(document.body);
|
|
199
|
+
"""
|
|
105
200
|
|
|
106
201
|
# 使用requests获取html,用于测试是否使用了瑞数和jsl
|
|
107
202
|
def requests_html(url):
|
|
@@ -268,11 +363,3 @@ def compress_image_bytes(input_bytes, target_size_mb=1):
|
|
|
268
363
|
|
|
269
364
|
return output_bytes
|
|
270
365
|
|
|
271
|
-
# todo: 大致盘一下各种判定的逻辑【以下的所有压缩比之间的差距均取“绝对值”】
|
|
272
|
-
# 1. 如果requests、无头、有头获取到的压缩比之间从差距都在15%以内,则认定该页面是静态页面,此时优先使用requests请求
|
|
273
|
-
# 2. 如果requests的status_code为特定的412,或者521,则判定是瑞数和jsl。[此时还有一个特点:requests的压缩比会与其他两种方式获取到的压缩比差距非常大(一两千的那种)]
|
|
274
|
-
# 3. 如果requests、无头、有头获取到的压缩比之间差距都在40%以上,则判定该页面只可以用有头采集
|
|
275
|
-
# 4. 如果无头和有头获取到的压缩比之间差距小于15%,但是requests和无头的差距大于40%,则认定该页面可以使用无头浏览器采集
|
|
276
|
-
# 5. 如果requests和有头获取到的压缩比之间差距小于15%,但是无头和有头的差距大于40%,则认定该页面优先使用有头浏览器采集
|
|
277
|
-
# 【此时可能是:1.使用了别的检测无头的waf。2.网站使用瑞数,但是这次请求没有拦截requests(不知道是不是瑞数那边故意设置的),
|
|
278
|
-
# 此时如果想进一步判定是否是瑞数,可以使用有头浏览器取一下cookies,如果cookies里面存在瑞数的cookie,那么就可以断定是瑞数】
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|