autoglm-gui 1.1.0__py3-none-any.whl → 1.2.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 (35) hide show
  1. AutoGLM_GUI/adb_plus/__init__.py +5 -1
  2. AutoGLM_GUI/adb_plus/serial.py +61 -2
  3. AutoGLM_GUI/adb_plus/version.py +81 -0
  4. AutoGLM_GUI/api/__init__.py +8 -1
  5. AutoGLM_GUI/api/agents.py +329 -94
  6. AutoGLM_GUI/api/devices.py +145 -164
  7. AutoGLM_GUI/api/workflows.py +70 -0
  8. AutoGLM_GUI/device_manager.py +760 -0
  9. AutoGLM_GUI/exceptions.py +18 -0
  10. AutoGLM_GUI/phone_agent_manager.py +549 -0
  11. AutoGLM_GUI/phone_agent_patches.py +146 -0
  12. AutoGLM_GUI/schemas.py +310 -2
  13. AutoGLM_GUI/state.py +21 -0
  14. AutoGLM_GUI/static/assets/{about-Crpy4Xue.js → about-BtBH1xKN.js} +1 -1
  15. AutoGLM_GUI/static/assets/chat-DPzFNNGu.js +124 -0
  16. AutoGLM_GUI/static/assets/dialog-Dwuk2Hgl.js +45 -0
  17. AutoGLM_GUI/static/assets/index-B_AaKuOT.js +1 -0
  18. AutoGLM_GUI/static/assets/index-BjYIY--m.css +1 -0
  19. AutoGLM_GUI/static/assets/index-CvQkCi2d.js +11 -0
  20. AutoGLM_GUI/static/assets/logo-Cyfm06Ym.png +0 -0
  21. AutoGLM_GUI/static/assets/workflows-xX_QH-wI.js +1 -0
  22. AutoGLM_GUI/static/favicon.ico +0 -0
  23. AutoGLM_GUI/static/index.html +9 -2
  24. AutoGLM_GUI/static/logo-192.png +0 -0
  25. AutoGLM_GUI/static/logo-512.png +0 -0
  26. AutoGLM_GUI/workflow_manager.py +181 -0
  27. {autoglm_gui-1.1.0.dist-info → autoglm_gui-1.2.1.dist-info}/METADATA +51 -6
  28. {autoglm_gui-1.1.0.dist-info → autoglm_gui-1.2.1.dist-info}/RECORD +31 -19
  29. AutoGLM_GUI/static/assets/chat-DGFuSj6_.js +0 -149
  30. AutoGLM_GUI/static/assets/index-C1k5Ch1V.js +0 -10
  31. AutoGLM_GUI/static/assets/index-COYnSjzf.js +0 -1
  32. AutoGLM_GUI/static/assets/index-QX6oy21q.css +0 -1
  33. {autoglm_gui-1.1.0.dist-info → autoglm_gui-1.2.1.dist-info}/WHEEL +0 -0
  34. {autoglm_gui-1.1.0.dist-info → autoglm_gui-1.2.1.dist-info}/entry_points.txt +0 -0
  35. {autoglm_gui-1.1.0.dist-info → autoglm_gui-1.2.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,146 @@
1
+ """
2
+ Monkey patches for phone_agent to add streaming functionality.
3
+
4
+ This module patches the upstream phone_agent code without modifying the original files.
5
+ """
6
+
7
+ from typing import Any, Callable
8
+
9
+ from phone_agent.model import ModelClient
10
+
11
+
12
+ # Store original methods
13
+ _original_model_request = ModelClient.request
14
+
15
+
16
+ def _patched_model_request(
17
+ self,
18
+ messages: list[dict[str, Any]],
19
+ on_thinking_chunk: Callable[[str], None] | None = None,
20
+ ) -> Any:
21
+ """
22
+ Patched version of ModelClient.request that supports streaming thinking chunks.
23
+
24
+ This wraps the original request method and adds callback support for thinking chunks.
25
+ """
26
+ import time
27
+
28
+ from phone_agent.model.client import ModelResponse
29
+
30
+ # Start timing
31
+ start_time = time.time()
32
+ time_to_first_token = None
33
+ time_to_thinking_end = None
34
+
35
+ stream = self.client.chat.completions.create(
36
+ messages=messages,
37
+ model=self.config.model_name,
38
+ max_tokens=self.config.max_tokens,
39
+ temperature=self.config.temperature,
40
+ top_p=self.config.top_p,
41
+ frequency_penalty=self.config.frequency_penalty,
42
+ extra_body=self.config.extra_body,
43
+ stream=True,
44
+ )
45
+
46
+ raw_content = ""
47
+ buffer = "" # Buffer to hold content that might be part of a marker
48
+ action_markers = ["finish(message=", "do(action="]
49
+ in_action_phase = False # Track if we've entered the action phase
50
+ first_token_received = False
51
+
52
+ for chunk in stream:
53
+ if len(chunk.choices) == 0:
54
+ continue
55
+ if chunk.choices[0].delta.content is not None:
56
+ content = chunk.choices[0].delta.content
57
+ raw_content += content
58
+
59
+ # Record time to first token
60
+ if not first_token_received:
61
+ time_to_first_token = time.time() - start_time
62
+ first_token_received = True
63
+
64
+ if in_action_phase:
65
+ # Already in action phase, just accumulate content without printing
66
+ continue
67
+
68
+ buffer += content
69
+
70
+ # Check if any marker is fully present in buffer
71
+ marker_found = False
72
+ for marker in action_markers:
73
+ if marker in buffer:
74
+ # Marker found, print everything before it
75
+ thinking_part = buffer.split(marker, 1)[0]
76
+ print(thinking_part, end="", flush=True)
77
+ if on_thinking_chunk:
78
+ on_thinking_chunk(thinking_part)
79
+ print() # Print newline after thinking is complete
80
+ in_action_phase = True
81
+ marker_found = True
82
+
83
+ # Record time to thinking end
84
+ if time_to_thinking_end is None:
85
+ time_to_thinking_end = time.time() - start_time
86
+
87
+ break
88
+
89
+ if marker_found:
90
+ continue # Continue to collect remaining content
91
+
92
+ # Check if buffer ends with a prefix of any marker
93
+ # If so, don't print yet (wait for more content)
94
+ is_potential_marker = False
95
+ for marker in action_markers:
96
+ for i in range(1, len(marker)):
97
+ if buffer.endswith(marker[:i]):
98
+ is_potential_marker = True
99
+ break
100
+ if is_potential_marker:
101
+ break
102
+
103
+ if not is_potential_marker:
104
+ # Safe to print the buffer
105
+ print(buffer, end="", flush=True)
106
+ if on_thinking_chunk:
107
+ on_thinking_chunk(buffer)
108
+ buffer = ""
109
+
110
+ # Calculate total time
111
+ total_time = time.time() - start_time
112
+
113
+ # Parse thinking and action from response
114
+ thinking, action = self._parse_response(raw_content)
115
+
116
+ # Print performance metrics
117
+ from phone_agent.config.i18n import get_message
118
+
119
+ lang = self.config.lang
120
+ print()
121
+ print("=" * 50)
122
+ print(f"⏱️ {get_message('performance_metrics', lang)}:")
123
+ print("-" * 50)
124
+ if time_to_first_token is not None:
125
+ print(f"{get_message('time_to_first_token', lang)}: {time_to_first_token:.3f}s")
126
+ if time_to_thinking_end is not None:
127
+ print(
128
+ f"{get_message('time_to_thinking_end', lang)}: {time_to_thinking_end:.3f}s"
129
+ )
130
+ print(f"{get_message('total_inference_time', lang)}: {total_time:.3f}s")
131
+ print("=" * 50)
132
+
133
+ return ModelResponse(
134
+ thinking=thinking,
135
+ action=action,
136
+ raw_content=raw_content,
137
+ time_to_first_token=time_to_first_token,
138
+ time_to_thinking_end=time_to_thinking_end,
139
+ total_time=total_time,
140
+ )
141
+
142
+
143
+ def apply_patches():
144
+ """Apply all monkey patches to phone_agent."""
145
+ # Patch ModelClient.request to support streaming callbacks
146
+ ModelClient.request = _patched_model_request
AutoGLM_GUI/schemas.py CHANGED
@@ -1,6 +1,8 @@
1
1
  """Shared Pydantic models for the AutoGLM-GUI API."""
2
2
 
3
- from pydantic import BaseModel, Field
3
+ import re
4
+
5
+ from pydantic import BaseModel, Field, field_validator
4
6
 
5
7
 
6
8
  class APIModelConfig(BaseModel):
@@ -12,6 +14,20 @@ class APIModelConfig(BaseModel):
12
14
  top_p: float = 0.85
13
15
  frequency_penalty: float = 0.2
14
16
 
17
+ @field_validator("base_url")
18
+ @classmethod
19
+ def validate_base_url(cls, v: str | None) -> str | None:
20
+ """验证 base_url 格式."""
21
+ if v is None:
22
+ return v
23
+ v = v.strip()
24
+ if not v:
25
+ return None
26
+ # 检查是否是有效的 HTTP/HTTPS URL
27
+ if not re.match(r"^https?://", v):
28
+ raise ValueError("base_url must start with http:// or https://")
29
+ return v
30
+
15
31
 
16
32
  class APIAgentConfig(BaseModel):
17
33
  max_steps: int = 100
@@ -20,6 +36,25 @@ class APIAgentConfig(BaseModel):
20
36
  system_prompt: str | None = None
21
37
  verbose: bool = True
22
38
 
39
+ @field_validator("max_steps")
40
+ @classmethod
41
+ def validate_max_steps(cls, v: int) -> int:
42
+ """验证 max_steps 范围."""
43
+ if v <= 0:
44
+ raise ValueError("max_steps must be positive")
45
+ if v > 1000:
46
+ raise ValueError("max_steps must be <= 1000")
47
+ return v
48
+
49
+ @field_validator("lang")
50
+ @classmethod
51
+ def validate_lang(cls, v: str) -> str:
52
+ """验证 lang 有效性."""
53
+ allowed_langs = ["cn", "en"]
54
+ if v not in allowed_langs:
55
+ raise ValueError(f"lang must be one of {allowed_langs}")
56
+ return v
57
+
23
58
 
24
59
  class InitRequest(BaseModel):
25
60
  model: APIModelConfig | None = Field(default=None, alias="model_config")
@@ -30,6 +65,16 @@ class ChatRequest(BaseModel):
30
65
  message: str
31
66
  device_id: str # 设备 ID(必填)
32
67
 
68
+ @field_validator("message")
69
+ @classmethod
70
+ def validate_message(cls, v: str) -> str:
71
+ """验证 message 非空."""
72
+ if not v or not v.strip():
73
+ raise ValueError("message cannot be empty")
74
+ if len(v) > 10000:
75
+ raise ValueError("message too long (max 10000 characters)")
76
+ return v.strip()
77
+
33
78
 
34
79
  class ChatResponse(BaseModel):
35
80
  result: str
@@ -47,6 +92,12 @@ class ResetRequest(BaseModel):
47
92
  device_id: str # 设备 ID(必填)
48
93
 
49
94
 
95
+ class AbortRequest(BaseModel):
96
+ """中断对话请求。"""
97
+
98
+ device_id: str # 设备 ID(必填)
99
+
100
+
50
101
  class ScreenshotRequest(BaseModel):
51
102
  device_id: str | None = None
52
103
 
@@ -66,6 +117,26 @@ class TapRequest(BaseModel):
66
117
  device_id: str | None = None
67
118
  delay: float = 0.0
68
119
 
120
+ @field_validator("x", "y")
121
+ @classmethod
122
+ def validate_coordinates(cls, v: int) -> int:
123
+ """验证坐标范围."""
124
+ if v < 0:
125
+ raise ValueError("coordinates must be non-negative")
126
+ if v > 10000: # 合理的最大屏幕尺寸
127
+ raise ValueError("coordinates must be <= 10000")
128
+ return v
129
+
130
+ @field_validator("delay")
131
+ @classmethod
132
+ def validate_delay(cls, v: float) -> float:
133
+ """验证 delay 范围."""
134
+ if v < 0.0:
135
+ raise ValueError("delay must be non-negative")
136
+ if v > 60.0: # 最大等待 60 秒
137
+ raise ValueError("delay must be <= 60.0 seconds")
138
+ return v
139
+
69
140
 
70
141
  class TapResponse(BaseModel):
71
142
  success: bool
@@ -81,6 +152,37 @@ class SwipeRequest(BaseModel):
81
152
  device_id: str | None = None
82
153
  delay: float = 0.0
83
154
 
155
+ @field_validator("start_x", "start_y", "end_x", "end_y")
156
+ @classmethod
157
+ def validate_coordinates(cls, v: int) -> int:
158
+ """验证坐标范围."""
159
+ if v < 0:
160
+ raise ValueError("coordinates must be non-negative")
161
+ if v > 10000:
162
+ raise ValueError("coordinates must be <= 10000")
163
+ return v
164
+
165
+ @field_validator("duration_ms")
166
+ @classmethod
167
+ def validate_duration(cls, v: int | None) -> int | None:
168
+ """验证滑动持续时间."""
169
+ if v is not None:
170
+ if v < 0:
171
+ raise ValueError("duration_ms must be non-negative")
172
+ if v > 10000: # 最大 10 秒
173
+ raise ValueError("duration_ms must be <= 10000")
174
+ return v
175
+
176
+ @field_validator("delay")
177
+ @classmethod
178
+ def validate_delay(cls, v: float) -> float:
179
+ """验证 delay 范围."""
180
+ if v < 0.0:
181
+ raise ValueError("delay must be non-negative")
182
+ if v > 60.0:
183
+ raise ValueError("delay must be <= 60.0 seconds")
184
+ return v
185
+
84
186
 
85
187
  class SwipeResponse(BaseModel):
86
188
  success: bool
@@ -93,6 +195,26 @@ class TouchDownRequest(BaseModel):
93
195
  device_id: str | None = None
94
196
  delay: float = 0.0
95
197
 
198
+ @field_validator("x", "y")
199
+ @classmethod
200
+ def validate_coordinates(cls, v: int) -> int:
201
+ """验证坐标范围."""
202
+ if v < 0:
203
+ raise ValueError("coordinates must be non-negative")
204
+ if v > 10000:
205
+ raise ValueError("coordinates must be <= 10000")
206
+ return v
207
+
208
+ @field_validator("delay")
209
+ @classmethod
210
+ def validate_delay(cls, v: float) -> float:
211
+ """验证 delay 范围."""
212
+ if v < 0.0:
213
+ raise ValueError("delay must be non-negative")
214
+ if v > 60.0:
215
+ raise ValueError("delay must be <= 60.0 seconds")
216
+ return v
217
+
96
218
 
97
219
  class TouchDownResponse(BaseModel):
98
220
  success: bool
@@ -105,6 +227,26 @@ class TouchMoveRequest(BaseModel):
105
227
  device_id: str | None = None
106
228
  delay: float = 0.0
107
229
 
230
+ @field_validator("x", "y")
231
+ @classmethod
232
+ def validate_coordinates(cls, v: int) -> int:
233
+ """验证坐标范围."""
234
+ if v < 0:
235
+ raise ValueError("coordinates must be non-negative")
236
+ if v > 10000:
237
+ raise ValueError("coordinates must be <= 10000")
238
+ return v
239
+
240
+ @field_validator("delay")
241
+ @classmethod
242
+ def validate_delay(cls, v: float) -> float:
243
+ """验证 delay 范围."""
244
+ if v < 0.0:
245
+ raise ValueError("delay must be non-negative")
246
+ if v > 60.0:
247
+ raise ValueError("delay must be <= 60.0 seconds")
248
+ return v
249
+
108
250
 
109
251
  class TouchMoveResponse(BaseModel):
110
252
  success: bool
@@ -117,14 +259,57 @@ class TouchUpRequest(BaseModel):
117
259
  device_id: str | None = None
118
260
  delay: float = 0.0
119
261
 
262
+ @field_validator("x", "y")
263
+ @classmethod
264
+ def validate_coordinates(cls, v: int) -> int:
265
+ """验证坐标范围."""
266
+ if v < 0:
267
+ raise ValueError("coordinates must be non-negative")
268
+ if v > 10000:
269
+ raise ValueError("coordinates must be <= 10000")
270
+ return v
271
+
272
+ @field_validator("delay")
273
+ @classmethod
274
+ def validate_delay(cls, v: float) -> float:
275
+ """验证 delay 范围."""
276
+ if v < 0.0:
277
+ raise ValueError("delay must be non-negative")
278
+ if v > 60.0:
279
+ raise ValueError("delay must be <= 60.0 seconds")
280
+ return v
281
+
120
282
 
121
283
  class TouchUpResponse(BaseModel):
122
284
  success: bool
123
285
  error: str | None = None
124
286
 
125
287
 
288
+ class AgentStatusResponse(BaseModel):
289
+ """Agent 运行状态信息."""
290
+
291
+ state: str # "idle" | "busy" | "error" | "initializing"
292
+ created_at: float # Unix 时间戳
293
+ last_used: float # Unix 时间戳
294
+ error_message: str | None = None
295
+ model_name: str # 来自 ModelConfig
296
+
297
+
298
+ class DeviceResponse(BaseModel):
299
+ """设备信息及可选的 Agent 状态."""
300
+
301
+ id: str
302
+ serial: str
303
+ model: str
304
+ status: str
305
+ connection_type: str
306
+ state: str
307
+ is_available_only: bool
308
+ agent: AgentStatusResponse | None = None
309
+
310
+
126
311
  class DeviceListResponse(BaseModel):
127
- devices: list[dict]
312
+ devices: list[DeviceResponse] # 从 list[dict] 改为强类型
128
313
 
129
314
 
130
315
  class ConfigResponse(BaseModel):
@@ -153,11 +338,38 @@ class ConfigSaveRequest(BaseModel):
153
338
  model_name: str = "autoglm-phone-9b"
154
339
  api_key: str | None = None
155
340
 
341
+ @field_validator("base_url")
342
+ @classmethod
343
+ def validate_base_url(cls, v: str) -> str:
344
+ """验证 base_url 格式."""
345
+ v = v.strip()
346
+ if not v:
347
+ raise ValueError("base_url cannot be empty")
348
+ if not re.match(r"^https?://", v):
349
+ raise ValueError("base_url must start with http:// or https://")
350
+ return v
351
+
352
+ @field_validator("model_name")
353
+ @classmethod
354
+ def validate_model_name(cls, v: str) -> str:
355
+ """验证 model_name 非空."""
356
+ if not v or not v.strip():
357
+ raise ValueError("model_name cannot be empty")
358
+ return v.strip()
359
+
156
360
 
157
361
  class WiFiConnectRequest(BaseModel):
158
362
  device_id: str | None = None
159
363
  port: int = 5555
160
364
 
365
+ @field_validator("port")
366
+ @classmethod
367
+ def validate_port(cls, v: int) -> int:
368
+ """验证端口范围."""
369
+ if v < 1 or v > 65535:
370
+ raise ValueError("port must be between 1 and 65535")
371
+ return v
372
+
161
373
 
162
374
  class WiFiConnectResponse(BaseModel):
163
375
  success: bool
@@ -183,6 +395,25 @@ class WiFiManualConnectRequest(BaseModel):
183
395
  ip: str # IP 地址
184
396
  port: int = 5555 # 端口,默认 5555
185
397
 
398
+ @field_validator("ip")
399
+ @classmethod
400
+ def validate_ip(cls, v: str) -> str:
401
+ """验证 IP 地址格式."""
402
+ v = v.strip()
403
+ # 简单的 IPv4 格式验证
404
+ ip_pattern = r"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$"
405
+ if not re.match(ip_pattern, v):
406
+ raise ValueError("invalid IPv4 address format")
407
+ return v
408
+
409
+ @field_validator("port")
410
+ @classmethod
411
+ def validate_port(cls, v: int) -> int:
412
+ """验证端口范围."""
413
+ if v < 1 or v > 65535:
414
+ raise ValueError("port must be between 1 and 65535")
415
+ return v
416
+
186
417
 
187
418
  class WiFiManualConnectResponse(BaseModel):
188
419
  """手动连接 WiFi 响应."""
@@ -201,6 +432,33 @@ class WiFiPairRequest(BaseModel):
201
432
  pairing_code: str # 6-digit pairing code
202
433
  connection_port: int = 5555 # Standard ADB connection port (default 5555)
203
434
 
435
+ @field_validator("ip")
436
+ @classmethod
437
+ def validate_ip(cls, v: str) -> str:
438
+ """验证 IP 地址格式."""
439
+ v = v.strip()
440
+ ip_pattern = r"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$"
441
+ if not re.match(ip_pattern, v):
442
+ raise ValueError("invalid IPv4 address format")
443
+ return v
444
+
445
+ @field_validator("pairing_port", "connection_port")
446
+ @classmethod
447
+ def validate_port(cls, v: int) -> int:
448
+ """验证端口范围."""
449
+ if v < 1 or v > 65535:
450
+ raise ValueError("port must be between 1 and 65535")
451
+ return v
452
+
453
+ @field_validator("pairing_code")
454
+ @classmethod
455
+ def validate_pairing_code(cls, v: str) -> str:
456
+ """验证配对码格式."""
457
+ v = v.strip()
458
+ if not re.match(r"^\d{6}$", v):
459
+ raise ValueError("pairing_code must be a 6-digit number")
460
+ return v
461
+
204
462
 
205
463
  class WiFiPairResponse(BaseModel):
206
464
  """WiFi pairing response."""
@@ -272,3 +530,53 @@ class QRPairCancelResponse(BaseModel):
272
530
 
273
531
  success: bool
274
532
  message: str
533
+
534
+
535
+ # Workflow Models
536
+
537
+
538
+ class WorkflowBase(BaseModel):
539
+ """Workflow 基础模型."""
540
+
541
+ name: str
542
+ text: str
543
+
544
+ @field_validator("name")
545
+ @classmethod
546
+ def validate_name(cls, v: str) -> str:
547
+ """验证 name 非空."""
548
+ if not v or not v.strip():
549
+ raise ValueError("name cannot be empty")
550
+ return v.strip()
551
+
552
+ @field_validator("text")
553
+ @classmethod
554
+ def validate_text(cls, v: str) -> str:
555
+ """验证 text 非空."""
556
+ if not v or not v.strip():
557
+ raise ValueError("text cannot be empty")
558
+ return v.strip()
559
+
560
+
561
+ class WorkflowCreate(WorkflowBase):
562
+ """创建 Workflow 请求."""
563
+
564
+ pass
565
+
566
+
567
+ class WorkflowUpdate(WorkflowBase):
568
+ """更新 Workflow 请求."""
569
+
570
+ pass
571
+
572
+
573
+ class WorkflowResponse(WorkflowBase):
574
+ """Workflow 响应."""
575
+
576
+ uuid: str
577
+
578
+
579
+ class WorkflowListResponse(BaseModel):
580
+ """Workflow 列表响应."""
581
+
582
+ workflows: list[WorkflowResponse]
AutoGLM_GUI/state.py CHANGED
@@ -14,8 +14,29 @@ if TYPE_CHECKING:
14
14
  from phone_agent import PhoneAgent
15
15
 
16
16
  # Agent instances keyed by device_id
17
+ #
18
+ # IMPORTANT: Managed by PhoneAgentManager (AutoGLM_GUI/phone_agent_manager.py)
19
+ # - Do NOT directly modify these dictionaries
20
+ # - Use PhoneAgentManager.get_instance() for all agent operations
21
+ #
22
+ # device_id changes when connection method changes
23
+ # (e.g., USB "ABC123" → WiFi "192.168.1.100:5555")
24
+ #
25
+ # This means the same physical device may have different device_ids:
26
+ # - USB connection: device_id = hardware serial (e.g., "ABC123DEF")
27
+ # - WiFi connection: device_id = IP:port (e.g., "192.168.1.100:5555")
28
+ # - mDNS connection: device_id = service name (e.g., "adb-ABC123._adb-tls-connect._tcp")
29
+ #
30
+ # DeviceManager tracks devices by hardware serial and maintains
31
+ # device_id ↔ serial mapping. Use PhoneAgentManager.find_agent_by_serial()
32
+ # to find agents when device_id changes.
33
+ #
34
+ # See CLAUDE.md "Device Identification" section for details.
17
35
  agents: dict[str, "PhoneAgent"] = {}
36
+
18
37
  # Cached configs to rebuild agents on reset
38
+ # Keyed by device_id (same semantics as agents dict)
39
+ # IMPORTANT: Managed by PhoneAgentManager - do NOT modify directly
19
40
  agent_configs: dict[str, tuple[ModelConfig, AgentConfig]] = {}
20
41
 
21
42
  # Scrcpy streaming per device
@@ -1 +1 @@
1
- import{j as o}from"./index-C1k5Ch1V.js";function t(){return o.jsx("div",{className:"p-2",children:o.jsx("h3",{children:"About"})})}export{t as component};
1
+ import{j as o}from"./index-CvQkCi2d.js";function t(){return o.jsx("div",{className:"p-2",children:o.jsx("h3",{children:"About"})})}export{t as component};