fix(ai): 补齐失败态判定的三处遗漏(评审跟进)
对抗式评审发现上一版仍有三处同类缺口,一并修掉: - formatCreativeResult 不再回落 payload.answer:失败报文里 answer 是 workflow_run_id(UUID),而 status 缺失时(message_end 事件、后端流式兜底 分支)失败短路不生效,裸 UUID 会进气泡 - 新增 isDisplayableAnswer / pickDisplayable:把 UUID 与无空格长 token 这类内部标识符挡在展示之外,formatOutlineResult 与 formatManuscriptResult 复用;大纳/稿件也一并加失败态短路,避免 03 落地后端后渲染成 「标题 + 失败文案」 - creative-assistant 的 workflowFailed 补 data.status 回退,与 assistant 页 及后端 workflowStatus(两处都看)对齐,避免退回误导性的「待执行」空态 验证:verify-frontend-fallback.js 17/17;verify-frontend-e2e.js 以后端真实产出 为输入跑通,渲染结果无内部标识符与原始 JSON。 Refs: .scratch/ai-assistant/issues/01-creative-assistant-raw-json-bubble.md
This commit is contained in:
@@ -826,11 +826,47 @@
|
||||
*
|
||||
* <p>绝不能退回 JSON.stringify 整包 —— 那会把 workflow_id / req_id
|
||||
* 等内部标识符与上游错误栈直接贴进聊天气泡(见 issue 01)。
|
||||
* 只有调用方明确传入字符串时才原样使用。
|
||||
*/
|
||||
buildFallbackText() {
|
||||
return '接口已返回,但未解析到可展示的内容。'
|
||||
},
|
||||
/**
|
||||
* 判断一个 `answer` 值是否可以直接展示给用户。
|
||||
*
|
||||
* <p>issue 01 的失败报文里 `extractAnswer` 会把顶层 `answer` 填成
|
||||
* `workflow_run_id`(UUID)。status 缺失时(如 `message_end` 事件)
|
||||
* 失败短路不生效,此时若直接信任 `answer`,气泡里就是一个裸 UUID。
|
||||
* 宁可回落到兜底文案,也不要把内部标识符当正文。
|
||||
*/
|
||||
isDisplayableAnswer(value) {
|
||||
if (typeof value !== 'string') {
|
||||
return false
|
||||
}
|
||||
const text = value.trim()
|
||||
if (!text) {
|
||||
return false
|
||||
}
|
||||
// 纯 UUID / 十六进制串 / 无空格的超长 token,都视为内部标识符
|
||||
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(text)) {
|
||||
return false
|
||||
}
|
||||
if (!/\s/.test(text) && /^[0-9a-zA-Z_-]{32,}$/.test(text)) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
/**
|
||||
* 同 `pickString`,但会跳过不像给人看的 `answer`(如 UUID)。
|
||||
*/
|
||||
pickDisplayable(...values) {
|
||||
for (let index = 0; index < values.length; index += 1) {
|
||||
const value = values[index]
|
||||
if (typeof value === 'string' && value.trim() && this.isDisplayableAnswer(value)) {
|
||||
return value.trim()
|
||||
}
|
||||
}
|
||||
return ''
|
||||
},
|
||||
pickString(...values) {
|
||||
for (let index = 0; index < values.length; index += 1) {
|
||||
const value = values[index]
|
||||
@@ -872,6 +908,11 @@
|
||||
if (typeof payload === 'string') {
|
||||
return payload
|
||||
}
|
||||
// 与大纲 / 创意链路对齐:失败态直接给可读文案。
|
||||
// 否则 answer 里的失败提示会被当成正文,渲染成「标题 + 失败文案」。
|
||||
if (this.isFailedPayload(payload)) {
|
||||
return '生成失败,请稍后重试'
|
||||
}
|
||||
const data = payload || {}
|
||||
const title = this.pickString(
|
||||
data.title,
|
||||
@@ -885,7 +926,7 @@
|
||||
data.text,
|
||||
data.result,
|
||||
data.output,
|
||||
data.answer,
|
||||
this.isDisplayableAnswer(data.answer) ? data.answer : '',
|
||||
data.manuscript,
|
||||
data.article,
|
||||
data.message,
|
||||
@@ -920,7 +961,7 @@
|
||||
}
|
||||
const data = payload || {}
|
||||
return (
|
||||
this.pickString(
|
||||
this.pickDisplayable(
|
||||
data.answer,
|
||||
data.raw && data.raw.answer,
|
||||
data.raw && data.raw.data && data.raw.data.answer,
|
||||
@@ -1028,7 +1069,11 @@
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n\n')
|
||||
return text || this.pickString(payload && payload.answer) || this.buildFallbackText()
|
||||
// 注意:这里不再回落到 payload.answer。issue 01 的报文里 answer 被
|
||||
// extractAnswer 填成了 workflow_run_id(UUID);status 缺失时(如 message_end
|
||||
// 事件)失败短路不生效,回落 answer 就会把裸 UUID 贴进气泡。
|
||||
// 拿不到已知业务字段时一律给可读兜底。
|
||||
return text || this.buildFallbackText()
|
||||
},
|
||||
async sendModeMessage(query, assistantId) {
|
||||
const target = this.messageList.find((item) => item.id === assistantId)
|
||||
|
||||
@@ -255,11 +255,20 @@
|
||||
/**
|
||||
* 后端判定工作流失败时不再下发 outputs/raw,只给 status + 可读 answer。
|
||||
* 这里据此提示失败,避免落到「待执行」空态误导用户(见 issue 01)。
|
||||
*
|
||||
* <p>判定需与 assistant 页及后端 `workflowStatus` 一致:顶层优先,
|
||||
* 回退 `data.status`(后端两处都看,前端不能只看一处)。
|
||||
*/
|
||||
workflowFailed() {
|
||||
return String((this.workflowResponse && this.workflowResponse.status) || '')
|
||||
.trim()
|
||||
.toLowerCase() === 'failed'
|
||||
const payload = this.workflowResponse
|
||||
if (!payload || typeof payload !== 'object') {
|
||||
return false
|
||||
}
|
||||
const status =
|
||||
payload.status !== undefined && payload.status !== null
|
||||
? payload.status
|
||||
: payload.data && payload.data.status
|
||||
return String(status || '').trim().toLowerCase() === 'failed'
|
||||
},
|
||||
loadingTitle() {
|
||||
if (this.uploadedFiles.length && String(this.content || '').trim()) {
|
||||
|
||||
Reference in New Issue
Block a user