import { API_BASE_URL } from '@/config/setting'; /** * 后台与大屏端(Vue)的聊天 WebSocket 细节,集中在这里。 * * 四个页面(创意助手、活动策划大纲、稿件生成、公文校验)此前各自抄了一份 * 「拼 URL + 连 + 断」,路径改一次要改四处;现在统一走这里。 * * 路径是 **`/api/chat/{userId}`**,不是 `/chat/{userId}`:生产网关只放行 `/api` 前缀, * 老路径的握手请求到不了后端(实测 404)。后端同时保留了根路径端点给老版本小程序兜底, * 见 `tuanwei-java` 的 `WebSocketConfig`。 */ /** 心跳间隔。nginx / 网关的空闲超时普遍在 60s 上下,25s 留够余量。 */ const HEARTBEAT_INTERVAL = 25000; export function buildWebSocketUrl(userId: number | string): string { if (/^https?:\/\//.test(API_BASE_URL)) { const url = new URL(API_BASE_URL); url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:'; url.pathname = url.pathname.replace(/\/api\/?$/, ''); return `${url.toString().replace(/\/$/, '')}/api/chat/${userId}`; } // 相对路径(同源反代):后台站点自己的域名下同样挂 /api/chat const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; return `${protocol}//${window.location.host}/api/chat/${userId}`; } /** * 每 25s 发一个 `ping`,顶住反向代理与网关的空闲断开。 * * 只发不收:页面把收到的每条消息当流式片段解析,服务端若回 pong 会被拼进正文。 * 服务端的 `@OnMessage` 收到文本直接丢弃。 * * 连接关闭后定时器会自行结束(下次触发时发现 readyState 已是 CLOSED), * 调用方不需要单独清理。 */ export function attachHeartbeat(socket: WebSocket): () => void { const timer = window.setInterval(() => { if (socket.readyState === WebSocket.OPEN) { try { socket.send('ping'); } catch { // 发送失败说明连接正在断,交给下一次 tick 或 onclose 处理 } return; } if (socket.readyState === WebSocket.CLOSED) { window.clearInterval(timer); } }, HEARTBEAT_INTERVAL); return () => window.clearInterval(timer); }