fix: 脱敏多模态图片请求日志
- 隐去图片 Data URI 编码正文并保留 MIME 与长度 - 补充日志脱敏和 image_url 序列化测试
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* 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 == '_';
|
||||
}
|
||||
}
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<String> 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=="));
|
||||
}
|
||||
}
|
||||
@@ -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<Map<String, Object>> 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"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user