Files
TrueGrowth/docs/IMAGE_GENERATION_API_PARAMS_LESSONS.md

29 KiB
Raw Blame History

图片生成 API 参数兼容性与比例转换修复经验

问题概述

本次修复涉及三个关联问题,全部为项目预存代码问题,非近期修改引入:

  1. rix API 400 错误response_format 参数不被 rix API 支持,导致图片生成失败
  2. 非 1:1 比例全部生成 1:1:用户选择 16:9、4:3 等比例时API 实际生成正方形图片
  3. 图片插入画布后强制 1:1 显示foreignObject 容器始终 400x400竖版长图被压缩

一、架构背景

请求路由架构

用户输入 → AI Input Parser → Workflow Engine → Image Generation Service
                                                    ↓
                                          executorParams (size/quality/...)
                                                    ↓
                                         Fallback Executor
                                               ↙        ↘
                              buildImageRequestBody     resolveAdapterForInvocation
                              (通用请求体构建)          (专用适配器路由)
                                    ↓                        ↓
                              providerTransport.send    gpt-image-adapter.ts
                              → 直接 fetch 到 API       → 专项处理 request body

关键设计点

  • buildImageRequestBody 是统一的请求体构建函数,被 fallback-executor.tsgenerateImageSync 两个路径调用
  • 适配器层gpt-image-adapter.tstuzi-gpt-image-adapter.ts)有独立的 response_format 处理逻辑,只在使用专属适配器时生效
  • 提供商路由provider-transport.ts)直接将请求 fetch 到外部 API 服务商,不做参数转换

二、问题 1response_format 硬编码导致 rix API 报错

错误日志

[TaskQueueService] Task execution failed: Error: Image generation failed: 400 -
{"error":{"message":"Unknown parameter: 'response_format'.",
 "type":"rix_api_error","param":"response_format","code":"unknown_parameter"}}

根因

buildImageRequestBody 无条件向请求体中添加了 response_format: 'url'

