fix(ai): 生成收尾不再只依赖 __END__,并清掉 WS 死代码

与后台两页保持一致:

- handleGenerate 收尾改 finally:loading 一定复位,不会再出现生成完按钮永久
  不可点(issue 20);提示按「失败 / 无正文 / 成功」区分
- 删掉已不会调用的 WebSocket 栈:connectWebSocket / disconnectWebSocket /
  refreshUser / socketTask / socketConnectPromise / socketHeartbeatStop /
  wsConnected / activeConversationId / activeMessageId,以及 attachHeartbeat、
  buildWebSocketUrl、getCurrentUser 与未使用的 manuscriptGen 导入
This commit is contained in:
2026-10-23 12:57:12 +08:00
parent f010fcd8ef
commit d99f4406fe
2 changed files with 25 additions and 275 deletions
+13 -142
View File
@@ -139,10 +139,6 @@
<script>
import {
activityOutlineGen,
attachHeartbeat,
buildWebSocketUrl,
getCurrentUser,
streamActivityOutlineGen
} from './service'
import {
@@ -152,7 +148,6 @@
} from '../../utils/ai-result'
const STREAM_END_TEXT = '__END__'
const SOCKET_CONNECTED_TEXT = '连接成功'
const THINK_OPEN_TAG = '<think>'
const THINK_CLOSE_TAG = '</think>'
@@ -174,17 +169,9 @@
result: null,
outlineMarkdown: '',
editableOutlineMarkdown: '',
activeConversationId: '',
activeMessageId: '',
socketTask: null,
socketConnectPromise: null,
socketHeartbeatStop: null,
streamStarted: false,
streamFailed: false,
streamResolver: null,
currentUser: {
userId: null
}
}
},
computed: {
@@ -211,7 +198,6 @@
},
onUnload() {
this.clearStreamResolver()
this.disconnectWebSocket()
},
methods: {
showToast(title) {
@@ -337,32 +323,10 @@
}
})
},
disconnectWebSocket() {
if (this.socketHeartbeatStop) {
this.socketHeartbeatStop()
this.socketHeartbeatStop = null
}
this.socketConnectPromise = null
const socketTask = this.socketTask
this.socketTask = null
if (socketTask && typeof socketTask.close === 'function') {
try {
socketTask.close({})
} catch (error) {
console.warn('socket close failed', error)
}
}
},
appendOutlineChunk(chunk, conversationId, messageId, reset) {
appendOutlineChunk(chunk, reset) {
if (!chunk) {
return
}
if (conversationId && conversationId !== '0') {
this.activeConversationId = conversationId
}
if (messageId && messageId !== '0') {
this.activeMessageId = messageId
}
if (!this.streamStarted || reset) {
this.streamStarted = true
// reset 表示后端判定失败、本条覆盖之前的流式残片
@@ -375,16 +339,9 @@
this.outlineMarkdown += chunk
this.syncEditableOutline()
},
finishStream(conversationId, messageId) {
if (conversationId && conversationId !== '0') {
this.activeConversationId = conversationId
}
if (messageId && messageId !== '0') {
this.activeMessageId = messageId
}
finishStream() {
this.loading = false
this.resolveStream()
this.disconnectWebSocket()
},
handleSocketMessage(raw) {
if (!raw) {
@@ -393,7 +350,7 @@
try {
const data = JSON.parse(raw)
if (data && data.answer === STREAM_END_TEXT) {
this.finishStream(data.conversationId, data.messageId)
this.finishStream()
return
}
// reset 需在 loading 之前读取:失败消息不带 loading,
@@ -404,103 +361,16 @@
}
const chunk = this.pickRawString(data && data.notice, data && data.answer)
if (typeof chunk === 'string') {
this.appendOutlineChunk(
chunk,
data && data.conversationId,
data && data.messageId,
reset
)
this.appendOutlineChunk(chunk, reset)
}
} catch (error) {
this.appendOutlineChunk(raw)
}
},
refreshUser() {
this.currentUser = getCurrentUser()
return this.currentUser
},
connectWebSocket() {
const user = this.refreshUser()
if (!user.userId) {
return Promise.reject(new Error('未获取到当前登录用户'))
}
const currentSocket = this.socketTask
if (currentSocket && this.socketConnectPromise) {
return this.socketConnectPromise
}
if (currentSocket) {
this.disconnectWebSocket()
}
let cleanupPending = () => undefined
const connectPromise = new Promise((resolve, reject) => {
let settled = false
const socketTask = uni.connectSocket({
url: buildWebSocketUrl(user.userId),
complete: () => {}
})
cleanupPending = () => {
if (this.socketConnectPromise === connectPromise) {
this.socketConnectPromise = null
}
}
socketTask.onOpen(() => {
// 握手成功后再挂心跳:连接没建起来时发出去只会失败
this.socketHeartbeatStop = attachHeartbeat(socketTask)
})
socketTask.onMessage((event) => {
const raw = String((event && event.data) || '')
if (raw === SOCKET_CONNECTED_TEXT) {
if (!settled) {
settled = true
cleanupPending()
resolve()
}
return
}
this.handleSocketMessage(raw)
})
socketTask.onClose(() => {
if (this.socketTask === socketTask) {
this.socketTask = null
}
if (!settled) {
settled = true
cleanupPending()
reject(new Error('活动策划大纲连接已关闭'))
return
}
if (this.loading) {
this.rejectStream(new Error('活动策划大纲连接已关闭'))
this.loading = false
}
cleanupPending()
})
socketTask.onError(() => {
if (!settled) {
settled = true
cleanupPending()
reject(new Error('活动策划大纲连接失败'))
return
}
if (this.loading) {
this.rejectStream(new Error('活动策划大纲连接失败'))
this.loading = false
}
})
this.socketTask = socketTask
})
this.socketConnectPromise = connectPromise
return connectPromise
},
clearResult() {
this.result = null
this.outlineMarkdown = ''
this.editableOutlineMarkdown = ''
this.activeConversationId = ''
this.activeMessageId = ''
this.streamFailed = false
},
resetForm() {
@@ -533,18 +403,19 @@
if (this.streamFailed) {
// 后端判定工作流失败,正文区已显示失败文案,这里同步提示
this.showToast('生成失败,请稍后重试')
return
} else if (!this.visibleOutlineMarkdown) {
this.showToast('这次没有生成出内容,请重试')
} else {
this.showToast('活动策划大纲生成完成')
}
if (!this.visibleOutlineMarkdown) {
this.showToast('接口已返回,但未解析到正文内容')
return
}
this.showToast('活动策划大纲生成完成')
} catch (error) {
this.disconnectWebSocket()
this.showToast((error && error.message) || '生成失败')
} finally {
// 收尾以「请求结束」为准,不能只靠 __END__ 帧:网关返回空流、
// 上游中途报错时都不会有 __END__,只把 loading 交给 __END__
// 会把生成按钮永久卡住。
this.clearStreamResolver()
this.loading = false
this.showToast((error && error.message) || '生成失败')
}
},
copyOutline() {
+12 -133
View File
@@ -192,10 +192,6 @@
<script>
import {
attachHeartbeat,
buildWebSocketUrl,
getCurrentUser,
manuscriptGen,
streamManuscriptGen,
uploadTempFile
} from './service'
@@ -206,7 +202,6 @@
} from '../../utils/ai-result'
const STREAM_END_TEXT = '__END__'
const SOCKET_CONNECTED_TEXT = '连接成功'
const THINK_OPEN_TAG = '<think>'
const THINK_CLOSE_TAG = '</think>'
@@ -235,20 +230,12 @@
eventTypeLabels: EVENT_TYPE_OPTIONS,
loading: false,
uploading: false,
wsConnected: false,
generatedTitle: '',
generatedContent: '',
uploadedFiles: [],
activeConversationId: '',
socketTask: null,
socketConnectPromise: null,
socketHeartbeatStop: null,
streamStarted: false,
streamFailed: false,
streamResolver: null,
currentUser: {
userId: null
}
}
},
computed: {
@@ -282,7 +269,6 @@
},
onUnload() {
this.clearStreamResolver()
this.disconnectWebSocket()
},
methods: {
showToast(title) {
@@ -498,30 +484,10 @@
}
})
},
disconnectWebSocket() {
if (this.socketHeartbeatStop) {
this.socketHeartbeatStop()
this.socketHeartbeatStop = null
}
this.socketConnectPromise = null
this.wsConnected = false
const socketTask = this.socketTask
this.socketTask = null
if (socketTask && typeof socketTask.close === 'function') {
try {
socketTask.close({})
} catch (error) {
console.warn('socket close failed', error)
}
}
},
appendAssistantChunk(chunk, conversationId, reset) {
appendAssistantChunk(chunk, reset) {
if (!chunk) {
return
}
if (conversationId) {
this.activeConversationId = conversationId
}
if (!this.streamStarted || reset) {
this.streamStarted = true
// reset 表示后端判定失败、本条覆盖之前的流式残片
@@ -534,15 +500,11 @@
}
this.generatedContent += chunk
},
finishStream(conversationId) {
if (conversationId) {
this.activeConversationId = conversationId
}
finishStream() {
const payload = this.parseSerializedResult(this.generatedContent)
this.applyParsedResult(payload)
this.loading = false
this.resolveStream()
this.disconnectWebSocket()
},
handleSocketMessage(raw) {
if (!raw) {
@@ -551,7 +513,7 @@
try {
const data = JSON.parse(raw)
if (data && data.answer === STREAM_END_TEXT) {
this.finishStream(data.conversationId)
this.finishStream()
return
}
const reset = !!(data && (data.reset === 'true' || data.reset === true))
@@ -560,99 +522,12 @@
}
const chunk = this.pickRawStreamString(data && data.notice, data && data.answer)
if (chunk.length > 0) {
this.appendAssistantChunk(chunk, data && data.conversationId, reset)
this.appendAssistantChunk(chunk, reset)
}
} catch (error) {
this.appendAssistantChunk(raw)
}
},
refreshUser() {
this.currentUser = getCurrentUser()
return this.currentUser
},
connectWebSocket() {
const user = this.refreshUser()
if (!user.userId) {
return Promise.reject(new Error('未获取到当前登录用户'))
}
const currentSocket = this.socketTask
if (currentSocket && this.wsConnected) {
return Promise.resolve()
}
if (currentSocket && this.socketConnectPromise) {
return this.socketConnectPromise
}
if (currentSocket) {
this.disconnectWebSocket()
}
let cleanupPending = () => undefined
const connectPromise = new Promise((resolve, reject) => {
let settled = false
const socketTask = uni.connectSocket({
url: buildWebSocketUrl(user.userId),
complete: () => {}
})
cleanupPending = () => {
if (this.socketConnectPromise === connectPromise) {
this.socketConnectPromise = null
}
}
socketTask.onOpen(() => {
this.wsConnected = false
// 握手成功后再挂心跳:连接没建起来时发出去只会失败
this.socketHeartbeatStop = attachHeartbeat(socketTask)
})
socketTask.onMessage((event) => {
const raw = String((event && event.data) || '')
if (raw === SOCKET_CONNECTED_TEXT) {
this.wsConnected = true
if (!settled) {
settled = true
cleanupPending()
resolve()
}
return
}
this.handleSocketMessage(raw)
})
socketTask.onClose(() => {
this.wsConnected = false
if (this.socketTask === socketTask) {
this.socketTask = null
}
if (!settled) {
settled = true
cleanupPending()
reject(new Error('稿件生成连接已关闭'))
return
}
if (this.loading) {
this.rejectStream(new Error('稿件生成连接已关闭'))
this.loading = false
}
cleanupPending()
})
socketTask.onError(() => {
this.wsConnected = false
if (!settled) {
settled = true
cleanupPending()
reject(new Error('稿件生成连接失败'))
return
}
if (this.loading) {
this.rejectStream(new Error('稿件生成连接失败'))
this.loading = false
}
})
this.socketTask = socketTask
})
this.socketConnectPromise = connectPromise
return connectPromise
},
clearGenerated() {
this.generatedTitle = ''
this.generatedContent = ''
@@ -782,14 +657,18 @@
if (this.streamFailed) {
// 后端判定工作流失败,正文区已显示失败文案,这里同步提示
this.showToast('生成失败,请稍后重试')
return
} else if (!this.markdownContent) {
this.showToast('这次没有生成出内容,请重试')
} else {
this.showToast('内容生成完成')
}
this.showToast('内容生成完成')
} catch (error) {
this.disconnectWebSocket()
this.showToast((error && error.message) || '内容生成失败')
} finally {
// 收尾以「请求结束」为准,不能只靠 __END__ 帧:网关返回空流、
// 上游中途报错时都不会有 __END__,只等它会把生成按钮永久卡在 loading。
this.clearStreamResolver()
this.loading = false
this.showToast((error && error.message) || '内容生成失败')
}
},
/**