Merge codex/sse-stream-architecture into architecture adjustment
This commit is contained in:
@@ -307,13 +307,18 @@ pub(crate) fn build_puzzle_session_snapshot_from_action_payload(
|
||||
levels,
|
||||
form_draft: None,
|
||||
};
|
||||
let stage = if is_puzzle_session_snapshot_publish_ready(&draft) {
|
||||
"ready_to_publish"
|
||||
} else {
|
||||
"image_refining"
|
||||
};
|
||||
|
||||
Ok(PuzzleAgentSessionRecord {
|
||||
session_id: session_id.to_string(),
|
||||
seed_text: String::new(),
|
||||
current_turn: 0,
|
||||
progress_percent: 94,
|
||||
stage: "ready_to_publish".to_string(),
|
||||
stage: stage.to_string(),
|
||||
anchor_pack,
|
||||
draft: Some(draft),
|
||||
messages: Vec::new(),
|
||||
@@ -1783,7 +1788,11 @@ pub(crate) fn apply_generated_puzzle_candidates_to_session_snapshot(
|
||||
sync_puzzle_primary_draft_fields_from_level(draft);
|
||||
}
|
||||
session.progress_percent = session.progress_percent.max(94);
|
||||
session.stage = "ready_to_publish".to_string();
|
||||
session.stage = if is_puzzle_session_snapshot_publish_ready(draft) {
|
||||
"ready_to_publish".to_string()
|
||||
} else {
|
||||
"image_refining".to_string()
|
||||
};
|
||||
session.last_assistant_reply = Some("拼图图片已经生成,并已替换当前正式图。".to_string());
|
||||
session.updated_at = format_timestamp_micros(updated_at_micros);
|
||||
session
|
||||
|
||||
@@ -248,6 +248,17 @@ pub(super) fn apply_generated_puzzle_tags_to_session_snapshot(
|
||||
session
|
||||
}
|
||||
|
||||
fn has_required_puzzle_asset_ref(image_src: &Option<String>, object_key: &Option<String>) -> bool {
|
||||
image_src
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.is_some_and(|value| !value.is_empty())
|
||||
|| object_key
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.is_some_and(|value| !value.is_empty())
|
||||
}
|
||||
|
||||
pub(super) fn is_puzzle_session_snapshot_publish_ready(draft: &PuzzleResultDraftRecord) -> bool {
|
||||
!draft.work_title.trim().is_empty()
|
||||
&& !draft.work_description.trim().is_empty()
|
||||
@@ -261,6 +272,18 @@ pub(super) fn is_puzzle_session_snapshot_publish_ready(draft: &PuzzleResultDraft
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.is_some_and(|value| !value.is_empty())
|
||||
&& has_required_puzzle_asset_ref(
|
||||
&level.level_scene_image_src,
|
||||
&level.level_scene_image_object_key,
|
||||
)
|
||||
&& has_required_puzzle_asset_ref(
|
||||
&level.ui_spritesheet_image_src,
|
||||
&level.ui_spritesheet_image_object_key,
|
||||
)
|
||||
&& has_required_puzzle_asset_ref(
|
||||
&level.level_background_image_src,
|
||||
&level.level_background_image_object_key,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -474,7 +474,7 @@ fn puzzle_image_generation_builds_fallback_session_from_levels_snapshot() {
|
||||
.expect("fallback session");
|
||||
|
||||
let draft = session.draft.expect("draft");
|
||||
assert_eq!(session.stage, "ready_to_publish");
|
||||
assert_eq!(session.stage, "image_refining");
|
||||
assert_eq!(draft.work_title, "暖灯猫街作品");
|
||||
assert_eq!(draft.theme_tags, vec!["猫咪", "雨夜"]);
|
||||
assert_eq!(draft.levels[0].level_id, "puzzle-level-1");
|
||||
@@ -484,6 +484,62 @@ fn puzzle_image_generation_builds_fallback_session_from_levels_snapshot() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn puzzle_image_generation_fallback_session_ready_when_asset_pack_complete() {
|
||||
let levels_json = serde_json::to_string(&vec![json!({
|
||||
"level_id": "puzzle-level-1",
|
||||
"level_name": "雨夜猫街",
|
||||
"picture_description": "一只猫在雨夜灯牌下回头。",
|
||||
"candidates": [],
|
||||
"selected_candidate_id": null,
|
||||
"cover_image_src": "/generated/puzzle/cover.png",
|
||||
"cover_asset_id": "asset-cover",
|
||||
"level_scene_image_src": "/generated/puzzle/level-scene.png",
|
||||
"level_scene_image_object_key": "generated/puzzle/level-scene.png",
|
||||
"ui_spritesheet_image_src": "/generated/puzzle/ui-spritesheet.png",
|
||||
"ui_spritesheet_image_object_key": "generated/puzzle/ui-spritesheet.png",
|
||||
"level_background_image_src": "/generated/puzzle/level-background.png",
|
||||
"level_background_image_object_key": "generated/puzzle/level-background.png",
|
||||
"generation_status": "ready",
|
||||
})])
|
||||
.expect("levels json");
|
||||
let payload = ExecutePuzzleAgentActionRequest {
|
||||
action: "generate_puzzle_images".to_string(),
|
||||
prompt_text: None,
|
||||
reference_image_src: None,
|
||||
reference_image_srcs: Vec::new(),
|
||||
reference_image_asset_object_id: None,
|
||||
reference_image_asset_object_ids: Vec::new(),
|
||||
image_model: Some(PUZZLE_IMAGE_MODEL_GPT_IMAGE_2.to_string()),
|
||||
ai_redraw: None,
|
||||
candidate_count: Some(1),
|
||||
should_auto_name_level: None,
|
||||
candidate_id: None,
|
||||
level_id: Some("puzzle-level-1".to_string()),
|
||||
work_title: Some("暖灯猫街作品".to_string()),
|
||||
work_description: Some("一套雨夜猫街主题拼图。".to_string()),
|
||||
picture_description: None,
|
||||
level_name: None,
|
||||
summary: Some("当前关卡画面。".to_string()),
|
||||
theme_tags: Some(vec![
|
||||
"猫咪".to_string(),
|
||||
"雨夜".to_string(),
|
||||
"灯牌".to_string(),
|
||||
]),
|
||||
levels_json: Some(levels_json.clone()),
|
||||
};
|
||||
|
||||
let session = build_puzzle_session_snapshot_from_action_payload(
|
||||
"puzzle-session-1",
|
||||
&payload,
|
||||
Some(levels_json.as_str()),
|
||||
1_713_686_401_234_567,
|
||||
)
|
||||
.expect("fallback session");
|
||||
|
||||
assert_eq!(session.stage, "ready_to_publish");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn puzzle_first_level_name_parser_accepts_json_and_normalizes_text() {
|
||||
assert_eq!(
|
||||
|
||||
@@ -541,6 +541,17 @@ pub fn build_result_preview(
|
||||
}
|
||||
}
|
||||
|
||||
fn has_required_puzzle_asset_ref(image_src: &Option<String>, object_key: &Option<String>) -> bool {
|
||||
image_src
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.is_some_and(|value| !value.is_empty())
|
||||
|| object_key
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.is_some_and(|value| !value.is_empty())
|
||||
}
|
||||
|
||||
pub fn validate_publish_requirements(
|
||||
draft: &PuzzleResultDraft,
|
||||
author_display_name: Option<&str>,
|
||||
@@ -582,6 +593,36 @@ pub fn validate_publish_requirements(
|
||||
message: "正式拼图图片尚未确定".to_string(),
|
||||
});
|
||||
}
|
||||
if !has_required_puzzle_asset_ref(
|
||||
&level.level_scene_image_src,
|
||||
&level.level_scene_image_object_key,
|
||||
) {
|
||||
blockers.push(PuzzleResultPreviewBlocker {
|
||||
id: format!("missing-level-scene-image-{}", level.level_id),
|
||||
code: "MISSING_LEVEL_SCENE_IMAGE".to_string(),
|
||||
message: "正式关卡画面尚未生成".to_string(),
|
||||
});
|
||||
}
|
||||
if !has_required_puzzle_asset_ref(
|
||||
&level.ui_spritesheet_image_src,
|
||||
&level.ui_spritesheet_image_object_key,
|
||||
) {
|
||||
blockers.push(PuzzleResultPreviewBlocker {
|
||||
id: format!("missing-ui-spritesheet-image-{}", level.level_id),
|
||||
code: "MISSING_UI_SPRITESHEET_IMAGE".to_string(),
|
||||
message: "UI spritesheet 尚未生成".to_string(),
|
||||
});
|
||||
}
|
||||
if !has_required_puzzle_asset_ref(
|
||||
&level.level_background_image_src,
|
||||
&level.level_background_image_object_key,
|
||||
) {
|
||||
blockers.push(PuzzleResultPreviewBlocker {
|
||||
id: format!("missing-level-background-image-{}", level.level_id),
|
||||
code: "MISSING_LEVEL_BACKGROUND_IMAGE".to_string(),
|
||||
message: "关卡背景图尚未生成".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
if draft.theme_tags.len() < PUZZLE_MIN_TAG_COUNT
|
||||
|| draft.theme_tags.len() > PUZZLE_MAX_TAG_COUNT
|
||||
@@ -4011,4 +4052,37 @@ mod tests {
|
||||
.any(|blocker| blocker.code == "MISSING_LEVEL_NAME")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_publish_requirements_requires_generated_level_asset_pack() {
|
||||
let anchor_pack = infer_anchor_pack("雨夜猫咪神庙", Some("雨夜猫咪神庙"));
|
||||
let mut draft = compile_result_draft(&anchor_pack, &[]);
|
||||
draft.levels[0].cover_image_src = Some("/cover.png".to_string());
|
||||
|
||||
let blockers = validate_publish_requirements(&draft, Some("玩家"));
|
||||
let blocker_codes = blockers
|
||||
.iter()
|
||||
.map(|blocker| blocker.code.as_str())
|
||||
.collect::<Vec<_>>();
|
||||
assert!(blocker_codes.contains(&"MISSING_LEVEL_SCENE_IMAGE"));
|
||||
assert!(blocker_codes.contains(&"MISSING_UI_SPRITESHEET_IMAGE"));
|
||||
assert!(blocker_codes.contains(&"MISSING_LEVEL_BACKGROUND_IMAGE"));
|
||||
|
||||
draft.levels[0].level_scene_image_object_key =
|
||||
Some("generated/puzzle/level-scene.png".to_string());
|
||||
draft.levels[0].ui_spritesheet_image_object_key =
|
||||
Some("generated/puzzle/ui-spritesheet.png".to_string());
|
||||
draft.levels[0].level_background_image_object_key =
|
||||
Some("generated/puzzle/level-background.png".to_string());
|
||||
|
||||
let blockers = validate_publish_requirements(&draft, Some("玩家"));
|
||||
assert!(!blockers.iter().any(|blocker| {
|
||||
matches!(
|
||||
blocker.code.as_str(),
|
||||
"MISSING_LEVEL_SCENE_IMAGE"
|
||||
| "MISSING_UI_SPRITESHEET_IMAGE"
|
||||
| "MISSING_LEVEL_BACKGROUND_IMAGE"
|
||||
)
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,10 +161,9 @@ fn normalize_creation_entry_announcement_banner_value(
|
||||
);
|
||||
}
|
||||
|
||||
let banner = serde_json::from_value::<CreationEntryEventBannerResponse>(Value::Object(
|
||||
object.clone(),
|
||||
))
|
||||
.map_err(|error| format!("第 {} 条公告对象非法:{error}", index + 1))?;
|
||||
let banner =
|
||||
serde_json::from_value::<CreationEntryEventBannerResponse>(Value::Object(object.clone()))
|
||||
.map_err(|error| format!("第 {} 条公告对象非法:{error}", index + 1))?;
|
||||
normalize_creation_entry_event_banner_response(index, banner)
|
||||
}
|
||||
|
||||
@@ -332,10 +331,7 @@ fn normalize_banner_html_code(
|
||||
}
|
||||
let lower_html_code = html_code.to_ascii_lowercase();
|
||||
if lower_html_code.contains("<script") || lower_html_code.contains("javascript:") {
|
||||
return Err(format!(
|
||||
"第 {} 条 HTML 公告含有不允许的脚本代码",
|
||||
index + 1
|
||||
));
|
||||
return Err(format!("第 {} 条 HTML 公告含有不允许的脚本代码", index + 1));
|
||||
}
|
||||
|
||||
Ok(Some(html_code))
|
||||
|
||||
@@ -172,18 +172,8 @@ pub fn build_phase1_unified_creation_spec(play_id: &str) -> Option<UnifiedCreati
|
||||
vec![
|
||||
unified_creation_field("title", "text", "作品标题", true),
|
||||
unified_creation_field("themeDescription", "text", "主题/场景描述", true),
|
||||
unified_creation_field(
|
||||
"playerImageDescription",
|
||||
"text",
|
||||
"玩家形象描述",
|
||||
true,
|
||||
),
|
||||
unified_creation_field(
|
||||
"opponentImageDescription",
|
||||
"text",
|
||||
"对手形象描述",
|
||||
true,
|
||||
),
|
||||
unified_creation_field("playerImageDescription", "text", "玩家形象描述", true),
|
||||
unified_creation_field("opponentImageDescription", "text", "对手形象描述", true),
|
||||
unified_creation_field("onomatopoeia", "text", "拟声词", false),
|
||||
unified_creation_field("difficultyPreset", "select", "难度", true),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user