autoglm-gui 1.3.1__py3-none-any.whl → 1.4.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/__main__.py +0 -4
- AutoGLM_GUI/adb_plus/qr_pair.py +8 -8
- AutoGLM_GUI/agents/__init__.py +20 -0
- AutoGLM_GUI/agents/factory.py +160 -0
- AutoGLM_GUI/agents/mai_adapter.py +627 -0
- AutoGLM_GUI/agents/protocols.py +23 -0
- AutoGLM_GUI/api/__init__.py +50 -7
- AutoGLM_GUI/api/agents.py +61 -19
- AutoGLM_GUI/api/devices.py +12 -18
- AutoGLM_GUI/api/dual_model.py +24 -17
- AutoGLM_GUI/api/health.py +13 -0
- AutoGLM_GUI/api/layered_agent.py +659 -0
- AutoGLM_GUI/api/mcp.py +11 -10
- AutoGLM_GUI/api/version.py +23 -10
- AutoGLM_GUI/api/workflows.py +2 -1
- AutoGLM_GUI/config_manager.py +56 -24
- AutoGLM_GUI/device_adapter.py +263 -0
- AutoGLM_GUI/device_protocol.py +266 -0
- AutoGLM_GUI/devices/__init__.py +49 -0
- AutoGLM_GUI/devices/adb_device.py +205 -0
- AutoGLM_GUI/devices/mock_device.py +183 -0
- AutoGLM_GUI/devices/remote_device.py +172 -0
- AutoGLM_GUI/dual_model/decision_model.py +4 -4
- AutoGLM_GUI/dual_model/protocols.py +3 -3
- AutoGLM_GUI/exceptions.py +3 -3
- AutoGLM_GUI/mai_ui_adapter/agent_wrapper.py +291 -0
- AutoGLM_GUI/metrics.py +13 -20
- AutoGLM_GUI/phone_agent_manager.py +219 -134
- AutoGLM_GUI/phone_agent_patches.py +2 -1
- AutoGLM_GUI/platform_utils.py +5 -2
- AutoGLM_GUI/prompts.py +6 -1
- AutoGLM_GUI/schemas.py +45 -14
- AutoGLM_GUI/scrcpy_stream.py +17 -13
- AutoGLM_GUI/server.py +3 -1
- AutoGLM_GUI/socketio_server.py +16 -4
- AutoGLM_GUI/state.py +10 -30
- AutoGLM_GUI/static/assets/{about-Cj6QXqMf.js → about-_XNhzQZX.js} +1 -1
- AutoGLM_GUI/static/assets/chat-DwJpiAWf.js +126 -0
- AutoGLM_GUI/static/assets/{dialog-CxJlnjzH.js → dialog-B3uW4T8V.js} +3 -3
- AutoGLM_GUI/static/assets/index-Cpv2gSF1.css +1 -0
- AutoGLM_GUI/static/assets/{index-C_B-Arvf.js → index-Cy8TmmHV.js} +1 -1
- AutoGLM_GUI/static/assets/{index-CxJQuE4y.js → index-UYYauTly.js} +6 -6
- AutoGLM_GUI/static/assets/{workflows-BTiGCNI0.js → workflows-Du_de-dt.js} +1 -1
- AutoGLM_GUI/static/index.html +2 -2
- AutoGLM_GUI/types.py +125 -0
- {autoglm_gui-1.3.1.dist-info → autoglm_gui-1.4.1.dist-info}/METADATA +147 -65
- {autoglm_gui-1.3.1.dist-info → autoglm_gui-1.4.1.dist-info}/RECORD +58 -39
- mai_agent/base.py +137 -0
- mai_agent/mai_grounding_agent.py +263 -0
- mai_agent/mai_naivigation_agent.py +526 -0
- mai_agent/prompt.py +148 -0
- mai_agent/unified_memory.py +67 -0
- mai_agent/utils.py +73 -0
- phone_agent/config/prompts.py +6 -1
- phone_agent/config/prompts_zh.py +6 -1
- AutoGLM_GUI/config.py +0 -23
- AutoGLM_GUI/static/assets/chat-BJeomZgh.js +0 -124
- AutoGLM_GUI/static/assets/index-Z0uYCPOO.css +0 -1
- {autoglm_gui-1.3.1.dist-info → autoglm_gui-1.4.1.dist-info}/WHEEL +0 -0
- {autoglm_gui-1.3.1.dist-info → autoglm_gui-1.4.1.dist-info}/entry_points.txt +0 -0
- {autoglm_gui-1.3.1.dist-info → autoglm_gui-1.4.1.dist-info}/licenses/LICENSE +0 -0
AutoGLM_GUI/api/mcp.py
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
"""MCP (Model Context Protocol) tools for AutoGLM-GUI."""
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from typing_extensions import TypedDict
|
|
4
4
|
|
|
5
5
|
from fastmcp import FastMCP
|
|
6
6
|
|
|
7
7
|
from AutoGLM_GUI.logger import logger
|
|
8
8
|
from AutoGLM_GUI.prompts import MCP_SYSTEM_PROMPT_ZH
|
|
9
|
+
from AutoGLM_GUI.schemas import DeviceResponse
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ChatResult(TypedDict):
|
|
13
|
+
result: str
|
|
14
|
+
steps: int
|
|
15
|
+
success: bool
|
|
16
|
+
|
|
9
17
|
|
|
10
18
|
# 创建 MCP 服务器实例
|
|
11
19
|
mcp = FastMCP("AutoGLM-GUI MCP Server")
|
|
@@ -15,7 +23,7 @@ MCP_MAX_STEPS = 5
|
|
|
15
23
|
|
|
16
24
|
|
|
17
25
|
@mcp.tool()
|
|
18
|
-
def chat(device_id: str, message: str) ->
|
|
26
|
+
def chat(device_id: str, message: str) -> ChatResult:
|
|
19
27
|
"""
|
|
20
28
|
Send a task to the AutoGLM Phone Agent for execution.
|
|
21
29
|
|
|
@@ -26,13 +34,6 @@ def chat(device_id: str, message: str) -> Dict[str, Any]:
|
|
|
26
34
|
Args:
|
|
27
35
|
device_id: Device identifier (e.g., "192.168.1.100:5555" or serial)
|
|
28
36
|
message: Natural language task (e.g., "打开微信", "发送消息")
|
|
29
|
-
|
|
30
|
-
Returns:
|
|
31
|
-
{
|
|
32
|
-
"result": str, # Task execution result
|
|
33
|
-
"steps": int, # Number of steps taken
|
|
34
|
-
"success": bool # Success flag
|
|
35
|
-
}
|
|
36
37
|
"""
|
|
37
38
|
from AutoGLM_GUI.exceptions import DeviceBusyError
|
|
38
39
|
from AutoGLM_GUI.phone_agent_manager import PhoneAgentManager
|
|
@@ -84,7 +85,7 @@ def chat(device_id: str, message: str) -> Dict[str, Any]:
|
|
|
84
85
|
|
|
85
86
|
|
|
86
87
|
@mcp.tool()
|
|
87
|
-
def list_devices() ->
|
|
88
|
+
def list_devices() -> list[DeviceResponse]:
|
|
88
89
|
"""
|
|
89
90
|
List all connected ADB devices and their agent status.
|
|
90
91
|
|
AutoGLM_GUI/api/version.py
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
import json
|
|
4
4
|
import re
|
|
5
5
|
import time
|
|
6
|
+
import urllib.error
|
|
6
7
|
import urllib.request
|
|
7
|
-
from
|
|
8
|
+
from typing_extensions import TypedDict
|
|
8
9
|
|
|
9
10
|
from fastapi import APIRouter
|
|
10
11
|
|
|
@@ -12,13 +13,30 @@ from AutoGLM_GUI.logger import logger
|
|
|
12
13
|
from AutoGLM_GUI.schemas import VersionCheckResponse
|
|
13
14
|
from AutoGLM_GUI.version import APP_VERSION
|
|
14
15
|
|
|
16
|
+
|
|
17
|
+
class GitHubRelease(TypedDict, total=False):
|
|
18
|
+
"""GitHub Release API response structure."""
|
|
19
|
+
|
|
20
|
+
tag_name: str
|
|
21
|
+
html_url: str
|
|
22
|
+
published_at: str
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class _VersionCache(TypedDict):
|
|
26
|
+
"""Internal cache structure for version checking."""
|
|
27
|
+
|
|
28
|
+
data: VersionCheckResponse | None
|
|
29
|
+
timestamp: float
|
|
30
|
+
ttl: int
|
|
31
|
+
|
|
32
|
+
|
|
15
33
|
router = APIRouter()
|
|
16
34
|
|
|
17
35
|
# In-memory cache for version check results
|
|
18
|
-
_version_cache:
|
|
36
|
+
_version_cache: _VersionCache = {
|
|
19
37
|
"data": None,
|
|
20
38
|
"timestamp": 0,
|
|
21
|
-
"ttl": 3600,
|
|
39
|
+
"ttl": 3600,
|
|
22
40
|
}
|
|
23
41
|
|
|
24
42
|
# GitHub repository information
|
|
@@ -74,13 +92,8 @@ def compare_versions(current: str, latest: str) -> bool:
|
|
|
74
92
|
return latest_tuple > current_tuple
|
|
75
93
|
|
|
76
94
|
|
|
77
|
-
def fetch_latest_release() ->
|
|
78
|
-
"""
|
|
79
|
-
Fetch latest release information from GitHub API.
|
|
80
|
-
|
|
81
|
-
Returns:
|
|
82
|
-
Release data dict with 'tag_name', 'html_url', 'published_at' or None on error
|
|
83
|
-
"""
|
|
95
|
+
def fetch_latest_release() -> GitHubRelease | None:
|
|
96
|
+
"""Fetch latest release information from GitHub API."""
|
|
84
97
|
try:
|
|
85
98
|
# Create request with User-Agent header (required by GitHub API)
|
|
86
99
|
req = urllib.request.Request(
|
AutoGLM_GUI/api/workflows.py
CHANGED
|
@@ -17,7 +17,8 @@ def list_workflows() -> WorkflowListResponse:
|
|
|
17
17
|
"""获取所有 workflows."""
|
|
18
18
|
from AutoGLM_GUI.workflow_manager import workflow_manager
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
workflow_dicts = workflow_manager.list_workflows()
|
|
21
|
+
workflows = [WorkflowResponse(**wf) for wf in workflow_dicts]
|
|
21
22
|
return WorkflowListResponse(workflows=workflows)
|
|
22
23
|
|
|
23
24
|
|
AutoGLM_GUI/config_manager.py
CHANGED
|
@@ -54,12 +54,26 @@ class ConfigModel(BaseModel):
|
|
|
54
54
|
|
|
55
55
|
# 双模型配置
|
|
56
56
|
dual_model_enabled: bool = False
|
|
57
|
-
decision_base_url: str = "
|
|
58
|
-
decision_model_name: str = "
|
|
57
|
+
decision_base_url: str = ""
|
|
58
|
+
decision_model_name: str = ""
|
|
59
59
|
decision_api_key: str = ""
|
|
60
60
|
|
|
61
|
-
#
|
|
62
|
-
|
|
61
|
+
# Agent 类型配置
|
|
62
|
+
agent_type: str = "glm" # Agent type (e.g., "glm", "mai")
|
|
63
|
+
agent_config_params: dict | None = None # Agent-specific configuration
|
|
64
|
+
|
|
65
|
+
# Agent 执行配置
|
|
66
|
+
default_max_steps: int = 100 # 单次任务最大执行步数
|
|
67
|
+
|
|
68
|
+
@field_validator("default_max_steps")
|
|
69
|
+
@classmethod
|
|
70
|
+
def validate_default_max_steps(cls, v: int) -> int:
|
|
71
|
+
"""验证 default_max_steps 范围."""
|
|
72
|
+
if v <= 0:
|
|
73
|
+
raise ValueError("default_max_steps must be positive")
|
|
74
|
+
if v > 1000:
|
|
75
|
+
raise ValueError("default_max_steps must be <= 1000")
|
|
76
|
+
return v
|
|
63
77
|
|
|
64
78
|
@field_validator("base_url")
|
|
65
79
|
@classmethod
|
|
@@ -85,14 +99,6 @@ class ConfigModel(BaseModel):
|
|
|
85
99
|
raise ValueError("decision_base_url must start with http:// or https://")
|
|
86
100
|
return v.rstrip("/") # 去除尾部斜杠
|
|
87
101
|
|
|
88
|
-
@field_validator("thinking_mode")
|
|
89
|
-
@classmethod
|
|
90
|
-
def validate_thinking_mode(cls, v: str) -> str:
|
|
91
|
-
"""验证思考模式."""
|
|
92
|
-
if v not in ("fast", "deep"):
|
|
93
|
-
raise ValueError("thinking_mode must be 'fast' or 'deep'")
|
|
94
|
-
return v
|
|
95
|
-
|
|
96
102
|
|
|
97
103
|
# ==================== 配置层数据类 ====================
|
|
98
104
|
|
|
@@ -109,8 +115,11 @@ class ConfigLayer:
|
|
|
109
115
|
decision_base_url: Optional[str] = None
|
|
110
116
|
decision_model_name: Optional[str] = None
|
|
111
117
|
decision_api_key: Optional[str] = None
|
|
112
|
-
#
|
|
113
|
-
|
|
118
|
+
# Agent 类型配置
|
|
119
|
+
agent_type: Optional[str] = None
|
|
120
|
+
agent_config_params: Optional[dict] = None
|
|
121
|
+
# Agent 执行配置
|
|
122
|
+
default_max_steps: Optional[int] = None
|
|
114
123
|
|
|
115
124
|
source: ConfigSource = ConfigSource.DEFAULT
|
|
116
125
|
|
|
@@ -142,7 +151,9 @@ class ConfigLayer:
|
|
|
142
151
|
"decision_base_url": self.decision_base_url,
|
|
143
152
|
"decision_model_name": self.decision_model_name,
|
|
144
153
|
"decision_api_key": self.decision_api_key,
|
|
145
|
-
"
|
|
154
|
+
"agent_type": self.agent_type,
|
|
155
|
+
"agent_config_params": self.agent_config_params,
|
|
156
|
+
"default_max_steps": self.default_max_steps,
|
|
146
157
|
}.items()
|
|
147
158
|
if v is not None
|
|
148
159
|
}
|
|
@@ -202,6 +213,9 @@ class UnifiedConfigManager:
|
|
|
202
213
|
base_url="",
|
|
203
214
|
model_name="autoglm-phone-9b",
|
|
204
215
|
api_key="EMPTY",
|
|
216
|
+
agent_type="glm",
|
|
217
|
+
agent_config_params=None,
|
|
218
|
+
default_max_steps=100,
|
|
205
219
|
source=ConfigSource.DEFAULT,
|
|
206
220
|
)
|
|
207
221
|
|
|
@@ -314,7 +328,11 @@ class UnifiedConfigManager:
|
|
|
314
328
|
decision_base_url=config_data.get("decision_base_url"),
|
|
315
329
|
decision_model_name=config_data.get("decision_model_name"),
|
|
316
330
|
decision_api_key=config_data.get("decision_api_key"),
|
|
317
|
-
|
|
331
|
+
agent_type=config_data.get(
|
|
332
|
+
"agent_type", "glm"
|
|
333
|
+
), # 默认 'glm',兼容旧配置
|
|
334
|
+
agent_config_params=config_data.get("agent_config_params"),
|
|
335
|
+
default_max_steps=config_data.get("default_max_steps"),
|
|
318
336
|
source=ConfigSource.FILE,
|
|
319
337
|
)
|
|
320
338
|
self._effective_config = None # 清除缓存
|
|
@@ -346,7 +364,9 @@ class UnifiedConfigManager:
|
|
|
346
364
|
decision_base_url: Optional[str] = None,
|
|
347
365
|
decision_model_name: Optional[str] = None,
|
|
348
366
|
decision_api_key: Optional[str] = None,
|
|
349
|
-
|
|
367
|
+
agent_type: Optional[str] = None,
|
|
368
|
+
agent_config_params: Optional[dict] = None,
|
|
369
|
+
default_max_steps: Optional[int] = None,
|
|
350
370
|
merge_mode: bool = True,
|
|
351
371
|
) -> bool:
|
|
352
372
|
"""
|
|
@@ -360,7 +380,9 @@ class UnifiedConfigManager:
|
|
|
360
380
|
decision_base_url: 决策模型 Base URL
|
|
361
381
|
decision_model_name: 决策模型名称
|
|
362
382
|
decision_api_key: 决策模型 API key
|
|
363
|
-
|
|
383
|
+
agent_type: Agent 类型(可选,如 "glm", "mai")
|
|
384
|
+
agent_config_params: Agent 特定配置参数(可选)
|
|
385
|
+
default_max_steps: 默认最大执行步数(可选)
|
|
364
386
|
merge_mode: 是否合并现有配置(True: 保留未提供的字段)
|
|
365
387
|
|
|
366
388
|
Returns:
|
|
@@ -371,7 +393,7 @@ class UnifiedConfigManager:
|
|
|
371
393
|
self._config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
372
394
|
|
|
373
395
|
# 准备新配置
|
|
374
|
-
new_config = {
|
|
396
|
+
new_config: dict[str, str | bool | int | dict | None] = {
|
|
375
397
|
"base_url": base_url,
|
|
376
398
|
"model_name": model_name,
|
|
377
399
|
}
|
|
@@ -386,8 +408,12 @@ class UnifiedConfigManager:
|
|
|
386
408
|
new_config["decision_model_name"] = decision_model_name
|
|
387
409
|
if decision_api_key:
|
|
388
410
|
new_config["decision_api_key"] = decision_api_key
|
|
389
|
-
if
|
|
390
|
-
new_config["
|
|
411
|
+
if agent_type is not None:
|
|
412
|
+
new_config["agent_type"] = agent_type
|
|
413
|
+
if agent_config_params is not None:
|
|
414
|
+
new_config["agent_config_params"] = agent_config_params
|
|
415
|
+
if default_max_steps is not None:
|
|
416
|
+
new_config["default_max_steps"] = default_max_steps
|
|
391
417
|
|
|
392
418
|
# 合并模式:保留现有文件中未提供的字段
|
|
393
419
|
if merge_mode and self._config_path.exists():
|
|
@@ -402,7 +428,9 @@ class UnifiedConfigManager:
|
|
|
402
428
|
"decision_base_url",
|
|
403
429
|
"decision_model_name",
|
|
404
430
|
"decision_api_key",
|
|
405
|
-
"
|
|
431
|
+
"agent_type",
|
|
432
|
+
"agent_config_params",
|
|
433
|
+
"default_max_steps",
|
|
406
434
|
]
|
|
407
435
|
for key in preserve_keys:
|
|
408
436
|
if key not in new_config and key in existing:
|
|
@@ -491,7 +519,9 @@ class UnifiedConfigManager:
|
|
|
491
519
|
"decision_base_url",
|
|
492
520
|
"decision_model_name",
|
|
493
521
|
"decision_api_key",
|
|
494
|
-
"
|
|
522
|
+
"agent_type",
|
|
523
|
+
"agent_config_params",
|
|
524
|
+
"default_max_steps",
|
|
495
525
|
]
|
|
496
526
|
|
|
497
527
|
for key in config_keys:
|
|
@@ -658,7 +688,9 @@ class UnifiedConfigManager:
|
|
|
658
688
|
"decision_base_url": config.decision_base_url,
|
|
659
689
|
"decision_model_name": config.decision_model_name,
|
|
660
690
|
"decision_api_key": config.decision_api_key,
|
|
661
|
-
"
|
|
691
|
+
"agent_type": config.agent_type,
|
|
692
|
+
"agent_config_params": config.agent_config_params,
|
|
693
|
+
"default_max_steps": config.default_max_steps,
|
|
662
694
|
}
|
|
663
695
|
|
|
664
696
|
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""Device Protocol Adapter for phone_agent integration.
|
|
2
|
+
|
|
3
|
+
This module provides an adapter that bridges DeviceProtocol implementations
|
|
4
|
+
to the interface expected by phone_agent's DeviceFactory.
|
|
5
|
+
|
|
6
|
+
The adapter allows injecting any DeviceProtocol implementation (ADB, Mock, Remote)
|
|
7
|
+
into phone_agent without modifying the third-party code.
|
|
8
|
+
|
|
9
|
+
Example:
|
|
10
|
+
>>> from AutoGLM_GUI.device_adapter import inject_device_protocol
|
|
11
|
+
>>> from AutoGLM_GUI.devices import MockDevice, ADBDevice
|
|
12
|
+
>>>
|
|
13
|
+
>>> # For testing: inject mock device
|
|
14
|
+
>>> mock = MockDevice("mock_001", state_machine)
|
|
15
|
+
>>> inject_device_protocol(lambda _: mock)
|
|
16
|
+
>>>
|
|
17
|
+
>>> # For production: inject ADB device
|
|
18
|
+
>>> devices = {"phone_1": ADBDevice("emulator-5554")}
|
|
19
|
+
>>> inject_device_protocol(lambda device_id: devices[device_id])
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from typing import Callable
|
|
23
|
+
|
|
24
|
+
import phone_agent.device_factory as device_factory_module
|
|
25
|
+
from AutoGLM_GUI.device_protocol import DeviceProtocol, Screenshot
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class DeviceProtocolAdapter:
|
|
29
|
+
"""
|
|
30
|
+
Adapter that bridges DeviceProtocol to phone_agent's DeviceFactory interface.
|
|
31
|
+
|
|
32
|
+
This adapter wraps a DeviceProtocol getter function and exposes the same
|
|
33
|
+
interface as phone_agent's DeviceFactory, allowing seamless injection.
|
|
34
|
+
|
|
35
|
+
The adapter handles:
|
|
36
|
+
- Routing device operations to the correct DeviceProtocol instance
|
|
37
|
+
- Converting between DeviceProtocol and DeviceFactory method signatures
|
|
38
|
+
- Managing device_id parameters (phone_agent passes device_id to each method)
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
get_device: Callable[[str | None], DeviceProtocol],
|
|
44
|
+
default_device_id: str | None = None,
|
|
45
|
+
):
|
|
46
|
+
"""
|
|
47
|
+
Initialize the adapter.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
get_device: Function that returns a DeviceProtocol given a device_id.
|
|
51
|
+
If device_id is None, should return a default device.
|
|
52
|
+
default_device_id: Default device ID to use when None is passed.
|
|
53
|
+
"""
|
|
54
|
+
self._get_device = get_device
|
|
55
|
+
self._default_device_id = default_device_id
|
|
56
|
+
# For compatibility with code that checks device_type
|
|
57
|
+
self.device_type = "protocol_adapter"
|
|
58
|
+
|
|
59
|
+
def _device(self, device_id: str | None) -> DeviceProtocol:
|
|
60
|
+
"""Get device for the given ID."""
|
|
61
|
+
effective_id = device_id or self._default_device_id
|
|
62
|
+
return self._get_device(effective_id)
|
|
63
|
+
|
|
64
|
+
# === Screenshot ===
|
|
65
|
+
def get_screenshot(
|
|
66
|
+
self, device_id: str | None = None, timeout: int = 10
|
|
67
|
+
) -> Screenshot:
|
|
68
|
+
"""Get screenshot from device."""
|
|
69
|
+
return self._device(device_id).get_screenshot(timeout)
|
|
70
|
+
|
|
71
|
+
# === Input Operations ===
|
|
72
|
+
def tap(
|
|
73
|
+
self, x: int, y: int, device_id: str | None = None, delay: float | None = None
|
|
74
|
+
) -> None:
|
|
75
|
+
"""Tap at coordinates."""
|
|
76
|
+
self._device(device_id).tap(x, y, delay)
|
|
77
|
+
|
|
78
|
+
def double_tap(
|
|
79
|
+
self, x: int, y: int, device_id: str | None = None, delay: float | None = None
|
|
80
|
+
) -> None:
|
|
81
|
+
"""Double tap at coordinates."""
|
|
82
|
+
self._device(device_id).double_tap(x, y, delay)
|
|
83
|
+
|
|
84
|
+
def long_press(
|
|
85
|
+
self,
|
|
86
|
+
x: int,
|
|
87
|
+
y: int,
|
|
88
|
+
duration_ms: int = 3000,
|
|
89
|
+
device_id: str | None = None,
|
|
90
|
+
delay: float | None = None,
|
|
91
|
+
) -> None:
|
|
92
|
+
"""Long press at coordinates."""
|
|
93
|
+
self._device(device_id).long_press(x, y, duration_ms, delay)
|
|
94
|
+
|
|
95
|
+
def swipe(
|
|
96
|
+
self,
|
|
97
|
+
start_x: int,
|
|
98
|
+
start_y: int,
|
|
99
|
+
end_x: int,
|
|
100
|
+
end_y: int,
|
|
101
|
+
duration_ms: int | None = None,
|
|
102
|
+
device_id: str | None = None,
|
|
103
|
+
delay: float | None = None,
|
|
104
|
+
) -> None:
|
|
105
|
+
"""Swipe from start to end."""
|
|
106
|
+
self._device(device_id).swipe(
|
|
107
|
+
start_x, start_y, end_x, end_y, duration_ms, delay
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
def type_text(self, text: str, device_id: str | None = None) -> None:
|
|
111
|
+
"""Type text."""
|
|
112
|
+
self._device(device_id).type_text(text)
|
|
113
|
+
|
|
114
|
+
def clear_text(self, device_id: str | None = None) -> None:
|
|
115
|
+
"""Clear text."""
|
|
116
|
+
self._device(device_id).clear_text()
|
|
117
|
+
|
|
118
|
+
# === Navigation ===
|
|
119
|
+
def back(self, device_id: str | None = None, delay: float | None = None) -> None:
|
|
120
|
+
"""Press back button."""
|
|
121
|
+
self._device(device_id).back(delay)
|
|
122
|
+
|
|
123
|
+
def home(self, device_id: str | None = None, delay: float | None = None) -> None:
|
|
124
|
+
"""Press home button."""
|
|
125
|
+
self._device(device_id).home(delay)
|
|
126
|
+
|
|
127
|
+
def launch_app(
|
|
128
|
+
self, app_name: str, device_id: str | None = None, delay: float | None = None
|
|
129
|
+
) -> bool:
|
|
130
|
+
"""Launch an app."""
|
|
131
|
+
return self._device(device_id).launch_app(app_name, delay)
|
|
132
|
+
|
|
133
|
+
# === State Query ===
|
|
134
|
+
def get_current_app(self, device_id: str | None = None) -> str:
|
|
135
|
+
"""Get current app name."""
|
|
136
|
+
return self._device(device_id).get_current_app()
|
|
137
|
+
|
|
138
|
+
# === Keyboard Management ===
|
|
139
|
+
def detect_and_set_adb_keyboard(self, device_id: str | None = None) -> str:
|
|
140
|
+
"""Detect and set keyboard."""
|
|
141
|
+
return self._device(device_id).detect_and_set_adb_keyboard()
|
|
142
|
+
|
|
143
|
+
def restore_keyboard(self, ime: str, device_id: str | None = None) -> None:
|
|
144
|
+
"""Restore keyboard."""
|
|
145
|
+
self._device(device_id).restore_keyboard(ime)
|
|
146
|
+
|
|
147
|
+
# === Device Management ===
|
|
148
|
+
def list_devices(self) -> list[str]:
|
|
149
|
+
"""
|
|
150
|
+
List connected devices.
|
|
151
|
+
|
|
152
|
+
Note: This is a simplified implementation. For full device listing,
|
|
153
|
+
use ADBDeviceManager.list_devices() directly.
|
|
154
|
+
"""
|
|
155
|
+
# This is called by some parts of phone_agent
|
|
156
|
+
# Return the default device if available
|
|
157
|
+
if self._default_device_id:
|
|
158
|
+
return [self._default_device_id]
|
|
159
|
+
return []
|
|
160
|
+
|
|
161
|
+
def get_connection_class(self):
|
|
162
|
+
"""Not applicable for protocol adapter."""
|
|
163
|
+
raise NotImplementedError(
|
|
164
|
+
"Protocol adapter does not support get_connection_class. "
|
|
165
|
+
"Use ADBDeviceManager for connection management."
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
# Store original factory for restoration
|
|
170
|
+
_original_factory = None
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def inject_device_protocol(
|
|
174
|
+
get_device: Callable[[str | None], DeviceProtocol],
|
|
175
|
+
default_device_id: str | None = None,
|
|
176
|
+
) -> DeviceProtocolAdapter:
|
|
177
|
+
"""
|
|
178
|
+
Inject a DeviceProtocol implementation into phone_agent.
|
|
179
|
+
|
|
180
|
+
This replaces phone_agent's global _device_factory with an adapter
|
|
181
|
+
that routes all device operations through the provided DeviceProtocol.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
get_device: Function that returns a DeviceProtocol given a device_id.
|
|
185
|
+
default_device_id: Default device ID when None is passed.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
The adapter instance (for inspection or further configuration).
|
|
189
|
+
|
|
190
|
+
Example:
|
|
191
|
+
>>> # Single mock device
|
|
192
|
+
>>> mock = MockDevice("mock_001", state_machine)
|
|
193
|
+
>>> inject_device_protocol(lambda _: mock)
|
|
194
|
+
>>>
|
|
195
|
+
>>> # Multiple devices
|
|
196
|
+
>>> devices = {
|
|
197
|
+
... "phone_1": ADBDevice("emulator-5554"),
|
|
198
|
+
... "phone_2": RemoteDevice("phone_2", "http://remote:8080"),
|
|
199
|
+
... }
|
|
200
|
+
>>> inject_device_protocol(lambda did: devices.get(did, devices["phone_1"]))
|
|
201
|
+
"""
|
|
202
|
+
# TODO: 不应该依赖这种全部变量
|
|
203
|
+
global _original_factory
|
|
204
|
+
|
|
205
|
+
# Save original factory if not already saved
|
|
206
|
+
if _original_factory is None:
|
|
207
|
+
_original_factory = device_factory_module._device_factory
|
|
208
|
+
|
|
209
|
+
# Create and inject adapter
|
|
210
|
+
adapter = DeviceProtocolAdapter(get_device, default_device_id)
|
|
211
|
+
device_factory_module._device_factory = adapter
|
|
212
|
+
|
|
213
|
+
return adapter
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def restore_device_factory() -> None:
|
|
217
|
+
"""
|
|
218
|
+
Restore the original device factory.
|
|
219
|
+
|
|
220
|
+
Call this after testing to restore normal operation.
|
|
221
|
+
"""
|
|
222
|
+
global _original_factory
|
|
223
|
+
|
|
224
|
+
if _original_factory is not None:
|
|
225
|
+
device_factory_module._device_factory = _original_factory
|
|
226
|
+
_original_factory = None
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class DeviceProtocolContext:
|
|
230
|
+
"""
|
|
231
|
+
Context manager for temporarily injecting a DeviceProtocol.
|
|
232
|
+
|
|
233
|
+
Example:
|
|
234
|
+
>>> with DeviceProtocolContext(lambda _: mock_device):
|
|
235
|
+
... agent.run("test instruction")
|
|
236
|
+
>>> # Original factory is automatically restored
|
|
237
|
+
"""
|
|
238
|
+
|
|
239
|
+
def __init__(
|
|
240
|
+
self,
|
|
241
|
+
get_device: Callable[[str | None], DeviceProtocol],
|
|
242
|
+
default_device_id: str | None = None,
|
|
243
|
+
):
|
|
244
|
+
"""
|
|
245
|
+
Initialize context.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
get_device: Function that returns a DeviceProtocol given a device_id.
|
|
249
|
+
default_device_id: Default device ID when None is passed.
|
|
250
|
+
"""
|
|
251
|
+
self._get_device = get_device
|
|
252
|
+
self._default_device_id = default_device_id
|
|
253
|
+
self._original_factory = None
|
|
254
|
+
|
|
255
|
+
def __enter__(self) -> DeviceProtocolAdapter:
|
|
256
|
+
"""Enter context and inject adapter."""
|
|
257
|
+
self._original_factory = device_factory_module._device_factory
|
|
258
|
+
return inject_device_protocol(self._get_device, self._default_device_id)
|
|
259
|
+
|
|
260
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
261
|
+
"""Exit context and restore original factory."""
|
|
262
|
+
device_factory_module._device_factory = self._original_factory
|
|
263
|
+
return None
|