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:
@@ -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
@@ -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) || '内容生成失败')
|
||||
}
|
||||
},
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user