1410 lines
46 KiB
Rust
1410 lines
46 KiB
Rust
use super::*;
|
||
|
||
pub async fn create_match3d_agent_session(
|
||
State(state): State<AppState>,
|
||
Extension(request_context): Extension<RequestContext>,
|
||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||
payload: Result<Json<CreateMatch3DAgentSessionRequest>, JsonRejection>,
|
||
) -> Result<Json<Value>, Response> {
|
||
let Json(payload) = match3d_json(payload, &request_context, MATCH3D_AGENT_PROVIDER)?;
|
||
let config = build_config_from_create_request(&payload);
|
||
let seed_text = build_seed_text(&payload, &config);
|
||
let welcome_message_text = MATCH3D_QUESTION_THEME.to_string();
|
||
|
||
let session = state
|
||
.spacetime_client()
|
||
.create_match3d_agent_session(Match3DAgentSessionCreateRecordInput {
|
||
session_id: build_prefixed_uuid_id(MATCH3D_SESSION_ID_PREFIX),
|
||
owner_user_id: authenticated.claims().user_id().to_string(),
|
||
seed_text,
|
||
welcome_message_id: build_prefixed_uuid_id(MATCH3D_MESSAGE_ID_PREFIX),
|
||
welcome_message_text,
|
||
config_json: serialize_match3d_config(&config),
|
||
created_at_micros: current_utc_micros(),
|
||
})
|
||
.await
|
||
.map_err(|error| {
|
||
match3d_error_response(
|
||
&request_context,
|
||
MATCH3D_AGENT_PROVIDER,
|
||
map_match3d_client_error(error),
|
||
)
|
||
})?;
|
||
|
||
Ok(json_success_body(
|
||
Some(&request_context),
|
||
Match3DAgentSessionResponse {
|
||
session: load_match3d_agent_session_response_with_persisted_assets(
|
||
&state,
|
||
authenticated.claims().user_id(),
|
||
session,
|
||
)
|
||
.await,
|
||
},
|
||
))
|
||
}
|
||
|
||
pub async fn get_match3d_agent_session(
|
||
State(state): State<AppState>,
|
||
Path(session_id): Path<String>,
|
||
Extension(request_context): Extension<RequestContext>,
|
||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||
) -> Result<Json<Value>, Response> {
|
||
ensure_non_empty(
|
||
&request_context,
|
||
MATCH3D_AGENT_PROVIDER,
|
||
&session_id,
|
||
"sessionId",
|
||
)?;
|
||
|
||
let session = state
|
||
.spacetime_client()
|
||
.get_match3d_agent_session(session_id, authenticated.claims().user_id().to_string())
|
||
.await
|
||
.map_err(|error| {
|
||
match3d_error_response(
|
||
&request_context,
|
||
MATCH3D_AGENT_PROVIDER,
|
||
map_match3d_client_error(error),
|
||
)
|
||
})?;
|
||
|
||
Ok(json_success_body(
|
||
Some(&request_context),
|
||
Match3DAgentSessionResponse {
|
||
session: load_match3d_agent_session_response_with_persisted_assets(
|
||
&state,
|
||
authenticated.claims().user_id(),
|
||
session,
|
||
)
|
||
.await,
|
||
},
|
||
))
|
||
}
|
||
|
||
pub async fn submit_match3d_agent_message(
|
||
State(state): State<AppState>,
|
||
Path(session_id): Path<String>,
|
||
Extension(request_context): Extension<RequestContext>,
|
||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||
payload: Result<Json<SendMatch3DAgentMessageRequest>, JsonRejection>,
|
||
) -> Result<Json<Value>, Response> {
|
||
let Json(payload) = match3d_json(payload, &request_context, MATCH3D_AGENT_PROVIDER)?;
|
||
let session = submit_and_finalize_match3d_message(
|
||
&state,
|
||
&request_context,
|
||
authenticated.claims().user_id(),
|
||
session_id,
|
||
payload,
|
||
)
|
||
.await?;
|
||
|
||
Ok(json_success_body(
|
||
Some(&request_context),
|
||
Match3DAgentSessionResponse {
|
||
session: load_match3d_agent_session_response_with_persisted_assets(
|
||
&state,
|
||
authenticated.claims().user_id(),
|
||
session,
|
||
)
|
||
.await,
|
||
},
|
||
))
|
||
}
|
||
|
||
pub async fn stream_match3d_agent_message(
|
||
State(state): State<AppState>,
|
||
Path(session_id): Path<String>,
|
||
Extension(request_context): Extension<RequestContext>,
|
||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||
payload: Result<Json<SendMatch3DAgentMessageRequest>, JsonRejection>,
|
||
) -> Result<Response, Response> {
|
||
let Json(payload) = match3d_json(payload, &request_context, MATCH3D_AGENT_PROVIDER)?;
|
||
ensure_non_empty(
|
||
&request_context,
|
||
MATCH3D_AGENT_PROVIDER,
|
||
&session_id,
|
||
"sessionId",
|
||
)?;
|
||
|
||
let owner_user_id = authenticated.claims().user_id().to_string();
|
||
let request_context_for_stream = request_context.clone();
|
||
let stream = async_stream::stream! {
|
||
let result = submit_and_finalize_match3d_message(
|
||
&state,
|
||
&request_context_for_stream,
|
||
owner_user_id.as_str(),
|
||
session_id,
|
||
payload,
|
||
)
|
||
.await;
|
||
|
||
match result {
|
||
Ok(session) => {
|
||
let session_response = load_match3d_agent_session_response_with_persisted_assets(
|
||
&state,
|
||
owner_user_id.as_str(),
|
||
session,
|
||
)
|
||
.await;
|
||
if let Some(reply) = session_response.last_assistant_reply.clone() {
|
||
yield Ok::<Event, Infallible>(match3d_sse_json_event_or_error(
|
||
"reply_delta",
|
||
json!({ "text": reply }),
|
||
));
|
||
}
|
||
yield Ok::<Event, Infallible>(match3d_sse_json_event_or_error(
|
||
"session",
|
||
json!({ "session": session_response }),
|
||
));
|
||
yield Ok::<Event, Infallible>(match3d_sse_json_event_or_error(
|
||
"done",
|
||
json!({ "ok": true }),
|
||
));
|
||
}
|
||
Err(response) => {
|
||
yield Ok::<Event, Infallible>(match3d_sse_json_event_or_error(
|
||
"error",
|
||
json!({ "message": response.status().to_string() }),
|
||
));
|
||
}
|
||
}
|
||
};
|
||
|
||
Ok(Sse::new(stream).into_response())
|
||
}
|
||
|
||
pub async fn execute_match3d_agent_action(
|
||
State(state): State<AppState>,
|
||
Path(session_id): Path<String>,
|
||
Extension(request_context): Extension<RequestContext>,
|
||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||
payload: Result<Json<ExecuteMatch3DAgentActionRequest>, JsonRejection>,
|
||
) -> Result<Json<Value>, Response> {
|
||
let Json(payload) = match3d_json(payload, &request_context, MATCH3D_AGENT_PROVIDER)?;
|
||
ensure_non_empty(
|
||
&request_context,
|
||
MATCH3D_AGENT_PROVIDER,
|
||
&session_id,
|
||
"sessionId",
|
||
)?;
|
||
|
||
if payload.action.trim() != "match3d_compile_draft" {
|
||
return Err(match3d_bad_request(
|
||
&request_context,
|
||
MATCH3D_AGENT_PROVIDER,
|
||
"unknown match3d action",
|
||
));
|
||
}
|
||
|
||
let (session, generated_item_assets) = compile_match3d_draft_for_session(
|
||
&state,
|
||
&request_context,
|
||
&authenticated,
|
||
session_id,
|
||
payload.game_name,
|
||
payload.summary,
|
||
payload.tags,
|
||
payload.cover_image_src,
|
||
payload.generate_click_sound,
|
||
)
|
||
.await?;
|
||
|
||
Ok(json_success_body(
|
||
Some(&request_context),
|
||
Match3DAgentActionResponse {
|
||
session: map_match3d_agent_session_response_with_assets(
|
||
session,
|
||
&generated_item_assets,
|
||
),
|
||
},
|
||
))
|
||
}
|
||
|
||
pub async fn compile_match3d_agent_draft(
|
||
State(state): State<AppState>,
|
||
Path(session_id): Path<String>,
|
||
Extension(request_context): Extension<RequestContext>,
|
||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||
payload: Result<Json<CompileMatch3DDraftRequest>, JsonRejection>,
|
||
) -> Result<Json<Value>, Response> {
|
||
let payload = payload
|
||
.map(|Json(payload)| payload)
|
||
.unwrap_or(CompileMatch3DDraftRequest {
|
||
game_name: None,
|
||
summary: None,
|
||
tags: None,
|
||
cover_image_src: None,
|
||
generate_click_sound: None,
|
||
});
|
||
ensure_non_empty(
|
||
&request_context,
|
||
MATCH3D_AGENT_PROVIDER,
|
||
&session_id,
|
||
"sessionId",
|
||
)?;
|
||
|
||
let (session, generated_item_assets) = compile_match3d_draft_for_session(
|
||
&state,
|
||
&request_context,
|
||
&authenticated,
|
||
session_id,
|
||
payload.game_name,
|
||
payload.summary,
|
||
payload.tags,
|
||
payload.cover_image_src,
|
||
payload.generate_click_sound,
|
||
)
|
||
.await?;
|
||
|
||
Ok(json_success_body(
|
||
Some(&request_context),
|
||
Match3DAgentActionResponse {
|
||
session: map_match3d_agent_session_response_with_assets(
|
||
session,
|
||
&generated_item_assets,
|
||
),
|
||
},
|
||
))
|
||
}
|
||
|
||
pub async fn get_match3d_works(
|
||
State(state): State<AppState>,
|
||
Extension(request_context): Extension<RequestContext>,
|
||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||
) -> Result<Json<Value>, Response> {
|
||
let items = state
|
||
.spacetime_client()
|
||
.list_match3d_works(authenticated.claims().user_id().to_string())
|
||
.await
|
||
.map_err(|error| {
|
||
match3d_error_response(
|
||
&request_context,
|
||
MATCH3D_WORKS_PROVIDER,
|
||
map_match3d_client_error(error),
|
||
)
|
||
})?;
|
||
|
||
Ok(json_success_body(
|
||
Some(&request_context),
|
||
Match3DWorksResponse {
|
||
items: items
|
||
.into_iter()
|
||
.map(map_match3d_work_summary_response)
|
||
.collect(),
|
||
},
|
||
))
|
||
}
|
||
|
||
pub async fn list_match3d_gallery(
|
||
State(state): State<AppState>,
|
||
Extension(request_context): Extension<RequestContext>,
|
||
) -> Result<Json<Value>, Response> {
|
||
let items = state
|
||
.spacetime_client()
|
||
.list_match3d_gallery()
|
||
.await
|
||
.map_err(|error| {
|
||
match3d_error_response(
|
||
&request_context,
|
||
MATCH3D_WORKS_PROVIDER,
|
||
map_match3d_client_error(error),
|
||
)
|
||
})?;
|
||
|
||
Ok(json_success_body(
|
||
Some(&request_context),
|
||
Match3DWorksResponse {
|
||
items: items
|
||
.into_iter()
|
||
.map(map_match3d_work_summary_response)
|
||
.collect(),
|
||
},
|
||
))
|
||
}
|
||
|
||
pub async fn get_match3d_work_detail(
|
||
State(state): State<AppState>,
|
||
Path(profile_id): Path<String>,
|
||
Extension(request_context): Extension<RequestContext>,
|
||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||
) -> Result<Json<Value>, Response> {
|
||
ensure_non_empty(
|
||
&request_context,
|
||
MATCH3D_WORKS_PROVIDER,
|
||
&profile_id,
|
||
"profileId",
|
||
)?;
|
||
|
||
let item = state
|
||
.spacetime_client()
|
||
.get_match3d_work_detail(profile_id, authenticated.claims().user_id().to_string())
|
||
.await
|
||
.map_err(|error| {
|
||
match3d_error_response(
|
||
&request_context,
|
||
MATCH3D_WORKS_PROVIDER,
|
||
map_match3d_client_error(error),
|
||
)
|
||
})?;
|
||
|
||
Ok(json_success_body(
|
||
Some(&request_context),
|
||
Match3DWorkDetailResponse {
|
||
item: map_match3d_work_profile_response(item),
|
||
},
|
||
))
|
||
}
|
||
|
||
pub async fn put_match3d_work(
|
||
State(state): State<AppState>,
|
||
Path(profile_id): Path<String>,
|
||
Extension(request_context): Extension<RequestContext>,
|
||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||
payload: Result<Json<PutMatch3DWorkRequest>, JsonRejection>,
|
||
) -> Result<Json<Value>, Response> {
|
||
let Json(payload) = match3d_json(payload, &request_context, MATCH3D_WORKS_PROVIDER)?;
|
||
ensure_non_empty(
|
||
&request_context,
|
||
MATCH3D_WORKS_PROVIDER,
|
||
&profile_id,
|
||
"profileId",
|
||
)?;
|
||
|
||
let existing = state
|
||
.spacetime_client()
|
||
.get_match3d_work_detail(
|
||
profile_id.clone(),
|
||
authenticated.claims().user_id().to_string(),
|
||
)
|
||
.await
|
||
.map_err(|error| {
|
||
match3d_error_response(
|
||
&request_context,
|
||
MATCH3D_WORKS_PROVIDER,
|
||
map_match3d_client_error(error),
|
||
)
|
||
})?;
|
||
let theme_text = payload
|
||
.theme_text
|
||
.clone()
|
||
.filter(|value| !value.trim().is_empty())
|
||
.unwrap_or(existing.theme_text);
|
||
let item = state
|
||
.spacetime_client()
|
||
.update_match3d_work(Match3DWorkUpdateRecordInput {
|
||
profile_id,
|
||
owner_user_id: authenticated.claims().user_id().to_string(),
|
||
game_name: payload.game_name,
|
||
theme_text,
|
||
summary_text: payload.summary,
|
||
tags_json: serde_json::to_string(&normalize_tags(payload.tags)).unwrap_or_default(),
|
||
cover_image_src: payload.cover_image_src.unwrap_or_default(),
|
||
cover_asset_id: String::new(),
|
||
clear_count: payload.clear_count,
|
||
difficulty: payload.difficulty,
|
||
updated_at_micros: current_utc_micros(),
|
||
})
|
||
.await
|
||
.map_err(|error| {
|
||
match3d_error_response(
|
||
&request_context,
|
||
MATCH3D_WORKS_PROVIDER,
|
||
map_match3d_client_error(error),
|
||
)
|
||
})?;
|
||
|
||
Ok(json_success_body(
|
||
Some(&request_context),
|
||
Match3DWorkMutationResponse {
|
||
item: map_match3d_work_profile_response(item),
|
||
},
|
||
))
|
||
}
|
||
|
||
pub async fn put_match3d_audio_assets(
|
||
State(state): State<AppState>,
|
||
Path(profile_id): Path<String>,
|
||
Extension(request_context): Extension<RequestContext>,
|
||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||
payload: Result<Json<PutMatch3DAudioAssetsRequest>, JsonRejection>,
|
||
) -> Result<Json<Value>, Response> {
|
||
let Json(payload) = match3d_json(payload, &request_context, MATCH3D_WORKS_PROVIDER)?;
|
||
ensure_non_empty(
|
||
&request_context,
|
||
MATCH3D_WORKS_PROVIDER,
|
||
&profile_id,
|
||
"profileId",
|
||
)?;
|
||
|
||
let owner_user_id = authenticated.claims().user_id().to_string();
|
||
let existing = state
|
||
.spacetime_client()
|
||
.get_match3d_work_detail(profile_id.clone(), owner_user_id.clone())
|
||
.await
|
||
.map_err(|error| {
|
||
match3d_error_response(
|
||
&request_context,
|
||
MATCH3D_WORKS_PROVIDER,
|
||
map_match3d_client_error(error),
|
||
)
|
||
})?;
|
||
let session_id = existing.source_session_id.clone().ok_or_else(|| {
|
||
match3d_error_response(
|
||
&request_context,
|
||
MATCH3D_WORKS_PROVIDER,
|
||
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||
"provider": MATCH3D_WORKS_PROVIDER,
|
||
"message": "抓大鹅作品缺少来源 session,无法写回音频素材",
|
||
})),
|
||
)
|
||
})?;
|
||
let assets = payload
|
||
.generated_item_assets
|
||
.into_iter()
|
||
.map(Match3DGeneratedItemAsset::from)
|
||
.collect::<Vec<_>>();
|
||
let session = upsert_match3d_draft_snapshot(
|
||
&state,
|
||
&request_context,
|
||
&authenticated,
|
||
session_id,
|
||
owner_user_id.clone(),
|
||
profile_id.clone(),
|
||
Some(existing.game_name),
|
||
Some(existing.summary),
|
||
Some(serde_json::to_string(&existing.tags).unwrap_or_default()),
|
||
existing.cover_image_src,
|
||
None,
|
||
serialize_match3d_generated_item_assets(&assets),
|
||
)
|
||
.await?;
|
||
|
||
let item = state
|
||
.spacetime_client()
|
||
.get_match3d_work_detail(profile_id, owner_user_id)
|
||
.await
|
||
.map_err(|error| {
|
||
match3d_error_response(
|
||
&request_context,
|
||
MATCH3D_WORKS_PROVIDER,
|
||
map_match3d_client_error(error),
|
||
)
|
||
})?;
|
||
let _ = session;
|
||
Ok(json_success_body(
|
||
Some(&request_context),
|
||
Match3DWorkMutationResponse {
|
||
item: map_match3d_work_profile_response(item),
|
||
},
|
||
))
|
||
}
|
||
|
||
pub async fn persist_match3d_generated_model(
|
||
State(state): State<AppState>,
|
||
Path(profile_id): Path<String>,
|
||
Extension(request_context): Extension<RequestContext>,
|
||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||
payload: Result<Json<PersistMatch3DGeneratedModelRequest>, JsonRejection>,
|
||
) -> Result<Json<Value>, Response> {
|
||
let Json(payload) = match3d_json(payload, &request_context, MATCH3D_WORKS_PROVIDER)?;
|
||
ensure_non_empty(
|
||
&request_context,
|
||
MATCH3D_WORKS_PROVIDER,
|
||
&profile_id,
|
||
"profileId",
|
||
)?;
|
||
ensure_non_empty(
|
||
&request_context,
|
||
MATCH3D_WORKS_PROVIDER,
|
||
&payload.item_id,
|
||
"itemId",
|
||
)?;
|
||
ensure_non_empty(
|
||
&request_context,
|
||
MATCH3D_WORKS_PROVIDER,
|
||
&payload.item_name,
|
||
"itemName",
|
||
)?;
|
||
ensure_non_empty(
|
||
&request_context,
|
||
MATCH3D_WORKS_PROVIDER,
|
||
&payload.source_url,
|
||
"sourceUrl",
|
||
)?;
|
||
|
||
let owner_user_id = authenticated.claims().user_id().to_string();
|
||
let existing = state
|
||
.spacetime_client()
|
||
.get_match3d_work_detail(profile_id.clone(), owner_user_id.clone())
|
||
.await
|
||
.map_err(|error| {
|
||
match3d_error_response(
|
||
&request_context,
|
||
MATCH3D_WORKS_PROVIDER,
|
||
map_match3d_client_error(error),
|
||
)
|
||
})?;
|
||
let session_id = existing.source_session_id.clone().ok_or_else(|| {
|
||
match3d_error_response(
|
||
&request_context,
|
||
MATCH3D_WORKS_PROVIDER,
|
||
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||
"provider": MATCH3D_WORKS_PROVIDER,
|
||
"message": "抓大鹅作品缺少来源 session,无法保存历史模型",
|
||
})),
|
||
)
|
||
})?;
|
||
|
||
let mut assets =
|
||
parse_match3d_generated_item_assets(existing.generated_item_assets_json.as_deref())
|
||
.into_iter()
|
||
.map(Match3DGeneratedItemAsset::from)
|
||
.collect::<Vec<_>>();
|
||
let current_asset = assets
|
||
.iter()
|
||
.find(|asset| asset.item_id == payload.item_id)
|
||
.cloned();
|
||
let item_name = normalize_match3d_item_name(payload.item_name.as_str());
|
||
let item_name = if item_name.is_empty() {
|
||
current_asset
|
||
.as_ref()
|
||
.map(|asset| asset.item_name.clone())
|
||
.unwrap_or_else(|| payload.item_name.trim().to_string())
|
||
} else {
|
||
item_name
|
||
};
|
||
let model_file = hyper3d_contract::Hyper3dDownloadFilePayload {
|
||
name: normalize_optional_text(payload.file_name.as_deref())
|
||
.unwrap_or_else(|| "model.glb".to_string()),
|
||
url: payload.source_url.trim().to_string(),
|
||
};
|
||
let downloaded_model = download_match3d_legacy_model(&model_file)
|
||
.await
|
||
.map_err(|error| match3d_error_response(&request_context, MATCH3D_WORKS_PROVIDER, error))?;
|
||
let task_uuid = normalize_optional_text(payload.task_uuid.as_deref());
|
||
let item_slug = build_match3d_item_slug(payload.item_id.as_str(), item_name.as_str());
|
||
let generated_at_micros = current_utc_micros();
|
||
let uploaded_model = persist_match3d_generated_bytes(
|
||
&state,
|
||
owner_user_id.as_str(),
|
||
session_id.as_str(),
|
||
profile_id.as_str(),
|
||
&[
|
||
"items",
|
||
item_slug.as_str(),
|
||
"model",
|
||
task_uuid.as_deref().unwrap_or("manual"),
|
||
],
|
||
downloaded_model.file_name.as_str(),
|
||
downloaded_model.content_type.as_str(),
|
||
downloaded_model.bytes,
|
||
"match3d_item_model",
|
||
task_uuid.as_deref(),
|
||
generated_at_micros,
|
||
)
|
||
.await
|
||
.map_err(|error| match3d_error_response(&request_context, MATCH3D_WORKS_PROVIDER, error))?;
|
||
let next_asset = Match3DGeneratedItemAsset {
|
||
item_id: payload.item_id,
|
||
item_name,
|
||
item_size: current_asset
|
||
.as_ref()
|
||
.and_then(|asset| asset.item_size.clone())
|
||
.or_else(|| Some(MATCH3D_ITEM_SIZE_LARGE.to_string())),
|
||
image_src: current_asset
|
||
.as_ref()
|
||
.and_then(|asset| asset.image_src.clone()),
|
||
image_object_key: current_asset
|
||
.as_ref()
|
||
.and_then(|asset| asset.image_object_key.clone()),
|
||
image_views: current_asset
|
||
.as_ref()
|
||
.map(|asset| asset.image_views.clone())
|
||
.unwrap_or_default(),
|
||
model_src: Some(uploaded_model.src),
|
||
model_object_key: Some(uploaded_model.object_key),
|
||
model_file_name: Some(downloaded_model.file_name),
|
||
task_uuid,
|
||
subscription_key: normalize_optional_text(payload.subscription_key.as_deref()).or_else(
|
||
|| {
|
||
current_asset
|
||
.as_ref()
|
||
.and_then(|asset| asset.subscription_key.clone())
|
||
},
|
||
),
|
||
sound_prompt: current_asset
|
||
.as_ref()
|
||
.and_then(|asset| asset.sound_prompt.clone()),
|
||
background_music_title: current_asset
|
||
.as_ref()
|
||
.and_then(|asset| asset.background_music_title.clone()),
|
||
background_music_style: current_asset
|
||
.as_ref()
|
||
.and_then(|asset| asset.background_music_style.clone()),
|
||
background_music_prompt: current_asset
|
||
.as_ref()
|
||
.and_then(|asset| asset.background_music_prompt.clone()),
|
||
background_music: current_asset
|
||
.as_ref()
|
||
.and_then(|asset| asset.background_music.clone()),
|
||
click_sound: current_asset
|
||
.as_ref()
|
||
.and_then(|asset| asset.click_sound.clone()),
|
||
background_asset: current_asset
|
||
.as_ref()
|
||
.and_then(|asset| asset.background_asset.clone()),
|
||
status: "model_ready".to_string(),
|
||
error: None,
|
||
};
|
||
upsert_match3d_generated_item_asset(&mut assets, next_asset.clone());
|
||
persist_match3d_generated_item_assets_snapshot(
|
||
&state,
|
||
&request_context,
|
||
&authenticated,
|
||
session_id.as_str(),
|
||
owner_user_id.as_str(),
|
||
profile_id.as_str(),
|
||
&assets,
|
||
)
|
||
.await?;
|
||
|
||
Ok(json_success_body(
|
||
Some(&request_context),
|
||
PersistMatch3DGeneratedModelResponse {
|
||
asset: map_match3d_generated_item_asset_for_work(Match3DGeneratedItemAssetJson::from(
|
||
next_asset,
|
||
)),
|
||
},
|
||
))
|
||
}
|
||
|
||
pub async fn generate_match3d_cover_image(
|
||
State(state): State<AppState>,
|
||
Path(profile_id): Path<String>,
|
||
Extension(request_context): Extension<RequestContext>,
|
||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||
payload: Result<Json<GenerateMatch3DCoverImageRequest>, JsonRejection>,
|
||
) -> Result<Json<Value>, Response> {
|
||
let Json(payload) = match3d_json(payload, &request_context, MATCH3D_WORKS_PROVIDER)?;
|
||
ensure_non_empty(
|
||
&request_context,
|
||
MATCH3D_WORKS_PROVIDER,
|
||
&profile_id,
|
||
"profileId",
|
||
)?;
|
||
let prompt = normalize_match3d_cover_prompt(payload.prompt.as_str());
|
||
ensure_non_empty(&request_context, MATCH3D_WORKS_PROVIDER, &prompt, "prompt")?;
|
||
|
||
let context =
|
||
load_match3d_work_asset_context(&state, &request_context, &authenticated, &profile_id)
|
||
.await?;
|
||
let generated_cover = generate_match3d_cover_image_asset(
|
||
&state,
|
||
&request_context,
|
||
&context.owner_user_id,
|
||
context.session_id.as_str(),
|
||
profile_id.as_str(),
|
||
&context.config,
|
||
prompt.as_str(),
|
||
payload.uploaded_image_src,
|
||
collect_match3d_cover_reference_image_sources(
|
||
payload.reference_image_src,
|
||
payload.reference_image_srcs,
|
||
),
|
||
)
|
||
.await
|
||
.map_err(|error| match3d_error_response(&request_context, MATCH3D_WORKS_PROVIDER, error))?;
|
||
|
||
let item = update_match3d_work_cover_only(
|
||
&state,
|
||
&request_context,
|
||
context.owner_user_id.as_str(),
|
||
context.profile,
|
||
generated_cover.src.as_str(),
|
||
)
|
||
.await?;
|
||
|
||
Ok(json_success_body(
|
||
Some(&request_context),
|
||
GenerateMatch3DCoverImageResponse {
|
||
item: map_match3d_work_profile_response(item),
|
||
cover_image_src: generated_cover.src,
|
||
cover_image_object_key: generated_cover.object_key,
|
||
prompt,
|
||
},
|
||
))
|
||
}
|
||
pub async fn generate_match3d_background_image_for_work(
|
||
State(state): State<AppState>,
|
||
Path(profile_id): Path<String>,
|
||
Extension(request_context): Extension<RequestContext>,
|
||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||
payload: Result<Json<GenerateMatch3DBackgroundImageRequest>, JsonRejection>,
|
||
) -> Result<Json<Value>, Response> {
|
||
let Json(payload) = match3d_json(payload, &request_context, MATCH3D_WORKS_PROVIDER)?;
|
||
ensure_non_empty(
|
||
&request_context,
|
||
MATCH3D_WORKS_PROVIDER,
|
||
&profile_id,
|
||
"profileId",
|
||
)?;
|
||
let prompt = normalize_match3d_background_prompt(payload.prompt.as_str());
|
||
ensure_non_empty(&request_context, MATCH3D_WORKS_PROVIDER, &prompt, "prompt")?;
|
||
let prompt_fingerprint = build_match3d_prompt_fingerprint(prompt.as_str());
|
||
|
||
let context =
|
||
load_match3d_work_asset_context(&state, &request_context, &authenticated, &profile_id)
|
||
.await?;
|
||
let Match3DWorkAssetContext {
|
||
owner_user_id,
|
||
session_id,
|
||
profile,
|
||
config,
|
||
assets,
|
||
} = context;
|
||
let billing_asset_id = format!("{}:{}:{}", session_id, profile_id, prompt_fingerprint);
|
||
let (generated_background, generated_assets) = execute_billable_asset_operation_with_cost(
|
||
&state,
|
||
owner_user_id.as_str(),
|
||
"match3d_ui_background_image",
|
||
billing_asset_id.as_str(),
|
||
MATCH3D_BACKGROUND_IMAGE_POINTS_COST,
|
||
async {
|
||
let generated_background = generate_match3d_background_image(
|
||
&state,
|
||
&request_context,
|
||
owner_user_id.as_str(),
|
||
session_id.as_str(),
|
||
profile_id.as_str(),
|
||
&config,
|
||
prompt.as_str(),
|
||
)
|
||
.await?;
|
||
let mut assets = assets;
|
||
attach_match3d_background_asset_to_assets(&mut assets, generated_background.clone());
|
||
let save_result = persist_match3d_generated_item_assets_snapshot(
|
||
&state,
|
||
&request_context,
|
||
&authenticated,
|
||
session_id.as_str(),
|
||
owner_user_id.as_str(),
|
||
profile_id.as_str(),
|
||
&assets,
|
||
)
|
||
.await;
|
||
if let Err(response) = save_result {
|
||
tracing::warn!(
|
||
provider = MATCH3D_WORKS_PROVIDER,
|
||
profile_id,
|
||
owner_user_id = %owner_user_id,
|
||
status = %response.status(),
|
||
"抓大鹅 UI 背景图已生成但 SpacetimeDB 草稿写回不可用,降级返回本次生成资产"
|
||
);
|
||
}
|
||
Ok((generated_background, assets))
|
||
},
|
||
)
|
||
.await
|
||
.map_err(|error| match3d_error_response(&request_context, MATCH3D_WORKS_PROVIDER, error))?;
|
||
|
||
let item = state
|
||
.spacetime_client()
|
||
.get_match3d_work_detail(profile_id.clone(), owner_user_id.clone())
|
||
.await
|
||
.map(|item| map_match3d_work_profile_response(item))
|
||
.unwrap_or_else(|error| {
|
||
tracing::warn!(
|
||
provider = MATCH3D_WORKS_PROVIDER,
|
||
profile_id,
|
||
owner_user_id = %owner_user_id,
|
||
error = %error,
|
||
"抓大鹅 UI 背景图生成后读取作品详情失败,降级使用写回前快照"
|
||
);
|
||
map_match3d_work_profile_response(build_match3d_work_profile_record_with_assets(
|
||
profile,
|
||
&generated_assets,
|
||
))
|
||
});
|
||
let background_image_src = generated_background.image_src.clone().unwrap_or_default();
|
||
let background_image_object_key = generated_background
|
||
.image_object_key
|
||
.clone()
|
||
.unwrap_or_default();
|
||
|
||
Ok(json_success_body(
|
||
Some(&request_context),
|
||
GenerateMatch3DBackgroundImageResponse {
|
||
item,
|
||
background_image_src,
|
||
background_image_object_key,
|
||
generated_background_asset: map_match3d_background_asset_for_work(generated_background),
|
||
prompt,
|
||
},
|
||
))
|
||
}
|
||
|
||
pub async fn generate_match3d_container_image_for_work(
|
||
State(state): State<AppState>,
|
||
Path(profile_id): Path<String>,
|
||
Extension(request_context): Extension<RequestContext>,
|
||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||
payload: Result<Json<GenerateMatch3DContainerImageRequest>, JsonRejection>,
|
||
) -> Result<Json<Value>, Response> {
|
||
let Json(payload) = match3d_json(payload, &request_context, MATCH3D_WORKS_PROVIDER)?;
|
||
ensure_non_empty(
|
||
&request_context,
|
||
MATCH3D_WORKS_PROVIDER,
|
||
&profile_id,
|
||
"profileId",
|
||
)?;
|
||
let prompt = normalize_match3d_background_prompt(payload.prompt.as_str());
|
||
ensure_non_empty(&request_context, MATCH3D_WORKS_PROVIDER, &prompt, "prompt")?;
|
||
let prompt_fingerprint = build_match3d_prompt_fingerprint(prompt.as_str());
|
||
|
||
let context =
|
||
load_match3d_work_asset_context(&state, &request_context, &authenticated, &profile_id)
|
||
.await?;
|
||
let Match3DWorkAssetContext {
|
||
owner_user_id,
|
||
session_id,
|
||
profile,
|
||
config,
|
||
assets,
|
||
} = context;
|
||
let billing_asset_id = format!(
|
||
"{}:{}:{}:container",
|
||
session_id, profile_id, prompt_fingerprint
|
||
);
|
||
let (generated_background, generated_assets) = execute_billable_asset_operation_with_cost(
|
||
&state,
|
||
owner_user_id.as_str(),
|
||
"match3d_ui_container_image",
|
||
billing_asset_id.as_str(),
|
||
MATCH3D_BACKGROUND_IMAGE_POINTS_COST,
|
||
async {
|
||
let generated_container = generate_match3d_container_image(
|
||
&state,
|
||
&request_context,
|
||
owner_user_id.as_str(),
|
||
session_id.as_str(),
|
||
profile_id.as_str(),
|
||
&config,
|
||
prompt.as_str(),
|
||
)
|
||
.await?;
|
||
let mut assets = assets;
|
||
let generated_background =
|
||
merge_match3d_container_image_into_background_asset(&assets, generated_container);
|
||
attach_match3d_background_asset_to_assets(&mut assets, generated_background.clone());
|
||
let save_result = persist_match3d_generated_item_assets_snapshot(
|
||
&state,
|
||
&request_context,
|
||
&authenticated,
|
||
session_id.as_str(),
|
||
owner_user_id.as_str(),
|
||
profile_id.as_str(),
|
||
&assets,
|
||
)
|
||
.await;
|
||
if let Err(response) = save_result {
|
||
tracing::warn!(
|
||
provider = MATCH3D_WORKS_PROVIDER,
|
||
profile_id,
|
||
owner_user_id = %owner_user_id,
|
||
status = %response.status(),
|
||
"抓大鹅容器形象已生成但 SpacetimeDB 草稿写回不可用,降级返回本次生成资产"
|
||
);
|
||
}
|
||
Ok((generated_background, assets))
|
||
},
|
||
)
|
||
.await
|
||
.map_err(|error| match3d_error_response(&request_context, MATCH3D_WORKS_PROVIDER, error))?;
|
||
|
||
let item = state
|
||
.spacetime_client()
|
||
.get_match3d_work_detail(profile_id.clone(), owner_user_id.clone())
|
||
.await
|
||
.map(|item| map_match3d_work_profile_response(item))
|
||
.unwrap_or_else(|error| {
|
||
tracing::warn!(
|
||
provider = MATCH3D_WORKS_PROVIDER,
|
||
profile_id,
|
||
owner_user_id = %owner_user_id,
|
||
error = %error,
|
||
"抓大鹅容器形象生成后读取作品详情失败,降级使用写回前快照"
|
||
);
|
||
map_match3d_work_profile_response(build_match3d_work_profile_record_with_assets(
|
||
profile,
|
||
&generated_assets,
|
||
))
|
||
});
|
||
let container_image_src = generated_background
|
||
.container_image_src
|
||
.clone()
|
||
.unwrap_or_default();
|
||
let container_image_object_key = generated_background
|
||
.container_image_object_key
|
||
.clone()
|
||
.unwrap_or_default();
|
||
|
||
Ok(json_success_body(
|
||
Some(&request_context),
|
||
GenerateMatch3DContainerImageResponse {
|
||
item,
|
||
container_image_src,
|
||
container_image_object_key,
|
||
generated_background_asset: map_match3d_background_asset_for_work(generated_background),
|
||
prompt,
|
||
},
|
||
))
|
||
}
|
||
|
||
pub async fn generate_match3d_item_assets_for_work(
|
||
State(state): State<AppState>,
|
||
Path(profile_id): Path<String>,
|
||
Extension(request_context): Extension<RequestContext>,
|
||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||
payload: Result<Json<GenerateMatch3DItemAssetsRequest>, JsonRejection>,
|
||
) -> Result<Json<Value>, Response> {
|
||
let Json(payload) = match3d_json(payload, &request_context, MATCH3D_WORKS_PROVIDER)?;
|
||
ensure_non_empty(
|
||
&request_context,
|
||
MATCH3D_WORKS_PROVIDER,
|
||
&profile_id,
|
||
"profileId",
|
||
)?;
|
||
let item_names = normalize_match3d_batch_item_names(payload.item_names);
|
||
if item_names.is_empty() {
|
||
return Err(match3d_bad_request(
|
||
&request_context,
|
||
MATCH3D_WORKS_PROVIDER,
|
||
"请填写至少一个物品名称",
|
||
));
|
||
}
|
||
let generation_mode = normalize_match3d_item_assets_generation_mode(payload.mode.as_deref());
|
||
|
||
let context =
|
||
load_match3d_work_asset_context(&state, &request_context, &authenticated, &profile_id)
|
||
.await?;
|
||
let Match3DWorkAssetContext {
|
||
owner_user_id,
|
||
session_id,
|
||
profile,
|
||
config,
|
||
assets,
|
||
} = context;
|
||
let generation_plan =
|
||
build_match3d_item_assets_generation_plan(generation_mode, item_names, &assets);
|
||
if generation_plan.billed_item_count() == 0 {
|
||
return Ok(json_success_body(
|
||
Some(&request_context),
|
||
GenerateMatch3DItemAssetsResponse {
|
||
item: map_match3d_work_profile_response(profile),
|
||
generated_item_assets: sort_match3d_generated_assets(assets)
|
||
.into_iter()
|
||
.map(Match3DGeneratedItemAssetJson::from)
|
||
.map(map_match3d_generated_item_asset_for_work)
|
||
.collect(),
|
||
},
|
||
));
|
||
}
|
||
let billed_item_count = generation_plan.billed_item_count();
|
||
let points_cost = calculate_match3d_item_assets_points_cost(billed_item_count);
|
||
let billing_asset_id = format!(
|
||
"{}:{}:{}:{}",
|
||
session_id,
|
||
profile_id,
|
||
billed_item_count,
|
||
build_match3d_prompt_fingerprint(generation_plan.billing_fingerprint_source().as_str())
|
||
);
|
||
let generated_assets = execute_billable_asset_operation_with_cost(
|
||
&state,
|
||
owner_user_id.as_str(),
|
||
"match3d_item_assets",
|
||
billing_asset_id.as_str(),
|
||
points_cost,
|
||
async {
|
||
append_match3d_item_assets(
|
||
&state,
|
||
&request_context,
|
||
&authenticated,
|
||
owner_user_id.as_str(),
|
||
session_id.as_str(),
|
||
profile_id.as_str(),
|
||
&config,
|
||
generation_plan,
|
||
assets,
|
||
)
|
||
.await
|
||
.map_err(|response| {
|
||
AppError::from_status(response.status()).with_details(json!({
|
||
"provider": MATCH3D_WORKS_PROVIDER,
|
||
"message": "抓大鹅批量新增物品素材失败",
|
||
}))
|
||
})
|
||
},
|
||
)
|
||
.await
|
||
.map_err(|error| match3d_error_response(&request_context, MATCH3D_WORKS_PROVIDER, error))?;
|
||
|
||
let item = state
|
||
.spacetime_client()
|
||
.get_match3d_work_detail(profile_id, owner_user_id)
|
||
.await
|
||
.map_err(|error| {
|
||
match3d_error_response(
|
||
&request_context,
|
||
MATCH3D_WORKS_PROVIDER,
|
||
map_match3d_client_error(error),
|
||
)
|
||
})?;
|
||
Ok(json_success_body(
|
||
Some(&request_context),
|
||
GenerateMatch3DItemAssetsResponse {
|
||
item: map_match3d_work_profile_response(item),
|
||
generated_item_assets: generated_assets
|
||
.into_iter()
|
||
.map(Match3DGeneratedItemAssetJson::from)
|
||
.map(map_match3d_generated_item_asset_for_work)
|
||
.collect(),
|
||
},
|
||
))
|
||
}
|
||
|
||
pub async fn generate_match3d_work_tags(
|
||
State(state): State<AppState>,
|
||
Extension(request_context): Extension<RequestContext>,
|
||
Extension(_authenticated): Extension<AuthenticatedAccessToken>,
|
||
payload: Result<Json<GenerateMatch3DWorkTagsRequest>, JsonRejection>,
|
||
) -> Result<Json<Value>, Response> {
|
||
let Json(payload) = match3d_json(payload, &request_context, MATCH3D_WORKS_PROVIDER)?;
|
||
let tags = generate_match3d_work_tags_for_profile(
|
||
&state,
|
||
payload.game_name.as_str(),
|
||
payload.theme_text.as_str(),
|
||
payload.summary.as_deref(),
|
||
)
|
||
.await;
|
||
|
||
Ok(json_success_body(
|
||
Some(&request_context),
|
||
GenerateMatch3DWorkTagsResponse { tags },
|
||
))
|
||
}
|
||
|
||
pub async fn publish_match3d_work(
|
||
State(state): State<AppState>,
|
||
Path(profile_id): Path<String>,
|
||
Extension(request_context): Extension<RequestContext>,
|
||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||
) -> Result<Json<Value>, Response> {
|
||
ensure_non_empty(
|
||
&request_context,
|
||
MATCH3D_WORKS_PROVIDER,
|
||
&profile_id,
|
||
"profileId",
|
||
)?;
|
||
|
||
let item = state
|
||
.spacetime_client()
|
||
.publish_match3d_work(
|
||
profile_id,
|
||
authenticated.claims().user_id().to_string(),
|
||
current_utc_micros(),
|
||
)
|
||
.await
|
||
.map_err(|error| {
|
||
match3d_error_response(
|
||
&request_context,
|
||
MATCH3D_WORKS_PROVIDER,
|
||
map_match3d_client_error(error),
|
||
)
|
||
})?;
|
||
|
||
Ok(json_success_body(
|
||
Some(&request_context),
|
||
Match3DWorkMutationResponse {
|
||
item: map_match3d_work_profile_response(item),
|
||
},
|
||
))
|
||
}
|
||
|
||
pub async fn delete_match3d_work(
|
||
State(state): State<AppState>,
|
||
Path(profile_id): Path<String>,
|
||
Extension(request_context): Extension<RequestContext>,
|
||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||
) -> Result<Json<Value>, Response> {
|
||
ensure_non_empty(
|
||
&request_context,
|
||
MATCH3D_WORKS_PROVIDER,
|
||
&profile_id,
|
||
"profileId",
|
||
)?;
|
||
|
||
let items = state
|
||
.spacetime_client()
|
||
.delete_match3d_work(profile_id, authenticated.claims().user_id().to_string())
|
||
.await
|
||
.map_err(|error| {
|
||
match3d_error_response(
|
||
&request_context,
|
||
MATCH3D_WORKS_PROVIDER,
|
||
map_match3d_client_error(error),
|
||
)
|
||
})?;
|
||
|
||
Ok(json_success_body(
|
||
Some(&request_context),
|
||
Match3DWorksResponse {
|
||
items: items
|
||
.into_iter()
|
||
.map(map_match3d_work_summary_response)
|
||
.collect(),
|
||
},
|
||
))
|
||
}
|
||
|
||
pub async fn start_match3d_run(
|
||
State(state): State<AppState>,
|
||
Path(profile_id): Path<String>,
|
||
Extension(request_context): Extension<RequestContext>,
|
||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||
payload: Result<Json<StartMatch3DRunRequest>, JsonRejection>,
|
||
) -> Result<Json<Value>, Response> {
|
||
let maybe_payload = payload.ok().map(|Json(payload)| payload);
|
||
let profile_id = maybe_payload
|
||
.as_ref()
|
||
.map(|payload| payload.profile_id.clone())
|
||
.filter(|value| !value.trim().is_empty())
|
||
.unwrap_or(profile_id);
|
||
ensure_non_empty(
|
||
&request_context,
|
||
MATCH3D_RUNTIME_PROVIDER,
|
||
&profile_id,
|
||
"profileId",
|
||
)?;
|
||
|
||
let run = state
|
||
.spacetime_client()
|
||
.start_match3d_run(Match3DRunStartRecordInput {
|
||
run_id: build_prefixed_uuid_id(MATCH3D_RUN_ID_PREFIX),
|
||
owner_user_id: authenticated.claims().user_id().to_string(),
|
||
profile_id: profile_id.clone(),
|
||
started_at_ms: current_utc_ms(),
|
||
item_type_count_override: maybe_payload
|
||
.as_ref()
|
||
.and_then(|payload| payload.item_type_count_override)
|
||
.unwrap_or(0),
|
||
})
|
||
.await
|
||
.map_err(|error| {
|
||
match3d_error_response(
|
||
&request_context,
|
||
MATCH3D_RUNTIME_PROVIDER,
|
||
map_match3d_client_error(error),
|
||
)
|
||
})?;
|
||
|
||
record_work_play_start_after_success(
|
||
&state,
|
||
&request_context,
|
||
WorkPlayTrackingDraft::new(
|
||
"match3d",
|
||
profile_id.clone(),
|
||
&authenticated,
|
||
"/api/runtime/match3d/...",
|
||
)
|
||
.profile_id(profile_id.clone())
|
||
.extra(json!({
|
||
"runId": run.run_id,
|
||
})),
|
||
)
|
||
.await;
|
||
|
||
Ok(json_success_body(
|
||
Some(&request_context),
|
||
Match3DRunResponse {
|
||
run: map_match3d_run_response(run),
|
||
},
|
||
))
|
||
}
|
||
|
||
pub async fn get_match3d_run(
|
||
State(state): State<AppState>,
|
||
Path(run_id): Path<String>,
|
||
Extension(request_context): Extension<RequestContext>,
|
||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||
) -> Result<Json<Value>, Response> {
|
||
ensure_non_empty(&request_context, MATCH3D_RUNTIME_PROVIDER, &run_id, "runId")?;
|
||
|
||
let run = state
|
||
.spacetime_client()
|
||
.get_match3d_run(run_id, authenticated.claims().user_id().to_string())
|
||
.await
|
||
.map_err(|error| {
|
||
match3d_error_response(
|
||
&request_context,
|
||
MATCH3D_RUNTIME_PROVIDER,
|
||
map_match3d_client_error(error),
|
||
)
|
||
})?;
|
||
|
||
Ok(json_success_body(
|
||
Some(&request_context),
|
||
Match3DRunResponse {
|
||
run: map_match3d_run_response(run),
|
||
},
|
||
))
|
||
}
|
||
|
||
pub async fn click_match3d_item(
|
||
State(state): State<AppState>,
|
||
Path(run_id): Path<String>,
|
||
Extension(request_context): Extension<RequestContext>,
|
||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||
payload: Result<Json<ClickMatch3DItemRequest>, JsonRejection>,
|
||
) -> Result<Json<Value>, Response> {
|
||
let Json(payload) = match3d_json(payload, &request_context, MATCH3D_RUNTIME_PROVIDER)?;
|
||
ensure_non_empty(&request_context, MATCH3D_RUNTIME_PROVIDER, &run_id, "runId")?;
|
||
ensure_non_empty(
|
||
&request_context,
|
||
MATCH3D_RUNTIME_PROVIDER,
|
||
&payload.item_instance_id,
|
||
"itemInstanceId",
|
||
)?;
|
||
ensure_non_empty(
|
||
&request_context,
|
||
MATCH3D_RUNTIME_PROVIDER,
|
||
&payload.client_event_id,
|
||
"clientEventId",
|
||
)?;
|
||
|
||
let confirmation = state
|
||
.spacetime_client()
|
||
.click_match3d_item(Match3DRunClickRecordInput {
|
||
run_id: payload.run_id.unwrap_or(run_id),
|
||
owner_user_id: authenticated.claims().user_id().to_string(),
|
||
item_instance_id: payload.item_instance_id,
|
||
client_snapshot_version: payload.client_snapshot_version.min(u32::MAX as u64) as u32,
|
||
client_event_id: payload.client_event_id,
|
||
clicked_at_ms: payload.clicked_at_ms.min(i64::MAX as u64) as i64,
|
||
})
|
||
.await
|
||
.map_err(|error| {
|
||
match3d_error_response(
|
||
&request_context,
|
||
MATCH3D_RUNTIME_PROVIDER,
|
||
map_match3d_client_error(error),
|
||
)
|
||
})?;
|
||
|
||
Ok(json_success_body(
|
||
Some(&request_context),
|
||
Match3DClickResponse {
|
||
confirmation: map_match3d_click_confirmation_response(confirmation),
|
||
},
|
||
))
|
||
}
|
||
|
||
pub async fn stop_match3d_run(
|
||
State(state): State<AppState>,
|
||
Path(run_id): Path<String>,
|
||
Extension(request_context): Extension<RequestContext>,
|
||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||
payload: Result<Json<StopMatch3DRunRequest>, JsonRejection>,
|
||
) -> Result<Json<Value>, Response> {
|
||
let _ = payload.ok();
|
||
ensure_non_empty(&request_context, MATCH3D_RUNTIME_PROVIDER, &run_id, "runId")?;
|
||
|
||
let run = state
|
||
.spacetime_client()
|
||
.stop_match3d_run(Match3DRunStopRecordInput {
|
||
run_id,
|
||
owner_user_id: authenticated.claims().user_id().to_string(),
|
||
stopped_at_ms: current_utc_ms(),
|
||
})
|
||
.await
|
||
.map_err(|error| {
|
||
match3d_error_response(
|
||
&request_context,
|
||
MATCH3D_RUNTIME_PROVIDER,
|
||
map_match3d_client_error(error),
|
||
)
|
||
})?;
|
||
|
||
Ok(json_success_body(
|
||
Some(&request_context),
|
||
Match3DRunResponse {
|
||
run: map_match3d_run_response(run),
|
||
},
|
||
))
|
||
}
|
||
|
||
pub async fn restart_match3d_run(
|
||
State(state): State<AppState>,
|
||
Path(run_id): Path<String>,
|
||
Extension(request_context): Extension<RequestContext>,
|
||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||
) -> Result<Json<Value>, Response> {
|
||
ensure_non_empty(&request_context, MATCH3D_RUNTIME_PROVIDER, &run_id, "runId")?;
|
||
|
||
let run = state
|
||
.spacetime_client()
|
||
.restart_match3d_run(Match3DRunRestartRecordInput {
|
||
source_run_id: run_id,
|
||
next_run_id: build_prefixed_uuid_id(MATCH3D_RUN_ID_PREFIX),
|
||
owner_user_id: authenticated.claims().user_id().to_string(),
|
||
restarted_at_ms: current_utc_ms(),
|
||
})
|
||
.await
|
||
.map_err(|error| {
|
||
match3d_error_response(
|
||
&request_context,
|
||
MATCH3D_RUNTIME_PROVIDER,
|
||
map_match3d_client_error(error),
|
||
)
|
||
})?;
|
||
|
||
Ok(json_success_body(
|
||
Some(&request_context),
|
||
Match3DRunResponse {
|
||
run: map_match3d_run_response(run),
|
||
},
|
||
))
|
||
}
|
||
|
||
pub async fn finish_match3d_time_up(
|
||
State(state): State<AppState>,
|
||
Path(run_id): Path<String>,
|
||
Extension(request_context): Extension<RequestContext>,
|
||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||
) -> Result<Json<Value>, Response> {
|
||
ensure_non_empty(&request_context, MATCH3D_RUNTIME_PROVIDER, &run_id, "runId")?;
|
||
|
||
let run = state
|
||
.spacetime_client()
|
||
.finish_match3d_time_up(Match3DRunTimeUpRecordInput {
|
||
run_id,
|
||
owner_user_id: authenticated.claims().user_id().to_string(),
|
||
finished_at_ms: current_utc_ms(),
|
||
})
|
||
.await
|
||
.map_err(|error| {
|
||
match3d_error_response(
|
||
&request_context,
|
||
MATCH3D_RUNTIME_PROVIDER,
|
||
map_match3d_client_error(error),
|
||
)
|
||
})?;
|
||
|
||
Ok(json_success_body(
|
||
Some(&request_context),
|
||
Match3DRunResponse {
|
||
run: map_match3d_run_response(run),
|
||
},
|
||
))
|
||
}
|