autoglm-gui 1.4.0__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 (120) hide show
  1. AutoGLM_GUI/__init__.py +11 -0
  2. AutoGLM_GUI/__main__.py +26 -8
  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/qr_pair.py +8 -8
  15. AutoGLM_GUI/adb_plus/screenshot.py +22 -1
  16. AutoGLM_GUI/adb_plus/serial.py +38 -20
  17. AutoGLM_GUI/adb_plus/touch.py +4 -9
  18. AutoGLM_GUI/agents/__init__.py +51 -0
  19. AutoGLM_GUI/agents/events.py +19 -0
  20. AutoGLM_GUI/agents/factory.py +153 -0
  21. AutoGLM_GUI/agents/glm/__init__.py +7 -0
  22. AutoGLM_GUI/agents/glm/agent.py +292 -0
  23. AutoGLM_GUI/agents/glm/message_builder.py +81 -0
  24. AutoGLM_GUI/agents/glm/parser.py +110 -0
  25. AutoGLM_GUI/agents/glm/prompts_en.py +77 -0
  26. AutoGLM_GUI/agents/glm/prompts_zh.py +75 -0
  27. AutoGLM_GUI/agents/mai/__init__.py +28 -0
  28. AutoGLM_GUI/agents/mai/agent.py +405 -0
  29. AutoGLM_GUI/agents/mai/parser.py +254 -0
  30. AutoGLM_GUI/agents/mai/prompts.py +103 -0
  31. AutoGLM_GUI/agents/mai/traj_memory.py +91 -0
  32. AutoGLM_GUI/agents/protocols.py +27 -0
  33. AutoGLM_GUI/agents/stream_runner.py +188 -0
  34. AutoGLM_GUI/api/__init__.py +71 -11
  35. AutoGLM_GUI/api/agents.py +190 -229
  36. AutoGLM_GUI/api/control.py +9 -6
  37. AutoGLM_GUI/api/devices.py +112 -28
  38. AutoGLM_GUI/api/health.py +13 -0
  39. AutoGLM_GUI/api/history.py +78 -0
  40. AutoGLM_GUI/api/layered_agent.py +306 -181
  41. AutoGLM_GUI/api/mcp.py +11 -10
  42. AutoGLM_GUI/api/media.py +64 -1
  43. AutoGLM_GUI/api/scheduled_tasks.py +98 -0
  44. AutoGLM_GUI/api/version.py +23 -10
  45. AutoGLM_GUI/api/workflows.py +2 -1
  46. AutoGLM_GUI/config.py +72 -14
  47. AutoGLM_GUI/config_manager.py +98 -27
  48. AutoGLM_GUI/device_adapter.py +263 -0
  49. AutoGLM_GUI/device_manager.py +248 -29
  50. AutoGLM_GUI/device_protocol.py +266 -0
  51. AutoGLM_GUI/devices/__init__.py +49 -0
  52. AutoGLM_GUI/devices/adb_device.py +200 -0
  53. AutoGLM_GUI/devices/mock_device.py +185 -0
  54. AutoGLM_GUI/devices/remote_device.py +177 -0
  55. AutoGLM_GUI/exceptions.py +3 -3
  56. AutoGLM_GUI/history_manager.py +164 -0
  57. AutoGLM_GUI/i18n.py +81 -0
  58. AutoGLM_GUI/metrics.py +13 -20
  59. AutoGLM_GUI/model/__init__.py +5 -0
  60. AutoGLM_GUI/model/message_builder.py +69 -0
  61. AutoGLM_GUI/model/types.py +24 -0
  62. AutoGLM_GUI/models/__init__.py +10 -0
  63. AutoGLM_GUI/models/history.py +96 -0
  64. AutoGLM_GUI/models/scheduled_task.py +71 -0
  65. AutoGLM_GUI/parsers/__init__.py +22 -0
  66. AutoGLM_GUI/parsers/base.py +50 -0
  67. AutoGLM_GUI/parsers/phone_parser.py +58 -0
  68. AutoGLM_GUI/phone_agent_manager.py +118 -367
  69. AutoGLM_GUI/platform_utils.py +31 -2
  70. AutoGLM_GUI/prompt_config.py +15 -0
  71. AutoGLM_GUI/prompts/__init__.py +32 -0
  72. AutoGLM_GUI/scheduler_manager.py +304 -0
  73. AutoGLM_GUI/schemas.py +272 -63
  74. AutoGLM_GUI/scrcpy_stream.py +159 -37
  75. AutoGLM_GUI/server.py +3 -1
  76. AutoGLM_GUI/socketio_server.py +114 -29
  77. AutoGLM_GUI/state.py +10 -30
  78. AutoGLM_GUI/static/assets/{about-DeclntHg.js → about-BQm96DAl.js} +1 -1
  79. AutoGLM_GUI/static/assets/alert-dialog-B42XxGPR.js +1 -0
  80. AutoGLM_GUI/static/assets/chat-C0L2gQYG.js +129 -0
  81. AutoGLM_GUI/static/assets/circle-alert-D4rSJh37.js +1 -0
  82. AutoGLM_GUI/static/assets/dialog-DZ78cEcj.js +45 -0
  83. AutoGLM_GUI/static/assets/history-DFBv7TGc.js +1 -0
  84. AutoGLM_GUI/static/assets/index-Bzyv2yQ2.css +1 -0
  85. AutoGLM_GUI/static/assets/{index-zQ4KKDHt.js → index-CmZSnDqc.js} +1 -1
  86. AutoGLM_GUI/static/assets/index-CssG-3TH.js +11 -0
  87. AutoGLM_GUI/static/assets/label-BCUzE_nm.js +1 -0
  88. AutoGLM_GUI/static/assets/logs-eoFxn5of.js +1 -0
  89. AutoGLM_GUI/static/assets/popover-DLsuV5Sx.js +1 -0
  90. AutoGLM_GUI/static/assets/scheduled-tasks-MyqGJvy_.js +1 -0
  91. AutoGLM_GUI/static/assets/square-pen-zGWYrdfj.js +1 -0
  92. AutoGLM_GUI/static/assets/textarea-BX6y7uM5.js +1 -0
  93. AutoGLM_GUI/static/assets/workflows-CYFs6ssC.js +1 -0
  94. AutoGLM_GUI/static/index.html +2 -2
  95. AutoGLM_GUI/types.py +142 -0
  96. {autoglm_gui-1.4.0.dist-info → autoglm_gui-1.5.0.dist-info}/METADATA +178 -92
  97. autoglm_gui-1.5.0.dist-info/RECORD +157 -0
  98. mai_agent/base.py +137 -0
  99. mai_agent/mai_grounding_agent.py +263 -0
  100. mai_agent/mai_naivigation_agent.py +526 -0
  101. mai_agent/prompt.py +148 -0
  102. mai_agent/unified_memory.py +67 -0
  103. mai_agent/utils.py +73 -0
  104. AutoGLM_GUI/api/dual_model.py +0 -311
  105. AutoGLM_GUI/dual_model/__init__.py +0 -53
  106. AutoGLM_GUI/dual_model/decision_model.py +0 -664
  107. AutoGLM_GUI/dual_model/dual_agent.py +0 -917
  108. AutoGLM_GUI/dual_model/protocols.py +0 -354
  109. AutoGLM_GUI/dual_model/vision_model.py +0 -442
  110. AutoGLM_GUI/mai_ui_adapter/agent_wrapper.py +0 -291
  111. AutoGLM_GUI/phone_agent_patches.py +0 -146
  112. AutoGLM_GUI/static/assets/chat-Iut2yhSw.js +0 -125
  113. AutoGLM_GUI/static/assets/dialog-BfdcBs1x.js +0 -45
  114. AutoGLM_GUI/static/assets/index-5hCCwHA7.css +0 -1
  115. AutoGLM_GUI/static/assets/index-DHF1NZh0.js +0 -12
  116. AutoGLM_GUI/static/assets/workflows-xiplap-r.js +0 -1
  117. autoglm_gui-1.4.0.dist-info/RECORD +0 -100
  118. {autoglm_gui-1.4.0.dist-info → autoglm_gui-1.5.0.dist-info}/WHEEL +0 -0
  119. {autoglm_gui-1.4.0.dist-info → autoglm_gui-1.5.0.dist-info}/entry_points.txt +0 -0
  120. {autoglm_gui-1.4.0.dist-info → autoglm_gui-1.5.0.dist-info}/licenses/LICENSE +0 -0
