1
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-04-29 23:10:43 +08:00
parent 0395bd7ec6
commit 89ab1bf1c0
20 changed files with 204 additions and 244 deletions

View File

@@ -83,12 +83,11 @@ use crate::{
profile_identity::update_profile_identity,
puzzle::{
advance_local_puzzle_next_level, advance_puzzle_next_level, create_puzzle_agent_session,
delete_puzzle_work, drag_puzzle_piece_or_group, execute_puzzle_agent_action,
get_puzzle_agent_session, get_puzzle_gallery_detail, get_puzzle_run,
get_puzzle_work_detail, get_puzzle_works, list_puzzle_gallery, put_puzzle_work,
remix_puzzle_gallery_work, start_puzzle_run, stream_puzzle_agent_message,
submit_puzzle_agent_message, submit_puzzle_leaderboard, swap_puzzle_pieces,
update_puzzle_run_pause, use_puzzle_runtime_prop,
delete_puzzle_work, execute_puzzle_agent_action, get_puzzle_agent_session,
get_puzzle_gallery_detail, get_puzzle_run, get_puzzle_work_detail, get_puzzle_works,
list_puzzle_gallery, put_puzzle_work, remix_puzzle_gallery_work, start_puzzle_run,
stream_puzzle_agent_message, submit_puzzle_agent_message, submit_puzzle_leaderboard,
swap_puzzle_pieces, update_puzzle_run_pause, use_puzzle_runtime_prop,
},
refresh_session::refresh_session,
request_context::{attach_request_context, resolve_request_id},
@@ -777,13 +776,6 @@ pub fn build_router(state: AppState) -> Router {
require_bearer_auth,
)),
)
.route(
"/api/runtime/puzzle/runs/{run_id}/drag",
post(drag_puzzle_piece_or_group).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/runtime/puzzle/runs/{run_id}/next-level",
post(advance_puzzle_next_level).route_layer(middleware::from_fn_with_state(

View File

@@ -9,10 +9,10 @@ pub(crate) const PUZZLE_DEFAULT_NEGATIVE_PROMPT: &str =
pub(crate) fn build_puzzle_image_prompt(level_name: &str, prompt: &str) -> String {
format!(
concat!(
"请生成一张适合 9:16 竖屏拼图关卡的高清插画。",
"请生成一张适合 1:1 正方形拼图关卡的高清插画。",
"关卡名:{level_name}。",
"画面主体:{prompt}。",
"画面要求:9:16 竖屏画布,适配 3x3 或 4x4 拼图切块,",
"画面要求:1:1 正方形画布,适配 3x3 或 4x4 拼图切块,",
"主体要清晰集中,前中后景层次明确,局部细节丰富但不要杂乱,",
"避免文字、水印、边框和 UI 元素。"
),
@@ -31,7 +31,7 @@ mod tests {
assert!(prompt.contains("雨夜神庙"));
assert!(prompt.contains("猫咪在发光遗迹前寻找线索"));
assert!(prompt.contains("9:16 竖屏拼图关卡"));
assert!(prompt.contains("1:1 正方形拼图关卡"));
assert!(prompt.contains("3x3 或 4x4"));
assert!(prompt.contains("避免文字、水印、边框和 UI 元素"));
}

View File

@@ -36,11 +36,11 @@ use shared_contracts::{
},
puzzle_gallery::{PuzzleGalleryDetailResponse, PuzzleGalleryResponse},
puzzle_runtime::{
AdvanceLocalPuzzleNextLevelRequest, DragPuzzlePieceRequest, PuzzleBoardSnapshotResponse,
PuzzleCellPositionResponse, PuzzleLeaderboardEntryResponse, PuzzleMergedGroupStateResponse,
PuzzlePieceStateResponse, PuzzleRunResponse, PuzzleRunSnapshotResponse,
PuzzleRuntimeLevelSnapshotResponse, StartPuzzleRunRequest, SubmitPuzzleLeaderboardRequest,
SwapPuzzlePiecesRequest, UpdatePuzzleRuntimePauseRequest, UsePuzzleRuntimePropRequest,
AdvanceLocalPuzzleNextLevelRequest, PuzzleBoardSnapshotResponse, PuzzleCellPositionResponse,
PuzzleLeaderboardEntryResponse, PuzzleMergedGroupStateResponse, PuzzlePieceStateResponse,
PuzzleRunResponse, PuzzleRunSnapshotResponse, PuzzleRuntimeLevelSnapshotResponse,
StartPuzzleRunRequest, SubmitPuzzleLeaderboardRequest, SwapPuzzlePiecesRequest,
UpdatePuzzleRuntimePauseRequest, UsePuzzleRuntimePropRequest,
},
puzzle_works::{
PutPuzzleWorkRequest, PuzzleWorkDetailResponse, PuzzleWorkMutationResponse,
@@ -57,10 +57,10 @@ use spacetime_client::{
PuzzleLeaderboardEntryRecord, PuzzleLeaderboardSubmitRecordInput, PuzzleMergedGroupRecord,
PuzzlePieceStateRecord, PuzzlePublishRecordInput, PuzzleResultDraftRecord,
PuzzleResultPreviewBlockerRecord, PuzzleResultPreviewFindingRecord, PuzzleResultPreviewRecord,
PuzzleRunDragRecordInput, PuzzleRunPauseRecordInput, PuzzleRunPropRecordInput, PuzzleRunRecord,
PuzzleRunStartRecordInput, PuzzleRunSwapRecordInput, PuzzleRuntimeLevelRecord,
PuzzleSelectCoverImageRecordInput, PuzzleWorkProfileRecord, PuzzleWorkRemixRecordInput,
PuzzleWorkUpsertRecordInput, SpacetimeClientError,
PuzzleRunPauseRecordInput, PuzzleRunPropRecordInput, PuzzleRunRecord, PuzzleRunStartRecordInput,
PuzzleRunSwapRecordInput, PuzzleRuntimeLevelRecord, PuzzleSelectCoverImageRecordInput,
PuzzleWorkProfileRecord, PuzzleWorkRemixRecordInput, PuzzleWorkUpsertRecordInput,
SpacetimeClientError,
};
use std::convert::Infallible;
use tokio::time::sleep;
@@ -86,7 +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";
const PUZZLE_GENERATED_IMAGE_SIZE: &str = "1024*1024";
pub async fn create_puzzle_agent_session(
State(state): State<AppState>,
@@ -1062,58 +1062,6 @@ pub async fn swap_puzzle_pieces(
))
}
pub async fn drag_puzzle_piece_or_group(
State(state): State<AppState>,
AxumPath(run_id): AxumPath<String>,
Extension(request_context): Extension<RequestContext>,
Extension(authenticated): Extension<AuthenticatedAccessToken>,
payload: Result<Json<DragPuzzlePieceRequest>, 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.piece_id,
"pieceId",
)?;
let run = state
.spacetime_client()
.drag_puzzle_piece_or_group(PuzzleRunDragRecordInput {
run_id,
owner_user_id: authenticated.claims().user_id().to_string(),
piece_id: payload.piece_id,
target_row: payload.target_row,
target_col: payload.target_col,
dragged_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 advance_puzzle_next_level(
State(state): State<AppState>,
AxumPath(run_id): AxumPath<String>,
@@ -1994,7 +1942,7 @@ async fn generate_puzzle_image_candidates(
None => None,
};
// 中文注释SpacetimeDB reducer 不能做外部 I/O参考图读取与 DashScope 图生图都必须停留在 api-server。
// 中文注释:拼图作品资产统一按 9:16 竖屏生成,运行时棋盘也按同一比例切块承载。
// 中文注释:拼图作品资产统一按 1:1 正方形生成,前端运行时也按正方形棋盘切块承载。
let generated = match reference_image.as_deref() {
Some(reference_image) => {
create_puzzle_image_to_image_generation(
@@ -2411,8 +2359,8 @@ mod tests {
}
#[test]
fn puzzle_generated_image_size_is_portrait_9_16() {
assert_eq!(PUZZLE_GENERATED_IMAGE_SIZE, "720*1280");
fn puzzle_generated_image_size_is_square_1_1() {
assert_eq!(PUZZLE_GENERATED_IMAGE_SIZE, "1024*1024");
}
}

View File

@@ -1551,9 +1551,6 @@ fn submit_puzzle_leaderboard_entry_tx(
.current_level
.as_ref()
.ok_or_else(|| "拼图关卡不存在".to_string())?;
if current_level.status != PuzzleRuntimeLevelStatus::Cleared {
return Err("当前关卡尚未通关".to_string());
}
if current_level.profile_id != input.profile_id {
return Err("提交成绩的拼图作品与当前关卡不匹配".to_string());
}
@@ -1592,9 +1589,14 @@ fn submit_puzzle_leaderboard_entry_tx(
10,
);
if let Some(level) = run.current_level.as_mut() {
// 拼图拖动、合并与通关判定由前端运行态即时裁决;服务端只负责真实榜单。
// 因此提交榜单时不能要求 SpacetimeDB 里的旧棋盘快照也已经通关。
level.status = PuzzleRuntimeLevelStatus::Cleared;
level.cleared_at_ms = Some(micros_to_millis(input.submitted_at_micros));
level.elapsed_ms = Some(input.elapsed_ms.max(1_000));
level.leaderboard_entries = leaderboard_entries.clone();
}
run.cleared_level_count = run.cleared_level_count.max(run.current_level_index);
run.leaderboard_entries = leaderboard_entries;
replace_puzzle_runtime_run(ctx, &row, &run, input.submitted_at_micros);
Ok(run)