autoglm-gui 0.3.1__py3-none-any.whl → 0.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.
Files changed (34) hide show
  1. AutoGLM_GUI/__main__.py +6 -2
  2. AutoGLM_GUI/adb_plus/__init__.py +8 -1
  3. AutoGLM_GUI/adb_plus/screenshot.py +1 -4
  4. AutoGLM_GUI/adb_plus/touch.py +92 -0
  5. AutoGLM_GUI/api/__init__.py +66 -0
  6. AutoGLM_GUI/api/agents.py +231 -0
  7. AutoGLM_GUI/api/control.py +111 -0
  8. AutoGLM_GUI/api/devices.py +29 -0
  9. AutoGLM_GUI/api/media.py +163 -0
  10. AutoGLM_GUI/schemas.py +127 -0
  11. AutoGLM_GUI/scrcpy_stream.py +65 -28
  12. AutoGLM_GUI/server.py +2 -491
  13. AutoGLM_GUI/state.py +33 -0
  14. AutoGLM_GUI/static/assets/{about-C71SI8ZQ.js → about-gHEqXVMQ.js} +1 -1
  15. AutoGLM_GUI/static/assets/chat-6a-qTECg.js +25 -0
  16. AutoGLM_GUI/static/assets/index-C8KPPfxe.js +10 -0
  17. AutoGLM_GUI/static/assets/index-D2-3f619.css +1 -0
  18. AutoGLM_GUI/static/assets/{index-DUCan6m6.js → index-DgzeSwgt.js} +1 -1
  19. AutoGLM_GUI/static/index.html +2 -2
  20. AutoGLM_GUI/version.py +8 -0
  21. {autoglm_gui-0.3.1.dist-info → autoglm_gui-0.4.1.dist-info}/METADATA +64 -9
  22. autoglm_gui-0.4.1.dist-info/RECORD +44 -0
  23. phone_agent/adb/connection.py +0 -1
  24. phone_agent/adb/device.py +0 -2
  25. phone_agent/adb/input.py +0 -1
  26. phone_agent/adb/screenshot.py +0 -1
  27. phone_agent/agent.py +1 -1
  28. AutoGLM_GUI/static/assets/chat-C6WtEfKW.js +0 -14
  29. AutoGLM_GUI/static/assets/index-Dd1xMRCa.css +0 -1
  30. AutoGLM_GUI/static/assets/index-RqglIZxV.js +0 -10
  31. autoglm_gui-0.3.1.dist-info/RECORD +0 -35
  32. {autoglm_gui-0.3.1.dist-info → autoglm_gui-0.4.1.dist-info}/WHEEL +0 -0
  33. {autoglm_gui-0.3.1.dist-info → autoglm_gui-0.4.1.dist-info}/entry_points.txt +0 -0
  34. {autoglm_gui-0.3.1.dist-info → autoglm_gui-0.4.1.dist-info}/licenses/LICENSE +0 -0
AutoGLM_GUI/__main__.py CHANGED
@@ -12,7 +12,9 @@ import webbrowser
12
12
  DEFAULT_MODEL_NAME = "autoglm-phone-9b"
13
13
 
14
14
 
