Merge codex/sse-stream-architecture into architecture adjustment

This commit is contained in:
2026-06-07 00:23:42 +08:00
136 changed files with 22344 additions and 7543 deletions

View File

@@ -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

View File

@@ -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,
)
})
}

View File

@@ -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!(

View File

@@ -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"
)
}));
}
}

View File

@@ -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))

View File

@@ -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),
],