# 多张图片生成技术实现记录 ## 背景 本功能在 `TEST-s` 分支中为底部生成对话框增加类似 libTV 的图片张数选择能力。用户可以在图片生成模式下选择 `1张`、`2张` 或 `4张`,一次生成得到多张候选图。 产品语义采用“单节点多候选图”:一次多图生成只创建或更新一个 `nanoBanana` 图片节点,多张结果进入该节点的轮播历史,当前选中的图片作为节点输出。 核心文件: - `src/components/composer/GenerationComposer.tsx` - `src/store/execution/nanoBananaExecutor.ts` - `src/types/nodes.ts` - `src/store/utils/nodeDefaults.ts` - `src/components/__tests__/GenerationComposer.test.tsx` - `src/store/execution/__tests__/nanoBananaExecutor.test.ts` - `src/store/utils/__tests__/nodeDefaults.test.ts` ## 当前功能范围 已实现: - 底部生成对话框在图片模式下显示张数下拉。 - 张数选项固定为 `1张`、`2张`、`4张`。 - 新建图片节点时写入 `imageCount`。 - 编辑已有图片节点时可以修改 `imageCount`。 - `nanoBanana` 执行器按 `imageCount` 顺序请求现有 `/api/generate`。 - 多张结果写入同一个节点的 `imageHistory`。 - 本次生成的第一张图写入 `outputImage`,并作为当前选中输出。 - 每张成功结果都会进入全局图片历史。 - 每张成功结果都会追加到下游 `outputGallery`。 - 自动保存路径启用时,每张成功结果都会独立保存。 - 如果批次中前几张成功、后续失败,保留已成功候选图并把错误记录在节点数据中。 未实现: - 不使用 provider 原生 `n` 参数。 - 不自动创建多个图片节点。 - 不把当前切换到的候选图自动作为下一次生成的参考图。 - 不提供“展开为多个图片节点”动作。 - 不提供“以当前图继续生成”动作。 ## 状态模型 图片生成张数由 `NanoBananaNodeData.imageCount` 表示: ```ts export type ImageGenerationCount = 1 | 2 | 4; export interface NanoBananaNodeData extends BaseNodeData { imageCount?: ImageGenerationCount; } ``` 字段为可选字段,旧工作流没有该字段时按 `1` 处理。 新建 `nanoBanana` 节点时默认写入: ```ts imageCount: 1 ``` 底部对话框内部草稿 `ComposerDraft` 同步维护同名字段,避免用户在提交前切换节点或刷新草稿时丢失选择。 ## 底部对话框 `GenerationComposer` 在图片能力模式下显示三个控制: ```text 模型 / 画幅比例 / 清晰度 / 张数 ``` 张数下拉定义为: ```ts const IMAGE_COUNTS: ImageGenerationCount[] = [1, 2, 4]; ``` 提交新节点时,`buildInitialDataForNode("nanoBanana", draft)` 会把 `draft.imageCount` 写入节点数据。 编辑已有图片节点时,`imageComposerAdapter.readDraft()` 从节点读取 `imageCount`,`buildPatch()` 在字段变脏时回写 `imageCount`。 ## 运行逻辑 `executeNanoBanana()` 不改变 provider 请求协议。执行器会根据节点的 `imageCount` 做顺序循环: ```text imageCount = 1 -> 请求 1 次 imageCount = 2 -> 请求 2 次 imageCount = 4 -> 请求 4 次 ``` 每次请求仍然使用现有 `/api/generate` payload,包括: - `images` - `prompt` - `aspectRatio` - `resolution` - `selectedModel` - `parameters` - `dynamicInputs` 这样可以兼容当前 PopiArt Nano Pro / NewApiWG / Gemini native 链路,因为该链路目前稳定返回单张图片。 ## 结果写入 每张成功结果会生成一个历史项: ```ts { id, image, timestamp, prompt, aspectRatio, model, modelDisplayName, modelProvider, } ``` 本次批量结果按生成顺序插入到 `imageHistory` 前面: ```text [本次第1张, 本次第2张, 本次第3张, 本次第4张, ...旧历史] ``` 节点输出规则: - `outputImage` 使用本次第 1 张。 - `selectedHistoryIndex` 设为 `0`。 - 节点内已有轮播控件负责切换候选图。 - 切换候选图时,下游读取的是当前 `outputImage`。 ## 输出画廊 如果图片生成节点连接到 `outputGallery`,本次成功的每一张候选图都会调用: ```ts appendOutputGalleryImage(target.id, image) ``` 因此画廊可以直接看到整批候选图,而普通下游节点仍然只读取当前选中的 `outputImage`。 ## 失败处理 执行器的失败策略: - 第一张就失败:节点进入 `error`,如果有 fallback model,则走现有 fallback 流程。 - 前几张成功、后续失败:节点进入 `complete`,保留成功候选图,并把错误写入 `error`。 - 用户取消:继续沿用 `AbortError` 逻辑,不提交未完成批次。 第一版没有做并发生成,顺序生成更容易保证取消、fallback、历史顺序和保存逻辑一致。 ## 参考图语义 多张候选图不会改变参考图来源。重新生成仍然使用: - 上游连接图片 - 节点已有 `inputImages` - 当前 prompt - 当前参数 - 当前 `imageCount` 用户切换到第 2/4 张候选图,不会自动把这张图变成下一次生成的参考图。后续如需迭代能力,应单独设计显式动作,例如“以当前图继续生成”。 ## 测试覆盖 当前相关测试: ```text src/components/__tests__/GenerationComposer.test.tsx src/store/execution/__tests__/nanoBananaExecutor.test.ts src/store/utils/__tests__/nodeDefaults.test.ts ``` 覆盖点: - 新建图片节点会保存用户选择的 `imageCount`。 - 默认 `nanoBanana` 节点数据包含 `imageCount: 1`。 - `imageCount=4` 时执行器请求 4 次。 - 多张结果写入同一个节点历史。 - 多张结果中的第一张成为 `outputImage`。 - 批次中后续失败时保留已成功候选图。 - 下游 `outputGallery` 会收到每张候选图。 建议验证命令: ```bash npm run test:run -- src/components/__tests__/GenerationComposer.test.tsx src/store/execution/__tests__/nanoBananaExecutor.test.ts src/store/utils/__tests__/nodeDefaults.test.ts ``` ## 维护注意点 - 不要把 `imageCount` 扩展为任意数字,当前产品选项只允许 `1 | 2 | 4`。 - 如果未来接入 provider 原生多图参数,需要保留当前循环实现作为兼容 fallback。 - 如果未来新增“展开为多个图片节点”,应作为显式用户动作,不应改变默认生成行为。 - 如果未来新增“以当前图继续生成”,应明确写入 `inputImages`,不要让普通轮播切换隐式改变参考图。 - 自动保存和输出画廊需要继续按“每张成功图片一次写入”维护。