Extend square-hole creation flow with visual asset timeout guard
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
use module_square_hole::{
|
||||
SQUARE_HOLE_MAX_DIFFICULTY, SQUARE_HOLE_MAX_SHAPE_COUNT, SQUARE_HOLE_MIN_DIFFICULTY,
|
||||
SQUARE_HOLE_MIN_SHAPE_COUNT,
|
||||
SQUARE_HOLE_MIN_SHAPE_COUNT, SquareHoleHoleOption, SquareHoleShapeOption,
|
||||
default_background_prompt, normalize_hole_options, normalize_shape_options,
|
||||
};
|
||||
use platform_llm::LlmClient;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -68,6 +69,34 @@ struct SquareHoleAgentConfigOutput {
|
||||
twist_rule: String,
|
||||
shape_count: u32,
|
||||
difficulty: u32,
|
||||
shape_options: Vec<SquareHoleAgentShapeOptionOutput>,
|
||||
hole_options: Vec<SquareHoleAgentHoleOptionOutput>,
|
||||
background_prompt: String,
|
||||
#[serde(default)]
|
||||
cover_image_src: String,
|
||||
#[serde(default)]
|
||||
background_image_src: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct SquareHoleAgentShapeOptionOutput {
|
||||
option_id: String,
|
||||
shape_kind: String,
|
||||
label: String,
|
||||
image_prompt: String,
|
||||
#[serde(default)]
|
||||
image_src: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct SquareHoleAgentHoleOptionOutput {
|
||||
hole_id: String,
|
||||
hole_kind: String,
|
||||
label: String,
|
||||
#[serde(default)]
|
||||
bonus: bool,
|
||||
}
|
||||
|
||||
pub(crate) async fn run_square_hole_agent_turn<F>(
|
||||
@@ -166,20 +195,136 @@ fn parse_model_config(
|
||||
));
|
||||
}
|
||||
|
||||
let theme_text = read_text_field(value, "themeText")
|
||||
.unwrap_or_else(|| session.config.theme_text.clone());
|
||||
let twist_rule = read_text_field(value, "twistRule")
|
||||
.unwrap_or_else(|| session.config.twist_rule.clone());
|
||||
let shape_options = parse_shape_options(value, session, &theme_text);
|
||||
let hole_options = parse_hole_options(value, session);
|
||||
let background_prompt = read_text_field(value, "backgroundPrompt")
|
||||
.or_else(|| {
|
||||
session
|
||||
.config
|
||||
.background_prompt
|
||||
.trim()
|
||||
.is_empty()
|
||||
.then(|| default_background_prompt(&theme_text))
|
||||
})
|
||||
.unwrap_or_else(|| session.config.background_prompt.clone());
|
||||
|
||||
Ok(SquareHoleAgentConfigOutput {
|
||||
theme_text: read_text_field(value, "themeText")
|
||||
.unwrap_or_else(|| session.config.theme_text.clone()),
|
||||
twist_rule: read_text_field(value, "twistRule")
|
||||
.unwrap_or_else(|| session.config.twist_rule.clone()),
|
||||
theme_text,
|
||||
twist_rule,
|
||||
shape_count: read_u32_field(value, "shapeCount")
|
||||
.unwrap_or(session.config.shape_count)
|
||||
.clamp(SQUARE_HOLE_MIN_SHAPE_COUNT, SQUARE_HOLE_MAX_SHAPE_COUNT),
|
||||
difficulty: read_u32_field(value, "difficulty")
|
||||
.unwrap_or(session.config.difficulty)
|
||||
.clamp(SQUARE_HOLE_MIN_DIFFICULTY, SQUARE_HOLE_MAX_DIFFICULTY),
|
||||
shape_options: shape_options
|
||||
.into_iter()
|
||||
.map(SquareHoleAgentShapeOptionOutput::from)
|
||||
.collect(),
|
||||
hole_options: hole_options
|
||||
.into_iter()
|
||||
.map(SquareHoleAgentHoleOptionOutput::from)
|
||||
.collect(),
|
||||
background_prompt,
|
||||
cover_image_src: session.config.cover_image_src.clone().unwrap_or_default(),
|
||||
background_image_src: session
|
||||
.config
|
||||
.background_image_src
|
||||
.clone()
|
||||
.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_shape_options(
|
||||
value: &JsonValue,
|
||||
session: &SquareHoleAgentSessionRecord,
|
||||
theme_text: &str,
|
||||
) -> Vec<SquareHoleShapeOption> {
|
||||
let parsed = value
|
||||
.get("shapeOptions")
|
||||
.and_then(JsonValue::as_array)
|
||||
.map(|items| {
|
||||
items
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, item)| SquareHoleShapeOption {
|
||||
option_id: read_text_field(item, "optionId")
|
||||
.unwrap_or_else(|| format!("shape-option-{index}")),
|
||||
shape_kind: read_text_field(item, "shapeKind")
|
||||
.unwrap_or_else(|| fallback_shape_kind(index).to_string()),
|
||||
label: read_text_field(item, "label")
|
||||
.unwrap_or_else(|| fallback_shape_label(index).to_string()),
|
||||
image_prompt: read_text_field(item, "imagePrompt").unwrap_or_else(|| {
|
||||
format!("{theme_text}主题的{}贴纸图,透明背景,明亮游戏资产", fallback_shape_label(index))
|
||||
}),
|
||||
image_src: read_text_field(item, "imageSrc"),
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
session
|
||||
.config
|
||||
.shape_options
|
||||
.iter()
|
||||
.map(|option| SquareHoleShapeOption {
|
||||
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(),
|
||||
})
|
||||
.collect()
|
||||
});
|
||||
|
||||
normalize_shape_options(parsed, theme_text)
|
||||
}
|
||||
|
||||
fn parse_hole_options(
|
||||
value: &JsonValue,
|
||||
session: &SquareHoleAgentSessionRecord,
|
||||
) -> Vec<SquareHoleHoleOption> {
|
||||
let parsed = value
|
||||
.get("holeOptions")
|
||||
.and_then(JsonValue::as_array)
|
||||
.map(|items| {
|
||||
items
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, item)| SquareHoleHoleOption {
|
||||
hole_id: read_text_field(item, "holeId")
|
||||
.unwrap_or_else(|| format!("hole-option-{index}")),
|
||||
hole_kind: read_text_field(item, "holeKind")
|
||||
.unwrap_or_else(|| fallback_shape_kind(index).to_string()),
|
||||
label: read_text_field(item, "label")
|
||||
.unwrap_or_else(|| fallback_hole_label(index).to_string()),
|
||||
bonus: item
|
||||
.get("bonus")
|
||||
.and_then(JsonValue::as_bool)
|
||||
.unwrap_or(index == 0),
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
session
|
||||
.config
|
||||
.hole_options
|
||||
.iter()
|
||||
.map(|option| SquareHoleHoleOption {
|
||||
hole_id: option.hole_id.clone(),
|
||||
hole_kind: option.hole_kind.clone(),
|
||||
label: option.label.clone(),
|
||||
bonus: option.bonus,
|
||||
})
|
||||
.collect()
|
||||
});
|
||||
|
||||
normalize_hole_options(parsed)
|
||||
}
|
||||
|
||||
fn read_text_field(value: &JsonValue, field_name: &str) -> Option<String> {
|
||||
value
|
||||
.get(field_name)
|
||||
@@ -196,6 +341,62 @@ fn read_u32_field(value: &JsonValue, field_name: &str) -> Option<u32> {
|
||||
.and_then(|number| u32::try_from(number).ok())
|
||||
}
|
||||
|
||||
fn fallback_shape_kind(index: usize) -> &'static str {
|
||||
match index % 6 {
|
||||
0 => "square",
|
||||
1 => "circle",
|
||||
2 => "triangle",
|
||||
3 => "diamond",
|
||||
4 => "star",
|
||||
_ => "arch",
|
||||
}
|
||||
}
|
||||
|
||||
fn fallback_shape_label(index: usize) -> &'static str {
|
||||
match fallback_shape_kind(index) {
|
||||
"square" => "方块",
|
||||
"circle" => "圆块",
|
||||
"triangle" => "三角块",
|
||||
"diamond" => "菱形块",
|
||||
"star" => "星形块",
|
||||
_ => "拱形块",
|
||||
}
|
||||
}
|
||||
|
||||
fn fallback_hole_label(index: usize) -> &'static str {
|
||||
match fallback_shape_kind(index) {
|
||||
"square" => "方洞",
|
||||
"circle" => "圆洞",
|
||||
"triangle" => "三角洞",
|
||||
"diamond" => "菱形洞",
|
||||
"star" => "星形洞",
|
||||
_ => "拱形洞",
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SquareHoleShapeOption> for SquareHoleAgentShapeOptionOutput {
|
||||
fn from(option: SquareHoleShapeOption) -> Self {
|
||||
Self {
|
||||
option_id: option.option_id,
|
||||
shape_kind: option.shape_kind,
|
||||
label: option.label,
|
||||
image_prompt: option.image_prompt,
|
||||
image_src: option.image_src.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SquareHoleHoleOption> for SquareHoleAgentHoleOptionOutput {
|
||||
fn from(option: SquareHoleHoleOption) -> Self {
|
||||
Self {
|
||||
hole_id: option.hole_id,
|
||||
hole_kind: option.hole_kind,
|
||||
label: option.label,
|
||||
bonus: option.bonus,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_stage(progress_percent: u32) -> String {
|
||||
if progress_percent >= 100 {
|
||||
"ReadyToCompile"
|
||||
@@ -228,6 +429,11 @@ mod tests {
|
||||
twist_rule: "方洞万能".to_string(),
|
||||
shape_count: 12,
|
||||
difficulty: 4,
|
||||
shape_options: Vec::new(),
|
||||
hole_options: Vec::new(),
|
||||
background_prompt: "纸箱玩具桌面背景".to_string(),
|
||||
cover_image_src: None,
|
||||
background_image_src: None,
|
||||
},
|
||||
draft: None,
|
||||
messages: Vec::new(),
|
||||
@@ -260,7 +466,24 @@ mod tests {
|
||||
"themeText": "办公室文具",
|
||||
"twistRule": "所有文具最终都优先进入方洞",
|
||||
"shapeCount": 14,
|
||||
"difficulty": 6
|
||||
"difficulty": 6,
|
||||
"shapeOptions": [
|
||||
{
|
||||
"optionId": "stamp",
|
||||
"shapeKind": "circle",
|
||||
"label": "圆形印章",
|
||||
"imagePrompt": "办公室圆形印章贴纸"
|
||||
}
|
||||
],
|
||||
"holeOptions": [
|
||||
{
|
||||
"holeId": "folder",
|
||||
"holeKind": "square",
|
||||
"label": "档案盒方洞",
|
||||
"bonus": true
|
||||
}
|
||||
],
|
||||
"backgroundPrompt": "办公室桌面纸箱玩具背景"
|
||||
}
|
||||
});
|
||||
|
||||
@@ -276,6 +499,14 @@ mod tests {
|
||||
assert_eq!(output.next_config.twist_rule, "所有文具最终都优先进入方洞");
|
||||
assert_eq!(output.next_config.shape_count, 14);
|
||||
assert_eq!(output.next_config.difficulty, 6);
|
||||
assert!(output.next_config.shape_options.len() >= 6);
|
||||
assert_eq!(output.next_config.shape_options[0].label, "圆形印章");
|
||||
assert_eq!(output.next_config.hole_options[0].label, "档案盒方洞");
|
||||
assert!(output.next_config.hole_options[0].bonus);
|
||||
assert_eq!(
|
||||
output.next_config.background_prompt,
|
||||
"办公室桌面纸箱玩具背景"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -287,7 +518,10 @@ mod tests {
|
||||
"themeText": "霓虹积木",
|
||||
"twistRule": "方洞优先",
|
||||
"shapeCount": 99,
|
||||
"difficulty": 0
|
||||
"difficulty": 0,
|
||||
"shapeOptions": [],
|
||||
"holeOptions": [],
|
||||
"backgroundPrompt": ""
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user