From 8a5d5d647476e5045928dc5ff4c53bf7d2c2c613 Mon Sep 17 00:00:00 2001 From: baiyanyun Date: Fri, 12 Jun 2026 21:48:33 +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=E8=B7=A8=E8=AE=BE=E5=A4=87=E7=AD=96=E7=95=A5=E5=86=B2?= =?UTF-8?q?=E7=AA=81=E8=A7=86=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...DigitalEmployeeSyncApplicationService.java | 12 ++ .../DigitalEmployeePolicySnapshotPageDto.java | 18 +++ .../DigitalEmployeePolicySnapshotRowDto.java | 38 ++++++ ...talEmployeeSyncApplicationServiceImpl.java | 116 ++++++++++++++++++ ...mployeeSyncApplicationServiceImplTest.java | 42 +++++++ .../DigitalEmployeeAdminPolicyController.java | 67 ++++++++++ .../Content/DigitalEmployeeOps/index.tsx | 102 +++++++++++++++ qiming/src/services/systemManage.ts | 21 ++++ qiming/src/types/interfaces/systemManage.ts | 41 ++++++- .../scripts/check-digital-employee.js | 61 +++++++++ .../digitalEmployee/syncService.test.ts | 78 +++++++++++- .../services/digitalEmployee/syncService.ts | 54 +++++++- ...ital-employee-frontend-integration-plan.md | 2 +- 13 files changed, 643 insertions(+), 9 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/DigitalEmployeePolicySnapshotPageDto.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/DigitalEmployeePolicySnapshotRowDto.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/DigitalEmployeeAdminPolicyController.java 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 5b45b83d..1da9d993 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 @@ -6,6 +6,7 @@ import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminActionRe import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminActionResultDto; import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeBusinessViewPageDto; import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeePlanBusinessViewDetailDto; +import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeePolicySnapshotPageDto; 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; @@ -62,6 +63,17 @@ public interface DigitalEmployeeSyncApplicationService { Long pageSize ); + DigitalEmployeePolicySnapshotPageDto queryAdminPolicySnapshots( + String clientKey, + String deviceId, + String status, + String policyKind, + Date updatedFrom, + Date updatedTo, + Long pageNo, + Long pageSize + ); + DigitalEmployeeAdminActionResultDto createAdminAction(DigitalEmployeeAdminActionRequestDto request); DigitalEmployeeAdminActionPageDto queryPendingAdminActions( 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/DigitalEmployeePolicySnapshotPageDto.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/DigitalEmployeePolicySnapshotPageDto.java new file mode 100644 index 00000000..b7c63f80 --- /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/DigitalEmployeePolicySnapshotPageDto.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 DigitalEmployeePolicySnapshotPageDto { + 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/DigitalEmployeePolicySnapshotRowDto.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/DigitalEmployeePolicySnapshotRowDto.java new file mode 100644 index 00000000..75712637 --- /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/DigitalEmployeePolicySnapshotRowDto.java @@ -0,0 +1,38 @@ +package com.xspaceagi.agent.core.adapter.dto.digital; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Data; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +@Data +@Builder +public class DigitalEmployeePolicySnapshotRowDto { + @JsonProperty("policy_kind") + private String policyKind; + @JsonProperty("policy_key") + private String policyKey; + private String status; + private String summary; + @JsonProperty("client_count") + private Integer clientCount; + @JsonProperty("device_count") + private Integer deviceCount; + @JsonProperty("conflict_keys") + private List conflictKeys; + @JsonProperty("latest_synced_at") + private Date latestSyncedAt; + @JsonProperty("recommended_resolution") + private String recommendedResolution; + @JsonProperty("sync_record_url") + private String syncRecordUrl; + @JsonProperty("client_key_masked") + private String clientKeyMasked; + @JsonProperty("device_id") + private String deviceId; + @JsonProperty("business_view") + private Map businessView; +} 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 029d7c87..8a9ca52e 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 @@ -16,6 +16,8 @@ import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminWorkbenc 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.DigitalEmployeePolicySnapshotPageDto; +import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeePolicySnapshotRowDto; 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; @@ -343,6 +345,120 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye return adminWorkbenchPage(rows, currentPage, currentPageSize); } + @Override + public DigitalEmployeePolicySnapshotPageDto queryAdminPolicySnapshots( + String clientKey, + String deviceId, + String status, + String policyKind, + 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 = currentTenantId(); + if (tenantId == null) { + return policySnapshotPage(List.of(), currentPage, currentPageSize); + } + IPage page = digitalEmployeeSyncRecordRepository.queryBusinessRecords( + tenantId, + clientKey, + deviceId, + "event", + null, + updatedFrom, + updatedTo, + null, + DEFAULT_PAGE_NO, + MAX_PAGE_SIZE + ); + Map> grouped = new LinkedHashMap<>(); + for (DigitalEmployeeSyncRecord record : page.getRecords()) { + Map view = readJsonObject(record.getBusinessView()); + String kind = firstString(view.get("policy_kind")); + if (StringUtils.isBlank(kind)) continue; + if (StringUtils.isNotBlank(policyKind) && !StringUtils.equals(kind, policyKind)) continue; + grouped.computeIfAbsent(kind, ignored -> new ArrayList<>()).add(record); + } + List rows = grouped.entrySet().stream() + .map(entry -> toPolicySnapshotRow(entry.getKey(), entry.getValue())) + .filter(row -> matchesText(status, row.getStatus())) + .toList(); + return policySnapshotPage(rows, currentPage, currentPageSize); + } + + private DigitalEmployeePolicySnapshotPageDto policySnapshotPage(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 DigitalEmployeePolicySnapshotPageDto.builder() + .total((long) rows.size()) + .pageNo(pageNo) + .pageSize(pageSize) + .records(rows.subList(fromIndex, toIndex)) + .build(); + } + + private DigitalEmployeePolicySnapshotRowDto toPolicySnapshotRow(String policyKind, List records) { + List> views = records.stream().map(record -> readJsonObject(record.getBusinessView())).toList(); + List signatures = views.stream().map(this::policySignature).distinct().toList(); + DigitalEmployeeSyncRecord latest = records.stream() + .max(Comparator.comparing(DigitalEmployeeSyncRecord::getSyncedAt, Comparator.nullsLast(Comparator.naturalOrder()))) + .orElse(records.get(0)); + Map latestView = readJsonObject(latest.getBusinessView()); + String status = signatures.size() > 1 ? "conflict" : "consistent"; + return DigitalEmployeePolicySnapshotRowDto.builder() + .policyKind(policyKind) + .policyKey(firstString(latestView.get("policy_key"), policyKind)) + .status(status) + .summary(firstString(latestView.get("summary"), latestView.get("title"), policyKind)) + .clientCount((int) records.stream().map(DigitalEmployeeSyncRecord::getClientKey).filter(StringUtils::isNotBlank).distinct().count()) + .deviceCount((int) records.stream().map(DigitalEmployeeSyncRecord::getDeviceId).filter(StringUtils::isNotBlank).distinct().count()) + .conflictKeys("conflict".equals(status) ? policyConflictKeys(views) : List.of()) + .latestSyncedAt(latest.getSyncedAt()) + .recommendedResolution("conflict".equals(status) ? "review_latest_policy_snapshot" : "keep_current") + .syncRecordUrl(syncRecordUrl(latest.getOutboxId())) + .clientKeyMasked(maskClientKey(latest.getClientKey())) + .deviceId(latest.getDeviceId()) + .businessView(latestView) + .build(); + } + + private String policySignature(Map view) { + Map signature = new LinkedHashMap<>(); + for (String key : policyConflictCandidateKeys()) { + signature.put(key, view.get(key)); + } + return toJson(signature); + } + + private List policyConflictKeys(List> views) { + List keys = new ArrayList<>(); + for (String key : policyConflictCandidateKeys()) { + if (views.stream().map(view -> Objects.toString(view.get(key), "")).distinct().count() > 1) { + keys.add(key); + } + } + return keys; + } + + private List policyConflictCandidateKeys() { + return List.of( + "enabled", + "policy_version", + "policy_source", + "auto_create_governance_commands", + "max_per_sweep", + "catch_up_on_startup", + "approval_required_risk_levels", + "approval_required_actions", + "auto_reject_actions" + ); + } + @Override @DSTransactional public DigitalEmployeeAdminActionResultDto createAdminAction(DigitalEmployeeAdminActionRequestDto request) { 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 5c7cdf39..91a9b88e 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 @@ -12,6 +12,8 @@ import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminActionPa import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminActionRequestDto; import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminActionResultDto; import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeePlanBusinessViewDetailDto; +import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeePolicySnapshotPageDto; +import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeePolicySnapshotRowDto; 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; @@ -334,6 +336,46 @@ class DigitalEmployeeSyncApplicationServiceImplTest { assertThat(action.getReasonCodes()).contains("admin_action_applied"); } + @Test + void queryAdminPolicySnapshotsDetectsCrossDeviceConflict() { + when(repository.queryBusinessRecords(eq(101L), eq(null), eq(null), eq("event"), eq(null), eq(null), eq(null), eq(null), eq(1L), eq(100L))) + .thenReturn(page(List.of( + record("outbox-policy-1", "event", "event-policy-1", 101L, new Date(10_000), + Map.of( + "entity_type", "event", + "category", "policy_snapshot", + "kind", "digital_workday_pull_route_decision_policy", + "policy_kind", "route_decision", + "policy_key", "route_decision", + "enabled", true, + "auto_create_governance_commands", true, + "title", "入口路由策略" + ), + Map.of("kind", "digital_workday_pull_route_decision_policy")), + record("outbox-policy-2", "event", "event-policy-2", 101L, new Date(11_000), + Map.of( + "entity_type", "event", + "category", "policy_snapshot", + "kind", "digital_workday_pull_route_decision_policy", + "policy_kind", "route_decision", + "policy_key", "route_decision", + "enabled", true, + "auto_create_governance_commands", false, + "title", "入口路由策略" + ), + Map.of("kind", "digital_workday_pull_route_decision_policy")) + ))); + + DigitalEmployeePolicySnapshotPageDto result = service.queryAdminPolicySnapshots(null, null, "conflict", "route_decision", null, null, 1L, 20L); + + assertThat(result.getTotal()).isEqualTo(1); + DigitalEmployeePolicySnapshotRowDto row = result.getRecords().get(0); + assertThat(row.getPolicyKind()).isEqualTo("route_decision"); + assertThat(row.getStatus()).isEqualTo("conflict"); + assertThat(row.getConflictKeys()).contains("auto_create_governance_commands"); + assertThat(row.getRecommendedResolution()).isEqualTo("review_latest_policy_snapshot"); + } + 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/DigitalEmployeeAdminPolicyController.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/DigitalEmployeeAdminPolicyController.java new file mode 100644 index 00000000..124e872b --- /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/DigitalEmployeeAdminPolicyController.java @@ -0,0 +1,67 @@ +package com.xspaceagi.agent.web.ui.controller; + +import com.xspaceagi.agent.core.adapter.application.DigitalEmployeeSyncApplicationService; +import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeePolicySnapshotPageDto; +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.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/policies") +public class DigitalEmployeeAdminPolicyController { + + @Resource + private DigitalEmployeeSyncApplicationService digitalEmployeeSyncApplicationService; + + @RequireResource(CONTENT_DIGITAL_EMPLOYEE_OPS_QUERY_LIST) + @RequestMapping(path = "/snapshots", method = RequestMethod.GET) + public ReqResult querySnapshots( + @RequestParam(name = "client_key", required = false) String clientKey, + @RequestParam(name = "device_id", required = false) String deviceId, + @RequestParam(name = "status", required = false) String status, + @RequestParam(name = "policy_kind", required = false) String policyKind, + @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.queryAdminPolicySnapshots( + clientKey, + deviceId, + status, + policyKind, + 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/src/pages/SystemManagement/Content/DigitalEmployeeOps/index.tsx b/qiming/src/pages/SystemManagement/Content/DigitalEmployeeOps/index.tsx index ce03ca01..268b463e 100644 --- a/qiming/src/pages/SystemManagement/Content/DigitalEmployeeOps/index.tsx +++ b/qiming/src/pages/SystemManagement/Content/DigitalEmployeeOps/index.tsx @@ -3,6 +3,7 @@ import WorkspaceLayout from '@/components/WorkspaceLayout'; import { SUCCESS_CODE } from '@/constants/codes.constants'; import { apiDigitalEmployeeAdminActionSubmit, + apiDigitalEmployeeAdminPolicySnapshots, apiDigitalEmployeeAdminWorkbenchList, } from '@/services/systemManage'; import type { @@ -29,6 +30,7 @@ const WORKBENCH_TABS: Array<{ { key: 'audits', label: '审计诊断' }, { key: 'daily_reports', label: '日报运营' }, { key: 'route_governance', label: '路由治理' }, + { key: 'policy_conflicts', label: '策略冲突' }, ]; const ENTITY_TYPE_OPTIONS = { @@ -38,6 +40,13 @@ const ENTITY_TYPE_OPTIONS = { event: { text: 'Event' }, artifact: { text: 'Artifact' }, approval: { text: 'Approval' }, + policy_snapshot: { text: 'Policy Snapshot' }, +}; + +const POLICY_KIND_OPTIONS = { + dispatch: { text: '派发策略' }, + risk_approval: { text: '风险审批策略' }, + route_decision: { text: '入口路由策略' }, }; const STATUS_TONE: Record = { @@ -80,6 +89,7 @@ function renderSummary(record: DigitalEmployeeAdminWorkbenchRow) { record.approval_id && `Approval ${record.approval_id}`, record.artifact_id && `Artifact ${record.artifact_id}`, record.route_decision_id && `Route ${record.route_decision_id}`, + record.policy_kind && `Policy ${record.policy_kind}`, ].filter((item): item is string => Boolean(item)); return ( @@ -129,6 +139,12 @@ function actionTarget(record: DigitalEmployeeAdminWorkbenchRow, view: DigitalEmp if (view === 'audits') { return { entityType: 'audit', entityId: record.entity_id }; } + if (view === 'policy_conflicts') { + return { + entityType: 'policy_snapshot', + entityId: record.policy_key || record.policy_kind, + }; + } return { entityType: record.entity_type, entityId: record.entity_id }; } @@ -286,6 +302,31 @@ const DigitalEmployeeOps: React.FC = () => { hideInTable: true, fieldProps: { placeholder: '输入 category', allowClear: true }, }, + { + title: '策略类型', + dataIndex: 'policy_kind', + width: 140, + valueType: 'select', + valueEnum: POLICY_KIND_OPTIONS, + fieldProps: { placeholder: '选择策略类型', allowClear: true }, + render: (_, record) => record.policy_kind ? {record.policy_kind} : '--', + }, + { + title: '冲突字段', + dataIndex: 'conflict_keys', + width: 220, + hideInSearch: true, + render: (_, record) => ( + + {(record.conflict_keys || []).slice(0, 3).map((item) => ( + + {item} + + ))} + {(!record.conflict_keys || record.conflict_keys.length === 0) && '--'} + + ), + }, { title: '等级', dataIndex: 'level', @@ -354,6 +395,52 @@ const DigitalEmployeeOps: React.FC = () => { const request = async (params: Record) => { const updatedRange = params.updated_range || []; + if (activeView === 'policy_conflicts') { + const response = await apiDigitalEmployeeAdminPolicySnapshots({ + pageNo: params.current || 1, + pageSize: params.pageSize || 15, + clientKey: params.client_key, + deviceId: params.device_id, + status: params.status, + policyKind: params.policy_kind || params.kind, + updatedFrom: updatedRange[0], + updatedTo: updatedRange[1], + }); + + if (response.code !== SUCCESS_CODE) { + message.error(response.message || '查询数字员工策略冲突失败'); + } + + return { + data: (response.data?.records || []).map((record, index) => ({ + view: 'policy_conflicts', + sync_record_id: index + 1, + sync_record_url: record.sync_record_url, + entity_type: 'policy_snapshot', + entity_id: record.policy_key || record.policy_kind || `policy-${index}`, + title: record.summary || record.policy_kind, + status: record.status, + kind: record.policy_kind, + category: 'policy_snapshot', + summary: record.conflict_keys?.length + ? `冲突字段:${record.conflict_keys.join(', ')}` + : record.summary, + policy_kind: record.policy_kind, + policy_key: record.policy_key, + conflict_keys: record.conflict_keys, + recommended_resolution: record.recommended_resolution, + latest_synced_at: record.latest_synced_at, + synced_at: record.latest_synced_at, + client_key_masked: record.client_key_masked, + device_id: record.device_id, + business_view: record.business_view, + payload: record.business_view, + })), + total: response.data?.total || 0, + success: response.code === SUCCESS_CODE, + }; + } + const response = await apiDigitalEmployeeAdminWorkbenchList({ view: activeView, pageNo: params.current || 1, @@ -438,10 +525,25 @@ const DigitalEmployeeOps: React.FC = () => { detailRecord.report_id && `Report ${detailRecord.report_id}`, detailRecord.route_decision_id && `Route ${detailRecord.route_decision_id}`, + detailRecord.policy_kind && `Policy ${detailRecord.policy_kind}`, ] .filter(Boolean) .join(' / ') || '--'} + {activeView === 'policy_conflicts' && ( + + 策略冲突: + {detailRecord.conflict_keys?.length + ? detailRecord.conflict_keys.join(' / ') + : '暂无冲突字段'} + {detailRecord.recommended_resolution && ( + + {' '} + · 建议:{detailRecord.recommended_resolution} + + )} + + )} {allowedActions(detailRecord, activeView).length > 0 && (
低风险动作 diff --git a/qiming/src/services/systemManage.ts b/qiming/src/services/systemManage.ts index cd3f1c4d..e69d4590 100644 --- a/qiming/src/services/systemManage.ts +++ b/qiming/src/services/systemManage.ts @@ -12,6 +12,8 @@ import type { ConversationStatsResult, DigitalEmployeeAdminWorkbenchListParams, DigitalEmployeeAdminWorkbenchPage, + DigitalEmployeePolicySnapshotListParams, + DigitalEmployeePolicySnapshotPage, DigitalEmployeeSyncRecordListParams, DigitalEmployeeSyncRecordPage, ModelConfigDto, @@ -312,6 +314,25 @@ export async function apiDigitalEmployeeAdminWorkbenchList( }); } +// 查询数字员工跨设备策略快照冲突 +export async function apiDigitalEmployeeAdminPolicySnapshots( + data: DigitalEmployeePolicySnapshotListParams, +): Promise> { + return request('/api/digital-employee/admin/policies/snapshots', { + method: 'GET', + params: { + client_key: data.clientKey, + device_id: data.deviceId, + status: data.status, + policy_kind: data.policyKind, + updated_from: data.updatedFrom, + updated_to: data.updatedTo, + page_no: data.pageNo, + page_size: data.pageSize, + }, + }); +} + // 记录数字员工管理端低风险动作意图 export async function apiDigitalEmployeeAdminActionSubmit( data: DigitalEmployeeAdminActionSubmitParams, diff --git a/qiming/src/types/interfaces/systemManage.ts b/qiming/src/types/interfaces/systemManage.ts index ab33058e..cc7e27ed 100644 --- a/qiming/src/types/interfaces/systemManage.ts +++ b/qiming/src/types/interfaces/systemManage.ts @@ -563,7 +563,8 @@ export type DigitalEmployeeAdminWorkbenchView = | 'notifications' | 'audits' | 'daily_reports' - | 'route_governance'; + | 'route_governance' + | 'policy_conflicts'; export interface DigitalEmployeeAdminWorkbenchRow { view: DigitalEmployeeAdminWorkbenchView | string; @@ -587,8 +588,13 @@ export interface DigitalEmployeeAdminWorkbenchRow { notification_id?: string; report_id?: string; route_decision_id?: string; + policy_kind?: string; + policy_key?: string; + conflict_keys?: string[]; + recommended_resolution?: string; occurred_at?: string; synced_at?: string; + latest_synced_at?: string; client_key_masked?: string; device_id?: string; business_view?: Record; @@ -618,6 +624,39 @@ export interface DigitalEmployeeAdminWorkbenchPage { records: DigitalEmployeeAdminWorkbenchRow[]; } +export interface DigitalEmployeePolicySnapshotRow { + policy_kind?: string; + policy_key?: string; + status?: string; + summary?: string; + client_count?: number; + device_count?: number; + conflict_keys?: string[]; + latest_synced_at?: string; + recommended_resolution?: string; + sync_record_url?: string; + client_key_masked?: string; + device_id?: string; + business_view?: Record; +} + +export interface DigitalEmployeePolicySnapshotListParams + extends SystemPaginationParams { + clientKey?: string; + deviceId?: string; + status?: string; + policyKind?: string; + updatedFrom?: string; + updatedTo?: string; +} + +export interface DigitalEmployeePolicySnapshotPage { + total: number; + page_no: number; + page_size: number; + records: DigitalEmployeePolicySnapshotRow[]; +} + export interface DigitalEmployeeAdminActionSubmitParams { syncRecordId?: number; clientKey?: string; diff --git a/qimingclaw/crates/agent-electron-client/scripts/check-digital-employee.js b/qimingclaw/crates/agent-electron-client/scripts/check-digital-employee.js index 1a1aa0c5..d3640b9c 100644 --- a/qimingclaw/crates/agent-electron-client/scripts/check-digital-employee.js +++ b/qimingclaw/crates/agent-electron-client/scripts/check-digital-employee.js @@ -1181,6 +1181,66 @@ function checkDigitalAdminActionLoop() { console.log("[DigitalEmployeeCheck] admin action pull/apply loop hooks OK"); } +function checkDigitalAdminPolicySnapshots() { + console.log("\n[DigitalEmployeeCheck] Verify admin policy snapshot conflict view hooks"); + const syncServicePath = path.join(packageRoot, "src", "main", "services", "digitalEmployee", "syncService.ts"); + const syncTestPath = path.join(packageRoot, "src", "main", "services", "digitalEmployee", "syncService.test.ts"); + const docsPath = path.resolve(packageRoot, "..", "..", "docs", "digital-employee-frontend-integration-plan.md"); + const qimingRoot = path.resolve(packageRoot, "..", "..", "..", "qiming"); + const backendRoot = path.resolve(packageRoot, "..", "..", "..", "qiming-backend"); + const qimingOpsPath = path.join(qimingRoot, "src", "pages", "SystemManagement", "Content", "DigitalEmployeeOps", "index.tsx"); + const qimingServicePath = path.join(qimingRoot, "src", "services", "systemManage.ts"); + const qimingTypesPath = path.join(qimingRoot, "src", "types", "interfaces", "systemManage.ts"); + const backendControllerPath = path.join( + backendRoot, + "app-platform-modules", + "app-platform-agent", + "app-platform-agent-core-ui", + "src", + "main", + "java", + "com", + "xspaceagi", + "agent", + "web", + "ui", + "controller", + "DigitalEmployeeAdminPolicyController.java", + ); + const syncService = fs.readFileSync(syncServicePath, "utf8"); + const syncTest = fs.readFileSync(syncTestPath, "utf8"); + const docs = fs.readFileSync(docsPath, "utf8"); + const qimingOps = fs.readFileSync(qimingOpsPath, "utf8"); + const qimingService = fs.readFileSync(qimingServicePath, "utf8"); + const qimingTypes = fs.readFileSync(qimingTypesPath, "utf8"); + const backendController = fs.readFileSync(backendControllerPath, "utf8"); + const requiredSnippets = [ + [syncService, "policySnapshotBusinessView", "client policy snapshot business view"], + [syncService, "policy_snapshot", "client policy snapshot category"], + [syncService, "policy_kind", "client policy kind field"], + [syncService, "auto_create_governance_commands", "route policy conflict field"], + [syncService, "max_per_sweep", "dispatch policy conflict field"], + [syncService, "catch_up_on_startup", "scheduler policy conflict field"], + [syncTest, "event-route-policy-pull", "route policy snapshot sync test"], + [syncTest, "event-risk-policy-pull", "risk policy snapshot sync test"], + [backendController, "/snapshots", "backend policy snapshot endpoint"], + [qimingService, "apiDigitalEmployeeAdminPolicySnapshots", "qiming policy snapshot service"], + [qimingService, "/api/digital-employee/admin/policies/snapshots", "qiming policy snapshot endpoint"], + [qimingTypes, "DigitalEmployeePolicySnapshotRow", "qiming policy snapshot row type"], + [qimingTypes, "policy_conflicts", "qiming policy conflicts view type"], + [qimingOps, "策略冲突", "qiming policy conflict tab text"], + [qimingOps, "conflict_keys", "qiming conflict key display"], + [docs, "跨设备策略冲突视图", "docs policy conflict absorption"], + ]; + const missing = requiredSnippets + .filter(([content, snippet]) => !content.includes(snippet)) + .map(([, , label]) => label); + if (missing.length > 0) { + throw new Error(`Missing admin policy snapshot hooks: ${missing.join(", ")}`); + } + console.log("[DigitalEmployeeCheck] admin policy snapshot conflict view hooks OK"); +} + function checkLocalDatabaseSchema() { console.log("\n[DigitalEmployeeCheck] Verify local SQLite digital schema"); const dbPath = path.join(os.homedir(), ".qimingclaw", "qimingclaw.db"); @@ -1240,6 +1300,7 @@ function main() { checkDigitalOpsNotificationReportWorkbenches(); checkDigitalPolicySchedulerWorkbenches(); checkDigitalAdminActionLoop(); + checkDigitalAdminPolicySnapshots(); checkLocalDatabaseSchema(); console.log("\n[DigitalEmployeeCheck] All checks passed"); } catch (error) { diff --git a/qimingclaw/crates/agent-electron-client/src/main/services/digitalEmployee/syncService.test.ts b/qimingclaw/crates/agent-electron-client/src/main/services/digitalEmployee/syncService.test.ts index f812f921..d89f1676 100644 --- a/qimingclaw/crates/agent-electron-client/src/main/services/digitalEmployee/syncService.test.ts +++ b/qimingclaw/crates/agent-electron-client/src/main/services/digitalEmployee/syncService.test.ts @@ -1655,6 +1655,41 @@ describe("digital employee sync service", () => { }, }, }); + insertEventEntity("event-risk-policy-pull", "digital_workday_pull_risk_approval_policy", { + metadata: { + action: "pull_risk_approval_policy", + source: "management-api", + ok: true, + risk_approval_policy: { + enabled: true, + source: "remote", + version: "risk-policy-v2", + approval_required_risk_levels: ["high", "critical"], + approval_required_actions: ["managed_service.restart.*"], + auto_reject_actions: ["service.stop.*"], + remote_updated_at: "2026-06-07T08:10:00.000Z", + last_pulled_at: "2026-06-07T08:11:00.000Z", + }, + }, + }); + insertEventEntity("event-route-policy-pull", "digital_workday_pull_route_decision_policy", { + metadata: { + action: "pull_route_decision_policy", + source: "management-api", + ok: true, + route_decision_policy: { + enabled: true, + source: "remote", + version: "route-policy-v3", + auto_create_governance_commands: false, + blocked_intents: ["delete"], + approval_required_intents: ["restart_service"], + preferred_route_kinds: ["PlanStepDispatch"], + remote_updated_at: "2026-06-07T08:12:00.000Z", + last_pulled_at: "2026-06-07T08:13:00.000Z", + }, + }, + }); insertEventEntity("event-risk-approval", "risk_approval_required", { action_kind: "managed_service.restart.crm-sync", action_label: "重启 CRM 同步服务", @@ -1701,6 +1736,8 @@ describe("digital employee sync service", () => { { entity_type: "event", entity_id: "event-approval-subscriptions" }, { entity_type: "event", entity_id: "event-plan-step-policy" }, { entity_type: "event", entity_id: "event-plan-step-policy-pull" }, + { entity_type: "event", entity_id: "event-risk-policy-pull" }, + { entity_type: "event", entity_id: "event-route-policy-pull" }, { entity_type: "event", entity_id: "event-risk-approval" }, ], }), @@ -1990,27 +2027,58 @@ describe("digital employee sync service", () => { expect(businessView("event-approval-subscriptions")).not.toHaveProperty("state"); expect(businessView("event-approval-subscriptions")).not.toHaveProperty("approvalSubscriptionPreferences"); expect(businessView("event-plan-step-policy")).toMatchObject({ - category: "dispatch", + category: "policy_snapshot", + policy_kind: "dispatch", + policy_key: "dispatch", action: "update_plan_step_dispatch_policy", - policy_enabled: false, + enabled: false, max_per_sweep: 1, auto_dispatch_ready_steps: false, - updated_at: "2026-06-07T08:07:00.000Z", + policy_version: "2026-06-07T08:07:00.000Z", }); expect(businessView("event-plan-step-policy")).not.toHaveProperty("state"); expect(businessView("event-plan-step-policy")).not.toHaveProperty("planStepDispatchPolicy"); expect(businessView("event-plan-step-policy-pull")).toMatchObject({ - category: "dispatch", + category: "policy_snapshot", + policy_kind: "dispatch", + policy_key: "dispatch", action: "pull_plan_step_dispatch_policy", source: "management-api", pull_ok: false, pull_error: "policy service unavailable", - policy_enabled: true, + enabled: true, max_per_sweep: 2, auto_dispatch_ready_steps: true, remote_updated_at: "2026-06-07T08:08:00.000Z", last_pulled_at: "2026-06-07T08:09:00.000Z", }); + expect(businessView("event-risk-policy-pull")).toMatchObject({ + category: "policy_snapshot", + policy_kind: "risk_approval", + policy_key: "risk_approval", + policy_version: "risk-policy-v2", + policy_source: "management-api", + enabled: true, + approval_required_risk_levels: ["high", "critical"], + approval_required_actions: ["managed_service.restart.*"], + auto_reject_actions: ["service.stop.*"], + remote_updated_at: "2026-06-07T08:10:00.000Z", + last_pulled_at: "2026-06-07T08:11:00.000Z", + }); + expect(businessView("event-route-policy-pull")).toMatchObject({ + category: "policy_snapshot", + policy_kind: "route_decision", + policy_key: "route_decision", + policy_version: "route-policy-v3", + policy_source: "management-api", + enabled: true, + auto_create_governance_commands: false, + blocked_intents: ["delete"], + approval_required_intents: ["restart_service"], + preferred_route_kinds: ["PlanStepDispatch"], + remote_updated_at: "2026-06-07T08:12:00.000Z", + last_pulled_at: "2026-06-07T08:13:00.000Z", + }); expect(businessView("event-risk-approval")).toMatchObject({ category: "approval", action_kind: "managed_service.restart.crm-sync", diff --git a/qimingclaw/crates/agent-electron-client/src/main/services/digitalEmployee/syncService.ts b/qimingclaw/crates/agent-electron-client/src/main/services/digitalEmployee/syncService.ts index 62984654..6a9d216e 100644 --- a/qimingclaw/crates/agent-electron-client/src/main/services/digitalEmployee/syncService.ts +++ b/qimingclaw/crates/agent-electron-client/src/main/services/digitalEmployee/syncService.ts @@ -1979,7 +1979,7 @@ function eventSpecificBusinessView( if (isPlanStepDependencyEvent(kind)) return planStepDependencyBusinessView(payload); if (kind.startsWith("plan_step_dispatch_")) return planStepDispatchBusinessView(payload); if (isPlanStepLeaseEvent(kind)) return planStepLeaseBusinessView(payload); - if (isPlanStepDispatchPolicyEvent(kind)) return planStepDispatchPolicyBusinessView(payload); + if (isPolicySnapshotEvent(kind)) return policySnapshotBusinessView(kind, payload); if (kind.startsWith("governance_")) return governanceCommandBusinessView(kind, payload); if (kind === "sandbox_audit") return sandboxAuditBusinessView(payload); if (kind === "token_cost") return tokenCostBusinessView(payload); @@ -2268,6 +2268,48 @@ function planStepDispatchPolicyBusinessView(payload: Record): R }; } +function policySnapshotBusinessView(kind: string, payload: Record): Record { + const metadata = objectRecord(payload.metadata) ?? {}; + const state = objectRecord(payload.state) ?? {}; + const policyKind = kind.includes("risk_approval") + ? "risk_approval" + : kind.includes("route_decision") + ? "route_decision" + : "dispatch"; + const policy = policyKind === "risk_approval" + ? objectRecord(metadata.risk_approval_policy) ?? objectRecord(state.riskApprovalPolicy) ?? {} + : policyKind === "route_decision" + ? objectRecord(metadata.route_decision_policy) ?? objectRecord(state.routeDecisionPolicy) ?? {} + : objectRecord(metadata.plan_step_dispatch_policy) ?? objectRecord(state.planStepDispatchPolicy) ?? {}; + return { + category: "policy_snapshot", + policy_kind: policyKind, + policy_key: policyKind, + policy_version: stringField(policy.version) || stringField(policy.updated_at ?? policy.updatedAt) || null, + policy_source: stringField(metadata.source ?? policy.source) || null, + source: stringField(metadata.source ?? policy.source) || null, + action: stringField(metadata.action ?? payload.action) || kind.replace(/^digital_workday_/, ""), + status: metadata.ok === false ? "pull_failed" : "snapshot", + enabled: typeof policy.enabled === "boolean" ? policy.enabled : null, + pull_ok: typeof metadata.ok === "boolean" ? metadata.ok : null, + pull_error: stringField(metadata.error ?? policy.last_pull_error) || null, + pull_endpoint: stringField(metadata.endpoint) || null, + max_per_sweep: numberField(policy.max_per_sweep ?? policy.maxPerSweep), + auto_dispatch_ready_steps: typeof policy.auto_dispatch_ready_steps === "boolean" ? policy.auto_dispatch_ready_steps : typeof policy.autoDispatchReadySteps === "boolean" ? policy.autoDispatchReadySteps : null, + auto_create_governance_commands: typeof policy.auto_create_governance_commands === "boolean" ? policy.auto_create_governance_commands : typeof policy.autoCreateGovernanceCommands === "boolean" ? policy.autoCreateGovernanceCommands : null, + catch_up_on_startup: typeof policy.catch_up_on_startup === "boolean" ? policy.catch_up_on_startup : typeof policy.catchUpOnStartup === "boolean" ? policy.catchUpOnStartup : null, + approval_required_risk_levels: stringArrayField(policy.approval_required_risk_levels ?? policy.approvalRequiredRiskLevels), + approval_required_actions: stringArrayField(policy.approval_required_actions ?? policy.approvalRequiredActions), + auto_reject_actions: stringArrayField(policy.auto_reject_actions ?? policy.autoRejectActions), + blocked_intents: stringArrayField(policy.blocked_intents ?? policy.blockedIntents), + approval_required_intents: stringArrayField(policy.approval_required_intents ?? policy.approvalRequiredIntents), + preferred_route_kinds: stringArrayField(policy.preferred_route_kinds ?? policy.preferredRouteKinds), + remote_updated_at: stringField(policy.remote_updated_at ?? policy.remoteUpdatedAt) || null, + last_pulled_at: stringField(policy.last_pulled_at ?? policy.lastPulledAt) || null, + summary: `${policyKind} policy snapshot`, + }; +} + function governanceCommandBusinessView(kind: string, payload: Record): Record { const commandPayload = objectRecord(payload.payload) ?? {}; const result = objectRecord(payload.result) ?? {}; @@ -2383,7 +2425,7 @@ function eventBusinessCategory(kind: string): string { if (isPlanStepDependencyEvent(kind)) return "dispatch"; if (kind.startsWith("plan_step_dispatch_")) return "dispatch"; if (isPlanStepLeaseEvent(kind)) return "dispatch"; - if (isPlanStepDispatchPolicyEvent(kind)) return "dispatch"; + if (isPolicySnapshotEvent(kind)) return "policy_snapshot"; if (isNotificationEvent(kind)) return "notification"; if (kind.startsWith("governance_")) return "governance"; if (kind === "sandbox_audit") return "sandbox_audit"; @@ -2419,6 +2461,14 @@ function isPlanStepDispatchPolicyEvent(kind: string): boolean { || kind === "digital_workday_pull_plan_step_dispatch_policy"; } +function isPolicySnapshotEvent(kind: string): boolean { + return isPlanStepDispatchPolicyEvent(kind) + || kind === "digital_workday_pull_risk_approval_policy" + || kind === "digital_workday_pull_route_decision_policy" + || kind === "digital_workday_update_risk_approval_policy" + || kind === "digital_workday_update_route_decision_policy"; +} + function isPlanStepLeaseEvent(kind: string): boolean { return kind === "plan_step_lease_acquired" || kind === "plan_step_lease_released" || kind === "plan_step_lease_skipped"; } diff --git a/qimingclaw/docs/digital-employee-frontend-integration-plan.md b/qimingclaw/docs/digital-employee-frontend-integration-plan.md index 017b9811..4127610b 100644 --- a/qimingclaw/docs/digital-employee-frontend-integration-plan.md +++ b/qimingclaw/docs/digital-employee-frontend-integration-plan.md @@ -609,7 +609,7 @@ GET /api/digital-employee/approvals - 建议:下一轮围绕管理端正式审计视图和诊断修复动作,把本地可追溯审计继续推进到可运营处置。 12. **管理端业务查询模型** - - 已吸收:管理端已有通用 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 投影生成统一分页视图;embedded 数字员工页已新增“业务视图”入口,支持计划/任务/运行/事件/产物/审批分段、实体/状态/同步状态/时间筛选、分页和详情 JSON 查看;客户端 outbox 的 event `business_view` 已开始按 route decision、governance command、approval action、artifact lifecycle/access、skill call audit 和 ready PlanStep dispatch 细分常用字段,本地与管理端业务视图投影已开始闭环。正式外部管理端台面已开始吸收:qiming-backend 新增 `/api/digital-employee/admin/workbench/{view}`,qiming 管理端新增“数字员工运营台”,统一展示业务视图、介入台、产物治理、通知运营、审计诊断、日报运营和路由治理,只读消费 sync record `business_view` 并提供同步记录深链。管理端低风险动作闭环已开始吸收:后端新增 `/api/digital-employee/admin/actions` 记录动作意图和 `/pending` 拉取队列,qimingclaw 只应用保留本地、标记复核、通知已读/忽略、产物保留/到期、日报交付/确认/审批请求等低风险动作并写入 `admin_action_applied` / `admin_action_rejected` / `admin_action_failed`,qiming 运营台抽屉只提示“已记录,等待客户端拉取”。 + - 已吸收:管理端已有通用 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 投影生成统一分页视图;embedded 数字员工页已新增“业务视图”入口,支持计划/任务/运行/事件/产物/审批分段、实体/状态/同步状态/时间筛选、分页和详情 JSON 查看;客户端 outbox 的 event `business_view` 已开始按 route decision、governance command、approval action、artifact lifecycle/access、skill call audit 和 ready PlanStep dispatch 细分常用字段,本地与管理端业务视图投影已开始闭环。正式外部管理端台面已开始吸收:qiming-backend 新增 `/api/digital-employee/admin/workbench/{view}`,qiming 管理端新增“数字员工运营台”,统一展示业务视图、介入台、产物治理、通知运营、审计诊断、日报运营和路由治理,只读消费 sync record `business_view` 并提供同步记录深链。管理端低风险动作闭环已开始吸收:后端新增 `/api/digital-employee/admin/actions` 记录动作意图和 `/pending` 拉取队列,qimingclaw 只应用保留本地、标记复核、通知已读/忽略、产物保留/到期、日报交付/确认/审批请求等低风险动作并写入 `admin_action_applied` / `admin_action_rejected` / `admin_action_failed`,qiming 运营台抽屉只提示“已记录,等待客户端拉取”。跨设备策略冲突视图已开始吸收:qimingclaw 将派发、风险审批和入口路由策略同步事件投影为 `policy_snapshot` business view,后端新增 `/api/digital-employee/admin/policies/snapshots` 聚合 client/device 维度差异并输出 `conflict_keys`,qiming 运营台新增“策略冲突” tab 展示 policy drift 与 sync record 深链。 - 缺口:复杂组合检索、跨设备聚合图、远程裁决/策略编辑动作和物化业务表仍待吸收。 - 建议:先使用 sync record 派生业务查询模型和 embedded 业务视图预览承接联调,再按容量和查询压力决定是否拆 Plan / Task / Run / Event 物理业务表。