autoglm-gui 1.4.1__py3-none-any.whl → 1.5.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 (135) 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. phone_agent/actions/handler_ios.py → AutoGLM_GUI/actions/handler.py +30 -112
  5. AutoGLM_GUI/actions/types.py +15 -0
  6. {phone_agent → AutoGLM_GUI}/adb/__init__.py +25 -23
  7. {phone_agent → AutoGLM_GUI}/adb/connection.py +5 -40
  8. {phone_agent → AutoGLM_GUI}/adb/device.py +12 -94
  9. {phone_agent → AutoGLM_GUI}/adb/input.py +6 -47
  10. AutoGLM_GUI/adb/screenshot.py +11 -0
  11. {phone_agent/config → AutoGLM_GUI/adb}/timing.py +1 -1
  12. AutoGLM_GUI/adb_plus/keyboard_installer.py +4 -2
  13. AutoGLM_GUI/adb_plus/screenshot.py +22 -1
  14. AutoGLM_GUI/adb_plus/serial.py +38 -20
  15. AutoGLM_GUI/adb_plus/touch.py +4 -9
  16. AutoGLM_GUI/agents/__init__.py +43 -12
  17. AutoGLM_GUI/agents/events.py +19 -0
  18. AutoGLM_GUI/agents/factory.py +31 -38
  19. AutoGLM_GUI/agents/glm/__init__.py +7 -0
  20. AutoGLM_GUI/agents/glm/agent.py +297 -0
  21. AutoGLM_GUI/agents/glm/message_builder.py +81 -0
  22. AutoGLM_GUI/agents/glm/parser.py +110 -0
  23. {phone_agent/config → AutoGLM_GUI/agents/glm}/prompts_en.py +7 -9
  24. {phone_agent/config → AutoGLM_GUI/agents/glm}/prompts_zh.py +18 -25
  25. AutoGLM_GUI/agents/mai/__init__.py +28 -0
  26. AutoGLM_GUI/agents/mai/agent.py +408 -0
  27. AutoGLM_GUI/agents/mai/parser.py +254 -0
  28. AutoGLM_GUI/agents/mai/prompts.py +103 -0
  29. AutoGLM_GUI/agents/mai/traj_memory.py +91 -0
  30. AutoGLM_GUI/agents/protocols.py +12 -8
  31. AutoGLM_GUI/agents/stream_runner.py +193 -0
  32. AutoGLM_GUI/api/__init__.py +40 -21
  33. AutoGLM_GUI/api/agents.py +181 -239
  34. AutoGLM_GUI/api/control.py +9 -6
  35. AutoGLM_GUI/api/devices.py +102 -12
  36. AutoGLM_GUI/api/history.py +104 -0
  37. AutoGLM_GUI/api/layered_agent.py +67 -15
  38. AutoGLM_GUI/api/media.py +64 -1
  39. AutoGLM_GUI/api/scheduled_tasks.py +98 -0
  40. AutoGLM_GUI/config.py +81 -0
  41. AutoGLM_GUI/config_manager.py +68 -51
  42. AutoGLM_GUI/device_manager.py +248 -29
  43. AutoGLM_GUI/device_protocol.py +1 -1
  44. AutoGLM_GUI/devices/adb_device.py +5 -10
  45. AutoGLM_GUI/devices/mock_device.py +4 -2
  46. AutoGLM_GUI/devices/remote_device.py +8 -3
  47. AutoGLM_GUI/history_manager.py +164 -0
  48. AutoGLM_GUI/model/__init__.py +5 -0
  49. AutoGLM_GUI/model/message_builder.py +69 -0
  50. AutoGLM_GUI/model/types.py +24 -0
  51. AutoGLM_GUI/models/__init__.py +10 -0
  52. AutoGLM_GUI/models/history.py +140 -0
  53. AutoGLM_GUI/models/scheduled_task.py +71 -0
  54. AutoGLM_GUI/parsers/__init__.py +22 -0
  55. AutoGLM_GUI/parsers/base.py +50 -0
  56. AutoGLM_GUI/parsers/phone_parser.py +58 -0
  57. AutoGLM_GUI/phone_agent_manager.py +62 -396
  58. AutoGLM_GUI/platform_utils.py +26 -0
  59. AutoGLM_GUI/prompt_config.py +15 -0
  60. AutoGLM_GUI/prompts/__init__.py +32 -0
  61. AutoGLM_GUI/scheduler_manager.py +350 -0
  62. AutoGLM_GUI/schemas.py +246 -72
  63. AutoGLM_GUI/scrcpy_stream.py +142 -24
  64. AutoGLM_GUI/socketio_server.py +100 -27
  65. AutoGLM_GUI/static/assets/{about-_XNhzQZX.js → about-CfwX1Cmc.js} +1 -1
  66. AutoGLM_GUI/static/assets/alert-dialog-CtGlN2IJ.js +1 -0
  67. AutoGLM_GUI/static/assets/chat-BYa-foUI.js +129 -0
  68. AutoGLM_GUI/static/assets/circle-alert-t08bEMPO.js +1 -0
  69. AutoGLM_GUI/static/assets/dialog-FNwZJFwk.js +45 -0
  70. AutoGLM_GUI/static/assets/eye-D0UPWCWC.js +1 -0
  71. AutoGLM_GUI/static/assets/history-CRo95B7i.js +1 -0
  72. AutoGLM_GUI/static/assets/{index-Cy8TmmHV.js → index-BaLMSqd3.js} +1 -1
  73. AutoGLM_GUI/static/assets/index-CTHbFvKl.js +11 -0
  74. AutoGLM_GUI/static/assets/index-CV7jGxGm.css +1 -0
  75. AutoGLM_GUI/static/assets/label-DJFevVmr.js +1 -0
  76. AutoGLM_GUI/static/assets/logs-RW09DyYY.js +1 -0
  77. AutoGLM_GUI/static/assets/popover--JTJrE5v.js +1 -0
  78. AutoGLM_GUI/static/assets/scheduled-tasks-DTRKsQXF.js +1 -0
  79. AutoGLM_GUI/static/assets/square-pen-CPK_K680.js +1 -0
  80. AutoGLM_GUI/static/assets/textarea-PRmVnWq5.js +1 -0
  81. AutoGLM_GUI/static/assets/workflows-CdcsAoaT.js +1 -0
  82. AutoGLM_GUI/static/index.html +2 -2
  83. AutoGLM_GUI/types.py +17 -0
  84. {autoglm_gui-1.4.1.dist-info → autoglm_gui-1.5.1.dist-info}/METADATA +179 -130
  85. autoglm_gui-1.5.1.dist-info/RECORD +118 -0
  86. AutoGLM_GUI/agents/mai_adapter.py +0 -627
  87. AutoGLM_GUI/api/dual_model.py +0 -317
  88. AutoGLM_GUI/device_adapter.py +0 -263
  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. mai_agent/base.py +0 -137
  103. mai_agent/mai_grounding_agent.py +0 -263
  104. mai_agent/mai_naivigation_agent.py +0 -526
  105. mai_agent/prompt.py +0 -148
  106. mai_agent/unified_memory.py +0 -67
  107. mai_agent/utils.py +0 -73
  108. phone_agent/__init__.py +0 -12
  109. phone_agent/actions/__init__.py +0 -5
  110. phone_agent/actions/handler.py +0 -400
  111. phone_agent/adb/screenshot.py +0 -108
  112. phone_agent/agent.py +0 -253
  113. phone_agent/agent_ios.py +0 -277
  114. phone_agent/config/__init__.py +0 -53
  115. phone_agent/config/apps_harmonyos.py +0 -256
  116. phone_agent/config/apps_ios.py +0 -339
  117. phone_agent/config/prompts.py +0 -80
  118. phone_agent/device_factory.py +0 -166
  119. phone_agent/hdc/__init__.py +0 -53
  120. phone_agent/hdc/connection.py +0 -384
  121. phone_agent/hdc/device.py +0 -269
  122. phone_agent/hdc/input.py +0 -145
  123. phone_agent/hdc/screenshot.py +0 -127
  124. phone_agent/model/__init__.py +0 -5
  125. phone_agent/model/client.py +0 -290
  126. phone_agent/xctest/__init__.py +0 -47
  127. phone_agent/xctest/connection.py +0 -379
  128. phone_agent/xctest/device.py +0 -472
  129. phone_agent/xctest/input.py +0 -311
  130. phone_agent/xctest/screenshot.py +0 -226
  131. {phone_agent/config → AutoGLM_GUI/adb}/apps.py +0 -0
  132. {phone_agent/config → AutoGLM_GUI}/i18n.py +0 -0
  133. {autoglm_gui-1.4.1.dist-info → autoglm_gui-1.5.1.dist-info}/WHEEL +0 -0
  134. {autoglm_gui-1.4.1.dist-info → autoglm_gui-1.5.1.dist-info}/entry_points.txt +0 -0
  135. {autoglm_gui-1.4.1.dist-info → autoglm_gui-1.5.1.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(已废弃,多设备支持)。
108
67
 
109
- req_model_config = request.model or APIModelConfig()
110
- req_agent_config = request.agent or APIAgentConfig()
68
+ ⚠️ 此端点已废弃,将在未来版本移除。
69
+
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,162 @@ 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, MessageRecord
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
215
+
216
+ # 收集完整对话消息
217
+ messages: list[MessageRecord] = []
218
+ # 添加用户消息
219
+ messages.append(
220
+ MessageRecord(
221
+ role="user",
222
+ content=request.message,
223
+ timestamp=start_time,
224
+ )
225
+ )
249
226
 
250
227
  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"
228
+ acquired = manager.acquire_device(
229
+ device_id, timeout=0, raise_on_timeout=True, auto_initialize=True
230
+ )
311
231
 
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"
232
+ try:
233
+ agent = manager.get_agent(device_id)
234
+ streamer = AgentStepStreamer(agent=agent, task=request.message)
333
235
 
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
236
+ with streamer.stream_context() as abort_fn:
237
+ manager.register_abort_handler(device_id, abort_fn)
238
+
239
+ for event in streamer:
240
+ event_type = event["type"]
241
+ event_data_dict = event["data"]
346
242
 
347
243
  if (
348
- streaming_agent.step_count
349
- >= streaming_agent.agent_config.max_steps
244
+ event_type == AgentEventType.STEP.value
245
+ and event_data_dict.get("step") == -1
350
246
  ):
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
- },
247
+ continue
248
+
249
+ # 收集每个 step 的消息
250
+ if event_type == AgentEventType.STEP.value:
251
+ messages.append(
252
+ MessageRecord(
253
+ role="assistant",
254
+ content="",
255
+ timestamp=datetime.now(),
256
+ thinking=event_data_dict.get("thinking"),
257
+ action=event_data_dict.get("action"),
258
+ step=event_data_dict.get("step"),
259
+ )
358
260
  )
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()
381
261
 
