From ff5f90121b2e71bff5c5f5e3f77b3a354068e7a6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E9=99=88=E5=AD=90=E9=BB=98?= <925456043@qq.com>
Date: Thu, 30 Jul 2026 15:05:42 +0800
Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BD=BF=E7=94=A8=E5=8F=91=E5=B8=83?=
=?UTF-8?q?=E5=BF=AB=E7=85=A7=E6=A0=A1=E9=AA=8C=E5=9B=BE=E7=89=87=E8=BE=93?=
=?UTF-8?q?=E5=85=A5=E8=83=BD=E5=8A=9B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 正式聊天和模型编译统一读取发布快照中的图片能力
- 草稿态保留实时模型能力并为旧快照提供安全关闭策略
---
.../runtime/AgentModelCapabilityResolver.java | 80 +++++++++
.../agent/runtime/AgentRunService.java | 9 +-
.../agent/runtime/AgentRuntimeCompiler.java | 7 +-
.../AgentRunServiceImageCapabilityTest.java | 167 ++++++++++++++++++
.../AgentRuntimeCompilerModelConfigTest.java | 84 ++++++++-
5 files changed, 340 insertions(+), 7 deletions(-)
create mode 100644 easyflow-modules/easyflow-module-agent/src/main/java/tech/easyflow/agent/runtime/AgentModelCapabilityResolver.java
create mode 100644 easyflow-modules/easyflow-module-agent/src/test/java/tech/easyflow/agent/runtime/AgentRunServiceImageCapabilityTest.java
diff --git a/easyflow-modules/easyflow-module-agent/src/main/java/tech/easyflow/agent/runtime/AgentModelCapabilityResolver.java b/easyflow-modules/easyflow-module-agent/src/main/java/tech/easyflow/agent/runtime/AgentModelCapabilityResolver.java
new file mode 100644
index 00000000..b744305f
--- /dev/null
+++ b/easyflow-modules/easyflow-module-agent/src/main/java/tech/easyflow/agent/runtime/AgentModelCapabilityResolver.java
@@ -0,0 +1,80 @@
+package tech.easyflow.agent.runtime;
+
+import tech.easyflow.agent.entity.Agent;
+import tech.easyflow.ai.entity.Model;
+
+import java.util.Map;
+import java.util.function.Supplier;
+
+/**
+ * 解析 Agent 运行时应使用的模型输入能力。
+ *
+ *
正式运行严格使用发布快照;草稿试运行使用当前模型配置。
+ */
+public final class AgentModelCapabilityResolver {
+
+ private static final String MODEL_SUMMARY = "modelSummary";
+ private static final String SUPPORT_IMAGE = "supportImage";
+ private static final String SUPPORT_IMAGE_BASE64_ONLY = "supportImageB64Only";
+
+ /**
+ * 禁止实例化能力解析工具。
+ */
+ private AgentModelCapabilityResolver() {
+ }
+
+ /**
+ * 解析 Agent 当前运行模式对应的模型能力。
+ *
+ * 只要 Agent 携带发布快照,就不会读取实时模型能力。旧快照缺少能力字段时按不支持处理。
+ *
+ * @param agent Agent 运行视图
+ * @param liveModelSupplier 草稿态实时模型提供器
+ * @return 规范化模型能力
+ */
+ public static Resolution resolve(Agent agent, Supplier liveModelSupplier) {
+ Map publishedSnapshot = agent == null
+ ? null : agent.getPublishedSnapshotJson();
+ if (publishedSnapshot != null && !publishedSnapshot.isEmpty()) {
+ return fromPublishedSnapshot(publishedSnapshot);
+ }
+ Model liveModel = liveModelSupplier == null ? null : liveModelSupplier.get();
+ return new Resolution(
+ liveModel != null && Boolean.TRUE.equals(liveModel.getSupportImage()),
+ liveModel != null && Boolean.TRUE.equals(liveModel.getSupportImageB64Only()));
+ }
+
+ /**
+ * 从发布快照读取模型能力。
+ *
+ * @param publishedSnapshot Agent 发布快照
+ * @return 快照中的模型能力
+ */
+ private static Resolution fromPublishedSnapshot(Map publishedSnapshot) {
+ Object summaryValue = publishedSnapshot.get(MODEL_SUMMARY);
+ if (!(summaryValue instanceof Map, ?> modelSummary)) {
+ return Resolution.disabled();
+ }
+ return new Resolution(
+ Boolean.TRUE.equals(modelSummary.get(SUPPORT_IMAGE)),
+ Boolean.TRUE.equals(modelSummary.get(SUPPORT_IMAGE_BASE64_ONLY)));
+ }
+
+ /**
+ * 模型输入能力解析结果。
+ *
+ * @param supportImage 是否支持图片输入
+ * @param supportImageBase64Only 是否仅支持 Base64 图片输入
+ */
+ public record Resolution(boolean supportImage, boolean supportImageBase64Only) {
+
+ /**
+ * 创建关闭全部图片能力的结果。
+ *
+ * @return 关闭图片能力的结果
+ */
+ private static Resolution disabled() {
+ return new Resolution(false, false);
+ }
+ }
+}
diff --git a/easyflow-modules/easyflow-module-agent/src/main/java/tech/easyflow/agent/runtime/AgentRunService.java b/easyflow-modules/easyflow-module-agent/src/main/java/tech/easyflow/agent/runtime/AgentRunService.java
index 5fe84ee9..06eea038 100644
--- a/easyflow-modules/easyflow-module-agent/src/main/java/tech/easyflow/agent/runtime/AgentRunService.java
+++ b/easyflow-modules/easyflow-module-agent/src/main/java/tech/easyflow/agent/runtime/AgentRunService.java
@@ -1482,10 +1482,11 @@ public class AgentRunService {
if (imageUploadIds == null || imageUploadIds.isEmpty()) {
return;
}
- tech.easyflow.ai.entity.Model model = agent == null || agent.getModelId() == null
- ? null
- : modelService.getModelInstance(agent.getModelId());
- if (model == null || !Boolean.TRUE.equals(model.getSupportImage())) {
+ AgentModelCapabilityResolver.Resolution capabilities =
+ AgentModelCapabilityResolver.resolve(agent, () ->
+ agent == null || agent.getModelId() == null
+ ? null : modelService.getModelInstance(agent.getModelId()));
+ if (!capabilities.supportImage()) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "当前 Agent 模型未启用多模态图片能力");
}
}
diff --git a/easyflow-modules/easyflow-module-agent/src/main/java/tech/easyflow/agent/runtime/AgentRuntimeCompiler.java b/easyflow-modules/easyflow-module-agent/src/main/java/tech/easyflow/agent/runtime/AgentRuntimeCompiler.java
index d7c94aba..c36188b6 100644
--- a/easyflow-modules/easyflow-module-agent/src/main/java/tech/easyflow/agent/runtime/AgentRuntimeCompiler.java
+++ b/easyflow-modules/easyflow-module-agent/src/main/java/tech/easyflow/agent/runtime/AgentRuntimeCompiler.java
@@ -108,7 +108,12 @@ public class AgentRuntimeCompiler {
if (model == null) {
throw new BusinessException("Agent 模型不存在");
}
- return AgentModelSpecMapper.fromModel(model, agent.getModelConfigJson());
+ AgentModelSpec spec = AgentModelSpecMapper.fromModel(model, agent.getModelConfigJson());
+ AgentModelCapabilityResolver.Resolution capabilities =
+ AgentModelCapabilityResolver.resolve(agent, () -> model);
+ spec.setSupportImage(capabilities.supportImage());
+ spec.setSupportImageBase64Only(capabilities.supportImageBase64Only());
+ return spec;
}
private AgentGenerationOptions buildGenerationOptions(Map config) {
diff --git a/easyflow-modules/easyflow-module-agent/src/test/java/tech/easyflow/agent/runtime/AgentRunServiceImageCapabilityTest.java b/easyflow-modules/easyflow-module-agent/src/test/java/tech/easyflow/agent/runtime/AgentRunServiceImageCapabilityTest.java
new file mode 100644
index 00000000..ff407a76
--- /dev/null
+++ b/easyflow-modules/easyflow-module-agent/src/test/java/tech/easyflow/agent/runtime/AgentRunServiceImageCapabilityTest.java
@@ -0,0 +1,167 @@
+package tech.easyflow.agent.runtime;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.springframework.web.server.ResponseStatusException;
+import tech.easyflow.agent.entity.Agent;
+import tech.easyflow.ai.entity.Model;
+import tech.easyflow.ai.service.ModelService;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.math.BigInteger;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Agent 正式与草稿聊天图片能力边界测试。
+ */
+public class AgentRunServiceImageCapabilityTest {
+
+ /**
+ * 验证正式聊天允许快照已启用的图片能力,且不读取实时模型能力。
+ *
+ * @throws Exception 反射调用失败时抛出
+ */
+ @Test
+ public void formalChatShouldUseEnabledPublishedCapability() throws Exception {
+ ModelService modelService = Mockito.mock(ModelService.class);
+ AgentRunService service = service(modelService);
+ Agent agent = publishedAgent(true);
+
+ invokeAssertImageCapability(service, agent);
+
+ Mockito.verifyNoInteractions(modelService);
+ }
+
+ /**
+ * 验证正式聊天拒绝快照未启用的图片能力,且不读取实时模型能力。
+ *
+ * @throws Exception 反射调用失败时抛出
+ */
+ @Test
+ public void formalChatShouldRejectDisabledPublishedCapability() throws Exception {
+ ModelService modelService = Mockito.mock(ModelService.class);
+ AgentRunService service = service(modelService);
+ Agent agent = publishedAgent(false);
+
+ ResponseStatusException error = expectImageCapabilityError(service, agent);
+
+ Assert.assertEquals(400, error.getStatusCode().value());
+ Mockito.verifyNoInteractions(modelService);
+ }
+
+ /**
+ * 验证旧快照缺少图片能力字段时安全拒绝图片。
+ *
+ * @throws Exception 反射调用失败时抛出
+ */
+ @Test
+ public void formalChatShouldFailClosedForLegacySnapshot() throws Exception {
+ ModelService modelService = Mockito.mock(ModelService.class);
+ AgentRunService service = service(modelService);
+ Agent agent = new Agent();
+ agent.setModelId(BigInteger.TEN);
+ agent.setPublishedSnapshotJson(Map.of(
+ "modelSummary", Map.of("modelName", "legacy-model")));
+
+ ResponseStatusException error = expectImageCapabilityError(service, agent);
+
+ Assert.assertEquals(400, error.getStatusCode().value());
+ Mockito.verifyNoInteractions(modelService);
+ }
+
+ /**
+ * 验证草稿聊天继续读取实时模型图片能力。
+ *
+ * @throws Exception 反射调用失败时抛出
+ */
+ @Test
+ public void draftChatShouldUseLiveModelCapability() throws Exception {
+ Model model = new Model();
+ model.setSupportImage(true);
+ ModelService modelService = Mockito.mock(ModelService.class);
+ Mockito.when(modelService.getModelInstance(BigInteger.TEN)).thenReturn(model);
+ AgentRunService service = service(modelService);
+ Agent agent = new Agent();
+ agent.setModelId(BigInteger.TEN);
+
+ invokeAssertImageCapability(service, agent);
+
+ Mockito.verify(modelService).getModelInstance(BigInteger.TEN);
+ }
+
+ /**
+ * 创建已注入模型服务的运行服务。
+ *
+ * @param modelService 模型服务
+ * @return Agent 运行服务
+ * @throws Exception 字段注入失败时抛出
+ */
+ private AgentRunService service(ModelService modelService) throws Exception {
+ AgentRunService service = new AgentRunService();
+ Field field = AgentRunService.class.getDeclaredField("modelService");
+ field.setAccessible(true);
+ field.set(service, modelService);
+ return service;
+ }
+
+ /**
+ * 创建携带图片能力的正式运行 Agent。
+ *
+ * @param supportImage 是否支持图片
+ * @return 正式运行 Agent
+ */
+ private Agent publishedAgent(boolean supportImage) {
+ Agent agent = new Agent();
+ agent.setModelId(BigInteger.TEN);
+ agent.setPublishedSnapshotJson(Map.of(
+ "modelSummary", Map.of(
+ "supportImage", supportImage,
+ "supportImageB64Only", false)));
+ return agent;
+ }
+
+ /**
+ * 调用图片能力校验。
+ *
+ * @param service Agent 运行服务
+ * @param agent Agent 运行视图
+ * @throws Exception 反射调用失败或能力校验失败时抛出
+ */
+ private void invokeAssertImageCapability(AgentRunService service,
+ Agent agent) throws Exception {
+ Method method = AgentRunService.class.getDeclaredMethod(
+ "assertImageCapability", Agent.class, List.class);
+ method.setAccessible(true);
+ try {
+ method.invoke(service, agent, List.of("image-upload-id"));
+ } catch (InvocationTargetException error) {
+ if (error.getCause() instanceof Exception cause) {
+ throw cause;
+ }
+ throw error;
+ }
+ }
+
+ /**
+ * 调用图片能力校验并返回预期的业务错误。
+ *
+ * @param service Agent 运行服务
+ * @param agent Agent 运行视图
+ * @return 图片能力错误
+ * @throws Exception 反射调用失败时抛出
+ */
+ private ResponseStatusException expectImageCapabilityError(AgentRunService service,
+ Agent agent) throws Exception {
+ try {
+ invokeAssertImageCapability(service, agent);
+ Assert.fail("expected ResponseStatusException");
+ return null;
+ } catch (ResponseStatusException error) {
+ return error;
+ }
+ }
+}
diff --git a/easyflow-modules/easyflow-module-agent/src/test/java/tech/easyflow/agent/runtime/AgentRuntimeCompilerModelConfigTest.java b/easyflow-modules/easyflow-module-agent/src/test/java/tech/easyflow/agent/runtime/AgentRuntimeCompilerModelConfigTest.java
index 7d916cd5..aae1b12a 100644
--- a/easyflow-modules/easyflow-module-agent/src/test/java/tech/easyflow/agent/runtime/AgentRuntimeCompilerModelConfigTest.java
+++ b/easyflow-modules/easyflow-module-agent/src/test/java/tech/easyflow/agent/runtime/AgentRuntimeCompilerModelConfigTest.java
@@ -144,6 +144,62 @@ public class AgentRuntimeCompilerModelConfigTest {
Assert.assertEquals(AgentMessageContentFormat.STANDARD, spec.getMessageContentFormat());
}
+ /**
+ * 验证正式运行的图片能力严格使用发布快照。
+ *
+ * @throws Exception 反射调用失败时抛出
+ */
+ @Test
+ public void publishedImageCapabilityShouldOverrideLiveModel() throws Exception {
+ Model model = model(Map.of());
+ model.setSupportImage(false);
+ model.setSupportImageB64Only(false);
+ Agent agent = agent(Map.of(
+ "modelSummary", Map.of(
+ "supportImage", true,
+ "supportImageB64Only", true)));
+
+ AgentModelSpec spec = invokeModelSpec(compiler(model), agent);
+
+ Assert.assertTrue(spec.isSupportImage());
+ Assert.assertTrue(spec.isSupportImageBase64Only());
+ }
+
+ /**
+ * 验证旧发布快照缺少能力字段时关闭图片能力。
+ *
+ * @throws Exception 反射调用失败时抛出
+ */
+ @Test
+ public void missingPublishedImageCapabilityShouldFailClosed() throws Exception {
+ Model model = model(Map.of());
+ model.setSupportImage(true);
+ model.setSupportImageB64Only(true);
+ Agent agent = agent(Map.of("modelSummary", Map.of("modelName", "legacy-model")));
+
+ AgentModelSpec spec = invokeModelSpec(compiler(model), agent);
+
+ Assert.assertFalse(spec.isSupportImage());
+ Assert.assertFalse(spec.isSupportImageBase64Only());
+ }
+
+ /**
+ * 验证草稿运行继续使用实时模型图片能力。
+ *
+ * @throws Exception 反射调用失败时抛出
+ */
+ @Test
+ public void draftImageCapabilityShouldUseLiveModel() throws Exception {
+ Model model = model(Map.of());
+ model.setSupportImage(true);
+ model.setSupportImageB64Only(false);
+
+ AgentModelSpec spec = invokeModelSpec(compiler(model));
+
+ Assert.assertTrue(spec.isSupportImage());
+ Assert.assertFalse(spec.isSupportImageBase64Only());
+ }
+
/**
* 验证 EasyFlow 忽略新旧消息数阈值,仅保留 Token 压缩配置。
*
@@ -230,13 +286,37 @@ public class AgentRuntimeCompilerModelConfigTest {
* @throws Exception 反射调用失败时抛出
*/
private AgentModelSpec invokeModelSpec(AgentRuntimeCompiler compiler) throws Exception {
- Agent agent = new Agent();
- agent.setModelId(BigInteger.TEN);
+ return invokeModelSpec(compiler, agent(Map.of()));
+ }
+
+ /**
+ * 调用私有模型声明编译方法。
+ *
+ * @param compiler 编译器
+ * @param agent Agent 运行视图
+ * @return 模型声明
+ * @throws Exception 反射调用失败时抛出
+ */
+ private AgentModelSpec invokeModelSpec(AgentRuntimeCompiler compiler,
+ Agent agent) throws Exception {
Method method = AgentRuntimeCompiler.class.getDeclaredMethod("buildModelSpec", Agent.class);
method.setAccessible(true);
return (AgentModelSpec) method.invoke(compiler, agent);
}
+ /**
+ * 创建测试 Agent。
+ *
+ * @param publishedSnapshot 发布快照;空映射表示草稿态
+ * @return 测试 Agent
+ */
+ private Agent agent(Map publishedSnapshot) {
+ Agent agent = new Agent();
+ agent.setModelId(BigInteger.TEN);
+ agent.setPublishedSnapshotJson(publishedSnapshot);
+ return agent;
+ }
+
/**
* 调用私有记忆策略编译方法。
*