修复编辑器参考图与后台日期问题
后台 Dashboard 默认日期按北京时间计算 图片快编请求补齐额外参考图、模型和尺寸参数 后端校验参考图 objectKey 归属并放宽编辑请求体限制 同步资产契约、测试和相关文档
This commit is contained in:
@@ -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 objectKey;objectKey 必须归属于当前账号的 `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。
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user