autoglm-gui 1.4.0__py3-none-any.whl → 1.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. AutoGLM_GUI/__init__.py +11 -0
  2. AutoGLM_GUI/__main__.py +26 -8
  3. AutoGLM_GUI/actions/__init__.py +6 -0
  4. AutoGLM_GUI/actions/handler.py +196 -0
  5. AutoGLM_GUI/actions/types.py +15 -0
  6. AutoGLM_GUI/adb/__init__.py +53 -0
  7. AutoGLM_GUI/adb/apps.py +227 -0
  8. AutoGLM_GUI/adb/connection.py +323 -0
  9. AutoGLM_GUI/adb/device.py +171 -0
  10. AutoGLM_GUI/adb/input.py +67 -0
  11. AutoGLM_GUI/adb/screenshot.py +11 -0
  12. AutoGLM_GUI/adb/timing.py +167 -0
  13. AutoGLM_GUI/adb_plus/keyboard_installer.py +4 -2
  14. AutoGLM_GUI/adb_plus/qr_pair.py +8 -8
  15. AutoGLM_GUI/adb_plus/screenshot.py +22 -1
  16. AutoGLM_GUI/adb_plus/serial.py +38 -20
  17. AutoGLM_GUI/adb_plus/touch.py +4 -9
  18. AutoGLM_GUI/agents/__init__.py +51 -0
  19. AutoGLM_GUI/agents/events.py +19 -0
  20. AutoGLM_GUI/agents/factory.py +153 -0
  21. AutoGLM_GUI/agents/glm/__init__.py +7 -0
  22. AutoGLM_GUI/agents/glm/agent.py +292 -0
  23. AutoGLM_GUI/agents/glm/message_builder.py +81 -0
  24. AutoGLM_GUI/agents/glm/parser.py +110 -0
  25. AutoGLM_GUI/agents/glm/prompts_en.py +77 -0
  26. AutoGLM_GUI/agents/glm/prompts_zh.py +75 -0
  27. AutoGLM_GUI/agents/mai/__init__.py +28 -0
  28. AutoGLM_GUI/agents/mai/agent.py +405 -0
  29. AutoGLM_GUI/agents/mai/parser.py +254 -0
  30. AutoGLM_GUI/agents/mai/prompts.py +103 -0
  31. AutoGLM_GUI/agents/mai/traj_memory.py +91 -0
  32. AutoGLM_GUI/agents/protocols.py +27 -0
  33. AutoGLM_GUI/agents/stream_runner.py +188 -0
  34. AutoGLM_GUI/api/__init__.py +71 -11
  35. AutoGLM_GUI/api/agents.py +190 -229
  36. AutoGLM_GUI/api/control.py +9 -6
  37. AutoGLM_GUI/api/devices.py +112 -28
  38. AutoGLM_GUI/api/health.py +13 -0
  39. AutoGLM_GUI/api/history.py +78 -0
  40. AutoGLM_GUI/api/layered_agent.py +306 -181
  41. AutoGLM_GUI/api/mcp.py +11 -10
  42. AutoGLM_GUI/api/media.py +64 -1
  43. AutoGLM_GUI/api/scheduled_tasks.py +98 -0
  44. AutoGLM_GUI/api/version.py +23 -10
  45. AutoGLM_GUI/api/workflows.py +2 -1
  46. AutoGLM_GUI/config.py +72 -14
  47. AutoGLM_GUI/config_manager.py +98 -27
  48. AutoGLM_GUI/device_adapter.py +263 -0
  49. AutoGLM_GUI/device_manager.py +248 -29
  50. AutoGLM_GUI/device_protocol.py +266 -0
  51. AutoGLM_GUI/devices/__init__.py +49 -0
  52. AutoGLM_GUI/devices/adb_device.py +200 -0
  53. AutoGLM_GUI/devices/mock_device.py +185 -0
  54. AutoGLM_GUI/devices/remote_device.py +177 -0
  55. AutoGLM_GUI/exceptions.py +3 -3
  56. AutoGLM_GUI/history_manager.py +164 -0
  57. AutoGLM_GUI/i18n.py +81 -0
  58. AutoGLM_GUI/metrics.py +13 -20
  59. AutoGLM_GUI/model/__init__.py +5 -0
  60. AutoGLM_GUI/model/message_builder.py +69 -0
  61. AutoGLM_GUI/model/types.py +24 -0
  62. AutoGLM_GUI/models/__init__.py +10 -0
  63. AutoGLM_GUI/models/history.py +96 -0
  64. AutoGLM_GUI/models/scheduled_task.py +71 -0
  65. AutoGLM_GUI/parsers/__init__.py +22 -0
  66. AutoGLM_GUI/parsers/base.py +50 -0
  67. AutoGLM_GUI/parsers/phone_parser.py +58 -0
  68. AutoGLM_GUI/phone_agent_manager.py +118 -367
  69. AutoGLM_GUI/platform_utils.py +31 -2
  70. AutoGLM_GUI/prompt_config.py +15 -0
  71. AutoGLM_GUI/prompts/__init__.py +32 -0
  72. AutoGLM_GUI/scheduler_manager.py +304 -0
  73. AutoGLM_GUI/schemas.py +272 -63
  74. AutoGLM_GUI/scrcpy_stream.py +159 -37
  75. AutoGLM_GUI/server.py +3 -1
  76. AutoGLM_GUI/socketio_server.py +114 -29
  77. AutoGLM_GUI/state.py +10 -30
  78. AutoGLM_GUI/static/assets/{about-DeclntHg.js → about-BQm96DAl.js} +1 -1
  79. AutoGLM_GUI/static/assets/alert-dialog-B42XxGPR.js +1 -0
  80. AutoGLM_GUI/static/assets/chat-C0L2gQYG.js +129 -0
  81. AutoGLM_GUI/static/assets/circle-alert-D4rSJh37.js +1 -0
  82. AutoGLM_GUI/static/assets/dialog-DZ78cEcj.js +45 -0
  83. AutoGLM_GUI/static/assets/history-DFBv7TGc.js +1 -0
  84. AutoGLM_GUI/static/assets/index-Bzyv2yQ2.css +1 -0
  85. AutoGLM_GUI/static/assets/{index-zQ4KKDHt.js → index-CmZSnDqc.js} +1 -1
  86. AutoGLM_GUI/static/assets/index-CssG-3TH.js +11 -0
  87. AutoGLM_GUI/static/assets/label-BCUzE_nm.js +1 -0
  88. AutoGLM_GUI/static/assets/logs-eoFxn5of.js +1 -0
  89. AutoGLM_GUI/static/assets/popover-DLsuV5Sx.js +1 -0
  90. AutoGLM_GUI/static/assets/scheduled-tasks-MyqGJvy_.js +1 -0
  91. AutoGLM_GUI/static/assets/square-pen-zGWYrdfj.js +1 -0
  92. AutoGLM_GUI/static/assets/textarea-BX6y7uM5.js +1 -0
  93. AutoGLM_GUI/static/assets/workflows-CYFs6ssC.js +1 -0
  94. AutoGLM_GUI/static/index.html +2 -2
  95. AutoGLM_GUI/types.py +142 -0
  96. {autoglm_gui-1.4.0.dist-info → autoglm_gui-1.5.0.dist-info}/METADATA +178 -92
  97. autoglm_gui-1.5.0.dist-info/RECORD +157 -0
  98. mai_agent/base.py +137 -0
  99. mai_agent/mai_grounding_agent.py +263 -0
  100. mai_agent/mai_naivigation_agent.py +526 -0
  101. mai_agent/prompt.py +148 -0
  102. mai_agent/unified_memory.py +67 -0
  103. mai_agent/utils.py +73 -0
  104. AutoGLM_GUI/api/dual_model.py +0 -311
  105. AutoGLM_GUI/dual_model/__init__.py +0 -53
  106. AutoGLM_GUI/dual_model/decision_model.py +0 -664
  107. AutoGLM_GUI/dual_model/dual_agent.py +0 -917
  108. AutoGLM_GUI/dual_model/protocols.py +0 -354
  109. AutoGLM_GUI/dual_model/vision_model.py +0 -442
  110. AutoGLM_GUI/mai_ui_adapter/agent_wrapper.py +0 -291
  111. AutoGLM_GUI/phone_agent_patches.py +0 -146
  112. AutoGLM_GUI/static/assets/chat-Iut2yhSw.js +0 -125
  113. AutoGLM_GUI/static/assets/dialog-BfdcBs1x.js +0 -45
  114. AutoGLM_GUI/static/assets/index-5hCCwHA7.css +0 -1
  115. AutoGLM_GUI/static/assets/index-DHF1NZh0.js +0 -12
  116. AutoGLM_GUI/static/assets/workflows-xiplap-r.js +0 -1
  117. autoglm_gui-1.4.0.dist-info/RECORD +0 -100
  118. {autoglm_gui-1.4.0.dist-info → autoglm_gui-1.5.0.dist-info}/WHEEL +0 -0
  119. {autoglm_gui-1.4.0.dist-info → autoglm_gui-1.5.0.dist-info}/entry_points.txt +0 -0
  120. {autoglm_gui-1.4.0.dist-info → autoglm_gui-1.5.0.dist-info}/licenses/LICENSE +0 -0
