autoglm-gui 1.5.5__py3-none-any.whl → 1.5.7__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/api/agents.py +118 -93
- AutoGLM_GUI/api/devices.py +177 -0
- AutoGLM_GUI/api/scheduled_tasks.py +7 -2
- AutoGLM_GUI/device_group_manager.py +354 -0
- AutoGLM_GUI/models/__init__.py +8 -0
- AutoGLM_GUI/models/device_group.py +63 -0
- AutoGLM_GUI/models/scheduled_task.py +47 -4
- AutoGLM_GUI/scheduler_manager.py +255 -80
- AutoGLM_GUI/schemas.py +148 -3
- AutoGLM_GUI/static/assets/{about-BwLRPh96.js → about-BN7TF-uS.js} +1 -1
- AutoGLM_GUI/static/assets/{alert-dialog-BvDNaR9v.js → alert-dialog-D62FbnlZ.js} +1 -1
- AutoGLM_GUI/static/assets/chat-OpHm69J3.js +134 -0
- AutoGLM_GUI/static/assets/{dialog-IM0Ds7Lf.js → dialog-LOCyYNCR.js} +3 -3
- AutoGLM_GUI/static/assets/{eye-BWBwz8sy.js → eye-CphvL3Nj.js} +1 -1
- AutoGLM_GUI/static/assets/folder-open-1SAaazEb.js +1 -0
- AutoGLM_GUI/static/assets/{history-BkQlPjpV.js → history-Ef0dqgi8.js} +1 -1
- AutoGLM_GUI/static/assets/index-84TrNz7w.css +1 -0
- AutoGLM_GUI/static/assets/index-CGykihaE.js +1 -0
- AutoGLM_GUI/static/assets/index-DGbZjnSP.js +11 -0
- AutoGLM_GUI/static/assets/{label-CmQFo_IT.js → label-YliMUBOr.js} +1 -1
- AutoGLM_GUI/static/assets/logs-DeHKldwN.js +1 -0
- AutoGLM_GUI/static/assets/popover-DQ7GD1WC.js +1 -0
- AutoGLM_GUI/static/assets/scheduled-tasks-DL6pE9tE.js +1 -0
- AutoGLM_GUI/static/assets/{textarea-BlKvI11g.js → textarea-thgveQFy.js} +1 -1
- AutoGLM_GUI/static/assets/{workflows-CRq1fJf5.js → workflows-DRsmfMuf.js} +1 -1
- AutoGLM_GUI/static/index.html +2 -2
- {autoglm_gui-1.5.5.dist-info → autoglm_gui-1.5.7.dist-info}/METADATA +355 -6
- {autoglm_gui-1.5.5.dist-info → autoglm_gui-1.5.7.dist-info}/RECORD +31 -29
- AutoGLM_GUI/static/assets/chat-DAmrsouh.js +0 -129
- AutoGLM_GUI/static/assets/circle-alert-C8768IhH.js +0 -1
- AutoGLM_GUI/static/assets/index-B0fISVXF.js +0 -1
- AutoGLM_GUI/static/assets/index-CH4jPveL.js +0 -11
- AutoGLM_GUI/static/assets/index-DSIMVL8V.css +0 -1
- AutoGLM_GUI/static/assets/logs-ChcSA2r_.js +0 -1
- AutoGLM_GUI/static/assets/popover-BnpBfSOh.js +0 -1
- AutoGLM_GUI/static/assets/scheduled-tasks-BwgoPEdP.js +0 -1
- {autoglm_gui-1.5.5.dist-info → autoglm_gui-1.5.7.dist-info}/WHEEL +0 -0
- {autoglm_gui-1.5.5.dist-info → autoglm_gui-1.5.7.dist-info}/entry_points.txt +0 -0
- {autoglm_gui-1.5.5.dist-info → autoglm_gui-1.5.7.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
"""Device group management module.
|
|
2
|
+
|
|
3
|
+
Features:
|
|
4
|
+
- 单例模式
|
|
5
|
+
- JSON 文件持久化
|
|
6
|
+
- 基于 mtime 的缓存机制
|
|
7
|
+
- 原子文件写入
|
|
8
|
+
- 默认分组自动创建
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from threading import RLock
|
|
15
|
+
from typing import Optional
|
|
16
|
+
|
|
17
|
+
from AutoGLM_GUI.logger import logger
|
|
18
|
+
from AutoGLM_GUI.models.device_group import (
|
|
19
|
+
DEFAULT_GROUP_ID,
|
|
20
|
+
DeviceGroup,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class DeviceGroupManager:
|
|
25
|
+
"""设备分组管理器(单例模式)."""
|
|
26
|
+
|
|
27
|
+
_instance: Optional["DeviceGroupManager"] = None
|
|
28
|
+
|
|
29
|
+
def __new__(cls):
|
|
30
|
+
"""单例模式:确保只有一个实例."""
|
|
31
|
+
if cls._instance is None:
|
|
32
|
+
cls._instance = super().__new__(cls)
|
|
33
|
+
return cls._instance
|
|
34
|
+
|
|
35
|
+
def __init__(self):
|
|
36
|
+
"""初始化管理器."""
|
|
37
|
+
if hasattr(self, "_initialized"):
|
|
38
|
+
return
|
|
39
|
+
self._initialized = True
|
|
40
|
+
self._groups_path = Path.home() / ".config" / "autoglm" / "device_groups.json"
|
|
41
|
+
self._lock = RLock()
|
|
42
|
+
|
|
43
|
+
# 缓存
|
|
44
|
+
self._groups_cache: Optional[list[DeviceGroup]] = None
|
|
45
|
+
self._assignments_cache: Optional[dict[str, str]] = None
|
|
46
|
+
self._file_mtime: Optional[float] = None
|
|
47
|
+
|
|
48
|
+
def list_groups(self) -> list[DeviceGroup]:
|
|
49
|
+
"""获取所有分组(按 order 排序).
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
list[DeviceGroup]: 分组列表
|
|
53
|
+
"""
|
|
54
|
+
with self._lock:
|
|
55
|
+
groups, _ = self._load_data()
|
|
56
|
+
return sorted(groups, key=lambda g: g.order)
|
|
57
|
+
|
|
58
|
+
def get_group(self, group_id: str) -> DeviceGroup | None:
|
|
59
|
+
"""根据 ID 获取单个分组.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
group_id: 分组 ID
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
DeviceGroup | None: 分组数据,如果不存在则返回 None
|
|
66
|
+
"""
|
|
67
|
+
with self._lock:
|
|
68
|
+
groups, _ = self._load_data()
|
|
69
|
+
return next((g for g in groups if g.id == group_id), None)
|
|
70
|
+
|
|
71
|
+
def create_group(self, name: str) -> DeviceGroup:
|
|
72
|
+
"""创建新分组.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
name: 分组名称
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
DeviceGroup: 新创建的分组
|
|
79
|
+
"""
|
|
80
|
+
with self._lock:
|
|
81
|
+
groups, assignments = self._load_data()
|
|
82
|
+
|
|
83
|
+
# 计算新的 order 值(放在最后)
|
|
84
|
+
max_order = max((g.order for g in groups), default=-1)
|
|
85
|
+
new_group = DeviceGroup(
|
|
86
|
+
name=name,
|
|
87
|
+
order=max_order + 1,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
groups.append(new_group)
|
|
91
|
+
self._save_data(groups, assignments)
|
|
92
|
+
logger.info(f"Created device group: {name} (id={new_group.id})")
|
|
93
|
+
return new_group
|
|
94
|
+
|
|
95
|
+
def update_group(self, group_id: str, name: str) -> DeviceGroup | None:
|
|
96
|
+
"""更新分组名称.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
group_id: 分组 ID
|
|
100
|
+
name: 新名称
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
DeviceGroup | None: 更新后的分组,如果不存在则返回 None
|
|
104
|
+
"""
|
|
105
|
+
with self._lock:
|
|
106
|
+
groups, assignments = self._load_data()
|
|
107
|
+
for group in groups:
|
|
108
|
+
if group.id == group_id:
|
|
109
|
+
group.name = name
|
|
110
|
+
group.updated_at = datetime.now()
|
|
111
|
+
self._save_data(groups, assignments)
|
|
112
|
+
logger.info(f"Updated device group: {name} (id={group_id})")
|
|
113
|
+
return group
|
|
114
|
+
logger.warning(f"Device group not found for update: id={group_id}")
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
def delete_group(self, group_id: str) -> bool:
|
|
118
|
+
"""删除分组(设备移回默认分组).
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
group_id: 分组 ID
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
bool: 删除成功返回 True,不存在或为默认分组返回 False
|
|
125
|
+
"""
|
|
126
|
+
if group_id == DEFAULT_GROUP_ID:
|
|
127
|
+
logger.warning("Cannot delete default group")
|
|
128
|
+
return False
|
|
129
|
+
|
|
130
|
+
with self._lock:
|
|
131
|
+
groups, assignments = self._load_data()
|
|
132
|
+
original_len = len(groups)
|
|
133
|
+
groups = [g for g in groups if g.id != group_id]
|
|
134
|
+
|
|
135
|
+
if len(groups) < original_len:
|
|
136
|
+
# 将该分组的设备移到默认分组
|
|
137
|
+
moved_count = 0
|
|
138
|
+
for serial, gid in list(assignments.items()):
|
|
139
|
+
if gid == group_id:
|
|
140
|
+
assignments[serial] = DEFAULT_GROUP_ID
|
|
141
|
+
moved_count += 1
|
|
142
|
+
|
|
143
|
+
self._save_data(groups, assignments)
|
|
144
|
+
logger.info(
|
|
145
|
+
f"Deleted device group: id={group_id}, "
|
|
146
|
+
f"moved {moved_count} device(s) to default group"
|
|
147
|
+
)
|
|
148
|
+
return True
|
|
149
|
+
|
|
150
|
+
logger.warning(f"Device group not found for deletion: id={group_id}")
|
|
151
|
+
return False
|
|
152
|
+
|
|
153
|
+
def reorder_groups(self, group_ids: list[str]) -> bool:
|
|
154
|
+
"""调整分组顺序.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
group_ids: 按新顺序排列的分组 ID 列表
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
bool: 调整成功返回 True
|
|
161
|
+
"""
|
|
162
|
+
with self._lock:
|
|
163
|
+
groups, assignments = self._load_data()
|
|
164
|
+
|
|
165
|
+
# 创建 ID 到 group 的映射
|
|
166
|
+
group_map = {g.id: g for g in groups}
|
|
167
|
+
|
|
168
|
+
# 验证所有 ID 都存在
|
|
169
|
+
for gid in group_ids:
|
|
170
|
+
if gid not in group_map:
|
|
171
|
+
logger.warning(f"Group not found for reorder: id={gid}")
|
|
172
|
+
return False
|
|
173
|
+
|
|
174
|
+
# 更新 order 值
|
|
175
|
+
for order, gid in enumerate(group_ids):
|
|
176
|
+
group_map[gid].order = order
|
|
177
|
+
group_map[gid].updated_at = datetime.now()
|
|
178
|
+
|
|
179
|
+
self._save_data(groups, assignments)
|
|
180
|
+
logger.info(f"Reordered {len(group_ids)} groups")
|
|
181
|
+
return True
|
|
182
|
+
|
|
183
|
+
def assign_device(self, serial: str, group_id: str) -> bool:
|
|
184
|
+
"""分配设备到分组.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
serial: 设备 serial
|
|
188
|
+
group_id: 目标分组 ID
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
bool: 分配成功返回 True
|
|
192
|
+
"""
|
|
193
|
+
with self._lock:
|
|
194
|
+
groups, assignments = self._load_data()
|
|
195
|
+
|
|
196
|
+
# 验证分组存在
|
|
197
|
+
if not any(g.id == group_id for g in groups):
|
|
198
|
+
logger.warning(f"Group not found for assignment: id={group_id}")
|
|
199
|
+
return False
|
|
200
|
+
|
|
201
|
+
old_group = assignments.get(serial, DEFAULT_GROUP_ID)
|
|
202
|
+
assignments[serial] = group_id
|
|
203
|
+
self._save_data(groups, assignments)
|
|
204
|
+
|
|
205
|
+
if old_group != group_id:
|
|
206
|
+
logger.info(
|
|
207
|
+
f"Assigned device {serial} to group {group_id} (was: {old_group})"
|
|
208
|
+
)
|
|
209
|
+
return True
|
|
210
|
+
|
|
211
|
+
def get_device_group(self, serial: str) -> str:
|
|
212
|
+
"""获取设备所属分组 ID.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
serial: 设备 serial
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
str: 分组 ID(未分配则返回默认分组 ID)
|
|
219
|
+
"""
|
|
220
|
+
with self._lock:
|
|
221
|
+
_, assignments = self._load_data()
|
|
222
|
+
return assignments.get(serial, DEFAULT_GROUP_ID)
|
|
223
|
+
|
|
224
|
+
def get_devices_in_group(self, group_id: str) -> list[str]:
|
|
225
|
+
"""获取分组内的设备 serial 列表.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
group_id: 分组 ID
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
list[str]: 设备 serial 列表
|
|
232
|
+
"""
|
|
233
|
+
with self._lock:
|
|
234
|
+
_, assignments = self._load_data()
|
|
235
|
+
|
|
236
|
+
# 对于默认分组,包含未显式分配的设备
|
|
237
|
+
if group_id == DEFAULT_GROUP_ID:
|
|
238
|
+
# 注意:这里只返回显式分配到默认分组的设备
|
|
239
|
+
# 如果需要包含所有未分配设备,需要配合 DeviceManager 使用
|
|
240
|
+
return [
|
|
241
|
+
serial
|
|
242
|
+
for serial, gid in assignments.items()
|
|
243
|
+
if gid == DEFAULT_GROUP_ID
|
|
244
|
+
]
|
|
245
|
+
|
|
246
|
+
return [serial for serial, gid in assignments.items() if gid == group_id]
|
|
247
|
+
|
|
248
|
+
def get_all_assignments(self) -> dict[str, str]:
|
|
249
|
+
"""获取所有设备分配信息.
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
dict[str, str]: serial -> group_id 的映射
|
|
253
|
+
"""
|
|
254
|
+
with self._lock:
|
|
255
|
+
_, assignments = self._load_data()
|
|
256
|
+
return assignments.copy()
|
|
257
|
+
|
|
258
|
+
def _load_data(self) -> tuple[list[DeviceGroup], dict[str, str]]:
|
|
259
|
+
"""从文件加载数据(带 mtime 缓存).
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
tuple[list[DeviceGroup], dict[str, str]]: (分组列表, 设备分配映射)
|
|
263
|
+
"""
|
|
264
|
+
# 检查文件是否存在
|
|
265
|
+
if not self._groups_path.exists():
|
|
266
|
+
# 创建默认分组
|
|
267
|
+
default_group = DeviceGroup.create_default_group()
|
|
268
|
+
groups = [default_group]
|
|
269
|
+
assignments: dict[str, str] = {}
|
|
270
|
+
self._save_data(groups, assignments)
|
|
271
|
+
return groups, assignments
|
|
272
|
+
|
|
273
|
+
# 检查缓存
|
|
274
|
+
current_mtime = self._groups_path.stat().st_mtime
|
|
275
|
+
if (
|
|
276
|
+
self._file_mtime == current_mtime
|
|
277
|
+
and self._groups_cache is not None
|
|
278
|
+
and self._assignments_cache is not None
|
|
279
|
+
):
|
|
280
|
+
return self._groups_cache.copy(), self._assignments_cache.copy()
|
|
281
|
+
|
|
282
|
+
# 重新加载
|
|
283
|
+
try:
|
|
284
|
+
with open(self._groups_path, encoding="utf-8") as f:
|
|
285
|
+
data = json.load(f)
|
|
286
|
+
|
|
287
|
+
groups_data = data.get("groups", [])
|
|
288
|
+
groups = [DeviceGroup.from_dict(g) for g in groups_data]
|
|
289
|
+
assignments = data.get("device_assignments", {})
|
|
290
|
+
|
|
291
|
+
# 确保默认分组存在
|
|
292
|
+
if not any(g.id == DEFAULT_GROUP_ID for g in groups):
|
|
293
|
+
default_group = DeviceGroup.create_default_group()
|
|
294
|
+
groups.insert(0, default_group)
|
|
295
|
+
self._save_data(groups, assignments)
|
|
296
|
+
|
|
297
|
+
self._groups_cache = groups
|
|
298
|
+
self._assignments_cache = assignments
|
|
299
|
+
self._file_mtime = current_mtime
|
|
300
|
+
logger.debug(
|
|
301
|
+
f"Loaded {len(groups)} groups, {len(assignments)} device assignments"
|
|
302
|
+
)
|
|
303
|
+
return groups.copy(), assignments.copy()
|
|
304
|
+
|
|
305
|
+
except (json.JSONDecodeError, FileNotFoundError) as e:
|
|
306
|
+
logger.warning(f"Failed to load device groups: {e}")
|
|
307
|
+
# 返回默认分组
|
|
308
|
+
default_group = DeviceGroup.create_default_group()
|
|
309
|
+
return [default_group], {}
|
|
310
|
+
|
|
311
|
+
def _save_data(
|
|
312
|
+
self, groups: list[DeviceGroup], assignments: dict[str, str]
|
|
313
|
+
) -> bool:
|
|
314
|
+
"""原子写入文件.
|
|
315
|
+
|
|
316
|
+
Args:
|
|
317
|
+
groups: 分组列表
|
|
318
|
+
assignments: 设备分配映射
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
bool: 保存成功返回 True
|
|
322
|
+
"""
|
|
323
|
+
self._groups_path.parent.mkdir(parents=True, exist_ok=True)
|
|
324
|
+
|
|
325
|
+
data = {
|
|
326
|
+
"groups": [g.to_dict() for g in groups],
|
|
327
|
+
"device_assignments": assignments,
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
# 原子写入:临时文件 + rename
|
|
331
|
+
temp_path = self._groups_path.with_suffix(".tmp")
|
|
332
|
+
try:
|
|
333
|
+
with open(temp_path, "w", encoding="utf-8") as f:
|
|
334
|
+
json.dump(data, f, indent=2, ensure_ascii=False)
|
|
335
|
+
temp_path.replace(self._groups_path)
|
|
336
|
+
|
|
337
|
+
# 更新缓存
|
|
338
|
+
self._groups_cache = groups.copy()
|
|
339
|
+
self._assignments_cache = assignments.copy()
|
|
340
|
+
self._file_mtime = self._groups_path.stat().st_mtime
|
|
341
|
+
logger.debug(
|
|
342
|
+
f"Saved {len(groups)} groups, {len(assignments)} device assignments"
|
|
343
|
+
)
|
|
344
|
+
return True
|
|
345
|
+
|
|
346
|
+
except Exception as e:
|
|
347
|
+
logger.error(f"Failed to save device groups: {e}")
|
|
348
|
+
if temp_path.exists():
|
|
349
|
+
temp_path.unlink()
|
|
350
|
+
return False
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
# 单例实例
|
|
354
|
+
device_group_manager = DeviceGroupManager()
|
AutoGLM_GUI/models/__init__.py
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
"""Data models for AutoGLM-GUI."""
|
|
2
2
|
|
|
3
|
+
from AutoGLM_GUI.models.device_group import (
|
|
4
|
+
DEFAULT_GROUP_ID,
|
|
5
|
+
DEFAULT_GROUP_NAME,
|
|
6
|
+
DeviceGroup,
|
|
7
|
+
)
|
|
3
8
|
from AutoGLM_GUI.models.history import ConversationRecord, DeviceHistory
|
|
4
9
|
from AutoGLM_GUI.models.scheduled_task import ScheduledTask
|
|
5
10
|
|
|
6
11
|
__all__ = [
|
|
7
12
|
"ConversationRecord",
|
|
8
13
|
"DeviceHistory",
|
|
14
|
+
"DeviceGroup",
|
|
15
|
+
"DEFAULT_GROUP_ID",
|
|
16
|
+
"DEFAULT_GROUP_NAME",
|
|
9
17
|
"ScheduledTask",
|
|
10
18
|
]
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Device group data models."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from uuid import uuid4
|
|
6
|
+
|
|
7
|
+
# Default group ID - this group cannot be deleted
|
|
8
|
+
DEFAULT_GROUP_ID = "default"
|
|
9
|
+
DEFAULT_GROUP_NAME = "默认分组"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class DeviceGroup:
|
|
14
|
+
"""设备分组定义."""
|
|
15
|
+
|
|
16
|
+
id: str = field(default_factory=lambda: str(uuid4()))
|
|
17
|
+
|
|
18
|
+
# 基础信息
|
|
19
|
+
name: str = "" # 分组名称
|
|
20
|
+
order: int = 0 # 排序顺序(数字越小越靠前)
|
|
21
|
+
|
|
22
|
+
# 元数据
|
|
23
|
+
created_at: datetime = field(default_factory=datetime.now)
|
|
24
|
+
updated_at: datetime = field(default_factory=datetime.now)
|
|
25
|
+
|
|
26
|
+
def to_dict(self) -> dict:
|
|
27
|
+
"""转换为可序列化的字典."""
|
|
28
|
+
return {
|
|
29
|
+
"id": self.id,
|
|
30
|
+
"name": self.name,
|
|
31
|
+
"order": self.order,
|
|
32
|
+
"created_at": self.created_at.isoformat(),
|
|
33
|
+
"updated_at": self.updated_at.isoformat(),
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def from_dict(cls, data: dict) -> "DeviceGroup":
|
|
38
|
+
"""从字典创建实例."""
|
|
39
|
+
return cls(
|
|
40
|
+
id=data.get("id", str(uuid4())),
|
|
41
|
+
name=data.get("name", ""),
|
|
42
|
+
order=data.get("order", 0),
|
|
43
|
+
created_at=datetime.fromisoformat(data["created_at"])
|
|
44
|
+
if data.get("created_at")
|
|
45
|
+
else datetime.now(),
|
|
46
|
+
updated_at=datetime.fromisoformat(data["updated_at"])
|
|
47
|
+
if data.get("updated_at")
|
|
48
|
+
else datetime.now(),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def create_default_group(cls) -> "DeviceGroup":
|
|
53
|
+
"""创建默认分组."""
|
|
54
|
+
return cls(
|
|
55
|
+
id=DEFAULT_GROUP_ID,
|
|
56
|
+
name=DEFAULT_GROUP_NAME,
|
|
57
|
+
order=0,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def is_default(self) -> bool:
|
|
62
|
+
"""是否为默认分组."""
|
|
63
|
+
return self.id == DEFAULT_GROUP_ID
|
|
@@ -5,6 +5,25 @@ from datetime import datetime
|
|
|
5
5
|
from uuid import uuid4
|
|
6
6
|
|
|
7
7
|
|
|
8
|
+
def _normalize_device_serialnos(serialnos: object) -> list[str]:
|
|
9
|
+
if isinstance(serialnos, str):
|
|
10
|
+
serialnos = [serialnos]
|
|
11
|
+
if not isinstance(serialnos, list):
|
|
12
|
+
return []
|
|
13
|
+
|
|
14
|
+
normalized: list[str] = []
|
|
15
|
+
seen: set[str] = set()
|
|
16
|
+
for raw in serialnos:
|
|
17
|
+
if not isinstance(raw, str):
|
|
18
|
+
continue
|
|
19
|
+
s = raw.strip()
|
|
20
|
+
if not s or s in seen:
|
|
21
|
+
continue
|
|
22
|
+
normalized.append(s)
|
|
23
|
+
seen.add(s)
|
|
24
|
+
return normalized
|
|
25
|
+
|
|
26
|
+
|
|
8
27
|
@dataclass
|
|
9
28
|
class ScheduledTask:
|
|
10
29
|
"""定时任务定义."""
|
|
@@ -14,7 +33,12 @@ class ScheduledTask:
|
|
|
14
33
|
# 基础信息
|
|
15
34
|
name: str = "" # 任务名称
|
|
16
35
|
workflow_uuid: str = "" # 关联的 Workflow UUID
|
|
17
|
-
|
|
36
|
+
device_serialnos: list[str] = field(
|
|
37
|
+
default_factory=list
|
|
38
|
+
) # 绑定的设备 serialno 列表
|
|
39
|
+
device_group_id: str | None = (
|
|
40
|
+
None # 绑定的设备分组 ID(与 device_serialnos 二选一)
|
|
41
|
+
)
|
|
18
42
|
|
|
19
43
|
# 调度配置
|
|
20
44
|
cron_expression: str = "" # Cron 表达式 (如 "0 8 * * *")
|
|
@@ -27,6 +51,10 @@ class ScheduledTask:
|
|
|
27
51
|
# 最近执行信息(只记录最后一次)
|
|
28
52
|
last_run_time: datetime | None = None
|
|
29
53
|
last_run_success: bool | None = None
|
|
54
|
+
# success: 全部设备成功;partial: 部分成功;failure: 全部失败
|
|
55
|
+
last_run_status: str | None = None
|
|
56
|
+
last_run_success_count: int | None = None
|
|
57
|
+
last_run_total_count: int | None = None
|
|
30
58
|
last_run_message: str | None = None
|
|
31
59
|
|
|
32
60
|
def to_dict(self) -> dict:
|
|
@@ -35,7 +63,8 @@ class ScheduledTask:
|
|
|
35
63
|
"id": self.id,
|
|
36
64
|
"name": self.name,
|
|
37
65
|
"workflow_uuid": self.workflow_uuid,
|
|
38
|
-
"
|
|
66
|
+
"device_serialnos": self.device_serialnos,
|
|
67
|
+
"device_group_id": self.device_group_id,
|
|
39
68
|
"cron_expression": self.cron_expression,
|
|
40
69
|
"enabled": self.enabled,
|
|
41
70
|
"created_at": self.created_at.isoformat(),
|
|
@@ -44,17 +73,28 @@ class ScheduledTask:
|
|
|
44
73
|
if self.last_run_time
|
|
45
74
|
else None,
|
|
46
75
|
"last_run_success": self.last_run_success,
|
|
76
|
+
"last_run_status": self.last_run_status,
|
|
77
|
+
"last_run_success_count": self.last_run_success_count,
|
|
78
|
+
"last_run_total_count": self.last_run_total_count,
|
|
47
79
|
"last_run_message": self.last_run_message,
|
|
48
80
|
}
|
|
49
81
|
|
|
50
82
|
@classmethod
|
|
51
83
|
def from_dict(cls, data: dict) -> "ScheduledTask":
|
|
52
|
-
"""
|
|
84
|
+
"""从字典创建实例,向后兼容旧数据格式."""
|
|
85
|
+
# 处理设备序列号:支持旧格式的单字符串和新格式的列表
|
|
86
|
+
device_serialnos = _normalize_device_serialnos(data.get("device_serialnos", []))
|
|
87
|
+
if not device_serialnos:
|
|
88
|
+
# 向后兼容:尝试读取旧字段 device_serialno
|
|
89
|
+
old_device = _normalize_device_serialnos(data.get("device_serialno", ""))
|
|
90
|
+
device_serialnos = old_device
|
|
91
|
+
|
|
53
92
|
return cls(
|
|
54
93
|
id=data.get("id", str(uuid4())),
|
|
55
94
|
name=data.get("name", ""),
|
|
56
95
|
workflow_uuid=data.get("workflow_uuid", ""),
|
|
57
|
-
|
|
96
|
+
device_serialnos=device_serialnos,
|
|
97
|
+
device_group_id=data.get("device_group_id"),
|
|
58
98
|
cron_expression=data.get("cron_expression", ""),
|
|
59
99
|
enabled=data.get("enabled", True),
|
|
60
100
|
created_at=datetime.fromisoformat(data["created_at"])
|
|
@@ -67,5 +107,8 @@ class ScheduledTask:
|
|
|
67
107
|
if data.get("last_run_time")
|
|
68
108
|
else None,
|
|
69
109
|
last_run_success=data.get("last_run_success"),
|
|
110
|
+
last_run_status=data.get("last_run_status"),
|
|
111
|
+
last_run_success_count=data.get("last_run_success_count"),
|
|
112
|
+
last_run_total_count=data.get("last_run_total_count"),
|
|
70
113
|
last_run_message=data.get("last_run_message"),
|
|
71
114
|
)
|