autoglm-gui 1.2.0__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-PcGX7dIG.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-DOt5XNhh.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-B1hgBC_O.js → workflows-DHadKApI.js} +1 -1
  25. AutoGLM_GUI/static/index.html +2 -2
  26. {autoglm_gui-1.2.0.dist-info → autoglm_gui-1.3.0.dist-info}/METADATA +11 -4
  27. {autoglm_gui-1.2.0.dist-info → autoglm_gui-1.3.0.dist-info}/RECORD +30 -20
  28. AutoGLM_GUI/static/assets/chat-B0FKL2ne.js +0 -124
  29. AutoGLM_GUI/static/assets/dialog-BSNX0L1i.js +0 -45
  30. AutoGLM_GUI/static/assets/index-BjYIY--m.css +0 -1
  31. AutoGLM_GUI/static/assets/index-CnEYDOXp.js +0 -11
  32. {autoglm_gui-1.2.0.dist-info → autoglm_gui-1.3.0.dist-info}/WHEEL +0 -0
  33. {autoglm_gui-1.2.0.dist-info → autoglm_gui-1.3.0.dist-info}/entry_points.txt +0 -0
  34. {autoglm_gui-1.2.0.dist-info → autoglm_gui-1.3.0.dist-info}/licenses/LICENSE +0 -0