// image-api.ts L54-61修复前
export function buildImageRequestBody(params: ImageGenerationParams) {
  const body: Record<string, unknown> = {
    prompt: params.prompt,
    model: params.model,
    response_format: 'url',   // ← 硬编码rix API 不支持
  };

该参数是 OpenAI 官方 API 的专有参数。当 provider-transport.ts 将请求路由到 rix API 提供商时,服务端不识别此参数,返回 400。

修复

// image-api.ts L54-60修复后
export function buildImageRequestBody(params: ImageGenerationParams) {
  const body: Record<string, unknown> = {
    prompt: params.prompt,
    model: params.model,
  };
  // response_format 已移除,由各适配器自行处理

影响范围审计

代码路径 是否受影响 说明
fallback-executor.ts L267 → buildImageRequestBody 已修复 不再发送 response_format
image-api.ts L151 → generateImageSyncbuildImageRequestBody 已修复 不再发送 response_format
gpt-image-adapter.ts L176-179 不受影响 有自己的 getGPTImageResponseFormat 处理,只在用户显式传入时才添加
tuzi-gpt-image-adapter.ts 不受影响 继承 GPT Image 适配器逻辑
image-api.ts L35-48 normalizeImageResultUrl 不受影响 同时支持 urlb64_json 两种返回格式

风险评估

零风险。原因:

  1. OpenAI API 的 response_format 默认值就是 'url',不传等效于传 'url'
  2. parseImageResponse + normalizeImageResultUrl 兼容 urlb64_json 两种格式
  3. GPT Image 适配器有自己的 response_format 处理,不依赖 buildImageRequestBody

三、问题 2非 1:1 比例全部生成 1:1

现象

用户选择 16:9但 API 生成 816x816 正方形图片,下方内容空白。下载后图片确实是正方形。

根因分析:参数链路断裂

用户选 16:9
  → normalizeSize('16:9') → '16x9'
  → ParsedGenerationParams.size = '16x9'
  → step.args.size = '16x9'
  → workflow engine: options.size = '16x9'
  → image-generation-service: executorParams.size = '16x9'
  → fallback-executor: params.size = '16x9'
  → buildImageRequestBody({ size: '16x9' })
  → body.size = '16x9'                    ← BUG比例字符串当作像素尺寸
  → API 收到 size: '16x9'                 ← 无效像素尺寸,回退默认 1:1

buildImageRequestBody 的逻辑缺陷:

// 修复前
if (params.size) {
  body.size = params.size;           // 直接透传,无论内容是比例还是像素
} else if (params.aspectRatio) {
  body.size = aspectRatioToSize(params.aspectRatio);  // 转换只在这个分支
}

上游链路将比例格式('16x9')放进 size 字段,导致 aspectRatioToSize 转换分支被跳过。

全部受影响比例

用户选择 传入 size 期望尺寸 修复前实际
16:9 16x9 1792x1024 1:1
9:16 9x16 1024x1792 1:1
4:3 4x3 1536x1152 1:1
3:4 3x4 1152x1536 1:1
3:2 3x2 1536x1024 1:1
2:3 2x3 1024x1536 1:1
4:5 4x5 1024x1280 1:1
5:4 5x4 1280x1024 1:1
21:9 21x9 1792x768 1:1
1:4 1x4 512x2048 1:1
4:1 4x1 2048x512 1:1

1:11x11024x1024 碰巧正常。

修复

// image-api.ts L66-69修复后
if (params.size) {
  body.size = aspectRatioToSize(params.size) || params.size;
  //          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  //          先尝试比例→像素转换,失败则透传(认为是像素尺寸)
} else if (params.aspectRatio) {
  body.size = aspectRatioToSize(params.aspectRatio);
}

aspectRatioToSize 安全分析

输入 查表结果 最终输出 正确?
'16x9' '1792x1024' '1792x1024'
'1024x1024' undefined '1024x1024'
'1920x1080' undefined '1920x1080'
undefined 函数短路返回 undefined 不进入 if 块
'auto' 函数短路返回 undefined 不进入 if 块

风险评估

零风险|| 运算符确保任何无法转换的值都会透传原值。

跨 AI 提供商兼容性分析

核心问题OpenAI 需要像素尺寸(如 1792x1024),其他 AI 可能不同,修改后会不会影响其他 AI

答案:不会。 反过来,修改发送的 '16x9' 才是对所有 API 都无效的值。

三种 size 格式与各 API 接受情况

格式类型 示例 接受的 API
像素尺寸 1792x10241024x1024 OpenAI、Gemini、Flux、Midjourney、rix 等几乎所有 API
比例字符串(冒号) 16:94:3 少数国产模型 API
混合格式x 分隔) 16x94x3 没有 API 接受这种格式

修改前后对比

阶段 用户选择 发送给 API API 行为
修改前 16:9 '16x9'(无效格式) 任何 API 都不认,统一回退 1:1
修改后 16:9 '1792x1024'(标准像素) 所有 API 都支持

为什么修改前不是发送 '16:9' 而是 '16x9'

链路中有一步 normalizeSize 将用户输入的 '16:9' 转为 '16x9',这一步本身没有问题(统一格式)。但 buildImageRequestBody 收到 '16x9' 后直接透传给 API没有调用 aspectRatioToSize 将其转为 '1792x1024''16x9' 这个格式对任何 API 都不是合法参数——它既不是像素尺寸,也不是标准比例格式。

原始设计意图本身就是转换像素

原代码中 aspectRatio 分支已经正确地调用了 aspectRatioToSize

} else if (params.aspectRatio) {
  body.size = aspectRatioToSize(params.aspectRatio);  // 转换为像素尺寸
}

说明原始设计的意图就是发送像素尺寸给 API。问题只是上游把比例字符串错放进 size 字段,跳过了 aspectRatio 这个转换分支。本次修复只是把被跳过的转换补回来,并没有改变语义——最终发送的仍然是像素尺寸。

结论:修改前发送的是无效值(所有 API 都不认),修改后发送的是标准像素尺寸(所有 API 都认)。不存在跨提供商兼容性问题。


