吸收数字员工管理端低风险动作闭环
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
package com.xspaceagi.agent.core.adapter.application;
|
||||
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminWorkbenchPageDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminActionPageDto;
|
||||
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.DigitalEmployeeBusinessViewPageDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeePlanBusinessViewDetailDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncRecordPageDto;
|
||||
@@ -8,6 +11,7 @@ import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncRequestDt
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncResponseDto;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
public interface DigitalEmployeeSyncApplicationService {
|
||||
DigitalEmployeeSyncResponseDto syncOutbox(DigitalEmployeeSyncRequestDto request);
|
||||
@@ -57,4 +61,19 @@ public interface DigitalEmployeeSyncApplicationService {
|
||||
Long pageNo,
|
||||
Long pageSize
|
||||
);
|
||||
|
||||
DigitalEmployeeAdminActionResultDto createAdminAction(DigitalEmployeeAdminActionRequestDto request);
|
||||
|
||||
DigitalEmployeeAdminActionPageDto queryPendingAdminActions(
|
||||
String clientKey,
|
||||
String deviceId,
|
||||
Long pageNo,
|
||||
Long pageSize
|
||||
);
|
||||
|
||||
DigitalEmployeeAdminActionResultDto markAdminActionResult(
|
||||
Long actionId,
|
||||
String status,
|
||||
List<String> reasonCodes
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
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 DigitalEmployeeAdminActionDto {
|
||||
private Long id;
|
||||
@JsonProperty("client_key_masked")
|
||||
private String clientKeyMasked;
|
||||
@JsonProperty("device_id")
|
||||
private String deviceId;
|
||||
@JsonProperty("entity_type")
|
||||
private String entityType;
|
||||
@JsonProperty("entity_id")
|
||||
private String entityId;
|
||||
private String action;
|
||||
private String status;
|
||||
@JsonProperty("reason_codes")
|
||||
private List<String> reasonCodes;
|
||||
@JsonProperty("request_payload")
|
||||
private Map<String, Object> requestPayload;
|
||||
@JsonProperty("created_at")
|
||||
private Date createdAt;
|
||||
@JsonProperty("processed_at")
|
||||
private Date processedAt;
|
||||
}
|
||||
@@ -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 DigitalEmployeeAdminActionPageDto {
|
||||
private Long total;
|
||||
@JsonProperty("page_no")
|
||||
private Long pageNo;
|
||||
@JsonProperty("page_size")
|
||||
private Long pageSize;
|
||||
private List<DigitalEmployeeAdminActionDto> records;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.xspaceagi.agent.core.adapter.dto.digital;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
public class DigitalEmployeeAdminActionRequestDto {
|
||||
@JsonProperty("sync_record_id")
|
||||
private Long syncRecordId;
|
||||
@JsonProperty("client_key")
|
||||
private String clientKey;
|
||||
@JsonProperty("device_id")
|
||||
private String deviceId;
|
||||
@JsonProperty("entity_type")
|
||||
private String entityType;
|
||||
@JsonProperty("entity_id")
|
||||
private String entityId;
|
||||
private String action;
|
||||
@JsonProperty("request_payload")
|
||||
private Map<String, Object> requestPayload;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
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 DigitalEmployeeAdminActionResultDto {
|
||||
private Boolean ok;
|
||||
private String status;
|
||||
@JsonProperty("reason_codes")
|
||||
private List<String> reasonCodes;
|
||||
private DigitalEmployeeAdminActionDto action;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.xspaceagi.agent.core.adapter.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.xspaceagi.agent.core.adapter.repository.entity.DigitalEmployeeAdminAction;
|
||||
|
||||
public interface DigitalEmployeeAdminActionRepository extends IService<DigitalEmployeeAdminAction> {
|
||||
IPage<DigitalEmployeeAdminAction> queryActions(
|
||||
Long tenantId,
|
||||
String clientKey,
|
||||
String deviceId,
|
||||
String status,
|
||||
long pageNo,
|
||||
long pageSize
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.xspaceagi.agent.core.adapter.repository.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@TableName("digital_employee_admin_action")
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class DigitalEmployeeAdminAction {
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
@TableField(value = "_tenant_id")
|
||||
private Long tenantId;
|
||||
private Long userId;
|
||||
private String clientKey;
|
||||
private String deviceId;
|
||||
private String entityType;
|
||||
private String entityId;
|
||||
private String action;
|
||||
private String status;
|
||||
private String reasonCodes;
|
||||
private String requestPayload;
|
||||
private Date processedAt;
|
||||
private Date modified;
|
||||
private Date created;
|
||||
}
|
||||
@@ -7,6 +7,10 @@ import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.xspaceagi.agent.core.adapter.application.DigitalEmployeeSyncApplicationService;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminActionDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminActionPageDto;
|
||||
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.DigitalEmployeeAdminWorkbenchPageDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminWorkbenchRowDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeBusinessViewPageDto;
|
||||
@@ -18,7 +22,9 @@ import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncRecordDto
|
||||
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;
|
||||
import com.xspaceagi.agent.core.adapter.repository.DigitalEmployeeAdminActionRepository;
|
||||
import com.xspaceagi.agent.core.adapter.repository.DigitalEmployeeSyncRecordRepository;
|
||||
import com.xspaceagi.agent.core.adapter.repository.entity.DigitalEmployeeAdminAction;
|
||||
import com.xspaceagi.agent.core.adapter.repository.entity.DigitalEmployeeSyncRecord;
|
||||
import com.xspaceagi.system.spec.common.RequestContext;
|
||||
import jakarta.annotation.Resource;
|
||||
@@ -42,10 +48,26 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
|
||||
private static final long DEFAULT_PAGE_NO = 1L;
|
||||
private static final long DEFAULT_PAGE_SIZE = 20L;
|
||||
private static final long MAX_PAGE_SIZE = 100L;
|
||||
private static final Set<String> HIGH_RISK_ADMIN_ACTIONS = Set.of(
|
||||
"accept_remote_approval", "cleanup_file_batch", "start_service", "stop_service", "hard_interrupt"
|
||||
);
|
||||
private static final Set<String> LOW_RISK_ADMIN_ACTIONS = Set.of(
|
||||
"approval:keep_local", "approval:mark_reviewed",
|
||||
"route_decision:mark_reviewed", "route_decision:dismiss", "route_decision:retry_policy_check",
|
||||
"route:mark_reviewed", "route:dismiss", "route:retry_policy_check",
|
||||
"notification:mark_read", "notification:dismiss",
|
||||
"artifact:mark_keep", "artifact:mark_expire", "artifact:mark_reviewed",
|
||||
"daily_report:mark_delivered", "daily_report:mark_confirmed", "daily_report:request_approval",
|
||||
"audit:mark_reviewed", "audit:dismiss",
|
||||
"event:mark_reviewed", "event:dismiss"
|
||||
);
|
||||
|
||||
@Resource
|
||||
private DigitalEmployeeSyncRecordRepository digitalEmployeeSyncRecordRepository;
|
||||
|
||||
@Resource
|
||||
private DigitalEmployeeAdminActionRepository digitalEmployeeAdminActionRepository;
|
||||
|
||||
@Resource
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@@ -321,6 +343,151 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
|
||||
return adminWorkbenchPage(rows, currentPage, currentPageSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
@DSTransactional
|
||||
public DigitalEmployeeAdminActionResultDto createAdminAction(DigitalEmployeeAdminActionRequestDto request) {
|
||||
Date now = new Date();
|
||||
request = enrichAdminActionRequestFromSyncRecord(request);
|
||||
List<String> reasonCodes = validateAdminAction(request);
|
||||
boolean allowed = reasonCodes.isEmpty();
|
||||
if (allowed) {
|
||||
reasonCodes = List.of("action_recorded");
|
||||
}
|
||||
DigitalEmployeeAdminAction action = new DigitalEmployeeAdminAction();
|
||||
action.setTenantId(currentTenantId());
|
||||
action.setUserId(currentUserId());
|
||||
action.setClientKey(StringUtils.trimToNull(request == null ? null : request.getClientKey()));
|
||||
action.setDeviceId(StringUtils.trimToNull(request == null ? null : request.getDeviceId()));
|
||||
action.setEntityType(normalizedText(request == null ? null : request.getEntityType()));
|
||||
action.setEntityId(StringUtils.trimToNull(request == null ? null : request.getEntityId()));
|
||||
action.setAction(normalizedText(request == null ? null : request.getAction()));
|
||||
action.setStatus(allowed ? "pending" : "rejected");
|
||||
action.setReasonCodes(toJson(reasonCodes));
|
||||
action.setRequestPayload(toJson(request == null ? null : request.getRequestPayload()));
|
||||
action.setProcessedAt(allowed ? null : now);
|
||||
action.setModified(now);
|
||||
action.setCreated(now);
|
||||
digitalEmployeeAdminActionRepository.save(action);
|
||||
return DigitalEmployeeAdminActionResultDto.builder()
|
||||
.ok(allowed)
|
||||
.status(action.getStatus())
|
||||
.reasonCodes(reasonCodes)
|
||||
.action(toAdminActionDto(action))
|
||||
.build();
|
||||
}
|
||||
|
||||
private DigitalEmployeeAdminActionRequestDto enrichAdminActionRequestFromSyncRecord(DigitalEmployeeAdminActionRequestDto request) {
|
||||
if (request == null || request.getSyncRecordId() == null || StringUtils.isNotBlank(request.getClientKey())) {
|
||||
return request;
|
||||
}
|
||||
DigitalEmployeeSyncRecord record = digitalEmployeeSyncRecordRepository.getById(request.getSyncRecordId());
|
||||
if (record == null || currentTenantId() == null || !Objects.equals(record.getTenantId(), currentTenantId())) {
|
||||
return request;
|
||||
}
|
||||
request.setClientKey(record.getClientKey());
|
||||
if (StringUtils.isBlank(request.getDeviceId())) request.setDeviceId(record.getDeviceId());
|
||||
if (StringUtils.isBlank(request.getEntityType())) request.setEntityType(record.getEntityType());
|
||||
if (StringUtils.isBlank(request.getEntityId())) request.setEntityId(record.getEntityId());
|
||||
return request;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DigitalEmployeeAdminActionPageDto queryPendingAdminActions(String clientKey, String deviceId, 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 || StringUtils.isBlank(clientKey)) {
|
||||
return DigitalEmployeeAdminActionPageDto.builder()
|
||||
.total(0L)
|
||||
.pageNo(currentPage)
|
||||
.pageSize(currentPageSize)
|
||||
.records(List.of())
|
||||
.build();
|
||||
}
|
||||
IPage<DigitalEmployeeAdminAction> page = digitalEmployeeAdminActionRepository.queryActions(
|
||||
tenantId,
|
||||
StringUtils.trimToNull(clientKey),
|
||||
StringUtils.trimToNull(deviceId),
|
||||
"pending",
|
||||
currentPage,
|
||||
currentPageSize
|
||||
);
|
||||
return DigitalEmployeeAdminActionPageDto.builder()
|
||||
.total(page.getTotal())
|
||||
.pageNo(page.getCurrent())
|
||||
.pageSize(page.getSize())
|
||||
.records(page.getRecords().stream().map(this::toAdminActionDto).toList())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
@DSTransactional
|
||||
public DigitalEmployeeAdminActionResultDto markAdminActionResult(Long actionId, String status, List<String> reasonCodes) {
|
||||
Long tenantId = currentTenantId();
|
||||
DigitalEmployeeAdminAction action = actionId == null ? null : digitalEmployeeAdminActionRepository.getById(actionId);
|
||||
if (action == null || tenantId == null || !Objects.equals(action.getTenantId(), tenantId)) {
|
||||
return DigitalEmployeeAdminActionResultDto.builder()
|
||||
.ok(false)
|
||||
.status("not_found")
|
||||
.reasonCodes(List.of("admin_action_not_found"))
|
||||
.build();
|
||||
}
|
||||
String normalizedStatus = normalizedText(status);
|
||||
if (!Set.of("applied", "rejected", "failed").contains(normalizedStatus)) {
|
||||
normalizedStatus = "failed";
|
||||
}
|
||||
Date now = new Date();
|
||||
List<String> normalizedReasons = CollectionUtils.isEmpty(reasonCodes) ? List.of("admin_action_" + normalizedStatus) : reasonCodes;
|
||||
action.setStatus(normalizedStatus);
|
||||
action.setReasonCodes(toJson(normalizedReasons));
|
||||
action.setProcessedAt(now);
|
||||
action.setModified(now);
|
||||
digitalEmployeeAdminActionRepository.updateById(action);
|
||||
return DigitalEmployeeAdminActionResultDto.builder()
|
||||
.ok(true)
|
||||
.status(action.getStatus())
|
||||
.reasonCodes(normalizedReasons)
|
||||
.action(toAdminActionDto(action))
|
||||
.build();
|
||||
}
|
||||
|
||||
private List<String> validateAdminAction(DigitalEmployeeAdminActionRequestDto request) {
|
||||
if (request == null) return List.of("request_required");
|
||||
String clientKey = StringUtils.trimToNull(request.getClientKey());
|
||||
String entityType = normalizedText(request.getEntityType());
|
||||
String entityId = StringUtils.trimToNull(request.getEntityId());
|
||||
String action = normalizedText(request.getAction());
|
||||
if (StringUtils.isBlank(clientKey) || StringUtils.isBlank(entityType) || StringUtils.isBlank(entityId) || StringUtils.isBlank(action)) {
|
||||
return List.of("required_field_missing");
|
||||
}
|
||||
if (HIGH_RISK_ADMIN_ACTIONS.contains(action)) {
|
||||
return List.of("high_risk_action_disabled");
|
||||
}
|
||||
if (!LOW_RISK_ADMIN_ACTIONS.contains(entityType + ":" + action)) {
|
||||
return List.of("unsupported_admin_action");
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
|
||||
private DigitalEmployeeAdminActionDto toAdminActionDto(DigitalEmployeeAdminAction action) {
|
||||
if (action == null) return null;
|
||||
return DigitalEmployeeAdminActionDto.builder()
|
||||
.id(action.getId())
|
||||
.clientKeyMasked(maskClientKey(action.getClientKey()))
|
||||
.deviceId(action.getDeviceId())
|
||||
.entityType(action.getEntityType())
|
||||
.entityId(action.getEntityId())
|
||||
.action(action.getAction())
|
||||
.status(action.getStatus())
|
||||
.reasonCodes(readStringList(action.getReasonCodes()))
|
||||
.requestPayload(readJsonObject(action.getRequestPayload()))
|
||||
.createdAt(action.getCreated())
|
||||
.processedAt(action.getProcessedAt())
|
||||
.build();
|
||||
}
|
||||
|
||||
private DigitalEmployeeAdminWorkbenchPageDto adminWorkbenchPage(List<DigitalEmployeeAdminWorkbenchRowDto> 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());
|
||||
@@ -572,6 +739,18 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private String normalizedText(String value) {
|
||||
return StringUtils.trimToEmpty(value).toLowerCase().replace('-', '_');
|
||||
}
|
||||
|
||||
private Long currentTenantId() {
|
||||
return RequestContext.get() == null ? null : RequestContext.get().getTenantId();
|
||||
}
|
||||
|
||||
private Long currentUserId() {
|
||||
return RequestContext.get() == null ? null : RequestContext.get().getUserId();
|
||||
}
|
||||
|
||||
private String syncRecordUrl(String outboxId) {
|
||||
return "/system/content/content-digital-employee-sync?outbox_id=" + StringUtils.defaultString(outboxId);
|
||||
}
|
||||
@@ -593,6 +772,16 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> readStringList(String value) {
|
||||
if (StringUtils.isBlank(value)) return List.of();
|
||||
try {
|
||||
List<String> parsed = objectMapper.readValue(value, new TypeReference<>() {});
|
||||
return parsed == null ? List.of() : parsed;
|
||||
} catch (Exception e) {
|
||||
return List.of(value);
|
||||
}
|
||||
}
|
||||
|
||||
private String firstString(Object... values) {
|
||||
for (Object value : values) {
|
||||
String candidate = stringValue(value, null);
|
||||
|
||||
@@ -7,11 +7,17 @@ import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeBusinessViewP
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeBusinessViewRowDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminWorkbenchPageDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminWorkbenchRowDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminActionDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminActionPageDto;
|
||||
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.DigitalEmployeeSyncItemDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncRecordPageDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncRequestDto;
|
||||
import com.xspaceagi.agent.core.adapter.repository.DigitalEmployeeAdminActionRepository;
|
||||
import com.xspaceagi.agent.core.adapter.repository.DigitalEmployeeSyncRecordRepository;
|
||||
import com.xspaceagi.agent.core.adapter.repository.entity.DigitalEmployeeAdminAction;
|
||||
import com.xspaceagi.agent.core.adapter.repository.entity.DigitalEmployeeSyncRecord;
|
||||
import com.xspaceagi.system.spec.common.RequestContext;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
@@ -35,6 +41,7 @@ import static org.mockito.Mockito.when;
|
||||
class DigitalEmployeeSyncApplicationServiceImplTest {
|
||||
|
||||
private DigitalEmployeeSyncRecordRepository repository;
|
||||
private DigitalEmployeeAdminActionRepository adminActionRepository;
|
||||
private DigitalEmployeeSyncApplicationServiceImpl service;
|
||||
|
||||
@BeforeEach
|
||||
@@ -45,8 +52,10 @@ class DigitalEmployeeSyncApplicationServiceImplTest {
|
||||
RequestContext.set(context);
|
||||
|
||||
repository = mock(DigitalEmployeeSyncRecordRepository.class);
|
||||
adminActionRepository = mock(DigitalEmployeeAdminActionRepository.class);
|
||||
service = new DigitalEmployeeSyncApplicationServiceImpl();
|
||||
ReflectionTestUtils.setField(service, "digitalEmployeeSyncRecordRepository", repository);
|
||||
ReflectionTestUtils.setField(service, "digitalEmployeeAdminActionRepository", adminActionRepository);
|
||||
ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper());
|
||||
}
|
||||
|
||||
@@ -260,12 +269,111 @@ class DigitalEmployeeSyncApplicationServiceImplTest {
|
||||
assertThat(row.getPayload()).containsEntry("tool_name", "shell");
|
||||
}
|
||||
|
||||
@Test
|
||||
void createAdminActionRecordsLowRiskActionAsPending() {
|
||||
DigitalEmployeeAdminActionRequestDto request = adminActionRequest("approval", "approval-1", "keep_local");
|
||||
request.setRequestPayload(Map.of("reason", "protect_local_fact"));
|
||||
|
||||
DigitalEmployeeAdminActionResultDto result = service.createAdminAction(request);
|
||||
|
||||
ArgumentCaptor<DigitalEmployeeAdminAction> captor = ArgumentCaptor.forClass(DigitalEmployeeAdminAction.class);
|
||||
verify(adminActionRepository).save(captor.capture());
|
||||
DigitalEmployeeAdminAction action = captor.getValue();
|
||||
assertThat(result.getOk()).isTrue();
|
||||
assertThat(result.getStatus()).isEqualTo("pending");
|
||||
assertThat(action.getTenantId()).isEqualTo(101L);
|
||||
assertThat(action.getUserId()).isEqualTo(202L);
|
||||
assertThat(action.getClientKey()).isEqualTo("client-key-001");
|
||||
assertThat(action.getDeviceId()).isEqualTo("device-001");
|
||||
assertThat(action.getStatus()).isEqualTo("pending");
|
||||
assertThat(action.getReasonCodes()).contains("action_recorded");
|
||||
assertThat(action.getRequestPayload()).contains("protect_local_fact");
|
||||
}
|
||||
|
||||
@Test
|
||||
void createAdminActionRejectsHighRiskActionWithoutPendingExecution() {
|
||||
DigitalEmployeeAdminActionRequestDto request = adminActionRequest("artifact", "artifact-1", "cleanup_file_batch");
|
||||
|
||||
DigitalEmployeeAdminActionResultDto result = service.createAdminAction(request);
|
||||
|
||||
assertThat(result.getOk()).isFalse();
|
||||
assertThat(result.getStatus()).isEqualTo("rejected");
|
||||
assertThat(result.getReasonCodes()).contains("high_risk_action_disabled");
|
||||
ArgumentCaptor<DigitalEmployeeAdminAction> captor = ArgumentCaptor.forClass(DigitalEmployeeAdminAction.class);
|
||||
verify(adminActionRepository).save(captor.capture());
|
||||
assertThat(captor.getValue().getStatus()).isEqualTo("rejected");
|
||||
}
|
||||
|
||||
@Test
|
||||
void queryPendingAdminActionsFiltersTenantClientDeviceAndMasksKey() {
|
||||
when(adminActionRepository.queryActions(eq(101L), eq("client-key-001"), eq("device-001"), eq("pending"), eq(1L), eq(20L)))
|
||||
.thenReturn(adminActionPage(List.of(adminAction(10L, "approval", "approval-1", "keep_local", "pending"))));
|
||||
|
||||
DigitalEmployeeAdminActionPageDto result = service.queryPendingAdminActions("client-key-001", "device-001", 1L, 20L);
|
||||
|
||||
assertThat(result.getTotal()).isEqualTo(1L);
|
||||
DigitalEmployeeAdminActionDto action = result.getRecords().get(0);
|
||||
assertThat(action.getId()).isEqualTo(10L);
|
||||
assertThat(action.getClientKeyMasked()).isEqualTo("clie*********001");
|
||||
assertThat(action.getStatus()).isEqualTo("pending");
|
||||
assertThat(action.getReasonCodes()).contains("action_recorded");
|
||||
}
|
||||
|
||||
@Test
|
||||
void markAdminActionResultUpdatesStatusAndProcessedAt() {
|
||||
DigitalEmployeeAdminAction action = adminAction(10L, "notification", "notification-1", "dismiss", "pending");
|
||||
when(adminActionRepository.getById(10L)).thenReturn(action);
|
||||
|
||||
DigitalEmployeeAdminActionResultDto result = service.markAdminActionResult(10L, "applied", List.of("admin_action_applied"));
|
||||
|
||||
assertThat(result.getOk()).isTrue();
|
||||
assertThat(result.getStatus()).isEqualTo("applied");
|
||||
verify(adminActionRepository).updateById(action);
|
||||
assertThat(action.getStatus()).isEqualTo("applied");
|
||||
assertThat(action.getProcessedAt()).isNotNull();
|
||||
assertThat(action.getReasonCodes()).contains("admin_action_applied");
|
||||
}
|
||||
|
||||
private IPage<DigitalEmployeeSyncRecord> page(List<DigitalEmployeeSyncRecord> records) {
|
||||
Page<DigitalEmployeeSyncRecord> page = new Page<>(1, 20, records.size());
|
||||
page.setRecords(records);
|
||||
return page;
|
||||
}
|
||||
|
||||
private IPage<DigitalEmployeeAdminAction> adminActionPage(List<DigitalEmployeeAdminAction> records) {
|
||||
Page<DigitalEmployeeAdminAction> page = new Page<>(1, 20, records.size());
|
||||
page.setRecords(records);
|
||||
return page;
|
||||
}
|
||||
|
||||
private DigitalEmployeeAdminActionRequestDto adminActionRequest(String entityType, String entityId, String action) {
|
||||
DigitalEmployeeAdminActionRequestDto request = new DigitalEmployeeAdminActionRequestDto();
|
||||
request.setClientKey("client-key-001");
|
||||
request.setDeviceId("device-001");
|
||||
request.setEntityType(entityType);
|
||||
request.setEntityId(entityId);
|
||||
request.setAction(action);
|
||||
return request;
|
||||
}
|
||||
|
||||
private DigitalEmployeeAdminAction adminAction(Long id, String entityType, String entityId, String action, String status) {
|
||||
DigitalEmployeeAdminAction record = new DigitalEmployeeAdminAction();
|
||||
record.setId(id);
|
||||
record.setTenantId(101L);
|
||||
record.setUserId(202L);
|
||||
record.setClientKey("client-key-001");
|
||||
record.setDeviceId("device-001");
|
||||
record.setEntityType(entityType);
|
||||
record.setEntityId(entityId);
|
||||
record.setAction(action);
|
||||
record.setStatus(status);
|
||||
record.setReasonCodes("[\"action_recorded\"]");
|
||||
record.setRequestPayload("{\"reason\":\"protect_local_fact\"}");
|
||||
record.setCreated(new Date(1_000));
|
||||
record.setModified(new Date(1_000));
|
||||
return record;
|
||||
}
|
||||
|
||||
private DigitalEmployeeSyncRecord planRecord(String planId, String title, String status, Long tenantId, Date syncedAt) {
|
||||
return record("outbox-" + planId, "plan", planId, tenantId, syncedAt,
|
||||
Map.of("entity_type", "plan", "local_id", planId, "title", title, "status", status, "summary", "计划摘要"),
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.xspaceagi.agent.core.infra.dao.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.xspaceagi.agent.core.adapter.repository.entity.DigitalEmployeeAdminAction;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface DigitalEmployeeAdminActionMapper extends BaseMapper<DigitalEmployeeAdminAction> {
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.xspaceagi.agent.core.infra.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.xspaceagi.agent.core.adapter.repository.DigitalEmployeeAdminActionRepository;
|
||||
import com.xspaceagi.agent.core.adapter.repository.entity.DigitalEmployeeAdminAction;
|
||||
import com.xspaceagi.agent.core.infra.dao.mapper.DigitalEmployeeAdminActionMapper;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class DigitalEmployeeAdminActionRepositoryImpl
|
||||
extends ServiceImpl<DigitalEmployeeAdminActionMapper, DigitalEmployeeAdminAction>
|
||||
implements DigitalEmployeeAdminActionRepository {
|
||||
|
||||
@Override
|
||||
public IPage<DigitalEmployeeAdminAction> queryActions(
|
||||
Long tenantId,
|
||||
String clientKey,
|
||||
String deviceId,
|
||||
String status,
|
||||
long pageNo,
|
||||
long pageSize
|
||||
) {
|
||||
LambdaQueryWrapper<DigitalEmployeeAdminAction> queryWrapper = new LambdaQueryWrapper<DigitalEmployeeAdminAction>()
|
||||
.eq(tenantId != null, DigitalEmployeeAdminAction::getTenantId, tenantId)
|
||||
.eq(StringUtils.isNotBlank(clientKey), DigitalEmployeeAdminAction::getClientKey, clientKey)
|
||||
.eq(StringUtils.isNotBlank(deviceId), DigitalEmployeeAdminAction::getDeviceId, deviceId)
|
||||
.eq(StringUtils.isNotBlank(status), DigitalEmployeeAdminAction::getStatus, status)
|
||||
.orderByAsc(DigitalEmployeeAdminAction::getCreated)
|
||||
.orderByAsc(DigitalEmployeeAdminAction::getId);
|
||||
return page(new Page<>(pageNo, pageSize), queryWrapper);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.xspaceagi.agent.web.ui.controller;
|
||||
|
||||
import com.xspaceagi.agent.core.adapter.application.DigitalEmployeeSyncApplicationService;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminActionPageDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminActionRequestDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminActionResultDto;
|
||||
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.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
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.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static com.xspaceagi.system.spec.enums.ResourceEnum.CONTENT_DIGITAL_EMPLOYEE_OPS_QUERY_LIST;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/digital-employee/admin/actions")
|
||||
public class DigitalEmployeeAdminActionController {
|
||||
|
||||
private static final String CLIENT_KEY_HEADER = "X-Qiming-Client-Key";
|
||||
|
||||
@Resource
|
||||
private DigitalEmployeeSyncApplicationService digitalEmployeeSyncApplicationService;
|
||||
|
||||
@RequireResource(CONTENT_DIGITAL_EMPLOYEE_OPS_QUERY_LIST)
|
||||
@RequestMapping(method = RequestMethod.POST)
|
||||
public ReqResult<DigitalEmployeeAdminActionResultDto> createAction(
|
||||
@RequestBody(required = false) DigitalEmployeeAdminActionRequestDto request
|
||||
) {
|
||||
return ReqResult.success(digitalEmployeeSyncApplicationService.createAdminAction(request));
|
||||
}
|
||||
|
||||
@RequestMapping(path = "/pending", method = RequestMethod.GET)
|
||||
public ReqResult<DigitalEmployeeAdminActionPageDto> pendingActions(
|
||||
@RequestHeader(name = CLIENT_KEY_HEADER, required = false) String headerClientKey,
|
||||
@RequestParam(name = "client_key", required = false) String clientKey,
|
||||
@RequestParam(name = "device_id", required = false) String deviceId,
|
||||
@RequestParam(name = "page_no", required = false) Long pageNo,
|
||||
@RequestParam(name = "page_size", required = false) Long pageSize
|
||||
) {
|
||||
String normalizedClientKey = StringUtils.defaultIfBlank(clientKey, headerClientKey);
|
||||
if (StringUtils.isBlank(normalizedClientKey)) {
|
||||
return ReqResult.create("0001", "客户端 Key 缺失", null);
|
||||
}
|
||||
return ReqResult.success(digitalEmployeeSyncApplicationService.queryPendingAdminActions(
|
||||
normalizedClientKey.trim(),
|
||||
deviceId,
|
||||
pageNo,
|
||||
pageSize
|
||||
));
|
||||
}
|
||||
|
||||
@RequestMapping(path = "/{actionId}/result", method = RequestMethod.POST)
|
||||
public ReqResult<DigitalEmployeeAdminActionResultDto> markResult(
|
||||
@PathVariable("actionId") Long actionId,
|
||||
@RequestBody(required = false) Map<String, Object> body
|
||||
) {
|
||||
String status = body == null ? null : String.valueOf(body.get("status"));
|
||||
List<String> reasonCodes = reasonCodes(body == null ? null : body.get("reason_codes"));
|
||||
return ReqResult.success(digitalEmployeeSyncApplicationService.markAdminActionResult(actionId, status, reasonCodes));
|
||||
}
|
||||
|
||||
private List<String> reasonCodes(Object value) {
|
||||
if (value instanceof List<?> list) {
|
||||
return list.stream().map(String::valueOf).toList();
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user