吸收数字员工管理端服务恢复安全壳预览

This commit is contained in:
baiyanyun
2026-06-15 11:05:02 +08:00
parent 053a321ec1
commit ff7576f33d
7 changed files with 106 additions and 3 deletions

View File

@@ -139,6 +139,16 @@ public class DigitalEmployeeAdminWorkbenchRowDto {
private String recoveryPolicyId;
@JsonProperty("auto_recovery_status")
private String autoRecoveryStatus;
@JsonProperty("service_start_disabled")
private Boolean serviceStartDisabled;
@JsonProperty("service_stop_disabled")
private Boolean serviceStopDisabled;
@JsonProperty("lease_takeover_disabled")
private Boolean leaseTakeoverDisabled;
@JsonProperty("auto_recovery_disabled")
private Boolean autoRecoveryDisabled;
@JsonProperty("recovery_action_request_id")
private String recoveryActionRequestId;
@JsonProperty("memory_id")
private String memoryId;
@JsonProperty("memory_key")

View File

@@ -1029,6 +1029,10 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
case "run_shell_repair" -> List.of("run_shell_repair_disabled");
case "modify_system_config" -> List.of("modify_system_config_disabled");
case "restart_unlisted_service" -> List.of("restart_unlisted_service_disabled");
case "start_service" -> List.of("start_service_disabled");
case "stop_service" -> List.of("stop_service_disabled");
case "lease_takeover" -> List.of("lease_takeover_disabled");
case "execute_auto_recovery" -> List.of("auto_recovery_execution_disabled");
case "delete_file_repair" -> List.of("delete_file_repair_disabled");
case "remote_policy_edit" -> List.of("remote_policy_edit_disabled");
case "activate_plan", "cancel_run", "cancel_task", "accept_remote_approval_batch" -> List.of("high_risk_governance_action_disabled");
@@ -1139,6 +1143,11 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
.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")))
.serviceStartDisabled(nullableBooleanValue(firstPresent(businessView.get("service_start_disabled"), businessView.get("serviceStartDisabled"), payload.get("service_start_disabled"), payload.get("serviceStartDisabled"))))
.serviceStopDisabled(nullableBooleanValue(firstPresent(businessView.get("service_stop_disabled"), businessView.get("serviceStopDisabled"), payload.get("service_stop_disabled"), payload.get("serviceStopDisabled"))))
.leaseTakeoverDisabled(nullableBooleanValue(firstPresent(businessView.get("lease_takeover_disabled"), businessView.get("leaseTakeoverDisabled"), payload.get("lease_takeover_disabled"), payload.get("leaseTakeoverDisabled"))))
.autoRecoveryDisabled(nullableBooleanValue(firstPresent(businessView.get("auto_recovery_disabled"), businessView.get("autoRecoveryDisabled"), payload.get("auto_recovery_disabled"), payload.get("autoRecoveryDisabled"))))
.recoveryActionRequestId(firstString(businessView.get("recovery_action_request_id"), businessView.get("recoveryActionRequestId"), payload.get("recovery_action_request_id"), payload.get("recoveryActionRequestId")))
.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")))
.governanceStatus(firstString(businessView.get("governance_status"), businessView.get("governanceStatus"), payload.get("governance_status"), payload.get("governanceStatus")))
@@ -1295,7 +1304,12 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
|| StringUtils.isNotBlank(row.getServiceId())
|| StringUtils.isNotBlank(row.getServiceLeaseId())
|| StringUtils.isNotBlank(row.getRestartRecordId())
|| StringUtils.isNotBlank(row.getRecoveryPolicyId());
|| StringUtils.isNotBlank(row.getRecoveryPolicyId())
|| row.getServiceStartDisabled() != null
|| row.getServiceStopDisabled() != null
|| row.getLeaseTakeoverDisabled() != null
|| row.getAutoRecoveryDisabled() != null
|| StringUtils.isNotBlank(row.getRecoveryActionRequestId());
}
private boolean isNotificationRow(DigitalEmployeeAdminWorkbenchRowDto row) {

View File

@@ -1157,6 +1157,40 @@ class DigitalEmployeeSyncApplicationServiceImplTest {
assertThat(result.getRecords().get(3).getSummary()).isEqualTo("health_check_failed");
}
@Test
void queryAdminWorkbenchServiceOperationsReturnsSafetyShellFields() {
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-recovery-shell", "event", "event-service-shell-1", 101L, new Date(10_000),
Map.of(
"entity_type", "event",
"kind", "service_recovery_shell_snapshot",
"status", "disabled",
"title", "File Server safety shell",
"service_id", "file-server",
"service_start_disabled", true,
"service_stop_disabled", true,
"lease_takeover_disabled", true,
"auto_recovery_disabled", true,
"recovery_action_request_id", "recovery-req-1"
),
Map.of("id", "event-service-shell-1", "kind", "service_recovery_shell_snapshot"))
)));
DigitalEmployeeAdminWorkbenchPageDto result = service.queryAdminWorkbench(
"service_operations", null, null, null, null, null, null, null, null, null, null, null, 1L, 20L
);
assertThat(result.getTotal()).isEqualTo(1);
DigitalEmployeeAdminWorkbenchRowDto row = result.getRecords().get(0);
assertThat(row.getServiceStartDisabled()).isTrue();
assertThat(row.getServiceStopDisabled()).isTrue();
assertThat(row.getLeaseTakeoverDisabled()).isTrue();
assertThat(row.getAutoRecoveryDisabled()).isTrue();
assertThat(row.getRecoveryActionRequestId()).isEqualTo("recovery-req-1");
assertThat(row.getTitle()).isEqualTo("File Server safety shell");
}
@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)))
@@ -1993,6 +2027,28 @@ class DigitalEmployeeSyncApplicationServiceImplTest {
assertThat(remotePolicyEdit.getReasonCodes()).contains("remote_policy_edit_disabled");
}
@Test
void createAdminActionRejectsHighRiskServiceRecoveryExecution() {
DigitalEmployeeAdminActionResultDto startService = service.createAdminAction(
adminActionRequest("service", "file-server", "start_service")
);
DigitalEmployeeAdminActionResultDto stopService = service.createAdminAction(
adminActionRequest("service", "file-server", "stop_service")
);
DigitalEmployeeAdminActionResultDto leaseTakeover = service.createAdminAction(
adminActionRequest("service", "file-server", "lease_takeover")
);
DigitalEmployeeAdminActionResultDto autoRecovery = service.createAdminAction(
adminActionRequest("service", "file-server", "execute_auto_recovery")
);
assertThat(startService.getOk()).isFalse();
assertThat(startService.getReasonCodes()).contains("start_service_disabled");
assertThat(stopService.getReasonCodes()).contains("stop_service_disabled");
assertThat(leaseTakeover.getReasonCodes()).contains("lease_takeover_disabled");
assertThat(autoRecovery.getReasonCodes()).contains("auto_recovery_execution_disabled");
}
@Test
void queryPendingAdminActionsFiltersTenantClientDeviceAndMasksKey() {
when(adminActionRepository.queryActions(eq(101L), eq("client-key-001"), eq("device-001"), eq("pending"), eq(1L), eq(20L)))

View File

@@ -1050,9 +1050,14 @@ const DigitalEmployeeOps: React.FC = () => {
{detailRecord.last_restart_at && ` · 最近重启 ${detailRecord.last_restart_at}`}
{detailRecord.recovery_policy_id && ` · 恢复策略 ${detailRecord.recovery_policy_id}`}
{detailRecord.auto_recovery_status && ` · 自动恢复 ${detailRecord.auto_recovery_status}`}
{detailRecord.recovery_action_request_id && ` · 恢复请求 ${detailRecord.recovery_action_request_id}`}
{detailRecord.service_start_disabled && ' · 启动禁用'}
{detailRecord.service_stop_disabled && ' · 停止禁用'}
{detailRecord.lease_takeover_disabled && ' · lease 接管禁用'}
{detailRecord.auto_recovery_disabled && ' · 自动恢复执行禁用'}
<Typography.Text type="secondary">
{' '}
· / start/stop lease
· / start/stop lease
</Typography.Text>
</Typography.Paragraph>
)}

