auto-coder 0.1.287__py3-none-any.whl → 0.1.289__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.

Potentially problematic release.


This version of auto-coder might be problematic. Click here for more details.

@@ -0,0 +1,1062 @@
1
+ import os
2
+ import time
3
+ import json
4
+ from typing import Dict, List, Any, Optional, Union
5
+ import pydantic
6
+ from loguru import logger
7
+ from rich.console import Console
8
+ from rich.panel import Panel
9
+ from rich.text import Text
10
+
11
+ import byzerllm
12
+ from autocoder.common import AutoCoderArgs
13
+ from autocoder.common.computer_use import ComputerUse
14
+ from autocoder.common.printer import Printer
15
+ from autocoder.utils.auto_coder_utils.chat_stream_out import stream_out
16
+ from autocoder.common.result_manager import ResultManager
17
+ from autocoder.common import git_utils
18
+ from autocoder.common.global_cancel import global_cancel
19
+ from byzerllm.utils.client import code_utils
20
+ from autocoder.common import detect_env
21
+ from autocoder.common import shells
22
+
23
+
24
+ class WebAction(pydantic.BaseModel):
25
+ """网页自动化操作"""
26
+ action: str
27
+ parameters: Dict[str, Any] = {}
28
+ description: Optional[str] = None
29
+ expected_outcome: Optional[str] = None # 新增: 期望的操作结果
30
+
31
+
32
+ class ActionResult(pydantic.BaseModel):
33
+ """Action执行结果"""
34
+ success: bool
35
+ action: WebAction
36
+ screenshot_path: Optional[str] = None
37
+ result: Dict[str, Any] = {}
38
+ error: Optional[str] = None
39
+
40
+
41
+ class AutoWebRequest(pydantic.BaseModel):
42
+ """自动化网页操作请求"""
43
+ user_input: str
44
+ context: Optional[str] = None
45
+ screenshot_path: Optional[str] = None
46
+
47
+
48
+ class AutoWebResponse(pydantic.BaseModel):
49
+ """自动化网页操作响应"""
50
+ actions: List[WebAction] = []
51
+ explanation: Optional[str] = None
52
+ additional_info: Optional[str] = None
53
+ suggested_next_steps: Optional[List[str]] = None
54
+ overall_status: Optional[str] = None # 新增: 整体任务状态
55
+
56
+
57
+ class AutoWebTuner:
58
+ """
59
+ 基于大模型的网页自动化工具
60
+
61
+ 该类使用ComputerUse工具集实现浏览器操作,从用户指令中自动解析需要执行的操作,
62
+ 并生成详细的执行步骤,验证每一步执行结果,最后执行并返回结果。
63
+ """
64
+
65
+ def __init__(self, llm: byzerllm.ByzerLLM, args: AutoCoderArgs):
66
+ """
67
+ 初始化
68
+
69
+ Args:
70
+ llm: 用于分析和生成操作的大语言模型
71
+ args: 自动编码器参数
72
+ """
73
+ self.llm = llm
74
+ self.args = args
75
+ self.printer = Printer()
76
+
77
+ # 初始化ComputerUse工具
78
+ self.computer = ComputerUse(llm=llm, args=args)
79
+
80
+ # 记录执行历史
81
+ self.execution_history = []
82
+
83
+ self.init_request = None
84
+ self.current_plan = None
85
+
86
+ self.current_action = None
87
+ self.current_action_result = None
88
+
89
+ @byzerllm.prompt()
90
+ def _guide_readme(self) -> str:
91
+ """
92
+ <guide>
93
+ 1. 在使用chrome浏览器的时候,如果在某个网页需要进行登录等动作时,请使用 ask_user 函数请求用户帮你登录,然后你再做之后的动作。
94
+ </guide>
95
+ """
96
+
97
+ @byzerllm.prompt()
98
+ def _command_readme(self) -> str:
99
+ '''
100
+ 你有如下函数可供使用:
101
+
102
+ <commands>
103
+
104
+ <command>
105
+ <name>screenshot</name>
106
+ <description>
107
+ 截取当前屏幕并保存为图片。可以用来获取当前屏幕状态,为后续的元素检测和操作提供基础。
108
+ </description>
109
+ <usage>
110
+ 该方法可以接受一个可选的 filename 参数,用于指定保存的文件名。
111
+ 如果不指定,会自动生成一个包含时间戳的文件名。
112
+
113
+ 使用例子:
114
+ screenshot() # 使用自动生成的文件名
115
+ screenshot(filename="my_screenshot.png") # 使用指定的文件名
116
+
117
+ 返回值:
118
+ 保存的截图文件路径
119
+ </usage>
120
+ </command>
121
+
122
+ <command>
123
+ <name>detect</name>
124
+ <description>
125
+ 分析图片,检测其中的各种界面元素(如按钮、输入框、链接等)并返回它们的位置和描述。
126
+ 对于需要点击或与特定界面元素交互的场景非常有用。
127
+ </description>
128
+ <usage>
129
+ 该方法需要一个image_path参数,通常是通过 screenshot() 函数获取的截图。
130
+
131
+ 使用例子:
132
+ detect(image_path="screenshot.png")
133
+
134
+ 返回值:
135
+ JSON格式的检测结果,包含检测到的界面元素列表及其边界框坐标和描述:
136
+ {
137
+ "objects": [
138
+ {
139
+ "type": "button",
140
+ "bounding_box": [x1, y1, x2, y2],
141
+ "text": "登录按钮"
142
+ },
143
+ ...
144
+ ]
145
+ }
146
+ </usage>
147
+ </command>
148
+
149
+ <command>
150
+ <name>click</name>
151
+ <description>
152
+ 在指定坐标处点击鼠标。适用于已知元素坐标的场景。
153
+ </description>
154
+ <usage>
155
+ 该方法需要以下参数:
156
+ 1. x: 点击位置的X坐标
157
+ 2. y: 点击位置的Y坐标
158
+ 3. button (可选): 使用哪个鼠标按钮,可以是'left'(默认),'right'或'middle'
159
+ 4. clicks (可选): 点击次数,默认为1
160
+
161
+ 使用例子:
162
+ click(x=100, y=200) # 在(100,200)处左键单击
163
+ click(x=100, y=200, button='right') # 右键点击
164
+ click(x=100, y=200, clicks=2) # 双击
165
+ </usage>
166
+ </command>
167
+
168
+ <command>
169
+ <name>find_and_click</name>
170
+ <description>
171
+ 查找并点击符合描述的界面元素。适用于不知道确切坐标,但知道元素描述的情况。
172
+ 内部会先截取屏幕图像,然后使用视觉模型查找元素,最后点击找到的元素中心位置。
173
+ </description>
174
+ <usage>
175
+ 该方法需要以下参数:
176
+ 1. element_desc: 元素的文本描述,例如"登录按钮"、"搜索框"等
177
+ 2. image_path: 图片路径,如果为None则自动截图
178
+
179
+ 使用例子:
180
+ find_and_click(element_desc="登录按钮") # 自动截图并查找点击
181
+ find_and_click(image_path="screenshot.png", element_desc="提交按钮")
182
+
183
+ 返回值:
184
+ 布尔值,表示是否成功找到并点击了元素
185
+ </usage>
186
+ </command>
187
+
188
+ <command>
189
+ <name>type</name>
190
+ <description>
191
+ 模拟键盘输入文本。适用于需要在输入框中输入内容的场景。
192
+ </description>
193
+ <usage>
194
+ 该方法需要以下参数:
195
+ 1. text: 要输入的文本内容
196
+ 2. interval (可选): 每个字符之间的时间间隔,默认为0.05秒
197
+
198
+ 使用例子:
199
+ type(text="Hello World")
200
+ type(text="慢速输入", interval=0.2) # 较慢的输入速度
201
+ </usage>
202
+ </command>
203
+
204
+ <command>
205
+ <name>press</name>
206
+ <description>
207
+ 按下指定的键盘按键
208
+ </description>
209
+ <usage>
210
+ 该方法需要一个key参数,支持数组和字符串。如果是字符串相当于直接输入该字符串,如果是数组相当于按下组合键。
211
+
212
+ 支持的键包括:
213
+ - 'enter', 'return'
214
+ - 'tab'
215
+ - 'space'
216
+ - 'backspace'
217
+ - 'esc', 'escape'
218
+ - 方向键: 'up', 'down', 'left', 'right'
219
+ - 功能键: 'f1', 'f2', ..., 'f12'
220
+ - 组合键: 'ctrl+c', 'ctrl+v', 'alt+tab'等
221
+
222
+ 使用例子:
223
+ press(key="text") # 输入 text 文本
224
+ press(key=["enter"]) # 按回车键
225
+ press(key=["ctrl", "a"]) # 全选
226
+ press(key=["alt", "tab"]) # 切换窗口
227
+ </usage>
228
+ </command>
229
+
230
+ <command>
231
+ <name>drag</name>
232
+ <description>
233
+ 从一个位置拖动到另一个位置。适用于拖拽操作,如滑块、拖动文件等。
234
+ </description>
235
+ <usage>
236
+ 该方法需要以下参数:
237
+ 1. start_x: 起始点X坐标
238
+ 2. start_y: 起始点Y坐标
239
+ 3. end_x: 终点X坐标
240
+ 4. end_y: 终点Y坐标
241
+ 5. duration (可选): 拖动持续时间,默认0.5秒
242
+
243
+ 使用例子:
244
+ drag(start_x=100, start_y=200, end_x=300, end_y=400)
245
+ drag(start_x=100, start_y=200, end_x=300, end_y=400, duration=1) # 较慢的拖动
246
+ </usage>
247
+ </command>
248
+
249
+ <command>
250
+ <name>scroll</name>
251
+ <description>
252
+ 在当前鼠标位置滚动滚轮。适用于网页滚动等场景。
253
+ </description>
254
+ <usage>
255
+ 该方法需要以下参数:
256
+ 1. clicks: 滚动的单位数量,正数向下滚动,负数向上滚动
257
+ 2. x (可选): 滚动时鼠标的X坐标,默认为当前位置
258
+ 3. y (可选): 滚动时鼠标的Y坐标,默认为当前位置
259
+
260
+ 使用例子:
261
+ scroll(clicks=10) # 向下滚动10个单位
262
+ scroll(clicks=-5) # 向上滚动5个单位
263
+ scroll(clicks=10, x=500, y=500) # 在指定位置滚动
264
+ </usage>
265
+ </command>
266
+
267
+ <command>
268
+ <name>extract_text</name>
269
+ <description>
270
+ 从屏幕截图中提取文本内容。适用于需要读取屏幕上文本信息的场景。
271
+ </description>
272
+ <usage>
273
+ 该方法需要以下参数:
274
+ 1. image_path: 图片路径,如果为None则自动截图
275
+ 2. region (可选): 提取区域[x1,y1,x2,y2],默认为整个图片
276
+
277
+ 使用例子:
278
+ extract_text() # 自动截图并提取所有文本
279
+ extract_text(image_path="screenshot.png") # 从指定图片提取文本
280
+ extract_text(image_path="screenshot.png", region=[100,100,300,200]) # 从指定区域提取
281
+
282
+ 返回值:
283
+ 提取到的文本内容
284
+ </usage>
285
+ </command>
286
+
287
+ <command>
288
+ <name>wait_loading</name>
289
+ <description>
290
+ 给定一个目标,等待目标出现。
291
+ </description>
292
+ <usage>
293
+ 该方法需要以下参数:
294
+ 1. target: 执行完动作后,用户期待看到的东西。
295
+
296
+ 使用例子:
297
+ wait_loading(
298
+ target="搜索列表页面"
299
+ )
300
+
301
+ 该函数无返回。
302
+
303
+ 此功能特别适用于执行完点击等需要等待一会的函数,避免操作完后立马就校验结果但系统还处于加载状态而导致误判。
304
+ </usage>
305
+ </command>
306
+
307
+ <command>
308
+ <name>ask_user</name>
309
+ <description>
310
+ 向用户提问并获取回答。适用于需要用户输入信息或确认的场景。
311
+ </description>
312
+ <usage>
313
+ 该方法需要一个question参数,指定要向用户提出的问题。
314
+
315
+ 使用例子:
316
+ ask_user(question="请输入您的用户名")
317
+ ask_user(question="是否继续操作?(yes/no)")
318
+
319
+ 返回值:
320
+ 用户输入的文本内容
321
+ </usage>
322
+ </command>
323
+
324
+ <command>
325
+ <name>response_user</name>
326
+ <description>
327
+ 向用户显示消息。适用于需要向用户提供反馈或信息的场景,但不需要用户响应。
328
+ </description>
329
+ <usage>
330
+ 该方法需要一个response参数,指定要向用户显示的消息内容。
331
+
332
+ 使用例子:
333
+ response_user(response="正在搜索网页...")
334
+ response_user(response="操作完成,请等待页面加载")
335
+
336
+ 该方法不等待用户输入,只是显示信息。
337
+ </usage>
338
+ </command>
339
+
340
+ <command>
341
+ <name>open_browser</name>
342
+ <description>
343
+ 打开指定的浏览器并
344
+ </description>
345
+ <usage>
346
+ 该方法支持以下参数:
347
+ 1. browser_name: 浏览器名称,默认为"chrome",也支持"firefox"和"edge"等
348
+
349
+ 推荐统一使用chrome浏览器。
350
+
351
+ 使用例子:
352
+ open_browser(browser_name="chrome")
353
+ open_browser(browser_name="firefox")
354
+ </usage>
355
+ </command>
356
+
357
+ <command>
358
+ <name>focus_app</name>
359
+ <description>
360
+ 查找并聚焦指定的应用程序窗口。
361
+ 这在需要确保某个应用程序处于活跃状态后才能进行后续操作时非常有用。
362
+ </description>
363
+ <usage>
364
+ 该方法需要一个app_name参数,表示要聚焦的应用程序名称或窗口标题的一部分。
365
+ 可选的retry_count参数表示重试次数,默认为3。
366
+
367
+ 使用例子:
368
+ focus_app(app_name="Chrome") # 聚焦Chrome浏览器
369
+ focus_app(app_name="记事本", retry_count=5) # 聚焦记事本,并增加重试次数
370
+
371
+ 返回值:
372
+ 布尔值,表示是否成功聚焦应用
373
+ </usage>
374
+ </command>
375
+ </commands>
376
+ '''
377
+
378
+ @byzerllm.prompt()
379
+ def analyze_task(self, request: AutoWebRequest) -> str:
380
+ """
381
+ 图片是当前屏幕截图。
382
+ {{ screenshot }}
383
+
384
+ 我是一个专业的网页自动化助手。我能帮助用户执行各种网页操作,包括点击按钮、输入文本、导航网页等。
385
+
386
+ 当前用户环境信息如下:
387
+ <os_info>
388
+ 操作系统: {{ env_info.os_name }} {{ env_info.os_version }}
389
+ 操作系统发行版: {{ os_distribution }}
390
+ Python版本: {{ env_info.python_version }}
391
+ 终端类型: {{ env_info.shell_type }}
392
+ 终端编码: {{ env_info.shell_encoding }}
393
+ 当前用户: {{ current_user }}
394
+
395
+ {%- if shell_type %}
396
+ 脚本类型:{{ shell_type }}
397
+ {%- endif %}
398
+
399
+ {%- if env_info.conda_env %}
400
+ Conda环境: {{ env_info.conda_env }}
401
+ {%- endif %}
402
+ {%- if env_info.virtualenv %}
403
+ 虚拟环境: {{ env_info.virtualenv }}
404
+ {%- endif %}
405
+ </os_info>
406
+
407
+ 你有如下函数可供使用:
408
+ {{ command_readme }}
409
+
410
+ {{ guide_readme }}
411
+
412
+ 用户请求:
413
+ {{ request.user_input }}
414
+
415
+ {% if request.context %}
416
+ 上下文信息:
417
+ {{ request.context }}
418
+ {% endif %}
419
+
420
+ 请我为用户制定一个详细的自动化操作计划,包括每一步需要执行的具体动作。
421
+
422
+ 对于每个操作,我需要提供:
423
+ 1. 要执行的动作类型
424
+ 2. 动作的参数(如坐标、文本内容等)
425
+ 3. 动作的目的描述
426
+ 4. 期望的结果
427
+ 5. 如果需要用户交互,请使用ask_user或response_user操作。
428
+
429
+ 我的回答必须以下面的JSON格式返回:
430
+ ```json
431
+ {
432
+ "explanation": "对整体任务的简要解释",
433
+ "actions": [
434
+ {
435
+ "action": "动作类型",
436
+ "parameters": {
437
+ "参数1": "值1",
438
+ "参数2": "值2"
439
+ },
440
+ "description": "这个动作的目的描述",
441
+ "expected_outcome": "执行此动作后预期看到的结果"
442
+ },
443
+ {
444
+ "action": "第二个动作",
445
+ ...
446
+ }
447
+ ],
448
+ "additional_info": "任何额外信息或建议",
449
+ "suggested_next_steps": ["完成当前任务后可能的后续步骤1", "后续步骤2"]
450
+ }
451
+ ```
452
+ """
453
+ env_info = detect_env()
454
+ shell_type = "bash"
455
+ if shells.is_running_in_cmd():
456
+ shell_type = "cmd"
457
+ elif shells.is_running_in_powershell():
458
+ shell_type = "powershell"
459
+
460
+ data = {
461
+ "command_readme": self._command_readme.prompt(),
462
+ "user_input": request.user_input,
463
+ "available_commands": self._command_readme.prompt(),
464
+ "env_info": env_info,
465
+ "shell_type": shell_type,
466
+ "shell_encoding": shells.get_terminal_encoding(),
467
+ "os_distribution": shells.get_os_distribution(),
468
+ "current_user": shells.get_current_username(),
469
+ "guide_readme": self._guide_readme.prompt()
470
+ }
471
+ if request.screenshot_path:
472
+ image = byzerllm.Image.load_image_from_path(
473
+ request.screenshot_path)
474
+ data["screenshot"] = image
475
+ return {"request": request, **data}
476
+
477
+ @byzerllm.prompt()
478
+ def verify_action_result(self) -> str:
479
+ """
480
+ 图片是当前屏幕截图。
481
+ {{ image }}
482
+
483
+ 你有如下函数可供使用:
484
+ {{ command_readme }}
485
+
486
+ {{ guide_readme }}
487
+
488
+ 用户请求:
489
+ {{ request.user_input }}
490
+
491
+
492
+ 当前的规划是:
493
+ ```json
494
+ {{ plan }}
495
+ ```
496
+
497
+ 执行历史:
498
+ {% for record in execution_history %}
499
+ 步骤 {{ record.step }}:
500
+ - 动作: {{ record.action.action }}
501
+ - 描述: {{ record.action.description or "无描述" }}
502
+ - 结果: {{ "成功" if record.result.success else "失败" }}
503
+ {% if not record.result.success and record.result.error %}
504
+ - 错误: {{ record.result.error }}
505
+ {% endif %}
506
+ {% if record.verification %}
507
+ - 验证: {{ "通过" if record.verification.success else "未通过" }}
508
+ - 原因: {{ record.verification.reason }}
509
+ {% endif %}
510
+
511
+ {% endfor %}
512
+
513
+ 你当前执行的最后一个操作为:
514
+ ```json
515
+ {{ action_json }}
516
+ ```
517
+
518
+ 该操作的期望结果:
519
+ {{ action.expected_outcome }}
520
+
521
+ 操作的实际执行结果:
522
+ ```json
523
+ {{ result_json }}
524
+ ```
525
+
526
+ 请根据前面的信息以及截图,判断当前操作是否达成了预期效果。
527
+
528
+ 返回以下JSON格式的验证结果:
529
+ ```json
530
+ {
531
+ "success": true或false, // 操作是否成功达成预期效果
532
+ "analysis": "详细分析当前屏幕和操作结果", // 分析当前屏幕和操作结果
533
+ "reason": "成功或失败的原因", // 操作成功或失败的原因
534
+ "suggestion": "如果失败,建议的下一步操作" // 如果操作失败,建议的下一步操作
535
+ }
536
+ ```
537
+
538
+ 如果你觉得需要调整自动执行计划,请将success设置为false,并提供详细的分析和建议,触发后续的执行计划的修改。
539
+ """
540
+ screenshot_path = self.current_action_result.screenshot_path
541
+ action = self.current_action
542
+ result = self.current_action_result.result
543
+ plan = self.current_plan.model_dump()
544
+ image = byzerllm.Image.load_image_from_path(screenshot_path)
545
+ return {
546
+ "action_json": json.dumps(action.model_dump(), ensure_ascii=False, indent=2),
547
+ "action": action,
548
+ "result_json": json.dumps(result, ensure_ascii=False, indent=2),
549
+ "image": image,
550
+ "plan": plan,
551
+ "execution_history": self.execution_history,
552
+ "request": self.init_request
553
+ }
554
+
555
+ @byzerllm.prompt()
556
+ def analyze_execution_result(self) -> str:
557
+ """
558
+ {{ screenshot }}
559
+
560
+ 图片是当前屏幕截图。
561
+
562
+ 当前用户环境信息如下:
563
+ <os_info>
564
+ 操作系统: {{ env_info.os_name }} {{ env_info.os_version }}
565
+ 操作系统发行版: {{ os_distribution }}
566
+ Python版本: {{ env_info.python_version }}
567
+ 终端类型: {{ env_info.shell_type }}
568
+ 终端编码: {{ env_info.shell_encoding }}
569
+ 当前用户: {{ current_user }}
570
+
571
+ {%- if shell_type %}
572
+ 脚本类型:{{ shell_type }}
573
+ {%- endif %}
574
+
575
+ {%- if env_info.conda_env %}
576
+ Conda环境: {{ env_info.conda_env }}
577
+ {%- endif %}
578
+ {%- if env_info.virtualenv %}
579
+ 虚拟环境: {{ env_info.virtualenv }}
580
+ {%- endif %}
581
+ </os_info>
582
+
583
+ 你有如下函数可供使用:
584
+ {{ command_readme }}
585
+
586
+ {{ guide_readme }}
587
+
588
+ 我需要分析当前的网页自动化执行情况并确定后续步骤。
589
+
590
+ 当前的自动化计划是:
591
+ ```json
592
+ {{ plan }}
593
+ ```
594
+
595
+ 执行历史:
596
+ {% for record in execution_history %}
597
+ 步骤 {{ record.step }}:
598
+ - 动作: {{ record.action.action }}
599
+ - 描述: {{ record.action.description or "无描述" }}
600
+ - 结果: {{ "成功" if record.result.success else "失败" }}
601
+ {% if not record.result.success and record.result.error %}
602
+ - 错误: {{ record.result.error }}
603
+ {% endif %}
604
+ {% if record.verification %}
605
+ - 验证: {{ "通过" if record.verification.success else "未通过" }}
606
+ - 原因: {{ record.verification.reason }}
607
+ {% endif %}
608
+
609
+ {% endfor %}
610
+
611
+ 原始任务:
612
+ {{ task.user_input }}
613
+
614
+ 请根据当前屏幕状态和执行历史,分析任务完成情况并确定下一步骤。如果任务已经完成,请明确说明。如果任务未完成,请提供新的操作计划。
615
+ 请以JSON格式返回结果:
616
+ ```json
617
+ {
618
+ "completed": true或false,
619
+ "current_status": "任务当前状态描述",
620
+ "analysis": "详细分析",
621
+ "actions": [
622
+ {
623
+ "action": "动作类型",
624
+ "parameters": {
625
+ "参数1": "值1",
626
+ "参数2": "值2"
627
+ },
628
+ "description": "这个动作的目的描述",
629
+ "expected_outcome": "执行此动作后预期看到的结果"
630
+ }
631
+ ]
632
+ }
633
+ ```
634
+ """
635
+ screenshot_path = self.computer.screenshot()
636
+ plan = self.current_plan.model_dump()
637
+ image = byzerllm.Image.load_image_from_path(screenshot_path)
638
+ data = {
639
+ "env_info": detect_env(),
640
+ "shell_type": "bash",
641
+ "shell_encoding": shells.get_terminal_encoding(),
642
+ "os_distribution": shells.get_os_distribution(),
643
+ "current_user": shells.get_current_username(),
644
+ "command_readme": self._command_readme.prompt(),
645
+ "guide_readme": self._guide_readme.prompt(),
646
+ "plan": plan
647
+ }
648
+ if screenshot_path:
649
+ image = byzerllm.Image.load_image_from_path(screenshot_path)
650
+ data["screenshot"] = image
651
+ return {"task": self.init_request, "execution_history": self.execution_history, **data}
652
+
653
+ def execute_action(self, action: WebAction) -> ActionResult:
654
+ """
655
+ 执行单个网页自动化操作
656
+
657
+ Args:
658
+ action: 要执行的操作
659
+
660
+ Returns:
661
+ 操作执行结果
662
+ """
663
+ self.printer.print_in_terminal(
664
+ "executing_web_action",
665
+ style="blue",
666
+ action=action.action,
667
+ description=action.description or ""
668
+ )
669
+
670
+ try:
671
+ # 构建工作流步骤
672
+ step = {
673
+ "action": action.action,
674
+ **action.parameters
675
+ }
676
+
677
+ # 执行步骤并获取结果
678
+ step_results = self.computer.run_workflow([step])
679
+
680
+ # 执行后截图
681
+ screenshot_path = self.computer.screenshot(
682
+ f"after_{action.action}_{int(time.time())}.png")
683
+
684
+ if step_results and len(step_results) > 0:
685
+ result = step_results[0]
686
+ return ActionResult(
687
+ success=result.get("success", True),
688
+ action=action,
689
+ screenshot_path=screenshot_path,
690
+ result=result
691
+ )
692
+ else:
693
+ return ActionResult(
694
+ success=False,
695
+ action=action,
696
+ screenshot_path=screenshot_path,
697
+ error="执行步骤返回空结果"
698
+ )
699
+
700
+ except Exception as e:
701
+ logger.error(f"执行操作 {action.action} 时出错: {str(e)}")
702
+ # 尝试截图记录错误状态
703
+ try:
704
+ screenshot_path = self.computer.screenshot(
705
+ f"error_{action.action}_{int(time.time())}.png")
706
+ except:
707
+ screenshot_path = None
708
+
709
+ return ActionResult(
710
+ success=False,
711
+ action=action,
712
+ screenshot_path=screenshot_path,
713
+ error=str(e)
714
+ )
715
+
716
+ def run_adaptive_flow(self, request: AutoWebRequest, max_iterations: int = 1000, debug: bool = False) -> AutoWebResponse:
717
+ """
718
+ 运行自适应的网页自动化流程
719
+
720
+ Args:
721
+ request: 自动化请求
722
+ max_iterations: 最大迭代次数
723
+ debug: 是否开启调试模式,设为True时每一步会要求用户确认
724
+
725
+ Returns:
726
+ 操作响应
727
+ """
728
+ console = Console()
729
+ self.printer.print_in_terminal("auto_web_analyzing", style="blue")
730
+
731
+ # 获取初始截图
732
+ if not request.screenshot_path:
733
+ screenshot_path = self.computer.screenshot()
734
+ request.screenshot_path = screenshot_path
735
+
736
+ # 记录执行历史
737
+ self.execution_history = []
738
+ self.init_request = request
739
+ # 添加时间统计
740
+ start_time = time.time()
741
+
742
+ # 使用LLM分析任务并生成操作计划
743
+ logger.info(f"开始分析任务: '{request.user_input}'")
744
+ console.print("正在分析任务,请稍候...", style="italic blue")
745
+ analysis = self.analyze_task.with_llm(self.llm).run(request)
746
+ logger.info(f"LLM分析任务结果: {analysis}")
747
+
748
+ # 打印LLM分析任务的耗时
749
+ analysis_time = time.time() - start_time
750
+ logger.info(f"任务分析完成,LLM耗时: {analysis_time:.2f}s")
751
+ console.print(f"任务分析完成,LLM耗时: {analysis_time:.2f}s", style="green")
752
+
753
+ try:
754
+ # 解析JSON结果
755
+ analysis_json = code_utils.extract_code(analysis)[-1][1]
756
+ plan_dict = json.loads(analysis_json)
757
+ logger.debug(
758
+ f"解析后的操作计划: {json.dumps(plan_dict, ensure_ascii=False, indent=2)}")
759
+
760
+ # 转换为AutoWebResponse对象
761
+ plan = AutoWebResponse(
762
+ explanation=plan_dict.get("explanation", ""),
763
+ actions=[WebAction.model_validate(
764
+ a) for a in plan_dict.get("actions", [])],
765
+ additional_info=plan_dict.get("additional_info", ""),
766
+ suggested_next_steps=plan_dict.get("suggested_next_steps", []),
767
+ overall_status="in_progress"
768
+ )
769
+ self.current_plan = plan
770
+
771
+ logger.info(f"生成的操作计划包含 {len(plan.actions)} 个步骤")
772
+ for i, action in enumerate(plan.actions):
773
+ logger.info(
774
+ f"步骤 {i+1}: {action.action} - {action.description}")
775
+
776
+ self.printer.print_in_terminal("auto_web_analyzed", style="green")
777
+ console.print(Panel(
778
+ Text(plan.explanation, style="italic"),
779
+ title="📋 自动化计划",
780
+ border_style="blue"
781
+ ))
782
+
783
+ except Exception as e:
784
+ logger.error(f"解析LLM响应失败: {str(e)}")
785
+ return AutoWebResponse(
786
+ explanation=f"无法解析LLM响应: {str(e)}",
787
+ overall_status="failed"
788
+ )
789
+
790
+ # 开始执行操作
791
+ iterations = 0
792
+ while iterations < max_iterations:
793
+ iterations += 1
794
+ logger.info(f"开始执行迭代 {iterations}/{max_iterations}")
795
+
796
+ # 检查是否需要取消操作
797
+ if global_cancel.requested:
798
+ logger.info("检测到取消请求,停止操作")
799
+ self.printer.print_in_terminal(
800
+ "operation_cancelled", style="yellow")
801
+ return AutoWebResponse(
802
+ explanation="操作已取消",
803
+ overall_status="cancelled",
804
+ actions=[]
805
+ )
806
+
807
+ # 如果没有更多操作,认为任务完成
808
+ if not plan.actions:
809
+ logger.info("没有更多操作,任务完成")
810
+ console.print(Panel(
811
+ Text(plan.explanation or "任务完成", style="green"),
812
+ title="✅ 完成",
813
+ border_style="green"
814
+ ))
815
+ plan.overall_status = "completed"
816
+ return plan
817
+
818
+ # 执行当前计划中的第一个操作
819
+ action = plan.actions[0]
820
+ logger.info(f"准备执行动作: {action.action}")
821
+ logger.info(f"动作描述: {action.description}")
822
+ logger.info(
823
+ f"动作参数: {json.dumps(action.parameters, ensure_ascii=False)}")
824
+
825
+ self.printer.print_in_terminal(
826
+ "executing_step",
827
+ style="blue",
828
+ step=iterations,
829
+ description=action.description or action.action
830
+ )
831
+
832
+ # 调试模式:如果开启调试,在每一步询问用户是否继续
833
+ if debug:
834
+ question = f"是否执行步骤 {iterations}: {action.action} - {action.description}? (yes/no/quit)"
835
+ answer = self.computer.ask_user(question)
836
+
837
+ if answer.lower() == "quit":
838
+ logger.info("用户选择退出调试模式")
839
+ return AutoWebResponse(
840
+ explanation="用户在调试模式中选择退出",
841
+ overall_status="cancelled",
842
+ actions=[]
843
+ )
844
+ elif answer.lower() != "yes":
845
+ # 用户选择跳过当前步骤
846
+ logger.info(f"用户选择跳过步骤: {action.action}")
847
+ plan.actions = plan.actions[1:]
848
+ continue
849
+
850
+ logger.info("用户确认执行当前步骤")
851
+
852
+ # 执行操作
853
+ logger.info(f"开始执行动作: {action.action}")
854
+ action_start = time.time()
855
+ self.current_action = action
856
+ action_result = self.execute_action(action)
857
+ self.current_action_result = action_result
858
+ action_time = time.time() - action_start
859
+ logger.info(f"动作执行完成,耗时: {action_time:.2f}s")
860
+ logger.info(f"执行结果: {'成功' if action_result.success else '失败'}")
861
+
862
+ if action_result.error:
863
+ logger.error(f"执行错误: {action_result.error}")
864
+
865
+ # 验证结果
866
+ if action_result.result.get("should_verify", True):
867
+ logger.info("开始验证执行结果")
868
+ verification_start = time.time()
869
+ verification_result = self.verify_action_result.with_llm(self.llm).run()
870
+ verification_time = time.time() - verification_start
871
+ logger.info(f"结果验证完成,LLM耗时: {verification_time:.2f}s")
872
+ logger.debug(f"验证结果: {verification_result}")
873
+
874
+ console.print(
875
+ f"结果验证完成,LLM耗时: {verification_time:.2f}s", style="cyan")
876
+
877
+ try:
878
+ verification_json = code_utils.extract_code(
879
+ verification_result)[-1][1]
880
+ verification = json.loads(verification_json)
881
+ logger.info(
882
+ f"验证结果: {'成功' if verification.get('success', False) else '失败'}")
883
+ if 'reason' in verification:
884
+ logger.info(f"验证理由: {verification['reason']}")
885
+ except Exception as e:
886
+ logger.error(f"解析验证结果失败: {str(e)}")
887
+ verification = {"success": False,
888
+ "reason": f"验证结果解析失败: {str(e)}"}
889
+ else:
890
+ verification = {"success": True, "reason": "验证成功"}
891
+
892
+ # 记录执行历史
893
+ execution_record = {
894
+ "step": iterations,
895
+ "action": action.model_dump(),
896
+ "result": action_result.model_dump(exclude={"action"}),
897
+ "verification": verification
898
+ }
899
+ self.execution_history.append(execution_record)
900
+ logger.debug(f"已添加执行记录 #{iterations}")
901
+
902
+ # 如果验证失败,需要重新规划
903
+ if not verification.get("success", False):
904
+ logger.info(f"验证失败: {verification.get('reason', '未知原因')}")
905
+ self.printer.print_in_terminal(
906
+ "action_verification_failed",
907
+ style="yellow",
908
+ action=action.action,
909
+ reason=verification.get("reason", "未知原因")
910
+ )
911
+
912
+ # 基于执行历史和当前状态进行分析
913
+ logger.info("开始重新规划")
914
+ analysis_start = time.time()
915
+ analysis_result = self.analyze_execution_result.with_llm(self.llm).run()
916
+ analysis_time = time.time() - analysis_start
917
+ logger.info(f"重新规划完成,LLM耗时: {analysis_time:.2f}s")
918
+ logger.debug(f"重新规划结果: {analysis_result}")
919
+
920
+ console.print(
921
+ f"重新规划完成,LLM耗时: {analysis_time:.2f}s", style="magenta")
922
+
923
+ try:
924
+ # 解析分析结果
925
+ analysis_json = code_utils.extract_code(
926
+ analysis_result)[-1][1]
927
+ new_plan = json.loads(analysis_json)
928
+ logger.debug(
929
+ f"新计划: {json.dumps(new_plan, ensure_ascii=False, indent=2)}")
930
+
931
+ # 更新计划
932
+ if new_plan.get("completed", False):
933
+ # 任务已完成
934
+ logger.info("分析结果: 任务已完成")
935
+ console.print(Panel(
936
+ Text(new_plan.get("analysis", "任务已完成"), style="green"),
937
+ title="✅ 完成",
938
+ border_style="green"
939
+ ))
940
+ return AutoWebResponse(
941
+ explanation=new_plan.get("analysis", "任务已完成"),
942
+ overall_status="completed",
943
+ actions=[]
944
+ )
945
+ else:
946
+ # 继续执行新计划
947
+ logger.info("更新操作计划")
948
+ plan = AutoWebResponse(
949
+ actions=[WebAction.model_validate(
950
+ a) for a in new_plan.get("actions", [])],
951
+ explanation=new_plan.get("explanation", ""),
952
+ additional_info=new_plan.get("analysis", ""),
953
+ overall_status=new_plan.get(
954
+ "current_status", "in_progress")
955
+ )
956
+
957
+ logger.info(f"新计划包含 {len(plan.actions)} 个步骤")
958
+ for i, action in enumerate(plan.actions):
959
+ logger.info(
960
+ f"新步骤 {i+1}: {action.action} - {action.description}")
961
+
962
+ self.printer.print_in_terminal(
963
+ "replanned_actions",
964
+ style="blue",
965
+ count=len(plan.actions)
966
+ )
967
+
968
+ except Exception as e:
969
+ logger.error(f"解析分析结果时出错: {str(e)}")
970
+ # 如果无法解析,默认继续执行下一个操作
971
+ logger.info("无法解析新计划,默认移除当前操作并继续")
972
+ plan.actions = plan.actions[1:]
973
+ else:
974
+ # 验证成功,移除已执行的操作
975
+ logger.info("验证成功,继续执行下一步")
976
+ plan.actions = plan.actions[1:]
977
+ self.printer.print_in_terminal(
978
+ "action_succeeded",
979
+ style="green",
980
+ action=action.action
981
+ )
982
+
983
+ # 调试模式:添加手动暂停
984
+ if debug:
985
+ self.computer.response_user(f"完成步骤 {iterations},按Enter继续...")
986
+ input()
987
+
988
+ # 达到最大迭代次数
989
+ logger.warning(f"达到最大迭代次数 ({max_iterations}),未能完成任务")
990
+ self.printer.print_in_terminal(
991
+ "max_iterations_reached",
992
+ style="yellow",
993
+ max_iterations=max_iterations
994
+ )
995
+
996
+ return AutoWebResponse(
997
+ explanation=f"达到最大迭代次数 ({max_iterations}),未能完成任务",
998
+ overall_status="max_iterations_reached",
999
+ actions=[]
1000
+ )
1001
+
1002
+ def save_to_memory_file(self, query: str, response: str):
1003
+ """保存对话到记忆文件"""
1004
+ memory_dir = os.path.join(".auto-coder", "memory")
1005
+ os.makedirs(memory_dir, exist_ok=True)
1006
+ file_path = os.path.join(memory_dir, "web_automation_history.json")
1007
+
1008
+ # 创建新的消息对象
1009
+ timestamp = str(int(time.time()))
1010
+
1011
+ # 加载现有对话或创建新的
1012
+ if os.path.exists(file_path):
1013
+ with open(file_path, "r", encoding="utf-8") as f:
1014
+ try:
1015
+ existing_conv = json.load(f)
1016
+ except Exception:
1017
+ existing_conv = {"history": {}, "conversations": []}
1018
+ else:
1019
+ existing_conv = {"history": {}, "conversations": []}
1020
+
1021
+ # 添加新记录
1022
+ existing_conv["conversations"].append({
1023
+ "user_message": query,
1024
+ "system_response": response,
1025
+ "timestamp": timestamp
1026
+ })
1027
+
1028
+ # 保存更新的对话
1029
+ with open(file_path, "w", encoding="utf-8") as f:
1030
+ json.dump(existing_conv, f, ensure_ascii=False, indent=2)
1031
+
1032
+
1033
+ def auto_web(llm: Union[byzerllm.ByzerLLM, byzerllm.SimpleByzerLLM], user_input: str,
1034
+ screenshot_path: Optional[str] = None,
1035
+ context: Optional[str] = None,
1036
+ args: Optional[AutoCoderArgs] = None, debug: bool = False):
1037
+ """
1038
+ 执行网页自动化操作的入口函数
1039
+
1040
+ Args:
1041
+ llm: ByzerLLM实例,用于分析和生成操作
1042
+ user_input: 用户输入的指令
1043
+ screenshot_path: 可选的截图路径
1044
+ context: 可选的上下文信息
1045
+ args: 可选的配置参数
1046
+ debug: 是否开启调试模式,设为True时每一步会要求用户确认
1047
+ """
1048
+
1049
+ # 创建请求
1050
+ request = AutoWebRequest(
1051
+ user_input=user_input,
1052
+ context=context,
1053
+ screenshot_path=screenshot_path
1054
+ )
1055
+
1056
+ # 初始化自动化工具
1057
+ tuner = AutoWebTuner(llm=llm, args=args)
1058
+
1059
+ # 执行自适应的自动化流程
1060
+ response = tuner.run_adaptive_flow(request, debug=debug)
1061
+
1062
+ return response