diff --git a/easy-agents-core/src/main/java/com/easyagents/core/model/chat/log/ChatLogSanitizer.java b/easy-agents-core/src/main/java/com/easyagents/core/model/chat/log/ChatLogSanitizer.java new file mode 100644 index 0000000..700ee4e --- /dev/null +++ b/easy-agents-core/src/main/java/com/easyagents/core/model/chat/log/ChatLogSanitizer.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + */ +package com.easyagents.core.model.chat.log; + +/** + * 清理聊天请求日志中的大体积或敏感媒体内容。 + */ +final class ChatLogSanitizer { + + private static final String DATA_URI_PREFIX = "data:image/"; + private static final String BASE64_MARKER = ";base64,"; + + private ChatLogSanitizer() { + } + + /** + * 隐去图片 Data URI 的编码正文,仅保留 MIME 和字符长度。 + * + * @param message 原始日志消息 + * @return 脱敏后的日志消息 + */ + static String redactImageDataUris(String message) { + if (message == null || message.indexOf(DATA_URI_PREFIX) < 0) { + return message; + } + + StringBuilder sanitized = new StringBuilder(message.length()); + int cursor = 0; + while (cursor < message.length()) { + int dataUriStart = message.indexOf(DATA_URI_PREFIX, cursor); + if (dataUriStart < 0) { + sanitized.append(message, cursor, message.length()); + break; + } + int base64Marker = message.indexOf(BASE64_MARKER, dataUriStart); + if (base64Marker < 0) { + sanitized.append(message, cursor, message.length()); + break; + } + int payloadStart = base64Marker + BASE64_MARKER.length(); + int payloadEnd = payloadStart; + while (payloadEnd < message.length() && isBase64Character(message.charAt(payloadEnd))) { + payloadEnd++; + } + + sanitized.append(message, cursor, payloadStart); + sanitized.append("<已脱敏,编码长度=").append(payloadEnd - payloadStart).append('>'); + cursor = payloadEnd; + } + return sanitized.toString(); + } + + /** + * 判断字符是否可能属于 Base64 正文。 + * + * @param value 待判断字符 + * @return 是否属于 Base64 字符集 + */ + private static boolean isBase64Character(char value) { + return value >= 'A' && value <= 'Z' + || value >= 'a' && value <= 'z' + || value >= '0' && value <= '9' + || value == '+' + || value == '/' + || value == '=' + || value == '-' + || value == '_'; + } +} diff --git a/easy-agents-core/src/main/java/com/easyagents/core/model/chat/log/DefaultChatMessageLogger.java b/easy-agents-core/src/main/java/com/easyagents/core/model/chat/log/DefaultChatMessageLogger.java index 809a75e..0f328e9 100644 --- a/easy-agents-core/src/main/java/com/easyagents/core/model/chat/log/DefaultChatMessageLogger.java +++ b/easy-agents-core/src/main/java/com/easyagents/core/model/chat/log/DefaultChatMessageLogger.java @@ -36,7 +36,8 @@ public class DefaultChatMessageLogger implements IChatMessageLogger { if (shouldLog(config)) { String provider = getProviderName(config); String model = getModelName(config); - logConsumer.accept(String.format("[%s/%s] >>>> request: %s", provider, model, message)); + logConsumer.accept(String.format("[%s/%s] >>>> request: %s", + provider, model, ChatLogSanitizer.redactImageDataUris(message))); } } diff --git a/easy-agents-core/src/test/java/com/easyagents/core/model/chat/log/DefaultChatMessageLoggerTest.java b/easy-agents-core/src/test/java/com/easyagents/core/model/chat/log/DefaultChatMessageLoggerTest.java new file mode 100644 index 0000000..7e7fd95 --- /dev/null +++ b/easy-agents-core/src/test/java/com/easyagents/core/model/chat/log/DefaultChatMessageLoggerTest.java @@ -0,0 +1,32 @@ +package com.easyagents.core.model.chat.log; + +import com.easyagents.core.model.chat.ChatConfig; +import org.junit.Assert; +import org.junit.Test; + +import java.util.concurrent.atomic.AtomicReference; + +/** + * 默认聊天日志记录器测试。 + */ +public class DefaultChatMessageLoggerTest { + + /** + * 验证请求日志不会输出完整图片 Base64。 + */ + @Test + public void shouldRedactImageDataUriPayload() { + AtomicReference logged = new AtomicReference<>(); + DefaultChatMessageLogger logger = new DefaultChatMessageLogger(logged::set); + ChatConfig config = new ChatConfig(); + config.setProvider("test"); + config.setModel("vision"); + + logger.logRequest(config, + "{\"url\":\"data:image/png;base64,AQIDBA==\",\"text\":\"ok\"}"); + + Assert.assertNotNull(logged.get()); + Assert.assertTrue(logged.get().contains("data:image/png;base64,<已脱敏,编码长度=8>")); + Assert.assertFalse(logged.get().contains("AQIDBA==")); + } +} diff --git a/easy-agents-core/src/test/java/com/easyagents/core/test/model/client/OpenAIChatMessageSerializerTest.java b/easy-agents-core/src/test/java/com/easyagents/core/test/model/client/OpenAIChatMessageSerializerTest.java new file mode 100644 index 0000000..6fa0b24 --- /dev/null +++ b/easy-agents-core/src/test/java/com/easyagents/core/test/model/client/OpenAIChatMessageSerializerTest.java @@ -0,0 +1,36 @@ +package com.easyagents.core.test.model.client; + +import com.easyagents.core.message.UserMessage; +import com.easyagents.core.model.chat.ChatConfig; +import com.easyagents.core.model.client.OpenAIChatMessageSerializer; +import org.junit.Assert; +import org.junit.Test; + +import java.util.List; +import java.util.Map; + +/** + * OpenAI-compatible 多模态消息序列化测试。 + */ +public class OpenAIChatMessageSerializerTest { + + /** + * 验证 Data URI 会写入标准的 image_url.url 字段。 + */ + @Test + public void shouldSerializeImageDataUriIntoImageUrlField() { + String dataUri = "data:image/png;base64,AQID"; + UserMessage message = new UserMessage("识别图片"); + message.addImageUrl(dataUri); + + List> messages = new OpenAIChatMessageSerializer() + .serializeMessages(List.of(message), new ChatConfig()); + + Assert.assertNotNull(messages); + List content = (List) messages.get(0).get("content"); + Map imageContent = (Map) content.get(1); + Map imageUrl = (Map) imageContent.get("image_url"); + Assert.assertEquals("image_url", imageContent.get("type")); + Assert.assertEquals(dataUri, imageUrl.get("url")); + } +}