Files
tuanwei-uniapp/utils/ai-result.js
T
weicw-dshsa 024d3dd122 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
2026-09-24 04:22:41 +08:00

137 lines
4.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* AI 结果的展示兜底。
*
* 背景:各 AI 链路在「拿不到已知字段」时会退回 `JSON.stringify(整包)`,
* 把后端原始报文(含 `workflow_id` / `task_id` / `req_id` 等内部标识符与上游错误栈)
* 直接渲染给用户。同一个缺陷曾在 3 个页面里各写一份,因此这里收敛为一处公共实现。
*
* 约定:
* - 解析不出内容时,一律给**可读文案**,绝不 stringify 整包;
* - 原始报文只应进日志(`console.error`),不进 UI;
* - 「内部标识符形状」的值(UUID、无空格长 token)不算可展示内容。
*
* 见 `.scratch/ai-assistant/issues/01-creative-assistant-raw-json-bubble.md`
* 与 `.scratch/ai-assistant/issues/03-other-links-raw-payload-fallback.md`。
*/
/**
* 解析不出内容时的通用兜底文案。
*/
export const FALLBACK_TEXT = '接口已返回,但未解析到可展示的内容。'
/**
* 工作流失败时的统一提示。
*/
export const FAILED_TEXT = '生成失败,请稍后重试'
/**
* 取载荷里的工作流执行状态。
*
* 后端把 status 放在顶层;兼容 `data.status` 的嵌套写法
* (后端 `workflowStatus` 两处都看,前端不能只看一处)。
*
* @param {*} payload 接口返回
* @returns {string} 小写、去空白的状态;取不到时为空串
*/
export function 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` 时为真;状态缺失视为未失败,避免误伤正常响应。
*
* @param {*} payload 接口返回
* @returns {boolean}
*/
export function isFailedPayload(payload) {
return getPayloadStatus(payload) === 'failed'
}
/**
* 判断一个字符串是否适合直接展示给用户。
*
* 失败报文里 `answer` 常被填成 `workflow_run_id`(UUID)这类内部标识符;
* 原样展示对用户毫无意义且构成泄露。
*
* @param {*} value 待判断的值
* @returns {boolean}
*/
export function isDisplayableAnswer(value) {
if (typeof value !== 'string') {
return false
}
const text = value.trim()
if (!text) {
return false
}
// 纯 UUID
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
}
// 无空格的长 token(task_id / run_id 等)
if (!/\s/.test(text) && /^[0-9a-zA-Z_-]{32,}$/.test(text)) {
return false
}
return true
}
/**
* 在候选值里挑第一个可展示的字符串。
*
* @param {...*} values 候选值,按优先级排列
* @returns {string} 挑中的值(已 trim);没有则空串
*/
export function pickDisplayable(...values) {
for (let index = 0; index < values.length; index += 1) {
const value = values[index]
if (isDisplayableAnswer(value)) {
return value.trim()
}
}
return ''
}
/**
* 把原始报文记进日志(不进 UI)。
*
* @param {string} scope 调用方标识,便于定位
* @param {*} payload 原始载荷
*/
export function logRawPayload(scope, payload) {
try {
console.error(`[ai-result] ${scope} 未解析出可展示内容,原始载荷:`, payload)
} catch (error) {
// 日志失败不影响渲染
}
}
/**
* 展示兜底:优先可展示的候选值,其次失败文案 / 通用兜底文案。
*
* @param {object} options
* @param {*} options.payload 原始载荷(用于失败判定与日志)
* @param {string} options.scope 日志标识
* @param {Array<*>} options.candidates 候选文案,按优先级排列
* @returns {string} 可读文案
*/
export function resolveDisplayText({ payload, scope, candidates } = {}) {
if (isFailedPayload(payload)) {
return FAILED_TEXT
}
const picked = pickDisplayable(...(candidates || []))
if (picked) {
return picked
}
logRawPayload(scope || 'unknown', payload)
return FALLBACK_TEXT
}