diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/application/DigitalEmployeeSyncApplicationService.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/application/DigitalEmployeeSyncApplicationService.java index af8d618d..f936994a 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/application/DigitalEmployeeSyncApplicationService.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/application/DigitalEmployeeSyncApplicationService.java @@ -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 + ); } diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/digital/DigitalEmployeeAdminWorkbenchPageDto.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/digital/DigitalEmployeeAdminWorkbenchPageDto.java new file mode 100644 index 00000000..ce99fbea --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/digital/DigitalEmployeeAdminWorkbenchPageDto.java @@ -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 records; +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/digital/DigitalEmployeeAdminWorkbenchRowDto.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/digital/DigitalEmployeeAdminWorkbenchRowDto.java new file mode 100644 index 00000000..5af3497e --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/digital/DigitalEmployeeAdminWorkbenchRowDto.java @@ -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 businessView; + private Map payload; +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/DigitalEmployeeSyncApplicationServiceImpl.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/DigitalEmployeeSyncApplicationServiceImpl.java index 28f8a0bf..9ad3eb4b 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/DigitalEmployeeSyncApplicationServiceImpl.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/DigitalEmployeeSyncApplicationServiceImpl.java @@ -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 page = digitalEmployeeSyncRecordRepository.queryBusinessRecords( + tenantId, + clientKey, + deviceId, + StringUtils.trimToNull(entityType), + entityId, + updatedFrom, + updatedTo, + null, + currentPage, + currentPageSize + ); + List 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 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 businessView = readJsonObject(record.getBusinessView()); + Map 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 businessView, Map 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 redactedPayload(Map payload) { + Map 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 rowsByType(List 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); diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/test/java/com/xspaceagi/agent/core/application/service/DigitalEmployeeSyncApplicationServiceImplTest.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/test/java/com/xspaceagi/agent/core/application/service/DigitalEmployeeSyncApplicationServiceImplTest.java index fc9c9a40..0e9e6c28 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/test/java/com/xspaceagi/agent/core/application/service/DigitalEmployeeSyncApplicationServiceImplTest.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/test/java/com/xspaceagi/agent/core/application/service/DigitalEmployeeSyncApplicationServiceImplTest.java @@ -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 page(List records) { Page page = new Page<>(1, 20, records.size()); page.setRecords(records); diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/DigitalEmployeeAdminWorkbenchController.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/DigitalEmployeeAdminWorkbenchController.java new file mode 100644 index 00000000..2343b8e8 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/DigitalEmployeeAdminWorkbenchController.java @@ -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 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; + } + } + } +} diff --git a/qiming-backend/app-platform-modules/platform-system/system-spec/src/main/java/com/xspaceagi/system/spec/enums/ResourceEnum.java b/qiming-backend/app-platform-modules/platform-system/system-spec/src/main/java/com/xspaceagi/system/spec/enums/ResourceEnum.java index a585021d..c990f890 100644 --- a/qiming-backend/app-platform-modules/platform-system/system-spec/src/main/java/com/xspaceagi/system/spec/enums/ResourceEnum.java +++ b/qiming-backend/app-platform-modules/platform-system/system-spec/src/main/java/com/xspaceagi/system/spec/enums/ResourceEnum.java @@ -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"), diff --git a/qiming-backend/sql/update-20260612.sql b/qiming-backend/sql/update-20260612.sql index 2f0a94ec..749b5252 100644 --- a/qiming-backend/sql/update-20260612.sql +++ b/qiming-backend/sql/update-20260612.sql @@ -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;