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.
- AutoGLM_GUI/__init__.py +11 -0
- AutoGLM_GUI/__main__.py +26 -4
- AutoGLM_GUI/actions/__init__.py +6 -0
- AutoGLM_GUI/actions/handler.py +196 -0
- AutoGLM_GUI/actions/types.py +15 -0
- AutoGLM_GUI/adb/__init__.py +53 -0
- AutoGLM_GUI/adb/apps.py +227 -0
- AutoGLM_GUI/adb/connection.py +323 -0
- AutoGLM_GUI/adb/device.py +171 -0
- AutoGLM_GUI/adb/input.py +67 -0
- AutoGLM_GUI/adb/screenshot.py +11 -0
- AutoGLM_GUI/adb/timing.py +167 -0
- 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 +292 -0
- AutoGLM_GUI/agents/glm/message_builder.py +81 -0
- AutoGLM_GUI/agents/glm/parser.py +110 -0
- AutoGLM_GUI/agents/glm/prompts_en.py +77 -0
- AutoGLM_GUI/agents/glm/prompts_zh.py +75 -0
- AutoGLM_GUI/agents/mai/__init__.py +28 -0
- AutoGLM_GUI/agents/mai/agent.py +405 -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 +188 -0
- AutoGLM_GUI/api/__init__.py +40 -21
- AutoGLM_GUI/api/agents.py +157 -240
- AutoGLM_GUI/api/control.py +9 -6
- AutoGLM_GUI/api/devices.py +102 -12
- AutoGLM_GUI/api/history.py +78 -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/i18n.py +81 -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 +96 -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 +304 -0
- AutoGLM_GUI/schemas.py +234 -72
- AutoGLM_GUI/scrcpy_stream.py +142 -24
- AutoGLM_GUI/socketio_server.py +100 -27
- AutoGLM_GUI/static/assets/{about-_XNhzQZX.js → about-BQm96DAl.js} +1 -1
- AutoGLM_GUI/static/assets/alert-dialog-B42XxGPR.js +1 -0
- AutoGLM_GUI/static/assets/chat-C0L2gQYG.js +129 -0
- AutoGLM_GUI/static/assets/circle-alert-D4rSJh37.js +1 -0
- AutoGLM_GUI/static/assets/dialog-DZ78cEcj.js +45 -0
- AutoGLM_GUI/static/assets/history-DFBv7TGc.js +1 -0
- AutoGLM_GUI/static/assets/index-Bzyv2yQ2.css +1 -0
- AutoGLM_GUI/static/assets/{index-Cy8TmmHV.js → index-CmZSnDqc.js} +1 -1
- AutoGLM_GUI/static/assets/index-CssG-3TH.js +11 -0
- AutoGLM_GUI/static/assets/label-BCUzE_nm.js +1 -0
- AutoGLM_GUI/static/assets/logs-eoFxn5of.js +1 -0
- AutoGLM_GUI/static/assets/popover-DLsuV5Sx.js +1 -0
- AutoGLM_GUI/static/assets/scheduled-tasks-MyqGJvy_.js +1 -0
- AutoGLM_GUI/static/assets/square-pen-zGWYrdfj.js +1 -0
- AutoGLM_GUI/static/assets/textarea-BX6y7uM5.js +1 -0
- AutoGLM_GUI/static/assets/workflows-CYFs6ssC.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.0.dist-info}/METADATA +137 -130
- autoglm_gui-1.5.0.dist-info/RECORD +157 -0
- AutoGLM_GUI/agents/mai_adapter.py +0 -627
- AutoGLM_GUI/api/dual_model.py +0 -317
- 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
- {autoglm_gui-1.4.1.dist-info → autoglm_gui-1.5.0.dist-info}/WHEEL +0 -0
- {autoglm_gui-1.4.1.dist-info → autoglm_gui-1.5.0.dist-info}/entry_points.txt +0 -0
- {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,
|
|
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,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]
|