# AI 生成失败提示优化 TODO > 状态:待讨论,未实施 > 范围:图片、脚本、故事板、视频、配音等 AI 生成流程面向普通用户的失败提示;不改变模型路由、供应商、计费或重试策略。 > 协作规则:先确认提示分层与文案原则,再逐 Step 实施;本文件建立时不修改业务代码。 ## 1. 当前问题 2026-07-15,视频项目 `cb4679f9-e813-44f4-af6f-e40e784a8d31` 使用 **AirShelf Script** 生成脚本失败。服务端记录的原始错误为: ```text 402 Client Error: Payment Required for url: https://www.yunqiai.chat/v1/chat/completions ``` 当前页面将它展示为“脚本生成失败:402 Client Error …”。这对用户没有可执行的信息:用户既不知道问题属于平台服务、额度、素材、内容审核、网络还是系统异常,也不知道重试是否有效。 ## 2. 目标 1. 普通用户只看到清晰、可行动、不过度暴露供应商与模型细节的提示。 2. 管理员/开发排障仍能保留完整原始错误、任务 ID、模型与供应商信息。 3. 同一类失败在所有生成入口中有一致的标题、说明和操作建议。 4. 不把“可重试”“联系客服”“更换素材”等建议误用于错误类型不明的情况。 5. 失败任务继续按既有规则释放预扣积分;提示层不改变结算逻辑。 ## 3. 推荐方向:后端归类 + 前端统一呈现(分层方案) 不建议只在页面对完整英文报错做枚举替换,也不建议只在后端写死中文文案。推荐返回稳定的**错误分类码**与安全上下文,由前端共享组件/函数负责文案和交互。 ```text 供应商原始错误 → 后端:识别 HTTP 状态、供应商错误码、任务阶段,归类为稳定 error_code → API/SSE:返回 error_code、retryable、user_message_key、task_id(原始错误仅留日志/管理端) → 前端:按 error_code 映射标题、说明、按钮与帮助入口 ``` ### 为什么这样分层 - 供应商原始文案会变化;前端不应依赖 `"Payment Required"`、英文句子或 URL 的完整匹配。 - HTTP `402` 在不同供应商的含义可能略有差异;后端最接近供应商响应,能结合 provider、模型和任务阶段做正确归类。 - 前端保留文案控制权,可统一语气、后续做多语言、按入口显示合适的“重试 / 返回修改 / 联系支持”操作。 - `error_code` 是契约,不是面向用户的文字;技术模型名、密钥、URL、原始响应不能进入普通用户提示。 ## 3.1 错误出口盘点(已完成,未改代码) 本轮只做了代码与当前失败链路盘点,结论如下。 | 入口 | 当前失败出口 | 当前用户可见内容 | 接入方式 | | --- | --- | --- | --- | | 脚本 Agent | `script_agent.py` SSE `type=error` | 直接拼接原始异常,例如 `脚本生成失败: 402 Client Error ...` | 优先接入;SSE 帧可增补安全错误对象 | | 图片创作 / 基础资产 / 三视图 | `AITask.error_message` → 轮询接口 | 多处直接写入并展示 `str(exc)` | 任务失败适配层统一写入安全文案 | | 故事板 | `StoryboardShot.error_message` + 轮询 | 部分路径已调用 `friendly_generation_error`,但规则分散 | 复用统一归类器,保留现有界面展示位置 | | 项目视频 | `VideoSegment.error_message` / 提交接口 | 多处直接透传第三方异常 | 接入统一归类器;接口不再返回原始 `detail` | | 自由创作视频 | `free_video.py` | 已有 `video_errors.py`,保存 `AITask.error_code` 与友好文案 | 作为已有样板迁入/兼容统一归类器,避免回归 | | 配音 / 实体提取 | 任务轮询 / 任务记录 | 多数路径仍保存原始 `str(exc)` | 与图片同一任务适配层接入 | ### 已有可复用基础 - `AITask` 已经有 `error_code` 字段,**第一期不需要新增数据库字段**。 - `free_video.py` 已实践“供应商错误码 + 安全文案 + 任务记录”的模式;其中 `video_errors.py` 只覆盖自由创作视频,不能直接扩展成前端全局映射。 - `OpenAICompatibleProvider` 已能在异常中保留上游响应体;归类应读取异常对象/响应状态,而不是在前端匹配英文字符串。 ### 当前需要收口的泄露点 - 脚本 SSE、项目视频提交接口、图片与任务轮询的部分路径直接向普通用户返回原始错误。 - AI 失败通知目前会把“第三方服务商 API 原始报错”写入普通用户可见正文;该行为必须随本 TODO 一并改为仅保留安全提示。原始信息应保存于服务端日志、任务原始响应或管理员任务详情。 ## 3.2 已确认的最小实施边界 第一期只改 AI 生成错误的适配与展示,不碰全局请求封装、登录、上传、项目 CRUD 或普通非 AI 接口。 1. 新建一个 AI 域内的纯归类模块:输入异常/供应商响应/操作阶段,输出稳定分类码、建议动作、是否可立即重试和安全文案。 2. 复用既有 `AITask.error_code`;原始供应商错误留在服务端日志与任务的原始响应记录,不将其作为普通用户 API 的 `error_message`。 3. SSE 与任务轮询返回增补结构化 `error` 对象;保留安全的文本回退,保障尚未改造的展示入口不白屏。 4. 前端仅新增一个 AI 专用共享映射层;它只替换 AI 页面当前直接渲染错误文本的地方,不影响其它页面。 这意味着第一期**无需数据库迁移,也不改变模型路由、任务状态机、计费、预扣或退费逻辑**。 ## 4. 首批错误分类草案 | 分类码 | 典型来源 | 用户提示方向 | 建议操作 | | --- | --- | --- | --- | | `provider_quota_exhausted` | 上游 402、明确的余额/额度不足 | “生成服务暂不可用,请稍后再试。” | 不建议立刻连续重试;提示平台处理,不归咎用户 | | `provider_rate_limited` | 429 | “当前生成请求较多,请稍后重试。” | 显示重试 | | `provider_unavailable` | 5xx、连接超时、网关故障 | “生成服务暂时不可用,请稍后重试。” | 显示重试 | | `provider_auth_failed` | 401/403、供应商凭证或权限异常 | “生成服务配置异常,已通知平台处理。” | 不显示用户侧重试或仅有限重试 | | `model_unavailable` | 模型不存在、已下线、能力不支持 | “当前生成能力暂不可用。” | 返回上一步 / 选择其他可用模型(若产品允许) | | `content_rejected` | 内容安全/审核拦截 | “内容未通过生成服务审核,请调整描述或素材后重试。” | 返回修改 | | `invalid_input` | 尺寸、格式、缺少必要素材、参数不合法 | “提交内容不完整或格式不支持,请检查后重试。” | 定位字段或返回修改 | | `insufficient_user_credit` | 平台自身积分预留失败 | “可用积分不足,充值后再试。” | 去充值 | | `generation_failed_unknown` | 未能安全归类的异常 | “生成遇到问题,请稍后重试。” | 重试 + 反馈入口 | ### 本次 402 的拟定归类 `yunqi_gemini / gemini-3.1-pro-preview` 返回的 402 应归为 `provider_quota_exhausted`。面向用户的文字不应出现 YunQi、Gemini、HTTP 402、URL 或“支付”字样;这些内容仅保留在任务记录和管理端排障信息中。 ## 4.1 统一错误对象契约(已确定,待实施) 第一期所有普通用户 AI 接口统一补充 `error` 对象;已有的 `detail` / `error_message` 字段保留,但其值改为安全的回退文案,保证旧页面不会空白。原始异常不再作为普通用户接口字段返回。 ```ts type PublicGenerationError = { domain: "generation"; code: | "provider_quota_exhausted" | "provider_rate_limited" | "provider_unavailable" | "provider_config_error" | "model_unavailable" | "content_rejected" | "invalid_input" | "asset_unavailable" | "user_credit_insufficient" | "task_timeout" | "processing_failed" | "unknown"; operation: | "script_generate" | "image_generate" | "base_asset_generate" | "triview_generate" | "storyboard_generate" | "video_generate" | "voiceover_generate" | "entity_extract"; action: "retry" | "retry_later" | "revise_input" | "recharge" | "contact_support" | "none"; retryable: boolean; reference_id?: string; }; ``` ### 字段职责 - `domain`:为未来上传、权限、项目操作等非 AI 域预留;本期固定为 `generation`。 - `code`:后端归类产生的稳定业务码;前端只依赖它,绝不匹配 HTTP 文本、URL 或供应商英文文案。 - `operation`:说明失败发生在哪种 AI 操作,使同一错误可显示“脚本/图片/视频”对应的自然语言。 - `action` 与 `retryable`:由后端根据失败性质决定用户可执行操作;前端不自行猜测是否应重试。 - `reference_id`:任务 ID,仅在需要联系客服或排障时展示/复制;不得包含密钥、供应商 URL 或原始报错。 ### 首批行为规则 | `code` | `action` | `retryable` | 用户侧处理原则 | | --- | --- | --- | --- | | `provider_quota_exhausted` | `retry_later` | 否 | 平台上游额度/套餐问题;不引导用户充值 | | `provider_rate_limited` | `retry_later` | 否 | 等待后可再尝试,避免连续点击 | | `provider_unavailable` / `task_timeout` | `retry` | 是 | 可安全重新发起一次 | | `provider_config_error` / `model_unavailable` | `contact_support` | 否 | 平台配置或模型可用性问题 | | `content_rejected` / `invalid_input` / `asset_unavailable` | `revise_input` | 否 | 给出修改文案或回到相应步骤 | | `user_credit_insufficient` | `recharge` | 否 | 仅平台自身积分不足时使用 | | `processing_failed` / `unknown` | `retry` | 是 | 使用通用回退,保留任务编号供反馈 | ### 传输示例 脚本 SSE: ```json { "type": "error", "detail": "脚本生成暂时不可用,请稍后再试。", "error": { "domain": "generation", "code": "provider_quota_exhausted", "operation": "script_generate", "action": "retry_later", "retryable": false, "reference_id": "" } } ``` 任务轮询、失败通知与失败卡片使用同一 `error` 对象;其中文字由前端共享映射层渲染。`detail` / `error_message` 仅作兼容旧界面的安全回退。 ### 与现有 `AITask.error_code` 的兼容 现有 `AITask.error_code` 在自由创作视频中承载部分供应商原始错误码,不能直接假定它已是面向用户的稳定码。第一期由后端的归类器从原始异常、HTTP 状态和已有内部错误码**动态投影**出 `PublicGenerationError`,不改数据库字段含义。 后续若需要错误分类统计,再单独决定是否持久化业务 `code`;这不阻塞本次提示优化。 ## 4.2 后端归类规则与优先级(已确定,待实施) 归类器的建议入口为: ```python classify_generation_error( exc, *, operation: str, provider_name: str = "", internal_kind: str = "", ) -> PublicGenerationError ``` 它从异常对象读取 HTTP 状态、供应商结构化错误码、供应商消息和调用阶段。中转站异常可从 `requests.HTTPError.response` 读取状态/响应体;火山异常可从既有 `火山报错 [code] message` 格式读取错误码。**前端不参与解析。** ### 优先级 按下面顺序判定,先命中的规则终止;避免把“审核 403”误判成“凭证 403”,或把上游额度误提示为用户积分不足。 | 优先级 | 识别信号 | 归类结果 | 说明 | | --- | --- | --- | --- | | 1 | 调用方明确传入 `internal_kind=user_credit_insufficient` | `user_credit_insufficient` | 本地积分预扣失败;不依赖异常文本猜测 | | 2 | 内容审核结构化码/白名单:`moderation_blocked`、`*SensitiveContentDetected*`、`*PolicyViolation*`、`safety_violation` 等 | `content_rejected` | 即使 HTTP 为 400/403,也优先按审核处理 | | 3 | 素材结构化码/白名单:`AssetNotFound`、明确的引用素材不存在 | `asset_unavailable` | 引导用户换素材 | | 4 | 模型结构化码/白名单:`model_not_found`、`model_not_available`、明确“不支持该能力” | `model_unavailable` | 不把模型下线误说成用户参数错误 | | 5 | 上游额度信号:HTTP 402、`InsufficientBalance`、明确的 provider quota/balance exhausted | `provider_quota_exhausted` | 属于平台供应商额度,不显示“去充值” | | 6 | 限流信号:HTTP 429、`RateLimitExceeded`、`ConcurrencyLimitExceeded` | `provider_rate_limited` | 只允许稍后再试 | | 7 | 超时信号:HTTP 408/504、`Timeout`、连接/读取超时 | `task_timeout` | 允许用户重新生成 | | 8 | 凭证/配置:HTTP 401;非审核原因的 403;密钥/配置缺失;端点配置错误 | `provider_config_error` | 引导平台排查,不归咎用户 | | 9 | 明确参数/文件错误:`InvalidParameter`、`InvalidImage`、`InvalidVideo`、`InvalidAudio`、受白名单保护的格式/尺寸错误 | `invalid_input` | 仅识别明确错误;不能把所有 400 都归为用户问题 | | 10 | HTTP 5xx、`ServerOverloaded`、`InternalError` | `provider_unavailable` | 供应商暂时不可用,可重试 | | 11 | 本地保存、转存、后处理失败 | `processing_failed` | 不同于供应商生成失败 | | 12 | 其余异常或无法安全判断的 4xx/5xx | `unknown` | 使用保守通用文案,保留任务编号 | ### 平台积分与供应商额度的硬性分流 - `reserve_credit()` 本地预扣抛出的 `ValueError("insufficient credit")`,在其调用边界显式标记为 `internal_kind=user_credit_insufficient`;前台显示“可用积分不足”,可去充值。 - 供应商返回的 `InsufficientBalance`、YunQi 的 HTTP 402、火山/其他渠道的套餐或余额耗尽,均标记为 `provider_quota_exhausted`;前台显示“生成服务正在处理中”,不提供充值入口。 - 禁止在通用归类器中以“余额不足”“额度不足”等中文关键词直接判为用户积分不足,避免误导。 ### 本次错误的判定链路 ```text YunQi chat/completions → HTTP 402 Payment Required → 规则 5:provider_quota_exhausted → 脚本暂时不可用 / 生成服务正在处理中,请稍后再试。 ``` ### 实施时的保护要求 1. 仅使用结构化码、HTTP 状态和经过白名单限定的关键词;未知内容一律回退 `unknown`,不臆测用户责任。 2. 保留原始异常给服务端日志、任务原始响应和管理员排障;普通用户 API、SSE 与通知正文只接收 `PublicGenerationError` 和安全回退文案。 3. 单元测试覆盖上表每一类信号;使用模拟异常/模拟响应,不发起真实模型请求、不产生供应商费用。 ## 5. 提示文案原则(待确认) - 先说结果与影响,再给下一步;避免“请求失败”“HTTP Error”等技术描述。 - 区分平台服务额度与用户账户积分:上游余额不足不能提示用户“去充值”。 - “请重试”只用于暂时性故障/限流;审核、参数和凭证类错误要给正确的修改或等待路径。 - 所有未知错误都可附带可复制的任务编号,供客服定位,但不展示密钥、供应商 URL 或原始堆栈。 - 同一错误在脚本、图片、视频入口保持核心文案一致;可按阶段替换“脚本/图片/视频/配音”这个对象词。 ## 5.1 首批普通用户文案与动作(已拟定,待实施) ### 操作名称 | `operation` | 页面展示名称 | | --- | --- | | `script_generate` | 脚本 | | `image_generate` | 图片 | | `base_asset_generate` | 基础素材 | | `triview_generate` | 三视图 | | `storyboard_generate` | 故事板 | | `video_generate` | 视频 | | `voiceover_generate` | 配音 | | `entity_extract` | 脚本信息 | ### 文案表 `{操作}` 由上表替换。标题和说明均不出现供应商、模型、HTTP 状态、接口地址或支付字样。 | `code` | 标题 | 说明 | 建议主操作 | | --- | --- | --- | --- | | `provider_quota_exhausted` | `{操作}暂时不可用` | 生成服务正在处理中,请稍后再试。 | 无即时重试;保留原页面稍后再试 | | `provider_rate_limited` | `{操作}请求较多` | 当前生成请求较多,请稍后再试。 | 无即时重试 | | `provider_unavailable` | `{操作}暂时不可用` | 生成服务暂时不可用,请稍后重试。 | `重新生成` | | `provider_config_error` | `{操作}暂时不可用` | 平台正在处理该问题,请稍后再试。 | `复制任务编号` | | `model_unavailable` | `{操作}暂时不可用` | 当前生成能力暂不可用,请稍后再试。 | `复制任务编号` | | `content_rejected` | `{操作}内容需要调整` | 内容未通过生成审核,请调整描述或素材后重试。 | `去修改` | | `invalid_input` | `{操作}内容需检查` | 请检查描述、参数或素材格式后重试。 | `去修改` | | `asset_unavailable` | 素材暂不可用 | 引用的素材不存在或暂不可用,请更换后重试。 | `去修改` | | `user_credit_insufficient` | 可用积分不足 | 充值后可继续生成。 | `去充值` | | `task_timeout` | `{操作}生成时间过长` | 服务响应较慢,未完成生成,请重试。 | `重新生成` | | `processing_failed` | `{操作}结果处理失败` | 本次未生成可用结果,请重试。 | `重新生成` | | `unknown` | `{操作}遇到问题` | 请稍后重试;若多次出现,请提供任务编号以便排查。 | `重新生成` / `复制任务编号` | ### 动作落地规则 1. `重新生成` 仅在当前页面本来就拥有对应提交能力时显示;失败卡、通知中心等没有安全重跑上下文的地方不补造一个按钮。 2. `去修改` 关闭提示后回到当前可编辑的描述/素材区域,不跳到无关页面。 3. `去充值` 仅用于 `user_credit_insufficient`,跳转现有账户充值页;上游额度不足绝不展示该动作。 4. `复制任务编号` 只在 `provider_config_error`、`model_unavailable`、`unknown` 这些需要人工排查的类别显示。复制值为 `reference_id`,不附带原始异常。 5. `retry_later` 不是可立即反复点击的“重试”;页面保留失败状态,用户稍后可从原入口重新发起。 ### 本次 402 的最终前台表现 | 项目 | 值 | | --- | --- | | 分类 | `provider_quota_exhausted` | | 标题 | `脚本暂时不可用` | | 说明 | `生成服务正在处理中,请稍后再试。` | | 页面动作 | 不显示“去充值”或即时“重新生成”;用户稍后可从原脚本入口再试 | 这条文案将同时用于脚本聊天流中的失败消息、任务失败通知摘要和后续脚本失败卡;视觉呈现沿用各页面现有 Toast、失败卡或聊天消息组件,不在本 TODO 内新增一套提示组件。 ## 6. 后续实施草案(确认后再开始) ### Step 1:盘点错误出口与现有展示 - 罗列 SSE、同步 API、异步任务轮询、通知中心的失败返回格式。 - 收集当前原始错误的来源与已存在的友好提示函数,避免新增第二套分散映射。 - 形成“原始错误 → 分类码 → 用户操作”的覆盖表。 ### Step 2:定义后端错误契约 - 为生成失败返回增加稳定 `error_code`、`retryable` 与安全的阶段信息。 - 原始错误只写入任务记录/日志;API 不再直接把原始第三方异常拼进普通用户文案。 - 先覆盖上表首批分类,并为无法识别的异常回退 `generation_failed_unknown`。 ### Step 3:前端统一提示层 - 在一个共享位置维护 `error_code → 标题 / 描述 / 操作`,页面不各自匹配英文错误字符串。 - SSE、任务轮询、Toast、失败卡片统一消费该结果。 - 根据 `retryable` 决定是否展示重试;将“返回修改”“充值”“联系支持”等作为明确动作而非泛用按钮。 ### Step 4:回归与验收 - 用代表性错误覆盖脚本、图片、故事板、视频与配音至少各一条失败路径。 - 验证用户提示不泄露技术细节,管理员仍可通过任务 ID 查到原始错误。 - 验证失败仍会释放预扣积分,且重复点击不会引入重复扣费。 ## 6.1 实际接入顺序与每步验收(已确定,待实施) 按“先共享基础、先本次故障入口、再异步任务、最后通知”的顺序实施。每一个 Step 可独立提交、独立验证;未完成的链路继续沿用旧行为,不会影响非 AI 模块。 | Step | 接入范围 | 预计改动边界 | 核心验收 | | --- | --- | --- | --- | | 1 | 纯后端归类器 + 前端 AI 共享映射 | 新增 AI 域内小模块;不改任何接口调用点 | 用模拟异常验证所有 `code → action`;不请求真实供应商 | | 2 | **脚本 Agent**:SSE + 流式聊天消息 | `script_agent.py`、脚本 SSE 类型、`pipeline.tsx` 的脚本消息处理 | HTTP 402 只显示“脚本暂时不可用”;SSE 不含 URL/模型/英文异常;失败释放预扣积分 | | 3 | 图片创作、基础素材、三视图与任务轮询 | AI 任务失败写入处、图片状态接口、工作台/流水线失败卡 | 原始异常不进入普通用户轮询响应;内容审核、素材格式、用户积分不足三类动作正确 | | 4 | 故事板与项目视频 | 故事板/视频失败写入处、项目序列化/提交响应、流水线失败展示 | 单场重跑仍可用;审核、限流、服务异常分别显示正确建议;不改变版本/采用逻辑 | | 5 | 自由创作视频、配音、实体提取 | 将现有 `video_errors.py` 兼容接入共享归类器;补齐异步任务出口 | 不回归已上线的视频审核文案;轮询、失败卡和通知使用同一分类 | | 6 | AI 失败通知与管理员排障 | 通知写入、管理员任务详情 | 普通用户通知只显示安全文案;管理员仍可查原始错误与任务信息 | | 7 | 全链路回归与收口 | 测试、文档、前端人工验收 | 所有 AI 入口覆盖;非 AI 接口返回与提示不变 | ### Step 1 的最小实现形态 后端仅新增纯函数/数据结构,例如 `apps/ai/generation_errors.py`;前端仅新增 `src/generation-error.ts`(名称以实际目录规范为准)。二者不直接发请求、不写数据库、不引用页面组件。 ```text 原始异常 / 上游响应 ──后端归类器──> PublicGenerationError PublicGenerationError ──前端映射器──> 标题、说明、动作配置 ``` 这一步完成后仍没有用户界面变化,因此可先通过单元测试锁定契约,再进入 Step 2 接入脚本。 ### 每步共同的回归闸门 1. 不发起真实模型调用,不产生供应商费用。 2. 失败任务仍按当前逻辑释放预扣积分;成功链路的扣费、版本与资产落库不变。 3. 普通用户响应、SSE、失败卡与通知不得包含 URL、密钥、供应商原始错误或堆栈。 4. 管理员排障仍可从任务 ID 查到原始错误;不可仅留友好文案而丢失根因。 5. 非 AI 路由、全局 `request` 封装、登录、上传、项目 CRUD 均不改动。 ### 本轮建议的停止点 先实施并验收 **Step 1 + Step 2(共享基础 + 脚本 Agent)**,解决当前 402 暴露问题后暂停确认。图片、故事板、视频等后续链路不在同一批改动中混入,避免放大回归面。 ### 实施进度 - **Step 1 已完成(2026-07-15)**:新增后端纯归类器 `apps/ai/generation_errors.py` 与前端纯映射器 `src/generation-error.ts`;二者均未接入页面、路由、任务写入或真实模型调用。 - 后端新增 `apps.ai.test_generation_errors`,以模拟的 402、审核 403、用户积分、限流、模型不可用、输入错误、超时和 5xx 异常验证归类。结果:`6 / 6` 通过。 - 前端已通过 TypeScript 类型构建(`tsc -b`)。 - **Step 2 已完成(2026-07-15)**:脚本 Agent 的 SSE `error` 帧现返回安全的 `detail + error` 对象;`pipeline.tsx` 仅按共享 `error.code` 映射聊天气泡文案,不再展示上游的 HTTP 状态、URL、模型或英文异常。 - 本次 402 的前台聊天提示为:`脚本暂时不可用:生成服务正在处理中,请稍后再试。` 原始异常仍保留在失败任务记录中供管理员排障。 - Step 2 回归:后端模拟测试 `7 / 7` 通过(新增 SSE 不泄露 YunQi URL / 402 的断言),前端 TypeScript 类型构建通过;未调用真实模型、未产生供应商费用。 - 浏览器自动化连接在本机初始化失败,本轮未取得页面截图;代码层已按既有 AI 聊天气泡和设计规范自检,无新增 CSS、颜色、圆角或按钮。 - **Step 3 已完成(2026-07-15)**:图片创作、基础素材与三视图共用的异步任务轮询接口现返回安全 `error + error_message`;图片工作台历史记录、对话历史与当前轮询均不再回传原始供应商异常。基础素材、三视图提交时的同步失败也返回同一安全对象。 - 三视图单独使用操作名 `triview_generate`,因此用户会看到“三视图暂时不可用”等准确文案,不会被泛化为“基础素材”。图片/基础素材/三视图继续复用既有失败卡、Toast、轮询和任务状态机,无新增视觉组件或 CSS。 - Step 3 回归:后端模拟测试 `9 / 9` 通过,前端 TypeScript 类型构建通过;未调用真实模型、未产生供应商费用。 ## 7. 待讨论决策 1. `provider_quota_exhausted` 是否统一表述为“生成服务暂不可用”,还是允许在内部/管理员可见页面说明“供应商额度不足”? 2. 普通用户是否需要“复制任务编号 / 联系支持”入口,还是仅在通知中心保留失败记录? 3. 对可重试失败,是否需要前端自动重试?本 TODO 默认不做自动重试,避免在供应商限流/额度异常时放大请求。 4. 是否把该错误分类契约同时用于邮件、站内通知和后台任务列表?推荐统一使用,但可分阶段接入。 ## 8. 非目标 - 不在本 TODO 内更换 YunQi、充值供应商账户、修改模型选择或切换模型路由。 - 不改变用户积分定价、预扣、扣费、退费或任务调度策略。 - 不把完整供应商报错、URL、模型密钥或堆栈暴露给普通用户。 - 未确认前不修改任何前后端业务代码。 ### Step 4 实施进度(2026-07-15) - **故事板与项目视频已完成接入。**故事板生成、项目视频提交、视频远端轮询失败时,任务记录仍保留原始异常用于管理员排障;页面使用完整的安全失败文案,例如“故事板暂时不可用:生成服务正在处理中,请稍后再试。” - 既有单场重跑、视频版本、状态胶囊和失败卡的交互结构不变;本步未新增 CSS、颜色、圆角或按钮。 - 通知中心仍保留原始错误正文,按既定边界留到 Step 6 统一收口,避免本步扩大改动范围。 - 验证:错误分类定向测试 `10 / 10` 通过;前端 TypeScript 构建通过;`git diff --check` 通过。全量相关回归 `87` 项中 `86` 项通过,唯一失败为既有三视图功能开关测试:用例期望默认关闭返回 `409`,实际环境返回 `202`,与本次提示优化路径无关,未为通过测试修改开关行为。 ### Step 5 实施进度(2026-07-15) - **自由创作视频、配音与脚本信息提取已完成接入。**自由创作视频沿用原有任务卡和重试按钮,但不再把火山错误正文写入页面;配音入口不再泄露语音服务配置或上游异常;实体提取的异步轮询现在返回安全 `error + error_message`。 - 自由创作视频将原始供应商错误保留在任务记录,并用共享 `error` 对象投影给列表、轮询和失败卡;旧的 `video_errors.py` 不再承担用户端文案职责,避免两套映射继续分叉。 - 前端复用既有失败卡、Toast 和提取失败区域,不新增 CSS、颜色、圆角或操作按钮;错误对象存在时统一按前端映射展示标题和说明。 - 验证:`apps.ai.test_generation_errors + apps.ai.test_free_video` 共 `45 / 45` 通过,`apps.projects.tests` 共 `46 / 46` 通过,前端 TypeScript 构建通过;测试全程使用模拟供应商,不产生真实调用或费用。 ### Step 6 实施进度(2026-07-15) - **通知中心与管理员排障已完成收口。**新的 AI 失败通知正文、摘要和 metadata 仅保存安全文案与稳定 `generation_error` 对象;不再写入供应商 URL、HTTP 状态、原始消息或 `api_error`。 - 历史通知的普通用户 API 也会在序列化时剥离旧正文中的“第三方服务商 API 返回的原始报错”区段,并移除历史 `api_error` metadata;不修改数据库历史记录。 - 管理员任务详情继续读取 `AITask.error_message`,因此可由通知中的任务编号定位原始错误,不需要新增后台页面、权限或数据库迁移。 - 验证:新增通知安全测试;通知定向测试 `5 / 5` 通过,相关生成、项目与通知回归共 `96 / 96` 通过,前端 TypeScript 构建通过。测试确认普通用户通知不含 YunQi URL / 402,管理员任务详情仍可见原始错误。 ### Step 7 实施进度(2026-07-15) - **全链路回归与收口验收完成。**脚本、图片、基础素材、三视图、故事板、项目视频、自由创作视频、配音、脚本信息提取、通知中心和管理员任务详情均纳入同一轮本地回归。 - 为消除测试环境对本机三视图开关的继承影响,既有“三视图关闭时返回 409”测试已显式设置关闭前置条件;仅修改测试确定性,不改运行时代码或功能开关。 - 验证:后端完整相关回归 `208 / 208` 通过,前端 TypeScript 构建通过,`git diff --check` 通过;所有测试使用模拟供应商,不产生真实模型调用或费用。 - 视觉层未新增组件、CSS、颜色、圆角或交互;失败提示继续复用现有聊天气泡、失败卡、Toast 与消息中心样式。