吸收数字员工管理端策略调度预览

This commit is contained in:
baiyanyun
2026-06-13 14:42:16 +08:00
parent de501c0252
commit f08b0c39d9
7 changed files with 336 additions and 6 deletions

View File

@@ -55,6 +55,14 @@ public class DigitalEmployeeAdminWorkbenchRowDto {
private String toolName;
@JsonProperty("server_id")
private String serverId;
@JsonProperty("policy_kind")
private String policyKind;
@JsonProperty("policy_key")
private String policyKey;
@JsonProperty("schedule_id")
private String scheduleId;
@JsonProperty("job_id")
private String jobId;
@JsonProperty("occurred_at")
private String occurredAt;
@JsonProperty("synced_at")

View File

@@ -72,6 +72,8 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
"daily_report:mark_delivered", "daily_report:mark_confirmed", "daily_report:request_approval",
"memory:archive_memory", "memory:restore_memory", "memory:mark_reviewed",
"skill:mark_reviewed", "skill:dismiss",
"policy_snapshot:pull_policy", "policy_snapshot:mark_reviewed", "policy_snapshot:dismiss",
"scheduler:retry_scheduler_check", "scheduler:mark_reviewed", "scheduler:dismiss",
"audit:mark_reviewed", "audit:dismiss",
"event:mark_reviewed", "event:dismiss"
);
@@ -917,6 +919,10 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
if (StringUtils.isBlank(clientKey) || StringUtils.isBlank(entityType) || StringUtils.isBlank(entityId) || StringUtils.isBlank(action)) {
return List.of("required_field_missing");
}
List<String> disabledReasonCodes = disabledAdminActionReasonCodes(action);
if (!disabledReasonCodes.isEmpty()) {
return disabledReasonCodes;
}
if (HIGH_RISK_ADMIN_ACTIONS.contains(action)) {
return List.of("high_risk_action_disabled");
}
@@ -926,6 +932,15 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
return List.of();
}
private List<String> disabledAdminActionReasonCodes(String action) {
return switch (StringUtils.trimToEmpty(action)) {
case "edit_policy_remote" -> List.of("edit_policy_remote_disabled");
case "run_schedule_now" -> List.of("run_schedule_now_disabled");
case "start_scheduler" -> List.of("start_scheduler_disabled");
default -> List.of();
};
}
private DigitalEmployeeAdminActionDto toAdminActionDto(DigitalEmployeeAdminAction action) {
if (action == null) return null;
return DigitalEmployeeAdminActionDto.builder()
@@ -987,6 +1002,10 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
.skillId(firstString(businessView.get("skill_id"), businessView.get("skillId"), payload.get("skill_id"), payload.get("skillId")))
.toolName(firstString(businessView.get("tool_name"), businessView.get("toolName"), payload.get("tool_name"), payload.get("toolName")))
.serverId(firstString(businessView.get("server_id"), businessView.get("serverId"), payload.get("server_id"), payload.get("serverId")))
.policyKind(firstString(businessView.get("policy_kind"), businessView.get("policyKind"), payload.get("policy_kind"), payload.get("policyKind")))
.policyKey(firstString(businessView.get("policy_key"), businessView.get("policyKey"), payload.get("policy_key"), payload.get("policyKey")))
.scheduleId(firstString(businessView.get("schedule_id"), businessView.get("scheduleId"), payload.get("schedule_id"), payload.get("scheduleId")))
.jobId(firstString(businessView.get("job_id"), businessView.get("jobId"), payload.get("job_id"), payload.get("jobId")))
.occurredAt(businessRow.getOccurredAt())
.syncedAt(record.getSyncedAt())
.clientKeyMasked(maskClientKey(record.getClientKey()))
@@ -1014,6 +1033,8 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
case "route_governance" -> isRouteGovernanceRow(row);
case "memory_governance" -> isMemoryGovernanceRow(row);
case "skill_audits" -> isSkillAuditRow(row);
case "policy_center" -> isPolicyCenterRow(row);
case "scheduler_operations" -> isSchedulerOperationsRow(row);
default -> true;
};
}
@@ -1121,6 +1142,30 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
|| StringUtils.isNotBlank(row.getServerId());
}
private boolean isPolicyCenterRow(DigitalEmployeeAdminWorkbenchRowDto row) {
String category = firstString(row.getCategory());
String kind = firstString(row.getKind());
return StringUtils.equals(category, "policy_snapshot")
|| StringUtils.equals(category, "policy_center")
|| startsWithAny(kind, "policy_", "skill_policy_", "risk_approval_policy", "route_decision_policy", "plan_step_dispatch_policy")
|| StringUtils.isNotBlank(row.getPolicyKind())
|| StringUtils.isNotBlank(row.getPolicyKey());
}
private boolean isSchedulerOperationsRow(DigitalEmployeeAdminWorkbenchRowDto row) {
String category = firstString(row.getCategory());
String kind = firstString(row.getKind());
return StringUtils.equals(category, "scheduler")
|| StringUtils.equals(category, "scheduler_operation")
|| StringUtils.equals(category, "cron")
|| contains(kind, "scheduler")
|| contains(kind, "cron")
|| contains(kind, "lease")
|| contains(kind, "plan_step_dispatch")
|| StringUtils.isNotBlank(row.getScheduleId())
|| StringUtils.isNotBlank(row.getJobId());
}
private String adminCategory(DigitalEmployeeBusinessViewRowDto row, Map<String, Object> businessView, Map<String, Object> payload) {
if (booleanValue(businessView.get("approval_conflict_active"))) return "approval_conflict";
String kind = firstString(row.getKind(), businessView.get("kind"), payload.get("kind"));
@@ -1140,6 +1185,8 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
if (startsWithAny(kind, "daily_report")) return "daily_report";
if (startsWithAny(kind, "memory_")) return "memory_governance";
if (startsWithAny(kind, "skill_call_")) return "skill_call_audit";
if (contains(kind, "policy")) return "policy_snapshot";
if (contains(kind, "scheduler") || contains(kind, "cron") || contains(kind, "lease")) return "scheduler_operation";
if (startsWithAny(kind, "governance_")) return "governance";
if (contains(kind, "audit")) return kind;
return row.getEntityType();

View File

@@ -461,6 +461,112 @@ class DigitalEmployeeSyncApplicationServiceImplTest {
assertThat(result.getRecords().get(2).getBusinessView()).containsEntry("total_tokens", 180);
}
@Test
void queryAdminWorkbenchPolicyCenterReturnsPolicySnapshotRowsOnly() {
when(repository.queryBusinessRecords(eq(101L), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), eq(1L), eq(20L)))
.thenReturn(page(List.of(
record("outbox-route-policy", "event", "event-policy-1", 101L, new Date(10_000),
Map.of(
"entity_type", "event",
"category", "policy_snapshot",
"kind", "route_decision_policy_snapshot",
"status", "synced",
"title", "入口路由策略快照",
"policy_kind", "route_decision",
"policy_key", "route_decision",
"policy_version", "v2",
"source", "remote",
"enabled", true,
"auto_create_governance_commands", false,
"recommended_resolution", "review_latest_policy_snapshot"
),
Map.of("id", "event-policy-1", "kind", "route_decision_policy_snapshot")),
record("outbox-dispatch-policy", "event", "event-policy-2", 101L, new Date(11_000),
Map.of(
"entity_type", "event",
"kind", "plan_step_dispatch_policy_pulled",
"status", "synced",
"title", "派发策略已拉取",
"policy_kind", "dispatch",
"policy_key", "plan_step_dispatch",
"max_per_sweep", 3
),
Map.of("id", "event-policy-2", "kind", "plan_step_dispatch_policy_pulled")),
record("outbox-scheduler", "event", "event-scheduler-1", 101L, new Date(12_000),
Map.of("entity_type", "event", "category", "scheduler", "title", "调度", "status", "ready", "job_id", "job-1"),
Map.of("id", "event-scheduler-1", "status", "ready")),
record("outbox-task", "task", "task-1", 101L, new Date(13_000),
Map.of("entity_type", "task", "title", "普通任务", "status", "running"),
Map.of("id", "task-1", "status", "running"))
)));
DigitalEmployeeAdminWorkbenchPageDto result = service.queryAdminWorkbench(
"policy_center", null, null, null, null, null, null, null, null, null, null, null, 1L, 20L
);
assertThat(result.getTotal()).isEqualTo(2);
assertThat(result.getRecords()).extracting(DigitalEmployeeAdminWorkbenchRowDto::getEntityId)
.containsExactly("event-policy-1", "event-policy-2");
assertThat(result.getRecords().get(0).getView()).isEqualTo("policy_center");
assertThat(result.getRecords().get(0).getPolicyKind()).isEqualTo("route_decision");
assertThat(result.getRecords().get(0).getPolicyKey()).isEqualTo("route_decision");
assertThat(result.getRecords().get(0).getBusinessView()).containsEntry("auto_create_governance_commands", false);
assertThat(result.getRecords().get(1).getPolicyKind()).isEqualTo("dispatch");
}
@Test
void queryAdminWorkbenchSchedulerOperationsReturnsSchedulerRowsOnly() {
when(repository.queryBusinessRecords(eq(101L), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), eq(1L), eq(20L)))
.thenReturn(page(List.of(
record("outbox-scheduler-job", "event", "event-scheduler-1", 101L, new Date(10_000),
Map.of(
"entity_type", "event",
"category", "scheduler",
"kind", "scheduler_job_snapshot",
"status", "enabled",
"title", "日报调度",
"schedule_id", "schedule-1",
"job_id", "job-daily",
"cron", "0 9 * * *",
"next_run_at", "2026-06-14T09:00:00+08:00",
"last_run_at", "2026-06-13T09:00:00+08:00",
"last_status", "completed",
"catch_up_on_startup", true,
"max_run_history", 20
),
Map.of("id", "event-scheduler-1", "kind", "scheduler_job_snapshot")),
record("outbox-scheduler-check", "event", "event-scheduler-2", 101L, new Date(11_000),
Map.of(
"entity_type", "event",
"kind", "scheduler_check_completed",
"status", "completed",
"title", "立即检查调度",
"schedule_id", "schedule-1",
"job_id", "job-daily"
),
Map.of("id", "event-scheduler-2", "kind", "scheduler_check_completed")),
record("outbox-policy", "event", "event-policy-1", 101L, new Date(12_000),
Map.of("entity_type", "event", "category", "policy_snapshot", "title", "策略", "status", "synced", "policy_kind", "route_decision"),
Map.of("id", "event-policy-1", "status", "synced")),
record("outbox-approval", "approval", "approval-1", 101L, new Date(13_000),
Map.of("entity_type", "approval", "title", "普通审批", "status", "pending"),
Map.of("id", "approval-1", "status", "pending"))
)));
DigitalEmployeeAdminWorkbenchPageDto result = service.queryAdminWorkbench(
"scheduler_operations", null, null, null, null, null, null, null, null, null, null, null, 1L, 20L
);
assertThat(result.getTotal()).isEqualTo(2);
assertThat(result.getRecords()).extracting(DigitalEmployeeAdminWorkbenchRowDto::getEntityId)
.containsExactly("event-scheduler-1", "event-scheduler-2");
assertThat(result.getRecords().get(0).getView()).isEqualTo("scheduler_operations");
assertThat(result.getRecords().get(0).getScheduleId()).isEqualTo("schedule-1");
assertThat(result.getRecords().get(0).getJobId()).isEqualTo("job-daily");
assertThat(result.getRecords().get(0).getBusinessView()).containsEntry("catch_up_on_startup", true);
assertThat(result.getRecords().get(1).getKind()).isEqualTo("scheduler_check_completed");
}
@Test
void queryAdminWorkbenchServiceOperationsReturnsServiceHealthAndRestartRowsOnly() {
when(repository.queryBusinessRecords(eq(101L), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), eq(1L), eq(20L)))
@@ -1020,6 +1126,24 @@ class DigitalEmployeeSyncApplicationServiceImplTest {
assertThat(captor.getValue().getStatus()).isEqualTo("rejected");
}
@Test
void createAdminActionReturnsSpecificPolicyAndSchedulerDisabledReasons() {
DigitalEmployeeAdminActionResultDto editPolicy = service.createAdminAction(
adminActionRequest("policy_snapshot", "route_decision", "edit_policy_remote")
);
DigitalEmployeeAdminActionResultDto runSchedule = service.createAdminAction(
adminActionRequest("scheduler", "job-daily", "run_schedule_now")
);
DigitalEmployeeAdminActionResultDto startScheduler = service.createAdminAction(
adminActionRequest("scheduler", "job-daily", "start_scheduler")
);
assertThat(editPolicy.getOk()).isFalse();
assertThat(editPolicy.getReasonCodes()).contains("edit_policy_remote_disabled");
assertThat(runSchedule.getReasonCodes()).contains("run_schedule_now_disabled");
assertThat(startScheduler.getReasonCodes()).contains("start_scheduler_disabled");
}
@Test
void queryPendingAdminActionsFiltersTenantClientDeviceAndMasksKey() {
when(adminActionRepository.queryActions(eq(101L), eq("client-key-001"), eq("device-001"), eq("pending"), eq(1L), eq(20L)))