beswarm 0.1.34__py3-none-any.whl → 0.1.35__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.
@@ -0,0 +1,145 @@
1
+ import os
2
+ import io
3
+ import copy
4
+ import base64
5
+ import platform
6
+ import pyautogui
7
+ from datetime import datetime
8
+ from ..aient.src.aient.plugins import register_tool, get_function_call_list
9
+
10
+ from ..aient.src.aient.models import chatgpt
11
+ from ..aient.src.aient.prompt import system_prompt, instruction_system_prompt
12
+ from ..aient.src.aient.core.utils import get_image_message, get_text_message
13
+
14
+ from ..utils import extract_xml_content
15
+
16
+ async def get_current_screen_image_message(prompt):
17
+ print("instruction agent 正在截取当前屏幕...")
18
+ try:
19
+ # 使用 pyautogui 截取屏幕,返回 PIL Image 对象
20
+ screenshot = pyautogui.screenshot()
21
+ # img_width, img_height = screenshot.size # 获取截图尺寸
22
+ img_width, img_height = pyautogui.size()
23
+ print(f"截图成功,尺寸: {img_width}x{img_height}")
24
+
25
+ # 将 PIL Image 对象转换为 Base64 编码的 PNG 字符串
26
+ buffered = io.BytesIO()
27
+ screenshot.save(buffered, format="PNG")
28
+ base64_encoded_image = base64.b64encode(buffered.getvalue()).decode("utf-8")
29
+ IMAGE_MIME_TYPE = "image/png" # 截图格式为 PNG
30
+
31
+ except ImportError:
32
+ # Pillow 也是 pyautogui 的依赖,但以防万一单独处理
33
+ print("\n❌ 请安装所需库: pip install Pillow pyautogui")
34
+ return False
35
+ except Exception as e:
36
+ print(f"\n❌ 截取屏幕或处理图像时出错: {e}")
37
+ return False
38
+
39
+ engine_type = "gpt"
40
+ message_list = []
41
+ text_message = await get_text_message(prompt, engine_type)
42
+ image_message = await get_image_message(f"data:{IMAGE_MIME_TYPE};base64," + base64_encoded_image, engine_type)
43
+ message_list.append(text_message)
44
+ message_list.append(image_message)
45
+ return message_list
46
+
47
+ @register_tool()
48
+ async def UIworker(goal, tools, work_dir, cache_messages=None):
49
+ """
50
+ 启动一个 **工作智能体 (Worker Agent)** 来自动完成指定的任务目标 (`goal`)。
51
+
52
+ 这个工作智能体接收一个清晰的任务描述、一组可供调用的工具 (`tools`),以及一个工作目录 (`work_dir`)。
53
+ 它会利用语言模型的能力,结合可用的工具,自主规划并逐步执行必要的操作,直到最终完成指定的任务目标。
54
+ 核心功能是根据输入的目标,驱动整个任务执行流程。
55
+
56
+ Args:
57
+ goal (str): 需要完成的具体任务目标描述。工作智能体将围绕此目标进行工作。必须清晰、具体。
58
+ tools (list[str]): 一个包含可用工具函数对象的列表。工作智能体在执行任务时可能会调用这些工具来与环境交互(例如读写文件、执行命令等)。
59
+ work_dir (str): 工作目录的绝对路径。工作智能体将在此目录上下文中执行操作。
60
+
61
+ Returns:
62
+ str: 当任务成功完成时,返回字符串 "任务已完成"。
63
+ """
64
+
65
+ tools_json = [value for _, value in get_function_call_list(tools).items()]
66
+ work_agent_system_prompt = system_prompt.format(
67
+ os_version=platform.platform(),
68
+ workspace_path=work_dir,
69
+ shell=os.getenv('SHELL', 'Unknown'),
70
+ tools_list=tools_json
71
+ )
72
+
73
+ work_agent_config = {
74
+ "api_key": os.getenv("API_KEY"),
75
+ "api_url": os.getenv("BASE_URL"),
76
+ "engine": os.getenv("MODEL"),
77
+ "system_prompt": work_agent_system_prompt,
78
+ "print_log": True,
79
+ # "max_tokens": 8000,
80
+ "temperature": 0.5,
81
+ "function_call_max_loop": 100,
82
+ }
83
+ if cache_messages:
84
+ work_agent_config["cache_messages"] = cache_messages
85
+
86
+ instruction_agent_config = {
87
+ "api_key": os.getenv("API_KEY"),
88
+ "api_url": os.getenv("BASE_URL"),
89
+ "engine": os.getenv("MODEL"),
90
+ "system_prompt": instruction_system_prompt.format(os_version=platform.platform(), tools_list=tools_json, workspace_path=work_dir, current_time=datetime.now().strftime("%Y-%m-%d %H:%M:%S")),
91
+ "print_log": False,
92
+ # "max_tokens": 4000,
93
+ "temperature": 0.7,
94
+ "use_plugins": False,
95
+ }
96
+
97
+ # 工作agent初始化
98
+ work_agent = chatgpt(**work_agent_config)
99
+ async def instruction_agent_task():
100
+ while True:
101
+ instruction_prompt = f"""
102
+ 任务目标: {goal}
103
+
104
+ 以上对话都是工作智能体的对话历史。
105
+
106
+ 根据以上对话历史和目标,请生成下一步指令。如果任务已完成,请回复"任务已完成"。
107
+ """
108
+ # 让指令agent分析对话历史并生成新指令
109
+ instruction_agent = chatgpt(**instruction_agent_config)
110
+ instruction_agent.conversation["default"] = copy.deepcopy(work_agent.conversation["default"])
111
+ new_prompt = await get_current_screen_image_message(instruction_prompt)
112
+ next_instruction = await instruction_agent.ask_async(new_prompt)
113
+ print("\n🤖 指令智能体生成的下一步指令:", next_instruction)
114
+ if "fetch_gpt_response_stream HTTP Error', 'status_code': 404" in next_instruction:
115
+ raise Exception(f"Model: {instruction_agent_config['engine']} not found!")
116
+ if "'status_code': 413" in next_instruction:
117
+ raise Exception(f"The request body is too long, please try again.")
118
+ next_instruction = extract_xml_content(next_instruction, "instructions")
119
+ if not next_instruction:
120
+ print("\n❌ 指令智能体生成的指令不符合要求,请重新生成。")
121
+ continue
122
+ else:
123
+ break
124
+ return next_instruction
125
+
126
+ need_instruction = True
127
+ while True:
128
+ next_instruction = ''
129
+ if need_instruction:
130
+ next_instruction = await instruction_agent_task()
131
+
132
+ # 检查任务是否完成
133
+ if "任务已完成" in next_instruction:
134
+ print("\n✅ 任务已完成!")
135
+ break
136
+ new_prompt = await get_current_screen_image_message(next_instruction)
137
+ result = await work_agent.ask_async(new_prompt)
138
+ if result.strip() == '':
139
+ print("\n❌ 工作智能体回复为空,请重新生成指令。")
140
+ need_instruction = False
141
+ continue
142
+ print("✅ 工作智能体回复:", result)
143
+ need_instruction = True
144
+
145
+ return "任务已完成"
beswarm/tools/__init__.py CHANGED
@@ -1,8 +1,11 @@
1
1
  from .think import think
