吸收数字员工管理端记忆技能审计预览

This commit is contained in:
baiyanyun
2026-06-13 14:21:16 +08:00
parent b6b0a9dcb3
commit de501c0252
7 changed files with 318 additions and 5 deletions

View File

@@ -45,6 +45,16 @@ public class DigitalEmployeeAdminWorkbenchRowDto {
private String routeDecisionId;
@JsonProperty("service_id")
private String serviceId;
@JsonProperty("memory_id")
private String memoryId;
@JsonProperty("memory_key")
private String memoryKey;
@JsonProperty("skill_id")
private String skillId;
@JsonProperty("tool_name")
private String toolName;
@JsonProperty("server_id")
private String serverId;
@JsonProperty("occurred_at")
private String occurredAt;
@JsonProperty("synced_at")

View File

@@ -70,6 +70,8 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
"notification:mark_read", "notification:dismiss",
"artifact:mark_keep", "artifact:mark_expire", "artifact:mark_reviewed",
"daily_report:mark_delivered", "daily_report:mark_confirmed", "daily_report:request_approval",
"memory:archive_memory", "memory:restore_memory", "memory:mark_reviewed",
"skill:mark_reviewed", "skill:dismiss",
"audit:mark_reviewed", "audit:dismiss",
"event:mark_reviewed", "event:dismiss"
);
@@ -980,6 +982,11 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
.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")))
.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")))
.toolName(firstString(businessView.get("tool_name"), businessView.get("toolName"), payload.get("tool_name"), payload.get("toolName")))
.serverId(firstString(businessView.get("server_id"), businessView.get("serverId"), payload.get("server_id"), payload.get("serverId")))
.occurredAt(businessRow.getOccurredAt())
.syncedAt(record.getSyncedAt())
.clientKeyMasked(maskClientKey(record.getClientKey()))
@@ -1005,6 +1012,8 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
case "daily_reports" -> isDailyReportRow(row);
case "governance_commands" -> isGovernanceCommandRow(row);
case "route_governance" -> isRouteGovernanceRow(row);
case "memory_governance" -> isMemoryGovernanceRow(row);
case "skill_audits" -> isSkillAuditRow(row);
default -> true;
};
}
@@ -1087,6 +1096,31 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
|| StringUtils.isNotBlank(row.getRouteDecisionId());
}
private boolean isMemoryGovernanceRow(DigitalEmployeeAdminWorkbenchRowDto row) {
String category = firstString(row.getCategory());
String kind = firstString(row.getKind());
return StringUtils.equals(row.getEntityType(), "memory")
|| StringUtils.equals(category, "memory")
|| StringUtils.equals(category, "memory_governance")
|| startsWithAny(kind, "memory_")
|| StringUtils.isNotBlank(row.getMemoryId())
|| StringUtils.isNotBlank(row.getMemoryKey());
}
private boolean isSkillAuditRow(DigitalEmployeeAdminWorkbenchRowDto row) {
String category = firstString(row.getCategory());
String kind = firstString(row.getKind());
return StringUtils.equals(category, "skill")
|| StringUtils.equals(category, "skill_call_audit")
|| startsWithAny(kind, "skill_call_")
|| StringUtils.equals(category, "tool_call_audit")
|| StringUtils.equals(category, "sandbox_audit")
|| StringUtils.equals(category, "token_cost")
|| StringUtils.isNotBlank(row.getSkillId())
|| StringUtils.isNotBlank(row.getToolName())
|| StringUtils.isNotBlank(row.getServerId());
}
private String adminCategory(DigitalEmployeeBusinessViewRowDto row, Map<String, Object> businessView, Map<String, Object> payload) {
if (booleanValue(businessView.get("approval_conflict_active"))) return "approval_conflict";
String kind = firstString(row.getKind(), businessView.get("kind"), payload.get("kind"));
@@ -1104,6 +1138,8 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
}
if (startsWithAny(kind, "artifact_")) return "artifact_lifecycle";
if (startsWithAny(kind, "daily_report")) return "daily_report";
if (startsWithAny(kind, "memory_")) return "memory_governance";
if (startsWithAny(kind, "skill_call_")) return "skill_call_audit";
if (startsWithAny(kind, "governance_")) return "governance";
if (contains(kind, "audit")) return kind;
return row.getEntityType();

View File

