Files
tuanwei-admin/src/utils/chat-socket.ts
T
weicw-dshsa 6f7f2f0d9a feat(ai): 后台 AI 链路改走 HTTP 流式,WS 地址与心跳收敛到 utils
写作 / 图片 / 大纲 / 稿件四条链路用普通 POST 时,生成期间链路上零字节,
会被 60s 空闲超时掐成 504(出图实测 58s 断开)。后端已给每条链路开
/stream(SSE,每 15s 一帧心跳),这里改成消费它。

- 新增 utils/ai-stream.ts:SSE 帧解析 + 末帧 {type:'final', result} 拆包;
  runCreativeAssistantStream / runManuscriptGenStream 对外仍返回原来的 data,
  页面渲染逻辑不动
- 新增 utils/chat-socket.ts:buildWebSocketUrl 统一拼 /api/chat/{userId}
  (网关只放行 /api 前缀),attachHeartbeat 每 25s 发 ping 顶住代理空闲超时
- chatgpt 对话先试 WebSocket,握手失败落 /ai/chat/message/stream,
  帧内容与 WS 完全一致,处理函数直接复用
- 四个页面删掉各自重复的 buildWebSocketUrl,统一从 utils 引入
2026-10-21 19:18:08 +08:00

54 lines
2.2 KiB
TypeScript

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);
}