四、问题 3图片插入画布后强制 1:1 显示

现象

foreignObject 容器始终 400x400非正方形图片被压缩。图片加载瞬间显示正确比例随后缩为 1:1。

根因

insertImageFromUrllockReferenceDimensions 参数为 true 时,跳过图片加载后的尺寸更新:

// image.ts L399
shouldUpdateSizeAfterLoad = !lockReferenceDimensions; // true → false不更新

修复

将所有自动插入场景的 lockReferenceDimensions 改为 false

文件 行号 场景 修改
canvas-insertion.ts 226 Services 批量插入 truefalse
handler.ts 231, 311 SW 自动插入 明确 false
mcp/tools/canvas-insertion.ts 225 MCP 协议插入 truefalse

全部插入路径审计

insertImageFromUrl 调用者 17 处
│
├── 自动插入场景lockReferenceDimensions=false
│   ├── canvas-insertion.ts:226      ✓ false
│   ├── handler.ts:231               ✓ false
│   ├── handler.ts:311               ✓ false
│   ├── mcp/tools/canvas-insertion.ts:225  ✓ false本次补修
│   └── media-quick-insert.ts:67     ✓ undefined等同 false
│
├── 用户手动插入场景lockReferenceDimensions 不传/undefined
│   ├── useWorkflowSubmission.ts:103  ✓ undefined图片尺寸已知
│   ├── useWorkflowSubmission.ts:112  ✓ undefined
│   ├── drawnix.tsx:1307             ✓ undefined传入 naturalWidth/Height
│   ├── popup-toolbar.tsx:2551       ✓ undefined传入 naturalWidth/Height
│   ├── quick-creation-toolbar.tsx:211 ✓ 不传
│   ├── creation-toolbar.tsx:295      ✓ 不传
│   ├── MediaLibraryGrid.tsx:1280     ✓ 不传
│   ├── MediaLibraryGrid.tsx:1324     ✓ 不传
│   ├── VideoAnalyzer.tsx:122         ✓ 不传
│   ├── TaskQueuePanel.tsx:607        ✓ 不传
│   ├── TaskQueuePanel.tsx:974        ✓ 不传
│   └── DialogTaskList.tsx:229        ✓ 不传

说明:手动插入场景传入的是图片 naturalWidth/naturalHeight(已加载的真实尺寸),无需 lockReferenceDimensions=false 也能正确显示。lockReferenceDimensions 主要影响使用固定参考尺寸400x400的自动插入场景。

尺寸更新机制

updateImageSizeAfterLoadimage.ts#L522-590

  1. 异步加载图片获取 naturalWidth/naturalHeight
  2. 计算实际宽高比与参考宽高比差异(< 1% 跳过)
  3. 以参考宽度为基准重新计算高度
  4. 通过 Transforms.setNode 更新元素 points(影响 foreignObject 尺寸)

风险评估

低风险。影响仅限画布渲染层:

  • 不影响 API 调用、图片生成流程
  • 初始 400x400 是暂时的,图片加载后异步更新
  • 极端网络条件下 updateImageSizeAfterLoad 可能加载失败(已有 .catch 保护),自动降级为参考尺寸

四-B、问题 4updateImageSizeAfterLoad 尺寸限制导致图片截断

现象

竖版长图9:16实际生成 1024x1792但插入画布后 foreignObject 容器仅为 400x400 或 400x711被参考尺寸限制导致图片被截断或显示不完整。

根因

updateImageSizeAfterLoad 的旧实现将尺寸限制在 referenceDimensions 范围内:

// 修改前image.ts L552-561
let newWidth = referenceDimensions.width;
let newHeight = newWidth / imageAspectRatio;

// 如果新高度超过预设高度,则以高度为基准
if (newHeight > referenceDimensions.height) {
  newHeight = referenceDimensions.height;
  newWidth = newHeight * imageAspectRatio;
}

问题:图片真实尺寸大于参考尺寸时被强制压缩。例如:

  • API 返回 1024×1792 图片
  • 插入时 referenceDimensions = 400×400
  • 旧逻辑:newWidth = 400, newHeight = 400 / (1024/1792) ≈ 700,但 700 > 400 → 被限制为 400×400

