吸收数字员工管理端服务运营预览

This commit is contained in:
baiyanyun
2026-06-12 23:30:31 +08:00
parent 8ed4e0ce3b
commit ca746366f6
7 changed files with 186 additions and 2 deletions

View File

@@ -43,6 +43,8 @@ public class DigitalEmployeeAdminWorkbenchRowDto {
private String reportId;
@JsonProperty("route_decision_id")
private String routeDecisionId;
@JsonProperty("service_id")
private String serviceId;
@JsonProperty("occurred_at")
private String occurredAt;
@JsonProperty("synced_at")

View File

@@ -979,6 +979,7 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
.notificationId(firstString(businessView.get("notification_id"), payload.get("notification_id")))
.reportId(firstString(businessView.get("report_id"), payload.get("report_id")))
.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")))
.occurredAt(businessRow.getOccurredAt())
.syncedAt(record.getSyncedAt())
.clientKeyMasked(maskClientKey(record.getClientKey()))
@@ -997,6 +998,7 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
return switch (view) {
case "interventions" -> isInterventionRow(row);
case "approval_governance" -> isApprovalGovernanceRow(row);
case "service_operations" -> isServiceOperationsRow(row);
case "artifact_lifecycle" -> isArtifactLifecycleRow(row);
case "notifications" -> isNotificationRow(row);
case "audits" -> isAuditRow(row);
@@ -1037,6 +1039,20 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
|| StringUtils.equals(row.getCategory(), "artifact_lifecycle");
}
private boolean isServiceOperationsRow(DigitalEmployeeAdminWorkbenchRowDto row) {
String kind = firstString(row.getKind());
String category = firstString(row.getCategory());
return StringUtils.equals(category, "managed_service")
|| StringUtils.equals(category, "service_operation")
|| StringUtils.equals(category, "service_health")
|| contains(kind, "managed_service")
|| contains(kind, "service_snapshot")
|| contains(kind, "service_restart")
|| contains(kind, "doctor_service")
|| contains(kind, "doctor_issue")
|| StringUtils.isNotBlank(row.getServiceId());
}
private boolean isNotificationRow(DigitalEmployeeAdminWorkbenchRowDto row) {
return StringUtils.equals(row.getCategory(), "notification")
|| StringUtils.equals(row.getCategory(), "action_notification")

View File

@@ -349,6 +349,60 @@ class DigitalEmployeeSyncApplicationServiceImplTest {
assertThat(result.getRecords().get(3).getApprovalId()).isEqualTo("approval-1");
}
@Test
void queryAdminWorkbenchServiceOperationsReturnsServiceHealthAndRestartRowsOnly() {
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-snapshot", "event", "event-service-1", 101L, new Date(10_000),
Map.of(
"entity_type", "event",
"category", "managed_service",
"kind", "service_snapshot",
"status", "degraded",
"title", "File Server 异常",
"service_id", "file-server",
"level", "warn",
"summary", "端口不可用"
),
Map.of("id", "event-service-1", "kind", "service_snapshot")),
record("outbox-service-restart", "event", "event-service-2", 101L, new Date(11_000),
Map.of(
"entity_type", "event",
"category", "service_operation",
"kind", "managed_service_restart",
"status", "recorded",
"title", "GUI Server 安全重启",
"service_id", "gui-server"
),
Map.of("id", "event-service-2", "kind", "managed_service_restart")),
record("outbox-doctor-service", "event", "event-service-3", 101L, new Date(12_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-3", "kind", "doctor_service_issue")),
record("outbox-approval", "approval", "approval-1", 101L, new Date(13_000),
Map.of("entity_type", "approval", "title", "审批", "status", "pending"),
Map.of("id", "approval-1", "status", "pending"))
)));
DigitalEmployeeAdminWorkbenchPageDto result = service.queryAdminWorkbench(
"service_operations", null, null, null, null, null, null, null, null, null, null, null, 1L, 20L
);
assertThat(result.getTotal()).isEqualTo(3);
assertThat(result.getRecords()).extracting(DigitalEmployeeAdminWorkbenchRowDto::getEntityId)
.containsExactly("event-service-1", "event-service-2", "event-service-3");
assertThat(result.getRecords().get(0).getView()).isEqualTo("service_operations");
assertThat(result.getRecords().get(0).getServiceId()).isEqualTo("file-server");
assertThat(result.getRecords().get(1).getCategory()).isEqualTo("service_operation");
assertThat(result.getRecords().get(2).getServiceId()).isEqualTo("agent-server");
}
@Test
void queryAdminWorkbenchAuditsRedactsSensitivePayloadFields() {
when(repository.queryBusinessRecords(eq(101L), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), eq(1L), eq(20L)))

