fix(aihr): tighten upload and rag evidence boundaries
This commit is contained in:
+23
-1
@@ -16,6 +16,8 @@ import java.sql.Statement;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@@ -24,6 +26,9 @@ public class AihrCandidateMaterialService {
|
||||
private static final String TENANT_ID = "000000";
|
||||
private static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ofPattern("MM-dd HH:mm");
|
||||
private static final long MAX_SIZE = 20L * 1024 * 1024;
|
||||
private static final Set<String> SUPPORTED_MATERIAL_EXTENSIONS = Set.of(
|
||||
".pdf", ".doc", ".docx", ".jpg", ".jpeg", ".png", ".webp"
|
||||
);
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
private final ISysOssService ossService;
|
||||
@@ -39,7 +44,10 @@ public class AihrCandidateMaterialService {
|
||||
throw new IllegalArgumentException("资料不能超过20MB");
|
||||
}
|
||||
String boundCandidateId = requireCandidateId(candidateId);
|
||||
String fileName = clean(file.getOriginalFilename(), "candidate-material");
|
||||
String fileName = sanitizeMaterialFileName(file.getOriginalFilename());
|
||||
if (!isSupportedMaterialFile(fileName)) {
|
||||
throw new IllegalArgumentException("候选人资料仅支持 PDF、Word 或 JPG/PNG/WebP 图片");
|
||||
}
|
||||
String type = clean(materialType, materialType(fileName));
|
||||
SysOssVo oss = ossService.upload(file);
|
||||
KeyHolder keyHolder = new GeneratedKeyHolder();
|
||||
@@ -205,6 +213,20 @@ public class AihrCandidateMaterialService {
|
||||
return text;
|
||||
}
|
||||
|
||||
static boolean isSupportedMaterialFile(String fileName) {
|
||||
String lower = clean(fileName, "").toLowerCase(Locale.ROOT);
|
||||
return SUPPORTED_MATERIAL_EXTENSIONS.stream().anyMatch(lower::endsWith);
|
||||
}
|
||||
|
||||
static String sanitizeMaterialFileName(String original) {
|
||||
String name = clean(original, "candidate-material");
|
||||
name = name.replaceAll("[\\\\/\\r\\n\\t]", "_");
|
||||
if (name.length() > 200) {
|
||||
name = name.substring(name.length() - 200);
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
private static String normalizeReviewStatus(String status) {
|
||||
String text = clean(status, "待审核");
|
||||
if ("已通过".equals(text) || "已驳回".equals(text) || "待审核".equals(text)) {
|
||||
|
||||
+23
-1
@@ -26,8 +26,10 @@ import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -38,6 +40,9 @@ import java.util.stream.Collectors;
|
||||
public class AihrCaseService {
|
||||
|
||||
private static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ofPattern("MM-dd HH:mm");
|
||||
private static final Set<String> SUPPORTED_AUDIO_EXTENSIONS = Set.of(
|
||||
".mp3", ".wav", ".m4a", ".webm", ".ogg", ".aac", ".flac"
|
||||
);
|
||||
|
||||
private final AihrSpeechService speechService;
|
||||
private final AihrModelSeedService modelService;
|
||||
@@ -51,8 +56,11 @@ public class AihrCaseService {
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new IllegalArgumentException("请上传语音文件");
|
||||
}
|
||||
String fileName = sanitizeAudioFileName(file.getOriginalFilename());
|
||||
if (!isSupportedAudioFile(fileName)) {
|
||||
throw new IllegalArgumentException("案例素材仅支持 MP3、WAV、M4A、WebM、OGG、AAC 或 FLAC 音频");
|
||||
}
|
||||
String scopedProject = resolveUploadProject(projectExtOrgId, projectScopes);
|
||||
String fileName = firstNonBlank(file.getOriginalFilename(), "case-audio.webm");
|
||||
String contentType = firstNonBlank(file.getContentType(), "application/octet-stream");
|
||||
try {
|
||||
String transcript = speechService.transcribe(file.getBytes(), fileName, contentType)
|
||||
@@ -527,6 +535,20 @@ public class AihrCaseService {
|
||||
return clean(AihrSensitiveText.forModel(text));
|
||||
}
|
||||
|
||||
static boolean isSupportedAudioFile(String fileName) {
|
||||
String lower = clean(fileName).toLowerCase(Locale.ROOT);
|
||||
return SUPPORTED_AUDIO_EXTENSIONS.stream().anyMatch(lower::endsWith);
|
||||
}
|
||||
|
||||
static String sanitizeAudioFileName(String original) {
|
||||
String name = firstNonBlank(original, "case-audio.webm");
|
||||
name = name.replaceAll("[\\\\/\\r\\n\\t]", "_");
|
||||
if (name.length() > 200) {
|
||||
name = name.substring(name.length() - 200);
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
private record CaseState(
|
||||
String id,
|
||||
String fileName,
|
||||
|
||||
+4
-1
@@ -1832,7 +1832,7 @@ public class AihrSopSeedService {
|
||||
}
|
||||
|
||||
private SearchResponse withReviewId(SearchResponse response, String source) {
|
||||
Long reviewId = skipSopReview(source) ? null : createSopReview(response, source);
|
||||
Long reviewId = shouldCreateSopReview(response.queryText(), source) ? createSopReview(response, source) : null;
|
||||
return new SearchResponse(
|
||||
response.queryText(),
|
||||
response.category(),
|
||||
@@ -1853,6 +1853,9 @@ public class AihrSopSeedService {
|
||||
return "verify_demo_questions".equals(source);
|
||||
}
|
||||
|
||||
static boolean shouldCreateSopReview(String queryText, String source) {
|
||||
return !isBlank(queryText) && !skipSopReview(source);
|
||||
}
|
||||
private Long createSopReview(SearchResponse response, String source) {
|
||||
try {
|
||||
ensureSopReviewTable();
|
||||
|
||||
+18
@@ -5,7 +5,9 @@ import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class AihrCandidateMaterialServiceTest {
|
||||
|
||||
@@ -16,4 +18,20 @@ class AihrCandidateMaterialServiceTest {
|
||||
assertThrows(ServiceException.class, () -> AihrCandidateMaterialService.requireCandidateId(""));
|
||||
assertThrows(ServiceException.class, () -> AihrCandidateMaterialService.requireCandidateId(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Tag("dev")
|
||||
void candidateMaterialFileMustUseTheSameAllowlistAsTheMobilePicker() {
|
||||
assertTrue(AihrCandidateMaterialService.isSupportedMaterialFile("resume.PDF"));
|
||||
assertTrue(AihrCandidateMaterialService.isSupportedMaterialFile("certificate.webp"));
|
||||
assertFalse(AihrCandidateMaterialService.isSupportedMaterialFile("payload.exe"));
|
||||
assertFalse(AihrCandidateMaterialService.isSupportedMaterialFile("archive.zip"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Tag("dev")
|
||||
void candidateMaterialFileNameCannotCarryPathOrControlCharacters() {
|
||||
assertEquals(".._.._candidate.pdf", AihrCandidateMaterialService.sanitizeMaterialFileName("../../candidate.pdf"));
|
||||
assertEquals("candidate_ name.pdf", AihrCandidateMaterialService.sanitizeMaterialFileName("candidate\t name.pdf"));
|
||||
}
|
||||
}
|
||||
|
||||
+10
@@ -129,6 +129,16 @@ class AihrCaseServiceTest {
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Tag("dev")
|
||||
void caseAudioAllowlistRejectsNonAudioExtensions() {
|
||||
assertTrue(AihrCaseService.isSupportedAudioFile("现场录音.WEBM"));
|
||||
assertTrue(AihrCaseService.isSupportedAudioFile("follow-up.m4a"));
|
||||
assertFalse(AihrCaseService.isSupportedAudioFile("payload.exe"));
|
||||
assertFalse(AihrCaseService.isSupportedAudioFile("notes.pdf"));
|
||||
assertEquals(".._.._case.webm", AihrCaseService.sanitizeAudioFileName("../../case.webm"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Tag("dev")
|
||||
void caseWriteControllerRequiresHrRoleForSystemUsers() throws Exception {
|
||||
|
||||
+8
@@ -36,6 +36,14 @@ public class AihrSopSeedServiceTest {
|
||||
assertFalse(AihrSopSeedService.skipSopReview("knowledge_search"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Tag("dev")
|
||||
public void emptySopQuestionCannotCreateReviewSample() {
|
||||
assertFalse(AihrSopSeedService.shouldCreateSopReview(" ", "knowledge_search"));
|
||||
assertTrue(AihrSopSeedService.shouldCreateSopReview("物业费怎么交", "knowledge_search"));
|
||||
assertFalse(AihrSopSeedService.shouldCreateSopReview("物业费怎么交", "verify_demo_questions"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Tag("dev")
|
||||
public void evidenceHitsKeepsNamedDocumentOnly() {
|
||||
|
||||
Reference in New Issue
Block a user