修复

直接使用图片的 naturalWidth/naturalHeight,仅设置上限 2048px 防止超大图片:

// 修改后image.ts L538-554
const MAX_IMAGE_WIDTH = 2048;
const MAX_IMAGE_HEIGHT = 2048;

let newWidth = naturalWidth;
let newHeight = naturalHeight;

if (newWidth > MAX_IMAGE_WIDTH) {
  const scale = MAX_IMAGE_WIDTH / newWidth;
  newWidth = MAX_IMAGE_WIDTH;
  newHeight = Math.round(newHeight * scale);
}
if (newHeight > MAX_IMAGE_HEIGHT) {
  const scale = MAX_IMAGE_HEIGHT / newHeight;
  newHeight = MAX_IMAGE_HEIGHT;
  newWidth = Math.round(newWidth * scale);
}

风险评估

低风险。该函数已充分考虑各种边界情况,具备完善的容错机制:

  1. 元素已被删除的保护updateImageSizeAfterLoad 是异步执行的(等待图片加载),用户可能在此期间删除元素。代码通过 findIndex 检查元素是否存在,不存在则直接返回,不会报错。

  2. 用户手动移动元素的保护:用户可能在图片加载过程中拖动元素到新位置。代码使用 currentTopLeft 获取当前位置,只调整右下角坐标,不会覆盖用户手动调整的位置。

  3. 图片加载失败的保护:网络问题或链接失效可能导致加载失败。代码使用 .catch() 捕获异常,仅打印警告日志,不抛出异常影响其他功能。

  4. 超大图片的保护API 可能返回超大尺寸图片(如 4096×4096。代码设置 2048px 上限,自动按比例缩小,避免内存占用过高。

  5. 无效尺寸的保护:如果图片尺寸为 0 或负数,代码在开头就会检测并返回,不会执行无效的更新操作。

  6. 触发条件控制:该函数仅在 skipImageLoad=true && lockReferenceDimensions=false 时触发,避免不必要的重复调用。


四-C、问题 5getTaskImageDimensions 忽略任务返回的真实尺寸

现象

任务执行后 task.result 包含真实图片尺寸(width/height),但插入时使用的仍是 parseSizeToPixels(task.params.size) 返回的估计值,而非真实值。

根因

getTaskImageDimensions 的优先级顺序不当——fallback 参数永远有值(来自 parseSizeToPixels),导致 task.result 的检查被跳过:

// 修改前
if (fallback) {
  return fallback;              // ← 直接返回,跳过 task.result
}
const result = task.result...;  // ← 永远执行不到

修复

task.result 的真实尺寸提到最优先:

// 修改后useAutoInsertToCanvas.ts L248-267
// 优先使用任务返回的真实尺寸
const result = task.result as { width?: number; height?: number } | undefined;
if (result?.width > 0 && result?.height > 0) {
  return { width: result.width, height: result.height };
}
if (fallback) {
  return fallback;
}
return parseSizeToPixels(task.params.size);

风险评估

零风险

  • task.result 无有效尺寸时降级到 fallback,行为不变 ✓
  • 两个调用点L683、L1070的 fallback 均来自 parseSizeToPixels,语义一致 ✓
  • 下游 insertedSize/syncImageAnchorGeometry 拿到更准确的尺寸,无副作用 ✓

四-D、问题 6素材库批量插入时图片叠加

现象

从素材库批量插入多张不同比例的图片到画布时,图片彼此堆叠覆盖,而非分开排列。

根因:尺寸链路不同步

批量插入
  → estimateInsertionItemSize: 无 dimensions → 默认 400×400
  → advanceBatchInsertionFlow: 按 400×400 估算间距排位
  → insertImageToCanvas: 用 400×400 插入
  → updateImageSizeAfterLoad (异步): 扩展为真实尺寸 (如 1024×1792)
  → 真实尺寸 >> 排位间距 → 图片叠加

核心矛盾:排位系统以 400×400 为基准计算间距,但图片最终显示为真实尺寸(修复后),导致超出分配空间。

修复

在批量插入前预加载每张素材图片的真实尺寸,传给 executeCanvasInsertionitem.dimensions

// quick-creation-toolbar.tsx L238-280
const loadImageDimensions = (url: string) =>
  new Promise((resolve) => {
    const img = new Image();
    img.onload = () => resolve({ width: img.naturalWidth, height: img.naturalHeight });
    img.onerror = () => resolve({ width: 400, height: 400 }); // 降级
    img.src = url;
  });

// 并行预加载所有图片尺寸
const imageDimensionsMap = new Map();
const results = await Promise.all(imageAssets.map(a => loadImageDimensions(a.url)));
// 传给 executeCanvasInsertion 的 item.dimensions

效果链路

预加载真实尺寸 (1024×1792)
  → estimateInsertionItemSize: 使用真实尺寸
  → advanceBatchInsertionFlow: 按真实间距排位 → 互不重叠 ✓
  → insertImageToCanvas: 用真实尺寸插入
  → updateImageSizeAfterLoad: 尺寸已匹配 → 几乎无变化 ✓

风险评估

零风险

  • 失败降级:img.onerror → 400×400不阻塞 ✓
  • 性能:本地缓存图片,Promise.all 并行加载,几乎无延迟 ✓
  • 影响范围:仅素材库「批量插入画布」这一个入口,其他插入路径完全不变 ✓
  • 视频/音频:loadImageDimensions 仅处理 AssetType.IMAGE,不触发 ✓
  • dimensions 为可选字段,不传时行为不变 ✓

四-E、问题 7批量插入流式布局导致不同尺寸素材间距不均 / 可能叠加

现象

素材库批量插入多张不同比例图片(如 9:16 竖版 + 16:9 横版)时:

  • 间距不均:同行中高素材(竖版)与矮素材(横版)顶部对齐,形成锯齿状布局
  • 潜在叠加:当某些素材的 updateImageSizeAfterLoad 异步扩展尺寸后,可能侵入相邻素材的空间

根因

旧的 advanceBatchInsertionFlow逐项流式布局Flow Layout

流式布局过程(迭代调用):
  第1项: 放入 (100, 100), cursorX → 520, rowMaxHeight = 500
  第2项: 放入 (520, 100), cursorX → 820, rowMaxHeight = 500同行顶部对齐
  第3项: 放入 (820, 100), cursorX → 1120 > rowRightLimit → 换行
  第4项: 放入 (100, 650), cursorX → 520新行从上一行 rowMaxHeight 开始)

