forked from b2894lxlx/tuanwei-admin
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 引入
This commit is contained in:
@@ -389,15 +389,14 @@ import type { AiChatHistory } from "@/api/ai/aiChatHistory/model";
|
||||
import { listAiChatList, removeAiChatList } from "@/api/ai/aiChatList";
|
||||
import type { AiChatList } from "@/api/ai/aiChatList/model";
|
||||
import { sendMessage, stopMessage } from "@/api/oa/chatgpt";
|
||||
import { manuscriptGen } from "@/api/ai/manuscriptGen";
|
||||
import { runCreativeAssistantStream, runManuscriptGenStream, streamChatMessage } from "@/utils/ai-stream";
|
||||
import {
|
||||
WRITING_DOC_TYPE_FREE,
|
||||
WRITING_DOC_TYPE_OPTIONS,
|
||||
WRITING_EVENT_TYPE_OPTIONS,
|
||||
WRITING_WORD_COUNT_OPTIONS
|
||||
} from "./writing-params";
|
||||
import { creativeAssistant } from "@/api/ai/creativeAssistant";
|
||||
import { API_BASE_URL } from "@/config/setting";
|
||||
import { attachHeartbeat, buildWebSocketUrl } from "@/utils/chat-socket";
|
||||
import type { Menu } from "@/api/system/menu/model";
|
||||
import { useUserStore } from "@/store/modules/user";
|
||||
import { logout } from "@/utils/page-tab-util";
|
||||
@@ -785,17 +784,6 @@ const findFirstChildMenuPath = (menus: AuthorityTreeItem[]) => {
|
||||
return "";
|
||||
};
|
||||
|
||||
const buildWebSocketUrl = (userId: number) => {
|
||||
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(/\/$/, "")}/chat/${userId}`;
|
||||
}
|
||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||
return `${protocol}//${window.location.host}/chat/${userId}`;
|
||||
};
|
||||
|
||||
const formatHistoryMessages = (list: AiChatHistory[]) => {
|
||||
const result: ChatMessageItem[] = [];
|
||||
[...list]
|
||||
@@ -972,6 +960,7 @@ const connectWebSocket = async () => {
|
||||
}
|
||||
};
|
||||
socket.onopen = () => {
|
||||
attachHeartbeat(socket);
|
||||
wsConnected.value = false;
|
||||
};
|
||||
socket.onmessage = (event) => {
|
||||
@@ -1335,7 +1324,8 @@ const sendModeMessage = async (
|
||||
}
|
||||
};
|
||||
if (activeChatMode.value === "writing") {
|
||||
const result = await manuscriptGen(buildWritingPayload(query), { signal });
|
||||
// 稿件生成同样会被链路的 60s 空闲超时掐掉,改走流式(正文边推边显示)
|
||||
const result = await runManuscriptGenStream(buildWritingPayload(query), signal);
|
||||
if (typeof (result as any)?.taskId === "string") {
|
||||
currentTaskId.value = (result as any).taskId;
|
||||
}
|
||||
@@ -1343,7 +1333,8 @@ const sendModeMessage = async (
|
||||
return;
|
||||
}
|
||||
if (activeChatMode.value === "image") {
|
||||
const result = await creativeAssistant(buildImagePayload(query), { signal });
|
||||
// 出图中途零字节,普通 POST 会被网关按 60s 空闲超时掐成 504,改走流式
|
||||
const result = await runCreativeAssistantStream(buildImagePayload(query), signal);
|
||||
if (typeof (result as any)?.taskId === "string") {
|
||||
currentTaskId.value = (result as any).taskId;
|
||||
}
|
||||
@@ -1471,13 +1462,24 @@ const onSend = async () => {
|
||||
content.value = "";
|
||||
scrollToBottom();
|
||||
if (activeChatMode.value === "chat") {
|
||||
await connectWebSocket();
|
||||
await sendMessage({
|
||||
// 先试 WebSocket;生产网关在 /api 前丢 Upgrade 头,握手必然失败,
|
||||
// 这时落到 HTTP 流式端点,帧内容与 WS 完全一致,解析逻辑不用改
|
||||
const payload = {
|
||||
query,
|
||||
conversationId: activeConversationId.value
|
||||
}, {
|
||||
signal: abortController.signal
|
||||
});
|
||||
};
|
||||
const viaSocket = await connectWebSocket()
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
if (viaSocket) {
|
||||
await sendMessage(payload, { signal: abortController.signal });
|
||||
} else {
|
||||
await streamChatMessage(
|
||||
payload as Record<string, unknown>,
|
||||
(raw) => void handleSocketMessage(raw),
|
||||
abortController.signal
|
||||
);
|
||||
}
|
||||
} else {
|
||||
await sendModeMessage(query, assistantId, abortController.signal);
|
||||
streamingMessageId.value = undefined;
|
||||
|
||||
Reference in New Issue
Block a user