吸收数字员工资产通知日报业务收口

This commit is contained in:
baiyanyun
2026-06-15 14:59:29 +08:00
parent 87b440e93f
commit de33710423
8 changed files with 327 additions and 10 deletions

View File

@@ -72,8 +72,14 @@ public class DigitalEmployeeAdminWorkbenchRowDto {
private Boolean hardInterruptDisabled;
@JsonProperty("artifact_id")
private String artifactId;
@JsonProperty("artifact_graph_id")
private String artifactGraphId;
@JsonProperty("artifact_group_id")
private String artifactGroupId;
@JsonProperty("group_key")
private String groupKey;
@JsonProperty("related_artifact_ids")
private List<String> relatedArtifactIds;
@JsonProperty("artifact_count")
private Long artifactCount;
@JsonProperty("device_count")
@@ -90,10 +96,20 @@ public class DigitalEmployeeAdminWorkbenchRowDto {
private Long cleanupEligibleCount;
@JsonProperty("batch_cleanup_disabled")
private Boolean batchCleanupDisabled;
@JsonProperty("retention_conflict_status")
private String retentionConflictStatus;
@JsonProperty("cleanup_conflict_status")
private String cleanupConflictStatus;
@JsonProperty("remote_delete_disabled")
private Boolean remoteDeleteDisabled;
@JsonProperty("notification_id")
private String notificationId;
@JsonProperty("collaboration_thread_id")
private String collaborationThreadId;
@JsonProperty("subscription_policy_id")
private String subscriptionPolicyId;
@JsonProperty("subscription_conflict_status")
private String subscriptionConflictStatus;
@JsonProperty("notification_policy_id")
private String notificationPolicyId;
@JsonProperty("subscription_status")
@@ -105,16 +121,38 @@ public class DigitalEmployeeAdminWorkbenchRowDto {
private String channel;
@JsonProperty("delivery_status")
private String deliveryStatus;
@JsonProperty("delivery_attempt_id")
private String deliveryAttemptId;
@JsonProperty("send_request_id")
private String sendRequestId;
@JsonProperty("real_push_disabled")
private Boolean realPushDisabled;
@JsonProperty("conflict_status")
private String conflictStatus;
@JsonProperty("report_id")
private String reportId;
@JsonProperty("daily_report_fact_id")
private String dailyReportFactId;
@JsonProperty("daily_report_physical_fact_status")
private String dailyReportPhysicalFactStatus;
@JsonProperty("send_record_id")
private String sendRecordId;
@JsonProperty("send_channel")
private String sendChannel;
@JsonProperty("send_request_status")
private String sendRequestStatus;
@JsonProperty("external_send_disabled")
private Boolean externalSendDisabled;
@JsonProperty("third_party_channel_disabled")
private Boolean thirdPartyChannelDisabled;
@JsonProperty("approval_decision_status")
private String approvalDecisionStatus;
@JsonProperty("advanced_query_supported")
private Boolean advancedQuerySupported;
@JsonProperty("aggregation_scope")
private String aggregationScope;
@JsonProperty("materialized_source")
private String materializedSource;
@JsonProperty("daily_report_approval_id")
private String dailyReportApprovalId;
@JsonProperty("daily_report_approval_action")

View File

@@ -58,5 +58,11 @@ public class DigitalEmployeeBusinessViewRowDto {
private String latestEventAt;
@JsonProperty("last_event_message")
private String lastEventMessage;
@JsonProperty("advanced_query_supported")
private Boolean advancedQuerySupported;
@JsonProperty("aggregation_scope")
private String aggregationScope;
@JsonProperty("materialized_source")
private String materializedSource;
private Boolean materialized;
}

View File

@@ -71,7 +71,8 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
"notification:mark_read", "notification:dismiss",
"notification_policy:pull_policy", "notification_policy:mark_reviewed", "notification_policy:dismiss",
"artifact:mark_keep", "artifact:mark_expire", "artifact:mark_reviewed", "artifact:record_cleanup_request",
"daily_report:mark_delivered", "daily_report:mark_confirmed", "daily_report:request_approval", "daily_report:record_approval_decision",
"notification:record_send_request", "notification:record_send_result",
"daily_report:mark_delivered", "daily_report:mark_confirmed", "daily_report:request_approval", "daily_report:record_approval_decision", "daily_report:record_send_request", "daily_report:record_send_result",
"memory:archive_memory", "memory:restore_memory", "memory:mark_reviewed",
"memory_policy:pull_policy", "memory_policy:mark_reviewed", "memory_policy:dismiss",
"skill:mark_reviewed", "skill:dismiss",
@@ -1028,6 +1029,11 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
case "auto_merge_policy" -> List.of("auto_merge_policy_disabled");
case "auto_decide_approval" -> List.of("auto_decision_disabled");
case "route_recover_execute" -> List.of("route_recovery_disabled");
case "cleanup_file_batch" -> List.of("batch_cleanup_disabled");
case "remote_delete_artifact" -> List.of("remote_delete_disabled");
case "real_push_notification" -> List.of("real_push_disabled");
case "external_send_daily_report" -> List.of("external_send_disabled");
case "third_party_send" -> List.of("third_party_channel_disabled");
case "run_schedule_now" -> List.of("run_schedule_now_disabled");
case "start_scheduler" -> List.of("start_scheduler_disabled");
case "hard_interrupt" -> List.of("hard_interrupt_disabled");
@@ -1114,7 +1120,10 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
.policyGuardStatus(firstString(businessView.get("policy_guard_status"), businessView.get("policyGuardStatus"), payload.get("policy_guard_status"), payload.get("policyGuardStatus")))
.hardInterruptDisabled(nullableBooleanValue(firstPresent(businessView.get("hard_interrupt_disabled"), businessView.get("hardInterruptDisabled"), payload.get("hard_interrupt_disabled"), payload.get("hardInterruptDisabled"))))
.artifactId(firstString(businessView.get("artifact_id"), payload.get("artifact_id"), "artifact".equals(record.getEntityType()) ? record.getEntityId() : null))
.artifactGraphId(firstString(businessView.get("artifact_graph_id"), businessView.get("artifactGraphId"), payload.get("artifact_graph_id"), payload.get("artifactGraphId")))
.artifactGroupId(firstString(businessView.get("artifact_group_id"), businessView.get("artifactGroupId"), payload.get("artifact_group_id"), payload.get("artifactGroupId")))
.groupKey(firstString(businessView.get("group_key"), businessView.get("groupKey"), payload.get("group_key"), payload.get("groupKey")))
.relatedArtifactIds(stringListValue(firstPresent(businessView.get("related_artifact_ids"), businessView.get("relatedArtifactIds"), payload.get("related_artifact_ids"), payload.get("relatedArtifactIds"))))
.artifactCount(numberValue(firstPresent(businessView.get("artifact_count"), businessView.get("artifactCount"), payload.get("artifact_count"), payload.get("artifactCount"))))
.deviceCount(numberValue(firstPresent(businessView.get("device_count"), businessView.get("deviceCount"), payload.get("device_count"), payload.get("deviceCount"))))
.versionCount(numberValue(firstPresent(businessView.get("version_count"), businessView.get("versionCount"), payload.get("version_count"), payload.get("versionCount"))))
@@ -1123,19 +1132,35 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
.cleanupRequestId(firstString(businessView.get("cleanup_request_id"), businessView.get("cleanupRequestId"), payload.get("cleanup_request_id"), payload.get("cleanupRequestId")))
.cleanupEligibleCount(numberValue(firstPresent(businessView.get("cleanup_eligible_count"), businessView.get("cleanupEligibleCount"), payload.get("cleanup_eligible_count"), payload.get("cleanupEligibleCount"))))
.batchCleanupDisabled(nullableBooleanValue(firstPresent(businessView.get("batch_cleanup_disabled"), businessView.get("batchCleanupDisabled"), payload.get("batch_cleanup_disabled"), payload.get("batchCleanupDisabled"))))
.retentionConflictStatus(firstString(businessView.get("retention_conflict_status"), businessView.get("retentionConflictStatus"), payload.get("retention_conflict_status"), payload.get("retentionConflictStatus")))
.cleanupConflictStatus(firstString(businessView.get("cleanup_conflict_status"), businessView.get("cleanupConflictStatus"), payload.get("cleanup_conflict_status"), payload.get("cleanupConflictStatus")))
.remoteDeleteDisabled(nullableBooleanValue(firstPresent(businessView.get("remote_delete_disabled"), businessView.get("remoteDeleteDisabled"), payload.get("remote_delete_disabled"), payload.get("remoteDeleteDisabled"))))
.notificationId(firstString(businessView.get("notification_id"), payload.get("notification_id")))
.collaborationThreadId(firstString(businessView.get("collaboration_thread_id"), businessView.get("collaborationThreadId"), payload.get("collaboration_thread_id"), payload.get("collaborationThreadId")))
.subscriptionPolicyId(firstString(businessView.get("subscription_policy_id"), businessView.get("subscriptionPolicyId"), payload.get("subscription_policy_id"), payload.get("subscriptionPolicyId")))
.subscriptionConflictStatus(firstString(businessView.get("subscription_conflict_status"), businessView.get("subscriptionConflictStatus"), payload.get("subscription_conflict_status"), payload.get("subscriptionConflictStatus")))
.notificationPolicyId(firstString(businessView.get("notification_policy_id"), businessView.get("notificationPolicyId"), payload.get("notification_policy_id"), payload.get("notificationPolicyId")))
.subscriptionStatus(firstString(businessView.get("subscription_status"), businessView.get("subscriptionStatus"), payload.get("subscription_status"), payload.get("subscriptionStatus")))
.desktopEnabled(nullableBooleanValue(firstPresent(businessView.get("desktop_enabled"), businessView.get("desktopEnabled"), payload.get("desktop_enabled"), payload.get("desktopEnabled"))))
.minimumLevel(firstString(businessView.get("minimum_level"), businessView.get("minimumLevel"), payload.get("minimum_level"), payload.get("minimumLevel")))
.channel(firstString(businessView.get("channel"), payload.get("channel")))
.deliveryStatus(firstString(businessView.get("delivery_status"), businessView.get("deliveryStatus"), payload.get("delivery_status"), payload.get("deliveryStatus")))
.deliveryAttemptId(firstString(businessView.get("delivery_attempt_id"), businessView.get("deliveryAttemptId"), payload.get("delivery_attempt_id"), payload.get("deliveryAttemptId")))
.sendRequestId(firstString(businessView.get("send_request_id"), businessView.get("sendRequestId"), payload.get("send_request_id"), payload.get("sendRequestId")))
.realPushDisabled(nullableBooleanValue(firstPresent(businessView.get("real_push_disabled"), businessView.get("realPushDisabled"), payload.get("real_push_disabled"), payload.get("realPushDisabled"))))
.conflictStatus(firstString(businessView.get("conflict_status"), businessView.get("conflictStatus"), payload.get("conflict_status"), payload.get("conflictStatus")))
.reportId(firstString(businessView.get("report_id"), payload.get("report_id")))
.dailyReportFactId(firstString(businessView.get("daily_report_fact_id"), businessView.get("dailyReportFactId"), payload.get("daily_report_fact_id"), payload.get("dailyReportFactId")))
.dailyReportPhysicalFactStatus(firstString(businessView.get("daily_report_physical_fact_status"), businessView.get("dailyReportPhysicalFactStatus"), payload.get("daily_report_physical_fact_status"), payload.get("dailyReportPhysicalFactStatus")))
.sendRecordId(firstString(businessView.get("send_record_id"), businessView.get("sendRecordId"), payload.get("send_record_id"), payload.get("sendRecordId")))
.sendChannel(firstString(businessView.get("send_channel"), businessView.get("sendChannel"), payload.get("send_channel"), payload.get("sendChannel")))
.sendRequestStatus(firstString(businessView.get("send_request_status"), businessView.get("sendRequestStatus"), payload.get("send_request_status"), payload.get("sendRequestStatus")))
.externalSendDisabled(nullableBooleanValue(firstPresent(businessView.get("external_send_disabled"), businessView.get("externalSendDisabled"), payload.get("external_send_disabled"), payload.get("externalSendDisabled"))))
.thirdPartyChannelDisabled(nullableBooleanValue(firstPresent(businessView.get("third_party_channel_disabled"), businessView.get("thirdPartyChannelDisabled"), payload.get("third_party_channel_disabled"), payload.get("thirdPartyChannelDisabled"))))
.approvalDecisionStatus(firstString(businessView.get("approval_decision_status"), businessView.get("approvalDecisionStatus"), payload.get("approval_decision_status"), payload.get("approvalDecisionStatus")))
.advancedQuerySupported(nullableBooleanValue(firstPresent(businessView.get("advanced_query_supported"), businessView.get("advancedQuerySupported"), payload.get("advanced_query_supported"), payload.get("advancedQuerySupported"))))
.aggregationScope(firstString(businessView.get("aggregation_scope"), businessView.get("aggregationScope"), payload.get("aggregation_scope"), payload.get("aggregationScope")))
.materializedSource(firstString(businessView.get("materialized_source"), businessView.get("materializedSource"), payload.get("materialized_source"), payload.get("materializedSource")))
.dailyReportApprovalId(firstString(businessView.get("daily_report_approval_id"), businessView.get("dailyReportApprovalId"), payload.get("daily_report_approval_id"), payload.get("dailyReportApprovalId")))
.dailyReportApprovalAction(firstString(businessView.get("daily_report_approval_action"), businessView.get("dailyReportApprovalAction"), payload.get("daily_report_approval_action"), payload.get("dailyReportApprovalAction")))
.dailyReportApprovalActor(firstString(businessView.get("daily_report_approval_actor"), businessView.get("dailyReportApprovalActor"), payload.get("daily_report_approval_actor"), payload.get("dailyReportApprovalActor")))
@@ -1304,9 +1329,14 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
return StringUtils.equals(row.getEntityType(), "artifact")
|| startsWithAny(row.getKind(), "artifact_")
|| StringUtils.equals(row.getCategory(), "artifact_lifecycle")
|| StringUtils.isNotBlank(row.getArtifactGraphId())
|| StringUtils.isNotBlank(row.getArtifactGroupId())
|| StringUtils.isNotBlank(row.getGroupKey())
|| StringUtils.isNotBlank(row.getLatestArtifactId())
|| row.getArtifactCount() != null;
|| row.getArtifactCount() != null
|| StringUtils.isNotBlank(row.getRetentionConflictStatus())
|| StringUtils.isNotBlank(row.getCleanupConflictStatus())
|| row.getRemoteDeleteDisabled() != null;
}
private boolean isServiceOperationsRow(DigitalEmployeeAdminWorkbenchRowDto row) {
@@ -1338,6 +1368,10 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
return StringUtils.equals(row.getCategory(), "notification")
|| StringUtils.equals(row.getCategory(), "action_notification")
|| contains(row.getKind(), "notification")
|| StringUtils.isNotBlank(row.getCollaborationThreadId())
|| StringUtils.isNotBlank(row.getDeliveryAttemptId())
|| StringUtils.isNotBlank(row.getSendRequestId())
|| row.getRealPushDisabled() != null
|| StringUtils.isNotBlank(row.getNotificationId());
}
@@ -1381,6 +1415,11 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
return StringUtils.equals(row.getCategory(), "daily_report")
|| startsWithAny(row.getKind(), "daily_report")
|| StringUtils.isNotBlank(row.getReportId())
|| StringUtils.isNotBlank(row.getDailyReportPhysicalFactStatus())
|| StringUtils.isNotBlank(row.getSendChannel())
|| StringUtils.isNotBlank(row.getSendRequestStatus())
|| row.getExternalSendDisabled() != null
|| row.getThirdPartyChannelDisabled() != null
|| StringUtils.isNotBlank(row.getDailyReportApprovalId())
|| StringUtils.isNotBlank(row.getDailyReportApprovalAction());
}
@@ -1702,6 +1741,9 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
.businessView(businessView)
.payload(payload)
.lastEventMessage(lastEventMessage(entityType, businessView, payload))
.advancedQuerySupported(nullableBooleanValue(firstPresent(businessView.get("advanced_query_supported"), businessView.get("advancedQuerySupported"), payload.get("advanced_query_supported"), payload.get("advancedQuerySupported"))))
.aggregationScope(firstString(businessView.get("aggregation_scope"), businessView.get("aggregationScope"), payload.get("aggregation_scope"), payload.get("aggregationScope")))
.materializedSource(firstString(businessView.get("materialized_source"), businessView.get("materializedSource"), payload.get("materialized_source"), payload.get("materializedSource")))
.materialized(false)
.build();
}
@@ -1733,6 +1775,9 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
.businessView(readJsonObject(fact.getBusinessView()))
.payload(readJsonObject(fact.getPayload()))
.lastEventMessage(lastEventMessage(fact.getEntityType(), readJsonObject(fact.getBusinessView()), readJsonObject(fact.getPayload())))
.advancedQuerySupported(nullableBooleanValue(firstPresent(readJsonObject(fact.getBusinessView()).get("advanced_query_supported"), readJsonObject(fact.getBusinessView()).get("advancedQuerySupported"))))
.aggregationScope(firstString(readJsonObject(fact.getBusinessView()).get("aggregation_scope"), readJsonObject(fact.getBusinessView()).get("aggregationScope")))
.materializedSource(firstString(readJsonObject(fact.getBusinessView()).get("materialized_source"), readJsonObject(fact.getBusinessView()).get("materializedSource")))
.materialized(true)
.build();
}

