吸收数字员工管理端审计归档账单预览

This commit is contained in:
baiyanyun
2026-06-14 13:21:51 +08:00
parent dd6c10f44b
commit 8b651e4ae1
7 changed files with 111 additions and 23 deletions

View File

@@ -141,6 +141,17 @@ public class DigitalEmployeeAdminWorkbenchRowDto {
private String toolName; private String toolName;
@JsonProperty("server_id") @JsonProperty("server_id")
private String serverId; private String serverId;
@JsonProperty("audit_archive_id")
private String auditArchiveId;
@JsonProperty("archive_status")
private String archiveStatus;
@JsonProperty("archive_location")
private String archiveLocation;
@JsonProperty("billing_estimate_id")
private String billingEstimateId;
@JsonProperty("billing_source")
private String billingSource;
private Boolean estimated;
@JsonProperty("policy_kind") @JsonProperty("policy_kind")
private String policyKind; private String policyKind;
@JsonProperty("policy_key") @JsonProperty("policy_key")

View File

@@ -1113,6 +1113,12 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
.policyStatus(firstString(businessView.get("policy_status"), businessView.get("policyStatus"), payload.get("policy_status"), payload.get("policyStatus"))) .policyStatus(firstString(businessView.get("policy_status"), businessView.get("policyStatus"), payload.get("policy_status"), payload.get("policyStatus")))
.toolName(firstString(businessView.get("tool_name"), businessView.get("toolName"), payload.get("tool_name"), payload.get("toolName"))) .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"))) .serverId(firstString(businessView.get("server_id"), businessView.get("serverId"), payload.get("server_id"), payload.get("serverId")))
.auditArchiveId(firstString(businessView.get("audit_archive_id"), businessView.get("auditArchiveId"), payload.get("audit_archive_id"), payload.get("auditArchiveId")))
.archiveStatus(firstString(businessView.get("archive_status"), businessView.get("archiveStatus"), payload.get("archive_status"), payload.get("archiveStatus")))
.archiveLocation(firstString(businessView.get("archive_location"), businessView.get("archiveLocation"), payload.get("archive_location"), payload.get("archiveLocation")))
.billingEstimateId(firstString(businessView.get("billing_estimate_id"), businessView.get("billingEstimateId"), payload.get("billing_estimate_id"), payload.get("billingEstimateId")))
.billingSource(firstString(businessView.get("billing_source"), businessView.get("billingSource"), payload.get("billing_source"), payload.get("billingSource")))
.estimated(nullableBooleanValue(firstPresent(businessView.get("estimated"), payload.get("estimated"))))
.policyKind(firstString(businessView.get("policy_kind"), businessView.get("policyKind"), payload.get("policy_kind"), payload.get("policyKind"))) .policyKind(firstString(businessView.get("policy_kind"), businessView.get("policyKind"), payload.get("policy_kind"), payload.get("policyKind")))
.policyKey(firstString(businessView.get("policy_key"), businessView.get("policyKey"), payload.get("policy_key"), payload.get("policyKey"))) .policyKey(firstString(businessView.get("policy_key"), businessView.get("policyKey"), payload.get("policy_key"), payload.get("policyKey")))
.scheduleId(firstString(businessView.get("schedule_id"), businessView.get("scheduleId"), payload.get("schedule_id"), payload.get("scheduleId"))) .scheduleId(firstString(businessView.get("schedule_id"), businessView.get("scheduleId"), payload.get("schedule_id"), payload.get("scheduleId")))
@@ -1216,9 +1222,12 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
} }
private boolean isAuditRow(DigitalEmployeeAdminWorkbenchRowDto row) { private boolean isAuditRow(DigitalEmployeeAdminWorkbenchRowDto row) {
return Set.of("sandbox_audit", "token_cost", "tool_call_audit", "governance", "sync_failure").contains(row.getCategory()) return Set.of("sandbox_audit", "token_cost", "tool_call_audit", "governance", "sync_failure", "audit_archive", "billing_estimate").contains(row.getCategory())
|| contains(row.getKind(), "audit") || contains(row.getKind(), "audit")
|| contains(row.getKind(), "cost"); || contains(row.getKind(), "cost")
|| contains(row.getKind(), "billing")
|| StringUtils.isNotBlank(row.getAuditArchiveId())
|| StringUtils.isNotBlank(row.getBillingEstimateId());
} }
private boolean isDailyReportRow(DigitalEmployeeAdminWorkbenchRowDto row) { private boolean isDailyReportRow(DigitalEmployeeAdminWorkbenchRowDto row) {

View File

@@ -1268,29 +1268,35 @@ class DigitalEmployeeSyncApplicationServiceImplTest {
when(repository.queryBusinessRecords(eq(101L), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), eq(1L), eq(20L))) 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( .thenReturn(page(List.of(
record("outbox-sandbox-audit", "event", "event-audit-1", 101L, new Date(10_000), record("outbox-sandbox-audit", "event", "event-audit-1", 101L, new Date(10_000),
Map.of( Map.ofEntries(
"entity_type", "event", Map.entry("entity_type", "event"),
"category", "sandbox_audit", Map.entry("category", "sandbox_audit"),
"kind", "sandbox_command_observed", Map.entry("kind", "sandbox_command_observed"),
"status", "warn", Map.entry("status", "warn"),
"level", "warn", Map.entry("level", "warn"),
"title", "Sandbox 命令观察", Map.entry("title", "Sandbox 命令观察"),
"operation", "shell_exec", Map.entry("operation", "shell_exec"),
"summary", "命令参数已脱敏" Map.entry("summary", "命令参数已脱敏"),
Map.entry("audit_archive_id", "archive-1"),
Map.entry("archive_status", "archived"),
Map.entry("archive_location", "admin://audit-archive/archive-1")
), ),
Map.of("id", "event-audit-1", "kind", "sandbox_command_observed")), Map.of("id", "event-audit-1", "kind", "sandbox_command_observed")),
record("outbox-token-cost", "event", "event-audit-2", 101L, new Date(11_000), record("outbox-token-cost", "event", "event-audit-2", 101L, new Date(11_000),
Map.of( Map.ofEntries(
"entity_type", "event", Map.entry("entity_type", "event"),
"category", "token_cost", Map.entry("category", "token_cost"),
"kind", "token_cost_recorded", Map.entry("kind", "token_cost_recorded"),
"status", "recorded", Map.entry("status", "recorded"),
"title", "Token 成本估算", Map.entry("title", "Token 成本估算"),
"model", "gpt-4.1", Map.entry("model", "gpt-4.1"),
"input_tokens", 120, Map.entry("input_tokens", 120),
"output_tokens", 60, Map.entry("output_tokens", 60),
"total_tokens", 180, Map.entry("total_tokens", 180),
"estimated_cost_usd", 0.012 Map.entry("estimated_cost_usd", 0.012),
Map.entry("billing_estimate_id", "billing-estimate-1"),
Map.entry("billing_source", "runtime_token_cost"),
Map.entry("estimated", true)
), ),
Map.of("id", "event-audit-2", "kind", "token_cost_recorded")), Map.of("id", "event-audit-2", "kind", "token_cost_recorded")),
record("outbox-tool-audit", "event", "event-audit-3", 101L, new Date(12_000), record("outbox-tool-audit", "event", "event-audit-3", 101L, new Date(12_000),
@@ -1351,8 +1357,14 @@ class DigitalEmployeeSyncApplicationServiceImplTest {
.containsExactly("sandbox_audit", "token_cost", "tool_call_audit", "sync_failure", "governance"); .containsExactly("sandbox_audit", "token_cost", "tool_call_audit", "sync_failure", "governance");
assertThat(result.getRecords().get(0).getLevel()).isEqualTo("warn"); assertThat(result.getRecords().get(0).getLevel()).isEqualTo("warn");
assertThat(result.getRecords().get(0).getSummary()).isEqualTo("命令参数已脱敏"); assertThat(result.getRecords().get(0).getSummary()).isEqualTo("命令参数已脱敏");
assertThat(result.getRecords().get(0).getAuditArchiveId()).isEqualTo("archive-1");
assertThat(result.getRecords().get(0).getArchiveStatus()).isEqualTo("archived");
assertThat(result.getRecords().get(0).getArchiveLocation()).isEqualTo("admin://audit-archive/archive-1");
assertThat(result.getRecords().get(1).getBusinessView()).containsEntry("total_tokens", 180); assertThat(result.getRecords().get(1).getBusinessView()).containsEntry("total_tokens", 180);
assertThat(result.getRecords().get(1).getBusinessView()).containsEntry("estimated_cost_usd", 0.012); assertThat(result.getRecords().get(1).getBusinessView()).containsEntry("estimated_cost_usd", 0.012);
assertThat(result.getRecords().get(1).getBillingEstimateId()).isEqualTo("billing-estimate-1");
assertThat(result.getRecords().get(1).getBillingSource()).isEqualTo("runtime_token_cost");
assertThat(result.getRecords().get(1).getEstimated()).isTrue();
assertThat(result.getRecords().get(2).getBusinessView()).containsEntry("tool_name", "shell"); assertThat(result.getRecords().get(2).getBusinessView()).containsEntry("tool_name", "shell");
assertThat(result.getRecords().get(3).getSummary()).isEqualTo("remote_timeout"); assertThat(result.getRecords().get(3).getSummary()).isEqualTo("remote_timeout");
assertThat(result.getRecords().get(4).getBusinessView()).containsEntry("command_id", "command-1"); assertThat(result.getRecords().get(4).getBusinessView()).containsEntry("command_id", "command-1");

View File

@@ -138,6 +138,10 @@ function renderSummary(record: DigitalEmployeeAdminWorkbenchRow) {
record.permission_scope && `Scope ${record.permission_scope}`, record.permission_scope && `Scope ${record.permission_scope}`,
record.policy_source && `PolicySource ${record.policy_source}`, record.policy_source && `PolicySource ${record.policy_source}`,
record.tool_name && `Tool ${record.tool_name}`, record.tool_name && `Tool ${record.tool_name}`,
record.audit_archive_id && `Archive ${record.audit_archive_id}`,
record.archive_status && `ArchiveStatus ${record.archive_status}`,
record.billing_estimate_id && `Billing ${record.billing_estimate_id}`,
record.billing_source && `BillingSource ${record.billing_source}`,
record.schedule_id && `Schedule ${record.schedule_id}`, record.schedule_id && `Schedule ${record.schedule_id}`,
record.job_id && `Job ${record.job_id}`, record.job_id && `Job ${record.job_id}`,
record.revision_id && `Revision ${record.revision_id}`, record.revision_id && `Revision ${record.revision_id}`,
@@ -976,6 +980,20 @@ const DigitalEmployeeOps: React.FC = () => {
</Typography.Text> </Typography.Text>
</Typography.Paragraph> </Typography.Paragraph>
)} )}
{activeView === 'audits' && (
<Typography.Paragraph>
<Typography.Text strong>/</Typography.Text>
{detailRecord.audit_archive_id || detailRecord.billing_estimate_id || detailRecord.entity_id || '--'}
{detailRecord.archive_status && ` · 归档状态 ${detailRecord.archive_status}`}
{detailRecord.archive_location && ` · 归档位置 ${detailRecord.archive_location}`}
{detailRecord.billing_source && ` · 估算来源 ${detailRecord.billing_source}`}
{detailRecord.estimated != null && ` · ${detailRecord.estimated ? '估算费用' : '真实费用'}`}
<Typography.Text type="secondary">
{' '}
· promptraw commandauthorization token
</Typography.Text>
</Typography.Paragraph>
)}
{activeView === 'governance_commands' && ( {activeView === 'governance_commands' && (
<Typography.Paragraph> <Typography.Paragraph>
<Typography.Text strong></Typography.Text> <Typography.Text strong></Typography.Text>

View File

@@ -647,6 +647,12 @@ export interface DigitalEmployeeAdminWorkbenchRow {
policy_status?: string; policy_status?: string;
tool_name?: string; tool_name?: string;
server_id?: string; server_id?: string;
audit_archive_id?: string;
archive_status?: string;
archive_location?: string;
billing_estimate_id?: string;
billing_source?: string;
estimated?: boolean;
policy_kind?: string; policy_kind?: string;
policy_key?: string; policy_key?: string;
schedule_id?: string; schedule_id?: string;

View File

@@ -2385,10 +2385,28 @@ function checkDigitalAdminAuditDiagnosticsWorkbench() {
"service", "service",
"DigitalEmployeeSyncApplicationServiceImplTest.java", "DigitalEmployeeSyncApplicationServiceImplTest.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 qimingOpsPath = path.join(qimingRoot, "src", "pages", "SystemManagement", "Content", "DigitalEmployeeOps", "index.tsx"); const qimingOpsPath = path.join(qimingRoot, "src", "pages", "SystemManagement", "Content", "DigitalEmployeeOps", "index.tsx");
const qimingTypesPath = path.join(qimingRoot, "src", "types", "interfaces", "systemManage.ts"); const qimingTypesPath = path.join(qimingRoot, "src", "types", "interfaces", "systemManage.ts");
const docsPath = path.resolve(packageRoot, "..", "..", "docs", "digital-employee-frontend-integration-plan.md"); const docsPath = path.resolve(packageRoot, "..", "..", "docs", "digital-employee-frontend-integration-plan.md");
const service = fs.readFileSync(servicePath, "utf8"); const service = fs.readFileSync(servicePath, "utf8");
const dto = fs.readFileSync(dtoPath, "utf8");
const test = fs.readFileSync(testPath, "utf8"); const test = fs.readFileSync(testPath, "utf8");
const qimingOps = fs.readFileSync(qimingOpsPath, "utf8"); const qimingOps = fs.readFileSync(qimingOpsPath, "utf8");
const qimingTypes = fs.readFileSync(qimingTypesPath, "utf8"); const qimingTypes = fs.readFileSync(qimingTypesPath, "utf8");
@@ -2400,14 +2418,28 @@ function checkDigitalAdminAuditDiagnosticsWorkbench() {
[service, "token_cost", "backend token cost category"], [service, "token_cost", "backend token cost category"],
[service, "tool_call_audit", "backend tool call audit category"], [service, "tool_call_audit", "backend tool call audit category"],
[service, "sync_failure", "backend sync failure audit category"], [service, "sync_failure", "backend sync failure audit category"],
[service, "audit_archive", "backend audit archive category"],
[service, "billing_estimate", "backend billing estimate category"],
[dto, "audit_archive_id", "backend audit archive id row field"],
[dto, "archive_status", "backend archive status row field"],
[dto, "archive_location", "backend archive location row field"],
[dto, "billing_estimate_id", "backend billing estimate id row field"],
[dto, "billing_source", "backend billing source row field"],
[test, "queryAdminWorkbenchAuditsReturnsAuditRowsOnly", "backend audit diagnostics test"], [test, "queryAdminWorkbenchAuditsReturnsAuditRowsOnly", "backend audit diagnostics test"],
[test, "getAuditArchiveId", "backend audit archive assertion"],
[test, "getBillingEstimateId", "backend billing estimate assertion"],
[test, "doesNotContainKeys(\"token\", \"authorization\", \"raw_input\", \"command\", \"prompt\")", "backend audit redaction assertion"], [test, "doesNotContainKeys(\"token\", \"authorization\", \"raw_input\", \"command\", \"prompt\")", "backend audit redaction assertion"],
[qimingTypes, "audits", "qiming audits view type"], [qimingTypes, "audits", "qiming audits view type"],
[qimingTypes, "audit_archive_id", "qiming audit archive id field"],
[qimingTypes, "billing_source", "qiming billing source field"],
[qimingOps, "审计诊断", "qiming audit diagnostics tab"], [qimingOps, "审计诊断", "qiming audit diagnostics tab"],
[qimingOps, "归档位置", "qiming audit archive location copy"],
[qimingOps, "估算来源", "qiming billing source copy"],
[qimingOps, "标记复核", "qiming audit reviewed action"], [qimingOps, "标记复核", "qiming audit reviewed action"],
[qimingOps, "忽略", "qiming audit dismiss action"], [qimingOps, "忽略", "qiming audit dismiss action"],
[qimingOps, "标记归档", "qiming audit archive action"], [qimingOps, "标记归档", "qiming audit archive action"],
[docs, "正式管理端审计诊断预览", "docs audit diagnostics absorption"], [docs, "正式管理端审计诊断预览", "docs audit diagnostics absorption"],
[docs, "正式管理端审计归档账单预览", "docs audit archive billing absorption"],
]; ];
const missing = requiredSnippets const missing = requiredSnippets
.filter(([content, snippet]) => !content.includes(snippet)) .filter(([content, snippet]) => !content.includes(snippet))

View File

@@ -604,7 +604,7 @@ GET /api/digital-employee/approvals
- 建议:下一轮围绕管理端正式路由 UI 和跨设备冲突可视化,把 embedded 批量干预入口推进到管理端运营台。 - 建议:下一轮围绕管理端正式路由 UI 和跨设备冲突可视化,把 embedded 批量干预入口推进到管理端运营台。
11. **诊断 / 成本 / 审计视图** 11. **诊断 / 成本 / 审计视图**
- 已吸收:同步失败诊断较完整,部分 sandbox / memory / service 日志在 qimingclaw 其他模块中存在embedded adapter 已接管 `/api/doctor``/api/cost``/api/audit`,从 snapshot、runtime records、sync status、managed services 和 artifact delivery facts 生成诊断、估算成本和审计投影sandbox audit、token/cost 和 CLI/tool 调用事件已开始通过 `digital_events` 与 outbox `business_view` 提炼为 `sandbox_audit``token_cost``tool_call_audit` 分类,并只同步 session、tool、server、operation、status、exit code、duration、model、token 计数和估算成本等安全字段;数字员工首页已展示诊断/审计/Token/估算成本摘要,`/api/digital-employee/audits` 已提供本地审计业务视图诊断与审计风险通知联动已开始闭环。管理端正式审计视图预览和正式管理端审计诊断预览已开始吸收qiming-backend 的 `audits` 运营台视图会聚合 sandbox / token cost / tool call / sync failure / governance 审计业务视图qiming“数字员工运营台”的“审计诊断”tab 可标记复核、忽略和脱敏归档。诊断修复动作预览已开始吸收,“处置台”可把 doctor/audit/service/policy 风险转为可筛选、可标记和可安全重试的运营事项。发送账单审计外壳已开始吸收后端提供估算账单查询和审计归档记录接口qiming 运营台可显示 Token/估算成本并标记脱敏归档;归档记录会剔除 token、authorization、raw_input、prompt 和 command 等敏感字段。 - 已吸收:同步失败诊断较完整,部分 sandbox / memory / service 日志在 qimingclaw 其他模块中存在embedded adapter 已接管 `/api/doctor``/api/cost``/api/audit`,从 snapshot、runtime records、sync status、managed services 和 artifact delivery facts 生成诊断、估算成本和审计投影sandbox audit、token/cost 和 CLI/tool 调用事件已开始通过 `digital_events` 与 outbox `business_view` 提炼为 `sandbox_audit``token_cost``tool_call_audit` 分类,并只同步 session、tool、server、operation、status、exit code、duration、model、token 计数和估算成本等安全字段;数字员工首页已展示诊断/审计/Token/估算成本摘要,`/api/digital-employee/audits` 已提供本地审计业务视图诊断与审计风险通知联动已开始闭环。管理端正式审计视图预览和正式管理端审计诊断预览已开始吸收qiming-backend 的 `audits` 运营台视图会聚合 sandbox / token cost / tool call / sync failure / governance 审计业务视图qiming“数字员工运营台”的“审计诊断”tab 可标记复核、忽略和脱敏归档。诊断修复动作预览已开始吸收,“处置台”可把 doctor/audit/service/policy 风险转为可筛选、可标记和可安全重试的运营事项。发送账单审计外壳已开始吸收后端提供估算账单查询和审计归档记录接口qiming 运营台可显示 Token/估算成本并标记脱敏归档;归档记录会剔除 token、authorization、raw_input、prompt 和 command 等敏感字段。正式管理端审计归档账单预览已开始吸收,审计诊断详情可展示 `audit_archive_id``archive_status``archive_location``billing_estimate_id``billing_source``estimated`,用于解释脱敏归档位置与账单估算来源。
- 缺口:真实计费账单、独立 sandbox audit 文件归档、自动诊断修复动作和正式外部管理端审计视图尚未统一吸收;当前账单仍是估算,审计归档只记录脱敏摘要和归档位置。 - 缺口:真实计费账单、独立 sandbox audit 文件归档、自动诊断修复动作和正式外部管理端审计视图尚未统一吸收;当前账单仍是估算,审计归档只记录脱敏摘要和归档位置。
- 建议:下一轮围绕管理端正式审计视图和诊断修复动作,把本地可追溯审计继续推进到可运营处置。 - 建议:下一轮围绕管理端正式审计视图和诊断修复动作,把本地可追溯审计继续推进到可运营处置。