feat(ai): 聊天 WebSocket 加 /api 端点与心跳,WS 不可用时退到 HTTP SSE
生产网关在 /api 前丢掉 Upgrade 头,wss://…/chat/{userId} 到不了后端,
端上一直卡在「正在建立 WebSocket 连接…」。
- 新增 ApiChatWebSocketServer(/api/chat/{userId}),与老端点共用会话表;
会话表改存 Session 而不是端点实例,两条路径都能收到推送
- 登记会话时关闭空闲超时(Tomcat 默认 20s,AI 首字慢时会被掐断),
并补 @OnMessage 丢弃心跳(缺了它容器会因不支持的文本帧直接断连)
- send() 找不到 WS 会话时改投 ChatStreamRegistry,把帧送到 HTTP SSE 连接,
推送侧仍只有 sendMessage 一个入口,三千多次调用点不用改
- prod 放开 com.gxwebsoft.ai.config 到 INFO:只保留连接/断开,推送路径仍静默
- 补 ChatStreamRegistryTest / WebSocketEndpointPathTest / AiControllerSseFrameTest
顺带补上 31eb75c 漏加的 ChatStreamRegistry.java:AiController 已经 import 它,
缺文件时 HEAD 编译不过。
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
package com.gxwebsoft.ai.controller;
|
||||
|
||||
import com.gxwebsoft.common.core.web.ApiResult;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* SSE 末帧格式的回归测试。
|
||||
*
|
||||
* <p>出图这类「业务方法自己不推帧、只返回一个结果」的链路,结果靠末帧下发;
|
||||
* 端上按 {@code type === 'final'} 识别。字段名一旦改了而端上没改,
|
||||
* 表现为「流正常结束但页面一直转圈」,不容易在联调时发现,所以钉死格式。
|
||||
*/
|
||||
public class AiControllerSseFrameTest {
|
||||
|
||||
@Test
|
||||
public void finalFrameMarksTypeAndKeepsResult() {
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("photo_files", "[\"https://example.com/a.jpeg\"]");
|
||||
|
||||
String json = AiController.finalFrameJson(new ApiResult<>(0, "ok", data));
|
||||
|
||||
assertTrue(json.contains("\"type\":\"final\""), "末帧必须带 type=final,端上靠它区分业务帧");
|
||||
assertTrue(json.contains("\"result\":{"), "业务返回值要原样放在 result 里");
|
||||
assertTrue(json.contains("photo_files"), "返回体里的字段不能丢");
|
||||
assertEquals(1, countOccurrences(json, "\"type\":"), "末帧只能有一个 type 字段");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void finalFrameKeepsStringResult() {
|
||||
String json = AiController.finalFrameJson("plain-text");
|
||||
assertTrue(json.contains("\"result\":\"plain-text\""));
|
||||
}
|
||||
|
||||
private static int countOccurrences(String text, String needle) {
|
||||
int count = 0;
|
||||
int index = text.indexOf(needle);
|
||||
while (index != -1) {
|
||||
count++;
|
||||
index = text.indexOf(needle, index + needle.length());
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user