吸收数字员工管理端运营台后端
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
package com.xspaceagi.agent.core.adapter.application;
|
||||
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminWorkbenchPageDto;
|
||||
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;
|
||||
@@ -39,4 +40,21 @@ public interface DigitalEmployeeSyncApplicationService {
|
||||
String clientKey,
|
||||
String deviceId
|
||||
);
|
||||
|
||||
DigitalEmployeeAdminWorkbenchPageDto queryAdminWorkbench(
|
||||
String view,
|
||||
String clientKey,
|
||||
String deviceId,
|
||||
String entityType,
|
||||
String entityId,
|
||||
String status,
|
||||
String kind,
|
||||
String category,
|
||||
String level,
|
||||
String priority,
|
||||
Date updatedFrom,
|
||||
Date updatedTo,
|
||||
Long pageNo,
|
||||
Long pageSize
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 DigitalEmployeeAdminWorkbenchPageDto {
|
||||
private Long total;
|
||||
@JsonProperty("page_no")
|
||||
private Long pageNo;
|
||||
@JsonProperty("page_size")
|
||||
private Long pageSize;
|
||||
private List<DigitalEmployeeAdminWorkbenchRowDto> records;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
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.Map;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class DigitalEmployeeAdminWorkbenchRowDto {
|
||||
private String view;
|
||||
@JsonProperty("sync_record_id")
|
||||
private Long syncRecordId;
|
||||
@JsonProperty("sync_record_url")
|
||||
private String syncRecordUrl;
|
||||
@JsonProperty("entity_type")
|
||||
private String entityType;
|
||||
@JsonProperty("entity_id")
|
||||
private String entityId;
|
||||
private String operation;
|
||||
private String title;
|
||||
private String status;
|
||||
private String kind;
|
||||
private String category;
|
||||
private String level;
|
||||
private String priority;
|
||||
private String summary;
|
||||
@JsonProperty("plan_id")
|
||||
private String planId;
|
||||
@JsonProperty("task_id")
|
||||
private String taskId;
|
||||
@JsonProperty("run_id")
|
||||
private String runId;
|
||||
@JsonProperty("approval_id")
|
||||
private String approvalId;
|
||||
@JsonProperty("artifact_id")
|
||||
private String artifactId;
|
||||
@JsonProperty("notification_id")
|
||||
private String notificationId;
|
||||
@JsonProperty("report_id")
|
||||
private String reportId;
|
||||
@JsonProperty("route_decision_id")
|
||||
private String routeDecisionId;
|
||||
@JsonProperty("occurred_at")
|
||||
private String occurredAt;
|
||||
@JsonProperty("synced_at")
|
||||
private Date syncedAt;
|
||||
@JsonProperty("client_key_masked")
|
||||
private String clientKeyMasked;
|
||||
@JsonProperty("device_id")
|
||||
private String deviceId;
|
||||
@JsonProperty("business_view")
|
||||
private Map<String, Object> businessView;
|
||||
private Map<String, Object> payload;
|
||||
}
|
||||
@@ -7,6 +7,8 @@ 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.DigitalEmployeeAdminWorkbenchPageDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminWorkbenchRowDto;
|
||||
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;
|
||||
@@ -32,6 +34,7 @@ import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
@Service
|
||||
public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmployeeSyncApplicationService {
|
||||
@@ -267,6 +270,197 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public DigitalEmployeeAdminWorkbenchPageDto queryAdminWorkbench(
|
||||
String view,
|
||||
String clientKey,
|
||||
String deviceId,
|
||||
String entityType,
|
||||
String entityId,
|
||||
String status,
|
||||
String kind,
|
||||
String category,
|
||||
String level,
|
||||
String priority,
|
||||
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 = RequestContext.get() == null ? null : RequestContext.get().getTenantId();
|
||||
if (tenantId == null) {
|
||||
return adminWorkbenchPage(List.of(), currentPage, currentPageSize);
|
||||
}
|
||||
|
||||
String normalizedView = normalizeAdminView(view);
|
||||
IPage<DigitalEmployeeSyncRecord> page = digitalEmployeeSyncRecordRepository.queryBusinessRecords(
|
||||
tenantId,
|
||||
clientKey,
|
||||
deviceId,
|
||||
StringUtils.trimToNull(entityType),
|
||||
entityId,
|
||||
updatedFrom,
|
||||
updatedTo,
|
||||
null,
|
||||
currentPage,
|
||||
currentPageSize
|
||||
);
|
||||
List<DigitalEmployeeAdminWorkbenchRowDto> rows = page.getRecords().stream()
|
||||
.map(record -> toAdminWorkbenchRow(record, normalizedView))
|
||||
.filter(row -> matchesAdminView(normalizedView, row))
|
||||
.filter(row -> matchesText(status, row.getStatus()))
|
||||
.filter(row -> matchesText(kind, row.getKind()))
|
||||
.filter(row -> matchesText(category, row.getCategory()))
|
||||
.filter(row -> matchesText(level, row.getLevel()))
|
||||
.filter(row -> matchesText(priority, row.getPriority()))
|
||||
.toList();
|
||||
return adminWorkbenchPage(rows, currentPage, currentPageSize);
|
||||
}
|
||||
|
||||
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());
|
||||
return DigitalEmployeeAdminWorkbenchPageDto.builder()
|
||||
.total((long) rows.size())
|
||||
.pageNo(pageNo)
|
||||
.pageSize(pageSize)
|
||||
.records(rows.subList(fromIndex, toIndex))
|
||||
.build();
|
||||
}
|
||||
|
||||
private DigitalEmployeeAdminWorkbenchRowDto toAdminWorkbenchRow(DigitalEmployeeSyncRecord record, String view) {
|
||||
DigitalEmployeeBusinessViewRowDto businessRow = toBusinessRow(record);
|
||||
Map<String, Object> businessView = readJsonObject(record.getBusinessView());
|
||||
Map<String, Object> payload = readJsonObject(record.getPayload());
|
||||
String category = adminCategory(businessRow, businessView, payload);
|
||||
return DigitalEmployeeAdminWorkbenchRowDto.builder()
|
||||
.view(view)
|
||||
.syncRecordId(record.getId())
|
||||
.syncRecordUrl(syncRecordUrl(record.getOutboxId()))
|
||||
.entityType(businessRow.getEntityType())
|
||||
.entityId(businessRow.getEntityId())
|
||||
.operation(record.getOperation())
|
||||
.title(businessRow.getTitle())
|
||||
.status(businessRow.getStatus())
|
||||
.kind(businessRow.getKind())
|
||||
.category(category)
|
||||
.level(firstString(businessView.get("level"), payload.get("level")))
|
||||
.priority(firstString(businessView.get("priority"), payload.get("priority")))
|
||||
.summary(firstString(businessRow.getSummary(), businessView.get("reason"), payload.get("reason")))
|
||||
.planId(businessRow.getPlanId())
|
||||
.taskId(businessRow.getTaskId())
|
||||
.runId(businessRow.getRunId())
|
||||
.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))
|
||||
.notificationId(firstString(businessView.get("notification_id"), payload.get("notification_id")))
|
||||
.reportId(firstString(businessView.get("report_id"), payload.get("report_id")))
|
||||
.routeDecisionId(firstString(businessView.get("route_decision_id"), payload.get("route_decision_id")))
|
||||
.occurredAt(businessRow.getOccurredAt())
|
||||
.syncedAt(record.getSyncedAt())
|
||||
.clientKeyMasked(maskClientKey(record.getClientKey()))
|
||||
.deviceId(record.getDeviceId())
|
||||
.businessView(businessView)
|
||||
.payload(redactedPayload(payload))
|
||||
.build();
|
||||
}
|
||||
|
||||
private String normalizeAdminView(String view) {
|
||||
String normalized = StringUtils.trimToEmpty(view).toLowerCase().replace('-', '_');
|
||||
return StringUtils.isBlank(normalized) ? "business" : normalized;
|
||||
}
|
||||
|
||||
private boolean matchesAdminView(String view, DigitalEmployeeAdminWorkbenchRowDto row) {
|
||||
return switch (view) {
|
||||
case "interventions" -> isInterventionRow(row);
|
||||
case "artifact_lifecycle" -> isArtifactLifecycleRow(row);
|
||||
case "notifications" -> isNotificationRow(row);
|
||||
case "audits" -> isAuditRow(row);
|
||||
case "daily_reports" -> isDailyReportRow(row);
|
||||
case "route_governance" -> isRouteGovernanceRow(row);
|
||||
default -> true;
|
||||
};
|
||||
}
|
||||
|
||||
private boolean isInterventionRow(DigitalEmployeeAdminWorkbenchRowDto row) {
|
||||
return StringUtils.equals(row.getCategory(), "approval_conflict")
|
||||
|| StringUtils.equals(row.getCategory(), "route_intervention")
|
||||
|| StringUtils.equals(row.getCategory(), "risk_approval")
|
||||
|| StringUtils.equals(row.getCategory(), "action_notification");
|
||||
}
|
||||
|
||||
private boolean isArtifactLifecycleRow(DigitalEmployeeAdminWorkbenchRowDto row) {
|
||||
return StringUtils.equals(row.getEntityType(), "artifact")
|
||||
|| startsWithAny(row.getKind(), "artifact_")
|
||||
|| StringUtils.equals(row.getCategory(), "artifact_lifecycle");
|
||||
}
|
||||
|
||||
private boolean isNotificationRow(DigitalEmployeeAdminWorkbenchRowDto row) {
|
||||
return StringUtils.equals(row.getCategory(), "notification")
|
||||
|| StringUtils.equals(row.getCategory(), "action_notification")
|
||||
|| contains(row.getKind(), "notification")
|
||||
|| StringUtils.isNotBlank(row.getNotificationId());
|
||||
}
|
||||
|
||||
private boolean isAuditRow(DigitalEmployeeAdminWorkbenchRowDto row) {
|
||||
return Set.of("sandbox_audit", "token_cost", "tool_call_audit", "governance", "sync_failure").contains(row.getCategory())
|
||||
|| contains(row.getKind(), "audit")
|
||||
|| contains(row.getKind(), "cost");
|
||||
}
|
||||
|
||||
private boolean isDailyReportRow(DigitalEmployeeAdminWorkbenchRowDto row) {
|
||||
return StringUtils.equals(row.getCategory(), "daily_report")
|
||||
|| startsWithAny(row.getKind(), "daily_report")
|
||||
|| StringUtils.isNotBlank(row.getReportId());
|
||||
}
|
||||
|
||||
private boolean isRouteGovernanceRow(DigitalEmployeeAdminWorkbenchRowDto row) {
|
||||
return StringUtils.equals(row.getCategory(), "route_governance")
|
||||
|| StringUtils.equals(row.getCategory(), "route_intervention")
|
||||
|| StringUtils.equals(row.getCategory(), "governance")
|
||||
|| startsWithAny(row.getKind(), "route_", "governance_")
|
||||
|| StringUtils.isNotBlank(row.getRouteDecisionId());
|
||||
}
|
||||
|
||||
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 (contains(kind, "route_decision_blocked") || contains(kind, "route_decision_approval_required")
|
||||
|| contains(kind, "route_action_missing_context") || contains(kind, "route_action_not_mapped")) {
|
||||
return "route_intervention";
|
||||
}
|
||||
String category = firstString(businessView.get("category"), payload.get("category"));
|
||||
if (StringUtils.isNotBlank(category)) return category;
|
||||
if (contains(kind, "risk_approval")) return "risk_approval";
|
||||
if (contains(kind, "notification") || StringUtils.isNotBlank(firstString(businessView.get("notification_id"), payload.get("notification_id")))) {
|
||||
String level = firstString(businessView.get("level"), payload.get("level"));
|
||||
return StringUtils.equals(level, "action") ? "action_notification" : "notification";
|
||||
}
|
||||
if (startsWithAny(kind, "artifact_")) return "artifact_lifecycle";
|
||||
if (startsWithAny(kind, "daily_report")) return "daily_report";
|
||||
if (startsWithAny(kind, "governance_")) return "governance";
|
||||
if (contains(kind, "audit")) return kind;
|
||||
return row.getEntityType();
|
||||
}
|
||||
|
||||
private Map<String, Object> redactedPayload(Map<String, Object> payload) {
|
||||
Map<String, Object> redacted = new LinkedHashMap<>();
|
||||
payload.forEach((key, value) -> {
|
||||
if (!isSensitivePayloadKey(key)) {
|
||||
redacted.put(key, value);
|
||||
}
|
||||
});
|
||||
return redacted;
|
||||
}
|
||||
|
||||
private boolean isSensitivePayloadKey(String key) {
|
||||
String normalized = StringUtils.trimToEmpty(key).toLowerCase();
|
||||
return Set.of("token", "authorization", "raw_input", "command", "prompt", "raw_prompt", "input").contains(normalized);
|
||||
}
|
||||
|
||||
private List<DigitalEmployeeBusinessViewRowDto> rowsByType(List<DigitalEmployeeBusinessViewRowDto> rows, String entityType) {
|
||||
return rows.stream().filter(row -> StringUtils.equals(row.getEntityType(), entityType)).toList();
|
||||
}
|
||||
@@ -353,6 +547,25 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
|
||||
return StringUtils.isBlank(expected) || StringUtils.equals(expected, actual);
|
||||
}
|
||||
|
||||
private boolean booleanValue(Object value) {
|
||||
if (value instanceof Boolean bool) return bool;
|
||||
if (value instanceof String text) return Boolean.parseBoolean(text.trim());
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean contains(String value, String fragment) {
|
||||
return StringUtils.containsIgnoreCase(value, fragment);
|
||||
}
|
||||
|
||||
private boolean startsWithAny(String value, String... prefixes) {
|
||||
if (StringUtils.isBlank(value)) return false;
|
||||
String normalized = value.toLowerCase();
|
||||
for (String prefix : prefixes) {
|
||||
if (normalized.startsWith(prefix)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private String normalizeBusinessEntityType(String value) {
|
||||
String normalized = StringUtils.trimToEmpty(value).toLowerCase();
|
||||
if (normalized.endsWith("s")) normalized = normalized.substring(0, normalized.length() - 1);
|
||||
|
||||
@@ -5,6 +5,8 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
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.DigitalEmployeeAdminWorkbenchPageDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminWorkbenchRowDto;
|
||||
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;
|
||||
@@ -177,6 +179,87 @@ class DigitalEmployeeSyncApplicationServiceImplTest {
|
||||
verify(repository, never()).queryBusinessRecords(any(), any(), any(), any(), any(), any(), any(), any(), any(Long.class), any(Long.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void queryAdminWorkbenchInterventionsReturnsConflictAndRouteRowsOnly() {
|
||||
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-approval-conflict", "approval", "approval-1", 101L, new Date(10_000),
|
||||
Map.of(
|
||||
"entity_type", "approval",
|
||||
"title", "远端审批冲突",
|
||||
"status", "pending",
|
||||
"approval_conflict_active", true,
|
||||
"approval_conflict_reason", "terminal_status_mismatch",
|
||||
"approval_id", "approval-1",
|
||||
"plan_id", "plan-1",
|
||||
"priority", "high"
|
||||
),
|
||||
Map.of("id", "approval-1", "status", "pending")),
|
||||
record("outbox-route-blocked", "event", "event-1", 101L, new Date(11_000),
|
||||
Map.of(
|
||||
"entity_type", "event",
|
||||
"category", "route_governance",
|
||||
"kind", "route_decision_blocked",
|
||||
"status", "blocked",
|
||||
"title", "路由策略阻断",
|
||||
"route_decision_id", "route-1",
|
||||
"reason", "route_action_policy_blocked"
|
||||
),
|
||||
Map.of("id", "event-1", "kind", "route_decision_blocked")),
|
||||
record("outbox-approval-normal", "approval", "approval-2", 101L, new Date(12_000),
|
||||
Map.of("entity_type", "approval", "title", "普通审批", "status", "approved"),
|
||||
Map.of("id", "approval-2", "status", "approved"))
|
||||
)));
|
||||
|
||||
DigitalEmployeeAdminWorkbenchPageDto result = service.queryAdminWorkbench(
|
||||
"interventions", 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("approval-1", "event-1");
|
||||
assertThat(result.getRecords().get(0).getView()).isEqualTo("interventions");
|
||||
assertThat(result.getRecords().get(0).getCategory()).isEqualTo("approval_conflict");
|
||||
assertThat(result.getRecords().get(0).getApprovalId()).isEqualTo("approval-1");
|
||||
assertThat(result.getRecords().get(1).getRouteDecisionId()).isEqualTo("route-1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void queryAdminWorkbenchAuditsRedactsSensitivePayloadFields() {
|
||||
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-audit", "event", "event-audit", 101L, new Date(10_000),
|
||||
Map.of(
|
||||
"entity_type", "event",
|
||||
"category", "tool_call_audit",
|
||||
"kind", "tool_call_audit",
|
||||
"status", "rejected",
|
||||
"level", "warn",
|
||||
"tool_name", "shell",
|
||||
"server_id", "local"
|
||||
),
|
||||
Map.of(
|
||||
"id", "event-audit",
|
||||
"token", "secret-token",
|
||||
"authorization", "Bearer secret",
|
||||
"raw_input", "full prompt",
|
||||
"command", "cat /private/key",
|
||||
"prompt", "sensitive prompt",
|
||||
"tool_name", "shell"
|
||||
))
|
||||
)));
|
||||
|
||||
DigitalEmployeeAdminWorkbenchPageDto result = service.queryAdminWorkbench(
|
||||
"audits", null, null, null, null, null, null, null, null, null, null, null, 1L, 20L
|
||||
);
|
||||
|
||||
assertThat(result.getTotal()).isEqualTo(1);
|
||||
DigitalEmployeeAdminWorkbenchRowDto row = result.getRecords().get(0);
|
||||
assertThat(row.getCategory()).isEqualTo("tool_call_audit");
|
||||
assertThat(row.getPayload()).doesNotContainKeys("token", "authorization", "raw_input", "command", "prompt");
|
||||
assertThat(row.getPayload()).containsEntry("tool_name", "shell");
|
||||
}
|
||||
|
||||
private IPage<DigitalEmployeeSyncRecord> page(List<DigitalEmployeeSyncRecord> records) {
|
||||
Page<DigitalEmployeeSyncRecord> page = new Page<>(1, 20, records.size());
|
||||
page.setRecords(records);
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package com.xspaceagi.agent.web.ui.controller;
|
||||
|
||||
import com.xspaceagi.agent.core.adapter.application.DigitalEmployeeSyncApplicationService;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminWorkbenchPageDto;
|
||||
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.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/workbench")
|
||||
public class DigitalEmployeeAdminWorkbenchController {
|
||||
|
||||
@Resource
|
||||
private DigitalEmployeeSyncApplicationService digitalEmployeeSyncApplicationService;
|
||||
|
||||
@RequireResource(CONTENT_DIGITAL_EMPLOYEE_OPS_QUERY_LIST)
|
||||
@RequestMapping(path = "/{view}", method = RequestMethod.GET)
|
||||
public ReqResult<DigitalEmployeeAdminWorkbenchPageDto> queryWorkbench(
|
||||
@PathVariable("view") String view,
|
||||
@RequestParam(name = "client_key", required = false) String clientKey,
|
||||
@RequestParam(name = "device_id", required = false) String deviceId,
|
||||
@RequestParam(name = "entity_type", required = false) String entityType,
|
||||
@RequestParam(name = "entity_id", required = false) String entityId,
|
||||
@RequestParam(name = "status", required = false) String status,
|
||||
@RequestParam(name = "kind", required = false) String kind,
|
||||
@RequestParam(name = "category", required = false) String category,
|
||||
@RequestParam(name = "level", required = false) String level,
|
||||
@RequestParam(name = "priority", required = false) String priority,
|
||||
@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.queryAdminWorkbench(
|
||||
view,
|
||||
clientKey,
|
||||
deviceId,
|
||||
entityType,
|
||||
entityId,
|
||||
status,
|
||||
kind,
|
||||
category,
|
||||
level,
|
||||
priority,
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -422,6 +422,11 @@ public enum ResourceEnum {
|
||||
|
||||
CONTENT_DIGITAL_EMPLOYEE_SYNC_QUERY_LIST(ResourceTypeEnum.OPERATION, "content_digital_employee_sync_query_list", "查询列表", "content_digital_employee_sync"),
|
||||
|
||||
// 2级模块 - 数字员工运营台
|
||||
CONTENT_DIGITAL_EMPLOYEE_OPS(ResourceTypeEnum.MODULE, "content_digital_employee_ops", "数字员工运营台", "content_manage"),
|
||||
|
||||
CONTENT_DIGITAL_EMPLOYEE_OPS_QUERY_LIST(ResourceTypeEnum.OPERATION, "content_digital_employee_ops_query_list", "查询列表", "content_digital_employee_ops"),
|
||||
|
||||
// ================== 多语言管理模块 ==================
|
||||
I18N_LANG_MANAGE(ResourceTypeEnum.MODULE, "i18n_lang_manage", "多语言管理模块", "root"),
|
||||
I18N_LANG_QUERY(ResourceTypeEnum.OPERATION, "i18n_lang_query", "查询", "i18n_lang_manage"),
|
||||
|
||||
@@ -2,3 +2,79 @@ ALTER TABLE `digital_employee_sync_record`
|
||||
ADD COLUMN `contract_version` VARCHAR(128) DEFAULT NULL COMMENT '客户端同步契约版本' AFTER `operation`,
|
||||
ADD COLUMN `entity_summary` JSON DEFAULT NULL COMMENT '客户端实体摘要' AFTER `contract_version`,
|
||||
ADD COLUMN `business_view` JSON DEFAULT NULL COMMENT '客户端业务视图投影' AFTER `entity_summary`;
|
||||
|
||||
-- Add digital employee admin operations workbench menu and resource.
|
||||
-- Idempotent for existing tenants: creates menu/resource rows, binds menu to resource,
|
||||
-- and grants super_admin default access. Other roles can be granted this menu
|
||||
-- from the menu permission management UI.
|
||||
|
||||
INSERT INTO sys_resource (`parent_id`, `code`, `name`, `description`, `source`, `type`, `sort_index`, `status`, `_tenant_id`, `creator_id`, `creator`, `yn`)
|
||||
SELECT parent.id, 'content_digital_employee_ops', '数字员工运营台', '数字员工运营台', 1, 1, 91, 1, parent.`_tenant_id`, 0, 'system', 1
|
||||
FROM sys_resource parent
|
||||
WHERE parent.`code` = 'content_manage' AND parent.`yn` = 1
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM sys_resource r
|
||||
WHERE r.`_tenant_id` = parent.`_tenant_id` AND r.`code` = 'content_digital_employee_ops' AND r.`yn` = 1
|
||||
);
|
||||
|
||||
INSERT INTO sys_resource (`parent_id`, `code`, `name`, `description`, `source`, `type`, `sort_index`, `status`, `_tenant_id`, `creator_id`, `creator`, `yn`)
|
||||
SELECT parent.id, 'content_digital_employee_ops_query_list', '查询列表', '查询数字员工运营台', 1, 2, 1, 1, parent.`_tenant_id`, 0, 'system', 1
|
||||
FROM sys_resource parent
|
||||
WHERE parent.`code` = 'content_digital_employee_ops' AND parent.`yn` = 1
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM sys_resource r
|
||||
WHERE r.`_tenant_id` = parent.`_tenant_id` AND r.`code` = 'content_digital_employee_ops_query_list' AND r.`yn` = 1
|
||||
);
|
||||
|
||||
INSERT INTO sys_menu (`parent_id`, `code`, `name`, `description`, `source`, `path`, `open_type`, `icon`, `sort_index`, `status`, `_tenant_id`, `creator_id`, `creator`, `yn`)
|
||||
SELECT parent.id, 'content_digital_employee_ops', '数字员工运营台', '数字员工运营台', 1, '/system/content/content-digital-employee-ops', 1, '', 91, 1, parent.`_tenant_id`, 0, 'system', 1
|
||||
FROM sys_menu parent
|
||||
WHERE parent.`code` = 'content_manage' AND parent.`yn` = 1
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM sys_menu m
|
||||
WHERE m.`_tenant_id` = parent.`_tenant_id` AND m.`code` = 'content_digital_employee_ops' AND m.`yn` = 1
|
||||
);
|
||||
|
||||
INSERT INTO sys_menu_resource (`menu_id`, `resource_id`, `resource_bind_type`, `_tenant_id`, `creator_id`, `creator`, `yn`)
|
||||
SELECT m.id, r.id, 1, m.`_tenant_id`, 0, 'system', 1
|
||||
FROM sys_menu m
|
||||
JOIN sys_resource r ON r.`_tenant_id` = m.`_tenant_id` AND r.`code` = 'content_digital_employee_ops' AND r.`yn` = 1
|
||||
WHERE m.`code` = 'content_digital_employee_ops' AND m.`yn` = 1
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM sys_menu_resource mr
|
||||
WHERE mr.`_tenant_id` = m.`_tenant_id` AND mr.`menu_id` = m.id AND mr.`resource_id` = r.id AND mr.`yn` = 1
|
||||
);
|
||||
|
||||
UPDATE sys_menu_resource mr
|
||||
JOIN sys_menu m ON m.id = mr.menu_id AND m.`_tenant_id` = mr.`_tenant_id` AND m.`code` = 'content_digital_employee_ops' AND m.`yn` = 1
|
||||
JOIN sys_resource r ON r.id = mr.resource_id AND r.`_tenant_id` = mr.`_tenant_id` AND r.`code` = 'content_digital_employee_ops' AND r.`yn` = 1
|
||||
SET mr.`resource_bind_type` = 1,
|
||||
mr.`modified` = CURRENT_TIMESTAMP
|
||||
WHERE mr.`yn` = 1;
|
||||
|
||||
INSERT INTO sys_role_menu (`role_id`, `menu_id`, `menu_bind_type`, `resource_tree_json`, `_tenant_id`, `creator_id`, `creator`, `yn`)
|
||||
SELECT role.id,
|
||||
menu.id,
|
||||
1,
|
||||
JSON_ARRAY(JSON_OBJECT('id', resource.id, 'code', resource.`code`, 'resourceBindType', 1)),
|
||||
menu.`_tenant_id`,
|
||||
0,
|
||||
'system',
|
||||
1
|
||||
FROM sys_role role
|
||||
JOIN sys_menu menu ON menu.`_tenant_id` = role.`_tenant_id` AND menu.`code` = 'content_digital_employee_ops' AND menu.`yn` = 1
|
||||
JOIN sys_resource resource ON resource.`_tenant_id` = role.`_tenant_id` AND resource.`code` = 'content_digital_employee_ops' AND resource.`yn` = 1
|
||||
WHERE role.`code` = 'super_admin' AND role.`yn` = 1
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM sys_role_menu rm
|
||||
WHERE rm.`_tenant_id` = role.`_tenant_id` AND rm.`role_id` = role.id AND rm.`menu_id` = menu.id AND rm.`yn` = 1
|
||||
);
|
||||
|
||||
UPDATE sys_role_menu rm
|
||||
JOIN sys_role role ON role.id = rm.role_id AND role.`_tenant_id` = rm.`_tenant_id` AND role.`code` = 'super_admin' AND role.`yn` = 1
|
||||
JOIN sys_menu menu ON menu.id = rm.menu_id AND menu.`_tenant_id` = rm.`_tenant_id` AND menu.`code` = 'content_digital_employee_ops' AND menu.`yn` = 1
|
||||
JOIN sys_resource resource ON resource.`_tenant_id` = rm.`_tenant_id` AND resource.`code` = 'content_digital_employee_ops' AND resource.`yn` = 1
|
||||
SET rm.`menu_bind_type` = 1,
|
||||
rm.`resource_tree_json` = JSON_ARRAY(JSON_OBJECT('id', resource.id, 'code', resource.`code`, 'resourceBindType', 1)),
|
||||
rm.`modified` = CURRENT_TIMESTAMP
|
||||
WHERE rm.`yn` = 1;
|
||||
|
||||
Reference in New Issue
Block a user