从一个 loading 到完整状态机:多模型图片编辑流程的前端建模

做一个最小图片生成页面时,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 是本文的具体实践背景。真正可复用的不是某个页面,而是这组建模原则:能力由契约驱动,异步过程由状态机表达,用户投入由恢复策略保护,结果由复核状态收口。

相关推荐
JaydenAI2 天前
[TypeScript学习笔记-11]创建一个极简的monorepo项目
typescript·monorepo
濮水大叔2 天前
舒服了,CabloyJS 的 AI Spec 驱动开发会自动生成甘特图和燃尽图
typescript·node.js·vibecoding
DreamLife☼4 天前
Agent开发环境搭建完全指南
python·docker·typescript·node·工业知识点
码艺-Alimjan4 天前
Tauri 2.x + Vue 3 桌面应用开发实战:从踩坑到完美落地
前端·javascript·vue.js·rust·typescript·go
大模型丫丫5 天前
用 TypeScript、NestJS 和 LangGraph 搭建一个可控的 AI Agent
javascript·人工智能·typescript
shmily麻瓜小菜鸡5 天前
JavaScript / TypeScript 易踩坑知识点 —— 异步编程类
开发语言·javascript·typescript
妙码生花5 天前
使用git更新ai-go-admin框架
前端·人工智能·git·golang·typescript·php