ai-chatbot/docs/incidents/detail-replacement-durable-batch-2026-09-26.md

66 lines
7.6 KiB
Markdown
Raw Permalink 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.

# 风格化“详情替换”长时间不出结果(2026-09-26)
## 现场证据
用户截图中的风格化项目已保存 33 张目标详情参考图,但输入图区为 0 张、结果图为 0 张。卡片上的 96% 不是服务商进度:`DetailResultProcessingScanOverlay` 按 240 秒把时间线性映射成百分比,并用 `Math.min(96, ...)` 封顶,所以只要没收到结果,它就会一直停在 96%。
生成请求走的是浏览器保持的对话流(`/api/chat` 的 SSE)。切换标签页、断网或连接中断后服务端结果无法回传,前端那条“处理中”的状态又存在浏览器里,重新打开页面会继续播放假进度,因此表现为“挂几个小时”。
另外确认了一个独立故障:详情替换专用模型绑定为 `gpt-image-2.5-flare`,该型号当时持续返回 HTTP 404;普通 `gpt-image-2.5` 有成功出图记录。2026-09-24 17:45 已把线上 `SystemSettings.detail_replacement_image_model_config.modelId` 热更新为 `gpt-image-2.5`。
上线前线上 `CanvasImageTask` 中 `taskKind = 'agent-detail-replacement-batch'` 的行数为 0,即任务从未进过服务端队列,符合“没有可恢复任务、只能挂假进度”的判断。
## 修复
- 详情替换改为服务器端后台批次:`POST /api/agents/detail-replacement/batches` 先落 `CanvasImageTask`,由 `lib/agents/detail-replacement-batch-runner.ts` 按参考图逐张生成,每张结果即时写回项目(`persistAgentDetailReplacementCard`)并更新任务进度。
- 页面不再依赖长连接:卡片状态来自任务查询(3 秒轮询 + 回到前台重新校验),刷新、切换标签页、换浏览器都能按任务状态恢复;没有后端批次时不再显示“处理中”。
- 删除按时间爬到 96% 的假进度动画,进度改为真实的排队 / 生成中 / 已完成 / 失败。
- 提交前校验:没有产品输入图直接拦截提示补图;参考图必须与项目已保存的一致;配额与积分不足在提交阶段返回明确错误。
- 服务重启恢复:`instrumentation.ts` 启动 5 秒后以及之后每 60 秒调用 `resumeDetailReplacementBatchTasks()`,把 `pending` 或心跳停滞(>90 秒无 `updatedAt`)的批次重新入队;运行期间每 30 秒写心跳。
- 长批次不再被清理规则误判:详情替换的运行超时单独放宽到 3 小时,`markStaleDatabaseRunningTasksFailed` 与 `getCanvasImageTaskForUser` 都排除该任务类型。
- 单张失败只落到该卡片并继续后续图片;已写入项目的卡片在恢复时按批次号 + 卡片号认领,不重复调用模型、不重复记账。
- 去重按“同一份提交内容”而不是“同一项目”:`submissionKey` 覆盖参考图集合、提示词、模型、尺寸等,配合局部唯一索引 `CanvasImageTask_detail_batch_submission_key` 拦住重复点击;单张重新生图因为卡片集合不同,不会被吞掉。
- 详情替换默认模型由 `gpt-image-2.5-flare` 改回 `gpt-image-2.5`(`lib/ai/detail-replacement-image-model-config.ts` + 迁移 `0101`),并保留前端传参路径不变。
## 验证
- 服务端链路端到端验证(生产库,纯沿用卡片,不调用模型、不消耗积分):直接插入 `pending` 批次后,恢复循环在 60 秒内接手,任务转为 `succeeded`,两张卡片写入项目 `replacementResultState.detail_replacement`,聊天记录写入 2 条对应结果消息;验证数据随后全部删除,线上无残留。
- 重启幂等验证:把同一任务改回 `pending`,恢复后 `attempts` 1 → 2,已完成卡片被跳过,结果卡片与消息条数不变,确认不会重复生成、重复记账。
- 本地 `tsc --noEmit`、Next.js 生产构建、`git diff --check` 通过;`tests/routes/detail-replacement-batch.test.ts` 与 `tests/routes/replacement-product-identity.test.ts` 共 12 项通过,覆盖批次识别、卡片消息 ID 稳定性、重启跳过已完成卡片、提交键去重与单张重跑区分。
## 发布
2026-09-26 02:45 发布至 45.192.99.48 / ai.yydd.ai。生产代码提交 `0907aaff`,构建目录 `.next-detail-batch-0907aaff-20260926T023939`(BUILD_ID `0wcSfql6ZB2g8EzNEP6Gy`)。切换 `.next` 符号链接后 `pm2 restart ai-chatbot`。
回滚点记录在服务器 `/root/detail-batch-rollback.txt`:旧提交 `1c2acee5`、旧构建 `.next-video-home-439af60c-20260924`。回滚只需把 `.next` 指回旧目录并重启。
服务器构建使用 `NEXT_IGNORE_BUILD_TYPECHECK=1`:该机器上遗留的 `backups/next-before-vip98k-0911/types/validator.ts` 会被 `tsconfig` 的 `**/*.ts` 扫到,它的相对导入在该目录下必然失效;同一提交已在本地跑通完整类型检查与构建。
线上检查:登录页 200、首页 200、未登录访问 `/api/agents/detail-replacement/batches` 返回 401;数据库去重索引已建,`CanvasImageTask` 无遗留批次。分支 `codex/detail-recovery-0924` 已推送 Git。
发布时经确认近 3 小时无任何生成任务、近 1 小时无新消息,重启未打断在途生成。
## 追加:上线后详情替换完全不可用(2026-09-27)
现场:用户点“参考图替换”弹出「任务编号或项目编号无效」,结果区一直是 0/33。nginx 日志显示同一个用户对 `/api/agents/detail-replacement/batches` 的请求全部 400——GET 返回 30 字节(`{"error":"项目编号无效"}`),POST 返回 45 字节(`{"error":"任务编号或项目编号无效"}`),且 GET 请求里的 projectId 是合法 UUID。
根因:新增批次路由自带的 UUID 正则漏写一组 `-{4}`,写成了 8-4-4-12,而标准 UUID 是 8-4-4-4-12。结果是**任何**真实编号都过不了校验,包括浏览器 `generateUUID()` 生成的 requestId,所以查询和提交在参数校验阶段就全部返回 400,页面自然拿不到任务状态。仓库里其他位置用的都是正确的 8-4-4-4-12,只有这个新文件写错。
为什么上一轮没发现:上一轮的端到端验证是直接往 `CanvasImageTask` 插入任务再观察恢复循环,绕过了 HTTP 层,而编号校验只存在于 HTTP 层。这是一次验证覆盖不足,不是环境问题。
修复:
- 校验抽成 `isDetailReplacementUuid` 并修正为规范的 8-4-4-4-12,路由不再自带正则。
- 回归测试覆盖真实线上项目编号、浏览器生成的请求编号,以及导致故障的 8-4-4-12 形状;另加源码守卫,禁止该路由再次出现自带的错误 UUID 正则。
- 前端在项目尚未保存时直接提示“请先保存项目后再执行详情替换”,不再把空编号丢给接口。
验证(这次走真实 HTTP,不再是绕过接口的数据库插桩):用一次性 QA 账号(`activeSessionKey` 与 JWT 会话)和一次性 agent 项目请求线上接口,验证后立即删除,不触碰任何真实账号数据。
- `GET ?projectId=<真实项目 UUID>` → 200 `{"task":null}`(修复前恒为 400,即用户遇到的现场)。
- `GET ?projectId=not-a-uuid` → 400「项目编号无效」,编号校验仍然生效。
- `POST` 合法 projectId/requestId → 通过编号校验并进入后续业务校验(返回“详情参考图尚未保存或已发生变化”),说明提交链路已恢复。
- `POST` 非法 requestId → 400「任务编号或项目编号无效」,非法输入仍被拒绝。
- QA 账号、项目、临时脚本全部删除,线上无残留。
发布:2026-09-27 19:16 发布至 45.192.99.48 / ai.yydd.ai,提交 `f75d99ba`,构建目录 `.next-detail-batch-f75d99ba-20260927T191533`(BUILD_ID `f1ghXv5zLVb8BQv8a-ifE`)。回滚点为上一构建 `.next-detail-batch-0907aaff-20260926T023939`。