
Civitai 迭代式图像编辑器从漫剧专用弹窗到可复用组件与独立页面的完整拆解【免费下载链接】civitaiA repository of models, textual inversions, and more项目地址: https://gitcode.com/GitHub_Trending/ci/civitai本文基于仓库中的规划文档 plan-iterative-editor-reusable.md 展开讲清楚 Civitai 前端如何把深埋在漫剧Comics工作区里的聊天式图像迭代编辑器抽离为一个与业务无关的通用组件IterativeImageEditor并落地为独立页面/images/iterate。读完你可以掌握如何把紧耦合业务弹窗按泛型核心 薄封装原则拆解、用回调函数抽象掉 tRPC 调用、信号驱动的生成结果轮询如何与兜底机制配合以及配套服务端端点与共享轮询工具的实现细节。背景与目标Civitai 的迭代式图像编辑器chat-based image refinement即通过聊天消息驱动的图片精修最初是漫剧工作区里的一个弹窗组件IterativePanelEditor.tsx与漫剧概念panel、chapter、reference 解析深度耦合。规划文档提出的两个目标是独立页面编辑器可访问于/images/iterate可复用组件对任意图片可用而不只是漫剧画格。规划的核心判断是聊天历史、迭代消息、撤销、批注、轮询、信号这些能力天然通用而漫剧特有的部分引用解析、画格 CRUD、项目/章节上下文应当被隔离到一个薄封装里。这个判断在仓库当前代码中得到了完整印证——通用核心位于 src/components/IterativeEditor/独立页面位于 src/pages/images/iterate.tsx共享轮询工具位于 src/server/services/orchestrator/poll-iteration.ts。总体架构规划文档给出的架构图如下它准确描述了最终落地的形态┌─────────────────────────────────────────────────────┐ │ Generic Core: IterativeImageEditor │ │ (src/components/IterativeEditor/) │ │ │ │ - Chat history iteration messages │ │ - Prompt input (optional mentions) │ │ - Annotations via DrawingEditorModal │ │ - Model/aspect ratio/enhance controls │ │ - Polling signal-based fast updates │ │ - Keyboard shortcuts, confirmation dialog │ │ - Configurable via callbacks config object │ ├──────────────────┬──────────────────────────────────┤ │ Standalone Page │ Comic Wrapper (modal) │ │ /images/iterate │ IterativePanelEditor.tsx │ │ │ │ │ Uses orchestrator│ Uses comics.iterateGenerate │ │ endpoints │ Passes comic references │ │ Generic commit │ Commits to panel on close │ └──────────────────┴──────────────────────────────────┘架构上有一条贯穿始终的原则组件不直接依赖 tRPC。所有与后端的交互发起生成、查询状态、提交结果都通过onGenerate/onPollStatus/onCommit三个回调注入组件本身只定义入参和返回值的类型契约。这样独立页面用 orchestrator 端点、漫剧侧用 comics 端点的切换只发生在各自的封装层核心组件一行不改。文件组织新增与改造规划文档列出的文件清单如下。以当前仓库对照新增文件均已落地Comics 目录保留的文件退化为 re-export 转发层如 src/components/Comics/IterationMessage.tsx 全文仅是对~/components/IterativeEditor/IterationMessage的再导出保证既有导入路径不破坏。新增文件文件说明src/components/IterativeEditor/IterativeImageEditor.tsx通用核心组件实际约 1700 行超出规划估计的 600 行因为后续迭代加入了信号轮询、NSFW 处理、多图片结果等能力src/components/IterativeEditor/IterativeImageEditor.module.scss样式含page/modal两种 mode 变体src/components/IterativeEditor/IterationMessage.tsx从 Comics 迁移的迭代消息组件src/components/IterativeEditor/AspectRatioSelector.tsx从 Comics 迁移原本就完全通用src/components/IterativeEditor/MentionTextarea.tsx从 Comics 迁移通用接收referencespropsrc/components/IterativeEditor/iterative-editor.types.ts共享类型SourceImage、IterationEntry、IterativeEditorConfig等src/pages/images/iterate.tsx独立页面src/server/services/orchestrator/poll-iteration.ts共享轮询逻辑从 comics 路由抽取统一导出入口 src/components/IterativeEditor/index.ts 将组件与全部类型一次性暴露给消费方。改造文件文件变更src/server/routers/orchestrator.router.ts新增iterateGeneratemutation 与pollIterationStatusquery通用不含漫剧概念src/server/routers/comics.router.tspollIterationStatus重构为直接调用共享工具pollIterationWorkflowsrc/components/Comics/AspectRatioSelector.tsx、src/components/Comics/MentionTextarea.tsx、src/components/Comics/IterationMessage.tsx退化为从新位置的 re-export从源码结构看漫剧侧的旧弹窗文件IterativePanelEditor.tsx已不在仓库中以该文件名存在漫剧项目内的迭代编辑入口目前落在漫剧项目页 src/pages/comics/project/[id]/iterate.tsx与 comics 路由配合使用同一套共享轮询工具。类型契约组件与后端之间的边界iterative-editor.types.ts 是整个抽象的核心值得逐一看。核心数据结构// src/components/IterativeEditor/iterative-editor.types.ts节选 export interface SourceImage { url: string; previewUrl: string; width: number; height: number; /** * Bitwise NSFW level reported by the orchestrator for the generation * output. Used to decide whether to blur the result against the users * blurLevels preference before the Image records nsfwLevel is finalized. */ nsfwLevel?: number; } export interface IterationEntry { id: string; prompt: string; enhancedPrompt?: string | null; annotated: boolean; sourceImage: SourceImage | null; resultImage: SourceImage | null; /** All generated images (when quantity 1) */ resultImages: SourceImage[]; cost: number; timestamp: Date; status: generating | ready | error | siteRestricted; errorMessage?: string; workflowId?: string; width?: number; height?: number; }几个设计点SourceImage携带nsfwLevel位域字段。注释说明得很清楚这个值来自 orchestrator 的生成输出用于在 Image 记录的 nsfwLevel 被异步入库流程最终确定之前先按用户blurLevels偏好决定是否打码——避免成熟内容在客户端先裸奔再打码。IterationEntry.status是四态机generating / ready / error / siteRestricted。其中siteRestricted是规划文档未预见的后续演进——工作流成功但输出在当前站点如 civitai.green不可展示时保留workflowId / width / height以便把用户接力到允许成熟内容的域名而不是丢弃已付费的工作流。IterationEntry.resultImages支持多张结果图quantity 1场景resultImage表示用户当前选中/生效的那一张。配置与回调参数export interface IterativeEditorConfig { modelOptions: { value: string; label: string }[]; modelSizes: Recordstring, { label: string; width: number; height: number }[]; modelMaxImages: Recordstring, number; // 规划后新增每个模型允许的最大图片槽位 defaultModel: string; defaultAspectRatio: string; /** Fallback cost if whatIf query is unavailable */ generationCost: number; /** Fallback enhance cost if whatIf query is unavailable */ enhanceCost: number; commitLabel?: string; } export interface GenerateParams { prompt: string; /** deprecated Enhancement now happens client-side for comics. Still used by non-comic iterative editors. */ enhance?: boolean; aspectRatio: string; baseModel: string | null; quantity: number; sourceImageUrl?: string; sourceImageWidth?: number; sourceImageHeight?: number; selectedImageIds?: number[]; referenceImages?: ReferenceImage[]; } export interface PollParams { workflowId: string; width?: number; height?: number; prompt?: string; }与规划文档相比有两处明显演进IterativeEditorConfig增加modelMaxImages。通用核心内建了图片槽位预算usedImageSlots (有源图?1:0) 用户参考图数 mention 角色图数remainingImageSlots max(0, maxReferenceImages - usedImageSlots)见 IterativeImageEditor.tsx。这使得最多 7 张参考图这类模型级限制成为通用能力而非漫剧专属逻辑。enhance被标注deprecated。提示词增强在漫剧场景改为客户端就地操作enhanceInPlaceprop而通用路径仍保留enhance布尔开关——类型注释明确记录了这条迁移轨迹。此外还有GenerateResult { workflowId; width; height; cost?; enhancedPrompt? }、PollResult { status; imageUrl; imageId?; images?; workflowId?; errorMessage? }以及面向插槽机制的EditorSlotContext / InputSlotProps / SidebarSlotProps见下一节。组件接口回调抽象 插槽扩展IterativeImageEditor.tsx 的 props 定义比规划文档中的最小集更完整。核心仍是四个回调onGenerate: (params: GenerateParams) PromiseGenerateResult; onPollStatus: (params: PollParams) PromisePollResult; onCommit?: (source: SourceImage) Promisevoid | void; onClose?: () void;在此基础上新增了几类演进型能力渲染插槽renderInput/renderSidebarExtra接收InputSlotProps/SidebarSlotProps都扩展自EditorSlotContext暴露prompt / setPrompt / isGenerating / selectedImageIds / effectiveModel / maxReferenceImages。插件可以用自定义输入区替换默认 textarea漫剧侧由此注入带 mention 的MentionTextarea而无需 fork 整个组件。projectReferencesCharacterReference[]全部项目引用组件内部用正则从 prompt 中计算哪些被 mention按名称长度降序匹配避免短名吞掉长名被引用角色的图片计入槽位预算并可经selectedImageIds挑选子集。动态成本估算costEstimate / isCostLoading / enhanceCostEstimate来自后端的 whatIf 计价查询就绪后覆盖config.generationCost的兜底值onSettingsChange回调让父级随编辑器设置变化重新拉取价格。enhanceInPlace漫剧场景用增强并原地改写提示词替换内部增强开关。站点限制接力buildSiteRestrictedUnlockUrl构造跳转到成熟内容域名的 URLinitialPendingWorkflow则支持挂载时直接恢复一个进行中工作流的轮询——用户在 civitai.green 被弹到 civitai.red 后编辑器插一条generating迭代并直接轮询已有 workflowId不再重复扣 Buzz。mode: page | modal与规划一致page为全视口布局modal填满父容器。客户端关键机制信号快通道 兜底轮询 并发防护这是组件实现里最有工程价值的部分规划文档只用一行 Polling signal-based fast updates 带过源码中则是三重保障。1. 信号驱动的快通道。组件通过useSignalConnection(SignalMessages.TextToImageUpdate, ...)订阅 orchestrator 的TextToImageUpdate信号IterativeImageEditor.tsx。只有当前活跃workflowId匹配、且 step 状态进入任一终态succeeded / failed / expired / canceled时才触发一次真实轮询——注释特别说明 expired/canceled 也必须触发否则服务端无法把它们翻译成failed的 PollResult编辑器会一直转圈。2. 20 秒兜底轮询 25 分钟硬超时。信号可能丢失websocket 掉线、服务端未发因此生成期间另有setInterval(() void doPollOnce(), 20_000)IterativeImageEditor.tsx再上层是 25 分钟硬超时——注释说明 orchestrator 生成步的超时约为 21 分钟25 分钟留了缓冲。等待超过 5 分钟DELAYED_WARNING_MS与 Generator 队列阈值对齐还会弹出比平时慢的提示并提供 Stop-waiting 按钮。3. 在途轮询守卫。doPollOnce开头有if (isPollingRef.current) returnIterativeImageEditor.tsx。注释解释了原因20 秒兜底轮询和信号触发轮询在工作流刚结束时可能竞争而pollIterationWorkflow在成功路径上不幂等每次调用都会重新下载输出并创建新的 Image 行两个并发轮询会为同一工作流重复上传图片和重复建 Image 记录。其余交互细节同样落在通用核心内批注handleAnnotateSource打开DrawingEditorModal用户画线后以 JPEG 上传 CloudflareuseCFImageUpload上传结果成为新的currentSource并向聊天插入一条prompt: (annotated)的零成本迭代记录保证操作在对话流里可见、可回溯。回退handleUseAsSource把任意历史迭代结果设为当前源图清空批注handleResetToOriginal用useRef保存的initialSource稳定引用回到最初图片。源图比例自适应pickClosestAspectRatioIterativeImageEditor.tsx按源图宽高比挑选最接近的模型比例避免无论输入什么第一张生成都被压成 3:4。NSFW 处理isImageBlurred判断仅在 green 域名硬打码成熟输出features.isGreen !hasSafeBrowsingLevel(img.nsfwLevel)blue/red 域名允许用户查看自己付费生成的成熟内容。被打码的图片不会自动提升为当前源图因为用户无法基于一张看不见的图继续生成。多结果选择handleSelectImage在多张结果中切换生效图且仅当该迭代本就是当前源图时才联动更新currentSource。服务端两个通用端点 一个共享轮询工具orchestrator.iterateGenerate新增通用orchestrator.router.ts 中iterateGenerate挂在guardedProcedure上声明requiredScope: TokenScope.AIServicesWrite兼容 scoped tokenzod 入参约束完整如下字段约束说明prompt字符串1–2000 字符用户提示词enhance布尔默认true是否服务端增强提示词enhanceComicPromptaspectRatio字符串默认3:4宽高比标签baseModel字符串可空为空时回落到DEFAULT_ITERATE_MODEL预设配置quantity整数 1–4默认 1生图数量sourceImageUrl/sourceImageWidth/sourceImageHeight可选有源图时三件套齐全才生效img2imgreferenceImages可选数组{ url, width, height }已解析好的参考图无漫剧 DB 查询处理流程从源码结构看按baseModel取预设模型配置——有源图时切到img2imgVersionId用pickAspectRatioSize算出返回给前端的尺寸真正提交的尺寸由图按 aspect-ratio 推导源图与参考图统一经getEdgeUrl(..., { original: true })转成 CDN 原图 URL 拼入 images 数组最后调用submitPresetImageGen并按域名打 taggreen 域为[iterate, green]、green 域强制allowMatureContent: false。返回{ workflowId, width, height, cost, enhancedPrompt }——enhancedPrompt仅在增强实际改变了 prompt 时非 null供前端把增强结果回显到聊天。同文件还有getIterateCostEstimatequeryorchestrator.router.ts走whatIfPresetImageGen干跑计价入参与 generate 对齐含 sourceImage 与 referenceImages失败时返回{ cost: 0, ready: false }而不是抛错——这正是CostEstimate { cost; ready }类型里ready字段的来由。orchestrator.pollIterationStatus新增通用orchestrator.router.ts 中入参为{ workflowId, width?, height?, prompt? }直接委托给共享工具pollIterationWorkflow。规划文档原话是 Takes pre-resolved inputs — no comic DB lookups实现严格照此执行。共享工具 pollIterationWorkflowpoll-iteration.ts 是从 comics 路由抽出的唯一事实来源comics.router.ts 的pollIterationStatus现在也调用同一个函数两侧行为天然一致。其判定顺序是理解整个轮询语义的关键取 orchestrator tokengetWorkflow拿工作流详情输出取第一步的output.images ?? output.blobs。站点限制接力工作流或 step成功、无可用 URL、且原始输出中存在blockedReason siteRestricted时返回{ status: siteRestricted, workflowId }——把 workflowId 交还客户端构造接力 URL保住用户已付费的工作流。成功路径workflowStatus succeeded且有输出时对每张图执行downloadAndUploadImagefetch 原图 → 随机 UUID 作为 S3 key →PutObjectCommand上传 →registerMediaLocation登记媒体位置 →createImage落库prompt 写入 Image meta并用normalizeOrchestratorNsfwLevel把 orchestrator 的字符串等级pg / pg13 / r ...映射成位域数字。源码注释强调这个映射的必要性不映射的话客户端对字符串做位运算会得到 NaNPG-13 内容会被误判为成熟内容而错误覆盖去 civitai.red 打开的遮罩。硬失败工作流本身failed / canceled / expired时返回failed错误信息优先取blockedReasonToMessage(blockedReason)否则取 provider 错误extractStepErrorssanitizeProviderError按 engine 脱敏。软失败step 或工作流到达终态但没有产出可用 blob——这是最容易把编辑器卡在转圈上的分支同样返回failed并附定制文案。其余情况返回processing。blockedReasonToMessagepoll-iteration.ts把各种拦截原因翻译成对用户有用的话术例如canUpgrade提示用 yellow Buzz 在 Generator 队列解锁Buzz 是被暂扣不是丢失、NSFWLevelSourceImageRestricted提示源图缺元数据导致只能生成 PG/PG-13——这与漫剧共享同一套队列拦截语义。独立页面 /images/iterateiterate.tsx 只有约 190 行恰好演示了薄封装该长什么样认证门槛createServerSideProps({ useSession: true })无 session 直接 302 到/login对应规划中Auth-gated via createServerSideProps。URL 参数可选?imageUrl...width...height...组装成initialSource缺省 512×512对应规划中Standalone with initial image场景无参数即从零生成。端点接线onGenerate走trpc.orchestrator.iterateGenerate.useMutation()onPollStatus走utils.orchestrator.pollIterationStatus.fetch。注意后者用fetch而非 mutation——轮询是读操作且每次调用都是独立查询。提交onCommit只弹成功通知。因为图片在轮询成功时就已经被服务端下载、上传并建了 Image 记录见上节成功路径提交语义退化为确认 告知。布局Page(IteratePage, { scrollable: false, header: null, footer: null })全视口布局源文件里有一段值得保留的布局注释——外层用flex: 1而非height: 100vh因为该页面是 shellmain的 flex item上方还有SubNav写死 100vh 会让底部被祖先的overflow: hidden裁掉。页面的IterativeEditorConfig实例值得全文列出它是把漫剧模型常量复用为通用页配置的直接证据const DEFAULT_MODEL NanoBanana; const config: IterativeEditorConfig { modelOptions: COMIC_MODEL_OPTIONS, modelSizes: COMIC_MODEL_SIZES, modelMaxImages: COMIC_MODEL_MAX_IMAGES, defaultModel: DEFAULT_MODEL, defaultAspectRatio: 3:4, generationCost: 25, // fallback if whatIf unavailable enhanceCost: 0, commitLabel: Save Image, };常量来自 comic-project-constants.ts动态价格则通过trpc.orchestrator.getIterateCostEstimate.useQuery拉取staleTime: 30skeepPreviousData平滑切换设置变化经onSettingsChange回传更新查询参数——这就是类型注释里 Overrides config.generationCost when ready 的闭环。漫剧侧的收尾re-export 与共享轮询规划要求漫剧侧变成约 80 行的薄封装。仓库中的现状是Comics 目录下的 AspectRatioSelector.tsx、MentionTextarea.tsx、IterationMessage.tsx 全部退化为对新位置的 re-export以 IterationMessage.tsx 为例全文即// Re-export from the generic IterativeEditor location加导出语句旧导入路径零破坏。服务端则完成规划第 8 步comics.router.ts 的pollIterationStatus与 orchestrator 版本逐字段同构并共同委托pollIterationWorkflow。漫剧项目内的迭代编辑入口位于 src/pages/comics/project/[id]/iterate.tsx其onGenerate仍走comics.iterateGenerate携带 projectId、chapterPosition 等漫剧上下文与通用页形成同一核心、不同后端的对照。实施顺序与验证清单规划文档给出的实施顺序与验证项原样保留可作为同类重构的检查单实施顺序类型 常量iterative-editor.types.ts迁移子组件IterationMessage、AspectRatioSelector、MentionTextarea并加 re-export抽取共享轮询工具poll-iteration.ts新增 orchestrator 端点iterateGeneratepollIterationStatus创建通用组件IterativeImageEditor.tsx SCSS重写漫剧薄封装创建独立页面src/pages/images/iterate.tsx重构 comics 路由使用共享工具。验证pnpm run typecheck零新增错误漫剧路径从画格卡片打开迭代编辑器 → 生成 → 提交 → 画格更新独立路径进入/images/iterate→ 从零生成 → 提交 → 图片已保存Image 记录在轮询时即建带初始图/images/iterate?imageUrl...→ 展示源图 → 迭代 → 提交信号快轮询在两种上下文中均生效。小结这个重构的价值不在抽了一个组件而在三层边界的清晰化类型层GenerateParams / PollParams / GenerateResult / PollResult定义了前后端交互的最小契约、回调层onGenerate / onPollStatus / onCommit把 tRPC 细节挡在核心之外、配置层IterativeEditorConfig 插槽让漫剧引用、模型差异、成本策略都成为外部注入。在此基础上信号快通道 兜底轮询 在途守卫的三重轮询策略、siteRestricted 域名接力、NSFW 位域映射这些易错细节都被沉淀在通用核心或共享服务端工具中漫剧页与独立页共享同一套行为。对任何想把业务专用弹窗升级为可复用页面 组件的 Next.js/tRPC 项目这份文档加上述源码路径就是一份可直接对照的参考实现。【免费下载链接】civitaiA repository of models, textual inversions, and more项目地址: https://gitcode.com/GitHub_Trending/ci/civitai创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考