合并master并保留Spine序列帧修复
合并master当前工程、后端、前端和文档更新 按已确认方案解决Spine序列帧多模态、帧数和快速编辑冲突 保留当前分支底部工具栏宽度与隐藏滚动条样式
This commit is contained in:
@@ -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}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
+59
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
+19
@@ -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
Reference in New Issue
Block a user