View File

@@ -653,6 +653,11 @@ export interface DigitalEmployeeAdminWorkbenchRow {
last_restart_at?: string;
recovery_policy_id?: string;
auto_recovery_status?: string;
service_start_disabled?: boolean;
service_stop_disabled?: boolean;
lease_takeover_disabled?: boolean;
auto_recovery_disabled?: boolean;
recovery_action_request_id?: string;
memory_id?: string;
memory_key?: string;
governance_status?: string;

View File

@@ -2231,20 +2231,33 @@ function checkDigitalAdminServiceOperationsWorkbench() {
[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"],
[dto, "service_start_disabled", "backend service start disabled row field"],
[dto, "service_stop_disabled", "backend service stop disabled row field"],
[dto, "lease_takeover_disabled", "backend lease takeover disabled row field"],
[dto, "auto_recovery_disabled", "backend auto recovery disabled row field"],
[dto, "recovery_action_request_id", "backend recovery action request row field"],
[test, "queryAdminWorkbenchServiceOperationsReturnsServiceHealthAndRestartRowsOnly", "backend service operations test"],
[test, "queryAdminWorkbenchServiceOperationsReturnsLeaseRestartAndRecoveryRowsOnly", "backend service recovery operations test"],
[test, "queryAdminWorkbenchServiceOperationsReturnsSafetyShellFields", "backend service recovery safety shell 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"],
[qimingTypes, "service_start_disabled", "qiming service start disabled field"],
[qimingTypes, "service_stop_disabled", "qiming service stop disabled field"],
[qimingTypes, "lease_takeover_disabled", "qiming lease takeover disabled field"],
[qimingTypes, "auto_recovery_disabled", "qiming auto recovery disabled field"],
[qimingTypes, "recovery_action_request_id", "qiming recovery action request field"],
[qimingOps, "服务运营", "qiming service operations tab"],
[qimingOps, "只记录复核/忽略意图", "qiming service operations safety copy"],
[qimingOps, "不开放 start/stop", "qiming service start stop guard copy"],
[qimingOps, "不接管 lease", "qiming service lease takeover guard copy"],
[qimingOps, "不自动恢复服务", "qiming service auto recovery guard copy"],
[qimingOps, "恢复请求", "qiming service recovery request copy"],
[docs, "正式管理端服务恢复运营预览", "docs service recovery absorption"],
[docs, "恢复请求状态", "docs service recovery request absorption"],
];
const missing = requiredSnippets
.filter(([content, snippet]) => !content.includes(snippet))

View File

@@ -573,7 +573,7 @@ GET /api/digital-employee/approvals
- 建议:下一轮围绕正式管理端治理策略和 ACP 硬中断安全设计,把本地治理审计推进到完整跨端裁决与执行控制。
5. **ManagedService 控制面**
- 已吸收Agent、MCP、Computer Server、Lanproxy、File Server、GUI Server 状态已进入 snapshot / projectionFile 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 issueqiming 详情可展示 lease holder/到期时间、重启次数/最近重启、恢复策略和自动恢复状态,并明确不接管 lease、不自动恢复服务。
- 已吸收Agent、MCP、Computer Server、Lanproxy、File Server、GUI Server 状态已进入 snapshot / projectionFile 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 issueqiming 详情可展示 lease holder/到期时间、重启次数/最近重启、恢复策略和自动恢复状态,并明确不接管 lease、不自动恢复服务。服务恢复安全壳预览已开始吸收,`service_operations` 会展示服务启动/停止禁用、lease 接管禁用、自动恢复执行禁用和恢复请求状态,管理端动作记录会对 `start_service``stop_service``lease_takeover``execute_auto_recovery` 返回明确禁用 reason不进入客户端待执行队列。
- 缺口:仍缺少真实 start / stop、lease 接管执行、自动恢复执行和完整外部管理端服务运营台Agent、MCP、Computer Server、Lanproxy 等高风险服务不暴露数字员工重启动作。
- 建议:先沉淀安全重启审计和人工确认体验,再评估更细粒度的服务恢复策略。