View File

@@ -39,6 +39,7 @@ const WORKBENCH_TABS: Array<{
{ key: 'business', label: '业务视图' },
{ key: 'interventions', label: '介入台' },
{ key: 'approval_governance', label: '审批治理' },
{ key: 'service_operations', label: '服务运营' },
{ key: 'artifact_lifecycle', label: '产物治理' },
{ key: 'notifications', label: '通知运营' },
{ key: 'audits', label: '审计诊断' },
@@ -101,6 +102,7 @@ function renderSummary(record: DigitalEmployeeAdminWorkbenchRow) {
record.task_id && `Task ${record.task_id}`,
record.run_id && `Run ${record.run_id}`,
record.approval_id && `Approval ${record.approval_id}`,
record.service_id && `Service ${record.service_id}`,
record.artifact_id && `Artifact ${record.artifact_id}`,
record.route_decision_id && `Route ${record.route_decision_id}`,
record.policy_kind && `Policy ${record.policy_kind}`,
@@ -154,6 +156,9 @@ function actionTarget(record: DigitalEmployeeAdminWorkbenchRow, view: DigitalEmp
if (view === 'notifications' && record.notification_id) {
return { entityType: 'notification', entityId: record.notification_id };
}
if (view === 'service_operations') {
return { entityType: 'event', entityId: record.entity_id };
}
if (view === 'artifact_lifecycle' && record.artifact_id) {
return { entityType: 'artifact', entityId: record.artifact_id };
}
@@ -211,6 +216,12 @@ function allowedActions(record: DigitalEmployeeAdminWorkbenchRow, view: DigitalE
{ action: 'dismiss', label: '忽略' },
];
}
if (view === 'service_operations') {
return [
{ action: 'mark_reviewed', label: '标记已看' },
{ action: 'dismiss', label: '忽略' },
];
}
if (view === 'artifact_lifecycle' && record.artifact_id) {
return [
{ action: 'mark_keep', label: '标记保留' },
@@ -686,6 +697,7 @@ const DigitalEmployeeOps: React.FC = () => {
detailRecord.task_id && `Task ${detailRecord.task_id}`,
detailRecord.run_id && `Run ${detailRecord.run_id}`,
detailRecord.approval_id && `Approval ${detailRecord.approval_id}`,
detailRecord.service_id && `Service ${detailRecord.service_id}`,
detailRecord.artifact_id && `Artifact ${detailRecord.artifact_id}`,
detailRecord.notification_id &&
`Notification ${detailRecord.notification_id}`,
@@ -725,6 +737,18 @@ const DigitalEmployeeOps: React.FC = () => {
</Typography.Text>
</Typography.Paragraph>
)}
{activeView === 'service_operations' && (
<Typography.Paragraph>
<Typography.Text strong></Typography.Text>
{detailRecord.service_id || detailRecord.entity_id || '--'}
{detailRecord.category && ` · ${detailRecord.category}`}
{detailRecord.level && ` · ${detailRecord.level}`}
<Typography.Text type="secondary">
{' '}
· / start/stop
</Typography.Text>
</Typography.Paragraph>
)}
{allowedActions(detailRecord, activeView).length > 0 && (
<div style={{ marginBottom: 16 }}>
<Typography.Title level={5}></Typography.Title>

View File

@@ -560,6 +560,7 @@ export type DigitalEmployeeAdminWorkbenchView =
| 'business'
| 'interventions'
| 'approval_governance'
| 'service_operations'
| 'artifact_lifecycle'
| 'notifications'
| 'audits'
@@ -589,6 +590,7 @@ export interface DigitalEmployeeAdminWorkbenchRow {
notification_id?: string;
report_id?: string;
route_decision_id?: string;
service_id?: string;
policy_kind?: string;
policy_key?: string;
conflict_keys?: string[];

View File

@@ -1449,6 +1449,91 @@ function checkDigitalAdminApprovalGovernanceWorkbench() {
console.log("[DigitalEmployeeCheck] admin approval governance workbench hooks OK");
}
function checkDigitalAdminServiceOperationsWorkbench() {
console.log("\n[DigitalEmployeeCheck] Verify admin service operations workbench hooks");
const backendRoot = path.resolve(packageRoot, "..", "..", "..", "qiming-backend");
const qimingRoot = path.resolve(packageRoot, "..", "..", "..", "qiming");
const servicePath = path.join(
backendRoot,
"app-platform-modules",
"app-platform-agent",
"app-platform-agent-core-application",
"src",
"main",
"java",
"com",
"xspaceagi",
"agent",
"core",
"application",
"service",
"DigitalEmployeeSyncApplicationServiceImpl.java",
);
const dtoPath = path.join(
backendRoot,
"app-platform-modules",
"app-platform-agent",
"app-platform-agent-core-adapter",
"src",
"main",
"java",
"com",
"xspaceagi",
"agent",
"core",
"adapter",
"dto",
"digital",
"DigitalEmployeeAdminWorkbenchRowDto.java",
);
const testPath = path.join(
backendRoot,
"app-platform-modules",
"app-platform-agent",
"app-platform-agent-core-application",
"src",
"test",
"java",
"com",
"xspaceagi",
"agent",
"core",
"application",
"service",
"DigitalEmployeeSyncApplicationServiceImplTest.java",
);
const qimingOpsPath = path.join(qimingRoot, "src", "pages", "SystemManagement", "Content", "DigitalEmployeeOps", "index.tsx");
const qimingTypesPath = path.join(qimingRoot, "src", "types", "interfaces", "systemManage.ts");
const docsPath = path.resolve(packageRoot, "..", "..", "docs", "digital-employee-frontend-integration-plan.md");
const service = fs.readFileSync(servicePath, "utf8");
const dto = fs.readFileSync(dtoPath, "utf8");
const test = fs.readFileSync(testPath, "utf8");
const qimingOps = fs.readFileSync(qimingOpsPath, "utf8");
const qimingTypes = fs.readFileSync(qimingTypesPath, "utf8");
const docs = fs.readFileSync(docsPath, "utf8");
const requiredSnippets = [
[service, "service_operations", "backend service operations view"],
[service, "isServiceOperationsRow", "backend service operations matcher"],
[service, "managed_service", "backend managed service classifier"],
[service, "service_restart", "backend service restart classifier"],
[dto, "service_id", "backend service id row field"],
[test, "queryAdminWorkbenchServiceOperationsReturnsServiceHealthAndRestartRowsOnly", "backend service operations test"],
[qimingTypes, "service_operations", "qiming service operations view type"],
[qimingTypes, "service_id", "qiming service id 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"],
];
const missing = requiredSnippets
.filter(([content, snippet]) => !content.includes(snippet))
.map(([, , label]) => label);
if (missing.length > 0) {
throw new Error(`Missing admin service operations workbench hooks: ${missing.join(", ")}`);
}
console.log("[DigitalEmployeeCheck] admin service operations workbench hooks OK");
}
function checkLocalDatabaseSchema() {
console.log("\n[DigitalEmployeeCheck] Verify local SQLite digital schema");
const dbPath = path.join(os.homedir(), ".qimingclaw", "qimingclaw.db");
@@ -1512,6 +1597,7 @@ function main() {
checkDigitalBackendMaterializedFacts();
checkDigitalAdminRecordBillingArchiveShell();
checkDigitalAdminApprovalGovernanceWorkbench();
checkDigitalAdminServiceOperationsWorkbench();
checkLocalDatabaseSchema();
console.log("\n[DigitalEmployeeCheck] All checks passed");
} catch (error) {

View File

@@ -573,8 +573,8 @@ 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 新增“处置台”聚合服务异常、诊断风险、审计风险和策略拉取错误,支持批量标记已看、忽略、策略重拉和白名单服务安全重启。
- 缺口:仍缺少 start / stop、lease 持久化、重启历史、自动恢复策略和正式外部管理端服务运营台Agent、MCP、Computer Server、Lanproxy 等高风险服务不暴露数字员工重启动作。
- 已吸收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。
- 缺口:仍缺少 start / stop、lease 持久化、重启历史、自动恢复策略和完整外部管理端服务运营台Agent、MCP、Computer Server、Lanproxy 等高风险服务不暴露数字员工重启动作。
- 建议:先沉淀安全重启审计和人工确认体验,再评估更细粒度的服务恢复策略。
6. **技能生命周期与策略**