吸收数字员工管理端步骤调度预览

This commit is contained in:
baiyanyun
2026-06-13 15:38:30 +08:00
parent c9d8aa3b93
commit 5ab6bc0fd0
7 changed files with 307 additions and 3 deletions

View File

@@ -33,6 +33,22 @@ public class DigitalEmployeeAdminWorkbenchRowDto {
private String taskId;
@JsonProperty("run_id")
private String runId;
@JsonProperty("step_id")
private String stepId;
@JsonProperty("dependency_status")
private String dependencyStatus;
@JsonProperty("lease_state")
private String leaseState;
@JsonProperty("lease_source")
private String leaseSource;
@JsonProperty("dispatch_status")
private String dispatchStatus;
@JsonProperty("dispatch_policy_key")
private String dispatchPolicyKey;
@JsonProperty("orchestration_id")
private String orchestrationId;
@JsonProperty("trigger_source")
private String triggerSource;
@JsonProperty("approval_id")
private String approvalId;
@JsonProperty("artifact_id")

View File

@@ -74,6 +74,7 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
"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",
"plan_step:mark_reviewed", "plan_step:dismiss", "plan_step:retry_policy_check",
"plan:mark_reviewed", "plan:dismiss", "plan:request_revision_review", "plan:record_escalation",
"revision:mark_reviewed", "revision:dismiss", "revision:request_revision_review",
"audit:mark_reviewed", "audit:dismiss",
@@ -995,6 +996,14 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
.planId(businessRow.getPlanId())
.taskId(businessRow.getTaskId())
.runId(businessRow.getRunId())
.stepId(firstString(businessView.get("step_id"), businessView.get("stepId"), payload.get("step_id"), payload.get("stepId"), "plan_step".equals(record.getEntityType()) ? record.getEntityId() : null))
.dependencyStatus(firstString(businessView.get("dependency_status"), businessView.get("dependencyStatus"), payload.get("dependency_status"), payload.get("dependencyStatus")))
.leaseState(firstString(businessView.get("lease_state"), businessView.get("leaseState"), payload.get("lease_state"), payload.get("leaseState")))
.leaseSource(firstString(businessView.get("lease_source"), businessView.get("leaseSource"), payload.get("lease_source"), payload.get("leaseSource")))
.dispatchStatus(firstString(businessView.get("dispatch_status"), businessView.get("dispatchStatus"), payload.get("dispatch_status"), payload.get("dispatchStatus")))
.dispatchPolicyKey(firstString(businessView.get("dispatch_policy_key"), businessView.get("dispatchPolicyKey"), businessView.get("policy_key"), payload.get("dispatch_policy_key"), payload.get("dispatchPolicyKey"), payload.get("policy_key")))
.orchestrationId(firstString(businessView.get("orchestration_id"), businessView.get("orchestrationId"), payload.get("orchestration_id"), payload.get("orchestrationId")))
.triggerSource(firstString(businessView.get("trigger_source"), businessView.get("triggerSource"), payload.get("trigger_source"), payload.get("triggerSource")))
.approvalId(firstString(businessView.get("approval_id"), payload.get("approval_id"), "approval".equals(record.getEntityType()) ? record.getEntityId() : null))
.artifactId(firstString(businessView.get("artifact_id"), payload.get("artifact_id"), "artifact".equals(record.getEntityType()) ? record.getEntityId() : null))
.cleanupRequestId(firstString(businessView.get("cleanup_request_id"), businessView.get("cleanupRequestId"), payload.get("cleanup_request_id"), payload.get("cleanupRequestId")))
@@ -1050,6 +1059,7 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
case "skill_audits" -> isSkillAuditRow(row);
case "policy_center" -> isPolicyCenterRow(row);
case "scheduler_operations" -> isSchedulerOperationsRow(row);
case "plan_step_dispatch" -> isPlanStepDispatchRow(row);
case "plan_governance" -> isPlanGovernanceRow(row);
case "plan_revisions" -> isPlanRevisionRow(row);
default -> true;
@@ -1183,6 +1193,36 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
|| StringUtils.isNotBlank(row.getJobId());
}
private boolean isPlanStepDispatchRow(DigitalEmployeeAdminWorkbenchRowDto row) {
String category = firstString(row.getCategory());
String kind = firstString(row.getKind());
String status = firstString(row.getStatus());
String source = firstString(
row.getLeaseSource(), row.getTriggerSource(),
row.getBusinessView().get("source"), row.getPayload().get("source"),
row.getBusinessView().get("lease_source"), row.getPayload().get("lease_source")
);
return StringUtils.equals(row.getEntityType(), "plan_step")
|| StringUtils.equals(category, "plan_step_dispatch")
|| StringUtils.equals(category, "plan_step")
|| startsWithAny(kind, "plan_step_dispatch", "plan_step_lease", "dispatch_ready_steps")
|| contains(kind, "plan_step_dispatch")
|| contains(kind, "plan_step_lease")
|| contains(kind, "dispatch_ready_steps")
|| contains(kind, "lease_active")
|| contains(kind, "remote_lease_rejected")
|| contains(kind, "local_fallback")
|| contains(status, "lease_active")
|| contains(status, "remote_lease_rejected")
|| contains(status, "local_fallback")
|| contains(source, "remote_lease_rejected")
|| contains(source, "local_fallback")
|| StringUtils.isNotBlank(row.getStepId())
|| StringUtils.isNotBlank(row.getLeaseState())
|| StringUtils.isNotBlank(row.getDispatchStatus())
|| StringUtils.isNotBlank(row.getDispatchPolicyKey());
}
private boolean isPlanGovernanceRow(DigitalEmployeeAdminWorkbenchRowDto row) {
String category = firstString(row.getCategory());
String kind = firstString(row.getKind());
@@ -1206,6 +1246,11 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
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"));
if (StringUtils.equals(row.getEntityType(), "plan_step")
|| startsWithAny(kind, "plan_step_dispatch", "plan_step_lease")
|| contains(kind, "dispatch_ready_steps")) {
return "plan_step_dispatch";
}
if (contains(kind, "route_decision_blocked") || contains(kind, "route_decision_approval_required")
|| contains(kind, "route_action_policy_blocked") || contains(kind, "route_action_missing_context")
|| contains(kind, "route_action_not_mapped")) {

View File

@@ -567,6 +567,94 @@ class DigitalEmployeeSyncApplicationServiceImplTest {
assertThat(result.getRecords().get(1).getKind()).isEqualTo("scheduler_check_completed");
}
@Test
void queryAdminWorkbenchPlanStepDispatchReturnsStepLeaseAndDispatchRowsOnly() {
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-step", "plan_step", "step-1", 101L, new Date(10_000),
Map.of(
"entity_type", "plan_step",
"category", "plan_step",
"title", "步骤一",
"status", "ready",
"summary", "依赖已满足",
"plan_id", "plan-1",
"task_id", "task-1",
"step_id", "step-1",
"dependency_status", "ready"
),
Map.of("id", "step-1", "status", "ready")),
record("outbox-dispatch", "event", "event-step-1", 101L, new Date(11_000),
Map.ofEntries(
Map.entry("entity_type", "event"),
Map.entry("kind", "plan_step_dispatch_started"),
Map.entry("status", "running"),
Map.entry("title", "步骤派发开始"),
Map.entry("plan_id", "plan-1"),
Map.entry("task_id", "task-1"),
Map.entry("run_id", "run-1"),
Map.entry("step_id", "step-1"),
Map.entry("dispatch_status", "started"),
Map.entry("orchestration_id", "orch-1"),
Map.entry("trigger_source", "scheduler_sweep")
),
Map.of("id", "event-step-1", "kind", "plan_step_dispatch_started")),
record("outbox-lease", "event", "event-step-2", 101L, new Date(12_000),
Map.of(
"entity_type", "event",
"kind", "plan_step_lease_acquired",
"status", "lease_active",
"title", "步骤租约已获取",
"plan_id", "plan-1",
"step_id", "step-1",
"lease_state", "active",
"lease_source", "remote"
),
Map.of("id", "event-step-2", "kind", "plan_step_lease_acquired")),
record("outbox-policy", "event", "event-step-3", 101L, new Date(13_000),
Map.of(
"entity_type", "event",
"kind", "plan_step_dispatch_policy_pulled",
"status", "synced",
"title", "步骤派发策略已拉取",
"step_id", "step-1",
"dispatch_policy_key", "plan_step_dispatch"
),
Map.of("id", "event-step-3", "kind", "plan_step_dispatch_policy_pulled")),
record("outbox-approval", "approval", "approval-1", 101L, new Date(14_000),
Map.of("entity_type", "approval", "title", "普通审批", "status", "pending"),
Map.of("id", "approval-1", "status", "pending")),
record("outbox-notification", "notification", "notification-1", 101L, new Date(15_000),
Map.of("entity_type", "notification", "title", "普通通知", "status", "unread"),
Map.of("id", "notification-1", "status", "unread")),
record("outbox-artifact", "artifact", "artifact-1", 101L, new Date(16_000),
Map.of("entity_type", "artifact", "title", "普通产物", "status", "available"),
Map.of("id", "artifact-1", "status", "available")),
record("outbox-audit", "event", "event-audit-1", 101L, new Date(17_000),
Map.of("entity_type", "event", "category", "tool_call_audit", "title", "审计", "status", "warn"),
Map.of("id", "event-audit-1", "status", "warn"))
)));
DigitalEmployeeAdminWorkbenchPageDto result = service.queryAdminWorkbench(
"plan_step_dispatch", null, null, null, null, null, null, null, null, null, null, null, 1L, 20L
);
assertThat(result.getTotal()).isEqualTo(4);
assertThat(result.getRecords()).extracting(DigitalEmployeeAdminWorkbenchRowDto::getEntityId)
.containsExactly("step-1", "event-step-1", "event-step-2", "event-step-3");
assertThat(result.getRecords().get(0).getView()).isEqualTo("plan_step_dispatch");
assertThat(result.getRecords().get(0).getStepId()).isEqualTo("step-1");
assertThat(result.getRecords().get(0).getDependencyStatus()).isEqualTo("ready");
assertThat(result.getRecords().get(1).getDispatchStatus()).isEqualTo("started");
assertThat(result.getRecords().get(1).getOrchestrationId()).isEqualTo("orch-1");
assertThat(result.getRecords().get(1).getTriggerSource()).isEqualTo("scheduler_sweep");
assertThat(result.getRecords().get(2).getLeaseState()).isEqualTo("active");
assertThat(result.getRecords().get(2).getLeaseSource()).isEqualTo("remote");
assertThat(result.getRecords().get(3).getDispatchPolicyKey()).isEqualTo("plan_step_dispatch");
assertThat(result.getRecords().get(3).getTitle()).isEqualTo("步骤派发策略已拉取");
assertThat(result.getRecords().get(0).getSummary()).isEqualTo("依赖已满足");
}
@Test
void queryAdminWorkbenchPlanGovernanceReturnsGovernanceEscalationRowsOnly() {
when(repository.queryBusinessRecords(eq(101L), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), eq(1L), eq(20L)))
@@ -1281,6 +1369,27 @@ class DigitalEmployeeSyncApplicationServiceImplTest {
assertThat(activatePlan.getReasonCodes()).contains("high_risk_governance_action_disabled");
}
@Test
void createAdminActionRecordsPlanStepLowRiskAndRejectsRemoteDispatch() {
DigitalEmployeeAdminActionResultDto retryPolicyCheck = service.createAdminAction(
adminActionRequest("plan_step", "step-1", "retry_policy_check")
);
DigitalEmployeeAdminActionResultDto dispatchReadyStep = service.createAdminAction(
adminActionRequest("plan_step", "step-1", "dispatch_ready_step")
);
DigitalEmployeeAdminActionResultDto takeoverLease = service.createAdminAction(
adminActionRequest("plan_step", "step-1", "takeover_lease")
);
assertThat(retryPolicyCheck.getOk()).isTrue();
assertThat(retryPolicyCheck.getStatus()).isEqualTo("pending");
assertThat(retryPolicyCheck.getReasonCodes()).contains("action_recorded");
assertThat(dispatchReadyStep.getOk()).isFalse();
assertThat(dispatchReadyStep.getReasonCodes()).contains("unsupported_admin_action");
assertThat(takeoverLease.getOk()).isFalse();
assertThat(takeoverLease.getReasonCodes()).contains("unsupported_admin_action");
}
@Test
void queryPendingAdminActionsFiltersTenantClientDeviceAndMasksKey() {
when(adminActionRepository.queryActions(eq(101L), eq("client-key-001"), eq("device-001"), eq("pending"), eq(1L), eq(20L)))