View File

@@ -183,6 +183,30 @@ class DigitalEmployeeSyncApplicationServiceImplTest {
verify(repository, never()).queryBusinessRecords(any(), any(), any(), any(), any(), any(), any(), any(), any(Long.class), any(Long.class));
}
@Test
void queryBusinessViewsExposeAdvancedQueryAndMaterializedSource() {
when(repository.queryBusinessRecords(eq(101L), eq(null), eq(null), eq("plan"), eq(null), eq(null), eq(null), eq(null), eq(1L), eq(20L)))
.thenReturn(page(List.of(
record("outbox-plan-query", "plan", "plan-query-1", 101L, new Date(10_000),
Map.of(
"entity_type", "plan",
"title", "组合检索计划",
"status", "active",
"advanced_query_supported", true,
"aggregation_scope", "client_device",
"materialized_source", "sync_record_fallback"
),
Map.of("id", "plan-query-1", "status", "active"))
)));
DigitalEmployeeBusinessViewPageDto result = service.queryBusinessViews("plan", null, null, null, null, null, null, null, 1L, 20L);
assertThat(result.getRecords()).hasSize(1);
assertThat(result.getRecords().get(0).getAdvancedQuerySupported()).isTrue();
assertThat(result.getRecords().get(0).getAggregationScope()).isEqualTo("client_device");
assertThat(result.getRecords().get(0).getMaterializedSource()).isEqualTo("sync_record_fallback");
}
@Test
void queryEventsFiltersStatusEntityAndTimeRangeFromBusinessView() {
Date from = new Date(10_000);
@@ -1476,9 +1500,14 @@ class DigitalEmployeeSyncApplicationServiceImplTest {
Map.entry("status", "unread"),
Map.entry("title", "通知未读"),
Map.entry("notification_id", "notification-1"),
Map.entry("collaboration_thread_id", "thread-1"),
Map.entry("subscription_policy_id", "subscription-policy-1"),
Map.entry("subscription_conflict_status", "conflict"),
Map.entry("channel", "desktop"),
Map.entry("delivery_status", "delivered"),
Map.entry("delivery_attempt_id", "delivery-attempt-1"),
Map.entry("send_request_id", "send-request-1"),
Map.entry("real_push_disabled", true),
Map.entry("conflict_status", "none"),
Map.entry("summary", "等待处理")
),
@@ -1528,14 +1557,24 @@ class DigitalEmployeeSyncApplicationServiceImplTest {
assertThat(result.getRecords().get(0).getCategory()).isEqualTo("notification");
assertThat(result.getRecords().get(0).getStatus()).isEqualTo("unread");
assertThat(result.getRecords().get(0).getSummary()).isEqualTo("等待处理");
assertThat(result.getRecords().get(0).getCollaborationThreadId()).isEqualTo("thread-1");
assertThat(result.getRecords().get(0).getSubscriptionPolicyId()).isEqualTo("subscription-policy-1");
assertThat(result.getRecords().get(0).getSubscriptionConflictStatus()).isEqualTo("conflict");
assertThat(result.getRecords().get(0).getChannel()).isEqualTo("desktop");
assertThat(result.getRecords().get(0).getDeliveryStatus()).isEqualTo("delivered");
assertThat(result.getRecords().get(0).getDeliveryAttemptId()).isEqualTo("delivery-attempt-1");
assertThat(result.getRecords().get(0).getSendRequestId()).isEqualTo("send-request-1");
assertThat(result.getRecords().get(0).getRealPushDisabled()).isTrue();
assertThat(result.getRecords().get(0).getConflictStatus()).isEqualTo("none");
assertThat(result.getRecords().get(1).getCategory()).isEqualTo("action_notification");
assertThat(result.getRecords().get(2).getNotificationId()).isEqualTo("notification-preference");
}
@Test
void queryAdminWorkbenchNotificationsReturnsCollaborationAndDeliveryRows() {
queryAdminWorkbenchNotificationsReturnsNotificationRowsOnly();
}
@Test
void queryAdminWorkbenchNotificationPoliciesReturnsSubscriptionPolicyRowsOnly() {
when(repository.queryBusinessRecords(eq(101L), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), eq(1L), eq(20L)))
@@ -1611,11 +1650,17 @@ class DigitalEmployeeSyncApplicationServiceImplTest {
"title", "日报 PDF",
"status", "available",
"artifact_id", "artifact-1",
"artifact_graph_id", "artifact-graph-1",
"group_key", "daily-report:pdf",
"related_artifact_ids", List.of("artifact-2", "artifact-3"),
"retention_status", "expired",
"cleanup_status", "pending",
"retention_conflict_status", "conflict",
"cleanup_conflict_status", "pending_review",
"cleanup_request_id", "cleanup-request-1",
"cleanup_eligible_count", 3,
"batch_cleanup_disabled", true,
"remote_delete_disabled", true,
"artifact_group_id", "daily-report-2026-06-13",
"artifact_count", 4,
"device_count", 2,
@@ -1679,18 +1724,29 @@ class DigitalEmployeeSyncApplicationServiceImplTest {
assertThat(result.getRecords().get(0).getCleanupRequestId()).isEqualTo("cleanup-request-1");
assertThat(result.getRecords().get(0).getCleanupEligibleCount()).isEqualTo(3L);
assertThat(result.getRecords().get(0).getBatchCleanupDisabled()).isTrue();
assertThat(result.getRecords().get(0).getArtifactGraphId()).isEqualTo("artifact-graph-1");
assertThat(result.getRecords().get(0).getArtifactGroupId()).isEqualTo("daily-report-2026-06-13");
assertThat(result.getRecords().get(0).getGroupKey()).isEqualTo("daily-report:pdf");
assertThat(result.getRecords().get(0).getRelatedArtifactIds()).containsExactly("artifact-2", "artifact-3");
assertThat(result.getRecords().get(0).getArtifactCount()).isEqualTo(4L);
assertThat(result.getRecords().get(0).getDeviceCount()).isEqualTo(2L);
assertThat(result.getRecords().get(0).getVersionCount()).isEqualTo(3L);
assertThat(result.getRecords().get(0).getLatestArtifactId()).isEqualTo("artifact-4");
assertThat(result.getRecords().get(0).getVersionKey()).isEqualTo("daily-report:2026-06-13");
assertThat(result.getRecords().get(0).getRetentionConflictStatus()).isEqualTo("conflict");
assertThat(result.getRecords().get(0).getCleanupConflictStatus()).isEqualTo("pending_review");
assertThat(result.getRecords().get(0).getRemoteDeleteDisabled()).isTrue();
assertThat(result.getRecords().get(0).getBusinessView()).containsEntry("retention_status", "expired");
assertThat(result.getRecords().get(1).getArtifactId()).isEqualTo("artifact-2");
assertThat(result.getRecords().get(2).getBusinessView()).containsEntry("cleanup_status", "deleted");
assertThat(result.getRecords().get(2).getBusinessView()).containsEntry("deleted_at", "2026-06-13T00:00:00+08:00");
}
@Test
void queryAdminWorkbenchArtifactLifecycleReturnsGraphAndConflictRows() {
queryAdminWorkbenchArtifactLifecycleReturnsArtifactRowsOnly();
}
@Test
void queryAdminWorkbenchAuditsReturnsAuditRowsOnly() {
when(repository.queryBusinessRecords(eq(101L), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), eq(1L), eq(20L)))
@@ -1951,7 +2007,12 @@ class DigitalEmployeeSyncApplicationServiceImplTest {
Map.entry("title", "6 13 日日报"),
Map.entry("report_id", "report-1"),
Map.entry("daily_report_fact_id", "daily-report-fact-1"),
Map.entry("daily_report_physical_fact_status", "pending_materialization"),
Map.entry("send_record_id", "send-record-1"),
Map.entry("send_channel", "email"),
Map.entry("send_request_status", "recorded"),
Map.entry("external_send_disabled", true),
Map.entry("third_party_channel_disabled", true),
Map.entry("approval_decision_status", "pending"),
Map.entry("summary", "日报已生成"),
Map.entry("artifact_id", "artifact-report-1")
@@ -2021,7 +2082,12 @@ class DigitalEmployeeSyncApplicationServiceImplTest {
assertThat(result.getRecords().get(0).getStatus()).isEqualTo("generated");
assertThat(result.getRecords().get(0).getSummary()).isEqualTo("日报已生成");
assertThat(result.getRecords().get(0).getDailyReportFactId()).isEqualTo("daily-report-fact-1");
assertThat(result.getRecords().get(0).getDailyReportPhysicalFactStatus()).isEqualTo("pending_materialization");
assertThat(result.getRecords().get(0).getSendRecordId()).isEqualTo("send-record-1");
assertThat(result.getRecords().get(0).getSendChannel()).isEqualTo("email");
assertThat(result.getRecords().get(0).getSendRequestStatus()).isEqualTo("recorded");
assertThat(result.getRecords().get(0).getExternalSendDisabled()).isTrue();
assertThat(result.getRecords().get(0).getThirdPartyChannelDisabled()).isTrue();
assertThat(result.getRecords().get(0).getApprovalDecisionStatus()).isEqualTo("pending");
assertThat(result.getRecords().get(1).getReportId()).isEqualTo("report-2");
assertThat(result.getRecords().get(1).getBusinessView()).containsEntry("delivery_status", "delivered");
@@ -2033,6 +2099,11 @@ class DigitalEmployeeSyncApplicationServiceImplTest {
assertThat(result.getRecords().get(3).getDailyReportApprovalRecordedAt()).isEqualTo("2026-06-13T10:30:00+08:00");
}
@Test
void queryAdminWorkbenchDailyReportsReturnsPhysicalFactAndSendShellRows() {
queryAdminWorkbenchDailyReportsReturnsDailyReportRowsOnly();
}
@Test
void createAdminActionRecordsLowRiskActionAsPending() {
DigitalEmployeeAdminActionRequestDto request = adminActionRequest("approval", "approval-1", "keep_local");
@@ -2062,12 +2133,33 @@ class DigitalEmployeeSyncApplicationServiceImplTest {
assertThat(result.getOk()).isFalse();
assertThat(result.getStatus()).isEqualTo("rejected");
assertThat(result.getReasonCodes()).contains("high_risk_action_disabled");
assertThat(result.getReasonCodes()).contains("batch_cleanup_disabled");
ArgumentCaptor<DigitalEmployeeAdminAction> captor = ArgumentCaptor.forClass(DigitalEmployeeAdminAction.class);
verify(adminActionRepository).save(captor.capture());
assertThat(captor.getValue().getStatus()).isEqualTo("rejected");
}
@Test
void createAdminActionAllowsLowRiskAssetNotificationReportIntents() {
assertThat(service.createAdminAction(adminActionRequest("artifact", "artifact-1", "record_cleanup_request")).getOk()).isTrue();
assertThat(service.createAdminAction(adminActionRequest("notification", "notification-1", "record_send_request")).getOk()).isTrue();
assertThat(service.createAdminAction(adminActionRequest("notification", "notification-1", "record_send_result")).getOk()).isTrue();
assertThat(service.createAdminAction(adminActionRequest("daily_report", "report-1", "record_send_request")).getOk()).isTrue();
assertThat(service.createAdminAction(adminActionRequest("daily_report", "report-1", "record_send_result")).getOk()).isTrue();
assertThat(service.createAdminAction(adminActionRequest("daily_report", "report-1", "request_approval")).getOk()).isTrue();
assertThat(service.createAdminAction(adminActionRequest("daily_report", "report-1", "mark_delivered")).getOk()).isTrue();
assertThat(service.createAdminAction(adminActionRequest("daily_report", "report-1", "mark_confirmed")).getOk()).isTrue();
}
@Test
void createAdminActionRejectsHighRiskAssetNotificationReportExecution() {
assertThat(service.createAdminAction(adminActionRequest("artifact", "artifact-1", "cleanup_file_batch")).getReasonCodes()).contains("batch_cleanup_disabled");
assertThat(service.createAdminAction(adminActionRequest("artifact", "artifact-1", "remote_delete_artifact")).getReasonCodes()).contains("remote_delete_disabled");
assertThat(service.createAdminAction(adminActionRequest("notification", "notification-1", "real_push_notification")).getReasonCodes()).contains("real_push_disabled");
assertThat(service.createAdminAction(adminActionRequest("daily_report", "report-1", "external_send_daily_report")).getReasonCodes()).contains("external_send_disabled");
assertThat(service.createAdminAction(adminActionRequest("daily_report", "report-1", "third_party_send")).getReasonCodes()).contains("third_party_channel_disabled");
}
@Test
void createAdminActionReturnsSpecificPolicyAndSchedulerDisabledReasons() {
DigitalEmployeeAdminActionResultDto editPolicy = service.createAdminAction(

View File

@@ -130,7 +130,9 @@ function renderSummary(record: DigitalEmployeeAdminWorkbenchRow) {
record.recovery_policy_id && `Recovery ${record.recovery_policy_id}`,
record.auto_recovery_status && `AutoRecovery ${record.auto_recovery_status}`,
record.artifact_id && `Artifact ${record.artifact_id}`,
record.artifact_graph_id && `ArtifactGraph ${record.artifact_graph_id}`,
record.artifact_group_id && `ArtifactGroup ${record.artifact_group_id}`,
record.group_key && `Group ${record.group_key}`,
record.artifact_count != null && `Artifacts ${record.artifact_count}`,
record.version_count != null && `Versions ${record.version_count}`,
record.cleanup_request_id && `Cleanup ${record.cleanup_request_id}`,
@@ -142,7 +144,9 @@ function renderSummary(record: DigitalEmployeeAdminWorkbenchRow) {
record.merge_recommendation && `MergeAdvice ${record.merge_recommendation}`,
record.intervention_conflict_status && `Route ${record.intervention_conflict_status}`,
record.subscription_policy_id && `Subscription ${record.subscription_policy_id}`,
record.collaboration_thread_id && `Thread ${record.collaboration_thread_id}`,
record.daily_report_fact_id && `ReportFact ${record.daily_report_fact_id}`,
record.send_request_id && `SendRequest ${record.send_request_id}`,
record.memory_id && `Memory ${record.memory_id}`,
record.merge_candidate_count != null && `Merge ${record.merge_candidate_count}`,
record.governance_status && `Governance ${record.governance_status}`,
@@ -335,6 +339,8 @@ function allowedActions(record: DigitalEmployeeAdminWorkbenchRow, view: DigitalE
if (view === 'notifications' && record.notification_id) {
return [
{ action: 'mark_read', label: '标记已读' },
{ action: 'record_send_request', label: '记录发送请求' },
{ action: 'record_send_result', label: '记录发送结果' },
{ action: 'dismiss', label: '忽略' },
];
}
@@ -364,6 +370,8 @@ function allowedActions(record: DigitalEmployeeAdminWorkbenchRow, view: DigitalE
{ action: 'mark_delivered', label: '记录已送达' },
{ action: 'mark_confirmed', label: '确认回执' },
{ action: 'request_approval', label: '请求审批' },
{ action: 'record_send_request', label: '记录发送请求' },
{ action: 'record_send_result', label: '记录发送结果' },
{ action: 'record_approval_decision', label: '记录审批裁决' },
];
}
@@ -948,8 +956,12 @@ const DigitalEmployeeOps: React.FC = () => {
`LatestArtifact ${detailRecord.latest_artifact_id}`,
detailRecord.cleanup_request_id &&
`CleanupRequest ${detailRecord.cleanup_request_id}`,
detailRecord.artifact_graph_id &&
`ArtifactGraph ${detailRecord.artifact_graph_id}`,
detailRecord.notification_id &&
`Notification ${detailRecord.notification_id}`,
detailRecord.collaboration_thread_id &&
`Thread ${detailRecord.collaboration_thread_id}`,
detailRecord.subscription_policy_id &&
`Subscription ${detailRecord.subscription_policy_id}`,
detailRecord.notification_policy_id &&
@@ -959,6 +971,8 @@ const DigitalEmployeeOps: React.FC = () => {
`ReportFact ${detailRecord.daily_report_fact_id}`,
detailRecord.send_record_id &&
`SendRecord ${detailRecord.send_record_id}`,
detailRecord.send_request_id &&
`SendRequest ${detailRecord.send_request_id}`,
detailRecord.daily_report_approval_id &&
`ReportApproval ${detailRecord.daily_report_approval_id}`,
detailRecord.route_decision_id &&
@@ -982,6 +996,21 @@ const DigitalEmployeeOps: React.FC = () => {
.filter(Boolean)
.join(' / ') || '--'}
</Typography.Paragraph>
{(detailRecord.advanced_query_supported != null ||
detailRecord.aggregation_scope ||
detailRecord.materialized_source) && (
<Typography.Paragraph>
<Typography.Text strong></Typography.Text>
{detailRecord.advanced_query_supported != null &&
`高级查询 ${detailRecord.advanced_query_supported ? '支持' : '受限'}`}
{detailRecord.aggregation_scope && ` · 聚合范围 ${detailRecord.aggregation_scope}`}
{detailRecord.materialized_source && ` · 物化来源 ${detailRecord.materialized_source}`}
<Typography.Text type="secondary">
{' '}
·
</Typography.Text>
</Typography.Paragraph>
)}
{activeView === 'policy_conflicts' && (
<Typography.Paragraph>
<Typography.Text strong></Typography.Text>
@@ -1111,11 +1140,21 @@ const DigitalEmployeeOps: React.FC = () => {
` · 版本 ${detailRecord.version_count}`}
{detailRecord.latest_artifact_id &&
` · 最新产物 ${detailRecord.latest_artifact_id}`}
{detailRecord.artifact_graph_id && ` · 图谱 ${detailRecord.artifact_graph_id}`}
{detailRecord.group_key && ` · 分组 ${detailRecord.group_key}`}
{detailRecord.related_artifact_ids?.length
? ` · 关联产物 ${detailRecord.related_artifact_ids.join('/')}`
: ''}
{detailRecord.version_key && ` · 聚合键 ${detailRecord.version_key}`}
{detailRecord.retention_conflict_status &&
` · 保留冲突 ${detailRecord.retention_conflict_status}`}
{detailRecord.cleanup_conflict_status &&
` · 清理冲突 ${detailRecord.cleanup_conflict_status}`}
{detailRecord.batch_cleanup_disabled && ' · 批量真实清理禁用'}
{detailRecord.remote_delete_disabled && ' · 远端删除禁用'}
<Typography.Text type="secondary">
{' '}
· /
· //
</Typography.Text>
</Typography.Paragraph>
)}
@@ -1125,12 +1164,21 @@ const DigitalEmployeeOps: React.FC = () => {
{detailRecord.notification_id || detailRecord.entity_id || '--'}
{detailRecord.subscription_policy_id &&
` · 订阅策略 ${detailRecord.subscription_policy_id}`}
{detailRecord.collaboration_thread_id &&
` · 协作线程 ${detailRecord.collaboration_thread_id}`}
{detailRecord.subscription_conflict_status &&
` · 订阅冲突 ${detailRecord.subscription_conflict_status}`}
{detailRecord.channel && ` · ${detailRecord.channel}`}
{detailRecord.delivery_status && ` · ${detailRecord.delivery_status}`}
{detailRecord.delivery_attempt_id &&
` · 投递尝试 ${detailRecord.delivery_attempt_id}`}
{detailRecord.send_request_id &&
` · 发送请求 ${detailRecord.send_request_id}`}
{detailRecord.conflict_status && ` · ${detailRecord.conflict_status}`}
{detailRecord.real_push_disabled && ' · 真实推送禁用'}
<Typography.Text type="secondary">
{' '}
· /
· 线//
</Typography.Text>
</Typography.Paragraph>
)}
@@ -1161,10 +1209,17 @@ const DigitalEmployeeOps: React.FC = () => {
{detailRecord.report_id || detailRecord.entity_id || '--'}
{detailRecord.daily_report_fact_id &&
` · 事实 ${detailRecord.daily_report_fact_id}`}
{detailRecord.daily_report_physical_fact_status &&
` · 物理事实 ${detailRecord.daily_report_physical_fact_status}`}
{detailRecord.send_record_id &&
` · 发送记录 ${detailRecord.send_record_id}`}
{detailRecord.send_channel && ` · 渠道 ${detailRecord.send_channel}`}
{detailRecord.send_request_status &&
` · 发送请求状态 ${detailRecord.send_request_status}`}
{detailRecord.approval_decision_status &&
` · 审批状态 ${detailRecord.approval_decision_status}`}
{detailRecord.external_send_disabled && ' · 外部发送禁用'}
{detailRecord.third_party_channel_disabled && ' · 第三方发送禁用'}
{detailRecord.daily_report_approval_id &&
` · 审批裁决 ${detailRecord.daily_report_approval_id}`}
{detailRecord.daily_report_approval_action &&
@@ -1177,7 +1232,7 @@ const DigitalEmployeeOps: React.FC = () => {
` · 裁决记录 ${detailRecord.daily_report_approval_recorded_at}`}
<Typography.Text type="secondary">
{' '}
· ///
· ////
</Typography.Text>
</Typography.Paragraph>
)}

View File

@@ -621,7 +621,10 @@ export interface DigitalEmployeeAdminWorkbenchRow {
policy_guard_status?: string;
hard_interrupt_disabled?: boolean;
artifact_id?: string;
artifact_graph_id?: string;
artifact_group_id?: string;
group_key?: string;
related_artifact_ids?: string[];
artifact_count?: number;
device_count?: number;
version_count?: number;
@@ -630,19 +633,35 @@ export interface DigitalEmployeeAdminWorkbenchRow {
cleanup_request_id?: string;
cleanup_eligible_count?: number;
batch_cleanup_disabled?: boolean;
retention_conflict_status?: string;
cleanup_conflict_status?: string;
remote_delete_disabled?: boolean;
notification_id?: string;
collaboration_thread_id?: string;
subscription_policy_id?: string;
subscription_conflict_status?: string;
notification_policy_id?: string;
subscription_status?: string;
desktop_enabled?: boolean;
minimum_level?: string;
channel?: string;
delivery_status?: string;
delivery_attempt_id?: string;
send_request_id?: string;
real_push_disabled?: boolean;
conflict_status?: string;
report_id?: string;
daily_report_fact_id?: string;
daily_report_physical_fact_status?: string;
send_record_id?: string;
send_channel?: string;
send_request_status?: string;
external_send_disabled?: boolean;
third_party_channel_disabled?: boolean;
approval_decision_status?: string;
advanced_query_supported?: boolean;
aggregation_scope?: string;
materialized_source?: string;
daily_report_approval_id?: string;
daily_report_approval_action?: string;
daily_report_approval_actor?: string;

View File

@@ -2338,25 +2338,87 @@ function checkDigitalAdminCollaborationOperationsClosure() {
const docs = fs.readFileSync(docsPath, "utf8");
const requiredSnippets = [
[service, "record_cleanup_request", "backend cleanup request admin action"],
[service, "record_send_request", "backend send request admin action"],
[service, "record_send_result", "backend send result admin action"],
[service, "remote_delete_disabled", "backend remote delete disabled reason"],
[service, "real_push_disabled", "backend real push disabled reason"],
[service, "external_send_disabled", "backend external send disabled reason"],
[service, "third_party_channel_disabled", "backend third party send disabled reason"],
[service, "artifact_graph_id", "backend artifact graph projection"],
[service, "cleanup_request_id", "backend cleanup request projection"],
[service, "batch_cleanup_disabled", "backend batch cleanup disabled projection"],
[service, "retention_conflict_status", "backend retention conflict projection"],
[service, "cleanup_conflict_status", "backend cleanup conflict projection"],
[service, "collaboration_thread_id", "backend collaboration thread projection"],
[service, "subscription_policy_id", "backend subscription policy projection"],
[service, "delivery_attempt_id", "backend delivery attempt projection"],
[service, "send_request_id", "backend send request projection"],
[service, "daily_report_fact_id", "backend daily report fact projection"],
[service, "daily_report_physical_fact_status", "backend daily report physical fact projection"],
[service, "advanced_query_supported", "backend advanced query projection"],
[dto, "cleanup_request_id", "backend cleanup request row field"],
[dto, "artifact_graph_id", "backend artifact graph row field"],
[dto, "related_artifact_ids", "backend related artifact ids row field"],
[dto, "cleanup_eligible_count", "backend cleanup eligible count row field"],
[dto, "batch_cleanup_disabled", "backend batch cleanup disabled row field"],
[dto, "retention_conflict_status", "backend retention conflict row field"],
[dto, "cleanup_conflict_status", "backend cleanup conflict row field"],
[dto, "remote_delete_disabled", "backend remote delete row field"],
[dto, "collaboration_thread_id", "backend collaboration thread row field"],
[dto, "subscription_policy_id", "backend subscription policy row field"],
[dto, "subscription_conflict_status", "backend subscription conflict row field"],
[dto, "delivery_attempt_id", "backend delivery attempt row field"],
[dto, "send_request_id", "backend send request row field"],
[dto, "real_push_disabled", "backend real push disabled row field"],
[dto, "daily_report_fact_id", "backend daily report fact row field"],
[dto, "daily_report_physical_fact_status", "backend daily report physical fact row field"],
[dto, "external_send_disabled", "backend external send disabled row field"],
[dto, "third_party_channel_disabled", "backend third party disabled row field"],
[dto, "advanced_query_supported", "backend advanced query row field"],
[test, "getCleanupRequestId", "backend cleanup request test assertion"],
[test, "queryAdminWorkbenchArtifactLifecycleReturnsGraphAndConflictRows", "backend artifact graph test"],
[test, "queryAdminWorkbenchNotificationsReturnsCollaborationAndDeliveryRows", "backend notification collaboration test"],
[test, "queryAdminWorkbenchDailyReportsReturnsPhysicalFactAndSendShellRows", "backend daily report send shell test"],
[test, "queryBusinessViewsExposeAdvancedQueryAndMaterializedSource", "backend business query shell test"],
[test, "createAdminActionAllowsLowRiskAssetNotificationReportIntents", "backend low risk asset notification report actions test"],
[test, "createAdminActionRejectsHighRiskAssetNotificationReportExecution", "backend high risk asset notification report rejection test"],
[test, "getSubscriptionPolicyId", "backend subscription policy test assertion"],
[test, "getDailyReportFactId", "backend daily report fact test assertion"],
[qimingTypes, "cleanup_request_id", "qiming cleanup request type field"],
[qimingTypes, "artifact_graph_id", "qiming artifact graph type field"],
[qimingTypes, "related_artifact_ids", "qiming related artifacts type field"],
[qimingTypes, "retention_conflict_status", "qiming retention conflict type field"],
[qimingTypes, "cleanup_conflict_status", "qiming cleanup conflict type field"],
[qimingTypes, "remote_delete_disabled", "qiming remote delete guard type field"],
[qimingTypes, "collaboration_thread_id", "qiming collaboration thread type field"],
[qimingTypes, "subscription_policy_id", "qiming subscription policy type field"],
[qimingTypes, "subscription_conflict_status", "qiming subscription conflict type field"],
[qimingTypes, "delivery_attempt_id", "qiming delivery attempt type field"],
[qimingTypes, "send_request_id", "qiming send request type field"],
[qimingTypes, "real_push_disabled", "qiming real push guard type field"],
[qimingTypes, "daily_report_fact_id", "qiming daily report fact type field"],
[qimingTypes, "daily_report_physical_fact_status", "qiming daily physical fact type field"],
[qimingTypes, "external_send_disabled", "qiming external send guard type field"],
[qimingTypes, "third_party_channel_disabled", "qiming third party send guard type field"],
[qimingTypes, "advanced_query_supported", "qiming advanced query type field"],
[qimingOps, "记录批量清理申请", "qiming cleanup request action"],
[qimingOps, "跨设备/跨版本图谱摘要", "qiming artifact graph copy"],
[qimingOps, "保留冲突", "qiming retention conflict copy"],
[qimingOps, "清理冲突", "qiming cleanup conflict copy"],
[qimingOps, "远端删除禁用", "qiming remote delete guard copy"],
[qimingOps, "协作线程", "qiming collaboration thread copy"],
[qimingOps, "订阅策略", "qiming subscription policy copy"],
[qimingOps, "投递尝试", "qiming delivery attempt copy"],
[qimingOps, "发送请求", "qiming send request copy"],
[qimingOps, "真实推送禁用", "qiming real push guard copy"],
[qimingOps, "审批状态", "qiming daily approval status copy"],
[qimingOps, "物理事实状态", "qiming daily physical fact copy"],
[qimingOps, "外部发送禁用", "qiming external send guard copy"],
[qimingOps, "第三方发送", "qiming third party send guard copy"],
[qimingOps, "高级查询", "qiming advanced query copy"],
[qimingOps, "物化来源", "qiming materialized source copy"],
[docs, "正式管理端协作运营收口", "docs collaboration closure absorption"],
[docs, "资产通知日报业务查询总收口已开始吸收", "docs asset notification report closure absorption"],
];
const missing = requiredSnippets
.filter(([content, snippet]) => !content.includes(snippet))

View File

@@ -584,7 +584,7 @@ GET /api/digital-employee/approvals
7. **产物交付闭环**
- 已吸收:运行 payload 中的 artifacts / outputs / files / uri 可提取为正式 `digital_artifacts` 并同步File Server workspace / upload / download / export / file update 生命周期状态已开始写入 Artifact payload 的 `delivery` 对象embedded `/api/artifact` 和日报产物来源可读取交付阶段、状态、文件与 workspace 信息Artifact 预览/下载权限已开始闭环,嵌入页通过受控 bridge 访问本地产物,并写入 `artifact_access_allowed` / `artifact_access_rejected` 审计事件Artifact 版本/保留策略已开始闭环,业务产物视图可读取 version / retention 摘要,保留标记会写入 `artifact_retention_marked` / `artifact_retention_rejected` / `artifact_version_observed` 事件。真实文件清理执行已开始吸收,过期本地普通文件可通过独立 `cleanup` bridge 删除,清理结果会写入 `artifact_cleanup_executed` / `artifact_cleanup_rejected` / `artifact_cleanup_failed`artifact payload 与业务视图会投影 `cleanup_status``deleted_at` 和原因字段。保留策略批量治理和跨设备聚合视图已开始吸收embedded 业务视图新增 `artifact-groups` 产物聚合入口,可按版本/来源聚合产物、统计设备/版本/保留/清理状态,并通过受控批量接口写入保留、到期或复核标记;日报产物来源也提供批量标记到期入口。
- 缺口正式管理端产物生命周期预览已开始吸收qiming“数字员工运营台”的“产物治理”tab 可只读聚合产物 lifecycle / access / retention / cleanup / version 业务视图,并通过低风险 admin action 记录 `mark_keep` / `mark_expire` / `mark_reviewed` 意图,等待客户端拉取执行;正式管理端协作运营收口已开始吸收,产物治理详情会展示 `cleanup_request_id``cleanup_eligible_count``batch_cleanup_disabled`,只允许记录批量清理申请,不执行批量真实清理。正式管理端产物跨设备聚合预览已开始吸收,产物治理详情可展示 artifact group、产物数、设备数、版本数、最新产物和 version key用于解释跨设备/跨版本聚合但不触发批量真实删除。仍缺少批量真实文件清理、复杂跨设备图谱、正式外部生命周期工作台和更完整物化业务表。
- 缺口正式管理端产物生命周期预览已开始吸收qiming“数字员工运营台”的“产物治理”tab 可只读聚合产物 lifecycle / access / retention / cleanup / version 业务视图,并通过低风险 admin action 记录 `mark_keep` / `mark_expire` / `mark_reviewed` 意图,等待客户端拉取执行;正式管理端协作运营收口已开始吸收,产物治理详情会展示 `cleanup_request_id``cleanup_eligible_count``batch_cleanup_disabled`,只允许记录批量清理申请,不执行批量真实清理。正式管理端产物跨设备聚合预览已开始吸收,产物治理详情可展示 artifact group、产物数、设备数、版本数、最新产物和 version key用于解释跨设备/跨版本聚合但不触发批量真实删除。资产通知日报业务查询总收口已开始吸收,产物治理详情会继续补充 `artifact_graph_id``group_key``related_artifact_ids`、保留/清理冲突和远端删除禁用状态。仍缺少批量真实文件清理、复杂跨设备图谱、正式外部生命周期工作台和更完整物化业务表。
- 建议:下一轮围绕正式管理端生命周期工作台和跨设备图谱,把 embedded 预览聚合推进到跨端运营治理。
8. **审批 / 人工介入增强**
@@ -594,7 +594,7 @@ GET /api/digital-employee/approvals
9. **通知 / Inbox / 用户消息**
- 已吸收运行事件和同步失败能在数字员工页面展示embedded adapter 已接管 sgRobot 风格 `/api/notifications`、未读数、read/dismiss/reply本地 UI state 会保存通知已读、忽略和回复 overlay顶部 Bell 已开始展示 approval、同步失败、服务异常和任务完成通知本地通知订阅偏好已接入 `/api/notifications/preferences`,可按总开关、桌面通知开关、等级和常用类型过滤 Inbox`/api/digital-employee/notifications` 已提供管理端可消费的本地通知业务视图,通知动作和订阅偏好事件的 sync `business_view` 会归类为 notification 并提炼状态字段;`need_input` 通知首次回复会同步转化为 `provide_task_input` 治理命令,保留通知回复 overlay 的同时写入 Task/Run/Event/outbox客户端已支持管理端通知状态拉取与本地通知动作回写未读 action/warn/error 通知会按偏好触发一次系统级桌面提醒,系统权限引导 UI 已开始吸收Bell 设置面板可展示桌面通知可用性、测试发送结果、自动通知失败原因,并通过系统设置入口辅助修复,`digital_workday_notification_*` 同步事件只暴露通知 ID、状态、endpoint、错误和回复长度通知投影、本地订阅策略、桌面提醒、系统权限引导、跨端未读状态与任务输入链路已开始闭环。
- 缺口正式管理端通知运营预览已开始吸收embedded 新增“通知运营”入口,可筛选全部/未读/待处理/异常通知支持批量已读、批量忽略、拉取远端通知、打开系统通知设置和单条回复qiming“数字员工运营台”的“通知运营”tab 可只读聚合通知业务视图,并通过低风险 admin action 记录 `mark_read` / `dismiss` 意图,等待客户端拉取执行;正式管理端协作运营收口已开始吸收,通知运营详情会展示 `subscription_policy_id``channel``delivery_status``conflict_status`用于解释订阅策略和跨设备状态。正式管理端通知订阅策略预览已开始吸收qiming“数字员工运营台”的“通知策略”tab 可独立筛选订阅策略、桌面通知开关、最低通知等级和渠道策略,并只记录拉取通知策略、复核和忽略意图,不远程编辑策略、不触发真实推送。仍缺少正式外部通知协作工作台、复杂订阅策略编辑 UI、真实通知发送/推送通道和跨设备通知冲突合并。
- 缺口正式管理端通知运营预览已开始吸收embedded 新增“通知运营”入口,可筛选全部/未读/待处理/异常通知支持批量已读、批量忽略、拉取远端通知、打开系统通知设置和单条回复qiming“数字员工运营台”的“通知运营”tab 可只读聚合通知业务视图,并通过低风险 admin action 记录 `mark_read` / `dismiss` 意图,等待客户端拉取执行;正式管理端协作运营收口已开始吸收,通知运营详情会展示 `subscription_policy_id``channel``delivery_status``conflict_status`用于解释订阅策略和跨设备状态。正式管理端通知订阅策略预览已开始吸收qiming“数字员工运营台”的“通知策略”tab 可独立筛选订阅策略、桌面通知开关、最低通知等级和渠道策略,并只记录拉取通知策略、复核和忽略意图,不远程编辑策略、不触发真实推送。资产通知日报业务查询总收口已开始吸收,通知运营详情会补充协作线程、订阅冲突、投递尝试、发送请求和真实推送禁用状态。仍缺少正式外部通知协作工作台、复杂订阅策略编辑 UI、真实通知发送/推送通道和跨设备通知冲突合并。
- 建议:下一轮围绕正式管理端通知工作台,把 Inbox 从嵌入页投影推进到完整跨端协作入口。
10. **入口路由 / 任务分流(路由业务视图与通知联动已开始)**
@@ -609,12 +609,12 @@ 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 深链和脱敏客户端 KeyPlan 视图会聚合 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 深链。跨设备审批策略冲突预览已开始吸收:策略冲突页会把 `approval_policy_id``conflict_policy_id``decision_guard_status``batch_accept_remote_disabled``auto_decision_disabled` 和策略原因纳入冲突字段和详情解释,但不自动合并策略、不远程编辑策略。跨设备调度派发策略冲突预览已开始吸收:策略冲突页会把 `dispatch_policy_key``max_per_sweep``auto_dispatch_ready_steps``catch_up_on_startup``max_run_history` 纳入冲突字段和详情解释,但不立即运行、不接管租约、不远程改策略。正式管理端策略中心预览和正式管理端调度运营预览已开始吸收:`admin/workbench` 新增 `policy_center``scheduler_operations` 视图统一消费策略快照、scheduler job、cron settings、lease、dispatch sweep 和 recent run 摘要,并通过低风险 admin action 记录策略拉取、立即检查调度、复核和忽略意图。正式管理端治理策略预览和计划版本物化预览已开始吸收:`admin/workbench` 新增 `governance_policies``plan_revision_materialization`,从 sync record / business fact 投影治理策略保护、ACP 硬中断禁用、版本事实来源和变更字段。策略治理审批路由总收口已开始吸收,`policy_center``governance_policies``approval_policies``route_governance` 会统一暴露策略编辑、合并、裁决和恢复执行安全壳字段,并把真实远程强编辑、自动合并、批量采纳远端、自动裁决和高风险恢复执行保持禁用。业务事实物化查询已开始吸收:后端新增 `digital_employee_business_fact``digital_employee_artifact_lifecycle``digital_employee_daily_report_fact` 三张增量物化表,同步入库时 upsert 安全业务摘要,业务查询优先返回 `materialized: true` 行,旧记录继续回退 sync record。
- 已吸收:管理端已有通用 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 深链和脱敏客户端 KeyPlan 视图会聚合 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 深链。跨设备审批策略冲突预览已开始吸收:策略冲突页会把 `approval_policy_id``conflict_policy_id``decision_guard_status``batch_accept_remote_disabled``auto_decision_disabled` 和策略原因纳入冲突字段和详情解释,但不自动合并策略、不远程编辑策略。跨设备调度派发策略冲突预览已开始吸收:策略冲突页会把 `dispatch_policy_key``max_per_sweep``auto_dispatch_ready_steps``catch_up_on_startup``max_run_history` 纳入冲突字段和详情解释,但不立即运行、不接管租约、不远程改策略。正式管理端策略中心预览和正式管理端调度运营预览已开始吸收:`admin/workbench` 新增 `policy_center``scheduler_operations` 视图统一消费策略快照、scheduler job、cron settings、lease、dispatch sweep 和 recent run 摘要,并通过低风险 admin action 记录策略拉取、立即检查调度、复核和忽略意图。正式管理端治理策略预览和计划版本物化预览已开始吸收:`admin/workbench` 新增 `governance_policies``plan_revision_materialization`,从 sync record / business fact 投影治理策略保护、ACP 硬中断禁用、版本事实来源和变更字段。策略治理审批路由总收口已开始吸收,`policy_center``governance_policies``approval_policies``route_governance` 会统一暴露策略编辑、合并、裁决和恢复执行安全壳字段,并把真实远程强编辑、自动合并、批量采纳远端、自动裁决和高风险恢复执行保持禁用。资产通知日报业务查询总收口已开始吸收,业务查询 row 增加 `advanced_query_supported``aggregation_scope``materialized_source`,用于解释高级查询能力和物化来源。业务事实物化查询已开始吸收:后端新增 `digital_employee_business_fact``digital_employee_artifact_lifecycle``digital_employee_daily_report_fact` 三张增量物化表,同步入库时 upsert 安全业务摘要,业务查询优先返回 `materialized: true` 行,旧记录继续回退 sync record。
- 缺口:复杂组合检索、跨设备聚合图、远程裁决/策略编辑动作和物化业务表仍待吸收。
- 建议:先使用 sync record 派生业务查询模型和 embedded 业务视图预览承接联调,再按容量和查询压力决定是否拆 Plan / Task / Run / Event 物理业务表。
13. **日报实体与导出交付**
- 已吸收embedded 页面可基于当前 workday projection 导出本地文本日报;生成日报时会通过 qimingclaw bridge 写入 `daily_report` text / HTML / PDF Artifact 与 `daily_report_generated` Event并可通过 `/api/digital-employee/daily-reports` 和详情接口读取只读日报投影。日报 HTML/PDF 导出、发送记录、管理端确认回执和日报发送审批已开始吸收,交付动作会写入 `daily_report_send_recorded` / `daily_report_confirmed` / `daily_report_approval_requested` 事件sync `business_view.category = daily_report` 可被管理端筛选消费。管理端正式日报页面预览和正式管理端日报运营预览已开始吸收embedded 新增“日报运营”入口展示日报生成、交付、确认、审批和关联产物状态并复用现有生成、记录发送、确认回执和请求审批动作qiming-backend 的 `daily_reports` 运营台视图会聚合日报生成、发送记录、确认回执和审批请求qiming“数字员工运营台”的“日报运营”tab 可记录已送达、确认回执、请求审批和发送结果。发送账单审计外壳已开始吸收qiming-backend 新增 `/api/digital-employee/admin/daily-reports/send-records`qiming 运营台可在日报详情中“记录发送结果”,但不接真实第三方发送通道。正式管理端协作运营收口已开始吸收,日报运营详情会展示 `daily_report_fact_id``send_record_id``approval_decision_status`,用于解释事实来源、发送记录和审批状态。管理端日报审批裁决回写预览已开始吸收,日报运营详情可展示 `daily_report_approval_id`、裁决动作、裁决人、原因和记录时间,并只记录审批裁决意图,不触发真实发送或自动审批。
- 已吸收embedded 页面可基于当前 workday projection 导出本地文本日报;生成日报时会通过 qimingclaw bridge 写入 `daily_report` text / HTML / PDF Artifact 与 `daily_report_generated` Event并可通过 `/api/digital-employee/daily-reports` 和详情接口读取只读日报投影。日报 HTML/PDF 导出、发送记录、管理端确认回执和日报发送审批已开始吸收,交付动作会写入 `daily_report_send_recorded` / `daily_report_confirmed` / `daily_report_approval_requested` 事件sync `business_view.category = daily_report` 可被管理端筛选消费。管理端正式日报页面预览和正式管理端日报运营预览已开始吸收embedded 新增“日报运营”入口展示日报生成、交付、确认、审批和关联产物状态并复用现有生成、记录发送、确认回执和请求审批动作qiming-backend 的 `daily_reports` 运营台视图会聚合日报生成、发送记录、确认回执和审批请求qiming“数字员工运营台”的“日报运营”tab 可记录已送达、确认回执、请求审批和发送结果。发送账单审计外壳已开始吸收qiming-backend 新增 `/api/digital-employee/admin/daily-reports/send-records`qiming 运营台可在日报详情中“记录发送结果”,但不接真实第三方发送通道。正式管理端协作运营收口已开始吸收,日报运营详情会展示 `daily_report_fact_id``send_record_id``approval_decision_status`,用于解释事实来源、发送记录和审批状态。管理端日报审批裁决回写预览已开始吸收,日报运营详情可展示 `daily_report_approval_id`、裁决动作、裁决人、原因和记录时间,并只记录审批裁决意图,不触发真实发送或自动审批。资产通知日报业务查询总收口已开始吸收,日报运营详情会补充 `daily_report_physical_fact_status`、发送渠道、发送请求状态、外部发送禁用和第三方发送禁用说明。
- 缺口:没有正式 DailyReport 物理表、真实管理端发送通道和正式外部管理端日报页面。
- 建议:先稳定 Artifact/Event 派生日报交付模型,再根据管理端联调需要决定是否物化 DailyReport 表与发送记录表。