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/server.py CHANGED
@@ -1,494 +1,5 @@
1
1
  """AutoGLM-GUI Backend API Server."""
2
2
 
3
- import asyncio
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
- from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect
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-RqglIZxV.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-C8KPPfxe.js";function t(){return o.jsx("div",{className:"p-2",children:o.jsx("h3",{children:"About"})})}export{t as component};