做一个最小图片生成页面时,const [loading, setLoading] = useState(false) 看起来完全够用:点击按钮设为 true,请求结束再设回 false。
当同一入口开始承载背景处理、对象清理、增强、扩图、修复和风格转换后,这个布尔值很快失去表达能力。文件可能仍在解码,选项可能刚因模型切换而失效,任务可能已提交但响应丢失,结果可能已返回却仍等待人工复核。这些状态都不是简单的"加载中"。
先把可用能力当作服务端契约
多模型产品最容易出现的前端债务,是把规则写成大量条件分支:
ts
if (model === "a") hide4K();
if (mode === "fusion") requireMoreImages();
if (!user) disablePremiumMode();
这些判断会逐渐与后端真实能力漂移。更合适的做法是让服务端返回版本化能力文档:
ts
type Capability = {
version: string;
models: Array<{
id: string;
modes: Array<{
id: string;
minSources: number;
maxSources: number;
mimeTypes: string[];
resolutions: string[];
aspectRatios: string[];
requiresAuth: boolean;
creditCost: number;
}>;
}>;
};
界面根据当前"模型 + 模式"分支生成控件。切换父级选项后,仍合法的选择继续保留;失效的分辨率、宽高比或输入数量被替换,同时给出原因。这样既减少硬编码,也不会无声清空用户已经写好的提示词。
用可区分联合类型表达流程
状态至少需要覆盖输入校验、提交、处理、复核和终态:
ts
type EditorState =
| { type: "idle" }
| { type: "validating_sources"; files: File[] }
| { type: "ready"; draft: EditDraft }
| { type: "submitting"; request: EditRequest }
| { type: "processing"; jobId: string; request: EditRequest }
| { type: "reviewing"; jobId: string; resultUrl: string; request: EditRequest }
| { type: "completed"; resultUrl: string }
| { type: "failed"; stage: string; recoverable: boolean; message: string };
这比多个互相独立的布尔值更安全。组件在 processing 状态一定能拿到 jobId,在 reviewing 状态一定同时拥有结果与原请求,类型系统也能提醒我们处理遗漏的分支。
提交前冻结请求快照
不要把仍可编辑的表单对象直接交给异步任务。点击生成时复制不可变快照,包括能力版本、源资源 ID、提示词、模型、模式、分辨率、宽高比和用户确认时的成本。
json
{
"capabilityVersion": "2026-08-22.3",
"sourceAssetIds": ["asset_7f31"],
"prompt": "移除线缆,保留桌面纹理和阴影",
"model": "general-edit-v2",
"mode": "single_edit",
"resolution": "hd",
"aspectRatio": "original",
"quotedCreditCost": 2
}
上传与生成分离后,失败重试可以引用已完成解码和登记的资源。提交请求再带上幂等键,即使服务端创建任务后响应丢失,重试也不会产生第二个任务。
校验为什么必须做两次
客户端预检负责尽快反馈:图片数量、声明类型、实际解码、压缩大小、像素总量与内存风险。服务端负责最终可信:能力版本是否有效、模式是否存在、输入数量是否匹配、输出选项是否属于同一分支、身份是否满足要求、成本是否需要重新确认。
前端传来的报价不能作为扣费依据。它只适合用于发现"用户确认后规则发生变化"这一冲突。
失败状态不要只放一条 message
不同失败需要不同恢复策略:
- 能力更新:刷新契约,保留图片和提示词,只标记失效字段;
- 图片解码失败:把错误绑定到具体文件;
- 登录或积分不足:派发任务前停止,恢复草稿后重新校验;
- 模型服务超时:保留任务 ID,区分未知与确定失败;
- 结果地址过期:刷新已有资源的访问权限,不重新生成;
- 结果质量不合格:保留原请求,让用户修改提示词或模式。
recoverable 也不应该只是装饰字段,它决定界面展示"修改输入""继续查询""重新授权"还是"创建新尝试"。
reviewing 是必要状态,不是成功页动画
背景处理需要检查头发、边缘和阴影;对象清理需要检查重复纹理;增强需要检查人脸、文字和光晕;扩图需要检查透视、光线与重复物体;修复需要检查身份、服装、标识和日期。
后端完成只说明输出存在。因此在 reviewing 中应同时显示原图、提示词、选项快照和生成结果。用户确认后才进入 completed,否则从原请求创建一次有上下文的新尝试。
边界比功能数量更重要
这套状态建模适合意图驱动的在线编辑,但不等于替代精确蒙版、分层合成、像素级修饰和批量生产流程。模型与选项会变化,高分辨率和多图输入不是全局能力,部分操作可能需要登录或积分,文件格式和大小存在限制,所有结果都需要人工复核。
我参与的 AI Photo Editor 是本文的具体实践背景。真正可复用的不是某个页面,而是这组建模原则:能力由契约驱动,异步过程由状态机表达,用户投入由恢复策略保护,结果由复核状态收口。