From fffa0ebe50046c1e3a90cb019c2b34964cbe64ef Mon Sep 17 00:00:00 2001 From: baiyanyun Date: Fri, 12 Jun 2026 11:21:43 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=B8=E6=94=B6=E6=95=B0=E5=AD=97=E5=91=98?= =?UTF-8?q?=E5=B7=A5=E7=AE=A1=E7=90=86=E7=AB=AF=E4=B8=9A=E5=8A=A1=E6=9F=A5?= =?UTF-8?q?=E8=AF=A2=E6=A8=A1=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...DigitalEmployeeSyncApplicationService.java | 23 ++ .../DigitalEmployeeBusinessViewPageDto.java | 18 ++ .../DigitalEmployeeBusinessViewRowDto.java | 61 ++++ ...italEmployeePlanBusinessViewDetailDto.java | 17 ++ .../digital/DigitalEmployeeSyncItemDto.java | 6 + .../digital/DigitalEmployeeSyncRecordDto.java | 6 + .../DigitalEmployeeSyncRecordRepository.java | 15 + .../entity/DigitalEmployeeSyncRecord.java | 3 + ...talEmployeeSyncApplicationServiceImpl.java | 268 ++++++++++++++++++ ...mployeeSyncApplicationServiceImplTest.java | 250 ++++++++++++++++ ...gitalEmployeeSyncRecordRepositoryImpl.java | 28 ++ ...DigitalEmployeeBusinessViewController.java | 177 ++++++++++++ qiming-backend/sql/update-20260612.sql | 4 + ...ital-employee-frontend-integration-plan.md | 6 +- 14 files changed, 879 insertions(+), 3 deletions(-) create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/digital/DigitalEmployeeBusinessViewPageDto.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/digital/DigitalEmployeeBusinessViewRowDto.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/digital/DigitalEmployeePlanBusinessViewDetailDto.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/test/java/com/xspaceagi/agent/core/application/service/DigitalEmployeeSyncApplicationServiceImplTest.java create mode 100644 qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/DigitalEmployeeBusinessViewController.java create mode 100644 qiming-backend/sql/update-20260612.sql 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 f10c2a74..af8d618d 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,9 +1,13 @@ package com.xspaceagi.agent.core.adapter.application; +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; import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncRequestDto; import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncResponseDto; +import java.util.Date; + public interface DigitalEmployeeSyncApplicationService { DigitalEmployeeSyncResponseDto syncOutbox(DigitalEmployeeSyncRequestDto request); @@ -16,4 +20,23 @@ public interface DigitalEmployeeSyncApplicationService { Long pageNo, Long pageSize ); + + DigitalEmployeeBusinessViewPageDto queryBusinessViews( + String entityType, + String clientKey, + String deviceId, + String entityId, + String status, + String syncStatus, + Date updatedFrom, + Date updatedTo, + Long pageNo, + Long pageSize + ); + + DigitalEmployeePlanBusinessViewDetailDto queryPlanBusinessViewDetail( + String planId, + String clientKey, + String deviceId + ); } 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/DigitalEmployeeBusinessViewPageDto.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/DigitalEmployeeBusinessViewPageDto.java new file mode 100644 index 00000000..c284283d --- /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/DigitalEmployeeBusinessViewPageDto.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 DigitalEmployeeBusinessViewPageDto { + 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/DigitalEmployeeBusinessViewRowDto.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/DigitalEmployeeBusinessViewRowDto.java new file mode 100644 index 00000000..0fb100a6 --- /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/DigitalEmployeeBusinessViewRowDto.java @@ -0,0 +1,61 @@ +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 DigitalEmployeeBusinessViewRowDto { + @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 summary; + @JsonProperty("plan_id") + private String planId; + @JsonProperty("task_id") + private String taskId; + @JsonProperty("run_id") + private String runId; + private String kind; + @JsonProperty("occurred_at") + private String occurredAt; + @JsonProperty("started_at") + private String startedAt; + @JsonProperty("finished_at") + private String finishedAt; + @JsonProperty("duration_ms") + private Long durationMs; + @JsonProperty("sync_status") + private String syncStatus; + @JsonProperty("client_key_masked") + private String clientKeyMasked; + @JsonProperty("device_id") + private String deviceId; + @JsonProperty("synced_at") + private Date syncedAt; + private Date modified; + private Date created; + @JsonProperty("business_view") + private Map businessView; + private Map payload; + @JsonProperty("task_count") + private Integer taskCount; + @JsonProperty("run_count") + private Integer runCount; + @JsonProperty("latest_event_at") + private String latestEventAt; + @JsonProperty("last_event_message") + private String lastEventMessage; +} 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/DigitalEmployeePlanBusinessViewDetailDto.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/DigitalEmployeePlanBusinessViewDetailDto.java new file mode 100644 index 00000000..629e5f7e --- /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/DigitalEmployeePlanBusinessViewDetailDto.java @@ -0,0 +1,17 @@ +package com.xspaceagi.agent.core.adapter.dto.digital; + +import lombok.Builder; +import lombok.Data; + +import java.util.List; + +@Data +@Builder +public class DigitalEmployeePlanBusinessViewDetailDto { + private DigitalEmployeeBusinessViewRowDto plan; + private List tasks; + private List runs; + private List events; + private List artifacts; + private List approvals; +} 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/DigitalEmployeeSyncItemDto.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/DigitalEmployeeSyncItemDto.java index 649ecdaf..a53e79cf 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/digital/DigitalEmployeeSyncItemDto.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/DigitalEmployeeSyncItemDto.java @@ -14,5 +14,11 @@ public class DigitalEmployeeSyncItemDto { @JsonAlias("entity_id") private String entityId; private String operation; + @JsonAlias("contract_version") + private String contractVersion; + @JsonAlias("entity_summary") + private Map entitySummary; + @JsonAlias("business_view") + private Map businessView; private Map payload; } 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/DigitalEmployeeSyncRecordDto.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/DigitalEmployeeSyncRecordDto.java index 321b6973..d8cf01b2 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/digital/DigitalEmployeeSyncRecordDto.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/DigitalEmployeeSyncRecordDto.java @@ -21,6 +21,12 @@ public class DigitalEmployeeSyncRecordDto { @JsonProperty("entity_id") private String entityId; private String operation; + @JsonProperty("contract_version") + private String contractVersion; + @JsonProperty("entity_summary") + private String entitySummary; + @JsonProperty("business_view") + private String businessView; private String payload; @JsonProperty("synced_at") private Date syncedAt; diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/DigitalEmployeeSyncRecordRepository.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/DigitalEmployeeSyncRecordRepository.java index b590f64f..c607e48d 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/DigitalEmployeeSyncRecordRepository.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/DigitalEmployeeSyncRecordRepository.java @@ -4,6 +4,8 @@ import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.service.IService; import com.xspaceagi.agent.core.adapter.repository.entity.DigitalEmployeeSyncRecord; +import java.util.Date; + public interface DigitalEmployeeSyncRecordRepository extends IService { IPage queryRecords( Long tenantId, @@ -15,4 +17,17 @@ public interface DigitalEmployeeSyncRecordRepository extends IService queryBusinessRecords( + Long tenantId, + String clientKey, + String deviceId, + String entityType, + String entityId, + Date updatedFrom, + Date updatedTo, + String syncStatus, + 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/repository/entity/DigitalEmployeeSyncRecord.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/entity/DigitalEmployeeSyncRecord.java index f579a891..9a31919f 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/entity/DigitalEmployeeSyncRecord.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/entity/DigitalEmployeeSyncRecord.java @@ -28,6 +28,9 @@ public class DigitalEmployeeSyncRecord { private String entityType; private String entityId; private String operation; + private String contractVersion; + private String entitySummary; + private String businessView; private String payload; private Date syncedAt; private Date modified; 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 a8f44b73..28f8a0bf 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 @@ -4,8 +4,12 @@ import com.baomidou.dynamic.datasource.annotation.DSTransactional; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; 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.DigitalEmployeeBusinessViewPageDto; +import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeBusinessViewRowDto; +import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeePlanBusinessViewDetailDto; import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncAckDto; import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncItemDto; import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncRecordDto; @@ -17,11 +21,17 @@ import com.xspaceagi.agent.core.adapter.repository.entity.DigitalEmployeeSyncRec import com.xspaceagi.system.spec.common.RequestContext; import jakarta.annotation.Resource; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; +import java.time.Instant; import java.util.ArrayList; +import java.util.Comparator; import java.util.Date; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; +import java.util.Objects; @Service public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmployeeSyncApplicationService { @@ -67,6 +77,9 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye record.setEntityType(item.getEntityType()); record.setEntityId(item.getEntityId()); record.setOperation(item.getOperation()); + record.setContractVersion(item.getContractVersion()); + record.setEntitySummary(toJson(item.getEntitySummary())); + record.setBusinessView(toJson(item.getBusinessView())); record.setPayload(toJson(item.getPayload())); record.setSyncedAt(now); record.setModified(now); @@ -140,6 +153,9 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye .entityType(record.getEntityType()) .entityId(record.getEntityId()) .operation(record.getOperation()) + .contractVersion(record.getContractVersion()) + .entitySummary(record.getEntitySummary()) + .businessView(record.getBusinessView()) .payload(record.getPayload()) .syncedAt(record.getSyncedAt()) .modified(record.getModified()) @@ -147,6 +163,258 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye .build(); } + @Override + public DigitalEmployeeBusinessViewPageDto queryBusinessViews( + String entityType, + String clientKey, + String deviceId, + String entityId, + String status, + String syncStatus, + 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 businessPage(List.of(), currentPage, currentPageSize); + } + + String normalizedEntityType = normalizeBusinessEntityType(entityType); + String repositoryEntityType = "plan".equals(normalizedEntityType) ? null : normalizedEntityType; + IPage page = digitalEmployeeSyncRecordRepository.queryBusinessRecords( + tenantId, + clientKey, + deviceId, + repositoryEntityType, + entityId, + updatedFrom, + updatedTo, + syncStatus, + currentPage, + currentPageSize + ); + List allRows = page.getRecords().stream() + .map(this::toBusinessRow) + .toList(); + List rows = "plan".equals(normalizedEntityType) + ? enrichPlanRows(allRows) + : allRows.stream().filter(row -> StringUtils.equals(row.getEntityType(), normalizedEntityType)).toList(); + rows = rows.stream() + .filter(row -> matchesText(status, row.getStatus())) + .filter(row -> matchesText(syncStatus, row.getSyncStatus())) + .toList(); + return businessPage(rows, currentPage, currentPageSize); + } + + @Override + public DigitalEmployeePlanBusinessViewDetailDto queryPlanBusinessViewDetail( + String planId, + String clientKey, + String deviceId + ) { + Long tenantId = RequestContext.get() == null ? null : RequestContext.get().getTenantId(); + if (tenantId == null || StringUtils.isBlank(planId)) { + return planDetail(null, List.of()); + } + IPage page = digitalEmployeeSyncRecordRepository.queryBusinessRecords( + tenantId, + clientKey, + deviceId, + null, + null, + null, + null, + null, + DEFAULT_PAGE_NO, + MAX_PAGE_SIZE + ); + List rows = page.getRecords().stream() + .map(this::toBusinessRow) + .filter(row -> StringUtils.equals(planId, planIdOf(row))) + .toList(); + DigitalEmployeeBusinessViewRowDto plan = enrichPlanRows(rows).stream() + .filter(row -> StringUtils.equals(row.getEntityId(), planId)) + .findFirst() + .orElse(null); + return planDetail(plan, rows); + } + + private DigitalEmployeeBusinessViewPageDto businessPage(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 DigitalEmployeeBusinessViewPageDto.builder() + .total((long) rows.size()) + .pageNo(pageNo) + .pageSize(pageSize) + .records(rows.subList(fromIndex, toIndex)) + .build(); + } + + private DigitalEmployeePlanBusinessViewDetailDto planDetail(DigitalEmployeeBusinessViewRowDto plan, List rows) { + return DigitalEmployeePlanBusinessViewDetailDto.builder() + .plan(plan) + .tasks(rowsByType(rows, "task")) + .runs(rowsByType(rows, "run")) + .events(rowsByType(rows, "event")) + .artifacts(rowsByType(rows, "artifact")) + .approvals(rowsByType(rows, "approval")) + .build(); + } + + private List rowsByType(List rows, String entityType) { + return rows.stream().filter(row -> StringUtils.equals(row.getEntityType(), entityType)).toList(); + } + + private List enrichPlanRows(List rows) { + return rows.stream() + .filter(row -> StringUtils.equals(row.getEntityType(), "plan")) + .map(plan -> { + String planId = plan.getEntityId(); + int taskCount = (int) rows.stream().filter(row -> StringUtils.equals(row.getEntityType(), "task") && StringUtils.equals(planId, row.getPlanId())).count(); + int runCount = (int) rows.stream().filter(row -> StringUtils.equals(row.getEntityType(), "run") && StringUtils.equals(planId, row.getPlanId())).count(); + String latestEventAt = rows.stream() + .filter(row -> StringUtils.equals(row.getEntityType(), "event") && StringUtils.equals(planId, row.getPlanId())) + .map(DigitalEmployeeBusinessViewRowDto::getOccurredAt) + .filter(StringUtils::isNotBlank) + .max(Comparator.naturalOrder()) + .orElse(null); + plan.setTaskCount(taskCount); + plan.setRunCount(runCount); + plan.setLatestEventAt(latestEventAt); + return plan; + }) + .toList(); + } + + private DigitalEmployeeBusinessViewRowDto toBusinessRow(DigitalEmployeeSyncRecord record) { + Map businessView = readJsonObject(record.getBusinessView()); + Map payload = readJsonObject(record.getPayload()); + Map summary = readJsonObject(record.getEntitySummary()); + String entityType = stringValue(businessView.get("entity_type"), record.getEntityType()); + String entityId = record.getEntityId(); + String title = firstString( + businessView.get("title"), businessView.get("name"), + summary.get("title"), + payload.get("title"), payload.get("name"), payload.get("objective"), payload.get("message"), + entityId + ); + String status = firstString(businessView.get("status"), summary.get("status"), payload.get("status"), payload.get("syncStatus"), payload.get("sync_status")); + String startedAt = firstString(businessView.get("started_at"), businessView.get("startedAt"), payload.get("startedAt"), payload.get("started_at")); + String finishedAt = firstString(businessView.get("finished_at"), businessView.get("finishedAt"), payload.get("finishedAt"), payload.get("finished_at")); + Long durationMs = numberValue(businessView.get("duration_ms")); + if (durationMs == null) { + durationMs = durationMs(startedAt, finishedAt); + } + return DigitalEmployeeBusinessViewRowDto.builder() + .syncRecordId(record.getId()) + .syncRecordUrl(syncRecordUrl(record.getOutboxId())) + .entityType(entityType) + .entityId(entityId) + .operation(record.getOperation()) + .title(title) + .status(status) + .summary(firstString(businessView.get("summary"), businessView.get("objective"), businessView.get("message"), summary.get("summary"), payload.get("summary"), payload.get("objective"), payload.get("message"))) + .planId(firstString(businessView.get("plan_id"), businessView.get("planId"), payload.get("plan_id"), payload.get("planId"), "plan".equals(entityType) ? entityId : null)) + .taskId(firstString(businessView.get("task_id"), businessView.get("taskId"), payload.get("task_id"), payload.get("taskId"), "task".equals(entityType) ? entityId : null)) + .runId(firstString(businessView.get("run_id"), businessView.get("runId"), payload.get("run_id"), payload.get("runId"), "run".equals(entityType) ? entityId : null)) + .kind(firstString(businessView.get("kind"), payload.get("kind"))) + .occurredAt(firstString(businessView.get("occurred_at"), businessView.get("occurredAt"), payload.get("occurred_at"), payload.get("occurredAt"))) + .startedAt(startedAt) + .finishedAt(finishedAt) + .durationMs(durationMs) + .syncStatus(firstString(businessView.get("sync_status"), businessView.get("syncStatus"), payload.get("sync_status"), payload.get("syncStatus"))) + .clientKeyMasked(maskClientKey(record.getClientKey())) + .deviceId(record.getDeviceId()) + .syncedAt(record.getSyncedAt()) + .modified(record.getModified()) + .created(record.getCreated()) + .businessView(businessView) + .payload(payload) + .lastEventMessage(lastEventMessage(entityType, businessView, payload)) + .build(); + } + + private String planIdOf(DigitalEmployeeBusinessViewRowDto row) { + return StringUtils.equals(row.getEntityType(), "plan") ? row.getEntityId() : row.getPlanId(); + } + + private String lastEventMessage(String entityType, Map businessView, Map payload) { + if (!StringUtils.equals(entityType, "run")) return null; + return firstString(businessView.get("last_event_message"), payload.get("lastEventMessage"), payload.get("message")); + } + + private boolean matchesText(String expected, String actual) { + return StringUtils.isBlank(expected) || StringUtils.equals(expected, actual); + } + + private String normalizeBusinessEntityType(String value) { + String normalized = StringUtils.trimToEmpty(value).toLowerCase(); + if (normalized.endsWith("s")) normalized = normalized.substring(0, normalized.length() - 1); + return normalized; + } + + private String syncRecordUrl(String outboxId) { + return "/system/content/content-digital-employee-sync?outbox_id=" + StringUtils.defaultString(outboxId); + } + + private String maskClientKey(String clientKey) { + if (StringUtils.isBlank(clientKey)) return null; + String trimmed = clientKey.trim(); + if (trimmed.length() <= 7) return "***"; + return trimmed.substring(0, 4) + "*********" + trimmed.substring(trimmed.length() - 3); + } + + private Map readJsonObject(String value) { + if (StringUtils.isBlank(value)) return new LinkedHashMap<>(); + try { + Map parsed = objectMapper.readValue(value, new TypeReference<>() {}); + return parsed == null ? new LinkedHashMap<>() : parsed; + } catch (Exception e) { + return new LinkedHashMap<>(); + } + } + + private String firstString(Object... values) { + for (Object value : values) { + String candidate = stringValue(value, null); + if (StringUtils.isNotBlank(candidate)) return candidate; + } + return null; + } + + private String stringValue(Object value, String fallback) { + if (value == null) return fallback; + String text = Objects.toString(value, "").trim(); + return StringUtils.isBlank(text) ? fallback : text; + } + + private Long numberValue(Object value) { + if (value instanceof Number number) return number.longValue(); + if (value instanceof String text && StringUtils.isNotBlank(text)) { + try { + return Long.parseLong(text.trim()); + } catch (NumberFormatException ignored) { + return null; + } + } + return null; + } + + private Long durationMs(String startedAt, String finishedAt) { + if (StringUtils.isBlank(startedAt) || StringUtils.isBlank(finishedAt)) return null; + try { + return Instant.parse(finishedAt).toEpochMilli() - Instant.parse(startedAt).toEpochMilli(); + } catch (Exception e) { + return null; + } + } + private String toJson(Object payload) { if (payload == null) { return "{}"; 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 new file mode 100644 index 00000000..fc9c9a40 --- /dev/null +++ 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 @@ -0,0 +1,250 @@ +package com.xspaceagi.agent.core.application.service; + +import com.baomidou.mybatisplus.core.metadata.IPage; +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.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.DigitalEmployeeSyncRecordRepository; +import com.xspaceagi.agent.core.adapter.repository.entity.DigitalEmployeeSyncRecord; +import com.xspaceagi.system.spec.common.RequestContext; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class DigitalEmployeeSyncApplicationServiceImplTest { + + private DigitalEmployeeSyncRecordRepository repository; + private DigitalEmployeeSyncApplicationServiceImpl service; + + @BeforeEach + void setUp() { + RequestContext context = new RequestContext<>(); + context.setTenantId(101L); + context.setUserId(202L); + RequestContext.set(context); + + repository = mock(DigitalEmployeeSyncRecordRepository.class); + service = new DigitalEmployeeSyncApplicationServiceImpl(); + ReflectionTestUtils.setField(service, "digitalEmployeeSyncRecordRepository", repository); + ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper()); + } + + @AfterEach + void tearDown() { + RequestContext.remove(); + } + + @Test + void syncOutboxPersistsBusinessViewSummaryAndContractVersion() { + DigitalEmployeeSyncItemDto item = new DigitalEmployeeSyncItemDto(); + item.setOutboxId("outbox-plan-1"); + item.setEntityType("plan"); + item.setEntityId("plan-1"); + item.setOperation("upsert"); + item.setContractVersion("qimingclaw.digital_employee.sync.v1"); + item.setEntitySummary(Map.of("title", "客户回访", "status", "active", "summary", "跟进高价值客户")); + item.setBusinessView(Map.of("local_id", "plan-1", "title", "客户回访", "status", "active", "objective", "跟进高价值客户")); + item.setPayload(Map.of("id", "plan-1", "title", "客户回访", "status", "active")); + + DigitalEmployeeSyncRequestDto request = new DigitalEmployeeSyncRequestDto(); + request.setClientKey("client-key-001"); + request.setDeviceId("device-001"); + request.setItems(List.of(item)); + + service.syncOutbox(request); + + ArgumentCaptor captor = ArgumentCaptor.forClass(DigitalEmployeeSyncRecord.class); + verify(repository).save(captor.capture()); + DigitalEmployeeSyncRecord record = captor.getValue(); + assertThat(record.getContractVersion()).isEqualTo("qimingclaw.digital_employee.sync.v1"); + assertThat(record.getEntitySummary()).contains("客户回访"); + assertThat(record.getBusinessView()).contains("objective").contains("跟进高价值客户"); + assertThat(record.getPayload()).contains("plan-1"); + assertThat(record.getTenantId()).isEqualTo(101L); + assertThat(record.getUserId()).isEqualTo(202L); + } + + @Test + void queryRecordsReturnsStoredBusinessMetadata() { + DigitalEmployeeSyncRecord record = planRecord("plan-1", "客户回访", "active", 101L, new Date(1_000)); + Page page = new Page<>(1, 20, 1); + page.setRecords(List.of(record)); + when(repository.queryRecords(eq(101L), eq("client-key-001"), eq("device-001"), eq("plan"), eq("plan-1"), eq("outbox-plan-1"), eq(1L), eq(20L))) + .thenReturn(page); + + DigitalEmployeeSyncRecordPageDto result = service.queryRecords("client-key-001", "device-001", "plan", "plan-1", "outbox-plan-1", 1L, 20L); + + assertThat(result.getRecords()).hasSize(1); + assertThat(result.getRecords().get(0).getContractVersion()).isEqualTo("qimingclaw.digital_employee.sync.v1"); + assertThat(result.getRecords().get(0).getEntitySummary()).contains("客户回访"); + assertThat(result.getRecords().get(0).getBusinessView()).contains("active"); + } + + @Test + void queryPlansBuildsBusinessRowsAndAggregatesRelatedCounts() { + Date now = new Date(10_000); + List records = List.of( + planRecord("plan-1", "客户回访", "active", 101L, now), + taskRecord("task-1", "plan-1", "running", 101L, new Date(11_000)), + runRecord("run-1", "plan-1", "task-1", "failed", 101L, new Date(12_000)), + eventRecord("event-1", "plan-1", "task-1", "run-1", "plan_step_dispatch_failed", "派发失败", "failed", 101L, new Date(13_000)) + ); + when(repository.queryBusinessRecords(eq(101L), eq("client-key-001"), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), eq(1L), eq(20L))) + .thenReturn(page(records)); + + DigitalEmployeeBusinessViewPageDto result = service.queryBusinessViews("plan", "client-key-001", null, null, null, null, null, null, 1L, 20L); + + assertThat(result.getTotal()).isEqualTo(1); + assertThat(result.getRecords()).hasSize(1); + DigitalEmployeeBusinessViewRowDto plan = result.getRecords().get(0); + assertThat(plan.getEntityType()).isEqualTo("plan"); + assertThat(plan.getEntityId()).isEqualTo("plan-1"); + assertThat(plan.getTitle()).isEqualTo("客户回访"); + assertThat(plan.getStatus()).isEqualTo("active"); + assertThat(plan.getTaskCount()).isEqualTo(1); + assertThat(plan.getRunCount()).isEqualTo(1); + assertThat(plan.getLatestEventAt()).isNotNull(); + assertThat(plan.getSyncRecordUrl()).contains("outbox-plan-1"); + assertThat(plan.getClientKeyMasked()).isEqualTo("clie*********001"); + } + + @Test + void queryEventsFiltersStatusEntityAndTimeRangeFromBusinessView() { + Date from = new Date(10_000); + Date to = new Date(20_000); + when(repository.queryBusinessRecords(eq(101L), eq(null), eq(null), eq("event"), eq("event-1"), eq(from), eq(to), eq(null), eq(1L), eq(20L))) + .thenReturn(page(List.of( + eventRecord("event-1", "plan-1", "task-1", "run-1", "tool_call_audit", "工具允许", "allowed", 101L, new Date(12_000)), + eventRecord("event-2", "plan-1", "task-1", "run-1", "tool_call_audit", "工具拒绝", "rejected", 101L, new Date(13_000)) + ))); + + DigitalEmployeeBusinessViewPageDto result = service.queryBusinessViews("event", null, null, "event-1", "allowed", null, from, to, 1L, 20L); + + assertThat(result.getTotal()).isEqualTo(1); + assertThat(result.getRecords().get(0).getEntityId()).isEqualTo("event-1"); + assertThat(result.getRecords().get(0).getKind()).isEqualTo("tool_call_audit"); + } + + @Test + void queryPlanDetailReturnsRelatedBusinessFacts() { + when(repository.queryBusinessRecords(eq(101L), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), eq(1L), eq(100L))) + .thenReturn(page(List.of( + planRecord("plan-1", "客户回访", "active", 101L, new Date(10_000)), + taskRecord("task-1", "plan-1", "running", 101L, new Date(11_000)), + runRecord("run-1", "plan-1", "task-1", "failed", 101L, new Date(12_000)), + eventRecord("event-1", "plan-1", "task-1", "run-1", "approval_decision", "审批通过", "approved", 101L, new Date(13_000)), + artifactRecord("artifact-1", "plan-1", "task-1", "ready", 101L, new Date(14_000)), + approvalRecord("approval-1", "plan-1", "task-1", "pending", 101L, new Date(15_000)) + ))); + + DigitalEmployeePlanBusinessViewDetailDto result = service.queryPlanBusinessViewDetail("plan-1", null, null); + + assertThat(result.getPlan().getEntityId()).isEqualTo("plan-1"); + assertThat(result.getTasks()).hasSize(1); + assertThat(result.getRuns()).hasSize(1); + assertThat(result.getEvents()).hasSize(1); + assertThat(result.getArtifacts()).hasSize(1); + assertThat(result.getApprovals()).hasSize(1); + } + + @Test + void queryBusinessViewsReturnsEmptyWhenTenantIsMissing() { + RequestContext.remove(); + + DigitalEmployeeBusinessViewPageDto result = service.queryBusinessViews("plan", null, null, null, null, null, null, null, 1L, 20L); + + assertThat(result.getTotal()).isZero(); + assertThat(result.getRecords()).isEmpty(); + verify(repository, never()).queryBusinessRecords(any(), any(), any(), any(), any(), any(), any(), any(), any(Long.class), any(Long.class)); + } + + private IPage page(List records) { + Page page = new Page<>(1, 20, records.size()); + page.setRecords(records); + return page; + } + + 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", "计划摘要"), + Map.of("id", planId, "title", title, "status", status)); + } + + private DigitalEmployeeSyncRecord taskRecord(String taskId, String planId, String status, Long tenantId, Date syncedAt) { + return record("outbox-" + taskId, "task", taskId, tenantId, syncedAt, + Map.of("entity_type", "task", "local_id", taskId, "plan_id", planId, "title", "任务", "status", status), + Map.of("id", taskId, "planId", planId, "status", status)); + } + + private DigitalEmployeeSyncRecord runRecord(String runId, String planId, String taskId, String status, Long tenantId, Date syncedAt) { + return record("outbox-" + runId, "run", runId, tenantId, syncedAt, + Map.of("entity_type", "run", "local_id", runId, "plan_id", planId, "task_id", taskId, "status", status, "started_at", "2026-06-12T08:00:00.000Z", "finished_at", "2026-06-12T08:01:00.000Z"), + Map.of("id", runId, "planId", planId, "taskId", taskId, "status", status)); + } + + private DigitalEmployeeSyncRecord eventRecord(String eventId, String planId, String taskId, String runId, String kind, String message, String status, Long tenantId, Date syncedAt) { + return record("outbox-" + eventId, "event", eventId, tenantId, syncedAt, + Map.of("entity_type", "event", "local_id", eventId, "plan_id", planId, "task_id", taskId, "run_id", runId, "kind", kind, "message", message, "status", status, "occurred_at", "2026-06-12T08:02:00.000Z"), + Map.of("id", eventId, "planId", planId, "taskId", taskId, "runId", runId, "kind", kind, "message", message, "status", status)); + } + + private DigitalEmployeeSyncRecord artifactRecord(String artifactId, String planId, String taskId, String status, Long tenantId, Date syncedAt) { + return record("outbox-" + artifactId, "artifact", artifactId, tenantId, syncedAt, + Map.of("entity_type", "artifact", "local_id", artifactId, "plan_id", planId, "task_id", taskId, "title", "交付物", "status", status), + Map.of("id", artifactId, "planId", planId, "taskId", taskId, "status", status)); + } + + private DigitalEmployeeSyncRecord approvalRecord(String approvalId, String planId, String taskId, String status, Long tenantId, Date syncedAt) { + return record("outbox-" + approvalId, "approval", approvalId, tenantId, syncedAt, + Map.of("entity_type", "approval", "local_id", approvalId, "plan_id", planId, "task_id", taskId, "title", "审批", "status", status), + Map.of("id", approvalId, "planId", planId, "taskId", taskId, "status", status)); + } + + private DigitalEmployeeSyncRecord record(String outboxId, String entityType, String entityId, Long tenantId, Date syncedAt, Map businessView, Map payload) { + DigitalEmployeeSyncRecord record = new DigitalEmployeeSyncRecord(); + record.setId((long) Math.abs(outboxId.hashCode())); + record.setTenantId(tenantId); + record.setUserId(202L); + record.setClientKey("client-key-001"); + record.setDeviceId("device-001"); + record.setOutboxId(outboxId); + record.setEntityType(entityType); + record.setEntityId(entityId); + record.setOperation("upsert"); + record.setContractVersion("qimingclaw.digital_employee.sync.v1"); + record.setEntitySummary(toJson(Map.of("title", businessView.getOrDefault("title", entityId), "status", businessView.getOrDefault("status", "unknown")))); + record.setBusinessView(toJson(businessView)); + record.setPayload(toJson(payload)); + record.setSyncedAt(syncedAt); + record.setModified(syncedAt); + record.setCreated(syncedAt); + return record; + } + + private String toJson(Map value) { + try { + return new ObjectMapper().writeValueAsString(value); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/repository/DigitalEmployeeSyncRecordRepositoryImpl.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/repository/DigitalEmployeeSyncRecordRepositoryImpl.java index c1c4747f..72d5b964 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/repository/DigitalEmployeeSyncRecordRepositoryImpl.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/repository/DigitalEmployeeSyncRecordRepositoryImpl.java @@ -10,6 +10,8 @@ import com.xspaceagi.agent.core.infra.dao.mapper.DigitalEmployeeSyncRecordMapper import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; +import java.util.Date; + @Service public class DigitalEmployeeSyncRecordRepositoryImpl extends ServiceImpl @@ -37,4 +39,30 @@ public class DigitalEmployeeSyncRecordRepositoryImpl .orderByDesc(DigitalEmployeeSyncRecord::getId); return page(new Page<>(pageNo, pageSize), queryWrapper); } + + @Override + public IPage queryBusinessRecords( + Long tenantId, + String clientKey, + String deviceId, + String entityType, + String entityId, + Date updatedFrom, + Date updatedTo, + String syncStatus, + long pageNo, + long pageSize + ) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper() + .eq(tenantId != null, DigitalEmployeeSyncRecord::getTenantId, tenantId) + .eq(StringUtils.isNotBlank(clientKey), DigitalEmployeeSyncRecord::getClientKey, clientKey) + .eq(StringUtils.isNotBlank(deviceId), DigitalEmployeeSyncRecord::getDeviceId, deviceId) + .eq(StringUtils.isNotBlank(entityType), DigitalEmployeeSyncRecord::getEntityType, entityType) + .eq(StringUtils.isNotBlank(entityId), DigitalEmployeeSyncRecord::getEntityId, entityId) + .ge(updatedFrom != null, DigitalEmployeeSyncRecord::getSyncedAt, updatedFrom) + .le(updatedTo != null, DigitalEmployeeSyncRecord::getSyncedAt, updatedTo) + .orderByDesc(DigitalEmployeeSyncRecord::getSyncedAt) + .orderByDesc(DigitalEmployeeSyncRecord::getId); + return page(new Page<>(pageNo, pageSize), queryWrapper); + } } 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/DigitalEmployeeBusinessViewController.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/DigitalEmployeeBusinessViewController.java new file mode 100644 index 00000000..cfb9b999 --- /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/DigitalEmployeeBusinessViewController.java @@ -0,0 +1,177 @@ +package com.xspaceagi.agent.web.ui.controller; + +import com.xspaceagi.agent.core.adapter.application.DigitalEmployeeSyncApplicationService; +import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeBusinessViewPageDto; +import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeePlanBusinessViewDetailDto; +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_SYNC_QUERY_LIST; + +@RestController +@RequestMapping("/api/digital-employee") +public class DigitalEmployeeBusinessViewController { + + @Resource + private DigitalEmployeeSyncApplicationService digitalEmployeeSyncApplicationService; + + @RequireResource(CONTENT_DIGITAL_EMPLOYEE_SYNC_QUERY_LIST) + @RequestMapping(path = "/plans", method = RequestMethod.GET) + public ReqResult queryPlans( + @RequestParam(name = "client_key", required = false) String clientKey, + @RequestParam(name = "device_id", required = false) String deviceId, + @RequestParam(name = "entity_id", required = false) String entityId, + @RequestParam(name = "status", required = false) String status, + @RequestParam(name = "sync_status", required = false) String syncStatus, + @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(query("plan", clientKey, deviceId, entityId, status, syncStatus, updatedFrom, updatedTo, pageNo, pageSize)); + } + + @RequireResource(CONTENT_DIGITAL_EMPLOYEE_SYNC_QUERY_LIST) + @RequestMapping(path = "/plans/{plan_id}", method = RequestMethod.GET) + public ReqResult queryPlanDetail( + @PathVariable("plan_id") String planId, + @RequestParam(name = "client_key", required = false) String clientKey, + @RequestParam(name = "device_id", required = false) String deviceId + ) { + return ReqResult.success(digitalEmployeeSyncApplicationService.queryPlanBusinessViewDetail(planId, clientKey, deviceId)); + } + + @RequireResource(CONTENT_DIGITAL_EMPLOYEE_SYNC_QUERY_LIST) + @RequestMapping(path = "/tasks", method = RequestMethod.GET) + public ReqResult queryTasks( + @RequestParam(name = "client_key", required = false) String clientKey, + @RequestParam(name = "device_id", required = false) String deviceId, + @RequestParam(name = "entity_id", required = false) String entityId, + @RequestParam(name = "status", required = false) String status, + @RequestParam(name = "sync_status", required = false) String syncStatus, + @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(query("task", clientKey, deviceId, entityId, status, syncStatus, updatedFrom, updatedTo, pageNo, pageSize)); + } + + @RequireResource(CONTENT_DIGITAL_EMPLOYEE_SYNC_QUERY_LIST) + @RequestMapping(path = "/runs", method = RequestMethod.GET) + public ReqResult queryRuns( + @RequestParam(name = "client_key", required = false) String clientKey, + @RequestParam(name = "device_id", required = false) String deviceId, + @RequestParam(name = "entity_id", required = false) String entityId, + @RequestParam(name = "status", required = false) String status, + @RequestParam(name = "sync_status", required = false) String syncStatus, + @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(query("run", clientKey, deviceId, entityId, status, syncStatus, updatedFrom, updatedTo, pageNo, pageSize)); + } + + @RequireResource(CONTENT_DIGITAL_EMPLOYEE_SYNC_QUERY_LIST) + @RequestMapping(path = "/events", method = RequestMethod.GET) + public ReqResult queryEvents( + @RequestParam(name = "client_key", required = false) String clientKey, + @RequestParam(name = "device_id", required = false) String deviceId, + @RequestParam(name = "entity_id", required = false) String entityId, + @RequestParam(name = "status", required = false) String status, + @RequestParam(name = "sync_status", required = false) String syncStatus, + @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(query("event", clientKey, deviceId, entityId, status, syncStatus, updatedFrom, updatedTo, pageNo, pageSize)); + } + + @RequireResource(CONTENT_DIGITAL_EMPLOYEE_SYNC_QUERY_LIST) + @RequestMapping(path = "/artifacts", method = RequestMethod.GET) + public ReqResult queryArtifacts( + @RequestParam(name = "client_key", required = false) String clientKey, + @RequestParam(name = "device_id", required = false) String deviceId, + @RequestParam(name = "entity_id", required = false) String entityId, + @RequestParam(name = "status", required = false) String status, + @RequestParam(name = "sync_status", required = false) String syncStatus, + @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(query("artifact", clientKey, deviceId, entityId, status, syncStatus, updatedFrom, updatedTo, pageNo, pageSize)); + } + + @RequireResource(CONTENT_DIGITAL_EMPLOYEE_SYNC_QUERY_LIST) + @RequestMapping(path = "/approvals", method = RequestMethod.GET) + public ReqResult queryApprovals( + @RequestParam(name = "client_key", required = false) String clientKey, + @RequestParam(name = "device_id", required = false) String deviceId, + @RequestParam(name = "entity_id", required = false) String entityId, + @RequestParam(name = "status", required = false) String status, + @RequestParam(name = "sync_status", required = false) String syncStatus, + @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(query("approval", clientKey, deviceId, entityId, status, syncStatus, updatedFrom, updatedTo, pageNo, pageSize)); + } + + private DigitalEmployeeBusinessViewPageDto query( + String entityType, + String clientKey, + String deviceId, + String entityId, + String status, + String syncStatus, + String updatedFrom, + String updatedTo, + Long pageNo, + Long pageSize + ) { + return digitalEmployeeSyncApplicationService.queryBusinessViews( + entityType, + clientKey, + deviceId, + entityId, + status, + syncStatus, + 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/sql/update-20260612.sql b/qiming-backend/sql/update-20260612.sql new file mode 100644 index 00000000..2f0a94ec --- /dev/null +++ b/qiming-backend/sql/update-20260612.sql @@ -0,0 +1,4 @@ +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`; diff --git a/qimingclaw/docs/digital-employee-frontend-integration-plan.md b/qimingclaw/docs/digital-employee-frontend-integration-plan.md index ed1b8b01..9b21e516 100644 --- a/qimingclaw/docs/digital-employee-frontend-integration-plan.md +++ b/qimingclaw/docs/digital-employee-frontend-integration-plan.md @@ -609,9 +609,9 @@ GET /api/digital-employee/approvals - 建议:下一轮围绕管理端正式审计视图和诊断修复动作,把本地可追溯审计继续推进到可运营处置。 12. **管理端业务查询模型** - - 已吸收:管理端已有通用 sync record 接收、查询、payload 摘要和深链;embedded adapter 已拆出 `/api/digital-employee/plans`、`/tasks`、`/runs`、`/events`、`/artifacts`、`/approvals` 与 `/plans/:planId` 只读业务查询接口,从 qimingclaw runtime、artifact、approval 和 canonical event 投影生成统一分页视图;客户端 outbox 的 event `business_view` 已开始按 route decision、governance command、approval action、artifact lifecycle/access、skill call audit 和 ready PlanStep dispatch 细分常用字段,本地业务视图投影已开始闭环。 - - 缺口:尚未接入管理端真实业务表、复杂组合检索、跨设备维度过滤和业务视图 UI。 - - 建议:先稳定本地投影 API 和管理端消费模型,再决定是否拆物理业务表。 + - 已吸收:管理端已有通用 sync record 接收、查询、payload 摘要和深链;后端 sync record 已开始保存客户端顶层 `contract_version`、`entity_summary` 和 `business_view`,并基于现有 `digital_employee_sync_record` 拆出 `/api/digital-employee/plans`、`/plans/{plan_id}`、`/tasks`、`/runs`、`/events`、`/artifacts`、`/approvals` 管理端业务查询模型;列表支持客户端 Key、设备、实体 ID、状态、同步状态和同步时间范围过滤,业务 row 会提炼 title/status/summary、Plan/Task/Run 关联、发生/开始/结束时间、sync record 深链和脱敏客户端 Key;Plan 视图会聚合 task/run 计数与最近事件,Plan 详情会返回关联 tasks/runs/events/artifacts/approvals 摘要。embedded adapter 已拆出同名只读业务查询接口,从 qimingclaw runtime、artifact、approval 和 canonical event 投影生成统一分页视图;客户端 outbox 的 event `business_view` 已开始按 route decision、governance command、approval action、artifact lifecycle/access、skill call audit 和 ready PlanStep dispatch 细分常用字段,本地与管理端业务视图投影已开始闭环。 + - 缺口:尚未新增正式管理端业务视图 UI、复杂组合检索、跨设备聚合图和物化业务表。 + - 建议:先使用 sync record 派生业务查询模型承接管理端页面,再按容量和查询压力决定是否拆 Plan / Task / Run / Event 物理业务表。 13. **日报实体与导出交付** - 已吸收:embedded 页面可基于当前 workday projection 导出本地文本日报;生成日报时会通过 qimingclaw bridge 写入 `daily_report` Artifact 与 `daily_report_generated` Event,并可通过 `/api/digital-employee/daily-reports` 和详情接口读取只读日报投影,日报 Artifact/Event 本地沉淀已开始闭环。