diff --git a/mateclaw-server/src/main/java/vip/mate/agent/AgentGraphBuilder.java b/mateclaw-server/src/main/java/vip/mate/agent/AgentGraphBuilder.java index 2ccc1a2d..cc22af1d 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/AgentGraphBuilder.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/AgentGraphBuilder.java @@ -1066,7 +1066,8 @@ public class AgentGraphBuilder { String completionsPath = resolveOpenAiCompletionsPath(baseUrl, kwargs); RestClient.Builder restClientBuilder = applyHttpTimeouts( restClientBuilderProvider.getIfAvailable(RestClient::builder)); - WebClient.Builder webClientBuilder = webClientBuilderProvider.getIfAvailable(WebClient::builder); + WebClient.Builder webClientBuilder = applyHttpTimeoutsToWebClient( + webClientBuilderProvider.getIfAvailable(WebClient::builder)); // Spring AI OpenAiApi 构造函数会先 set User-Agent 为 "spring-ai",再 addAll 我们的 headers, // 导致自定义 User-Agent 被追加而非覆盖。因此对需要伪装客户端身份的 provider(如 kimi-code), @@ -1431,6 +1432,31 @@ public class AgentGraphBuilder { return builder.requestFactory(rf); } + /** + * Apply equivalent timeouts to the WebClient that backs OpenAI-compatible + * STREAMING calls (chat completions with {@code stream:true}). The + * RestClient version above only protects synchronous HTTP — without this, + * the streaming code path uses the default {@code WebClient} which has + * neither connect nor read timeout, so a stalled provider can hang the + * call forever (observed: a single volcengine-plan request held the agent + * thread for 9+ minutes with no error, until the user manually pressed + * Stop). That kept the failover chain idle because nothing threw. + *
+ * Uses {@link JdkClientHttpConnector} with the same {@link HttpClient} we + * already use for the RestClient so the dependency surface stays clean + * (reactor-netty is not on this project's classpath — Spring's webflux + * starter is excluded by design). + */ + private WebClient.Builder applyHttpTimeoutsToWebClient(WebClient.Builder builder) { + HttpClient httpClient = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(10)) + .build(); + org.springframework.http.client.reactive.JdkClientHttpConnector connector = + new org.springframework.http.client.reactive.JdkClientHttpConnector(httpClient); + connector.setReadTimeout(Duration.ofSeconds(180)); + return builder.clientConnector(connector); + } + /** * 从 generateKwargs.headers 中提取需要强制覆盖的 headers。 * 用于通过 RestClient/WebClient 拦截器绕过 Spring AI OpenAiApi 的默认 User-Agent。 diff --git a/mateclaw-server/src/main/java/vip/mate/agent/chatmodel/AgentAnthropicChatModelBuilder.java b/mateclaw-server/src/main/java/vip/mate/agent/chatmodel/AgentAnthropicChatModelBuilder.java index 4aeb7dd1..10c53de2 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/chatmodel/AgentAnthropicChatModelBuilder.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/chatmodel/AgentAnthropicChatModelBuilder.java @@ -86,7 +86,8 @@ public class AgentAnthropicChatModelBuilder implements ChatModelBuilder { String baseUrl = provider.getBaseUrl(); RestClient.Builder restClientBuilder = applyHttpTimeouts( restClientBuilderProvider.getIfAvailable(RestClient::builder)); - WebClient.Builder webClientBuilder = webClientBuilderProvider.getIfAvailable(WebClient::builder); + WebClient.Builder webClientBuilder = applyHttpTimeoutsToWebClient( + webClientBuilderProvider.getIfAvailable(WebClient::builder)); AnthropicApi.Builder builder = AnthropicApi.builder() .apiKey(apiKey.trim()) @@ -200,4 +201,25 @@ public class AgentAnthropicChatModelBuilder implements ChatModelBuilder { rf.setReadTimeout(Duration.ofSeconds(180)); return builder.requestFactory(rf); } + + /** + * Streaming counterpart of {@link #applyHttpTimeouts(RestClient.Builder)}. + * Without this, Spring AI's AnthropicApi would back its streaming chat + * call by a default WebClient with neither connect nor read timeout — a + * stalled provider could hang the agent thread indefinitely while the + * failover chain idles (no exception = no signal). + *
+ * Mirrors AgentGraphBuilder.applyHttpTimeoutsToWebClient: same JDK + * HttpClient + JdkClientHttpConnector path, so the dependency surface + * doesn't pull in reactor-netty (excluded by this project's pom). + */ + static WebClient.Builder applyHttpTimeoutsToWebClient(WebClient.Builder builder) { + HttpClient httpClient = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(10)) + .build(); + org.springframework.http.client.reactive.JdkClientHttpConnector connector = + new org.springframework.http.client.reactive.JdkClientHttpConnector(httpClient); + connector.setReadTimeout(Duration.ofSeconds(180)); + return builder.clientConnector(connector); + } }