2
2
  from .edit_file import edit_file
3
3
  from .worker import worker
4
+ from .UIworker import UIworker
5
+
4
6
  from .search_arxiv import search_arxiv
5
7
  from .repomap import get_code_repo_map
8
+ from .click import find_and_click_element, scroll_screen
6
9
  #显式导入 aient.plugins 中的所需内容
7
10
  from ..aient.src.aient.plugins import (
8
11
  excute_command,
@@ -15,6 +18,7 @@ from ..aient.src.aient.plugins import (
15
18
  write_to_file,
16
19
  download_read_arxiv_pdf,
17
20
  get_url_content,
21
+ register_tool,
18
22
  )
19
23
 
20
24
  __all__ = [
@@ -34,4 +38,8 @@ __all__ = [
34
38
  "write_to_file",
35
39
  "download_read_arxiv_pdf",
36
40
  "get_url_content",
41
+ "find_and_click_element",
42
+ "scroll_screen",
43
+ "register_tool",
44
+ "UIworker",
37
45
  ]
beswarm/tools/click.py ADDED
@@ -0,0 +1,456 @@
1
+ import io
2
+ import os
3
+ import re
4
+ import json
5
+ import time
6
+ import base64
7
+ import pyautogui # 用于桌面屏幕点击
8
+ import pyperclip # 新增:用于操作剪贴板
9
+ import platform # 新增:用于检测操作系统
10
+ from PIL import Image, ImageDraw
11
+ from ..aient.src.aient.plugins import register_tool
12
+
13
+ from ..aient.src.aient.models import chatgpt
14
+ from ..aient.src.aient.core.utils import get_image_message, get_text_message
15
+
16
+ def display_image_with_bounding_boxes_and_masks_py(
17
+ original_image,
18
+ box_and_mask_data,
19
+ output_overlay_path="overlay_image.png",
20
+ output_compare_dir="comparison_outputs"
21
+ ):
22
+ """
23
+ 在原始图像上绘制边界框和掩码,并生成裁剪区域与掩码的对比图。
24
+
25
+ Args:
26
+ original_image (str): 原始图像的文件路径。
27
+ box_and_mask_data (list): extract_box_and_mask_py 的输出列表。
28
+ output_overlay_path (str): 保存带有叠加效果的图像的路径。
29
+ output_compare_dir (str): 保存对比图像的目录路径。
30
+ """
31
+ try:
32
+ # 修改:直接使用传入的 PIL Image 对象,并确保是 RGBA
33
+ img_original = original_image.convert("RGBA")
34
+ img_width, img_height = img_original.size
35
+ # except FileNotFoundError: # 移除:不再需要从文件加载
36
+ # print(f"Error: Original image not found at {original_image}")
37
+ # return
38
+ except Exception as e:
39
+ # 修改:更新错误消息
40
+ print(f"Error processing original image object: {e}")
41
+ return
42
+
43
+ # 创建一个副本用于绘制叠加效果
44
+ img_overlay = img_original.copy()
45
+ draw = ImageDraw.Draw(img_overlay, "RGBA") # 使用 RGBA 模式以支持透明度
46
+
47
+ # 定义颜色列表
48
+ colors_hex = ['#FF0000', '#00FF00', '#0000FF', '#FFFF00', '#FF00FF', '#00FFFF']
49
+ # 将十六进制颜色转换为 RGBA 元组 (用于绘制)
50
+ colors_rgba = []
51
+ for hex_color in colors_hex:
52
+ h = hex_color.lstrip('#')
53
+ rgb = tuple(int(h[i:i+2], 16) for i in (0, 2, 4))
54
+ colors_rgba.append(rgb + (255,)) # (R, G, B, Alpha) - 边框完全不透明
55
+
56
+ # 创建输出目录(如果不存在)
57
+ import os
58
+ os.makedirs(output_compare_dir, exist_ok=True)
59
+
60
+ print(f"Found {len(box_and_mask_data)} box/mask pairs to process.")
61
+
62
+ for i, data in enumerate(box_and_mask_data):
63
+ box_0_1000 = data['box'] # [ymin, xmin, ymax, xmax] in 0-1000 range
64
+ mask_b64 = data['mask_base64']
65
+ color_index = i % len(colors_rgba)
66
+ outline_color = colors_rgba[color_index]
67
+ # 叠加掩码时使用半透明颜色
68
+ mask_fill_color = outline_color[:3] + (int(255 * 0.7),) # 70% Alpha
69
+
70
+ # --- 1. 坐标转换与验证 ---
71
+ # 将 0-1000 坐标转换为图像像素坐标 (left, top, right, bottom)
72
+ # 假设 box 是 [ymin, xmin, ymax, xmax]
73
+ try:
74
+ ymin_norm, xmin_norm, ymax_norm, xmax_norm = [c / 1000.0 for c in box_0_1000]
75
+
76
+ left = int(xmin_norm * img_width)
77
+ top = int(ymin_norm * img_height)
78
+ right = int(xmax_norm * img_width)
79
+ bottom = int(ymax_norm * img_height)
80
+
81
+ # 确保坐标在图像范围内且有效
82
+ left = max(0, left)
83
+ top = max(0, top)
84
+ right = min(img_width, right)
85
+ bottom = min(img_height, bottom)
86
+
87
+ box_width_px = right - left
88
+ box_height_px = bottom - top
89
+
90
+ if box_width_px <= 0 or box_height_px <= 0:
91
+ print(f"Skipping box {i+1} due to zero or negative dimensions after conversion.")
92
+ continue
93
+
94
+ except Exception as e:
95
+ print(f"Error processing coordinates for box {i+1}: {box_0_1000}. Error: {e}")
96
+ continue
97
+
98
+ print(f"Processing Box {i+1}: Pixels(L,T,R,B)=({left},{top},{right},{bottom}) Color={colors_hex[color_index]}")
99
+
100
+ # --- 2. 在叠加图像上绘制边界框 ---
101
+ try:
102
+ draw.rectangle([left, top, right, bottom], outline=outline_color, width=5)
103
+ except Exception as e:
104
+ print(f"Error drawing rectangle for box {i+1}: {e}")
105
+ continue
106
+
107
+ # --- 3. 处理并绘制掩码 ---
108
+ try:
109
+ # 解码 Base64 掩码数据
110
+ mask_bytes = base64.b64decode(mask_b64)
111
+ mask_img_raw = Image.open(io.BytesIO(mask_bytes)).convert("RGBA")
112
+
113
+ # 将掩码图像缩放到边界框的像素尺寸
114
+ mask_img_resized = mask_img_raw.resize((box_width_px, box_height_px), Image.Resampling.NEAREST)
115
+
116
+ # 创建一个纯色块,应用掩码的 alpha 通道
117
+ color_block = Image.new('RGBA', mask_img_resized.size, mask_fill_color)
118
+
119
+ # 将带有透明度的颜色块粘贴到叠加图像上,使用掩码的 alpha 通道作为粘贴蒙版
120
+ # mask_img_resized.split()[-1] 提取 alpha 通道
121
+ img_overlay.paste(color_block, (left, top), mask=mask_img_resized.split()[-1])
122
+
123
+ except base64.binascii.Error:
124
+ print(f"Error: Invalid Base64 data for mask {i+1}.")
125
+ continue
126
+ except Exception as e:
127
+ print(f"Error processing or drawing mask for box {i+1}: {e}")
128
+ continue
129
+
130
+ # --- 4. 生成对比图 ---
131
+ try:
132
+ # 从原始图像中裁剪出边界框区域
133
+ img_crop = img_original.crop((left, top, right, bottom))
134
+
135
+ # 准备掩码预览图(使用原始解码后的掩码,调整大小以匹配裁剪区域)
136
+ # 这里直接使用缩放后的 mask_img_resized 的 RGB 部分可能更直观
137
+ mask_preview = mask_img_resized.convert("RGB") # 转换为 RGB 以便保存为常见格式
138
+
139
+ # 保存裁剪图和掩码预览图
140
+ crop_filename = os.path.join(output_compare_dir, f"compare_{i+1}_crop.png")
141
+ mask_filename = os.path.join(output_compare_dir, f"compare_{i+1}_mask.png")
142
+ img_crop.save(crop_filename)
143
+ mask_preview.save(mask_filename)
144
+ print(f" - Saved comparison: {crop_filename}, {mask_filename}")
145
+
146
+ except Exception as e:
147
+ print(f"Error creating or saving comparison images for box {i+1}: {e}")
148
+
149
+ # --- 5. 保存最终的叠加图像 ---
150
+ try:
151
+ img_overlay.save(output_overlay_path)
152
+ print(f"\nOverlay image saved to: {output_overlay_path}")
153
+ print(f"Comparison images saved in: {output_compare_dir}")
154
+ except Exception as e:
155
+ print(f"Error saving the final overlay image: {e}")
156
+
157
+ def get_json_from_text(text):
158
+ regex_pattern = r'({\"box_2d\".+?})' # 匹配包含至少一个对象的数组
159
+ # regex_pattern = r'(\[\s*\{.*?\}\s*\])' # 匹配包含至少一个对象的数组
160
+
161
+ # 使用 re.search 查找第一个匹配项,re.MULTILINE 使点号能匹配换行符
162
+ match = re.search(regex_pattern, text, re.MULTILINE)
163
+
164
+
165
+ if match:
166
+ # 提取匹配到的整个 JSON 数组字符串 (group 1 因为模式中有括号)
167
+ json_string = match.group(1)
168
+ # print(f"匹配到的 JSON 字符串: {json_string}")
169
+
170
+ try:
171
+ # 使用 json.loads() 解析字符串
172
+ parsed_data = json.loads(json_string)
173
+ # 使用 json.dumps 美化打印输出
174
+ # print(json.dumps(parsed_data, indent=2, ensure_ascii=False))
175
+
176
+ # 例如,获取第一个元素的 label
177
+ if isinstance(parsed_data, list) and len(parsed_data) > 0:
178
+ first_item = parsed_data[0]
179
+ if isinstance(first_item, dict):
180
+ label = first_item.get('label')
181
+ print(f"\n第一个元素的 label 是: {label}")
182
+ return first_item
183
+
184
+ return parsed_data
185
+
186
+ except json.JSONDecodeError as e:
187
+ print(f"JSON 解析错误: {e}")
188
+ print(f"出错的字符串是: {json_string}")
189
+ else:
190
+ print("在文本中未找到匹配的 JSON 数组。")
191
+
192
+
193
+ @register_tool()
194
+ async def find_and_click_element(target_element, input_text=None):
195
+ """
196
+ 在当前屏幕截图中查找目标 UI 元素,并在屏幕上点击其中心点。
197
+
198
+ 此函数首先截取当前屏幕,然后将截图和目标元素的描述 (`target_element`) 发送给配置好的大语言模型 (LLM)。
199
+ LLM 被要求识别出目标元素,并返回其在截图中的边界框 (bounding box) 和掩码 (mask) 信息(通常以 JSON 格式)。
200
+ 函数接着解析 LLM 的响应,提取出边界框坐标。
201
+ (可选)为了调试和验证,函数可以根据 LLM 返回的数据在截图副本上绘制边界框和掩码,并将结果保存为图像文件。
202
+ 最后,函数计算边界框的中心点像素坐标,并使用 `pyautogui` 库在该屏幕坐标上模拟鼠标点击。如果提供了 `input_text`,则会在点击后尝试输入该文本。
203
+
204
+ Args:
205
+ target_element (str): 需要查找和点击的 UI 元素的文本描述 (例如 "登录按钮", "用户名输入框")。LLM 将使用此描述来定位元素。
206
+ input_text (str, optional): 在点击元素后需要输入的文本。如果为 None 或空字符串,则只执行点击操作。默认为 None。
207
+
208
+ Returns:
209
+ str: 如果成功找到元素、计算坐标并执行点击(以及可能的输入),则返回表示成功的字符串消息 (例如 "点击成功!", "点击并输入 '...' 成功!")。
210
+ 如果在任何步骤中失败(例如截图失败、LLM 未返回有效坐标、点击失败),则返回 False。
211
+ 如果点击成功但输入失败,则返回包含错误信息的字符串。
212
+ """
213
+
214
+ click_agent_config = {
215
+ "api_key": os.getenv("API_KEY"),
216
+ "api_url": os.getenv("BASE_URL"),
217
+ "engine": "gemini-2.5-pro",
218
+ "system_prompt": "you are a professional UI test engineer, now you need to find the specified screen element.",
219
+ # "system_prompt": "你是一个专业的UI测试工程师,现在需要你找到指定屏幕元素。",
220
+ "print_log": True,
221
+ "temperature": 0.7,
222
+ "use_plugins": False,
223
+ }
224
+
225
+ # 工作agent初始化
226
+ click_agent = chatgpt(**click_agent_config)
227
+ prompt = f"Give the segmentation masks for the {target_element}. Output a JSON list of segmentation masks where each entry contains the 2D bounding box in \"box_2d\" and the mask in \"mask\". Only output the one that meets the criteria the most."
228
+
229
+ print("正在截取当前屏幕...")
230
+ try:
231
+ # 使用 pyautogui 截取屏幕,返回 PIL Image 对象
232
+ screenshot = pyautogui.screenshot()
233
+ # img_width, img_height = screenshot.size # 获取截图尺寸
234
+ img_width, img_height = pyautogui.size()
235
+ print(f"截图成功,尺寸: {img_width}x{img_height}")
236
+
237
+ # 将 PIL Image 对象转换为 Base64 编码的 PNG 字符串
238
+ buffered = io.BytesIO()
239
+ screenshot.save(buffered, format="PNG")
240
+ base64_encoded_image = base64.b64encode(buffered.getvalue()).decode("utf-8")
241
+ IMAGE_MIME_TYPE = "image/png" # 截图格式为 PNG
242
+
243
+ except ImportError:
244
+ # Pillow 也是 pyautogui 的依赖,但以防万一单独处理
245
+ print("\n❌ 请安装所需库: pip install Pillow pyautogui")
246
+ return False
247
+ except Exception as e:
248
+ print(f"\n❌ 截取屏幕或处理图像时出错: {e}")
249
+ return False
250
+
251
+ engine_type = "gpt"
252
+ message_list = []
253
+ text_message = await get_text_message(prompt, engine_type)
254
+ image_message = await get_image_message(f"data:{IMAGE_MIME_TYPE};base64," + base64_encoded_image, engine_type)
255
+ message_list.append(text_message)
256
+ message_list.append(image_message)
257
+
258
+ result = await click_agent.ask_async(message_list)
259
+ if result.strip() == '':
260
+ print("\n❌ click智能体回复为空,请重新生成指令。")
261
+ return False
262
+ first_item = get_json_from_text(result)
263
+ if not first_item or "box_2d" not in first_item:
264
+ print("\n❌ 未能从模型响应中提取有效的 box_2d。")
265
+ return False
266
+
267
+
268
+ box_0_1000 = first_item.get("box_2d") # 假设格式为 [ymin, xmin, ymax, xmax],范围 0-1000
269
+ mask_data_uri = first_item.get("mask") # 假设格式为 "data:image/png;base64,..."
270
+
271
+ if not box_0_1000 or not isinstance(box_0_1000, list) or len(box_0_1000) != 4:
272
+ print(f"\n❌ 未能从模型响应中提取有效的 box_2d: {box_0_1000}")
273
+ return False
274
+ if not mask_data_uri or not isinstance(mask_data_uri, str) or not mask_data_uri.startswith("data:image/png;base64,"):
275
+ print(f"\n❌ 未能从模型响应中提取有效的 mask data URI: {mask_data_uri}")
276
+ # 如果找不到蒙版,可以选择是失败返回还是继续点击(这里选择继续)
277
+ mask_b64 = None # 没有有效的蒙版
278
+ else:
279
+ # 提取 Base64 部分
280
+ mask_b64 = mask_data_uri.split(',')[-1]
281
+
282
+ print(f"✅ click智能体回复 (box_2d 范围 0-1000): {box_0_1000}")
283
+ # ----------------------------------------------
284
+
285
+ # --- 新增:调用 display 函数进行可视化 ---
286
+ if box_0_1000: # 仅在有蒙版数据时才尝试绘制
287
+ try:
288
+ print("尝试生成可视化叠加图像...")
289
+ box_and_mask_data_for_display = [{
290
+ "box": box_0_1000,
291
+ "mask_base64": mask_b64
292
+ }]
293
+ display_image_with_bounding_boxes_and_masks_py(
294
+ original_image=screenshot, # 传递 PIL Image 对象
295
+ box_and_mask_data=box_and_mask_data_for_display,
296
+ output_overlay_path=f"click_overlay_{time.strftime('%Y%m%d_%H%M%S')}.png", # 可以自定义输出文件名
297
+ output_compare_dir="click_compare" # 可以自定义输出目录
298
+ )
299
+ except Exception as e:
300
+ print(f"⚠️ 生成可视化图像时出错: {e}") # 出错不影响点击逻辑继续
301
+ else:
302
+ print("⚠️ 未找到有效的坐标数据,跳过可视化。")
303
+
304
+ try:
305
+
306
+ # 检查 box_0_1000 格式是否正确
307
+ if not (isinstance(box_0_1000, list) and len(box_0_1000) == 4 and all(isinstance(c, int) for c in box_0_1000)):
308
+ print(f"\n❌ 无效的 box_2d 格式或类型: {box_0_1000},期望是包含4个整数的列表。")
309
+ return False
310
+
311
+ # 坐标转换 (0-1000 范围到 0.0-1.0 范围)
312
+ ymin_norm, xmin_norm, ymax_norm, xmax_norm = [c / 1000.0 for c in box_0_1000]
313
+
314
+ # 计算相对于截图的像素坐标
315
+ left = int(xmin_norm * img_width)
316
+ top = int(ymin_norm * img_height)
317
+ right = int(xmax_norm * img_width)
318
+ bottom = int(ymax_norm * img_height)
319
+
320
+ # 确保坐标在截图范围内且有效
321
+ left = max(0, left)
322
+ top = max(0, top)
323
+ right = min(img_width, right)
324
+ bottom = min(img_height, bottom)
325
+
326
+ # 检查边界框是否有效
327
+ if left >= right or top >= bottom:
328
+ print(f"\n❌ 计算出的边界框无效: left={left}, top={top}, right={right}, bottom={bottom}")
329
+ return False
330
+
331
+ # 计算点击的中心点 (相对于截图的坐标)
332
+ # **注意**: 这个坐标现在是相对于截图左上角的像素坐标。
333
+ # 如果截图是全屏的,那么这个坐标也就是屏幕坐标。
334
+ center_x = (left + right) // 2
335
+ center_y = (top + bottom) // 2
336
+
337
+ print(f"截图尺寸: width={img_width}, height={img_height}")
338
+ print(f"计算出的像素坐标 (相对于截图): left={left}, top={top}, right={right}, bottom={bottom}")
339
+ print(f"计算出的点击中心点 (屏幕坐标): x={center_x}, y={center_y}")
340
+
341
+ # 执行点击操作
342
+ print(f"尝试在屏幕坐标 ({center_x}, {center_y}) 点击...")
343
+ # 使用 pyautogui 在电脑屏幕上点击
344
+ pyautogui.click(center_x, center_y)
345
+ pyautogui.click(center_x, center_y)
346
+ print(f"✅ 在 ({center_x}, {center_y}) 点击成功。")
347
+ # input_text = "123456"
348
+ if input_text:
349
+ try:
350
+ print(f"尝试通过剪贴板输入文本: '{input_text}'")
351
+ # 保存当前剪贴板内容
352
+ original_clipboard_content = pyperclip.paste()
353
+ pyperclip.copy(input_text) # 将文本复制到剪贴板
354
+
355
+ # 根据操作系统执行粘贴操作
356
+ if platform.system() == "Darwin": # macOS
357
+ pyautogui.hotkey('command', 'v')
358
+ else: # Windows, Linux, etc.
359
+ pyautogui.hotkey('ctrl', 'v')
360
+
361
+ time.sleep(0.1) # 给粘贴操作一点时间,确保文本已粘贴
362
+
363
+ # 恢复原始剪贴板内容
364
+ # 如果不希望恢复,可以注释掉下面这行
365
+ pyperclip.copy(original_clipboard_content)
366
+
367
+ print(f"✅ 通过剪贴板输入文本成功。")
368
+ return f"点击并输入 '{input_text}' 成功!"
369
+ except ImportError:
370
+ print("\n❌ pyperclip 库未安装。请运行 'pip install pyperclip' 以支持通过剪贴板输入中文。")
371
+ print(f"将尝试使用 pyautogui.typewrite (可能无法正确输入中文): '{input_text}'")
372
+ try:
373
+ pyautogui.typewrite(input_text, interval=0.1) # 尝试原始方法作为备选
374
+ print(f"✅ (备用 typewrite) 已尝试输入文本。")
375
+ return f"点击并尝试输入 '{input_text}' (使用 typewrite,中文可能失败)!"
376
+ except Exception as e_typewrite:
377
+ print(f"\n❌ 使用 pyautogui.typewrite 输入文本时也发生错误: {e_typewrite}")
378
+ return f"点击成功,但输入文本 '{input_text}' (typewrite) 失败: {e_typewrite}"
379
+ except Exception as e:
380
+ print(f"\n❌ 通过剪贴板输入文本时发生错误: {e}")
381
+ # 即使输入失败,点击也算成功了
382
+ return f"点击成功,但输入文本 '{input_text}' (剪贴板) 失败: {e}"
383
+ else:
384
+ # 如果没有提供 input_text,只返回点击成功
385
+ return "点击成功!"
386
+
387
+ # except FileNotFoundError:
388
+ # print(f"错误:找不到图片文件 '{image_path}' 用于获取尺寸。")
389
+ # return False
390
+ except ImportError:
391
+ print("\n❌ 请安装所需库: pip install Pillow pyautogui")
392
+ return False
393
+ # 移除 AdbError 捕获
394
+ except Exception as e:
395
+ # 添加 pyautogui 可能抛出的异常类型,如果需要更精细的处理
396
+ print(f"\n❌ 处理点击时发生意外错误: {e}")
397
+ return False
398
+
399
+
400
+ @register_tool()
401
+ async def scroll_screen(direction: str = "down"):
402
+ """
403
+ 控制屏幕向上或向下滑动固定的距离。
404
+
405
+ Args:
406
+ direction (str, optional): 滚动的方向。可以是 "up" 或 "down"。
407
+ 默认为 "down"。
408
+
409
+ Returns:
410
+ str: 如果成功执行滚动,则返回相应的成功消息。
411
+ 如果方向无效或发生错误,则返回错误信息。
412
+ """
413
+ scroll_offset = 20
414
+ actual_scroll_amount = 0
415
+
416
+ if direction == "down":
417
+ actual_scroll_amount = -scroll_offset # 向下滚动为负值
418
+ print(f"尝试向下滚动屏幕,固定偏移量: {scroll_offset}...")
419
+ elif direction == "up":
420
+ actual_scroll_amount = scroll_offset # 向上滚动为正值
421
+ print(f"尝试向上滚动屏幕,固定偏移量: {scroll_offset}...")
422
+ else:
423
+ error_msg = f"错误:无效的滚动方向 '{direction}'。请使用 'up' 或 'down'。"
424
+ print(f"\n❌ {error_msg}")
425
+ return error_msg
426
+
427
+ try:
428
+ pyautogui.scroll(actual_scroll_amount)
429
+ success_msg = f"✅ 屏幕向 {direction} 滑动 {scroll_offset} 成功。"
430
+ print(success_msg)
431
+ return success_msg
432
+ except ImportError:
433
+ print("\n❌ pyautogui 库未安装。请运行 'pip install pyautogui'。")
434
+ return "错误:pyautogui 库未安装。"
435
+ except Exception as e:
436
+ error_msg = f"错误:屏幕滚动时发生: {e}"
437
+ print(f"\n❌ {error_msg}")
438
+ return error_msg
439
+
440
+
441
+ if __name__ == "__main__":
442
+ import asyncio
443
+ IMAGE_PATH = os.environ.get("IMAGE_PATH")
444
+ import time
445
+ time.sleep(2)
446
+ # asyncio.run(find_and_click_element("Write a message...", "你好"))
447
+ # asyncio.run(find_and_click_element("搜索框"))
448
+ # print(get_json_from_text(text))
449
+
450
+ # 测试滚动功能
451
+ asyncio.run(scroll_screen("down")) # 向下滚动
452
+ time.sleep(2) # 等待2秒观察效果
453
+ asyncio.run(scroll_screen("up")) # 向上滚动
454
+ # asyncio.run(scroll_screen("sideways")) # 测试无效方向
455
+
456
+ # python -m beswarm.tools.click
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: beswarm
3
- Version: 0.1.34
3
+ Version: 0.1.35
4
4
  Summary: MAS
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -19,8 +19,11 @@ Requires-Dist: networkx>=3.4.2
19
19
  Requires-Dist: numpy>=2.2.4
20
20
  Requires-Dist: pdfminer-six==20240706
21
21
  Requires-Dist: pillow>=11.2.1
22
+ Requires-Dist: pip>=25.1.1
23
+ Requires-Dist: pyautogui>=0.9.54
22
24
  Requires-Dist: pyexecjs>=1.5.1
23
25
  Requires-Dist: pygments>=2.19.1
26
+ Requires-Dist: pyperclip>=1.9.0
24
27
  Requires-Dist: pytz>=2025.2
25
28
  Requires-Dist: requests>=2.32.3
26
29
  Requires-Dist: scipy>=1.15.2
@@ -67,6 +70,22 @@ cd ~/Downloads/GitHub/beswarm && docker run --rm \
67
70
  --goal "分析这个仓库 https://github.com/cloneofsimo/minRF"
68
71
  ```
69
72
 
73
+ 测试 docker 是否可以用 GPU:
74
+
75
+ ```
76
+ docker run --gpus all -it --rm --entrypoint nvidia-smi yym68686/beswarm
77
+ ```
78
+
79
+ 服务器安装
80
+
81
+ ```
82
+ pip install pipx
83
+ pipx ensurepath
84
+ source ~/.bashrc
85
+ pipx install nvitop
86
+ pip install beswarm -i https://pypi.tuna.tsinghua.edu.cn/simple
87
+ ```
88
+
70
89
  main.py
71
90
 
72
91
  ```python