AutoGLM_GUI/metrics.py ADDED
@@ -0,0 +1,283 @@
1
+ """Prometheus metrics collector for AutoGLM-GUI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from typing import TYPE_CHECKING
7
+
8
+ from prometheus_client.core import CollectorRegistry, GaugeMetricFamily
9
+ from prometheus_client.registry import Collector
10
+
11
+ if TYPE_CHECKING:
12
+ from prometheus_client.core import Metric
13
+
14
+ from AutoGLM_GUI.logger import logger
15
+ from AutoGLM_GUI.version import APP_VERSION
16
+
17
+
18
+ class AutoGLMMetricsCollector(Collector):
19
+ """
20
+ Custom Prometheus collector for AutoGLM-GUI metrics.
21
+
22
+ Implements on-demand metric collection to avoid:
23
+ - Stale metric data
24
+ - Memory leaks from unbounded label cardinality
25
+ - Complexity of background metric updates
26
+
27
+ Thread Safety:
28
+ - Acquires manager locks during collect() only
29
+ - Read-only operations (no state modification)
30
+ - Uses shallow copies where needed
31
+ """
32
+
33
+ def collect(self) -> list[Metric]:
34
+ """
35
+ Called by Prometheus client on each scrape.
36
+
37
+ Returns:
38
+ List of MetricFamily objects
39
+ """
40
+ metrics = []
41
+
42
+ try:
43
+ # Agent metrics
44
+ metrics.extend(self._collect_agent_metrics())
45
+
46
+ # Device metrics
47
+ metrics.extend(self._collect_device_metrics())
48
+
49
+ # Build info
50
+ metrics.append(self._collect_build_info())
51
+
52
+ except Exception as e:
53
+ logger.error(f"Error collecting Prometheus metrics: {e}")
54
+
55
+ return metrics
56
+
57
+ def _collect_agent_metrics(self) -> list[Metric]:
58
+ """Collect agent-related metrics (high priority only)."""
59
+ from AutoGLM_GUI.device_manager import DeviceManager
60
+ from AutoGLM_GUI.phone_agent_manager import AgentState, PhoneAgentManager
61
+
62
+ metrics = []
63
+ manager = PhoneAgentManager.get_instance()
64
+ device_manager = DeviceManager.get_instance()
65
+
66
+ # Metric 1: autoglm_agents_total (per-agent state)
67
+ agents_gauge = GaugeMetricFamily(
68
+ "autoglm_agents_total",
69
+ "Agent state by device",
70
+ labels=["device_id", "serial", "state"],
71
+ )
72
+
73
+ # Metric 4: autoglm_agent_last_used_timestamp_seconds
74
+ last_used_gauge = GaugeMetricFamily(
75
+ "autoglm_agent_last_used_timestamp_seconds",
76
+ "Agent last used timestamp",
77
+ labels=["device_id", "serial"],
78
+ )
79
+
80
+ # Metric 5: autoglm_agent_created_timestamp_seconds
81
+ created_gauge = GaugeMetricFamily(
82
+ "autoglm_agent_created_timestamp_seconds",
83
+ "Agent creation timestamp",
84
+ labels=["device_id", "serial"],
85
+ )
86
+
87
+ busy_count = 0
88
+
89
+ with manager._manager_lock:
90
+ # Get snapshots (shallow copy to minimize lock time)
91
+ metadata_snapshot = dict(manager._metadata)
92
+ states_snapshot = dict(manager._states)
93
+
94
+ # Iterate over _states (not _metadata) to capture failed agents
95
+ for device_id, state in states_snapshot.items():
96
+ # Get metadata if exists (will be None for failed initialization)
97
+ metadata = metadata_snapshot.get(device_id)
98
+
99
+ # Get serial from DeviceManager
100
+ with device_manager._devices_lock:
101
+ device = device_manager.get_device_by_device_id(device_id)
102
+ serial = device.serial if device else "unknown"
103
+
104
+ # Per-agent state (1 for actual state, 0 for others)
105
+ for agent_state in AgentState:
106
+ value = 1 if state == agent_state else 0
107
+ agents_gauge.add_metric(
108
+ [device_id, serial, agent_state.value],
109
+ value,
110
+ )
111
+
112
+ # Count busy agents
113
+ if state == AgentState.BUSY:
114
+ busy_count += 1
115
+
116
+ # Timestamps (0 if metadata doesn't exist, e.g., failed init)
117
+ if metadata:
118
+ last_used_gauge.add_metric(
119
+ [device_id, serial],
120
+ metadata.last_used,
121
+ )
122
+ created_gauge.add_metric(
123
+ [device_id, serial],
124
+ metadata.created_at,
125
+ )
126
+ else:
127
+ # Failed initialization: report 0 timestamps
128
+ last_used_gauge.add_metric([device_id, serial], 0)
129
+ created_gauge.add_metric([device_id, serial], 0)
130
+
131
+ metrics.extend([agents_gauge, last_used_gauge, created_gauge])
132
+
133
+ # Metric 2: autoglm_agents_busy_count
134
+ busy_gauge = GaugeMetricFamily(
135
+ "autoglm_agents_busy_count",
136
+ "Number of busy agents",
137
+ )
138
+ busy_gauge.add_metric([], busy_count)
139
+ metrics.append(busy_gauge)
140
+
141
+ # Metric 3: autoglm_streaming_sessions_active
142
+ with manager._streaming_contexts_lock:
143
+ streaming_count = len(manager._streaming_contexts)
144
+
145
+ streaming_gauge = GaugeMetricFamily(
146
+ "autoglm_streaming_sessions_active",
147
+ "Active streaming agent sessions",
148
+ )
149
+ streaming_gauge.add_metric([], streaming_count)
150
+ metrics.append(streaming_gauge)
151
+
152
+ return metrics
153
+
154
+ def _collect_device_metrics(self) -> list[Metric]:
155
+ """Collect device-related metrics (high priority only)."""
156
+ from AutoGLM_GUI.device_manager import DeviceManager, DeviceState
157
+
158
+ metrics = []
159
+ manager = DeviceManager.get_instance()
160
+
161
+ # Metric 6: autoglm_devices_total
162
+ devices_gauge = GaugeMetricFamily(
163
+ "autoglm_devices_total",
164
+ "Device state by serial",
165
+ labels=["serial", "model", "state", "connection_type", "status"],
166
+ )
167
+
168
+ # Metric 8: autoglm_device_connections_total
169
+ connections_gauge = GaugeMetricFamily(
170
+ "autoglm_device_connections_total",
171
+ "Connection count by type",
172
+ labels=["serial", "connection_type", "status"],
173
+ )
174
+
175
+ # Metric 10: autoglm_device_last_seen_timestamp_seconds
176
+ last_seen_gauge = GaugeMetricFamily(
177
+ "autoglm_device_last_seen_timestamp_seconds",
178
+ "Device last seen timestamp",
179
+ labels=["serial", "model"],
180
+ )
181
+
182
+ online_count = 0
183
+ unauthorized_count = 0
184
+
185
+ with manager._devices_lock:
186
+ devices_snapshot = list(manager._devices.values())
187
+
188
+ # Process connected devices
189
+ for device in devices_snapshot:
190
+ model = device.model or "unknown"
191
+
192
+ # Per-device state
193
+ for dev_state in DeviceState:
194
+ value = 1 if device.state == dev_state else 0
195
+ devices_gauge.add_metric(
196
+ [
197
+ device.serial,
198
+ model,
199
+ dev_state.value,
200
+ device.connection_type.value,
201
+ device.status,
202
+ ],
203
+ value,
204
+ )
205
+
206
+ # Count online devices
207
+ if device.state == DeviceState.ONLINE:
208
+ online_count += 1
209
+
210
+ # Connection breakdown
211
+ for conn in device.connections:
212
+ connections_gauge.add_metric(
213
+ [device.serial, conn.connection_type.value, conn.status],
214
+ 1, # Each connection counts as 1
215
+ )
216
+
217
+ if conn.status == "unauthorized":
218
+ unauthorized_count += 1
219
+
220
+ # Last seen timestamp
221
+ last_seen_gauge.add_metric([device.serial, model], device.last_seen)
222
+
223
+ metrics.extend(
224
+ [
225
+ devices_gauge,
226
+ connections_gauge,
227
+ last_seen_gauge,
228
+ ]
229
+ )
230
+
231
+ # Metric 7: autoglm_devices_online_count
232
+ online_gauge = GaugeMetricFamily(
233
+ "autoglm_devices_online_count",
234
+ "Number of online devices",
235
+ )
236
+ online_gauge.add_metric([], online_count)
237
+ metrics.append(online_gauge)
238
+
239
+ # Metric 9: autoglm_device_unauthorized_connections_total
240
+ unauth_gauge = GaugeMetricFamily(
241
+ "autoglm_device_unauthorized_connections_total",
242
+ "Total unauthorized connections",
243
+ )
244
+ unauth_gauge.add_metric([], unauthorized_count)
245
+ metrics.append(unauth_gauge)
246
+
247
+ return metrics
248
+
249
+ def _collect_build_info(self) -> Metric:
250
+ """Collect build information."""
251
+ build_info = GaugeMetricFamily(
252
+ "autoglm_build_info",
253
+ "Build information",
254
+ labels=["version", "python_version"],
255
+ )
256
+
257
+ python_version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
258
+ build_info.add_metric([APP_VERSION, python_version], 1)
259
+
260
+ return build_info
261
+
262
+
263
+ # Global collector instance (registered once)
264
+ _collector_registry: CollectorRegistry | None = None
265
+ _collector_instance: AutoGLMMetricsCollector | None = None
266
+
267
+
268
+ def get_metrics_registry() -> CollectorRegistry:
269
+ """
270
+ Get or create the Prometheus registry with AutoGLM collector.
271
+
272
+ Returns:
273
+ CollectorRegistry: Registry instance for prometheus_client
274
+ """
275
+ global _collector_registry, _collector_instance
276
+
277
+ if _collector_registry is None:
278
+ _collector_registry = CollectorRegistry()
279
+ _collector_instance = AutoGLMMetricsCollector()
280
+ _collector_registry.register(_collector_instance)
281
+ logger.info("Prometheus metrics collector registered")
282
+
283
+ return _collector_registry
@@ -44,6 +44,15 @@ class AgentMetadata:
44
44
  error_message: Optional[str] = None
45
45
 
46
46
 
47
+ @dataclass
48
+ class StreamingAgentContext:
49
+ """Streaming agent 会话上下文."""
50
+
51
+ streaming_agent: "PhoneAgent"
52
+ original_agent: "PhoneAgent"
53
+ stop_event: threading.Event
54
+
55
+
47
56
  class PhoneAgentManager:
48
57
  """
