Files
TrueGrowth/docs/IMAGE_GENERATION_API_PARAMS_LESSONS.md

672 lines
29 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 图片生成 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.ts``generateImageSync` 两个路径调用
- **适配器层**`gpt-image-adapter.ts``tuzi-gpt-image-adapter.ts`)有独立的 `response_format` 处理逻辑,只在使用专属适配器时生效
- **提供商路由**`provider-transport.ts`)直接将请求 fetch 到外部 API 服务商,不做参数转换
---
## 二、问题 1`response_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'`
```typescript
// 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。
### 修复
```typescript
// 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 → `generateImageSync``buildImageRequestBody` | 已修复 | 不再发送 `response_format` |
| `gpt-image-adapter.ts` L176-179 | **不受影响** | 有自己的 `getGPTImageResponseFormat` 处理,只在用户显式传入时才添加 |
| `tuzi-gpt-image-adapter.ts` | **不受影响** | 继承 GPT Image 适配器逻辑 |
| `image-api.ts` L35-48 `normalizeImageResultUrl` | **不受影响** | 同时支持 `url``b64_json` 两种返回格式 |
### 风险评估
**零风险**。原因:
1. OpenAI API 的 `response_format` **默认值就是 `'url'`**,不传等效于传 `'url'`
2. `parseImageResponse` + `normalizeImageResultUrl` 兼容 `url``b64_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` 的逻辑缺陷:
```typescript
// 修复前
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:1``1x1``1024x1024` 碰巧正常。
### 修复
```typescript
// 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 |
|---------|------|-----------|
| 像素尺寸 | `1792x1024``1024x1024` | OpenAI、Gemini、Flux、Midjourney、rix 等几乎所有 API |
| 比例字符串(冒号) | `16:9``4:3` | 少数国产模型 API |
| 混合格式x 分隔) | `16x9``4x3` | **没有 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`
```typescript
} else if (params.aspectRatio) {
body.size = aspectRatioToSize(params.aspectRatio); // 转换为像素尺寸
}
```
说明原始设计的意图就是**发送像素尺寸给 API**。问题只是上游把比例字符串错放进 `size` 字段,跳过了 `aspectRatio` 这个转换分支。本次修复只是**把被跳过的转换补回来**,并没有改变语义——最终发送的仍然是像素尺寸。
**结论**:修改前发送的是无效值(所有 API 都不认),修改后发送的是标准像素尺寸(所有 API 都认)。不存在跨提供商兼容性问题。
---
## 四、问题 3图片插入画布后强制 1:1 显示
### 现象
`foreignObject` 容器始终 400x400非正方形图片被压缩。图片加载瞬间显示正确比例随后缩为 1:1。
### 根因
`insertImageFromUrl``lockReferenceDimensions` 参数为 `true` 时,跳过图片加载后的尺寸更新:
```typescript
// image.ts L399
shouldUpdateSizeAfterLoad = !lockReferenceDimensions; // true → false不更新
```
### 修复
将所有自动插入场景的 `lockReferenceDimensions` 改为 `false`
| 文件 | 行号 | 场景 | 修改 |
|------|------|------|------|
| `canvas-insertion.ts` | 226 | Services 批量插入 | `true``false` |
| `handler.ts` | 231, 311 | SW 自动插入 | 明确 `false` |
| `mcp/tools/canvas-insertion.ts` | 225 | MCP 协议插入 | `true``false` |
### 全部插入路径审计
```
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的自动插入场景。
### 尺寸更新机制
`updateImageSizeAfterLoad`[image.ts#L522-590](file:///d:/工作/opentu_new/packages/drawnix/src/data/image.ts#L522-L590)
1. 异步加载图片获取 `naturalWidth`/`naturalHeight`
2. 计算实际宽高比与参考宽高比差异(< 1% 跳过)
3. 以参考宽度为基准重新计算高度
4. 通过 `Transforms.setNode` 更新元素 `points`(影响 `foreignObject` 尺寸)
### 风险评估
**低风险**。影响仅限画布渲染层:
- 不影响 API 调用、图片生成流程
- 初始 400x400 是暂时的,图片加载后异步更新
- 极端网络条件下 `updateImageSizeAfterLoad` 可能加载失败(已有 `.catch` 保护),自动降级为参考尺寸
---
## 四-B、问题 4`updateImageSizeAfterLoad` 尺寸限制导致图片截断
### 现象
竖版长图9:16实际生成 1024x1792但插入画布后 `foreignObject` 容器仅为 400x400 或 400x711被参考尺寸限制导致图片被截断或显示不完整。
### 根因
`updateImageSizeAfterLoad` 的旧实现将尺寸限制在 `referenceDimensions` 范围内:
```typescript
// 修改前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 防止超大图片:
```typescript
// 修改后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、问题 5`getTaskImageDimensions` 忽略任务返回的真实尺寸
### 现象
任务执行后 `task.result` 包含真实图片尺寸(`width`/`height`),但插入时使用的仍是 `parseSizeToPixels(task.params.size)` 返回的估计值,而非真实值。
### 根因
`getTaskImageDimensions` 的优先级顺序不当——`fallback` 参数永远有值(来自 `parseSizeToPixels`),导致 `task.result` 的检查被跳过:
```typescript
// 修改前
if (fallback) {
return fallback; // ← 直接返回,跳过 task.result
}
const result = task.result...; // ← 永远执行不到
```
### 修复
`task.result` 的真实尺寸提到最优先:
```typescript
// 修改后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 为基准计算间距,但图片最终显示为真实尺寸(修复后),导致超出分配空间。
### 修复
在批量插入前预加载每张素材图片的真实尺寸,传给 `executeCanvasInsertion``item.dimensions`
```typescript
// 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替代逐项流式布局
```typescript
// 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
```
### 风险评估
**零风险**
- 向后兼容:`advanceBatchInsertionFlow``createBatchInsertionFlowState` **保留原样**,仅不再被批量插入调用,其他使用方不受影响 ✓
- 空数组安全:`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
> ⚠️ **重要**:此修复已单独文档化,详见 [批量插入图片叠加问题修复经验总结](./CANVAS_BATCH_INSERTION_FIX_LESSONS.md)
**问题**
- 素材库批量插入多张图片时,图片全部叠加在一起
- 根本原因:预计算的网格布局尺寸与图片加载后更新的尺寸不一致
**核心解决方案**
- 新增 `calculateImageDisplayDimensions` 函数,统一尺寸计算逻辑
- `updateImageSizeAfterLoad``loadImageDimensions` 使用同一函数
- 确保预计算和异步更新使用完全相同的尺寸
**新增文件**
| 文件 | 描述 |
|------|------|
| `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: 30``verticalGap: 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画布层完全独立
- 问题 4`updateImageSizeAfterLoad`)是问题 6批量插入叠加的前提——如果恢复旧的尺寸限制批量插入预加载的间距也将失效
- 问题 5`getTaskImageDimensions`)独立于其他修改,仅优化 AI 生成场景的尺寸获取
- **问题 7批量插入图片叠加**
- 核心修复:新增 `calculateImageDisplayDimensions` 函数,统一所有尺寸计算逻辑
- 依赖关系:`loadImageDimensions``precalculateGridLayout``updateImageSizeAfterLoad`
- 关键保证:预计算和异步更新使用完全相同的尺寸计算函数
---
## 七、经验总结
### 关键设计原则
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 全部调用者,确保所有路径一致