修复编辑器参考图与后台日期问题

后台 Dashboard 默认日期按北京时间计算

图片快编请求补齐额外参考图、模型和尺寸参数

后端校验参考图 objectKey 归属并放宽编辑请求体限制

同步资产契约、测试和相关文档
This commit is contained in:
2026-06-28 01:09:40 +08:00
parent d31cac8cbc
commit 8ee57210a4
18 changed files with 499 additions and 75 deletions
@@ -93,6 +93,21 @@ test('Dashboard 默认加载今日指标并支持运营汇总页签', async () =
expect(screen.getByText('访问模块分布')).toBeTruthy();
});
test('Dashboard 默认日期使用北京时间', async () => {
vi.setSystemTime(new Date('2026-06-26T17:30:00Z'));
render(<AdminDashboardPage token="admin-token" onUnauthorized={vi.fn()} />);
await waitFor(() => {
expect(getAdminDashboard).toHaveBeenLastCalledWith('admin-token', {
granularity: 'period',
anchor: undefined,
startDate: '2026-06-27',
endDate: '2026-06-27',
});
});
});
test('Dashboard 选择本周时按今天填充整周范围', async () => {
const user = setupUser();
render(<AdminDashboardPage token="admin-token" onUnauthorized={vi.fn()} />);
@@ -36,7 +36,7 @@ export function AdminDashboardPage({
const [granularity, setGranularity] =
useState<AdminDashboardGranularity>('day');
const [dateRange, setDateRange] = useState(() => {
const today = formatDateInput(new Date());
const today = formatBeijingDateInput(new Date());
return { startDate: today, endDate: today };
});
const [activeTab, setActiveTab] = useState<AdminDashboardTab>('metrics');
@@ -327,7 +327,7 @@ export function AdminDashboardPage({
) {
const nextRange = buildPresetDateRange(
nextGranularity,
formatDateInput(new Date()),
formatBeijingDateInput(new Date()),
);
const rangeChanged =
nextRange.startDate !== dateRange.startDate ||
@@ -450,11 +450,13 @@ function formatNumber(value: number) {
return new Intl.NumberFormat('zh-CN').format(value);
}
function formatDateInput(date: Date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
function formatBeijingDateInput(date: Date) {
return new Intl.DateTimeFormat('en-CA', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
day: '2-digit',
}).format(date);
}
function rangePrefix(granularity: AdminDashboardGranularity) {
@@ -474,7 +476,7 @@ function buildPresetDateRange(
granularity: AdminDashboardGranularity,
selectedDate: string,
) {
const fallbackDate = formatDateInput(new Date());
const fallbackDate = formatBeijingDateInput(new Date());
const anchor = parseDateValueAsUtc(selectedDate)
? selectedDate
: fallbackDate;
@@ -62,6 +62,14 @@
- 验证:画板生成 workflow 测试覆盖 queueState 持续 `running` 到前端等待窗口结束时,不进入 failed、不显示该排队文案、不添加本地临时结果层。
- 关联:`src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts``src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx`
## 画板参考图 objectKey 必须先做归属校验
- 现象:画板生成、快速编辑、图标素材或 UI 素材提取如果允许直接提交 generated objectKey,用户只要知道其他账号的私有 objectKey,就可能让 api-server 签名读取并送给外部生成供应商。
- 原因:Data URL 参考图可以直接解析,但 objectKey 是服务端私有对象引用;只校验 generated 前缀、mime 和大小不能证明它属于当前账号。
- 处理:所有编辑器参考图入口统一走 `parse_editor_reference_image(state, owner_user_id, source)`;objectKey 分支必须先在当前账号的项目资源、素材库资产或 `asset_object` 中匹配 owner / bucket / key,再读取 OSS。快速编辑和图标素材额外参考图也必须真实传到 provider,不只写 metadata。
- 验证:`cargo test -p api-server --manifest-path server-rs/Cargo.toml editor_reference`,并用前端 workflow 测试覆盖 `referenceImageSrcs` 进入快速编辑 / 图标生成请求。
- 关联:`server-rs/crates/api-server/src/editor_project.rs``server-rs/crates/spacetime-client/src/assets.rs``src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts`
## 编辑器生成按钮显示泥点后仍要查真实钱包预扣
- 现象:画板生成按钮显示 `N泥点`,后端也能按模型配置计算出价格,但用户点击后钱包余额不变。
@@ -75,7 +75,7 @@
- `POST /api/editor/images/generations`:按提示词调用 VectorEngine 生成图片;角色生成可携带 `model``aspectRatio``imageSize``referenceImageSrcs`,宣发素材携带 `kind: "publication-material"` 时固定归一为 `gpt-image-2`,不支持 `nanobanana2``nanobanana2` 参考图作为 `inline_data` 进入 `generateContent``gpt-image-2` 参考图进入 edits。普通重绘继续走该接口并把当前图层图片作为参考图;图片快速编辑不走该接口。请求可携带 `projectId``assetFolderId``assetKind``generationInputs``sourceResourceId`,后端生成成功后创建 project resource / 账号素材并在响应中返回 resource / asset 快照。
- `POST /api/editor/icon-spritesheets/generations`:按图标规范图和素材描述数组生成 spritesheet,再由后端切分为独立透明图标。请求支持 `model``aspectRatio``imageSize``priceMudPoints``projectId``assetFolderId``generationInputs``priceMudPoints` 必须来自编辑器生成计费配置中对应生图模型的尺寸档位(如 `nanobanana2``0.5K / 1K / 2K``gpt-image-2``1K / 2K`),后端用 `editor_generation_config` 校验后才调用上游;`nanobanana2` 走原生 `generateContent` 并写入 `generationConfig.imageConfig.aspectRatio/imageSize``0.5K``"512"``gpt-image-2``/v1/images/edits`。后端把 spritesheet 和拆分后的 icon 都保存为 project resource / 账号素材,并随响应返回对应快照。
- `POST /api/editor/ui-designs/assets/extractions`:以前端已绘入红色框选轮廓的 UI 设计图 Data URL 作为参考图,固定 `gpt-image-2` 和提示词 `仅提取被红色框框选的素材并整理成spritesheet,图集背景必须使用单一纯绿色 #00FF00 / RGB(0,255,0) 绿幕,方便后续扣除背景;素材自身不要出现绿色描边、绿色底板或绿色阴影。` 生成素材 spritesheet,再按连通域自动拆分为 `素材 1..N`,返回结构复用图标 spritesheet 响应。请求必须携带 `aspectRatio: "1:1"``imageSize: "1K" | "2K"``priceMudPoints`;框选数量不超过 6 个时前端按 `1:1·1K` 与 gpt-image-2 1K 价格提交,超过 6 个时按 `1:1·2K` 与 2K 价格提交。后端必须在调用上游前校验比例、尺寸和泥点价格,只允许 `1:1 / 1K / 2K`。请求可携带 `projectId``assetFolderId``generationInputs``spritesheetLabel`,后端保存 spritesheet / 拆分素材并返回对应 resource / asset 快照;前端必须把 spritesheet 原图与拆分素材都加入画布。
- `POST /api/editor/images/edits`:按提示词当前图片 Data URL 调用 VectorEngine edits,返回新的生成图片元数据;请求携带 project / asset 上下文时由后端创建新 resource / asset,前端只消费响应快照。
- `POST /api/editor/images/edits`:按提示词当前图片 Data URL 和最多 8 张额外参考图调用 VectorEngine edits,返回新的生成图片元数据;请求携带 project / asset 上下文时由后端创建新 resource / asset,前端只消费响应快照。
- `POST /api/editor/videos/generations`:按视频描述、模型、比例、时长、分辨率、模式、声音、默认联网搜索标记和泥点价格生成视频。前端可选模型为 `seedance2.0-fast``seedance2.0``kling3.0``kling3.0-omni`,默认 `seedance2.0-fast`;后端必须将 `seedance2.0-fast` 映射到 `doubao-seedance-2-0-fast-260128`,将 `seedance2.0` 映射到 `doubao-seedance-2-0-260128`,两者不得混用。后端允许 6 类比例、4 到 15 秒整数、`480p / 720p / 1080p`,并拒绝 `seedance2.0-fast + 1080p``sound=on/off` 映射 Ark `generate_audio=true/false`。后端复用 Ark / VectorEngine content generation task 轮询链路,下载最终视频并持久化到 OSS,返回 `videoSrc`、尺寸、prompt、model、provider、taskId、durationSeconds、resolution 和 `priceMudPoints`
所有写接口都必须校验 Bearer 登录态和 owner;接口只返回当前用户有权读取的工程与资源。
@@ -108,7 +108,7 @@
- 图片选中后的浮动工具栏按钮顺序固定为:快速编辑、分割线、裁扩按钮、去除背景按钮、UI设计图专属提取素材、角色图专属生成动画、分割线、重绘、下载按钮。裁扩通过画布边界拖拉完成,不再展示四边数值输入;默认自由比例,选择固定比例后拖拉边界保持对应比例,完成后在原素材旁边新增裁扩结果图层,扩展区域透明填充。去除背景使用前端轻量去背并回写为新的工程资源快照;前端轻量去背必须复用共享 `chromaKey` 的绿幕 / 近白背景模型,不再只按四角颜色扩散,避免纯白角色主体或主体内部浅色区域被扣碎。UI设计图的提取素材必须先进入红框素材框选状态,默认启用矩形框选,右侧框选工具与快速编辑统一且可再次点击取消启用态,当前启用工具按钮必须保持高亮。素材提取面板必须在素材下方,使用与生成新素材一致的面板宽度和底部模型 / 按钮样式,提示语显示 `使用框选工具框选你希望从画面中提取的素材`,并展示按原图坐标准确裁剪的框选区域截图预览、固定模型 `gpt-image-2`、左下角计划规格 `1:1·1K/2K``提取 · N泥点` 按钮,不显示额外取消按钮;点击素材和面板以外的画布区域即退出 UI 素材提取。至少框选一个区域后才可提交,前端把红色轮廓绘入原图后固定走 `gpt-image-2` 和提示词 `仅提取被红色框框选的素材并整理成spritesheet,图集背景必须使用单一纯绿色 #00FF00 / RGB(0,255,0) 绿幕,方便后续扣除背景;素材自身不要出现绿色描边、绿色底板或绿色阴影。`,生成的 spritesheet 原图和拆分后的独立素材都作为画布图层保留。
- 重绘生成资源后,右侧出现新生成结果图层,并自动 fit 原图 + 新图,且重绘面板保持打开。
- 快速编辑 / 重绘站内 public 示例图、历史 generated 图或 OSS generated 图时,前端先读取成 `data:image/*;base64,...` 再提交,后端不得再收到 `/creation-type-references/*``/generated-*` 或 OSS URL 作为 `referenceImageSrcs/sourceImageSrc`
- 快速编辑的额外参考图进入输入快照和前端上下文,不再作为图片编辑请求的 `referenceImageSrcs` 提交;需要真正多参考图编辑时再扩展 `/api/editor/images/edits` 契约
- 快速编辑的额外参考图进入输入快照,也作为 `/api/editor/images/edits``referenceImageSrcs` 提交给后端;原图仍只作为 `sourceImageSrc`,不占用额外参考图额度
- 素材文件夹可以新建、折叠、重命名和删除;删除普通文件夹后,其素材移动到“项目素材”。普通上传默认落入“上传素材”文件夹;素材库缺少该文件夹时,前端在首次普通上传前创建一次并复用,拖到指定文件夹或点击指定文件夹上传时仍进入目标文件夹。
- 上传按钮和拖拽上传都支持多文件;底部工具栏的上传入口选择文件后直接进入“上传素材”并在当前画布视口中心创建画布图层,素材栏文件夹内的上传入口只写入对应素材文件夹、不自动入画布;拖到文件夹或该文件夹内素材时进入目标文件夹;拖到画布时进入“上传素材”并在投放点创建画布图层。上传图片必须在创建占位素材、画布图层和账号级素材记录前先读取原图 Resolution,图层宽高、`originalWidth/originalHeight` 和素材库 `width/height` 都使用图片本身尺寸;仅在无法解析尺寸时才使用上传兜底尺寸。
- 音频 / 视频素材卡和画布媒体图层必须提供稳定的非文字视觉预览:优先使用 `thumbnailSrc` / 视频 `poster`,没有真实首帧或音频封面时使用由媒体类型、素材名和地址派生的确定性视觉底图。视频图层使用原生 `<video controls preload="metadata" playsInline>` 播放,外层图层仍承接选择和拖拽语义;音频图层播放前继续通过 `/api/assets/read-url` 换签。画布素材导出按 `mediaType` 保留真实媒体格式:图片进入 `images/`,音频 / 视频进入 `media/`,文件扩展名从响应 MIME、`objectKey` 或源 URL 推断,不得把音频 / 视频导出成 PNG。
@@ -14,6 +14,7 @@
- `anchor` 使用北京时间日历日期,保留给 `day` / `week` / `month` 兼容旧查询口径。`week``month` 选择包含该日期的自然周 / 自然月。
- `period` 使用 `startDate` / `endDate` 作为闭区间自定义时段,最多 366 天。
- 前端统一展示起始日期和终止日期两个日期选择器;点击“本日 / 本周 / 本月”按当天真实日期自动填充对应自然日 / 自然周 / 自然月范围并刷新,手动修改起止日期后按当前日期范围查询。
- 前端默认日期和“本日 / 本周 / 本月”快捷入口都按 `Asia/Shanghai` 计算,不使用浏览器本地时区。
- 前端每 5 分钟自动刷新一次,同时保留手动刷新按钮。
## 指标口径
@@ -202,6 +202,7 @@ npm run check:server-rs-ddd
2. 编辑器画板所有会调用外部生成 provider 的入口都不从前端请求接收 `priceMudPoints`;实际扣费真相以后端运行时模型定价配置为准,前端按钮泥点只作为展示。
3. 编辑器图片生成 / 图片修改 / 图标 spritesheet / UI 设计图提取素材 / 视频 / 角色动作 / 音效 / 背景音乐必须在后端计算模型价格后使用 `execute_billable_asset_operation_with_cost` 预扣泥点;预扣失败必须 fail-closed,不得继续提交 VectorEngine、Ark、Suno 或 Vidu 上游任务。
4. 音频生成的编辑器链路虽然任务提交和结果发布分离,仍必须把提交时后端计算出的模型价格写入 `AudioAssetBindingTarget.billing_points_cost`,最终发布落资产时按该价格扣费;创作音频目标未提供该字段时才使用旧的创作音频固定成本。
5. 编辑器图片生成、图片修改、图标 spritesheet 和 UI 设计图提取素材的参考图可以提交 Data URL 或已登记的 generated objectKeyobjectKey 必须归属于当前账号的 `editor_project_resource``editor_asset``asset_object`,后端通过归属校验后才签名读取 OSS。快速编辑、图标素材和 UI 素材提取的额外参考图必须真正传入 provider,不得只写入 `generationInputs` 展示快照;UI 素材提取额外参考图上限为 5 张,普通图片生成上限 5 张,快速编辑 / 图标素材上限 8 张额外参考图。
## 外部服务与资产
@@ -178,7 +178,7 @@
- 快速编辑底部展示当前选择的比例 / 尺寸和模型,视觉对齐可编辑面板的比例 / 尺寸 / 模型按钮,并允许展开修改;额外参考图最多 8 张,原图作为 `sourceImageSrc` 直接编辑,不在参考图条里固定显示 `图x`
- 快速编辑打开后画布自动缩放平移到原图完整展示,并让面板位于原图下方且不遮挡原图;原图右侧出现竖向矩形 / 椭圆 / 画笔自由框选按钮。进入快速编辑不默认启用框选,点击工具启用并保持高亮,再点同一工具取消;完成框选后画布红色细框显示连续序号,输入框同步追加 `对N号红色圈选框里的内容做以下修改:`
- 快速编辑提交前保留提示词里对原图的 `原图``当前图片``当前图``图1` 引用,不再改写成 `图N`
- 快速编辑提交给后端时只把原图或已绘制红框和序号的标注图作为 `sourceImageSrc`;额外参考图进入输入快照和前端上下文,不作为图片编辑请求的 `referenceImageSrcs`
- 快速编辑提交给后端时只把原图或已绘制红框和序号的标注图作为 `sourceImageSrc`;额外参考图进入输入快照,并作为图片编辑请求的 `referenceImageSrcs` 一起提交
- 生成中的占位图聚焦后可用 `Delete` / `Backspace` 删除;删除后异步结果不再落回画布,也不显示额外删除 UI。
- 快速编辑生成中的占位图同样只支持键盘 `Delete` / `Backspace` 删除,不新增 UI 删除按钮;删除后异步结果不得再落回画布。
- 生成视频 / 角色形象 / 角色动作 / 音效 / 背景音乐新建后,画布占位空白样式和右上角标签均与对应生成类型一致,不再统一使用图片占位 icon。
+48
View File
@@ -1695,6 +1695,54 @@ mod tests {
assert!(!body_text.contains("length limit exceeded"));
}
#[tokio::test]
async fn editor_image_edit_accepts_reference_body_above_default_limit() {
let state = AppState::new(AppConfig {
external_generation_mode: ExternalGenerationMode::Inline,
..AppConfig::default()
})
.expect("state should build");
let seed_user = seed_phone_user_with_password(&state, "13800138029", TEST_PASSWORD).await;
let token = sign_test_user_token(&state, &seed_user, "sess_editor_image_edit_body");
let app = build_router(state);
let source_image_src = format!("data:image/png;base64,{}", "A".repeat(3 * 1024 * 1024));
let request_body = serde_json::json!({
"prompt": "快速编辑图片",
"sourceImageSrc": source_image_src,
"size": "1024x1024",
"model": "gpt-image-2"
})
.to_string();
assert!(request_body.len() > 2 * 1024 * 1024);
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/api/editor/images/edits")
.header("authorization", format!("Bearer {token}"))
.header("content-type", "application/json")
.body(Body::from(request_body))
.expect("request should build"),
)
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE);
let body = response
.into_body()
.collect()
.await
.expect("response body should collect")
.to_bytes();
let body_text = String::from_utf8_lossy(&body);
assert!(
body_text.contains("VECTOR_ENGINE_BASE_URL"),
"handler should parse the oversized editor edit image before checking VectorEngine config: {body_text}"
);
assert!(!body_text.contains("length limit exceeded"));
}
#[tokio::test]
async fn password_entry_rejects_unknown_phone_without_registration() {
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));
@@ -1124,13 +1124,12 @@ pub async fn resolve_role_asset_workflow(
})?;
let cache_scope_id = trim_optional_text(payload.cache_scope_id.as_deref());
let cache = match load_workflow_cache(&state, character_id.as_str(), cache_scope_id.as_deref())
.await
{
Ok(cache) => cache,
Err(error) if is_missing_oss_config_error(&error) => None,
Err(error) => return Err(character_animation_error_response(&request_context, error)),
};
let cache =
match load_workflow_cache(&state, character_id.as_str(), cache_scope_id.as_deref()).await {
Ok(cache) => cache,
Err(error) if is_missing_oss_config_error(&error) => None,
Err(error) => return Err(character_animation_error_response(&request_context, error)),
};
let workflow = build_role_asset_workflow(payload.role, cache.as_ref());
Ok(json_success_body(
File diff suppressed because it is too large Load Diff
@@ -131,10 +131,14 @@ pub fn router(state: AppState) -> Router<AppState> {
)
.route(
"/api/editor/images/edits",
post(edit_editor_image).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
post(edit_editor_image)
.layer(DefaultBodyLimit::max(
EDITOR_IMAGE_REFERENCE_BODY_LIMIT_BYTES,
))
.route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/editor/icon-spritesheets/generations",
@@ -444,6 +444,8 @@ pub struct EditorCanvasGenerationCompletionPayload {
#[serde(rename_all = "camelCase")]
pub struct EditorIconSpritesheetGenerateRequest {
pub reference_image_src: String,
#[serde(default)]
pub reference_image_srcs: Vec<String>,
pub icon_descriptions: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
@@ -1231,6 +1233,7 @@ mod tests {
fn editor_icon_spritesheet_request_omits_price_and_response_includes_price() {
let request_payload = serde_json::to_value(EditorIconSpritesheetGenerateRequest {
reference_image_src: "data:image/png;base64,spec".to_string(),
reference_image_srcs: vec!["data:image/png;base64,style".to_string()],
icon_descriptions: vec!["返回按钮".to_string(), "设置按钮".to_string()],
model: Some("gemini-3.1-flash-image-preview".to_string()),
aspect_ratio: Some("1:1".to_string()),
@@ -1242,6 +1245,10 @@ mod tests {
request_payload["referenceImageSrc"],
json!("data:image/png;base64,spec")
);
assert_eq!(
request_payload["referenceImageSrcs"],
json!(["data:image/png;base64,style"])
);
assert!(request_payload.get("priceMudPoints").is_none());
let response_payload = serde_json::to_value(EditorIconSpritesheetGenerateResponse {
@@ -61,6 +61,21 @@ impl SpacetimeClient {
.await
}
pub async fn get_asset_object_by_object_key(
&self,
object_key: String,
) -> Result<Option<AssetObjectRecord>, SpacetimeClientError> {
self.read_after_connect("get_asset_object_by_object_key", move |connection| {
Ok(connection
.db()
.asset_object()
.iter()
.find(|row| row.object_key == object_key)
.map(map_asset_object_row))
})
.await
}
pub async fn bind_asset_object_to_entity(
&self,
input: module_assets::AssetEntityBindingInput,
@@ -1847,12 +1847,7 @@ fn project_resource_input_copies_source_media(
.find(&source_resource_id.to_string())
.filter(|source_resource| source_resource.owner_user_id == owner_user_id)
.is_some_and(|source_resource| {
project_resource_matches_media(
&source_resource,
asset_object_id,
object_key,
image_src,
)
project_resource_matches_media(&source_resource, asset_object_id, object_key, image_src)
})
}
@@ -1883,12 +1878,7 @@ fn find_reusable_project_resource_for_input(
.as_deref()
.map(str::trim)
.is_some_and(|value| value == source_resource_id))
&& project_resource_matches_media(
resource,
asset_object_id,
object_key,
image_src,
)
&& project_resource_matches_media(resource, asset_object_id, object_key, image_src)
})
.collect::<Vec<_>>();
matches.sort_by(|left, right| {
@@ -1934,12 +1924,7 @@ fn matching_media_ref(left: &str, right: &str) -> bool {
}
fn normalize_media_ref(value: &str) -> String {
let trimmed_value = value
.trim()
.split('?')
.next()
.unwrap_or("")
.trim();
let trimmed_value = value.trim().split('?').next().unwrap_or("").trim();
let path_value = if let Some(protocol_index) = trimmed_value.find("://") {
let after_protocol = &trimmed_value[protocol_index + 3..];
after_protocol
@@ -676,6 +676,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
expect(editEditorImageMock).toHaveBeenCalledWith(
expect.objectContaining({
sourceImageSrc: 'data:image/png;base64,marked-source',
referenceImageSrcs: ['data:image/png;base64,style.png'],
}),
);
});
@@ -1015,7 +1015,9 @@ export function useImageCanvasGenerationSubmissionWorkflow({
try {
const needsExtraReferenceImageSrcs =
panelMode === 'redraw' || isVideoQuickEditSource(quickEditSourceLayer);
panelMode === 'quick-edit' ||
panelMode === 'redraw' ||
isVideoQuickEditSource(quickEditSourceLayer);
const extraReferenceImageSrcs = needsExtraReferenceImageSrcs
? await Promise.all(
quickEditReferences.map((reference) =>
@@ -1215,6 +1217,9 @@ export function useImageCanvasGenerationSubmissionWorkflow({
sourceImageSrc: sourceReferenceImageSrc,
size: quickEditSize,
model: normalizeEditorImageModel(quickEditPanel.model),
...(extraReferenceImageSrcs.length
? { referenceImageSrcs: extraReferenceImageSrcs }
: {}),
projectId,
assetKind: quickEditSourceLayer.assetKind,
generationInputs,
@@ -1324,6 +1324,7 @@ describe('editorProjectClient', () => {
sourceImageSrc: 'data:image/png;base64,source',
size: '1024x1024',
model: 'gpt-image-2',
referenceImageSrcs: ['data:image/png;base64,style'],
projectId: 'editor-project-1',
assetKind: 'character',
assetFolderId: 'project',
@@ -1346,6 +1347,7 @@ describe('editorProjectClient', () => {
sourceImageSrc: 'data:image/png;base64,source',
size: '1024x1024',
model: 'gpt-image-2',
referenceImageSrcs: ['data:image/png;base64,style'],
projectId: 'editor-project-1',
assetKind: 'character',
generationInputs: {
@@ -187,6 +187,7 @@ export type EditorImageEditInput = {
sourceImageSrc: string;
size?: string;
model?: string;
referenceImageSrcs?: string[];
projectId?: string | null;
assetKind?: string | null;
generationInputs?: EditorAssetGenerationInputs | null;
@@ -832,6 +833,9 @@ export async function editEditorImage(input: EditorImageEditInput) {
sourceImageSrc: input.sourceImageSrc,
...(input.size ? { size: input.size } : {}),
...(input.model ? { model: input.model } : {}),
...(input.referenceImageSrcs?.length
? { referenceImageSrcs: input.referenceImageSrcs }
: {}),
...(input.projectId ? { projectId: input.projectId } : {}),
...(input.assetKind ? { assetKind: input.assetKind } : {}),
...(input.generationInputs