合并master并保留Spine序列帧修复

合并master当前工程、后端、前端和文档更新

按已确认方案解决Spine序列帧多模态、帧数和快速编辑冲突

保留当前分支底部工具栏宽度与隐藏滚动条样式
This commit is contained in:
2026-08-14 21:33:26 +08:00
338 changed files with 69729 additions and 8828 deletions
+34 -2
View File
@@ -419,6 +419,38 @@ mod tests {
.expect("password login request should succeed")
}
#[tokio::test]
async fn showcase_invalid_bearer_is_not_downgraded_and_is_not_cacheable() {
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));
let response = app
.oneshot(
Request::builder()
.uri("/api/editor/showcase/resources")
.header("authorization", "Bearer invalid-token")
.body(Body::empty())
.expect("request should build"),
)
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
assert_eq!(
response
.headers()
.get("cache-control")
.and_then(|value| value.to_str().ok()),
Some("private, no-store")
);
assert_eq!(
response
.headers()
.get("vary")
.and_then(|value| value.to_str().ok()),
Some("Authorization")
);
}
#[cfg(any())]
fn build_internal_creative_agent_app() -> Router {
let mut config = AppConfig::default();
@@ -2390,7 +2422,7 @@ mod tests {
let app = build_router(state);
let request_body = serde_json::json!({
"prompt": "快速编辑图片",
"sourceImageSrc": "data:image/png;base64,AAAA",
"sourceReferenceId": "data:image/png;base64,AAAA",
"size": "1024x1024",
"model": "gpt-image-2"
})
@@ -2418,7 +2450,7 @@ mod tests {
.to_bytes();
let body_text = String::from_utf8_lossy(&body);
assert!(
body_text.contains("先上传 OSS"),
body_text.contains("只接受已登记的项目资源 ID 或素材 ID"),
"handler should reject inline editor edit sources: {body_text}"
);
}
+66
View File
@@ -1394,6 +1394,72 @@ mod tests {
assert!(payload["data"]["upload"].get("publicUrl").is_none());
}
#[tokio::test]
async fn direct_upload_ticket_accepts_asset_canvas_reference_namespace() {
let config = AppConfig {
oss_bucket: Some("genarrative-assets".to_string()),
oss_endpoint: Some("oss-cn-shanghai.aliyuncs.com".to_string()),
oss_access_key_id: Some("test-access-key-id".to_string()),
oss_access_key_secret: Some("test-access-key-secret".to_string()),
..AppConfig::default()
};
let state = AppState::new(config).expect("state should build");
let token =
seed_authenticated_token(&state, "13800138122", "sess_asset_canvas_reference_ticket")
.await;
let app = build_router(state);
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/api/assets/direct-upload-tickets")
.header("authorization", format!("Bearer {token}"))
.header("content-type", "application/json")
.header("x-genarrative-response-envelope", "1")
.body(Body::from(
json!({
"legacyPrefix": "generated-character-drafts",
"pathSegments": [
"editor",
"asset-canvas-references",
"project-123",
"draft-456",
"generation-789"
],
"fileName": "reference-sha256.png",
"contentType": "image/png",
"access": "private",
"maxSizeBytes": 4096,
"successActionStatus": 204
})
.to_string(),
))
.expect("request should build"),
)
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::OK);
let body = response
.into_body()
.collect()
.await
.expect("body should collect")
.to_bytes();
let payload: Value =
serde_json::from_slice(&body).expect("response body should be valid json");
let expected_object_key = "generated-character-drafts/editor/asset-canvas-references/project-123/draft-456/generation-789/reference-sha256.png";
assert_eq!(payload["data"]["upload"]["objectKey"], expected_object_key);
assert_eq!(
payload["data"]["upload"]["formFields"]["key"],
expected_object_key
);
assert_eq!(payload["data"]["upload"]["access"], "private");
assert_eq!(payload["data"]["upload"]["successActionStatus"], 204);
assert_eq!(payload["data"]["upload"]["maxSizeBytes"], 4096);
}
#[tokio::test]
async fn read_url_fails_closed_when_asset_metadata_authority_is_unavailable() {
let config = AppConfig {
@@ -11,7 +11,7 @@ use std::{
use axum::{
Json,
extract::{Extension, Path as AxumPath, Query, State, rejection::JsonRejection},
http::StatusCode,
http::{HeaderMap, StatusCode},
response::Response,
};
use image::{
@@ -43,10 +43,11 @@ use shared_contracts::assets::{
CharacterAssetJobStatusPayload, CharacterAssetJobStatusText, CharacterVisualDraftPayload,
CharacterWorkflowCacheGetResponse, CharacterWorkflowCachePayload,
CharacterWorkflowCacheSaveRequest, CharacterWorkflowCacheSaveResponse,
EditorCharacterAnimationFramePayload, EditorCharacterAnimationFrameSplitRequest,
EditorCharacterAnimationFrameSplitResponse, EditorCharacterAnimationGenerateRequest,
EditorCharacterAnimationGenerateResponse, EditorCharacterAnimationBackgroundRemovalRequest,
EditorCharacterAnimationBackgroundRemovalResponse, EditorCharacterAnimationVideoConversionRequest,
EditorCharacterAnimationBackgroundRemovalRequest,
EditorCharacterAnimationBackgroundRemovalResponse, EditorCharacterAnimationFramePayload,
EditorCharacterAnimationFrameSplitRequest, EditorCharacterAnimationFrameSplitResponse,
EditorCharacterAnimationGenerateRequest, EditorCharacterAnimationGenerateResponse,
EditorCharacterAnimationVideoConversionRequest,
EditorCharacterAnimationVideoConversionResponse, EditorVideoGenerateRequest,
EditorVideoGenerateResponse,
};
@@ -92,8 +93,9 @@ use crate::{
build_editor_canvas_generated_layer_item, editor_asset_payload_from_record,
editor_media_src_from_object_key, editor_project_payload_from_record,
editor_project_resource_payload_from_record, map_editor_project_error,
normalize_editor_record_object_key, persist_editor_generation_result_atomically,
preflight_editor_billable_generation_target, rebuild_editor_generation_input_references,
normalize_editor_record_object_key, optional_editor_idempotency_key,
persist_editor_generation_result_atomically, preflight_editor_billable_generation_target,
rebuild_editor_generation_input_references,
remove_editor_generated_screen_background_with_bgfilter,
request_editor_background_removal_image_with_bgfilter_worker,
resolve_editor_reference_object_key_for_owner, sanitize_editor_client_generation_inputs,
@@ -106,8 +108,8 @@ use crate::{
},
editor_sequence_animation::{
EDITOR_CHARACTER_ANIMATION_MODEL, EDITOR_CHARACTER_ANIMATION_REFERENCE_VIDEO_MAX_BYTES,
EditorCharacterAnimationInputMode, EditorCharacterAnimationReferenceMediaKind,
NormalizedEditorCharacterAnimationRequest,
EDITOR_CHARACTER_ANIMATION_VIDEO_CONVERSION_MAX_FRAMES, EditorCharacterAnimationInputMode,
EditorCharacterAnimationReferenceMediaKind, NormalizedEditorCharacterAnimationRequest,
apply_editor_character_animation_authoritative_reference_dimensions,
build_authoritative_editor_character_animation_generation_inputs,
build_authoritative_editor_character_animation_video_conversion_inputs,
@@ -810,11 +812,13 @@ pub async fn split_editor_character_animation_frames(
"message": "序列帧资源缺少完整的正式帧列表。",
}))
})?;
if frames.len() > 120 {
if frames.len() > EDITOR_CHARACTER_ANIMATION_VIDEO_CONVERSION_MAX_FRAMES as usize {
return Err(
AppError::from_status(StatusCode::UNPROCESSABLE_ENTITY).with_details(json!({
"provider": "editor-character-animation-frame-split",
"message": "单次最多拆分120帧。",
"message": format!(
"单次最多拆分{EDITOR_CHARACTER_ANIMATION_VIDEO_CONVERSION_MAX_FRAMES}帧。"
),
})),
);
}
@@ -1180,8 +1184,7 @@ pub(crate) async fn remove_editor_character_animation_background_for_owner(
})),
));
}
if source_resource.asset_kind.as_deref()
!= Some(EDITOR_CHARACTER_ANIMATION_RESOURCE_ASSET_KIND)
if source_resource.asset_kind.as_deref() != Some(EDITOR_CHARACTER_ANIMATION_RESOURCE_ASSET_KIND)
{
return Err(character_animation_error_response(
&request_context,
@@ -1202,10 +1205,12 @@ pub(crate) async fn remove_editor_character_animation_background_for_owner(
editor_character_animation_bad_request("序列帧资源缺少完整的正式帧列表。"),
)
})?;
if frames.len() > 120 {
if frames.len() > EDITOR_CHARACTER_ANIMATION_VIDEO_CONVERSION_MAX_FRAMES as usize {
return Err(character_animation_error_response(
&request_context,
editor_character_animation_bad_request("单次最多处理120帧。"),
editor_character_animation_bad_request(format!(
"单次最多处理{EDITOR_CHARACTER_ANIMATION_VIDEO_CONVERSION_MAX_FRAMES}帧。"
)),
));
}
let frame_width = source_resource.width;
@@ -2459,6 +2464,7 @@ pub async fn generate_editor_video(
State(state): State<AppState>,
Extension(request_context): Extension<RequestContext>,
Extension(authenticated): Extension<AuthenticatedAccessToken>,
headers: HeaderMap,
payload: Result<Json<EditorVideoGenerateRequest>, JsonRejection>,
) -> Result<Json<Value>, Response> {
let Json(payload) = payload.map_err(|error| {
@@ -2470,6 +2476,8 @@ pub async fn generate_editor_video(
})),
)
})?;
let idempotency_key = optional_editor_idempotency_key(&headers)
.map_err(|error| editor_video_error_response(&request_context, error))?;
let owner_user_id = authenticated.claims().user_id().to_string();
if !state.config.external_generation_mode.is_inline() {
let queue_job = enqueue_editor_video_generation_for_owner(
@@ -2477,7 +2485,7 @@ pub async fn generate_editor_video(
&request_context,
owner_user_id.as_str(),
payload,
None,
idempotency_key,
)
.await?;
return Ok(json_success_body(
@@ -9460,8 +9468,10 @@ mod tests {
&settings,
&normalized,
Some("users/owner/reference/action.mp4"),
&["generated-character-drafts/editor/seedance-references/image/appearance.png"
.to_string()],
&[
"generated-character-drafts/editor/seedance-references/image/appearance.png"
.to_string(),
],
)
.expect("video request with appearance reference should build");
assert_eq!(
@@ -1,6 +1,7 @@
use std::time::Duration;
use axum::extract::{Path, State};
use axum::http::StatusCode;
use axum::{Extension, Json};
use module_editor_agent::{
EDITOR_AGENT_CONVERSATION_ID_PREFIX, EDITOR_AGENT_DEFAULT_CONVERSATION_TITLE,
@@ -42,8 +43,13 @@ use crate::editor_agent::utils::{
};
use crate::editor_agent::{context, reconcile};
use crate::editor_generation_config::EditorGenerationPricingConfig;
use crate::editor_generation_queue::enqueue_editor_generation_job_with_identity;
use crate::editor_project::{current_utc_micros, map_editor_project_error};
use crate::editor_generation_queue::{
EDITOR_IMAGE_EDIT_JOB_KIND, enqueue_editor_generation_job_with_identity,
};
use crate::editor_project::{
EditorImageEditRequest, current_utc_micros, map_editor_project_error,
prepare_editor_image_edit_queue_payload_for_owner,
};
use crate::http_error::AppError;
use crate::request_context::RequestContext;
use crate::state::AppState;
@@ -712,6 +718,25 @@ mod tests {
"ERROR 美术 Agent 规划轮数已达上限:3"
);
}
#[test]
fn confirmed_image_edit_is_wrapped_before_queueing() {
let source = include_str!("api.rs");
let start = source
.rfind("pub async fn confirm_editor_agent_tool_call")
.expect("confirm endpoint should exist");
let body = &source[start..];
let prepare = body
.find("prepare_editor_image_edit_queue_payload_for_owner")
.expect("image edit should build its versioned queue payload");
let enqueue = body
.find("enqueue_editor_generation_job_with_identity")
.expect("confirmed job should be queued");
assert!(body[..prepare].contains("job_kind == EDITOR_IMAGE_EDIT_JOB_KIND"));
assert!(body[prepare..enqueue].contains("serde_json::to_value(queue_payload)"));
assert!(prepare < enqueue);
}
}
fn build_delta_messages(
outputs: Vec<PromptOutput>,
@@ -1067,7 +1092,29 @@ pub async fn confirm_editor_agent_tool_call(
let job_kind = prepared_job.job_kind;
let request_label = prepared_job.request_label;
let price_mud_points = prepared_job.price_mud_points;
let payload = prepared_job.payload;
let payload = if job_kind == EDITOR_IMAGE_EDIT_JOB_KIND {
let request = serde_json::from_value::<EditorImageEditRequest>(prepared_job.payload)
.map_err(|error| {
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(json!({
"provider": "editor-agent",
"message": format!("画布 Agent 图片编辑任务参数无效:{error}"),
}))
})?;
let queue_payload = prepare_editor_image_edit_queue_payload_for_owner(
&state,
conversation.owner_user_id.as_str(),
request,
)
.await?;
serde_json::to_value(queue_payload).map_err(|error| {
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(json!({
"provider": "editor-agent",
"message": format!("画布 Agent 图片编辑任务载荷序列化失败:{error}"),
}))
})?
} else {
prepared_job.payload
};
let (job_id, dedupe_key) = editor_agent_tool_job_identity(
conversation.conversation_id.as_str(),
message_id,
@@ -182,7 +182,7 @@ mod tests {
}
}
fn project_with_resource_asset_kind(asset_kind: &str) -> EditorProjectRecord {
fn project_with_resource_asset_kind(asset_kind: Option<&str>) -> EditorProjectRecord {
let viewport = EditorCanvasViewportRecord {
x: 0.0,
y: 0.0,
@@ -222,7 +222,7 @@ mod tests {
provider: None,
task_id: None,
source_resource_id: None,
asset_kind: Some(asset_kind.to_string()),
asset_kind: asset_kind.map(str::to_string),
generation_inputs: None,
public_showcase_enabled: false,
created_at: "2026-08-08T00:00:00Z".to_string(),
@@ -271,7 +271,7 @@ mod tests {
"icon_descriptions": ["背包"]
});
let ordinary_project = project_with_resource_asset_kind("image");
let ordinary_project = project_with_resource_asset_kind(None);
let ordinary_context =
build_tool_context_from_authoritative_records(&document, Some(&ordinary_project), None);
let ordinary_tool = editor_agent_tool(GenerateIconSpritesheetTool::NAME, &ordinary_context)
@@ -281,7 +281,7 @@ mod tests {
.expect_err("ordinary registered image must be rejected before confirmation");
assert!(error.to_string().contains("icon-spec"));
let icon_spec_project = project_with_resource_asset_kind("icon-spec");
let icon_spec_project = project_with_resource_asset_kind(Some("icon-spec"));
let icon_spec_context = build_tool_context_from_authoritative_records(
&document,
Some(&icon_spec_project),
@@ -695,27 +695,30 @@ impl EditorAgentTool for EditImageTool {
) -> Result<PreparedEditorAgentJob, EditorAgentToolError> {
let price_mud_points = self.pricing(context.pricing, args)?;
let args: EditImageToolArgs = parse_invalid_args(Self::NAME, args)?;
let source_image_src = self
let source_reference_id = self
.context
.image_data_key(&args.object_image_id)
.ok_or_else(|| EditorAgentToolError::invalid_args("object image not found"))?
.image_metadata(&args.object_image_id)
.and_then(|metadata| metadata.reference_id.as_deref())
.ok_or_else(|| {
EditorAgentToolError::invalid_args(
"object image is not a registered resource or asset",
)
})?
.to_string();
let reference_image_srcs = resolve_image_ids(&args.reference_image_ids, &self.context)?;
let title = args.prompt.clone();
let payload = EditorImageEditRequest {
prompt: args.prompt,
source_image_src,
source_reference_id,
size: None,
model: Some(args.model),
aspect_ratio: None,
image_size: None,
reference_image_srcs: Some(reference_image_srcs),
project_id: Some(context.conversation.project_id.clone()),
asset_kind: Some("editor_agent_edit_image".to_string()),
generation_inputs: context.generation_inputs("用户指令", &title),
asset_folder_id: Some("project".to_string()),
asset_label: Some(title.clone()),
source_resource_id: None,
target_layer_id: None,
canvas_completion: Some(build_editor_agent_canvas_completion(
context.project,
@@ -1647,6 +1650,82 @@ mod tests {
);
}
#[test]
fn edit_image_job_uses_registered_reference_id_only() {
let pricing = pricing();
let conversation = conversation();
let project = project();
let tool = editor_agent_tool(EditImageTool::NAME, &context_with_image("image-1"))
.expect("edit tool should resolve");
let args = tool
.validate_args(&json!({
"object_image_id": "image-1",
"prompt": "改成蓝色"
}))
.expect("edit args should validate");
let job = tool
.prepare_job(
&args,
&EditorAgentPrepareJobContext {
conversation: &conversation,
project: &project,
message_id: 7,
pricing: &pricing,
},
)
.expect("edit job should prepare");
assert_eq!(job.job_kind, EDITOR_IMAGE_EDIT_JOB_KIND);
assert_eq!(job.payload["sourceReferenceId"], "resource-image-1");
assert!(job.payload.get("sourceImageSrc").is_none());
assert!(job.payload.get("sourceResourceId").is_none());
assert!(job.payload.get("assetKind").is_none());
assert_eq!(job.payload["projectId"], "project-1");
assert!(job.payload["targetLayerId"].is_null());
}
#[test]
fn edit_image_job_rejects_image_without_registered_reference_id() {
let pricing = pricing();
let conversation = conversation();
let project = project();
let mut context = context_with_image("image-1");
context
.images
.values_mut()
.next()
.expect("test image should exist")
.reference_id = None;
let tool =
editor_agent_tool(EditImageTool::NAME, &context).expect("edit tool should resolve");
let args = tool
.validate_args(&json!({
"object_image_id": "image-1",
"prompt": "改成蓝色"
}))
.expect("edit args should validate");
let error = match tool.prepare_job(
&args,
&EditorAgentPrepareJobContext {
conversation: &conversation,
project: &project,
message_id: 7,
pricing: &pricing,
},
) {
Ok(_) => panic!("unregistered image should fail closed"),
Err(error) => error,
};
assert!(error.is_invalid_args());
assert_eq!(
error.to_string(),
"object image is not a registered resource or asset"
);
}
#[test]
fn dyn_job_preparation_keeps_queue_contract_and_defaults() {
let pricing = pricing();
File diff suppressed because it is too large Load Diff
@@ -17,7 +17,7 @@ pub(crate) const EDITOR_CHARACTER_ANIMATION_MODEL: &str = "seedance2.0-fast";
pub(crate) const EDITOR_CHARACTER_ANIMATION_REFERENCE_VIDEO_MAX_BYTES: u64 = 50 * 1024 * 1024;
const EDITOR_CHARACTER_ANIMATION_REFERENCE_VIDEO_MIN_DURATION_SECONDS: f64 = 2.0;
const EDITOR_CHARACTER_ANIMATION_REFERENCE_VIDEO_MAX_DURATION_SECONDS: f64 = 15.0;
pub(crate) const EDITOR_CHARACTER_ANIMATION_VIDEO_CONVERSION_MAX_FRAMES: u32 = 120;
pub(crate) const EDITOR_CHARACTER_ANIMATION_VIDEO_CONVERSION_MAX_FRAMES: u32 = 66;
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct EditorCharacterAnimationVideoConversionPlan {
@@ -70,11 +70,12 @@ pub(crate) fn build_editor_character_animation_video_conversion_plan(
let scale = (f64::from(max_long_edge) / f64::from(source_long_edge)).min(1.0);
let frame_width = (f64::from(source_width) * scale).round().max(1.0) as u32;
let frame_height = (f64::from(source_height) * scale).round().max(1.0) as u32;
let sampling_fps = source_fps.min(8.0).max(1.0);
let frame_count = (duration_seconds * sampling_fps).round().clamp(
let requested_sampling_fps = source_fps.min(8.0).max(1.0);
let frame_count = (duration_seconds * requested_sampling_fps).round().clamp(
2.0,
f64::from(EDITOR_CHARACTER_ANIMATION_VIDEO_CONVERSION_MAX_FRAMES),
) as u32;
let sampling_fps = f64::from(frame_count) / duration_seconds;
Ok(EditorCharacterAnimationVideoConversionPlan {
duration_seconds,
duration_ms: (duration_seconds * 1_000.0).round().max(1.0) as u64,
@@ -547,8 +548,7 @@ pub(crate) fn build_authoritative_editor_character_animation_generation_inputs(
));
}
for appearance_object_key in appearance_object_keys {
let appearance_object_key =
appearance_object_key.trim().trim_start_matches('/');
let appearance_object_key = appearance_object_key.trim().trim_start_matches('/');
if appearance_object_key.is_empty() {
continue;
}
@@ -914,13 +914,13 @@ mod tests {
}
#[test]
fn video_conversion_plan_downscales_and_caps_at_120_frames() {
fn video_conversion_plan_downscales_and_caps_at_66_frames() {
let plan =
build_editor_character_animation_video_conversion_plan(15.0, 60.0, 1920, 1080, "480p")
.unwrap();
assert_eq!((plan.frame_width, plan.frame_height), (480, 270));
assert_eq!(plan.frame_count, 120);
assert_eq!(plan.sampling_fps, 8.0);
assert_eq!(plan.frame_count, 66);
assert!((plan.sampling_fps - 4.4).abs() < 0.001);
}
#[test]
@@ -811,8 +811,9 @@ pub async fn edit_external_editor_image(
Extension(request_context): Extension<RequestContext>,
Extension(principal): Extension<ExternalApiPrincipal>,
headers: HeaderMap,
Json(payload): Json<EditorImageEditRequest>,
payload: Result<Json<EditorImageEditRequest>, JsonRejection>,
) -> Result<Response, AppError> {
let Json(payload) = parse_editor_generation_json_payload(payload)?;
require_scope(&principal, SCOPE_EDITOR_IMAGE_GENERATE)?;
let idempotency_key = require_idempotency_key(&headers)?;
let project_id = payload.project_id.clone();
@@ -2162,7 +2163,8 @@ mod tests {
assert!(
parsed["components"]["schemas"]["EditorImageEditRequest"]["required"]
.as_array()
.is_some_and(|required| !required.contains(&json!("priceMudPoints")))
.is_some_and(|required| required.contains(&json!("sourceReferenceId"))
&& !required.contains(&json!("priceMudPoints")))
);
assert!(
parsed["components"]["schemas"]["EditorImageEditRequest"]["properties"]
@@ -2174,6 +2176,59 @@ mod tests {
.get("targetLayerId")
.is_some()
);
let image_edit_schema =
&parsed["components"]["schemas"]["EditorImageEditRequest"]["properties"];
for legacy_field in ["sourceImageSrc", "sourceResourceId", "assetKind"] {
assert!(
image_edit_schema.get(legacy_field).is_none(),
"{legacy_field}"
);
}
assert!(
image_edit_schema["sourceReferenceId"]["description"]
.as_str()
.is_some_and(|description| description.contains("项目资源 ID 或素材 ID")
&& description.contains("objectKey")
&& description.contains("未登记上传对象")
&& description.contains("400"))
);
let image_edit_references = &image_edit_schema["referenceImageSrcs"];
assert!(
image_edit_references["items"]["description"]
.as_str()
.is_some_and(|description| description.contains("sourceReferenceId")
&& !description.contains("sourceImageSrc"))
);
assert_eq!(
parsed["components"]["schemas"]["EditorImageEditRequest"]["additionalProperties"],
json!(false)
);
let image_edit_operation = &parsed["paths"]["/api/external/v1/editor/images/edits"]["post"];
assert_eq!(
image_edit_operation["x-genarrative-allowed-effective-asset-kinds"],
json!([
null,
"spec",
"character",
"icon-spritesheet",
"icon-spec",
"publication-material",
"ui-design",
"scene"
])
);
assert!(
image_edit_operation["description"]
.as_str()
.is_some_and(|description| description.contains("targetLayerId")
&& description.contains("sourceReferenceId")
&& description.contains("assetObjectId")
&& description.contains("bucket/objectKey")
&& description.contains("scene")
&& description.contains("未知类型")
&& description.contains("返回 400"))
);
assert!(image_edit_operation["responses"].get("400").is_some());
for (schema, max_items) in [
("EditorImageEditRequest", 8),
("EditorIconSpritesheetGenerationRequest", 8),
@@ -138,20 +138,29 @@ pub async fn get_external_generation_job_status(
Path(job_id): Path<String>,
) -> Result<Json<serde_json::Value>, Response> {
let owner_user_id = authenticated.claims().user_id().to_string();
let input = ExternalGenerationJobGetRecordInput {
job_id,
owner_user_id,
};
let job = state
.spacetime_client()
.get_external_generation_job_summary(ExternalGenerationJobGetRecordInput {
job_id,
owner_user_id,
})
.get_external_generation_job_summary(input.clone())
.await
.map_err(|error| external_generation_error_response(&request_context, error))?;
let mut detail = map_external_generation_job_status_detail(job);
if detail.status.status == ExternalGenerationJobStatus::Completed {
let artifacts = state
.spacetime_client()
.get_external_generation_job_generated_artifacts(input)
.await
.map_err(|error| external_generation_error_response(&request_context, error))?;
detail.result =
external_generation_completed_result(artifacts.result_payload_json.as_deref());
}
Ok(json_success_body(
Some(&request_context),
ExternalGenerationJobStatusResponse {
job: map_external_generation_job_status_detail(job),
},
ExternalGenerationJobStatusResponse { job: detail },
))
}
@@ -235,9 +244,18 @@ pub(crate) fn map_external_generation_job_status_detail(
ExternalGenerationJobStatusDetailRecord {
status: map_external_generation_job_status(job),
warning,
result: None,
}
}
fn external_generation_completed_result(
result_payload_json: Option<&str>,
) -> Option<serde_json::Value> {
result_payload_json
.and_then(|payload| serde_json::from_str::<serde_json::Value>(payload).ok())
.and_then(|payload| payload.get("result").cloned())
}
fn map_external_generation_task_record(
job: ExternalGenerationJobSummaryRecord,
) -> ExternalGenerationTaskRecord {
@@ -328,6 +346,18 @@ mod tests {
);
}
#[test]
fn completed_generation_result_uses_compact_result_payload() {
let result = external_generation_completed_result(Some(
r#"{"result":{"resourceId":"resource-1","objectKey":"generated/1.png"}}"#,
))
.expect("completed result should be readable");
assert_eq!(result["resourceId"], "resource-1");
assert!(external_generation_completed_result(Some(r#"{"data":{}}"#)).is_none());
assert!(external_generation_completed_result(Some("not-json")).is_none());
}
#[test]
fn maps_task_from_payload_free_summary_projection() {
let summary = ExternalGenerationJobSummaryRecord {
@@ -44,12 +44,14 @@ use crate::{
EDITOR_UI_DESIGN_ASSET_EXTRACTION_JOB_KIND, EDITOR_VIDEO_GENERATION_JOB_KIND,
},
editor_project::{
EDITOR_GENERATION_MULTIPLE_WARNINGS_CODE, EditorBackgroundRemovalRequest,
EditorGenerationCaller, EditorGenerationOperationContext, EditorGenerationPhaseReporter,
EditorGenerationQueueResultContext, EditorImageEditRequest, EditorImageGenerationRequest,
EditorUiDesignAssetExtractionRequest, compact_editor_generation_result,
edit_editor_image_for_owner, extract_editor_ui_design_assets_for_owner,
generate_editor_image_for_owner, remove_editor_image_background_for_owner,
EDITOR_GENERATION_MULTIPLE_WARNINGS_CODE, EDITOR_IMAGE_EDIT_QUEUE_PAYLOAD_VERSION,
EditorBackgroundRemovalRequest, EditorGenerationCaller, EditorGenerationOperationContext,
EditorGenerationPhaseReporter, EditorGenerationQueueResultContext,
EditorImageEditQueuePayload, EditorImageEditRequest, EditorImageEditResolvedSource,
EditorImageGenerationRequest, EditorUiDesignAssetExtractionRequest,
compact_editor_generation_result, edit_editor_image_for_owner_with_source_snapshot,
extract_editor_ui_design_assets_for_owner, generate_editor_image_for_owner,
remove_editor_image_background_for_owner,
},
editor_project_icon::{
EditorIconSpecGenerationRequest, EditorIconSpritesheetGenerationRequest,
@@ -793,22 +795,20 @@ async fn process_external_generation_job_once(
}
}
EDITOR_IMAGE_EDIT_JOB_KIND => {
let payload = match serde_json::from_str::<EditorImageEditRequest>(
job.request_payload_json.as_str(),
) {
let payload = match parse_editor_image_edit_worker_payload(&job) {
Ok(payload) => payload,
Err(error) => {
let message = format!("图片画布改图任务参数解析失败:{error}");
Err(message) => {
fail_job(&state, &worker_id, &job, message.clone()).await?;
return Err(message);
}
};
let request_context = worker_request_context(&job, provider_deadline);
match edit_editor_image_for_owner(
match edit_editor_image_for_owner_with_source_snapshot(
&state,
&request_context,
editor_generation_worker_caller(&worker_id, &job)?,
payload,
payload.request,
payload.source.as_ref(),
)
.await
{
@@ -1197,6 +1197,80 @@ fn worker_request_context(
.with_external_call_deadline(provider_deadline)
}
struct ParsedEditorImageEditWorkerPayload {
request: EditorImageEditRequest,
source: Option<EditorImageEditResolvedSource>,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct LegacyEditorImageEditWorkerPayload {
prompt: String,
source_image_src: String,
size: Option<String>,
model: Option<String>,
aspect_ratio: Option<String>,
image_size: Option<String>,
reference_image_srcs: Option<Vec<String>>,
project_id: Option<String>,
generation_inputs: Option<Value>,
asset_folder_id: Option<String>,
asset_label: Option<String>,
source_resource_id: Option<String>,
target_layer_id: Option<String>,
canvas_completion: Option<shared_contracts::assets::EditorCanvasGenerationCompletionPayload>,
}
fn parse_editor_image_edit_worker_payload(
job: &ExternalGenerationJobRecord,
) -> Result<ParsedEditorImageEditWorkerPayload, String> {
if let Ok(payload) =
serde_json::from_str::<EditorImageEditQueuePayload>(job.request_payload_json.as_str())
{
if payload.version != EDITOR_IMAGE_EDIT_QUEUE_PAYLOAD_VERSION {
return Err(format!(
"图片画布改图任务载荷版本不受支持:{}",
payload.version
));
}
return Ok(ParsedEditorImageEditWorkerPayload {
request: payload.request,
source: Some(payload.source),
});
}
let legacy: LegacyEditorImageEditWorkerPayload =
serde_json::from_str(job.request_payload_json.as_str())
.map_err(|error| format!("图片画布改图任务参数解析失败:{error}"))?;
let source_reference_id = legacy
.source_resource_id
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.unwrap_or_else(|| legacy.source_image_src.trim())
.to_string();
if source_reference_id.is_empty() {
return Err("历史图片画布改图任务缺少可迁移的业务引用 ID".to_string());
}
Ok(ParsedEditorImageEditWorkerPayload {
request: EditorImageEditRequest {
prompt: legacy.prompt,
source_reference_id,
size: legacy.size,
model: legacy.model,
aspect_ratio: legacy.aspect_ratio,
image_size: legacy.image_size,
reference_image_srcs: legacy.reference_image_srcs,
project_id: legacy.project_id,
generation_inputs: legacy.generation_inputs,
asset_folder_id: legacy.asset_folder_id,
asset_label: legacy.asset_label,
target_layer_id: legacy.target_layer_id,
canvas_completion: legacy.canvas_completion,
},
source: None,
})
}
fn editor_generation_worker_caller(
worker_id: &str,
job: &ExternalGenerationJobRecord,
@@ -1289,15 +1363,7 @@ fn is_external_api_generation_job(job: &ExternalGenerationJobRecord) -> bool {
}
fn is_editor_agent_generation_job(job: &ExternalGenerationJobRecord) -> bool {
serde_json::from_str::<Value>(job.request_payload_json.as_str())
.ok()
.is_some_and(|payload| {
payload
.pointer("/generationInputs/source")
.and_then(Value::as_str)
// TODO extract const
.is_some_and(|source| source.trim() == "editor-agent")
})
job.dedupe_key.trim().starts_with("editor-agent:")
}
fn compact_external_api_generation_result(result: Value) -> Value {
@@ -1745,6 +1811,78 @@ mod tests {
assert!(caller.phase_reporter.is_some());
}
#[test]
fn editor_image_edit_worker_parses_versioned_snapshot_payload() {
let mut job = external_generation_job_record_fixture(Some("lease-1"));
job.job_kind = EDITOR_IMAGE_EDIT_JOB_KIND.to_string();
job.request_payload_json = json!({
"version": 1,
"request": {
"prompt": "改成蓝色",
"sourceReferenceId": "resource-source"
},
"source": {
"referenceKind": "project-resource",
"sourceReferenceId": "resource-source",
"resourceId": "resource-source",
"assetId": null,
"assetObjectId": "object-source",
"bucket": "bucket",
"objectKey": "generated/source.png",
"sourceAssetKind": "character",
"effectiveAssetKind": "character",
"mediaType": "image",
"targetResourceId": null
}
})
.to_string();
let payload = parse_editor_image_edit_worker_payload(&job)
.expect("versioned image edit payload should parse");
assert_eq!(payload.request.source_reference_id, "resource-source");
assert_eq!(
payload.source.expect("snapshot should exist").object_key,
"generated/source.png"
);
}
#[test]
fn legacy_editor_image_edit_payload_migrates_only_business_ids() {
let mut job = external_generation_job_record_fixture(Some("lease-1"));
job.job_kind = EDITOR_IMAGE_EDIT_JOB_KIND.to_string();
job.request_payload_json = json!({
"prompt": "改成蓝色",
"sourceImageSrc": "raw/generated/source.png",
"sourceResourceId": "resource-source",
"projectId": "project-1",
"assetKind": "editor_agent_edit_image",
"generationInputs": { "source": "editor-agent" }
})
.to_string();
let legacy_agent_payload = parse_editor_image_edit_worker_payload(&job)
.expect("历史 Agent 图片编辑 payload 应由 worker 解析");
assert_eq!(
legacy_agent_payload.request.source_reference_id,
"resource-source"
);
assert!(legacy_agent_payload.source.is_none());
job.request_payload_json = json!({
"prompt": "改成蓝色",
"sourceImageSrc": "raw/generated/source.png",
"assetKind": "character"
})
.to_string();
let raw_only_payload = parse_editor_image_edit_worker_payload(&job)
.expect("raw-only legacy payload should migrate without object-key lookup");
assert_eq!(
raw_only_payload.request.source_reference_id,
"raw/generated/source.png"
);
assert!(raw_only_payload.source.is_none());
}
#[cfg(any())]
#[test]
fn worker_write_guard_requires_claimed_job_lease_token() {
@@ -1966,6 +2104,7 @@ mod tests {
#[test]
fn editor_agent_result_payload_keeps_compact_response() {
let mut job = external_generation_job_record_fixture(Some("lease-1"));
job.dedupe_key = "editor-agent:conversation-1:7:generate-image".to_string();
job.request_payload_json = json!({
"generationInputs": { "source": "editor-agent" },
})
@@ -2095,6 +2234,7 @@ mod tests {
),
] {
let mut job = external_generation_job_record_fixture(Some("lease-1"));
job.dedupe_key = format!("editor-agent:conversation-1:7:{tool_name}");
job.request_payload_json = json!({
"generationInputs": { "source": "editor-agent" },
})
@@ -2118,6 +2258,7 @@ mod tests {
let payload: Value =
serde_json::from_str(&editor_generation_result_payload_json(&job, &response))
.expect("worker compact payload should serialize");
assert!(payload.get("editor-agent-tool-call-result").is_some());
assert!(
payload["editor-agent-tool-call-result"]
.get("provider")
@@ -2159,6 +2300,7 @@ mod tests {
#[test]
fn editor_agent_spritesheet_result_keeps_all_persisted_slices() {
let mut job = external_generation_job_record_fixture(Some("lease-1"));
job.dedupe_key = "editor-agent:conversation-1:7:generate-icon-spritesheet".to_string();
job.request_payload_json = json!({
"generationInputs": { "source": "editor-agent" },
})
@@ -1,8 +1,11 @@
use axum::{
Router,
extract::DefaultBodyLimit,
extract::{DefaultBodyLimit, Request},
handler::Handler,
http::{HeaderValue, header},
middleware,
middleware::Next,
response::Response,
routing::{get, patch, post},
};
@@ -35,6 +38,18 @@ use crate::{
const EDITOR_AGENT_MESSAGE_BODY_LIMIT_BYTES: usize = 2 * 1024 * 1024;
async fn add_showcase_private_response_headers(request: Request, next: Next) -> Response {
let mut response = next.run(request).await;
response.headers_mut().insert(
header::CACHE_CONTROL,
HeaderValue::from_static("private, no-store"),
);
response
.headers_mut()
.append(header::VARY, HeaderValue::from_static("Authorization"));
response
}
pub fn router(state: AppState) -> Router<AppState> {
Router::new()
.route(
@@ -43,7 +58,8 @@ pub fn router(state: AppState) -> Router<AppState> {
)
.route(
"/api/editor/showcase/resources",
get(list_public_editor_project_resources),
get(list_public_editor_project_resources)
.route_layer(middleware::from_fn(add_showcase_private_response_headers)),
)
.route(
"/api/editor/projects/recent",
@@ -183,10 +199,12 @@ pub fn router(state: AppState) -> Router<AppState> {
)
.route(
"/api/editor/showcase/assets/{showcase_id}/likes",
post(toggle_editor_showcase_asset_like).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
post(toggle_editor_showcase_asset_like)
.route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
))
.route_layer(middleware::from_fn(add_showcase_private_response_headers)),
)
.route(
"/api/editor/images/generations",
@@ -266,3 +284,53 @@ pub fn router(state: AppState) -> Router<AppState> {
)),
)
}
#[cfg(test)]
mod tests {
use axum::{
Router,
body::Body,
http::{Request, header},
middleware,
response::{IntoResponse, Response},
routing::get,
};
use tower::ServiceExt;
use super::add_showcase_private_response_headers;
async fn response_with_existing_vary() -> Response {
let mut response = "ok".into_response();
response
.headers_mut()
.insert(header::VARY, "Origin".parse().expect("valid Vary value"));
response
}
#[tokio::test]
async fn showcase_private_headers_preserve_existing_vary_fields() {
let app = Router::new().route(
"/",
get(response_with_existing_vary)
.route_layer(middleware::from_fn(add_showcase_private_response_headers)),
);
let response = app
.oneshot(
Request::builder()
.uri("/")
.body(Body::empty())
.expect("request should build"),
)
.await
.expect("request should succeed");
let vary_values = response
.headers()
.get_all(header::VARY)
.iter()
.map(|value| value.to_str().expect("Vary should be ASCII"))
.collect::<Vec<_>>();
assert_eq!(vary_values, vec!["Origin", "Authorization"]);
}
}
@@ -1,7 +1,7 @@
use std::future::Future;
use axum::Extension;
use axum::http::StatusCode;
use axum::http::{HeaderMap, StatusCode};
use axum::{
Json,
extract::{State, rejection::JsonRejection},
@@ -34,9 +34,9 @@ use crate::{
EditorGenerationOperationContext, PersistEditorGeneratedAssetInput,
build_editor_canvas_generated_layer_item, editor_asset_payload_from_record,
editor_project_payload_from_record, editor_project_resource_payload_from_record,
normalize_optional_string, persist_editor_generation_result_atomically,
preflight_editor_billable_generation_target, prepare_editor_generated_asset,
sanitize_editor_client_generation_inputs,
normalize_optional_string, optional_editor_idempotency_key,
persist_editor_generation_result_atomically, preflight_editor_billable_generation_target,
prepare_editor_generated_asset, sanitize_editor_client_generation_inputs,
},
http_error::AppError,
request_context::RequestContext,
@@ -224,9 +224,12 @@ pub async fn generate_editor_sound_effect(
State(state): State<AppState>,
Extension(request_context): Extension<RequestContext>,
Extension(authenticated): Extension<AuthenticatedAccessToken>,
headers: HeaderMap,
payload: Result<Json<assets::EditorSoundEffectGenerateRequest>, JsonRejection>,
) -> Result<Json<Value>, Response> {
let Json(payload) = parse_json_payload(&request_context, payload)?;
let idempotency_key = optional_editor_idempotency_key(&headers)
.map_err(|error| error.into_response_with_context(Some(&request_context)))?;
let owner_user_id = authenticated.claims().user_id().to_string();
if !state.config.external_generation_mode.is_inline() {
let queue_job = enqueue_editor_sound_effect_generation_for_owner(
@@ -234,7 +237,7 @@ pub async fn generate_editor_sound_effect(
&request_context,
owner_user_id.as_str(),
payload,
None,
idempotency_key,
)
.await?;
return Ok(json_success_body(
@@ -823,9 +826,12 @@ pub async fn generate_editor_background_music(
State(state): State<AppState>,
Extension(request_context): Extension<RequestContext>,
Extension(authenticated): Extension<AuthenticatedAccessToken>,
headers: HeaderMap,
payload: Result<Json<assets::EditorBackgroundMusicGenerateRequest>, JsonRejection>,
) -> Result<Json<Value>, Response> {
let Json(payload) = parse_json_payload(&request_context, payload)?;
let idempotency_key = optional_editor_idempotency_key(&headers)
.map_err(|error| error.into_response_with_context(Some(&request_context)))?;
let payload = CanonicalEditorBackgroundMusicSubmissionPayload::new(payload)
.map_err(|error| error.into_response_with_context(Some(&request_context)))?;
let owner_user_id = authenticated.claims().user_id().to_string();
@@ -835,6 +841,7 @@ pub async fn generate_editor_background_music(
&request_context,
owner_user_id.as_str(),
payload,
idempotency_key,
)
.await?;
return Ok(json_success_body(
@@ -915,6 +922,7 @@ async fn enqueue_logged_in_editor_background_music_generation_for_owner(
request_context: &RequestContext,
owner_user_id: &str,
payload: CanonicalEditorBackgroundMusicSubmissionPayload,
external_idempotency_key: Option<&str>,
) -> Result<ExternalGenerationJobRecord, Response> {
let pricing = load_editor_background_music_queue_pricing(state, request_context).await?;
let prepared = payload
@@ -925,7 +933,7 @@ async fn enqueue_logged_in_editor_background_music_generation_for_owner(
request_context,
owner_user_id,
prepared,
None,
external_idempotency_key,
)
.await
}
@@ -1558,8 +1558,8 @@ mod tests {
}
#[test]
fn editor_character_animation_120_frame_response_stays_within_queue_json_gate() {
let frames = (0..120)
fn editor_character_animation_66_frame_response_stays_within_queue_json_gate() {
let frames = (0..66)
.map(|index| EditorCharacterAnimationFramePayload {
image_src: format!(
"/generated-character-animations/editor/video-conversions/task/frame{:03}.png",
@@ -1579,12 +1579,12 @@ mod tests {
task_id: "task-max-frames".to_string(),
processing_method: "视频直接转换".to_string(),
frames,
frame_count: 120,
frame_count: 66,
duration_ms: 15_000,
frame_width: 720,
frame_height: 405,
source_fps: 29.970_029_97,
sampling_fps: 8.0,
sampling_fps: 4.4,
price_mud_points: 0,
project: None,
resource: None,
@@ -1594,7 +1594,7 @@ mod tests {
let bytes = serde_json::to_vec(&payload).expect("response should serialize");
assert!(
bytes.len() < 512 * 1024,
"120 frame payload must fit queue gate"
"66 frame payload must fit queue gate"
);
}
@@ -44,6 +44,8 @@ pub struct ExternalGenerationJobStatusDetailRecord {
pub status: ExternalGenerationJobStatusRecord,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub warning: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub result: Option<Value>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
@@ -148,11 +150,13 @@ mod tests {
updated_at_micros: 1,
},
warning: Some("连通域数量不足".to_string()),
result: Some(json!({ "resourceId": "resource-1" })),
},
})
.expect("状态响应应可序列化");
assert_eq!(payload["job"]["warning"], json!("连通域数量不足"));
assert_eq!(payload["job"]["result"]["resourceId"], json!("resource-1"));
assert_eq!(payload["job"]["error"], json!(null));
assert_eq!(payload["job"]["status"], json!("completed"));
}
@@ -640,6 +640,8 @@ pub struct GameIterationVersion {
pub resource_bindings: Vec<GameIterationVersionResourceBinding>,
pub created_reason: GameIterationVersionCreatedReason,
pub created_at: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub edit_prompt: Option<String>,
}
fn validate_iteration_version_id(value: &str, label: &str, max_chars: usize) -> Result<(), String> {
@@ -677,6 +679,19 @@ pub fn validate_game_iteration_versions(versions: &[GameIterationVersion]) -> Re
version.version_id
));
}
if let Some(edit_prompt) = version.edit_prompt.as_deref() {
if edit_prompt.trim().is_empty()
|| edit_prompt.chars().count() > 32_000
|| edit_prompt.chars().any(|character| {
character.is_control() && !matches!(character, '\n' | '\r' | '\t')
})
{
return Err(format!(
"项目版本 {} 的 editPrompt 必须在 1..=32000 字符内且不能包含非法控制字符",
version.version_id
));
}
}
if index == 0 {
if version.parent_version_id.is_some()
@@ -1749,6 +1764,7 @@ mod tests {
}],
created_reason: GameIterationVersionCreatedReason::Initial,
created_at: 456,
edit_prompt: None,
});
manifest.assets.push(GameCreationAppAssetManifestEntry {
id: "asset-player".to_string(),
@@ -1806,6 +1822,7 @@ mod tests {
}],
created_reason: GameIterationVersionCreatedReason::Initial,
created_at: 100,
edit_prompt: None,
};
let child = GameIterationVersion {
version_id: "version-child".to_string(),
@@ -1814,6 +1831,7 @@ mod tests {
resource_bindings: Vec::new(),
created_reason: GameIterationVersionCreatedReason::AgentRevision,
created_at: 101,
edit_prompt: Some("继续优化这一版".to_string()),
};
validate_game_iteration_versions(&[root.clone(), child.clone()])
@@ -62,7 +62,8 @@ pub use self::editor_project::{
EditorShowcaseAssetAdminReviewRecordInput, EditorShowcaseAssetDisplayUpdateRecordInput,
EditorShowcaseAssetLikeToggleRecordInput, EditorShowcaseAssetPublicListRecordInput,
EditorShowcaseAssetRecord, EditorShowcaseAssetRefundMarkRecordInput,
EditorShowcaseAssetSubmitRecordInput, EditorShowcaseCampaignConfigGetRecordInput,
EditorShowcaseAssetSubmitRecordInput, EditorShowcaseAssetViewerListRecordInput,
EditorShowcaseAssetViewerRecord, EditorShowcaseCampaignConfigGetRecordInput,
EditorShowcaseCampaignConfigRecord, EditorShowcaseCampaignConfigUpsertRecordInput,
};
pub use self::external_api_key::{
@@ -111,6 +112,8 @@ pub(crate) use self::editor_project::{
map_editor_project_resource_list_procedure_result,
map_editor_project_resource_procedure_result, map_editor_reference_procedure_result,
map_editor_showcase_asset_list_procedure_result, map_editor_showcase_asset_procedure_result,
map_editor_showcase_asset_viewer_list_procedure_result,
map_editor_showcase_asset_viewer_procedure_result,
map_editor_showcase_campaign_config_procedure_result,
map_optional_editor_asset_procedure_result,
};
@@ -474,6 +474,13 @@ pub struct EditorShowcaseAssetPublicListRecordInput {
pub cursor: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EditorShowcaseAssetViewerListRecordInput {
pub limit: u32,
pub cursor: Option<String>,
pub viewer_user_id: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EditorShowcaseAssetAdminListRecordInput {
pub limit: u32,
@@ -517,6 +524,12 @@ pub struct EditorShowcaseAssetLikeToggleRecordInput {
pub updated_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct EditorShowcaseAssetViewerRecord {
pub asset: EditorShowcaseAssetRecord,
pub viewer_liked: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EditorShowcaseCampaignConfigGetRecordInput {
pub include_disabled: bool,
@@ -862,6 +875,18 @@ impl From<EditorShowcaseAssetPublicListRecordInput>
}
}
impl From<EditorShowcaseAssetViewerListRecordInput>
for crate::module_bindings::EditorShowcaseAssetViewerListInput
{
fn from(input: EditorShowcaseAssetViewerListRecordInput) -> Self {
Self {
limit: input.limit,
cursor: input.cursor,
viewer_user_id: input.viewer_user_id,
}
}
}
impl From<EditorShowcaseAssetAdminListRecordInput>
for crate::module_bindings::EditorShowcaseAssetAdminListInput
{
@@ -1226,6 +1251,34 @@ pub(crate) fn map_editor_showcase_asset_list_procedure_result(
.collect()
}
pub(crate) fn map_editor_showcase_asset_viewer_procedure_result(
result: EditorShowcaseAssetViewerProcedureResult,
) -> Result<EditorShowcaseAssetViewerRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
result
.item
.map(map_editor_showcase_asset_viewer_snapshot)
.transpose()?
.ok_or_else(|| SpacetimeClientError::missing_snapshot("陶泥儿精选浏览者快照"))
}
pub(crate) fn map_editor_showcase_asset_viewer_list_procedure_result(
result: EditorShowcaseAssetViewerListProcedureResult,
) -> Result<Vec<EditorShowcaseAssetViewerRecord>, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
result
.items
.into_iter()
.map(map_editor_showcase_asset_viewer_snapshot)
.collect()
}
pub(crate) fn map_editor_showcase_campaign_config_procedure_result(
result: EditorShowcaseCampaignConfigProcedureResult,
) -> Result<Option<EditorShowcaseCampaignConfigRecord>, SpacetimeClientError> {
@@ -1506,6 +1559,15 @@ fn map_editor_showcase_asset_snapshot(
})
}
fn map_editor_showcase_asset_viewer_snapshot(
snapshot: EditorShowcaseAssetViewerSnapshot,
) -> Result<EditorShowcaseAssetViewerRecord, SpacetimeClientError> {
Ok(EditorShowcaseAssetViewerRecord {
asset: map_editor_showcase_asset_snapshot(snapshot.asset)?,
viewer_liked: snapshot.viewer_liked,
})
}
fn map_editor_showcase_campaign_config_snapshot(
snapshot: EditorShowcaseCampaignConfigSnapshot,
) -> EditorShowcaseCampaignConfigRecord {
@@ -257,6 +257,27 @@ impl From<EditorGenerationResultPersistRecordInput>
}
impl SpacetimeClient {
pub async fn resolve_editor_reference(
&self,
input: EditorReferenceResolveRecordInput,
) -> Result<EditorReferenceRecord, SpacetimeClientError> {
let procedure_input = input.into();
self.call_after_connect(
"resolve_editor_reference_and_return",
move |connection, sender| {
connection
.procedures()
.resolve_editor_reference_and_return_then(procedure_input, move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_reference_procedure_result);
send_once(&sender, mapped);
});
},
)
.await
}
pub async fn preflight_editor_generation_target(
&self,
input: EditorGenerationTargetPreflightRecordInput,
@@ -281,27 +302,6 @@ impl SpacetimeClient {
.await
}
pub async fn resolve_editor_reference(
&self,
input: EditorReferenceResolveRecordInput,
) -> Result<EditorReferenceRecord, SpacetimeClientError> {
let procedure_input = input.into();
self.call_after_connect(
"resolve_editor_reference_and_return",
move |connection, sender| {
connection
.procedures()
.resolve_editor_reference_and_return_then(procedure_input, move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_reference_procedure_result);
send_once(&sender, mapped);
});
},
)
.await
}
pub async fn preflight_editor_pixel_art_result(
&self,
input: EditorPixelArtResultPreflightRecordInput,
@@ -1063,6 +1063,31 @@ impl SpacetimeClient {
.await
}
pub async fn list_public_editor_showcase_assets_for_viewer(
&self,
input: EditorShowcaseAssetViewerListRecordInput,
) -> Result<Vec<EditorShowcaseAssetViewerRecord>, SpacetimeClientError> {
let procedure_input = input.into();
self.call_after_connect(
"list_public_editor_showcase_assets_for_viewer_and_return",
move |connection, sender| {
connection
.procedures()
.list_public_editor_showcase_assets_for_viewer_and_return_then(
procedure_input,
move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_showcase_asset_viewer_list_procedure_result);
send_once(&sender, mapped);
},
);
},
)
.await
}
pub async fn admin_list_editor_showcase_assets(
&self,
input: EditorShowcaseAssetAdminListRecordInput,
@@ -1163,23 +1188,23 @@ impl SpacetimeClient {
.await
}
pub async fn toggle_editor_showcase_asset_like(
pub async fn set_editor_showcase_asset_like_for_viewer(
&self,
input: EditorShowcaseAssetLikeToggleRecordInput,
) -> Result<EditorShowcaseAssetRecord, SpacetimeClientError> {
) -> Result<EditorShowcaseAssetViewerRecord, SpacetimeClientError> {
let procedure_input = input.into();
self.call_after_connect(
"toggle_editor_showcase_asset_like_and_return",
"set_editor_showcase_asset_like_for_viewer_and_return",
move |connection, sender| {
connection
.procedures()
.toggle_editor_showcase_asset_like_and_return_then(
.set_editor_showcase_asset_like_for_viewer_and_return_then(
procedure_input,
move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_showcase_asset_procedure_result);
.and_then(map_editor_showcase_asset_viewer_procedure_result);
send_once(&sender, mapped);
},
);
@@ -1696,7 +1721,7 @@ mod pixel_art_persist_mapper_tests {
owner_user_id: Some("user-1".to_string()),
profile_id: None,
entity_id: Some("project-1".to_string()),
asset_kind: "image".to_string(),
asset_kind: "editor_pixel_art_snap".to_string(),
created_at_micros: 10,
updated_at_micros: 11,
}
@@ -1720,7 +1745,7 @@ mod pixel_art_persist_mapper_tests {
provider: None,
task_id: Some("pixel-art-snap-dialog-1".to_string()),
source_resource_id: Some("source-resource".to_string()),
asset_kind: Some("image".to_string()),
asset_kind: None,
generation_inputs_json: Some(r#"{"source":"canvas"}"#.to_string()),
public_showcase_enabled: false,
created_at_micros: 10,
@@ -1746,7 +1771,7 @@ mod pixel_art_persist_mapper_tests {
model: None,
provider: None,
task_id: Some("pixel-art-snap-dialog-1".to_string()),
asset_kind: Some("image".to_string()),
asset_kind: None,
generation_inputs_json: Some(r#"{"source":"canvas"}"#.to_string()),
source_resource_id: Some("source-resource".to_string()),
public_showcase_enabled: Some(false),
@@ -56,6 +56,7 @@ pub use mapper::{
EditorShowcaseAssetLikeToggleRecordInput, EditorShowcaseAssetPublicListRecordInput,
EditorShowcaseAssetRecord, EditorShowcaseAssetRefundMarkRecordInput,
EditorShowcaseAssetSubmitRecordInput, EditorShowcaseCampaignConfigGetRecordInput,
EditorShowcaseAssetViewerListRecordInput, EditorShowcaseAssetViewerRecord,
EditorShowcaseCampaignConfigRecord, EditorShowcaseCampaignConfigUpsertRecordInput,
ExternalApiKeyAuthenticateRecordInput, ExternalApiKeyCreateRecordInput, ExternalApiKeyRecord,
ExternalApiKeyRevokeRecordInput, ExternalGenerationJobAcknowledgeRecordInput,
@@ -108,6 +108,7 @@ pub use self::editor_project::{
EditorShowcaseAssetLikeToggleRecordInput, EditorShowcaseAssetPublicListRecordInput,
EditorShowcaseAssetRecord, EditorShowcaseAssetRefundMarkRecordInput,
EditorShowcaseAssetSubmitRecordInput, EditorShowcaseCampaignConfigGetRecordInput,
EditorShowcaseAssetViewerListRecordInput, EditorShowcaseAssetViewerRecord,
EditorReferenceRecord, EditorReferenceResolveRecordInput, EditorShowcaseCampaignConfigRecord,
EditorShowcaseCampaignConfigUpsertRecordInput,
};
@@ -260,9 +261,12 @@ pub(crate) use self::editor_project::{
map_editor_project_delete_procedure_result, map_editor_project_layout_save_procedure_result,
map_editor_project_layout_save_v2_procedure_result, map_editor_project_list_procedure_result,
map_editor_project_optional_procedure_result, map_editor_project_required_procedure_result,
map_editor_project_resource_list_procedure_result, map_editor_reference_procedure_result,
map_editor_project_resource_procedure_result, map_editor_showcase_asset_list_procedure_result,
map_editor_project_resource_list_procedure_result,
map_editor_project_resource_procedure_result, map_editor_reference_procedure_result,
map_editor_showcase_asset_list_procedure_result,
map_editor_showcase_asset_procedure_result,
map_editor_showcase_asset_viewer_list_procedure_result,
map_editor_showcase_asset_viewer_procedure_result,
map_editor_showcase_campaign_config_procedure_result,
map_optional_editor_asset_procedure_result,
};
@@ -473,6 +473,13 @@ pub struct EditorShowcaseAssetPublicListRecordInput {
pub cursor: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EditorShowcaseAssetViewerListRecordInput {
pub limit: u32,
pub cursor: Option<String>,
pub viewer_user_id: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EditorShowcaseAssetAdminListRecordInput {
pub limit: u32,
@@ -516,6 +523,12 @@ pub struct EditorShowcaseAssetLikeToggleRecordInput {
pub updated_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct EditorShowcaseAssetViewerRecord {
pub asset: EditorShowcaseAssetRecord,
pub viewer_liked: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EditorShowcaseCampaignConfigGetRecordInput {
pub include_disabled: bool,
@@ -861,6 +874,18 @@ impl From<EditorShowcaseAssetPublicListRecordInput>
}
}
impl From<EditorShowcaseAssetViewerListRecordInput>
for crate::module_bindings::EditorShowcaseAssetViewerListInput
{
fn from(input: EditorShowcaseAssetViewerListRecordInput) -> Self {
Self {
limit: input.limit,
cursor: input.cursor,
viewer_user_id: input.viewer_user_id,
}
}
}
impl From<EditorShowcaseAssetAdminListRecordInput>
for crate::module_bindings::EditorShowcaseAssetAdminListInput
{
@@ -1225,6 +1250,34 @@ pub(crate) fn map_editor_showcase_asset_list_procedure_result(
.collect()
}
pub(crate) fn map_editor_showcase_asset_viewer_procedure_result(
result: EditorShowcaseAssetViewerProcedureResult,
) -> Result<EditorShowcaseAssetViewerRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
result
.item
.map(map_editor_showcase_asset_viewer_snapshot)
.transpose()?
.ok_or_else(|| SpacetimeClientError::missing_snapshot("陶泥儿精选浏览者快照"))
}
pub(crate) fn map_editor_showcase_asset_viewer_list_procedure_result(
result: EditorShowcaseAssetViewerListProcedureResult,
) -> Result<Vec<EditorShowcaseAssetViewerRecord>, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
result
.items
.into_iter()
.map(map_editor_showcase_asset_viewer_snapshot)
.collect()
}
pub(crate) fn map_editor_showcase_campaign_config_procedure_result(
result: EditorShowcaseCampaignConfigProcedureResult,
) -> Result<Option<EditorShowcaseCampaignConfigRecord>, SpacetimeClientError> {
@@ -1505,6 +1558,15 @@ fn map_editor_showcase_asset_snapshot(
})
}
fn map_editor_showcase_asset_viewer_snapshot(
snapshot: EditorShowcaseAssetViewerSnapshot,
) -> Result<EditorShowcaseAssetViewerRecord, SpacetimeClientError> {
Ok(EditorShowcaseAssetViewerRecord {
asset: map_editor_showcase_asset_snapshot(snapshot.asset)?,
viewer_liked: snapshot.viewer_liked,
})
}
fn map_editor_showcase_campaign_config_snapshot(
snapshot: EditorShowcaseCampaignConfigSnapshot,
) -> EditorShowcaseCampaignConfigRecord {
@@ -172,6 +172,7 @@ pub mod chapter_progression_type;
pub mod claim_external_generation_jobs_and_return_procedure;
pub mod claim_profile_recharge_order_expiration_schedule_and_return_procedure;
pub mod claim_profile_task_reward_and_return_procedure;
pub mod clean_editor_image_asset_kind_and_return_procedure;
pub mod clear_database_migration_import_chunks_procedure;
pub mod close_profile_recharge_order_and_return_procedure;
pub mod combat_outcome_type;
@@ -308,6 +309,8 @@ pub mod editor_generation_runtime_identity_rotation_table;
pub mod editor_generation_runtime_identity_rotation_type;
pub mod editor_generation_target_preflight_input_type;
pub mod editor_generation_target_preflight_result_type;
pub mod editor_image_asset_kind_cleanup_input_type;
pub mod editor_image_asset_kind_cleanup_procedure_result_type;
pub mod editor_pixel_art_canvas_completion_input_type;
pub mod editor_pixel_art_canvas_placeholder_input_type;
pub mod editor_pixel_art_result_persist_input_type;
@@ -359,6 +362,10 @@ pub mod editor_showcase_asset_snapshot_type;
pub mod editor_showcase_asset_submit_input_type;
pub mod editor_showcase_asset_table;
pub mod editor_showcase_asset_type;
pub mod editor_showcase_asset_viewer_list_input_type;
pub mod editor_showcase_asset_viewer_list_procedure_result_type;
pub mod editor_showcase_asset_viewer_procedure_result_type;
pub mod editor_showcase_asset_viewer_snapshot_type;
pub mod editor_showcase_campaign_config_get_input_type;
pub mod editor_showcase_campaign_config_procedure_result_type;
pub mod editor_showcase_campaign_config_snapshot_type;
@@ -478,6 +485,7 @@ pub mod list_profile_recharge_refunds_for_reconciliation_procedure;
pub mod list_profile_wallet_ledger_procedure;
pub mod list_public_editor_project_resources_and_return_procedure;
pub mod list_public_editor_showcase_assets_and_return_procedure;
pub mod list_public_editor_showcase_assets_for_viewer_and_return_procedure;
pub mod list_unchecked_expired_profile_recharge_orders_procedure;
pub mod mark_editor_showcase_asset_refunded_and_return_procedure;
pub mod mark_profile_recharge_order_expiration_checked_procedure;
@@ -792,6 +800,7 @@ pub mod save_editor_project_layout_and_return_procedure;
pub mod save_editor_project_layout_v_2_ack_procedure;
pub mod save_editor_project_layout_v_2_and_return_procedure;
pub mod seed_analytics_date_dimensions_reducer;
pub mod set_editor_showcase_asset_like_for_viewer_and_return_procedure;
pub mod square_hole_agent_message_row_type;
pub mod square_hole_agent_message_table;
pub mod square_hole_agent_session_row_type;
@@ -811,7 +820,6 @@ pub mod story_session_type;
pub mod submit_editor_showcase_asset_and_return_procedure;
pub mod submit_profile_feedback_and_return_procedure;
pub mod sync_auth_store_projection_procedure;
pub mod toggle_editor_showcase_asset_like_and_return_procedure;
pub mod touch_editor_agent_conversation_and_return_procedure;
pub mod tracking_daily_stat_table;
pub mod tracking_daily_stat_type;
@@ -1021,6 +1029,7 @@ pub use chapter_progression_type::ChapterProgression;
pub use claim_external_generation_jobs_and_return_procedure::claim_external_generation_jobs_and_return;
pub use claim_profile_recharge_order_expiration_schedule_and_return_procedure::claim_profile_recharge_order_expiration_schedule_and_return;
pub use claim_profile_task_reward_and_return_procedure::claim_profile_task_reward_and_return;
pub use clean_editor_image_asset_kind_and_return_procedure::clean_editor_image_asset_kind_and_return;
pub use clear_database_migration_import_chunks_procedure::clear_database_migration_import_chunks;
pub use close_profile_recharge_order_and_return_procedure::close_profile_recharge_order_and_return;
pub use combat_outcome_type::CombatOutcome;
@@ -1157,6 +1166,8 @@ pub use editor_generation_runtime_identity_rotation_table::*;
pub use editor_generation_runtime_identity_rotation_type::EditorGenerationRuntimeIdentityRotation;
pub use editor_generation_target_preflight_input_type::EditorGenerationTargetPreflightInput;
pub use editor_generation_target_preflight_result_type::EditorGenerationTargetPreflightResult;
pub use editor_image_asset_kind_cleanup_input_type::EditorImageAssetKindCleanupInput;
pub use editor_image_asset_kind_cleanup_procedure_result_type::EditorImageAssetKindCleanupProcedureResult;
pub use editor_pixel_art_canvas_completion_input_type::EditorPixelArtCanvasCompletionInput;
pub use editor_pixel_art_canvas_placeholder_input_type::EditorPixelArtCanvasPlaceholderInput;
pub use editor_pixel_art_result_persist_input_type::EditorPixelArtResultPersistInput;
@@ -1208,6 +1219,10 @@ pub use editor_showcase_asset_snapshot_type::EditorShowcaseAssetSnapshot;
pub use editor_showcase_asset_submit_input_type::EditorShowcaseAssetSubmitInput;
pub use editor_showcase_asset_table::*;
pub use editor_showcase_asset_type::EditorShowcaseAsset;
pub use editor_showcase_asset_viewer_list_input_type::EditorShowcaseAssetViewerListInput;
pub use editor_showcase_asset_viewer_list_procedure_result_type::EditorShowcaseAssetViewerListProcedureResult;
pub use editor_showcase_asset_viewer_procedure_result_type::EditorShowcaseAssetViewerProcedureResult;
pub use editor_showcase_asset_viewer_snapshot_type::EditorShowcaseAssetViewerSnapshot;
pub use editor_showcase_campaign_config_get_input_type::EditorShowcaseCampaignConfigGetInput;
pub use editor_showcase_campaign_config_procedure_result_type::EditorShowcaseCampaignConfigProcedureResult;
pub use editor_showcase_campaign_config_snapshot_type::EditorShowcaseCampaignConfigSnapshot;
@@ -1327,6 +1342,7 @@ pub use list_profile_recharge_refunds_for_reconciliation_procedure::list_profile
pub use list_profile_wallet_ledger_procedure::list_profile_wallet_ledger;
pub use list_public_editor_project_resources_and_return_procedure::list_public_editor_project_resources_and_return;
pub use list_public_editor_showcase_assets_and_return_procedure::list_public_editor_showcase_assets_and_return;
pub use list_public_editor_showcase_assets_for_viewer_and_return_procedure::list_public_editor_showcase_assets_for_viewer_and_return;
pub use list_unchecked_expired_profile_recharge_orders_procedure::list_unchecked_expired_profile_recharge_orders;
pub use mark_editor_showcase_asset_refunded_and_return_procedure::mark_editor_showcase_asset_refunded_and_return;
pub use mark_profile_recharge_order_expiration_checked_procedure::mark_profile_recharge_order_expiration_checked;
@@ -1641,6 +1657,7 @@ pub use save_editor_project_layout_and_return_procedure::save_editor_project_lay
pub use save_editor_project_layout_v_2_ack_procedure::save_editor_project_layout_v_2_ack;
pub use save_editor_project_layout_v_2_and_return_procedure::save_editor_project_layout_v_2_and_return;
pub use seed_analytics_date_dimensions_reducer::seed_analytics_date_dimensions;
pub use set_editor_showcase_asset_like_for_viewer_and_return_procedure::set_editor_showcase_asset_like_for_viewer_and_return;
pub use square_hole_agent_message_row_type::SquareHoleAgentMessageRow;
pub use square_hole_agent_message_table::*;
pub use square_hole_agent_session_row_type::SquareHoleAgentSessionRow;
@@ -1660,7 +1677,6 @@ pub use story_session_type::StorySession;
pub use submit_editor_showcase_asset_and_return_procedure::submit_editor_showcase_asset_and_return;
pub use submit_profile_feedback_and_return_procedure::submit_profile_feedback_and_return;
pub use sync_auth_store_projection_procedure::sync_auth_store_projection;
pub use toggle_editor_showcase_asset_like_and_return_procedure::toggle_editor_showcase_asset_like_and_return;
pub use touch_editor_agent_conversation_and_return_procedure::touch_editor_agent_conversation_and_return;
pub use tracking_daily_stat_table::*;
pub use tracking_daily_stat_type::TrackingDailyStat;
@@ -0,0 +1,59 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
use super::editor_image_asset_kind_cleanup_input_type::EditorImageAssetKindCleanupInput;
use super::editor_image_asset_kind_cleanup_procedure_result_type::EditorImageAssetKindCleanupProcedureResult;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
struct CleanEditorImageAssetKindAndReturnArgs {
pub input: EditorImageAssetKindCleanupInput,
}
impl __sdk::InModule for CleanEditorImageAssetKindAndReturnArgs {
type Module = super::RemoteModule;
}
#[allow(non_camel_case_types)]
/// Extension trait for access to the procedure `clean_editor_image_asset_kind_and_return`.
///
/// Implemented for [`super::RemoteProcedures`].
pub trait clean_editor_image_asset_kind_and_return {
fn clean_editor_image_asset_kind_and_return(&self, input: EditorImageAssetKindCleanupInput) {
self.clean_editor_image_asset_kind_and_return_then(input, |_, _| {});
}
fn clean_editor_image_asset_kind_and_return_then(
&self,
input: EditorImageAssetKindCleanupInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<EditorImageAssetKindCleanupProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
impl clean_editor_image_asset_kind_and_return for super::RemoteProcedures {
fn clean_editor_image_asset_kind_and_return_then(
&self,
input: EditorImageAssetKindCleanupInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<EditorImageAssetKindCleanupProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, EditorImageAssetKindCleanupProcedureResult>(
"clean_editor_image_asset_kind_and_return",
CleanEditorImageAssetKindAndReturnArgs { input },
__callback,
);
}
}
@@ -0,0 +1,19 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub struct EditorImageAssetKindCleanupInput {
pub scope: String,
pub cursor: Option<String>,
pub limit: u32,
pub dry_run: bool,
pub expected_batch_sha_256: Option<String>,
}
impl __sdk::InModule for EditorImageAssetKindCleanupInput {
type Module = super::RemoteModule;
}

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