autoglm-gui 1.5.4__py3-none-any.whl → 1.5.6__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/agents/glm/async_agent.py +3 -50
- AutoGLM_GUI/agents/protocols.py +3 -14
- 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-BZglkj97.js → about-DVviVdH2.js} +1 -1
- AutoGLM_GUI/static/assets/{alert-dialog-5vNoxwIO.js → alert-dialog-IHmO2JCQ.js} +1 -1
- AutoGLM_GUI/static/assets/chat-C_3D0Ao7.js +134 -0
- AutoGLM_GUI/static/assets/{dialog-DSAhQHru.js → dialog-DOpd71Lu.js} +3 -3
- AutoGLM_GUI/static/assets/{eye-Deqw6dbm.js → eye-CZP5ZJ_Y.js} +1 -1
- AutoGLM_GUI/static/assets/folder-open-_KlT8ZW7.js +1 -0
- AutoGLM_GUI/static/assets/{history-CL-JjUbk.js → history-BXMlCwUV.js} +1 -1
- AutoGLM_GUI/static/assets/index-84TrNz7w.css +1 -0
- AutoGLM_GUI/static/assets/index-Bh2f556h.js +1 -0
- AutoGLM_GUI/static/assets/index-DUNWZsFq.js +11 -0
- AutoGLM_GUI/static/assets/{label-CEmK7RW4.js → label-CRZhpiYG.js} +1 -1
- AutoGLM_GUI/static/assets/logs-DEN9nDRS.js +1 -0
- AutoGLM_GUI/static/assets/popover-DitUZhUk.js +1 -0
- AutoGLM_GUI/static/assets/scheduled-tasks-boxDKe87.js +1 -0
- AutoGLM_GUI/static/assets/{textarea-nLU4tGQH.js → textarea-CvRHzjfV.js} +1 -1
- AutoGLM_GUI/static/assets/{workflows-QIA3_mdp.js → workflows-Bg3qN-6j.js} +1 -1
- AutoGLM_GUI/static/index.html +2 -2
- {autoglm_gui-1.5.4.dist-info → autoglm_gui-1.5.6.dist-info}/METADATA +361 -10
- {autoglm_gui-1.5.4.dist-info → autoglm_gui-1.5.6.dist-info}/RECORD +33 -31
- AutoGLM_GUI/static/assets/chat-ta_RqZfZ.js +0 -129
- AutoGLM_GUI/static/assets/circle-alert-CnwO7Du-.js +0 -1
- AutoGLM_GUI/static/assets/index-BjaUZM-7.js +0 -1
- AutoGLM_GUI/static/assets/index-CX4NAYCk.js +0 -11
- AutoGLM_GUI/static/assets/index-DSIMVL8V.css +0 -1
- AutoGLM_GUI/static/assets/logs-C-Pnb4jI.js +0 -1
- AutoGLM_GUI/static/assets/popover-CauTjrhB.js +0 -1
- AutoGLM_GUI/static/assets/scheduled-tasks-Ds1WrRVN.js +0 -1
- {autoglm_gui-1.5.4.dist-info → autoglm_gui-1.5.6.dist-info}/WHEEL +0 -0
- {autoglm_gui-1.5.4.dist-info → autoglm_gui-1.5.6.dist-info}/entry_points.txt +0 -0
- {autoglm_gui-1.5.4.dist-info → autoglm_gui-1.5.6.dist-info}/licenses/LICENSE +0 -0
AutoGLM_GUI/scheduler_manager.py
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"""Scheduled task manager with APScheduler."""
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
3
4
|
import json
|
|
5
|
+
from dataclasses import dataclass
|
|
4
6
|
from datetime import datetime
|
|
5
7
|
from pathlib import Path
|
|
6
|
-
from typing import Optional
|
|
8
|
+
from typing import TYPE_CHECKING, Any, Optional
|
|
7
9
|
|
|
8
10
|
from apscheduler.schedulers.background import BackgroundScheduler
|
|
9
11
|
from apscheduler.triggers.cron import CronTrigger
|
|
@@ -11,6 +13,17 @@ from apscheduler.triggers.cron import CronTrigger
|
|
|
11
13
|
from AutoGLM_GUI.logger import logger
|
|
12
14
|
from AutoGLM_GUI.models.scheduled_task import ScheduledTask
|
|
13
15
|
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from AutoGLM_GUI.models.history import MessageRecord
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class DeviceExecutionResult:
|
|
22
|
+
serialno: str
|
|
23
|
+
success: bool
|
|
24
|
+
message: str
|
|
25
|
+
device_model: str = ""
|
|
26
|
+
|
|
14
27
|
|
|
15
28
|
class SchedulerManager:
|
|
16
29
|
_instance: Optional["SchedulerManager"] = None
|
|
@@ -45,14 +58,16 @@ class SchedulerManager:
|
|
|
45
58
|
self,
|
|
46
59
|
name: str,
|
|
47
60
|
workflow_uuid: str,
|
|
48
|
-
|
|
61
|
+
device_serialnos: list[str] | None,
|
|
49
62
|
cron_expression: str,
|
|
50
63
|
enabled: bool = True,
|
|
64
|
+
device_group_id: str | None = None,
|
|
51
65
|
) -> ScheduledTask:
|
|
52
66
|
task = ScheduledTask(
|
|
53
67
|
name=name,
|
|
54
68
|
workflow_uuid=workflow_uuid,
|
|
55
|
-
|
|
69
|
+
device_serialnos=device_serialnos or [],
|
|
70
|
+
device_group_id=device_group_id,
|
|
56
71
|
cron_expression=cron_expression,
|
|
57
72
|
enabled=enabled,
|
|
58
73
|
)
|
|
@@ -167,37 +182,31 @@ class SchedulerManager:
|
|
|
167
182
|
except Exception as e:
|
|
168
183
|
logger.warning(f"Failed to remove job {task_id}: {e}")
|
|
169
184
|
|
|
170
|
-
def
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
from AutoGLM_GUI.history_manager import history_manager
|
|
185
|
+
def _execute_single_device(
|
|
186
|
+
self,
|
|
187
|
+
serialno: str,
|
|
188
|
+
workflow: dict[str, Any],
|
|
189
|
+
task_name: str,
|
|
190
|
+
manager: Any,
|
|
191
|
+
device_manager: Any,
|
|
192
|
+
history_manager: Any,
|
|
193
|
+
) -> DeviceExecutionResult:
|
|
180
194
|
from AutoGLM_GUI.models.history import ConversationRecord, MessageRecord
|
|
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
195
|
|
|
189
|
-
device_manager = DeviceManager.get_instance()
|
|
190
196
|
device = None
|
|
191
197
|
for d in device_manager.get_devices():
|
|
192
|
-
if d.serial ==
|
|
198
|
+
if d.serial == serialno and d.state.value == "online":
|
|
193
199
|
device = d
|
|
194
200
|
break
|
|
195
201
|
|
|
196
202
|
if not device:
|
|
197
|
-
|
|
198
|
-
|
|
203
|
+
return DeviceExecutionResult(
|
|
204
|
+
serialno=serialno,
|
|
205
|
+
success=False,
|
|
206
|
+
message="Device offline",
|
|
207
|
+
device_model="",
|
|
208
|
+
)
|
|
199
209
|
|
|
200
|
-
manager = PhoneAgentManager.get_instance()
|
|
201
210
|
acquired = manager.acquire_device(
|
|
202
211
|
device.primary_device_id,
|
|
203
212
|
timeout=0,
|
|
@@ -206,56 +215,85 @@ class SchedulerManager:
|
|
|
206
215
|
)
|
|
207
216
|
|
|
208
217
|
if not acquired:
|
|
209
|
-
|
|
210
|
-
|
|
218
|
+
return DeviceExecutionResult(
|
|
219
|
+
serialno=serialno,
|
|
220
|
+
success=False,
|
|
221
|
+
message="Device busy",
|
|
222
|
+
device_model=device.model or serialno,
|
|
223
|
+
)
|
|
211
224
|
|
|
212
225
|
start_time = datetime.now()
|
|
213
|
-
|
|
214
|
-
# 收集完整对话消息
|
|
215
|
-
messages: list[MessageRecord] = []
|
|
216
|
-
messages.append(
|
|
226
|
+
messages: list["MessageRecord"] = [
|
|
217
227
|
MessageRecord(
|
|
218
228
|
role="user",
|
|
219
229
|
content=workflow["text"],
|
|
220
230
|
timestamp=start_time,
|
|
221
231
|
)
|
|
222
|
-
|
|
232
|
+
]
|
|
233
|
+
|
|
234
|
+
result_message = ""
|
|
235
|
+
task_success = False
|
|
223
236
|
|
|
224
237
|
try:
|
|
225
|
-
|
|
226
|
-
agent.reset()
|
|
238
|
+
from AutoGLM_GUI.agents.protocols import is_async_agent
|
|
227
239
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
result_message = ""
|
|
231
|
-
task_success = False
|
|
232
|
-
|
|
233
|
-
while agent.step_count < agent.agent_config.max_steps:
|
|
234
|
-
step_result = agent.step(workflow["text"] if is_first else None) # type: ignore[misc]
|
|
235
|
-
is_first = False
|
|
236
|
-
|
|
237
|
-
# 收集每个 step 的消息
|
|
238
|
-
messages.append(
|
|
239
|
-
MessageRecord(
|
|
240
|
-
role="assistant",
|
|
241
|
-
content="",
|
|
242
|
-
timestamp=datetime.now(),
|
|
243
|
-
thinking=step_result.thinking, # type: ignore[union-attr]
|
|
244
|
-
action=step_result.action, # type: ignore[union-attr]
|
|
245
|
-
step=agent.step_count,
|
|
246
|
-
)
|
|
247
|
-
)
|
|
240
|
+
agent: Any = manager.get_agent(device.primary_device_id)
|
|
241
|
+
agent.reset()
|
|
248
242
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
243
|
+
if is_async_agent(agent):
|
|
244
|
+
|
|
245
|
+
async def run_async():
|
|
246
|
+
nonlocal result_message, task_success
|
|
247
|
+
stream_gen = agent.stream(workflow["text"])
|
|
248
|
+
async for event in stream_gen:
|
|
249
|
+
step_data: dict[str, Any] = event.get("data", {})
|
|
250
|
+
if event["type"] == "step":
|
|
251
|
+
messages.append(
|
|
252
|
+
MessageRecord(
|
|
253
|
+
role="assistant",
|
|
254
|
+
content="",
|
|
255
|
+
timestamp=datetime.now(),
|
|
256
|
+
thinking=step_data.get("thinking", ""),
|
|
257
|
+
action=step_data.get("action", {}),
|
|
258
|
+
step=step_data.get("step", 0),
|
|
259
|
+
)
|
|
260
|
+
)
|
|
261
|
+
elif event["type"] == "done":
|
|
262
|
+
result_message = step_data.get("message", "Task completed")
|
|
263
|
+
task_success = step_data.get("success", False)
|
|
264
|
+
break
|
|
265
|
+
elif event["type"] == "error":
|
|
266
|
+
result_message = step_data.get("message", "Task failed")
|
|
267
|
+
task_success = False
|
|
268
|
+
break
|
|
269
|
+
|
|
270
|
+
asyncio.run(run_async())
|
|
253
271
|
else:
|
|
254
|
-
|
|
255
|
-
|
|
272
|
+
is_first = True
|
|
273
|
+
while agent.step_count < agent.agent_config.max_steps:
|
|
274
|
+
step_result = agent.step(workflow["text"] if is_first else None)
|
|
275
|
+
is_first = False
|
|
276
|
+
messages.append(
|
|
277
|
+
MessageRecord(
|
|
278
|
+
role="assistant",
|
|
279
|
+
content="",
|
|
280
|
+
timestamp=datetime.now(),
|
|
281
|
+
thinking=step_result.thinking,
|
|
282
|
+
action=step_result.action,
|
|
283
|
+
step=agent.step_count,
|
|
284
|
+
)
|
|
285
|
+
)
|
|
286
|
+
if step_result.finished:
|
|
287
|
+
result_message = step_result.message or "Task completed"
|
|
288
|
+
task_success = step_result.success
|
|
289
|
+
break
|
|
290
|
+
else:
|
|
291
|
+
result_message = "Max steps reached"
|
|
292
|
+
task_success = False
|
|
256
293
|
|
|
257
294
|
steps = agent.step_count
|
|
258
295
|
end_time = datetime.now()
|
|
296
|
+
device_model = device.model or serialno
|
|
259
297
|
|
|
260
298
|
record = ConversationRecord(
|
|
261
299
|
task_text=workflow["text"],
|
|
@@ -266,21 +304,23 @@ class SchedulerManager:
|
|
|
266
304
|
end_time=end_time,
|
|
267
305
|
duration_ms=int((end_time - start_time).total_seconds() * 1000),
|
|
268
306
|
source="scheduled",
|
|
269
|
-
source_detail=
|
|
307
|
+
source_detail=f"{task_name} [{device_model}]",
|
|
270
308
|
error_message=None if task_success else result_message,
|
|
271
309
|
messages=messages,
|
|
272
310
|
)
|
|
273
|
-
history_manager.add_record(
|
|
311
|
+
history_manager.add_record(serialno, record)
|
|
274
312
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
313
|
+
return DeviceExecutionResult(
|
|
314
|
+
serialno=serialno,
|
|
315
|
+
success=task_success,
|
|
316
|
+
message=result_message,
|
|
317
|
+
device_model=device_model,
|
|
318
|
+
)
|
|
279
319
|
|
|
280
320
|
except Exception as e:
|
|
281
321
|
end_time = datetime.now()
|
|
282
322
|
error_msg = str(e)
|
|
283
|
-
|
|
323
|
+
device_model = device.model or serialno
|
|
284
324
|
|
|
285
325
|
record = ConversationRecord(
|
|
286
326
|
task_text=workflow["text"],
|
|
@@ -291,30 +331,165 @@ class SchedulerManager:
|
|
|
291
331
|
end_time=end_time,
|
|
292
332
|
duration_ms=int((end_time - start_time).total_seconds() * 1000),
|
|
293
333
|
source="scheduled",
|
|
294
|
-
source_detail=
|
|
334
|
+
source_detail=f"{task_name} [{device_model}]",
|
|
295
335
|
error_message=error_msg,
|
|
296
336
|
messages=messages,
|
|
297
337
|
)
|
|
298
|
-
history_manager.add_record(
|
|
338
|
+
history_manager.add_record(serialno, record)
|
|
299
339
|
|
|
300
|
-
|
|
340
|
+
return DeviceExecutionResult(
|
|
341
|
+
serialno=serialno,
|
|
342
|
+
success=False,
|
|
343
|
+
message=error_msg,
|
|
344
|
+
device_model=device_model,
|
|
345
|
+
)
|
|
301
346
|
|
|
302
347
|
finally:
|
|
303
348
|
manager.release_device(device.primary_device_id)
|
|
304
349
|
|
|
305
|
-
def
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
350
|
+
def _resolve_device_serialnos(self, task: ScheduledTask) -> list[str]:
|
|
351
|
+
"""解析任务的目标设备列表.
|
|
352
|
+
|
|
353
|
+
如果指定了 device_group_id,则从分组获取设备列表;
|
|
354
|
+
否则使用 device_serialnos 字段。
|
|
355
|
+
"""
|
|
356
|
+
if task.device_group_id:
|
|
357
|
+
from AutoGLM_GUI.device_group_manager import device_group_manager
|
|
358
|
+
from AutoGLM_GUI.device_manager import DeviceManager
|
|
359
|
+
|
|
360
|
+
device_manager = DeviceManager.get_instance()
|
|
361
|
+
|
|
362
|
+
# 获取分组内的所有设备
|
|
363
|
+
if task.device_group_id == "default":
|
|
364
|
+
# 默认分组:获取所有未分配到其他分组的设备
|
|
365
|
+
assignments = device_group_manager.get_all_assignments()
|
|
366
|
+
assigned_serials = {
|
|
367
|
+
s for s, gid in assignments.items() if gid != "default"
|
|
368
|
+
}
|
|
369
|
+
managed_devices = device_manager.get_devices()
|
|
370
|
+
return [
|
|
371
|
+
d.serial
|
|
372
|
+
for d in managed_devices
|
|
373
|
+
if d.serial not in assigned_serials
|
|
374
|
+
]
|
|
375
|
+
else:
|
|
376
|
+
# 其他分组:从分配中获取
|
|
377
|
+
return device_group_manager.get_devices_in_group(task.device_group_id)
|
|
378
|
+
else:
|
|
379
|
+
return task.device_serialnos
|
|
311
380
|
|
|
312
|
-
def
|
|
381
|
+
def _execute_task(self, task_id: str) -> None:
|
|
382
|
+
task = self._tasks.get(task_id)
|
|
383
|
+
if not task:
|
|
384
|
+
logger.warning(f"Task {task_id} not found for execution")
|
|
385
|
+
return
|
|
386
|
+
|
|
387
|
+
# 解析目标设备列表
|
|
388
|
+
device_serialnos = self._resolve_device_serialnos(task)
|
|
389
|
+
|
|
390
|
+
logger.info(
|
|
391
|
+
f"Executing scheduled task: {task.name} on {len(device_serialnos)} device(s)"
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
from AutoGLM_GUI.device_manager import DeviceManager
|
|
395
|
+
from AutoGLM_GUI.history_manager import history_manager
|
|
396
|
+
from AutoGLM_GUI.phone_agent_manager import PhoneAgentManager
|
|
397
|
+
from AutoGLM_GUI.workflow_manager import workflow_manager
|
|
398
|
+
|
|
399
|
+
workflow = workflow_manager.get_workflow(task.workflow_uuid)
|
|
400
|
+
if not workflow:
|
|
401
|
+
self._record_run(
|
|
402
|
+
task=task,
|
|
403
|
+
status="failure",
|
|
404
|
+
message="Workflow not found",
|
|
405
|
+
success_count=0,
|
|
406
|
+
total_count=len(device_serialnos),
|
|
407
|
+
)
|
|
408
|
+
return
|
|
409
|
+
|
|
410
|
+
device_manager = DeviceManager.get_instance()
|
|
411
|
+
manager = PhoneAgentManager.get_instance()
|
|
412
|
+
|
|
413
|
+
total_count = len(device_serialnos)
|
|
414
|
+
if total_count == 0:
|
|
415
|
+
self._record_run(
|
|
416
|
+
task=task,
|
|
417
|
+
status="failure",
|
|
418
|
+
message="No devices selected",
|
|
419
|
+
success_count=0,
|
|
420
|
+
total_count=0,
|
|
421
|
+
)
|
|
422
|
+
return
|
|
423
|
+
|
|
424
|
+
results: list[DeviceExecutionResult] = []
|
|
425
|
+
for serialno in device_serialnos:
|
|
426
|
+
result = self._execute_single_device(
|
|
427
|
+
serialno=serialno,
|
|
428
|
+
workflow=workflow,
|
|
429
|
+
task_name=task.name,
|
|
430
|
+
manager=manager,
|
|
431
|
+
device_manager=device_manager,
|
|
432
|
+
history_manager=history_manager,
|
|
433
|
+
)
|
|
434
|
+
results.append(result)
|
|
435
|
+
status_icon = "✓" if result.success else "✗"
|
|
436
|
+
logger.info(
|
|
437
|
+
f" {status_icon} {result.device_model or result.serialno}: {result.message[:50]}"
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
success_count = sum(1 for r in results if r.success)
|
|
441
|
+
any_success = success_count > 0
|
|
442
|
+
all_success = success_count == total_count
|
|
443
|
+
|
|
444
|
+
summary_parts = []
|
|
445
|
+
for r in results:
|
|
446
|
+
status = "✓" if r.success else "✗"
|
|
447
|
+
short_serial = r.serialno[:8] + "..." if len(r.serialno) > 8 else r.serialno
|
|
448
|
+
display_name = r.device_model or short_serial
|
|
449
|
+
summary_parts.append(f"{status} {display_name}: {r.message[:30]}")
|
|
450
|
+
summary_message = " | ".join(summary_parts)
|
|
451
|
+
|
|
452
|
+
logger.info(
|
|
453
|
+
f"Task {task.name} completed: {success_count}/{total_count} devices succeeded"
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
status: str
|
|
457
|
+
if all_success:
|
|
458
|
+
status = "success"
|
|
459
|
+
elif any_success:
|
|
460
|
+
status = "partial"
|
|
461
|
+
else:
|
|
462
|
+
status = "failure"
|
|
463
|
+
|
|
464
|
+
self._record_run(
|
|
465
|
+
task=task,
|
|
466
|
+
status=status,
|
|
467
|
+
message=summary_message,
|
|
468
|
+
success_count=success_count,
|
|
469
|
+
total_count=total_count,
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
def _record_run(
|
|
473
|
+
self,
|
|
474
|
+
task: ScheduledTask,
|
|
475
|
+
status: str,
|
|
476
|
+
message: str,
|
|
477
|
+
success_count: int,
|
|
478
|
+
total_count: int,
|
|
479
|
+
) -> None:
|
|
313
480
|
task.last_run_time = datetime.now()
|
|
314
|
-
task.
|
|
315
|
-
task.
|
|
481
|
+
task.last_run_status = status
|
|
482
|
+
task.last_run_success = status == "success"
|
|
483
|
+
task.last_run_success_count = success_count
|
|
484
|
+
task.last_run_total_count = total_count
|
|
485
|
+
task.last_run_message = message[:500] if message else ""
|
|
316
486
|
self._save_tasks()
|
|
317
|
-
|
|
487
|
+
if status == "success":
|
|
488
|
+
logger.info(f"Scheduled task completed: {task.name}")
|
|
489
|
+
elif status == "partial":
|
|
490
|
+
logger.warning(f"Scheduled task partially succeeded: {task.name}")
|
|
491
|
+
else:
|
|
492
|
+
logger.warning(f"Scheduled task failed: {task.name} - {message}")
|
|
318
493
|
|
|
319
494
|
def _load_tasks(self) -> None:
|
|
320
495
|
if not self._tasks_path.exists():
|
AutoGLM_GUI/schemas.py
CHANGED
|
@@ -277,6 +277,7 @@ class DeviceResponse(BaseModel):
|
|
|
277
277
|
state: str
|
|
278
278
|
is_available_only: bool
|
|
279
279
|
display_name: str | None = None
|
|
280
|
+
group_id: str = "default" # 所属分组 ID
|
|
280
281
|
agent: AgentStatusResponse | None = None
|
|
281
282
|
|
|
282
283
|
|
|
@@ -765,7 +766,8 @@ class ScheduledTaskCreate(BaseModel):
|
|
|
765
766
|
|
|
766
767
|
name: str
|
|
767
768
|
workflow_uuid: str
|
|
768
|
-
|
|
769
|
+
device_serialnos: list[str] | None = None # 直接指定设备列表
|
|
770
|
+
device_group_id: str | None = None # 或指定设备分组
|
|
769
771
|
cron_expression: str
|
|
770
772
|
enabled: bool = True
|
|
771
773
|
|
|
@@ -776,6 +778,21 @@ class ScheduledTaskCreate(BaseModel):
|
|
|
776
778
|
raise ValueError("name cannot be empty")
|
|
777
779
|
return v.strip()
|
|
778
780
|
|
|
781
|
+
@field_validator("device_serialnos")
|
|
782
|
+
@classmethod
|
|
783
|
+
def validate_devices(cls, v: list[str] | None) -> list[str] | None:
|
|
784
|
+
if v is None:
|
|
785
|
+
return None
|
|
786
|
+
normalized: list[str] = []
|
|
787
|
+
seen: set[str] = set()
|
|
788
|
+
for raw in v or []:
|
|
789
|
+
s = raw.strip()
|
|
790
|
+
if not s or s in seen:
|
|
791
|
+
continue
|
|
792
|
+
normalized.append(s)
|
|
793
|
+
seen.add(s)
|
|
794
|
+
return normalized if normalized else None
|
|
795
|
+
|
|
779
796
|
@field_validator("cron_expression")
|
|
780
797
|
@classmethod
|
|
781
798
|
def validate_cron(cls, v: str) -> str:
|
|
@@ -788,16 +805,41 @@ class ScheduledTaskCreate(BaseModel):
|
|
|
788
805
|
)
|
|
789
806
|
return v.strip()
|
|
790
807
|
|
|
808
|
+
def model_post_init(self, __context) -> None:
|
|
809
|
+
"""验证必须指定 device_serialnos 或 device_group_id 之一."""
|
|
810
|
+
if not self.device_serialnos and not self.device_group_id:
|
|
811
|
+
raise ValueError(
|
|
812
|
+
"either device_serialnos or device_group_id must be specified"
|
|
813
|
+
)
|
|
814
|
+
|
|
791
815
|
|
|
792
816
|
class ScheduledTaskUpdate(BaseModel):
|
|
793
817
|
"""更新定时任务请求."""
|
|
794
818
|
|
|
795
819
|
name: str | None = None
|
|
796
820
|
workflow_uuid: str | None = None
|
|
797
|
-
|
|
821
|
+
device_serialnos: list[str] | None = None
|
|
822
|
+
device_group_id: str | None = None
|
|
798
823
|
cron_expression: str | None = None
|
|
799
824
|
enabled: bool | None = None
|
|
800
825
|
|
|
826
|
+
@field_validator("device_serialnos")
|
|
827
|
+
@classmethod
|
|
828
|
+
def validate_devices(cls, v: list[str] | None) -> list[str] | None:
|
|
829
|
+
if v is None:
|
|
830
|
+
return None
|
|
831
|
+
|
|
832
|
+
normalized: list[str] = []
|
|
833
|
+
seen: set[str] = set()
|
|
834
|
+
for raw in v:
|
|
835
|
+
s = raw.strip()
|
|
836
|
+
if not s or s in seen:
|
|
837
|
+
continue
|
|
838
|
+
normalized.append(s)
|
|
839
|
+
seen.add(s)
|
|
840
|
+
|
|
841
|
+
return normalized if normalized else None
|
|
842
|
+
|
|
801
843
|
@field_validator("cron_expression")
|
|
802
844
|
@classmethod
|
|
803
845
|
def validate_cron(cls, v: str | None) -> str | None:
|
|
@@ -819,13 +861,17 @@ class ScheduledTaskResponse(BaseModel):
|
|
|
819
861
|
id: str
|
|
820
862
|
name: str
|
|
821
863
|
workflow_uuid: str
|
|
822
|
-
|
|
864
|
+
device_serialnos: list[str]
|
|
865
|
+
device_group_id: str | None = None
|
|
823
866
|
cron_expression: str
|
|
824
867
|
enabled: bool
|
|
825
868
|
created_at: str
|
|
826
869
|
updated_at: str
|
|
827
870
|
last_run_time: str | None
|
|
828
871
|
last_run_success: bool | None
|
|
872
|
+
last_run_status: str | None = None
|
|
873
|
+
last_run_success_count: int | None = None
|
|
874
|
+
last_run_total_count: int | None = None
|
|
829
875
|
last_run_message: str | None
|
|
830
876
|
next_run_time: str | None = None
|
|
831
877
|
|
|
@@ -874,6 +920,105 @@ class EnableDisableResponse(BaseModel):
|
|
|
874
920
|
enabled: bool
|
|
875
921
|
|
|
876
922
|
|
|
923
|
+
# Device Group Models
|
|
924
|
+
|
|
925
|
+
|
|
926
|
+
class DeviceGroupResponse(BaseModel):
|
|
927
|
+
"""设备分组响应."""
|
|
928
|
+
|
|
929
|
+
id: str
|
|
930
|
+
name: str
|
|
931
|
+
order: int
|
|
932
|
+
created_at: str
|
|
933
|
+
updated_at: str
|
|
934
|
+
is_default: bool = False
|
|
935
|
+
device_count: int = 0 # 分组内设备数量
|
|
936
|
+
|
|
937
|
+
|
|
938
|
+
class DeviceGroupListResponse(BaseModel):
|
|
939
|
+
"""设备分组列表响应."""
|
|
940
|
+
|
|
941
|
+
groups: list[DeviceGroupResponse]
|
|
942
|
+
|
|
943
|
+
|
|
944
|
+
class DeviceGroupCreateRequest(BaseModel):
|
|
945
|
+
"""创建设备分组请求."""
|
|
946
|
+
|
|
947
|
+
name: str
|
|
948
|
+
|
|
949
|
+
@field_validator("name")
|
|
950
|
+
@classmethod
|
|
951
|
+
def validate_name(cls, v: str) -> str:
|
|
952
|
+
"""验证 name 非空."""
|
|
953
|
+
if not v or not v.strip():
|
|
954
|
+
raise ValueError("name cannot be empty")
|
|
955
|
+
v = v.strip()
|
|
956
|
+
if len(v) > 50:
|
|
957
|
+
raise ValueError("name too long (max 50 characters)")
|
|
958
|
+
return v
|
|
959
|
+
|
|
960
|
+
|
|
961
|
+
class DeviceGroupUpdateRequest(BaseModel):
|
|
962
|
+
"""更新设备分组请求."""
|
|
963
|
+
|
|
964
|
+
name: str
|
|
965
|
+
|
|
966
|
+
@field_validator("name")
|
|
967
|
+
@classmethod
|
|
968
|
+
def validate_name(cls, v: str) -> str:
|
|
969
|
+
"""验证 name 非空."""
|
|
970
|
+
if not v or not v.strip():
|
|
971
|
+
raise ValueError("name cannot be empty")
|
|
972
|
+
v = v.strip()
|
|
973
|
+
if len(v) > 50:
|
|
974
|
+
raise ValueError("name too long (max 50 characters)")
|
|
975
|
+
return v
|
|
976
|
+
|
|
977
|
+
|
|
978
|
+
class DeviceGroupReorderRequest(BaseModel):
|
|
979
|
+
"""调整设备分组顺序请求."""
|
|
980
|
+
|
|
981
|
+
group_ids: list[str]
|
|
982
|
+
|
|
983
|
+
@field_validator("group_ids")
|
|
984
|
+
@classmethod
|
|
985
|
+
def validate_group_ids(cls, v: list[str]) -> list[str]:
|
|
986
|
+
"""验证 group_ids 非空且无重复."""
|
|
987
|
+
if not v:
|
|
988
|
+
raise ValueError("group_ids cannot be empty")
|
|
989
|
+
seen: set[str] = set()
|
|
990
|
+
result: list[str] = []
|
|
991
|
+
for gid in v:
|
|
992
|
+
gid = gid.strip()
|
|
993
|
+
if gid in seen:
|
|
994
|
+
raise ValueError(f"duplicate group_id: {gid}")
|
|
995
|
+
seen.add(gid)
|
|
996
|
+
result.append(gid)
|
|
997
|
+
return result
|
|
998
|
+
|
|
999
|
+
|
|
1000
|
+
class DeviceGroupAssignRequest(BaseModel):
|
|
1001
|
+
"""分配设备到分组请求."""
|
|
1002
|
+
|
|
1003
|
+
group_id: str
|
|
1004
|
+
|
|
1005
|
+
@field_validator("group_id")
|
|
1006
|
+
@classmethod
|
|
1007
|
+
def validate_group_id(cls, v: str) -> str:
|
|
1008
|
+
"""验证 group_id 非空."""
|
|
1009
|
+
if not v or not v.strip():
|
|
1010
|
+
raise ValueError("group_id cannot be empty")
|
|
1011
|
+
return v.strip()
|
|
1012
|
+
|
|
1013
|
+
|
|
1014
|
+
class DeviceGroupOperationResponse(BaseModel):
|
|
1015
|
+
"""设备分组操作响应."""
|
|
1016
|
+
|
|
1017
|
+
success: bool
|
|
1018
|
+
message: str
|
|
1019
|
+
error: str | None = None
|
|
1020
|
+
|
|
1021
|
+
|
|
877
1022
|
# Device Name Models
|
|
878
1023
|
|
|
879
1024
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{j as o}from"./index-
|
|
1
|
+
import{j as o}from"./index-DUNWZsFq.js";function t(){return o.jsx("div",{className:"p-2",children:o.jsx("h3",{children:"About"})})}export{t as component};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{b as u,r as o,j as a,e as r,B as d}from"./index-DUNWZsFq.js";import{P as g,c as x,b as f,d as m}from"./popover-DitUZhUk.js";import{D as p,d as h,e as w,f as j,g as D}from"./dialog-DOpd71Lu.js";const N=[["path",{d:"M20 6 9 17l-5-5",key:"1gmf2c"}]],b=u("check",N),c=o.createContext(void 0),P=({value:t="",onValueChange:e,children:s})=>{const[n,l]=o.useState(!1);return a.jsx(c.Provider,{value:{value:t,onValueChange:e||(()=>{}),open:n,setOpen:l},children:a.jsx(g,{open:n,onOpenChange:l,children:s})})},C=o.forwardRef(({className:t,children:e,...s},n)=>{if(!o.useContext(c))throw new Error("SelectTrigger must be used within Select");return a.jsx(x,{asChild:!0,children:a.jsxs("button",{ref:n,className:r("flex h-10 w-full items-center justify-between rounded-md border border-slate-200 bg-white px-3 py-2 text-sm ring-offset-white placeholder:text-slate-500 focus:outline-none focus:ring-2 focus:ring-slate-950 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-800 dark:bg-slate-950 dark:ring-offset-slate-950 dark:placeholder:text-slate-400 dark:focus:ring-slate-300",t),...s,children:[e,a.jsx(f,{className:"h-4 w-4 opacity-50"})]})})});C.displayName="SelectTrigger";const V=({placeholder:t})=>{const e=o.useContext(c);if(!e)throw new Error("SelectValue must be used within Select");return a.jsx("span",{className:e.value?"":"text-slate-500",children:e.value||t})},I=({children:t,className:e})=>a.jsx(m,{className:r("w-[var(--radix-popover-trigger-width)] p-1",e),children:t}),O=({value:t,children:e,disabled:s,className:n})=>{const l=o.useContext(c);if(!l)throw new Error("SelectItem must be used within Select");const i=l.value===t;return a.jsxs("div",{role:"option","aria-selected":i,className:r("relative flex w-full cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none hover:bg-slate-100 focus:bg-slate-100 dark:hover:bg-slate-800 dark:focus:bg-slate-800",s&&"pointer-events-none opacity-50",n),onClick:()=>{s||(l.onValueChange(t),l.setOpen(!1))},children:[a.jsx("span",{className:"absolute left-2 flex h-3.5 w-3.5 items-center justify-center",children:i&&a.jsx(b,{className:"h-4 w-4"})}),e]})},B=({open:t,onOpenChange:e,children:s})=>a.jsx(p,{open:t,onOpenChange:e,children:s}),v=o.forwardRef(({className:t,...e},s)=>a.jsx(h,{ref:s,className:r("sm:max-w-[425px]",t),...e}));v.displayName="AlertDialogContent";const F=({className:t,...e})=>a.jsx(w,{className:r(t),...e}),H=({className:t,...e})=>a.jsx(D,{className:r(t),...e}),A=o.forwardRef(({className:t,...e},s)=>a.jsx(j,{ref:s,className:r(t),...e}));A.displayName="AlertDialogTitle";const S=o.forwardRef(({className:t,...e},s)=>a.jsx("p",{ref:s,className:r("text-sm text-slate-500 dark:text-slate-400",t),...e}));S.displayName="AlertDialogDescription";const y=o.forwardRef(({className:t,...e},s)=>a.jsx(d,{ref:s,className:r(t),...e}));y.displayName="AlertDialogAction";const k=o.forwardRef(({className:t,...e},s)=>a.jsx(d,{ref:s,variant:"outline",className:r(t),...e}));k.displayName="AlertDialogCancel";export{B as A,P as S,C as a,V as b,I as c,O as d,v as e,F as f,A as g,S as h,H as i,k as j,y as k};
|