fix(aihr): mask sensitive case transcripts
This commit is contained in:
+17
-4
@@ -48,6 +48,7 @@ public class AihrCaseService {
|
|||||||
try {
|
try {
|
||||||
String transcript = speechService.transcribe(file.getBytes(), fileName, contentType)
|
String transcript = speechService.transcribe(file.getBytes(), fileName, contentType)
|
||||||
.orElseThrow(() -> new IllegalStateException("ASR 未配置或转写失败"));
|
.orElseThrow(() -> new IllegalStateException("ASR 未配置或转写失败"));
|
||||||
|
transcript = maskSensitiveText(transcript);
|
||||||
String caseId = "case-" + UUID.randomUUID();
|
String caseId = "case-" + UUID.randomUUID();
|
||||||
CaseState state = new CaseState(caseId, fileName, projectExtOrgId, transcript, tagsFromText(transcript), null);
|
CaseState state = new CaseState(caseId, fileName, projectExtOrgId, transcript, tagsFromText(transcript), null);
|
||||||
cases.put(caseId, state);
|
cases.put(caseId, state);
|
||||||
@@ -127,8 +128,8 @@ public class AihrCaseService {
|
|||||||
truncate(title, 120),
|
truncate(title, 120),
|
||||||
primaryTag(state.tags()),
|
primaryTag(state.tags()),
|
||||||
status,
|
status,
|
||||||
truncate(state.transcript(), 4000),
|
truncate(maskSensitiveText(state.transcript()), 4000),
|
||||||
truncate(summary, 1000)
|
truncate(maskSensitiveText(summary), 1000)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,7 +184,7 @@ public class AihrCaseService {
|
|||||||
if (summaryNode.isArray()) {
|
if (summaryNode.isArray()) {
|
||||||
for (JsonNode item : summaryNode) {
|
for (JsonNode item : summaryNode) {
|
||||||
String label = clean(item.path("label").asText(""));
|
String label = clean(item.path("label").asText(""));
|
||||||
String text = clean(item.path("text").asText(""));
|
String text = maskSensitiveText(item.path("text").asText(""));
|
||||||
if (!label.isBlank() && !text.isBlank()) {
|
if (!label.isBlank() && !text.isBlank()) {
|
||||||
summary.add(new SummaryResponse(label, truncate(text, 120)));
|
summary.add(new SummaryResponse(label, truncate(text, 120)));
|
||||||
}
|
}
|
||||||
@@ -205,7 +206,7 @@ public class AihrCaseService {
|
|||||||
if (tags.isEmpty()) {
|
if (tags.isEmpty()) {
|
||||||
tags = tagsFromText(root.path("aiSummary").asText(""));
|
tags = tagsFromText(root.path("aiSummary").asText(""));
|
||||||
}
|
}
|
||||||
return Optional.of(new CaseSummary(summary, tags, truncate(root.path("aiSummary").asText(""), 180), "real-llm"));
|
return Optional.of(new CaseSummary(summary, tags, truncate(maskSensitiveText(root.path("aiSummary").asText("")), 180), "real-llm"));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("case summary parse failed, uses local transcript summary: {}", e.getMessage());
|
log.warn("case summary parse failed, uses local transcript summary: {}", e.getMessage());
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
@@ -294,6 +295,18 @@ public class AihrCaseService {
|
|||||||
return value == null ? "" : value.trim();
|
return value == null ? "" : value.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String maskSensitiveText(String text) {
|
||||||
|
if (text == null || text.isBlank()) {
|
||||||
|
return clean(text);
|
||||||
|
}
|
||||||
|
String masked = text;
|
||||||
|
masked = masked.replaceAll("(?<!\\d)(1[3-9]\\d{2})\\d{3}(\\d{4})(?!\\d)", "$1****$2");
|
||||||
|
masked = masked.replaceAll("(?<!\\d)(\\d{1,2})(栋|幢|号楼|单元)(\\d{3,4})(?!\\d)", "$1$2****");
|
||||||
|
masked = masked.replaceAll("(?<!\\d)(\\d{1,2})[--](\\d{3,4})(?!\\d)", "$1-****");
|
||||||
|
masked = masked.replaceAll("([\\u4e00-\\u9fa5])([\\u4e00-\\u9fa5])(先生|女士|经理)", "$1*$3");
|
||||||
|
return clean(masked);
|
||||||
|
}
|
||||||
|
|
||||||
private record CaseState(
|
private record CaseState(
|
||||||
String id,
|
String id,
|
||||||
String fileName,
|
String fileName,
|
||||||
|
|||||||
+53
@@ -0,0 +1,53 @@
|
|||||||
|
package org.dromara.aihr.service;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import org.dromara.aihr.domain.AihrCaseDto.UploadResponse;
|
||||||
|
import org.junit.jupiter.api.Tag;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
import org.springframework.mock.web.MockMultipartFile;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
|
||||||
|
class AihrCaseServiceTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Tag("dev")
|
||||||
|
void uploadMasksSensitiveTranscriptBeforeReturningAndSaving() {
|
||||||
|
AihrSpeechService speechService = new AihrSpeechService(null, new ObjectMapper()) {
|
||||||
|
@Override
|
||||||
|
public Optional<String> transcribe(byte[] audio, String filename, String contentType) {
|
||||||
|
return Optional.of("张三先生住在12栋1201,手机号13900001111,说2-304漏水");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
CaseJdbcTemplate jdbcTemplate = new CaseJdbcTemplate();
|
||||||
|
AihrCaseService service = new AihrCaseService(speechService, null, new ObjectMapper(), jdbcTemplate);
|
||||||
|
|
||||||
|
UploadResponse response = service.upload(new MockMultipartFile("file", "case.webm", "audio/webm", new byte[]{1}), "P1");
|
||||||
|
|
||||||
|
assertEquals("张*先生住在12栋****,手机号1390****1111,说2-****漏水", response.transcript());
|
||||||
|
assertEquals(response.transcript(), jdbcTemplate.updateArgs[6]);
|
||||||
|
assertEquals(response.transcript(), jdbcTemplate.updateArgs[7]);
|
||||||
|
assertFalse(response.transcript().contains("13900001111"));
|
||||||
|
assertFalse(response.transcript().contains("1201"));
|
||||||
|
assertFalse(response.transcript().contains("2-304"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class CaseJdbcTemplate extends JdbcTemplate {
|
||||||
|
private Object[] updateArgs = new Object[0];
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(String sql) {
|
||||||
|
// Table DDL is not relevant for this unit test.
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int update(String sql, Object... args) {
|
||||||
|
this.updateArgs = args;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user