吸收数字员工发送账单审计外壳
This commit is contained in:
@@ -4,6 +4,9 @@ import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminWorkbenc
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminActionPageDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminActionRequestDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminActionResultDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminRecordRequestDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminRecordResultDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeBillingEstimateDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeBusinessViewPageDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeePlanBusinessViewDetailDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeePolicySnapshotPageDto;
|
||||
@@ -88,4 +91,15 @@ public interface DigitalEmployeeSyncApplicationService {
|
||||
String status,
|
||||
List<String> reasonCodes
|
||||
);
|
||||
|
||||
DigitalEmployeeAdminRecordResultDto recordDailyReportSendResult(DigitalEmployeeAdminRecordRequestDto request);
|
||||
|
||||
DigitalEmployeeBillingEstimateDto queryBillingEstimates(
|
||||
String clientKey,
|
||||
String deviceId,
|
||||
Date updatedFrom,
|
||||
Date updatedTo
|
||||
);
|
||||
|
||||
DigitalEmployeeAdminRecordResultDto recordAuditArchive(DigitalEmployeeAdminRecordRequestDto request);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.xspaceagi.agent.core.adapter.dto.digital;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAlias;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
public class DigitalEmployeeAdminRecordRequestDto {
|
||||
@JsonAlias("sync_record_id")
|
||||
private Long syncRecordId;
|
||||
@JsonAlias("client_key")
|
||||
private String clientKey;
|
||||
@JsonAlias("device_id")
|
||||
private String deviceId;
|
||||
@JsonAlias("entity_type")
|
||||
private String entityType;
|
||||
@JsonAlias("entity_id")
|
||||
private String entityId;
|
||||
private String status;
|
||||
private String endpoint;
|
||||
@JsonAlias("remote_id")
|
||||
private String remoteId;
|
||||
@JsonAlias("archive_location")
|
||||
private String archiveLocation;
|
||||
@JsonAlias("archive_summary")
|
||||
private Map<String, Object> archiveSummary;
|
||||
private String reason;
|
||||
@JsonProperty("request_payload")
|
||||
@JsonAlias("requestPayload")
|
||||
private Map<String, Object> requestPayload;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.xspaceagi.agent.core.adapter.dto.digital;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class DigitalEmployeeAdminRecordResultDto {
|
||||
private Boolean ok;
|
||||
private String status;
|
||||
@JsonProperty("reason_codes")
|
||||
private List<String> reasonCodes;
|
||||
private DigitalEmployeeAdminActionDto record;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.xspaceagi.agent.core.adapter.dto.digital;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class DigitalEmployeeBillingEstimateDto {
|
||||
@JsonProperty("total_tokens")
|
||||
private Long totalTokens;
|
||||
@JsonProperty("input_tokens")
|
||||
private Long inputTokens;
|
||||
@JsonProperty("output_tokens")
|
||||
private Long outputTokens;
|
||||
@JsonProperty("estimated_cost_usd")
|
||||
private BigDecimal estimatedCostUsd;
|
||||
@JsonProperty("request_count")
|
||||
private Long requestCount;
|
||||
private Boolean estimated;
|
||||
@JsonProperty("by_model")
|
||||
private Map<String, ModelEstimate> byModel;
|
||||
@JsonProperty("updated_at")
|
||||
private Date updatedAt;
|
||||
@JsonProperty("source_records")
|
||||
private List<Long> sourceRecords;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public static class ModelEstimate {
|
||||
@JsonProperty("total_tokens")
|
||||
private Long totalTokens;
|
||||
@JsonProperty("input_tokens")
|
||||
private Long inputTokens;
|
||||
@JsonProperty("output_tokens")
|
||||
private Long outputTokens;
|
||||
@JsonProperty("estimated_cost_usd")
|
||||
private BigDecimal estimatedCostUsd;
|
||||
@JsonProperty("request_count")
|
||||
private Long requestCount;
|
||||
}
|
||||
}
|
||||
@@ -11,8 +11,11 @@ import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminActionDt
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminActionPageDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminActionRequestDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminActionResultDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminRecordRequestDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminRecordResultDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminWorkbenchPageDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminWorkbenchRowDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeBillingEstimateDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeBusinessViewPageDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeBusinessViewRowDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeePlanBusinessViewDetailDto;
|
||||
@@ -40,6 +43,7 @@ import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
@@ -724,6 +728,184 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
@DSTransactional
|
||||
public DigitalEmployeeAdminRecordResultDto recordDailyReportSendResult(DigitalEmployeeAdminRecordRequestDto request) {
|
||||
return recordAdminObservation(enrichAdminRecordRequestFromSyncRecord(request), "daily_report", "daily_report_send_recorded", "send_record_recorded");
|
||||
}
|
||||
|
||||
@Override
|
||||
public DigitalEmployeeBillingEstimateDto queryBillingEstimates(String clientKey, String deviceId, Date updatedFrom, Date updatedTo) {
|
||||
Long tenantId = currentTenantId();
|
||||
if (tenantId == null) {
|
||||
return DigitalEmployeeBillingEstimateDto.builder()
|
||||
.totalTokens(0L)
|
||||
.inputTokens(0L)
|
||||
.outputTokens(0L)
|
||||
.estimatedCostUsd(BigDecimal.ZERO)
|
||||
.requestCount(0L)
|
||||
.estimated(true)
|
||||
.byModel(Map.of())
|
||||
.sourceRecords(List.of())
|
||||
.build();
|
||||
}
|
||||
IPage<DigitalEmployeeSyncRecord> page = digitalEmployeeSyncRecordRepository.queryBusinessRecords(
|
||||
tenantId,
|
||||
clientKey,
|
||||
deviceId,
|
||||
"event",
|
||||
null,
|
||||
updatedFrom,
|
||||
updatedTo,
|
||||
null,
|
||||
DEFAULT_PAGE_NO,
|
||||
MAX_PAGE_SIZE
|
||||
);
|
||||
long inputTokens = 0L;
|
||||
long outputTokens = 0L;
|
||||
long totalTokens = 0L;
|
||||
long requestCount = 0L;
|
||||
boolean estimated = false;
|
||||
BigDecimal cost = BigDecimal.ZERO;
|
||||
Date updatedAt = null;
|
||||
List<Long> sourceRecords = new ArrayList<>();
|
||||
Map<String, ModelAccumulator> byModel = new LinkedHashMap<>();
|
||||
for (DigitalEmployeeSyncRecord record : page.getRecords()) {
|
||||
Map<String, Object> view = readJsonObject(record.getBusinessView());
|
||||
if (!StringUtils.equals(firstString(view.get("category")), "token_cost")) continue;
|
||||
long input = numberValue(view.get("input_tokens")) == null ? 0L : numberValue(view.get("input_tokens"));
|
||||
long output = numberValue(view.get("output_tokens")) == null ? 0L : numberValue(view.get("output_tokens"));
|
||||
long total = numberValue(view.get("total_tokens")) == null ? input + output : numberValue(view.get("total_tokens"));
|
||||
BigDecimal rowCost = decimalValue(view.get("estimated_cost_usd"));
|
||||
inputTokens += input;
|
||||
outputTokens += output;
|
||||
totalTokens += total;
|
||||
cost = cost.add(rowCost);
|
||||
requestCount += 1;
|
||||
estimated = estimated || Boolean.TRUE.equals(view.get("estimated")) || rowCost.compareTo(BigDecimal.ZERO) == 0;
|
||||
sourceRecords.add(record.getId());
|
||||
if (record.getSyncedAt() != null && (updatedAt == null || record.getSyncedAt().after(updatedAt))) updatedAt = record.getSyncedAt();
|
||||
String model = firstString(view.get("model"), "unknown");
|
||||
byModel.computeIfAbsent(model, ignored -> new ModelAccumulator()).add(input, output, total, rowCost);
|
||||
}
|
||||
Map<String, DigitalEmployeeBillingEstimateDto.ModelEstimate> modelEstimates = new LinkedHashMap<>();
|
||||
byModel.forEach((model, accumulator) -> modelEstimates.put(model, accumulator.toDto()));
|
||||
return DigitalEmployeeBillingEstimateDto.builder()
|
||||
.totalTokens(totalTokens)
|
||||
.inputTokens(inputTokens)
|
||||
.outputTokens(outputTokens)
|
||||
.estimatedCostUsd(cost)
|
||||
.requestCount(requestCount)
|
||||
.estimated(estimated || requestCount == 0)
|
||||
.byModel(modelEstimates)
|
||||
.updatedAt(updatedAt)
|
||||
.sourceRecords(sourceRecords)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
@DSTransactional
|
||||
public DigitalEmployeeAdminRecordResultDto recordAuditArchive(DigitalEmployeeAdminRecordRequestDto request) {
|
||||
return recordAdminObservation(enrichAdminRecordRequestFromSyncRecord(request), "audit", "audit_archive_recorded", "audit_archive_recorded");
|
||||
}
|
||||
|
||||
private DigitalEmployeeAdminRecordResultDto recordAdminObservation(
|
||||
DigitalEmployeeAdminRecordRequestDto request,
|
||||
String defaultEntityType,
|
||||
String action,
|
||||
String reasonCode
|
||||
) {
|
||||
if (request == null) return DigitalEmployeeAdminRecordResultDto.builder().ok(false).status("rejected").reasonCodes(List.of("request_required")).build();
|
||||
String clientKey = StringUtils.trimToNull(request.getClientKey());
|
||||
String entityType = firstString(normalizedText(request.getEntityType()), defaultEntityType);
|
||||
String entityId = StringUtils.trimToNull(request.getEntityId());
|
||||
if (StringUtils.isBlank(clientKey) || StringUtils.isBlank(entityType) || StringUtils.isBlank(entityId)) {
|
||||
return DigitalEmployeeAdminRecordResultDto.builder().ok(false).status("rejected").reasonCodes(List.of("required_field_missing")).build();
|
||||
}
|
||||
Date now = new Date();
|
||||
Map<String, Object> payload = new LinkedHashMap<>();
|
||||
payload.put("status", firstString(request.getStatus(), "recorded"));
|
||||
payload.put("endpoint", request.getEndpoint());
|
||||
payload.put("remote_id", request.getRemoteId());
|
||||
payload.put("archive_location", request.getArchiveLocation());
|
||||
payload.put("archive_summary", sanitizeAdminRecordValue(request.getArchiveSummary()));
|
||||
payload.put("reason", request.getReason());
|
||||
payload.put("request_payload", sanitizeAdminRecordValue(request.getRequestPayload()));
|
||||
DigitalEmployeeAdminAction record = new DigitalEmployeeAdminAction();
|
||||
record.setTenantId(currentTenantId());
|
||||
record.setUserId(currentUserId());
|
||||
record.setClientKey(clientKey);
|
||||
record.setDeviceId(StringUtils.trimToNull(request.getDeviceId()));
|
||||
record.setEntityType(entityType);
|
||||
record.setEntityId(entityId);
|
||||
record.setAction(action);
|
||||
record.setStatus("recorded");
|
||||
record.setReasonCodes(toJson(List.of(reasonCode)));
|
||||
record.setRequestPayload(toJson(payload));
|
||||
record.setProcessedAt(now);
|
||||
record.setCreated(now);
|
||||
record.setModified(now);
|
||||
digitalEmployeeAdminActionRepository.save(record);
|
||||
return DigitalEmployeeAdminRecordResultDto.builder()
|
||||
.ok(true)
|
||||
.status("recorded")
|
||||
.reasonCodes(List.of(reasonCode))
|
||||
.record(toAdminActionDto(record))
|
||||
.build();
|
||||
}
|
||||
|
||||
private DigitalEmployeeAdminRecordRequestDto enrichAdminRecordRequestFromSyncRecord(DigitalEmployeeAdminRecordRequestDto request) {
|
||||
if (request == null || request.getSyncRecordId() == null || StringUtils.isNotBlank(request.getClientKey())) {
|
||||
return request;
|
||||
}
|
||||
DigitalEmployeeSyncRecord record = digitalEmployeeSyncRecordRepository.getById(request.getSyncRecordId());
|
||||
if (record == null || currentTenantId() == null || !Objects.equals(record.getTenantId(), currentTenantId())) {
|
||||
return request;
|
||||
}
|
||||
request.setClientKey(record.getClientKey());
|
||||
if (StringUtils.isBlank(request.getDeviceId())) request.setDeviceId(record.getDeviceId());
|
||||
if (StringUtils.isBlank(request.getEntityType())) request.setEntityType(record.getEntityType());
|
||||
if (StringUtils.isBlank(request.getEntityId())) request.setEntityId(record.getEntityId());
|
||||
return request;
|
||||
}
|
||||
|
||||
private BigDecimal decimalValue(Object value) {
|
||||
if (value == null) return BigDecimal.ZERO;
|
||||
if (value instanceof BigDecimal decimal) return decimal;
|
||||
if (value instanceof Number number) return BigDecimal.valueOf(number.doubleValue());
|
||||
try {
|
||||
return new BigDecimal(String.valueOf(value));
|
||||
} catch (Exception ignored) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
}
|
||||
|
||||
private static class ModelAccumulator {
|
||||
private long inputTokens;
|
||||
private long outputTokens;
|
||||
private long totalTokens;
|
||||
private long requestCount;
|
||||
private BigDecimal estimatedCostUsd = BigDecimal.ZERO;
|
||||
|
||||
void add(long input, long output, long total, BigDecimal cost) {
|
||||
inputTokens += input;
|
||||
outputTokens += output;
|
||||
totalTokens += total;
|
||||
requestCount += 1;
|
||||
estimatedCostUsd = estimatedCostUsd.add(cost);
|
||||
}
|
||||
|
||||
DigitalEmployeeBillingEstimateDto.ModelEstimate toDto() {
|
||||
return DigitalEmployeeBillingEstimateDto.ModelEstimate.builder()
|
||||
.inputTokens(inputTokens)
|
||||
.outputTokens(outputTokens)
|
||||
.totalTokens(totalTokens)
|
||||
.requestCount(requestCount)
|
||||
.estimatedCostUsd(estimatedCostUsd)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> validateAdminAction(DigitalEmployeeAdminActionRequestDto request) {
|
||||
if (request == null) return List.of("request_required");
|
||||
String clientKey = StringUtils.trimToNull(request.getClientKey());
|
||||
@@ -899,6 +1081,23 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
|
||||
return Set.of("token", "authorization", "raw_input", "command", "prompt", "raw_prompt", "input").contains(normalized);
|
||||
}
|
||||
|
||||
private Object sanitizeAdminRecordValue(Object value) {
|
||||
if (value instanceof Map<?, ?> map) {
|
||||
Map<String, Object> sanitized = new LinkedHashMap<>();
|
||||
map.forEach((key, nestedValue) -> {
|
||||
String textKey = String.valueOf(key);
|
||||
if (!isSensitivePayloadKey(textKey)) {
|
||||
sanitized.put(textKey, sanitizeAdminRecordValue(nestedValue));
|
||||
}
|
||||
});
|
||||
return sanitized;
|
||||
}
|
||||
if (value instanceof List<?> list) {
|
||||
return list.stream().map(this::sanitizeAdminRecordValue).toList();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private List<DigitalEmployeeBusinessViewRowDto> rowsByType(List<DigitalEmployeeBusinessViewRowDto> rows, String entityType) {
|
||||
return rows.stream().filter(row -> StringUtils.equals(row.getEntityType(), entityType)).toList();
|
||||
}
|
||||
|
||||
@@ -11,6 +11,9 @@ import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminActionDt
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminActionPageDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminActionRequestDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminActionResultDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminRecordRequestDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminRecordResultDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeBillingEstimateDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeePlanBusinessViewDetailDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeePolicySnapshotPageDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeePolicySnapshotRowDto;
|
||||
@@ -416,6 +419,100 @@ class DigitalEmployeeSyncApplicationServiceImplTest {
|
||||
assertThat(row.getRecommendedResolution()).isEqualTo("review_latest_policy_snapshot");
|
||||
}
|
||||
|
||||
@Test
|
||||
void recordDailyReportSendResultStoresRecordedObservation() {
|
||||
DigitalEmployeeAdminRecordRequestDto request = adminRecordRequest("daily_report", "report-1");
|
||||
request.setStatus("sent");
|
||||
request.setEndpoint("email");
|
||||
request.setRemoteId("mail-1");
|
||||
request.setReason("operator_recorded_delivery");
|
||||
|
||||
DigitalEmployeeAdminRecordResultDto result = service.recordDailyReportSendResult(request);
|
||||
|
||||
assertThat(result.getOk()).isTrue();
|
||||
assertThat(result.getStatus()).isEqualTo("recorded");
|
||||
assertThat(result.getReasonCodes()).contains("send_record_recorded");
|
||||
ArgumentCaptor<DigitalEmployeeAdminAction> captor = ArgumentCaptor.forClass(DigitalEmployeeAdminAction.class);
|
||||
verify(adminActionRepository).save(captor.capture());
|
||||
DigitalEmployeeAdminAction record = captor.getValue();
|
||||
assertThat(record.getEntityType()).isEqualTo("daily_report");
|
||||
assertThat(record.getEntityId()).isEqualTo("report-1");
|
||||
assertThat(record.getAction()).isEqualTo("daily_report_send_recorded");
|
||||
assertThat(record.getStatus()).isEqualTo("recorded");
|
||||
assertThat(record.getRequestPayload()).contains("mail-1").contains("operator_recorded_delivery");
|
||||
}
|
||||
|
||||
@Test
|
||||
void queryBillingEstimatesAggregatesTokenCostByModel() {
|
||||
when(repository.queryBusinessRecords(eq(101L), eq("client-key-001"), eq("device-001"), eq("event"), eq(null), eq(null), eq(null), eq(null), eq(1L), eq(100L)))
|
||||
.thenReturn(page(List.of(
|
||||
record("outbox-cost-1", "event", "event-cost-1", 101L, new Date(10_000),
|
||||
Map.of(
|
||||
"entity_type", "event",
|
||||
"category", "token_cost",
|
||||
"model", "gpt-test",
|
||||
"input_tokens", 100,
|
||||
"output_tokens", 50,
|
||||
"total_tokens", 150,
|
||||
"estimated_cost_usd", "0.012",
|
||||
"estimated", false
|
||||
),
|
||||
Map.of("id", "event-cost-1")),
|
||||
record("outbox-cost-2", "event", "event-cost-2", 101L, new Date(11_000),
|
||||
Map.of(
|
||||
"entity_type", "event",
|
||||
"category", "token_cost",
|
||||
"model", "gpt-test",
|
||||
"input_tokens", 20,
|
||||
"output_tokens", 10,
|
||||
"total_tokens", 30,
|
||||
"estimated_cost_usd", "0.003",
|
||||
"estimated", true
|
||||
),
|
||||
Map.of("id", "event-cost-2")),
|
||||
eventRecord("event-audit", "plan-1", "task-1", "run-1", "tool_call_audit", "工具调用", "allowed", 101L, new Date(12_000))
|
||||
)));
|
||||
|
||||
DigitalEmployeeBillingEstimateDto result = service.queryBillingEstimates("client-key-001", "device-001", null, null);
|
||||
|
||||
assertThat(result.getInputTokens()).isEqualTo(120L);
|
||||
assertThat(result.getOutputTokens()).isEqualTo(60L);
|
||||
assertThat(result.getTotalTokens()).isEqualTo(180L);
|
||||
assertThat(result.getEstimatedCostUsd()).isEqualByComparingTo("0.015");
|
||||
assertThat(result.getRequestCount()).isEqualTo(2L);
|
||||
assertThat(result.getEstimated()).isTrue();
|
||||
assertThat(result.getByModel()).containsKey("gpt-test");
|
||||
assertThat(result.getByModel().get("gpt-test").getTotalTokens()).isEqualTo(180L);
|
||||
assertThat(result.getSourceRecords()).hasSize(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void recordAuditArchiveRedactsSensitiveSummaryAndPayload() {
|
||||
DigitalEmployeeAdminRecordRequestDto request = adminRecordRequest("audit", "audit-1");
|
||||
request.setArchiveLocation("s3://audit/safe-summary.json");
|
||||
request.setArchiveSummary(Map.<String, Object>of(
|
||||
"status", "archived",
|
||||
"token", "secret-token",
|
||||
"nested", Map.<String, Object>of("authorization", "Bearer secret", "status", "ok")
|
||||
));
|
||||
request.setRequestPayload(Map.<String, Object>of(
|
||||
"raw_input", "full prompt",
|
||||
"prompt", "sensitive prompt",
|
||||
"safe_reason", "sandbox archived"
|
||||
));
|
||||
|
||||
DigitalEmployeeAdminRecordResultDto result = service.recordAuditArchive(request);
|
||||
|
||||
assertThat(result.getOk()).isTrue();
|
||||
ArgumentCaptor<DigitalEmployeeAdminAction> captor = ArgumentCaptor.forClass(DigitalEmployeeAdminAction.class);
|
||||
verify(adminActionRepository).save(captor.capture());
|
||||
DigitalEmployeeAdminAction record = captor.getValue();
|
||||
assertThat(record.getAction()).isEqualTo("audit_archive_recorded");
|
||||
assertThat(record.getRequestPayload()).contains("s3://audit/safe-summary.json");
|
||||
assertThat(record.getRequestPayload()).contains("sandbox archived");
|
||||
assertThat(record.getRequestPayload()).doesNotContain("secret-token", "Bearer secret", "full prompt", "sensitive prompt");
|
||||
}
|
||||
|
||||
private IPage<DigitalEmployeeSyncRecord> page(List<DigitalEmployeeSyncRecord> records) {
|
||||
Page<DigitalEmployeeSyncRecord> page = new Page<>(1, 20, records.size());
|
||||
page.setRecords(records);
|
||||
@@ -456,6 +553,15 @@ class DigitalEmployeeSyncApplicationServiceImplTest {
|
||||
return record;
|
||||
}
|
||||
|
||||
private DigitalEmployeeAdminRecordRequestDto adminRecordRequest(String entityType, String entityId) {
|
||||
DigitalEmployeeAdminRecordRequestDto request = new DigitalEmployeeAdminRecordRequestDto();
|
||||
request.setClientKey("client-key-001");
|
||||
request.setDeviceId("device-001");
|
||||
request.setEntityType(entityType);
|
||||
request.setEntityId(entityId);
|
||||
return request;
|
||||
}
|
||||
|
||||
private DigitalEmployeeBusinessFact businessFact(String planId, String title, String status, Date syncedAt) {
|
||||
DigitalEmployeeBusinessFact fact = new DigitalEmployeeBusinessFact();
|
||||
fact.setId(7001L);
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.xspaceagi.agent.web.ui.controller;
|
||||
|
||||
import com.xspaceagi.agent.core.adapter.application.DigitalEmployeeSyncApplicationService;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminRecordRequestDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminRecordResultDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeBillingEstimateDto;
|
||||
import com.xspaceagi.system.spec.annotation.RequireResource;
|
||||
import com.xspaceagi.system.spec.dto.ReqResult;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Date;
|
||||
|
||||
import static com.xspaceagi.system.spec.enums.ResourceEnum.CONTENT_DIGITAL_EMPLOYEE_OPS_QUERY_LIST;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/digital-employee/admin")
|
||||
public class DigitalEmployeeAdminRecordController {
|
||||
|
||||
@Resource
|
||||
private DigitalEmployeeSyncApplicationService digitalEmployeeSyncApplicationService;
|
||||
|
||||
@RequireResource(CONTENT_DIGITAL_EMPLOYEE_OPS_QUERY_LIST)
|
||||
@RequestMapping(path = "/daily-reports/send-records", method = RequestMethod.POST)
|
||||
public ReqResult<DigitalEmployeeAdminRecordResultDto> recordDailyReportSendResult(
|
||||
@RequestBody(required = false) DigitalEmployeeAdminRecordRequestDto request
|
||||
) {
|
||||
return ReqResult.success(digitalEmployeeSyncApplicationService.recordDailyReportSendResult(request));
|
||||
}
|
||||
|
||||
@RequireResource(CONTENT_DIGITAL_EMPLOYEE_OPS_QUERY_LIST)
|
||||
@RequestMapping(path = "/billing/estimates", method = RequestMethod.GET)
|
||||
public ReqResult<DigitalEmployeeBillingEstimateDto> queryBillingEstimates(
|
||||
@RequestParam(name = "client_key", required = false) String clientKey,
|
||||
@RequestParam(name = "device_id", required = false) String deviceId,
|
||||
@RequestParam(name = "updated_from", required = false) String updatedFrom,
|
||||
@RequestParam(name = "updated_to", required = false) String updatedTo
|
||||
) {
|
||||
return ReqResult.success(digitalEmployeeSyncApplicationService.queryBillingEstimates(
|
||||
clientKey,
|
||||
deviceId,
|
||||
parseDate(updatedFrom),
|
||||
parseDate(updatedTo)
|
||||
));
|
||||
}
|
||||
|
||||
@RequireResource(CONTENT_DIGITAL_EMPLOYEE_OPS_QUERY_LIST)
|
||||
@RequestMapping(path = "/audit-archives", method = RequestMethod.POST)
|
||||
public ReqResult<DigitalEmployeeAdminRecordResultDto> recordAuditArchive(
|
||||
@RequestBody(required = false) DigitalEmployeeAdminRecordRequestDto request
|
||||
) {
|
||||
return ReqResult.success(digitalEmployeeSyncApplicationService.recordAuditArchive(request));
|
||||
}
|
||||
|
||||
private Date parseDate(String value) {
|
||||
if (StringUtils.isBlank(value)) return null;
|
||||
String trimmed = value.trim();
|
||||
try {
|
||||
return Date.from(Instant.parse(trimmed));
|
||||
} catch (Exception ignored) {
|
||||
try {
|
||||
LocalDateTime localDateTime = LocalDateTime.parse(trimmed, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
|
||||
return Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
|
||||
} catch (Exception ignoredAgain) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user