fix(aihr): close practice audio URL exposure
This commit is contained in:
+6
-2
@@ -75,7 +75,9 @@ public class AihrSpeechController {
|
||||
return R.ok(new AsrResponse(text, "openai-compatible"));
|
||||
}
|
||||
}
|
||||
return R.ok(new AsrResponse(text, "openai-compatible", oss.getOssId(), oss.getUrl()));
|
||||
// The OSS URL is intentionally not exposed. Playback goes through the
|
||||
// authenticated /api/aihr/mobile/oss/{ossId} endpoint after binding.
|
||||
return R.ok(new AsrResponse(text, "openai-compatible", oss.getOssId(), ""));
|
||||
} catch (Exception e) {
|
||||
log.warn("asr audio upload failed(处理错误已隐藏)");
|
||||
return R.ok(new AsrResponse(text, "openai-compatible"));
|
||||
@@ -135,7 +137,9 @@ public class AihrSpeechController {
|
||||
Files.write(tempFile, audio);
|
||||
File file = tempFile.toFile();
|
||||
SysOssVo oss = ossService.upload(file);
|
||||
return R.ok(new TtsResponse(oss.getUrl(), "openai-compatible", oss.getOssId(), ""));
|
||||
// Keep the old client contract playable without returning a public OSS URL.
|
||||
String inlineAudioUrl = "data:audio/mp3;base64," + Base64.getEncoder().encodeToString(audio);
|
||||
return R.ok(new TtsResponse(inlineAudioUrl, "openai-compatible", oss.getOssId(), inlineAudioUrl));
|
||||
} catch (Exception e) {
|
||||
log.warn("tts audio upload failed(处理错误已隐藏)");
|
||||
String inlineAudioUrl = "data:audio/mp3;base64," + Base64.getEncoder().encodeToString(audio);
|
||||
|
||||
+19
-7
@@ -2290,7 +2290,7 @@ public class AihrPracticeSeedService {
|
||||
}
|
||||
DailyDrillParts parts = dailyDrillParts((String) rows.get(0).get("reason"), (String) rows.get(0).get("scenario_id"));
|
||||
String answer = maskSensitiveText(firstNonBlank(request == null ? null : request.answer(), ""));
|
||||
String audioUrl = firstNonBlank(request == null ? null : request.audioUrl(), "");
|
||||
String audioUrl = exposedAudioUrl(request == null ? null : request.audioUrl());
|
||||
Long audioOssId = request == null ? null : request.audioOssId();
|
||||
validateAudioOwnership(audioOssId, extPartyId, ownerUserId);
|
||||
DailyDrillScore score = scoreDailyDrill(parts.question(), answer, parts.referenceAnswer());
|
||||
@@ -2327,7 +2327,7 @@ public class AihrPracticeSeedService {
|
||||
return null;
|
||||
}
|
||||
bindPracticeAudio(audioOssId);
|
||||
return new DailyDrillAnswerResponse(id, "已完成", score.correct(), score.score(), score.feedback(), parts.referenceAnswer(), score.mode(), audioUrl, audioOssId);
|
||||
return new DailyDrillAnswerResponse(id, "已完成", score.correct(), score.score(), score.feedback(), parts.referenceAnswer(), score.mode(), exposedAudioUrl(audioUrl), audioOssId);
|
||||
}
|
||||
|
||||
public List<PromptTemplateResponse> promptTemplates(String category, String code, Boolean enabled) {
|
||||
@@ -2933,7 +2933,7 @@ public class AihrPracticeSeedService {
|
||||
correctValue == null ? null : rs.getBoolean("correct"),
|
||||
rs.getObject("score") == null ? null : rs.getInt("score"),
|
||||
firstNonBlank(rs.getString("score_mode"), "RULE_FALLBACK"),
|
||||
rs.getString("answer_audio_url"),
|
||||
exposedAudioUrl(rs.getString("answer_audio_url")),
|
||||
rs.getObject("answer_audio_oss_id") == null ? null : rs.getLong("answer_audio_oss_id")
|
||||
);
|
||||
}
|
||||
@@ -3267,7 +3267,7 @@ public class AihrPracticeSeedService {
|
||||
while (audioUrls.size() <= roundIndex) {
|
||||
audioUrls.add("");
|
||||
}
|
||||
audioUrls.set(roundIndex, firstNonBlank(request.traineeAudioUrl(), ""));
|
||||
audioUrls.set(roundIndex, exposedAudioUrl(request.traineeAudioUrl()));
|
||||
}
|
||||
List<Long> audioOssIds = session.traineeAudioOssIds();
|
||||
synchronized (audioOssIds) {
|
||||
@@ -3631,7 +3631,7 @@ public class AihrPracticeSeedService {
|
||||
item.path("role").asText(),
|
||||
item.path("label").asText(),
|
||||
item.path("text").asText(),
|
||||
item.path("audioUrl").asText(""),
|
||||
exposedAudioUrl(item.path("audioUrl").asText("")),
|
||||
item.hasNonNull("audioOssId") ? item.path("audioOssId").asLong() : null,
|
||||
item.hasNonNull("emotion") ? item.path("emotion").asInt() : null,
|
||||
item.hasNonNull("trust") ? item.path("trust").asInt() : null,
|
||||
@@ -3667,7 +3667,7 @@ public class AihrPracticeSeedService {
|
||||
"trainee",
|
||||
"员工话术",
|
||||
replies.get(i),
|
||||
i < audioUrls.size() ? audioUrls.get(i) : "",
|
||||
i < audioUrls.size() ? exposedAudioUrl(audioUrls.get(i)) : "",
|
||||
i < audioOssIds.size() ? audioOssIds.get(i) : null,
|
||||
evidence == null ? null : evidence.emotion(),
|
||||
evidence == null ? null : evidence.trust(),
|
||||
@@ -3725,7 +3725,7 @@ public class AihrPracticeSeedService {
|
||||
turn.role(),
|
||||
turn.label(),
|
||||
maskSensitiveText(turn.text()),
|
||||
firstNonBlank(turn.audioUrl(), ""),
|
||||
exposedAudioUrl(turn.audioUrl()),
|
||||
turn.audioOssId(),
|
||||
turn.emotion(),
|
||||
turn.trust(),
|
||||
@@ -3739,6 +3739,18 @@ public class AihrPracticeSeedService {
|
||||
return AihrSensitiveText.forModel(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Never expose a raw sys_oss URL through a practice response. OSS playback is
|
||||
* authorized by audioOssId; only an inline data URL is safe to return directly.
|
||||
*/
|
||||
private static String exposedAudioUrl(String audioUrl) {
|
||||
if (audioUrl == null || audioUrl.isBlank()) {
|
||||
return "";
|
||||
}
|
||||
String value = audioUrl.trim();
|
||||
return value.startsWith("data:audio/") ? value : "";
|
||||
}
|
||||
|
||||
private Integer scoreValue(List<DimensionResponse> scores, String label) {
|
||||
for (DimensionResponse score : scores) {
|
||||
if (label.equals(score.label()) && score.value() instanceof Number) {
|
||||
|
||||
+2
@@ -317,6 +317,8 @@ public class AihrPracticeSeedServiceTest {
|
||||
assertTrue(service.contains("d.answer_audio_oss_id = ?"));
|
||||
assertTrue(service.contains("aihr_practice_audio_upload"));
|
||||
assertTrue(service.contains("cleanupExpiredPracticeAudioUploads"));
|
||||
assertTrue(service.contains("private static String exposedAudioUrl(String audioUrl)"));
|
||||
assertTrue(service.contains("return value.startsWith(\"data:audio/\") ? value : \"\";"));
|
||||
assertTrue(controller.contains("currentAppUserId()"));
|
||||
assertTrue(mobileController.contains("answerDailyDrill(id, request, ownMobileExtPartyId(null), currentAppUserId())"));
|
||||
}
|
||||
|
||||
+3
-1
@@ -75,7 +75,9 @@ class AihrSensitiveTextTest {
|
||||
void ttsUploadSuccessDoesNotDuplicateInlineAudio() throws Exception {
|
||||
String speechControllerSource = Files.readString(controllerSource("AihrSpeechController.java"));
|
||||
|
||||
assertTrue(speechControllerSource.contains("new TtsResponse(oss.getUrl(), \"openai-compatible\", oss.getOssId(), \"\")"));
|
||||
assertTrue(speechControllerSource.contains("new AsrResponse(text, \"openai-compatible\", oss.getOssId(), \"\")"));
|
||||
assertTrue(speechControllerSource.contains("new TtsResponse(inlineAudioUrl, \"openai-compatible\", oss.getOssId(), inlineAudioUrl)"));
|
||||
assertFalse(speechControllerSource.contains("new TtsResponse(oss.getUrl(),"));
|
||||
assertTrue(speechControllerSource.contains("new TtsResponse(inlineAudioUrl, \"openai-compatible\")"));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user