autoglm-gui 1.4.1__py3-none-any.whl → 1.5.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 (104) hide show
  1. AutoGLM_GUI/__init__.py +11 -0
  2. AutoGLM_GUI/__main__.py +26 -4
  3. AutoGLM_GUI/actions/__init__.py +6 -0
  4. AutoGLM_GUI/actions/handler.py +196 -0
  5. AutoGLM_GUI/actions/types.py +15 -0
  6. AutoGLM_GUI/adb/__init__.py +53 -0
  7. AutoGLM_GUI/adb/apps.py +227 -0
  8. AutoGLM_GUI/adb/connection.py +323 -0
  9. AutoGLM_GUI/adb/device.py +171 -0
  10. AutoGLM_GUI/adb/input.py +67 -0
  11. AutoGLM_GUI/adb/screenshot.py +11 -0
  12. AutoGLM_GUI/adb/timing.py +167 -0
  13. AutoGLM_GUI/adb_plus/keyboard_installer.py +4 -2
  14. AutoGLM_GUI/adb_plus/screenshot.py +22 -1
  15. AutoGLM_GUI/adb_plus/serial.py +38 -20
  16. AutoGLM_GUI/adb_plus/touch.py +4 -9
  17. AutoGLM_GUI/agents/__init__.py +43 -12
  18. AutoGLM_GUI/agents/events.py +19 -0
  19. AutoGLM_GUI/agents/factory.py +31 -38
  20. AutoGLM_GUI/agents/glm/__init__.py +7 -0
  21. AutoGLM_GUI/agents/glm/agent.py +292 -0
  22. AutoGLM_GUI/agents/glm/message_builder.py +81 -0
  23. AutoGLM_GUI/agents/glm/parser.py +110 -0
  24. AutoGLM_GUI/agents/glm/prompts_en.py +77 -0
  25. AutoGLM_GUI/agents/glm/prompts_zh.py +75 -0
  26. AutoGLM_GUI/agents/mai/__init__.py +28 -0
  27. AutoGLM_GUI/agents/mai/agent.py +405 -0
  28. AutoGLM_GUI/agents/mai/parser.py +254 -0
  29. AutoGLM_GUI/agents/mai/prompts.py +103 -0
  30. AutoGLM_GUI/agents/mai/traj_memory.py +91 -0
  31. AutoGLM_GUI/agents/protocols.py +12 -8
  32. AutoGLM_GUI/agents/stream_runner.py +188 -0
  33. AutoGLM_GUI/api/__init__.py +40 -21
  34. AutoGLM_GUI/api/agents.py +157 -240
  35. AutoGLM_GUI/api/control.py +9 -6
  36. AutoGLM_GUI/api/devices.py +102 -12
  37. AutoGLM_GUI/api/history.py +78 -0
  38. AutoGLM_GUI/api/layered_agent.py +67 -15
  39. AutoGLM_GUI/api/media.py +64 -1
  40. AutoGLM_GUI/api/scheduled_tasks.py +98 -0
  41. AutoGLM_GUI/config.py +81 -0
  42. AutoGLM_GUI/config_manager.py +68 -51
  43. AutoGLM_GUI/device_manager.py +248 -29
  44. AutoGLM_GUI/device_protocol.py +1 -1
  45. AutoGLM_GUI/devices/adb_device.py +5 -10
  46. AutoGLM_GUI/devices/mock_device.py +4 -2
  47. AutoGLM_GUI/devices/remote_device.py +8 -3
  48. AutoGLM_GUI/history_manager.py +164 -0
  49. AutoGLM_GUI/i18n.py +81 -0
  50. AutoGLM_GUI/model/__init__.py +5 -0
  51. AutoGLM_GUI/model/message_builder.py +69 -0
  52. AutoGLM_GUI/model/types.py +24 -0
  53. AutoGLM_GUI/models/__init__.py +10 -0
  54. AutoGLM_GUI/models/history.py +96 -0
  55. AutoGLM_GUI/models/scheduled_task.py +71 -0
  56. AutoGLM_GUI/parsers/__init__.py +22 -0
  57. AutoGLM_GUI/parsers/base.py +50 -0
  58. AutoGLM_GUI/parsers/phone_parser.py +58 -0
  59. AutoGLM_GUI/phone_agent_manager.py +62 -396
  60. AutoGLM_GUI/platform_utils.py +26 -0
  61. AutoGLM_GUI/prompt_config.py +15 -0
  62. AutoGLM_GUI/prompts/__init__.py +32 -0
  63. AutoGLM_GUI/scheduler_manager.py +304 -0
  64. AutoGLM_GUI/schemas.py +234 -72
  65. AutoGLM_GUI/scrcpy_stream.py +142 -24
  66. AutoGLM_GUI/socketio_server.py +100 -27
  67. AutoGLM_GUI/static/assets/{about-_XNhzQZX.js → about-BQm96DAl.js} +1 -1
  68. AutoGLM_GUI/static/assets/alert-dialog-B42XxGPR.js +1 -0
  69. AutoGLM_GUI/static/assets/chat-C0L2gQYG.js +129 -0
  70. AutoGLM_GUI/static/assets/circle-alert-D4rSJh37.js +1 -0
  71. AutoGLM_GUI/static/assets/dialog-DZ78cEcj.js +45 -0
  72. AutoGLM_GUI/static/assets/history-DFBv7TGc.js +1 -0
  73. AutoGLM_GUI/static/assets/index-Bzyv2yQ2.css +1 -0
  74. AutoGLM_GUI/static/assets/{index-Cy8TmmHV.js → index-CmZSnDqc.js} +1 -1
  75. AutoGLM_GUI/static/assets/index-CssG-3TH.js +11 -0
  76. AutoGLM_GUI/static/assets/label-BCUzE_nm.js +1 -0
  77. AutoGLM_GUI/static/assets/logs-eoFxn5of.js +1 -0
  78. AutoGLM_GUI/static/assets/popover-DLsuV5Sx.js +1 -0
  79. AutoGLM_GUI/static/assets/scheduled-tasks-MyqGJvy_.js +1 -0
  80. AutoGLM_GUI/static/assets/square-pen-zGWYrdfj.js +1 -0
  81. AutoGLM_GUI/static/assets/textarea-BX6y7uM5.js +1 -0
  82. AutoGLM_GUI/static/assets/workflows-CYFs6ssC.js +1 -0
  83. AutoGLM_GUI/static/index.html +2 -2
  84. AutoGLM_GUI/types.py +17 -0
  85. {autoglm_gui-1.4.1.dist-info → autoglm_gui-1.5.0.dist-info}/METADATA +137 -130
  86. autoglm_gui-1.5.0.dist-info/RECORD +157 -0
  87. AutoGLM_GUI/agents/mai_adapter.py +0 -627
  88. AutoGLM_GUI/api/dual_model.py +0 -317
  89. AutoGLM_GUI/dual_model/__init__.py +0 -53
  90. AutoGLM_GUI/dual_model/decision_model.py +0 -664
  91. AutoGLM_GUI/dual_model/dual_agent.py +0 -917
  92. AutoGLM_GUI/dual_model/protocols.py +0 -354
  93. AutoGLM_GUI/dual_model/vision_model.py +0 -442
  94. AutoGLM_GUI/mai_ui_adapter/agent_wrapper.py +0 -291
  95. AutoGLM_GUI/phone_agent_patches.py +0 -147
  96. AutoGLM_GUI/static/assets/chat-DwJpiAWf.js +0 -126
  97. AutoGLM_GUI/static/assets/dialog-B3uW4T8V.js +0 -45
  98. AutoGLM_GUI/static/assets/index-Cpv2gSF1.css +0 -1
  99. AutoGLM_GUI/static/assets/index-UYYauTly.js +0 -12
  100. AutoGLM_GUI/static/assets/workflows-Du_de-dt.js +0 -1
  101. autoglm_gui-1.4.1.dist-info/RECORD +0 -117
  102. {autoglm_gui-1.4.1.dist-info → autoglm_gui-1.5.0.dist-info}/WHEEL +0 -0
  103. {autoglm_gui-1.4.1.dist-info → autoglm_gui-1.5.0.dist-info}/entry_points.txt +0 -0
  104. {autoglm_gui-1.4.1.dist-info → autoglm_gui-1.5.0.dist-info}/licenses/LICENSE +0 -0
