fix(ai): 失败态给出可读文案,不再 stringify 整包兜底
后端判定工作流失败后不再下发 outputs / raw,前端需据此给出可读提示, 而不是把整包对象 JSON.stringify 进气泡(含 workflow_id / req_id 等内部标识符)。 - assistant/index.vue:新增 getPayloadStatus / isFailedPayload / buildFallbackText; formatCreativeResult 与 formatOutlineResult 失败态短路为固定文案; formatManuscriptResult、formatOutlineResult、formatCreativeResult 的整包 stringifyUnknown 兜底一律改为可读兜底文案 - 失败文案不取自 payload.answer:失败报文里它是 workflow_run_id(UUID), 原样展示等于把内部标识符贴给用户 - creative-assistant/index.vue:新增 workflowFailed,状态标签显示「执行失败」, 结果区给出失败文案,不再停在误导性的「待执行」空态 Refs: .scratch/ai-assistant/issues/01-creative-assistant-raw-json-bubble.md
This commit is contained in:
@@ -800,6 +800,37 @@
|
|||||||
return String(value ?? '')
|
return String(value ?? '')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* 取载荷里的工作流执行状态。
|
||||||
|
* 后端会把 status 放在顶层;兼容 data.status 的嵌套写法。
|
||||||
|
*/
|
||||||
|
getPayloadStatus(payload) {
|
||||||
|
if (!payload || typeof payload !== 'object') {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
const status =
|
||||||
|
payload.status !== undefined && payload.status !== null
|
||||||
|
? payload.status
|
||||||
|
: payload.data && payload.data.status
|
||||||
|
return String(status || '').trim().toLowerCase()
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 工作流是否以失败收场。
|
||||||
|
* 只在拿到明确 failed 时为真,状态缺失视为未失败,避免误伤正常响应。
|
||||||
|
*/
|
||||||
|
isFailedPayload(payload) {
|
||||||
|
return this.getPayloadStatus(payload) === 'failed'
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 兜底文案:解析不出业务字段时的可读提示。
|
||||||
|
*
|
||||||
|
* <p>绝不能退回 JSON.stringify 整包 —— 那会把 workflow_id / req_id
|
||||||
|
* 等内部标识符与上游错误栈直接贴进聊天气泡(见 issue 01)。
|
||||||
|
* 只有调用方明确传入字符串时才原样使用。
|
||||||
|
*/
|
||||||
|
buildFallbackText() {
|
||||||
|
return '接口已返回,但未解析到可展示的内容。'
|
||||||
|
},
|
||||||
pickString(...values) {
|
pickString(...values) {
|
||||||
for (let index = 0; index < values.length; index += 1) {
|
for (let index = 0; index < values.length; index += 1) {
|
||||||
const value = values[index]
|
const value = values[index]
|
||||||
@@ -867,7 +898,7 @@
|
|||||||
if (title || body) {
|
if (title || body) {
|
||||||
return `${title || this.buildManuscriptPayload(query).theme}\n\n${body}`
|
return `${title || this.buildManuscriptPayload(query).theme}\n\n${body}`
|
||||||
}
|
}
|
||||||
return this.stringifyUnknown(payload)
|
return this.buildFallbackText()
|
||||||
},
|
},
|
||||||
buildOutlinePayload(query) {
|
buildOutlinePayload(query) {
|
||||||
const fields = this.parseKeyValueInput(query)
|
const fields = this.parseKeyValueInput(query)
|
||||||
@@ -882,6 +913,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
formatOutlineResult(payload) {
|
formatOutlineResult(payload) {
|
||||||
|
// 与创意链路同理:失败态直接给可读文案,不落到整包兜底,
|
||||||
|
// 也不信任 payload.answer(失败时它可能是 UUID)。
|
||||||
|
if (this.isFailedPayload(payload)) {
|
||||||
|
return '生成失败,请稍后重试'
|
||||||
|
}
|
||||||
const data = payload || {}
|
const data = payload || {}
|
||||||
return (
|
return (
|
||||||
this.pickString(
|
this.pickString(
|
||||||
@@ -892,7 +928,7 @@
|
|||||||
data.raw && data.raw.result,
|
data.raw && data.raw.result,
|
||||||
data.raw && data.raw.content,
|
data.raw && data.raw.content,
|
||||||
data.raw && data.raw.message
|
data.raw && data.raw.message
|
||||||
) || this.stringifyUnknown(payload)
|
) || this.buildFallbackText()
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
getCreativeOutputs(payload) {
|
getCreativeOutputs(payload) {
|
||||||
@@ -961,6 +997,13 @@
|
|||||||
return `\n\n### ${title}\n${lines.join('\n\n')}`
|
return `\n\n### ${title}\n${lines.join('\n\n')}`
|
||||||
},
|
},
|
||||||
formatCreativeResult(payload) {
|
formatCreativeResult(payload) {
|
||||||
|
// 失败态优先:此时 outputs 为空、拿不到任何业务字段,
|
||||||
|
// 必须直接给可读文案,不能落到下面的整包兜底(issue 01)。
|
||||||
|
// 注意不能信任 payload.answer:失败报文中它可能是 workflow_run_id
|
||||||
|
// 这类 UUID(后端 extractAnswer 的伴生现象),必须换成固定文案。
|
||||||
|
if (this.isFailedPayload(payload)) {
|
||||||
|
return '生成失败,请稍后重试'
|
||||||
|
}
|
||||||
const outputs = this.getCreativeOutputs(payload)
|
const outputs = this.getCreativeOutputs(payload)
|
||||||
const script = this.pickString(outputs.Script_result, outputs.script, outputs.text)
|
const script = this.pickString(outputs.Script_result, outputs.script, outputs.text)
|
||||||
const videoMessage = this.pickString(outputs.video_message)
|
const videoMessage = this.pickString(outputs.video_message)
|
||||||
@@ -985,7 +1028,7 @@
|
|||||||
]
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join('\n\n')
|
.join('\n\n')
|
||||||
return text || this.stringifyUnknown(payload)
|
return text || this.pickString(payload && payload.answer) || this.buildFallbackText()
|
||||||
},
|
},
|
||||||
async sendModeMessage(query, assistantId) {
|
async sendModeMessage(query, assistantId) {
|
||||||
const target = this.messageList.find((item) => item.id === assistantId)
|
const target = this.messageList.find((item) => item.id === assistantId)
|
||||||
|
|||||||
@@ -63,8 +63,8 @@
|
|||||||
<view class="panel-card result-card">
|
<view class="panel-card result-card">
|
||||||
<view class="result-head">
|
<view class="result-head">
|
||||||
<view class="panel-title">工作流结果</view>
|
<view class="panel-title">工作流结果</view>
|
||||||
<view class="result-status" :class="loading ? 'result-status--loading' : hasAnyResult ? 'result-status--done' : ''">
|
<view class="result-status" :class="loading ? 'result-status--loading' : workflowFailed ? 'result-status--failed' : hasAnyResult ? 'result-status--done' : ''">
|
||||||
{{ loading ? '执行中' : hasAnyResult ? '已完成' : '待执行' }}
|
{{ loading ? '执行中' : workflowFailed ? '执行失败' : hasAnyResult ? '已完成' : '待执行' }}
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
@@ -88,6 +88,10 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<view v-else-if="workflowFailed" class="result-empty">
|
||||||
|
{{ (workflowResponse && workflowResponse.answer) || '工作流执行失败,请稍后重试' }}
|
||||||
|
</view>
|
||||||
|
|
||||||
<view v-else-if="!hasAnyResult" class="result-empty">
|
<view v-else-if="!hasAnyResult" class="result-empty">
|
||||||
填写文本指令并上传附件后执行工作流
|
填写文本指令并上传附件后执行工作流
|
||||||
</view>
|
</view>
|
||||||
@@ -248,6 +252,15 @@
|
|||||||
this.photoFiles.length
|
this.photoFiles.length
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* 后端判定工作流失败时不再下发 outputs/raw,只给 status + 可读 answer。
|
||||||
|
* 这里据此提示失败,避免落到「待执行」空态误导用户(见 issue 01)。
|
||||||
|
*/
|
||||||
|
workflowFailed() {
|
||||||
|
return String((this.workflowResponse && this.workflowResponse.status) || '')
|
||||||
|
.trim()
|
||||||
|
.toLowerCase() === 'failed'
|
||||||
|
},
|
||||||
loadingTitle() {
|
loadingTitle() {
|
||||||
if (this.uploadedFiles.length && String(this.content || '').trim()) {
|
if (this.uploadedFiles.length && String(this.content || '').trim()) {
|
||||||
return '正在分析指令并处理附件'
|
return '正在分析指令并处理附件'
|
||||||
@@ -443,7 +456,11 @@
|
|||||||
const result = await creativeAssistant(this.buildRequestPayload())
|
const result = await creativeAssistant(this.buildRequestPayload())
|
||||||
this.workflowResponse = result || null
|
this.workflowResponse = result || null
|
||||||
this.workflowOutputs = this.normalizeOutputPayload(result || null)
|
this.workflowOutputs = this.normalizeOutputPayload(result || null)
|
||||||
if (!this.hasAnyResult) {
|
if (this.workflowFailed) {
|
||||||
|
this.showToast(
|
||||||
|
(result && result.answer) || '工作流执行失败,请稍后重试'
|
||||||
|
)
|
||||||
|
} else if (!this.hasAnyResult) {
|
||||||
this.showToast('接口已返回,但未识别到文档约定的输出字段')
|
this.showToast('接口已返回,但未识别到文档约定的输出字段')
|
||||||
} else {
|
} else {
|
||||||
this.showToast('工作流执行完成')
|
this.showToast('工作流执行完成')
|
||||||
@@ -787,6 +804,11 @@
|
|||||||
color: #1f8f49;
|
color: #1f8f49;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.result-status--failed {
|
||||||
|
background: #fdeeee;
|
||||||
|
color: #c0392b;
|
||||||
|
}
|
||||||
|
|
||||||
.result-empty {
|
.result-empty {
|
||||||
margin-top: 22rpx;
|
margin-top: 22rpx;
|
||||||
padding: 32rpx 24rpx;
|
padding: 32rpx 24rpx;
|
||||||
|
|||||||
Reference in New Issue
Block a user