Files
yingqing/docs/bug_todo/AI生成失败提示优化-todo.md
T
2026-07-15 13:12:35 +08:00

412 lines
30 KiB
Markdown
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 生成失败提示优化 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": "<task-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
→ 规则 5provider_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 与消息中心样式。