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.
- AutoGLM_GUI/__main__.py +6 -2
- AutoGLM_GUI/adb_plus/__init__.py +8 -1
- AutoGLM_GUI/adb_plus/screenshot.py +1 -4
- AutoGLM_GUI/adb_plus/touch.py +92 -0
- 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 -491
- AutoGLM_GUI/state.py +33 -0
- AutoGLM_GUI/static/assets/{about-C71SI8ZQ.js → about-gHEqXVMQ.js} +1 -1
- AutoGLM_GUI/static/assets/chat-6a-qTECg.js +25 -0
- AutoGLM_GUI/static/assets/index-C8KPPfxe.js +10 -0
- AutoGLM_GUI/static/assets/index-D2-3f619.css +1 -0
- AutoGLM_GUI/static/assets/{index-DUCan6m6.js → index-DgzeSwgt.js} +1 -1
- AutoGLM_GUI/static/index.html +2 -2
- AutoGLM_GUI/version.py +8 -0
- {autoglm_gui-0.3.1.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-C6WtEfKW.js +0 -14
- AutoGLM_GUI/static/assets/index-Dd1xMRCa.css +0 -1
- AutoGLM_GUI/static/assets/index-RqglIZxV.js +0 -10
- autoglm_gui-0.3.1.dist-info/RECORD +0 -35
- {autoglm_gui-0.3.1.dist-info → autoglm_gui-0.4.1.dist-info}/WHEEL +0 -0
- {autoglm_gui-0.3.1.dist-info → autoglm_gui-0.4.1.dist-info}/entry_points.txt +0 -0
- {autoglm_gui-0.3.1.dist-info → autoglm_gui-0.4.1.dist-info}/licenses/LICENSE +0 -0
AutoGLM_GUI/server.py
CHANGED
|
@@ -1,494 +1,5 @@
|
|
|
1
1
|
"""AutoGLM-GUI Backend API Server."""
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import json
|
|
5
|
-
import os
|
|
6
|
-
from importlib.metadata import version as get_version
|
|
7
|
-
from importlib.resources import files
|
|
8
|
-
from pathlib import Path
|
|
3
|
+
from AutoGLM_GUI.api import app
|
|
9
4
|
|
|
10
|
-
|
|
11
|
-
from fastapi.middleware.cors import CORSMiddleware
|
|
12
|
-
from fastapi.responses import FileResponse, StreamingResponse
|
|
13
|
-
from fastapi.staticfiles import StaticFiles
|
|
14
|
-
from phone_agent import PhoneAgent
|
|
15
|
-
from phone_agent.agent import AgentConfig
|
|
16
|
-
from phone_agent.model import ModelConfig
|
|
17
|
-
from pydantic import BaseModel, Field
|
|
18
|
-
|
|
19
|
-
from AutoGLM_GUI.adb_plus import capture_screenshot
|
|
20
|
-
from AutoGLM_GUI.scrcpy_stream import ScrcpyStreamer
|
|
21
|
-
|
|
22
|
-
# 全局 scrcpy streamer 实例和锁
|
|
23
|
-
scrcpy_streamer: ScrcpyStreamer | None = None
|
|
24
|
-
scrcpy_lock = asyncio.Lock()
|
|
25
|
-
|
|
26
|
-
# 获取包版本号
|
|
27
|
-
try:
|
|
28
|
-
__version__ = get_version("autoglm-gui")
|
|
29
|
-
except Exception:
|
|
30
|
-
__version__ = "dev"
|
|
31
|
-
|
|
32
|
-
app = FastAPI(title="AutoGLM-GUI API", version=__version__)
|
|
33
|
-
|
|
34
|
-
# CORS 配置 (开发环境需要)
|
|
35
|
-
app.add_middleware(
|
|
36
|
-
CORSMiddleware,
|
|
37
|
-
allow_origins=["http://localhost:3000"],
|
|
38
|
-
allow_credentials=True,
|
|
39
|
-
allow_methods=["*"],
|
|
40
|
-
allow_headers=["*"],
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
# 全局单例 agent
|
|
44
|
-
agent: PhoneAgent | None = None
|
|
45
|
-
last_model_config: ModelConfig | None = None
|
|
46
|
-
last_agent_config: AgentConfig | None = None
|
|
47
|
-
|
|
48
|
-
# 默认配置 (优先从环境变量读取,支持 reload 模式)
|
|
49
|
-
DEFAULT_BASE_URL: str = os.getenv("AUTOGLM_BASE_URL", "")
|
|
50
|
-
DEFAULT_MODEL_NAME: str = os.getenv("AUTOGLM_MODEL_NAME", "autoglm-phone-9b")
|
|
51
|
-
DEFAULT_API_KEY: str = os.getenv("AUTOGLM_API_KEY", "EMPTY")
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def _non_blocking_takeover(message: str) -> None:
|
|
55
|
-
"""Log takeover requests without blocking for console input."""
|
|
56
|
-
print(f"[Takeover] {message}")
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
# 请求/响应模型
|
|
60
|
-
class APIModelConfig(BaseModel):
|
|
61
|
-
base_url: str | None = None
|
|
62
|
-
api_key: str | None = None
|
|
63
|
-
model_name: str | None = None
|
|
64
|
-
max_tokens: int = 3000
|
|
65
|
-
temperature: float = 0.0
|
|
66
|
-
top_p: float = 0.85
|
|
67
|
-
frequency_penalty: float = 0.2
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
class APIAgentConfig(BaseModel):
|
|
71
|
-
max_steps: int = 100
|
|
72
|
-
device_id: str | None = None
|
|
73
|
-
lang: str = "cn"
|
|
74
|
-
system_prompt: str | None = None
|
|
75
|
-
verbose: bool = True
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
class InitRequest(BaseModel):
|
|
79
|
-
model: APIModelConfig | None = Field(default=None, alias="model_config")
|
|
80
|
-
agent: APIAgentConfig | None = Field(default=None, alias="agent_config")
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
class ChatRequest(BaseModel):
|
|
84
|
-
message: str
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
class ChatResponse(BaseModel):
|
|
88
|
-
result: str
|
|
89
|
-
steps: int
|
|
90
|
-
success: bool
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
class StatusResponse(BaseModel):
|
|
94
|
-
version: str
|
|
95
|
-
initialized: bool
|
|
96
|
-
step_count: int
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
class ScreenshotRequest(BaseModel):
|
|
100
|
-
device_id: str | None = None
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
class ScreenshotResponse(BaseModel):
|
|
104
|
-
success: bool
|
|
105
|
-
image: str # base64 encoded PNG
|
|
106
|
-
width: int
|
|
107
|
-
height: int
|
|
108
|
-
is_sensitive: bool
|
|
109
|
-
error: str | None = None
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
class TapRequest(BaseModel):
|
|
113
|
-
x: int
|
|
114
|
-
y: int
|
|
115
|
-
device_id: str | None = None
|
|
116
|
-
delay: float = 0.0
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
class TapResponse(BaseModel):
|
|
120
|
-
success: bool
|
|
121
|
-
error: str | None = None
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
# API 端点
|
|
125
|
-
@app.post("/api/init")
|
|
126
|
-
def init_agent(request: InitRequest) -> dict:
|
|
127
|
-
"""初始化 PhoneAgent。"""
|
|
128
|
-
global agent, last_model_config, last_agent_config
|
|
129
|
-
|
|
130
|
-
# 提取配置或使用空对象
|
|
131
|
-
req_model_config = request.model or APIModelConfig()
|
|
132
|
-
req_agent_config = request.agent or APIAgentConfig()
|
|
133
|
-
|
|
134
|
-
# 使用请求参数或默认值
|
|
135
|
-
base_url = req_model_config.base_url or DEFAULT_BASE_URL
|
|
136
|
-
api_key = req_model_config.api_key or DEFAULT_API_KEY
|
|
137
|
-
model_name = req_model_config.model_name or DEFAULT_MODEL_NAME
|
|
138
|
-
|
|
139
|
-
if not base_url:
|
|
140
|
-
raise HTTPException(
|
|
141
|
-
status_code=400, detail="base_url is required (in model_config or env)"
|
|
142
|
-
)
|
|
143
|
-
|
|
144
|
-
model_config = ModelConfig(
|
|
145
|
-
base_url=base_url,
|
|
146
|
-
api_key=api_key,
|
|
147
|
-
model_name=model_name,
|
|
148
|
-
max_tokens=req_model_config.max_tokens,
|
|
149
|
-
temperature=req_model_config.temperature,
|
|
150
|
-
top_p=req_model_config.top_p,
|
|
151
|
-
frequency_penalty=req_model_config.frequency_penalty,
|
|
152
|
-
)
|
|
153
|
-
|
|
154
|
-
agent_config = AgentConfig(
|
|
155
|
-
max_steps=req_agent_config.max_steps,
|
|
156
|
-
device_id=req_agent_config.device_id,
|
|
157
|
-
lang=req_agent_config.lang,
|
|
158
|
-
system_prompt=req_agent_config.system_prompt,
|
|
159
|
-
verbose=req_agent_config.verbose,
|
|
160
|
-
)
|
|
161
|
-
|
|
162
|
-
agent = PhoneAgent(
|
|
163
|
-
model_config=model_config,
|
|
164
|
-
agent_config=agent_config,
|
|
165
|
-
takeover_callback=_non_blocking_takeover,
|
|
166
|
-
)
|
|
167
|
-
|
|
168
|
-
# 记录最新配置,便于 reset 时自动重建
|
|
169
|
-
last_model_config = model_config
|
|
170
|
-
last_agent_config = agent_config
|
|
171
|
-
|
|
172
|
-
return {"success": True, "message": "Agent initialized"}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
@app.post("/api/chat", response_model=ChatResponse)
|
|
176
|
-
def chat(request: ChatRequest) -> ChatResponse:
|
|
177
|
-
"""发送任务给 Agent 并执行。"""
|
|
178
|
-
global agent
|
|
179
|
-
|
|
180
|
-
if agent is None:
|
|
181
|
-
raise HTTPException(
|
|
182
|
-
status_code=400, detail="Agent not initialized. Call /api/init first."
|
|
183
|
-
)
|
|
184
|
-
|
|
185
|
-
try:
|
|
186
|
-
result = agent.run(request.message)
|
|
187
|
-
steps = agent.step_count
|
|
188
|
-
agent.reset()
|
|
189
|
-
|
|
190
|
-
return ChatResponse(result=result, steps=steps, success=True)
|
|
191
|
-
except Exception as e:
|
|
192
|
-
return ChatResponse(result=str(e), steps=0, success=False)
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
@app.post("/api/chat/stream")
|
|
196
|
-
def chat_stream(request: ChatRequest):
|
|
197
|
-
"""发送任务给 Agent 并实时推送执行进度(SSE)。"""
|
|
198
|
-
global agent
|
|
199
|
-
|
|
200
|
-
if agent is None:
|
|
201
|
-
raise HTTPException(
|
|
202
|
-
status_code=400, detail="Agent not initialized. Call /api/init first."
|
|
203
|
-
)
|
|
204
|
-
|
|
205
|
-
def event_generator():
|
|
206
|
-
"""SSE 事件生成器"""
|
|
207
|
-
try:
|
|
208
|
-
# 使用 step() 逐步执行
|
|
209
|
-
step_result = agent.step(request.message)
|
|
210
|
-
while True:
|
|
211
|
-
# 发送 step 事件
|
|
212
|
-
event_data = {
|
|
213
|
-
"type": "step",
|
|
214
|
-
"step": agent.step_count,
|
|
215
|
-
"thinking": step_result.thinking,
|
|
216
|
-
"action": step_result.action,
|
|
217
|
-
"success": step_result.success,
|
|
218
|
-
"finished": step_result.finished,
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
yield "event: step\n"
|
|
222
|
-
yield f"data: {json.dumps(event_data, ensure_ascii=False)}\n\n"
|
|
223
|
-
|
|
224
|
-
if step_result.finished:
|
|
225
|
-
done_data = {
|
|
226
|
-
"type": "done",
|
|
227
|
-
"message": step_result.message,
|
|
228
|
-
"steps": agent.step_count,
|
|
229
|
-
"success": step_result.success,
|
|
230
|
-
}
|
|
231
|
-
yield "event: done\n"
|
|
232
|
-
yield f"data: {json.dumps(done_data, ensure_ascii=False)}\n\n"
|
|
233
|
-
break
|
|
234
|
-
|
|
235
|
-
if agent.step_count >= agent.agent_config.max_steps:
|
|
236
|
-
done_data = {
|
|
237
|
-
"type": "done",
|
|
238
|
-
"message": "Max steps reached",
|
|
239
|
-
"steps": agent.step_count,
|
|
240
|
-
"success": step_result.success,
|
|
241
|
-
}
|
|
242
|
-
yield "event: done\n"
|
|
243
|
-
yield f"data: {json.dumps(done_data, ensure_ascii=False)}\n\n"
|
|
244
|
-
break
|
|
245
|
-
|
|
246
|
-
step_result = agent.step()
|
|
247
|
-
|
|
248
|
-
# 任务完成后重置
|
|
249
|
-
agent.reset()
|
|
250
|
-
|
|
251
|
-
except Exception as e:
|
|
252
|
-
# 发送错误事件
|
|
253
|
-
error_data = {
|
|
254
|
-
"type": "error",
|
|
255
|
-
"message": str(e),
|
|
256
|
-
}
|
|
257
|
-
yield "event: error\n"
|
|
258
|
-
yield f"data: {json.dumps(error_data, ensure_ascii=False)}\n\n"
|
|
259
|
-
|
|
260
|
-
return StreamingResponse(
|
|
261
|
-
event_generator(),
|
|
262
|
-
media_type="text/event-stream",
|
|
263
|
-
headers={
|
|
264
|
-
"Cache-Control": "no-cache",
|
|
265
|
-
"Connection": "keep-alive",
|
|
266
|
-
"X-Accel-Buffering": "no", # 禁用 nginx 缓冲
|
|
267
|
-
},
|
|
268
|
-
)
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
@app.get("/api/status", response_model=StatusResponse)
|
|
272
|
-
def get_status() -> StatusResponse:
|
|
273
|
-
"""获取 Agent 状态和版本信息。"""
|
|
274
|
-
global agent
|
|
275
|
-
|
|
276
|
-
return StatusResponse(
|
|
277
|
-
version=__version__,
|
|
278
|
-
initialized=agent is not None,
|
|
279
|
-
step_count=agent.step_count if agent else 0,
|
|
280
|
-
)
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
@app.post("/api/reset")
|
|
284
|
-
def reset_agent() -> dict:
|
|
285
|
-
"""重置 Agent 状态。"""
|
|
286
|
-
global agent, last_model_config, last_agent_config
|
|
287
|
-
|
|
288
|
-
reinitialized = False
|
|
289
|
-
|
|
290
|
-
# 先清空当前实例
|
|
291
|
-
if agent is not None:
|
|
292
|
-
agent.reset()
|
|
293
|
-
|
|
294
|
-
# 如有历史配置,自动重建实例;否则置空
|
|
295
|
-
if last_model_config and last_agent_config:
|
|
296
|
-
agent = PhoneAgent(
|
|
297
|
-
model_config=last_model_config,
|
|
298
|
-
agent_config=last_agent_config,
|
|
299
|
-
takeover_callback=_non_blocking_takeover,
|
|
300
|
-
)
|
|
301
|
-
reinitialized = True
|
|
302
|
-
else:
|
|
303
|
-
agent = None
|
|
304
|
-
|
|
305
|
-
return {
|
|
306
|
-
"success": True,
|
|
307
|
-
"message": "Agent reset",
|
|
308
|
-
"reinitialized": reinitialized,
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
@app.post("/api/video/reset")
|
|
313
|
-
async def reset_video_stream() -> dict:
|
|
314
|
-
"""Reset video stream (cleanup scrcpy server)."""
|
|
315
|
-
global scrcpy_streamer
|
|
316
|
-
|
|
317
|
-
async with scrcpy_lock:
|
|
318
|
-
if scrcpy_streamer is not None:
|
|
319
|
-
print("[video/reset] Stopping existing streamer...")
|
|
320
|
-
scrcpy_streamer.stop()
|
|
321
|
-
scrcpy_streamer = None
|
|
322
|
-
print("[video/reset] Streamer reset complete")
|
|
323
|
-
return {"success": True, "message": "Video stream reset"}
|
|
324
|
-
else:
|
|
325
|
-
return {"success": True, "message": "No active video stream"}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
@app.post("/api/screenshot", response_model=ScreenshotResponse)
|
|
329
|
-
def take_screenshot(request: ScreenshotRequest) -> ScreenshotResponse:
|
|
330
|
-
"""获取设备截图。此操作无副作用,不影响 PhoneAgent 运行。"""
|
|
331
|
-
try:
|
|
332
|
-
screenshot = capture_screenshot(device_id=request.device_id)
|
|
333
|
-
return ScreenshotResponse(
|
|
334
|
-
success=True,
|
|
335
|
-
image=screenshot.base64_data,
|
|
336
|
-
width=screenshot.width,
|
|
337
|
-
height=screenshot.height,
|
|
338
|
-
is_sensitive=screenshot.is_sensitive,
|
|
339
|
-
)
|
|
340
|
-
except Exception as e:
|
|
341
|
-
return ScreenshotResponse(
|
|
342
|
-
success=False,
|
|
343
|
-
image="",
|
|
344
|
-
width=0,
|
|
345
|
-
height=0,
|
|
346
|
-
is_sensitive=False,
|
|
347
|
-
error=str(e),
|
|
348
|
-
)
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
@app.post("/api/control/tap", response_model=TapResponse)
|
|
352
|
-
def control_tap(request: TapRequest) -> TapResponse:
|
|
353
|
-
"""Execute tap at specified device coordinates."""
|
|
354
|
-
try:
|
|
355
|
-
from phone_agent.adb import tap
|
|
356
|
-
|
|
357
|
-
tap(
|
|
358
|
-
x=request.x,
|
|
359
|
-
y=request.y,
|
|
360
|
-
device_id=request.device_id,
|
|
361
|
-
delay=request.delay
|
|
362
|
-
)
|
|
363
|
-
|
|
364
|
-
return TapResponse(success=True)
|
|
365
|
-
except Exception as e:
|
|
366
|
-
return TapResponse(success=False, error=str(e))
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
@app.websocket("/api/video/stream")
|
|
370
|
-
async def video_stream_ws(websocket: WebSocket):
|
|
371
|
-
"""Stream real-time H.264 video from scrcpy server via WebSocket."""
|
|
372
|
-
global scrcpy_streamer
|
|
373
|
-
|
|
374
|
-
await websocket.accept()
|
|
375
|
-
print("[video/stream] WebSocket connection accepted")
|
|
376
|
-
|
|
377
|
-
# Use global lock to prevent concurrent streamer initialization
|
|
378
|
-
async with scrcpy_lock:
|
|
379
|
-
# Reuse existing streamer if available
|
|
380
|
-
if scrcpy_streamer is None:
|
|
381
|
-
print("[video/stream] Creating new streamer instance...")
|
|
382
|
-
scrcpy_streamer = ScrcpyStreamer(max_size=1280, bit_rate=4_000_000)
|
|
383
|
-
|
|
384
|
-
try:
|
|
385
|
-
print("[video/stream] Starting scrcpy server...")
|
|
386
|
-
await scrcpy_streamer.start()
|
|
387
|
-
print("[video/stream] Scrcpy server started successfully")
|
|
388
|
-
except Exception as e:
|
|
389
|
-
import traceback
|
|
390
|
-
print(f"[video/stream] Failed to start streamer: {e}")
|
|
391
|
-
print(f"[video/stream] Traceback:\n{traceback.format_exc()}")
|
|
392
|
-
scrcpy_streamer.stop()
|
|
393
|
-
scrcpy_streamer = None
|
|
394
|
-
try:
|
|
395
|
-
await websocket.send_json({"error": str(e)})
|
|
396
|
-
except Exception:
|
|
397
|
-
pass
|
|
398
|
-
return
|
|
399
|
-
else:
|
|
400
|
-
print("[video/stream] Reusing existing streamer instance")
|
|
401
|
-
|
|
402
|
-
# Send ONLY SPS/PPS (not IDR) to initialize decoder
|
|
403
|
-
# Client will then wait for next live IDR frame (max 1s with i-frame-interval=1)
|
|
404
|
-
# This avoids issues with potentially corrupted cached IDR frames
|
|
405
|
-
if scrcpy_streamer.cached_sps and scrcpy_streamer.cached_pps:
|
|
406
|
-
init_data = scrcpy_streamer.cached_sps + scrcpy_streamer.cached_pps
|
|
407
|
-
await websocket.send_bytes(init_data)
|
|
408
|
-
print(f"[video/stream] ✓ Sent SPS/PPS ({len(init_data)} bytes), client will wait for live IDR")
|
|
409
|
-
else:
|
|
410
|
-
print("[video/stream] ⚠ Warning: No cached SPS/PPS available")
|
|
411
|
-
|
|
412
|
-
# Stream H.264 data to client
|
|
413
|
-
stream_failed = False
|
|
414
|
-
try:
|
|
415
|
-
chunk_count = 0
|
|
416
|
-
while True:
|
|
417
|
-
try:
|
|
418
|
-
h264_chunk = await scrcpy_streamer.read_h264_chunk()
|
|
419
|
-
await websocket.send_bytes(h264_chunk)
|
|
420
|
-
chunk_count += 1
|
|
421
|
-
if chunk_count % 100 == 0:
|
|
422
|
-
print(f"[video/stream] Sent {chunk_count} chunks")
|
|
423
|
-
except ConnectionError as e:
|
|
424
|
-
print(f"[video/stream] Connection error after {chunk_count} chunks: {e}")
|
|
425
|
-
stream_failed = True
|
|
426
|
-
# Don't send error if WebSocket already disconnected
|
|
427
|
-
try:
|
|
428
|
-
await websocket.send_json({"error": f"Stream error: {str(e)}"})
|
|
429
|
-
except Exception:
|
|
430
|
-
pass
|
|
431
|
-
break
|
|
432
|
-
|
|
433
|
-
except WebSocketDisconnect:
|
|
434
|
-
print("[video/stream] Client disconnected")
|
|
435
|
-
except Exception as e:
|
|
436
|
-
import traceback
|
|
437
|
-
print(f"[video/stream] Error: {e}")
|
|
438
|
-
print(f"[video/stream] Traceback:\n{traceback.format_exc()}")
|
|
439
|
-
stream_failed = True
|
|
440
|
-
try:
|
|
441
|
-
await websocket.send_json({"error": str(e)})
|
|
442
|
-
except Exception:
|
|
443
|
-
pass
|
|
444
|
-
|
|
445
|
-
# Reset global streamer if stream failed
|
|
446
|
-
if stream_failed:
|
|
447
|
-
async with scrcpy_lock:
|
|
448
|
-
print("[video/stream] Stream failed, resetting global streamer...")
|
|
449
|
-
if scrcpy_streamer is not None:
|
|
450
|
-
scrcpy_streamer.stop()
|
|
451
|
-
scrcpy_streamer = None
|
|
452
|
-
|
|
453
|
-
print("[video/stream] Client stream ended")
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
# 静态文件托管 - 使用包内资源定位
|
|
457
|
-
def _get_static_dir() -> Path | None:
|
|
458
|
-
"""获取静态文件目录路径。"""
|
|
459
|
-
try:
|
|
460
|
-
# 尝试从包内资源获取
|
|
461
|
-
static_dir = files("AutoGLM_GUI").joinpath("static")
|
|
462
|
-
if hasattr(static_dir, "_path"):
|
|
463
|
-
# Traversable 对象
|
|
464
|
-
path = Path(str(static_dir))
|
|
465
|
-
if path.exists():
|
|
466
|
-
return path
|
|
467
|
-
# 直接转换为 Path
|
|
468
|
-
path = Path(str(static_dir))
|
|
469
|
-
if path.exists():
|
|
470
|
-
return path
|
|
471
|
-
except (TypeError, FileNotFoundError):
|
|
472
|
-
pass
|
|
473
|
-
|
|
474
|
-
return None
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
STATIC_DIR = _get_static_dir()
|
|
478
|
-
|
|
479
|
-
if STATIC_DIR is not None and STATIC_DIR.exists():
|
|
480
|
-
# 托管静态资源
|
|
481
|
-
assets_dir = STATIC_DIR / "assets"
|
|
482
|
-
if assets_dir.exists():
|
|
483
|
-
app.mount("/assets", StaticFiles(directory=assets_dir), name="assets")
|
|
484
|
-
|
|
485
|
-
# 所有非 API 路由返回 index.html (支持前端路由)
|
|
486
|
-
@app.get("/{full_path:path}")
|
|
487
|
-
async def serve_spa(full_path: str) -> FileResponse:
|
|
488
|
-
"""Serve the SPA for all non-API routes."""
|
|
489
|
-
# 如果请求的是具体文件且存在,则返回该文件
|
|
490
|
-
file_path = STATIC_DIR / full_path
|
|
491
|
-
if file_path.is_file():
|
|
492
|
-
return FileResponse(file_path)
|
|
493
|
-
# 否则返回 index.html (支持前端路由)
|
|
494
|
-
return FileResponse(STATIC_DIR / "index.html")
|
|
5
|
+
__all__ = ["app"]
|
AutoGLM_GUI/state.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Shared runtime state for the AutoGLM-GUI API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import os
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from phone_agent.agent import AgentConfig
|
|
10
|
+
from phone_agent.model import ModelConfig
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from AutoGLM_GUI.scrcpy_stream import ScrcpyStreamer
|
|
14
|
+
from phone_agent import PhoneAgent
|
|
15
|
+
|
|
16
|
+
# Agent instances keyed by device_id
|
|
17
|
+
agents: dict[str, "PhoneAgent"] = {}
|
|
18
|
+
# Cached configs to rebuild agents on reset
|
|
19
|
+
agent_configs: dict[str, tuple[ModelConfig, AgentConfig]] = {}
|
|
20
|
+
|
|
21
|
+
# Scrcpy streaming per device
|
|
22
|
+
scrcpy_streamers: dict[str, "ScrcpyStreamer"] = {}
|
|
23
|
+
scrcpy_locks: dict[str, asyncio.Lock] = {}
|
|
24
|
+
|
|
25
|
+
# Defaults pulled from env (used when request omits config)
|
|
26
|
+
DEFAULT_BASE_URL: str = os.getenv("AUTOGLM_BASE_URL", "")
|
|
27
|
+
DEFAULT_MODEL_NAME: str = os.getenv("AUTOGLM_MODEL_NAME", "autoglm-phone-9b")
|
|
28
|
+
DEFAULT_API_KEY: str = os.getenv("AUTOGLM_API_KEY", "EMPTY")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def non_blocking_takeover(message: str) -> None:
|
|
32
|
+
"""Log takeover requests without blocking for console input."""
|
|
33
|
+
print(f"[Takeover] {message}")
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{j as o}from"./index-
|
|
1
|
+
import{j as o}from"./index-C8KPPfxe.js";function t(){return o.jsx("div",{className:"p-2",children:o.jsx("h3",{children:"About"})})}export{t as component};
|