autoglm-gui 1.0.2__py3-none-any.whl → 1.2.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.
- AutoGLM_GUI/adb_plus/__init__.py +12 -1
- AutoGLM_GUI/adb_plus/mdns.py +192 -0
- AutoGLM_GUI/adb_plus/pair.py +60 -0
- AutoGLM_GUI/adb_plus/qr_pair.py +372 -0
- AutoGLM_GUI/adb_plus/serial.py +61 -2
- AutoGLM_GUI/adb_plus/version.py +81 -0
- AutoGLM_GUI/api/__init__.py +16 -1
- AutoGLM_GUI/api/agents.py +329 -94
- AutoGLM_GUI/api/devices.py +304 -100
- AutoGLM_GUI/api/workflows.py +70 -0
- AutoGLM_GUI/device_manager.py +760 -0
- AutoGLM_GUI/exceptions.py +18 -0
- AutoGLM_GUI/phone_agent_manager.py +549 -0
- AutoGLM_GUI/phone_agent_patches.py +146 -0
- AutoGLM_GUI/schemas.py +380 -2
- AutoGLM_GUI/state.py +21 -0
- AutoGLM_GUI/static/assets/{about-BOnRPlKQ.js → about-PcGX7dIG.js} +1 -1
- AutoGLM_GUI/static/assets/chat-B0FKL2ne.js +124 -0
- AutoGLM_GUI/static/assets/dialog-BSNX0L1i.js +45 -0
- AutoGLM_GUI/static/assets/index-BjYIY--m.css +1 -0
- AutoGLM_GUI/static/assets/index-CnEYDOXp.js +11 -0
- AutoGLM_GUI/static/assets/index-DOt5XNhh.js +1 -0
- AutoGLM_GUI/static/assets/logo-Cyfm06Ym.png +0 -0
- AutoGLM_GUI/static/assets/workflows-B1hgBC_O.js +1 -0
- AutoGLM_GUI/static/favicon.ico +0 -0
- AutoGLM_GUI/static/index.html +9 -2
- AutoGLM_GUI/static/logo-192.png +0 -0
- AutoGLM_GUI/static/logo-512.png +0 -0
- AutoGLM_GUI/workflow_manager.py +181 -0
- {autoglm_gui-1.0.2.dist-info → autoglm_gui-1.2.0.dist-info}/METADATA +80 -35
- {autoglm_gui-1.0.2.dist-info → autoglm_gui-1.2.0.dist-info}/RECORD +34 -19
- AutoGLM_GUI/static/assets/chat-CGW6uMKB.js +0 -149
- AutoGLM_GUI/static/assets/index-CRFVU0eu.js +0 -1
- AutoGLM_GUI/static/assets/index-DH-Dl4tK.js +0 -10
- AutoGLM_GUI/static/assets/index-DzUQ89YC.css +0 -1
- {autoglm_gui-1.0.2.dist-info → autoglm_gui-1.2.0.dist-info}/WHEEL +0 -0
- {autoglm_gui-1.0.2.dist-info → autoglm_gui-1.2.0.dist-info}/entry_points.txt +0 -0
- {autoglm_gui-1.0.2.dist-info → autoglm_gui-1.2.0.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
|
-
|
|
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 响应."""
|
|
@@ -193,6 +424,51 @@ class WiFiManualConnectResponse(BaseModel):
|
|
|
193
424
|
error: str | None = None
|
|
194
425
|
|
|
195
426
|
|
|
427
|
+
class WiFiPairRequest(BaseModel):
|
|
428
|
+
"""WiFi pairing request (Android 11+ wireless debugging)."""
|
|
429
|
+
|
|
430
|
+
ip: str # Device IP address
|
|
431
|
+
pairing_port: int # Pairing port (from "Pair device with code" dialog)
|
|
432
|
+
pairing_code: str # 6-digit pairing code
|
|
433
|
+
connection_port: int = 5555 # Standard ADB connection port (default 5555)
|
|
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
|
+
|
|
462
|
+
|
|
463
|
+
class WiFiPairResponse(BaseModel):
|
|
464
|
+
"""WiFi pairing response."""
|
|
465
|
+
|
|
466
|
+
success: bool
|
|
467
|
+
message: str
|
|
468
|
+
device_id: str | None = None # Device ID after connection (ip:connection_port)
|
|
469
|
+
error: str | None = None # Error code for frontend handling
|
|
470
|
+
|
|
471
|
+
|
|
196
472
|
class VersionCheckResponse(BaseModel):
|
|
197
473
|
"""Version update check response."""
|
|
198
474
|
|
|
@@ -202,3 +478,105 @@ class VersionCheckResponse(BaseModel):
|
|
|
202
478
|
release_url: str | None = None
|
|
203
479
|
published_at: str | None = None
|
|
204
480
|
error: str | None = None
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
class MdnsDeviceResponse(BaseModel):
|
|
484
|
+
"""Single mDNS-discovered device."""
|
|
485
|
+
|
|
486
|
+
name: str # Device name (e.g., "adb-243a09b7-cbCO6P")
|
|
487
|
+
ip: str # IP address
|
|
488
|
+
port: int # Port number
|
|
489
|
+
has_pairing: bool # Whether pairing service was also advertised
|
|
490
|
+
service_type: str # Service type
|
|
491
|
+
pairing_port: int | None = None # Pairing port if has_pairing is True
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
class MdnsDiscoverResponse(BaseModel):
|
|
495
|
+
"""mDNS device discovery response."""
|
|
496
|
+
|
|
497
|
+
success: bool
|
|
498
|
+
devices: list[MdnsDeviceResponse]
|
|
499
|
+
error: str | None = None
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
# QR Code Pairing Models
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
class QRPairGenerateResponse(BaseModel):
|
|
506
|
+
"""QR code pairing generation response."""
|
|
507
|
+
|
|
508
|
+
success: bool
|
|
509
|
+
qr_payload: str | None = (
|
|
510
|
+
None # QR text payload (WIFI:T:ADB;S:{name};P:{password};;)
|
|
511
|
+
)
|
|
512
|
+
session_id: str | None = None # Session tracking ID (UUID)
|
|
513
|
+
expires_at: float | None = None # Unix timestamp when session expires
|
|
514
|
+
message: str
|
|
515
|
+
error: str | None = None # Error code for frontend handling
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
class QRPairStatusResponse(BaseModel):
|
|
519
|
+
"""QR code pairing status response."""
|
|
520
|
+
|
|
521
|
+
session_id: str
|
|
522
|
+
status: str # "listening" | "pairing" | "paired" | "connecting" | "connected" | "timeout" | "error"
|
|
523
|
+
device_id: str | None = None # Device ID when connected (ip:port)
|
|
524
|
+
message: str
|
|
525
|
+
error: str | None = None # Error details
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
class QRPairCancelResponse(BaseModel):
|
|
529
|
+
"""QR code pairing cancellation response."""
|
|
530
|
+
|
|
531
|
+
success: bool
|
|
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-
|
|
1
|
+
import{j as o}from"./index-CnEYDOXp.js";function t(){return o.jsx("div",{className:"p-2",children:o.jsx("h3",{children:"About"})})}export{t as component};
|