AutoGLM_GUI/api/media.py CHANGED
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  from fastapi import APIRouter
6
6
 
7
7
  from AutoGLM_GUI.adb_plus import capture_screenshot
8
+ from AutoGLM_GUI.exceptions import DeviceNotAvailableError
8
9
  from AutoGLM_GUI.logger import logger
9
10
  from AutoGLM_GUI.schemas import ScreenshotRequest, ScreenshotResponse
10
11
  from AutoGLM_GUI.socketio_server import stop_streamers
@@ -29,8 +30,59 @@ async def reset_video_stream(device_id: str | None = None) -> dict:
29
30
  @router.post("/api/screenshot", response_model=ScreenshotResponse)
30
31
  def take_screenshot(request: ScreenshotRequest) -> ScreenshotResponse:
31
32
  """获取设备截图。此操作无副作用,不影响 PhoneAgent 运行。"""
33
+ from AutoGLM_GUI.device_manager import DeviceManager
34
+
32
35
  try:
33
- screenshot = capture_screenshot(device_id=request.device_id)
36
+ device_id = request.device_id
37
+
38
+ if not device_id:
39
+ return ScreenshotResponse(
40
+ success=False,
41
+ image="",
42
+ width=0,
43
+ height=0,
44
+ is_sensitive=False,
45
+ error="device_id is required",
46
+ )
47
+
48
+ device_manager = DeviceManager.get_instance()
49
+ serial = device_manager.get_serial_by_device_id(device_id)
50
+
51
+ if not serial:
52
+ return ScreenshotResponse(
53
+ success=False,
54
+ image="",
55
+ width=0,
56
+ height=0,
57
+ is_sensitive=False,
58
+ error=f"Device {device_id} not found",
59
+ )
60
+
61
+ if serial:
62
+ managed = device_manager._devices.get(serial)
63
+ if managed and managed.connection_type.value == "remote":
64
+ remote_device = device_manager.get_remote_device_instance(serial)
65
+
66
+ if not remote_device:
67
+ return ScreenshotResponse(
68
+ success=False,
69
+ image="",
70
+ width=0,
71
+ height=0,
72
+ is_sensitive=False,
73
+ error=f"Remote device {serial} not found",
74
+ )
75
+
76
+ screenshot = remote_device.get_screenshot(timeout=10) # type: ignore
77
+ return ScreenshotResponse(
78
+ success=True,
79
+ image=screenshot.base64_data,
80
+ width=screenshot.width,
81
+ height=screenshot.height,
82
+ is_sensitive=screenshot.is_sensitive,
83
+ )
84
+
85
+ screenshot = capture_screenshot(device_id=device_id)
34
86
  return ScreenshotResponse(
35
87
  success=True,
36
88
  image=screenshot.base64_data,
@@ -38,7 +90,18 @@ def take_screenshot(request: ScreenshotRequest) -> ScreenshotResponse:
38
90
  height=screenshot.height,
39
91
  is_sensitive=screenshot.is_sensitive,
40
92
  )
