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:
+18
-62
@@ -189,6 +189,13 @@
|
||||
removeAiChatList,
|
||||
sendMessage as sendChatMessage
|
||||
} from './chat-service'
|
||||
import {
|
||||
FALLBACK_TEXT,
|
||||
FAILED_TEXT,
|
||||
isDisplayableAnswer,
|
||||
isFailedPayload,
|
||||
pickDisplayable
|
||||
} from '../../utils/ai-result'
|
||||
|
||||
const ASSISTANT_PENDING_TEXT = '正在思考中...'
|
||||
const THINK_OPEN_TAG = '<think>'
|
||||
@@ -793,79 +800,28 @@
|
||||
this.draft = ''
|
||||
this.showHistoryPanel = false
|
||||
},
|
||||
stringifyUnknown(value) {
|
||||
try {
|
||||
return JSON.stringify(value, null, 2)
|
||||
} catch (error) {
|
||||
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)。
|
||||
* <p>统一来自 `utils/ai-result`,不再各页面自带一份(见 issue 03 定位第 1 条)。
|
||||
*/
|
||||
buildFallbackText() {
|
||||
return '接口已返回,但未解析到可展示的内容。'
|
||||
return FALLBACK_TEXT
|
||||
},
|
||||
/**
|
||||
* 判断一个 `answer` 值是否可以直接展示给用户。
|
||||
*
|
||||
* <p>issue 01 的失败报文里 `extractAnswer` 会把顶层 `answer` 填成
|
||||
* `workflow_run_id`(UUID)。status 缺失时(如 `message_end` 事件)
|
||||
* 失败短路不生效,此时若直接信任 `answer`,气泡里就是一个裸 UUID。
|
||||
* 宁可回落到兜底文案,也不要把内部标识符当正文。
|
||||
* 工作流是否以失败收场。判定逻辑统一在 `utils/ai-result`。
|
||||
*/
|
||||
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
|
||||
isFailedPayload(payload) {
|
||||
return isFailedPayload(payload)
|
||||
},
|
||||
/**
|
||||
* 同 `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 ''
|
||||
pickDisplayableText(...values) {
|
||||
return pickDisplayable(...values)
|
||||
},
|
||||
isDisplayableAnswer(value) {
|
||||
return isDisplayableAnswer(value)
|
||||
},
|
||||
pickString(...values) {
|
||||
for (let index = 0; index < values.length; index += 1) {
|
||||
@@ -961,7 +917,7 @@
|
||||
}
|
||||
const data = payload || {}
|
||||
return (
|
||||
this.pickDisplayable(
|
||||
this.pickDisplayableText(
|
||||
data.answer,
|
||||
data.raw && data.raw.answer,
|
||||
data.raw && data.raw.data && data.raw.data.answer,
|
||||
|
||||
Reference in New Issue
Block a user