问题

  1. 布局是基于"顺序迭代"而非"全局规划",无法预先知道所有素材的尺寸分布
  2. 同行素材仅顶部对齐,高度不同导致底部参差不齐
  3. 行高由当前行最高素材决定后固定,后续无法调整
  4. 单张插入后才异步更新为真实尺寸(updateImageSizeAfterLoad),容易超出流式布局预留空间

修复

新增 precalculateGridLayout 函数,采用网格预计算Grid Pre-calculation替代逐项流式布局

// canvas-insertion-layout.ts L228-328
export function precalculateGridLayout(
  startPoint: Point,
  itemSizes: { width: number; height: number }[],
  options: {
    canvasWidth?: number;
    maxColumns?: number;
    horizontalGap?: number;
    verticalGap?: number;
  } = {}
): { positions: Point[]; bounds: FlowBounds }

算法流程

步骤1: 根据画布宽度和素材平均宽度计算最优列数
  columns = max(1, min(5, floor((canvasWidth + gap) / (avgWidth + gap))))

步骤2: 将素材按行分组(先填第一行,再填第二行...
  6个素材, 3列 → 行0: [素材0, 素材1, 素材2], 行1: [素材3, 素材4, 素材5]

步骤3: 计算每列最大宽度(该列所有素材的最宽值)
  columnWidths[c] = max(列c中所有素材的宽度)

步骤4: 计算每行最大高度(该行所有素材的最高值)
  rowHeights[r] = max(行r中所有素材的高度)

步骤5: 计算坐标
  X偏移: colX[c] = colX[c-1] + columnWidths[c-1] + horizontalGap
  Y偏移: rowY[r] = rowY[r-1] + rowHeights[r-1] + verticalGap
  素材i位置: [colX[i % columns], rowY[floor(i / columns)]]

步骤6: 计算整体边界(用于滚动定位)

与旧布局的关键区别

特性 advanceBatchInsertionFlow precalculateGridLayout
计算方式 逐项迭代 一次性全局预计算
列数 自适应(溢出换行) 根据画布宽度动态计算
同行对齐 顶部对齐 顶部对齐(每个格子左上角)
列宽 按该项实际宽度 按该列最大宽度
行高 按该行最高项 按该行最高项
可预测性 低(依赖顺序和逐个计算结果) 高(所有位置一次性确定)

示例4 张素材2 张 400×700 竖版 + 2 张 700×400 横版),画布宽 2000

