Extend square-hole creation flow with visual asset timeout guard

This commit is contained in:
kdletters
2026-05-05 15:27:09 +08:00
parent 2252afb080
commit 60b667a9d1
30 changed files with 2838 additions and 215 deletions

View File

@@ -10,12 +10,17 @@ use module_square_hole::{
SquareHoleDropFeedback as DomainSquareHoleDropFeedback,
SquareHoleDropInput as DomainSquareHoleDropInput,
SquareHoleDropRejectReason as DomainSquareHoleDropRejectReason,
SquareHoleHoleOption as DomainSquareHoleHoleOption,
SquareHoleHoleSnapshot as DomainSquareHoleHoleSnapshot,
SquareHoleRunSnapshot as DomainSquareHoleRunSnapshot,
SquareHoleRunStatus as DomainSquareHoleRunStatus,
SquareHoleShapeOption as DomainSquareHoleShapeOption,
SquareHoleShapeSnapshot as DomainSquareHoleShapeSnapshot,
build_creator_config as build_domain_creator_config,
compile_result_draft as compile_domain_result_draft, confirm_drop_at as confirm_domain_drop_at,
default_background_prompt as default_domain_background_prompt,
normalize_hole_options as normalize_domain_hole_options,
normalize_shape_options as normalize_domain_shape_options,
resolve_run_timer_at as resolve_domain_run_timer_at, start_run_at as start_domain_run_at,
stop_run_at as stop_domain_run_at,
};
@@ -432,6 +437,9 @@ fn compile_square_hole_draft_tx(
domain_draft.blockers = module_square_hole::validate_publish_requirements(&domain_draft);
domain_draft.publish_ready = domain_draft.blockers.is_empty();
let cover_image_src = clean_optional(&input.cover_image_src)
.or_else(|| clean_optional(&Some(config.cover_image_src.clone())))
.unwrap_or_default();
let draft = SquareHoleDraftSnapshot {
profile_id: input.profile_id.clone(),
game_name: domain_draft.game_name.clone(),
@@ -439,6 +447,14 @@ fn compile_square_hole_draft_tx(
twist_rule: domain_draft.twist_rule.clone(),
summary_text: domain_draft.summary.clone(),
tags: domain_draft.tags.clone(),
cover_image_src: cover_image_src.clone(),
background_prompt: domain_draft.background_prompt.clone(),
background_image_src: domain_draft
.background_image_src
.clone()
.unwrap_or_default(),
shape_options: shape_options_to_snapshot(&domain_draft.shape_options),
hole_options: hole_options_to_snapshot(&domain_draft.hole_options),
shape_count: domain_draft.shape_count,
difficulty: domain_draft.difficulty,
};
@@ -454,7 +470,7 @@ fn compile_square_hole_draft_tx(
twist_rule: config.twist_rule.clone(),
summary_text: draft.summary_text.clone(),
tags_json: to_json_string(&draft.tags),
cover_image_src: clean_optional(&input.cover_image_src).unwrap_or_default(),
cover_image_src,
shape_count: config.shape_count,
difficulty: config.difficulty,
config_json: to_json_string(&config),
@@ -493,12 +509,31 @@ fn update_square_hole_work_tx(
) -> Result<SquareHoleWorkSnapshot, String> {
let current = find_owned_work(ctx, &input.profile_id, &input.owner_user_id)?;
let tags = parse_tags(&input.tags_json)?;
let config = SquareHoleCreatorConfigSnapshot {
let current_config = parse_config(&current.config_json)?;
let shape_options = parse_optional_json::<Vec<SquareHoleShapeOptionSnapshot>>(
input.shape_options_json.as_str(),
"square_hole shape_options_json",
)?
.unwrap_or_else(|| current_config.shape_options.clone());
let hole_options = parse_optional_json::<Vec<SquareHoleHoleOptionSnapshot>>(
input.hole_options_json.as_str(),
"square_hole hole_options_json",
)?
.unwrap_or_else(|| current_config.hole_options.clone());
let config = normalize_config(SquareHoleCreatorConfigSnapshot {
theme_text: clean_string(&input.theme_text, "玩具"),
twist_rule: clean_string(&input.twist_rule, "方洞万能"),
shape_count: input.shape_count,
difficulty: input.difficulty,
};
shape_options,
hole_options,
background_prompt: clean_string(
&input.background_prompt,
&default_domain_background_prompt(&input.theme_text),
),
cover_image_src: input.cover_image_src.trim().to_string(),
background_image_src: input.background_image_src.trim().to_string(),
});
validate_config(&config)?;
let updated_at = Timestamp::from_micros_since_unix_epoch(input.updated_at_micros);
let next = SquareHoleWorkProfileRow {
@@ -828,6 +863,10 @@ fn build_work_snapshot(row: &SquareHoleWorkProfileRow) -> Result<SquareHoleWorkS
summary_text: row.summary_text.clone(),
tags: parse_tags(&row.tags_json)?,
cover_image_src: row.cover_image_src.clone(),
background_prompt: config.background_prompt.clone(),
background_image_src: config.background_image_src.clone(),
shape_options: config.shape_options.clone(),
hole_options: config.hole_options.clone(),
shape_count: row.shape_count,
difficulty: row.difficulty,
config,
@@ -1040,12 +1079,17 @@ fn is_work_publish_ready(row: &SquareHoleWorkProfileRow) -> bool {
}
fn default_config_from_seed(seed_text: &str) -> SquareHoleCreatorConfigSnapshot {
SquareHoleCreatorConfigSnapshot {
normalize_config(SquareHoleCreatorConfigSnapshot {
theme_text: clean_string(seed_text, "玩具"),
twist_rule: "方洞万能".to_string(),
shape_count: 12,
difficulty: 4,
}
shape_options: Vec::new(),
hole_options: Vec::new(),
background_prompt: String::new(),
cover_image_src: String::new(),
background_image_src: String::new(),
})
}
fn parse_config_or_default(value: &str) -> SquareHoleCreatorConfigSnapshot {
@@ -1053,17 +1097,34 @@ fn parse_config_or_default(value: &str) -> SquareHoleCreatorConfigSnapshot {
}
fn parse_config(value: &str) -> Result<SquareHoleCreatorConfigSnapshot, String> {
parse_json(value, "square_hole config_json").map(
|mut config: SquareHoleCreatorConfigSnapshot| {
config.theme_text = clean_string(&config.theme_text, "玩具");
config.twist_rule = clean_string(&config.twist_rule, "方洞万能");
config.difficulty = config.difficulty.clamp(
module_square_hole::SQUARE_HOLE_MIN_DIFFICULTY,
module_square_hole::SQUARE_HOLE_MAX_DIFFICULTY,
);
config
},
)
parse_json(value, "square_hole config_json").map(normalize_config)
}
fn normalize_config(
mut config: SquareHoleCreatorConfigSnapshot,
) -> SquareHoleCreatorConfigSnapshot {
config.theme_text = clean_string(&config.theme_text, "玩具");
config.twist_rule = clean_string(&config.twist_rule, "方洞万能");
config.difficulty = config.difficulty.clamp(
module_square_hole::SQUARE_HOLE_MIN_DIFFICULTY,
module_square_hole::SQUARE_HOLE_MAX_DIFFICULTY,
);
config.background_prompt = clean_string(
&config.background_prompt,
&default_domain_background_prompt(&config.theme_text),
);
config.cover_image_src = config.cover_image_src.trim().to_string();
config.background_image_src = config.background_image_src.trim().to_string();
let shape_options = normalize_domain_shape_options(
domain_shape_options_from_snapshot(&config.shape_options),
&config.theme_text,
);
let hole_options =
normalize_domain_hole_options(domain_hole_options_from_snapshot(&config.hole_options));
config.shape_options = shape_options_to_snapshot(&shape_options);
config.hole_options = hole_options_to_snapshot(&hole_options);
config
}
fn parse_tags(value: &str) -> Result<Vec<String>, String> {
@@ -1071,6 +1132,13 @@ fn parse_tags(value: &str) -> Result<Vec<String>, String> {
Ok(normalize_tags(parsed))
}
fn parse_optional_json<T: DeserializeOwned>(value: &str, label: &str) -> Result<Option<T>, String> {
if value.trim().is_empty() {
return Ok(None);
}
parse_json(value, label).map(Some)
}
fn normalize_tags(tags: Vec<String>) -> Vec<String> {
let mut result = Vec::new();
for tag in tags {
@@ -1097,12 +1165,18 @@ fn normalize_stage(value: &str) -> String {
fn domain_config_from_snapshot(
config: &SquareHoleCreatorConfigSnapshot,
) -> Result<DomainSquareHoleCreatorConfig, module_square_hole::SquareHoleError> {
build_domain_creator_config(
let mut domain = build_domain_creator_config(
&config.theme_text,
&config.twist_rule,
config.shape_count,
config.difficulty,
)
)?;
domain.shape_options = domain_shape_options_from_snapshot(&config.shape_options);
domain.hole_options = domain_hole_options_from_snapshot(&config.hole_options);
domain.background_prompt = config.background_prompt.clone();
domain.cover_image_src = empty_to_none(&config.cover_image_src);
domain.background_image_src = empty_to_none(&config.background_image_src);
Ok(domain)
}
fn snapshot_from_domain(
@@ -1125,6 +1199,8 @@ fn snapshot_from_domain(
best_combo: run.best_combo,
score: run.score,
rule_label: run.rule_label.clone(),
background_image_src: run.background_image_src.clone().unwrap_or_default(),
shape_options: shape_options_to_snapshot(&run.shape_options),
current_shape: run.current_shape.as_ref().map(shape_from_domain),
holes: run.holes.iter().map(hole_from_domain).collect(),
last_feedback: run.last_feedback.as_ref().map(feedback_from_domain),
@@ -1150,6 +1226,8 @@ fn domain_snapshot_from_snapshot(
best_combo: snapshot.best_combo,
score: snapshot.score,
rule_label: snapshot.rule_label.clone(),
background_image_src: empty_to_none(&snapshot.background_image_src),
shape_options: domain_shape_options_from_snapshot(&snapshot.shape_options),
current_shape: snapshot
.current_shape
.as_ref()
@@ -1172,6 +1250,7 @@ fn shape_from_domain(shape: &DomainSquareHoleShapeSnapshot) -> SquareHoleShapeSn
shape_kind: shape.shape_kind.clone(),
label: shape.label.clone(),
color: shape.color.clone(),
image_src: shape.image_src.clone().unwrap_or_default(),
}
}
@@ -1181,6 +1260,7 @@ fn domain_shape_from_snapshot(shape: &SquareHoleShapeSnapshot) -> DomainSquareHo
shape_kind: shape.shape_kind.clone(),
label: shape.label.clone(),
color: shape.color.clone(),
image_src: empty_to_none(&shape.image_src),
}
}
@@ -1191,6 +1271,7 @@ fn hole_from_domain(hole: &DomainSquareHoleHoleSnapshot) -> SquareHoleHoleSnapsh
label: hole.label.clone(),
x: hole.x,
y: hole.y,
bonus: hole.bonus,
}
}
@@ -1201,9 +1282,68 @@ fn domain_hole_from_snapshot(hole: &SquareHoleHoleSnapshot) -> DomainSquareHoleH
label: hole.label.clone(),
x: hole.x,
y: hole.y,
bonus: hole.bonus,
}
}
fn shape_options_to_snapshot(
options: &[DomainSquareHoleShapeOption],
) -> Vec<SquareHoleShapeOptionSnapshot> {
options
.iter()
.map(|option| SquareHoleShapeOptionSnapshot {
option_id: option.option_id.clone(),
shape_kind: option.shape_kind.clone(),
label: option.label.clone(),
image_prompt: option.image_prompt.clone(),
image_src: option.image_src.clone().unwrap_or_default(),
})
.collect()
}
fn domain_shape_options_from_snapshot(
options: &[SquareHoleShapeOptionSnapshot],
) -> Vec<DomainSquareHoleShapeOption> {
options
.iter()
.map(|option| DomainSquareHoleShapeOption {
option_id: option.option_id.clone(),
shape_kind: option.shape_kind.clone(),
label: option.label.clone(),
image_prompt: option.image_prompt.clone(),
image_src: empty_to_none(&option.image_src),
})
.collect()
}
fn hole_options_to_snapshot(
options: &[DomainSquareHoleHoleOption],
) -> Vec<SquareHoleHoleOptionSnapshot> {
options
.iter()
.map(|option| SquareHoleHoleOptionSnapshot {
hole_id: option.hole_id.clone(),
hole_kind: option.hole_kind.clone(),
label: option.label.clone(),
bonus: option.bonus,
})
.collect()
}
fn domain_hole_options_from_snapshot(
options: &[SquareHoleHoleOptionSnapshot],
) -> Vec<DomainSquareHoleHoleOption> {
options
.iter()
.map(|option| DomainSquareHoleHoleOption {
hole_id: option.hole_id.clone(),
hole_kind: option.hole_kind.clone(),
label: option.label.clone(),
bonus: option.bonus,
})
.collect()
}
fn feedback_from_domain(feedback: &DomainSquareHoleDropFeedback) -> SquareHoleDropFeedbackSnapshot {
SquareHoleDropFeedbackSnapshot {
accepted: feedback.accepted,

View File

@@ -85,6 +85,10 @@ pub struct SquareHoleWorkUpdateInput {
pub summary_text: String,
pub tags_json: String,
pub cover_image_src: String,
pub background_prompt: String,
pub background_image_src: String,
pub shape_options_json: String,
pub hole_options_json: String,
pub shape_count: u32,
pub difficulty: u32,
pub updated_at_micros: i64,
@@ -206,6 +210,37 @@ pub struct SquareHoleCreatorConfigSnapshot {
pub twist_rule: String,
pub shape_count: u32,
pub difficulty: u32,
#[serde(default)]
pub shape_options: Vec<SquareHoleShapeOptionSnapshot>,
#[serde(default)]
pub hole_options: Vec<SquareHoleHoleOptionSnapshot>,
#[serde(default)]
pub background_prompt: String,
#[serde(default)]
pub cover_image_src: String,
#[serde(default)]
pub background_image_src: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SquareHoleShapeOptionSnapshot {
pub option_id: String,
pub shape_kind: String,
pub label: String,
pub image_prompt: String,
#[serde(default)]
pub image_src: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SquareHoleHoleOptionSnapshot {
pub hole_id: String,
pub hole_kind: String,
pub label: String,
#[serde(default)]
pub bonus: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
@@ -228,6 +263,16 @@ pub struct SquareHoleDraftSnapshot {
pub twist_rule: String,
pub summary_text: String,
pub tags: Vec<String>,
#[serde(default)]
pub cover_image_src: String,
#[serde(default)]
pub background_prompt: String,
#[serde(default)]
pub background_image_src: String,
#[serde(default)]
pub shape_options: Vec<SquareHoleShapeOptionSnapshot>,
#[serde(default)]
pub hole_options: Vec<SquareHoleHoleOptionSnapshot>,
pub shape_count: u32,
pub difficulty: u32,
}
@@ -264,6 +309,14 @@ pub struct SquareHoleWorkSnapshot {
pub summary_text: String,
pub tags: Vec<String>,
pub cover_image_src: String,
#[serde(default)]
pub background_prompt: String,
#[serde(default)]
pub background_image_src: String,
#[serde(default)]
pub shape_options: Vec<SquareHoleShapeOptionSnapshot>,
#[serde(default)]
pub hole_options: Vec<SquareHoleHoleOptionSnapshot>,
pub shape_count: u32,
pub difficulty: u32,
pub config: SquareHoleCreatorConfigSnapshot,
@@ -281,6 +334,8 @@ pub struct SquareHoleShapeSnapshot {
pub shape_kind: String,
pub label: String,
pub color: String,
#[serde(default)]
pub image_src: String,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
@@ -291,6 +346,8 @@ pub struct SquareHoleHoleSnapshot {
pub label: String,
pub x: f32,
pub y: f32,
#[serde(default)]
pub bonus: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
@@ -319,6 +376,10 @@ pub struct SquareHoleRunSnapshot {
pub best_combo: u32,
pub score: u32,
pub rule_label: String,
#[serde(default)]
pub background_image_src: String,
#[serde(default)]
pub shape_options: Vec<SquareHoleShapeOptionSnapshot>,
pub current_shape: Option<SquareHoleShapeSnapshot>,
pub holes: Vec<SquareHoleHoleSnapshot>,
pub last_feedback: Option<SquareHoleDropFeedbackSnapshot>,