Initial TrueGrowth source import

This commit is contained in:
2026-07-07 09:36:36 +08:00
commit 3b6781d695
2283 changed files with 691996 additions and 0 deletions

View File

@@ -0,0 +1,672 @@
# 图片生成 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 全部调用者,确保所有路径一致