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
@@ -3,7 +3,8 @@
3
3
  import asyncio
4
4
  import platform
5
5
  import subprocess
6
- from typing import Any, Sequence
6
+ from asyncio.subprocess import Process as AsyncProcess
7
+ from typing import Sequence
7
8
 
8
9
 
9
10
  def is_windows() -> bool:
@@ -51,7 +52,9 @@ async def run_cmd_silently(cmd: Sequence[str]) -> subprocess.CompletedProcess:
51
52
  return subprocess.CompletedProcess(cmd, return_code, stdout_str, stderr_str)
52
53
 
53
54
 
54
- async def spawn_process(cmd: Sequence[str], *, capture_output: bool = False) -> Any:
55
+ async def spawn_process(
56
+ cmd: Sequence[str], *, capture_output: bool = False
57
+ ) -> subprocess.Popen[bytes] | AsyncProcess:
55
58
  """Start a long-running process with optional stdio capture."""
56
59
  stdout = subprocess.PIPE if capture_output else None
57
60
  stderr = subprocess.PIPE if capture_output else None
@@ -60,3 +63,29 @@ async def spawn_process(cmd: Sequence[str], *, capture_output: bool = False) ->
60
63
  return subprocess.Popen(cmd, stdout=stdout, stderr=stderr)
61
64
 
62
65
  return await asyncio.create_subprocess_exec(*cmd, stdout=stdout, stderr=stderr)
66
+
67
+
68
+ def build_adb_command(device_id: str | None = None, adb_path: str = "adb") -> list[str]:
69
+ """Build ADB command prefix with optional device specifier.
70
+
71
+ This centralizes the logic for constructing ADB commands across all modules.
72
+
73
+ Args:
74
+ device_id: Optional ADB device serial (e.g., "192.168.1.100:5555" or USB serial)
75
+ adb_path: Path to ADB executable (default: "adb")
76
+
77
+ Returns:
78
+ List of command parts to use with subprocess (e.g., ["adb", "-s", "device_id"])
79
+
80
+ Examples:
81
+ >>> build_adb_command()
82
+ ['adb']
83
+ >>> build_adb_command(device_id="192.168.1.100:5555")
84
+ ['adb', '-s', '192.168.1.100:5555']
85
+ >>> build_adb_command(device_id="emulator-5554", adb_path="/usr/local/bin/adb")
86
+ ['/usr/local/bin/adb', '-s', 'emulator-5554']
87
+ """
88
+ cmd = [adb_path]
89
+ if device_id:
90
+ cmd.extend(["-s", device_id])
91
+ return cmd
@@ -0,0 +1,15 @@
1
+ from AutoGLM_GUI.agents.glm import SYSTEM_PROMPT_EN, SYSTEM_PROMPT_ZH
2
+ from AutoGLM_GUI.i18n import get_message, get_messages
3
+
4
+
5
+ def get_system_prompt(lang: str = "cn") -> str:
6
+ if lang == "en":
7
+ return SYSTEM_PROMPT_EN
8
+ return SYSTEM_PROMPT_ZH
9
+
10
+
11
+ __all__ = [
12
+ "get_system_prompt",
13
+ "get_messages",
14
+ "get_message",
15
+ ]
@@ -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()