完善抓大鹅创作入口与运行态表现

This commit is contained in:
2026-05-01 22:07:55 +08:00
parent 8c03ec95c6
commit 9a3db67e13
25 changed files with 1320 additions and 183 deletions

View File

@@ -58,6 +58,9 @@ const MATCH3D_RUNTIME_PROVIDER: &str = "match3d-runtime";
const MATCH3D_DEFAULT_THEME: &str = "缤纷玩具";
const MATCH3D_DEFAULT_CLEAR_COUNT: u32 = 12;
const MATCH3D_DEFAULT_DIFFICULTY: u32 = 4;
const MATCH3D_QUESTION_THEME: &str = "你想创作什么题材";
const MATCH3D_QUESTION_CLEAR_COUNT: &str = "需要消除多少次才能通关";
const MATCH3D_QUESTION_DIFFICULTY: &str = "如果难度是从1-10你要创作的关卡是难度几";
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
@@ -90,7 +93,7 @@ pub async fn create_match3d_agent_session(
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 = build_match3d_assistant_reply(&config);
let welcome_message_text = MATCH3D_QUESTION_THEME.to_string();
let session = state
.spacetime_client()
@@ -810,9 +813,10 @@ async fn submit_and_finalize_match3d_message(
map_match3d_client_error(error),
)
})?;
let next_turn = submitted.current_turn.saturating_add(1);
let next_config = build_config_from_message(&submitted, &payload);
let assistant_reply = build_match3d_assistant_reply(&next_config);
let progress_percent = resolve_progress_percent(&next_config);
let assistant_reply = build_match3d_assistant_reply_for_turn(&next_config, next_turn);
let progress_percent = resolve_progress_percent_for_turn(next_turn);
let stage = if progress_percent >= 100 {
"ReadyToCompile"
} else {
@@ -865,6 +869,14 @@ async fn compile_match3d_draft_for_session(
map_match3d_client_error(error),
)
})?;
if session.current_turn < 3 || session.progress_percent < 100 {
return Err(match3d_bad_request(
request_context,
MATCH3D_AGENT_PROVIDER,
"match3d 创作配置尚未确认完成",
));
}
let config = resolve_config_or_default(session.config.as_ref());
let tags_json = tags
.as_ref()
@@ -901,8 +913,12 @@ fn map_match3d_agent_session_response(
session_id: session.session_id,
current_turn: session.current_turn,
progress_percent: session.progress_percent,
stage: session.stage,
anchor_pack: map_match3d_anchor_pack_response(session.anchor_pack),
stage: session.stage.clone(),
anchor_pack: map_match3d_anchor_pack_response_for_turn(
session.anchor_pack,
session.current_turn,
session.stage.as_str(),
),
config: session.config.map(map_match3d_config_response),
draft: session.draft.map(map_match3d_draft_response),
messages: session
@@ -916,11 +932,35 @@ fn map_match3d_agent_session_response(
}
}
fn map_match3d_anchor_pack_response(anchor: Match3DAnchorPackRecord) -> Match3DAnchorPackResponse {
fn map_match3d_anchor_pack_response_for_turn(
anchor: Match3DAnchorPackRecord,
current_turn: u32,
stage: &str,
) -> Match3DAnchorPackResponse {
let is_ready = matches!(
stage,
"ReadyToCompile"
| "ready_to_compile"
| "DraftCompiled"
| "draft_compiled"
| "draft_ready"
| "ReadyToPublish"
| "ready_to_publish"
| "Published"
| "published"
);
let collected_count = if is_ready { 3 } else { current_turn.min(3) };
Match3DAnchorPackResponse {
theme: map_match3d_anchor_item_response(anchor.theme),
clear_count: map_match3d_anchor_item_response(anchor.clear_count),
difficulty: map_match3d_anchor_item_response(anchor.difficulty),
theme: map_match3d_anchor_item_response_for_collected(anchor.theme, collected_count >= 1),
clear_count: map_match3d_anchor_item_response_for_collected(
anchor.clear_count,
collected_count >= 2,
),
difficulty: map_match3d_anchor_item_response_for_collected(
anchor.difficulty,
collected_count >= 3,
),
}
}
@@ -933,6 +973,22 @@ fn map_match3d_anchor_item_response(anchor: Match3DAnchorItemRecord) -> Match3DA
}
}
fn map_match3d_anchor_item_response_for_collected(
anchor: Match3DAnchorItemRecord,
collected: bool,
) -> Match3DAnchorItemResponse {
if collected {
return map_match3d_anchor_item_response(anchor);
}
Match3DAnchorItemResponse {
key: anchor.key,
label: anchor.label,
value: String::new(),
status: "missing".to_string(),
}
}
fn map_match3d_config_response(config: Match3DCreatorConfigRecord) -> Match3DCreatorConfigResponse {
Match3DCreatorConfigResponse {
theme_text: config.theme_text,
@@ -1096,31 +1152,51 @@ fn build_config_from_message(
payload: &SendMatch3DAgentMessageRequest,
) -> Match3DConfigJson {
let current = resolve_config_or_default(session.config.as_ref());
if payload.quick_fill_requested.unwrap_or(false) || payload.text.contains("自动配置") {
return Match3DConfigJson {
theme_text: if current.theme_text.trim().is_empty() {
let text = payload.text.trim();
let reference_image_src = payload
.reference_image_src
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.map(str::to_string)
.or(current.reference_image_src);
let quick_fill_requested =
payload.quick_fill_requested.unwrap_or(false) || text.contains("自动配置");
let mut theme_text = current.theme_text;
let mut clear_count = current.clear_count.max(1);
let mut difficulty = current.difficulty.clamp(1, 10);
match session.current_turn {
0 => {
theme_text = if quick_fill_requested {
MATCH3D_DEFAULT_THEME.to_string()
} else {
current.theme_text
},
reference_image_src: current.reference_image_src,
clear_count: current.clear_count.max(1),
difficulty: current.difficulty.clamp(1, 10),
};
parse_theme_answer(text).unwrap_or(theme_text)
};
}
1 => {
clear_count = if quick_fill_requested {
clear_count
} else {
parse_number_after_keywords(text, &["消除", "次数", "clearCount"])
.unwrap_or(clear_count)
}
.max(1);
}
_ => {
difficulty = if quick_fill_requested {
difficulty
} else {
parse_number_after_keywords(text, &["难度", "difficulty"]).unwrap_or(difficulty)
}
.clamp(1, 10);
}
}
let text = payload.text.trim();
let theme_text = parse_theme_from_text(text).unwrap_or(current.theme_text);
let clear_count = parse_number_after_keywords(text, &["消除", "次数", "clearCount"])
.unwrap_or(current.clear_count)
.max(1);
let difficulty = parse_number_after_keywords(text, &["难度", "difficulty"])
.unwrap_or(current.difficulty)
.clamp(1, 10);
Match3DConfigJson {
theme_text,
reference_image_src: current.reference_image_src,
reference_image_src,
clear_count,
difficulty,
}
@@ -1174,19 +1250,25 @@ fn build_match3d_assistant_reply(config: &Match3DConfigJson) -> String {
)
}
fn resolve_progress_percent(config: &Match3DConfigJson) -> u32 {
let completed = [
!config.theme_text.trim().is_empty(),
config.clear_count > 0,
(1..=10).contains(&config.difficulty),
]
.into_iter()
.filter(|done| *done)
.count();
((completed as u32) * 100) / 3
fn build_match3d_assistant_reply_for_turn(config: &Match3DConfigJson, current_turn: u32) -> String {
match current_turn {
0 => MATCH3D_QUESTION_THEME.to_string(),
1 => MATCH3D_QUESTION_CLEAR_COUNT.to_string(),
2 => MATCH3D_QUESTION_DIFFICULTY.to_string(),
_ => build_match3d_assistant_reply(config),
}
}
fn parse_theme_from_text(text: &str) -> Option<String> {
fn resolve_progress_percent_for_turn(current_turn: u32) -> u32 {
match current_turn {
0 => 0,
1 => 33,
2 => 66,
_ => 100,
}
}
fn parse_theme_answer(text: &str) -> Option<String> {
for marker in ["题材", "主题"] {
if let Some((_, value)) = text.split_once(marker) {
let normalized = value
@@ -1416,3 +1498,81 @@ fn current_utc_micros() -> i64 {
fn current_utc_ms() -> i64 {
current_utc_micros().saturating_div(1000)
}
#[cfg(test)]
mod tests {
use super::*;
fn config(theme_text: &str, clear_count: u32, difficulty: u32) -> Match3DConfigJson {
Match3DConfigJson {
theme_text: theme_text.to_string(),
reference_image_src: None,
clear_count,
difficulty,
}
}
#[test]
fn match3d_agent_reply_asks_three_questions_before_confirmation() {
let current = config("水果", 4, 6);
assert_eq!(
build_match3d_assistant_reply_for_turn(&current, 0),
MATCH3D_QUESTION_THEME
);
assert_eq!(
build_match3d_assistant_reply_for_turn(&current, 1),
MATCH3D_QUESTION_CLEAR_COUNT
);
assert_eq!(
build_match3d_assistant_reply_for_turn(&current, 2),
MATCH3D_QUESTION_DIFFICULTY
);
assert_eq!(
build_match3d_assistant_reply_for_turn(&current, 3),
"已确认:水果题材,需要消除 4 次,共 12 件物品,难度 6。"
);
}
#[test]
fn match3d_agent_progress_follows_question_turns() {
assert_eq!(resolve_progress_percent_for_turn(0), 0);
assert_eq!(resolve_progress_percent_for_turn(1), 33);
assert_eq!(resolve_progress_percent_for_turn(2), 66);
assert_eq!(resolve_progress_percent_for_turn(3), 100);
assert_eq!(resolve_progress_percent_for_turn(8), 100);
}
#[test]
fn match3d_anchor_pack_masks_uncollected_default_values() {
let pack = Match3DAnchorPackRecord {
theme: Match3DAnchorItemRecord {
key: "theme".to_string(),
label: "题材主题".to_string(),
value: "缤纷玩具".to_string(),
status: "confirmed".to_string(),
},
clear_count: Match3DAnchorItemRecord {
key: "clearCount".to_string(),
label: "需要消除次数".to_string(),
value: "12".to_string(),
status: "confirmed".to_string(),
},
difficulty: Match3DAnchorItemRecord {
key: "difficulty".to_string(),
label: "难度".to_string(),
value: "4".to_string(),
status: "confirmed".to_string(),
},
};
let response = map_match3d_anchor_pack_response_for_turn(pack, 0, "Collecting");
assert_eq!(response.theme.value, "");
assert_eq!(response.theme.status, "missing");
assert_eq!(response.clear_count.value, "");
assert_eq!(response.clear_count.status, "missing");
assert_eq!(response.difficulty.value, "");
assert_eq!(response.difficulty.status, "missing");
}
}