@@ -349,6 +349,118 @@ class DigitalEmployeeSyncApplicationServiceImplTest {
assertThat(result.getRecords().get(3).getApprovalId()).isEqualTo("approval-1");
}
@Test
void queryAdminWorkbenchMemoryGovernanceReturnsMemoryRowsOnly() {
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-memory", "memory", "memory-1", 101L, new Date(10_000),
Map.of(
"entity_type", "memory",
"title", "客户偏好记忆",
"status", "active",
"memory_id", "memory-1",
"memory_key", "customer.preference",
"governance_status", "active",
"merge_candidate_count", 2,
"duplicate_memory_ids", List.of("memory-2")
),
Map.of("id", "memory-1", "key", "customer.preference")),
record("outbox-memory-archive", "event", "event-memory-1", 101L, new Date(11_000),
Map.of(
"entity_type", "event",
"category", "memory_governance",
"kind", "memory_archived",
"status", "recorded",
"title", "记忆已归档",
"memory_id", "memory-3",
"memory_key", "customer.old",
"last_governance_action", "archive"
),
Map.of("id", "event-memory-1", "kind", "memory_archived")),
record("outbox-task", "task", "task-1", 101L, new Date(12_000),
Map.of("entity_type", "task", "title", "普通任务", "status", "running"),
Map.of("id", "task-1", "status", "running")),
record("outbox-audit", "event", "event-audit-1", 101L, new Date(13_000),
Map.of("entity_type", "event", "category", "tool_call_audit", "title", "工具审计", "status", "warn"),
Map.of("id", "event-audit-1", "status", "warn"))
)));
DigitalEmployeeAdminWorkbenchPageDto result = service.queryAdminWorkbench(
"memory_governance", null, null, null, null, null, null, null, null, null, null, null, 1L, 20L
);
assertThat(result.getTotal()).isEqualTo(2);
assertThat(result.getRecords()).extracting(DigitalEmployeeAdminWorkbenchRowDto::getEntityId)
.containsExactly("memory-1", "event-memory-1");
assertThat(result.getRecords().get(0).getView()).isEqualTo("memory_governance");
assertThat(result.getRecords().get(0).getMemoryId()).isEqualTo("memory-1");
assertThat(result.getRecords().get(0).getMemoryKey()).isEqualTo("customer.preference");
assertThat(result.getRecords().get(0).getBusinessView()).containsEntry("merge_candidate_count", 2);
assertThat(result.getRecords().get(1).getCategory()).isEqualTo("memory_governance");
}
@Test
void queryAdminWorkbenchSkillAuditsReturnsSkillAndToolAuditRowsOnly() {
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-skill-allowed", "event", "event-skill-1", 101L, new Date(10_000),
Map.of(
"entity_type", "event",
"category", "skill",
"kind", "skill_call_allowed",
"status", "allowed",
"title", "技能调用允许",
"skill_id", "skill-web",
"tool_name", "browser.open",
"server_id", "mcp-browser",
"decision", "allowed",
"risk_level", "low"
),
Map.of("id", "event-skill-1", "kind", "skill_call_allowed")),
record("outbox-tool-audit", "event", "event-skill-2", 101L, new Date(11_000),
Map.of(
"entity_type", "event",
"category", "tool_call_audit",
"kind", "tool_call_audit",
"status", "rejected",
"title", "工具调用拒绝",
"tool_name", "shell",
"server_id", "local",
"decision", "rejected"
),
Map.of("id", "event-skill-2", "kind", "tool_call_audit")),
record("outbox-token-cost", "event", "event-skill-3", 101L, new Date(12_000),
Map.of(
"entity_type", "event",
"category", "token_cost",
"kind", "token_cost_recorded",
"status", "recorded",
"title", "Token 成本估算",
"model", "gpt-4.1",
"total_tokens", 180,
"estimated_cost_usd", 0.012
),
Map.of("id", "event-skill-3", "kind", "token_cost_recorded")),
record("outbox-notification", "notification", "notification-1", 101L, new Date(13_000),
Map.of("entity_type", "notification", "title", "普通通知", "status", "unread"),
Map.of("id", "notification-1", "status", "unread"))
)));
DigitalEmployeeAdminWorkbenchPageDto result = service.queryAdminWorkbench(
"skill_audits", 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-skill-1", "event-skill-2", "event-skill-3");
assertThat(result.getRecords().get(0).getView()).isEqualTo("skill_audits");
assertThat(result.getRecords().get(0).getSkillId()).isEqualTo("skill-web");
assertThat(result.getRecords().get(0).getToolName()).isEqualTo("browser.open");
assertThat(result.getRecords().get(0).getServerId()).isEqualTo("mcp-browser");
assertThat(result.getRecords().get(1).getCategory()).isEqualTo("tool_call_audit");
assertThat(result.getRecords().get(2).getBusinessView()).containsEntry("total_tokens", 180);
}
@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)))