AutoGLM_GUI/api/agents.py CHANGED
@@ -1,20 +1,16 @@
1
1
  """Agent lifecycle and chat routes."""
2
2
 
3
3
  import json
4
- import queue
5
- import threading
6
4
 
7
5
  from fastapi import APIRouter, HTTPException
8
6
  from fastapi.responses import StreamingResponse
9
- from phone_agent.agent import StepResult
10
7
  from pydantic import ValidationError
11
8
 
9
+ from AutoGLM_GUI.agents.events import AgentEventType
10
+ from AutoGLM_GUI.config import AgentConfig, ModelConfig
12
11
  from AutoGLM_GUI.logger import logger
13
- from AutoGLM_GUI.phone_agent_patches import apply_patches
14
12
  from AutoGLM_GUI.schemas import (
15
13
  AbortRequest,
16
- APIAgentConfig,
17
- APIModelConfig,
18
14
  ChatRequest,
19
15
  ChatResponse,
20
16
  ConfigResponse,
@@ -27,11 +23,6 @@ from AutoGLM_GUI.state import (
27
23
  non_blocking_takeover,
28
24
  )
29
25
  from AutoGLM_GUI.version import APP_VERSION
30
- from phone_agent.agent import AgentConfig
31
- from phone_agent.model import ModelConfig
32
-
33
- # Apply monkey patches to phone_agent
34
- apply_patches()
35
26
 
36
27
  router = APIRouter()
37
28
 
@@ -59,37 +50,6 @@ def _setup_adb_keyboard(device_id: str) -> None:
59
50
  logger.info(f"✓ Device {device_id}: ADB Keyboard ready")
60
51
 
61
52
 
62
- def _initialize_agent_with_config(
63
- device_id: str,
64
- model_config: ModelConfig,
65
- agent_config: AgentConfig,
66
- ) -> None:
67
- """使用给定配置初始化 Agent。
68
-
69
- Args:
70
- device_id: 设备 ID
71
- model_config: 模型配置
72
- agent_config: Agent 配置
73
-
74
- Raises:
75
- Exception: 初始化失败时抛出异常
76
- """
77
- from AutoGLM_GUI.phone_agent_manager import PhoneAgentManager
78
-
79
- # Setup ADB Keyboard first
80
- _setup_adb_keyboard(device_id)
81
-
82
- # Initialize agent
83
- manager = PhoneAgentManager.get_instance()
84
- manager.initialize_agent(
85
- device_id=device_id,
86
- model_config=model_config,
87
- agent_config=agent_config,
88
- takeover_callback=non_blocking_takeover,
89
- )
90
- logger.info(f"Agent initialized successfully for device {device_id}")
91
-
92
-
93
53
  SSEPayload = dict[str, str | int | bool | None | dict]
94
54
 
95
55
 
@@ -101,57 +61,50 @@ def _create_sse_event(
101
61
  return event_data
102
62
 
103
63
 
104
- @router.post("/api/init")
64
+ @router.post("/api/init", deprecated=True)
105
65
  def init_agent(request: InitRequest) -> dict:
106
- """初始化 PhoneAgent(多设备支持)。"""
107
- from AutoGLM_GUI.config_manager import config_manager
66
+ """初始化 PhoneAgent(已废弃,多设备支持)。
67
+
68
+ ⚠️ 此端点已废弃,将在未来版本移除。
108
69
 
109
- req_model_config = request.model or APIModelConfig()
110
- req_agent_config = request.agent or APIAgentConfig()
70
+ Agent 现在会在首次使用时自动初始化,无需手动调用此端点。
71
+ 如需修改配置,请使用 /api/config 端点或直接修改配置文件 ~/.config/autoglm/config.json。
72
+ 配置保存后会自动销毁所有 Agent,确保下次使用时应用新配置。
73
+
74
+ 配置完全由 ConfigManager 提供(CLI > ENV > FILE > DEFAULT),
75
+ 不接受运行时覆盖。
76
+ """
77
+ from AutoGLM_GUI.config_manager import config_manager
111
78
 
112
- device_id = req_agent_config.device_id
79
+ device_id = request.device_id
113
80
  if not device_id:
114
- raise HTTPException(
115
- status_code=400, detail="device_id is required in agent_config"
116
- )
81
+ raise HTTPException(status_code=400, detail="device_id is required")
117
82
 
118
83
  # 热重载配置文件(支持运行时手动修改)
119
84
  config_manager.load_file_config()
120
85
  config_manager.sync_to_env()
121
86
 
122
- # 获取有效配置(已合并 CLI > ENV > FILE > DEFAULT)
87
+ # 获取有效配置(CLI > ENV > FILE > DEFAULT)
123
88
  effective_config = config_manager.get_effective_config()
124
89
 
125
- # 优先级:请求参数 > 有效配置
126
- base_url = req_model_config.base_url or effective_config.base_url
127
- api_key = req_model_config.api_key or effective_config.api_key
128
- model_name = req_model_config.model_name or effective_config.model_name
129
-
130
- # 获取配置的默认最大步数
131
- max_steps = effective_config.default_max_steps
132
-
133
- if not base_url:
90
+ if not effective_config.base_url:
134
91
  raise HTTPException(
135
92
  status_code=400,
136
93
  detail="base_url is required. Please configure via Settings or start with --base-url",
137
94
  )
138
95
 
96
+ # 直接使用有效配置构造 ModelConfig 和 AgentConfig
139
97
  model_config = ModelConfig(
140
- base_url=base_url,
141
- api_key=api_key,
142
- model_name=model_name,
143
- max_tokens=req_model_config.max_tokens,
144
- temperature=req_model_config.temperature,
145
- top_p=req_model_config.top_p,
146
- frequency_penalty=req_model_config.frequency_penalty,
98
+ base_url=effective_config.base_url,
99
+ api_key=effective_config.api_key,
100
+ model_name=effective_config.model_name,
101
+ # max_tokens, temperature, top_p, frequency_penalty 使用 ModelConfig 默认值
147
102
  )
148
103
 
149
104
  agent_config = AgentConfig(
150
- max_steps=max_steps,
105
+ max_steps=effective_config.default_max_steps,
151
106
  device_id=device_id,
152
- lang=req_agent_config.lang,
153
- system_prompt=req_agent_config.system_prompt,
154
- verbose=req_agent_config.verbose,
107
+ # lang, system_prompt, verbose 使用 AgentConfig 默认值
155
108
  )
156
109
 
157
110
  # Initialize agent (includes ADB Keyboard setup)
@@ -182,8 +135,9 @@ def init_agent(request: InitRequest) -> dict:
182
135
  force=request.force,
183
136
  )
184
137
 
185
- logger.info(
186
- f"Agent of type '{request.agent_type}' initialized for device {device_id}"
138
+ logger.warning(
139
+ f"/api/init is deprecated. Agent of type '{request.agent_type}' initialized for device {device_id}. "
140
+ f"Consider using auto-initialization instead."
187
141
  )
188
142
  except Exception as e:
189
143
  logger.error(f"Failed to initialize agent: {e}")
@@ -192,193 +146,137 @@ def init_agent(request: InitRequest) -> dict:
192
146
  return {
193
147
  "success": True,
194
148
  "device_id": device_id,
195
- "message": f"Agent initialized for device {device_id}",
149
+ "message": f"Agent initialized for device {device_id} (⚠️ /api/init is deprecated)",
196
150
  "agent_type": request.agent_type,
151
+ "deprecated": True,
152
+ "hint": "Agent 会在首次使用时自动初始化,无需手动调用此端点",
197
153
  }
198
154
 
199
155
 
200
156
  @router.post("/api/chat", response_model=ChatResponse)
201
157
  def chat(request: ChatRequest) -> ChatResponse:
202
- """发送任务给 Agent 并执行。"""
203
- from AutoGLM_GUI.exceptions import DeviceBusyError
158
+ """发送任务给 Agent 并执行。
159
+
160
+ Agent 会在首次使用时自动初始化,无需手动调用 /api/init。
161
+ """
162
+ from AutoGLM_GUI.exceptions import AgentInitializationError, DeviceBusyError
204
163
  from AutoGLM_GUI.phone_agent_manager import PhoneAgentManager
205
164
 
206
165
  device_id = request.device_id
207
166
  manager = PhoneAgentManager.get_instance()
208
167
 
209
- # Check if agent is initialized
210
- if not manager.is_initialized(device_id):
211
- raise HTTPException(
212
- status_code=400, detail="Agent not initialized. Call /api/init first."
213
- )
214
-
215
- # Use context manager for automatic lock management
168
+ # use_agent 默认 auto_initialize=True,会自动初始化 Agent
216
169
  try:
217
170
  with manager.use_agent(device_id, timeout=None) as agent:
218
171
  result = agent.run(request.message)
219
172
  steps = agent.step_count
220
173
  agent.reset()
221
174
  return ChatResponse(result=result, steps=steps, success=True)
175
+ except AgentInitializationError as e:
176
+ # 配置错误或初始化失败
177
+ logger.error(f"Failed to initialize agent for {device_id}: {e}")
178
+ raise HTTPException(
179
+ status_code=500,
180
+ detail=f"初始化失败: {str(e)}. 请检查全局配置 (base_url, api_key, model_name)",
181
+ )
222
182
  except DeviceBusyError:
223
183
  raise HTTPException(
224
184
  status_code=409, detail=f"Device {device_id} is busy. Please wait."
225
185
  )
226
186
  except Exception as e:
187
+ logger.exception(f"Unexpected error in chat for {device_id}")
227
188
  return ChatResponse(result=str(e), steps=0, success=False)
228
189
 
229
190
 
230
191
  @router.post("/api/chat/stream")
231
192
  def chat_stream(request: ChatRequest):
232
- """发送任务给 Agent 并实时推送执行进度(SSE,多设备支持)。"""
233
- from AutoGLM_GUI.exceptions import DeviceBusyError
193
+ """发送任务给 Agent 并实时推送执行进度(SSE,多设备支持)。
194
+
195
+ Agent 会在首次使用时自动初始化,无需手动调用 /api/init。
196
+ """
197
+ from datetime import datetime
198
+
199
+ from AutoGLM_GUI.agents.stream_runner import AgentStepStreamer
200
+ from AutoGLM_GUI.device_manager import DeviceManager
201
+ from AutoGLM_GUI.exceptions import AgentInitializationError, DeviceBusyError
202
+ from AutoGLM_GUI.history_manager import history_manager
203
+ from AutoGLM_GUI.models.history import ConversationRecord
234
204
  from AutoGLM_GUI.phone_agent_manager import PhoneAgentManager
235
205
 
236
206
  device_id = request.device_id
237
207
  manager = PhoneAgentManager.get_instance()
238
208
 
239
- # 验证 agent 已初始化
240
- if not manager.is_initialized(device_id):
241
- raise HTTPException(
242
- status_code=400,
243
- detail=f"Device {device_id} not initialized. Call /api/init first.",
244
- )
245
-
246
209
  def event_generator():
247
- threads: list[threading.Thread] = []
248
- stop_event: threading.Event | None = None
210
+ acquired = False
211
+ start_time = datetime.now()
212
+ final_message = ""
213
+ final_success = False
214
+ final_steps = 0
249
215
 
250
216
  try:
251
- # 创建事件队列用于 agent → SSE 通信
252
- event_queue: queue.Queue[tuple[str, SSEPayload | None]] = queue.Queue()
253
-
254
- # 思考块回调
255
- def on_thinking_chunk(chunk: str):
256
- chunk_data = _create_sse_event("thinking_chunk", {"chunk": chunk})
257
- event_queue.put(("thinking_chunk", chunk_data))
258
-
259
- # 使用 streaming agent context manager(自动处理所有管理逻辑!)
260
- with manager.use_streaming_agent(
261
- device_id, on_thinking_chunk, timeout=0
262
- ) as (streaming_agent, stop_event):
263
- # 早期 abort 检查
264
- if stop_event.is_set():
265
- logger.info(f"[Abort] Chat aborted before starting for {device_id}")
266
- yield "event: aborted\n"
267
- yield 'data: {"type": "aborted", "role": "assistant", "message": "Chat aborted by user"}\n\n'
268
- return
269
-
270
- # 在线程中运行 agent 步骤
271
- step_result: list[StepResult | None] = [None]
272
- error_result: list[Exception | None] = [None]
273
-
274
- def run_step(is_first: bool = True, task: str | None = None):
275
- try:
276
- if stop_event.is_set():
277
- return
278
-
279
- result = (
280
- streaming_agent.step(task)
281
- if is_first
282
- else streaming_agent.step()
283
- )
284
-
285
- if stop_event.is_set():
286
- return
287
-
288
- step_result[0] = result
289
- except Exception as e:
290
- error_result[0] = e
291
- finally:
292
- event_queue.put(("step_done", None))
293
-
294
- # 启动第一步
295
- thread = threading.Thread(
296
- target=run_step, args=(True, request.message), daemon=True
297
- )
298
- thread.start()
299
- threads.append(thread)
300
-
301
- # 事件循环
302
- while not stop_event.is_set():
303
- try:
304
- event_type, event_data = event_queue.get(timeout=0.1)
305
- except queue.Empty:
306
- continue
307
-
308
- if event_type == "thinking_chunk":
309
- yield "event: thinking_chunk\n"
310
- yield f"data: {json.dumps(event_data, ensure_ascii=False)}\n\n"
217
+ acquired = manager.acquire_device(
218
+ device_id, timeout=0, raise_on_timeout=True, auto_initialize=True
219
+ )
311
220
 
312
- elif event_type == "step_done":
313
- if error_result[0]:
314
- raise error_result[0]
315
-
316
- result = step_result[0]
317
- if result is None:
318
- raise RuntimeError("step_result is None after step_done")
319
-
320
- event_data = _create_sse_event(
321
- "step",
322
- {
323
- "step": streaming_agent.step_count,
324
- "thinking": result.thinking,
325
- "action": result.action,
326
- "success": result.success,
327
- "finished": result.finished,
328
- },
329
- )
330
-
331
- yield "event: step\n"
332
- yield f"data: {json.dumps(event_data, ensure_ascii=False)}\n\n"
221
+ try:
222
+ agent = manager.get_agent(device_id)
223
+ streamer = AgentStepStreamer(agent=agent, task=request.message)
224
+
225
+ with streamer.stream_context() as abort_fn:
226
+ manager.register_abort_handler(device_id, abort_fn)
333
227
 
334
- if result.finished:
335
- done_data = _create_sse_event(
336
- "done",
337
- {
338
- "message": result.message,
339
- "steps": streaming_agent.step_count,
340
- "success": result.success,
341
- },
342
- )
343
- yield "event: done\n"
344
- yield f"data: {json.dumps(done_data, ensure_ascii=False)}\n\n"
345
- break
228
+ for event in streamer:
229
+ event_type = event["type"]
230
+ event_data_dict = event["data"]
346
231
 
347
232
  if (
348
- streaming_agent.step_count
349
- >= streaming_agent.agent_config.max_steps
233
+ event_type == AgentEventType.STEP.value
234
+ and event_data_dict.get("step") == -1
350
235
  ):
351
- done_data = _create_sse_event(
352
- "done",
353
- {
354
- "message": "Max steps reached",
355
- "steps": streaming_agent.step_count,
356
- "success": result.success,
357
- },
358
- )
359
- yield "event: done\n"
360
- yield f"data: {json.dumps(done_data, ensure_ascii=False)}\n\n"
361
- break
362
-
363
- # 启动下一步
364
- step_result[0] = None
365
- error_result[0] = None
366
- thread = threading.Thread(
367
- target=run_step, args=(False, None), daemon=True
368
- )
369
- thread.start()
370
- threads.append(thread)
371
-
372
- # 检查是否被中止
373
- if stop_event.is_set():
374
- logger.info(f"[Abort] Streaming chat terminated for {device_id}")
375
- yield "event: aborted\n"
376
- yield 'data: {"type": "aborted", "role": "assistant", "message": "Chat aborted by user"}\n\n'
377
-
378
- # 重置原始 agent(context 已由 use_streaming_agent 同步)
379
- original_agent = manager.get_agent(device_id)
380
- original_agent.reset()
236
+ continue
381
237
 
238
+ if event_type == AgentEventType.DONE.value:
239
+ final_message = event_data_dict.get("message", "")
240
+ final_success = event_data_dict.get("success", False)
241
+ final_steps = event_data_dict.get("steps", 0)
242
+
243
+ event_data = _create_sse_event(event_type, event_data_dict)
244
+
245
+ yield f"event: {event_type}\n"
246
+ yield f"data: {json.dumps(event_data, ensure_ascii=False)}\n\n"
247
+
248
+ finally:
249
+ if acquired:
250
+ manager.release_device(device_id)
251
+
252
+ device_manager = DeviceManager.get_instance()
253
+ serialno = device_manager.get_serial_by_device_id(device_id)
254
+ if serialno and final_message:
255
+ end_time = datetime.now()
256
+ record = ConversationRecord(
257
+ task_text=request.message,
258
+ final_message=final_message,
259
+ success=final_success,
260
+ steps=final_steps,
261
+ start_time=start_time,
262
+ end_time=end_time,
263
+ duration_ms=int((end_time - start_time).total_seconds() * 1000),
264
+ source="chat",
265
+ error_message=None if final_success else final_message,
266
+ )
267
+ history_manager.add_record(serialno, record)
268
+
269
+ except AgentInitializationError as e:
270
+ logger.error(f"Failed to initialize agent for {device_id}: {e}")
271
+ error_data = _create_sse_event(
272
+ "error",
273
+ {
274
+ "message": f"初始化失败: {str(e)}",
275
+ "hint": "请检查全局配置 (base_url, api_key, model_name)",
276
+ },
277
+ )
278
+ yield "event: error\n"
279
+ yield f"data: {json.dumps(error_data, ensure_ascii=False)}\n\n"
382
280
  except DeviceBusyError:
383
281
  error_data = _create_sse_event("error", {"message": "Device is busy"})
384
282
  yield "event: error\n"
@@ -389,13 +287,7 @@ def chat_stream(request: ChatRequest):
389
287
  yield "event: error\n"
390
288
  yield f"data: {json.dumps(error_data, ensure_ascii=False)}\n\n"
391
289
  finally:
392
- if stop_event is not None:
393
- stop_event.set()
394
-
395
- # 等待线程完成(带超时)
396
- for thread in threads:
397
- if thread.is_alive():
398
- thread.join(timeout=5.0)
290
+ manager.unregister_abort_handler(device_id)
399
291
 
400
292
  return StreamingResponse(
401
293
  event_generator(),
@@ -493,15 +385,12 @@ def get_config_endpoint() -> ConfigResponse:
493
385
  model_name=effective_config.model_name,
494
386
  api_key=effective_config.api_key if effective_config.api_key != "EMPTY" else "",
495
387
  source=source.value,
496
- dual_model_enabled=effective_config.dual_model_enabled,
497
- decision_base_url=effective_config.decision_base_url,
498
- decision_model_name=effective_config.decision_model_name,
499
- decision_api_key=effective_config.decision_api_key
500
- if effective_config.decision_api_key
501
- else "",
502
388
  agent_type=effective_config.agent_type,
503
389
  agent_config_params=effective_config.agent_config_params,
504
390
  default_max_steps=effective_config.default_max_steps,
391
+ decision_base_url=effective_config.decision_base_url,
392
+ decision_model_name=effective_config.decision_model_name,
393
+ decision_api_key=effective_config.decision_api_key,
505
394
  conflicts=[
506
395
  {
507
396
  "field": c.field,
@@ -518,8 +407,13 @@ def get_config_endpoint() -> ConfigResponse:
518
407
 
519
408
  @router.post("/api/config")
520
409
  def save_config_endpoint(request: ConfigSaveRequest) -> dict:
521
- """保存配置到文件."""
410
+ """保存配置到文件.
411
+
412
+ 副作用:保存配置后会自动销毁所有已初始化的 Agent,
413
+ 确保下次使用时所有 Agent 都使用新配置。
414
+ """
522
415
  from AutoGLM_GUI.config_manager import ConfigModel, config_manager
416
+ from AutoGLM_GUI.phone_agent_manager import PhoneAgentManager
523
417
 
524
418
  try:
525
419
  # Validate incoming configuration
@@ -534,13 +428,12 @@ def save_config_endpoint(request: ConfigSaveRequest) -> dict:
534
428
  base_url=request.base_url,
535
429
  model_name=request.model_name,
536
430
  api_key=request.api_key,
537
- dual_model_enabled=request.dual_model_enabled,
538
- decision_base_url=request.decision_base_url,
539
- decision_model_name=request.decision_model_name,
540
- decision_api_key=request.decision_api_key,
541
431
  agent_type=request.agent_type,
542
432
  agent_config_params=request.agent_config_params,
543
433
  default_max_steps=request.default_max_steps,
434
+ decision_base_url=request.decision_base_url,
435
+ decision_model_name=request.decision_model_name,
436
+ decision_api_key=request.decision_api_key,
544
437
  merge_mode=True,
545
438
  )
546
439
 
@@ -550,9 +443,26 @@ def save_config_endpoint(request: ConfigSaveRequest) -> dict:
550
443
  # 同步到环境变量
551
444
  config_manager.sync_to_env()
552
445
 
446
+ # 副作用:销毁所有已初始化的 Agent,确保下次使用新配置
447
+ manager = PhoneAgentManager.get_instance()
448
+ destroyed_agents = manager.list_agents() # 获取需要销毁的 agent 列表
449
+
450
+ for device_id in destroyed_agents:
451
+ try:
452
+ manager.destroy_agent(device_id)
453
+ logger.info(f"Destroyed agent for {device_id} after config change")
454
+ except Exception as e:
455
+ logger.warning(f"Failed to destroy agent for {device_id}: {e}")
456
+
553
457
  # 检测冲突并返回警告
554
458
  conflicts = config_manager.detect_conflicts()
555
459
 
460
+ response_message = f"Configuration saved to {config_manager.get_config_path()}"
461
+ if destroyed_agents:
462
+ response_message += (
463
+ f". Destroyed {len(destroyed_agents)} agent(s) to apply new config."
464
+ )
465
+
556
466
  if conflicts:
557
467
  warnings = [
558
468
  f"{c.field}: file value overridden by {c.override_source.value}"
@@ -560,13 +470,15 @@ def save_config_endpoint(request: ConfigSaveRequest) -> dict:
560
470
  ]
561
471
  return {
562
472
  "success": True,
563
- "message": f"Configuration saved to {config_manager.get_config_path()}",
473
+ "message": response_message,
564
474
  "warnings": warnings,
475
+ "destroyed_agents": len(destroyed_agents),
565
476
  }
566
477
 
567
478
  return {
568
479
  "success": True,
569
- "message": f"Configuration saved to {config_manager.get_config_path()}",
480
+ "message": response_message,
481
+ "destroyed_agents": len(destroyed_agents),
570
482
  }
571
483
 
572
484
  except ValidationError as e:
@@ -590,3 +502,8 @@ def delete_config_endpoint() -> dict:
590
502
 
591
503
  except Exception as e:
592
504
  raise HTTPException(status_code=500, detail=str(e))
505
+
506
+
507
+ # ✅ 已删除 /api/agents/reinit-all 端点
508
+ # 原因:配置保存时自动销毁所有 Agent(副作用),无需单独的 reinit 端点
509
+ # 见 /api/config POST 端点的实现
@@ -2,6 +2,7 @@
2
2
 
3
3
  from fastapi import APIRouter
4
4
 
5
+ from AutoGLM_GUI.devices.adb_device import ADBDevice
5
6
  from AutoGLM_GUI.schemas import (
6
7
  SwipeRequest,
7
8
  SwipeResponse,
@@ -22,12 +23,13 @@ router = APIRouter()
22
23
  def control_tap(request: TapRequest) -> TapResponse:
23
24
  """Execute tap at specified device coordinates."""
24
25
  try:
25
- from phone_agent.adb import tap
26
+ if not request.device_id:
27
+ return TapResponse(success=False, error="device_id is required")
26
28
 
27
- tap(
29
+ device = ADBDevice(request.device_id)
30
+ device.tap(
28
31
  x=request.x,
29
32
  y=request.y,
30
- device_id=request.device_id,
31
33
  delay=request.delay,
32
34
  )
33
35
 
@@ -40,15 +42,16 @@ def control_tap(request: TapRequest) -> TapResponse:
40
42
  def control_swipe(request: SwipeRequest) -> SwipeResponse:
41
43
  """Execute swipe from start to end coordinates."""
42
44
  try:
43
- from phone_agent.adb import swipe
45
+ if not request.device_id:
46
+ return SwipeResponse(success=False, error="device_id is required")
44
47
 
45
- swipe(
48
+ device = ADBDevice(request.device_id)
49
+ device.swipe(
46
50
  start_x=request.start_x,
47
51
  start_y=request.start_y,
48
52
  end_x=request.end_x,
49
53
  end_y=request.end_y,
50
54
  duration_ms=request.duration_ms,
51
- device_id=request.device_id,
52
55
  delay=request.delay,
53
56
  )
54
57