forked from b2894lxlx/tuanwei-admin
6f7f2f0d9a
写作 / 图片 / 大纲 / 稿件四条链路用普通 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 引入
54 lines
2.2 KiB
TypeScript
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);
|
|
}
|