fix(ai): 兜底收敛公共模块,失败态不再 stringify 整包(issue 03)

「拿不到字段就 stringify 整包」的兜底此前在 3 个页面各写一份,
其中 activity-outline:332 显式把 payload.raw 当兜底内容源,是最严重的一例。

- 新增 utils/ai-result.js:getPayloadStatus / isFailedPayload /
  isDisplayableAnswer / pickDisplayable / resolveDisplayText /
  logRawPayload 统一一处;解析不出内容给可读文案,原始报文只进
  console.error 不进 UI;UUID 与无空格长 token 不算可展示内容
- manuscript parseResult:失败态短路、pickDisplayable 挡标识符、
  summary 不再回显内部字段名
- activity-outline parseOutline:删掉 payload.raw 显式兜底,失败态短路
- assistant 页删除自带副本,改为 import 公共模块
- 流式收尾:appendAssistantChunk / appendOutlineChunk 支持 reset——
  后端判定失败时丢弃已累积的半截内容;新增 streamFailed 跟踪,
  失败后不再 toast「内容生成完成」/「活动策划大纲生成完成」
- 删除死代码:两页 stringifyUnknown、大纲页 pickNonEmptyString

验证:verify-other-links.js 27/27(修复前代码跑同脚本 13 失败,判别力已证伪);
verify-frontend-fallback.js 17/17 回归通过。

Refs: .scratch/ai-assistant/issues/03-other-links-raw-payload-fallback.md
This commit is contained in:
2026-09-24 04:22:41 +08:00
parent 6bbbd386c8
commit 024d3dd122
4 changed files with 231 additions and 97 deletions
+38 -21
View File
@@ -161,6 +161,11 @@
buildWebSocketUrl,
getCurrentUser
} from './service'
import {
isFailedPayload,
pickDisplayable,
resolveDisplayText
} from '../../utils/ai-result'
const STREAM_END_TEXT = '__END__'
const SOCKET_CONNECTED_TEXT = '连接成功'
@@ -191,6 +196,7 @@
socketTask: null,
socketConnectPromise: null,
streamStarted: false,
streamFailed: false,
streamResolver: null,
currentUser: {
userId: null
@@ -288,15 +294,6 @@
event_meme: String(this.form.event_meme || '').trim()
}
},
pickNonEmptyString() {
for (let index = 0; index < arguments.length; index += 1) {
const value = arguments[index]
if (typeof value === 'string' && value.length > 0) {
return value
}
}
return ''
},
pickRawString() {
for (let index = 0; index < arguments.length; index += 1) {
const value = arguments[index]
@@ -306,18 +303,16 @@
}
return undefined
},
stringifyUnknown(value) {
try {
return JSON.stringify(value, null, 2)
} catch (error) {
return String(value || '')
}
},
parseOutline(payload) {
if (!payload) {
return ''
}
const answer = this.pickNonEmptyString(
// 失败态优先:此时 answer 可能为空或就是 workflow_run_id,
// 且 raw 里含内部标识符,绝不能作为内容源。
if (isFailedPayload(payload)) {
return resolveDisplayText({ payload, scope: 'activity-outline' })
}
const answer = pickDisplayable(
payload.answer,
payload.raw && payload.raw.answer,
payload.raw && payload.raw.data && payload.raw.data.answer,
@@ -329,7 +324,10 @@
if (answer) {
return answer
}
return this.stringifyUnknown((payload && payload.raw) || payload)
// 原实现是 return this.stringifyUnknown((payload && payload.raw) || payload),
// 即**主动**把后端整包当兜底内容渲染 —— 这是本 issue 最严重的一处。
// 现改为可读文案,原始载荷只进日志。
return resolveDisplayText({ payload, scope: 'activity-outline' })
},
buildSummary() {
const parts = []
@@ -377,7 +375,7 @@
}
}
},
appendOutlineChunk(chunk, conversationId, messageId) {
appendOutlineChunk(chunk, conversationId, messageId, reset) {
if (!chunk) {
return
}
@@ -387,10 +385,15 @@
if (messageId && messageId !== '0') {
this.activeMessageId = messageId
}
if (!this.streamStarted) {
if (!this.streamStarted || reset) {
this.streamStarted = true
// reset 表示后端判定失败、本条覆盖之前的流式残片
this.outlineMarkdown = ''
}
if (reset) {
// 记住失败态,供收尾时给出正确提示而不是「生成完成」
this.streamFailed = true
}
this.outlineMarkdown += chunk
this.syncEditableOutline()
this.resultSummary = this.buildSummary() || '正在通过 WebSocket 接收流式内容'
@@ -417,13 +420,21 @@
this.finishStream(data.conversationId, data.messageId)
return
}
// reset 需在 loading 之前读取:失败消息不带 loading,
// 但语义上必须先清掉流式残片(见 issue 03)。
const reset = !!(data && (data.reset === 'true' || data.reset === true))
if (data && (data.loading === 'true' || data.loading === true)) {
this.resultSummary = 'AI 正在组织活动策划大纲结构...'
return
}
const chunk = this.pickRawString(data && data.notice, data && data.answer)
if (typeof chunk === 'string') {
this.appendOutlineChunk(chunk, data && data.conversationId, data && data.messageId)
this.appendOutlineChunk(
chunk,
data && data.conversationId,
data && data.messageId,
reset
)
}
} catch (error) {
this.appendOutlineChunk(raw)
@@ -512,6 +523,7 @@
this.resultSummary = ''
this.activeConversationId = ''
this.activeMessageId = ''
this.streamFailed = false
},
resetForm() {
this.form = createDefaultForm()
@@ -544,6 +556,11 @@
} else {
await waitStream
}
if (this.streamFailed) {
// 后端判定工作流失败,正文区已显示失败文案,这里同步提示
this.showToast('生成失败,请稍后重试')
return
}
if (!this.visibleOutlineMarkdown) {
this.showToast('接口已返回,但未解析到正文内容')
return