diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrModelSeedService.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrModelSeedService.java index 0bf5d2e4..9bc2dfc0 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrModelSeedService.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrModelSeedService.java @@ -230,7 +230,7 @@ public class AihrModelSeedService { modelName, "真实模型调用失败,已回退内置默认兜底:先确认事实、同步时限、再生成工单闭环。", "openai-compatible-failed", - e.getMessage(), + safeFailureCode(e), List.of("检查 aihr_model_provider.api_host/api_key 和 aihr_model_config.model_name 是否与供应商一致。") ); } @@ -291,7 +291,8 @@ public class AihrModelSeedService { try { return Optional.of(callOpenAiCompatible(runtime, runtime.modelName(), userPrompt, systemPrompt, temperature)); } catch (Exception e) { - log.warn("aihr llm tryChat failed, caller falls back to seed: {}", e.getMessage()); + log.warn("event=aihr_chat_call_failed providerCode={} modelName={} exception={}", + runtime.providerCode(), runtime.modelName(), e.getClass().getSimpleName()); return Optional.empty(); } } @@ -416,21 +417,26 @@ public class AihrModelSeedService { .send(builder.build(), HttpResponse.BodyHandlers.ofString()); if (response.statusCode() < 200 || response.statusCode() >= 300) { - throw new IllegalStateException("LLM HTTP " + response.statusCode() + ": " + truncate(response.body(), 240)); + throw new IllegalStateException(httpFailureCode(response.statusCode())); } - JsonNode root = objectMapper.readTree(response.body()); + JsonNode root; + try { + root = objectMapper.readTree(response.body()); + } catch (Exception ex) { + throw new IllegalStateException("LLM_RESPONSE_INVALID_JSON"); + } return parseChatCallResult(root, modelName); } static ChatCallResult parseChatCallResult(JsonNode root, String fallbackModelName) { JsonNode choices = root.path("choices"); if (!choices.isArray() || choices.isEmpty()) { - throw new IllegalStateException("LLM response missing choices"); + throw new IllegalStateException("LLM_RESPONSE_CHOICES_MISSING"); } String content = choices.get(0).path("message").path("content").asText(); if (isBlank(content)) { - throw new IllegalStateException("LLM response missing message.content"); + throw new IllegalStateException("LLM_RESPONSE_CONTENT_MISSING"); } String responseModel = root.path("model").asText(); String actualModel = isBlank(responseModel) ? fallbackModelName : responseModel; @@ -439,6 +445,20 @@ public class AihrModelSeedService { return new ChatCallResult(content, actualModel, inputTokens, outputTokens); } + static String httpFailureCode(int statusCode) { + return "LLM_HTTP_" + statusCode; + } + + static String safeFailureCode(Exception exception) { + if (exception instanceof IllegalStateException) { + String message = exception.getMessage(); + if (message != null && message.matches("LLM_(HTTP_[0-9]{3}|RESPONSE_[A-Z_]+)")) { + return message; + } + } + return "LLM_CALL_FAILED_" + exception.getClass().getSimpleName(); + } + private RuntimeConfig runtimeConfig(String requestedModel) { Optional resolved = dbRuntimeConfig(requestedModel); if (resolved.isEmpty() && !isBlank(requestedModel)) { diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrModelSeedServiceTest.java b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrModelSeedServiceTest.java index d4d12f99..5d129056 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrModelSeedServiceTest.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrModelSeedServiceTest.java @@ -7,6 +7,8 @@ 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; @Tag("dev") class AihrModelSeedServiceTest { @@ -43,4 +45,19 @@ class AihrModelSeedServiceTest { assertEquals(0, result.inputTokens()); assertEquals(0, result.outputTokens()); } + + @Test + void failureCodesNeverExposeProviderBodyOrExceptionMessage() throws Exception { + String sensitive = "api_key=secret prompt=业主手机号13800000000 raw-provider-body"; + + assertEquals("LLM_HTTP_502", AihrModelSeedService.httpFailureCode(502)); + String safe = AihrModelSeedService.safeFailureCode(new IllegalStateException(sensitive)); + assertEquals("LLM_CALL_FAILED_IllegalStateException", safe); + assertFalse(safe.contains(sensitive)); + + IllegalStateException missing = assertThrows(IllegalStateException.class, + () -> AihrModelSeedService.parseChatCallResult(objectMapper.readTree("{}"), "configured-model")); + assertEquals("LLM_RESPONSE_CHOICES_MISSING", missing.getMessage()); + assertFalse(missing.getMessage().contains(sensitive)); + } }