29 KiB
图片生成 API 参数兼容性与比例转换修复经验
问题概述
本次修复涉及三个关联问题,全部为项目预存代码问题,非近期修改引入:
- rix API 400 错误:
response_format参数不被 rix API 支持,导致图片生成失败 - 非 1:1 比例全部生成 1:1:用户选择 16:9、4:3 等比例时,API 实际生成正方形图片
- 图片插入画布后强制 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':
// 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 → 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 两种返回格式 |
风险评估
零风险。原因:
- OpenAI API 的
response_format默认值就是'url',不传等效于传'url' parseImageResponse+normalizeImageResultUrl兼容url和b64_json两种格式- 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:1 → 1x1 → 1024x1024 碰巧正常。
修复
// 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:
} else if (params.aspectRatio) {
body.size = aspectRatioToSize(params.aspectRatio); // 转换为像素尺寸
}
说明原始设计的意图就是发送像素尺寸给 API。问题只是上游把比例字符串错放进 size 字段,跳过了 aspectRatio 这个转换分支。本次修复只是把被跳过的转换补回来,并没有改变语义——最终发送的仍然是像素尺寸。
结论:修改前发送的是无效值(所有 API 都不认),修改后发送的是标准像素尺寸(所有 API 都认)。不存在跨提供商兼容性问题。
四、问题 3:图片插入画布后强制 1:1 显示
现象
foreignObject 容器始终 400x400,非正方形图片被压缩。图片加载瞬间显示正确比例,随后缩为 1:1。
根因
insertImageFromUrl 的 lockReferenceDimensions 参数为 true 时,跳过图片加载后的尺寸更新:
// 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):
- 异步加载图片获取
naturalWidth/naturalHeight - 计算实际宽高比与参考宽高比差异(< 1% 跳过)
- 以参考宽度为基准重新计算高度
- 通过
Transforms.setNode更新元素points(影响foreignObject尺寸)
风险评估
低风险。影响仅限画布渲染层:
- 不影响 API 调用、图片生成流程
- 初始 400x400 是暂时的,图片加载后异步更新
- 极端网络条件下
updateImageSizeAfterLoad可能加载失败(已有.catch保护),自动降级为参考尺寸
四-B、问题 4:updateImageSizeAfterLoad 尺寸限制导致图片截断
现象
竖版长图(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);
}
风险评估
低风险。该函数已充分考虑各种边界情况,具备完善的容错机制:
-
元素已被删除的保护:
updateImageSizeAfterLoad是异步执行的(等待图片加载),用户可能在此期间删除元素。代码通过findIndex检查元素是否存在,不存在则直接返回,不会报错。 -
用户手动移动元素的保护:用户可能在图片加载过程中拖动元素到新位置。代码使用
currentTopLeft获取当前位置,只调整右下角坐标,不会覆盖用户手动调整的位置。 -
图片加载失败的保护:网络问题或链接失效可能导致加载失败。代码使用
.catch()捕获异常,仅打印警告日志,不抛出异常影响其他功能。 -
超大图片的保护:API 可能返回超大尺寸图片(如 4096×4096)。代码设置 2048px 上限,自动按比例缩小,避免内存占用过高。
-
无效尺寸的保护:如果图片尺寸为 0 或负数,代码在开头就会检测并返回,不会执行无效的更新操作。
-
触发条件控制:该函数仅在
skipImageLoad=true && lockReferenceDimensions=false时触发,避免不必要的重复调用。
四-C、问题 5:getTaskImageDimensions 忽略任务返回的真实尺寸
现象
任务执行后 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 为基准计算间距,但图片最终显示为真实尺寸(修复后),导致超出分配空间。
修复
在批量插入前预加载每张素材图片的真实尺寸,传给 executeCanvasInsertion 的 item.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 开始)
问题:
- 布局是基于"顺序迭代"而非"全局规划",无法预先知道所有素材的尺寸分布
- 同行素材仅顶部对齐,高度不同导致底部参差不齐
- 行高由当前行最高素材决定后固定,后续无法调整
- 单张插入后才异步更新为真实尺寸(
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
风险评估
零风险。
- 向后兼容:
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)
⚠️ 重要:此修复已单独文档化,详见 批量插入图片叠加问题修复经验总结
问题:
- 素材库批量插入多张图片时,图片全部叠加在一起
- 根本原因:预计算的网格布局尺寸与图片加载后更新的尺寸不一致
核心解决方案:
- 新增
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+2(API 层)与问题 3-6(画布层)完全独立
- 问题 4(
updateImageSizeAfterLoad)是问题 6(批量插入叠加)的前提——如果恢复旧的尺寸限制,批量插入预加载的间距也将失效 - 问题 5(
getTaskImageDimensions)独立于其他修改,仅优化 AI 生成场景的尺寸获取 - 问题 7(批量插入图片叠加):
- 核心修复:新增
calculateImageDisplayDimensions函数,统一所有尺寸计算逻辑 - 依赖关系:
loadImageDimensions→precalculateGridLayout→updateImageSizeAfterLoad - 关键保证:预计算和异步更新使用完全相同的尺寸计算函数
- 核心修复:新增
七、经验总结
关键设计原则
- 不要硬编码特定提供商的参数:
response_format: 'url'对 OpenAI 是默认值,对 rix 是致命错误 - API 边界做参数规范化:上游可能传入比例格式(
'16x9')或像素尺寸('1792x1024'),应在发送前统一转换 ||降级模式比if/else更安全:aspectRatioToSize(x) || x保证无法转换的值会透传- 异步尺寸更新优于同步锁定:
lockReferenceDimensions=false允许图片加载后自适应,用户体验更好 - 全局预计算优于逐项流式布局:批量操作中先收集所有尺寸再一次性计算网格布局,比逐项迭代更可预测、间距更均匀
排查方法论
- 参数链路追踪:从 UI → Parser → Engine → Service → Executor → API Body,逐层验证
- 边界层防御:在 API 边界(
buildImageRequestBody)做参数校验和转换,不依赖上游正确性 - 全局调用审计:修改参数签名后需 grep 全部调用者,确保所有路径一致