1
This commit is contained in:
@@ -17,7 +17,7 @@ use module_assets::{
|
||||
AssetObjectAccessPolicy, AssetObjectFieldError, build_asset_entity_binding_input,
|
||||
build_asset_object_upsert_input, generate_asset_binding_id, generate_asset_object_id,
|
||||
};
|
||||
use module_puzzle::{PuzzleBoardSnapshot, PuzzleGeneratedImageCandidate};
|
||||
use module_puzzle::{PuzzleBoardSnapshot, PuzzleGeneratedImageCandidate, PuzzleRuntimeLevelStatus};
|
||||
use platform_oss::{
|
||||
LegacyAssetPrefix, OssHeadObjectRequest, OssObjectAccess, OssPutObjectRequest,
|
||||
OssSignedGetObjectUrlRequest,
|
||||
@@ -40,7 +40,7 @@ use shared_contracts::{
|
||||
PuzzleCellPositionResponse, PuzzleLeaderboardEntryResponse, PuzzleMergedGroupStateResponse,
|
||||
PuzzlePieceStateResponse, PuzzleRunResponse, PuzzleRunSnapshotResponse,
|
||||
PuzzleRuntimeLevelSnapshotResponse, StartPuzzleRunRequest, SubmitPuzzleLeaderboardRequest,
|
||||
SwapPuzzlePiecesRequest,
|
||||
SwapPuzzlePiecesRequest, UpdatePuzzleRuntimePauseRequest, UsePuzzleRuntimePropRequest,
|
||||
},
|
||||
puzzle_works::{
|
||||
PutPuzzleWorkRequest, PuzzleWorkDetailResponse, PuzzleWorkMutationResponse,
|
||||
@@ -57,9 +57,10 @@ use spacetime_client::{
|
||||
PuzzleLeaderboardEntryRecord, PuzzleLeaderboardSubmitRecordInput, PuzzleMergedGroupRecord,
|
||||
PuzzlePieceStateRecord, PuzzlePublishRecordInput, PuzzleResultDraftRecord,
|
||||
PuzzleResultPreviewBlockerRecord, PuzzleResultPreviewFindingRecord, PuzzleResultPreviewRecord,
|
||||
PuzzleRunDragRecordInput, PuzzleRunRecord, PuzzleRunStartRecordInput, PuzzleRunSwapRecordInput,
|
||||
PuzzleRuntimeLevelRecord, PuzzleSelectCoverImageRecordInput, PuzzleWorkProfileRecord,
|
||||
PuzzleWorkRemixRecordInput, PuzzleWorkUpsertRecordInput, SpacetimeClientError,
|
||||
PuzzleRunDragRecordInput, PuzzleRunPauseRecordInput, PuzzleRunPropRecordInput, PuzzleRunRecord,
|
||||
PuzzleRunStartRecordInput, PuzzleRunSwapRecordInput, PuzzleRuntimeLevelRecord,
|
||||
PuzzleSelectCoverImageRecordInput, PuzzleWorkProfileRecord, PuzzleWorkRemixRecordInput,
|
||||
PuzzleWorkUpsertRecordInput, SpacetimeClientError,
|
||||
};
|
||||
use std::convert::Infallible;
|
||||
use tokio::time::sleep;
|
||||
@@ -85,6 +86,7 @@ const PUZZLE_GALLERY_PROVIDER: &str = "puzzle-gallery";
|
||||
const PUZZLE_RUNTIME_PROVIDER: &str = "puzzle-runtime";
|
||||
const PUZZLE_TEXT_TO_IMAGE_MODEL: &str = "wan2.2-t2i-flash";
|
||||
const PUZZLE_ENTITY_KIND: &str = "puzzle_work";
|
||||
const PUZZLE_GENERATED_IMAGE_SIZE: &str = "720*1280";
|
||||
|
||||
pub async fn create_puzzle_agent_session(
|
||||
State(state): State<AppState>,
|
||||
@@ -103,7 +105,7 @@ pub async fn create_puzzle_agent_session(
|
||||
)
|
||||
})?;
|
||||
|
||||
let seed_text = payload.seed_text.unwrap_or_default().trim().to_string();
|
||||
let seed_text = build_puzzle_form_seed_text(&payload);
|
||||
let session = state
|
||||
.spacetime_client()
|
||||
.create_puzzle_agent_session(PuzzleAgentSessionCreateRecordInput {
|
||||
@@ -455,6 +457,8 @@ pub async fn execute_puzzle_agent_action(
|
||||
&state,
|
||||
session_id.clone(),
|
||||
owner_user_id.clone(),
|
||||
payload.prompt_text.as_deref(),
|
||||
payload.reference_image_src.as_deref(),
|
||||
now,
|
||||
)
|
||||
.await
|
||||
@@ -1142,6 +1146,120 @@ pub async fn advance_puzzle_next_level(
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn update_puzzle_run_pause(
|
||||
State(state): State<AppState>,
|
||||
AxumPath(run_id): AxumPath<String>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
payload: Result<Json<UpdatePuzzleRuntimePauseRequest>, JsonRejection>,
|
||||
) -> Result<Json<Value>, Response> {
|
||||
let Json(payload) = payload.map_err(|error| {
|
||||
puzzle_error_response(
|
||||
&request_context,
|
||||
PUZZLE_RUNTIME_PROVIDER,
|
||||
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||||
"provider": PUZZLE_RUNTIME_PROVIDER,
|
||||
"message": error.body_text(),
|
||||
})),
|
||||
)
|
||||
})?;
|
||||
ensure_non_empty(&request_context, PUZZLE_RUNTIME_PROVIDER, &run_id, "runId")?;
|
||||
|
||||
let run = state
|
||||
.spacetime_client()
|
||||
.update_puzzle_run_pause(PuzzleRunPauseRecordInput {
|
||||
run_id,
|
||||
owner_user_id: authenticated.claims().user_id().to_string(),
|
||||
paused: payload.paused,
|
||||
updated_at_micros: current_utc_micros(),
|
||||
})
|
||||
.await
|
||||
.map_err(|error| {
|
||||
puzzle_error_response(
|
||||
&request_context,
|
||||
PUZZLE_RUNTIME_PROVIDER,
|
||||
map_puzzle_client_error(error),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(json_success_body(
|
||||
Some(&request_context),
|
||||
PuzzleRunResponse {
|
||||
run: map_puzzle_run_response(run),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn use_puzzle_runtime_prop(
|
||||
State(state): State<AppState>,
|
||||
AxumPath(run_id): AxumPath<String>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
payload: Result<Json<UsePuzzleRuntimePropRequest>, JsonRejection>,
|
||||
) -> Result<Json<Value>, Response> {
|
||||
let Json(payload) = payload.map_err(|error| {
|
||||
puzzle_error_response(
|
||||
&request_context,
|
||||
PUZZLE_RUNTIME_PROVIDER,
|
||||
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||||
"provider": PUZZLE_RUNTIME_PROVIDER,
|
||||
"message": error.body_text(),
|
||||
})),
|
||||
)
|
||||
})?;
|
||||
ensure_non_empty(&request_context, PUZZLE_RUNTIME_PROVIDER, &run_id, "runId")?;
|
||||
ensure_non_empty(
|
||||
&request_context,
|
||||
PUZZLE_RUNTIME_PROVIDER,
|
||||
&payload.prop_kind,
|
||||
"propKind",
|
||||
)?;
|
||||
|
||||
let owner_user_id = authenticated.claims().user_id().to_string();
|
||||
let prop_kind = payload.prop_kind.trim().to_string();
|
||||
let billing_asset_kind = match prop_kind.as_str() {
|
||||
"hint" => "puzzle_prop_hint",
|
||||
"reference" => "puzzle_prop_preview",
|
||||
"freezeTime" | "freeze_time" => "puzzle_prop_freeze_time",
|
||||
_ => {
|
||||
return Err(puzzle_bad_request(
|
||||
&request_context,
|
||||
PUZZLE_RUNTIME_PROVIDER,
|
||||
"unknown puzzle prop kind",
|
||||
));
|
||||
}
|
||||
};
|
||||
let billing_asset_id = format!("{}:{}:{}", run_id, prop_kind, current_utc_micros());
|
||||
let reducer_owner_user_id = owner_user_id.clone();
|
||||
let run = execute_billable_asset_operation(
|
||||
&state,
|
||||
&owner_user_id,
|
||||
billing_asset_kind,
|
||||
billing_asset_id.as_str(),
|
||||
async {
|
||||
state
|
||||
.spacetime_client()
|
||||
.use_puzzle_runtime_prop(PuzzleRunPropRecordInput {
|
||||
run_id,
|
||||
owner_user_id: reducer_owner_user_id,
|
||||
prop_kind,
|
||||
used_at_micros: current_utc_micros(),
|
||||
})
|
||||
.await
|
||||
.map_err(map_puzzle_client_error)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(|error| puzzle_error_response(&request_context, PUZZLE_RUNTIME_PROVIDER, error))?;
|
||||
|
||||
Ok(json_success_body(
|
||||
Some(&request_context),
|
||||
PuzzleRunResponse {
|
||||
run: map_puzzle_run_response(run),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn advance_local_puzzle_next_level(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
@@ -1399,6 +1517,7 @@ fn map_puzzle_work_summary_response(item: PuzzleWorkProfileRecord) -> PuzzleWork
|
||||
play_count: item.play_count,
|
||||
remix_count: item.remix_count,
|
||||
like_count: item.like_count,
|
||||
recent_play_count_7d: item.recent_play_count_7d,
|
||||
publish_ready: item.publish_ready,
|
||||
}
|
||||
}
|
||||
@@ -1465,6 +1584,13 @@ fn map_puzzle_level_request_record(
|
||||
started_at_ms: level.started_at_ms,
|
||||
cleared_at_ms: level.cleared_at_ms,
|
||||
elapsed_ms: level.elapsed_ms,
|
||||
time_limit_ms: level.time_limit_ms,
|
||||
remaining_ms: level.remaining_ms,
|
||||
paused_accumulated_ms: level.paused_accumulated_ms,
|
||||
pause_started_at_ms: level.pause_started_at_ms,
|
||||
freeze_accumulated_ms: level.freeze_accumulated_ms,
|
||||
freeze_started_at_ms: level.freeze_started_at_ms,
|
||||
freeze_until_ms: level.freeze_until_ms,
|
||||
leaderboard_entries: level
|
||||
.leaderboard_entries
|
||||
.into_iter()
|
||||
@@ -1524,6 +1650,18 @@ fn map_puzzle_board_request_record(board: PuzzleBoardSnapshotResponse) -> Puzzle
|
||||
fn map_puzzle_runtime_level_response(
|
||||
level: spacetime_client::PuzzleRuntimeLevelRecord,
|
||||
) -> PuzzleRuntimeLevelSnapshotResponse {
|
||||
let timer_defaults = build_puzzle_runtime_timer_response_defaults(level.grid_size);
|
||||
let time_limit_ms = if level.time_limit_ms == 0 {
|
||||
timer_defaults.time_limit_ms
|
||||
} else {
|
||||
level.time_limit_ms
|
||||
};
|
||||
let remaining_ms =
|
||||
if level.remaining_ms == 0 && level.status == PuzzleRuntimeLevelStatus::Playing.as_str() {
|
||||
time_limit_ms
|
||||
} else {
|
||||
level.remaining_ms.min(time_limit_ms)
|
||||
};
|
||||
PuzzleRuntimeLevelSnapshotResponse {
|
||||
run_id: level.run_id,
|
||||
level_index: level.level_index,
|
||||
@@ -1538,6 +1676,13 @@ fn map_puzzle_runtime_level_response(
|
||||
started_at_ms: level.started_at_ms,
|
||||
cleared_at_ms: level.cleared_at_ms,
|
||||
elapsed_ms: level.elapsed_ms,
|
||||
time_limit_ms,
|
||||
remaining_ms,
|
||||
paused_accumulated_ms: level.paused_accumulated_ms,
|
||||
pause_started_at_ms: level.pause_started_at_ms,
|
||||
freeze_accumulated_ms: level.freeze_accumulated_ms,
|
||||
freeze_started_at_ms: level.freeze_started_at_ms,
|
||||
freeze_until_ms: level.freeze_until_ms,
|
||||
leaderboard_entries: level
|
||||
.leaderboard_entries
|
||||
.into_iter()
|
||||
@@ -1546,6 +1691,17 @@ fn map_puzzle_runtime_level_response(
|
||||
}
|
||||
}
|
||||
|
||||
struct PuzzleRuntimeTimerResponseDefaults {
|
||||
time_limit_ms: u64,
|
||||
}
|
||||
|
||||
fn build_puzzle_runtime_timer_response_defaults(
|
||||
grid_size: u32,
|
||||
) -> PuzzleRuntimeTimerResponseDefaults {
|
||||
let time_limit_ms = module_puzzle::resolve_puzzle_level_time_limit_ms(grid_size);
|
||||
PuzzleRuntimeTimerResponseDefaults { time_limit_ms }
|
||||
}
|
||||
|
||||
fn map_puzzle_leaderboard_entry_response(
|
||||
entry: PuzzleLeaderboardEntryRecord,
|
||||
) -> PuzzleLeaderboardEntryResponse {
|
||||
@@ -1612,10 +1768,28 @@ fn resolve_author_display_name(
|
||||
|
||||
fn build_puzzle_welcome_text(seed_text: &str) -> String {
|
||||
if seed_text.trim().is_empty() {
|
||||
return "先告诉我你想做一张什么样的拼图图像,我会帮你收束成可发布的题材锚点。".to_string();
|
||||
return "拼图创作信息已准备好。".to_string();
|
||||
}
|
||||
|
||||
"我先接住你的画面灵感,再一起把它收束成正式拼图关卡。".to_string()
|
||||
"拼图创作信息已准备好。".to_string()
|
||||
}
|
||||
|
||||
fn build_puzzle_form_seed_text(payload: &CreatePuzzleAgentSessionRequest) -> String {
|
||||
let title = payload.seed_text.as_deref().unwrap_or_default().trim();
|
||||
let picture_description = payload
|
||||
.picture_description
|
||||
.as_deref()
|
||||
.unwrap_or_default()
|
||||
.trim();
|
||||
|
||||
if title.is_empty() && picture_description.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
if title.is_empty() || picture_description.is_empty() {
|
||||
return format!("{title}{picture_description}");
|
||||
}
|
||||
|
||||
format!("拼图标题:{title}\n画面描述:{picture_description}")
|
||||
}
|
||||
|
||||
fn build_stable_puzzle_work_ids(session_id: &str) -> (String, String) {
|
||||
@@ -1632,6 +1806,8 @@ async fn compile_puzzle_draft_with_initial_cover(
|
||||
state: &AppState,
|
||||
session_id: String,
|
||||
owner_user_id: String,
|
||||
prompt_text: Option<&str>,
|
||||
reference_image_src: Option<&str>,
|
||||
now: i64,
|
||||
) -> Result<PuzzleAgentSessionRecord, SpacetimeClientError> {
|
||||
let compiled_session = state
|
||||
@@ -1648,8 +1824,11 @@ async fn compile_puzzle_draft_with_initial_cover(
|
||||
owner_user_id.as_str(),
|
||||
&compiled_session.session_id,
|
||||
&draft.level_name,
|
||||
&draft.summary,
|
||||
None,
|
||||
prompt_text
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or(draft.summary.as_str()),
|
||||
reference_image_src,
|
||||
1,
|
||||
draft.candidates.len(),
|
||||
)
|
||||
@@ -1815,6 +1994,7 @@ async fn generate_puzzle_image_candidates(
|
||||
None => None,
|
||||
};
|
||||
// 中文注释:SpacetimeDB reducer 不能做外部 I/O,参考图读取与 DashScope 图生图都必须停留在 api-server。
|
||||
// 中文注释:拼图作品资产统一按 9:16 竖屏生成,运行时棋盘也按同一比例切块承载。
|
||||
let generated = match reference_image.as_deref() {
|
||||
Some(reference_image) => {
|
||||
create_puzzle_image_to_image_generation(
|
||||
@@ -1822,7 +2002,7 @@ async fn generate_puzzle_image_candidates(
|
||||
&settings,
|
||||
actual_prompt.as_str(),
|
||||
PUZZLE_DEFAULT_NEGATIVE_PROMPT,
|
||||
"1024*1024",
|
||||
PUZZLE_GENERATED_IMAGE_SIZE,
|
||||
count,
|
||||
reference_image,
|
||||
)
|
||||
@@ -1834,7 +2014,7 @@ async fn generate_puzzle_image_candidates(
|
||||
&settings,
|
||||
actual_prompt.as_str(),
|
||||
PUZZLE_DEFAULT_NEGATIVE_PROMPT,
|
||||
"1024*1024",
|
||||
PUZZLE_GENERATED_IMAGE_SIZE,
|
||||
count,
|
||||
)
|
||||
.await
|
||||
@@ -2079,6 +2259,7 @@ fn build_next_run_from_parts(
|
||||
) -> PuzzleRunRecord {
|
||||
let next_level_index = run.current_level_index + 1;
|
||||
let grid_size = if run.cleared_level_count >= 3 { 4 } else { 3 };
|
||||
let time_limit_ms = module_puzzle::resolve_puzzle_level_time_limit_ms(grid_size);
|
||||
let mut played_profile_ids = run.played_profile_ids.clone();
|
||||
if !played_profile_ids.contains(&profile_id) {
|
||||
played_profile_ids.push(profile_id.clone());
|
||||
@@ -2106,6 +2287,13 @@ fn build_next_run_from_parts(
|
||||
started_at_ms: (current_utc_micros().max(0) as u64) / 1_000,
|
||||
cleared_at_ms: None,
|
||||
elapsed_ms: None,
|
||||
time_limit_ms,
|
||||
remaining_ms: time_limit_ms,
|
||||
paused_accumulated_ms: 0,
|
||||
pause_started_at_ms: None,
|
||||
freeze_accumulated_ms: 0,
|
||||
freeze_started_at_ms: None,
|
||||
freeze_until_ms: None,
|
||||
leaderboard_entries: Vec::new(),
|
||||
}),
|
||||
recommended_next_profile_id: None,
|
||||
@@ -2221,6 +2409,11 @@ mod tests {
|
||||
assert!(!has_original_neighbor_pair(&second));
|
||||
assert!(!has_original_neighbor_pair(&third));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn puzzle_generated_image_size_is_portrait_9_16() {
|
||||
assert_eq!(PUZZLE_GENERATED_IMAGE_SIZE, "720*1280");
|
||||
}
|
||||
}
|
||||
|
||||
struct PuzzleDashScopeSettings {
|
||||
|
||||
Reference in New Issue
Block a user