吸收数字员工跨设备策略冲突视图
This commit is contained in:
@@ -6,6 +6,7 @@ import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminActionRe
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminActionResultDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeBusinessViewPageDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeePlanBusinessViewDetailDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeePolicySnapshotPageDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncRecordPageDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncRequestDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncResponseDto;
|
||||
@@ -62,6 +63,17 @@ public interface DigitalEmployeeSyncApplicationService {
|
||||
Long pageSize
|
||||
);
|
||||
|
||||
DigitalEmployeePolicySnapshotPageDto queryAdminPolicySnapshots(
|
||||
String clientKey,
|
||||
String deviceId,
|
||||
String status,
|
||||
String policyKind,
|
||||
Date updatedFrom,
|
||||
Date updatedTo,
|
||||
Long pageNo,
|
||||
Long pageSize
|
||||
);
|
||||
|
||||
DigitalEmployeeAdminActionResultDto createAdminAction(DigitalEmployeeAdminActionRequestDto request);
|
||||
|
||||
DigitalEmployeeAdminActionPageDto queryPendingAdminActions(
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.xspaceagi.agent.core.adapter.dto.digital;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class DigitalEmployeePolicySnapshotPageDto {
|
||||
private Long total;
|
||||
@JsonProperty("page_no")
|
||||
private Long pageNo;
|
||||
@JsonProperty("page_size")
|
||||
private Long pageSize;
|
||||
private List<DigitalEmployeePolicySnapshotRowDto> records;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.xspaceagi.agent.core.adapter.dto.digital;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class DigitalEmployeePolicySnapshotRowDto {
|
||||
@JsonProperty("policy_kind")
|
||||
private String policyKind;
|
||||
@JsonProperty("policy_key")
|
||||
private String policyKey;
|
||||
private String status;
|
||||
private String summary;
|
||||
@JsonProperty("client_count")
|
||||
private Integer clientCount;
|
||||
@JsonProperty("device_count")
|
||||
private Integer deviceCount;
|
||||
@JsonProperty("conflict_keys")
|
||||
private List<String> conflictKeys;
|
||||
@JsonProperty("latest_synced_at")
|
||||
private Date latestSyncedAt;
|
||||
@JsonProperty("recommended_resolution")
|
||||
private String recommendedResolution;
|
||||
@JsonProperty("sync_record_url")
|
||||
private String syncRecordUrl;
|
||||
@JsonProperty("client_key_masked")
|
||||
private String clientKeyMasked;
|
||||
@JsonProperty("device_id")
|
||||
private String deviceId;
|
||||
@JsonProperty("business_view")
|
||||
private Map<String, Object> businessView;
|
||||
}
|
||||
@@ -16,6 +16,8 @@ import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminWorkbenc
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeBusinessViewPageDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeBusinessViewRowDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeePlanBusinessViewDetailDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeePolicySnapshotPageDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeePolicySnapshotRowDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncAckDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncItemDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncRecordDto;
|
||||
@@ -343,6 +345,120 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
|
||||
return adminWorkbenchPage(rows, currentPage, currentPageSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DigitalEmployeePolicySnapshotPageDto queryAdminPolicySnapshots(
|
||||
String clientKey,
|
||||
String deviceId,
|
||||
String status,
|
||||
String policyKind,
|
||||
Date updatedFrom,
|
||||
Date updatedTo,
|
||||
Long pageNo,
|
||||
Long pageSize
|
||||
) {
|
||||
long currentPage = pageNo == null || pageNo < 1 ? DEFAULT_PAGE_NO : pageNo;
|
||||
long currentPageSize = pageSize == null || pageSize < 1
|
||||
? DEFAULT_PAGE_SIZE
|
||||
: Math.min(pageSize, MAX_PAGE_SIZE);
|
||||
Long tenantId = currentTenantId();
|
||||
if (tenantId == null) {
|
||||
return policySnapshotPage(List.of(), currentPage, currentPageSize);
|
||||
}
|
||||
IPage<DigitalEmployeeSyncRecord> page = digitalEmployeeSyncRecordRepository.queryBusinessRecords(
|
||||
tenantId,
|
||||
clientKey,
|
||||
deviceId,
|
||||
"event",
|
||||
null,
|
||||
updatedFrom,
|
||||
updatedTo,
|
||||
null,
|
||||
DEFAULT_PAGE_NO,
|
||||
MAX_PAGE_SIZE
|
||||
);
|
||||
Map<String, List<DigitalEmployeeSyncRecord>> grouped = new LinkedHashMap<>();
|
||||
for (DigitalEmployeeSyncRecord record : page.getRecords()) {
|
||||
Map<String, Object> view = readJsonObject(record.getBusinessView());
|
||||
String kind = firstString(view.get("policy_kind"));
|
||||
if (StringUtils.isBlank(kind)) continue;
|
||||
if (StringUtils.isNotBlank(policyKind) && !StringUtils.equals(kind, policyKind)) continue;
|
||||
grouped.computeIfAbsent(kind, ignored -> new ArrayList<>()).add(record);
|
||||
}
|
||||
List<DigitalEmployeePolicySnapshotRowDto> rows = grouped.entrySet().stream()
|
||||
.map(entry -> toPolicySnapshotRow(entry.getKey(), entry.getValue()))
|
||||
.filter(row -> matchesText(status, row.getStatus()))
|
||||
.toList();
|
||||
return policySnapshotPage(rows, currentPage, currentPageSize);
|
||||
}
|
||||
|
||||
private DigitalEmployeePolicySnapshotPageDto policySnapshotPage(List<DigitalEmployeePolicySnapshotRowDto> rows, long pageNo, long pageSize) {
|
||||
int fromIndex = (int) Math.min(Math.max(pageNo - 1, 0) * pageSize, rows.size());
|
||||
int toIndex = (int) Math.min(fromIndex + pageSize, rows.size());
|
||||
return DigitalEmployeePolicySnapshotPageDto.builder()
|
||||
.total((long) rows.size())
|
||||
.pageNo(pageNo)
|
||||
.pageSize(pageSize)
|
||||
.records(rows.subList(fromIndex, toIndex))
|
||||
.build();
|
||||
}
|
||||
|
||||
private DigitalEmployeePolicySnapshotRowDto toPolicySnapshotRow(String policyKind, List<DigitalEmployeeSyncRecord> records) {
|
||||
List<Map<String, Object>> views = records.stream().map(record -> readJsonObject(record.getBusinessView())).toList();
|
||||
List<String> signatures = views.stream().map(this::policySignature).distinct().toList();
|
||||
DigitalEmployeeSyncRecord latest = records.stream()
|
||||
.max(Comparator.comparing(DigitalEmployeeSyncRecord::getSyncedAt, Comparator.nullsLast(Comparator.naturalOrder())))
|
||||
.orElse(records.get(0));
|
||||
Map<String, Object> latestView = readJsonObject(latest.getBusinessView());
|
||||
String status = signatures.size() > 1 ? "conflict" : "consistent";
|
||||
return DigitalEmployeePolicySnapshotRowDto.builder()
|
||||
.policyKind(policyKind)
|
||||
.policyKey(firstString(latestView.get("policy_key"), policyKind))
|
||||
.status(status)
|
||||
.summary(firstString(latestView.get("summary"), latestView.get("title"), policyKind))
|
||||
.clientCount((int) records.stream().map(DigitalEmployeeSyncRecord::getClientKey).filter(StringUtils::isNotBlank).distinct().count())
|
||||
.deviceCount((int) records.stream().map(DigitalEmployeeSyncRecord::getDeviceId).filter(StringUtils::isNotBlank).distinct().count())
|
||||
.conflictKeys("conflict".equals(status) ? policyConflictKeys(views) : List.of())
|
||||
.latestSyncedAt(latest.getSyncedAt())
|
||||
.recommendedResolution("conflict".equals(status) ? "review_latest_policy_snapshot" : "keep_current")
|
||||
.syncRecordUrl(syncRecordUrl(latest.getOutboxId()))
|
||||
.clientKeyMasked(maskClientKey(latest.getClientKey()))
|
||||
.deviceId(latest.getDeviceId())
|
||||
.businessView(latestView)
|
||||
.build();
|
||||
}
|
||||
|
||||
private String policySignature(Map<String, Object> view) {
|
||||
Map<String, Object> signature = new LinkedHashMap<>();
|
||||
for (String key : policyConflictCandidateKeys()) {
|
||||
signature.put(key, view.get(key));
|
||||
}
|
||||
return toJson(signature);
|
||||
}
|
||||
|
||||
private List<String> policyConflictKeys(List<Map<String, Object>> views) {
|
||||
List<String> keys = new ArrayList<>();
|
||||
for (String key : policyConflictCandidateKeys()) {
|
||||
if (views.stream().map(view -> Objects.toString(view.get(key), "")).distinct().count() > 1) {
|
||||
keys.add(key);
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
private List<String> policyConflictCandidateKeys() {
|
||||
return List.of(
|
||||
"enabled",
|
||||
"policy_version",
|
||||
"policy_source",
|
||||
"auto_create_governance_commands",
|
||||
"max_per_sweep",
|
||||
"catch_up_on_startup",
|
||||
"approval_required_risk_levels",
|
||||
"approval_required_actions",
|
||||
"auto_reject_actions"
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
@DSTransactional
|
||||
public DigitalEmployeeAdminActionResultDto createAdminAction(DigitalEmployeeAdminActionRequestDto request) {
|
||||
|
||||
@@ -12,6 +12,8 @@ import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminActionPa
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminActionRequestDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminActionResultDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeePlanBusinessViewDetailDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeePolicySnapshotPageDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeePolicySnapshotRowDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncItemDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncRecordPageDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncRequestDto;
|
||||
@@ -334,6 +336,46 @@ class DigitalEmployeeSyncApplicationServiceImplTest {
|
||||
assertThat(action.getReasonCodes()).contains("admin_action_applied");
|
||||
}
|
||||
|
||||
@Test
|
||||
void queryAdminPolicySnapshotsDetectsCrossDeviceConflict() {
|
||||
when(repository.queryBusinessRecords(eq(101L), eq(null), eq(null), eq("event"), eq(null), eq(null), eq(null), eq(null), eq(1L), eq(100L)))
|
||||
.thenReturn(page(List.of(
|
||||
record("outbox-policy-1", "event", "event-policy-1", 101L, new Date(10_000),
|
||||
Map.of(
|
||||
"entity_type", "event",
|
||||
"category", "policy_snapshot",
|
||||
"kind", "digital_workday_pull_route_decision_policy",
|
||||
"policy_kind", "route_decision",
|
||||
"policy_key", "route_decision",
|
||||
"enabled", true,
|
||||
"auto_create_governance_commands", true,
|
||||
"title", "入口路由策略"
|
||||
),
|
||||
Map.of("kind", "digital_workday_pull_route_decision_policy")),
|
||||
record("outbox-policy-2", "event", "event-policy-2", 101L, new Date(11_000),
|
||||
Map.of(
|
||||
"entity_type", "event",
|
||||
"category", "policy_snapshot",
|
||||
"kind", "digital_workday_pull_route_decision_policy",
|
||||
"policy_kind", "route_decision",
|
||||
"policy_key", "route_decision",
|
||||
"enabled", true,
|
||||
"auto_create_governance_commands", false,
|
||||
"title", "入口路由策略"
|
||||
),
|
||||
Map.of("kind", "digital_workday_pull_route_decision_policy"))
|
||||
)));
|
||||
|
||||
DigitalEmployeePolicySnapshotPageDto result = service.queryAdminPolicySnapshots(null, null, "conflict", "route_decision", null, null, 1L, 20L);
|
||||
|
||||
assertThat(result.getTotal()).isEqualTo(1);
|
||||
DigitalEmployeePolicySnapshotRowDto row = result.getRecords().get(0);
|
||||
assertThat(row.getPolicyKind()).isEqualTo("route_decision");
|
||||
assertThat(row.getStatus()).isEqualTo("conflict");
|
||||
assertThat(row.getConflictKeys()).contains("auto_create_governance_commands");
|
||||
assertThat(row.getRecommendedResolution()).isEqualTo("review_latest_policy_snapshot");
|
||||
}
|
||||
|
||||
private IPage<DigitalEmployeeSyncRecord> page(List<DigitalEmployeeSyncRecord> records) {
|
||||
Page<DigitalEmployeeSyncRecord> page = new Page<>(1, 20, records.size());
|
||||
page.setRecords(records);
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.xspaceagi.agent.web.ui.controller;
|
||||
|
||||
import com.xspaceagi.agent.core.adapter.application.DigitalEmployeeSyncApplicationService;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeePolicySnapshotPageDto;
|
||||
import com.xspaceagi.system.spec.annotation.RequireResource;
|
||||
import com.xspaceagi.system.spec.dto.ReqResult;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Date;
|
||||
|
||||
import static com.xspaceagi.system.spec.enums.ResourceEnum.CONTENT_DIGITAL_EMPLOYEE_OPS_QUERY_LIST;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/digital-employee/admin/policies")
|
||||
public class DigitalEmployeeAdminPolicyController {
|
||||
|
||||
@Resource
|
||||
private DigitalEmployeeSyncApplicationService digitalEmployeeSyncApplicationService;
|
||||
|
||||
@RequireResource(CONTENT_DIGITAL_EMPLOYEE_OPS_QUERY_LIST)
|
||||
@RequestMapping(path = "/snapshots", method = RequestMethod.GET)
|
||||
public ReqResult<DigitalEmployeePolicySnapshotPageDto> querySnapshots(
|
||||
@RequestParam(name = "client_key", required = false) String clientKey,
|
||||
@RequestParam(name = "device_id", required = false) String deviceId,
|
||||
@RequestParam(name = "status", required = false) String status,
|
||||
@RequestParam(name = "policy_kind", required = false) String policyKind,
|
||||
@RequestParam(name = "updated_from", required = false) String updatedFrom,
|
||||
@RequestParam(name = "updated_to", required = false) String updatedTo,
|
||||
@RequestParam(name = "page_no", required = false) Long pageNo,
|
||||
@RequestParam(name = "page_size", required = false) Long pageSize
|
||||
) {
|
||||
return ReqResult.success(digitalEmployeeSyncApplicationService.queryAdminPolicySnapshots(
|
||||
clientKey,
|
||||
deviceId,
|
||||
status,
|
||||
policyKind,
|
||||
parseDate(updatedFrom),
|
||||
parseDate(updatedTo),
|
||||
pageNo,
|
||||
pageSize
|
||||
));
|
||||
}
|
||||
|
||||
private Date parseDate(String value) {
|
||||
if (StringUtils.isBlank(value)) return null;
|
||||
String trimmed = value.trim();
|
||||
try {
|
||||
return Date.from(Instant.parse(trimmed));
|
||||
} catch (Exception ignored) {
|
||||
try {
|
||||
LocalDateTime localDateTime = LocalDateTime.parse(trimmed, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
|
||||
return Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
|
||||
} catch (Exception ignoredAgain) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user