autoglm-gui 1.2.1__tar.gz → 1.3.1__tar.gz
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-1.2.1 → autoglm_gui-1.3.1}/AutoGLM_GUI/adb_plus/__init__.py +6 -6
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/AutoGLM_GUI/api/__init__.py +54 -16
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/AutoGLM_GUI/api/agents.py +163 -209
- autoglm_gui-1.3.1/AutoGLM_GUI/api/dual_model.py +310 -0
- autoglm_gui-1.3.1/AutoGLM_GUI/api/mcp.py +134 -0
- autoglm_gui-1.3.1/AutoGLM_GUI/api/metrics.py +36 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/AutoGLM_GUI/config_manager.py +110 -6
- autoglm_gui-1.3.1/AutoGLM_GUI/dual_model/__init__.py +53 -0
- autoglm_gui-1.3.1/AutoGLM_GUI/dual_model/decision_model.py +664 -0
- autoglm_gui-1.3.1/AutoGLM_GUI/dual_model/dual_agent.py +917 -0
- autoglm_gui-1.3.1/AutoGLM_GUI/dual_model/protocols.py +354 -0
- autoglm_gui-1.3.1/AutoGLM_GUI/dual_model/vision_model.py +442 -0
- autoglm_gui-1.3.1/AutoGLM_GUI/exceptions.py +97 -0
- autoglm_gui-1.3.1/AutoGLM_GUI/metrics.py +283 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/AutoGLM_GUI/phone_agent_manager.py +264 -14
- autoglm_gui-1.3.1/AutoGLM_GUI/prompts.py +97 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/AutoGLM_GUI/schemas.py +40 -9
- autoglm_gui-1.2.1/AutoGLM_GUI/static/assets/about-BtBH1xKN.js → autoglm_gui-1.3.1/AutoGLM_GUI/static/assets/about-Cj6QXqMf.js +1 -1
- autoglm_gui-1.3.1/AutoGLM_GUI/static/assets/chat-BJeomZgh.js +124 -0
- autoglm_gui-1.3.1/AutoGLM_GUI/static/assets/dialog-CxJlnjzH.js +45 -0
- autoglm_gui-1.2.1/AutoGLM_GUI/static/assets/index-B_AaKuOT.js → autoglm_gui-1.3.1/AutoGLM_GUI/static/assets/index-C_B-Arvf.js +1 -1
- autoglm_gui-1.3.1/AutoGLM_GUI/static/assets/index-CxJQuE4y.js +12 -0
- autoglm_gui-1.3.1/AutoGLM_GUI/static/assets/index-Z0uYCPOO.css +1 -0
- autoglm_gui-1.2.1/AutoGLM_GUI/static/assets/workflows-xX_QH-wI.js → autoglm_gui-1.3.1/AutoGLM_GUI/static/assets/workflows-BTiGCNI0.js +1 -1
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/AutoGLM_GUI/static/index.html +2 -2
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/PKG-INFO +90 -5
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/README.md +87 -4
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/pyproject.toml +3 -1
- autoglm_gui-1.2.1/AutoGLM_GUI/exceptions.py +0 -25
- autoglm_gui-1.2.1/AutoGLM_GUI/static/assets/chat-DPzFNNGu.js +0 -124
- autoglm_gui-1.2.1/AutoGLM_GUI/static/assets/dialog-Dwuk2Hgl.js +0 -45
- autoglm_gui-1.2.1/AutoGLM_GUI/static/assets/index-BjYIY--m.css +0 -1
- autoglm_gui-1.2.1/AutoGLM_GUI/static/assets/index-CvQkCi2d.js +0 -11
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/.gitignore +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/AutoGLM_GUI/__init__.py +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/AutoGLM_GUI/__main__.py +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/AutoGLM_GUI/adb_plus/device.py +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/AutoGLM_GUI/adb_plus/ip.py +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/AutoGLM_GUI/adb_plus/keyboard_installer.py +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/AutoGLM_GUI/adb_plus/mdns.py +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/AutoGLM_GUI/adb_plus/pair.py +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/AutoGLM_GUI/adb_plus/qr_pair.py +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/AutoGLM_GUI/adb_plus/screenshot.py +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/AutoGLM_GUI/adb_plus/serial.py +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/AutoGLM_GUI/adb_plus/touch.py +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/AutoGLM_GUI/adb_plus/version.py +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/AutoGLM_GUI/api/control.py +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/AutoGLM_GUI/api/devices.py +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/AutoGLM_GUI/api/media.py +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/AutoGLM_GUI/api/version.py +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/AutoGLM_GUI/api/workflows.py +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/AutoGLM_GUI/config.py +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/AutoGLM_GUI/device_manager.py +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/AutoGLM_GUI/logger.py +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/AutoGLM_GUI/phone_agent_patches.py +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/AutoGLM_GUI/platform_utils.py +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/AutoGLM_GUI/scrcpy_protocol.py +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/AutoGLM_GUI/scrcpy_stream.py +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/AutoGLM_GUI/server.py +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/AutoGLM_GUI/socketio_server.py +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/AutoGLM_GUI/state.py +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/AutoGLM_GUI/static/assets/logo-Cyfm06Ym.png +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/AutoGLM_GUI/static/assets/worker-D6BRitjy.js +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/AutoGLM_GUI/static/favicon.ico +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/AutoGLM_GUI/static/logo-192.png +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/AutoGLM_GUI/static/logo-512.png +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/AutoGLM_GUI/version.py +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/AutoGLM_GUI/workflow_manager.py +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/LICENSE +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/phone_agent/__init__.py +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/phone_agent/actions/__init__.py +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/phone_agent/actions/handler.py +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/phone_agent/actions/handler_ios.py +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/phone_agent/adb/__init__.py +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/phone_agent/adb/connection.py +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/phone_agent/adb/device.py +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/phone_agent/adb/input.py +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/phone_agent/adb/screenshot.py +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/phone_agent/agent.py +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/phone_agent/agent_ios.py +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/phone_agent/config/__init__.py +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/phone_agent/config/apps.py +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/phone_agent/config/apps_harmonyos.py +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/phone_agent/config/apps_ios.py +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/phone_agent/config/i18n.py +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/phone_agent/config/prompts.py +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/phone_agent/config/prompts_en.py +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/phone_agent/config/prompts_zh.py +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/phone_agent/config/timing.py +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/phone_agent/device_factory.py +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/phone_agent/hdc/__init__.py +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/phone_agent/hdc/connection.py +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/phone_agent/hdc/device.py +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/phone_agent/hdc/input.py +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/phone_agent/hdc/screenshot.py +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/phone_agent/model/__init__.py +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/phone_agent/model/client.py +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/phone_agent/xctest/__init__.py +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/phone_agent/xctest/connection.py +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/phone_agent/xctest/device.py +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/phone_agent/xctest/input.py +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/phone_agent/xctest/screenshot.py +0 -0
- {autoglm_gui-1.2.1 → autoglm_gui-1.3.1}/scrcpy-server-v3.3.3 +0 -0
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
"""Lightweight ADB helpers with a more robust screenshot implementation."""
|
|
2
2
|
|
|
3
|
-
from .keyboard_installer import ADBKeyboardInstaller
|
|
4
|
-
from .screenshot import Screenshot, capture_screenshot
|
|
5
|
-
from .touch import touch_down, touch_move, touch_up
|
|
6
|
-
from .ip import get_wifi_ip
|
|
7
|
-
from .serial import get_device_serial, extract_serial_from_mdns
|
|
8
3
|
from .device import check_device_available
|
|
4
|
+
from .ip import get_wifi_ip
|
|
5
|
+
from .keyboard_installer import ADBKeyboardInstaller
|
|
6
|
+
from .mdns import MdnsDevice, discover_mdns_devices
|
|
9
7
|
from .pair import pair_device
|
|
10
|
-
from .mdns import discover_mdns_devices, MdnsDevice
|
|
11
8
|
from .qr_pair import qr_pairing_manager
|
|
9
|
+
from .screenshot import Screenshot, capture_screenshot
|
|
10
|
+
from .serial import extract_serial_from_mdns, get_device_serial
|
|
11
|
+
from .touch import touch_down, touch_move, touch_up
|
|
12
12
|
from .version import get_adb_version, supports_mdns_services
|
|
13
13
|
|
|
14
14
|
__all__ = [
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import sys
|
|
5
|
+
from contextlib import asynccontextmanager
|
|
5
6
|
from importlib.resources import files
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
|
|
@@ -10,10 +11,20 @@ from fastapi.middleware.cors import CORSMiddleware
|
|
|
10
11
|
from fastapi.responses import FileResponse
|
|
11
12
|
from fastapi.staticfiles import StaticFiles
|
|
12
13
|
|
|
13
|
-
from AutoGLM_GUI.version import APP_VERSION
|
|
14
14
|
from AutoGLM_GUI.adb_plus.qr_pair import qr_pairing_manager
|
|
15
|
+
from AutoGLM_GUI.version import APP_VERSION
|
|
15
16
|
|
|
16
|
-
from . import
|
|
17
|
+
from . import (
|
|
18
|
+
agents,
|
|
19
|
+
control,
|
|
20
|
+
devices,
|
|
21
|
+
dual_model,
|
|
22
|
+
mcp,
|
|
23
|
+
media,
|
|
24
|
+
metrics,
|
|
25
|
+
version,
|
|
26
|
+
workflows,
|
|
27
|
+
)
|
|
17
28
|
|
|
18
29
|
|
|
19
30
|
def _get_static_dir() -> Path | None:
|
|
@@ -42,7 +53,32 @@ def _get_static_dir() -> Path | None:
|
|
|
42
53
|
|
|
43
54
|
def create_app() -> FastAPI:
|
|
44
55
|
"""Build the FastAPI app with routers and static assets."""
|
|
45
|
-
|
|
56
|
+
|
|
57
|
+
# Create MCP ASGI app
|
|
58
|
+
mcp_app = mcp.get_mcp_asgi_app()
|
|
59
|
+
|
|
60
|
+
# Define combined lifespan
|
|
61
|
+
@asynccontextmanager
|
|
62
|
+
async def combined_lifespan(app: FastAPI):
|
|
63
|
+
"""Combine app startup logic with MCP lifespan."""
|
|
64
|
+
# App startup
|
|
65
|
+
asyncio.create_task(qr_pairing_manager.cleanup_expired_sessions())
|
|
66
|
+
|
|
67
|
+
from AutoGLM_GUI.device_manager import DeviceManager
|
|
68
|
+
|
|
69
|
+
device_manager = DeviceManager.get_instance()
|
|
70
|
+
device_manager.start_polling()
|
|
71
|
+
|
|
72
|
+
# Run MCP lifespan
|
|
73
|
+
async with mcp_app.lifespan(app):
|
|
74
|
+
yield
|
|
75
|
+
|
|
76
|
+
# App shutdown (if needed in the future)
|
|
77
|
+
|
|
78
|
+
# Create FastAPI app with combined lifespan
|
|
79
|
+
app = FastAPI(
|
|
80
|
+
title="AutoGLM-GUI API", version=APP_VERSION, lifespan=combined_lifespan
|
|
81
|
+
)
|
|
46
82
|
|
|
47
83
|
app.add_middleware(
|
|
48
84
|
CORSMiddleware,
|
|
@@ -56,34 +92,36 @@ def create_app() -> FastAPI:
|
|
|
56
92
|
app.include_router(devices.router)
|
|
57
93
|
app.include_router(control.router)
|
|
58
94
|
app.include_router(media.router)
|
|
95
|
+
app.include_router(metrics.router)
|
|
59
96
|
app.include_router(version.router)
|
|
60
97
|
app.include_router(workflows.router)
|
|
98
|
+
app.include_router(dual_model.router)
|
|
61
99
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
# Start QR pairing session cleanup task
|
|
66
|
-
asyncio.create_task(qr_pairing_manager.cleanup_expired_sessions())
|
|
67
|
-
|
|
68
|
-
# Start device polling
|
|
69
|
-
from AutoGLM_GUI.device_manager import DeviceManager
|
|
70
|
-
|
|
71
|
-
device_manager = DeviceManager.get_instance()
|
|
72
|
-
device_manager.start_polling()
|
|
73
|
-
|
|
100
|
+
# Mount static files BEFORE MCP to ensure they have priority
|
|
101
|
+
# This is critical: FastAPI processes mounts in order, so static files
|
|
102
|
+
# must be mounted before the catch-all MCP mount
|
|
74
103
|
static_dir = _get_static_dir()
|
|
75
104
|
if static_dir is not None and static_dir.exists():
|
|
76
105
|
assets_dir = static_dir / "assets"
|
|
77
106
|
if assets_dir.exists():
|
|
78
107
|
app.mount("/assets", StaticFiles(directory=assets_dir), name="assets")
|
|
79
108
|
|
|
80
|
-
|
|
109
|
+
# Define SPA serving function
|
|
81
110
|
async def serve_spa(full_path: str) -> FileResponse:
|
|
82
111
|
file_path = static_dir / full_path
|
|
83
112
|
if file_path.is_file():
|
|
84
113
|
return FileResponse(file_path)
|
|
85
114
|
return FileResponse(static_dir / "index.html")
|
|
86
115
|
|
|
116
|
+
# Add catch-all route for SPA (handles all non-API routes)
|
|
117
|
+
app.add_api_route(
|
|
118
|
+
"/{full_path:path}", serve_spa, methods=["GET"], include_in_schema=False
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# Mount MCP server at root (mcp_app already has /mcp path prefix)
|
|
122
|
+
# This must be AFTER static files to avoid intercepting them
|
|
123
|
+
app.mount("/", mcp_app)
|
|
124
|
+
|
|
87
125
|
return app
|
|
88
126
|
|
|
89
127
|
|
|
@@ -28,7 +28,6 @@ from AutoGLM_GUI.state import (
|
|
|
28
28
|
non_blocking_takeover,
|
|
29
29
|
)
|
|
30
30
|
from AutoGLM_GUI.version import APP_VERSION
|
|
31
|
-
from phone_agent import PhoneAgent
|
|
32
31
|
from phone_agent.agent import AgentConfig
|
|
33
32
|
from phone_agent.model import ModelConfig
|
|
34
33
|
|
|
@@ -37,31 +36,73 @@ apply_patches()
|
|
|
37
36
|
|
|
38
37
|
router = APIRouter()
|
|
39
38
|
|
|
40
|
-
# Active chat sessions (device_id -> stop_event)
|
|
41
|
-
# Used for aborting ongoing conversations
|
|
42
|
-
_active_chats: dict[str, threading.Event] = {}
|
|
43
|
-
_active_chats_lock = threading.Lock()
|
|
44
39
|
|
|
40
|
+
def _setup_adb_keyboard(device_id: str) -> None:
|
|
41
|
+
"""检查并自动安装 ADB Keyboard。
|
|
45
42
|
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
Args:
|
|
44
|
+
device_id: 设备 ID
|
|
45
|
+
"""
|
|
46
|
+
from AutoGLM_GUI.adb_plus import ADBKeyboardInstaller
|
|
47
|
+
|
|
48
|
+
logger.info(f"Checking ADB Keyboard for device {device_id}...")
|
|
49
|
+
installer = ADBKeyboardInstaller(device_id=device_id)
|
|
50
|
+
status = installer.get_status()
|
|
51
|
+
|
|
52
|
+
if not (status["installed"] and status["enabled"]):
|
|
53
|
+
logger.info(f"Setting up ADB Keyboard for device {device_id}...")
|
|
54
|
+
success, message = installer.auto_setup()
|
|
55
|
+
if success:
|
|
56
|
+
logger.info(f"✓ Device {device_id}: {message}")
|
|
57
|
+
else:
|
|
58
|
+
logger.warning(f"✗ Device {device_id}: {message}")
|
|
59
|
+
else:
|
|
60
|
+
logger.info(f"✓ Device {device_id}: ADB Keyboard ready")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _initialize_agent_with_config(
|
|
64
|
+
device_id: str,
|
|
65
|
+
model_config: ModelConfig,
|
|
66
|
+
agent_config: AgentConfig,
|
|
48
67
|
) -> None:
|
|
49
|
-
"""
|
|
68
|
+
"""使用给定配置初始化 Agent。
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
device_id: 设备 ID
|
|
72
|
+
model_config: 模型配置
|
|
73
|
+
agent_config: Agent 配置
|
|
74
|
+
|
|
75
|
+
Raises:
|
|
76
|
+
Exception: 初始化失败时抛出异常
|
|
77
|
+
"""
|
|
50
78
|
from AutoGLM_GUI.phone_agent_manager import PhoneAgentManager
|
|
51
79
|
|
|
52
|
-
|
|
53
|
-
|
|
80
|
+
# Setup ADB Keyboard first
|
|
81
|
+
_setup_adb_keyboard(device_id)
|
|
54
82
|
|
|
83
|
+
# Initialize agent
|
|
55
84
|
manager = PhoneAgentManager.get_instance()
|
|
56
|
-
manager.
|
|
85
|
+
manager.initialize_agent(
|
|
86
|
+
device_id=device_id,
|
|
87
|
+
model_config=model_config,
|
|
88
|
+
agent_config=agent_config,
|
|
89
|
+
takeover_callback=non_blocking_takeover,
|
|
90
|
+
)
|
|
91
|
+
logger.info(f"Agent initialized successfully for device {device_id}")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _create_sse_event(
|
|
95
|
+
event_type: str, data: dict[str, Any], role: str = "assistant"
|
|
96
|
+
) -> dict[str, Any]:
|
|
97
|
+
"""Create an SSE event with standardized fields including role."""
|
|
98
|
+
event_data = {"type": event_type, "role": role, **data}
|
|
99
|
+
return event_data
|
|
57
100
|
|
|
58
101
|
|
|
59
102
|
@router.post("/api/init")
|
|
60
103
|
def init_agent(request: InitRequest) -> dict:
|
|
61
104
|
"""初始化 PhoneAgent(多设备支持)。"""
|
|
62
|
-
from AutoGLM_GUI.adb_plus import ADBKeyboardInstaller
|
|
63
105
|
from AutoGLM_GUI.config_manager import config_manager
|
|
64
|
-
from AutoGLM_GUI.logger import logger
|
|
65
106
|
|
|
66
107
|
req_model_config = request.model or APIModelConfig()
|
|
67
108
|
req_agent_config = request.agent or APIAgentConfig()
|
|
@@ -77,21 +118,6 @@ def init_agent(request: InitRequest) -> dict:
|
|
|
77
118
|
config_manager.sync_to_env()
|
|
78
119
|
config.refresh_from_env()
|
|
79
120
|
|
|
80
|
-
# 检查并自动安装 ADB Keyboard
|
|
81
|
-
logger.info(f"Checking ADB Keyboard for device {device_id}...")
|
|
82
|
-
installer = ADBKeyboardInstaller(device_id=device_id)
|
|
83
|
-
status = installer.get_status()
|
|
84
|
-
|
|
85
|
-
if not (status["installed"] and status["enabled"]):
|
|
86
|
-
logger.info(f"Setting up ADB Keyboard for device {device_id}...")
|
|
87
|
-
success, message = installer.auto_setup()
|
|
88
|
-
if success:
|
|
89
|
-
logger.info(f"✓ Device {device_id}: {message}")
|
|
90
|
-
else:
|
|
91
|
-
logger.warning(f"✗ Device {device_id}: {message}")
|
|
92
|
-
else:
|
|
93
|
-
logger.info(f"✓ Device {device_id}: ADB Keyboard ready")
|
|
94
|
-
|
|
95
121
|
base_url = req_model_config.base_url or config.base_url
|
|
96
122
|
api_key = req_model_config.api_key or config.api_key
|
|
97
123
|
model_name = req_model_config.model_name or config.model_name
|
|
@@ -120,17 +146,9 @@ def init_agent(request: InitRequest) -> dict:
|
|
|
120
146
|
verbose=req_agent_config.verbose,
|
|
121
147
|
)
|
|
122
148
|
|
|
123
|
-
# Initialize agent
|
|
124
|
-
from AutoGLM_GUI.phone_agent_manager import PhoneAgentManager
|
|
125
|
-
|
|
126
|
-
manager = PhoneAgentManager.get_instance()
|
|
149
|
+
# Initialize agent (includes ADB Keyboard setup)
|
|
127
150
|
try:
|
|
128
|
-
|
|
129
|
-
device_id=device_id,
|
|
130
|
-
model_config=model_config,
|
|
131
|
-
agent_config=agent_config,
|
|
132
|
-
takeover_callback=non_blocking_takeover,
|
|
133
|
-
)
|
|
151
|
+
_initialize_agent_with_config(device_id, model_config, agent_config)
|
|
134
152
|
except Exception as e:
|
|
135
153
|
logger.error(f"Failed to initialize agent: {e}")
|
|
136
154
|
raise HTTPException(status_code=500, detail=str(e))
|
|
@@ -175,119 +193,59 @@ def chat(request: ChatRequest) -> ChatResponse:
|
|
|
175
193
|
@router.post("/api/chat/stream")
|
|
176
194
|
def chat_stream(request: ChatRequest):
|
|
177
195
|
"""发送任务给 Agent 并实时推送执行进度(SSE,多设备支持)。"""
|
|
178
|
-
from AutoGLM_GUI.exceptions import
|
|
196
|
+
from AutoGLM_GUI.exceptions import DeviceBusyError
|
|
179
197
|
from AutoGLM_GUI.phone_agent_manager import PhoneAgentManager
|
|
180
198
|
|
|
181
199
|
device_id = request.device_id
|
|
182
200
|
manager = PhoneAgentManager.get_instance()
|
|
183
201
|
|
|
184
|
-
#
|
|
202
|
+
# 验证 agent 已初始化
|
|
185
203
|
if not manager.is_initialized(device_id):
|
|
186
204
|
raise HTTPException(
|
|
187
205
|
status_code=400,
|
|
188
206
|
detail=f"Device {device_id} not initialized. Call /api/init first.",
|
|
189
207
|
)
|
|
190
208
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
except DeviceBusyError:
|
|
195
|
-
raise HTTPException(
|
|
196
|
-
status_code=409,
|
|
197
|
-
detail=f"Device {device_id} is already processing a request. Please wait.",
|
|
198
|
-
)
|
|
209
|
+
def event_generator():
|
|
210
|
+
"""SSE 事件生成器."""
|
|
211
|
+
threads: list[threading.Thread] = []
|
|
199
212
|
|
|
200
|
-
try:
|
|
201
|
-
# Get the original agent to copy its config
|
|
202
|
-
original_agent = manager.get_agent(device_id)
|
|
203
|
-
|
|
204
|
-
# Get the stored configs for this device
|
|
205
213
|
try:
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
# Register stop_event to global mapping for abort support
|
|
220
|
-
with _active_chats_lock:
|
|
221
|
-
_active_chats[device_id] = stop_event
|
|
222
|
-
|
|
223
|
-
try:
|
|
224
|
-
# Create a queue to collect events from the agent
|
|
225
|
-
event_queue: queue.Queue[tuple[str, Any]] = queue.Queue()
|
|
226
|
-
|
|
227
|
-
# Create a callback to handle thinking chunks
|
|
228
|
-
def on_thinking_chunk(chunk: str):
|
|
229
|
-
"""Emit thinking chunks as they arrive"""
|
|
230
|
-
if not stop_event.is_set():
|
|
231
|
-
chunk_data = {
|
|
232
|
-
"type": "thinking_chunk",
|
|
233
|
-
"chunk": chunk,
|
|
234
|
-
}
|
|
235
|
-
event_queue.put(("thinking_chunk", chunk_data))
|
|
236
|
-
|
|
237
|
-
# Create a new agent instance
|
|
238
|
-
streaming_agent = PhoneAgent(
|
|
239
|
-
model_config=model_config,
|
|
240
|
-
agent_config=agent_config,
|
|
241
|
-
takeover_callback=non_blocking_takeover,
|
|
242
|
-
)
|
|
243
|
-
|
|
244
|
-
# Copy context from original agent (thread-safe due to device lock)
|
|
245
|
-
streaming_agent._context = original_agent._context.copy()
|
|
246
|
-
streaming_agent._step_count = original_agent._step_count
|
|
247
|
-
|
|
248
|
-
# Monkey-patch the model_client.request to inject the callback
|
|
249
|
-
original_request = streaming_agent.model_client.request
|
|
250
|
-
|
|
251
|
-
def patched_request(messages, **kwargs):
|
|
252
|
-
# Inject the on_thinking_chunk callback
|
|
253
|
-
return original_request(
|
|
254
|
-
messages, on_thinking_chunk=on_thinking_chunk
|
|
255
|
-
)
|
|
256
|
-
|
|
257
|
-
streaming_agent.model_client.request = patched_request
|
|
258
|
-
|
|
259
|
-
# Early abort check (before starting any steps)
|
|
214
|
+
# 创建事件队列用于 agent → SSE 通信
|
|
215
|
+
event_queue: queue.Queue[tuple[str, Any]] = queue.Queue()
|
|
216
|
+
|
|
217
|
+
# 思考块回调
|
|
218
|
+
def on_thinking_chunk(chunk: str):
|
|
219
|
+
chunk_data = _create_sse_event("thinking_chunk", {"chunk": chunk})
|
|
220
|
+
event_queue.put(("thinking_chunk", chunk_data))
|
|
221
|
+
|
|
222
|
+
# 使用 streaming agent context manager(自动处理所有管理逻辑!)
|
|
223
|
+
with manager.use_streaming_agent(
|
|
224
|
+
device_id, on_thinking_chunk, timeout=0
|
|
225
|
+
) as (streaming_agent, stop_event):
|
|
226
|
+
# 早期 abort 检查
|
|
260
227
|
if stop_event.is_set():
|
|
261
|
-
logger.info(
|
|
262
|
-
f"[Abort] Agent for device {device_id} received abort signal before starting steps"
|
|
263
|
-
)
|
|
228
|
+
logger.info(f"[Abort] Chat aborted before starting for {device_id}")
|
|
264
229
|
yield "event: aborted\n"
|
|
265
|
-
yield 'data: {"type": "aborted", "message": "Chat aborted by user"}\n\n'
|
|
230
|
+
yield 'data: {"type": "aborted", "role": "assistant", "message": "Chat aborted by user"}\n\n'
|
|
266
231
|
return
|
|
267
232
|
|
|
268
|
-
#
|
|
233
|
+
# 在线程中运行 agent 步骤
|
|
269
234
|
step_result: list[Any] = [None]
|
|
270
235
|
error_result: list[Any] = [None]
|
|
271
236
|
|
|
272
237
|
def run_step(is_first: bool = True, task: str | None = None):
|
|
273
238
|
try:
|
|
274
|
-
# Check before starting step
|
|
275
239
|
if stop_event.is_set():
|
|
276
|
-
logger.info(
|
|
277
|
-
f"[Abort] Agent for device {device_id} received abort signal before step execution"
|
|
278
|
-
)
|
|
279
240
|
return
|
|
280
241
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
242
|
+
result = (
|
|
243
|
+
streaming_agent.step(task)
|
|
244
|
+
if is_first
|
|
245
|
+
else streaming_agent.step()
|
|
246
|
+
)
|
|
285
247
|
|
|
286
|
-
# Check after step completes
|
|
287
248
|
if stop_event.is_set():
|
|
288
|
-
logger.info(
|
|
289
|
-
f"[Abort] Agent for device {device_id} received abort signal after step execution"
|
|
290
|
-
)
|
|
291
249
|
return
|
|
292
250
|
|
|
293
251
|
step_result[0] = result
|
|
@@ -296,21 +254,18 @@ def chat_stream(request: ChatRequest):
|
|
|
296
254
|
finally:
|
|
297
255
|
event_queue.put(("step_done", None))
|
|
298
256
|
|
|
299
|
-
#
|
|
257
|
+
# 启动第一步
|
|
300
258
|
thread = threading.Thread(
|
|
301
259
|
target=run_step, args=(True, request.message), daemon=True
|
|
302
260
|
)
|
|
303
261
|
thread.start()
|
|
304
262
|
threads.append(thread)
|
|
305
263
|
|
|
264
|
+
# 事件循环
|
|
306
265
|
while not stop_event.is_set():
|
|
307
|
-
# Wait for events from the queue
|
|
308
266
|
try:
|
|
309
267
|
event_type, event_data = event_queue.get(timeout=0.1)
|
|
310
268
|
except queue.Empty:
|
|
311
|
-
# Check again on timeout
|
|
312
|
-
if stop_event.is_set():
|
|
313
|
-
break
|
|
314
269
|
continue
|
|
315
270
|
|
|
316
271
|
if event_type == "thinking_chunk":
|
|
@@ -318,30 +273,33 @@ def chat_stream(request: ChatRequest):
|
|
|
318
273
|
yield f"data: {json.dumps(event_data, ensure_ascii=False)}\n\n"
|
|
319
274
|
|
|
320
275
|
elif event_type == "step_done":
|
|
321
|
-
# Check for errors
|
|
322
276
|
if error_result[0]:
|
|
323
277
|
raise error_result[0]
|
|
324
278
|
|
|
325
279
|
result = step_result[0]
|
|
326
|
-
event_data =
|
|
327
|
-
"
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
280
|
+
event_data = _create_sse_event(
|
|
281
|
+
"step",
|
|
282
|
+
{
|
|
283
|
+
"step": streaming_agent.step_count,
|
|
284
|
+
"thinking": result.thinking,
|
|
285
|
+
"action": result.action,
|
|
286
|
+
"success": result.success,
|
|
287
|
+
"finished": result.finished,
|
|
288
|
+
},
|
|
289
|
+
)
|
|
334
290
|
|
|
335
291
|
yield "event: step\n"
|
|
336
292
|
yield f"data: {json.dumps(event_data, ensure_ascii=False)}\n\n"
|
|
337
293
|
|
|
338
294
|
if result.finished:
|
|
339
|
-
done_data =
|
|
340
|
-
"
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
295
|
+
done_data = _create_sse_event(
|
|
296
|
+
"done",
|
|
297
|
+
{
|
|
298
|
+
"message": result.message,
|
|
299
|
+
"steps": streaming_agent.step_count,
|
|
300
|
+
"success": result.success,
|
|
301
|
+
},
|
|
302
|
+
)
|
|
345
303
|
yield "event: done\n"
|
|
346
304
|
yield f"data: {json.dumps(done_data, ensure_ascii=False)}\n\n"
|
|
347
305
|
break
|
|
@@ -350,17 +308,19 @@ def chat_stream(request: ChatRequest):
|
|
|
350
308
|
streaming_agent.step_count
|
|
351
309
|
>= streaming_agent.agent_config.max_steps
|
|
352
310
|
):
|
|
353
|
-
done_data =
|
|
354
|
-
"
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
311
|
+
done_data = _create_sse_event(
|
|
312
|
+
"done",
|
|
313
|
+
{
|
|
314
|
+
"message": "Max steps reached",
|
|
315
|
+
"steps": streaming_agent.step_count,
|
|
316
|
+
"success": result.success,
|
|
317
|
+
},
|
|
318
|
+
)
|
|
359
319
|
yield "event: done\n"
|
|
360
320
|
yield f"data: {json.dumps(done_data, ensure_ascii=False)}\n\n"
|
|
361
321
|
break
|
|
362
322
|
|
|
363
|
-
#
|
|
323
|
+
# 启动下一步
|
|
364
324
|
step_result[0] = None
|
|
365
325
|
error_result[0] = None
|
|
366
326
|
thread = threading.Thread(
|
|
@@ -369,61 +329,44 @@ def chat_stream(request: ChatRequest):
|
|
|
369
329
|
thread.start()
|
|
370
330
|
threads.append(thread)
|
|
371
331
|
|
|
372
|
-
#
|
|
332
|
+
# 检查是否被中止
|
|
373
333
|
if stop_event.is_set():
|
|
374
|
-
logger.info(
|
|
375
|
-
f"[Abort] Agent for device {device_id} event loop terminated due to abort signal"
|
|
376
|
-
)
|
|
334
|
+
logger.info(f"[Abort] Streaming chat terminated for {device_id}")
|
|
377
335
|
yield "event: aborted\n"
|
|
378
|
-
yield 'data: {"type": "aborted", "message": "Chat aborted by user"}\n\n'
|
|
379
|
-
|
|
380
|
-
# Update original agent state (thread-safe due to device lock)
|
|
381
|
-
original_agent._context = streaming_agent._context
|
|
382
|
-
original_agent._step_count = streaming_agent._step_count
|
|
336
|
+
yield 'data: {"type": "aborted", "role": "assistant", "message": "Chat aborted by user"}\n\n'
|
|
383
337
|
|
|
338
|
+
# 重置原始 agent(context 已由 use_streaming_agent 同步)
|
|
339
|
+
original_agent = manager.get_agent(device_id)
|
|
384
340
|
original_agent.reset()
|
|
385
341
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
# Signal all threads to stop
|
|
342
|
+
except DeviceBusyError:
|
|
343
|
+
error_data = _create_sse_event("error", {"message": "Device is busy"})
|
|
344
|
+
yield "event: error\n"
|
|
345
|
+
yield f"data: {json.dumps(error_data, ensure_ascii=False)}\n\n"
|
|
346
|
+
except Exception as e:
|
|
347
|
+
logger.exception(f"Error in streaming chat for {device_id}")
|
|
348
|
+
error_data = _create_sse_event("error", {"message": str(e)})
|
|
349
|
+
yield "event: error\n"
|
|
350
|
+
yield f"data: {json.dumps(error_data, ensure_ascii=False)}\n\n"
|
|
351
|
+
finally:
|
|
352
|
+
# 通知线程停止
|
|
353
|
+
if "stop_event" in locals():
|
|
399
354
|
stop_event.set()
|
|
400
355
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
event_generator(),
|
|
416
|
-
media_type="text/event-stream",
|
|
417
|
-
headers={
|
|
418
|
-
"Cache-Control": "no-cache",
|
|
419
|
-
"Connection": "keep-alive",
|
|
420
|
-
"X-Accel-Buffering": "no",
|
|
421
|
-
},
|
|
422
|
-
)
|
|
423
|
-
except Exception:
|
|
424
|
-
# Release lock if exception occurs before generator starts
|
|
425
|
-
manager.release_device(device_id)
|
|
426
|
-
raise
|
|
356
|
+
# 等待线程完成(带超时)
|
|
357
|
+
for thread in threads:
|
|
358
|
+
if thread.is_alive():
|
|
359
|
+
thread.join(timeout=5.0)
|
|
360
|
+
|
|
361
|
+
return StreamingResponse(
|
|
362
|
+
event_generator(),
|
|
363
|
+
media_type="text/event-stream",
|
|
364
|
+
headers={
|
|
365
|
+
"Cache-Control": "no-cache",
|
|
366
|
+
"Connection": "keep-alive",
|
|
367
|
+
"X-Accel-Buffering": "no",
|
|
368
|
+
},
|
|
369
|
+
)
|
|
427
370
|
|
|
428
371
|
|
|
429
372
|
@router.get("/api/status", response_model=StatusResponse)
|
|
@@ -478,18 +421,17 @@ def reset_agent(request: ResetRequest) -> dict:
|
|
|
478
421
|
@router.post("/api/chat/abort")
|
|
479
422
|
def abort_chat(request: AbortRequest) -> dict:
|
|
480
423
|
"""中断正在进行的对话流。"""
|
|
481
|
-
from AutoGLM_GUI.
|
|
424
|
+
from AutoGLM_GUI.phone_agent_manager import PhoneAgentManager
|
|
482
425
|
|
|
483
426
|
device_id = request.device_id
|
|
427
|
+
manager = PhoneAgentManager.get_instance()
|
|
484
428
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
logger.warning(f"No active chat found for device {device_id}")
|
|
492
|
-
return {"success": False, "message": "No active chat found"}
|
|
429
|
+
success = manager.abort_streaming_chat(device_id)
|
|
430
|
+
|
|
431
|
+
return {
|
|
432
|
+
"success": success,
|
|
433
|
+
"message": "Abort requested" if success else "No active chat found",
|
|
434
|
+
}
|
|
493
435
|
|
|
494
436
|
|
|
495
437
|
@router.get("/api/config", response_model=ConfigResponse)
|
|
@@ -512,6 +454,13 @@ def get_config_endpoint() -> ConfigResponse:
|
|
|
512
454
|
model_name=effective_config.model_name,
|
|
513
455
|
api_key=effective_config.api_key if effective_config.api_key != "EMPTY" else "",
|
|
514
456
|
source=source.value,
|
|
457
|
+
dual_model_enabled=effective_config.dual_model_enabled,
|
|
458
|
+
decision_base_url=effective_config.decision_base_url,
|
|
459
|
+
decision_model_name=effective_config.decision_model_name,
|
|
460
|
+
decision_api_key=effective_config.decision_api_key
|
|
461
|
+
if effective_config.decision_api_key
|
|
462
|
+
else "",
|
|
463
|
+
thinking_mode=effective_config.thinking_mode,
|
|
515
464
|
conflicts=[
|
|
516
465
|
{
|
|
517
466
|
"field": c.field,
|
|
@@ -532,7 +481,7 @@ def save_config_endpoint(request: ConfigSaveRequest) -> dict:
|
|
|
532
481
|
from AutoGLM_GUI.config_manager import ConfigModel, config_manager
|
|
533
482
|
|
|
534
483
|
try:
|
|
535
|
-
# Validate incoming configuration
|
|
484
|
+
# Validate incoming configuration
|
|
536
485
|
ConfigModel(
|
|
537
486
|
base_url=request.base_url,
|
|
538
487
|
model_name=request.model_name,
|
|
@@ -544,6 +493,11 @@ def save_config_endpoint(request: ConfigSaveRequest) -> dict:
|
|
|
544
493
|
base_url=request.base_url,
|
|
545
494
|
model_name=request.model_name,
|
|
546
495
|
api_key=request.api_key,
|
|
496
|
+
dual_model_enabled=request.dual_model_enabled,
|
|
497
|
+
decision_base_url=request.decision_base_url,
|
|
498
|
+
decision_model_name=request.decision_model_name,
|
|
499
|
+
decision_api_key=request.decision_api_key,
|
|
500
|
+
thinking_mode=request.thinking_mode,
|
|
547
501
|
merge_mode=True,
|
|
548
502
|
)
|
|
549
503
|
|