autoglm-gui 1.4.1__py3-none-any.whl → 1.5.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- AutoGLM_GUI/__init__.py +11 -0
- AutoGLM_GUI/__main__.py +26 -4
- AutoGLM_GUI/actions/__init__.py +6 -0
- phone_agent/actions/handler_ios.py → AutoGLM_GUI/actions/handler.py +30 -112
- AutoGLM_GUI/actions/types.py +15 -0
- {phone_agent → AutoGLM_GUI}/adb/__init__.py +25 -23
- {phone_agent → AutoGLM_GUI}/adb/connection.py +5 -40
- {phone_agent → AutoGLM_GUI}/adb/device.py +12 -94
- {phone_agent → AutoGLM_GUI}/adb/input.py +6 -47
- AutoGLM_GUI/adb/screenshot.py +11 -0
- {phone_agent/config → AutoGLM_GUI/adb}/timing.py +1 -1
- AutoGLM_GUI/adb_plus/keyboard_installer.py +4 -2
- AutoGLM_GUI/adb_plus/screenshot.py +22 -1
- AutoGLM_GUI/adb_plus/serial.py +38 -20
- AutoGLM_GUI/adb_plus/touch.py +4 -9
- AutoGLM_GUI/agents/__init__.py +43 -12
- AutoGLM_GUI/agents/events.py +19 -0
- AutoGLM_GUI/agents/factory.py +31 -38
- AutoGLM_GUI/agents/glm/__init__.py +7 -0
- AutoGLM_GUI/agents/glm/agent.py +297 -0
- AutoGLM_GUI/agents/glm/message_builder.py +81 -0
- AutoGLM_GUI/agents/glm/parser.py +110 -0
- {phone_agent/config → AutoGLM_GUI/agents/glm}/prompts_en.py +7 -9
- {phone_agent/config → AutoGLM_GUI/agents/glm}/prompts_zh.py +18 -25
- AutoGLM_GUI/agents/mai/__init__.py +28 -0
- AutoGLM_GUI/agents/mai/agent.py +408 -0
- AutoGLM_GUI/agents/mai/parser.py +254 -0
- AutoGLM_GUI/agents/mai/prompts.py +103 -0
- AutoGLM_GUI/agents/mai/traj_memory.py +91 -0
- AutoGLM_GUI/agents/protocols.py +12 -8
- AutoGLM_GUI/agents/stream_runner.py +193 -0
- AutoGLM_GUI/api/__init__.py +40 -21
- AutoGLM_GUI/api/agents.py +181 -239
- AutoGLM_GUI/api/control.py +9 -6
- AutoGLM_GUI/api/devices.py +102 -12
- AutoGLM_GUI/api/history.py +104 -0
- AutoGLM_GUI/api/layered_agent.py +67 -15
- AutoGLM_GUI/api/media.py +64 -1
- AutoGLM_GUI/api/scheduled_tasks.py +98 -0
- AutoGLM_GUI/config.py +81 -0
- AutoGLM_GUI/config_manager.py +68 -51
- AutoGLM_GUI/device_manager.py +248 -29
- AutoGLM_GUI/device_protocol.py +1 -1
- AutoGLM_GUI/devices/adb_device.py +5 -10
- AutoGLM_GUI/devices/mock_device.py +4 -2
- AutoGLM_GUI/devices/remote_device.py +8 -3
- AutoGLM_GUI/history_manager.py +164 -0
- AutoGLM_GUI/model/__init__.py +5 -0
- AutoGLM_GUI/model/message_builder.py +69 -0
- AutoGLM_GUI/model/types.py +24 -0
- AutoGLM_GUI/models/__init__.py +10 -0
- AutoGLM_GUI/models/history.py +140 -0
- AutoGLM_GUI/models/scheduled_task.py +71 -0
- AutoGLM_GUI/parsers/__init__.py +22 -0
- AutoGLM_GUI/parsers/base.py +50 -0
- AutoGLM_GUI/parsers/phone_parser.py +58 -0
- AutoGLM_GUI/phone_agent_manager.py +62 -396
- AutoGLM_GUI/platform_utils.py +26 -0
- AutoGLM_GUI/prompt_config.py +15 -0
- AutoGLM_GUI/prompts/__init__.py +32 -0
- AutoGLM_GUI/scheduler_manager.py +350 -0
- AutoGLM_GUI/schemas.py +246 -72
- AutoGLM_GUI/scrcpy_stream.py +142 -24
- AutoGLM_GUI/socketio_server.py +100 -27
- AutoGLM_GUI/static/assets/{about-_XNhzQZX.js → about-CfwX1Cmc.js} +1 -1
- AutoGLM_GUI/static/assets/alert-dialog-CtGlN2IJ.js +1 -0
- AutoGLM_GUI/static/assets/chat-BYa-foUI.js +129 -0
- AutoGLM_GUI/static/assets/circle-alert-t08bEMPO.js +1 -0
- AutoGLM_GUI/static/assets/dialog-FNwZJFwk.js +45 -0
- AutoGLM_GUI/static/assets/eye-D0UPWCWC.js +1 -0
- AutoGLM_GUI/static/assets/history-CRo95B7i.js +1 -0
- AutoGLM_GUI/static/assets/{index-Cy8TmmHV.js → index-BaLMSqd3.js} +1 -1
- AutoGLM_GUI/static/assets/index-CTHbFvKl.js +11 -0
- AutoGLM_GUI/static/assets/index-CV7jGxGm.css +1 -0
- AutoGLM_GUI/static/assets/label-DJFevVmr.js +1 -0
- AutoGLM_GUI/static/assets/logs-RW09DyYY.js +1 -0
- AutoGLM_GUI/static/assets/popover--JTJrE5v.js +1 -0
- AutoGLM_GUI/static/assets/scheduled-tasks-DTRKsQXF.js +1 -0
- AutoGLM_GUI/static/assets/square-pen-CPK_K680.js +1 -0
- AutoGLM_GUI/static/assets/textarea-PRmVnWq5.js +1 -0
- AutoGLM_GUI/static/assets/workflows-CdcsAoaT.js +1 -0
- AutoGLM_GUI/static/index.html +2 -2
- AutoGLM_GUI/types.py +17 -0
- {autoglm_gui-1.4.1.dist-info → autoglm_gui-1.5.1.dist-info}/METADATA +179 -130
- autoglm_gui-1.5.1.dist-info/RECORD +118 -0
- AutoGLM_GUI/agents/mai_adapter.py +0 -627
- AutoGLM_GUI/api/dual_model.py +0 -317
- AutoGLM_GUI/device_adapter.py +0 -263
- AutoGLM_GUI/dual_model/__init__.py +0 -53
- AutoGLM_GUI/dual_model/decision_model.py +0 -664
- AutoGLM_GUI/dual_model/dual_agent.py +0 -917
- AutoGLM_GUI/dual_model/protocols.py +0 -354
- AutoGLM_GUI/dual_model/vision_model.py +0 -442
- AutoGLM_GUI/mai_ui_adapter/agent_wrapper.py +0 -291
- AutoGLM_GUI/phone_agent_patches.py +0 -147
- AutoGLM_GUI/static/assets/chat-DwJpiAWf.js +0 -126
- AutoGLM_GUI/static/assets/dialog-B3uW4T8V.js +0 -45
- AutoGLM_GUI/static/assets/index-Cpv2gSF1.css +0 -1
- AutoGLM_GUI/static/assets/index-UYYauTly.js +0 -12
- AutoGLM_GUI/static/assets/workflows-Du_de-dt.js +0 -1
- autoglm_gui-1.4.1.dist-info/RECORD +0 -117
- mai_agent/base.py +0 -137
- mai_agent/mai_grounding_agent.py +0 -263
- mai_agent/mai_naivigation_agent.py +0 -526
- mai_agent/prompt.py +0 -148
- mai_agent/unified_memory.py +0 -67
- mai_agent/utils.py +0 -73
- phone_agent/__init__.py +0 -12
- phone_agent/actions/__init__.py +0 -5
- phone_agent/actions/handler.py +0 -400
- phone_agent/adb/screenshot.py +0 -108
- phone_agent/agent.py +0 -253
- phone_agent/agent_ios.py +0 -277
- phone_agent/config/__init__.py +0 -53
- phone_agent/config/apps_harmonyos.py +0 -256
- phone_agent/config/apps_ios.py +0 -339
- phone_agent/config/prompts.py +0 -80
- phone_agent/device_factory.py +0 -166
- phone_agent/hdc/__init__.py +0 -53
- phone_agent/hdc/connection.py +0 -384
- phone_agent/hdc/device.py +0 -269
- phone_agent/hdc/input.py +0 -145
- phone_agent/hdc/screenshot.py +0 -127
- phone_agent/model/__init__.py +0 -5
- phone_agent/model/client.py +0 -290
- phone_agent/xctest/__init__.py +0 -47
- phone_agent/xctest/connection.py +0 -379
- phone_agent/xctest/device.py +0 -472
- phone_agent/xctest/input.py +0 -311
- phone_agent/xctest/screenshot.py +0 -226
- {phone_agent/config → AutoGLM_GUI/adb}/apps.py +0 -0
- {phone_agent/config → AutoGLM_GUI}/i18n.py +0 -0
- {autoglm_gui-1.4.1.dist-info → autoglm_gui-1.5.1.dist-info}/WHEEL +0 -0
- {autoglm_gui-1.4.1.dist-info → autoglm_gui-1.5.1.dist-info}/entry_points.txt +0 -0
- {autoglm_gui-1.4.1.dist-info → autoglm_gui-1.5.1.dist-info}/licenses/LICENSE +0 -0
AutoGLM_GUI/schemas.py
CHANGED
|
@@ -2,63 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
4
|
|
|
5
|
-
from pydantic import BaseModel,
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class APIModelConfig(BaseModel):
|
|
9
|
-
base_url: str | None = None
|
|
10
|
-
api_key: str | None = None
|
|
11
|
-
model_name: str | None = None
|
|
12
|
-
max_tokens: int = 3000
|
|
13
|
-
temperature: float = 0.0
|
|
14
|
-
top_p: float = 0.85
|
|
15
|
-
frequency_penalty: float = 0.2
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
class APIAgentConfig(BaseModel):
|
|
33
|
-
max_steps: int = 100
|
|
34
|
-
device_id: str | None = None
|
|
35
|
-
lang: str = "cn"
|
|
36
|
-
system_prompt: str | None = None
|
|
37
|
-
verbose: bool = True
|
|
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
|
|
5
|
+
from pydantic import BaseModel, field_validator
|
|
57
6
|
|
|
58
7
|
|
|
59
8
|
class InitRequest(BaseModel):
|
|
60
|
-
|
|
61
|
-
agent: APIAgentConfig | None = Field(default=None, alias="agent_config")
|
|
9
|
+
device_id: str # Device ID (required)
|
|
62
10
|
|
|
63
11
|
# Agent configuration (factory pattern)
|
|
64
12
|
agent_type: str = "glm" # Agent type to use (e.g., "glm", "mai")
|
|
@@ -341,12 +289,6 @@ class ConfigResponse(BaseModel):
|
|
|
341
289
|
api_key: str # 返回实际值(明文)
|
|
342
290
|
source: str # "CLI arguments" | "environment variables" | "config file (...)" | "default"
|
|
343
291
|
|
|
344
|
-
# 双模型配置
|
|
345
|
-
dual_model_enabled: bool = False
|
|
346
|
-
decision_base_url: str = ""
|
|
347
|
-
decision_model_name: str = ""
|
|
348
|
-
decision_api_key: str = ""
|
|
349
|
-
|
|
350
292
|
# Agent 类型配置
|
|
351
293
|
agent_type: str = "glm" # Agent type (e.g., "glm", "mai")
|
|
352
294
|
agent_config_params: dict | None = None # Agent-specific configuration
|
|
@@ -354,6 +296,11 @@ class ConfigResponse(BaseModel):
|
|
|
354
296
|
# Agent 执行配置
|
|
355
297
|
default_max_steps: int = 100 # 单次任务最大执行步数
|
|
356
298
|
|
|
299
|
+
# 决策模型配置(用于分层代理)
|
|
300
|
+
decision_base_url: str | None = None
|
|
301
|
+
decision_model_name: str | None = None
|
|
302
|
+
decision_api_key: str | None = None
|
|
303
|
+
|
|
357
304
|
conflicts: list[dict] | None = None # 配置冲突信息(可选)
|
|
358
305
|
|
|
359
306
|
|
|
@@ -364,12 +311,6 @@ class ConfigSaveRequest(BaseModel):
|
|
|
364
311
|
model_name: str = "autoglm-phone-9b"
|
|
365
312
|
api_key: str | None = None
|
|
366
313
|
|
|
367
|
-
# 双模型配置
|
|
368
|
-
dual_model_enabled: bool | None = None
|
|
369
|
-
decision_base_url: str | None = None
|
|
370
|
-
decision_model_name: str | None = None
|
|
371
|
-
decision_api_key: str | None = None
|
|
372
|
-
|
|
373
314
|
# Agent 类型配置
|
|
374
315
|
agent_type: str = "glm" # Agent type to use (e.g., "glm", "mai")
|
|
375
316
|
agent_config_params: dict | None = None # Agent-specific configuration parameters
|
|
@@ -377,6 +318,11 @@ class ConfigSaveRequest(BaseModel):
|
|
|
377
318
|
# Agent 执行配置
|
|
378
319
|
default_max_steps: int | None = None # 单次任务最大执行步数
|
|
379
320
|
|
|
321
|
+
# 决策模型配置(用于分层代理)
|
|
322
|
+
decision_base_url: str | None = None
|
|
323
|
+
decision_model_name: str | None = None
|
|
324
|
+
decision_api_key: str | None = None
|
|
325
|
+
|
|
380
326
|
@field_validator("default_max_steps")
|
|
381
327
|
@classmethod
|
|
382
328
|
def validate_default_max_steps(cls, v: int | None) -> int | None:
|
|
@@ -412,12 +358,21 @@ class ConfigSaveRequest(BaseModel):
|
|
|
412
358
|
@classmethod
|
|
413
359
|
def validate_decision_base_url(cls, v: str | None) -> str | None:
|
|
414
360
|
"""验证 decision_base_url 格式."""
|
|
415
|
-
if v is None
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
361
|
+
if v is not None and v.strip():
|
|
362
|
+
if not re.match(r"^https?://", v):
|
|
363
|
+
raise ValueError(
|
|
364
|
+
"decision_base_url must start with http:// or https://"
|
|
365
|
+
)
|
|
366
|
+
return v.rstrip("/")
|
|
367
|
+
return None
|
|
368
|
+
|
|
369
|
+
@field_validator("decision_model_name")
|
|
370
|
+
@classmethod
|
|
371
|
+
def validate_decision_model_name(cls, v: str | None) -> str | None:
|
|
372
|
+
"""验证 decision_model_name 非空."""
|
|
373
|
+
if v is not None and v.strip():
|
|
374
|
+
return v.strip()
|
|
375
|
+
return None
|
|
421
376
|
|
|
422
377
|
|
|
423
378
|
class WiFiConnectRequest(BaseModel):
|
|
@@ -642,3 +597,222 @@ class WorkflowListResponse(BaseModel):
|
|
|
642
597
|
"""Workflow 列表响应."""
|
|
643
598
|
|
|
644
599
|
workflows: list[WorkflowResponse]
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
class RemoteDeviceInfo(BaseModel):
|
|
603
|
+
"""远程设备信息."""
|
|
604
|
+
|
|
605
|
+
device_id: str
|
|
606
|
+
model: str
|
|
607
|
+
platform: str
|
|
608
|
+
status: str
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
class RemoteDeviceDiscoverRequest(BaseModel):
|
|
612
|
+
"""远程设备发现请求."""
|
|
613
|
+
|
|
614
|
+
base_url: str
|
|
615
|
+
timeout: int = 5
|
|
616
|
+
|
|
617
|
+
@field_validator("base_url")
|
|
618
|
+
@classmethod
|
|
619
|
+
def validate_base_url(cls, v: str) -> str:
|
|
620
|
+
v = v.strip().rstrip("/")
|
|
621
|
+
if not v.startswith(("http://", "https://")):
|
|
622
|
+
raise ValueError("base_url must start with http:// or https://")
|
|
623
|
+
return v
|
|
624
|
+
|
|
625
|
+
@field_validator("timeout")
|
|
626
|
+
@classmethod
|
|
627
|
+
def validate_timeout(cls, v: int) -> int:
|
|
628
|
+
if v <= 0:
|
|
629
|
+
raise ValueError("timeout must be positive")
|
|
630
|
+
if v > 30:
|
|
631
|
+
raise ValueError("timeout must be <= 30 seconds")
|
|
632
|
+
return v
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
class RemoteDeviceDiscoverResponse(BaseModel):
|
|
636
|
+
"""远程设备发现响应."""
|
|
637
|
+
|
|
638
|
+
success: bool
|
|
639
|
+
devices: list[RemoteDeviceInfo]
|
|
640
|
+
message: str
|
|
641
|
+
error: str | None = None
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
class RemoteDeviceAddRequest(BaseModel):
|
|
645
|
+
"""添加远程设备请求."""
|
|
646
|
+
|
|
647
|
+
base_url: str
|
|
648
|
+
device_id: str
|
|
649
|
+
|
|
650
|
+
@field_validator("base_url")
|
|
651
|
+
@classmethod
|
|
652
|
+
def validate_base_url(cls, v: str) -> str:
|
|
653
|
+
v = v.strip().rstrip("/")
|
|
654
|
+
if not v.startswith(("http://", "https://")):
|
|
655
|
+
raise ValueError("base_url must start with http:// or https://")
|
|
656
|
+
return v
|
|
657
|
+
|
|
658
|
+
@field_validator("device_id")
|
|
659
|
+
@classmethod
|
|
660
|
+
def validate_device_id(cls, v: str) -> str:
|
|
661
|
+
v = v.strip()
|
|
662
|
+
if not v:
|
|
663
|
+
raise ValueError("device_id cannot be empty")
|
|
664
|
+
if len(v) > 100:
|
|
665
|
+
raise ValueError("device_id too long (max 100 characters)")
|
|
666
|
+
return v
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
class RemoteDeviceAddResponse(BaseModel):
|
|
670
|
+
"""添加远程设备响应."""
|
|
671
|
+
|
|
672
|
+
success: bool
|
|
673
|
+
message: str
|
|
674
|
+
serial: str | None = None
|
|
675
|
+
error: str | None = None
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
class RemoteDeviceRemoveRequest(BaseModel):
|
|
679
|
+
"""移除远程设备请求."""
|
|
680
|
+
|
|
681
|
+
serial: str
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
class RemoteDeviceRemoveResponse(BaseModel):
|
|
685
|
+
"""移除远程设备响应."""
|
|
686
|
+
|
|
687
|
+
success: bool
|
|
688
|
+
message: str
|
|
689
|
+
error: str | None = None
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
class ReinitAllAgentsResponse(BaseModel):
|
|
693
|
+
"""批量重新初始化 agent 响应."""
|
|
694
|
+
|
|
695
|
+
success: bool
|
|
696
|
+
total: int
|
|
697
|
+
succeeded: list[str]
|
|
698
|
+
failed: dict[str, str]
|
|
699
|
+
message: str
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
# History Models
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
class MessageRecordResponse(BaseModel):
|
|
706
|
+
"""对话消息响应."""
|
|
707
|
+
|
|
708
|
+
role: str # "user" | "assistant"
|
|
709
|
+
content: str
|
|
710
|
+
timestamp: str
|
|
711
|
+
thinking: str | None = None
|
|
712
|
+
action: dict | None = None
|
|
713
|
+
step: int | None = None
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
class HistoryRecordResponse(BaseModel):
|
|
717
|
+
"""历史记录条目响应."""
|
|
718
|
+
|
|
719
|
+
id: str
|
|
720
|
+
task_text: str
|
|
721
|
+
final_message: str
|
|
722
|
+
success: bool
|
|
723
|
+
steps: int
|
|
724
|
+
start_time: str
|
|
725
|
+
end_time: str | None
|
|
726
|
+
duration_ms: int
|
|
727
|
+
source: str
|
|
728
|
+
source_detail: str
|
|
729
|
+
error_message: str | None
|
|
730
|
+
messages: list[MessageRecordResponse] = []
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
class HistoryListResponse(BaseModel):
|
|
734
|
+
"""历史记录列表响应."""
|
|
735
|
+
|
|
736
|
+
records: list[HistoryRecordResponse]
|
|
737
|
+
total: int
|
|
738
|
+
limit: int
|
|
739
|
+
offset: int
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
# Scheduled Task Models
|
|
743
|
+
|
|
744
|
+
|
|
745
|
+
class ScheduledTaskCreate(BaseModel):
|
|
746
|
+
"""创建定时任务请求."""
|
|
747
|
+
|
|
748
|
+
name: str
|
|
749
|
+
workflow_uuid: str
|
|
750
|
+
device_serialno: str
|
|
751
|
+
cron_expression: str
|
|
752
|
+
enabled: bool = True
|
|
753
|
+
|
|
754
|
+
@field_validator("name")
|
|
755
|
+
@classmethod
|
|
756
|
+
def validate_name(cls, v: str) -> str:
|
|
757
|
+
if not v or not v.strip():
|
|
758
|
+
raise ValueError("name cannot be empty")
|
|
759
|
+
return v.strip()
|
|
760
|
+
|
|
761
|
+
@field_validator("cron_expression")
|
|
762
|
+
@classmethod
|
|
763
|
+
def validate_cron(cls, v: str) -> str:
|
|
764
|
+
if not v or not v.strip():
|
|
765
|
+
raise ValueError("cron_expression cannot be empty")
|
|
766
|
+
parts = v.strip().split()
|
|
767
|
+
if len(parts) != 5:
|
|
768
|
+
raise ValueError(
|
|
769
|
+
"cron_expression must have 5 fields (minute hour day month weekday)"
|
|
770
|
+
)
|
|
771
|
+
return v.strip()
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
class ScheduledTaskUpdate(BaseModel):
|
|
775
|
+
"""更新定时任务请求."""
|
|
776
|
+
|
|
777
|
+
name: str | None = None
|
|
778
|
+
workflow_uuid: str | None = None
|
|
779
|
+
device_serialno: str | None = None
|
|
780
|
+
cron_expression: str | None = None
|
|
781
|
+
enabled: bool | None = None
|
|
782
|
+
|
|
783
|
+
@field_validator("cron_expression")
|
|
784
|
+
@classmethod
|
|
785
|
+
def validate_cron(cls, v: str | None) -> str | None:
|
|
786
|
+
if v is None:
|
|
787
|
+
return v
|
|
788
|
+
if not v.strip():
|
|
789
|
+
raise ValueError("cron_expression cannot be empty")
|
|
790
|
+
parts = v.strip().split()
|
|
791
|
+
if len(parts) != 5:
|
|
792
|
+
raise ValueError(
|
|
793
|
+
"cron_expression must have 5 fields (minute hour day month weekday)"
|
|
794
|
+
)
|
|
795
|
+
return v.strip()
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
class ScheduledTaskResponse(BaseModel):
|
|
799
|
+
"""定时任务响应."""
|
|
800
|
+
|
|
801
|
+
id: str
|
|
802
|
+
name: str
|
|
803
|
+
workflow_uuid: str
|
|
804
|
+
device_serialno: str
|
|
805
|
+
cron_expression: str
|
|
806
|
+
enabled: bool
|
|
807
|
+
created_at: str
|
|
808
|
+
updated_at: str
|
|
809
|
+
last_run_time: str | None
|
|
810
|
+
last_run_success: bool | None
|
|
811
|
+
last_run_message: str | None
|
|
812
|
+
next_run_time: str | None = None
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
class ScheduledTaskListResponse(BaseModel):
|
|
816
|
+
"""定时任务列表响应."""
|
|
817
|
+
|
|
818
|
+
tasks: list[ScheduledTaskResponse]
|
AutoGLM_GUI/scrcpy_stream.py
CHANGED
|
@@ -5,6 +5,7 @@ import os
|
|
|
5
5
|
import socket
|
|
6
6
|
import subprocess
|
|
7
7
|
import sys
|
|
8
|
+
import time
|
|
8
9
|
from dataclasses import dataclass
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
from asyncio.subprocess import Process as AsyncProcess
|
|
@@ -23,6 +24,74 @@ from AutoGLM_GUI.scrcpy_protocol import (
|
|
|
23
24
|
)
|
|
24
25
|
|
|
25
26
|
|
|
27
|
+
async def is_port_available(port: int, host: str = "127.0.0.1") -> bool:
|
|
28
|
+
"""Test if TCP port is available for binding.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
port: TCP port number
|
|
32
|
+
host: Host address to test
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
True if port can be bound (available), False otherwise
|
|
36
|
+
"""
|
|
37
|
+
sock = None
|
|
38
|
+
try:
|
|
39
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
40
|
+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
41
|
+
sock.setblocking(False)
|
|
42
|
+
sock.bind((host, port))
|
|
43
|
+
logger.debug(f"Port {port} is available for binding")
|
|
44
|
+
return True
|
|
45
|
+
except OSError as e:
|
|
46
|
+
# Handle cross-platform errno for "Address already in use"
|
|
47
|
+
# macOS: 48, Linux: 98, Windows: 10048
|
|
48
|
+
logger.debug(f"Port {port} is occupied: {e}")
|
|
49
|
+
return False
|
|
50
|
+
finally:
|
|
51
|
+
if sock:
|
|
52
|
+
sock.close()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
async def wait_for_port_release(
|
|
56
|
+
port: int,
|
|
57
|
+
timeout: float = 5.0,
|
|
58
|
+
poll_interval: float = 0.2,
|
|
59
|
+
host: str = "127.0.0.1",
|
|
60
|
+
) -> bool:
|
|
61
|
+
"""Wait for TCP port to become available with polling.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
port: TCP port to wait for
|
|
65
|
+
timeout: Maximum wait time in seconds (default: 5.0)
|
|
66
|
+
poll_interval: Check interval in seconds (default: 0.2)
|
|
67
|
+
host: Host address
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
True if port became available, False if timeout
|
|
71
|
+
"""
|
|
72
|
+
start_time = time.time()
|
|
73
|
+
attempt = 0
|
|
74
|
+
|
|
75
|
+
while time.time() - start_time < timeout:
|
|
76
|
+
attempt += 1
|
|
77
|
+
if await is_port_available(port, host):
|
|
78
|
+
elapsed = time.time() - start_time
|
|
79
|
+
logger.info(
|
|
80
|
+
f"Port {port} became available after {elapsed:.2f}s ({attempt} checks)"
|
|
81
|
+
)
|
|
82
|
+
return True
|
|
83
|
+
|
|
84
|
+
# Log progress every second for debugging
|
|
85
|
+
if attempt % 5 == 0: # Every 1 second (5 * 0.2s)
|
|
86
|
+
elapsed = time.time() - start_time
|
|
87
|
+
logger.debug(f"Still waiting for port {port}... ({elapsed:.1f}s elapsed)")
|
|
88
|
+
|
|
89
|
+
await asyncio.sleep(poll_interval)
|
|
90
|
+
|
|
91
|
+
logger.warning(f"Port {port} did not release within {timeout}s timeout")
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
|
|
26
95
|
@dataclass
|
|
27
96
|
class ScrcpyServerOptions:
|
|
28
97
|
max_size: int
|
|
@@ -159,29 +228,44 @@ class ScrcpyStreamer:
|
|
|
159
228
|
raise RuntimeError(f"Failed to start scrcpy server: {e}") from e
|
|
160
229
|
|
|
161
230
|
async def _cleanup_existing_server(self) -> None:
|
|
162
|
-
"""Kill existing scrcpy server processes
|
|
231
|
+
"""Kill existing scrcpy server processes and wait for port release."""
|
|
163
232
|
cmd_base = ["adb"]
|
|
164
233
|
if self.device_id:
|
|
165
234
|
cmd_base.extend(["-s", self.device_id])
|
|
166
235
|
|
|
167
236
|
# Method 1: Try pkill
|
|
237
|
+
logger.debug("Killing scrcpy processes via pkill...")
|
|
168
238
|
cmd = cmd_base + ["shell", "pkill", "-9", "-f", "app_process.*scrcpy"]
|
|
169
239
|
await run_cmd_silently(cmd)
|
|
170
240
|
|
|
171
241
|
# Method 2: Find and kill by PID (more reliable)
|
|
242
|
+
logger.debug("Killing scrcpy processes via PID...")
|
|
172
243
|
cmd = cmd_base + [
|
|
173
244
|
"shell",
|
|
174
245
|
"ps -ef | grep 'app_process.*scrcpy' | grep -v grep | awk '{print $2}' | xargs kill -9",
|
|
175
246
|
]
|
|
176
247
|
await run_cmd_silently(cmd)
|
|
177
248
|
|
|
178
|
-
# Method 3: Remove port forward
|
|
249
|
+
# Method 3: Remove port forward
|
|
250
|
+
logger.debug(f"Removing ADB port forward on port {self.port}...")
|
|
179
251
|
cmd_remove_forward = cmd_base + ["forward", "--remove", f"tcp:{self.port}"]
|
|
180
252
|
await run_cmd_silently(cmd_remove_forward)
|
|
181
253
|
|
|
182
|
-
# Wait for
|
|
183
|
-
logger.
|
|
184
|
-
await
|
|
254
|
+
# Wait for port to be truly available (instead of fixed sleep)
|
|
255
|
+
logger.info(f"Waiting for port {self.port} to be released...")
|
|
256
|
+
port_released = await wait_for_port_release(
|
|
257
|
+
self.port,
|
|
258
|
+
timeout=5.0, # Max 5 seconds (vs old fixed 2s)
|
|
259
|
+
poll_interval=0.2, # Check every 200ms
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
if not port_released:
|
|
263
|
+
logger.warning(
|
|
264
|
+
f"Port {self.port} still occupied after cleanup. "
|
|
265
|
+
"Will attempt to start anyway (may fail)."
|
|
266
|
+
)
|
|
267
|
+
else:
|
|
268
|
+
logger.info(f"Port {self.port} successfully released and ready")
|
|
185
269
|
|
|
186
270
|
async def _push_server(self) -> None:
|
|
187
271
|
"""Push scrcpy-server to device."""
|
|
@@ -221,9 +305,9 @@ class ScrcpyStreamer:
|
|
|
221
305
|
)
|
|
222
306
|
|
|
223
307
|
async def _start_server(self) -> None:
|
|
224
|
-
"""Start scrcpy server on device with retry
|
|
308
|
+
"""Start scrcpy server on device with intelligent retry."""
|
|
225
309
|
max_retries = 3
|
|
226
|
-
retry_delay =
|
|
310
|
+
retry_delay = 1.0 # Reduced from 2s (cleanup handles waiting now)
|
|
227
311
|
|
|
228
312
|
options = self._build_server_options()
|
|
229
313
|
|
|
@@ -275,43 +359,77 @@ class ScrcpyStreamer:
|
|
|
275
359
|
error_msg = stderr.decode() if stderr else stdout.decode()
|
|
276
360
|
|
|
277
361
|
if error_msg is not None:
|
|
362
|
+
# Detailed error classification
|
|
278
363
|
if "Address already in use" in error_msg:
|
|
364
|
+
logger.error(
|
|
365
|
+
f"Port {self.port} conflict detected (attempt {attempt + 1}/{max_retries}). "
|
|
366
|
+
f"Error: {error_msg[:200]}"
|
|
367
|
+
)
|
|
279
368
|
if attempt < max_retries - 1:
|
|
280
369
|
logger.warning(
|
|
281
|
-
f"
|
|
370
|
+
f"Retrying with aggressive cleanup in {retry_delay}s..."
|
|
282
371
|
)
|
|
283
372
|
await self._cleanup_existing_server()
|
|
284
373
|
await asyncio.sleep(retry_delay)
|
|
285
374
|
continue
|
|
375
|
+
# Specific error for port conflicts
|
|
286
376
|
raise RuntimeError(
|
|
287
|
-
f"
|
|
377
|
+
f"Port {self.port} persistently occupied after {max_retries} attempts. "
|
|
378
|
+
"Please check if another scrcpy instance is running."
|
|
288
379
|
)
|
|
289
|
-
|
|
380
|
+
else:
|
|
381
|
+
# Non-port errors fail immediately (no retry)
|
|
382
|
+
logger.error(f"Scrcpy server startup failed: {error_msg[:200]}")
|
|
383
|
+
raise RuntimeError(f"Scrcpy server failed to start: {error_msg}")
|
|
290
384
|
|
|
385
|
+
logger.info("Scrcpy server started successfully")
|
|
291
386
|
return
|
|
292
387
|
|
|
293
388
|
raise RuntimeError("Failed to start scrcpy server after maximum retries")
|
|
294
389
|
|
|
295
390
|
async def _connect_socket(self) -> None:
|
|
296
391
|
"""Connect to scrcpy TCP socket."""
|
|
297
|
-
|
|
298
|
-
|
|
392
|
+
# Retry connection with exponential backoff (max ~6 seconds total)
|
|
393
|
+
max_attempts = 10
|
|
394
|
+
retry_delay = 0.3
|
|
299
395
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
)
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
396
|
+
for attempt in range(max_attempts):
|
|
397
|
+
# Create a fresh socket for each attempt to avoid "Invalid argument" error
|
|
398
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
399
|
+
sock.settimeout(5)
|
|
400
|
+
|
|
401
|
+
try:
|
|
402
|
+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 2 * 1024 * 1024)
|
|
403
|
+
except OSError as e:
|
|
404
|
+
logger.debug(f"Failed to set socket buffer size: {e}")
|
|
307
405
|
|
|
308
|
-
for _ in range(5):
|
|
309
406
|
try:
|
|
310
|
-
|
|
311
|
-
|
|
407
|
+
sock.connect(("localhost", self.port))
|
|
408
|
+
sock.settimeout(None)
|
|
409
|
+
self.tcp_socket = sock # Only assign on success
|
|
410
|
+
logger.debug(f"Connected to scrcpy server on attempt {attempt + 1}")
|
|
312
411
|
return
|
|
313
|
-
except (ConnectionRefusedError, OSError):
|
|
314
|
-
|
|
412
|
+
except (ConnectionRefusedError, OSError) as e:
|
|
413
|
+
# Close the failed socket
|
|
414
|
+
try:
|
|
415
|
+
sock.close()
|
|
416
|
+
except Exception:
|
|
417
|
+
pass
|
|
418
|
+
|
|
419
|
+
if attempt < max_attempts - 1:
|
|
420
|
+
logger.debug(
|
|
421
|
+
f"Connection attempt {attempt + 1}/{max_attempts} failed: {e}. "
|
|
422
|
+
f"Retrying in {retry_delay}s..."
|
|
423
|
+
)
|
|
424
|
+
await asyncio.sleep(retry_delay)
|
|
425
|
+
# Gradually increase delay for later attempts
|
|
426
|
+
if attempt >= 3:
|
|
427
|
+
retry_delay = 0.5
|
|
428
|
+
else:
|
|
429
|
+
logger.error(
|
|
430
|
+
f"Failed to connect after {max_attempts} attempts. "
|
|
431
|
+
f"Last error: {e}"
|
|
432
|
+
)
|
|
315
433
|
|
|
316
434
|
raise ConnectionError("Failed to connect to scrcpy server")
|
|
317
435
|
|