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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Jarvis_Brain
3
- Version: 0.1.11.10
3
+ Version: 0.1.13.9
4
4
  Summary: Jarvis brain mcp
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: beautifulsoup4
@@ -7,7 +7,7 @@ import os
7
7
  import time
8
8
  from typing import Any
9
9
 
10
- import DrissionPage
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 = True
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
- target_tab.scroll.down(pixel)
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
- target_tab.scroll.up(pixel)
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
- target_tab.scroll.left(pixel)
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
- target_tab.scroll.right(pixel)
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
- target_tab.scroll.down()
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
- # time.sleep(1)
328
- origin_png = target_tab.get_screenshot(as_bytes="jpg", full_page=True)
329
- compress_png = compress_image_bytes(origin_png, 0.5)
330
- image_path = os.path.join(html_source_code_local_save_path, f"{browser_port}_{tab_id}_{timestamp}.jpg")
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)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "Jarvis_Brain" # 别人下载时用的名字,必须在 PyPI 上唯一
3
- version = "0.1.11.10"
3
+ version = "0.1.13.9"
4
4
  description = "Jarvis brain mcp"
5
5
  dependencies = [
6
6
  "fastmcp",
@@ -10,7 +10,7 @@ import base64
10
10
  from PIL import Image
11
11
  import io
12
12
 
13
- compress_html_js = """
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, '&quot;')}"`;
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,那么就可以断定是瑞数】