旧布局(流式):
  [竖400x700] [竖400x700] [横700x400]  ← 第3项换行
  [横700x400]                          ← 单占一行,大量空白

新布局网格2列
  [竖400x700] [横700x400]  ← 行0, 行高=max(700,400)=700
  [竖400x700] [横700x400]  ← 行1, 列0宽=max(400,400)=400, 列1宽=max(700,700)=700

风险评估

零风险

  • 向后兼容:advanceBatchInsertionFlowcreateBatchInsertionFlowState 保留原样,仅不再被批量插入调用,其他使用方不受影响 ✓
  • 空数组安全:itemSizes.length === 0 时返回空 positions 和零宽高 bounds ✓
  • 单列降级:maxColumns=5 且画布极窄时自动降为 1 列(Math.max(1, ...) 保护) ✓
  • 无画布宽度时降级:canvasWidth 未提供时使用 Math.min(maxColumns, itemSizes.length)
  • flowState.bounds 替换:新算法产生的 bounds 与旧格式完全兼容,getBatchInsertionFlowCenter 无缝工作 ✓
  • Lint 0 errors ✓
  • 影响范围仅批量插入(executeCanvasInsertion 的两份实现) ✓

五、修改文件清单(完整)

第一轮修复(已提交)

文件 修改内容 影响范围
packages/drawnix/src/services/media-api/image-api.ts 删除 response_format: 'url'size 参数增加比例→像素自动转换 API 请求体构建
packages/drawnix/src/services/canvas-operations/canvas-insertion.ts lockReferenceDimensions: false 批量图片插入
packages/drawnix/src/services/sw-capabilities/handler.ts 2 处 lockReferenceDimensions: false SW 自动插入
packages/drawnix/src/mcp/tools/canvas-insertion.ts lockReferenceDimensions: true → false MCP 协议插入

第二轮修复(当前,未提交)

文件 修改内容 影响范围
packages/drawnix/src/data/image.ts updateImageSizeAfterLoad 改用图片真实 naturalWidth/naturalHeight,上限 2048px 异步尺寸更新
packages/drawnix/src/hooks/useAutoInsertToCanvas.ts getTaskImageDimensions 优先使用 task.result 真实尺寸 AI 生成图片插入
packages/drawnix/src/components/toolbar/quick-creation-toolbar/quick-creation-toolbar.tsx 批量插入前预加载素材库图片真实尺寸 素材库批量插入

第三轮修复批量插入图片叠加2026-05-25

⚠️ 重要:此修复已单独文档化,详见 批量插入图片叠加问题修复经验总结

问题

  • 素材库批量插入多张图片时,图片全部叠加在一起
  • 根本原因:预计算的网格布局尺寸与图片加载后更新的尺寸不一致

核心解决方案

  • 新增 calculateImageDisplayDimensions 函数,统一尺寸计算逻辑
  • updateImageSizeAfterLoadloadImageDimensions 使用同一函数
  • 确保预计算和异步更新使用完全相同的尺寸

新增文件

