diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/digital/DigitalEmployeeAdminWorkbenchRowDto.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/digital/DigitalEmployeeAdminWorkbenchRowDto.java index b4be41a8..022af334 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/digital/DigitalEmployeeAdminWorkbenchRowDto.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/digital/DigitalEmployeeAdminWorkbenchRowDto.java @@ -80,6 +80,22 @@ public class DigitalEmployeeAdminWorkbenchRowDto { private String routeDecisionId; @JsonProperty("service_id") private String serviceId; + @JsonProperty("service_lease_id") + private String serviceLeaseId; + @JsonProperty("lease_holder") + private String leaseHolder; + @JsonProperty("lease_expires_at") + private String leaseExpiresAt; + @JsonProperty("restart_record_id") + private String restartRecordId; + @JsonProperty("restart_count") + private Long restartCount; + @JsonProperty("last_restart_at") + private String lastRestartAt; + @JsonProperty("recovery_policy_id") + private String recoveryPolicyId; + @JsonProperty("auto_recovery_status") + private String autoRecoveryStatus; @JsonProperty("memory_id") private String memoryId; @JsonProperty("memory_key") 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 26937f9e..8bb00195 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 @@ -1082,6 +1082,14 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye .approvalDecisionStatus(firstString(businessView.get("approval_decision_status"), businessView.get("approvalDecisionStatus"), payload.get("approval_decision_status"), payload.get("approvalDecisionStatus"))) .routeDecisionId(firstString(businessView.get("route_decision_id"), payload.get("route_decision_id"))) .serviceId(firstString(businessView.get("service_id"), businessView.get("serviceId"), payload.get("service_id"), payload.get("serviceId"))) + .serviceLeaseId(firstString(businessView.get("service_lease_id"), businessView.get("serviceLeaseId"), payload.get("service_lease_id"), payload.get("serviceLeaseId"))) + .leaseHolder(firstString(businessView.get("lease_holder"), businessView.get("leaseHolder"), payload.get("lease_holder"), payload.get("leaseHolder"))) + .leaseExpiresAt(firstString(businessView.get("lease_expires_at"), businessView.get("leaseExpiresAt"), payload.get("lease_expires_at"), payload.get("leaseExpiresAt"))) + .restartRecordId(firstString(businessView.get("restart_record_id"), businessView.get("restartRecordId"), payload.get("restart_record_id"), payload.get("restartRecordId"))) + .restartCount(numberValue(firstPresent(businessView.get("restart_count"), businessView.get("restartCount"), payload.get("restart_count"), payload.get("restartCount")))) + .lastRestartAt(firstString(businessView.get("last_restart_at"), businessView.get("lastRestartAt"), payload.get("last_restart_at"), payload.get("lastRestartAt"))) + .recoveryPolicyId(firstString(businessView.get("recovery_policy_id"), businessView.get("recoveryPolicyId"), payload.get("recovery_policy_id"), payload.get("recoveryPolicyId"))) + .autoRecoveryStatus(firstString(businessView.get("auto_recovery_status"), businessView.get("autoRecoveryStatus"), payload.get("auto_recovery_status"), payload.get("autoRecoveryStatus"))) .memoryId(firstString(businessView.get("memory_id"), businessView.get("memoryId"), payload.get("memory_id"), payload.get("memoryId"), "memory".equals(record.getEntityType()) ? record.getEntityId() : null)) .memoryKey(firstString(businessView.get("memory_key"), businessView.get("memoryKey"), businessView.get("key"), payload.get("memory_key"), payload.get("memoryKey"), payload.get("key"))) .skillId(firstString(businessView.get("skill_id"), businessView.get("skillId"), payload.get("skill_id"), payload.get("skillId"))) @@ -1166,11 +1174,17 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye || StringUtils.equals(category, "service_operation") || StringUtils.equals(category, "service_health") || contains(kind, "managed_service") + || contains(kind, "managed_service_snapshot") + || contains(kind, "service_lease") || contains(kind, "service_snapshot") || contains(kind, "service_restart") + || contains(kind, "service_recovery") || contains(kind, "doctor_service") || contains(kind, "doctor_issue") - || StringUtils.isNotBlank(row.getServiceId()); + || StringUtils.isNotBlank(row.getServiceId()) + || StringUtils.isNotBlank(row.getServiceLeaseId()) + || StringUtils.isNotBlank(row.getRestartRecordId()) + || StringUtils.isNotBlank(row.getRecoveryPolicyId()); } private boolean isNotificationRow(DigitalEmployeeAdminWorkbenchRowDto row) { @@ -1320,6 +1334,10 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye } String category = firstString(businessView.get("category"), payload.get("category")); if (StringUtils.isNotBlank(category)) return category; + if (contains(kind, "managed_service") || contains(kind, "service_lease") || contains(kind, "service_restart") + || contains(kind, "service_recovery") || contains(kind, "doctor_service") || contains(kind, "doctor_issue")) { + return contains(kind, "doctor") ? "service_health" : "service_operation"; + } if (contains(kind, "risk_approval")) return "risk_approval"; if (contains(kind, "notification") || StringUtils.isNotBlank(firstString(businessView.get("notification_id"), payload.get("notification_id")))) { String level = firstString(businessView.get("level"), payload.get("level")); 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 b6e1a322..6102af74 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 @@ -818,6 +818,90 @@ class DigitalEmployeeSyncApplicationServiceImplTest { assertThat(result.getRecords().get(2).getServiceId()).isEqualTo("agent-server"); } + @Test + void queryAdminWorkbenchServiceOperationsReturnsLeaseRestartAndRecoveryRowsOnly() { + when(repository.queryBusinessRecords(eq(101L), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), eq(1L), eq(20L))) + .thenReturn(page(List.of( + record("outbox-service-lease", "event", "event-service-lease-1", 101L, new Date(10_000), + Map.of( + "entity_type", "event", + "kind", "service_lease_acquired", + "status", "active", + "title", "File Server lease acquired", + "summary", "File Server lease held by device-001", + "service_id", "file-server", + "service_lease_id", "lease-1", + "lease_holder", "device-001", + "lease_expires_at", "2026-06-13T12:30:00Z" + ), + Map.of("id", "event-service-lease-1", "kind", "service_lease_acquired")), + record("outbox-service-restart-record", "event", "event-service-restart-1", 101L, new Date(11_000), + Map.of( + "entity_type", "event", + "kind", "service_restart_recorded", + "status", "recorded", + "title", "File Server restart recorded", + "service_id", "file-server", + "restart_record_id", "restart-1", + "restart_count", 2, + "last_restart_at", "2026-06-13T12:00:00Z" + ), + Map.of("id", "event-service-restart-1", "kind", "service_restart_recorded")), + record("outbox-service-recovery-policy", "event", "event-service-recovery-1", 101L, new Date(12_000), + Map.of( + "entity_type", "event", + "kind", "service_recovery_policy_snapshot", + "status", "observing", + "title", "File Server recovery policy", + "service_id", "file-server", + "recovery_policy_id", "recovery-policy-1", + "auto_recovery_status", "observing" + ), + Map.of("id", "event-service-recovery-1", "kind", "service_recovery_policy_snapshot")), + record("outbox-doctor-service", "event", "event-service-doctor-1", 101L, new Date(13_000), + Map.of( + "entity_type", "event", + "kind", "doctor_service_issue", + "status", "failed", + "title", "服务诊断失败", + "service_id", "agent-server", + "reason", "health_check_failed" + ), + Map.of("id", "event-service-doctor-1", "kind", "doctor_service_issue")), + record("outbox-approval", "approval", "approval-1", 101L, new Date(14_000), + Map.of("entity_type", "approval", "title", "审批", "status", "pending"), + Map.of("id", "approval-1", "status", "pending")), + record("outbox-notification", "notification", "notification-1", 101L, new Date(15_000), + Map.of("entity_type", "notification", "title", "通知", "status", "unread"), + Map.of("id", "notification-1", "status", "unread")), + record("outbox-artifact", "artifact", "artifact-1", 101L, new Date(16_000), + Map.of("entity_type", "artifact", "title", "产物", "status", "active"), + Map.of("id", "artifact-1", "status", "active")), + record("outbox-route", "event", "event-route-1", 101L, new Date(17_000), + Map.of("entity_type", "event", "kind", "route_decision_blocked", "title", "路由阻断", "status", "blocked"), + Map.of("id", "event-route-1", "kind", "route_decision_blocked")) + ))); + + DigitalEmployeeAdminWorkbenchPageDto result = service.queryAdminWorkbench( + "service_operations", null, null, null, null, null, null, null, null, null, null, null, 1L, 20L + ); + + assertThat(result.getTotal()).isEqualTo(4); + assertThat(result.getRecords()).extracting(DigitalEmployeeAdminWorkbenchRowDto::getEntityId) + .containsExactly("event-service-lease-1", "event-service-restart-1", "event-service-recovery-1", "event-service-doctor-1"); + assertThat(result.getRecords().get(0).getView()).isEqualTo("service_operations"); + assertThat(result.getRecords().get(0).getServiceId()).isEqualTo("file-server"); + assertThat(result.getRecords().get(0).getServiceLeaseId()).isEqualTo("lease-1"); + assertThat(result.getRecords().get(0).getLeaseHolder()).isEqualTo("device-001"); + assertThat(result.getRecords().get(0).getLeaseExpiresAt()).isEqualTo("2026-06-13T12:30:00Z"); + assertThat(result.getRecords().get(1).getRestartRecordId()).isEqualTo("restart-1"); + assertThat(result.getRecords().get(1).getRestartCount()).isEqualTo(2L); + assertThat(result.getRecords().get(1).getLastRestartAt()).isEqualTo("2026-06-13T12:00:00Z"); + assertThat(result.getRecords().get(2).getRecoveryPolicyId()).isEqualTo("recovery-policy-1"); + assertThat(result.getRecords().get(2).getAutoRecoveryStatus()).isEqualTo("observing"); + assertThat(result.getRecords().get(3).getSummary()).isEqualTo("health_check_failed"); + } + @Test void queryAdminWorkbenchRouteGovernanceReturnsRouteAndGovernanceRowsOnly() { when(repository.queryBusinessRecords(eq(101L), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), eq(1L), eq(20L))) diff --git a/qiming/src/pages/SystemManagement/Content/DigitalEmployeeOps/index.tsx b/qiming/src/pages/SystemManagement/Content/DigitalEmployeeOps/index.tsx index 9558bb92..d2db5e4e 100644 --- a/qiming/src/pages/SystemManagement/Content/DigitalEmployeeOps/index.tsx +++ b/qiming/src/pages/SystemManagement/Content/DigitalEmployeeOps/index.tsx @@ -115,6 +115,10 @@ function renderSummary(record: DigitalEmployeeAdminWorkbenchRow) { record.run_id && `Run ${record.run_id}`, record.approval_id && `Approval ${record.approval_id}`, record.service_id && `Service ${record.service_id}`, + record.service_lease_id && `Lease ${record.service_lease_id}`, + record.restart_record_id && `Restart ${record.restart_record_id}`, + 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.cleanup_request_id && `Cleanup ${record.cleanup_request_id}`, record.route_decision_id && `Route ${record.route_decision_id}`, @@ -807,6 +811,12 @@ const DigitalEmployeeOps: React.FC = () => { detailRecord.step_id && `Step ${detailRecord.step_id}`, detailRecord.approval_id && `Approval ${detailRecord.approval_id}`, detailRecord.service_id && `Service ${detailRecord.service_id}`, + detailRecord.service_lease_id && + `ServiceLease ${detailRecord.service_lease_id}`, + detailRecord.restart_record_id && + `RestartRecord ${detailRecord.restart_record_id}`, + detailRecord.recovery_policy_id && + `RecoveryPolicy ${detailRecord.recovery_policy_id}`, detailRecord.artifact_id && `Artifact ${detailRecord.artifact_id}`, detailRecord.cleanup_request_id && `CleanupRequest ${detailRecord.cleanup_request_id}`, @@ -884,9 +894,15 @@ const DigitalEmployeeOps: React.FC = () => { {detailRecord.service_id || detailRecord.entity_id || '--'} {detailRecord.category && ` · ${detailRecord.category}`} {detailRecord.level && ` · ${detailRecord.level}`} + {detailRecord.lease_holder && ` · lease holder ${detailRecord.lease_holder}`} + {detailRecord.lease_expires_at && ` · lease 到期 ${detailRecord.lease_expires_at}`} + {detailRecord.restart_count != null && ` · 重启 ${detailRecord.restart_count} 次`} + {detailRecord.last_restart_at && ` · 最近重启 ${detailRecord.last_restart_at}`} + {detailRecord.recovery_policy_id && ` · 恢复策略 ${detailRecord.recovery_policy_id}`} + {detailRecord.auto_recovery_status && ` · 自动恢复 ${detailRecord.auto_recovery_status}`} {' '} - · 只记录复核/忽略意图,不开放 start/stop。 + · 只记录复核/忽略意图,不开放 start/stop、不接管 lease、不自动恢复服务。 )} diff --git a/qiming/src/types/interfaces/systemManage.ts b/qiming/src/types/interfaces/systemManage.ts index a4a43a7b..8d44e281 100644 --- a/qiming/src/types/interfaces/systemManage.ts +++ b/qiming/src/types/interfaces/systemManage.ts @@ -617,6 +617,14 @@ export interface DigitalEmployeeAdminWorkbenchRow { approval_decision_status?: string; route_decision_id?: string; service_id?: string; + service_lease_id?: string; + lease_holder?: string; + lease_expires_at?: string; + restart_record_id?: string; + restart_count?: number; + last_restart_at?: string; + recovery_policy_id?: string; + auto_recovery_status?: string; memory_id?: string; memory_key?: string; skill_id?: 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 3e03348f..aa8cbf1e 100644 --- a/qimingclaw/crates/agent-electron-client/scripts/check-digital-employee.js +++ b/qimingclaw/crates/agent-electron-client/scripts/check-digital-employee.js @@ -2053,15 +2053,29 @@ function checkDigitalAdminServiceOperationsWorkbench() { [service, "service_operations", "backend service operations view"], [service, "isServiceOperationsRow", "backend service operations matcher"], [service, "managed_service", "backend managed service classifier"], + [service, "managed_service_snapshot", "backend managed service snapshot classifier"], + [service, "service_lease", "backend service lease classifier"], [service, "service_restart", "backend service restart classifier"], + [service, "service_recovery", "backend service recovery classifier"], [dto, "service_id", "backend service id row field"], + [dto, "service_lease_id", "backend service lease row field"], + [dto, "restart_record_id", "backend service restart record row field"], + [dto, "recovery_policy_id", "backend service recovery policy row field"], + [dto, "auto_recovery_status", "backend auto recovery status row field"], [test, "queryAdminWorkbenchServiceOperationsReturnsServiceHealthAndRestartRowsOnly", "backend service operations test"], + [test, "queryAdminWorkbenchServiceOperationsReturnsLeaseRestartAndRecoveryRowsOnly", "backend service recovery operations test"], [qimingTypes, "service_operations", "qiming service operations view type"], [qimingTypes, "service_id", "qiming service id field"], + [qimingTypes, "service_lease_id", "qiming service lease id field"], + [qimingTypes, "restart_record_id", "qiming restart record id field"], + [qimingTypes, "recovery_policy_id", "qiming recovery policy id field"], + [qimingTypes, "auto_recovery_status", "qiming auto recovery status field"], [qimingOps, "服务运营", "qiming service operations tab"], [qimingOps, "只记录复核/忽略意图", "qiming service operations safety copy"], [qimingOps, "不开放 start/stop", "qiming service start stop guard copy"], - [docs, "正式外部管理端服务运营台预览", "docs service operations absorption"], + [qimingOps, "不接管 lease", "qiming service lease takeover guard copy"], + [qimingOps, "不自动恢复服务", "qiming service auto recovery guard copy"], + [docs, "正式管理端服务恢复运营预览", "docs service recovery absorption"], ]; const missing = requiredSnippets .filter(([content, snippet]) => !content.includes(snippet)) diff --git a/qimingclaw/docs/digital-employee-frontend-integration-plan.md b/qimingclaw/docs/digital-employee-frontend-integration-plan.md index b0d4e3b4..0c10bd3f 100644 --- a/qimingclaw/docs/digital-employee-frontend-integration-plan.md +++ b/qimingclaw/docs/digital-employee-frontend-integration-plan.md @@ -573,8 +573,8 @@ GET /api/digital-employee/approvals - 建议:下一轮围绕正式管理端治理策略和 ACP 硬中断安全设计,把本地治理审计推进到完整跨端裁决与执行控制。 5. **ManagedService 控制面** - - 已吸收:Agent、MCP、Computer Server、Lanproxy、File Server、GUI Server 状态已进入 snapshot / projection;File Server 与 GUI Server 已开放白名单式 restart bridge,并会写入 `managed_service_restart` / `managed_service_restart_rejected` 事件,安全服务 restart bridge 已开始闭环。完整人机介入入口已开始吸收,embedded 新增“处置台”聚合服务异常、诊断风险、审计风险和策略拉取错误,支持批量标记已看、忽略、策略重拉和白名单服务安全重启。正式外部管理端服务运营台预览已开始吸收,qiming-backend 新增 `service_operations` 运营台视图聚合 managed service 快照、服务诊断和重启审计,qiming“数字员工运营台”新增“服务运营” tab,并只开放复核/忽略等低风险动作意图,不开放 start/stop。 - - 缺口:仍缺少 start / stop、lease 持久化、重启历史、自动恢复策略和完整外部管理端服务运营台;Agent、MCP、Computer Server、Lanproxy 等高风险服务不暴露数字员工重启动作。 + - 已吸收:Agent、MCP、Computer Server、Lanproxy、File Server、GUI Server 状态已进入 snapshot / projection;File Server 与 GUI Server 已开放白名单式 restart bridge,并会写入 `managed_service_restart` / `managed_service_restart_rejected` 事件,安全服务 restart bridge 已开始闭环。完整人机介入入口已开始吸收,embedded 新增“处置台”聚合服务异常、诊断风险、审计风险和策略拉取错误,支持批量标记已看、忽略、策略重拉和白名单服务安全重启。正式外部管理端服务运营台预览已开始吸收,qiming-backend 新增 `service_operations` 运营台视图聚合 managed service 快照、服务诊断和重启审计,qiming“数字员工运营台”新增“服务运营” tab,并只开放复核/忽略等低风险动作意图,不开放 start/stop。正式管理端服务恢复运营预览已开始吸收,`service_operations` 继续聚合 service lease、restart record、recovery policy 和 doctor service issue,qiming 详情可展示 lease holder/到期时间、重启次数/最近重启、恢复策略和自动恢复状态,并明确不接管 lease、不自动恢复服务。 + - 缺口:仍缺少真实 start / stop、lease 接管执行、自动恢复执行和完整外部管理端服务运营台;Agent、MCP、Computer Server、Lanproxy 等高风险服务不暴露数字员工重启动作。 - 建议:先沉淀安全重启审计和人工确认体验,再评估更细粒度的服务恢复策略。 6. **技能生命周期与策略**