7.6 KiB
风格化“详情替换”长时间不出结果(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,恢复后attempts1 → 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。