autoglm-gui 0.3.2__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.
- AutoGLM_GUI/__main__.py +6 -2
- AutoGLM_GUI/adb_plus/screenshot.py +1 -4
- AutoGLM_GUI/api/__init__.py +66 -0
- AutoGLM_GUI/api/agents.py +231 -0
- AutoGLM_GUI/api/control.py +111 -0
- AutoGLM_GUI/api/devices.py +29 -0
- AutoGLM_GUI/api/media.py +163 -0
- AutoGLM_GUI/schemas.py +127 -0
- AutoGLM_GUI/scrcpy_stream.py +65 -28
- AutoGLM_GUI/server.py +2 -617
- AutoGLM_GUI/state.py +33 -0
- AutoGLM_GUI/static/assets/{about-2K7DgoQw.js → about-gHEqXVMQ.js} +1 -1
- AutoGLM_GUI/static/assets/chat-6a-qTECg.js +25 -0
- AutoGLM_GUI/static/assets/{index-BynheeWl.js → index-C8KPPfxe.js} +6 -6
- AutoGLM_GUI/static/assets/index-D2-3f619.css +1 -0
- AutoGLM_GUI/static/assets/{index-Cc7aUqXq.js → index-DgzeSwgt.js} +1 -1
- AutoGLM_GUI/static/index.html +2 -2
- AutoGLM_GUI/version.py +8 -0
- {autoglm_gui-0.3.2.dist-info → autoglm_gui-0.4.1.dist-info}/METADATA +64 -9
- autoglm_gui-0.4.1.dist-info/RECORD +44 -0
- phone_agent/adb/connection.py +0 -1
- phone_agent/adb/device.py +0 -2
- phone_agent/adb/input.py +0 -1
- phone_agent/adb/screenshot.py +0 -1
- phone_agent/agent.py +1 -1
- AutoGLM_GUI/static/assets/chat-DjOHP9wp.js +0 -25
- AutoGLM_GUI/static/assets/index-CrqBLMxN.css +0 -1
- autoglm_gui-0.3.2.dist-info/RECORD +0 -36
- {autoglm_gui-0.3.2.dist-info → autoglm_gui-0.4.1.dist-info}/WHEEL +0 -0
- {autoglm_gui-0.3.2.dist-info → autoglm_gui-0.4.1.dist-info}/entry_points.txt +0 -0
- {autoglm_gui-0.3.2.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(
|
|
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 =
|
|
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:
|
|
@@ -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,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
|
+
)
|
AutoGLM_GUI/api/media.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""Media routes: screenshot, video stream, stream reset."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
|
|
6
|
+
|
|
7
|
+
from AutoGLM_GUI.adb_plus import capture_screenshot
|
|
8
|
+
from AutoGLM_GUI.schemas import ScreenshotRequest, ScreenshotResponse
|
|
9
|
+
from AutoGLM_GUI.scrcpy_stream import ScrcpyStreamer
|
|
10
|
+
from AutoGLM_GUI.state import scrcpy_locks, scrcpy_streamers
|
|
11
|
+
|
|
12
|
+
router = APIRouter()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@router.post("/api/video/reset")
|
|
16
|
+
async def reset_video_stream(device_id: str | None = None) -> dict:
|
|
17
|
+
"""Reset video stream (cleanup scrcpy server,多设备支持)."""
|
|
18
|
+
if device_id:
|
|
19
|
+
if device_id in scrcpy_locks:
|
|
20
|
+
async with scrcpy_locks[device_id]:
|
|
21
|
+
if device_id in scrcpy_streamers:
|
|
22
|
+
print(f"[video/reset] Stopping streamer for device {device_id}")
|
|
23
|
+
scrcpy_streamers[device_id].stop()
|
|
24
|
+
del scrcpy_streamers[device_id]
|
|
25
|
+
print(f"[video/reset] Streamer reset for device {device_id}")
|
|
26
|
+
return {
|
|
27
|
+
"success": True,
|
|
28
|
+
"message": f"Video stream reset for device {device_id}",
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
"success": True,
|
|
32
|
+
"message": f"No active video stream for device {device_id}",
|
|
33
|
+
}
|
|
34
|
+
return {"success": True, "message": f"No video stream for device {device_id}"}
|
|
35
|
+
|
|
36
|
+
device_ids = list(scrcpy_streamers.keys())
|
|
37
|
+
for dev_id in device_ids:
|
|
38
|
+
if dev_id in scrcpy_locks:
|
|
39
|
+
async with scrcpy_locks[dev_id]:
|
|
40
|
+
if dev_id in scrcpy_streamers:
|
|
41
|
+
scrcpy_streamers[dev_id].stop()
|
|
42
|
+
del scrcpy_streamers[dev_id]
|
|
43
|
+
print("[video/reset] All streamers reset")
|
|
44
|
+
return {"success": True, "message": "All video streams reset"}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@router.post("/api/screenshot", response_model=ScreenshotResponse)
|
|
48
|
+
def take_screenshot(request: ScreenshotRequest) -> ScreenshotResponse:
|
|
49
|
+
"""获取设备截图。此操作无副作用,不影响 PhoneAgent 运行。"""
|
|
50
|
+
try:
|
|
51
|
+
screenshot = capture_screenshot(device_id=request.device_id)
|
|
52
|
+
return ScreenshotResponse(
|
|
53
|
+
success=True,
|
|
54
|
+
image=screenshot.base64_data,
|
|
55
|
+
width=screenshot.width,
|
|
56
|
+
height=screenshot.height,
|
|
57
|
+
is_sensitive=screenshot.is_sensitive,
|
|
58
|
+
)
|
|
59
|
+
except Exception as e:
|
|
60
|
+
return ScreenshotResponse(
|
|
61
|
+
success=False,
|
|
62
|
+
image="",
|
|
63
|
+
width=0,
|
|
64
|
+
height=0,
|
|
65
|
+
is_sensitive=False,
|
|
66
|
+
error=str(e),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@router.websocket("/api/video/stream")
|
|
71
|
+
async def video_stream_ws(websocket: WebSocket, device_id: str | None = None):
|
|
72
|
+
"""Stream real-time H.264 video from scrcpy server via WebSocket(多设备支持)."""
|
|
73
|
+
await websocket.accept()
|
|
74
|
+
|
|
75
|
+
if not device_id:
|
|
76
|
+
await websocket.send_json({"error": "device_id is required"})
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
print(f"[video/stream] WebSocket connection for device {device_id}")
|
|
80
|
+
|
|
81
|
+
if device_id not in scrcpy_locks:
|
|
82
|
+
scrcpy_locks[device_id] = asyncio.Lock()
|
|
83
|
+
|
|
84
|
+
async with scrcpy_locks[device_id]:
|
|
85
|
+
if device_id not in scrcpy_streamers:
|
|
86
|
+
print(f"[video/stream] Creating streamer for device {device_id}")
|
|
87
|
+
scrcpy_streamers[device_id] = ScrcpyStreamer(
|
|
88
|
+
device_id=device_id, max_size=1280, bit_rate=4_000_000
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
print(f"[video/stream] Starting scrcpy server for device {device_id}")
|
|
93
|
+
await scrcpy_streamers[device_id].start()
|
|
94
|
+
print(f"[video/stream] Scrcpy server started for device {device_id}")
|
|
95
|
+
except Exception as e:
|
|
96
|
+
import traceback
|
|
97
|
+
|
|
98
|
+
print(f"[video/stream] Failed to start streamer: {e}")
|
|
99
|
+
print(f"[video/stream] Traceback:\n{traceback.format_exc()}")
|
|
100
|
+
scrcpy_streamers[device_id].stop()
|
|
101
|
+
del scrcpy_streamers[device_id]
|
|
102
|
+
try:
|
|
103
|
+
await websocket.send_json({"error": str(e)})
|
|
104
|
+
except Exception:
|
|
105
|
+
pass
|
|
106
|
+
return
|
|
107
|
+
else:
|
|
108
|
+
print(f"[video/stream] Reusing streamer for device {device_id}")
|
|
109
|
+
|
|
110
|
+
streamer = scrcpy_streamers[device_id]
|
|
111
|
+
if streamer.cached_sps and streamer.cached_pps:
|
|
112
|
+
init_data = streamer.cached_sps + streamer.cached_pps
|
|
113
|
+
await websocket.send_bytes(init_data)
|
|
114
|
+
print(f"[video/stream] Sent SPS/PPS for device {device_id}")
|
|
115
|
+
else:
|
|
116
|
+
print(
|
|
117
|
+
f"[video/stream] Warning: No cached SPS/PPS for device {device_id}"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
streamer = scrcpy_streamers[device_id]
|
|
121
|
+
|
|
122
|
+
stream_failed = False
|
|
123
|
+
try:
|
|
124
|
+
chunk_count = 0
|
|
125
|
+
while True:
|
|
126
|
+
try:
|
|
127
|
+
h264_chunk = await streamer.read_h264_chunk()
|
|
128
|
+
await websocket.send_bytes(h264_chunk)
|
|
129
|
+
chunk_count += 1
|
|
130
|
+
if chunk_count % 100 == 0:
|
|
131
|
+
print(
|
|
132
|
+
f"[video/stream] Device {device_id}: Sent {chunk_count} chunks"
|
|
133
|
+
)
|
|
134
|
+
except ConnectionError as e:
|
|
135
|
+
print(f"[video/stream] Device {device_id}: Connection error: {e}")
|
|
136
|
+
stream_failed = True
|
|
137
|
+
try:
|
|
138
|
+
await websocket.send_json({"error": f"Stream error: {str(e)}"})
|
|
139
|
+
except Exception:
|
|
140
|
+
pass
|
|
141
|
+
break
|
|
142
|
+
|
|
143
|
+
except WebSocketDisconnect:
|
|
144
|
+
print(f"[video/stream] Device {device_id}: Client disconnected")
|
|
145
|
+
except Exception as e:
|
|
146
|
+
import traceback
|
|
147
|
+
|
|
148
|
+
print(f"[video/stream] Device {device_id}: Error: {e}")
|
|
149
|
+
print(f"[video/stream] Traceback:\n{traceback.format_exc()}")
|
|
150
|
+
stream_failed = True
|
|
151
|
+
try:
|
|
152
|
+
await websocket.send_json({"error": str(e)})
|
|
153
|
+
except Exception:
|
|
154
|
+
pass
|
|
155
|
+
|
|
156
|
+
if stream_failed:
|
|
157
|
+
async with scrcpy_locks[device_id]:
|
|
158
|
+
if device_id in scrcpy_streamers:
|
|
159
|
+
print(f"[video/stream] Resetting streamer for device {device_id}")
|
|
160
|
+
scrcpy_streamers[device_id].stop()
|
|
161
|
+
del scrcpy_streamers[device_id]
|
|
162
|
+
|
|
163
|
+
print(f"[video/stream] Device {device_id}: Stream ended")
|