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