screenforge 0.4.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.
- cli/__init__.py +0 -0
- cli/_version.py +1 -0
- cli/dispatch.py +266 -0
- cli/doctor.py +487 -0
- cli/modes/__init__.py +0 -0
- cli/modes/action.py +262 -0
- cli/modes/default.py +248 -0
- cli/modes/demo.py +162 -0
- cli/modes/dry_run.py +237 -0
- cli/modes/init.py +133 -0
- cli/modes/plan.py +148 -0
- cli/modes/workflow.py +354 -0
- cli/parser.py +305 -0
- cli/reporter.py +207 -0
- cli/session.py +146 -0
- cli/shared.py +427 -0
- cli/shorthand.py +90 -0
- cli/tool_protocol_handlers.py +446 -0
- common/__init__.py +0 -0
- common/adapters/__init__.py +21 -0
- common/adapters/android_adapter.py +273 -0
- common/adapters/base_adapter.py +24 -0
- common/adapters/ios_adapter.py +278 -0
- common/adapters/web_adapter.py +271 -0
- common/ai.py +277 -0
- common/ai_autonomous.py +273 -0
- common/ai_heal.py +222 -0
- common/cache/__init__.py +15 -0
- common/cache/cache_hash.py +57 -0
- common/cache/cache_manager.py +300 -0
- common/cache/cache_stats.py +133 -0
- common/cache/cache_storage.py +79 -0
- common/cache/embedding_loader.py +150 -0
- common/capabilities.py +121 -0
- common/case_memory.py +327 -0
- common/error_codes.py +61 -0
- common/exceptions.py +18 -0
- common/executor.py +1504 -0
- common/failure_diagnosis.py +138 -0
- common/history_manager.py +75 -0
- common/logs.py +168 -0
- common/mcp_server.py +467 -0
- common/preflight.py +496 -0
- common/progress.py +37 -0
- common/run_reporter.py +415 -0
- common/run_resume.py +149 -0
- common/runtime_modes.py +35 -0
- common/tool_protocol.py +196 -0
- common/visual_fallback.py +71 -0
- common/workflow_schema.py +150 -0
- config/__init__.py +0 -0
- config/config.py +167 -0
- config/env_loader.py +76 -0
- screenforge-0.4.0.dist-info/METADATA +43 -0
- screenforge-0.4.0.dist-info/RECORD +64 -0
- screenforge-0.4.0.dist-info/WHEEL +5 -0
- screenforge-0.4.0.dist-info/entry_points.txt +2 -0
- screenforge-0.4.0.dist-info/licenses/LICENSE +21 -0
- screenforge-0.4.0.dist-info/top_level.txt +4 -0
- utils/__init__.py +0 -0
- utils/screenshot_annotator.py +60 -0
- utils/utils_ios.py +195 -0
- utils/utils_web.py +304 -0
- utils/utils_xml.py +218 -0
cli/modes/dry_run.py
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""Dry-run execution modes."""
|
|
2
|
+
|
|
3
|
+
import cli.shared as _shared
|
|
4
|
+
from cli.reporter import (
|
|
5
|
+
_apply_resume_summary,
|
|
6
|
+
_build_action_summary,
|
|
7
|
+
_build_inline_action_data,
|
|
8
|
+
_build_reporter,
|
|
9
|
+
_emit_run_started,
|
|
10
|
+
)
|
|
11
|
+
from cli.shared import (
|
|
12
|
+
_capture_ui_state,
|
|
13
|
+
_connect_adapter,
|
|
14
|
+
_ensure_executor_runtime,
|
|
15
|
+
_ensure_runtime_classes,
|
|
16
|
+
log,
|
|
17
|
+
)
|
|
18
|
+
from common.runtime_modes import MODE_DRY_RUN
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _preview_action_resolution(device, platform: str, action_data: dict) -> dict:
|
|
22
|
+
_ensure_executor_runtime()
|
|
23
|
+
l_type = action_data.get("locator_type", "")
|
|
24
|
+
l_value = action_data.get("locator_value", "")
|
|
25
|
+
if not l_type or str(l_type).lower() == "global" or str(l_value).lower() == "global":
|
|
26
|
+
return {"resolvable": True, "resolution_error": ""}
|
|
27
|
+
|
|
28
|
+
u2_locator_map = {
|
|
29
|
+
"resourceId": "resourceId",
|
|
30
|
+
"text": "text",
|
|
31
|
+
"description": "description",
|
|
32
|
+
"id": "resourceId",
|
|
33
|
+
}
|
|
34
|
+
u2_key = u2_locator_map.get(l_type, l_type)
|
|
35
|
+
try:
|
|
36
|
+
element = _shared.get_actual_element(device, platform, u2_key, l_value)
|
|
37
|
+
return {"resolvable": element is not None, "resolution_error": ""}
|
|
38
|
+
except Exception as e:
|
|
39
|
+
return {"resolvable": False, "resolution_error": str(e)}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _build_resolution_hint(args, action_data: dict, resolution: dict) -> str:
|
|
43
|
+
if resolution.get("resolvable", False):
|
|
44
|
+
return ""
|
|
45
|
+
|
|
46
|
+
locator_type = action_data.get("locator_type", "")
|
|
47
|
+
if not args.vision and str(locator_type).lower() != "global":
|
|
48
|
+
return "Locator resolution failed. Verify current page state; consider enabling --vision."
|
|
49
|
+
return "Locator resolution failed. Verify page structure and whether the target element exists."
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def run_dry_run_mode(
|
|
53
|
+
args,
|
|
54
|
+
output_script_path: str,
|
|
55
|
+
context_content: str,
|
|
56
|
+
resume_context: dict,
|
|
57
|
+
) -> int:
|
|
58
|
+
reporter = _build_reporter(args, output_script_path, MODE_DRY_RUN)
|
|
59
|
+
final_status = "failed"
|
|
60
|
+
exit_code = 1
|
|
61
|
+
final_error = ""
|
|
62
|
+
adapter = None
|
|
63
|
+
_emit_run_started(reporter, args, output_script_path, MODE_DRY_RUN)
|
|
64
|
+
_apply_resume_summary(reporter, resume_context)
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
adapter = _connect_adapter(args, reporter)
|
|
68
|
+
ui_json, screenshot_base64 = _capture_ui_state(args, adapter, reporter, 1)
|
|
69
|
+
_ensure_runtime_classes()
|
|
70
|
+
brain = _shared.AutonomousBrain()
|
|
71
|
+
decision_data = brain.get_next_autonomous_action(
|
|
72
|
+
goal=args.goal,
|
|
73
|
+
context=context_content,
|
|
74
|
+
ui_json=ui_json,
|
|
75
|
+
history=[],
|
|
76
|
+
platform=args.platform,
|
|
77
|
+
last_error="",
|
|
78
|
+
screenshot_base64=screenshot_base64,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
status = decision_data.get("status", "failed")
|
|
82
|
+
action_data = decision_data.get("result", {})
|
|
83
|
+
resolution = _preview_action_resolution(
|
|
84
|
+
adapter.driver, args.platform, action_data
|
|
85
|
+
)
|
|
86
|
+
resolution_hint = _build_resolution_hint(args, action_data, resolution)
|
|
87
|
+
reporter.emit_event(
|
|
88
|
+
"dry_run_preview",
|
|
89
|
+
status=status,
|
|
90
|
+
action=action_data.get("action", ""),
|
|
91
|
+
locator_type=action_data.get("locator_type", ""),
|
|
92
|
+
locator_value=action_data.get("locator_value", ""),
|
|
93
|
+
extra_value=action_data.get("extra_value", ""),
|
|
94
|
+
resolvable=resolution.get("resolvable", False),
|
|
95
|
+
resolution_error=resolution.get("resolution_error", ""),
|
|
96
|
+
resolution_hint=resolution_hint,
|
|
97
|
+
)
|
|
98
|
+
reporter.update_summary(
|
|
99
|
+
dry_run_preview={
|
|
100
|
+
"status": status,
|
|
101
|
+
"action": action_data.get("action", ""),
|
|
102
|
+
"locator_type": action_data.get("locator_type", ""),
|
|
103
|
+
"locator_value": action_data.get("locator_value", ""),
|
|
104
|
+
"extra_value": action_data.get("extra_value", ""),
|
|
105
|
+
"resolvable": resolution.get("resolvable", False),
|
|
106
|
+
"resolution_error": resolution.get("resolution_error", ""),
|
|
107
|
+
"resolution_hint": resolution_hint,
|
|
108
|
+
}
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
if status == "failed":
|
|
112
|
+
final_error = "Task cannot continue — AI determined failure"
|
|
113
|
+
log.warning("⚠️ [Dry Run] AI determined task cannot continue.")
|
|
114
|
+
exit_code = 1
|
|
115
|
+
else:
|
|
116
|
+
log.info(
|
|
117
|
+
f"🧪 [Dry Run] would_execute: {action_data.get('action', '')} "
|
|
118
|
+
f"{action_data.get('locator_type', '')}={action_data.get('locator_value', '')}"
|
|
119
|
+
)
|
|
120
|
+
final_status = "success"
|
|
121
|
+
exit_code = 0
|
|
122
|
+
except Exception as e:
|
|
123
|
+
final_error = str(e)
|
|
124
|
+
reporter.emit_event("dry_run_failed", error=str(e))
|
|
125
|
+
log.error(f"❌ [Dry Run] Simulation failed: {e}")
|
|
126
|
+
finally:
|
|
127
|
+
reporter.finalize(
|
|
128
|
+
status=final_status,
|
|
129
|
+
exit_code=exit_code,
|
|
130
|
+
steps_executed=1 if not final_error else 0,
|
|
131
|
+
last_error=final_error,
|
|
132
|
+
)
|
|
133
|
+
if adapter:
|
|
134
|
+
try:
|
|
135
|
+
adapter.teardown()
|
|
136
|
+
except Exception as e:
|
|
137
|
+
log.warning(f"⚠️ [Warning] Cleanup failed: {e}")
|
|
138
|
+
return exit_code
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def run_action_dry_run_mode(
|
|
142
|
+
args,
|
|
143
|
+
output_script_path: str,
|
|
144
|
+
resume_context: dict,
|
|
145
|
+
) -> int:
|
|
146
|
+
reporter = _build_reporter(args, output_script_path, MODE_DRY_RUN)
|
|
147
|
+
final_status = "failed"
|
|
148
|
+
exit_code = 1
|
|
149
|
+
final_error = ""
|
|
150
|
+
adapter = None
|
|
151
|
+
preview_steps = []
|
|
152
|
+
_emit_run_started(reporter, args, output_script_path, MODE_DRY_RUN)
|
|
153
|
+
_apply_resume_summary(reporter, resume_context)
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
action_data = _build_inline_action_data(args)
|
|
157
|
+
action_summary = _build_action_summary(args, action_data)
|
|
158
|
+
reporter.update_control_summary(
|
|
159
|
+
control_kind="action",
|
|
160
|
+
control_label=action_summary["action_name"],
|
|
161
|
+
source_ref="inline://action",
|
|
162
|
+
action=action_summary["action"],
|
|
163
|
+
locator_type=action_summary["locator_type"],
|
|
164
|
+
locator_value=action_summary["locator_value"],
|
|
165
|
+
extra_value=action_summary["extra_value"],
|
|
166
|
+
)
|
|
167
|
+
reporter.emit_event(
|
|
168
|
+
"action_loaded",
|
|
169
|
+
action_name=action_summary["action_name"],
|
|
170
|
+
action=action_summary["action"],
|
|
171
|
+
locator_type=action_summary["locator_type"],
|
|
172
|
+
locator_value=action_summary["locator_value"],
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
adapter = _connect_adapter(args, reporter)
|
|
176
|
+
resolution = _preview_action_resolution(
|
|
177
|
+
adapter.driver, args.platform, action_data
|
|
178
|
+
)
|
|
179
|
+
resolution_hint = _build_resolution_hint(args, action_data, resolution)
|
|
180
|
+
preview = {
|
|
181
|
+
"step": 1,
|
|
182
|
+
"name": action_data["name"],
|
|
183
|
+
"action": action_data["action"],
|
|
184
|
+
"locator_type": action_data["locator_type"],
|
|
185
|
+
"locator_value": action_data["locator_value"],
|
|
186
|
+
"extra_value": action_data["extra_value"],
|
|
187
|
+
"resolvable": resolution.get("resolvable", False),
|
|
188
|
+
"resolution_error": resolution.get("resolution_error", ""),
|
|
189
|
+
"resolution_hint": resolution_hint,
|
|
190
|
+
}
|
|
191
|
+
preview_steps.append(preview)
|
|
192
|
+
reporter.emit_event("action_step_preview", **preview)
|
|
193
|
+
|
|
194
|
+
reporter.update_summary(
|
|
195
|
+
action_summary=_build_action_summary(
|
|
196
|
+
args,
|
|
197
|
+
action_data,
|
|
198
|
+
resolvable=preview["resolvable"],
|
|
199
|
+
resolution_error=preview["resolution_error"],
|
|
200
|
+
resolution_hint=preview["resolution_hint"],
|
|
201
|
+
),
|
|
202
|
+
dry_run_preview={
|
|
203
|
+
"workflow": False,
|
|
204
|
+
"step_count": 1,
|
|
205
|
+
"unresolved_steps": 0 if preview["resolvable"] else 1,
|
|
206
|
+
"preview_steps": preview_steps,
|
|
207
|
+
},
|
|
208
|
+
)
|
|
209
|
+
reporter.update_control_summary(
|
|
210
|
+
resolvable=preview["resolvable"],
|
|
211
|
+
resolution_error=preview["resolution_error"],
|
|
212
|
+
resolution_hint=preview["resolution_hint"],
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
if preview["resolvable"]:
|
|
216
|
+
final_status = "success"
|
|
217
|
+
exit_code = 0
|
|
218
|
+
else:
|
|
219
|
+
final_error = "Action locator cannot be resolved"
|
|
220
|
+
exit_code = 1
|
|
221
|
+
except Exception as e:
|
|
222
|
+
final_error = str(e)
|
|
223
|
+
reporter.emit_event("action_dry_run_failed", error=str(e))
|
|
224
|
+
log.error(f"❌ [Action] Dry-run failed: {e}")
|
|
225
|
+
finally:
|
|
226
|
+
reporter.finalize(
|
|
227
|
+
status=final_status,
|
|
228
|
+
exit_code=exit_code,
|
|
229
|
+
steps_executed=len(preview_steps),
|
|
230
|
+
last_error=final_error,
|
|
231
|
+
)
|
|
232
|
+
if adapter:
|
|
233
|
+
try:
|
|
234
|
+
adapter.teardown()
|
|
235
|
+
except Exception as e:
|
|
236
|
+
log.warning(f"⚠️ [Warning] Cleanup failed: {e}")
|
|
237
|
+
return exit_code
|
cli/modes/init.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Interactive init wizard: guides new users through first-time setup."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _prompt_choice(question: str, options: list[str], default: int = 0) -> int:
|
|
7
|
+
print(f"\n {question}")
|
|
8
|
+
for i, opt in enumerate(options):
|
|
9
|
+
marker = ">" if i == default else " "
|
|
10
|
+
print(f" {marker} [{i + 1}] {opt}")
|
|
11
|
+
while True:
|
|
12
|
+
raw = input(f" Choice [{default + 1}]: ").strip()
|
|
13
|
+
if not raw:
|
|
14
|
+
return default
|
|
15
|
+
try:
|
|
16
|
+
choice = int(raw) - 1
|
|
17
|
+
if 0 <= choice < len(options):
|
|
18
|
+
return choice
|
|
19
|
+
except ValueError:
|
|
20
|
+
pass
|
|
21
|
+
print(f" Please enter 1-{len(options)}")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _prompt_input(question: str, default: str = "") -> str:
|
|
25
|
+
suffix = f" [{default}]" if default else ""
|
|
26
|
+
raw = input(f" {question}{suffix}: ").strip()
|
|
27
|
+
return raw or default
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def run_init_mode() -> int:
|
|
31
|
+
print("\n screenforge init — First-time setup wizard")
|
|
32
|
+
print(" " + "=" * 44)
|
|
33
|
+
|
|
34
|
+
# Step 1: Platform
|
|
35
|
+
platform_idx = _prompt_choice(
|
|
36
|
+
"Which platform will you test?",
|
|
37
|
+
["Web (Chrome/Playwright)", "Android (uiautomator2)", "iOS (WebDriverAgent)"],
|
|
38
|
+
default=0,
|
|
39
|
+
)
|
|
40
|
+
platform = ["web", "android", "ios"][platform_idx]
|
|
41
|
+
|
|
42
|
+
# Step 2: LLM config
|
|
43
|
+
print("\n ScreenForge needs an LLM API key for AI-driven testing.")
|
|
44
|
+
print(" (Skip this for --demo mode which requires no key)")
|
|
45
|
+
|
|
46
|
+
api_key = _prompt_input("OPENAI_API_KEY (or compatible)", "sk-...")
|
|
47
|
+
base_url = _prompt_input("OPENAI_BASE_URL", "https://api.openai.com/v1")
|
|
48
|
+
model = _prompt_input("MODEL_NAME", "gpt-4o")
|
|
49
|
+
|
|
50
|
+
# Step 3: Write .env
|
|
51
|
+
env_path = Path(".env")
|
|
52
|
+
if env_path.exists():
|
|
53
|
+
overwrite = _prompt_choice(
|
|
54
|
+
".env already exists. Overwrite?",
|
|
55
|
+
["No, keep existing", "Yes, overwrite"],
|
|
56
|
+
default=0,
|
|
57
|
+
)
|
|
58
|
+
if overwrite == 0:
|
|
59
|
+
print(" Keeping existing .env")
|
|
60
|
+
else:
|
|
61
|
+
_write_env(env_path, api_key, base_url, model)
|
|
62
|
+
else:
|
|
63
|
+
_write_env(env_path, api_key, base_url, model)
|
|
64
|
+
|
|
65
|
+
# Step 4: Platform-specific checks
|
|
66
|
+
print(f"\n Platform: {platform}")
|
|
67
|
+
if platform == "web":
|
|
68
|
+
_check_web_deps()
|
|
69
|
+
elif platform == "android":
|
|
70
|
+
_check_android_deps()
|
|
71
|
+
elif platform == "ios":
|
|
72
|
+
_check_ios_deps()
|
|
73
|
+
|
|
74
|
+
# Step 5: Suggest next steps
|
|
75
|
+
print("\n Setup complete! Next steps:")
|
|
76
|
+
print(" " + "-" * 30)
|
|
77
|
+
print(" 1. screenforge --demo # See it work (no API key needed)")
|
|
78
|
+
print(f" 2. screenforge --doctor --platform {platform} # Verify environment")
|
|
79
|
+
print(f" 3. screenforge --action goto --platform {platform} --extra-value \"https://example.com\"")
|
|
80
|
+
print()
|
|
81
|
+
|
|
82
|
+
return 0
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _write_env(path: Path, api_key: str, base_url: str, model: str) -> None:
|
|
86
|
+
content = f"""OPENAI_API_KEY = "{api_key}"
|
|
87
|
+
OPENAI_BASE_URL = "{base_url}"
|
|
88
|
+
MODEL_NAME = "{model}"
|
|
89
|
+
"""
|
|
90
|
+
path.write_text(content)
|
|
91
|
+
print(f" Written: {path}")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _check_web_deps() -> None:
|
|
95
|
+
try:
|
|
96
|
+
import playwright # noqa: F401
|
|
97
|
+
print(" [ok] playwright installed")
|
|
98
|
+
except ImportError:
|
|
99
|
+
print(" [!!] playwright not installed")
|
|
100
|
+
print(" Fix: pip install playwright && playwright install chromium")
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
from shutil import which
|
|
104
|
+
if which("chromium") or which("google-chrome") or which("chrome"):
|
|
105
|
+
print(" [ok] Chrome/Chromium found")
|
|
106
|
+
else:
|
|
107
|
+
print(" [ok] Will use Playwright's bundled Chromium")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _check_android_deps() -> None:
|
|
111
|
+
from shutil import which
|
|
112
|
+
|
|
113
|
+
if which("adb"):
|
|
114
|
+
print(" [ok] adb found")
|
|
115
|
+
else:
|
|
116
|
+
print(" [!!] adb not found in PATH")
|
|
117
|
+
print(" Fix: install Android SDK platform-tools")
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
import uiautomator2 # noqa: F401
|
|
121
|
+
print(" [ok] uiautomator2 installed")
|
|
122
|
+
except ImportError:
|
|
123
|
+
print(" [!!] uiautomator2 not installed")
|
|
124
|
+
print(" Fix: pip install screenforge[android]")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _check_ios_deps() -> None:
|
|
128
|
+
try:
|
|
129
|
+
import wda # noqa: F401
|
|
130
|
+
print(" [ok] facebook-wda installed")
|
|
131
|
+
except ImportError:
|
|
132
|
+
print(" [!!] facebook-wda not installed")
|
|
133
|
+
print(" Fix: pip install screenforge[ios]")
|
cli/modes/plan.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Plan-only execution modes."""
|
|
2
|
+
|
|
3
|
+
import cli.shared as _shared
|
|
4
|
+
from cli.reporter import (
|
|
5
|
+
_apply_resume_summary,
|
|
6
|
+
_build_action_summary,
|
|
7
|
+
_build_inline_action_data,
|
|
8
|
+
_build_reporter,
|
|
9
|
+
_emit_run_started,
|
|
10
|
+
)
|
|
11
|
+
from cli.shared import (
|
|
12
|
+
_capture_ui_state,
|
|
13
|
+
_connect_adapter,
|
|
14
|
+
_ensure_runtime_classes,
|
|
15
|
+
log,
|
|
16
|
+
)
|
|
17
|
+
from common.runtime_modes import MODE_PLAN_ONLY
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def run_plan_only_mode(
|
|
21
|
+
args,
|
|
22
|
+
output_script_path: str,
|
|
23
|
+
context_content: str,
|
|
24
|
+
resume_context: dict,
|
|
25
|
+
) -> int:
|
|
26
|
+
reporter = _build_reporter(args, output_script_path, MODE_PLAN_ONLY)
|
|
27
|
+
final_status = "failed"
|
|
28
|
+
exit_code = 1
|
|
29
|
+
final_error = ""
|
|
30
|
+
adapter = None
|
|
31
|
+
steps_executed = 0
|
|
32
|
+
_emit_run_started(reporter, args, output_script_path, MODE_PLAN_ONLY)
|
|
33
|
+
_apply_resume_summary(reporter, resume_context)
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
adapter = _connect_adapter(args, reporter)
|
|
37
|
+
ui_json, screenshot_base64 = _capture_ui_state(args, adapter, reporter, 1)
|
|
38
|
+
_ensure_runtime_classes()
|
|
39
|
+
brain = _shared.AutonomousBrain()
|
|
40
|
+
plan = brain.get_execution_plan(
|
|
41
|
+
goal=args.goal,
|
|
42
|
+
context=context_content,
|
|
43
|
+
ui_json=ui_json,
|
|
44
|
+
history=[],
|
|
45
|
+
platform=args.platform,
|
|
46
|
+
screenshot_base64=screenshot_base64,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
planned_steps = plan.get("planned_steps", [])
|
|
50
|
+
steps_executed = len(planned_steps) or 1
|
|
51
|
+
reporter.emit_event(
|
|
52
|
+
"plan_generated",
|
|
53
|
+
current_state_summary=plan.get("current_state_summary", ""),
|
|
54
|
+
planned_steps=planned_steps,
|
|
55
|
+
suggested_assertion=plan.get("suggested_assertion", ""),
|
|
56
|
+
risks=plan.get("risks", []),
|
|
57
|
+
)
|
|
58
|
+
reporter.update_summary(plan_preview=plan)
|
|
59
|
+
log.info(f"🧭 [Plan] Current page summary: {plan.get('current_state_summary', '')}")
|
|
60
|
+
for index, step in enumerate(planned_steps, start=1):
|
|
61
|
+
log.info(f"🧭 [Plan] Step {index}: {step}")
|
|
62
|
+
if plan.get("suggested_assertion"):
|
|
63
|
+
log.info(f"🧭 [Plan] Suggested assertion: {plan.get('suggested_assertion', '')}")
|
|
64
|
+
|
|
65
|
+
final_status = "success"
|
|
66
|
+
exit_code = 0
|
|
67
|
+
except Exception as e:
|
|
68
|
+
final_error = str(e)
|
|
69
|
+
reporter.emit_event("plan_failed", error=str(e))
|
|
70
|
+
log.error(f"❌ [Plan] Plan generation failed: {e}")
|
|
71
|
+
finally:
|
|
72
|
+
reporter.finalize(
|
|
73
|
+
status=final_status,
|
|
74
|
+
exit_code=exit_code,
|
|
75
|
+
steps_executed=steps_executed,
|
|
76
|
+
last_error=final_error,
|
|
77
|
+
)
|
|
78
|
+
if adapter:
|
|
79
|
+
try:
|
|
80
|
+
adapter.teardown()
|
|
81
|
+
except Exception as e:
|
|
82
|
+
log.warning(f"⚠️ [Warning] Cleanup failed: {e}")
|
|
83
|
+
return exit_code
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def run_action_plan_only_mode(
|
|
87
|
+
args,
|
|
88
|
+
output_script_path: str,
|
|
89
|
+
resume_context: dict,
|
|
90
|
+
) -> int:
|
|
91
|
+
reporter = _build_reporter(args, output_script_path, MODE_PLAN_ONLY)
|
|
92
|
+
final_status = "failed"
|
|
93
|
+
exit_code = 1
|
|
94
|
+
final_error = ""
|
|
95
|
+
_emit_run_started(reporter, args, output_script_path, MODE_PLAN_ONLY)
|
|
96
|
+
_apply_resume_summary(reporter, resume_context)
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
action_data = _build_inline_action_data(args)
|
|
100
|
+
plan = {
|
|
101
|
+
"current_state_summary": f"Inline action [{action_data['name']}] preview",
|
|
102
|
+
"planned_steps": [action_data["name"]],
|
|
103
|
+
"suggested_assertion": "",
|
|
104
|
+
"risks": [],
|
|
105
|
+
}
|
|
106
|
+
action_summary = _build_action_summary(args, action_data)
|
|
107
|
+
reporter.update_control_summary(
|
|
108
|
+
control_kind="action",
|
|
109
|
+
control_label=action_summary["action_name"],
|
|
110
|
+
source_ref="inline://action",
|
|
111
|
+
action=action_summary["action"],
|
|
112
|
+
locator_type=action_summary["locator_type"],
|
|
113
|
+
locator_value=action_summary["locator_value"],
|
|
114
|
+
extra_value=action_summary["extra_value"],
|
|
115
|
+
)
|
|
116
|
+
reporter.emit_event(
|
|
117
|
+
"action_loaded",
|
|
118
|
+
action_name=action_summary["action_name"],
|
|
119
|
+
action=action_summary["action"],
|
|
120
|
+
locator_type=action_summary["locator_type"],
|
|
121
|
+
locator_value=action_summary["locator_value"],
|
|
122
|
+
)
|
|
123
|
+
reporter.emit_event(
|
|
124
|
+
"plan_generated",
|
|
125
|
+
current_state_summary=plan["current_state_summary"],
|
|
126
|
+
planned_steps=plan["planned_steps"],
|
|
127
|
+
suggested_assertion="",
|
|
128
|
+
risks=[],
|
|
129
|
+
)
|
|
130
|
+
reporter.update_summary(plan_preview=plan, action_summary=action_summary)
|
|
131
|
+
|
|
132
|
+
log.info(f"🧭 [Action] Action name: {action_summary['action_name']}")
|
|
133
|
+
log.info(f"🧭 [Action] Preview step: {action_summary['action_name']}")
|
|
134
|
+
|
|
135
|
+
final_status = "success"
|
|
136
|
+
exit_code = 0
|
|
137
|
+
except Exception as e:
|
|
138
|
+
final_error = str(e)
|
|
139
|
+
reporter.emit_event("action_plan_failed", error=str(e))
|
|
140
|
+
log.error(f"❌ [Action] Plan generation failed: {e}")
|
|
141
|
+
finally:
|
|
142
|
+
reporter.finalize(
|
|
143
|
+
status=final_status,
|
|
144
|
+
exit_code=exit_code,
|
|
145
|
+
steps_executed=1 if final_status == "success" else 0,
|
|
146
|
+
last_error=final_error,
|
|
147
|
+
)
|
|
148
|
+
return exit_code
|