49
58
  Singleton manager for PhoneAgent lifecycle and concurrency control.
@@ -91,6 +100,13 @@ class PhoneAgentManager:
91
100
  # State tracking
92
101
  self._states: dict[str, AgentState] = {}
93
102
 
103
+ # Streaming agent state (device_id -> StreamingAgentContext)
104
+ self._streaming_contexts: dict[str, StreamingAgentContext] = {}
105
+ self._streaming_contexts_lock = threading.Lock()
106
+
107
+ # Abort events (device_id -> threading.Event)
108
+ self._abort_events: dict[str, threading.Event] = {}
109
+
94
110
  @classmethod
95
111
  def get_instance(cls) -> PhoneAgentManager:
96
112
  """Get singleton instance (thread-safe, double-checked locking)."""
@@ -190,10 +206,194 @@ class PhoneAgentManager:
190
206
  f"Failed to initialize agent: {str(e)}"
191
207
  ) from e
192
208
 
209
+ def _create_streaming_agent(
210
+ self,
211
+ model_config: "ModelConfig",
212
+ agent_config: "AgentConfig",
213
+ on_thinking_chunk: Callable[[str], None],
214
+ ) -> "PhoneAgent":
215
+ """
216
+ 创建支持流式输出的 PhoneAgent(monkey-patched model_client).
217
+
218
+ Args:
219
+ model_config: 模型配置
220
+ agent_config: Agent 配置
221
+ on_thinking_chunk: 思考块回调函数
222
+
223
+ Returns:
224
+ 已 patch 的 PhoneAgent 实例
225
+ """
226
+ from phone_agent import PhoneAgent
227
+
228
+ from AutoGLM_GUI.state import non_blocking_takeover
229
+
230
+ # 创建 agent
231
+ agent = PhoneAgent(
232
+ model_config=model_config,
233
+ agent_config=agent_config,
234
+ takeover_callback=non_blocking_takeover,
235
+ )
236
+
237
+ # Monkey-patch model_client.request 以支持流式回调
238
+ original_request = agent.model_client.request
239
+
240
+ def patched_request(messages, **kwargs):
241
+ return original_request(messages, on_thinking_chunk=on_thinking_chunk)
242
+
243
+ agent.model_client.request = patched_request
244
+
245
+ return agent
246
+
247
+ @contextmanager
248
+ def use_streaming_agent(
249
+ self,
250
+ device_id: str,
251
+ on_thinking_chunk: Callable[[str], None],
252
+ timeout: Optional[float] = None,
253
+ auto_initialize: bool = True,
254
+ ):
255
+ """
256
+ Context manager for streaming-enabled agent with automatic:
257
+ - 设备锁获取/释放
258
+ - Streaming agent 创建(带 monkey-patch)
259
+ - 上下文隔离和同步
260
+ - Abort 事件注册/清理
261
+
262
+ By default, automatically initializes the agent using global configuration
263
+ if not already initialized. Set auto_initialize=False to require explicit
264
+ initialization via initialize_agent().
265
+
266
+ Args:
267
+ device_id: 设备标识符
268
+ on_thinking_chunk: 流式思考块回调函数
269
+ timeout: 锁获取超时时间(None=阻塞,0=非阻塞)
270
+ auto_initialize: 自动初始化(默认: True)
271
+
272
+ Yields:
273
+ tuple[PhoneAgent, threading.Event]: (streaming_agent, stop_event)
274
+
275
+ Raises:
276
+ DeviceBusyError: 设备忙
277
+ AgentNotInitializedError: Agent 未初始化且 auto_initialize=False
278
+ AgentInitializationError: auto_initialize=True 且初始化失败
279
+
280
+ Example:
281
+ >>> def on_chunk(chunk: str):
282
+ >>> print(chunk, end='', flush=True)
283
+ >>>
284
+ >>> with manager.use_streaming_agent("device_123", on_chunk) as (agent, stop_event):
285
+ >>> result = agent.step("Open WeChat")
286
+ """
287
+ acquired = False
288
+ streaming_agent = None
289
+ stop_event = threading.Event()
290
+
291
+ try:
292
+ # 获取设备锁(默认非阻塞)
293
+ acquired = self.acquire_device(
294
+ device_id,
295
+ timeout=timeout if timeout is not None else 0,
296
+ raise_on_timeout=True,
297
+ auto_initialize=auto_initialize,
298
+ )
299
+
300
+ # 获取原始 agent 和配置
301
+ original_agent = self.get_agent(device_id)
302
+ model_config, agent_config = self.get_config(device_id)
303
+
304
+ # 创建 streaming agent
305
+ streaming_agent = self._create_streaming_agent(
306
+ model_config=model_config,
307
+ agent_config=agent_config,
308
+ on_thinking_chunk=on_thinking_chunk,
309
+ )
310
+
311
+ # 复制上下文(由于持有设备锁,线程安全)
312
+ streaming_agent._context = original_agent._context.copy()
313
+ streaming_agent._step_count = original_agent._step_count
314
+
315
+ # 注册 abort 事件
316
+ with self._streaming_contexts_lock:
317
+ self._abort_events[device_id] = stop_event
318
+ self._streaming_contexts[device_id] = StreamingAgentContext(
319
+ streaming_agent=streaming_agent,
320
+ original_agent=original_agent,
321
+ stop_event=stop_event,
322
+ )
323
+
324
+ logger.debug(f"Streaming agent created for {device_id}")
325
+
326
+ yield streaming_agent, stop_event
327
+
328
+ finally:
329
+ # 同步状态回原始 agent
330
+ if streaming_agent and not stop_event.is_set():
331
+ original_agent = self.get_agent_safe(device_id)
332
+ if original_agent:
333
+ original_agent._context = streaming_agent._context
334
+ original_agent._step_count = streaming_agent._step_count
335
+ logger.debug(
336
+ f"Synchronized context back to original agent for {device_id}"
337
+ )
338
+
339
+ # 清理 abort 事件注册
340
+ with self._streaming_contexts_lock:
341
+ self._abort_events.pop(device_id, None)
342
+ self._streaming_contexts.pop(device_id, None)
343
+
344
+ # 释放设备锁
345
+ if acquired:
346
+ self.release_device(device_id)
347
+
348
+ def _auto_initialize_agent(self, device_id: str) -> None:
349
+ """
350
+ 使用全局配置自动初始化 agent(内部方法,需在 manager_lock 内调用).
351
+
352
+ Args:
353
+ device_id: 设备标识符
354
+
355
+ Raises:
356
+ AgentInitializationError: 如果配置不完整或初始化失败
357
+ """
358
+ from phone_agent.agent import AgentConfig
359
+ from phone_agent.model import ModelConfig
360
+
361
+ from AutoGLM_GUI.config import config
362
+ from AutoGLM_GUI.config_manager import config_manager
363
+
364
+ logger.info(f"Auto-initializing agent for device {device_id}...")
365
+
366
+ # 热重载配置
367
+ config_manager.load_file_config()
368
+ config_manager.sync_to_env()
369
+ config.refresh_from_env()
370
+
371
+ effective_config = config_manager.get_effective_config()
372
+
373
+ if not effective_config.base_url:
374
+ raise AgentInitializationError(
375
+ f"Cannot auto-initialize agent for {device_id}: base_url not configured. "
376
+ f"Please configure base_url via /api/config or call /api/init explicitly."
377
+ )
378
+
379
+ model_config = ModelConfig(
380
+ base_url=effective_config.base_url,
381
+ api_key=effective_config.api_key,
382
+ model_name=effective_config.model_name,
383
+ )
384
+
385
+ agent_config = AgentConfig(device_id=device_id)
386
+
387
+ # 调用 initialize_agent(RLock 支持重入,不会死锁)
388
+ self.initialize_agent(device_id, model_config, agent_config)
389
+ logger.info(f"Agent auto-initialized for device {device_id}")
390
+
193
391
  def get_agent(self, device_id: str) -> PhoneAgent:
