This commit is contained in:
2026-04-30 17:49:07 +08:00
parent 805d6f8cae
commit 9d684cb7b3
615 changed files with 15368 additions and 6172 deletions

View File

@@ -113,9 +113,26 @@ pub struct PuzzleGeneratedImageCandidate {
pub selected: bool,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PuzzleDraftLevel {
pub level_id: String,
pub level_name: String,
pub picture_description: String,
pub candidates: Vec<PuzzleGeneratedImageCandidate>,
pub selected_candidate_id: Option<String>,
pub cover_image_src: Option<String>,
pub cover_asset_id: Option<String>,
pub generation_status: String,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PuzzleResultDraft {
#[serde(default)]
pub work_title: String,
#[serde(default)]
pub work_description: String,
pub level_name: String,
pub summary: String,
pub theme_tags: Vec<String>,
@@ -127,6 +144,18 @@ pub struct PuzzleResultDraft {
pub cover_image_src: Option<String>,
pub cover_asset_id: Option<String>,
pub generation_status: String,
#[serde(default)]
pub levels: Vec<PuzzleDraftLevel>,
#[serde(default)]
pub form_draft: Option<PuzzleFormDraft>,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PuzzleFormDraft {
pub work_title: Option<String>,
pub work_description: Option<String>,
pub picture_description: Option<String>,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
@@ -202,11 +231,17 @@ pub struct PuzzleWorkProfile {
pub owner_user_id: String,
pub source_session_id: Option<String>,
pub author_display_name: String,
#[serde(default)]
pub work_title: String,
#[serde(default)]
pub work_description: String,
pub level_name: String,
pub summary: String,
pub theme_tags: Vec<String>,
pub cover_image_src: Option<String>,
pub cover_asset_id: Option<String>,
#[serde(default)]
pub levels: Vec<PuzzleDraftLevel>,
pub publication_status: PuzzlePublicationStatus,
pub updated_at_micros: i64,
pub published_at_micros: Option<i64>,
@@ -334,6 +369,15 @@ pub struct PuzzleAgentSessionCreateInput {
pub created_at_micros: i64,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PuzzleFormDraftSaveInput {
pub session_id: String,
pub owner_user_id: String,
pub seed_text: String,
pub saved_at_micros: i64,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PuzzleAgentSessionGetInput {
@@ -378,6 +422,7 @@ pub struct PuzzleDraftCompileInput {
pub struct PuzzleGeneratedImagesSaveInput {
pub session_id: String,
pub owner_user_id: String,
pub level_id: Option<String>,
pub candidates_json: String,
pub saved_at_micros: i64,
}
@@ -387,6 +432,7 @@ pub struct PuzzleGeneratedImagesSaveInput {
pub struct PuzzleSelectCoverImageInput {
pub session_id: String,
pub owner_user_id: String,
pub level_id: Option<String>,
pub candidate_id: String,
pub selected_at_micros: i64,
}
@@ -399,9 +445,12 @@ pub struct PuzzlePublishInput {
pub work_id: String,
pub profile_id: String,
pub author_display_name: String,
pub work_title: Option<String>,
pub work_description: Option<String>,
pub level_name: Option<String>,
pub summary: Option<String>,
pub theme_tags: Option<Vec<String>>,
pub levels_json: Option<String>,
pub published_at_micros: i64,
}
@@ -429,11 +478,14 @@ pub struct PuzzleWorkDeleteInput {
pub struct PuzzleWorkUpsertInput {
pub profile_id: String,
pub owner_user_id: String,
pub work_title: String,
pub work_description: String,
pub level_name: String,
pub summary: String,
pub theme_tags: Vec<String>,
pub cover_image_src: Option<String>,
pub cover_asset_id: Option<String>,
pub levels_json: Option<String>,
pub updated_at_micros: i64,
}
@@ -450,12 +502,21 @@ pub struct PuzzleWorkRemixInput {
pub remixed_at_micros: i64,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PuzzleWorkLikeRecordInput {
pub profile_id: String,
pub user_id: String,
pub liked_at_micros: i64,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PuzzleRunStartInput {
pub run_id: String,
pub owner_user_id: String,
pub profile_id: String,
pub level_id: Option<String>,
pub started_at_micros: i64,
}
@@ -690,10 +751,17 @@ pub fn empty_anchor_pack() -> PuzzleAnchorPack {
pub fn infer_anchor_pack(seed_text: &str, latest_message: Option<&str>) -> PuzzleAnchorPack {
let source = normalize_required_string(latest_message.unwrap_or(seed_text))
.or_else(|| normalize_required_string(seed_text))
.unwrap_or_else(|| "童话森林里的发光猫咪遗迹".to_string());
if let Some((title, picture_description)) = parse_form_seed_text(&source) {
return build_form_anchor_pack(title.as_str(), picture_description.as_str());
.or_else(|| normalize_required_string(seed_text));
let Some(source) = source else {
return empty_anchor_pack();
};
if let Some(form_seed) = parse_form_seed_text(&source) {
if form_seed.has_any_value() {
return build_form_anchor_pack(
form_seed.work_title.as_deref().unwrap_or(""),
form_seed.picture_description.as_deref().unwrap_or(""),
);
}
}
let mut pack = empty_anchor_pack();
@@ -711,22 +779,26 @@ pub fn infer_anchor_pack(seed_text: &str, latest_message: Option<&str>) -> Puzzl
}
pub fn build_form_anchor_pack(title: &str, picture_description: &str) -> PuzzleAnchorPack {
let normalized_title =
normalize_required_string(title).unwrap_or_else(|| "奇景拼图".to_string());
let normalized_description =
normalize_required_string(picture_description).unwrap_or_else(|| normalized_title.clone());
let normalized_title = normalize_required_string(title);
let normalized_description = normalize_required_string(picture_description);
let mut pack = empty_anchor_pack();
pack.theme_promise.value = normalized_title.clone();
pack.theme_promise.status = PuzzleAnchorStatus::Locked;
pack.visual_subject.value = normalized_description.clone();
pack.visual_subject.status = PuzzleAnchorStatus::Locked;
if let Some(title) = normalized_title.as_ref() {
pack.theme_promise.value = title.clone();
pack.theme_promise.status = PuzzleAnchorStatus::Locked;
}
if let Some(description) = normalized_description.as_ref() {
pack.visual_subject.value = description.clone();
pack.visual_subject.status = PuzzleAnchorStatus::Locked;
}
pack.visual_mood.value = "清晰、适合拼图切块".to_string();
pack.visual_mood.status = PuzzleAnchorStatus::Inferred;
pack.composition_hooks.value = "主体轮廓、色块分区、局部细节".to_string();
pack.composition_hooks.status = PuzzleAnchorStatus::Inferred;
pack.tags_and_forbidden.value =
build_form_tags_and_forbidden(normalized_title.as_str(), normalized_description.as_str());
pack.tags_and_forbidden.value = build_form_tags_and_forbidden(
normalized_title.as_deref().unwrap_or(""),
normalized_description.as_deref().unwrap_or(""),
);
pack.tags_and_forbidden.status = PuzzleAnchorStatus::Inferred;
pack
@@ -766,13 +838,37 @@ pub fn build_creator_intent(
pub fn compile_result_draft(
anchor_pack: &PuzzleAnchorPack,
messages: &[PuzzleAgentMessageSnapshot],
) -> PuzzleResultDraft {
compile_result_draft_from_seed(anchor_pack, messages, None)
}
pub fn compile_result_draft_from_seed(
anchor_pack: &PuzzleAnchorPack,
messages: &[PuzzleAgentMessageSnapshot],
seed_text: Option<&str>,
) -> PuzzleResultDraft {
let creator_intent = build_creator_intent(anchor_pack, messages);
let normalized_tags = normalize_theme_tags(creator_intent.theme_tags.clone());
let level_name = build_level_name(anchor_pack, &normalized_tags);
let work_title = build_work_title(anchor_pack);
let work_description = resolve_work_description(seed_text, anchor_pack);
let picture_description = fallback_text(&anchor_pack.visual_subject.value, "画面主体");
let level_name =
build_level_name_from_picture(picture_description.as_str(), &normalized_tags, 1);
let level = PuzzleDraftLevel {
level_id: "puzzle-level-1".to_string(),
level_name: level_name.clone(),
picture_description,
candidates: Vec::new(),
selected_candidate_id: None,
cover_image_src: None,
cover_asset_id: None,
generation_status: "idle".to_string(),
};
PuzzleResultDraft {
work_title,
work_description: work_description.clone(),
level_name,
summary: build_result_summary(anchor_pack),
summary: work_description,
theme_tags: normalized_tags,
forbidden_directives: creator_intent.forbidden_directives.clone(),
creator_intent: Some(creator_intent),
@@ -782,6 +878,79 @@ pub fn compile_result_draft(
cover_image_src: None,
cover_asset_id: None,
generation_status: "idle".to_string(),
levels: vec![level],
form_draft: None,
}
}
pub fn build_form_draft_from_seed(
anchor_pack: &PuzzleAnchorPack,
seed_text: Option<&str>,
) -> PuzzleResultDraft {
let form_seed = seed_text.and_then(parse_form_seed_text);
build_form_draft_from_parts(
anchor_pack,
form_seed.as_ref().and_then(|seed| seed.work_title.clone()),
form_seed
.as_ref()
.and_then(|seed| seed.work_description.clone()),
form_seed.and_then(|seed| seed.picture_description),
)
}
pub fn build_form_draft_from_parts(
anchor_pack: &PuzzleAnchorPack,
work_title: Option<String>,
work_description: Option<String>,
picture_description: Option<String>,
) -> PuzzleResultDraft {
let work_title = work_title.and_then(|value| normalize_required_string(&value));
let work_description = work_description.and_then(|value| normalize_required_string(&value));
let picture_description = picture_description.and_then(|value| normalize_required_string(&value));
let title_for_tags = work_title.as_deref().unwrap_or("");
let picture_for_tags = picture_description.as_deref().unwrap_or("");
let mut tags = normalize_theme_tags(derive_form_theme_tags(title_for_tags, picture_for_tags));
if tags.is_empty() {
tags = vec!["拼图".to_string(), "插画".to_string(), "清晰构图".to_string()];
}
let level_name = picture_description
.as_deref()
.map(|value| build_level_name_from_picture(value, &tags, 1))
.or_else(|| work_title.clone())
.unwrap_or_else(|| "未命名拼图".to_string());
let summary = work_description.clone().unwrap_or_default();
let level = PuzzleDraftLevel {
level_id: "puzzle-level-1".to_string(),
level_name: level_name.clone(),
picture_description: picture_description.clone().unwrap_or_default(),
candidates: Vec::new(),
selected_candidate_id: None,
cover_image_src: None,
cover_asset_id: None,
generation_status: "idle".to_string(),
};
// 中文注释:这是生成前的表单草稿,只用于创作中心恢复和表单回填,不进入发布就绪判断。
PuzzleResultDraft {
work_title: work_title.clone().unwrap_or_default(),
work_description: summary.clone(),
level_name,
summary,
theme_tags: tags,
forbidden_directives: Vec::new(),
creator_intent: None,
anchor_pack: anchor_pack.clone(),
candidates: Vec::new(),
selected_candidate_id: None,
cover_image_src: None,
cover_asset_id: None,
generation_status: "idle".to_string(),
levels: vec![level],
form_draft: Some(PuzzleFormDraft {
work_title,
work_description,
picture_description,
}),
}
}
@@ -849,13 +1018,155 @@ pub fn apply_selected_candidate(
Ok(draft)
}
pub fn normalize_puzzle_draft(mut draft: PuzzleResultDraft) -> PuzzleResultDraft {
if draft.work_title.trim().is_empty() {
draft.work_title = fallback_text(&draft.anchor_pack.theme_promise.value, &draft.level_name);
}
if draft.work_description.trim().is_empty() {
draft.work_description = draft.summary.clone();
}
if draft.levels.is_empty() {
draft.levels = vec![PuzzleDraftLevel {
level_id: "puzzle-level-1".to_string(),
level_name: draft.level_name.clone(),
picture_description: fallback_text(
&draft.anchor_pack.visual_subject.value,
&draft.summary,
),
candidates: draft.candidates.clone(),
selected_candidate_id: draft.selected_candidate_id.clone(),
cover_image_src: draft.cover_image_src.clone(),
cover_asset_id: draft.cover_asset_id.clone(),
generation_status: draft.generation_status.clone(),
}];
}
sync_primary_level_fields(&mut draft);
draft
}
pub fn sync_primary_level_fields(draft: &mut PuzzleResultDraft) {
if let Some(primary_level) = draft.levels.first() {
draft.level_name = primary_level.level_name.clone();
draft.candidates = primary_level.candidates.clone();
draft.selected_candidate_id = primary_level.selected_candidate_id.clone();
draft.cover_image_src = primary_level.cover_image_src.clone();
draft.cover_asset_id = primary_level.cover_asset_id.clone();
draft.generation_status = primary_level.generation_status.clone();
}
if draft.work_description.trim().is_empty() {
draft.work_description = draft.summary.clone();
}
draft.summary = draft.work_description.clone();
if draft.form_draft.is_some() {
draft.form_draft = Some(PuzzleFormDraft {
work_title: normalize_required_string(&draft.work_title),
work_description: normalize_required_string(&draft.work_description),
picture_description: draft
.levels
.first()
.and_then(|level| normalize_required_string(&level.picture_description)),
});
}
}
pub fn selected_puzzle_level(
draft: &PuzzleResultDraft,
level_id: Option<&str>,
) -> Option<PuzzleDraftLevel> {
let normalized = normalize_puzzle_draft(draft.clone());
let requested_level_id = level_id.and_then(normalize_required_string);
requested_level_id
.as_deref()
.and_then(|target_id| {
normalized
.levels
.iter()
.find(|level| level.level_id == target_id)
.cloned()
})
.or_else(|| normalized.levels.first().cloned())
}
pub fn replace_puzzle_level(
draft: &PuzzleResultDraft,
level: PuzzleDraftLevel,
) -> Result<PuzzleResultDraft, PuzzleFieldError> {
let mut next_draft = normalize_puzzle_draft(draft.clone());
let Some(index) = next_draft
.levels
.iter()
.position(|entry| entry.level_id == level.level_id)
else {
return Err(PuzzleFieldError::InvalidOperation);
};
next_draft.levels[index] = level;
sync_primary_level_fields(&mut next_draft);
Ok(next_draft)
}
pub fn append_blank_puzzle_level(draft: &PuzzleResultDraft) -> PuzzleResultDraft {
let mut next_draft = normalize_puzzle_draft(draft.clone());
let next_index = next_draft.levels.len() + 1;
let picture_description = next_draft
.levels
.first()
.map(|level| level.picture_description.clone())
.filter(|value| !value.trim().is_empty())
.unwrap_or_else(|| fallback_text(&next_draft.anchor_pack.visual_subject.value, "画面主体"));
next_draft.levels.push(PuzzleDraftLevel {
level_id: format!("puzzle-level-{next_index}"),
level_name: build_level_name_from_picture(
picture_description.as_str(),
&next_draft.theme_tags,
next_index,
),
picture_description,
candidates: Vec::new(),
selected_candidate_id: None,
cover_image_src: None,
cover_asset_id: None,
generation_status: "idle".to_string(),
});
sync_primary_level_fields(&mut next_draft);
next_draft
}
pub fn remove_puzzle_level(
draft: &PuzzleResultDraft,
level_id: &str,
) -> Result<PuzzleResultDraft, PuzzleFieldError> {
let mut next_draft = normalize_puzzle_draft(draft.clone());
if next_draft.levels.len() <= 1 {
return Err(PuzzleFieldError::InvalidOperation);
}
let normalized_level_id =
normalize_required_string(level_id).ok_or(PuzzleFieldError::InvalidOperation)?;
next_draft
.levels
.retain(|level| level.level_id != normalized_level_id);
if next_draft.levels.is_empty() {
return Err(PuzzleFieldError::InvalidOperation);
}
sync_primary_level_fields(&mut next_draft);
Ok(next_draft)
}
pub fn build_result_preview(
draft: &PuzzleResultDraft,
author_display_name: Option<&str>,
) -> PuzzleResultPreviewEnvelope {
let blockers = validate_publish_requirements(draft, author_display_name);
let normalized_draft = normalize_puzzle_draft(draft.clone());
if normalized_draft.form_draft.is_some() {
return PuzzleResultPreviewEnvelope {
draft: normalized_draft,
blockers: Vec::new(),
quality_findings: Vec::new(),
publish_ready: false,
};
}
let blockers = validate_publish_requirements(&normalized_draft, author_display_name);
PuzzleResultPreviewEnvelope {
draft: draft.clone(),
draft: normalized_draft,
blockers,
quality_findings: Vec::new(),
publish_ready: validate_publish_requirements(draft, author_display_name).is_empty(),
@@ -866,27 +1177,44 @@ pub fn validate_publish_requirements(
draft: &PuzzleResultDraft,
author_display_name: Option<&str>,
) -> Vec<PuzzleResultPreviewBlocker> {
let draft = normalize_puzzle_draft(draft.clone());
let mut blockers = Vec::new();
if normalize_required_string(&draft.level_name).is_none() {
if normalize_required_string(&draft.work_title).is_none() {
blockers.push(PuzzleResultPreviewBlocker {
id: "missing-level-name".to_string(),
code: "MISSING_LEVEL_NAME".to_string(),
message: "关卡名不能为空".to_string(),
id: "missing-work-title".to_string(),
code: "MISSING_WORK_TITLE".to_string(),
message: "作品名称不能为空".to_string(),
});
}
if draft
.cover_image_src
.as_deref()
.map(str::trim)
.unwrap_or("")
.is_empty()
{
if normalize_required_string(&draft.work_description).is_none() {
blockers.push(PuzzleResultPreviewBlocker {
id: "missing-cover-image".to_string(),
code: "MISSING_COVER_IMAGE".to_string(),
message: "正式拼图图片尚未确定".to_string(),
id: "missing-work-description".to_string(),
code: "MISSING_WORK_DESCRIPTION".to_string(),
message: "作品描述不能为空".to_string(),
});
}
for level in &draft.levels {
if normalize_required_string(&level.level_name).is_none() {
blockers.push(PuzzleResultPreviewBlocker {
id: format!("missing-level-name-{}", level.level_id),
code: "MISSING_LEVEL_NAME".to_string(),
message: "关卡名不能为空".to_string(),
});
}
if level
.cover_image_src
.as_deref()
.map(str::trim)
.unwrap_or("")
.is_empty()
{
blockers.push(PuzzleResultPreviewBlocker {
id: format!("missing-cover-image-{}", level.level_id),
code: "MISSING_COVER_IMAGE".to_string(),
message: "正式拼图图片尚未确定".to_string(),
});
}
}
if draft.theme_tags.len() < PUZZLE_MIN_TAG_COUNT
|| draft.theme_tags.len() > PUZZLE_MAX_TAG_COUNT
{
@@ -917,18 +1245,22 @@ pub fn create_work_profile(
) -> Result<PuzzleWorkProfile, PuzzleFieldError> {
let author_display_name = normalize_required_string(author_display_name)
.ok_or(PuzzleFieldError::MissingAuthorDisplayName)?;
let preview = build_result_preview(draft, Some(&author_display_name));
let draft = normalize_puzzle_draft(draft.clone());
let preview = build_result_preview(&draft, Some(&author_display_name));
Ok(PuzzleWorkProfile {
work_id,
profile_id,
owner_user_id,
source_session_id,
author_display_name,
work_title: draft.work_title.clone(),
work_description: draft.work_description.clone(),
level_name: draft.level_name.clone(),
summary: draft.summary.clone(),
theme_tags: normalize_theme_tags(draft.theme_tags.clone()),
cover_image_src: draft.cover_image_src.clone(),
cover_asset_id: draft.cover_asset_id.clone(),
levels: draft.levels.clone(),
publication_status: PuzzlePublicationStatus::Draft,
updated_at_micros,
published_at_micros: None,
@@ -946,14 +1278,18 @@ pub fn publish_work_profile(
draft: &PuzzleResultDraft,
published_at_micros: i64,
) -> Result<PuzzleWorkProfile, PuzzleFieldError> {
if !validate_publish_requirements(draft, Some(&profile.author_display_name)).is_empty() {
let draft = normalize_puzzle_draft(draft.clone());
if !validate_publish_requirements(&draft, Some(&profile.author_display_name)).is_empty() {
return Err(PuzzleFieldError::InvalidOperation);
}
profile.work_title = draft.work_title.clone();
profile.work_description = draft.work_description.clone();
profile.level_name = draft.level_name.clone();
profile.summary = draft.summary.clone();
profile.theme_tags = normalize_theme_tags(draft.theme_tags.clone());
profile.cover_image_src = draft.cover_image_src.clone();
profile.cover_asset_id = draft.cover_asset_id.clone();
profile.levels = draft.levels.clone();
profile.publication_status = PuzzlePublicationStatus::Published;
profile.publish_ready = true;
profile.updated_at_micros = published_at_micros;
@@ -965,22 +1301,39 @@ pub fn publish_work_profile(
/// 这里只允许覆盖 PRD 明确要求的关卡名、摘要与标签,不额外扩到更多结果页元数据。
pub fn apply_publish_overrides_to_draft(
draft: &PuzzleResultDraft,
work_title: Option<String>,
work_description: Option<String>,
level_name: Option<String>,
summary: Option<String>,
theme_tags: Option<Vec<String>>,
levels: Option<Vec<PuzzleDraftLevel>>,
) -> Result<PuzzleResultDraft, PuzzleFieldError> {
let mut next_draft = draft.clone();
let mut next_draft = normalize_puzzle_draft(draft.clone());
if let Some(next_work_title) = work_title
&& let Some(normalized_work_title) = normalize_required_string(&next_work_title)
{
next_draft.work_title = normalized_work_title;
}
if let Some(next_work_description) = work_description
&& let Some(normalized_work_description) = normalize_required_string(&next_work_description)
{
next_draft.work_description = normalized_work_description;
}
if let Some(next_level_name) = level_name
&& let Some(normalized_level_name) = normalize_required_string(&next_level_name)
{
next_draft.level_name = normalized_level_name;
if let Some(primary_level) = next_draft.levels.first_mut() {
primary_level.level_name = normalized_level_name;
}
}
if let Some(next_summary) = summary
&& let Some(normalized_summary) = normalize_required_string(&next_summary)
{
next_draft.summary = normalized_summary;
next_draft.work_description = normalized_summary;
}
if let Some(next_theme_tags) = theme_tags {
@@ -993,9 +1346,41 @@ pub fn apply_publish_overrides_to_draft(
next_draft.theme_tags = normalized_theme_tags;
}
if let Some(next_levels) = levels {
let normalized_levels = normalize_puzzle_levels(next_levels, &next_draft.theme_tags)?;
next_draft.levels = normalized_levels;
}
sync_primary_level_fields(&mut next_draft);
Ok(next_draft)
}
pub fn normalize_puzzle_levels(
levels: Vec<PuzzleDraftLevel>,
theme_tags: &[String],
) -> Result<Vec<PuzzleDraftLevel>, PuzzleFieldError> {
let mut normalized_levels = Vec::new();
for (index, mut level) in levels.into_iter().enumerate() {
let level_id = normalize_required_string(&level.level_id)
.unwrap_or_else(|| format!("puzzle-level-{}", index + 1));
let picture_description = normalize_required_string(&level.picture_description)
.unwrap_or_else(|| format!("{}关画面", index + 1));
let level_name = normalize_required_string(&level.level_name).unwrap_or_else(|| {
build_level_name_from_picture(picture_description.as_str(), theme_tags, index + 1)
});
level.level_id = level_id;
level.level_name = level_name;
level.picture_description = picture_description;
level.generation_status = normalize_required_string(&level.generation_status)
.unwrap_or_else(|| "idle".to_string());
normalized_levels.push(level);
}
if normalized_levels.is_empty() {
return Err(PuzzleFieldError::InvalidOperation);
}
Ok(normalized_levels)
}
pub fn resolve_puzzle_grid_size(cleared_level_count: u32) -> u32 {
if cleared_level_count >= 3 { 4 } else { 3 }
}
@@ -1683,25 +2068,54 @@ fn infer_tags_and_forbidden(source: &str) -> String {
}
}
fn parse_form_seed_text(source: &str) -> Option<(String, String)> {
#[derive(Clone, Debug, Default, PartialEq, Eq)]
struct PuzzleFormSeedParts {
work_title: Option<String>,
work_description: Option<String>,
picture_description: Option<String>,
}
impl PuzzleFormSeedParts {
fn has_any_value(&self) -> bool {
self.work_title.is_some()
|| self.work_description.is_some()
|| self.picture_description.is_some()
}
}
fn parse_form_seed_text(source: &str) -> Option<PuzzleFormSeedParts> {
let normalized_source = source.trim();
let title_marker = "拼图标题:";
let description_marker = "画面描述:";
let title_start = normalized_source.find(title_marker)? + title_marker.len();
let description_start = normalized_source.find(description_marker)?;
if description_start <= title_start {
if normalized_source.is_empty() {
return None;
}
let title = normalize_required_string(&normalized_source[title_start..description_start]);
let picture_description = normalize_required_string(
&normalized_source[description_start + description_marker.len()..],
);
let title_marker = if normalized_source.contains("作品名称:") {
"作品名称:"
} else {
"拼图标题:"
};
let parts = PuzzleFormSeedParts {
work_title: extract_form_seed_value(normalized_source, title_marker),
work_description: extract_form_seed_value(normalized_source, "作品描述:"),
picture_description: extract_form_seed_value(normalized_source, "画面描述:"),
};
match (title, picture_description) {
(Some(title), Some(picture_description)) => Some((title, picture_description)),
_ => None,
}
parts.has_any_value().then_some(parts)
}
fn extract_form_seed_value(source: &str, marker: &str) -> Option<String> {
let value_start = source.find(marker)? + marker.len();
let value_end = ["作品名称:", "拼图标题:", "作品描述:", "画面描述:"]
.into_iter()
.filter(|next_marker| *next_marker != marker)
.filter_map(|next_marker| {
source[value_start..]
.find(next_marker)
.map(|index| value_start + index)
})
.min()
.unwrap_or(source.len());
normalize_required_string(&source[value_start..value_end])
}
fn build_form_tags_and_forbidden(title: &str, picture_description: &str) -> String {
@@ -1777,6 +2191,22 @@ fn build_result_summary(anchor_pack: &PuzzleAnchorPack) -> String {
)
}
fn resolve_work_description(seed_text: Option<&str>, anchor_pack: &PuzzleAnchorPack) -> String {
seed_text
.and_then(parse_form_seed_text)
.and_then(|parts| {
parts
.work_description
.or(parts.picture_description)
.or(parts.work_title)
})
.unwrap_or_else(|| build_result_summary(anchor_pack))
}
fn build_work_title(anchor_pack: &PuzzleAnchorPack) -> String {
fallback_text(&anchor_pack.theme_promise.value, "奇景拼图")
}
fn extract_forbidden_directive(source: &str) -> String {
if let Some((_, tail)) = source.split_once('') {
return normalize_required_string(tail).unwrap_or_else(|| "禁止标题字".to_string());
@@ -1784,20 +2214,24 @@ fn extract_forbidden_directive(source: &str) -> String {
"禁止标题字".to_string()
}
fn build_level_name(anchor_pack: &PuzzleAnchorPack, normalized_tags: &[String]) -> String {
if is_form_anchor_pack(anchor_pack)
&& let Some(title) = normalize_required_string(&anchor_pack.theme_promise.value)
{
return title;
fn build_level_name_from_picture(
picture_description: &str,
normalized_tags: &[String],
level_index: usize,
) -> String {
let source = normalize_required_string(picture_description).unwrap_or_default();
for keyword in [
"", "", "神庙", "遗迹", "森林", "雨夜", "城市", "机械", "", "", "", "", "",
"",
] {
if source.contains(keyword) {
return format!("{keyword}画面");
}
}
if let Some(tag) = normalized_tags.first() {
return format!("{tag}拼图");
return format!("{tag}{level_index}");
}
if let Some(subject) = normalize_required_string(&anchor_pack.visual_subject.value) {
return subject.chars().take(8).collect::<String>();
}
"奇景拼图".to_string()
format!("{level_index}")
}
fn fallback_text(value: &str, fallback: &str) -> String {
@@ -2432,15 +2866,28 @@ mod tests {
owner_user_id: owner_user_id.to_string(),
source_session_id: None,
author_display_name: "作者".to_string(),
work_title: format!("{profile_id} 作品"),
work_description: "summary".to_string(),
level_name: format!("{profile_id} 关"),
summary: "summary".to_string(),
theme_tags: tags.into_iter().map(|value| value.to_string()).collect(),
cover_image_src: Some("/cover.png".to_string()),
cover_asset_id: Some("asset-1".to_string()),
levels: vec![PuzzleDraftLevel {
level_id: "puzzle-level-1".to_string(),
level_name: format!("{profile_id} 关"),
picture_description: "summary".to_string(),
candidates: Vec::new(),
selected_candidate_id: None,
cover_image_src: Some("/cover.png".to_string()),
cover_asset_id: Some("asset-1".to_string()),
generation_status: "ready".to_string(),
}],
publication_status: PuzzlePublicationStatus::Published,
updated_at_micros: 100,
published_at_micros: Some(100),
play_count: 0,
recent_play_count_7d: 0,
remix_count: 0,
like_count: 0,
publish_ready: true,
@@ -2488,10 +2935,16 @@ mod tests {
#[test]
fn form_seed_locks_title_and_picture_description_as_primary_anchors() {
let anchor_pack = infer_anchor_pack(
"拼图标题:暖灯猫街\n画面描述:一只猫在雨夜灯牌下回头。",
"作品名称:暖灯猫街\n作品描述:一套雨夜猫街主题拼图。\n画面描述:一只猫在雨夜灯牌下回头。",
None,
);
let draft = compile_result_draft(&anchor_pack, &[]);
let draft = compile_result_draft_from_seed(
&anchor_pack,
&[],
Some(
"作品名称:暖灯猫街\n作品描述:一套雨夜猫街主题拼图。\n画面描述:一只猫在雨夜灯牌下回头。",
),
);
assert_eq!(anchor_pack.theme_promise.value, "暖灯猫街");
assert_eq!(anchor_pack.theme_promise.status, PuzzleAnchorStatus::Locked);
@@ -2500,8 +2953,14 @@ mod tests {
anchor_pack.visual_subject.status,
PuzzleAnchorStatus::Locked
);
assert_eq!(draft.level_name, "暖灯猫街");
assert_eq!(draft.summary, "只猫在雨夜灯牌下回头");
assert_eq!(draft.work_title, "暖灯猫街");
assert_eq!(draft.work_description, "套雨夜猫街主题拼图");
assert_eq!(draft.summary, "一套雨夜猫街主题拼图。");
assert_eq!(draft.level_name, "猫画面");
assert_eq!(
draft.levels[0].picture_description,
"一只猫在雨夜灯牌下回头。"
);
assert_eq!(
draft
.creator_intent
@@ -2525,9 +2984,10 @@ mod tests {
"一只猫在雨夜灯牌下回头。\n远处有暖色霓虹和玻璃雨滴。"
);
assert_eq!(
draft.summary,
draft.levels[0].picture_description,
"一只猫在雨夜灯牌下回头。\n远处有暖色霓虹和玻璃雨滴。"
);
assert_eq!(draft.summary, draft.work_description);
assert!(draft.theme_tags.iter().any(|tag| tag == "猫咪"));
assert!(draft.theme_tags.iter().any(|tag| tag == "雨夜"));
}
@@ -2909,6 +3369,8 @@ mod tests {
let updated = apply_publish_overrides_to_draft(
&draft,
Some("雨夜猫塔作品".to_string()),
Some("作品描述。".to_string()),
Some("雨夜猫塔".to_string()),
Some("一张更聚焦猫咪塔楼的夜景拼图。".to_string()),
Some(vec![
@@ -2916,10 +3378,12 @@ mod tests {
"猫咪".to_string(),
"遗迹".to_string(),
]),
None,
)
.expect("publish overrides should succeed");
assert_eq!(updated.level_name, "雨夜猫塔");
assert_eq!(updated.work_title, "雨夜猫塔作品");
assert_eq!(updated.summary, "一张更聚焦猫咪塔楼的夜景拼图。");
assert_eq!(
updated.theme_tags,
@@ -2935,9 +3399,16 @@ mod tests {
fn apply_publish_overrides_rejects_invalid_tag_count() {
let anchor_pack = infer_anchor_pack("蒸汽城市", Some("蒸汽城市"));
let draft = compile_result_draft(&anchor_pack, &[]);
let error =
apply_publish_overrides_to_draft(&draft, None, None, Some(vec!["蒸汽".to_string()]))
.expect_err("invalid tag count should fail");
let error = apply_publish_overrides_to_draft(
&draft,
None,
None,
None,
None,
Some(vec!["蒸汽".to_string()]),
None,
)
.expect_err("invalid tag count should fail");
assert_eq!(error, PuzzleFieldError::InvalidTagCount);
}