文件 描述
packages/drawnix/src/utils/canvas-insertion-layout.ts 新增 calculateImageDisplayDimensions 函数

修改文件

文件 修改内容
packages/drawnix/src/data/image.ts updateImageSizeAfterLoad 使用 calculateImageDisplayDimensions
packages/drawnix/src/components/toolbar/quick-creation-toolbar/quick-creation-toolbar.tsx loadImageDimensions 使用 calculateImageDisplayDimensions;添加 horizontalGap: 30verticalGap: 40

未修改的文件(已验证不受影响)

  • packages/drawnix/src/services/model-adapters/gpt-image-adapter.ts — 自有 response_format 处理
  • packages/drawnix/src/services/model-adapters/tuzi-gpt-image-adapter.ts — 继承 GPT 适配器
  • packages/drawnix/src/services/media-api/utils.ts — 仅 aspectRatioToSize 被引用,逻辑不变
  • packages/drawnix/src/hooks/useWorkflowSubmission.ts — 手动插入场景,传入真实尺寸
  • 其他手动插入路径 — lockReferenceDimensions 未传或使用真实尺寸
  • 其他批量插入入口 — 不走素材库,不受预加载修改影响

六、修改间依赖关系

                    ┌─────────────────────────┐
                    │   buildImageRequestBody  │
                    │  (问题1+2) 已提交         │
                    │  - 删除 response_format│
                    │  - size 比例转换        │
                    └─────────────────────────┘
                                │
                                ▼
                    ┌─────────────────────────┐
                    │   insertImageFromUrl     │
                    │  (问题3) 已提交          │
                    │  - lockReferenceDimensions = false │
                    └───────────┬─────────────┘
                                │
              ┌─────────────────┼─────────────────┐
              ▼                 ▼                  ▼
  ┌──────────────────┐ ┌──────────────┐ ┌────────────────────┐
  │ updateImageSize  │ │getTaskImage  │ │ 素材库批量预加载     │
  │ AfterLoad        │ │Dimensions    │ │ (问题6) 当前         │
  │ (问题4) 当前     │ │(问题5) 当前  │ │ - 预加载真实尺寸     │
  │ - 使用 natural   │ │- task.result │ │ - 传给 dimensions   │
  │   Width/Height   │ │  优先级提前  │ │                     │
  └──────────────────┘ └──────────────┘ └────────────────────┘

独立性

  • 问题 1+2API 层)与问题 3-6画布层完全独立
  • 问题 4updateImageSizeAfterLoad)是问题 6批量插入叠加的前提——如果恢复旧的尺寸限制批量插入预加载的间距也将失效
  • 问题 5getTaskImageDimensions)独立于其他修改,仅优化 AI 生成场景的尺寸获取
  • 问题 7批量插入图片叠加
    • 核心修复:新增 calculateImageDisplayDimensions 函数,统一所有尺寸计算逻辑
    • 依赖关系:loadImageDimensionsprecalculateGridLayoutupdateImageSizeAfterLoad
    • 关键保证:预计算和异步更新使用完全相同的尺寸计算函数

七、经验总结

关键设计原则

  1. 不要硬编码特定提供商的参数response_format: 'url' 对 OpenAI 是默认值,对 rix 是致命错误
  2. API 边界做参数规范化:上游可能传入比例格式('16x9')或像素尺寸('1792x1024'),应在发送前统一转换
  3. || 降级模式比 if/else 更安全aspectRatioToSize(x) || x 保证无法转换的值会透传
  4. 异步尺寸更新优于同步锁定lockReferenceDimensions=false 允许图片加载后自适应,用户体验更好
  5. 全局预计算优于逐项流式布局:批量操作中先收集所有尺寸再一次性计算网格布局,比逐项迭代更可预测、间距更均匀

排查方法论

  • 参数链路追踪:从 UI → Parser → Engine → Service → Executor → API Body逐层验证
  • 边界层防御:在 API 边界(buildImageRequestBody)做参数校验和转换,不依赖上游正确性
  • 全局调用审计:修改参数签名后需 grep 全部调用者,确保所有路径一致