194
392
  """
195
393
  Get initialized agent for a device.
196
394
 
395
+ Auto-initializes the agent using global config if not already initialized.
396
+
197
397
  Args:
198
398
  device_id: Device identifier
199
399
 
@@ -201,16 +401,14 @@ class PhoneAgentManager:
201
401
  PhoneAgent: Agent instance
202
402
 
203
403
  Raises:
204
- AgentNotInitializedError: If agent not initialized
404
+ AgentInitializationError: If agent not initialized and auto-init fails
205
405
  """
206
406
  from AutoGLM_GUI.state import agents
207
407
 
208
408
  with self._manager_lock:
209
409
  if device_id not in agents:
210
- raise AgentNotInitializedError(
211
- f"Agent not initialized for device {device_id}. "
212
- f"Call /api/init first."
213
- )
410
+ # 自动初始化:使用全局配置
411
+ self._auto_initialize_agent(device_id)
214
412
  return agents[device_id]
215
413
 
216
414
  def get_agent_safe(self, device_id: str) -> Optional[PhoneAgent]:
@@ -336,6 +534,7 @@ class PhoneAgentManager:
336
534
  device_id: str,
337
535
  timeout: Optional[float] = None,
338
536
  raise_on_timeout: bool = True,
537
+ auto_initialize: bool = False,
339
538
  ) -> bool:
