autoglm-gui 1.5.5__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.
Files changed (39) hide show
  1. AutoGLM_GUI/api/agents.py +118 -93
  2. AutoGLM_GUI/api/devices.py +177 -0
  3. AutoGLM_GUI/api/scheduled_tasks.py +7 -2
  4. AutoGLM_GUI/device_group_manager.py +354 -0
  5. AutoGLM_GUI/models/__init__.py +8 -0
  6. AutoGLM_GUI/models/device_group.py +63 -0
  7. AutoGLM_GUI/models/scheduled_task.py +47 -4
  8. AutoGLM_GUI/scheduler_manager.py +255 -80
  9. AutoGLM_GUI/schemas.py +148 -3
  10. AutoGLM_GUI/static/assets/{about-BwLRPh96.js → about-DVviVdH2.js} +1 -1
  11. AutoGLM_GUI/static/assets/{alert-dialog-BvDNaR9v.js → alert-dialog-IHmO2JCQ.js} +1 -1
  12. AutoGLM_GUI/static/assets/chat-C_3D0Ao7.js +134 -0
  13. AutoGLM_GUI/static/assets/{dialog-IM0Ds7Lf.js → dialog-DOpd71Lu.js} +3 -3
  14. AutoGLM_GUI/static/assets/{eye-BWBwz8sy.js → eye-CZP5ZJ_Y.js} +1 -1
  15. AutoGLM_GUI/static/assets/folder-open-_KlT8ZW7.js +1 -0
  16. AutoGLM_GUI/static/assets/{history-BkQlPjpV.js → history-BXMlCwUV.js} +1 -1
  17. AutoGLM_GUI/static/assets/index-84TrNz7w.css +1 -0
  18. AutoGLM_GUI/static/assets/index-Bh2f556h.js +1 -0
  19. AutoGLM_GUI/static/assets/index-DUNWZsFq.js +11 -0
  20. AutoGLM_GUI/static/assets/{label-CmQFo_IT.js → label-CRZhpiYG.js} +1 -1
  21. AutoGLM_GUI/static/assets/logs-DEN9nDRS.js +1 -0
  22. AutoGLM_GUI/static/assets/popover-DitUZhUk.js +1 -0
  23. AutoGLM_GUI/static/assets/scheduled-tasks-boxDKe87.js +1 -0
  24. AutoGLM_GUI/static/assets/{textarea-BlKvI11g.js → textarea-CvRHzjfV.js} +1 -1
  25. AutoGLM_GUI/static/assets/{workflows-CRq1fJf5.js → workflows-Bg3qN-6j.js} +1 -1
  26. AutoGLM_GUI/static/index.html +2 -2
  27. {autoglm_gui-1.5.5.dist-info → autoglm_gui-1.5.6.dist-info}/METADATA +355 -6
  28. {autoglm_gui-1.5.5.dist-info → autoglm_gui-1.5.6.dist-info}/RECORD +31 -29
  29. AutoGLM_GUI/static/assets/chat-DAmrsouh.js +0 -129
  30. AutoGLM_GUI/static/assets/circle-alert-C8768IhH.js +0 -1
  31. AutoGLM_GUI/static/assets/index-B0fISVXF.js +0 -1
  32. AutoGLM_GUI/static/assets/index-CH4jPveL.js +0 -11
  33. AutoGLM_GUI/static/assets/index-DSIMVL8V.css +0 -1
  34. AutoGLM_GUI/static/assets/logs-ChcSA2r_.js +0 -1
  35. AutoGLM_GUI/static/assets/popover-BnpBfSOh.js +0 -1
  36. AutoGLM_GUI/static/assets/scheduled-tasks-BwgoPEdP.js +0 -1
  37. {autoglm_gui-1.5.5.dist-info → autoglm_gui-1.5.6.dist-info}/WHEEL +0 -0
  38. {autoglm_gui-1.5.5.dist-info → autoglm_gui-1.5.6.dist-info}/entry_points.txt +0 -0
  39. {autoglm_gui-1.5.5.dist-info → autoglm_gui-1.5.6.dist-info}/licenses/LICENSE +0 -0
@@ -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
- device_serialno: str,
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
- device_serialno=device_serialno,
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 _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
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 == task.device_serialno and d.state.value == "online":
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
- self._record_failure(task, "Device offline")
198
- return
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
- self._record_failure(task, "Device busy")
210
- return
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
- agent = manager.get_agent(device.primary_device_id)
226
- agent.reset()
238
+ from AutoGLM_GUI.agents.protocols import is_async_agent
227
239
 
228
- # 使用 step 循环执行,收集每步信息
229
- is_first = True
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
- if step_result.finished: # type: ignore[union-attr]
250
- result_message = step_result.message or "Task completed" # type: ignore[union-attr]
251
- task_success = step_result.success # type: ignore[union-attr]
252
- break
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
- result_message = "Max steps reached"
255
- task_success = False
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=task.name,
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(task.device_serialno, record)
311
+ history_manager.add_record(serialno, record)
274
312
 
275
- if task_success:
276
- self._record_success(task, result_message)
277
- else:
278
- self._record_failure(task, result_message)
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
- logger.error(f"Scheduled task failed: {task.name} - {error_msg}")
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=task.name,
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(task.device_serialno, record)
338
+ history_manager.add_record(serialno, record)
299
339
 
300
- self._record_failure(task, error_msg)
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 _record_success(self, task: ScheduledTask, message: str) -> None:
306
- task.last_run_time = datetime.now()
307
- task.last_run_success = True
308
- task.last_run_message = message[:500] if message else ""
309
- self._save_tasks()
310
- logger.info(f"Scheduled task completed: {task.name}")
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 _record_failure(self, task: ScheduledTask, error: str) -> None:
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.last_run_success = False
315
- task.last_run_message = error[:500] if error else ""
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
- logger.warning(f"Scheduled task failed: {task.name} - {error}")
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
- device_serialno: str
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
- device_serialno: str | None = None
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
- device_serialno: str
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-CH4jPveL.js";function t(){return o.jsx("div",{className:"p-2",children:o.jsx("h3",{children:"About"})})}export{t as component};
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{o as u,r as o,j as a,b as r,B as d}from"./index-CH4jPveL.js";import{P as g,c as x,b as f,d as m}from"./popover-BnpBfSOh.js";import{D as p,d as h,e as w,f as j,g as D}from"./dialog-IM0Ds7Lf.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};
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};