autoglm-gui 1.4.0__py3-none-any.whl → 1.5.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.
Files changed (120) hide show
  1. AutoGLM_GUI/__init__.py +11 -0
  2. AutoGLM_GUI/__main__.py +26 -8
  3. AutoGLM_GUI/actions/__init__.py +6 -0
  4. AutoGLM_GUI/actions/handler.py +196 -0
  5. AutoGLM_GUI/actions/types.py +15 -0
  6. AutoGLM_GUI/adb/__init__.py +53 -0
  7. AutoGLM_GUI/adb/apps.py +227 -0
  8. AutoGLM_GUI/adb/connection.py +323 -0
  9. AutoGLM_GUI/adb/device.py +171 -0
  10. AutoGLM_GUI/adb/input.py +67 -0
  11. AutoGLM_GUI/adb/screenshot.py +11 -0
  12. AutoGLM_GUI/adb/timing.py +167 -0
  13. AutoGLM_GUI/adb_plus/keyboard_installer.py +4 -2
  14. AutoGLM_GUI/adb_plus/qr_pair.py +8 -8
  15. AutoGLM_GUI/adb_plus/screenshot.py +22 -1
  16. AutoGLM_GUI/adb_plus/serial.py +38 -20
  17. AutoGLM_GUI/adb_plus/touch.py +4 -9
  18. AutoGLM_GUI/agents/__init__.py +51 -0
  19. AutoGLM_GUI/agents/events.py +19 -0
  20. AutoGLM_GUI/agents/factory.py +153 -0
  21. AutoGLM_GUI/agents/glm/__init__.py +7 -0
  22. AutoGLM_GUI/agents/glm/agent.py +292 -0
  23. AutoGLM_GUI/agents/glm/message_builder.py +81 -0
  24. AutoGLM_GUI/agents/glm/parser.py +110 -0
  25. AutoGLM_GUI/agents/glm/prompts_en.py +77 -0
  26. AutoGLM_GUI/agents/glm/prompts_zh.py +75 -0
  27. AutoGLM_GUI/agents/mai/__init__.py +28 -0
  28. AutoGLM_GUI/agents/mai/agent.py +405 -0
  29. AutoGLM_GUI/agents/mai/parser.py +254 -0
  30. AutoGLM_GUI/agents/mai/prompts.py +103 -0
  31. AutoGLM_GUI/agents/mai/traj_memory.py +91 -0
  32. AutoGLM_GUI/agents/protocols.py +27 -0
  33. AutoGLM_GUI/agents/stream_runner.py +188 -0
  34. AutoGLM_GUI/api/__init__.py +71 -11
  35. AutoGLM_GUI/api/agents.py +190 -229
  36. AutoGLM_GUI/api/control.py +9 -6
  37. AutoGLM_GUI/api/devices.py +112 -28
  38. AutoGLM_GUI/api/health.py +13 -0
  39. AutoGLM_GUI/api/history.py +78 -0
  40. AutoGLM_GUI/api/layered_agent.py +306 -181
  41. AutoGLM_GUI/api/mcp.py +11 -10
  42. AutoGLM_GUI/api/media.py +64 -1
  43. AutoGLM_GUI/api/scheduled_tasks.py +98 -0
  44. AutoGLM_GUI/api/version.py +23 -10
  45. AutoGLM_GUI/api/workflows.py +2 -1
  46. AutoGLM_GUI/config.py +72 -14
  47. AutoGLM_GUI/config_manager.py +98 -27
  48. AutoGLM_GUI/device_adapter.py +263 -0
  49. AutoGLM_GUI/device_manager.py +248 -29
  50. AutoGLM_GUI/device_protocol.py +266 -0
  51. AutoGLM_GUI/devices/__init__.py +49 -0
  52. AutoGLM_GUI/devices/adb_device.py +200 -0
  53. AutoGLM_GUI/devices/mock_device.py +185 -0
  54. AutoGLM_GUI/devices/remote_device.py +177 -0
  55. AutoGLM_GUI/exceptions.py +3 -3
  56. AutoGLM_GUI/history_manager.py +164 -0
  57. AutoGLM_GUI/i18n.py +81 -0
  58. AutoGLM_GUI/metrics.py +13 -20
  59. AutoGLM_GUI/model/__init__.py +5 -0
  60. AutoGLM_GUI/model/message_builder.py +69 -0
  61. AutoGLM_GUI/model/types.py +24 -0
  62. AutoGLM_GUI/models/__init__.py +10 -0
  63. AutoGLM_GUI/models/history.py +96 -0
  64. AutoGLM_GUI/models/scheduled_task.py +71 -0
  65. AutoGLM_GUI/parsers/__init__.py +22 -0
  66. AutoGLM_GUI/parsers/base.py +50 -0
  67. AutoGLM_GUI/parsers/phone_parser.py +58 -0
  68. AutoGLM_GUI/phone_agent_manager.py +118 -367
  69. AutoGLM_GUI/platform_utils.py +31 -2
  70. AutoGLM_GUI/prompt_config.py +15 -0
  71. AutoGLM_GUI/prompts/__init__.py +32 -0
  72. AutoGLM_GUI/scheduler_manager.py +304 -0
  73. AutoGLM_GUI/schemas.py +272 -63
  74. AutoGLM_GUI/scrcpy_stream.py +159 -37
  75. AutoGLM_GUI/server.py +3 -1
  76. AutoGLM_GUI/socketio_server.py +114 -29
  77. AutoGLM_GUI/state.py +10 -30
  78. AutoGLM_GUI/static/assets/{about-DeclntHg.js → about-BQm96DAl.js} +1 -1
  79. AutoGLM_GUI/static/assets/alert-dialog-B42XxGPR.js +1 -0
  80. AutoGLM_GUI/static/assets/chat-C0L2gQYG.js +129 -0
  81. AutoGLM_GUI/static/assets/circle-alert-D4rSJh37.js +1 -0
  82. AutoGLM_GUI/static/assets/dialog-DZ78cEcj.js +45 -0
  83. AutoGLM_GUI/static/assets/history-DFBv7TGc.js +1 -0
  84. AutoGLM_GUI/static/assets/index-Bzyv2yQ2.css +1 -0
  85. AutoGLM_GUI/static/assets/{index-zQ4KKDHt.js → index-CmZSnDqc.js} +1 -1
  86. AutoGLM_GUI/static/assets/index-CssG-3TH.js +11 -0
  87. AutoGLM_GUI/static/assets/label-BCUzE_nm.js +1 -0
  88. AutoGLM_GUI/static/assets/logs-eoFxn5of.js +1 -0
  89. AutoGLM_GUI/static/assets/popover-DLsuV5Sx.js +1 -0
  90. AutoGLM_GUI/static/assets/scheduled-tasks-MyqGJvy_.js +1 -0
  91. AutoGLM_GUI/static/assets/square-pen-zGWYrdfj.js +1 -0
  92. AutoGLM_GUI/static/assets/textarea-BX6y7uM5.js +1 -0
  93. AutoGLM_GUI/static/assets/workflows-CYFs6ssC.js +1 -0
  94. AutoGLM_GUI/static/index.html +2 -2
  95. AutoGLM_GUI/types.py +142 -0
  96. {autoglm_gui-1.4.0.dist-info → autoglm_gui-1.5.0.dist-info}/METADATA +178 -92
  97. autoglm_gui-1.5.0.dist-info/RECORD +157 -0
  98. mai_agent/base.py +137 -0
  99. mai_agent/mai_grounding_agent.py +263 -0
  100. mai_agent/mai_naivigation_agent.py +526 -0
  101. mai_agent/prompt.py +148 -0
  102. mai_agent/unified_memory.py +67 -0
  103. mai_agent/utils.py +73 -0
  104. AutoGLM_GUI/api/dual_model.py +0 -311
  105. AutoGLM_GUI/dual_model/__init__.py +0 -53
  106. AutoGLM_GUI/dual_model/decision_model.py +0 -664
  107. AutoGLM_GUI/dual_model/dual_agent.py +0 -917
  108. AutoGLM_GUI/dual_model/protocols.py +0 -354
  109. AutoGLM_GUI/dual_model/vision_model.py +0 -442
  110. AutoGLM_GUI/mai_ui_adapter/agent_wrapper.py +0 -291
  111. AutoGLM_GUI/phone_agent_patches.py +0 -146
  112. AutoGLM_GUI/static/assets/chat-Iut2yhSw.js +0 -125
  113. AutoGLM_GUI/static/assets/dialog-BfdcBs1x.js +0 -45
  114. AutoGLM_GUI/static/assets/index-5hCCwHA7.css +0 -1
  115. AutoGLM_GUI/static/assets/index-DHF1NZh0.js +0 -12
  116. AutoGLM_GUI/static/assets/workflows-xiplap-r.js +0 -1
  117. autoglm_gui-1.4.0.dist-info/RECORD +0 -100
  118. {autoglm_gui-1.4.0.dist-info → autoglm_gui-1.5.0.dist-info}/WHEEL +0 -0
  119. {autoglm_gui-1.4.0.dist-info → autoglm_gui-1.5.0.dist-info}/entry_points.txt +0 -0
  120. {autoglm_gui-1.4.0.dist-info → autoglm_gui-1.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -4,7 +4,8 @@ from __future__ import annotations
4
4
 
5
5
  import asyncio
6
6
  import time
7
- from typing import Any
7
+
8
+ from typing_extensions import NotRequired, TypedDict
8
9
 
9
10
  import socketio
10
11
 
@@ -12,13 +13,26 @@ from AutoGLM_GUI.logger import logger
12
13
  from AutoGLM_GUI.scrcpy_protocol import ScrcpyMediaStreamPacket
13
14
  from AutoGLM_GUI.scrcpy_stream import ScrcpyStreamer
14
15
 
16
+
17
+ class VideoPacketPayload(TypedDict):
18
+ type: str
19
+ data: bytes
20
+ timestamp: int
21
+ keyframe: NotRequired[bool | None]
22
+ pts: NotRequired[int | None]
23
+
24
+
15
25
  sio = socketio.AsyncServer(
16
26
  async_mode="asgi",
17
27
  cors_allowed_origins="*",
28
+ server_kwargs={"socketio_path": "/socket.io"},
18
29
  )
19
30
 
20
31
  _socket_streamers: dict[str, ScrcpyStreamer] = {}
21
32
  _stream_tasks: dict[str, asyncio.Task] = {}
33
+ _device_locks: dict[
34
+ str, asyncio.Lock
35
+ ] = {} # Lock per device to prevent concurrent connections
22
36
 
23
37
 
24
38
  async def _stop_stream_for_sid(sid: str) -> None:
@@ -31,6 +45,46 @@ async def _stop_stream_for_sid(sid: str) -> None:
31
45
  streamer.stop()
32
46
 
33
47
 
48
+ def _classify_error(exc: Exception) -> dict:
49
+ """Classify error and return user-friendly message."""
50
+ error_str = str(exc)
51
+
52
+ if "Address already in use" in error_str or (
53
+ "Port" in error_str and "occupied" in error_str
54
+ ):
55
+ return {
56
+ "message": "端口冲突,视频流端口仍被占用。通常会自动解决,如果持续出现请重启应用。",
57
+ "type": "port_conflict",
58
+ "technical_details": error_str,
59
+ }
60
+ elif "Device" in error_str and (
61
+ "not available" in error_str or "not found" in error_str
62
+ ):
63
+ return {
64
+ "message": "设备无响应,请检查 USB/WiFi 连接。",
65
+ "type": "device_offline",
66
+ "technical_details": error_str,
67
+ }
68
+ elif "timeout" in error_str.lower() or "timed out" in error_str.lower():
69
+ return {
70
+ "message": "连接超时,请检查设备连接后重试。",
71
+ "type": "timeout",
72
+ "technical_details": error_str,
73
+ }
74
+ elif "Failed to connect" in error_str:
75
+ return {
76
+ "message": "无法连接到 scrcpy 服务器,请检查设备连接。",
77
+ "type": "connection_failed",
78
+ "technical_details": error_str,
79
+ }
80
+ else:
81
+ return {
82
+ "message": error_str,
83
+ "type": "unknown",
84
+ "technical_details": error_str,
85
+ }
86
+
87
+
34
88
  def stop_streamers(device_id: str | None = None) -> None:
35
89
  """Stop active scrcpy streamers (all or by device)."""
36
90
  sids = list(_socket_streamers.keys())
@@ -64,8 +118,8 @@ async def _stream_packets(sid: str, streamer: ScrcpyStreamer) -> None:
64
118
  await _stop_stream_for_sid(sid)
65
119
 
66
120
 
67
- def _packet_to_payload(packet: ScrcpyMediaStreamPacket) -> dict[str, Any]:
68
- payload: dict[str, Any] = {
121
+ def _packet_to_payload(packet: ScrcpyMediaStreamPacket) -> VideoPacketPayload:
122
+ payload: VideoPacketPayload = {
69
123
  "type": packet.type,
70
124
  "data": packet.data,
71
125
  "timestamp": int(time.time() * 1000),
@@ -87,39 +141,70 @@ async def disconnect(sid: str) -> None:
87
141
  await _stop_stream_for_sid(sid)
88
142
 
89
143
 
90
- @sio.on("connect-device")
144
+ @sio.on("connect-device") # type: ignore[misc]
91
145
  async def connect_device(sid: str, data: dict | None) -> None:
92
146
  payload = data or {}
93
147
  device_id = payload.get("device_id") or payload.get("deviceId")
148
+ if not device_id:
149
+ await sio.emit(
150
+ "error",
151
+ {"message": "Device ID is required", "type": "invalid_request"},
152
+ to=sid,
153
+ )
154
+ return
155
+
94
156
  max_size = int(payload.get("maxSize") or 1280)
95
157
  bit_rate = int(payload.get("bitRate") or 4_000_000)
96
158
 
159
+ # Stop any existing stream for this sid
97
160
  await _stop_stream_for_sid(sid)
98
161
 
99
- streamer = ScrcpyStreamer(
100
- device_id=device_id,
101
- max_size=max_size,
102
- bit_rate=bit_rate,
103
- )
104
-
105
- try:
106
- await streamer.start()
107
- metadata = await streamer.read_video_metadata()
108
- await sio.emit(
109
- "video-metadata",
110
- {
111
- "deviceName": metadata.device_name,
112
- "width": metadata.width,
113
- "height": metadata.height,
114
- "codec": metadata.codec,
115
- },
116
- to=sid,
162
+ # Get or create a lock for this device
163
+ if device_id not in _device_locks:
164
+ _device_locks[device_id] = asyncio.Lock()
165
+
166
+ device_lock = _device_locks[device_id]
167
+
168
+ # Acquire lock to prevent concurrent connections to the same device
169
+ async with device_lock:
170
+ logger.debug(f"Acquired device lock for {device_id}, sid: {sid}")
171
+
172
+ # Stop any existing streams for the same device (from other sids)
173
+ sids_to_stop = [
174
+ s
175
+ for s, streamer in _socket_streamers.items()
176
+ if s != sid and streamer.device_id == device_id
177
+ ]
178
+ for s in sids_to_stop:
179
+ logger.info(f"Stopping existing stream for device {device_id} from sid {s}")
180
+ await _stop_stream_for_sid(s)
181
+
182
+ streamer = ScrcpyStreamer(
183
+ device_id=device_id,
184
+ max_size=max_size,
185
+ bit_rate=bit_rate,
117
186
  )
118
- except Exception as exc:
119
- streamer.stop()
120
- logger.exception("Failed to start scrcpy stream: %s", exc)
121
- await sio.emit("error", {"message": str(exc)}, to=sid)
122
- return
123
187
 
124
- _socket_streamers[sid] = streamer
125
- _stream_tasks[sid] = asyncio.create_task(_stream_packets(sid, streamer))
188
+ try:
189
+ await streamer.start() # ScrcpyStreamer has built-in retry logic
190
+ metadata = await streamer.read_video_metadata()
191
+ await sio.emit(
192
+ "video-metadata",
193
+ {
194
+ "deviceName": metadata.device_name,
195
+ "width": metadata.width,
196
+ "height": metadata.height,
197
+ "codec": metadata.codec,
198
+ },
199
+ to=sid,
200
+ )
201
+
202
+ _socket_streamers[sid] = streamer
203
+ _stream_tasks[sid] = asyncio.create_task(_stream_packets(sid, streamer))
204
+
205
+ except Exception as exc:
206
+ streamer.stop()
207
+ logger.exception("Failed to start scrcpy stream: %s", exc)
208
+ # Use unified error classification
209
+ error_info = _classify_error(exc)
210
+ await sio.emit("error", error_info, to=sid)
AutoGLM_GUI/state.py CHANGED
@@ -1,4 +1,13 @@
1
- """Shared runtime state for the AutoGLM-GUI API."""
1
+ """Shared runtime state for the AutoGLM-GUI API.
2
+
3
+ NOTE: Agent instances and configurations are now managed internally
4
+ by PhoneAgentManager singleton. This module only contains:
5
+ - scrcpy_streamers: Video streaming state per device
6
+ - scrcpy_locks: Async locks for stream management
7
+ - non_blocking_takeover: Takeover callback handler
8
+
9
+ See PhoneAgentManager (phone_agent_manager.py) for agent lifecycle management.
10
+ """
2
11
 
3
12
  from __future__ import annotations
4
13
 
@@ -6,38 +15,9 @@ import asyncio
6
15
  from typing import TYPE_CHECKING
7
16
 
8
17
  from AutoGLM_GUI.logger import logger
9
- from phone_agent.agent import AgentConfig
10
- from phone_agent.model import ModelConfig
11
18
 
12
19
  if TYPE_CHECKING:
13
20
  from AutoGLM_GUI.scrcpy_stream import ScrcpyStreamer
14
- from phone_agent import PhoneAgent
15
-
16
- # Agent instances keyed by device_id
17
- #
18
- # IMPORTANT: Managed by PhoneAgentManager (AutoGLM_GUI/phone_agent_manager.py)
19
- # - Do NOT directly modify these dictionaries
20
- # - Use PhoneAgentManager.get_instance() for all agent operations
21
- #
22
- # device_id changes when connection method changes
23
- # (e.g., USB "ABC123" → WiFi "192.168.1.100:5555")
24
- #
25
- # This means the same physical device may have different device_ids:
26
- # - USB connection: device_id = hardware serial (e.g., "ABC123DEF")
27
- # - WiFi connection: device_id = IP:port (e.g., "192.168.1.100:5555")
28
- # - mDNS connection: device_id = service name (e.g., "adb-ABC123._adb-tls-connect._tcp")
29
- #
30
- # DeviceManager tracks devices by hardware serial and maintains
31
- # device_id ↔ serial mapping. Use PhoneAgentManager.find_agent_by_serial()
32
- # to find agents when device_id changes.
33
- #
34
- # See CLAUDE.md "Device Identification" section for details.
35
- agents: dict[str, "PhoneAgent"] = {}
36
-
37
- # Cached configs to rebuild agents on reset
38
- # Keyed by device_id (same semantics as agents dict)
39
- # IMPORTANT: Managed by PhoneAgentManager - do NOT modify directly
40
- agent_configs: dict[str, tuple[ModelConfig, AgentConfig]] = {}
41
21
 
42
22
  # Scrcpy streaming per device
43
23
  scrcpy_streamers: dict[str, "ScrcpyStreamer"] = {}
@@ -1 +1 @@
1
- import{j as o}from"./index-DHF1NZh0.js";function t(){return o.jsx("div",{className:"p-2",children:o.jsx("h3",{children:"About"})})}export{t as component};
1
+ import{j as o}from"./index-CssG-3TH.js";function t(){return o.jsx("div",{className:"p-2",children:o.jsx("h3",{children:"About"})})}export{t as component};
@@ -0,0 +1 @@
1
+ import{o as u,r as o,j as a,b as r,B as d}from"./index-CssG-3TH.js";import{P as g,b as x,c as f,d as m}from"./popover-DLsuV5Sx.js";import{D as p,d as h,e as w,f as j,g as D}from"./dialog-DZ78cEcj.js";const N=[["path",{d:"M20 6 9 17l-5-5",key:"1gmf2c"}]],b=u("check",N),c=o.createContext(void 0),P=({value:t="",onValueChange:e,children:s})=>{const[n,l]=o.useState(!1);return a.jsx(c.Provider,{value:{value:t,onValueChange:e||(()=>{}),open:n,setOpen:l},children:a.jsx(g,{open:n,onOpenChange:l,children:s})})},C=o.forwardRef(({className:t,children:e,...s},n)=>{if(!o.useContext(c))throw new Error("SelectTrigger must be used within Select");return a.jsx(x,{asChild:!0,children:a.jsxs("button",{ref:n,className:r("flex h-10 w-full items-center justify-between rounded-md border border-slate-200 bg-white px-3 py-2 text-sm ring-offset-white placeholder:text-slate-500 focus:outline-none focus:ring-2 focus:ring-slate-950 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-800 dark:bg-slate-950 dark:ring-offset-slate-950 dark:placeholder:text-slate-400 dark:focus:ring-slate-300",t),...s,children:[e,a.jsx(f,{className:"h-4 w-4 opacity-50"})]})})});C.displayName="SelectTrigger";const V=({placeholder:t})=>{const e=o.useContext(c);if(!e)throw new Error("SelectValue must be used within Select");return a.jsx("span",{className:e.value?"":"text-slate-500",children:e.value||t})},I=({children:t,className:e})=>a.jsx(m,{className:r("w-[var(--radix-popover-trigger-width)] p-1",e),children:t}),O=({value:t,children:e,disabled:s,className:n})=>{const l=o.useContext(c);if(!l)throw new Error("SelectItem must be used within Select");const i=l.value===t;return a.jsxs("div",{role:"option","aria-selected":i,className:r("relative flex w-full cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none hover:bg-slate-100 focus:bg-slate-100 dark:hover:bg-slate-800 dark:focus:bg-slate-800",s&&"pointer-events-none opacity-50",n),onClick:()=>{s||(l.onValueChange(t),l.setOpen(!1))},children:[a.jsx("span",{className:"absolute left-2 flex h-3.5 w-3.5 items-center justify-center",children:i&&a.jsx(b,{className:"h-4 w-4"})}),e]})},B=({open:t,onOpenChange:e,children:s})=>a.jsx(p,{open:t,onOpenChange:e,children:s}),v=o.forwardRef(({className:t,...e},s)=>a.jsx(h,{ref:s,className:r("sm:max-w-[425px]",t),...e}));v.displayName="AlertDialogContent";const F=({className:t,...e})=>a.jsx(w,{className:r(t),...e}),H=({className:t,...e})=>a.jsx(D,{className:r(t),...e}),A=o.forwardRef(({className:t,...e},s)=>a.jsx(j,{ref:s,className:r(t),...e}));A.displayName="AlertDialogTitle";const S=o.forwardRef(({className:t,...e},s)=>a.jsx("p",{ref:s,className:r("text-sm text-slate-500 dark:text-slate-400",t),...e}));S.displayName="AlertDialogDescription";const y=o.forwardRef(({className:t,...e},s)=>a.jsx(d,{ref:s,className:r(t),...e}));y.displayName="AlertDialogAction";const k=o.forwardRef(({className:t,...e},s)=>a.jsx(d,{ref:s,variant:"outline",className:r(t),...e}));k.displayName="AlertDialogCancel";export{B as A,P as S,C as a,V as b,I as c,O as d,v as e,F as f,A as g,S as h,H as i,k as j,y as k};