340
539
  """
341
540
  Acquire device lock for exclusive access.
@@ -344,19 +543,28 @@ class PhoneAgentManager:
344
543
  device_id: Device identifier
345
544
  timeout: Lock acquisition timeout (None = blocking, 0 = non-blocking)
346
545
  raise_on_timeout: Raise DeviceBusyError on timeout
546
+ auto_initialize: Auto-initialize agent if not already initialized (default: False)
347
547
 
348
548
  Returns:
349
549
  bool: True if acquired, False if timeout (when raise_on_timeout=False)
350
550
 
351
551
  Raises:
352
552
  DeviceBusyError: If timeout and raise_on_timeout=True
353
- AgentNotInitializedError: If agent not initialized
553
+ AgentNotInitializedError: If agent not initialized AND auto_initialize=False
554
+ AgentInitializationError: If auto_initialize=True and initialization fails
354
555
  """
355
- # Verify agent exists
556
+ # Verify agent exists (with optional auto-initialization)
356
557
  if not self.is_initialized(device_id):
357
- raise AgentNotInitializedError(
358
- f"Agent not initialized for device {device_id}"
359
- )
558
+ if auto_initialize:
559
+ # Double-check locking pattern for thread safety
560
+ with self._manager_lock:
561
+ if not self.is_initialized(device_id):
562
+ self._auto_initialize_agent(device_id)
563
+ else:
564
+ raise AgentNotInitializedError(
565
+ f"Agent not initialized for device {device_id}. "
566
+ f"Use auto_initialize=True or call initialize_agent() first."
567
+ )
360
568
 
