autoglm-gui 1.4.1__py3-none-any.whl → 1.5.1__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.
- AutoGLM_GUI/__init__.py +11 -0
- AutoGLM_GUI/__main__.py +26 -4
- AutoGLM_GUI/actions/__init__.py +6 -0
- phone_agent/actions/handler_ios.py → AutoGLM_GUI/actions/handler.py +30 -112
- AutoGLM_GUI/actions/types.py +15 -0
- {phone_agent → AutoGLM_GUI}/adb/__init__.py +25 -23
- {phone_agent → AutoGLM_GUI}/adb/connection.py +5 -40
- {phone_agent → AutoGLM_GUI}/adb/device.py +12 -94
- {phone_agent → AutoGLM_GUI}/adb/input.py +6 -47
- AutoGLM_GUI/adb/screenshot.py +11 -0
- {phone_agent/config → AutoGLM_GUI/adb}/timing.py +1 -1
- AutoGLM_GUI/adb_plus/keyboard_installer.py +4 -2
- AutoGLM_GUI/adb_plus/screenshot.py +22 -1
- AutoGLM_GUI/adb_plus/serial.py +38 -20
- AutoGLM_GUI/adb_plus/touch.py +4 -9
- AutoGLM_GUI/agents/__init__.py +43 -12
- AutoGLM_GUI/agents/events.py +19 -0
- AutoGLM_GUI/agents/factory.py +31 -38
- AutoGLM_GUI/agents/glm/__init__.py +7 -0
- AutoGLM_GUI/agents/glm/agent.py +297 -0
- AutoGLM_GUI/agents/glm/message_builder.py +81 -0
- AutoGLM_GUI/agents/glm/parser.py +110 -0
- {phone_agent/config → AutoGLM_GUI/agents/glm}/prompts_en.py +7 -9
- {phone_agent/config → AutoGLM_GUI/agents/glm}/prompts_zh.py +18 -25
- AutoGLM_GUI/agents/mai/__init__.py +28 -0
- AutoGLM_GUI/agents/mai/agent.py +408 -0
- AutoGLM_GUI/agents/mai/parser.py +254 -0
- AutoGLM_GUI/agents/mai/prompts.py +103 -0
- AutoGLM_GUI/agents/mai/traj_memory.py +91 -0
- AutoGLM_GUI/agents/protocols.py +12 -8
- AutoGLM_GUI/agents/stream_runner.py +193 -0
- AutoGLM_GUI/api/__init__.py +40 -21
- AutoGLM_GUI/api/agents.py +181 -239
- AutoGLM_GUI/api/control.py +9 -6
- AutoGLM_GUI/api/devices.py +102 -12
- AutoGLM_GUI/api/history.py +104 -0
- AutoGLM_GUI/api/layered_agent.py +67 -15
- AutoGLM_GUI/api/media.py +64 -1
- AutoGLM_GUI/api/scheduled_tasks.py +98 -0
- AutoGLM_GUI/config.py +81 -0
- AutoGLM_GUI/config_manager.py +68 -51
- AutoGLM_GUI/device_manager.py +248 -29
- AutoGLM_GUI/device_protocol.py +1 -1
- AutoGLM_GUI/devices/adb_device.py +5 -10
- AutoGLM_GUI/devices/mock_device.py +4 -2
- AutoGLM_GUI/devices/remote_device.py +8 -3
- AutoGLM_GUI/history_manager.py +164 -0
- AutoGLM_GUI/model/__init__.py +5 -0
- AutoGLM_GUI/model/message_builder.py +69 -0
- AutoGLM_GUI/model/types.py +24 -0
- AutoGLM_GUI/models/__init__.py +10 -0
- AutoGLM_GUI/models/history.py +140 -0
- AutoGLM_GUI/models/scheduled_task.py +71 -0
- AutoGLM_GUI/parsers/__init__.py +22 -0
- AutoGLM_GUI/parsers/base.py +50 -0
- AutoGLM_GUI/parsers/phone_parser.py +58 -0
- AutoGLM_GUI/phone_agent_manager.py +62 -396
- AutoGLM_GUI/platform_utils.py +26 -0
- AutoGLM_GUI/prompt_config.py +15 -0
- AutoGLM_GUI/prompts/__init__.py +32 -0
- AutoGLM_GUI/scheduler_manager.py +350 -0
- AutoGLM_GUI/schemas.py +246 -72
- AutoGLM_GUI/scrcpy_stream.py +142 -24
- AutoGLM_GUI/socketio_server.py +100 -27
- AutoGLM_GUI/static/assets/{about-_XNhzQZX.js → about-CfwX1Cmc.js} +1 -1
- AutoGLM_GUI/static/assets/alert-dialog-CtGlN2IJ.js +1 -0
- AutoGLM_GUI/static/assets/chat-BYa-foUI.js +129 -0
- AutoGLM_GUI/static/assets/circle-alert-t08bEMPO.js +1 -0
- AutoGLM_GUI/static/assets/dialog-FNwZJFwk.js +45 -0
- AutoGLM_GUI/static/assets/eye-D0UPWCWC.js +1 -0
- AutoGLM_GUI/static/assets/history-CRo95B7i.js +1 -0
- AutoGLM_GUI/static/assets/{index-Cy8TmmHV.js → index-BaLMSqd3.js} +1 -1
- AutoGLM_GUI/static/assets/index-CTHbFvKl.js +11 -0
- AutoGLM_GUI/static/assets/index-CV7jGxGm.css +1 -0
- AutoGLM_GUI/static/assets/label-DJFevVmr.js +1 -0
- AutoGLM_GUI/static/assets/logs-RW09DyYY.js +1 -0
- AutoGLM_GUI/static/assets/popover--JTJrE5v.js +1 -0
- AutoGLM_GUI/static/assets/scheduled-tasks-DTRKsQXF.js +1 -0
- AutoGLM_GUI/static/assets/square-pen-CPK_K680.js +1 -0
- AutoGLM_GUI/static/assets/textarea-PRmVnWq5.js +1 -0
- AutoGLM_GUI/static/assets/workflows-CdcsAoaT.js +1 -0
- AutoGLM_GUI/static/index.html +2 -2
- AutoGLM_GUI/types.py +17 -0
- {autoglm_gui-1.4.1.dist-info → autoglm_gui-1.5.1.dist-info}/METADATA +179 -130
- autoglm_gui-1.5.1.dist-info/RECORD +118 -0
- AutoGLM_GUI/agents/mai_adapter.py +0 -627
- AutoGLM_GUI/api/dual_model.py +0 -317
- AutoGLM_GUI/device_adapter.py +0 -263
- AutoGLM_GUI/dual_model/__init__.py +0 -53
- AutoGLM_GUI/dual_model/decision_model.py +0 -664
- AutoGLM_GUI/dual_model/dual_agent.py +0 -917
- AutoGLM_GUI/dual_model/protocols.py +0 -354
- AutoGLM_GUI/dual_model/vision_model.py +0 -442
- AutoGLM_GUI/mai_ui_adapter/agent_wrapper.py +0 -291
- AutoGLM_GUI/phone_agent_patches.py +0 -147
- AutoGLM_GUI/static/assets/chat-DwJpiAWf.js +0 -126
- AutoGLM_GUI/static/assets/dialog-B3uW4T8V.js +0 -45
- AutoGLM_GUI/static/assets/index-Cpv2gSF1.css +0 -1
- AutoGLM_GUI/static/assets/index-UYYauTly.js +0 -12
- AutoGLM_GUI/static/assets/workflows-Du_de-dt.js +0 -1
- autoglm_gui-1.4.1.dist-info/RECORD +0 -117
- mai_agent/base.py +0 -137
- mai_agent/mai_grounding_agent.py +0 -263
- mai_agent/mai_naivigation_agent.py +0 -526
- mai_agent/prompt.py +0 -148
- mai_agent/unified_memory.py +0 -67
- mai_agent/utils.py +0 -73
- phone_agent/__init__.py +0 -12
- phone_agent/actions/__init__.py +0 -5
- phone_agent/actions/handler.py +0 -400
- phone_agent/adb/screenshot.py +0 -108
- phone_agent/agent.py +0 -253
- phone_agent/agent_ios.py +0 -277
- phone_agent/config/__init__.py +0 -53
- phone_agent/config/apps_harmonyos.py +0 -256
- phone_agent/config/apps_ios.py +0 -339
- phone_agent/config/prompts.py +0 -80
- phone_agent/device_factory.py +0 -166
- phone_agent/hdc/__init__.py +0 -53
- phone_agent/hdc/connection.py +0 -384
- phone_agent/hdc/device.py +0 -269
- phone_agent/hdc/input.py +0 -145
- phone_agent/hdc/screenshot.py +0 -127
- phone_agent/model/__init__.py +0 -5
- phone_agent/model/client.py +0 -290
- phone_agent/xctest/__init__.py +0 -47
- phone_agent/xctest/connection.py +0 -379
- phone_agent/xctest/device.py +0 -472
- phone_agent/xctest/input.py +0 -311
- phone_agent/xctest/screenshot.py +0 -226
- {phone_agent/config → AutoGLM_GUI/adb}/apps.py +0 -0
- {phone_agent/config → AutoGLM_GUI}/i18n.py +0 -0
- {autoglm_gui-1.4.1.dist-info → autoglm_gui-1.5.1.dist-info}/WHEEL +0 -0
- {autoglm_gui-1.4.1.dist-info → autoglm_gui-1.5.1.dist-info}/entry_points.txt +0 -0
- {autoglm_gui-1.4.1.dist-info → autoglm_gui-1.5.1.dist-info}/licenses/LICENSE +0 -0
phone_agent/model/client.py
DELETED
|
@@ -1,290 +0,0 @@
|
|
|
1
|
-
"""Model client for AI inference using OpenAI-compatible API."""
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
import time
|
|
5
|
-
from dataclasses import dataclass, field
|
|
6
|
-
from typing import Any
|
|
7
|
-
|
|
8
|
-
from openai import OpenAI
|
|
9
|
-
|
|
10
|
-
from phone_agent.config.i18n import get_message
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
@dataclass
|
|
14
|
-
class ModelConfig:
|
|
15
|
-
"""Configuration for the AI model."""
|
|
16
|
-
|
|
17
|
-
base_url: str = "http://localhost:8000/v1"
|
|
18
|
-
api_key: str = "EMPTY"
|
|
19
|
-
model_name: str = "autoglm-phone-9b"
|
|
20
|
-
max_tokens: int = 3000
|
|
21
|
-
temperature: float = 0.0
|
|
22
|
-
top_p: float = 0.85
|
|
23
|
-
frequency_penalty: float = 0.2
|
|
24
|
-
extra_body: dict[str, Any] = field(default_factory=dict)
|
|
25
|
-
lang: str = "cn" # Language for UI messages: 'cn' or 'en'
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
@dataclass
|
|
29
|
-
class ModelResponse:
|
|
30
|
-
"""Response from the AI model."""
|
|
31
|
-
|
|
32
|
-
thinking: str
|
|
33
|
-
action: str
|
|
34
|
-
raw_content: str
|
|
35
|
-
# Performance metrics
|
|
36
|
-
time_to_first_token: float | None = None # Time to first token (seconds)
|
|
37
|
-
time_to_thinking_end: float | None = None # Time to thinking end (seconds)
|
|
38
|
-
total_time: float | None = None # Total inference time (seconds)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
class ModelClient:
|
|
42
|
-
"""
|
|
43
|
-
Client for interacting with OpenAI-compatible vision-language models.
|
|
44
|
-
|
|
45
|
-
Args:
|
|
46
|
-
config: Model configuration.
|
|
47
|
-
"""
|
|
48
|
-
|
|
49
|
-
def __init__(self, config: ModelConfig | None = None):
|
|
50
|
-
self.config = config or ModelConfig()
|
|
51
|
-
self.client = OpenAI(base_url=self.config.base_url, api_key=self.config.api_key)
|
|
52
|
-
|
|
53
|
-
def request(self, messages: list[dict[str, Any]]) -> ModelResponse:
|
|
54
|
-
"""
|
|
55
|
-
Send a request to the model.
|
|
56
|
-
|
|
57
|
-
Args:
|
|
58
|
-
messages: List of message dictionaries in OpenAI format.
|
|
59
|
-
|
|
60
|
-
Returns:
|
|
61
|
-
ModelResponse containing thinking and action.
|
|
62
|
-
|
|
63
|
-
Raises:
|
|
64
|
-
ValueError: If the response cannot be parsed.
|
|
65
|
-
"""
|
|
66
|
-
# Start timing
|
|
67
|
-
start_time = time.time()
|
|
68
|
-
time_to_first_token = None
|
|
69
|
-
time_to_thinking_end = None
|
|
70
|
-
|
|
71
|
-
stream = self.client.chat.completions.create(
|
|
72
|
-
messages=messages,
|
|
73
|
-
model=self.config.model_name,
|
|
74
|
-
max_tokens=self.config.max_tokens,
|
|
75
|
-
temperature=self.config.temperature,
|
|
76
|
-
top_p=self.config.top_p,
|
|
77
|
-
frequency_penalty=self.config.frequency_penalty,
|
|
78
|
-
extra_body=self.config.extra_body,
|
|
79
|
-
stream=True,
|
|
80
|
-
)
|
|
81
|
-
|
|
82
|
-
raw_content = ""
|
|
83
|
-
buffer = "" # Buffer to hold content that might be part of a marker
|
|
84
|
-
action_markers = ["finish(message=", "do(action="]
|
|
85
|
-
in_action_phase = False # Track if we've entered the action phase
|
|
86
|
-
first_token_received = False
|
|
87
|
-
|
|
88
|
-
for chunk in stream:
|
|
89
|
-
if len(chunk.choices) == 0:
|
|
90
|
-
continue
|
|
91
|
-
if chunk.choices[0].delta.content is not None:
|
|
92
|
-
content = chunk.choices[0].delta.content
|
|
93
|
-
raw_content += content
|
|
94
|
-
|
|
95
|
-
# Record time to first token
|
|
96
|
-
if not first_token_received:
|
|
97
|
-
time_to_first_token = time.time() - start_time
|
|
98
|
-
first_token_received = True
|
|
99
|
-
|
|
100
|
-
if in_action_phase:
|
|
101
|
-
# Already in action phase, just accumulate content without printing
|
|
102
|
-
continue
|
|
103
|
-
|
|
104
|
-
buffer += content
|
|
105
|
-
|
|
106
|
-
# Check if any marker is fully present in buffer
|
|
107
|
-
marker_found = False
|
|
108
|
-
for marker in action_markers:
|
|
109
|
-
if marker in buffer:
|
|
110
|
-
# Marker found, print everything before it
|
|
111
|
-
thinking_part = buffer.split(marker, 1)[0]
|
|
112
|
-
print(thinking_part, end="", flush=True)
|
|
113
|
-
print() # Print newline after thinking is complete
|
|
114
|
-
in_action_phase = True
|
|
115
|
-
marker_found = True
|
|
116
|
-
|
|
117
|
-
# Record time to thinking end
|
|
118
|
-
if time_to_thinking_end is None:
|
|
119
|
-
time_to_thinking_end = time.time() - start_time
|
|
120
|
-
|
|
121
|
-
break
|
|
122
|
-
|
|
123
|
-
if marker_found:
|
|
124
|
-
continue # Continue to collect remaining content
|
|
125
|
-
|
|
126
|
-
# Check if buffer ends with a prefix of any marker
|
|
127
|
-
# If so, don't print yet (wait for more content)
|
|
128
|
-
is_potential_marker = False
|
|
129
|
-
for marker in action_markers:
|
|
130
|
-
for i in range(1, len(marker)):
|
|
131
|
-
if buffer.endswith(marker[:i]):
|
|
132
|
-
is_potential_marker = True
|
|
133
|
-
break
|
|
134
|
-
if is_potential_marker:
|
|
135
|
-
break
|
|
136
|
-
|
|
137
|
-
if not is_potential_marker:
|
|
138
|
-
# Safe to print the buffer
|
|
139
|
-
print(buffer, end="", flush=True)
|
|
140
|
-
buffer = ""
|
|
141
|
-
|
|
142
|
-
# Calculate total time
|
|
143
|
-
total_time = time.time() - start_time
|
|
144
|
-
|
|
145
|
-
# Parse thinking and action from response
|
|
146
|
-
thinking, action = self._parse_response(raw_content)
|
|
147
|
-
|
|
148
|
-
# Print performance metrics
|
|
149
|
-
lang = self.config.lang
|
|
150
|
-
print()
|
|
151
|
-
print("=" * 50)
|
|
152
|
-
print(f"⏱️ {get_message('performance_metrics', lang)}:")
|
|
153
|
-
print("-" * 50)
|
|
154
|
-
if time_to_first_token is not None:
|
|
155
|
-
print(
|
|
156
|
-
f"{get_message('time_to_first_token', lang)}: {time_to_first_token:.3f}s"
|
|
157
|
-
)
|
|
158
|
-
if time_to_thinking_end is not None:
|
|
159
|
-
print(
|
|
160
|
-
f"{get_message('time_to_thinking_end', lang)}: {time_to_thinking_end:.3f}s"
|
|
161
|
-
)
|
|
162
|
-
print(
|
|
163
|
-
f"{get_message('total_inference_time', lang)}: {total_time:.3f}s"
|
|
164
|
-
)
|
|
165
|
-
print("=" * 50)
|
|
166
|
-
|
|
167
|
-
return ModelResponse(
|
|
168
|
-
thinking=thinking,
|
|
169
|
-
action=action,
|
|
170
|
-
raw_content=raw_content,
|
|
171
|
-
time_to_first_token=time_to_first_token,
|
|
172
|
-
time_to_thinking_end=time_to_thinking_end,
|
|
173
|
-
total_time=total_time,
|
|
174
|
-
)
|
|
175
|
-
|
|
176
|
-
def _parse_response(self, content: str) -> tuple[str, str]:
|
|
177
|
-
"""
|
|
178
|
-
Parse the model response into thinking and action parts.
|
|
179
|
-
|
|
180
|
-
Parsing rules:
|
|
181
|
-
1. If content contains 'finish(message=', everything before is thinking,
|
|
182
|
-
everything from 'finish(message=' onwards is action.
|
|
183
|
-
2. If rule 1 doesn't apply but content contains 'do(action=',
|
|
184
|
-
everything before is thinking, everything from 'do(action=' onwards is action.
|
|
185
|
-
3. Fallback: If content contains '<answer>', use legacy parsing with XML tags.
|
|
186
|
-
4. Otherwise, return empty thinking and full content as action.
|
|
187
|
-
|
|
188
|
-
Args:
|
|
189
|
-
content: Raw response content.
|
|
190
|
-
|
|
191
|
-
Returns:
|
|
192
|
-
Tuple of (thinking, action).
|
|
193
|
-
"""
|
|
194
|
-
# Rule 1: Check for finish(message=
|
|
195
|
-
if "finish(message=" in content:
|
|
196
|
-
parts = content.split("finish(message=", 1)
|
|
197
|
-
thinking = parts[0].strip()
|
|
198
|
-
action = "finish(message=" + parts[1]
|
|
199
|
-
return thinking, action
|
|
200
|
-
|
|
201
|
-
# Rule 2: Check for do(action=
|
|
202
|
-
if "do(action=" in content:
|
|
203
|
-
parts = content.split("do(action=", 1)
|
|
204
|
-
thinking = parts[0].strip()
|
|
205
|
-
action = "do(action=" + parts[1]
|
|
206
|
-
return thinking, action
|
|
207
|
-
|
|
208
|
-
# Rule 3: Fallback to legacy XML tag parsing
|
|
209
|
-
if "<answer>" in content:
|
|
210
|
-
parts = content.split("<answer>", 1)
|
|
211
|
-
thinking = parts[0].replace("<think>", "").replace("</think>", "").strip()
|
|
212
|
-
action = parts[1].replace("</answer>", "").strip()
|
|
213
|
-
return thinking, action
|
|
214
|
-
|
|
215
|
-
# Rule 4: No markers found, return content as action
|
|
216
|
-
return "", content
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
class MessageBuilder:
|
|
220
|
-
"""Helper class for building conversation messages."""
|
|
221
|
-
|
|
222
|
-
@staticmethod
|
|
223
|
-
def create_system_message(content: str) -> dict[str, Any]:
|
|
224
|
-
"""Create a system message."""
|
|
225
|
-
return {"role": "system", "content": content}
|
|
226
|
-
|
|
227
|
-
@staticmethod
|
|
228
|
-
def create_user_message(
|
|
229
|
-
text: str, image_base64: str | None = None
|
|
230
|
-
) -> dict[str, Any]:
|
|
231
|
-
"""
|
|
232
|
-
Create a user message with optional image.
|
|
233
|
-
|
|
234
|
-
Args:
|
|
235
|
-
text: Text content.
|
|
236
|
-
image_base64: Optional base64-encoded image.
|
|
237
|
-
|
|
238
|
-
Returns:
|
|
239
|
-
Message dictionary.
|
|
240
|
-
"""
|
|
241
|
-
content = []
|
|
242
|
-
|
|
243
|
-
if image_base64:
|
|
244
|
-
content.append(
|
|
245
|
-
{
|
|
246
|
-
"type": "image_url",
|
|
247
|
-
"image_url": {"url": f"data:image/png;base64,{image_base64}"},
|
|
248
|
-
}
|
|
249
|
-
)
|
|
250
|
-
|
|
251
|
-
content.append({"type": "text", "text": text})
|
|
252
|
-
|
|
253
|
-
return {"role": "user", "content": content}
|
|
254
|
-
|
|
255
|
-
@staticmethod
|
|
256
|
-
def create_assistant_message(content: str) -> dict[str, Any]:
|
|
257
|
-
"""Create an assistant message."""
|
|
258
|
-
return {"role": "assistant", "content": content}
|
|
259
|
-
|
|
260
|
-
@staticmethod
|
|
261
|
-
def remove_images_from_message(message: dict[str, Any]) -> dict[str, Any]:
|
|
262
|
-
"""
|
|
263
|
-
Remove image content from a message to save context space.
|
|
264
|
-
|
|
265
|
-
Args:
|
|
266
|
-
message: Message dictionary.
|
|
267
|
-
|
|
268
|
-
Returns:
|
|
269
|
-
Message with images removed.
|
|
270
|
-
"""
|
|
271
|
-
if isinstance(message.get("content"), list):
|
|
272
|
-
message["content"] = [
|
|
273
|
-
item for item in message["content"] if item.get("type") == "text"
|
|
274
|
-
]
|
|
275
|
-
return message
|
|
276
|
-
|
|
277
|
-
@staticmethod
|
|
278
|
-
def build_screen_info(current_app: str, **extra_info) -> str:
|
|
279
|
-
"""
|
|
280
|
-
Build screen info string for the model.
|
|
281
|
-
|
|
282
|
-
Args:
|
|
283
|
-
current_app: Current app name.
|
|
284
|
-
**extra_info: Additional info to include.
|
|
285
|
-
|
|
286
|
-
Returns:
|
|
287
|
-
JSON string with screen info.
|
|
288
|
-
"""
|
|
289
|
-
info = {"current_app": current_app, **extra_info}
|
|
290
|
-
return json.dumps(info, ensure_ascii=False)
|
phone_agent/xctest/__init__.py
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
"""XCTest utilities for iOS device interaction via WebDriverAgent/XCUITest."""
|
|
2
|
-
|
|
3
|
-
from phone_agent.xctest.connection import (
|
|
4
|
-
ConnectionType,
|
|
5
|
-
DeviceInfo,
|
|
6
|
-
XCTestConnection,
|
|
7
|
-
list_devices,
|
|
8
|
-
quick_connect,
|
|
9
|
-
)
|
|
10
|
-
from phone_agent.xctest.device import (
|
|
11
|
-
back,
|
|
12
|
-
double_tap,
|
|
13
|
-
get_current_app,
|
|
14
|
-
home,
|
|
15
|
-
launch_app,
|
|
16
|
-
long_press,
|
|
17
|
-
swipe,
|
|
18
|
-
tap,
|
|
19
|
-
)
|
|
20
|
-
from phone_agent.xctest.input import (
|
|
21
|
-
clear_text,
|
|
22
|
-
type_text,
|
|
23
|
-
)
|
|
24
|
-
from phone_agent.xctest.screenshot import get_screenshot
|
|
25
|
-
|
|
26
|
-
__all__ = [
|
|
27
|
-
# Screenshot
|
|
28
|
-
"get_screenshot",
|
|
29
|
-
# Input
|
|
30
|
-
"type_text",
|
|
31
|
-
"clear_text",
|
|
32
|
-
# Device control
|
|
33
|
-
"get_current_app",
|
|
34
|
-
"tap",
|
|
35
|
-
"swipe",
|
|
36
|
-
"back",
|
|
37
|
-
"home",
|
|
38
|
-
"double_tap",
|
|
39
|
-
"long_press",
|
|
40
|
-
"launch_app",
|
|
41
|
-
# Connection management
|
|
42
|
-
"XCTestConnection",
|
|
43
|
-
"DeviceInfo",
|
|
44
|
-
"ConnectionType",
|
|
45
|
-
"quick_connect",
|
|
46
|
-
"list_devices",
|
|
47
|
-
]
|
phone_agent/xctest/connection.py
DELETED
|
@@ -1,379 +0,0 @@
|
|
|
1
|
-
"""iOS device connection management via idevice tools and WebDriverAgent."""
|
|
2
|
-
|
|
3
|
-
import subprocess
|
|
4
|
-
from dataclasses import dataclass
|
|
5
|
-
from enum import Enum
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class ConnectionType(Enum):
|
|
9
|
-
"""Type of iOS connection."""
|
|
10
|
-
|
|
11
|
-
USB = "usb"
|
|
12
|
-
NETWORK = "network"
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
@dataclass
|
|
16
|
-
class DeviceInfo:
|
|
17
|
-
"""Information about a connected iOS device."""
|
|
18
|
-
|
|
19
|
-
device_id: str # UDID
|
|
20
|
-
status: str
|
|
21
|
-
connection_type: ConnectionType
|
|
22
|
-
model: str | None = None
|
|
23
|
-
ios_version: str | None = None
|
|
24
|
-
device_name: str | None = None
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
class XCTestConnection:
|
|
28
|
-
"""
|
|
29
|
-
Manages connections to iOS devices via libimobiledevice and WebDriverAgent.
|
|
30
|
-
|
|
31
|
-
Requires:
|
|
32
|
-
- libimobiledevice (idevice_id, ideviceinfo)
|
|
33
|
-
- WebDriverAgent running on the iOS device
|
|
34
|
-
- ios-deploy (optional, for app installation)
|
|
35
|
-
|
|
36
|
-
Example:
|
|
37
|
-
>>> conn = XCTestConnection()
|
|
38
|
-
>>> # List connected devices
|
|
39
|
-
>>> devices = conn.list_devices()
|
|
40
|
-
>>> # Get device info
|
|
41
|
-
>>> info = conn.get_device_info()
|
|
42
|
-
>>> # Check if WDA is running
|
|
43
|
-
>>> is_ready = conn.is_wda_ready()
|
|
44
|
-
"""
|
|
45
|
-
|
|
46
|
-
def __init__(self, wda_url: str = "http://localhost:8100"):
|
|
47
|
-
"""
|
|
48
|
-
Initialize iOS connection manager.
|
|
49
|
-
|
|
50
|
-
Args:
|
|
51
|
-
wda_url: WebDriverAgent URL (default: http://localhost:8100).
|
|
52
|
-
For network devices, use http://<device-ip>:8100
|
|
53
|
-
"""
|
|
54
|
-
self.wda_url = wda_url.rstrip("/")
|
|
55
|
-
|
|
56
|
-
def list_devices(self) -> list[DeviceInfo]:
|
|
57
|
-
"""
|
|
58
|
-
List all connected iOS devices.
|
|
59
|
-
|
|
60
|
-
Returns:
|
|
61
|
-
List of DeviceInfo objects.
|
|
62
|
-
|
|
63
|
-
Note:
|
|
64
|
-
Requires libimobiledevice to be installed.
|
|
65
|
-
Install on macOS: brew install libimobiledevice
|
|
66
|
-
"""
|
|
67
|
-
try:
|
|
68
|
-
# Get list of device UDIDs
|
|
69
|
-
result = subprocess.run(
|
|
70
|
-
["idevice_id", "-ln"],
|
|
71
|
-
capture_output=True,
|
|
72
|
-
text=True,
|
|
73
|
-
timeout=5,
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
devices = []
|
|
77
|
-
for line in result.stdout.strip().split("\n"):
|
|
78
|
-
udid = line.strip()
|
|
79
|
-
if not udid:
|
|
80
|
-
continue
|
|
81
|
-
|
|
82
|
-
# Determine connection type (network devices have specific format)
|
|
83
|
-
conn_type = (
|
|
84
|
-
ConnectionType.NETWORK
|
|
85
|
-
if "-" in udid and len(udid) > 40
|
|
86
|
-
else ConnectionType.USB
|
|
87
|
-
)
|
|
88
|
-
|
|
89
|
-
# Get detailed device info
|
|
90
|
-
device_info = self._get_device_details(udid)
|
|
91
|
-
|
|
92
|
-
devices.append(
|
|
93
|
-
DeviceInfo(
|
|
94
|
-
device_id=udid,
|
|
95
|
-
status="connected",
|
|
96
|
-
connection_type=conn_type,
|
|
97
|
-
model=device_info.get("model"),
|
|
98
|
-
ios_version=device_info.get("ios_version"),
|
|
99
|
-
device_name=device_info.get("name"),
|
|
100
|
-
)
|
|
101
|
-
)
|
|
102
|
-
|
|
103
|
-
return devices
|
|
104
|
-
|
|
105
|
-
except FileNotFoundError:
|
|
106
|
-
print(
|
|
107
|
-
"Error: idevice_id not found. Install libimobiledevice: brew install libimobiledevice"
|
|
108
|
-
)
|
|
109
|
-
return []
|
|
110
|
-
except Exception as e:
|
|
111
|
-
print(f"Error listing devices: {e}")
|
|
112
|
-
return []
|
|
113
|
-
|
|
114
|
-
def _get_device_details(self, udid: str) -> dict[str, str]:
|
|
115
|
-
"""
|
|
116
|
-
Get detailed information about a specific device.
|
|
117
|
-
|
|
118
|
-
Args:
|
|
119
|
-
udid: Device UDID.
|
|
120
|
-
|
|
121
|
-
Returns:
|
|
122
|
-
Dictionary with device details.
|
|
123
|
-
"""
|
|
124
|
-
try:
|
|
125
|
-
result = subprocess.run(
|
|
126
|
-
["ideviceinfo", "-u", udid],
|
|
127
|
-
capture_output=True,
|
|
128
|
-
text=True,
|
|
129
|
-
timeout=5,
|
|
130
|
-
)
|
|
131
|
-
|
|
132
|
-
info = {}
|
|
133
|
-
for line in result.stdout.split("\n"):
|
|
134
|
-
if ": " in line:
|
|
135
|
-
key, value = line.split(": ", 1)
|
|
136
|
-
key = key.strip()
|
|
137
|
-
value = value.strip()
|
|
138
|
-
|
|
139
|
-
if key == "ProductType":
|
|
140
|
-
info["model"] = value
|
|
141
|
-
elif key == "ProductVersion":
|
|
142
|
-
info["ios_version"] = value
|
|
143
|
-
elif key == "DeviceName":
|
|
144
|
-
info["name"] = value
|
|
145
|
-
|
|
146
|
-
return info
|
|
147
|
-
|
|
148
|
-
except Exception:
|
|
149
|
-
return {}
|
|
150
|
-
|
|
151
|
-
def get_device_info(self, device_id: str | None = None) -> DeviceInfo | None:
|
|
152
|
-
"""
|
|
153
|
-
Get detailed information about a device.
|
|
154
|
-
|
|
155
|
-
Args:
|
|
156
|
-
device_id: Device UDID. If None, uses first available device.
|
|
157
|
-
|
|
158
|
-
Returns:
|
|
159
|
-
DeviceInfo or None if not found.
|
|
160
|
-
"""
|
|
161
|
-
devices = self.list_devices()
|
|
162
|
-
|
|
163
|
-
if not devices:
|
|
164
|
-
return None
|
|
165
|
-
|
|
166
|
-
if device_id is None:
|
|
167
|
-
return devices[0]
|
|
168
|
-
|
|
169
|
-
for device in devices:
|
|
170
|
-
if device.device_id == device_id:
|
|
171
|
-
return device
|
|
172
|
-
|
|
173
|
-
return None
|
|
174
|
-
|
|
175
|
-
def is_connected(self, device_id: str | None = None) -> bool:
|
|
176
|
-
"""
|
|
177
|
-
Check if a device is connected.
|
|
178
|
-
|
|
179
|
-
Args:
|
|
180
|
-
device_id: Device UDID to check. If None, checks if any device is connected.
|
|
181
|
-
|
|
182
|
-
Returns:
|
|
183
|
-
True if connected, False otherwise.
|
|
184
|
-
"""
|
|
185
|
-
devices = self.list_devices()
|
|
186
|
-
|
|
187
|
-
if not devices:
|
|
188
|
-
return False
|
|
189
|
-
|
|
190
|
-
if device_id is None:
|
|
191
|
-
return len(devices) > 0
|
|
192
|
-
|
|
193
|
-
return any(d.device_id == device_id for d in devices)
|
|
194
|
-
|
|
195
|
-
def is_wda_ready(self, timeout: int = 2) -> bool:
|
|
196
|
-
"""
|
|
197
|
-
Check if WebDriverAgent is running and accessible.
|
|
198
|
-
|
|
199
|
-
Args:
|
|
200
|
-
timeout: Request timeout in seconds.
|
|
201
|
-
|
|
202
|
-
Returns:
|
|
203
|
-
True if WDA is ready, False otherwise.
|
|
204
|
-
"""
|
|
205
|
-
try:
|
|
206
|
-
import requests
|
|
207
|
-
|
|
208
|
-
response = requests.get(
|
|
209
|
-
f"{self.wda_url}/status", timeout=timeout, verify=False
|
|
210
|
-
)
|
|
211
|
-
return response.status_code == 200
|
|
212
|
-
except ImportError:
|
|
213
|
-
print("Error: requests library not found. Install it: pip install requests")
|
|
214
|
-
return False
|
|
215
|
-
except Exception:
|
|
216
|
-
return False
|
|
217
|
-
|
|
218
|
-
def start_wda_session(self) -> tuple[bool, str]:
|
|
219
|
-
"""
|
|
220
|
-
Start a new WebDriverAgent session.
|
|
221
|
-
|
|
222
|
-
Returns:
|
|
223
|
-
Tuple of (success, session_id or error_message).
|
|
224
|
-
"""
|
|
225
|
-
try:
|
|
226
|
-
import requests
|
|
227
|
-
|
|
228
|
-
response = requests.post(
|
|
229
|
-
f"{self.wda_url}/session",
|
|
230
|
-
json={"capabilities": {}},
|
|
231
|
-
timeout=30,
|
|
232
|
-
verify=False,
|
|
233
|
-
)
|
|
234
|
-
|
|
235
|
-
if response.status_code in (200, 201):
|
|
236
|
-
data = response.json()
|
|
237
|
-
session_id = data.get("sessionId") or data.get("value", {}).get(
|
|
238
|
-
"sessionId"
|
|
239
|
-
)
|
|
240
|
-
return True, session_id or "session_started"
|
|
241
|
-
else:
|
|
242
|
-
return False, f"Failed to start session: {response.text}"
|
|
243
|
-
|
|
244
|
-
except ImportError:
|
|
245
|
-
return (
|
|
246
|
-
False,
|
|
247
|
-
"requests library not found. Install it: pip install requests",
|
|
248
|
-
)
|
|
249
|
-
except Exception as e:
|
|
250
|
-
return False, f"Error starting WDA session: {e}"
|
|
251
|
-
|
|
252
|
-
def get_wda_status(self) -> dict | None:
|
|
253
|
-
"""
|
|
254
|
-
Get WebDriverAgent status information.
|
|
255
|
-
|
|
256
|
-
Returns:
|
|
257
|
-
Status dictionary or None if not available.
|
|
258
|
-
"""
|
|
259
|
-
try:
|
|
260
|
-
import requests
|
|
261
|
-
|
|
262
|
-
response = requests.get(f"{self.wda_url}/status", timeout=5, verify=False)
|
|
263
|
-
|
|
264
|
-
if response.status_code == 200:
|
|
265
|
-
return response.json()
|
|
266
|
-
return None
|
|
267
|
-
|
|
268
|
-
except Exception:
|
|
269
|
-
return None
|
|
270
|
-
|
|
271
|
-
def pair_device(self, device_id: str | None = None) -> tuple[bool, str]:
|
|
272
|
-
"""
|
|
273
|
-
Pair with an iOS device (required for some operations).
|
|
274
|
-
|
|
275
|
-
Args:
|
|
276
|
-
device_id: Device UDID. If None, uses first available device.
|
|
277
|
-
|
|
278
|
-
Returns:
|
|
279
|
-
Tuple of (success, message).
|
|
280
|
-
"""
|
|
281
|
-
try:
|
|
282
|
-
cmd = ["idevicepair"]
|
|
283
|
-
if device_id:
|
|
284
|
-
cmd.extend(["-u", device_id])
|
|
285
|
-
cmd.append("pair")
|
|
286
|
-
|
|
287
|
-
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
|
288
|
-
|
|
289
|
-
output = result.stdout + result.stderr
|
|
290
|
-
|
|
291
|
-
if "SUCCESS" in output or "already paired" in output.lower():
|
|
292
|
-
return True, "Device paired successfully"
|
|
293
|
-
else:
|
|
294
|
-
return False, output.strip()
|
|
295
|
-
|
|
296
|
-
except FileNotFoundError:
|
|
297
|
-
return (
|
|
298
|
-
False,
|
|
299
|
-
"idevicepair not found. Install libimobiledevice: brew install libimobiledevice",
|
|
300
|
-
)
|
|
301
|
-
except Exception as e:
|
|
302
|
-
return False, f"Error pairing device: {e}"
|
|
303
|
-
|
|
304
|
-
def get_device_name(self, device_id: str | None = None) -> str | None:
|
|
305
|
-
"""
|
|
306
|
-
Get the device name.
|
|
307
|
-
|
|
308
|
-
Args:
|
|
309
|
-
device_id: Device UDID. If None, uses first available device.
|
|
310
|
-
|
|
311
|
-
Returns:
|
|
312
|
-
Device name string or None if not found.
|
|
313
|
-
"""
|
|
314
|
-
try:
|
|
315
|
-
cmd = ["ideviceinfo"]
|
|
316
|
-
if device_id:
|
|
317
|
-
cmd.extend(["-u", device_id])
|
|
318
|
-
cmd.extend(["-k", "DeviceName"])
|
|
319
|
-
|
|
320
|
-
result = subprocess.run(cmd, capture_output=True, text=True, timeout=5)
|
|
321
|
-
|
|
322
|
-
return result.stdout.strip() or None
|
|
323
|
-
|
|
324
|
-
except Exception as e:
|
|
325
|
-
print(f"Error getting device name: {e}")
|
|
326
|
-
return None
|
|
327
|
-
|
|
328
|
-
def restart_wda(self) -> tuple[bool, str]:
|
|
329
|
-
"""
|
|
330
|
-
Restart WebDriverAgent (requires manual restart on device).
|
|
331
|
-
|
|
332
|
-
Returns:
|
|
333
|
-
Tuple of (success, message).
|
|
334
|
-
|
|
335
|
-
Note:
|
|
336
|
-
This method only checks if WDA needs restart.
|
|
337
|
-
Actual restart requires re-running WDA on the device via Xcode or other means.
|
|
338
|
-
"""
|
|
339
|
-
if self.is_wda_ready():
|
|
340
|
-
return True, "WDA is already running"
|
|
341
|
-
else:
|
|
342
|
-
return (
|
|
343
|
-
False,
|
|
344
|
-
"WDA is not running. Please start it manually on the device.",
|
|
345
|
-
)
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
def quick_connect(wda_url: str = "http://localhost:8100") -> tuple[bool, str]:
|
|
349
|
-
"""
|
|
350
|
-
Quick helper to check iOS device connection and WDA status.
|
|
351
|
-
|
|
352
|
-
Args:
|
|
353
|
-
wda_url: WebDriverAgent URL.
|
|
354
|
-
|
|
355
|
-
Returns:
|
|
356
|
-
Tuple of (success, message).
|
|
357
|
-
"""
|
|
358
|
-
conn = XCTestConnection(wda_url=wda_url)
|
|
359
|
-
|
|
360
|
-
# Check if device is connected
|
|
361
|
-
if not conn.is_connected():
|
|
362
|
-
return False, "No iOS device connected"
|
|
363
|
-
|
|
364
|
-
# Check if WDA is ready
|
|
365
|
-
if not conn.is_wda_ready():
|
|
366
|
-
return False, "WebDriverAgent is not running"
|
|
367
|
-
|
|
368
|
-
return True, "iOS device connected and WDA ready"
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
def list_devices() -> list[DeviceInfo]:
|
|
372
|
-
"""
|
|
373
|
-
Quick helper to list connected iOS devices.
|
|
374
|
-
|
|
375
|
-
Returns:
|
|
376
|
-
List of DeviceInfo objects.
|
|
377
|
-
"""
|
|
378
|
-
conn = XCTestConnection()
|
|
379
|
-
return conn.list_devices()
|