AutoGLM_GUI/api/agents.py CHANGED
@@ -1,21 +1,16 @@
1
1
  """Agent lifecycle and chat routes."""
2
2
 
3
3
  import json
4
- import queue
5
- import threading
6
- from typing import Any
7
4
 
8
5
  from fastapi import APIRouter, HTTPException
9
6
  from fastapi.responses import StreamingResponse
10
7
  from pydantic import ValidationError
11
8
 
12
- from AutoGLM_GUI.config import config
9
+ from AutoGLM_GUI.agents.events import AgentEventType
10
+ from AutoGLM_GUI.config import AgentConfig, ModelConfig
13
11
  from AutoGLM_GUI.logger import logger
14
- from AutoGLM_GUI.phone_agent_patches import apply_patches
15
12
  from AutoGLM_GUI.schemas import (
16
13
  AbortRequest,
17
- APIAgentConfig,
18
- APIModelConfig,
19
14
  ChatRequest,
20
15
  ChatResponse,
21
16
  ConfigResponse,
@@ -28,11 +23,6 @@ from AutoGLM_GUI.state import (
28
23
  non_blocking_takeover,
29
24
  )
30
25
  from AutoGLM_GUI.version import APP_VERSION
31
- from phone_agent.agent import AgentConfig
32
- from phone_agent.model import ModelConfig
33
-
34
- # Apply monkey patches to phone_agent
35
- apply_patches()
36
26
 
37
27
  router = APIRouter()
38
28
 
@@ -60,95 +50,95 @@ def _setup_adb_keyboard(device_id: str) -> None:
60
50
  logger.info(f"✓ Device {device_id}: ADB Keyboard ready")
61
51
 
62
52
 
63
- def _initialize_agent_with_config(
64
- device_id: str,
65
- model_config: ModelConfig,
66
- agent_config: AgentConfig,
67
- ) -> None:
68
- """使用给定配置初始化 Agent。
69
-
70
- Args:
71
- device_id: 设备 ID
72
- model_config: 模型配置
73
- agent_config: Agent 配置
74
-
75
- Raises:
76
- Exception: 初始化失败时抛出异常
77
- """
78
- from AutoGLM_GUI.phone_agent_manager import PhoneAgentManager
79
-
80
- # Setup ADB Keyboard first
81
- _setup_adb_keyboard(device_id)
82
-
83
- # Initialize agent
84
- manager = PhoneAgentManager.get_instance()
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}")
53
+ SSEPayload = dict[str, str | int | bool | None | dict]
92
54
 
