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)) {
|
if (shouldLog(config)) {
|
||||||
String provider = getProviderName(config);
|
String provider = getProviderName(config);
|
||||||
String model = getModelName(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