fix(ai): WebSocket 推送静默化,并补重试与静默推送用例(issue 11 / 12)

- WebSocketServer.sendMessage 里两行 System.out.println 删除。该方法被**每个 SSE
  事件**调用一次(大纲单次生成实测 3062 个事件,即一次请求刷 6000+ 行),而且绕过
  日志框架——既不受 logging.level 控制(生产把 com.gxwebsoft 压到 ERROR 也拦不住),
  也不进日志文件,真故障时需要的几行会被顶掉。日志改到低频的 onOpen / onClose;
  同时把 onOpen 里静默吞掉的握手回应异常改成 WARN(那是「连上了却收不到内容」的
  第一现场)。
- 新增用例:AiCreativeRetryTest、AiCreativeRetryIntegrationTest(issue 11,
  后者用本地 HTTP server 扮演 Dify,断言瞬时故障确实重跑一次、凭证失效只请求一次)、
  WebSocketServerSilentPushTest(issue 12,捕获 System.out 断言推送路径零输出)。
This commit is contained in:
2026-10-13 20:00:01 +08:00
parent 8406c90cf3
commit 21e49c6a1a
4 changed files with 532 additions and 88 deletions
@@ -1,88 +1,106 @@
package com.gxwebsoft.ai.config;
import org.springframework.stereotype.Component;
import javax.websocket.OnClose;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
@ServerEndpoint(value = "/chat/{userId}")
@Component
public class WebSocketServer {
/**
* concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
*/
private static ConcurrentHashMap<String, WebSocketServer> webSocketMap = new ConcurrentHashMap<>();
/**
* 与某个客户端的连接会话,需要通过它来给客户端发送数据
*/
private Session session;
/**
* 接收userId
*/
private String userId = "";
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam("userId") String userId) {
this.session = session;
this.userId = userId;
if (webSocketMap.containsKey(userId)) {
webSocketMap.remove(userId);
webSocketMap.put(userId, this);
//加入set中
} else {
webSocketMap.put(userId, this);
}
try {
sendMessage(userId, "连接成功");
} catch (IOException e) {
}
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose() {
if (webSocketMap.containsKey(userId)) {
webSocketMap.remove(userId);
}
}
/**
* 实现服务器主动推送
*/
public void sendMessage(String userId, String message) throws IOException {
System.out.println("userId:" + userId);
if (webSocketMap.containsKey(userId)) {
Session session1 = webSocketMap.get(userId).session;
System.out.println("session1:" + session1);
if (session1 != null) session1.getBasicRemote().sendText(message);
}
}
/**
* 实现服务器主动推送
*/
public void sendAllMessage(String message) throws IOException {
ConcurrentHashMap.KeySetView<String, WebSocketServer> userIds = webSocketMap.keySet();
for (String userId : userIds) {
WebSocketServer webSocketServer = webSocketMap.get(userId);
webSocketServer.session.getBasicRemote().sendText(message);
}
}
}
package com.gxwebsoft.ai.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.websocket.OnClose;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
@ServerEndpoint(value = "/chat/{userId}")
@Component
public class WebSocketServer {
private static final Logger logger = LoggerFactory.getLogger(WebSocketServer.class);
/**
* concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
*/
private static ConcurrentHashMap<String, WebSocketServer> webSocketMap = new ConcurrentHashMap<>();
/**
* 与某个客户端的连接会话,需要通过它来给客户端发送数据
*/
private Session session;
/**
* 接收userId
*/
private String userId = "";
/**
* 连接建立成功调用的方法
*
* <p>这里才是值得打日志的位置:一次连接一条,低频且信息量大
* (排查「大纲/创意生成时端上收不到流式内容」时,第一件事就是确认端到底连没连上)。
* 推送路径 {@link #sendMessage(String, String)} 相反,单次请求会被调用上千次,必须静默。
*/
@OnOpen
public void onOpen(Session session, @PathParam("userId") String userId) {
this.session = session;
this.userId = userId;
if (webSocketMap.containsKey(userId)) {
webSocketMap.remove(userId);
webSocketMap.put(userId, this);
//加入set中
} else {
webSocketMap.put(userId, this);
}
logger.info("websocket connected, userId={}", userId);
try {
sendMessage(userId, "连接成功");
} catch (IOException e) {
logger.warn("websocket 握手回应发送失败, userId={}", userId, e);
}
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose() {
if (webSocketMap.containsKey(userId)) {
webSocketMap.remove(userId);
logger.info("websocket closed, userId={}", userId);
}
}
/**
* 实现服务器主动推送
*
* <p>两条 AI 流式链路(创意助手、活动策划大纲)**每个 SSE 事件**都会走到这里:
* 实测一次大纲生成 3062 个事件,即单次请求就会调 3000+ 次。
* 因此这里**一行日志都不能打**——原先的两行 {@code System.out.println}
* ({@code userId} 与 {@code session1})在控制台刷出上万行「userId:833」,
* 既刷屏、又绕过日志级别、还会淹没真正的告警。
*
* <p>连接与断开另见 {@link #onOpen(Session, String)} / {@link #onClose()},那里是低频的。
*/
public void sendMessage(String userId, String message) throws IOException {
if (webSocketMap.containsKey(userId)) {
Session session1 = webSocketMap.get(userId).session;
if (session1 != null) {
session1.getBasicRemote().sendText(message);
}
}
}
/**
* 实现服务器主动推送
*/
public void sendAllMessage(String message) throws IOException {
ConcurrentHashMap.KeySetView<String, WebSocketServer> userIds = webSocketMap.keySet();
for (String userId : userIds) {
WebSocketServer webSocketServer = webSocketMap.get(userId);
webSocketServer.session.getBasicRemote().sendText(message);
}
}
}