262
+ if event_type == AgentEventType.DONE.value:
263
+ final_message = event_data_dict.get("message", "")
264
+ final_success = event_data_dict.get("success", False)
265
+ final_steps = event_data_dict.get("steps", 0)
266
+
267
+ event_data = _create_sse_event(event_type, event_data_dict)
268
+
269
+ yield f"event: {event_type}\n"
270
+ yield f"data: {json.dumps(event_data, ensure_ascii=False)}\n\n"
271
+
272
+ finally:
273
+ if acquired:
274
+ manager.release_device(device_id)
275
+
276
+ device_manager = DeviceManager.get_instance()
277
+ serialno = device_manager.get_serial_by_device_id(device_id)
278
+ if serialno and final_message:
279
+ end_time = datetime.now()
280
+ record = ConversationRecord(
281
+ task_text=request.message,
282
+ final_message=final_message,
283
+ success=final_success,
284
+ steps=final_steps,
285
+ start_time=start_time,
286
+ end_time=end_time,
287
+ duration_ms=int((end_time - start_time).total_seconds() * 1000),
288
+ source="chat",
289
+ error_message=None if final_success else final_message,
290
+ messages=messages,
291
+ )
292
+ history_manager.add_record(serialno, record)
293
+
294
+ except AgentInitializationError as e:
295
+ logger.error(f"Failed to initialize agent for {device_id}: {e}")
296
+ error_data = _create_sse_event(
297
+ "error",
298
+ {
299
+ "message": f"初始化失败: {str(e)}",
300
+ "hint": "请检查全局配置 (base_url, api_key, model_name)",
301
+ },
302
+ )
303
+ yield "event: error\n"
304
+ yield f"data: {json.dumps(error_data, ensure_ascii=False)}\n\n"
382
305
  except DeviceBusyError:
383
306
  error_data = _create_sse_event("error", {"message": "Device is busy"})
384
307
  yield "event: error\n"
@@ -389,13 +312,7 @@ def chat_stream(request: ChatRequest):
389
312
  yield "event: error\n"
390
313
  yield f"data: {json.dumps(error_data, ensure_ascii=False)}\n\n"
391
314
  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)
315
+ manager.unregister_abort_handler(device_id)
399
316
 
400
317
  return StreamingResponse(
401
318
  event_generator(),
@@ -493,15 +410,12 @@ def get_config_endpoint() -> ConfigResponse:
493
410
  model_name=effective_config.model_name,
494
411
  api_key=effective_config.api_key if effective_config.api_key != "EMPTY" else "",
495
412
  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
413
  agent_type=effective_config.agent_type,
503
414
  agent_config_params=effective_config.agent_config_params,
504
415
  default_max_steps=effective_config.default_max_steps,
416
+ decision_base_url=effective_config.decision_base_url,
417
+ decision_model_name=effective_config.decision_model_name,
418
+ decision_api_key=effective_config.decision_api_key,
505
419
  conflicts=[
506
420
  {
507
421
  "field": c.field,
@@ -518,8 +432,13 @@ def get_config_endpoint() -> ConfigResponse:
518
432
 
519
433
  @router.post("/api/config")
520
434
  def save_config_endpoint(request: ConfigSaveRequest) -> dict:
521
- """保存配置到文件."""
435
+ """保存配置到文件.
436
+
437
+ 副作用:保存配置后会自动销毁所有已初始化的 Agent,
438
+ 确保下次使用时所有 Agent 都使用新配置。
439
+ """
522
440
  from AutoGLM_GUI.config_manager import ConfigModel, config_manager
441
+ from AutoGLM_GUI.phone_agent_manager import PhoneAgentManager
523
442
 
524
443
  try:
525
444
  # Validate incoming configuration
@@ -534,13 +453,12 @@ def save_config_endpoint(request: ConfigSaveRequest) -> dict:
534
453
  base_url=request.base_url,
535
454
  model_name=request.model_name,
536
455
  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
456
  agent_type=request.agent_type,
542
457
  agent_config_params=request.agent_config_params,
543
458
  default_max_steps=request.default_max_steps,
459
+ decision_base_url=request.decision_base_url,
460
+ decision_model_name=request.decision_model_name,
461
+ decision_api_key=request.decision_api_key,
544
462
  merge_mode=True,
545
463
  )
546
464
 
@@ -550,9 +468,26 @@ def save_config_endpoint(request: ConfigSaveRequest) -> dict:
550
468
  # 同步到环境变量
551
469
  config_manager.sync_to_env()
552
470
 
471
+ # 副作用:销毁所有已初始化的 Agent,确保下次使用新配置
472
+ manager = PhoneAgentManager.get_instance()
473
+ destroyed_agents = manager.list_agents() # 获取需要销毁的 agent 列表
474
+
475
+ for device_id in destroyed_agents:
476
+ try:
477
+ manager.destroy_agent(device_id)
478
+ logger.info(f"Destroyed agent for {device_id} after config change")
479
+ except Exception as e:
480
+ logger.warning(f"Failed to destroy agent for {device_id}: {e}")
481
+
553
482
  # 检测冲突并返回警告
554
483
  conflicts = config_manager.detect_conflicts()
555
484
 
485
+ response_message = f"Configuration saved to {config_manager.get_config_path()}"
486
+ if destroyed_agents:
487
+ response_message += (
488
+ f". Destroyed {len(destroyed_agents)} agent(s) to apply new config."
489
+ )
490
+
556
491
  if conflicts:
557
492
  warnings = [
558
493
  f"{c.field}: file value overridden by {c.override_source.value}"
@@ -560,13 +495,15 @@ def save_config_endpoint(request: ConfigSaveRequest) -> dict:
560
495
  ]
561
496
  return {
562
497
  "success": True,
563
- "message": f"Configuration saved to {config_manager.get_config_path()}",
498
+ "message": response_message,
564
499
  "warnings": warnings,
500
+ "destroyed_agents": len(destroyed_agents),
565
501
  }
566
502
 
567
503
  return {
568
504
  "success": True,
569
- "message": f"Configuration saved to {config_manager.get_config_path()}",
505
+ "message": response_message,
506
+ "destroyed_agents": len(destroyed_agents),
570
507
  }
571
508
 
572
509
  except ValidationError as e:
@@ -590,3 +527,8 @@ def delete_config_endpoint() -> dict:
590
527
 
591
528
  except Exception as e:
592
529
  raise HTTPException(status_code=500, detail=str(e))
530
+
531
+
532
+ # ✅ 已删除 /api/agents/reinit-all 端点
533
+ # 原因:配置保存时自动销毁所有 Agent(副作用),无需单独的 reinit 端点
534
+ # 见 /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