autoglm-gui 1.4.1__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 (104) hide show
  1. AutoGLM_GUI/__init__.py +11 -0
  2. AutoGLM_GUI/__main__.py +26 -4
  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/screenshot.py +22 -1
  15. AutoGLM_GUI/adb_plus/serial.py +38 -20
  16. AutoGLM_GUI/adb_plus/touch.py +4 -9
  17. AutoGLM_GUI/agents/__init__.py +43 -12
  18. AutoGLM_GUI/agents/events.py +19 -0
  19. AutoGLM_GUI/agents/factory.py +31 -38
  20. AutoGLM_GUI/agents/glm/__init__.py +7 -0
  21. AutoGLM_GUI/agents/glm/agent.py +292 -0
  22. AutoGLM_GUI/agents/glm/message_builder.py +81 -0
  23. AutoGLM_GUI/agents/glm/parser.py +110 -0
  24. AutoGLM_GUI/agents/glm/prompts_en.py +77 -0
  25. AutoGLM_GUI/agents/glm/prompts_zh.py +75 -0
  26. AutoGLM_GUI/agents/mai/__init__.py +28 -0
  27. AutoGLM_GUI/agents/mai/agent.py +405 -0
  28. AutoGLM_GUI/agents/mai/parser.py +254 -0
  29. AutoGLM_GUI/agents/mai/prompts.py +103 -0
  30. AutoGLM_GUI/agents/mai/traj_memory.py +91 -0
  31. AutoGLM_GUI/agents/protocols.py +12 -8
  32. AutoGLM_GUI/agents/stream_runner.py +188 -0
  33. AutoGLM_GUI/api/__init__.py +40 -21
  34. AutoGLM_GUI/api/agents.py +157 -240
  35. AutoGLM_GUI/api/control.py +9 -6
  36. AutoGLM_GUI/api/devices.py +102 -12
  37. AutoGLM_GUI/api/history.py +78 -0
  38. AutoGLM_GUI/api/layered_agent.py +67 -15
  39. AutoGLM_GUI/api/media.py +64 -1
  40. AutoGLM_GUI/api/scheduled_tasks.py +98 -0
  41. AutoGLM_GUI/config.py +81 -0
  42. AutoGLM_GUI/config_manager.py +68 -51
  43. AutoGLM_GUI/device_manager.py +248 -29
  44. AutoGLM_GUI/device_protocol.py +1 -1
  45. AutoGLM_GUI/devices/adb_device.py +5 -10
  46. AutoGLM_GUI/devices/mock_device.py +4 -2
  47. AutoGLM_GUI/devices/remote_device.py +8 -3
  48. AutoGLM_GUI/history_manager.py +164 -0
  49. AutoGLM_GUI/i18n.py +81 -0
  50. AutoGLM_GUI/model/__init__.py +5 -0
  51. AutoGLM_GUI/model/message_builder.py +69 -0
  52. AutoGLM_GUI/model/types.py +24 -0
  53. AutoGLM_GUI/models/__init__.py +10 -0
  54. AutoGLM_GUI/models/history.py +96 -0
  55. AutoGLM_GUI/models/scheduled_task.py +71 -0
  56. AutoGLM_GUI/parsers/__init__.py +22 -0
  57. AutoGLM_GUI/parsers/base.py +50 -0
  58. AutoGLM_GUI/parsers/phone_parser.py +58 -0
  59. AutoGLM_GUI/phone_agent_manager.py +62 -396
  60. AutoGLM_GUI/platform_utils.py +26 -0
  61. AutoGLM_GUI/prompt_config.py +15 -0
  62. AutoGLM_GUI/prompts/__init__.py +32 -0
  63. AutoGLM_GUI/scheduler_manager.py +304 -0
  64. AutoGLM_GUI/schemas.py +234 -72
  65. AutoGLM_GUI/scrcpy_stream.py +142 -24
  66. AutoGLM_GUI/socketio_server.py +100 -27
  67. AutoGLM_GUI/static/assets/{about-_XNhzQZX.js → about-BQm96DAl.js} +1 -1
  68. AutoGLM_GUI/static/assets/alert-dialog-B42XxGPR.js +1 -0
  69. AutoGLM_GUI/static/assets/chat-C0L2gQYG.js +129 -0
  70. AutoGLM_GUI/static/assets/circle-alert-D4rSJh37.js +1 -0
  71. AutoGLM_GUI/static/assets/dialog-DZ78cEcj.js +45 -0
  72. AutoGLM_GUI/static/assets/history-DFBv7TGc.js +1 -0
  73. AutoGLM_GUI/static/assets/index-Bzyv2yQ2.css +1 -0
  74. AutoGLM_GUI/static/assets/{index-Cy8TmmHV.js → index-CmZSnDqc.js} +1 -1
  75. AutoGLM_GUI/static/assets/index-CssG-3TH.js +11 -0
  76. AutoGLM_GUI/static/assets/label-BCUzE_nm.js +1 -0
  77. AutoGLM_GUI/static/assets/logs-eoFxn5of.js +1 -0
  78. AutoGLM_GUI/static/assets/popover-DLsuV5Sx.js +1 -0
  79. AutoGLM_GUI/static/assets/scheduled-tasks-MyqGJvy_.js +1 -0
  80. AutoGLM_GUI/static/assets/square-pen-zGWYrdfj.js +1 -0
  81. AutoGLM_GUI/static/assets/textarea-BX6y7uM5.js +1 -0
  82. AutoGLM_GUI/static/assets/workflows-CYFs6ssC.js +1 -0
  83. AutoGLM_GUI/static/index.html +2 -2
  84. AutoGLM_GUI/types.py +17 -0
  85. {autoglm_gui-1.4.1.dist-info → autoglm_gui-1.5.0.dist-info}/METADATA +137 -130
  86. autoglm_gui-1.5.0.dist-info/RECORD +157 -0
  87. AutoGLM_GUI/agents/mai_adapter.py +0 -627
  88. AutoGLM_GUI/api/dual_model.py +0 -317
  89. AutoGLM_GUI/dual_model/__init__.py +0 -53
  90. AutoGLM_GUI/dual_model/decision_model.py +0 -664
  91. AutoGLM_GUI/dual_model/dual_agent.py +0 -917
  92. AutoGLM_GUI/dual_model/protocols.py +0 -354
  93. AutoGLM_GUI/dual_model/vision_model.py +0 -442
  94. AutoGLM_GUI/mai_ui_adapter/agent_wrapper.py +0 -291
  95. AutoGLM_GUI/phone_agent_patches.py +0 -147
  96. AutoGLM_GUI/static/assets/chat-DwJpiAWf.js +0 -126
  97. AutoGLM_GUI/static/assets/dialog-B3uW4T8V.js +0 -45
  98. AutoGLM_GUI/static/assets/index-Cpv2gSF1.css +0 -1
  99. AutoGLM_GUI/static/assets/index-UYYauTly.js +0 -12
  100. AutoGLM_GUI/static/assets/workflows-Du_de-dt.js +0 -1
  101. autoglm_gui-1.4.1.dist-info/RECORD +0 -117
  102. {autoglm_gui-1.4.1.dist-info → autoglm_gui-1.5.0.dist-info}/WHEEL +0 -0
  103. {autoglm_gui-1.4.1.dist-info → autoglm_gui-1.5.0.dist-info}/entry_points.txt +0 -0
  104. {autoglm_gui-1.4.1.dist-info → autoglm_gui-1.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,32 @@
1
+ """Prompt templates for agents."""
2
+
3
+ from pathlib import Path
4
+
5
+ # Import MAI prompt from new location
6
+ from AutoGLM_GUI.agents.mai.prompts import MAI_MOBILE_SYSTEM_PROMPT
7
+
8
+ # Import from parent-level prompts.py file
9
+ # When prompts/ directory exists, Python prioritizes it over prompts.py
10
+ # We need to import from sibling prompts.py file
11
+ parent_dir = Path(__file__).parent.parent
12
+ prompts_file = parent_dir / "prompts.py"
13
+
14
+ if prompts_file.exists():
15
+ import importlib.util
16
+
17
+ spec = importlib.util.spec_from_file_location("_prompts_legacy", prompts_file)
18
+ if spec and spec.loader:
19
+ _prompts_legacy = importlib.util.module_from_spec(spec)
20
+ spec.loader.exec_module(_prompts_legacy)
21
+ MCP_SYSTEM_PROMPT_ZH = getattr(_prompts_legacy, "MCP_SYSTEM_PROMPT_ZH", "")
22
+ MCP_SYSTEM_PROMPT_EN = getattr(_prompts_legacy, "MCP_SYSTEM_PROMPT_EN", "")
23
+ else:
24
+ # Fallback if file doesn't exist
25
+ MCP_SYSTEM_PROMPT_ZH = ""
26
+ MCP_SYSTEM_PROMPT_EN = ""
27
+
28
+ __all__ = [
29
+ "MAI_MOBILE_SYSTEM_PROMPT",
30
+ "MCP_SYSTEM_PROMPT_ZH",
31
+ "MCP_SYSTEM_PROMPT_EN",
32
+ ]
@@ -0,0 +1,304 @@
1
+ """Scheduled task manager with APScheduler."""
2
+
3
+ import json
4
+ from datetime import datetime
5
+ from pathlib import Path
6
+ from typing import Optional
7
+
8
+ from apscheduler.schedulers.background import BackgroundScheduler
9
+ from apscheduler.triggers.cron import CronTrigger
10
+
11
+ from AutoGLM_GUI.logger import logger
12
+ from AutoGLM_GUI.models.scheduled_task import ScheduledTask
13
+
14
+
15
+ class SchedulerManager:
16
+ _instance: Optional["SchedulerManager"] = None
17
+
18
+ def __new__(cls):
19
+ if cls._instance is None:
20
+ cls._instance = super().__new__(cls)
21
+ return cls._instance
22
+
23
+ def __init__(self):
24
+ if hasattr(self, "_initialized"):
25
+ return
26
+ self._initialized = True
27
+ self._tasks_path = Path.home() / ".config" / "autoglm" / "scheduled_tasks.json"
28
+ self._scheduler = BackgroundScheduler()
29
+ self._tasks: dict[str, ScheduledTask] = {}
30
+ self._file_mtime: Optional[float] = None
31
+
32
+ def start(self) -> None:
33
+ self._load_tasks()
34
+ for task in self._tasks.values():
35
+ if task.enabled:
36
+ self._add_job(task)
37
+ self._scheduler.start()
38
+ logger.info(f"SchedulerManager started with {len(self._tasks)} task(s)")
39
+
40
+ def shutdown(self) -> None:
41
+ self._scheduler.shutdown(wait=False)
42
+ logger.info("SchedulerManager shutdown")
43
+
44
+ def create_task(
45
+ self,
46
+ name: str,
47
+ workflow_uuid: str,
48
+ device_serialno: str,
49
+ cron_expression: str,
50
+ enabled: bool = True,
51
+ ) -> ScheduledTask:
52
+ task = ScheduledTask(
53
+ name=name,
54
+ workflow_uuid=workflow_uuid,
55
+ device_serialno=device_serialno,
56
+ cron_expression=cron_expression,
57
+ enabled=enabled,
58
+ )
59
+ self._tasks[task.id] = task
60
+ self._save_tasks()
61
+
62
+ if enabled:
63
+ self._add_job(task)
64
+
65
+ logger.info(f"Created scheduled task: {name} (id={task.id})")
66
+ return task
67
+
68
+ def update_task(self, task_id: str, **kwargs) -> Optional[ScheduledTask]:
69
+ task = self._tasks.get(task_id)
70
+ if not task:
71
+ return None
72
+
73
+ old_enabled = task.enabled
74
+ old_cron = task.cron_expression
75
+
76
+ for key, value in kwargs.items():
77
+ if value is not None and hasattr(task, key):
78
+ setattr(task, key, value)
79
+
80
+ task.updated_at = datetime.now()
81
+ self._save_tasks()
82
+
83
+ if old_enabled and not task.enabled:
84
+ self._remove_job(task_id)
85
+ elif not old_enabled and task.enabled:
86
+ self._add_job(task)
87
+ elif task.enabled and old_cron != task.cron_expression:
88
+ self._remove_job(task_id)
89
+ self._add_job(task)
90
+
91
+ logger.info(f"Updated scheduled task: {task.name} (id={task_id})")
92
+ return task
93
+
94
+ def delete_task(self, task_id: str) -> bool:
95
+ task = self._tasks.pop(task_id, None)
96
+ if not task:
97
+ return False
98
+
99
+ self._remove_job(task_id)
100
+ self._save_tasks()
101
+ logger.info(f"Deleted scheduled task: {task.name} (id={task_id})")
102
+ return True
103
+
104
+ def list_tasks(self) -> list[ScheduledTask]:
105
+ return list(self._tasks.values())
106
+
107
+ def get_task(self, task_id: str) -> Optional[ScheduledTask]:
108
+ return self._tasks.get(task_id)
109
+
110
+ def set_enabled(self, task_id: str, enabled: bool) -> bool:
111
+ task = self._tasks.get(task_id)
112
+ if not task:
113
+ return False
114
+
115
+ if task.enabled == enabled:
116
+ return True
117
+
118
+ task.enabled = enabled
119
+ task.updated_at = datetime.now()
120
+ self._save_tasks()
121
+
122
+ if enabled:
123
+ self._add_job(task)
124
+ else:
125
+ self._remove_job(task_id)
126
+
127
+ logger.info(f"{'Enabled' if enabled else 'Disabled'} task: {task.name}")
128
+ return True
129
+
130
+ def get_next_run_time(self, task_id: str) -> Optional[datetime]:
131
+ job = self._scheduler.get_job(task_id)
132
+ if job and job.next_run_time:
133
+ return job.next_run_time.replace(tzinfo=None)
134
+ return None
135
+
136
+ def _add_job(self, task: ScheduledTask) -> None:
137
+ try:
138
+ parts = task.cron_expression.split()
139
+ if len(parts) != 5:
140
+ logger.error(f"Invalid cron expression: {task.cron_expression}")
141
+ return
142
+
143
+ trigger = CronTrigger(
144
+ minute=parts[0],
145
+ hour=parts[1],
146
+ day=parts[2],
147
+ month=parts[3],
148
+ day_of_week=parts[4],
149
+ )
150
+
151
+ self._scheduler.add_job(
152
+ self._execute_task,
153
+ trigger=trigger,
154
+ id=task.id,
155
+ args=[task.id],
156
+ replace_existing=True,
157
+ )
158
+ logger.debug(f"Added job for task: {task.name}")
159
+ except Exception as e:
160
+ logger.error(f"Failed to add job for task {task.name}: {e}")
161
+
162
+ def _remove_job(self, task_id: str) -> None:
163
+ try:
164
+ if self._scheduler.get_job(task_id):
165
+ self._scheduler.remove_job(task_id)
166
+ logger.debug(f"Removed job: {task_id}")
167
+ except Exception as e:
168
+ logger.warning(f"Failed to remove job {task_id}: {e}")
169
+
170
+ def _execute_task(self, task_id: str) -> None:
171
+ task = self._tasks.get(task_id)
172
+ if not task:
173
+ logger.warning(f"Task {task_id} not found for execution")
174
+ return
175
+
176
+ logger.info(f"Executing scheduled task: {task.name}")
177
+
178
+ from AutoGLM_GUI.device_manager import DeviceManager
179
+ from AutoGLM_GUI.history_manager import history_manager
180
+ from AutoGLM_GUI.models.history import ConversationRecord
181
+ from AutoGLM_GUI.phone_agent_manager import PhoneAgentManager
182
+ from AutoGLM_GUI.workflow_manager import workflow_manager
183
+
184
+ workflow = workflow_manager.get_workflow(task.workflow_uuid)
185
+ if not workflow:
186
+ self._record_failure(task, "Workflow not found")
187
+ return
188
+
189
+ device_manager = DeviceManager.get_instance()
190
+ device = None
191
+ for d in device_manager.get_devices():
192
+ if d.serial == task.device_serialno and d.state.value == "online":
193
+ device = d
194
+ break
195
+
196
+ if not device:
197
+ self._record_failure(task, "Device offline")
198
+ return
199
+
200
+ manager = PhoneAgentManager.get_instance()
201
+ acquired = manager.acquire_device(
202
+ device.primary_device_id,
203
+ timeout=0,
204
+ raise_on_timeout=False,
205
+ auto_initialize=True,
206
+ )
207
+
208
+ if not acquired:
209
+ self._record_failure(task, "Device busy")
210
+ return
211
+
212
+ start_time = datetime.now()
213
+ try:
214
+ agent = manager.get_agent(device.primary_device_id)
215
+ agent.reset()
216
+ result = agent.run(workflow["text"])
217
+ steps = agent.step_count
218
+
219
+ end_time = datetime.now()
220
+ record = ConversationRecord(
221
+ task_text=workflow["text"],
222
+ final_message=result,
223
+ success=True,
224
+ steps=steps,
225
+ start_time=start_time,
226
+ end_time=end_time,
227
+ duration_ms=int((end_time - start_time).total_seconds() * 1000),
228
+ source="scheduled",
229
+ source_detail=task.name,
230
+ )
231
+ history_manager.add_record(task.device_serialno, record)
232
+
233
+ self._record_success(task, result)
234
+
235
+ except Exception as e:
236
+ end_time = datetime.now()
237
+ error_msg = str(e)
238
+ logger.error(f"Scheduled task failed: {task.name} - {error_msg}")
239
+
240
+ record = ConversationRecord(
241
+ task_text=workflow["text"],
242
+ final_message=error_msg,
243
+ success=False,
244
+ steps=0,
245
+ start_time=start_time,
246
+ end_time=end_time,
247
+ duration_ms=int((end_time - start_time).total_seconds() * 1000),
248
+ source="scheduled",
249
+ source_detail=task.name,
250
+ error_message=error_msg,
251
+ )
252
+ history_manager.add_record(task.device_serialno, record)
253
+
254
+ self._record_failure(task, error_msg)
255
+
256
+ finally:
257
+ manager.release_device(device.primary_device_id)
258
+
259
+ def _record_success(self, task: ScheduledTask, message: str) -> None:
260
+ task.last_run_time = datetime.now()
261
+ task.last_run_success = True
262
+ task.last_run_message = message[:500] if message else ""
263
+ self._save_tasks()
264
+ logger.info(f"Scheduled task completed: {task.name}")
265
+
266
+ def _record_failure(self, task: ScheduledTask, error: str) -> None:
267
+ task.last_run_time = datetime.now()
268
+ task.last_run_success = False
269
+ task.last_run_message = error[:500] if error else ""
270
+ self._save_tasks()
271
+ logger.warning(f"Scheduled task failed: {task.name} - {error}")
272
+
273
+ def _load_tasks(self) -> None:
274
+ if not self._tasks_path.exists():
275
+ return
276
+
277
+ try:
278
+ with open(self._tasks_path, encoding="utf-8") as f:
279
+ data = json.load(f)
280
+ tasks_data = data.get("tasks", [])
281
+ self._tasks = {t["id"]: ScheduledTask.from_dict(t) for t in tasks_data}
282
+ self._file_mtime = self._tasks_path.stat().st_mtime
283
+ logger.debug(f"Loaded {len(self._tasks)} scheduled tasks")
284
+ except Exception as e:
285
+ logger.warning(f"Failed to load scheduled tasks: {e}")
286
+
287
+ def _save_tasks(self) -> None:
288
+ self._tasks_path.parent.mkdir(parents=True, exist_ok=True)
289
+ temp_path = self._tasks_path.with_suffix(".tmp")
290
+
291
+ try:
292
+ data = {"tasks": [t.to_dict() for t in self._tasks.values()]}
293
+ with open(temp_path, "w", encoding="utf-8") as f:
294
+ json.dump(data, f, indent=2, ensure_ascii=False)
295
+ temp_path.replace(self._tasks_path)
296
+ self._file_mtime = self._tasks_path.stat().st_mtime
297
+ logger.debug(f"Saved {len(self._tasks)} scheduled tasks")
298
+ except Exception as e:
299
+ logger.error(f"Failed to save scheduled tasks: {e}")
300
+ if temp_path.exists():
301
+ temp_path.unlink()
302
+
303
+
304
+ scheduler_manager = SchedulerManager()
AutoGLM_GUI/schemas.py CHANGED
@@ -2,63 +2,11 @@
2
2
 
3
3
  import re
4
4
 
5
- from pydantic import BaseModel, Field, field_validator
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
- model: APIModelConfig | None = Field(default=None, alias="model_config")
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 or not v.strip():
416
- return None
417
- v = v.strip()
418
- if not re.match(r"^https?://", v):
419
- raise ValueError("decision_base_url must start with http:// or https://")
420
- return v
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,210 @@ 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 HistoryRecordResponse(BaseModel):
706
+ """历史记录条目响应."""
707
+
708
+ id: str
709
+ task_text: str
710
+ final_message: str
711
+ success: bool
712
+ steps: int
713
+ start_time: str
714
+ end_time: str | None
715
+ duration_ms: int
716
+ source: str
717
+ source_detail: str
718
+ error_message: str | None
719
+
720
+
721
+ class HistoryListResponse(BaseModel):
722
+ """历史记录列表响应."""
723
+
724
+ records: list[HistoryRecordResponse]
725
+ total: int
726
+ limit: int
727
+ offset: int
728
+
729
+
730
+ # Scheduled Task Models
731
+
732
+
733
+ class ScheduledTaskCreate(BaseModel):
734
+ """创建定时任务请求."""
735
+
736
+ name: str
737
+ workflow_uuid: str
738
+ device_serialno: str
739
+ cron_expression: str
740
+ enabled: bool = True
741
+
742
+ @field_validator("name")
743
+ @classmethod
744
+ def validate_name(cls, v: str) -> str:
745
+ if not v or not v.strip():
746
+ raise ValueError("name cannot be empty")
747
+ return v.strip()
748
+
749
+ @field_validator("cron_expression")
750
+ @classmethod
751
+ def validate_cron(cls, v: str) -> str:
752
+ if not v or not v.strip():
753
+ raise ValueError("cron_expression cannot be empty")
754
+ parts = v.strip().split()
755
+ if len(parts) != 5:
756
+ raise ValueError(
757
+ "cron_expression must have 5 fields (minute hour day month weekday)"
758
+ )
759
+ return v.strip()
760
+
761
+
762
+ class ScheduledTaskUpdate(BaseModel):
763
+ """更新定时任务请求."""
764
+
765
+ name: str | None = None
766
+ workflow_uuid: str | None = None
767
+ device_serialno: str | None = None
768
+ cron_expression: str | None = None
769
+ enabled: bool | None = None
770
+
771
+ @field_validator("cron_expression")
772
+ @classmethod
773
+ def validate_cron(cls, v: str | None) -> str | None:
774
+ if v is None:
775
+ return v
776
+ if not v.strip():
777
+ raise ValueError("cron_expression cannot be empty")
778
+ parts = v.strip().split()
779
+ if len(parts) != 5:
780
+ raise ValueError(
781
+ "cron_expression must have 5 fields (minute hour day month weekday)"
782
+ )
783
+ return v.strip()
784
+
785
+
786
+ class ScheduledTaskResponse(BaseModel):
787
+ """定时任务响应."""
788
+
789
+ id: str
790
+ name: str
791
+ workflow_uuid: str
792
+ device_serialno: str
793
+ cron_expression: str
794
+ enabled: bool
795
+ created_at: str
796
+ updated_at: str
797
+ last_run_time: str | None
798
+ last_run_success: bool | None
799
+ last_run_message: str | None
800
+ next_run_time: str | None = None
801
+
802
+
803
+ class ScheduledTaskListResponse(BaseModel):
804
+ """定时任务列表响应."""
805
+
806
+ tasks: list[ScheduledTaskResponse]