mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
feat(agent): Utf8SseEmitter + returnDirect end-to-end chain test
This commit is contained in:
parent
4a95e7dfe4
commit
0b55d5a227
@ -6,6 +6,7 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
import vip.mate.channel.web.Utf8SseEmitter;
|
||||
import vip.mate.agent.AgentService;
|
||||
import vip.mate.agent.AgentState;
|
||||
import vip.mate.agent.model.AgentEntity;
|
||||
@ -105,7 +106,8 @@ public class AgentController {
|
||||
AgentEntity agent = agentService.getAgent(id);
|
||||
verifyResourceWorkspace(agent != null ? agent.getWorkspaceId() : null, workspaceId);
|
||||
|
||||
SseEmitter emitter = new SseEmitter(5 * 60 * 1000L);
|
||||
// RFC-058 PR-1: Utf8SseEmitter 显式 charset=UTF-8,防止中文 SSE 乱码
|
||||
SseEmitter emitter = new Utf8SseEmitter(5 * 60 * 1000L);
|
||||
sseExecutor.execute(() -> {
|
||||
try {
|
||||
agentService.chatStream(id, message, conversationId)
|
||||
|
||||
@ -79,7 +79,8 @@ public class ChatController {
|
||||
|
||||
String conversationId = request.getConversationId() != null ? request.getConversationId() : "default";
|
||||
// SSE 超时设为 10 分钟,覆盖 servlet 默认的 30s,避免长回答被中断
|
||||
SseEmitter emitter = new SseEmitter(10 * 60 * 1000L);
|
||||
// RFC-058 PR-1: Utf8SseEmitter 显式声明 charset=UTF-8,防止中文在 Windows 中文 Chrome / 部分代理处乱码
|
||||
SseEmitter emitter = new Utf8SseEmitter(10 * 60 * 1000L);
|
||||
|
||||
// ---- 分支 A:断线重连 ----
|
||||
if (Boolean.TRUE.equals(request.getReconnect())) {
|
||||
|
||||
@ -0,0 +1,56 @@
|
||||
package vip.mate.channel.web;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* RFC-058 PR-1: SSE emitter that explicitly advertises {@code charset=UTF-8}
|
||||
* on its {@code Content-Type} header.
|
||||
*
|
||||
* <p>Solves Chinese character mojibake on:
|
||||
* <ul>
|
||||
* <li>Windows / GBK locales whose Chrome / Edge defaults to system codepage
|
||||
* when no explicit charset is given on {@code text/event-stream}</li>
|
||||
* <li>Reverse proxies (Nginx + ICAP, certain corporate gateways) that
|
||||
* transcode according to system locale when no explicit charset is on
|
||||
* the wire</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Inspired by joyagent-jdgenie's {@code SseEmitterUTF8} — same one-line
|
||||
* idea applied throughout MateClaw's SSE endpoints.
|
||||
*
|
||||
* <p>Drop-in replacement: every {@code new SseEmitter(timeout)} should become
|
||||
* {@code new Utf8SseEmitter(timeout)}.
|
||||
*
|
||||
* @author MateClaw Team
|
||||
*/
|
||||
public class Utf8SseEmitter extends SseEmitter {
|
||||
|
||||
private static final MediaType TEXT_EVENT_STREAM_UTF8 =
|
||||
new MediaType("text", "event-stream", StandardCharsets.UTF_8);
|
||||
|
||||
public Utf8SseEmitter() {
|
||||
super();
|
||||
}
|
||||
|
||||
public Utf8SseEmitter(Long timeoutMillis) {
|
||||
super(timeoutMillis);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void extendResponse(ServerHttpResponse response) {
|
||||
super.extendResponse(response);
|
||||
HttpHeaders headers = response.getHeaders();
|
||||
// Spring's default sets Content-Type=text/event-stream without charset.
|
||||
// Only override when no charset is already specified, so callers that
|
||||
// want to roll their own (rare) keep working.
|
||||
MediaType contentType = headers.getContentType();
|
||||
if (contentType == null || contentType.getCharset() == null) {
|
||||
headers.setContentType(TEXT_EVENT_STREAM_UTF8);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -9,6 +9,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
import vip.mate.channel.web.Utf8SseEmitter;
|
||||
import vip.mate.agent.AgentService;
|
||||
import vip.mate.channel.model.ChannelEntity;
|
||||
import vip.mate.channel.service.ChannelService;
|
||||
@ -61,7 +62,8 @@ public class WebChatController {
|
||||
@RequestHeader("X-MC-Key") String apiKey,
|
||||
@RequestBody WebChatRequest request) {
|
||||
|
||||
SseEmitter emitter = new SseEmitter(10 * 60 * 1000L);
|
||||
// RFC-058 PR-1: Utf8SseEmitter 显式 charset=UTF-8,防止中文 SSE 乱码
|
||||
SseEmitter emitter = new Utf8SseEmitter(10 * 60 * 1000L);
|
||||
|
||||
// 验证 API Key 并获取关联的 Channel 配置
|
||||
ChannelEntity channel = resolveChannel(apiKey);
|
||||
|
||||
@ -7,6 +7,7 @@ import org.springframework.context.event.EventListener;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
import vip.mate.channel.web.Utf8SseEmitter;
|
||||
import vip.mate.memory.event.DreamCompletedEvent;
|
||||
import vip.mate.memory.event.DreamFailedEvent;
|
||||
import vip.mate.memory.service.DreamReport;
|
||||
@ -35,7 +36,8 @@ public class DreamEventBroadcaster {
|
||||
* Register a new SSE emitter for an agent.
|
||||
*/
|
||||
public SseEmitter register(Long agentId) {
|
||||
SseEmitter emitter = new SseEmitter(300_000L); // 5 min timeout
|
||||
// RFC-058 PR-1: Utf8SseEmitter 显式 charset=UTF-8,防止中文 SSE 乱码
|
||||
SseEmitter emitter = new Utf8SseEmitter(300_000L); // 5 min timeout
|
||||
EmitterEntry entry = new EmitterEntry(agentId, emitter);
|
||||
emitters.add(entry);
|
||||
emitter.onCompletion(() -> emitters.remove(entry));
|
||||
|
||||
@ -9,6 +9,7 @@ import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
import vip.mate.channel.web.Utf8SseEmitter;
|
||||
import vip.mate.common.result.R;
|
||||
import vip.mate.exception.MateClawException;
|
||||
import vip.mate.workspace.core.annotation.RequireWorkspaceRole;
|
||||
@ -472,7 +473,8 @@ public class WikiController {
|
||||
public SseEmitter subscribeProgress(@PathVariable Long kbId,
|
||||
@RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) {
|
||||
verifyKBWorkspace(kbId, workspaceId);
|
||||
SseEmitter emitter = new SseEmitter(30L * 60 * 1000); // 30min
|
||||
// RFC-058 PR-1: Utf8SseEmitter 显式 charset=UTF-8,防止中文 SSE 乱码
|
||||
SseEmitter emitter = new Utf8SseEmitter(30L * 60 * 1000); // 30min
|
||||
progressBus.subscribe(kbId, emitter);
|
||||
|
||||
emitter.onCompletion(() -> {
|
||||
|
||||
@ -8,6 +8,7 @@ import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
import vip.mate.channel.web.ChatStreamTracker;
|
||||
import vip.mate.channel.web.Utf8SseEmitter;
|
||||
import vip.mate.common.result.R;
|
||||
import vip.mate.wiki.service.WikiKnowledgeBaseService;
|
||||
import vip.mate.wiki.service.WikiResearchService;
|
||||
@ -98,7 +99,8 @@ public class WikiResearchController {
|
||||
@GetMapping(value = "/stream/{sessionId}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||
public SseEmitter stream(@PathVariable String sessionId) {
|
||||
// 10 分钟超时(research 典型 < 1 分钟,10 分钟给重连留余地)
|
||||
SseEmitter emitter = new SseEmitter(10 * 60 * 1000L);
|
||||
// RFC-058 PR-1: Utf8SseEmitter 显式 charset=UTF-8,防止中文 SSE 乱码
|
||||
SseEmitter emitter = new Utf8SseEmitter(10 * 60 * 1000L);
|
||||
|
||||
boolean attached = streamTracker.attach(sessionId, emitter);
|
||||
if (!attached) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user