autoglm-gui 1.2.1__py3-none-any.whl → 1.3.0__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/adb_plus/__init__.py +6 -6
  2. AutoGLM_GUI/api/__init__.py +49 -15
  3. AutoGLM_GUI/api/agents.py +163 -209
  4. AutoGLM_GUI/api/dual_model.py +310 -0
  5. AutoGLM_GUI/api/mcp.py +134 -0
  6. AutoGLM_GUI/api/metrics.py +36 -0
  7. AutoGLM_GUI/config_manager.py +110 -6
  8. AutoGLM_GUI/dual_model/__init__.py +53 -0
  9. AutoGLM_GUI/dual_model/decision_model.py +664 -0
  10. AutoGLM_GUI/dual_model/dual_agent.py +917 -0
  11. AutoGLM_GUI/dual_model/protocols.py +354 -0
  12. AutoGLM_GUI/dual_model/vision_model.py +442 -0
  13. AutoGLM_GUI/exceptions.py +75 -3
  14. AutoGLM_GUI/metrics.py +283 -0
  15. AutoGLM_GUI/phone_agent_manager.py +264 -14
  16. AutoGLM_GUI/prompts.py +97 -0
  17. AutoGLM_GUI/schemas.py +40 -9
  18. AutoGLM_GUI/static/assets/{about-BtBH1xKN.js → about-CrBXGOgB.js} +1 -1
  19. AutoGLM_GUI/static/assets/chat-Di2fwu8V.js +124 -0
  20. AutoGLM_GUI/static/assets/dialog-CHJSPLHJ.js +45 -0
  21. AutoGLM_GUI/static/assets/{index-B_AaKuOT.js → index-9IaIXvyy.js} +1 -1
  22. AutoGLM_GUI/static/assets/index-Dt7cVkfR.js +12 -0
  23. AutoGLM_GUI/static/assets/index-Z0uYCPOO.css +1 -0
  24. AutoGLM_GUI/static/assets/{workflows-xX_QH-wI.js → workflows-DHadKApI.js} +1 -1
  25. AutoGLM_GUI/static/index.html +2 -2
  26. {autoglm_gui-1.2.1.dist-info → autoglm_gui-1.3.0.dist-info}/METADATA +11 -4
  27. {autoglm_gui-1.2.1.dist-info → autoglm_gui-1.3.0.dist-info}/RECORD +30 -20
  28. AutoGLM_GUI/static/assets/chat-DPzFNNGu.js +0 -124
  29. AutoGLM_GUI/static/assets/dialog-Dwuk2Hgl.js +0 -45
  30. AutoGLM_GUI/static/assets/index-BjYIY--m.css +0 -1
  31. AutoGLM_GUI/static/assets/index-CvQkCi2d.js +0 -11
  32. {autoglm_gui-1.2.1.dist-info → autoglm_gui-1.3.0.dist-info}/WHEEL +0 -0
  33. {autoglm_gui-1.2.1.dist-info → autoglm_gui-1.3.0.dist-info}/entry_points.txt +0 -0
  34. {autoglm_gui-1.2.1.dist-info → autoglm_gui-1.3.0.dist-info}/licenses/LICENSE +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 agents, control, devices, media, version, workflows
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
- app = FastAPI(title="AutoGLM-GUI API", version=APP_VERSION)
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,20 +92,13 @@ 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
- @app.on_event("startup")
63
- async def startup_event():
64
- """Initialize background tasks on server startup."""
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()
100
+ # Mount MCP server at root (mcp_app already has /mcp path prefix)
101
+ app.mount("/", mcp_app)
73
102
 
74
103
  static_dir = _get_static_dir()
75
104
  if static_dir is not None and static_dir.exists():
@@ -77,13 +106,18 @@ def create_app() -> FastAPI:
77
106
  if assets_dir.exists():
78
107
  app.mount("/assets", StaticFiles(directory=assets_dir), name="assets")
79
108
 
80
- @app.get("/{full_path:path}")
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 AFTER all mounts to ensure lower priority
117
+ app.add_api_route(
118
+ "/{full_path:path}", serve_spa, methods=["GET"], include_in_schema=False
119
+ )
120
+
87
121
  return app
88
122
 
89
123
 
AutoGLM_GUI/api/agents.py CHANGED
@@ -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
- def _release_device_lock_when_done(
47
- device_id: str, threads: list[threading.Thread]
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
- """Block until threads finish, then release the device lock via manager."""
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
- for thread in threads:
53
- thread.join()
80
+ # Setup ADB Keyboard first
81
+ _setup_adb_keyboard(device_id)
54
82
 
83
+ # Initialize agent
55
84
  manager = PhoneAgentManager.get_instance()
56
- manager.release_device(device_id)
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 via PhoneAgentManager (thread-safe, transactional)
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
- manager.initialize_agent(
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 AgentNotInitializedError, DeviceBusyError
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
- # Check if agent is initialized
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
- # Acquire device lock (non-blocking) to prevent concurrent requests
192
- try:
193
- manager.acquire_device(device_id, timeout=0, raise_on_timeout=True)
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
- model_config, agent_config = manager.get_config(device_id)
207
- except AgentNotInitializedError:
208
- manager.release_device(device_id)
209
- raise HTTPException(
210
- status_code=400,
211
- detail=f"Device {device_id} config not found.",
212
- )
213
-
214
- def event_generator():
215
- """SSE 事件生成器"""
216
- threads: list[threading.Thread] = []
217
- stop_event = threading.Event()
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
- # Run agent step in a separate thread
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
- if is_first:
282
- result = streaming_agent.step(task)
283
- else:
284
- result = streaming_agent.step()
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
- # Start first step
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
- "type": "step",
328
- "step": streaming_agent.step_count,
329
- "thinking": result.thinking,
330
- "action": result.action,
331
- "success": result.success,
332
- "finished": result.finished,
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
- "type": "done",
341
- "message": result.message,
342
- "steps": streaming_agent.step_count,
343
- "success": result.success,
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
- "type": "done",
355
- "message": "Max steps reached",
356
- "steps": streaming_agent.step_count,
357
- "success": result.success,
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
- # Start next step
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
- # Check if loop exited due to abort
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
- except Exception as e:
387
- error_data = {
388
- "type": "error",
389
- "message": str(e),
390
- }
391
- yield "event: error\n"
392
- yield f"data: {json.dumps(error_data, ensure_ascii=False)}\n\n"
393
- finally:
394
- # Clean up active chats mapping
395
- with _active_chats_lock:
396
- _active_chats.pop(device_id, None)
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
- alive_threads = [thread for thread in threads if thread.is_alive()]
402
- if alive_threads:
403
- # Release lock after background threads complete
404
- cleanup_thread = threading.Thread(
405
- target=_release_device_lock_when_done,
406
- args=(device_id, alive_threads),
407
- daemon=True,
408
- )
409
- cleanup_thread.start()
410
- else:
411
- # Release lock immediately if no threads are alive
412
- manager.release_device(device_id)
413
-
414
- return StreamingResponse(
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.logger import logger
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
- with _active_chats_lock:
486
- if device_id in _active_chats:
487
- logger.info(f"Aborting chat for device {device_id}")
488
- _active_chats[device_id].set() # 设置中断标志
489
- return {"success": True, "message": "Abort requested"}
490
- else:
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 to avoid silently falling back to defaults
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