361
569
  lock = self._get_device_lock(device_id)
362
570
 
@@ -407,29 +615,47 @@ class PhoneAgentManager:
407
615
  logger.debug(f"Device lock released for {device_id}")
408
616
 
409
617
  @contextmanager
410
- def use_agent(self, device_id: str, timeout: Optional[float] = None):
618
+ def use_agent(
619
+ self,
620
+ device_id: str,
621
+ timeout: Optional[float] = None,
622
+ auto_initialize: bool = True,
623
+ ):
411
624
  """
412
625
  Context manager for automatic lock acquisition/release.
413
626
 
627
+ By default, automatically initializes the agent using global configuration
628
+ if not already initialized. Set auto_initialize=False to require explicit
629
+ initialization via initialize_agent().
630
+
414
631
  Args:
415
632
  device_id: Device identifier
416
633
  timeout: Lock acquisition timeout
634
+ auto_initialize: Auto-initialize if not already initialized (default: True)
417
635
 
418
636
  Yields:
419
637
  PhoneAgent: Agent instance
420
638
 
421
639
  Raises:
422
640
  DeviceBusyError: If device is busy
423
- AgentNotInitializedError: If agent not initialized
641
+ AgentNotInitializedError: If agent not initialized AND auto_initialize=False
642
+ AgentInitializationError: If auto_initialize=True and initialization fails
424
643
 
425
644
  Example:
426
645
  >>> manager = PhoneAgentManager.get_instance()
427
- >>> with manager.use_agent("device_123") as agent:
646
+ >>> with manager.use_agent("device_123") as agent: # Auto-initializes
428
647
  >>> result = agent.run("Open WeChat")
648
+ >>> with manager.use_agent("device_123", auto_initialize=False) as agent:
649
+ >>> result = agent.run("Open WeChat") # Requires prior init
429
650
  """
430
651
  acquired = False
431
652
  try:
432
- acquired = self.acquire_device(device_id, timeout, raise_on_timeout=True)
653
+ acquired = self.acquire_device(
654
+ device_id,
655
+ timeout,
656
+ raise_on_timeout=True,
657
+ auto_initialize=auto_initialize,
658
+ )
433
659
  agent = self.get_agent(device_id)
434
660
  yield agent
435
661
  except Exception as exc:
@@ -547,3 +773,27 @@ class PhoneAgentManager:
547
773
  """Get agent metadata."""
548
774
  with self._manager_lock:
549
775
  return self._metadata.get(device_id)
776
+
777
+ def abort_streaming_chat(self, device_id: str) -> bool:
778
+ """
779
+ 中止正在进行的流式对话.
780
+
781
+ Args:
782
+ device_id: 设备标识符
783
+
784
+ Returns:
785
+ bool: True 表示发送了中止信号,False 表示没有活跃会话
786
+ """
787
+ with self._streaming_contexts_lock:
788
+ if device_id in self._abort_events:
789
+ logger.info(f"Aborting streaming chat for device {device_id}")
790
+ self._abort_events[device_id].set()
791
+ return True
792
+ else:
793
+ logger.warning(f"No active streaming chat for device {device_id}")
794
+ return False
795
+
796
+ def is_streaming_active(self, device_id: str) -> bool:
797
+ """检查设备是否有活跃的流式会话."""
798
+ with self._streaming_contexts_lock:
799
+ return device_id in self._abort_events