五月中旬写项目计划时,我把这件事分成基础目标、核心目标、演示目标,以及这一期明确不做的部分。那份计划创建于 5 月 13 日晚上,博客里的旧文章对象建于第二天;公开仓库到 6 月 12 日才建立,本文能够核对的实现则停在 6 月 14 日的提交。
这些日期不能顺手连成一段“按计划完成”的经历。五月的文档只记录当时准备解决什么,六月的代码才说明后来写进仓库的内容。把两层材料分开以后,变化反而更清楚:最初的目标是生成并展示一个模型,后来的实现花了更多力气处理参考图确认、任务状态、资源门禁,以及一次生成中断后还能从哪里继续。

可恢复的 3D 生成链路:先确认参考图并保存任务状态,再按远端记录和资源条件选择续接、重提或轻量着色。
图:依据公开提交 4ce0bf1d 整理的自制流程图。“复用参考图”和“轻量着色”由不同失败条件触发,两者不构成固定顺序。
参考图要先停下来检查
最短的产品描述可以写成“输入文字,得到 3D 模型”。实际把链路拆开,中间还会经过一张参考图。图片生成或上传以后不会立刻送去建模:界面先把它放进待确认状态,要求检查是否只有一个主体、剖面是否清楚、四周是否留出空间、结构方向是否合适。只有点过“接收图片”,确认建模的操作才会开放;服务端也会拒绝没有有效 referenceId 的自部署任务。
这一步增加了一次停顿,却把成本较低的判断放到了重任务之前。主体被截断、剖面含糊或构图不适合图生 3D 时,先退回输入,比等模型跑完以后再解释结果更直接。这里的人工确认只是输入门禁,不承担生物结构准确性或教学适用性的审核。
确认过的图片还会成为恢复任务时的锚点。参考图保存在独立目录,它的 ID 同时写进任务记录。远端状态中断以后,只要这份输入还在,后续路径仍能找到原先接受的图片,不必重新生成一张随机结果,再把它误当成同一个任务的起点。
任务状态不能只留在页面上
三维生成要等待服务、进入队列、执行工作流并产生输出,每一段都有可能中断。页面若只剩一个旋转图标,刷新以后甚至无法判断请求是否已经提交。
六月的实现把任务快照写进本地 JSON,把状态变化逐行追加到另一份 JSONL。任务保存 queued、processing、completed 或 failed 等状态,也记录当前阶段和进度。快照用来回答“任务现在在哪里”,事件轨迹则帮助区分它怎样走到这里:请求可能还没有离开本机,也可能已经取得远端 ID,只在拉取输出时失败。两种情况最后都可能显示 failed,后续处理却完全不同。
重任务还经过一个单机保护队列,默认串行执行,并且最多只保留一个等待任务。远端 ComfyUI 已经繁忙,或者队列状态暂时不可观测时,新提交会被拦下。这个默认值可以由环境变量改变,因此它只是一层部署内的资源保护,不能据此推断系统具备固定吞吐量。
恢复能力也有明确时限。服务启动时,自动恢复默认只选择最近 3 小时内更新的 queued 或 processing 任务;self-host 任务手动续接时,还需要有效的 providerJobId 等字段,默认窗口是最近 24 小时。文件超过窗口后可能依然留在磁盘上,却已经不满足相应的恢复条件。这套存储适合单机进程重启后的有限续接,解决不了多实例竞争或跨机器一致性。
prompt_id 是一次提交的回执
任务送进 ComfyUI 后,远端会返回一个 prompt_id。代码在开始轮询 history 之前,就把它写成任务的 providerJobId。这个保存顺序很重要:服务若在轮询期间重启,本地仍知道上一轮计算对应哪个远端提交。
进入续接路径后,系统先检查本地缓存的 history,再按当前、上一次或已恢复的 prompt ID 查询远端 history 与 GLB。只要其中仍有可用输出,任务便沿用原输入完成收尾,这条路径不会重新生成参考图。
prompt_id 是一次提交留下的回执,无法作为永久凭证。远端 history 可能被清理,进度也可能暂时无法取得;但在记录尚存时,这个 ID 能把本地任务和远端计算对应起来,让系统知道此刻是在取回已有结果,还是准备发起一次新的计算。
history 消失以后,只允许重提一次
本地还没缓存到有效输出,而 ComfyUI 的 history 又已经消失时,继续轮询旧 ID 不会得到 GLB;无条件重新提交,则可能形成难以察觉的循环。
代码给重提路径加了几项前提:任务仍保留有效参考图,provider 是自部署 TripoSG,此前没有执行过“从参考图重提”,错误也确实落在 history 缺失、队列为空或没有发现 GLB 这一类范围内。条件都满足时,系统才会复用已经确认的参考图,并且只重提一次。
旧的 providerJobId 和 restartFromReferenceAttempted 标记会先保存,再尝试向远端发请求。只有 ComfyUI 接受请求并返回新的 prompt_id,任务记录里才会出现新 ID。请求在这之前失败,留下的仍是旧 ID、错误和阶段;取得新 ID 后再失败,旧、新两次提交都可以用于诊断。先落状态再执行重任务,既限制了循环,也没有抹去失败经过。
到这里,“继续”已经分成三件事:续接是在寻找原先算出的结果;重提是拿同一张确认过的参考图再发起一次计算;降级则是在已有稳定 GLB 上完成更轻的本地处理。它们的成本和结果不同,事件记录必须把三条路径分开,否则一次新提交很容易被误认成旧任务恢复,轻量结果也可能被当成原生贴图产物。
资源不足时,先保住已有几何
贴图阶段暴露了另一条边界:接口允许提交,并不意味着此刻适合提交。工作流在重任务前检查服务状态、可用内存、可用显存和远端队列。资源不足时,它可以先请求释放 ComfyUI 缓存,再做一次复查;条件仍不安全,就不继续挤入 Hunyuan3D-Paint。
如果稳定 GLB 已经存在,fallback-color 会复用这份几何和确认过的参考图,写入轻量纹理、材质或顶点颜色,之后重新校验 GLB 文件。它只保住已经得到的几何和展示链路;失败的重贴图仍然是失败。
因此结果至少要分成三类:原生贴图工作流产出的模型,fallback-color 生成的轻量着色模型,以及只有稳定几何、仍可能显示为白模的结果。三者可以进入同一个查看器,质量口径却不能混在一起。轻量着色没有证明纹理精细,也不会替模型结构做科学审核。
绿色测试回答不了线上效果
8 月 9 日,我从固定提交导出隔离副本重新执行验证:API 测试 109 项全部通过,前端构建成功。它能说明任务状态、恢复判断、资源保护和 GLB 处理等被覆盖的代码契约仍然成立,也说明这个提交能够完成前端生产构建。
本轮没有开启 live smoke,没有提交真实生成任务,也没有验证 provider、GPU、生成质量、耗时、费用或线上服务。测试数字只能放在它实际覆盖的范围里;一条真实生成链路是否可用,仍需要独立的运行验收。
最后留下的是恢复条件
五月的计划试图把输入、生成、缓存和展示接成一条可验收的路径。六月的代码把问题进一步拆细:参考图在哪里由人确认,长任务怎样留下状态,服务重启后哪些任务进入默认 3 小时自动恢复窗口,self-host 任务怎样在默认 24 小时续接窗口内沿 prompt_id 取回结果,history 消失时何时允许一次重提,资源不足时又该保留哪份已有产物。
边界也同样具体。本地 JSON、JSONL 和文件目录仍是单机方案;第三方模型与工作流各有自己的能力和许可;测试与构建通过也没有替真实生成效果背书。若以后扩展到多实例,任务存储、队列所有权、对象存储和幂等策略都需要重新设计,复制当前目录并不能得到一致性。
模型文件记录的是一次运行。输入、远端 ID、停止阶段、失败原因,以及下一步应当续接、重提、降级还是停在资源门禁前,则决定了后续维护从哪里开始。这些信息留得足够清楚,失败任务才不会在下一次打开时又变成一件来历不明的新问题。