15
- def find_available_port(start_port: int = 8000, max_attempts: int = 100, host: str = "127.0.0.1") -> int:
15
+ def find_available_port(
16
+ start_port: int = 8000, max_attempts: int = 100, host: str = "127.0.0.1"
17
+ ) -> int:
16
18
  """Find an available port starting from start_port.
17
19
 
18
20
  Args:
@@ -52,7 +54,9 @@ def open_browser(host: str, port: int, delay: float = 1.5) -> None:
52
54
 
53
55
  def _open():
54
56
  time.sleep(delay)
55
- url = f"http://127.0.0.1:{port}" if host == "0.0.0.0" else f"http://{host}:{port}"
57
+ url = (
58
+ f"http://127.0.0.1:{port}" if host == "0.0.0.0" else f"http://{host}:{port}"
59
+ )
56
60
  try:
57
61
  webbrowser.open(url)
58
62
  except Exception as e:
@@ -1,5 +1,12 @@
1
1
  """Lightweight ADB helpers with a more robust screenshot implementation."""
2
2
 
3
3
  from .screenshot import Screenshot, capture_screenshot
4
+ from .touch import touch_down, touch_move, touch_up
4
5
 
5
- __all__ = ["Screenshot", "capture_screenshot"]
6
+ __all__ = [
7
+ "Screenshot",
8
+ "capture_screenshot",
9
+ "touch_down",
10
+ "touch_move",
11
+ "touch_up",
12
+ ]
@@ -10,7 +10,6 @@ import base64
10
10
  import subprocess
11
11
  from dataclasses import dataclass
12
12
  from io import BytesIO
13
- from typing import Iterable
14
13
 
15
14
  from PIL import Image
16
15
 
@@ -72,9 +71,7 @@ def capture_screenshot(
72
71
  return _fallback_screenshot()
73
72
 
74
73
 
75
- def _try_capture(
76
- device_id: str | None, adb_path: str, timeout: int
77
- ) -> bytes | None:
74
+ def _try_capture(device_id: str | None, adb_path: str, timeout: int) -> bytes | None:
78
75
  """Run exec-out screencap and return raw bytes or None on failure."""
79
76
  cmd: list[str | bytes] = [adb_path]
80
77
  if device_id:
@@ -0,0 +1,92 @@
1
+ """Touch control utilities using ADB motion events for real-time dragging."""
2
+
3
+ import subprocess
4
+ import time
5
+
6
+
7
+ def _get_adb_prefix(device_id: str | None, adb_path: str = "adb") -> list[str]:
8
+ """Get ADB command prefix with optional device specifier."""
9
+ if device_id:
10
+ return [adb_path, "-s", device_id]
11
+ return [adb_path]
12
+
13
+
14
+ def touch_down(
15
+ x: int,
16
+ y: int,
17
+ device_id: str | None = None,
18
+ delay: float = 0.0,
19
+ adb_path: str = "adb",
20
+ ) -> None:
21
+ """
22
+ Send touch DOWN event at specified coordinates.
23
+
24
+ Args:
25
+ x: X coordinate.
26
+ y: Y coordinate.
27
+ device_id: Optional ADB device ID.
28
+ delay: Delay in seconds after event (default: 0.0 for real-time).
29
+ adb_path: Path to adb binary.
30
+ """
31
+ adb_prefix = _get_adb_prefix(device_id, adb_path)
32
+
33
+ subprocess.run(
34
+ adb_prefix + ["shell", "input", "motionevent", "DOWN", str(x), str(y)],
35
+ capture_output=True,
36
+ )
37
+ if delay > 0:
38
+ time.sleep(delay)
39
+
40
+
41
+ def touch_move(
42
+ x: int,
43
+ y: int,
44
+ device_id: str | None = None,
45
+ delay: float = 0.0,
46
+ adb_path: str = "adb",
47
+ ) -> None:
48
+ """
49
+ Send touch MOVE event at specified coordinates.
50
+
51
+ Args:
52
+ x: X coordinate.
53
+ y: Y coordinate.
54
+ device_id: Optional ADB device ID.
55
+ delay: Delay in seconds after event (default: 0.0 for real-time).
56
+ adb_path: Path to adb binary.
57
+ """
58
+ adb_prefix = _get_adb_prefix(device_id, adb_path)
59
+
60
+ subprocess.run(
61
+ adb_prefix + ["shell", "input", "motionevent", "MOVE", str(x), str(y)],
62
+ capture_output=True,
63
+ )
64
+ if delay > 0:
65
+ time.sleep(delay)
66
+
67
+
68
+ def touch_up(
69
+ x: int,
70
+ y: int,
71
+ device_id: str | None = None,
72
+ delay: float = 0.0,
73
+ adb_path: str = "adb",
74
+ ) -> None:
75
+ """
76
+ Send touch UP event at specified coordinates.
77
+
78
+ Args:
79
+ x: X coordinate.
80
+ y: Y coordinate.
81
+ device_id: Optional ADB device ID.
82
+ delay: Delay in seconds after event (default: 0.0 for real-time).
83
+ adb_path: Path to adb binary.
84
+ """
85
+ adb_prefix = _get_adb_prefix(device_id, adb_path)
86
+
87
+ subprocess.run(
88
+ adb_prefix + ["shell", "input", "motionevent", "UP", str(x), str(y)],
89
+ capture_output=True,
90
+ )
91
+ if delay > 0:
92
+ time.sleep(delay)
@@ -0,0 +1,66 @@
1
+ """FastAPI application factory and route registration."""
2
+
3
+ from importlib.resources import files
4
+ from pathlib import Path
5
+
6
+ from fastapi import FastAPI
7
+ from fastapi.middleware.cors import CORSMiddleware
8
+ from fastapi.responses import FileResponse
9
+ from fastapi.staticfiles import StaticFiles
10
+
11
+ from AutoGLM_GUI.version import APP_VERSION
12
+
13
+ from . import agents, control, devices, media
14
+
15
+
16
+ def _get_static_dir() -> Path | None:
17
+ """Locate packaged static assets."""
18
+ try:
19
+ static_dir = files("AutoGLM_GUI").joinpath("static")
20
+ if hasattr(static_dir, "_path"):
21
+ path = Path(str(static_dir))
22
+ if path.exists():
23
+ return path
24
+ path = Path(str(static_dir))
25
+ if path.exists():
26
+ return path
27
+ except (TypeError, FileNotFoundError):
28
+ pass
29
+
30
+ return None
31
+
32
+
33
+ def create_app() -> FastAPI:
34
+ """Build the FastAPI app with routers and static assets."""
35
+ app = FastAPI(title="AutoGLM-GUI API", version=APP_VERSION)
36
+
37
+ app.add_middleware(
38
+ CORSMiddleware,
39
+ allow_origins=["http://localhost:3000"],
40
+ allow_credentials=True,
41
+ allow_methods=["*"],
42
+ allow_headers=["*"],
43
+ )
44
+
45
+ app.include_router(agents.router)
46
+ app.include_router(devices.router)
47
+ app.include_router(control.router)
48
+ app.include_router(media.router)
49
+
50
+ static_dir = _get_static_dir()
51
+ if static_dir is not None and static_dir.exists():
52
+ assets_dir = static_dir / "assets"
53
+ if assets_dir.exists():
54
+ app.mount("/assets", StaticFiles(directory=assets_dir), name="assets")
55
+
56
+ @app.get("/{full_path:path}")
57
+ async def serve_spa(full_path: str) -> FileResponse:
58
+ file_path = static_dir / full_path
59
+ if file_path.is_file():
60
+ return FileResponse(file_path)
61
+ return FileResponse(static_dir / "index.html")
62
+
63
+ return app
64
+
65
+
66
+ app = create_app()
@@ -0,0 +1,231 @@
1
+ """Agent lifecycle and chat routes."""
2
+
3
+ import json
4
+
5
+ from fastapi import APIRouter, HTTPException
6
+ from fastapi.responses import StreamingResponse
7
+ from phone_agent import PhoneAgent
8
+ from phone_agent.agent import AgentConfig
9
+ from phone_agent.model import ModelConfig
10
+
11
+ from AutoGLM_GUI.schemas import (
12
+ APIAgentConfig,
13
+ APIModelConfig,
14
+ ChatRequest,
15
+ ChatResponse,
16
+ InitRequest,
17
+ ResetRequest,
18
+ StatusResponse,
19
+ )
20
+ from AutoGLM_GUI.state import (
21
+ DEFAULT_API_KEY,
22
+ DEFAULT_BASE_URL,
23
+ DEFAULT_MODEL_NAME,
24
+ agent_configs,
25
+ agents,
26
+ non_blocking_takeover,
27
+ )
28
+ from AutoGLM_GUI.version import APP_VERSION
29
+
30
+ router = APIRouter()
31
+
32
+
33
+ @router.post("/api/init")
34
+ def init_agent(request: InitRequest) -> dict:
35
+ """初始化 PhoneAgent(多设备支持)。"""
36
+ req_model_config = request.model or APIModelConfig()
37
+ req_agent_config = request.agent or APIAgentConfig()
38
+
39
+ device_id = req_agent_config.device_id
40
+ if not device_id:
41
+ raise HTTPException(
42
+ status_code=400, detail="device_id is required in agent_config"
43
+ )
44
+
45
+ base_url = req_model_config.base_url or DEFAULT_BASE_URL
46
+ api_key = req_model_config.api_key or DEFAULT_API_KEY
47
+ model_name = req_model_config.model_name or DEFAULT_MODEL_NAME
48
+
49
+ if not base_url:
50
+ raise HTTPException(
51
+ status_code=400, detail="base_url is required (in model_config or env)"
52
+ )
53
+
54
+ model_config = ModelConfig(
55
+ base_url=base_url,
56
+ api_key=api_key,
57
+ model_name=model_name,
58
+ max_tokens=req_model_config.max_tokens,
59
+ temperature=req_model_config.temperature,
60
+ top_p=req_model_config.top_p,
61
+ frequency_penalty=req_model_config.frequency_penalty,
62
+ )
63
+
64
+ agent_config = AgentConfig(
65
+ max_steps=req_agent_config.max_steps,
66
+ device_id=device_id,
67
+ lang=req_agent_config.lang,
68
+ system_prompt=req_agent_config.system_prompt,
69
+ verbose=req_agent_config.verbose,
70
+ )
71
+
72
+ agents[device_id] = PhoneAgent(
73
+ model_config=model_config,
74
+ agent_config=agent_config,
75
+ takeover_callback=non_blocking_takeover,
76
+ )
77
+
78
+ agent_configs[device_id] = (model_config, agent_config)
79
+
80
+ return {
81
+ "success": True,
82
+ "device_id": device_id,
83
+ "message": f"Agent initialized for device {device_id}",
84
+ }
85
+
86
+
87
+ @router.post("/api/chat", response_model=ChatResponse)
88
+ def chat(request: ChatRequest) -> ChatResponse:
89
+ """发送任务给 Agent 并执行。"""
90
+ device_id = request.device_id
91
+ if device_id not in agents:
92
+ raise HTTPException(
93
+ status_code=400, detail="Agent not initialized. Call /api/init first."
94
+ )
95
+
96
+ agent = agents[device_id]
97
+
98
+ try:
99
+ result = agent.run(request.message)
100
+ steps = agent.step_count
101
+ agent.reset()
102
+
103
+ return ChatResponse(result=result, steps=steps, success=True)
104
+ except Exception as e:
105
+ return ChatResponse(result=str(e), steps=0, success=False)
106
+
107
+
108
+ @router.post("/api/chat/stream")
109
+ def chat_stream(request: ChatRequest):
110
+ """发送任务给 Agent 并实时推送执行进度(SSE,多设备支持)。"""
111
+ device_id = request.device_id
112
+
113
+ if device_id not in agents:
114
+ raise HTTPException(
115
+ status_code=400,
116
+ detail=f"Device {device_id} not initialized. Call /api/init first.",
117
+ )
118
+
119
+ agent = agents[device_id]
120
+
121
+ def event_generator():
122
+ """SSE 事件生成器"""
123
+ try:
124
+ step_result = agent.step(request.message)
125
+ while True:
126
+ event_data = {
127
+ "type": "step",
128
+ "step": agent.step_count,
129
+ "thinking": step_result.thinking,
130
+ "action": step_result.action,
131
+ "success": step_result.success,
132
+ "finished": step_result.finished,
133
+ }
134
+
135
+ yield "event: step\n"
136
+ yield f"data: {json.dumps(event_data, ensure_ascii=False)}\n\n"
137
+
138
+ if step_result.finished:
139
+ done_data = {
140
+ "type": "done",
141
+ "message": step_result.message,
142
+ "steps": agent.step_count,
143
+ "success": step_result.success,
144
+ }
145
+ yield "event: done\n"
146
+ yield f"data: {json.dumps(done_data, ensure_ascii=False)}\n\n"
147
+ break
148
+
149
+ if agent.step_count >= agent.agent_config.max_steps:
150
+ done_data = {
151
+ "type": "done",
152
+ "message": "Max steps reached",
153
+ "steps": agent.step_count,
154
+ "success": step_result.success,
155
+ }
156
+ yield "event: done\n"
157
+ yield f"data: {json.dumps(done_data, ensure_ascii=False)}\n\n"
158
+ break
159
+
160
+ step_result = agent.step()
161
+
162
+ agent.reset()
163
+
164
+ except Exception as e:
165
+ error_data = {
166
+ "type": "error",
167
+ "message": str(e),
168
+ }
169
+ yield "event: error\n"
170
+ yield f"data: {json.dumps(error_data, ensure_ascii=False)}\n\n"
171
+
172
+ return StreamingResponse(
173
+ event_generator(),
174
+ media_type="text/event-stream",
175
+ headers={
176
+ "Cache-Control": "no-cache",
177
+ "Connection": "keep-alive",
178
+ "X-Accel-Buffering": "no",
179
+ },
180
+ )
181
+
182
+
183
+ @router.get("/api/status", response_model=StatusResponse)
184
+ def get_status(device_id: str | None = None) -> StatusResponse:
185
+ """获取 Agent 状态和版本信息(多设备支持)。"""
186
+ if device_id is None:
187
+ return StatusResponse(
188
+ version=APP_VERSION,
189
+ initialized=len(agents) > 0,
190
+ step_count=0,
191
+ )
192
+
193
+ if device_id not in agents:
194
+ return StatusResponse(
195
+ version=APP_VERSION,
196
+ initialized=False,
197
+ step_count=0,
198
+ )
199
+
200
+ agent = agents[device_id]
201
+ return StatusResponse(
202
+ version=APP_VERSION,
203
+ initialized=True,
204
+ step_count=agent.step_count,
205
+ )
206
+
207
+
208
+ @router.post("/api/reset")
209
+ def reset_agent(request: ResetRequest) -> dict:
210
+ """重置 Agent 状态(多设备支持)。"""
211
+ device_id = request.device_id
212
+
213
+ if device_id not in agents:
214
+ raise HTTPException(status_code=404, detail=f"Device {device_id} not found")
215
+
216
+ agent = agents[device_id]
217
+ agent.reset()
218
+
219
+ if device_id in agent_configs:
220
+ model_config, agent_config = agent_configs[device_id]
221
+ agents[device_id] = PhoneAgent(
222
+ model_config=model_config,
223
+ agent_config=agent_config,
224
+ takeover_callback=non_blocking_takeover,
225
+ )
226
+
227
+ return {
228
+ "success": True,
229
+ "device_id": device_id,
230
+ "message": f"Agent reset for device {device_id}",
231
+ }
@@ -0,0 +1,111 @@
1
+ """Device control routes (tap/swipe/touch)."""
2
+
3
+ from fastapi import APIRouter
4
+
5
+ from AutoGLM_GUI.schemas import (
6
+ SwipeRequest,
7
+ SwipeResponse,
8
+ TapRequest,
9
+ TapResponse,
10
+ TouchDownRequest,
11
+ TouchDownResponse,
12
+ TouchMoveRequest,
13
+ TouchMoveResponse,
14
+ TouchUpRequest,
15
+ TouchUpResponse,
16
+ )
17
+
18
+ router = APIRouter()
19
+
20
+
21
+ @router.post("/api/control/tap", response_model=TapResponse)
22
+ def control_tap(request: TapRequest) -> TapResponse:
23
+ """Execute tap at specified device coordinates."""
24
+ try:
25
+ from phone_agent.adb import tap
26
+
27
+ tap(
28
+ x=request.x,
29
+ y=request.y,
30
+ device_id=request.device_id,
31
+ delay=request.delay,
32
+ )
33
+
34
+ return TapResponse(success=True)
35
+ except Exception as e:
36
+ return TapResponse(success=False, error=str(e))
37
+
38
+
39
+ @router.post("/api/control/swipe", response_model=SwipeResponse)
40
+ def control_swipe(request: SwipeRequest) -> SwipeResponse:
41
+ """Execute swipe from start to end coordinates."""
42
+ try:
43
+ from phone_agent.adb import swipe
44
+
45
+ swipe(
46
+ start_x=request.start_x,
47
+ start_y=request.start_y,
48
+ end_x=request.end_x,
49
+ end_y=request.end_y,
50
+ duration_ms=request.duration_ms,
51
+ device_id=request.device_id,
52
+ delay=request.delay,
53
+ )
54
+
55
+ return SwipeResponse(success=True)
56
+ except Exception as e:
57
+ return SwipeResponse(success=False, error=str(e))
58
+
59
+
60
+ @router.post("/api/control/touch/down", response_model=TouchDownResponse)
61
+ def control_touch_down(request: TouchDownRequest) -> TouchDownResponse:
62
+ """Send touch DOWN event at specified device coordinates."""
63
+ try:
64
+ from AutoGLM_GUI.adb_plus import touch_down
65
+
66
+ touch_down(
67
+ x=request.x,
68
+ y=request.y,
69
+ device_id=request.device_id,
70
+ delay=request.delay,
71
+ )
72
+
73
+ return TouchDownResponse(success=True)
74
+ except Exception as e:
75
+ return TouchDownResponse(success=False, error=str(e))
76
+
77
+
78
+ @router.post("/api/control/touch/move", response_model=TouchMoveResponse)
79
+ def control_touch_move(request: TouchMoveRequest) -> TouchMoveResponse:
80
+ """Send touch MOVE event at specified device coordinates."""
81
+ try:
82
+ from AutoGLM_GUI.adb_plus import touch_move
83
+
84
+ touch_move(
85
+ x=request.x,
86
+ y=request.y,
87
+ device_id=request.device_id,
88
+ delay=request.delay,
89
+ )
90
+
91
+ return TouchMoveResponse(success=True)
92
+ except Exception as e:
93
+ return TouchMoveResponse(success=False, error=str(e))
94
+
95
+
96
+ @router.post("/api/control/touch/up", response_model=TouchUpResponse)
97
+ def control_touch_up(request: TouchUpRequest) -> TouchUpResponse:
98
+ """Send touch UP event at specified device coordinates."""
99
+ try:
100
+ from AutoGLM_GUI.adb_plus import touch_up
101
+
102
+ touch_up(
103
+ x=request.x,
104
+ y=request.y,
105
+ device_id=request.device_id,
106
+ delay=request.delay,
107
+ )
108
+
109
+ return TouchUpResponse(success=True)
110
+ except Exception as e:
111
+ return TouchUpResponse(success=False, error=str(e))
@@ -0,0 +1,29 @@
1
+ """Device discovery routes."""
2
+
3
+ from fastapi import APIRouter
4
+
5
+ from AutoGLM_GUI.schemas import DeviceListResponse
6
+ from AutoGLM_GUI.state import agents
7
+
8
+ router = APIRouter()
9
+
10
+
11
+ @router.get("/api/devices", response_model=DeviceListResponse)
12
+ def list_devices() -> DeviceListResponse:
13
+ """列出所有 ADB 设备。"""
14
+ from phone_agent.adb import list_devices as adb_list
15
+
16
+ adb_devices = adb_list()
17
+
18
+ return DeviceListResponse(
19
+ devices=[
20
+ {
21
+ "id": d.device_id,
22
+ "model": d.model or "Unknown",
23
+ "status": d.status,
24
+ "connection_type": d.connection_type.value,
25
+ "is_initialized": d.device_id in agents,
26
+ }
27
+ for d in adb_devices
28
+ ]
29
+ )