93
55
 
94
56
  def _create_sse_event(
95
- event_type: str, data: dict[str, Any], role: str = "assistant"
96
- ) -> dict[str, Any]:
57
+ event_type: str, data: SSEPayload, role: str = "assistant"
58
+ ) -> SSEPayload:
97
59
  """Create an SSE event with standardized fields including role."""
98
60
  event_data = {"type": event_type, "role": role, **data}
99
61
  return event_data
100
62
 
101
63
 
102
- @router.post("/api/init")
64
+ @router.post("/api/init", deprecated=True)
103
65
  def init_agent(request: InitRequest) -> dict:
104
- """初始化 PhoneAgent(多设备支持)。"""
105
- from AutoGLM_GUI.config_manager import config_manager
66
+ """初始化 PhoneAgent(已废弃,多设备支持)。
106
67
 
107
- req_model_config = request.model or APIModelConfig()
108
- 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
109
78
 
110
- device_id = req_agent_config.device_id
79
+ device_id = request.device_id
111
80
  if not device_id:
112
- raise HTTPException(
113
- status_code=400, detail="device_id is required in agent_config"
114
- )
81
+ raise HTTPException(status_code=400, detail="device_id is required")
115
82
 
116
83
  # 热重载配置文件(支持运行时手动修改)
117
84
  config_manager.load_file_config()
118
85
  config_manager.sync_to_env()
119
- config.refresh_from_env()
120
86
 
121
- base_url = req_model_config.base_url or config.base_url
122
- api_key = req_model_config.api_key or config.api_key
123
- model_name = req_model_config.model_name or config.model_name
87
+ # 获取有效配置(CLI > ENV > FILE > DEFAULT)
88
+ effective_config = config_manager.get_effective_config()
124
89
 
125
- if not base_url:
90
+ if not effective_config.base_url:
126
91
  raise HTTPException(
127
92
  status_code=400,
128
93
  detail="base_url is required. Please configure via Settings or start with --base-url",
129
94
  )
130
95
 
96
+ # 直接使用有效配置构造 ModelConfig 和 AgentConfig
131
97
  model_config = ModelConfig(
132
- base_url=base_url,
133
- api_key=api_key,
134
- model_name=model_name,
135
- max_tokens=req_model_config.max_tokens,
136
- temperature=req_model_config.temperature,
137
- top_p=req_model_config.top_p,
138
- 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 默认值
139
102
  )
140
103
 
141
104
  agent_config = AgentConfig(
142
- max_steps=req_agent_config.max_steps,
105
+ max_steps=effective_config.default_max_steps,
143
106
  device_id=device_id,
144
- lang=req_agent_config.lang,
145
- system_prompt=req_agent_config.system_prompt,
146
- verbose=req_agent_config.verbose,
107
+ # lang, system_prompt, verbose 使用 AgentConfig 默认值
147
108
  )
148
109
 
149
110
  # Initialize agent (includes ADB Keyboard setup)
150
111
  try:
151
- _initialize_agent_with_config(device_id, model_config, agent_config)
112
+ # Setup ADB Keyboard (common for all agents)
113
+ _setup_adb_keyboard(device_id)
114
+
115
+ # Use agent factory to create agent
116
+ from AutoGLM_GUI.phone_agent_manager import PhoneAgentManager
117
+
118
+ manager = PhoneAgentManager.get_instance()
119
+
120
+ # Initialize agent using factory pattern
121
+ from typing import cast
122
+
123
+ from AutoGLM_GUI.types import AgentSpecificConfig
124
+
125
+ agent_config_params = cast(
126
+ AgentSpecificConfig, request.agent_config_params or {}
127
+ )
128
+ manager.initialize_agent_with_factory(
129
+ device_id=device_id,
130
+ agent_type=request.agent_type,
131
+ model_config=model_config,
132
+ agent_config=agent_config,
133
+ agent_specific_config=agent_config_params,
134
+ takeover_callback=non_blocking_takeover,
135
+ force=request.force,
136
+ )
137
+
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."
141
+ )
152
142
  except Exception as e:
153
143
  logger.error(f"Failed to initialize agent: {e}")
154
144
  raise HTTPException(status_code=500, detail=str(e))
@@ -156,189 +146,137 @@ def init_agent(request: InitRequest) -> dict:
156
146
  return {
157
147
  "success": True,
158
148
  "device_id": device_id,
159
- "message": f"Agent initialized for device {device_id}",
149
+ "message": f"Agent initialized for device {device_id} (⚠️ /api/init is deprecated)",
150
+ "agent_type": request.agent_type,
151
+ "deprecated": True,
152
+ "hint": "Agent 会在首次使用时自动初始化,无需手动调用此端点",
160
153
  }
161
154
 
162
155
 
163
156
  @router.post("/api/chat", response_model=ChatResponse)
164
157
  def chat(request: ChatRequest) -> ChatResponse:
165
- """发送任务给 Agent 并执行。"""
166
- from AutoGLM_GUI.exceptions import DeviceBusyError
158
+ """发送任务给 Agent 并执行。
159
+
160
+ Agent 会在首次使用时自动初始化,无需手动调用 /api/init。
161
+ """
162
+ from AutoGLM_GUI.exceptions import AgentInitializationError, DeviceBusyError
167
163
  from AutoGLM_GUI.phone_agent_manager import PhoneAgentManager
168
164
 
169
165
  device_id = request.device_id
170
166
  manager = PhoneAgentManager.get_instance()
171
167
 
172
- # Check if agent is initialized
173
- if not manager.is_initialized(device_id):
174
- raise HTTPException(
175
- status_code=400, detail="Agent not initialized. Call /api/init first."
176
- )
177
-
178
- # Use context manager for automatic lock management
168
+ # use_agent 默认 auto_initialize=True,会自动初始化 Agent
179
169
  try:
180
170
  with manager.use_agent(device_id, timeout=None) as agent:
181
171
  result = agent.run(request.message)
182
172
  steps = agent.step_count
183
173
  agent.reset()
184
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
+ )
185
182
  except DeviceBusyError:
186
183
  raise HTTPException(
187
184
  status_code=409, detail=f"Device {device_id} is busy. Please wait."
188
185
  )
189
186
  except Exception as e:
187
+ logger.exception(f"Unexpected error in chat for {device_id}")
190
188
  return ChatResponse(result=str(e), steps=0, success=False)
191
189
 
192
190
 
193
191
  @router.post("/api/chat/stream")
194
192
  def chat_stream(request: ChatRequest):
195
- """发送任务给 Agent 并实时推送执行进度(SSE,多设备支持)。"""
196
- 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
197
204
  from AutoGLM_GUI.phone_agent_manager import PhoneAgentManager
198
205
 
199
206
  device_id = request.device_id
200
207
  manager = PhoneAgentManager.get_instance()
201
208
 
202
- # 验证 agent 已初始化
203
- if not manager.is_initialized(device_id):
204
- raise HTTPException(
205
- status_code=400,
206
- detail=f"Device {device_id} not initialized. Call /api/init first.",
207
- )
208
-
209
209
  def event_generator():
210
- """SSE 事件生成器."""
211
- threads: list[threading.Thread] = []
210
+ acquired = False
211
+ start_time = datetime.now()
212
+ final_message = ""
213
+ final_success = False
214
+ final_steps = 0
212
215
 
213
216
  try:
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 检查
227
- if stop_event.is_set():
228
- logger.info(f"[Abort] Chat aborted before starting for {device_id}")
229
- yield "event: aborted\n"
230
- yield 'data: {"type": "aborted", "role": "assistant", "message": "Chat aborted by user"}\n\n'
231
- return
232
-
233
- # 在线程中运行 agent 步骤
234
- step_result: list[Any] = [None]
235
- error_result: list[Any] = [None]
236
-
237
- def run_step(is_first: bool = True, task: str | None = None):
238
- try:
239
- if stop_event.is_set():
240
- return
241
-
242
- result = (
243
- streaming_agent.step(task)
244
- if is_first
245
- else streaming_agent.step()
246
- )
247
-
248
- if stop_event.is_set():
249
- return
250
-
251
- step_result[0] = result
252
- except Exception as e:
253
- error_result[0] = e
254
- finally:
255
- event_queue.put(("step_done", None))
256
-
257
- # 启动第一步
258
- thread = threading.Thread(
259
- target=run_step, args=(True, request.message), daemon=True
260
- )
261
- thread.start()
262
- threads.append(thread)
263
-
264
- # 事件循环
265
- while not stop_event.is_set():
266
- try:
267
- event_type, event_data = event_queue.get(timeout=0.1)
268
- except queue.Empty:
269
- continue
270
-
271
- if event_type == "thinking_chunk":
272
- yield "event: thinking_chunk\n"
273
- 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
+ )
274
220
 
275
- elif event_type == "step_done":
276
- if error_result[0]:
277
- raise error_result[0]
278
-
279
- result = step_result[0]
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
- )
290
-
291
- yield "event: step\n"
292
- 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)
293
224
 
294
- if result.finished:
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
- )
303
- yield "event: done\n"
304
- yield f"data: {json.dumps(done_data, ensure_ascii=False)}\n\n"
305
- break
225
+ with streamer.stream_context() as abort_fn:
226
+ manager.register_abort_handler(device_id, abort_fn)
227
+
228
+ for event in streamer:
229
+ event_type = event["type"]
230
+ event_data_dict = event["data"]
306
231
 
307
232
  if (
308
- streaming_agent.step_count
309
- >= streaming_agent.agent_config.max_steps
233
+ event_type == AgentEventType.STEP.value
234
+ and event_data_dict.get("step") == -1
310
235
  ):
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
- )
319
- yield "event: done\n"
320
- yield f"data: {json.dumps(done_data, ensure_ascii=False)}\n\n"
321
- break
322
-
323
- # 启动下一步
324
- step_result[0] = None
325
- error_result[0] = None
326
- thread = threading.Thread(
327
- target=run_step, args=(False, None), daemon=True
328
- )
329
- thread.start()
330
- threads.append(thread)
331
-
332
- # 检查是否被中止
333
- if stop_event.is_set():
334
- logger.info(f"[Abort] Streaming chat terminated for {device_id}")
335
- yield "event: aborted\n"
336
- yield 'data: {"type": "aborted", "role": "assistant", "message": "Chat aborted by user"}\n\n'
337
-
338
- # 重置原始 agent(context 已由 use_streaming_agent 同步)
339
- original_agent = manager.get_agent(device_id)
340
- original_agent.reset()
236
+ continue
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"
341
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"
342
280
  except DeviceBusyError:
343
281
  error_data = _create_sse_event("error", {"message": "Device is busy"})
344
282
  yield "event: error\n"
@@ -349,14 +287,7 @@ def chat_stream(request: ChatRequest):
349
287
  yield "event: error\n"
350
288
  yield f"data: {json.dumps(error_data, ensure_ascii=False)}\n\n"
351
289
  finally:
352
- # 通知线程停止
353
- if "stop_event" in locals():
354
- stop_event.set()
355
-
356
- # 等待线程完成(带超时)
357
- for thread in threads:
358
- if thread.is_alive():
359
- thread.join(timeout=5.0)
290
+ manager.unregister_abort_handler(device_id)
360
291
 
361
292
  return StreamingResponse(
362
293
  event_generator(),
@@ -454,12 +385,12 @@ def get_config_endpoint() -> ConfigResponse:
454
385
  model_name=effective_config.model_name,
455
386
  api_key=effective_config.api_key if effective_config.api_key != "EMPTY" else "",
456
387
  source=source.value,
457
- dual_model_enabled=effective_config.dual_model_enabled,
388
+ agent_type=effective_config.agent_type,
389
+ agent_config_params=effective_config.agent_config_params,
390
+ default_max_steps=effective_config.default_max_steps,
458
391
  decision_base_url=effective_config.decision_base_url,
459
392
  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 "",
393
+ decision_api_key=effective_config.decision_api_key,
463
394
  conflicts=[
464
395
  {
465
396
  "field": c.field,
@@ -476,8 +407,13 @@ def get_config_endpoint() -> ConfigResponse:
476
407
 
477
408
  @router.post("/api/config")
478
409
  def save_config_endpoint(request: ConfigSaveRequest) -> dict:
479
- """保存配置到文件."""
410
+ """保存配置到文件.
411
+
412
+ 副作用:保存配置后会自动销毁所有已初始化的 Agent,
413
+ 确保下次使用时所有 Agent 都使用新配置。
414
+ """
480
415
  from AutoGLM_GUI.config_manager import ConfigModel, config_manager
416
+ from AutoGLM_GUI.phone_agent_manager import PhoneAgentManager
481
417
 
482
418
  try:
483
419
  # Validate incoming configuration
@@ -492,7 +428,9 @@ def save_config_endpoint(request: ConfigSaveRequest) -> dict:
492
428
  base_url=request.base_url,
493
429
  model_name=request.model_name,
494
430
  api_key=request.api_key,
495
- dual_model_enabled=request.dual_model_enabled,
431
+ agent_type=request.agent_type,
432
+ agent_config_params=request.agent_config_params,
433
+ default_max_steps=request.default_max_steps,
496
434
  decision_base_url=request.decision_base_url,
497
435
  decision_model_name=request.decision_model_name,
498
436
  decision_api_key=request.decision_api_key,
@@ -504,11 +442,27 @@ def save_config_endpoint(request: ConfigSaveRequest) -> dict:
504
442
 
505
443
  # 同步到环境变量
506
444
  config_manager.sync_to_env()
507
- config.refresh_from_env()
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}")
508
456
 
509
457
  # 检测冲突并返回警告
510
458
  conflicts = config_manager.detect_conflicts()
511
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
+
512
466
  if conflicts:
513
467
  warnings = [
514
468
  f"{c.field}: file value overridden by {c.override_source.value}"
@@ -516,13 +470,15 @@ def save_config_endpoint(request: ConfigSaveRequest) -> dict:
516
470
  ]
517
471
  return {
518
472
  "success": True,
519
- "message": f"Configuration saved to {config_manager.get_config_path()}",
473
+ "message": response_message,
520
474
  "warnings": warnings,
475
+ "destroyed_agents": len(destroyed_agents),
521
476
  }
522
477
 
523
478
  return {
524
479
  "success": True,
525
- "message": f"Configuration saved to {config_manager.get_config_path()}",
480
+ "message": response_message,
481
+ "destroyed_agents": len(destroyed_agents),
526
482
  }
527
483
 
528
484
  except ValidationError as e:
@@ -546,3 +502,8 @@ def delete_config_endpoint() -> dict:
546
502
 
547
503
  except Exception as e:
548
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