From 6bbbd386c8c0c8c9d99ff59dcbc8a1093b9f61e1 Mon Sep 17 00:00:00 2001 From: "weicw1996@qq.com" Date: Thu, 24 Sep 2026 03:51:53 +0800 Subject: [PATCH] =?UTF-8?q?fix(ai):=20=E8=A1=A5=E9=BD=90=E5=A4=B1=E8=B4=A5?= =?UTF-8?q?=E6=80=81=E5=88=A4=E5=AE=9A=E7=9A=84=E4=B8=89=E5=A4=84=E9=81=97?= =?UTF-8?q?=E6=BC=8F=EF=BC=88=E8=AF=84=E5=AE=A1=E8=B7=9F=E8=BF=9B=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 对抗式评审发现上一版仍有三处同类缺口,一并修掉: - 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 --- pages/assistant/index.vue | 53 +++++++++++++++++++++++++++--- pages/creative-assistant/index.vue | 15 +++++++-- 2 files changed, 61 insertions(+), 7 deletions(-) diff --git a/pages/assistant/index.vue b/pages/assistant/index.vue index fe21837..80f55c2 100644 --- a/pages/assistant/index.vue +++ b/pages/assistant/index.vue @@ -826,11 +826,47 @@ * *

绝不能退回 JSON.stringify 整包 —— 那会把 workflow_id / req_id * 等内部标识符与上游错误栈直接贴进聊天气泡(见 issue 01)。 - * 只有调用方明确传入字符串时才原样使用。 */ buildFallbackText() { return '接口已返回,但未解析到可展示的内容。' }, + /** + * 判断一个 `answer` 值是否可以直接展示给用户。 + * + *

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) diff --git a/pages/creative-assistant/index.vue b/pages/creative-assistant/index.vue index 883ad43..fc96954 100644 --- a/pages/creative-assistant/index.vue +++ b/pages/creative-assistant/index.vue @@ -255,11 +255,20 @@ /** * 后端判定工作流失败时不再下发 outputs/raw,只给 status + 可读 answer。 * 这里据此提示失败,避免落到「待执行」空态误导用户(见 issue 01)。 + * + *

判定需与 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()) {