93
+ except DeviceNotAvailableError as e:
94
+ logger.warning("Screenshot failed - device not available: %s", e)
95
+ return ScreenshotResponse(
96
+ success=False,
97
+ image="",
98
+ width=0,
99
+ height=0,
100
+ is_sensitive=False,
101
+ error=str(e),
102
+ )
41
103
  except Exception as e:
104
+ logger.exception("Screenshot failed for device %s", request.device_id)
42
105
  return ScreenshotResponse(
43
106
  success=False,
44
107
  image="",
@@ -0,0 +1,98 @@
1
+ """Scheduled tasks API routes."""
2
+
3
+ from fastapi import APIRouter, HTTPException
4
+
5
+ from AutoGLM_GUI.scheduler_manager import scheduler_manager
6
+ from AutoGLM_GUI.schemas import (
7
+ ScheduledTaskCreate,
8
+ ScheduledTaskListResponse,
9
+ ScheduledTaskResponse,
10
+ ScheduledTaskUpdate,
11
+ )
12
+
13
+ router = APIRouter()
14
+
15
+
16
+ def _task_to_response(task) -> ScheduledTaskResponse:
17
+ next_run = scheduler_manager.get_next_run_time(task.id)
18
+ return ScheduledTaskResponse(
19
+ id=task.id,
20
+ name=task.name,
21
+ workflow_uuid=task.workflow_uuid,
22
+ device_serialno=task.device_serialno,
23
+ cron_expression=task.cron_expression,
24
+ enabled=task.enabled,
25
+ created_at=task.created_at.isoformat(),
26
+ updated_at=task.updated_at.isoformat(),
27
+ last_run_time=task.last_run_time.isoformat() if task.last_run_time else None,
28
+ last_run_success=task.last_run_success,
29
+ last_run_message=task.last_run_message,
30
+ next_run_time=next_run.isoformat() if next_run else None,
31
+ )
32
+
33
+
34
+ @router.get("/api/scheduled-tasks", response_model=ScheduledTaskListResponse)
35
+ def list_scheduled_tasks() -> ScheduledTaskListResponse:
36
+ tasks = scheduler_manager.list_tasks()
37
+ return ScheduledTaskListResponse(tasks=[_task_to_response(t) for t in tasks])
38
+
39
+
40
+ @router.post("/api/scheduled-tasks", response_model=ScheduledTaskResponse)
41
+ def create_scheduled_task(request: ScheduledTaskCreate) -> ScheduledTaskResponse:
42
+ from AutoGLM_GUI.workflow_manager import workflow_manager
43
+
44
+ workflow = workflow_manager.get_workflow(request.workflow_uuid)
45
+ if not workflow:
46
+ raise HTTPException(status_code=400, detail="Workflow not found")
47
+
48
+ task = scheduler_manager.create_task(
49
+ name=request.name,
50
+ workflow_uuid=request.workflow_uuid,
51
+ device_serialno=request.device_serialno,
52
+ cron_expression=request.cron_expression,
53
+ enabled=request.enabled,
54
+ )
55
+ return _task_to_response(task)
56
+
57
+
58
+ @router.get("/api/scheduled-tasks/{task_id}", response_model=ScheduledTaskResponse)
59
+ def get_scheduled_task(task_id: str) -> ScheduledTaskResponse:
60
+ task = scheduler_manager.get_task(task_id)
61
+ if not task:
62
+ raise HTTPException(status_code=404, detail="Task not found")
63
+ return _task_to_response(task)
64
+
65
+
66
+ @router.put("/api/scheduled-tasks/{task_id}", response_model=ScheduledTaskResponse)
67
+ def update_scheduled_task(
68
+ task_id: str, request: ScheduledTaskUpdate
69
+ ) -> ScheduledTaskResponse:
70
+ update_data = request.model_dump(exclude_unset=True)
71
+ task = scheduler_manager.update_task(task_id, **update_data)
72
+ if not task:
73
+ raise HTTPException(status_code=404, detail="Task not found")
74
+ return _task_to_response(task)
75
+
76
+
77
+ @router.delete("/api/scheduled-tasks/{task_id}")
78
+ def delete_scheduled_task(task_id: str) -> dict:
79
+ success = scheduler_manager.delete_task(task_id)
80
+ if not success:
81
+ raise HTTPException(status_code=404, detail="Task not found")
82
+ return {"success": True, "message": "Task deleted"}
83
+
84
+
85
+ @router.post("/api/scheduled-tasks/{task_id}/enable")
86
+ def enable_scheduled_task(task_id: str) -> dict:
87
+ success = scheduler_manager.set_enabled(task_id, True)
88
+ if not success:
89
+ raise HTTPException(status_code=404, detail="Task not found")
90
+ return {"success": True, "message": "Task enabled"}
91
+
92
+
93
+ @router.post("/api/scheduled-tasks/{task_id}/disable")
94
+ def disable_scheduled_task(task_id: str) -> dict:
95
+ success = scheduler_manager.set_enabled(task_id, False)
96
+ if not success:
97
+ raise HTTPException(status_code=404, detail="Task not found")
98
+ return {"success": True, "message": "Task disabled"}
@@ -3,8 +3,9 @@
3
3
  import json
4
4
  import re
5
5
  import time
6
+ import urllib.error
6
7
  import urllib.request
7
- from typing import Any
8
+ from typing_extensions import TypedDict
8
9
 
9
10
  from fastapi import APIRouter
10
11
 
@@ -12,13 +13,30 @@ from AutoGLM_GUI.logger import logger
12
13
  from AutoGLM_GUI.schemas import VersionCheckResponse
13
14
  from AutoGLM_GUI.version import APP_VERSION
14
15
 
16
+
17
+ class GitHubRelease(TypedDict, total=False):
18
+ """GitHub Release API response structure."""
19
+
20
+ tag_name: str
21
+ html_url: str
22
+ published_at: str
23
+
24
+
25
+ class _VersionCache(TypedDict):
26
+ """Internal cache structure for version checking."""
27
+
28
+ data: VersionCheckResponse | None
29
+ timestamp: float
30
+ ttl: int
31
+
32
+
15
33
  router = APIRouter()
16
34
 
17
35
  # In-memory cache for version check results
18
- _version_cache: dict[str, Any] = {
36
+ _version_cache: _VersionCache = {
19
37
  "data": None,
20
38
  "timestamp": 0,
21
- "ttl": 3600, # 1 hour cache TTL
39
+ "ttl": 3600,
22
40
  }
23
41
 
24
42
  # GitHub repository information
@@ -74,13 +92,8 @@ def compare_versions(current: str, latest: str) -> bool:
74
92
  return latest_tuple > current_tuple
75
93
 
76
94
 
77
- def fetch_latest_release() -> dict[str, Any] | None:
78
- """
79
- Fetch latest release information from GitHub API.
80
-
81
- Returns:
82
- Release data dict with 'tag_name', 'html_url', 'published_at' or None on error
83
- """
95
+ def fetch_latest_release() -> GitHubRelease | None:
96
+ """Fetch latest release information from GitHub API."""
84
97
  try:
85
98
  # Create request with User-Agent header (required by GitHub API)
86
99
  req = urllib.request.Request(
@@ -17,7 +17,8 @@ def list_workflows() -> WorkflowListResponse:
17
17
  """获取所有 workflows."""
18
18
  from AutoGLM_GUI.workflow_manager import workflow_manager
19
19
 
20
- workflows = workflow_manager.list_workflows()
20
+ workflow_dicts = workflow_manager.list_workflows()
21
+ workflows = [WorkflowResponse(**wf) for wf in workflow_dicts]
21
22
  return WorkflowListResponse(workflows=workflows)
22
23
 
23
24
 
AutoGLM_GUI/config.py CHANGED
@@ -1,23 +1,81 @@
1
- """Application configuration singleton."""
1
+ """AutoGLM-GUI 核心配置定义
2
2
 
3
- import os
4
- from dataclasses import dataclass
3
+ 这个模块定义了项目自己的配置类,替代 phone_agent 的配置,
4
+ 实现配置层的解耦和扩展性。
5
+
6
+ 设计原则:
7
+ - 配置类提供 AutoGLM-GUI 核心业务逻辑所需的参数
8
+ - 避免在 API 层和业务层直接使用外部库的类型
9
+ """
10
+
11
+ from dataclasses import dataclass, field
12
+ from typing import Any
5
13
 
6
14
 
7
15
  @dataclass
8
- class AppConfig:
9
- """Global application configuration."""
16
+ class ModelConfig:
17
+ """模型配置
10
18
 
11
- base_url: str = ""
12
- model_name: str = "autoglm-phone-9b"
19
+ OpenAI 兼容 API 的配置参数。
20
+
21
+ Attributes:
22
+ base_url: API 端点 URL (例如: "http://localhost:8000/v1")
23
+ api_key: API 认证密钥 (本地部署可选)
24
+ model_name: 模型标识符 (例如: "autoglm-phone-9b")
25
+ max_tokens: 响应最大 token 数 (默认: 3000)
26
+ temperature: 采样温度 0-1 (默认: 0.0)
27
+ top_p: Nucleus 采样阈值 (默认: 0.85)
28
+ frequency_penalty: 频率惩罚 -2 到 2 (默认: 0.2)
29
+ extra_body: 特定后端的额外参数
30
+ lang: UI 消息语言: 'cn' 或 'en'
31
+ """
32
+
33
+ base_url: str = "http://localhost:8000/v1"
13
34
  api_key: str = "EMPTY"
35
+ model_name: str = "autoglm-phone-9b"
36
+ max_tokens: int = 3000
37
+ temperature: float = 0.0
38
+ top_p: float = 0.85
39
+ frequency_penalty: float = 0.2
40
+ extra_body: dict[str, Any] = field(default_factory=dict)
41
+ lang: str = "cn"
42
+
14
43
 
15
- def refresh_from_env(self):
16
- """从环境变量刷新配置(适用于 reload 模式)"""
17
- self.base_url = os.getenv("AUTOGLM_BASE_URL", self.base_url)
18
- self.model_name = os.getenv("AUTOGLM_MODEL_NAME", self.model_name)
19
- self.api_key = os.getenv("AUTOGLM_API_KEY", self.api_key)
44
+ @dataclass
45
+ class AgentConfig:
46
+ """Agent 配置
47
+
48
+ 控制 Agent 的行为参数。
49
+
50
+ Attributes:
51
+ max_steps: 单次任务最大执行步数 (默认: 100)
52
+ device_id: 设备标识符 (USB serial 或 IP:port)
53
+ lang: 语言设置 'cn' 或 'en'
54
+ system_prompt: 自定义系统提示词 (None 则使用默认)
55
+ verbose: 是否输出详细日志
56
+ """
57
+
58
+ max_steps: int = 100
59
+ device_id: str | None = None
60
+ lang: str = "cn"
61
+ system_prompt: str | None = None
62
+ verbose: bool = True
63
+
64
+
65
+ @dataclass
66
+ class StepResult:
67
+ """Agent 单步执行结果
20
68
 
69
+ Attributes:
70
+ success: 本步骤是否执行成功
71
+ finished: 整个任务是否已完成
72
+ action: 执行的动作字典 (包含 action type 和参数)
73
+ thinking: Agent 的思考过程
74
+ message: 结果消息 (可选)
75
+ """
21
76
 
22
- # Global singleton instance
23
- config = AppConfig()
77
+ success: bool
78
+ finished: bool
79
+ action: dict[str, Any] | None
80
+ thinking: str
81
+ message: str | None = None
@@ -52,11 +52,27 @@ class ConfigModel(BaseModel):
52
52
  model_name: str = "autoglm-phone-9b"
53
53
  api_key: str = "EMPTY"
54
54
 
55
- # 双模型配置
56
- dual_model_enabled: bool = False
57
- decision_base_url: str = ""
58
- decision_model_name: str = ""
59
- decision_api_key: str = ""
55
+ # Agent 类型配置
56
+ agent_type: str = "glm" # Agent type (e.g., "glm", "mai")
57
+ agent_config_params: dict | None = None # Agent-specific configuration
58
+
59
+ # Agent 执行配置
60
+ default_max_steps: int = 100 # 单次任务最大执行步数
61
+
62
+ # 决策模型配置(用于分层代理)
63
+ decision_base_url: str | None = None
64
+ decision_model_name: str | None = None
65
+ decision_api_key: str | None = None
66
+
67
+ @field_validator("default_max_steps")
68
+ @classmethod
69
+ def validate_default_max_steps(cls, v: int) -> int:
70
+ """验证 default_max_steps 范围."""
71
+ if v <= 0:
72
+ raise ValueError("default_max_steps must be positive")
73
+ if v > 1000:
74
+ raise ValueError("default_max_steps must be <= 1000")
75
+ return v
60
76
 
61
77
  @field_validator("base_url")
62
78
  @classmethod
@@ -76,11 +92,23 @@ class ConfigModel(BaseModel):
76
92
 
77
93
  @field_validator("decision_base_url")
78
94
  @classmethod
79
- def validate_decision_base_url(cls, v: str) -> str:
95
+ def validate_decision_base_url(cls, v: str | None) -> str | None:
80
96
  """验证 decision_base_url 格式."""
81
- if v and not v.startswith(("http://", "https://")):
82
- raise ValueError("decision_base_url must start with http:// or https://")
83
- return v.rstrip("/") # 去除尾部斜杠
97
+ if v is not None:
98
+ if not v.startswith(("http://", "https://")):
99
+ raise ValueError(
100
+ "decision_base_url must start with http:// or https://"
101
+ )
102
+ return v.rstrip("/")
103
+ return v
104
+
105
+ @field_validator("decision_model_name")
106
+ @classmethod
107
+ def validate_decision_model_name(cls, v: str | None) -> str | None:
108
+ """验证 decision_model_name 非空."""
109
+ if v is not None and (not v or not v.strip()):
110
+ raise ValueError("decision_model_name cannot be empty string")
111
+ return v.strip() if v else v
84
112
 
85
113
 
86
114
  # ==================== 配置层数据类 ====================
@@ -93,8 +121,12 @@ class ConfigLayer:
93
121
  base_url: Optional[str] = None
94
122
  model_name: Optional[str] = None
95
123
  api_key: Optional[str] = None
96
- # 双模型配置
97
- dual_model_enabled: Optional[bool] = None
124
+ # Agent 类型配置
125
+ agent_type: Optional[str] = None
126
+ agent_config_params: Optional[dict] = None
127
+ # Agent 执行配置
128
+ default_max_steps: Optional[int] = None
129
+ # 决策模型配置
98
130
  decision_base_url: Optional[str] = None
99
131
  decision_model_name: Optional[str] = None
100
132
  decision_api_key: Optional[str] = None
@@ -125,7 +157,9 @@ class ConfigLayer:
125
157
  "base_url": self.base_url,
126
158
  "model_name": self.model_name,
127
159
  "api_key": self.api_key,
128
- "dual_model_enabled": self.dual_model_enabled,
160
+ "agent_type": self.agent_type,
161
+ "agent_config_params": self.agent_config_params,
162
+ "default_max_steps": self.default_max_steps,
129
163
  "decision_base_url": self.decision_base_url,
130
164
  "decision_model_name": self.decision_model_name,
131
165
  "decision_api_key": self.decision_api_key,
@@ -188,6 +222,12 @@ class UnifiedConfigManager:
188
222
  base_url="",
189
223
  model_name="autoglm-phone-9b",
190
224
  api_key="EMPTY",
225
+ agent_type="glm",
226
+ agent_config_params=None,
227
+ default_max_steps=100,
228
+ decision_base_url=None,
229
+ decision_model_name=None,
230
+ decision_api_key=None,
191
231
  source=ConfigSource.DEFAULT,
192
232
  )
193
233
 
@@ -234,15 +274,26 @@ class UnifiedConfigManager:
234
274
  - AUTOGLM_BASE_URL
235
275
  - AUTOGLM_MODEL_NAME
236
276
  - AUTOGLM_API_KEY
277
+ - AUTOGLM_DECISION_BASE_URL
278
+ - AUTOGLM_DECISION_MODEL_NAME
279
+ - AUTOGLM_DECISION_API_KEY
237
280
  """
238
281
  base_url = os.getenv("AUTOGLM_BASE_URL")
239
282
  model_name = os.getenv("AUTOGLM_MODEL_NAME")
240
283
  api_key = os.getenv("AUTOGLM_API_KEY")
241
284
 
285
+ # 决策模型环境变量
286
+ decision_base_url = os.getenv("AUTOGLM_DECISION_BASE_URL")
287
+ decision_model_name = os.getenv("AUTOGLM_DECISION_MODEL_NAME")
288
+ decision_api_key = os.getenv("AUTOGLM_DECISION_API_KEY")
289
+
242
290
  self._env_layer = ConfigLayer(
243
291
  base_url=base_url if base_url else None,
244
292
  model_name=model_name if model_name else None,
245
293
  api_key=api_key if api_key else None,
294
+ decision_base_url=decision_base_url if decision_base_url else None,
295
+ decision_model_name=decision_model_name if decision_model_name else None,
296
+ decision_api_key=decision_api_key if decision_api_key else None,
246
297
  source=ConfigSource.ENV,
247
298
  )
248
299
  self._effective_config = None # 清除缓存
@@ -296,7 +347,11 @@ class UnifiedConfigManager:
296
347
  base_url=config_data.get("base_url"),
297
348
  model_name=config_data.get("model_name"),
298
349
  api_key=config_data.get("api_key"),
299
- dual_model_enabled=config_data.get("dual_model_enabled"),
350
+ agent_type=config_data.get(
351
+ "agent_type", "glm"
352
+ ), # 默认 'glm',兼容旧配置
353
+ agent_config_params=config_data.get("agent_config_params"),
354
+ default_max_steps=config_data.get("default_max_steps"),
300
355
  decision_base_url=config_data.get("decision_base_url"),
301
356
  decision_model_name=config_data.get("decision_model_name"),
302
357
  decision_api_key=config_data.get("decision_api_key"),
@@ -327,7 +382,9 @@ class UnifiedConfigManager:
327
382
  base_url: str,
328
383
  model_name: str,
329
384
  api_key: Optional[str] = None,
330
- dual_model_enabled: Optional[bool] = None,
385
+ agent_type: Optional[str] = None,
386
+ agent_config_params: Optional[dict] = None,
387
+ default_max_steps: Optional[int] = None,
331
388
  decision_base_url: Optional[str] = None,
332
389
  decision_model_name: Optional[str] = None,
333
390
  decision_api_key: Optional[str] = None,
@@ -340,10 +397,12 @@ class UnifiedConfigManager:
340
397
  base_url: Base URL
341
398
  model_name: 模型名称
342
399
  api_key: API key(可选)
343
- dual_model_enabled: 是否启用双模型
344
- decision_base_url: 决策模型 Base URL
345
- decision_model_name: 决策模型名称
346
- decision_api_key: 决策模型 API key
400
+ agent_type: Agent 类型(可选,如 "glm", "mai")
401
+ agent_config_params: Agent 特定配置参数(可选)
402
+ default_max_steps: 默认最大执行步数(可选)
403
+ decision_base_url: 决策模型 Base URL(可选)
404
+ decision_model_name: 决策模型名称(可选)
405
+ decision_api_key: 决策模型 API Key(可选)
347
406
  merge_mode: 是否合并现有配置(True: 保留未提供的字段)
348
407
 
349
408
  Returns:
@@ -354,20 +413,26 @@ class UnifiedConfigManager:
354
413
  self._config_path.parent.mkdir(parents=True, exist_ok=True)
355
414
 
356
415
  # 准备新配置
357
- new_config = {
416
+ new_config: dict[str, str | bool | int | dict | None] = {
358
417
  "base_url": base_url,
359
418
  "model_name": model_name,
360
419
  }
361
420
 
362
421
  if api_key:
363
422
  new_config["api_key"] = api_key
364
- if dual_model_enabled is not None:
365
- new_config["dual_model_enabled"] = dual_model_enabled
366
- if decision_base_url:
423
+ if agent_type is not None:
424
+ new_config["agent_type"] = agent_type
425
+ if agent_config_params is not None:
426
+ new_config["agent_config_params"] = agent_config_params
427
+ if default_max_steps is not None:
428
+ new_config["default_max_steps"] = default_max_steps
429
+
430
+ # 决策模型配置
431
+ if decision_base_url is not None:
367
432
  new_config["decision_base_url"] = decision_base_url
368
- if decision_model_name:
433
+ if decision_model_name is not None:
369
434
  new_config["decision_model_name"] = decision_model_name
370
- if decision_api_key:
435
+ if decision_api_key is not None:
371
436
  new_config["decision_api_key"] = decision_api_key
372
437
 
373
438
  # 合并模式:保留现有文件中未提供的字段
@@ -379,7 +444,9 @@ class UnifiedConfigManager:
379
444
  # 保留未提供的字段
380
445
  preserve_keys = [
381
446
  "api_key",
382
- "dual_model_enabled",
447
+ "agent_type",
448
+ "agent_config_params",
449
+ "default_max_steps",
383
450
  "decision_base_url",
384
451
  "decision_model_name",
385
452
  "decision_api_key",
@@ -467,7 +534,9 @@ class UnifiedConfigManager:
467
534
  "base_url",
468
535
  "model_name",
469
536
  "api_key",
470
- "dual_model_enabled",
537
+ "agent_type",
538
+ "agent_config_params",
539
+ "default_max_steps",
471
540
  "decision_base_url",
472
541
  "decision_model_name",
473
542
  "decision_api_key",
@@ -633,7 +702,9 @@ class UnifiedConfigManager:
633
702
  "base_url": config.base_url,
634
703
  "model_name": config.model_name,
635
704
  "api_key": config.api_key,
636
- "dual_model_enabled": config.dual_model_enabled,
705
+ "agent_type": config.agent_type,
706
+ "agent_config_params": config.agent_config_params,
707
+ "default_max_steps": config.default_max_steps,
637
708
  "decision_base_url": config.decision_base_url,
638
709
  "decision_model_name": config.decision_model_name,
639
710
  "decision_api_key": config.decision_api_key,