吸收数字员工管理端低风险动作闭环

This commit is contained in:
baiyanyun
2026-06-12 21:17:51 +08:00
parent be901ee112
commit b3f21adecb
27 changed files with 1186 additions and 11 deletions

View File

@@ -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
);
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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
);
}

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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", "计划摘要"),

View File

@@ -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> {
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}