重构图标规范与图集生成链路

新增图标规范独立队列任务并将参数补全纳入统一计费操作
将图标图集生成和手工拆分迁入独立图标模块并复用公共生图能力
将图标规范引用收紧为项目资源或素材 ID
同步 SpacetimeDB 引用解析契约、External OpenAPI、前端与 Agent 提交链路
补充相关回归测试和长期架构约定
This commit is contained in:
2026-08-06 15:43:10 +08:00
parent e6f0e6248e
commit 3fdbe81bfe
30 changed files with 3042 additions and 2691 deletions
@@ -3360,19 +3360,19 @@
"EditorIconSpritesheetGenerationRequest": {
"type": "object",
"required": [
"referenceImageSrc",
"referenceId",
"iconDescriptions"
],
"properties": {
"referenceImageSrc": {
"referenceId": {
"type": "string",
"description": "图标规范的稳定引用:当前账号的 objectKey、项目资源 ID 或素材 ID;本地临时图必须先上传 OSS。禁止 Data URL / Blob URL。"
"description": "当前账号中已登记为 icon-spec 的项目资源 ID 或素材 ID。只接受 ID,不接受 objectKey、图片 URL、Data URL Blob URL。"
},
"referenceImageSrcs": {
"type": "array",
"items": {
"type": "string",
"description": "额外图标素材参考图的稳定引用:objectKey、项目资源 ID 或素材 ID;本地临时图必须先上传 OSS。禁止 Data URL / Blob URL。referenceImageSrc 占用 1 张 provider 容量,因此 gpt-image-2 最多再提交 4 张、nanobanana2 最多再提交 8 张;超限返回 400,不会静默截断。"
"description": "额外图标素材参考图的稳定引用:objectKey、项目资源 ID 或素材 ID;本地临时图必须先上传 OSS。禁止 Data URL / Blob URL。referenceId 占用 1 张 provider 容量,因此 gpt-image-2 最多再提交 4 张、nanobanana2 最多再提交 8 张;超限返回 400,不会静默截断。"
},
"maxItems": 8
},
@@ -6637,3 +6637,11 @@
- 图标规范 HTTP handler 在调用文本 LLM 前,先通过可复用图片请求预检完成参考图稳定性、owner 授权、Provider 配置与运行时定价校验;随后补齐 `ExtraParam` 与最终 prompt,并继续走既有 `editor_image_generation` inline / queue 分流,不新增图标规范专用 worker job。最终执行仍重新校验请求,以处理排队期间发生的权限或资源变化。
- 图标图集主规范引用由通用 SpacetimeDB procedure `resolve_editor_reference_and_return` 在同一事务快照内解析和校验 owner。ID 使用资源 / 素材主键;objectKey 按规范化 `image_src="/<objectKey>"` 索引读取唯一行,并使用 `asset_object(bucket, object_key)` 复合索引校验对象 owner。procedure 不接收业务 / 存储类型 allowlist,复用既有 `EditorProjectResourceSnapshot` / `EditorAssetSnapshot` 返回完整单行,不新增图标专属 DTO,也不再拉取完整项目和素材库;`icon-spec` 业务类型与游戏类型由 API 业务代码校验和提取。缺失引用、跨 owner、asset object 不匹配、业务类型错误、元数据解析与数据库错误全部失败关闭;仅“合法记录没有 genre”允许返回 `None`
- 关联文档:`docs/【编辑器】生成类面板Lovart统一改造方案-2026-06-17.md``docs/【编辑器】画板图标素材生成入口设计-2026-06-15.md``docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`
## 2026-08-06 图标规范与图标图集使用独立任务及 ID-only 主规范引用
- 本条取代上一条“不新增图标规范专用 worker job”和“图标图集主规范允许 objectKey”的结论。图标规范生成使用独立 `editor_icon_spec_generation` job kind;队列保存原始强类型业务参数,worker 在统一计费操作预扣成功后执行 ExtraParam LLM、构建最终 prompt,再进入共享图片生成和既有后处理 / 持久化链。余额不足不调用文本 LLM,执行失败沿用统一退款;普通 `editor_image_generation` 的 DTO、payload 与执行行为保持不变。
- `editor_project_icon.rs` 独立承接图标规范、图标 spritesheet、自动切片和手工拆分。普通图片模块只暴露可复用的 prompt builder 执行入口与强类型 provider 请求分流;nanobanana、无参考图 generation、有参考图 edit 的选择集中在同一个 helper,图标图集复用该 helper,不复制 provider 分支。
- 图标规范生成的可选参考图与图标图集的必选主规范统一使用 `referenceId`,只接受当前 owner 的项目资源 ID 或账号素材 ID,不接受 objectKey、URL 或临时 key。SpacetimeDB `resolve_editor_reference_and_return` 因而只接收 `reference_id` 并按两张表主键查询,删除 `image_src` 二级索引。图标图集的普通附加参考图 `referenceImageSrcs` 仍允许 owned objectKey、项目资源 ID 或素材 ID,并继续走既有 owner 校验与真实图片下载链。
- External v1 图标图集请求与 OpenAPI 同步改为必填 `referenceId`。前端和 Editor Agent 优先传正式 resource ID,其次传 asset ID;本地临时 ID、objectKey 和图片地址不得回退成主规范引用,未登记时明确失败并要求先上传或登记。
- 关联:`server-rs/crates/api-server/src/editor_project_icon.rs``server-rs/crates/api-server/src/editor_project.rs``server-rs/crates/spacetime-module/src/editor_project_storage.rs``docs/openapi/genarrative-external-v1.openapi.json`
@@ -338,7 +338,7 @@
- 现象:画板生成、快速编辑、图标素材或 UI 素材提取如果允许直接提交 generated objectKey,用户只要知道其他账号的私有 objectKey,就可能让 api-server 签名读取并送给外部生成供应商。
- 原因:Data URL/Blob URL 只允许停留在浏览器临时态,正式编辑器引用必须先上传并经统一 resolver 校验归属。
- 处理:所有私有对象引用在读取字节或签发 URL 前统一走 `resolve_editor_reference_object_key_for_owner(state, owner_user_id, source)`,先在当前账号的项目资源、素材库资产或 `asset_object` 中匹配 owner / bucket / key。只有确实需要图片字节的入口(生成 / 重绘 / 图标 / UI 提取等交给 provider 的路径)再走 `parse_editor_reference_image`(内部仍先 resolve,再下载 OSS 字节);手动去背景等只签发短期 URL 的入口不要 `parse` 整图。手动去背景还要恢复源模型时,object key 解析、所有权校验和源模型回溯必须复用同一轮账号项目 / 素材快照,项目与素材快照各最多读取一次;不得先走通用 resolver 全量读取,再为模型回溯重复拉取完整画布和素材库。图标素材等额外参考图必须真实传到 provider,不只写 metadata;图片快速编辑当前不开放额外参考图,若后续重开入口也必须沿用同一归属校验。
- 处理:普通图片、重绘、图标图集附加参考图和 UI 提取等允许 objectKey 的私有对象引用在读取字节或签发 URL 前统一走 `resolve_editor_reference_object_key_for_owner(state, owner_user_id, source)`,先在当前账号的项目资源、素材库资产或 `asset_object` 中匹配 owner / bucket / key。只有确实需要图片字节的入口再走 `parse_editor_reference_image`(内部仍先 resolve,再下载 OSS 字节);手动去背景等只签发短期 URL 的入口不要 `parse` 整图。图标规范生成的可选参考图与图标图集的主规范引用是更窄的业务契约:只接受正式 `referenceId`(项目资源 ID / 素材 ID),不得回退 objectKey、URL 或临时 key;图标图集的额外 `referenceImageSrcs` 才继续沿用通用 owned objectKey 规则。手动去背景还要恢复源模型时,object key 解析、所有权校验和源模型回溯必须复用同一轮账号项目 / 素材快照,项目与素材快照各最多读取一次;不得先走通用 resolver 全量读取,再为模型回溯重复拉取完整画布和素材库。图标素材等额外参考图必须真实传到 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`
@@ -630,7 +630,7 @@ Responses 的终态载荷既是工具调用的恢复源,也是正文的恢复
- `generation_inputs_json` 包络契约:`fields` / `references` 是图片信息读取的用户可见生成输入快照;顶层允许保存后端内部结果扩展。现有 `screenColorHex` 保存实际背景色,角色、图标图集和 UI 图集抠图派生资产使用 `mattingProvider` / `mattingModel` 保存实际成功的处理后端与模型。BgFilter 保存本次 `seg_model`,阿里云通用抠图保存 `Aliyun Matting / segment-common-image`,本地键色保存 `Genarrative Local / screen-color-keying`。同源画布 BFF 的角色、图标和 UI 请求由前端自动提交 `screenColor=auto` 与默认 `segModel=birefnet`,其中 `segModel` 是不可由用户选择的请求控制字段,不进入 `generationInputs``background_mode``cross_check` 只属于 api-server 到 worker 的内部 RPC。External OpenAPI 不开放 `segModel`。上述内部结果字段不写入 `fields`,普通用户(包括素材 owner)与匿名公开读取均不得取得;普通用户响应还必须省略素材顶层 `provider` 和内部处理 `model`,但保留正常用户可见 `model` 与其他合法的顶层功能字段。后台管理和服务端审计可读取原始值。过滤只作用于普通用户 / 公开响应边界,不修改素材或精选快照,因此历史数据无需迁移。
- 普通用户生成结果契约:图片、图标图集、视频、音频和角色动画的完成响应与新建画布图层均不返回或写入生成 provider;项目资源、素材、精选和 Agent 紧凑结果使用同一读取边界。真实 provider 只保留在持久化、tracking / tracing 和后台管理原始审计中。该规则针对生成供应商元数据,不改变直传票据等必须由客户端执行的存储协议字段。
- 普通用户错误契约:手动去背景和角色动作透明化的原始服务端错误可能包含 BgFilter、分割模型或 provider 细节;Owner HTTP 响应与外部任务状态必须按 job kind 返回稳定业务文案,原始错误只保留在任务记录、tracing 与后台审计。
- 索引:`by_editor_project_resource_project_id``by_editor_project_resource_owner_user_id``by_editor_project_resource_image_src`
- 索引:`by_editor_project_resource_project_id``by_editor_project_resource_owner_user_id`
### `editor_asset_folder`
@@ -644,7 +644,7 @@ Responses 的终态载荷既是工具调用的恢复源,也是正文的恢复
- Rust 结构体:`EditorAsset`
- 源码:`server-rs/crates/spacetime-module/src/editor_project_storage.rs`
- 说明:图片画布账号级素材表,保存用户上传 / 生成素材的名称、文件夹、图片读取地址、可选封面 `thumbnail_src`、OSS 引用、尺寸、来源类型、prompt、provider、真实操作 `task_id`、可选后台归组 `group_task_id`、拆分批次预期数量 `group_task_expected_asset_count``asset_kind``generation_inputs_json`、可选 `source_resource_id``generation_cost_mud_points``prompt` 固定表示规范化后的用户原始意图,供跨资源搜索和用户侧元数据使用;provider 实际返回的改写只写 `actual_prompt`,提交给 provider 的系统 / 工程化 prompt 不得写入 `prompt`。角色透明图、图标透明图和自动切片等派生产物继承源用户 prompt,并用 `source_resource_id``generation_inputs_json`、provider / asset kind 表达处理来源。归组字段追加在表尾并默认 `None`,只用于稳定派生任务的后台分组,不替代 `task_id`;批次是否初始完整以独立完成事实为准,不按当前剩余素材数反推。素材在同一账号的所有项目中可见;图片 / 图标 / UI 提取等生成 BFF 在请求携带 `asset_folder_id` 时负责创建账号级生成素材并返回 asset 快照,若同次生成也创建了 `editor_project_resource`,则把该 `resource_id` 写入 `source_resource_id`。角色动作生成保留原始绿幕视频中间素材,同时把最终帧序列作为一条 `asset_kind = character-animation` 素材入库:首帧写入 `image_src` / `thumbnail_src`,完整帧列表、FPS、时长和预览视频写入 `generation_inputs_json.characterAnimation`,不把每帧拆成独立素材。生成视频会抽取首帧封面写入 `thumbnail_src`,素材库和再次放入画布时用它作为 video poster。素材库快照通过 `asset_id` 回查对应 `editor_showcase_asset`,供左侧素材菜单展示 `pending` / `approved` / `rejected` 审核状态;公开事实不落在账号素材表,素材库只发起提交审核。素材放入画布时复制为 `editor_project_resource` 并由图层引用 resourceId,画布从 resource / asset 级元数据恢复素材类别和用户可见生成输入快照。
- 索引:`by_editor_asset_owner_user_id``by_editor_asset_folder_id``by_editor_asset_image_src`
- 索引:`by_editor_asset_owner_user_id``by_editor_asset_folder_id`
### `editor_asset_group_source_provenance`
@@ -13,6 +13,7 @@ pub fn build_tool_context(document: &EditorAgentConversationMessagesDocument) ->
let image_id = ImageId::from_data_key(&data_key);
let metadata = ImageMetadata {
data_key,
reference_id: Some(a.reference_id.clone()),
image_src: a.image_src.clone(),
object_key: a.object_key.clone(),
thumbnail_src: a.thumbnail_src.clone(),
@@ -29,6 +30,7 @@ pub fn build_tool_context(document: &EditorAgentConversationMessagesDocument) ->
let image_id = ImageId::from_data_key(&data_key);
let metadata = ImageMetadata {
data_key,
reference_id: img.resource_id.clone(),
image_src: img.image_src.clone(),
object_key: img.object_key.clone(),
thumbnail_src: img.thumbnail_src.clone(),
@@ -53,9 +53,8 @@ use crate::editor_generation_queue::{
EDITOR_IMAGE_EDIT_JOB_KIND, EDITOR_IMAGE_GENERATION_JOB_KIND,
EDITOR_SOUND_EFFECT_GENERATION_JOB_KIND, EDITOR_VIDEO_GENERATION_JOB_KIND,
};
use crate::editor_project::{
EditorIconSpritesheetGenerationRequest, EditorImageEditRequest, EditorImageGenerationRequest,
};
use crate::editor_project::{EditorImageEditRequest, EditorImageGenerationRequest};
use crate::editor_project_icon::EditorIconSpritesheetGenerationRequest;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum EditorAgentToolError {
@@ -792,14 +791,19 @@ impl EditorAgentTool for GenerateIconSpritesheetTool {
) -> Result<PreparedEditorAgentJob, EditorAgentToolError> {
let price_mud_points = self.pricing(context.pricing, args)?;
let args: GenerateIconSpritesheetToolArgs = parse_invalid_args(Self::NAME, args)?;
let reference_image_src = self
let reference_id = self
.context
.image_data_key(&args.reference_image_id)
.ok_or_else(|| EditorAgentToolError::invalid_args("reference image not found"))?
.image_metadata(&args.reference_image_id)
.and_then(|metadata| metadata.reference_id.as_deref())
.ok_or_else(|| {
EditorAgentToolError::invalid_args(
"reference image is not a registered resource or asset",
)
})?
.to_string();
let reference_image_srcs = resolve_image_ids(&args.reference_image_ids, &self.context)?;
let payload = EditorIconSpritesheetGenerationRequest {
reference_image_src,
reference_id,
reference_image_srcs: Some(reference_image_srcs),
icon_descriptions: args.icon_descriptions,
style: None,
@@ -1184,6 +1188,7 @@ mod tests {
},
ImageMetadata {
data_key: format!("generated/{image_id}.png"),
reference_id: Some(format!("resource-{image_id}")),
image_src: format!("/api/assets/read/generated/{image_id}.png"),
object_key: Some(format!("generated/{image_id}.png")),
thumbnail_src: Some(format!("/api/assets/read/generated/{image_id}-thumb.png")),
@@ -11,6 +11,7 @@ use spacetime_client::{ExternalGenerationJobEnqueueRecordInput, ExternalGenerati
use crate::{http_error::AppError, request_context::RequestContext, state::AppState};
pub(crate) const EDITOR_IMAGE_GENERATION_JOB_KIND: &str = "editor_image_generation";
pub(crate) const EDITOR_ICON_SPEC_GENERATION_JOB_KIND: &str = "editor_icon_spec_generation";
pub(crate) const EDITOR_IMAGE_EDIT_JOB_KIND: &str = "editor_image_edit";
pub(crate) const EDITOR_BACKGROUND_REMOVAL_JOB_KIND: &str = "editor_background_removal";
pub(crate) const EDITOR_ICON_SPRITESHEET_GENERATION_JOB_KIND: &str =
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -21,6 +21,9 @@ use spacetime_client::{
ExternalGenerationJobRecord, SpacetimeClientError,
};
use crate::editor_project_icon::{
EditorIconSpritesheetGenerationRequest, enqueue_editor_icon_spritesheet_generation_for_owner,
};
use crate::{
api_response::json_success_body,
character_animation_assets::{
@@ -31,14 +34,12 @@ use crate::{
EDITOR_ASSET_FOLDER_ID_PREFIX, EDITOR_ASSET_ID_PREFIX, EDITOR_PROJECT_DEFAULT_TITLE,
EDITOR_PROJECT_ID_PREFIX, EDITOR_RESOURCE_ID_PREFIX, EditorAssetFolderPayload,
EditorAssetLibraryPayload, EditorAssetPayload, EditorCanvasViewportPayload,
EditorGenerationCaller, EditorIconSpritesheetGenerationRequest, EditorImageEditRequest,
EditorImageGenerationRequest, EditorProjectPayload, EditorProjectResourcePayload,
EditorUiDesignAssetExtractionRequest, current_utc_micros,
editor_asset_folder_payload_from_record, editor_asset_library_payload_from_record,
editor_asset_payload_from_record, editor_project_payload_from_record,
editor_project_resource_payload_from_record,
enqueue_editor_icon_spritesheet_generation_for_owner, enqueue_editor_image_edit_for_owner,
enqueue_editor_image_generation_for_owner,
EditorGenerationCaller, EditorImageEditRequest, EditorImageGenerationRequest,
EditorProjectPayload, EditorProjectResourcePayload, EditorUiDesignAssetExtractionRequest,
current_utc_micros, editor_asset_folder_payload_from_record,
editor_asset_library_payload_from_record, editor_asset_payload_from_record,
editor_project_payload_from_record, editor_project_resource_payload_from_record,
enqueue_editor_image_edit_for_owner, enqueue_editor_image_generation_for_owner,
enqueue_editor_ui_design_asset_extraction_for_owner, map_editor_project_error,
normalize_editor_persisted_media_src, normalize_optional_string,
parse_editor_generation_json_payload, sanitize_editor_untrusted_generation_inputs,
@@ -1370,6 +1371,18 @@ mod tests {
.get("/api/external/v1/editor/icon-spritesheets/generations")
.is_some()
);
let icon_spritesheet_request =
&parsed["components"]["schemas"]["EditorIconSpritesheetGenerationRequest"];
assert!(
icon_spritesheet_request["required"]
.as_array()
.is_some_and(|required| required.contains(&json!("referenceId")))
);
assert!(
icon_spritesheet_request["properties"]
.get("referenceImageSrc")
.is_none()
);
let icon_style_schema = &parsed["components"]["schemas"]["EditorIconSpritesheetGenerationRequest"]
["properties"]["style"];
assert_eq!(icon_style_schema["anyOf"][0]["type"], "string");
@@ -34,19 +34,21 @@ use crate::{
config::AppConfig,
editor_generation_queue::{
EDITOR_BACKGROUND_MUSIC_GENERATION_JOB_KIND, EDITOR_BACKGROUND_REMOVAL_JOB_KIND,
EDITOR_CHARACTER_ANIMATION_GENERATION_JOB_KIND,
EDITOR_CHARACTER_ANIMATION_GENERATION_JOB_KIND, EDITOR_ICON_SPEC_GENERATION_JOB_KIND,
EDITOR_ICON_SPRITESHEET_GENERATION_JOB_KIND, EDITOR_IMAGE_EDIT_JOB_KIND,
EDITOR_IMAGE_GENERATION_JOB_KIND, EDITOR_SOUND_EFFECT_GENERATION_JOB_KIND,
EDITOR_UI_DESIGN_ASSET_EXTRACTION_JOB_KIND, EDITOR_VIDEO_GENERATION_JOB_KIND,
},
editor_project::{
EDITOR_GENERATION_MULTIPLE_WARNINGS_CODE, EditorBackgroundRemovalRequest,
EditorGenerationCaller, EditorGenerationPhaseReporter,
EditorIconSpritesheetGenerationRequest, EditorImageEditRequest,
EditorGenerationCaller, EditorGenerationPhaseReporter, EditorImageEditRequest,
EditorImageGenerationRequest, EditorUiDesignAssetExtractionRequest,
edit_editor_image_for_owner, extract_editor_ui_design_assets_for_owner,
generate_editor_icon_spritesheet_for_owner, generate_editor_image_for_owner,
remove_editor_image_background_for_owner,
generate_editor_image_for_owner, remove_editor_image_background_for_owner,
},
editor_project_icon::{
EditorIconSpecGenerationRequest, EditorIconSpritesheetGenerationRequest,
generate_editor_icon_spritesheet_for_owner, generate_icon_spec_for_owner,
},
request_context::RequestContext,
state::AppState,
@@ -793,6 +795,42 @@ async fn process_external_generation_job_once(
}
}
}
EDITOR_ICON_SPEC_GENERATION_JOB_KIND => {
let payload = match serde_json::from_str::<EditorIconSpecGenerationRequest>(
job.request_payload_json.as_str(),
) {
Ok(payload) => payload,
Err(error) => {
let message = format!("图片画布图标规范任务参数解析失败:{error}");
fail_job(&state, &worker_id, &job, message.clone()).await?;
return Err(message);
}
};
let request_context = worker_request_context(&job, provider_deadline);
match generate_icon_spec_for_owner(
&state,
&request_context,
editor_generation_worker_caller(&worker_id, &job)?,
payload,
)
.await
{
Ok(response) => {
complete_editor_generation_job_with_response(
&state,
&worker_id,
&job,
&response.0,
)
.await
}
Err(error) => {
let message = error.body_text();
fail_job(&state, &worker_id, &job, message.clone()).await?;
Err(message)
}
}
}
EDITOR_IMAGE_EDIT_JOB_KIND => {
let payload = match serde_json::from_str::<EditorImageEditRequest>(
job.request_payload_json.as_str(),
@@ -1699,6 +1737,7 @@ fn external_generation_worker_job_timeout(config: &AppConfig, job_kind: &str) ->
match job_kind {
EDITOR_IMAGE_GENERATION_JOB_KIND
| EDITOR_IMAGE_EDIT_JOB_KIND
| EDITOR_ICON_SPEC_GENERATION_JOB_KIND
| EDITOR_ICON_SPRITESHEET_GENERATION_JOB_KIND
| EDITOR_UI_DESIGN_ASSET_EXTRACTION_JOB_KIND
| EDITOR_CHARACTER_ANIMATION_GENERATION_JOB_KIND
@@ -2403,6 +2442,10 @@ mod tests {
external_generation_worker_job_timeout(&config, EDITOR_IMAGE_EDIT_JOB_KIND),
Duration::from_secs(600)
);
assert_eq!(
external_generation_worker_job_timeout(&config, EDITOR_ICON_SPEC_GENERATION_JOB_KIND),
Duration::from_secs(600)
);
assert_eq!(
external_generation_worker_job_timeout(
&config,
+1 -1
View File
@@ -26,7 +26,7 @@ mod editor_generation_config;
mod editor_generation_queue;
mod editor_green_screen;
mod editor_project;
mod editor_project_icon_specs;
mod editor_project_icon;
mod editor_screen_background_decision;
mod editor_screen_background_filter;
mod error_middleware;
@@ -18,15 +18,17 @@ use crate::{
EDITOR_LAYOUT_REQUEST_BODY_MAX_BYTES, create_editor_asset, create_editor_asset_folder,
create_editor_project, create_editor_project_resource, delete_editor_asset,
delete_editor_asset_folder, delete_editor_project, edit_editor_image,
extract_editor_ui_design_assets, generate_editor_icon_spritesheet, generate_editor_image,
get_editor_asset_library, get_editor_generation_pricing, get_editor_project,
list_editor_projects, list_public_editor_project_resources, load_recent_editor_project,
extract_editor_ui_design_assets, generate_editor_image, get_editor_asset_library,
get_editor_generation_pricing, get_editor_project, list_editor_projects,
list_public_editor_project_resources, load_recent_editor_project,
remove_editor_image_background, rename_editor_project, save_editor_project_layout,
snap_editor_image_to_pixel_art, split_editor_icon_spritesheet,
submit_editor_asset_showcase, toggle_editor_showcase_asset_like, update_editor_asset,
update_editor_asset_folder, update_editor_project_resource_showcase,
snap_editor_image_to_pixel_art, submit_editor_asset_showcase,
toggle_editor_showcase_asset_like, update_editor_asset, update_editor_asset_folder,
update_editor_project_resource_showcase,
},
editor_project_icon::{
generate_editor_icon_spritesheet, generate_icon_spec, split_editor_icon_spritesheet,
},
editor_project_icon_specs::generate_icon_spec,
llm::icon_specs::{refine_art_style, refine_game_play},
state::AppState,
};
@@ -1,4 +1,4 @@
use super::super::editor_project_icon_specs::{ExtraParam, GameGenre, RequestParam};
use super::super::editor_project_icon::{ExtraParam, GameGenre, RequestParam};
use crate::editor_green_screen::EditorScreenBackgroundColor;
pub const PROMPT_MAX_LEN: usize = 200;
@@ -39,6 +39,7 @@ impl Display for ImageId {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImageMetadata {
pub data_key: String,
pub reference_id: Option<String>,
pub image_src: String,
pub object_key: Option<String>,
pub thumbnail_src: Option<String>,
@@ -257,6 +257,7 @@ mod tests {
reference_image_id.clone(),
ImageMetadata {
data_key: "asset://reference-image".to_string(),
reference_id: Some("resource-reference-image".to_string()),
image_src: "asset://reference-image".to_string(),
object_key: None,
thumbnail_src: None,
@@ -4,8 +4,7 @@ use super::*;
pub struct EditorReferenceResolveRecordInput {
pub owner_user_id: String,
pub bucket: String,
pub reference_id: Option<String>,
pub object_key: Option<String>,
pub reference_id: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
@@ -22,7 +21,6 @@ impl From<EditorReferenceResolveRecordInput>
owner_user_id: input.owner_user_id,
bucket: input.bucket,
reference_id: input.reference_id,
object_key: input.object_key,
}
}
}
@@ -4,8 +4,7 @@ use super::*;
pub struct EditorReferenceResolveRecordInput {
pub owner_user_id: String,
pub bucket: String,
pub reference_id: Option<String>,
pub object_key: Option<String>,
pub reference_id: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
@@ -21,7 +20,6 @@ impl From<EditorReferenceResolveRecordInput> for crate::module_bindings::EditorR
owner_user_id: input.owner_user_id,
bucket: input.bucket,
reference_id: input.reference_id,
object_key: input.object_key,
}
}
}
@@ -114,7 +114,6 @@ impl __sdk::__query_builder::HasCols for EditorAsset {
pub struct EditorAssetIxCols {
pub asset_id: __sdk::__query_builder::IxCol<EditorAsset, String>,
pub folder_id: __sdk::__query_builder::IxCol<EditorAsset, String>,
pub image_src: __sdk::__query_builder::IxCol<EditorAsset, String>,
pub owner_user_id: __sdk::__query_builder::IxCol<EditorAsset, String>,
}
@@ -124,7 +123,6 @@ impl __sdk::__query_builder::HasIxCols for EditorAsset {
EditorAssetIxCols {
asset_id: __sdk::__query_builder::IxCol::new(table_name, "asset_id"),
folder_id: __sdk::__query_builder::IxCol::new(table_name, "folder_id"),
image_src: __sdk::__query_builder::IxCol::new(table_name, "image_src"),
owner_user_id: __sdk::__query_builder::IxCol::new(table_name, "owner_user_id"),
}
}
@@ -97,7 +97,6 @@ impl __sdk::__query_builder::HasCols for EditorProjectResource {
///
/// Provides typed access to indexed columns for query building.
pub struct EditorProjectResourceIxCols {
pub image_src: __sdk::__query_builder::IxCol<EditorProjectResource, String>,
pub owner_user_id: __sdk::__query_builder::IxCol<EditorProjectResource, String>,
pub project_id: __sdk::__query_builder::IxCol<EditorProjectResource, String>,
pub resource_id: __sdk::__query_builder::IxCol<EditorProjectResource, String>,
@@ -107,7 +106,6 @@ impl __sdk::__query_builder::HasIxCols for EditorProjectResource {
type IxCols = EditorProjectResourceIxCols;
fn ix_cols(table_name: &'static str) -> Self::IxCols {
EditorProjectResourceIxCols {
image_src: __sdk::__query_builder::IxCol::new(table_name, "image_src"),
owner_user_id: __sdk::__query_builder::IxCol::new(table_name, "owner_user_id"),
project_id: __sdk::__query_builder::IxCol::new(table_name, "project_id"),
resource_id: __sdk::__query_builder::IxCol::new(table_name, "resource_id"),
@@ -9,8 +9,7 @@ use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
pub struct EditorReferenceResolveInput {
pub owner_user_id: String,
pub bucket: String,
pub reference_id: Option<String>,
pub object_key: Option<String>,
pub reference_id: String,
}
impl __sdk::InModule for EditorReferenceResolveInput {
@@ -221,8 +221,7 @@ pub struct EditorCanvasLayoutMigration {
#[spacetimedb::table(
accessor = editor_project_resource,
index(accessor = by_editor_project_resource_project_id, btree(columns = [project_id])),
index(accessor = by_editor_project_resource_owner_user_id, btree(columns = [owner_user_id])),
index(accessor = by_editor_project_resource_image_src, btree(columns = [image_src]))
index(accessor = by_editor_project_resource_owner_user_id, btree(columns = [owner_user_id]))
)]
#[derive(Clone)]
pub struct EditorProjectResource {
@@ -271,8 +270,7 @@ pub struct EditorAssetFolder {
#[spacetimedb::table(
accessor = editor_asset,
index(accessor = by_editor_asset_owner_user_id, btree(columns = [owner_user_id])),
index(accessor = by_editor_asset_folder_id, btree(columns = [folder_id])),
index(accessor = by_editor_asset_image_src, btree(columns = [image_src]))
index(accessor = by_editor_asset_folder_id, btree(columns = [folder_id]))
)]
pub struct EditorAsset {
#[primary_key]
@@ -611,8 +609,7 @@ pub struct EditorProjectResourceMediaRepairInput {
pub struct EditorReferenceResolveInput {
pub owner_user_id: String,
pub bucket: String,
pub reference_id: Option<String>,
pub object_key: Option<String>,
pub reference_id: String,
}
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
@@ -6113,60 +6110,27 @@ fn resolve_editor_reference(
if owner_user_id.is_empty() || bucket.is_empty() {
return Err("编辑器引用的 owner_user_id 与 bucket 不能为空".to_string());
}
let reference_id = input
.reference_id
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty());
let object_key = input
.object_key
.as_deref()
.map(str::trim)
.map(|value| value.trim_start_matches('/'))
.filter(|value| !value.is_empty());
if reference_id.is_some() == object_key.is_some() {
return Err("编辑器引用必须且只能提供 reference_id 或 object_key".to_string());
let reference_id = input.reference_id.trim();
if reference_id.is_empty() {
return Err("编辑器引用的 reference_id 不能为空".to_string());
}
if let Some(reference_id) = reference_id {
if let Some(resource) = ctx
.db
.editor_project_resource()
.resource_id()
.find(&reference_id.to_string())
{
return resolve_editor_reference_resource(ctx, bucket, owner_user_id, &resource);
}
if let Some(asset) = ctx
.db
.editor_asset()
.asset_id()
.find(&reference_id.to_string())
{
return resolve_editor_reference_asset(ctx, bucket, owner_user_id, asset);
}
return Err("编辑器引用不存在".to_string());
}
let object_key = object_key.expect("reference shape checked above");
let image_src = format!("/{object_key}");
for resource in ctx
if let Some(resource) = ctx
.db
.editor_project_resource()
.by_editor_project_resource_image_src()
.filter(&image_src)
.resource_id()
.find(&reference_id.to_string())
{
return resolve_editor_reference_resource(ctx, bucket, owner_user_id, &resource);
}
for asset in ctx
if let Some(asset) = ctx
.db
.editor_asset()
.by_editor_asset_image_src()
.filter(&image_src)
.asset_id()
.find(&reference_id.to_string())
{
return resolve_editor_reference_asset(ctx, bucket, owner_user_id, asset);
}
Err("编辑器引用未关联资源或素材记录".to_string())
Err("编辑器引用不存在".to_string())
}
fn resolve_editor_reference_resource(
@@ -2701,7 +2701,7 @@ describe('ImageCanvasEditorView generation integration', () => {
await waitFor(() => {
expect(generateEditorIconSpritesheetMock).toHaveBeenCalledWith(
expect.objectContaining({
referenceImageSrc: 'resource-icon-spec',
referenceId: 'resource-icon-spec',
iconDescriptions: ['返回按钮\n设置按钮'],
model: 'gemini-3.1-flash-image-preview',
aspectRatio: '1:1',
@@ -3171,7 +3171,7 @@ describe('ImageCanvasEditorView generation integration', () => {
await waitFor(() => {
expect(generateEditorIconSpritesheetMock).toHaveBeenCalledWith(
expect.objectContaining({
referenceImageSrc: 'resource-editor-icon-spec-renamed-1',
referenceId: 'resource-editor-icon-spec-renamed-1',
iconDescriptions: ['背包按钮'],
model: 'gemini-3.1-flash-image-preview',
aspectRatio: '1:1',
@@ -243,7 +243,7 @@ describe('ImageCanvasGenerationSubmissionModel', () => {
input: {
playSetting: '休闲消除',
artStyle: '清爽卡通',
referenceImageSrcs: ['data:image/png;base64,ref'],
referenceId: 'resource-spec-ref',
generationInputs: {
fields: [
{ title: '玩法设定', value: '休闲消除' },
@@ -262,6 +262,20 @@ describe('ImageCanvasGenerationSubmissionModel', () => {
result: {
assetKind: 'icon-spec',
title: '图标规范 8',
generationInputs: {
fields: [
{ title: '玩法设定', value: '休闲消除' },
{ title: '美术风格', value: '清爽卡通' },
],
references: [
{
title: '参考图',
label: '参考.png',
refType: 'project-resource',
refId: 'resource-spec-ref',
},
],
},
},
});
expect(plan.kind === 'icon-spec' ? plan.result.generationInputs : null).toEqual(
@@ -733,7 +747,7 @@ describe('ImageCanvasGenerationSubmissionModel', () => {
ok: true,
iconDescriptions: ['返回按钮\n\n设置按钮'],
input: {
referenceImageSrc: 'data:image/png;base64,spec',
referenceId: 'resource-icon-spec',
referenceImageSrcs: ['data:image/png;base64,ref'],
iconDescriptions: ['返回按钮\n\n设置按钮'],
model: 'gpt-image-2',

Some files were not shown because too many files have changed in this diff Show More