feat(ai): 小程序 AI 链路改走 HTTP 流式,WS 细节收敛到 utils/chat-socket
与 web 端同因:生成期间普通 POST 零字节,会被 60s 空闲超时掐成 504。
- 新增 utils/chat-socket.js:地址统一拼 /api/chat/{userId}(网关只放行 /api),
attachHeartbeat 每 25s 发 ping;三个页面不再各抄一份
- chat-service 新增 streamAiRequest:H5 走 fetch + ReadableStream,小程序走
enableChunked + onChunkReceived,并手写 UTF-8 分块解码,避免中文被切半
- 写作 / 图片 / 大纲 / 稿件改走 /stream,结构化结果从末帧 {type:'final'} 取;
流断但有正文时保留已生成部分,不整块覆盖成错误文案
- 对话仍先试 WebSocket,连不上回退 /ai/chat/message/stream,帧处理复用
handleSocketMessage
This commit is contained in:
@@ -212,9 +212,11 @@
|
||||
|
||||
<script>
|
||||
import {
|
||||
attachHeartbeat,
|
||||
buildWebSocketUrl,
|
||||
getCurrentUser,
|
||||
manuscriptGen,
|
||||
streamManuscriptGen,
|
||||
uploadTempFile
|
||||
} from './service'
|
||||
import {
|
||||
@@ -261,6 +263,7 @@
|
||||
activeConversationId: '',
|
||||
socketTask: null,
|
||||
socketConnectPromise: null,
|
||||
socketHeartbeatStop: null,
|
||||
streamStarted: false,
|
||||
streamFailed: false,
|
||||
streamResolver: null,
|
||||
@@ -521,6 +524,10 @@
|
||||
})
|
||||
},
|
||||
disconnectWebSocket() {
|
||||
if (this.socketHeartbeatStop) {
|
||||
this.socketHeartbeatStop()
|
||||
this.socketHeartbeatStop = null
|
||||
}
|
||||
this.socketConnectPromise = null
|
||||
this.wsConnected = false
|
||||
const socketTask = this.socketTask
|
||||
@@ -621,6 +628,8 @@
|
||||
|
||||
socketTask.onOpen(() => {
|
||||
this.wsConnected = false
|
||||
// 握手成功后再挂心跳:连接没建起来时发出去只会失败
|
||||
this.socketHeartbeatStop = attachHeartbeat(socketTask)
|
||||
})
|
||||
socketTask.onMessage((event) => {
|
||||
const raw = String((event && event.data) || '')
|
||||
@@ -784,25 +793,23 @@
|
||||
this.loading = true
|
||||
this.clearGenerated()
|
||||
this.generatedTitle = String(this.form.theme || '').trim()
|
||||
this.responseSummary = '正在建立 WebSocket 连接...'
|
||||
this.responseSummary = '正在提交生成任务...'
|
||||
try {
|
||||
await this.connectWebSocket()
|
||||
// 一律走 HTTP 流式(/stream),不再先连 WebSocket:
|
||||
// 触发生成的那个 POST 在整段生成期间是零字节的,会被链路上的
|
||||
// 60s 空闲超时掐成 504 Gateway Time-out(自由创作、不选文体时最先撞上)。
|
||||
// /stream 每 15s 有后端心跳续命,帧内容与 WS 完全一致。
|
||||
const payload = this.buildPayload()
|
||||
this.responseSummary = this.freeMode
|
||||
? '自由创作已提交给 AI 创意助手,生成完成后一次性返回...'
|
||||
: '连接成功,已提交生成任务,等待流式返回...'
|
||||
: '已提交生成任务,等待流式返回...'
|
||||
const waitStream = this.createStreamWaiter()
|
||||
const result = await manuscriptGen(this.buildPayload())
|
||||
if (!this.streamStarted && this.shouldTreatAsDirectResult(result)) {
|
||||
this.applyParsedResult(
|
||||
typeof result === 'string' ? this.parseSerializedResult(result) : result,
|
||||
'接口直接返回生成结果'
|
||||
)
|
||||
this.loading = false
|
||||
this.resolveStream()
|
||||
this.disconnectWebSocket()
|
||||
} else {
|
||||
await waitStream
|
||||
}
|
||||
await streamManuscriptGen(payload, {
|
||||
onMessage: (raw) => this.handleStreamFrame(raw)
|
||||
}).promise
|
||||
// 兜底:流结束但没收到 __END__ 时也别把页面卡在加载中
|
||||
this.resolveStream()
|
||||
await waitStream
|
||||
if (this.streamFailed) {
|
||||
// 后端判定工作流失败,正文区已显示失败文案,这里同步提示
|
||||
this.showToast('生成失败,请稍后重试')
|
||||
@@ -816,6 +823,34 @@
|
||||
this.showToast((error && error.message) || '内容生成失败')
|
||||
}
|
||||
},
|
||||
/**
|
||||
* SSE 帧分发:末帧带着接口的返回值(自由创作模式整段都不推正文,只能靠它),
|
||||
* 其余帧与 WebSocket 完全一致,继续交给 handleSocketMessage。
|
||||
*/
|
||||
handleStreamFrame(raw) {
|
||||
let frame = null
|
||||
try {
|
||||
frame = JSON.parse(raw)
|
||||
} catch (error) {
|
||||
this.handleSocketMessage(raw)
|
||||
return
|
||||
}
|
||||
if (!frame || frame.type !== 'final') {
|
||||
this.handleSocketMessage(raw)
|
||||
return
|
||||
}
|
||||
const result = frame.result
|
||||
if (result && Number(result.code) === 0 && result.data) {
|
||||
this.applyParsedResult(
|
||||
typeof result.data === 'string' ? this.parseSerializedResult(result.data) : result.data,
|
||||
'接口直接返回生成结果'
|
||||
)
|
||||
return
|
||||
}
|
||||
if (result && result.message) {
|
||||
this.showToast(result.message)
|
||||
}
|
||||
},
|
||||
copyGenerated() {
|
||||
if (!this.markdownContent) {
|
||||
this.showToast('暂无可复制的正文内容')
|
||||
|
||||
Reference in New Issue
Block a user